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.
@@ -0,0 +1,543 @@
1
+ const b = {
2
+ minSpins: 2,
3
+ durationMs: 4e3,
4
+ speedPxS: 4e3,
5
+ staggerMs: 300
6
+ }, y = {
7
+ warpAngleDeg: 90,
8
+ edgePower: 0.5
9
+ }, I = {
10
+ maxPx: 4,
11
+ speedAtMax: 2200
12
+ }, g = {
13
+ amplitudePx: 8,
14
+ speedHz: 0.25,
15
+ phaseOffsetRad: 0.9,
16
+ delayMs: 500,
17
+ rampMs: 800
18
+ }, L = 0.1, x = 0.6, B = (a, e, i) => Math.max(e, Math.min(i, Math.floor(a))), P = (a, e) => (a % e + e) % e, v = (a) => Math.max(0, Math.min(1, a)), U = (a) => {
19
+ const e = v(a);
20
+ return e ** 2 * (2 - e);
21
+ }, G = (a) => {
22
+ const e = v(a);
23
+ return e + e ** 2 - e ** 3;
24
+ }, X = (a) => {
25
+ const e = v(a);
26
+ if (e <= L) {
27
+ const s = e / L;
28
+ return U(s) * L;
29
+ }
30
+ if (e < x)
31
+ return e;
32
+ const i = (e - x) / (1 - x);
33
+ return x + G(i) * (1 - x);
34
+ }, E = (a, e) => typeof a == "number" && Number.isFinite(a) ? a : e, m = (a, e) => Math.max(0, E(a, e)), p = (a, e) => Math.max(0, Math.floor(E(a, e))), z = 2, D = (a) => {
35
+ const e = { ...b, ...a ?? {} };
36
+ return {
37
+ minSpins: p(e.minSpins, b.minSpins),
38
+ durationMs: p(e.durationMs, b.durationMs ?? 0),
39
+ speedPxS: p(e.speedPxS, b.speedPxS ?? 0),
40
+ staggerMs: p(e.staggerMs, b.staggerMs ?? 0)
41
+ };
42
+ }, N = (a) => {
43
+ const e = { ...I, ...a ?? {} };
44
+ return {
45
+ maxPx: m(e.maxPx, I.maxPx),
46
+ speedAtMax: m(e.speedAtMax, I.speedAtMax)
47
+ };
48
+ }, F = (a) => {
49
+ const e = { ...y, ...a ?? {} };
50
+ return {
51
+ warpAngleDeg: m(e.warpAngleDeg, y.warpAngleDeg),
52
+ edgePower: m(e.edgePower, y.edgePower)
53
+ };
54
+ }, V = (a) => {
55
+ if (a === !1) return null;
56
+ const e = { ...g, ...a ?? {} };
57
+ return {
58
+ amplitudePx: m(e.amplitudePx, g.amplitudePx),
59
+ speedHz: m(e.speedHz, g.speedHz),
60
+ phaseOffsetRad: E(e.phaseOffsetRad, g.phaseOffsetRad),
61
+ delayMs: p(e.delayMs, g.delayMs),
62
+ rampMs: p(e.rampMs, g.rampMs)
63
+ };
64
+ }, k = (a) => Math.max(1, E(a, z)), Y = (a, e) => {
65
+ const i = document.getElementById(a);
66
+ if (!i)
67
+ throw new Error(`${e} not found: ${a}`);
68
+ return i;
69
+ }, H = (a, e) => typeof a == "string" ? Y(a, e) : a, q = 5, C = 0.72, $ = 0.28, Q = 7, W = 0.85, K = 1 - W;
70
+ class j {
71
+ canvas;
72
+ container;
73
+ opts;
74
+ visibleSlots = 3;
75
+ heightScale = 2 / 3;
76
+ centerIndex = 1;
77
+ reels;
78
+ spinConfig;
79
+ spinBlur;
80
+ webglShading;
81
+ idleBob;
82
+ maxDpr;
83
+ spriteImg = null;
84
+ spriteWidth = 0;
85
+ spriteHeight = 0;
86
+ frameHeight = 0;
87
+ width = 0;
88
+ height = 0;
89
+ dpr = 1;
90
+ offsets;
91
+ velocities;
92
+ lastVelT = null;
93
+ lastVelOffsets;
94
+ idleStartTime = null;
95
+ idleBaseOffsets;
96
+ gl = null;
97
+ glProgram = null;
98
+ glBuffer = null;
99
+ glTexture = null;
100
+ glAttribs = null;
101
+ webglInitError = null;
102
+ glUniforms = null;
103
+ anim = null;
104
+ rafId = null;
105
+ pendingSpinResolvers = [];
106
+ resizeObserver = null;
107
+ resizeQueued = !1;
108
+ button = null;
109
+ buttonHandlerAttached = !1;
110
+ spinning = !1;
111
+ queuedSpins = null;
112
+ constructor(e) {
113
+ 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);
114
+ }
115
+ async init() {
116
+ if (await this.loadSprite(), this.tryInitWebGL(), !this.gl)
117
+ throw new Error(
118
+ `WebGL is not supported or failed to initialize. ${this.webglInitError ?? ""}`.trim()
119
+ );
120
+ 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();
121
+ }
122
+ destroy() {
123
+ 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();
124
+ }
125
+ ensureCanvasCoversContainer() {
126
+ 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"));
127
+ }
128
+ syncContainerLayoutVars() {
129
+ const e = this.canvas === this.container ? this.canvas : this.container;
130
+ 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");
131
+ }
132
+ setSegments(e) {
133
+ this.ensureReady();
134
+ const i = e ?? [];
135
+ for (let s = 0; s < this.reels; s += 1) {
136
+ const t = i[s] ?? 0, r = B(t, 0, this.opts.slotCount - 1);
137
+ 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];
138
+ }
139
+ this.lastVelT = null, this.idleStartTime = null, this.anim = null, this.stop(), this.render(), this.startLoop();
140
+ }
141
+ setSegment(e) {
142
+ this.setSegments(new Array(this.reels).fill(e));
143
+ }
144
+ spinOnce(e, i) {
145
+ this.ensureReady();
146
+ 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);
147
+ for (let l = 0; l < this.reels; l += 1) {
148
+ const T = B(e[l] ?? 0, 0, this.opts.slotCount - 1) * this.frameHeight - this.centerIndex * this.frameHeight, A = P(T, this.spriteHeight), O = P(this.offsets[l], this.spriteHeight), w = P(O - A, this.spriteHeight);
149
+ let _ = -(s * this.spriteHeight + w);
150
+ if (n && o > 0) {
151
+ const M = r * o / 1e3, R = Math.max(
152
+ s,
153
+ Math.ceil(Math.max(0, M - w) / this.spriteHeight)
154
+ );
155
+ _ = -(w + R * this.spriteHeight);
156
+ }
157
+ if (t > 0 && n) {
158
+ const M = r * t * l / 1e3, R = Math.ceil(M / this.spriteHeight);
159
+ _ -= R * this.spriteHeight;
160
+ }
161
+ 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);
162
+ }
163
+ if (f.reduce((l, S) => l + Math.abs(S), 0) === 0 || u.every((l) => l <= 0)) {
164
+ for (let l = 0; l < this.reels; l += 1)
165
+ this.offsets[l] = this.offsets[l] + f[l];
166
+ return this.anim = null, this.resolvePendingSpins(), this.render(), Promise.resolve();
167
+ }
168
+ const h = performance.now();
169
+ return this.anim = { startTime: h, startOffsets: c, deltas: f, durationMs: u }, this.idleStartTime = null, this.startLoop(), new Promise((l) => {
170
+ this.pendingSpinResolvers.push(l);
171
+ });
172
+ }
173
+ getSpinConfig(e) {
174
+ return e ? D({ ...this.spinConfig, ...e }) : this.spinConfig;
175
+ }
176
+ async spinQueue(e) {
177
+ for (let i = 0; i < e.length; i += 1) {
178
+ const s = e[i];
179
+ await this.spinOnce(s.stopAtSegments), s.callback?.(i, s.stopAtSegments);
180
+ }
181
+ }
182
+ stop() {
183
+ this.rafId !== null && cancelAnimationFrame(this.rafId), this.rafId = null, this.anim = null, this.resolvePendingSpins(), this.lastVelT = null;
184
+ for (let e = 0; e < this.reels; e += 1)
185
+ this.velocities[e] = 0;
186
+ }
187
+ resolvePendingSpins() {
188
+ if (this.pendingSpinResolvers.length === 0) return;
189
+ const e = this.pendingSpinResolvers;
190
+ this.pendingSpinResolvers = [];
191
+ for (const i of e)
192
+ i();
193
+ }
194
+ requestResize() {
195
+ this.queueResize();
196
+ }
197
+ ensureReady() {
198
+ if (!this.spriteImg || this.spriteWidth <= 0 || this.spriteHeight <= 0 || this.frameHeight <= 0)
199
+ throw new Error("Sprite is not ready. Call init() and await it first.");
200
+ }
201
+ attachButtonHandler() {
202
+ !this.button || this.buttonHandlerAttached || (this.button.addEventListener("click", this.handleButtonClick, { passive: !0 }), this.buttonHandlerAttached = !0);
203
+ }
204
+ handleButtonClick = async () => {
205
+ if (this.spinning) return;
206
+ const e = this.consumeNextSpin() ?? {
207
+ stopAtSegments: this.buildRandomSegments()
208
+ };
209
+ this.spinning = !0, this.setButtonDisabled(!0);
210
+ try {
211
+ await this.spinOnce(e.stopAtSegments);
212
+ } finally {
213
+ this.queuedSpins && this.queuedSpins.length === 0 ? this.setButtonDisabled(!0) : this.setButtonDisabled(!1), this.spinning = !1;
214
+ }
215
+ };
216
+ resolveButton() {
217
+ return this.opts.button ? H(this.opts.button, "Button") : null;
218
+ }
219
+ resolveSpinQueue() {
220
+ return this.opts.queuedSpinStates ?? null;
221
+ }
222
+ consumeNextSpin() {
223
+ return this.queuedSpins || (this.queuedSpins = this.resolveSpinQueue()), !this.queuedSpins || this.queuedSpins.length === 0 ? null : this.queuedSpins.shift() ?? null;
224
+ }
225
+ buildRandomSegments() {
226
+ const e = Math.max(1, Math.floor(this.opts.slotCount)), i = [];
227
+ for (let s = 0; s < this.reels; s += 1)
228
+ i.push(Math.floor(Math.random() * e));
229
+ return i;
230
+ }
231
+ setButtonDisabled(e) {
232
+ this.button && (this.button.disabled = e, e ? this.button.setAttribute("aria-busy", "true") : this.button.removeAttribute("aria-busy"));
233
+ }
234
+ shouldAnimate() {
235
+ return this.anim !== null || this.idleBob !== null;
236
+ }
237
+ startLoop() {
238
+ this.rafId === null && this.shouldAnimate() && (this.rafId = requestAnimationFrame(this.loop));
239
+ }
240
+ loop = (e) => {
241
+ if (this.anim) {
242
+ const { startTime: i, durationMs: s, startOffsets: t, deltas: r } = this.anim;
243
+ let o = !0;
244
+ for (let n = 0; n < this.reels; n += 1) {
245
+ const c = s[n] ?? 0, f = c <= 0 ? 1 : Math.min(1, (e - i) / c), u = X(f);
246
+ f < 1 && (o = !1);
247
+ const d = t[n] + r[n] * u;
248
+ let h = 0;
249
+ if (f > C && this.frameHeight > 0) {
250
+ const S = (1 - v((f - C) / $)) ** 2, T = d / this.frameHeight * Math.PI * 2, A = Math.min(Q, this.frameHeight * 0.06);
251
+ h = Math.sin(T) * S * A;
252
+ }
253
+ this.offsets[n] = d + h;
254
+ }
255
+ if (this.updateVelocity(e), this.render(), o) {
256
+ for (let n = 0; n < this.reels; n += 1)
257
+ this.offsets[n] = this.anim.startOffsets[n] + this.anim.deltas[n], this.idleBaseOffsets[n] = this.offsets[n];
258
+ this.updateVelocity(e);
259
+ for (let n = 0; n < this.reels; n += 1)
260
+ this.velocities[n] = 0;
261
+ this.anim = null, this.idleStartTime = null, this.render(), this.resolvePendingSpins();
262
+ }
263
+ } else this.idleBob && (this.updateIdle(e), this.render());
264
+ if (!this.shouldAnimate()) {
265
+ this.rafId = null;
266
+ return;
267
+ }
268
+ this.rafId !== null && (this.rafId = requestAnimationFrame(this.loop));
269
+ };
270
+ render() {
271
+ this.spriteImg && (this.width <= 0 || this.height <= 0 || this.gl && this.renderWebGL());
272
+ }
273
+ getSpinBlurPxForReel(e) {
274
+ if (!this.anim) return 0;
275
+ const i = Math.abs(this.velocities[e] ?? 0), s = Math.max(1, this.spinBlur.speedAtMax);
276
+ return v(i / s) * Math.max(0, this.spinBlur.maxPx);
277
+ }
278
+ renderWebGL() {
279
+ if (!this.gl || !this.glProgram || !this.glAttribs || !this.glUniforms || !this.glTexture || !this.glBuffer || !this.spriteImg) return;
280
+ const e = this.gl;
281
+ 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(0.5, this.webglShading.edgePower));
282
+ const i = this.webglShading, s = Math.max(0, i.warpAngleDeg) * Math.PI / 180;
283
+ e.uniform1f(this.glUniforms.warpAngle, s), e.clear(e.COLOR_BUFFER_BIT);
284
+ const t = 1 / this.reels;
285
+ for (let r = 0; r < this.reels; r += 1) {
286
+ e.uniform1f(this.glUniforms.reelX, r * t), e.uniform1f(this.glUniforms.reelW, t);
287
+ const o = P(this.offsets[r], this.spriteHeight);
288
+ e.uniform1f(this.glUniforms.offsetPx, o), e.uniform1f(this.glUniforms.blurPx, this.getSpinBlurPxForReel(r)), e.drawArrays(e.TRIANGLE_STRIP, 0, 4);
289
+ }
290
+ }
291
+ updateVelocity(e) {
292
+ if (this.lastVelT === null) {
293
+ this.lastVelT = e;
294
+ for (let s = 0; s < this.reels; s += 1)
295
+ this.lastVelOffsets[s] = this.offsets[s], this.velocities[s] = 0;
296
+ return;
297
+ }
298
+ const i = (e - this.lastVelT) / 1e3;
299
+ if (!(i <= 0)) {
300
+ for (let s = 0; s < this.reels; s += 1) {
301
+ const t = (this.offsets[s] - this.lastVelOffsets[s]) / i;
302
+ this.velocities[s] = this.velocities[s] * W + t * K, this.lastVelOffsets[s] = this.offsets[s];
303
+ }
304
+ this.lastVelT = e;
305
+ }
306
+ }
307
+ updateIdle(e) {
308
+ if (!this.idleBob) return;
309
+ this.idleStartTime === null && (this.idleStartTime = e);
310
+ const { amplitudePx: i, speedHz: s, phaseOffsetRad: t, delayMs: r, rampMs: o } = this.idleBob, n = e - this.idleStartTime;
311
+ if (n < Math.max(0, r)) {
312
+ for (let h = 0; h < this.reels; h += 1)
313
+ this.offsets[h] = this.idleBaseOffsets[h], this.velocities[h] = 0;
314
+ return;
315
+ }
316
+ const c = (n - Math.max(0, r)) / 1e3, f = Math.max(
317
+ 0,
318
+ Math.min(1, (n - Math.max(0, r)) / Math.max(1, o))
319
+ ), u = U(f), d = s * Math.PI * 2;
320
+ for (let h = 0; h < this.reels; h += 1) {
321
+ const l = c * d + h * t;
322
+ this.offsets[h] = this.idleBaseOffsets[h] + Math.sin(l) * (i * u), this.velocities[h] = 0;
323
+ }
324
+ }
325
+ async loadSprite() {
326
+ const { sprite: e } = this.opts, i = typeof e == "string" ? new Image() : e;
327
+ typeof e == "string" && (i.src = e), (!i.complete || i.naturalWidth <= 0) && await new Promise((f, u) => {
328
+ const d = () => f(), h = () => u(new Error("Failed to load sprite image"));
329
+ i.addEventListener("load", d, { once: !0 }), i.addEventListener("error", h, { once: !0 });
330
+ });
331
+ const s = Math.max(1, Math.floor(this.opts.slotCount)), t = i.naturalWidth, r = t, o = r * s, n = i.naturalHeight;
332
+ if (Math.abs(n - o) > 2)
333
+ throw new Error(
334
+ `Sprite size mismatch. Expected height ~${Math.round(o)}px (slotCount=${s}, slot aspect 1/1), got ${n}px.`
335
+ );
336
+ this.spriteImg = i, this.spriteWidth = t, this.frameHeight = Math.max(1, Math.round(r)), this.spriteHeight = Math.max(1, Math.round(this.frameHeight * s));
337
+ }
338
+ setupResizeWatcher() {
339
+ typeof ResizeObserver > "u" || this.resizeObserver || (this.resizeObserver = new ResizeObserver(() => this.queueResize()), this.resizeObserver.observe(this.container));
340
+ }
341
+ queueResize = () => {
342
+ this.resizeQueued || (this.resizeQueued = !0, requestAnimationFrame(() => {
343
+ this.resizeQueued = !1, this.resizeToContainer();
344
+ }));
345
+ };
346
+ resizeToContainer() {
347
+ const e = this.container.clientWidth, i = this.container.clientHeight, s = this.visibleSlots * this.heightScale;
348
+ if (e <= 0 || i <= 0) {
349
+ this.applyResize(1, 1);
350
+ return;
351
+ }
352
+ const t = s, r = this.reels, o = e / r * t;
353
+ let n = e, c = o;
354
+ c > i && (c = i, n = i / t * r);
355
+ const f = Math.max(1, Math.floor(n)), u = Math.max(1, Math.floor(c));
356
+ this.applyResize(f, u), this.render();
357
+ }
358
+ applyViewportCrop() {
359
+ this.canvas.style.width = "100%", this.canvas.style.height = "100%", this.canvas.style.transform = "";
360
+ }
361
+ applyResize(e, i) {
362
+ const s = Math.max(1, Math.floor(e)), t = Math.max(1, Math.floor(i)), r = this.getTargetDpr();
363
+ 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();
364
+ }
365
+ getTargetDpr() {
366
+ const e = window.devicePixelRatio || 1, i = Math.max(1, e);
367
+ return Math.min(this.maxDpr, i);
368
+ }
369
+ tryInitWebGL() {
370
+ if (!this.spriteImg) return;
371
+ this.releaseWebGLResources(), this.webglInitError = null;
372
+ const e = this.canvas.getContext("webgl2", {
373
+ antialias: !0,
374
+ alpha: !0
375
+ }) ?? this.canvas.getContext("webgl", {
376
+ antialias: !0,
377
+ alpha: !0
378
+ }) ?? this.canvas.getContext("experimental-webgl", {
379
+ antialias: !0,
380
+ alpha: !0
381
+ });
382
+ if (!e) {
383
+ const u = document.createElement("canvas"), d = typeof WebGLRenderingContext < "u", h = u.getContext("webgl2") ?? u.getContext("webgl");
384
+ this.webglInitError = `context is null (browser WebGL=${d ? "yes" : "no"}, fallback canvas=${h ? "yes" : "no"})`, console.warn("ReelDeal WebGL:", this.webglInitError);
385
+ return;
386
+ }
387
+ const t = this.createWebGLProgram(e, `
388
+ attribute vec2 a_pos;
389
+ attribute vec2 a_uv;
390
+ varying vec2 v_uv;
391
+ void main() {
392
+ v_uv = a_uv;
393
+ gl_Position = vec4(a_pos, 0.0, 1.0);
394
+ }
395
+ `, `
396
+ #ifdef GL_FRAGMENT_PRECISION_HIGH
397
+ precision highp float;
398
+ #else
399
+ precision mediump float;
400
+ #endif
401
+
402
+ varying vec2 v_uv;
403
+ uniform sampler2D u_tex;
404
+ uniform float u_reelX;
405
+ uniform float u_reelW;
406
+ uniform float u_offsetPx;
407
+ uniform float u_frameHeight;
408
+ uniform float u_spriteHeight;
409
+ uniform float u_visibleSlots;
410
+ uniform float u_heightScale;
411
+ uniform float u_edgePower;
412
+ uniform float u_warpAngle;
413
+ uniform float u_blurPx;
414
+
415
+ float saturate(float x) { return clamp(x, 0.0, 1.0); }
416
+
417
+ void main() {
418
+ if (v_uv.x < u_reelX || v_uv.x > (u_reelX + u_reelW)) {
419
+ discard;
420
+ }
421
+
422
+ float localX = (v_uv.x - u_reelX) / u_reelW;
423
+ // v_uv.y is authored as 0 at TOP, 1 at BOTTOM (Canvas-like).
424
+ float localY = v_uv.y;
425
+ float hs = max(0.0001, u_heightScale);
426
+ float fullY = localY * hs + (1.0 - hs) * 0.5;
427
+
428
+ float warp = max(0.0, u_warpAngle);
429
+ float warpStrength = saturate(warp / 1.2);
430
+ float center = 0.5;
431
+ float centerBand = 1.0 / 3.0;
432
+ float halfBand = centerBand * 0.5;
433
+ float dist = abs(fullY - center);
434
+ // Smoother transition: avoid a hard boundary at halfBand.
435
+ float edgeWeight = smoothstep(halfBand, 0.5, dist);
436
+ float edgePow = max(0.5, u_edgePower);
437
+ edgeWeight = pow(edgeWeight, edgePow) * warpStrength;
438
+ float tEase = smoothstep(0.0, 1.0, fullY);
439
+ float warpedY = mix(fullY, tEase, edgeWeight);
440
+ float y = (warpedY - 0.5) * 2.0;
441
+ float theta = y * warp;
442
+ float edgeWarp = edgeWeight;
443
+
444
+ float yPx = u_offsetPx + warpedY * (u_frameHeight * u_visibleSlots);
445
+ // With UNPACK_FLIP_Y_WEBGL=0 and Canvas-like UVs (v=0 at TOP),
446
+ // v=0 samples the TOP of the original image, so pixel Y-from-top maps directly to v.
447
+ // Normalize early to improve precision on mobile GPUs.
448
+ float v = fract((u_offsetPx / u_spriteHeight) +
449
+ warpedY * ((u_frameHeight * u_visibleSlots) / u_spriteHeight));
450
+
451
+ vec4 base = texture2D(u_tex, vec2(localX, v));
452
+
453
+ if (u_blurPx > 0.0) {
454
+ // Clamp blur step to reduce banding on mobile GPUs.
455
+ // Scale max step with speed proxy (u_blurPx).
456
+ float maxStepPx = 1.0 + 3.0 * saturate(u_blurPx / 4.0);
457
+ float blurPx = min(u_blurPx, maxStepPx);
458
+ float blurStep = blurPx / max(1.0, u_spriteHeight);
459
+ // 9-tap gaussian-ish blur to reduce banding on mobile.
460
+ vec4 sum = base * 0.227027;
461
+ sum += texture2D(u_tex, vec2(localX, fract(v + blurStep))) * 0.1945946;
462
+ sum += texture2D(u_tex, vec2(localX, fract(v - blurStep))) * 0.1945946;
463
+ sum += texture2D(u_tex, vec2(localX, fract(v + 2.0 * blurStep))) * 0.1216216;
464
+ sum += texture2D(u_tex, vec2(localX, fract(v - 2.0 * blurStep))) * 0.1216216;
465
+ sum += texture2D(u_tex, vec2(localX, fract(v + 3.0 * blurStep))) * 0.054054;
466
+ sum += texture2D(u_tex, vec2(localX, fract(v - 3.0 * blurStep))) * 0.054054;
467
+ sum += texture2D(u_tex, vec2(localX, fract(v + 4.0 * blurStep))) * 0.016216;
468
+ sum += texture2D(u_tex, vec2(localX, fract(v - 4.0 * blurStep))) * 0.016216;
469
+ base = sum;
470
+ }
471
+
472
+ gl_FragColor = base;
473
+ }
474
+ `);
475
+ if (!t) {
476
+ this.webglInitError = "shader program failed to compile/link (see console)", console.warn("ReelDeal WebGL:", this.webglInitError);
477
+ return;
478
+ }
479
+ const r = e.createBuffer();
480
+ if (!r) {
481
+ this.webglInitError = "failed to create vertex buffer", console.warn("ReelDeal WebGL:", this.webglInitError), e.deleteProgram(t);
482
+ return;
483
+ }
484
+ e.bindBuffer(e.ARRAY_BUFFER, r);
485
+ const o = new Float32Array([-1, -1, 0, 1, 1, -1, 1, 1, -1, 1, 0, 0, 1, 1, 1, 0]);
486
+ e.bufferData(e.ARRAY_BUFFER, o, e.STATIC_DRAW);
487
+ const n = e.createTexture();
488
+ if (!n) {
489
+ this.webglInitError = "failed to create texture", console.warn("ReelDeal WebGL:", this.webglInitError), e.deleteBuffer(r), e.deleteProgram(t);
490
+ return;
491
+ }
492
+ 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);
493
+ const c = (u) => u > 0 && (u & u - 1) === 0;
494
+ 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 = {
495
+ pos: e.getAttribLocation(t, "a_pos"),
496
+ uv: e.getAttribLocation(t, "a_uv")
497
+ }, this.glUniforms = {
498
+ texture: e.getUniformLocation(t, "u_tex"),
499
+ reelX: e.getUniformLocation(t, "u_reelX"),
500
+ reelW: e.getUniformLocation(t, "u_reelW"),
501
+ offsetPx: e.getUniformLocation(t, "u_offsetPx"),
502
+ frameHeight: e.getUniformLocation(t, "u_frameHeight"),
503
+ spriteHeight: e.getUniformLocation(t, "u_spriteHeight"),
504
+ visibleSlots: e.getUniformLocation(t, "u_visibleSlots"),
505
+ heightScale: e.getUniformLocation(t, "u_heightScale"),
506
+ edgePower: e.getUniformLocation(t, "u_edgePower"),
507
+ warpAngle: e.getUniformLocation(t, "u_warpAngle"),
508
+ blurPx: e.getUniformLocation(t, "u_blurPx")
509
+ };
510
+ }
511
+ createWebGLProgram(e, i, s) {
512
+ const t = this.createWebGLShader(e, e.VERTEX_SHADER, i), r = this.createWebGLShader(e, e.FRAGMENT_SHADER, s);
513
+ if (!t || !r)
514
+ return t && e.deleteShader(t), r && e.deleteShader(r), null;
515
+ const o = e.createProgram();
516
+ if (!o) return null;
517
+ if (e.attachShader(o, t), e.attachShader(o, r), e.linkProgram(o), !e.getProgramParameter(o, e.LINK_STATUS)) {
518
+ const n = e.getProgramInfoLog(o) ?? "unknown link error";
519
+ 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;
520
+ }
521
+ return e.detachShader(o, t), e.detachShader(o, r), e.deleteShader(t), e.deleteShader(r), o;
522
+ }
523
+ createWebGLShader(e, i, s) {
524
+ const t = e.createShader(i);
525
+ if (!t) return null;
526
+ if (e.shaderSource(t, s), e.compileShader(t), !e.getShaderParameter(t, e.COMPILE_STATUS)) {
527
+ const r = e.getShaderInfoLog(t) ?? "unknown compile error";
528
+ return this.webglInitError = `shader compile failed: ${r}`, console.warn("ReelDeal WebGL: shader compile failed", r), e.deleteShader(t), null;
529
+ }
530
+ return t;
531
+ }
532
+ releaseWebGLResources() {
533
+ 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);
534
+ }
535
+ }
536
+ export {
537
+ j as ReelDeal,
538
+ g as defaultIdleBob,
539
+ I as defaultSpinBlur,
540
+ b as defaultSpinConfig,
541
+ y as defaultWebGLShading
542
+ };
543
+ //# sourceMappingURL=index.es.js.map