]> nyquistdev.net Git - basic-3d-example.git/commitdiff
first commit rasterization
authorA-Larsen <austin.larsen@mailfence.com>
Thu, 15 Oct 2026 01:08:58 +0000 (21:08 -0400)
committerA-Larsen <austin.larsen@mailfence.com>
Thu, 15 Oct 2026 01:08:58 +0000 (21:08 -0400)
18 files changed:
README.adoc [new file with mode: 0644]
docinfo.conf [new file with mode: 0644]
docs/chatgpt-dump/Makehtml [new file with mode: 0755]
docs/chatgpt-dump/camera.html [new file with mode: 0644]
docs/chatgpt-dump/camera.md [new file with mode: 0644]
docs/chatgpt-dump/euler-angle.html [new file with mode: 0644]
docs/chatgpt-dump/euler-angle.md [new file with mode: 0644]
docs/chatgpt-dump/quaternion-rotation.png [new file with mode: 0644]
docs/chatgpt-dump/quaternions.html [new file with mode: 0644]
docs/chatgpt-dump/quaternions.md [new file with mode: 0644]
docs/chatgpt-dump/rendered.html [new file with mode: 0644]
example.html [new file with mode: 0644]
index.html [new file with mode: 0644]
index.js [new file with mode: 0644]
look-at(getViewMatrix).png [new file with mode: 0644]
notes.adoc [new file with mode: 0644]
reference.js [new file with mode: 0644]
rendered.html [new file with mode: 0644]

diff --git a/README.adoc b/README.adoc
new file mode 100644 (file)
index 0000000..ac1ab78
--- /dev/null
@@ -0,0 +1,420 @@
+= Basic 3D Example
+
+click https://nyquistdev.net/git/?p=basic-3d-example.git;a=blob_plain;f=example.html;hb=HEAD"[here] for an interactive example
+
+A good explaination of how 3D works.
+
+NOTE: This started off as code from Tsoding, I used chatgpt to change
+      specific things, and also made some of my own changes. This is a learning
+      tool! not something I wrote off the top of my head
+
+== Graphics Pipeline
+
+----
+vertex
+ -> rotate cube
+ -> translate cube (object position)
+ -> apply camera (inverse transform)
+ -> project
+ -> screen
+----
+
+NOTE: Some parts of these pipeline may be left out if that part of the pipeline
+      is not needed
+
+== Basic Projection Formula
+
+Projection formula for a vector latexmath:[[x,y,z]]:
+
+[latexmath]
+++++
+\[
+\begin{aligned}
+x' &= x/z \\
+y' &= y/z
+\end{aligned}
+\]
+++++
+
+== Vertex Rotation Formulas
+click https://nyquistdev.net/git/?p=basic-3d-example.git;a=blob_plain;f=example.html;hb=refs/heads/simple-rotation[here] for an interactive example
+
+Rotate vector formula for x-axis (y-z plane):
+
+[latexmath]
+++++
+\[
+\begin{aligned}
+x' &= x \\
+y' &= y\cos\theta - z\sin\theta \\
+z' &= y\sin\theta + z\cos\theta
+\end{aligned}
+\]
+++++
+
+Rotate vector formula for y-axis (x-z plane):
+
+[latexmath]
+++++
+\[
+\begin{aligned}
+x' &= x\cos\theta - z\sin\theta \\
+y' &= y \\
+z' &= x\sin\theta + z\cos\theta
+\end{aligned} \]
+++++
+
+Rotate vector formula for z-axis (x-y plane):
+
+[latexmath]
+++++
+\[
+\begin{aligned}
+x' &= x\cos\theta - y\sin\theta \\
+y' &= x\sin\theta + y\cos\theta \\
+z' &=  z
+\end{aligned}
+\]
+++++
+
+== 2. Single-axis rotation matrices (Euler Angles)
+
+=== Rotation about X-axis (roll)
+
+[latexmath]
+++++
+\boxed{
+R_x(\alpha) =
+\begin{bmatrix}
+1 & 0 & 0 \\
+0 & \cos\alpha & -\sin\alpha \\
+0 & \sin\alpha & \cos\alpha
+\end{bmatrix}
+}
+++++
+
+=== Rotation about Y-axis (pitch)
+
+[latexmath]
+++++
+\boxed{
+R_y(\beta) =
+\begin{bmatrix}
+\cos\beta & 0 & \sin\beta \\
+0 & 1 & 0 \\
+-\sin\beta & 0 & \cos\beta
+\end{bmatrix}
+}
+++++
+
+=== Rotation about Z-axis (yaw)
+
+[latexmath]
+++++
+\boxed{
+R_z(\gamma) =
+\begin{bmatrix}
+\cos\gamma & -\sin\gamma & 0 \\
+\sin\gamma & \cos\gamma & 0 \\
+0 & 0 & 1 
+\end{bmatrix}
+}
+++++
+
+
+Euler angles will always have gimbal lock. You can switch which axis has gimbal
+lock by switching the order. Most application use quaternions instead of eular
+angles.
+
+== Quaternions
+click https://nyquistdev.net/git/?p=basic-3d-example.git;a=blob_plain;f=example.html;hb=refs/heads/quaternions[here] for an interactive example
+
+Quaternions allow you to rotate smoothly around multiple axes without gimbal
+lock. Chat-gpt rewrote the code to use quaternions. Here are some of the
+replys about https://nyquistdev.net/git/?p=basic-3d-example.git;a=blob_plain;f=docs/chatgpt-dump/quaternions.html;hb=HEAD[quaternions].
+
+=== 1. What is a quaternion (in graphics)?
+
+A quaternion is a 4-component number:
+
+[latexmath]
+++++
+q = (w, x, y, z)
+++++
+
+In 3D graphics, we use **unit quaternions** to represent **rotations**.
+
+A rotation by:
+
+* angle latexmath:[\theta]
+* around a **unit axis** latexmath:[\vec{u} = (u_x, u_y, u_z)]
+
+is encoded as:
+
+[latexmath]
+++++
+\boxed{
+q = \left(
+\cos\frac{\theta}{2},
+\; u_x \sin\frac{\theta}{2},
+\; u_y \sin\frac{\theta}{2},
+\; u_z \sin\frac{\theta}{2}
+\right)
+}
+++++
+
+This is the **axis–angle -> quaternion** formula.
+
+==== Why half-angle?
+Because quaternion rotation comes from complex numbers and Euler’s formula —
+doubling happens naturally in the math.
+
+=== 2. quatFromAxisAngle
+
+[source, js]
+----
+function quatFromAxisAngle(axis, angle) {
+    const h = angle / 2;
+    const s = Math.sin(h);
+    return [Math.cos(h), axis.x*s, axis.y*s, axis.z*s];
+}
+----
+
+You’re directly implementing:
+
+[latexmath]
+++++
+q = (\cos(\theta/2), \vec{u}\sin(\theta/2))
+++++
+
+* latexmath:[h = angle / 2] -> half-angle
+* latexmath:[cos(h)] → scalar part (w)
+* latexmath:[axis * sin(h)] → vector part latexmath:[(x,y,z)]
+
+IMPORTANT: `axis` **must be normalized** (length = 1), otherwise rotation will
+           scale or skew.
+
+=== 3. Quaternion multiplication (rotation composition)
+
+[source, js]
+----
+function quatMultiply(a, b) {
+    const [w1,x1,y1,z1] = a;
+    const [w2,x2,y2,z2] = b;
+    return [
+        w1*w2 - x1*x2 - y1*y2 - z1*z2,
+        w1*x2 + x1*w2 + y1*z2 - z1*y2,
+        w1*y2 - x1*z2 + y1*w2 + z1*x2,
+        w1*z2 + x1*y2 - y1*x2 + z1*w2
+    ];
+}
+----
+
+formula:
+
+[latexmath]
+++++
+q_1 = (w_1, \vec{v}_1), \quad q_2 = (w_2, \vec{v}_2)
+++++
+
+Quaternion multiplication is:
+
+[latexmath]
+++++
+\boxed{
+q_1 q_2 =
+\left(
+w_1 w_2 - \vec{v}_1 \cdot \vec{v}_2,\;
+w_1 \vec{v}_2 + w_2 \vec{v}_1 + \vec{v}_1 \times \vec{v}_2
+\right)
+}
+++++
+
+Expanded component-wise -> exactly your code.
+
+==== Why this matters
+
+* Quaternion multiplication **combines rotations**
+* Order matters:
+
+[latexmath]
+++++
+q_{\text{final}} = q_{\text{new}} \cdot q_{\text{current}}
+++++
+
+This avoids **gimbal lock**, unlike Euler angles.
+
+=== 4. Quaternion conjugate
+
+[source, js]
+----
+function quatConjugate(q) {
+    return [q[0], -q[1], -q[2], -q[3]];
+}
+----
+
+For:
+
+[latexmath]
+++++
+q = (w, x, y, z)
+++++
+
+The conjugate is:
+
+[latexmath]
+++++
+\boxed{
+q^* = (w, -x, -y, -z)
+}
+++++
+
+=== Why this works
+
+If (q) is a **unit quaternion**:
+
+[latexmath]
+++++
+q^{-1} = q^*
+++++
+
+So conjugate = **inverse rotation**.
+
+This is why cameras use conjugates — moving the world opposite to the camera
+rotation.
+
+=== 5. Rotating a vector using quaternions
+
+[source, js]
+----
+function quatRotateVector(q, v) {
+    const qv = [0, v.x, v.y, v.z];
+    const r = quatMultiply(quatMultiply(q, qv), quatConjugate(q));
+    return { x: r[1], y: r[2], z: r[3] };
+}
+----
+
+The key formula
+
+To rotate a vector latexmath:[\vec{v}]:
+
+1. Convert it to a **pure quaternion**
+
+[latexmath]
+++++
+v_q = (0, v_x, v_y, v_z)
+++++
+
+2. Apply:
+
+[latexmath]
+++++
+\boxed{
+v' = q \; v_q \; q^{-1}
+}
+++++
+
+3. Extract the vector part
+
+==== Why this works
+
+Quaternion rotation encodes:
+
+* axis
+* angle
+* orientation
+
+The sandwich product rotates without:
+
+* scaling
+* skewing
+* drifting
+
+=== Visual intuition
+
+A rotation quaternion represents:
+
+[latexmath]
+++++
+q = \cos(\theta/2) + \sin(\theta/2)(u_x i + u_y j + u_z k)
+++++
+
+This is the 3D analog of:
+
+[latexmath]
+++++
+e^{i\theta} = \cos\theta + i\sin\theta
+++++
+
+== Camera
+
+chat-gpt replys about https://nyquistdev.net/git/?p=basic-3d-example.git;a=blob_plain;f=docs/chatgpt-dump/camera.html;hb=HEAD[camera].
+
+== Simple clickable object
+
+click https://nyquistdev.net/git/?p=basic-3d-example.git;a=blob_plain;f=example.html;hb=refs/heads/clickable[here] for an interactive example
+
+This does not account for the rotation of the object but it does check to see if
+your mouse is within the rectangular bounds of the mesh.
+
+1. finding the min and max values for x and y of the mesh:
+
+[source, js]
+----
+function computeBounds(mesh) {
+    let min = {x: Infinity, y: Infinity}
+    let max = {x: -Infinity, y: -Infinity}
+
+    for (const v of mesh) {
+        min.x = Math.min(min.x, v.x)
+        min.y = Math.min(min.y, v.y)
+
+        max.x = Math.max(max.x, v.x)
+        max.y = Math.max(max.y, v.y)
+    }
+    return {min, max}
+}
+----
+
+2. Define the box corners of the rect for the mesh
+
+[source, js]
+----
+function boundingBoxCorners({min, max}) {
+    return {
+        bottom: {left: {x: min.x, y: max.y}, right: {x: max.x, y: max.y}},
+        top: {left: {x: min.x, y: min.y}, right: {x: max.x, y: min.y}},
+
+    }
+}
+----
+
+3. Check to see if the mouse is in the rect in the game loop. If it is, and the
+   mouse button is down, then you can move the object
+
+[source, js]
+----
+inObject =  (
+    mousePos.x > rect.top.left.x 
+    &&
+    mousePos.x < rect.top.right.x 
+    &&
+    mousePos.y > rect.top.left.y
+    &&
+    mousePos.y < rect.bottom.right.y
+) 
+
+if (mouseDown && inObject) canMoveObject = true
+
+if (!mouseDown) canMoveObject = false
+----
+
+== Links
+
+* https://www.youtube.com/watch?v=qjWkNZ0SXfo[One Formula That demystifies 3D Graphics]
+* https://www.youtube.com/watch?v=EZufiIwwqFA[Rotation matrix derivation (step-by-step prove)]
+* https://matthew-brett.github.io/teaching/rotation_2d.html[rotation 2D by mathew]
+* https://www.youtube.com/watch?v=zc8b2Jo7mno[gimbal lock visual representation]
+* https://www.youtube.com/watch?v=zjMuIxRvygQ[3Blue1Brown | Quaternions and 3d rotation, explained interactively]
+* https://www.youtube.com/watch?v=d4EgbgTm0Bg&t=17s[3Blue1Brown | Visualizing the 4d numbers Quanternions]
+* https://phas.ubc.ca/~berciu/TEACHING/PHYS206/LECTURES/FILES/euler.pdf[euler.pdf]
diff --git a/docinfo.conf b/docinfo.conf
new file mode 100644 (file)
index 0000000..fc4596c
--- /dev/null
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+HOSTNAME=nyquistdev.net
+DOCINFODIR=/usr/local/share/asciidoctor-styles/dark1/
diff --git a/docs/chatgpt-dump/Makehtml b/docs/chatgpt-dump/Makehtml
new file mode 100755 (executable)
index 0000000..615bffe
--- /dev/null
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+set -e
+
+PANDOC_OPTS=(
+  --from markdown+smart
+  --to html
+  --standalone
+  --metadata charset=utf-8
+  --mathjax
+)
+
+for md in *.md; do
+  html="${md%.md}.html"
+  pandoc "$md" "${PANDOC_OPTS[@]}" -o "$html" --mathjax
+done
diff --git a/docs/chatgpt-dump/camera.html b/docs/chatgpt-dump/camera.html
new file mode 100644 (file)
index 0000000..833aff7
--- /dev/null
@@ -0,0 +1,1122 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" lang="" xml:lang="">
+<head>
+  <meta charset="utf-8" />
+  <meta name="generator" content="pandoc" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
+  <title>camera</title>
+  <style>
+    html {
+      color: #1a1a1a;
+      background-color: #fdfdfd;
+    }
+    body {
+      margin: 0 auto;
+      max-width: 36em;
+      padding-left: 50px;
+      padding-right: 50px;
+      padding-top: 50px;
+      padding-bottom: 50px;
+      hyphens: auto;
+      overflow-wrap: break-word;
+      text-rendering: optimizeLegibility;
+      font-kerning: normal;
+    }
+    @media (max-width: 600px) {
+      body {
+        font-size: 0.9em;
+        padding: 12px;
+      }
+      h1 {
+        font-size: 1.8em;
+      }
+    }
+    @media print {
+      html {
+        background-color: white;
+      }
+      body {
+        background-color: transparent;
+        color: black;
+        font-size: 12pt;
+      }
+      p, h2, h3 {
+        orphans: 3;
+        widows: 3;
+      }
+      h2, h3, h4 {
+        page-break-after: avoid;
+      }
+    }
+    p {
+      margin: 1em 0;
+    }
+    a {
+      color: #1a1a1a;
+    }
+    a:visited {
+      color: #1a1a1a;
+    }
+    img {
+      max-width: 100%;
+    }
+    h1, h2, h3, h4, h5, h6 {
+      margin-top: 1.4em;
+    }
+    h5, h6 {
+      font-size: 1em;
+      font-style: italic;
+    }
+    h6 {
+      font-weight: normal;
+    }
+    ol, ul {
+      padding-left: 1.7em;
+      margin-top: 1em;
+    }
+    li > ol, li > ul {
+      margin-top: 0;
+    }
+    blockquote {
+      margin: 1em 0 1em 1.7em;
+      padding-left: 1em;
+      border-left: 2px solid #e6e6e6;
+      color: #606060;
+    }
+    code {
+      font-family: Menlo, Monaco, Consolas, 'Lucida Console', monospace;
+      font-size: 85%;
+      margin: 0;
+      hyphens: manual;
+    }
+    pre {
+      margin: 1em 0;
+      overflow: auto;
+    }
+    pre code {
+      padding: 0;
+      overflow: visible;
+      overflow-wrap: normal;
+    }
+    .sourceCode {
+     background-color: transparent;
+     overflow: visible;
+    }
+    hr {
+      background-color: #1a1a1a;
+      border: none;
+      height: 1px;
+      margin: 1em 0;
+    }
+    table {
+      margin: 1em 0;
+      border-collapse: collapse;
+      width: 100%;
+      overflow-x: auto;
+      display: block;
+      font-variant-numeric: lining-nums tabular-nums;
+    }
+    table caption {
+      margin-bottom: 0.75em;
+    }
+    tbody {
+      margin-top: 0.5em;
+      border-top: 1px solid #1a1a1a;
+      border-bottom: 1px solid #1a1a1a;
+    }
+    th {
+      border-top: 1px solid #1a1a1a;
+      padding: 0.25em 0.5em 0.25em 0.5em;
+    }
+    td {
+      padding: 0.125em 0.5em 0.25em 0.5em;
+    }
+    header {
+      margin-bottom: 4em;
+      text-align: center;
+    }
+    #TOC li {
+      list-style: none;
+    }
+    #TOC ul {
+      padding-left: 1.3em;
+    }
+    #TOC > ul {
+      padding-left: 0;
+    }
+    #TOC a:not(:hover) {
+      text-decoration: none;
+    }
+    code{white-space: pre-wrap;}
+    span.smallcaps{font-variant: small-caps;}
+    div.columns{display: flex; gap: min(4vw, 1.5em);}
+    div.column{flex: auto; overflow-x: auto;}
+    div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;}
+    /* The extra [class] is a hack that increases specificity enough to
+       override a similar rule in reveal.js */
+    ul.task-list[class]{list-style: none;}
+    ul.task-list li input[type="checkbox"] {
+      font-size: inherit;
+      width: 0.8em;
+      margin: 0 0.8em 0.2em -1.6em;
+      vertical-align: middle;
+    }
+    /* CSS for syntax highlighting */
+    pre > code.sourceCode { white-space: pre; position: relative; }
+    pre > code.sourceCode > span { line-height: 1.25; }
+    pre > code.sourceCode > span:empty { height: 1.2em; }
+    .sourceCode { overflow: visible; }
+    code.sourceCode > span { color: inherit; text-decoration: inherit; }
+    div.sourceCode { margin: 1em 0; }
+    pre.sourceCode { margin: 0; }
+    @media screen {
+    div.sourceCode { overflow: auto; }
+    }
+    @media print {
+    pre > code.sourceCode { white-space: pre-wrap; }
+    pre > code.sourceCode > span { text-indent: -5em; padding-left: 5em; }
+    }
+    pre.numberSource code
+      { counter-reset: source-line 0; }
+    pre.numberSource code > span
+      { position: relative; left: -4em; counter-increment: source-line; }
+    pre.numberSource code > span > a:first-child::before
+      { content: counter(source-line);
+        position: relative; left: -1em; text-align: right; vertical-align: baseline;
+        border: none; display: inline-block;
+        -webkit-touch-callout: none; -webkit-user-select: none;
+        -khtml-user-select: none; -moz-user-select: none;
+        -ms-user-select: none; user-select: none;
+        padding: 0 4px; width: 4em;
+        color: #aaaaaa;
+      }
+    pre.numberSource { margin-left: 3em; border-left: 1px solid #aaaaaa;  padding-left: 4px; }
+    div.sourceCode
+      {   }
+    @media screen {
+    pre > code.sourceCode > span > a:first-child::before { text-decoration: underline; }
+    }
+    code span.al { color: #ff0000; font-weight: bold; } /* Alert */
+    code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */
+    code span.at { color: #7d9029; } /* Attribute */
+    code span.bn { color: #40a070; } /* BaseN */
+    code span.bu { color: #008000; } /* BuiltIn */
+    code span.cf { color: #007020; font-weight: bold; } /* ControlFlow */
+    code span.ch { color: #4070a0; } /* Char */
+    code span.cn { color: #880000; } /* Constant */
+    code span.co { color: #60a0b0; font-style: italic; } /* Comment */
+    code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */
+    code span.do { color: #ba2121; font-style: italic; } /* Documentation */
+    code span.dt { color: #902000; } /* DataType */
+    code span.dv { color: #40a070; } /* DecVal */
+    code span.er { color: #ff0000; font-weight: bold; } /* Error */
+    code span.ex { } /* Extension */
+    code span.fl { color: #40a070; } /* Float */
+    code span.fu { color: #06287e; } /* Function */
+    code span.im { color: #008000; font-weight: bold; } /* Import */
+    code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */
+    code span.kw { color: #007020; font-weight: bold; } /* Keyword */
+    code span.op { color: #666666; } /* Operator */
+    code span.ot { color: #007020; } /* Other */
+    code span.pp { color: #bc7a00; } /* Preprocessor */
+    code span.sc { color: #4070a0; } /* SpecialChar */
+    code span.ss { color: #bb6688; } /* SpecialString */
+    code span.st { color: #4070a0; } /* String */
+    code span.va { color: #19177c; } /* Variable */
+    code span.vs { color: #4070a0; } /* VerbatimString */
+    code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */
+  </style>
+</head>
+<body>
+<p>Yes 👍 You’re <em>very close already</em>. What you currently have is
+<strong>object movement</strong>, not a camera. We’ll add a <strong>real
+camera</strong> while changing as little of your code as possible.</p>
+<p>I’ll do this in <strong>small, precise steps</strong>, and I’ll
+clearly mark <strong>what to add</strong> and <strong>what to
+replace</strong>.</p>
+<hr />
+<h1 id="what-adding-a-camera-means-in-your-code">1️⃣ What “adding a
+camera” means in your code</h1>
+<p>Right now your pipeline is:</p>
+<pre><code>vertex
+ → rotate cube
+ → translate cube (position)
+ → project
+ → screen</code></pre>
+<p>With a camera, it becomes:</p>
+<pre><code>vertex
+ → rotate cube
+ → translate cube (object position)
+ → apply camera (inverse transform)
+ → project
+ → screen</code></pre>
+<p>So we need:</p>
+<ul>
+<li>a <strong>camera position</strong></li>
+<li>a <strong>camera rotation (quaternion)</strong></li>
+<li>a function that applies the <strong>inverse camera
+transform</strong></li>
+</ul>
+<hr />
+<h1 id="add-a-camera-object-new">2️⃣ Add a camera object (NEW)</h1>
+<p>Add this near your <code>position</code> variable:</p>
+<div class="sourceCode" id="cb3"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> camera <span class="op">=</span> {</span>
+<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a>    <span class="dt">position</span><span class="op">:</span> { <span class="dt">x</span><span class="op">:</span> <span class="dv">0</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span> <span class="dv">0</span><span class="op">,</span> <span class="dt">z</span><span class="op">:</span> <span class="dv">0</span> }<span class="op">,</span></span>
+<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a>    <span class="dt">rotation</span><span class="op">:</span> [<span class="dv">1</span><span class="op">,</span> <span class="dv">0</span><span class="op">,</span> <span class="dv">0</span><span class="op">,</span> <span class="dv">0</span>] <span class="co">// identity quaternion</span></span>
+<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a>}<span class="op">;</span></span></code></pre></div>
+<hr />
+<h1 id="camera-helper-functions-new">3️⃣ Camera helper functions
+(NEW)</h1>
+<p>Add these <strong>below your quaternion functions</strong>:</p>
+<div class="sourceCode" id="cb4"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">quatConjugate</span>(q) {</span>
+<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> [q[<span class="dv">0</span>]<span class="op">,</span> <span class="op">-</span>q[<span class="dv">1</span>]<span class="op">,</span> <span class="op">-</span>q[<span class="dv">2</span>]<span class="op">,</span> <span class="op">-</span>q[<span class="dv">3</span>]]<span class="op">;</span></span>
+<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">applyCamera</span>(p<span class="op">,</span> cam) {</span>
+<span id="cb4-6"><a href="#cb4-6" aria-hidden="true" tabindex="-1"></a>    <span class="co">// inverse translation</span></span>
+<span id="cb4-7"><a href="#cb4-7" aria-hidden="true" tabindex="-1"></a>    <span class="kw">let</span> v <span class="op">=</span> {</span>
+<span id="cb4-8"><a href="#cb4-8" aria-hidden="true" tabindex="-1"></a>        <span class="dt">x</span><span class="op">:</span> p<span class="op">.</span><span class="at">x</span> <span class="op">-</span> cam<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">x</span><span class="op">,</span></span>
+<span id="cb4-9"><a href="#cb4-9" aria-hidden="true" tabindex="-1"></a>        <span class="dt">y</span><span class="op">:</span> p<span class="op">.</span><span class="at">y</span> <span class="op">-</span> cam<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">y</span><span class="op">,</span></span>
+<span id="cb4-10"><a href="#cb4-10" aria-hidden="true" tabindex="-1"></a>        <span class="dt">z</span><span class="op">:</span> p<span class="op">.</span><span class="at">z</span> <span class="op">-</span> cam<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">z</span></span>
+<span id="cb4-11"><a href="#cb4-11" aria-hidden="true" tabindex="-1"></a>    }<span class="op">;</span></span>
+<span id="cb4-12"><a href="#cb4-12" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb4-13"><a href="#cb4-13" aria-hidden="true" tabindex="-1"></a>    <span class="co">// inverse rotation</span></span>
+<span id="cb4-14"><a href="#cb4-14" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> invRot <span class="op">=</span> <span class="fu">quatConjugate</span>(cam<span class="op">.</span><span class="at">rotation</span>)<span class="op">;</span></span>
+<span id="cb4-15"><a href="#cb4-15" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> <span class="fu">quatRotateVector</span>(invRot<span class="op">,</span> v)<span class="op">;</span></span>
+<span id="cb4-16"><a href="#cb4-16" aria-hidden="true" tabindex="-1"></a>}</span></code></pre></div>
+<p>Why this works:</p>
+<ul>
+<li>Camera movement = <strong>world moves opposite</strong></li>
+<li>Camera rotation = <strong>world rotates inverse</strong></li>
+</ul>
+<hr />
+<h1 id="modify-your-render-loop-important">4️⃣ Modify your render loop
+(IMPORTANT)</h1>
+<h3 id="old-code-inside-frame">❌ OLD code (inside
+<code>frame()</code>):</h3>
+<div class="sourceCode" id="cb5"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="fu">line</span>(</span>
+<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a>    <span class="bu">screen</span>(<span class="fu">project</span>(<span class="fu">translate</span>(<span class="fu">quatRotateVector</span>(q<span class="op">,</span> a)<span class="op">,</span> position)))<span class="op">,</span></span>
+<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a>    <span class="bu">screen</span>(<span class="fu">project</span>(<span class="fu">translate</span>(<span class="fu">quatRotateVector</span>(q<span class="op">,</span> b)<span class="op">,</span> position)))</span>
+<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a>)<span class="op">;</span></span></code></pre></div>
+<h3 id="replace-with-this">✅ REPLACE with this:</h3>
+<div class="sourceCode" id="cb6"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> ra <span class="op">=</span> <span class="fu">translate</span>(<span class="fu">quatRotateVector</span>(q<span class="op">,</span> a)<span class="op">,</span> position)<span class="op">;</span></span>
+<span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> rb <span class="op">=</span> <span class="fu">translate</span>(<span class="fu">quatRotateVector</span>(q<span class="op">,</span> b)<span class="op">,</span> position)<span class="op">;</span></span>
+<span id="cb6-3"><a href="#cb6-3" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb6-4"><a href="#cb6-4" aria-hidden="true" tabindex="-1"></a>ra <span class="op">=</span> <span class="fu">applyCamera</span>(ra<span class="op">,</span> camera)<span class="op">;</span></span>
+<span id="cb6-5"><a href="#cb6-5" aria-hidden="true" tabindex="-1"></a>rb <span class="op">=</span> <span class="fu">applyCamera</span>(rb<span class="op">,</span> camera)<span class="op">;</span></span>
+<span id="cb6-6"><a href="#cb6-6" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb6-7"><a href="#cb6-7" aria-hidden="true" tabindex="-1"></a><span class="fu">line</span>(</span>
+<span id="cb6-8"><a href="#cb6-8" aria-hidden="true" tabindex="-1"></a>    <span class="bu">screen</span>(<span class="fu">project</span>(ra))<span class="op">,</span></span>
+<span id="cb6-9"><a href="#cb6-9" aria-hidden="true" tabindex="-1"></a>    <span class="bu">screen</span>(<span class="fu">project</span>(rb))</span>
+<span id="cb6-10"><a href="#cb6-10" aria-hidden="true" tabindex="-1"></a>)<span class="op">;</span></span></code></pre></div>
+<p>That’s the <strong>entire camera integration</strong>.</p>
+<hr />
+<h1 id="move-the-camera-instead-of-the-cube">5️⃣ Move the camera (instead
+of the cube)</h1>
+<p>Right now your scroll code moves <code>position</code>. We’ll change
+it to move <strong>the camera instead</strong>.</p>
+<hr />
+<h2 id="forward-backward-scroll">5.1 Forward / backward (scroll)</h2>
+<h3 id="old">❌ OLD:</h3>
+<div class="sourceCode" id="cb7"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a>position<span class="op">.</span><span class="at">z</span> <span class="op">+=</span> <span class="op">.</span><span class="bn">03</span><span class="op">;</span></span>
+<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a>position<span class="op">.</span><span class="at">z</span> <span class="op">-=</span> <span class="op">.</span><span class="bn">03</span><span class="op">;</span></span></code></pre></div>
+<h3 id="replace-with">✅ REPLACE with:</h3>
+<div class="sourceCode" id="cb8"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a>camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">z</span> <span class="op">+=</span> <span class="op">.</span><span class="bn">03</span><span class="op">;</span></span>
+<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a>camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">z</span> <span class="op">-=</span> <span class="op">.</span><span class="bn">03</span><span class="op">;</span></span></code></pre></div>
+<hr />
+<h2 id="left-right-shift-scroll">5.2 Left / right (shift + scroll)</h2>
+<h3 id="old-1">❌ OLD:</h3>
+<div class="sourceCode" id="cb9"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a>position<span class="op">.</span><span class="at">x</span> <span class="op">+=</span> <span class="op">.</span><span class="bn">004</span><span class="op">;</span></span>
+<span id="cb9-2"><a href="#cb9-2" aria-hidden="true" tabindex="-1"></a>position<span class="op">.</span><span class="at">x</span> <span class="op">-=</span> <span class="op">.</span><span class="bn">004</span><span class="op">;</span></span></code></pre></div>
+<h3 id="replace-with-1">✅ REPLACE with:</h3>
+<div class="sourceCode" id="cb10"><pre
+class="sourceCode js"><code class="sourceCode javascript"><span id="cb10-1"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a>camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">x</span> <span class="op">+=</span> <span class="op">.</span><span class="bn">04</span><span class="op">;</span></span>
+<span id="cb10-2"><a href="#cb10-2" aria-hidden="true" tabindex="-1"></a>camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">x</span> <span class="op">-=</span> <span class="op">.</span><span class="bn">04</span><span class="op">;</span></span></code></pre></div>
+<p>Also remove this loop (it does nothing useful):</p>
+<div class="sourceCode" id="cb11"><pre
+class="sourceCode js"><code class="sourceCode javascript"><span id="cb11-1"><a href="#cb11-1" aria-hidden="true" tabindex="-1"></a><span class="cf">for</span> (<span class="kw">const</span> v <span class="kw">of</span> vs) { <span class="op">...</span> }</span></code></pre></div>
+<p>You should <strong>never modify vertices for movement</strong>.</p>
+<hr />
+<h1 id="optional-but-recommended-rotate-the-camera">6️⃣ (Optional but
+recommended) Rotate the camera</h1>
+<p>Add this helper:</p>
+<div class="sourceCode" id="cb12"><pre
+class="sourceCode js"><code class="sourceCode javascript"><span id="cb12-1"><a href="#cb12-1" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">rotateCamera</span>(axis<span class="op">,</span> angle) {</span>
+<span id="cb12-2"><a href="#cb12-2" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> q <span class="op">=</span> <span class="fu">quatFromAxisAngle</span>(axis<span class="op">,</span> angle)<span class="op">;</span></span>
+<span id="cb12-3"><a href="#cb12-3" aria-hidden="true" tabindex="-1"></a>    camera<span class="op">.</span><span class="at">rotation</span> <span class="op">=</span> <span class="fu">quatMultiply</span>(q<span class="op">,</span> camera<span class="op">.</span><span class="at">rotation</span>)<span class="op">;</span></span>
+<span id="cb12-4"><a href="#cb12-4" aria-hidden="true" tabindex="-1"></a>}</span></code></pre></div>
+<p>Add keyboard look:</p>
+<div class="sourceCode" id="cb13"><pre
+class="sourceCode js"><code class="sourceCode javascript"><span id="cb13-1"><a href="#cb13-1" aria-hidden="true" tabindex="-1"></a><span class="bu">window</span><span class="op">.</span><span class="fu">addEventListener</span>(<span class="st">&quot;keydown&quot;</span><span class="op">,</span> e <span class="kw">=&gt;</span> {</span>
+<span id="cb13-2"><a href="#cb13-2" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> rot <span class="op">=</span> <span class="fl">0.03</span><span class="op">;</span></span>
+<span id="cb13-3"><a href="#cb13-3" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb13-4"><a href="#cb13-4" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> (e<span class="op">.</span><span class="at">key</span> <span class="op">===</span> <span class="st">&quot;ArrowLeft&quot;</span>)  <span class="fu">rotateCamera</span>({<span class="dt">x</span><span class="op">:</span><span class="dv">0</span><span class="op">,</span><span class="dt">y</span><span class="op">:</span><span class="dv">1</span><span class="op">,</span><span class="dt">z</span><span class="op">:</span><span class="dv">0</span>}<span class="op">,</span>  rot)<span class="op">;</span></span>
+<span id="cb13-5"><a href="#cb13-5" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> (e<span class="op">.</span><span class="at">key</span> <span class="op">===</span> <span class="st">&quot;ArrowRight&quot;</span>) <span class="fu">rotateCamera</span>({<span class="dt">x</span><span class="op">:</span><span class="dv">0</span><span class="op">,</span><span class="dt">y</span><span class="op">:</span><span class="dv">1</span><span class="op">,</span><span class="dt">z</span><span class="op">:</span><span class="dv">0</span>}<span class="op">,</span> <span class="op">-</span>rot)<span class="op">;</span></span>
+<span id="cb13-6"><a href="#cb13-6" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> (e<span class="op">.</span><span class="at">key</span> <span class="op">===</span> <span class="st">&quot;ArrowUp&quot;</span>)    <span class="fu">rotateCamera</span>({<span class="dt">x</span><span class="op">:</span><span class="dv">1</span><span class="op">,</span><span class="dt">y</span><span class="op">:</span><span class="dv">0</span><span class="op">,</span><span class="dt">z</span><span class="op">:</span><span class="dv">0</span>}<span class="op">,</span>  rot)<span class="op">;</span></span>
+<span id="cb13-7"><a href="#cb13-7" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> (e<span class="op">.</span><span class="at">key</span> <span class="op">===</span> <span class="st">&quot;ArrowDown&quot;</span>)  <span class="fu">rotateCamera</span>({<span class="dt">x</span><span class="op">:</span><span class="dv">1</span><span class="op">,</span><span class="dt">y</span><span class="op">:</span><span class="dv">0</span><span class="op">,</span><span class="dt">z</span><span class="op">:</span><span class="dv">0</span>}<span class="op">,</span> <span class="op">-</span>rot)<span class="op">;</span></span>
+<span id="cb13-8"><a href="#cb13-8" aria-hidden="true" tabindex="-1"></a>})<span class="op">;</span></span></code></pre></div>
+<p>Now you have <strong>true camera yaw &amp; pitch</strong>.</p>
+<hr />
+<h1 id="what-you-have-now-important-milestone">7️⃣ What you have now
+(important milestone)</h1>
+<p>You now have:</p>
+<p>✔ Object-local rotation ✔ Object world position ✔ Camera position ✔
+Camera rotation ✔ World → view → projection pipeline</p>
+<p>This is <strong>literally the same architecture</strong> used by:</p>
+<ul>
+<li>OpenGL</li>
+<li>WebGL</li>
+<li>Unity</li>
+<li>Unreal</li>
+</ul>
+<p>You’re writing a <strong>software 3D renderer</strong>.</p>
+<hr />
+<h1 id="one-key-mental-rule-remember-this">8️⃣ One key mental rule
+(remember this)</h1>
+<blockquote>
+<p>Objects move themselves. Cameras move the world.</p>
+</blockquote>
+<p>If something feels “backwards”, it usually means you forgot the
+inverse.</p>
+<hr />
+<h2 id="next-things-you-can-add-recommended-order">🔥 Next things you
+can add (recommended order)</h2>
+<p>1️⃣ FPS-style movement (WASD relative to camera) 2️⃣ Proper near-plane
+clipping 3️⃣ Field-of-view control 4️⃣ Depth sorting / painter’s algorithm
+5️⃣ Backface culling</p>
+<p>If you want, say <strong>which one</strong>, and I’ll guide you
+through it step by step.</p>
+<p>Yes — below is a <strong>clean, complete rewrite of your entire
+snippet</strong> that:</p>
+<ul>
+<li>Uses a <strong>real camera</strong> (position + quaternion
+rotation)</li>
+<li>Keeps your <strong>cube rotation via quaternions</strong></li>
+<li>Uses <strong>scroll + shift+scroll</strong> for camera movement</li>
+<li>Uses <strong>arrow keys to rotate the camera</strong></li>
+<li>Uses a <strong>proper 3D pipeline</strong> (object → world → view →
+projection)</li>
+</ul>
+<p>I’ve kept it <strong>very close to your original style</strong>, just
+corrected architecturally.</p>
+<hr />
+<h2 id="full-working-camera-based-version">✅ FULL WORKING CAMERA-BASED
+VERSION</h2>
+<div class="sourceCode" id="cb14"><pre
+class="sourceCode html"><code class="sourceCode html"><span id="cb14-1"><a href="#cb14-1" aria-hidden="true" tabindex="-1"></a><span class="dt">&lt;</span><span class="kw">canvas</span> <span class="er">id</span><span class="ot">=</span><span class="st">&quot;game&quot;</span><span class="dt">&gt;&lt;/</span><span class="kw">canvas</span><span class="dt">&gt;</span></span>
+<span id="cb14-2"><a href="#cb14-2" aria-hidden="true" tabindex="-1"></a><span class="dt">&lt;</span><span class="kw">script</span><span class="dt">&gt;</span></span>
+<span id="cb14-3"><a href="#cb14-3" aria-hidden="true" tabindex="-1"></a><span class="co">/* ===================== SETUP ===================== */</span></span>
+<span id="cb14-4"><a href="#cb14-4" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> game <span class="op">=</span> <span class="bu">document</span><span class="op">.</span><span class="fu">getElementById</span>(<span class="st">&quot;game&quot;</span>)<span class="op">;</span></span>
+<span id="cb14-5"><a href="#cb14-5" aria-hidden="true" tabindex="-1"></a>game<span class="op">.</span><span class="at">width</span> <span class="op">=</span> <span class="dv">400</span><span class="op">;</span></span>
+<span id="cb14-6"><a href="#cb14-6" aria-hidden="true" tabindex="-1"></a>game<span class="op">.</span><span class="at">height</span> <span class="op">=</span> <span class="dv">400</span><span class="op">;</span></span>
+<span id="cb14-7"><a href="#cb14-7" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb14-8"><a href="#cb14-8" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> ctx <span class="op">=</span> game<span class="op">.</span><span class="fu">getContext</span>(<span class="st">&quot;2d&quot;</span>)<span class="op">;</span></span>
+<span id="cb14-9"><a href="#cb14-9" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb14-10"><a href="#cb14-10" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> BACKGROUND <span class="op">=</span> <span class="st">&quot;#101010&quot;</span><span class="op">;</span></span>
+<span id="cb14-11"><a href="#cb14-11" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> FOREGROUND <span class="op">=</span> <span class="st">&quot;#50FF50&quot;</span><span class="op">;</span></span>
+<span id="cb14-12"><a href="#cb14-12" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> TEXT <span class="op">=</span> <span class="st">&quot;#FFFFFF&quot;</span><span class="op">;</span></span>
+<span id="cb14-13"><a href="#cb14-13" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb14-14"><a href="#cb14-14" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> FPS <span class="op">=</span> <span class="dv">60</span><span class="op">;</span></span>
+<span id="cb14-15"><a href="#cb14-15" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> NEAR <span class="op">=</span> <span class="fl">0.1</span><span class="op">;</span></span>
+<span id="cb14-16"><a href="#cb14-16" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb14-17"><a href="#cb14-17" aria-hidden="true" tabindex="-1"></a><span class="co">/* ===================== GEOMETRY ===================== */</span></span>
+<span id="cb14-18"><a href="#cb14-18" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> vertices <span class="op">=</span> [</span>
+<span id="cb14-19"><a href="#cb14-19" aria-hidden="true" tabindex="-1"></a>    {<span class="dt">x</span><span class="op">:</span>  <span class="fl">0.25</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span>  <span class="fl">0.25</span><span class="op">,</span> <span class="dt">z</span><span class="op">:</span>  <span class="fl">0.25</span>}<span class="op">,</span></span>
+<span id="cb14-20"><a href="#cb14-20" aria-hidden="true" tabindex="-1"></a>    {<span class="dt">x</span><span class="op">:</span> <span class="op">-</span><span class="fl">0.25</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span>  <span class="fl">0.25</span><span class="op">,</span> <span class="dt">z</span><span class="op">:</span>  <span class="fl">0.25</span>}<span class="op">,</span></span>
+<span id="cb14-21"><a href="#cb14-21" aria-hidden="true" tabindex="-1"></a>    {<span class="dt">x</span><span class="op">:</span> <span class="op">-</span><span class="fl">0.25</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span> <span class="op">-</span><span class="fl">0.25</span><span class="op">,</span> <span class="dt">z</span><span class="op">:</span>  <span class="fl">0.25</span>}<span class="op">,</span></span>
+<span id="cb14-22"><a href="#cb14-22" aria-hidden="true" tabindex="-1"></a>    {<span class="dt">x</span><span class="op">:</span>  <span class="fl">0.25</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span> <span class="op">-</span><span class="fl">0.25</span><span class="op">,</span> <span class="dt">z</span><span class="op">:</span>  <span class="fl">0.25</span>}<span class="op">,</span></span>
+<span id="cb14-23"><a href="#cb14-23" aria-hidden="true" tabindex="-1"></a>    {<span class="dt">x</span><span class="op">:</span>  <span class="fl">0.25</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span>  <span class="fl">0.25</span><span class="op">,</span> <span class="dt">z</span><span class="op">:</span> <span class="op">-</span><span class="fl">0.25</span>}<span class="op">,</span></span>
+<span id="cb14-24"><a href="#cb14-24" aria-hidden="true" tabindex="-1"></a>    {<span class="dt">x</span><span class="op">:</span> <span class="op">-</span><span class="fl">0.25</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span>  <span class="fl">0.25</span><span class="op">,</span> <span class="dt">z</span><span class="op">:</span> <span class="op">-</span><span class="fl">0.25</span>}<span class="op">,</span></span>
+<span id="cb14-25"><a href="#cb14-25" aria-hidden="true" tabindex="-1"></a>    {<span class="dt">x</span><span class="op">:</span> <span class="op">-</span><span class="fl">0.25</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span> <span class="op">-</span><span class="fl">0.25</span><span class="op">,</span> <span class="dt">z</span><span class="op">:</span> <span class="op">-</span><span class="fl">0.25</span>}<span class="op">,</span></span>
+<span id="cb14-26"><a href="#cb14-26" aria-hidden="true" tabindex="-1"></a>    {<span class="dt">x</span><span class="op">:</span>  <span class="fl">0.25</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span> <span class="op">-</span><span class="fl">0.25</span><span class="op">,</span> <span class="dt">z</span><span class="op">:</span> <span class="op">-</span><span class="fl">0.25</span>}<span class="op">,</span></span>
+<span id="cb14-27"><a href="#cb14-27" aria-hidden="true" tabindex="-1"></a>]<span class="op">;</span></span>
+<span id="cb14-28"><a href="#cb14-28" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb14-29"><a href="#cb14-29" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> edges <span class="op">=</span> [</span>
+<span id="cb14-30"><a href="#cb14-30" aria-hidden="true" tabindex="-1"></a>    [<span class="dv">0</span><span class="op">,</span><span class="dv">1</span><span class="op">,</span><span class="dv">2</span><span class="op">,</span><span class="dv">3</span>]<span class="op">,</span></span>
+<span id="cb14-31"><a href="#cb14-31" aria-hidden="true" tabindex="-1"></a>    [<span class="dv">4</span><span class="op">,</span><span class="dv">5</span><span class="op">,</span><span class="dv">6</span><span class="op">,</span><span class="dv">7</span>]<span class="op">,</span></span>
+<span id="cb14-32"><a href="#cb14-32" aria-hidden="true" tabindex="-1"></a>    [<span class="dv">0</span><span class="op">,</span><span class="dv">4</span>]<span class="op">,</span></span>
+<span id="cb14-33"><a href="#cb14-33" aria-hidden="true" tabindex="-1"></a>    [<span class="dv">1</span><span class="op">,</span><span class="dv">5</span>]<span class="op">,</span></span>
+<span id="cb14-34"><a href="#cb14-34" aria-hidden="true" tabindex="-1"></a>    [<span class="dv">2</span><span class="op">,</span><span class="dv">6</span>]<span class="op">,</span></span>
+<span id="cb14-35"><a href="#cb14-35" aria-hidden="true" tabindex="-1"></a>    [<span class="dv">3</span><span class="op">,</span><span class="dv">7</span>]<span class="op">,</span></span>
+<span id="cb14-36"><a href="#cb14-36" aria-hidden="true" tabindex="-1"></a>]<span class="op">;</span></span>
+<span id="cb14-37"><a href="#cb14-37" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb14-38"><a href="#cb14-38" aria-hidden="true" tabindex="-1"></a><span class="co">/* ===================== OBJECT ===================== */</span></span>
+<span id="cb14-39"><a href="#cb14-39" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> objectPosition <span class="op">=</span> { <span class="dt">x</span><span class="op">:</span> <span class="dv">0</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span> <span class="dv">0</span><span class="op">,</span> <span class="dt">z</span><span class="op">:</span> <span class="dv">2</span> }<span class="op">;</span></span>
+<span id="cb14-40"><a href="#cb14-40" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> angle <span class="op">=</span> <span class="dv">0</span><span class="op">;</span></span>
+<span id="cb14-41"><a href="#cb14-41" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> activeAxes <span class="op">=</span> [<span class="st">&quot;x&quot;</span>]<span class="op">;</span></span>
+<span id="cb14-42"><a href="#cb14-42" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb14-43"><a href="#cb14-43" aria-hidden="true" tabindex="-1"></a><span class="co">/* ===================== CAMERA ===================== */</span></span>
+<span id="cb14-44"><a href="#cb14-44" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> camera <span class="op">=</span> {</span>
+<span id="cb14-45"><a href="#cb14-45" aria-hidden="true" tabindex="-1"></a>    <span class="dt">position</span><span class="op">:</span> { <span class="dt">x</span><span class="op">:</span> <span class="dv">0</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span> <span class="dv">0</span><span class="op">,</span> <span class="dt">z</span><span class="op">:</span> <span class="dv">0</span> }<span class="op">,</span></span>
+<span id="cb14-46"><a href="#cb14-46" aria-hidden="true" tabindex="-1"></a>    <span class="dt">rotation</span><span class="op">:</span> [<span class="dv">1</span><span class="op">,</span><span class="dv">0</span><span class="op">,</span><span class="dv">0</span><span class="op">,</span><span class="dv">0</span>] <span class="co">// quaternion</span></span>
+<span id="cb14-47"><a href="#cb14-47" aria-hidden="true" tabindex="-1"></a>}<span class="op">;</span></span>
+<span id="cb14-48"><a href="#cb14-48" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb14-49"><a href="#cb14-49" aria-hidden="true" tabindex="-1"></a><span class="co">/* ===================== QUATERNIONS ===================== */</span></span>
+<span id="cb14-50"><a href="#cb14-50" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">quatFromAxisAngle</span>(axis<span class="op">,</span> angle) {</span>
+<span id="cb14-51"><a href="#cb14-51" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> h <span class="op">=</span> angle <span class="op">/</span> <span class="dv">2</span><span class="op">;</span></span>
+<span id="cb14-52"><a href="#cb14-52" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> s <span class="op">=</span> <span class="bu">Math</span><span class="op">.</span><span class="fu">sin</span>(h)<span class="op">;</span></span>
+<span id="cb14-53"><a href="#cb14-53" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> [<span class="bu">Math</span><span class="op">.</span><span class="fu">cos</span>(h)<span class="op">,</span> axis<span class="op">.</span><span class="at">x</span><span class="op">*</span>s<span class="op">,</span> axis<span class="op">.</span><span class="at">y</span><span class="op">*</span>s<span class="op">,</span> axis<span class="op">.</span><span class="at">z</span><span class="op">*</span>s]<span class="op">;</span></span>
+<span id="cb14-54"><a href="#cb14-54" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb14-55"><a href="#cb14-55" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb14-56"><a href="#cb14-56" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">quatMultiply</span>(a<span class="op">,</span> b) {</span>
+<span id="cb14-57"><a href="#cb14-57" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> [w1<span class="op">,</span>x1<span class="op">,</span>y1<span class="op">,</span>z1] <span class="op">=</span> a<span class="op">;</span></span>
+<span id="cb14-58"><a href="#cb14-58" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> [w2<span class="op">,</span>x2<span class="op">,</span>y2<span class="op">,</span>z2] <span class="op">=</span> b<span class="op">;</span></span>
+<span id="cb14-59"><a href="#cb14-59" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> [</span>
+<span id="cb14-60"><a href="#cb14-60" aria-hidden="true" tabindex="-1"></a>        w1<span class="op">*</span>w2 <span class="op">-</span> x1<span class="op">*</span>x2 <span class="op">-</span> y1<span class="op">*</span>y2 <span class="op">-</span> z1<span class="op">*</span>z2<span class="op">,</span></span>
+<span id="cb14-61"><a href="#cb14-61" aria-hidden="true" tabindex="-1"></a>        w1<span class="op">*</span>x2 <span class="op">+</span> x1<span class="op">*</span>w2 <span class="op">+</span> y1<span class="op">*</span>z2 <span class="op">-</span> z1<span class="op">*</span>y2<span class="op">,</span></span>
+<span id="cb14-62"><a href="#cb14-62" aria-hidden="true" tabindex="-1"></a>        w1<span class="op">*</span>y2 <span class="op">-</span> x1<span class="op">*</span>z2 <span class="op">+</span> y1<span class="op">*</span>w2 <span class="op">+</span> z1<span class="op">*</span>x2<span class="op">,</span></span>
+<span id="cb14-63"><a href="#cb14-63" aria-hidden="true" tabindex="-1"></a>        w1<span class="op">*</span>z2 <span class="op">+</span> x1<span class="op">*</span>y2 <span class="op">-</span> y1<span class="op">*</span>x2 <span class="op">+</span> z1<span class="op">*</span>w2</span>
+<span id="cb14-64"><a href="#cb14-64" aria-hidden="true" tabindex="-1"></a>    ]<span class="op">;</span></span>
+<span id="cb14-65"><a href="#cb14-65" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb14-66"><a href="#cb14-66" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb14-67"><a href="#cb14-67" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">quatConjugate</span>(q) {</span>
+<span id="cb14-68"><a href="#cb14-68" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> [q[<span class="dv">0</span>]<span class="op">,</span> <span class="op">-</span>q[<span class="dv">1</span>]<span class="op">,</span> <span class="op">-</span>q[<span class="dv">2</span>]<span class="op">,</span> <span class="op">-</span>q[<span class="dv">3</span>]]<span class="op">;</span></span>
+<span id="cb14-69"><a href="#cb14-69" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb14-70"><a href="#cb14-70" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb14-71"><a href="#cb14-71" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">quatRotateVector</span>(q<span class="op">,</span> v) {</span>
+<span id="cb14-72"><a href="#cb14-72" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> qv <span class="op">=</span> [<span class="dv">0</span><span class="op">,</span> v<span class="op">.</span><span class="at">x</span><span class="op">,</span> v<span class="op">.</span><span class="at">y</span><span class="op">,</span> v<span class="op">.</span><span class="at">z</span>]<span class="op">;</span></span>
+<span id="cb14-73"><a href="#cb14-73" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> inv <span class="op">=</span> <span class="fu">quatConjugate</span>(q)<span class="op">;</span></span>
+<span id="cb14-74"><a href="#cb14-74" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> t <span class="op">=</span> <span class="fu">quatMultiply</span>(q<span class="op">,</span> qv)<span class="op">;</span></span>
+<span id="cb14-75"><a href="#cb14-75" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> r <span class="op">=</span> <span class="fu">quatMultiply</span>(t<span class="op">,</span> inv)<span class="op">;</span></span>
+<span id="cb14-76"><a href="#cb14-76" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> { <span class="dt">x</span><span class="op">:</span> r[<span class="dv">1</span>]<span class="op">,</span> <span class="dt">y</span><span class="op">:</span> r[<span class="dv">2</span>]<span class="op">,</span> <span class="dt">z</span><span class="op">:</span> r[<span class="dv">3</span>] }<span class="op">;</span></span>
+<span id="cb14-77"><a href="#cb14-77" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb14-78"><a href="#cb14-78" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb14-79"><a href="#cb14-79" aria-hidden="true" tabindex="-1"></a><span class="co">/* ===================== TRANSFORMS ===================== */</span></span>
+<span id="cb14-80"><a href="#cb14-80" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">translate</span>(p<span class="op">,</span> t) {</span>
+<span id="cb14-81"><a href="#cb14-81" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> { <span class="dt">x</span><span class="op">:</span> p<span class="op">.</span><span class="at">x</span> <span class="op">+</span> t<span class="op">.</span><span class="at">x</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span> p<span class="op">.</span><span class="at">y</span> <span class="op">+</span> t<span class="op">.</span><span class="at">y</span><span class="op">,</span> <span class="dt">z</span><span class="op">:</span> p<span class="op">.</span><span class="at">z</span> <span class="op">+</span> t<span class="op">.</span><span class="at">z</span> }<span class="op">;</span></span>
+<span id="cb14-82"><a href="#cb14-82" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb14-83"><a href="#cb14-83" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb14-84"><a href="#cb14-84" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">applyCamera</span>(p) {</span>
+<span id="cb14-85"><a href="#cb14-85" aria-hidden="true" tabindex="-1"></a>    <span class="kw">let</span> v <span class="op">=</span> {</span>
+<span id="cb14-86"><a href="#cb14-86" aria-hidden="true" tabindex="-1"></a>        <span class="dt">x</span><span class="op">:</span> p<span class="op">.</span><span class="at">x</span> <span class="op">-</span> camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">x</span><span class="op">,</span></span>
+<span id="cb14-87"><a href="#cb14-87" aria-hidden="true" tabindex="-1"></a>        <span class="dt">y</span><span class="op">:</span> p<span class="op">.</span><span class="at">y</span> <span class="op">-</span> camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">y</span><span class="op">,</span></span>
+<span id="cb14-88"><a href="#cb14-88" aria-hidden="true" tabindex="-1"></a>        <span class="dt">z</span><span class="op">:</span> p<span class="op">.</span><span class="at">z</span> <span class="op">-</span> camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">z</span></span>
+<span id="cb14-89"><a href="#cb14-89" aria-hidden="true" tabindex="-1"></a>    }<span class="op">;</span></span>
+<span id="cb14-90"><a href="#cb14-90" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> <span class="fu">quatRotateVector</span>(<span class="fu">quatConjugate</span>(camera<span class="op">.</span><span class="at">rotation</span>)<span class="op">,</span> v)<span class="op">;</span></span>
+<span id="cb14-91"><a href="#cb14-91" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb14-92"><a href="#cb14-92" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb14-93"><a href="#cb14-93" aria-hidden="true" tabindex="-1"></a><span class="co">/* ===================== PROJECTION ===================== */</span></span>
+<span id="cb14-94"><a href="#cb14-94" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">project</span>(p) {</span>
+<span id="cb14-95"><a href="#cb14-95" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> { <span class="dt">x</span><span class="op">:</span> p<span class="op">.</span><span class="at">x</span> <span class="op">/</span> p<span class="op">.</span><span class="at">z</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span> p<span class="op">.</span><span class="at">y</span> <span class="op">/</span> p<span class="op">.</span><span class="at">z</span> }<span class="op">;</span></span>
+<span id="cb14-96"><a href="#cb14-96" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb14-97"><a href="#cb14-97" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb14-98"><a href="#cb14-98" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">screen</span>(p) {</span>
+<span id="cb14-99"><a href="#cb14-99" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> {</span>
+<span id="cb14-100"><a href="#cb14-100" aria-hidden="true" tabindex="-1"></a>        <span class="dt">x</span><span class="op">:</span> (p<span class="op">.</span><span class="at">x</span> <span class="op">+</span> <span class="dv">1</span>) <span class="op">*</span> <span class="fl">0.5</span> <span class="op">*</span> game<span class="op">.</span><span class="at">width</span><span class="op">,</span></span>
+<span id="cb14-101"><a href="#cb14-101" aria-hidden="true" tabindex="-1"></a>        <span class="dt">y</span><span class="op">:</span> (<span class="dv">1</span> <span class="op">-</span> (p<span class="op">.</span><span class="at">y</span> <span class="op">+</span> <span class="dv">1</span>) <span class="op">*</span> <span class="fl">0.5</span>) <span class="op">*</span> game<span class="op">.</span><span class="at">height</span></span>
+<span id="cb14-102"><a href="#cb14-102" aria-hidden="true" tabindex="-1"></a>    }<span class="op">;</span></span>
+<span id="cb14-103"><a href="#cb14-103" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb14-104"><a href="#cb14-104" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb14-105"><a href="#cb14-105" aria-hidden="true" tabindex="-1"></a><span class="co">/* ===================== DRAW ===================== */</span></span>
+<span id="cb14-106"><a href="#cb14-106" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">clear</span>() {</span>
+<span id="cb14-107"><a href="#cb14-107" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="at">fillStyle</span> <span class="op">=</span> BACKGROUND<span class="op">;</span></span>
+<span id="cb14-108"><a href="#cb14-108" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="fu">fillRect</span>(<span class="dv">0</span><span class="op">,</span> <span class="dv">0</span><span class="op">,</span> game<span class="op">.</span><span class="at">width</span><span class="op">,</span> game<span class="op">.</span><span class="at">height</span>)<span class="op">;</span></span>
+<span id="cb14-109"><a href="#cb14-109" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb14-110"><a href="#cb14-110" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="at">fillStyle</span> <span class="op">=</span> TEXT<span class="op">;</span></span>
+<span id="cb14-111"><a href="#cb14-111" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="at">font</span> <span class="op">=</span> <span class="st">&quot;14px monospace&quot;</span><span class="op">;</span></span>
+<span id="cb14-112"><a href="#cb14-112" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="fu">fillText</span>(<span class="st">&quot;Click: change cube rotation axes&quot;</span><span class="op">,</span> <span class="dv">10</span><span class="op">,</span> <span class="dv">18</span>)<span class="op">;</span></span>
+<span id="cb14-113"><a href="#cb14-113" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="fu">fillText</span>(<span class="st">&quot;Scroll: move camera forward/back&quot;</span><span class="op">,</span> <span class="dv">10</span><span class="op">,</span> <span class="dv">36</span>)<span class="op">;</span></span>
+<span id="cb14-114"><a href="#cb14-114" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="fu">fillText</span>(<span class="st">&quot;Shift+Scroll: strafe camera&quot;</span><span class="op">,</span> <span class="dv">10</span><span class="op">,</span> <span class="dv">54</span>)<span class="op">;</span></span>
+<span id="cb14-115"><a href="#cb14-115" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="fu">fillText</span>(<span class="st">&quot;Arrow keys: look around&quot;</span><span class="op">,</span> <span class="dv">10</span><span class="op">,</span> <span class="dv">72</span>)<span class="op">;</span></span>
+<span id="cb14-116"><a href="#cb14-116" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="fu">fillText</span>(<span class="st">&quot;Axes: &quot;</span> <span class="op">+</span> activeAxes<span class="op">.</span><span class="fu">join</span>(<span class="st">&quot;,&quot;</span>)<span class="op">,</span> <span class="dv">10</span><span class="op">,</span> <span class="dv">90</span>)<span class="op">;</span></span>
+<span id="cb14-117"><a href="#cb14-117" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb14-118"><a href="#cb14-118" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb14-119"><a href="#cb14-119" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">line</span>(a<span class="op">,</span> b) {</span>
+<span id="cb14-120"><a href="#cb14-120" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="at">strokeStyle</span> <span class="op">=</span> FOREGROUND<span class="op">;</span></span>
+<span id="cb14-121"><a href="#cb14-121" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="at">lineWidth</span> <span class="op">=</span> <span class="dv">2</span><span class="op">;</span></span>
+<span id="cb14-122"><a href="#cb14-122" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="fu">beginPath</span>()<span class="op">;</span></span>
+<span id="cb14-123"><a href="#cb14-123" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="fu">moveTo</span>(a<span class="op">.</span><span class="at">x</span><span class="op">,</span> a<span class="op">.</span><span class="at">y</span>)<span class="op">;</span></span>
+<span id="cb14-124"><a href="#cb14-124" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="fu">lineTo</span>(b<span class="op">.</span><span class="at">x</span><span class="op">,</span> b<span class="op">.</span><span class="at">y</span>)<span class="op">;</span></span>
+<span id="cb14-125"><a href="#cb14-125" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="fu">stroke</span>()<span class="op">;</span></span>
+<span id="cb14-126"><a href="#cb14-126" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb14-127"><a href="#cb14-127" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb14-128"><a href="#cb14-128" aria-hidden="true" tabindex="-1"></a><span class="co">/* ===================== LOOP ===================== */</span></span>
+<span id="cb14-129"><a href="#cb14-129" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">frame</span>() {</span>
+<span id="cb14-130"><a href="#cb14-130" aria-hidden="true" tabindex="-1"></a>    angle <span class="op">+=</span> <span class="bu">Math</span><span class="op">.</span><span class="cn">PI</span> <span class="op">/</span> FPS<span class="op">;</span></span>
+<span id="cb14-131"><a href="#cb14-131" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb14-132"><a href="#cb14-132" aria-hidden="true" tabindex="-1"></a>    <span class="kw">let</span> q <span class="op">=</span> [<span class="dv">1</span><span class="op">,</span><span class="dv">0</span><span class="op">,</span><span class="dv">0</span><span class="op">,</span><span class="dv">0</span>]<span class="op">;</span></span>
+<span id="cb14-133"><a href="#cb14-133" aria-hidden="true" tabindex="-1"></a>    <span class="cf">for</span> (<span class="kw">const</span> ax <span class="kw">of</span> activeAxes) {</span>
+<span id="cb14-134"><a href="#cb14-134" aria-hidden="true" tabindex="-1"></a>        <span class="cf">if</span> (ax <span class="op">===</span> <span class="st">&quot;x&quot;</span>) q <span class="op">=</span> <span class="fu">quatMultiply</span>(<span class="fu">quatFromAxisAngle</span>({<span class="dt">x</span><span class="op">:</span><span class="dv">1</span><span class="op">,</span><span class="dt">y</span><span class="op">:</span><span class="dv">0</span><span class="op">,</span><span class="dt">z</span><span class="op">:</span><span class="dv">0</span>}<span class="op">,</span> angle)<span class="op">,</span> q)<span class="op">;</span></span>
+<span id="cb14-135"><a href="#cb14-135" aria-hidden="true" tabindex="-1"></a>        <span class="cf">if</span> (ax <span class="op">===</span> <span class="st">&quot;y&quot;</span>) q <span class="op">=</span> <span class="fu">quatMultiply</span>(<span class="fu">quatFromAxisAngle</span>({<span class="dt">x</span><span class="op">:</span><span class="dv">0</span><span class="op">,</span><span class="dt">y</span><span class="op">:</span><span class="dv">1</span><span class="op">,</span><span class="dt">z</span><span class="op">:</span><span class="dv">0</span>}<span class="op">,</span> angle)<span class="op">,</span> q)<span class="op">;</span></span>
+<span id="cb14-136"><a href="#cb14-136" aria-hidden="true" tabindex="-1"></a>        <span class="cf">if</span> (ax <span class="op">===</span> <span class="st">&quot;z&quot;</span>) q <span class="op">=</span> <span class="fu">quatMultiply</span>(<span class="fu">quatFromAxisAngle</span>({<span class="dt">x</span><span class="op">:</span><span class="dv">0</span><span class="op">,</span><span class="dt">y</span><span class="op">:</span><span class="dv">0</span><span class="op">,</span><span class="dt">z</span><span class="op">:</span><span class="dv">1</span>}<span class="op">,</span> angle)<span class="op">,</span> q)<span class="op">;</span></span>
+<span id="cb14-137"><a href="#cb14-137" aria-hidden="true" tabindex="-1"></a>    }</span>
+<span id="cb14-138"><a href="#cb14-138" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb14-139"><a href="#cb14-139" aria-hidden="true" tabindex="-1"></a>    <span class="fu">clear</span>()<span class="op">;</span></span>
+<span id="cb14-140"><a href="#cb14-140" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb14-141"><a href="#cb14-141" aria-hidden="true" tabindex="-1"></a>    <span class="cf">for</span> (<span class="kw">const</span> e <span class="kw">of</span> edges) {</span>
+<span id="cb14-142"><a href="#cb14-142" aria-hidden="true" tabindex="-1"></a>        <span class="cf">for</span> (<span class="kw">let</span> i <span class="op">=</span> <span class="dv">0</span><span class="op">;</span> i <span class="op">&lt;</span> e<span class="op">.</span><span class="at">length</span><span class="op">;</span> i<span class="op">++</span>) {</span>
+<span id="cb14-143"><a href="#cb14-143" aria-hidden="true" tabindex="-1"></a>            <span class="kw">let</span> a <span class="op">=</span> vertices[e[i]]<span class="op">;</span></span>
+<span id="cb14-144"><a href="#cb14-144" aria-hidden="true" tabindex="-1"></a>            <span class="kw">let</span> b <span class="op">=</span> vertices[e[(i<span class="op">+</span><span class="dv">1</span>)<span class="op">%</span>e<span class="op">.</span><span class="at">length</span>]]<span class="op">;</span></span>
+<span id="cb14-145"><a href="#cb14-145" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb14-146"><a href="#cb14-146" aria-hidden="true" tabindex="-1"></a>            a <span class="op">=</span> <span class="fu">translate</span>(<span class="fu">quatRotateVector</span>(q<span class="op">,</span> a)<span class="op">,</span> objectPosition)<span class="op">;</span></span>
+<span id="cb14-147"><a href="#cb14-147" aria-hidden="true" tabindex="-1"></a>            b <span class="op">=</span> <span class="fu">translate</span>(<span class="fu">quatRotateVector</span>(q<span class="op">,</span> b)<span class="op">,</span> objectPosition)<span class="op">;</span></span>
+<span id="cb14-148"><a href="#cb14-148" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb14-149"><a href="#cb14-149" aria-hidden="true" tabindex="-1"></a>            a <span class="op">=</span> <span class="fu">applyCamera</span>(a)<span class="op">;</span></span>
+<span id="cb14-150"><a href="#cb14-150" aria-hidden="true" tabindex="-1"></a>            b <span class="op">=</span> <span class="fu">applyCamera</span>(b)<span class="op">;</span></span>
+<span id="cb14-151"><a href="#cb14-151" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb14-152"><a href="#cb14-152" aria-hidden="true" tabindex="-1"></a>            <span class="cf">if</span> (a<span class="op">.</span><span class="at">z</span> <span class="op">&lt;=</span> NEAR <span class="op">||</span> b<span class="op">.</span><span class="at">z</span> <span class="op">&lt;=</span> NEAR) <span class="cf">continue</span><span class="op">;</span></span>
+<span id="cb14-153"><a href="#cb14-153" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb14-154"><a href="#cb14-154" aria-hidden="true" tabindex="-1"></a>            <span class="fu">line</span>(<span class="bu">screen</span>(<span class="fu">project</span>(a))<span class="op">,</span> <span class="bu">screen</span>(<span class="fu">project</span>(b)))<span class="op">;</span></span>
+<span id="cb14-155"><a href="#cb14-155" aria-hidden="true" tabindex="-1"></a>        }</span>
+<span id="cb14-156"><a href="#cb14-156" aria-hidden="true" tabindex="-1"></a>    }</span>
+<span id="cb14-157"><a href="#cb14-157" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb14-158"><a href="#cb14-158" aria-hidden="true" tabindex="-1"></a>    <span class="pp">setTimeout</span>(frame<span class="op">,</span> <span class="dv">1000</span><span class="op">/</span>FPS)<span class="op">;</span></span>
+<span id="cb14-159"><a href="#cb14-159" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb14-160"><a href="#cb14-160" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb14-161"><a href="#cb14-161" aria-hidden="true" tabindex="-1"></a><span class="fu">frame</span>()<span class="op">;</span></span>
+<span id="cb14-162"><a href="#cb14-162" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb14-163"><a href="#cb14-163" aria-hidden="true" tabindex="-1"></a><span class="co">/* ===================== INPUT ===================== */</span></span>
+<span id="cb14-164"><a href="#cb14-164" aria-hidden="true" tabindex="-1"></a>game<span class="op">.</span><span class="at">onclick</span> <span class="op">=</span> () <span class="kw">=&gt;</span> {</span>
+<span id="cb14-165"><a href="#cb14-165" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> modes <span class="op">=</span> [[<span class="st">&quot;x&quot;</span>]<span class="op">,</span>[<span class="st">&quot;y&quot;</span>]<span class="op">,</span>[<span class="st">&quot;z&quot;</span>]<span class="op">,</span>[<span class="st">&quot;x&quot;</span><span class="op">,</span><span class="st">&quot;y&quot;</span>]<span class="op">,</span>[<span class="st">&quot;y&quot;</span><span class="op">,</span><span class="st">&quot;z&quot;</span>]<span class="op">,</span>[<span class="st">&quot;x&quot;</span><span class="op">,</span><span class="st">&quot;z&quot;</span>]<span class="op">,</span>[<span class="st">&quot;x&quot;</span><span class="op">,</span><span class="st">&quot;y&quot;</span><span class="op">,</span><span class="st">&quot;z&quot;</span>]]<span class="op">;</span></span>
+<span id="cb14-166"><a href="#cb14-166" aria-hidden="true" tabindex="-1"></a>    activeAxes <span class="op">=</span> modes[(modes<span class="op">.</span><span class="fu">findIndex</span>(m <span class="kw">=&gt;</span> m<span class="op">+</span><span class="st">&quot;&quot;</span> <span class="op">===</span> activeAxes<span class="op">+</span><span class="st">&quot;&quot;</span>) <span class="op">+</span> <span class="dv">1</span>) <span class="op">%</span> modes<span class="op">.</span><span class="at">length</span>]<span class="op">;</span></span>
+<span id="cb14-167"><a href="#cb14-167" aria-hidden="true" tabindex="-1"></a>}<span class="op">;</span></span>
+<span id="cb14-168"><a href="#cb14-168" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb14-169"><a href="#cb14-169" aria-hidden="true" tabindex="-1"></a>game<span class="op">.</span><span class="fu">addEventListener</span>(<span class="st">&quot;wheel&quot;</span><span class="op">,</span> e <span class="kw">=&gt;</span> {</span>
+<span id="cb14-170"><a href="#cb14-170" aria-hidden="true" tabindex="-1"></a>    e<span class="op">.</span><span class="fu">preventDefault</span>()<span class="op">;</span></span>
+<span id="cb14-171"><a href="#cb14-171" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> (e<span class="op">.</span><span class="at">shiftKey</span>) camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">x</span> <span class="op">+=</span> e<span class="op">.</span><span class="at">deltaY</span> <span class="op">*</span> <span class="fl">0.002</span><span class="op">;</span></span>
+<span id="cb14-172"><a href="#cb14-172" aria-hidden="true" tabindex="-1"></a>    <span class="cf">else</span> camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">z</span> <span class="op">+=</span> e<span class="op">.</span><span class="at">deltaY</span> <span class="op">*</span> <span class="fl">0.01</span><span class="op">;</span></span>
+<span id="cb14-173"><a href="#cb14-173" aria-hidden="true" tabindex="-1"></a>}<span class="op">,</span> { <span class="dt">passive</span><span class="op">:</span><span class="kw">false</span> })<span class="op">;</span></span>
+<span id="cb14-174"><a href="#cb14-174" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb14-175"><a href="#cb14-175" aria-hidden="true" tabindex="-1"></a><span class="bu">window</span><span class="op">.</span><span class="fu">addEventListener</span>(<span class="st">&quot;keydown&quot;</span><span class="op">,</span> e <span class="kw">=&gt;</span> {</span>
+<span id="cb14-176"><a href="#cb14-176" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> r <span class="op">=</span> <span class="fl">0.04</span><span class="op">;</span></span>
+<span id="cb14-177"><a href="#cb14-177" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> (e<span class="op">.</span><span class="at">key</span> <span class="op">===</span> <span class="st">&quot;ArrowLeft&quot;</span>)  camera<span class="op">.</span><span class="at">rotation</span> <span class="op">=</span> <span class="fu">quatMultiply</span>(<span class="fu">quatFromAxisAngle</span>({<span class="dt">x</span><span class="op">:</span><span class="dv">0</span><span class="op">,</span><span class="dt">y</span><span class="op">:</span><span class="dv">1</span><span class="op">,</span><span class="dt">z</span><span class="op">:</span><span class="dv">0</span>}<span class="op">,</span>  r)<span class="op">,</span> camera<span class="op">.</span><span class="at">rotation</span>)<span class="op">;</span></span>
+<span id="cb14-178"><a href="#cb14-178" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> (e<span class="op">.</span><span class="at">key</span> <span class="op">===</span> <span class="st">&quot;ArrowRight&quot;</span>) camera<span class="op">.</span><span class="at">rotation</span> <span class="op">=</span> <span class="fu">quatMultiply</span>(<span class="fu">quatFromAxisAngle</span>({<span class="dt">x</span><span class="op">:</span><span class="dv">0</span><span class="op">,</span><span class="dt">y</span><span class="op">:</span><span class="dv">1</span><span class="op">,</span><span class="dt">z</span><span class="op">:</span><span class="dv">0</span>}<span class="op">,</span> <span class="op">-</span>r)<span class="op">,</span> camera<span class="op">.</span><span class="at">rotation</span>)<span class="op">;</span></span>
+<span id="cb14-179"><a href="#cb14-179" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> (e<span class="op">.</span><span class="at">key</span> <span class="op">===</span> <span class="st">&quot;ArrowUp&quot;</span>)    camera<span class="op">.</span><span class="at">rotation</span> <span class="op">=</span> <span class="fu">quatMultiply</span>(<span class="fu">quatFromAxisAngle</span>({<span class="dt">x</span><span class="op">:</span><span class="dv">1</span><span class="op">,</span><span class="dt">y</span><span class="op">:</span><span class="dv">0</span><span class="op">,</span><span class="dt">z</span><span class="op">:</span><span class="dv">0</span>}<span class="op">,</span>  r)<span class="op">,</span> camera<span class="op">.</span><span class="at">rotation</span>)<span class="op">;</span></span>
+<span id="cb14-180"><a href="#cb14-180" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> (e<span class="op">.</span><span class="at">key</span> <span class="op">===</span> <span class="st">&quot;ArrowDown&quot;</span>)  camera<span class="op">.</span><span class="at">rotation</span> <span class="op">=</span> <span class="fu">quatMultiply</span>(<span class="fu">quatFromAxisAngle</span>({<span class="dt">x</span><span class="op">:</span><span class="dv">1</span><span class="op">,</span><span class="dt">y</span><span class="op">:</span><span class="dv">0</span><span class="op">,</span><span class="dt">z</span><span class="op">:</span><span class="dv">0</span>}<span class="op">,</span> <span class="op">-</span>r)<span class="op">,</span> camera<span class="op">.</span><span class="at">rotation</span>)<span class="op">;</span></span>
+<span id="cb14-181"><a href="#cb14-181" aria-hidden="true" tabindex="-1"></a>})<span class="op">;</span></span>
+<span id="cb14-182"><a href="#cb14-182" aria-hidden="true" tabindex="-1"></a><span class="dt">&lt;/</span><span class="kw">script</span><span class="dt">&gt;</span></span></code></pre></div>
+<hr />
+<h2 id="what-this-version-gives-you">🎯 What this version gives you</h2>
+<p>✔ True <strong>camera system</strong> ✔ Object rotation separate from
+camera ✔ Proper <strong>world → view → projection</strong> pipeline ✔ No
+vertex mutation ✔ No fake camera tricks ✔ Clean mental model identical
+to real engines</p>
+<hr />
+<h3 id="you-are-now-at-the-level-where">🚀 You are now at the level
+where:</h3>
+<ul>
+<li>WebGL will suddenly make sense</li>
+<li>Matrices will feel intuitive</li>
+<li>Game engines won’t feel “magical” anymore</li>
+</ul>
+<p>If you want next, I strongly recommend: 👉 <strong>FPS-style movement
+(WASD relative to camera direction)</strong> 👉
+<strong>Mouse-look</strong> 👉 <strong>Perspective FOV math</strong></p>
+<p>Just say which one 👍</p>
+<p>Absolutely — this is the <strong>correct next step</strong> 👍 “Fly
+mode” means:</p>
+<ul>
+<li><strong>W / S</strong> → move forward / backward <strong>relative to
+where the camera is facing</strong></li>
+<li><strong>A / D</strong> → strafe left / right</li>
+<li><strong>Q / E</strong> → move down / up</li>
+<li><strong>Mouse</strong> → look around (yaw + pitch)</li>
+<li>The cube stays in the world; <strong>the camera flies</strong></li>
+</ul>
+<p>Below is a <strong>full rewrite</strong> of the previous camera-based
+code, upgraded to a <strong>true fly camera</strong>. Nothing is faked,
+and the math matches real engines.</p>
+<hr />
+<h2 id="full-fly-camera-version-wasd-mouse-look">✅ FULL FLY-CAMERA
+VERSION (WASD + Mouse Look)</h2>
+<div class="sourceCode" id="cb15"><pre
+class="sourceCode html"><code class="sourceCode html"><span id="cb15-1"><a href="#cb15-1" aria-hidden="true" tabindex="-1"></a><span class="dt">&lt;</span><span class="kw">canvas</span> <span class="er">id</span><span class="ot">=</span><span class="st">&quot;game&quot;</span><span class="dt">&gt;&lt;/</span><span class="kw">canvas</span><span class="dt">&gt;</span></span>
+<span id="cb15-2"><a href="#cb15-2" aria-hidden="true" tabindex="-1"></a><span class="dt">&lt;</span><span class="kw">script</span><span class="dt">&gt;</span></span>
+<span id="cb15-3"><a href="#cb15-3" aria-hidden="true" tabindex="-1"></a><span class="co">/* ===================== SETUP ===================== */</span></span>
+<span id="cb15-4"><a href="#cb15-4" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> game <span class="op">=</span> <span class="bu">document</span><span class="op">.</span><span class="fu">getElementById</span>(<span class="st">&quot;game&quot;</span>)<span class="op">;</span></span>
+<span id="cb15-5"><a href="#cb15-5" aria-hidden="true" tabindex="-1"></a>game<span class="op">.</span><span class="at">width</span> <span class="op">=</span> <span class="dv">400</span><span class="op">;</span></span>
+<span id="cb15-6"><a href="#cb15-6" aria-hidden="true" tabindex="-1"></a>game<span class="op">.</span><span class="at">height</span> <span class="op">=</span> <span class="dv">400</span><span class="op">;</span></span>
+<span id="cb15-7"><a href="#cb15-7" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> ctx <span class="op">=</span> game<span class="op">.</span><span class="fu">getContext</span>(<span class="st">&quot;2d&quot;</span>)<span class="op">;</span></span>
+<span id="cb15-8"><a href="#cb15-8" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb15-9"><a href="#cb15-9" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> BACKGROUND <span class="op">=</span> <span class="st">&quot;#101010&quot;</span><span class="op">;</span></span>
+<span id="cb15-10"><a href="#cb15-10" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> FOREGROUND <span class="op">=</span> <span class="st">&quot;#50FF50&quot;</span><span class="op">;</span></span>
+<span id="cb15-11"><a href="#cb15-11" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> TEXT <span class="op">=</span> <span class="st">&quot;#FFFFFF&quot;</span><span class="op">;</span></span>
+<span id="cb15-12"><a href="#cb15-12" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb15-13"><a href="#cb15-13" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> FPS <span class="op">=</span> <span class="dv">60</span><span class="op">;</span></span>
+<span id="cb15-14"><a href="#cb15-14" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> NEAR <span class="op">=</span> <span class="fl">0.1</span><span class="op">;</span></span>
+<span id="cb15-15"><a href="#cb15-15" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb15-16"><a href="#cb15-16" aria-hidden="true" tabindex="-1"></a><span class="co">/* ===================== GEOMETRY ===================== */</span></span>
+<span id="cb15-17"><a href="#cb15-17" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> vertices <span class="op">=</span> [</span>
+<span id="cb15-18"><a href="#cb15-18" aria-hidden="true" tabindex="-1"></a>    {<span class="dt">x</span><span class="op">:</span>  <span class="fl">0.25</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span>  <span class="fl">0.25</span><span class="op">,</span> <span class="dt">z</span><span class="op">:</span>  <span class="fl">0.25</span>}<span class="op">,</span></span>
+<span id="cb15-19"><a href="#cb15-19" aria-hidden="true" tabindex="-1"></a>    {<span class="dt">x</span><span class="op">:</span> <span class="op">-</span><span class="fl">0.25</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span>  <span class="fl">0.25</span><span class="op">,</span> <span class="dt">z</span><span class="op">:</span>  <span class="fl">0.25</span>}<span class="op">,</span></span>
+<span id="cb15-20"><a href="#cb15-20" aria-hidden="true" tabindex="-1"></a>    {<span class="dt">x</span><span class="op">:</span> <span class="op">-</span><span class="fl">0.25</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span> <span class="op">-</span><span class="fl">0.25</span><span class="op">,</span> <span class="dt">z</span><span class="op">:</span>  <span class="fl">0.25</span>}<span class="op">,</span></span>
+<span id="cb15-21"><a href="#cb15-21" aria-hidden="true" tabindex="-1"></a>    {<span class="dt">x</span><span class="op">:</span>  <span class="fl">0.25</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span> <span class="op">-</span><span class="fl">0.25</span><span class="op">,</span> <span class="dt">z</span><span class="op">:</span>  <span class="fl">0.25</span>}<span class="op">,</span></span>
+<span id="cb15-22"><a href="#cb15-22" aria-hidden="true" tabindex="-1"></a>    {<span class="dt">x</span><span class="op">:</span>  <span class="fl">0.25</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span>  <span class="fl">0.25</span><span class="op">,</span> <span class="dt">z</span><span class="op">:</span> <span class="op">-</span><span class="fl">0.25</span>}<span class="op">,</span></span>
+<span id="cb15-23"><a href="#cb15-23" aria-hidden="true" tabindex="-1"></a>    {<span class="dt">x</span><span class="op">:</span> <span class="op">-</span><span class="fl">0.25</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span>  <span class="fl">0.25</span><span class="op">,</span> <span class="dt">z</span><span class="op">:</span> <span class="op">-</span><span class="fl">0.25</span>}<span class="op">,</span></span>
+<span id="cb15-24"><a href="#cb15-24" aria-hidden="true" tabindex="-1"></a>    {<span class="dt">x</span><span class="op">:</span> <span class="op">-</span><span class="fl">0.25</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span> <span class="op">-</span><span class="fl">0.25</span><span class="op">,</span> <span class="dt">z</span><span class="op">:</span> <span class="op">-</span><span class="fl">0.25</span>}<span class="op">,</span></span>
+<span id="cb15-25"><a href="#cb15-25" aria-hidden="true" tabindex="-1"></a>    {<span class="dt">x</span><span class="op">:</span>  <span class="fl">0.25</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span> <span class="op">-</span><span class="fl">0.25</span><span class="op">,</span> <span class="dt">z</span><span class="op">:</span> <span class="op">-</span><span class="fl">0.25</span>}<span class="op">,</span></span>
+<span id="cb15-26"><a href="#cb15-26" aria-hidden="true" tabindex="-1"></a>]<span class="op">;</span></span>
+<span id="cb15-27"><a href="#cb15-27" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb15-28"><a href="#cb15-28" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> edges <span class="op">=</span> [</span>
+<span id="cb15-29"><a href="#cb15-29" aria-hidden="true" tabindex="-1"></a>    [<span class="dv">0</span><span class="op">,</span><span class="dv">1</span><span class="op">,</span><span class="dv">2</span><span class="op">,</span><span class="dv">3</span>]<span class="op">,</span></span>
+<span id="cb15-30"><a href="#cb15-30" aria-hidden="true" tabindex="-1"></a>    [<span class="dv">4</span><span class="op">,</span><span class="dv">5</span><span class="op">,</span><span class="dv">6</span><span class="op">,</span><span class="dv">7</span>]<span class="op">,</span></span>
+<span id="cb15-31"><a href="#cb15-31" aria-hidden="true" tabindex="-1"></a>    [<span class="dv">0</span><span class="op">,</span><span class="dv">4</span>]<span class="op">,</span>[<span class="dv">1</span><span class="op">,</span><span class="dv">5</span>]<span class="op">,</span>[<span class="dv">2</span><span class="op">,</span><span class="dv">6</span>]<span class="op">,</span>[<span class="dv">3</span><span class="op">,</span><span class="dv">7</span>]</span>
+<span id="cb15-32"><a href="#cb15-32" aria-hidden="true" tabindex="-1"></a>]<span class="op">;</span></span>
+<span id="cb15-33"><a href="#cb15-33" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb15-34"><a href="#cb15-34" aria-hidden="true" tabindex="-1"></a><span class="co">/* ===================== OBJECT ===================== */</span></span>
+<span id="cb15-35"><a href="#cb15-35" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> objectPos <span class="op">=</span> { <span class="dt">x</span><span class="op">:</span> <span class="dv">0</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span> <span class="dv">0</span><span class="op">,</span> <span class="dt">z</span><span class="op">:</span> <span class="dv">2</span> }<span class="op">;</span></span>
+<span id="cb15-36"><a href="#cb15-36" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> angle <span class="op">=</span> <span class="dv">0</span><span class="op">;</span></span>
+<span id="cb15-37"><a href="#cb15-37" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> activeAxes <span class="op">=</span> [<span class="st">&quot;x&quot;</span><span class="op">,</span><span class="st">&quot;y&quot;</span>]<span class="op">;</span></span>
+<span id="cb15-38"><a href="#cb15-38" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb15-39"><a href="#cb15-39" aria-hidden="true" tabindex="-1"></a><span class="co">/* ===================== CAMERA ===================== */</span></span>
+<span id="cb15-40"><a href="#cb15-40" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> camera <span class="op">=</span> {</span>
+<span id="cb15-41"><a href="#cb15-41" aria-hidden="true" tabindex="-1"></a>    <span class="dt">position</span><span class="op">:</span> { <span class="dt">x</span><span class="op">:</span> <span class="dv">0</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span> <span class="dv">0</span><span class="op">,</span> <span class="dt">z</span><span class="op">:</span> <span class="dv">0</span> }<span class="op">,</span></span>
+<span id="cb15-42"><a href="#cb15-42" aria-hidden="true" tabindex="-1"></a>    <span class="dt">rotation</span><span class="op">:</span> [<span class="dv">1</span><span class="op">,</span><span class="dv">0</span><span class="op">,</span><span class="dv">0</span><span class="op">,</span><span class="dv">0</span>]<span class="op">,</span> <span class="co">// quaternion</span></span>
+<span id="cb15-43"><a href="#cb15-43" aria-hidden="true" tabindex="-1"></a>    <span class="dt">speed</span><span class="op">:</span> <span class="fl">0.05</span><span class="op">,</span></span>
+<span id="cb15-44"><a href="#cb15-44" aria-hidden="true" tabindex="-1"></a>    <span class="dt">sensitivity</span><span class="op">:</span> <span class="fl">0.002</span></span>
+<span id="cb15-45"><a href="#cb15-45" aria-hidden="true" tabindex="-1"></a>}<span class="op">;</span></span>
+<span id="cb15-46"><a href="#cb15-46" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb15-47"><a href="#cb15-47" aria-hidden="true" tabindex="-1"></a><span class="co">/* ===================== INPUT ===================== */</span></span>
+<span id="cb15-48"><a href="#cb15-48" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> keys <span class="op">=</span> {}<span class="op">;</span></span>
+<span id="cb15-49"><a href="#cb15-49" aria-hidden="true" tabindex="-1"></a><span class="bu">window</span><span class="op">.</span><span class="fu">addEventListener</span>(<span class="st">&quot;keydown&quot;</span><span class="op">,</span> e <span class="kw">=&gt;</span> keys[e<span class="op">.</span><span class="at">key</span><span class="op">.</span><span class="fu">toLowerCase</span>()] <span class="op">=</span> <span class="kw">true</span>)<span class="op">;</span></span>
+<span id="cb15-50"><a href="#cb15-50" aria-hidden="true" tabindex="-1"></a><span class="bu">window</span><span class="op">.</span><span class="fu">addEventListener</span>(<span class="st">&quot;keyup&quot;</span><span class="op">,</span>   e <span class="kw">=&gt;</span> keys[e<span class="op">.</span><span class="at">key</span><span class="op">.</span><span class="fu">toLowerCase</span>()] <span class="op">=</span> <span class="kw">false</span>)<span class="op">;</span></span>
+<span id="cb15-51"><a href="#cb15-51" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb15-52"><a href="#cb15-52" aria-hidden="true" tabindex="-1"></a><span class="co">/* ===================== QUATERNIONS ===================== */</span></span>
+<span id="cb15-53"><a href="#cb15-53" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">quatFromAxisAngle</span>(axis<span class="op">,</span> angle) {</span>
+<span id="cb15-54"><a href="#cb15-54" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> h <span class="op">=</span> angle <span class="op">/</span> <span class="dv">2</span><span class="op">;</span></span>
+<span id="cb15-55"><a href="#cb15-55" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> s <span class="op">=</span> <span class="bu">Math</span><span class="op">.</span><span class="fu">sin</span>(h)<span class="op">;</span></span>
+<span id="cb15-56"><a href="#cb15-56" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> [<span class="bu">Math</span><span class="op">.</span><span class="fu">cos</span>(h)<span class="op">,</span> axis<span class="op">.</span><span class="at">x</span><span class="op">*</span>s<span class="op">,</span> axis<span class="op">.</span><span class="at">y</span><span class="op">*</span>s<span class="op">,</span> axis<span class="op">.</span><span class="at">z</span><span class="op">*</span>s]<span class="op">;</span></span>
+<span id="cb15-57"><a href="#cb15-57" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb15-58"><a href="#cb15-58" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb15-59"><a href="#cb15-59" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">quatMultiply</span>(a<span class="op">,</span> b) {</span>
+<span id="cb15-60"><a href="#cb15-60" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> [w1<span class="op">,</span>x1<span class="op">,</span>y1<span class="op">,</span>z1] <span class="op">=</span> a<span class="op">;</span></span>
+<span id="cb15-61"><a href="#cb15-61" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> [w2<span class="op">,</span>x2<span class="op">,</span>y2<span class="op">,</span>z2] <span class="op">=</span> b<span class="op">;</span></span>
+<span id="cb15-62"><a href="#cb15-62" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> [</span>
+<span id="cb15-63"><a href="#cb15-63" aria-hidden="true" tabindex="-1"></a>        w1<span class="op">*</span>w2 <span class="op">-</span> x1<span class="op">*</span>x2 <span class="op">-</span> y1<span class="op">*</span>y2 <span class="op">-</span> z1<span class="op">*</span>z2<span class="op">,</span></span>
+<span id="cb15-64"><a href="#cb15-64" aria-hidden="true" tabindex="-1"></a>        w1<span class="op">*</span>x2 <span class="op">+</span> x1<span class="op">*</span>w2 <span class="op">+</span> y1<span class="op">*</span>z2 <span class="op">-</span> z1<span class="op">*</span>y2<span class="op">,</span></span>
+<span id="cb15-65"><a href="#cb15-65" aria-hidden="true" tabindex="-1"></a>        w1<span class="op">*</span>y2 <span class="op">-</span> x1<span class="op">*</span>z2 <span class="op">+</span> y1<span class="op">*</span>w2 <span class="op">+</span> z1<span class="op">*</span>x2<span class="op">,</span></span>
+<span id="cb15-66"><a href="#cb15-66" aria-hidden="true" tabindex="-1"></a>        w1<span class="op">*</span>z2 <span class="op">+</span> x1<span class="op">*</span>y2 <span class="op">-</span> y1<span class="op">*</span>x2 <span class="op">+</span> z1<span class="op">*</span>w2</span>
+<span id="cb15-67"><a href="#cb15-67" aria-hidden="true" tabindex="-1"></a>    ]<span class="op">;</span></span>
+<span id="cb15-68"><a href="#cb15-68" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb15-69"><a href="#cb15-69" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb15-70"><a href="#cb15-70" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">quatConjugate</span>(q) {</span>
+<span id="cb15-71"><a href="#cb15-71" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> [q[<span class="dv">0</span>]<span class="op">,</span> <span class="op">-</span>q[<span class="dv">1</span>]<span class="op">,</span> <span class="op">-</span>q[<span class="dv">2</span>]<span class="op">,</span> <span class="op">-</span>q[<span class="dv">3</span>]]<span class="op">;</span></span>
+<span id="cb15-72"><a href="#cb15-72" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb15-73"><a href="#cb15-73" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb15-74"><a href="#cb15-74" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">quatRotateVector</span>(q<span class="op">,</span> v) {</span>
+<span id="cb15-75"><a href="#cb15-75" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> qv <span class="op">=</span> [<span class="dv">0</span><span class="op">,</span> v<span class="op">.</span><span class="at">x</span><span class="op">,</span> v<span class="op">.</span><span class="at">y</span><span class="op">,</span> v<span class="op">.</span><span class="at">z</span>]<span class="op">;</span></span>
+<span id="cb15-76"><a href="#cb15-76" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> r <span class="op">=</span> <span class="fu">quatMultiply</span>(<span class="fu">quatMultiply</span>(q<span class="op">,</span> qv)<span class="op">,</span> <span class="fu">quatConjugate</span>(q))<span class="op">;</span></span>
+<span id="cb15-77"><a href="#cb15-77" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> { <span class="dt">x</span><span class="op">:</span> r[<span class="dv">1</span>]<span class="op">,</span> <span class="dt">y</span><span class="op">:</span> r[<span class="dv">2</span>]<span class="op">,</span> <span class="dt">z</span><span class="op">:</span> r[<span class="dv">3</span>] }<span class="op">;</span></span>
+<span id="cb15-78"><a href="#cb15-78" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb15-79"><a href="#cb15-79" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb15-80"><a href="#cb15-80" aria-hidden="true" tabindex="-1"></a><span class="co">/* ===================== CAMERA MATH ===================== */</span></span>
+<span id="cb15-81"><a href="#cb15-81" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">cameraForward</span>() {</span>
+<span id="cb15-82"><a href="#cb15-82" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> <span class="fu">quatRotateVector</span>(camera<span class="op">.</span><span class="at">rotation</span><span class="op">,</span> {<span class="dt">x</span><span class="op">:</span><span class="dv">0</span><span class="op">,</span><span class="dt">y</span><span class="op">:</span><span class="dv">0</span><span class="op">,</span><span class="dt">z</span><span class="op">:</span><span class="dv">1</span>})<span class="op">;</span></span>
+<span id="cb15-83"><a href="#cb15-83" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb15-84"><a href="#cb15-84" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">cameraRight</span>() {</span>
+<span id="cb15-85"><a href="#cb15-85" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> <span class="fu">quatRotateVector</span>(camera<span class="op">.</span><span class="at">rotation</span><span class="op">,</span> {<span class="dt">x</span><span class="op">:</span><span class="dv">1</span><span class="op">,</span><span class="dt">y</span><span class="op">:</span><span class="dv">0</span><span class="op">,</span><span class="dt">z</span><span class="op">:</span><span class="dv">0</span>})<span class="op">;</span></span>
+<span id="cb15-86"><a href="#cb15-86" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb15-87"><a href="#cb15-87" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">cameraUp</span>() {</span>
+<span id="cb15-88"><a href="#cb15-88" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> <span class="fu">quatRotateVector</span>(camera<span class="op">.</span><span class="at">rotation</span><span class="op">,</span> {<span class="dt">x</span><span class="op">:</span><span class="dv">0</span><span class="op">,</span><span class="dt">y</span><span class="op">:</span><span class="dv">1</span><span class="op">,</span><span class="dt">z</span><span class="op">:</span><span class="dv">0</span>})<span class="op">;</span></span>
+<span id="cb15-89"><a href="#cb15-89" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb15-90"><a href="#cb15-90" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb15-91"><a href="#cb15-91" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">applyCamera</span>(p) {</span>
+<span id="cb15-92"><a href="#cb15-92" aria-hidden="true" tabindex="-1"></a>    <span class="kw">let</span> v <span class="op">=</span> {</span>
+<span id="cb15-93"><a href="#cb15-93" aria-hidden="true" tabindex="-1"></a>        <span class="dt">x</span><span class="op">:</span> p<span class="op">.</span><span class="at">x</span> <span class="op">-</span> camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">x</span><span class="op">,</span></span>
+<span id="cb15-94"><a href="#cb15-94" aria-hidden="true" tabindex="-1"></a>        <span class="dt">y</span><span class="op">:</span> p<span class="op">.</span><span class="at">y</span> <span class="op">-</span> camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">y</span><span class="op">,</span></span>
+<span id="cb15-95"><a href="#cb15-95" aria-hidden="true" tabindex="-1"></a>        <span class="dt">z</span><span class="op">:</span> p<span class="op">.</span><span class="at">z</span> <span class="op">-</span> camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">z</span></span>
+<span id="cb15-96"><a href="#cb15-96" aria-hidden="true" tabindex="-1"></a>    }<span class="op">;</span></span>
+<span id="cb15-97"><a href="#cb15-97" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> <span class="fu">quatRotateVector</span>(<span class="fu">quatConjugate</span>(camera<span class="op">.</span><span class="at">rotation</span>)<span class="op">,</span> v)<span class="op">;</span></span>
+<span id="cb15-98"><a href="#cb15-98" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb15-99"><a href="#cb15-99" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb15-100"><a href="#cb15-100" aria-hidden="true" tabindex="-1"></a><span class="co">/* ===================== PROJECTION ===================== */</span></span>
+<span id="cb15-101"><a href="#cb15-101" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">project</span>(p) {</span>
+<span id="cb15-102"><a href="#cb15-102" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> { <span class="dt">x</span><span class="op">:</span> p<span class="op">.</span><span class="at">x</span> <span class="op">/</span> p<span class="op">.</span><span class="at">z</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span> p<span class="op">.</span><span class="at">y</span> <span class="op">/</span> p<span class="op">.</span><span class="at">z</span> }<span class="op">;</span></span>
+<span id="cb15-103"><a href="#cb15-103" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb15-104"><a href="#cb15-104" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">screen</span>(p) {</span>
+<span id="cb15-105"><a href="#cb15-105" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> {</span>
+<span id="cb15-106"><a href="#cb15-106" aria-hidden="true" tabindex="-1"></a>        <span class="dt">x</span><span class="op">:</span> (p<span class="op">.</span><span class="at">x</span> <span class="op">+</span> <span class="dv">1</span>) <span class="op">*</span> <span class="fl">0.5</span> <span class="op">*</span> game<span class="op">.</span><span class="at">width</span><span class="op">,</span></span>
+<span id="cb15-107"><a href="#cb15-107" aria-hidden="true" tabindex="-1"></a>        <span class="dt">y</span><span class="op">:</span> (<span class="dv">1</span> <span class="op">-</span> (p<span class="op">.</span><span class="at">y</span> <span class="op">+</span> <span class="dv">1</span>) <span class="op">*</span> <span class="fl">0.5</span>) <span class="op">*</span> game<span class="op">.</span><span class="at">height</span></span>
+<span id="cb15-108"><a href="#cb15-108" aria-hidden="true" tabindex="-1"></a>    }<span class="op">;</span></span>
+<span id="cb15-109"><a href="#cb15-109" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb15-110"><a href="#cb15-110" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb15-111"><a href="#cb15-111" aria-hidden="true" tabindex="-1"></a><span class="co">/* ===================== DRAW ===================== */</span></span>
+<span id="cb15-112"><a href="#cb15-112" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">clear</span>() {</span>
+<span id="cb15-113"><a href="#cb15-113" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="at">fillStyle</span> <span class="op">=</span> BACKGROUND<span class="op">;</span></span>
+<span id="cb15-114"><a href="#cb15-114" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="fu">fillRect</span>(<span class="dv">0</span><span class="op">,</span><span class="dv">0</span><span class="op">,</span>game<span class="op">.</span><span class="at">width</span><span class="op">,</span>game<span class="op">.</span><span class="at">height</span>)<span class="op">;</span></span>
+<span id="cb15-115"><a href="#cb15-115" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="at">fillStyle</span> <span class="op">=</span> TEXT<span class="op">;</span></span>
+<span id="cb15-116"><a href="#cb15-116" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="at">font</span> <span class="op">=</span> <span class="st">&quot;14px monospace&quot;</span><span class="op">;</span></span>
+<span id="cb15-117"><a href="#cb15-117" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="fu">fillText</span>(<span class="st">&quot;WASD: move | QE: up/down&quot;</span><span class="op">,</span> <span class="dv">10</span><span class="op">,</span> <span class="dv">18</span>)<span class="op">;</span></span>
+<span id="cb15-118"><a href="#cb15-118" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="fu">fillText</span>(<span class="st">&quot;Mouse: look | Click canvas to lock&quot;</span><span class="op">,</span> <span class="dv">10</span><span class="op">,</span> <span class="dv">36</span>)<span class="op">;</span></span>
+<span id="cb15-119"><a href="#cb15-119" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb15-120"><a href="#cb15-120" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb15-121"><a href="#cb15-121" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">line</span>(a<span class="op">,</span>b) {</span>
+<span id="cb15-122"><a href="#cb15-122" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="at">strokeStyle</span> <span class="op">=</span> FOREGROUND<span class="op">;</span></span>
+<span id="cb15-123"><a href="#cb15-123" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="at">lineWidth</span> <span class="op">=</span> <span class="dv">2</span><span class="op">;</span></span>
+<span id="cb15-124"><a href="#cb15-124" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="fu">beginPath</span>()<span class="op">;</span></span>
+<span id="cb15-125"><a href="#cb15-125" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="fu">moveTo</span>(a<span class="op">.</span><span class="at">x</span><span class="op">,</span>a<span class="op">.</span><span class="at">y</span>)<span class="op">;</span></span>
+<span id="cb15-126"><a href="#cb15-126" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="fu">lineTo</span>(b<span class="op">.</span><span class="at">x</span><span class="op">,</span>b<span class="op">.</span><span class="at">y</span>)<span class="op">;</span></span>
+<span id="cb15-127"><a href="#cb15-127" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="fu">stroke</span>()<span class="op">;</span></span>
+<span id="cb15-128"><a href="#cb15-128" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb15-129"><a href="#cb15-129" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb15-130"><a href="#cb15-130" aria-hidden="true" tabindex="-1"></a><span class="co">/* ===================== LOOP ===================== */</span></span>
+<span id="cb15-131"><a href="#cb15-131" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">frame</span>() {</span>
+<span id="cb15-132"><a href="#cb15-132" aria-hidden="true" tabindex="-1"></a>    angle <span class="op">+=</span> <span class="bu">Math</span><span class="op">.</span><span class="cn">PI</span> <span class="op">/</span> FPS<span class="op">;</span></span>
+<span id="cb15-133"><a href="#cb15-133" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb15-134"><a href="#cb15-134" aria-hidden="true" tabindex="-1"></a>    <span class="co">// cube rotation</span></span>
+<span id="cb15-135"><a href="#cb15-135" aria-hidden="true" tabindex="-1"></a>    <span class="kw">let</span> q <span class="op">=</span> [<span class="dv">1</span><span class="op">,</span><span class="dv">0</span><span class="op">,</span><span class="dv">0</span><span class="op">,</span><span class="dv">0</span>]<span class="op">;</span></span>
+<span id="cb15-136"><a href="#cb15-136" aria-hidden="true" tabindex="-1"></a>    <span class="cf">for</span> (<span class="kw">const</span> ax <span class="kw">of</span> activeAxes) {</span>
+<span id="cb15-137"><a href="#cb15-137" aria-hidden="true" tabindex="-1"></a>        <span class="cf">if</span> (ax<span class="op">===</span><span class="st">&quot;x&quot;</span>) q <span class="op">=</span> <span class="fu">quatMultiply</span>(<span class="fu">quatFromAxisAngle</span>({<span class="dt">x</span><span class="op">:</span><span class="dv">1</span><span class="op">,</span><span class="dt">y</span><span class="op">:</span><span class="dv">0</span><span class="op">,</span><span class="dt">z</span><span class="op">:</span><span class="dv">0</span>}<span class="op">,</span> angle)<span class="op">,</span> q)<span class="op">;</span></span>
+<span id="cb15-138"><a href="#cb15-138" aria-hidden="true" tabindex="-1"></a>        <span class="cf">if</span> (ax<span class="op">===</span><span class="st">&quot;y&quot;</span>) q <span class="op">=</span> <span class="fu">quatMultiply</span>(<span class="fu">quatFromAxisAngle</span>({<span class="dt">x</span><span class="op">:</span><span class="dv">0</span><span class="op">,</span><span class="dt">y</span><span class="op">:</span><span class="dv">1</span><span class="op">,</span><span class="dt">z</span><span class="op">:</span><span class="dv">0</span>}<span class="op">,</span> angle)<span class="op">,</span> q)<span class="op">;</span></span>
+<span id="cb15-139"><a href="#cb15-139" aria-hidden="true" tabindex="-1"></a>        <span class="cf">if</span> (ax<span class="op">===</span><span class="st">&quot;z&quot;</span>) q <span class="op">=</span> <span class="fu">quatMultiply</span>(<span class="fu">quatFromAxisAngle</span>({<span class="dt">x</span><span class="op">:</span><span class="dv">0</span><span class="op">,</span><span class="dt">y</span><span class="op">:</span><span class="dv">0</span><span class="op">,</span><span class="dt">z</span><span class="op">:</span><span class="dv">1</span>}<span class="op">,</span> angle)<span class="op">,</span> q)<span class="op">;</span></span>
+<span id="cb15-140"><a href="#cb15-140" aria-hidden="true" tabindex="-1"></a>    }</span>
+<span id="cb15-141"><a href="#cb15-141" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb15-142"><a href="#cb15-142" aria-hidden="true" tabindex="-1"></a>    <span class="co">// camera movement</span></span>
+<span id="cb15-143"><a href="#cb15-143" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> (keys[<span class="st">&quot;w&quot;</span>]) {</span>
+<span id="cb15-144"><a href="#cb15-144" aria-hidden="true" tabindex="-1"></a>        <span class="kw">const</span> f <span class="op">=</span> <span class="fu">cameraForward</span>()<span class="op">;</span></span>
+<span id="cb15-145"><a href="#cb15-145" aria-hidden="true" tabindex="-1"></a>        camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">x</span> <span class="op">+=</span> f<span class="op">.</span><span class="at">x</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">speed</span><span class="op">;</span></span>
+<span id="cb15-146"><a href="#cb15-146" aria-hidden="true" tabindex="-1"></a>        camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">y</span> <span class="op">+=</span> f<span class="op">.</span><span class="at">y</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">speed</span><span class="op">;</span></span>
+<span id="cb15-147"><a href="#cb15-147" aria-hidden="true" tabindex="-1"></a>        camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">z</span> <span class="op">+=</span> f<span class="op">.</span><span class="at">z</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">speed</span><span class="op">;</span></span>
+<span id="cb15-148"><a href="#cb15-148" aria-hidden="true" tabindex="-1"></a>    }</span>
+<span id="cb15-149"><a href="#cb15-149" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> (keys[<span class="st">&quot;s&quot;</span>]) {</span>
+<span id="cb15-150"><a href="#cb15-150" aria-hidden="true" tabindex="-1"></a>        <span class="kw">const</span> f <span class="op">=</span> <span class="fu">cameraForward</span>()<span class="op">;</span></span>
+<span id="cb15-151"><a href="#cb15-151" aria-hidden="true" tabindex="-1"></a>        camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">x</span> <span class="op">-=</span> f<span class="op">.</span><span class="at">x</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">speed</span><span class="op">;</span></span>
+<span id="cb15-152"><a href="#cb15-152" aria-hidden="true" tabindex="-1"></a>        camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">y</span> <span class="op">-=</span> f<span class="op">.</span><span class="at">y</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">speed</span><span class="op">;</span></span>
+<span id="cb15-153"><a href="#cb15-153" aria-hidden="true" tabindex="-1"></a>        camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">z</span> <span class="op">-=</span> f<span class="op">.</span><span class="at">z</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">speed</span><span class="op">;</span></span>
+<span id="cb15-154"><a href="#cb15-154" aria-hidden="true" tabindex="-1"></a>    }</span>
+<span id="cb15-155"><a href="#cb15-155" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> (keys[<span class="st">&quot;a&quot;</span>]) {</span>
+<span id="cb15-156"><a href="#cb15-156" aria-hidden="true" tabindex="-1"></a>        <span class="kw">const</span> r <span class="op">=</span> <span class="fu">cameraRight</span>()<span class="op">;</span></span>
+<span id="cb15-157"><a href="#cb15-157" aria-hidden="true" tabindex="-1"></a>        camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">x</span> <span class="op">-=</span> r<span class="op">.</span><span class="at">x</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">speed</span><span class="op">;</span></span>
+<span id="cb15-158"><a href="#cb15-158" aria-hidden="true" tabindex="-1"></a>        camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">y</span> <span class="op">-=</span> r<span class="op">.</span><span class="at">y</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">speed</span><span class="op">;</span></span>
+<span id="cb15-159"><a href="#cb15-159" aria-hidden="true" tabindex="-1"></a>        camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">z</span> <span class="op">-=</span> r<span class="op">.</span><span class="at">z</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">speed</span><span class="op">;</span></span>
+<span id="cb15-160"><a href="#cb15-160" aria-hidden="true" tabindex="-1"></a>    }</span>
+<span id="cb15-161"><a href="#cb15-161" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> (keys[<span class="st">&quot;d&quot;</span>]) {</span>
+<span id="cb15-162"><a href="#cb15-162" aria-hidden="true" tabindex="-1"></a>        <span class="kw">const</span> r <span class="op">=</span> <span class="fu">cameraRight</span>()<span class="op">;</span></span>
+<span id="cb15-163"><a href="#cb15-163" aria-hidden="true" tabindex="-1"></a>        camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">x</span> <span class="op">+=</span> r<span class="op">.</span><span class="at">x</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">speed</span><span class="op">;</span></span>
+<span id="cb15-164"><a href="#cb15-164" aria-hidden="true" tabindex="-1"></a>        camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">y</span> <span class="op">+=</span> r<span class="op">.</span><span class="at">y</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">speed</span><span class="op">;</span></span>
+<span id="cb15-165"><a href="#cb15-165" aria-hidden="true" tabindex="-1"></a>        camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">z</span> <span class="op">+=</span> r<span class="op">.</span><span class="at">z</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">speed</span><span class="op">;</span></span>
+<span id="cb15-166"><a href="#cb15-166" aria-hidden="true" tabindex="-1"></a>    }</span>
+<span id="cb15-167"><a href="#cb15-167" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> (keys[<span class="st">&quot;q&quot;</span>]) {</span>
+<span id="cb15-168"><a href="#cb15-168" aria-hidden="true" tabindex="-1"></a>        <span class="kw">const</span> u <span class="op">=</span> <span class="fu">cameraUp</span>()<span class="op">;</span></span>
+<span id="cb15-169"><a href="#cb15-169" aria-hidden="true" tabindex="-1"></a>        camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">x</span> <span class="op">-=</span> u<span class="op">.</span><span class="at">x</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">speed</span><span class="op">;</span></span>
+<span id="cb15-170"><a href="#cb15-170" aria-hidden="true" tabindex="-1"></a>        camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">y</span> <span class="op">-=</span> u<span class="op">.</span><span class="at">y</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">speed</span><span class="op">;</span></span>
+<span id="cb15-171"><a href="#cb15-171" aria-hidden="true" tabindex="-1"></a>        camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">z</span> <span class="op">-=</span> u<span class="op">.</span><span class="at">z</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">speed</span><span class="op">;</span></span>
+<span id="cb15-172"><a href="#cb15-172" aria-hidden="true" tabindex="-1"></a>    }</span>
+<span id="cb15-173"><a href="#cb15-173" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> (keys[<span class="st">&quot;e&quot;</span>]) {</span>
+<span id="cb15-174"><a href="#cb15-174" aria-hidden="true" tabindex="-1"></a>        <span class="kw">const</span> u <span class="op">=</span> <span class="fu">cameraUp</span>()<span class="op">;</span></span>
+<span id="cb15-175"><a href="#cb15-175" aria-hidden="true" tabindex="-1"></a>        camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">x</span> <span class="op">+=</span> u<span class="op">.</span><span class="at">x</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">speed</span><span class="op">;</span></span>
+<span id="cb15-176"><a href="#cb15-176" aria-hidden="true" tabindex="-1"></a>        camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">y</span> <span class="op">+=</span> u<span class="op">.</span><span class="at">y</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">speed</span><span class="op">;</span></span>
+<span id="cb15-177"><a href="#cb15-177" aria-hidden="true" tabindex="-1"></a>        camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">z</span> <span class="op">+=</span> u<span class="op">.</span><span class="at">z</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">speed</span><span class="op">;</span></span>
+<span id="cb15-178"><a href="#cb15-178" aria-hidden="true" tabindex="-1"></a>    }</span>
+<span id="cb15-179"><a href="#cb15-179" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb15-180"><a href="#cb15-180" aria-hidden="true" tabindex="-1"></a>    <span class="fu">clear</span>()<span class="op">;</span></span>
+<span id="cb15-181"><a href="#cb15-181" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb15-182"><a href="#cb15-182" aria-hidden="true" tabindex="-1"></a>    <span class="cf">for</span> (<span class="kw">const</span> e <span class="kw">of</span> edges) {</span>
+<span id="cb15-183"><a href="#cb15-183" aria-hidden="true" tabindex="-1"></a>        <span class="cf">for</span> (<span class="kw">let</span> i<span class="op">=</span><span class="dv">0</span><span class="op">;</span>i<span class="op">&lt;</span>e<span class="op">.</span><span class="at">length</span><span class="op">;</span>i<span class="op">++</span>) {</span>
+<span id="cb15-184"><a href="#cb15-184" aria-hidden="true" tabindex="-1"></a>            <span class="kw">let</span> a <span class="op">=</span> vertices[e[i]]<span class="op">;</span></span>
+<span id="cb15-185"><a href="#cb15-185" aria-hidden="true" tabindex="-1"></a>            <span class="kw">let</span> b <span class="op">=</span> vertices[e[(i<span class="op">+</span><span class="dv">1</span>)<span class="op">%</span>e<span class="op">.</span><span class="at">length</span>]]<span class="op">;</span></span>
+<span id="cb15-186"><a href="#cb15-186" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb15-187"><a href="#cb15-187" aria-hidden="true" tabindex="-1"></a>            a <span class="op">=</span> <span class="fu">quatRotateVector</span>(q<span class="op">,</span> a)<span class="op">;</span></span>
+<span id="cb15-188"><a href="#cb15-188" aria-hidden="true" tabindex="-1"></a>            b <span class="op">=</span> <span class="fu">quatRotateVector</span>(q<span class="op">,</span> b)<span class="op">;</span></span>
+<span id="cb15-189"><a href="#cb15-189" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb15-190"><a href="#cb15-190" aria-hidden="true" tabindex="-1"></a>            a <span class="op">=</span> { <span class="dt">x</span><span class="op">:</span>a<span class="op">.</span><span class="at">x</span><span class="op">+</span>objectPos<span class="op">.</span><span class="at">x</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span>a<span class="op">.</span><span class="at">y</span><span class="op">+</span>objectPos<span class="op">.</span><span class="at">y</span><span class="op">,</span> <span class="dt">z</span><span class="op">:</span>a<span class="op">.</span><span class="at">z</span><span class="op">+</span>objectPos<span class="op">.</span><span class="at">z</span> }<span class="op">;</span></span>
+<span id="cb15-191"><a href="#cb15-191" aria-hidden="true" tabindex="-1"></a>            b <span class="op">=</span> { <span class="dt">x</span><span class="op">:</span>b<span class="op">.</span><span class="at">x</span><span class="op">+</span>objectPos<span class="op">.</span><span class="at">x</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span>b<span class="op">.</span><span class="at">y</span><span class="op">+</span>objectPos<span class="op">.</span><span class="at">y</span><span class="op">,</span> <span class="dt">z</span><span class="op">:</span>b<span class="op">.</span><span class="at">z</span><span class="op">+</span>objectPos<span class="op">.</span><span class="at">z</span> }<span class="op">;</span></span>
+<span id="cb15-192"><a href="#cb15-192" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb15-193"><a href="#cb15-193" aria-hidden="true" tabindex="-1"></a>            a <span class="op">=</span> <span class="fu">applyCamera</span>(a)<span class="op">;</span></span>
+<span id="cb15-194"><a href="#cb15-194" aria-hidden="true" tabindex="-1"></a>            b <span class="op">=</span> <span class="fu">applyCamera</span>(b)<span class="op">;</span></span>
+<span id="cb15-195"><a href="#cb15-195" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb15-196"><a href="#cb15-196" aria-hidden="true" tabindex="-1"></a>            <span class="cf">if</span> (a<span class="op">.</span><span class="at">z</span><span class="op">&lt;=</span>NEAR <span class="op">||</span> b<span class="op">.</span><span class="at">z</span><span class="op">&lt;=</span>NEAR) <span class="cf">continue</span><span class="op">;</span></span>
+<span id="cb15-197"><a href="#cb15-197" aria-hidden="true" tabindex="-1"></a>            <span class="fu">line</span>(<span class="bu">screen</span>(<span class="fu">project</span>(a))<span class="op">,</span> <span class="bu">screen</span>(<span class="fu">project</span>(b)))<span class="op">;</span></span>
+<span id="cb15-198"><a href="#cb15-198" aria-hidden="true" tabindex="-1"></a>        }</span>
+<span id="cb15-199"><a href="#cb15-199" aria-hidden="true" tabindex="-1"></a>    }</span>
+<span id="cb15-200"><a href="#cb15-200" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb15-201"><a href="#cb15-201" aria-hidden="true" tabindex="-1"></a>    <span class="pp">setTimeout</span>(frame<span class="op">,</span> <span class="dv">1000</span><span class="op">/</span>FPS)<span class="op">;</span></span>
+<span id="cb15-202"><a href="#cb15-202" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb15-203"><a href="#cb15-203" aria-hidden="true" tabindex="-1"></a><span class="fu">frame</span>()<span class="op">;</span></span>
+<span id="cb15-204"><a href="#cb15-204" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb15-205"><a href="#cb15-205" aria-hidden="true" tabindex="-1"></a><span class="co">/* ===================== MOUSE LOOK ===================== */</span></span>
+<span id="cb15-206"><a href="#cb15-206" aria-hidden="true" tabindex="-1"></a>game<span class="op">.</span><span class="at">onclick</span> <span class="op">=</span> () <span class="kw">=&gt;</span> game<span class="op">.</span><span class="fu">requestPointerLock</span>()<span class="op">;</span></span>
+<span id="cb15-207"><a href="#cb15-207" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb15-208"><a href="#cb15-208" aria-hidden="true" tabindex="-1"></a><span class="bu">window</span><span class="op">.</span><span class="fu">addEventListener</span>(<span class="st">&quot;mousemove&quot;</span><span class="op">,</span> e <span class="kw">=&gt;</span> {</span>
+<span id="cb15-209"><a href="#cb15-209" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> (<span class="bu">document</span><span class="op">.</span><span class="at">pointerLockElement</span> <span class="op">!==</span> game) <span class="cf">return</span><span class="op">;</span></span>
+<span id="cb15-210"><a href="#cb15-210" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb15-211"><a href="#cb15-211" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> yaw   <span class="op">=</span> <span class="fu">quatFromAxisAngle</span>({<span class="dt">x</span><span class="op">:</span><span class="dv">0</span><span class="op">,</span><span class="dt">y</span><span class="op">:</span><span class="dv">1</span><span class="op">,</span><span class="dt">z</span><span class="op">:</span><span class="dv">0</span>}<span class="op">,</span> <span class="op">-</span>e<span class="op">.</span><span class="at">movementX</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">sensitivity</span>)<span class="op">;</span></span>
+<span id="cb15-212"><a href="#cb15-212" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> pitch <span class="op">=</span> <span class="fu">quatFromAxisAngle</span>({<span class="dt">x</span><span class="op">:</span><span class="dv">1</span><span class="op">,</span><span class="dt">y</span><span class="op">:</span><span class="dv">0</span><span class="op">,</span><span class="dt">z</span><span class="op">:</span><span class="dv">0</span>}<span class="op">,</span> <span class="op">-</span>e<span class="op">.</span><span class="at">movementY</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">sensitivity</span>)<span class="op">;</span></span>
+<span id="cb15-213"><a href="#cb15-213" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb15-214"><a href="#cb15-214" aria-hidden="true" tabindex="-1"></a>    camera<span class="op">.</span><span class="at">rotation</span> <span class="op">=</span> <span class="fu">quatMultiply</span>(yaw<span class="op">,</span> camera<span class="op">.</span><span class="at">rotation</span>)<span class="op">;</span></span>
+<span id="cb15-215"><a href="#cb15-215" aria-hidden="true" tabindex="-1"></a>    camera<span class="op">.</span><span class="at">rotation</span> <span class="op">=</span> <span class="fu">quatMultiply</span>(camera<span class="op">.</span><span class="at">rotation</span><span class="op">,</span> pitch)<span class="op">;</span></span>
+<span id="cb15-216"><a href="#cb15-216" aria-hidden="true" tabindex="-1"></a>})<span class="op">;</span></span>
+<span id="cb15-217"><a href="#cb15-217" aria-hidden="true" tabindex="-1"></a><span class="dt">&lt;/</span><span class="kw">script</span><span class="dt">&gt;</span></span></code></pre></div>
+<hr />
+<h2 id="what-you-now-have">🎯 What you now have</h2>
+<p>✔ <strong>True fly camera</strong> ✔ Movement relative to view
+direction ✔ Mouse-look yaw + pitch ✔ Quaternion-based, no gimbal lock ✔
+Identical behavior to Unity / Unreal fly mode</p>
+<p>This is <em>real engine-level camera control</em>.</p>
+<hr />
+<h3 id="next-upgrades-strongly-recommended">🔥 Next upgrades (strongly
+recommended)</h3>
+<p>1️⃣ Clamp pitch to avoid flipping 2️⃣ Add roll (airplane-style) 3️⃣ Add
+acceleration / inertia 4️⃣ Add multiple objects + scene graph 5️⃣ Replace
+projection with real FOV math</p>
+<p>If you want any of those, say which one — you’re doing excellent
+work.</p>
+<p>Yes — below is a <strong>full rewrite of the fly-camera
+version</strong> that <strong>keeps the ability to change which axes the
+cube rotates on</strong>, exactly like your earlier versions.</p>
+<p>Nothing is removed:</p>
+<ul>
+<li>✅ Fly camera (WASD + QE + mouse look)</li>
+<li>✅ Quaternion camera</li>
+<li>✅ Quaternion cube rotation</li>
+<li>✅ <strong>Click to cycle cube rotation axes</strong></li>
+<li>✅ Clean, correct 3D pipeline</li>
+</ul>
+<hr />
+<h2 id="full-fly-camera-rotation-axis-toggle-version">✅ FULL FLY-CAMERA
++ ROTATION-AXIS TOGGLE VERSION</h2>
+<div class="sourceCode" id="cb16"><pre
+class="sourceCode html"><code class="sourceCode html"><span id="cb16-1"><a href="#cb16-1" aria-hidden="true" tabindex="-1"></a><span class="dt">&lt;</span><span class="kw">canvas</span> <span class="er">id</span><span class="ot">=</span><span class="st">&quot;game&quot;</span><span class="dt">&gt;&lt;/</span><span class="kw">canvas</span><span class="dt">&gt;</span></span>
+<span id="cb16-2"><a href="#cb16-2" aria-hidden="true" tabindex="-1"></a><span class="dt">&lt;</span><span class="kw">script</span><span class="dt">&gt;</span></span>
+<span id="cb16-3"><a href="#cb16-3" aria-hidden="true" tabindex="-1"></a><span class="co">/* ===================== SETUP ===================== */</span></span>
+<span id="cb16-4"><a href="#cb16-4" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> game <span class="op">=</span> <span class="bu">document</span><span class="op">.</span><span class="fu">getElementById</span>(<span class="st">&quot;game&quot;</span>)<span class="op">;</span></span>
+<span id="cb16-5"><a href="#cb16-5" aria-hidden="true" tabindex="-1"></a>game<span class="op">.</span><span class="at">width</span> <span class="op">=</span> <span class="dv">400</span><span class="op">;</span></span>
+<span id="cb16-6"><a href="#cb16-6" aria-hidden="true" tabindex="-1"></a>game<span class="op">.</span><span class="at">height</span> <span class="op">=</span> <span class="dv">400</span><span class="op">;</span></span>
+<span id="cb16-7"><a href="#cb16-7" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> ctx <span class="op">=</span> game<span class="op">.</span><span class="fu">getContext</span>(<span class="st">&quot;2d&quot;</span>)<span class="op">;</span></span>
+<span id="cb16-8"><a href="#cb16-8" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb16-9"><a href="#cb16-9" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> BACKGROUND <span class="op">=</span> <span class="st">&quot;#101010&quot;</span><span class="op">;</span></span>
+<span id="cb16-10"><a href="#cb16-10" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> FOREGROUND <span class="op">=</span> <span class="st">&quot;#50FF50&quot;</span><span class="op">;</span></span>
+<span id="cb16-11"><a href="#cb16-11" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> TEXT <span class="op">=</span> <span class="st">&quot;#FFFFFF&quot;</span><span class="op">;</span></span>
+<span id="cb16-12"><a href="#cb16-12" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb16-13"><a href="#cb16-13" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> FPS <span class="op">=</span> <span class="dv">60</span><span class="op">;</span></span>
+<span id="cb16-14"><a href="#cb16-14" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> NEAR <span class="op">=</span> <span class="fl">0.1</span><span class="op">;</span></span>
+<span id="cb16-15"><a href="#cb16-15" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb16-16"><a href="#cb16-16" aria-hidden="true" tabindex="-1"></a><span class="co">/* ===================== GEOMETRY ===================== */</span></span>
+<span id="cb16-17"><a href="#cb16-17" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> vertices <span class="op">=</span> [</span>
+<span id="cb16-18"><a href="#cb16-18" aria-hidden="true" tabindex="-1"></a>    {<span class="dt">x</span><span class="op">:</span>  <span class="fl">0.25</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span>  <span class="fl">0.25</span><span class="op">,</span> <span class="dt">z</span><span class="op">:</span>  <span class="fl">0.25</span>}<span class="op">,</span></span>
+<span id="cb16-19"><a href="#cb16-19" aria-hidden="true" tabindex="-1"></a>    {<span class="dt">x</span><span class="op">:</span> <span class="op">-</span><span class="fl">0.25</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span>  <span class="fl">0.25</span><span class="op">,</span> <span class="dt">z</span><span class="op">:</span>  <span class="fl">0.25</span>}<span class="op">,</span></span>
+<span id="cb16-20"><a href="#cb16-20" aria-hidden="true" tabindex="-1"></a>    {<span class="dt">x</span><span class="op">:</span> <span class="op">-</span><span class="fl">0.25</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span> <span class="op">-</span><span class="fl">0.25</span><span class="op">,</span> <span class="dt">z</span><span class="op">:</span>  <span class="fl">0.25</span>}<span class="op">,</span></span>
+<span id="cb16-21"><a href="#cb16-21" aria-hidden="true" tabindex="-1"></a>    {<span class="dt">x</span><span class="op">:</span>  <span class="fl">0.25</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span> <span class="op">-</span><span class="fl">0.25</span><span class="op">,</span> <span class="dt">z</span><span class="op">:</span>  <span class="fl">0.25</span>}<span class="op">,</span></span>
+<span id="cb16-22"><a href="#cb16-22" aria-hidden="true" tabindex="-1"></a>    {<span class="dt">x</span><span class="op">:</span>  <span class="fl">0.25</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span>  <span class="fl">0.25</span><span class="op">,</span> <span class="dt">z</span><span class="op">:</span> <span class="op">-</span><span class="fl">0.25</span>}<span class="op">,</span></span>
+<span id="cb16-23"><a href="#cb16-23" aria-hidden="true" tabindex="-1"></a>    {<span class="dt">x</span><span class="op">:</span> <span class="op">-</span><span class="fl">0.25</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span>  <span class="fl">0.25</span><span class="op">,</span> <span class="dt">z</span><span class="op">:</span> <span class="op">-</span><span class="fl">0.25</span>}<span class="op">,</span></span>
+<span id="cb16-24"><a href="#cb16-24" aria-hidden="true" tabindex="-1"></a>    {<span class="dt">x</span><span class="op">:</span> <span class="op">-</span><span class="fl">0.25</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span> <span class="op">-</span><span class="fl">0.25</span><span class="op">,</span> <span class="dt">z</span><span class="op">:</span> <span class="op">-</span><span class="fl">0.25</span>}<span class="op">,</span></span>
+<span id="cb16-25"><a href="#cb16-25" aria-hidden="true" tabindex="-1"></a>    {<span class="dt">x</span><span class="op">:</span>  <span class="fl">0.25</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span> <span class="op">-</span><span class="fl">0.25</span><span class="op">,</span> <span class="dt">z</span><span class="op">:</span> <span class="op">-</span><span class="fl">0.25</span>}<span class="op">,</span></span>
+<span id="cb16-26"><a href="#cb16-26" aria-hidden="true" tabindex="-1"></a>]<span class="op">;</span></span>
+<span id="cb16-27"><a href="#cb16-27" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb16-28"><a href="#cb16-28" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> edges <span class="op">=</span> [</span>
+<span id="cb16-29"><a href="#cb16-29" aria-hidden="true" tabindex="-1"></a>    [<span class="dv">0</span><span class="op">,</span><span class="dv">1</span><span class="op">,</span><span class="dv">2</span><span class="op">,</span><span class="dv">3</span>]<span class="op">,</span></span>
+<span id="cb16-30"><a href="#cb16-30" aria-hidden="true" tabindex="-1"></a>    [<span class="dv">4</span><span class="op">,</span><span class="dv">5</span><span class="op">,</span><span class="dv">6</span><span class="op">,</span><span class="dv">7</span>]<span class="op">,</span></span>
+<span id="cb16-31"><a href="#cb16-31" aria-hidden="true" tabindex="-1"></a>    [<span class="dv">0</span><span class="op">,</span><span class="dv">4</span>]<span class="op">,</span>[<span class="dv">1</span><span class="op">,</span><span class="dv">5</span>]<span class="op">,</span>[<span class="dv">2</span><span class="op">,</span><span class="dv">6</span>]<span class="op">,</span>[<span class="dv">3</span><span class="op">,</span><span class="dv">7</span>]</span>
+<span id="cb16-32"><a href="#cb16-32" aria-hidden="true" tabindex="-1"></a>]<span class="op">;</span></span>
+<span id="cb16-33"><a href="#cb16-33" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb16-34"><a href="#cb16-34" aria-hidden="true" tabindex="-1"></a><span class="co">/* ===================== OBJECT ===================== */</span></span>
+<span id="cb16-35"><a href="#cb16-35" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> objectPos <span class="op">=</span> { <span class="dt">x</span><span class="op">:</span> <span class="dv">0</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span> <span class="dv">0</span><span class="op">,</span> <span class="dt">z</span><span class="op">:</span> <span class="dv">2</span> }<span class="op">;</span></span>
+<span id="cb16-36"><a href="#cb16-36" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> angle <span class="op">=</span> <span class="dv">0</span><span class="op">;</span></span>
+<span id="cb16-37"><a href="#cb16-37" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb16-38"><a href="#cb16-38" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> AXIS_MODES <span class="op">=</span> [</span>
+<span id="cb16-39"><a href="#cb16-39" aria-hidden="true" tabindex="-1"></a>    [<span class="st">&quot;x&quot;</span>]<span class="op">,</span> [<span class="st">&quot;y&quot;</span>]<span class="op">,</span> [<span class="st">&quot;z&quot;</span>]<span class="op">,</span></span>
+<span id="cb16-40"><a href="#cb16-40" aria-hidden="true" tabindex="-1"></a>    [<span class="st">&quot;x&quot;</span><span class="op">,</span><span class="st">&quot;y&quot;</span>]<span class="op">,</span> [<span class="st">&quot;y&quot;</span><span class="op">,</span><span class="st">&quot;z&quot;</span>]<span class="op">,</span> [<span class="st">&quot;x&quot;</span><span class="op">,</span><span class="st">&quot;z&quot;</span>]<span class="op">,</span></span>
+<span id="cb16-41"><a href="#cb16-41" aria-hidden="true" tabindex="-1"></a>    [<span class="st">&quot;x&quot;</span><span class="op">,</span><span class="st">&quot;y&quot;</span><span class="op">,</span><span class="st">&quot;z&quot;</span>]</span>
+<span id="cb16-42"><a href="#cb16-42" aria-hidden="true" tabindex="-1"></a>]<span class="op">;</span></span>
+<span id="cb16-43"><a href="#cb16-43" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> axisModeIndex <span class="op">=</span> <span class="dv">0</span><span class="op">;</span></span>
+<span id="cb16-44"><a href="#cb16-44" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> activeAxes <span class="op">=</span> AXIS_MODES[axisModeIndex]<span class="op">;</span></span>
+<span id="cb16-45"><a href="#cb16-45" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb16-46"><a href="#cb16-46" aria-hidden="true" tabindex="-1"></a><span class="co">/* ===================== CAMERA ===================== */</span></span>
+<span id="cb16-47"><a href="#cb16-47" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> camera <span class="op">=</span> {</span>
+<span id="cb16-48"><a href="#cb16-48" aria-hidden="true" tabindex="-1"></a>    <span class="dt">position</span><span class="op">:</span> { <span class="dt">x</span><span class="op">:</span> <span class="dv">0</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span> <span class="dv">0</span><span class="op">,</span> <span class="dt">z</span><span class="op">:</span> <span class="dv">0</span> }<span class="op">,</span></span>
+<span id="cb16-49"><a href="#cb16-49" aria-hidden="true" tabindex="-1"></a>    <span class="dt">rotation</span><span class="op">:</span> [<span class="dv">1</span><span class="op">,</span><span class="dv">0</span><span class="op">,</span><span class="dv">0</span><span class="op">,</span><span class="dv">0</span>]<span class="op">,</span></span>
+<span id="cb16-50"><a href="#cb16-50" aria-hidden="true" tabindex="-1"></a>    <span class="dt">speed</span><span class="op">:</span> <span class="fl">0.05</span><span class="op">,</span></span>
+<span id="cb16-51"><a href="#cb16-51" aria-hidden="true" tabindex="-1"></a>    <span class="dt">sensitivity</span><span class="op">:</span> <span class="fl">0.002</span></span>
+<span id="cb16-52"><a href="#cb16-52" aria-hidden="true" tabindex="-1"></a>}<span class="op">;</span></span>
+<span id="cb16-53"><a href="#cb16-53" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb16-54"><a href="#cb16-54" aria-hidden="true" tabindex="-1"></a><span class="co">/* ===================== INPUT ===================== */</span></span>
+<span id="cb16-55"><a href="#cb16-55" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> keys <span class="op">=</span> {}<span class="op">;</span></span>
+<span id="cb16-56"><a href="#cb16-56" aria-hidden="true" tabindex="-1"></a><span class="bu">window</span><span class="op">.</span><span class="fu">addEventListener</span>(<span class="st">&quot;keydown&quot;</span><span class="op">,</span> e <span class="kw">=&gt;</span> keys[e<span class="op">.</span><span class="at">key</span><span class="op">.</span><span class="fu">toLowerCase</span>()] <span class="op">=</span> <span class="kw">true</span>)<span class="op">;</span></span>
+<span id="cb16-57"><a href="#cb16-57" aria-hidden="true" tabindex="-1"></a><span class="bu">window</span><span class="op">.</span><span class="fu">addEventListener</span>(<span class="st">&quot;keyup&quot;</span><span class="op">,</span>   e <span class="kw">=&gt;</span> keys[e<span class="op">.</span><span class="at">key</span><span class="op">.</span><span class="fu">toLowerCase</span>()] <span class="op">=</span> <span class="kw">false</span>)<span class="op">;</span></span>
+<span id="cb16-58"><a href="#cb16-58" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb16-59"><a href="#cb16-59" aria-hidden="true" tabindex="-1"></a><span class="co">/* ===================== QUATERNIONS ===================== */</span></span>
+<span id="cb16-60"><a href="#cb16-60" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">quatFromAxisAngle</span>(axis<span class="op">,</span> angle) {</span>
+<span id="cb16-61"><a href="#cb16-61" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> h <span class="op">=</span> angle <span class="op">/</span> <span class="dv">2</span><span class="op">;</span></span>
+<span id="cb16-62"><a href="#cb16-62" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> s <span class="op">=</span> <span class="bu">Math</span><span class="op">.</span><span class="fu">sin</span>(h)<span class="op">;</span></span>
+<span id="cb16-63"><a href="#cb16-63" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> [<span class="bu">Math</span><span class="op">.</span><span class="fu">cos</span>(h)<span class="op">,</span> axis<span class="op">.</span><span class="at">x</span><span class="op">*</span>s<span class="op">,</span> axis<span class="op">.</span><span class="at">y</span><span class="op">*</span>s<span class="op">,</span> axis<span class="op">.</span><span class="at">z</span><span class="op">*</span>s]<span class="op">;</span></span>
+<span id="cb16-64"><a href="#cb16-64" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb16-65"><a href="#cb16-65" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb16-66"><a href="#cb16-66" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">quatMultiply</span>(a<span class="op">,</span> b) {</span>
+<span id="cb16-67"><a href="#cb16-67" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> [w1<span class="op">,</span>x1<span class="op">,</span>y1<span class="op">,</span>z1] <span class="op">=</span> a<span class="op">;</span></span>
+<span id="cb16-68"><a href="#cb16-68" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> [w2<span class="op">,</span>x2<span class="op">,</span>y2<span class="op">,</span>z2] <span class="op">=</span> b<span class="op">;</span></span>
+<span id="cb16-69"><a href="#cb16-69" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> [</span>
+<span id="cb16-70"><a href="#cb16-70" aria-hidden="true" tabindex="-1"></a>        w1<span class="op">*</span>w2 <span class="op">-</span> x1<span class="op">*</span>x2 <span class="op">-</span> y1<span class="op">*</span>y2 <span class="op">-</span> z1<span class="op">*</span>z2<span class="op">,</span></span>
+<span id="cb16-71"><a href="#cb16-71" aria-hidden="true" tabindex="-1"></a>        w1<span class="op">*</span>x2 <span class="op">+</span> x1<span class="op">*</span>w2 <span class="op">+</span> y1<span class="op">*</span>z2 <span class="op">-</span> z1<span class="op">*</span>y2<span class="op">,</span></span>
+<span id="cb16-72"><a href="#cb16-72" aria-hidden="true" tabindex="-1"></a>        w1<span class="op">*</span>y2 <span class="op">-</span> x1<span class="op">*</span>z2 <span class="op">+</span> y1<span class="op">*</span>w2 <span class="op">+</span> z1<span class="op">*</span>x2<span class="op">,</span></span>
+<span id="cb16-73"><a href="#cb16-73" aria-hidden="true" tabindex="-1"></a>        w1<span class="op">*</span>z2 <span class="op">+</span> x1<span class="op">*</span>y2 <span class="op">-</span> y1<span class="op">*</span>x2 <span class="op">+</span> z1<span class="op">*</span>w2</span>
+<span id="cb16-74"><a href="#cb16-74" aria-hidden="true" tabindex="-1"></a>    ]<span class="op">;</span></span>
+<span id="cb16-75"><a href="#cb16-75" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb16-76"><a href="#cb16-76" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb16-77"><a href="#cb16-77" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">quatConjugate</span>(q) {</span>
+<span id="cb16-78"><a href="#cb16-78" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> [q[<span class="dv">0</span>]<span class="op">,</span> <span class="op">-</span>q[<span class="dv">1</span>]<span class="op">,</span> <span class="op">-</span>q[<span class="dv">2</span>]<span class="op">,</span> <span class="op">-</span>q[<span class="dv">3</span>]]<span class="op">;</span></span>
+<span id="cb16-79"><a href="#cb16-79" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb16-80"><a href="#cb16-80" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb16-81"><a href="#cb16-81" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">quatRotateVector</span>(q<span class="op">,</span> v) {</span>
+<span id="cb16-82"><a href="#cb16-82" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> qv <span class="op">=</span> [<span class="dv">0</span><span class="op">,</span> v<span class="op">.</span><span class="at">x</span><span class="op">,</span> v<span class="op">.</span><span class="at">y</span><span class="op">,</span> v<span class="op">.</span><span class="at">z</span>]<span class="op">;</span></span>
+<span id="cb16-83"><a href="#cb16-83" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> r <span class="op">=</span> <span class="fu">quatMultiply</span>(<span class="fu">quatMultiply</span>(q<span class="op">,</span> qv)<span class="op">,</span> <span class="fu">quatConjugate</span>(q))<span class="op">;</span></span>
+<span id="cb16-84"><a href="#cb16-84" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> { <span class="dt">x</span><span class="op">:</span> r[<span class="dv">1</span>]<span class="op">,</span> <span class="dt">y</span><span class="op">:</span> r[<span class="dv">2</span>]<span class="op">,</span> <span class="dt">z</span><span class="op">:</span> r[<span class="dv">3</span>] }<span class="op">;</span></span>
+<span id="cb16-85"><a href="#cb16-85" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb16-86"><a href="#cb16-86" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb16-87"><a href="#cb16-87" aria-hidden="true" tabindex="-1"></a><span class="co">/* ===================== CAMERA VECTORS ===================== */</span></span>
+<span id="cb16-88"><a href="#cb16-88" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">cameraForward</span>() {</span>
+<span id="cb16-89"><a href="#cb16-89" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> <span class="fu">quatRotateVector</span>(camera<span class="op">.</span><span class="at">rotation</span><span class="op">,</span> {<span class="dt">x</span><span class="op">:</span><span class="dv">0</span><span class="op">,</span><span class="dt">y</span><span class="op">:</span><span class="dv">0</span><span class="op">,</span><span class="dt">z</span><span class="op">:</span><span class="dv">1</span>})<span class="op">;</span></span>
+<span id="cb16-90"><a href="#cb16-90" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb16-91"><a href="#cb16-91" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">cameraRight</span>() {</span>
+<span id="cb16-92"><a href="#cb16-92" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> <span class="fu">quatRotateVector</span>(camera<span class="op">.</span><span class="at">rotation</span><span class="op">,</span> {<span class="dt">x</span><span class="op">:</span><span class="dv">1</span><span class="op">,</span><span class="dt">y</span><span class="op">:</span><span class="dv">0</span><span class="op">,</span><span class="dt">z</span><span class="op">:</span><span class="dv">0</span>})<span class="op">;</span></span>
+<span id="cb16-93"><a href="#cb16-93" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb16-94"><a href="#cb16-94" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">cameraUp</span>() {</span>
+<span id="cb16-95"><a href="#cb16-95" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> <span class="fu">quatRotateVector</span>(camera<span class="op">.</span><span class="at">rotation</span><span class="op">,</span> {<span class="dt">x</span><span class="op">:</span><span class="dv">0</span><span class="op">,</span><span class="dt">y</span><span class="op">:</span><span class="dv">1</span><span class="op">,</span><span class="dt">z</span><span class="op">:</span><span class="dv">0</span>})<span class="op">;</span></span>
+<span id="cb16-96"><a href="#cb16-96" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb16-97"><a href="#cb16-97" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb16-98"><a href="#cb16-98" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">applyCamera</span>(p) {</span>
+<span id="cb16-99"><a href="#cb16-99" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> v <span class="op">=</span> {</span>
+<span id="cb16-100"><a href="#cb16-100" aria-hidden="true" tabindex="-1"></a>        <span class="dt">x</span><span class="op">:</span> p<span class="op">.</span><span class="at">x</span> <span class="op">-</span> camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">x</span><span class="op">,</span></span>
+<span id="cb16-101"><a href="#cb16-101" aria-hidden="true" tabindex="-1"></a>        <span class="dt">y</span><span class="op">:</span> p<span class="op">.</span><span class="at">y</span> <span class="op">-</span> camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">y</span><span class="op">,</span></span>
+<span id="cb16-102"><a href="#cb16-102" aria-hidden="true" tabindex="-1"></a>        <span class="dt">z</span><span class="op">:</span> p<span class="op">.</span><span class="at">z</span> <span class="op">-</span> camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">z</span></span>
+<span id="cb16-103"><a href="#cb16-103" aria-hidden="true" tabindex="-1"></a>    }<span class="op">;</span></span>
+<span id="cb16-104"><a href="#cb16-104" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> <span class="fu">quatRotateVector</span>(<span class="fu">quatConjugate</span>(camera<span class="op">.</span><span class="at">rotation</span>)<span class="op">,</span> v)<span class="op">;</span></span>
+<span id="cb16-105"><a href="#cb16-105" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb16-106"><a href="#cb16-106" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb16-107"><a href="#cb16-107" aria-hidden="true" tabindex="-1"></a><span class="co">/* ===================== PROJECTION ===================== */</span></span>
+<span id="cb16-108"><a href="#cb16-108" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">project</span>(p) {</span>
+<span id="cb16-109"><a href="#cb16-109" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> { <span class="dt">x</span><span class="op">:</span> p<span class="op">.</span><span class="at">x</span> <span class="op">/</span> p<span class="op">.</span><span class="at">z</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span> p<span class="op">.</span><span class="at">y</span> <span class="op">/</span> p<span class="op">.</span><span class="at">z</span> }<span class="op">;</span></span>
+<span id="cb16-110"><a href="#cb16-110" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb16-111"><a href="#cb16-111" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">screen</span>(p) {</span>
+<span id="cb16-112"><a href="#cb16-112" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> {</span>
+<span id="cb16-113"><a href="#cb16-113" aria-hidden="true" tabindex="-1"></a>        <span class="dt">x</span><span class="op">:</span> (p<span class="op">.</span><span class="at">x</span> <span class="op">+</span> <span class="dv">1</span>) <span class="op">*</span> <span class="fl">0.5</span> <span class="op">*</span> game<span class="op">.</span><span class="at">width</span><span class="op">,</span></span>
+<span id="cb16-114"><a href="#cb16-114" aria-hidden="true" tabindex="-1"></a>        <span class="dt">y</span><span class="op">:</span> (<span class="dv">1</span> <span class="op">-</span> (p<span class="op">.</span><span class="at">y</span> <span class="op">+</span> <span class="dv">1</span>) <span class="op">*</span> <span class="fl">0.5</span>) <span class="op">*</span> game<span class="op">.</span><span class="at">height</span></span>
+<span id="cb16-115"><a href="#cb16-115" aria-hidden="true" tabindex="-1"></a>    }<span class="op">;</span></span>
+<span id="cb16-116"><a href="#cb16-116" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb16-117"><a href="#cb16-117" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb16-118"><a href="#cb16-118" aria-hidden="true" tabindex="-1"></a><span class="co">/* ===================== DRAW ===================== */</span></span>
+<span id="cb16-119"><a href="#cb16-119" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">clear</span>() {</span>
+<span id="cb16-120"><a href="#cb16-120" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="at">fillStyle</span> <span class="op">=</span> BACKGROUND<span class="op">;</span></span>
+<span id="cb16-121"><a href="#cb16-121" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="fu">fillRect</span>(<span class="dv">0</span><span class="op">,</span><span class="dv">0</span><span class="op">,</span>game<span class="op">.</span><span class="at">width</span><span class="op">,</span>game<span class="op">.</span><span class="at">height</span>)<span class="op">;</span></span>
+<span id="cb16-122"><a href="#cb16-122" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb16-123"><a href="#cb16-123" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="at">fillStyle</span> <span class="op">=</span> TEXT<span class="op">;</span></span>
+<span id="cb16-124"><a href="#cb16-124" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="at">font</span> <span class="op">=</span> <span class="st">&quot;14px monospace&quot;</span><span class="op">;</span></span>
+<span id="cb16-125"><a href="#cb16-125" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="fu">fillText</span>(<span class="st">&quot;WASD: move | QE: up/down&quot;</span><span class="op">,</span> <span class="dv">10</span><span class="op">,</span> <span class="dv">18</span>)<span class="op">;</span></span>
+<span id="cb16-126"><a href="#cb16-126" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="fu">fillText</span>(<span class="st">&quot;Mouse: look | Click canvas to lock&quot;</span><span class="op">,</span> <span class="dv">10</span><span class="op">,</span> <span class="dv">36</span>)<span class="op">;</span></span>
+<span id="cb16-127"><a href="#cb16-127" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="fu">fillText</span>(<span class="st">&quot;Click canvas again: change cube axes&quot;</span><span class="op">,</span> <span class="dv">10</span><span class="op">,</span> <span class="dv">54</span>)<span class="op">;</span></span>
+<span id="cb16-128"><a href="#cb16-128" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="fu">fillText</span>(<span class="st">&quot;Cube axes: &quot;</span> <span class="op">+</span> activeAxes<span class="op">.</span><span class="fu">join</span>(<span class="st">&quot;,&quot;</span>)<span class="op">,</span> <span class="dv">10</span><span class="op">,</span> <span class="dv">72</span>)<span class="op">;</span></span>
+<span id="cb16-129"><a href="#cb16-129" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb16-130"><a href="#cb16-130" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb16-131"><a href="#cb16-131" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">line</span>(a<span class="op">,</span>b) {</span>
+<span id="cb16-132"><a href="#cb16-132" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="at">strokeStyle</span> <span class="op">=</span> FOREGROUND<span class="op">;</span></span>
+<span id="cb16-133"><a href="#cb16-133" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="at">lineWidth</span> <span class="op">=</span> <span class="dv">2</span><span class="op">;</span></span>
+<span id="cb16-134"><a href="#cb16-134" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="fu">beginPath</span>()<span class="op">;</span></span>
+<span id="cb16-135"><a href="#cb16-135" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="fu">moveTo</span>(a<span class="op">.</span><span class="at">x</span><span class="op">,</span>a<span class="op">.</span><span class="at">y</span>)<span class="op">;</span></span>
+<span id="cb16-136"><a href="#cb16-136" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="fu">lineTo</span>(b<span class="op">.</span><span class="at">x</span><span class="op">,</span>b<span class="op">.</span><span class="at">y</span>)<span class="op">;</span></span>
+<span id="cb16-137"><a href="#cb16-137" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="fu">stroke</span>()<span class="op">;</span></span>
+<span id="cb16-138"><a href="#cb16-138" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb16-139"><a href="#cb16-139" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb16-140"><a href="#cb16-140" aria-hidden="true" tabindex="-1"></a><span class="co">/* ===================== LOOP ===================== */</span></span>
+<span id="cb16-141"><a href="#cb16-141" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">frame</span>() {</span>
+<span id="cb16-142"><a href="#cb16-142" aria-hidden="true" tabindex="-1"></a>    angle <span class="op">+=</span> <span class="bu">Math</span><span class="op">.</span><span class="cn">PI</span> <span class="op">/</span> FPS<span class="op">;</span></span>
+<span id="cb16-143"><a href="#cb16-143" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb16-144"><a href="#cb16-144" aria-hidden="true" tabindex="-1"></a>    <span class="co">// --- cube rotation quaternion ---</span></span>
+<span id="cb16-145"><a href="#cb16-145" aria-hidden="true" tabindex="-1"></a>    <span class="kw">let</span> q <span class="op">=</span> [<span class="dv">1</span><span class="op">,</span><span class="dv">0</span><span class="op">,</span><span class="dv">0</span><span class="op">,</span><span class="dv">0</span>]<span class="op">;</span></span>
+<span id="cb16-146"><a href="#cb16-146" aria-hidden="true" tabindex="-1"></a>    <span class="cf">for</span> (<span class="kw">const</span> ax <span class="kw">of</span> activeAxes) {</span>
+<span id="cb16-147"><a href="#cb16-147" aria-hidden="true" tabindex="-1"></a>        <span class="cf">if</span> (ax<span class="op">===</span><span class="st">&quot;x&quot;</span>) q <span class="op">=</span> <span class="fu">quatMultiply</span>(<span class="fu">quatFromAxisAngle</span>({<span class="dt">x</span><span class="op">:</span><span class="dv">1</span><span class="op">,</span><span class="dt">y</span><span class="op">:</span><span class="dv">0</span><span class="op">,</span><span class="dt">z</span><span class="op">:</span><span class="dv">0</span>}<span class="op">,</span> angle)<span class="op">,</span> q)<span class="op">;</span></span>
+<span id="cb16-148"><a href="#cb16-148" aria-hidden="true" tabindex="-1"></a>        <span class="cf">if</span> (ax<span class="op">===</span><span class="st">&quot;y&quot;</span>) q <span class="op">=</span> <span class="fu">quatMultiply</span>(<span class="fu">quatFromAxisAngle</span>({<span class="dt">x</span><span class="op">:</span><span class="dv">0</span><span class="op">,</span><span class="dt">y</span><span class="op">:</span><span class="dv">1</span><span class="op">,</span><span class="dt">z</span><span class="op">:</span><span class="dv">0</span>}<span class="op">,</span> angle)<span class="op">,</span> q)<span class="op">;</span></span>
+<span id="cb16-149"><a href="#cb16-149" aria-hidden="true" tabindex="-1"></a>        <span class="cf">if</span> (ax<span class="op">===</span><span class="st">&quot;z&quot;</span>) q <span class="op">=</span> <span class="fu">quatMultiply</span>(<span class="fu">quatFromAxisAngle</span>({<span class="dt">x</span><span class="op">:</span><span class="dv">0</span><span class="op">,</span><span class="dt">y</span><span class="op">:</span><span class="dv">0</span><span class="op">,</span><span class="dt">z</span><span class="op">:</span><span class="dv">1</span>}<span class="op">,</span> angle)<span class="op">,</span> q)<span class="op">;</span></span>
+<span id="cb16-150"><a href="#cb16-150" aria-hidden="true" tabindex="-1"></a>    }</span>
+<span id="cb16-151"><a href="#cb16-151" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb16-152"><a href="#cb16-152" aria-hidden="true" tabindex="-1"></a>    <span class="co">// --- camera movement ---</span></span>
+<span id="cb16-153"><a href="#cb16-153" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> (keys[<span class="st">&quot;w&quot;</span>]) {</span>
+<span id="cb16-154"><a href="#cb16-154" aria-hidden="true" tabindex="-1"></a>        <span class="kw">const</span> f <span class="op">=</span> <span class="fu">cameraForward</span>()<span class="op">;</span></span>
+<span id="cb16-155"><a href="#cb16-155" aria-hidden="true" tabindex="-1"></a>        camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">x</span> <span class="op">+=</span> f<span class="op">.</span><span class="at">x</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">speed</span><span class="op">;</span></span>
+<span id="cb16-156"><a href="#cb16-156" aria-hidden="true" tabindex="-1"></a>        camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">y</span> <span class="op">+=</span> f<span class="op">.</span><span class="at">y</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">speed</span><span class="op">;</span></span>
+<span id="cb16-157"><a href="#cb16-157" aria-hidden="true" tabindex="-1"></a>        camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">z</span> <span class="op">+=</span> f<span class="op">.</span><span class="at">z</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">speed</span><span class="op">;</span></span>
+<span id="cb16-158"><a href="#cb16-158" aria-hidden="true" tabindex="-1"></a>    }</span>
+<span id="cb16-159"><a href="#cb16-159" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> (keys[<span class="st">&quot;s&quot;</span>]) {</span>
+<span id="cb16-160"><a href="#cb16-160" aria-hidden="true" tabindex="-1"></a>        <span class="kw">const</span> f <span class="op">=</span> <span class="fu">cameraForward</span>()<span class="op">;</span></span>
+<span id="cb16-161"><a href="#cb16-161" aria-hidden="true" tabindex="-1"></a>        camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">x</span> <span class="op">-=</span> f<span class="op">.</span><span class="at">x</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">speed</span><span class="op">;</span></span>
+<span id="cb16-162"><a href="#cb16-162" aria-hidden="true" tabindex="-1"></a>        camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">y</span> <span class="op">-=</span> f<span class="op">.</span><span class="at">y</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">speed</span><span class="op">;</span></span>
+<span id="cb16-163"><a href="#cb16-163" aria-hidden="true" tabindex="-1"></a>        camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">z</span> <span class="op">-=</span> f<span class="op">.</span><span class="at">z</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">speed</span><span class="op">;</span></span>
+<span id="cb16-164"><a href="#cb16-164" aria-hidden="true" tabindex="-1"></a>    }</span>
+<span id="cb16-165"><a href="#cb16-165" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> (keys[<span class="st">&quot;a&quot;</span>]) {</span>
+<span id="cb16-166"><a href="#cb16-166" aria-hidden="true" tabindex="-1"></a>        <span class="kw">const</span> r <span class="op">=</span> <span class="fu">cameraRight</span>()<span class="op">;</span></span>
+<span id="cb16-167"><a href="#cb16-167" aria-hidden="true" tabindex="-1"></a>        camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">x</span> <span class="op">-=</span> r<span class="op">.</span><span class="at">x</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">speed</span><span class="op">;</span></span>
+<span id="cb16-168"><a href="#cb16-168" aria-hidden="true" tabindex="-1"></a>        camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">y</span> <span class="op">-=</span> r<span class="op">.</span><span class="at">y</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">speed</span><span class="op">;</span></span>
+<span id="cb16-169"><a href="#cb16-169" aria-hidden="true" tabindex="-1"></a>        camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">z</span> <span class="op">-=</span> r<span class="op">.</span><span class="at">z</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">speed</span><span class="op">;</span></span>
+<span id="cb16-170"><a href="#cb16-170" aria-hidden="true" tabindex="-1"></a>    }</span>
+<span id="cb16-171"><a href="#cb16-171" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> (keys[<span class="st">&quot;d&quot;</span>]) {</span>
+<span id="cb16-172"><a href="#cb16-172" aria-hidden="true" tabindex="-1"></a>        <span class="kw">const</span> r <span class="op">=</span> <span class="fu">cameraRight</span>()<span class="op">;</span></span>
+<span id="cb16-173"><a href="#cb16-173" aria-hidden="true" tabindex="-1"></a>        camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">x</span> <span class="op">+=</span> r<span class="op">.</span><span class="at">x</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">speed</span><span class="op">;</span></span>
+<span id="cb16-174"><a href="#cb16-174" aria-hidden="true" tabindex="-1"></a>        camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">y</span> <span class="op">+=</span> r<span class="op">.</span><span class="at">y</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">speed</span><span class="op">;</span></span>
+<span id="cb16-175"><a href="#cb16-175" aria-hidden="true" tabindex="-1"></a>        camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">z</span> <span class="op">+=</span> r<span class="op">.</span><span class="at">z</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">speed</span><span class="op">;</span></span>
+<span id="cb16-176"><a href="#cb16-176" aria-hidden="true" tabindex="-1"></a>    }</span>
+<span id="cb16-177"><a href="#cb16-177" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> (keys[<span class="st">&quot;q&quot;</span>]) {</span>
+<span id="cb16-178"><a href="#cb16-178" aria-hidden="true" tabindex="-1"></a>        <span class="kw">const</span> u <span class="op">=</span> <span class="fu">cameraUp</span>()<span class="op">;</span></span>
+<span id="cb16-179"><a href="#cb16-179" aria-hidden="true" tabindex="-1"></a>        camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">x</span> <span class="op">-=</span> u<span class="op">.</span><span class="at">x</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">speed</span><span class="op">;</span></span>
+<span id="cb16-180"><a href="#cb16-180" aria-hidden="true" tabindex="-1"></a>        camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">y</span> <span class="op">-=</span> u<span class="op">.</span><span class="at">y</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">speed</span><span class="op">;</span></span>
+<span id="cb16-181"><a href="#cb16-181" aria-hidden="true" tabindex="-1"></a>        camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">z</span> <span class="op">-=</span> u<span class="op">.</span><span class="at">z</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">speed</span><span class="op">;</span></span>
+<span id="cb16-182"><a href="#cb16-182" aria-hidden="true" tabindex="-1"></a>    }</span>
+<span id="cb16-183"><a href="#cb16-183" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> (keys[<span class="st">&quot;e&quot;</span>]) {</span>
+<span id="cb16-184"><a href="#cb16-184" aria-hidden="true" tabindex="-1"></a>        <span class="kw">const</span> u <span class="op">=</span> <span class="fu">cameraUp</span>()<span class="op">;</span></span>
+<span id="cb16-185"><a href="#cb16-185" aria-hidden="true" tabindex="-1"></a>        camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">x</span> <span class="op">+=</span> u<span class="op">.</span><span class="at">x</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">speed</span><span class="op">;</span></span>
+<span id="cb16-186"><a href="#cb16-186" aria-hidden="true" tabindex="-1"></a>        camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">y</span> <span class="op">+=</span> u<span class="op">.</span><span class="at">y</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">speed</span><span class="op">;</span></span>
+<span id="cb16-187"><a href="#cb16-187" aria-hidden="true" tabindex="-1"></a>        camera<span class="op">.</span><span class="at">position</span><span class="op">.</span><span class="at">z</span> <span class="op">+=</span> u<span class="op">.</span><span class="at">z</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">speed</span><span class="op">;</span></span>
+<span id="cb16-188"><a href="#cb16-188" aria-hidden="true" tabindex="-1"></a>    }</span>
+<span id="cb16-189"><a href="#cb16-189" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb16-190"><a href="#cb16-190" aria-hidden="true" tabindex="-1"></a>    <span class="fu">clear</span>()<span class="op">;</span></span>
+<span id="cb16-191"><a href="#cb16-191" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb16-192"><a href="#cb16-192" aria-hidden="true" tabindex="-1"></a>    <span class="cf">for</span> (<span class="kw">const</span> e <span class="kw">of</span> edges) {</span>
+<span id="cb16-193"><a href="#cb16-193" aria-hidden="true" tabindex="-1"></a>        <span class="cf">for</span> (<span class="kw">let</span> i<span class="op">=</span><span class="dv">0</span><span class="op">;</span>i<span class="op">&lt;</span>e<span class="op">.</span><span class="at">length</span><span class="op">;</span>i<span class="op">++</span>) {</span>
+<span id="cb16-194"><a href="#cb16-194" aria-hidden="true" tabindex="-1"></a>            <span class="kw">let</span> a <span class="op">=</span> vertices[e[i]]<span class="op">;</span></span>
+<span id="cb16-195"><a href="#cb16-195" aria-hidden="true" tabindex="-1"></a>            <span class="kw">let</span> b <span class="op">=</span> vertices[e[(i<span class="op">+</span><span class="dv">1</span>)<span class="op">%</span>e<span class="op">.</span><span class="at">length</span>]]<span class="op">;</span></span>
+<span id="cb16-196"><a href="#cb16-196" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb16-197"><a href="#cb16-197" aria-hidden="true" tabindex="-1"></a>            a <span class="op">=</span> <span class="fu">quatRotateVector</span>(q<span class="op">,</span> a)<span class="op">;</span></span>
+<span id="cb16-198"><a href="#cb16-198" aria-hidden="true" tabindex="-1"></a>            b <span class="op">=</span> <span class="fu">quatRotateVector</span>(q<span class="op">,</span> b)<span class="op">;</span></span>
+<span id="cb16-199"><a href="#cb16-199" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb16-200"><a href="#cb16-200" aria-hidden="true" tabindex="-1"></a>            a <span class="op">=</span> { <span class="dt">x</span><span class="op">:</span>a<span class="op">.</span><span class="at">x</span><span class="op">+</span>objectPos<span class="op">.</span><span class="at">x</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span>a<span class="op">.</span><span class="at">y</span><span class="op">+</span>objectPos<span class="op">.</span><span class="at">y</span><span class="op">,</span> <span class="dt">z</span><span class="op">:</span>a<span class="op">.</span><span class="at">z</span><span class="op">+</span>objectPos<span class="op">.</span><span class="at">z</span> }<span class="op">;</span></span>
+<span id="cb16-201"><a href="#cb16-201" aria-hidden="true" tabindex="-1"></a>            b <span class="op">=</span> { <span class="dt">x</span><span class="op">:</span>b<span class="op">.</span><span class="at">x</span><span class="op">+</span>objectPos<span class="op">.</span><span class="at">x</span><span class="op">,</span> <span class="dt">y</span><span class="op">:</span>b<span class="op">.</span><span class="at">y</span><span class="op">+</span>objectPos<span class="op">.</span><span class="at">y</span><span class="op">,</span> <span class="dt">z</span><span class="op">:</span>b<span class="op">.</span><span class="at">z</span><span class="op">+</span>objectPos<span class="op">.</span><span class="at">z</span> }<span class="op">;</span></span>
+<span id="cb16-202"><a href="#cb16-202" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb16-203"><a href="#cb16-203" aria-hidden="true" tabindex="-1"></a>            a <span class="op">=</span> <span class="fu">applyCamera</span>(a)<span class="op">;</span></span>
+<span id="cb16-204"><a href="#cb16-204" aria-hidden="true" tabindex="-1"></a>            b <span class="op">=</span> <span class="fu">applyCamera</span>(b)<span class="op">;</span></span>
+<span id="cb16-205"><a href="#cb16-205" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb16-206"><a href="#cb16-206" aria-hidden="true" tabindex="-1"></a>            <span class="cf">if</span> (a<span class="op">.</span><span class="at">z</span><span class="op">&lt;=</span>NEAR <span class="op">||</span> b<span class="op">.</span><span class="at">z</span><span class="op">&lt;=</span>NEAR) <span class="cf">continue</span><span class="op">;</span></span>
+<span id="cb16-207"><a href="#cb16-207" aria-hidden="true" tabindex="-1"></a>            <span class="fu">line</span>(<span class="bu">screen</span>(<span class="fu">project</span>(a))<span class="op">,</span> <span class="bu">screen</span>(<span class="fu">project</span>(b)))<span class="op">;</span></span>
+<span id="cb16-208"><a href="#cb16-208" aria-hidden="true" tabindex="-1"></a>        }</span>
+<span id="cb16-209"><a href="#cb16-209" aria-hidden="true" tabindex="-1"></a>    }</span>
+<span id="cb16-210"><a href="#cb16-210" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb16-211"><a href="#cb16-211" aria-hidden="true" tabindex="-1"></a>    <span class="pp">setTimeout</span>(frame<span class="op">,</span> <span class="dv">1000</span><span class="op">/</span>FPS)<span class="op">;</span></span>
+<span id="cb16-212"><a href="#cb16-212" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb16-213"><a href="#cb16-213" aria-hidden="true" tabindex="-1"></a><span class="fu">frame</span>()<span class="op">;</span></span>
+<span id="cb16-214"><a href="#cb16-214" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb16-215"><a href="#cb16-215" aria-hidden="true" tabindex="-1"></a><span class="co">/* ===================== INTERACTION ===================== */</span></span>
+<span id="cb16-216"><a href="#cb16-216" aria-hidden="true" tabindex="-1"></a>game<span class="op">.</span><span class="at">onclick</span> <span class="op">=</span> () <span class="kw">=&gt;</span> {</span>
+<span id="cb16-217"><a href="#cb16-217" aria-hidden="true" tabindex="-1"></a>    game<span class="op">.</span><span class="fu">requestPointerLock</span>()<span class="op">;</span></span>
+<span id="cb16-218"><a href="#cb16-218" aria-hidden="true" tabindex="-1"></a>    axisModeIndex <span class="op">=</span> (axisModeIndex <span class="op">+</span> <span class="dv">1</span>) <span class="op">%</span> AXIS_MODES<span class="op">.</span><span class="at">length</span><span class="op">;</span></span>
+<span id="cb16-219"><a href="#cb16-219" aria-hidden="true" tabindex="-1"></a>    activeAxes <span class="op">=</span> AXIS_MODES[axisModeIndex]<span class="op">;</span></span>
+<span id="cb16-220"><a href="#cb16-220" aria-hidden="true" tabindex="-1"></a>}<span class="op">;</span></span>
+<span id="cb16-221"><a href="#cb16-221" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb16-222"><a href="#cb16-222" aria-hidden="true" tabindex="-1"></a><span class="bu">window</span><span class="op">.</span><span class="fu">addEventListener</span>(<span class="st">&quot;mousemove&quot;</span><span class="op">,</span> e <span class="kw">=&gt;</span> {</span>
+<span id="cb16-223"><a href="#cb16-223" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> (<span class="bu">document</span><span class="op">.</span><span class="at">pointerLockElement</span> <span class="op">!==</span> game) <span class="cf">return</span><span class="op">;</span></span>
+<span id="cb16-224"><a href="#cb16-224" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb16-225"><a href="#cb16-225" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> yaw   <span class="op">=</span> <span class="fu">quatFromAxisAngle</span>({<span class="dt">x</span><span class="op">:</span><span class="dv">0</span><span class="op">,</span><span class="dt">y</span><span class="op">:</span><span class="dv">1</span><span class="op">,</span><span class="dt">z</span><span class="op">:</span><span class="dv">0</span>}<span class="op">,</span> <span class="op">-</span>e<span class="op">.</span><span class="at">movementX</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">sensitivity</span>)<span class="op">;</span></span>
+<span id="cb16-226"><a href="#cb16-226" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> pitch <span class="op">=</span> <span class="fu">quatFromAxisAngle</span>({<span class="dt">x</span><span class="op">:</span><span class="dv">1</span><span class="op">,</span><span class="dt">y</span><span class="op">:</span><span class="dv">0</span><span class="op">,</span><span class="dt">z</span><span class="op">:</span><span class="dv">0</span>}<span class="op">,</span> <span class="op">-</span>e<span class="op">.</span><span class="at">movementY</span> <span class="op">*</span> camera<span class="op">.</span><span class="at">sensitivity</span>)<span class="op">;</span></span>
+<span id="cb16-227"><a href="#cb16-227" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb16-228"><a href="#cb16-228" aria-hidden="true" tabindex="-1"></a>    camera<span class="op">.</span><span class="at">rotation</span> <span class="op">=</span> <span class="fu">quatMultiply</span>(yaw<span class="op">,</span> camera<span class="op">.</span><span class="at">rotation</span>)<span class="op">;</span></span>
+<span id="cb16-229"><a href="#cb16-229" aria-hidden="true" tabindex="-1"></a>    camera<span class="op">.</span><span class="at">rotation</span> <span class="op">=</span> <span class="fu">quatMultiply</span>(camera<span class="op">.</span><span class="at">rotation</span><span class="op">,</span> pitch)<span class="op">;</span></span>
+<span id="cb16-230"><a href="#cb16-230" aria-hidden="true" tabindex="-1"></a>})<span class="op">;</span></span>
+<span id="cb16-231"><a href="#cb16-231" aria-hidden="true" tabindex="-1"></a><span class="dt">&lt;/</span><span class="kw">script</span><span class="dt">&gt;</span></span></code></pre></div>
+<hr />
+<h2 id="what-this-version-teaches-important">🧠 What this version
+teaches (important)</h2>
+<ul>
+<li>Cube rotation is <strong>object-local</strong></li>
+<li>Camera rotation is <strong>view-space</strong></li>
+<li>Both use <strong>quaternions</strong>, but for different
+purposes</li>
+<li>Axis toggling affects <strong>only the cube</strong>, never the
+camera</li>
+</ul>
+<p>This separation is <strong>exactly</strong> how real engines
+work.</p>
+<hr />
+<h2 id="strong-next-steps-recommended">🚀 Strong next steps
+(recommended)</h2>
+<p>1️⃣ Clamp camera pitch (avoid flipping) 2️⃣ Add roll for airplane
+controls 3️⃣ Add multiple objects 4️⃣ Add face culling 5️⃣ Add real
+perspective FOV</p>
+<p>If you want any of those, tell me which — you’re doing <em>real 3D
+graphics now</em>.</p>
+</body>
+</html>
diff --git a/docs/chatgpt-dump/camera.md b/docs/chatgpt-dump/camera.md
new file mode 100644 (file)
index 0000000..5be4a45
--- /dev/null
@@ -0,0 +1,987 @@
+Yes 👍
+You’re *very close already*. What you currently have is **object movement**, not a camera.
+We’ll add a **real camera** while changing as little of your code as possible.
+
+I’ll do this in **small, precise steps**, and I’ll clearly mark **what to add** and **what to replace**.
+
+---
+
+# 1️⃣ What “adding a camera” means in your code
+
+Right now your pipeline is:
+
+```
+vertex
+ → rotate cube
+ → translate cube (position)
+ → project
+ → screen
+```
+
+With a camera, it becomes:
+
+```
+vertex
+ → rotate cube
+ → translate cube (object position)
+ → apply camera (inverse transform)
+ → project
+ → screen
+```
+
+So we need:
+
+* a **camera position**
+* a **camera rotation (quaternion)**
+* a function that applies the **inverse camera transform**
+
+---
+
+# 2️⃣ Add a camera object (NEW)
+
+Add this near your `position` variable:
+
+```js
+let camera = {
+    position: { x: 0, y: 0, z: 0 },
+    rotation: [1, 0, 0, 0] // identity quaternion
+};
+```
+
+---
+
+# 3️⃣ Camera helper functions (NEW)
+
+Add these **below your quaternion functions**:
+
+```js
+function quatConjugate(q) {
+    return [q[0], -q[1], -q[2], -q[3]];
+}
+
+function applyCamera(p, cam) {
+    // inverse translation
+    let v = {
+        x: p.x - cam.position.x,
+        y: p.y - cam.position.y,
+        z: p.z - cam.position.z
+    };
+
+    // inverse rotation
+    const invRot = quatConjugate(cam.rotation);
+    return quatRotateVector(invRot, v);
+}
+```
+
+Why this works:
+
+* Camera movement = **world moves opposite**
+* Camera rotation = **world rotates inverse**
+
+---
+
+# 4️⃣ Modify your render loop (IMPORTANT)
+
+### ❌ OLD code (inside `frame()`):
+
+```js
+line(
+    screen(project(translate(quatRotateVector(q, a), position))),
+    screen(project(translate(quatRotateVector(q, b), position)))
+);
+```
+
+### ✅ REPLACE with this:
+
+```js
+let ra = translate(quatRotateVector(q, a), position);
+let rb = translate(quatRotateVector(q, b), position);
+
+ra = applyCamera(ra, camera);
+rb = applyCamera(rb, camera);
+
+line(
+    screen(project(ra)),
+    screen(project(rb))
+);
+```
+
+That’s the **entire camera integration**.
+
+---
+
+# 5️⃣ Move the camera (instead of the cube)
+
+Right now your scroll code moves `position`.
+We’ll change it to move **the camera instead**.
+
+---
+
+## 5.1 Forward / backward (scroll)
+
+### ❌ OLD:
+
+```js
+position.z += .03;
+position.z -= .03;
+```
+
+### ✅ REPLACE with:
+
+```js
+camera.position.z += .03;
+camera.position.z -= .03;
+```
+
+---
+
+## 5.2 Left / right (shift + scroll)
+
+### ❌ OLD:
+
+```js
+position.x += .004;
+position.x -= .004;
+```
+
+### ✅ REPLACE with:
+
+```js
+camera.position.x += .04;
+camera.position.x -= .04;
+```
+
+Also remove this loop (it does nothing useful):
+
+```js
+for (const v of vs) { ... }
+```
+
+You should **never modify vertices for movement**.
+
+---
+
+# 6️⃣ (Optional but recommended) Rotate the camera
+
+Add this helper:
+
+```js
+function rotateCamera(axis, angle) {
+    const q = quatFromAxisAngle(axis, angle);
+    camera.rotation = quatMultiply(q, camera.rotation);
+}
+```
+
+Add keyboard look:
+
+```js
+window.addEventListener("keydown", e => {
+    const rot = 0.03;
+
+    if (e.key === "ArrowLeft")  rotateCamera({x:0,y:1,z:0},  rot);
+    if (e.key === "ArrowRight") rotateCamera({x:0,y:1,z:0}, -rot);
+    if (e.key === "ArrowUp")    rotateCamera({x:1,y:0,z:0},  rot);
+    if (e.key === "ArrowDown")  rotateCamera({x:1,y:0,z:0}, -rot);
+});
+```
+
+Now you have **true camera yaw & pitch**.
+
+---
+
+# 7️⃣ What you have now (important milestone)
+
+You now have:
+
+✔ Object-local rotation
+✔ Object world position
+✔ Camera position
+✔ Camera rotation
+✔ World → view → projection pipeline
+
+This is **literally the same architecture** used by:
+
+* OpenGL
+* WebGL
+* Unity
+* Unreal
+
+You’re writing a **software 3D renderer**.
+
+---
+
+# 8️⃣ One key mental rule (remember this)
+
+> Objects move themselves.
+> Cameras move the world.
+
+If something feels “backwards”, it usually means you forgot the inverse.
+
+---
+
+## 🔥 Next things you can add (recommended order)
+
+1️⃣ FPS-style movement (WASD relative to camera)
+2️⃣ Proper near-plane clipping
+3️⃣ Field-of-view control
+4️⃣ Depth sorting / painter’s algorithm
+5️⃣ Backface culling
+
+If you want, say **which one**, and I’ll guide you through it step by step.
+
+Yes — below is a **clean, complete rewrite of your entire snippet** that:
+
+* Uses a **real camera** (position + quaternion rotation)
+* Keeps your **cube rotation via quaternions**
+* Uses **scroll + shift+scroll** for camera movement
+* Uses **arrow keys to rotate the camera**
+* Uses a **proper 3D pipeline** (object → world → view → projection)
+
+I’ve kept it **very close to your original style**, just corrected architecturally.
+
+---
+
+## ✅ FULL WORKING CAMERA-BASED VERSION
+
+```html
+<canvas id="game"></canvas>
+<script>
+/* ===================== SETUP ===================== */
+const game = document.getElementById("game");
+game.width = 400;
+game.height = 400;
+
+const ctx = game.getContext("2d");
+
+const BACKGROUND = "#101010";
+const FOREGROUND = "#50FF50";
+const TEXT = "#FFFFFF";
+
+const FPS = 60;
+const NEAR = 0.1;
+
+/* ===================== GEOMETRY ===================== */
+const vertices = [
+    {x:  0.25, y:  0.25, z:  0.25},
+    {x: -0.25, y:  0.25, z:  0.25},
+    {x: -0.25, y: -0.25, z:  0.25},
+    {x:  0.25, y: -0.25, z:  0.25},
+    {x:  0.25, y:  0.25, z: -0.25},
+    {x: -0.25, y:  0.25, z: -0.25},
+    {x: -0.25, y: -0.25, z: -0.25},
+    {x:  0.25, y: -0.25, z: -0.25},
+];
+
+const edges = [
+    [0,1,2,3],
+    [4,5,6,7],
+    [0,4],
+    [1,5],
+    [2,6],
+    [3,7],
+];
+
+/* ===================== OBJECT ===================== */
+let objectPosition = { x: 0, y: 0, z: 2 };
+let angle = 0;
+let activeAxes = ["x"];
+
+/* ===================== CAMERA ===================== */
+let camera = {
+    position: { x: 0, y: 0, z: 0 },
+    rotation: [1,0,0,0] // quaternion
+};
+
+/* ===================== QUATERNIONS ===================== */
+function quatFromAxisAngle(axis, angle) {
+    const h = angle / 2;
+    const s = Math.sin(h);
+    return [Math.cos(h), axis.x*s, axis.y*s, axis.z*s];
+}
+
+function quatMultiply(a, b) {
+    const [w1,x1,y1,z1] = a;
+    const [w2,x2,y2,z2] = b;
+    return [
+        w1*w2 - x1*x2 - y1*y2 - z1*z2,
+        w1*x2 + x1*w2 + y1*z2 - z1*y2,
+        w1*y2 - x1*z2 + y1*w2 + z1*x2,
+        w1*z2 + x1*y2 - y1*x2 + z1*w2
+    ];
+}
+
+function quatConjugate(q) {
+    return [q[0], -q[1], -q[2], -q[3]];
+}
+
+function quatRotateVector(q, v) {
+    const qv = [0, v.x, v.y, v.z];
+    const inv = quatConjugate(q);
+    const t = quatMultiply(q, qv);
+    const r = quatMultiply(t, inv);
+    return { x: r[1], y: r[2], z: r[3] };
+}
+
+/* ===================== TRANSFORMS ===================== */
+function translate(p, t) {
+    return { x: p.x + t.x, y: p.y + t.y, z: p.z + t.z };
+}
+
+function applyCamera(p) {
+    let v = {
+        x: p.x - camera.position.x,
+        y: p.y - camera.position.y,
+        z: p.z - camera.position.z
+    };
+    return quatRotateVector(quatConjugate(camera.rotation), v);
+}
+
+/* ===================== PROJECTION ===================== */
+function project(p) {
+    return { x: p.x / p.z, y: p.y / p.z };
+}
+
+function screen(p) {
+    return {
+        x: (p.x + 1) * 0.5 * game.width,
+        y: (1 - (p.y + 1) * 0.5) * game.height
+    };
+}
+
+/* ===================== DRAW ===================== */
+function clear() {
+    ctx.fillStyle = BACKGROUND;
+    ctx.fillRect(0, 0, game.width, game.height);
+
+    ctx.fillStyle = TEXT;
+    ctx.font = "14px monospace";
+    ctx.fillText("Click: change cube rotation axes", 10, 18);
+    ctx.fillText("Scroll: move camera forward/back", 10, 36);
+    ctx.fillText("Shift+Scroll: strafe camera", 10, 54);
+    ctx.fillText("Arrow keys: look around", 10, 72);
+    ctx.fillText("Axes: " + activeAxes.join(","), 10, 90);
+}
+
+function line(a, b) {
+    ctx.strokeStyle = FOREGROUND;
+    ctx.lineWidth = 2;
+    ctx.beginPath();
+    ctx.moveTo(a.x, a.y);
+    ctx.lineTo(b.x, b.y);
+    ctx.stroke();
+}
+
+/* ===================== LOOP ===================== */
+function frame() {
+    angle += Math.PI / FPS;
+
+    let q = [1,0,0,0];
+    for (const ax of activeAxes) {
+        if (ax === "x") q = quatMultiply(quatFromAxisAngle({x:1,y:0,z:0}, angle), q);
+        if (ax === "y") q = quatMultiply(quatFromAxisAngle({x:0,y:1,z:0}, angle), q);
+        if (ax === "z") q = quatMultiply(quatFromAxisAngle({x:0,y:0,z:1}, angle), q);
+    }
+
+    clear();
+
+    for (const e of edges) {
+        for (let i = 0; i < e.length; i++) {
+            let a = vertices[e[i]];
+            let b = vertices[e[(i+1)%e.length]];
+
+            a = translate(quatRotateVector(q, a), objectPosition);
+            b = translate(quatRotateVector(q, b), objectPosition);
+
+            a = applyCamera(a);
+            b = applyCamera(b);
+
+            if (a.z <= NEAR || b.z <= NEAR) continue;
+
+            line(screen(project(a)), screen(project(b)));
+        }
+    }
+
+    setTimeout(frame, 1000/FPS);
+}
+
+frame();
+
+/* ===================== INPUT ===================== */
+game.onclick = () => {
+    const modes = [["x"],["y"],["z"],["x","y"],["y","z"],["x","z"],["x","y","z"]];
+    activeAxes = modes[(modes.findIndex(m => m+"" === activeAxes+"") + 1) % modes.length];
+};
+
+game.addEventListener("wheel", e => {
+    e.preventDefault();
+    if (e.shiftKey) camera.position.x += e.deltaY * 0.002;
+    else camera.position.z += e.deltaY * 0.01;
+}, { passive:false });
+
+window.addEventListener("keydown", e => {
+    const r = 0.04;
+    if (e.key === "ArrowLeft")  camera.rotation = quatMultiply(quatFromAxisAngle({x:0,y:1,z:0},  r), camera.rotation);
+    if (e.key === "ArrowRight") camera.rotation = quatMultiply(quatFromAxisAngle({x:0,y:1,z:0}, -r), camera.rotation);
+    if (e.key === "ArrowUp")    camera.rotation = quatMultiply(quatFromAxisAngle({x:1,y:0,z:0},  r), camera.rotation);
+    if (e.key === "ArrowDown")  camera.rotation = quatMultiply(quatFromAxisAngle({x:1,y:0,z:0}, -r), camera.rotation);
+});
+</script>
+```
+
+---
+
+## 🎯 What this version gives you
+
+✔ True **camera system**
+✔ Object rotation separate from camera
+✔ Proper **world → view → projection** pipeline
+✔ No vertex mutation
+✔ No fake camera tricks
+✔ Clean mental model identical to real engines
+
+---
+
+### 🚀 You are now at the level where:
+
+* WebGL will suddenly make sense
+* Matrices will feel intuitive
+* Game engines won’t feel “magical” anymore
+
+If you want next, I strongly recommend:
+👉 **FPS-style movement (WASD relative to camera direction)**
+👉 **Mouse-look**
+👉 **Perspective FOV math**
+
+Just say which one 👍
+
+Absolutely — this is the **correct next step** 👍
+“Fly mode” means:
+
+* **W / S** → move forward / backward **relative to where the camera is facing**
+* **A / D** → strafe left / right
+* **Q / E** → move down / up
+* **Mouse** → look around (yaw + pitch)
+* The cube stays in the world; **the camera flies**
+
+Below is a **full rewrite** of the previous camera-based code, upgraded to a **true fly camera**.
+Nothing is faked, and the math matches real engines.
+
+---
+
+## ✅ FULL FLY-CAMERA VERSION (WASD + Mouse Look)
+
+```html
+<canvas id="game"></canvas>
+<script>
+/* ===================== SETUP ===================== */
+const game = document.getElementById("game");
+game.width = 400;
+game.height = 400;
+const ctx = game.getContext("2d");
+
+const BACKGROUND = "#101010";
+const FOREGROUND = "#50FF50";
+const TEXT = "#FFFFFF";
+
+const FPS = 60;
+const NEAR = 0.1;
+
+/* ===================== GEOMETRY ===================== */
+const vertices = [
+    {x:  0.25, y:  0.25, z:  0.25},
+    {x: -0.25, y:  0.25, z:  0.25},
+    {x: -0.25, y: -0.25, z:  0.25},
+    {x:  0.25, y: -0.25, z:  0.25},
+    {x:  0.25, y:  0.25, z: -0.25},
+    {x: -0.25, y:  0.25, z: -0.25},
+    {x: -0.25, y: -0.25, z: -0.25},
+    {x:  0.25, y: -0.25, z: -0.25},
+];
+
+const edges = [
+    [0,1,2,3],
+    [4,5,6,7],
+    [0,4],[1,5],[2,6],[3,7]
+];
+
+/* ===================== OBJECT ===================== */
+let objectPos = { x: 0, y: 0, z: 2 };
+let angle = 0;
+let activeAxes = ["x","y"];
+
+/* ===================== CAMERA ===================== */
+let camera = {
+    position: { x: 0, y: 0, z: 0 },
+    rotation: [1,0,0,0], // quaternion
+    speed: 0.05,
+    sensitivity: 0.002
+};
+
+/* ===================== INPUT ===================== */
+const keys = {};
+window.addEventListener("keydown", e => keys[e.key.toLowerCase()] = true);
+window.addEventListener("keyup",   e => keys[e.key.toLowerCase()] = false);
+
+/* ===================== QUATERNIONS ===================== */
+function quatFromAxisAngle(axis, angle) {
+    const h = angle / 2;
+    const s = Math.sin(h);
+    return [Math.cos(h), axis.x*s, axis.y*s, axis.z*s];
+}
+
+function quatMultiply(a, b) {
+    const [w1,x1,y1,z1] = a;
+    const [w2,x2,y2,z2] = b;
+    return [
+        w1*w2 - x1*x2 - y1*y2 - z1*z2,
+        w1*x2 + x1*w2 + y1*z2 - z1*y2,
+        w1*y2 - x1*z2 + y1*w2 + z1*x2,
+        w1*z2 + x1*y2 - y1*x2 + z1*w2
+    ];
+}
+
+function quatConjugate(q) {
+    return [q[0], -q[1], -q[2], -q[3]];
+}
+
+function quatRotateVector(q, v) {
+    const qv = [0, v.x, v.y, v.z];
+    const r = quatMultiply(quatMultiply(q, qv), quatConjugate(q));
+    return { x: r[1], y: r[2], z: r[3] };
+}
+
+/* ===================== CAMERA MATH ===================== */
+function cameraForward() {
+    return quatRotateVector(camera.rotation, {x:0,y:0,z:1});
+}
+function cameraRight() {
+    return quatRotateVector(camera.rotation, {x:1,y:0,z:0});
+}
+function cameraUp() {
+    return quatRotateVector(camera.rotation, {x:0,y:1,z:0});
+}
+
+function applyCamera(p) {
+    let v = {
+        x: p.x - camera.position.x,
+        y: p.y - camera.position.y,
+        z: p.z - camera.position.z
+    };
+    return quatRotateVector(quatConjugate(camera.rotation), v);
+}
+
+/* ===================== PROJECTION ===================== */
+function project(p) {
+    return { x: p.x / p.z, y: p.y / p.z };
+}
+function screen(p) {
+    return {
+        x: (p.x + 1) * 0.5 * game.width,
+        y: (1 - (p.y + 1) * 0.5) * game.height
+    };
+}
+
+/* ===================== DRAW ===================== */
+function clear() {
+    ctx.fillStyle = BACKGROUND;
+    ctx.fillRect(0,0,game.width,game.height);
+    ctx.fillStyle = TEXT;
+    ctx.font = "14px monospace";
+    ctx.fillText("WASD: move | QE: up/down", 10, 18);
+    ctx.fillText("Mouse: look | Click canvas to lock", 10, 36);
+}
+
+function line(a,b) {
+    ctx.strokeStyle = FOREGROUND;
+    ctx.lineWidth = 2;
+    ctx.beginPath();
+    ctx.moveTo(a.x,a.y);
+    ctx.lineTo(b.x,b.y);
+    ctx.stroke();
+}
+
+/* ===================== LOOP ===================== */
+function frame() {
+    angle += Math.PI / FPS;
+
+    // cube rotation
+    let q = [1,0,0,0];
+    for (const ax of activeAxes) {
+        if (ax==="x") q = quatMultiply(quatFromAxisAngle({x:1,y:0,z:0}, angle), q);
+        if (ax==="y") q = quatMultiply(quatFromAxisAngle({x:0,y:1,z:0}, angle), q);
+        if (ax==="z") q = quatMultiply(quatFromAxisAngle({x:0,y:0,z:1}, angle), q);
+    }
+
+    // camera movement
+    if (keys["w"]) {
+        const f = cameraForward();
+        camera.position.x += f.x * camera.speed;
+        camera.position.y += f.y * camera.speed;
+        camera.position.z += f.z * camera.speed;
+    }
+    if (keys["s"]) {
+        const f = cameraForward();
+        camera.position.x -= f.x * camera.speed;
+        camera.position.y -= f.y * camera.speed;
+        camera.position.z -= f.z * camera.speed;
+    }
+    if (keys["a"]) {
+        const r = cameraRight();
+        camera.position.x -= r.x * camera.speed;
+        camera.position.y -= r.y * camera.speed;
+        camera.position.z -= r.z * camera.speed;
+    }
+    if (keys["d"]) {
+        const r = cameraRight();
+        camera.position.x += r.x * camera.speed;
+        camera.position.y += r.y * camera.speed;
+        camera.position.z += r.z * camera.speed;
+    }
+    if (keys["q"]) {
+        const u = cameraUp();
+        camera.position.x -= u.x * camera.speed;
+        camera.position.y -= u.y * camera.speed;
+        camera.position.z -= u.z * camera.speed;
+    }
+    if (keys["e"]) {
+        const u = cameraUp();
+        camera.position.x += u.x * camera.speed;
+        camera.position.y += u.y * camera.speed;
+        camera.position.z += u.z * camera.speed;
+    }
+
+    clear();
+
+    for (const e of edges) {
+        for (let i=0;i<e.length;i++) {
+            let a = vertices[e[i]];
+            let b = vertices[e[(i+1)%e.length]];
+
+            a = quatRotateVector(q, a);
+            b = quatRotateVector(q, b);
+
+            a = { x:a.x+objectPos.x, y:a.y+objectPos.y, z:a.z+objectPos.z };
+            b = { x:b.x+objectPos.x, y:b.y+objectPos.y, z:b.z+objectPos.z };
+
+            a = applyCamera(a);
+            b = applyCamera(b);
+
+            if (a.z<=NEAR || b.z<=NEAR) continue;
+            line(screen(project(a)), screen(project(b)));
+        }
+    }
+
+    setTimeout(frame, 1000/FPS);
+}
+frame();
+
+/* ===================== MOUSE LOOK ===================== */
+game.onclick = () => game.requestPointerLock();
+
+window.addEventListener("mousemove", e => {
+    if (document.pointerLockElement !== game) return;
+
+    const yaw   = quatFromAxisAngle({x:0,y:1,z:0}, -e.movementX * camera.sensitivity);
+    const pitch = quatFromAxisAngle({x:1,y:0,z:0}, -e.movementY * camera.sensitivity);
+
+    camera.rotation = quatMultiply(yaw, camera.rotation);
+    camera.rotation = quatMultiply(camera.rotation, pitch);
+});
+</script>
+```
+
+---
+
+## 🎯 What you now have
+
+✔ **True fly camera**
+✔ Movement relative to view direction
+✔ Mouse-look yaw + pitch
+✔ Quaternion-based, no gimbal lock
+✔ Identical behavior to Unity / Unreal fly mode
+
+This is *real engine-level camera control*.
+
+---
+
+### 🔥 Next upgrades (strongly recommended)
+
+1️⃣ Clamp pitch to avoid flipping
+2️⃣ Add roll (airplane-style)
+3️⃣ Add acceleration / inertia
+4️⃣ Add multiple objects + scene graph
+5️⃣ Replace projection with real FOV math
+
+If you want any of those, say which one — you’re doing excellent work.
+
+Yes — below is a **full rewrite of the fly-camera version** that **keeps the ability to change which axes the cube rotates on**, exactly like your earlier versions.
+
+Nothing is removed:
+
+* ✅ Fly camera (WASD + QE + mouse look)
+* ✅ Quaternion camera
+* ✅ Quaternion cube rotation
+* ✅ **Click to cycle cube rotation axes**
+* ✅ Clean, correct 3D pipeline
+
+---
+
+## ✅ FULL FLY-CAMERA + ROTATION-AXIS TOGGLE VERSION
+
+```html
+<canvas id="game"></canvas>
+<script>
+/* ===================== SETUP ===================== */
+const game = document.getElementById("game");
+game.width = 400;
+game.height = 400;
+const ctx = game.getContext("2d");
+
+const BACKGROUND = "#101010";
+const FOREGROUND = "#50FF50";
+const TEXT = "#FFFFFF";
+
+const FPS = 60;
+const NEAR = 0.1;
+
+/* ===================== GEOMETRY ===================== */
+const vertices = [
+    {x:  0.25, y:  0.25, z:  0.25},
+    {x: -0.25, y:  0.25, z:  0.25},
+    {x: -0.25, y: -0.25, z:  0.25},
+    {x:  0.25, y: -0.25, z:  0.25},
+    {x:  0.25, y:  0.25, z: -0.25},
+    {x: -0.25, y:  0.25, z: -0.25},
+    {x: -0.25, y: -0.25, z: -0.25},
+    {x:  0.25, y: -0.25, z: -0.25},
+];
+
+const edges = [
+    [0,1,2,3],
+    [4,5,6,7],
+    [0,4],[1,5],[2,6],[3,7]
+];
+
+/* ===================== OBJECT ===================== */
+let objectPos = { x: 0, y: 0, z: 2 };
+let angle = 0;
+
+const AXIS_MODES = [
+    ["x"], ["y"], ["z"],
+    ["x","y"], ["y","z"], ["x","z"],
+    ["x","y","z"]
+];
+let axisModeIndex = 0;
+let activeAxes = AXIS_MODES[axisModeIndex];
+
+/* ===================== CAMERA ===================== */
+let camera = {
+    position: { x: 0, y: 0, z: 0 },
+    rotation: [1,0,0,0],
+    speed: 0.05,
+    sensitivity: 0.002
+};
+
+/* ===================== INPUT ===================== */
+const keys = {};
+window.addEventListener("keydown", e => keys[e.key.toLowerCase()] = true);
+window.addEventListener("keyup",   e => keys[e.key.toLowerCase()] = false);
+
+/* ===================== QUATERNIONS ===================== */
+function quatFromAxisAngle(axis, angle) {
+    const h = angle / 2;
+    const s = Math.sin(h);
+    return [Math.cos(h), axis.x*s, axis.y*s, axis.z*s];
+}
+
+function quatMultiply(a, b) {
+    const [w1,x1,y1,z1] = a;
+    const [w2,x2,y2,z2] = b;
+    return [
+        w1*w2 - x1*x2 - y1*y2 - z1*z2,
+        w1*x2 + x1*w2 + y1*z2 - z1*y2,
+        w1*y2 - x1*z2 + y1*w2 + z1*x2,
+        w1*z2 + x1*y2 - y1*x2 + z1*w2
+    ];
+}
+
+function quatConjugate(q) {
+    return [q[0], -q[1], -q[2], -q[3]];
+}
+
+function quatRotateVector(q, v) {
+    const qv = [0, v.x, v.y, v.z];
+    const r = quatMultiply(quatMultiply(q, qv), quatConjugate(q));
+    return { x: r[1], y: r[2], z: r[3] };
+}
+
+/* ===================== CAMERA VECTORS ===================== */
+function cameraForward() {
+    return quatRotateVector(camera.rotation, {x:0,y:0,z:1});
+}
+function cameraRight() {
+    return quatRotateVector(camera.rotation, {x:1,y:0,z:0});
+}
+function cameraUp() {
+    return quatRotateVector(camera.rotation, {x:0,y:1,z:0});
+}
+
+function applyCamera(p) {
+    const v = {
+        x: p.x - camera.position.x,
+        y: p.y - camera.position.y,
+        z: p.z - camera.position.z
+    };
+    return quatRotateVector(quatConjugate(camera.rotation), v);
+}
+
+/* ===================== PROJECTION ===================== */
+function project(p) {
+    return { x: p.x / p.z, y: p.y / p.z };
+}
+function screen(p) {
+    return {
+        x: (p.x + 1) * 0.5 * game.width,
+        y: (1 - (p.y + 1) * 0.5) * game.height
+    };
+}
+
+/* ===================== DRAW ===================== */
+function clear() {
+    ctx.fillStyle = BACKGROUND;
+    ctx.fillRect(0,0,game.width,game.height);
+
+    ctx.fillStyle = TEXT;
+    ctx.font = "14px monospace";
+    ctx.fillText("WASD: move | QE: up/down", 10, 18);
+    ctx.fillText("Mouse: look | Click canvas to lock", 10, 36);
+    ctx.fillText("Click canvas again: change cube axes", 10, 54);
+    ctx.fillText("Cube axes: " + activeAxes.join(","), 10, 72);
+}
+
+function line(a,b) {
+    ctx.strokeStyle = FOREGROUND;
+    ctx.lineWidth = 2;
+    ctx.beginPath();
+    ctx.moveTo(a.x,a.y);
+    ctx.lineTo(b.x,b.y);
+    ctx.stroke();
+}
+
+/* ===================== LOOP ===================== */
+function frame() {
+    angle += Math.PI / FPS;
+
+    // --- cube rotation quaternion ---
+    let q = [1,0,0,0];
+    for (const ax of activeAxes) {
+        if (ax==="x") q = quatMultiply(quatFromAxisAngle({x:1,y:0,z:0}, angle), q);
+        if (ax==="y") q = quatMultiply(quatFromAxisAngle({x:0,y:1,z:0}, angle), q);
+        if (ax==="z") q = quatMultiply(quatFromAxisAngle({x:0,y:0,z:1}, angle), q);
+    }
+
+    // --- camera movement ---
+    if (keys["w"]) {
+        const f = cameraForward();
+        camera.position.x += f.x * camera.speed;
+        camera.position.y += f.y * camera.speed;
+        camera.position.z += f.z * camera.speed;
+    }
+    if (keys["s"]) {
+        const f = cameraForward();
+        camera.position.x -= f.x * camera.speed;
+        camera.position.y -= f.y * camera.speed;
+        camera.position.z -= f.z * camera.speed;
+    }
+    if (keys["a"]) {
+        const r = cameraRight();
+        camera.position.x -= r.x * camera.speed;
+        camera.position.y -= r.y * camera.speed;
+        camera.position.z -= r.z * camera.speed;
+    }
+    if (keys["d"]) {
+        const r = cameraRight();
+        camera.position.x += r.x * camera.speed;
+        camera.position.y += r.y * camera.speed;
+        camera.position.z += r.z * camera.speed;
+    }
+    if (keys["q"]) {
+        const u = cameraUp();
+        camera.position.x -= u.x * camera.speed;
+        camera.position.y -= u.y * camera.speed;
+        camera.position.z -= u.z * camera.speed;
+    }
+    if (keys["e"]) {
+        const u = cameraUp();
+        camera.position.x += u.x * camera.speed;
+        camera.position.y += u.y * camera.speed;
+        camera.position.z += u.z * camera.speed;
+    }
+
+    clear();
+
+    for (const e of edges) {
+        for (let i=0;i<e.length;i++) {
+            let a = vertices[e[i]];
+            let b = vertices[e[(i+1)%e.length]];
+
+            a = quatRotateVector(q, a);
+            b = quatRotateVector(q, b);
+
+            a = { x:a.x+objectPos.x, y:a.y+objectPos.y, z:a.z+objectPos.z };
+            b = { x:b.x+objectPos.x, y:b.y+objectPos.y, z:b.z+objectPos.z };
+
+            a = applyCamera(a);
+            b = applyCamera(b);
+
+            if (a.z<=NEAR || b.z<=NEAR) continue;
+            line(screen(project(a)), screen(project(b)));
+        }
+    }
+
+    setTimeout(frame, 1000/FPS);
+}
+frame();
+
+/* ===================== INTERACTION ===================== */
+game.onclick = () => {
+    game.requestPointerLock();
+    axisModeIndex = (axisModeIndex + 1) % AXIS_MODES.length;
+    activeAxes = AXIS_MODES[axisModeIndex];
+};
+
+window.addEventListener("mousemove", e => {
+    if (document.pointerLockElement !== game) return;
+
+    const yaw   = quatFromAxisAngle({x:0,y:1,z:0}, -e.movementX * camera.sensitivity);
+    const pitch = quatFromAxisAngle({x:1,y:0,z:0}, -e.movementY * camera.sensitivity);
+
+    camera.rotation = quatMultiply(yaw, camera.rotation);
+    camera.rotation = quatMultiply(camera.rotation, pitch);
+});
+</script>
+```
+
+---
+
+## 🧠 What this version teaches (important)
+
+* Cube rotation is **object-local**
+* Camera rotation is **view-space**
+* Both use **quaternions**, but for different purposes
+* Axis toggling affects **only the cube**, never the camera
+
+This separation is **exactly** how real engines work.
+
+---
+
+## 🚀 Strong next steps (recommended)
+
+1️⃣ Clamp camera pitch (avoid flipping)
+2️⃣ Add roll for airplane controls
+3️⃣ Add multiple objects
+4️⃣ Add face culling
+5️⃣ Add real perspective FOV
+
+If you want any of those, tell me which — you’re doing *real 3D graphics now*.
+
diff --git a/docs/chatgpt-dump/euler-angle.html b/docs/chatgpt-dump/euler-angle.html
new file mode 100644 (file)
index 0000000..5f3ed29
--- /dev/null
@@ -0,0 +1,460 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" lang="" xml:lang="">
+<head>
+  <meta charset="utf-8" />
+  <meta name="generator" content="pandoc" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
+  <title>euler-angle</title>
+  <style>
+    html {
+      color: #1a1a1a;
+      background-color: #fdfdfd;
+    }
+    body {
+      margin: 0 auto;
+      max-width: 36em;
+      padding-left: 50px;
+      padding-right: 50px;
+      padding-top: 50px;
+      padding-bottom: 50px;
+      hyphens: auto;
+      overflow-wrap: break-word;
+      text-rendering: optimizeLegibility;
+      font-kerning: normal;
+    }
+    @media (max-width: 600px) {
+      body {
+        font-size: 0.9em;
+        padding: 12px;
+      }
+      h1 {
+        font-size: 1.8em;
+      }
+    }
+    @media print {
+      html {
+        background-color: white;
+      }
+      body {
+        background-color: transparent;
+        color: black;
+        font-size: 12pt;
+      }
+      p, h2, h3 {
+        orphans: 3;
+        widows: 3;
+      }
+      h2, h3, h4 {
+        page-break-after: avoid;
+      }
+    }
+    p {
+      margin: 1em 0;
+    }
+    a {
+      color: #1a1a1a;
+    }
+    a:visited {
+      color: #1a1a1a;
+    }
+    img {
+      max-width: 100%;
+    }
+    h1, h2, h3, h4, h5, h6 {
+      margin-top: 1.4em;
+    }
+    h5, h6 {
+      font-size: 1em;
+      font-style: italic;
+    }
+    h6 {
+      font-weight: normal;
+    }
+    ol, ul {
+      padding-left: 1.7em;
+      margin-top: 1em;
+    }
+    li > ol, li > ul {
+      margin-top: 0;
+    }
+    blockquote {
+      margin: 1em 0 1em 1.7em;
+      padding-left: 1em;
+      border-left: 2px solid #e6e6e6;
+      color: #606060;
+    }
+    code {
+      font-family: Menlo, Monaco, Consolas, 'Lucida Console', monospace;
+      font-size: 85%;
+      margin: 0;
+      hyphens: manual;
+    }
+    pre {
+      margin: 1em 0;
+      overflow: auto;
+    }
+    pre code {
+      padding: 0;
+      overflow: visible;
+      overflow-wrap: normal;
+    }
+    .sourceCode {
+     background-color: transparent;
+     overflow: visible;
+    }
+    hr {
+      background-color: #1a1a1a;
+      border: none;
+      height: 1px;
+      margin: 1em 0;
+    }
+    table {
+      margin: 1em 0;
+      border-collapse: collapse;
+      width: 100%;
+      overflow-x: auto;
+      display: block;
+      font-variant-numeric: lining-nums tabular-nums;
+    }
+    table caption {
+      margin-bottom: 0.75em;
+    }
+    tbody {
+      margin-top: 0.5em;
+      border-top: 1px solid #1a1a1a;
+      border-bottom: 1px solid #1a1a1a;
+    }
+    th {
+      border-top: 1px solid #1a1a1a;
+      padding: 0.25em 0.5em 0.25em 0.5em;
+    }
+    td {
+      padding: 0.125em 0.5em 0.25em 0.5em;
+    }
+    header {
+      margin-bottom: 4em;
+      text-align: center;
+    }
+    #TOC li {
+      list-style: none;
+    }
+    #TOC ul {
+      padding-left: 1.3em;
+    }
+    #TOC > ul {
+      padding-left: 0;
+    }
+    #TOC a:not(:hover) {
+      text-decoration: none;
+    }
+    code{white-space: pre-wrap;}
+    span.smallcaps{font-variant: small-caps;}
+    div.columns{display: flex; gap: min(4vw, 1.5em);}
+    div.column{flex: auto; overflow-x: auto;}
+    div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;}
+    /* The extra [class] is a hack that increases specificity enough to
+       override a similar rule in reveal.js */
+    ul.task-list[class]{list-style: none;}
+    ul.task-list li input[type="checkbox"] {
+      font-size: inherit;
+      width: 0.8em;
+      margin: 0 0.8em 0.2em -1.6em;
+      vertical-align: middle;
+    }
+    /* CSS for syntax highlighting */
+    pre > code.sourceCode { white-space: pre; position: relative; }
+    pre > code.sourceCode > span { line-height: 1.25; }
+    pre > code.sourceCode > span:empty { height: 1.2em; }
+    .sourceCode { overflow: visible; }
+    code.sourceCode > span { color: inherit; text-decoration: inherit; }
+    div.sourceCode { margin: 1em 0; }
+    pre.sourceCode { margin: 0; }
+    @media screen {
+    div.sourceCode { overflow: auto; }
+    }
+    @media print {
+    pre > code.sourceCode { white-space: pre-wrap; }
+    pre > code.sourceCode > span { text-indent: -5em; padding-left: 5em; }
+    }
+    pre.numberSource code
+      { counter-reset: source-line 0; }
+    pre.numberSource code > span
+      { position: relative; left: -4em; counter-increment: source-line; }
+    pre.numberSource code > span > a:first-child::before
+      { content: counter(source-line);
+        position: relative; left: -1em; text-align: right; vertical-align: baseline;
+        border: none; display: inline-block;
+        -webkit-touch-callout: none; -webkit-user-select: none;
+        -khtml-user-select: none; -moz-user-select: none;
+        -ms-user-select: none; user-select: none;
+        padding: 0 4px; width: 4em;
+        color: #aaaaaa;
+      }
+    pre.numberSource { margin-left: 3em; border-left: 1px solid #aaaaaa;  padding-left: 4px; }
+    div.sourceCode
+      {   }
+    @media screen {
+    pre > code.sourceCode > span > a:first-child::before { text-decoration: underline; }
+    }
+    code span.al { color: #ff0000; font-weight: bold; } /* Alert */
+    code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */
+    code span.at { color: #7d9029; } /* Attribute */
+    code span.bn { color: #40a070; } /* BaseN */
+    code span.bu { color: #008000; } /* BuiltIn */
+    code span.cf { color: #007020; font-weight: bold; } /* ControlFlow */
+    code span.ch { color: #4070a0; } /* Char */
+    code span.cn { color: #880000; } /* Constant */
+    code span.co { color: #60a0b0; font-style: italic; } /* Comment */
+    code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */
+    code span.do { color: #ba2121; font-style: italic; } /* Documentation */
+    code span.dt { color: #902000; } /* DataType */
+    code span.dv { color: #40a070; } /* DecVal */
+    code span.er { color: #ff0000; font-weight: bold; } /* Error */
+    code span.ex { } /* Extension */
+    code span.fl { color: #40a070; } /* Float */
+    code span.fu { color: #06287e; } /* Function */
+    code span.im { color: #008000; font-weight: bold; } /* Import */
+    code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */
+    code span.kw { color: #007020; font-weight: bold; } /* Keyword */
+    code span.op { color: #666666; } /* Operator */
+    code span.ot { color: #007020; } /* Other */
+    code span.pp { color: #bc7a00; } /* Preprocessor */
+    code span.sc { color: #4070a0; } /* SpecialChar */
+    code span.ss { color: #bb6688; } /* SpecialString */
+    code span.st { color: #4070a0; } /* String */
+    code span.va { color: #19177c; } /* Variable */
+    code span.vs { color: #4070a0; } /* VerbatimString */
+    code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */
+  </style>
+  <script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
+  <script src="/usr/share/javascript/mathjax/MathJax.js"
+  type="text/javascript"></script>
+</head>
+<body>
+<p>Absolutely. I’ll give you the <strong>actual math formulas</strong>,
+show the <strong>rotation matrices</strong>, explain the <strong>order
+issue</strong>, and relate them directly to what you’ve been doing with
+quaternions.</p>
+<hr />
+<h2 id="what-are-euler-angles">1. What are Euler angles?</h2>
+<p><strong>Euler angles</strong> describe a 3D rotation as <strong>three
+sequential rotations</strong> about coordinate axes.</p>
+<p>Typically:</p>
+<ul>
+<li><strong>Roll</strong> → rotation about X</li>
+<li><strong>Pitch</strong> → rotation about Y</li>
+<li><strong>Yaw</strong> → rotation about Z</li>
+</ul>
+<p>We’ll denote:</p>
+<ul>
+<li>( ) = rotation about X (roll)</li>
+<li>( ) = rotation about Y (pitch)</li>
+<li>( ) = rotation about Z (yaw)</li>
+</ul>
+<p>⚠️ <strong>Order matters</strong>. <code>XYZ</code> is <em>not</em>
+the same as <code>ZYX</code>.</p>
+<hr />
+<h2 id="single-axis-rotation-matrices">2. Single-axis rotation
+matrices</h2>
+<h3 id="rotation-about-x-axis-roll">Rotation about X-axis (roll)</h3>
+<p>[ &amp; &amp; \end{bmatrix} }]</p>
+<hr />
+<h3 id="rotation-about-y-axis-pitch">Rotation about Y-axis (pitch)</h3>
+<p>[ &amp; 0 &amp; \end{bmatrix} }]</p>
+<hr />
+<h3 id="rotation-about-z-axis-yaw">Rotation about Z-axis (yaw)</h3>
+<p>[ &amp; 0 &amp; 1 \end{bmatrix} }]</p>
+<hr />
+<h2 id="combined-euler-rotation-xyz-order">3. Combined Euler rotation
+(XYZ order)</h2>
+<p>If you rotate <strong>X → then Y → then Z</strong>:</p>
+<p>[ ]</p>
+<p>Matrix multiplication is <strong>right to left</strong> (apply X
+first).</p>
+<p>Expanded matrix:</p>
+<p>[ ]</p>
+<p>Where:</p>
+<ul>
+<li>( c_x = (x) )</li>
+<li>( s_x = (x) )</li>
+</ul>
+<hr />
+<h2 id="rotating-a-vector-with-euler-angles">4. Rotating a vector with
+Euler angles</h2>
+<p>Given a vector:</p>
+<p>[ = ]</p>
+<p>Rotated vector:</p>
+<p>[ ]</p>
+<hr />
+<h2 id="euler-angles-as-equations-explicit">5. Euler angles as equations
+(explicit)</h2>
+<p>If using <strong>yaw–pitch–roll (Z–Y–X)</strong>:</p>
+<p>[ ]</p>
+<p>This is what people usually implement <strong>directly</strong> (and
+regret later).</p>
+<hr />
+<h2 id="why-euler-angles-cause-problems-gimbal-lock">6. Why Euler angles
+cause problems (gimbal lock)</h2>
+<p>If: [ = ^]</p>
+<p>Then:</p>
+<ul>
+<li>X and Z axes align</li>
+<li>You lose one degree of freedom</li>
+<li>Rotations become unstable</li>
+</ul>
+<p>This is <strong>gimbal lock</strong>.</p>
+<p>Your quaternion code avoids this entirely.</p>
+<hr />
+<h2 id="euler-quaternion-conversion">7. Euler → Quaternion
+conversion</h2>
+<p>Given Euler angles ( (, , ) ):</p>
+<p>[ _z &amp;= - \end{aligned} }]</p>
+<p>(This is why quaternions feel scary — but they’re actually simpler in
+practice.)</p>
+<hr />
+<h2 id="when-euler-angles-are-still-useful">8. When Euler angles are
+still useful</h2>
+<p>Euler angles are fine for:</p>
+<ul>
+<li>UI sliders</li>
+<li>Editing tools</li>
+<li>Debugging</li>
+<li>Serialization</li>
+</ul>
+<p>But <strong>not</strong> for:</p>
+<ul>
+<li>Camera motion</li>
+<li>Free rotation</li>
+<li>Animation interpolation</li>
+</ul>
+<hr />
+<h2 id="key-mental-takeaway">9. Key mental takeaway</h2>
+<blockquote>
+<p>Euler angles describe <strong>how</strong> you rotate. Quaternions
+describe <strong>what rotation you have</strong>.</p>
+</blockquote>
+<p>That’s why engines store quaternions internally but expose Euler
+angles in editors.</p>
+<hr />
+<h2 id="great-references">10. Great references</h2>
+<ul>
+<li>Wikipedia (math-heavy but correct): <a
+href="https://en.wikipedia.org/wiki/Euler_angles">https://en.wikipedia.org/wiki/Euler_angles</a></li>
+<li>LearnOpenGL (practical): <a
+href="https://learnopengl.com/Getting-started/Camera">https://learnopengl.com/Getting-started/Camera</a></li>
+<li>3Blue1Brown (intuition): <a
+href="https://www.youtube.com/watch?v=zc8b2Jo7mno">https://www.youtube.com/watch?v=zc8b2Jo7mno</a></li>
+</ul>
+<hr />
+<p>If you want next:</p>
+<ul>
+<li>🔁 Euler ↔︎ Quaternion code</li>
+<li>🎥 Camera math explained geometrically</li>
+<li>🧠 Why order matters visually</li>
+<li>🧩 Gimbal lock demo</li>
+</ul>
+<p>Just say the word.</p>
+<p>Great question — this is exactly where rotation matrices stop being
+“symbols on a page” and start making sense.</p>
+<p>I’ll walk you through <strong>how to actually do the math</strong>,
+step by step, for <strong>rotation about the X-axis</strong>, and show
+<strong>why the formula looks the way it does</strong>.</p>
+<hr />
+<h1 id="the-x-axis-rotation-matrix">1. The X-axis rotation matrix</h1>
+<p>Rotation by angle ( ) around the <strong>X-axis</strong>:</p>
+<p>[ R_x() = ]</p>
+<p>This matrix rotates points in the <strong>YZ-plane</strong>, leaving
+X unchanged.</p>
+<hr />
+<h1 id="the-vector-youre-rotating">2. The vector you’re rotating</h1>
+<p>Let your 3D point be:</p>
+<p>[ = ]</p>
+<p>To rotate it:</p>
+<p>[ ]</p>
+<hr />
+<h1 id="matrix-multiplication-rule-important">3. Matrix multiplication
+rule (important)</h1>
+<p>When multiplying a matrix by a vector:</p>
+[
+<span class="math display">\[\begin{bmatrix}
+a &amp; b &amp; c \
+d &amp; e &amp; f \
+g &amp; h &amp; i
+\end{bmatrix}\]</span>
+g &amp; h &amp; i \end{bmatrix}
+<span class="math display">\[\begin{bmatrix}
+x \ y \ z
+\end{bmatrix}\]</span>
+<p>=============</p>
+<span class="math display">\[\begin{bmatrix}
+ax + by + cz \
+dx + ey + fz \
+gx + hy + iz
+\end{bmatrix}\]</span>
+<p><br />
+gx + hy + iz \end{bmatrix} ]</p>
+<p>Each <strong>row</strong> produces one output component.</p>
+<hr />
+<h1 id="multiply-r_x-by-the-vector">4. Multiply (R_x()) by the
+vector</h1>
+<h3 id="first-row-x">First row → (x’)</h3>
+<p>[ x’ = 1x + 0y + 0z = x]</p>
+<p>✔ X stays the same</p>
+<hr />
+<h3 id="second-row-y">Second row → (y’)</h3>
+<p>[ y’ = 0x + y + (-)z]</p>
+<p>[ ]</p>
+<hr />
+<h3 id="third-row-z">Third row → (z’)</h3>
+<p>[ z’ = 0x + y + z]</p>
+<p>[ ]</p>
+<hr />
+<h1 id="final-result-important">5. Final result (important)</h1>
+<p>[ ’ &amp;= y+ z \end{aligned} }]</p>
+<p>This is the <strong>explicit math</strong> behind X-axis
+rotation.</p>
+<hr />
+<h1 id="why-this-makes-geometric-sense">6. Why this makes geometric
+sense</h1>
+<h3 id="imagine-looking-down-the-x-axis">Imagine looking down the
+X-axis</h3>
+<p>You see the <strong>YZ-plane</strong> as a flat 2D plane.</p>
+<p>The matrix:</p>
+<p>[ ]</p>
+<p>is just the <strong>2D rotation matrix</strong>, applied to
+<code>(y, z)</code>.</p>
+<p>So rotating around X in 3D = rotating <code>(y, z)</code> in 2D.</p>
+<hr />
+<h1 id="connection-to-your-quaternion-code">7. Connection to your
+quaternion code</h1>
+<p>This line in your earlier code:</p>
+<div class="sourceCode" id="cb1"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a>y <span class="op">=</span> y<span class="op">*</span>c <span class="op">-</span> z<span class="op">*</span>s</span>
+<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>z <span class="op">=</span> y<span class="op">*</span>s <span class="op">+</span> z<span class="op">*</span>c</span></code></pre></div>
+<p>is <strong>exactly</strong> this matrix multiplication.</p>
+<p>Quaternions just wrap this logic in a more stable form.</p>
+<hr />
+<h1 id="quick-numeric-example">8. Quick numeric example</h1>
+<p>Rotate point <code>(0, 1, 0)</code> by <strong>90° (π/2)</strong>
+around X:</p>
+<p>[ (90<sup>)=0,(90</sup>)=1]</p>
+<p>[ ]</p>
+<p>Result:</p>
+<p>[ (0,0,1)]</p>
+<p>✔ Point moved from Y-axis to Z-axis</p>
+<hr />
+<h1 id="key-mental-model">9. Key mental model</h1>
+<blockquote>
+<p>A rotation matrix <strong>does not move axes</strong>. It
+re-expresses the point in a rotated coordinate system.</p>
+</blockquote>
+<hr />
+<p>If you want next, I can show:</p>
+<ul>
+<li>Y-axis and Z-axis derivations (they follow the same logic)</li>
+<li>Why the sine signs flip</li>
+<li>How to derive rotation matrices from scratch</li>
+<li>How this turns into quaternion math</li>
+</ul>
+<p>Just tell me 👍</p>
+</body>
+</html>
diff --git a/docs/chatgpt-dump/euler-angle.md b/docs/chatgpt-dump/euler-angle.md
new file mode 100644 (file)
index 0000000..1e3d20a
--- /dev/null
@@ -0,0 +1,443 @@
+Absolutely. I’ll give you the **actual math formulas**, show the **rotation matrices**, explain the **order issue**, and relate them directly to what you’ve been doing with quaternions.
+
+---
+
+## 1. What are Euler angles?
+
+**Euler angles** describe a 3D rotation as **three sequential rotations** about coordinate axes.
+
+Typically:
+
+* **Roll** → rotation about X
+* **Pitch** → rotation about Y
+* **Yaw** → rotation about Z
+
+We’ll denote:
+
+* ( \alpha ) = rotation about X (roll)
+* ( \beta ) = rotation about Y (pitch)
+* ( \gamma ) = rotation about Z (yaw)
+
+⚠️ **Order matters**.
+`XYZ` is *not* the same as `ZYX`.
+
+---
+
+## 2. Single-axis rotation matrices
+
+### Rotation about X-axis (roll)
+
+[
+\boxed{
+R_x(\alpha) =
+\begin{bmatrix}
+1 & 0 & 0 \
+0 & \cos\alpha & -\sin\alpha \
+0 & \sin\alpha & \cos\alpha
+\end{bmatrix}
+}
+]
+
+---
+
+### Rotation about Y-axis (pitch)
+
+[
+\boxed{
+R_y(\beta) =
+\begin{bmatrix}
+\cos\beta & 0 & \sin\beta \
+0 & 1 & 0 \
+-\sin\beta & 0 & \cos\beta
+\end{bmatrix}
+}
+]
+
+---
+
+### Rotation about Z-axis (yaw)
+
+[
+\boxed{
+R_z(\gamma) =
+\begin{bmatrix}
+\cos\gamma & -\sin\gamma & 0 \
+\sin\gamma & \cos\gamma & 0 \
+0 & 0 & 1
+\end{bmatrix}
+}
+]
+
+---
+
+## 3. Combined Euler rotation (XYZ order)
+
+If you rotate **X → then Y → then Z**:
+
+[
+\boxed{
+R = R_z(\gamma) ; R_y(\beta) ; R_x(\alpha)
+}
+]
+
+Matrix multiplication is **right to left** (apply X first).
+
+Expanded matrix:
+
+[
+\begin{bmatrix}
+c_\gamma c_\beta &
+c_\gamma s_\beta s_\alpha - s_\gamma c_\alpha &
+c_\gamma s_\beta c_\alpha + s_\gamma s_\alpha \
+
+s_\gamma c_\beta &
+s_\gamma s_\beta s_\alpha + c_\gamma c_\alpha &
+s_\gamma s_\beta c_\alpha - c_\gamma s_\alpha \
+
+-s_\beta &
+c_\beta s_\alpha &
+c_\beta c_\alpha
+\end{bmatrix}
+]
+
+Where:
+
+* ( c_x = \cos(x) )
+* ( s_x = \sin(x) )
+
+---
+
+## 4. Rotating a vector with Euler angles
+
+Given a vector:
+
+[
+\vec{v} =
+\begin{bmatrix}
+x \ y \ z
+\end{bmatrix}
+]
+
+Rotated vector:
+
+[
+\boxed{
+\vec{v}' = R \vec{v}
+}
+]
+
+---
+
+## 5. Euler angles as equations (explicit)
+
+If using **yaw–pitch–roll (Z–Y–X)**:
+
+[
+\begin{aligned}
+x' &= x(\cos\gamma\cos\beta)
++ y(\cos\gamma\sin\beta\sin\alpha - \sin\gamma\cos\alpha)
++ z(\cos\gamma\sin\beta\cos\alpha + \sin\gamma\sin\alpha) \
+y' &= x(\sin\gamma\cos\beta)
++ y(\sin\gamma\sin\beta\sin\alpha + \cos\gamma\cos\alpha)
++ z(\sin\gamma\sin\beta\cos\alpha - \cos\gamma\sin\alpha) \
+z' &= x(-\sin\beta)
++ y(\cos\beta\sin\alpha)
++ z(\cos\beta\cos\alpha)
+\end{aligned}
+]
+
+This is what people usually implement **directly** (and regret later).
+
+---
+
+## 6. Why Euler angles cause problems (gimbal lock)
+
+If:
+[
+\beta = \pm 90^\circ
+]
+
+Then:
+
+* X and Z axes align
+* You lose one degree of freedom
+* Rotations become unstable
+
+This is **gimbal lock**.
+
+Your quaternion code avoids this entirely.
+
+---
+
+## 7. Euler → Quaternion conversion
+
+Given Euler angles ( (\alpha, \beta, \gamma) ):
+
+[
+\boxed{
+\begin{aligned}
+q_w &= \cos\frac{\alpha}{2}\cos\frac{\beta}{2}\cos\frac{\gamma}{2}
++ \sin\frac{\alpha}{2}\sin\frac{\beta}{2}\sin\frac{\gamma}{2} \
+q_x &= \sin\frac{\alpha}{2}\cos\frac{\beta}{2}\cos\frac{\gamma}{2}
+- \cos\frac{\alpha}{2}\sin\frac{\beta}{2}\sin\frac{\gamma}{2} \
+q_y &= \cos\frac{\alpha}{2}\sin\frac{\beta}{2}\cos\frac{\gamma}{2}
++ \sin\frac{\alpha}{2}\cos\frac{\beta}{2}\sin\frac{\gamma}{2} \
+q_z &= \cos\frac{\alpha}{2}\cos\frac{\beta}{2}\sin\frac{\gamma}{2}
+- \sin\frac{\alpha}{2}\sin\frac{\beta}{2}\cos\frac{\gamma}{2}
+\end{aligned}
+}
+]
+
+(This is why quaternions feel scary — but they’re actually simpler in practice.)
+
+---
+
+## 8. When Euler angles are still useful
+
+Euler angles are fine for:
+
+* UI sliders
+* Editing tools
+* Debugging
+* Serialization
+
+But **not** for:
+
+* Camera motion
+* Free rotation
+* Animation interpolation
+
+---
+
+## 9. Key mental takeaway
+
+> Euler angles describe **how** you rotate.
+> Quaternions describe **what rotation you have**.
+
+That’s why engines store quaternions internally but expose Euler angles in editors.
+
+---
+
+## 10. Great references
+
+* Wikipedia (math-heavy but correct):
+  [https://en.wikipedia.org/wiki/Euler_angles](https://en.wikipedia.org/wiki/Euler_angles)
+* LearnOpenGL (practical):
+  [https://learnopengl.com/Getting-started/Camera](https://learnopengl.com/Getting-started/Camera)
+* 3Blue1Brown (intuition):
+  [https://www.youtube.com/watch?v=zc8b2Jo7mno](https://www.youtube.com/watch?v=zc8b2Jo7mno)
+
+---
+
+If you want next:
+
+* 🔁 Euler ↔ Quaternion code
+* 🎥 Camera math explained geometrically
+* 🧠 Why order matters visually
+* 🧩 Gimbal lock demo
+
+Just say the word.
+
+Great question — this is exactly where rotation matrices stop being “symbols on a page” and start making sense.
+
+I’ll walk you through **how to actually do the math**, step by step, for **rotation about the X-axis**, and show **why the formula looks the way it does**.
+
+---
+
+# 1. The X-axis rotation matrix
+
+Rotation by angle ( \alpha ) around the **X-axis**:
+
+[
+R_x(\alpha) =
+\begin{bmatrix}
+1 & 0 & 0 \
+0 & \cos\alpha & -\sin\alpha \
+0 & \sin\alpha & \cos\alpha
+\end{bmatrix}
+]
+
+This matrix rotates points in the **YZ-plane**, leaving X unchanged.
+
+---
+
+# 2. The vector you’re rotating
+
+Let your 3D point be:
+
+[
+\vec{v} =
+\begin{bmatrix}
+x \ y \ z
+\end{bmatrix}
+]
+
+To rotate it:
+
+[
+\boxed{
+\vec{v}' = R_x(\alpha)\vec{v}
+}
+]
+
+---
+
+# 3. Matrix multiplication rule (important)
+
+When multiplying a matrix by a vector:
+
+[
+\begin{bmatrix}
+a & b & c \
+d & e & f \
+g & h & i
+\end{bmatrix}
+\begin{bmatrix}
+x \ y \ z
+\end{bmatrix}
+=============
+
+\begin{bmatrix}
+ax + by + cz \
+dx + ey + fz \
+gx + hy + iz
+\end{bmatrix}
+]
+
+Each **row** produces one output component.
+
+---
+
+# 4. Multiply (R_x(\alpha)) by the vector
+
+### First row → (x')
+
+[
+x' = 1\cdot x + 0\cdot y + 0\cdot z = x
+]
+
+✔ X stays the same
+
+---
+
+### Second row → (y')
+
+[
+y' = 0\cdot x + \cos\alpha \cdot y + (-\sin\alpha)\cdot z
+]
+
+[
+\boxed{
+y' = y\cos\alpha - z\sin\alpha
+}
+]
+
+---
+
+### Third row → (z')
+
+[
+z' = 0\cdot x + \sin\alpha \cdot y + \cos\alpha \cdot z
+]
+
+[
+\boxed{
+z' = y\sin\alpha + z\cos\alpha
+}
+]
+
+---
+
+# 5. Final result (important)
+
+[
+\boxed{
+\begin{aligned}
+x' &= x \
+y' &= y\cos\alpha - z\sin\alpha \
+z' &= y\sin\alpha + z\cos\alpha
+\end{aligned}
+}
+]
+
+This is the **explicit math** behind X-axis rotation.
+
+---
+
+# 6. Why this makes geometric sense
+
+### Imagine looking down the X-axis
+
+You see the **YZ-plane** as a flat 2D plane.
+
+The matrix:
+
+[
+\begin{bmatrix}
+\cos\alpha & -\sin\alpha \
+\sin\alpha & \cos\alpha
+\end{bmatrix}
+]
+
+is just the **2D rotation matrix**, applied to `(y, z)`.
+
+So rotating around X in 3D = rotating `(y, z)` in 2D.
+
+---
+
+# 7. Connection to your quaternion code
+
+This line in your earlier code:
+
+```js
+y = y*c - z*s
+z = y*s + z*c
+```
+
+is **exactly** this matrix multiplication.
+
+Quaternions just wrap this logic in a more stable form.
+
+---
+
+# 8. Quick numeric example
+
+Rotate point `(0, 1, 0)` by **90° (π/2)** around X:
+
+[
+\cos(90^\circ)=0,\quad \sin(90^\circ)=1
+]
+
+[
+\begin{aligned}
+x' &= 0 \
+y' &= 1\cdot0 - 0\cdot1 = 0 \
+z' &= 1\cdot1 + 0\cdot0 = 1
+\end{aligned}
+]
+
+Result:
+
+[
+(0,0,1)
+]
+
+✔ Point moved from Y-axis to Z-axis
+
+---
+
+# 9. Key mental model
+
+> A rotation matrix **does not move axes**.
+> It re-expresses the point in a rotated coordinate system.
+
+---
+
+If you want next, I can show:
+
+* Y-axis and Z-axis derivations (they follow the same logic)
+* Why the sine signs flip
+* How to derive rotation matrices from scratch
+* How this turns into quaternion math
+
+Just tell me 👍
diff --git a/docs/chatgpt-dump/quaternion-rotation.png b/docs/chatgpt-dump/quaternion-rotation.png
new file mode 100644 (file)
index 0000000..06eddd8
Binary files /dev/null and b/docs/chatgpt-dump/quaternion-rotation.png differ
diff --git a/docs/chatgpt-dump/quaternions.html b/docs/chatgpt-dump/quaternions.html
new file mode 100644 (file)
index 0000000..35f63d2
--- /dev/null
@@ -0,0 +1,809 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" lang="" xml:lang="">
+<head>
+  <meta charset="utf-8" />
+  <meta name="generator" content="pandoc" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
+  <title>quaternions</title>
+  <style>
+    html {
+      color: #1a1a1a;
+      background-color: #fdfdfd;
+    }
+    body {
+      margin: 0 auto;
+      max-width: 36em;
+      padding-left: 50px;
+      padding-right: 50px;
+      padding-top: 50px;
+      padding-bottom: 50px;
+      hyphens: auto;
+      overflow-wrap: break-word;
+      text-rendering: optimizeLegibility;
+      font-kerning: normal;
+    }
+    @media (max-width: 600px) {
+      body {
+        font-size: 0.9em;
+        padding: 12px;
+      }
+      h1 {
+        font-size: 1.8em;
+      }
+    }
+    @media print {
+      html {
+        background-color: white;
+      }
+      body {
+        background-color: transparent;
+        color: black;
+        font-size: 12pt;
+      }
+      p, h2, h3 {
+        orphans: 3;
+        widows: 3;
+      }
+      h2, h3, h4 {
+        page-break-after: avoid;
+      }
+    }
+    p {
+      margin: 1em 0;
+    }
+    a {
+      color: #1a1a1a;
+    }
+    a:visited {
+      color: #1a1a1a;
+    }
+    img {
+      max-width: 100%;
+    }
+    h1, h2, h3, h4, h5, h6 {
+      margin-top: 1.4em;
+    }
+    h5, h6 {
+      font-size: 1em;
+      font-style: italic;
+    }
+    h6 {
+      font-weight: normal;
+    }
+    ol, ul {
+      padding-left: 1.7em;
+      margin-top: 1em;
+    }
+    li > ol, li > ul {
+      margin-top: 0;
+    }
+    blockquote {
+      margin: 1em 0 1em 1.7em;
+      padding-left: 1em;
+      border-left: 2px solid #e6e6e6;
+      color: #606060;
+    }
+    code {
+      font-family: Menlo, Monaco, Consolas, 'Lucida Console', monospace;
+      font-size: 85%;
+      margin: 0;
+      hyphens: manual;
+    }
+    pre {
+      margin: 1em 0;
+      overflow: auto;
+    }
+    pre code {
+      padding: 0;
+      overflow: visible;
+      overflow-wrap: normal;
+    }
+    .sourceCode {
+     background-color: transparent;
+     overflow: visible;
+    }
+    hr {
+      background-color: #1a1a1a;
+      border: none;
+      height: 1px;
+      margin: 1em 0;
+    }
+    table {
+      margin: 1em 0;
+      border-collapse: collapse;
+      width: 100%;
+      overflow-x: auto;
+      display: block;
+      font-variant-numeric: lining-nums tabular-nums;
+    }
+    table caption {
+      margin-bottom: 0.75em;
+    }
+    tbody {
+      margin-top: 0.5em;
+      border-top: 1px solid #1a1a1a;
+      border-bottom: 1px solid #1a1a1a;
+    }
+    th {
+      border-top: 1px solid #1a1a1a;
+      padding: 0.25em 0.5em 0.25em 0.5em;
+    }
+    td {
+      padding: 0.125em 0.5em 0.25em 0.5em;
+    }
+    header {
+      margin-bottom: 4em;
+      text-align: center;
+    }
+    #TOC li {
+      list-style: none;
+    }
+    #TOC ul {
+      padding-left: 1.3em;
+    }
+    #TOC > ul {
+      padding-left: 0;
+    }
+    #TOC a:not(:hover) {
+      text-decoration: none;
+    }
+    code{white-space: pre-wrap;}
+    span.smallcaps{font-variant: small-caps;}
+    div.columns{display: flex; gap: min(4vw, 1.5em);}
+    div.column{flex: auto; overflow-x: auto;}
+    div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;}
+    /* The extra [class] is a hack that increases specificity enough to
+       override a similar rule in reveal.js */
+    ul.task-list[class]{list-style: none;}
+    ul.task-list li input[type="checkbox"] {
+      font-size: inherit;
+      width: 0.8em;
+      margin: 0 0.8em 0.2em -1.6em;
+      vertical-align: middle;
+    }
+    /* CSS for syntax highlighting */
+    pre > code.sourceCode { white-space: pre; position: relative; }
+    pre > code.sourceCode > span { line-height: 1.25; }
+    pre > code.sourceCode > span:empty { height: 1.2em; }
+    .sourceCode { overflow: visible; }
+    code.sourceCode > span { color: inherit; text-decoration: inherit; }
+    div.sourceCode { margin: 1em 0; }
+    pre.sourceCode { margin: 0; }
+    @media screen {
+    div.sourceCode { overflow: auto; }
+    }
+    @media print {
+    pre > code.sourceCode { white-space: pre-wrap; }
+    pre > code.sourceCode > span { text-indent: -5em; padding-left: 5em; }
+    }
+    pre.numberSource code
+      { counter-reset: source-line 0; }
+    pre.numberSource code > span
+      { position: relative; left: -4em; counter-increment: source-line; }
+    pre.numberSource code > span > a:first-child::before
+      { content: counter(source-line);
+        position: relative; left: -1em; text-align: right; vertical-align: baseline;
+        border: none; display: inline-block;
+        -webkit-touch-callout: none; -webkit-user-select: none;
+        -khtml-user-select: none; -moz-user-select: none;
+        -ms-user-select: none; user-select: none;
+        padding: 0 4px; width: 4em;
+        color: #aaaaaa;
+      }
+    pre.numberSource { margin-left: 3em; border-left: 1px solid #aaaaaa;  padding-left: 4px; }
+    div.sourceCode
+      {   }
+    @media screen {
+    pre > code.sourceCode > span > a:first-child::before { text-decoration: underline; }
+    }
+    code span.al { color: #ff0000; font-weight: bold; } /* Alert */
+    code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */
+    code span.at { color: #7d9029; } /* Attribute */
+    code span.bn { color: #40a070; } /* BaseN */
+    code span.bu { color: #008000; } /* BuiltIn */
+    code span.cf { color: #007020; font-weight: bold; } /* ControlFlow */
+    code span.ch { color: #4070a0; } /* Char */
+    code span.cn { color: #880000; } /* Constant */
+    code span.co { color: #60a0b0; font-style: italic; } /* Comment */
+    code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */
+    code span.do { color: #ba2121; font-style: italic; } /* Documentation */
+    code span.dt { color: #902000; } /* DataType */
+    code span.dv { color: #40a070; } /* DecVal */
+    code span.er { color: #ff0000; font-weight: bold; } /* Error */
+    code span.ex { } /* Extension */
+    code span.fl { color: #40a070; } /* Float */
+    code span.fu { color: #06287e; } /* Function */
+    code span.im { color: #008000; font-weight: bold; } /* Import */
+    code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */
+    code span.kw { color: #007020; font-weight: bold; } /* Keyword */
+    code span.op { color: #666666; } /* Operator */
+    code span.ot { color: #007020; } /* Other */
+    code span.pp { color: #bc7a00; } /* Preprocessor */
+    code span.sc { color: #4070a0; } /* SpecialChar */
+    code span.ss { color: #bb6688; } /* SpecialString */
+    code span.st { color: #4070a0; } /* String */
+    code span.va { color: #19177c; } /* Variable */
+    code span.vs { color: #4070a0; } /* VerbatimString */
+    code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */
+  </style>
+</head>
+<body>
+<p>Absolutely! I can show you how to incorporate
+<strong>quaternions</strong> into your code to allow smooth rotation
+around <strong>multiple axes at once</strong>, avoiding the limitations
+of your current <code>rotate</code> function.</p>
+<p>Let’s go step by step.</p>
+<hr />
+<h3 id="why-quaternions-here">1. <strong>Why Quaternions
+Here</strong></h3>
+<p>Currently, you rotate around a single axis per frame:</p>
+<div class="sourceCode" id="cb1"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="fu">rotate</span>(a<span class="op">,</span> rotationAxis<span class="op">,</span> angle)</span></code></pre></div>
+<p>With quaternions, you can <strong>combine rotations around multiple
+axes</strong> seamlessly. For example, you could rotate simultaneously
+around X <strong>and</strong> Y.</p>
+<hr />
+<h3 id="quaternion-basics">2. <strong>Quaternion Basics</strong></h3>
+<p>A quaternion can be represented as:</p>
+<p>[ q = w + xi + yj + zk]</p>
+<p>For 3D rotation:</p>
+<div class="sourceCode" id="cb2"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a>q <span class="op">=</span> [w<span class="op">,</span> x<span class="op">,</span> y<span class="op">,</span> z]</span></code></pre></div>
+<ul>
+<li><code>w</code> = cos(angle/2)</li>
+<li><code>x, y, z</code> = axis vector * sin(angle/2)</li>
+</ul>
+<p>To rotate a point <code>v</code>:</p>
+<ol type="1">
+<li>Convert <code>v</code> to a quaternion
+<code>[0, vx, vy, vz]</code>.</li>
+<li>Apply <code>v' = q * v * q⁻¹</code>.</li>
+</ol>
+<hr />
+<h3 id="quaternion-utilities-in-js">3. <strong>Quaternion Utilities in
+JS</strong></h3>
+<div class="sourceCode" id="cb3"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">quatFromAxisAngle</span>(axis<span class="op">,</span> angle) {</span>
+<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> half <span class="op">=</span> angle <span class="op">/</span> <span class="dv">2</span></span>
+<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> s <span class="op">=</span> <span class="bu">Math</span><span class="op">.</span><span class="fu">sin</span>(half)</span>
+<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> [</span>
+<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a>        <span class="bu">Math</span><span class="op">.</span><span class="fu">cos</span>(half)<span class="op">,</span>   <span class="co">// w</span></span>
+<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a>        axis<span class="op">.</span><span class="at">x</span> <span class="op">*</span> s<span class="op">,</span>       <span class="co">// x</span></span>
+<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a>        axis<span class="op">.</span><span class="at">y</span> <span class="op">*</span> s<span class="op">,</span>       <span class="co">// y</span></span>
+<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a>        axis<span class="op">.</span><span class="at">z</span> <span class="op">*</span> s        <span class="co">// z</span></span>
+<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a>    ]</span>
+<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">quatMultiply</span>(q1<span class="op">,</span> q2) {</span>
+<span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> [w1<span class="op">,</span>x1<span class="op">,</span>y1<span class="op">,</span>z1] <span class="op">=</span> q1</span>
+<span id="cb3-14"><a href="#cb3-14" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> [w2<span class="op">,</span>x2<span class="op">,</span>y2<span class="op">,</span>z2] <span class="op">=</span> q2</span>
+<span id="cb3-15"><a href="#cb3-15" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> [</span>
+<span id="cb3-16"><a href="#cb3-16" aria-hidden="true" tabindex="-1"></a>        w1<span class="op">*</span>w2 <span class="op">-</span> x1<span class="op">*</span>x2 <span class="op">-</span> y1<span class="op">*</span>y2 <span class="op">-</span> z1<span class="op">*</span>z2<span class="op">,</span></span>
+<span id="cb3-17"><a href="#cb3-17" aria-hidden="true" tabindex="-1"></a>        w1<span class="op">*</span>x2 <span class="op">+</span> x1<span class="op">*</span>w2 <span class="op">+</span> y1<span class="op">*</span>z2 <span class="op">-</span> z1<span class="op">*</span>y2<span class="op">,</span></span>
+<span id="cb3-18"><a href="#cb3-18" aria-hidden="true" tabindex="-1"></a>        w1<span class="op">*</span>y2 <span class="op">-</span> x1<span class="op">*</span>z2 <span class="op">+</span> y1<span class="op">*</span>w2 <span class="op">+</span> z1<span class="op">*</span>x2<span class="op">,</span></span>
+<span id="cb3-19"><a href="#cb3-19" aria-hidden="true" tabindex="-1"></a>        w1<span class="op">*</span>z2 <span class="op">+</span> x1<span class="op">*</span>y2 <span class="op">-</span> y1<span class="op">*</span>x2 <span class="op">+</span> z1<span class="op">*</span>w2</span>
+<span id="cb3-20"><a href="#cb3-20" aria-hidden="true" tabindex="-1"></a>    ]</span>
+<span id="cb3-21"><a href="#cb3-21" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb3-22"><a href="#cb3-22" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb3-23"><a href="#cb3-23" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">quatRotateVector</span>(q<span class="op">,</span> v) {</span>
+<span id="cb3-24"><a href="#cb3-24" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> [w<span class="op">,</span>x<span class="op">,</span>y<span class="op">,</span>z] <span class="op">=</span> q</span>
+<span id="cb3-25"><a href="#cb3-25" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> [vx<span class="op">,</span>vy<span class="op">,</span>vz] <span class="op">=</span> [v<span class="op">.</span><span class="at">x</span><span class="op">,</span>v<span class="op">.</span><span class="at">y</span><span class="op">,</span>v<span class="op">.</span><span class="at">z</span>]</span>
+<span id="cb3-26"><a href="#cb3-26" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb3-27"><a href="#cb3-27" aria-hidden="true" tabindex="-1"></a>    <span class="co">// Quaternion multiplication: v&#39; = q * v_quat * q⁻¹</span></span>
+<span id="cb3-28"><a href="#cb3-28" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> qv <span class="op">=</span> [<span class="dv">0</span><span class="op">,</span> vx<span class="op">,</span> vy<span class="op">,</span> vz]</span>
+<span id="cb3-29"><a href="#cb3-29" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> qConj <span class="op">=</span> [w<span class="op">,</span> <span class="op">-</span>x<span class="op">,</span> <span class="op">-</span>y<span class="op">,</span> <span class="op">-</span>z]</span>
+<span id="cb3-30"><a href="#cb3-30" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb3-31"><a href="#cb3-31" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> temp <span class="op">=</span> <span class="fu">quatMultiply</span>(q<span class="op">,</span> qv)</span>
+<span id="cb3-32"><a href="#cb3-32" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> result <span class="op">=</span> <span class="fu">quatMultiply</span>(temp<span class="op">,</span> qConj)</span>
+<span id="cb3-33"><a href="#cb3-33" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> { <span class="dt">x</span><span class="op">:</span> result[<span class="dv">1</span>]<span class="op">,</span> <span class="dt">y</span><span class="op">:</span> result[<span class="dv">2</span>]<span class="op">,</span> <span class="dt">z</span><span class="op">:</span> result[<span class="dv">3</span>] }</span>
+<span id="cb3-34"><a href="#cb3-34" aria-hidden="true" tabindex="-1"></a>}</span></code></pre></div>
+<hr />
+<h3 id="using-quaternions-in-your-frame">4. <strong>Using Quaternions in
+Your Frame</strong></h3>
+<p>Instead of your <code>rotate</code> function:</p>
+<div class="sourceCode" id="cb4"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> angleX <span class="op">=</span> angle<span class="op">;</span> </span>
+<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> angleY <span class="op">=</span> angle<span class="op">/</span><span class="dv">2</span><span class="op">;</span> <span class="co">// different speed for demo</span></span>
+<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> qx <span class="op">=</span> <span class="fu">quatFromAxisAngle</span>({<span class="dt">x</span><span class="op">:</span><span class="dv">1</span><span class="op">,</span><span class="dt">y</span><span class="op">:</span><span class="dv">0</span><span class="op">,</span><span class="dt">z</span><span class="op">:</span><span class="dv">0</span>}<span class="op">,</span> angleX)</span>
+<span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> qy <span class="op">=</span> <span class="fu">quatFromAxisAngle</span>({<span class="dt">x</span><span class="op">:</span><span class="dv">0</span><span class="op">,</span><span class="dt">y</span><span class="op">:</span><span class="dv">1</span><span class="op">,</span><span class="dt">z</span><span class="op">:</span><span class="dv">0</span>}<span class="op">,</span> angleY)</span>
+<span id="cb4-6"><a href="#cb4-6" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb4-7"><a href="#cb4-7" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> q <span class="op">=</span> <span class="fu">quatMultiply</span>(qy<span class="op">,</span> qx) <span class="co">// combined rotation</span></span>
+<span id="cb4-8"><a href="#cb4-8" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb4-9"><a href="#cb4-9" aria-hidden="true" tabindex="-1"></a><span class="cf">for</span> (<span class="kw">const</span> f <span class="kw">of</span> fs) {</span>
+<span id="cb4-10"><a href="#cb4-10" aria-hidden="true" tabindex="-1"></a>    <span class="cf">for</span> (<span class="kw">let</span> i <span class="op">=</span> <span class="dv">0</span><span class="op">;</span> i <span class="op">&lt;</span> f<span class="op">.</span><span class="at">length</span><span class="op">;</span> <span class="op">++</span>i) {</span>
+<span id="cb4-11"><a href="#cb4-11" aria-hidden="true" tabindex="-1"></a>        <span class="kw">const</span> a <span class="op">=</span> vs[f[i]]</span>
+<span id="cb4-12"><a href="#cb4-12" aria-hidden="true" tabindex="-1"></a>        <span class="kw">const</span> b <span class="op">=</span> vs[f[(i <span class="op">+</span> <span class="dv">1</span>)<span class="op">%</span>f<span class="op">.</span><span class="at">length</span>]]</span>
+<span id="cb4-13"><a href="#cb4-13" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb4-14"><a href="#cb4-14" aria-hidden="true" tabindex="-1"></a>        <span class="fu">line</span>(</span>
+<span id="cb4-15"><a href="#cb4-15" aria-hidden="true" tabindex="-1"></a>            <span class="bu">screen</span>(<span class="fu">project</span>(<span class="fu">translateZ</span>(<span class="fu">quatRotateVector</span>(q<span class="op">,</span> a)<span class="op">,</span> dz)))<span class="op">,</span></span>
+<span id="cb4-16"><a href="#cb4-16" aria-hidden="true" tabindex="-1"></a>            <span class="bu">screen</span>(<span class="fu">project</span>(<span class="fu">translateZ</span>(<span class="fu">quatRotateVector</span>(q<span class="op">,</span> b)<span class="op">,</span> dz)))</span>
+<span id="cb4-17"><a href="#cb4-17" aria-hidden="true" tabindex="-1"></a>        )</span>
+<span id="cb4-18"><a href="#cb4-18" aria-hidden="true" tabindex="-1"></a>    }</span>
+<span id="cb4-19"><a href="#cb4-19" aria-hidden="true" tabindex="-1"></a>}</span></code></pre></div>
+<ul>
+<li>Now the cube rotates around <strong>both X and Y axes
+simultaneously</strong>.</li>
+<li>You can extend this to any combination of axes by multiplying more
+quaternions.</li>
+</ul>
+<hr />
+<h3 id="optional-change-axes-dynamically">5. <strong>Optional: Change
+Axes Dynamically</strong></h3>
+<p>You could replace your <code>rotationAxis</code> system by letting
+the user click to toggle <strong>which axes are active</strong>, then
+generate quaternions for each active axis and multiply them
+together.</p>
+<hr />
+<p>If you want, I can rewrite your <strong>entire code snippet using
+quaternions</strong>, so the cube rotates smoothly around multiple axes
+and keeps your click-to-change-axis feature.</p>
+<p>Do you want me to do that?</p>
+<p>Absolutely! Let’s go through your <strong>quaternion-based 3D cube
+code</strong> line by line. I’ll break it down into sections so it’s
+easier to digest.</p>
+<hr />
+<h2 id="canvas-setup"><strong>1. Canvas Setup</strong></h2>
+<div class="sourceCode" id="cb5"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> game <span class="op">=</span> <span class="bu">document</span><span class="op">.</span><span class="fu">getElementById</span>(<span class="st">&quot;game&quot;</span>)<span class="op">;</span></span>
+<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a>game<span class="op">.</span><span class="at">width</span> <span class="op">=</span> <span class="dv">400</span><span class="op">;</span></span>
+<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a>game<span class="op">.</span><span class="at">height</span> <span class="op">=</span> <span class="dv">400</span><span class="op">;</span></span></code></pre></div>
+<ul>
+<li>Gets the <code>&lt;canvas&gt;</code> element with ID
+<code>game</code>.</li>
+<li>Sets the canvas dimensions to 400×400 pixels.</li>
+</ul>
+<div class="sourceCode" id="cb6"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> BACKGROUND <span class="op">=</span> <span class="st">&quot;#101010&quot;</span><span class="op">;</span></span>
+<span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> FOREGROUND <span class="op">=</span> <span class="st">&quot;#50FF50&quot;</span><span class="op">;</span></span>
+<span id="cb6-3"><a href="#cb6-3" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> TEXT <span class="op">=</span> <span class="st">&quot;#FFFFFF&quot;</span><span class="op">;</span></span></code></pre></div>
+<ul>
+<li>Defines colors for background, cube lines, and text.</li>
+</ul>
+<div class="sourceCode" id="cb7"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> ctx <span class="op">=</span> game<span class="op">.</span><span class="fu">getContext</span>(<span class="st">&quot;2d&quot;</span>)<span class="op">;</span></span></code></pre></div>
+<ul>
+<li>Gets the 2D drawing context so we can draw shapes on the
+canvas.</li>
+</ul>
+<hr />
+<h2 id="cube-geometry"><strong>2. Cube Geometry</strong></h2>
+<div class="sourceCode" id="cb8"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> vs <span class="op">=</span> [ <span class="op">...</span> ]<span class="op">;</span></span></code></pre></div>
+<ul>
+<li><code>vs</code> = <strong>vertices</strong> of a cube (8
+points).</li>
+<li>Each vertex has <code>x, y, z</code> coordinates in 3D space (values
+between -0.25 and 0.25).</li>
+</ul>
+<div class="sourceCode" id="cb9"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> fs <span class="op">=</span> [ <span class="op">...</span> ]<span class="op">;</span></span></code></pre></div>
+<ul>
+<li><code>fs</code> = <strong>faces/edges</strong> to connect
+vertices.</li>
+<li>Some are quads <code>[0,1,2,3]</code>, some are edges
+<code>[0,4]</code>.</li>
+<li>This tells the code <strong>which points to draw lines
+between</strong>.</li>
+</ul>
+<div class="sourceCode" id="cb10"><pre
+class="sourceCode js"><code class="sourceCode javascript"><span id="cb10-1"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> dz <span class="op">=</span> <span class="dv">1</span><span class="op">;</span></span>
+<span id="cb10-2"><a href="#cb10-2" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> angle <span class="op">=</span> <span class="dv">0</span><span class="op">;</span></span>
+<span id="cb10-3"><a href="#cb10-3" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> activeAxes <span class="op">=</span> [<span class="st">&quot;x&quot;</span>]<span class="op">;</span></span></code></pre></div>
+<ul>
+<li><code>dz</code>: distance to move the cube along Z (so it’s in front
+of the camera).</li>
+<li><code>angle</code>: current rotation angle.</li>
+<li><code>activeAxes</code>: which axes to rotate around (initially just
+<code>x</code>).</li>
+</ul>
+<hr />
+<h2 id="quaternion-functions"><strong>3. Quaternion
+Functions</strong></h2>
+<p><strong>a. Create a quaternion from an axis + angle</strong></p>
+<div class="sourceCode" id="cb11"><pre
+class="sourceCode js"><code class="sourceCode javascript"><span id="cb11-1"><a href="#cb11-1" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">quatFromAxisAngle</span>(axis<span class="op">,</span> angle) {</span>
+<span id="cb11-2"><a href="#cb11-2" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> half <span class="op">=</span> angle <span class="op">/</span> <span class="dv">2</span><span class="op">;</span></span>
+<span id="cb11-3"><a href="#cb11-3" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> s <span class="op">=</span> <span class="bu">Math</span><span class="op">.</span><span class="fu">sin</span>(half)<span class="op">;</span></span>
+<span id="cb11-4"><a href="#cb11-4" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> [</span>
+<span id="cb11-5"><a href="#cb11-5" aria-hidden="true" tabindex="-1"></a>        <span class="bu">Math</span><span class="op">.</span><span class="fu">cos</span>(half)<span class="op">,</span>   <span class="co">// w</span></span>
+<span id="cb11-6"><a href="#cb11-6" aria-hidden="true" tabindex="-1"></a>        axis<span class="op">.</span><span class="at">x</span> <span class="op">*</span> s<span class="op">,</span>       <span class="co">// x</span></span>
+<span id="cb11-7"><a href="#cb11-7" aria-hidden="true" tabindex="-1"></a>        axis<span class="op">.</span><span class="at">y</span> <span class="op">*</span> s<span class="op">,</span>       <span class="co">// y</span></span>
+<span id="cb11-8"><a href="#cb11-8" aria-hidden="true" tabindex="-1"></a>        axis<span class="op">.</span><span class="at">z</span> <span class="op">*</span> s        <span class="co">// z</span></span>
+<span id="cb11-9"><a href="#cb11-9" aria-hidden="true" tabindex="-1"></a>    ]<span class="op">;</span></span>
+<span id="cb11-10"><a href="#cb11-10" aria-hidden="true" tabindex="-1"></a>}</span></code></pre></div>
+<ul>
+<li>Converts <strong>axis-angle rotation</strong> into a quaternion
+<code>[w, x, y, z]</code>.</li>
+<li><code>half</code> = half the rotation angle.</li>
+<li><code>s = sin(half)</code> scales the axis.</li>
+<li><code>w</code> = cos(half).</li>
+</ul>
+<p><strong>b. Multiply two quaternions</strong></p>
+<div class="sourceCode" id="cb12"><pre
+class="sourceCode js"><code class="sourceCode javascript"><span id="cb12-1"><a href="#cb12-1" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">quatMultiply</span>(q1<span class="op">,</span> q2) { <span class="op">...</span> }</span></code></pre></div>
+<ul>
+<li>Standard quaternion multiplication formula.</li>
+<li>Combines two rotations into one. <strong>Order matters!</strong>
+<code>q1*q2</code> rotates q2 first, then q1.</li>
+</ul>
+<p><strong>c. Rotate a vector by a quaternion</strong></p>
+<div class="sourceCode" id="cb13"><pre
+class="sourceCode js"><code class="sourceCode javascript"><span id="cb13-1"><a href="#cb13-1" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">quatRotateVector</span>(q<span class="op">,</span> v) { <span class="op">...</span> }</span></code></pre></div>
+<ul>
+<li><p>Rotates a 3D vector <code>v</code> using quaternion
+<code>q</code>.</p></li>
+<li><p>Steps:</p>
+<ol type="1">
+<li>Convert vector to quaternion <code>qv = [0, vx, vy, vz]</code>.</li>
+<li>Compute conjugate <code>qConj = [w, -x, -y, -z]</code>.</li>
+<li>Rotate: <code>v' = q * qv * q⁻¹</code>.</li>
+<li>Return new <code>x,y,z</code> of rotated vector.</li>
+</ol></li>
+</ul>
+<hr />
+<h2 id="drawing-functions"><strong>4. Drawing Functions</strong></h2>
+<p><strong>Clear canvas and draw text</strong></p>
+<div class="sourceCode" id="cb14"><pre
+class="sourceCode js"><code class="sourceCode javascript"><span id="cb14-1"><a href="#cb14-1" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">clear</span>() {</span>
+<span id="cb14-2"><a href="#cb14-2" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="at">fillStyle</span> <span class="op">=</span> BACKGROUND<span class="op">;</span></span>
+<span id="cb14-3"><a href="#cb14-3" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="fu">fillRect</span>(<span class="dv">0</span><span class="op">,</span> <span class="dv">0</span><span class="op">,</span> game<span class="op">.</span><span class="at">width</span><span class="op">,</span> game<span class="op">.</span><span class="at">height</span>)<span class="op">;</span></span>
+<span id="cb14-4"><a href="#cb14-4" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="at">fillStyle</span> <span class="op">=</span> TEXT<span class="op">;</span></span>
+<span id="cb14-5"><a href="#cb14-5" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="at">font</span> <span class="op">=</span> <span class="st">&quot;20px serif&quot;</span><span class="op">;</span></span>
+<span id="cb14-6"><a href="#cb14-6" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="fu">fillText</span>(<span class="st">&quot; *click me to change rotation axes&quot;</span><span class="op">,</span> <span class="dv">10</span><span class="op">,</span> game<span class="op">.</span><span class="at">height</span> <span class="op">-</span> <span class="dv">15</span>)<span class="op">;</span></span>
+<span id="cb14-7"><a href="#cb14-7" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="fu">fillText</span>(<span class="st">&quot;current axes: &quot;</span> <span class="op">+</span> activeAxes<span class="op">.</span><span class="fu">join</span>(<span class="st">&quot;,&quot;</span>)<span class="op">,</span> <span class="dv">10</span><span class="op">,</span> <span class="dv">20</span>)<span class="op">;</span></span>
+<span id="cb14-8"><a href="#cb14-8" aria-hidden="true" tabindex="-1"></a>}</span></code></pre></div>
+<ul>
+<li>Clears the canvas with the background color.</li>
+<li>Draws instructions and current axes at the top/bottom.</li>
+</ul>
+<p><strong>Draw a point</strong></p>
+<div class="sourceCode" id="cb15"><pre
+class="sourceCode js"><code class="sourceCode javascript"><span id="cb15-1"><a href="#cb15-1" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">point</span>({x<span class="op">,</span> y}) {</span>
+<span id="cb15-2"><a href="#cb15-2" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> s <span class="op">=</span> <span class="dv">20</span><span class="op">;</span></span>
+<span id="cb15-3"><a href="#cb15-3" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="at">fillStyle</span> <span class="op">=</span> FOREGROUND<span class="op">;</span></span>
+<span id="cb15-4"><a href="#cb15-4" aria-hidden="true" tabindex="-1"></a>    ctx<span class="op">.</span><span class="fu">fillRect</span>(x <span class="op">-</span> s<span class="op">/</span><span class="dv">2</span><span class="op">,</span> y <span class="op">-</span> s<span class="op">/</span><span class="dv">2</span><span class="op">,</span> s<span class="op">,</span> s)<span class="op">;</span></span>
+<span id="cb15-5"><a href="#cb15-5" aria-hidden="true" tabindex="-1"></a>}</span></code></pre></div>
+<ul>
+<li>Draws a small square at <code>(x, y)</code> on the screen.</li>
+<li>Not used in the final version, but useful for debugging
+vertices.</li>
+</ul>
+<p><strong>Draw a line</strong></p>
+<div class="sourceCode" id="cb16"><pre
+class="sourceCode js"><code class="sourceCode javascript"><span id="cb16-1"><a href="#cb16-1" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">line</span>(p1<span class="op">,</span> p2) { <span class="op">...</span> }</span></code></pre></div>
+<ul>
+<li>Draws a thick line from <code>p1</code> to <code>p2</code> in canvas
+coordinates.</li>
+</ul>
+<p><strong>Convert 3D to screen coordinates</strong></p>
+<div class="sourceCode" id="cb17"><pre
+class="sourceCode js"><code class="sourceCode javascript"><span id="cb17-1"><a href="#cb17-1" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">screen</span>(p) {</span>
+<span id="cb17-2"><a href="#cb17-2" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> {</span>
+<span id="cb17-3"><a href="#cb17-3" aria-hidden="true" tabindex="-1"></a>        <span class="dt">x</span><span class="op">:</span> (p<span class="op">.</span><span class="at">x</span> <span class="op">+</span> <span class="dv">1</span>)<span class="op">/</span><span class="dv">2</span> <span class="op">*</span> game<span class="op">.</span><span class="at">width</span><span class="op">,</span></span>
+<span id="cb17-4"><a href="#cb17-4" aria-hidden="true" tabindex="-1"></a>        <span class="dt">y</span><span class="op">:</span> (<span class="dv">1</span> <span class="op">-</span>(p<span class="op">.</span><span class="at">y</span> <span class="op">+</span> <span class="dv">1</span>)<span class="op">/</span><span class="dv">2</span>) <span class="op">*</span> game<span class="op">.</span><span class="at">height</span></span>
+<span id="cb17-5"><a href="#cb17-5" aria-hidden="true" tabindex="-1"></a>    }<span class="op">;</span></span>
+<span id="cb17-6"><a href="#cb17-6" aria-hidden="true" tabindex="-1"></a>}</span></code></pre></div>
+<ul>
+<li>Maps normalized coordinates <code>[-1,1]</code> to screen
+coordinates <code>[0, width/height]</code>.</li>
+<li>Y-axis is flipped because canvas y=0 is top.</li>
+</ul>
+<p><strong>Project 3D to 2D</strong></p>
+<div class="sourceCode" id="cb18"><pre
+class="sourceCode js"><code class="sourceCode javascript"><span id="cb18-1"><a href="#cb18-1" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">project</span>({x<span class="op">,</span> y<span class="op">,</span> z}) {</span>
+<span id="cb18-2"><a href="#cb18-2" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> { <span class="dt">x</span><span class="op">:</span> x<span class="op">/</span>z<span class="op">,</span> <span class="dt">y</span><span class="op">:</span> y<span class="op">/</span>z }<span class="op">;</span></span>
+<span id="cb18-3"><a href="#cb18-3" aria-hidden="true" tabindex="-1"></a>}</span></code></pre></div>
+<ul>
+<li>Simple <strong>perspective projection</strong>: divides x and y by z
+(makes things farther away appear smaller).</li>
+</ul>
+<p><strong>Translate along Z</strong></p>
+<div class="sourceCode" id="cb19"><pre
+class="sourceCode js"><code class="sourceCode javascript"><span id="cb19-1"><a href="#cb19-1" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">translateZ</span>({x<span class="op">,</span>y<span class="op">,</span>z}<span class="op">,</span> dz) {</span>
+<span id="cb19-2"><a href="#cb19-2" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> {x<span class="op">,</span> y<span class="op">,</span> <span class="dt">z</span><span class="op">:</span> z <span class="op">+</span> dz}<span class="op">;</span></span>
+<span id="cb19-3"><a href="#cb19-3" aria-hidden="true" tabindex="-1"></a>}</span></code></pre></div>
+<ul>
+<li>Moves the cube forward/backward so it appears in front of the
+camera.</li>
+</ul>
+<hr />
+<h2 id="animation-loop"><strong>5. Animation Loop</strong></h2>
+<div class="sourceCode" id="cb20"><pre
+class="sourceCode js"><code class="sourceCode javascript"><span id="cb20-1"><a href="#cb20-1" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> FPS <span class="op">=</span> <span class="dv">60</span><span class="op">;</span></span>
+<span id="cb20-2"><a href="#cb20-2" aria-hidden="true" tabindex="-1"></a></span>
+<span id="cb20-3"><a href="#cb20-3" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">frame</span>() {</span>
+<span id="cb20-4"><a href="#cb20-4" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> dt <span class="op">=</span> <span class="dv">1</span><span class="op">/</span>FPS<span class="op">;</span></span>
+<span id="cb20-5"><a href="#cb20-5" aria-hidden="true" tabindex="-1"></a>    angle <span class="op">+=</span> <span class="bu">Math</span><span class="op">.</span><span class="cn">PI</span><span class="op">*</span>dt<span class="op">;</span></span></code></pre></div>
+<ul>
+<li>Updates rotation angle every frame based on FPS.</li>
+</ul>
+<p><strong>Create combined quaternion</strong></p>
+<div class="sourceCode" id="cb21"><pre
+class="sourceCode js"><code class="sourceCode javascript"><span id="cb21-1"><a href="#cb21-1" aria-hidden="true" tabindex="-1"></a>    <span class="kw">let</span> q <span class="op">=</span> [<span class="dv">1</span><span class="op">,</span><span class="dv">0</span><span class="op">,</span><span class="dv">0</span><span class="op">,</span><span class="dv">0</span>]<span class="op">;</span> <span class="co">// identity quaternion</span></span>
+<span id="cb21-2"><a href="#cb21-2" aria-hidden="true" tabindex="-1"></a>    activeAxes<span class="op">.</span><span class="fu">forEach</span>(axis <span class="kw">=&gt;</span> {</span>
+<span id="cb21-3"><a href="#cb21-3" aria-hidden="true" tabindex="-1"></a>        <span class="kw">let</span> qAxis<span class="op">;</span></span>
+<span id="cb21-4"><a href="#cb21-4" aria-hidden="true" tabindex="-1"></a>        <span class="cf">if</span> (axis <span class="op">===</span> <span class="st">&quot;x&quot;</span>) qAxis <span class="op">=</span> <span class="fu">quatFromAxisAngle</span>({<span class="dt">x</span><span class="op">:</span><span class="dv">1</span><span class="op">,</span><span class="dt">y</span><span class="op">:</span><span class="dv">0</span><span class="op">,</span><span class="dt">z</span><span class="op">:</span><span class="dv">0</span>}<span class="op">,</span> angle)<span class="op">;</span></span>
+<span id="cb21-5"><a href="#cb21-5" aria-hidden="true" tabindex="-1"></a>        <span class="cf">if</span> (axis <span class="op">===</span> <span class="st">&quot;y&quot;</span>) qAxis <span class="op">=</span> <span class="fu">quatFromAxisAngle</span>({<span class="dt">x</span><span class="op">:</span><span class="dv">0</span><span class="op">,</span><span class="dt">y</span><span class="op">:</span><span class="dv">1</span><span class="op">,</span><span class="dt">z</span><span class="op">:</span><span class="dv">0</span>}<span class="op">,</span> angle)<span class="op">;</span></span>
+<span id="cb21-6"><a href="#cb21-6" aria-hidden="true" tabindex="-1"></a>        <span class="cf">if</span> (axis <span class="op">===</span> <span class="st">&quot;z&quot;</span>) qAxis <span class="op">=</span> <span class="fu">quatFromAxisAngle</span>({<span class="dt">x</span><span class="op">:</span><span class="dv">0</span><span class="op">,</span><span class="dt">y</span><span class="op">:</span><span class="dv">0</span><span class="op">,</span><span class="dt">z</span><span class="op">:</span><span class="dv">1</span>}<span class="op">,</span> angle)<span class="op">;</span></span>
+<span id="cb21-7"><a href="#cb21-7" aria-hidden="true" tabindex="-1"></a>        q <span class="op">=</span> <span class="fu">quatMultiply</span>(qAxis<span class="op">,</span> q)<span class="op">;</span> <span class="co">// multiply in order</span></span>
+<span id="cb21-8"><a href="#cb21-8" aria-hidden="true" tabindex="-1"></a>    })<span class="op">;</span></span></code></pre></div>
+<ul>
+<li>Starts with <strong>identity quaternion</strong> (no rotation).</li>
+<li>For each active axis, creates a quaternion for rotation around that
+axis.</li>
+<li>Combines them by multiplying — this lets the cube rotate around
+multiple axes <strong>simultaneously</strong>.</li>
+</ul>
+<p><strong>Draw the cube</strong></p>
+<div class="sourceCode" id="cb22"><pre
+class="sourceCode js"><code class="sourceCode javascript"><span id="cb22-1"><a href="#cb22-1" aria-hidden="true" tabindex="-1"></a>    <span class="fu">clear</span>()<span class="op">;</span></span>
+<span id="cb22-2"><a href="#cb22-2" aria-hidden="true" tabindex="-1"></a>    <span class="cf">for</span> (<span class="kw">const</span> f <span class="kw">of</span> fs) {</span>
+<span id="cb22-3"><a href="#cb22-3" aria-hidden="true" tabindex="-1"></a>        <span class="cf">for</span> (<span class="kw">let</span> i <span class="op">=</span> <span class="dv">0</span><span class="op">;</span> i <span class="op">&lt;</span> f<span class="op">.</span><span class="at">length</span><span class="op">;</span> <span class="op">++</span>i) {</span>
+<span id="cb22-4"><a href="#cb22-4" aria-hidden="true" tabindex="-1"></a>            <span class="kw">const</span> a <span class="op">=</span> vs[f[i]]<span class="op">;</span></span>
+<span id="cb22-5"><a href="#cb22-5" aria-hidden="true" tabindex="-1"></a>            <span class="kw">const</span> b <span class="op">=</span> vs[f[(i <span class="op">+</span> <span class="dv">1</span>)<span class="op">%</span>f<span class="op">.</span><span class="at">length</span>]]<span class="op">;</span></span>
+<span id="cb22-6"><a href="#cb22-6" aria-hidden="true" tabindex="-1"></a>            <span class="fu">line</span>(</span>
+<span id="cb22-7"><a href="#cb22-7" aria-hidden="true" tabindex="-1"></a>                <span class="bu">screen</span>(<span class="fu">project</span>(<span class="fu">translateZ</span>(<span class="fu">quatRotateVector</span>(q<span class="op">,</span> a)<span class="op">,</span> dz)))<span class="op">,</span></span>
+<span id="cb22-8"><a href="#cb22-8" aria-hidden="true" tabindex="-1"></a>                <span class="bu">screen</span>(<span class="fu">project</span>(<span class="fu">translateZ</span>(<span class="fu">quatRotateVector</span>(q<span class="op">,</span> b)<span class="op">,</span> dz)))</span>
+<span id="cb22-9"><a href="#cb22-9" aria-hidden="true" tabindex="-1"></a>            )<span class="op">;</span></span>
+<span id="cb22-10"><a href="#cb22-10" aria-hidden="true" tabindex="-1"></a>        }</span>
+<span id="cb22-11"><a href="#cb22-11" aria-hidden="true" tabindex="-1"></a>    }</span></code></pre></div>
+<ul>
+<li>Clears the canvas.</li>
+<li>Loops through each face/edge <code>f</code>.</li>
+<li>Rotates each vertex using the combined quaternion.</li>
+<li>Translates it forward along Z.</li>
+<li>Projects 3D coordinates into 2D screen coordinates.</li>
+<li>Draws lines connecting vertices.</li>
+</ul>
+<p><strong>Schedule next frame</strong></p>
+<div class="sourceCode" id="cb23"><pre
+class="sourceCode js"><code class="sourceCode javascript"><span id="cb23-1"><a href="#cb23-1" aria-hidden="true" tabindex="-1"></a>    <span class="pp">setTimeout</span>(frame<span class="op">,</span> <span class="dv">1000</span><span class="op">/</span>FPS)<span class="op">;</span></span>
+<span id="cb23-2"><a href="#cb23-2" aria-hidden="true" tabindex="-1"></a>}</span>
+<span id="cb23-3"><a href="#cb23-3" aria-hidden="true" tabindex="-1"></a><span class="pp">setTimeout</span>(frame<span class="op">,</span> <span class="dv">1000</span><span class="op">/</span>FPS)<span class="op">;</span></span></code></pre></div>
+<ul>
+<li>Calls <code>frame()</code> every 1/60th of a second to animate.</li>
+</ul>
+<hr />
+<h2 id="handle-clicks"><strong>6. Handle Clicks</strong></h2>
+<div class="sourceCode" id="cb24"><pre
+class="sourceCode js"><code class="sourceCode javascript"><span id="cb24-1"><a href="#cb24-1" aria-hidden="true" tabindex="-1"></a>game<span class="op">.</span><span class="at">onclick</span> <span class="op">=</span> () <span class="kw">=&gt;</span> {</span>
+<span id="cb24-2"><a href="#cb24-2" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> allAxes <span class="op">=</span> [[<span class="st">&quot;x&quot;</span>]<span class="op">,</span> [<span class="st">&quot;y&quot;</span>]<span class="op">,</span> [<span class="st">&quot;z&quot;</span>]<span class="op">,</span> [<span class="st">&quot;x&quot;</span><span class="op">,</span><span class="st">&quot;y&quot;</span>]<span class="op">,</span> [<span class="st">&quot;y&quot;</span><span class="op">,</span><span class="st">&quot;z&quot;</span>]<span class="op">,</span> [<span class="st">&quot;x&quot;</span><span class="op">,</span><span class="st">&quot;z&quot;</span>]<span class="op">,</span> [<span class="st">&quot;x&quot;</span><span class="op">,</span><span class="st">&quot;y&quot;</span><span class="op">,</span><span class="st">&quot;z&quot;</span>]]<span class="op">;</span></span>
+<span id="cb24-3"><a href="#cb24-3" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> index <span class="op">=</span> allAxes<span class="op">.</span><span class="fu">findIndex</span>(a <span class="kw">=&gt;</span> <span class="bu">JSON</span><span class="op">.</span><span class="fu">stringify</span>(a) <span class="op">===</span> <span class="bu">JSON</span><span class="op">.</span><span class="fu">stringify</span>(activeAxes))<span class="op">;</span></span>
+<span id="cb24-4"><a href="#cb24-4" aria-hidden="true" tabindex="-1"></a>    activeAxes <span class="op">=</span> allAxes[(index <span class="op">+</span> <span class="dv">1</span>) <span class="op">%</span> allAxes<span class="op">.</span><span class="at">length</span>]<span class="op">;</span></span>
+<span id="cb24-5"><a href="#cb24-5" aria-hidden="true" tabindex="-1"></a>}<span class="op">;</span></span></code></pre></div>
+<ul>
+<li>On click, cycles through different <strong>axis
+combinations</strong>: x, y, z, xy, yz, xz, xyz.</li>
+<li>Updates <code>activeAxes</code> so the cube rotates differently next
+frame.</li>
+</ul>
+<hr />
+<h3 id="summary">✅ <strong>Summary</strong></h3>
+<ul>
+<li><strong>Vertices + Faces</strong>: Define cube geometry.</li>
+<li><strong>Quaternions</strong>: Handle rotation around multiple axes
+safely.</li>
+<li><strong>Project &amp; Screen Functions</strong>: Convert 3D points
+to 2D canvas.</li>
+<li><strong>Animation Loop</strong>: Updates rotation, applies
+quaternion rotation, and draws cube.</li>
+<li><strong>Click Handler</strong>: Change which axes to rotate
+around.</li>
+</ul>
+<hr />
+<p>If you want, I can also <strong>draw a diagram showing the quaternion
+rotation concept</strong> visually — it really helps to understand how
+it rotates the cube around multiple axes at once.</p>
+<p>Do you want me to make that diagram?</p>
+<figure>
+<img
+src="https://nyquistdev.net/git/?p=basic-3d-example.git;a=blob_plain;f=docs/chatgpt-dump/quaternion-rotation.png;hb=HEAD"
+alt="diagram" />
+<figcaption aria-hidden="true">diagram</figcaption>
+</figure>
+<p>Great question — this is exactly the right point to slow down and
+understand the <strong>math</strong>, because quaternions feel “magical”
+until you see <em>why</em> each line exists.</p>
+<p>I’ll go step by step:</p>
+<hr />
+<h1 id="what-is-a-quaternion-in-graphics">1. What is a quaternion (in
+graphics)?</h1>
+<p>A <strong>quaternion</strong> is a 4-component number:</p>
+<p>[ q = (w, x, y, z)]</p>
+<p>In 3D graphics, we use <strong>unit quaternions</strong> to represent
+<strong>rotations</strong>.</p>
+<p>A rotation by:</p>
+<ul>
+<li>angle ( )</li>
+<li>around a <strong>unit axis</strong> ( = (u_x, u_y, u_z) )</li>
+</ul>
+<p>is encoded as:</p>
+<p>[ ]</p>
+<p>This is the <strong>axis–angle → quaternion</strong> formula.</p>
+<p>Why half-angle? Because quaternion rotation comes from complex
+numbers and Euler’s formula — doubling happens naturally in the
+math.</p>
+<hr />
+<h1 id="quatfromaxisangle">2. <code>quatFromAxisAngle</code></h1>
+<div class="sourceCode" id="cb25"><pre
+class="sourceCode js"><code class="sourceCode javascript"><span id="cb25-1"><a href="#cb25-1" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">quatFromAxisAngle</span>(axis<span class="op">,</span> angle) {</span>
+<span id="cb25-2"><a href="#cb25-2" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> h <span class="op">=</span> angle <span class="op">/</span> <span class="dv">2</span><span class="op">;</span></span>
+<span id="cb25-3"><a href="#cb25-3" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> s <span class="op">=</span> <span class="bu">Math</span><span class="op">.</span><span class="fu">sin</span>(h)<span class="op">;</span></span>
+<span id="cb25-4"><a href="#cb25-4" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> [<span class="bu">Math</span><span class="op">.</span><span class="fu">cos</span>(h)<span class="op">,</span> axis<span class="op">.</span><span class="at">x</span><span class="op">*</span>s<span class="op">,</span> axis<span class="op">.</span><span class="at">y</span><span class="op">*</span>s<span class="op">,</span> axis<span class="op">.</span><span class="at">z</span><span class="op">*</span>s]<span class="op">;</span></span>
+<span id="cb25-5"><a href="#cb25-5" aria-hidden="true" tabindex="-1"></a>}</span></code></pre></div>
+<h3 id="math-behind-it">Math behind it</h3>
+<p>You’re directly implementing:</p>
+<p>[ q = ((/2), (/2))]</p>
+<ul>
+<li><code>h = angle / 2</code> → half-angle</li>
+<li><code>cos(h)</code> → scalar part (w)</li>
+<li><code>axis * sin(h)</code> → vector part ((x,y,z))</li>
+</ul>
+<h3 id="important-rule">Important rule</h3>
+<p><code>axis</code> <strong>must be normalized</strong> (length = 1),
+otherwise rotation will scale or skew.</p>
+<hr />
+<h1 id="quaternion-multiplication-rotation-composition">3. Quaternion
+multiplication (rotation composition)</h1>
+<div class="sourceCode" id="cb26"><pre
+class="sourceCode js"><code class="sourceCode javascript"><span id="cb26-1"><a href="#cb26-1" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">quatMultiply</span>(a<span class="op">,</span> b) {</span>
+<span id="cb26-2"><a href="#cb26-2" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> [w1<span class="op">,</span>x1<span class="op">,</span>y1<span class="op">,</span>z1] <span class="op">=</span> a<span class="op">;</span></span>
+<span id="cb26-3"><a href="#cb26-3" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> [w2<span class="op">,</span>x2<span class="op">,</span>y2<span class="op">,</span>z2] <span class="op">=</span> b<span class="op">;</span></span>
+<span id="cb26-4"><a href="#cb26-4" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> [</span>
+<span id="cb26-5"><a href="#cb26-5" aria-hidden="true" tabindex="-1"></a>        w1<span class="op">*</span>w2 <span class="op">-</span> x1<span class="op">*</span>x2 <span class="op">-</span> y1<span class="op">*</span>y2 <span class="op">-</span> z1<span class="op">*</span>z2<span class="op">,</span></span>
+<span id="cb26-6"><a href="#cb26-6" aria-hidden="true" tabindex="-1"></a>        w1<span class="op">*</span>x2 <span class="op">+</span> x1<span class="op">*</span>w2 <span class="op">+</span> y1<span class="op">*</span>z2 <span class="op">-</span> z1<span class="op">*</span>y2<span class="op">,</span></span>
+<span id="cb26-7"><a href="#cb26-7" aria-hidden="true" tabindex="-1"></a>        w1<span class="op">*</span>y2 <span class="op">-</span> x1<span class="op">*</span>z2 <span class="op">+</span> y1<span class="op">*</span>w2 <span class="op">+</span> z1<span class="op">*</span>x2<span class="op">,</span></span>
+<span id="cb26-8"><a href="#cb26-8" aria-hidden="true" tabindex="-1"></a>        w1<span class="op">*</span>z2 <span class="op">+</span> x1<span class="op">*</span>y2 <span class="op">-</span> y1<span class="op">*</span>x2 <span class="op">+</span> z1<span class="op">*</span>w2</span>
+<span id="cb26-9"><a href="#cb26-9" aria-hidden="true" tabindex="-1"></a>    ]<span class="op">;</span></span>
+<span id="cb26-10"><a href="#cb26-10" aria-hidden="true" tabindex="-1"></a>}</span></code></pre></div>
+<h3 id="math-formula">Math formula</h3>
+<p>Given: [ q_1 = (w_1, _1), q_2 = (w_2, _2)]</p>
+<p>Quaternion multiplication is:</p>
+<p>[ ]</p>
+<p>Expanded component-wise → exactly your code.</p>
+<h3 id="why-this-matters">Why this matters</h3>
+<ul>
+<li>Quaternion multiplication <strong>combines rotations</strong></li>
+<li>Order matters: [ q_{} = q_{} q_{}]</li>
+</ul>
+<p>This avoids <strong>gimbal lock</strong>, unlike Euler angles.</p>
+<hr />
+<h1 id="quaternion-conjugate">4. Quaternion conjugate</h1>
+<div class="sourceCode" id="cb27"><pre
+class="sourceCode js"><code class="sourceCode javascript"><span id="cb27-1"><a href="#cb27-1" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">quatConjugate</span>(q) {</span>
+<span id="cb27-2"><a href="#cb27-2" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> [q[<span class="dv">0</span>]<span class="op">,</span> <span class="op">-</span>q[<span class="dv">1</span>]<span class="op">,</span> <span class="op">-</span>q[<span class="dv">2</span>]<span class="op">,</span> <span class="op">-</span>q[<span class="dv">3</span>]]<span class="op">;</span></span>
+<span id="cb27-3"><a href="#cb27-3" aria-hidden="true" tabindex="-1"></a>}</span></code></pre></div>
+<h3 id="math-meaning">Math meaning</h3>
+<p>For: [ q = (w, x, y, z)]</p>
+<p>The conjugate is:</p>
+<p>[ ]</p>
+<h3 id="why-this-works">Why this works</h3>
+<p>If (q) is a <strong>unit quaternion</strong>:</p>
+<p>[ q^{-1} = q^*]</p>
+<p>So conjugate = <strong>inverse rotation</strong>.</p>
+<p>This is why cameras use conjugates — moving the world opposite to the
+camera rotation.</p>
+<hr />
+<h1 id="rotating-a-vector-using-quaternions">5. Rotating a vector using
+quaternions</h1>
+<div class="sourceCode" id="cb28"><pre
+class="sourceCode js"><code class="sourceCode javascript"><span id="cb28-1"><a href="#cb28-1" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">quatRotateVector</span>(q<span class="op">,</span> v) {</span>
+<span id="cb28-2"><a href="#cb28-2" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> qv <span class="op">=</span> [<span class="dv">0</span><span class="op">,</span> v<span class="op">.</span><span class="at">x</span><span class="op">,</span> v<span class="op">.</span><span class="at">y</span><span class="op">,</span> v<span class="op">.</span><span class="at">z</span>]<span class="op">;</span></span>
+<span id="cb28-3"><a href="#cb28-3" aria-hidden="true" tabindex="-1"></a>    <span class="kw">const</span> r <span class="op">=</span> <span class="fu">quatMultiply</span>(<span class="fu">quatMultiply</span>(q<span class="op">,</span> qv)<span class="op">,</span> <span class="fu">quatConjugate</span>(q))<span class="op">;</span></span>
+<span id="cb28-4"><a href="#cb28-4" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> { <span class="dt">x</span><span class="op">:</span> r[<span class="dv">1</span>]<span class="op">,</span> <span class="dt">y</span><span class="op">:</span> r[<span class="dv">2</span>]<span class="op">,</span> <span class="dt">z</span><span class="op">:</span> r[<span class="dv">3</span>] }<span class="op">;</span></span>
+<span id="cb28-5"><a href="#cb28-5" aria-hidden="true" tabindex="-1"></a>}</span></code></pre></div>
+<h3 id="the-key-formula">The key formula</h3>
+<p>To rotate a vector ( ):</p>
+<ol type="1">
+<li><p>Convert it to a <strong>pure quaternion</strong> [ v_q = (0, v_x,
+v_y, v_z)]</p></li>
+<li><p>Apply: [ ]</p></li>
+<li><p>Extract the vector part</p></li>
+</ol>
+<h3 id="why-this-works-1">Why this works</h3>
+<p>Quaternion rotation encodes:</p>
+<ul>
+<li>axis</li>
+<li>angle</li>
+<li>orientation</li>
+</ul>
+<p>The sandwich product rotates without:</p>
+<ul>
+<li>scaling</li>
+<li>skewing</li>
+<li>drifting</li>
+</ul>
+<hr />
+<h1 id="why-quaternions-are-better-than-matrices-for-rotation">6. Why
+quaternions are better than matrices (for rotation)</h1>
+<table>
+<thead>
+<tr class="header">
+<th>Problem</th>
+<th>Euler Angles</th>
+<th>Matrices</th>
+<th>Quaternions</th>
+</tr>
+</thead>
+<tbody>
+<tr class="odd">
+<td>Gimbal lock</td>
+<td>❌</td>
+<td>❌</td>
+<td>✅</td>
+</tr>
+<tr class="even">
+<td>Smooth interpolation</td>
+<td>❌</td>
+<td>⚠️</td>
+<td>✅ (SLERP)</td>
+</tr>
+<tr class="odd">
+<td>Compact</td>
+<td>✅</td>
+<td>❌</td>
+<td>✅</td>
+</tr>
+<tr class="even">
+<td>Numerical stability</td>
+<td>⚠️</td>
+<td>⚠️</td>
+<td>✅</td>
+</tr>
+</tbody>
+</table>
+<hr />
+<h1 id="visual-intuition">7. Visual intuition</h1>
+<p>A rotation quaternion represents:</p>
+<p>[ q = (/2) + (/2)(u_x i + u_y j + u_z k)]</p>
+<p>This is the 3D analog of:</p>
+<p>[ e^{i} = + i]</p>
+<hr />
+<h1 id="best-learning-resources-highly-recommended">8. Best learning
+resources (highly recommended)</h1>
+<h3 id="beginner-friendly">Beginner-friendly</h3>
+<ul>
+<li><p><strong>3Blue1Brown – Quaternions</strong></p>
+<ul>
+<li><a
+href="https://www.youtube.com/watch?v=zjMuIxRvygQ">https://www.youtube.com/watch?v=zjMuIxRvygQ</a></li>
+</ul></li>
+<li><p><strong>Visualizing Quaternions</strong></p>
+<ul>
+<li><a
+href="https://eater.net/quaternions">https://eater.net/quaternions</a></li>
+</ul></li>
+</ul>
+<h3 id="graphics-oriented">Graphics-oriented</h3>
+<ul>
+<li><p><strong>LearnOpenGL – Camera &amp; Quaternions</strong></p>
+<ul>
+<li><a
+href="https://learnopengl.com/Getting-started/Camera">https://learnopengl.com/Getting-started/Camera</a></li>
+</ul></li>
+<li><p><strong>Scratchapixel – Quaternions</strong></p>
+<ul>
+<li><a
+href="https://www.scratchapixel.com/lessons/mathematics-physics-for-computer-graphics/quaternions">https://www.scratchapixel.com/lessons/mathematics-physics-for-computer-graphics/quaternions</a></li>
+</ul></li>
+</ul>
+<h3 id="math-reference">Math reference</h3>
+<ul>
+<li><p>Wikipedia (surprisingly solid)</p>
+<ul>
+<li><a
+href="https://en.wikipedia.org/wiki/Quaternion">https://en.wikipedia.org/wiki/Quaternion</a></li>
+<li><a
+href="https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation">https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation</a></li>
+</ul></li>
+</ul>
+<hr />
+<h1 id="mental-model-to-keep">9. Mental model to keep</h1>
+<blockquote>
+<p><strong>Quaternions don’t rotate vectors directly. They rotate
+<em>space itself</em> around the vector.</strong></p>
+</blockquote>
+<p>Once that clicks, camera math suddenly makes sense.</p>
+<hr />
+<p>If you want next:</p>
+<ul>
+<li>🔄 quaternion → rotation matrix</li>
+<li>🎥 why camera uses inverse rotation</li>
+<li>🎯 SLERP (smooth rotation interpolation)</li>
+<li>🧠 geometric intuition with diagrams</li>
+</ul>
+<p>Just tell me 👍</p>
+</body>
+</html>
diff --git a/docs/chatgpt-dump/quaternions.md b/docs/chatgpt-dump/quaternions.md
new file mode 100644 (file)
index 0000000..c406140
--- /dev/null
@@ -0,0 +1,982 @@
+Absolutely! I can show you how to incorporate **quaternions** into your code to
+allow smooth rotation around **multiple axes at once**, avoiding the limitations
+of your current `rotate` function.
+
+Let’s go step by step.
+
+---
+
+### 1. **Why Quaternions Here**
+
+Currently, you rotate around a single axis per frame:
+
+```js
+rotate(a, rotationAxis, angle)
+```
+
+With quaternions, you can **combine rotations around multiple axes** seamlessly. For example, you could rotate simultaneously around X **and** Y.
+
+---
+
+### 2. **Quaternion Basics**
+
+A quaternion can be represented as:
+
+[
+q = w + xi + yj + zk
+]
+
+For 3D rotation:
+
+```js
+q = [w, x, y, z]
+```
+
+* `w` = cos(angle/2)
+* `x, y, z` = axis vector * sin(angle/2)
+
+To rotate a point `v`:
+
+1. Convert `v` to a quaternion `[0, vx, vy, vz]`.
+2. Apply `v' = q * v * q⁻¹`.
+
+---
+
+### 3. **Quaternion Utilities in JS**
+
+```js
+function quatFromAxisAngle(axis, angle) {
+    const half = angle / 2
+    const s = Math.sin(half)
+    return [
+        Math.cos(half),   // w
+        axis.x * s,       // x
+        axis.y * s,       // y
+        axis.z * s        // z
+    ]
+}
+
+function quatMultiply(q1, q2) {
+    const [w1,x1,y1,z1] = q1
+    const [w2,x2,y2,z2] = q2
+    return [
+        w1*w2 - x1*x2 - y1*y2 - z1*z2,
+        w1*x2 + x1*w2 + y1*z2 - z1*y2,
+        w1*y2 - x1*z2 + y1*w2 + z1*x2,
+        w1*z2 + x1*y2 - y1*x2 + z1*w2
+    ]
+}
+
+function quatRotateVector(q, v) {
+    const [w,x,y,z] = q
+    const [vx,vy,vz] = [v.x,v.y,v.z]
+
+    // Quaternion multiplication: v' = q * v_quat * q⁻¹
+    const qv = [0, vx, vy, vz]
+    const qConj = [w, -x, -y, -z]
+
+    const temp = quatMultiply(q, qv)
+    const result = quatMultiply(temp, qConj)
+    return { x: result[1], y: result[2], z: result[3] }
+}
+```
+
+---
+
+### 4. **Using Quaternions in Your Frame**
+
+Instead of your `rotate` function:
+
+```js
+const angleX = angle; 
+const angleY = angle/2; // different speed for demo
+
+const qx = quatFromAxisAngle({x:1,y:0,z:0}, angleX)
+const qy = quatFromAxisAngle({x:0,y:1,z:0}, angleY)
+
+const q = quatMultiply(qy, qx) // combined rotation
+
+for (const f of fs) {
+    for (let i = 0; i < f.length; ++i) {
+        const a = vs[f[i]]
+        const b = vs[f[(i + 1)%f.length]]
+
+        line(
+            screen(project(translateZ(quatRotateVector(q, a), dz))),
+            screen(project(translateZ(quatRotateVector(q, b), dz)))
+        )
+    }
+}
+```
+
+* Now the cube rotates around **both X and Y axes simultaneously**.
+* You can extend this to any combination of axes by multiplying more quaternions.
+
+---
+
+### 5. **Optional: Change Axes Dynamically**
+
+You could replace your `rotationAxis` system by letting the user click to toggle **which axes are active**, then generate quaternions for each active axis and multiply them together.
+
+---
+
+If you want, I can rewrite your **entire code snippet using quaternions**, so the cube rotates smoothly around multiple axes and keeps your click-to-change-axis feature.
+
+Do you want me to do that?
+
+Absolutely! Let’s go through your **quaternion-based 3D cube code** line by line. I’ll break it down into sections so it’s easier to digest.
+
+---
+
+## **1. Canvas Setup**
+
+```js
+const game = document.getElementById("game");
+game.width = 400;
+game.height = 400;
+```
+
+* Gets the `<canvas>` element with ID `game`.
+* Sets the canvas dimensions to 400×400 pixels.
+
+```js
+const BACKGROUND = "#101010";
+const FOREGROUND = "#50FF50";
+const TEXT = "#FFFFFF";
+```
+
+* Defines colors for background, cube lines, and text.
+
+```js
+const ctx = game.getContext("2d");
+```
+
+* Gets the 2D drawing context so we can draw shapes on the canvas.
+
+---
+
+## **2. Cube Geometry**
+
+```js
+const vs = [ ... ];
+```
+
+* `vs` = **vertices** of a cube (8 points).
+* Each vertex has `x, y, z` coordinates in 3D space (values between -0.25 and 0.25).
+
+```js
+const fs = [ ... ];
+```
+
+* `fs` = **faces/edges** to connect vertices.
+* Some are quads `[0,1,2,3]`, some are edges `[0,4]`.
+* This tells the code **which points to draw lines between**.
+
+```js
+let dz = 1;
+let angle = 0;
+let activeAxes = ["x"];
+```
+
+* `dz`: distance to move the cube along Z (so it’s in front of the camera).
+* `angle`: current rotation angle.
+* `activeAxes`: which axes to rotate around (initially just `x`).
+
+---
+
+## **3. Quaternion Functions**
+
+**a. Create a quaternion from an axis + angle**
+
+```js
+function quatFromAxisAngle(axis, angle) {
+    const half = angle / 2;
+    const s = Math.sin(half);
+    return [
+        Math.cos(half),   // w
+        axis.x * s,       // x
+        axis.y * s,       // y
+        axis.z * s        // z
+    ];
+}
+```
+
+* Converts **axis-angle rotation** into a quaternion `[w, x, y, z]`.
+* `half` = half the rotation angle.
+* `s = sin(half)` scales the axis.
+* `w` = cos(half).
+
+**b. Multiply two quaternions**
+
+```js
+function quatMultiply(q1, q2) { ... }
+```
+
+* Standard quaternion multiplication formula.
+* Combines two rotations into one. **Order matters!** `q1*q2` rotates q2 first, then q1.
+
+**c. Rotate a vector by a quaternion**
+
+```js
+function quatRotateVector(q, v) { ... }
+```
+
+* Rotates a 3D vector `v` using quaternion `q`.
+* Steps:
+
+  1. Convert vector to quaternion `qv = [0, vx, vy, vz]`.
+  2. Compute conjugate `qConj = [w, -x, -y, -z]`.
+  3. Rotate: `v' = q * qv * q⁻¹`.
+  4. Return new `x,y,z` of rotated vector.
+
+---
+
+## **4. Drawing Functions**
+
+**Clear canvas and draw text**
+
+```js
+function clear() {
+    ctx.fillStyle = BACKGROUND;
+    ctx.fillRect(0, 0, game.width, game.height);
+    ctx.fillStyle = TEXT;
+    ctx.font = "20px serif";
+    ctx.fillText(" *click me to change rotation axes", 10, game.height - 15);
+    ctx.fillText("current axes: " + activeAxes.join(","), 10, 20);
+}
+```
+
+* Clears the canvas with the background color.
+* Draws instructions and current axes at the top/bottom.
+
+**Draw a point**
+
+```js
+function point({x, y}) {
+    const s = 20;
+    ctx.fillStyle = FOREGROUND;
+    ctx.fillRect(x - s/2, y - s/2, s, s);
+}
+```
+
+* Draws a small square at `(x, y)` on the screen.
+* Not used in the final version, but useful for debugging vertices.
+
+**Draw a line**
+
+```js
+function line(p1, p2) { ... }
+```
+
+* Draws a thick line from `p1` to `p2` in canvas coordinates.
+
+**Convert 3D to screen coordinates**
+
+```js
+function screen(p) {
+    return {
+        x: (p.x + 1)/2 * game.width,
+        y: (1 -(p.y + 1)/2) * game.height
+    };
+}
+```
+
+* Maps normalized coordinates `[-1,1]` to screen coordinates `[0, width/height]`.
+* Y-axis is flipped because canvas y=0 is top.
+
+**Project 3D to 2D**
+
+```js
+function project({x, y, z}) {
+    return { x: x/z, y: y/z };
+}
+```
+
+* Simple **perspective projection**: divides x and y by z (makes things farther away appear smaller).
+
+**Translate along Z**
+
+```js
+function translateZ({x,y,z}, dz) {
+    return {x, y, z: z + dz};
+}
+```
+
+* Moves the cube forward/backward so it appears in front of the camera.
+
+---
+
+## **5. Animation Loop**
+
+```js
+const FPS = 60;
+
+function frame() {
+    const dt = 1/FPS;
+    angle += Math.PI*dt;
+```
+
+* Updates rotation angle every frame based on FPS.
+
+**Create combined quaternion**
+
+```js
+    let q = [1,0,0,0]; // identity quaternion
+    activeAxes.forEach(axis => {
+        let qAxis;
+        if (axis === "x") qAxis = quatFromAxisAngle({x:1,y:0,z:0}, angle);
+        if (axis === "y") qAxis = quatFromAxisAngle({x:0,y:1,z:0}, angle);
+        if (axis === "z") qAxis = quatFromAxisAngle({x:0,y:0,z:1}, angle);
+        q = quatMultiply(qAxis, q); // multiply in order
+    });
+```
+
+* Starts with **identity quaternion** (no rotation).
+* For each active axis, creates a quaternion for rotation around that axis.
+* Combines them by multiplying — this lets the cube rotate around multiple axes **simultaneously**.
+
+**Draw the cube**
+
+```js
+    clear();
+    for (const f of fs) {
+        for (let i = 0; i < f.length; ++i) {
+            const a = vs[f[i]];
+            const b = vs[f[(i + 1)%f.length]];
+            line(
+                screen(project(translateZ(quatRotateVector(q, a), dz))),
+                screen(project(translateZ(quatRotateVector(q, b), dz)))
+            );
+        }
+    }
+```
+
+* Clears the canvas.
+* Loops through each face/edge `f`.
+* Rotates each vertex using the combined quaternion.
+* Translates it forward along Z.
+* Projects 3D coordinates into 2D screen coordinates.
+* Draws lines connecting vertices.
+
+**Schedule next frame**
+
+```js
+    setTimeout(frame, 1000/FPS);
+}
+setTimeout(frame, 1000/FPS);
+```
+
+* Calls `frame()` every 1/60th of a second to animate.
+
+---
+
+## **6. Handle Clicks**
+
+```js
+game.onclick = () => {
+    const allAxes = [["x"], ["y"], ["z"], ["x","y"], ["y","z"], ["x","z"], ["x","y","z"]];
+    const index = allAxes.findIndex(a => JSON.stringify(a) === JSON.stringify(activeAxes));
+    activeAxes = allAxes[(index + 1) % allAxes.length];
+};
+```
+
+* On click, cycles through different **axis combinations**: x, y, z, xy, yz, xz, xyz.
+* Updates `activeAxes` so the cube rotates differently next frame.
+
+---
+
+### ✅ **Summary**
+
+* **Vertices + Faces**: Define cube geometry.
+* **Quaternions**: Handle rotation around multiple axes safely.
+* **Project & Screen Functions**: Convert 3D points to 2D canvas.
+* **Animation Loop**: Updates rotation, applies quaternion rotation, and draws cube.
+* **Click Handler**: Change which axes to rotate around.
+
+---
+
+If you want, I can also **draw a diagram showing the quaternion rotation concept** visually — it really helps to understand how it rotates the cube around multiple axes at once.
+
+Do you want me to make that diagram?
+
+![diagram](https://nyquistdev.net/git/?p=basic-3d-example.git;a=blob_plain;f=docs/chatgpt-dump/quaternion-rotation.png;hb=HEAD)
+
+Great question — this is exactly the right point to slow down and understand the **math**, because quaternions feel “magical” until you see *why* each line exists.
+
+I’ll go step by step:
+
+---
+
+# 1. What is a quaternion (in graphics)?
+
+A **quaternion** is a 4-component number:
+
+[
+q = (w, x, y, z)
+]
+
+In 3D graphics, we use **unit quaternions** to represent **rotations**.
+
+A rotation by:
+
+* angle ( \theta )
+* around a **unit axis** ( \vec{u} = (u_x, u_y, u_z) )
+
+is encoded as:
+
+[
+\boxed{
+q = \left(
+\cos\frac{\theta}{2},
+; u_x \sin\frac{\theta}{2},
+; u_y \sin\frac{\theta}{2},
+; u_z \sin\frac{\theta}{2}
+\right)
+}
+]
+
+This is the **axis–angle → quaternion** formula.
+
+Why half-angle?
+Because quaternion rotation comes from complex numbers and Euler’s formula — doubling happens naturally in the math.
+
+---
+
+# 2. `quatFromAxisAngle`
+
+```js
+function quatFromAxisAngle(axis, angle) {
+    const h = angle / 2;
+    const s = Math.sin(h);
+    return [Math.cos(h), axis.x*s, axis.y*s, axis.z*s];
+}
+```
+
+### Math behind it
+
+You’re directly implementing:
+
+[
+q = (\cos(\theta/2), \vec{u}\sin(\theta/2))
+]
+
+* `h = angle / 2` → half-angle
+* `cos(h)` → scalar part (w)
+* `axis * sin(h)` → vector part ((x,y,z))
+
+### Important rule
+
+`axis` **must be normalized** (length = 1), otherwise rotation will scale or skew.
+
+---
+
+# 3. Quaternion multiplication (rotation composition)
+
+```js
+function quatMultiply(a, b) {
+    const [w1,x1,y1,z1] = a;
+    const [w2,x2,y2,z2] = b;
+    return [
+        w1*w2 - x1*x2 - y1*y2 - z1*z2,
+        w1*x2 + x1*w2 + y1*z2 - z1*y2,
+        w1*y2 - x1*z2 + y1*w2 + z1*x2,
+        w1*z2 + x1*y2 - y1*x2 + z1*w2
+    ];
+}
+```
+
+### Math formula
+
+Given:
+[
+q_1 = (w_1, \vec{v}_1), \quad q_2 = (w_2, \vec{v}_2)
+]
+
+Quaternion multiplication is:
+
+[
+\boxed{
+q_1 q_2 =
+\left(
+w_1 w_2 - \vec{v}_1 \cdot \vec{v}_2,;
+w_1 \vec{v}_2 + w_2 \vec{v}_1 + \vec{v}_1 \times \vec{v}_2
+\right)
+}
+]
+
+Expanded component-wise → exactly your code.
+
+### Why this matters
+
+* Quaternion multiplication **combines rotations**
+* Order matters:
+  [
+  q_{\text{final}} = q_{\text{new}} \cdot q_{\text{current}}
+  ]
+
+This avoids **gimbal lock**, unlike Euler angles.
+
+---
+
+# 4. Quaternion conjugate
+
+```js
+function quatConjugate(q) {
+    return [q[0], -q[1], -q[2], -q[3]];
+}
+```
+
+### Math meaning
+
+For:
+[
+q = (w, x, y, z)
+]
+
+The conjugate is:
+
+[
+\boxed{
+q^* = (w, -x, -y, -z)
+}
+]
+
+### Why this works
+
+If (q) is a **unit quaternion**:
+
+[
+q^{-1} = q^*
+]
+
+So conjugate = **inverse rotation**.
+
+This is why cameras use conjugates — moving the world opposite to the camera rotation.
+
+---
+
+# 5. Rotating a vector using quaternions
+
+```js
+function quatRotateVector(q, v) {
+    const qv = [0, v.x, v.y, v.z];
+    const r = quatMultiply(quatMultiply(q, qv), quatConjugate(q));
+    return { x: r[1], y: r[2], z: r[3] };
+}
+```
+
+### The key formula
+
+To rotate a vector ( \vec{v} ):
+
+1. Convert it to a **pure quaternion**
+   [
+   v_q = (0, v_x, v_y, v_z)
+   ]
+
+2. Apply:
+   [
+   \boxed{
+   v' = q ; v_q ; q^{-1}
+   }
+   ]
+
+3. Extract the vector part
+
+### Why this works
+
+Quaternion rotation encodes:
+
+* axis
+* angle
+* orientation
+
+The sandwich product rotates without:
+
+* scaling
+* skewing
+* drifting
+
+---
+
+# 6. Why quaternions are better than matrices (for rotation)
+
+| Problem              | Euler Angles | Matrices | Quaternions |
+| -------------------- | ------------ | -------- | ----------- |
+| Gimbal lock          | ❌            | ❌        | ✅           |
+| Smooth interpolation | ❌            | ⚠️       | ✅ (SLERP)   |
+| Compact              | ✅            | ❌        | ✅           |
+| Numerical stability  | ⚠️           | ⚠️       | ✅           |
+
+---
+
+# 7. Visual intuition
+
+A rotation quaternion represents:
+
+[
+q = \cos(\theta/2) + \sin(\theta/2)(u_x i + u_y j + u_z k)
+]
+
+This is the 3D analog of:
+
+[
+e^{i\theta} = \cos\theta + i\sin\theta
+]
+
+---
+
+# 8. Best learning resources (highly recommended)
+
+### Beginner-friendly
+
+* **3Blue1Brown – Quaternions**
+
+  * [https://www.youtube.com/watch?v=zjMuIxRvygQ](https://www.youtube.com/watch?v=zjMuIxRvygQ)
+* **Visualizing Quaternions**
+
+  * [https://eater.net/quaternions](https://eater.net/quaternions)
+
+### Graphics-oriented
+
+* **LearnOpenGL – Camera & Quaternions**
+
+  * [https://learnopengl.com/Getting-started/Camera](https://learnopengl.com/Getting-started/Camera)
+* **Scratchapixel – Quaternions**
+
+  * [https://www.scratchapixel.com/lessons/mathematics-physics-for-computer-graphics/quaternions](https://www.scratchapixel.com/lessons/mathematics-physics-for-computer-graphics/quaternions)
+
+### Math reference
+
+* Wikipedia (surprisingly solid)
+
+  * [https://en.wikipedia.org/wiki/Quaternion](https://en.wikipedia.org/wiki/Quaternion)
+  * [https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation](https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation)
+
+---
+
+# 9. Mental model to keep
+
+> **Quaternions don’t rotate vectors directly.
+> They rotate *space itself* around the vector.**
+
+Once that clicks, camera math suddenly makes sense.
+
+---
+
+If you want next:
+
+* 🔄 quaternion → rotation matrix
+* 🎥 why camera uses inverse rotation
+* 🎯 SLERP (smooth rotation interpolation)
+* 🧠 geometric intuition with diagrams
+
+Just tell me 👍
+
+Absolutely — let’s unpack those sentences slowly and connect the symbols to intuition.
+
+---
+
+## 1. “A quaternion is an expression of the form”
+
+[
+a + b,\mathbf{i} + c,\mathbf{j} + d,\mathbf{k}
+]
+
+This means that **every quaternion can be written as a sum of four parts**:
+
+* one **real (scalar)** part
+* three **imaginary (vector-like)** parts
+
+So a quaternion has **four real numbers** in it, not just two like a complex number.
+
+---
+
+## 2. The coefficients (a, b, c, d)
+
+> “where (a, b, c, d) are real numbers”
+
+* These are just ordinary real numbers (like (2), (-1.3), (\pi), etc.).
+* They tell you **how much** of each basis element the quaternion contains.
+
+Think of them as coordinates.
+
+---
+
+## 3. The symbols (\mathbf{i}, \mathbf{j}, \mathbf{k})
+
+> “and (\mathbf{i}, \mathbf{j}, \mathbf{k}) are symbols that can be interpreted as unit-vectors pointing along the three spatial axes.”
+
+These symbols are **not numbers** in the usual sense. They are **basis elements** that behave in special ways.
+
+### Interpretation:
+
+* (\mathbf{i}) → x-axis direction
+* (\mathbf{j}) → y-axis direction
+* (\mathbf{k}) → z-axis direction
+
+So:
+[
+b,\mathbf{i} + c,\mathbf{j} + d,\mathbf{k}
+]
+looks exactly like a 3D vector:
+[
+(b, c, d)
+]
+
+---
+
+## 4. Scalar part vs vector part
+
+The quaternion naturally splits into two pieces:
+
+[
+\underbrace{a}*{\text{scalar part}}
+\quad + \quad
+\underbrace{(b,\mathbf{i} + c,\mathbf{j} + d,\mathbf{k})}*{\text{vector part}}
+]
+
+This matches the earlier notation:
+[
+\mathbf{q} = (r, \vec{v})
+]
+where:
+
+* (r = a)
+* (\vec{v} = (b, c, d))
+
+---
+
+## 5. How this extends complex numbers
+
+Complex numbers look like:
+[
+a + b,\mathbf{i}
+]
+
+Quaternions **extend** this idea:
+
+* one real axis
+* three imaginary axes
+
+So instead of a 2D number system, quaternions live in **4D**.
+
+---
+
+## 6. Important difference from vectors
+
+Although (\mathbf{i}, \mathbf{j}, \mathbf{k}) *look* like unit vectors, they are actually **algebraic objects with multiplication rules**:
+
+[
+\mathbf{i}^2 = \mathbf{j}^2 = \mathbf{k}^2 = \mathbf{i}\mathbf{j}\mathbf{k} = -1
+]
+
+and multiplication is **not commutative**:
+[
+\mathbf{i}\mathbf{j} = \mathbf{k}, \quad
+\mathbf{j}\mathbf{i} = -\mathbf{k}
+]
+
+This is why quaternions are so powerful for rotations.
+
+---
+
+## 7. Geometric intuition
+
+You can think of a quaternion as:
+
+* a **real number** (a), plus
+* a **3D direction and magnitude** ((b, c, d))
+
+When used for rotations:
+
+* (a) relates to the rotation angle
+* ((b, c, d)) relates to the rotation axis
+
+---
+
+## 8. Plain-English summary
+
+> A quaternion is built from one real number and three imaginary components. The imaginary parts behave like directions in 3D space and follow special multiplication rules. Together, they form a 4-dimensional number system that can represent 3D rotations cleanly and efficiently.
+
+If you’d like, I can next:
+
+* show how quaternion multiplication works geometrically
+* explain why quaternions avoid gimbal lock
+* relate quaternions directly to rotation matrices
+
+Great question — this is where quaternions really become *geometric*, not just algebraic.
+
+I’ll build this up step by step, from intuition to the final geometric meaning.
+
+---
+
+## 1. Unit quaternions represent rotations
+
+A **unit quaternion** (length = 1) represents a 3D rotation.
+
+It is written as:
+[
+q = \cos\frac{\theta}{2} + \hat{u},\sin\frac{\theta}{2}
+]
+where:
+
+* (\theta) = rotation angle
+* (\hat{u} = u_x\mathbf{i} + u_y\mathbf{j} + u_z\mathbf{k}) is a **unit vector** (rotation axis)
+
+This already encodes geometry:
+
+* **axis** → direction of the vector part
+* **angle** → stored in the sine/cosine terms
+
+---
+
+## 2. Rotating a vector using a quaternion
+
+A 3D vector (\vec{v}) is treated as a **pure quaternion**:
+[
+\vec{v} ;\longrightarrow; 0 + v_x\mathbf{i} + v_y\mathbf{j} + v_z\mathbf{k}
+]
+
+The rotated vector is:
+[
+\vec{v},' = q,\vec{v},q^{-1}
+]
+
+This is not arbitrary — this *is* the geometric action of rotation.
+
+---
+
+## 3. What quaternion multiplication does geometrically
+
+### Key idea:
+
+> **Quaternion multiplication composes rotations.**
+
+If:
+
+* (q_1) rotates by (\theta_1) around axis (\hat{u}_1)
+* (q_2) rotates by (\theta_2) around axis (\hat{u}_2)
+
+Then:
+[
+q = q_2 q_1
+]
+represents:
+
+> “First rotate by (q_1), then rotate by (q_2)”
+
+This is exactly how matrix multiplication works for rotations.
+
+---
+
+## 4. Why order matters (non-commutativity)
+
+Quaternion multiplication is **not commutative**:
+[
+q_1 q_2 \neq q_2 q_1
+]
+
+Geometrically:
+
+* Rotating around **x then y** is not the same as **y then x**
+
+This matches real-world 3D rotations.
+
+---
+
+## 5. Vector–scalar decomposition of multiplication
+
+Let:
+[
+q_1 = (r_1, \vec{v}_1), \quad
+q_2 = (r_2, \vec{v}_2)
+]
+
+Their product is:
+[
+q_2 q_1 =
+\big(
+r_2 r_1 - \vec{v}_2 \cdot \vec{v}_1,;
+r_2 \vec{v}_1 + r_1 \vec{v}_2 + \vec{v}_2 \times \vec{v}_1
+\big)
+]
+
+### Geometric meaning of each term:
+
+* **Dot product** → angle alignment
+* **Cross product** → axis perpendicular to both rotations
+* **Linear terms** → mixing scalar and vector parts
+
+So quaternion multiplication naturally combines:
+
+* angle information (dot)
+* axis orientation (cross)
+
+---
+
+## 6. Visual intuition
+
+Think of a unit quaternion as:
+
+* a point on a **4D unit sphere**
+* encoding a rotation in 3D space
+
+Multiplying two unit quaternions:
+
+* moves you along that sphere
+* in a way that corresponds exactly to doing one rotation after another
+
+---
+
+## 7. Why the half-angle appears
+
+You might wonder:
+
+> Why (\theta/2), not (\theta)?
+
+Geometrically:
+
+* Rotations form a curved space (SO(3))
+* Unit quaternions live in a simpler space (SU(2)) that *double-covers* rotations
+* The half-angle makes multiplication correspond cleanly to rotation composition
+
+That’s why:
+[
+q \text{ and } -q
+]
+represent the **same rotation**.
+
+---
+
+## 8. Simple concrete example
+
+Rotate 90° about the z-axis:
+[
+q = \cos 45^\circ + \mathbf{k}\sin 45^\circ
+]
+
+Apply it to (\mathbf{i}) (the x-axis):
+[
+\mathbf{i}' = q,\mathbf{i},q^{-1} = \mathbf{j}
+]
+
+Geometrically:
+
+* x-axis rotates into y-axis
+* exactly what you expect
+
+---
+
+## 9. Plain-English summary
+
+> Quaternion multiplication is not “multiplying numbers.”
+> It is **combining rotations in 3D space**.
+> The algebra mirrors the geometry: dot products handle angles, cross products handle axes, and order encodes rotation order.
+
+If you want, next I can:
+
+* derive the rotation formula step-by-step
+* show the link to rotation matrices
+* explain the double-cover and why (q) and (-q) are the same rotation
+
diff --git a/docs/chatgpt-dump/rendered.html b/docs/chatgpt-dump/rendered.html
new file mode 100644 (file)
index 0000000..a9595b6
--- /dev/null
@@ -0,0 +1,468 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta http-equiv="X-UA-Compatible" content="IE=edge">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<meta name="generator" content="Asciidoctor 2.0.20">
+<title>Untitled</title>
+<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400,700">
+<style>
+/*! Asciidoctor default stylesheet | MIT License | https://asciidoctor.org */
+/* Uncomment the following line when using as a custom stylesheet */
+/* @import "https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400,700"; */
+html{font-family:sans-serif;-webkit-text-size-adjust:100%}
+a{background:none}
+a:focus{outline:thin dotted}
+a:active,a:hover{outline:0}
+h1{font-size:2em;margin:.67em 0}
+b,strong{font-weight:bold}
+abbr{font-size:.9em}
+abbr[title]{cursor:help;border-bottom:1px dotted #dddddf;text-decoration:none}
+dfn{font-style:italic}
+hr{height:0}
+mark{background:#ff0;color:#000}
+code,kbd,pre,samp{font-family:monospace;font-size:1em}
+pre{white-space:pre-wrap}
+q{quotes:"\201C" "\201D" "\2018" "\2019"}
+small{font-size:80%}
+sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
+sup{top:-.5em}
+sub{bottom:-.25em}
+img{border:0}
+svg:not(:root){overflow:hidden}
+figure{margin:0}
+audio,video{display:inline-block}
+audio:not([controls]){display:none;height:0}
+fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}
+legend{border:0;padding:0}
+button,input,select,textarea{font-family:inherit;font-size:100%;margin:0}
+button,input{line-height:normal}
+button,select{text-transform:none}
+button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}
+button[disabled],html input[disabled]{cursor:default}
+input[type=checkbox],input[type=radio]{padding:0}
+button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}
+textarea{overflow:auto;vertical-align:top}
+table{border-collapse:collapse;border-spacing:0}
+*,::before,::after{box-sizing:border-box}
+html,body{font-size:100%}
+body{background:#fff;color:rgba(0,0,0,.8);padding:0;margin:0;font-family:"Noto Serif","DejaVu Serif",serif;line-height:1;position:relative;cursor:auto;-moz-tab-size:4;-o-tab-size:4;tab-size:4;word-wrap:anywhere;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased}
+a:hover{cursor:pointer}
+img,object,embed{max-width:100%;height:auto}
+object,embed{height:100%}
+img{-ms-interpolation-mode:bicubic}
+.left{float:left!important}
+.right{float:right!important}
+.text-left{text-align:left!important}
+.text-right{text-align:right!important}
+.text-center{text-align:center!important}
+.text-justify{text-align:justify!important}
+.hide{display:none}
+img,object,svg{display:inline-block;vertical-align:middle}
+textarea{height:auto;min-height:50px}
+select{width:100%}
+.subheader,.admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{line-height:1.45;color:#7a2518;font-weight:400;margin-top:0;margin-bottom:.25em}
+div,dl,dt,dd,ul,ol,li,h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6,pre,form,p,blockquote,th,td{margin:0;padding:0}
+a{color:#2156a5;text-decoration:underline;line-height:inherit}
+a:hover,a:focus{color:#1d4b8f}
+a img{border:0}
+p{line-height:1.6;margin-bottom:1.25em;text-rendering:optimizeLegibility}
+p aside{font-size:.875em;line-height:1.35;font-style:italic}
+h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{font-family:"Open Sans","DejaVu Sans",sans-serif;font-weight:300;font-style:normal;color:#ba3925;text-rendering:optimizeLegibility;margin-top:1em;margin-bottom:.5em;line-height:1.0125em}
+h1 small,h2 small,h3 small,#toctitle small,.sidebarblock>.content>.title small,h4 small,h5 small,h6 small{font-size:60%;color:#e99b8f;line-height:0}
+h1{font-size:2.125em}
+h2{font-size:1.6875em}
+h3,#toctitle,.sidebarblock>.content>.title{font-size:1.375em}
+h4,h5{font-size:1.125em}
+h6{font-size:1em}
+hr{border:solid #dddddf;border-width:1px 0 0;clear:both;margin:1.25em 0 1.1875em}
+em,i{font-style:italic;line-height:inherit}
+strong,b{font-weight:bold;line-height:inherit}
+small{font-size:60%;line-height:inherit}
+code{font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;font-weight:400;color:rgba(0,0,0,.9)}
+ul,ol,dl{line-height:1.6;margin-bottom:1.25em;list-style-position:outside;font-family:inherit}
+ul,ol{margin-left:1.5em}
+ul li ul,ul li ol{margin-left:1.25em;margin-bottom:0}
+ul.circle{list-style-type:circle}
+ul.disc{list-style-type:disc}
+ul.square{list-style-type:square}
+ul.circle ul:not([class]),ul.disc ul:not([class]),ul.square ul:not([class]){list-style:inherit}
+ol li ul,ol li ol{margin-left:1.25em;margin-bottom:0}
+dl dt{margin-bottom:.3125em;font-weight:bold}
+dl dd{margin-bottom:1.25em}
+blockquote{margin:0 0 1.25em;padding:.5625em 1.25em 0 1.1875em;border-left:1px solid #ddd}
+blockquote,blockquote p{line-height:1.6;color:rgba(0,0,0,.85)}
+@media screen and (min-width:768px){h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2}
+h1{font-size:2.75em}
+h2{font-size:2.3125em}
+h3,#toctitle,.sidebarblock>.content>.title{font-size:1.6875em}
+h4{font-size:1.4375em}}
+table{background:#fff;margin-bottom:1.25em;border:1px solid #dedede;word-wrap:normal}
+table thead,table tfoot{background:#f7f8f7}
+table thead tr th,table thead tr td,table tfoot tr th,table tfoot tr td{padding:.5em .625em .625em;font-size:inherit;color:rgba(0,0,0,.8);text-align:left}
+table tr th,table tr td{padding:.5625em .625em;font-size:inherit;color:rgba(0,0,0,.8)}
+table tr.even,table tr.alt{background:#f8f8f7}
+table thead tr th,table tfoot tr th,table tbody tr td,table tr td,table tfoot tr td{line-height:1.6}
+h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2;word-spacing:-.05em}
+h1 strong,h2 strong,h3 strong,#toctitle strong,.sidebarblock>.content>.title strong,h4 strong,h5 strong,h6 strong{font-weight:400}
+.center{margin-left:auto;margin-right:auto}
+.stretch{width:100%}
+.clearfix::before,.clearfix::after,.float-group::before,.float-group::after{content:" ";display:table}
+.clearfix::after,.float-group::after{clear:both}
+:not(pre).nobreak{word-wrap:normal}
+:not(pre).nowrap{white-space:nowrap}
+:not(pre).pre-wrap{white-space:pre-wrap}
+:not(pre):not([class^=L])>code{font-size:.9375em;font-style:normal!important;letter-spacing:0;padding:.1em .5ex;word-spacing:-.15em;background:#f7f7f8;border-radius:4px;line-height:1.45;text-rendering:optimizeSpeed}
+pre{color:rgba(0,0,0,.9);font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;line-height:1.45;text-rendering:optimizeSpeed}
+pre code,pre pre{color:inherit;font-size:inherit;line-height:inherit}
+pre>code{display:block}
+pre.nowrap,pre.nowrap pre{white-space:pre;word-wrap:normal}
+em em{font-style:normal}
+strong strong{font-weight:400}
+.keyseq{color:rgba(51,51,51,.8)}
+kbd{font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;display:inline-block;color:rgba(0,0,0,.8);font-size:.65em;line-height:1.45;background:#f7f7f7;border:1px solid #ccc;border-radius:3px;box-shadow:0 1px 0 rgba(0,0,0,.2),inset 0 0 0 .1em #fff;margin:0 .15em;padding:.2em .5em;vertical-align:middle;position:relative;top:-.1em;white-space:nowrap}
+.keyseq kbd:first-child{margin-left:0}
+.keyseq kbd:last-child{margin-right:0}
+.menuseq,.menuref{color:#000}
+.menuseq b:not(.caret),.menuref{font-weight:inherit}
+.menuseq{word-spacing:-.02em}
+.menuseq b.caret{font-size:1.25em;line-height:.8}
+.menuseq i.caret{font-weight:bold;text-align:center;width:.45em}
+b.button::before,b.button::after{position:relative;top:-1px;font-weight:400}
+b.button::before{content:"[";padding:0 3px 0 2px}
+b.button::after{content:"]";padding:0 2px 0 3px}
+p a>code:hover{color:rgba(0,0,0,.9)}
+#header,#content,#footnotes,#footer{width:100%;margin:0 auto;max-width:62.5em;*zoom:1;position:relative;padding-left:.9375em;padding-right:.9375em}
+#header::before,#header::after,#content::before,#content::after,#footnotes::before,#footnotes::after,#footer::before,#footer::after{content:" ";display:table}
+#header::after,#content::after,#footnotes::after,#footer::after{clear:both}
+#content{margin-top:1.25em}
+#content::before{content:none}
+#header>h1:first-child{color:rgba(0,0,0,.85);margin-top:2.25rem;margin-bottom:0}
+#header>h1:first-child+#toc{margin-top:8px;border-top:1px solid #dddddf}
+#header>h1:only-child,body.toc2 #header>h1:nth-last-child(2){border-bottom:1px solid #dddddf;padding-bottom:8px}
+#header .details{border-bottom:1px solid #dddddf;line-height:1.45;padding-top:.25em;padding-bottom:.25em;padding-left:.25em;color:rgba(0,0,0,.6);display:flex;flex-flow:row wrap}
+#header .details span:first-child{margin-left:-.125em}
+#header .details span.email a{color:rgba(0,0,0,.85)}
+#header .details br{display:none}
+#header .details br+span::before{content:"\00a0\2013\00a0"}
+#header .details br+span.author::before{content:"\00a0\22c5\00a0";color:rgba(0,0,0,.85)}
+#header .details br+span#revremark::before{content:"\00a0|\00a0"}
+#header #revnumber{text-transform:capitalize}
+#header #revnumber::after{content:"\00a0"}
+#content>h1:first-child:not([class]){color:rgba(0,0,0,.85);border-bottom:1px solid #dddddf;padding-bottom:8px;margin-top:0;padding-top:1rem;margin-bottom:1.25rem}
+#toc{border-bottom:1px solid #e7e7e9;padding-bottom:.5em}
+#toc>ul{margin-left:.125em}
+#toc ul.sectlevel0>li>a{font-style:italic}
+#toc ul.sectlevel0 ul.sectlevel1{margin:.5em 0}
+#toc ul{font-family:"Open Sans","DejaVu Sans",sans-serif;list-style-type:none}
+#toc li{line-height:1.3334;margin-top:.3334em}
+#toc a{text-decoration:none}
+#toc a:active{text-decoration:underline}
+#toctitle{color:#7a2518;font-size:1.2em}
+@media screen and (min-width:768px){#toctitle{font-size:1.375em}
+body.toc2{padding-left:15em;padding-right:0}
+#toc.toc2{margin-top:0!important;background:#f8f8f7;position:fixed;width:15em;left:0;top:0;border-right:1px solid #e7e7e9;border-top-width:0!important;border-bottom-width:0!important;z-index:1000;padding:1.25em 1em;height:100%;overflow:auto}
+#toc.toc2 #toctitle{margin-top:0;margin-bottom:.8rem;font-size:1.2em}
+#toc.toc2>ul{font-size:.9em;margin-bottom:0}
+#toc.toc2 ul ul{margin-left:0;padding-left:1em}
+#toc.toc2 ul.sectlevel0 ul.sectlevel1{padding-left:0;margin-top:.5em;margin-bottom:.5em}
+body.toc2.toc-right{padding-left:0;padding-right:15em}
+body.toc2.toc-right #toc.toc2{border-right-width:0;border-left:1px solid #e7e7e9;left:auto;right:0}}
+@media screen and (min-width:1280px){body.toc2{padding-left:20em;padding-right:0}
+#toc.toc2{width:20em}
+#toc.toc2 #toctitle{font-size:1.375em}
+#toc.toc2>ul{font-size:.95em}
+#toc.toc2 ul ul{padding-left:1.25em}
+body.toc2.toc-right{padding-left:0;padding-right:20em}}
+#content #toc{border:1px solid #e0e0dc;margin-bottom:1.25em;padding:1.25em;background:#f8f8f7;border-radius:4px}
+#content #toc>:first-child{margin-top:0}
+#content #toc>:last-child{margin-bottom:0}
+#footer{max-width:none;background:rgba(0,0,0,.8);padding:1.25em}
+#footer-text{color:hsla(0,0%,100%,.8);line-height:1.44}
+#content{margin-bottom:.625em}
+.sect1{padding-bottom:.625em}
+@media screen and (min-width:768px){#content{margin-bottom:1.25em}
+.sect1{padding-bottom:1.25em}}
+.sect1:last-child{padding-bottom:0}
+.sect1+.sect1{border-top:1px solid #e7e7e9}
+#content h1>a.anchor,h2>a.anchor,h3>a.anchor,#toctitle>a.anchor,.sidebarblock>.content>.title>a.anchor,h4>a.anchor,h5>a.anchor,h6>a.anchor{position:absolute;z-index:1001;width:1.5ex;margin-left:-1.5ex;display:block;text-decoration:none!important;visibility:hidden;text-align:center;font-weight:400}
+#content h1>a.anchor::before,h2>a.anchor::before,h3>a.anchor::before,#toctitle>a.anchor::before,.sidebarblock>.content>.title>a.anchor::before,h4>a.anchor::before,h5>a.anchor::before,h6>a.anchor::before{content:"\00A7";font-size:.85em;display:block;padding-top:.1em}
+#content h1:hover>a.anchor,#content h1>a.anchor:hover,h2:hover>a.anchor,h2>a.anchor:hover,h3:hover>a.anchor,#toctitle:hover>a.anchor,.sidebarblock>.content>.title:hover>a.anchor,h3>a.anchor:hover,#toctitle>a.anchor:hover,.sidebarblock>.content>.title>a.anchor:hover,h4:hover>a.anchor,h4>a.anchor:hover,h5:hover>a.anchor,h5>a.anchor:hover,h6:hover>a.anchor,h6>a.anchor:hover{visibility:visible}
+#content h1>a.link,h2>a.link,h3>a.link,#toctitle>a.link,.sidebarblock>.content>.title>a.link,h4>a.link,h5>a.link,h6>a.link{color:#ba3925;text-decoration:none}
+#content h1>a.link:hover,h2>a.link:hover,h3>a.link:hover,#toctitle>a.link:hover,.sidebarblock>.content>.title>a.link:hover,h4>a.link:hover,h5>a.link:hover,h6>a.link:hover{color:#a53221}
+details,.audioblock,.imageblock,.literalblock,.listingblock,.stemblock,.videoblock{margin-bottom:1.25em}
+details{margin-left:1.25rem}
+details>summary{cursor:pointer;display:block;position:relative;line-height:1.6;margin-bottom:.625rem;outline:none;-webkit-tap-highlight-color:transparent}
+details>summary::-webkit-details-marker{display:none}
+details>summary::before{content:"";border:solid transparent;border-left:solid;border-width:.3em 0 .3em .5em;position:absolute;top:.5em;left:-1.25rem;transform:translateX(15%)}
+details[open]>summary::before{border:solid transparent;border-top:solid;border-width:.5em .3em 0;transform:translateY(15%)}
+details>summary::after{content:"";width:1.25rem;height:1em;position:absolute;top:.3em;left:-1.25rem}
+.admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{text-rendering:optimizeLegibility;text-align:left;font-family:"Noto Serif","DejaVu Serif",serif;font-size:1rem;font-style:italic}
+table.tableblock.fit-content>caption.title{white-space:nowrap;width:0}
+.paragraph.lead>p,#preamble>.sectionbody>[class=paragraph]:first-of-type p{font-size:1.21875em;line-height:1.6;color:rgba(0,0,0,.85)}
+.admonitionblock>table{border-collapse:separate;border:0;background:none;width:100%}
+.admonitionblock>table td.icon{text-align:center;width:80px}
+.admonitionblock>table td.icon img{max-width:none}
+.admonitionblock>table td.icon .title{font-weight:bold;font-family:"Open Sans","DejaVu Sans",sans-serif;text-transform:uppercase}
+.admonitionblock>table td.content{padding-left:1.125em;padding-right:1.25em;border-left:1px solid #dddddf;color:rgba(0,0,0,.6);word-wrap:anywhere}
+.admonitionblock>table td.content>:last-child>:last-child{margin-bottom:0}
+.exampleblock>.content{border:1px solid #e6e6e6;margin-bottom:1.25em;padding:1.25em;background:#fff;border-radius:4px}
+.sidebarblock{border:1px solid #dbdbd6;margin-bottom:1.25em;padding:1.25em;background:#f3f3f2;border-radius:4px}
+.sidebarblock>.content>.title{color:#7a2518;margin-top:0;text-align:center}
+.exampleblock>.content>:first-child,.sidebarblock>.content>:first-child{margin-top:0}
+.exampleblock>.content>:last-child,.exampleblock>.content>:last-child>:last-child,.exampleblock>.content .olist>ol>li:last-child>:last-child,.exampleblock>.content .ulist>ul>li:last-child>:last-child,.exampleblock>.content .qlist>ol>li:last-child>:last-child,.sidebarblock>.content>:last-child,.sidebarblock>.content>:last-child>:last-child,.sidebarblock>.content .olist>ol>li:last-child>:last-child,.sidebarblock>.content .ulist>ul>li:last-child>:last-child,.sidebarblock>.content .qlist>ol>li:last-child>:last-child{margin-bottom:0}
+.literalblock pre,.listingblock>.content>pre{border-radius:4px;overflow-x:auto;padding:1em;font-size:.8125em}
+@media screen and (min-width:768px){.literalblock pre,.listingblock>.content>pre{font-size:.90625em}}
+@media screen and (min-width:1280px){.literalblock pre,.listingblock>.content>pre{font-size:1em}}
+.literalblock pre,.listingblock>.content>pre:not(.highlight),.listingblock>.content>pre[class=highlight],.listingblock>.content>pre[class^="highlight "]{background:#f7f7f8}
+.literalblock.output pre{color:#f7f7f8;background:rgba(0,0,0,.9)}
+.listingblock>.content{position:relative}
+.listingblock code[data-lang]::before{display:none;content:attr(data-lang);position:absolute;font-size:.75em;top:.425rem;right:.5rem;line-height:1;text-transform:uppercase;color:inherit;opacity:.5}
+.listingblock:hover code[data-lang]::before{display:block}
+.listingblock.terminal pre .command::before{content:attr(data-prompt);padding-right:.5em;color:inherit;opacity:.5}
+.listingblock.terminal pre .command:not([data-prompt])::before{content:"$"}
+.listingblock pre.highlightjs{padding:0}
+.listingblock pre.highlightjs>code{padding:1em;border-radius:4px}
+.listingblock pre.prettyprint{border-width:0}
+.prettyprint{background:#f7f7f8}
+pre.prettyprint .linenums{line-height:1.45;margin-left:2em}
+pre.prettyprint li{background:none;list-style-type:inherit;padding-left:0}
+pre.prettyprint li code[data-lang]::before{opacity:1}
+pre.prettyprint li:not(:first-child) code[data-lang]::before{display:none}
+table.linenotable{border-collapse:separate;border:0;margin-bottom:0;background:none}
+table.linenotable td[class]{color:inherit;vertical-align:top;padding:0;line-height:inherit;white-space:normal}
+table.linenotable td.code{padding-left:.75em}
+table.linenotable td.linenos,pre.pygments .linenos{border-right:1px solid;opacity:.35;padding-right:.5em;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
+pre.pygments span.linenos{display:inline-block;margin-right:.75em}
+.quoteblock{margin:0 1em 1.25em 1.5em;display:table}
+.quoteblock:not(.excerpt)>.title{margin-left:-1.5em;margin-bottom:.75em}
+.quoteblock blockquote,.quoteblock p{color:rgba(0,0,0,.85);font-size:1.15rem;line-height:1.75;word-spacing:.1em;letter-spacing:0;font-style:italic;text-align:justify}
+.quoteblock blockquote{margin:0;padding:0;border:0}
+.quoteblock blockquote::before{content:"\201c";float:left;font-size:2.75em;font-weight:bold;line-height:.6em;margin-left:-.6em;color:#7a2518;text-shadow:0 1px 2px rgba(0,0,0,.1)}
+.quoteblock blockquote>.paragraph:last-child p{margin-bottom:0}
+.quoteblock .attribution{margin-top:.75em;margin-right:.5ex;text-align:right}
+.verseblock{margin:0 1em 1.25em}
+.verseblock pre{font-family:"Open Sans","DejaVu Sans",sans-serif;font-size:1.15rem;color:rgba(0,0,0,.85);font-weight:300;text-rendering:optimizeLegibility}
+.verseblock pre strong{font-weight:400}
+.verseblock .attribution{margin-top:1.25rem;margin-left:.5ex}
+.quoteblock .attribution,.verseblock .attribution{font-size:.9375em;line-height:1.45;font-style:italic}
+.quoteblock .attribution br,.verseblock .attribution br{display:none}
+.quoteblock .attribution cite,.verseblock .attribution cite{display:block;letter-spacing:-.025em;color:rgba(0,0,0,.6)}
+.quoteblock.abstract blockquote::before,.quoteblock.excerpt blockquote::before,.quoteblock .quoteblock blockquote::before{display:none}
+.quoteblock.abstract blockquote,.quoteblock.abstract p,.quoteblock.excerpt blockquote,.quoteblock.excerpt p,.quoteblock .quoteblock blockquote,.quoteblock .quoteblock p{line-height:1.6;word-spacing:0}
+.quoteblock.abstract{margin:0 1em 1.25em;display:block}
+.quoteblock.abstract>.title{margin:0 0 .375em;font-size:1.15em;text-align:center}
+.quoteblock.excerpt>blockquote,.quoteblock .quoteblock{padding:0 0 .25em 1em;border-left:.25em solid #dddddf}
+.quoteblock.excerpt,.quoteblock .quoteblock{margin-left:0}
+.quoteblock.excerpt blockquote,.quoteblock.excerpt p,.quoteblock .quoteblock blockquote,.quoteblock .quoteblock p{color:inherit;font-size:1.0625rem}
+.quoteblock.excerpt .attribution,.quoteblock .quoteblock .attribution{color:inherit;font-size:.85rem;text-align:left;margin-right:0}
+p.tableblock:last-child{margin-bottom:0}
+td.tableblock>.content{margin-bottom:1.25em;word-wrap:anywhere}
+td.tableblock>.content>:last-child{margin-bottom:-1.25em}
+table.tableblock,th.tableblock,td.tableblock{border:0 solid #dedede}
+table.grid-all>*>tr>*{border-width:1px}
+table.grid-cols>*>tr>*{border-width:0 1px}
+table.grid-rows>*>tr>*{border-width:1px 0}
+table.frame-all{border-width:1px}
+table.frame-ends{border-width:1px 0}
+table.frame-sides{border-width:0 1px}
+table.frame-none>colgroup+*>:first-child>*,table.frame-sides>colgroup+*>:first-child>*{border-top-width:0}
+table.frame-none>:last-child>:last-child>*,table.frame-sides>:last-child>:last-child>*{border-bottom-width:0}
+table.frame-none>*>tr>:first-child,table.frame-ends>*>tr>:first-child{border-left-width:0}
+table.frame-none>*>tr>:last-child,table.frame-ends>*>tr>:last-child{border-right-width:0}
+table.stripes-all>*>tr,table.stripes-odd>*>tr:nth-of-type(odd),table.stripes-even>*>tr:nth-of-type(even),table.stripes-hover>*>tr:hover{background:#f8f8f7}
+th.halign-left,td.halign-left{text-align:left}
+th.halign-right,td.halign-right{text-align:right}
+th.halign-center,td.halign-center{text-align:center}
+th.valign-top,td.valign-top{vertical-align:top}
+th.valign-bottom,td.valign-bottom{vertical-align:bottom}
+th.valign-middle,td.valign-middle{vertical-align:middle}
+table thead th,table tfoot th{font-weight:bold}
+tbody tr th{background:#f7f8f7}
+tbody tr th,tbody tr th p,tfoot tr th,tfoot tr th p{color:rgba(0,0,0,.8);font-weight:bold}
+p.tableblock>code:only-child{background:none;padding:0}
+p.tableblock{font-size:1em}
+ol{margin-left:1.75em}
+ul li ol{margin-left:1.5em}
+dl dd{margin-left:1.125em}
+dl dd:last-child,dl dd:last-child>:last-child{margin-bottom:0}
+li p,ul dd,ol dd,.olist .olist,.ulist .ulist,.ulist .olist,.olist .ulist{margin-bottom:.625em}
+ul.checklist,ul.none,ol.none,ul.no-bullet,ol.no-bullet,ol.unnumbered,ul.unstyled,ol.unstyled{list-style-type:none}
+ul.no-bullet,ol.no-bullet,ol.unnumbered{margin-left:.625em}
+ul.unstyled,ol.unstyled{margin-left:0}
+li>p:empty:only-child::before{content:"";display:inline-block}
+ul.checklist>li>p:first-child{margin-left:-1em}
+ul.checklist>li>p:first-child>.fa-square-o:first-child,ul.checklist>li>p:first-child>.fa-check-square-o:first-child{width:1.25em;font-size:.8em;position:relative;bottom:.125em}
+ul.checklist>li>p:first-child>input[type=checkbox]:first-child{margin-right:.25em}
+ul.inline{display:flex;flex-flow:row wrap;list-style:none;margin:0 0 .625em -1.25em}
+ul.inline>li{margin-left:1.25em}
+.unstyled dl dt{font-weight:400;font-style:normal}
+ol.arabic{list-style-type:decimal}
+ol.decimal{list-style-type:decimal-leading-zero}
+ol.loweralpha{list-style-type:lower-alpha}
+ol.upperalpha{list-style-type:upper-alpha}
+ol.lowerroman{list-style-type:lower-roman}
+ol.upperroman{list-style-type:upper-roman}
+ol.lowergreek{list-style-type:lower-greek}
+.hdlist>table,.colist>table{border:0;background:none}
+.hdlist>table>tbody>tr,.colist>table>tbody>tr{background:none}
+td.hdlist1,td.hdlist2{vertical-align:top;padding:0 .625em}
+td.hdlist1{font-weight:bold;padding-bottom:1.25em}
+td.hdlist2{word-wrap:anywhere}
+.literalblock+.colist,.listingblock+.colist{margin-top:-.5em}
+.colist td:not([class]):first-child{padding:.4em .75em 0;line-height:1;vertical-align:top}
+.colist td:not([class]):first-child img{max-width:none}
+.colist td:not([class]):last-child{padding:.25em 0}
+.thumb,.th{line-height:0;display:inline-block;border:4px solid #fff;box-shadow:0 0 0 1px #ddd}
+.imageblock.left{margin:.25em .625em 1.25em 0}
+.imageblock.right{margin:.25em 0 1.25em .625em}
+.imageblock>.title{margin-bottom:0}
+.imageblock.thumb,.imageblock.th{border-width:6px}
+.imageblock.thumb>.title,.imageblock.th>.title{padding:0 .125em}
+.image.left,.image.right{margin-top:.25em;margin-bottom:.25em;display:inline-block;line-height:0}
+.image.left{margin-right:.625em}
+.image.right{margin-left:.625em}
+a.image{text-decoration:none;display:inline-block}
+a.image object{pointer-events:none}
+sup.footnote,sup.footnoteref{font-size:.875em;position:static;vertical-align:super}
+sup.footnote a,sup.footnoteref a{text-decoration:none}
+sup.footnote a:active,sup.footnoteref a:active{text-decoration:underline}
+#footnotes{padding-top:.75em;padding-bottom:.75em;margin-bottom:.625em}
+#footnotes hr{width:20%;min-width:6.25em;margin:-.25em 0 .75em;border-width:1px 0 0}
+#footnotes .footnote{padding:0 .375em 0 .225em;line-height:1.3334;font-size:.875em;margin-left:1.2em;margin-bottom:.2em}
+#footnotes .footnote a:first-of-type{font-weight:bold;text-decoration:none;margin-left:-1.05em}
+#footnotes .footnote:last-of-type{margin-bottom:0}
+#content #footnotes{margin-top:-.625em;margin-bottom:0;padding:.75em 0}
+div.unbreakable{page-break-inside:avoid}
+.big{font-size:larger}
+.small{font-size:smaller}
+.underline{text-decoration:underline}
+.overline{text-decoration:overline}
+.line-through{text-decoration:line-through}
+.aqua{color:#00bfbf}
+.aqua-background{background:#00fafa}
+.black{color:#000}
+.black-background{background:#000}
+.blue{color:#0000bf}
+.blue-background{background:#0000fa}
+.fuchsia{color:#bf00bf}
+.fuchsia-background{background:#fa00fa}
+.gray{color:#606060}
+.gray-background{background:#7d7d7d}
+.green{color:#006000}
+.green-background{background:#007d00}
+.lime{color:#00bf00}
+.lime-background{background:#00fa00}
+.maroon{color:#600000}
+.maroon-background{background:#7d0000}
+.navy{color:#000060}
+.navy-background{background:#00007d}
+.olive{color:#606000}
+.olive-background{background:#7d7d00}
+.purple{color:#600060}
+.purple-background{background:#7d007d}
+.red{color:#bf0000}
+.red-background{background:#fa0000}
+.silver{color:#909090}
+.silver-background{background:#bcbcbc}
+.teal{color:#006060}
+.teal-background{background:#007d7d}
+.white{color:#bfbfbf}
+.white-background{background:#fafafa}
+.yellow{color:#bfbf00}
+.yellow-background{background:#fafa00}
+span.icon>.fa{cursor:default}
+a span.icon>.fa{cursor:inherit}
+.admonitionblock td.icon [class^="fa icon-"]{font-size:2.5em;text-shadow:1px 1px 2px rgba(0,0,0,.5);cursor:default}
+.admonitionblock td.icon .icon-note::before{content:"\f05a";color:#19407c}
+.admonitionblock td.icon .icon-tip::before{content:"\f0eb";text-shadow:1px 1px 2px rgba(155,155,0,.8);color:#111}
+.admonitionblock td.icon .icon-warning::before{content:"\f071";color:#bf6900}
+.admonitionblock td.icon .icon-caution::before{content:"\f06d";color:#bf3400}
+.admonitionblock td.icon .icon-important::before{content:"\f06a";color:#bf0000}
+.conum[data-value]{display:inline-block;color:#fff!important;background:rgba(0,0,0,.8);border-radius:50%;text-align:center;font-size:.75em;width:1.67em;height:1.67em;line-height:1.67em;font-family:"Open Sans","DejaVu Sans",sans-serif;font-style:normal;font-weight:bold}
+.conum[data-value] *{color:#fff!important}
+.conum[data-value]+b{display:none}
+.conum[data-value]::after{content:attr(data-value)}
+pre .conum[data-value]{position:relative;top:-.125em}
+b.conum *{color:inherit!important}
+.conum:not([data-value]):empty{display:none}
+dt,th.tableblock,td.content,div.footnote{text-rendering:optimizeLegibility}
+h1,h2,p,td.content,span.alt,summary{letter-spacing:-.01em}
+p strong,td.content strong,div.footnote strong{letter-spacing:-.005em}
+p,blockquote,dt,td.content,td.hdlist1,span.alt,summary{font-size:1.0625rem}
+p{margin-bottom:1.25rem}
+.sidebarblock p,.sidebarblock dt,.sidebarblock td.content,p.tableblock{font-size:1em}
+.exampleblock>.content{background:#fffef7;border-color:#e0e0dc;box-shadow:0 1px 4px #e0e0dc}
+.print-only{display:none!important}
+@page{margin:1.25cm .75cm}
+@media print{*{box-shadow:none!important;text-shadow:none!important}
+html{font-size:80%}
+a{color:inherit!important;text-decoration:underline!important}
+a.bare,a[href^="#"],a[href^="mailto:"]{text-decoration:none!important}
+a[href^="http:"]:not(.bare)::after,a[href^="https:"]:not(.bare)::after{content:"(" attr(href) ")";display:inline-block;font-size:.875em;padding-left:.25em}
+abbr[title]{border-bottom:1px dotted}
+abbr[title]::after{content:" (" attr(title) ")"}
+pre,blockquote,tr,img,object,svg{page-break-inside:avoid}
+thead{display:table-header-group}
+svg{max-width:100%}
+p,blockquote,dt,td.content{font-size:1em;orphans:3;widows:3}
+h2,h3,#toctitle,.sidebarblock>.content>.title{page-break-after:avoid}
+#header,#content,#footnotes,#footer{max-width:none}
+#toc,.sidebarblock,.exampleblock>.content{background:none!important}
+#toc{border-bottom:1px solid #dddddf!important;padding-bottom:0!important}
+body.book #header{text-align:center}
+body.book #header>h1:first-child{border:0!important;margin:2.5em 0 1em}
+body.book #header .details{border:0!important;display:block;padding:0!important}
+body.book #header .details span:first-child{margin-left:0!important}
+body.book #header .details br{display:block}
+body.book #header .details br+span::before{content:none!important}
+body.book #toc{border:0!important;text-align:left!important;padding:0!important;margin:0!important}
+body.book #toc,body.book #preamble,body.book h1.sect0,body.book .sect1>h2{page-break-before:always}
+.listingblock code[data-lang]::before{display:block}
+#footer{padding:0 .9375em}
+.hide-on-print{display:none!important}
+.print-only{display:block!important}
+.hide-for-print{display:none!important}
+.show-for-print{display:inherit!important}}
+@media amzn-kf8,print{#header>h1:first-child{margin-top:1.25rem}
+.sect1{padding:0!important}
+.sect1+.sect1{border:0}
+#footer{background:none}
+#footer-text{color:rgba(0,0,0,.6);font-size:.9em}}
+@media amzn-kf8{#header,#content,#footnotes,#footer{padding:0}}
+</style>
+<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
+</head>
+<body class="article">
+<div id="header">
+</div>
+<div id="content">
+
+</div>
+<script type="text/x-mathjax-config">
+MathJax.Hub.Config({
+  messageStyle: "none",
+  tex2jax: {
+    inlineMath: [["\\(", "\\)"]],
+    displayMath: [["\\[", "\\]"]],
+    ignoreClass: "nostem|nolatexmath"
+  },
+  asciimath2jax: {
+    delimiters: [["\\$", "\\$"]],
+    ignoreClass: "nostem|noasciimath"
+  },
+  TeX: { equationNumbers: { autoNumber: "none" } }
+})
+MathJax.Hub.Register.StartupHook("AsciiMath Jax Ready", function () {
+  MathJax.InputJax.AsciiMath.postfilterHooks.Add(function (data, node) {
+    if ((node = data.script.parentNode) && (node = node.parentNode) && node.classList.contains("stemblock")) {
+      data.math.root.display = "block"
+    }
+    return data
+  })
+})
+</script>
+<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.9/MathJax.js?config=TeX-MML-AM_HTMLorMML"></script>
+</body>
+</html>
\ No newline at end of file
diff --git a/example.html b/example.html
new file mode 100644 (file)
index 0000000..0883092
--- /dev/null
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs/themes/prism.css">
+<script src="https://cdn.jsdelivr.net/npm/prismjs/prism.js"></script>
+<script src="https://cdn.jsdelivr.net/npm/prismjs/components/prism-javascript.min.js"></script>
+<title>Canvas + Code</title>
+<style>
+/* Container grid: canvas + code side by side */
+
+html {
+    background-image: url(https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExNHNmMm03bndxZXlwcW81ZHY4aXc3dXkzMXZhNmx2OTFqYWEwaG13bCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/9ufQTaDtvGX51NkpWL/giphy.gif);
+    background-size: cover;
+    background-attachment: fixed;
+    overflow: hidden;
+}
+
+body {
+    opacity: 85%;
+}
+
+#container {
+    display: grid;
+    grid-template-columns: 1fr 5fr; /* canvas fixed, code fills remaining space */
+    height: 100vh; /* full viewport height */
+    max-width: 1600px;
+    gap: 10px;
+    margin: 0 auto;
+    align-items: center;
+}
+
+/* Canvas itself */
+#game {
+    border: 4px solid white;
+    margin: 0;
+    padding: 0;
+    display: block;
+}
+
+/* Code viewer */
+#code-viewer {
+    margin: 0;
+    box-sizing: border-box; /* include padding in height */
+    overflow: auto; /* scroll if content too long */
+    font-family: monospace;
+    white-space: pre-wrap;
+    width: 100%;
+    height: 100%;
+    max-height: 808px;
+    max-width: 808px;
+    margin-left: auto;
+}
+</style>
+</head>
+<body>
+
+<div id="container">
+    <canvas id="game"></canvas>
+    <pre id="code-viewer">
+      <code class="language-javascript"> </code>
+    </pre>
+
+</div>
+
+<script src="https://nyquistdev.net/git/?p=basic-3d-example.git;a=blob_plain;f=index.js;hb=HEAD"></script>
+<script>
+fetch('https://nyquistdev.net/git/?p=basic-3d-example.git;a=blob_plain;f=index.js;hb=HEAD')
+    .then(res => res.text())
+    .then(code => {
+        const codeEl = document.querySelector('#code-viewer code');
+        codeEl.textContent = code;
+        Prism.highlightElement(codeEl);
+    })
+    .catch(err => console.error('Could not load index.js:', err));
+</script>
+</body>
+</html>
diff --git a/index.html b/index.html
new file mode 100644 (file)
index 0000000..f2f8a47
--- /dev/null
@@ -0,0 +1,9 @@
+<style>
+#game {
+    display: block;
+    margin: 0 auto;
+}
+</style>
+
+<canvas id="game"></canvas>
+<script src="index.js"></script>
diff --git a/index.js b/index.js
new file mode 100644 (file)
index 0000000..8bfa577
--- /dev/null
+++ b/index.js
@@ -0,0 +1,186 @@
+const canvas = document.getElementById("game");
+const ctx = canvas.getContext("2d")
+
+const BACKGROUND = "#000000";
+const FOREGROUND = "#00FF00";
+const FPS = 60;
+
+// let debug = true;
+let debug = false;
+
+let debugColors = [
+    "#FF0000",
+    "#00FF00",
+    "#0000FF",
+]
+
+const highContrastColors = [
+  "#FFFF00", // 1. Bright Yellow
+  "#FF00FF", // 2. Magenta
+  "#00FFFF", // 3. Cyan / Aqua
+  "#FFA500", // 4. Pure Orange
+  "#800080", // 5. Deep Purple
+  "#008080", // 6. Teal
+  "#A52A2A", // 7. Brown
+  "#800000", // 8. Maroon
+  "#008000", // 9. Forest Green
+  "#000080", // 10. Navy Blue
+  "#808000", // 11. Olive
+  "#C71585", // 12. Hot Pink
+  "#FFC0CB", // 13. Light Pink
+  "#7FFFD4", // 14. Aquamarine
+  "#FFE4E1", // 15. Misty Rose
+  "#F0E68C", // 16. Khaki
+  "#E6E6FA", // 17. Lavender
+  "#B0C4DE", // 18. Light Steel Blue
+  "#FF4500", // 19. Red-Orange
+  "#1E90FF", // 20. Dodger Blue
+  "#FFD700", // 21. Gold
+  "#9370DB", // 22. Medium Purple
+  "#8B008B", // 23. Dark Magenta
+  "#556B2F", // 24. Dark Olive Green
+  "#FF8C00", // 25. Dark Orange
+  "#48D1CC", // 26. Medium Turquoise
+  "#32CD32", // 27. Bright Green
+  "#2F4F4F", // 28. Dark Slate Gray
+  "#98FF98", // 29. Mint Green
+  "#FF7F50", // 30. Coral
+  "#4B0082", // 31. Indigo
+  "#D2B48C", // 32. Tan / Sand
+  "#FF7F55", // 30. Coral
+];
+
+canvas.width = 800;
+canvas.height = 800;
+
+function clear() {
+    ctx.fillStyle = BACKGROUND;
+    ctx.fillRect(0, 0, canvas.width, canvas.height);
+}
+
+function line(p1, p2) {
+    ctx.lineWidth = 8;
+    if (!debug) ctx.strokeStyle = FOREGROUND;
+    ctx.beginPath();
+    ctx.moveTo(p1.x, p1.y);
+    ctx.lineTo(p2.x, p2.y);
+    ctx.stroke();
+}
+
+function getMagnitude(a, b) {
+    let x = (b.x - a.x);
+    let y = (b.y - a.y);
+    return Math.sqrt(x * x + y * y);
+}
+
+function interpolate(a, b, y) {
+    let startingPos = a.x;
+    let incX = (b.x - a.x) / (b.y - a.y);
+    let numberOfyIncs = y - a.y;
+    return startingPos + incX * numberOfyIncs;
+}
+
+function drawTriangle(coords, color) {
+    coords.sort((a,b) => a.y - b.y);
+
+    let blueisSteeperThanRed = 
+        (coords[2].y - coords[0].y) * (coords[1].x - coords[0].x) >
+        (coords[1].y - coords[0].y) * (coords[2].x - coords[0].x)
+
+    for (let y = coords[0].y; y <= coords[2].y; ++y) {
+
+        let x1 = interpolate(coords[0], coords[2], y);
+
+        let x2 = (y < coords[1].y) ? 
+            interpolate(coords[0], coords[1], y) :
+            interpolate(coords[1], coords[2], y);
+
+        let fill = (i, y) => {
+            ctx.fillStyle = color;
+            ctx.fillRect(i, y, 2, 2);
+        }
+
+        if (blueisSteeperThanRed) {
+            for (let i = x1; i < x2; ++i) fill(i, y);
+        } else {
+            for (let i = x1; i > x2; --i) fill(i, y);
+
+        }
+    }
+
+    if (debug) {
+        for (const i in coords) {
+            ctx.strokeStyle = debugColors[i];
+            let a = coords[i];
+            let b = coords[ (i + 1) % coords.length];
+            line(a, b);
+        }
+
+    }
+
+
+}
+
+
+clear();
+let moveX = 0;
+
+let triangleCoords = [
+  // --- CENTER CRADLE (Core focal points) ---
+  [{x: 400, y: 400}, {x: 300, y: 300}, {x: 500, y: 250}], // High acute top wedge
+  [{x: 400, y: 400}, {x: 500, y: 250}, {x: 550, y: 420}], // Narrow vertical sliver
+  [{x: 400, y: 400}, {x: 550, y: 420}, {x: 480, y: 550}], // Rotated downward blade
+  [{x: 400, y: 400}, {x: 480, y: 550}, {x: 280, y: 480}], // Broad foundational center piece
+  [{x: 400, y: 400}, {x: 280, y: 480}, {x: 300, y: 300}], // Left-leaning internal wedge
+
+  // --- NORTH FLANK (Radiating to the top edge: y = 0) ---
+  [{x: 300, y: 300}, {x: 0,   y: 0},   {x: 250, y: 0}],
+  [{x: 300, y: 300}, {x: 250, y: 0},   {x: 500, y: 0}],
+  [{x: 300, y: 300}, {x: 500, y: 0},   {x: 500, y: 250}], // Massive sweeping North-East shard
+  [{x: 500, y: 250}, {x: 500, y: 0},   {x: 700, y: 0}],
+  [{x: 500, y: 250}, {x: 700, y: 0},   {x: 800, y: 150}], // Long, sharp slope
+
+  // --- EAST FLANK (Radiating to the right edge: x = 800) ---
+  [{x: 500, y: 250}, {x: 800, y: 150}, {x: 550, y: 420}],
+  [{x: 550, y: 420}, {x: 800, y: 150}, {x: 800, y: 450}], // Tight accent block
+  [{x: 550, y: 420}, {x: 800, y: 450}, {x: 800, y: 600}], 
+  [{x: 550, y: 420}, {x: 800, y: 600}, {x: 480, y: 550}], // Giant horizontal slice
+
+  // --- SOUTH FLANK (Radiating to the bottom edge: y = 800) ---
+  [{x: 480, y: 550}, {x: 800, y: 600}, {x: 800, y: 800}], // Clears the bottom right corner
+  [{x: 480, y: 550}, {x: 800, y: 800}, {x: 550, y: 800}],
+  [{x: 480, y: 550}, {x: 550, y: 800}, {x: 350, y: 800}], // Long vertical grounding anchor
+  [{x: 480, y: 550}, {x: 350, y: 800}, {x: 150, y: 800}],
+  [{x: 480, y: 550}, {x: 150, y: 800}, {x: 280, y: 480}], // Sweeping South-West transitional floor
+
+  // --- WEST FLANK (Radiating to the left edge: x = 0) ---
+  [{x: 280, y: 480}, {x: 150, y: 800}, {x: 0,   y: 800}], // Clears the bottom left corner
+  [{x: 280, y: 480}, {x: 0,   y: 800}, {x: 0,   y: 500}],
+  [{x: 280, y: 480}, {x: 0,   y: 500}, {x: 0,   y: 300}], // Tall vertical perimeter guard
+  [{x: 280, y: 480}, {x: 0,   y: 300}, {x: 300, y: 300}], // Steep internal left block
+
+  // --- NORTH-WEST FILLER ZONE (Completing upper left frame gaps) ---
+  [{x: 300, y: 300}, {x: 0,   y: 300}, {x: 0,   y: 120}],
+  [{x: 300, y: 300}, {x: 0,   y: 120}, {x: 0,   y: 0}],
+
+  // --- OUTER SUBDIVISIONS (Adding complexity to the borders for visual contrast) ---
+  [{x: 700, y: 0},   {x: 800, y: 0},   {x: 800, y: 150}], // Top right micro-corner
+  [{x: 550, y: 800}, {x: 680, y: 800}, {x: 480, y: 550}], // Tiny baseline shard A
+  [{x: 680, y: 800}, {x: 800, y: 800}, {x: 480, y: 550}], // Tiny baseline shard B
+  [{x: 350, y: 800}, {x: 250, y: 800}, {x: 480, y: 550}], // Tiny baseline shard C
+  [{x: 250, y: 800}, {x: 150, y: 800}, {x: 480, y: 550}], // Tiny baseline shard D
+  [{x: 0,   y: 500}, {x: 0,   y: 650}, {x: 280, y: 480}], // Left margin splitter
+  [{x: 0,   y: 650}, {x: 0,   y: 800}, {x: 280, y: 480}], // Left floor splitter
+  [{x: 0,   y: 300}, {x: 0,   y: 200}, {x: 300, y: 300}], // Left ceiling splitter
+  [{x: 0,   y: 200}, {x: 0,   y: 0},   {x: 300, y: 300}]  // Left corner lock
+];
+
+// drawTriangle([{x: moveX + 200, y: 200}, {x: moveX + 600, y: 310},{x: moveX + 450, y: 400}]);
+// drawTriangle([{x: 120, y: 200}, {x:200, y: 310},{x: 450, y: 400}], "#FF0000");
+// drawTriangle([{x: 200, y: 200}, {x:600, y: 230},{x: 250, y: 300}]);
+
+
+for (const i in triangleCoords) {
+    drawTriangle(triangleCoords[i], highContrastColors[i % highContrastColors.length]);
+    await new Promise(r => setTimeout(r, 2000));
+}
diff --git a/look-at(getViewMatrix).png b/look-at(getViewMatrix).png
new file mode 100644 (file)
index 0000000..81abb7d
Binary files /dev/null and b/look-at(getViewMatrix).png differ
diff --git a/notes.adoc b/notes.adoc
new file mode 100644 (file)
index 0000000..a6ea044
--- /dev/null
@@ -0,0 +1,6 @@
+== Links
+* https://www.youtube.com/watch?v=U0_ONQQ5ZNM
+
+* https://learnopengl.com/Getting-started/Camera[Learn OpenGL | Camera]
+    ** You can use this link to understand the getViewMatrix (look at matrix)
+       function better
diff --git a/reference.js b/reference.js
new file mode 100644 (file)
index 0000000..e25705f
--- /dev/null
@@ -0,0 +1,264 @@
+      
+/* ===================== SETUP ===================== */
+const game = document.getElementById("game");
+game.width = 800;
+game.height = 800;
+const ctx = game.getContext("2d");
+
+const BACKGROUND = "#101010";
+const FOREGROUND = "#50FF50";
+const TEXT = "#FFFFFF";
+
+const FPS = 60;
+const NEAR = 0.1;
+
+/* ===================== GEOMETRY ===================== */
+const vertices = [
+    {x:  0.25, y:  0.25, z:  0.25},
+    {x: -0.25, y:  0.25, z:  0.25},
+    {x: -0.25, y: -0.25, z:  0.25},
+    {x:  0.25, y: -0.25, z:  0.25},
+    {x:  0.25, y:  0.25, z: -0.25},
+    {x: -0.25, y:  0.25, z: -0.25},
+    {x: -0.25, y: -0.25, z: -0.25},
+    {x:  0.25, y: -0.25, z: -0.25},
+];
+
+const edges = [
+    [0,1,2,3],
+    [4,5,6,7],
+    [0,4],[1,5],[2,6],[3,7]
+];
+
+/* ===================== OBJECT ===================== */
+let objectPos = { x: 0, y: 0, z: 2 };
+let angle = 0;
+
+const AXIS_MODES = [
+    ["x"], ["y"], ["z"],
+    ["x","y"], ["y","z"], ["x","z"],
+    ["x","y","z"]
+];
+let axisModeIndex = AXIS_MODES.length-1;
+let activeAxes = AXIS_MODES[axisModeIndex];
+
+/* ===================== CAMERA ===================== */
+let camera = {
+    position: { x: 0, y: 0, z: 0 },
+    rotation: [1,0,0,0],
+    speed: 0.05,
+    sensitivity: 0.002
+};
+
+/* ===================== INPUT ===================== */
+const keys = {};
+window.addEventListener("keydown", e => keys[e.key.toLowerCase()] = true);
+window.addEventListener("keyup",   e => keys[e.key.toLowerCase()] = false);
+
+/* ===================== QUATERNIONS ===================== */
+function quatFromAxisAngle(axis, angle) {
+    const h = angle / 2;
+    const s = Math.sin(h);
+    return [Math.cos(h), axis.x*s, axis.y*s, axis.z*s];
+}
+
+function quatMultiply(a, b) {
+    const [w1,x1,y1,z1] = a;
+    const [w2,x2,y2,z2] = b;
+    return [
+        w1*w2 - x1*x2 - y1*y2 - z1*z2,
+        w1*x2 + x1*w2 + y1*z2 - z1*y2,
+        w1*y2 - x1*z2 + y1*w2 + z1*x2,
+        w1*z2 + x1*y2 - y1*x2 + z1*w2
+    ];
+}
+
+function quatConjugate(q) {
+    return [q[0], -q[1], -q[2], -q[3]];
+}
+
+function quatRotateVector(q, v) {
+    // whenever w is zero then we are on the unit sphere 
+    // unit quanternion
+    const qv = [0, v.x, v.y, v.z];
+
+    // the conjugate is equal to the inverse quanternion if q is a unit-norm
+    // quanternion. in this case it is a unit-norm quanternion
+    const r = quatMultiply(quatMultiply(q, qv), quatConjugate(q));
+    return { x: r[1], y: r[2], z: r[3] };
+}
+
+/* ===================== CAMERA VECTORS ===================== */
+function cameraForward() {
+    return quatRotateVector(camera.rotation, {x:0,y:0,z:1});
+}
+function cameraRight() {
+    return quatRotateVector(camera.rotation, {x:1,y:0,z:0});
+}
+function cameraUp() {
+    return quatRotateVector(camera.rotation, {x:0,y:1,z:0});
+}
+
+function applyCamera(p) {
+    const v = {
+        x: p.x - camera.position.x,
+        y: p.y - camera.position.y,
+        z: p.z - camera.position.z
+    };
+    // we use the conjugate here because the camera should move everything in the
+    // opposite direction that we are moving
+    return quatRotateVector(quatConjugate(camera.rotation), v);
+}
+
+/* ===================== PROJECTION ===================== */
+function project(p) {
+    return { x: p.x / p.z, y: p.y / p.z };
+}
+function screen(p) {
+    return {
+        x: (p.x + 1) * 0.5 * game.width,
+        y: (1 - (p.y + 1) * 0.5) * game.height
+    };
+}
+
+/* ===================== DRAW ===================== */
+function clear() {
+    ctx.fillStyle = BACKGROUND;
+    ctx.fillRect(0,0,game.width,game.height);
+
+    ctx.fillStyle = TEXT;
+    ctx.font = "14px monospace";
+    ctx.fillText("WASD: move | QE: up/down", 10, 18);
+    ctx.fillText("Mouse: look | Click canvas to lock", 10, 36);
+    ctx.fillText("Click canvas again: change cube axes", 10, 54);
+    ctx.fillText("Cube axes: " + activeAxes.join(","), 10, 72);
+}
+
+function line(a,b) {
+    ctx.strokeStyle = FOREGROUND;
+    ctx.lineWidth = 2;
+    ctx.beginPath();
+    ctx.moveTo(a.x,a.y);
+    ctx.lineTo(b.x,b.y);
+    ctx.stroke();
+}
+
+/* ===================== LOOP ===================== */
+function frame() {
+    angle += Math.PI / FPS;
+
+    // --- cube rotation quaternion ---
+
+    // This is the center of the cube. we are going to rotate around the center
+    // of the cube.
+    let q = [1,0,0,0];
+
+    // First, have the correct unit quanternion
+    for (const ax of activeAxes) {
+        // quatFromAxixAngle are unit quanternions
+        // as we keep pulling the quanternion in the axis direction further and
+        // further, you keep rotating the quanternion
+
+        // we will use the right hand rule to figure out what is going on.
+
+        // the left side of the multiplication is the function of the right
+
+        // REMEMBER
+        //     These axis angles are part of the fourth demension and won't be
+        //     visible
+
+        // x is i. so we are going in the positive i direction
+        // ---------------------------------------------------
+        // the jk circle (The part we see) rotates from j to kk
+        if (ax==="x") q = quatMultiply(quatFromAxisAngle({x:1,y:0,z:0}, angle), q);
+
+        // y is j. So we are going in the positive j direction
+        // ---------------------------------------------------
+        // the ij circle (part we see) rotates from -i to k
+        if (ax==="y") q = quatMultiply(quatFromAxisAngle({x:0,y:1,z:0}, angle), q);
+
+        // z is k. So we are going in the positve k direction
+        // ---------------------------------------------------
+        // The ij circle rotates from j to -i
+        if (ax==="z") q = quatMultiply(quatFromAxisAngle({x:0,y:0,z:1}, angle), q);
+    }
+
+    // --- camera movement ---
+    if (keys["w"]) {
+        const f = cameraForward();
+        camera.position.x += f.x * camera.speed;
+        camera.position.y += f.y * camera.speed;
+        camera.position.z += f.z * camera.speed;
+    }
+    if (keys["s"]) {
+        const f = cameraForward();
+        camera.position.x -= f.x * camera.speed;
+        camera.position.y -= f.y * camera.speed;
+        camera.position.z -= f.z * camera.speed;
+    }
+    if (keys["a"]) {
+        const r = cameraRight();
+        camera.position.x -= r.x * camera.speed;
+        camera.position.y -= r.y * camera.speed;
+        camera.position.z -= r.z * camera.speed;
+    }
+    if (keys["d"]) {
+        const r = cameraRight();
+        camera.position.x += r.x * camera.speed;
+        camera.position.y += r.y * camera.speed;
+        camera.position.z += r.z * camera.speed;
+    }
+    if (keys["q"]) {
+        const u = cameraUp();
+        camera.position.x -= u.x * camera.speed;
+        camera.position.y -= u.y * camera.speed;
+        camera.position.z -= u.z * camera.speed;
+    }
+    if (keys["e"]) {
+        const u = cameraUp();
+        camera.position.x += u.x * camera.speed;
+        camera.position.y += u.y * camera.speed;
+        camera.position.z += u.z * camera.speed;
+    }
+
+    clear();
+
+    for (const e of edges) {
+        for (let i=0;i<e.length;i++) {
+            let a = vertices[e[i]];
+            let b = vertices[e[(i+1)%e.length]];
+
+            a = quatRotateVector(q, a);
+            b = quatRotateVector(q, b);
+
+            a = { x:a.x+objectPos.x, y:a.y+objectPos.y, z:a.z+objectPos.z };
+            b = { x:b.x+objectPos.x, y:b.y+objectPos.y, z:b.z+objectPos.z };
+
+            a = applyCamera(a);
+            b = applyCamera(b);
+
+            if (a.z<=NEAR || b.z<=NEAR) continue;
+            line(screen(project(a)), screen(project(b)));
+        }
+    }
+
+    setTimeout(frame, 1000/FPS);
+}
+frame();
+
+/* ===================== INTERACTION ===================== */
+game.onclick = () => {
+    game.requestPointerLock();
+    axisModeIndex = (axisModeIndex + 1) % AXIS_MODES.length;
+    activeAxes = AXIS_MODES[axisModeIndex];
+};
+
+window.addEventListener("mousemove", e => {
+    if (document.pointerLockElement !== game) return;
+
+    const yaw   = quatFromAxisAngle({x:0,y:1,z:0}, -e.movementX * camera.sensitivity);
+    const pitch = quatFromAxisAngle({x:1,y:0,z:0}, -e.movementY * camera.sensitivity);
+
+    camera.rotation = quatMultiply(yaw, camera.rotation);
+    camera.rotation = quatMultiply(camera.rotation, pitch);
+});
diff --git a/rendered.html b/rendered.html
new file mode 100644 (file)
index 0000000..45d3678
--- /dev/null
@@ -0,0 +1,1538 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta http-equiv="X-UA-Compatible" content="IE=edge">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<meta name="generator" content="Asciidoctor 2.0.20">
+<title>Basic 3D Example</title>
+<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400,700">
+<style>
+/*! Asciidoctor default stylesheet | MIT License | https://asciidoctor.org */
+/* Uncomment the following line when using as a custom stylesheet */
+/* @import "https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400,700"; */
+html{font-family:sans-serif;-webkit-text-size-adjust:100%}
+a{background:none}
+a:focus{outline:thin dotted}
+a:active,a:hover{outline:0}
+h1{font-size:2em;margin:.67em 0}
+b,strong{font-weight:bold}
+abbr{font-size:.9em}
+abbr[title]{cursor:help;border-bottom:1px dotted #dddddf;text-decoration:none}
+dfn{font-style:italic}
+hr{height:0}
+mark{background:#ff0;color:#000}
+code,kbd,pre,samp{font-family:monospace;font-size:1em}
+pre{white-space:pre-wrap}
+q{quotes:"\201C" "\201D" "\2018" "\2019"}
+small{font-size:80%}
+sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
+sup{top:-.5em}
+sub{bottom:-.25em}
+img{border:0}
+svg:not(:root){overflow:hidden}
+figure{margin:0}
+audio,video{display:inline-block}
+audio:not([controls]){display:none;height:0}
+fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}
+legend{border:0;padding:0}
+button,input,select,textarea{font-family:inherit;font-size:100%;margin:0}
+button,input{line-height:normal}
+button,select{text-transform:none}
+button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}
+button[disabled],html input[disabled]{cursor:default}
+input[type=checkbox],input[type=radio]{padding:0}
+button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}
+textarea{overflow:auto;vertical-align:top}
+table{border-collapse:collapse;border-spacing:0}
+*,::before,::after{box-sizing:border-box}
+html,body{font-size:100%}
+body{background:#fff;color:rgba(0,0,0,.8);padding:0;margin:0;font-family:"Noto Serif","DejaVu Serif",serif;line-height:1;position:relative;cursor:auto;-moz-tab-size:4;-o-tab-size:4;tab-size:4;word-wrap:anywhere;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased}
+a:hover{cursor:pointer}
+img,object,embed{max-width:100%;height:auto}
+object,embed{height:100%}
+img{-ms-interpolation-mode:bicubic}
+.left{float:left!important}
+.right{float:right!important}
+.text-left{text-align:left!important}
+.text-right{text-align:right!important}
+.text-center{text-align:center!important}
+.text-justify{text-align:justify!important}
+.hide{display:none}
+img,object,svg{display:inline-block;vertical-align:middle}
+textarea{height:auto;min-height:50px}
+select{width:100%}
+.subheader,.admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{line-height:1.45;color:#7a2518;font-weight:400;margin-top:0;margin-bottom:.25em}
+div,dl,dt,dd,ul,ol,li,h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6,pre,form,p,blockquote,th,td{margin:0;padding:0}
+a{color:#2156a5;text-decoration:underline;line-height:inherit}
+a:hover,a:focus{color:#1d4b8f}
+a img{border:0}
+p{line-height:1.6;margin-bottom:1.25em;text-rendering:optimizeLegibility}
+p aside{font-size:.875em;line-height:1.35;font-style:italic}
+h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{font-family:"Open Sans","DejaVu Sans",sans-serif;font-weight:300;font-style:normal;color:#ba3925;text-rendering:optimizeLegibility;margin-top:1em;margin-bottom:.5em;line-height:1.0125em}
+h1 small,h2 small,h3 small,#toctitle small,.sidebarblock>.content>.title small,h4 small,h5 small,h6 small{font-size:60%;color:#e99b8f;line-height:0}
+h1{font-size:2.125em}
+h2{font-size:1.6875em}
+h3,#toctitle,.sidebarblock>.content>.title{font-size:1.375em}
+h4,h5{font-size:1.125em}
+h6{font-size:1em}
+hr{border:solid #dddddf;border-width:1px 0 0;clear:both;margin:1.25em 0 1.1875em}
+em,i{font-style:italic;line-height:inherit}
+strong,b{font-weight:bold;line-height:inherit}
+small{font-size:60%;line-height:inherit}
+code{font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;font-weight:400;color:rgba(0,0,0,.9)}
+ul,ol,dl{line-height:1.6;margin-bottom:1.25em;list-style-position:outside;font-family:inherit}
+ul,ol{margin-left:1.5em}
+ul li ul,ul li ol{margin-left:1.25em;margin-bottom:0}
+ul.circle{list-style-type:circle}
+ul.disc{list-style-type:disc}
+ul.square{list-style-type:square}
+ul.circle ul:not([class]),ul.disc ul:not([class]),ul.square ul:not([class]){list-style:inherit}
+ol li ul,ol li ol{margin-left:1.25em;margin-bottom:0}
+dl dt{margin-bottom:.3125em;font-weight:bold}
+dl dd{margin-bottom:1.25em}
+blockquote{margin:0 0 1.25em;padding:.5625em 1.25em 0 1.1875em;border-left:1px solid #ddd}
+blockquote,blockquote p{line-height:1.6;color:rgba(0,0,0,.85)}
+@media screen and (min-width:768px){h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2}
+h1{font-size:2.75em}
+h2{font-size:2.3125em}
+h3,#toctitle,.sidebarblock>.content>.title{font-size:1.6875em}
+h4{font-size:1.4375em}}
+table{background:#fff;margin-bottom:1.25em;border:1px solid #dedede;word-wrap:normal}
+table thead,table tfoot{background:#f7f8f7}
+table thead tr th,table thead tr td,table tfoot tr th,table tfoot tr td{padding:.5em .625em .625em;font-size:inherit;color:rgba(0,0,0,.8);text-align:left}
+table tr th,table tr td{padding:.5625em .625em;font-size:inherit;color:rgba(0,0,0,.8)}
+table tr.even,table tr.alt{background:#f8f8f7}
+table thead tr th,table tfoot tr th,table tbody tr td,table tr td,table tfoot tr td{line-height:1.6}
+h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2;word-spacing:-.05em}
+h1 strong,h2 strong,h3 strong,#toctitle strong,.sidebarblock>.content>.title strong,h4 strong,h5 strong,h6 strong{font-weight:400}
+.center{margin-left:auto;margin-right:auto}
+.stretch{width:100%}
+.clearfix::before,.clearfix::after,.float-group::before,.float-group::after{content:" ";display:table}
+.clearfix::after,.float-group::after{clear:both}
+:not(pre).nobreak{word-wrap:normal}
+:not(pre).nowrap{white-space:nowrap}
+:not(pre).pre-wrap{white-space:pre-wrap}
+:not(pre):not([class^=L])>code{font-size:.9375em;font-style:normal!important;letter-spacing:0;padding:.1em .5ex;word-spacing:-.15em;background:#f7f7f8;border-radius:4px;line-height:1.45;text-rendering:optimizeSpeed}
+pre{color:rgba(0,0,0,.9);font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;line-height:1.45;text-rendering:optimizeSpeed}
+pre code,pre pre{color:inherit;font-size:inherit;line-height:inherit}
+pre>code{display:block}
+pre.nowrap,pre.nowrap pre{white-space:pre;word-wrap:normal}
+em em{font-style:normal}
+strong strong{font-weight:400}
+.keyseq{color:rgba(51,51,51,.8)}
+kbd{font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;display:inline-block;color:rgba(0,0,0,.8);font-size:.65em;line-height:1.45;background:#f7f7f7;border:1px solid #ccc;border-radius:3px;box-shadow:0 1px 0 rgba(0,0,0,.2),inset 0 0 0 .1em #fff;margin:0 .15em;padding:.2em .5em;vertical-align:middle;position:relative;top:-.1em;white-space:nowrap}
+.keyseq kbd:first-child{margin-left:0}
+.keyseq kbd:last-child{margin-right:0}
+.menuseq,.menuref{color:#000}
+.menuseq b:not(.caret),.menuref{font-weight:inherit}
+.menuseq{word-spacing:-.02em}
+.menuseq b.caret{font-size:1.25em;line-height:.8}
+.menuseq i.caret{font-weight:bold;text-align:center;width:.45em}
+b.button::before,b.button::after{position:relative;top:-1px;font-weight:400}
+b.button::before{content:"[";padding:0 3px 0 2px}
+b.button::after{content:"]";padding:0 2px 0 3px}
+p a>code:hover{color:rgba(0,0,0,.9)}
+#header,#content,#footnotes,#footer{width:100%;margin:0 auto;max-width:62.5em;*zoom:1;position:relative;padding-left:.9375em;padding-right:.9375em}
+#header::before,#header::after,#content::before,#content::after,#footnotes::before,#footnotes::after,#footer::before,#footer::after{content:" ";display:table}
+#header::after,#content::after,#footnotes::after,#footer::after{clear:both}
+#content{margin-top:1.25em}
+#content::before{content:none}
+#header>h1:first-child{color:rgba(0,0,0,.85);margin-top:2.25rem;margin-bottom:0}
+#header>h1:first-child+#toc{margin-top:8px;border-top:1px solid #dddddf}
+#header>h1:only-child,body.toc2 #header>h1:nth-last-child(2){border-bottom:1px solid #dddddf;padding-bottom:8px}
+#header .details{border-bottom:1px solid #dddddf;line-height:1.45;padding-top:.25em;padding-bottom:.25em;padding-left:.25em;color:rgba(0,0,0,.6);display:flex;flex-flow:row wrap}
+#header .details span:first-child{margin-left:-.125em}
+#header .details span.email a{color:rgba(0,0,0,.85)}
+#header .details br{display:none}
+#header .details br+span::before{content:"\00a0\2013\00a0"}
+#header .details br+span.author::before{content:"\00a0\22c5\00a0";color:rgba(0,0,0,.85)}
+#header .details br+span#revremark::before{content:"\00a0|\00a0"}
+#header #revnumber{text-transform:capitalize}
+#header #revnumber::after{content:"\00a0"}
+#content>h1:first-child:not([class]){color:rgba(0,0,0,.85);border-bottom:1px solid #dddddf;padding-bottom:8px;margin-top:0;padding-top:1rem;margin-bottom:1.25rem}
+#toc{border-bottom:1px solid #e7e7e9;padding-bottom:.5em}
+#toc>ul{margin-left:.125em}
+#toc ul.sectlevel0>li>a{font-style:italic}
+#toc ul.sectlevel0 ul.sectlevel1{margin:.5em 0}
+#toc ul{font-family:"Open Sans","DejaVu Sans",sans-serif;list-style-type:none}
+#toc li{line-height:1.3334;margin-top:.3334em}
+#toc a{text-decoration:none}
+#toc a:active{text-decoration:underline}
+#toctitle{color:#7a2518;font-size:1.2em}
+@media screen and (min-width:768px){#toctitle{font-size:1.375em}
+body.toc2{padding-left:15em;padding-right:0}
+#toc.toc2{margin-top:0!important;background:#f8f8f7;position:fixed;width:15em;left:0;top:0;border-right:1px solid #e7e7e9;border-top-width:0!important;border-bottom-width:0!important;z-index:1000;padding:1.25em 1em;height:100%;overflow:auto}
+#toc.toc2 #toctitle{margin-top:0;margin-bottom:.8rem;font-size:1.2em}
+#toc.toc2>ul{font-size:.9em;margin-bottom:0}
+#toc.toc2 ul ul{margin-left:0;padding-left:1em}
+#toc.toc2 ul.sectlevel0 ul.sectlevel1{padding-left:0;margin-top:.5em;margin-bottom:.5em}
+body.toc2.toc-right{padding-left:0;padding-right:15em}
+body.toc2.toc-right #toc.toc2{border-right-width:0;border-left:1px solid #e7e7e9;left:auto;right:0}}
+@media screen and (min-width:1280px){body.toc2{padding-left:20em;padding-right:0}
+#toc.toc2{width:20em}
+#toc.toc2 #toctitle{font-size:1.375em}
+#toc.toc2>ul{font-size:.95em}
+#toc.toc2 ul ul{padding-left:1.25em}
+body.toc2.toc-right{padding-left:0;padding-right:20em}}
+#content #toc{border:1px solid #e0e0dc;margin-bottom:1.25em;padding:1.25em;background:#f8f8f7;border-radius:4px}
+#content #toc>:first-child{margin-top:0}
+#content #toc>:last-child{margin-bottom:0}
+#footer{max-width:none;background:rgba(0,0,0,.8);padding:1.25em}
+#footer-text{color:hsla(0,0%,100%,.8);line-height:1.44}
+#content{margin-bottom:.625em}
+.sect1{padding-bottom:.625em}
+@media screen and (min-width:768px){#content{margin-bottom:1.25em}
+.sect1{padding-bottom:1.25em}}
+.sect1:last-child{padding-bottom:0}
+.sect1+.sect1{border-top:1px solid #e7e7e9}
+#content h1>a.anchor,h2>a.anchor,h3>a.anchor,#toctitle>a.anchor,.sidebarblock>.content>.title>a.anchor,h4>a.anchor,h5>a.anchor,h6>a.anchor{position:absolute;z-index:1001;width:1.5ex;margin-left:-1.5ex;display:block;text-decoration:none!important;visibility:hidden;text-align:center;font-weight:400}
+#content h1>a.anchor::before,h2>a.anchor::before,h3>a.anchor::before,#toctitle>a.anchor::before,.sidebarblock>.content>.title>a.anchor::before,h4>a.anchor::before,h5>a.anchor::before,h6>a.anchor::before{content:"\00A7";font-size:.85em;display:block;padding-top:.1em}
+#content h1:hover>a.anchor,#content h1>a.anchor:hover,h2:hover>a.anchor,h2>a.anchor:hover,h3:hover>a.anchor,#toctitle:hover>a.anchor,.sidebarblock>.content>.title:hover>a.anchor,h3>a.anchor:hover,#toctitle>a.anchor:hover,.sidebarblock>.content>.title>a.anchor:hover,h4:hover>a.anchor,h4>a.anchor:hover,h5:hover>a.anchor,h5>a.anchor:hover,h6:hover>a.anchor,h6>a.anchor:hover{visibility:visible}
+#content h1>a.link,h2>a.link,h3>a.link,#toctitle>a.link,.sidebarblock>.content>.title>a.link,h4>a.link,h5>a.link,h6>a.link{color:#ba3925;text-decoration:none}
+#content h1>a.link:hover,h2>a.link:hover,h3>a.link:hover,#toctitle>a.link:hover,.sidebarblock>.content>.title>a.link:hover,h4>a.link:hover,h5>a.link:hover,h6>a.link:hover{color:#a53221}
+details,.audioblock,.imageblock,.literalblock,.listingblock,.stemblock,.videoblock{margin-bottom:1.25em}
+details{margin-left:1.25rem}
+details>summary{cursor:pointer;display:block;position:relative;line-height:1.6;margin-bottom:.625rem;outline:none;-webkit-tap-highlight-color:transparent}
+details>summary::-webkit-details-marker{display:none}
+details>summary::before{content:"";border:solid transparent;border-left:solid;border-width:.3em 0 .3em .5em;position:absolute;top:.5em;left:-1.25rem;transform:translateX(15%)}
+details[open]>summary::before{border:solid transparent;border-top:solid;border-width:.5em .3em 0;transform:translateY(15%)}
+details>summary::after{content:"";width:1.25rem;height:1em;position:absolute;top:.3em;left:-1.25rem}
+.admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{text-rendering:optimizeLegibility;text-align:left;font-family:"Noto Serif","DejaVu Serif",serif;font-size:1rem;font-style:italic}
+table.tableblock.fit-content>caption.title{white-space:nowrap;width:0}
+.paragraph.lead>p,#preamble>.sectionbody>[class=paragraph]:first-of-type p{font-size:1.21875em;line-height:1.6;color:rgba(0,0,0,.85)}
+.admonitionblock>table{border-collapse:separate;border:0;background:none;width:100%}
+.admonitionblock>table td.icon{text-align:center;width:80px}
+.admonitionblock>table td.icon img{max-width:none}
+.admonitionblock>table td.icon .title{font-weight:bold;font-family:"Open Sans","DejaVu Sans",sans-serif;text-transform:uppercase}
+.admonitionblock>table td.content{padding-left:1.125em;padding-right:1.25em;border-left:1px solid #dddddf;color:rgba(0,0,0,.6);word-wrap:anywhere}
+.admonitionblock>table td.content>:last-child>:last-child{margin-bottom:0}
+.exampleblock>.content{border:1px solid #e6e6e6;margin-bottom:1.25em;padding:1.25em;background:#fff;border-radius:4px}
+.sidebarblock{border:1px solid #dbdbd6;margin-bottom:1.25em;padding:1.25em;background:#f3f3f2;border-radius:4px}
+.sidebarblock>.content>.title{color:#7a2518;margin-top:0;text-align:center}
+.exampleblock>.content>:first-child,.sidebarblock>.content>:first-child{margin-top:0}
+.exampleblock>.content>:last-child,.exampleblock>.content>:last-child>:last-child,.exampleblock>.content .olist>ol>li:last-child>:last-child,.exampleblock>.content .ulist>ul>li:last-child>:last-child,.exampleblock>.content .qlist>ol>li:last-child>:last-child,.sidebarblock>.content>:last-child,.sidebarblock>.content>:last-child>:last-child,.sidebarblock>.content .olist>ol>li:last-child>:last-child,.sidebarblock>.content .ulist>ul>li:last-child>:last-child,.sidebarblock>.content .qlist>ol>li:last-child>:last-child{margin-bottom:0}
+.literalblock pre,.listingblock>.content>pre{border-radius:4px;overflow-x:auto;padding:1em;font-size:.8125em}
+@media screen and (min-width:768px){.literalblock pre,.listingblock>.content>pre{font-size:.90625em}}
+@media screen and (min-width:1280px){.literalblock pre,.listingblock>.content>pre{font-size:1em}}
+.literalblock pre,.listingblock>.content>pre:not(.highlight),.listingblock>.content>pre[class=highlight],.listingblock>.content>pre[class^="highlight "]{background:#f7f7f8}
+.literalblock.output pre{color:#f7f7f8;background:rgba(0,0,0,.9)}
+.listingblock>.content{position:relative}
+.listingblock code[data-lang]::before{display:none;content:attr(data-lang);position:absolute;font-size:.75em;top:.425rem;right:.5rem;line-height:1;text-transform:uppercase;color:inherit;opacity:.5}
+.listingblock:hover code[data-lang]::before{display:block}
+.listingblock.terminal pre .command::before{content:attr(data-prompt);padding-right:.5em;color:inherit;opacity:.5}
+.listingblock.terminal pre .command:not([data-prompt])::before{content:"$"}
+.listingblock pre.highlightjs{padding:0}
+.listingblock pre.highlightjs>code{padding:1em;border-radius:4px}
+.listingblock pre.prettyprint{border-width:0}
+.prettyprint{background:#f7f7f8}
+pre.prettyprint .linenums{line-height:1.45;margin-left:2em}
+pre.prettyprint li{background:none;list-style-type:inherit;padding-left:0}
+pre.prettyprint li code[data-lang]::before{opacity:1}
+pre.prettyprint li:not(:first-child) code[data-lang]::before{display:none}
+table.linenotable{border-collapse:separate;border:0;margin-bottom:0;background:none}
+table.linenotable td[class]{color:inherit;vertical-align:top;padding:0;line-height:inherit;white-space:normal}
+table.linenotable td.code{padding-left:.75em}
+table.linenotable td.linenos,pre.pygments .linenos{border-right:1px solid;opacity:.35;padding-right:.5em;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
+pre.pygments span.linenos{display:inline-block;margin-right:.75em}
+.quoteblock{margin:0 1em 1.25em 1.5em;display:table}
+.quoteblock:not(.excerpt)>.title{margin-left:-1.5em;margin-bottom:.75em}
+.quoteblock blockquote,.quoteblock p{color:rgba(0,0,0,.85);font-size:1.15rem;line-height:1.75;word-spacing:.1em;letter-spacing:0;font-style:italic;text-align:justify}
+.quoteblock blockquote{margin:0;padding:0;border:0}
+.quoteblock blockquote::before{content:"\201c";float:left;font-size:2.75em;font-weight:bold;line-height:.6em;margin-left:-.6em;color:#7a2518;text-shadow:0 1px 2px rgba(0,0,0,.1)}
+.quoteblock blockquote>.paragraph:last-child p{margin-bottom:0}
+.quoteblock .attribution{margin-top:.75em;margin-right:.5ex;text-align:right}
+.verseblock{margin:0 1em 1.25em}
+.verseblock pre{font-family:"Open Sans","DejaVu Sans",sans-serif;font-size:1.15rem;color:rgba(0,0,0,.85);font-weight:300;text-rendering:optimizeLegibility}
+.verseblock pre strong{font-weight:400}
+.verseblock .attribution{margin-top:1.25rem;margin-left:.5ex}
+.quoteblock .attribution,.verseblock .attribution{font-size:.9375em;line-height:1.45;font-style:italic}
+.quoteblock .attribution br,.verseblock .attribution br{display:none}
+.quoteblock .attribution cite,.verseblock .attribution cite{display:block;letter-spacing:-.025em;color:rgba(0,0,0,.6)}
+.quoteblock.abstract blockquote::before,.quoteblock.excerpt blockquote::before,.quoteblock .quoteblock blockquote::before{display:none}
+.quoteblock.abstract blockquote,.quoteblock.abstract p,.quoteblock.excerpt blockquote,.quoteblock.excerpt p,.quoteblock .quoteblock blockquote,.quoteblock .quoteblock p{line-height:1.6;word-spacing:0}
+.quoteblock.abstract{margin:0 1em 1.25em;display:block}
+.quoteblock.abstract>.title{margin:0 0 .375em;font-size:1.15em;text-align:center}
+.quoteblock.excerpt>blockquote,.quoteblock .quoteblock{padding:0 0 .25em 1em;border-left:.25em solid #dddddf}
+.quoteblock.excerpt,.quoteblock .quoteblock{margin-left:0}
+.quoteblock.excerpt blockquote,.quoteblock.excerpt p,.quoteblock .quoteblock blockquote,.quoteblock .quoteblock p{color:inherit;font-size:1.0625rem}
+.quoteblock.excerpt .attribution,.quoteblock .quoteblock .attribution{color:inherit;font-size:.85rem;text-align:left;margin-right:0}
+p.tableblock:last-child{margin-bottom:0}
+td.tableblock>.content{margin-bottom:1.25em;word-wrap:anywhere}
+td.tableblock>.content>:last-child{margin-bottom:-1.25em}
+table.tableblock,th.tableblock,td.tableblock{border:0 solid #dedede}
+table.grid-all>*>tr>*{border-width:1px}
+table.grid-cols>*>tr>*{border-width:0 1px}
+table.grid-rows>*>tr>*{border-width:1px 0}
+table.frame-all{border-width:1px}
+table.frame-ends{border-width:1px 0}
+table.frame-sides{border-width:0 1px}
+table.frame-none>colgroup+*>:first-child>*,table.frame-sides>colgroup+*>:first-child>*{border-top-width:0}
+table.frame-none>:last-child>:last-child>*,table.frame-sides>:last-child>:last-child>*{border-bottom-width:0}
+table.frame-none>*>tr>:first-child,table.frame-ends>*>tr>:first-child{border-left-width:0}
+table.frame-none>*>tr>:last-child,table.frame-ends>*>tr>:last-child{border-right-width:0}
+table.stripes-all>*>tr,table.stripes-odd>*>tr:nth-of-type(odd),table.stripes-even>*>tr:nth-of-type(even),table.stripes-hover>*>tr:hover{background:#f8f8f7}
+th.halign-left,td.halign-left{text-align:left}
+th.halign-right,td.halign-right{text-align:right}
+th.halign-center,td.halign-center{text-align:center}
+th.valign-top,td.valign-top{vertical-align:top}
+th.valign-bottom,td.valign-bottom{vertical-align:bottom}
+th.valign-middle,td.valign-middle{vertical-align:middle}
+table thead th,table tfoot th{font-weight:bold}
+tbody tr th{background:#f7f8f7}
+tbody tr th,tbody tr th p,tfoot tr th,tfoot tr th p{color:rgba(0,0,0,.8);font-weight:bold}
+p.tableblock>code:only-child{background:none;padding:0}
+p.tableblock{font-size:1em}
+ol{margin-left:1.75em}
+ul li ol{margin-left:1.5em}
+dl dd{margin-left:1.125em}
+dl dd:last-child,dl dd:last-child>:last-child{margin-bottom:0}
+li p,ul dd,ol dd,.olist .olist,.ulist .ulist,.ulist .olist,.olist .ulist{margin-bottom:.625em}
+ul.checklist,ul.none,ol.none,ul.no-bullet,ol.no-bullet,ol.unnumbered,ul.unstyled,ol.unstyled{list-style-type:none}
+ul.no-bullet,ol.no-bullet,ol.unnumbered{margin-left:.625em}
+ul.unstyled,ol.unstyled{margin-left:0}
+li>p:empty:only-child::before{content:"";display:inline-block}
+ul.checklist>li>p:first-child{margin-left:-1em}
+ul.checklist>li>p:first-child>.fa-square-o:first-child,ul.checklist>li>p:first-child>.fa-check-square-o:first-child{width:1.25em;font-size:.8em;position:relative;bottom:.125em}
+ul.checklist>li>p:first-child>input[type=checkbox]:first-child{margin-right:.25em}
+ul.inline{display:flex;flex-flow:row wrap;list-style:none;margin:0 0 .625em -1.25em}
+ul.inline>li{margin-left:1.25em}
+.unstyled dl dt{font-weight:400;font-style:normal}
+ol.arabic{list-style-type:decimal}
+ol.decimal{list-style-type:decimal-leading-zero}
+ol.loweralpha{list-style-type:lower-alpha}
+ol.upperalpha{list-style-type:upper-alpha}
+ol.lowerroman{list-style-type:lower-roman}
+ol.upperroman{list-style-type:upper-roman}
+ol.lowergreek{list-style-type:lower-greek}
+.hdlist>table,.colist>table{border:0;background:none}
+.hdlist>table>tbody>tr,.colist>table>tbody>tr{background:none}
+td.hdlist1,td.hdlist2{vertical-align:top;padding:0 .625em}
+td.hdlist1{font-weight:bold;padding-bottom:1.25em}
+td.hdlist2{word-wrap:anywhere}
+.literalblock+.colist,.listingblock+.colist{margin-top:-.5em}
+.colist td:not([class]):first-child{padding:.4em .75em 0;line-height:1;vertical-align:top}
+.colist td:not([class]):first-child img{max-width:none}
+.colist td:not([class]):last-child{padding:.25em 0}
+.thumb,.th{line-height:0;display:inline-block;border:4px solid #fff;box-shadow:0 0 0 1px #ddd}
+.imageblock.left{margin:.25em .625em 1.25em 0}
+.imageblock.right{margin:.25em 0 1.25em .625em}
+.imageblock>.title{margin-bottom:0}
+.imageblock.thumb,.imageblock.th{border-width:6px}
+.imageblock.thumb>.title,.imageblock.th>.title{padding:0 .125em}
+.image.left,.image.right{margin-top:.25em;margin-bottom:.25em;display:inline-block;line-height:0}
+.image.left{margin-right:.625em}
+.image.right{margin-left:.625em}
+a.image{text-decoration:none;display:inline-block}
+a.image object{pointer-events:none}
+sup.footnote,sup.footnoteref{font-size:.875em;position:static;vertical-align:super}
+sup.footnote a,sup.footnoteref a{text-decoration:none}
+sup.footnote a:active,sup.footnoteref a:active{text-decoration:underline}
+#footnotes{padding-top:.75em;padding-bottom:.75em;margin-bottom:.625em}
+#footnotes hr{width:20%;min-width:6.25em;margin:-.25em 0 .75em;border-width:1px 0 0}
+#footnotes .footnote{padding:0 .375em 0 .225em;line-height:1.3334;font-size:.875em;margin-left:1.2em;margin-bottom:.2em}
+#footnotes .footnote a:first-of-type{font-weight:bold;text-decoration:none;margin-left:-1.05em}
+#footnotes .footnote:last-of-type{margin-bottom:0}
+#content #footnotes{margin-top:-.625em;margin-bottom:0;padding:.75em 0}
+div.unbreakable{page-break-inside:avoid}
+.big{font-size:larger}
+.small{font-size:smaller}
+.underline{text-decoration:underline}
+.overline{text-decoration:overline}
+.line-through{text-decoration:line-through}
+.aqua{color:#00bfbf}
+.aqua-background{background:#00fafa}
+.black{color:#000}
+.black-background{background:#000}
+.blue{color:#0000bf}
+.blue-background{background:#0000fa}
+.fuchsia{color:#bf00bf}
+.fuchsia-background{background:#fa00fa}
+.gray{color:#606060}
+.gray-background{background:#7d7d7d}
+.green{color:#006000}
+.green-background{background:#007d00}
+.lime{color:#00bf00}
+.lime-background{background:#00fa00}
+.maroon{color:#600000}
+.maroon-background{background:#7d0000}
+.navy{color:#000060}
+.navy-background{background:#00007d}
+.olive{color:#606000}
+.olive-background{background:#7d7d00}
+.purple{color:#600060}
+.purple-background{background:#7d007d}
+.red{color:#bf0000}
+.red-background{background:#fa0000}
+.silver{color:#909090}
+.silver-background{background:#bcbcbc}
+.teal{color:#006060}
+.teal-background{background:#007d7d}
+.white{color:#bfbfbf}
+.white-background{background:#fafafa}
+.yellow{color:#bfbf00}
+.yellow-background{background:#fafa00}
+span.icon>.fa{cursor:default}
+a span.icon>.fa{cursor:inherit}
+.admonitionblock td.icon [class^="fa icon-"]{font-size:2.5em;text-shadow:1px 1px 2px rgba(0,0,0,.5);cursor:default}
+.admonitionblock td.icon .icon-note::before{content:"\f05a";color:#19407c}
+.admonitionblock td.icon .icon-tip::before{content:"\f0eb";text-shadow:1px 1px 2px rgba(155,155,0,.8);color:#111}
+.admonitionblock td.icon .icon-warning::before{content:"\f071";color:#bf6900}
+.admonitionblock td.icon .icon-caution::before{content:"\f06d";color:#bf3400}
+.admonitionblock td.icon .icon-important::before{content:"\f06a";color:#bf0000}
+.conum[data-value]{display:inline-block;color:#fff!important;background:rgba(0,0,0,.8);border-radius:50%;text-align:center;font-size:.75em;width:1.67em;height:1.67em;line-height:1.67em;font-family:"Open Sans","DejaVu Sans",sans-serif;font-style:normal;font-weight:bold}
+.conum[data-value] *{color:#fff!important}
+.conum[data-value]+b{display:none}
+.conum[data-value]::after{content:attr(data-value)}
+pre .conum[data-value]{position:relative;top:-.125em}
+b.conum *{color:inherit!important}
+.conum:not([data-value]):empty{display:none}
+dt,th.tableblock,td.content,div.footnote{text-rendering:optimizeLegibility}
+h1,h2,p,td.content,span.alt,summary{letter-spacing:-.01em}
+p strong,td.content strong,div.footnote strong{letter-spacing:-.005em}
+p,blockquote,dt,td.content,td.hdlist1,span.alt,summary{font-size:1.0625rem}
+p{margin-bottom:1.25rem}
+.sidebarblock p,.sidebarblock dt,.sidebarblock td.content,p.tableblock{font-size:1em}
+.exampleblock>.content{background:#fffef7;border-color:#e0e0dc;box-shadow:0 1px 4px #e0e0dc}
+.print-only{display:none!important}
+@page{margin:1.25cm .75cm}
+@media print{*{box-shadow:none!important;text-shadow:none!important}
+html{font-size:80%}
+a{color:inherit!important;text-decoration:underline!important}
+a.bare,a[href^="#"],a[href^="mailto:"]{text-decoration:none!important}
+a[href^="http:"]:not(.bare)::after,a[href^="https:"]:not(.bare)::after{content:"(" attr(href) ")";display:inline-block;font-size:.875em;padding-left:.25em}
+abbr[title]{border-bottom:1px dotted}
+abbr[title]::after{content:" (" attr(title) ")"}
+pre,blockquote,tr,img,object,svg{page-break-inside:avoid}
+thead{display:table-header-group}
+svg{max-width:100%}
+p,blockquote,dt,td.content{font-size:1em;orphans:3;widows:3}
+h2,h3,#toctitle,.sidebarblock>.content>.title{page-break-after:avoid}
+#header,#content,#footnotes,#footer{max-width:none}
+#toc,.sidebarblock,.exampleblock>.content{background:none!important}
+#toc{border-bottom:1px solid #dddddf!important;padding-bottom:0!important}
+body.book #header{text-align:center}
+body.book #header>h1:first-child{border:0!important;margin:2.5em 0 1em}
+body.book #header .details{border:0!important;display:block;padding:0!important}
+body.book #header .details span:first-child{margin-left:0!important}
+body.book #header .details br{display:block}
+body.book #header .details br+span::before{content:none!important}
+body.book #toc{border:0!important;text-align:left!important;padding:0!important;margin:0!important}
+body.book #toc,body.book #preamble,body.book h1.sect0,body.book .sect1>h2{page-break-before:always}
+.listingblock code[data-lang]::before{display:block}
+#footer{padding:0 .9375em}
+.hide-on-print{display:none!important}
+.print-only{display:block!important}
+.hide-for-print{display:none!important}
+.show-for-print{display:inherit!important}}
+@media amzn-kf8,print{#header>h1:first-child{margin-top:1.25rem}
+.sect1{padding:0!important}
+.sect1+.sect1{border:0}
+#footer{background:none}
+#footer-text{color:rgba(0,0,0,.6);font-size:.9em}}
+@media amzn-kf8{#header,#content,#footnotes,#footer{padding:0}}
+</style>
+<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
+<style>
+pre.pygments .hll { background-color: #49483e }
+pre.pygments { background: #272822; color: #F8F8F2 }
+pre.pygments .tok-c { color: #959077 } /* Comment */
+pre.pygments .tok-err { color: #ED007E; background-color: #1E0010 } /* Error */
+pre.pygments .tok-esc { color: #F8F8F2 } /* Escape */
+pre.pygments .tok-g { color: #F8F8F2 } /* Generic */
+pre.pygments .tok-k { color: #66D9EF } /* Keyword */
+pre.pygments .tok-l { color: #AE81FF } /* Literal */
+pre.pygments .tok-n { color: #F8F8F2 } /* Name */
+pre.pygments .tok-o { color: #FF4689 } /* Operator */
+pre.pygments .tok-x { color: #F8F8F2 } /* Other */
+pre.pygments .tok-p { color: #F8F8F2 } /* Punctuation */
+pre.pygments .tok-ch { color: #959077 } /* Comment.Hashbang */
+pre.pygments .tok-cm { color: #959077 } /* Comment.Multiline */
+pre.pygments .tok-cp { color: #959077 } /* Comment.Preproc */
+pre.pygments .tok-cpf { color: #959077 } /* Comment.PreprocFile */
+pre.pygments .tok-c1 { color: #959077 } /* Comment.Single */
+pre.pygments .tok-cs { color: #959077 } /* Comment.Special */
+pre.pygments .tok-gd { color: #FF4689 } /* Generic.Deleted */
+pre.pygments .tok-ge { color: #F8F8F2; font-style: italic } /* Generic.Emph */
+pre.pygments .tok-ges { color: #F8F8F2; font-weight: bold; font-style: italic } /* Generic.EmphStrong */
+pre.pygments .tok-gr { color: #F8F8F2 } /* Generic.Error */
+pre.pygments .tok-gh { color: #F8F8F2 } /* Generic.Heading */
+pre.pygments .tok-gi { color: #A6E22E } /* Generic.Inserted */
+pre.pygments .tok-go { color: #66D9EF } /* Generic.Output */
+pre.pygments .tok-gp { color: #FF4689; font-weight: bold } /* Generic.Prompt */
+pre.pygments .tok-gs { color: #F8F8F2; font-weight: bold } /* Generic.Strong */
+pre.pygments .tok-gu { color: #959077 } /* Generic.Subheading */
+pre.pygments .tok-gt { color: #F8F8F2 } /* Generic.Traceback */
+pre.pygments .tok-kc { color: #66D9EF } /* Keyword.Constant */
+pre.pygments .tok-kd { color: #66D9EF } /* Keyword.Declaration */
+pre.pygments .tok-kn { color: #FF4689 } /* Keyword.Namespace */
+pre.pygments .tok-kp { color: #66D9EF } /* Keyword.Pseudo */
+pre.pygments .tok-kr { color: #66D9EF } /* Keyword.Reserved */
+pre.pygments .tok-kt { color: #66D9EF } /* Keyword.Type */
+pre.pygments .tok-ld { color: #E6DB74 } /* Literal.Date */
+pre.pygments .tok-m { color: #AE81FF } /* Literal.Number */
+pre.pygments .tok-s { color: #E6DB74 } /* Literal.String */
+pre.pygments .tok-na { color: #A6E22E } /* Name.Attribute */
+pre.pygments .tok-nb { color: #F8F8F2 } /* Name.Builtin */
+pre.pygments .tok-nc { color: #A6E22E } /* Name.Class */
+pre.pygments .tok-no { color: #66D9EF } /* Name.Constant */
+pre.pygments .tok-nd { color: #A6E22E } /* Name.Decorator */
+pre.pygments .tok-ni { color: #F8F8F2 } /* Name.Entity */
+pre.pygments .tok-ne { color: #A6E22E } /* Name.Exception */
+pre.pygments .tok-nf { color: #A6E22E } /* Name.Function */
+pre.pygments .tok-nl { color: #F8F8F2 } /* Name.Label */
+pre.pygments .tok-nn { color: #F8F8F2 } /* Name.Namespace */
+pre.pygments .tok-nx { color: #A6E22E } /* Name.Other */
+pre.pygments .tok-py { color: #F8F8F2 } /* Name.Property */
+pre.pygments .tok-nt { color: #FF4689 } /* Name.Tag */
+pre.pygments .tok-nv { color: #F8F8F2 } /* Name.Variable */
+pre.pygments .tok-ow { color: #FF4689 } /* Operator.Word */
+pre.pygments .tok-pm { color: #F8F8F2 } /* Punctuation.Marker */
+pre.pygments .tok-w { color: #F8F8F2 } /* Text.Whitespace */
+pre.pygments .tok-mb { color: #AE81FF } /* Literal.Number.Bin */
+pre.pygments .tok-mf { color: #AE81FF } /* Literal.Number.Float */
+pre.pygments .tok-mh { color: #AE81FF } /* Literal.Number.Hex */
+pre.pygments .tok-mi { color: #AE81FF } /* Literal.Number.Integer */
+pre.pygments .tok-mo { color: #AE81FF } /* Literal.Number.Oct */
+pre.pygments .tok-sa { color: #E6DB74 } /* Literal.String.Affix */
+pre.pygments .tok-sb { color: #E6DB74 } /* Literal.String.Backtick */
+pre.pygments .tok-sc { color: #E6DB74 } /* Literal.String.Char */
+pre.pygments .tok-dl { color: #E6DB74 } /* Literal.String.Delimiter */
+pre.pygments .tok-sd { color: #E6DB74 } /* Literal.String.Doc */
+pre.pygments .tok-s2 { color: #E6DB74 } /* Literal.String.Double */
+pre.pygments .tok-se { color: #AE81FF } /* Literal.String.Escape */
+pre.pygments .tok-sh { color: #E6DB74 } /* Literal.String.Heredoc */
+pre.pygments .tok-si { color: #E6DB74 } /* Literal.String.Interpol */
+pre.pygments .tok-sx { color: #E6DB74 } /* Literal.String.Other */
+pre.pygments .tok-sr { color: #E6DB74 } /* Literal.String.Regex */
+pre.pygments .tok-s1 { color: #E6DB74 } /* Literal.String.Single */
+pre.pygments .tok-ss { color: #E6DB74 } /* Literal.String.Symbol */
+pre.pygments .tok-bp { color: #F8F8F2 } /* Name.Builtin.Pseudo */
+pre.pygments .tok-fm { color: #A6E22E } /* Name.Function.Magic */
+pre.pygments .tok-vc { color: #F8F8F2 } /* Name.Variable.Class */
+pre.pygments .tok-vg { color: #F8F8F2 } /* Name.Variable.Global */
+pre.pygments .tok-vi { color: #F8F8F2 } /* Name.Variable.Instance */
+pre.pygments .tok-vm { color: #F8F8F2 } /* Name.Variable.Magic */
+pre.pygments .tok-il { color: #AE81FF } /* Literal.Number.Integer.Long */
+</style>
+<head>
+<!-- <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=DotGothic16"> -->
+<!-- <link rel="icon" type="image/x-icon" href="https://raw.githubusercontent.com/A-Larsen/math-notes/refs/heads/main/favicon.png"> -->
+<!-- <link rel="icon" type="image/png" href="N2DE_favicon.png"/> -->
+<!-- <link rel="preconnect" href="https://fonts.googleapis.com"> -->
+<!-- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> -->
+<!-- <link href="https://fonts.googleapis.com/css2?family=Abril+Fatface&display=swap" rel="stylesheet"> -->
+<style>
+
+/*--------------------------------------------------------------*/
+/*use '!important' to override default css used by asciidoctor*/
+/*--------------------------------------------------------------*/
+
+:root {
+    --gruvbox-black: #282828;
+    --gruvbox-black2: #1d2021;
+    --gruvbox-black3: #463f39;
+    --gruvbox-red: #fc7869;
+    --gruvbox-yellow: #fabd2f;
+    --gruvbox-white: #ebdbb2;
+    --gruvbox-white2: #a7a187;
+    --gruvbox-blue: #83a598;
+    --gruvbox-green: #b8bb26;
+    --gruvbox-bg1: #7c6f64;
+    --gruvbox-bg2: #665c54;
+
+}
+
+.title {
+  color: var(--gruvbox-red) !important;
+}
+
+blockquote {
+    color: var(--gruvbox-white) !important;
+}
+
+table {
+    margin: 0 auto !important;
+}
+
+.hdlist1 {
+  font-size: 1.3em;
+}
+
+cite {
+    color: var(--gruvbox-green) !important;
+}
+
+.tableblock {
+    background-color: inherit;
+    color: var(--gruvbox-white);
+    max-width: 95%;
+}
+
+.tableblock th {
+    background-color: var(--gruvbox-black3);
+    color: var(--gruvbox-white);
+    max-width: 97%;
+}
+
+code:not([data-lang]) {
+    background-color: var(--gruvbox-bg2) !important;
+    color: var(--gruvbox-white);
+    text-shadow: none;
+}
+
+.checklist {
+    text-align: left;
+    font-size: .8em;
+}
+
+.checklist li ul { margin-left: 3%;
+}
+
+#content, h1, pre {
+    border-radius: 8px !important;
+}
+
+#header, #content, #footnotes, #footer {
+  padding-left: 0 !important;
+  padding-right: 0 !important;
+}
+
+.api-type a:hover {
+    text-decoration: underline;
+}
+
+.api-type, .api-type a {
+    color: var(--gruvbox-green);
+    text-decoration: none;
+}
+
+pre {
+    border-style: solid !important;
+    border-color: white !important;
+    border-width: 2px !important;
+}
+
+h1 {
+   /* font-family: "DotGothic16", sans-serif !important;*/
+
+
+    background-color: var(--gruvbox-white2) !important;
+    color: var(--gruvbox-black) !important;
+}
+
+.admonitionblock > table td.content {
+    color: var(--gruvbox-white) !important;
+}
+
+/*
+html {
+    margin: 0;
+    height: 100%;
+    width: 100%;
+}
+
+
+*/
+/*html {*/
+
+body {
+    /*background: black url("background-image-space.jpeg") no-repeat fixed center;*/
+    /*background-color: black;*/
+    background-color: var(--gruvbox-black);
+    background-attachment: fixed;
+    background-size: cover;
+    overflow-y: scroll;
+    /*
+    margin: 0;
+    height: 100%;
+    width: 100%;
+    overflow: hidden;
+    */
+}
+
+
+/*
+body {
+    overflow-y: scroll;
+    background: rgba(0,0,0,0);
+    height: 100%;
+    margin: 2%;
+}
+*/
+
+#toc {
+    background-color: var(--gruvbox-black2) !important;
+    /*opacity: .9;*/
+}
+
+#toctitle {
+    color: var(--gruvbox-yellow);
+}
+
+a {
+    color: var(--gruvbox-bg1);
+}
+
+#content {
+    color: #ebdbb2;
+    background: var(--gruvbox-black);
+    /*opacity: .9;*/
+}
+
+#header > h1:nth-last-child(2) {
+    border-bottom: none !important;
+    background-color: rgba(0, 0, 0, .1);
+    padding-left: 2%;
+}
+
+.sect1 + .sect1 {
+  border-top: none !important;
+}
+
+.sect1 h2 {
+    background-size: cover;
+    background-attachment: fixed;
+    /*
+    border-bottom: 1px solid var(--gruvbox-white);
+    border-top: 1px solid var(--gruvbox-white);
+    */
+    padding: 10px;
+    text-align: left;
+    color: var(--gruvbox-red) !important;
+    /*
+    text-shadow: 1px 1px 1px var(--gruvbox-black);
+    text-shadow: 3px 3px 3px var(--gruvbox-black);
+    text-shadow: 5px 5px 5px var(--gruvbox-blue);
+    */
+}
+
+h3, h4, h5 {
+    /*
+    text-shadow: 1px 1px 1px var(--gruvbox-white);
+    */
+}
+
+h3 {
+  color: var(--gruvbox-red) !important;
+}
+
+h4 {
+  color: var(--gruvbox-yellow) !important;
+}
+
+h5 {
+  color: #83a598 !important;
+}
+
+h2 ~ *:not(h1) {
+  margin-left: .5em;
+}
+
+.pygments {
+    max-width: 97% !important;
+    white-space: pre;
+    overflow-x: scroll !important;
+}
+
+::before {
+    /*makes sure that the programming language that show up on hover is within
+      pre tag*/
+    padding-right: 3%;
+}
+
+@media screen and (max-width: 800px) {
+    .checklist li ul {
+        margin-left: 6% !important;
+    }
+    #toc {
+        padding-left: 10px;
+    }
+
+    footer {
+        margin-bottom: 10px !important;
+    }
+    html {
+        margin-bottom: 10px !important;
+    }
+}
+@media screen and (max-width: 535px) {
+    h3 ~ *:not(h2):not(h3) {
+      margin-left: 0 !important;
+    }
+
+    h4 ~ *:not(h2):not(h3):not(.h3):not(h4) {
+      margin-left: 0 !important;
+    }
+    h5 ~ *:not(h2):not(h3):not(.h3):not(h4):not(.h4):not(h5) {
+      margin-left: 0 !important;
+    }
+
+    footer {
+        margin-bottom: 10px !important;
+    }
+    html {
+        margin-bottom: 10px !important;
+    }
+}
+h3 ~ *:not(h2):not(h3) {
+  margin-left: 1.5em;
+}
+
+h4 ~ *:not(h2):not(h3):not(.h3):not(h4) {
+  margin-left: 2em;
+}
+h5 ~ *:not(h2):not(h3):not(.h3):not(h4):not(.h4):not(h5) {
+  margin-left: 2.5em;
+}
+
+
+.sect1 h2 {
+
+    /*
+    background: linear-gradient(
+        to bottom,
+        rgba(251, 189, 47, 0.2), /* yellow */
+        rgba(0, 0, 0, 0.4)       /* black */
+    );
+    */
+    /*opacity: .9;*/
+  }
+
+#preamble p{
+  padding-left: 10px;
+  color: var(--gruvbox-white) !important;
+}
+
+#footer {
+    max-width: 62.5em !important;
+    padding-left: 10px !important;
+    text-align: center;
+}
+
+.logo img {
+    display: block !important;
+    margin-left: auto !important;
+    margin-right: auto !important;
+}
+
+#header > h1:first-child {
+  margin-top: .3em !important;
+  padding-top: .2em !important;
+  text-align: center;
+}
+
+footer {
+    max-width: 62.5em;
+    margin-left: auto;
+    margin-right: auto;
+    display: block;
+
+}
+
+details[open] > summary::before {
+   /* display: none;*/
+    border-top: none;
+    content: "▼";
+    margin-top: -12px;
+}
+
+.example.stemblock {
+    border: solid;
+    font-size: 2vw;
+    max-width: 40%;
+    margin: auto;
+}
+
+.center {
+    max-width: 500px;
+    display: flex;
+    justify-content: center;
+}
+
+.highlight {
+    color: var(--gruvbox-yellow)
+}
+
+.border {
+    padding: 4px;
+    border: 2px solid rgba(255, 0, 0, .3);
+}
+
+.dull {
+    opacity: 40%;
+}
+
+.inline {
+    margin-right: 5%;
+    display: inline-block;
+}
+
+img {
+    margin: auto;
+    width: 100%;
+}
+
+
+</style>
+
+</head>
+
+<script>
+function sendHeight() {
+  const height = document.documentElement.scrollHeight;
+  parent.postMessage({ type: "HEIGHT", height }, "*");
+}
+
+window.addEventListener("load", sendHeight);
+window.addEventListener("resize", sendHeight);
+</script>
+</head>
+<body class="article">
+<div id="header">
+<h1>Basic 3D Example</h1>
+</div>
+<div id="content">
+<div id="preamble">
+<div class="sectionbody">
+<div class="paragraph">
+<p>click <a href="https://nyquistdev.net/git/?p=basic-3d-example.git;a=blob_plain;f=example.html;hb=HEAD"">here</a> for an interactive example</p>
+</div>
+<div class="paragraph">
+<p>A good explaination of how 3D works.</p>
+</div>
+<div class="admonitionblock note">
+<table>
+<tr>
+<td class="icon">
+<i class="fa icon-note" title="Note"></i>
+</td>
+<td class="content">
+This started off as code from Tsoding, I used chatgpt to change
+      specific things, and also made some of my own changes. This is a learning
+      tool! not something I wrote off the top of my head
+</td>
+</tr>
+</table>
+</div>
+</div>
+</div>
+<div class="sect1">
+<h2 id="_graphics_pipeline">Graphics Pipeline</h2>
+<div class="sectionbody">
+<div class="listingblock">
+<div class="content">
+<pre>vertex
+ -&gt; rotate cube
+ -&gt; translate cube (object position)
+ -&gt; apply camera (inverse transform)
+ -&gt; project
+ -&gt; screen</pre>
+</div>
+</div>
+<div class="admonitionblock note">
+<table>
+<tr>
+<td class="icon">
+<i class="fa icon-note" title="Note"></i>
+</td>
+<td class="content">
+Some parts of these pipeline may be left out if that part of the pipeline
+      is not needed
+</td>
+</tr>
+</table>
+</div>
+</div>
+</div>
+<div class="sect1">
+<h2 id="_basic_projection_formula">Basic Projection Formula</h2>
+<div class="sectionbody">
+<div class="paragraph">
+<p>Projection formula for a vector \([x,y,z\)]:</p>
+</div>
+<div class="stemblock">
+<div class="content">
+\[
+\begin{aligned}
+x' &amp;= x/z \\
+y' &amp;= y/z
+\end{aligned}
+\]
+</div>
+</div>
+</div>
+</div>
+<div class="sect1">
+<h2 id="_vertex_rotation_formulas">Vertex Rotation Formulas</h2>
+<div class="sectionbody">
+<div class="paragraph">
+<p>click <a href="https://nyquistdev.net/git/?p=basic-3d-example.git;a=blob_plain;f=example.html;hb=refs/heads/simple-rotation">here</a> for an interactive example</p>
+</div>
+<div class="paragraph">
+<p>Rotate vector formula for x-axis (y-z plane):</p>
+</div>
+<div class="stemblock">
+<div class="content">
+\[
+\begin{aligned}
+x' &amp;= x \\
+y' &amp;= y\cos\theta - z\sin\theta \\
+z' &amp;= y\sin\theta + z\cos\theta
+\end{aligned}
+\]
+</div>
+</div>
+<div class="paragraph">
+<p>Rotate vector formula for y-axis (x-z plane):</p>
+</div>
+<div class="stemblock">
+<div class="content">
+\[
+\begin{aligned}
+x' &amp;= x\cos\theta - z\sin\theta \\
+y' &amp;= y \\
+z' &amp;= x\sin\theta + z\cos\theta
+\end{aligned} \]
+</div>
+</div>
+<div class="paragraph">
+<p>Rotate vector formula for z-axis (x-y plane):</p>
+</div>
+<div class="stemblock">
+<div class="content">
+\[
+\begin{aligned}
+x' &amp;= x\cos\theta - y\sin\theta \\
+y' &amp;= x\sin\theta + y\cos\theta \\
+z' &amp;=  z
+\end{aligned}
+\]
+</div>
+</div>
+</div>
+</div>
+<div class="sect1">
+<h2 id="_2_single_axis_rotation_matrices_euler_angles">2. Single-axis rotation matrices (Euler Angles)</h2>
+<div class="sectionbody">
+<div class="sect2">
+<h3 id="_rotation_about_x_axis_roll">Rotation about X-axis (roll)</h3>
+<div class="stemblock">
+<div class="content">
+\[\boxed{
+R_x(\alpha) =
+\begin{bmatrix}
+1 &amp; 0 &amp; 0 \\
+0 &amp; \cos\alpha &amp; -\sin\alpha \\
+0 &amp; \sin\alpha &amp; \cos\alpha
+\end{bmatrix}
+}\]
+</div>
+</div>
+</div>
+<div class="sect2">
+<h3 id="_rotation_about_y_axis_pitch">Rotation about Y-axis (pitch)</h3>
+<div class="stemblock">
+<div class="content">
+\[\boxed{
+R_y(\beta) =
+\begin{bmatrix}
+\cos\beta &amp; 0 &amp; \sin\beta \\
+0 &amp; 1 &amp; 0 \\
+-\sin\beta &amp; 0 &amp; \cos\beta
+\end{bmatrix}
+}\]
+</div>
+</div>
+</div>
+<div class="sect2">
+<h3 id="_rotation_about_z_axis_yaw">Rotation about Z-axis (yaw)</h3>
+<div class="stemblock">
+<div class="content">
+\[\boxed{
+R_z(\gamma) =
+\begin{bmatrix}
+\cos\gamma &amp; -\sin\gamma &amp; 0 \\
+\sin\gamma &amp; \cos\gamma &amp; 0 \\
+0 &amp; 0 &amp; 1
+\end{bmatrix}
+}\]
+</div>
+</div>
+<div class="paragraph">
+<p>Euler angles will always have gimbal lock. You can switch which axis has gimbal
+lock by switching the order. Most application use quaternions instead of eular
+angles.</p>
+</div>
+</div>
+</div>
+</div>
+<div class="sect1">
+<h2 id="_quaternions">Quaternions</h2>
+<div class="sectionbody">
+<div class="paragraph">
+<p>click <a href="https://nyquistdev.net/git/?p=basic-3d-example.git;a=blob_plain;f=example.html;hb=refs/heads/quaternions">here</a> for an interactive example</p>
+</div>
+<div class="paragraph">
+<p>Quaternions allow you to rotate smoothly around multiple axes without gimbal
+lock. Chat-gpt rewrote the code to use quaternions. Here are some of the
+replys about <a href="https://nyquistdev.net/git/?p=basic-3d-example.git;a=blob_plain;f=docs/chatgpt-dump/quaternions.html;hb=HEAD">quaternions</a>.</p>
+</div>
+<div class="sect2">
+<h3 id="_1_what_is_a_quaternion_in_graphics">1. What is a quaternion (in graphics)?</h3>
+<div class="paragraph">
+<p>A quaternion is a 4-component number:</p>
+</div>
+<div class="stemblock">
+<div class="content">
+\[q = (w, x, y, z)\]
+</div>
+</div>
+<div class="paragraph">
+<p>In 3D graphics, we use <strong>unit quaternions</strong> to represent <strong>rotations</strong>.</p>
+</div>
+<div class="paragraph">
+<p>A rotation by:</p>
+</div>
+<div class="ulist">
+<ul>
+<li>
+<p>angle \(\theta\)</p>
+</li>
+<li>
+<p>around a <strong>unit axis</strong> \(\vec{u} = (u_x, u_y, u_z)\)</p>
+</li>
+</ul>
+</div>
+<div class="paragraph">
+<p>is encoded as:</p>
+</div>
+<div class="stemblock">
+<div class="content">
+\[\boxed{
+q = \left(
+\cos\frac{\theta}{2},
+\; u_x \sin\frac{\theta}{2},
+\; u_y \sin\frac{\theta}{2},
+\; u_z \sin\frac{\theta}{2}
+\right)
+}\]
+</div>
+</div>
+<div class="paragraph">
+<p>This is the <strong>axis–angle &#8594; quaternion</strong> formula.</p>
+</div>
+<div class="sect3">
+<h4 id="_why_half_angle">Why half-angle?</h4>
+<div class="paragraph">
+<p>Because quaternion rotation comes from complex numbers and Euler’s formula —
+doubling happens naturally in the math.</p>
+</div>
+</div>
+</div>
+<div class="sect2">
+<h3 id="_2_quatfromaxisangle">2. quatFromAxisAngle</h3>
+<div class="listingblock">
+<div class="content">
+<pre class="pygments highlight"><code data-lang="js"><span></span><span class="tok-kd">function</span><span class="tok-w"> </span><span class="tok-nx">quatFromAxisAngle</span><span class="tok-p">(</span><span class="tok-nx">axis</span><span class="tok-p">,</span><span class="tok-w"> </span><span class="tok-nx">angle</span><span class="tok-p">)</span><span class="tok-w"> </span><span class="tok-p">{</span>
+<span class="tok-w">    </span><span class="tok-kd">const</span><span class="tok-w"> </span><span class="tok-nx">h</span><span class="tok-w"> </span><span class="tok-o">=</span><span class="tok-w"> </span><span class="tok-nx">angle</span><span class="tok-w"> </span><span class="tok-o">/</span><span class="tok-w"> </span><span class="tok-mf">2</span><span class="tok-p">;</span>
+<span class="tok-w">    </span><span class="tok-kd">const</span><span class="tok-w"> </span><span class="tok-nx">s</span><span class="tok-w"> </span><span class="tok-o">=</span><span class="tok-w"> </span><span class="tok-nb">Math</span><span class="tok-p">.</span><span class="tok-nx">sin</span><span class="tok-p">(</span><span class="tok-nx">h</span><span class="tok-p">);</span>
+<span class="tok-w">    </span><span class="tok-k">return</span><span class="tok-w"> </span><span class="tok-p">[</span><span class="tok-nb">Math</span><span class="tok-p">.</span><span class="tok-nx">cos</span><span class="tok-p">(</span><span class="tok-nx">h</span><span class="tok-p">),</span><span class="tok-w"> </span><span class="tok-nx">axis</span><span class="tok-p">.</span><span class="tok-nx">x</span><span class="tok-o">*</span><span class="tok-nx">s</span><span class="tok-p">,</span><span class="tok-w"> </span><span class="tok-nx">axis</span><span class="tok-p">.</span><span class="tok-nx">y</span><span class="tok-o">*</span><span class="tok-nx">s</span><span class="tok-p">,</span><span class="tok-w"> </span><span class="tok-nx">axis</span><span class="tok-p">.</span><span class="tok-nx">z</span><span class="tok-o">*</span><span class="tok-nx">s</span><span class="tok-p">];</span>
+<span class="tok-p">}</span></code></pre>
+</div>
+</div>
+<div class="paragraph">
+<p>You’re directly implementing:</p>
+</div>
+<div class="stemblock">
+<div class="content">
+\[q = (\cos(\theta/2), \vec{u}\sin(\theta/2))\]
+</div>
+</div>
+<div class="ulist">
+<ul>
+<li>
+<p>\(h = angle / 2\) &#8594; half-angle</p>
+</li>
+<li>
+<p>\(cos(h)\) → scalar part (w)</p>
+</li>
+<li>
+<p>\(axis * sin(h)\) → vector part \((x,y,z)\)</p>
+</li>
+</ul>
+</div>
+<div class="admonitionblock important">
+<table>
+<tr>
+<td class="icon">
+<i class="fa icon-important" title="Important"></i>
+</td>
+<td class="content">
+<code>axis</code> <strong>must be normalized</strong> (length = 1), otherwise rotation will
+           scale or skew.
+</td>
+</tr>
+</table>
+</div>
+</div>
+<div class="sect2">
+<h3 id="_3_quaternion_multiplication_rotation_composition">3. Quaternion multiplication (rotation composition)</h3>
+<div class="listingblock">
+<div class="content">
+<pre class="pygments highlight"><code data-lang="js"><span></span><span class="tok-kd">function</span><span class="tok-w"> </span><span class="tok-nx">quatMultiply</span><span class="tok-p">(</span><span class="tok-nx">a</span><span class="tok-p">,</span><span class="tok-w"> </span><span class="tok-nx">b</span><span class="tok-p">)</span><span class="tok-w"> </span><span class="tok-p">{</span>
+<span class="tok-w">    </span><span class="tok-kd">const</span><span class="tok-w"> </span><span class="tok-p">[</span><span class="tok-nx">w1</span><span class="tok-p">,</span><span class="tok-nx">x1</span><span class="tok-p">,</span><span class="tok-nx">y1</span><span class="tok-p">,</span><span class="tok-nx">z1</span><span class="tok-p">]</span><span class="tok-w"> </span><span class="tok-o">=</span><span class="tok-w"> </span><span class="tok-nx">a</span><span class="tok-p">;</span>
+<span class="tok-w">    </span><span class="tok-kd">const</span><span class="tok-w"> </span><span class="tok-p">[</span><span class="tok-nx">w2</span><span class="tok-p">,</span><span class="tok-nx">x2</span><span class="tok-p">,</span><span class="tok-nx">y2</span><span class="tok-p">,</span><span class="tok-nx">z2</span><span class="tok-p">]</span><span class="tok-w"> </span><span class="tok-o">=</span><span class="tok-w"> </span><span class="tok-nx">b</span><span class="tok-p">;</span>
+<span class="tok-w">    </span><span class="tok-k">return</span><span class="tok-w"> </span><span class="tok-p">[</span>
+<span class="tok-w">        </span><span class="tok-nx">w1</span><span class="tok-o">*</span><span class="tok-nx">w2</span><span class="tok-w"> </span><span class="tok-o">-</span><span class="tok-w"> </span><span class="tok-nx">x1</span><span class="tok-o">*</span><span class="tok-nx">x2</span><span class="tok-w"> </span><span class="tok-o">-</span><span class="tok-w"> </span><span class="tok-nx">y1</span><span class="tok-o">*</span><span class="tok-nx">y2</span><span class="tok-w"> </span><span class="tok-o">-</span><span class="tok-w"> </span><span class="tok-nx">z1</span><span class="tok-o">*</span><span class="tok-nx">z2</span><span class="tok-p">,</span>
+<span class="tok-w">        </span><span class="tok-nx">w1</span><span class="tok-o">*</span><span class="tok-nx">x2</span><span class="tok-w"> </span><span class="tok-o">+</span><span class="tok-w"> </span><span class="tok-nx">x1</span><span class="tok-o">*</span><span class="tok-nx">w2</span><span class="tok-w"> </span><span class="tok-o">+</span><span class="tok-w"> </span><span class="tok-nx">y1</span><span class="tok-o">*</span><span class="tok-nx">z2</span><span class="tok-w"> </span><span class="tok-o">-</span><span class="tok-w"> </span><span class="tok-nx">z1</span><span class="tok-o">*</span><span class="tok-nx">y2</span><span class="tok-p">,</span>
+<span class="tok-w">        </span><span class="tok-nx">w1</span><span class="tok-o">*</span><span class="tok-nx">y2</span><span class="tok-w"> </span><span class="tok-o">-</span><span class="tok-w"> </span><span class="tok-nx">x1</span><span class="tok-o">*</span><span class="tok-nx">z2</span><span class="tok-w"> </span><span class="tok-o">+</span><span class="tok-w"> </span><span class="tok-nx">y1</span><span class="tok-o">*</span><span class="tok-nx">w2</span><span class="tok-w"> </span><span class="tok-o">+</span><span class="tok-w"> </span><span class="tok-nx">z1</span><span class="tok-o">*</span><span class="tok-nx">x2</span><span class="tok-p">,</span>
+<span class="tok-w">        </span><span class="tok-nx">w1</span><span class="tok-o">*</span><span class="tok-nx">z2</span><span class="tok-w"> </span><span class="tok-o">+</span><span class="tok-w"> </span><span class="tok-nx">x1</span><span class="tok-o">*</span><span class="tok-nx">y2</span><span class="tok-w"> </span><span class="tok-o">-</span><span class="tok-w"> </span><span class="tok-nx">y1</span><span class="tok-o">*</span><span class="tok-nx">x2</span><span class="tok-w"> </span><span class="tok-o">+</span><span class="tok-w"> </span><span class="tok-nx">z1</span><span class="tok-o">*</span><span class="tok-nx">w2</span>
+<span class="tok-w">    </span><span class="tok-p">];</span>
+<span class="tok-p">}</span></code></pre>
+</div>
+</div>
+<div class="paragraph">
+<p>formula:</p>
+</div>
+<div class="stemblock">
+<div class="content">
+\[q_1 = (w_1, \vec{v}_1), \quad q_2 = (w_2, \vec{v}_2)\]
+</div>
+</div>
+<div class="paragraph">
+<p>Quaternion multiplication is:</p>
+</div>
+<div class="stemblock">
+<div class="content">
+\[\boxed{
+q_1 q_2 =
+\left(
+w_1 w_2 - \vec{v}_1 \cdot \vec{v}_2,\;
+w_1 \vec{v}_2 + w_2 \vec{v}_1 + \vec{v}_1 \times \vec{v}_2
+\right)
+}\]
+</div>
+</div>
+<div class="paragraph">
+<p>Expanded component-wise &#8594; exactly your code.</p>
+</div>
+<div class="sect3">
+<h4 id="_why_this_matters">Why this matters</h4>
+<div class="ulist">
+<ul>
+<li>
+<p>Quaternion multiplication <strong>combines rotations</strong></p>
+</li>
+<li>
+<p>Order matters:</p>
+</li>
+</ul>
+</div>
+<div class="stemblock">
+<div class="content">
+\[q_{\text{final}} = q_{\text{new}} \cdot q_{\text{current}}\]
+</div>
+</div>
+<div class="paragraph">
+<p>This avoids <strong>gimbal lock</strong>, unlike Euler angles.</p>
+</div>
+</div>
+</div>
+<div class="sect2">
+<h3 id="_4_quaternion_conjugate">4. Quaternion conjugate</h3>
+<div class="listingblock">
+<div class="content">
+<pre class="pygments highlight"><code data-lang="js"><span></span><span class="tok-kd">function</span><span class="tok-w"> </span><span class="tok-nx">quatConjugate</span><span class="tok-p">(</span><span class="tok-nx">q</span><span class="tok-p">)</span><span class="tok-w"> </span><span class="tok-p">{</span>
+<span class="tok-w">    </span><span class="tok-k">return</span><span class="tok-w"> </span><span class="tok-p">[</span><span class="tok-nx">q</span><span class="tok-p">[</span><span class="tok-mf">0</span><span class="tok-p">],</span><span class="tok-w"> </span><span class="tok-o">-</span><span class="tok-nx">q</span><span class="tok-p">[</span><span class="tok-mf">1</span><span class="tok-p">],</span><span class="tok-w"> </span><span class="tok-o">-</span><span class="tok-nx">q</span><span class="tok-p">[</span><span class="tok-mf">2</span><span class="tok-p">],</span><span class="tok-w"> </span><span class="tok-o">-</span><span class="tok-nx">q</span><span class="tok-p">[</span><span class="tok-mf">3</span><span class="tok-p">]];</span>
+<span class="tok-p">}</span></code></pre>
+</div>
+</div>
+<div class="paragraph">
+<p>For:</p>
+</div>
+<div class="stemblock">
+<div class="content">
+\[q = (w, x, y, z)\]
+</div>
+</div>
+<div class="paragraph">
+<p>The conjugate is:</p>
+</div>
+<div class="stemblock">
+<div class="content">
+\[\boxed{
+q^* = (w, -x, -y, -z)
+}\]
+</div>
+</div>
+</div>
+<div class="sect2">
+<h3 id="_why_this_works">Why this works</h3>
+<div class="paragraph">
+<p>If (q) is a <strong>unit quaternion</strong>:</p>
+</div>
+<div class="stemblock">
+<div class="content">
+\[q^{-1} = q^*\]
+</div>
+</div>
+<div class="paragraph">
+<p>So conjugate = <strong>inverse rotation</strong>.</p>
+</div>
+<div class="paragraph">
+<p>This is why cameras use conjugates — moving the world opposite to the camera
+rotation.</p>
+</div>
+</div>
+<div class="sect2">
+<h3 id="_5_rotating_a_vector_using_quaternions">5. Rotating a vector using quaternions</h3>
+<div class="listingblock">
+<div class="content">
+<pre class="pygments highlight"><code data-lang="js"><span></span><span class="tok-kd">function</span><span class="tok-w"> </span><span class="tok-nx">quatRotateVector</span><span class="tok-p">(</span><span class="tok-nx">q</span><span class="tok-p">,</span><span class="tok-w"> </span><span class="tok-nx">v</span><span class="tok-p">)</span><span class="tok-w"> </span><span class="tok-p">{</span>
+<span class="tok-w">    </span><span class="tok-kd">const</span><span class="tok-w"> </span><span class="tok-nx">qv</span><span class="tok-w"> </span><span class="tok-o">=</span><span class="tok-w"> </span><span class="tok-p">[</span><span class="tok-mf">0</span><span class="tok-p">,</span><span class="tok-w"> </span><span class="tok-nx">v</span><span class="tok-p">.</span><span class="tok-nx">x</span><span class="tok-p">,</span><span class="tok-w"> </span><span class="tok-nx">v</span><span class="tok-p">.</span><span class="tok-nx">y</span><span class="tok-p">,</span><span class="tok-w"> </span><span class="tok-nx">v</span><span class="tok-p">.</span><span class="tok-nx">z</span><span class="tok-p">];</span>
+<span class="tok-w">    </span><span class="tok-kd">const</span><span class="tok-w"> </span><span class="tok-nx">r</span><span class="tok-w"> </span><span class="tok-o">=</span><span class="tok-w"> </span><span class="tok-nx">quatMultiply</span><span class="tok-p">(</span><span class="tok-nx">quatMultiply</span><span class="tok-p">(</span><span class="tok-nx">q</span><span class="tok-p">,</span><span class="tok-w"> </span><span class="tok-nx">qv</span><span class="tok-p">),</span><span class="tok-w"> </span><span class="tok-nx">quatConjugate</span><span class="tok-p">(</span><span class="tok-nx">q</span><span class="tok-p">));</span>
+<span class="tok-w">    </span><span class="tok-k">return</span><span class="tok-w"> </span><span class="tok-p">{</span><span class="tok-w"> </span><span class="tok-nx">x</span><span class="tok-o">:</span><span class="tok-w"> </span><span class="tok-nx">r</span><span class="tok-p">[</span><span class="tok-mf">1</span><span class="tok-p">],</span><span class="tok-w"> </span><span class="tok-nx">y</span><span class="tok-o">:</span><span class="tok-w"> </span><span class="tok-nx">r</span><span class="tok-p">[</span><span class="tok-mf">2</span><span class="tok-p">],</span><span class="tok-w"> </span><span class="tok-nx">z</span><span class="tok-o">:</span><span class="tok-w"> </span><span class="tok-nx">r</span><span class="tok-p">[</span><span class="tok-mf">3</span><span class="tok-p">]</span><span class="tok-w"> </span><span class="tok-p">};</span>
+<span class="tok-p">}</span></code></pre>
+</div>
+</div>
+<div class="paragraph">
+<p>The key formula</p>
+</div>
+<div class="paragraph">
+<p>To rotate a vector \(\vec{v}\):</p>
+</div>
+<div class="olist arabic">
+<ol class="arabic">
+<li>
+<p>Convert it to a <strong>pure quaternion</strong></p>
+</li>
+</ol>
+</div>
+<div class="stemblock">
+<div class="content">
+\[v_q = (0, v_x, v_y, v_z)\]
+</div>
+</div>
+<div class="olist arabic">
+<ol class="arabic">
+<li>
+<p>Apply:</p>
+</li>
+</ol>
+</div>
+<div class="stemblock">
+<div class="content">
+\[\boxed{
+v' = q \; v_q \; q^{-1}
+}\]
+</div>
+</div>
+<div class="olist arabic">
+<ol class="arabic">
+<li>
+<p>Extract the vector part</p>
+</li>
+</ol>
+</div>
+<div class="sect3">
+<h4 id="_why_this_works_2">Why this works</h4>
+<div class="paragraph">
+<p>Quaternion rotation encodes:</p>
+</div>
+<div class="ulist">
+<ul>
+<li>
+<p>axis</p>
+</li>
+<li>
+<p>angle</p>
+</li>
+<li>
+<p>orientation</p>
+</li>
+</ul>
+</div>
+<div class="paragraph">
+<p>The sandwich product rotates without:</p>
+</div>
+<div class="ulist">
+<ul>
+<li>
+<p>scaling</p>
+</li>
+<li>
+<p>skewing</p>
+</li>
+<li>
+<p>drifting</p>
+</li>
+</ul>
+</div>
+</div>
+</div>
+<div class="sect2">
+<h3 id="_visual_intuition">Visual intuition</h3>
+<div class="paragraph">
+<p>A rotation quaternion represents:</p>
+</div>
+<div class="stemblock">
+<div class="content">
+\[q = \cos(\theta/2) + \sin(\theta/2)(u_x i + u_y j + u_z k)\]
+</div>
+</div>
+<div class="paragraph">
+<p>This is the 3D analog of:</p>
+</div>
+<div class="stemblock">
+<div class="content">
+\[e^{i\theta} = \cos\theta + i\sin\theta\]
+</div>
+</div>
+</div>
+</div>
+</div>
+<div class="sect1">
+<h2 id="_camera">Camera</h2>
+<div class="sectionbody">
+<div class="paragraph">
+<p>chat-gpt replys about <a href="https://nyquistdev.net/git/?p=basic-3d-example.git;a=blob_plain;f=docs/chatgpt-dump/camera.html;hb=HEAD">camera</a>.</p>
+</div>
+</div>
+</div>
+<div class="sect1">
+<h2 id="_simple_clickable_object">Simple clickable object</h2>
+<div class="sectionbody">
+<div class="paragraph">
+<p>click <a href="https://nyquistdev.net/git/?p=basic-3d-example.git;a=blob_plain;f=example.html;hb=refs/heads/clickable">here</a> for an interactive example</p>
+</div>
+<div class="paragraph">
+<p>This does not account for the rotation of the object but it does check to see if
+your mouse is within the rectangular bounds of the mesh.</p>
+</div>
+<div class="olist arabic">
+<ol class="arabic">
+<li>
+<p>finding the min and max values for x and y of the mesh:</p>
+</li>
+</ol>
+</div>
+<div class="listingblock">
+<div class="content">
+<pre class="pygments highlight"><code data-lang="js"><span></span><span class="tok-kd">function</span><span class="tok-w"> </span><span class="tok-nx">computeBounds</span><span class="tok-p">(</span><span class="tok-nx">mesh</span><span class="tok-p">)</span><span class="tok-w"> </span><span class="tok-p">{</span>
+<span class="tok-w">    </span><span class="tok-kd">let</span><span class="tok-w"> </span><span class="tok-nx">min</span><span class="tok-w"> </span><span class="tok-o">=</span><span class="tok-w"> </span><span class="tok-p">{</span><span class="tok-nx">x</span><span class="tok-o">:</span><span class="tok-w"> </span><span class="tok-kc">Infinity</span><span class="tok-p">,</span><span class="tok-w"> </span><span class="tok-nx">y</span><span class="tok-o">:</span><span class="tok-w"> </span><span class="tok-kc">Infinity</span><span class="tok-p">}</span>
+<span class="tok-w">    </span><span class="tok-kd">let</span><span class="tok-w"> </span><span class="tok-nx">max</span><span class="tok-w"> </span><span class="tok-o">=</span><span class="tok-w"> </span><span class="tok-p">{</span><span class="tok-nx">x</span><span class="tok-o">:</span><span class="tok-w"> </span><span class="tok-o">-</span><span class="tok-kc">Infinity</span><span class="tok-p">,</span><span class="tok-w"> </span><span class="tok-nx">y</span><span class="tok-o">:</span><span class="tok-w"> </span><span class="tok-o">-</span><span class="tok-kc">Infinity</span><span class="tok-p">}</span>
+
+<span class="tok-w">    </span><span class="tok-k">for</span><span class="tok-w"> </span><span class="tok-p">(</span><span class="tok-kd">const</span><span class="tok-w"> </span><span class="tok-nx">v</span><span class="tok-w"> </span><span class="tok-k">of</span><span class="tok-w"> </span><span class="tok-nx">mesh</span><span class="tok-p">)</span><span class="tok-w"> </span><span class="tok-p">{</span>
+<span class="tok-w">        </span><span class="tok-nx">min</span><span class="tok-p">.</span><span class="tok-nx">x</span><span class="tok-w"> </span><span class="tok-o">=</span><span class="tok-w"> </span><span class="tok-nb">Math</span><span class="tok-p">.</span><span class="tok-nx">min</span><span class="tok-p">(</span><span class="tok-nx">min</span><span class="tok-p">.</span><span class="tok-nx">x</span><span class="tok-p">,</span><span class="tok-w"> </span><span class="tok-nx">v</span><span class="tok-p">.</span><span class="tok-nx">x</span><span class="tok-p">)</span>
+<span class="tok-w">        </span><span class="tok-nx">min</span><span class="tok-p">.</span><span class="tok-nx">y</span><span class="tok-w"> </span><span class="tok-o">=</span><span class="tok-w"> </span><span class="tok-nb">Math</span><span class="tok-p">.</span><span class="tok-nx">min</span><span class="tok-p">(</span><span class="tok-nx">min</span><span class="tok-p">.</span><span class="tok-nx">y</span><span class="tok-p">,</span><span class="tok-w"> </span><span class="tok-nx">v</span><span class="tok-p">.</span><span class="tok-nx">y</span><span class="tok-p">)</span>
+
+<span class="tok-w">        </span><span class="tok-nx">max</span><span class="tok-p">.</span><span class="tok-nx">x</span><span class="tok-w"> </span><span class="tok-o">=</span><span class="tok-w"> </span><span class="tok-nb">Math</span><span class="tok-p">.</span><span class="tok-nx">max</span><span class="tok-p">(</span><span class="tok-nx">max</span><span class="tok-p">.</span><span class="tok-nx">x</span><span class="tok-p">,</span><span class="tok-w"> </span><span class="tok-nx">v</span><span class="tok-p">.</span><span class="tok-nx">x</span><span class="tok-p">)</span>
+<span class="tok-w">        </span><span class="tok-nx">max</span><span class="tok-p">.</span><span class="tok-nx">y</span><span class="tok-w"> </span><span class="tok-o">=</span><span class="tok-w"> </span><span class="tok-nb">Math</span><span class="tok-p">.</span><span class="tok-nx">max</span><span class="tok-p">(</span><span class="tok-nx">max</span><span class="tok-p">.</span><span class="tok-nx">y</span><span class="tok-p">,</span><span class="tok-w"> </span><span class="tok-nx">v</span><span class="tok-p">.</span><span class="tok-nx">y</span><span class="tok-p">)</span>
+<span class="tok-w">    </span><span class="tok-p">}</span>
+<span class="tok-w">    </span><span class="tok-k">return</span><span class="tok-w"> </span><span class="tok-p">{</span><span class="tok-nx">min</span><span class="tok-p">,</span><span class="tok-w"> </span><span class="tok-nx">max</span><span class="tok-p">}</span>
+<span class="tok-p">}</span></code></pre>
+</div>
+</div>
+<div class="olist arabic">
+<ol class="arabic">
+<li>
+<p>Define the box corners of the rect for the mesh</p>
+</li>
+</ol>
+</div>
+<div class="listingblock">
+<div class="content">
+<pre class="pygments highlight"><code data-lang="js"><span></span><span class="tok-kd">function</span><span class="tok-w"> </span><span class="tok-nx">boundingBoxCorners</span><span class="tok-p">({</span><span class="tok-nx">min</span><span class="tok-p">,</span><span class="tok-w"> </span><span class="tok-nx">max</span><span class="tok-p">})</span><span class="tok-w"> </span><span class="tok-p">{</span>
+<span class="tok-w">    </span><span class="tok-k">return</span><span class="tok-w"> </span><span class="tok-p">{</span>
+<span class="tok-w">        </span><span class="tok-nx">bottom</span><span class="tok-o">:</span><span class="tok-w"> </span><span class="tok-p">{</span><span class="tok-nx">left</span><span class="tok-o">:</span><span class="tok-w"> </span><span class="tok-p">{</span><span class="tok-nx">x</span><span class="tok-o">:</span><span class="tok-w"> </span><span class="tok-nx">min</span><span class="tok-p">.</span><span class="tok-nx">x</span><span class="tok-p">,</span><span class="tok-w"> </span><span class="tok-nx">y</span><span class="tok-o">:</span><span class="tok-w"> </span><span class="tok-nx">max</span><span class="tok-p">.</span><span class="tok-nx">y</span><span class="tok-p">},</span><span class="tok-w"> </span><span class="tok-nx">right</span><span class="tok-o">:</span><span class="tok-w"> </span><span class="tok-p">{</span><span class="tok-nx">x</span><span class="tok-o">:</span><span class="tok-w"> </span><span class="tok-nx">max</span><span class="tok-p">.</span><span class="tok-nx">x</span><span class="tok-p">,</span><span class="tok-w"> </span><span class="tok-nx">y</span><span class="tok-o">:</span><span class="tok-w"> </span><span class="tok-nx">max</span><span class="tok-p">.</span><span class="tok-nx">y</span><span class="tok-p">}},</span>
+<span class="tok-w">        </span><span class="tok-nx">top</span><span class="tok-o">:</span><span class="tok-w"> </span><span class="tok-p">{</span><span class="tok-nx">left</span><span class="tok-o">:</span><span class="tok-w"> </span><span class="tok-p">{</span><span class="tok-nx">x</span><span class="tok-o">:</span><span class="tok-w"> </span><span class="tok-nx">min</span><span class="tok-p">.</span><span class="tok-nx">x</span><span class="tok-p">,</span><span class="tok-w"> </span><span class="tok-nx">y</span><span class="tok-o">:</span><span class="tok-w"> </span><span class="tok-nx">min</span><span class="tok-p">.</span><span class="tok-nx">y</span><span class="tok-p">},</span><span class="tok-w"> </span><span class="tok-nx">right</span><span class="tok-o">:</span><span class="tok-w"> </span><span class="tok-p">{</span><span class="tok-nx">x</span><span class="tok-o">:</span><span class="tok-w"> </span><span class="tok-nx">max</span><span class="tok-p">.</span><span class="tok-nx">x</span><span class="tok-p">,</span><span class="tok-w"> </span><span class="tok-nx">y</span><span class="tok-o">:</span><span class="tok-w"> </span><span class="tok-nx">min</span><span class="tok-p">.</span><span class="tok-nx">y</span><span class="tok-p">}},</span>
+
+<span class="tok-w">    </span><span class="tok-p">}</span>
+<span class="tok-p">}</span></code></pre>
+</div>
+</div>
+<div class="olist arabic">
+<ol class="arabic">
+<li>
+<p>Check to see if the mouse is in the rect in the game loop. If it is, and the
+mouse button is down, then you can move the object</p>
+</li>
+</ol>
+</div>
+<div class="listingblock">
+<div class="content">
+<pre class="pygments highlight"><code data-lang="js"><span></span><span class="tok-nx">inObject</span><span class="tok-w"> </span><span class="tok-o">=</span><span class="tok-w">  </span><span class="tok-p">(</span>
+<span class="tok-w">    </span><span class="tok-nx">mousePos</span><span class="tok-p">.</span><span class="tok-nx">x</span><span class="tok-w"> </span><span class="tok-o">&gt;</span><span class="tok-w"> </span><span class="tok-nx">rect</span><span class="tok-p">.</span><span class="tok-nx">top</span><span class="tok-p">.</span><span class="tok-nx">left</span><span class="tok-p">.</span><span class="tok-nx">x</span>
+<span class="tok-w">    </span><span class="tok-o">&amp;&amp;</span>
+<span class="tok-w">    </span><span class="tok-nx">mousePos</span><span class="tok-p">.</span><span class="tok-nx">x</span><span class="tok-w"> </span><span class="tok-o">&lt;</span><span class="tok-w"> </span><span class="tok-nx">rect</span><span class="tok-p">.</span><span class="tok-nx">top</span><span class="tok-p">.</span><span class="tok-nx">right</span><span class="tok-p">.</span><span class="tok-nx">x</span>
+<span class="tok-w">    </span><span class="tok-o">&amp;&amp;</span>
+<span class="tok-w">    </span><span class="tok-nx">mousePos</span><span class="tok-p">.</span><span class="tok-nx">y</span><span class="tok-w"> </span><span class="tok-o">&gt;</span><span class="tok-w"> </span><span class="tok-nx">rect</span><span class="tok-p">.</span><span class="tok-nx">top</span><span class="tok-p">.</span><span class="tok-nx">left</span><span class="tok-p">.</span><span class="tok-nx">y</span>
+<span class="tok-w">    </span><span class="tok-o">&amp;&amp;</span>
+<span class="tok-w">    </span><span class="tok-nx">mousePos</span><span class="tok-p">.</span><span class="tok-nx">y</span><span class="tok-w"> </span><span class="tok-o">&lt;</span><span class="tok-w"> </span><span class="tok-nx">rect</span><span class="tok-p">.</span><span class="tok-nx">bottom</span><span class="tok-p">.</span><span class="tok-nx">right</span><span class="tok-p">.</span><span class="tok-nx">y</span>
+<span class="tok-p">)</span>
+
+<span class="tok-k">if</span><span class="tok-w"> </span><span class="tok-p">(</span><span class="tok-nx">mouseDown</span><span class="tok-w"> </span><span class="tok-o">&amp;&amp;</span><span class="tok-w"> </span><span class="tok-nx">inObject</span><span class="tok-p">)</span><span class="tok-w"> </span><span class="tok-nx">canMoveObject</span><span class="tok-w"> </span><span class="tok-o">=</span><span class="tok-w"> </span><span class="tok-kc">true</span>
+
+<span class="tok-k">if</span><span class="tok-w"> </span><span class="tok-p">(</span><span class="tok-o">!</span><span class="tok-nx">mouseDown</span><span class="tok-p">)</span><span class="tok-w"> </span><span class="tok-nx">canMoveObject</span><span class="tok-w"> </span><span class="tok-o">=</span><span class="tok-w"> </span><span class="tok-kc">false</span></code></pre>
+</div>
+</div>
+</div>
+</div>
+<div class="sect1">
+<h2 id="_links">Links</h2>
+<div class="sectionbody">
+<div class="ulist">
+<ul>
+<li>
+<p><a href="https://www.youtube.com/watch?v=qjWkNZ0SXfo">One Formula That demystifies 3D Graphics</a></p>
+</li>
+<li>
+<p><a href="https://www.youtube.com/watch?v=EZufiIwwqFA">Rotation matrix derivation (step-by-step prove)</a></p>
+</li>
+<li>
+<p><a href="https://matthew-brett.github.io/teaching/rotation_2d.html">rotation 2D by mathew</a></p>
+</li>
+<li>
+<p><a href="https://www.youtube.com/watch?v=zc8b2Jo7mno">gimbal lock visual representation</a></p>
+</li>
+<li>
+<p><a href="https://www.youtube.com/watch?v=zjMuIxRvygQ">3Blue1Brown | Quaternions and 3d rotation, explained interactively</a></p>
+</li>
+<li>
+<p><a href="https://www.youtube.com/watch?v=d4EgbgTm0Bg&amp;t=17s">3Blue1Brown | Visualizing the 4d numbers Quanternions</a></p>
+</li>
+<li>
+<p><a href="https://phas.ubc.ca/~berciu/TEACHING/PHYS206/LECTURES/FILES/euler.pdf">euler.pdf</a></p>
+</li>
+</ul>
+</div>
+</div>
+</div>
+</div>
+<script type="text/x-mathjax-config">
+MathJax.Hub.Config({
+  messageStyle: "none",
+  tex2jax: {
+    inlineMath: [["\\(", "\\)"]],
+    displayMath: [["\\[", "\\]"]],
+    ignoreClass: "nostem|nolatexmath"
+  },
+  asciimath2jax: {
+    delimiters: [["\\$", "\\$"]],
+    ignoreClass: "nostem|noasciimath"
+  },
+  TeX: { equationNumbers: { autoNumber: "none" } }
+})
+MathJax.Hub.Register.StartupHook("AsciiMath Jax Ready", function () {
+  MathJax.InputJax.AsciiMath.postfilterHooks.Add(function (data, node) {
+    if ((node = data.script.parentNode) && (node = node.parentNode) && node.classList.contains("stemblock")) {
+      data.math.root.display = "block"
+    }
+    return data
+  })
+})
+</script>
+<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.9/MathJax.js?config=TeX-MML-AM_HTMLorMML"></script>
+</body>
+</html>
\ No newline at end of file