@lemoncat7/dsh-theme-xiaohei 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -42,6 +42,8 @@ export function installComposerBloub(doc = typeof document === 'undefined' ? und
42
42
  let hero = null, heroRow = null;
43
43
  let bounds;
44
44
  let layoutFrame, animationFrame, wake;
45
+ let frameTimer;
46
+ let lastPointerStamp = -Infinity;
45
47
  let time = 0, lastStamp, activeUntil = 2.5, actionUntil = 0, reactionIndex = 0;
46
48
  let pointerSide = 1;
47
49
  let special = false, specialIndex = 0;
@@ -67,9 +69,12 @@ export function installComposerBloub(doc = typeof document === 'undefined' ? und
67
69
  flight.cancel();
68
70
  if (animationFrame !== undefined)
69
71
  win.cancelAnimationFrame(animationFrame);
72
+ if (frameTimer !== undefined)
73
+ win.clearTimeout(frameTimer);
70
74
  if (wake !== undefined)
71
75
  win.clearTimeout(wake);
72
76
  animationFrame = undefined;
77
+ frameTimer = undefined;
73
78
  wake = undefined;
74
79
  lastStamp = undefined;
75
80
  }
@@ -105,7 +110,7 @@ export function installComposerBloub(doc = typeof document === 'undefined' ? und
105
110
  }
106
111
  function animateFor(seconds) {
107
112
  activeUntil = Math.max(activeUntil, time + seconds);
108
- if (!allowed() || animationFrame !== undefined)
113
+ if (!allowed() || animationFrame !== undefined || frameTimer !== undefined)
109
114
  return;
110
115
  if (wake !== undefined)
111
116
  win.clearTimeout(wake);
@@ -136,11 +141,17 @@ export function installComposerBloub(doc = typeof document === 'undefined' ? und
136
141
  // Stop only with open eyes. The next short life window resumes the same
137
142
  // clock, so neither gaze nor a partly completed blink can jump on waking.
138
143
  if (time < activeUntil || actionUntil || liveliness(time).lid < .99) {
139
- animationFrame = win.requestAnimationFrame(tick);
144
+ // SVG path deformation needs painting. Cap it instead of following a
145
+ // 120/144 Hz desktop display; preserve elapsed time and action continuity.
146
+ frameTimer = win.setTimeout(() => {
147
+ frameTimer = undefined;
148
+ if (allowed())
149
+ animationFrame = win.requestAnimationFrame(tick);
150
+ }, 1000 / 30);
140
151
  }
141
152
  else {
142
153
  lastStamp = undefined;
143
- wake = win.setTimeout(() => { wake = undefined; animateFor(2); }, 3200);
154
+ wake = win.setTimeout(() => { wake = undefined; animateFor(1); }, 8000);
144
155
  }
145
156
  }
146
157
  function syncPlayback() {
@@ -251,6 +262,9 @@ export function installComposerBloub(doc = typeof document === 'undefined' ? und
251
262
  leave();
252
263
  return;
253
264
  }
265
+ if (event.timeStamp - lastPointerStamp < 1000 / 30)
266
+ return;
267
+ lastPointerStamp = event.timeStamp;
254
268
  const dx = event.clientX - (bounds.left + bounds.width / 2);
255
269
  const dy = event.clientY - (bounds.top + bounds.height / 2);
256
270
  follow(dx, dy);
@@ -78,8 +78,8 @@ export const XIAOHEI_CONVERSATION_MESSAGES_CSS = `
78
78
  inset 0 1px 0 var(--xiaohei-conversation-highlight),
79
79
  inset 1px 0 0 color-mix(in srgb, var(--xiaohei-conversation-highlight) 42%, transparent),
80
80
  0 7px 22px var(--xiaohei-conversation-shadow) !important;
81
- -webkit-backdrop-filter: blur(var(--xiaohei-conversation-blur)) saturate(124%);
82
- backdrop-filter: blur(var(--xiaohei-conversation-blur)) saturate(124%);
81
+ -webkit-backdrop-filter: none;
82
+ backdrop-filter: none;
83
83
  overflow-wrap: anywhere;
84
84
  }
85
85
 
@@ -113,8 +113,8 @@ export const XIAOHEI_CONVERSATION_MESSAGES_CSS = `
113
113
  inset 0 1px 0 var(--xiaohei-conversation-highlight),
114
114
  inset -1px 0 0 color-mix(in srgb, var(--xiaohei-conversation-highlight) 42%, transparent),
115
115
  0 7px 22px var(--xiaohei-conversation-shadow) !important;
116
- -webkit-backdrop-filter: blur(var(--xiaohei-conversation-blur)) saturate(124%);
117
- backdrop-filter: blur(var(--xiaohei-conversation-blur)) saturate(124%);
116
+ -webkit-backdrop-filter: none;
117
+ backdrop-filter: none;
118
118
  overflow-wrap: anywhere;
119
119
  }
120
120
 
@@ -146,8 +146,8 @@ export const XIAOHEI_CONVERSATION_MESSAGES_CSS = `
146
146
  box-shadow:
147
147
  inset 0 1px 0 color-mix(in srgb, var(--xiaohei-conversation-highlight) 52%, transparent),
148
148
  0 4px 14px color-mix(in srgb, var(--xiaohei-conversation-shadow) 58%, transparent);
149
- -webkit-backdrop-filter: blur(12px) saturate(118%);
150
- backdrop-filter: blur(12px) saturate(118%);
149
+ -webkit-backdrop-filter: none;
150
+ backdrop-filter: none;
151
151
  overflow: hidden;
152
152
  }
153
153
 
@@ -158,8 +158,8 @@ export const XIAOHEI_CONVERSATION_MESSAGES_CSS = `
158
158
  border-radius: 9px;
159
159
  background: var(--xiaohei-conversation-meta-surface) !important;
160
160
  box-shadow: inset 0 1px 0 color-mix(in srgb, var(--xiaohei-conversation-highlight) 48%, transparent);
161
- -webkit-backdrop-filter: blur(10px) saturate(116%);
162
- backdrop-filter: blur(10px) saturate(116%);
161
+ -webkit-backdrop-filter: none;
162
+ backdrop-filter: none;
163
163
  }
164
164
 
165
165
  #root [data-chat-flow-kind='context'],
package/lib/client.js CHANGED
@@ -928,8 +928,8 @@ var XIAOHEI_CONVERSATION_MESSAGES_CSS = `
928
928
  inset 0 1px 0 var(--xiaohei-conversation-highlight),
929
929
  inset 1px 0 0 color-mix(in srgb, var(--xiaohei-conversation-highlight) 42%, transparent),
930
930
  0 7px 22px var(--xiaohei-conversation-shadow) !important;
931
- -webkit-backdrop-filter: blur(var(--xiaohei-conversation-blur)) saturate(124%);
932
- backdrop-filter: blur(var(--xiaohei-conversation-blur)) saturate(124%);
931
+ -webkit-backdrop-filter: none;
932
+ backdrop-filter: none;
933
933
  overflow-wrap: anywhere;
934
934
  }
935
935
 
@@ -963,8 +963,8 @@ var XIAOHEI_CONVERSATION_MESSAGES_CSS = `
963
963
  inset 0 1px 0 var(--xiaohei-conversation-highlight),
964
964
  inset -1px 0 0 color-mix(in srgb, var(--xiaohei-conversation-highlight) 42%, transparent),
965
965
  0 7px 22px var(--xiaohei-conversation-shadow) !important;
966
- -webkit-backdrop-filter: blur(var(--xiaohei-conversation-blur)) saturate(124%);
967
- backdrop-filter: blur(var(--xiaohei-conversation-blur)) saturate(124%);
966
+ -webkit-backdrop-filter: none;
967
+ backdrop-filter: none;
968
968
  overflow-wrap: anywhere;
969
969
  }
