@fungi.computer/sprite-zdog 0.0.0-stage → 0.0.1

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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +125 -2
  3. package/dist/agent-proof/moth.d.ts +50 -0
  4. package/dist/agent-proof/moth.js +52 -0
  5. package/dist/author.d.ts +217 -0
  6. package/dist/author.js +372 -0
  7. package/dist/avatar.d.ts +69 -0
  8. package/dist/avatar.js +40 -0
  9. package/dist/builtins/base-face.d.ts +290 -0
  10. package/dist/builtins/base-face.js +615 -0
  11. package/dist/builtins/base.d.ts +331 -0
  12. package/dist/builtins/base.js +579 -0
  13. package/dist/builtins/burger-input.d.ts +310 -0
  14. package/dist/builtins/burger-input.js +517 -0
  15. package/dist/builtins/burger.d.ts +305 -0
  16. package/dist/builtins/burger.js +4 -0
  17. package/dist/builtins/cat-input.d.ts +316 -0
  18. package/dist/builtins/cat-input.js +255 -0
  19. package/dist/builtins/cat.d.ts +311 -0
  20. package/dist/builtins/cat.js +4 -0
  21. package/dist/builtins/index.d.ts +4 -0
  22. package/dist/builtins/index.js +4 -0
  23. package/dist/builtins/mushroom-input.d.ts +314 -0
  24. package/dist/builtins/mushroom-input.js +94 -0
  25. package/dist/builtins/mushroom.d.ts +310 -0
  26. package/dist/builtins/mushroom.js +4 -0
  27. package/dist/burger.d.ts +9 -0
  28. package/dist/burger.js +15 -0
  29. package/dist/cat.d.ts +9 -0
  30. package/dist/cat.js +15 -0
  31. package/dist/catalog.d.ts +3 -0
  32. package/dist/catalog.js +63 -0
  33. package/dist/document.d.ts +444 -0
  34. package/dist/document.js +204 -0
  35. package/dist/index.d.ts +10 -0
  36. package/dist/index.js +10 -0
  37. package/dist/internal.d.ts +41 -0
  38. package/dist/internal.js +21 -0
  39. package/dist/mushroom.character.json +1930 -0
  40. package/dist/mushroom.d.ts +4 -0
  41. package/dist/mushroom.js +9 -0
  42. package/dist/palette.d.ts +3 -0
  43. package/dist/palette.js +5 -0
  44. package/dist/portable.d.ts +4 -0
  45. package/dist/portable.js +61 -0
  46. package/dist/renderer.d.ts +4 -0
  47. package/dist/renderer.js +106 -0
  48. package/dist/zdog-runtime.d.ts +21 -0
  49. package/dist/zdog-runtime.js +622 -0
  50. package/package.json +112 -3
