Programming - magic of new world

Rotating Cube Math: How it WorksRotating Coffee-Cup Math: How it Works

01 / 06

[ The Cube Geometry ][ The Coffee-Cup Geometry ]

The cube is not a triangle mesh. Each of its 6 faces is swept directly as a dense point cloud — a 99×99 grid of interior samples per face.

A face is defined by its outward unit normal (sx, sy, sz) — one coordinate is ±1, the rest 0. The same normal pins the face in space: the matching coordinate is fixed at ±0.5 while the other two sweep:

faces: (±1,0,0)  (0,±1,0)  (0,0,±1)

// e.g. the sx=+1 face, u,v ∈ [1..99]:
ox = +0.5              // pinned by the normal
oy = u/100 - 0.5       // ∈ (-0.5, +0.5)
oz = v/100 - 0.5

The cube is centred at the origin with side length 1, so every sample lands in [−0.5, +0.5]³ and carries its face's normal along for lighting (panel 05).

The coffee cup is built from parametric surfaces, not a mesh. Each is swept as a dense point cloud — every sample carries a position and an outward normal.

Two coaxial cylinders form the hollow wall (angle θ, height y) — note the opposite normals:

// outer wall -- normal points OUT
P = (Ro·cosθ, y, Ro·sinθ),  N = ( cosθ, 0,  sinθ)
// inner wall -- normal points IN
P = (Ri·cosθ, y, Ri·sinθ),  N = (-cosθ, 0, -sinθ)

A closed disk seals the base (normal down) so it is a cup, not a tube; an interior floor + top rim finish the bowl:

bottom: P=(r·cosθ, hb,   r·sinθ), N=(0,-1,0)
floor : P=(r·cosθ, hb+t, r·sinθ), N=(0,+1,0)

The handle is half a torus on the +x side (sweep φ ∈ [−90°,90°]) — see panel 05.

x y z 6 faces × 99×99 grid  |  pinned coord ±0.5 = face normal
½ torus N out N in Ro 2 cylinders + closed bottom + ½-torus handle
[ CODE SNAPSHOT ]
var FACES = [[-1,0,0],[1,0,0],[0,-1,0],
             [0,1,0],[0,0,-1],[0,0,1]];
for (var f=0; f<6; f++) {
  var sx=FACES[f][0], sy=FACES[f][1], sz=FACES[f][2];
  for (var u=1; u<=99; u++) {
    for (var v=1; v<=99; v++) {
      // pinned coord = ±0.5, the other two sweep u/v
      var ox = sx ? sx*0.5 : u/100-0.5, …
      // → rotation & transform: panels 02–03
    }
  }
}
// surfaces of revolution → point cloud (pos + normal)
for (th=0; th<TAU; th+=dth) {
  c=cos(th); s=sin(th);
  for (y=hb; y<=ht; y+=dy) {
    add(Ro*c, y, Ro*s,   c,0, s); // outer wall, N out
    add(Ri*c, y, Ri*s,  -c,0,-s); // inner wall, N in
  }
}
add(r*c, hb, r*s, 0,-1,0);        // closed bottom
// + interior floor, top rim, half-torus handle
// → rotation & transform: panels 02–03
02 / 06

[ Rotation Matrices ]

Any 3D rotation is a linear map — a 3×3 orthogonal matrix that preserves lengths and angles.

┌ 1     0        0    ┐
│ 0   cos(a)  -sin(a) │
└ 0   sin(a)   cos(a) ┘
┌  cos(a)  0   sin(a) ┐
│    0     1     0    │
└ -sin(a)  0   cos(a) ┘
┌ cos(a)  -sin(a)  0 ┐
│ sin(a)   cos(a)  0 │
└   0        0     1 ┘

The cube applies Ry(θ)·Rz(45°)·Rx(45°) — matrices act right-to-left: tilt 45° about X, tilt 45° about Z, then the continuous Y-spin.The cup applies Ry(θ)·Rx(−45°) — no Rz, so its axis stays a fixed 45° from vertical while it spins (adding Rz would tip it to 60° and make it precess). 45° bakes in as √2/2 ≈ 0.7071.

