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;
}