Public
Edited
Mar 3, 2024
23 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
const height = 300;
const onUpdate = (mover, {}) => {
mover.applyForce(new THREE.Vector2(0, -0.01));

mover.dt = mover.dt || 0;
mover.dt += 0.01;
mover.applyForce(new THREE.Vector2(simplex.noise2D(mover.dt, 1) / 100, 0));

if (mover.y <= mover.radius + 5 && mover.velocity.y < 0) {
const f = mover.velocity
.clone()
.negate()
.multiply({ x: 0, y: 1.9 });
mover.applyForce(f);
}
};
return renderMovers(
{ visibility, width, height },
new Mover({
x: width / 2,
y: height - 30,
mass: 1,
onUpdate
})
);
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
const height = 300;
const massRng = d3.randomUniform.source(random)(0.1, 5);
const movers = Array.from(
{ length: 100 },
() =>
new Mover({
x: 50,
y: 50,
mass: massRng(),
onUpdate: (mover) => {
const xNorm = mover.x / width;
const yNorm = mover.y / height;
const edgeForce = new THREE.Vector2(0.5 - xNorm, 0.5 - yNorm);
mover.applyForce(edgeForce);
}
})
);
return renderMovers(
{
width,
height,
visibility
},
...movers
);
}
Insert cell
Insert cell
gravityForce = (mass = 1, g = 0.1) => new THREE.Vector2(0, g * mass)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
frictionForce = (v, c = 0.01) =>
v
.clone()
.normalize()
.negate()
.multiplyScalar(c)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
const height = 300;
const wind = new THREE.Vector2(0.01, 0);
const massRng = d3.randomUniform.source(random)(0.1, 5);
const frictionArea1 = {
x: ex_2_4_friction_area_pos_1,
y: 0,
width: 200,
height
};
const frictionArea2 = {
x: ex_2_4_friction_area_pos_2,
y: 0,
width: 300,
height
};
const overlapsArea = (mover, area) => {
if (
mover.x + mover.radius > area.x &&
mover.x - mover.radius < area.x + area.width &&
mover.y + mover.radius > area.y &&
mover.y - mover.radius < area.y + area.height
) {
return true;
}
return false;
};
const movers = Array.from(
{ length: 50 },
() =>
new Mover({
x: random() * width,
y: random() * 100,
mass: massRng(),
onUpdate: (mover) => {
applyGravity(mover);

if (overlapsArea(mover, frictionArea1)) {
applyFriction(mover, ex_2_4_friction_coeffictient_1);
}
if (overlapsArea(mover, frictionArea2)) {
applyFriction(mover, ex_2_4_friction_coeffictient_2);
}
}
})
);
const colorBySign = (val) =>
val < 0 ? "#7EB3FF66" : val > 0 ? "#F85C5066" : "#D7E1E966";
const drawBkg = (ctx, width, height) => {
ctx.fillStyle = colorBySign(ex_2_4_friction_coeffictient_1);
ctx.fillRect(
frictionArea1.x,
frictionArea1.y,
frictionArea1.width,
frictionArea1.height
);
ctx.fillStyle = colorBySign(ex_2_4_friction_coeffictient_2);
ctx.fillRect(
frictionArea2.x,
frictionArea2.y,
frictionArea2.width,
frictionArea2.height
);
};
return renderMovers(
{
width,
height,
visibility,
overflow: bounceMover({ width, height }),
forces: [wind],
drawBkg
},
...movers
);
}
Insert cell
Insert cell
dragForce = (v, c = 0.1, a = 1) =>
v
.clone()
.normalize()
.negate()
.multiplyScalar(a * c * Math.pow(v.length(), 2))
Insert cell
Insert cell
applyDrag = (mover, liquid) =>
mover.applyForce(dragForce(mover.velocity, liquid.c, mover.surfaceArea))
Insert cell
Insert cell
Insert cell
Insert cell
{
const height = 500;
const liquid = new Liquid({
x: 0,
y: height - 150,
width,
height: 150,
c: 0.1
});
const movers = Array.from(
{ length: 50 },
() =>
new Mover({
x: random() * width,
y: random() * 300,
mass: 2,
onUpdate: (mover) => {
applyGravity(mover);
}
})
);
return renderMovers(
{
width,
height,
visibility,
overflow: bounceMover({ width, height }),
liquids: [liquid]
},
...movers
);
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
const height = 500;
const liquid = new Liquid({
x: 0,
y: height - 150,
width,
height: 150,
c: 0.05
});
const movers = Array.from(
{ length: 50 },
() =>
new SquareMover({
x: random() * width,
y: random() * 300
})
);
return renderMovers(
{
width,
height,
visibility,
overflow: limitMover({ width, height }),
liquids: [liquid]
},
...movers
);
}
Insert cell
Insert cell
Insert cell
// TODO
Insert cell
Insert cell
gravitationalAttractionForce = (
mover,
attractor,
{ G = 1, minDist = 5, maxDist = 1000 } = {}
) => {
const posAttr = attractor.location || attractor.position;
const posMover = mover.location || mover.position;
const r = posAttr.clone().sub(posMover);
const rLen = clamp(r.length(), minDist, maxDist); // apply some constraints to not blow sim off

return r
.normalize()
.multiplyScalar((G * mover.mass * attractor.mass) / (rLen * rLen));
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
const target = DOM.element('div');
functionPlot({
target,
data: [{ fn: "log(x/2)" }]
});
return target;
}
Insert cell
{
const height = 500;
const attractor = new Attractor({
x: width / 2,
y: height / 2,
G: 1,
maxDist: 25
});

attractor.attractionForce = function(mover) {
const v = new THREE.Vector2().subVectors(this.location, mover.location);
const len = v.length();
return v
.normalize()
.multiplyScalar((mover.mass / this.mass) * Math.log(len / mover.mass));
};

return renderMovers(
{
width,
height,
visibility,
trace: true,
overflow: () => {},
attractors: [attractor]
},
...Array.from({ length: 50 }, () =>
new Mover({
x: random() * width,
y: random() * height,
mass: 0.1 + 2 * random()
}).applyForce(new THREE.Vector2(random(), random()))
)
);
}
Insert cell
Insert cell
{
const height = 500;

return renderMovers(
{
width,
height,
visibility,
trace: true,
overflow: () => {}
},
...Array.from(
{ length: 50 },
() =>
new AttractingMover({
x: random() * width,
y: random() * height,
mass: 0.1 + 2 * random()
})
)
);
}
Insert cell
Insert cell
{
const height = 500;

return renderMovers(
{
width,
height,
visibility,
trace: true,
overflow: () => {}
},
new AttractingMover({
x: width / 2,
y: height / 2,
mass: 10,
G: 2,
onUpdate: function (self, { mouse }) {
this.acceleration.setScalar(0);
if (mouse.down && mouse.pos) {
this.location = mouse.pos;
}
}
}),
...Array.from(
{ length: 50 },
() =>
new AttractingMover({
x: random() * width,
y: random() * height,
mass: 0.1 + 2 * random(),
G: -0.5
})
)
);
}
Insert cell
Insert cell
Insert cell
class Mover {
constructor({ x = 0, y = 0, vx = 0, vy = 0, mass = 1, r, onUpdate } = {}) {
this.location = new THREE.Vector2(x, y);
this.velocity = new THREE.Vector2(vx, vy);
this.acceleration = new THREE.Vector2(0, 0);
this.mass = mass;
this.radius = r || mass * 10;

this.onUpdate = onUpdate || (() => {});
}

applyForce(force) {
const f = force.clone();
f.divideScalar(this.mass);
this.acceleration.add(f);
return this;
}

update(opts) {
this.onUpdate(this, opts);

this.velocity.add(this.acceleration);
this.location.add(this.velocity);
this.acceleration.setScalar(0);
}

display(ctx) {
ctx.strokeStyle = '#000';
ctx.lineWidth = 1;
ctx.fillStyle = '#faaa';
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, 2 * PI);
ctx.fill();
ctx.stroke();
ctx.closePath();
}

