Nested Tesseracts
Fun little Tesseract turtle.
Log in to post a comment.
// ============================================================
// NESTED STRING-ART CUBES
// TurtleToy / pen-plotter friendly
//
// Inspired by geometric ruled-line cube drawings.
// Every mark is a straight line segment.
// ============================================================
Canvas.setpenopacity(1);
const turtle = new Turtle();
// ------------------------------------------------------------
// CONTROLS
// ------------------------------------------------------------
// Overall construction
const levels = 4; // min=1 max=8 step=1
const linesPerFace = 24; // min=4 max=70 step=1
const connectorLines = 14; // min=0 max=50 step=1
// Nested cube geometry
const shrink = 0.57; // min=0.35 max=0.82 step=0.01
const twist = 24; // min=-90 max=90 step=1
const tumbleX = 11; // min=-60 max=60 step=1
const tumbleY = 17; // min=-60 max=60 step=1
// Let the tunnel wander slightly as it moves inward
const driftX = 0.025; // min=-0.15 max=0.15 step=0.005
const driftY = -0.020; // min=-0.15 max=0.15 step=0.005
const driftZ = 0.015; // min=-0.15 max=0.15 step=0.005
// Projection
const drawingSize = 59; // min=35 max=72 step=1
const depthX = 0.42; // min=-0.8 max=0.8 step=0.01
const depthY = -0.32; // min=-0.8 max=0.8 step=0.01
const perspective = 0.08; // min=0 max=0.30 step=0.01
// Drawing options
const drawCubeEdges = 1; // min=0 max=1 step=1 (Off, On)
const faceFans = 1; // min=0 max=1 step=1 (Off, On)
const crossHatch = 0; // min=0 max=1 step=1 (Off, On)
const connectors = 1; // min=0 max=1 step=1 (Off, On)
// Alter which corner each face radiates from.
// Changing this can dramatically alter the design.
const fanRotation = 1; // min=0 max=3 step=1
// ------------------------------------------------------------
// STORAGE
// ------------------------------------------------------------
const segments = [];
function addLine(a, b) {
segments.push([a, b]);
}
function lerp(a, b, t) {
return [
a[0] + (b[0] - a[0]) * t,
a[1] + (b[1] - a[1]) * t
];
}
// ------------------------------------------------------------
// 3D MATH
// ------------------------------------------------------------
function rotateX(p, angle) {
const a = angle * Math.PI / 180;
const c = Math.cos(a);
const s = Math.sin(a);
return [
p[0],
p[1] * c - p[2] * s,
p[1] * s + p[2] * c
];
}
function rotateY(p, angle) {
const a = angle * Math.PI / 180;
const c = Math.cos(a);
const s = Math.sin(a);
return [
p[0] * c + p[2] * s,
p[1],
-p[0] * s + p[2] * c
];
}
function rotateZ(p, angle) {
const a = angle * Math.PI / 180;
const c = Math.cos(a);
const s = Math.sin(a);
return [
p[0] * c - p[1] * s,
p[0] * s + p[1] * c,
p[2]
];
}
// ------------------------------------------------------------
// OBLIQUE / PERSPECTIVE PROJECTION
// ------------------------------------------------------------
function project(p) {
let x = p[0] + p[2] * depthX;
let y = p[1] + p[2] * depthY;
// Very mild perspective.
// 0 = traditional parallel / architectural projection.
const pf = 1 / (1 - p[2] * perspective);
x *= pf;
y *= pf;
return [
x * drawingSize,
y * drawingSize
];
}
// ------------------------------------------------------------
// CUBE
// ------------------------------------------------------------
// Vertex order:
//
// 7 -------- 6
// /| /|
// 4 -------- 5 |
// | | | |
// | 3 -------|-2
// |/ |/
// 0 -------- 1
//
const baseVertices = [
[-1,-1,-1], // 0
[ 1,-1,-1], // 1
[ 1, 1,-1], // 2
[-1, 1,-1], // 3
[-1,-1, 1], // 4
[ 1,-1, 1], // 5
[ 1, 1, 1], // 6
[-1, 1, 1] // 7
];
const cubeEdges = [
[0,1], [1,2], [2,3], [3,0],
[4,5], [5,6], [6,7], [7,4],
[0,4], [1,5], [2,6], [3,7]
];
const cubeFaces = [
[0,1,2,3],
[4,5,6,7],
[0,1,5,4],
[1,2,6,5],
[2,3,7,6],
[3,0,4,7]
];
// ------------------------------------------------------------
// GENERATE ONE NESTED CUBE
// ------------------------------------------------------------
function makeCube(level) {
const scale = Math.pow(shrink, level);
const rz = level * twist;
const rx = level * tumbleX;
const ry = level * tumbleY;
const cx = driftX * level;
const cy = driftY * level;
const cz = driftZ * level;
const verts3D = [];
const verts2D = [];
for (let i = 0; i < baseVertices.length; i++) {
let p = [
baseVertices[i][0] * scale,
baseVertices[i][1] * scale,
baseVertices[i][2] * scale
];
// Outer cube remains architectural.
// Inner cubes progressively tumble.
p = rotateX(p, rx);
p = rotateY(p, ry);
p = rotateZ(p, rz);
p[0] += cx;
p[1] += cy;
p[2] += cz;
verts3D.push(p);
verts2D.push(project(p));
}
return {
p3: verts3D,
p2: verts2D
};
}
// ------------------------------------------------------------
// FRAME
// ------------------------------------------------------------
function drawEdges(cube) {
if (!drawCubeEdges) return;
for (let e = 0; e < cubeEdges.length; e++) {
const edge = cubeEdges[e];
addLine(
cube.p2[edge[0]],
cube.p2[edge[1]]
);
}
}
// ------------------------------------------------------------
// CLASSIC STRING-ART FAN
//
// B
// |\
// | \
// | \
// | \
// A----C
//
// Points walk along AB and BC in opposite directions.
// Every segment is straight, but their envelope looks curved.
// ------------------------------------------------------------
function cornerFan(A, B, C, count) {
for (let i = 0; i <= count; i++) {
const t = i / count;
const p = lerp(B, A, t);
const q = lerp(B, C, 1 - t);
addLine(p, q);
}
}
// ------------------------------------------------------------
// DRAW RULED LINES ON CUBE FACES
// ------------------------------------------------------------
function drawFaceFans(cube, level) {
if (!faceFans) return;
for (let f = 0; f < cubeFaces.length; f++) {
const face = cubeFaces[f];
// Alternate the active corner around the cube.
// This prevents all faces from looking identical.
const corner =
(f + level + fanRotation) % 4;
const previous =
(corner + 3) % 4;
const next =
(corner + 1) % 4;
const A = cube.p2[face[previous]];
const B = cube.p2[face[corner]];
const C = cube.p2[face[next]];
cornerFan(
A,
B,
C,
linesPerFace
);
// Optional opposite family.
// Produces the darker cross-hatched regions
// visible in drawings like the reference image.
if (crossHatch) {
const corner2 =
(corner + 2) % 4;
const previous2 =
(corner2 + 3) % 4;
const next2 =
(corner2 + 1) % 4;
cornerFan(
cube.p2[face[previous2]],
cube.p2[face[corner2]],
cube.p2[face[next2]],
linesPerFace
);
}
}
}
// ------------------------------------------------------------
// CONNECT TWO NESTED CUBES
//
// Corresponding cube edges are joined in reversed order.
// This creates the twisted ruled surfaces between levels.
// ------------------------------------------------------------
function connectCubes(outer, inner, level) {
if (!connectors) return;
if (connectorLines <= 0) return;
for (let e = 0; e < cubeEdges.length; e++) {
// Don't necessarily use every edge at every depth.
// Alternating them keeps the drawing from turning
// into an undifferentiated black mass.
if ((e + level) % 2 !== 0)
continue;
const edge = cubeEdges[e];
const oa = outer.p2[edge[0]];
const ob = outer.p2[edge[1]];
const ia = inner.p2[edge[0]];
const ib = inner.p2[edge[1]];
for (let i = 0; i <= connectorLines; i++) {
const t = i / connectorLines;
const p = lerp(oa, ob, t);
// Reverse direction on inner edge.
// This is what generates the twist.
const q = lerp(ia, ib, 1 - t);
addLine(p, q);
}
}
}
// ------------------------------------------------------------
// BUILD SCENE
// ------------------------------------------------------------
const cubes = [];
for (let level = 0; level < levels; level++) {
cubes.push(
makeCube(level)
);
}
// First draw face structures.
for (let level = 0; level < cubes.length; level++) {
drawFaceFans(
cubes[level],
level
);
}
// Then ruled surfaces between cubes.
for (let level = 0; level < cubes.length - 1; level++) {
connectCubes(
cubes[level],
cubes[level + 1],
level
);
}
// Put cube outlines on top visually.
for (let level = 0; level < cubes.length; level++) {
drawEdges(cubes[level]);
}
// ------------------------------------------------------------
// TURTLETOY WALK
// ------------------------------------------------------------
function walk(i) {
if (i >= segments.length)
return false;
const s = segments[i];
turtle.jump(
s[0][0],
s[0][1]
);
turtle.goto(
s[1][0],
s[1][1]
);
return true;
}