1
Fork 0
mirror of https://github.com/RGBCube/Site synced 2025-07-30 20:47:46 +00:00
Site/site/_includes/cube.vto

376 lines
9.7 KiB
Text

{{ set cube_minus_px = cube_small ? "" : "- 1px" }}
{{ if cube_last }}
<style>
cube-face {
height: {{ cube_size }};
width: {{ cube_size }};
position: absolute;
}
/* Guess what? Yeah, you guessed right. Safari can't render shit. */
.cube-face-front { transform: rotateY(0deg) translateZ(calc({{ cube_size }} / 2 {{ cube_minus_px }})); }
.cube-face-top { transform: rotateX( 89.99999999999999deg) translateZ(calc({{ cube_size }} / 2 {{ cube_minus_px }})); }
.cube-face-back { transform: rotateY(180deg) translateZ(calc({{ cube_size }} / 2 {{ cube_minus_px }})); }
.cube-face-bottom { transform: rotateX(-89.99999999999999deg) translateZ(calc({{ cube_size }} / 2 {{ cube_minus_px }})); }
.cube-face-right { transform: rotateY( 89.99999999999999deg) translateZ(calc({{ cube_size }} / 2 {{ cube_minus_px }})); }
.cube-face-left { transform: rotateY(-89.99999999999999deg) translateZ(calc({{ cube_size }} / 2 {{ cube_minus_px }})); }
</style>
{{ /if }}
<cube-scene class="
flex items-center justify-center
perspective-[calc({{ cube_size.replaceAll(" ", "_") }}*3)]
overscroll-none
">
<cube-itself class="
size-[{{ cube_size }}]
transform-3d transform-[translateZ(calc({{ cube_size.replaceAll(" ", "_") }}_/_-2))]
">
<cube-face draggable="false" class="cube-face-front"> {{ cube_face_front }} </cube-face>
<cube-face draggable="false" class="cube-face-back"> {{ cube_face_back }} </cube-face>
<cube-face draggable="false" class="cube-face-left"> {{ cube_face_left }} </cube-face>
<cube-face draggable="false" class="cube-face-right"> {{ cube_face_right }} </cube-face>
<cube-face draggable="false" class="cube-face-top"> {{ cube_face_top }} </cube-face>
<cube-face draggable="false" class="cube-face-bottom"> {{ cube_face_bottom }} </cube-face>
</cube-itself>
</cube-scene>
{{ if cube_last }}
<script>
"use strict";
const Vec = (x, y, z) => ({
x,
y,
z,
length() {
return Math.sqrt(this.x ** 2 + this.y ** 2 + this.z ** 2);
},
scale(factor) {
return Vec(this.x * factor, this.y * factor, this.z * factor);
},
normalize() {
let length = this.length();
length = length == 0 ? 1 : length;
return Vec(this.x / length, this.y / length, this.z / length);
},
sum(that) {
return Vec(
this.x + that.x,
this.y + that.y,
this.z + that.z,
);
},
sub(that) {
return Vec(
this.x - that.x,
this.y - that.y,
this.z - that.z,
);
},
mul(that) {
return Vec(
this.x * that.x,
this.y * that.y,
this.z * that.z,
);
},
});
Vec.ZERO = Vec(0, 0, 0);
const Quat = (x, y, z, w) => ({
x,
y,
z,
w,
mul(that) {
return Quat(
this.w * that.x + this.x * that.w + this.y * that.z - this.z * that.y,
this.w * that.y - this.x * that.z + this.y * that.w + this.z * that.x,
this.w * that.z + this.x * that.y - this.y * that.x + this.z * that.w,
this.w * that.w - this.x * that.x - this.y * that.y - this.z * that.z,
);
}
});
Quat.fromAxis = (axis) => {
const angle = axis.length();
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 Quat(x, y, z, w);
};
const friction = 3;
const sensitivityMouse = 0.01;
const sensitivityWheel = 0.006;
// 10 seconds.
const screensaverTimeoutMs = 10 * 1000;
// 10 minutes.
const stateDeleteTimeoutMs = 10 * 60 * 1000;
const lastSave = JSON.parse(localStorage.getItem("lastSave"));
if (!lastSave || Date.now() - lastSave > stateDeleteTimeoutMs) {
localStorage.removeItem("mouseDown");
localStorage.removeItem("mouseLastMove");
localStorage.removeItem("mousePrevious");
localStorage.removeItem("cubeOrient");
localStorage.removeItem("velocity");
localStorage.removeItem("impulseThisFrame");
}
const mouse = {
down: JSON.parse(localStorage.getItem("mouseDown")) ?? false,
lastMove: JSON.parse(localStorage.getItem("mouseLastMove")) ?? -screensaverTimeoutMs,
previous: Vec(...JSON.parse(localStorage.getItem("mousePrevious")) ?? [0, 0, 0, 1]),
};
const orient = {
elements: document.querySelectorAll("cube-itself"),
quat: Quat(...JSON.parse(localStorage.getItem("cubeOrient")) ?? [0, 0, 0, 1]),
set(q) {
this.quat = q;
for (const element of this.elements) {
element.style.transform =
`rotate3d(${q.x}, ${q.y}, ${q.z}, ${Math.acos(q.w) * 2}rad)`;
}
},
get() {
return this.quat;
},
};
let velocity = Vec(...JSON.parse(localStorage.getItem("velocity")) ?? [0, 0, 0]);
let impulseThisFrame = Vec(...JSON.parse(localStorage.getItem("impulseThisFrame")) ?? [0, 0, 0]);
window.addEventListener("beforeunload", () => {
localStorage.setItem("mouseDown", JSON.stringify(mouse.down));
localStorage.setItem("mouseLastMove", JSON.stringify(-(globalThis.performance.now() - mouse.lastMove)));
localStorage.setItem("mousePrevious", JSON.stringify([
mouse.previous.x,
mouse.previous.y,
mouse.previous.z,
mouse.previous.w,
]));
localStorage.setItem("cubeOrient", JSON.stringify([
orient.quat.x,
orient.quat.y,
orient.quat.z,
orient.quat.w,
]));
localStorage.setItem("velocity", JSON.stringify([
velocity.x,
velocity.y,
velocity.z,
]));
localStorage.setItem("impulseThisFrame", JSON.stringify([
impulseThisFrame.x,
impulseThisFrame.y,
impulseThisFrame.z,
]));
localStorage.setItem("lastSave", JSON.stringify(Date.now()));
});
document.addEventListener("keydown", (event) => {
const shift = event.shiftKey ? Vec(-1, -1, -1) : Vec(1, 1, 1);
let effect;
switch (event.key) {
case "Enter":
effect = Vec(0, 0, 4).mul(shift);
break;
case " ":
effect = Vec(4, 0, 0).mul(shift);
break;
case "h":
case "ArrowLeft":
effect = Vec(0, -4, 0);
break;
case "j":
case "ArrowDown":
effect = Vec(-4, 0, 0);
break;
case "k":
case "ArrowUp":
effect = Vec(4, 0, 0);
break;
case "l":
case "ArrowRight":
effect = Vec(0, 4, 0);
break;
default:
return;
}
velocity = velocity.sum(effect);
mouse.lastMove = globalThis.performance.now();
});
const handleUp = () => {
mouse.down = false;
};
document.addEventListener("mouseup", handleUp);
document.addEventListener("touchend", handleUp);
const handleDown = (event) => {
mouse.down = true;
velocity = Vec.ZERO;
};
document.addEventListener("mousedown", handleDown);
document.addEventListener("touchstart", handleDown);
const handleMove = (event) => {
// Disables scrolling.
event.preventDefault();
if (!mouse.down) return;
const newMouse = Vec(event.clientX, event.clientY, 0);
if (globalThis.performance.now() - mouse.lastMove > 100) {
// This is a fresh scroll.
mouse.previous = newMouse;
}
const delta = newMouse.sub(mouse.previous);
mouse.previous = newMouse;
mouse.lastMove = globalThis.performance.now();
const axis = Vec(-delta.y, delta.x, 0)
.normalize()
.scale(delta.length())
.scale(sensitivityMouse);
impulseThisFrame = impulseThisFrame.sum(axis);
const rotation = Quat.fromAxis(axis);
orient.set(rotation.mul(orient.get()));
};
document.addEventListener("mousemove", handleMove);
document.addEventListener("touchmove", (event) => {
const { x, y } = Array
.from(event.touches)
.reduce(
(acc, touch) => ({
x: acc.x + touch.clientX,
y: acc.y + touch.clientY,
}),
{ x: 0, y: 0 }
);
event.clientX = x / event.touches.length;
event.clientY = y / event.touches.length;
handleMove(event);
});
const handleWheel = (event) => {
mouse.lastMove = globalThis.performance.now();
const axis = Vec(event.deltaY, -event.deltaX, 0)
.scale(sensitivityWheel);
impulseThisFrame = impulseThisFrame.sum(axis);
const rotation = Quat.fromAxis(axis);
orient.set(rotation.mul(orient.get()));
};
document.addEventListener("wheel", handleWheel, { passive: false });
let lastUpdate = 0;
const updateFrame = (timestamp) => {
if (lastUpdate == 0) lastUpdate = timestamp;
const delta = (timestamp - lastUpdate) / 1000;
lastUpdate = timestamp;
if (mouse.down) {
velocity = impulseThisFrame.scale(1 / delta);
impulseThisFrame = Vec.ZERO;
requestAnimationFrame(updateFrame);
return;
}
const decay = Math.exp(-delta * friction);
const effectiveDelta = friction > 0 ? (1 - decay) / friction : delta;
let theta = effectiveDelta * velocity.length();
velocity.x *= decay;
velocity.y *= decay;
velocity.z *= decay;
if (friction > 0 && velocity.length() < 0.00001) {
theta += velocity.length() / friction;
velocity.x = 0;
velocity.y = 0;
velocity.z = 0;
}
if (globalThis.performance.now() - mouse.lastMove > screensaverTimeoutMs) {
const impulseIdle = Vec(2, 2, -2);
velocity = velocity.sum(impulseIdle.scale(effectiveDelta));
}
const axis = Vec(velocity.x, velocity.y, velocity.z)
.normalize()
.scale(theta);
const rotation = Quat.fromAxis(axis);
orient.set(rotation.mul(orient.get()));
requestAnimationFrame(updateFrame);
};
updateFrame(0);
</script>
{{ /if }}