970
970
 
@@ -996,8 +996,8 @@ var XIAOHEI_CONVERSATION_MESSAGES_CSS = `
996
996
  box-shadow:
997
997
  inset 0 1px 0 color-mix(in srgb, var(--xiaohei-conversation-highlight) 52%, transparent),
998
998
  0 4px 14px color-mix(in srgb, var(--xiaohei-conversation-shadow) 58%, transparent);
999
- -webkit-backdrop-filter: blur(12px) saturate(118%);
1000
- backdrop-filter: blur(12px) saturate(118%);
999
+ -webkit-backdrop-filter: none;
1000
+ backdrop-filter: none;
1001
1001
  overflow: hidden;
1002
1002
  }
1003
1003
 
@@ -1008,8 +1008,8 @@ var XIAOHEI_CONVERSATION_MESSAGES_CSS = `
1008
1008
  border-radius: 9px;
1009
1009
  background: var(--xiaohei-conversation-meta-surface) !important;
1010
1010
  box-shadow: inset 0 1px 0 color-mix(in srgb, var(--xiaohei-conversation-highlight) 48%, transparent);
1011
- -webkit-backdrop-filter: blur(10px) saturate(116%);
1012
- backdrop-filter: blur(10px) saturate(116%);
1011
+ -webkit-backdrop-filter: none;
1012
+ backdrop-filter: none;
1013
1013
  }
