reel-deal 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.umd.js CHANGED
@@ -1,81 +1,135 @@
1
- (function(m,g){typeof exports=="object"&&typeof module<"u"?g(exports):typeof define=="function"&&define.amd?define(["exports"],g):(m=typeof globalThis<"u"?globalThis:m||self,g(m.ReelDeal={}))})(this,(function(m){"use strict";const g={minSpins:2,durationMs:4e3,speedPxS:4e3,staggerMs:300},w={maxPx:4,speedAtMax:2200},x={amplitudePx:9,speedHz:.25,phaseOffsetRad:.9,rampMs:800},H=.1,A=.6,C=(a,t,s)=>Math.max(t,Math.min(s,Math.floor(a))),I=(a,t)=>(a%t+t)%t,v=a=>Math.max(0,Math.min(1,a)),G=a=>{const t=v(a);return t**2*(2-t)},k=a=>{const t=v(a);return t+t**2-t**3},q=a=>{const t=v(a);if(t<=H){const i=t/H;return G(i)*H}if(t<A)return t;const s=(t-A)/(1-A);return A+k(s)*(1-A)},L=(a,t)=>typeof a=="number"&&Number.isFinite(a)?a:t,_=(a,t)=>Math.max(0,L(a,t)),R=(a,t)=>Math.max(0,Math.floor(L(a,t))),$=2,X=a=>{const t={...g,...a??{}};return{minSpins:R(t.minSpins,g.minSpins),durationMs:R(t.durationMs,g.durationMs??0),speedPxS:R(t.speedPxS,g.speedPxS??0),staggerMs:R(t.staggerMs,g.staggerMs??0)}},Q=a=>{const t={...w,...a??{}};return{maxPx:_(t.maxPx,w.maxPx),speedAtMax:_(t.speedAtMax,w.speedAtMax)}},K=a=>{if(a===!1)return null;const t={...x,...a??{}};return{amplitudePx:_(t.amplitudePx,x.amplitudePx),speedHz:_(t.speedHz,x.speedHz),phaseOffsetRad:L(t.phaseOffsetRad,x.phaseOffsetRad),rampMs:R(t.rampMs,x.rampMs)}},j=a=>Math.max(1,L(a,$)),Z=(a,t)=>{const s=document.getElementById(a);if(!s)throw new Error(`${t} not found: ${a}`);return s},B=(a,t)=>typeof a=="string"?Z(a,t):a,J=5,z=.72,tt=.28,et=7,N=.85,st=1-N,it=55,rt=300,nt=1.25,at=.35,F={warpAngleDeg:60,edgePower:4.8},ot=a=>({warpAngleDeg:_(a?.warpAngleDeg,F.warpAngleDeg),edgePower:_(a?.edgePower,F.edgePower)});class lt{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(t){this.opts=t,this.reels=C(t.reels??1,1,J),this.spinConfig=X(t.spinConfig),this.spinBlur=Q(t.spinBlur),this.webglShading=ot(t.webglShading),this.idleBob=K(t.idleBob),this.maxDpr=j(t.maxDpr),this.canvas=B(t.canvas,"Canvas"),this.container=B(t.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 t=this.canvas===this.container?this.canvas:this.container;t.style.setProperty("--reels",String(this.reels)),t.style.setProperty("--visible-slots",String(this.visibleSlots)),t.style.aspectRatio=`${this.reels} / ${this.visibleSlots*this.heightScale}`,this.canvas!==this.container&&(this.container.style.overflow="hidden")}setSegments(t){this.ensureReady();const s=t??[];for(let i=0;i<this.reels;i+=1){const e=s[i]??0,r=C(e,0,this.opts.slotCount-1);this.offsets[i]=r*this.frameHeight-this.centerIndex*this.frameHeight,this.lastVelOffsets[i]=this.offsets[i],this.velocities[i]=0,this.idleBaseOffsets[i]=this.offsets[i]}this.lastVelT=null,this.idleStartTime=null,this.anim=null,this.stop(),this.render(),this.startLoop()}setSegment(t){this.setSegments(new Array(this.reels).fill(t))}spinOnce(t,s){this.ensureReady();const{minSpins:i,staggerMs:e,speedPxS:r,durationMs:l}=this.getSpinConfig(s),u=r>0,f=this.getStopSpringOvershootPx(),c=Math.max(0,rt),n=c>0&&f>0,h=new Array(this.reels),d=new Array(this.reels),S=new Array(this.reels),p=new Array(this.reels),b=new Array(this.reels);for(let o=0;o<this.reels;o+=1){const ht=C(t[o]??0,0,this.opts.slotCount-1)*this.frameHeight-this.centerIndex*this.frameHeight,ut=I(ht,this.spriteHeight),ft=I(this.offsets[o],this.spriteHeight),U=I(ft-ut,this.spriteHeight);let E=-(i*this.spriteHeight+U);if(u&&l>0){const D=r*l/1e3,M=Math.max(i,Math.ceil(Math.max(0,D-U)/this.spriteHeight));E=-(U+M*this.spriteHeight)}if(e>0&&u){const D=r*e*o/1e3,M=Math.ceil(D/this.spriteHeight);E-=M*this.spriteHeight}h[o]=this.offsets[o],b[o]=this.offsets[o]+E;let W=E,Y=0;if(n&&E!==0&&f>0){const M=Math.sign(E)*f;W+=M,Y=c}d[o]=W,p[o]=Y,u?S[o]=Math.max(0,Math.round(Math.abs(W)/r*1e3)):S[o]=Math.max(0,l+e*o)}const P=d.reduce((o,y)=>o+Math.abs(y),0),T=S.every(o=>o<=0),V=p.every(o=>o<=0);if(P===0||T&&V){for(let o=0;o<this.reels;o+=1)this.offsets[o]=b[o];return this.anim=null,this.resolvePendingSpins(),this.render(),Promise.resolve()}const O=performance.now();return this.anim={startTime:O,startOffsets:h,deltas:d,durationMs:S,settleDurationMs:p,targets:b},this.idleStartTime=null,this.startLoop(),new Promise(o=>{this.pendingSpinResolvers.push(o)})}getSpinConfig(t){return t?X({...this.spinConfig,...t}):this.spinConfig}getStopSpringOvershootPx(){const t=this.frameHeight*at,s=Math.max(0,it);return Math.min(s,t)}getStopSpringOffset(t,s){if(t===0)return 0;const i=Math.exp(-5*s),e=Math.PI*2*nt*s;return t*i*Math.cos(e)}async spinQueue(t){for(let s=0;s<t.length;s+=1){const i=t[s];await this.spinOnce(i.stopAtSegments),i.callback?.(s,i.stopAtSegments)}}stop(){this.rafId!==null&&cancelAnimationFrame(this.rafId),this.rafId=null,this.anim=null,this.resolvePendingSpins(),this.lastVelT=null;for(let t=0;t<this.reels;t+=1)this.velocities[t]=0}resolvePendingSpins(){if(this.pendingSpinResolvers.length===0)return;const t=this.pendingSpinResolvers;this.pendingSpinResolvers=[];for(const s of t)s()}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 t=this.consumeNextSpin()??{stopAtSegments:this.buildRandomSegments()};this.spinning=!0,this.setButtonDisabled(!0);try{await this.spinOnce(t.stopAtSegments)}finally{this.queuedSpins&&this.queuedSpins.length===0?this.setButtonDisabled(!0):this.setButtonDisabled(!1),this.spinning=!1}};resolveButton(){return this.opts.button?B(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 t=Math.max(1,Math.floor(this.opts.slotCount)),s=[];for(let i=0;i<this.reels;i+=1)s.push(Math.floor(Math.random()*t));return s}setButtonDisabled(t){this.button&&(this.button.disabled=t,t?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=t=>{if(this.anim){const{startTime:s,durationMs:i,startOffsets:e,deltas:r,settleDurationMs:l,targets:u}=this.anim,f=t-s;let c=!0;for(let n=0;n<this.reels;n+=1){const h=i[n]??0,d=l[n]??0,S=u[n]??e[n]+r[n];if(h>0&&f<h){const p=Math.min(1,f/h),b=q(p);p<1&&(c=!1);const P=e[n]+r[n]*b;let T=0;if(p>z&&this.frameHeight>0){const O=(1-v((p-z)/tt))**2,o=P/this.frameHeight*Math.PI*2,y=Math.min(et,this.frameHeight*.06);T=Math.sin(o)*O*y}this.offsets[n]=P+T}else if(d>0){const p=h<=0?f/d:(f-h)/d,b=v(p),P=e[n]+r[n]-S,T=this.getStopSpringOffset(P,b);b<1&&(c=!1),this.offsets[n]=S+T}else this.offsets[n]=S}if(this.updateVelocity(t),this.render(),c){for(let n=0;n<this.reels;n+=1){const h=this.anim.targets[n]??this.anim.startOffsets[n]+this.anim.deltas[n];this.offsets[n]=h,this.idleBaseOffsets[n]=this.offsets[n]}this.updateVelocity(t);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(t),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(t){if(!this.anim)return 0;const s=Math.abs(this.velocities[t]??0),i=Math.max(1,this.spinBlur.speedAtMax);return v(s/i)*Math.max(0,this.spinBlur.maxPx)}renderWebGL(){if(!this.gl||!this.glProgram||!this.glAttribs||!this.glUniforms||!this.glTexture||!this.glBuffer||!this.spriteImg)return;const t=this.gl;t.useProgram(this.glProgram),t.bindBuffer(t.ARRAY_BUFFER,this.glBuffer),t.enableVertexAttribArray(this.glAttribs.pos),t.vertexAttribPointer(this.glAttribs.pos,2,t.FLOAT,!1,16,0),t.enableVertexAttribArray(this.glAttribs.uv),t.vertexAttribPointer(this.glAttribs.uv,2,t.FLOAT,!1,16,8),t.activeTexture(t.TEXTURE0),t.bindTexture(t.TEXTURE_2D,this.glTexture),t.uniform1i(this.glUniforms.texture,0),t.uniform1f(this.glUniforms.frameHeight,this.frameHeight),t.uniform1f(this.glUniforms.spriteHeight,this.spriteHeight),t.uniform1f(this.glUniforms.visibleSlots,this.visibleSlots),t.uniform1f(this.glUniforms.heightScale,this.heightScale),t.uniform1f(this.glUniforms.edgePower,Math.max(.5,this.webglShading.edgePower));const s=this.webglShading,i=Math.max(0,s.warpAngleDeg)*Math.PI/180;t.uniform1f(this.glUniforms.warpAngle,i),t.clear(t.COLOR_BUFFER_BIT);const e=1/this.reels;for(let r=0;r<this.reels;r+=1){t.uniform1f(this.glUniforms.reelX,r*e),t.uniform1f(this.glUniforms.reelW,e);const l=I(this.offsets[r],this.spriteHeight);t.uniform1f(this.glUniforms.offsetPx,l),t.uniform1f(this.glUniforms.blurPx,this.getSpinBlurPxForReel(r)),t.drawArrays(t.TRIANGLE_STRIP,0,4)}}updateVelocity(t){if(this.lastVelT===null){this.lastVelT=t;for(let i=0;i<this.reels;i+=1)this.lastVelOffsets[i]=this.offsets[i],this.velocities[i]=0;return}const s=(t-this.lastVelT)/1e3;if(!(s<=0)){for(let i=0;i<this.reels;i+=1){const e=(this.offsets[i]-this.lastVelOffsets[i])/s;this.velocities[i]=this.velocities[i]*N+e*st,this.lastVelOffsets[i]=this.offsets[i]}this.lastVelT=t}}updateIdle(t){if(!this.idleBob)return;this.idleStartTime===null&&(this.idleStartTime=t);const{amplitudePx:s,speedHz:i,phaseOffsetRad:e,rampMs:r}=this.idleBob,l=t-this.idleStartTime,u=l/1e3,f=Math.max(0,Math.min(1,l/Math.max(1,r))),c=G(f),n=i*Math.PI*2;for(let h=0;h<this.reels;h+=1){const d=u*n+h*e;this.offsets[h]=this.idleBaseOffsets[h]+Math.sin(d)*(s*c),this.velocities[h]=0}}async loadSprite(){const{sprite:t}=this.opts,s=typeof t=="string"?new Image:t;typeof t=="string"&&(s.src=t),(!s.complete||s.naturalWidth<=0)&&await new Promise((c,n)=>{const h=()=>c(),d=()=>n(new Error("Failed to load sprite image"));s.addEventListener("load",h,{once:!0}),s.addEventListener("error",d,{once:!0})});const i=Math.max(1,Math.floor(this.opts.slotCount)),e=s.naturalWidth,r=e,l=r*i,u=s.naturalHeight;if(Math.abs(u-l)>2)throw new Error(`Sprite size mismatch. Expected height ~${Math.round(l)}px (slotCount=${i}, slot aspect 1/1), got ${u}px.`);this.spriteImg=s,this.spriteWidth=e,this.frameHeight=Math.max(1,Math.round(r)),this.spriteHeight=Math.max(1,Math.round(this.frameHeight*i))}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 t=this.container.clientWidth,s=this.container.clientHeight,i=this.visibleSlots*this.heightScale;if(t<=0||s<=0){this.applyResize(1,1);return}const e=i,r=this.reels,l=t/r*e;let u=t,f=l;f>s&&(f=s,u=s/e*r);const c=Math.max(1,Math.floor(u)),n=Math.max(1,Math.floor(f));this.applyResize(c,n),this.render()}applyViewportCrop(){this.ensureCanvasCoversContainer(),this.canvas.style.transform=""}applyResize(t,s){const i=Math.max(1,Math.floor(t)),e=Math.max(1,Math.floor(s)),r=this.getTargetDpr();this.width=i,this.height=e,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 t=window.devicePixelRatio||1,s=Math.max(1,t);return Math.min(this.maxDpr,s)}tryInitWebGL(){if(!this.spriteImg)return;this.releaseWebGLResources(),this.webglInitError=null;const t=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(!t){const n=document.createElement("canvas"),h=typeof WebGLRenderingContext<"u",d=n.getContext("webgl2")??n.getContext("webgl");this.webglInitError=`context is null (browser WebGL=${h?"yes":"no"}, fallback canvas=${d?"yes":"no"})`,console.warn("ReelDeal WebGL:",this.webglInitError);return}const e=this.createWebGLProgram(t,`
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
- const float HALF_PI = 1.5707963;
30
-
31
- float saturate(float x) { return clamp(x, 0.0, 1.0); }
32
-
33
- void main() {
34
- if (v_uv.x < u_reelX || v_uv.x > (u_reelX + u_reelW)) {
35
- discard;
36
- }
37
-
38
- float localX = (v_uv.x - u_reelX) / u_reelW;
39
- float localY = v_uv.y;
40
- float hs = max(0.0001, u_heightScale);
41
- float fullY = localY * hs + (1.0 - hs) * 0.5;
42
-
43
- float warp = max(0.0, u_warpAngle);
44
- float warpStrength = saturate(warp / HALF_PI);
45
- float distNorm = abs(fullY - 0.5) * 2.0;
46
- float edgePow = max(0.5, u_edgePower);
47
- float edgeWeight = pow(saturate(distNorm), edgePow) * warpStrength;
48
-
49
- float stretchMax = 6.0;
50
- float stretch = mix(1.0, stretchMax, edgeWeight);
51
- float warpedY = 0.5 + (fullY - 0.5) * stretch;
52
-
53
- float arcY = 0.5 + 0.5 * sin((warpedY - 0.5) * HALF_PI);
54
- warpedY = mix(warpedY, arcY, 0.2);
55
-
56
- float yPx = u_offsetPx + warpedY * (u_frameHeight * u_visibleSlots);
57
- float v = fract((u_offsetPx / u_spriteHeight) +
58
- warpedY * ((u_frameHeight * u_visibleSlots) / u_spriteHeight));
59
-
60
- vec4 base = texture2D(u_tex, vec2(localX, v));
61
-
62
- if (u_blurPx > 0.0) {
63
- float maxStepPx = 1.0 + 3.0 * saturate(u_blurPx / 4.0);
64
- float blurPx = min(u_blurPx, maxStepPx);
65
- float blurStep = blurPx / max(1.0, u_spriteHeight);
66
- vec4 sum = base * 0.227027;
67
- sum += texture2D(u_tex, vec2(localX, fract(v + blurStep))) * 0.1945946;
68
- sum += texture2D(u_tex, vec2(localX, fract(v - blurStep))) * 0.1945946;
69
- sum += texture2D(u_tex, vec2(localX, fract(v + 2.0 * blurStep))) * 0.1216216;
70
- sum += texture2D(u_tex, vec2(localX, fract(v - 2.0 * blurStep))) * 0.1216216;
71
- sum += texture2D(u_tex, vec2(localX, fract(v + 3.0 * blurStep))) * 0.054054;
72
- sum += texture2D(u_tex, vec2(localX, fract(v - 3.0 * blurStep))) * 0.054054;
73
- sum += texture2D(u_tex, vec2(localX, fract(v + 4.0 * blurStep))) * 0.016216;
74
- sum += texture2D(u_tex, vec2(localX, fract(v - 4.0 * blurStep))) * 0.016216;
75
- base = sum;
76
- }
77
-
78
- gl_FragColor = base;
79
- }
80
- `);if(!e){this.webglInitError="shader program failed to compile/link (see console)",console.warn("ReelDeal WebGL:",this.webglInitError);return}const r=t.createBuffer();if(!r){this.webglInitError="failed to create vertex buffer",console.warn("ReelDeal WebGL:",this.webglInitError),t.deleteProgram(e);return}t.bindBuffer(t.ARRAY_BUFFER,r);const l=new Float32Array([-1,-1,0,1,1,-1,1,1,-1,1,0,0,1,1,1,0]);t.bufferData(t.ARRAY_BUFFER,l,t.STATIC_DRAW);const u=t.createTexture();if(!u){this.webglInitError="failed to create texture",console.warn("ReelDeal WebGL:",this.webglInitError),t.deleteBuffer(r),t.deleteProgram(e);return}t.bindTexture(t.TEXTURE_2D,u),t.pixelStorei(t.UNPACK_FLIP_Y_WEBGL,0),t.texImage2D(t.TEXTURE_2D,0,t.RGBA,t.RGBA,t.UNSIGNED_BYTE,this.spriteImg);const f=n=>n>0&&(n&n-1)===0;f(this.spriteWidth)&&f(this.spriteHeight)?(t.texParameteri(t.TEXTURE_2D,t.TEXTURE_WRAP_S,t.REPEAT),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_WRAP_T,t.REPEAT),t.generateMipmap(t.TEXTURE_2D),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_MIN_FILTER,t.LINEAR_MIPMAP_LINEAR),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_MAG_FILTER,t.LINEAR)):(t.texParameteri(t.TEXTURE_2D,t.TEXTURE_MIN_FILTER,t.LINEAR),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_MAG_FILTER,t.LINEAR),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_WRAP_S,t.CLAMP_TO_EDGE),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_WRAP_T,t.CLAMP_TO_EDGE)),t.useProgram(e),t.clearColor(0,0,0,0),t.enable(t.BLEND),t.blendFunc(t.SRC_ALPHA,t.ONE_MINUS_SRC_ALPHA),this.gl=t,this.glProgram=e,this.glBuffer=r,this.glTexture=u,this.glAttribs={pos:t.getAttribLocation(e,"a_pos"),uv:t.getAttribLocation(e,"a_uv")},this.glUniforms={texture:t.getUniformLocation(e,"u_tex"),reelX:t.getUniformLocation(e,"u_reelX"),reelW:t.getUniformLocation(e,"u_reelW"),offsetPx:t.getUniformLocation(e,"u_offsetPx"),frameHeight:t.getUniformLocation(e,"u_frameHeight"),spriteHeight:t.getUniformLocation(e,"u_spriteHeight"),visibleSlots:t.getUniformLocation(e,"u_visibleSlots"),heightScale:t.getUniformLocation(e,"u_heightScale"),edgePower:t.getUniformLocation(e,"u_edgePower"),warpAngle:t.getUniformLocation(e,"u_warpAngle"),blurPx:t.getUniformLocation(e,"u_blurPx")}}createWebGLProgram(t,s,i){const e=this.createWebGLShader(t,t.VERTEX_SHADER,s),r=this.createWebGLShader(t,t.FRAGMENT_SHADER,i);if(!e||!r)return e&&t.deleteShader(e),r&&t.deleteShader(r),null;const l=t.createProgram();if(!l)return null;if(t.attachShader(l,e),t.attachShader(l,r),t.linkProgram(l),!t.getProgramParameter(l,t.LINK_STATUS)){const u=t.getProgramInfoLog(l)??"unknown link error";return this.webglInitError=`program link failed: ${u}`,console.warn("ReelDeal WebGL: program link failed",u),t.deleteProgram(l),t.deleteShader(e),t.deleteShader(r),null}return t.detachShader(l,e),t.detachShader(l,r),t.deleteShader(e),t.deleteShader(r),l}createWebGLShader(t,s,i){const e=t.createShader(s);if(!e)return null;if(t.shaderSource(e,i),t.compileShader(e),!t.getShaderParameter(e,t.COMPILE_STATUS)){const r=t.getShaderInfoLog(e)??"unknown compile error";return this.webglInitError=`shader compile failed: ${r}`,console.warn("ReelDeal WebGL: shader compile failed",r),t.deleteShader(e),null}return e}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)}}m.ReelDeal=lt,m.defaultIdleBob=x,m.defaultSpinBlur=w,m.defaultSpinConfig=g,Object.defineProperty(m,Symbol.toStringTag,{value:"Module"})}));
1
+ (function(d,P){typeof exports=="object"&&typeof module<"u"?P(exports):typeof define=="function"&&define.amd?define(["exports"],P):(d=typeof globalThis<"u"?globalThis:d||self,P(d.ReelDeal={}))})(this,(function(d){"use strict";const P=r=>new Promise((e,t)=>{const i=()=>e(),s=()=>t(new Error("Failed to load sprite image"));r.addEventListener("load",i,{once:!0}),r.addEventListener("error",s,{once:!0})}),k=async(r,e)=>{const t=typeof r=="string"?new Image:r;typeof r=="string"&&(t.src=r),(!t.complete||t.naturalWidth<=0)&&await P(t);const i=Math.max(1,Math.floor(e)),s=t.naturalWidth,n=s,a=n*i,u=t.naturalHeight;if(Math.abs(u-a)>2)throw new Error(`Sprite size mismatch. Expected height ~${Math.round(a)}px (slotCount=${i}, slot aspect 1/1), got ${u}px.`);return{image:t,spriteWidth:s,frameHeight:Math.max(1,Math.round(n)),spriteHeight:Math.max(1,Math.round(n*i)),slotCount:i}},w={minSpins:2,durationMs:4e3,speedPxS:4e3,staggerMs:300},C={maxPx:4,speedAtMax:2200},E={amplitudePx:9,speedHz:.25,phaseOffsetRad:.9,rampMs:800},N={warpAngleDeg:60,edgePower:4.8},V={thickness:6,intensity:1},U=.1,R=.6,T=(r,e,t)=>Math.max(e,Math.min(t,Math.floor(r))),D=(r,e)=>(r%e+e)%e,_=r=>Math.max(0,Math.min(1,r)),te=r=>{const e=_(r);return e**2*(2-e)},ie=r=>{const e=_(r);return e+e**2-e**3},se=r=>{const e=_(r);if(e<=U){const i=e/U;return te(i)*U}if(e<R)return e;const t=(e-R)/(1-R);return R+ie(t)*(1-R)},H=(r,e)=>typeof r=="number"&&Number.isFinite(r)?r:e,x=(r,e)=>Math.max(0,H(r,e)),L=(r,e)=>Math.max(0,Math.floor(H(r,e))),re=2,F=r=>{const e={...w,...r??{}};return{minSpins:L(e.minSpins,w.minSpins??0),durationMs:L(e.durationMs,w.durationMs??0),speedPxS:L(e.speedPxS,w.speedPxS??0),staggerMs:L(e.staggerMs,w.staggerMs??0)}},Y=r=>{const e={...C,...r??{}};return{maxPx:x(e.maxPx,C.maxPx),speedAtMax:x(e.speedAtMax,C.speedAtMax)}},q=r=>{if(r===!1)return null;const e={...E,...r??{}};return{amplitudePx:x(e.amplitudePx,E.amplitudePx),speedHz:x(e.speedHz,E.speedHz),phaseOffsetRad:H(e.phaseOffsetRad,E.phaseOffsetRad),rampMs:L(e.rampMs,E.rampMs)}},$=r=>Math.max(1,H(r,re)),Q=r=>({warpAngleDeg:x(r?.warpAngleDeg,N.warpAngleDeg),edgePower:x(r?.edgePower,N.edgePower)}),j=r=>r===!1||!r?null:{thickness:x(r.thickness,V.thickness),intensity:_(r.intensity??V.intensity)},ne=r=>{const{stopAtSegments:e,currentOffsets:t,reels:i,frameHeight:s,spriteHeight:n,centerIndex:a,slotCount:u,spinConfig:l,stopSpringOvershootPx:p,stopSpringSettleMs:o,useSpeed:c}=r,S=new Array(i),m=new Array(i),h=new Array(i),g=new Array(i),b=new Array(i),{minSpins:v,staggerMs:O,speedPxS:M,durationMs:I}=l,z=o>0&&p>0;for(let f=0;f<i;f+=1){const Le=T(e[f]??0,0,u-1)*s-a*s,Me=D(Le,n),Ie=D(t[f],n),G=D(Ie-Me,n);let A=-(v*n+G);if(c&&I>0){const B=M*I/1e3,y=Math.max(v,Math.ceil(Math.max(0,B-G)/n));A=-(G+y*n)}if(O>0&&c){const B=M*O*f/1e3,y=Math.ceil(B/n);A-=y*n}S[f]=t[f],b[f]=t[f]+A;let X=A,ee=0;if(z&&A!==0&&p>0){const y=Math.sign(A)*p;X+=y,ee=o}m[f]=X,g[f]=ee,c?h[f]=Math.max(0,Math.round(Math.abs(X)/M*1e3)):h[f]=Math.max(0,I+O*f)}const Pe=m.reduce((f,J)=>f+Math.abs(J),0),Re=h.every(f=>f<=0),Te=g.every(f=>f<=0);return{startOffsets:S,deltas:m,durations:h,settleDurationMs:g,targets:b,totalDelta:Pe,allSpinZero:Re,allSettleZero:Te}},ae=(r,e)=>{if(r===0)return 0;const t=Math.exp(-5*e),i=Math.PI*2*1.25*e;return r*t*Math.cos(i)},oe=(r,e,t)=>{if(e.lastVelT===null){e.lastVelT=r;for(let a=0;a<t.length;a+=1)e.lastVelOffsets[a]=t[a],e.velocities[a]=0;return e.lastVelT}const i=(r-e.lastVelT)/1e3;if(i<=0)return e.lastVelT;const s=.85,n=1-s;for(let a=0;a<t.length;a+=1){const u=(t[a]-e.lastVelOffsets[a])/i;e.velocities[a]=e.velocities[a]*s+u*n,e.lastVelOffsets[a]=t[a]}return e.lastVelT=r,e.lastVelT},le=(r,e,t,i,s)=>{e.idleStartTime===null&&(e.idleStartTime=r);const{amplitudePx:n,speedHz:a,phaseOffsetRad:u,rampMs:l}=t,p=r-e.idleStartTime,o=p/1e3,c=Math.max(0,Math.min(1,p/Math.max(1,l))),S=Math.min(1,Math.max(0,c*c*(2-c))),m=a*Math.PI*2;for(let h=0;h<s;h+=1){const g=o*m+h*u;i[h]=e.idleBaseOffsets[h]+Math.sin(g)*(n*S),e.velocities[h]=0}return e.idleStartTime};class he{rafId=null;step=null;abortListener=null;start(e,t){this.rafId===null&&(this.step=e,this.attachAbort(t),this.rafId=requestAnimationFrame(this.tick))}stop(){this.rafId!==null&&(cancelAnimationFrame(this.rafId),this.rafId=null),this.step=null,this.detachAbort()}isRunning(){return this.rafId!==null}tick=e=>{if(!this.step){this.stop();return}if(this.step(e)===!1){this.stop();return}this.rafId!==null&&(this.rafId=requestAnimationFrame(this.tick))};attachAbort(e){if(!e)return;if(e.aborted){this.stop();return}const t=()=>{this.stop()};e.addEventListener("abort",t,{once:!0}),this.abortListener=()=>e.removeEventListener("abort",t)}detachAbort(){this.abortListener&&this.abortListener(),this.abortListener=null}}class ue{slotCount;reels;queue;constructor(e){this.slotCount=Math.max(1,Math.floor(e.slotCount)),this.reels=Math.max(1,Math.floor(e.reels)),this.queue=[...e.initialQueue??[]]}reset(e){this.queue=[...e??[]]}hasPending(){return this.queue.length>0}consume(){return this.queue.length===0?{state:{stopAtSegments:this.buildRandomSegments()},remaining:0,isLast:!0}:{state:this.queue.shift()??{stopAtSegments:this.buildRandomSegments()},remaining:this.queue.length,isLast:this.queue.length===0}}buildRandomSegments(){const e=this.slotCount,t=[];for(let i=0;i<this.reels;i+=1)t.push(T(Math.floor(Math.random()*e),0,e-1));return t}}class fe{button;handler;attached=!1;spinning=!1;constructor(e,t){this.button=e,this.handler=t,this.attach()}updateAvailability(e){this.button&&this.setDisabled(!e)}dispose(){this.button&&this.attached&&(this.button.removeEventListener("click",this.onClick),this.attached=!1),this.spinning=!1}attach(){!this.button||this.attached||(this.button.addEventListener("click",this.onClick,{passive:!0}),this.attached=!0)}setDisabled(e){this.button&&(this.button.disabled=e,e?this.button.setAttribute("aria-busy","true"):this.button.removeAttribute("aria-busy"))}onClick=async()=>{if(!this.spinning){this.spinning=!0,this.setDisabled(!0);try{await this.handler()}finally{this.spinning=!1,this.setDisabled(!1)}}}}const ce=`
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
+ `,de=`
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
+ const float HALF_PI = 1.5707963;
30
+
31
+ float saturate(float x) { return clamp(x, 0.0, 1.0); }
32
+
33
+ void main() {
34
+ if (v_uv.x < u_reelX || v_uv.x > (u_reelX + u_reelW)) {
35
+ discard;
36
+ }
37
+
38
+ float localX = (v_uv.x - u_reelX) / u_reelW;
39
+ float localY = v_uv.y;
40
+ float hs = max(0.0001, u_heightScale);
41
+ float fullY = localY * hs + (1.0 - hs) * 0.5;
42
+
43
+ float warp = max(0.0, u_warpAngle);
44
+ float warpStrength = saturate(warp / HALF_PI);
45
+ float distNorm = abs(fullY - 0.5) * 2.0;
46
+ float edgePow = max(0.5, u_edgePower);
47
+ float edgeWeight = pow(saturate(distNorm), edgePow) * warpStrength;
48
+
49
+ float stretchMax = 6.0;
50
+ float stretch = mix(1.0, stretchMax, edgeWeight);
51
+ float warpedY = 0.5 + (fullY - 0.5) * stretch;
52
+
53
+ float arcY = 0.5 + 0.5 * sin((warpedY - 0.5) * HALF_PI);
54
+ warpedY = mix(warpedY, arcY, 0.2);
55
+
56
+ float yPx = u_offsetPx + warpedY * (u_frameHeight * u_visibleSlots);
57
+ float v = fract((u_offsetPx / u_spriteHeight) +
58
+ warpedY * ((u_frameHeight * u_visibleSlots) / u_spriteHeight));
59
+
60
+ vec4 base = texture2D(u_tex, vec2(localX, v));
61
+
62
+ if (u_blurPx > 0.0) {
63
+ float maxStepPx = 1.0 + 3.0 * saturate(u_blurPx / 4.0);
64
+ float blurPx = min(u_blurPx, maxStepPx);
65
+ float blurStep = blurPx / max(1.0, u_spriteHeight);
66
+ vec4 sum = base * 0.227027;
67
+ sum += texture2D(u_tex, vec2(localX, fract(v + blurStep))) * 0.1945946;
68
+ sum += texture2D(u_tex, vec2(localX, fract(v - blurStep))) * 0.1945946;
69
+ sum += texture2D(u_tex, vec2(localX, fract(v + 2.0 * blurStep))) * 0.1216216;
70
+ sum += texture2D(u_tex, vec2(localX, fract(v - 2.0 * blurStep))) * 0.1216216;
71
+ sum += texture2D(u_tex, vec2(localX, fract(v + 3.0 * blurStep))) * 0.054054;
72
+ sum += texture2D(u_tex, vec2(localX, fract(v - 3.0 * blurStep))) * 0.054054;
73
+ sum += texture2D(u_tex, vec2(localX, fract(v + 4.0 * blurStep))) * 0.016216;
74
+ sum += texture2D(u_tex, vec2(localX, fract(v - 4.0 * blurStep))) * 0.016216;
75
+ base = sum;
76
+ }
77
+
78
+ gl_FragColor = base;
79
+ }
80
+ `,pe=`
81
+ attribute vec2 a_pos;
82
+ varying vec2 v_pos;
83
+ void main() {
84
+ v_pos = a_pos;
85
+ gl_Position = vec4(a_pos, 0.0, 1.0);
86
+ }
87
+ `,me=`
88
+ #ifdef GL_FRAGMENT_PRECISION_HIGH
89
+ precision highp float;
90
+ #else
91
+ precision mediump float;
92
+ #endif
93
+
94
+ varying vec2 v_pos;
95
+ uniform float u_time;
96
+ uniform vec2 u_resolution;
97
+ uniform float u_thickness;
98
+ uniform float u_intensity;
99
+
100
+ void main() {
101
+ vec2 uv = v_pos;
102
+ float centerY = 0.0;
103
+ float distY = abs(uv.y - centerY);
104
+ float thicknessPx = u_thickness / u_resolution.y * 2.0;
105
+
106
+ float lineMask = 1.0 - smoothstep(0.0, thicknessPx * 2.0, distY);
107
+
108
+ if (lineMask <= 0.001) {
109
+ discard;
110
+ }
111
+
112
+ vec3 color;
113
+ float alpha = lineMask * u_intensity;
114
+
115
+ float warm = 1.0 - exp(-u_time * 2.6);
116
+ float flicker = (sin(u_time * 37.0) * sin(u_time * 13.0)) * 0.28;
117
+ flicker *= exp(-u_time * 2.2);
118
+ warm = clamp(warm + flicker, 0.0, 1.0);
119
+ float warmColor = mix(0.65, 1.0, warm);
120
+ alpha *= warm;
121
+
122
+ float pulse = sin(u_time * 4.0) * 0.3 + 0.7;
123
+ float x = (uv.x + 1.0) * 0.5;
124
+ float wave = sin(x * 30.0 - u_time * 8.0) * 0.2;
125
+
126
+ color = vec3(1.0, 0.8, 0.2) * (pulse + wave);
127
+
128
+ float core = exp(-distY * 120.0 / thicknessPx);
129
+ color = mix(color, vec3(1.0), core * 0.6);
130
+
131
+ color *= warmColor;
132
+ gl_FragColor = vec4(color, alpha);
133
+ }
134
+ `;class ge{program=null;buffer=null;attribs=null;uniforms=null;startTime=null;show=!1;init(e,t){const i=this.createProgram(e,pe,me);if(!i)return;const s=e.createBuffer();if(!s){e.deleteProgram(i);return}e.bindBuffer(e.ARRAY_BUFFER,s);const n=new Float32Array([-1,-1,1,-1,-1,1,1,1]);e.bufferData(e.ARRAY_BUFFER,n,e.STATIC_DRAW),this.program=i,this.buffer=s,this.attribs={pos:e.getAttribLocation(i,"a_pos")},this.uniforms={time:e.getUniformLocation(i,"u_time"),resolution:e.getUniformLocation(i,"u_resolution"),thickness:e.getUniformLocation(i,"u_thickness"),intensity:e.getUniformLocation(i,"u_intensity")},this.startTime=null,this.show=!1}showLine(){this.show=!0,this.startTime=null}hideLine(){this.show=!1,this.startTime=null}dispose(e){this.program&&e.deleteProgram(this.program),this.buffer&&e.deleteBuffer(this.buffer),this.program=null,this.buffer=null,this.attribs=null,this.uniforms=null,this.startTime=null,this.show=!1}render(e,t,i,s){if(!this.show||!this.program||!this.buffer||!this.attribs||!this.uniforms)return;const n=performance.now();this.startTime===null&&(this.startTime=n);const a=(n-this.startTime)/1e3;e.useProgram(this.program),e.bindBuffer(e.ARRAY_BUFFER,this.buffer),e.enableVertexAttribArray(this.attribs.pos),e.vertexAttribPointer(this.attribs.pos,2,e.FLOAT,!1,0,0),e.uniform1f(this.uniforms.time,a),e.uniform2f(this.uniforms.resolution,t.width,t.height),e.uniform1f(this.uniforms.thickness,i.thickness*s),e.uniform1f(this.uniforms.intensity,i.intensity),e.drawArrays(e.TRIANGLE_STRIP,0,4)}createProgram(e,t,i){const s=this.createShader(e,e.VERTEX_SHADER,t),n=this.createShader(e,e.FRAGMENT_SHADER,i);if(!s||!n)return s&&e.deleteShader(s),n&&e.deleteShader(n),null;const a=e.createProgram();return a?(e.attachShader(a,s),e.attachShader(a,n),e.linkProgram(a),e.getProgramParameter(a,e.LINK_STATUS)?(e.detachShader(a,s),e.detachShader(a,n),e.deleteShader(s),e.deleteShader(n),a):(e.deleteProgram(a),e.deleteShader(s),e.deleteShader(n),null)):null}createShader(e,t,i){const s=e.createShader(t);return s?(e.shaderSource(s,i),e.compileShader(s),e.getShaderParameter(s,e.COMPILE_STATUS)?s:(e.deleteShader(s),null)):null}}class K{gl=null;glProgram=null;glBuffer=null;glTexture=null;glAttribs=null;glUniforms=null;config=null;canvas=null;spriteImg=null;webglInitError=null;lineRenderer=null;showLine=!1;init(e){if(this.canvas=e.canvas,this.spriteImg=e.spriteImg,this.config={spriteWidth:e.spriteWidth,spriteHeight:e.spriteHeight,frameHeight:e.frameHeight,reels:e.reels,visibleSlots:e.visibleSlots,heightScale:e.heightScale,spinBlur:e.spinBlur,webglShading:e.webglShading,finalSpinLine:e.finalSpinLine},this.lineRenderer=e.finalSpinLine?new ge:null,this.tryInitWebGL(),!this.gl)throw new Error(`WebGL is not supported or failed to initialize. ${this.webglInitError??""}`.trim())}getError(){return this.webglInitError}render(e){!this.gl||!this.config||!this.spriteImg||!this.glProgram||!this.glAttribs||!this.glUniforms||!this.glBuffer||(this.renderWebGL(e),this.renderFinalLine(e))}onResize(e,t){!this.gl||!this.canvas||this.gl.viewport(0,0,e,t)}showFinalLine(){this.config?.finalSpinLine&&(this.showLine=!0,this.lineRenderer?.showLine())}hideFinalLine(){this.showLine=!1,this.lineRenderer?.hideLine()}dispose(){this.gl&&(this.glTexture&&this.gl.deleteTexture(this.glTexture),this.glBuffer&&this.gl.deleteBuffer(this.glBuffer),this.glProgram&&this.gl.deleteProgram(this.glProgram),this.lineRenderer&&this.lineRenderer.dispose(this.gl),this.glTexture=null,this.glBuffer=null,this.glProgram=null,this.glAttribs=null,this.glUniforms=null,this.gl=null,this.webglInitError=null)}tryInitWebGL(){if(!this.canvas||!this.spriteImg||!this.config)return;this.dispose(),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 l=document.createElement("canvas"),p=typeof WebGLRenderingContext<"u",o=l.getContext("webgl2")??l.getContext("webgl");this.webglInitError=`context is null (browser WebGL=${p?"yes":"no"}, fallback canvas=${o?"yes":"no"})`,console.warn("ReelDeal WebGL:",this.webglInitError);return}const t=this.createWebGLProgram(e,ce,de);if(!t){this.webglInitError="shader program failed to compile/link (see console)",console.warn("ReelDeal WebGL:",this.webglInitError);return}const i=e.createBuffer();if(!i){this.webglInitError="failed to create vertex buffer",console.warn("ReelDeal WebGL:",this.webglInitError),e.deleteProgram(t);return}e.bindBuffer(e.ARRAY_BUFFER,i);const s=new Float32Array([-1,-1,0,1,1,-1,1,1,-1,1,0,0,1,1,1,0]);e.bufferData(e.ARRAY_BUFFER,s,e.STATIC_DRAW);const n=e.createTexture();if(!n){this.webglInitError="failed to create texture",console.warn("ReelDeal WebGL:",this.webglInitError),e.deleteBuffer(i),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 a=l=>l>0&&(l&l-1)===0;a(this.config.spriteWidth)&&a(this.config.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=i,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")},this.config.finalSpinLine&&this.lineRenderer&&this.lineRenderer.init(e,this.config.finalSpinLine)}createWebGLProgram(e,t,i){const s=this.createWebGLShader(e,e.VERTEX_SHADER,t),n=this.createWebGLShader(e,e.FRAGMENT_SHADER,i);if(!s||!n)return s&&e.deleteShader(s),n&&e.deleteShader(n),null;const a=e.createProgram();if(!a)return null;if(e.attachShader(a,s),e.attachShader(a,n),e.linkProgram(a),!e.getProgramParameter(a,e.LINK_STATUS)){const u=e.getProgramInfoLog(a)??"unknown link error";return this.webglInitError=`program link failed: ${u}`,console.warn("ReelDeal WebGL: program link failed",u),e.deleteProgram(a),e.deleteShader(s),e.deleteShader(n),null}return e.detachShader(a,s),e.detachShader(a,n),e.deleteShader(s),e.deleteShader(n),a}createWebGLShader(e,t,i){const s=e.createShader(t);if(!s)return null;if(e.shaderSource(s,i),e.compileShader(s),!e.getShaderParameter(s,e.COMPILE_STATUS)){const n=e.getShaderInfoLog(s)??"unknown compile error";return this.webglInitError=`shader compile failed: ${n}`,console.warn("ReelDeal WebGL: shader compile failed",n),e.deleteShader(s),null}return s}getSpinBlurPxForReel(e,t){if(!this.config)return 0;const i=Math.abs(t[e]??0),s=Math.max(1,this.config.spinBlur.speedAtMax);return _(i/s)*Math.max(0,this.config.spinBlur.maxPx)}renderWebGL(e){if(!this.gl||!this.glProgram||!this.glAttribs||!this.glUniforms||!this.glTexture||!this.glBuffer||!this.config||!this.spriteImg||!this.canvas)return;const t=this.gl;t.useProgram(this.glProgram),t.bindBuffer(t.ARRAY_BUFFER,this.glBuffer),t.enableVertexAttribArray(this.glAttribs.pos),t.vertexAttribPointer(this.glAttribs.pos,2,t.FLOAT,!1,16,0),t.enableVertexAttribArray(this.glAttribs.uv),t.vertexAttribPointer(this.glAttribs.uv,2,t.FLOAT,!1,16,8),t.activeTexture(t.TEXTURE0),t.bindTexture(t.TEXTURE_2D,this.glTexture),t.uniform1i(this.glUniforms.texture,0),t.uniform1f(this.glUniforms.frameHeight,this.config.frameHeight),t.uniform1f(this.glUniforms.spriteHeight,this.config.spriteHeight),t.uniform1f(this.glUniforms.visibleSlots,this.config.visibleSlots),t.uniform1f(this.glUniforms.heightScale,this.config.heightScale),t.uniform1f(this.glUniforms.edgePower,Math.max(.5,this.config.webglShading.edgePower));const i=this.config.webglShading,s=Math.max(0,i.warpAngleDeg)*Math.PI/180;t.uniform1f(this.glUniforms.warpAngle,s),t.clear(t.COLOR_BUFFER_BIT);const n=1/this.config.reels;for(let a=0;a<this.config.reels;a+=1){t.uniform1f(this.glUniforms.reelX,a*n),t.uniform1f(this.glUniforms.reelW,n);const u=D(e.offsets[a],this.config.spriteHeight);t.uniform1f(this.glUniforms.offsetPx,u),t.uniform1f(this.glUniforms.blurPx,this.getSpinBlurPxForReel(a,e.velocities)),t.drawArrays(t.TRIANGLE_STRIP,0,4)}}renderFinalLine(e){!this.showLine||!this.config?.finalSpinLine||!this.gl||!this.canvas||!this.lineRenderer||this.lineRenderer.render(this.gl,this.canvas,this.config.finalSpinLine,e.dpr)}}const be=(r,e)=>{const t=document.getElementById(r);if(!t)throw new Error(`${e} not found: ${r}`);return t},W=(r,e)=>typeof r=="string"?be(r,e):r,Se=5,Z=.72,ve=.28,_e=7,xe=55,we=300,Ee=.35;class Ae{canvas;container;opts;visibleSlots=3;heightScale=2/3;centerIndex=1;reels;spinConfig;spinBlur;webglShading;idleBob;maxDpr;finalSpinLine;spriteImg=null;spriteWidth=0;spriteHeight=0;frameHeight=0;width=0;height=0;dpr=1;offsets;velocities;lastVelT=null;lastVelOffsets;idleStartTime=null;idleBaseOffsets;renderer=null;webglInitError=null;anim=null;loop=new he;pendingSpinResolvers=[];showFinalLine=!1;isLastSpin=!1;resizeObserver=null;resizeQueued=!1;button=null;uiController=null;spinQueueController;constructor(e){if(!e.canvas)throw new Error("ReelDeal: canvas is required");if(!e.container)throw new Error("ReelDeal: container is required");if(!e.sprite)throw new Error("ReelDeal: sprite is required");if(e.slotCount===void 0)throw new Error("ReelDeal: slotCount is required");const t={...e,canvas:e.canvas,container:e.container,sprite:e.sprite,slotCount:T(e.slotCount,1,Number.MAX_SAFE_INTEGER)};this.opts=t,this.reels=T(e.reels??1,1,Se),this.spinConfig=F(e.spinConfig),this.spinBlur=Y(e.spinBlur),this.webglShading=Q(e.webglShading),this.idleBob=q(e.idleBob),this.maxDpr=$(e.maxDpr),this.finalSpinLine=j(e.finalSpinLine),this.canvas=W(t.canvas,"Canvas"),this.container=W(t.container,"Container"),this.offsets=new Float32Array(this.reels),this.velocities=new Float32Array(this.reels),this.lastVelOffsets=new Float32Array(this.reels),this.idleBaseOffsets=new Float32Array(this.reels),this.spinQueueController=new ue({slotCount:t.slotCount,reels:this.reels,initialQueue:t.queuedSpinStates})}initRenderer(){if(!this.spriteImg)return;const e=new K;try{e.init({canvas:this.canvas,spriteImg:this.spriteImg,spriteWidth:this.spriteWidth,spriteHeight:this.spriteHeight,frameHeight:this.frameHeight,reels:this.reels,visibleSlots:this.visibleSlots,heightScale:this.heightScale,spinBlur:this.spinBlur,webglShading:this.webglShading,finalSpinLine:this.finalSpinLine}),this.webglInitError=e.getError(),this.renderer=e}catch(t){throw this.webglInitError=e.getError()??(t instanceof Error?t.message:String(t)),t}}async init(){const e=await k(this.opts.sprite,this.opts.slotCount);if(this.spriteImg=e.image,this.spriteWidth=e.spriteWidth,this.frameHeight=e.frameHeight,this.spriteHeight=e.spriteHeight,this.initRenderer(),!this.renderer)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.uiController=new fe(this.button,this.handleButtonClick),this.uiController.updateAvailability(!0),this.render(),this.startLoop()}destroy(){this.stop(),this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=null),this.uiController?.dispose(),this.uiController=null,this.renderer&&(this.renderer.dispose(),this.renderer=null)}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 t=e??[];for(let i=0;i<this.reels;i+=1){const s=t[i]??0,n=T(s,0,this.opts.slotCount-1);this.offsets[i]=n*this.frameHeight-this.centerIndex*this.frameHeight,this.lastVelOffsets[i]=this.offsets[i],this.velocities[i]=0,this.idleBaseOffsets[i]=this.offsets[i]}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,t){this.ensureReady();const i=Array.isArray(e)?{stopAtSegments:e,config:t}:e;if(i.signal?.aborted)return Promise.reject(i.signal.reason??new DOMException("Aborted","AbortError"));const s=this.getSpinConfig(i.config??t),n=s.speedPxS>0,a=this.getStopSpringOvershootPx(),u=Math.max(0,we),l=ne({stopAtSegments:i.stopAtSegments,currentOffsets:this.offsets,reels:this.reels,frameHeight:this.frameHeight,spriteHeight:this.spriteHeight,centerIndex:this.centerIndex,slotCount:this.opts.slotCount,spinConfig:s,stopSpringOvershootPx:a,stopSpringSettleMs:u,useSpeed:n});if(l.totalDelta===0||l.allSpinZero&&l.allSettleZero){for(let h=0;h<this.reels;h+=1)this.offsets[h]=l.targets[h];return this.anim=null,this.resolvePendingSpins(),this.render(),Promise.resolve()}const p=performance.now();this.anim={startTime:p,startOffsets:l.startOffsets,deltas:l.deltas,durationMs:l.durations,settleDurationMs:l.settleDurationMs,targets:l.targets},this.idleStartTime=null,this.startLoop();const o=new AbortController,c=()=>{const h=o.signal.reason??i.signal?.reason??"Spin aborted";this.stop(h)},S=()=>{o.abort(i.signal?.reason??new DOMException("Aborted","AbortError"))};i.signal&&i.signal.addEventListener("abort",S,{once:!0});let m=null;return i.timeoutMs&&i.timeoutMs>0&&(m=window.setTimeout(()=>{o.abort(new DOMException("Spin timed out","TimeoutError"))},i.timeoutMs)),new Promise((h,g)=>{const b=()=>{m!==null&&window.clearTimeout(m),o.signal.removeEventListener("abort",c),i.signal&&i.signal.removeEventListener("abort",S)};if(o.signal.aborted){b(),g(o.signal.reason??new DOMException("Aborted","AbortError"));return}o.signal.addEventListener("abort",c,{once:!0}),this.pendingSpinResolvers.push({resolve:()=>{b(),h()},reject:v=>{b(),g(v)},cleanup:b})})}getSpinConfig(e){return e?F({...this.spinConfig,...e}):this.spinConfig}getStopSpringOvershootPx(){const e=this.frameHeight*Ee,t=Math.max(0,xe);return Math.min(t,e)}async spinQueue(e){this.spinQueueController.reset(e),this.uiController?.updateAvailability(!0);for(let t=0;t<e.length;t+=1){const i=e[t];await this.spinOnce(i.stopAtSegments),i.callback?.(t,i.stopAtSegments)}this.uiController?.updateAvailability(!0)}stop(e){this.loop.stop(),this.anim=null,e!==void 0?this.rejectPendingSpins(e):this.resolvePendingSpins(),this.lastVelT=null,this.velocities.fill(0)}resolvePendingSpins(){if(this.pendingSpinResolvers.length===0)return;const e=this.pendingSpinResolvers;this.pendingSpinResolvers=[];for(const{resolve:t,cleanup:i}of e)i(),t()}rejectPendingSpins(e){if(this.pendingSpinResolvers.length===0)return;const t=this.pendingSpinResolvers;this.pendingSpinResolvers=[];for(const{reject:i,cleanup:s}of t)s(),i(e)}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.")}handleButtonClick=async()=>{this.hideFinalLine();const e=this.spinQueueController.consume();this.isLastSpin=e.isLast,this.uiController?.updateAvailability(!0),await this.spinOnce(e.state.stopAtSegments),this.isLastSpin&&this.finalSpinLine&&this.showFinalLineEffect()};resolveButton(){return this.opts.button?W(this.opts.button,"Button"):null}shouldAnimate(){return this.anim!==null||this.idleBob!==null||this.showFinalLine}startLoop(){this.loop.isRunning()||this.shouldAnimate()&&this.loop.start(this.tick)}tick=e=>{if(this.anim){const{startTime:t,durationMs:i,startOffsets:s,deltas:n,settleDurationMs:a,targets:u}=this.anim,l=e-t;let p=!0;for(let o=0;o<this.reels;o+=1){const c=i[o]??0,S=a[o]??0,m=u[o]??s[o]+n[o];if(c>0&&l<c){const h=Math.min(1,l/c),g=se(h);h<1&&(p=!1);const b=s[o]+n[o]*g;let v=0;if(h>Z&&this.frameHeight>0){const M=(1-_((h-Z)/ve))**2,I=b/this.frameHeight*Math.PI*2,z=Math.min(_e,this.frameHeight*.06);v=Math.sin(I)*M*z}this.offsets[o]=b+v}else if(S>0){const h=c<=0?l/S:(l-c)/S,g=_(h),b=s[o]+n[o]-m,v=ae(b,g);g<1&&(p=!1),this.offsets[o]=m+v}else this.offsets[o]=m}if(this.updateVelocity(e),this.render(),p){for(let o=0;o<this.reels;o+=1){const c=this.anim.targets[o]??this.anim.startOffsets[o]+this.anim.deltas[o];this.offsets[o]=c,this.idleBaseOffsets[o]=this.offsets[o]}this.updateVelocity(e);for(let o=0;o<this.reels;o+=1)this.velocities[o]=0;this.anim=null,this.idleStartTime=null,this.render(),this.resolvePendingSpins()}}else this.idleBob&&(this.updateIdle(e),this.render());return this.shouldAnimate()};render(){this.spriteImg&&(this.width<=0||this.height<=0||this.renderer&&this.renderer.render({offsets:this.offsets,velocities:this.velocities,dpr:this.dpr}))}updateVelocity(e){this.lastVelT=oe(e,{lastVelT:this.lastVelT,lastVelOffsets:this.lastVelOffsets,velocities:this.velocities},this.offsets)}updateIdle(e){this.idleBob&&(this.idleStartTime=le(e,{idleStartTime:this.idleStartTime,idleBaseOffsets:this.idleBaseOffsets,velocities:this.velocities},this.idleBob,this.offsets,this.reels))}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,t=this.container.clientHeight,i=this.visibleSlots*this.heightScale;if(e<=0||t<=0){this.applyResize(1,1);return}const s=i,n=this.reels,a=e/n*s;let u=e,l=a;l>t&&(l=t,u=t/s*n);const p=Math.max(1,Math.floor(u)),o=Math.max(1,Math.floor(l));this.applyResize(p,o),this.render()}applyViewportCrop(){this.ensureCanvasCoversContainer(),this.canvas.style.transform=""}applyResize(e,t){const i=Math.max(1,Math.floor(e)),s=Math.max(1,Math.floor(t)),n=this.getTargetDpr();this.width=i,this.height=s,this.dpr=n,this.canvas.width=Math.floor(this.width*this.dpr),this.canvas.height=Math.floor(this.height*this.dpr),this.renderer&&this.renderer.onResize(this.canvas.width,this.canvas.height),this.applyViewportCrop()}getTargetDpr(){const e=window.devicePixelRatio||1,t=Math.max(1,e);return Math.min(this.maxDpr,t)}hideFinalLine(){this.showFinalLine=!1,this.renderer?.hideFinalLine(),this.render()}showFinalLineEffect(){this.finalSpinLine&&(this.showFinalLine=!0,this.renderer?.showFinalLine(),this.startLoop())}}d.ReelDeal=Ae,d.WebGLRenderer=K,d.defaultIdleBob=E,d.defaultSpinBlur=C,d.defaultSpinConfig=w,d.loadSprite=k,d.normalizeFinalSpinLine=j,d.normalizeIdleBob=q,d.normalizeMaxDpr=$,d.normalizeSpinBlur=Y,d.normalizeSpinConfig=F,d.normalizeWebGLShading=Q,Object.defineProperty(d,Symbol.toStringTag,{value:"Module"})}));
81
135
  //# sourceMappingURL=index.umd.js.map