X Rx(a) Y Ry(a) Z Rz(a) Combined: M = Ry(θ) · Rz(45°) · Rx(45°) Ry(θ) varies · Rz(45°) fixed tilt · Rx(45°) fixed tilt rightmost acts on the point first Combined: M = Ry(θ) · Rx(−45°) Ry(θ) varies · Rx(−45°) fixed tilt rightmost acts on the point first
[ CODE SNAPSHOT ]
var ct=Math.cos(A), st=Math.sin(A); // pre-compute each frame
for (var f=0; f<6; f++) {        // 6 faces
  for (var u=1; u<=99; u++) {
    for (var v=1; v<=99; v++) {
      … ox/oy/oz from the face sweep …
      // → combined transform: panel 03
    }
  }
}
var ct=Math.cos(A), st=Math.sin(A); // pre-compute each frame
for (var s=0; s<n; s++) {        // every cup sample
  … ox/oy/oz, nx/ny/nz from the point cloud …
  // → combined transform: panel 03
}
03 / 06

[ Multiplying Out M — Row by Row ]

Multiply the two fixed tilts first. Row i of P is (row i of Rz) dotted with each column of Rx. With c = cos45° = sin45° = 1/√2:

┌ c  -c  0 ┐   ┌ 1  0   0 ┐
│ c   c  0 │ · │ 0  c  -c │
└ 0   0  1 ┘   └ 0  c   c ┘

row1 = [c·1+0+0, 0-c·c+0, 0+c·c+0]
     = [ 1/√2   -1/2    1/2  ]
row2 = [c·1+0+0, 0+c·c+0, 0-c·c+0]
     = [ 1/√2    1/2   -1/2  ]
row3 = [ 0+0+0,   0+c+0,   0+c+0 ]
     = [ 0       1/√2   1/√2 ]

Now the spin: each row of Ry dotted with the columns of P (ct = cosθ, st = sinθ):

┌  ct  0  st ┐   ┌ 1/√2  -1/2   1/2  ┐
│   0  1   0 │ · │ 1/√2   1/2  -1/2  │
└ -st  0  ct ┘   └  0     1/√2  1/√2 ┘

row1 = [ct/√2+0, -ct/2+st/√2, ct/2+st/√2]
     = [ ct/√2  (√2·st-ct)/2  (ct+√2·st)/2 ]
row2 = P row2  (Ry row 2 is [0 1 0])
     = [ 1/√2   1/2  -1/2 ]
row3 = [-st/√2+0, st/2+ct/√2, -st/2+ct/√2]
     = [ -st/√2  (st+√2·ct)/2  (√2·ct-st)/2 ]

Row 2 contains no θ — the spin axis is Y, so a point's height never changes as the cube turns.

Each output is one row dotted with the point. Factoring out /2 and writing √2 as 1.414 gives exactly the code:

x0 = row1·v = (1.414·st·(oy+oz) + ct·(-oy+oz+1.414·ox)) / 2
y0 = row2·v = (1.414·ox + oy - oz) / 2
z0 = row3·v = (1.414·ct·(oy+oz) - st·(-oy+oz+1.414·ox)) / 2
   + 1.5   ← camera offset, z only

Face normals (sx,sy,sz) ride the same rows with no +1.5 — directions rotate, never translate. Only rows 2–3 are evaluated for them (→ panel 05).

One fixed tilt, so a single multiply builds M. Row i of Ry is dotted with each column of Rx(−45°). With ct = cosθ, st = sinθ and cos(−45°) = 1/√2, sin(−45°) = −1/√2:

┌  ct  0  st ┐   ┌ 1    0     0    ┐
│   0  1   0 │ · │ 0   1/√2  1/√2 │
└ -st  0  ct ┘   └ 0  -1/√2  1/√2 ┘

row1 = [ct·1+0, 0-st/√2, 0+st/√2]
     = [ ct   -st/√2   st/√2 ]
row2 = [ 0     1/√2    1/√2 ]  (copies Rx row 2)
row3 = [-st·1+0, 0-ct/√2, 0+ct/√2]
     = [ -st  -ct/√2   ct/√2 ]

No Rz — the cup's axis stays a fixed 45° from vertical while it spins; row 2 has no θ, so heights never wobble.

Each output is one row dotted with the point (0.7071 ≈ 1/√2):

x0 = row1·v = ox·ct + 0.7071·(oz-oy)·st
y0 = row2·v = 0.7071·(oy+oz)
z0 = row3·v = -ox·st + 0.7071·(oz-oy)·ct
   + 1.5   ← camera offset, z only

