Published
Edited
May 20, 2021
20 stars
Insert cell
Insert cell
Insert cell
water = {
const div = document.createElement("div");
div.appendChild(renderer.domElement);
const simulator = initSimulator();
simulator.run();
div.addEventListener("pointerup", onclick, false);
invalidation.then(() => {
simulator.dispose();
dispose();
div.removeEventListener("pointerup", onclick);
const mem = renderer.info.memory;
if (mem.geometries > 0 || mem.textures > 0) console.log(mem);
});
mutable rendering = "";
return div;
function onclick(e) {
trackMouse(e, simulator.touch);
}
}
Insert cell
Insert cell
camera = {
const camera = new THREE.PerspectiveCamera(75, size.width / size.height, 0.2, 1500);
camera.updateProjectionMatrix();
camera.position.set(100, worldDims.width * 3, 100);
camera.rotation.set(-0.85, -0.15, -0.15);
return camera;
}
Insert cell
scene = {
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xffffff);
return scene;
}
Insert cell
Insert cell
controls = {
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.screenSpacePanning = false;
//controls.enablePan = false;
controls.maxPolarAngle = Math.PI / 2.5;
controls.minDistance = worldDims.width / 10;
controls.maxDistance = worldDims.width * 1.25;
controls.addEventListener("change", () => {
renderer.render(scene, camera);
});
controls.update();
invalidation.then(() => controls.dispose());
return controls;
}
Insert cell
Insert cell
renderer = {
const renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setSize(size.width, size.height);
renderer.setPixelRatio(window.devicePixelRatio);
invalidation.then(() => renderer.dispose());
return renderer;
}
Insert cell
render = () => {
renderer.render(scene, camera);
}
Insert cell
Insert cell
raycaster = new THREE.Raycaster()
Insert cell
mouse = ({
screen: new THREE.Vector2(),
scene: new THREE.Vector2(),
animating: false,
focus: null
})
Insert cell
trackMouse = (e, action) => {
e.preventDefault();
if (e.target instanceof HTMLCanvasElement) {
mouse.screen.x = e.offsetX;
mouse.screen.y = e.offsetY;
mouse.scene.x = (e.offsetX / size.width) * 2 - 1;
mouse.scene.y = -(e.offsetY / size.height) * 2 + 1;
if (!mouse.animating) intersect(action);
}
}
Insert cell
intersect = action => {
camera.updateMatrixWorld();
raycaster.setFromCamera(mouse.scene, camera);
const intersects = raycaster.intersectObjects(scene.children);
if (intersects.length > 0) {
for (let current of intersects) {
if (current.object) {
//simulator.focus(current.object);
action(current.object);
break;
}
}
}
}
Insert cell
Insert cell
initSimulator = () => {
return (function() {
const maxRadius = worldDims.width / 2,
level = -brickDims.height / 2;

let requestId,
restartTimeout,
waving = false,
disposing = false,
matrix = [], tweens = [], timelines = [];

function all() { return matrix.flatMap(r => r); }

function getBricksOnCircle(target, radius) {
return all().filter(b => isOnCircle(b, target.location.x, target.location.y, radius));
}

function isOnCircle(brick, h, k, r) {
const m = r > 1 ? r - 1 : r;
const v = Math.pow(brick.location.x - h, 2) + Math.pow(brick.location.y - k, 2);
return v >= m * m && v <= r * r;
}

function killTweens() {
tweens.forEach(t => t.kill());
tweens = [];
}

function killTimelines() {
timelines.forEach(t => t.kill());
timelines = [];
}

function levelBricks(y, onComplete) {
all().forEach(b => tweens.push(gsap.to(b.position, {duration: 1, y: y})));
Promise.all(tweens).then(() => {
killTweens();
if (onComplete) onComplete();
});
}
function drawBricks() {
let x = 0, y = 0;
for(let i = -worldDims.width; i < worldDims.width; i += brickDims.width) {
const row = [];
matrix.push(row);
for(let j = -worldDims.height; j < worldDims.height; j += brickDims.depth) {
const c = addCuboid(
brickDims.width, brickDims.height, brickDims.depth,
i, 0, j
);
c.location = new THREE.Vector2(x, y++);
row.push(c);
}
x++;
y = 0;
}
}

function run() {
disposing = false;
drawBricks();
animate();
vibrate();
function animate() {
requestId = requestAnimationFrame(animate);
render();
}
}

function restart() {
Promise.all(timelines).then(() => {
// Check if any timeline is still active
if (!timelines.some(tl => tl.isActive())) {
restartTimeout = setTimeout(() => {
// Check if there is any timeline activated between setTimeout and timeout
if (!timelines.some(tl => tl.isActive())) levelBricks(brickDims.height, vibrate);
}, 1000);
}
});
}

function vibrate() {
waving = false;
killTweens();
all().forEach(b => {
const y = brickDims.height * Math.random();
tweens.push(
gsap.to(
b.position,
{duration: 0.5, y: y, onUpdate: () => onUpdate(b)}
));
});
Promise.all(tweens).then(() => vibrate());

function onUpdate(b) {
const mIndex = Math.floor(b.position.y);
if (b.mIndex != mIndex) {
b.mIndex = mIndex;
b.material = pool.floorMaterials[mIndex];
}
}
}

function touch(target) {
clearTimeout(restartTimeout);
killTweens();
if (waving)
drop(target);
else {
//levelBricks(level, () => drop(target));
levelBricks(level);
drop(target);
}
}

function drop(target) {
const p = target.position;
const cuboid = addCuboid(5, 5, 5, p.x, 100, p.z);
gsap.to(
cuboid.position,
{
duration: 1,
y: level,
onComplete: () => {
scene.remove(cuboid);
cuboid.remove(cuboid.children[0]);
wave(target, 1);
}
});
}

function wave(center, radius) {
if (disposing) return;
if (radius > maxRadius) {
restart();
return;
}

waving = true;
let bricks = getBricksOnCircle(center, radius);
if (radius === 1) bricks = [center].concat(bricks);

const tl = gsap.timeline();
timelines.push(tl);
const targets = bricks.map(b => b.position);
for(let p = brickDims.height; p > 0; p--) {
if (!disposing) {
tl.to(targets, {duration: 0.25, y: p * Math.random() / 2, onUpdate: onUpdate})
.to(targets, {duration: 0.25, y: -p * Math.random() / 2, onUpdate: onUpdate})
.to(targets, {duration: 0.25, y: level, onComplete: onComplete});
if (p === brickDims.height) setTimeout(() => wave(center, ++radius), 25);
}
}

function onUpdate() {
const y = bricks[0].position.y;
const mIndex = (y < 0 ? Math.ceil(y) : Math.floor(y)) * 2 + brickDims.height - 1;
bricks.forEach(b => {
if (b.mIndex !== mIndex) {
b.mIndex = mIndex;
b.material = pool.floorMaterials[mIndex];
}
});
}

function onComplete() {
bricks.forEach(b => {
const mIndex = Math.floor(brickDims.height * Math.random());
b.material = pool.floorMaterials[mIndex];
});
}
}

return {
run: () => run(),
touch: target => touch(target),
dispose: () => {
disposing = true;
cancelAnimationFrame(requestId);
killTweens();
killTimelines();
matrix = [];
}
}
})();
}
Insert cell
Insert cell
addCuboid = (w, h, d, x, y, z) => {
const cuboid = new THREE.Mesh(pool.floorGeometry, pool.floorMaterials[0]);
cuboid.position.set(x + w / 2, y, z + d / 2);
cuboid.scale.set(w, h, d);
const frame = new THREE.LineSegments(pool.edgeGeometry, pool.lineMaterial);
cuboid.add(frame);
scene.add(cuboid);
return cuboid;
}
Insert cell
seq = (s, n) => Array.apply(null, {length: n}).map((d, i) => s + i)
Insert cell
color = {
const edge = brickDims.height - 1;
return d3.scaleSequential(t => d3.interpolateBlues(t/8 + 0.25)).domain([-edge, edge]);
}
Insert cell
pool = {
const geometry = new THREE.BoxBufferGeometry(1, 1, 1);
const colorIndices = seq(-(brickDims.height - 1), brickDims.height * 2 - 1);
return {
floorGeometry: geometry,
floorMaterials: colorIndices.map(d =>
new THREE.MeshBasicMaterial({
color: color(d),
opacity: 0.6,
transparent: true
})),
edgeGeometry: new THREE.EdgesGeometry(geometry),
lineMaterial: new THREE.LineBasicMaterial({
color: d3.color(color(0)).darker(0.75).formatHex(),
linewidth: 1
})
}
}
Insert cell
dispose = () => {
cleanup(scene);
function cleanup(obj) {
for(let i = obj.children.length - 1; i >= 0; i--) {
const child = obj.children[i];
obj.remove(child);
if (child.geometry) child.geometry.dispose();
if (child.material) child.material.dispose();
if (child.children && child.children.length > 0) cleanup(child);
}
}
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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