mirror of
https://gitlab.com/vyla-entertainment/home.git
synced 2026-11-09 19:57:20 -05:00
967 lines
37 KiB
HTML
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> |