packing-bingo 0.1.0 → 0.2.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 +379 -96
- package/dist/{chunk-RJDKWHWG.js → chunk-WGZKPODU.js} +23 -4
- package/dist/core.cjs +22 -4
- package/dist/core.d.cts +6 -2
- package/dist/core.d.ts +6 -2
- package/dist/core.js +1 -1
- package/dist/index.cjs +26 -5
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +7 -3
- package/dist/packing-bingo.global.js +7 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,181 +1,464 @@
|
|
|
1
|
-
# Packing Bingo
|
|
1
|
+
# 持ちものビンゴ(Packing Bingo)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**修学旅行の持ち物チェックリストを、4×4のビンゴにするReactコンポーネントです。持ち物をかばんに入れるたびにマスをタップしてスタンプを押し、タテ・ヨコ・ナナメがそろうと、コンボ・ファンファーレ・花火で盛り上がります。進み具合は各自のブラウザに自動で保存されます。**
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/packing-bingo)
|
|
6
6
|
[](LICENSE)
|
|
7
|
-
[](
|
|
8
|
-
[](
|
|
9
|
-
[](
|
|
7
|
+
[](#概要)
|
|
8
|
+
[](#api)
|
|
9
|
+
[](#単体のページ)
|
|
10
10
|
|
|
11
|
-
[
|
|
11
|
+
[クイックスタート](#クイックスタート) · [使い方の例](#使い方の例) · [API](#api) · [組み込むときの注意](#組み込むときの注意) · [Reactなしで使う](#reactなしで使う) · [English](#english) · [変更履歴](CHANGELOG.md)
|
|
12
12
|
|
|
13
13
|
<p align="center">
|
|
14
|
-
<a href="docs/images/bingo-desktop.png"><img src="docs/images/bingo-desktop.png" alt="
|
|
14
|
+
<a href="docs/images/bingo-desktop.png"><img src="docs/images/bingo-desktop.png" alt="持ち物が並んだ4×4のビンゴカード。ナナメの列がそろった瞬間で、大きなBINGO!の文字、+100の得点、紙吹雪が出ている。上の欄には8/16、2ライン、2コンボと表示されている。" width="560"></a>
|
|
15
15
|
</p>
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
_2コンボで2列目がそろったところ。列に線が引かれ、BINGO!と+100が出て、紙吹雪が舞います。_
|
|
18
18
|
|
|
19
19
|
<p>
|
|
20
|
-
<a href="docs/images/phone-reach.png"><img src="docs/images/phone-reach.png" alt="
|
|
21
|
-
<a href="docs/images/phone-combo.png"><img src="docs/images/phone-combo.png" alt="
|
|
22
|
-
<a href="docs/images/phone-perfect.png"><img src="docs/images/phone-perfect.png" alt="
|
|
20
|
+
<a href="docs/images/phone-reach.png"><img src="docs/images/phone-reach.png" alt="スマホの画面。1列そろっていて、次の列を完成させる最後のマスが点線で囲まれ、リーチの札が付いて点滅している。" height="320"></a>
|
|
21
|
+
<a href="docs/images/phone-combo.png"><img src="docs/images/phone-combo.png" alt="スマホの画面。3回続けてタップしたところで、+20と+30、2 COMBO!と3 COMBO!の表示、減っていくコンボのゲージ、2つのリーチが見える。" height="320"></a>
|
|
22
|
+
<a href="docs/images/phone-perfect.png"><img src="docs/images/phone-perfect.png" alt="スマホの画面。16個すべてにスタンプを押したあと。準備完了のスタンプと、得点3,410、最大コンボ×10、10ラインのパーフェクト画面に、紙吹雪と持ち物の絵文字が降っている。" height="320"></a>
|
|
23
23
|
</p>
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
_スマホでの表示。左から、1列そろって次の列にリーチがかかったところ、3回続けたコンボ、全部そろったときのパーフェクト画面です。_
|
|
26
26
|
|
|
27
|
-
##
|
|
27
|
+
## できること
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
修学旅行の前の晩の荷造りは面倒です。持ちものビンゴは16個の持ち物をビンゴカードに並べるので、持ち物を1つ入れるたびにスタンプが押せて、何個かごとにお祝いがあります。
|
|
30
30
|
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
- **Sound needs no audio files.** All effects are synthesized with the Web Audio API. The 🔊 toggle mutes them, and the choice is remembered in the browser.
|
|
38
|
-
- **It's considerate by default.** Squares are real checkboxes, so the keyboard works. `prefers-reduced-motion` turns off particles, shaking and bouncing. Android phones vibrate on taps and bingos.
|
|
31
|
+
- **タップするたびに反応があります。** スタンプ、波紋、火花、少しずつ高くなる音。2.5秒以内に続けてタップすると、コンボになります。
|
|
32
|
+
- **次の目標がいつも見えます。** あと1つで列がそろうマスは、リーチの札を付けて点滅し、下の1行にその持ち物の名前が出ます。
|
|
33
|
+
- **列がそろうたびにお祝いします。** タテ・ヨコ・ナナメの10列のどれかがそろうと、BINGO!の文字と紙吹雪が出ます。16個すべてそろうと、花火とスコアカードのフィナーレになります。
|
|
34
|
+
- **得点は、そろえることへのごほうびです。** 付けたり外したりしても増えません。[得点のルール](#得点のルール)を見てください。
|
|
35
|
+
- **進み具合は自動で保存されます。** 各自のブラウザ(localStorage)に保存されるので、ページを閉じても続きから始められます。サーバーには何も送りません。
|
|
36
|
+
- **だれでも使えるように配慮しています。** マスは本物のチェックボックスなので、キーボードで操作できます。`prefers-reduced-motion`(動きを減らす設定)に従います。音は合成した音(音声ファイルなし)で、消すこともできます。
|
|
39
37
|
|
|
40
|
-
##
|
|
38
|
+
## 概要
|
|
41
39
|
|
|
42
|
-
|
|
40
|
+
| 項目 | 内容 |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| インストール | `npm install packing-bingo`。ESMとCommonJSの両方、TypeScriptの型付き |
|
|
43
|
+
| 必要なもの | React 18以上と `react-dom`。ほかに依存するパッケージはありません。CSSの読み込みもProviderも不要です |
|
|
44
|
+
| サイズ | 約13 kB(minify・gzip後、CSS込み、Reactは含まず) |
|
|
45
|
+
| サーバー側での表示 | `"use client"` 付きで配布しています。サーバー側では空の `<div>` を出し、ブラウザでカードを作ります |
|
|
46
|
+
| 見た目 | `<head>` に `<style>` を1つ追加します。クラス名はすべて `pkb-` で始まります。CSS変数で配色を変えられ、置いた場所の幅に合わせて自動で大きさが変わります |
|
|
47
|
+
| 通信 | Google Fontsからフォントを2つ読み込みます(`fonts={false}` で止められます) |
|
|
48
|
+
| 保存 | localStorageだけ。進み具合と、音のオン・オフの設定 |
|
|
49
|
+
| ブラウザ | Chrome・Edge 105以上、Safari 16以上、Firefox 121以上。今のところChromiumでしか試していません([詳しく](#対応ブラウザと確認済みの範囲)) |
|
|
50
|
+
|
|
51
|
+
## クイックスタート
|
|
52
|
+
|
|
53
|
+
コピーして貼り付けるだけで、サイトの好きな場所に置けます。進み具合は各自のブラウザに自動で保存されるので、保存の設定も要りません。
|
|
54
|
+
|
|
55
|
+
### 1. インストールする
|
|
43
56
|
|
|
44
57
|
```sh
|
|
45
|
-
npm install packing-bingo
|
|
58
|
+
npm install packing-bingo
|
|
46
59
|
```
|
|
47
60
|
|
|
48
|
-
|
|
61
|
+
pnpmなら `pnpm add packing-bingo`、yarnなら `yarn add packing-bingo` です。
|
|
62
|
+
|
|
63
|
+
### 2. 部品のファイルを作る
|
|
64
|
+
|
|
65
|
+
`OkinawaPackingBingo.tsx` というファイルを作り、次をそのまま貼り付けます。置き場所はどこでも構いません(例:`components/OkinawaPackingBingo.tsx`)。
|
|
49
66
|
|
|
50
67
|
```tsx
|
|
51
|
-
import { PackingBingo } from "packing-bingo";
|
|
68
|
+
import { PackingBingo, type BingoItem } from "packing-bingo";
|
|
69
|
+
|
|
70
|
+
// 沖縄の修学旅行の持ち物。ちょうど16個、左上から横に4つずつ並びます。
|
|
71
|
+
const items: BingoItem[] = [
|
|
72
|
+
{ emoji: "📖", label: "旅のしおり" },
|
|
73
|
+
{ emoji: "🪪", label: "生徒手帳" },
|
|
74
|
+
{ emoji: "🏥", label: "保険証コピー" },
|
|
75
|
+
{ emoji: "👛", label: "おこづかい" },
|
|
76
|
+
{ emoji: "👕", label: "着替え" },
|
|
77
|
+
{ emoji: "🩱", label: "水着" },
|
|
78
|
+
{ emoji: "🧴", label: "日焼け止め" },
|
|
79
|
+
{ emoji: "🧢", label: "帽子" },
|
|
80
|
+
{ emoji: "🕶️", label: "サングラス" },
|
|
81
|
+
{ emoji: "🩴", label: "ビーチサンダル" },
|
|
82
|
+
{ emoji: "🪥", label: "洗面用具" },
|
|
83
|
+
{ emoji: "🛁", label: "タオル" },
|
|
84
|
+
{ emoji: "💊", label: "常備薬・酔い止め" },
|
|
85
|
+
{ emoji: "☂️", label: "折りたたみ傘" },
|
|
86
|
+
{ emoji: "🛍️", label: "ビニール袋" },
|
|
87
|
+
{ emoji: "📱", label: "スマホ・充電器" },
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
export function OkinawaPackingBingo() {
|
|
91
|
+
return <PackingBingo items={items} tag="沖縄修学旅行" />;
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 3. 表示したい場所に置く
|
|
96
|
+
|
|
97
|
+
表示したいページやコンポーネントで読み込み、置きたい場所に `<OkinawaPackingBingo />` と書きます。`import` のパスは、ファイルを置いた場所に合わせてください。
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
import { OkinawaPackingBingo } from "./components/OkinawaPackingBingo";
|
|
101
|
+
|
|
102
|
+
// JSXの中の、表示したい場所に
|
|
103
|
+
<OkinawaPackingBingo />
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Next.js(App Router)で専用のページを作るなら、`app/packing/page.tsx` を作って次を貼り付けます。`/packing` を開くと表示されます。
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
import { OkinawaPackingBingo } from "@/components/OkinawaPackingBingo";
|
|
52
110
|
|
|
53
111
|
export default function PackingPage() {
|
|
54
|
-
return
|
|
112
|
+
return (
|
|
113
|
+
<main style={{ padding: 16 }}>
|
|
114
|
+
<OkinawaPackingBingo />
|
|
115
|
+
</main>
|
|
116
|
+
);
|
|
55
117
|
}
|
|
56
118
|
```
|
|
57
119
|
|
|
58
|
-
|
|
120
|
+
`@/` は、`create-next-app` で作ったプロジェクトに最初から設定されている、プロジェクトの一番上を指す書き方です。部品のファイルは `components/OkinawaPackingBingo.tsx` に置いてください。`src/` フォルダを使うプロジェクトでは `src/components/OkinawaPackingBingo.tsx` です。`@/` が使えないプロジェクトでは、`../../components/OkinawaPackingBingo` のような相対パスに書き換えてください。
|
|
121
|
+
|
|
122
|
+
カードは置いた場所の幅いっぱい(最大720 px)に広がり、狭い場所ではスマホ向けの配置に自動で切り替わります。持ち物を変えたいときは `items` の中身を書き換えるだけです。`items` を省くと、一般的な修学旅行の持ち物リストになります。
|
|
123
|
+
|
|
124
|
+
Next.jsのServer Componentのページからでも、そのまま使えます。ただし、`onBingo` などの関数(コールバック)はServer Componentから渡せず、「Event handlers cannot be passed to Client Component props」というエラーでビルドが止まります。コールバックを使うときは、部品のファイルの1行目に `"use client";` を書いてください。
|
|
125
|
+
|
|
126
|
+
### Reactを使っていないページに置く場合
|
|
127
|
+
|
|
128
|
+
HTMLの好きな場所に、次をそのまま貼り付けます。インストールは要りません。
|
|
129
|
+
|
|
130
|
+
```html
|
|
131
|
+
<div id="packing-bingo"></div>
|
|
132
|
+
<script src="https://cdn.jsdelivr.net/npm/packing-bingo@0.2/dist/packing-bingo.global.js"></script>
|
|
133
|
+
<script>
|
|
134
|
+
PackingBingo.createPackingBingo(document.getElementById("packing-bingo"), {
|
|
135
|
+
tag: "沖縄修学旅行",
|
|
136
|
+
items: [
|
|
137
|
+
{ emoji: "📖", label: "旅のしおり" }, { emoji: "🪪", label: "生徒手帳" },
|
|
138
|
+
{ emoji: "🏥", label: "保険証コピー" }, { emoji: "👛", label: "おこづかい" },
|
|
139
|
+
{ emoji: "👕", label: "着替え" }, { emoji: "🩱", label: "水着" },
|
|
140
|
+
{ emoji: "🧴", label: "日焼け止め" }, { emoji: "🧢", label: "帽子" },
|
|
141
|
+
{ emoji: "🕶️", label: "サングラス" }, { emoji: "🩴", label: "ビーチサンダル" },
|
|
142
|
+
{ emoji: "🪥", label: "洗面用具" }, { emoji: "🛁", label: "タオル" },
|
|
143
|
+
{ emoji: "💊", label: "常備薬・酔い止め" }, { emoji: "☂️", label: "折りたたみ傘" },
|
|
144
|
+
{ emoji: "🛍️", label: "ビニール袋" }, { emoji: "📱", label: "スマホ・充電器" },
|
|
145
|
+
],
|
|
146
|
+
});
|
|
147
|
+
</script>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## 使い方の例
|
|
59
151
|
|
|
60
|
-
###
|
|
152
|
+
### 持ち物と文言を変える
|
|
61
153
|
|
|
62
154
|
```tsx
|
|
63
155
|
import { PackingBingo, en, type BingoItem } from "packing-bingo";
|
|
64
156
|
|
|
65
157
|
const items: BingoItem[] = [
|
|
66
|
-
{ emoji: "
|
|
67
|
-
{ emoji: "
|
|
68
|
-
// …
|
|
158
|
+
{ emoji: "🩱", label: "Swimsuit" },
|
|
159
|
+
{ emoji: "🧴", label: "Sunscreen" },
|
|
160
|
+
// … ちょうど16個、左上から横に1行ずつ
|
|
69
161
|
];
|
|
70
162
|
|
|
71
|
-
<PackingBingo items={items} messages={en} title="
|
|
163
|
+
<PackingBingo items={items} messages={en} title="Okinawa Trip" tag="Class 2-B" />;
|
|
72
164
|
```
|
|
73
165
|
|
|
74
|
-
`
|
|
166
|
+
- `items` はちょうど16個にしてください。それ以外の数だと `[packing-bingo] "items" must have exactly 16 entries` というエラーになります。
|
|
167
|
+
- `messages` は日本語の既定値の上に重なるので、`messages={{ reset: "やり直す" }}` のように一部の文言だけを変えられます。`en` は英語の文言一式です。キーの一覧は[文言のキー](#文言のキー)にあります。
|
|
168
|
+
- 狭いカードでは、ラベルは `・` の後ろで折り返します。`常備薬・酔い止め` は「常備薬・」と「酔い止め」の2行になります。
|
|
169
|
+
|
|
170
|
+
### 保存のしかたを変える
|
|
75
171
|
|
|
76
|
-
|
|
172
|
+
何も指定しなければ、進み具合は持ち物リストから自動で決まるキーで、各自のブラウザに保存されます。持ち物リストが違うカードは別々に保存されるので、1つのサイトに複数のチェックリストを置いても混ざりません。
|
|
77
173
|
|
|
78
|
-
|
|
174
|
+
- **キーを自分で決める:** `storageKey="okinawa-2026"` のように指定します。持ち物リストから決まるキーは、持ち物を1文字でも直すと変わり、カードは新しく始まります。生徒が使い始めたあとで持ち物を直す可能性があるなら、最初からキーを決めておくと、直したあとも続きから使えます。同じ持ち物のカードを2枚、別々に保存したいときも、それぞれにキーを付けます。
|
|
175
|
+
- **保存しない:** `storageKey={null}` にします。
|
|
176
|
+
|
|
177
|
+
### 自分のサーバーに保存する
|
|
178
|
+
|
|
179
|
+
ブラウザへの保存は、その端末の中だけで使えます。別の端末でも続きから使いたい場合だけ、チェックを自分の state で持って `checked` で渡します(controlledといいます)。このときブラウザには保存しません。
|
|
79
180
|
|
|
80
181
|
```tsx
|
|
81
|
-
|
|
182
|
+
import { useEffect, useRef, useState } from "react";
|
|
183
|
+
import { PackingBingo } from "packing-bingo";
|
|
184
|
+
|
|
185
|
+
export function PackingCard({ studentId }: { studentId: string }) {
|
|
186
|
+
const [checked, setChecked] = useState<boolean[] | null>(null);
|
|
187
|
+
const saving = useRef(Promise.resolve());
|
|
188
|
+
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
let current = true; // 生徒が切り替わったあとに届いた古い応答は使わない
|
|
191
|
+
setChecked(null);
|
|
192
|
+
fetch(`/api/packing/${studentId}`)
|
|
193
|
+
.then((res) => res.json())
|
|
194
|
+
.then((data) => {
|
|
195
|
+
if (current) setChecked(data.checked ?? Array(16).fill(false));
|
|
196
|
+
});
|
|
197
|
+
return () => {
|
|
198
|
+
current = false;
|
|
199
|
+
};
|
|
200
|
+
}, [studentId]);
|
|
201
|
+
|
|
202
|
+
if (!checked) return null; // または同じ大きさの仮表示
|
|
203
|
+
|
|
204
|
+
const save = (next: boolean[]) => {
|
|
205
|
+
setChecked(next);
|
|
206
|
+
// 保存は1つずつ順に送る。遅れた古い保存が、新しい保存を上書きしないように。
|
|
207
|
+
saving.current = saving.current
|
|
208
|
+
.then(() => fetch(`/api/packing/${studentId}`, { method: "PUT", body: JSON.stringify({ checked: next }) }))
|
|
209
|
+
.then(() => undefined, () => undefined);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
return <PackingBingo checked={checked} onChange={save} />;
|
|
213
|
+
}
|
|
214
|
+
```
|
|
82
215
|
|
|
216
|
+
生徒のタップは通信より速いので、この例では保存を順番に送り、`studentId` が変わったあとに届いた読み込みは捨てています。保存のたびに16個すべてを送るので、最後の保存がいつも最新の状態になります。
|
|
217
|
+
|
|
218
|
+
controlledのカードの動き:
|
|
219
|
+
|
|
220
|
+
- タップしただけでは、カードは変わりません。ユーザーが求めた16個の状態で `onChange` が呼ばれ、`checked` が変わるとカードがそれに合わせます。`checked` を変えなければ、タップは取り消されます。
|
|
221
|
+
- `checked` がちょうど1個だけ変わったときは、タップと同じように演出します(スタンプ、音、ビンゴやフィナーレ)。サーバーからデータが届いたときのように一度に何個も変わったときは、演出なしで静かに反映し、同時にそろった列のボーナスも付けません。
|
|
222
|
+
- リセットボタンを押すと、16個すべて `false` で `onChange` が呼ばれます。
|
|
223
|
+
- `storageKey` は使われません。
|
|
224
|
+
|
|
225
|
+
### ビンゴの瞬間を受け取る
|
|
226
|
+
|
|
227
|
+
```tsx
|
|
83
228
|
<PackingBingo
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
/>;
|
|
229
|
+
items={items}
|
|
230
|
+
onBingo={({ newLines, totalLines, score }) => analytics.track("bingo", { totalLines, score })}
|
|
231
|
+
onPerfect={({ score, maxCombo }) => analytics.track("perfect", { score, maxCombo })}
|
|
232
|
+
/>
|
|
89
233
|
```
|
|
90
234
|
|
|
91
|
-
|
|
235
|
+
- `onBingo` は、タップで1列以上そろったときに呼ばれます。カード全体がそろったタップでは、代わりに `onPerfect` が呼ばれます。
|
|
236
|
+
- `onChange` は、ユーザーの操作(タップ、リセットボタン、`ref.reset()`)のときだけ呼ばれます。`checked` や `ref.setChecked()` で変えたときは呼ばれません。
|
|
237
|
+
- `onBingo` と `onPerfect` は、タップと同じように演出した変化のときに呼ばれます。`checked` が1個だけ変わったときも含みます。静かに反映した変化や、最初の表示では呼ばれません。
|
|
238
|
+
- 順番は、まず `onChange`、次に `onBingo` か `onPerfect` です。どれもカードの表示が変わったあとに呼ばれるので、中でカードをリセットしたり、画面から外したりしても大丈夫です。
|
|
92
239
|
|
|
93
|
-
###
|
|
240
|
+
### 外から操作する
|
|
94
241
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
242
|
+
```tsx
|
|
243
|
+
import { useRef } from "react";
|
|
244
|
+
import { PackingBingo, type PackingBingoHandle } from "packing-bingo";
|
|
245
|
+
|
|
246
|
+
function PackingPage() {
|
|
247
|
+
const card = useRef<PackingBingoHandle>(null);
|
|
248
|
+
return (
|
|
249
|
+
<>
|
|
250
|
+
<button onClick={() => card.current?.reset()}>最初からやり直す</button>
|
|
251
|
+
<PackingBingo ref={card} />
|
|
252
|
+
</>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
`reset()`・`setChecked()`・`getState()` が使えます。くわしくは [ref](#ref) を見てください。controlledのカードでは、`setChecked()` ではなく `checked` を変えてください。
|
|
258
|
+
|
|
259
|
+
### 配色とレイアウト
|
|
260
|
+
|
|
261
|
+
次のCSS変数を、カードかその親要素に指定します。
|
|
262
|
+
|
|
263
|
+
| 変数 | 使われる場所 | 既定値 |
|
|
264
|
+
| --- | --- | --- |
|
|
265
|
+
| `--pkb-sun` | カードの背景 | `#FFD54F` |
|
|
266
|
+
| `--pkb-ink` | 線、枠、文字 | `#2A1F14` |
|
|
267
|
+
| `--pkb-paper` | マス | `#FFFDF6` |
|
|
268
|
+
| `--pkb-red` | スタンプとアクセント | `#E8473B` |
|
|
269
|
+
| `--pkb-mint` | そろった列 | `#BDEBD2` |
|
|
270
|
+
| `--pkb-font-fallback` | 見出しなどのフォントが使えないときのフォント | `sans-serif` |
|
|
116
271
|
|
|
117
272
|
```tsx
|
|
118
273
|
<PackingBingo style={{ "--pkb-sun": "#9AD0F5" } as React.CSSProperties} />
|
|
119
274
|
```
|
|
120
275
|
|
|
121
|
-
|
|
276
|
+
カードは置いた場所の幅いっぱい(最大720 px)に広がり、中央に寄ります。置いた場所の幅が560 pxより狭いと、スマホ向けの配置に切り替わります。画面の幅ではなく置いた場所の幅で決まるので、広い画面のサイドバーに置いてもスマホ向けの配置になります。高さは幅から自動で決まります。サーバー側では空の `<div>` を出すので、カードはhydrationのあとに現れます。レイアウトのずれが気になるときは、外側の要素に `min-height` を付けてください。
|
|
277
|
+
|
|
278
|
+
## API
|
|
279
|
+
|
|
280
|
+
### props
|
|
281
|
+
|
|
282
|
+
| prop | 型 | 既定値 | 内容 |
|
|
283
|
+
| --- | --- | --- | --- |
|
|
284
|
+
| `items` | `readonly BingoItem[]`(ちょうど16個) | `DEFAULT_ITEMS`(一般的な修学旅行の持ち物) | マスの中身。左上から横に1行ずつ:`{ emoji, label }` |
|
|
285
|
+
| `messages` | `Partial<Messages>` | `ja` | 画面の文言。日本語の既定値の上に重なります。英語の `en` も入っています |
|
|
286
|
+
| `title`・`tag`・`subtitle` | `string` | `messages` の値 | 見出し、その上の小さなラベル、その下の1行 |
|
|
287
|
+
| `storageKey` | `string \| null` | 持ち物から自動で決まる | 進み具合を保存するlocalStorageのキー。`null` で保存しません。controlledのときは使われません |
|
|
288
|
+
| `defaultChecked` | `readonly boolean[]`(16個) | すべて `false` | uncontrolledのカードの最初の状態。保存された進み具合があれば、そちらが優先です |
|
|
289
|
+
| `checked` | `readonly boolean[]`(16個) | なし | 指定するとcontrolledになります。[自分のサーバーに保存する](#自分のサーバーに保存する)を見てください |
|
|
290
|
+
| `onChange` | `(checked: boolean[]) => void` | なし | ユーザーがタップかリセットをした。controlledのときは、求められた状態 |
|
|
291
|
+
| `onBingo` | `(event: BingoEvent) => void` | なし | タップで1列以上そろった |
|
|
292
|
+
| `onPerfect` | `(event: PerfectEvent) => void` | なし | タップでカード全体がそろった |
|
|
293
|
+
| `sound` | `boolean` | `true` | 音の最初の設定。ユーザーが🔊ボタンで選ぶまで使われます |
|
|
294
|
+
| `fonts` | `boolean` | `true` | Dela Gothic OneとM PLUS Rounded 1cをGoogle Fontsから読み込む |
|
|
295
|
+
| `demo` | `boolean` | `false` | デモ用のボタンを出します。保存された進み具合も `defaultChecked` もなければ、6マス埋まった状態から始めます。`storageKey` を指定しない限り保存しません |
|
|
296
|
+
| `className`・`style` | | なし | 外側の `<div>` に付きます |
|
|
297
|
+
| `ref` | `Ref<PackingBingoHandle>` | なし | [ref](#ref) を見てください |
|
|
298
|
+
|
|
299
|
+
propsは中身で比べるので、配列やオブジェクトをその場で書いても構いません。`items`・`title`・`tag`・`subtitle`・`storageKey`・`sound`・`fonts`・`demo`、または `messages` の文字列が変わると、カードを作り直します。保存先のキーが変わらなければ、チェックはそのまま残ります。キーが変わったとき(`items` を変えて自動のキーが変わったときも含む)は、新しいキーに保存された進み具合を読み込みます。得点は[得点のルール](#得点のルール)のとおり計算し直します。`messages` の関数はpropsやstateを使っても構いません。`messages` が変わると、作り直さずに下の1行だけを描き直します。
|
|
300
|
+
|
|
301
|
+
### イベント
|
|
302
|
+
|
|
303
|
+
```ts
|
|
304
|
+
interface BingoEvent {
|
|
305
|
+
newLines: number[]; // このタップでそろった列(LINESの番号)
|
|
306
|
+
totalLines: number; // これまでにそろった列の数
|
|
307
|
+
score: number; // このタップのあとの得点
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
interface PerfectEvent {
|
|
311
|
+
score: number; // 最終得点。初めて全部そろえたときは1000点のボーナスを含む
|
|
312
|
+
maxCombo: number;
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### ref
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
interface PackingBingoHandle {
|
|
320
|
+
/** リセットボタンと同じ。カードと得点を消して onChange を呼ぶ。controlled のカードでは onChange を呼ぶだけ。 */
|
|
321
|
+
reset(): void;
|
|
322
|
+
/** 1個だけの変化はタップと同じように演出し、それより大きな変化は静かに反映する。onChange は呼ばない。 */
|
|
323
|
+
setChecked(checked: readonly boolean[]): void;
|
|
324
|
+
/** 表示前とサーバー側では null。 */
|
|
325
|
+
getState(): PackingBingoState | null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
interface PackingBingoState {
|
|
329
|
+
checked: boolean[]; // 16個、左上から横に1行ずつ
|
|
330
|
+
lines: number[]; // そろった列(LINESの番号)
|
|
331
|
+
score: number;
|
|
332
|
+
maxCombo: number;
|
|
333
|
+
complete: boolean; // 16個すべてにチェックが付いている
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
`LINES` もexportしています。0〜3がヨコの列、4〜7がタテの列、8が左上からのナナメ、9が右上からのナナメです。
|
|
338
|
+
|
|
339
|
+
### 得点のルール
|
|
340
|
+
|
|
341
|
+
- 初めてチェックした持ち物は、10点 × そのときのコンボ数。コンボは、2.5秒以内に続けたタップの数です。
|
|
342
|
+
- 1回のタップでn列そろうと、100点 × n²。同じ列で得点するのは1回だけです。
|
|
343
|
+
- 初めてカード全体をそろえると、1000点を足します。
|
|
344
|
+
- チェックを外しても得点は減らず、付け直しても増えません。
|
|
345
|
+
- 静かに反映したあとや作り直したあと(保存された進み具合を読み込んだときなど)は、チェックした持ち物1個につき10点、そろった列1つにつき100点で計算し直し、最大コンボは0からになります。
|
|
346
|
+
|
|
347
|
+
### 文言のキー
|
|
348
|
+
|
|
349
|
+
`messages` には、次のキーを好きなだけ指定できます。日本語と英語の一式は、[`src/messages.ts`](src/messages.ts) の `ja` と `en` です。
|
|
350
|
+
|
|
351
|
+
- **文字列:** `title`、`tag`、`subtitle`、`packed`、`bingo`、`score`、`reset`、`soundOn`、`soundOff`、`stamp`、`reachBadge`、`reachPop`、`bannerSingle`、`bannerDouble`、`bannerTriple`、`statusDone`、`perfectTag`、`perfectStamp`、`perfectTitle`、`perfectBody`、`statScore`、`statCombo`、`statLines`、`close`、`demoLabel`、`demoBingo`、`demoPerfect`、`demoRestore`。`perfectStamp` と `perfectBody` では、`\n` で改行します。
|
|
352
|
+
- **関数(どれも文字列を返します):** `combo(n)`、`comboPop(n)`、`bannerNth(totalLines)`、`bannerMulti(newLines)`、`statusStart(left)`、`statusLines(lines, left)`、`statusReach(labels)`、`perfectBonus(points)`。
|
|
353
|
+
|
|
354
|
+
### export
|
|
355
|
+
|
|
356
|
+
- `packing-bingo`:`PackingBingo`、`createPackingBingo`、`LINES`、`DEFAULT_ITEMS`、`ja`、`en`。型は `PackingBingoProps`、`PackingBingoHandle`、`PackingBingoState`、`BingoItem`、`BingoEvent`、`PerfectEvent`、`Messages`、`PackingBingoOptions`、`PackingBingoInstance`。
|
|
357
|
+
- `packing-bingo/core`:Reactのコンポーネントとその型(`PackingBingoProps`、`PackingBingoHandle`)を除いた残り。Reactを読み込みません。
|
|
358
|
+
|
|
359
|
+
## 組み込むときの注意
|
|
360
|
+
|
|
361
|
+
### ページに追加されるもの
|
|
362
|
+
|
|
363
|
+
最初に表示したとき、`<head>` に `<style id="pkb-styles">` を追加します。これはすべてのカードで共有し、カードを外したあとも残ります。`fonts={false}` でなければ、Google Fonts用の `<link id="pkb-fonts">` も追加します。CSSはすべて `.pkb` の中だけに効き、クラス名とアニメーション名はすべて `pkb-` で始まるので、ページのほかの部分には影響しません。
|
|
364
|
+
|
|
365
|
+
カードが描くもの(紙吹雪、BINGO!の文字、フィナーレの画面)は、すべてカード自身の枠の中に収まります。`position: fixed` を使ったり `<body>` に要素を足したりはしません。カードは独立した重なり順(stacking context)を持つので、上部に固定したヘッダーの上に描かれることもありません。
|
|
122
366
|
|
|
123
|
-
###
|
|
367
|
+
### CSP(Content Security Policy)
|
|
124
368
|
|
|
125
|
-
|
|
369
|
+
追加する `<style>` と一部の `style` 属性のために、`style-src 'unsafe-inline'` が必要です。フォントを読み込む場合は、`style-src` に `https://fonts.googleapis.com`、`font-src` に `https://fonts.gstatic.com` も許可してください。
|
|
370
|
+
|
|
371
|
+
### フォント
|
|
372
|
+
|
|
373
|
+
Dela Gothic OneとM PLUS Rounded 1cを使います。`fonts={false}` にするとGoogleには何も取りに行きません。サイトで同じ名前のフォントを読み込んでいれば(自分のサーバーに置いている場合など)それを使い、なければ `--pkb-font-fallback` のフォントを使います。
|
|
374
|
+
|
|
375
|
+
### 音と振動
|
|
376
|
+
|
|
377
|
+
音はWeb Audio APIで合成しています。音を鳴らす仕組みは、ユーザーがページを操作したあとにだけ作るので、ブラウザの自動再生の警告は出ません。🔊ボタンの選択は、localStorageの `packing-bingo-muted` に保存されます。そのあとに作られるカードは、ページを読み込み直したあとも含めて、この設定で始まります。すでに表示中のほかのカードは、それぞれの設定のままです。タップやビンゴのときは、Vibration APIがある端末で振動します。実際にはAndroidのブラウザです。
|
|
378
|
+
|
|
379
|
+
### 保存されるデータ
|
|
380
|
+
|
|
381
|
+
チェックは、16個の真偽値のJSON配列としてlocalStorageに保存されます。キーは `storageKey`、指定しなければ `packing-bingo:` で始まる、持ち物リストから決まるキーです。得点は保存しません。保存された進み具合は `defaultChecked` より優先されます。プライベートブラウズなどで保存できない環境では、保存せずにそのまま動きます。
|
|
382
|
+
|
|
383
|
+
### アクセシビリティ
|
|
384
|
+
|
|
385
|
+
マスは `<label>` の中の本物のチェックボックスなので、TabキーとSpaceキーで操作でき、持ち物の名前がそのまま読み上げられます。絵文字とスタンプは、読み上げの対象から外しています。下の1行は、変化を控えめに読み上げる領域(polite live region)です。`prefers-reduced-motion` のときは、揺れ、弾み、紙吹雪を止めます。スクリーンリーダーでの確認は、まだしていません。
|
|
386
|
+
|
|
387
|
+
### 対応ブラウザと確認済みの範囲
|
|
388
|
+
|
|
389
|
+
CSSのコンテナクエリ(Chrome・Edge 105以上、Safari 16以上、Firefox 110以上)と、キーボード操作時の枠に `:has()`(Firefox 121以上)を使っています。
|
|
390
|
+
|
|
391
|
+
確認済み:
|
|
392
|
+
|
|
393
|
+
- React 19のStrictModeで、1ページに3枚、controlledとuncontrolledの両方
|
|
394
|
+
- `renderToString` でのサーバー側の表示(ESMとCommonJS)
|
|
395
|
+
- Next.js 16のApp Routerで本番ビルドし、Server Componentのページとclient componentの両方から使う
|
|
396
|
+
- npmとpnpmでのnpmからのインストール
|
|
397
|
+
- Chromiumで、パソコンとスマホの大きさでの自動タップ。演出の途中でのリセットやコールバックも含む
|
|
398
|
+
- 自動保存。持ち物リストごとに別々に保存されること、`storageKey={null}` で保存しないこと、持ち物を変えると新しく始まり、戻すと前の進み具合が戻ること
|
|
399
|
+
|
|
400
|
+
これらの自動テストは、まだこのリポジトリには入っていません。Safari、Firefox、実際のスマホでは試しておらず、実機での音も確かめていません。絵文字は端末の絵文字フォントで表示されるので、端末ごとに見た目が違います。
|
|
401
|
+
|
|
402
|
+
## Reactなしで使う
|
|
403
|
+
|
|
404
|
+
エンジン部分はフレームワークに依存しません。
|
|
126
405
|
|
|
127
406
|
```ts
|
|
128
407
|
import { createPackingBingo } from "packing-bingo/core";
|
|
129
408
|
|
|
130
|
-
const card = createPackingBingo(document.getElementById("bingo")!, {
|
|
131
|
-
|
|
409
|
+
const card = createPackingBingo(document.getElementById("bingo")!, {
|
|
410
|
+
tag: "沖縄修学旅行",
|
|
411
|
+
onBingo: (e) => console.log(e),
|
|
412
|
+
});
|
|
413
|
+
// card.reset()、card.getState()、card.setChecked(next, { animate: true })、card.destroy()
|
|
132
414
|
```
|
|
133
415
|
|
|
134
|
-
|
|
416
|
+
オプションは、Reactのpropsから `checked`・`className`・`style`・`ref` を除いたものです(型は `PackingBingoOptions`)。controlledにするときは `controlled: true` を渡し、`setChecked()` で状態を変えます。`setChecked()` は、`{ animate: true }` を付けないと、いつも静かに反映します。
|
|
135
417
|
|
|
136
|
-
`
|
|
418
|
+
バンドラーを使わないページでは、CDNからブラウザ用のファイルを読み込みます。グローバル変数 `PackingBingo` ができます。使い方は[クイックスタート](#reactを使っていないページに置く場合)にあります。
|
|
419
|
+
|
|
420
|
+
## 単体のページ
|
|
421
|
+
|
|
422
|
+
`index.html` は、アプリ全体が1つに入ったファイルです。そのまま開くことも、静的なホスティングに置くこともできます。進み具合はブラウザに保存されます。
|
|
137
423
|
|
|
138
424
|
```sh
|
|
139
425
|
git clone https://github.com/Jin-hiroo/packing-bingo.git
|
|
140
426
|
cd packing-bingo
|
|
141
|
-
open index.html # macOS
|
|
427
|
+
open index.html # macOSの場合。ほかのOSでは、ファイルをブラウザで開いてください
|
|
142
428
|
```
|
|
143
429
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
## Customize
|
|
430
|
+
URLの末尾に `?demo` を付けると、途中まで埋まった状態から始まり、デモ用のボタンが出ます。「▶ ビンゴを見る」でビンゴを、「▶ コンボ → パーフェクト」でコンボからパーフェクトまでを再生します。GitHub Pagesで公開するときは、**Settings → Pages → Deploy from a branch → `main` / root** を選びます。
|
|
147
431
|
|
|
148
|
-
|
|
149
|
-
- **Standalone page:** the last `<script>` in `index.html` calls `PackingBingo.createPackingBingo(…)`. Add options there, for example `items: [{ emoji: "🛂", label: "Passport" }, …]`. To change the defaults for every build, edit `src/items.ts` or `src/messages.ts` and run `npm run build`.
|
|
432
|
+
ページの中身を変えるときは、最後の `<script>` にある `PackingBingo.createPackingBingo(…)` のオプションを書き換えます。どのビルドでも使われる既定値を変えるときは、`src/items.ts` か `src/messages.ts` を編集して `npm run build` を実行します。
|
|
150
433
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
## Development
|
|
434
|
+
## 開発
|
|
154
435
|
|
|
155
436
|
```sh
|
|
156
|
-
npm install #
|
|
157
|
-
npm run build # dist
|
|
437
|
+
npm install # dist/ と index.html もビルドされます
|
|
438
|
+
npm run build # dist/(ESM、CJS、型、ブラウザ用)と単体の index.html
|
|
158
439
|
npm run typecheck
|
|
159
440
|
```
|
|
160
441
|
|
|
161
|
-
`index.html`
|
|
442
|
+
`index.html` は `scripts/build-standalone.mjs` が `src/` から作るので、直接編集しないでください。
|
|
443
|
+
|
|
444
|
+
## English
|
|
162
445
|
|
|
163
|
-
|
|
446
|
+
Packing Bingo is a React component that turns a school-trip packing checklist into a 4×4 bingo card, with combos, fanfares and fireworks. Progress is saved in each student's browser automatically. It has no runtime dependencies and is safe to render on the server (it ships with `"use client"`).
|
|
164
447
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
- **Testing so far:** the React component was tested with React 19 in StrictMode, three cards on one page, controlled and uncontrolled, and server rendering with `renderToString` (ESM and CommonJS). Resets and callbacks that land mid-celebration were checked with scripted browser tests, which aren't part of this repository yet. Taps were automated in Chromium at desktop and phone sizes. Safari, Firefox and real phones haven't been tried yet, and the sound hasn't been checked on a real device.
|
|
448
|
+
```sh
|
|
449
|
+
npm install packing-bingo
|
|
450
|
+
```
|
|
169
451
|
|
|
170
|
-
|
|
452
|
+
```tsx
|
|
453
|
+
import { PackingBingo, en } from "packing-bingo";
|
|
171
454
|
|
|
172
|
-
|
|
455
|
+
<PackingBingo messages={en} />;
|
|
456
|
+
```
|
|
173
457
|
|
|
174
|
-
|
|
175
|
-
- **単体のページで使う場合:** `index.html` を開くだけで動き、進み具合はブラウザに保存されます。`?demo` を付けると、途中まで埋まった状態とデモ用のボタンが出ます。
|
|
458
|
+
The default UI text is Japanese; `en` is a complete English set. Pass exactly 16 `items` (`{ emoji, label }`) to use your own list. The sections above cover the props, events and integration notes; the code snippets and type names can be read as they are.
|
|
176
459
|
|
|
177
|
-
##
|
|
460
|
+
## ライセンス
|
|
178
461
|
|
|
179
462
|
[MIT](LICENSE) © 2026 Jin-hiroo.
|
|
180
463
|
|
|
181
|
-
|
|
464
|
+
Dela Gothic OneとM PLUS Rounded 1cのフォントはGoogle Fontsから読み込んでおり、SIL Open Font Licenseで提供されています。このリポジトリには含まれていません。
|
|
@@ -195,10 +195,27 @@ function createSound(isMuted) {
|
|
|
195
195
|
};
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
// src/storage-key.ts
|
|
199
|
+
function resolveStorageKey(options) {
|
|
200
|
+
if (options.controlled) return null;
|
|
201
|
+
if (options.storageKey !== void 0) return options.storageKey || null;
|
|
202
|
+
if (options.demo) return null;
|
|
203
|
+
return itemsKey(options.items ?? DEFAULT_ITEMS);
|
|
204
|
+
}
|
|
205
|
+
function itemsKey(items) {
|
|
206
|
+
const text = items.map((it) => `${it.emoji}\0${it.label}`).join("");
|
|
207
|
+
let h = 2166136261;
|
|
208
|
+
for (let i = 0; i < text.length; i++) {
|
|
209
|
+
h ^= text.charCodeAt(i);
|
|
210
|
+
h = Math.imul(h, 16777619);
|
|
211
|
+
}
|
|
212
|
+
return `packing-bingo:${(h >>> 0).toString(36)}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
198
215
|
// src/styles.ts
|
|
199
216
|
var FONT_URL = "https://fonts.googleapis.com/css2?family=Dela+Gothic+One&family=M+PLUS+Rounded+1c:wght@500;800&display=swap";
|
|
200
217
|
var CSS = `
|
|
201
|
-
.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);container-type:inline-size;width:100%}
|
|
218
|
+
.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);container-type:inline-size;isolation:isolate;width:100%}
|
|
202
219
|
.pkb,.pkb *{box-sizing:border-box;margin:0;padding:0}
|
|
203
220
|
.pkb-board{position:relative;overflow:hidden;max-width:720px;margin:0 auto;padding:22px 26px 20px;border-radius:24px;background-color:var(--pk-sun);background-image:radial-gradient(rgba(42,31,20,.12) 1.3px,transparent 1.6px);background-size:16px 16px;border:2.5px solid var(--pk-ink);color:var(--pk-ink);font-family:var(--pk-body);line-height:1.4;text-align:left}
|
|
204
221
|
.pkb-board.pkb-shake{animation:pkb-shake .45s cubic-bezier(.36,.07,.19,.97)}
|
|
@@ -384,7 +401,7 @@ function createPackingBingo(container, options = {}) {
|
|
|
384
401
|
if (options.tag != null) M.tag = options.tag;
|
|
385
402
|
if (options.subtitle != null) M.subtitle = options.subtitle;
|
|
386
403
|
const controlled = !!options.controlled;
|
|
387
|
-
const storageKey =
|
|
404
|
+
const storageKey = resolveStorageKey(options);
|
|
388
405
|
const demo = !!options.demo;
|
|
389
406
|
const calm = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
390
407
|
let dead = false;
|
|
@@ -915,14 +932,15 @@ function createPackingBingo(container, options = {}) {
|
|
|
915
932
|
if (!busy) request(demoStart(), null, { reset: true });
|
|
916
933
|
});
|
|
917
934
|
}
|
|
918
|
-
let initial =
|
|
919
|
-
if (
|
|
935
|
+
let initial = null;
|
|
936
|
+
if (storageKey) {
|
|
920
937
|
try {
|
|
921
938
|
const saved = JSON.parse(readStore(storageKey) ?? "null");
|
|
922
939
|
if (Array.isArray(saved)) initial = normalize(saved);
|
|
923
940
|
} catch {
|
|
924
941
|
}
|
|
925
942
|
}
|
|
943
|
+
if (!initial && options.defaultChecked) initial = normalize(options.defaultChecked);
|
|
926
944
|
if (!initial && demo) initial = demoStart();
|
|
927
945
|
fit();
|
|
928
946
|
baseline(initial ?? normalize(void 0));
|
|
@@ -971,6 +989,7 @@ export {
|
|
|
971
989
|
DEFAULT_ITEMS,
|
|
972
990
|
ja,
|
|
973
991
|
en,
|
|
992
|
+
resolveStorageKey,
|
|
974
993
|
LINES,
|
|
975
994
|
createPackingBingo
|
|
976
995
|
};
|
package/dist/core.cjs
CHANGED
|
@@ -224,10 +224,27 @@ function createSound(isMuted) {
|
|
|
224
224
|
};
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
// src/storage-key.ts
|
|
228
|
+
function resolveStorageKey(options) {
|
|
229
|
+
if (options.controlled) return null;
|
|
230
|
+
if (options.storageKey !== void 0) return options.storageKey || null;
|
|
231
|
+
if (options.demo) return null;
|
|
232
|
+
return itemsKey(options.items ?? DEFAULT_ITEMS);
|
|
233
|
+
}
|
|
234
|
+
function itemsKey(items) {
|
|
235
|
+
const text = items.map((it) => `${it.emoji}\0${it.label}`).join("");
|
|
236
|
+
let h = 2166136261;
|
|
237
|
+
for (let i = 0; i < text.length; i++) {
|
|
238
|
+
h ^= text.charCodeAt(i);
|
|
239
|
+
h = Math.imul(h, 16777619);
|
|
240
|
+
}
|
|
241
|
+
return `packing-bingo:${(h >>> 0).toString(36)}`;
|
|
242
|
+
}
|
|
243
|
+
|
|
227
244
|
// src/styles.ts
|
|
228
245
|
var FONT_URL = "https://fonts.googleapis.com/css2?family=Dela+Gothic+One&family=M+PLUS+Rounded+1c:wght@500;800&display=swap";
|
|
229
246
|
var CSS = `
|
|
230
|
-
.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);container-type:inline-size;width:100%}
|
|
247
|
+
.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);container-type:inline-size;isolation:isolate;width:100%}
|
|
231
248
|
.pkb,.pkb *{box-sizing:border-box;margin:0;padding:0}
|
|
232
249
|
.pkb-board{position:relative;overflow:hidden;max-width:720px;margin:0 auto;padding:22px 26px 20px;border-radius:24px;background-color:var(--pk-sun);background-image:radial-gradient(rgba(42,31,20,.12) 1.3px,transparent 1.6px);background-size:16px 16px;border:2.5px solid var(--pk-ink);color:var(--pk-ink);font-family:var(--pk-body);line-height:1.4;text-align:left}
|
|
233
250
|
.pkb-board.pkb-shake{animation:pkb-shake .45s cubic-bezier(.36,.07,.19,.97)}
|
|
@@ -413,7 +430,7 @@ function createPackingBingo(container, options = {}) {
|
|
|
413
430
|
if (options.tag != null) M.tag = options.tag;
|
|
414
431
|
if (options.subtitle != null) M.subtitle = options.subtitle;
|
|
415
432
|
const controlled = !!options.controlled;
|
|
416
|
-
const storageKey =
|
|
433
|
+
const storageKey = resolveStorageKey(options);
|
|
417
434
|
const demo = !!options.demo;
|
|
418
435
|
const calm = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
419
436
|
let dead = false;
|
|
@@ -944,14 +961,15 @@ function createPackingBingo(container, options = {}) {
|
|
|
944
961
|
if (!busy) request(demoStart(), null, { reset: true });
|
|
945
962
|
});
|
|
946
963
|
}
|
|
947
|
-
let initial =
|
|
948
|
-
if (
|
|
964
|
+
let initial = null;
|
|
965
|
+
if (storageKey) {
|
|
949
966
|
try {
|
|
950
967
|
const saved = JSON.parse(readStore(storageKey) ?? "null");
|
|
951
968
|
if (Array.isArray(saved)) initial = normalize(saved);
|
|
952
969
|
} catch {
|
|
953
970
|
}
|
|
954
971
|
}
|
|
972
|
+
if (!initial && options.defaultChecked) initial = normalize(options.defaultChecked);
|
|
955
973
|
if (!initial && demo) initial = demoStart();
|
|
956
974
|
fit();
|
|
957
975
|
baseline(initial ?? normalize(void 0));
|
package/dist/core.d.cts
CHANGED
|
@@ -82,14 +82,18 @@ interface PackingBingoOptions {
|
|
|
82
82
|
title?: string;
|
|
83
83
|
tag?: string;
|
|
84
84
|
subtitle?: string;
|
|
85
|
-
/** Initial state
|
|
85
|
+
/** Initial state (16 booleans), used when nothing is saved under `storageKey`. */
|
|
86
86
|
defaultChecked?: readonly boolean[];
|
|
87
87
|
/**
|
|
88
88
|
* When true, taps don't change the card by themselves: they call `onChange` with the
|
|
89
89
|
* requested state, and the card only changes through `setChecked`. (React's `checked` prop.)
|
|
90
90
|
*/
|
|
91
91
|
controlled?: boolean;
|
|
92
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* Where the checks are saved in localStorage. By default (omitted) a key derived from the
|
|
94
|
+
* items is used, so progress is saved automatically; pass a string to choose the key, or null
|
|
95
|
+
* to turn saving off. Ignored when controlled. Demo cards only save when given a key.
|
|
96
|
+
*/
|
|
93
97
|
storageKey?: string | null;
|
|
94
98
|
/** Start with sound on (default) or off. A choice made with the sound button is remembered. */
|
|
95
99
|
sound?: boolean;
|
package/dist/core.d.ts
CHANGED
|
@@ -82,14 +82,18 @@ interface PackingBingoOptions {
|
|
|
82
82
|
title?: string;
|
|
83
83
|
tag?: string;
|
|
84
84
|
subtitle?: string;
|
|
85
|
-
/** Initial state
|
|
85
|
+
/** Initial state (16 booleans), used when nothing is saved under `storageKey`. */
|
|
86
86
|
defaultChecked?: readonly boolean[];
|
|
87
87
|
/**
|
|
88
88
|
* When true, taps don't change the card by themselves: they call `onChange` with the
|
|
89
89
|
* requested state, and the card only changes through `setChecked`. (React's `checked` prop.)
|
|
90
90
|
*/
|
|
91
91
|
controlled?: boolean;
|
|
92
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* Where the checks are saved in localStorage. By default (omitted) a key derived from the
|
|
94
|
+
* items is used, so progress is saved automatically; pass a string to choose the key, or null
|
|
95
|
+
* to turn saving off. Ignored when controlled. Demo cards only save when given a key.
|
|
96
|
+
*/
|
|
93
97
|
storageKey?: string | null;
|
|
94
98
|
/** Start with sound on (default) or off. A choice made with the sound button is remembered. */
|
|
95
99
|
sound?: boolean;
|
package/dist/core.js
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -228,10 +228,27 @@ function createSound(isMuted) {
|
|
|
228
228
|
};
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
// src/storage-key.ts
|
|
232
|
+
function resolveStorageKey(options) {
|
|
233
|
+
if (options.controlled) return null;
|
|
234
|
+
if (options.storageKey !== void 0) return options.storageKey || null;
|
|
235
|
+
if (options.demo) return null;
|
|
236
|
+
return itemsKey(options.items ?? DEFAULT_ITEMS);
|
|
237
|
+
}
|
|
238
|
+
function itemsKey(items) {
|
|
239
|
+
const text = items.map((it) => `${it.emoji}\0${it.label}`).join("");
|
|
240
|
+
let h = 2166136261;
|
|
241
|
+
for (let i = 0; i < text.length; i++) {
|
|
242
|
+
h ^= text.charCodeAt(i);
|
|
243
|
+
h = Math.imul(h, 16777619);
|
|
244
|
+
}
|
|
245
|
+
return `packing-bingo:${(h >>> 0).toString(36)}`;
|
|
246
|
+
}
|
|
247
|
+
|
|
231
248
|
// src/styles.ts
|
|
232
249
|
var FONT_URL = "https://fonts.googleapis.com/css2?family=Dela+Gothic+One&family=M+PLUS+Rounded+1c:wght@500;800&display=swap";
|
|
233
250
|
var CSS = `
|
|
234
|
-
.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);container-type:inline-size;width:100%}
|
|
251
|
+
.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);container-type:inline-size;isolation:isolate;width:100%}
|
|
235
252
|
.pkb,.pkb *{box-sizing:border-box;margin:0;padding:0}
|
|
236
253
|
.pkb-board{position:relative;overflow:hidden;max-width:720px;margin:0 auto;padding:22px 26px 20px;border-radius:24px;background-color:var(--pk-sun);background-image:radial-gradient(rgba(42,31,20,.12) 1.3px,transparent 1.6px);background-size:16px 16px;border:2.5px solid var(--pk-ink);color:var(--pk-ink);font-family:var(--pk-body);line-height:1.4;text-align:left}
|
|
237
254
|
.pkb-board.pkb-shake{animation:pkb-shake .45s cubic-bezier(.36,.07,.19,.97)}
|
|
@@ -417,7 +434,7 @@ function createPackingBingo(container, options = {}) {
|
|
|
417
434
|
if (options.tag != null) M.tag = options.tag;
|
|
418
435
|
if (options.subtitle != null) M.subtitle = options.subtitle;
|
|
419
436
|
const controlled = !!options.controlled;
|
|
420
|
-
const storageKey =
|
|
437
|
+
const storageKey = resolveStorageKey(options);
|
|
421
438
|
const demo = !!options.demo;
|
|
422
439
|
const calm = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
423
440
|
let dead = false;
|
|
@@ -948,14 +965,15 @@ function createPackingBingo(container, options = {}) {
|
|
|
948
965
|
if (!busy) request(demoStart(), null, { reset: true });
|
|
949
966
|
});
|
|
950
967
|
}
|
|
951
|
-
let initial =
|
|
952
|
-
if (
|
|
968
|
+
let initial = null;
|
|
969
|
+
if (storageKey) {
|
|
953
970
|
try {
|
|
954
971
|
const saved = JSON.parse(readStore(storageKey) ?? "null");
|
|
955
972
|
if (Array.isArray(saved)) initial = normalize(saved);
|
|
956
973
|
} catch {
|
|
957
974
|
}
|
|
958
975
|
}
|
|
976
|
+
if (!initial && options.defaultChecked) initial = normalize(options.defaultChecked);
|
|
959
977
|
if (!initial && demo) initial = demoStart();
|
|
960
978
|
fit();
|
|
961
979
|
baseline(initial ?? normalize(void 0));
|
|
@@ -1012,6 +1030,8 @@ var PackingBingo = (0, import_react.forwardRef)(function PackingBingo2(props, re
|
|
|
1012
1030
|
messagesRef.current = messages;
|
|
1013
1031
|
const controlled = checked !== void 0;
|
|
1014
1032
|
const latest = (0, import_react.useRef)(checked ?? defaultChecked);
|
|
1033
|
+
const storageKey = resolveStorageKey({ ...config, controlled });
|
|
1034
|
+
const latestKey = (0, import_react.useRef)(storageKey);
|
|
1015
1035
|
if (controlled) latest.current = checked;
|
|
1016
1036
|
const messageShape = messages ? Object.fromEntries(Object.entries(messages).map(([k, v]) => [k, typeof v === "function" ? "\u0192" : v])) : null;
|
|
1017
1037
|
const key = JSON.stringify({ ...config, messageShape, controlled });
|
|
@@ -1027,7 +1047,7 @@ var PackingBingo = (0, import_react.forwardRef)(function PackingBingo2(props, re
|
|
|
1027
1047
|
...config,
|
|
1028
1048
|
messages: liveMessages,
|
|
1029
1049
|
controlled,
|
|
1030
|
-
defaultChecked: latest.current,
|
|
1050
|
+
defaultChecked: controlled || latestKey.current === storageKey ? latest.current : defaultChecked,
|
|
1031
1051
|
onChange: (next) => {
|
|
1032
1052
|
if (!controlled) latest.current = next;
|
|
1033
1053
|
callbacks.current.onChange?.(next);
|
|
@@ -1038,6 +1058,7 @@ var PackingBingo = (0, import_react.forwardRef)(function PackingBingo2(props, re
|
|
|
1038
1058
|
inst.current = instance;
|
|
1039
1059
|
return () => {
|
|
1040
1060
|
if (!controlled) latest.current = instance.getState().checked;
|
|
1061
|
+
latestKey.current = storageKey;
|
|
1041
1062
|
instance.destroy();
|
|
1042
1063
|
if (inst.current === instance) inst.current = null;
|
|
1043
1064
|
};
|
package/dist/index.d.cts
CHANGED
|
@@ -6,7 +6,7 @@ export { BingoEvent, BingoItem, DEFAULT_ITEMS, LINES, Messages, PackingBingoInst
|
|
|
6
6
|
interface PackingBingoProps extends Omit<PackingBingoOptions, "controlled" | "defaultChecked"> {
|
|
7
7
|
/** Controlled state (16 booleans). Update it from `onChange`. */
|
|
8
8
|
checked?: readonly boolean[];
|
|
9
|
-
/** Initial state for an uncontrolled card (16 booleans)
|
|
9
|
+
/** Initial state for an uncontrolled card (16 booleans), used when nothing is saved under `storageKey`. */
|
|
10
10
|
defaultChecked?: readonly boolean[];
|
|
11
11
|
className?: string;
|
|
12
12
|
style?: CSSProperties;
|
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export { BingoEvent, BingoItem, DEFAULT_ITEMS, LINES, Messages, PackingBingoInst
|
|
|
6
6
|
interface PackingBingoProps extends Omit<PackingBingoOptions, "controlled" | "defaultChecked"> {
|
|
7
7
|
/** Controlled state (16 booleans). Update it from `onChange`. */
|
|
8
8
|
checked?: readonly boolean[];
|
|
9
|
-
/** Initial state for an uncontrolled card (16 booleans)
|
|
9
|
+
/** Initial state for an uncontrolled card (16 booleans), used when nothing is saved under `storageKey`. */
|
|
10
10
|
defaultChecked?: readonly boolean[];
|
|
11
11
|
className?: string;
|
|
12
12
|
style?: CSSProperties;
|
package/dist/index.js
CHANGED
|
@@ -4,8 +4,9 @@ import {
|
|
|
4
4
|
LINES,
|
|
5
5
|
createPackingBingo,
|
|
6
6
|
en,
|
|
7
|
-
ja
|
|
8
|
-
|
|
7
|
+
ja,
|
|
8
|
+
resolveStorageKey
|
|
9
|
+
} from "./chunk-WGZKPODU.js";
|
|
9
10
|
|
|
10
11
|
// src/react.tsx
|
|
11
12
|
import { forwardRef, useEffect, useImperativeHandle, useRef } from "react";
|
|
@@ -20,6 +21,8 @@ var PackingBingo = forwardRef(function PackingBingo2(props, ref) {
|
|
|
20
21
|
messagesRef.current = messages;
|
|
21
22
|
const controlled = checked !== void 0;
|
|
22
23
|
const latest = useRef(checked ?? defaultChecked);
|
|
24
|
+
const storageKey = resolveStorageKey({ ...config, controlled });
|
|
25
|
+
const latestKey = useRef(storageKey);
|
|
23
26
|
if (controlled) latest.current = checked;
|
|
24
27
|
const messageShape = messages ? Object.fromEntries(Object.entries(messages).map(([k, v]) => [k, typeof v === "function" ? "\u0192" : v])) : null;
|
|
25
28
|
const key = JSON.stringify({ ...config, messageShape, controlled });
|
|
@@ -35,7 +38,7 @@ var PackingBingo = forwardRef(function PackingBingo2(props, ref) {
|
|
|
35
38
|
...config,
|
|
36
39
|
messages: liveMessages,
|
|
37
40
|
controlled,
|
|
38
|
-
defaultChecked: latest.current,
|
|
41
|
+
defaultChecked: controlled || latestKey.current === storageKey ? latest.current : defaultChecked,
|
|
39
42
|
onChange: (next) => {
|
|
40
43
|
if (!controlled) latest.current = next;
|
|
41
44
|
callbacks.current.onChange?.(next);
|
|
@@ -46,6 +49,7 @@ var PackingBingo = forwardRef(function PackingBingo2(props, ref) {
|
|
|
46
49
|
inst.current = instance;
|
|
47
50
|
return () => {
|
|
48
51
|
if (!controlled) latest.current = instance.getState().checked;
|
|
52
|
+
latestKey.current = storageKey;
|
|
49
53
|
instance.destroy();
|
|
50
54
|
if (inst.current === instance) inst.current = null;
|
|
51
55
|
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
"use strict";var PackingBingo=(()=>{var
|
|
1
|
+
"use strict";var PackingBingo=(()=>{var Se=Object.defineProperty;var bt=Object.getOwnPropertyDescriptor;var dt=Object.getOwnPropertyNames;var mt=Object.prototype.hasOwnProperty;var ft=(o,n)=>{for(var m in n)Se(o,m,{get:n[m],enumerable:!0})},kt=(o,n,m,l)=>{if(n&&typeof n=="object"||typeof n=="function")for(let f of dt(n))!mt.call(o,f)&&f!==m&&Se(o,f,{get:()=>n[f],enumerable:!(l=bt(n,f))||l.enumerable});return o};var ut=o=>kt(Se({},"__esModule",{value:!0}),o);var Et={};ft(Et,{DEFAULT_ITEMS:()=>re,LINES:()=>F,createPackingBingo:()=>wt,en:()=>Ge,ja:()=>Be});var re=[{emoji:"\u{1F4D6}",label:"\u65C5\u306E\u3057\u304A\u308A"},{emoji:"\u{1FAAA}",label:"\u751F\u5F92\u624B\u5E33"},{emoji:"\u{1F3E5}",label:"\u4FDD\u967A\u8A3C\u30B3\u30D4\u30FC"},{emoji:"\u{1F45B}",label:"\u304A\u3053\u3065\u304B\u3044"},{emoji:"\u{1F455}",label:"\u7740\u66FF\u3048"},{emoji:"\u{1F319}",label:"\u30D1\u30B8\u30E3\u30DE"},{emoji:"\u{1F9E5}",label:"\u4E0A\u7740"},{emoji:"\u{1FAA5}",label:"\u6D17\u9762\u7528\u5177"},{emoji:"\u{1F6C1}",label:"\u30BF\u30AA\u30EB"},{emoji:"\u{1F927}",label:"\u30CF\u30F3\u30AB\u30C1\u30FB\u30C6\u30A3\u30C3\u30B7\u30E5"},{emoji:"\u{1F637}",label:"\u30DE\u30B9\u30AF"},{emoji:"\u{1F48A}",label:"\u5E38\u5099\u85AC\u30FB\u9154\u3044\u6B62\u3081"},{emoji:"\u2602\uFE0F",label:"\u6298\u308A\u305F\u305F\u307F\u5098"},{emoji:"\u270F\uFE0F",label:"\u7B46\u8A18\u7528\u5177"},{emoji:"\u{1F6CD}\uFE0F",label:"\u30D3\u30CB\u30FC\u30EB\u888B"},{emoji:"\u{1F4F1}",label:"\u30B9\u30DE\u30DB\u30FB\u5145\u96FB\u5668"}];var Be={title:"\u6301\u3061\u3082\u306E\u30D3\u30F3\u30B4",tag:"\u4FEE\u5B66\u65C5\u884C",subtitle:"\u30BF\u30C3\u30D7\u3067\u30B9\u30BF\u30F3\u30D7\u3002\u30BF\u30C6\u30FB\u30E8\u30B3\u30FB\u30CA\u30CA\u30E1\u3067\u30D3\u30F3\u30B4\uFF01",packed:"\u305D\u308D\u3063\u305F",bingo:"BINGO",score:"SCORE",combo:o=>`${o} COMBO`,comboPop:o=>`${o} COMBO!`,reset:"\u30EA\u30BB\u30C3\u30C8",soundOn:"\u{1F50A} \u97F3\u3042\u308A",soundOff:"\u{1F507} \u97F3\u306A\u3057",stamp:"OK",reachBadge:"\u30EA\u30FC\u30C1",reachPop:"\u30EA\u30FC\u30C1!",bannerSingle:"BINGO!",bannerDouble:"\u30C0\u30D6\u30EB\u30D3\u30F3\u30B4!",bannerTriple:"\u30C8\u30EA\u30D7\u30EB\u30D3\u30F3\u30B4!",bannerNth:o=>`${o} \u30E9\u30A4\u30F3\u76EE\uFF01`,bannerMulti:o=>`\u4E00\u6C17\u306B ${o} \u30E9\u30A4\u30F3\uFF01`,statusStart:o=>`\u306E\u3053\u308A ${o} \u3053\u3002\u307E\u305A\u306F1\u30E9\u30A4\u30F3\u305D\u308D\u3048\u3088\u3046`,statusLines:(o,n)=>`\u30D3\u30F3\u30B4 ${o} \u30E9\u30A4\u30F3\uFF01 \u306E\u3053\u308A ${n} \u3053`,statusReach:o=>`\u30EA\u30FC\u30C1\uFF01 \u3042\u3068${o.slice(0,2).map(n=>`\u300C${n}\u300D`).join("\u304B")}${o.length>2?"\u306A\u3069":""}\u3067\u30D3\u30F3\u30B4`,statusDone:"\u{1F389} \u30D1\u30FC\u30D5\u30A7\u30AF\u30C8\uFF01\u5FD8\u308C\u7269\u30BC\u30ED\u3067\u51FA\u767A\u3067\u304D\u307E\u3059",perfectTag:"\u{1F3C6} PERFECT",perfectStamp:`\u6E96\u5099
|
|
2
2
|
\u5B8C\u4E86`,perfectTitle:"\u30D1\u30FC\u30D5\u30A7\u30AF\u30C8\uFF01",perfectBody:`16\u3053 \u305C\u3093\u3076\u305D\u308D\u3063\u305F\uFF01
|
|
3
|
-
\u5FD8\u308C\u7269\u30BC\u30ED\u3067\u3001\u3044\u3063\u3066\u3089\u3063\u3057\u3083\u3044\uFF01`,perfectBonus:
|
|
3
|
+
\u5FD8\u308C\u7269\u30BC\u30ED\u3067\u3001\u3044\u3063\u3066\u3089\u3063\u3057\u3083\u3044\uFF01`,perfectBonus:o=>`PERFECT BONUS +${o}`,statScore:"SCORE",statCombo:"MAX COMBO",statLines:"BINGO",close:"\u3068\u3058\u308B",demoLabel:"\u30C7\u30E2\uFF1A",demoBingo:"\u25B6 \u30D3\u30F3\u30B4\u3092\u898B\u308B",demoPerfect:"\u25B6 \u30B3\u30F3\u30DC \u2192 \u30D1\u30FC\u30D5\u30A7\u30AF\u30C8",demoRestore:"\u6700\u521D\u306B\u623B\u3059"},Ge={title:"Packing Bingo",tag:"School trip",subtitle:"Tap to stamp. Rows, columns and diagonals all count!",packed:"PACKED",bingo:"BINGO",score:"SCORE",combo:o=>`${o} COMBO`,comboPop:o=>`${o} COMBO!`,reset:"Reset",soundOn:"\u{1F50A} Sound on",soundOff:"\u{1F507} Muted",stamp:"OK",reachBadge:"REACH",reachPop:"Reach!",bannerSingle:"BINGO!",bannerDouble:"DOUBLE BINGO!",bannerTriple:"TRIPLE BINGO!",bannerNth:o=>`Line ${o}!`,bannerMulti:o=>`${o} lines at once!`,statusStart:o=>`${o} to go. Finish a line first!`,statusLines:(o,n)=>`${o} bingo line${o===1?"":"s"}! ${n} to go`,statusReach:o=>`Reach! Pack ${o.slice(0,2).join(" or ")}${o.length>2?" (or more)":""} for a bingo`,statusDone:"\u{1F389} Perfect! Nothing left behind.",perfectTag:"\u{1F3C6} PERFECT",perfectStamp:`ALL
|
|
4
4
|
SET`,perfectTitle:"Perfect!",perfectBody:`All 16 packed!
|
|
5
|
-
Have a great trip!`,perfectBonus:
|
|
6
|
-
.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);container-type:inline-size;width:100%}
|
|
5
|
+
Have a great trip!`,perfectBonus:o=>`PERFECT BONUS +${o}`,statScore:"SCORE",statCombo:"MAX COMBO",statLines:"BINGO",close:"Close",demoLabel:"Demo:",demoBingo:"\u25B6 Show a bingo",demoPerfect:"\u25B6 Combo \u2192 perfect",demoRestore:"Restore"};var Ue=[0,2,4,7,9,12,14,16,19,21,24,26,28,31,33,36],U=o=>523.25*Math.pow(2,o/12);function Ke(o){let n=null,m=null;function l(){if(o()||typeof window>"u")return null;let b=navigator.userActivation;if(!n&&b&&!b.hasBeenActive)return null;if(!n){let d=window.AudioContext??window.webkitAudioContext;if(!d)return null;n=new d;let y=n.createDynamicsCompressor();m=n.createGain(),m.gain.value=.6,m.connect(y).connect(n.destination)}return n.state==="suspended"&&n.resume(),n}function f(b,d=0,y=.15,E="triangle",M=.2,A=0){let p=l();if(!p||!m)return;let x=p.currentTime+d,B=p.createOscillator(),C=p.createGain();B.type=E,B.frequency.setValueAtTime(b,x),A&&B.frequency.exponentialRampToValueAtTime(A,x+y),C.gain.setValueAtTime(1e-4,x),C.gain.exponentialRampToValueAtTime(M,x+.01),C.gain.exponentialRampToValueAtTime(1e-4,x+y),B.connect(C).connect(m),B.start(x),B.stop(x+y+.05)}function P(b=0,d=.3,y=.25,E=1200){let M=l();if(!M||!m)return;let A=M.currentTime+b,p=Math.ceil(M.sampleRate*d),x=M.createBuffer(1,p,M.sampleRate),B=x.getChannelData(0);for(let k=0;k<p;k++)B[k]=(Math.random()*2-1)*Math.pow(1-k/p,2.5);let C=M.createBufferSource(),_=M.createBiquadFilter(),j=M.createGain();C.buffer=x,_.type="bandpass",_.frequency.value=E,_.Q.value=.7,j.gain.value=y,C.connect(_).connect(j).connect(m),C.start(A)}let R=b=>U(Ue[Math.min(Math.max(b,0),Ue.length-1)]);return{pop(b){let d=R(b);f(d,0,.16,"triangle",.22),f(d*2,0,.08,"sine",.06),f(150,0,.09,"sine",.25,60)},off:()=>f(392,0,.1,"sine",.1,262),reach(){f(988,0,.08,"square",.05),f(1319,.09,.14,"square",.05)},bingo(b){[0,4,7,12,16,19].forEach((d,y)=>f(U(d),y*.06,.22,"square",.06)),[12,16,19].forEach(d=>f(U(d),.38,.6,"triangle",.12)),b>1&&[24,28,31].forEach(d=>f(U(d),.55,.6,"triangle",.08)),P(0,.25,.12,3e3)},perfect(){[0,4,7,12,16,19,24].forEach((b,d)=>f(U(b-12),d*.07,.25,"square",.06)),[0,4,7,12].forEach(b=>f(U(b),.55,1.2,"triangle",.12)),[12,16,19].forEach(b=>f(U(b),.55,1.2,"sawtooth",.03)),f(U(-12),.55,1.2,"sine",.2)},boom(){P(0,.5,.3,500),f(90,0,.3,"sine",.25,40)},tick:()=>f(1760,0,.03,"square",.03),note:(b,d=0)=>f(R(b),d,.12,"square",.05),close(){n&&n.close().catch(()=>{}),n=null,m=null}}}function _e(o){return o.controlled?null:o.storageKey!==void 0?o.storageKey||null:o.demo?null:gt(o.items??re)}function gt(o){let n=o.map(l=>`${l.emoji}\0${l.label}`).join(""),m=2166136261;for(let l=0;l<n.length;l++)m^=n.charCodeAt(l),m=Math.imul(m,16777619);return`packing-bingo:${(m>>>0).toString(36)}`}var We="https://fonts.googleapis.com/css2?family=Dela+Gothic+One&family=M+PLUS+Rounded+1c:wght@500;800&display=swap",Ve=`
|
|
6
|
+
.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);container-type:inline-size;isolation:isolate;width:100%}
|
|
7
7
|
.pkb,.pkb *{box-sizing:border-box;margin:0;padding:0}
|
|
8
8
|
.pkb-board{position:relative;overflow:hidden;max-width:720px;margin:0 auto;padding:22px 26px 20px;border-radius:24px;background-color:var(--pk-sun);background-image:radial-gradient(rgba(42,31,20,.12) 1.3px,transparent 1.6px);background-size:16px 16px;border:2.5px solid var(--pk-ink);color:var(--pk-ink);font-family:var(--pk-body);line-height:1.4;text-align:left}
|
|
9
9
|
.pkb-board.pkb-shake{animation:pkb-shake .45s cubic-bezier(.36,.07,.19,.97)}
|
|
@@ -106,7 +106,7 @@ Have a great trip!`,perfectBonus:r=>`PERFECT BONUS +${r}`,statScore:"SCORE",stat
|
|
|
106
106
|
@container (max-width:560px){.pkb-board{padding:18px 14px 16px}.pkb-head{flex-direction:column;align-items:stretch;gap:12px}.pkb-score{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}.pkb-pill,.pkb-pill.pkb-sc{min-width:0;padding:5px 4px 4px}.pkb-pill b{font-size:19px}.pkb-title{font-size:26px}.pkb-grid{gap:8px}.pkb-tile{border-radius:12px;box-shadow:3px 3px 0 var(--pk-ink);gap:3px;aspect-ratio:1/1}.pkb-emo{font-size:24px}.pkb-lbl{font-size:10.5px;padding:0 2px}.pkb-stamp{font-size:14px}.pkb-tile.pkb-reach::after{font-size:9px;padding:2px 5px;top:-9px;right:-5px}.pkb-lines .pkb-u{stroke-width:10}.pkb-lines .pkb-o{stroke-width:5}.pkb-banner::before{width:380px;height:380px;margin:-190px 0 0 -190px}.pkb-banner span{font-size:12px}.pkb-medal{width:96px;height:96px;font-size:20px}.pkb-card{padding:20px 14px 16px}.pkb-ptitle{font-size:24px}.pkb-stats b{font-size:17px}.pkb-float.pkb-pt{font-size:17px}.pkb-float.pkb-cmb{font-size:13px}}
|
|
107
107
|
@media (prefers-reduced-motion:reduce){.pkb-board.pkb-shake,.pkb-board.pkb-punch,.pkb-flash.pkb-go,.pkb-tile.pkb-hop,.pkb-tile.pkb-squash,.pkb-tile.pkb-reach,.pkb-tile.pkb-line::before,.pkb-pill.pkb-hot,.pkb-bump,.pkb-tile.pkb-reach::after,.pkb-banner::before,.pkb-lines .pkb-draw line,.pkb-kicker.pkb-gold{animation:none}.pkb-banner.pkb-show{animation:pkb-fade 1.8s forwards}.pkb-float{animation:pkb-fade 1s both}.pkb-perfect.pkb-show .pkb-medal{animation:none;opacity:1;transform:rotate(-14deg)}.pkb-card{transition:none}}
|
|
108
108
|
@keyframes pkb-fade{0%,100%{opacity:0}15%,80%{opacity:1}}
|
|
109
|
-
`;var F=[[0,1,2,3],[4,5,6,7],[8,9,10,11],[12,13,14,15],[0,4,8,12],[1,5,9,13],[2,6,10,14],[3,7,11,15],[0,5,10,15],[3,6,9,12]],
|
|
109
|
+
`;var F=[[0,1,2,3],[4,5,6,7],[8,9,10,11],[12,13,14,15],[0,4,8,12],[1,5,9,13],[2,6,10,14],[3,7,11,15],[0,5,10,15],[3,6,9,12]],ht=[0,1,2,3,5,10],Ye=["#E8473B","#2F6FED","#1FA971","#FFFDF6","#2A1F14","#FF9F1C"],Xe=["#FF9F1C","#1FA971","#2F6FED","#9B5DE5","#E8473B"],xt=2500,Je="packing-bingo-muted",g=o=>o.replace(/[&<>"']/g,n=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[n]),K=o=>Array.from({length:16},(n,m)=>!!o?.[m]),Qe=()=>Array.from({length:16},(o,n)=>ht.includes(n));function Ze(o){try{return localStorage.getItem(o)}catch{return null}}function et(o,n){try{localStorage.setItem(o,n)}catch{}}function vt(o){if(!document.getElementById("pkb-styles")){let n=document.createElement("style");n.id="pkb-styles",n.textContent=Ve,document.head.appendChild(n)}if(o&&!document.getElementById("pkb-fonts")){let n=document.createElement("link");n.id="pkb-fonts",n.rel="stylesheet",n.href=We,document.head.appendChild(n)}}function yt(o,n,m){let l=o.map((P,R)=>`<label class="pkb-tile" data-reach="${g(n.reachBadge)}" style="--pkb-gd:${((R>>2)+(R&3))*.12}s"><input type="checkbox"><span class="pkb-emo" aria-hidden="true">${g(P.emoji)}</span><span class="pkb-lbl">${g(P.label).replace(/・/g,"\u30FB<wbr>")}</span><span class="pkb-stamp" aria-hidden="true">${g(n.stamp)}</span></label>`).join(""),f=m?`<div class="pkb-demo"><span>${g(n.demoLabel)}</span><button type="button" data-r="dB">${g(n.demoBingo)}</button><button type="button" data-r="dP">${g(n.demoPerfect)}</button><button type="button" data-r="dR">${g(n.demoRestore)}</button></div>`:"";return`<div class="pkb"><div class="pkb-board">
|
|
110
110
|
<div class="pkb-head"><div>
|
|
111
111
|
<div class="pkb-tags"><span class="pkb-kicker" data-r="kick">${g(n.tag)}</span><button type="button" class="pkb-snd" data-r="snd"></button></div>
|
|
112
112
|
<h2 class="pkb-title">${g(n.title)}</h2><p class="pkb-sub">${g(n.subtitle)}</p></div>
|
|
@@ -115,7 +115,7 @@ Have a great trip!`,perfectBonus:r=>`PERFECT BONUS +${r}`,statScore:"SCORE",stat
|
|
|
115
115
|
<div class="pkb-pill" data-r="bp"><small>${g(n.bingo)}</small><b data-r="bng"></b></div>
|
|
116
116
|
<div class="pkb-pill pkb-sc" data-r="sp"><small>${g(n.score)}</small><b data-r="sc">0</b></div></div>
|
|
117
117
|
<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>
|
|
118
|
-
<div class="pkb-gw"><div class="pkb-grid">${
|
|
118
|
+
<div class="pkb-gw"><div class="pkb-grid">${l}</div><svg class="pkb-lines" aria-hidden="true"></svg>
|
|
119
119
|
<div class="pkb-banner" data-r="banner" aria-hidden="true"><b data-r="bT"></b><span data-r="bS"></span></div></div>
|
|
120
120
|
<div class="pkb-foot"><span data-r="msg" aria-live="polite"></span><button type="button" class="pkb-reset" data-r="reset">${g(n.reset)}</button></div>
|
|
121
121
|
<div class="pkb-flash" data-r="flash"></div>
|
|
@@ -123,4 +123,4 @@ Have a great trip!`,perfectBonus:r=>`PERFECT BONUS +${r}`,statScore:"SCORE",stat
|
|
|
123
123
|
<div class="pkb-medal">${g(n.perfectStamp)}</div><h3 class="pkb-ptitle">${g(n.perfectTitle)}</h3><p class="pkb-pbody">${g(n.perfectBody)}</p>
|
|
124
124
|
<div class="pkb-stats"><div><small>${g(n.statScore)}</small><b data-r="sScore">0</b></div><div><small>${g(n.statCombo)}</small><b data-r="sCombo">\xD70</b></div><div><small>${g(n.statLines)}</small><b data-r="sBingo">0</b></div></div>
|
|
125
125
|
<button type="button" class="pkb-close" data-r="close">${g(n.close)}</button></div></div>
|
|
126
|
-
<canvas class="pkb-fx"></canvas></div>${m}</div>`}function vt(r,n={}){let k=n.items??Le;if(k.length!==16)throw new Error(`[packing-bingo] "items" must have exactly 16 entries (got ${k.length}).`);let d={...Se,...n.messages};n.title!=null&&(d.title=n.title),n.tag!=null&&(d.tag=n.tag),n.subtitle!=null&&(d.subtitle=n.subtitle);let m=!!n.controlled,$=m?null:n.storageKey??null,R=!!n.demo,c=typeof matchMedia=="function"&&matchMedia("(prefers-reduced-motion: reduce)").matches,b=!1,y=new Set,E=(e,t)=>{let a=window.setTimeout(()=>{y.delete(a),b||e()},t);y.add(a)},M=e=>new Promise(t=>window.setTimeout(t,e)),A=0;ht(n.fonts!==!1),r.innerHTML=xt(k,d,R);let p=e=>{let t=r.querySelector(`[data-r="${e}"]`);if(!t)throw new Error(`[packing-bingo] internal: missing element "${e}"`);return t},x=r.querySelector(".pkb-board"),S=r.querySelector(".pkb-gw"),C=r.querySelector(".pkb-grid"),W=r.querySelector(".pkb-lines"),I=r.querySelector(".pkb-fx"),f=I.getContext("2d"),L=Array.from(C.children),re=L.map(e=>e.querySelector("input")),Be=p("kick"),be=p("snd"),N=p("perfect"),h=_(void 0),w=[],ie=new Set,de=new Set,se=!1,Me=0,z=0,te=0,B=0,X=0,Ce=0,me=!1,ne=new Set,J=new Set,ze=Qe(Xe),V=ze!=null?ze==="1":n.sound===!1,T=_e(()=>V),Te=()=>{be.textContent=V?d.soundOff:d.soundOn,be.setAttribute("aria-pressed",String(!V))};be.addEventListener("click",()=>{V=!V,Ze(Xe,V?"1":"0"),Te(),V||T.pop(4)}),Te();let D=[],K=0,ae=1,H=0,G=0,Pe=()=>{let e=x.getBoundingClientRect();ae=Math.min(2,window.devicePixelRatio||1),H=e.width,G=e.height,I.width=Math.round(H*ae),I.height=Math.round(G*ae)},fe=(e,t,a,o,s,i,l,v)=>({x:e,y:t,vx:a,vy:o,g:s,life:l,shape:v,w:(5+Math.random()*6)*i,h:(3+Math.random()*5)*i,r:Math.random()*6,vr:(Math.random()-.5)*.35,c:Ke[Math.random()*Ke.length|0]}),ke=()=>{!K&&f&&(K=requestAnimationFrame($e))};function Q(e,t,a,o={}){if(!c){for(let s=0;s<a;s++){let i=(o.angle??-Math.PI/2)+(Math.random()-.5)*(o.spread??Math.PI*2),l=(o.speed??10)*(.35+Math.random()*.85),v=Math.random()<(o.star??.15)?"star":Math.random()<.3?"round":"rect";D.push(fe(e,t,Math.cos(i)*l,Math.sin(i)*l,o.g??.28,o.size??1,(o.life??110)+Math.random()*40,v))}ke()}}function et(e){if(c)return;let t=performance.now()+e,a=()=>{for(let o=0;o<4;o++)D.push(fe(Math.random()*H,-12,(Math.random()-.5)*2,2+Math.random()*3,.045,1,420,Math.random()<.2?"star":"rect"));ke(),performance.now()<t&&E(a,45)};a()}function tt(e){if(c)return;let t=performance.now()+e,a=()=>{let o=fe(Math.random()*H,-30,(Math.random()-.5)*1.5,1.5+Math.random()*2,.05,1,500,"emoji");o.e=k[Math.random()*k.length|0].emoji,o.w=22+Math.random()*12,o.vr=(Math.random()-.5)*.06,D.push(o),ke(),performance.now()<t&&E(a,110)};a()}function nt(e,t){Q(e,t,80,{speed:7.5,g:.06,life:85,star:.4}),T.boom()}function at(e,t){e.beginPath();for(let a=0;a<10;a++){let o=a*Math.PI/5-Math.PI/2,s=a%2?t*.45:t;e.lineTo(Math.cos(o)*s,Math.sin(o)*s)}e.closePath(),e.fill()}function $e(){if(K=0,!(b||!f)){f.setTransform(ae,0,0,ae,0,0),f.clearRect(0,0,H,G),D=D.filter(e=>e.life>0&&e.y<G+40);for(let e of D)e.vx*=.985,e.vy=e.vy*.985+e.g,e.x+=e.vx,e.y+=e.vy,e.r+=e.vr,e.life--,f.save(),f.globalAlpha=Math.min(1,e.life/25),f.translate(e.x,e.y),f.rotate(e.r),f.fillStyle=e.c,e.shape==="round"?(f.beginPath(),f.arc(0,0,e.h/1.6,0,7),f.fill()):e.shape==="star"?at(f,e.w*.8):e.shape==="emoji"?(f.font=`${e.w}px "Apple Color Emoji","Segoe UI Emoji","Noto Color Emoji",sans-serif`,f.textAlign="center",f.textBaseline="middle",f.fillText(e.e??"",0,0)):(f.scale(1,Math.cos(e.r*2.3)),f.fillRect(-e.w/2,-e.h/2,e.w,e.h)),f.restore();D.length&&(K=requestAnimationFrame($e))}}let pe=e=>{let t=x.getBoundingClientRect(),a=e.getBoundingClientRect();return{x:a.left-t.left+a.width/2,y:a.top-t.top+a.height/2}},P=(e,t)=>{e.classList.remove(t),e.offsetWidth,e.classList.add(t)},Ae=(e,t)=>{c||(L[e].classList.remove("pkb-squash"),L[e].style.setProperty("--pkb-d",t+"ms"),P(L[e],"pkb-hop"))};C.addEventListener("animationend",e=>{let t=e.target;t.classList.contains("pkb-tile")&&(e.animationName==="pkb-hop"||e.animationName==="pkb-squash")&&t.classList.remove(e.animationName)});let ot=e=>{let t=document.createElement("span");t.className="pkb-ring",e.appendChild(t),t.addEventListener("animationend",()=>t.remove())},ue=e=>{let t=navigator.userActivation;if(!(t&&!t.hasBeenActive))try{navigator.vibrate?.(e)}catch{}},le=(e,t,a,o,s=0,i)=>{let l=document.createElement("div");l.className="pkb-float pkb-"+o,l.textContent=e,l.style.left=t+"px",l.style.top=a+"px",s&&(l.style.animationDelay=s+"ms"),i&&(l.style.color=i),x.appendChild(l),l.addEventListener("animationend",()=>l.remove())},Oe=(e,t,a)=>Math.max(0,Math.min(1,(e-t)/a)),Fe=e=>1-Math.pow(1-e,3),ce=0;function ge(){cancelAnimationFrame(ce);let e=te,t=z,a=performance.now(),o=s=>{if(b)return;let i=Oe(s,a,450);te=Math.round(e+(t-e)*Fe(i)),p("sc").textContent=te.toLocaleString(),i<1&&(ce=requestAnimationFrame(o))};o(a),c||P(p("sp"),"pkb-bump")}let Re=(e,t,a,o="pt")=>{le(`+${e}`,t,a,o),ge()};function he(e,t,a,o,s,i=!1){let l=performance.now(),v=A,q=0,j=Y=>{if(b||v!==A)return;let ee=Oe(Y,l,o);e.textContent=s(Math.round(t+(a-t)*Fe(ee))),i&&ee<1&&Y-q>55&&(T.tick(),q=Y),ee<1?requestAnimationFrame(j):c||P(e,"pkb-bump")};j(l)}let qe=e=>{let t=L[e];return{x:t.offsetLeft+t.offsetWidth/2,y:t.offsetTop+t.offsetHeight/2}};function je(e=[]){W.setAttribute("viewBox",`0 0 ${S.clientWidth} ${S.clientHeight}`),W.innerHTML=w.map(t=>{let a=qe(F[t][0]),o=qe(F[t][3]),s=o.x-a.x,i=o.y-a.y,l=Math.hypot(s,i)||1,v=s/l*26,q=i/l*26,j=`x1="${a.x-v}" y1="${a.y-q}" x2="${o.x+v}" y2="${o.y+q}" pathLength="1"`;return`<g${e.includes(t)?' class="pkb-draw"':""}><line class="pkb-u" ${j}/><line class="pkb-o" ${j}/></g>`}).join("")}function rt(e,t){let a=pe(L[e]);if(!t){B=0,p("combo").classList.remove("pkb-on"),T.off();return}let o=performance.now();B=o-Ce<gt?B+1:1,Ce=o,X=Math.max(X,B),T.pop(B-1),c||(P(L[e],"pkb-squash"),ot(L[e])),Q(a.x,a.y,18,{speed:6,g:.2,life:30,size:.6,star:.5}),ne.has(e)||(ne.add(e),z+=10*B,Re(10*B,a.x,a.y-12)),B>=2&&(le(d.comboPop(B),a.x,a.y+22,"cmb",90,Ye[(B-2)%Ye.length]),p("cmbN").textContent=d.combo(B),P(p("combo"),"pkb-on")),ue(12)}p("cbar").addEventListener("animationend",()=>{p("combo").classList.remove("pkb-on"),B=0});function it(e){e.forEach(t=>{let a=pe(L[t]);le(d.reachPop,a.x,a.y-34,"rch",150)}),E(()=>T.reach(),150)}function st(e,t){let a=e.length,o=[...new Set(e.flatMap(i=>F[i]))],s=Math.min(70,300/o.length);o.forEach((i,l)=>E(()=>{Ae(i,0);let v=pe(L[i]);Q(v.x,v.y,14,{speed:5,g:.15,life:30,size:.7,star:.8}),T.note(l+3)},l*s)),E(()=>{p("bT").textContent=a>=3?d.bannerTriple:a===2?d.bannerDouble:d.bannerSingle,p("bS").textContent=a>1?d.bannerMulti(a):d.bannerNth(w.length),P(p("banner"),"pkb-show"),c||(P(p("flash"),"pkb-go"),P(x,"pkb-punch"));let i=pe(S);Q(i.x,i.y,a>1?180:110,{speed:a>1?16:13,star:.3}),T.bingo(a),ue(a>1?[40,60,40,60,90]:[40,60,40]),t&&E(()=>Re(t,i.x,i.y+105,"big"),350)},o.length*s+60)}function pt(e){let t=me?0:1e3;me=!0,z+=t,L.forEach((o,s)=>Ae(s,((s>>2)+(s&3))*60)),T.perfect(),c||P(p("flash"),"pkb-go"),p("sScore").textContent="0",p("sCombo").textContent="\xD70",p("sBingo").textContent="0",E(()=>{P(N,"pkb-show"),p("close").focus({preventScroll:!0}),E(()=>{c||P(x,"pkb-shake"),T.boom(),Q(0,G,90,{angle:-Math.PI/3,spread:.8,speed:22,g:.32,star:.3}),Q(H,G,90,{angle:-Math.PI*2/3,spread:.8,speed:22,g:.32,star:.3}),et(3500),ue([60,80,60,80,200]),t&&le(d.perfectBonus(t),H/2,G*.16,"big"),he(p("sScore"),0,z,1300,o=>o.toLocaleString(),!0),he(p("sCombo"),0,X,700,o=>"\xD7"+o),he(p("sBingo"),0,w.length,700,String),ge()},720),[1300,1650,2e3,2350,2700].forEach(o=>E(()=>nt(H*(.15+Math.random()*.7),G*(.12+Math.random()*.3)),o)),E(()=>tt(3e3),1500)},520);let a={score:z,maxCombo:X};e.push(()=>n.onPerfect?.(a))}function Ie(){A++,y.forEach(e=>clearTimeout(e)),y.clear(),cancelAnimationFrame(ce),te=z,p("sc").textContent=z.toLocaleString()}function lt(){D=[],cancelAnimationFrame(K),K=0,f?.clearRect(0,0,I.width,I.height),x.querySelectorAll(".pkb-float, .pkb-ring").forEach(e=>e.remove()),p("banner").classList.remove("pkb-show"),p("flash").classList.remove("pkb-go"),p("combo").classList.remove("pkb-on"),N.classList.remove("pkb-show")}function xe(e,t){let a=[],o=h,s=o.filter(Boolean).length;w=F.flatMap((u,O)=>u.every(we=>o[we])?[O]:[]);let i=w.filter(u=>!ie.has(u));[...ie].some(u=>!w.includes(u))&&(Ie(),s<16&&N.classList.remove("pkb-show"));let l=new Set(w.flatMap(u=>F[u])),v=new Set;for(let u of F){let O=u.filter(we=>!o[we]);O.length===1&&v.add(O[0])}L.forEach((u,O)=>{u.classList.toggle("pkb-on",o[O]),u.classList.toggle("pkb-line",l.has(O)),u.classList.toggle("pkb-reach",v.has(O))}),x.classList.toggle("pkb-all",s===16),Be.classList.toggle("pkb-gold",s===16),Be.textContent=s===16?d.perfectTag:d.tag,p("cnt").textContent=`${s}/16`,p("bng").textContent=String(w.length);let q=p("bp");w.length>Me&&(q.classList.remove("pkb-hot"),q.offsetWidth),q.classList.toggle("pkb-hot",w.length>0),Me=w.length,je(i),e!==null&&rt(e,o[e]);let j=i.filter(u=>!J.has(u));j.forEach(u=>J.add(u));let Y=100*j.length*j.length;if(z+=Y,t)Y&&ge();else if(s===16&&!se)pt(a);else if(i.length){st(i,Y);let u={newLines:i,totalLines:w.length,score:z};a.push(()=>n.onBingo?.(u))}else if(e!==null&&o[e]){let u=[...v].filter(O=>!de.has(O));u.length&&it(u)}ie=new Set(w),de=v,se=s===16;let ee=[...v].map(u=>k[u].label);return p("msg").textContent=s===16?d.statusDone:ee.length?d.statusReach(ee):w.length?d.statusLines(w.length,16-s):d.statusStart(16-s),a}let Ne=e=>{for(let t of e){if(b)return;t()}},De=()=>{$&&Ze($,JSON.stringify(h))};function He(e,t,a){h=e,re.forEach((s,i)=>s.checked=h[i]);let o=xe(t,a);return De(),o}function ve(e){Ie(),lt(),h=e,re.forEach((t,a)=>t.checked=h[a]),ne.clear(),J.clear(),h.forEach((t,a)=>t&&ne.add(a)),F.forEach((t,a)=>t.every(o=>h[o])&&J.add(a)),ie=new Set(J),de=new Set,se=h.every(Boolean),me=se,z=te=ne.size*10+J.size*100,B=0,X=0,p("sc").textContent=z.toLocaleString(),xe(null,!0),De()}function Z(e,t,a={}){if(m){re.forEach((s,i)=>s.checked=h[i]),n.onChange?.(e.slice());return}let o=[];a.reset?ve(e):o=He(e,t,!!a.quiet),n.onChange?.(h.slice()),Ne(o)}if(C.addEventListener("change",e=>{let t=re.indexOf(e.target);if(t<0)return;let a=h.slice();a[t]=!a[t],Z(a,t)}),p("reset").addEventListener("click",()=>Z(_(void 0),null,{reset:!0})),p("close").addEventListener("click",()=>N.classList.remove("pkb-show")),N.addEventListener("click",e=>{e.target===N&&N.classList.remove("pkb-show")}),R){let e=!1,t=()=>{h.every(Boolean)?Z(_(void 0),null,{reset:!0}):N.classList.remove("pkb-show")},a=async o=>{if(!e){e=!0;try{t();let s=A;for(let i of o){if(await M(i.delay),b||s!==A)return;let l=h.slice();l[i.i]=!0,Z(l,i.i,{quiet:i.quiet})}}finally{e=!1}}};p("dB").addEventListener("click",()=>{let o=()=>F.map(i=>i.filter(l=>!h[l])).filter(i=>i.length).sort((i,l)=>i.length-l.length)[0]??[],s=h.every(Boolean)?[...F[0]]:o();a(s.map(i=>({i,delay:320})))}),p("dP").addEventListener("click",()=>{let o=h.every(Boolean)?L.map((s,i)=>i):L.map((s,i)=>i).filter(s=>!h[s]);a(o.map((s,i)=>i<o.length-1?{i:s,delay:170,quiet:!0}:{i:s,delay:450}))}),p("dR").addEventListener("click",()=>{e||Z(Je(),null,{reset:!0})})}let oe=n.defaultChecked?_(n.defaultChecked):null;if(!oe&&$)try{let e=JSON.parse(Qe($)??"null");Array.isArray(e)&&(oe=_(e))}catch{}!oe&&R&&(oe=Je()),Pe(),ve(oe??_(void 0));let ye=typeof ResizeObserver=="function"?new ResizeObserver(()=>{Pe(),je()}):null;return ye?.observe(x),ye?.observe(S),{setChecked(e,t={}){if(b)return;let a=_(e),o=a.flatMap((s,i)=>s!==h[i]?[i]:[]);if(o.length){if(!t.animate||o.length!==1){ve(a);return}Ne(He(a,o[0],!1))}},reset(){b||Z(_(void 0),null,{reset:!0})},refresh(){b||xe(null,!0)},getState(){return{checked:h.slice(),lines:w.slice(),score:z,maxCombo:X,complete:h.every(Boolean)}},destroy(){b||(b=!0,y.forEach(e=>clearTimeout(e)),y.clear(),cancelAnimationFrame(K),cancelAnimationFrame(ce),ye?.disconnect(),T.close(),r.innerHTML="")}}}return kt(yt);})();
|
|
126
|
+
<canvas class="pkb-fx"></canvas></div>${f}</div>`}function wt(o,n={}){let m=n.items??re;if(m.length!==16)throw new Error(`[packing-bingo] "items" must have exactly 16 entries (got ${m.length}).`);let l={...Be,...n.messages};n.title!=null&&(l.title=n.title),n.tag!=null&&(l.tag=n.tag),n.subtitle!=null&&(l.subtitle=n.subtitle);let f=!!n.controlled,P=_e(n),R=!!n.demo,b=typeof matchMedia=="function"&&matchMedia("(prefers-reduced-motion: reduce)").matches,d=!1,y=new Set,E=(e,t)=>{let a=window.setTimeout(()=>{y.delete(a),d||e()},t);y.add(a)},M=e=>new Promise(t=>window.setTimeout(t,e)),A=0;vt(n.fonts!==!1),o.innerHTML=yt(m,l,R);let p=e=>{let t=o.querySelector(`[data-r="${e}"]`);if(!t)throw new Error(`[packing-bingo] internal: missing element "${e}"`);return t},x=o.querySelector(".pkb-board"),B=o.querySelector(".pkb-gw"),C=o.querySelector(".pkb-grid"),_=o.querySelector(".pkb-lines"),j=o.querySelector(".pkb-fx"),k=j.getContext("2d"),S=Array.from(C.children),ie=S.map(e=>e.querySelector("input")),Le=p("kick"),de=p("snd"),N=p("perfect"),h=K(void 0),w=[],se=new Set,me=new Set,pe=!1,Me=0,z=0,ne=0,L=0,X=0,Ce=0,fe=!1,ae=new Set,J=new Set,ze=Ze(Je),W=ze!=null?ze==="1":n.sound===!1,T=Ke(()=>W),Te=()=>{de.textContent=W?l.soundOff:l.soundOn,de.setAttribute("aria-pressed",String(!W))};de.addEventListener("click",()=>{W=!W,et(Je,W?"1":"0"),Te(),W||T.pop(4)}),Te();let D=[],V=0,oe=1,H=0,G=0,$e=()=>{let e=x.getBoundingClientRect();oe=Math.min(2,window.devicePixelRatio||1),H=e.width,G=e.height,j.width=Math.round(H*oe),j.height=Math.round(G*oe)},ke=(e,t,a,r,s,i,c,v)=>({x:e,y:t,vx:a,vy:r,g:s,life:c,shape:v,w:(5+Math.random()*6)*i,h:(3+Math.random()*5)*i,r:Math.random()*6,vr:(Math.random()-.5)*.35,c:Ye[Math.random()*Ye.length|0]}),ue=()=>{!V&&k&&(V=requestAnimationFrame(Pe))};function Q(e,t,a,r={}){if(!b){for(let s=0;s<a;s++){let i=(r.angle??-Math.PI/2)+(Math.random()-.5)*(r.spread??Math.PI*2),c=(r.speed??10)*(.35+Math.random()*.85),v=Math.random()<(r.star??.15)?"star":Math.random()<.3?"round":"rect";D.push(ke(e,t,Math.cos(i)*c,Math.sin(i)*c,r.g??.28,r.size??1,(r.life??110)+Math.random()*40,v))}ue()}}function tt(e){if(b)return;let t=performance.now()+e,a=()=>{for(let r=0;r<4;r++)D.push(ke(Math.random()*H,-12,(Math.random()-.5)*2,2+Math.random()*3,.045,1,420,Math.random()<.2?"star":"rect"));ue(),performance.now()<t&&E(a,45)};a()}function nt(e){if(b)return;let t=performance.now()+e,a=()=>{let r=ke(Math.random()*H,-30,(Math.random()-.5)*1.5,1.5+Math.random()*2,.05,1,500,"emoji");r.e=m[Math.random()*m.length|0].emoji,r.w=22+Math.random()*12,r.vr=(Math.random()-.5)*.06,D.push(r),ue(),performance.now()<t&&E(a,110)};a()}function at(e,t){Q(e,t,80,{speed:7.5,g:.06,life:85,star:.4}),T.boom()}function ot(e,t){e.beginPath();for(let a=0;a<10;a++){let r=a*Math.PI/5-Math.PI/2,s=a%2?t*.45:t;e.lineTo(Math.cos(r)*s,Math.sin(r)*s)}e.closePath(),e.fill()}function Pe(){if(V=0,!(d||!k)){k.setTransform(oe,0,0,oe,0,0),k.clearRect(0,0,H,G),D=D.filter(e=>e.life>0&&e.y<G+40);for(let e of D)e.vx*=.985,e.vy=e.vy*.985+e.g,e.x+=e.vx,e.y+=e.vy,e.r+=e.vr,e.life--,k.save(),k.globalAlpha=Math.min(1,e.life/25),k.translate(e.x,e.y),k.rotate(e.r),k.fillStyle=e.c,e.shape==="round"?(k.beginPath(),k.arc(0,0,e.h/1.6,0,7),k.fill()):e.shape==="star"?ot(k,e.w*.8):e.shape==="emoji"?(k.font=`${e.w}px "Apple Color Emoji","Segoe UI Emoji","Noto Color Emoji",sans-serif`,k.textAlign="center",k.textBaseline="middle",k.fillText(e.e??"",0,0)):(k.scale(1,Math.cos(e.r*2.3)),k.fillRect(-e.w/2,-e.h/2,e.w,e.h)),k.restore();D.length&&(V=requestAnimationFrame(Pe))}}let le=e=>{let t=x.getBoundingClientRect(),a=e.getBoundingClientRect();return{x:a.left-t.left+a.width/2,y:a.top-t.top+a.height/2}},$=(e,t)=>{e.classList.remove(t),e.offsetWidth,e.classList.add(t)},Ae=(e,t)=>{b||(S[e].classList.remove("pkb-squash"),S[e].style.setProperty("--pkb-d",t+"ms"),$(S[e],"pkb-hop"))};C.addEventListener("animationend",e=>{let t=e.target;t.classList.contains("pkb-tile")&&(e.animationName==="pkb-hop"||e.animationName==="pkb-squash")&&t.classList.remove(e.animationName)});let rt=e=>{let t=document.createElement("span");t.className="pkb-ring",e.appendChild(t),t.addEventListener("animationend",()=>t.remove())},ge=e=>{let t=navigator.userActivation;if(!(t&&!t.hasBeenActive))try{navigator.vibrate?.(e)}catch{}},ce=(e,t,a,r,s=0,i)=>{let c=document.createElement("div");c.className="pkb-float pkb-"+r,c.textContent=e,c.style.left=t+"px",c.style.top=a+"px",s&&(c.style.animationDelay=s+"ms"),i&&(c.style.color=i),x.appendChild(c),c.addEventListener("animationend",()=>c.remove())},Oe=(e,t,a)=>Math.max(0,Math.min(1,(e-t)/a)),Fe=e=>1-Math.pow(1-e,3),be=0;function he(){cancelAnimationFrame(be);let e=ne,t=z,a=performance.now(),r=s=>{if(d)return;let i=Oe(s,a,450);ne=Math.round(e+(t-e)*Fe(i)),p("sc").textContent=ne.toLocaleString(),i<1&&(be=requestAnimationFrame(r))};r(a),b||$(p("sp"),"pkb-bump")}let Re=(e,t,a,r="pt")=>{ce(`+${e}`,t,a,r),he()};function xe(e,t,a,r,s,i=!1){let c=performance.now(),v=A,q=0,I=Y=>{if(d||v!==A)return;let te=Oe(Y,c,r);e.textContent=s(Math.round(t+(a-t)*Fe(te))),i&&te<1&&Y-q>55&&(T.tick(),q=Y),te<1?requestAnimationFrame(I):b||$(e,"pkb-bump")};I(c)}let qe=e=>{let t=S[e];return{x:t.offsetLeft+t.offsetWidth/2,y:t.offsetTop+t.offsetHeight/2}};function Ie(e=[]){_.setAttribute("viewBox",`0 0 ${B.clientWidth} ${B.clientHeight}`),_.innerHTML=w.map(t=>{let a=qe(F[t][0]),r=qe(F[t][3]),s=r.x-a.x,i=r.y-a.y,c=Math.hypot(s,i)||1,v=s/c*26,q=i/c*26,I=`x1="${a.x-v}" y1="${a.y-q}" x2="${r.x+v}" y2="${r.y+q}" pathLength="1"`;return`<g${e.includes(t)?' class="pkb-draw"':""}><line class="pkb-u" ${I}/><line class="pkb-o" ${I}/></g>`}).join("")}function it(e,t){let a=le(S[e]);if(!t){L=0,p("combo").classList.remove("pkb-on"),T.off();return}let r=performance.now();L=r-Ce<xt?L+1:1,Ce=r,X=Math.max(X,L),T.pop(L-1),b||($(S[e],"pkb-squash"),rt(S[e])),Q(a.x,a.y,18,{speed:6,g:.2,life:30,size:.6,star:.5}),ae.has(e)||(ae.add(e),z+=10*L,Re(10*L,a.x,a.y-12)),L>=2&&(ce(l.comboPop(L),a.x,a.y+22,"cmb",90,Xe[(L-2)%Xe.length]),p("cmbN").textContent=l.combo(L),$(p("combo"),"pkb-on")),ge(12)}p("cbar").addEventListener("animationend",()=>{p("combo").classList.remove("pkb-on"),L=0});function st(e){e.forEach(t=>{let a=le(S[t]);ce(l.reachPop,a.x,a.y-34,"rch",150)}),E(()=>T.reach(),150)}function pt(e,t){let a=e.length,r=[...new Set(e.flatMap(i=>F[i]))],s=Math.min(70,300/r.length);r.forEach((i,c)=>E(()=>{Ae(i,0);let v=le(S[i]);Q(v.x,v.y,14,{speed:5,g:.15,life:30,size:.7,star:.8}),T.note(c+3)},c*s)),E(()=>{p("bT").textContent=a>=3?l.bannerTriple:a===2?l.bannerDouble:l.bannerSingle,p("bS").textContent=a>1?l.bannerMulti(a):l.bannerNth(w.length),$(p("banner"),"pkb-show"),b||($(p("flash"),"pkb-go"),$(x,"pkb-punch"));let i=le(B);Q(i.x,i.y,a>1?180:110,{speed:a>1?16:13,star:.3}),T.bingo(a),ge(a>1?[40,60,40,60,90]:[40,60,40]),t&&E(()=>Re(t,i.x,i.y+105,"big"),350)},r.length*s+60)}function lt(e){let t=fe?0:1e3;fe=!0,z+=t,S.forEach((r,s)=>Ae(s,((s>>2)+(s&3))*60)),T.perfect(),b||$(p("flash"),"pkb-go"),p("sScore").textContent="0",p("sCombo").textContent="\xD70",p("sBingo").textContent="0",E(()=>{$(N,"pkb-show"),p("close").focus({preventScroll:!0}),E(()=>{b||$(x,"pkb-shake"),T.boom(),Q(0,G,90,{angle:-Math.PI/3,spread:.8,speed:22,g:.32,star:.3}),Q(H,G,90,{angle:-Math.PI*2/3,spread:.8,speed:22,g:.32,star:.3}),tt(3500),ge([60,80,60,80,200]),t&&ce(l.perfectBonus(t),H/2,G*.16,"big"),xe(p("sScore"),0,z,1300,r=>r.toLocaleString(),!0),xe(p("sCombo"),0,X,700,r=>"\xD7"+r),xe(p("sBingo"),0,w.length,700,String),he()},720),[1300,1650,2e3,2350,2700].forEach(r=>E(()=>at(H*(.15+Math.random()*.7),G*(.12+Math.random()*.3)),r)),E(()=>nt(3e3),1500)},520);let a={score:z,maxCombo:X};e.push(()=>n.onPerfect?.(a))}function je(){A++,y.forEach(e=>clearTimeout(e)),y.clear(),cancelAnimationFrame(be),ne=z,p("sc").textContent=z.toLocaleString()}function ct(){D=[],cancelAnimationFrame(V),V=0,k?.clearRect(0,0,j.width,j.height),x.querySelectorAll(".pkb-float, .pkb-ring").forEach(e=>e.remove()),p("banner").classList.remove("pkb-show"),p("flash").classList.remove("pkb-go"),p("combo").classList.remove("pkb-on"),N.classList.remove("pkb-show")}function ve(e,t){let a=[],r=h,s=r.filter(Boolean).length;w=F.flatMap((u,O)=>u.every(Ee=>r[Ee])?[O]:[]);let i=w.filter(u=>!se.has(u));[...se].some(u=>!w.includes(u))&&(je(),s<16&&N.classList.remove("pkb-show"));let c=new Set(w.flatMap(u=>F[u])),v=new Set;for(let u of F){let O=u.filter(Ee=>!r[Ee]);O.length===1&&v.add(O[0])}S.forEach((u,O)=>{u.classList.toggle("pkb-on",r[O]),u.classList.toggle("pkb-line",c.has(O)),u.classList.toggle("pkb-reach",v.has(O))}),x.classList.toggle("pkb-all",s===16),Le.classList.toggle("pkb-gold",s===16),Le.textContent=s===16?l.perfectTag:l.tag,p("cnt").textContent=`${s}/16`,p("bng").textContent=String(w.length);let q=p("bp");w.length>Me&&(q.classList.remove("pkb-hot"),q.offsetWidth),q.classList.toggle("pkb-hot",w.length>0),Me=w.length,Ie(i),e!==null&&it(e,r[e]);let I=i.filter(u=>!J.has(u));I.forEach(u=>J.add(u));let Y=100*I.length*I.length;if(z+=Y,t)Y&&he();else if(s===16&&!pe)lt(a);else if(i.length){pt(i,Y);let u={newLines:i,totalLines:w.length,score:z};a.push(()=>n.onBingo?.(u))}else if(e!==null&&r[e]){let u=[...v].filter(O=>!me.has(O));u.length&&st(u)}se=new Set(w),me=v,pe=s===16;let te=[...v].map(u=>m[u].label);return p("msg").textContent=s===16?l.statusDone:te.length?l.statusReach(te):w.length?l.statusLines(w.length,16-s):l.statusStart(16-s),a}let Ne=e=>{for(let t of e){if(d)return;t()}},De=()=>{P&&et(P,JSON.stringify(h))};function He(e,t,a){h=e,ie.forEach((s,i)=>s.checked=h[i]);let r=ve(t,a);return De(),r}function ye(e){je(),ct(),h=e,ie.forEach((t,a)=>t.checked=h[a]),ae.clear(),J.clear(),h.forEach((t,a)=>t&&ae.add(a)),F.forEach((t,a)=>t.every(r=>h[r])&&J.add(a)),se=new Set(J),me=new Set,pe=h.every(Boolean),fe=pe,z=ne=ae.size*10+J.size*100,L=0,X=0,p("sc").textContent=z.toLocaleString(),ve(null,!0),De()}function Z(e,t,a={}){if(f){ie.forEach((s,i)=>s.checked=h[i]),n.onChange?.(e.slice());return}let r=[];a.reset?ye(e):r=He(e,t,!!a.quiet),n.onChange?.(h.slice()),Ne(r)}if(C.addEventListener("change",e=>{let t=ie.indexOf(e.target);if(t<0)return;let a=h.slice();a[t]=!a[t],Z(a,t)}),p("reset").addEventListener("click",()=>Z(K(void 0),null,{reset:!0})),p("close").addEventListener("click",()=>N.classList.remove("pkb-show")),N.addEventListener("click",e=>{e.target===N&&N.classList.remove("pkb-show")}),R){let e=!1,t=()=>{h.every(Boolean)?Z(K(void 0),null,{reset:!0}):N.classList.remove("pkb-show")},a=async r=>{if(!e){e=!0;try{t();let s=A;for(let i of r){if(await M(i.delay),d||s!==A)return;let c=h.slice();c[i.i]=!0,Z(c,i.i,{quiet:i.quiet})}}finally{e=!1}}};p("dB").addEventListener("click",()=>{let r=()=>F.map(i=>i.filter(c=>!h[c])).filter(i=>i.length).sort((i,c)=>i.length-c.length)[0]??[],s=h.every(Boolean)?[...F[0]]:r();a(s.map(i=>({i,delay:320})))}),p("dP").addEventListener("click",()=>{let r=h.every(Boolean)?S.map((s,i)=>i):S.map((s,i)=>i).filter(s=>!h[s]);a(r.map((s,i)=>i<r.length-1?{i:s,delay:170,quiet:!0}:{i:s,delay:450}))}),p("dR").addEventListener("click",()=>{e||Z(Qe(),null,{reset:!0})})}let ee=null;if(P)try{let e=JSON.parse(Ze(P)??"null");Array.isArray(e)&&(ee=K(e))}catch{}!ee&&n.defaultChecked&&(ee=K(n.defaultChecked)),!ee&&R&&(ee=Qe()),$e(),ye(ee??K(void 0));let we=typeof ResizeObserver=="function"?new ResizeObserver(()=>{$e(),Ie()}):null;return we?.observe(x),we?.observe(B),{setChecked(e,t={}){if(d)return;let a=K(e),r=a.flatMap((s,i)=>s!==h[i]?[i]:[]);if(r.length){if(!t.animate||r.length!==1){ye(a);return}Ne(He(a,r[0],!1))}},reset(){d||Z(K(void 0),null,{reset:!0})},refresh(){d||ve(null,!0)},getState(){return{checked:h.slice(),lines:w.slice(),score:z,maxCombo:X,complete:h.every(Boolean)}},destroy(){d||(d=!0,y.forEach(e=>clearTimeout(e)),y.clear(),cancelAnimationFrame(V),cancelAnimationFrame(be),we?.disconnect(),T.close(),o.innerHTML="")}}}return ut(Et);})();
|
package/package.json
CHANGED