@lemoncat7/dsh-theme-xiaohei 0.3.1 → 0.3.2

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.
@@ -1,6 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from 'react';
3
3
  import { XIAOHEI_BRAND_AVATAR } from './generated-identity.js';
4
+ // Native wide/rail switches remount this slot. A tiny navigation mark does not
5
+ // need a fresh GPU context and shader compilation on every toggle.
6
+ let sidebarMetallicSnapshot;
4
7
  const VERTEX_SHADER = `#version 300 es
5
8
  precision highp float;
6
9
  in vec2 a_position;
@@ -167,7 +170,10 @@ export function XiaoheiMetallicBrandMark({ size, className }) {
167
170
  const [ready, setReady] = useState(false);
168
171
  const visualSize = Math.max(24, size + 4);
169
172
  const context = size >= 32 ? 'hero' : 'sidebar';
173
+ const [snapshot, setSnapshot] = useState(context === 'sidebar' ? sidebarMetallicSnapshot : undefined);
170
174
  useEffect(() => {
175
+ if (snapshot !== undefined)
176
+ return;
171
177
  const canvas = canvasRef.current;
172
178
  if (canvas === null)
173
179
  return;
@@ -273,6 +279,11 @@ export function XiaoheiMetallicBrandMark({ size, className }) {
273
279
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
274
280
  gl.uniform1i(uniforms.u_tex, 0);
275
281
  draw();
282
+ if (context === 'sidebar') {
283
+ sidebarMetallicSnapshot = canvas.toDataURL();
284
+ setSnapshot(sidebarMetallicSnapshot);
285
+ return;
286
+ }
276
287
  setReady(true);
277
288
  if (!reduceMotion.matches)
278
289
  animationFrame = window.requestAnimationFrame(render);
@@ -295,12 +306,15 @@ export function XiaoheiMetallicBrandMark({ size, className }) {
295
306
  gl.deleteTexture(texture);
296
307
  gl.deleteBuffer(buffer);
297
308
  gl.deleteProgram(program);
309
+ gl.getExtension('WEBGL_lose_context')?.loseContext();
298
310
  };
299
- }, [visualSize]);
311
+ }, [visualSize, context, snapshot]);
300
312
  const geometry = {
301
313
  '--xiaohei-brand-mark-size': `${visualSize}px`,
302
314
  };
303
- return (_jsxs("span", { className: className === undefined ? 'xiaohei-brand-mark' : `${className} xiaohei-brand-mark`, "data-brand-context": context, "data-metallic-ready": ready ? 'true' : 'false', style: geometry, "aria-hidden": "true", children: [_jsx("img", { className: "xiaohei-brand-mark__fallback", src: XIAOHEI_BRAND_AVATAR, alt: "" }), _jsx("canvas", { className: "xiaohei-brand-mark__metal", ref: canvasRef })] }));
315
+ return (_jsxs("span", { className: className === undefined ? 'xiaohei-brand-mark' : `${className} xiaohei-brand-mark`, "data-brand-context": context, "data-metallic-ready": ready || snapshot !== undefined ? 'true' : 'false', style: geometry, "aria-hidden": "true", children: [_jsx("img", { className: "xiaohei-brand-mark__fallback", src: XIAOHEI_BRAND_AVATAR, alt: "" }), snapshot !== undefined
316
+ ? _jsx("img", { className: "xiaohei-brand-mark__metal", src: snapshot, alt: "" })
317
+ : _jsx("canvas", { className: "xiaohei-brand-mark__metal", ref: canvasRef })] }));
304
318
  }
305
319
  /** Official sidebar brand-name occupant paired with the Xiaohei mark. */
306
320
  export function XiaoheiBrandName() {
@@ -0,0 +1,18 @@
1
+ interface Clock {
2
+ setTimeout(callback: () => void, delay: number): number;
3
+ clearTimeout(handle: number): void;
4
+ performance: {
5
+ now(): number;
6
+ };
7
+ }
8
+ export interface CharacterMotionClip {
9
+ duration: number;
10
+ }
11
+ /** One deadline; elapsed-time continuous sampling, capped at 30 fps. */
12
+ export declare function createCharacterMotionClock(clock: Clock, changed: (action: string | undefined, progress: number) => void, random?: () => number): {
13
+ setClips(next: Readonly<Record<string, CharacterMotionClip>> | undefined): void;
14
+ attention(): void;
15
+ dispose(): void;
16
+ };
17
+ export {};
18
+ //# sourceMappingURL=character-motion-clock.d.ts.map
@@ -0,0 +1,57 @@
1
+ /** One deadline; elapsed-time continuous sampling, capped at 30 fps. */
2
+ export function createCharacterMotionClock(clock, changed, random = Math.random) {
3
+ let timer, disposed = false, turn = 0, active = '';
4
+ let clips;
5
+ const clear = () => { if (timer !== undefined)
6
+ clock.clearTimeout(timer); timer = undefined; };
7
+ function rest() {
8
+ active = '';
9
+ changed(undefined, 0);
10
+ if (clips && !disposed)
11
+ timer = clock.setTimeout(start, 2800 + random() * 2800);
12
+ }
13
+ function play(action) {
14
+ const clip = clips?.[action];
15
+ if (!clip || disposed)
16
+ return;
17
+ clear();
18
+ active = action;
19
+ const started = clock.performance.now();
20
+ const step = () => {
21
+ timer = undefined;
22
+ if (!clips || disposed)
23
+ return;
24
+ const t = Math.min(1, (clock.performance.now() - started) / clip.duration);
25
+ changed(active, t);
26
+ if (t >= 1) {
27
+ rest();
28
+ return;
29
+ }
30
+ timer = clock.setTimeout(step, 1000 / 30);
31
+ };
32
+ step();
33
+ }
34
+ function start() {
35
+ timer = undefined;
36
+ if (!clips || disposed)
37
+ return;
38
+ const gestures = Object.keys(clips).filter(name => name !== 'blink' && name !== 'attention');
39
+ play((turn++ % 2 === 0 && clips.blink) || !gestures.length ? 'blink' : gestures[Math.floor(random() * gestures.length)]);
40
+ }
41
+ return {
42
+ setClips(next) {
43
+ if (disposed || next === clips)
44
+ return;
45
+ clear();
46
+ clips = next;
47
+ active = '';
48
+ changed(undefined, 0);
49
+ if (clips)
50
+ timer = clock.setTimeout(start, 1200 + random() * 800);
51
+ },
52
+ attention() { if (!active)
53
+ play('attention'); },
54
+ dispose() { disposed = true; clear(); clips = undefined; changed(undefined, 0); },
55
+ };
56
+ }
57
+ //# sourceMappingURL=character-motion-clock.js.map
@@ -0,0 +1,12 @@
1
+ export type Point = readonly [number, number];
2
+ export interface CharacterLayer {
3
+ readonly src: string;
4
+ readonly box: readonly [number, number, number, number];
5
+ }
6
+ export interface CharacterRig {
7
+ readonly layers: Readonly<Record<string, CharacterLayer>>;
8
+ readonly ears: readonly Point[];
9
+ readonly size: Point;
10
+ }
11
+ export type CharacterMotionCatalog = Readonly<Record<string, Readonly<Record<'light' | 'dark', CharacterRig>>>>;
12
+ //# sourceMappingURL=character-motion-types.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=character-motion-types.js.map
@@ -0,0 +1,6 @@
1
+ /** Owns loading and lifecycle only. Rig math and painting stay independent. */
2
+ export declare function createCharacterMotion(doc: Document): {
3
+ setPose(nextPart: HTMLElement | undefined, nextPose: string): void;
4
+ dispose(): void;
5
+ };
6
+ //# sourceMappingURL=character-motion.d.ts.map
@@ -0,0 +1,126 @@
1
+ import { CHARACTER_MOTION } from '../generated-character-motion.js';
2
+ import { createCharacterMotionClock } from './character-motion-clock.js';
3
+ import { createRigRenderer } from './character-rig-renderer.js';
4
+ /** Owns loading and lifecycle only. Rig math and painting stay independent. */
5
+ export function createCharacterMotion(doc) {
6
+ const win = doc.defaultView, reduced = win.matchMedia('(prefers-reduced-motion: reduce)');
7
+ const images = new Map(), loading = new Map();
8
+ let disposed = false, part, rig;
9
+ let canvas, render;
10
+ let pose = '', appearance = '', revision = 0, nearby = false, lastPointer = 0;
11
+ function decode(src) {
12
+ if (loading.has(src))
13
+ return loading.get(src);
14
+ if (images.has(src))
15
+ return Promise.resolve();
16
+ const image = new win.Image();
17
+ image.src = src;
18
+ const pending = image.decode().then(() => { if (!disposed)
19
+ images.set(src, image); }, () => { })
20
+ .finally(() => loading.delete(src));
21
+ loading.set(src, pending);
22
+ return pending;
23
+ }
24
+ const clock = createCharacterMotionClock(win, (action, progress) => {
25
+ if (!canvas || !part || !render)
26
+ return;
27
+ // Keep the same layered rest drawing after an action: no plate swap/pop.
28
+ render(action, progress);
29
+ part.dataset.motion = action ?? 'rig-rest';
30
+ });
31
+ function update() {
32
+ if (disposed)
33
+ return;
34
+ const mode = doc.documentElement.dataset.xiaoheiAppearance === 'light' ? 'light' : 'dark';
35
+ const moving = doc.documentElement.hasAttribute('data-xiaohei-sidebar-resizing');
36
+ const next = !doc.hidden && !reduced.matches && !moving && part ? CHARACTER_MOTION[pose]?.[mode] : undefined;
37
+ if (next === rig && appearance === mode)
38
+ return;
39
+ clock.setClips(undefined);
40
+ render = undefined;
41
+ const current = ++revision;
42
+ canvas?.remove();
43
+ canvas = undefined;
44
+ if (part)
45
+ part.dataset.motion = 'rest';
46
+ rig = next;
47
+ appearance = mode;
48
+ nearby = false;
49
+ if (!next || !part)
50
+ return;
51
+ const currentPart = part;
52
+ void Promise.all(Object.values(next.layers).map(layer => decode(layer.src))).then(() => {
53
+ if (disposed || current !== revision || Object.values(next.layers).some(layer => !images.get(layer.src)))
54
+ return;
55
+ canvas = doc.createElement('canvas');
56
+ canvas.className = 'xiaohei-character-motion';
57
+ canvas.width = next.size[0];
58
+ canvas.height = next.size[1];
59
+ const ctx = canvas.getContext('2d');
60
+ if (!ctx) {
61
+ canvas = undefined;
62
+ return;
63
+ }
64
+ currentPart.append(canvas);
65
+ render = createRigRenderer(ctx, next, images);
66
+ clock.setClips({
67
+ blink: { duration: 330 },
68
+ ...Object.fromEntries(next.ears.map((_, i) => ['ear' + i, { duration: 1050 }])),
69
+ ...(next.layers.tail ? { tail: { duration: 2400 } } : {}),
70
+ attention: { duration: 2400 },
71
+ });
72
+ });
73
+ }
74
+ const pointer = (event) => {
75
+ if (!part || !render || event.pointerType !== 'mouse')
76
+ return;
77
+ const now = win.performance.now();
78
+ if (now - lastPointer < 100)
79
+ return;
80
+ lastPointer = now;
81
+ const rect = part.getBoundingClientRect();
82
+ const near = event.clientX >= rect.left - 70 && event.clientX <= rect.right + 70
83
+ && event.clientY >= rect.top - 50 && event.clientY <= rect.bottom + 50;
84
+ if (near && !nearby)
85
+ clock.attention();
86
+ nearby = near;
87
+ };
88
+ reduced.addEventListener('change', update);
89
+ doc.addEventListener('visibilitychange', update);
90
+ doc.addEventListener('pointermove', pointer, { passive: true });
91
+ const observer = new win.MutationObserver(update);
92
+ observer.observe(doc.documentElement, { attributes: true, attributeFilter: ['data-xiaohei-appearance', 'data-xiaohei-sidebar-resizing'] });
93
+ return {
94
+ setPose(nextPart, nextPose) {
95
+ if (nextPart !== part || pose !== nextPose) {
96
+ revision++;
97
+ clock.setClips(undefined);
98
+ render = undefined;
99
+ canvas?.remove();
100
+ canvas = undefined;
101
+ rig = undefined;
102
+ if (part)
103
+ part.dataset.motion = 'rest';
104
+ }
105
+ part = nextPart;
106
+ pose = nextPose;
107
+ update();
108
+ },
109
+ dispose() {
110
+ disposed = true;
111
+ revision++;
112
+ clock.dispose();
113
+ canvas?.remove();
114
+ render = undefined;
115
+ images.clear();
116
+ loading.clear();
117
+ if (part)
118
+ part.dataset.motion = 'rest';
119
+ reduced.removeEventListener('change', update);
120
+ observer.disconnect();
121
+ doc.removeEventListener('visibilitychange', update);
122
+ doc.removeEventListener('pointermove', pointer);
123
+ },
124
+ };
125
+ }
126
+ //# sourceMappingURL=character-motion.js.map
@@ -0,0 +1,4 @@
1
+ import type { CharacterRig } from './character-motion-types.js';
2
+ /** Small CPU-skinned tail only; body and ears use normal image draws. */
3
+ export declare function createRigRenderer(ctx: CanvasRenderingContext2D, rig: CharacterRig, images: ReadonlyMap<string, HTMLImageElement | null>): (action: string | undefined, progress: number) => void;
4
+ //# sourceMappingURL=character-rig-renderer.d.ts.map
@@ -0,0 +1,85 @@
1
+ import { bindVertex, rigAngles, skinVertex, solveBones, TAIL_JOINTS } from './character-rig.js';
2
+ /** Small CPU-skinned tail only; body and ears use normal image draws. */
3
+ export function createRigRenderer(ctx, rig, images) {
4
+ const tail = rig.layers.tail;
5
+ const vertices = [], triangles = [];
6
+ if (tail) {
7
+ const [x, y, r, b] = tail.box, cols = Math.ceil((r - x) / 10), rows = Math.ceil((b - y) / 10);
8
+ for (let row = 0; row <= rows; row++)
9
+ for (let col = 0; col <= cols; col++)
10
+ vertices.push(bindVertex(x + (r - x) * col / cols, y + (b - y) * row / rows));
11
+ for (let row = 0; row < rows; row++)
12
+ for (let col = 0; col < cols; col++) {
13
+ const i = row * (cols + 1) + col;
14
+ triangles.push([i, i + 1, i + cols + 1], [i + 1, i + cols + 2, i + cols + 1]);
15
+ }
16
+ }
17
+ function layer(name) {
18
+ const layer = rig.layers[name], image = layer && images.get(layer.src);
19
+ if (layer && image) {
20
+ const [x, y, r, b] = layer.box;
21
+ ctx.drawImage(image, x, y, r - x, b - y);
22
+ }
23
+ }
24
+ function drawTail(progress) {
25
+ if (!tail)
26
+ return;
27
+ const image = images.get(tail.src);
28
+ if (!image)
29
+ return;
30
+ if (progress <= 0 || progress >= 1) {
31
+ layer('tail');
32
+ return;
33
+ }
34
+ const joints = solveBones(TAIL_JOINTS, rigAngles(progress, TAIL_JOINTS.length, true));
35
+ const posed = vertices.map(v => skinVertex(v, joints));
36
+ const [left, top] = tail.box;
37
+ for (const indices of triangles) {
38
+ const [ia, ib, ic] = indices;
39
+ const a = vertices[ia], b = vertices[ib], c = vertices[ic];
40
+ const p = posed[ia], q = posed[ib], r = posed[ic];
41
+ const ux = b.x - a.x, uy = b.y - a.y, vx = c.x - a.x, vy = c.y - a.y, det = ux * vy - uy * vx;
42
+ const aa = ((q[0] - p[0]) * vy - (r[0] - p[0]) * uy) / det;
43
+ const bb = ((q[1] - p[1]) * vy - (r[1] - p[1]) * uy) / det;
44
+ const cc = ((r[0] - p[0]) * ux - (q[0] - p[0]) * vx) / det;
45
+ const dd = ((r[1] - p[1]) * ux - (q[1] - p[1]) * vx) / det;
46
+ ctx.save();
47
+ ctx.beginPath();
48
+ // Slightly overlap the interior triangles to avoid antialias hairlines.
49
+ const cx = (p[0] + q[0] + r[0]) / 3, cy = (p[1] + q[1] + r[1]) / 3;
50
+ [p, q, r].forEach((v, i) => {
51
+ const dx = v[0] - cx, dy = v[1] - cy, scale = 1 + 1.2 / Math.max(1, Math.hypot(dx, dy));
52
+ if (i)
53
+ ctx.lineTo(cx + dx * scale, cy + dy * scale);
54
+ else
55
+ ctx.moveTo(cx + dx * scale, cy + dy * scale);
56
+ });
57
+ ctx.closePath();
58
+ ctx.clip();
59
+ ctx.transform(aa, bb, cc, dd, p[0] - aa * (a.x - left) - cc * (a.y - top), p[1] - bb * (a.x - left) - dd * (a.y - top));
60
+ ctx.drawImage(image, 0, 0);
61
+ ctx.restore();
62
+ }
63
+ }
64
+ return (action, progress) => {
65
+ ctx.clearRect(0, 0, rig.size[0], rig.size[1]);
66
+ const ears = rigAngles(progress, rig.ears.length, false);
67
+ rig.ears.forEach(([x, y], i) => {
68
+ ctx.save();
69
+ ctx.translate(x, y);
70
+ ctx.rotate(action === 'attention' || action === 'ear' + i ? ears[i] : 0);
71
+ ctx.translate(-x, -y);
72
+ layer('ear' + i);
73
+ ctx.restore();
74
+ });
75
+ layer('body');
76
+ drawTail(action === 'tail' || action === 'attention' ? progress : 0);
77
+ if (action === 'blink') {
78
+ ctx.save();
79
+ ctx.globalAlpha = Math.min(1, Math.sin(Math.PI * progress) * 2);
80
+ layer('blink');
81
+ ctx.restore();
82
+ }
83
+ };
84
+ }
85
+ //# sourceMappingURL=character-rig-renderer.js.map
@@ -0,0 +1,20 @@
1
+ import type { Point } from './character-motion-types.js';
2
+ /** Bind-space joint positions. Each child inherits its parent's rotation. */
3
+ export declare const TAIL_JOINTS: readonly Point[];
4
+ export interface Joint {
5
+ x: number;
6
+ y: number;
7
+ angle: number;
8
+ }
9
+ export interface Vertex {
10
+ x: number;
11
+ y: number;
12
+ bone: number;
13
+ weight: number;
14
+ }
15
+ export declare function solveBones(rest: readonly Point[], angles: readonly number[]): Joint[];
16
+ export declare function bindVertex(x: number, y: number, rest?: readonly Point[]): Vertex;
17
+ /** Linear blend skinning, inverse bind translation followed by posed rotation. */
18
+ export declare function skinVertex(v: Vertex, joints: readonly Joint[], rest?: readonly Point[]): Point;
19
+ export declare function rigAngles(progress: number, count: number, tail: boolean): number[];
20
+ //# sourceMappingURL=character-rig.d.ts.map
@@ -0,0 +1,53 @@
1
+ /** Bind-space joint positions. Each child inherits its parent's rotation. */
2
+ export const TAIL_JOINTS = [
3
+ [284, 328], [310, 347], [324, 373], [319, 398],
4
+ [307, 423], [307, 448], [319, 464],
5
+ ];
6
+ export function solveBones(rest, angles) {
7
+ return rest.reduce((out, p, i) => {
8
+ const parent = out[i - 1];
9
+ const angle = (parent?.angle ?? 0) + (angles[i] ?? 0);
10
+ if (!parent)
11
+ out.push({ x: p[0], y: p[1], angle });
12
+ else {
13
+ const prev = rest[i - 1], dx = p[0] - prev[0], dy = p[1] - prev[1];
14
+ const c = Math.cos(parent.angle), s = Math.sin(parent.angle);
15
+ out.push({ x: parent.x + dx * c - dy * s, y: parent.y + dx * s + dy * c, angle });
16
+ }
17
+ return out;
18
+ }, []);
19
+ }
20
+ export function bindVertex(x, y, rest = TAIL_JOINTS) {
21
+ let distance = Infinity, bone = 0, weight = 0;
22
+ for (let i = 0; i < rest.length - 1; i++) {
23
+ const a = rest[i], b = rest[i + 1], dx = b[0] - a[0], dy = b[1] - a[1];
24
+ const t = Math.max(0, Math.min(1, ((x - a[0]) * dx + (y - a[1]) * dy) / (dx * dx + dy * dy)));
25
+ const d = (x - a[0] - dx * t) ** 2 + (y - a[1] - dy * t) ** 2;
26
+ if (d < distance) {
27
+ distance = d;
28
+ bone = i;
29
+ weight = t;
30
+ }
31
+ }
32
+ return { x, y, bone, weight };
33
+ }
34
+ /** Linear blend skinning, inverse bind translation followed by posed rotation. */
35
+ export function skinVertex(v, joints, rest = TAIL_JOINTS) {
36
+ let x = 0, y = 0;
37
+ for (let k = 0; k < 2; k++) {
38
+ const i = v.bone + k, p = rest[i], j = joints[i], w = k ? v.weight : 1 - v.weight;
39
+ const dx = v.x - p[0], dy = v.y - p[1], c = Math.cos(j.angle), s = Math.sin(j.angle);
40
+ x += w * (j.x + dx * c - dy * s);
41
+ y += w * (j.y + dx * s + dy * c);
42
+ }
43
+ return [x, y];
44
+ }
45
+ export function rigAngles(progress, count, tail) {
46
+ const t = Math.max(0, Math.min(1, progress));
47
+ const envelope = Math.sin(Math.PI * t) ** 2;
48
+ // Delayed distal joints create follow-through, with zero displacement/velocity
49
+ // at both ends; no discrete frame lookup and no idle simulation.
50
+ return Array.from({ length: count }, (_, i) => envelope * (tail ? (i ? .12 : 0) : .22)
51
+ * Math.sin(t * Math.PI * (tail ? 3 : 4) - i * (tail ? .65 : 1.1)));
52
+ }
53
+ //# sourceMappingURL=character-rig.js.map
@@ -8,6 +8,9 @@ export const XIAOHEI_WALLPAPER_CHARACTER_CSS = `
8
8
  pointer-events: none;
