Public
Edited
Mar 6, 2023
Fork of Three.js
3 forks
4 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
function loop() {
if (animationBuffer.length) {
const { axisName, level, step, fun, select } = animationBuffer.shift();
fun();
} else {
if (animationBuffer.active) rndRotateTheMagic();
}
renderer.render(scene, camera);
requestAnimationFrame(loop);
}

loop();
}
Insert cell
{
gCubes.map((group) => {
group.children.slice(1).map((d) => {
d.visible = toggle2;
});
});
}
Insert cell
{
animationBuffer.active = toggle;
}
Insert cell
/** !!! Do not delete it.
* It is an important buffer.
* The animation operations are stored and operated one-by-one
*/
animationBuffer = []
Insert cell
// {
// textureCubesBuffer.map((d) => {
// d.visible = false;
// });
// }
Insert cell
rndAxisLevel = () => {
const rnd3 = d3.randomInt(0, 3),
rnd2 = d3.randomInt(0, 2);
return {
axisName: ["x", "y", "z"][rnd3()],
level: [-1, 0, 1][rnd3()],
dir: [-1, 1][rnd2()]
};
}
Insert cell
rndAxisLevel()
Insert cell
rndRotateTheMagic = () => {
const { axisName, level, dir } = rndAxisLevel();

const select = gCubes.filter(
(cube) =>
Math.abs(realWorldPosition(cube)[axisName] - level) < ConstantValues.eps
);

var radian;
for (let i = 1; i < animationTable.length; ++i) {
animationBuffer.push({
axisName,
level,
step: i,
select: select,
fun: () =>
select.map((g) => {
radian =
(animationTable[i].y - animationTable[i - 1].y) *
ConstantValues.pi2 *
0.25;
// g.matrixAutoUpdate = false;
rotGroupBy(g, axisName, dir * radian, i === 1);
})
});
}
}
Insert cell
// {
// rndRotateTheMagic();
// }
Insert cell
rotGroupBy = (group, axisName = "x", radian = 0, fixFlag = false) => {
// Only accept axisName in ['x', 'y', 'z']
// The radian controls to rotation
// fixFlag, whether to call the fixRotation to zero-out the near-2pi rotation

const worldAxis = new THREE.Vector3(0, 0, 0);
worldAxis[axisName] = 1;

group.rotateOnWorldAxis(worldAxis, radian);
// group.rotation[axisName] += radian;
if (fixFlag) fixRotation(group);
}
Insert cell
fixRotation = (group) => {
["x", "y", "z"].map((n) => {
group.rotation[n] %= ConstantValues.pi2
});
}
Insert cell
ConstantValues = {
return {
pi2: Math.PI * 2,
eps: Math.PI * 2 * 0.005
}
}
Insert cell
realWorldPosition = (group, childIdx = 0) => {
return group.children[childIdx].getWorldPosition(worldPosition);
}
Insert cell
Plot.plot({
grid: true,
marks: [
Plot.line(animationTable, {
x: "x",
y: "y",
stroke: "y",
z: (d) => undefined
})
]
})
Insert cell
animationTable = {
var x, y, y1;

const step = 100,
table = [...new Array(step + 1)].map((d, i) => {
x = i / step;
y = x;
if (x < 0.5) {
y = 2 * x ** 2;
} else {
y = 1 - 2 * (1 - x) ** 2;
}
return { i, x, y };
});

return table;
}
Insert cell
animationTable
Type Table, then Shift-Enter. Ctrl-space for more options.

Insert cell
worldPosition = new THREE.Vector3();
Insert cell
height = 600
Insert cell
mkGroup = (position) => {
const pivot = new THREE.Group();

// The sub-cube
const material = new THREE.MeshNormalMaterial(),
size = 0.95,
geometry = new RoundedBoxGeometry(size, size, size, 20, 0.1),
cube = new THREE.Mesh(geometry, material);

Object.assign(cube.position, position);
pivot.add(cube);

// The texture
function mkTexture(color, geo, axisName, value) {
const material2 = new THREE.MeshPhongMaterial({
color, // red (can also use a CSS color string here)
flatShading: true
}),
geometry2 = new RoundedBoxGeometry(geo.x, geo.y, geo.z, 20, 0.1),
cube2 = new THREE.Mesh(geometry2, material2);

const pos = Object.assign({}, position);
pos[axisName] += value;
Object.assign(cube2.position, pos);

pivot.add(cube2);
}

const offset = 0.45;

if (Math.abs(position.x) === 1) {
mkTexture(
d3.schemeCategory10[0],
{ x: 0.1, y: 0.8, z: 0.8 },
"x",
position.x === 1 ? offset : -offset
);
}

if (Math.abs(position.y) === 1) {
mkTexture(
d3.schemeCategory10[1],
{ x: 0.8, y: 0.1, z: 0.8 },
"y",
position.y === 1 ? offset : -offset
);
}

if (Math.abs(position.z) === 1) {
mkTexture(
d3.schemeCategory10[2],
{ x: 0.8, y: 0.8, z: 0.1 },
"z",
position.z === 1 ? offset : -offset
);
}

return pivot;
}
Insert cell
d3.schemeCategory10
Insert cell
gCubes = {
const cubes = [];

for (let x = 0; x < 3; ++x) {
for (let y = 0; y < 3; ++y) {
for (let z = 0; z < 3; ++z) {
cubes.push(mkGroup({ x: x - 1, y: y - 1, z: z - 1 }));
}
}
}

return cubes;
}
Insert cell
scene = {
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x001b42);

const light = new THREE.AmbientLight();

scene.add(light);

