Files
Home/ui/index.html
2026-10-30 20:02:41 -07:00

967 lines
37 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vyla Home</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,
body {
width: 100%;
height: 100%;
overflow: hidden;
background: transparent !important;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display", "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
color: #ffffff;
user-select: none;
-webkit-font-smoothing: antialiased;
}
#bg-canvas-container {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
z-index: 0;
pointer-events: auto;
opacity: 0.92;
}
.gradient-ascii-canvas {
position: absolute;
inset: 0;
display: block;
width: 100%;
height: 100%;
}
.loader-container {
position: relative;
z-index: 10;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 32px;
pointer-events: none;
}
.loader-card {
pointer-events: auto;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
max-width: 420px;
width: 100%;
padding: 40px 32px 28px;
border-radius: 36px;
animation: cardFloat 1.2s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
@keyframes cardFloat {
from {
opacity: 0;
transform: translateY(12px) scale(0.97);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.app-icon {
width: 70px;
height: 70px;
object-fit: contain;
margin-bottom: 22px;
user-select: none;
-webkit-user-drag: none;
}
.status-text {
font-size: 17px;
font-weight: 600;
letter-spacing: -0.022em;
color: rgba(255, 255, 255, 0.95);
margin-bottom: 20px;
min-height: 24px;
display: inline-flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
}
.status-char {
display: inline-block;
opacity: 0;
transform: translateY(8px) scale(0.85);
filter: blur(6px);
animation: liquidCharIn 0.45s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
white-space: pre;
}
@keyframes liquidCharIn {
0% {
opacity: 0;
transform: translateY(8px) scale(0.85);
filter: blur(6px);
}
60% {
opacity: 0.9;
transform: translateY(-1.5px);
filter: blur(0.5px);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
filter: blur(0px);
}
}
.file-line {
width: 100%;
min-height: 16px;
margin-top: -8px;
margin-bottom: 18px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 11px;
color: rgba(255, 255, 255, 0.65);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
letter-spacing: 0.2px;
}
.progress-track {
width: 100%;
height: 4px;
background: rgba(255, 255, 255, 0.14);
border-radius: 9999px;
overflow: hidden;
position: relative;
margin-bottom: 24px;
}
.progress-fill {
height: 100%;
width: 0%;
background: rgba(255, 255, 255, 0.95);
border-radius: 9999px;
transition: width 0.35s cubic-bezier(0.2, 0.8, 0.2, 1);
}
#debug-log-box {
display: none;
width: 100%;
max-height: 150px;
overflow-y: auto;
background: rgba(255, 59, 48, 0.18);
border-radius: 10px;
padding: 12px;
font-family: ui-monospace, SFMono-Regular, monospace;
font-size: 11px;
color: #ff9b9b;
text-align: left;
white-space: pre-wrap;
word-break: break-all;
margin-bottom: 16px;
user-select: text;
}
.notice-line {
display: none;
max-width: 100%;
margin-bottom: 14px;
padding: 8px 12px;
border-radius: 10px;
background: rgba(255, 204, 102, 0.14);
color: #ffd98a;
font-size: 12px;
font-weight: 500;
text-align: center;
word-break: break-word;
}
</style>
</head>
<body>
<div id="bg-canvas-container">
<canvas id="ascii-canvas" class="gradient-ascii-canvas" aria-hidden="true"></canvas>
</div>
<div class="loader-container">
<div class="loader-card">
<img class="app-icon" src="/white_favicon.png" alt="Vyla" draggable="false">
<div class="status-text" id="status-message">Starting runtime engine...</div>
<div class="notice-line" id="notice-line"></div>
<div id="debug-log-box"></div>
<div class="file-line" id="file-line"></div>
<div class="progress-track" id="progress-track" style="display: none;">
<div class="progress-fill" id="progress-fill"></div>
</div>
</div>
</div>
<script>
const MAX_COLORS = 8;
const MAX_CLICKS = 4;
const MAX_TRAIL = 24;
const CLICK_LIFE = 1.2;
const TRAIL_LIFE = 0.7;
const TRAIL_STEP = 0.015;
const INTRO_SECONDS = 2.4;
const ATLAS_CELL_W = 60;
const ATLAS_CELL_H = 100;
const ATLAS_COLUMNS = 16;
const FONT_SCALE = 0.78;
const DEFAULT_COLORS = ['#1e3fd8', '#f6f8ff', '#a5d8ff', '#38bdf8'];
const DEFAULT_CHARACTERS = ' =+xX0S8#@';
const DEFAULT_FONT = '"Geist Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace';
const SHAPES = { horizon: 0, pinch: 1, rise: 2, eclipse: 3 };
const CLICK_EFFECTS = { none: 0, wave: 1, burst: 2 };
const VERTEX = `#version 300 es
out vec2 vUv;
void main() {
vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
vUv = p;
gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);
}`;
const COMMON = `
float hash12(vec2 p) {
vec3 q = fract(vec3(p.xyx) * 0.1031);
q += dot(q, q.yzx + 33.33);
return fract((q.x + q.y) * q.z);
}
`;
const FIELD = `#version 300 es
precision highp float;
precision highp int;
uniform float uAspect;
uniform float uTime;
uniform int uShape;
uniform vec3 uRamp[8];
uniform int uCount;
uniform float uScale;
uniform float uSoftness;
uniform float uFlow;
uniform vec2 uSeed;
uniform vec4 uTrail[24];
uniform float uRadius;
uniform vec4 uClicks[4];
uniform int uClickEffect;
uniform float uIntro;
uniform float uFrame;
in vec2 vUv;
layout(location = 0) out vec4 outColor;
layout(location = 1) out vec4 outAux;
${COMMON}
vec2 hash22(vec2 p) {
vec3 q = fract(vec3(p.xyx) * vec3(0.1031, 0.103, 0.0973));
q += dot(q, q.yzx + 33.33);
return fract((q.xx + q.yz) * q.zy) * 2.0 - 1.0;
}
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
float a = dot(hash22(i), f);
float b = dot(hash22(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0));
float c = dot(hash22(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0));
float d = dot(hash22(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0));
return 0.5 + 0.5 * mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
vec3 labToRgb(vec3 lab) {
float l = lab.x + 0.3963377774 * lab.y + 0.2158037573 * lab.z;
float m = lab.x - 0.1055613458 * lab.y - 0.0638541728 * lab.z;
float s = lab.x - 0.0894841775 * lab.y - 1.2914855480 * lab.z;
l = l * l * l;
m = m * m * m;
s = s * s * s;
return vec3(
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s
);
}
vec3 encode(vec3 c) {
c = clamp(c, 0.0, 1.0);
return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));
}
vec3 horizon(vec2 p, float time, out float order) {
float span = 0.5 * max(uAspect, 1.0);
float x = p.x / span;
float arch = -0.17 - 0.2 * x * x + 0.02 * sin(time * 0.6);
float soft = mix(0.05, 0.22, uSoftness);
float edge = p.y - arch;
float rim = 0.38 * smoothstep(-soft, 0.42, edge);
vec2 sun = vec2(0.08 * sin(time * 0.31) * span, 0.58);
float spread = length((p - sun) / vec2(0.95 * span, 0.75));
float sky = mix(0.55, 1.0, smoothstep(0.2, 0.95, spread));
order = clamp(edge * 1.3, 0.0, 1.0);
return vec3(rim, sky, smoothstep(0.18, 0.62, edge));
}
vec3 pinch(vec2 p, float time, out float order) {
float span = 0.5 * max(uAspect, 0.75);
float x = p.x / span;
float sway = 0.02 * sin(x * 1.7 + time * 0.55);
float breathe = 1.0 + 0.06 * sin(time * 0.8);
float width = (0.13 + 0.22 * x * x) * breathe * mix(0.85, 1.2, uSoftness);
float y = abs(p.y - sway) / width;
float t = exp(-pow(y, 2.2)) * (0.42 + 0.58 * exp(-x * x * 1.8));
order = clamp(abs(p.y - sway) * 2.4 + abs(x) * 0.3, 0.0, 1.0);
return vec3(t, t, 0.0);
}
vec3 rise(vec2 p, float time, out float order) {
float wide = max(uAspect, 0.9);
vec2 center = vec2(0.05 * sin(time * 0.45) * wide, -0.7);
vec2 radius = vec2(0.82 * wide, 0.64 * (1.0 + 0.04 * sin(time * 0.8)));
float d = length((p - center) / radius);
float soft = mix(0.6, 1.15, uSoftness);
float t = clamp(pow(clamp(1.0 - d, 0.0, 1.0), soft) * 1.35, 0.0, 1.0);
order = clamp((d - 0.2) * 1.3, 0.0, 1.0);
return vec3(t, t, 0.0);
}
vec3 eclipse(vec2 p, float time, out float order) {
float fit = min(1.0, uAspect * 1.25);
float r = 0.23 * fit * (1.0 + 0.015 * sin(time * 0.9));
float angle = atan(p.y, p.x);
float rays = noise(vec2(cos(angle), sin(angle)) * 2.2 + time * 0.08) * 0.8 + 0.6;
float d = length(p) - r;
float reach = mix(0.07, 0.15, uSoftness) * rays * fit;
float t = smoothstep(-0.006, 0.006 + 0.02 * uSoftness, d) * exp(-max(d, 0.0) / reach);
order = clamp(d * 2.0, 0.0, 1.0);
return vec3(t, t, 0.0);
}
vec4 tone(float t, int count, vec3 ramp[8]) {
float x = smoothstep(0.3, 1.0, t) * float(count - 1);
int index = int(floor(x));
return vec4(mix(ramp[index], ramp[min(index + 1, count - 1)], fract(x)), smoothstep(0.0, 0.38, t));
}
void main() {
vec2 p = (vUv - 0.5) * vec2(uAspect, 1.0);
float time = uTime;
vec2 q = p / uScale;
vec2 warp = vec2(
noise(q * 1.6 + uSeed + vec2(0.0, time * 0.3)),
noise(q * 1.6 + uSeed.yx + vec2(4.3, -time * 0.24))
) - 0.5;
q += warp * uFlow * (uShape == 3 ? 0.04 : 0.18);
float order = 0.0;
vec3 shape;
if (uShape == 0) shape = horizon(q, time, order);
else if (uShape == 1) shape = pinch(q, time, order);
else if (uShape == 2) shape = rise(q, time, order);
else shape = eclipse(q, time, order);
float reveal = clamp((uIntro * 1.45 - order) / 0.45, 0.0, 1.0);
reveal = reveal * reveal * (3.0 - 2.0 * reveal);
float scramble = 0.0;
for (int i = 0; i < 24; i++) {
vec4 point = uTrail[i];
if (point.z <= 0.0) continue;
vec2 d = p - point.xy;
scramble = max(scramble, point.z * exp(-dot(d, d) / (uRadius * uRadius)));
}
for (int i = 0; i < 4; i++) {
vec4 c = uClicks[i];
if (c.w <= 0.0) continue;
float d = length(p - c.xy);
if (uClickEffect == 1) {
scramble = max(scramble, exp(-pow((d - c.z * 0.75) / 0.06, 2.0)) * c.w);
} else if (uClickEffect == 2) {
float size = 0.08 + 0.22 * (1.0 - exp(-c.z * 5.0));
scramble = max(scramble, (1.0 - smoothstep(size * 0.6, size, d)) * c.w);
}
}
vec2 t = clamp(shape.xy * mix(0.7, 1.0, reveal), 0.0, 1.0);
vec4 mixed = mix(tone(t.x, uCount, uRamp), tone(t.y, uCount, uRamp), shape.z);
vec3 color = encode(labToRgb(mixed.rgb));
float alpha = mixed.a * reveal;
float dither = (hash12(gl_FragCoord.xy + uFrame * 13.7) - 0.5) / 255.0;
outColor = clamp(vec4(color * alpha + dither, alpha + dither), 0.0, 1.0);
outAux = vec4(reveal, scramble, 0.0, 1.0);
}`;
const GLYPHS = `
uniform sampler2D uField;
uniform sampler2D uAux;
uniform sampler2D uAtlas;
uniform vec2 uResolution;
uniform vec2 uCell;
uniform vec2 uOffset;
uniform vec2 uAtlasGrid;
uniform float uCount;
uniform float uDensity;
uniform float uOpacity;
uniform float uFlicker;
uniform float uDepth;
uniform float uGlyphTime;
uniform float uSeedValue;
uniform float uLight;
${COMMON}
float sampleGlyph(float index, vec2 local, vec2 coord, float soft) {
float column = mod(index, uAtlasGrid.x);
float row = floor(index / uAtlasGrid.x);
vec2 atlasUv = vec2((column + local.x) / uAtlasGrid.x, (row + 1.0 - local.y) / uAtlasGrid.y);
vec2 continuous = coord / uAtlasGrid;
float scale = exp2(soft);
return textureGrad(uAtlas, atlasUv, dFdx(continuous) * scale, dFdy(continuous) * scale).a;
}
vec4 glyphAt(vec2 px) {
vec2 coord = (px - uOffset) / uCell;
vec2 cell = floor(coord);
vec2 local = coord - cell;
vec2 uv = clamp(((cell + 0.5) * uCell + uOffset) / uResolution, vec2(0.0), vec2(1.0));
vec4 field = texture(uField, uv);
vec4 aux = texture(uAux, uv);
vec3 visible = uLight > 0.5 ? field.rgb + (1.0 - field.a) : field.rgb;
float luma = dot(visible, vec3(0.2126, 0.7152, 0.0722));
float value = uLight > 0.5 ? clamp((1.0 - luma) * 1.5, 0.0, 1.0) : luma;
float pick = hash12(cell + uSeedValue * 7.31);
float level = hash12(cell * 1.73 + 4.1 + uSeedValue);
float tick = floor(uGlyphTime * 9.0 + level * 9.0);
float wobble = (hash12(cell * 2.31 + tick * 17.7) - 0.5) * uFlicker * 0.4 + (level - 0.5) * 0.1;
float tone = clamp(pow(value, 0.75) + wobble, 0.0, 1.0);
float index = floor(tone * (uCount - 1.0) + 0.5);
float decode = max(aux.g, 1.0 - aux.r);
if (decode > 0.02) {
float frame = floor(uGlyphTime * 20.0);
if (hash12(cell * 3.7 + frame * 5.3) < decode * 1.25) index = 1.0 + floor(hash12(cell + frame * 1.37) * (uCount - 1.0));
}
float present = step(pick, uDensity * mix(1.0, 0.3 + tone * 1.1, 0.55));
float cutoff = smoothstep(0.03, 0.1, value);
float focus = step(hash12(cell * 0.71 + 9.2 + uSeedValue), uDepth);
float coverage = sampleGlyph(index, local, coord, focus * 1.2);
float alpha = uOpacity * present * cutoff * coverage * mix(1.0, 0.35 + 0.65 * level, 0.55) * (1.0 - focus * 0.5);
if (uLight < 0.5) {
vec3 light = field.rgb * 0.55 + vec3(0.03) * field.a;
return vec4(light * alpha, 0.6 * field.a * alpha);
}
vec3 ink = visible * 0.8;
return vec4(ink * alpha, alpha);
}
`;
const HALO = `#version 300 es
precision highp float;
uniform float uDownscale;
in vec2 vUv;
out vec4 outColor;
${GLYPHS}
void main() {
outColor = glyphAt(gl_FragCoord.xy * uDownscale);
}`;
const BLUR = `#version 300 es
precision highp float;
uniform sampler2D uSource;
uniform vec2 uStep;
in vec2 vUv;
out vec4 outColor;
void main() {
vec4 sum = texture(uSource, vUv) * 0.2270270270;
sum += texture(uSource, vUv + uStep * 1.3846153846) * 0.3162162162;
sum += texture(uSource, vUv - uStep * 1.3846153846) * 0.3162162162;
sum += texture(uSource, vUv + uStep * 3.2307692308) * 0.0702702703;
sum += texture(uSource, vUv - uStep * 3.2307692308) * 0.0702702703;
outColor = sum;
}`;
const COMPOSITE = `#version 300 es
precision highp float;
uniform sampler2D uHalo;
uniform float uGlow;
uniform float uGradient;
uniform float uGrain;
uniform float uFrame;
in vec2 vUv;
out vec4 outColor;
${GLYPHS}
void main() {
vec4 base = texture(uField, vUv) * uGradient;
vec4 glyph = glyphAt(gl_FragCoord.xy);
vec4 halo = texture(uHalo, vUv) * uGlow;
vec4 color;
if (uLight < 0.5) {
color = base + glyph + halo;
} else {
color = glyph + base * (1.0 - glyph.a);
color += halo * 0.6 * (1.0 - color.a);
}
float grain = (hash12(gl_FragCoord.xy + uFrame * vec2(17.0, 59.0)) + hash12(gl_FragCoord.xy * 1.31 + uFrame * 7.0) - 1.0) * uGrain;
float body = clamp(color.a, 0.0, 1.0);
color.rgb += grain * (0.25 + body) * 0.5;
color.a = clamp(color.a, 0.0, 1.0);
color.rgb = clamp(color.rgb, 0.0, color.a);
outColor = color;
}`;
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
const toLinear = value => (value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4);
const toLab = ([r, g, b]) => {
const lr = toLinear(r);
const lg = toLinear(g);
const lb = toLinear(b);
const l = Math.cbrt(0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb);
const m = Math.cbrt(0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb);
const s = Math.cbrt(0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb);
return [
0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s
];
};
const compile = (gl, type, source) => {
const shader = gl.createShader(type);
if (!shader) return null;
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) return shader;
gl.deleteShader(shader);
return null;
};
const link = (gl, fragmentSource) => {
const vertex = compile(gl, gl.VERTEX_SHADER, VERTEX);
const fragment = compile(gl, gl.FRAGMENT_SHADER, fragmentSource);
const program = gl.createProgram();
if (!vertex || !fragment || !program) return null;
gl.attachShader(program, vertex);
gl.attachShader(program, fragment);
gl.linkProgram(program);
gl.deleteShader(vertex);
gl.deleteShader(fragment);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
gl.deleteProgram(program);
return null;
}
const uniforms = {};
const count = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);
for (let i = 0; i < count; i++) {
const info = gl.getActiveUniform(program, i);
if (!info) continue;
const name = info.name.replace(/\[0\]$/, '');
uniforms[name] = gl.getUniformLocation(program, name);
}
return { program, uniforms };
};
const createTarget = (gl, count) => {
const textures = Array.from({ length: count }, () => {
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
return texture;
});
return { textures, framebuffer: gl.createFramebuffer(), width: 0, height: 0 };
};
const sizeTarget = (gl, target, width, height) => {
if (target.width === width && target.height === height) return;
target.width = width;
target.height = height;
gl.bindFramebuffer(gl.FRAMEBUFFER, target.framebuffer);
target.textures.forEach((texture, i) => {
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + i, gl.TEXTURE_2D, texture, 0);
});
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
};
const buildAtlas = (gl, characters, fontFamily) => {
const list = [...new Set([...characters].filter(char => char !== '\n' && char !== '\r'))];
if (!list.length) list.push(' ');
const font = `300 ${Math.round(ATLAS_CELL_H * FONT_SCALE)}px ${fontFamily}`;
const columns = Math.min(ATLAS_COLUMNS, list.length);
const rows = Math.ceil(list.length / columns);
const canvas = document.createElement('canvas');
canvas.width = ATLAS_CELL_W * columns;
canvas.height = ATLAS_CELL_H * rows;
const context = canvas.getContext('2d');
if (context) {
context.font = font;
context.textAlign = 'center';
context.textBaseline = 'middle';
context.fillStyle = '#fff';
list.forEach((char, index) => {
const column = index % columns;
const row = Math.floor(index / columns);
context.fillText(char, column * ATLAS_CELL_W + ATLAS_CELL_W / 2, row * ATLAS_CELL_H + ATLAS_CELL_H / 2 + 3);
});
}
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, gl.RGBA, gl.UNSIGNED_BYTE, canvas);
gl.generateMipmap(gl.TEXTURE_2D);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
return { texture, count: list.length, grid: [columns, rows] };
};
function initGradientAscii() {
const container = document.getElementById('bg-canvas-container');
const canvas = document.getElementById('ascii-canvas');
const gl = canvas.getContext('webgl2', {
alpha: true,
premultipliedAlpha: true,
antialias: false,
depth: false,
stencil: false
});
if (!container || !canvas || !gl) return;
const field = link(gl, FIELD);
const halo = link(gl, HALO);
const blur = link(gl, BLUR);
const composite = link(gl, COMPOSITE);
if (!field || !halo || !blur || !composite) return;
const vao = gl.createVertexArray();
const fieldTarget = createTarget(gl, 2);
const haloA = createTarget(gl, 1);
const haloB = createTarget(gl, 1);
const palette = ['#1e3fd8', '#f6f8ff', '#a5d8ff', '#38bdf8'];
const ramp = new Float32Array(MAX_COLORS * 3);
const hexToRgb = (hex) => {
const match = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return match ? [parseInt(match[1], 16) / 255, parseInt(match[2], 16) / 255, parseInt(match[3], 16) / 255] : [0, 0, 0];
};
palette.forEach((color, i) => ramp.set(toLab(hexToRgb(color)), i * 3));
const view = { width: 1, height: 1, ratio: 1 };
const clickData = new Float32Array(MAX_CLICKS * 4);
const clicks = [];
const trailData = new Float32Array(MAX_TRAIL * 4);
const trail = [];
const pointer = { x: 0, y: 0, inside: false };
let atlas = buildAtlas(gl, DEFAULT_CHARACTERS, DEFAULT_FONT);
let time = 0;
let glyphTime = 0;
let introClock = 0;
let frameSeed = 0;
let grainClock = 0;
let raf = 0;
let last = 0;
const bindGlyphs = (program, width, height) => {
if (!atlas) return;
const u = program.uniforms;
const cellH = 18 * view.ratio;
const cellW = cellH * (ATLAS_CELL_W / ATLAS_CELL_H);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, fieldTarget.textures[0]);
gl.uniform1i(u.uField, 0);
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, fieldTarget.textures[1]);
gl.uniform1i(u.uAux, 1);
gl.activeTexture(gl.TEXTURE2);
gl.bindTexture(gl.TEXTURE_2D, atlas.texture);
gl.uniform1i(u.uAtlas, 2);
gl.uniform2f(u.uResolution, width, height);
gl.uniform2f(u.uCell, cellW, cellH);
gl.uniform2f(
u.uOffset,
(width - Math.ceil(width / cellW) * cellW) / 2,
(height - Math.ceil(height / cellH) * cellH) / 2
);
gl.uniform2f(u.uAtlasGrid, atlas.grid[0], atlas.grid[1]);
gl.uniform1f(u.uCount, atlas.count);
gl.uniform1f(u.uDensity, 0.8);
gl.uniform1f(u.uOpacity, 0.85);
gl.uniform1f(u.uFlicker, 0.2);
gl.uniform1f(u.uDepth, 0);
gl.uniform1f(u.uGlyphTime, glyphTime);
gl.uniform1f(u.uSeedValue, 0);
gl.uniform1f(u.uLight, 0);
};
const draw = () => {
const aspect = view.width / view.height;
gl.bindVertexArray(vao);
gl.disable(gl.BLEND);
gl.bindFramebuffer(gl.FRAMEBUFFER, fieldTarget.framebuffer);
gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1]);
gl.viewport(0, 0, fieldTarget.width, fieldTarget.height);
gl.useProgram(field.program);
const fu = field.uniforms;
gl.uniform1f(fu.uAspect, aspect);
gl.uniform1f(fu.uTime, time);
gl.uniform1i(fu.uShape, 0);
gl.uniform3fv(fu.uRamp, ramp);
gl.uniform1i(fu.uCount, palette.length);
gl.uniform1f(fu.uScale, 1.0);
gl.uniform1f(fu.uSoftness, 0.5);
gl.uniform1f(fu.uFlow, 0.5);
gl.uniform2f(fu.uSeed, 0, 0);
gl.uniform4fv(fu.uTrail, trailData);
gl.uniform1f(fu.uRadius, 0.1);
gl.uniform4fv(fu.uClicks, clickData);
gl.uniform1i(fu.uClickEffect, 1);
gl.uniform1f(fu.uIntro, introClock);
gl.uniform1f(fu.uFrame, frameSeed);
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.bindFramebuffer(gl.FRAMEBUFFER, haloA.framebuffer);
gl.drawBuffers([gl.COLOR_ATTACHMENT0]);
gl.viewport(0, 0, haloA.width, haloA.height);
gl.useProgram(halo.program);
bindGlyphs(halo, canvas.width, canvas.height);
gl.uniform1f(halo.uniforms.uDownscale, canvas.width / haloA.width);
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.useProgram(blur.program);
gl.activeTexture(gl.TEXTURE0);
gl.uniform1i(blur.uniforms.uSource, 0);
gl.bindFramebuffer(gl.FRAMEBUFFER, haloB.framebuffer);
gl.bindTexture(gl.TEXTURE_2D, haloA.textures[0]);
gl.uniform2f(blur.uniforms.uStep, 1 / haloA.width, 0);
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.bindFramebuffer(gl.FRAMEBUFFER, haloA.framebuffer);
gl.bindTexture(gl.TEXTURE_2D, haloB.textures[0]);
gl.uniform2f(blur.uniforms.uStep, 0, 1 / haloA.height);
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0, 0, canvas.width, canvas.height);
gl.useProgram(composite.program);
bindGlyphs(composite, canvas.width, canvas.height);
const cu = composite.uniforms;
gl.activeTexture(gl.TEXTURE3);
gl.bindTexture(gl.TEXTURE_2D, haloA.textures[0]);
gl.uniform1i(cu.uHalo, 3);
gl.uniform1f(cu.uGlow, 0.2);
gl.uniform1f(cu.uGradient, 0.9);
gl.uniform1f(cu.uGrain, 0.08);
gl.uniform1f(cu.uFrame, frameSeed);
gl.drawArrays(gl.TRIANGLES, 0, 3);
};
const resize = () => {
view.width = Math.max(1, container.clientWidth);
view.height = Math.max(1, container.clientHeight);
view.ratio = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = Math.max(1, Math.round(view.width * view.ratio));
canvas.height = Math.max(1, Math.round(view.height * view.ratio));
sizeTarget(gl, fieldTarget, Math.max(1, Math.round(canvas.width / 2)), Math.max(1, Math.round(canvas.height / 2)));
const haloWidth = Math.max(1, Math.round(canvas.width / 4));
const haloHeight = Math.max(1, Math.round(canvas.height / 4));
sizeTarget(gl, haloA, haloWidth, haloHeight);
sizeTarget(gl, haloB, haloWidth, haloHeight);
draw();
};
const updateInput = dt => {
trail.forEach(point => { point.age += dt; });
while (trail.length && trail[trail.length - 1].age > TRAIL_LIFE) trail.pop();
for (let i = 0; i < MAX_TRAIL; i++) {
const point = trail[i];
if (!point) { trailData[i * 4 + 2] = 0; continue; }
const k = 1 - point.age / TRAIL_LIFE;
trailData.set([point.x, point.y, k * k, 0], i * 4);
}
clicks.forEach(click => { click.age += dt; });
while (clicks.length && clicks[clicks.length - 1].age > CLICK_LIFE) clicks.pop();
for (let i = 0; i < MAX_CLICKS; i++) {
const click = clicks[i];
if (!click) { clickData[i * 4 + 3] = 0; continue; }
const k = click.age / CLICK_LIFE;
const envelope = Math.min(1, click.age / 0.08) * (1 - k) * (1 - k);
clickData.set([click.x, click.y, click.age, envelope], i * 4);
}
};
const frame = now => {
const dt = last ? clamp((now - last) / 1000, 0.001, 0.05) : 1 / 60;
last = now;
time += dt * 0.5;
glyphTime += dt * 0.75;
if (introClock < 1) introClock = Math.min(1, introClock + dt / INTRO_SECONDS);
grainClock += dt;
if (grainClock > 1 / 24) {
grainClock = 0;
frameSeed = (frameSeed + 1) % 97;
}
updateInput(dt);
draw();
raf = requestAnimationFrame(frame);
};
const locate = event => {
const rect = container.getBoundingClientRect();
const x = (event.clientX - rect.left) / Math.max(1, rect.width);
const y = 1 - (event.clientY - rect.top) / Math.max(1, rect.height);
const aspect = rect.width / Math.max(1, rect.height);
pointer.inside = x >= 0 && x <= 1 && y >= 0 && y <= 1;
pointer.x = (x - 0.5) * aspect;
pointer.y = y - 0.5;
};
window.addEventListener('pointermove', event => {
locate(event);
if (!pointer.inside) return;
const head = trail[0];
const gap = head ? Math.hypot(pointer.x - head.x, pointer.y - head.y) : Infinity;
if (gap < TRAIL_STEP) return;
trail.unshift({ x: pointer.x, y: pointer.y, age: 0 });
trail.length = Math.min(trail.length, MAX_TRAIL);
}, { passive: true });
window.addEventListener('pointerdown', event => {
locate(event);
if (!pointer.inside) return;
clicks.unshift({ x: pointer.x, y: pointer.y, age: 0 });
if (clicks.length > MAX_CLICKS) clicks.pop();
}, { passive: true });
window.addEventListener('resize', resize);
resize();
raf = requestAnimationFrame(frame);
}
const statusEl = document.getElementById("status-message");
const progressTrack = document.getElementById("progress-track");
const progressFill = document.getElementById("progress-fill");
const fileLine = document.getElementById("file-line");
const debugBox = document.getElementById("debug-log-box");
function setStatus(text, color = "rgba(255, 255, 255, 0.95)") {
if (!statusEl) return;
statusEl.innerHTML = "";
statusEl.style.color = color;
const chars = text.split("");
chars.forEach((c, idx) => {
const s = document.createElement("span");
s.className = "status-char";
s.textContent = c;
s.style.animationDelay = `${idx * 0.016}s`;
statusEl.appendChild(s);
});
}
function setProgress(pct) {
if (!progressTrack) return;
if (pct > 0) {
progressTrack.style.display = "block";
if (progressFill) progressFill.style.width = Math.min(100, Math.max(0, pct)) + "%";
} else {
progressTrack.style.display = "none";
if (progressFill) progressFill.style.width = "0%";
}
}
function showDebug(msg) {
if (!debugBox) return;
debugBox.style.display = "block";
debugBox.textContent = debugBox.textContent ? debugBox.textContent + "\n" + msg : msg;
debugBox.scrollTop = debugBox.scrollHeight;
}
function initTauriEvents() {
if (!window.__TAURI__ || !window.__TAURI__.event) {
setTimeout(initTauriEvents, 50);
return;
}
const eventApi = window.__TAURI__.event;
eventApi.listen("status", (e) => {
const data = e.payload || e;
if (data && data.message) setStatus(data.message);
});
eventApi.listen("notice", (e) => {
const data = e.payload || e;
const el = document.getElementById("notice-line");
if (!el) return;
if (data && data.message) {
el.textContent = data.message;
el.style.display = "block";
} else {
el.textContent = "";
el.style.display = "none";
}
});
eventApi.listen("download-progress", (e) => {
const data = e.payload || e;
if (data && typeof data.percent === "number") {
setProgress(data.percent);
const mbDown = (data.downloaded / (1024 * 1024)).toFixed(1);
const mbTotal = (data.total / (1024 * 1024)).toFixed(1);
if (fileLine) fileLine.textContent = `Downloading update: ${mbDown} MB / ${mbTotal} MB (${data.percent}%)`;
}
});
eventApi.listen("extract-progress", (e) => {
const data = e.payload || e;
if (data && typeof data.percent === "number") {
setProgress(data.percent);
if (fileLine && data.file) fileLine.textContent = data.file;
}
});
eventApi.listen("ready", (e) => {
const data = e.payload || e;
if (data && data.url) {
if (progressTrack) progressTrack.style.display = "none";
if (fileLine) fileLine.textContent = "";
setStatus("Launching...");
}
});
eventApi.listen("error", (e) => {
const data = e.payload || e;
const msg = data && data.message ? data.message : JSON.stringify(data);
if (fileLine) fileLine.textContent = "";
setStatus("Runtime Error", "#ff6b6b");
showDebug(msg);
});
}
window.addEventListener('DOMContentLoaded', () => {
initTauriEvents();
try { initGradientAscii(); } catch (e) { }
});
</script>
</body>
</html>