Directions rotate but never translate (no +1.5), and the light has no x-component, so only two rows are ever computed:

ny1 = row2·N = 0.7071·(ny+nz)
nz1 = row3·N = -nx·st + 0.7071·(nz-ny)·ct
M = Ry(θ) · Rz(45°) · Rx(45°) ct/√2 (√2·st−ct)/2 (ct+√2·st)/2 1/√2 1/2 -1/2 -st/√2 (st+√2·ct)/2 (√2·ct−st)/2 ct = cos(A), st = sin(A), A = animation angle row 2 is θ-free — heights never wobble
M = Ry(θ) · Rx(−45°) ct -st/√2 st/√2 0 1/√2 1/√2 -st -ct/√2 ct/√2 ct = cos(A), st = sin(A), A = animation angle row 2 is θ-free — heights never wobble
[ CODE SNAPSHOT ]
var ct=Math.cos(A), st=Math.sin(A);
for (each face f) { for (each u,v) {
  … ox/oy/oz from the face sweep …
  var x0=(1.414*st*(oy+oz)+ct*(-oy+oz+1.414*ox))/2,
      y0=(1.414*ox+oy-oz)/2,
      z0=(1.414*ct*(oy+oz)-st*(-oy+oz+1.414*ox))/2+1.5,
      y1=(1.414*sx+sy-sz)/2,                    // row2·N
      z1=(1.414*ct*(sy+sz)-st*(-sy+sz+1.414*sx))/2; // row3·N
  // → projection: panel 04
}}
var ct=Math.cos(A), st=Math.sin(A);
for (var s=0; s<n; s++) {   // every cup sample
  … ox/oy/oz, nx/ny/nz from the point cloud …
  var x0 = ox*ct + 0.7071*(oz-oy)*st,
      y0 = 0.7071*(oy+oz),
      z0 = -ox*st + 0.7071*(oz-oy)*ct + 1.5,
      ny1 = 0.7071*(ny+nz),                // row2·N
      nz1 = -nx*st + 0.7071*(nz-ny)*ct;    // row3·N
  // → projection: panel 04
}
04 / 06

[ Perspective Projection ]

After rotation, (x0, y0, z0) lands on the character grid by plain scaling — no divide by z. The hero solid is drawn orthographically, so depth never shrinks it:

px = centerCol + scaleX · x0
py = centerRow + scaleY · y0   // +y0 draws downward
px = centerCol + scaleX · x0
py = centerRow - scaleY · y0   // minus: +y0 is up on screen

(The navbar's mini solid does divide by z — 30·x0/z0 — a true pinhole projection in miniature.)

Multiple surface points can hit the same cell. The z-buffer keeps only the closest (largest D = 1/z0); the +1.5 camera offset keeps z0 > 0 so D stays finite and ordered:

D = 1 / z0          // bigger D = closer
if (D > zbuf[o]) {
  zbuf[o] = D
  buf[o] = char     // overwrite what's behind
}

scaleX ≈ COLS×0.22 fills ~half the viewport. scaleY = scaleX/2 corrects the 2:1 aspect ratio of monospace chars.

eye screen P₁ (near) P₂ (far) p₁' p₂' (closer to p₁') z px = centerCol + scaleX · x0  (no ÷z) z0 only breaks ties: D = 1/z0, nearest wins the cell
[ CODE SNAPSHOT ]
… ct/st, face sweep, x0..z1 …
  var px = 0|(centerCol + scaleX*x0),
      py = 0|(centerRow + scaleY*y0),  // ortho: no ÷z
      D  = 1/z0, o = px + stride*py;
  if (D > zbuf[o]) {
    zbuf[o] = D;
    // → per-face luminance N: panel 05
  }
}}
… ct/st, sample loop, x0..z0, ny1/nz1 …
  var px = 0|(centerCol + scaleX*x0),
      py = 0|(centerRow - scaleY*y0),  // minus: +y0 up
      D  = 1/z0, o = px + stride*py;
  if (D > zbuf[o]) {
    zbuf[o] = D;
    // → per-sample luminance N: panel 05
  }
}
05 / 06

[ Lighting & Dot Product ]

Each of the six face sweeps carries a fixed (sx, sy, sz) — one of the six unit axis vectors, the outward normal of the face being swept (panel 01).

