mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-29 04:54:49 +02:00
[eric] splash for start up ux improvements
This commit is contained in:
+26
-5
@@ -166,6 +166,30 @@ function emitSplashStatus(payload) {
|
||||
}
|
||||
}
|
||||
|
||||
// OS-tailored status copy. The "first launch is slow" experience has very
|
||||
// different causes per platform (Defender on Windows, Gatekeeper +
|
||||
// XProtect notarization scan on macOS), and naming the actual culprit
|
||||
// helps users feel like the wait is intentional rather than the app being
|
||||
// broken. Used by the long-wait branches in waitForBackend below.
|
||||
function osStillStartingText() {
|
||||
if (process.platform === 'win32') {
|
||||
return 'Still starting — Windows Defender is scanning files (first launch only)…';
|
||||
}
|
||||
if (process.platform === 'darwin') {
|
||||
return 'Still starting — macOS is verifying the bundle (first launch only)…';
|
||||
}
|
||||
return 'Still starting (first launch is slower than subsequent launches)…';
|
||||
}
|
||||
function osTakingTooLongText() {
|
||||
if (process.platform === 'win32') {
|
||||
return 'Backend is taking longer than usual. Defender scans of 14k files can take a few minutes on slow drives.';
|
||||
}
|
||||
if (process.platform === 'darwin') {
|
||||
return 'Backend is taking longer than usual. macOS first-launch checks can be slow on cold cache.';
|
||||
}
|
||||
return 'Backend is taking longer than usual. You can wait, view logs, or restart.';
|
||||
}
|
||||
|
||||
/**
|
||||
* macOS GUI apps launched from Finder/Dock inherit a minimal PATH from launchd
|
||||
* (/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin) — none of the user's shell
|
||||
@@ -294,15 +318,12 @@ function waitForBackend(port, opts = {}) {
|
||||
const elapsed = Date.now() - start;
|
||||
if (elapsed > 60_000 && !stillStartingNotified) {
|
||||
stillStartingNotified = true;
|
||||
emitSplashStatus({
|
||||
text: 'Still starting (first launch can take 2-3 minutes on Windows while Defender scans)…',
|
||||
level: 'warning',
|
||||
});
|
||||
emitSplashStatus({ text: osStillStartingText(), level: 'warning' });
|
||||
}
|
||||
if (elapsed > 180_000 && !actionsShown) {
|
||||
actionsShown = true;
|
||||
emitSplashStatus({
|
||||
text: 'Backend is taking unusually long. You can wait, view logs, or restart.',
|
||||
text: osTakingTooLongText(),
|
||||
level: 'warning',
|
||||
showActions: true,
|
||||
logs: recentBackendStderr.slice(-20).join(''),
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
// Standalone splash preview — opens the splash window in isolation and
|
||||
// cycles through every status state so you can eyeball the UI without
|
||||
// running the full backend + frontend + electron stack.
|
||||
//
|
||||
// Run from the electron/ dir:
|
||||
// npx electron splash/preview.js
|
||||
//
|
||||
// Cycle (~26s total, then idles on the error state with action buttons):
|
||||
// 0s "Starting OpenSwarm…"
|
||||
// 3s "Connecting to dev backend…"
|
||||
// 6s "Loading components…"
|
||||
// 9s "Almost ready…"
|
||||
// 12s "Still starting (warning, no actions)"
|
||||
// 18s "Backend taking too long" (with View Logs / Restart / Quit buttons)
|
||||
// 24s error state: "OpenSwarm couldn't start" + last lines of stderr
|
||||
|
||||
const { app, BrowserWindow, ipcMain, shell } = require('electron');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
let splashWindow = null;
|
||||
|
||||
function loadSplashDataUrl() {
|
||||
const html = fs.readFileSync(path.join(__dirname, 'splash.html'), 'utf8');
|
||||
const iconPath = path.join(__dirname, '..', 'build', 'icon.png');
|
||||
const iconBytes = fs.readFileSync(iconPath);
|
||||
const iconDataUrl = 'data:image/png;base64,' + iconBytes.toString('base64');
|
||||
const finalHtml = html.replace('__OPENSWARM_LOGO__', iconDataUrl);
|
||||
return 'data:text/html;charset=utf-8;base64,' + Buffer.from(finalHtml).toString('base64');
|
||||
}
|
||||
|
||||
function emit(payload) {
|
||||
if (splashWindow && !splashWindow.isDestroyed()) {
|
||||
splashWindow.webContents.send('splash:status', payload);
|
||||
const txt = typeof payload === 'string' ? payload : payload.text;
|
||||
console.log(`[preview] -> ${txt}`);
|
||||
}
|
||||
}
|
||||
|
||||
ipcMain.on('splash:action', (_e, action) => {
|
||||
console.log(`[preview] splash button clicked: ${action}`);
|
||||
if (action === 'quit') app.quit();
|
||||
else if (action === 'restart') { app.relaunch(); app.exit(0); }
|
||||
else if (action === 'open-logs') console.log('[preview] (would open log dir)');
|
||||
});
|
||||
|
||||
app.whenReady().then(() => {
|
||||
splashWindow = new BrowserWindow({
|
||||
width: 460,
|
||||
height: 340,
|
||||
frame: false,
|
||||
resizable: false,
|
||||
skipTaskbar: false, // keep visible in taskbar for the preview
|
||||
show: true,
|
||||
center: true,
|
||||
backgroundColor: '#0a0a10',
|
||||
title: 'OpenSwarm splash preview',
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
sandbox: false,
|
||||
backgroundThrottling: false,
|
||||
},
|
||||
});
|
||||
splashWindow.setMenuBarVisibility(false);
|
||||
splashWindow.loadURL(loadSplashDataUrl());
|
||||
|
||||
// Cycle through every state with delays so you can see each one.
|
||||
splashWindow.webContents.once('did-finish-load', () => {
|
||||
setTimeout(() => emit('Starting OpenSwarm…'), 0);
|
||||
setTimeout(() => emit('Connecting to dev backend…'), 3000);
|
||||
setTimeout(() => emit('Loading components…'), 6000);
|
||||
setTimeout(() => emit('Almost ready…'), 9000);
|
||||
// Mirror the OS-tailored copy from main.js so the preview shows what
|
||||
// real users would see on this platform.
|
||||
const stillStarting = process.platform === 'win32'
|
||||
? 'Still starting — Windows Defender is scanning files (first launch only)…'
|
||||
: process.platform === 'darwin'
|
||||
? 'Still starting — macOS is verifying the bundle (first launch only)…'
|
||||
: 'Still starting (first launch is slower than subsequent launches)…';
|
||||
const takingTooLong = process.platform === 'win32'
|
||||
? 'Backend is taking longer than usual. Defender scans of 14k files can take a few minutes on slow drives.'
|
||||
: process.platform === 'darwin'
|
||||
? 'Backend is taking longer than usual. macOS first-launch checks can be slow on cold cache.'
|
||||
: 'Backend is taking longer than usual. You can wait, view logs, or restart.';
|
||||
setTimeout(() => emit({ text: stillStarting, level: 'warning' }), 12000);
|
||||
setTimeout(() => emit({
|
||||
text: takingTooLong,
|
||||
level: 'warning',
|
||||
showActions: true,
|
||||
logs: '[backend] uvicorn warming up...\n[backend] importing claude_agent_sdk...\n[backend] (still working)',
|
||||
}), 18000);
|
||||
setTimeout(() => emit({
|
||||
text: "OpenSwarm couldn't start: Backend process exited with code 1 during startup",
|
||||
level: 'error',
|
||||
showActions: true,
|
||||
logs: '[backend] Traceback (most recent call last):\n[backend] File "backend/main.py", line 48\n[backend] ImportError: simulated failure for splash preview',
|
||||
}), 24000);
|
||||
});
|
||||
|
||||
splashWindow.on('closed', () => { app.quit(); });
|
||||
});
|
||||
+227
-31
@@ -36,13 +36,23 @@
|
||||
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: auto;
|
||||
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;
|
||||
@@ -152,25 +162,157 @@
|
||||
'void main() { gl_Position = vec4(a_pos, 0.0, 1.0); }'
|
||||
].join('\n');
|
||||
|
||||
// Animated layered fluid (sin/cos blend) → bayer-dithered into 2 colors.
|
||||
// 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 sampler2D u_bayer;',
|
||||
'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 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);',
|
||||
' 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');
|
||||
|
||||
@@ -202,31 +344,85 @@
|
||||
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);
|
||||
// ------ 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.REPEAT);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
|
||||
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);
|
||||
|
||||
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);
|
||||
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);
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user