reel-deal 0.1.0-dev.0 → 0.1.0-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +137 -0
- package/dist/index.cjs.js +89 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +141 -0
- package/dist/index.es.js +543 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +89 -0
- package/dist/index.umd.js.map +1 -0
- package/package.json +38 -9
package/README.md
CHANGED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
<h1 align="center"><strong>reel-deal</strong></h1>
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
<p align="center">High-performance WebGL slot reel animation for any HTMLCanvasElement.</p>
|
|
5
|
+
|
|
6
|
+
[](https://www.npmjs.com/package/reel-deal)
|
|
7
|
+
[](https://github.com/ux-ui-pro/reel-deal)
|
|
8
|
+
[](https://www.npmjs.org/package/reel-deal)
|
|
9
|
+
|
|
10
|
+
<a href="https://codepen.io/ux-ui/pen/azZLXja">Demo</a>
|
|
11
|
+
</div>
|
|
12
|
+
<br>
|
|
13
|
+
|
|
14
|
+
# Install
|
|
15
|
+
```console
|
|
16
|
+
yarn add reel-deal
|
|
17
|
+
```
|
|
18
|
+
<br>
|
|
19
|
+
|
|
20
|
+
# Usage (TypeScript)
|
|
21
|
+
```ts
|
|
22
|
+
import { ReelDeal } from 'reel-deal';
|
|
23
|
+
|
|
24
|
+
const reel = new ReelDeal({
|
|
25
|
+
canvas: 'canvas',
|
|
26
|
+
container: 'reelWrap',
|
|
27
|
+
sprite: new URL('./assets/reel.webp', import.meta.url).href,
|
|
28
|
+
slotCount: 8,
|
|
29
|
+
reels: 3,
|
|
30
|
+
initialSegments: [3, 2, 0],
|
|
31
|
+
spinConfig: {
|
|
32
|
+
minSpins: 2,
|
|
33
|
+
durationMs: 4000,
|
|
34
|
+
speedPxS: 4000,
|
|
35
|
+
staggerMs: 300,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
await reel.init();
|
|
40
|
+
await reel.spinOnce([1, 1, 0]);
|
|
41
|
+
```
|
|
42
|
+
<br>
|
|
43
|
+
|
|
44
|
+
# Usage (Vue 3)
|
|
45
|
+
```vue
|
|
46
|
+
<script setup lang="ts">
|
|
47
|
+
import { onBeforeUnmount, onMounted, ref, shallowRef } from 'vue';
|
|
48
|
+
import { ReelDeal } from 'reel-deal';
|
|
49
|
+
|
|
50
|
+
const containerRef = ref<HTMLDivElement | null>(null);
|
|
51
|
+
const canvasRef = ref<HTMLCanvasElement | null>(null);
|
|
52
|
+
const buttonRef = ref<HTMLButtonElement | null>(null);
|
|
53
|
+
const reel = shallowRef<ReelDeal | null>(null);
|
|
54
|
+
|
|
55
|
+
onMounted(async () => {
|
|
56
|
+
if (!containerRef.value || !canvasRef.value) return;
|
|
57
|
+
reel.value = new ReelDeal({
|
|
58
|
+
container: containerRef.value,
|
|
59
|
+
canvas: canvasRef.value,
|
|
60
|
+
button: buttonRef.value ?? undefined,
|
|
61
|
+
sprite: new URL('./assets/reel.webp', import.meta.url).href,
|
|
62
|
+
slotCount: 8,
|
|
63
|
+
reels: 3,
|
|
64
|
+
});
|
|
65
|
+
await reel.value.init();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
onBeforeUnmount(() => {
|
|
69
|
+
reel.value?.destroy();
|
|
70
|
+
reel.value = null;
|
|
71
|
+
});
|
|
72
|
+
</script>
|
|
73
|
+
|
|
74
|
+
<template>
|
|
75
|
+
<button ref="buttonRef">Spin</button>
|
|
76
|
+
<div ref="containerRef">
|
|
77
|
+
<canvas ref="canvasRef"></canvas>
|
|
78
|
+
</div>
|
|
79
|
+
</template>
|
|
80
|
+
```
|
|
81
|
+
— Initialize in `onMounted`, destroy in `onBeforeUnmount`.
|
|
82
|
+
|
|
83
|
+
<br>
|
|
84
|
+
|
|
85
|
+
# Usage (HTML)
|
|
86
|
+
```html
|
|
87
|
+
<button id="spinBtn">Spin</button>
|
|
88
|
+
<div id="reelWrap">
|
|
89
|
+
<canvas id="canvas"></canvas>
|
|
90
|
+
</div>
|
|
91
|
+
```
|
|
92
|
+
— To enable click-to-spin, pass `button` in options.
|
|
93
|
+
— Use `queuedSpinStates` for predefined spin sequences.
|
|
94
|
+
|
|
95
|
+
<br>
|
|
96
|
+
|
|
97
|
+
# Sprite format
|
|
98
|
+
- Single vertical spritesheet.
|
|
99
|
+
- Each slot is a square frame.
|
|
100
|
+
- Total height is `slotCount * frameWidth`.
|
|
101
|
+
|
|
102
|
+
<br>
|
|
103
|
+
|
|
104
|
+
# Options
|
|
105
|
+
| Option | Type | Default | Description |
|
|
106
|
+
|:-------------------|:------------------------------------|:---------------------:|:-----------------------------------------------------------------|
|
|
107
|
+
| `canvas` | `HTMLCanvasElement \| string` | — | Canvas element or element id. |
|
|
108
|
+
| `container` | `HTMLElement \| string` | — | Container element or element id. |
|
|
109
|
+
| `sprite` | `string \| HTMLImageElement` | — | Sprite sheet URL or image element. |
|
|
110
|
+
| `slotCount` | `number` | — | Number of frames in the sprite. |
|
|
111
|
+
| `reels` | `number` | `1` | Number of reels (1–5). |
|
|
112
|
+
| `initialSegments` | `number[]` | — | Initial stop index per reel. |
|
|
113
|
+
| `button` | `HTMLButtonElement \| string` | — | Optional spin button element or DOM id. |
|
|
114
|
+
| `queuedSpinStates` | `ReelDealSpinState[]` | — | Optional queue of predefined spins. |
|
|
115
|
+
| `spinConfig` | `Partial<ReelDealSpinConfig>` | `defaultSpinConfig` | Default spin config (minSpins, durationMs, speedPxS, staggerMs). |
|
|
116
|
+
| `spinBlur` | `Partial<ReelDealSpinBlur>` | `defaultSpinBlur` | Motion blur settings. |
|
|
117
|
+
| `webglShading` | `Partial<ReelDealWebGLShading>` | `defaultWebGLShading` | Warp and edge shading. |
|
|
118
|
+
| `idleBob` | `Partial<ReelDealIdleBob> \| false` | `defaultIdleBob` | Idle bobbing animation (set `false` to disable). |
|
|
119
|
+
| `maxDpr` | `number` | `2` | Max device pixel ratio. |
|
|
120
|
+
<br>
|
|
121
|
+
|
|
122
|
+
# Methods
|
|
123
|
+
```ts
|
|
124
|
+
await reel.init();
|
|
125
|
+
reel.setSegments([0, 1, 2]);
|
|
126
|
+
reel.setSegment(3);
|
|
127
|
+
await reel.spinOnce([1, 1, 0]);
|
|
128
|
+
await reel.spinOnce([1, 1, 0], { durationMs: 3200 });
|
|
129
|
+
await reel.spinQueue([{ stopAtSegments: [2, 2, 2] }]);
|
|
130
|
+
reel.requestResize();
|
|
131
|
+
reel.stop();
|
|
132
|
+
reel.destroy();
|
|
133
|
+
```
|
|
134
|
+
<br>
|
|
135
|
+
|
|
136
|
+
# License
|
|
137
|
+
MIT
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const p={minSpins:2,durationMs:4e3,speedPxS:4e3,staggerMs:300},E={warpAngleDeg:90,edgePower:.5},T={maxPx:4,speedAtMax:2200},g={amplitudePx:8,speedHz:.25,phaseOffsetRad:.9,delayMs:500,rampMs:800},L=.1,x=.6,B=(a,e,i)=>Math.max(e,Math.min(i,Math.floor(a))),P=(a,e)=>(a%e+e)%e,S=a=>Math.max(0,Math.min(1,a)),U=a=>{const e=S(a);return e**2*(2-e)},G=a=>{const e=S(a);return e+e**2-e**3},X=a=>{const e=S(a);if(e<=L){const s=e/L;return U(s)*L}if(e<x)return e;const i=(e-x)/(1-x);return x+G(i)*(1-x)},A=(a,e)=>typeof a=="number"&&Number.isFinite(a)?a:e,b=(a,e)=>Math.max(0,A(a,e)),m=(a,e)=>Math.max(0,Math.floor(A(a,e))),z=2,D=a=>{const e={...p,...a??{}};return{minSpins:m(e.minSpins,p.minSpins),durationMs:m(e.durationMs,p.durationMs??0),speedPxS:m(e.speedPxS,p.speedPxS??0),staggerMs:m(e.staggerMs,p.staggerMs??0)}},N=a=>{const e={...T,...a??{}};return{maxPx:b(e.maxPx,T.maxPx),speedAtMax:b(e.speedAtMax,T.speedAtMax)}},F=a=>{const e={...E,...a??{}};return{warpAngleDeg:b(e.warpAngleDeg,E.warpAngleDeg),edgePower:b(e.edgePower,E.edgePower)}},V=a=>{if(a===!1)return null;const e={...g,...a??{}};return{amplitudePx:b(e.amplitudePx,g.amplitudePx),speedHz:b(e.speedHz,g.speedHz),phaseOffsetRad:A(e.phaseOffsetRad,g.phaseOffsetRad),delayMs:m(e.delayMs,g.delayMs),rampMs:m(e.rampMs,g.rampMs)}},k=a=>Math.max(1,A(a,z)),Y=(a,e)=>{const i=document.getElementById(a);if(!i)throw new Error(`${e} not found: ${a}`);return i},H=(a,e)=>typeof a=="string"?Y(a,e):a,q=5,C=.72,$=.28,Q=7,W=.85,K=1-W;class j{canvas;container;opts;visibleSlots=3;heightScale=2/3;centerIndex=1;reels;spinConfig;spinBlur;webglShading;idleBob;maxDpr;spriteImg=null;spriteWidth=0;spriteHeight=0;frameHeight=0;width=0;height=0;dpr=1;offsets;velocities;lastVelT=null;lastVelOffsets;idleStartTime=null;idleBaseOffsets;gl=null;glProgram=null;glBuffer=null;glTexture=null;glAttribs=null;webglInitError=null;glUniforms=null;anim=null;rafId=null;pendingSpinResolvers=[];resizeObserver=null;resizeQueued=!1;button=null;buttonHandlerAttached=!1;spinning=!1;queuedSpins=null;constructor(e){this.opts=e,this.reels=B(e.reels??1,1,q),this.spinConfig=D(e.spinConfig),this.spinBlur=N(e.spinBlur),this.webglShading=F(e.webglShading),this.idleBob=V(e.idleBob),this.maxDpr=k(e.maxDpr),this.canvas=H(e.canvas,"Canvas"),this.container=H(e.container,"Container"),this.offsets=new Array(this.reels).fill(0),this.velocities=new Array(this.reels).fill(0),this.lastVelOffsets=new Array(this.reels).fill(0),this.idleBaseOffsets=new Array(this.reels).fill(0)}async init(){if(await this.loadSprite(),this.tryInitWebGL(),!this.gl)throw new Error(`WebGL is not supported or failed to initialize. ${this.webglInitError??""}`.trim());this.syncContainerLayoutVars(),this.ensureCanvasCoversContainer(),this.setupResizeWatcher(),this.resizeToContainer(),this.setSegments(this.opts.initialSegments),this.button=this.resolveButton(),this.queuedSpins=this.resolveSpinQueue(),this.attachButtonHandler(),this.render(),this.startLoop()}destroy(){this.stop(),this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=null),this.button&&this.buttonHandlerAttached&&(this.button.removeEventListener("click",this.handleButtonClick),this.buttonHandlerAttached=!1),this.releaseWebGLResources()}ensureCanvasCoversContainer(){this.canvas!==this.container&&this.container.contains(this.canvas)&&(this.canvas.style.width||(this.canvas.style.width="100%"),this.canvas.style.height||(this.canvas.style.height="100%"),this.canvas.style.display||(this.canvas.style.display="block"))}syncContainerLayoutVars(){const e=this.canvas===this.container?this.canvas:this.container;e.style.setProperty("--reels",String(this.reels)),e.style.setProperty("--visible-slots",String(this.visibleSlots)),e.style.aspectRatio=`${this.reels} / ${this.visibleSlots*this.heightScale}`,this.canvas!==this.container&&(this.container.style.overflow="hidden")}setSegments(e){this.ensureReady();const i=e??[];for(let s=0;s<this.reels;s+=1){const t=i[s]??0,r=B(t,0,this.opts.slotCount-1);this.offsets[s]=r*this.frameHeight-this.centerIndex*this.frameHeight,this.lastVelOffsets[s]=this.offsets[s],this.velocities[s]=0,this.idleBaseOffsets[s]=this.offsets[s]}this.lastVelT=null,this.idleStartTime=null,this.anim=null,this.stop(),this.render(),this.startLoop()}setSegment(e){this.setSegments(new Array(this.reels).fill(e))}spinOnce(e,i){this.ensureReady();const{minSpins:s,staggerMs:t,speedPxS:r,durationMs:o}=this.getSpinConfig(i),n=r>0,c=new Array(this.reels),f=new Array(this.reels),u=new Array(this.reels);for(let l=0;l<this.reels;l+=1){const w=B(e[l]??0,0,this.opts.slotCount-1)*this.frameHeight-this.centerIndex*this.frameHeight,M=P(w,this.spriteHeight),O=P(this.offsets[l],this.spriteHeight),R=P(O-M,this.spriteHeight);let _=-(s*this.spriteHeight+R);if(n&&o>0){const y=r*o/1e3,I=Math.max(s,Math.ceil(Math.max(0,y-R)/this.spriteHeight));_=-(R+I*this.spriteHeight)}if(t>0&&n){const y=r*t*l/1e3,I=Math.ceil(y/this.spriteHeight);_-=I*this.spriteHeight}c[l]=this.offsets[l],f[l]=_,n?u[l]=Math.max(0,Math.round(Math.abs(_)/r*1e3)):u[l]=Math.max(0,o+t*l)}if(f.reduce((l,v)=>l+Math.abs(v),0)===0||u.every(l=>l<=0)){for(let l=0;l<this.reels;l+=1)this.offsets[l]=this.offsets[l]+f[l];return this.anim=null,this.resolvePendingSpins(),this.render(),Promise.resolve()}const h=performance.now();return this.anim={startTime:h,startOffsets:c,deltas:f,durationMs:u},this.idleStartTime=null,this.startLoop(),new Promise(l=>{this.pendingSpinResolvers.push(l)})}getSpinConfig(e){return e?D({...this.spinConfig,...e}):this.spinConfig}async spinQueue(e){for(let i=0;i<e.length;i+=1){const s=e[i];await this.spinOnce(s.stopAtSegments),s.callback?.(i,s.stopAtSegments)}}stop(){this.rafId!==null&&cancelAnimationFrame(this.rafId),this.rafId=null,this.anim=null,this.resolvePendingSpins(),this.lastVelT=null;for(let e=0;e<this.reels;e+=1)this.velocities[e]=0}resolvePendingSpins(){if(this.pendingSpinResolvers.length===0)return;const e=this.pendingSpinResolvers;this.pendingSpinResolvers=[];for(const i of e)i()}requestResize(){this.queueResize()}ensureReady(){if(!this.spriteImg||this.spriteWidth<=0||this.spriteHeight<=0||this.frameHeight<=0)throw new Error("Sprite is not ready. Call init() and await it first.")}attachButtonHandler(){!this.button||this.buttonHandlerAttached||(this.button.addEventListener("click",this.handleButtonClick,{passive:!0}),this.buttonHandlerAttached=!0)}handleButtonClick=async()=>{if(this.spinning)return;const e=this.consumeNextSpin()??{stopAtSegments:this.buildRandomSegments()};this.spinning=!0,this.setButtonDisabled(!0);try{await this.spinOnce(e.stopAtSegments)}finally{this.queuedSpins&&this.queuedSpins.length===0?this.setButtonDisabled(!0):this.setButtonDisabled(!1),this.spinning=!1}};resolveButton(){return this.opts.button?H(this.opts.button,"Button"):null}resolveSpinQueue(){return this.opts.queuedSpinStates??null}consumeNextSpin(){return this.queuedSpins||(this.queuedSpins=this.resolveSpinQueue()),!this.queuedSpins||this.queuedSpins.length===0?null:this.queuedSpins.shift()??null}buildRandomSegments(){const e=Math.max(1,Math.floor(this.opts.slotCount)),i=[];for(let s=0;s<this.reels;s+=1)i.push(Math.floor(Math.random()*e));return i}setButtonDisabled(e){this.button&&(this.button.disabled=e,e?this.button.setAttribute("aria-busy","true"):this.button.removeAttribute("aria-busy"))}shouldAnimate(){return this.anim!==null||this.idleBob!==null}startLoop(){this.rafId===null&&this.shouldAnimate()&&(this.rafId=requestAnimationFrame(this.loop))}loop=e=>{if(this.anim){const{startTime:i,durationMs:s,startOffsets:t,deltas:r}=this.anim;let o=!0;for(let n=0;n<this.reels;n+=1){const c=s[n]??0,f=c<=0?1:Math.min(1,(e-i)/c),u=X(f);f<1&&(o=!1);const d=t[n]+r[n]*u;let h=0;if(f>C&&this.frameHeight>0){const v=(1-S((f-C)/$))**2,w=d/this.frameHeight*Math.PI*2,M=Math.min(Q,this.frameHeight*.06);h=Math.sin(w)*v*M}this.offsets[n]=d+h}if(this.updateVelocity(e),this.render(),o){for(let n=0;n<this.reels;n+=1)this.offsets[n]=this.anim.startOffsets[n]+this.anim.deltas[n],this.idleBaseOffsets[n]=this.offsets[n];this.updateVelocity(e);for(let n=0;n<this.reels;n+=1)this.velocities[n]=0;this.anim=null,this.idleStartTime=null,this.render(),this.resolvePendingSpins()}}else this.idleBob&&(this.updateIdle(e),this.render());if(!this.shouldAnimate()){this.rafId=null;return}this.rafId!==null&&(this.rafId=requestAnimationFrame(this.loop))};render(){this.spriteImg&&(this.width<=0||this.height<=0||this.gl&&this.renderWebGL())}getSpinBlurPxForReel(e){if(!this.anim)return 0;const i=Math.abs(this.velocities[e]??0),s=Math.max(1,this.spinBlur.speedAtMax);return S(i/s)*Math.max(0,this.spinBlur.maxPx)}renderWebGL(){if(!this.gl||!this.glProgram||!this.glAttribs||!this.glUniforms||!this.glTexture||!this.glBuffer||!this.spriteImg)return;const e=this.gl;e.useProgram(this.glProgram),e.bindBuffer(e.ARRAY_BUFFER,this.glBuffer),e.enableVertexAttribArray(this.glAttribs.pos),e.vertexAttribPointer(this.glAttribs.pos,2,e.FLOAT,!1,16,0),e.enableVertexAttribArray(this.glAttribs.uv),e.vertexAttribPointer(this.glAttribs.uv,2,e.FLOAT,!1,16,8),e.activeTexture(e.TEXTURE0),e.bindTexture(e.TEXTURE_2D,this.glTexture),e.uniform1i(this.glUniforms.texture,0),e.uniform1f(this.glUniforms.frameHeight,this.frameHeight),e.uniform1f(this.glUniforms.spriteHeight,this.spriteHeight),e.uniform1f(this.glUniforms.visibleSlots,this.visibleSlots),e.uniform1f(this.glUniforms.heightScale,this.heightScale),e.uniform1f(this.glUniforms.edgePower,Math.max(.5,this.webglShading.edgePower));const i=this.webglShading,s=Math.max(0,i.warpAngleDeg)*Math.PI/180;e.uniform1f(this.glUniforms.warpAngle,s),e.clear(e.COLOR_BUFFER_BIT);const t=1/this.reels;for(let r=0;r<this.reels;r+=1){e.uniform1f(this.glUniforms.reelX,r*t),e.uniform1f(this.glUniforms.reelW,t);const o=P(this.offsets[r],this.spriteHeight);e.uniform1f(this.glUniforms.offsetPx,o),e.uniform1f(this.glUniforms.blurPx,this.getSpinBlurPxForReel(r)),e.drawArrays(e.TRIANGLE_STRIP,0,4)}}updateVelocity(e){if(this.lastVelT===null){this.lastVelT=e;for(let s=0;s<this.reels;s+=1)this.lastVelOffsets[s]=this.offsets[s],this.velocities[s]=0;return}const i=(e-this.lastVelT)/1e3;if(!(i<=0)){for(let s=0;s<this.reels;s+=1){const t=(this.offsets[s]-this.lastVelOffsets[s])/i;this.velocities[s]=this.velocities[s]*W+t*K,this.lastVelOffsets[s]=this.offsets[s]}this.lastVelT=e}}updateIdle(e){if(!this.idleBob)return;this.idleStartTime===null&&(this.idleStartTime=e);const{amplitudePx:i,speedHz:s,phaseOffsetRad:t,delayMs:r,rampMs:o}=this.idleBob,n=e-this.idleStartTime;if(n<Math.max(0,r)){for(let h=0;h<this.reels;h+=1)this.offsets[h]=this.idleBaseOffsets[h],this.velocities[h]=0;return}const c=(n-Math.max(0,r))/1e3,f=Math.max(0,Math.min(1,(n-Math.max(0,r))/Math.max(1,o))),u=U(f),d=s*Math.PI*2;for(let h=0;h<this.reels;h+=1){const l=c*d+h*t;this.offsets[h]=this.idleBaseOffsets[h]+Math.sin(l)*(i*u),this.velocities[h]=0}}async loadSprite(){const{sprite:e}=this.opts,i=typeof e=="string"?new Image:e;typeof e=="string"&&(i.src=e),(!i.complete||i.naturalWidth<=0)&&await new Promise((f,u)=>{const d=()=>f(),h=()=>u(new Error("Failed to load sprite image"));i.addEventListener("load",d,{once:!0}),i.addEventListener("error",h,{once:!0})});const s=Math.max(1,Math.floor(this.opts.slotCount)),t=i.naturalWidth,r=t,o=r*s,n=i.naturalHeight;if(Math.abs(n-o)>2)throw new Error(`Sprite size mismatch. Expected height ~${Math.round(o)}px (slotCount=${s}, slot aspect 1/1), got ${n}px.`);this.spriteImg=i,this.spriteWidth=t,this.frameHeight=Math.max(1,Math.round(r)),this.spriteHeight=Math.max(1,Math.round(this.frameHeight*s))}setupResizeWatcher(){typeof ResizeObserver>"u"||this.resizeObserver||(this.resizeObserver=new ResizeObserver(()=>this.queueResize()),this.resizeObserver.observe(this.container))}queueResize=()=>{this.resizeQueued||(this.resizeQueued=!0,requestAnimationFrame(()=>{this.resizeQueued=!1,this.resizeToContainer()}))};resizeToContainer(){const e=this.container.clientWidth,i=this.container.clientHeight,s=this.visibleSlots*this.heightScale;if(e<=0||i<=0){this.applyResize(1,1);return}const t=s,r=this.reels,o=e/r*t;let n=e,c=o;c>i&&(c=i,n=i/t*r);const f=Math.max(1,Math.floor(n)),u=Math.max(1,Math.floor(c));this.applyResize(f,u),this.render()}applyViewportCrop(){this.canvas.style.width="100%",this.canvas.style.height="100%",this.canvas.style.transform=""}applyResize(e,i){const s=Math.max(1,Math.floor(e)),t=Math.max(1,Math.floor(i)),r=this.getTargetDpr();this.width=s,this.height=t,this.dpr=r,this.canvas.width=Math.floor(this.width*this.dpr),this.canvas.height=Math.floor(this.height*this.dpr),this.gl&&this.gl.viewport(0,0,this.canvas.width,this.canvas.height),this.applyViewportCrop()}getTargetDpr(){const e=window.devicePixelRatio||1,i=Math.max(1,e);return Math.min(this.maxDpr,i)}tryInitWebGL(){if(!this.spriteImg)return;this.releaseWebGLResources(),this.webglInitError=null;const e=this.canvas.getContext("webgl2",{antialias:!0,alpha:!0})??this.canvas.getContext("webgl",{antialias:!0,alpha:!0})??this.canvas.getContext("experimental-webgl",{antialias:!0,alpha:!0});if(!e){const u=document.createElement("canvas"),d=typeof WebGLRenderingContext<"u",h=u.getContext("webgl2")??u.getContext("webgl");this.webglInitError=`context is null (browser WebGL=${d?"yes":"no"}, fallback canvas=${h?"yes":"no"})`,console.warn("ReelDeal WebGL:",this.webglInitError);return}const t=this.createWebGLProgram(e,`
|
|
2
|
+
attribute vec2 a_pos;
|
|
3
|
+
attribute vec2 a_uv;
|
|
4
|
+
varying vec2 v_uv;
|
|
5
|
+
void main() {
|
|
6
|
+
v_uv = a_uv;
|
|
7
|
+
gl_Position = vec4(a_pos, 0.0, 1.0);
|
|
8
|
+
}
|
|
9
|
+
`,`
|
|
10
|
+
#ifdef GL_FRAGMENT_PRECISION_HIGH
|
|
11
|
+
precision highp float;
|
|
12
|
+
#else
|
|
13
|
+
precision mediump float;
|
|
14
|
+
#endif
|
|
15
|
+
|
|
16
|
+
varying vec2 v_uv;
|
|
17
|
+
uniform sampler2D u_tex;
|
|
18
|
+
uniform float u_reelX;
|
|
19
|
+
uniform float u_reelW;
|
|
20
|
+
uniform float u_offsetPx;
|
|
21
|
+
uniform float u_frameHeight;
|
|
22
|
+
uniform float u_spriteHeight;
|
|
23
|
+
uniform float u_visibleSlots;
|
|
24
|
+
uniform float u_heightScale;
|
|
25
|
+
uniform float u_edgePower;
|
|
26
|
+
uniform float u_warpAngle;
|
|
27
|
+
uniform float u_blurPx;
|
|
28
|
+
|
|
29
|
+
float saturate(float x) { return clamp(x, 0.0, 1.0); }
|
|
30
|
+
|
|
31
|
+
void main() {
|
|
32
|
+
if (v_uv.x < u_reelX || v_uv.x > (u_reelX + u_reelW)) {
|
|
33
|
+
discard;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
float localX = (v_uv.x - u_reelX) / u_reelW;
|
|
37
|
+
// v_uv.y is authored as 0 at TOP, 1 at BOTTOM (Canvas-like).
|
|
38
|
+
float localY = v_uv.y;
|
|
39
|
+
float hs = max(0.0001, u_heightScale);
|
|
40
|
+
float fullY = localY * hs + (1.0 - hs) * 0.5;
|
|
41
|
+
|
|
42
|
+
float warp = max(0.0, u_warpAngle);
|
|
43
|
+
float warpStrength = saturate(warp / 1.2);
|
|
44
|
+
float center = 0.5;
|
|
45
|
+
float centerBand = 1.0 / 3.0;
|
|
46
|
+
float halfBand = centerBand * 0.5;
|
|
47
|
+
float dist = abs(fullY - center);
|
|
48
|
+
// Smoother transition: avoid a hard boundary at halfBand.
|
|
49
|
+
float edgeWeight = smoothstep(halfBand, 0.5, dist);
|
|
50
|
+
float edgePow = max(0.5, u_edgePower);
|
|
51
|
+
edgeWeight = pow(edgeWeight, edgePow) * warpStrength;
|
|
52
|
+
float tEase = smoothstep(0.0, 1.0, fullY);
|
|
53
|
+
float warpedY = mix(fullY, tEase, edgeWeight);
|
|
54
|
+
float y = (warpedY - 0.5) * 2.0;
|
|
55
|
+
float theta = y * warp;
|
|
56
|
+
float edgeWarp = edgeWeight;
|
|
57
|
+
|
|
58
|
+
float yPx = u_offsetPx + warpedY * (u_frameHeight * u_visibleSlots);
|
|
59
|
+
// With UNPACK_FLIP_Y_WEBGL=0 and Canvas-like UVs (v=0 at TOP),
|
|
60
|
+
// v=0 samples the TOP of the original image, so pixel Y-from-top maps directly to v.
|
|
61
|
+
// Normalize early to improve precision on mobile GPUs.
|
|
62
|
+
float v = fract((u_offsetPx / u_spriteHeight) +
|
|
63
|
+
warpedY * ((u_frameHeight * u_visibleSlots) / u_spriteHeight));
|
|
64
|
+
|
|
65
|
+
vec4 base = texture2D(u_tex, vec2(localX, v));
|
|
66
|
+
|
|
67
|
+
if (u_blurPx > 0.0) {
|
|
68
|
+
// Clamp blur step to reduce banding on mobile GPUs.
|
|
69
|
+
// Scale max step with speed proxy (u_blurPx).
|
|
70
|
+
float maxStepPx = 1.0 + 3.0 * saturate(u_blurPx / 4.0);
|
|
71
|
+
float blurPx = min(u_blurPx, maxStepPx);
|
|
72
|
+
float blurStep = blurPx / max(1.0, u_spriteHeight);
|
|
73
|
+
// 9-tap gaussian-ish blur to reduce banding on mobile.
|
|
74
|
+
vec4 sum = base * 0.227027;
|
|
75
|
+
sum += texture2D(u_tex, vec2(localX, fract(v + blurStep))) * 0.1945946;
|
|
76
|
+
sum += texture2D(u_tex, vec2(localX, fract(v - blurStep))) * 0.1945946;
|
|
77
|
+
sum += texture2D(u_tex, vec2(localX, fract(v + 2.0 * blurStep))) * 0.1216216;
|
|
78
|
+
sum += texture2D(u_tex, vec2(localX, fract(v - 2.0 * blurStep))) * 0.1216216;
|
|
79
|
+
sum += texture2D(u_tex, vec2(localX, fract(v + 3.0 * blurStep))) * 0.054054;
|
|
80
|
+
sum += texture2D(u_tex, vec2(localX, fract(v - 3.0 * blurStep))) * 0.054054;
|
|
81
|
+
sum += texture2D(u_tex, vec2(localX, fract(v + 4.0 * blurStep))) * 0.016216;
|
|
82
|
+
sum += texture2D(u_tex, vec2(localX, fract(v - 4.0 * blurStep))) * 0.016216;
|
|
83
|
+
base = sum;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
gl_FragColor = base;
|
|
87
|
+
}
|
|
88
|
+
`);if(!t){this.webglInitError="shader program failed to compile/link (see console)",console.warn("ReelDeal WebGL:",this.webglInitError);return}const r=e.createBuffer();if(!r){this.webglInitError="failed to create vertex buffer",console.warn("ReelDeal WebGL:",this.webglInitError),e.deleteProgram(t);return}e.bindBuffer(e.ARRAY_BUFFER,r);const o=new Float32Array([-1,-1,0,1,1,-1,1,1,-1,1,0,0,1,1,1,0]);e.bufferData(e.ARRAY_BUFFER,o,e.STATIC_DRAW);const n=e.createTexture();if(!n){this.webglInitError="failed to create texture",console.warn("ReelDeal WebGL:",this.webglInitError),e.deleteBuffer(r),e.deleteProgram(t);return}e.bindTexture(e.TEXTURE_2D,n),e.pixelStorei(e.UNPACK_FLIP_Y_WEBGL,0),e.texImage2D(e.TEXTURE_2D,0,e.RGBA,e.RGBA,e.UNSIGNED_BYTE,this.spriteImg);const c=u=>u>0&&(u&u-1)===0;c(this.spriteWidth)&&c(this.spriteHeight)?(e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_S,e.REPEAT),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_T,e.REPEAT),e.generateMipmap(e.TEXTURE_2D),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MIN_FILTER,e.LINEAR_MIPMAP_LINEAR),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MAG_FILTER,e.LINEAR)):(e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MIN_FILTER,e.LINEAR),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MAG_FILTER,e.LINEAR),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_S,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_T,e.CLAMP_TO_EDGE)),e.useProgram(t),e.clearColor(0,0,0,0),e.enable(e.BLEND),e.blendFunc(e.SRC_ALPHA,e.ONE_MINUS_SRC_ALPHA),this.gl=e,this.glProgram=t,this.glBuffer=r,this.glTexture=n,this.glAttribs={pos:e.getAttribLocation(t,"a_pos"),uv:e.getAttribLocation(t,"a_uv")},this.glUniforms={texture:e.getUniformLocation(t,"u_tex"),reelX:e.getUniformLocation(t,"u_reelX"),reelW:e.getUniformLocation(t,"u_reelW"),offsetPx:e.getUniformLocation(t,"u_offsetPx"),frameHeight:e.getUniformLocation(t,"u_frameHeight"),spriteHeight:e.getUniformLocation(t,"u_spriteHeight"),visibleSlots:e.getUniformLocation(t,"u_visibleSlots"),heightScale:e.getUniformLocation(t,"u_heightScale"),edgePower:e.getUniformLocation(t,"u_edgePower"),warpAngle:e.getUniformLocation(t,"u_warpAngle"),blurPx:e.getUniformLocation(t,"u_blurPx")}}createWebGLProgram(e,i,s){const t=this.createWebGLShader(e,e.VERTEX_SHADER,i),r=this.createWebGLShader(e,e.FRAGMENT_SHADER,s);if(!t||!r)return t&&e.deleteShader(t),r&&e.deleteShader(r),null;const o=e.createProgram();if(!o)return null;if(e.attachShader(o,t),e.attachShader(o,r),e.linkProgram(o),!e.getProgramParameter(o,e.LINK_STATUS)){const n=e.getProgramInfoLog(o)??"unknown link error";return this.webglInitError=`program link failed: ${n}`,console.warn("ReelDeal WebGL: program link failed",n),e.deleteProgram(o),e.deleteShader(t),e.deleteShader(r),null}return e.detachShader(o,t),e.detachShader(o,r),e.deleteShader(t),e.deleteShader(r),o}createWebGLShader(e,i,s){const t=e.createShader(i);if(!t)return null;if(e.shaderSource(t,s),e.compileShader(t),!e.getShaderParameter(t,e.COMPILE_STATUS)){const r=e.getShaderInfoLog(t)??"unknown compile error";return this.webglInitError=`shader compile failed: ${r}`,console.warn("ReelDeal WebGL: shader compile failed",r),e.deleteShader(t),null}return t}releaseWebGLResources(){this.gl&&(this.glTexture&&this.gl.deleteTexture(this.glTexture),this.glBuffer&&this.gl.deleteBuffer(this.glBuffer),this.glProgram&&this.gl.deleteProgram(this.glProgram),this.glTexture=null,this.glBuffer=null,this.glProgram=null,this.glAttribs=null,this.glUniforms=null,this.gl=null)}}exports.ReelDeal=j;exports.defaultIdleBob=g;exports.defaultSpinBlur=T;exports.defaultSpinConfig=p;exports.defaultWebGLShading=E;
|
|
89
|
+
//# sourceMappingURL=index.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs.js","sources":["../src/defaults.ts","../src/utils/math.ts","../src/normalize.ts","../src/utils/dom.ts","../src/ReelDeal.ts"],"sourcesContent":["import type {\n ReelDealIdleBob,\n ReelDealSpinBlur,\n ReelDealSpinConfig,\n ReelDealWebGLShading,\n} from './types';\n\nexport const defaultSpinConfig: ReelDealSpinConfig = {\n minSpins: 2,\n durationMs: 4000,\n speedPxS: 4000,\n staggerMs: 300,\n};\n\nexport const defaultWebGLShading: ReelDealWebGLShading = {\n warpAngleDeg: 90,\n edgePower: 0.5,\n};\n\nexport const defaultSpinBlur: ReelDealSpinBlur = {\n maxPx: 4,\n speedAtMax: 2200,\n};\n\nexport const defaultIdleBob: ReelDealIdleBob = {\n amplitudePx: 8,\n speedHz: 0.25,\n phaseOffsetRad: 0.9,\n delayMs: 500,\n rampMs: 800,\n};\n","const SPIN_ACCEL_END = 0.1;\nconst SPIN_DECEL_START = 0.6;\n\nexport const clampInt = (value: number, min: number, max: number): number =>\n Math.max(min, Math.min(max, Math.floor(value)));\n\nexport const mod = (n: number, m: number): number => ((n % m) + m) % m;\n\nexport const clamp01 = (t: number): number => Math.max(0, Math.min(1, t));\n\nexport const rampHermite = (u: number): number => {\n const x = clamp01(u);\n\n return x ** 2 * (2 - x);\n};\n\nexport const brakeHermite = (u: number): number => {\n const x = clamp01(u);\n\n return x + x ** 2 - x ** 3;\n};\n\nexport const reelProgress = (t: number): number => {\n const tt = clamp01(t);\n\n if (tt <= SPIN_ACCEL_END) {\n const u = tt / SPIN_ACCEL_END;\n\n return rampHermite(u) * SPIN_ACCEL_END;\n }\n\n if (tt < SPIN_DECEL_START) {\n return tt;\n }\n\n const u = (tt - SPIN_DECEL_START) / (1 - SPIN_DECEL_START);\n\n return SPIN_DECEL_START + brakeHermite(u) * (1 - SPIN_DECEL_START);\n};\n\nexport const finiteOr = (value: number | undefined, fallback: number): number =>\n typeof value === 'number' && Number.isFinite(value) ? value : fallback;\n\nexport const nonNegative = (value: number | undefined, fallback: number): number =>\n Math.max(0, finiteOr(value, fallback));\n\nexport const nonNegativeInt = (value: number | undefined, fallback: number): number =>\n Math.max(0, Math.floor(finiteOr(value, fallback)));\n","import {\n defaultIdleBob,\n defaultSpinBlur,\n defaultSpinConfig,\n defaultWebGLShading,\n} from './defaults';\nimport type {\n ReelDealIdleBob,\n ReelDealSpinBlur,\n ReelDealSpinConfig,\n ReelDealWebGLShading,\n} from './types';\nimport { finiteOr, nonNegative, nonNegativeInt } from './utils/math';\n\nconst DEFAULT_MAX_DPR = 2;\n\nexport type NormalizedSpinConfig = Required<ReelDealSpinConfig>;\n\nexport const normalizeSpinConfig = (\n overrides?: Partial<ReelDealSpinConfig>,\n): NormalizedSpinConfig => {\n const base = { ...defaultSpinConfig, ...(overrides ?? {}) };\n\n return {\n minSpins: nonNegativeInt(base.minSpins, defaultSpinConfig.minSpins),\n durationMs: nonNegativeInt(base.durationMs, defaultSpinConfig.durationMs ?? 0),\n speedPxS: nonNegativeInt(base.speedPxS, defaultSpinConfig.speedPxS ?? 0),\n staggerMs: nonNegativeInt(base.staggerMs, defaultSpinConfig.staggerMs ?? 0),\n };\n};\n\nexport const normalizeSpinBlur = (overrides?: Partial<ReelDealSpinBlur>): ReelDealSpinBlur => {\n const base = { ...defaultSpinBlur, ...(overrides ?? {}) };\n\n return {\n maxPx: nonNegative(base.maxPx, defaultSpinBlur.maxPx),\n speedAtMax: nonNegative(base.speedAtMax, defaultSpinBlur.speedAtMax),\n };\n};\n\nexport const normalizeWebGLShading = (\n overrides?: Partial<ReelDealWebGLShading>,\n): ReelDealWebGLShading => {\n const base = { ...defaultWebGLShading, ...(overrides ?? {}) };\n\n return {\n warpAngleDeg: nonNegative(base.warpAngleDeg, defaultWebGLShading.warpAngleDeg),\n edgePower: nonNegative(base.edgePower, defaultWebGLShading.edgePower),\n };\n};\n\nexport const normalizeIdleBob = (\n overrides?: Partial<ReelDealIdleBob> | false,\n): ReelDealIdleBob | null => {\n if (overrides === false) return null;\n\n const base = { ...defaultIdleBob, ...(overrides ?? {}) };\n\n return {\n amplitudePx: nonNegative(base.amplitudePx, defaultIdleBob.amplitudePx),\n speedHz: nonNegative(base.speedHz, defaultIdleBob.speedHz),\n phaseOffsetRad: finiteOr(base.phaseOffsetRad, defaultIdleBob.phaseOffsetRad),\n delayMs: nonNegativeInt(base.delayMs, defaultIdleBob.delayMs),\n rampMs: nonNegativeInt(base.rampMs, defaultIdleBob.rampMs),\n };\n};\n\nexport const normalizeMaxDpr = (value?: number): number =>\n Math.max(1, finiteOr(value, DEFAULT_MAX_DPR));\n","export const resolveElementById = <T extends HTMLElement>(id: string, kind: string): T => {\n const el = document.getElementById(id);\n\n if (!el) {\n throw new Error(`${kind} not found: ${id}`);\n }\n\n return el as T;\n};\n\nexport const resolveElement = <T extends HTMLElement>(elOrId: T | string, kind: string): T => {\n if (typeof elOrId === 'string') return resolveElementById<T>(elOrId, kind);\n\n return elOrId;\n};\n","import {\n type NormalizedSpinConfig,\n normalizeIdleBob,\n normalizeMaxDpr,\n normalizeSpinBlur,\n normalizeSpinConfig,\n normalizeWebGLShading,\n} from './normalize';\nimport type {\n ReelDealIdleBob,\n ReelDealOptions,\n ReelDealSpinBlur,\n ReelDealSpinConfig,\n ReelDealSpinState,\n ReelDealWebGLShading,\n} from './types';\nimport { resolveElement } from './utils/dom';\nimport { clamp01, clampInt, mod, rampHermite, reelProgress } from './utils/math';\n\nconst MAX_REELS = 5;\nconst SPIN_TICK_START = 0.72;\nconst SPIN_TICK_RANGE = 0.28;\nconst SPIN_TICK_MAX_PX = 7;\nconst VELOCITY_DECAY = 0.85;\nconst VELOCITY_GAIN = 1 - VELOCITY_DECAY;\n\ntype SpinAnimation = {\n startTime: number;\n startOffsets: number[];\n deltas: number[];\n durationMs: number[];\n};\n\nexport class ReelDeal {\n private readonly canvas: HTMLCanvasElement;\n private readonly container: HTMLElement;\n\n private readonly opts: ReelDealOptions;\n private readonly visibleSlots = 3;\n private readonly heightScale = 2 / 3;\n private readonly centerIndex = 1;\n private readonly reels: number;\n private readonly spinConfig: NormalizedSpinConfig;\n private readonly spinBlur: ReelDealSpinBlur;\n private readonly webglShading: ReelDealWebGLShading;\n private readonly idleBob: ReelDealIdleBob | null;\n private readonly maxDpr: number;\n\n private spriteImg: HTMLImageElement | null = null;\n private spriteWidth = 0;\n private spriteHeight = 0;\n private frameHeight = 0;\n\n private width = 0;\n private height = 0;\n private dpr = 1;\n\n private offsets: number[];\n private velocities: number[];\n private lastVelT: number | null = null;\n private lastVelOffsets: number[];\n private idleStartTime: number | null = null;\n private idleBaseOffsets: number[];\n\n private gl: WebGLRenderingContext | WebGL2RenderingContext | null = null;\n private glProgram: WebGLProgram | null = null;\n private glBuffer: WebGLBuffer | null = null;\n private glTexture: WebGLTexture | null = null;\n private glAttribs: { pos: number; uv: number } | null = null;\n private webglInitError: string | null = null;\n private glUniforms: {\n texture: WebGLUniformLocation | null;\n reelX: WebGLUniformLocation | null;\n reelW: WebGLUniformLocation | null;\n offsetPx: WebGLUniformLocation | null;\n frameHeight: WebGLUniformLocation | null;\n spriteHeight: WebGLUniformLocation | null;\n visibleSlots: WebGLUniformLocation | null;\n heightScale: WebGLUniformLocation | null;\n edgePower: WebGLUniformLocation | null;\n warpAngle: WebGLUniformLocation | null;\n blurPx: WebGLUniformLocation | null;\n } | null = null;\n\n private anim: SpinAnimation | null = null;\n private rafId: number | null = null;\n private pendingSpinResolvers: Array<() => void> = [];\n\n private resizeObserver: ResizeObserver | null = null;\n private resizeQueued = false;\n\n private button: HTMLButtonElement | null = null;\n private buttonHandlerAttached = false;\n private spinning = false;\n private queuedSpins: ReelDealSpinState[] | null = null;\n\n constructor(options: ReelDealOptions) {\n this.opts = options;\n this.reels = clampInt(options.reels ?? 1, 1, MAX_REELS);\n this.spinConfig = normalizeSpinConfig(options.spinConfig);\n this.spinBlur = normalizeSpinBlur(options.spinBlur);\n this.webglShading = normalizeWebGLShading(options.webglShading);\n this.idleBob = normalizeIdleBob(options.idleBob);\n this.maxDpr = normalizeMaxDpr(options.maxDpr);\n\n this.canvas = resolveElement<HTMLCanvasElement>(options.canvas, 'Canvas');\n this.container = resolveElement<HTMLElement>(options.container, 'Container');\n\n this.offsets = new Array(this.reels).fill(0);\n this.velocities = new Array(this.reels).fill(0);\n this.lastVelOffsets = new Array(this.reels).fill(0);\n this.idleBaseOffsets = new Array(this.reels).fill(0);\n }\n\n async init(): Promise<void> {\n await this.loadSprite();\n\n this.tryInitWebGL();\n\n if (!this.gl) {\n throw new Error(\n `WebGL is not supported or failed to initialize. ${this.webglInitError ?? ''}`.trim(),\n );\n }\n\n this.syncContainerLayoutVars();\n this.ensureCanvasCoversContainer();\n this.setupResizeWatcher();\n this.resizeToContainer();\n\n this.setSegments(this.opts.initialSegments);\n\n this.button = this.resolveButton();\n this.queuedSpins = this.resolveSpinQueue();\n this.attachButtonHandler();\n\n this.render();\n this.startLoop();\n }\n\n destroy(): void {\n this.stop();\n\n if (this.resizeObserver) {\n this.resizeObserver.disconnect();\n this.resizeObserver = null;\n }\n\n if (this.button && this.buttonHandlerAttached) {\n this.button.removeEventListener('click', this.handleButtonClick);\n this.buttonHandlerAttached = false;\n }\n\n this.releaseWebGLResources();\n }\n\n private ensureCanvasCoversContainer(): void {\n if (this.canvas === this.container) return;\n if (!this.container.contains(this.canvas)) return;\n\n if (!this.canvas.style.width) this.canvas.style.width = '100%';\n if (!this.canvas.style.height) this.canvas.style.height = '100%';\n if (!this.canvas.style.display) this.canvas.style.display = 'block';\n }\n\n private syncContainerLayoutVars(): void {\n const target = this.canvas === this.container ? this.canvas : this.container;\n\n target.style.setProperty('--reels', String(this.reels));\n target.style.setProperty('--visible-slots', String(this.visibleSlots));\n target.style.aspectRatio = `${this.reels} / ${this.visibleSlots * this.heightScale}`;\n\n if (this.canvas !== this.container) {\n this.container.style.overflow = 'hidden';\n }\n }\n\n setSegments(initialSegments?: number[]): void {\n this.ensureReady();\n\n const segments = initialSegments ?? [];\n\n for (let r = 0; r < this.reels; r += 1) {\n const segIndex = segments[r] ?? 0;\n const idx = clampInt(segIndex, 0, this.opts.slotCount - 1);\n\n this.offsets[r] = idx * this.frameHeight - this.centerIndex * this.frameHeight;\n this.lastVelOffsets[r] = this.offsets[r];\n this.velocities[r] = 0;\n this.idleBaseOffsets[r] = this.offsets[r];\n }\n\n this.lastVelT = null;\n this.idleStartTime = null;\n this.anim = null;\n\n this.stop();\n this.render();\n this.startLoop();\n }\n\n setSegment(segmentIndex: number): void {\n this.setSegments(new Array(this.reels).fill(segmentIndex));\n }\n\n spinOnce(stopAtSegments: number[], config?: Partial<ReelDealSpinConfig>): Promise<void> {\n this.ensureReady();\n\n const { minSpins, staggerMs, speedPxS, durationMs } = this.getSpinConfig(config);\n const useSpeed = speedPxS > 0;\n\n const startOffsets = new Array(this.reels);\n const deltas = new Array(this.reels);\n const durations = new Array(this.reels);\n\n for (let r = 0; r < this.reels; r += 1) {\n const idx = clampInt(stopAtSegments[r] ?? 0, 0, this.opts.slotCount - 1);\n const desiredTop = idx * this.frameHeight - this.centerIndex * this.frameHeight;\n const desired = mod(desiredTop, this.spriteHeight);\n const current = mod(this.offsets[r], this.spriteHeight);\n\n const alignDelta = mod(current - desired, this.spriteHeight);\n\n let delta = -(minSpins * this.spriteHeight + alignDelta);\n\n if (useSpeed && durationMs > 0) {\n const desiredDistance = (speedPxS * durationMs) / 1000;\n const loops = Math.max(\n minSpins,\n Math.ceil(Math.max(0, desiredDistance - alignDelta) / this.spriteHeight),\n );\n\n delta = -(alignDelta + loops * this.spriteHeight);\n }\n\n if (staggerMs > 0 && useSpeed) {\n const extraDelta = (speedPxS * staggerMs * r) / 1000;\n const extraLoops = Math.ceil(extraDelta / this.spriteHeight);\n\n delta -= extraLoops * this.spriteHeight;\n }\n\n startOffsets[r] = this.offsets[r];\n deltas[r] = delta;\n\n if (useSpeed) {\n durations[r] = Math.max(0, Math.round((Math.abs(delta) / speedPxS) * 1000));\n } else {\n durations[r] = Math.max(0, durationMs + staggerMs * r);\n }\n }\n\n const totalDelta = deltas.reduce((acc, v) => acc + Math.abs(v), 0);\n\n if (totalDelta === 0 || durations.every((entry) => entry <= 0)) {\n for (let r = 0; r < this.reels; r += 1) {\n this.offsets[r] = this.offsets[r] + deltas[r];\n }\n\n this.anim = null;\n this.resolvePendingSpins();\n this.render();\n\n return Promise.resolve();\n }\n\n const startTime = performance.now();\n\n this.anim = { startTime, startOffsets, deltas, durationMs: durations };\n this.idleStartTime = null;\n this.startLoop();\n\n return new Promise((resolve) => {\n this.pendingSpinResolvers.push(resolve);\n });\n }\n\n private getSpinConfig(overrides?: Partial<ReelDealSpinConfig>): NormalizedSpinConfig {\n if (!overrides) return this.spinConfig;\n\n return normalizeSpinConfig({ ...this.spinConfig, ...overrides });\n }\n\n async spinQueue(spins: ReelDealSpinState[]): Promise<void> {\n for (let i = 0; i < spins.length; i += 1) {\n const spin = spins[i];\n\n await this.spinOnce(spin.stopAtSegments);\n\n spin.callback?.(i, spin.stopAtSegments);\n }\n }\n\n stop(): void {\n if (this.rafId !== null) cancelAnimationFrame(this.rafId);\n\n this.rafId = null;\n this.anim = null;\n this.resolvePendingSpins();\n this.lastVelT = null;\n\n for (let r = 0; r < this.reels; r += 1) {\n this.velocities[r] = 0;\n }\n }\n\n private resolvePendingSpins(): void {\n if (this.pendingSpinResolvers.length === 0) return;\n\n const resolvers = this.pendingSpinResolvers;\n\n this.pendingSpinResolvers = [];\n\n for (const resolve of resolvers) {\n resolve();\n }\n }\n\n requestResize(): void {\n this.queueResize();\n }\n\n private ensureReady(): void {\n if (\n !this.spriteImg ||\n this.spriteWidth <= 0 ||\n this.spriteHeight <= 0 ||\n this.frameHeight <= 0\n ) {\n throw new Error('Sprite is not ready. Call init() and await it first.');\n }\n }\n\n private attachButtonHandler(): void {\n if (!this.button || this.buttonHandlerAttached) return;\n\n this.button.addEventListener('click', this.handleButtonClick, { passive: true });\n this.buttonHandlerAttached = true;\n }\n\n private handleButtonClick = async (): Promise<void> => {\n if (this.spinning) return;\n\n const spin = this.consumeNextSpin() ?? {\n stopAtSegments: this.buildRandomSegments(),\n };\n\n this.spinning = true;\n this.setButtonDisabled(true);\n\n try {\n await this.spinOnce(spin.stopAtSegments);\n } finally {\n if (this.queuedSpins && this.queuedSpins.length === 0) {\n this.setButtonDisabled(true);\n } else {\n this.setButtonDisabled(false);\n }\n\n this.spinning = false;\n }\n };\n\n private resolveButton(): HTMLButtonElement | null {\n if (!this.opts.button) return null;\n\n return resolveElement<HTMLButtonElement>(this.opts.button, 'Button');\n }\n\n private resolveSpinQueue(): ReelDealSpinState[] | null {\n return this.opts.queuedSpinStates ?? null;\n }\n\n private consumeNextSpin(): ReelDealSpinState | null {\n if (!this.queuedSpins) {\n this.queuedSpins = this.resolveSpinQueue();\n }\n\n if (!this.queuedSpins || this.queuedSpins.length === 0) return null;\n\n return this.queuedSpins.shift() ?? null;\n }\n\n private buildRandomSegments(): number[] {\n const max = Math.max(1, Math.floor(this.opts.slotCount));\n const segments: number[] = [];\n\n for (let r = 0; r < this.reels; r += 1) {\n segments.push(Math.floor(Math.random() * max));\n }\n\n return segments;\n }\n\n private setButtonDisabled(disabled: boolean): void {\n if (!this.button) return;\n\n this.button.disabled = disabled;\n\n if (disabled) this.button.setAttribute('aria-busy', 'true');\n else this.button.removeAttribute('aria-busy');\n }\n\n private shouldAnimate(): boolean {\n return this.anim !== null || this.idleBob !== null;\n }\n\n private startLoop(): void {\n if (this.rafId !== null) return;\n if (!this.shouldAnimate()) return;\n\n this.rafId = requestAnimationFrame(this.loop);\n }\n\n private loop = (now: number): void => {\n if (this.anim) {\n const { startTime, durationMs, startOffsets, deltas } = this.anim;\n\n let allDone = true;\n\n for (let r = 0; r < this.reels; r += 1) {\n const duration = durationMs[r] ?? 0;\n const t = duration <= 0 ? 1 : Math.min(1, (now - startTime) / duration);\n const eased = reelProgress(t);\n\n if (t < 1) allDone = false;\n\n const baseOffset = startOffsets[r] + deltas[r] * eased;\n\n let tick = 0;\n\n if (t > SPIN_TICK_START && this.frameHeight > 0) {\n const u = clamp01((t - SPIN_TICK_START) / SPIN_TICK_RANGE);\n const env = (1 - u) ** 2;\n const phase = (baseOffset / this.frameHeight) * Math.PI * 2;\n const amp = Math.min(SPIN_TICK_MAX_PX, this.frameHeight * 0.06);\n\n tick = Math.sin(phase) * env * amp;\n }\n\n this.offsets[r] = baseOffset + tick;\n }\n\n this.updateVelocity(now);\n this.render();\n\n if (allDone) {\n for (let r = 0; r < this.reels; r += 1) {\n this.offsets[r] = this.anim.startOffsets[r] + this.anim.deltas[r];\n this.idleBaseOffsets[r] = this.offsets[r];\n }\n\n this.updateVelocity(now);\n\n for (let r = 0; r < this.reels; r += 1) {\n this.velocities[r] = 0;\n }\n\n this.anim = null;\n this.idleStartTime = null;\n\n this.render();\n this.resolvePendingSpins();\n }\n } else if (this.idleBob) {\n this.updateIdle(now);\n this.render();\n }\n\n if (!this.shouldAnimate()) {\n this.rafId = null;\n\n return;\n }\n\n if (this.rafId === null) return;\n\n this.rafId = requestAnimationFrame(this.loop);\n };\n\n private render(): void {\n if (!this.spriteImg) return;\n if (this.width <= 0 || this.height <= 0) return;\n if (!this.gl) return;\n\n this.renderWebGL();\n }\n\n private getSpinBlurPxForReel(reel: number): number {\n if (!this.anim) return 0;\n\n const speed = Math.abs(this.velocities[reel] ?? 0);\n const speedAtMax = Math.max(1, this.spinBlur.speedAtMax);\n const t = clamp01(speed / speedAtMax);\n\n return t * Math.max(0, this.spinBlur.maxPx);\n }\n\n private renderWebGL(): void {\n if (\n !this.gl ||\n !this.glProgram ||\n !this.glAttribs ||\n !this.glUniforms ||\n !this.glTexture ||\n !this.glBuffer\n ) {\n return;\n }\n\n if (!this.spriteImg) return;\n\n const gl = this.gl;\n\n gl.useProgram(this.glProgram);\n gl.bindBuffer(gl.ARRAY_BUFFER, this.glBuffer);\n\n gl.enableVertexAttribArray(this.glAttribs.pos);\n gl.vertexAttribPointer(this.glAttribs.pos, 2, gl.FLOAT, false, 16, 0);\n gl.enableVertexAttribArray(this.glAttribs.uv);\n gl.vertexAttribPointer(this.glAttribs.uv, 2, gl.FLOAT, false, 16, 8);\n\n gl.activeTexture(gl.TEXTURE0);\n gl.bindTexture(gl.TEXTURE_2D, this.glTexture);\n gl.uniform1i(this.glUniforms.texture, 0);\n\n gl.uniform1f(this.glUniforms.frameHeight, this.frameHeight);\n gl.uniform1f(this.glUniforms.spriteHeight, this.spriteHeight);\n gl.uniform1f(this.glUniforms.visibleSlots, this.visibleSlots);\n gl.uniform1f(this.glUniforms.heightScale, this.heightScale);\n gl.uniform1f(this.glUniforms.edgePower, Math.max(0.5, this.webglShading.edgePower));\n\n const shading = this.webglShading;\n const warpAngle = (Math.max(0, shading.warpAngleDeg) * Math.PI) / 180;\n\n gl.uniform1f(this.glUniforms.warpAngle, warpAngle);\n gl.clear(gl.COLOR_BUFFER_BIT);\n\n const reelW = 1 / this.reels;\n\n for (let r = 0; r < this.reels; r += 1) {\n gl.uniform1f(this.glUniforms.reelX, r * reelW);\n gl.uniform1f(this.glUniforms.reelW, reelW);\n\n const offsetPx = mod(this.offsets[r], this.spriteHeight);\n\n gl.uniform1f(this.glUniforms.offsetPx, offsetPx);\n gl.uniform1f(this.glUniforms.blurPx, this.getSpinBlurPxForReel(r));\n gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);\n }\n }\n\n private updateVelocity(now: number): void {\n if (this.lastVelT === null) {\n this.lastVelT = now;\n\n for (let r = 0; r < this.reels; r += 1) {\n this.lastVelOffsets[r] = this.offsets[r];\n this.velocities[r] = 0;\n }\n\n return;\n }\n\n const dt = (now - this.lastVelT) / 1000;\n\n if (dt <= 0) return;\n\n for (let r = 0; r < this.reels; r += 1) {\n const v = (this.offsets[r] - this.lastVelOffsets[r]) / dt;\n\n this.velocities[r] = this.velocities[r] * VELOCITY_DECAY + v * VELOCITY_GAIN;\n this.lastVelOffsets[r] = this.offsets[r];\n }\n\n this.lastVelT = now;\n }\n\n private updateIdle(now: number): void {\n if (!this.idleBob) return;\n if (this.idleStartTime === null) this.idleStartTime = now;\n\n const { amplitudePx, speedHz, phaseOffsetRad, delayMs, rampMs } = this.idleBob;\n const elapsedMs = now - this.idleStartTime;\n\n if (elapsedMs < Math.max(0, delayMs)) {\n for (let r = 0; r < this.reels; r += 1) {\n this.offsets[r] = this.idleBaseOffsets[r];\n this.velocities[r] = 0;\n }\n\n return;\n }\n\n const tSec = (elapsedMs - Math.max(0, delayMs)) / 1000;\n const rampT = Math.max(\n 0,\n Math.min(1, (elapsedMs - Math.max(0, delayMs)) / Math.max(1, rampMs)),\n );\n const ramp = rampHermite(rampT);\n const omega = speedHz * Math.PI * 2;\n\n for (let r = 0; r < this.reels; r += 1) {\n const phase = tSec * omega + r * phaseOffsetRad;\n\n this.offsets[r] = this.idleBaseOffsets[r] + Math.sin(phase) * (amplitudePx * ramp);\n this.velocities[r] = 0;\n }\n }\n\n private async loadSprite(): Promise<void> {\n const { sprite } = this.opts;\n const img = typeof sprite === 'string' ? new Image() : sprite;\n\n if (typeof sprite === 'string') {\n img.src = sprite;\n }\n\n if (!img.complete || img.naturalWidth <= 0) {\n await new Promise<void>((resolve, reject) => {\n const onLoad = () => resolve();\n const onError = () => reject(new Error('Failed to load sprite image'));\n\n img.addEventListener('load', onLoad, { once: true });\n img.addEventListener('error', onError, { once: true });\n });\n }\n\n const slotCount = Math.max(1, Math.floor(this.opts.slotCount));\n const spriteWidth = img.naturalWidth;\n const frameHeight = spriteWidth;\n const expectedSpriteHeight = frameHeight * slotCount;\n\n const naturalHeight = img.naturalHeight;\n const diff = Math.abs(naturalHeight - expectedSpriteHeight);\n\n if (diff > 2) {\n throw new Error(\n `Sprite size mismatch. Expected height ~${Math.round(expectedSpriteHeight)}px (slotCount=${slotCount}, slot aspect 1/1), got ${naturalHeight}px.`,\n );\n }\n\n this.spriteImg = img;\n this.spriteWidth = spriteWidth;\n this.frameHeight = Math.max(1, Math.round(frameHeight));\n this.spriteHeight = Math.max(1, Math.round(this.frameHeight * slotCount));\n }\n\n private setupResizeWatcher(): void {\n if (typeof ResizeObserver === 'undefined') return;\n if (this.resizeObserver) return;\n\n this.resizeObserver = new ResizeObserver(() => this.queueResize());\n this.resizeObserver.observe(this.container);\n }\n\n private queueResize = (): void => {\n if (this.resizeQueued) return;\n\n this.resizeQueued = true;\n\n requestAnimationFrame(() => {\n this.resizeQueued = false;\n this.resizeToContainer();\n });\n };\n\n private resizeToContainer(): void {\n const cssWidth = this.container.clientWidth;\n const cssHeight = this.container.clientHeight;\n const slotsViewport = this.visibleSlots * this.heightScale;\n\n if (cssWidth <= 0 || cssHeight <= 0) {\n this.applyResize(1, 1);\n\n return;\n }\n\n const slots = slotsViewport;\n const reels = this.reels;\n const fitByWidthH = (cssWidth / reels) * slots;\n\n let w = cssWidth;\n let h = fitByWidthH;\n\n if (h > cssHeight) {\n h = cssHeight;\n w = (cssHeight / slots) * reels;\n }\n\n const viewportWidth = Math.max(1, Math.floor(w));\n const viewportHeight = Math.max(1, Math.floor(h));\n\n this.applyResize(viewportWidth, viewportHeight);\n this.render();\n }\n\n private applyViewportCrop(): void {\n this.canvas.style.width = '100%';\n this.canvas.style.height = '100%';\n this.canvas.style.transform = '';\n }\n\n private applyResize(width: number, height: number): void {\n const normalizedWidth = Math.max(1, Math.floor(width));\n const normalizedHeight = Math.max(1, Math.floor(height));\n\n const dpr = this.getTargetDpr();\n\n this.width = normalizedWidth;\n this.height = normalizedHeight;\n this.dpr = dpr;\n\n this.canvas.width = Math.floor(this.width * this.dpr);\n this.canvas.height = Math.floor(this.height * this.dpr);\n\n if (this.gl) {\n this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);\n }\n\n this.applyViewportCrop();\n }\n\n private getTargetDpr(): number {\n const rawDpr = window.devicePixelRatio || 1;\n const base = Math.max(1, rawDpr);\n\n return Math.min(this.maxDpr, base);\n }\n\n private tryInitWebGL(): void {\n if (!this.spriteImg) return;\n\n this.releaseWebGLResources();\n this.webglInitError = null;\n\n const gl =\n (this.canvas.getContext('webgl2', {\n antialias: true,\n alpha: true,\n }) as WebGL2RenderingContext | null) ??\n (this.canvas.getContext('webgl', {\n antialias: true,\n alpha: true,\n }) as WebGLRenderingContext | null) ??\n (this.canvas.getContext('experimental-webgl', {\n antialias: true,\n alpha: true,\n }) as WebGLRenderingContext | null);\n\n if (!gl) {\n const fallback = document.createElement('canvas');\n const canWebGL = typeof WebGLRenderingContext !== 'undefined';\n const fallbackGl =\n (fallback.getContext('webgl2') as WebGL2RenderingContext | null) ??\n (fallback.getContext('webgl') as WebGLRenderingContext | null);\n\n this.webglInitError = `context is null (browser WebGL=${canWebGL ? 'yes' : 'no'}, fallback canvas=${fallbackGl ? 'yes' : 'no'})`;\n\n console.warn('ReelDeal WebGL:', this.webglInitError);\n\n return;\n }\n\n const vsSource = `\n attribute vec2 a_pos;\n attribute vec2 a_uv;\n varying vec2 v_uv;\n void main() {\n v_uv = a_uv;\n gl_Position = vec4(a_pos, 0.0, 1.0);\n }\n `;\n\n const fsSource = `\n #ifdef GL_FRAGMENT_PRECISION_HIGH\n precision highp float;\n #else\n precision mediump float;\n #endif\n\n varying vec2 v_uv;\n uniform sampler2D u_tex;\n uniform float u_reelX;\n uniform float u_reelW;\n uniform float u_offsetPx;\n uniform float u_frameHeight;\n uniform float u_spriteHeight;\n uniform float u_visibleSlots;\n uniform float u_heightScale;\n uniform float u_edgePower;\n uniform float u_warpAngle;\n uniform float u_blurPx;\n\n float saturate(float x) { return clamp(x, 0.0, 1.0); }\n\n void main() {\n if (v_uv.x < u_reelX || v_uv.x > (u_reelX + u_reelW)) {\n discard;\n }\n\n float localX = (v_uv.x - u_reelX) / u_reelW;\n // v_uv.y is authored as 0 at TOP, 1 at BOTTOM (Canvas-like).\n float localY = v_uv.y;\n float hs = max(0.0001, u_heightScale);\n float fullY = localY * hs + (1.0 - hs) * 0.5;\n\n float warp = max(0.0, u_warpAngle);\n float warpStrength = saturate(warp / 1.2);\n float center = 0.5;\n float centerBand = 1.0 / 3.0;\n float halfBand = centerBand * 0.5;\n float dist = abs(fullY - center);\n // Smoother transition: avoid a hard boundary at halfBand.\n float edgeWeight = smoothstep(halfBand, 0.5, dist);\n float edgePow = max(0.5, u_edgePower);\n edgeWeight = pow(edgeWeight, edgePow) * warpStrength;\n float tEase = smoothstep(0.0, 1.0, fullY);\n float warpedY = mix(fullY, tEase, edgeWeight);\n float y = (warpedY - 0.5) * 2.0;\n float theta = y * warp;\n float edgeWarp = edgeWeight;\n\n float yPx = u_offsetPx + warpedY * (u_frameHeight * u_visibleSlots);\n // With UNPACK_FLIP_Y_WEBGL=0 and Canvas-like UVs (v=0 at TOP),\n // v=0 samples the TOP of the original image, so pixel Y-from-top maps directly to v.\n // Normalize early to improve precision on mobile GPUs.\n float v = fract((u_offsetPx / u_spriteHeight) +\n warpedY * ((u_frameHeight * u_visibleSlots) / u_spriteHeight));\n\n vec4 base = texture2D(u_tex, vec2(localX, v));\n\n if (u_blurPx > 0.0) {\n // Clamp blur step to reduce banding on mobile GPUs.\n // Scale max step with speed proxy (u_blurPx).\n float maxStepPx = 1.0 + 3.0 * saturate(u_blurPx / 4.0);\n float blurPx = min(u_blurPx, maxStepPx);\n float blurStep = blurPx / max(1.0, u_spriteHeight);\n // 9-tap gaussian-ish blur to reduce banding on mobile.\n vec4 sum = base * 0.227027;\n sum += texture2D(u_tex, vec2(localX, fract(v + blurStep))) * 0.1945946;\n sum += texture2D(u_tex, vec2(localX, fract(v - blurStep))) * 0.1945946;\n sum += texture2D(u_tex, vec2(localX, fract(v + 2.0 * blurStep))) * 0.1216216;\n sum += texture2D(u_tex, vec2(localX, fract(v - 2.0 * blurStep))) * 0.1216216;\n sum += texture2D(u_tex, vec2(localX, fract(v + 3.0 * blurStep))) * 0.054054;\n sum += texture2D(u_tex, vec2(localX, fract(v - 3.0 * blurStep))) * 0.054054;\n sum += texture2D(u_tex, vec2(localX, fract(v + 4.0 * blurStep))) * 0.016216;\n sum += texture2D(u_tex, vec2(localX, fract(v - 4.0 * blurStep))) * 0.016216;\n base = sum;\n }\n\n gl_FragColor = base;\n }\n `;\n\n const program = this.createWebGLProgram(gl, vsSource, fsSource);\n\n if (!program) {\n this.webglInitError = 'shader program failed to compile/link (see console)';\n\n console.warn('ReelDeal WebGL:', this.webglInitError);\n\n return;\n }\n\n const buffer = gl.createBuffer();\n\n if (!buffer) {\n this.webglInitError = 'failed to create vertex buffer';\n\n console.warn('ReelDeal WebGL:', this.webglInitError);\n\n gl.deleteProgram(program);\n\n return;\n }\n\n gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n\n const quad = new Float32Array([-1, -1, 0, 1, 1, -1, 1, 1, -1, 1, 0, 0, 1, 1, 1, 0]);\n\n gl.bufferData(gl.ARRAY_BUFFER, quad, gl.STATIC_DRAW);\n\n const texture = gl.createTexture();\n\n if (!texture) {\n this.webglInitError = 'failed to create texture';\n\n console.warn('ReelDeal WebGL:', this.webglInitError);\n\n gl.deleteBuffer(buffer);\n gl.deleteProgram(program);\n\n return;\n }\n\n gl.bindTexture(gl.TEXTURE_2D, texture);\n gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 0);\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this.spriteImg);\n\n const isPowerOfTwo = (n: number): boolean => n > 0 && (n & (n - 1)) === 0;\n const pot = isPowerOfTwo(this.spriteWidth) && isPowerOfTwo(this.spriteHeight);\n\n if (pot) {\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);\n gl.generateMipmap(gl.TEXTURE_2D);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n } else {\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n }\n\n gl.useProgram(program);\n gl.clearColor(0, 0, 0, 0);\n gl.enable(gl.BLEND);\n gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);\n\n this.gl = gl;\n this.glProgram = program;\n this.glBuffer = buffer;\n this.glTexture = texture;\n this.glAttribs = {\n pos: gl.getAttribLocation(program, 'a_pos'),\n uv: gl.getAttribLocation(program, 'a_uv'),\n };\n this.glUniforms = {\n texture: gl.getUniformLocation(program, 'u_tex'),\n reelX: gl.getUniformLocation(program, 'u_reelX'),\n reelW: gl.getUniformLocation(program, 'u_reelW'),\n offsetPx: gl.getUniformLocation(program, 'u_offsetPx'),\n frameHeight: gl.getUniformLocation(program, 'u_frameHeight'),\n spriteHeight: gl.getUniformLocation(program, 'u_spriteHeight'),\n visibleSlots: gl.getUniformLocation(program, 'u_visibleSlots'),\n heightScale: gl.getUniformLocation(program, 'u_heightScale'),\n edgePower: gl.getUniformLocation(program, 'u_edgePower'),\n warpAngle: gl.getUniformLocation(program, 'u_warpAngle'),\n blurPx: gl.getUniformLocation(program, 'u_blurPx'),\n };\n }\n\n private createWebGLProgram(\n gl: WebGLRenderingContext | WebGL2RenderingContext,\n vsSource: string,\n fsSource: string,\n ): WebGLProgram | null {\n const vs = this.createWebGLShader(gl, gl.VERTEX_SHADER, vsSource);\n const fs = this.createWebGLShader(gl, gl.FRAGMENT_SHADER, fsSource);\n\n if (!vs || !fs) {\n if (vs) gl.deleteShader(vs);\n if (fs) gl.deleteShader(fs);\n\n return null;\n }\n\n const program = gl.createProgram();\n\n if (!program) return null;\n\n gl.attachShader(program, vs);\n gl.attachShader(program, fs);\n gl.linkProgram(program);\n\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n const info = gl.getProgramInfoLog(program) ?? 'unknown link error';\n\n this.webglInitError = `program link failed: ${info}`;\n\n console.warn('ReelDeal WebGL: program link failed', info);\n\n gl.deleteProgram(program);\n gl.deleteShader(vs);\n gl.deleteShader(fs);\n\n return null;\n }\n\n gl.detachShader(program, vs);\n gl.detachShader(program, fs);\n gl.deleteShader(vs);\n gl.deleteShader(fs);\n\n return program;\n }\n\n private createWebGLShader(\n gl: WebGLRenderingContext | WebGL2RenderingContext,\n type: number,\n source: string,\n ): WebGLShader | null {\n const shader = gl.createShader(type);\n\n if (!shader) return null;\n\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n const info = gl.getShaderInfoLog(shader) ?? 'unknown compile error';\n\n this.webglInitError = `shader compile failed: ${info}`;\n\n console.warn('ReelDeal WebGL: shader compile failed', info);\n\n gl.deleteShader(shader);\n\n return null;\n }\n\n return shader;\n }\n\n private releaseWebGLResources(): void {\n if (!this.gl) return;\n if (this.glTexture) this.gl.deleteTexture(this.glTexture);\n if (this.glBuffer) this.gl.deleteBuffer(this.glBuffer);\n if (this.glProgram) this.gl.deleteProgram(this.glProgram);\n\n this.glTexture = null;\n this.glBuffer = null;\n this.glProgram = null;\n this.glAttribs = null;\n this.glUniforms = null;\n this.gl = null;\n }\n}\n"],"names":["defaultSpinConfig","defaultWebGLShading","defaultSpinBlur","defaultIdleBob","SPIN_ACCEL_END","SPIN_DECEL_START","clampInt","value","min","max","mod","n","m","clamp01","t","rampHermite","u","x","brakeHermite","reelProgress","tt","finiteOr","fallback","nonNegative","nonNegativeInt","DEFAULT_MAX_DPR","normalizeSpinConfig","overrides","base","normalizeSpinBlur","normalizeWebGLShading","normalizeIdleBob","normalizeMaxDpr","resolveElementById","id","kind","el","resolveElement","elOrId","MAX_REELS","SPIN_TICK_START","SPIN_TICK_RANGE","SPIN_TICK_MAX_PX","VELOCITY_DECAY","VELOCITY_GAIN","ReelDeal","options","target","initialSegments","segments","r","segIndex","idx","segmentIndex","stopAtSegments","config","minSpins","staggerMs","speedPxS","durationMs","useSpeed","startOffsets","deltas","durations","desiredTop","desired","current","alignDelta","delta","desiredDistance","loops","extraDelta","extraLoops","acc","entry","startTime","resolve","spins","spin","resolvers","disabled","now","allDone","duration","eased","baseOffset","tick","env","phase","amp","reel","speed","speedAtMax","gl","shading","warpAngle","reelW","offsetPx","dt","v","amplitudePx","speedHz","phaseOffsetRad","delayMs","rampMs","elapsedMs","tSec","rampT","ramp","omega","sprite","img","reject","onLoad","onError","slotCount","spriteWidth","frameHeight","expectedSpriteHeight","naturalHeight","cssWidth","cssHeight","slotsViewport","slots","reels","fitByWidthH","w","h","viewportWidth","viewportHeight","width","height","normalizedWidth","normalizedHeight","dpr","rawDpr","canWebGL","fallbackGl","program","buffer","quad","texture","isPowerOfTwo","vsSource","fsSource","vs","fs","info","type","source","shader"],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
export declare const defaultIdleBob: ReelDealIdleBob;
|
|
2
|
+
|
|
3
|
+
export declare const defaultSpinBlur: ReelDealSpinBlur;
|
|
4
|
+
|
|
5
|
+
export declare const defaultSpinConfig: ReelDealSpinConfig;
|
|
6
|
+
|
|
7
|
+
export declare const defaultWebGLShading: ReelDealWebGLShading;
|
|
8
|
+
|
|
9
|
+
export declare class ReelDeal {
|
|
10
|
+
private readonly canvas;
|
|
11
|
+
private readonly container;
|
|
12
|
+
private readonly opts;
|
|
13
|
+
private readonly visibleSlots;
|
|
14
|
+
private readonly heightScale;
|
|
15
|
+
private readonly centerIndex;
|
|
16
|
+
private readonly reels;
|
|
17
|
+
private readonly spinConfig;
|
|
18
|
+
private readonly spinBlur;
|
|
19
|
+
private readonly webglShading;
|
|
20
|
+
private readonly idleBob;
|
|
21
|
+
private readonly maxDpr;
|
|
22
|
+
private spriteImg;
|
|
23
|
+
private spriteWidth;
|
|
24
|
+
private spriteHeight;
|
|
25
|
+
private frameHeight;
|
|
26
|
+
private width;
|
|
27
|
+
private height;
|
|
28
|
+
private dpr;
|
|
29
|
+
private offsets;
|
|
30
|
+
private velocities;
|
|
31
|
+
private lastVelT;
|
|
32
|
+
private lastVelOffsets;
|
|
33
|
+
private idleStartTime;
|
|
34
|
+
private idleBaseOffsets;
|
|
35
|
+
private gl;
|
|
36
|
+
private glProgram;
|
|
37
|
+
private glBuffer;
|
|
38
|
+
private glTexture;
|
|
39
|
+
private glAttribs;
|
|
40
|
+
private webglInitError;
|
|
41
|
+
private glUniforms;
|
|
42
|
+
private anim;
|
|
43
|
+
private rafId;
|
|
44
|
+
private pendingSpinResolvers;
|
|
45
|
+
private resizeObserver;
|
|
46
|
+
private resizeQueued;
|
|
47
|
+
private button;
|
|
48
|
+
private buttonHandlerAttached;
|
|
49
|
+
private spinning;
|
|
50
|
+
private queuedSpins;
|
|
51
|
+
constructor(options: ReelDealOptions);
|
|
52
|
+
init(): Promise<void>;
|
|
53
|
+
destroy(): void;
|
|
54
|
+
private ensureCanvasCoversContainer;
|
|
55
|
+
private syncContainerLayoutVars;
|
|
56
|
+
setSegments(initialSegments?: number[]): void;
|
|
57
|
+
setSegment(segmentIndex: number): void;
|
|
58
|
+
spinOnce(stopAtSegments: number[], config?: Partial<ReelDealSpinConfig>): Promise<void>;
|
|
59
|
+
private getSpinConfig;
|
|
60
|
+
spinQueue(spins: ReelDealSpinState[]): Promise<void>;
|
|
61
|
+
stop(): void;
|
|
62
|
+
private resolvePendingSpins;
|
|
63
|
+
requestResize(): void;
|
|
64
|
+
private ensureReady;
|
|
65
|
+
private attachButtonHandler;
|
|
66
|
+
private handleButtonClick;
|
|
67
|
+
private resolveButton;
|
|
68
|
+
private resolveSpinQueue;
|
|
69
|
+
private consumeNextSpin;
|
|
70
|
+
private buildRandomSegments;
|
|
71
|
+
private setButtonDisabled;
|
|
72
|
+
private shouldAnimate;
|
|
73
|
+
private startLoop;
|
|
74
|
+
private loop;
|
|
75
|
+
private render;
|
|
76
|
+
private getSpinBlurPxForReel;
|
|
77
|
+
private renderWebGL;
|
|
78
|
+
private updateVelocity;
|
|
79
|
+
private updateIdle;
|
|
80
|
+
private loadSprite;
|
|
81
|
+
private setupResizeWatcher;
|
|
82
|
+
private queueResize;
|
|
83
|
+
private resizeToContainer;
|
|
84
|
+
private applyViewportCrop;
|
|
85
|
+
private applyResize;
|
|
86
|
+
private getTargetDpr;
|
|
87
|
+
private tryInitWebGL;
|
|
88
|
+
private createWebGLProgram;
|
|
89
|
+
private createWebGLShader;
|
|
90
|
+
private releaseWebGLResources;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export declare interface ReelDealIdleBob {
|
|
94
|
+
amplitudePx: number;
|
|
95
|
+
speedHz: number;
|
|
96
|
+
phaseOffsetRad: number;
|
|
97
|
+
delayMs: number;
|
|
98
|
+
rampMs: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export declare interface ReelDealOptions {
|
|
102
|
+
canvas: HTMLCanvasElement | string;
|
|
103
|
+
container: HTMLElement | string;
|
|
104
|
+
sprite: ReelDealSpriteSource;
|
|
105
|
+
slotCount: number;
|
|
106
|
+
initialSegments?: number[];
|
|
107
|
+
button?: HTMLButtonElement | string;
|
|
108
|
+
queuedSpinStates?: ReelDealSpinState[];
|
|
109
|
+
reels?: number;
|
|
110
|
+
spinConfig?: Partial<ReelDealSpinConfig>;
|
|
111
|
+
spinBlur?: Partial<ReelDealSpinBlur>;
|
|
112
|
+
webglShading?: Partial<ReelDealWebGLShading>;
|
|
113
|
+
idleBob?: Partial<ReelDealIdleBob> | false;
|
|
114
|
+
maxDpr?: number;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export declare interface ReelDealSpinBlur {
|
|
118
|
+
maxPx: number;
|
|
119
|
+
speedAtMax: number;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export declare interface ReelDealSpinConfig {
|
|
123
|
+
durationMs?: number;
|
|
124
|
+
minSpins: number;
|
|
125
|
+
speedPxS?: number;
|
|
126
|
+
staggerMs?: number;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export declare interface ReelDealSpinState {
|
|
130
|
+
stopAtSegments: number[];
|
|
131
|
+
callback?: (spinIndex: number, stopAtSegments: number[]) => void;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export declare type ReelDealSpriteSource = string | HTMLImageElement;
|
|
135
|
+
|
|
136
|
+
export declare interface ReelDealWebGLShading {
|
|
137
|
+
warpAngleDeg: number;
|
|
138
|
+
edgePower: number;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export { }
|