diff --git a/.gitignore b/.gitignore
index 4b4b284..b1b2783 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,5 +4,6 @@
!*.gif
!*.html
+!*.js
!*.md
!*.woff2
diff --git a/cube.js b/cube.js
new file mode 100644
index 0000000..0184837
--- /dev/null
+++ b/cube.js
@@ -0,0 +1,142 @@
+"use strict";
+
+const __cube = document.querySelector(".cube");
+
+class Vec3 {
+ static up = new Vec3(0, 1, 0);
+ static right = new Vec3(1, 0, 0);
+
+ constructor(x, y, z) {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ }
+
+ length() {
+ return Math.sqrt(this.x ** 2 + this.y ** 2 + this.z ** 2);
+ }
+
+ normalize() {
+ const length = this.length();
+
+ if (length != 0) {
+ this.x /= length;
+ this.y /= length;
+ this.z /= length;
+ }
+ }
+
+ static sub(v, t) {
+ return new Vec3(
+ v.x - t.x,
+ v.y - t.y,
+ v.z - t.z,
+ )
+ }
+}
+
+class Vec2 extends Vec3 {
+ constructor(x, y) {
+ super(x, y, 0)
+ }
+}
+
+class Quat {
+ constructor(x, y, z, w) {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ this.w = w;
+ }
+
+ static fromAngleAxis(angle, axis) {
+ axis.normalize();
+
+ const half = angle / 2;
+
+ const sinHalf = Math.sin(half);
+ const cosHalf = Math.cos(half);
+
+ const x = axis.x * sinHalf;
+ const y = axis.y * sinHalf;
+ const z = axis.z * sinHalf;
+ const w = cosHalf;
+
+ return new Quat(x, y, z, w);
+ }
+
+ static mul(q, r) {
+ return new Quat(
+ q.w * r.x + q.x * r.w + q.y * r.z - q.z * r.y,
+ q.w * r.y - q.x * r.z + q.y * r.w + q.z * r.x,
+ q.w * r.z + q.x * r.y - q.y * r.x + q.z * r.w,
+ q.w * r.w - q.x * r.x - q.y * r.y - q.z * r.z,
+ );
+ }
+
+ apply() {
+ __cube.style.transform = `rotate3d(${this.x}, ${this.y}, ${this.z}, ${this.w}rad)`;
+ }
+}
+
+let friction = 0.01;
+let sensitivity = 0.01;
+let velocity = 0;
+
+const orientation = {
+ __value: new Quat(0, 0, 0, 1),
+
+ set(value) {
+ this.__value = value;
+ this.__value.apply();
+ },
+
+ get() {
+ return this.__value;
+ },
+};
+
+(() => {
+ const mouse = {
+ down: false,
+ lastMove: window.performance.now(),
+ previous: new Vec2(0, 0),
+ };
+
+ document.addEventListener("mouseleave", () => {
+ mouse.down = false;
+ });
+
+ document.addEventListener("mouseup", () => {
+ mouse.down = false;
+ });
+
+ document.addEventListener("mousedown", (event) => {
+ // Disables link dragging that occurs when spinning.
+ event.preventDefault();
+ mouse.down = true;
+ });
+
+ document.addEventListener("mousemove", (event) => {
+ if (!mouse.down) return;
+
+ const newMouse = new Vec2(event.clientX, event.clientY);
+
+ if (window.performance.now() - mouse.lastMove > 100) {
+ // This is a fresh scroll.
+ mouse.previous = newMouse;
+ }
+
+ const delta = Vec2.sub(newMouse, mouse.previous);
+
+ mouse.previous = newMouse;
+ mouse.lastMove = window.performance.now();
+
+ const rotation = Quat.mul(
+ Quat.fromAngleAxis(delta.x * sensitivity, Vec3.up),
+ Quat.fromAngleAxis(delta.y * sensitivity, Vec3.right),
+ );
+
+ orientation.set(Quat.mul(orientation.get(), rotation));
+ });
+})();
diff --git a/index.html b/index.html
index d5b022e..4822efd 100644
--- a/index.html
+++ b/index.html
@@ -223,150 +223,7 @@
-
+