352 lines
9.1 KiB
JavaScript
352 lines
9.1 KiB
JavaScript
import init, { MatrixEngine } from "./matrix_engine.js";
|
|
|
|
let engine;
|
|
let wasm;
|
|
let ctx;
|
|
let atlasCanvas, atlasCtx;
|
|
let width, height;
|
|
let fontSize = 16;
|
|
let isMobile = false,
|
|
isVpnOn = false,
|
|
isDarkMode = true;
|
|
|
|
const chars =
|
|
"0101アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホ마미무메모야유요라릴레로와원>_±÷×=≠≈≡≤≥";
|
|
|
|
function buildAtlas() {
|
|
atlasCanvas = new OffscreenCanvas(chars.length * fontSize, fontSize * 2);
|
|
atlasCtx = atlasCanvas.getContext("2d", { alpha: false });
|
|
|
|
let r, g, b;
|
|
if (isVpnOn && isDarkMode) {
|
|
r = 139;
|
|
g = 61;
|
|
b = 255;
|
|
} else if (!isVpnOn && isDarkMode) {
|
|
r = 0;
|
|
g = 229;
|
|
b = 242;
|
|
} else if (isVpnOn && !isDarkMode) {
|
|
r = 109;
|
|
g = 40;
|
|
b = 217;
|
|
} else {
|
|
r = 2;
|
|
g = 132;
|
|
b = 199;
|
|
}
|
|
|
|
atlasCtx.fillStyle = isDarkMode ? "rgb(10, 10, 12)" : "rgb(250, 250, 252)";
|
|
atlasCtx.fillRect(0, 0, atlasCanvas.width, atlasCanvas.height);
|
|
|
|
atlasCtx.font = `bold ${fontSize}px monospace`;
|
|
atlasCtx.textBaseline = "top";
|
|
atlasCtx.textAlign = "center";
|
|
const halfFs = fontSize / 2;
|
|
|
|
atlasCtx.fillStyle = `rgb(${r}, ${g}, ${b})`;
|
|
for (let i = 0; i < chars.length; i++)
|
|
atlasCtx.fillText(chars[i], i * fontSize + halfFs, 0);
|
|
|
|
atlasCtx.fillStyle = "rgb(255, 255, 255)";
|
|
for (let i = 0; i < chars.length; i++)
|
|
atlasCtx.fillText(chars[i], i * fontSize + halfFs, fontSize);
|
|
}
|
|
|
|
function getGlitchColor(type) {
|
|
let r, g, b;
|
|
if (isVpnOn && isDarkMode) {
|
|
r = 139;
|
|
g = 61;
|
|
b = 255;
|
|
} else if (!isVpnOn && isDarkMode) {
|
|
r = 0;
|
|
g = 229;
|
|
b = 242;
|
|
} else if (isVpnOn && !isDarkMode) {
|
|
r = 109;
|
|
g = 40;
|
|
b = 217;
|
|
} else {
|
|
r = 2;
|
|
g = 132;
|
|
b = 199;
|
|
}
|
|
|
|
if (type === 0) return isDarkMode ? "rgb(10, 10, 12)" : "rgb(250, 250, 252)";
|
|
if (type === 1) return `rgba(${r},${g},${b},0.9)`;
|
|
if (type === 2) return `rgba(${255 - r},${255 - g},${255 - b},0.7)`;
|
|
if (type === 3) return "rgba(255,255,255,0.95)";
|
|
return "rgba(255,20,80,0.95)";
|
|
}
|
|
|
|
self.onmessage = async (e) => {
|
|
const { type, payload } = e.data;
|
|
|
|
if (type === "INIT") {
|
|
ctx = payload.canvas.getContext("2d", { alpha: false });
|
|
width = payload.width;
|
|
height = payload.height;
|
|
isMobile = payload.isMobile;
|
|
isVpnOn = payload.isVpnOn;
|
|
isDarkMode = payload.isDarkMode;
|
|
fontSize = payload.fontSize || 16;
|
|
|
|
wasm = await init("./matrix_engine_bg.wasm?=v1.0");
|
|
engine = new MatrixEngine(width, height, fontSize);
|
|
engine.set_mobile(isMobile);
|
|
engine.set_vpn_status(isVpnOn);
|
|
if (payload.eyeY) engine.set_eye_anchor(payload.eyeY);
|
|
|
|
buildAtlas();
|
|
requestAnimationFrame(render);
|
|
} else if (type === "RESIZE") {
|
|
if (!engine) return;
|
|
width = payload.width;
|
|
height = payload.height;
|
|
fontSize = payload.fontSize || 16;
|
|
ctx.canvas.width = width;
|
|
ctx.canvas.height = height;
|
|
engine.set_mobile(payload.isMobile);
|
|
engine.resize(width, height);
|
|
if (payload.eyeY) engine.set_eye_anchor(payload.eyeY);
|
|
buildAtlas();
|
|
} else if (type === "THEME") {
|
|
if (!engine) return;
|
|
isVpnOn = payload.isVpnOn;
|
|
isDarkMode = payload.isDarkMode;
|
|
engine.set_vpn_status(isVpnOn);
|
|
buildAtlas();
|
|
|
|
if (ctx) {
|
|
ctx.globalAlpha = 1.0;
|
|
ctx.fillStyle = isDarkMode ? "rgb(10, 10, 12)" : "rgb(250, 250, 252)";
|
|
ctx.fillRect(0, 0, width, height);
|
|
}
|
|
} else if (type === "MOUSE_MOVE") {
|
|
if (engine) engine.update_mouse(payload.x, payload.y);
|
|
} else if (type === "DRAW_START") {
|
|
if (engine) {
|
|
engine.set_drawing(true);
|
|
engine.trigger_glitch(payload.x, payload.y);
|
|
}
|
|
} else if (type === "DRAW_END") {
|
|
if (engine) engine.set_drawing(false);
|
|
} else if (type === "GET_EYE_DATA") {
|
|
if (!engine || !wasm) return;
|
|
const eyeLen = engine.eye_len();
|
|
if (eyeLen > 0) {
|
|
const eyePtr = engine.eye_ptr();
|
|
const eyeData = new Float32Array(wasm.memory.buffer, eyePtr, eyeLen);
|
|
self.postMessage({ type: "EYE_DATA", payload: eyeData.slice() });
|
|
}
|
|
}
|
|
};
|
|
|
|
function drawEye(ctx, cx, cy, gazeX, gazeY, dir, isClosed) {
|
|
const color = isVpnOn
|
|
? isDarkMode
|
|
? "rgb(139, 61, 255)"
|
|
: "rgb(109, 40, 217)"
|
|
: isDarkMode
|
|
? "rgb(0, 229, 242)"
|
|
: "rgb(2, 132, 199)";
|
|
|
|
const bgFill = isDarkMode ? "rgb(10, 10, 12)" : "rgb(250, 250, 252)";
|
|
|
|
// Interpolate properties based on isClosed (0.0 to 1.0)
|
|
const eyeHeight = 72 * (1 - isClosed) + 6 * isClosed;
|
|
const eyeWidth = 120 * (1 - isClosed) + 140 * isClosed;
|
|
const eyeRadius = 36 * (1 - isClosed) + 8 * isClosed;
|
|
const laserWidth = 140 * isClosed;
|
|
|
|
ctx.save();
|
|
ctx.fillStyle = bgFill;
|
|
ctx.beginPath();
|
|
ctx.roundRect(
|
|
cx - eyeWidth / 2,
|
|
cy - eyeHeight / 2,
|
|
eyeWidth,
|
|
eyeHeight,
|
|
eyeRadius,
|
|
);
|
|
ctx.fill();
|
|
|
|
ctx.strokeStyle = color;
|
|
ctx.lineWidth = 4 * (1 - isClosed) + 3 * isClosed;
|
|
ctx.shadowColor = color;
|
|
ctx.shadowBlur = 15 * (1 - isClosed) + 8 * isClosed;
|
|
ctx.stroke();
|
|
|
|
if (isVpnOn && isClosed > 0.1) {
|
|
ctx.fillStyle = color;
|
|
ctx.shadowColor = color;
|
|
ctx.shadowBlur = 8;
|
|
ctx.globalAlpha = isClosed;
|
|
ctx.fillRect(cx - laserWidth / 2, cy - 1, laserWidth, 2);
|
|
}
|
|
|
|
if (isClosed < 0.99) {
|
|
const alphaMult = 1 - isClosed;
|
|
ctx.save();
|
|
ctx.translate(cx + gazeX, cy + gazeY);
|
|
|
|
// Outer cyber ring
|
|
ctx.strokeStyle = isDarkMode ? "rgba(255,255,255,0.7)" : "rgba(0,0,0,0.6)";
|
|
ctx.lineWidth = 1.5;
|
|
ctx.globalAlpha = alphaMult;
|
|
ctx.beginPath();
|
|
ctx.arc(0, 0, 10, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
|
|
// Inner dot
|
|
ctx.fillStyle = isDarkMode ? "rgba(255,255,255,0.9)" : "rgba(0,0,0,0.8)";
|
|
ctx.shadowBlur = 5;
|
|
ctx.shadowColor = isDarkMode ? "white" : "black";
|
|
ctx.beginPath();
|
|
ctx.arc(0, 0, 3.5, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Cyber crosshairs
|
|
ctx.strokeStyle = isDarkMode ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.4)";
|
|
ctx.lineWidth = 1;
|
|
ctx.shadowBlur = 0;
|
|
ctx.beginPath();
|
|
ctx.moveTo(-16, 0);
|
|
ctx.lineTo(-8, 0);
|
|
ctx.moveTo(16, 0);
|
|
ctx.lineTo(8, 0);
|
|
ctx.moveTo(0, -16);
|
|
ctx.lineTo(0, -8);
|
|
ctx.moveTo(0, 16);
|
|
ctx.lineTo(0, 8);
|
|
ctx.stroke();
|
|
|
|
// Arrows
|
|
ctx.fillStyle = isDarkMode ? "white" : "black";
|
|
ctx.font = "10px monospace";
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "middle";
|
|
ctx.globalAlpha = (dir === 1 ? 1.0 : 0.2) * alphaMult;
|
|
ctx.fillText("▴", 0, -20);
|
|
ctx.globalAlpha = (dir === 2 ? 1.0 : 0.2) * alphaMult;
|
|
ctx.fillText("▾", 0, 22);
|
|
ctx.globalAlpha = (dir === 3 ? 1.0 : 0.2) * alphaMult;
|
|
ctx.fillText("◂", -22, 1);
|
|
ctx.globalAlpha = (dir === 4 ? 1.0 : 0.2) * alphaMult;
|
|
ctx.fillText("▸", 22, 1);
|
|
|
|
ctx.restore();
|
|
}
|
|
ctx.restore();
|
|
}
|
|
|
|
function render() {
|
|
if (!engine) return requestAnimationFrame(render);
|
|
|
|
engine.tick();
|
|
|
|
ctx.globalAlpha = 1.0;
|
|
ctx.fillStyle = isDarkMode
|
|
? "rgba(10, 10, 12, 0.2)"
|
|
: "rgba(250, 250, 252, 0.2)";
|
|
ctx.fillRect(0, 0, width, height);
|
|
|
|
const bgLen = engine.bg_len();
|
|
if (bgLen > 0) {
|
|
const bgData = new Float32Array(wasm.memory.buffer, engine.bg_ptr(), bgLen);
|
|
const bgF = fontSize * 0.6;
|
|
for (let i = 0; i < bgLen; i += 4) {
|
|
ctx.globalAlpha = bgData[i + 3];
|
|
ctx.drawImage(
|
|
atlasCanvas,
|
|
bgData[i + 2] * fontSize,
|
|
0,
|
|
fontSize,
|
|
fontSize,
|
|
bgData[i],
|
|
bgData[i + 1],
|
|
bgF,
|
|
bgF,
|
|
);
|
|
}
|
|
}
|
|
|
|
const fgLen = engine.fg_len();
|
|
const fgData = new Float32Array(wasm.memory.buffer, engine.fg_ptr(), fgLen);
|
|
for (let i = 0; i < fgLen; i += 5) {
|
|
ctx.globalAlpha = fgData[i + 4];
|
|
ctx.drawImage(
|
|
atlasCanvas,
|
|
fgData[i + 2] * fontSize,
|
|
fgData[i + 3] * fontSize,
|
|
fontSize,
|
|
fontSize,
|
|
fgData[i],
|
|
fgData[i + 1],
|
|
fontSize,
|
|
fontSize,
|
|
);
|
|
}
|
|
|
|
// Draw Eyes
|
|
const eyeLen = engine.eye_len();
|
|
if (eyeLen >= 6) {
|
|
const eyeData = new Float32Array(
|
|
wasm.memory.buffer,
|
|
engine.eye_ptr(),
|
|
eyeLen,
|
|
);
|
|
const cx = eyeData[0];
|
|
const cy = eyeData[1];
|
|
const gazeX = eyeData[2];
|
|
const gazeY = eyeData[3];
|
|
const dir = eyeData[4];
|
|
const isClosed = eyeData[5];
|
|
|
|
const gap = isMobile ? 65 : 75;
|
|
ctx.globalAlpha = 1.0;
|
|
drawEye(ctx, cx - gap, cy, gazeX, gazeY, dir, isClosed);
|
|
drawEye(ctx, cx + gap, cy, gazeX, gazeY, dir, isClosed);
|
|
}
|
|
|
|
const glLen = engine.glitch_len();
|
|
if (glLen > 0) {
|
|
ctx.globalAlpha = 1.0;
|
|
const glData = new Float32Array(
|
|
wasm.memory.buffer,
|
|
engine.glitch_ptr(),
|
|
glLen,
|
|
);
|
|
for (let i = 0; i < glLen; i += 5) {
|
|
ctx.fillStyle = getGlitchColor(glData[i + 4]);
|
|
ctx.fillRect(glData[i], glData[i + 1], glData[i + 2], glData[i + 3]);
|
|
}
|
|
}
|
|
|
|
const gsLen = engine.glass_len();
|
|
if (gsLen > 0) {
|
|
const gsData = new Float32Array(
|
|
wasm.memory.buffer,
|
|
engine.glass_ptr(),
|
|
gsLen,
|
|
);
|
|
for (let i = 0; i < gsLen; i += 4) {
|
|
ctx.globalAlpha = gsData[i + 3];
|
|
ctx.drawImage(
|
|
atlasCanvas,
|
|
gsData[i + 2] * fontSize,
|
|
0,
|
|
fontSize,
|
|
fontSize,
|
|
gsData[i],
|
|
gsData[i + 1],
|
|
fontSize,
|
|
fontSize,
|
|
);
|
|
}
|
|
}
|
|
|
|
requestAnimationFrame(render);
|
|
}
|