mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-10-20 23:11:29 +02:00
(138MB → 7MB), pre-compile python bytecode, parallelize Widevine, never silently quit
304 lines
10 KiB
HTML
304 lines
10 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 {
|
||
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…</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>
|