TesserActions
Fun little Tesseract turtle.
Note: You can add up to 4 cubes. If you only have 2 cubes, the controls for Cube3 & Cube4 will do nothing.
Enjoy, and please share your variants!
Log in to post a comment.
// ============================================================
// ARCHITECTURAL RULED CUBES — V6.0
// TurtleToy / Pen Plotter Friendly
//
// MASTER VERSION:
// cubeCount = 2, 3, or 4
//
// Active cubes:
// 2 cubes = Cube 1 + Cube 4
// 3 cubes = Cube 1 + Cube 2 + Cube 4
// 4 cubes = Cube 1 + Cube 2 + Cube 3 + Cube 4
//
// ORIENTATION MODES:
//
// 0 = Free XYZ
// 1 = Square
// 2 = Z snap 15°
// 3 = Z snap 22.5°
// 4 = Z snap 30°
// 5 = Z snap 45°
// 6 = XYZ snap 15°
// 7 = XYZ snap 22.5°
//
// Modes 1-5 preserve architectural integrity.
// Modes 6-7 allow deliberate snapped 3D tilting.
//
// All geometry is straight-line / plotter friendly.
// ============================================================
Canvas.setpenopacity(1);
const turtle = new Turtle();
// ============================================================
// MASTER CONTROLS
// ============================================================
const cubeCount = 3; // min=2 max=4 step=1
const size = 58; // min=35 max=70 step=1
// Projection mode:
//
// 0 = Free
// 1 = Reference / Architectural
// 2 = Strong Axonometric
//
const projectionMode = 1; // min=0 max=2 step=1
// Used only when Projection Mode = 0
const depthX = 0.42; // min=0.10 max=0.70 step=0.01
const depthY = -0.31; // min=-0.65 max=-0.05 step=0.01
const perspective = 0.00; // min=0 max=0.12 step=0.005
// Surface controls
const localSheets = 1; // min=0 max=1 step=1
const connectionSheets = 1; // min=0 max=1 step=1
const centerMesh = 1; // min=0 max=1 step=1
const crossHatch = 1; // min=0 max=1 step=1
// Connection complexity:
//
// 1 = main architectural sheets
// 2 = + corner fans
// 3 = + depth sheets
//
const connectionComplexity = 3; // min=1 max=3 step=1
const connectionDensity = 1.00; // min=0.40 max=1.50 step=0.05
const patternPhase = 0; // min=0 max=3 step=1
// ============================================================
// FRAME CONTROLS
// ============================================================
const frames = 1; // min=0 max=1 step=1
const thickFrames = 1; // min=0 max=1 step=1
// Very closely spaced 3D lines.
// These should visually merge into a heavier edge.
const framePasses = 9; // min=1 max=13 step=2
const frameRadius = 0.0020; // min=0 max=0.008 step=0.0005
// ============================================================
// CUBE 1 — OUTER
// ============================================================
const cube1Size = 1.00; // min=0.85 max=1.10 step=0.01
const cube1Orientation = 1; // min=0 max=7 step=1
const cube1RotX = 0; // min=-90 max=90 step=0.5
const cube1RotY = 0; // min=-90 max=90 step=0.5
const cube1RotZ = 0; // min=-90 max=90 step=0.5
const cube1OffsetX = 0.00; // min=-0.30 max=0.30 step=0.005
const cube1OffsetY = 0.00; // min=-0.30 max=0.30 step=0.005
const cube1OffsetZ = 0.00; // min=-0.30 max=0.30 step=0.005
const cube1Lines = 28; // min=6 max=50 step=1
// ============================================================
// CUBE 2 — PRIMARY MIDDLE
// ============================================================
const cube2Size = 0.62; // min=0.40 max=0.82 step=0.01
// Default = perfectly clean 22.5° Z rotation
const cube2Orientation = 3; // min=0 max=7 step=1
const cube2RotX = 0; // min=-90 max=90 step=0.5
const cube2RotY = 0; // min=-90 max=90 step=0.5
const cube2RotZ = -22.5; // min=-90 max=90 step=0.5
const cube2OffsetX = 0.00; // min=-0.30 max=0.30 step=0.005
const cube2OffsetY = 0.00; // min=-0.30 max=0.30 step=0.005
const cube2OffsetZ = 0.00; // min=-0.30 max=0.30 step=0.005
const cube2Lines = 23; // min=6 max=50 step=1
// ============================================================
// CUBE 3 — SECOND MIDDLE
//
// Used only when cubeCount = 4.
// ============================================================
const cube3Size = 0.47; // min=0.28 max=0.70 step=0.01
const cube3Orientation = 3; // min=0 max=7 step=1
const cube3RotX = 0; // min=-90 max=90 step=0.5
const cube3RotY = 0; // min=-90 max=90 step=0.5
const cube3RotZ = 22.5; // min=-90 max=90 step=0.5
const cube3OffsetX = 0.00; // min=-0.30 max=0.30 step=0.005
const cube3OffsetY = 0.00; // min=-0.30 max=0.30 step=0.005
const cube3OffsetZ = 0.00; // min=-0.30 max=0.30 step=0.005
const cube3Lines = 20; // min=6 max=50 step=1
// ============================================================
// CUBE 4 — INNER
// ============================================================
const cube4Size = 0.34; // min=0.16 max=0.55 step=0.01
const cube4Orientation = 1; // min=0 max=7 step=1
const cube4RotX = 0; // min=-90 max=90 step=0.5
const cube4RotY = 0; // min=-90 max=90 step=0.5
const cube4RotZ = 0; // min=-90 max=90 step=0.5
const cube4OffsetX = 0.00; // min=-0.30 max=0.30 step=0.005
const cube4OffsetY = 0.00; // min=-0.30 max=0.30 step=0.005
const cube4OffsetZ = 0.00; // min=-0.30 max=0.30 step=0.005
const cube4Lines = 18; // min=6 max=50 step=1
// ============================================================
// STORAGE
// ============================================================
const paths = [];
function addLine(a, b) {
paths.push([
a,
b
]);
}
function lerp(a, b, t) {
return [
a[0] + (b[0] - a[0]) * t,
a[1] + (b[1] - a[1]) * t
];
}
// ============================================================
// 3D VECTOR MATH
// ============================================================
function add3(a, b) {
return [
a[0] + b[0],
a[1] + b[1],
a[2] + b[2]
];
}
function sub3(a, b) {
return [
a[0] - b[0],
a[1] - b[1],
a[2] - b[2]
];
}
function scale3(a, s) {
return [
a[0] * s,
a[1] * s,
a[2] * s
];
}
function length3(a) {
return Math.sqrt(
a[0] * a[0] +
a[1] * a[1] +
a[2] * a[2]
);
}
function normalize3(a) {
const len =
length3(a);
if (len < 0.0000001) {
return [0, 0, 0];
}
return [
a[0] / len,
a[1] / len,
a[2] / len
];
}
function cross3(a, b) {
return [
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0]
];
}
// ============================================================
// ROTATIONS
// ============================================================
function rotateX(p, degrees) {
const a =
degrees * 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, degrees) {
const a =
degrees * 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, degrees) {
const a =
degrees * 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]
];
}
// ============================================================
// ROTATION SNAPPING
// ============================================================
function snapAngle(value, increment) {
return (
Math.round(
value / increment
)
*
increment
);
}
function resolveOrientation(
mode,
rx,
ry,
rz
) {
// FREE XYZ
if (mode === 0) {
return [
rx,
ry,
rz
];
}
// SQUARE
if (mode === 1) {
return [
0,
0,
0
];
}
// Z ONLY — 15 DEGREE SNAP
if (mode === 2) {
return [
0,
0,
snapAngle(
rz,
15
)
];
}
// Z ONLY — 22.5 DEGREE SNAP
if (mode === 3) {
return [
0,
0,
snapAngle(
rz,
22.5
)
];
}
// Z ONLY — 30 DEGREE SNAP
if (mode === 4) {
return [
0,
0,
snapAngle(
rz,
30
)
];
}
// Z ONLY — 45 DEGREE SNAP
if (mode === 5) {
return [
0,
0,
snapAngle(
rz,
45
)
];
}
// FULL XYZ — 15 DEGREE SNAP
if (mode === 6) {
return [
snapAngle(
rx,
15
),
snapAngle(
ry,
15
),
snapAngle(
rz,
15
)
];
}
// FULL XYZ — 22.5 DEGREE SNAP
return [
snapAngle(
rx,
22.5
),
snapAngle(
ry,
22.5
),
snapAngle(
rz,
22.5
)
];
}
// ============================================================
// PROJECTION
// ============================================================
function project(p) {
let dx;
let dy;
let persp;
// FREE
if (projectionMode === 0) {
dx =
depthX;
dy =
depthY;
persp =
perspective;
}
// REFERENCE / ARCHITECTURAL
else if (projectionMode === 1) {
dx =
0.42;
dy =
-0.31;
persp =
0;
}
// STRONG AXONOMETRIC
else {
dx =
0.50;
dy =
-0.50;
persp =
0;
}
let x =
p[0] +
p[2] * dx;
let y =
p[1] +
p[2] * dy;
const factor =
1 /
(1 - p[2] * persp);
return [
x * factor * size,
y * factor * size
];
}
// ============================================================
// BASE CUBE
// ============================================================
const base = [
[-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
];
// ============================================================
// EDGES
// ============================================================
const edges = [
[0,1], // 0 front bottom
[1,2], // 1 front right
[2,3], // 2 front top
[3,0], // 3 front left
[4,5], // 4 back bottom
[5,6], // 5 back right
[6,7], // 6 back top
[7,4], // 7 back left
[0,4], // 8 depth bottom-left
[1,5], // 9 depth bottom-right
[2,6], // 10 depth top-right
[3,7] // 11 depth top-left
];
const FRONT_BOTTOM = 0;
const FRONT_RIGHT = 1;
const FRONT_TOP = 2;
const FRONT_LEFT = 3;
const BACK_BOTTOM = 4;
const BACK_RIGHT = 5;
const BACK_TOP = 6;
const BACK_LEFT = 7;
const DEPTH_BL = 8;
const DEPTH_BR = 9;
const DEPTH_TR = 10;
const DEPTH_TL = 11;
// ============================================================
// FACES
// ============================================================
const FRONT_FACE = [0,1,2,3];
const BACK_FACE = [4,5,6,7];
const LEFT_FACE = [0,3,7,4];
const RIGHT_FACE = [1,2,6,5];
const BOTTOM_FACE = [0,1,5,4];
const TOP_FACE = [3,2,6,7];
// ============================================================
// BUILD CUBE
// ============================================================
function makeCube(
cubeSize,
orientationMode,
rx,
ry,
rz,
ox,
oy,
oz,
lineCount
) {
const orientation =
resolveOrientation(
orientationMode,
rx,
ry,
rz
);
const finalX =
orientation[0];
const finalY =
orientation[1];
const finalZ =
orientation[2];
const p3 = [];
const p2 = [];
for (
let i = 0;
i < base.length;
i++
) {
let p = [
base[i][0] * cubeSize,
base[i][1] * cubeSize,
base[i][2] * cubeSize
];
p =
rotateX(
p,
finalX
);
p =
rotateY(
p,
finalY
);
p =
rotateZ(
p,
finalZ
);
p[0] += ox;
p[1] += oy;
p[2] += oz;
p3.push(
p
);
p2.push(
project(p)
);
}
return {
p3: p3,
p2: p2,
size: cubeSize,
lines: lineCount
};
}
// ============================================================
// CREATE ALL FOUR POSSIBLE CUBES
// ============================================================
const cube1 = makeCube(
cube1Size,
cube1Orientation,
cube1RotX,
cube1RotY,
cube1RotZ,
cube1OffsetX,
cube1OffsetY,
cube1OffsetZ,
cube1Lines
);
const cube2 = makeCube(
cube2Size,
cube2Orientation,
cube2RotX,
cube2RotY,
cube2RotZ,
cube2OffsetX,
cube2OffsetY,
cube2OffsetZ,
cube2Lines
);
const cube3 = makeCube(
cube3Size,
cube3Orientation,
cube3RotX,
cube3RotY,
cube3RotZ,
cube3OffsetX,
cube3OffsetY,
cube3OffsetZ,
cube3Lines
);
const cube4 = makeCube(
cube4Size,
cube4Orientation,
cube4RotX,
cube4RotY,
cube4RotZ,
cube4OffsetX,
cube4OffsetY,
cube4OffsetZ,
cube4Lines
);
// ============================================================
// ACTIVE CUBE LIST
// ============================================================
const cubes = [];
if (cubeCount === 2) {
cubes.push(
cube1
);
cubes.push(
cube4
);
}
else if (cubeCount === 3) {
cubes.push(
cube1
);
cubes.push(
cube2
);
cubes.push(
cube4
);
}
else {
cubes.push(
cube1
);
cubes.push(
cube2
);
cubes.push(
cube3
);
cubes.push(
cube4
);
}
// ============================================================
// EDGE HELPER
// ============================================================
function getEdge(
cube,
edgeNumber
) {
const e =
edges[edgeNumber];
return [
cube.p2[e[0]],
cube.p2[e[1]]
];
}
// ============================================================
// CORNER STRING-ART SHEET
// ============================================================
function cornerSheet(
A,
B,
C,
count,
reverse
) {
for (
let i = 0;
i <= count;
i++
) {
const t =
i / count;
let a =
t;
let b =
1 - t;
if (reverse) {
a =
1 - t;
b =
t;
}
addLine(
lerp(
A,
B,
a
),
lerp(
B,
C,
b
)
);
}
}
// ============================================================
// RULED EDGE-TO-EDGE SHEET
// ============================================================
function ruledEdges(
cubeA,
edgeA,
cubeB,
edgeB,
count,
reverse
) {
const A =
getEdge(
cubeA,
edgeA
);
const B =
getEdge(
cubeB,
edgeB
);
for (
let i = 0;
i <= count;
i++
) {
const t =
i / count;
const q =
reverse
? 1 - t
: t;
addLine(
lerp(
A[0],
A[1],
t
),
lerp(
B[0],
B[1],
q
)
);
}
}
// ============================================================
// VERTEX-TO-EDGE FAN
// ============================================================
function vertexFan(
vertex,
cube,
edgeNumber,
count,
reverse
) {
const E =
getEdge(
cube,
edgeNumber
);
for (
let i = 0;
i <= count;
i++
) {
let t =
i / count;
if (reverse) {
t =
1 - t;
}
addLine(
vertex,
lerp(
E[0],
E[1],
t
)
);
}
}
// ============================================================
// LOCAL ARCHITECTURAL SHEETS
// ============================================================
function drawLocalSheets(
cube,
index
) {
if (!localSheets)
return;
const c =
cube.p2;
const n =
cube.lines;
const phase =
(index + patternPhase) % 4;
if (phase === 0) {
cornerSheet(
c[4],
c[7],
c[6],
n,
false
);
cornerSheet(
c[3],
c[0],
c[4],
n,
false
);
cornerSheet(
c[2],
c[6],
c[5],
n,
true
);
cornerSheet(
c[0],
c[1],
c[5],
n,
true
);
}
else if (phase === 1) {
cornerSheet(
c[3],
c[2],
c[6],
n,
false
);
cornerSheet(
c[0],
c[4],
c[5],
n,
true
);
cornerSheet(
c[7],
c[6],
c[5],
n,
false
);
cornerSheet(
c[3],
c[0],
c[1],
n,
true
);
}
else if (phase === 2) {
cornerSheet(
c[4],
c[7],
c[3],
n,
false
);
cornerSheet(
c[2],
c[1],
c[5],
n,
true
);
cornerSheet(
c[0],
c[4],
c[5],
n,
false
);
cornerSheet(
c[3],
c[2],
c[6],
n,
true
);
}
else {
cornerSheet(
c[7],
c[6],
c[2],
n,
true
);
cornerSheet(
c[3],
c[0],
c[4],
n,
false
);
cornerSheet(
c[1],
c[5],
c[6],
n,
false
);
cornerSheet(
c[0],
c[1],
c[2],
n,
true
);
}
}
// ============================================================
// MODULAR CUBE-TO-CUBE CONNECTION
// ============================================================
function drawConnection(
outer,
inner,
connectionIndex
) {
if (!connectionSheets)
return;
const n = Math.max(
6,
Math.round(
(
outer.lines +
inner.lines
)
*
0.5
*
connectionDensity
)
);
const reverse =
(
connectionIndex +
patternPhase
) % 2 === 0;
// ========================================================
// LEVEL 1
// MAIN ARCHITECTURAL SURFACES
// ========================================================
ruledEdges(
outer,
BACK_TOP,
inner,
FRONT_TOP,
n,
reverse
);
ruledEdges(
outer,
FRONT_LEFT,
inner,
BACK_LEFT,
n,
!reverse
);
ruledEdges(
outer,
BACK_RIGHT,
inner,
FRONT_RIGHT,
n,
reverse
);
ruledEdges(
outer,
FRONT_BOTTOM,
inner,
BACK_BOTTOM,
n,
!reverse
);
// ========================================================
// LEVEL 2
// CORNER FANS
// ========================================================
if (connectionComplexity >= 2) {
const O =
outer.p2;
vertexFan(
O[7],
inner,
FRONT_TOP,
n,
false
);
vertexFan(
O[6],
inner,
FRONT_RIGHT,
n,
true
);
vertexFan(
O[0],
inner,
BACK_BOTTOM,
n,
false
);
vertexFan(
O[1],
inner,
BACK_RIGHT,
n,
true
);
}
// ========================================================
// LEVEL 3
// DEPTH SURFACES
// ========================================================
if (connectionComplexity >= 3) {
const d =
Math.max(
6,
Math.round(
n * 0.70
)
);
ruledEdges(
outer,
DEPTH_TL,
inner,
FRONT_LEFT,
d,
!reverse
);
ruledEdges(
outer,
DEPTH_TR,
inner,
BACK_RIGHT,
d,
reverse
);
ruledEdges(
outer,
DEPTH_BL,
inner,
FRONT_BOTTOM,
d,
reverse
);
}
}
// ============================================================
// CENTER FACE HATCH
// ============================================================
function hatchFace(
cube,
face,
count,
cross
) {
const A =
cube.p2[face[0]];
const B =
cube.p2[face[1]];
const C =
cube.p2[face[2]];
const D =
cube.p2[face[3]];
for (
let i = 0;
i <= count;
i++
) {
const t =
i / count;
addLine(
lerp(
A,
B,
t
),
lerp(
D,
C,
1 - t
)
);
}
if (cross) {
for (
let i = 0;
i <= count;
i++
) {
const t =
i / count;
addLine(
lerp(
A,
D,
t
),
lerp(
B,
C,
1 - t
)
);
}
}
}
// ============================================================
// 3D FRAME MICRO-BUNDLE
//
// Multiple lines sit extremely close together in actual
// 3D space so they read as one stronger cube edge.
// ============================================================
function draw3DThickEdge(
A,
B
) {
addLine(
project(A),
project(B)
);
if (
!thickFrames ||
framePasses <= 1 ||
frameRadius <= 0
) {
return;
}
const direction =
normalize3(
sub3(
B,
A
)
);
let reference;
if (
Math.abs(
direction[2]
) < 0.90
) {
reference =
[0, 0, 1];
}
else {
reference =
[0, 1, 0];
}
const U =
normalize3(
cross3(
direction,
reference
)
);
const V =
normalize3(
cross3(
direction,
U
)
);
const shellCount =
framePasses - 1;
for (
let i = 0;
i < shellCount;
i++
) {
const angle =
Math.PI *
2 *
i /
shellCount;
const offset =
add3(
scale3(
U,
Math.cos(angle) *
frameRadius
),
scale3(
V,
Math.sin(angle) *
frameRadius
)
);
addLine(
project(
add3(
A,
offset
)
),
project(
add3(
B,
offset
)
)
);
}
}
// ============================================================
// DRAW FRAME
// ============================================================
function drawFrame(
cube
) {
for (
let i = 0;
i < edges.length;
i++
) {
const e =
edges[i];
draw3DThickEdge(
cube.p3[e[0]],
cube.p3[e[1]]
);
}
}
// ============================================================
// BUILD LOCAL SHEETS
// ============================================================
for (
let i = 0;
i < cubes.length;
i++
) {
drawLocalSheets(
cubes[i],
i
);
}
// ============================================================
// BUILD BETWEEN-CUBE SURFACES
// ============================================================
for (
let i = 0;
i < cubes.length - 1;
i++
) {
drawConnection(
cubes[i],
cubes[i + 1],
i
);
}
// ============================================================
// DENSE INNER CUBE
// ============================================================
if (
centerMesh &&
cubes.length > 0
) {
const center =
cubes[
cubes.length - 1
];
const n =
center.lines;
hatchFace(
center,
FRONT_FACE,
n,
crossHatch
);
hatchFace(
center,
TOP_FACE,
Math.max(
6,
Math.round(
n * 0.80
)
),
crossHatch
);
hatchFace(
center,
RIGHT_FACE,
Math.max(
6,
Math.round(
n * 0.80
)
),
crossHatch
);
}
// ============================================================
// FRAMES LAST
// ============================================================
if (frames) {
for (
let i = 0;
i < cubes.length;
i++
) {
drawFrame(
cubes[i]
);
}
}
// ============================================================
// TURTLETOY WALK
// ============================================================
function walk(i) {
if (
i >= paths.length
) {
return false;
}
const p =
paths[i];
turtle.jump(
p[0][0],
p[0][1]
);
turtle.goto(
p[1][0],
p[1][1]
);
return true;
}