Published
Edited
Jun 4, 2022
Importers
Insert cell
Insert cell
Insert cell
Universe({
satellites: [[`satellite`, 0.2, 0.5, 10e3]],
plans: [[0, `satellite`, 10, 0]],
aspect: 6,
total_time_seconds: 4
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function Universe({
satellites,
plans: plans_all,
aspect,
total_time_seconds
}) {
const engine = Matter.Engine.create({
gravity: { x: 0, y: 0 },
positionIterations: 10,
velocityIterations: 10
});
Matter.Events.on(engine.world, "afterAdd", () => {
engine.world.bodies = d3.sort(engine.world.bodies, (d) => d?.plugin?.order);
});
add_bodies(engine, { plans_all, satellites, aspect });
const frames = get_frames({ engine, total_time_seconds });
const scrubber = Scrubber(frames, {
autoplay: false,
format: (d) => d3.format(`,`)(Math.round(d?.timestamp))
});
const container = html`<div class="universe"></div>`;
const div = html`${scrubber}${container}`;
let current_frame = scrubber.value;
const update = () => {
container.innerHTML = ``;
container.appendChild(render({ frames, current_frame, width, aspect }));
};
scrubber.oninput = () => {
current_frame = scrubber.value;
update();
};
update();
return div;
}
Insert cell
function add_bodies(
engine,
{ satellites = [], planets = [], plans_all = [], aspect } = {}
) {
const { world } = engine;

const world_height = world_width / aspect;

planets.forEach(([label, planet_x, planet_y, mass, radius = 5]) => {
const planet = Matter.Bodies.circle(
planet_x * world_width,
planet_y * world_height,
radius,
{
label: `planet`,
mass,
frictionAir: 0,
isStatic: true,
plugin: { gravity: true },
render: { style: `opacity: 0.3` },
collisionFilter: { mask: 0b0 }
}
);
Matter.Composite.add(world, planet);
});

satellites.forEach(([label, satellite_x, satellite_y, mass]) => {
const satellite = Matter.Bodies.circle(
satellite_x * world_width,
satellite_y * world_height,
world_width * 0.01,
{
label,
mass,
friction: 0,
frictionAir: 0,
restitution: 0,
plugin: {
showInfo: true,
beforeUpdate: () => {
apply_plan(satellite, engine, plans_all);
apply_gravity(satellite, engine);
}
}
}
);
Matter.Composite.add(world, satellite);
});
}
Insert cell
Insert cell
function render({ frames, current_frame, aspect }) {
const flight_path = d3
.line()
.x((d) => d?.position?.x?.toFixed(2) ?? 0)
.y((d) => d?.position?.y?.toFixed(2) ?? 0)
.defined((d) => in_bounds(d))(
frames.map((d) => d.bodies.find((d) => d.label === `satellite`))
);

const svg_style = {
display: `block`,
width: `${width}px`,
aspectRatio: `${aspect} / 1`,
backgroundColor: `black`,
fontFamily: `monospace`
};

const flight_path_style = {
fill: `none`,
stroke: `white`,
vectorEffect: `non-scaling-stroke`,
strokeDasharray: `10 10`
};

return html`<svg
style=${svg_style}
viewBox="0 0 ${world_width} ${world_width / aspect}"
>
<path d=${flight_path} stroke="white" style=${flight_path_style} />
${get_rendered_bodies(current_frame)}
${get_rendered_info(current_frame, { aspect })}
</svg>`;
}
Insert cell
function get_rendered_bodies(current_frame) {
const rendered = [];
const render = (body) => {
if (!body) return;
const { position, label, render, circleRadius } = body;
if (!in_bounds(body)) return;
const style = `
fill: ${default_fill};
stroke: ${default_stroke};
${render?.style ?? ``};
`;
const circle = htl.svg`
<circle cx=${position.x} cy=${position.y} r=${circleRadius} style=${style} />
`;
rendered.push(circle);
};
current_frame.bodies.forEach(render);
return rendered;
}
Insert cell
function get_rendered_info(current_frame, { font_size = 1.5 } = {}) {
const size = font_size;

return current_frame.bodies
.filter((d) => d.plugin?.showInfo)
.map((body) => {
const { speed, position, velocity, force } = body;
const fmt = d3.format(`.3f`);
const texts = [
`SPEED: ${fmt(speed)}`,
`VX: ${fmt(velocity.x)}`,
`VY: ${fmt(velocity.y)}`,
`X: ${fmt(position.x)}`
].map((text, index) => {
return htl.svg`<text
x=${position.x}
y=${position.y}
dx=${size}
dy=${-size * (index + 1)}
style="
font-family: monospace;
fill: white;
font-size: ${size}px;
"
>
${text}
</text>`;
});
return htl.svg`${texts}`;
});
}
Insert cell
function in_bounds(body) {
return (
body?.position?.x > 0 &&
body?.position?.y > 0 &&
body?.position?.x < 100 &&
body?.position?.y < 100
);
}
Insert cell
Insert cell
function get_frames({ engine, total_time_seconds = 60 }) {
const num_frames = Math.ceil((total_time_seconds * 1000) / ms_per_frame);
const { world } = engine;
const before_update = () => {
Matter.Composite.allBodies(world).forEach((body) => {
body.plugin?.beforeUpdate?.(engine);
});
};
const out = [];
for (const frame_index of d3.range(num_frames)) {
const bodies = Matter.Composite.allBodies(world).map((body) => {
return {
...body,
// Vectors must be cloned!
position: { ...body.position },
velocity: { ...body.velocity },
force: { ...body.force }
};
});
const frame = {
bodies,
timestamp: engine.timing.timestamp,
index: frame_index
};
out.push(frame);
before_update();
Matter.Engine.update(engine, ms_per_frame, 1);
}
return out;
}
Insert cell
Insert cell
function apply_plan(body, engine, plans_all = []) {
const {
world,
timing: { timestamp }
} = engine;
const next_timestamp = timestamp + ms_per_frame;
// Find all plans between timestamp and timestamp+lastDelta
const plans = plans_all
.map(([timestamp, label, magnitude, angle]) => ({
timestamp,
label,
magnitude,
angle
}))
.filter((d) => d.timestamp >= timestamp && d.timestamp <= next_timestamp);
for (const plan_ of plans) {
const { angle, magnitude } = plan_;
const force = vector_from_angle(angle, magnitude);
const flame_mass = body.mass * 0.5;
const flame = Matter.Bodies.circle(
body.position.x,
body.position.y,
body.circleRadius * 5,
{
label: `flame-${Math.floor(Math.random())}`,
plugin: {
order: -1,

beforeUpdate: () => {
const flame_shrink_percent = 0.98;
Matter.Body.scale(
flame,
flame_shrink_percent,
flame_shrink_percent
);
const newArea = flame.area;
if (newArea < 0.1) {
Matter.Composite.remove(world, flame);
}
}
},
render: {
style: `fill: orange; stroke: none;`
},
collisionFilter: { mask: 0b0 },
mass: flame_mass,
friction: 0,
frictionAir: 0,
restitution: 0
}
);
Matter.Body.applyForce(body, body.position, force);
Matter.Body.applyForce(flame, flame.position, Matter.Vector.neg(force));
Matter.Composite.add(world, flame);
}
body.plugin.plans = plans;
}
Insert cell
Insert cell
function apply_gravity(satellite, engine) {
const { world } = engine;
Matter.Composite.allBodies(world)
.filter((d) => d !== satellite)
.filter((d) => d.plugin.gravity)
.forEach((planet) => {
gravity_force(planet, satellite);
});
}
Insert cell
Insert cell
function gravity_force(planet, satellite) {
const { mass: planet_mass } = planet;
const { mass: satellite_mass } = satellite;
const distance_vector = Matter.Vector.sub(
planet.position,
satellite.position
);
let distance = Matter.Vector.magnitude(distance_vector);
const min_distance = 1;
if (distance < min_distance) distance = min_distance;
const G = gravitational_constant;
const magnitude = G * planet_mass * distance ** -2;
// Apply the force using the distance vector
const normalized = Matter.Vector.normalise(distance_vector);
const force = Matter.Vector.mult(normalized, magnitude);
Matter.Body.applyForce(satellite, satellite.position, force);
}
Insert cell
function vector_from_angle(angle, magnitude) {
const radians = angle * Math.PI * 180 ** -1;
const vector = Matter.Vector.create(magnitude, 0);
return Matter.Vector.rotate(vector, radians);
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more