cascading-reel 1.0.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +169 -169
- package/dist/index.cjs +1512 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +168 -0
- package/dist/index.d.ts +168 -169
- package/dist/index.js +1510 -0
- package/dist/index.js.map +1 -0
- package/package.json +35 -27
- package/dist/index.cjs.js +0 -131
- package/dist/index.cjs.js.map +0 -1
- package/dist/index.es.js +0 -1069
- package/dist/index.es.js.map +0 -1
- package/dist/index.umd.js +0 -131
- package/dist/index.umd.js.map +0 -1
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1512 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
|
|
3
|
+
//#region src/constants.ts
|
|
4
|
+
const ROW_COMPACT_OFFSETS_RATIO = [
|
|
5
|
+
.04,
|
|
6
|
+
0,
|
|
7
|
+
-.04
|
|
8
|
+
];
|
|
9
|
+
const DEFAULT_MOTION_PROFILE = {
|
|
10
|
+
columnStaggerMs: 76,
|
|
11
|
+
fallMs: 800,
|
|
12
|
+
outroOverlapMs: 620,
|
|
13
|
+
outroRowGapMs: 14,
|
|
14
|
+
rowBaseSpacingRatio: .05,
|
|
15
|
+
incomingAlphaRampMs: 34,
|
|
16
|
+
fixedStepMs: 1e3 / 120,
|
|
17
|
+
maxCatchUpStepsPerFrame: 6
|
|
18
|
+
};
|
|
19
|
+
const FLOW_OUTRO_ROW_GAP_MS = DEFAULT_MOTION_PROFILE.outroRowGapMs;
|
|
20
|
+
const FLOW_ROW_BASE_SPACING_RATIO = DEFAULT_MOTION_PROFILE.rowBaseSpacingRatio;
|
|
21
|
+
const FLOW_WIN_PULSE_PERIOD_MS = 1800;
|
|
22
|
+
const FLOW_WIN_PULSE_AMPLITUDE = .15;
|
|
23
|
+
const DEFAULT_PARTICLE_COLOR_RGB = [
|
|
24
|
+
255,
|
|
25
|
+
235,
|
|
26
|
+
110
|
|
27
|
+
];
|
|
28
|
+
const FLOW_COLUMN_STAGGER_MS = DEFAULT_MOTION_PROFILE.columnStaggerMs;
|
|
29
|
+
const FLOW_FALL_MS = DEFAULT_MOTION_PROFILE.fallMs;
|
|
30
|
+
const FLOW_OUTRO_OVERLAP_MS = DEFAULT_MOTION_PROFILE.outroOverlapMs;
|
|
31
|
+
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/utils/math.ts
|
|
34
|
+
function clamp(value, min, max) {
|
|
35
|
+
if (value < min) return min;
|
|
36
|
+
if (value > max) return max;
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
function randomInt(maxExclusive) {
|
|
40
|
+
return Math.floor(Math.random() * maxExclusive);
|
|
41
|
+
}
|
|
42
|
+
function normalizeSegment(segment, elementsCount) {
|
|
43
|
+
return (segment % elementsCount + elementsCount) % elementsCount;
|
|
44
|
+
}
|
|
45
|
+
function normalizeRgbChannel(value) {
|
|
46
|
+
return clamp(Math.round(value), 0, 255);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/core/grid.ts
|
|
51
|
+
function createRandomGrid(spriteElementsCount) {
|
|
52
|
+
const grid = [];
|
|
53
|
+
for (let col = 0; col < 3; col += 1) {
|
|
54
|
+
const column = [];
|
|
55
|
+
for (let row = 0; row < 3; row += 1) column.push(randomInt(spriteElementsCount));
|
|
56
|
+
grid.push(column);
|
|
57
|
+
}
|
|
58
|
+
return grid;
|
|
59
|
+
}
|
|
60
|
+
function findMostFrequentCells(grid) {
|
|
61
|
+
const counts = /* @__PURE__ */ new Map();
|
|
62
|
+
for (let col = 0; col < 3; col += 1) for (let row = 0; row < 3; row += 1) {
|
|
63
|
+
const symbol = grid[col][row];
|
|
64
|
+
counts.set(symbol, (counts.get(symbol) ?? 0) + 1);
|
|
65
|
+
}
|
|
66
|
+
let selectedSymbol = grid[0][0];
|
|
67
|
+
let maxCount = -1;
|
|
68
|
+
for (const [symbol, count] of counts.entries()) if (count > maxCount) {
|
|
69
|
+
maxCount = count;
|
|
70
|
+
selectedSymbol = symbol;
|
|
71
|
+
}
|
|
72
|
+
const cells = [];
|
|
73
|
+
for (let col = 0; col < 3; col += 1) for (let row = 0; row < 3; row += 1) if (grid[col][row] === selectedSymbol) cells.push({
|
|
74
|
+
col,
|
|
75
|
+
row
|
|
76
|
+
});
|
|
77
|
+
return cells;
|
|
78
|
+
}
|
|
79
|
+
function createZeroOffsets() {
|
|
80
|
+
return Array.from({ length: 3 }, () => Array.from({ length: 3 }, () => 0));
|
|
81
|
+
}
|
|
82
|
+
function fillOffsets(offsets, value) {
|
|
83
|
+
for (let col = 0; col < 3; col += 1) for (let row = 0; row < 3; row += 1) offsets[col][row] = value;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/core/loop.ts
|
|
88
|
+
var RafLoop = class {
|
|
89
|
+
rafId = null;
|
|
90
|
+
step = null;
|
|
91
|
+
start(step) {
|
|
92
|
+
if (this.rafId !== null) return;
|
|
93
|
+
this.step = step;
|
|
94
|
+
this.rafId = requestAnimationFrame(this.tick);
|
|
95
|
+
}
|
|
96
|
+
stop() {
|
|
97
|
+
if (this.rafId !== null) {
|
|
98
|
+
cancelAnimationFrame(this.rafId);
|
|
99
|
+
this.rafId = null;
|
|
100
|
+
}
|
|
101
|
+
this.step = null;
|
|
102
|
+
}
|
|
103
|
+
isRunning() {
|
|
104
|
+
return this.rafId !== null;
|
|
105
|
+
}
|
|
106
|
+
tick = (now) => {
|
|
107
|
+
if (!this.step) {
|
|
108
|
+
this.stop();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (this.step(now) === false) {
|
|
112
|
+
this.stop();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (this.rafId === null) return;
|
|
116
|
+
this.rafId = requestAnimationFrame(this.tick);
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region src/core/motionTimeline.ts
|
|
122
|
+
function smootherStep(t) {
|
|
123
|
+
const x = clamp(t, 0, 1);
|
|
124
|
+
return x * x * x * (x * (x * 6 - 15) + 10);
|
|
125
|
+
}
|
|
126
|
+
function sampleSegment(segment, nowMs) {
|
|
127
|
+
if (segment.endMs <= segment.startMs) return {
|
|
128
|
+
value: segment.to,
|
|
129
|
+
t: 1,
|
|
130
|
+
done: true
|
|
131
|
+
};
|
|
132
|
+
const t = clamp((nowMs - segment.startMs) / (segment.endMs - segment.startMs), 0, 1);
|
|
133
|
+
const eased = smootherStep(t);
|
|
134
|
+
return {
|
|
135
|
+
value: segment.from + (segment.to - segment.from) * eased,
|
|
136
|
+
t,
|
|
137
|
+
done: t >= 1
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
//#endregion
|
|
142
|
+
//#region src/core/outro.ts
|
|
143
|
+
function buildSequentialRowStartDelays(fromRowOffsets, durationMs, gapMs, rowBaseSpacingRatio) {
|
|
144
|
+
const delays = [
|
|
145
|
+
0,
|
|
146
|
+
0,
|
|
147
|
+
0
|
|
148
|
+
];
|
|
149
|
+
let nextDelay = 0;
|
|
150
|
+
for (let row = 3 - 1; row >= 0; row -= 1) {
|
|
151
|
+
if (fromRowOffsets[row] === 0) {
|
|
152
|
+
delays[row] = 0;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
delays[row] = nextDelay;
|
|
156
|
+
const baseSpacing = Math.floor(durationMs * rowBaseSpacingRatio);
|
|
157
|
+
nextDelay += baseSpacing + gapMs;
|
|
158
|
+
}
|
|
159
|
+
return delays;
|
|
160
|
+
}
|
|
161
|
+
function buildOutroMotionPlan(params) {
|
|
162
|
+
const outgoingDistance = params.height - params.boardY + params.cellH + 2;
|
|
163
|
+
const outgoingOffsetsForOrder = [
|
|
164
|
+
outgoingDistance,
|
|
165
|
+
outgoingDistance,
|
|
166
|
+
outgoingDistance
|
|
167
|
+
];
|
|
168
|
+
return {
|
|
169
|
+
columnStaggerMs: params.motionProfile.columnStaggerMs,
|
|
170
|
+
fallMs: params.motionProfile.fallMs,
|
|
171
|
+
incomingAlphaRampMs: params.motionProfile.incomingAlphaRampMs,
|
|
172
|
+
outgoingDistance,
|
|
173
|
+
incomingFromOffsets: [
|
|
174
|
+
-params.cellH,
|
|
175
|
+
-params.cellH * 2,
|
|
176
|
+
-params.cellH * 3
|
|
177
|
+
],
|
|
178
|
+
rowStartDelays: buildSequentialRowStartDelays(outgoingOffsetsForOrder, params.motionProfile.fallMs, params.motionProfile.outroRowGapMs, params.motionProfile.rowBaseSpacingRatio),
|
|
179
|
+
incomingStartShift: Math.max(0, params.motionProfile.fallMs - params.motionProfile.outroOverlapMs)
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function updateOutroOffsets(params) {
|
|
183
|
+
let allOutgoingDone = true;
|
|
184
|
+
let allIncomingDone = true;
|
|
185
|
+
for (let col = 0; col < params.scriptedOutgoingOffsets.length; col += 1) {
|
|
186
|
+
const columnElapsed = params.elapsedMs - col * params.motionPlan.columnStaggerMs;
|
|
187
|
+
for (let row = 0; row < 3; row += 1) {
|
|
188
|
+
const rowElapsed = columnElapsed - params.motionPlan.rowStartDelays[row];
|
|
189
|
+
if (rowElapsed <= 0) {
|
|
190
|
+
params.scriptedOutgoingOffsets[col][row] = 0;
|
|
191
|
+
params.scriptedIncomingOffsets[col][row] = params.motionPlan.incomingFromOffsets[row];
|
|
192
|
+
params.scriptedIncomingAlpha[col][row] = 0;
|
|
193
|
+
params.scriptedIncomingVisibility[col][row] = "hidden";
|
|
194
|
+
allOutgoingDone = false;
|
|
195
|
+
allIncomingDone = false;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const outgoing = sampleSegment({
|
|
199
|
+
startMs: 0,
|
|
200
|
+
endMs: params.motionPlan.fallMs,
|
|
201
|
+
from: 0,
|
|
202
|
+
to: params.motionPlan.outgoingDistance
|
|
203
|
+
}, rowElapsed);
|
|
204
|
+
params.scriptedOutgoingOffsets[col][row] = outgoing.value;
|
|
205
|
+
params.scriptedIncomingVisibility[col][row] = outgoing.done ? "entering" : "exiting";
|
|
206
|
+
if (!outgoing.done) allOutgoingDone = false;
|
|
207
|
+
const incomingElapsed = rowElapsed - params.motionPlan.incomingStartShift;
|
|
208
|
+
if (incomingElapsed <= 0) {
|
|
209
|
+
params.scriptedIncomingOffsets[col][row] = params.motionPlan.incomingFromOffsets[row];
|
|
210
|
+
params.scriptedIncomingAlpha[col][row] = 0;
|
|
211
|
+
params.scriptedIncomingVisibility[col][row] = "hidden";
|
|
212
|
+
allIncomingDone = false;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const incoming = sampleSegment({
|
|
216
|
+
startMs: 0,
|
|
217
|
+
endMs: params.motionPlan.fallMs,
|
|
218
|
+
from: params.motionPlan.incomingFromOffsets[row],
|
|
219
|
+
to: 0
|
|
220
|
+
}, incomingElapsed);
|
|
221
|
+
params.scriptedIncomingOffsets[col][row] = incoming.value;
|
|
222
|
+
params.scriptedIncomingAlpha[col][row] = clamp(incomingElapsed / Math.max(1, params.motionPlan.incomingAlphaRampMs), 0, 1);
|
|
223
|
+
params.scriptedIncomingVisibility[col][row] = incoming.done ? "active" : "entering";
|
|
224
|
+
if (!incoming.done) allIncomingDone = false;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
allOutgoingDone,
|
|
229
|
+
allIncomingDone
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
//#endregion
|
|
234
|
+
//#region src/normalize.ts
|
|
235
|
+
function normalizeParticleColor(color) {
|
|
236
|
+
if (color === "rainbow") return {
|
|
237
|
+
mode: "rainbow",
|
|
238
|
+
rgb: DEFAULT_PARTICLE_COLOR_RGB
|
|
239
|
+
};
|
|
240
|
+
const rgb = color ?? DEFAULT_PARTICLE_COLOR_RGB;
|
|
241
|
+
return {
|
|
242
|
+
mode: "solid",
|
|
243
|
+
rgb: [
|
|
244
|
+
normalizeRgbChannel(rgb[0]),
|
|
245
|
+
normalizeRgbChannel(rgb[1]),
|
|
246
|
+
normalizeRgbChannel(rgb[2])
|
|
247
|
+
]
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function normalizeSymbolScale(value, fallback) {
|
|
251
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
252
|
+
return clamp(value, .5, 1.2);
|
|
253
|
+
}
|
|
254
|
+
function rowsToStopGrid(rows) {
|
|
255
|
+
if (rows.length !== 3) throw new Error(`rows must contain ${3} rows`);
|
|
256
|
+
for (let row = 0; row < 3; row += 1) if (!Array.isArray(rows[row]) || rows[row].length !== 3) throw new Error(`rows[${row}] must contain ${3} columns`);
|
|
257
|
+
return [
|
|
258
|
+
[
|
|
259
|
+
rows[0][0],
|
|
260
|
+
rows[1][0],
|
|
261
|
+
rows[2][0]
|
|
262
|
+
],
|
|
263
|
+
[
|
|
264
|
+
rows[0][1],
|
|
265
|
+
rows[1][1],
|
|
266
|
+
rows[2][1]
|
|
267
|
+
],
|
|
268
|
+
[
|
|
269
|
+
rows[0][2],
|
|
270
|
+
rows[1][2],
|
|
271
|
+
rows[2][2]
|
|
272
|
+
]
|
|
273
|
+
];
|
|
274
|
+
}
|
|
275
|
+
function normalizeStopGrid(stopGrid, elementsCount) {
|
|
276
|
+
if (stopGrid.length !== 3) throw new Error(`stopGrid must contain ${3} columns`);
|
|
277
|
+
const next = [];
|
|
278
|
+
for (let col = 0; col < 3; col += 1) {
|
|
279
|
+
const column = stopGrid[col];
|
|
280
|
+
if (!Array.isArray(column) || column.length !== 3) throw new Error(`stopGrid[${col}] must contain ${3} rows`);
|
|
281
|
+
next[col] = [
|
|
282
|
+
normalizeSegment(column[0], elementsCount),
|
|
283
|
+
normalizeSegment(column[1], elementsCount),
|
|
284
|
+
normalizeSegment(column[2], elementsCount)
|
|
285
|
+
];
|
|
286
|
+
}
|
|
287
|
+
return next;
|
|
288
|
+
}
|
|
289
|
+
function normalizeInitialSegments(initialSegments, elementsCount) {
|
|
290
|
+
return normalizeStopGrid(rowsToStopGrid(initialSegments), elementsCount);
|
|
291
|
+
}
|
|
292
|
+
function cloneSpinState(state) {
|
|
293
|
+
return {
|
|
294
|
+
stopGrid: state.stopGrid?.map((column) => [...column]),
|
|
295
|
+
stopRows: state.stopRows?.map((row) => [...row]),
|
|
296
|
+
finaleSequence: state.finaleSequence?.map((grid) => grid.map((column) => [...column])),
|
|
297
|
+
finaleSequenceRows: state.finaleSequenceRows?.map((grid) => grid.map((row) => [...row])),
|
|
298
|
+
highlightWin: state.highlightWin,
|
|
299
|
+
callback: state.callback
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
//#endregion
|
|
304
|
+
//#region src/core/spinQueue.ts
|
|
305
|
+
var SpinQueueController = class {
|
|
306
|
+
queue;
|
|
307
|
+
constructor(initialQueue) {
|
|
308
|
+
this.queue = (initialQueue ?? []).map((entry) => cloneSpinState(entry));
|
|
309
|
+
}
|
|
310
|
+
hasPending() {
|
|
311
|
+
return this.queue.length > 0;
|
|
312
|
+
}
|
|
313
|
+
consume() {
|
|
314
|
+
if (this.queue.length === 0) return null;
|
|
315
|
+
return this.queue.shift() ?? null;
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
//#endregion
|
|
320
|
+
//#region src/core/state.ts
|
|
321
|
+
function createRuntimeState() {
|
|
322
|
+
return {
|
|
323
|
+
isSpinning: false,
|
|
324
|
+
hasStartedFirstSpin: false,
|
|
325
|
+
queueFinished: false,
|
|
326
|
+
shouldHighlightCurrentSpin: false,
|
|
327
|
+
activeSpinState: null,
|
|
328
|
+
phase: "idle",
|
|
329
|
+
winFlashStartedAt: 0,
|
|
330
|
+
outroStartedAt: 0,
|
|
331
|
+
idleStartedAt: 0,
|
|
332
|
+
preSpinStartedAt: 0,
|
|
333
|
+
winEffectsEnvelope: 1
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
function beginSpin(state, params) {
|
|
337
|
+
state.hasStartedFirstSpin = true;
|
|
338
|
+
state.isSpinning = true;
|
|
339
|
+
state.phase = "outro";
|
|
340
|
+
state.outroStartedAt = params.startedAt;
|
|
341
|
+
state.activeSpinState = params.activeSpinState;
|
|
342
|
+
state.shouldHighlightCurrentSpin = params.shouldHighlightCurrentSpin;
|
|
343
|
+
}
|
|
344
|
+
function finishSpin(state, hasPendingInQueue, now) {
|
|
345
|
+
state.phase = "idle";
|
|
346
|
+
state.idleStartedAt = now;
|
|
347
|
+
state.isSpinning = false;
|
|
348
|
+
state.shouldHighlightCurrentSpin = false;
|
|
349
|
+
state.queueFinished = !hasPendingInQueue;
|
|
350
|
+
state.activeSpinState = null;
|
|
351
|
+
}
|
|
352
|
+
function startWinFlash(state, startedAt) {
|
|
353
|
+
state.winFlashStartedAt = startedAt;
|
|
354
|
+
state.phase = "winFlash";
|
|
355
|
+
}
|
|
356
|
+
function destroyState(state) {
|
|
357
|
+
state.isSpinning = false;
|
|
358
|
+
state.queueFinished = true;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
//#endregion
|
|
362
|
+
//#region src/render/webglRenderer.ts
|
|
363
|
+
const VERTEX_SHADER_SOURCE = `
|
|
364
|
+
attribute vec2 a_pos;
|
|
365
|
+
uniform vec2 u_resolution;
|
|
366
|
+
uniform mediump vec4 u_destRect;
|
|
367
|
+
varying vec2 v_uv;
|
|
368
|
+
|
|
369
|
+
void main() {
|
|
370
|
+
vec2 local = a_pos;
|
|
371
|
+
vec2 pixel = vec2(
|
|
372
|
+
u_destRect.x + local.x * u_destRect.z,
|
|
373
|
+
u_destRect.y + local.y * u_destRect.w
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
vec2 zeroToOne = pixel / u_resolution;
|
|
377
|
+
vec2 clip = vec2(
|
|
378
|
+
zeroToOne.x * 2.0 - 1.0,
|
|
379
|
+
1.0 - zeroToOne.y * 2.0
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
gl_Position = vec4(clip, 0.0, 1.0);
|
|
383
|
+
v_uv = local;
|
|
384
|
+
}
|
|
385
|
+
`;
|
|
386
|
+
const FRAGMENT_SHADER_SOURCE = `
|
|
387
|
+
precision mediump float;
|
|
388
|
+
|
|
389
|
+
uniform sampler2D u_texture;
|
|
390
|
+
uniform mediump vec4 u_destRect;
|
|
391
|
+
uniform vec4 u_srcRect;
|
|
392
|
+
uniform vec4 u_color;
|
|
393
|
+
uniform float u_useTexture;
|
|
394
|
+
uniform float u_shapeMode;
|
|
395
|
+
uniform float u_time;
|
|
396
|
+
uniform float u_borderPx;
|
|
397
|
+
uniform float u_borderInsetPx;
|
|
398
|
+
uniform float u_cornerRadiusPx;
|
|
399
|
+
uniform float u_noiseAmp;
|
|
400
|
+
uniform float u_pulseStrength;
|
|
401
|
+
varying vec2 v_uv;
|
|
402
|
+
|
|
403
|
+
float hash1(float n) {
|
|
404
|
+
return fract(sin(n) * 43758.5453);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
void main() {
|
|
408
|
+
if (u_shapeMode > 1.5) {
|
|
409
|
+
vec2 sizePx = max(vec2(1.0), u_destRect.zw);
|
|
410
|
+
vec2 px = v_uv * sizePx;
|
|
411
|
+
float cornerRadius = clamp(u_cornerRadiusPx, 0.0, max(0.0, min(sizePx.x, sizePx.y) * 0.5 - 0.01));
|
|
412
|
+
|
|
413
|
+
vec2 halfSize = sizePx * 0.5;
|
|
414
|
+
vec2 p = px - halfSize;
|
|
415
|
+
vec2 q = abs(p) - (halfSize - vec2(cornerRadius));
|
|
416
|
+
float outside = length(max(q, vec2(0.0)));
|
|
417
|
+
float inside = min(max(q.x, q.y), 0.0);
|
|
418
|
+
float d = -(outside + inside - cornerRadius);
|
|
419
|
+
|
|
420
|
+
float left = px.x;
|
|
421
|
+
float right = sizePx.x - px.x;
|
|
422
|
+
float top = px.y;
|
|
423
|
+
float bottom = sizePx.y - px.y;
|
|
424
|
+
|
|
425
|
+
float side = 0.0;
|
|
426
|
+
float s = 0.0;
|
|
427
|
+
float perimeter = max(1.0, (sizePx.x + sizePx.y) * 2.0);
|
|
428
|
+
if (top <= left && top <= right && top <= bottom) {
|
|
429
|
+
side = 0.0;
|
|
430
|
+
s = px.x;
|
|
431
|
+
} else if (right <= left && right <= top && right <= bottom) {
|
|
432
|
+
side = 1.0;
|
|
433
|
+
s = sizePx.x + px.y;
|
|
434
|
+
} else if (bottom <= left && bottom <= right && bottom <= top) {
|
|
435
|
+
side = 2.0;
|
|
436
|
+
s = sizePx.x + sizePx.y + (sizePx.x - px.x);
|
|
437
|
+
} else {
|
|
438
|
+
side = 3.0;
|
|
439
|
+
s = sizePx.x + sizePx.y + sizePx.x + (sizePx.y - px.y);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
float sideSeed = hash1(side * 17.31 + sizePx.x * 0.013 + sizePx.y * 0.007);
|
|
443
|
+
float phase = s * (0.12 + sideSeed * 0.06) + u_time * (6.0 + sideSeed * 4.0);
|
|
444
|
+
float n1 = sin(phase);
|
|
445
|
+
float n2 = sin(phase * 1.87 + 1.6 + sideSeed * 5.1);
|
|
446
|
+
float n3 = sin(phase * 2.53 + 0.73);
|
|
447
|
+
float waviness = (n1 * 0.58 + n2 * 0.3 + n3 * 0.12) * u_noiseAmp;
|
|
448
|
+
|
|
449
|
+
float borderCenter = max(0.5, u_borderInsetPx + waviness);
|
|
450
|
+
float borderDist = d - borderCenter;
|
|
451
|
+
float core = exp(-pow(borderDist / max(1.0, u_borderPx * 0.72), 2.0));
|
|
452
|
+
float glow = exp(-pow(borderDist / max(1.0, u_borderPx * 2.1), 2.0)) * 0.55;
|
|
453
|
+
|
|
454
|
+
float pulseA = fract(u_time * 0.11 + 0.07);
|
|
455
|
+
float pulseB = fract(u_time * 0.11 + 0.41);
|
|
456
|
+
float pulseC = fract(u_time * 0.11 + 0.78);
|
|
457
|
+
float posA = pulseA * perimeter;
|
|
458
|
+
float posB = pulseB * perimeter;
|
|
459
|
+
float posC = pulseC * perimeter;
|
|
460
|
+
float dsA = min(abs(s - posA), perimeter - abs(s - posA));
|
|
461
|
+
float dsB = min(abs(s - posB), perimeter - abs(s - posB));
|
|
462
|
+
float dsC = min(abs(s - posC), perimeter - abs(s - posC));
|
|
463
|
+
float pulseBand = exp(-pow(dsA / 18.0, 2.0)) + exp(-pow(dsB / 22.0, 2.0)) + exp(-pow(dsC / 16.0, 2.0));
|
|
464
|
+
pulseBand *= exp(-pow(borderDist / max(1.0, u_borderPx * 1.25), 2.0));
|
|
465
|
+
float outerFade = smoothstep(0.0, max(0.75, u_borderInsetPx * 0.95), d);
|
|
466
|
+
|
|
467
|
+
float flicker = 0.84 + 0.16 * sin(u_time * 21.0 + s * 0.2 + side * 3.1);
|
|
468
|
+
float alpha = (core + glow + pulseBand * u_pulseStrength) * u_color.a * flicker * outerFade;
|
|
469
|
+
if (alpha <= 0.003) {
|
|
470
|
+
discard;
|
|
471
|
+
}
|
|
472
|
+
gl_FragColor = vec4(u_color.rgb * (0.9 + pulseBand * 0.24), alpha);
|
|
473
|
+
} else if (u_shapeMode > 0.5) {
|
|
474
|
+
vec2 centered = v_uv - vec2(0.5, 0.5);
|
|
475
|
+
float dist = length(centered) * 2.0;
|
|
476
|
+
if (dist > 1.0) {
|
|
477
|
+
discard;
|
|
478
|
+
}
|
|
479
|
+
float feather = smoothstep(1.0, 0.72, dist);
|
|
480
|
+
gl_FragColor = vec4(u_color.rgb, u_color.a * feather);
|
|
481
|
+
} else if (u_useTexture > 0.5) {
|
|
482
|
+
vec2 uv = vec2(
|
|
483
|
+
mix(u_srcRect.x, u_srcRect.z, v_uv.x),
|
|
484
|
+
mix(u_srcRect.y, u_srcRect.w, v_uv.y)
|
|
485
|
+
);
|
|
486
|
+
vec4 tex = texture2D(u_texture, uv);
|
|
487
|
+
gl_FragColor = tex * u_color;
|
|
488
|
+
} else {
|
|
489
|
+
gl_FragColor = u_color;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
`;
|
|
493
|
+
var WebGLRenderer = class {
|
|
494
|
+
canvas;
|
|
495
|
+
spriteImage;
|
|
496
|
+
spriteElementsCount;
|
|
497
|
+
gl;
|
|
498
|
+
program;
|
|
499
|
+
uniforms;
|
|
500
|
+
quadBuffer;
|
|
501
|
+
texture;
|
|
502
|
+
viewportW = 1;
|
|
503
|
+
viewportH = 1;
|
|
504
|
+
spriteWidth;
|
|
505
|
+
spriteHeight;
|
|
506
|
+
spriteSegmentHeight;
|
|
507
|
+
constructor(params) {
|
|
508
|
+
this.canvas = params.canvas;
|
|
509
|
+
this.spriteImage = params.spriteImage;
|
|
510
|
+
this.spriteElementsCount = Math.max(1, params.spriteElementsCount);
|
|
511
|
+
this.spriteWidth = this.spriteImage.width;
|
|
512
|
+
this.spriteHeight = this.spriteImage.height;
|
|
513
|
+
this.spriteSegmentHeight = this.spriteHeight / this.spriteElementsCount;
|
|
514
|
+
const gl = this.canvas.getContext("webgl2", {
|
|
515
|
+
alpha: true,
|
|
516
|
+
antialias: false
|
|
517
|
+
}) ?? this.canvas.getContext("webgl", {
|
|
518
|
+
alpha: true,
|
|
519
|
+
antialias: false
|
|
520
|
+
});
|
|
521
|
+
if (!gl) throw new Error("WebGL context is not available");
|
|
522
|
+
this.gl = gl;
|
|
523
|
+
const vertexShader = this.createShader(this.gl.VERTEX_SHADER, VERTEX_SHADER_SOURCE);
|
|
524
|
+
const fragmentShader = this.createShader(this.gl.FRAGMENT_SHADER, FRAGMENT_SHADER_SOURCE);
|
|
525
|
+
this.program = this.createProgram(vertexShader, fragmentShader);
|
|
526
|
+
this.gl.deleteShader(vertexShader);
|
|
527
|
+
this.gl.deleteShader(fragmentShader);
|
|
528
|
+
const quadBuffer = this.gl.createBuffer();
|
|
529
|
+
if (!quadBuffer) throw new Error("Failed to create WebGL quad buffer");
|
|
530
|
+
this.quadBuffer = quadBuffer;
|
|
531
|
+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.quadBuffer);
|
|
532
|
+
this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array([
|
|
533
|
+
0,
|
|
534
|
+
0,
|
|
535
|
+
1,
|
|
536
|
+
0,
|
|
537
|
+
0,
|
|
538
|
+
1,
|
|
539
|
+
0,
|
|
540
|
+
1,
|
|
541
|
+
1,
|
|
542
|
+
0,
|
|
543
|
+
1,
|
|
544
|
+
1
|
|
545
|
+
]), this.gl.STATIC_DRAW);
|
|
546
|
+
const texture = this.gl.createTexture();
|
|
547
|
+
if (!texture) throw new Error("Failed to create WebGL texture");
|
|
548
|
+
this.texture = texture;
|
|
549
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);
|
|
550
|
+
this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL, 0);
|
|
551
|
+
this.gl.pixelStorei(this.gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 1);
|
|
552
|
+
this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, this.spriteImage);
|
|
553
|
+
this.gl.pixelStorei(this.gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 0);
|
|
554
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR);
|
|
555
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);
|
|
556
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
|
|
557
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
|
|
558
|
+
this.gl.useProgram(this.program);
|
|
559
|
+
const posLocation = this.gl.getAttribLocation(this.program, "a_pos");
|
|
560
|
+
this.gl.enableVertexAttribArray(posLocation);
|
|
561
|
+
this.gl.vertexAttribPointer(posLocation, 2, this.gl.FLOAT, false, 8, 0);
|
|
562
|
+
this.uniforms = {
|
|
563
|
+
resolution: this.gl.getUniformLocation(this.program, "u_resolution"),
|
|
564
|
+
destRect: this.gl.getUniformLocation(this.program, "u_destRect"),
|
|
565
|
+
srcRect: this.gl.getUniformLocation(this.program, "u_srcRect"),
|
|
566
|
+
color: this.gl.getUniformLocation(this.program, "u_color"),
|
|
567
|
+
useTexture: this.gl.getUniformLocation(this.program, "u_useTexture"),
|
|
568
|
+
shapeMode: this.gl.getUniformLocation(this.program, "u_shapeMode"),
|
|
569
|
+
texture: this.gl.getUniformLocation(this.program, "u_texture"),
|
|
570
|
+
time: this.gl.getUniformLocation(this.program, "u_time"),
|
|
571
|
+
borderPx: this.gl.getUniformLocation(this.program, "u_borderPx"),
|
|
572
|
+
borderInsetPx: this.gl.getUniformLocation(this.program, "u_borderInsetPx"),
|
|
573
|
+
cornerRadiusPx: this.gl.getUniformLocation(this.program, "u_cornerRadiusPx"),
|
|
574
|
+
noiseAmp: this.gl.getUniformLocation(this.program, "u_noiseAmp"),
|
|
575
|
+
pulseStrength: this.gl.getUniformLocation(this.program, "u_pulseStrength")
|
|
576
|
+
};
|
|
577
|
+
this.gl.uniform1i(this.uniforms.texture, 0);
|
|
578
|
+
this.gl.uniform1f(this.uniforms.time, 0);
|
|
579
|
+
this.gl.uniform1f(this.uniforms.borderPx, 1);
|
|
580
|
+
this.gl.uniform1f(this.uniforms.borderInsetPx, 0);
|
|
581
|
+
this.gl.uniform1f(this.uniforms.cornerRadiusPx, 0);
|
|
582
|
+
this.gl.uniform1f(this.uniforms.noiseAmp, 0);
|
|
583
|
+
this.gl.uniform1f(this.uniforms.pulseStrength, 0);
|
|
584
|
+
this.gl.clearColor(0, 0, 0, 0);
|
|
585
|
+
this.gl.enable(this.gl.BLEND);
|
|
586
|
+
this.gl.blendFunc(this.gl.ONE, this.gl.ONE_MINUS_SRC_ALPHA);
|
|
587
|
+
}
|
|
588
|
+
resize(width, height) {
|
|
589
|
+
this.viewportW = Math.max(1, Math.floor(width));
|
|
590
|
+
this.viewportH = Math.max(1, Math.floor(height));
|
|
591
|
+
this.gl.viewport(0, 0, this.viewportW, this.viewportH);
|
|
592
|
+
}
|
|
593
|
+
beginFrame() {
|
|
594
|
+
this.gl.useProgram(this.program);
|
|
595
|
+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.quadBuffer);
|
|
596
|
+
this.gl.activeTexture(this.gl.TEXTURE0);
|
|
597
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);
|
|
598
|
+
this.gl.uniform2f(this.uniforms.resolution, this.viewportW, this.viewportH);
|
|
599
|
+
this.gl.uniform1f(this.uniforms.shapeMode, 0);
|
|
600
|
+
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
|
|
601
|
+
}
|
|
602
|
+
drawSprite(symbolId, x, y, width, height, alpha = 1) {
|
|
603
|
+
const srcTop = normalizeSegment(symbolId, this.spriteElementsCount) * this.spriteSegmentHeight;
|
|
604
|
+
const srcBottom = srcTop + this.spriteSegmentHeight;
|
|
605
|
+
const inset = .5;
|
|
606
|
+
const u0 = inset / this.spriteWidth;
|
|
607
|
+
const u1 = 1 - inset / this.spriteWidth;
|
|
608
|
+
const v0 = 1 - (srcBottom - inset) / this.spriteHeight;
|
|
609
|
+
const v1 = 1 - (srcTop + inset) / this.spriteHeight;
|
|
610
|
+
this.gl.uniform4f(this.uniforms.destRect, x, y, width, height);
|
|
611
|
+
this.gl.uniform4f(this.uniforms.srcRect, u0, v0, u1, v1);
|
|
612
|
+
this.gl.uniform4f(this.uniforms.color, 1, 1, 1, alpha);
|
|
613
|
+
this.gl.uniform1f(this.uniforms.shapeMode, 0);
|
|
614
|
+
this.gl.uniform1f(this.uniforms.useTexture, 1);
|
|
615
|
+
this.gl.drawArrays(this.gl.TRIANGLES, 0, 6);
|
|
616
|
+
}
|
|
617
|
+
drawSolidRect(x, y, width, height, rgba) {
|
|
618
|
+
this.gl.uniform4f(this.uniforms.destRect, x, y, width, height);
|
|
619
|
+
this.gl.uniform4f(this.uniforms.srcRect, 0, 0, 1, 1);
|
|
620
|
+
this.gl.uniform4f(this.uniforms.color, rgba[0], rgba[1], rgba[2], rgba[3]);
|
|
621
|
+
this.gl.uniform1f(this.uniforms.shapeMode, 0);
|
|
622
|
+
this.gl.uniform1f(this.uniforms.useTexture, 0);
|
|
623
|
+
this.gl.drawArrays(this.gl.TRIANGLES, 0, 6);
|
|
624
|
+
}
|
|
625
|
+
drawSoftCircle(centerX, centerY, radius, rgba) {
|
|
626
|
+
const diameter = radius * 2;
|
|
627
|
+
this.gl.uniform4f(this.uniforms.destRect, centerX - radius, centerY - radius, diameter, diameter);
|
|
628
|
+
this.gl.uniform4f(this.uniforms.srcRect, 0, 0, 1, 1);
|
|
629
|
+
this.gl.uniform4f(this.uniforms.color, rgba[0], rgba[1], rgba[2], rgba[3]);
|
|
630
|
+
this.gl.uniform1f(this.uniforms.useTexture, 0);
|
|
631
|
+
this.gl.uniform1f(this.uniforms.shapeMode, 1);
|
|
632
|
+
this.gl.drawArrays(this.gl.TRIANGLES, 0, 6);
|
|
633
|
+
this.gl.uniform1f(this.uniforms.shapeMode, 0);
|
|
634
|
+
}
|
|
635
|
+
drawElectricBorder(params) {
|
|
636
|
+
this.gl.uniform4f(this.uniforms.destRect, params.x, params.y, params.width, params.height);
|
|
637
|
+
this.gl.uniform4f(this.uniforms.srcRect, 0, 0, 1, 1);
|
|
638
|
+
this.gl.uniform4f(this.uniforms.color, params.rgba[0], params.rgba[1], params.rgba[2], params.rgba[3]);
|
|
639
|
+
this.gl.uniform1f(this.uniforms.useTexture, 0);
|
|
640
|
+
this.gl.uniform1f(this.uniforms.shapeMode, 2);
|
|
641
|
+
this.gl.uniform1f(this.uniforms.time, params.timeMs * .001);
|
|
642
|
+
this.gl.uniform1f(this.uniforms.borderPx, Math.max(.5, params.borderThicknessPx));
|
|
643
|
+
this.gl.uniform1f(this.uniforms.borderInsetPx, Math.max(0, params.borderInsetPx));
|
|
644
|
+
this.gl.uniform1f(this.uniforms.cornerRadiusPx, Math.max(0, params.cornerRadiusPx));
|
|
645
|
+
this.gl.uniform1f(this.uniforms.noiseAmp, Math.max(0, params.noiseAmplitudePx));
|
|
646
|
+
this.gl.uniform1f(this.uniforms.pulseStrength, Math.max(0, params.pulseStrength));
|
|
647
|
+
this.gl.drawArrays(this.gl.TRIANGLES, 0, 6);
|
|
648
|
+
this.gl.uniform1f(this.uniforms.shapeMode, 0);
|
|
649
|
+
}
|
|
650
|
+
beginAdditiveBlend() {
|
|
651
|
+
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE);
|
|
652
|
+
}
|
|
653
|
+
endAdditiveBlend() {
|
|
654
|
+
this.gl.blendFunc(this.gl.ONE, this.gl.ONE_MINUS_SRC_ALPHA);
|
|
655
|
+
}
|
|
656
|
+
dispose() {
|
|
657
|
+
this.gl.deleteTexture(this.texture);
|
|
658
|
+
this.gl.deleteBuffer(this.quadBuffer);
|
|
659
|
+
this.gl.deleteProgram(this.program);
|
|
660
|
+
}
|
|
661
|
+
createShader(type, source) {
|
|
662
|
+
const shader = this.gl.createShader(type);
|
|
663
|
+
if (!shader) throw new Error("Failed to create WebGL shader");
|
|
664
|
+
this.gl.shaderSource(shader, source);
|
|
665
|
+
this.gl.compileShader(shader);
|
|
666
|
+
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
|
|
667
|
+
const message = this.gl.getShaderInfoLog(shader) ?? "unknown error";
|
|
668
|
+
this.gl.deleteShader(shader);
|
|
669
|
+
throw new Error(`WebGL shader compile failed: ${message}`);
|
|
670
|
+
}
|
|
671
|
+
return shader;
|
|
672
|
+
}
|
|
673
|
+
createProgram(vertexShader, fragmentShader) {
|
|
674
|
+
const program = this.gl.createProgram();
|
|
675
|
+
if (!program) throw new Error("Failed to create WebGL program");
|
|
676
|
+
this.gl.attachShader(program, vertexShader);
|
|
677
|
+
this.gl.attachShader(program, fragmentShader);
|
|
678
|
+
this.gl.linkProgram(program);
|
|
679
|
+
if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
|
|
680
|
+
const message = this.gl.getProgramInfoLog(program) ?? "unknown error";
|
|
681
|
+
this.gl.deleteProgram(program);
|
|
682
|
+
throw new Error(`WebGL program link failed: ${message}`);
|
|
683
|
+
}
|
|
684
|
+
return program;
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
//#endregion
|
|
689
|
+
//#region src/CascadingReel.ts
|
|
690
|
+
var CascadingReel = class CascadingReel {
|
|
691
|
+
static RAINBOW_HUE_BUCKETS = 24;
|
|
692
|
+
static PARTICLE_GLOBAL_ALPHA = .9;
|
|
693
|
+
static PARTICLE_MAX_DISTANCE = .72;
|
|
694
|
+
static PARTICLE_BASE_RADIUS = .028;
|
|
695
|
+
static DEFAULT_SYMBOL_SCALE = .9;
|
|
696
|
+
canvas;
|
|
697
|
+
container;
|
|
698
|
+
button;
|
|
699
|
+
spinQueueController;
|
|
700
|
+
spriteSource;
|
|
701
|
+
spriteCrossOrigin;
|
|
702
|
+
spriteElementsCount;
|
|
703
|
+
highlightInitialWinningCells;
|
|
704
|
+
particleColorRgb;
|
|
705
|
+
particleColorMode;
|
|
706
|
+
symbolScale;
|
|
707
|
+
motionProfile;
|
|
708
|
+
isCoarsePointerDevice;
|
|
709
|
+
spriteImage = null;
|
|
710
|
+
spriteLoadError = null;
|
|
711
|
+
webglRenderer = null;
|
|
712
|
+
rafLoop = new RafLoop();
|
|
713
|
+
runtime = createRuntimeState();
|
|
714
|
+
width = 0;
|
|
715
|
+
height = 0;
|
|
716
|
+
cellW = 0;
|
|
717
|
+
cellH = 0;
|
|
718
|
+
boardX = 0;
|
|
719
|
+
boardY = 0;
|
|
720
|
+
scriptedCascadeQueue = [];
|
|
721
|
+
scriptedOutgoingGrid = null;
|
|
722
|
+
scriptedPendingGrid = null;
|
|
723
|
+
scriptedOutroStartedAt = 0;
|
|
724
|
+
scriptedOutroElapsedMs = 0;
|
|
725
|
+
outroMotionPlan = null;
|
|
726
|
+
scriptedOutgoingOffsets = createZeroOffsets();
|
|
727
|
+
scriptedOutgoingOffsetsPrev = createZeroOffsets();
|
|
728
|
+
scriptedIncomingOffsets = createZeroOffsets();
|
|
729
|
+
scriptedIncomingOffsetsPrev = createZeroOffsets();
|
|
730
|
+
scriptedIncomingAlpha = createZeroOffsets();
|
|
731
|
+
scriptedIncomingAlphaPrev = createZeroOffsets();
|
|
732
|
+
scriptedIncomingVisibility = CascadingReel.createVisibilityGrid("hidden");
|
|
733
|
+
scriptedIncomingVisibilityPrev = CascadingReel.createVisibilityGrid("hidden");
|
|
734
|
+
winningCells = [];
|
|
735
|
+
winningCellKeys = /* @__PURE__ */ new Set();
|
|
736
|
+
grid;
|
|
737
|
+
particlesPerCell = 34;
|
|
738
|
+
lastRafTime = 0;
|
|
739
|
+
initialHighlightRequestedAt = 0;
|
|
740
|
+
simulationLastNow = 0;
|
|
741
|
+
simulationAccumulatorMs = 0;
|
|
742
|
+
outroInterpolationAlpha = 1;
|
|
743
|
+
isOutroPipelineWarmedUp = false;
|
|
744
|
+
isGpuPipelineWarmedUp = false;
|
|
745
|
+
perfWindowStartedAt = 0;
|
|
746
|
+
perfFrameCount = 0;
|
|
747
|
+
perfOver20MsCount = 0;
|
|
748
|
+
perfDtSamples = [];
|
|
749
|
+
mobileDprCap = 2;
|
|
750
|
+
mobilePerfGoodWindows = 0;
|
|
751
|
+
mobilePerfBadWindows = 0;
|
|
752
|
+
static PERF_WINDOW_MS = 1500;
|
|
753
|
+
static PERF_BAD_P95_DT_MS = 19;
|
|
754
|
+
static PERF_GOOD_P95_DT_MS = 17.5;
|
|
755
|
+
static PERF_BAD_SLOW_RATIO = .03;
|
|
756
|
+
static MOBILE_BAD_WINDOWS_TO_DECREASE_DPR = 2;
|
|
757
|
+
static MOBILE_GOOD_WINDOWS_TO_INCREASE_DPR = 4;
|
|
758
|
+
static MOBILE_DPR_CAP_MIN = 1.25;
|
|
759
|
+
static MOBILE_DPR_CAP_MAX = 1.5;
|
|
760
|
+
static MOBILE_DPR_STEP_DOWN = .1;
|
|
761
|
+
static MOBILE_DPR_STEP_UP = .05;
|
|
762
|
+
static DPR_QUANT_STEP = .25;
|
|
763
|
+
static MAX_CANVAS_AREA_PX_COARSE = 900 * 900;
|
|
764
|
+
static MAX_CANVAS_AREA_PX_FINE = 1400 * 1400;
|
|
765
|
+
static PRE_SPIN_MS = 150;
|
|
766
|
+
static WIN_EFFECTS_ENVELOPE_TAU_MS = 120;
|
|
767
|
+
static MAX_FRAME_DELTA_MS = 100;
|
|
768
|
+
static WIN_BORDER_ALPHA = .72;
|
|
769
|
+
static WIN_BORDER_INSET_RATIO = .08;
|
|
770
|
+
static particleSeedsCache = /* @__PURE__ */ new Map();
|
|
771
|
+
constructor(config) {
|
|
772
|
+
this.canvas = config.canvas;
|
|
773
|
+
this.container = config.container;
|
|
774
|
+
this.button = config.button;
|
|
775
|
+
this.spriteSource = config.sprite;
|
|
776
|
+
this.spriteCrossOrigin = config.spriteCrossOrigin ?? "anonymous";
|
|
777
|
+
this.spriteElementsCount = Math.max(1, config.spriteElementsCount ?? 6);
|
|
778
|
+
this.highlightInitialWinningCells = config.highlightInitialWinningCells !== false;
|
|
779
|
+
this.spinQueueController = new SpinQueueController(config.queuedSpinStates);
|
|
780
|
+
const particleColor = normalizeParticleColor(config.particleColor);
|
|
781
|
+
this.particleColorRgb = particleColor.rgb;
|
|
782
|
+
this.particleColorMode = particleColor.mode;
|
|
783
|
+
this.symbolScale = normalizeSymbolScale(config.symbolScale, CascadingReel.DEFAULT_SYMBOL_SCALE);
|
|
784
|
+
this.motionProfile = DEFAULT_MOTION_PROFILE;
|
|
785
|
+
this.isCoarsePointerDevice = CascadingReel.detectCoarsePointerDevice();
|
|
786
|
+
this.mobileDprCap = this.isCoarsePointerDevice ? CascadingReel.MOBILE_DPR_CAP_MAX : 2;
|
|
787
|
+
this.grid = config.initialSegments ? normalizeInitialSegments(config.initialSegments, this.spriteElementsCount) : createRandomGrid(this.spriteElementsCount);
|
|
788
|
+
}
|
|
789
|
+
async init() {
|
|
790
|
+
this.bindEvents();
|
|
791
|
+
this.resize();
|
|
792
|
+
await this.loadSpriteIfProvided();
|
|
793
|
+
if (!this.spriteImage) {
|
|
794
|
+
const reason = this.spriteLoadError ? ` (${this.spriteLoadError})` : "";
|
|
795
|
+
throw new Error(`sprite is required for WebGL renderer${reason}`);
|
|
796
|
+
}
|
|
797
|
+
this.webglRenderer = new WebGLRenderer({
|
|
798
|
+
canvas: this.canvas,
|
|
799
|
+
spriteImage: this.spriteImage,
|
|
800
|
+
spriteElementsCount: this.spriteElementsCount
|
|
801
|
+
});
|
|
802
|
+
this.webglRenderer.resize(this.width, this.height);
|
|
803
|
+
this.warmUpOutroPipeline();
|
|
804
|
+
this.warmUpGpuPipeline();
|
|
805
|
+
this.applyInitialHighlightIfNeeded();
|
|
806
|
+
requestAnimationFrame((warmNow) => {
|
|
807
|
+
this.render(warmNow);
|
|
808
|
+
this.startLoop();
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
destroy() {
|
|
812
|
+
this.unbindEvents();
|
|
813
|
+
this.rafLoop.stop();
|
|
814
|
+
this.simulationLastNow = 0;
|
|
815
|
+
this.simulationAccumulatorMs = 0;
|
|
816
|
+
destroyState(this.runtime);
|
|
817
|
+
this.webglRenderer?.dispose();
|
|
818
|
+
this.webglRenderer = null;
|
|
819
|
+
this.clearWinningCells();
|
|
820
|
+
}
|
|
821
|
+
spin() {
|
|
822
|
+
this.dismissHighlightIfActive();
|
|
823
|
+
if (this.runtime.isSpinning) return;
|
|
824
|
+
if (this.runtime.queueFinished) return;
|
|
825
|
+
if (!this.spinQueueController.hasPending()) {
|
|
826
|
+
this.runtime.queueFinished = true;
|
|
827
|
+
if (this.button) this.button.disabled = true;
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
const activeSpinState = this.spinQueueController.consume();
|
|
831
|
+
const shouldHighlightCurrentSpin = activeSpinState?.highlightWin === true;
|
|
832
|
+
this.runtime.isSpinning = true;
|
|
833
|
+
this.runtime.phase = "preSpin";
|
|
834
|
+
this.runtime.preSpinStartedAt = performance.now();
|
|
835
|
+
this.runtime.activeSpinState = activeSpinState;
|
|
836
|
+
this.runtime.shouldHighlightCurrentSpin = shouldHighlightCurrentSpin;
|
|
837
|
+
this.runtime.hasStartedFirstSpin = true;
|
|
838
|
+
this.simulationLastNow = 0;
|
|
839
|
+
this.simulationAccumulatorMs = 0;
|
|
840
|
+
if (this.button) this.button.disabled = true;
|
|
841
|
+
this.startLoop();
|
|
842
|
+
}
|
|
843
|
+
bindEvents() {
|
|
844
|
+
window.addEventListener("resize", this.resize);
|
|
845
|
+
document.addEventListener("visibilitychange", this.onVisibilityChange);
|
|
846
|
+
this.button?.addEventListener("click", this.onSpinClick);
|
|
847
|
+
}
|
|
848
|
+
unbindEvents() {
|
|
849
|
+
window.removeEventListener("resize", this.resize);
|
|
850
|
+
document.removeEventListener("visibilitychange", this.onVisibilityChange);
|
|
851
|
+
this.button?.removeEventListener("click", this.onSpinClick);
|
|
852
|
+
}
|
|
853
|
+
onVisibilityChange = () => {
|
|
854
|
+
this.lastRafTime = 0;
|
|
855
|
+
this.simulationLastNow = 0;
|
|
856
|
+
this.simulationAccumulatorMs = 0;
|
|
857
|
+
this.resetPerfWindow();
|
|
858
|
+
};
|
|
859
|
+
onSpinClick = () => {
|
|
860
|
+
if (this.runtime.isSpinning && this.runtime.phase !== "winFlash") return;
|
|
861
|
+
this.spin();
|
|
862
|
+
};
|
|
863
|
+
getNextGrid(stopGrid) {
|
|
864
|
+
if (!stopGrid) return createRandomGrid(this.spriteElementsCount);
|
|
865
|
+
return normalizeStopGrid(stopGrid, this.spriteElementsCount);
|
|
866
|
+
}
|
|
867
|
+
update(now) {
|
|
868
|
+
if (!this.runtime.isSpinning) return;
|
|
869
|
+
if (this.runtime.phase === "preSpin") {
|
|
870
|
+
if (now - this.runtime.preSpinStartedAt < CascadingReel.PRE_SPIN_MS) return;
|
|
871
|
+
beginSpin(this.runtime, {
|
|
872
|
+
activeSpinState: this.runtime.activeSpinState,
|
|
873
|
+
shouldHighlightCurrentSpin: this.runtime.shouldHighlightCurrentSpin,
|
|
874
|
+
startedAt: now
|
|
875
|
+
});
|
|
876
|
+
this.runtime.preSpinStartedAt = 0;
|
|
877
|
+
const scriptedSource = this.runtime.activeSpinState?.finaleSequenceRows ? this.runtime.activeSpinState.finaleSequenceRows.map((rows) => rowsToStopGrid(rows)) : this.runtime.activeSpinState?.finaleSequence ?? [];
|
|
878
|
+
this.scriptedCascadeQueue = scriptedSource.map((grid) => grid.map((column) => [...column]));
|
|
879
|
+
this.clearWinningCells();
|
|
880
|
+
const stopGridSource = this.runtime.activeSpinState?.stopRows ? rowsToStopGrid(this.runtime.activeSpinState.stopRows) : this.runtime.activeSpinState?.stopGrid;
|
|
881
|
+
const nextGrid = this.getNextGrid(stopGridSource);
|
|
882
|
+
this.startOutroTransition(nextGrid, now);
|
|
883
|
+
}
|
|
884
|
+
if (this.runtime.phase === "outro") return;
|
|
885
|
+
}
|
|
886
|
+
stepScriptedOutro(stepMs) {
|
|
887
|
+
if (!this.scriptedOutgoingGrid || !this.scriptedPendingGrid) {
|
|
888
|
+
this.finishSpinWithUi();
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
if (!this.outroMotionPlan) this.outroMotionPlan = this.createOutroMotionPlan();
|
|
892
|
+
this.scriptedOutroElapsedMs += stepMs;
|
|
893
|
+
const { allOutgoingDone, allIncomingDone } = updateOutroOffsets({
|
|
894
|
+
elapsedMs: this.scriptedOutroElapsedMs,
|
|
895
|
+
scriptedOutgoingOffsets: this.scriptedOutgoingOffsets,
|
|
896
|
+
scriptedIncomingOffsets: this.scriptedIncomingOffsets,
|
|
897
|
+
scriptedIncomingAlpha: this.scriptedIncomingAlpha,
|
|
898
|
+
scriptedIncomingVisibility: this.scriptedIncomingVisibility,
|
|
899
|
+
motionPlan: this.outroMotionPlan
|
|
900
|
+
});
|
|
901
|
+
if (!allOutgoingDone || !allIncomingDone) return;
|
|
902
|
+
this.grid = this.scriptedPendingGrid;
|
|
903
|
+
this.scriptedOutgoingGrid = null;
|
|
904
|
+
this.scriptedPendingGrid = null;
|
|
905
|
+
this.outroMotionPlan = null;
|
|
906
|
+
this.clearWinningCells();
|
|
907
|
+
this.resetOutroBuffers(1, "active");
|
|
908
|
+
if (this.tryStartScriptedCascade(this.scriptedOutroStartedAt + this.scriptedOutroElapsedMs)) return;
|
|
909
|
+
if (!this.runtime.shouldHighlightCurrentSpin) {
|
|
910
|
+
this.finishSpinWithUi();
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
this.setWinningCells(findMostFrequentCells(this.grid));
|
|
914
|
+
startWinFlash(this.runtime, this.scriptedOutroStartedAt + this.scriptedOutroElapsedMs);
|
|
915
|
+
this.runtime.activeSpinState?.callback?.();
|
|
916
|
+
if (this.button && this.runtime.shouldHighlightCurrentSpin && this.spinQueueController.hasPending()) this.button.disabled = false;
|
|
917
|
+
}
|
|
918
|
+
tryStartScriptedCascade(now) {
|
|
919
|
+
if (this.scriptedCascadeQueue.length === 0) return false;
|
|
920
|
+
const nextGrid = this.scriptedCascadeQueue.shift();
|
|
921
|
+
if (!nextGrid) return false;
|
|
922
|
+
this.startOutroTransition(normalizeStopGrid(nextGrid, this.spriteElementsCount), now);
|
|
923
|
+
return true;
|
|
924
|
+
}
|
|
925
|
+
startOutroTransition(nextGrid, now) {
|
|
926
|
+
this.scriptedOutgoingGrid = this.grid.map((column) => [...column]);
|
|
927
|
+
this.scriptedPendingGrid = nextGrid.map((column) => [...column]);
|
|
928
|
+
this.outroMotionPlan = this.createOutroMotionPlan();
|
|
929
|
+
this.resetOutroBuffers(0, "hidden");
|
|
930
|
+
this.clearWinningCells();
|
|
931
|
+
this.runtime.phase = "outro";
|
|
932
|
+
this.scriptedOutroStartedAt = now;
|
|
933
|
+
this.scriptedOutroElapsedMs = 0;
|
|
934
|
+
this.outroInterpolationAlpha = 1;
|
|
935
|
+
}
|
|
936
|
+
finishSpinWithUi(skipCallback = false) {
|
|
937
|
+
const finishedSpinState = this.runtime.activeSpinState;
|
|
938
|
+
const callback = skipCallback ? void 0 : finishedSpinState?.callback;
|
|
939
|
+
const hasPending = this.spinQueueController.hasPending();
|
|
940
|
+
finishSpin(this.runtime, hasPending, performance.now());
|
|
941
|
+
if (this.button) this.button.disabled = this.runtime.queueFinished;
|
|
942
|
+
callback?.();
|
|
943
|
+
}
|
|
944
|
+
dismissHighlightIfActive() {
|
|
945
|
+
if (this.runtime.phase !== "winFlash") return;
|
|
946
|
+
this.finishSpinWithUi(true);
|
|
947
|
+
}
|
|
948
|
+
applyInitialHighlightIfNeeded() {
|
|
949
|
+
if (!this.highlightInitialWinningCells) return;
|
|
950
|
+
this.setWinningCells(findMostFrequentCells(this.grid));
|
|
951
|
+
this.initialHighlightRequestedAt = performance.now();
|
|
952
|
+
}
|
|
953
|
+
warmUpOutroPipeline() {
|
|
954
|
+
if (this.isOutroPipelineWarmedUp) return;
|
|
955
|
+
const motionPlan = this.createOutroMotionPlan();
|
|
956
|
+
const outgoingOffsets = createZeroOffsets();
|
|
957
|
+
const incomingOffsets = createZeroOffsets();
|
|
958
|
+
const incomingAlpha = createZeroOffsets();
|
|
959
|
+
const incomingVisibility = CascadingReel.createVisibilityGrid("hidden");
|
|
960
|
+
const maxRowDelay = Math.max(...motionPlan.rowStartDelays);
|
|
961
|
+
const maxColumnDelay = (3 - 1) * motionPlan.columnStaggerMs;
|
|
962
|
+
const warmupMoments = [
|
|
963
|
+
0,
|
|
964
|
+
this.motionProfile.fixedStepMs,
|
|
965
|
+
motionPlan.incomingStartShift + this.motionProfile.fixedStepMs,
|
|
966
|
+
motionPlan.fallMs + maxRowDelay + maxColumnDelay + this.motionProfile.fixedStepMs
|
|
967
|
+
];
|
|
968
|
+
for (const elapsedMs of warmupMoments) updateOutroOffsets({
|
|
969
|
+
elapsedMs,
|
|
970
|
+
scriptedOutgoingOffsets: outgoingOffsets,
|
|
971
|
+
scriptedIncomingOffsets: incomingOffsets,
|
|
972
|
+
scriptedIncomingAlpha: incomingAlpha,
|
|
973
|
+
scriptedIncomingVisibility: incomingVisibility,
|
|
974
|
+
motionPlan
|
|
975
|
+
});
|
|
976
|
+
this.isOutroPipelineWarmedUp = true;
|
|
977
|
+
}
|
|
978
|
+
createOutroMotionPlan() {
|
|
979
|
+
const plan = buildOutroMotionPlan({
|
|
980
|
+
height: this.height,
|
|
981
|
+
boardY: this.boardY,
|
|
982
|
+
cellH: this.cellH,
|
|
983
|
+
motionProfile: this.motionProfile
|
|
984
|
+
});
|
|
985
|
+
if (!this.isCoarsePointerDevice) return plan;
|
|
986
|
+
const frameMs = 1e3 / 60;
|
|
987
|
+
return {
|
|
988
|
+
...plan,
|
|
989
|
+
columnStaggerMs: this.quantizeMs(plan.columnStaggerMs, frameMs),
|
|
990
|
+
incomingStartShift: this.quantizeMs(plan.incomingStartShift, frameMs),
|
|
991
|
+
rowStartDelays: [
|
|
992
|
+
this.quantizeMs(plan.rowStartDelays[0], frameMs),
|
|
993
|
+
this.quantizeMs(plan.rowStartDelays[1], frameMs),
|
|
994
|
+
this.quantizeMs(plan.rowStartDelays[2], frameMs)
|
|
995
|
+
]
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
quantizeMs(valueMs, frameMs) {
|
|
999
|
+
if (valueMs <= 0) return 0;
|
|
1000
|
+
return Math.max(1, Math.round(valueMs / frameMs)) * frameMs;
|
|
1001
|
+
}
|
|
1002
|
+
warmUpGpuPipeline() {
|
|
1003
|
+
if (this.isGpuPipelineWarmedUp) return;
|
|
1004
|
+
const renderer = this.webglRenderer;
|
|
1005
|
+
if (!renderer) return;
|
|
1006
|
+
const warmW = Math.max(8, this.cellW * .2);
|
|
1007
|
+
const warmH = Math.max(8, this.cellH * .2);
|
|
1008
|
+
const warmX = this.boardX + 2;
|
|
1009
|
+
const warmY = this.boardY + 2;
|
|
1010
|
+
renderer.beginFrame();
|
|
1011
|
+
renderer.drawSprite(0, warmX, warmY, warmW, warmH, 1);
|
|
1012
|
+
renderer.drawSolidRect(warmX + warmW + 2, warmY, warmW, warmH, [
|
|
1013
|
+
1,
|
|
1014
|
+
1,
|
|
1015
|
+
1,
|
|
1016
|
+
.35
|
|
1017
|
+
]);
|
|
1018
|
+
renderer.beginAdditiveBlend();
|
|
1019
|
+
renderer.drawSoftCircle(warmX + warmW * .5, warmY + warmH * .5, Math.max(2, warmW * .25), [
|
|
1020
|
+
1,
|
|
1021
|
+
.9,
|
|
1022
|
+
.4,
|
|
1023
|
+
.4
|
|
1024
|
+
]);
|
|
1025
|
+
renderer.endAdditiveBlend();
|
|
1026
|
+
this.isGpuPipelineWarmedUp = true;
|
|
1027
|
+
}
|
|
1028
|
+
trackOutroPerf(dt, now) {
|
|
1029
|
+
if (this.runtime.phase !== "outro") {
|
|
1030
|
+
this.resetPerfWindow();
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
if (this.perfWindowStartedAt <= 0) this.perfWindowStartedAt = now;
|
|
1034
|
+
this.perfFrameCount += 1;
|
|
1035
|
+
if (dt > 20) this.perfOver20MsCount += 1;
|
|
1036
|
+
this.perfDtSamples.push(dt);
|
|
1037
|
+
if (now - this.perfWindowStartedAt < CascadingReel.PERF_WINDOW_MS) return;
|
|
1038
|
+
this.flushPerfWindow();
|
|
1039
|
+
this.perfWindowStartedAt = now;
|
|
1040
|
+
}
|
|
1041
|
+
resetPerfWindow() {
|
|
1042
|
+
this.perfWindowStartedAt = 0;
|
|
1043
|
+
this.perfFrameCount = 0;
|
|
1044
|
+
this.perfOver20MsCount = 0;
|
|
1045
|
+
this.perfDtSamples.length = 0;
|
|
1046
|
+
}
|
|
1047
|
+
flushPerfWindow() {
|
|
1048
|
+
if (this.perfFrameCount === 0) return;
|
|
1049
|
+
const sorted = [...this.perfDtSamples].sort((a, b) => a - b);
|
|
1050
|
+
const p95Dt = sorted[Math.max(0, Math.min(sorted.length - 1, Math.floor(sorted.length * .95)))];
|
|
1051
|
+
this.adjustMobileDprCap(p95Dt, this.perfOver20MsCount, this.perfFrameCount);
|
|
1052
|
+
this.perfFrameCount = 0;
|
|
1053
|
+
this.perfOver20MsCount = 0;
|
|
1054
|
+
this.perfDtSamples.length = 0;
|
|
1055
|
+
}
|
|
1056
|
+
adjustMobileDprCap(p95Dt, slowFrames, totalFrames) {
|
|
1057
|
+
if (!this.isCoarsePointerDevice) return;
|
|
1058
|
+
if (totalFrames <= 0) return;
|
|
1059
|
+
const slowRatio = slowFrames / totalFrames;
|
|
1060
|
+
const isBadWindow = p95Dt > CascadingReel.PERF_BAD_P95_DT_MS || slowRatio > CascadingReel.PERF_BAD_SLOW_RATIO;
|
|
1061
|
+
const isGoodWindow = p95Dt <= CascadingReel.PERF_GOOD_P95_DT_MS && slowFrames === 0;
|
|
1062
|
+
let nextCap = this.mobileDprCap;
|
|
1063
|
+
if (isBadWindow) {
|
|
1064
|
+
this.mobilePerfBadWindows += 1;
|
|
1065
|
+
this.mobilePerfGoodWindows = 0;
|
|
1066
|
+
if (this.mobilePerfBadWindows >= CascadingReel.MOBILE_BAD_WINDOWS_TO_DECREASE_DPR) {
|
|
1067
|
+
nextCap = Math.max(CascadingReel.MOBILE_DPR_CAP_MIN, this.mobileDprCap - CascadingReel.MOBILE_DPR_STEP_DOWN);
|
|
1068
|
+
this.mobilePerfBadWindows = 0;
|
|
1069
|
+
}
|
|
1070
|
+
} else if (isGoodWindow) {
|
|
1071
|
+
this.mobilePerfGoodWindows += 1;
|
|
1072
|
+
this.mobilePerfBadWindows = 0;
|
|
1073
|
+
if (this.mobilePerfGoodWindows >= CascadingReel.MOBILE_GOOD_WINDOWS_TO_INCREASE_DPR) {
|
|
1074
|
+
nextCap = Math.min(CascadingReel.MOBILE_DPR_CAP_MAX, this.mobileDprCap + CascadingReel.MOBILE_DPR_STEP_UP);
|
|
1075
|
+
this.mobilePerfGoodWindows = 0;
|
|
1076
|
+
}
|
|
1077
|
+
} else {
|
|
1078
|
+
this.mobilePerfGoodWindows = 0;
|
|
1079
|
+
this.mobilePerfBadWindows = 0;
|
|
1080
|
+
}
|
|
1081
|
+
if (Math.abs(nextCap - this.mobileDprCap) < .001) return;
|
|
1082
|
+
this.mobileDprCap = nextCap;
|
|
1083
|
+
this.resize();
|
|
1084
|
+
}
|
|
1085
|
+
render(now) {
|
|
1086
|
+
if (!this.webglRenderer) return;
|
|
1087
|
+
if (this.initialHighlightRequestedAt > 0 && now - this.initialHighlightRequestedAt >= 200) {
|
|
1088
|
+
startWinFlash(this.runtime, this.initialHighlightRequestedAt);
|
|
1089
|
+
this.runtime.winEffectsEnvelope = 1;
|
|
1090
|
+
this.initialHighlightRequestedAt = 0;
|
|
1091
|
+
if (this.button) this.button.disabled = false;
|
|
1092
|
+
}
|
|
1093
|
+
const winEffectsTarget = this.runtime.phase === "preSpin" ? 0 : this.runtime.phase === "winFlash" ? 1 : 0;
|
|
1094
|
+
if (this.lastRafTime > 0) {
|
|
1095
|
+
const dt = Math.min(now - this.lastRafTime, 50);
|
|
1096
|
+
this.trackOutroPerf(dt, now);
|
|
1097
|
+
const kWin = 1 - Math.exp(-dt / CascadingReel.WIN_EFFECTS_ENVELOPE_TAU_MS);
|
|
1098
|
+
this.runtime.winEffectsEnvelope += (winEffectsTarget - this.runtime.winEffectsEnvelope) * kWin;
|
|
1099
|
+
} else this.resetPerfWindow();
|
|
1100
|
+
this.lastRafTime = now;
|
|
1101
|
+
this.webglRenderer.beginFrame();
|
|
1102
|
+
const skipWinningCells = this.runtime.phase === "winFlash" || this.runtime.phase === "preSpin" || this.initialHighlightRequestedAt > 0 && this.winningCells.length > 0;
|
|
1103
|
+
if (this.runtime.phase === "outro" && this.scriptedOutgoingGrid && this.scriptedPendingGrid) {
|
|
1104
|
+
this.drawGridInterpolated(this.scriptedOutgoingGrid, this.scriptedOutgoingOffsetsPrev, this.scriptedOutgoingOffsets, this.outroInterpolationAlpha, skipWinningCells);
|
|
1105
|
+
this.drawGridInterpolated(this.scriptedPendingGrid, this.scriptedIncomingOffsetsPrev, this.scriptedIncomingOffsets, this.outroInterpolationAlpha, skipWinningCells, this.scriptedIncomingAlphaPrev, this.scriptedIncomingAlpha, this.scriptedIncomingVisibility);
|
|
1106
|
+
} else this.drawGrid(this.grid, null, skipWinningCells);
|
|
1107
|
+
const winPhase = this.initialHighlightRequestedAt > 0 ? "winFlash" : this.runtime.phase;
|
|
1108
|
+
const winFlashStartedAt = this.initialHighlightRequestedAt > 0 ? this.initialHighlightRequestedAt : this.runtime.winFlashStartedAt;
|
|
1109
|
+
const winEffectsEnvelope = this.initialHighlightRequestedAt > 0 ? 1 : this.runtime.winEffectsEnvelope;
|
|
1110
|
+
this.drawWinningEffects({
|
|
1111
|
+
now,
|
|
1112
|
+
phase: winPhase,
|
|
1113
|
+
winFlashStartedAt,
|
|
1114
|
+
winEffectsEnvelope
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
isWinningCell = (col, row) => {
|
|
1118
|
+
return this.winningCellKeys.has(`${col}:${row}`);
|
|
1119
|
+
};
|
|
1120
|
+
getRowCompactOffset(row) {
|
|
1121
|
+
return (ROW_COMPACT_OFFSETS_RATIO[row] ?? 0) * this.cellH;
|
|
1122
|
+
}
|
|
1123
|
+
applyPixelSnapY(y) {
|
|
1124
|
+
if (this.isCoarsePointerDevice) return Math.round(y * 2) / 2;
|
|
1125
|
+
return y;
|
|
1126
|
+
}
|
|
1127
|
+
drawGrid(grid, offsets, skipWinningCells) {
|
|
1128
|
+
this.drawGridWithSampler({
|
|
1129
|
+
grid,
|
|
1130
|
+
skipWinningCells,
|
|
1131
|
+
sampleOffsetY: (col, row) => offsets ? offsets[col][row] : 0,
|
|
1132
|
+
sampleAlpha: () => 1,
|
|
1133
|
+
isVisible: () => true
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
drawGridInterpolated(grid, prevOffsets, currOffsets, alpha, skipWinningCells, prevOpacity, currOpacity, visibility) {
|
|
1137
|
+
this.drawGridWithSampler({
|
|
1138
|
+
grid,
|
|
1139
|
+
skipWinningCells,
|
|
1140
|
+
sampleOffsetY: (col, row) => prevOffsets[col][row] + (currOffsets[col][row] - prevOffsets[col][row]) * alpha,
|
|
1141
|
+
sampleAlpha: (col, row) => prevOpacity && currOpacity ? prevOpacity[col][row] + (currOpacity[col][row] - prevOpacity[col][row]) * alpha : 1,
|
|
1142
|
+
isVisible: (col, row) => !visibility || visibility[col][row] !== "hidden"
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
drawGridWithSampler(sampler) {
|
|
1146
|
+
const renderer = this.webglRenderer;
|
|
1147
|
+
if (!renderer) return;
|
|
1148
|
+
for (let col = 0; col < 3; col += 1) {
|
|
1149
|
+
const x = this.boardX + col * this.cellW;
|
|
1150
|
+
for (let row = 0; row < 3; row += 1) {
|
|
1151
|
+
if (sampler.skipWinningCells && this.isWinningCell(col, row)) continue;
|
|
1152
|
+
if (!sampler.isVisible(col, row)) continue;
|
|
1153
|
+
const offsetY = sampler.sampleOffsetY(col, row);
|
|
1154
|
+
const y = this.applyPixelSnapY(this.boardY + row * this.cellH + offsetY + this.getRowCompactOffset(row));
|
|
1155
|
+
if (y > this.height || y + this.cellH < 0) continue;
|
|
1156
|
+
const spriteAlpha = sampler.sampleAlpha(col, row);
|
|
1157
|
+
if (spriteAlpha <= 0) continue;
|
|
1158
|
+
const symbolW = this.cellW * this.symbolScale;
|
|
1159
|
+
const symbolH = this.cellH * this.symbolScale;
|
|
1160
|
+
const symbolOffsetX = (this.cellW - symbolW) * .5;
|
|
1161
|
+
const symbolOffsetY = (this.cellH - symbolH) * .5;
|
|
1162
|
+
renderer.drawSprite(sampler.grid[col][row], x + symbolOffsetX, y + symbolOffsetY, symbolW, symbolH, spriteAlpha);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
drawWinningEffects(params) {
|
|
1167
|
+
const renderer = this.webglRenderer;
|
|
1168
|
+
if (!renderer) return;
|
|
1169
|
+
if (this.winningCells.length === 0) return;
|
|
1170
|
+
if (params.phase !== "winFlash" && params.phase !== "preSpin") return;
|
|
1171
|
+
const envelope = Math.max(0, Math.min(1, params.winEffectsEnvelope));
|
|
1172
|
+
const elapsed = Math.max(0, params.now - params.winFlashStartedAt);
|
|
1173
|
+
const pulseProgress = elapsed % FLOW_WIN_PULSE_PERIOD_MS / FLOW_WIN_PULSE_PERIOD_MS;
|
|
1174
|
+
const pulse = 1 + Math.sin(pulseProgress * Math.PI * 2) * FLOW_WIN_PULSE_AMPLITUDE * envelope;
|
|
1175
|
+
const borderInset = Math.max(1, this.cellW * CascadingReel.WIN_BORDER_INSET_RATIO);
|
|
1176
|
+
const borderThickness = Math.max(1, this.cellW * .022);
|
|
1177
|
+
const particleModeActive = this.runtime.phase === "winFlash" && this.runtime.shouldHighlightCurrentSpin && this.runtime.hasStartedFirstSpin;
|
|
1178
|
+
for (const cell of this.winningCells) {
|
|
1179
|
+
const baseX = this.boardX + cell.col * this.cellW;
|
|
1180
|
+
const baseY = this.boardY + cell.row * this.cellH + this.getRowCompactOffset(cell.row);
|
|
1181
|
+
const symbol = this.grid[cell.col][cell.row];
|
|
1182
|
+
const alpha = CascadingReel.WIN_BORDER_ALPHA * envelope;
|
|
1183
|
+
const borderColor = this.particleColorMode === "rainbow" ? CascadingReel.hslToRgb01((elapsed * .2 + cell.col * 36 + cell.row * 22) % 360, .96, .64) : [
|
|
1184
|
+
this.particleColorRgb[0] / 255,
|
|
1185
|
+
this.particleColorRgb[1] / 255,
|
|
1186
|
+
this.particleColorRgb[2] / 255
|
|
1187
|
+
];
|
|
1188
|
+
const scaledW = this.cellW * this.symbolScale * pulse;
|
|
1189
|
+
const scaledH = this.cellH * this.symbolScale * pulse;
|
|
1190
|
+
const offsetX = (this.cellW - scaledW) * .5;
|
|
1191
|
+
const offsetY = (this.cellH - scaledH) * .5;
|
|
1192
|
+
renderer.drawSprite(symbol, baseX + offsetX, baseY + offsetY, scaledW, scaledH, 1);
|
|
1193
|
+
const innerX = baseX + borderInset;
|
|
1194
|
+
const innerY = baseY + borderInset;
|
|
1195
|
+
const innerW = this.cellW - borderInset * 2;
|
|
1196
|
+
const innerH = this.cellH - borderInset * 2;
|
|
1197
|
+
if (innerW <= borderThickness * 2 || innerH <= borderThickness * 2) continue;
|
|
1198
|
+
this.drawElectricBorder({
|
|
1199
|
+
renderer,
|
|
1200
|
+
cell,
|
|
1201
|
+
x: innerX,
|
|
1202
|
+
y: innerY,
|
|
1203
|
+
w: innerW,
|
|
1204
|
+
h: innerH,
|
|
1205
|
+
borderThickness,
|
|
1206
|
+
borderColor,
|
|
1207
|
+
alpha,
|
|
1208
|
+
elapsed,
|
|
1209
|
+
envelope
|
|
1210
|
+
});
|
|
1211
|
+
if (particleModeActive) this.drawCellParticleBurst({
|
|
1212
|
+
renderer,
|
|
1213
|
+
cell,
|
|
1214
|
+
centerX: baseX + this.cellW * .5,
|
|
1215
|
+
centerY: baseY + this.cellH * .5,
|
|
1216
|
+
elapsed,
|
|
1217
|
+
envelope
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
drawCellParticleBurst(params) {
|
|
1222
|
+
const maxDistance = Math.min(this.cellW, this.cellH) * CascadingReel.PARTICLE_MAX_DISTANCE;
|
|
1223
|
+
const baseRadius = Math.min(this.cellW, this.cellH) * CascadingReel.PARTICLE_BASE_RADIUS;
|
|
1224
|
+
const seedsList = CascadingReel.getParticleSeeds(params.cell.col, params.cell.row);
|
|
1225
|
+
const solidColor = [
|
|
1226
|
+
this.particleColorRgb[0] / 255,
|
|
1227
|
+
this.particleColorRgb[1] / 255,
|
|
1228
|
+
this.particleColorRgb[2] / 255
|
|
1229
|
+
];
|
|
1230
|
+
params.renderer.beginAdditiveBlend();
|
|
1231
|
+
for (let i = 0; i < this.particlesPerCell; i += 1) {
|
|
1232
|
+
const s = seedsList[i];
|
|
1233
|
+
const startTime = s.phaseOffset * 720;
|
|
1234
|
+
const age = params.elapsed - startTime;
|
|
1235
|
+
if (age < 0) continue;
|
|
1236
|
+
const particleT = age % 720 / 720;
|
|
1237
|
+
const direction = s.seedA * Math.PI * 2;
|
|
1238
|
+
const distance = maxDistance * particleT * (.35 + s.seedB * .65);
|
|
1239
|
+
const px = params.centerX + Math.cos(direction) * distance;
|
|
1240
|
+
const py = params.centerY + Math.sin(direction) * distance;
|
|
1241
|
+
const twinkle = .7 + .9 * Math.max(0, Math.sin((params.elapsed * .012 + s.twinkleSeed * 2) * Math.PI * 2));
|
|
1242
|
+
const radius = Math.max(1, baseRadius * (.55 + s.seedC * .6) * (1 - particleT * .5));
|
|
1243
|
+
const alpha = Math.max(0, Math.min(1, (.9 + twinkle * .2) * CascadingReel.PARTICLE_GLOBAL_ALPHA * params.envelope));
|
|
1244
|
+
if (alpha <= 0) continue;
|
|
1245
|
+
const rgb = this.particleColorMode === "rainbow" ? CascadingReel.getRainbowParticleColor(params.elapsed, params.cell.col, params.cell.row, s.seedA) : solidColor;
|
|
1246
|
+
params.renderer.drawSoftCircle(px, py, radius, [
|
|
1247
|
+
rgb[0],
|
|
1248
|
+
rgb[1],
|
|
1249
|
+
rgb[2],
|
|
1250
|
+
alpha
|
|
1251
|
+
]);
|
|
1252
|
+
}
|
|
1253
|
+
params.renderer.endAdditiveBlend();
|
|
1254
|
+
}
|
|
1255
|
+
drawElectricBorder(params) {
|
|
1256
|
+
const baseA = Math.max(0, Math.min(1, params.alpha * params.envelope));
|
|
1257
|
+
const expand = params.borderThickness * 1.8;
|
|
1258
|
+
const seedTime = params.elapsed + params.cell.col * 170 + params.cell.row * 290;
|
|
1259
|
+
params.renderer.beginAdditiveBlend();
|
|
1260
|
+
params.renderer.drawElectricBorder({
|
|
1261
|
+
x: params.x - expand,
|
|
1262
|
+
y: params.y - expand,
|
|
1263
|
+
width: params.w + expand * 2,
|
|
1264
|
+
height: params.h + expand * 2,
|
|
1265
|
+
rgba: [
|
|
1266
|
+
params.borderColor[0],
|
|
1267
|
+
params.borderColor[1],
|
|
1268
|
+
params.borderColor[2],
|
|
1269
|
+
baseA * .55
|
|
1270
|
+
],
|
|
1271
|
+
timeMs: seedTime,
|
|
1272
|
+
borderThicknessPx: Math.max(.8, params.borderThickness * 1.1),
|
|
1273
|
+
borderInsetPx: expand * .85,
|
|
1274
|
+
cornerRadiusPx: Math.max(2, params.borderThickness * 2.5),
|
|
1275
|
+
noiseAmplitudePx: Math.max(.15, params.borderThickness * .6),
|
|
1276
|
+
pulseStrength: .9
|
|
1277
|
+
});
|
|
1278
|
+
params.renderer.drawElectricBorder({
|
|
1279
|
+
x: params.x - expand * .5,
|
|
1280
|
+
y: params.y - expand * .5,
|
|
1281
|
+
width: params.w + expand,
|
|
1282
|
+
height: params.h + expand,
|
|
1283
|
+
rgba: [
|
|
1284
|
+
params.borderColor[0],
|
|
1285
|
+
params.borderColor[1],
|
|
1286
|
+
params.borderColor[2],
|
|
1287
|
+
baseA * .9
|
|
1288
|
+
],
|
|
1289
|
+
timeMs: seedTime * 1.03,
|
|
1290
|
+
borderThicknessPx: Math.max(.7, params.borderThickness * .8),
|
|
1291
|
+
borderInsetPx: expand * .5,
|
|
1292
|
+
cornerRadiusPx: Math.max(1.5, params.borderThickness * 1.7),
|
|
1293
|
+
noiseAmplitudePx: Math.max(.12, params.borderThickness * .42),
|
|
1294
|
+
pulseStrength: 1.25
|
|
1295
|
+
});
|
|
1296
|
+
params.renderer.endAdditiveBlend();
|
|
1297
|
+
}
|
|
1298
|
+
static hslToRgb01(hueDeg, saturation, lightness) {
|
|
1299
|
+
const h = (hueDeg % 360 + 360) % 360;
|
|
1300
|
+
const s = Math.max(0, Math.min(1, saturation));
|
|
1301
|
+
const l = Math.max(0, Math.min(1, lightness));
|
|
1302
|
+
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
1303
|
+
const hp = h / 60;
|
|
1304
|
+
const x = c * (1 - Math.abs(hp % 2 - 1));
|
|
1305
|
+
let r = 0;
|
|
1306
|
+
let g = 0;
|
|
1307
|
+
let b = 0;
|
|
1308
|
+
if (hp >= 0 && hp < 1) {
|
|
1309
|
+
r = c;
|
|
1310
|
+
g = x;
|
|
1311
|
+
} else if (hp < 2) {
|
|
1312
|
+
r = x;
|
|
1313
|
+
g = c;
|
|
1314
|
+
} else if (hp < 3) {
|
|
1315
|
+
g = c;
|
|
1316
|
+
b = x;
|
|
1317
|
+
} else if (hp < 4) {
|
|
1318
|
+
g = x;
|
|
1319
|
+
b = c;
|
|
1320
|
+
} else if (hp < 5) {
|
|
1321
|
+
r = x;
|
|
1322
|
+
b = c;
|
|
1323
|
+
} else {
|
|
1324
|
+
r = c;
|
|
1325
|
+
b = x;
|
|
1326
|
+
}
|
|
1327
|
+
const m = l - c * .5;
|
|
1328
|
+
return [
|
|
1329
|
+
r + m,
|
|
1330
|
+
g + m,
|
|
1331
|
+
b + m
|
|
1332
|
+
];
|
|
1333
|
+
}
|
|
1334
|
+
static getRainbowParticleColor(elapsed, col, row, seedA) {
|
|
1335
|
+
const hueRaw = (seedA * 360 + elapsed * .24 + col * 38 + row * 22) % 360;
|
|
1336
|
+
const bucket = 360 / CascadingReel.RAINBOW_HUE_BUCKETS;
|
|
1337
|
+
const hue = Math.floor(hueRaw / bucket) * bucket;
|
|
1338
|
+
return CascadingReel.hslToRgb01(hue, .98, .64);
|
|
1339
|
+
}
|
|
1340
|
+
static detectCoarsePointerDevice() {
|
|
1341
|
+
if (typeof window === "undefined") return false;
|
|
1342
|
+
const coarseMatch = typeof window.matchMedia === "function" && window.matchMedia("(pointer: coarse)").matches;
|
|
1343
|
+
const touchPoints = typeof navigator !== "undefined" && typeof navigator.maxTouchPoints === "number" ? navigator.maxTouchPoints : 0;
|
|
1344
|
+
return coarseMatch || touchPoints > 0;
|
|
1345
|
+
}
|
|
1346
|
+
quantizeDprCap(value) {
|
|
1347
|
+
const step = CascadingReel.DPR_QUANT_STEP;
|
|
1348
|
+
return Math.max(1, Math.round(value / step) * step);
|
|
1349
|
+
}
|
|
1350
|
+
static hash01(a, b, c, d) {
|
|
1351
|
+
const value = Math.sin(a * 127.1 + b * 311.7 + c * 74.7 + d * 19.3) * 43758.5453;
|
|
1352
|
+
return value - Math.floor(value);
|
|
1353
|
+
}
|
|
1354
|
+
static getParticleSeeds(col, row) {
|
|
1355
|
+
const key = `${col},${row}`;
|
|
1356
|
+
const cached = CascadingReel.particleSeedsCache.get(key);
|
|
1357
|
+
if (cached) return cached;
|
|
1358
|
+
const generated = [];
|
|
1359
|
+
for (let i = 0; i < 34; i += 1) generated.push({
|
|
1360
|
+
seedA: CascadingReel.hash01(col, row, i, 1),
|
|
1361
|
+
seedB: CascadingReel.hash01(col, row, i, 2),
|
|
1362
|
+
seedC: CascadingReel.hash01(col, row, i, 3),
|
|
1363
|
+
phaseOffset: CascadingReel.hash01(col, row, i, 4),
|
|
1364
|
+
twinkleSeed: CascadingReel.hash01(col, row, i, 5)
|
|
1365
|
+
});
|
|
1366
|
+
CascadingReel.particleSeedsCache.set(key, generated);
|
|
1367
|
+
return generated;
|
|
1368
|
+
}
|
|
1369
|
+
clearWinningCells() {
|
|
1370
|
+
this.winningCells = [];
|
|
1371
|
+
this.winningCellKeys.clear();
|
|
1372
|
+
}
|
|
1373
|
+
setWinningCells(cells) {
|
|
1374
|
+
this.winningCells = cells;
|
|
1375
|
+
this.winningCellKeys.clear();
|
|
1376
|
+
for (const cell of cells) this.winningCellKeys.add(`${cell.col}:${cell.row}`);
|
|
1377
|
+
}
|
|
1378
|
+
resize = () => {
|
|
1379
|
+
const bounds = this.container.getBoundingClientRect();
|
|
1380
|
+
const cssSide = Math.max(300, Math.floor(bounds.width));
|
|
1381
|
+
const cssArea = Math.max(1, cssSide * cssSide);
|
|
1382
|
+
const maxCanvasArea = this.isCoarsePointerDevice ? CascadingReel.MAX_CANVAS_AREA_PX_COARSE : CascadingReel.MAX_CANVAS_AREA_PX_FINE;
|
|
1383
|
+
const dprAreaCap = Math.max(1, Math.sqrt(maxCanvasArea / cssArea));
|
|
1384
|
+
const requestedCap = this.isCoarsePointerDevice ? this.mobileDprCap : 2;
|
|
1385
|
+
const dprCap = this.quantizeDprCap(Math.min(requestedCap, dprAreaCap));
|
|
1386
|
+
const dpr = Math.max(1, Math.min(window.devicePixelRatio || 1, dprCap));
|
|
1387
|
+
const side = Math.max(300, Math.floor(cssSide * dpr));
|
|
1388
|
+
this.width = side;
|
|
1389
|
+
this.height = side;
|
|
1390
|
+
const squareSize = Math.floor(Math.min(this.width / 3, this.height / 3));
|
|
1391
|
+
this.cellW = squareSize;
|
|
1392
|
+
this.cellH = squareSize;
|
|
1393
|
+
this.boardX = Math.floor((this.width - this.cellW * 3) / 2);
|
|
1394
|
+
this.boardY = Math.floor((this.height - this.cellH * 3) / 2);
|
|
1395
|
+
this.canvas.width = this.width;
|
|
1396
|
+
this.canvas.height = this.height;
|
|
1397
|
+
this.webglRenderer?.resize(this.width, this.height);
|
|
1398
|
+
};
|
|
1399
|
+
async loadSpriteIfProvided() {
|
|
1400
|
+
if (!this.spriteSource) {
|
|
1401
|
+
this.spriteImage = null;
|
|
1402
|
+
this.spriteLoadError = null;
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
if (typeof this.spriteSource !== "string") {
|
|
1406
|
+
try {
|
|
1407
|
+
await this.decodeSpriteImage(this.spriteSource);
|
|
1408
|
+
this.spriteImage = this.spriteSource;
|
|
1409
|
+
this.spriteLoadError = null;
|
|
1410
|
+
} catch (error) {
|
|
1411
|
+
this.spriteImage = null;
|
|
1412
|
+
this.spriteLoadError = this.toSpriteLoadErrorMessage(error, "[HTMLImageElement]");
|
|
1413
|
+
}
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
const image = new Image();
|
|
1417
|
+
image.decoding = "async";
|
|
1418
|
+
image.crossOrigin = this.spriteCrossOrigin;
|
|
1419
|
+
image.src = this.spriteSource;
|
|
1420
|
+
try {
|
|
1421
|
+
await this.decodeSpriteImage(image);
|
|
1422
|
+
this.spriteImage = image;
|
|
1423
|
+
this.spriteLoadError = null;
|
|
1424
|
+
} catch (error) {
|
|
1425
|
+
this.spriteImage = null;
|
|
1426
|
+
this.spriteLoadError = this.toSpriteLoadErrorMessage(error, this.spriteSource);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
async decodeSpriteImage(image) {
|
|
1430
|
+
if (image.complete && image.naturalWidth > 0 && image.naturalHeight > 0) return;
|
|
1431
|
+
await image.decode();
|
|
1432
|
+
}
|
|
1433
|
+
toSpriteLoadErrorMessage(error, spriteSourceLabel) {
|
|
1434
|
+
return `failed to load sprite "${spriteSourceLabel}" - ${error instanceof Error ? `${error.name}: ${error.message}` : String(error)}`;
|
|
1435
|
+
}
|
|
1436
|
+
advanceSimulation(now) {
|
|
1437
|
+
if (this.simulationLastNow <= 0) {
|
|
1438
|
+
this.simulationLastNow = now;
|
|
1439
|
+
this.update(now);
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
const frameDt = Math.max(0, Math.min(now - this.simulationLastNow, CascadingReel.MAX_FRAME_DELTA_MS));
|
|
1443
|
+
this.simulationLastNow = now;
|
|
1444
|
+
this.update(now);
|
|
1445
|
+
if (this.runtime.phase !== "outro") {
|
|
1446
|
+
this.outroInterpolationAlpha = 1;
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
this.advanceOutroFixedStep(frameDt);
|
|
1450
|
+
}
|
|
1451
|
+
advanceOutroFixedStep(frameDt) {
|
|
1452
|
+
this.simulationAccumulatorMs += frameDt;
|
|
1453
|
+
const stepMs = this.motionProfile.fixedStepMs;
|
|
1454
|
+
let steps = 0;
|
|
1455
|
+
while (this.simulationAccumulatorMs >= stepMs && steps < this.motionProfile.maxCatchUpStepsPerFrame && this.runtime.phase === "outro") {
|
|
1456
|
+
this.snapshotOutroState();
|
|
1457
|
+
this.stepScriptedOutro(stepMs);
|
|
1458
|
+
this.simulationAccumulatorMs -= stepMs;
|
|
1459
|
+
steps += 1;
|
|
1460
|
+
}
|
|
1461
|
+
if (this.runtime.phase !== "outro") {
|
|
1462
|
+
this.outroInterpolationAlpha = 1;
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
this.outroInterpolationAlpha = Math.max(0, Math.min(1, this.simulationAccumulatorMs / stepMs));
|
|
1466
|
+
}
|
|
1467
|
+
snapshotOutroState() {
|
|
1468
|
+
this.copyOffsets(this.scriptedOutgoingOffsetsPrev, this.scriptedOutgoingOffsets);
|
|
1469
|
+
this.copyOffsets(this.scriptedIncomingOffsetsPrev, this.scriptedIncomingOffsets);
|
|
1470
|
+
this.copyOffsets(this.scriptedIncomingAlphaPrev, this.scriptedIncomingAlpha);
|
|
1471
|
+
this.copyVisibility(this.scriptedIncomingVisibilityPrev, this.scriptedIncomingVisibility);
|
|
1472
|
+
}
|
|
1473
|
+
copyOffsets(target, source) {
|
|
1474
|
+
for (let col = 0; col < 3; col += 1) for (let row = 0; row < 3; row += 1) target[col][row] = source[col][row];
|
|
1475
|
+
}
|
|
1476
|
+
copyVisibility(target, source) {
|
|
1477
|
+
for (let col = 0; col < 3; col += 1) for (let row = 0; row < 3; row += 1) target[col][row] = source[col][row];
|
|
1478
|
+
}
|
|
1479
|
+
resetOutroBuffers(incomingAlpha, incomingVisibility) {
|
|
1480
|
+
fillOffsets(this.scriptedOutgoingOffsets, 0);
|
|
1481
|
+
fillOffsets(this.scriptedIncomingOffsets, 0);
|
|
1482
|
+
fillOffsets(this.scriptedOutgoingOffsetsPrev, 0);
|
|
1483
|
+
fillOffsets(this.scriptedIncomingOffsetsPrev, 0);
|
|
1484
|
+
fillOffsets(this.scriptedIncomingAlpha, incomingAlpha);
|
|
1485
|
+
fillOffsets(this.scriptedIncomingAlphaPrev, incomingAlpha);
|
|
1486
|
+
this.fillVisibilityGrid(this.scriptedIncomingVisibility, incomingVisibility);
|
|
1487
|
+
this.fillVisibilityGrid(this.scriptedIncomingVisibilityPrev, incomingVisibility);
|
|
1488
|
+
}
|
|
1489
|
+
static createVisibilityGrid(value) {
|
|
1490
|
+
return Array.from({ length: 3 }, () => Array.from({ length: 3 }, () => value));
|
|
1491
|
+
}
|
|
1492
|
+
fillVisibilityGrid(grid, value) {
|
|
1493
|
+
for (let col = 0; col < 3; col += 1) for (let row = 0; row < 3; row += 1) grid[col][row] = value;
|
|
1494
|
+
}
|
|
1495
|
+
startLoop() {
|
|
1496
|
+
if (this.rafLoop.isRunning()) return;
|
|
1497
|
+
this.rafLoop.start((time) => {
|
|
1498
|
+
this.advanceSimulation(time);
|
|
1499
|
+
this.render(time);
|
|
1500
|
+
return this.shouldKeepAnimating();
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1503
|
+
shouldKeepAnimating() {
|
|
1504
|
+
if (this.runtime.isSpinning) return true;
|
|
1505
|
+
if (this.initialHighlightRequestedAt > 0) return true;
|
|
1506
|
+
return this.runtime.winEffectsEnvelope > .001;
|
|
1507
|
+
}
|
|
1508
|
+
};
|
|
1509
|
+
|
|
1510
|
+
//#endregion
|
|
1511
|
+
exports.CascadingReel = CascadingReel;
|
|
1512
|
+
//# sourceMappingURL=index.cjs.map
|