@@ -0,0 +1,622 @@
1
+ import { createTimeline, createTimer, } from "animejs";
2
+ import * as Zdog from "zdog";
3
+ import { colorKeyFromValue } from "./palette.js";
4
+ const STANDARD_ACTIVITY_DEFAULTS = new Map([
5
+ [
6
+ "idle",
7
+ {
8
+ geometry: {},
9
+ motion: {
10
+ target: "root",
11
+ translate: { y: 0.5 },
12
+ durationMs: 2000,
13
+ repeat: "yoyo",
14
+ },
15
+ mouseInfluence: 1,
16
+ },
17
+ ],
18
+ [
19
+ "listening",
20
+ {
21
+ geometry: {},
22
+ motion: {
23
+ target: "root",
24
+ rotate: { x: -0.2 },
25
+ durationMs: 300,
26
+ repeat: "yoyo",
27
+ },
28
+ mouseInfluence: 0.3,
29
+ },
30
+ ],
31
+ ["processing", { geometry: {}, mouseInfluence: 0.2 }],
32
+ [
33
+ "speaking",
34
+ {
35
+ geometry: {},
36
+ motion: {
37
+ target: "root",
38
+ translate: { y: 0.5 },
39
+ durationMs: 1500,
40
+ repeat: "yoyo",
41
+ },
42
+ mouseInfluence: 0,
43
+ mouthCycle: [
44
+ { durationMs: 100, mouth: "open" },
45
+ { durationMs: 150, mouth: "base" },
46
+ { durationMs: 100, mouth: "open" },
47
+ { durationMs: 200, mouth: "base" },
48
+ ],
49
+ },
50
+ ],
51
+ [
52
+ "thinking",
53
+ {
54
+ geometry: {},
55
+ motion: {
56
+ target: "root",
57
+ rotate: { z: 0.1 },
58
+ durationMs: 800,
59
+ repeat: "yoyo",
60
+ },
61
+ mouseInfluence: 0,
62
+ },
63
+ ],
64
+ [
65
+ "error",
66
+ {
67
+ geometry: { expression: "dead" },
68
+ mouseInfluence: 0.1,
69
+ },
70
+ ],
71
+ ]);
72
+ // Built-in geometry was authored against the old 48 CSS-pixel avatar cell.
73
+ // Keep camera framing renderer-owned so every host scales the same scene rather
74
+ // than learning a private Zdog camera knob.
75
+ const SPRITE_REFERENCE_CSS_SIZE = 48;
76
+ const framingZoom = (width, height) => Math.min(width, height) / SPRITE_REFERENCE_CSS_SIZE;
77
+ const positiveDevicePixelRatio = (value) => value !== undefined && value > 0 ? value : 1;
78
+ const boundedSize = (width, height, maxPixels, devicePixelRatio) => {
79
+ const ratio = Math.min(1, Math.sqrt(maxPixels / (width * height * devicePixelRatio * devicePixelRatio)));
80
+ return [
81
+ Math.max(1, Math.floor(width * ratio)),
82
+ Math.max(1, Math.floor(height * ratio)),
83
+ ];
84
+ };
85
+ const finite = (value, label) => {
86
+ if (!Number.isFinite(value))
87
+ throw new Error(`Sprite ${label} must be finite`);
88
+ };
89
+ const readDimension = (target, axis) => {
90
+ const view = target.ownerDocument.defaultView;
91
+ const computed = view?.getComputedStyle(target);
92
+ const computedSize = computed?.[axis] ?? "";
93
+ const cssValue = Number.parseFloat(computedSize);
94
+ if (cssValue > 0 && computedSize.trim().endsWith("px"))
95
+ return cssValue;
96
+ const inlineValue = target.style[axis];
97
+ const inlineCssValue = Number.parseFloat(inlineValue);
98
+ if (inlineCssValue > 0 && inlineValue.trim().endsWith("px"))
99
+ return inlineCssValue;
100
+ const rect = target.getBoundingClientRect();
101
+ const rectValue = axis === "width" ? rect.width : rect.height;
102
+ if (rectValue > 0)
103
+ return rectValue;
104
+ for (let parent = target.parentElement; parent; parent = parent.parentElement) {
105
+ const parentSize = view?.getComputedStyle(parent)[axis];
106
+ const parentValue = Number.parseFloat(parentSize ?? "");
107
+ if (parentValue > 0 && parentSize?.trim().endsWith("px"))
108
+ return parentValue;
109
+ }
110
+ return target[axis] || 48;
111
+ };
112
+ const transform = (value) => {
113
+ if (!value)
114
+ return {};
115
+ const result = {};
116
+ if (value.x !== undefined)
117
+ result.x = value.x;
118
+ if (value.y !== undefined)
119
+ result.y = value.y;
120
+ if (value.z !== undefined)
121
+ result.z = value.z;
122
+ return result;
123
+ };
124
+ const valueForBlush = (value) => {
125
+ if (!value)
126
+ return "off";
127
+ if (value <= 0.5)
128
+ return "small";
129
+ if (value <= 0.8)
130
+ return "medium";
131
+ return "big";
132
+ };
133
+ /** Build renderer indexes from validated portable data, without author objects. */
134
+ const indexScene = (scene) => {
135
+ const nodes = new Map();
136
+ const nodesByName = new Map();
137
+ for (const entry of scene.nodes) {
138
+ const parent = entry.parent === undefined ? undefined : nodes.get(entry.parent);
139
+ const node = {
140
+ kind: entry.kind,
141
+ name: entry.options.name,
142
+ children: [],
143
+ options: entry.options,
144
+ };
145
+ nodes.set(entry.id, node);
146
+ if (node.name)
147
+ nodesByName.set(node.name, node);
148
+ parent?.children.push(node);
149
+ }
150
+ const root = nodes.get(scene.root);
151
+ if (!root)
152
+ throw new Error("Portable Zdog scene has no root");
153
+ const variants = new Map();
154
+ for (const [channel, values] of Object.entries(scene.variants)) {
155
+ const channelNodes = new Map();
156
+ for (const [name, id] of Object.entries(values)) {
157
+ const node = nodes.get(id);
158
+ if (!node)
159
+ throw new Error(`Portable Zdog variant node is missing: ${id}`);
160
+ channelNodes.set(name, node);
161
+ }
162
+ variants.set(channel, channelNodes);
163
+ }
164
+ return {
165
+ root,
166
+ nodesByName,
167
+ variants,
168
+ palette: scene.palette,
169
+ activities: scene.activities,
170
+ expressions: scene.expressions,
171
+ };
172
+ };
173
+ class ZdogSpriteRenderer {
174
+ internals;
175
+ target;
176
+ view;
177
+ onInvalidate;
178
+ maxPixels;
179
+ originalStyle;
180
+ originalHasInlineWidth;
181
+ originalHasInlineHeight;
182
+ originalWidthAttribute;
183
+ originalHeightAttribute;
184
+ illustration;
185
+ avatar;
186
+ animationPivot;
187
+ items = new Map();
188
+ variantItems = new Map();
189
+ palette;
190
+ mouseListener;
191
+ resizeObserver;
192
+ animationTween = null;
193
+ mouthCycleTimeline = null;
194
+ entryTween = null;
195
+ mouseX = 0;
196
+ mouseY = 0;
197
+ mouseInfluence = 1;
198
+ dirty = true;
199
+ destroyed = false;
200
+ paused = false;
201
+ lastRenderTime = 0;
202
+ constructor({ target, scene, onInvalidate, palette, maxPixels, }) {
203
+ this.target = target;
204
+ this.view = target.ownerDocument.defaultView;
205
+ this.onInvalidate = onInvalidate;
206
+ this.maxPixels = maxPixels;
207
+ this.originalStyle = target.getAttribute("style");
208
+ this.originalHasInlineWidth = target.style.width !== "";
209
+ this.originalHasInlineHeight = target.style.height !== "";
210
+ this.originalWidthAttribute = target.getAttribute("width");
211
+ this.originalHeightAttribute = target.getAttribute("height");
212
+ this.internals = indexScene(scene);
213
+ this.palette = { ...this.internals.palette };
214
+ for (const [key, value] of Object.entries(palette ?? {})) {
215
+ if (!(key in this.internals.palette))
216
+ throw new Error(`Unknown Sprite palette slot: ${key}`);
217
+ if (value !== undefined)
218
+ this.palette[key] = value;
219
+ }
220
+ // Zdog reads the backing-store attributes while constructing the
221
+ // illustration. Measure the authored CSS box before replacing the backing
222
+ // store so remounts never compound a previous device-pixel-ratio multiplier.
223
+ const cssWidth = readDimension(target, "width");
224
+ const cssHeight = readDimension(target, "height");
225
+ const [width, height] = boundedSize(cssWidth, cssHeight, maxPixels, positiveDevicePixelRatio(this.view?.devicePixelRatio));
226
+ target.width = Math.round(width);
227
+ target.height = Math.round(height);
228
+ this.illustration = new Zdog.Illustration({
229
+ element: target,
230
+ resize: false,
231
+ zoom: framingZoom(width, height),
232
+ });
233
+ this.illustration.setSize(width, height);
234
+ this.preserveCanvasStyle(cssWidth, cssHeight);
235
+ this.avatar = new Zdog.Group({
236
+ addTo: this.illustration,
237
+ rotate: { x: -Zdog.TAU / 1.05 },
238
+ });
239
+ this.animationPivot = new Zdog.Group({ addTo: this.avatar });
240
+ this.createScene(this.internals.root, this.animationPivot);
241
+ this.indexVariants();
242
+ this.mouseListener = (event) => {
243
+ this.onMouseMove(event);
244
+ };
245
+ this.view?.addEventListener("mousemove", this.mouseListener);
246
+ const ResizeObserverCtor = globalThis.ResizeObserver;
247
+ this.resizeObserver = new ResizeObserverCtor(() => {
248
+ if (!this.destroyed)
249
+ this.resize();
250
+ });
251
+ try {
252
+ this.resizeObserver.observe(target);
253
+ this.present({
254
+ activity: scene.defaultPresentation.activity,
255
+ expression: scene.defaultPresentation.expression,
256
+ });
257
+ }
258
+ catch (error) {
259
+ this.stopAnimations();
260
+ this.view?.removeEventListener("mousemove", this.mouseListener);
261
+ this.resizeObserver.disconnect();
262
+ throw error;
263
+ }
264
+ }
265
+ present(presentation) {
266
+ this.assertAlive();
267
+ if (!(presentation.expression in this.internals.expressions))
268
+ throw new Error(`Unknown Sprite expression: ${presentation.expression}`);
269
+ if (!(presentation.activity in this.internals.activities) &&
270
+ !STANDARD_ACTIVITY_DEFAULTS.has(presentation.activity))
271
+ throw new Error(`Unknown Sprite activity: ${presentation.activity}`);
272
+ this.stopAnimations();
273
+ const activity = this.activitySpec(presentation.activity);
274
+ const expression = this.internals.expressions[presentation.expression];
275
+ const effectiveExpression = activity.geometry?.expression ?? presentation.expression;
276
+ const effective = this.internals.expressions[effectiveExpression] ?? expression;
277
+ this.mouseInfluence = activity.mouseInfluence ?? 1;
278
+ this.applyVariants(effective, activity.geometry);
279
+ this.resetPivot();
280
+ if (activity.motion)
281
+ this.startMotion(activity.motion, true);
282
+ if (effective?.motion)
283
+ this.startMotion(effective.motion, false);
284
+ if (activity.mouthCycle && this.internals.variants.get("mouth")?.size)
285
+ this.startMouthCycle(activity.mouthCycle, effective);
286
+ this.dirty = true;
287
+ this.pause(this.paused);
288
+ }
289
+ setPalette(palette) {
290
+ this.assertAlive();
291
+ for (const key of Object.keys(palette)) {
292
+ if (!Object.hasOwn(this.internals.palette, key))
293
+ throw new Error(`Unknown Sprite palette slot: ${key}`);
294
+ }
295
+ Object.assign(this.palette, this.internals.palette, palette);
296
+ for (const [node, item] of this.items) {
297
+ for (const property of ["color", "backface", "frontFace"]) {
298
+ const value = node.options[property];
299
+ const key = colorKeyFromValue(value);
300
+ if (key)
301
+ item[property] = this.palette[key];
302
+ }
303
+ // Zdog's cylinder does not propagate backface/frontFace changes itself.
304
+ if (node.kind === "cylinder" &&
305
+ item.frontBase !== undefined &&
306
+ item.rearBase !== undefined) {
307
+ if (item.frontFace)
308
+ item.frontBase.backface = item.frontFace;
309
+ else if (item.backface)
310
+ item.frontBase.backface = item.backface;
311
+ else
312
+ item.frontBase.backface = true;
313
+ if (item.backface)
314
+ item.rearBase.backface = item.backface;
315
+ else
316
+ item.rearBase.backface = true;
317
+ }
318
+ }
319
+ this.dirty = true;
320
+ this.onInvalidate();
321
+ }
322
+ pause(paused) {
323
+ this.paused = paused;
324
+ for (const tween of [
325
+ this.animationTween,
326
+ this.mouthCycleTimeline,
327
+ this.entryTween,
328
+ ]) {
329
+ if (paused)
330
+ tween?.pause();
331
+ else
332
+ tween?.resume();
333
+ }
334
+ }
335
+ tick(timestamp) {
336
+ if (this.destroyed)
337
+ return false;
338
+ if (!this.illustration.ctx)
339
+ return false;
340
+ const activeTween = this.hasActiveTween();
341
+ const wantsRender = this.dirty || activeTween || this.mouseNeedsRender();
342
+ if (wantsRender && timestamp - this.lastRenderTime >= 1000 / 24) {
343
+ this.updateMouseTracking();
344
+ this.illustration.updateRenderGraph();
345
+ this.lastRenderTime = timestamp;
346
+ this.dirty = false;
347
+ }
348
+ return this.dirty || this.hasActiveTween() || this.mouseNeedsRender();
349
+ }
350
+ renderStill() {
351
+ if (this.destroyed || !this.dirty || !this.illustration.ctx)
352
+ return;
353
+ this.illustration.updateRenderGraph();
354
+ this.dirty = false;
355
+ }
356
+ resize() {
357
+ this.assertAlive();
358
+ const cssWidth = readDimension(this.target, "width");
359
+ const cssHeight = readDimension(this.target, "height");
360
+ const [width, height] = boundedSize(cssWidth, cssHeight, this.maxPixels, positiveDevicePixelRatio(this.view?.devicePixelRatio));
361
+ this.illustration.zoom = framingZoom(width, height);
362
+ this.illustration.setSize(width, height);
363
+ this.preserveCanvasStyle(cssWidth, cssHeight);
364
+ this.dirty = true;
365
+ this.onInvalidate();
366
+ }
367
+ destroy() {
368
+ if (this.destroyed)
369
+ throw new Error("Sprite instance has already been destroyed");
370
+ this.destroyed = true;
371
+ this.stopAnimations();
372
+ this.view?.removeEventListener("mousemove", this.mouseListener);
373
+ this.resizeObserver.disconnect();
374
+ const context = this.target.getContext("2d");
375
+ if (context)
376
+ context.clearRect(0, 0, this.target.width, this.target.height);
377
+ this.restoreCanvasAttributes();
378
+ }
379
+ createScene(node, parent) {
380
+ const options = { ...node.options };
381
+ delete options.parent;
382
+ delete options.name;
383
+ delete options.x;
384
+ delete options.y;
385
+ delete options.z;
386
+ options.addTo = parent;
387
+ if (options.translate)
388
+ options.translate = transform(options.translate);
389
+ else if (node.options.x !== undefined ||
390
+ node.options.y !== undefined ||
391
+ node.options.z !== undefined) {
392
+ options.translate = transform({
393
+ x: node.options.x,
394
+ y: node.options.y,
395
+ z: node.options.z,
396
+ });
397
+ }
398
+ const colorKey = colorKeyFromValue(options.color);
399
+ if (colorKey)
400
+ options.color = this.palette[colorKey];
401
+ const backfaceKey = colorKeyFromValue(options.backface);
402
+ if (backfaceKey)
403
+ options.backface = this.palette[backfaceKey];
404
+ const frontFaceKey = colorKeyFromValue(options.frontFace);
405
+ if (frontFaceKey)
406
+ options.frontFace = this.palette[frontFaceKey];
407
+ let item;
408
+ switch (node.kind) {
409
+ case "group":
410
+ item = new Zdog.Group(options);
411
+ break;
412
+ case "shape":
413
+ options.path = options.path ?? [{}];
414
+ item = new Zdog.Shape(options);
415
+ break;
416
+ case "ellipse":
417
+ item = new Zdog.Ellipse(options);
418
+ break;
419
+ case "hemisphere":
420
+ item = new Zdog.Hemisphere(options);
421
+ break;
422
+ case "cylinder":
423
+ item = new Zdog.Cylinder(options);
424
+ break;
425
+ }
426
+ this.items.set(node, item);
427
+ for (const child of node.children)
428
+ this.createScene(child, item);
429
+ return item;
430
+ }
431
+ indexVariants() {
432
+ for (const [channel, values] of this.internals.variants.entries()) {
433
+ const channelItems = new Map();
434
+ for (const [variant, node] of values.entries()) {
435
+ const item = this.items.get(node);
436
+ if (item)
437
+ channelItems.set(variant, item);
438
+ }
439
+ this.variantItems.set(channel, channelItems);
440
+ }
441
+ }
442
+ applyVariants(expression, geometry) {
443
+ for (const channelItems of this.variantItems.values()) {
444
+ for (const item of channelItems.values())
445
+ item.visible = false;
446
+ }
447
+ const selected = { ...expression?.variants };
448
+ for (const [channel, choices] of Object.entries(expression?.variantChoices ?? {})) {
449
+ const index = Math.min(choices.length - 1, Math.floor(Math.random() * choices.length));
450
+ const choice = choices[index];
451
+ if (choice !== undefined)
452
+ selected[channel] = choice;
453
+ }
454
+ if (geometry?.eyes)
455
+ selected.eyes = geometry.eyes;
456
+ if (geometry?.mouth)
457
+ selected.mouth = geometry.mouth;
458
+ if (geometry?.blush !== undefined)
459
+ selected.blush = valueForBlush(geometry.blush);
460
+ else if (expression?.blush !== undefined)
461
+ selected.blush = valueForBlush(expression.blush);
462
+ for (const [channel, variant] of Object.entries(selected)) {
463
+ const item = this.variantItems.get(channel)?.get(variant);
464
+ if (item)
465
+ item.visible = true;
466
+ }
467
+ }
468
+ activitySpec(name) {
469
+ const defaults = STANDARD_ACTIVITY_DEFAULTS.get(name) ?? {
470
+ geometry: {},
471
+ mouseInfluence: 1,
472
+ };
473
+ const custom = this.internals.activities[name];
474
+ if (!custom)
475
+ return defaults;
476
+ const motion = custom.motion ?? defaults.motion;
477
+ const mouthCycle = custom.mouthCycle ?? defaults.mouthCycle;
478
+ const activity = {
479
+ ...defaults,
480
+ ...custom,
481
+ geometry: { ...defaults.geometry, ...custom.geometry },
482
+ };
483
+ if (motion !== undefined)
484
+ activity.motion = motion;
485
+ if (mouthCycle !== undefined)
486
+ activity.mouthCycle = mouthCycle;
487
+ return activity;
488
+ }
489
+ startMotion(motion, base) {
490
+ const targetNode = this.internals.nodesByName.get(motion.target);
491
+ const target = targetNode === this.internals.root
492
+ ? this.animationPivot
493
+ : targetNode
494
+ ? this.items.get(targetNode)
495
+ : undefined;
496
+ if (!target)
497
+ return;
498
+ const timeline = createTimeline({
499
+ loop: motion.repeat === "loop" || motion.repeat === "yoyo",
500
+ alternate: motion.repeat === "yoyo",
501
+ });
502
+ const duration = motion.durationMs ?? 1000;
503
+ if (motion.translate)
504
+ timeline.add(target.translate, { ...transform(motion.translate), duration, ease: "inOutSine" }, 0);
505
+ if (motion.rotate)
506
+ timeline.add(target.rotate, { ...transform(motion.rotate), duration, ease: "inOutSine" }, 0);
507
+ if (base)
508
+ this.animationTween = timeline;
509
+ else
510
+ this.entryTween = timeline;
511
+ }
512
+ startMouthCycle(cycle, expression) {
513
+ const steps = [];
514
+ let duration = 0;
515
+ for (const step of cycle) {
516
+ finite(step.durationMs, "mouthCycle.durationMs");
517
+ duration += step.durationMs;
518
+ steps.push({
519
+ until: duration,
520
+ mouth: step.mouth === "open" ? "open" : expression?.variants?.mouth,
521
+ });
522
+ }
523
+ let shown;
524
+ this.mouthCycleTimeline = createTimer({
525
+ duration,
526
+ loop: true,
527
+ onUpdate: (timer) => {
528
+ const mouth = steps.find((step) => timer.iterationCurrentTime < step.until)?.mouth;
529
+ if (mouth && mouth !== shown) {
530
+ shown = mouth;
531
+ this.showVariant("mouth", mouth);
532
+ }
533
+ },
534
+ onLoop: () => {
535
+ shown = undefined;
536
+ },
537
+ });
538
+ }
539
+ showVariant(channel, variant) {
540
+ const channelItems = this.variantItems.get(channel);
541
+ if (channelItems === undefined)
542
+ return;
543
+ const selected = channelItems.get(variant);
544
+ if (selected === undefined)
545
+ return;
546
+ for (const item of channelItems.values())
547
+ item.visible = false;
548
+ selected.visible = true;
549
+ this.dirty = true;
550
+ }
551
+ resetPivot() {
552
+ Object.assign(this.animationPivot.rotate, { x: 0, y: 0, z: 0 });
553
+ Object.assign(this.animationPivot.translate, { x: 0, y: 0, z: 0 });
554
+ Object.assign(this.avatar.rotate, { x: -Zdog.TAU / 1.05, y: 0, z: 0 });
555
+ }
556
+ stopAnimations() {
557
+ this.animationTween?.cancel();
558
+ this.mouthCycleTimeline?.cancel();
559
+ this.entryTween?.cancel();
560
+ this.animationTween = null;
561
+ this.mouthCycleTimeline = null;
562
+ this.entryTween = null;
563
+ }
564
+ hasActiveTween() {
565
+ return [this.animationTween, this.mouthCycleTimeline, this.entryTween].some((tween) => tween !== null && !tween.paused && !tween.completed && !tween.cancelled);
566
+ }
567
+ onMouseMove(event) {
568
+ if (this.destroyed || this.paused)
569
+ return;
570
+ const rect = this.target.getBoundingClientRect();
571
+ const width = rect.width || 1;
572
+ const height = rect.height || 1;
573
+ this.mouseX = Math.max(-1, Math.min(1, (event.clientX - rect.left - width / 2) / (width / 2)));
574
+ this.mouseY = Math.max(-1, Math.min(1, (event.clientY - rect.top - height / 2) / (height / 2)));
575
+ this.dirty = true;
576
+ this.onInvalidate();
577
+ }
578
+ updateMouseTracking() {
579
+ const targetY = -this.mouseX * 0.4 * this.mouseInfluence;
580
+ const targetX = -Zdog.TAU / 1.05 - this.mouseY * 0.2 * this.mouseInfluence;
581
+ const dy = (targetY - this.avatar.rotate.y) * 0.08;
582
+ const dx = (targetX - this.avatar.rotate.x) * 0.08;
583
+ this.avatar.rotate.y += dy;
584
+ this.avatar.rotate.x += dx;
585
+ if (Math.abs(dy) > 0.001 || Math.abs(dx) > 0.001)
586
+ this.dirty = true;
587
+ }
588
+ mouseNeedsRender() {
589
+ return (Math.abs(this.avatar.rotate.y + this.mouseX * 0.4 * this.mouseInfluence) >
590
+ 0.001 ||
591
+ Math.abs(this.avatar.rotate.x +
592
+ Zdog.TAU / 1.05 +
593
+ this.mouseY * 0.2 * this.mouseInfluence) > 0.001);
594
+ }
595
+ assertAlive() {
596
+ if (this.destroyed)
597
+ throw new Error("Sprite instance has been destroyed");
598
+ }
599
+ restoreCanvasAttributes() {
600
+ if (this.originalStyle === null)
601
+ this.target.removeAttribute("style");
602
+ else
603
+ this.target.setAttribute("style", this.originalStyle);
604
+ if (this.originalWidthAttribute === null)
605
+ this.target.removeAttribute("width");
606
+ else
607
+ this.target.setAttribute("width", this.originalWidthAttribute);
608
+ if (this.originalHeightAttribute === null)
609
+ this.target.removeAttribute("height");
610
+ else
611
+ this.target.setAttribute("height", this.originalHeightAttribute);
612
+ }
613
+ preserveCanvasStyle(width, height) {
614
+ if (this.originalStyle !== null)
615
+ this.target.setAttribute("style", this.originalStyle);
616
+ if (!this.originalHasInlineWidth)
617
+ this.target.style.width = `${String(Math.round(width))}px`;
618
+ if (!this.originalHasInlineHeight)
619
+ this.target.style.height = `${String(Math.round(height))}px`;
620
+ }
621
+ }
622
+ export const createZdogRenderer = (options) => new ZdogSpriteRenderer(options);