9
9
  user-select: none;
10
10
  }
11
+ html[data-xiaohei-sidebar-resizing] .xiaohei-wallpaper-character {
12
+ visibility: hidden;
13
+ }
11
14
  html[data-xiaohei-appearance='light'] .xiaohei-wallpaper-character {
12
15
  ${CHARACTER_POSE_NAMES.map(pose => `--xiaohei-${pose}-art: url("${CHARACTER_POSES[pose].light}");`).join('\n ')}
13
16
  }
@@ -21,6 +24,13 @@ html[data-xiaohei-appearance='light'] .xiaohei-wallpaper-character {
21
24
  opacity: 0;
22
25
  transition: opacity 220ms ease, transform 220ms cubic-bezier(.2,.8,.2,1);
23
26
  }
27
+ .xiaohei-wallpaper-character .xiaohei-character-motion {
28
+ position: absolute; inset: 0; width: 100%; height: 100%;
29
+ object-fit: contain; object-position: left center; pointer-events: none;
30
+ }
31
+ .xiaohei-wallpaper-character > span[data-motion]:not([data-motion='rest']) {
32
+ background-image: none;
33
+ }
24
34
  ${CHARACTER_POSE_NAMES.map(pose => `
25
35
  .xiaohei-wallpaper-character__${pose}[data-ready] {
26
36
  background-image: var(--xiaohei-${pose}-art);
@@ -1,4 +1,4 @@
1
1
  export declare const XIAOHEI_WALLPAPER_CHARACTER_ID = "dsh-theme-xiaohei/wallpaper-character";
2
- /** Complete raster poses, no skeleton deformation or perpetual animation loop. */
2
+ /** Places the layered character only when the host geometry is settled. */
3
3
  export declare function installXiaoheiWallpaperCharacter(doc?: Document | undefined): () => void;
4
4
  //# sourceMappingURL=wallpaper-character.d.ts.map
@@ -4,8 +4,9 @@ import { XIAOHEI_SCENE_LAYER_ID } from './styles.js';
4
4
  import { resolveCharacterPlacement } from './character-layout.js';
5
5
  import { createCharacterIdleController } from './character-idle.js';
6
6
  import { CHARACTER_POSES, CHARACTER_POSE_NAMES } from './character-poses.js';
7
+ import { createCharacterMotion } from './character-motion.js';
7
8
  export const XIAOHEI_WALLPAPER_CHARACTER_ID = 'dsh-theme-xiaohei/wallpaper-character';
8
- /** Complete raster poses, no skeleton deformation or perpetual animation loop. */
9
+ /** Places the layered character only when the host geometry is settled. */
9
10
  export function installXiaoheiWallpaperCharacter(doc = typeof document === 'undefined' ? undefined : document) {
10
11
  const win = doc?.defaultView;
11
12
  if (!doc || !win)
@@ -20,6 +21,7 @@ export function installXiaoheiWallpaperCharacter(doc = typeof document === 'unde
20
21
  const ready = new Set();
21
22
  const loading = new Set();
22
23
  const failed = new Set();
24
+ const motion = typeof win.matchMedia === 'function' ? createCharacterMotion(doc) : undefined;
23
25
  const idle = createCharacterIdleController({ now: () => win.performance.now(),
24
26
  setTimeout: (fn, delay) => win.setTimeout(fn, delay), clearTimeout: handle => win.clearTimeout(handle),
25
27
  }, () => schedule());
@@ -43,6 +45,17 @@ export function installXiaoheiWallpaperCharacter(doc = typeof document === 'unde
43
45
  function reconcile() {
44
46
  if (disposed || !doc)
45
47
  return;
48
+ // The glass owner already tracks the native column's animated geometry.
49
+ // Do not measure controls or rebuild a different rig at intermediate widths.
50
+ if (doc.documentElement?.hasAttribute('data-xiaohei-sidebar-resizing')) {
51
+ dirty = true;
52
+ if (host && host.dataset.pose !== 'hidden')
53
+ host.dataset.pose = 'hidden';
54
+ idle.setPaused(true);
55
+ motion?.setPose(undefined, 'hidden');
56
+ scheduleSettled();
57
+ return;
58
+ }
46
59
  const layer = doc.getElementById(XIAOHEI_SCENE_LAYER_ID);
47
60
  const next = [
48
61
  doc.querySelector(XIAOHEI_HOST_SELECTORS.sidebarShell),
@@ -62,6 +75,7 @@ export function installXiaoheiWallpaperCharacter(doc = typeof document === 'unde
62
75
  host?.remove();
63
76
  host = undefined;
64
77
  idle.setPaused(true);
78
+ motion?.setPose(undefined, 'hidden');
65
79
  return;
66
80
  }
67
81
  if (!host || host.parentElement !== layer) {
@@ -115,9 +129,11 @@ export function installXiaoheiWallpaperCharacter(doc = typeof document === 'unde
115
129
  if (pose === 'hidden') {
116
130
  if (host.dataset.pose !== 'hidden')
117
131
  host.dataset.pose = 'hidden';
132
+ motion?.setPose(undefined, 'hidden');
118
133
  return;
119
134
  }
120
135
  if (!ready.has(pose)) {
136
+ motion?.setPose(undefined, 'hidden');
121
137
  // Do not retain the old location across a layout change: it could now
122
138
  // overlap a message. Idle changes keep the already decoded seated pose.
123
139
  if (placement.pose !== 'seated' || !['seated', 'chin', 'doze'].includes(host.dataset.pose ?? ''))
@@ -152,6 +168,7 @@ export function installXiaoheiWallpaperCharacter(doc = typeof document === 'unde
152
168
  part.dataset.ready = 'true';
153
169
  if (host.dataset.pose !== pose)
154
170
  host.dataset.pose = pose;
171
+ motion?.setPose(part, pose);
155
172
  }
156
173
  function schedule() {
157
174
  if (disposed || frame !== undefined)
@@ -185,6 +202,7 @@ export function installXiaoheiWallpaperCharacter(doc = typeof document === 'unde
185
202
  unsubscribe();
186
203
  observer?.disconnect();
187
204
  idle.dispose();
205
+ motion?.dispose();
188
206
  for (const event of events)
189
207
  doc.removeEventListener(event, activity, true);
190
208
  doc.removeEventListener('visibilitychange', visibility);
@@ -1,6 +1,7 @@
1
1
  import { XIAOHEI_SCENE_LAYER_ID, XIAOHEI_SCENE_WORLD_CLASS } from './scene.js';
2
2
  import { subscribeXiaoheiHostDom } from './host-dom.js';
3
3
  import { XIAOHEI_HOST_SELECTORS } from './host-contract.js';
4
+ import { createSidebarReveal } from './sidebar-reveal.js';
4
5
  /** Stable id for the paint-only glass surface behind DSH's native sidebar. */
5
6
  export const XIAOHEI_SIDEBAR_GLASS_ID = 'dsh-theme-xiaohei/sidebar-glass';
6
7
  const HORIZONTAL_INSET_START = 7;
@@ -35,6 +36,7 @@ export function installXiaoheiSidebarGlass(doc = typeof document === 'undefined'
35
36
  let resizing = false;
36
37
  let appliedBounds;
37
38
  let geometryDirty = true;
39
+ const reveal = createSidebarReveal(doc);
38
40
  const clearResizeState = () => {
39
41
  if (resizeSettleTimer !== undefined)
40
42
  win.clearTimeout(resizeSettleTimer);
@@ -63,15 +65,19 @@ export function installXiaoheiSidebarGlass(doc = typeof document === 'undefined'
63
65
  return;
64
66
  // RO runs after host layout and before paint. Update the independent
65
67
  // paint layer now, not in next frame's RAF (which visibly trails).
66
- applyBounds();
68
+ const changed = applyBounds();
67
69
  geometryDirty = false;
68
- markResizeActivity();
70
+ if (changed)
71
+ markResizeActivity();
69
72
  })
70
73
  : undefined;
71
74
  const applyBounds = () => {
72
75
  if (glass === undefined || sidebarColumn === undefined)
73
- return;
76
+ return false;
74
77
  const bounds = resolveXiaoheiSidebarGlassBounds(sidebarColumn.getBoundingClientRect());
78
+ reveal.resize(bounds.width + HORIZONTAL_INSET_START + HORIZONTAL_INSET_END);
79
+ const changed = appliedBounds !== undefined && (bounds.left !== appliedBounds.left || bounds.top !== appliedBounds.top ||
80
+ bounds.width !== appliedBounds.width || bounds.height !== appliedBounds.height);
75
81
  if (bounds.left !== appliedBounds?.left) {
76
82
  glass.style.setProperty('--xiaohei-sidebar-glass-left', `${bounds.left}px`);
77
83
  }
@@ -85,6 +91,7 @@ export function installXiaoheiSidebarGlass(doc = typeof document === 'undefined'
85
91
  glass.style.setProperty('--xiaohei-sidebar-glass-height', `${bounds.height}px`);
86
92
  }
87
93
  appliedBounds = bounds;
94
+ return changed;
88
95
  };
89
96
  const reconcile = () => {
90
97
  if (disposed)
@@ -99,6 +106,7 @@ export function installXiaoheiSidebarGlass(doc = typeof document === 'undefined'
99
106
  resizeObserver?.observe(sidebarColumn);
100
107
  }
101
108
  if (sceneLayer === null || sidebarColumn === undefined) {
109
+ reveal.setColumn(undefined);
102
110
  clearResizeState();
103
111
  glass?.remove();
104
112
  glass = undefined;
@@ -106,6 +114,7 @@ export function installXiaoheiSidebarGlass(doc = typeof document === 'undefined'
106
114
  geometryDirty = true;
107
115
  return;
108
116
  }
117
+ reveal.setColumn(sidebarColumn);
109
118
  if (glass === undefined || glass.parentElement !== sceneLayer) {
110
119
  doc.getElementById(XIAOHEI_SIDEBAR_GLASS_ID)?.remove();
111
120
  glass = doc.createElement('div');
@@ -147,6 +156,7 @@ export function installXiaoheiSidebarGlass(doc = typeof document === 'undefined'
147
156
  animationFrame = undefined;
148
157
  unsubscribeHostDom();
149
158
  resizeObserver?.disconnect();
159
+ reveal.dispose();
150
160
  win.removeEventListener('resize', onViewportResize);
151
161
  clearResizeState();
152
162
  glass?.remove();
@@ -0,0 +1,8 @@
1
+ /** Synchronize native wide content with the visible column, without changing
2
+ * Host layout/state or adding a competing animation timer. */
3
+ export declare function createSidebarReveal(doc: Document): {
4
+ setColumn(column: HTMLElement | undefined): void;
5
+ resize(nextWidth: number): void;
6
+ dispose(): void;
7
+ };
8
+ //# sourceMappingURL=sidebar-reveal.d.ts.map