PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.2
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.2
1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 All 34 releases
desktop-mode / assets / js / animated-logo-wallpaper.js

animated-logo-wallpaper.js in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 0.9.2, at assets/js/animated-logo-wallpaper.js

426 lines 13.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 (function() {
2 "use strict";
3 const CONFIG = {
4 /** Grid stride when sampling the logo PNG. Smaller → denser particle field → heavier frame cost. */
5 sampleStride: 7,
6 /** Alpha threshold (0–255) for "this pixel is part of the logo." */
7 alphaThreshold: 128,
8 /**
9 * Target logo rendering width in CSS pixels. Capped at this value
10 * on huge screens; on normal screens we take 72% of the smaller
11 * shell axis so the logo reads as "hero-sized" without cropping.
12 */
13 targetLogoWidth: 1e3,
14 /** Fraction of the smaller shell dimension the logo is allowed to occupy. */
15 logoShellFraction: 0.72,
16 /**
17 * Spring stiffness — how hard a particle pulls back to its home.
18 * Lower = slower, floatier return. At 0.015 the natural-frequency
19 * period is ~50 frames (~0.85 s at 60 fps), so particles visibly
20 * drift back after a cursor flick rather than snapping home.
21 */
22 springK: 0.015,
23 /** Velocity damping per tick. 1 = no damping, 0 = instant stop. */
24 damping: 0.86,
25 /**
26 * Velocity floor below which a particle is considered at rest —
27 * its position snaps to its home and its velocity zeroes out. Kills
28 * the subpixel jitter that made the resting logo flicker.
29 */
30 restVelocityEpsilon: 0.02,
31 /**
32 * Sand-drag brush radius in CSS pixels. Particles within this
33 * distance of the cursor pick up a fraction of the cursor's
34 * per-frame displacement — they're carried in the direction the
35 * cursor is moving, not pushed away from its position. Beyond
36 * the radius the cursor has no effect.
37 */
38 dragRadius: 150,
39 /**
40 * Base fraction of the cursor's per-frame displacement that a
41 * particle inherits when it's at the dead center of the brush.
42 * At 0.22 a particle in the brush core picks up roughly a
43 * quarter of the cursor's velocity per frame — enough to read
44 * as "dragged" without the particles chasing the cursor.
45 */
46 dragStrength: 0.22,
47 /**
48 * Super-linear speed boost. For every {@link dragBoostRefSpeed}
49 * pixels-per-frame of cursor speed, the applied drag force is
50 * additionally scaled by this factor. Kept gentle (0.3) so fast
51 * flicks feel a bit punchier than linear without flinging
52 * particles across the screen.
53 */
54 dragBoost: 0.3,
55 /** Reference cursor speed for the boost curve (CSS px / frame). */
56 dragBoostRefSpeed: 40,
57 /**
58 * Cap on the mouse delta a single frame can accumulate. Prevents
59 * a wild delta from a stale pointer (e.g. first pointermove after
60 * the cursor entered from offscreen) from launching particles
61 * into orbit. A real fast mouse rarely exceeds 80 px/frame.
62 */
63 maxMouseDelta: 80,
64 /**
65 * Radial-gradient brush texture size. Larger = smoother edges at
66 * the cost of texture memory. 128px is plenty — sprites scale
67 * down to 10–30 px range for rendering so we have headroom.
68 */
69 brushSize: 128,
70 /** Min/max sprite scale relative to the brush texture size. */
71 spriteScaleMin: 0.1,
72 spriteScaleMax: 0.26,
73 /** Min/max per-particle alpha. */
74 spriteAlphaMin: 0.55,
75 spriteAlphaMax: 0.92
76 };
77 const PARTICLE_PALETTE = [
78 // Rainbow six (higher weight — the flag's main body).
79 16726843,
80 16726843,
81 // red
82 16747562,
83 16747562,
84 // orange
85 16767293,
86 16767293,
87 // yellow
88 5036388,
89 5036388,
90 // green
91 4104447,
92 4104447,
93 // blue
94 11037695,
95 11037695,
96 // purple
97 // Trans flag stripes.
98 16757703,
99 // pink
100 8380415,
101 // light blue
102 16777215,
103 // white
104 // POC inclusion stripe.
105 13140042
106 // warm brown (boosted for visibility under additive)
107 ];
108 const BACKDROP_CSS = "radial-gradient(circle at 50% 50%, #1e40af 0%, #152a6b 45%, #0a1024 100%)";
109 async function mountScene({ container, logoUrl, prefersReducedMotion }) {
110 const pixi = window.PIXI;
111 if (!pixi) {
112 throw new Error(
113 "[animated-logo-wallpaper] window.PIXI is undefined; declare `needs: ['pixijs']` on the wallpaper def so the shell loads it before mount."
114 );
115 }
116 const homes = await sampleLogoHomes(logoUrl);
117 const priorBackground = container.style.background;
118 container.style.background = BACKDROP_CSS;
119 const app = new pixi.Application();
120 await app.init({
121 resizeTo: container,
122 backgroundAlpha: 0,
123 antialias: true,
124 autoDensity: true,
125 resolution: Math.min(window.devicePixelRatio || 1, 2)
126 });
127 container.appendChild(app.canvas);
128 applyCanvasLayout(app.canvas);
129 const brushTexture = buildBrushTexture(pixi);
130 const particleLayer = new pixi.Container();
131 app.stage.addChild(particleLayer);
132 const n = homes.length;
133 const homeX = new Float32Array(n);
134 const homeY = new Float32Array(n);
135 const x = new Float32Array(n);
136 const y = new Float32Array(n);
137 const vx = new Float32Array(n);
138 const vy = new Float32Array(n);
139 const sprites = new Array(n);
140 for (let i = 0; i < n; i++) {
141 const sprite = new pixi.Sprite(brushTexture);
142 sprite.anchor.set(0.5);
143 sprite.blendMode = "add";
144 sprite.tint = PARTICLE_PALETTE[Math.floor(Math.random() * PARTICLE_PALETTE.length)];
145 const scale = CONFIG.spriteScaleMin + Math.random() * (CONFIG.spriteScaleMax - CONFIG.spriteScaleMin);
146 sprite.scale.set(scale);
147 sprite.alpha = CONFIG.spriteAlphaMin + Math.random() * (CONFIG.spriteAlphaMax - CONFIG.spriteAlphaMin);
148 particleLayer.addChild(sprite);
149 sprites[i] = sprite;
150 }
151 let logoScale = 1;
152 let logoOffsetX = 0;
153 let logoOffsetY = 0;
154 const computeLayout = () => {
155 const w = app.canvas.clientWidth;
156 const h = app.canvas.clientHeight;
157 const target = Math.min(
158 CONFIG.targetLogoWidth,
159 Math.min(w, h) * CONFIG.logoShellFraction
160 );
161 logoScale = target;
162 logoOffsetX = (w - target) / 2;
163 logoOffsetY = (h - target) / 2;
164 for (let i = 0; i < n; i++) {
165 homeX[i] = logoOffsetX + homes[i][0] * logoScale;
166 homeY[i] = logoOffsetY + homes[i][1] * logoScale;
167 if (x[i] === 0 && y[i] === 0) {
168 x[i] = homeX[i];
169 y[i] = homeY[i];
170 }
171 }
172 };
173 computeLayout();
174 const resizeObserver = new ResizeObserver(() => computeLayout());
175 resizeObserver.observe(container);
176 let pointerX = -1e6;
177 let pointerY = -1e6;
178 let pointerActive = false;
179 let mouseDx = 0;
180 let mouseDy = 0;
181 const onPointerMove = (e) => {
182 const rect = app.canvas.getBoundingClientRect();
183 const nx = e.clientX - rect.left;
184 const ny = e.clientY - rect.top;
185 if (pointerActive) {
186 const rawDx = nx - pointerX;
187 const rawDy = ny - pointerY;
188 const cap = CONFIG.maxMouseDelta;
189 mouseDx += Math.max(-cap, Math.min(cap, rawDx));
190 mouseDy += Math.max(-cap, Math.min(cap, rawDy));
191 }
192 pointerX = nx;
193 pointerY = ny;
194 pointerActive = true;
195 };
196 const onPointerLeave = () => {
197 pointerX = -1e6;
198 pointerY = -1e6;
199 pointerActive = false;
200 mouseDx = 0;
201 mouseDy = 0;
202 };
203 window.addEventListener("pointermove", onPointerMove, { passive: true });
204 window.addEventListener("pointerleave", onPointerLeave);
205 let animating = !prefersReducedMotion;
206 const syncSprites = () => {
207 for (let i = 0; i < n; i++) {
208 sprites[i].x = x[i];
209 sprites[i].y = y[i];
210 }
211 };
212 const tick = () => {
213 if (animating) {
214 step(
215 n,
216 homeX,
217 homeY,
218 x,
219 y,
220 vx,
221 vy,
222 pointerX,
223 pointerY,
224 pointerActive ? mouseDx : 0,
225 pointerActive ? mouseDy : 0
226 );
227 }
228 mouseDx = 0;
229 mouseDy = 0;
230 syncSprites();
231 };
232 app.ticker.add(tick);
233 syncSprites();
234 if (!animating) {
235 app.renderer.render(app.stage);
236 app.ticker.stop();
237 }
238 return {
239 destroy() {
240 resizeObserver.disconnect();
241 window.removeEventListener("pointermove", onPointerMove);
242 window.removeEventListener("pointerleave", onPointerLeave);
243 app.destroy(true, {
244 children: true,
245 texture: true,
246 textureSource: true,
247 context: true
248 });
249 try {
250 brushTexture.destroy(true);
251 } catch {
252 }
253 container.style.background = priorBackground;
254 },
255 setAnimating(playing) {
256 animating = playing && !prefersReducedMotion;
257 if (animating) {
258 app.ticker.start();
259 } else {
260 app.ticker.stop();
261 }
262 }
263 };
264 }
265 function step(n, homeX, homeY, x, y, vx, vy, pointerX, pointerY, mouseDx, mouseDy) {
266 const {
267 springK,
268 damping,
269 dragRadius,
270 dragStrength,
271 dragBoost,
272 dragBoostRefSpeed,
273 restVelocityEpsilon
274 } = CONFIG;
275 const dragRadiusSq = dragRadius * dragRadius;
276 const restEpsSq = restVelocityEpsilon * restVelocityEpsilon;
277 const restPosEps = 0.25;
278 const restPosEpsSq = restPosEps * restPosEps;
279 const mouseSpeed = Math.sqrt(mouseDx * mouseDx + mouseDy * mouseDy);
280 const speedMultiplier = 1 + mouseSpeed / dragBoostRefSpeed * dragBoost;
281 const dragFx = mouseDx * dragStrength * speedMultiplier;
282 const dragFy = mouseDy * dragStrength * speedMultiplier;
283 const cursorMoving = mouseDx !== 0 || mouseDy !== 0;
284 for (let i = 0; i < n; i++) {
285 const dhx = homeX[i] - x[i];
286 const dhy = homeY[i] - y[i];
287 let fx = dhx * springK;
288 let fy = dhy * springK;
289 const dx = x[i] - pointerX;
290 const dy = y[i] - pointerY;
291 const distSq = dx * dx + dy * dy;
292 let disturbed = false;
293 if (cursorMoving && distSq < dragRadiusSq) {
294 const t = 1 - Math.sqrt(distSq) / dragRadius;
295 const falloff = t * t;
296 fx += dragFx * falloff;
297 fy += dragFy * falloff;
298 disturbed = true;
299 }
300 const nvx = (vx[i] + fx) * damping;
301 const nvy = (vy[i] + fy) * damping;
302 if (!disturbed && nvx * nvx + nvy * nvy < restEpsSq && dhx * dhx + dhy * dhy < restPosEpsSq) {
303 x[i] = homeX[i];
304 y[i] = homeY[i];
305 vx[i] = 0;
306 vy[i] = 0;
307 continue;
308 }
309 vx[i] = nvx;
310 vy[i] = nvy;
311 x[i] += nvx;
312 y[i] += nvy;
313 }
314 }
315 function buildBrushTexture(pixi) {
316 const size = CONFIG.brushSize;
317 const canvas = document.createElement("canvas");
318 canvas.width = size;
319 canvas.height = size;
320 const ctx = canvas.getContext("2d");
321 if (!ctx) {
322 throw new Error("[animated-logo-wallpaper] 2D canvas context unavailable.");
323 }
324 const center = size / 2;
325 const gradient = ctx.createRadialGradient(
326 center,
327 center,
328 0,
329 center,
330 center,
331 center
332 );
333 gradient.addColorStop(0, "rgba(255, 255, 255, 1)");
334 gradient.addColorStop(0.18, "rgba(255, 255, 255, 0.85)");
335 gradient.addColorStop(0.42, "rgba(255, 255, 255, 0.28)");
336 gradient.addColorStop(0.75, "rgba(255, 255, 255, 0.06)");
337 gradient.addColorStop(1, "rgba(255, 255, 255, 0)");
338 ctx.fillStyle = gradient;
339 ctx.fillRect(0, 0, size, size);
340 return pixi.Texture.from(canvas);
341 }
342 async function sampleLogoHomes(url) {
343 const img = await loadImage(url);
344 const maxSide = 400;
345 const ratio = img.naturalWidth / img.naturalHeight;
346 const sampleWidth = ratio >= 1 ? maxSide : Math.round(maxSide * ratio);
347 const sampleHeight = ratio >= 1 ? Math.round(maxSide / ratio) : maxSide;
348 const off = document.createElement("canvas");
349 off.width = sampleWidth;
350 off.height = sampleHeight;
351 const ctx = off.getContext("2d", { willReadFrequently: true });
352 if (!ctx) {
353 return [];
354 }
355 ctx.drawImage(img, 0, 0, sampleWidth, sampleHeight);
356 const data = ctx.getImageData(0, 0, sampleWidth, sampleHeight).data;
357 const homes = [];
358 const stride = CONFIG.sampleStride;
359 const threshold = CONFIG.alphaThreshold;
360 for (let row = 0; row < sampleHeight; row += stride) {
361 const rowOffset = row / stride % 2 === 0 ? 0 : stride / 2;
362 for (let col = 0; col < sampleWidth; col += stride) {
363 const px = Math.min(sampleWidth - 1, Math.round(col + rowOffset));
364 const py = row;
365 const alpha = data[(py * sampleWidth + px) * 4 + 3];
366 if (alpha > threshold) {
367 homes.push([px / sampleWidth, py / sampleHeight]);
368 }
369 }
370 }
371 return homes;
372 }
373 function loadImage(url) {
374 return new Promise((resolve, reject) => {
375 const img = new Image();
376 img.crossOrigin = "anonymous";
377 img.onload = () => resolve(img);
378 img.onerror = () => reject(new Error(`Failed to load logo: ${url}`));
379 img.src = url;
380 });
381 }
382 function applyCanvasLayout(canvas) {
383 canvas.style.display = "block";
384 canvas.style.width = "100%";
385 canvas.style.height = "100%";
386 }
387 const WALLPAPER_ID = "wp-animated-logo";
388 const PREVIEW = "radial-gradient(circle at 50% 50%, #1e3a8a 0%, #0b0f25 100%)";
389 const def = {
390 id: WALLPAPER_ID,
391 label: "Animated WordPress Logo",
392 type: "canvas",
393 preview: PREVIEW,
394 needs: ["pixijs"],
395 mount: async (container, ctx) => {
396 const logoUrl = `${ctx.pluginUrl}/assets/images/wp-logo.png`;
397 const scene = await mountScene({
398 container,
399 logoUrl,
400 prefersReducedMotion: ctx.prefersReducedMotion
401 });
402 const NAMESPACE = "desktop-mode/animated-logo";
403 const HOOK = "desktop-mode.wallpaper.visibility";
404 const api = window.wp?.desktop;
405 const visibilityHandler = (...args) => {
406 const detail = args[0];
407 if (!detail || detail.id !== WALLPAPER_ID) {
408 return;
409 }
410 scene.setAnimating(detail.state === "visible");
411 };
412 api?.hooks?.addAction(
413 HOOK,
414 `${NAMESPACE}/visibility`,
415 visibilityHandler
416 );
417 return () => {
418 api?.hooks?.removeAction(HOOK, `${NAMESPACE}/visibility`);
419 scene.destroy();
420 };
421 }
422 };
423 window.desktopModeWallpapers = window.desktopModeWallpapers || {};
424 window.desktopModeWallpapers[WALLPAPER_ID] = def;
425 })();
426