get x() {
return this.location.x;
}
set x(xx) {
this.location.x = xx;
}
get y() {
return this.location.y;
}
set y(yy) {
this.location.y = yy;
}

get bbox() {
return {
minX: -this.radius,
minY: -this.radius,
maxX: this.radius,
maxY: this.radius
};
}

contains(obj) {
const pos1 = this.location || this.position;
// const pos2 = obj.location || obj.position || obj;
return pos1.distanceTo(obj) <= this.radius;
}
}
Insert cell
class AttractingMover extends Mover {
constructor({ G = 1, minDist = 1, maxDist = 25, ...baseOpts }) {
super(baseOpts);
this.G = G;
this.minDist = minDist;
this.maxDist = maxDist;
}

attractionForce(target) {
return gravitationalAttractionForce(target, this, {
G: this.G,
minDist: this.minDist,
maxDist: this.maxDist
});
}
}
Insert cell
Insert cell
class Liquid {
constructor({
x = 0,
y = 0,
width = 100,
height = 100,
c = 0.1,
color = '#0033ffaa'
}) {
this.location = new THREE.Vector2(x, y);
this.width = width;
this.height = height;
this.c = c;
this.color = color;
}

contains(obj) {
return (
obj.x >= this.x &&
obj.x < this.x + this.width &&
obj.y >= this.y &&
obj.y < this.y + this.height
);
}

display(ctx) {
ctx.lineWidth = 0;
ctx.fillStyle = this.color;
ctx.fillRect(this.x, this.y, this.width, this.height);
}

get x() {
return this.location.x;
}
set x(xx) {
this.location.x = xx;
}
get y() {
return this.location.y;
}
set y(yy) {
this.location.y = yy;
}

get bbox() {
return {
minX: 0,
minY: 0,
maxX: this.width,
maxY: this.height
};
}
}
Insert cell
class Attractor {
constructor({
x = 0,
y = 0,
mass = 20,
G = 1,
minDist = 5,
maxDist = 100,
r
} = {}) {
this.location = new THREE.Vector2(x, y);
this.mass = mass;
this.G = G;
this.minDist = minDist;
this.maxDist = maxDist;
this.radius = r || mass * 2;
}

attractionForce(target) {
return gravitationalAttractionForce(target, this, {
G: this.G,
minDist: this.minDist,
maxDist: this.maxDist
});
}

contains(obj) {
const pos1 = this.location || this.position;
// const pos2 = obj.location || obj.position;
return pos1.distanceTo(obj) <= this.radius;
}

display(ctx) {
ctx.strokeStyle = '#000';
ctx.lineWidth = 1;
ctx.fillStyle = '#33ffaaaa';
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, 2 * PI);
ctx.fill();
ctx.stroke();
ctx.closePath();
}