Consequence: every sample on the same face shares the same normal. The shading we compute below is therefore per-face, not per-pixel — this is exactly why each face appears as one ASCII character at a time.

Apply the rotation rows from Panel 03 to (sx, sy, sz) — same coefficients, but with the +1.5 translate omitted. Translation moves points; it must never move directions. The rotated normal is N̂ = (x1, y1, z1) — and since the light has no x-component, the code never computes x1 at all.

The cube's fixed light direction is L̂ = (0, 2, 1) / √5 — mostly from above, leaning into the screen. Since both N̂ and L̂ are unit vectors, their dot product is exactly the cosine of the angle between them — the textbook Lambertian shading term:

|L| = √(0² + 2² + 1²) = √5 ≈ 2.236

L̂        = (0, 2/√5, 1/√5)
luminance = N̂ · L̂
          = x1·0 + y1·(2/√5) + z1·(1/√5)
          = (2·y1 + z1) / 2.236   ← matches code

If luminance ≤ 0 the face is tilted away from the light. The cube does not skip it — the value clamps to 0, and after panel 06's inverted ramp that face fills with the densest glyph @. Hidden surfaces are removed only by the 1/z depth buffer.

Every cup sample stores its own outward normal. On the walls it is radial — N = (±cosθ, 0, ±sinθ) (outer “+”, inner “−”); on the handle it sweeps the torus — N = (cosψ·cosφ, cosψ·sinφ, sinψ).

Consequence: unlike the cube, the normal changes at every sample — so shading is a continuous gradient, not one flat value per face.

Apply the cup's rotation Ry(θ)·Rx(−45°) from Panel 03 to the surface normal — identical coefficients, +1.5 translate omitted (directions never translate). The rotated normal is N̂ = (x1, y1, z1).

The cup's fixed light is L̂ = (0, 1, −2) / √5 — from above, toward the viewer (the cube uses a different light: (0,2,1)/√5). Each sample also carries a baked ambient-occlusion factor — inner wall 0.30–0.60, floor 0.35, rim 1.25 — darkening the cavity and popping the lip:

luminance = (N̂ · L̂) · AO
          = ((ny1 - 2·nz1) / 2.236) · AO
if (luminance > 1) luminance = 1   // rim boost can overshoot

Outer wall, inner wall and floor can hit the same pixel; the 1/z depth buffer keeps the nearest, so you see into the cup through the rim while the closed bottom blocks any see-through.

face N̂ (x1,y1,z1) L̂ (0,2,1)/√5 θ cos(θ) = N̂·L̂ = (2·y1+z1) / 2.236 same normal across the face ⇒ per-face shading
N out N in L̂ N̂ = (±cosθ, 0, ±sinθ) · L̂ normal turns with θ ⇒ smooth gradient
[ CODE SNAPSHOT ]
… face sweep: y1/z1, px/py/D/o ready …
  // per-face normal: (sx,sy,sz) constant over each face
  var lum = (2*y1 + z1) / 2.236;     // L̂ = (0,2,1)/√5
  if (D > zbuf[o]) {                 // solid: keep nearest
    zbuf[o] = D;
    // → 12-glyph INVERTED ramp: panel 06
  }
}}
… per-sample pos + normal ready …
  // normal varies per sample -> smooth gradient
  var lum = ((ny1 - 2*nz1) / 2.236) * AO[s];
  if (lum > 1) lum = 1;              // rim boost overshoots
  if (D > zbuf[o]) {                 // depth buffer = hollow cup
    zbuf[o] = D;
    // → 70-step ramp + round: panel 06
  }
}
06 / 06

[ Quantization & the Brightness Ramp ]

The cube uses the classic 12-character “donut” ramp, ordered sparsest ink → densest:

RAMP_CUBE = ".,-~:;!=#&%@"   // 12 glyphs, sparse → dense

The index is flipped — high luminance maps to the start of the ramp. Faces catching the light dissolve into faint dots; faces turning away fill with heavy ink:

last = RAMP_CUBE.length - 1          // 11
i    = last - Math.round(max(lum, 0) · last)
char = RAMP_CUBE[ min(last, i) ]

lum = 1 (lit)   → i = 0    → '.'  sparsest
lum = 0 (away)  → i = 11   → '@'  densest

A cube face is flat — one normal, one luminance, one glyph for the whole face. 12 coarse levels + the inverted map give the dark theme its engraved, retro pulse as faces snap between shades. A clamped lum ≤ 0 face renders solid @ — never a hole — and the z-buffer alone hides back faces.

