mirror of
https://github.com/RGBCube/Site
synced 2025-07-30 20:47:46 +00:00
376 lines
9.7 KiB
Text
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 }}
|