get x() {
return this.location.x;
}
set x(xx) {
this.location.x = xx;
}
get y() {
return this.location.y;
}
set y(yy) {
this.location.y = yy;
}

get bbox() {
return {
minX: -this.radius,
minY: -this.radius,
maxX: this.radius,
maxY: this.radius
};
}
}
Insert cell
Insert cell
renderMovers = function*(
{
visibility,
width = 200,
height = 200,
speed = 1,
overflow,
trace = false,
forces = [],
liquids = [],
attractors = [],
drawBkg = (ctx, width, height) => {}
},
...movers
) {
const checkEdges = overflow || wrapMover({ width, height });
const ctx = DOM.context2d(width, height);
let trailsCtx;
const srcDim = [0, 0, width * devicePixelRatio, height * devicePixelRatio];
const destDim = [0, 0, width, height];
if (trace) {
trailsCtx = DOM.context2d(width, height);
trailsCtx.strokeStyle = '#00f';
trailsCtx.lineWidth = 1;
trailsCtx.globalAlpha = 0.2;
}

if (attractors.length === 0) {
attractors.push(...movers.filter(m => !!m.attractionForce));
}

const mouse = {
pos: null,
down: false,
shift: false
};

ctx.canvas.onmousemove = e => {
mouse.pos = new THREE.Vector2(e.offsetX, e.offsetY);
};
ctx.canvas.onmouseleave = e => {
mouse.pos = null;
mouse.down = false;
mouse.shift = false;
};
ctx.canvas.onmousedown = e => {
mouse.down = true;
mouse.shift = e.shiftKey;
};
ctx.canvas.onmouseup = e => {
mouse.down = false;
mouse.shift = false;
};

const update = mover => {
const start = [mover.x, mover.y];

forces.forEach(force => mover.applyForce(force));

liquids.forEach(
liquid => liquid.contains(mover) && applyDrag(mover, liquid)
);

attractors.forEach(attractor => {
if (attractor !== mover) {
mover.applyForce(attractor.attractionForce(mover));
}
});

mover.update({
mouse
});

const end = [mover.x, mover.y];

checkEdges(mover);

if (trace) {
trailsCtx.beginPath();
trailsCtx.moveTo(...start);
trailsCtx.lineTo(...end);
trailsCtx.stroke();
trailsCtx.closePath();
}
};

while (true) {
// update
repeat(() => movers.forEach(update), speed);

// draw
ctx.clearRect(0, 0, width, height);
drawBkg(ctx, width, height);

liquids.forEach(liquid => liquid.display(ctx));
attractors
.filter(attractor => !movers.includes(attractor))
.forEach(attractor => attractor.display(ctx));

if (trace) {
ctx.drawImage(trailsCtx.canvas, ...srcDim, ...destDim);
}

movers.forEach(mover => mover.display(ctx));

yield visibility(ctx.canvas);
}
}
Insert cell
function bounceMover({
width,
height,
x = 0,
y = 0,
velocityLoss = 0
}) {
const check = (mover, prop, min, max) => {
if (mover[prop] < min) {
mover[prop] = min;
mover.velocity[prop] *= -(1 - velocityLoss);
} else if (mover[prop] > max) {
mover[prop] = max;
mover.velocity[prop] *= -(1 - velocityLoss);
}
};
return mover => {
const bbox = mover.bbox;
check(mover, 'x', x + mover.bbox.minX, x + width - mover.bbox.maxX);
check(mover, 'y', y + mover.bbox.minY, y + height - mover.bbox.maxY);
};
}
Insert cell
function wrapMover({ width, height, x = 0, y = 0 }) {
return mover => {
mover.x = wrap(mover.x, x, x + width);
mover.y = wrap(mover.y, y, y + height);
};
}
Insert cell
function limitMover({ width, height, x = 0, y = 0 }) {
const check = (mover, prop, min, max) => {
if (mover[prop] < min) {
mover[prop] = min;
mover.velocity[prop] = 0;
} else if (mover[prop] > max) {
mover[prop] = max;
mover.velocity[prop] = 0;
}
};
return mover => {
const bbox = mover.bbox;
check(mover, 'x', x + mover.bbox.minX, x + width - mover.bbox.maxX);
check(mover, 'y', y + mover.bbox.minY, y + height - mover.bbox.maxY);
};
}
Insert cell
clamp = (num, min, max) => Math.min(max, Math.max(min, num))
Insert cell
Insert cell
d3 = require("d3", "d3-random")
Insert cell
import {
simplex,
random,
wrap,
repeat
} from '@ajur/the-nature-of-code-1-vectors'
Insert cell
THREE = require("three@0.147.0")
Insert cell
PI = Math.PI
Insert cell
import { slider } from '@jashkenas/inputs'
Insert cell
Insert cell
math = require('https://cdnjs.cloudflare.com/ajax/libs/mathjs/1.5.2/math.min.js')
Insert cell
functionPlot = require("https://unpkg.com/function-plot@1/dist/function-plot.js")
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