packing-bingo 0.3.0 → 0.4.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 +57 -25
- package/dist/{chunk-Z34GMJW2.js → chunk-BAA2CFUA.js} +174 -41
- package/dist/core.cjs +174 -41
- package/dist/core.d.cts +36 -3
- package/dist/core.d.ts +36 -3
- package/dist/core.js +1 -1
- package/dist/index.cjs +181 -43
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +8 -3
- package/dist/packing-bingo.global.js +26 -20
- package/package.json +1 -1
package/dist/core.cjs
CHANGED
|
@@ -122,7 +122,7 @@ function bingoLines(size, free = false) {
|
|
|
122
122
|
// src/messages.ts
|
|
123
123
|
var ja = {
|
|
124
124
|
title: "\u6301\u3061\u3082\u306E\u30D3\u30F3\u30B4",
|
|
125
|
-
tag: "
|
|
125
|
+
tag: "",
|
|
126
126
|
subtitle: "",
|
|
127
127
|
packed: "\u305D\u308D\u3063\u305F",
|
|
128
128
|
bingo: "BINGO",
|
|
@@ -130,6 +130,7 @@ var ja = {
|
|
|
130
130
|
combo: (n) => `${n} COMBO`,
|
|
131
131
|
comboPop: (n) => `${n} COMBO!`,
|
|
132
132
|
reset: "\u30EA\u30BB\u30C3\u30C8",
|
|
133
|
+
shuffle: "\u{1F500} \u30B7\u30E3\u30C3\u30D5\u30EB",
|
|
133
134
|
soundOn: "\u{1F50A} \u97F3\u3042\u308A",
|
|
134
135
|
soundOff: "\u{1F507} \u97F3\u306A\u3057",
|
|
135
136
|
stamp: "OK",
|
|
@@ -143,7 +144,7 @@ var ja = {
|
|
|
143
144
|
bannerMulti: (n) => `\u4E00\u6C17\u306B ${n} \u30E9\u30A4\u30F3\uFF01`,
|
|
144
145
|
statusStart: () => "",
|
|
145
146
|
statusLines: (lines, left) => `\u30D3\u30F3\u30B4 ${lines} \u30E9\u30A4\u30F3\uFF01 \u306E\u3053\u308A ${left} \u3053`,
|
|
146
|
-
statusReach: (
|
|
147
|
+
statusReach: () => "",
|
|
147
148
|
statusDone: "\u{1F389} \u30D1\u30FC\u30D5\u30A7\u30AF\u30C8\uFF01\u5FD8\u308C\u7269\u30BC\u30ED\u3067\u51FA\u767A\u3067\u304D\u307E\u3059",
|
|
148
149
|
perfectTag: "\u{1F3C6} PERFECT",
|
|
149
150
|
perfectStamp: "\u6E96\u5099\n\u5B8C\u4E86",
|
|
@@ -162,7 +163,7 @@ var ja = {
|
|
|
162
163
|
};
|
|
163
164
|
var en = {
|
|
164
165
|
title: "Packing Bingo",
|
|
165
|
-
tag: "
|
|
166
|
+
tag: "",
|
|
166
167
|
subtitle: "",
|
|
167
168
|
packed: "PACKED",
|
|
168
169
|
bingo: "BINGO",
|
|
@@ -170,6 +171,7 @@ var en = {
|
|
|
170
171
|
combo: (n) => `${n} COMBO`,
|
|
171
172
|
comboPop: (n) => `${n} COMBO!`,
|
|
172
173
|
reset: "Reset",
|
|
174
|
+
shuffle: "\u{1F500} Shuffle",
|
|
173
175
|
soundOn: "\u{1F50A} Sound on",
|
|
174
176
|
soundOff: "\u{1F507} Muted",
|
|
175
177
|
stamp: "OK",
|
|
@@ -183,7 +185,7 @@ var en = {
|
|
|
183
185
|
bannerMulti: (n) => `${n} lines at once!`,
|
|
184
186
|
statusStart: () => "",
|
|
185
187
|
statusLines: (lines, left) => `${lines} bingo line${lines === 1 ? "" : "s"}! ${left} to go`,
|
|
186
|
-
statusReach: (
|
|
188
|
+
statusReach: () => "",
|
|
187
189
|
statusDone: "\u{1F389} Perfect! Nothing left behind.",
|
|
188
190
|
perfectTag: "\u{1F3C6} PERFECT",
|
|
189
191
|
perfectStamp: "ALL\nSET",
|
|
@@ -289,6 +291,11 @@ function createSound(isMuted) {
|
|
|
289
291
|
},
|
|
290
292
|
tick: () => tone(1760, 0, 0.03, "square", 0.03),
|
|
291
293
|
note: (k, at = 0) => tone(step(k), at, 0.12, "square", 0.05),
|
|
294
|
+
shuffle() {
|
|
295
|
+
for (let i = 0; i < 7; i++) noise(i * 0.042, 0.05, 0.16, 1600 + i * 260);
|
|
296
|
+
tone(hz(7), 0.34, 0.12, "triangle", 0.14);
|
|
297
|
+
tone(hz(12), 0.42, 0.2, "triangle", 0.14);
|
|
298
|
+
},
|
|
292
299
|
close() {
|
|
293
300
|
if (ac) void ac.close().catch(() => {
|
|
294
301
|
});
|
|
@@ -316,7 +323,6 @@ function itemsKey(items) {
|
|
|
316
323
|
}
|
|
317
324
|
|
|
318
325
|
// src/styles.ts
|
|
319
|
-
var FONT_URL = "https://fonts.googleapis.com/css2?family=Dela+Gothic+One&family=M+PLUS+Rounded+1c:wght@500;800&display=swap";
|
|
320
326
|
var CSS = `
|
|
321
327
|
.pkb{--pk-sun:var(--pkb-sun,#FFD54F);--pk-ink:var(--pkb-ink,#2A1F14);--pk-paper:var(--pkb-paper,#FFFDF6);--pk-red:var(--pkb-red,#E8473B);--pk-mint:var(--pkb-mint,#BDEBD2);--pk-display:"Dela Gothic One",var(--pkb-font-fallback,sans-serif);--pk-body:"M PLUS Rounded 1c","Hiragino Maru Gothic ProN",var(--pkb-font-fallback,sans-serif);--pk-n:4;--pk-s:1;--pk-d:1;container-type:inline-size;isolation:isolate;width:100%}
|
|
322
328
|
.pkb,.pkb *{box-sizing:border-box;margin:0;padding:0}
|
|
@@ -328,6 +334,7 @@ var CSS = `
|
|
|
328
334
|
.pkb-head{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:12px 16px;margin-bottom:18px}
|
|
329
335
|
.pkb-tags{display:flex;gap:8px;align-items:center}
|
|
330
336
|
.pkb-kicker{display:inline-block;background:var(--pk-ink);color:var(--pk-sun);font-weight:800;font-size:11px;letter-spacing:.14em;padding:4px 10px;border-radius:999px}
|
|
337
|
+
.pkb-kicker:empty{display:none}
|
|
331
338
|
.pkb-kicker.pkb-gold{color:#fff;background:linear-gradient(90deg,#E8473B,#FF9F1C,#1FA971,#2F6FED,#E8473B);background-size:200% 100%;animation:pkb-rb 2s linear infinite}
|
|
332
339
|
@keyframes pkb-rb{to{background-position:-200% 0}}
|
|
333
340
|
.pkb button{font-family:var(--pk-body)}
|
|
@@ -384,8 +391,13 @@ var CSS = `
|
|
|
384
391
|
.pkb-lines .pkb-o{stroke:var(--pk-ink);stroke-width:calc(6px*var(--pk-d))}
|
|
385
392
|
.pkb-lines .pkb-draw line{animation:pkb-draw .45s ease-out .3s both}
|
|
386
393
|
@keyframes pkb-draw{from{stroke-dashoffset:1}}
|
|
387
|
-
.pkb-foot{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-top:18px;font-weight:800;font-size:13px;word-break:auto-phrase}
|
|
388
|
-
.pkb-
|
|
394
|
+
.pkb-foot{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:8px 10px;margin-top:18px;font-weight:800;font-size:13px;word-break:auto-phrase}
|
|
395
|
+
.pkb-foot>span{flex:1 1 14em}
|
|
396
|
+
.pkb-foot>span:empty{flex-basis:0}
|
|
397
|
+
.pkb-acts{display:flex;gap:8px;flex:none;margin-left:auto}
|
|
398
|
+
.pkb-reset,.pkb-shuf{appearance:none;flex:none;border:2px solid var(--pk-ink);background:transparent;border-radius:999px;padding:5px 12px;font-weight:800;font-size:12px;line-height:1.4;color:var(--pk-ink);cursor:pointer}
|
|
399
|
+
.pkb-shuf{background:var(--pk-paper);box-shadow:2px 2px 0 var(--pk-ink)}
|
|
400
|
+
.pkb-shuf:active{transform:translate(2px,2px);box-shadow:none}
|
|
389
401
|
.pkb-flash{position:absolute;inset:0;z-index:7;background:#fff;opacity:0;pointer-events:none}
|
|
390
402
|
.pkb-flash.pkb-go{animation:pkb-flash .4s ease-out}
|
|
391
403
|
@keyframes pkb-flash{0%{opacity:.8}100%{opacity:0}}
|
|
@@ -433,6 +445,7 @@ var CONFETTI = ["#E8473B", "#2F6FED", "#1FA971", "#FFFDF6", "#2A1F14", "#FF9F1C"
|
|
|
433
445
|
var COMBO_COLORS = ["#FF9F1C", "#1FA971", "#2F6FED", "#9B5DE5", "#E8473B"];
|
|
434
446
|
var COMBO_MS = 2500;
|
|
435
447
|
var MUTE_KEY = "packing-bingo-muted";
|
|
448
|
+
var ORDER_PREFIX = "packing-bingo-order:";
|
|
436
449
|
var esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
437
450
|
function readStore(key) {
|
|
438
451
|
try {
|
|
@@ -447,29 +460,25 @@ function writeStore(key, value) {
|
|
|
447
460
|
} catch {
|
|
448
461
|
}
|
|
449
462
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
l.id = "pkb-fonts";
|
|
460
|
-
l.rel = "stylesheet";
|
|
461
|
-
l.href = FONT_URL;
|
|
462
|
-
document.head.appendChild(l);
|
|
463
|
-
}
|
|
463
|
+
var glintDelay = (c, n) => `${+((Math.floor(c / n) + c % n) * 0.36 / Math.max(1, n - 1)).toFixed(3)}s`;
|
|
464
|
+
var lineKey = (l) => l.slice().sort((a, b) => a - b).join(",");
|
|
465
|
+
var FLY_MS = 560;
|
|
466
|
+
function ensureStyles() {
|
|
467
|
+
if (document.getElementById("pkb-styles")) return;
|
|
468
|
+
const s = document.createElement("style");
|
|
469
|
+
s.id = "pkb-styles";
|
|
470
|
+
s.textContent = CSS;
|
|
471
|
+
document.head.appendChild(s);
|
|
464
472
|
}
|
|
465
|
-
function template(L, M, demo) {
|
|
473
|
+
function template(L, M, { demo, shuffle, vibrate }) {
|
|
466
474
|
const n = L.size;
|
|
475
|
+
const input = vibrate ? `<input type="checkbox" switch role="checkbox">` : `<input type="checkbox">`;
|
|
467
476
|
const tiles = Array.from({ length: n * n }, (_, c) => {
|
|
468
|
-
const gd = `--pkb-gd:${
|
|
477
|
+
const gd = `--pkb-gd:${glintDelay(c, n)}`;
|
|
469
478
|
if (c === L.center)
|
|
470
479
|
return `<div class="pkb-tile pkb-free pkb-on" style="${gd}"><span class="pkb-stamp">${esc(M.free)}</span></div>`;
|
|
471
480
|
const it = L.items[L.itemCell.indexOf(c)];
|
|
472
|
-
return `<label class="pkb-tile" data-reach="${esc(M.reachBadge)}" style="${gd}"
|
|
481
|
+
return `<label class="pkb-tile" data-reach="${esc(M.reachBadge)}" style="${gd}">${input}<span class="pkb-emo" aria-hidden="true">${esc(it.emoji)}</span><span class="pkb-lbl">${esc(it.label).replace(/・/g, "\u30FB<wbr>")}</span><span class="pkb-stamp" aria-hidden="true">${esc(M.stamp)}</span></label>`;
|
|
473
482
|
}).join("");
|
|
474
483
|
const scale = `--pk-n:${n};--pk-s:${+Math.min(1.35, 4 / n).toFixed(3)};--pk-d:${+Math.min(1, 4 / n).toFixed(3)}`;
|
|
475
484
|
const demoBar = demo ? `<div class="pkb-demo"><span>${esc(M.demoLabel)}</span><button type="button" data-r="dB">${esc(M.demoBingo)}</button><button type="button" data-r="dP">${esc(M.demoPerfect)}</button><button type="button" data-r="dR">${esc(M.demoRestore)}</button></div>` : "";
|
|
@@ -484,7 +493,7 @@ function template(L, M, demo) {
|
|
|
484
493
|
<div class="pkb-combo" data-r="combo"><b data-r="cmbN"></b><span class="pkb-bar"><i data-r="cbar"></i></span></div></div></div>
|
|
485
494
|
<div class="pkb-gw"><div class="pkb-grid">${tiles}</div><svg class="pkb-lines" aria-hidden="true"></svg>
|
|
486
495
|
<div class="pkb-banner" data-r="banner" aria-hidden="true"><b data-r="bT"></b><span data-r="bS"></span></div></div>
|
|
487
|
-
<div class="pkb-foot"><span data-r="msg" aria-live="polite"></span><button type="button" class="pkb-reset" data-r="reset">${esc(M.reset)}</button></div>
|
|
496
|
+
<div class="pkb-foot"><span data-r="msg" aria-live="polite"></span><div class="pkb-acts">${shuffle ? `<button type="button" class="pkb-shuf" data-r="shuf">${esc(M.shuffle)}</button>` : ""}<button type="button" class="pkb-reset" data-r="reset">${esc(M.reset)}</button></div></div>
|
|
488
497
|
<div class="pkb-flash" data-r="flash"></div>
|
|
489
498
|
<div class="pkb-perfect" data-r="perfect"><div class="pkb-card" role="dialog" aria-label="${esc(M.perfectTitle)}">
|
|
490
499
|
<div class="pkb-medal">${esc(M.perfectStamp)}</div><h3 class="pkb-ptitle">${esc(M.perfectTitle)}</h3><p class="pkb-pbody" data-r="pbody">${esc(perfectText(M, L.items.length))}</p>
|
|
@@ -495,12 +504,22 @@ function template(L, M, demo) {
|
|
|
495
504
|
var perfectText = (M, total) => typeof M.perfectBody === "function" ? M.perfectBody(total) : M.perfectBody;
|
|
496
505
|
function createPackingBingo(container, options = {}) {
|
|
497
506
|
const L = resolveLayout(options);
|
|
498
|
-
const { items,
|
|
507
|
+
const { items, cellLines } = L;
|
|
499
508
|
const n = L.size, N = items.length;
|
|
500
509
|
const normalize = (v) => Array.from({ length: N }, (_, i) => !!v?.[i]);
|
|
510
|
+
const identity = () => Array.from({ length: N }, (_, s) => s);
|
|
511
|
+
const isIdentity = (o) => o.every((v, s) => v === s);
|
|
512
|
+
const validOrder = (o) => Array.isArray(o) && o.length === N && o.every((v) => Number.isInteger(v) && v >= 0 && v < N) && new Set(o).size === N;
|
|
513
|
+
let order = identity();
|
|
514
|
+
const slotOf = identity();
|
|
515
|
+
let lines = L.lines;
|
|
501
516
|
const demoCells = [...cellLines[0], ...cellLines[2 * n].slice(1, -1)];
|
|
502
|
-
const
|
|
503
|
-
const demoStart = () =>
|
|
517
|
+
const demoSlots = L.itemCell.flatMap((c, s) => demoCells.includes(c) ? [s] : []);
|
|
518
|
+
const demoStart = () => {
|
|
519
|
+
const on = normalize(void 0);
|
|
520
|
+
demoSlots.forEach((s) => on[order[s]] = true);
|
|
521
|
+
return on;
|
|
522
|
+
};
|
|
504
523
|
const far = (c) => Math.floor(c / n) + c % n;
|
|
505
524
|
const dScale = Math.min(1, 4 / n);
|
|
506
525
|
const M = { ...ja, ...options.messages };
|
|
@@ -509,7 +528,9 @@ function createPackingBingo(container, options = {}) {
|
|
|
509
528
|
if (options.subtitle != null) M.subtitle = options.subtitle;
|
|
510
529
|
const controlled = !!options.controlled;
|
|
511
530
|
const storageKey = resolveStorageKey(options);
|
|
531
|
+
const orderKey = storageKey && ORDER_PREFIX + storageKey;
|
|
512
532
|
const demo = !!options.demo;
|
|
533
|
+
const vibrate = options.vibrate !== false;
|
|
513
534
|
const calm = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
514
535
|
let dead = false;
|
|
515
536
|
const timers = /* @__PURE__ */ new Set();
|
|
@@ -522,8 +543,8 @@ function createPackingBingo(container, options = {}) {
|
|
|
522
543
|
};
|
|
523
544
|
const wait = (ms) => new Promise((r) => window.setTimeout(r, ms));
|
|
524
545
|
let gen = 0;
|
|
525
|
-
|
|
526
|
-
container.innerHTML = template(L, M, demo);
|
|
546
|
+
ensureStyles();
|
|
547
|
+
container.innerHTML = template(L, M, { demo, shuffle: options.shuffle !== false, vibrate });
|
|
527
548
|
const $ = (r) => {
|
|
528
549
|
const el = container.querySelector(`[data-r="${r}"]`);
|
|
529
550
|
if (!el) throw new Error(`[packing-bingo] internal: missing element "${r}"`);
|
|
@@ -700,6 +721,7 @@ function createPackingBingo(container, options = {}) {
|
|
|
700
721
|
r.addEventListener("animationend", () => r.remove());
|
|
701
722
|
};
|
|
702
723
|
const buzz = (pattern) => {
|
|
724
|
+
if (!vibrate) return;
|
|
703
725
|
const ua = navigator.userActivation;
|
|
704
726
|
if (ua && !ua.hasBeenActive) return;
|
|
705
727
|
try {
|
|
@@ -758,7 +780,12 @@ function createPackingBingo(container, options = {}) {
|
|
|
758
780
|
const ctr = (t) => {
|
|
759
781
|
return { x: t.offsetLeft + t.offsetWidth / 2, y: t.offsetTop + t.offsetHeight / 2 };
|
|
760
782
|
};
|
|
783
|
+
let landing = 0;
|
|
761
784
|
function drawLines(fresh = []) {
|
|
785
|
+
if (landing) {
|
|
786
|
+
svg.innerHTML = "";
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
762
789
|
svg.setAttribute("viewBox", `0 0 ${gw.clientWidth} ${gw.clientHeight}`);
|
|
763
790
|
svg.innerHTML = cur.map((k) => {
|
|
764
791
|
const a = ctr(cellTiles[cellLines[k][0]]), b = ctr(cellTiles[cellLines[k][n - 1]]);
|
|
@@ -773,6 +800,7 @@ function createPackingBingo(container, options = {}) {
|
|
|
773
800
|
combo = 0;
|
|
774
801
|
$("combo").classList.remove("pkb-on");
|
|
775
802
|
sfx.off();
|
|
803
|
+
buzz(8);
|
|
776
804
|
return;
|
|
777
805
|
}
|
|
778
806
|
const now = performance.now();
|
|
@@ -795,7 +823,7 @@ function createPackingBingo(container, options = {}) {
|
|
|
795
823
|
$("cmbN").textContent = M.combo(combo);
|
|
796
824
|
replay($("combo"), "pkb-on");
|
|
797
825
|
}
|
|
798
|
-
buzz(
|
|
826
|
+
buzz(18);
|
|
799
827
|
}
|
|
800
828
|
$("cbar").addEventListener("animationend", () => {
|
|
801
829
|
$("combo").classList.remove("pkb-on");
|
|
@@ -889,10 +917,11 @@ function createPackingBingo(container, options = {}) {
|
|
|
889
917
|
$("combo").classList.remove("pkb-on");
|
|
890
918
|
perfectEl.classList.remove("pkb-show");
|
|
891
919
|
}
|
|
920
|
+
const completeLines = () => lines.flatMap((l, k) => l.every((i) => checked[i]) ? [k] : []);
|
|
892
921
|
function render(src, quiet) {
|
|
893
922
|
const events = [];
|
|
894
923
|
const on = checked, c = on.filter(Boolean).length;
|
|
895
|
-
cur =
|
|
924
|
+
cur = completeLines();
|
|
896
925
|
const fresh = cur.filter((k) => !prevLines.has(k));
|
|
897
926
|
if ([...prevLines].some((k) => !cur.includes(k))) {
|
|
898
927
|
cancelFx();
|
|
@@ -906,7 +935,7 @@ function createPackingBingo(container, options = {}) {
|
|
|
906
935
|
}
|
|
907
936
|
tiles.forEach((t, i) => {
|
|
908
937
|
t.classList.toggle("pkb-on", on[i]);
|
|
909
|
-
t.classList.toggle("pkb-line", hit.has(L.itemCell[i]));
|
|
938
|
+
t.classList.toggle("pkb-line", hit.has(L.itemCell[slotOf[i]]));
|
|
910
939
|
t.classList.toggle("pkb-reach", reach.has(i));
|
|
911
940
|
});
|
|
912
941
|
freeTile?.classList.toggle("pkb-line", hit.has(L.center));
|
|
@@ -924,8 +953,8 @@ function createPackingBingo(container, options = {}) {
|
|
|
924
953
|
lastN = cur.length;
|
|
925
954
|
drawLines(fresh);
|
|
926
955
|
if (src !== null) tapFx(src, on[src]);
|
|
927
|
-
const newL = fresh.
|
|
928
|
-
newL.forEach((
|
|
956
|
+
const newL = fresh.map((k) => lineKey(lines[k])).filter((key) => !gotL.has(key));
|
|
957
|
+
newL.forEach((key) => gotL.add(key));
|
|
929
958
|
const linePts = 100 * newL.length * newL.length;
|
|
930
959
|
score += linePts;
|
|
931
960
|
if (quiet) {
|
|
@@ -970,8 +999,9 @@ function createPackingBingo(container, options = {}) {
|
|
|
970
999
|
got.clear();
|
|
971
1000
|
gotL.clear();
|
|
972
1001
|
checked.forEach((v, i) => v && got.add(i));
|
|
973
|
-
|
|
974
|
-
|
|
1002
|
+
const done = completeLines();
|
|
1003
|
+
done.forEach((k) => gotL.add(lineKey(lines[k])));
|
|
1004
|
+
prevLines = new Set(done);
|
|
975
1005
|
prevReach = /* @__PURE__ */ new Set();
|
|
976
1006
|
prevAll = checked.every(Boolean);
|
|
977
1007
|
gotP = prevAll;
|
|
@@ -994,6 +1024,77 @@ function createPackingBingo(container, options = {}) {
|
|
|
994
1024
|
options.onChange?.(checked.slice());
|
|
995
1025
|
flush(events);
|
|
996
1026
|
}
|
|
1027
|
+
function place(next) {
|
|
1028
|
+
order = next;
|
|
1029
|
+
order.forEach((i, s) => {
|
|
1030
|
+
slotOf[i] = s;
|
|
1031
|
+
cellTiles[L.itemCell[s]] = tiles[i];
|
|
1032
|
+
});
|
|
1033
|
+
lines = L.lines.map((l) => l.map((s) => order[s]));
|
|
1034
|
+
const focused = document.activeElement;
|
|
1035
|
+
cellTiles.forEach((t, c) => {
|
|
1036
|
+
t.style.setProperty("--pkb-gd", glintDelay(c, n));
|
|
1037
|
+
grid.appendChild(t);
|
|
1038
|
+
});
|
|
1039
|
+
if (focused instanceof HTMLElement && grid.contains(focused) && document.activeElement !== focused)
|
|
1040
|
+
focused.focus({ preventScroll: true });
|
|
1041
|
+
}
|
|
1042
|
+
const saveOrder = () => {
|
|
1043
|
+
if (!orderKey) return;
|
|
1044
|
+
writeStore(orderKey, JSON.stringify(order));
|
|
1045
|
+
};
|
|
1046
|
+
let flights = [];
|
|
1047
|
+
function arrange(next) {
|
|
1048
|
+
if (!checked.every(Boolean)) cancelFx();
|
|
1049
|
+
const from = calm ? null : tiles.map((t) => t.getBoundingClientRect());
|
|
1050
|
+
flights.forEach((a) => a.cancel());
|
|
1051
|
+
flights = [];
|
|
1052
|
+
place(next);
|
|
1053
|
+
lines.forEach((l) => {
|
|
1054
|
+
if (l.every((i) => got.has(i))) gotL.add(lineKey(l));
|
|
1055
|
+
});
|
|
1056
|
+
const done = completeLines();
|
|
1057
|
+
done.forEach((k) => gotL.add(lineKey(lines[k])));
|
|
1058
|
+
prevLines = new Set(done);
|
|
1059
|
+
render(null, true);
|
|
1060
|
+
saveOrder();
|
|
1061
|
+
if (!from || typeof Element.prototype.animate !== "function") return;
|
|
1062
|
+
tiles.forEach((t, i) => {
|
|
1063
|
+
const a = from[i], b = t.getBoundingClientRect();
|
|
1064
|
+
const dx = a.left - b.left, dy = a.top - b.top;
|
|
1065
|
+
if (Math.abs(dx) + Math.abs(dy) < 1) return;
|
|
1066
|
+
flights.push(
|
|
1067
|
+
t.animate([{ transform: `translate(${dx}px,${dy}px)` }, { transform: "none" }], {
|
|
1068
|
+
duration: FLY_MS - 140,
|
|
1069
|
+
delay: Math.random() * 140,
|
|
1070
|
+
easing: "cubic-bezier(.3,1.25,.5,1)",
|
|
1071
|
+
fill: "backwards"
|
|
1072
|
+
})
|
|
1073
|
+
);
|
|
1074
|
+
});
|
|
1075
|
+
clearTimeout(landing);
|
|
1076
|
+
landing = window.setTimeout(() => {
|
|
1077
|
+
landing = 0;
|
|
1078
|
+
flights = [];
|
|
1079
|
+
if (!dead) drawLines(cur);
|
|
1080
|
+
}, FLY_MS);
|
|
1081
|
+
svg.innerHTML = "";
|
|
1082
|
+
}
|
|
1083
|
+
function shuffleCard() {
|
|
1084
|
+
let next = order.slice();
|
|
1085
|
+
do {
|
|
1086
|
+
for (let s = N - 1; s > 0; s--) {
|
|
1087
|
+
const r = Math.floor(Math.random() * (s + 1));
|
|
1088
|
+
[next[s], next[r]] = [next[r], next[s]];
|
|
1089
|
+
}
|
|
1090
|
+
} while (next.every((v, s) => v === order[s]));
|
|
1091
|
+
arrange(next);
|
|
1092
|
+
sfx.shuffle();
|
|
1093
|
+
buzz([10, 40, 10, 40, 10, 40, 24]);
|
|
1094
|
+
}
|
|
1095
|
+
const unshuffle = () => {
|
|
1096
|
+
if (!isIdentity(order)) arrange(identity());
|
|
1097
|
+
};
|
|
997
1098
|
grid.addEventListener("change", (e) => {
|
|
998
1099
|
const i = ins.indexOf(e.target);
|
|
999
1100
|
if (i < 0) return;
|
|
@@ -1001,7 +1102,11 @@ function createPackingBingo(container, options = {}) {
|
|
|
1001
1102
|
next[i] = !next[i];
|
|
1002
1103
|
request(next, i);
|
|
1003
1104
|
});
|
|
1004
|
-
|
|
1105
|
+
container.querySelector('[data-r="shuf"]')?.addEventListener("click", shuffleCard);
|
|
1106
|
+
$("reset").addEventListener("click", () => {
|
|
1107
|
+
unshuffle();
|
|
1108
|
+
request(normalize(void 0), null, { reset: true });
|
|
1109
|
+
});
|
|
1005
1110
|
$("close").addEventListener("click", () => perfectEl.classList.remove("pkb-show"));
|
|
1006
1111
|
perfectEl.addEventListener("click", (e) => {
|
|
1007
1112
|
if (e.target === perfectEl) perfectEl.classList.remove("pkb-show");
|
|
@@ -1040,9 +1145,21 @@ function createPackingBingo(container, options = {}) {
|
|
|
1040
1145
|
void run(miss.map((i, k) => k < miss.length - 1 ? { i, delay: 170, quiet: true } : { i, delay: 450 }));
|
|
1041
1146
|
});
|
|
1042
1147
|
$("dR").addEventListener("click", () => {
|
|
1043
|
-
if (
|
|
1148
|
+
if (busy) return;
|
|
1149
|
+
unshuffle();
|
|
1150
|
+
request(demoStart(), null, { reset: true });
|
|
1044
1151
|
});
|
|
1045
1152
|
}
|
|
1153
|
+
let startOrder = null;
|
|
1154
|
+
if (orderKey) {
|
|
1155
|
+
try {
|
|
1156
|
+
const saved = JSON.parse(readStore(orderKey) ?? "null");
|
|
1157
|
+
if (validOrder(saved)) startOrder = saved;
|
|
1158
|
+
} catch {
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
if (!startOrder && validOrder(options.defaultOrder)) startOrder = options.defaultOrder.slice();
|
|
1162
|
+
if (startOrder && !isIdentity(startOrder)) place(startOrder);
|
|
1046
1163
|
let initial = null;
|
|
1047
1164
|
if (storageKey) {
|
|
1048
1165
|
try {
|
|
@@ -1074,19 +1191,35 @@ function createPackingBingo(container, options = {}) {
|
|
|
1074
1191
|
flush(apply(n2, changed[0], false));
|
|
1075
1192
|
},
|
|
1076
1193
|
reset() {
|
|
1077
|
-
if (
|
|
1194
|
+
if (dead) return;
|
|
1195
|
+
unshuffle();
|
|
1196
|
+
request(normalize(void 0), null, { reset: true });
|
|
1197
|
+
},
|
|
1198
|
+
shuffle() {
|
|
1199
|
+
if (!dead) shuffleCard();
|
|
1078
1200
|
},
|
|
1079
1201
|
refresh() {
|
|
1080
1202
|
if (!dead) render(null, true);
|
|
1081
1203
|
},
|
|
1082
1204
|
getState() {
|
|
1083
|
-
return {
|
|
1205
|
+
return {
|
|
1206
|
+
checked: checked.slice(),
|
|
1207
|
+
lines: cur.slice(),
|
|
1208
|
+
score,
|
|
1209
|
+
maxCombo,
|
|
1210
|
+
complete: checked.every(Boolean),
|
|
1211
|
+
size: n,
|
|
1212
|
+
free: L.free,
|
|
1213
|
+
order: order.slice()
|
|
1214
|
+
};
|
|
1084
1215
|
},
|
|
1085
1216
|
destroy() {
|
|
1086
1217
|
if (dead) return;
|
|
1087
1218
|
dead = true;
|
|
1088
1219
|
timers.forEach((id) => clearTimeout(id));
|
|
1089
1220
|
timers.clear();
|
|
1221
|
+
clearTimeout(landing);
|
|
1222
|
+
flights.forEach((a) => a.cancel());
|
|
1090
1223
|
cancelAnimationFrame(raf);
|
|
1091
1224
|
cancelAnimationFrame(tw);
|
|
1092
1225
|
ro?.disconnect();
|
package/dist/core.d.cts
CHANGED
|
@@ -21,6 +21,7 @@ declare function bingoLines(size: number, free?: boolean): number[][];
|
|
|
21
21
|
/** Every piece of UI text. Pass a partial object as `messages` to override some of them. */
|
|
22
22
|
interface Messages {
|
|
23
23
|
title: string;
|
|
24
|
+
/** A label above the title; empty (not shown) by default. */
|
|
24
25
|
tag: string;
|
|
25
26
|
/** A line under the title; empty (not shown) by default. */
|
|
26
27
|
subtitle: string;
|
|
@@ -30,6 +31,8 @@ interface Messages {
|
|
|
30
31
|
combo: (n: number) => string;
|
|
31
32
|
comboPop: (n: number) => string;
|
|
32
33
|
reset: string;
|
|
34
|
+
/** The button that moves the items to random squares. */
|
|
35
|
+
shuffle: string;
|
|
33
36
|
soundOn: string;
|
|
34
37
|
soundOff: string;
|
|
35
38
|
stamp: string;
|
|
@@ -45,7 +48,7 @@ interface Messages {
|
|
|
45
48
|
/** The status line before the first line is complete; empty by default. */
|
|
46
49
|
statusStart: (left: number) => string;
|
|
47
50
|
statusLines: (lines: number, left: number) => string;
|
|
48
|
-
/** `labels` are the items that would each complete a line right now. */
|
|
51
|
+
/** `labels` are the items that would each complete a line right now; empty by default. */
|
|
49
52
|
statusReach: (labels: string[]) => string;
|
|
50
53
|
statusDone: string;
|
|
51
54
|
perfectTag: string;
|
|
@@ -86,6 +89,11 @@ interface PackingBingoState {
|
|
|
86
89
|
size: number;
|
|
87
90
|
/** Whether the center is a FREE square. */
|
|
88
91
|
free: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* The item in each slot (row by row, skipping the FREE square): `[0, 1, 2, …]` until the card
|
|
94
|
+
* is shuffled. Line `k` holds the items `bingoLines(size, free)[k].map((s) => order[s])`.
|
|
95
|
+
*/
|
|
96
|
+
order: number[];
|
|
89
97
|
}
|
|
90
98
|
interface BingoEvent {
|
|
91
99
|
/** Lines (indices into `bingoLines(size, free)`, which is LINES for 4×4) completed by this change. */
|
|
@@ -133,7 +141,27 @@ interface PackingBingoOptions {
|
|
|
133
141
|
storageKey?: string | null;
|
|
134
142
|
/** Start with sound on (default) or off. A choice made with the sound button is remembered. */
|
|
135
143
|
sound?: boolean;
|
|
136
|
-
/**
|
|
144
|
+
/**
|
|
145
|
+
* Show the 🔀 button that moves the items to random squares (default true). Checks stay with
|
|
146
|
+
* their items. The arrangement is saved next to the checks, under "packing-bingo-order:" +
|
|
147
|
+
* the storage key.
|
|
148
|
+
*/
|
|
149
|
+
shuffle?: boolean;
|
|
150
|
+
/**
|
|
151
|
+
* The starting arrangement, as in `getState().order`, used when no shuffled arrangement is
|
|
152
|
+
* saved. Ignored unless it lists every item index exactly once.
|
|
153
|
+
*/
|
|
154
|
+
defaultOrder?: readonly number[];
|
|
155
|
+
/**
|
|
156
|
+
* Buzz on phones (default true): a short buzz on each tap and longer patterns for a bingo, a
|
|
157
|
+
* perfect card and a shuffle where the Vibration API exists (Android), and the system tap
|
|
158
|
+
* haptic on iPhone (iOS 18 and later), which only follows a real tap.
|
|
159
|
+
*/
|
|
160
|
+
vibrate?: boolean;
|
|
161
|
+
/**
|
|
162
|
+
* @deprecated Ignored since 0.4.0: the card no longer loads fonts. To get Dela Gothic One and
|
|
163
|
+
* M PLUS Rounded 1c, load them on the page (see the README's フォント section).
|
|
164
|
+
*/
|
|
137
165
|
fonts?: boolean;
|
|
138
166
|
/** Pre-fill a few items and show demo buttons. */
|
|
139
167
|
demo?: boolean;
|
|
@@ -149,8 +177,13 @@ interface PackingBingoInstance {
|
|
|
149
177
|
setChecked(next: readonly boolean[], options?: {
|
|
150
178
|
animate?: boolean;
|
|
151
179
|
}): void;
|
|
152
|
-
/** Clear the card and score (calls onChange). */
|
|
180
|
+
/** Clear the card and score (calls onChange), and put the items back in their own squares. */
|
|
153
181
|
reset(): void;
|
|
182
|
+
/**
|
|
183
|
+
* Move the items to random squares, as the 🔀 button does. Checks stay with their items; lines
|
|
184
|
+
* the new arrangement happens to complete count as done but earn no points.
|
|
185
|
+
*/
|
|
186
|
+
shuffle(): void;
|
|
154
187
|
getState(): PackingBingoState;
|
|
155
188
|
/** Redraw the status text, e.g. after functions in `messages` start returning something else. */
|
|
156
189
|
refresh(): void;
|
package/dist/core.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ declare function bingoLines(size: number, free?: boolean): number[][];
|
|
|
21
21
|
/** Every piece of UI text. Pass a partial object as `messages` to override some of them. */
|
|
22
22
|
interface Messages {
|
|
23
23
|
title: string;
|
|
24
|
+
/** A label above the title; empty (not shown) by default. */
|
|
24
25
|
tag: string;
|
|
25
26
|
/** A line under the title; empty (not shown) by default. */
|
|
26
27
|
subtitle: string;
|
|
@@ -30,6 +31,8 @@ interface Messages {
|
|
|
30
31
|
combo: (n: number) => string;
|
|
31
32
|
comboPop: (n: number) => string;
|
|
32
33
|
reset: string;
|
|
34
|
+
/** The button that moves the items to random squares. */
|
|
35
|
+
shuffle: string;
|
|
33
36
|
soundOn: string;
|
|
34
37
|
soundOff: string;
|
|
35
38
|
stamp: string;
|
|
@@ -45,7 +48,7 @@ interface Messages {
|
|
|
45
48
|
/** The status line before the first line is complete; empty by default. */
|
|
46
49
|
statusStart: (left: number) => string;
|
|
47
50
|
statusLines: (lines: number, left: number) => string;
|
|
48
|
-
/** `labels` are the items that would each complete a line right now. */
|
|
51
|
+
/** `labels` are the items that would each complete a line right now; empty by default. */
|
|
49
52
|
statusReach: (labels: string[]) => string;
|
|
50
53
|
statusDone: string;
|
|
51
54
|
perfectTag: string;
|
|
@@ -86,6 +89,11 @@ interface PackingBingoState {
|
|
|
86
89
|
size: number;
|
|
87
90
|
/** Whether the center is a FREE square. */
|
|
88
91
|
free: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* The item in each slot (row by row, skipping the FREE square): `[0, 1, 2, …]` until the card
|
|
94
|
+
* is shuffled. Line `k` holds the items `bingoLines(size, free)[k].map((s) => order[s])`.
|
|
95
|
+
*/
|
|
96
|
+
order: number[];
|
|
89
97
|
}
|
|
90
98
|
interface BingoEvent {
|
|
91
99
|
/** Lines (indices into `bingoLines(size, free)`, which is LINES for 4×4) completed by this change. */
|
|
@@ -133,7 +141,27 @@ interface PackingBingoOptions {
|
|
|
133
141
|
storageKey?: string | null;
|
|
134
142
|
/** Start with sound on (default) or off. A choice made with the sound button is remembered. */
|
|
135
143
|
sound?: boolean;
|
|
136
|
-
/**
|
|
144
|
+
/**
|
|
145
|
+
* Show the 🔀 button that moves the items to random squares (default true). Checks stay with
|
|
146
|
+
* their items. The arrangement is saved next to the checks, under "packing-bingo-order:" +
|
|
147
|
+
* the storage key.
|
|
148
|
+
*/
|
|
149
|
+
shuffle?: boolean;
|
|
150
|
+
/**
|
|
151
|
+
* The starting arrangement, as in `getState().order`, used when no shuffled arrangement is
|
|
152
|
+
* saved. Ignored unless it lists every item index exactly once.
|
|
153
|
+
*/
|
|
154
|
+
defaultOrder?: readonly number[];
|
|
155
|
+
/**
|
|
156
|
+
* Buzz on phones (default true): a short buzz on each tap and longer patterns for a bingo, a
|
|
157
|
+
* perfect card and a shuffle where the Vibration API exists (Android), and the system tap
|
|
158
|
+
* haptic on iPhone (iOS 18 and later), which only follows a real tap.
|
|
159
|
+
*/
|
|
160
|
+
vibrate?: boolean;
|
|
161
|
+
/**
|
|
162
|
+
* @deprecated Ignored since 0.4.0: the card no longer loads fonts. To get Dela Gothic One and
|
|
163
|
+
* M PLUS Rounded 1c, load them on the page (see the README's フォント section).
|
|
164
|
+
*/
|
|
137
165
|
fonts?: boolean;
|
|
138
166
|
/** Pre-fill a few items and show demo buttons. */
|
|
139
167
|
demo?: boolean;
|
|
@@ -149,8 +177,13 @@ interface PackingBingoInstance {
|
|
|
149
177
|
setChecked(next: readonly boolean[], options?: {
|
|
150
178
|
animate?: boolean;
|
|
151
179
|
}): void;
|
|
152
|
-
/** Clear the card and score (calls onChange). */
|
|
180
|
+
/** Clear the card and score (calls onChange), and put the items back in their own squares. */
|
|
153
181
|
reset(): void;
|
|
182
|
+
/**
|
|
183
|
+
* Move the items to random squares, as the 🔀 button does. Checks stay with their items; lines
|
|
184
|
+
* the new arrangement happens to complete count as done but earn no points.
|
|
185
|
+
*/
|
|
186
|
+
shuffle(): void;
|
|
154
187
|
getState(): PackingBingoState;
|
|
155
188
|
/** Redraw the status text, e.g. after functions in `messages` start returning something else. */
|
|
156
189
|
refresh(): void;
|