@effing/annie-player 0.34.0 → 0.35.0

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.
@@ -48,6 +48,7 @@ var AnniePlayerCore = class {
48
48
  isLoading: this._isLoading,
49
49
  isPlaying: this._isPlaying,
50
50
  frameCount: this.frames.length,
51
+ currentFrame: this.currentFrameIndex,
51
52
  dimensions: this._dimensions
52
53
  };
53
54
  }
@@ -155,18 +156,18 @@ var AnniePlayerCore = class {
155
156
  }
156
157
  }
157
158
  /**
158
- * Start playing the animation.
159
+ * Start playing the animation from the current frame.
159
160
  */
160
161
  play() {
161
162
  if (this._isPlaying || this.frames.length === 0) return;
162
163
  this._isPlaying = true;
163
- this.currentFrameIndex = 0;
164
164
  this.lastFrameTime = 0;
165
+ this.drawFrame(this.currentFrameIndex);
165
166
  this.setStatus("Playing...");
166
- this.animate(performance.now());
167
+ this.animationFrameId = requestAnimationFrame(this.animate);
167
168
  }
168
169
  /**
169
- * Pause the animation.
170
+ * Pause the animation at the current frame.
170
171
  */
171
172
  pause() {
172
173
  if (!this._isPlaying) return;
@@ -178,7 +179,7 @@ var AnniePlayerCore = class {
178
179
  this.setStatus("Paused.");
179
180
  }
180
181
  /**
181
- * Stop the animation and reset to the beginning.
182
+ * Stop the animation and reset to the first frame.
182
183
  */
183
184
  stop() {
184
185
  this._isPlaying = false;
@@ -189,6 +190,37 @@ var AnniePlayerCore = class {
189
190
  this.currentFrameIndex = 0;
190
191
  this.lastFrameTime = 0;
191
192
  }
193
+ /**
194
+ * Seek to a specific frame. The frame is drawn immediately. Playback state
195
+ * is preserved — if playing, playback continues forward from the new frame.
196
+ */
197
+ seek(frameIndex) {
198
+ if (this.frames.length === 0) return;
199
+ const clamped = Math.max(
200
+ 0,
201
+ Math.min(Math.floor(frameIndex), this.frames.length - 1)
202
+ );
203
+ this.lastFrameTime = 0;
204
+ this.drawFrame(clamped);
205
+ }
206
+ drawFrame(index) {
207
+ const indexChanged = index !== this.currentFrameIndex;
208
+ if (!this.canvas || !this.context) {
209
+ if (indexChanged) {
210
+ this.currentFrameIndex = index;
211
+ this.emitStateChange();
212
+ }
213
+ return;
214
+ }
215
+ const frame = this.frames[index];
216
+ if (!frame) return;
217
+ this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
218
+ this.context.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);
219
+ if (indexChanged) {
220
+ this.currentFrameIndex = index;
221
+ this.emitStateChange();
222
+ }
223
+ }
192
224
  animate = (timestamp) => {
193
225
  if (!this._isPlaying || this.frames.length === 0) {
194
226
  this.animationFrameId = null;
@@ -204,18 +236,8 @@ var AnniePlayerCore = class {
204
236
  const elapsed = timestamp - this.lastFrameTime;
205
237
  if (elapsed >= this.frameInterval) {
206
238
  this.lastFrameTime = timestamp - elapsed % this.frameInterval;
207
- this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
208
- const frameToDraw = this.frames[this.currentFrameIndex];
209
- if (frameToDraw) {
210
- this.context.drawImage(
211
- frameToDraw,
212
- 0,
213
- 0,
214
- this.canvas.width,
215
- this.canvas.height
216
- );
217
- }
218
- this.currentFrameIndex = (this.currentFrameIndex + 1) % this.frames.length;
239
+ const nextIndex = (this.currentFrameIndex + 1) % this.frames.length;
240
+ this.drawFrame(nextIndex);
219
241
  }
220
242
  this.animationFrameId = requestAnimationFrame(this.animate);
221
243
  };
@@ -244,4 +266,4 @@ var AnniePlayerCore = class {
244
266
  export {
245
267
  AnniePlayerCore
246
268
  };
247
- //# sourceMappingURL=chunk-AFGBXFMF.js.map
269
+ //# sourceMappingURL=chunk-XXS5ARJX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core.ts"],"sourcesContent":["import { untar } from \"@andrewbranch/untar.js\";\n\nexport type AnniePlayerState = {\n status: string;\n error: string | null;\n isLoading: boolean;\n isPlaying: boolean;\n frameCount: number;\n currentFrame: number;\n dimensions: { width: number; height: number } | null;\n};\n\nexport type AnniePlayerEvents = {\n statechange: AnniePlayerState;\n load: { frameCount: number; dimensions: { width: number; height: number } };\n error: Error;\n};\n\nexport type AnniePlayerOptions = {\n src: string;\n fps?: number;\n autoPlay?: boolean;\n};\n\ntype EventCallback<T> = (data: T) => void;\n\n/**\n * Framework-agnostic Annie animation player.\n * Handles loading TAR archives of image frames and playing them on a canvas.\n */\nexport class AnniePlayerCore {\n private src: string;\n private fps: number;\n private autoPlay: boolean;\n private frameInterval: number;\n\n private frames: HTMLImageElement[] = [];\n private canvas: HTMLCanvasElement | null = null;\n private context: CanvasRenderingContext2D | null = null;\n\n private animationFrameId: number | null = null;\n private currentFrameIndex = 0;\n private lastFrameTime = 0;\n\n private _isLoading = false;\n private _isPlaying = false;\n private _error: string | null = null;\n private _status = \"Ready to load animation.\";\n private _dimensions: { width: number; height: number } | null = null;\n\n private listeners: Map<\n keyof AnniePlayerEvents,\n Set<EventCallback<AnniePlayerEvents[keyof AnniePlayerEvents]>>\n > = new Map();\n\n constructor(options: AnniePlayerOptions) {\n this.src = options.src;\n this.fps = options.fps ?? 30;\n this.autoPlay = options.autoPlay ?? false;\n this.frameInterval = 1000 / this.fps;\n }\n\n /**\n * Attach a canvas element to render the animation on.\n */\n attachCanvas(canvas: HTMLCanvasElement): void {\n this.canvas = canvas;\n this.context = canvas.getContext(\"2d\");\n }\n\n /**\n * Detach the canvas and stop playback.\n */\n detachCanvas(): void {\n this.stop();\n this.canvas = null;\n this.context = null;\n }\n\n /**\n * Get the current state of the player.\n */\n getState(): AnniePlayerState {\n return {\n status: this._status,\n error: this._error,\n isLoading: this._isLoading,\n isPlaying: this._isPlaying,\n frameCount: this.frames.length,\n currentFrame: this.currentFrameIndex,\n dimensions: this._dimensions,\n };\n }\n\n /**\n * Subscribe to player events.\n */\n on<K extends keyof AnniePlayerEvents>(\n event: K,\n callback: EventCallback<AnniePlayerEvents[K]>,\n ): () => void {\n if (!this.listeners.has(event)) {\n this.listeners.set(event, new Set());\n }\n const callbacks = this.listeners.get(event)!;\n callbacks.add(\n callback as EventCallback<AnniePlayerEvents[keyof AnniePlayerEvents]>,\n );\n\n return () => {\n callbacks.delete(\n callback as EventCallback<AnniePlayerEvents[keyof AnniePlayerEvents]>,\n );\n };\n }\n\n private emit<K extends keyof AnniePlayerEvents>(\n event: K,\n data: AnniePlayerEvents[K],\n ): void {\n const callbacks = this.listeners.get(event);\n if (callbacks) {\n callbacks.forEach((cb) => cb(data));\n }\n }\n\n private emitStateChange(): void {\n this.emit(\"statechange\", this.getState());\n }\n\n private setStatus(status: string): void {\n this._status = status;\n this.emitStateChange();\n }\n\n private setError(error: string | null): void {\n this._error = error;\n if (error) {\n this.emit(\"error\", new Error(error));\n }\n this.emitStateChange();\n }\n\n /**\n * Load the animation from the source URL.\n */\n async load(): Promise<void> {\n if (this._isLoading) return;\n\n this.stop();\n this._isLoading = true;\n this._error = null;\n this.setStatus(\"Loading Annie file...\");\n this.cleanup();\n\n const imageFiles: { name: string; image: HTMLImageElement }[] = [];\n const imageLoadPromises: Promise<void>[] = [];\n\n try {\n const response = await fetch(this.src);\n if (!response.ok) {\n throw new Error(`Failed to fetch animation: ${response.statusText}`);\n }\n if (!response.body) {\n throw new Error(\"Response body is null\");\n }\n\n this.setStatus(\"Extracting frames...\");\n\n untar(await response.arrayBuffer()).forEach((file) => {\n if (!file.name || file.typeflag !== \"0\") return;\n if (!file.fileData) return;\n\n const blob = new Blob([new Uint8Array(file.fileData)]);\n\n const promise = new Promise<void>((resolve, reject) => {\n const img = new Image();\n img.onload = () => {\n imageFiles.push({ name: file.name!, image: img });\n if (imageFiles.length === 1) {\n this._dimensions = {\n width: img.naturalWidth,\n height: img.naturalHeight,\n };\n }\n resolve();\n };\n img.onerror = (err) => {\n reject(new Error(`Could not load image: ${file.name} (${err})`));\n };\n img.src = URL.createObjectURL(blob);\n });\n imageLoadPromises.push(promise);\n });\n\n await Promise.all(imageLoadPromises);\n\n if (imageFiles.length === 0) {\n throw new Error(\"No frames found in the TAR file.\");\n }\n\n imageFiles.sort((a, b) => a.name.localeCompare(b.name));\n this.frames = imageFiles.map((f) => f.image);\n this._isLoading = false;\n this.setStatus(`Loaded ${this.frames.length} frames. Ready to play.`);\n\n this.emit(\"load\", {\n frameCount: this.frames.length,\n dimensions: this._dimensions!,\n });\n\n if (this.autoPlay && this.frames.length > 0) {\n this.play();\n }\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : \"Unknown error\";\n this._isLoading = false;\n this.setError(errorMessage);\n this.setStatus(`Error: ${errorMessage}`);\n }\n }\n\n /**\n * Start playing the animation from the current frame.\n */\n play(): void {\n if (this._isPlaying || this.frames.length === 0) return;\n\n this._isPlaying = true;\n this.lastFrameTime = 0;\n this.drawFrame(this.currentFrameIndex);\n this.setStatus(\"Playing...\");\n this.animationFrameId = requestAnimationFrame(this.animate);\n }\n\n /**\n * Pause the animation at the current frame.\n */\n pause(): void {\n if (!this._isPlaying) return;\n\n this._isPlaying = false;\n if (this.animationFrameId !== null) {\n cancelAnimationFrame(this.animationFrameId);\n this.animationFrameId = null;\n }\n this.setStatus(\"Paused.\");\n }\n\n /**\n * Stop the animation and reset to the first frame.\n */\n stop(): void {\n this._isPlaying = false;\n if (this.animationFrameId !== null) {\n cancelAnimationFrame(this.animationFrameId);\n this.animationFrameId = null;\n }\n this.currentFrameIndex = 0;\n this.lastFrameTime = 0;\n }\n\n /**\n * Seek to a specific frame. The frame is drawn immediately. Playback state\n * is preserved — if playing, playback continues forward from the new frame.\n */\n seek(frameIndex: number): void {\n if (this.frames.length === 0) return;\n const clamped = Math.max(\n 0,\n Math.min(Math.floor(frameIndex), this.frames.length - 1),\n );\n this.lastFrameTime = 0;\n this.drawFrame(clamped);\n }\n\n private drawFrame(index: number): void {\n const indexChanged = index !== this.currentFrameIndex;\n if (!this.canvas || !this.context) {\n if (indexChanged) {\n this.currentFrameIndex = index;\n this.emitStateChange();\n }\n return;\n }\n const frame = this.frames[index];\n if (!frame) return;\n this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);\n this.context.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);\n if (indexChanged) {\n this.currentFrameIndex = index;\n this.emitStateChange();\n }\n }\n\n private animate = (timestamp: number): void => {\n if (!this._isPlaying || this.frames.length === 0) {\n this.animationFrameId = null;\n return;\n }\n\n if (!this.canvas || !this.context) {\n this.animationFrameId = null;\n return;\n }\n\n if (this.lastFrameTime === 0) {\n this.lastFrameTime = timestamp;\n }\n\n const elapsed = timestamp - this.lastFrameTime;\n\n if (elapsed >= this.frameInterval) {\n this.lastFrameTime = timestamp - (elapsed % this.frameInterval);\n const nextIndex = (this.currentFrameIndex + 1) % this.frames.length;\n this.drawFrame(nextIndex);\n }\n\n this.animationFrameId = requestAnimationFrame(this.animate);\n };\n\n /**\n * Clean up resources (revoke blob URLs).\n */\n cleanup(): void {\n this.frames.forEach((img) => {\n if (img.src.startsWith(\"blob:\")) {\n URL.revokeObjectURL(img.src);\n }\n });\n this.frames = [];\n }\n\n /**\n * Destroy the player and clean up all resources.\n */\n destroy(): void {\n this.stop();\n this.cleanup();\n this.detachCanvas();\n this.listeners.clear();\n }\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ type AnniePlayerState = {
4
4
  isLoading: boolean;
5
5
  isPlaying: boolean;
6
6
  frameCount: number;
7
+ currentFrame: number;
7
8
  dimensions: {
8
9
  width: number;
9
10
  height: number;
@@ -73,17 +74,23 @@ declare class AnniePlayerCore {
73
74
  */
74
75
  load(): Promise<void>;
75
76
  /**
76
- * Start playing the animation.
77
+ * Start playing the animation from the current frame.
77
78
  */
78
79
  play(): void;
79
80
  /**
80
- * Pause the animation.
81
+ * Pause the animation at the current frame.
81
82
  */
82
83
  pause(): void;
83
84
  /**
84
- * Stop the animation and reset to the beginning.
85
+ * Stop the animation and reset to the first frame.
85
86
  */
86
87
  stop(): void;
88
+ /**
89
+ * Seek to a specific frame. The frame is drawn immediately. Playback state
90
+ * is preserved — if playing, playback continues forward from the new frame.
91
+ */
92
+ seek(frameIndex: number): void;
93
+ private drawFrame;
87
94
  private animate;
88
95
  /**
89
96
  * Clean up resources (revoke blob URLs).
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  AnniePlayerCore
3
- } from "./chunk-AFGBXFMF.js";
3
+ } from "./chunk-XXS5ARJX.js";
4
4
  export {
5
5
  AnniePlayerCore
6
6
  };
@@ -7,6 +7,7 @@ type AnniePlayerContextValue = {
7
7
  load: () => void;
8
8
  play: () => void;
9
9
  pause: () => void;
10
+ seek: (frameIndex: number) => void;
10
11
  };
11
12
  canvasRef: React.RefObject<HTMLCanvasElement | null>;
12
13
  canvasDimensions: {
@@ -66,6 +67,23 @@ type AnniePlayerPauseButtonProps = {
66
67
  style?: React.CSSProperties;
67
68
  };
68
69
  declare function AnniePlayerPauseButton({ children, className, style, }: AnniePlayerPauseButtonProps): react_jsx_runtime.JSX.Element;
70
+ type AnniePlayerScrubberProps = {
71
+ className?: string;
72
+ style?: React.CSSProperties;
73
+ /** If true, pause playback while the user is interacting with the scrubber. */
74
+ pauseWhileScrubbing?: boolean;
75
+ };
76
+ declare function AnniePlayerScrubber({ className, style, pauseWhileScrubbing, }: AnniePlayerScrubberProps): react_jsx_runtime.JSX.Element;
77
+ type AnniePlayerFrameCounterProps = {
78
+ className?: string;
79
+ style?: React.CSSProperties;
80
+ /** Render the counter contents. Defaults to "current / total". */
81
+ children?: (frame: {
82
+ current: number;
83
+ total: number;
84
+ }) => React.ReactNode;
85
+ };
86
+ declare function AnniePlayerFrameCounter({ className, style, children, }: AnniePlayerFrameCounterProps): react_jsx_runtime.JSX.Element;
69
87
  type AnniePlayerStatusProps = {
70
88
  children?: React.ReactNode;
71
89
  className?: string;
@@ -101,6 +119,8 @@ declare const AnniePlayer: typeof AnniePlayerSimple & {
101
119
  LoadButton: typeof AnniePlayerLoadButton;
102
120
  PlayButton: typeof AnniePlayerPlayButton;
103
121
  PauseButton: typeof AnniePlayerPauseButton;
122
+ Scrubber: typeof AnniePlayerScrubber;
123
+ FrameCounter: typeof AnniePlayerFrameCounter;
104
124
  Status: typeof AnniePlayerStatus;
105
125
  };
106
126
 
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  AnniePlayerCore
3
- } from "../chunk-AFGBXFMF.js";
3
+ } from "../chunk-XXS5ARJX.js";
4
4
 
5
5
  // src/react/index.tsx
6
6
  import { createContext, useContext, useEffect, useRef, useState } from "react";
@@ -34,6 +34,7 @@ function AnniePlayerRoot({
34
34
  isLoading: false,
35
35
  isPlaying: false,
36
36
  frameCount: 0,
37
+ currentFrame: 0,
37
38
  dimensions: null
38
39
  });
39
40
  const [canvasDimensions, setCanvasDimensions] = useState({
@@ -74,7 +75,8 @@ function AnniePlayerRoot({
74
75
  const actions = {
75
76
  load: () => playerRef.current?.load(),
76
77
  play: () => playerRef.current?.play(),
77
- pause: () => playerRef.current?.pause()
78
+ pause: () => playerRef.current?.pause(),
79
+ seek: (frameIndex) => playerRef.current?.seek(frameIndex)
78
80
  };
79
81
  const value = {
80
82
  state,
@@ -210,6 +212,63 @@ function AnniePlayerPauseButton({
210
212
  }
211
213
  );
212
214
  }
215
+ function AnniePlayerScrubber({
216
+ className,
217
+ style,
218
+ pauseWhileScrubbing = false
219
+ }) {
220
+ const { state, actions } = useAnniePlayerContext();
221
+ const wasPlayingRef = useRef(false);
222
+ const disabled = state.frameCount === 0;
223
+ const max = Math.max(0, state.frameCount - 1);
224
+ const handlePointerDown = () => {
225
+ if (pauseWhileScrubbing && state.isPlaying) {
226
+ wasPlayingRef.current = true;
227
+ actions.pause();
228
+ }
229
+ };
230
+ const handlePointerUp = () => {
231
+ if (pauseWhileScrubbing && wasPlayingRef.current) {
232
+ wasPlayingRef.current = false;
233
+ actions.play();
234
+ }
235
+ };
236
+ return /* @__PURE__ */ jsx(
237
+ "input",
238
+ {
239
+ type: "range",
240
+ min: 0,
241
+ max,
242
+ step: 1,
243
+ value: state.currentFrame,
244
+ disabled,
245
+ "data-disabled": disabled,
246
+ onChange: (e) => actions.seek(Number(e.target.value)),
247
+ onPointerDown: handlePointerDown,
248
+ onPointerUp: handlePointerUp,
249
+ onPointerCancel: handlePointerUp,
250
+ className,
251
+ style: { width: "100%", ...style }
252
+ }
253
+ );
254
+ }
255
+ function AnniePlayerFrameCounter({
256
+ className,
257
+ style,
258
+ children
259
+ }) {
260
+ const { state } = useAnniePlayerContext();
261
+ const current = state.currentFrame;
262
+ const total = Math.max(0, state.frameCount - 1);
263
+ return /* @__PURE__ */ jsx(
264
+ "span",
265
+ {
266
+ className,
267
+ style: { fontVariantNumeric: "tabular-nums", ...style },
268
+ children: children ? children({ current, total }) : `${current} / ${total}`
269
+ }
270
+ );
271
+ }
213
272
  function AnniePlayerStatus({
214
273
  children,
215
274
  className,
@@ -268,8 +327,9 @@ function AnniePlayerSimple({
268
327
  const loadDisabled = state.isLoading;
269
328
  const playDisabled = state.isLoading || state.isPlaying || state.frameCount === 0;
270
329
  const pauseDisabled = !state.isPlaying;
271
- const buttonContainerVisible = !state.isPlaying || isHovering;
272
- const statusVisible = isHovering;
330
+ const scrubberDisabled = state.frameCount === 0;
331
+ const overlayVisible = !state.isPlaying || isHovering;
332
+ const totalFrames = Math.max(0, state.frameCount - 1);
273
333
  const buttonStyle = (disabled) => ({
274
334
  backgroundColor: "#222",
275
335
  color: "white",
@@ -310,7 +370,7 @@ function AnniePlayerSimple({
310
370
  gap: 4,
311
371
  zIndex: 10,
312
372
  transition: "opacity 0.3s ease",
313
- opacity: buttonContainerVisible ? 1 : 0
373
+ opacity: overlayVisible ? 1 : 0
314
374
  },
315
375
  children: [
316
376
  /* @__PURE__ */ jsx(
@@ -352,26 +412,79 @@ function AnniePlayerSimple({
352
412
  style: {
353
413
  position: "absolute",
354
414
  bottom: 16,
415
+ left: 16,
416
+ right: 16,
417
+ zIndex: 10,
418
+ display: "flex",
419
+ alignItems: "center",
420
+ gap: 12,
421
+ padding: "8px 12px",
422
+ backgroundColor: "rgba(0, 0, 0, 0.7)",
423
+ color: "white",
424
+ borderRadius: 4,
425
+ fontSize: 13,
426
+ transition: "opacity 0.3s ease",
427
+ opacity: overlayVisible ? 1 : 0,
428
+ pointerEvents: overlayVisible ? "auto" : "none"
429
+ },
430
+ children: [
431
+ /* @__PURE__ */ jsx(
432
+ "input",
433
+ {
434
+ type: "range",
435
+ min: 0,
436
+ max: totalFrames,
437
+ step: 1,
438
+ value: state.currentFrame,
439
+ disabled: scrubberDisabled,
440
+ onChange: (e) => actions.seek(Number(e.target.value)),
441
+ style: {
442
+ flex: 1,
443
+ accentColor: "#fff",
444
+ cursor: scrubberDisabled ? "not-allowed" : "pointer"
445
+ }
446
+ }
447
+ ),
448
+ /* @__PURE__ */ jsxs(
449
+ "span",
450
+ {
451
+ style: {
452
+ fontVariantNumeric: "tabular-nums",
453
+ whiteSpace: "nowrap",
454
+ minWidth: 64,
455
+ textAlign: "right"
456
+ },
457
+ children: [
458
+ state.currentFrame,
459
+ " / ",
460
+ totalFrames
461
+ ]
462
+ }
463
+ )
464
+ ]
465
+ }
466
+ ),
467
+ state.error && /* @__PURE__ */ jsxs(
468
+ "div",
469
+ {
470
+ style: {
471
+ position: "absolute",
472
+ top: 64,
355
473
  left: 0,
356
474
  right: 0,
357
475
  margin: "0 auto",
358
476
  width: "fit-content",
359
477
  maxWidth: "90%",
360
478
  zIndex: 10,
361
- backgroundColor: "rgba(0, 0, 0, 0.8)",
479
+ backgroundColor: "rgba(180, 0, 0, 0.85)",
362
480
  color: "white",
363
481
  padding: 8,
364
482
  borderRadius: 4,
365
- fontSize: 14,
366
- transition: "opacity 0.3s ease",
367
- opacity: statusVisible ? 1 : 0
483
+ fontSize: 14
368
484
  },
369
485
  children: [
370
- state.status,
371
- state.error && /* @__PURE__ */ jsxs("span", { children: [
372
- " Error: ",
373
- state.error
374
- ] })
486
+ "Error: ",
487
+ state.error
375
488
  ]
376
489
  }
377
490
  ),
@@ -403,6 +516,8 @@ var AnniePlayer = Object.assign(AnniePlayerSimple, {
403
516
  LoadButton: AnniePlayerLoadButton,
404
517
  PlayButton: AnniePlayerPlayButton,
405
518
  PauseButton: AnniePlayerPauseButton,
519
+ Scrubber: AnniePlayerScrubber,
520
+ FrameCounter: AnniePlayerFrameCounter,
406
521
  Status: AnniePlayerStatus
407
522
  });
408
523
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/react/index.tsx"],"sourcesContent":["import { createContext, useContext, useEffect, useRef, useState } from \"react\";\nimport { AnniePlayerCore, type AnniePlayerState } from \"../core\";\n\n// ============ Context ============\n\ntype AnniePlayerContextValue = {\n state: AnniePlayerState;\n actions: {\n load: () => void;\n play: () => void;\n pause: () => void;\n };\n canvasRef: React.RefObject<HTMLCanvasElement | null>;\n canvasDimensions: { width: number; height: number };\n isHovering: boolean;\n setIsHovering: (v: boolean) => void;\n};\n\nconst AnniePlayerContext = createContext<AnniePlayerContextValue | null>(null);\n\nfunction useAnniePlayerContext() {\n const ctx = useContext(AnniePlayerContext);\n if (!ctx) {\n throw new Error(\n \"AnniePlayer compound components must be used within AnniePlayer.Root\",\n );\n }\n return ctx;\n}\n\n// ============ Root Component ============\n\nexport type AnniePlayerRootProps = {\n src: string;\n height: number;\n defaultWidth?: number;\n autoLoad?: boolean;\n autoPlay?: boolean;\n fps?: number;\n /** Style for the root container */\n style?: React.CSSProperties;\n /** Class name for the root container */\n className?: string;\n /** Children can be React nodes or a render function for headless usage */\n children?:\n | React.ReactNode\n | ((value: AnniePlayerContextValue) => React.ReactNode);\n};\n\nfunction AnniePlayerRoot({\n src,\n height,\n defaultWidth,\n autoLoad = false,\n autoPlay = false,\n fps = 30,\n style,\n className,\n children,\n}: AnniePlayerRootProps) {\n const canvasRef = useRef<HTMLCanvasElement>(null);\n const playerRef = useRef<AnniePlayerCore | null>(null);\n const [state, setState] = useState<AnniePlayerState>({\n status: \"Ready to load animation.\",\n error: null,\n isLoading: false,\n isPlaying: false,\n frameCount: 0,\n dimensions: null,\n });\n const [canvasDimensions, setCanvasDimensions] = useState({\n width: defaultWidth ?? height,\n height,\n });\n const [isHovering, setIsHovering] = useState(false);\n\n // Initialize the core player\n useEffect(() => {\n const player = new AnniePlayerCore({ src, fps, autoPlay });\n playerRef.current = player;\n\n const unsubscribeState = player.on(\"statechange\", (newState) => {\n setState(newState);\n });\n\n const unsubscribeLoad = player.on(\"load\", ({ dimensions }) => {\n setCanvasDimensions({\n width: (dimensions.width / dimensions.height) * height,\n height,\n });\n });\n\n return () => {\n unsubscribeState();\n unsubscribeLoad();\n player.destroy();\n };\n }, [src, fps, autoPlay, height]);\n\n // Attach canvas to player when ready\n useEffect(() => {\n const player = playerRef.current;\n const canvas = canvasRef.current;\n if (player && canvas) {\n player.attachCanvas(canvas);\n }\n }, [canvasDimensions]);\n\n // Auto-load if enabled\n useEffect(() => {\n if (autoLoad && playerRef.current) {\n playerRef.current.load();\n }\n }, [autoLoad]);\n\n const actions = {\n load: () => playerRef.current?.load(),\n play: () => playerRef.current?.play(),\n pause: () => playerRef.current?.pause(),\n };\n\n const value: AnniePlayerContextValue = {\n state,\n actions,\n canvasRef,\n canvasDimensions,\n isHovering,\n setIsHovering,\n };\n\n // Render function pattern for headless usage\n if (typeof children === \"function\") {\n return <>{children(value)}</>;\n }\n\n return (\n <AnniePlayerContext.Provider value={value}>\n <div\n style={{ display: \"inline-block\", width: \"fit-content\", ...style }}\n className={className}\n >\n {children}\n </div>\n </AnniePlayerContext.Provider>\n );\n}\n\n// ============ Wrapper Component ============\n\ntype AnniePlayerWrapperProps = {\n children: React.ReactNode;\n className?: string;\n style?: React.CSSProperties;\n};\n\nfunction AnniePlayerWrapper({\n children,\n className,\n style,\n}: AnniePlayerWrapperProps) {\n const { isHovering, setIsHovering, state } = useAnniePlayerContext();\n return (\n <div\n style={{\n position: \"relative\",\n display: \"inline-block\",\n width: \"fit-content\",\n ...style,\n }}\n className={className}\n onMouseEnter={() => setIsHovering(true)}\n onMouseLeave={() => setIsHovering(false)}\n data-hovering={isHovering}\n data-playing={state.isPlaying}\n >\n {children}\n </div>\n );\n}\n\n// ============ Canvas Component ============\n\ntype AnniePlayerCanvasProps = {\n className?: string;\n style?: React.CSSProperties;\n};\n\nfunction AnniePlayerCanvas({ className, style }: AnniePlayerCanvasProps) {\n const { canvasRef, canvasDimensions } = useAnniePlayerContext();\n return (\n <canvas\n ref={canvasRef}\n width={canvasDimensions.width}\n height={canvasDimensions.height}\n className={className}\n style={{ display: \"block\", ...style }}\n />\n );\n}\n\n// ============ Controls Component ============\n\ntype AnniePlayerControlsProps = {\n children: React.ReactNode;\n className?: string;\n style?: React.CSSProperties;\n};\n\nfunction AnniePlayerControls({\n children,\n className,\n style,\n}: AnniePlayerControlsProps) {\n const { state, isHovering } = useAnniePlayerContext();\n const visible = !state.isPlaying || isHovering;\n return (\n <div\n className={className}\n style={{\n transition: \"opacity 0.3s ease\",\n opacity: visible ? 1 : 0,\n ...style,\n }}\n >\n {children}\n </div>\n );\n}\n\n// ============ Button Components ============\n\ntype AnniePlayerLoadButtonProps = {\n children?: React.ReactNode;\n className?: string;\n style?: React.CSSProperties;\n};\n\nfunction AnniePlayerLoadButton({\n children,\n className,\n style,\n}: AnniePlayerLoadButtonProps) {\n const { state, actions } = useAnniePlayerContext();\n const disabled = state.isLoading;\n return (\n <button\n onClick={actions.load}\n disabled={disabled}\n data-disabled={disabled}\n className={className}\n style={style}\n >\n {children ?? (state.isLoading ? \"Loading...\" : \"Load\")}\n </button>\n );\n}\n\ntype AnniePlayerPlayButtonProps = {\n children?: React.ReactNode;\n className?: string;\n style?: React.CSSProperties;\n};\n\nfunction AnniePlayerPlayButton({\n children,\n className,\n style,\n}: AnniePlayerPlayButtonProps) {\n const { state, actions } = useAnniePlayerContext();\n const disabled = state.isLoading || state.isPlaying || state.frameCount === 0;\n return (\n <button\n onClick={actions.play}\n disabled={disabled}\n data-disabled={disabled}\n className={className}\n style={style}\n >\n {children ?? \"Play\"}\n </button>\n );\n}\n\ntype AnniePlayerPauseButtonProps = {\n children?: React.ReactNode;\n className?: string;\n style?: React.CSSProperties;\n};\n\nfunction AnniePlayerPauseButton({\n children,\n className,\n style,\n}: AnniePlayerPauseButtonProps) {\n const { state, actions } = useAnniePlayerContext();\n const disabled = !state.isPlaying;\n return (\n <button\n onClick={actions.pause}\n disabled={disabled}\n data-disabled={disabled}\n className={className}\n style={style}\n >\n {children ?? \"Pause\"}\n </button>\n );\n}\n\n// ============ Status Component ============\n\ntype AnniePlayerStatusProps = {\n children?: React.ReactNode;\n className?: string;\n style?: React.CSSProperties;\n};\n\nfunction AnniePlayerStatus({\n children,\n className,\n style,\n}: AnniePlayerStatusProps) {\n const { state, isHovering } = useAnniePlayerContext();\n const visible = isHovering;\n return (\n <div\n className={className}\n style={{\n transition: \"opacity 0.3s ease\",\n opacity: visible ? 1 : 0,\n ...style,\n }}\n >\n {children ?? (\n <>\n {state.status}\n {state.error && <span> Error: {state.error}</span>}\n </>\n )}\n </div>\n );\n}\n\n// ============ Simple Component ============\n\nexport type AnniePlayerProps = {\n src: string;\n height: number;\n defaultWidth?: number;\n autoLoad?: boolean;\n autoPlay?: boolean;\n fps?: number;\n /** Style for the root container */\n style?: React.CSSProperties;\n /** Class name for the root container */\n className?: string;\n};\n\nfunction AnniePlayerSimple({\n src,\n height,\n defaultWidth,\n autoLoad = false,\n autoPlay = false,\n fps = 30,\n style,\n className,\n}: AnniePlayerProps) {\n return (\n <AnniePlayerRoot\n src={src}\n height={height}\n defaultWidth={defaultWidth}\n autoLoad={autoLoad}\n autoPlay={autoPlay}\n fps={fps}\n style={style}\n className={className}\n >\n {({\n state,\n actions,\n canvasRef,\n canvasDimensions,\n isHovering,\n setIsHovering,\n }) => {\n const loadDisabled = state.isLoading;\n const playDisabled =\n state.isLoading || state.isPlaying || state.frameCount === 0;\n const pauseDisabled = !state.isPlaying;\n const buttonContainerVisible = !state.isPlaying || isHovering;\n const statusVisible = isHovering;\n\n const buttonStyle = (disabled: boolean): React.CSSProperties => ({\n backgroundColor: \"#222\",\n color: \"white\",\n border: \"none\",\n borderRadius: 4,\n padding: \"0.5rem 1rem\",\n cursor: \"pointer\",\n margin: \"0 4px\",\n fontSize: \"14px\",\n fontWeight: 500,\n opacity: disabled ? 0.6 : 1,\n });\n\n return (\n <div\n style={{\n position: \"relative\",\n display: \"inline-block\",\n width: \"fit-content\",\n }}\n data-hovering={isHovering}\n data-playing={state.isPlaying}\n onMouseEnter={() => setIsHovering(true)}\n onMouseLeave={() => setIsHovering(false)}\n >\n <div\n style={{\n position: \"absolute\",\n width: \"90%\",\n top: 16,\n left: \"50%\",\n transform: \"translateX(-50%)\",\n display: \"flex\",\n flexWrap: \"wrap\",\n justifyContent: \"center\",\n gap: 4,\n zIndex: 10,\n transition: \"opacity 0.3s ease\",\n opacity: buttonContainerVisible ? 1 : 0,\n }}\n >\n <button\n onClick={actions.load}\n disabled={loadDisabled}\n data-disabled={loadDisabled}\n style={buttonStyle(loadDisabled)}\n >\n {state.isLoading ? \"Loading...\" : \"Load\"}\n </button>\n <button\n onClick={actions.play}\n disabled={playDisabled}\n data-disabled={playDisabled}\n style={buttonStyle(playDisabled)}\n >\n Play\n </button>\n <button\n onClick={actions.pause}\n disabled={pauseDisabled}\n data-disabled={pauseDisabled}\n style={buttonStyle(pauseDisabled)}\n >\n Pause\n </button>\n </div>\n\n <div\n style={{\n position: \"absolute\",\n bottom: 16,\n left: 0,\n right: 0,\n margin: \"0 auto\",\n width: \"fit-content\",\n maxWidth: \"90%\",\n zIndex: 10,\n backgroundColor: \"rgba(0, 0, 0, 0.8)\",\n color: \"white\",\n padding: 8,\n borderRadius: 4,\n fontSize: 14,\n transition: \"opacity 0.3s ease\",\n opacity: statusVisible ? 1 : 0,\n }}\n >\n {state.status}\n {state.error && <span> Error: {state.error}</span>}\n </div>\n\n <canvas\n ref={canvasRef}\n width={canvasDimensions.width}\n height={canvasDimensions.height}\n style={{\n display: \"block\",\n border: \"1px solid #ccc\",\n backgroundColor: \"#f5f5f5\",\n }}\n />\n </div>\n );\n }}\n </AnniePlayerRoot>\n );\n}\n\n/**\n * AnniePlayer - Play TAR archives of PNG/JPEG frames in the browser.\n *\n * Usage:\n * - Simple: `<AnniePlayer src={url} height={540} />`\n * - Compound: `<AnniePlayer.Root src={url} height={540}>...</AnniePlayer.Root>`\n * - Headless: `<AnniePlayer.Root src={url} height={540}>{({ state, actions }) => ...}</AnniePlayer.Root>`\n */\nexport const AnniePlayer = Object.assign(AnniePlayerSimple, {\n Root: AnniePlayerRoot,\n Wrapper: AnniePlayerWrapper,\n Canvas: AnniePlayerCanvas,\n Controls: AnniePlayerControls,\n LoadButton: AnniePlayerLoadButton,\n PlayButton: AnniePlayerPlayButton,\n PauseButton: AnniePlayerPauseButton,\n Status: AnniePlayerStatus,\n});\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/react/index.tsx"],"sourcesContent":["import { createContext, useContext, useEffect, useRef, useState } from \"react\";\nimport { AnniePlayerCore, type AnniePlayerState } from \"../core\";\n\n// ============ Context ============\n\ntype AnniePlayerContextValue = {\n state: AnniePlayerState;\n actions: {\n load: () => void;\n play: () => void;\n pause: () => void;\n seek: (frameIndex: number) => void;\n };\n canvasRef: React.RefObject<HTMLCanvasElement | null>;\n canvasDimensions: { width: number; height: number };\n isHovering: boolean;\n setIsHovering: (v: boolean) => void;\n};\n\nconst AnniePlayerContext = createContext<AnniePlayerContextValue | null>(null);\n\nfunction useAnniePlayerContext() {\n const ctx = useContext(AnniePlayerContext);\n if (!ctx) {\n throw new Error(\n \"AnniePlayer compound components must be used within AnniePlayer.Root\",\n );\n }\n return ctx;\n}\n\n// ============ Root Component ============\n\nexport type AnniePlayerRootProps = {\n src: string;\n height: number;\n defaultWidth?: number;\n autoLoad?: boolean;\n autoPlay?: boolean;\n fps?: number;\n /** Style for the root container */\n style?: React.CSSProperties;\n /** Class name for the root container */\n className?: string;\n /** Children can be React nodes or a render function for headless usage */\n children?:\n | React.ReactNode\n | ((value: AnniePlayerContextValue) => React.ReactNode);\n};\n\nfunction AnniePlayerRoot({\n src,\n height,\n defaultWidth,\n autoLoad = false,\n autoPlay = false,\n fps = 30,\n style,\n className,\n children,\n}: AnniePlayerRootProps) {\n const canvasRef = useRef<HTMLCanvasElement>(null);\n const playerRef = useRef<AnniePlayerCore | null>(null);\n const [state, setState] = useState<AnniePlayerState>({\n status: \"Ready to load animation.\",\n error: null,\n isLoading: false,\n isPlaying: false,\n frameCount: 0,\n currentFrame: 0,\n dimensions: null,\n });\n const [canvasDimensions, setCanvasDimensions] = useState({\n width: defaultWidth ?? height,\n height,\n });\n const [isHovering, setIsHovering] = useState(false);\n\n // Initialize the core player\n useEffect(() => {\n const player = new AnniePlayerCore({ src, fps, autoPlay });\n playerRef.current = player;\n\n const unsubscribeState = player.on(\"statechange\", (newState) => {\n setState(newState);\n });\n\n const unsubscribeLoad = player.on(\"load\", ({ dimensions }) => {\n setCanvasDimensions({\n width: (dimensions.width / dimensions.height) * height,\n height,\n });\n });\n\n return () => {\n unsubscribeState();\n unsubscribeLoad();\n player.destroy();\n };\n }, [src, fps, autoPlay, height]);\n\n // Attach canvas to player when ready\n useEffect(() => {\n const player = playerRef.current;\n const canvas = canvasRef.current;\n if (player && canvas) {\n player.attachCanvas(canvas);\n }\n }, [canvasDimensions]);\n\n // Auto-load if enabled\n useEffect(() => {\n if (autoLoad && playerRef.current) {\n playerRef.current.load();\n }\n }, [autoLoad]);\n\n const actions = {\n load: () => playerRef.current?.load(),\n play: () => playerRef.current?.play(),\n pause: () => playerRef.current?.pause(),\n seek: (frameIndex: number) => playerRef.current?.seek(frameIndex),\n };\n\n const value: AnniePlayerContextValue = {\n state,\n actions,\n canvasRef,\n canvasDimensions,\n isHovering,\n setIsHovering,\n };\n\n // Render function pattern for headless usage\n if (typeof children === \"function\") {\n return <>{children(value)}</>;\n }\n\n return (\n <AnniePlayerContext.Provider value={value}>\n <div\n style={{ display: \"inline-block\", width: \"fit-content\", ...style }}\n className={className}\n >\n {children}\n </div>\n </AnniePlayerContext.Provider>\n );\n}\n\n// ============ Wrapper Component ============\n\ntype AnniePlayerWrapperProps = {\n children: React.ReactNode;\n className?: string;\n style?: React.CSSProperties;\n};\n\nfunction AnniePlayerWrapper({\n children,\n className,\n style,\n}: AnniePlayerWrapperProps) {\n const { isHovering, setIsHovering, state } = useAnniePlayerContext();\n return (\n <div\n style={{\n position: \"relative\",\n display: \"inline-block\",\n width: \"fit-content\",\n ...style,\n }}\n className={className}\n onMouseEnter={() => setIsHovering(true)}\n onMouseLeave={() => setIsHovering(false)}\n data-hovering={isHovering}\n data-playing={state.isPlaying}\n >\n {children}\n </div>\n );\n}\n\n// ============ Canvas Component ============\n\ntype AnniePlayerCanvasProps = {\n className?: string;\n style?: React.CSSProperties;\n};\n\nfunction AnniePlayerCanvas({ className, style }: AnniePlayerCanvasProps) {\n const { canvasRef, canvasDimensions } = useAnniePlayerContext();\n return (\n <canvas\n ref={canvasRef}\n width={canvasDimensions.width}\n height={canvasDimensions.height}\n className={className}\n style={{ display: \"block\", ...style }}\n />\n );\n}\n\n// ============ Controls Component ============\n\ntype AnniePlayerControlsProps = {\n children: React.ReactNode;\n className?: string;\n style?: React.CSSProperties;\n};\n\nfunction AnniePlayerControls({\n children,\n className,\n style,\n}: AnniePlayerControlsProps) {\n const { state, isHovering } = useAnniePlayerContext();\n const visible = !state.isPlaying || isHovering;\n return (\n <div\n className={className}\n style={{\n transition: \"opacity 0.3s ease\",\n opacity: visible ? 1 : 0,\n ...style,\n }}\n >\n {children}\n </div>\n );\n}\n\n// ============ Button Components ============\n\ntype AnniePlayerLoadButtonProps = {\n children?: React.ReactNode;\n className?: string;\n style?: React.CSSProperties;\n};\n\nfunction AnniePlayerLoadButton({\n children,\n className,\n style,\n}: AnniePlayerLoadButtonProps) {\n const { state, actions } = useAnniePlayerContext();\n const disabled = state.isLoading;\n return (\n <button\n onClick={actions.load}\n disabled={disabled}\n data-disabled={disabled}\n className={className}\n style={style}\n >\n {children ?? (state.isLoading ? \"Loading...\" : \"Load\")}\n </button>\n );\n}\n\ntype AnniePlayerPlayButtonProps = {\n children?: React.ReactNode;\n className?: string;\n style?: React.CSSProperties;\n};\n\nfunction AnniePlayerPlayButton({\n children,\n className,\n style,\n}: AnniePlayerPlayButtonProps) {\n const { state, actions } = useAnniePlayerContext();\n const disabled = state.isLoading || state.isPlaying || state.frameCount === 0;\n return (\n <button\n onClick={actions.play}\n disabled={disabled}\n data-disabled={disabled}\n className={className}\n style={style}\n >\n {children ?? \"Play\"}\n </button>\n );\n}\n\ntype AnniePlayerPauseButtonProps = {\n children?: React.ReactNode;\n className?: string;\n style?: React.CSSProperties;\n};\n\nfunction AnniePlayerPauseButton({\n children,\n className,\n style,\n}: AnniePlayerPauseButtonProps) {\n const { state, actions } = useAnniePlayerContext();\n const disabled = !state.isPlaying;\n return (\n <button\n onClick={actions.pause}\n disabled={disabled}\n data-disabled={disabled}\n className={className}\n style={style}\n >\n {children ?? \"Pause\"}\n </button>\n );\n}\n\n// ============ Scrubber Component ============\n\ntype AnniePlayerScrubberProps = {\n className?: string;\n style?: React.CSSProperties;\n /** If true, pause playback while the user is interacting with the scrubber. */\n pauseWhileScrubbing?: boolean;\n};\n\nfunction AnniePlayerScrubber({\n className,\n style,\n pauseWhileScrubbing = false,\n}: AnniePlayerScrubberProps) {\n const { state, actions } = useAnniePlayerContext();\n const wasPlayingRef = useRef(false);\n const disabled = state.frameCount === 0;\n const max = Math.max(0, state.frameCount - 1);\n\n const handlePointerDown = () => {\n if (pauseWhileScrubbing && state.isPlaying) {\n wasPlayingRef.current = true;\n actions.pause();\n }\n };\n\n const handlePointerUp = () => {\n if (pauseWhileScrubbing && wasPlayingRef.current) {\n wasPlayingRef.current = false;\n actions.play();\n }\n };\n\n return (\n <input\n type=\"range\"\n min={0}\n max={max}\n step={1}\n value={state.currentFrame}\n disabled={disabled}\n data-disabled={disabled}\n onChange={(e) => actions.seek(Number(e.target.value))}\n onPointerDown={handlePointerDown}\n onPointerUp={handlePointerUp}\n onPointerCancel={handlePointerUp}\n className={className}\n style={{ width: \"100%\", ...style }}\n />\n );\n}\n\n// ============ Frame Counter Component ============\n\ntype AnniePlayerFrameCounterProps = {\n className?: string;\n style?: React.CSSProperties;\n /** Render the counter contents. Defaults to \"current / total\". */\n children?: (frame: { current: number; total: number }) => React.ReactNode;\n};\n\nfunction AnniePlayerFrameCounter({\n className,\n style,\n children,\n}: AnniePlayerFrameCounterProps) {\n const { state } = useAnniePlayerContext();\n const current = state.currentFrame;\n const total = Math.max(0, state.frameCount - 1);\n return (\n <span\n className={className}\n style={{ fontVariantNumeric: \"tabular-nums\", ...style }}\n >\n {children ? children({ current, total }) : `${current} / ${total}`}\n </span>\n );\n}\n\n// ============ Status Component ============\n\ntype AnniePlayerStatusProps = {\n children?: React.ReactNode;\n className?: string;\n style?: React.CSSProperties;\n};\n\nfunction AnniePlayerStatus({\n children,\n className,\n style,\n}: AnniePlayerStatusProps) {\n const { state, isHovering } = useAnniePlayerContext();\n const visible = isHovering;\n return (\n <div\n className={className}\n style={{\n transition: \"opacity 0.3s ease\",\n opacity: visible ? 1 : 0,\n ...style,\n }}\n >\n {children ?? (\n <>\n {state.status}\n {state.error && <span> Error: {state.error}</span>}\n </>\n )}\n </div>\n );\n}\n\n// ============ Simple Component ============\n\nexport type AnniePlayerProps = {\n src: string;\n height: number;\n defaultWidth?: number;\n autoLoad?: boolean;\n autoPlay?: boolean;\n fps?: number;\n /** Style for the root container */\n style?: React.CSSProperties;\n /** Class name for the root container */\n className?: string;\n};\n\nfunction AnniePlayerSimple({\n src,\n height,\n defaultWidth,\n autoLoad = false,\n autoPlay = false,\n fps = 30,\n style,\n className,\n}: AnniePlayerProps) {\n return (\n <AnniePlayerRoot\n src={src}\n height={height}\n defaultWidth={defaultWidth}\n autoLoad={autoLoad}\n autoPlay={autoPlay}\n fps={fps}\n style={style}\n className={className}\n >\n {({\n state,\n actions,\n canvasRef,\n canvasDimensions,\n isHovering,\n setIsHovering,\n }) => {\n const loadDisabled = state.isLoading;\n const playDisabled =\n state.isLoading || state.isPlaying || state.frameCount === 0;\n const pauseDisabled = !state.isPlaying;\n const scrubberDisabled = state.frameCount === 0;\n const overlayVisible = !state.isPlaying || isHovering;\n const totalFrames = Math.max(0, state.frameCount - 1);\n\n const buttonStyle = (disabled: boolean): React.CSSProperties => ({\n backgroundColor: \"#222\",\n color: \"white\",\n border: \"none\",\n borderRadius: 4,\n padding: \"0.5rem 1rem\",\n cursor: \"pointer\",\n margin: \"0 4px\",\n fontSize: \"14px\",\n fontWeight: 500,\n opacity: disabled ? 0.6 : 1,\n });\n\n return (\n <div\n style={{\n position: \"relative\",\n display: \"inline-block\",\n width: \"fit-content\",\n }}\n data-hovering={isHovering}\n data-playing={state.isPlaying}\n onMouseEnter={() => setIsHovering(true)}\n onMouseLeave={() => setIsHovering(false)}\n >\n <div\n style={{\n position: \"absolute\",\n width: \"90%\",\n top: 16,\n left: \"50%\",\n transform: \"translateX(-50%)\",\n display: \"flex\",\n flexWrap: \"wrap\",\n justifyContent: \"center\",\n gap: 4,\n zIndex: 10,\n transition: \"opacity 0.3s ease\",\n opacity: overlayVisible ? 1 : 0,\n }}\n >\n <button\n onClick={actions.load}\n disabled={loadDisabled}\n data-disabled={loadDisabled}\n style={buttonStyle(loadDisabled)}\n >\n {state.isLoading ? \"Loading...\" : \"Load\"}\n </button>\n <button\n onClick={actions.play}\n disabled={playDisabled}\n data-disabled={playDisabled}\n style={buttonStyle(playDisabled)}\n >\n Play\n </button>\n <button\n onClick={actions.pause}\n disabled={pauseDisabled}\n data-disabled={pauseDisabled}\n style={buttonStyle(pauseDisabled)}\n >\n Pause\n </button>\n </div>\n\n <div\n style={{\n position: \"absolute\",\n bottom: 16,\n left: 16,\n right: 16,\n zIndex: 10,\n display: \"flex\",\n alignItems: \"center\",\n gap: 12,\n padding: \"8px 12px\",\n backgroundColor: \"rgba(0, 0, 0, 0.7)\",\n color: \"white\",\n borderRadius: 4,\n fontSize: 13,\n transition: \"opacity 0.3s ease\",\n opacity: overlayVisible ? 1 : 0,\n pointerEvents: overlayVisible ? \"auto\" : \"none\",\n }}\n >\n <input\n type=\"range\"\n min={0}\n max={totalFrames}\n step={1}\n value={state.currentFrame}\n disabled={scrubberDisabled}\n onChange={(e) => actions.seek(Number(e.target.value))}\n style={{\n flex: 1,\n accentColor: \"#fff\",\n cursor: scrubberDisabled ? \"not-allowed\" : \"pointer\",\n }}\n />\n <span\n style={{\n fontVariantNumeric: \"tabular-nums\",\n whiteSpace: \"nowrap\",\n minWidth: 64,\n textAlign: \"right\",\n }}\n >\n {state.currentFrame} / {totalFrames}\n </span>\n </div>\n\n {state.error && (\n <div\n style={{\n position: \"absolute\",\n top: 64,\n left: 0,\n right: 0,\n margin: \"0 auto\",\n width: \"fit-content\",\n maxWidth: \"90%\",\n zIndex: 10,\n backgroundColor: \"rgba(180, 0, 0, 0.85)\",\n color: \"white\",\n padding: 8,\n borderRadius: 4,\n fontSize: 14,\n }}\n >\n Error: {state.error}\n </div>\n )}\n\n <canvas\n ref={canvasRef}\n width={canvasDimensions.width}\n height={canvasDimensions.height}\n style={{\n display: \"block\",\n border: \"1px solid #ccc\",\n backgroundColor: \"#f5f5f5\",\n }}\n />\n </div>\n );\n }}\n </AnniePlayerRoot>\n );\n}\n\n/**\n * AnniePlayer - Play TAR archives of PNG/JPEG frames in the browser.\n *\n * Usage:\n * - Simple: `<AnniePlayer src={url} height={540} />`\n * - Compound: `<AnniePlayer.Root src={url} height={540}>...</AnniePlayer.Root>`\n * - Headless: `<AnniePlayer.Root src={url} height={540}>{({ state, actions }) => ...}</AnniePlayer.Root>`\n */\nexport const AnniePlayer = Object.assign(AnniePlayerSimple, {\n Root: AnniePlayerRoot,\n Wrapper: AnniePlayerWrapper,\n Canvas: AnniePlayerCanvas,\n Controls: AnniePlayerControls,\n LoadButton: AnniePlayerLoadButton,\n PlayButton: AnniePlayerPlayButton,\n PauseButton: AnniePlayerPauseButton,\n Scrubber: AnniePlayerScrubber,\n FrameCounter: AnniePlayerFrameCounter,\n Status: AnniePlayerStatus,\n});\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effing/annie-player",
3
- "version": "0.34.0",
3
+ "version": "0.35.0",
4
4
  "description": "Annie animation player that runs in the browser",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/core.ts"],"sourcesContent":["import { untar } from \"@andrewbranch/untar.js\";\n\nexport type AnniePlayerState = {\n status: string;\n error: string | null;\n isLoading: boolean;\n isPlaying: boolean;\n frameCount: number;\n dimensions: { width: number; height: number } | null;\n};\n\nexport type AnniePlayerEvents = {\n statechange: AnniePlayerState;\n load: { frameCount: number; dimensions: { width: number; height: number } };\n error: Error;\n};\n\nexport type AnniePlayerOptions = {\n src: string;\n fps?: number;\n autoPlay?: boolean;\n};\n\ntype EventCallback<T> = (data: T) => void;\n\n/**\n * Framework-agnostic Annie animation player.\n * Handles loading TAR archives of image frames and playing them on a canvas.\n */\nexport class AnniePlayerCore {\n private src: string;\n private fps: number;\n private autoPlay: boolean;\n private frameInterval: number;\n\n private frames: HTMLImageElement[] = [];\n private canvas: HTMLCanvasElement | null = null;\n private context: CanvasRenderingContext2D | null = null;\n\n private animationFrameId: number | null = null;\n private currentFrameIndex = 0;\n private lastFrameTime = 0;\n\n private _isLoading = false;\n private _isPlaying = false;\n private _error: string | null = null;\n private _status = \"Ready to load animation.\";\n private _dimensions: { width: number; height: number } | null = null;\n\n private listeners: Map<\n keyof AnniePlayerEvents,\n Set<EventCallback<AnniePlayerEvents[keyof AnniePlayerEvents]>>\n > = new Map();\n\n constructor(options: AnniePlayerOptions) {\n this.src = options.src;\n this.fps = options.fps ?? 30;\n this.autoPlay = options.autoPlay ?? false;\n this.frameInterval = 1000 / this.fps;\n }\n\n /**\n * Attach a canvas element to render the animation on.\n */\n attachCanvas(canvas: HTMLCanvasElement): void {\n this.canvas = canvas;\n this.context = canvas.getContext(\"2d\");\n }\n\n /**\n * Detach the canvas and stop playback.\n */\n detachCanvas(): void {\n this.stop();\n this.canvas = null;\n this.context = null;\n }\n\n /**\n * Get the current state of the player.\n */\n getState(): AnniePlayerState {\n return {\n status: this._status,\n error: this._error,\n isLoading: this._isLoading,\n isPlaying: this._isPlaying,\n frameCount: this.frames.length,\n dimensions: this._dimensions,\n };\n }\n\n /**\n * Subscribe to player events.\n */\n on<K extends keyof AnniePlayerEvents>(\n event: K,\n callback: EventCallback<AnniePlayerEvents[K]>,\n ): () => void {\n if (!this.listeners.has(event)) {\n this.listeners.set(event, new Set());\n }\n const callbacks = this.listeners.get(event)!;\n callbacks.add(\n callback as EventCallback<AnniePlayerEvents[keyof AnniePlayerEvents]>,\n );\n\n return () => {\n callbacks.delete(\n callback as EventCallback<AnniePlayerEvents[keyof AnniePlayerEvents]>,\n );\n };\n }\n\n private emit<K extends keyof AnniePlayerEvents>(\n event: K,\n data: AnniePlayerEvents[K],\n ): void {\n const callbacks = this.listeners.get(event);\n if (callbacks) {\n callbacks.forEach((cb) => cb(data));\n }\n }\n\n private emitStateChange(): void {\n this.emit(\"statechange\", this.getState());\n }\n\n private setStatus(status: string): void {\n this._status = status;\n this.emitStateChange();\n }\n\n private setError(error: string | null): void {\n this._error = error;\n if (error) {\n this.emit(\"error\", new Error(error));\n }\n this.emitStateChange();\n }\n\n /**\n * Load the animation from the source URL.\n */\n async load(): Promise<void> {\n if (this._isLoading) return;\n\n this.stop();\n this._isLoading = true;\n this._error = null;\n this.setStatus(\"Loading Annie file...\");\n this.cleanup();\n\n const imageFiles: { name: string; image: HTMLImageElement }[] = [];\n const imageLoadPromises: Promise<void>[] = [];\n\n try {\n const response = await fetch(this.src);\n if (!response.ok) {\n throw new Error(`Failed to fetch animation: ${response.statusText}`);\n }\n if (!response.body) {\n throw new Error(\"Response body is null\");\n }\n\n this.setStatus(\"Extracting frames...\");\n\n untar(await response.arrayBuffer()).forEach((file) => {\n if (!file.name || file.typeflag !== \"0\") return;\n if (!file.fileData) return;\n\n const blob = new Blob([new Uint8Array(file.fileData)]);\n\n const promise = new Promise<void>((resolve, reject) => {\n const img = new Image();\n img.onload = () => {\n imageFiles.push({ name: file.name!, image: img });\n if (imageFiles.length === 1) {\n this._dimensions = {\n width: img.naturalWidth,\n height: img.naturalHeight,\n };\n }\n resolve();\n };\n img.onerror = (err) => {\n reject(new Error(`Could not load image: ${file.name} (${err})`));\n };\n img.src = URL.createObjectURL(blob);\n });\n imageLoadPromises.push(promise);\n });\n\n await Promise.all(imageLoadPromises);\n\n if (imageFiles.length === 0) {\n throw new Error(\"No frames found in the TAR file.\");\n }\n\n imageFiles.sort((a, b) => a.name.localeCompare(b.name));\n this.frames = imageFiles.map((f) => f.image);\n this._isLoading = false;\n this.setStatus(`Loaded ${this.frames.length} frames. Ready to play.`);\n\n this.emit(\"load\", {\n frameCount: this.frames.length,\n dimensions: this._dimensions!,\n });\n\n if (this.autoPlay && this.frames.length > 0) {\n this.play();\n }\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : \"Unknown error\";\n this._isLoading = false;\n this.setError(errorMessage);\n this.setStatus(`Error: ${errorMessage}`);\n }\n }\n\n /**\n * Start playing the animation.\n */\n play(): void {\n if (this._isPlaying || this.frames.length === 0) return;\n\n this._isPlaying = true;\n this.currentFrameIndex = 0;\n this.lastFrameTime = 0;\n this.setStatus(\"Playing...\");\n this.animate(performance.now());\n }\n\n /**\n * Pause the animation.\n */\n pause(): void {\n if (!this._isPlaying) return;\n\n this._isPlaying = false;\n if (this.animationFrameId !== null) {\n cancelAnimationFrame(this.animationFrameId);\n this.animationFrameId = null;\n }\n this.setStatus(\"Paused.\");\n }\n\n /**\n * Stop the animation and reset to the beginning.\n */\n stop(): void {\n this._isPlaying = false;\n if (this.animationFrameId !== null) {\n cancelAnimationFrame(this.animationFrameId);\n this.animationFrameId = null;\n }\n this.currentFrameIndex = 0;\n this.lastFrameTime = 0;\n }\n\n private animate = (timestamp: number): void => {\n if (!this._isPlaying || this.frames.length === 0) {\n this.animationFrameId = null;\n return;\n }\n\n if (!this.canvas || !this.context) {\n this.animationFrameId = null;\n return;\n }\n\n if (this.lastFrameTime === 0) {\n this.lastFrameTime = timestamp;\n }\n\n const elapsed = timestamp - this.lastFrameTime;\n\n if (elapsed >= this.frameInterval) {\n this.lastFrameTime = timestamp - (elapsed % this.frameInterval);\n this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);\n const frameToDraw = this.frames[this.currentFrameIndex];\n if (frameToDraw) {\n this.context.drawImage(\n frameToDraw,\n 0,\n 0,\n this.canvas.width,\n this.canvas.height,\n );\n }\n this.currentFrameIndex =\n (this.currentFrameIndex + 1) % this.frames.length;\n }\n\n this.animationFrameId = requestAnimationFrame(this.animate);\n };\n\n /**\n * Clean up resources (revoke blob URLs).\n */\n cleanup(): void {\n this.frames.forEach((img) => {\n if (img.src.startsWith(\"blob:\")) {\n URL.revokeObjectURL(img.src);\n }\n });\n this.frames = [];\n }\n\n /**\n * Destroy the player and clean up all resources.\n */\n destroy(): void {\n this.stop();\n this.cleanup();\n this.detachCanvas();\n this.listeners.clear();\n }\n}\n"],"mappings":"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","names":[]}