pixi-reels 2.0.0 → 2.2.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.
Files changed (62) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/dist/ReelSymbol-0kVAZr4S.cjs +2 -0
  3. package/dist/ReelSymbol-0kVAZr4S.cjs.map +1 -0
  4. package/dist/{ReelSymbol-Da6FHDO-.js → ReelSymbol-CRXD3IO3.js} +14 -2
  5. package/dist/ReelSymbol-CRXD3IO3.js.map +1 -0
  6. package/dist/{SpineSymbol-CzcEUfsp.js → SpineSymbol-BApOkJM6.js} +2 -2
  7. package/dist/{SpineSymbol-CzcEUfsp.js.map → SpineSymbol-BApOkJM6.js.map} +1 -1
  8. package/dist/{SpineSymbol-1c5vjgQu.cjs → SpineSymbol-Fe0Fz1JW.cjs} +2 -2
  9. package/dist/{SpineSymbol-1c5vjgQu.cjs.map → SpineSymbol-Fe0Fz1JW.cjs.map} +1 -1
  10. package/dist/config/types.d.ts +55 -0
  11. package/dist/config/types.d.ts.map +1 -1
  12. package/dist/core/Reel.d.ts +87 -11
  13. package/dist/core/Reel.d.ts.map +1 -1
  14. package/dist/core/ReelCurve.d.ts +170 -0
  15. package/dist/core/ReelCurve.d.ts.map +1 -0
  16. package/dist/core/ReelMotion.d.ts +9 -1
  17. package/dist/core/ReelMotion.d.ts.map +1 -1
  18. package/dist/core/ReelSet.d.ts +51 -0
  19. package/dist/core/ReelSet.d.ts.map +1 -1
  20. package/dist/core/ReelSetBuilder.d.ts +127 -1
  21. package/dist/core/ReelSetBuilder.d.ts.map +1 -1
  22. package/dist/core/ReelViewport.d.ts +11 -1
  23. package/dist/core/ReelViewport.d.ts.map +1 -1
  24. package/dist/core/ReelWarp.d.ts +63 -0
  25. package/dist/core/ReelWarp.d.ts.map +1 -0
  26. package/dist/debug-AjBRiekN.cjs +4 -0
  27. package/dist/debug-AjBRiekN.cjs.map +1 -0
  28. package/dist/{debug-DLQkFWGB.js → debug-BVIT3Sjs.js} +736 -362
  29. package/dist/debug-BVIT3Sjs.js.map +1 -0
  30. package/dist/frame/RandomSymbolProvider.d.ts +71 -14
  31. package/dist/frame/RandomSymbolProvider.d.ts.map +1 -1
  32. package/dist/frame/SymbolPool.d.ts +100 -0
  33. package/dist/frame/SymbolPool.d.ts.map +1 -0
  34. package/dist/index.cjs +1 -1
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.ts +7 -1
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +147 -54
  39. package/dist/index.js.map +1 -1
  40. package/dist/spine.cjs +1 -1
  41. package/dist/spine.js +2 -2
  42. package/dist/symbols/AnimatedSpriteSymbol.d.ts +5 -0
  43. package/dist/symbols/AnimatedSpriteSymbol.d.ts.map +1 -1
  44. package/dist/symbols/PerspectiveCell.d.ts +30 -0
  45. package/dist/symbols/PerspectiveCell.d.ts.map +1 -0
  46. package/dist/symbols/ReelSymbol.d.ts +41 -0
  47. package/dist/symbols/ReelSymbol.d.ts.map +1 -1
  48. package/dist/symbols/SpriteSymbol.d.ts +8 -0
  49. package/dist/symbols/SpriteSymbol.d.ts.map +1 -1
  50. package/dist/testing/testHarness.d.ts +6 -0
  51. package/dist/testing/testHarness.d.ts.map +1 -1
  52. package/dist/testing.cjs +1 -1
  53. package/dist/testing.cjs.map +1 -1
  54. package/dist/testing.js +3 -3
  55. package/dist/testing.js.map +1 -1
  56. package/package.json +2 -2
  57. package/dist/ReelSymbol-Da6FHDO-.js.map +0 -1
  58. package/dist/ReelSymbol-DldUQdRR.cjs +0 -2
  59. package/dist/ReelSymbol-DldUQdRR.cjs.map +0 -1
  60. package/dist/debug-DFdorSrM.cjs +0 -4
  61. package/dist/debug-DFdorSrM.cjs.map +0 -1
  62. package/dist/debug-DLQkFWGB.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,79 @@
1
1
  # pixi-reels
2
2
 
