@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.
- package/lib/bloub-heixiu/runtime.js +17 -3
- package/lib/chrome/conversation-messages.js +8 -8
- package/lib/client.js +30 -44
- package/lib/gaze.js +15 -2
- package/lib/metallic-brand-mark.js +11 -38
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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(
|
|
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:
|
|
82
|
-
backdrop-filter:
|
|
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:
|
|
117
|
-
backdrop-filter:
|
|
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:
|
|
150
|
-
backdrop-filter:
|
|
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:
|
|
162
|
-
backdrop-filter:
|
|
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:
|
|
932
|
-
backdrop-filter:
|
|
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:
|
|
967
|
-
backdrop-filter:
|
|
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:
|
|
1000
|
-
backdrop-filter:
|
|
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:
|
|
1012
|
-
backdrop-filter:
|
|
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
|
-
|
|
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(
|
|
6476
|
-
},
|
|
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
|
|
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)(
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
7233
|
-
|
|
7234
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
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);
|