1014
1014
 
1015
1015
  #root [data-chat-flow-kind='context'],
@@ -6380,6 +6380,8 @@ function installComposerBloub(doc = typeof document === "undefined" ? void 0 : d
6380
6380
  let hero = null, heroRow = null;
6381
6381
  let bounds;
6382
6382
  let layoutFrame, animationFrame, wake;
6383
+ let frameTimer;
6384
+ let lastPointerStamp = -Infinity;
6383
6385
  let time = 0, lastStamp, activeUntil = 2.5, actionUntil = 0, reactionIndex = 0;
6384
6386
  let pointerSide = 1;
6385
6387
  let special = false, specialIndex = 0;
@@ -6406,8 +6408,10 @@ function installComposerBloub(doc = typeof document === "undefined" ? void 0 : d
6406
6408
  ambientTimer = void 0;
6407
6409
  flight.cancel();
6408
6410
  if (animationFrame !== void 0) win.cancelAnimationFrame(animationFrame);
6411
+ if (frameTimer !== void 0) win.clearTimeout(frameTimer);
6409
6412
  if (wake !== void 0) win.clearTimeout(wake);
6410
6413
  animationFrame = void 0;
6414
+ frameTimer = void 0;
6411
6415
  wake = void 0;
6412
6416
  lastStamp = void 0;
6413
6417
  }
@@ -6442,7 +6446,7 @@ function installComposerBloub(doc = typeof document === "undefined" ? void 0 : d
6442
6446
  }
6443
6447
  function animateFor(seconds) {
6444
6448
  activeUntil = Math.max(activeUntil, time + seconds);
6445
- if (!allowed() || animationFrame !== void 0) return;
6449
+ if (!allowed() || animationFrame !== void 0 || frameTimer !== void 0) return;
6446
6450
  if (wake !== void 0) win.clearTimeout(wake);
6447
6451
  wake = void 0;
6448
6452
  lastStamp = void 0;
@@ -6467,13 +6471,16 @@ function installComposerBloub(doc = typeof document === "undefined" ? void 0 : d
6467
6471
  }
6468
6472
  renderer.render(engine.sample(time), reactions.sample(time));
6469
6473
  if (time < activeUntil || actionUntil || liveliness(time).lid < 0.99) {
6470
- animationFrame = win.requestAnimationFrame(tick);
6474
+ frameTimer = win.setTimeout(() => {
6475
+ frameTimer = void 0;
6476
+ if (allowed()) animationFrame = win.requestAnimationFrame(tick);
6477
+ }, 1e3 / 30);
6471
6478
  } else {
6472
6479
  lastStamp = void 0;
6473
6480
  wake = win.setTimeout(() => {
6474
6481
  wake = void 0;
6475
- animateFor(2);
6476
- }, 3200);
6482
+ animateFor(1);
6483
+ }, 8e3);
6477
6484
  }
6478
6485
  }
