mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
500 lines
22 KiB
HTML
500 lines
22 KiB
HTML
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta http-equiv="Content-Security-Policy" content="default-src 'self' data: 'unsafe-inline'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src data:;">
|
||
<title>OpenSwarm</title>
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
html, body {
|
||
width: 100%; height: 100%; overflow: hidden;
|
||
background: #0a0a10;
|
||
}
|
||
body {
|
||
-webkit-app-region: drag;
|
||
color: #e8e8f4;
|
||
font-family: -apple-system, "Segoe UI", system-ui, sans-serif;
|
||
user-select: none;
|
||
cursor: default;
|
||
}
|
||
#stage {
|
||
position: relative;
|
||
width: 100%; height: 100%;
|
||
display: flex; align-items: center; justify-content: center;
|
||
}
|
||
canvas#bg {
|
||
position: absolute; inset: 0;
|
||
width: 100%; height: 100%;
|
||
image-rendering: pixelated;
|
||
image-rendering: crisp-edges;
|
||
-ms-interpolation-mode: nearest-neighbor;
|
||
}
|
||
.center {
|
||
position: relative; z-index: 1;
|
||
display: flex; flex-direction: column;
|
||
align-items: center; gap: 14px;
|
||
padding: 0 24px;
|
||
max-width: 100%;
|
||
}
|
||
/* Logo is rendered inside the WebGL canvas now (sampled as a texture
|
||
so the dither pattern can flow through it). The <img> stays in the
|
||
DOM purely as the WebGL-failure fallback; CSS hides it once the
|
||
shader has uploaded the texture, and reveals it if WebGL init fails. */
|
||
.logo {
|
||
width: 84px; height: 84px;
|
||
image-rendering: pixelated;
|
||
image-rendering: crisp-edges;
|
||
filter: drop-shadow(0 0 18px rgba(160, 100, 240, 0.55));
|
||
opacity: 0;
|
||
animation: fadeIn 700ms ease forwards 60ms;
|
||
}
|
||
/* `visibility: hidden` (not display:none) so the layout slot is preserved
|
||
— the shader queries .logo.getBoundingClientRect() to know where to
|
||
render the dithered octopus, and we want that slot to keep flexbox
|
||
centering the brand text + status text below it. */
|
||
.logo.shader-active { visibility: hidden; }
|
||
.name {
|
||
font-size: 14px; font-weight: 600; letter-spacing: 0.32em;
|
||
text-transform: uppercase;
|
||
color: #f0f0fa;
|
||
text-shadow: 0 0 12px rgba(0,0,0,0.6);
|
||
opacity: 0;
|
||
animation: fadeIn 700ms ease forwards 220ms;
|
||
}
|
||
.status {
|
||
font-size: 11.5px; font-weight: 500;
|
||
color: rgba(232, 232, 244, 0.7);
|
||
text-align: center; min-height: 16px;
|
||
letter-spacing: 0.05em;
|
||
text-shadow: 0 0 8px rgba(0,0,0,0.7);
|
||
transition: color 240ms ease;
|
||
opacity: 0;
|
||
animation: fadeIn 700ms ease forwards 360ms;
|
||
max-width: 380px;
|
||
}
|
||
.status.warning { color: rgba(255, 198, 110, 0.92); }
|
||
.status.error { color: rgba(255, 118, 118, 0.95); }
|
||
.actions {
|
||
display: none; gap: 8px; margin-top: 4px;
|
||
}
|
||
.actions.visible { display: flex; }
|
||
button {
|
||
-webkit-app-region: no-drag;
|
||
background: rgba(140, 100, 220, 0.16);
|
||
border: 1px solid rgba(170, 130, 240, 0.45);
|
||
color: #e8e8f4;
|
||
padding: 6px 14px; border-radius: 4px;
|
||
font-family: inherit;
|
||
font-size: 10.5px; font-weight: 600;
|
||
letter-spacing: 0.08em; text-transform: uppercase;
|
||
cursor: pointer;
|
||
transition: background 180ms ease, border-color 180ms ease;
|
||
}
|
||
button:hover {
|
||
background: rgba(170, 130, 240, 0.30);
|
||
border-color: rgba(190, 150, 255, 0.7);
|
||
}
|
||
button:active { transform: translateY(1px); }
|
||
pre.logs {
|
||
-webkit-app-region: no-drag;
|
||
display: none;
|
||
background: rgba(0, 0, 0, 0.55);
|
||
border: 1px solid rgba(255, 118, 118, 0.4);
|
||
color: #e8e8f4;
|
||
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
||
font-size: 9px; line-height: 1.45;
|
||
padding: 6px 8px;
|
||
width: 360px; max-height: 70px;
|
||
overflow: auto; white-space: pre-wrap; word-break: break-all;
|
||
border-radius: 3px;
|
||
user-select: text; cursor: text;
|
||
}
|
||
pre.logs.visible { display: block; }
|
||
@keyframes fadeIn {
|
||
from { opacity: 0; transform: translateY(2px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="stage">
|
||
<canvas id="bg"></canvas>
|
||
<div class="center">
|
||
<img class="logo" src="__OPENSWARM_LOGO__" alt="">
|
||
<div class="name">OpenSwarm</div>
|
||
<div class="status" id="status">Starting…</div>
|
||
<div class="actions" id="actions">
|
||
<button id="logsBtn">View logs</button>
|
||
<button id="restartBtn">Restart</button>
|
||
<button id="quitBtn">Quit</button>
|
||
</div>
|
||
<pre class="logs" id="logsPanel"></pre>
|
||
</div>
|
||
</div>
|
||
<script>
|
||
(function () {
|
||
'use strict';
|
||
const { ipcRenderer } = require('electron');
|
||
|
||
// -----------------------------------------------------------------
|
||
// Dither shader background. WebGL-first; falls back to a solid dark
|
||
// panel if WebGL init fails (very old GPUs / locked-down enterprise
|
||
// VMs). Logo + text remain visible either way.
|
||
// -----------------------------------------------------------------
|
||
const canvas = document.getElementById('bg');
|
||
let renderFrame = function () {}; // no-op until WebGL initializes
|
||
|
||
function resizeCanvas() {
|
||
const dpr = Math.min(window.devicePixelRatio || 1, 1.5);
|
||
const w = Math.max(1, Math.floor(canvas.clientWidth * dpr));
|
||
const h = Math.max(1, Math.floor(canvas.clientHeight * dpr));
|
||
if (canvas.width !== w || canvas.height !== h) {
|
||
canvas.width = w; canvas.height = h;
|
||
}
|
||
}
|
||
|
||
function initWebGL() {
|
||
const gl = canvas.getContext('webgl', { antialias: false, alpha: false, premultipliedAlpha: false });
|
||
if (!gl) throw new Error('webgl unavailable');
|
||
|
||
const vsSrc = [
|
||
'attribute vec2 a_pos;',
|
||
'void main() { gl_Position = vec4(a_pos, 0.0, 1.0); }'
|
||
].join('\n');
|
||
|
||
// Port of the reactbits Dither shader (https://reactbits.dev/backgrounds/dither)
|
||
// — same Perlin noise + fbm + 8×8 Bayer + N-color quantization. Difference
|
||
// from upstream: we render the wave AND the octopus icon overlay in a
|
||
// single fragment program (one draw call), and the per-pixel "downsample
|
||
// to fat pixels then dither" trick is folded into the same pass instead of
|
||
// being a postprocessing EffectComposer chain. Costs the same per pixel
|
||
// as upstream, no postprocess RTT.
|
||
//
|
||
// Octopus overlay: pixel-art icon sampled at NEAREST (so the dither
|
||
// doesn't smear the source pixels into a blur first). Its body color
|
||
// gets quantized through the same dither so it visually belongs in
|
||
// the same stippled world as the background.
|
||
const fsSrc = [
|
||
'precision highp float;',
|
||
'uniform vec2 u_res;',
|
||
'uniform float u_time;',
|
||
'uniform float u_dpr;',
|
||
'uniform sampler2D u_icon;',
|
||
'uniform vec2 u_icon_center_px;',
|
||
'uniform float u_icon_size_px;',
|
||
'uniform vec3 u_wave_color;',
|
||
'uniform float u_wave_speed;',
|
||
'uniform float u_wave_freq;',
|
||
'uniform float u_wave_amp;',
|
||
'uniform float u_color_num;',
|
||
'uniform float u_pixel_size;',
|
||
'',
|
||
// ----- Classic Perlin noise (Stefan Gustavson) -----
|
||
'vec4 mod289(vec4 x){return x-floor(x*(1.0/289.0))*289.0;}',
|
||
'vec4 permute(vec4 x){return mod289(((x*34.0)+1.0)*x);}',
|
||
'vec4 taylorInvSqrt(vec4 r){return 1.79284291400159-0.85373472095314*r;}',
|
||
'vec2 fade(vec2 t){return t*t*t*(t*(t*6.0-15.0)+10.0);}',
|
||
'float cnoise(vec2 P){',
|
||
' vec4 Pi=floor(P.xyxy)+vec4(0.0,0.0,1.0,1.0);',
|
||
' vec4 Pf=fract(P.xyxy)-vec4(0.0,0.0,1.0,1.0);',
|
||
' Pi=mod289(Pi);',
|
||
' vec4 ix=Pi.xzxz; vec4 iy=Pi.yyww;',
|
||
' vec4 fx=Pf.xzxz; vec4 fy=Pf.yyww;',
|
||
' vec4 i=permute(permute(ix)+iy);',
|
||
' vec4 gx=fract(i*(1.0/41.0))*2.0-1.0;',
|
||
' vec4 gy=abs(gx)-0.5;',
|
||
' vec4 tx=floor(gx+0.5); gx=gx-tx;',
|
||
' vec2 g00=vec2(gx.x,gy.x); vec2 g10=vec2(gx.y,gy.y);',
|
||
' vec2 g01=vec2(gx.z,gy.z); vec2 g11=vec2(gx.w,gy.w);',
|
||
' vec4 norm=taylorInvSqrt(vec4(dot(g00,g00),dot(g01,g01),dot(g10,g10),dot(g11,g11)));',
|
||
' g00*=norm.x; g01*=norm.y; g10*=norm.z; g11*=norm.w;',
|
||
' float n00=dot(g00,vec2(fx.x,fy.x));',
|
||
' float n10=dot(g10,vec2(fx.y,fy.y));',
|
||
' float n01=dot(g01,vec2(fx.z,fy.z));',
|
||
' float n11=dot(g11,vec2(fx.w,fy.w));',
|
||
' vec2 fade_xy=fade(Pf.xy);',
|
||
' vec2 n_x=mix(vec2(n00,n01),vec2(n10,n11),fade_xy.x);',
|
||
' return 2.3*mix(n_x.x,n_x.y,fade_xy.y);',
|
||
'}',
|
||
'',
|
||
// ----- 4-octave fractal-Brownian-motion of |cnoise| -----
|
||
'float fbm(vec2 p){',
|
||
' float v=0.0; float amp=1.0; float freq=u_wave_freq;',
|
||
' for(int i=0;i<4;i++){',
|
||
' v+=amp*abs(cnoise(p));',
|
||
' p*=freq;',
|
||
' amp*=u_wave_amp;',
|
||
' }',
|
||
' return v;',
|
||
'}',
|
||
'float pattern(vec2 p){',
|
||
' vec2 p2=p-u_time*u_wave_speed;',
|
||
' return fbm(p+fbm(p2));',
|
||
'}',
|
||
'',
|
||
// ----- 8×8 Bayer threshold (computed inline — no texture lookup) -----
|
||
'float bayer8(vec2 c){',
|
||
' int x=int(mod(c.x,8.0));',
|
||
' int y=int(mod(c.y,8.0));',
|
||
// Inline lookup: avoids the WebGL1 dynamic-array-indexing footgun that
|
||
// killed earlier tries. 64 if-statements compile to a fast jump.
|
||
' if(y==0){if(x==0)return 0.0/64.0;if(x==1)return 48.0/64.0;if(x==2)return 12.0/64.0;if(x==3)return 60.0/64.0;if(x==4)return 3.0/64.0;if(x==5)return 51.0/64.0;if(x==6)return 15.0/64.0;return 63.0/64.0;}',
|
||
' if(y==1){if(x==0)return 32.0/64.0;if(x==1)return 16.0/64.0;if(x==2)return 44.0/64.0;if(x==3)return 28.0/64.0;if(x==4)return 35.0/64.0;if(x==5)return 19.0/64.0;if(x==6)return 47.0/64.0;return 31.0/64.0;}',
|
||
' if(y==2){if(x==0)return 8.0/64.0;if(x==1)return 56.0/64.0;if(x==2)return 4.0/64.0;if(x==3)return 52.0/64.0;if(x==4)return 11.0/64.0;if(x==5)return 59.0/64.0;if(x==6)return 7.0/64.0;return 55.0/64.0;}',
|
||
' if(y==3){if(x==0)return 40.0/64.0;if(x==1)return 24.0/64.0;if(x==2)return 36.0/64.0;if(x==3)return 20.0/64.0;if(x==4)return 43.0/64.0;if(x==5)return 27.0/64.0;if(x==6)return 39.0/64.0;return 23.0/64.0;}',
|
||
' if(y==4){if(x==0)return 2.0/64.0;if(x==1)return 50.0/64.0;if(x==2)return 14.0/64.0;if(x==3)return 62.0/64.0;if(x==4)return 1.0/64.0;if(x==5)return 49.0/64.0;if(x==6)return 13.0/64.0;return 61.0/64.0;}',
|
||
' if(y==5){if(x==0)return 34.0/64.0;if(x==1)return 18.0/64.0;if(x==2)return 46.0/64.0;if(x==3)return 30.0/64.0;if(x==4)return 33.0/64.0;if(x==5)return 17.0/64.0;if(x==6)return 45.0/64.0;return 29.0/64.0;}',
|
||
' if(y==6){if(x==0)return 10.0/64.0;if(x==1)return 58.0/64.0;if(x==2)return 6.0/64.0;if(x==3)return 54.0/64.0;if(x==4)return 9.0/64.0;if(x==5)return 57.0/64.0;if(x==6)return 5.0/64.0;return 53.0/64.0;}',
|
||
' if(x==0)return 42.0/64.0;if(x==1)return 26.0/64.0;if(x==2)return 38.0/64.0;if(x==3)return 22.0/64.0;if(x==4)return 41.0/64.0;if(x==5)return 25.0/64.0;if(x==6)return 37.0/64.0;return 21.0/64.0;',
|
||
'}',
|
||
'',
|
||
// ----- N-color ordered dither, exactly matching reactbits -----
|
||
'vec3 dither(vec2 pixCoord, vec3 color){',
|
||
' float threshold = bayer8(pixCoord) - 0.25;',
|
||
' float stepSize = 1.0 / (u_color_num - 1.0);',
|
||
' color += threshold * stepSize;',
|
||
' color = clamp(color - 0.2, 0.0, 1.0);',
|
||
' return floor(color*(u_color_num - 1.0) + 0.5) / (u_color_num - 1.0);',
|
||
'}',
|
||
'',
|
||
'void main() {',
|
||
' vec2 fc = gl_FragCoord.xy;',
|
||
// Snap to fat pixel grid (matches reactbits pixelSize behavior)
|
||
' vec2 snapped = floor(fc / u_pixel_size) * u_pixel_size;',
|
||
// ----- Background: cnoise/fbm pattern in aspect-corrected coords -----
|
||
' vec2 uv = snapped / u_res - 0.5;',
|
||
' uv.x *= u_res.x / u_res.y;',
|
||
' float f = pattern(uv);',
|
||
' vec3 col = mix(vec3(0.0), u_wave_color, f);',
|
||
'',
|
||
// ----- Foreground: octopus icon, integrated into the dithered world -----
|
||
// Goal: the octopus shouldn't look "stuck on top of" the background. It
|
||
// should feel like part of the same dithered terrain — same color
|
||
// grammar, same quantization. We achieve that by:
|
||
// (a) Letting some of the bg pattern bleed INTO the octopus body
|
||
// (multiply icon brightness by a subtle pattern factor).
|
||
// (b) Surrounding the icon with a soft glow that's also dithered
|
||
// (the glow shares the same palette as the icon body).
|
||
// (c) Boosting the icon's contrast slightly so it remains the focal
|
||
// point even with the integration.
|
||
// Eyes (very dark pixels) stay solid via a luminance early-out below.
|
||
' vec2 logical = fc / u_dpr;',
|
||
' logical.y = (u_res.y / u_dpr) - logical.y;',
|
||
' vec2 iconRel = (logical - u_icon_center_px) / u_icon_size_px + 0.5;',
|
||
// Distance-from-icon-center, 0 at center, 1 at edge of icon bbox, >1 outside
|
||
' vec2 iconLocal = (logical - u_icon_center_px) / (u_icon_size_px * 0.5);',
|
||
' float iconDist = length(iconLocal);',
|
||
// Soft glow ring around the icon — bleeds the icon color into bg waves
|
||
' float glow = exp(-iconDist * iconDist * 1.8);',
|
||
' vec3 glowColor = vec3(1.00, 0.45, 0.32);', // coral matches octopus body
|
||
' col = mix(col, glowColor, glow * 0.18);',
|
||
' if (iconRel.x >= 0.0 && iconRel.x <= 1.0 && iconRel.y >= 0.0 && iconRel.y <= 1.0) {',
|
||
' vec4 iconTex = texture2D(u_icon, iconRel);',
|
||
' if (iconTex.a > 0.05) {',
|
||
' float lum = dot(iconTex.rgb, vec3(0.30, 0.59, 0.11));',
|
||
' vec3 iconCol = iconTex.rgb;',
|
||
// Boost coral saturation slightly so the dither doesn\'t mute it.
|
||
' iconCol = mix(vec3(lum), iconCol, 1.15);',
|
||
// Bleed bg pattern into icon body (only on bright pixels, not eyes):
|
||
// multiply the body color by a subtle pattern factor so it shimmers
|
||
// with the same noise that drives the bg. Eyes (lum<0.2) skip this.
|
||
' if (lum > 0.20) {',
|
||
' float bleed = 0.85 + 0.30 * f;',
|
||
' iconCol *= bleed;',
|
||
' }',
|
||
' col = mix(col, iconCol, iconTex.a);',
|
||
' }',
|
||
' }',
|
||
'',
|
||
// ----- Final ordered-dither over the composited frame -----
|
||
// Apply dither to the WHOLE image (bg + icon together) so the icon and
|
||
// background share the same color quantization. Matches reactbits.
|
||
' vec2 ditherCoord = floor(fc / u_pixel_size);',
|
||
' col = dither(ditherCoord, col);',
|
||
'',
|
||
' gl_FragColor = vec4(col, 1.0);',
|
||
'}'
|
||
].join('\n');
|
||
|
||
function compile(type, src) {
|
||
const sh = gl.createShader(type);
|
||
gl.shaderSource(sh, src);
|
||
gl.compileShader(sh);
|
||
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
|
||
throw new Error('shader: ' + gl.getShaderInfoLog(sh));
|
||
}
|
||
return sh;
|
||
}
|
||
const vs = compile(gl.VERTEX_SHADER, vsSrc);
|
||
const fs = compile(gl.FRAGMENT_SHADER, fsSrc);
|
||
const prog = gl.createProgram();
|
||
gl.attachShader(prog, vs); gl.attachShader(prog, fs);
|
||
gl.linkProgram(prog);
|
||
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
|
||
throw new Error('link: ' + gl.getProgramInfoLog(prog));
|
||
}
|
||
gl.useProgram(prog);
|
||
|
||
// Fullscreen quad (two triangles).
|
||
const quad = new Float32Array([-1,-1, 1,-1, -1,1, -1,1, 1,-1, 1,1]);
|
||
const buf = gl.createBuffer();
|
||
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
|
||
gl.bufferData(gl.ARRAY_BUFFER, quad, gl.STATIC_DRAW);
|
||
const aPos = gl.getAttribLocation(prog, 'a_pos');
|
||
gl.enableVertexAttribArray(aPos);
|
||
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
|
||
|
||
// ------ Octopus icon texture (only texture we use; bayer lives inline in shader) ------
|
||
const iconTex = gl.createTexture();
|
||
gl.activeTexture(gl.TEXTURE0);
|
||
gl.bindTexture(gl.TEXTURE_2D, iconTex);
|
||
// 1-px transparent stub so the shader doesn't sample undefined memory
|
||
// before the PNG finishes decoding.
|
||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE,
|
||
new Uint8Array([0, 0, 0, 0]));
|
||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||
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);
|
||
|
||
var iconLoaded = false;
|
||
var logoEl = document.querySelector('.logo');
|
||
function uploadIcon() {
|
||
if (iconLoaded || !logoEl || !logoEl.naturalWidth) return;
|
||
gl.activeTexture(gl.TEXTURE0);
|
||
gl.bindTexture(gl.TEXTURE_2D, iconTex);
|
||
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
|
||
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
|
||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, logoEl);
|
||
iconLoaded = true;
|
||
// Hide the DOM <img> now that the texture is on the GPU.
|
||
logoEl.classList.add('shader-active');
|
||
}
|
||
if (logoEl) {
|
||
if (logoEl.complete && logoEl.naturalWidth > 0) uploadIcon();
|
||
else logoEl.addEventListener('load', uploadIcon, { once: true });
|
||
}
|
||
|
||
const uRes = gl.getUniformLocation(prog, 'u_res');
|
||
const uTime = gl.getUniformLocation(prog, 'u_time');
|
||
const uDpr = gl.getUniformLocation(prog, 'u_dpr');
|
||
const uIcon = gl.getUniformLocation(prog, 'u_icon');
|
||
const uIconCenter = gl.getUniformLocation(prog, 'u_icon_center_px');
|
||
const uIconSize = gl.getUniformLocation(prog, 'u_icon_size_px');
|
||
const uWaveColor = gl.getUniformLocation(prog, 'u_wave_color');
|
||
const uWaveSpeed = gl.getUniformLocation(prog, 'u_wave_speed');
|
||
const uWaveFreq = gl.getUniformLocation(prog, 'u_wave_freq');
|
||
const uWaveAmp = gl.getUniformLocation(prog, 'u_wave_amp');
|
||
const uColorNum = gl.getUniformLocation(prog, 'u_color_num');
|
||
const uPixelSize = gl.getUniformLocation(prog, 'u_pixel_size');
|
||
gl.uniform1i(uIcon, 0);
|
||
// Warm magenta-leaning purple — the orange tilt resonates with the
|
||
// octopus's coral body, so the icon and bg feel like the same world.
|
||
// Pure purple (0.52, 0.29, 0.88) was too cool and the octopus looked
|
||
// pasted on; pure orange would clash with brand. This sits between.
|
||
gl.uniform3f(uWaveColor, 0.78, 0.32, 0.62);
|
||
gl.uniform1f(uWaveSpeed, 0.05);
|
||
gl.uniform1f(uWaveFreq, 3.0);
|
||
gl.uniform1f(uWaveAmp, 0.3);
|
||
gl.uniform1f(uColorNum, 4.0);
|
||
gl.uniform1f(uPixelSize, 2.0);
|
||
|
||
renderFrame = function (tSec) {
|
||
resizeCanvas();
|
||
const dpr = Math.min(window.devicePixelRatio || 1, 1.5);
|
||
// Octopus geometry: read the live .logo element's bounding rect so
|
||
// the shader draws the dithered icon EXACTLY where the (now-hidden)
|
||
// <img> would have appeared. This stays correct if CSS sizing
|
||
// changes or window is resized — no constants to drift.
|
||
var rect = logoEl ? logoEl.getBoundingClientRect() : null;
|
||
var cx, cy, sz;
|
||
if (rect && rect.width > 0) {
|
||
cx = rect.left + rect.width * 0.5;
|
||
cy = rect.top + rect.height * 0.5;
|
||
sz = Math.max(rect.width, rect.height);
|
||
} else {
|
||
// Fallback: center horizontally, top third vertically.
|
||
var cssW = canvas.width / dpr, cssH = canvas.height / dpr;
|
||
cx = cssW * 0.5; cy = cssH * 0.35; sz = 96;
|
||
}
|
||
gl.viewport(0, 0, canvas.width, canvas.height);
|
||
gl.uniform2f(uRes, canvas.width, canvas.height);
|
||
gl.uniform1f(uTime, tSec);
|
||
gl.uniform1f(uDpr, dpr);
|
||
gl.uniform2f(uIconCenter, cx, cy);
|
||
gl.uniform1f(uIconSize, sz);
|
||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||
};
|
||
}
|
||
|
||
try { initWebGL(); } catch (e) {
|
||
console.warn('[splash] WebGL fallback:', e && e.message);
|
||
}
|
||
|
||
let running = true;
|
||
const start = performance.now();
|
||
function loop() {
|
||
if (!running) return;
|
||
renderFrame((performance.now() - start) * 0.001);
|
||
requestAnimationFrame(loop);
|
||
}
|
||
resizeCanvas();
|
||
requestAnimationFrame(loop);
|
||
|
||
// Free the GPU once main window takes over so we don't burn cycles
|
||
// during the user's workflow.
|
||
window.addEventListener('beforeunload', function () { running = false; });
|
||
document.addEventListener('visibilitychange', function () {
|
||
if (document.hidden) running = false;
|
||
});
|
||
|
||
// -----------------------------------------------------------------
|
||
// Status updates from main process. payload is either a plain string
|
||
// (info) or { text, level, logs?, showActions? }.
|
||
// -----------------------------------------------------------------
|
||
const statusEl = document.getElementById('status');
|
||
const actionsEl = document.getElementById('actions');
|
||
const logsPanelEl = document.getElementById('logsPanel');
|
||
|
||
// Once the action buttons have appeared (long-running boot or hard
|
||
// failure), keep them visible across subsequent status updates —
|
||
// hiding them would yank a target the user is reading/about to click.
|
||
let actionsLatched = false;
|
||
ipcRenderer.on('splash:status', function (_e, payload) {
|
||
if (typeof payload === 'string') {
|
||
statusEl.textContent = payload;
|
||
statusEl.className = 'status';
|
||
return;
|
||
}
|
||
const text = (payload && payload.text) || '';
|
||
const level = (payload && payload.level) || 'info';
|
||
statusEl.textContent = text;
|
||
statusEl.className = 'status' + (level !== 'info' ? ' ' + level : '');
|
||
if (payload && payload.showActions) {
|
||
actionsLatched = true;
|
||
actionsEl.classList.add('visible');
|
||
} else if (!actionsLatched) {
|
||
actionsEl.classList.remove('visible');
|
||
}
|
||
if (payload && payload.logs) {
|
||
logsPanelEl.textContent = payload.logs;
|
||
logsPanelEl.classList.add('visible');
|
||
} else if (!actionsLatched) {
|
||
logsPanelEl.classList.remove('visible');
|
||
}
|
||
});
|
||
|
||
document.getElementById('logsBtn').addEventListener('click', function () {
|
||
ipcRenderer.send('splash:action', 'open-logs');
|
||
});
|
||
document.getElementById('restartBtn').addEventListener('click', function () {
|
||
ipcRenderer.send('splash:action', 'restart');
|
||
});
|
||
document.getElementById('quitBtn').addEventListener('click', function () {
|
||
ipcRenderer.send('splash:action', 'quit');
|
||
});
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|