3
+ ## 2.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#203](https://github.com/schmooky/pixi-reels/pull/203) [`6012925`](https://github.com/schmooky/pixi-reels/commit/60129257d46fec94be9af21669afeac1c41f6898) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Add: reel curvature. `builder.curve(0.45)` projects the set onto a drum, `builder.curvePerReel([...])` gives each reel its own camera, and `reelSet.setCurve(...)` re-projects at runtime for tuning. `amount` is how far round the drum the window sees; `depth` is how strong the perspective is, capped below the angle at which cells would fold back over each other. A set with no `curve()` builds no curve object and is unchanged.
8
+
9
+ **The cell facing the camera is drawn at 1:1** - authored size, both axes, no keystone - and everything else bends around it. That has a consequence worth planning for: a drum whose middle is 1:1 cannot also reach the window edges. Its ends fall short, the buffer cells fill that band compressed as they curve away, and you frame or mask it the way a real cabinet's bezel does. Normalizing to the window edges instead would magnify the main axis at the centre while leaving the cross axis alone, i.e. a visibly stretched middle row.
10
+
11
+ **Two ways to draw it.**
12
+
13
+ `curveMode('symbol')` (default) projects each cell on its own - crisp, free, and a real keystone, but only for content that IS a texture, because a `Container` transform is affine. The engine hands each symbol a projected quad through the new `ReelSymbol.applyCellQuad()`; `SpriteSymbol` and `AnimatedSpriteSymbol` draw it through a PixiJS `PerspectiveMesh` at no extra render pass. Everything else (Spine, `Graphics`, composite subtrees) takes the closest affine fit: a UNIFORM scale sized to fit inside the projected footprint, so art is never distorted along one axis and no cell overlaps its neighbour. `PerspectiveCell` and `canProjectTexture()` are exported for custom texture-backed symbols.
14
+
15
+ `curveMode('warp')` + `renderer(app.renderer)` bends the whole reel instead - each reel is rendered to a texture and drawn through a mesh whose VERTICES are displaced by the projection. Spine, atlas art, text and composites all bend, no symbol cooperates, and because the bend is on the rendered reel rather than in each cell, the spin, the stop bounce and cascade falls travel ALONG the curve instead of translating flat. Costs one render pass per reel per frame plus one resample; `build()` throws without a renderer.
16
+
17
+ KNOWN LIMITATION (`'symbol'` mode only): an ATLAS sub-frame does not take the mesh path. The mesh addresses its source with plain 0..1 UVs and remapping them onto the frame has not produced a correct draw, so `canProjectTexture()` refuses those and the symbol takes the affine fit - correct, but not keystoned. That is most production art. `'warp'` has no such limit, since a render texture owns its whole source.
18
+
19
+ `builder.curveFocus('reel' | 'set-lean' | 'set')` picks where the camera stands across the strip: one per reel (default, five separate drums), one on the middle of the board (receding cells lean IN and the grid reads as one wide cylinder), or halfway. Anything but `'reel'` auto-selects `SharedRectMaskStrategy`, since the lean crosses each reel's own column.
20
+
21
+ `builder.curveBleed(px)` gives the warp texture room across the strip for art wider than its cell - an overflowing mystery or scatter plate - so the overhang is captured, warped with everything else, and hangs over its neighbours instead of being sliced at the texture edge. `MaskContext` gains a matching `bleed` so `SharedRectMaskStrategy` stops clipping it back to the board, which mattered most at the outermost reels where the overhang leaves the board entirely. Defaults to `0`; the field is read defensively so a `MaskContext` built before it existed still yields a valid mask.
22
+
23
+ `ReelSet.getCellQuad(reel, cell)` returns the four corners a curved cell is actually drawn on, or `null` when flat - `getCellBounds()` has to return a rectangle and widens to the trapezoid's bounding box. Outline with the quad, hit-test with the box. The debug overlay's `cells` and `buffers` layers use it, so the overlay shows the projection rather than a box around it.
24
+
25
+ Art that does not fill its cell reports its real footprint through the new `ReelSymbol.cellInset`, derived automatically from an atlas frame's trim, so a small symbol is projected where it actually sits instead of being inflated to the cell's edges.
26
+
27
+ The projection never touches a view's `position`, so landing, wrapping, cascades, big symbols and MultiWays reshapes are unaffected.
28
+
29
+ Also fixes `ReelSymbol.playDestroy()` compensating its pivot move by the raw offset instead of the offset times scale, which made a scaled symbol jump on the first frame of the destroy animation.
30
+
31
+ ## 2.1.0
32
+
33
+ ### Minor Changes
34
+
35
+ - [#204](https://github.com/schmooky/pixi-reels/pull/204) [`9f10dd5`](https://github.com/schmooky/pixi-reels/commit/9f10dd59cc2116957a0d77d944179b382ecb0809) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Add: symbol pools. `builder.randomSymbols(pool, scope)` and the runtime `reelSet.randomSymbols` decide what the engine may draw for cells the game does not name. A pool is `{ weights?, exclude? }`; its scope is `{ reel?, slots?: 'spinning' | 'buffer' }`.
36
+
37
+ Until now `weights()` was one table for the whole set, and the only levers past it. `setExcludeSpinning` / `setExcludeBuffer` on `RandomSymbolProvider` were unreachable from a built set (the provider is deliberately not exported, and `Reel` keeps its reference private), so "keep this symbol out of the buffer cells" meant writing a `FrameMiddleware`. Two things games actually ask for now have a call:
38
+
39
+ ```ts
40
+ // A coin may blur past mid-spin, but must never park half-visible
41
+ // above or below the grid.
42
+ reelSet.randomSymbols.set({ exclude: ["COIN"] }, { slots: "buffer" });
43
+
44
+ // Reel 2 runs hot on wilds for the feature, then back to the base table.
45
+ reelSet.randomSymbols.set({ weights: { WILD: 40 } }, { reel: 2 });
46
+ reelSet.randomSymbols.set(null, { reel: 2 });
47
+ ```
48
+
49
+ The two ends of the strip can be governed separately: `slots` takes `'bufferStart'` and `'bufferEnd'` as well as `'buffer'` (both) and `'spinning'` (everything). `bufferStart` is the side at the smaller main coordinate - above on a vertical set, left on a horizontal one - the same end `ColumnTarget.bufferStart` addresses, whichever way the reel travels.
50
+
51
+ ```ts
52
+ // Nothing peeks in from above; the cell below the grid is left alone.
53
+ reelSet.randomSymbols.set({ exclude: ["COIN"] }, { slots: "bufferStart" });
54
+ // ...on one reel only.
55
+ reelSet.randomSymbols.set(
56
+ { exclude: ["COIN"] },
57
+ { reel: 2, slots: "bufferEnd" }
58
+ );
59
+ ```
60
+
61
+ Layers resolve base weights -> global spinning -> per-reel spinning -> global buffer -> per-reel buffer -> global side -> per-reel side. Weights override per symbol id, exclusions accumulate, and a narrower layer can never re-admit what a wider one banned. A weight of `0` bans a symbol as surely as `exclude` does, in a pool and in `weights()` alike. Pools govern the RANDOM draw only. an explicit `setResult` / `initialFrame` target is the game speaking and always wins. `weights(scope?)` reports the effective table, so a game can assert its own configuration in a test; ask for `'buffer'` to see what both sides inherit, or for a side to see exactly what it draws from.
62
+
63
+ Two failure modes now fail loud instead of quietly doing nothing: naming an unregistered symbol id in a pool throws (with the registered ids listed), and a pool that leaves some reachable scope with nothing to draw throws at the call that caused it, naming the scope, rather than mid-spin on whichever reel wraps first. The rejected pool is not installed. `setExcludeSpinning` / `setExcludeBuffer` keep working as sugar over the global spinning / buffer pools.
64
+
65
+ `Reel.placeStrip` now random-fills each empty slot from the pool that slot belongs to. it used to apply the buffer rules to visible cells too, which mattered the moment "buffer" stopped meaning "everything a skip places".
66
+
67
+ ### Patch Changes
68
+
69
+ - [#204](https://github.com/schmooky/pixi-reels/pull/204) [`9f10dd5`](https://github.com/schmooky/pixi-reels/commit/9f10dd59cc2116957a0d77d944179b382ecb0809) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Fix: an `unmask: true` symbol in a BUFFER cell no longer renders above the mask, where it hung outside the grid in plain sight.
70
+
71
+ `unmask` lifts a view out of the reel's masked container. That is an at-rest presentation for a cell the player is looking at, and `notifyLanded()` has always lifted visible cells only. But the lift decision itself was made from the symbol id alone, so any at-rest write to a buffer slot lifted it as well. and a buffer slot is parked outside the window precisely because the mask should hide it.
72
+
73
+ The path that showed it in a real game was a skip. `StopPhase.onSkip` lands the full strip (buffers included) through `placeStrip`, so a skip taken once the bounce has started. i.e. after `notifyLanded()` put the reel back at rest. lifted every unmask symbol the target frame had in `bufferStart` / `bufferEnd`, and they stayed up there until the next spin pulled them back down. `Reel.reshape` growing a strip at rest did the same to its new tail cells.
74
+
75
+ The lift now takes the slot into account, so a buffer cell never lifts. A second case needed the reverse: a symbol lifted while it was VISIBLE can still travel into a buffer slot without being replaced. a nudge rotates the array and only the wrapped symbol goes through `_replaceSymbol`, so an unmask symbol nudged out of the window kept its seat above the mask. Every settle now re-masks any lifted view that ended up outside the window.
76
+
3
77
  ## 2.0.0
4
78
 
5
79
  ### Major Changes
@@ -0,0 +1,2 @@
1
+ let e=require(`pixi.js`);var t=require(`gsap`).gsap,n=class{view;_symbolId=``;_isDestroyed=!1;_gsap=t;_mainAxis=`y`;constructor(){this.view=new e.Container}get gsap(){return this._gsap}bindGsap(e){this._gsap=e}get mainAxis(){return this._mainAxis}bindMainAxis(e){this._mainAxis=e}get symbolId(){return this._symbolId}get isDestroyed(){return this._isDestroyed}activate(e){this._symbolId=e,this.view.visible=!0,this.view.alpha=1,this.view.scale.set(1,1),this.view.rotation=0,this.view.filters=null,this.view.zIndex=0,this.onActivate(e)}deactivate(){this.stopAnimation(),this.onDeactivate(),this._symbolId=``,this.view.visible=!1,this.view.alpha=1,this.view.scale.set(1,1),this.view.rotation=0,this.view.filters=null,this.view.zIndex=0}reset(){this.deactivate()}destroy(){this._isDestroyed||=(this.stopAnimation(),this.onDeactivate(),this.onDestroy(),this.view.destroyed||this.view.destroy({children:!0}),!0)}onDestroy(){}async playDestroy(e){let t=this.view,n=t.pivot.x,r=t.pivot.y,i=t.x,a=t.y,o=t.getLocalBounds(),s=o.x+o.width/2,c=o.y+o.height/2;t.pivot.set(s,c),t.x=i+(s-n)*t.scale.x,t.y=a+(c-r)*t.scale.y;let l=e?.delay??0,u=e?.signal,d=()=>{t.alpha=0,t.scale.set(0,0)};if(u?.aborted){d(),t.pivot.set(n,r),t.x=i,t.y=a,t.scale.set(1,1),t.alpha=0;return}await new Promise(e=>{let n=this.gsap.timeline({onComplete:()=>{u&&u.removeEventListener(`abort`,r),e()},delay:l}).to(t.scale,{x:1.1,y:1.1,duration:.06,ease:`back.out(2.5)`}).to(t.scale,{x:0,y:0,duration:.14,ease:`power3.in`},`<+=0.04`).to(t,{alpha:0,duration:.14,ease:`power3.in`},`<`),r=()=>{n.kill(),d(),e()};u&&u.addEventListener(`abort`,r,{once:!0})}),t.pivot.set(n,r),t.x=i,t.y=a,t.scale.set(1,1)}get cellInset(){return null}applyCellQuad(e){let t=this.view;if(e===null){t.scale.set(1,1),t.pivot.set(0,0);return}let n=(Math.hypot(e.x1-e.x0,e.y1-e.y0)+Math.hypot(e.x2-e.x3,e.y2-e.y3))/2,r=Math.hypot((e.x3+e.x2)/2-(e.x0+e.x1)/2,(e.y3+e.y2)/2-(e.y0+e.y1)/2),i=e.width>0&&e.height>0?Math.min(n/e.width,r/e.height):1,a=(e.x0+e.x1+e.x2+e.x3)/4,o=(e.y0+e.y1+e.y2+e.y3)/4;t.scale.set(i,i),t.pivot.set(e.x+e.width/2-a/i,e.y+e.height/2-o/i)}onReelSpinStart(e){}onReelSpinEnd(){}onReelAnticipationStart(){}onReelLanded(){}};Object.defineProperty(exports,`n`,{enumerable:!0,get:function(){return t}}),Object.defineProperty(exports,`t`,{enumerable:!0,get:function(){return n}});
2
+ //# sourceMappingURL=ReelSymbol-0kVAZr4S.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ReelSymbol-0kVAZr4S.cjs","names":[],"sources":["../src/utils/gsap.ts","../src/symbols/ReelSymbol.ts"],"sourcesContent":["import { gsap as resolvedGsap } from 'gsap';\n\n/** The gsap namespace type, as the engine passes it around. */\nexport type Gsap = typeof resolvedGsap;\n\n/**\n * The gsap instance resolved at lib-load time. Used by any `ReelSet` whose\n * builder did not call `.gsap(...)`, and by any symbol not yet bound to a\n * set.\n *\n * **Why the engine passes gsap around at all:** under tools that resolve\n * modules through symlinked workspaces (vite + a locally-linked pixi-reels,\n * pnpm dev setups, esbuild plugin chains), the gsap import inside the lib's\n * compiled `dist/index.js` and the gsap import in the consumer's source can\n * resolve to *different module instances*. each with its own root timeline.\n * The consumer drives one, the lib's tweens live on the other, and reels\n * stall at progress 0. `ReelSetBuilder.gsap(myGsap)` hands the engine the\n * consumer's instance so both live on the same timeline.\n *\n * Held PER REEL SET, not process-wide: two sets on one stage can be driven\n * by different gsap instances, which is what a composed stage (a banner reel\n * above a main grid, a bonus board beside it) needs.\n */\nexport const DEFAULT_GSAP: Gsap = resolvedGsap;\n","import { Container } from 'pixi.js';\nimport type { Disposable } from '../utils/Disposable.js';\nimport type { ReelCellInset, ReelCellQuad } from '../config/types.js';\nimport { DEFAULT_GSAP, type Gsap } from '../utils/gsap.js';\n\n/**\n * One visible cell on a reel. the thing that actually draws.\n *\n * `ReelSymbol` is the abstract base class. Subclass it to pick a rendering\n * technology (`SpriteSymbol`, `AnimatedSpriteSymbol`, `SpineSymbol`, or a\n * custom class of your own). The reel set pools instances aggressively:\n * one instance is reused many times as it scrolls off one identity and on\n * to another, so implementations must never assume \"I was just created\".\n *\n * Required lifecycle hooks:\n *\n * - `onActivate(symbolId)`. the pool just handed me a new identity. Swap\n * texture, restart animations, bring myself out of any \"ended\" pose.\n * - `onDeactivate()`. I am about to be pooled. Pause animations, clear\n * listeners, leave myself in a clean state for the next activation.\n * - `playWin()`. the spotlight is celebrating me. Return a promise that\n * resolves when the one-shot animation is done.\n * - `stopAnimation()`. spotlight is over, return to idle.\n * - `resize(w, h)`. the reel's cell size changed (on every symbol swap).\n * Store the dimensions and reposition internal children. Forgetting\n * this is the single most common \"why do my symbols scatter\" bug.\n *\n * ```\n * create → activate(symbolId) → [playWin / stopAnimation]\n * → deactivate\n * → activate(newId) → ...\n * ```\n *\n * There's no hidden GC. Hold resources? Override `onDestroy()`.\n */\nexport abstract class ReelSymbol implements Disposable {\n /** The PixiJS container that holds this symbol's visual. */\n public readonly view: Container;\n\n private _symbolId: string = '';\n private _isDestroyed = false;\n\n private _gsap: Gsap = DEFAULT_GSAP;\n private _mainAxis: 'x' | 'y' = 'y';\n\n constructor() {\n this.view = new Container();\n }\n\n /**\n * The gsap instance this symbol should animate on. Use it instead of\n * importing `gsap` in a subclass: under a symlinked-workspace module\n * resolution your import and the engine's can be different instances, and\n * only this one is on the timeline the reel set actually drives.\n *\n * Bound to the owning set by `SymbolFactory`; falls back to the instance\n * resolved at lib-load time for a symbol built outside a set.\n */\n protected get gsap(): Gsap {\n return this._gsap;\n }\n\n /**\n * @internal. Called by `SymbolFactory` when the symbol is created, so a\n * pooled symbol animates on its own set's gsap rather than whichever set\n * happened to build last.\n */\n bindGsap(instance: Gsap): void {\n this._gsap = instance;\n }\n\n /**\n * The screen axis the owning set's strips travel along: `'y'` for a\n * vertical set, `'x'` for a horizontal one.\n *\n * Symbols are otherwise orientation-agnostic - `resize(width, height)` is\n * screen-space and always will be. This exists for the few effects that\n * genuinely follow travel, motion blur being the one in the box.\n */\n protected get mainAxis(): 'x' | 'y' {\n return this._mainAxis;\n }\n\n /** @internal. Bound by `SymbolFactory` from the set's orientation. */\n bindMainAxis(prop: 'x' | 'y'): void {\n this._mainAxis = prop;\n }\n\n get symbolId(): string {\n return this._symbolId;\n }\n\n get isDestroyed(): boolean {\n return this._isDestroyed;\n }\n\n /**\n * Activate the symbol with a new identity. Called when the symbol enters\n * the visible reel or is recycled from the pool. Resets container\n * transform / filter state for parity with deactivate().\n */\n activate(symbolId: string): void {\n this._symbolId = symbolId;\n this.view.visible = true;\n this.view.alpha = 1;\n this.view.scale.set(1, 1);\n this.view.rotation = 0;\n this.view.filters = null;\n this.view.zIndex = 0;\n this.onActivate(symbolId);\n }\n\n /**\n * Deactivate the symbol before returning it to the pool. Stops\n * animations, hides the view, and resets container transform / filter\n * state so subclass decorations don't leak across recycles.\n */\n deactivate(): void {\n this.stopAnimation();\n this.onDeactivate();\n this._symbolId = '';\n this.view.visible = false;\n this.view.alpha = 1;\n this.view.scale.set(1, 1);\n this.view.rotation = 0;\n this.view.filters = null;\n this.view.zIndex = 0;\n }\n\n /** Pool reset. aliases deactivate. */\n reset(): void {\n this.deactivate();\n }\n\n destroy(): void {\n if (this._isDestroyed) return;\n this.stopAnimation();\n this.onDeactivate();\n this.onDestroy();\n if (!this.view.destroyed) this.view.destroy({ children: true });\n this._isDestroyed = true;\n }\n\n /** Subclass hook: set up visuals for the given symbolId. */\n protected abstract onActivate(symbolId: string): void;\n\n /** Subclass hook: clean up visuals. */\n protected abstract onDeactivate(): void;\n\n /** Subclass hook: additional cleanup on destroy. */\n protected onDestroy(): void {}\n\n /** Play the win/highlight animation for this symbol. Resolves when complete. */\n abstract playWin(): Promise<void>;\n\n /** Immediately stop any running animation and return to idle. */\n abstract stopAnimation(): void;\n\n /** Resize the symbol's visual to fit the given dimensions. */\n abstract resize(width: number, height: number): void;\n\n /**\n * Play the cascade-destruction animation for this symbol. Called by\n * consumers (typically via `reelSet.destroySymbols(...)`) to disintegrate\n * a winning cell before the next cascade refill drops fresh symbols in.\n *\n * Override in subclasses for art-appropriate destruction, e.g. a Spine\n * symbol can play its `disintegration` track here, or a sprite symbol can\n * swap to a shatter atlas. The promise must resolve when the symbol is no\n * longer visible.\n *\n * Default: a snappy \"poof\" centered on the symbol's bounds regardless of\n * the view's anchor. Tiny anticipation pop (~60 ms) then a fast implode to\n * `scale: 0` + `alpha: 0` (~140 ms), ~200 ms total, no rotation. Reads\n * cleanly under win-cluster pacing without competing with the win\n * presenter. The view is left at `alpha: 0` (destroyed); position / pivot\n * are restored so pool reuse via `_replaceSymbol`'s same-id fast path\n * doesn't inherit a stale pivot offset.\n *\n * `opts.delay`. seconds to wait before the animation starts. Use to\n * stagger a cluster of winners (e.g. `i * 0.015`).\n * `opts.signal`. abort signal. If aborted (now or mid-animation), the\n * tween is killed and the view is snapped to its destroyed pose\n * (`alpha: 0`, transform restored). The promise resolves normally. abort\n * means \"skip to the end,\" not \"fail\". Subclasses that override this\n * method MUST honor the signal or document why they can't (e.g. a Spine\n * `disintegration` track is uninterruptible).\n */\n async playDestroy(opts?: { delay?: number; signal?: AbortSignal }): Promise<void> {\n const view = this.view;\n // Capture original transform so pool reuse sees a clean state.\n const originalPivotX = view.pivot.x;\n const originalPivotY = view.pivot.y;\n const originalX = view.x;\n const originalY = view.y;\n\n // Pivot to bounds-center so the scale collapses around the visual\n // centre instead of the view's (0,0) corner. and compensate position\n // so the symbol doesn't visibly jump when the pivot moves.\n const bounds = view.getLocalBounds();\n const cx = bounds.x + bounds.width / 2;\n const cy = bounds.y + bounds.height / 2;\n // Moving the pivot moves the view by `delta * scale`, not by `delta`: a\n // container renders a local point at `position + (point - pivot) * scale`.\n // Scale is 1 on a plain reel, which is why dropping it went unnoticed, but\n // a curved reel scales every cell and the symbol would jump on destroy.\n view.pivot.set(cx, cy);\n view.x = originalX + (cx - originalPivotX) * view.scale.x;\n view.y = originalY + (cy - originalPivotY) * view.scale.y;\n\n const delay = opts?.delay ?? 0;\n const signal = opts?.signal;\n\n const snapDestroyed = (): void => {\n view.alpha = 0;\n view.scale.set(0, 0);\n };\n\n // Pre-abort: skip the tween entirely and snap to the destroyed pose.\n if (signal?.aborted) {\n snapDestroyed();\n view.pivot.set(originalPivotX, originalPivotY);\n view.x = originalX;\n view.y = originalY;\n view.scale.set(1, 1);\n view.alpha = 0;\n return;\n }\n\n await new Promise<void>((resolve) => {\n const tl = this.gsap\n .timeline({ onComplete: () => {\n if (signal) signal.removeEventListener('abort', onAbort);\n resolve();\n }, delay })\n // Brief anticipation pop. small upscale, ~60 ms, with overshoot\n // so the implode reads as a release. No rotation.\n .to(view.scale, { x: 1.1, y: 1.1, duration: 0.06, ease: 'back.out(2.5)' })\n // Snap implode. scale -> 0 + alpha -> 0 together, snappy ease-in\n // so the symbol collapses into the cell centre and is gone.\n .to(view.scale, { x: 0, y: 0, duration: 0.14, ease: 'power3.in' }, '<+=0.04')\n .to(view, { alpha: 0, duration: 0.14, ease: 'power3.in' }, '<');\n\n const onAbort = (): void => {\n tl.kill();\n snapDestroyed();\n resolve();\n };\n if (signal) signal.addEventListener('abort', onAbort, { once: true });\n });\n\n // Restore transform. alpha stays 0 (the symbol IS destroyed). Scale\n // restored to 1 so pool reuse via `_replaceSymbol`'s same-id fast path\n // doesn't inherit a stale 0× scale; _replaceSymbol also resets scale\n // explicitly but a defensive restore here makes the destroyed cell\n // observably \"ready to be re-skinned\" between calls.\n view.pivot.set(originalPivotX, originalPivotY);\n view.x = originalX;\n view.y = originalY;\n view.scale.set(1, 1);\n }\n\n /**\n * The owning reel is curved: render into this projected quad instead of the\n * flat cell rectangle. `null` means the reel is flat again.\n *\n * The quad's corners are SCREEN-space and local to this view's own origin,\n * clockwise from top-left, and `width` / `height` give the flat cell box the\n * quad replaces. Called on every frame of motion and on every placement, so\n * it must be cheap and idempotent.\n *\n * **The default is an approximation.** A `Container` transform is affine, so\n * it cannot express a trapezoid; the base class fits the quad as closely as\n * an affine transform can - a UNIFORM scale about the quad's centre. Uniform\n * on purpose: symbol art is usually smaller than its cell and has a shape a\n * player recognises, and squashing one axis turns a `7` into a squashed `7`\n * rather than a `7` seen at an angle.\n *\n * Override this to render the real perspective. {@link SpriteSymbol} and\n * {@link AnimatedSpriteSymbol} do, by drawing their texture through a\n * `PerspectiveMesh`, which costs no extra render pass because the content is\n * already a texture. A symbol whose content is an arbitrary subtree (Spine, a\n * composite of sprites and text) cannot do that without rendering itself to a\n * texture every frame, so it keeps the affine fit.\n *\n * Whatever you do here, do NOT move `view.position`: the reel reads that\n * coordinate back to work out which slot this symbol is in.\n */\n /**\n * The part of its cell this symbol's art actually covers, or `null` (the\n * default) for \"all of it\".\n *\n * Slot art is usually smaller than its cell - a trimmed atlas frame is a\n * shape floating in a much bigger transparent box - and the reel needs to\n * know that to project the rectangle the art is really in. Overriding this\n * is what stops a small symbol being inflated to the cell's edges and given\n * the cell's keystone instead of its own, milder one.\n *\n * Read once per projection, so it may change with the symbol's identity.\n */\n get cellInset(): ReelCellInset | null {\n return null;\n }\n\n applyCellQuad(quad: ReelCellQuad | null): void {\n const view = this.view;\n if (quad === null) {\n view.scale.set(1, 1);\n view.pivot.set(0, 0);\n return;\n }\n // Measure the trapezoid: the mean of its two parallel edges, and the\n // distance between their midpoints.\n const nearWidth = Math.hypot(quad.x1 - quad.x0, quad.y1 - quad.y0);\n const farWidth = Math.hypot(quad.x2 - quad.x3, quad.y2 - quad.y3);\n const across = (nearWidth + farWidth) / 2;\n const along = Math.hypot(\n (quad.x3 + quad.x2) / 2 - (quad.x0 + quad.x1) / 2,\n (quad.y3 + quad.y2) / 2 - (quad.y0 + quad.y1) / 2,\n );\n // CONTAIN, not cover. A quad in the middle of the window is TALLER than\n // the flat cell (that is the drum magnifying what faces you), so a scale\n // picked to fill it would also make the symbol wider than its column and\n // overlap its neighbours - very visible on art that fills its cell\n // edge-to-edge. Taking the smaller ratio keeps every symbol inside its own\n // projected footprint at the cost of a little slack on one axis.\n const scale =\n quad.width > 0 && quad.height > 0\n ? Math.min(across / quad.width, along / quad.height)\n : 1;\n\n const cx = (quad.x0 + quad.x1 + quad.x2 + quad.x3) / 4;\n const cy = (quad.y0 + quad.y1 + quad.y2 + quad.y3) / 4;\n view.scale.set(scale, scale);\n // A container renders a local point at `position + (point - pivot) * scale`\n // and `position` must not move, so putting the flat box's centre on the\n // quad's centre has to be paid for entirely out of the pivot.\n view.pivot.set(\n quad.x + quad.width / 2 - cx / scale,\n quad.y + quad.height / 2 - cy / scale,\n );\n }\n\n /**\n * Lifecycle hook: the owning reel is spinning.\n * Default: no-op. Override (e.g. SpineReelSymbol.autoPlayBlur,\n * StaticSpinSymbol) to swap to a spin presentation automatically.\n *\n * Fired on every strip symbol (visible AND buffer cells) when the reel\n * enters the spin phase, and again with `joinedMidSpin: true` on each\n * symbol freshly installed while the reel is already spinning (pool\n * recycling wipes symbol state, so a wrapped-in symbol can't know the\n * reel is moving without this). Implementations MUST be idempotent.\n * the same instance can be notified more than once per spin.\n *\n * @param joinedMidSpin true when this symbol was installed into a reel\n * already at speed (skip start-of-spin transitions like blur ramps).\n */\n onReelSpinStart(joinedMidSpin?: boolean): void {}\n\n /**\n * Lifecycle hook: the owning reel is about to stop (just before bounce).\n * Default: no-op.\n */\n onReelSpinEnd(): void {}\n\n /**\n * Lifecycle hook: the owning reel entered its anticipation (tease) phase.\n * it is still spinning, but slowed enough that the strip is readable.\n * Spin presentations that obscure symbols (blur textures, smear\n * animations) should relax so the player can follow the tease. Also\n * fired on symbols installed while the reel is anticipating.\n * Implementations MUST be idempotent. Default: no-op.\n */\n onReelAnticipationStart(): void {}\n\n /**\n * Lifecycle hook: the owning reel has landed on its final symbols.\n * Default: no-op. Override (e.g. SpineReelSymbol.autoPlayLanding) to fire\n * a landing animation concurrently with the bounce.\n */\n onReelLanded(): void {}\n}\n"],"mappings":"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"}
@@ -43,7 +43,7 @@ var n = t, r = class {
43
43
  onDestroy() {}
44
44
  async playDestroy(e) {
45
45
  let t = this.view, n = t.pivot.x, r = t.pivot.y, i = t.x, a = t.y, o = t.getLocalBounds(), s = o.x + o.width / 2, c = o.y + o.height / 2;
46
- t.pivot.set(s, c), t.x = i + (s - n), t.y = a + (c - r);
46
+ t.pivot.set(s, c), t.x = i + (s - n) * t.scale.x, t.y = a + (c - r) * t.scale.y;
47
47
  let l = e?.delay ?? 0, u = e?.signal, d = () => {
48
48
  t.alpha = 0, t.scale.set(0, 0);
49
49
  };
@@ -77,6 +77,18 @@ var n = t, r = class {
77
77
  u && u.addEventListener("abort", r, { once: !0 });
78
78
  }), t.pivot.set(n, r), t.x = i, t.y = a, t.scale.set(1, 1);
79
79
  }
80
+ get cellInset() {
81
+ return null;
82
+ }
83
+ applyCellQuad(e) {
84
+ let t = this.view;
85
+ if (e === null) {
86
+ t.scale.set(1, 1), t.pivot.set(0, 0);
87
+ return;
88
+ }
89
+ let n = (Math.hypot(e.x1 - e.x0, e.y1 - e.y0) + Math.hypot(e.x2 - e.x3, e.y2 - e.y3)) / 2, r = Math.hypot((e.x3 + e.x2) / 2 - (e.x0 + e.x1) / 2, (e.y3 + e.y2) / 2 - (e.y0 + e.y1) / 2), i = e.width > 0 && e.height > 0 ? Math.min(n / e.width, r / e.height) : 1, a = (e.x0 + e.x1 + e.x2 + e.x3) / 4, o = (e.y0 + e.y1 + e.y2 + e.y3) / 4;
90
+ t.scale.set(i, i), t.pivot.set(e.x + e.width / 2 - a / i, e.y + e.height / 2 - o / i);
91
+ }
80
92
  onReelSpinStart(e) {}
81
93
  onReelSpinEnd() {}
82
94
  onReelAnticipationStart() {}
@@ -85,4 +97,4 @@ var n = t, r = class {
85
97
  //#endregion
86
98
  export { n, r as t };
87
99
 
88
- //# sourceMappingURL=ReelSymbol-Da6FHDO-.js.map
100
+ //# sourceMappingURL=ReelSymbol-CRXD3IO3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ReelSymbol-CRXD3IO3.js","names":[],"sources":["../src/utils/gsap.ts","../src/symbols/ReelSymbol.ts"],"sourcesContent":["import { gsap as resolvedGsap } from 'gsap';\n\n/** The gsap namespace type, as the engine passes it around. */\nexport type Gsap = typeof resolvedGsap;\n\n/**\n * The gsap instance resolved at lib-load time. Used by any `ReelSet` whose\n * builder did not call `.gsap(...)`, and by any symbol not yet bound to a\n * set.\n *\n * **Why the engine passes gsap around at all:** under tools that resolve\n * modules through symlinked workspaces (vite + a locally-linked pixi-reels,\n * pnpm dev setups, esbuild plugin chains), the gsap import inside the lib's\n * compiled `dist/index.js` and the gsap import in the consumer's source can\n * resolve to *different module instances*. each with its own root timeline.\n * The consumer drives one, the lib's tweens live on the other, and reels\n * stall at progress 0. `ReelSetBuilder.gsap(myGsap)` hands the engine the\n * consumer's instance so both live on the same timeline.\n *\n * Held PER REEL SET, not process-wide: two sets on one stage can be driven\n * by different gsap instances, which is what a composed stage (a banner reel\n * above a main grid, a bonus board beside it) needs.\n */\nexport const DEFAULT_GSAP: Gsap = resolvedGsap;\n","import { Container } from 'pixi.js';\nimport type { Disposable } from '../utils/Disposable.js';\nimport type { ReelCellInset, ReelCellQuad } from '../config/types.js';\nimport { DEFAULT_GSAP, type Gsap } from '../utils/gsap.js';\n\n/**\n * One visible cell on a reel. the thing that actually draws.\n *\n * `ReelSymbol` is the abstract base class. Subclass it to pick a rendering\n * technology (`SpriteSymbol`, `AnimatedSpriteSymbol`, `SpineSymbol`, or a\n * custom class of your own). The reel set pools instances aggressively:\n * one instance is reused many times as it scrolls off one identity and on\n * to another, so implementations must never assume \"I was just created\".\n *\n * Required lifecycle hooks:\n *\n * - `onActivate(symbolId)`. the pool just handed me a new identity. Swap\n * texture, restart animations, bring myself out of any \"ended\" pose.\n * - `onDeactivate()`. I am about to be pooled. Pause animations, clear\n * listeners, leave myself in a clean state for the next activation.\n * - `playWin()`. the spotlight is celebrating me. Return a promise that\n * resolves when the one-shot animation is done.\n * - `stopAnimation()`. spotlight is over, return to idle.\n * - `resize(w, h)`. the reel's cell size changed (on every symbol swap).\n * Store the dimensions and reposition internal children. Forgetting\n * this is the single most common \"why do my symbols scatter\" bug.\n *\n * ```\n * create → activate(symbolId) → [playWin / stopAnimation]\n * → deactivate\n * → activate(newId) → ...\n * ```\n *\n * There's no hidden GC. Hold resources? Override `onDestroy()`.\n */\nexport abstract class ReelSymbol implements Disposable {\n /** The PixiJS container that holds this symbol's visual. */\n public readonly view: Container;\n\n private _symbolId: string = '';\n private _isDestroyed = false;\n\n private _gsap: Gsap = DEFAULT_GSAP;\n private _mainAxis: 'x' | 'y' = 'y';\n\n constructor() {\n this.view = new Container();\n }\n\n /**\n * The gsap instance this symbol should animate on. Use it instead of\n * importing `gsap` in a subclass: under a symlinked-workspace module\n * resolution your import and the engine's can be different instances, and\n * only this one is on the timeline the reel set actually drives.\n *\n * Bound to the owning set by `SymbolFactory`; falls back to the instance\n * resolved at lib-load time for a symbol built outside a set.\n */\n protected get gsap(): Gsap {\n return this._gsap;\n }\n\n /**\n * @internal. Called by `SymbolFactory` when the symbol is created, so a\n * pooled symbol animates on its own set's gsap rather than whichever set\n * happened to build last.\n */\n bindGsap(instance: Gsap): void {\n this._gsap = instance;\n }\n\n /**\n * The screen axis the owning set's strips travel along: `'y'` for a\n * vertical set, `'x'` for a horizontal one.\n *\n * Symbols are otherwise orientation-agnostic - `resize(width, height)` is\n * screen-space and always will be. This exists for the few effects that\n * genuinely follow travel, motion blur being the one in the box.\n */\n protected get mainAxis(): 'x' | 'y' {\n return this._mainAxis;\n }\n\n /** @internal. Bound by `SymbolFactory` from the set's orientation. */\n bindMainAxis(prop: 'x' | 'y'): void {\n this._mainAxis = prop;\n }\n\n get symbolId(): string {\n return this._symbolId;\n }\n\n get isDestroyed(): boolean {\n return this._isDestroyed;\n }\n\n /**\n * Activate the symbol with a new identity. Called when the symbol enters\n * the visible reel or is recycled from the pool. Resets container\n * transform / filter state for parity with deactivate().\n */\n activate(symbolId: string): void {\n this._symbolId = symbolId;\n this.view.visible = true;\n this.view.alpha = 1;\n this.view.scale.set(1, 1);\n this.view.rotation = 0;\n this.view.filters = null;\n this.view.zIndex = 0;\n this.onActivate(symbolId);\n }\n\n /**\n * Deactivate the symbol before returning it to the pool. Stops\n * animations, hides the view, and resets container transform / filter\n * state so subclass decorations don't leak across recycles.\n */\n deactivate(): void {\n this.stopAnimation();\n this.onDeactivate();\n this._symbolId = '';\n this.view.visible = false;\n this.view.alpha = 1;\n this.view.scale.set(1, 1);\n this.view.rotation = 0;\n this.view.filters = null;\n this.view.zIndex = 0;\n }\n\n /** Pool reset. aliases deactivate. */\n reset(): void {\n this.deactivate();\n }\n\n destroy(): void {\n if (this._isDestroyed) return;\n this.stopAnimation();\n this.onDeactivate();\n this.onDestroy();\n if (!this.view.destroyed) this.view.destroy({ children: true });\n this._isDestroyed = true;\n }\n\n /** Subclass hook: set up visuals for the given symbolId. */\n protected abstract onActivate(symbolId: string): void;\n\n /** Subclass hook: clean up visuals. */\n protected abstract onDeactivate(): void;\n\n /** Subclass hook: additional cleanup on destroy. */\n protected onDestroy(): void {}\n\n /** Play the win/highlight animation for this symbol. Resolves when complete. */\n abstract playWin(): Promise<void>;\n\n /** Immediately stop any running animation and return to idle. */\n abstract stopAnimation(): void;\n\n /** Resize the symbol's visual to fit the given dimensions. */\n abstract resize(width: number, height: number): void;\n\n /**\n * Play the cascade-destruction animation for this symbol. Called by\n * consumers (typically via `reelSet.destroySymbols(...)`) to disintegrate\n * a winning cell before the next cascade refill drops fresh symbols in.\n *\n * Override in subclasses for art-appropriate destruction, e.g. a Spine\n * symbol can play its `disintegration` track here, or a sprite symbol can\n * swap to a shatter atlas. The promise must resolve when the symbol is no\n * longer visible.\n *\n * Default: a snappy \"poof\" centered on the symbol's bounds regardless of\n * the view's anchor. Tiny anticipation pop (~60 ms) then a fast implode to\n * `scale: 0` + `alpha: 0` (~140 ms), ~200 ms total, no rotation. Reads\n * cleanly under win-cluster pacing without competing with the win\n * presenter. The view is left at `alpha: 0` (destroyed); position / pivot\n * are restored so pool reuse via `_replaceSymbol`'s same-id fast path\n * doesn't inherit a stale pivot offset.\n *\n * `opts.delay`. seconds to wait before the animation starts. Use to\n * stagger a cluster of winners (e.g. `i * 0.015`).\n * `opts.signal`. abort signal. If aborted (now or mid-animation), the\n * tween is killed and the view is snapped to its destroyed pose\n * (`alpha: 0`, transform restored). The promise resolves normally. abort\n * means \"skip to the end,\" not \"fail\". Subclasses that override this\n * method MUST honor the signal or document why they can't (e.g. a Spine\n * `disintegration` track is uninterruptible).\n */\n async playDestroy(opts?: { delay?: number; signal?: AbortSignal }): Promise<void> {\n const view = this.view;\n // Capture original transform so pool reuse sees a clean state.\n const originalPivotX = view.pivot.x;\n const originalPivotY = view.pivot.y;\n const originalX = view.x;\n const originalY = view.y;\n\n // Pivot to bounds-center so the scale collapses around the visual\n // centre instead of the view's (0,0) corner. and compensate position\n // so the symbol doesn't visibly jump when the pivot moves.\n const bounds = view.getLocalBounds();\n const cx = bounds.x + bounds.width / 2;\n const cy = bounds.y + bounds.height / 2;\n // Moving the pivot moves the view by `delta * scale`, not by `delta`: a\n // container renders a local point at `position + (point - pivot) * scale`.\n // Scale is 1 on a plain reel, which is why dropping it went unnoticed, but\n // a curved reel scales every cell and the symbol would jump on destroy.\n view.pivot.set(cx, cy);\n view.x = originalX + (cx - originalPivotX) * view.scale.x;\n view.y = originalY + (cy - originalPivotY) * view.scale.y;\n\n const delay = opts?.delay ?? 0;\n const signal = opts?.signal;\n\n const snapDestroyed = (): void => {\n view.alpha = 0;\n view.scale.set(0, 0);\n };\n\n // Pre-abort: skip the tween entirely and snap to the destroyed pose.\n if (signal?.aborted) {\n snapDestroyed();\n view.pivot.set(originalPivotX, originalPivotY);\n view.x = originalX;\n view.y = originalY;\n view.scale.set(1, 1);\n view.alpha = 0;\n return;\n }\n\n await new Promise<void>((resolve) => {\n const tl = this.gsap\n .timeline({ onComplete: () => {\n if (signal) signal.removeEventListener('abort', onAbort);\n resolve();\n }, delay })\n // Brief anticipation pop. small upscale, ~60 ms, with overshoot\n // so the implode reads as a release. No rotation.\n .to(view.scale, { x: 1.1, y: 1.1, duration: 0.06, ease: 'back.out(2.5)' })\n // Snap implode. scale -> 0 + alpha -> 0 together, snappy ease-in\n // so the symbol collapses into the cell centre and is gone.\n .to(view.scale, { x: 0, y: 0, duration: 0.14, ease: 'power3.in' }, '<+=0.04')\n .to(view, { alpha: 0, duration: 0.14, ease: 'power3.in' }, '<');\n\n const onAbort = (): void => {\n tl.kill();\n snapDestroyed();\n resolve();\n };\n if (signal) signal.addEventListener('abort', onAbort, { once: true });\n });\n\n // Restore transform. alpha stays 0 (the symbol IS destroyed). Scale\n // restored to 1 so pool reuse via `_replaceSymbol`'s same-id fast path\n // doesn't inherit a stale 0× scale; _replaceSymbol also resets scale\n // explicitly but a defensive restore here makes the destroyed cell\n // observably \"ready to be re-skinned\" between calls.\n view.pivot.set(originalPivotX, originalPivotY);\n view.x = originalX;\n view.y = originalY;\n view.scale.set(1, 1);\n }\n\n /**\n * The owning reel is curved: render into this projected quad instead of the\n * flat cell rectangle. `null` means the reel is flat again.\n *\n * The quad's corners are SCREEN-space and local to this view's own origin,\n * clockwise from top-left, and `width` / `height` give the flat cell box the\n * quad replaces. Called on every frame of motion and on every placement, so\n * it must be cheap and idempotent.\n *\n * **The default is an approximation.** A `Container` transform is affine, so\n * it cannot express a trapezoid; the base class fits the quad as closely as\n * an affine transform can - a UNIFORM scale about the quad's centre. Uniform\n * on purpose: symbol art is usually smaller than its cell and has a shape a\n * player recognises, and squashing one axis turns a `7` into a squashed `7`\n * rather than a `7` seen at an angle.\n *\n * Override this to render the real perspective. {@link SpriteSymbol} and\n * {@link AnimatedSpriteSymbol} do, by drawing their texture through a\n * `PerspectiveMesh`, which costs no extra render pass because the content is\n * already a texture. A symbol whose content is an arbitrary subtree (Spine, a\n * composite of sprites and text) cannot do that without rendering itself to a\n * texture every frame, so it keeps the affine fit.\n *\n * Whatever you do here, do NOT move `view.position`: the reel reads that\n * coordinate back to work out which slot this symbol is in.\n */\n /**\n * The part of its cell this symbol's art actually covers, or `null` (the\n * default) for \"all of it\".\n *\n * Slot art is usually smaller than its cell - a trimmed atlas frame is a\n * shape floating in a much bigger transparent box - and the reel needs to\n * know that to project the rectangle the art is really in. Overriding this\n * is what stops a small symbol being inflated to the cell's edges and given\n * the cell's keystone instead of its own, milder one.\n *\n * Read once per projection, so it may change with the symbol's identity.\n */\n get cellInset(): ReelCellInset | null {\n return null;\n }\n\n applyCellQuad(quad: ReelCellQuad | null): void {\n const view = this.view;\n if (quad === null) {\n view.scale.set(1, 1);\n view.pivot.set(0, 0);\n return;\n }\n // Measure the trapezoid: the mean of its two parallel edges, and the\n // distance between their midpoints.\n const nearWidth = Math.hypot(quad.x1 - quad.x0, quad.y1 - quad.y0);\n const farWidth = Math.hypot(quad.x2 - quad.x3, quad.y2 - quad.y3);\n const across = (nearWidth + farWidth) / 2;\n const along = Math.hypot(\n (quad.x3 + quad.x2) / 2 - (quad.x0 + quad.x1) / 2,\n (quad.y3 + quad.y2) / 2 - (quad.y0 + quad.y1) / 2,\n );\n // CONTAIN, not cover. A quad in the middle of the window is TALLER than\n // the flat cell (that is the drum magnifying what faces you), so a scale\n // picked to fill it would also make the symbol wider than its column and\n // overlap its neighbours - very visible on art that fills its cell\n // edge-to-edge. Taking the smaller ratio keeps every symbol inside its own\n // projected footprint at the cost of a little slack on one axis.\n const scale =\n quad.width > 0 && quad.height > 0\n ? Math.min(across / quad.width, along / quad.height)\n : 1;\n\n const cx = (quad.x0 + quad.x1 + quad.x2 + quad.x3) / 4;\n const cy = (quad.y0 + quad.y1 + quad.y2 + quad.y3) / 4;\n view.scale.set(scale, scale);\n // A container renders a local point at `position + (point - pivot) * scale`\n // and `position` must not move, so putting the flat box's centre on the\n // quad's centre has to be paid for entirely out of the pivot.\n view.pivot.set(\n quad.x + quad.width / 2 - cx / scale,\n quad.y + quad.height / 2 - cy / scale,\n );\n }\n\n /**\n * Lifecycle hook: the owning reel is spinning.\n * Default: no-op. Override (e.g. SpineReelSymbol.autoPlayBlur,\n * StaticSpinSymbol) to swap to a spin presentation automatically.\n *\n * Fired on every strip symbol (visible AND buffer cells) when the reel\n * enters the spin phase, and again with `joinedMidSpin: true` on each\n * symbol freshly installed while the reel is already spinning (pool\n * recycling wipes symbol state, so a wrapped-in symbol can't know the\n * reel is moving without this). Implementations MUST be idempotent.\n * the same instance can be notified more than once per spin.\n *\n * @param joinedMidSpin true when this symbol was installed into a reel\n * already at speed (skip start-of-spin transitions like blur ramps).\n */\n onReelSpinStart(joinedMidSpin?: boolean): void {}\n\n /**\n * Lifecycle hook: the owning reel is about to stop (just before bounce).\n * Default: no-op.\n */\n onReelSpinEnd(): void {}\n\n /**\n * Lifecycle hook: the owning reel entered its anticipation (tease) phase.\n * it is still spinning, but slowed enough that the strip is readable.\n * Spin presentations that obscure symbols (blur textures, smear\n * animations) should relax so the player can follow the tease. Also\n * fired on symbols installed while the reel is anticipating.\n * Implementations MUST be idempotent. Default: no-op.\n */\n onReelAnticipationStart(): void {}\n\n /**\n * Lifecycle hook: the owning reel has landed on its final symbols.\n * Default: no-op. Override (e.g. SpineReelSymbol.autoPlayLanding) to fire\n * a landing animation concurrently with the bounce.\n */\n onReelLanded(): void {}\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { t as e } from "./ReelSymbol-Da6FHDO-.js";
1
+ import { t as e } from "./ReelSymbol-CRXD3IO3.js";
2
2
  //#region src/symbols/SpineSymbol.ts
3
3
  var t = null, n = (async () => {
4
4
  try {
@@ -67,4 +67,4 @@ var i = class extends e {
67
67
  //#endregion
68
68
  export { r as n, i as t };
69
69
 
70
- //# sourceMappingURL=SpineSymbol-CzcEUfsp.js.map
70
+ //# sourceMappingURL=SpineSymbol-BApOkJM6.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"SpineSymbol-CzcEUfsp.js","names":[],"sources":["../src/symbols/SpineSymbol.ts"],"sourcesContent":["import { ReelSymbol } from './ReelSymbol.js';\n\n// Spine types imported dynamically. this is an optional peer dependency\nlet SpineClass: any = null;\n\n// Kick off the optional import at module init and keep the promise so callers\n// can wait for it. Without this, constructing a SpineSymbol on a cold start\n// (before the dynamic import resolves) throws the \"not installed\" error even\n// when Spine IS installed - it just hadn't finished loading yet.\nconst spineReady: Promise<void> = (async () => {\n try {\n const spineModule = await import('@esotericsoftware/spine-pixi-v8');\n SpineClass = spineModule.Spine;\n } catch {\n // Spine not available. SpineSymbol will throw on construction.\n }\n})();\n\n/**\n * Resolves once the optional `@esotericsoftware/spine-pixi-v8` import has\n * settled (whether or not it was installed). Await this before constructing\n * `SpineSymbol`s if you build the reel set immediately on app start, so a cold\n * load can't throw the misleading \"not installed\" error.\n */\nexport function whenSpineReady(): Promise<void> {\n return spineReady;\n}\n\nexport interface SpineSymbolOptions {\n /** Map of symbolId → SkeletonData. */\n skeletonDataMap: Record<string, any>;\n /** Default animation name to play in idle. Default: 'idle'. */\n idleAnimation?: string;\n /** Animation name to play on win. Default: 'win'. */\n winAnimation?: string;\n /** Default skin name. Default: 'default'. */\n defaultSkin?: string;\n /**\n * Fail loud on unmapped animations. When `true`, activating or winning with\n * a configured animation name that the skeleton doesn't define throws instead\n * of silently showing nothing (catches typos like `ide` vs `idle`). Default\n * `false` to preserve the lenient behavior. See ADR 011.\n */\n strict?: boolean;\n}\n\n/**\n * Symbol implementation using Spine 2D skeletal animation.\n *\n * Requires `@esotericsoftware/spine-pixi-v8` as an optional peer dependency.\n * If Spine is not installed, constructing a SpineSymbol will throw.\n */\nexport class SpineSymbol extends ReelSymbol {\n private _spine: any = null;\n private _skeletonDataMap: Record<string, any>;\n private _idleAnimation: string;\n private _winAnimation: string;\n private _defaultSkin: string;\n private _winResolve: (() => void) | null = null;\n private _currentSkeletonKey: string = '';\n private _strict: boolean;\n\n constructor(options: SpineSymbolOptions) {\n super();\n if (!SpineClass) {\n throw new Error(\n 'SpineSymbol could not use @esotericsoftware/spine-pixi-v8. Either it is ' +\n 'not installed (npm install @esotericsoftware/spine-pixi-v8), or its ' +\n 'dynamic import has not resolved yet on a cold start — await ' +\n 'whenSpineReady() before constructing SpineSymbols.',\n );\n }\n this._skeletonDataMap = options.skeletonDataMap;\n this._idleAnimation = options.idleAnimation ?? 'idle';\n this._winAnimation = options.winAnimation ?? 'win';\n this._defaultSkin = options.defaultSkin ?? 'default';\n this._strict = options.strict ?? false;\n }\n\n protected onActivate(symbolId: string): void {\n const skeletonData = this._skeletonDataMap[symbolId];\n if (!skeletonData) return;\n\n // Reuse existing spine if same skeleton data\n if (this._currentSkeletonKey !== symbolId) {\n if (this._spine) {\n this.view.removeChild(this._spine);\n this._spine.destroy();\n }\n this._spine = new SpineClass({ skeletonData });\n this.view.addChild(this._spine);\n this._currentSkeletonKey = symbolId;\n }\n\n if (this._spine.skeleton.data.findSkin(this._defaultSkin)) {\n this._spine.skeleton.setSkinByName(this._defaultSkin);\n this._spine.skeleton.setSlotsToSetupPose();\n }\n if (this._spine.skeleton.data.findAnimation(this._idleAnimation)) {\n this._spine.state.setAnimation(0, this._idleAnimation, true);\n } else if (this._strict) {\n throw new Error(\n `SpineSymbol(strict): idle animation '${this._idleAnimation}' not found ` +\n `on skeleton '${symbolId}'.`,\n );\n }\n }\n\n protected onDeactivate(): void {\n if (this._spine) {\n this._spine.state.clearListeners();\n this._spine.state.clearTracks();\n }\n // Settle any pending playWin so awaiters don't hang when the symbol is\n // recycled mid-animation. Mirrors the same fix in SpineReelSymbol.\n if (this._winResolve) {\n const r = this._winResolve;\n this._winResolve = null;\n r();\n }\n }\n\n async playWin(): Promise<void> {\n if (!this._spine) return;\n if (!this._spine.skeleton.data.findAnimation(this._winAnimation)) {\n if (this._strict) {\n throw new Error(\n `SpineSymbol(strict): win animation '${this._winAnimation}' not found ` +\n `on skeleton '${this._currentSkeletonKey}'.`,\n );\n }\n return;\n }\n\n return new Promise<void>((resolve) => {\n this._winResolve = resolve;\n const entry = this._spine.state.setAnimation(0, this._winAnimation, false);\n this._spine.state.addListener({\n complete: (trackEntry: any) => {\n if (trackEntry === entry) {\n this._spine.state.clearListeners();\n if (this._spine.skeleton.data.findAnimation(this._idleAnimation)) {\n this._spine.state.setAnimation(0, this._idleAnimation, true);\n }\n this._winResolve = null;\n resolve();\n }\n },\n });\n });\n }\n\n stopAnimation(): void {\n if (!this._spine) return;\n this._spine.state.clearListeners();\n if (this._spine.skeleton.data.findAnimation(this._idleAnimation)) {\n this._spine.state.setAnimation(0, this._idleAnimation, true);\n }\n if (this._winResolve) {\n this._winResolve();\n this._winResolve = null;\n }\n }\n\n resize(width: number, height: number): void {\n if (!this._spine) return;\n const bounds = this._spine.getBounds();\n if (bounds.width > 0 && bounds.height > 0) {\n this._spine.scale.set(\n width / bounds.width,\n height / bounds.height,\n );\n }\n }\n\n protected override onDestroy(): void {\n if (this._spine) {\n this._spine.state.clearListeners();\n this._spine.destroy();\n this._spine = null;\n }\n if (this._winResolve) {\n const r = this._winResolve;\n this._winResolve = null;\n r();\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"SpineSymbol-BApOkJM6.js","names":[],"sources":["../src/symbols/SpineSymbol.ts"],"sourcesContent":["import { ReelSymbol } from './ReelSymbol.js';\n\n// Spine types imported dynamically. this is an optional peer dependency\nlet SpineClass: any = null;\n\n// Kick off the optional import at module init and keep the promise so callers\n// can wait for it. Without this, constructing a SpineSymbol on a cold start\n// (before the dynamic import resolves) throws the \"not installed\" error even\n// when Spine IS installed - it just hadn't finished loading yet.\nconst spineReady: Promise<void> = (async () => {\n try {\n const spineModule = await import('@esotericsoftware/spine-pixi-v8');\n SpineClass = spineModule.Spine;\n } catch {\n // Spine not available. SpineSymbol will throw on construction.\n }\n})();\n\n/**\n * Resolves once the optional `@esotericsoftware/spine-pixi-v8` import has\n * settled (whether or not it was installed). Await this before constructing\n * `SpineSymbol`s if you build the reel set immediately on app start, so a cold\n * load can't throw the misleading \"not installed\" error.\n */\nexport function whenSpineReady(): Promise<void> {\n return spineReady;\n}\n\nexport interface SpineSymbolOptions {\n /** Map of symbolId → SkeletonData. */\n skeletonDataMap: Record<string, any>;\n /** Default animation name to play in idle. Default: 'idle'. */\n idleAnimation?: string;\n /** Animation name to play on win. Default: 'win'. */\n winAnimation?: string;\n /** Default skin name. Default: 'default'. */\n defaultSkin?: string;\n /**\n * Fail loud on unmapped animations. When `true`, activating or winning with\n * a configured animation name that the skeleton doesn't define throws instead\n * of silently showing nothing (catches typos like `ide` vs `idle`). Default\n * `false` to preserve the lenient behavior. See ADR 011.\n */\n strict?: boolean;\n}\n\n/**\n * Symbol implementation using Spine 2D skeletal animation.\n *\n * Requires `@esotericsoftware/spine-pixi-v8` as an optional peer dependency.\n * If Spine is not installed, constructing a SpineSymbol will throw.\n */\nexport class SpineSymbol extends ReelSymbol {\n private _spine: any = null;\n private _skeletonDataMap: Record<string, any>;\n private _idleAnimation: string;\n private _winAnimation: string;\n private _defaultSkin: string;\n private _winResolve: (() => void) | null = null;\n private _currentSkeletonKey: string = '';\n private _strict: boolean;\n\n constructor(options: SpineSymbolOptions) {\n super();\n if (!SpineClass) {\n throw new Error(\n 'SpineSymbol could not use @esotericsoftware/spine-pixi-v8. Either it is ' +\n 'not installed (npm install @esotericsoftware/spine-pixi-v8), or its ' +\n 'dynamic import has not resolved yet on a cold start — await ' +\n 'whenSpineReady() before constructing SpineSymbols.',\n );\n }\n this._skeletonDataMap = options.skeletonDataMap;\n this._idleAnimation = options.idleAnimation ?? 'idle';\n this._winAnimation = options.winAnimation ?? 'win';\n this._defaultSkin = options.defaultSkin ?? 'default';\n this._strict = options.strict ?? false;\n }\n\n protected onActivate(symbolId: string): void {\n const skeletonData = this._skeletonDataMap[symbolId];\n if (!skeletonData) return;\n\n // Reuse existing spine if same skeleton data\n if (this._currentSkeletonKey !== symbolId) {\n if (this._spine) {\n this.view.removeChild(this._spine);\n this._spine.destroy();\n }\n this._spine = new SpineClass({ skeletonData });\n this.view.addChild(this._spine);\n this._currentSkeletonKey = symbolId;\n }\n\n if (this._spine.skeleton.data.findSkin(this._defaultSkin)) {\n this._spine.skeleton.setSkinByName(this._defaultSkin);\n this._spine.skeleton.setSlotsToSetupPose();\n }\n if (this._spine.skeleton.data.findAnimation(this._idleAnimation)) {\n this._spine.state.setAnimation(0, this._idleAnimation, true);\n } else if (this._strict) {\n throw new Error(\n `SpineSymbol(strict): idle animation '${this._idleAnimation}' not found ` +\n `on skeleton '${symbolId}'.`,\n );\n }\n }\n\n protected onDeactivate(): void {\n if (this._spine) {\n this._spine.state.clearListeners();\n this._spine.state.clearTracks();\n }\n // Settle any pending playWin so awaiters don't hang when the symbol is\n // recycled mid-animation. Mirrors the same fix in SpineReelSymbol.\n if (this._winResolve) {\n const r = this._winResolve;\n this._winResolve = null;\n r();\n }\n }\n\n async playWin(): Promise<void> {\n if (!this._spine) return;\n if (!this._spine.skeleton.data.findAnimation(this._winAnimation)) {\n if (this._strict) {\n throw new Error(\n `SpineSymbol(strict): win animation '${this._winAnimation}' not found ` +\n `on skeleton '${this._currentSkeletonKey}'.`,\n );\n }\n return;\n }\n\n return new Promise<void>((resolve) => {\n this._winResolve = resolve;\n const entry = this._spine.state.setAnimation(0, this._winAnimation, false);\n this._spine.state.addListener({\n complete: (trackEntry: any) => {\n if (trackEntry === entry) {\n this._spine.state.clearListeners();\n if (this._spine.skeleton.data.findAnimation(this._idleAnimation)) {\n this._spine.state.setAnimation(0, this._idleAnimation, true);\n }\n this._winResolve = null;\n resolve();\n }\n },\n });\n });\n }\n\n stopAnimation(): void {\n if (!this._spine) return;\n this._spine.state.clearListeners();\n if (this._spine.skeleton.data.findAnimation(this._idleAnimation)) {\n this._spine.state.setAnimation(0, this._idleAnimation, true);\n }\n if (this._winResolve) {\n this._winResolve();\n this._winResolve = null;\n }\n }\n\n resize(width: number, height: number): void {\n if (!this._spine) return;\n const bounds = this._spine.getBounds();\n if (bounds.width > 0 && bounds.height > 0) {\n this._spine.scale.set(\n width / bounds.width,\n height / bounds.height,\n );\n }\n }\n\n protected override onDestroy(): void {\n if (this._spine) {\n this._spine.state.clearListeners();\n this._spine.destroy();\n this._spine = null;\n }\n if (this._winResolve) {\n const r = this._winResolve;\n this._winResolve = null;\n r();\n }\n }\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- const e=require(`./ReelSymbol-DldUQdRR.cjs`);var t=null,n=(async()=>{try{t=(await import(`@esotericsoftware/spine-pixi-v8`)).Spine}catch{}})();function r(){return n}var i=class extends e.t{_spine=null;_skeletonDataMap;_idleAnimation;_winAnimation;_defaultSkin;_winResolve=null;_currentSkeletonKey=``;_strict;constructor(e){if(super(),!t)throw Error(`SpineSymbol could not use @esotericsoftware/spine-pixi-v8. Either it is not installed (npm install @esotericsoftware/spine-pixi-v8), or its dynamic import has not resolved yet on a cold start — await whenSpineReady() before constructing SpineSymbols.`);this._skeletonDataMap=e.skeletonDataMap,this._idleAnimation=e.idleAnimation??`idle`,this._winAnimation=e.winAnimation??`win`,this._defaultSkin=e.defaultSkin??`default`,this._strict=e.strict??!1}onActivate(e){let n=this._skeletonDataMap[e];if(n){if(this._currentSkeletonKey!==e&&(this._spine&&(this.view.removeChild(this._spine),this._spine.destroy()),this._spine=new t({skeletonData:n}),this.view.addChild(this._spine),this._currentSkeletonKey=e),this._spine.skeleton.data.findSkin(this._defaultSkin)&&(this._spine.skeleton.setSkinByName(this._defaultSkin),this._spine.skeleton.setSlotsToSetupPose()),this._spine.skeleton.data.findAnimation(this._idleAnimation))this._spine.state.setAnimation(0,this._idleAnimation,!0);else if(this._strict)throw Error(`SpineSymbol(strict): idle animation '${this._idleAnimation}' not found on skeleton '${e}'.`)}}onDeactivate(){if(this._spine&&(this._spine.state.clearListeners(),this._spine.state.clearTracks()),this._winResolve){let e=this._winResolve;this._winResolve=null,e()}}async playWin(){if(this._spine){if(!this._spine.skeleton.data.findAnimation(this._winAnimation)){if(this._strict)throw Error(`SpineSymbol(strict): win animation '${this._winAnimation}' not found on skeleton '${this._currentSkeletonKey}'.`);return}return new Promise(e=>{this._winResolve=e;let t=this._spine.state.setAnimation(0,this._winAnimation,!1);this._spine.state.addListener({complete:n=>{n===t&&(this._spine.state.clearListeners(),this._spine.skeleton.data.findAnimation(this._idleAnimation)&&this._spine.state.setAnimation(0,this._idleAnimation,!0),this._winResolve=null,e())}})})}}stopAnimation(){this._spine&&(this._spine.state.clearListeners(),this._spine.skeleton.data.findAnimation(this._idleAnimation)&&this._spine.state.setAnimation(0,this._idleAnimation,!0),this._winResolve&&=(this._winResolve(),null))}resize(e,t){if(!this._spine)return;let n=this._spine.getBounds();n.width>0&&n.height>0&&this._spine.scale.set(e/n.width,t/n.height)}onDestroy(){if(this._spine&&=(this._spine.state.clearListeners(),this._spine.destroy(),null),this._winResolve){let e=this._winResolve;this._winResolve=null,e()}}};Object.defineProperty(exports,`n`,{enumerable:!0,get:function(){return r}}),Object.defineProperty(exports,`t`,{enumerable:!0,get:function(){return i}});
2
- //# sourceMappingURL=SpineSymbol-1c5vjgQu.cjs.map
1
+ const e=require(`./ReelSymbol-0kVAZr4S.cjs`);var t=null,n=(async()=>{try{t=(await import(`@esotericsoftware/spine-pixi-v8`)).Spine}catch{}})();function r(){return n}var i=class extends e.t{_spine=null;_skeletonDataMap;_idleAnimation;_winAnimation;_defaultSkin;_winResolve=null;_currentSkeletonKey=``;_strict;constructor(e){if(super(),!t)throw Error(`SpineSymbol could not use @esotericsoftware/spine-pixi-v8. Either it is not installed (npm install @esotericsoftware/spine-pixi-v8), or its dynamic import has not resolved yet on a cold start — await whenSpineReady() before constructing SpineSymbols.`);this._skeletonDataMap=e.skeletonDataMap,this._idleAnimation=e.idleAnimation??`idle`,this._winAnimation=e.winAnimation??`win`,this._defaultSkin=e.defaultSkin??`default`,this._strict=e.strict??!1}onActivate(e){let n=this._skeletonDataMap[e];if(n){if(this._currentSkeletonKey!==e&&(this._spine&&(this.view.removeChild(this._spine),this._spine.destroy()),this._spine=new t({skeletonData:n}),this.view.addChild(this._spine),this._currentSkeletonKey=e),this._spine.skeleton.data.findSkin(this._defaultSkin)&&(this._spine.skeleton.setSkinByName(this._defaultSkin),this._spine.skeleton.setSlotsToSetupPose()),this._spine.skeleton.data.findAnimation(this._idleAnimation))this._spine.state.setAnimation(0,this._idleAnimation,!0);else if(this._strict)throw Error(`SpineSymbol(strict): idle animation '${this._idleAnimation}' not found on skeleton '${e}'.`)}}onDeactivate(){if(this._spine&&(this._spine.state.clearListeners(),this._spine.state.clearTracks()),this._winResolve){let e=this._winResolve;this._winResolve=null,e()}}async playWin(){if(this._spine){if(!this._spine.skeleton.data.findAnimation(this._winAnimation)){if(this._strict)throw Error(`SpineSymbol(strict): win animation '${this._winAnimation}' not found on skeleton '${this._currentSkeletonKey}'.`);return}return new Promise(e=>{this._winResolve=e;let t=this._spine.state.setAnimation(0,this._winAnimation,!1);this._spine.state.addListener({complete:n=>{n===t&&(this._spine.state.clearListeners(),this._spine.skeleton.data.findAnimation(this._idleAnimation)&&this._spine.state.setAnimation(0,this._idleAnimation,!0),this._winResolve=null,e())}})})}}stopAnimation(){this._spine&&(this._spine.state.clearListeners(),this._spine.skeleton.data.findAnimation(this._idleAnimation)&&this._spine.state.setAnimation(0,this._idleAnimation,!0),this._winResolve&&=(this._winResolve(),null))}resize(e,t){if(!this._spine)return;let n=this._spine.getBounds();n.width>0&&n.height>0&&this._spine.scale.set(e/n.width,t/n.height)}onDestroy(){if(this._spine&&=(this._spine.state.clearListeners(),this._spine.destroy(),null),this._winResolve){let e=this._winResolve;this._winResolve=null,e()}}};Object.defineProperty(exports,`n`,{enumerable:!0,get:function(){return r}}),Object.defineProperty(exports,`t`,{enumerable:!0,get:function(){return i}});
2
+ //# sourceMappingURL=SpineSymbol-Fe0Fz1JW.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"SpineSymbol-1c5vjgQu.cjs","names":[],"sources":["../src/symbols/SpineSymbol.ts"],"sourcesContent":["import { ReelSymbol } from './ReelSymbol.js';\n\n// Spine types imported dynamically. this is an optional peer dependency\nlet SpineClass: any = null;\n\n// Kick off the optional import at module init and keep the promise so callers\n// can wait for it. Without this, constructing a SpineSymbol on a cold start\n// (before the dynamic import resolves) throws the \"not installed\" error even\n// when Spine IS installed - it just hadn't finished loading yet.\nconst spineReady: Promise<void> = (async () => {\n try {\n const spineModule = await import('@esotericsoftware/spine-pixi-v8');\n SpineClass = spineModule.Spine;\n } catch {\n // Spine not available. SpineSymbol will throw on construction.\n }\n})();\n\n/**\n * Resolves once the optional `@esotericsoftware/spine-pixi-v8` import has\n * settled (whether or not it was installed). Await this before constructing\n * `SpineSymbol`s if you build the reel set immediately on app start, so a cold\n * load can't throw the misleading \"not installed\" error.\n */\nexport function whenSpineReady(): Promise<void> {\n return spineReady;\n}\n\nexport interface SpineSymbolOptions {\n /** Map of symbolId → SkeletonData. */\n skeletonDataMap: Record<string, any>;\n /** Default animation name to play in idle. Default: 'idle'. */\n idleAnimation?: string;\n /** Animation name to play on win. Default: 'win'. */\n winAnimation?: string;\n /** Default skin name. Default: 'default'. */\n defaultSkin?: string;\n /**\n * Fail loud on unmapped animations. When `true`, activating or winning with\n * a configured animation name that the skeleton doesn't define throws instead\n * of silently showing nothing (catches typos like `ide` vs `idle`). Default\n * `false` to preserve the lenient behavior. See ADR 011.\n */\n strict?: boolean;\n}\n\n/**\n * Symbol implementation using Spine 2D skeletal animation.\n *\n * Requires `@esotericsoftware/spine-pixi-v8` as an optional peer dependency.\n * If Spine is not installed, constructing a SpineSymbol will throw.\n */\nexport class SpineSymbol extends ReelSymbol {\n private _spine: any = null;\n private _skeletonDataMap: Record<string, any>;\n private _idleAnimation: string;\n private _winAnimation: string;\n private _defaultSkin: string;\n private _winResolve: (() => void) | null = null;\n private _currentSkeletonKey: string = '';\n private _strict: boolean;\n\n constructor(options: SpineSymbolOptions) {\n super();\n if (!SpineClass) {\n throw new Error(\n 'SpineSymbol could not use @esotericsoftware/spine-pixi-v8. Either it is ' +\n 'not installed (npm install @esotericsoftware/spine-pixi-v8), or its ' +\n 'dynamic import has not resolved yet on a cold start — await ' +\n 'whenSpineReady() before constructing SpineSymbols.',\n );\n }\n this._skeletonDataMap = options.skeletonDataMap;\n this._idleAnimation = options.idleAnimation ?? 'idle';\n this._winAnimation = options.winAnimation ?? 'win';\n this._defaultSkin = options.defaultSkin ?? 'default';\n this._strict = options.strict ?? false;\n }\n\n protected onActivate(symbolId: string): void {\n const skeletonData = this._skeletonDataMap[symbolId];\n if (!skeletonData) return;\n\n // Reuse existing spine if same skeleton data\n if (this._currentSkeletonKey !== symbolId) {\n if (this._spine) {\n this.view.removeChild(this._spine);\n this._spine.destroy();\n }\n this._spine = new SpineClass({ skeletonData });\n this.view.addChild(this._spine);\n this._currentSkeletonKey = symbolId;\n }\n\n if (this._spine.skeleton.data.findSkin(this._defaultSkin)) {\n this._spine.skeleton.setSkinByName(this._defaultSkin);\n this._spine.skeleton.setSlotsToSetupPose();\n }\n if (this._spine.skeleton.data.findAnimation(this._idleAnimation)) {\n this._spine.state.setAnimation(0, this._idleAnimation, true);\n } else if (this._strict) {\n throw new Error(\n `SpineSymbol(strict): idle animation '${this._idleAnimation}' not found ` +\n `on skeleton '${symbolId}'.`,\n );\n }\n }\n\n protected onDeactivate(): void {\n if (this._spine) {\n this._spine.state.clearListeners();\n this._spine.state.clearTracks();\n }\n // Settle any pending playWin so awaiters don't hang when the symbol is\n // recycled mid-animation. Mirrors the same fix in SpineReelSymbol.\n if (this._winResolve) {\n const r = this._winResolve;\n this._winResolve = null;\n r();\n }\n }\n\n async playWin(): Promise<void> {\n if (!this._spine) return;\n if (!this._spine.skeleton.data.findAnimation(this._winAnimation)) {\n if (this._strict) {\n throw new Error(\n `SpineSymbol(strict): win animation '${this._winAnimation}' not found ` +\n `on skeleton '${this._currentSkeletonKey}'.`,\n );\n }\n return;\n }\n\n return new Promise<void>((resolve) => {\n this._winResolve = resolve;\n const entry = this._spine.state.setAnimation(0, this._winAnimation, false);\n this._spine.state.addListener({\n complete: (trackEntry: any) => {\n if (trackEntry === entry) {\n this._spine.state.clearListeners();\n if (this._spine.skeleton.data.findAnimation(this._idleAnimation)) {\n this._spine.state.setAnimation(0, this._idleAnimation, true);\n }\n this._winResolve = null;\n resolve();\n }\n },\n });\n });\n }\n\n stopAnimation(): void {\n if (!this._spine) return;\n this._spine.state.clearListeners();\n if (this._spine.skeleton.data.findAnimation(this._idleAnimation)) {\n this._spine.state.setAnimation(0, this._idleAnimation, true);\n }\n if (this._winResolve) {\n this._winResolve();\n this._winResolve = null;\n }\n }\n\n resize(width: number, height: number): void {\n if (!this._spine) return;\n const bounds = this._spine.getBounds();\n if (bounds.width > 0 && bounds.height > 0) {\n this._spine.scale.set(\n width / bounds.width,\n height / bounds.height,\n );\n }\n }\n\n protected override onDestroy(): void {\n if (this._spine) {\n this._spine.state.clearListeners();\n this._spine.destroy();\n this._spine = null;\n }\n if (this._winResolve) {\n const r = this._winResolve;\n this._winResolve = null;\n r();\n }\n }\n}\n"],"mappings":"6CAGA,IAAI,EAAkB,KAMhB,GAA6B,SAAY,CAC7C,GAAI,CAEF,GADoB,MAAM,OAAO,oCACR,WACnB,MAGN,CAQJ,SAAgB,GAAgC,CAC9C,OAAO,EA2BT,IAAa,EAAb,cAAiC,EAAA,CAAW,CAC1C,OAAsB,KACtB,iBACA,eACA,cACA,aACA,YAA2C,KAC3C,oBAAsC,GACtC,QAEA,YAAY,EAA6B,CAEvC,GADA,OAAO,CACH,CAAC,EACH,MAAU,MACR,6PAID,CAEH,KAAK,iBAAmB,EAAQ,gBAChC,KAAK,eAAiB,EAAQ,eAAiB,OAC/C,KAAK,cAAgB,EAAQ,cAAgB,MAC7C,KAAK,aAAe,EAAQ,aAAe,UAC3C,KAAK,QAAU,EAAQ,QAAU,GAGnC,WAAqB,EAAwB,CAC3C,IAAM,EAAe,KAAK,iBAAiB,GACtC,KAiBL,IAdI,KAAK,sBAAwB,IAC3B,KAAK,SACP,KAAK,KAAK,YAAY,KAAK,OAAO,CAClC,KAAK,OAAO,SAAS,EAEvB,KAAK,OAAS,IAAI,EAAW,CAAE,eAAc,CAAC,CAC9C,KAAK,KAAK,SAAS,KAAK,OAAO,CAC/B,KAAK,oBAAsB,GAGzB,KAAK,OAAO,SAAS,KAAK,SAAS,KAAK,aAAa,GACvD,KAAK,OAAO,SAAS,cAAc,KAAK,aAAa,CACrD,KAAK,OAAO,SAAS,qBAAqB,EAExC,KAAK,OAAO,SAAS,KAAK,cAAc,KAAK,eAAe,CAC9D,KAAK,OAAO,MAAM,aAAa,EAAG,KAAK,eAAgB,GAAK,SACnD,KAAK,QACd,MAAU,MACR,wCAAwC,KAAK,eAAe,2BAC5C,EAAS,IAC1B,EAIL,cAA+B,CAO7B,GANI,KAAK,SACP,KAAK,OAAO,MAAM,gBAAgB,CAClC,KAAK,OAAO,MAAM,aAAa,EAI7B,KAAK,YAAa,CACpB,IAAM,EAAI,KAAK,YACf,KAAK,YAAc,KACnB,GAAG,EAIP,MAAM,SAAyB,CACxB,QAAK,OACV,IAAI,CAAC,KAAK,OAAO,SAAS,KAAK,cAAc,KAAK,cAAc,CAAE,CAChE,GAAI,KAAK,QACP,MAAU,MACR,uCAAuC,KAAK,cAAc,2BAC1C,KAAK,oBAAoB,IAC1C,CAEH,OAGF,OAAO,IAAI,QAAe,GAAY,CACpC,KAAK,YAAc,EACnB,IAAM,EAAQ,KAAK,OAAO,MAAM,aAAa,EAAG,KAAK,cAAe,GAAM,CAC1E,KAAK,OAAO,MAAM,YAAY,CAC5B,SAAW,GAAoB,CACzB,IAAe,IACjB,KAAK,OAAO,MAAM,gBAAgB,CAC9B,KAAK,OAAO,SAAS,KAAK,cAAc,KAAK,eAAe,EAC9D,KAAK,OAAO,MAAM,aAAa,EAAG,KAAK,eAAgB,GAAK,CAE9D,KAAK,YAAc,KACnB,GAAS,GAGd,CAAC,EACF,EAGJ,eAAsB,CACf,KAAK,SACV,KAAK,OAAO,MAAM,gBAAgB,CAC9B,KAAK,OAAO,SAAS,KAAK,cAAc,KAAK,eAAe,EAC9D,KAAK,OAAO,MAAM,aAAa,EAAG,KAAK,eAAgB,GAAK,CAE9D,AAEE,KAAK,eADL,KAAK,aAAa,CACC,OAIvB,OAAO,EAAe,EAAsB,CAC1C,GAAI,CAAC,KAAK,OAAQ,OAClB,IAAM,EAAS,KAAK,OAAO,WAAW,CAClC,EAAO,MAAQ,GAAK,EAAO,OAAS,GACtC,KAAK,OAAO,MAAM,IAChB,EAAQ,EAAO,MACf,EAAS,EAAO,OACjB,CAIL,WAAqC,CAMnC,GALA,AAGE,KAAK,UAFL,KAAK,OAAO,MAAM,gBAAgB,CAClC,KAAK,OAAO,SAAS,CACP,MAEZ,KAAK,YAAa,CACpB,IAAM,EAAI,KAAK,YACf,KAAK,YAAc,KACnB,GAAG"}
1
+ {"version":3,"file":"SpineSymbol-Fe0Fz1JW.cjs","names":[],"sources":["../src/symbols/SpineSymbol.ts"],"sourcesContent":["import { ReelSymbol } from './ReelSymbol.js';\n\n// Spine types imported dynamically. this is an optional peer dependency\nlet SpineClass: any = null;\n\n// Kick off the optional import at module init and keep the promise so callers\n// can wait for it. Without this, constructing a SpineSymbol on a cold start\n// (before the dynamic import resolves) throws the \"not installed\" error even\n// when Spine IS installed - it just hadn't finished loading yet.\nconst spineReady: Promise<void> = (async () => {\n try {\n const spineModule = await import('@esotericsoftware/spine-pixi-v8');\n SpineClass = spineModule.Spine;\n } catch {\n // Spine not available. SpineSymbol will throw on construction.\n }\n})();\n\n/**\n * Resolves once the optional `@esotericsoftware/spine-pixi-v8` import has\n * settled (whether or not it was installed). Await this before constructing\n * `SpineSymbol`s if you build the reel set immediately on app start, so a cold\n * load can't throw the misleading \"not installed\" error.\n */\nexport function whenSpineReady(): Promise<void> {\n return spineReady;\n}\n\nexport interface SpineSymbolOptions {\n /** Map of symbolId → SkeletonData. */\n skeletonDataMap: Record<string, any>;\n /** Default animation name to play in idle. Default: 'idle'. */\n idleAnimation?: string;\n /** Animation name to play on win. Default: 'win'. */\n winAnimation?: string;\n /** Default skin name. Default: 'default'. */\n defaultSkin?: string;\n /**\n * Fail loud on unmapped animations. When `true`, activating or winning with\n * a configured animation name that the skeleton doesn't define throws instead\n * of silently showing nothing (catches typos like `ide` vs `idle`). Default\n * `false` to preserve the lenient behavior. See ADR 011.\n */\n strict?: boolean;\n}\n\n/**\n * Symbol implementation using Spine 2D skeletal animation.\n *\n * Requires `@esotericsoftware/spine-pixi-v8` as an optional peer dependency.\n * If Spine is not installed, constructing a SpineSymbol will throw.\n */\nexport class SpineSymbol extends ReelSymbol {\n private _spine: any = null;\n private _skeletonDataMap: Record<string, any>;\n private _idleAnimation: string;\n private _winAnimation: string;\n private _defaultSkin: string;\n private _winResolve: (() => void) | null = null;\n private _currentSkeletonKey: string = '';\n private _strict: boolean;\n\n constructor(options: SpineSymbolOptions) {\n super();\n if (!SpineClass) {\n throw new Error(\n 'SpineSymbol could not use @esotericsoftware/spine-pixi-v8. Either it is ' +\n 'not installed (npm install @esotericsoftware/spine-pixi-v8), or its ' +\n 'dynamic import has not resolved yet on a cold start — await ' +\n 'whenSpineReady() before constructing SpineSymbols.',\n );\n }\n this._skeletonDataMap = options.skeletonDataMap;\n this._idleAnimation = options.idleAnimation ?? 'idle';\n this._winAnimation = options.winAnimation ?? 'win';\n this._defaultSkin = options.defaultSkin ?? 'default';\n this._strict = options.strict ?? false;\n }\n\n protected onActivate(symbolId: string): void {\n const skeletonData = this._skeletonDataMap[symbolId];\n if (!skeletonData) return;\n\n // Reuse existing spine if same skeleton data\n if (this._currentSkeletonKey !== symbolId) {\n if (this._spine) {\n this.view.removeChild(this._spine);\n this._spine.destroy();\n }\n this._spine = new SpineClass({ skeletonData });\n this.view.addChild(this._spine);\n this._currentSkeletonKey = symbolId;\n }\n\n if (this._spine.skeleton.data.findSkin(this._defaultSkin)) {\n this._spine.skeleton.setSkinByName(this._defaultSkin);\n this._spine.skeleton.setSlotsToSetupPose();\n }\n if (this._spine.skeleton.data.findAnimation(this._idleAnimation)) {\n this._spine.state.setAnimation(0, this._idleAnimation, true);\n } else if (this._strict) {\n throw new Error(\n `SpineSymbol(strict): idle animation '${this._idleAnimation}' not found ` +\n `on skeleton '${symbolId}'.`,\n );\n }\n }\n\n protected onDeactivate(): void {\n if (this._spine) {\n this._spine.state.clearListeners();\n this._spine.state.clearTracks();\n }\n // Settle any pending playWin so awaiters don't hang when the symbol is\n // recycled mid-animation. Mirrors the same fix in SpineReelSymbol.\n if (this._winResolve) {\n const r = this._winResolve;\n this._winResolve = null;\n r();\n }\n }\n\n async playWin(): Promise<void> {\n if (!this._spine) return;\n if (!this._spine.skeleton.data.findAnimation(this._winAnimation)) {\n if (this._strict) {\n throw new Error(\n `SpineSymbol(strict): win animation '${this._winAnimation}' not found ` +\n `on skeleton '${this._currentSkeletonKey}'.`,\n );\n }\n return;\n }\n\n return new Promise<void>((resolve) => {\n this._winResolve = resolve;\n const entry = this._spine.state.setAnimation(0, this._winAnimation, false);\n this._spine.state.addListener({\n complete: (trackEntry: any) => {\n if (trackEntry === entry) {\n this._spine.state.clearListeners();\n if (this._spine.skeleton.data.findAnimation(this._idleAnimation)) {\n this._spine.state.setAnimation(0, this._idleAnimation, true);\n }\n this._winResolve = null;\n resolve();\n }\n },\n });\n });\n }\n\n stopAnimation(): void {\n if (!this._spine) return;\n this._spine.state.clearListeners();\n if (this._spine.skeleton.data.findAnimation(this._idleAnimation)) {\n this._spine.state.setAnimation(0, this._idleAnimation, true);\n }\n if (this._winResolve) {\n this._winResolve();\n this._winResolve = null;\n }\n }\n\n resize(width: number, height: number): void {\n if (!this._spine) return;\n const bounds = this._spine.getBounds();\n if (bounds.width > 0 && bounds.height > 0) {\n this._spine.scale.set(\n width / bounds.width,\n height / bounds.height,\n );\n }\n }\n\n protected override onDestroy(): void {\n if (this._spine) {\n this._spine.state.clearListeners();\n this._spine.destroy();\n this._spine = null;\n }\n if (this._winResolve) {\n const r = this._winResolve;\n this._winResolve = null;\n r();\n }\n }\n}\n"],"mappings":"6CAGA,IAAI,EAAkB,KAMhB,GAA6B,SAAY,CAC7C,GAAI,CAEF,GADoB,MAAM,OAAO,oCACR,WACnB,MAGN,CAQJ,SAAgB,GAAgC,CAC9C,OAAO,EA2BT,IAAa,EAAb,cAAiC,EAAA,CAAW,CAC1C,OAAsB,KACtB,iBACA,eACA,cACA,aACA,YAA2C,KAC3C,oBAAsC,GACtC,QAEA,YAAY,EAA6B,CAEvC,GADA,OAAO,CACH,CAAC,EACH,MAAU,MACR,6PAID,CAEH,KAAK,iBAAmB,EAAQ,gBAChC,KAAK,eAAiB,EAAQ,eAAiB,OAC/C,KAAK,cAAgB,EAAQ,cAAgB,MAC7C,KAAK,aAAe,EAAQ,aAAe,UAC3C,KAAK,QAAU,EAAQ,QAAU,GAGnC,WAAqB,EAAwB,CAC3C,IAAM,EAAe,KAAK,iBAAiB,GACtC,KAiBL,IAdI,KAAK,sBAAwB,IAC3B,KAAK,SACP,KAAK,KAAK,YAAY,KAAK,OAAO,CAClC,KAAK,OAAO,SAAS,EAEvB,KAAK,OAAS,IAAI,EAAW,CAAE,eAAc,CAAC,CAC9C,KAAK,KAAK,SAAS,KAAK,OAAO,CAC/B,KAAK,oBAAsB,GAGzB,KAAK,OAAO,SAAS,KAAK,SAAS,KAAK,aAAa,GACvD,KAAK,OAAO,SAAS,cAAc,KAAK,aAAa,CACrD,KAAK,OAAO,SAAS,qBAAqB,EAExC,KAAK,OAAO,SAAS,KAAK,cAAc,KAAK,eAAe,CAC9D,KAAK,OAAO,MAAM,aAAa,EAAG,KAAK,eAAgB,GAAK,SACnD,KAAK,QACd,MAAU,MACR,wCAAwC,KAAK,eAAe,2BAC5C,EAAS,IAC1B,EAIL,cAA+B,CAO7B,GANI,KAAK,SACP,KAAK,OAAO,MAAM,gBAAgB,CAClC,KAAK,OAAO,MAAM,aAAa,EAI7B,KAAK,YAAa,CACpB,IAAM,EAAI,KAAK,YACf,KAAK,YAAc,KACnB,GAAG,EAIP,MAAM,SAAyB,CACxB,QAAK,OACV,IAAI,CAAC,KAAK,OAAO,SAAS,KAAK,cAAc,KAAK,cAAc,CAAE,CAChE,GAAI,KAAK,QACP,MAAU,MACR,uCAAuC,KAAK,cAAc,2BAC1C,KAAK,oBAAoB,IAC1C,CAEH,OAGF,OAAO,IAAI,QAAe,GAAY,CACpC,KAAK,YAAc,EACnB,IAAM,EAAQ,KAAK,OAAO,MAAM,aAAa,EAAG,KAAK,cAAe,GAAM,CAC1E,KAAK,OAAO,MAAM,YAAY,CAC5B,SAAW,GAAoB,CACzB,IAAe,IACjB,KAAK,OAAO,MAAM,gBAAgB,CAC9B,KAAK,OAAO,SAAS,KAAK,cAAc,KAAK,eAAe,EAC9D,KAAK,OAAO,MAAM,aAAa,EAAG,KAAK,eAAgB,GAAK,CAE9D,KAAK,YAAc,KACnB,GAAS,GAGd,CAAC,EACF,EAGJ,eAAsB,CACf,KAAK,SACV,KAAK,OAAO,MAAM,gBAAgB,CAC9B,KAAK,OAAO,SAAS,KAAK,cAAc,KAAK,eAAe,EAC9D,KAAK,OAAO,MAAM,aAAa,EAAG,KAAK,eAAgB,GAAK,CAE9D,AAEE,KAAK,eADL,KAAK,aAAa,CACC,OAIvB,OAAO,EAAe,EAAsB,CAC1C,GAAI,CAAC,KAAK,OAAQ,OAClB,IAAM,EAAS,KAAK,OAAO,WAAW,CAClC,EAAO,MAAQ,GAAK,EAAO,OAAS,GACtC,KAAK,OAAO,MAAM,IAChB,EAAQ,EAAO,MACf,EAAS,EAAO,OACjB,CAIL,WAAqC,CAMnC,GALA,AAGE,KAAK,UAFL,KAAK,OAAO,MAAM,gBAAgB,CAClC,KAAK,OAAO,SAAS,CACP,MAEZ,KAAK,YAAa,CACpB,IAAM,EAAI,KAAK,YACf,KAAK,YAAc,KACnB,GAAG"}
@@ -291,6 +291,61 @@ export interface NoOffsetConfig {
291
291
  mode: 'none';
292
292
  }
293
293
  export type OffsetConfig = TrapezoidConfig | NoOffsetConfig;
294
+ /**
295
+ * One cell's projected footprint on a curved reel: the four corners of the
296
+ * quad the symbol should render into, replacing its flat rectangle.
297
+ *
298
+ * Corners are SCREEN-space and LOCAL to the symbol view's own origin, in the
299
+ * order PixiJS `PerspectiveMesh` wants them - clockwise from top-left - in
300
+ * every orientation, because symbol art stays upright however the strip
301
+ * travels. `width` / `height` describe the flat cell box the quad replaces, so
302
+ * a symbol can work out its own scale without having to remember what
303
+ * `resize()` said.
304
+ *
305
+ * Built by `ReelCurve.quadFor()`, consumed by `ReelSymbol.applyCellQuad()`.
306
+ * It lives here rather than next to `ReelCurve` so `symbols/` can name it
307
+ * without importing `core/` and inverting the dependency flow.
308
+ */
309
+ export interface ReelCellQuad {
310
+ /** Flat box's left edge, view-local. Non-zero when the symbol set an inset. */
311
+ x: number;
312
+ /** Flat box's top edge, view-local. */
313
+ y: number;
314
+ /** Flat box width in screen pixels. */
315
+ width: number;
316
+ /** Flat box height in screen pixels. */
317
+ height: number;
318
+ /** Top-left. */
319
+ x0: number;
320
+ y0: number;
321
+ /** Top-right. */
322
+ x1: number;
323
+ y1: number;
324
+ /** Bottom-right. */
325
+ x2: number;
326
+ y2: number;
327
+ /** Bottom-left. */
328
+ x3: number;
329
+ y3: number;
330
+ }
331
+ /**
332
+ * The part of its cell a symbol's art actually covers, as fractions of the flat
333
+ * cell box in SCREEN space (`0,0` top-left to `1,1` bottom-right).
334
+ *
335
+ * Most slot art does not fill its cell: a trimmed atlas frame is typically a
336
+ * small shape floating in a much larger transparent box. Projecting the whole
337
+ * cell and stretching that art across it would blow the symbol up to the cell's
338
+ * edges and give it the cell's keystone instead of its own. Reporting the inset
339
+ * instead means the drum projects the rectangle the art is really in.
340
+ *
341
+ * Returned by `ReelSymbol.cellInset`; `null` means "my art fills the cell".
342
+ */
343
+ export interface ReelCellInset {
344
+ left: number;
345
+ top: number;
346
+ right: number;
347
+ bottom: number;
348
+ }
294
349
  /** 2D matrix type (reel × cell). */
295
350
  export type Matrix<T> = T[][];
296
351
  /** 2D position. */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/config/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/config/types.ts"],"names":[],"mappings":"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"}
@@ -1,9 +1,10 @@
1
- import { Container } from 'pixi.js';
1
+ import { Container, Renderer, Ticker } from 'pixi.js';
2
2
  import { Disposable } from '../utils/Disposable.js';
3
3
  import { ReelSymbol } from '../symbols/ReelSymbol.js';
4
4
  import { SymbolFactory } from '../symbols/SymbolFactory.js';
5
5
  import { SymbolData, Stacking } from '../config/types.js';
6
6
  import { ReelAxis } from './ReelAxis.js';
7
+ import { ReelCurve, ReelCurveInput } from './ReelCurve.js';
7
8
  import { EventEmitter } from '../events/EventEmitter.js';
8
9
  import { ReelEvents } from '../events/ReelEvents.js';
9
10
  import { RandomSymbolProvider } from '../frame/RandomSymbolProvider.js';
@@ -145,6 +146,30 @@ export interface ReelConfig {
145
146
  * reel draws in front.
146
147
  */
147
148
  reelStacking?: Stacking;
149
+ /**
150
+ * Cylinder curvature for this reel. Omitted or `0` leaves it dead flat and
151
+ * costs nothing. See {@link ReelCurveConfig}.
152
+ */
153
+ curve?: ReelCurveInput;
154
+ /**
155
+ * Reel-local cross coordinate the curve's perspective converges on. Set by
156
+ * the builder from `curveFocus(...)`; omitted means the reel's own centre.
157
+ */
158
+ curveFocus?: number;
159
+ /**
160
+ * Renderer to draw the reel's warp texture with. Present only when the set
161
+ * was built with `curveMode('warp')` and a `renderer(...)`; its presence is
162
+ * what switches this reel from the per-symbol projection to the whole-reel
163
+ * vertex warp.
164
+ */
165
+ curveRenderer?: Renderer;
166
+ /** Ticker driving the warp's per-frame texture refresh. Warp mode only. */
167
+ curveTicker?: Ticker;
168
+ /**
169
+ * Cross-axis room, in pixels per side, for art that is wider than its cell.
170
+ * Warp mode only; set by `ReelSetBuilder.curveBleed()`.
171
+ */
172
+ curveBleed?: number;
148
173
  }
149
174
  /**
150
175
  * Internal sentinel marking non-anchor cells of a big symbol's block.
@@ -180,6 +205,19 @@ export declare class Reel implements Disposable {
180
205
  /** Current spinning mode. */
181
206
  spinningMode: SpinningMode;
182
207
  private readonly _axis;
208
+ /** Cylinder curvature, or `undefined` when this reel is flat. */
209
+ private _curve?;
210
+ /** Reel-local cross coordinate the curve converges on. `null` = own centre. */
211
+ private readonly _curveFocus;
212
+ /**
213
+ * Whole-reel vertex warp, when the set is in `curveMode('warp')`. Present
214
+ * means the reel container is rendered to a texture and drawn through a
215
+ * displaced mesh instead of being drawn directly, and symbols are left
216
+ * completely alone - the warp bends all of them at once.
217
+ */
218
+ private _warp?;
219
+ /** True when this reel is drawn through a whole-reel warp. */
220
+ private readonly _warping;
183
221
  private readonly _mainCell;
184
222
  private readonly _mainGap;
185
223
  private readonly _crossGap;
@@ -326,6 +364,11 @@ export declare class Reel implements Disposable {
326
364
  get gsap(): Gsap;
327
365
  /** This reel's travel projection (orientation + direction). */
328
366
  get axis(): ReelAxis;
367
+ /**
368
+ * This reel's cylinder curvature, or `undefined` when it renders flat.
369
+ * Read it to map your own overlays onto the bent grid.
370
+ */
371
+ get curve(): ReelCurve | undefined;
329
372
  /** Update reel for one frame. Called by SpinController via ticker. */
330
373
  update(deltaMs: number): void;
331
374
  /**
@@ -358,6 +401,18 @@ export declare class Reel implements Disposable {
358
401
  * returns the anchor symbol so animations target the actual visual.
359
402
  */
360
403
  getSymbolAt(visibleCell: number): ReelSymbol;
404
+ /**
405
+ * Pull any lifted (unmasked) view that has ended up in a buffer slot back
406
+ * under the mask.
407
+ *
408
+ * `_replaceSymbol` never lifts a buffer slot, but a symbol lifted while it
409
+ * was VISIBLE can still travel into a buffer slot without being replaced:
410
+ * a nudge rotates the array and only the wrapped symbol goes through
411
+ * `_replaceSymbol`, so an unmask symbol nudged out of the window kept its
412
+ * seat above the mask and hung there outside the grid. Runs on every
413
+ * settle, where the strip's final slots are known.
414
+ */
415
+ private _reMaskLiftedBufferSlots;
361
416
  /**
362
417
  * Swap the symbol at a single visible cell in-place, without restarting
363
418
  * the spin or rebuilding the rest of the strip.
@@ -480,22 +535,37 @@ export declare class Reel implements Disposable {
480
535
  * above the reel mask.
481
536
  */
482
537
  private _isUnmasked;
538
+ /** Whether strip slot `index` sits outside the visible window. */
539
+ private _isBufferSlot;
540
+ /**
541
+ * Which pool slot `index` draws from: the visible window is `'spinning'`,
542
+ * and a buffer cell names its side so that side's pools apply.
543
+ */
544
+ private _slotKind;
483
545
  /**
484
- * Whether the symbol should render above the mask RIGHT NOW. Unmask is
485
- * an at-rest presentation: while the reel is in motion (including the
486
- * stop approach and bounce, when the result symbols are installed),
487
- * every view (unmask ids included) stays in the masked reel container so
488
- * nothing scrolls visibly outside the grid or sits parked in a buffer
489
- * cell. Landed visible-cell symbols are lifted by `notifyLanded()`;
546
+ * Whether the symbol at strip slot `index` should render above the mask
547
+ * RIGHT NOW. Unmask is an at-rest presentation of a VISIBLE cell:
548
+ *
549
+ * - while the reel is in motion (including the stop approach and
550
+ * bounce, when the result symbols are installed), every view
551
+ * (unmask ids included) stays in the masked reel container so
552
+ * nothing scrolls visibly outside the grid, and
553
+ * - a buffer slot never lifts at all. it is parked outside the window
554
+ * precisely so the mask hides it, and a lifted one hangs above or
555
+ * below the grid in plain sight until the next spin re-masks it.
556
+ * `placeStrip` writes buffer slots at rest on every skip, which is
557
+ * exactly when that used to happen.
558
+ *
559
+ * Landed visible-cell symbols are lifted by `notifyLanded()`;
490
560
  * `notifySpinStart()` pulls them back down before the strip moves.
491
561
  */
492
562
  private _effectiveUnmask;
493
563
  /**
494
564
  * Pick the right parent container for a symbol view based on its
495
- * `unmask` flag and the reel's spin state. At-rest unmasked symbols sit
496
- * in `viewport.unmaskedContainer` (above the reel mask); everything
497
- * else lives in this reel's own container (which is itself inside
498
- * `viewport.maskedContainer`).
565
+ * `unmask` flag, its slot, and the reel's spin state. At-rest unmasked
566
+ * symbols in a visible cell sit in `viewport.unmaskedContainer` (above
567
+ * the reel mask); everything else lives in this reel's own container
568
+ * (which is itself inside `viewport.maskedContainer`).
499
569
  */
500
570
  private _parentForSymbolId;
501
571
  /**
@@ -508,6 +578,12 @@ export declare class Reel implements Disposable {
508
578
  * live in `this.container`, so reel-local coords map directly.
509
579
  */
510
580
  private _placeSymbolView;
581
+ /**
582
+ * Build the curvature for this reel, or `undefined` when it would be flat.
583
+ * A flat set must not carry a curve object at all: that keeps the render
584
+ * loop and every placement byte-identical to an uncurved build.
585
+ */
586
+ private _buildCurve;
511
587
  /**
512
588
  * Convert a view's current y back to reel-local coords. The view may
513
589
  * be parented to either `this.container` (already reel-local) or