gCubes.map((cube) => scene.add(cube));
return scene;
}
Insert cell
camera = {
const fov = 45;
const aspect = width / height;
const near = 1;
const far = 1000;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.set(5, 5, -5);
camera.lookAt(new THREE.Vector3(0, 0, 0));
return camera;
}
Insert cell
renderer = {
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(width, height);
renderer.setPixelRatio(devicePixelRatio);
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.addEventListener("change", () => renderer.render(scene, camera));
invalidation.then(() => (controls.dispose(), renderer.dispose()));
return renderer;
}
Insert cell
RoundedBoxGeometry = {
const { BoxGeometry, Vector3 } = THREE;

const _tempNormal = new Vector3();

function getUv(
faceDirVector,
normal,
uvAxis,
projectionAxis,
radius,
sideLength
) {
const totArcLength = (2 * Math.PI * radius) / 4;

// length of the planes between the arcs on each axis
const centerLength = Math.max(sideLength - 2 * radius, 0);
const halfArc = Math.PI / 4;

// Get the vector projected onto the Y plane
_tempNormal.copy(normal);
_tempNormal[projectionAxis] = 0;
_tempNormal.normalize();

// total amount of UV space alloted to a single arc
const arcUvRatio = (0.5 * totArcLength) / (totArcLength + centerLength);

// the distance along one arc the point is at
const arcAngleRatio = 1.0 - _tempNormal.angleTo(faceDirVector) / halfArc;

if (Math.sign(_tempNormal[uvAxis]) === 1) {
return arcAngleRatio * arcUvRatio;
} else {
// total amount of UV space alloted to the plane between the arcs
const lenUv = centerLength / (totArcLength + centerLength);
return lenUv + arcUvRatio + arcUvRatio * (1.0 - arcAngleRatio);
}
}

class RoundedBoxGeometry extends BoxGeometry {
constructor(width = 1, height = 1, depth = 1, segments = 2, radius = 0.1) {
// ensure segments is odd so we have a plane connecting the rounded corners
segments = segments * 2 + 1;

// ensure radius isn't bigger than shortest side
radius = Math.min(width / 2, height / 2, depth / 2, radius);

super(1, 1, 1, segments, segments, segments);

// if we just have one segment we're the same as a regular box
if (segments === 1) return;

const geometry2 = this.toNonIndexed();

this.index = null;
this.attributes.position = geometry2.attributes.position;
this.attributes.normal = geometry2.attributes.normal;
this.attributes.uv = geometry2.attributes.uv;

//

const position = new Vector3();
const normal = new Vector3();

const box = new Vector3(width, height, depth)
.divideScalar(2)
.subScalar(radius);

const positions = this.attributes.position.array;
const normals = this.attributes.normal.array;
const uvs = this.attributes.uv.array;

const faceTris = positions.length / 6;
const faceDirVector = new Vector3();
const halfSegmentSize = 0.5 / segments;

for (let i = 0, j = 0; i < positions.length; i += 3, j += 2) {
position.fromArray(positions, i);
normal.copy(position);
normal.x -= Math.sign(normal.x) * halfSegmentSize;
normal.y -= Math.sign(normal.y) * halfSegmentSize;
normal.z -= Math.sign(normal.z) * halfSegmentSize;
normal.normalize();

positions[i + 0] = box.x * Math.sign(position.x) + normal.x * radius;
positions[i + 1] = box.y * Math.sign(position.y) + normal.y * radius;
positions[i + 2] = box.z * Math.sign(position.z) + normal.z * radius;

normals[i + 0] = normal.x;
normals[i + 1] = normal.y;
normals[i + 2] = normal.z;

const side = Math.floor(i / faceTris);

switch (side) {
case 0: // right
// generate UVs along Z then Y
faceDirVector.set(1, 0, 0);
uvs[j + 0] = getUv(faceDirVector, normal, "z", "y", radius, depth);
uvs[j + 1] =
1.0 - getUv(faceDirVector, normal, "y", "z", radius, height);
break;

case 1: // left
// generate UVs along Z then Y
faceDirVector.set(-1, 0, 0);
uvs[j + 0] =
1.0 - getUv(faceDirVector, normal, "z", "y", radius, depth);
uvs[j + 1] =
1.0 - getUv(faceDirVector, normal, "y", "z", radius, height);
break;

case 2: // top
// generate UVs along X then Z
faceDirVector.set(0, 1, 0);
uvs[j + 0] =
1.0 - getUv(faceDirVector, normal, "x", "z", radius, width);
uvs[j + 1] = getUv(faceDirVector, normal, "z", "x", radius, depth);
break;

case 3: // bottom
// generate UVs along X then Z
faceDirVector.set(0, -1, 0);
uvs[j + 0] =
1.0 - getUv(faceDirVector, normal, "x", "z", radius, width);
uvs[j + 1] =
1.0 - getUv(faceDirVector, normal, "z", "x", radius, depth);
break;

case 4: // front
// generate UVs along X then Y
faceDirVector.set(0, 0, 1);
uvs[j + 0] =
1.0 - getUv(faceDirVector, normal, "x", "y", radius, width);
uvs[j + 1] =
1.0 - getUv(faceDirVector, normal, "y", "x", radius, height);
break;

case 5: // back
// generate UVs along X then Y
faceDirVector.set(0, 0, -1);
uvs[j + 0] = getUv(faceDirVector, normal, "x", "y", radius, width);
uvs[j + 1] =
1.0 - getUv(faceDirVector, normal, "y", "x", radius, height);
break;
}
}
}
}

return RoundedBoxGeometry;
}
Insert cell
THREE = {
const THREE = (window.THREE =
await require("three@0.130.0/build/three.min.js"));
await require("three@0.130.0/examples/js/controls/OrbitControls.js").catch(
() => {}
);
await require("three@0.130.0/examples/jsm/geometries/RoundedBoxGeometry.js").catch(
() => {}
);
return THREE;
}
Insert cell
d3 = require("d3")
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