6479
6486
  function syncPlayback() {
@@ -6567,6 +6574,8 @@ function installComposerBloub(doc = typeof document === "undefined" ? void 0 : d
6567
6574
  leave();
6568
6575
  return;
6569
6576
  }
6577
+ if (event.timeStamp - lastPointerStamp < 1e3 / 30) return;
6578
+ lastPointerStamp = event.timeStamp;
6570
6579
  const dx = event.clientX - (bounds.left + bounds.width / 2);
6571
6580
  const dy = event.clientY - (bounds.top + bounds.height / 2);
6572
6581
  follow(dx, dy);
@@ -6946,7 +6955,7 @@ var XIAOHEI_THEME_TOKEN_OVERRIDES = Object.fromEntries(
6946
6955
  // src/metallic-brand-mark.tsx
6947
6956
  var import_react = require("react");
6948
6957
  var import_jsx_runtime = require("react/jsx-runtime");
6949
- var sidebarMetallicSnapshot;
6958
+ var metallicSnapshots = /* @__PURE__ */ new Map();
6950
6959
  var VERTEX_SHADER = `#version 300 es
6951
6960
  precision highp float;
6952
6961
  in vec2 a_position;
@@ -7125,14 +7134,14 @@ function XiaoheiMetallicBrandMark({ size, className }) {
7125
7134
  const [ready, setReady] = (0, import_react.useState)(false);
7126
7135
  const visualSize = Math.max(24, size + 4);
7127
7136
  const context = size >= 32 ? "hero" : "sidebar";
7128
- const [snapshot, setSnapshot] = (0, import_react.useState)(context === "sidebar" ? sidebarMetallicSnapshot : void 0);
7137
+ const [snapshot, setSnapshot] = (0, import_react.useState)(metallicSnapshots.get(visualSize));
7129
7138
  (0, import_react.useEffect)(() => {
7130
7139
  if (snapshot !== void 0) return;
7131
7140
  const canvas = canvasRef.current;
7132
7141
  if (canvas === null) return;
7133
7142
  const gl = canvas.getContext("webgl2", {
7134
7143
  alpha: true,
7135
- antialias: true,
7144
+ antialias: false,
7136
7145
  powerPreference: "low-power",
7137
7146
  premultipliedAlpha: true
7138
7147
  });
@@ -7165,7 +7174,7 @@ function XiaoheiMetallicBrandMark({ size, className }) {
7165
7174
  UNIFORM_NAMES.map((name) => [name, gl.getUniformLocation(program, name)])
7166
7175
  );
7167
7176
  const pixelRatio = Math.min(window.devicePixelRatio || 1, 2);
7168
- const renderSize = Math.max(96, Math.ceil(visualSize * pixelRatio * 2));
7177
+ const renderSize = Math.max(64, Math.ceil(visualSize * pixelRatio));
7169
7178
  canvas.width = renderSize;
7170
7179
  canvas.height = renderSize;
7171
7180
  gl.viewport(0, 0, renderSize, renderSize);
@@ -7196,27 +7205,13 @@ function XiaoheiMetallicBrandMark({ size, className }) {
7196
7205
  setRgb(gl, uniforms.u_darkColor, [0.025, 0.04, 0.045]);
7197
7206
  setRgb(gl, uniforms.u_tint, [0.84, 0.92, 0.89]);
7198
7207
  let disposed = false;
7199
- let visible = true;
7200
- let animationFrame;
7201
- let lastFrame = 0;
7202
- let metallicTime = 1450;
7203
- const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
7208
+ const metallicTime = 1450;
7204
7209
  const texture = gl.createTexture();
7205
7210
  const image = new Image();
7206
7211
  const draw = () => {
7207
7212
  gl.uniform1f(uniforms.u_time, metallicTime);
7208
7213
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
7209
7214
  };
7210
- const render = (now) => {
7211
- if (disposed) return;
7212
- if (visible && document.visibilityState !== "hidden" && now - lastFrame >= 40) {
7213
- const delta = lastFrame === 0 ? 0 : Math.min(80, now - lastFrame);
7214
- lastFrame = now;
7215
- metallicTime += delta * 0.11;
7216
- draw();
7217
- }
7218
- animationFrame = window.requestAnimationFrame(render);
7219
- };
7220
7215
  image.onload = () => {
7221
7216
  if (disposed || texture === null) return;
7222
7217
  gl.activeTexture(gl.TEXTURE0);
@@ -7229,24 +7224,15 @@ function XiaoheiMetallicBrandMark({ size, className }) {
7229
7224
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
7230
7225
  gl.uniform1i(uniforms.u_tex, 0);
7231
7226
  draw();
7232
- if (context === "sidebar") {
7233
- sidebarMetallicSnapshot = canvas.toDataURL();
7234
- setSnapshot(sidebarMetallicSnapshot);
7235
- return;
7236
- }
7227
+ const result = canvas.toDataURL();
7228
+ metallicSnapshots.set(visualSize, result);
7229
+ setSnapshot(result);
7237
7230
  setReady(true);
7238
- if (!reduceMotion.matches) animationFrame = window.requestAnimationFrame(render);
7239
7231
  };
7240
7232
  image.src = XIAOHEI_BRAND_AVATAR;
7241
- const visibilityObserver = typeof IntersectionObserver === "function" ? new IntersectionObserver((entries) => {
7242
- visible = entries.some((entry) => entry.isIntersecting);
7243
- if (visible) lastFrame = 0;
7244
- }) : void 0;
7245
- visibilityObserver?.observe(canvas);
7246
7233
  return () => {
7247
7234
  disposed = true;
7248
- if (animationFrame !== void 0) window.cancelAnimationFrame(animationFrame);
7249
- visibilityObserver?.disconnect();
7235
+ image.onload = null;
7250
7236
  if (texture !== null) gl.deleteTexture(texture);
7251
7237
  gl.deleteBuffer(buffer);
7252
7238
  gl.deleteProgram(program);
package/lib/gaze.js CHANGED
@@ -43,14 +43,14 @@ export function installXiaoheiGaze(doc = typeof document === 'undefined' ? undef
43
43
  gaze.style.setProperty('--xiaohei-gaze-y', `${currentY.toFixed(3)}px`);
44
44
  };
45
45
  const wake = () => {
46
- if (disposed || frameId !== 0 || gaze === undefined)
46
+ if (disposed || doc.hidden || frameId !== 0 || gaze === undefined)
47
47
  return;
48
48
  setMoving(true);
49
49
  frameId = win.requestAnimationFrame(tick);
50
50
  };
51
51
  const tick = (time) => {
52
52
  frameId = 0;
53
- if (disposed || gaze === undefined || mascot === undefined)
53
+ if (disposed || doc.hidden || gaze === undefined || mascot === undefined)
54
54
  return;
55
55
  const target = resolveGazeTarget(doc, mascot, pointer, portalActive, behaviorDisabled());
56
56
  const elapsed = lastFrameTime === 0 ? 16 : Math.min(48, Math.max(1, time - lastFrameTime));
@@ -107,6 +107,17 @@ export function installXiaoheiGaze(doc = typeof document === 'undefined' ? undef
107
107
  };
108
108
  const onPreferenceChange = () => wake();
109
109
  const onStateChange = () => wake();
110
+ const onVisibilityChange = () => {
111
+ if (doc.hidden) {
112
+ if (frameId !== 0)
113
+ win.cancelAnimationFrame(frameId);
114
+ frameId = 0;
115
+ lastFrameTime = 0;
116
+ setMoving(false);
117
+ }
118
+ else
119
+ wake();
120
+ };
110
121
  const unsubscribeHostDom = subscribeXiaoheiHostDom(doc, attach);
111
122
  const stateObserver = new win.MutationObserver(onStateChange);
112
123
  stateObserver.observe(doc.documentElement, {
@@ -114,6 +125,7 @@ export function installXiaoheiGaze(doc = typeof document === 'undefined' ? undef
114
125
  attributeFilter: ['data-xiaohei-state'],
115
126
  });
116
127
  doc.addEventListener('pointermove', onPointerMove, { passive: true });
128
+ doc.addEventListener('visibilitychange', onVisibilityChange);
117
129
  doc.addEventListener('pointerleave', clearPointer);
118
130
  doc.addEventListener(XIAOHEI_PORTAL_ACTIVITY_EVENT, onPortalActivity);
119
131
  win.addEventListener('blur', clearPointer);
@@ -127,6 +139,7 @@ export function installXiaoheiGaze(doc = typeof document === 'undefined' ? undef
127
139
  unsubscribeHostDom();
128
140
  stateObserver.disconnect();
129
141
  doc.removeEventListener('pointermove', onPointerMove);
142
+ doc.removeEventListener('visibilitychange', onVisibilityChange);
130
143
  doc.removeEventListener('pointerleave', clearPointer);
131
144
  doc.removeEventListener(XIAOHEI_PORTAL_ACTIVITY_EVENT, onPortalActivity);
132
145
  win.removeEventListener('blur', clearPointer);
@@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from 'react';
3
3
  import { XIAOHEI_BRAND_AVATAR } from './generated-identity.js';
4
4
  // Native wide/rail switches remount this slot. A tiny navigation mark does not
5
5
  // need a fresh GPU context and shader compilation on every toggle.
6
- let sidebarMetallicSnapshot;
6
+ const metallicSnapshots = new Map();
7
7
  const VERTEX_SHADER = `#version 300 es
8
8
  precision highp float;
9
9
  in vec2 a_position;
@@ -170,7 +170,7 @@ export function XiaoheiMetallicBrandMark({ size, className }) {
170
170
  const [ready, setReady] = useState(false);
171
171
  const visualSize = Math.max(24, size + 4);
172
172
  const context = size >= 32 ? 'hero' : 'sidebar';
173
- const [snapshot, setSnapshot] = useState(context === 'sidebar' ? sidebarMetallicSnapshot : undefined);
173
+ const [snapshot, setSnapshot] = useState(metallicSnapshots.get(visualSize));
174
174
  useEffect(() => {
175
175
  if (snapshot !== undefined)
176
176
  return;
@@ -179,7 +179,7 @@ export function XiaoheiMetallicBrandMark({ size, className }) {
179
179
  return;
180
180
  const gl = canvas.getContext('webgl2', {
181
181
  alpha: true,
182
- antialias: true,
182
+ antialias: false,
183
183
  powerPreference: 'low-power',
184
184
  premultipliedAlpha: true,
185
185
  });
@@ -213,7 +213,7 @@ export function XiaoheiMetallicBrandMark({ size, className }) {
213
213
  }
214
214
  const uniforms = Object.fromEntries(UNIFORM_NAMES.map(name => [name, gl.getUniformLocation(program, name)]));
215
215
  const pixelRatio = Math.min(window.devicePixelRatio || 1, 2);
216
- const renderSize = Math.max(96, Math.ceil(visualSize * pixelRatio * 2));
216
+ const renderSize = Math.max(64, Math.ceil(visualSize * pixelRatio));
217
217
  canvas.width = renderSize;
218
218
  canvas.height = renderSize;
219
219
  gl.viewport(0, 0, renderSize, renderSize);
@@ -244,28 +244,13 @@ export function XiaoheiMetallicBrandMark({ size, className }) {
244
244
  setRgb(gl, uniforms.u_darkColor, [0.025, 0.04, 0.045]);
245
245
  setRgb(gl, uniforms.u_tint, [0.84, 0.92, 0.89]);
246
246
  let disposed = false;
247
- let visible = true;
248
- let animationFrame;
249
- let lastFrame = 0;
250
- let metallicTime = 1450;
251
- const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
247
+ const metallicTime = 1450;
252
248
  const texture = gl.createTexture();
253
249
  const image = new Image();
254
250
  const draw = () => {
255
251
  gl.uniform1f(uniforms.u_time, metallicTime);
256
252
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
257
253
  };
258
- const render = (now) => {
259
- if (disposed)
260
- return;
261
- if (visible && document.visibilityState !== 'hidden' && now - lastFrame >= 40) {
262
- const delta = lastFrame === 0 ? 0 : Math.min(80, now - lastFrame);
263
- lastFrame = now;
264
- metallicTime += delta * 0.11;
265
- draw();
266
- }
267
- animationFrame = window.requestAnimationFrame(render);
268
- };
269
254
  image.onload = () => {
270
255
  if (disposed || texture === null)
271
256
  return;
@@ -279,29 +264,17 @@ export function XiaoheiMetallicBrandMark({ size, className }) {
279
264
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
280
265
  gl.uniform1i(uniforms.u_tex, 0);
281
266
  draw();
282
- if (context === 'sidebar') {
283
- sidebarMetallicSnapshot = canvas.toDataURL();
284
- setSnapshot(sidebarMetallicSnapshot);
285
- return;
286
- }
267
+ // Preserve the metallic appearance without a permanent GPU animation.
268
+ // Updating snapshot unmounts the canvas and runs the resource cleanup.
269
+ const result = canvas.toDataURL();
270
+ metallicSnapshots.set(visualSize, result);
271
+ setSnapshot(result);
287
272
  setReady(true);
288
- if (!reduceMotion.matches)
289
- animationFrame = window.requestAnimationFrame(render);
290
273
  };
291
274
  image.src = XIAOHEI_BRAND_AVATAR;
292
- const visibilityObserver = typeof IntersectionObserver === 'function'
293
- ? new IntersectionObserver(entries => {
294
- visible = entries.some(entry => entry.isIntersecting);
295
- if (visible)
296
- lastFrame = 0;
297
- })
298
- : undefined;
299
- visibilityObserver?.observe(canvas);
300
275
  return () => {
301
276
  disposed = true;
302
- if (animationFrame !== undefined)
303
- window.cancelAnimationFrame(animationFrame);
304
- visibilityObserver?.disconnect();
277
+ image.onload = null;
305
278
  if (texture !== null)
306
279
  gl.deleteTexture(texture);
307
280
  gl.deleteBuffer(buffer);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lemoncat7/dsh-theme-xiaohei",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "A Luo Xiaohei-inspired animated dark theme for DeepSeek Harness",
5
5
  "keywords": [
6
6
  "deepseek-harness",