Published
Edited
Aug 30, 2021
1 fork
22 stars
Also listed in…
5. D3 + Three.js
Insert cell
Insert cell
Insert cell
Insert cell
model = {
const div = document.createElement("div");
div.appendChild(renderer.domElement);
div.appendChild(rgbChart());
camera.position.set(0, 250, 0);
controls.update();
cubes.forEach(cube => scene.add(cube));
render();
invalidation.then(() => dispose());
return div;
}
Insert cell
animation = {
cubes.forEach((cube, i) => {
const
p = pixels[i],
x = p.x + p.xi * step,
y = p.yi * step,
z = p.y + p.zi * step;
cube.position.set(x, y, z);
});
camera.position.set(0, 250 + cameraStep.y * step, 0);
controls.update();
if (step === steps - 1) axes.forEach(axis => scene.add(axis));
else axes.forEach(axis => scene.remove(axis));
render();
}
Insert cell
Insert cell
pixels = {
let img;
if (photo === "Photo 1") img = await FileAttachment("photo1.jpg").image();
else if (photo === "Photo 2") img = await FileAttachment("photo2.jpg").image();
else img = await FileAttachment("photo3.jpg").image();

const
width = pixelsPerLine,
height = Math.floor(width * img.height / img.width),
hw = width / 2, hh = height / 2;

const ctx = DOM.context2d(width, height, 1);
ctx.drawImage(img, 0, 0, width, height);
const
{data: array} = ctx.getImageData(0, 0, width, height),
len = array.length / 4,
pixels = new Array(len);
for(let i = 0; i < len; i++) {
const
x = i % width * pixelSize.ratio - width * pixelSize.half,
y = Math.floor(i / width) * pixelSize.ratio - height * pixelSize.half,
ai = i * 4, r = array[ai], g = array[ai + 1], b = array[ai + 2],
index = r * 65536 + g * 256 + b,
xi = ((r - 128) - (x - hw)) / steps,
yi = (g - 128) / steps,
zi = ((b - 128) - (y - hh)) / steps;
pixels[i] = {
x, y,
r, g, b,
xi, yi, zi, index
};
}
return pixels;

invalidation.then(() => dispose());
}
Insert cell
Insert cell
rgbChart = () => {
const colorData = pixels.flatMap(p => [{color: "r", value: p.r}, {color: "g", value: p.g}, {color: "b", value: p.b}]);
const plot = Plot.plot({
width: width,
height: 100,
color: {
domain: ["r", "g", "b"],
range: ["red", "green", "blue"]
},
fx: {
axis: null,
label: null,
domain: ["r", "g", "b"]
},
facet: {
data: colorData,
x: "color"
},
x: { axis: null },
y: { axis: null },
marks: [
Plot.frame({stroke: "#bbb"}),
Plot.areaY(
colorData,
Plot.groupX({y: "count"}, {x: "value", fill: "color"})
)
]
});
return plot;
}
Insert cell
Insert cell
cubes = pixels.map(p => addCube(p.x, 0, p.y, p.index))
Insert cell
axes = [
addLine(new THREE.Vector3(-128, -128, -128), new THREE.Vector3(128, -128, -128), 0xff0000),
addLine(new THREE.Vector3(-128, -128, -128), new THREE.Vector3(-128, -128, 128), 0x00ff00),
addLine(new THREE.Vector3(-128, -128, -128), new THREE.Vector3(-128, 128, -128), 0x0000ff)
];
Insert cell
addCube = (x, y, z, c) => {
const cube = new THREE.Mesh(resourcePool.geometry, resourcePool.materials.find(m => m.index === c).material);
cube.position.set(x, y, z);
cube.scale.set(pixelSize.full, pixelSize.full, pixelSize.full);
return cube;
}
Insert cell
addLine = (from, to, color) => {
const geometry = new THREE.BufferGeometry().setFromPoints([from, to]);
const line = new THREE.Line(geometry, new THREE.LineBasicMaterial({color: color}));
return line;
}
Insert cell
resourcePool = {
const geometry = new THREE.BoxBufferGeometry(1, 1, 1);
const materials = [...new Set(pixels.map(d => d.index))].map(color => ({
index: color,
material: new THREE.MeshBasicMaterial({color: color})
}));
return {geometry, materials};
}
Insert cell
Insert cell
camera = {
const camera = new THREE.PerspectiveCamera(25, size.width / size.height, 1, 1000);
camera.updateProjectionMatrix();
camera.position.set(0, 250, 0);
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.minDistance = 30;
controls.maxDistance = 800;
controls.addEventListener("change", () => renderer.render(scene, camera));
controls.update();
invalidation.then(() => controls.dispose());
return controls;
}
Insert cell
md`### Renderer`
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
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
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