Order 70 characters from sparsest ink (dim) to densest (bright). In a monospace font each glyph fills a different fraction of its cell (. ≈ 5% → $ ≈ 80%) and the eye reads ink density as brightness:

RAMP = ".'`^\",:;Il!i><~+-?][}{1)(|\/
tfjrxnuvczXYUJCLQ0OZmwqpdbkhao
*#MW&8%B@$"     // one 70-glyph string

Scale luminance [0,1] across the ramp and round to the nearest level (not floor), then clamp:

i    = Math.round(lum · (RAMP.length - 1))
char = RAMP[ min(RAMP.length - 1, max(0, i)) ]

Rounding removes the half-step bias of a 0 | x floor. Curved normals + the AO factor vary luminance continuously, and 70 levels resolve it into a true gradient around the cup's curve.

When luminance ≤ 0 a surface faces away from the light; we draw the dimmest glyph . instead of a hole, and the depth buffer keeps the nearest surface — so the silhouette stays solid.

L=1 (lit) L=0 (away) . , - ~ : ; ! = # & % @ i = 11 − round(L·11) ← INVERTED lit face → . , - (sparse ink) half-lit → ~ : ; ! = away → # & % @ (heavy ink) 12 flat levels → the stepped, engraved pulse L ≤ 0: densest glyph '@' (solid kept)
L=0 (dim) L=1 (bright) . : i ? ( t x X Q m % $ 0 1 i = round(L·69) — 70 levels, direct dim → . : - ~ + ? c n x z o a o a h k b d q w m Z O 0 Q W # M W & 8 % B @ $ ← bright fine increments — a smooth continuous gradient L ≤ 0: dimmest glyph '.' (solid kept)
[ CODE SNAPSHOT ]
… face sweep; y1/z1, D, o ready …
  var lum = (2*y1 + z1)/2.236, last = RAMP_CUBE.length-1;
  if (D > zbuf[o]) {
    zbuf[o] = D;
    var i = last - Math.round((lum > 0 ? lum : 0) * last);
    buf[o] = RAMP_CUBE.charAt(i > last ? last : i); // inverted
  }
}
pretag.textContent = buf.join(''); // render frame to DOM
… sample loop; ny1/nz1, D, o ready …
  var lum = ((ny1 - 2*nz1)/2.236) * AO[s];
  if (lum > 1) lum = 1;
  if (D > zbuf[o]) {
    zbuf[o] = D;
    var i = Math.round((lum > 0 ? lum : 0) * last);
    buf[o] = RAMP.charAt(i > last ? last : i); // 70 levels
  }
}
pretag.textContent = buf.join(''); // render frame to DOM
[ DB-System Explorer ]
the real /db explorer of my market terminal · 13,000+ catalogued series (curated sample embedded) · search · transform · combine · rebase
loading…
Data
[ Trackers — ADR Premium Lab ]
the real /trackers page of my terminal · 1-minute bars · the join / price / fx toolbar re-runs the pair merge on raw embedded bars
loading…
Folders
Tracked data
Select a tracker
days X Grey bands = leg not live / carried forward · keys: 1-9 range · ←/→ switch chart (★ = only starred) · J join · X fx · G gaps · L legs · T tools
Y Y2 Draw
Click chart to place · trend: click, follow the preview, click again · drag a trend line or its endpoints to edit · Alt+click a trend to delete · click other labels to delete · Esc cancels
Latest detail

[ Quant Analytics — OLS & Correlation ]
regression & correlation lab on the terminal's daily series · — · every statistic computed live in your browser
loading…
Y
X₁ X₂
Returns
Corr
Scatter — returns + OLS fit
Rebased performance window start = 100
Correlation matrix
Rolling β & ρ
Model summary — OLS
Single-factor screen — Y on each X click a row to set X₁
[ US Treasury Yield Surface — 3D ]
tenor × time × yield from the terminal's daily Treasury curve · — · drag to rotate · right-drag to pan · wheel / pinch to zoom
loading…
View
Opacity 0.50
Yield surface lines = tenors (click the legend to show / hide) · grey = yearly curve grid · holes = tenor not quoted in that period (never interpolated)

Get in touch

> connect_

Quant · Python · Crypto · Open to collab