Files
openswarm/electron/splash/splash.html
T
2026-04-25 03:05:08 -07:00

304 lines
10 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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 {
width: 84px; height: 84px;
image-rendering: auto;
filter: drop-shadow(0 0 18px rgba(160, 100, 240, 0.55));
opacity: 0;
animation: fadeIn 700ms ease forwards 60ms;
}
.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&hellip;</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');
// Animated layered fluid (sin/cos blend) → bayer-dithered into 2 colors.
const fsSrc = [
'precision highp float;',
'uniform vec2 u_res;',
'uniform float u_time;',
'uniform sampler2D u_bayer;',
'void main() {',
' vec2 uv = gl_FragCoord.xy / u_res;',
' float t = u_time;',
' float a = sin(uv.x * 5.2 + t * 0.42) * cos(uv.y * 3.8 + t * 0.29);',
' float b = sin((uv.x + uv.y) * 4.6 + t * 0.58) * 0.55;',
' float c = cos(uv.x * 7.1 - uv.y * 5.3 + t * 0.21) * 0.35;',
' float fluid = (a + b + c) * 0.32 + 0.5;',
' vec2 bUv = fract(gl_FragCoord.xy / 4.0);',
' float thresh = texture2D(u_bayer, bUv).r;',
' float lit = step(thresh, fluid);',
' vec3 dark = vec3(0.038, 0.038, 0.062);', // ~#0a0a10
' vec3 lite = vec3(0.522, 0.286, 0.878);', // ~#854ae0 (electric purple)
' gl_FragColor = vec4(mix(dark, lite, lit), 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);
// 4×4 Bayer ordered-dither matrix as a luminance texture.
const bayer = new Uint8Array([
0, 8, 2, 10,
12, 4, 14, 6,
3, 11, 1, 9,
15, 7, 13, 5
].map(function (v) { return Math.floor(v * 255 / 16); }));
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, 4, 4, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, bayer);
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.REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
const uRes = gl.getUniformLocation(prog, 'u_res');
const uTime = gl.getUniformLocation(prog, 'u_time');
const uTex = gl.getUniformLocation(prog, 'u_bayer');
gl.uniform1i(uTex, 0);
renderFrame = function (tSec) {
resizeCanvas();
gl.viewport(0, 0, canvas.width, canvas.height);
gl.uniform2f(uRes, canvas.width, canvas.height);
gl.uniform1f(uTime, tSec);
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>