split-flap-board 0.0.3 → 0.0.5
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 +179 -112
- package/dist/cjs/SplitFlapBoard.js +53 -15
- package/dist/cjs/lib/board.js +1 -1
- package/dist/cjs/lib/flap.js +1 -1
- package/dist/cjs/spools/SplitFlapSpool.js +2 -2
- package/dist/cjs/spools/SplitFlapSpoolBase.js +3 -3
- package/dist/cjs/spools/SplitFlapSpoolMinimal.js +8 -8
- package/dist/cjs/spools/SplitFlapSpoolRealistic.js +21 -9
- package/dist/esm/SplitFlapBoard.js +53 -15
- package/dist/esm/lib/board.js +1 -1
- package/dist/esm/lib/flap.js +1 -1
- package/dist/esm/spools/SplitFlapSpool.js +3 -3
- package/dist/esm/spools/SplitFlapSpoolBase.js +7 -7
- package/dist/esm/spools/SplitFlapSpoolMinimal.js +8 -8
- package/dist/esm/spools/SplitFlapSpoolRealistic.js +22 -10
- package/dist/types/SplitFlapBoard.d.ts +5 -5
- package/dist/types/lib/board.d.ts +2 -10
- package/dist/types/lib/flap.d.ts +1 -1
- package/dist/types/spools/SplitFlapSpool.d.ts +3 -2
- package/dist/types/spools/SplitFlapSpoolBase.d.ts +6 -3
- package/dist/types/spools/SplitFlapSpoolRealistic.d.ts +4 -4
- package/dist/types/types.d.ts +18 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,67 +17,154 @@
|
|
|
17
17
|
</a>
|
|
18
18
|
</p>
|
|
19
19
|
|
|
20
|
-
Web component
|
|
20
|
+
Web component library for split-flap displays, the mechanical boards found in airports and train stations. Built with [Lit](https://lit.dev/), it works in any framework or plain HTML.
|
|
21
|
+
|
|
22
|
+
- **Simple mental model**: flap -> spool -> board
|
|
23
|
+
- **Framework friendly**: works in plain HTML and can be used from React or other frameworks
|
|
24
|
+
- **Flexible content**: supports character, color, image, and custom flaps
|
|
25
|
+
- **Two built-in looks**: `minimal` and `realistic`
|
|
21
26
|
|
|
22
27
|
## How a Split-Flap Display Works
|
|
23
28
|
|
|
24
29
|
> [How a Split-Flap Display Works (YouTube)](https://www.youtube.com/watch?v=UAQJJAQSg_g)
|
|
25
30
|
|
|
26
|
-
A split-flap display
|
|
31
|
+
A split-flap display, also called a Solari board, works through a simple mechanical loop:
|
|
27
32
|
|
|
28
|
-
- A **spool** (drum) holds a series of **flaps** (thin cards), each printed with the top half of one character on the front and the bottom half of
|
|
33
|
+
- A **spool** (drum) holds a series of **flaps** (thin cards), each printed with the top half of one character on the front and the bottom half of another on the back.
|
|
29
34
|
- A **stepper motor** rotates the spool precisely. As each flap passes vertical, gravity pulls it down, snapping it against a **backstop** and creating the characteristic clacking sound.
|
|
30
|
-
- This reveal happens one flap at a time, so going from `A` to `Z` means cycling through every character in between. The order of the flaps on the spool is fixed
|
|
31
|
-
- A **hall effect sensor** and magnet
|
|
35
|
+
- This reveal happens one flap at a time, so going from `A` to `Z` means cycling through every character in between. The order of the flaps on the spool is fixed.
|
|
36
|
+
- A **hall effect sensor** and magnet give the controller a consistent home position, so it always knows which character is showing even after a power cycle.
|
|
37
|
+
|
|
38
|
+
In code, each `<split-flap-spool>` mirrors that behavior: it holds an ordered sequence of **flaps** and steps forward through them until it reaches a target key.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pnpm add split-flap-board
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Quick Start
|
|
47
|
+
|
|
48
|
+
### Board
|
|
49
|
+
|
|
50
|
+
```html
|
|
51
|
+
<script type="module">
|
|
52
|
+
import 'split-flap-board';
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<split-flap-board></split-flap-board>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
import { fromLines } from 'split-flap-board';
|
|
32
60
|
|
|
33
|
-
|
|
61
|
+
const board = document.querySelector('split-flap-board');
|
|
62
|
+
const { spools, grid } = fromLines(['HELLO WORLD'], 11);
|
|
63
|
+
|
|
64
|
+
board.spools = spools;
|
|
65
|
+
board.grid = grid;
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
For most apps, this is the easiest way to start:
|
|
69
|
+
|
|
70
|
+
1. build `spools` once
|
|
71
|
+
2. update `grid` whenever the displayed text changes
|
|
72
|
+
|
|
73
|
+
### Single Spool
|
|
74
|
+
|
|
75
|
+
```html
|
|
76
|
+
<script type="module">
|
|
77
|
+
import 'split-flap-board';
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
<split-flap-spool value="A"></split-flap-spool>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```js
|
|
84
|
+
const spool = document.querySelector('split-flap-spool');
|
|
85
|
+
spool.value = 'Z'; // steps forward: A -> B -> ... -> Z
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Switch to the realistic look:
|
|
89
|
+
|
|
90
|
+
```html
|
|
91
|
+
<split-flap-spool variant="realistic" value="A"></split-flap-spool>
|
|
92
|
+
```
|
|
34
93
|
|
|
35
94
|
## Core Concepts
|
|
36
95
|
|
|
37
96
|
### Flap
|
|
38
97
|
|
|
39
|
-
A flap is one card on the spool, the
|
|
98
|
+
A flap is one card on the spool, the smallest unit of display content. The library ships with four built-in flap types:
|
|
40
99
|
|
|
41
100
|
```ts
|
|
42
101
|
// Character (default)
|
|
43
|
-
{
|
|
102
|
+
{
|
|
103
|
+
type: 'char';
|
|
104
|
+
key?: string;
|
|
105
|
+
value: string;
|
|
106
|
+
color?: string;
|
|
107
|
+
bg?: string;
|
|
108
|
+
fontSize?: string;
|
|
109
|
+
fontFamily?: string;
|
|
110
|
+
fontWeight?: string;
|
|
111
|
+
}
|
|
44
112
|
|
|
45
113
|
// Solid color
|
|
46
|
-
{
|
|
114
|
+
{
|
|
115
|
+
type: 'color';
|
|
116
|
+
key?: string;
|
|
117
|
+
value: string;
|
|
118
|
+
}
|
|
47
119
|
|
|
48
120
|
// Image
|
|
49
|
-
{
|
|
121
|
+
{
|
|
122
|
+
type: 'image';
|
|
123
|
+
key?: string;
|
|
124
|
+
src: string;
|
|
125
|
+
alt?: string;
|
|
126
|
+
}
|
|
50
127
|
|
|
51
|
-
// Custom
|
|
52
|
-
{
|
|
128
|
+
// Custom, top and bottom halves rendered independently
|
|
129
|
+
{
|
|
130
|
+
type: 'custom';
|
|
131
|
+
key: string;
|
|
132
|
+
top: TemplateResult;
|
|
133
|
+
bottom: TemplateResult;
|
|
134
|
+
}
|
|
53
135
|
```
|
|
54
136
|
|
|
55
|
-
The `key` field is optional on all types except `custom`. When omitted
|
|
137
|
+
The `key` field is optional on all types except `custom`. When omitted, the library uses the natural identifier:
|
|
138
|
+
|
|
139
|
+
- `value` for `char` and `color`
|
|
140
|
+
- `src` for `image`
|
|
56
141
|
|
|
57
|
-
Char flaps
|
|
142
|
+
Char flaps can be styled per flap when needed:
|
|
58
143
|
|
|
59
144
|
```ts
|
|
60
|
-
const
|
|
61
|
-
|
|
145
|
+
const styledSpool = charSpool.map((flap) =>
|
|
146
|
+
flap.type === 'char' ? { ...flap, fontSize: '3rem', color: '#fff', bg: '#2563eb' } : flap
|
|
62
147
|
);
|
|
63
|
-
|
|
148
|
+
|
|
149
|
+
spool.flaps = styledSpool;
|
|
64
150
|
```
|
|
65
151
|
|
|
66
152
|
### Spool
|
|
67
153
|
|
|
68
|
-
A spool is an ordered array of flaps, the sequence a `<split-flap-spool>` steps through. Define it once
|
|
154
|
+
A spool is an ordered array of flaps, the sequence a `<split-flap-spool>` steps through. Define it once and reuse it anywhere.
|
|
69
155
|
|
|
70
156
|
```ts
|
|
71
157
|
type TSpool = TFlap[];
|
|
72
158
|
```
|
|
73
159
|
|
|
74
|
-
The library ships built-in spools:
|
|
160
|
+
The library ships with built-in spools:
|
|
75
161
|
|
|
76
162
|
```ts
|
|
77
|
-
import { charSpool, numericSpool } from 'split-flap-board';
|
|
163
|
+
import { charSpool, colorSpool, numericSpool } from 'split-flap-board';
|
|
78
164
|
|
|
79
|
-
// charSpool
|
|
80
|
-
// numericSpool
|
|
165
|
+
// charSpool -> [' ', A-Z, 0-9, . - / : ]
|
|
166
|
+
// numericSpool -> [' ', 0-9]
|
|
167
|
+
// colorSpool -> named color keys such as 'red', 'green', 'blue'
|
|
81
168
|
```
|
|
82
169
|
|
|
83
170
|
Custom spools are just arrays:
|
|
@@ -91,7 +178,7 @@ const statusSpool: TSpool = [
|
|
|
91
178
|
];
|
|
92
179
|
```
|
|
93
180
|
|
|
94
|
-
You can mix flap types within a spool:
|
|
181
|
+
You can also mix flap types within a single spool:
|
|
95
182
|
|
|
96
183
|
```ts
|
|
97
184
|
const mixedSpool: TSpool = [
|
|
@@ -102,12 +189,17 @@ const mixedSpool: TSpool = [
|
|
|
102
189
|
];
|
|
103
190
|
```
|
|
104
191
|
|
|
105
|
-
### Spools
|
|
192
|
+
### Spools Grid vs. Target Grid
|
|
106
193
|
|
|
107
194
|
A board has two separate grids:
|
|
108
195
|
|
|
109
|
-
- **`spools
|
|
110
|
-
- **`grid
|
|
196
|
+
- **`spools`**: a `TSpool[][]` that defines what each cell can show. This is usually set once.
|
|
197
|
+
- **`grid`**: a `string[][]` of target keys that defines what each cell should show right now. This is the part you usually update at runtime.
|
|
198
|
+
|
|
199
|
+
If you are unsure which one to change, use this rule of thumb:
|
|
200
|
+
|
|
201
|
+
- change `spools` when the available flap set changes
|
|
202
|
+
- change `grid` when the displayed content changes
|
|
111
203
|
|
|
112
204
|
```ts
|
|
113
205
|
import { charSpool, spoolGrid } from 'split-flap-board';
|
|
@@ -115,10 +207,10 @@ import { charSpool, spoolGrid } from 'split-flap-board';
|
|
|
115
207
|
// spoolGrid(spool, cols, rows) fills a uniform TSpool[][]
|
|
116
208
|
board.spools = spoolGrid(charSpool, 10, 3);
|
|
117
209
|
|
|
118
|
-
//
|
|
210
|
+
// Per-column: pass an array of spools, one per column. Shorter arrays repeat.
|
|
119
211
|
board.spools = spoolGrid([charSpool, charSpool, statusSpool], 3, 2);
|
|
120
212
|
|
|
121
|
-
//
|
|
213
|
+
// Fully custom: build the 2D array directly.
|
|
122
214
|
board.spools = [
|
|
123
215
|
[charSpool, charSpool, statusSpool],
|
|
124
216
|
[charSpool, charSpool, statusSpool]
|
|
@@ -135,65 +227,16 @@ Board dimensions are inferred from `spools`.
|
|
|
135
227
|
|
|
136
228
|
## Usage
|
|
137
229
|
|
|
138
|
-
###
|
|
230
|
+
### Updating at Runtime
|
|
139
231
|
|
|
140
|
-
|
|
141
|
-
<script type="module">
|
|
142
|
-
import 'split-flap-board';
|
|
143
|
-
</script>
|
|
144
|
-
|
|
145
|
-
<split-flap-spool value="A"></split-flap-spool>
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
```js
|
|
149
|
-
const spool = document.querySelector('split-flap-spool');
|
|
150
|
-
spool.value = 'Z'; // steps forward: A → B → ... → Z
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
Switch to the realistic look:
|
|
154
|
-
|
|
155
|
-
```html
|
|
156
|
-
<split-flap-spool variant="realistic" value="A"></split-flap-spool>
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
Custom spool:
|
|
160
|
-
|
|
161
|
-
```js
|
|
162
|
-
import { statusSpool } from './my-spools';
|
|
163
|
-
|
|
164
|
-
spool.flaps = statusSpool;
|
|
165
|
-
spool.value = 'green';
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
### Board
|
|
169
|
-
|
|
170
|
-
```html
|
|
171
|
-
<script type="module">
|
|
172
|
-
import 'split-flap-board';
|
|
173
|
-
</script>
|
|
174
|
-
|
|
175
|
-
<split-flap-board></split-flap-board>
|
|
176
|
-
```
|
|
232
|
+
For content changes, only `grid` needs to change. Assign a new array reference:
|
|
177
233
|
|
|
178
234
|
```js
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const board = document.querySelector('split-flap-board');
|
|
182
|
-
const { spools, grid } = fromLines(['HELLO WORLD'], 11);
|
|
183
|
-
board.spools = spools;
|
|
184
|
-
board.grid = grid;
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
### Updating at runtime
|
|
188
|
-
|
|
189
|
-
Only `grid` needs to change for content updates. Assign a new array reference:
|
|
190
|
-
|
|
191
|
-
```js
|
|
192
|
-
// refresh content - spools stay the same
|
|
235
|
+
// Refresh content, spools stay the same.
|
|
193
236
|
board.grid = [['G', 'O', 'O', 'D', 'B', 'Y', 'E', ' ', ' ', ' ', ' ']];
|
|
194
237
|
```
|
|
195
238
|
|
|
196
|
-
### Multi-
|
|
239
|
+
### Multi-Row Board
|
|
197
240
|
|
|
198
241
|
```js
|
|
199
242
|
import { fromLines } from 'split-flap-board';
|
|
@@ -207,7 +250,20 @@ board.spools = spools;
|
|
|
207
250
|
board.grid = grid;
|
|
208
251
|
```
|
|
209
252
|
|
|
210
|
-
###
|
|
253
|
+
### Custom Spool
|
|
254
|
+
|
|
255
|
+
```js
|
|
256
|
+
const statusSpool = [
|
|
257
|
+
{ type: 'color', value: '#111', key: 'off' },
|
|
258
|
+
{ type: 'color', value: '#16a34a', key: 'green' },
|
|
259
|
+
{ type: 'color', value: '#dc2626', key: 'red' }
|
|
260
|
+
];
|
|
261
|
+
|
|
262
|
+
spool.flaps = statusSpool;
|
|
263
|
+
spool.value = 'green';
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Mixed Spools per Column
|
|
211
267
|
|
|
212
268
|
```js
|
|
213
269
|
import { charSpool, spoolGrid } from 'split-flap-board';
|
|
@@ -226,7 +282,7 @@ board.grid = [
|
|
|
226
282
|
];
|
|
227
283
|
```
|
|
228
284
|
|
|
229
|
-
### Colored
|
|
285
|
+
### Colored Rows
|
|
230
286
|
|
|
231
287
|
```js
|
|
232
288
|
import { fromLines } from 'split-flap-board';
|
|
@@ -244,6 +300,8 @@ board.spools = spools;
|
|
|
244
300
|
board.grid = grid;
|
|
245
301
|
```
|
|
246
302
|
|
|
303
|
+
Use row colors when you want text-style boards with a highlighted row, for example boarding status or delays.
|
|
304
|
+
|
|
247
305
|
### React
|
|
248
306
|
|
|
249
307
|
```tsx
|
|
@@ -264,6 +322,7 @@ export function DeparturesBoard() {
|
|
|
264
322
|
|
|
265
323
|
useEffect(() => {
|
|
266
324
|
if (ref.current == null) return;
|
|
325
|
+
|
|
267
326
|
const { spools, grid } = fromLines(['DEPARTURES'], 10);
|
|
268
327
|
(ref.current as any).spools = spools;
|
|
269
328
|
(ref.current as any).grid = grid;
|
|
@@ -273,6 +332,8 @@ export function DeparturesBoard() {
|
|
|
273
332
|
}
|
|
274
333
|
```
|
|
275
334
|
|
|
335
|
+
For richer framework integrations, the simplest approach is usually to keep `spools` stable and only update `grid`.
|
|
336
|
+
|
|
276
337
|
## API Reference
|
|
277
338
|
|
|
278
339
|
### `<split-flap-spool>`
|
|
@@ -298,9 +359,9 @@ The variant elements can also be used directly if you prefer not to use the wrap
|
|
|
298
359
|
|
|
299
360
|
| Property | Type | Default | Description |
|
|
300
361
|
| ------------------ | -------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
301
|
-
| `spools` | `TSpool[][]` |
|
|
362
|
+
| `spools` | `TSpool[][]` | `[]` | 2D spool configuration, one per cell. Board dimensions are inferred from this. Always assign a new array reference to update. |
|
|
302
363
|
| `grid` | `string[][]` | `[]` | 2D array of target keys. Always assign a new array reference to trigger a re-render. |
|
|
303
|
-
| `speed` | `number` | `60` | Flip speed in milliseconds forwarded to every child spool.
|
|
364
|
+
| `speed` | `number` | `60` | Flip speed in milliseconds forwarded to every child spool. |
|
|
304
365
|
| `variant` | `'minimal' \| 'realistic'` | `'minimal'` | Visual variant forwarded to every child spool. |
|
|
305
366
|
| `visibleSideCount` | `number` | `-1` | Forwarded to child spools. Only affects the `realistic` variant. |
|
|
306
367
|
|
|
@@ -333,10 +394,10 @@ function spoolGrid(spool: TSpool | TSpool[], cols: number, rows: number): TSpool
|
|
|
333
394
|
Creates a `TSpool[][]` for use with `board.spools`.
|
|
334
395
|
|
|
335
396
|
```ts
|
|
336
|
-
// Uniform
|
|
397
|
+
// Uniform, same spool for every cell
|
|
337
398
|
spoolGrid(charSpool, 10, 3);
|
|
338
399
|
|
|
339
|
-
// Per-column
|
|
400
|
+
// Per-column, pass an array where index = column. Shorter arrays repeat.
|
|
340
401
|
spoolGrid([charSpool, charSpool, statusSpool], 3, 2);
|
|
341
402
|
```
|
|
342
403
|
|
|
@@ -349,7 +410,7 @@ function fromLines(
|
|
|
349
410
|
): { spools: TSpool[][]; grid: string[][] };
|
|
350
411
|
```
|
|
351
412
|
|
|
352
|
-
Creates a char `spools` grid and a `grid` of target keys from an array of
|
|
413
|
+
Creates a char `spools` grid and a `grid` of target keys from an array of lines. Each line is uppercased, padded with spaces, or truncated to `cols`. Rows with `bg` or `color` get those values baked into their char flaps.
|
|
353
414
|
|
|
354
415
|
```ts
|
|
355
416
|
const { spools, grid } = fromLines(
|
|
@@ -363,15 +424,22 @@ board.grid = grid;
|
|
|
363
424
|
|
|
364
425
|
### CSS Custom Properties
|
|
365
426
|
|
|
366
|
-
Set these on the board to theme all spools at once, or override on individual spools via CSS selectors.
|
|
427
|
+
Set these on the board to theme all spools at once, or override them on individual spools via CSS selectors.
|
|
367
428
|
|
|
368
429
|
```css
|
|
369
|
-
/*
|
|
430
|
+
/* Board panel */
|
|
431
|
+
split-flap-board {
|
|
432
|
+
--sfb-board-bg: #1c1c1c; /* panel and frame background */
|
|
433
|
+
--sfb-board-padding: 10px; /* inset spacing around the cell grid */
|
|
434
|
+
--sfb-board-radius: 8px; /* corner radius of the panel itself */
|
|
435
|
+
--sfb-gap: 3px; /* gap between spool cells */
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/* Shared flap styles */
|
|
370
439
|
split-flap-board {
|
|
371
|
-
--sfb-bg: #111; /* flap background */
|
|
372
|
-
--sfb-color: #f5f0e0; /* flap text color */
|
|
373
|
-
--sfb-
|
|
374
|
-
--sfb-gap: 2px; /* gap between spool cells */
|
|
440
|
+
--sfb-flap-bg: #111; /* flap background */
|
|
441
|
+
--sfb-flap-color: #f5f0e0; /* flap text color */
|
|
442
|
+
--sfb-flap-radius: 4px; /* corner radius on each flap */
|
|
375
443
|
}
|
|
376
444
|
|
|
377
445
|
/* Minimal variant */
|
|
@@ -383,46 +451,46 @@ split-flap-board {
|
|
|
383
451
|
|
|
384
452
|
/* Realistic variant */
|
|
385
453
|
split-flap-board {
|
|
386
|
-
--sfb-spool-width: 1em; /* flap width
|
|
387
|
-
--sfb-spool-height: 2em; /* flap height
|
|
388
|
-
--sfb-drum-radius: 0px; /* cylinder radius
|
|
454
|
+
--sfb-spool-width: 1em; /* flap width, defaults to 1x font-size */
|
|
455
|
+
--sfb-spool-height: 2em; /* flap height, defaults to 2x font-size */
|
|
456
|
+
--sfb-drum-radius: 0px; /* cylinder radius, 0 keeps the flip flat */
|
|
389
457
|
--sfb-crease: 1px; /* gap between the two flap halves */
|
|
390
458
|
--sfb-perspective: 400px; /* CSS perspective depth */
|
|
391
459
|
--sfb-view-transform: none; /* e.g. rotateY(-30deg) */
|
|
392
|
-
--sfb-max-step-angle: 1turn; /* per-step angle cap
|
|
393
|
-
--sfb-flap-border: #2a2a2a; /* border on each flap card
|
|
460
|
+
--sfb-max-step-angle: 1turn; /* per-step angle cap, 8deg tightens small spools */
|
|
461
|
+
--sfb-flap-border: #2a2a2a; /* border on each flap card */
|
|
394
462
|
}
|
|
395
463
|
|
|
396
464
|
/* Per-spool override */
|
|
397
465
|
split-flap-spool.highlight {
|
|
398
|
-
--sfb-bg: #16a34a;
|
|
399
|
-
--sfb-color: #fff;
|
|
466
|
+
--sfb-flap-bg: #16a34a;
|
|
467
|
+
--sfb-flap-color: #fff;
|
|
400
468
|
}
|
|
401
469
|
```
|
|
402
470
|
|
|
403
471
|
## Behavior
|
|
404
472
|
|
|
405
|
-
### Initial
|
|
473
|
+
### Initial State
|
|
406
474
|
|
|
407
|
-
Before `value` is set, a `<split-flap-spool>` shows the first flap in its sequence
|
|
475
|
+
Before `value` is set, a `<split-flap-spool>` shows the first flap in its sequence. For `charSpool`, that is a space. This mirrors the physical home position a real board establishes on startup.
|
|
408
476
|
|
|
409
477
|
### Animation
|
|
410
478
|
|
|
411
|
-
Each flap step plays a fold animation where the top half falls away
|
|
479
|
+
Each flap step plays a fold animation where the top half falls away and reveals the next card underneath. The animation duration is derived from `speed`, so it always fits inside one step interval.
|
|
412
480
|
|
|
413
|
-
### Unknown
|
|
481
|
+
### Unknown Key
|
|
414
482
|
|
|
415
|
-
If `value` is set to a key that does not exist in `flaps`, the spool does not start a new search and no error is thrown. If
|
|
483
|
+
If `value` is set to a key that does not exist in `flaps`, the spool does not start a new search and no error is thrown. If this happens during an in-flight animation, the current flip finishes and `settled` reports the flap the spool actually landed on.
|
|
416
484
|
|
|
417
|
-
### Retargeting
|
|
485
|
+
### Retargeting During Motion
|
|
418
486
|
|
|
419
487
|
If `value` changes to another valid key while the spool is already moving, the spool keeps its current forward motion and retargets to the newest valid key. It does not snap backward or restart from the beginning.
|
|
420
488
|
|
|
421
|
-
### Spool
|
|
489
|
+
### Spool Changes During Motion
|
|
422
490
|
|
|
423
491
|
If `flaps` changes while the spool is moving, the component remaps the currently visible flap by key into the new spool, clears stale animation bookkeeping, and continues from the new coherent state.
|
|
424
492
|
|
|
425
|
-
### Grid
|
|
493
|
+
### Grid Size Mismatch
|
|
426
494
|
|
|
427
495
|
If `grid` has more rows or columns than `spools`, the extra entries are ignored. If `grid` is smaller than `spools`, spools without a matching target key stay on their current flap. No errors are thrown.
|
|
428
496
|
|
|
@@ -431,8 +499,8 @@ If `grid` has more rows or columns than `spools`, the extra entries are ignored.
|
|
|
431
499
|
Because a spool only rotates forward, the number of steps depends on the distance ahead in the spool, wrapping around if needed.
|
|
432
500
|
|
|
433
501
|
```
|
|
434
|
-
'A'
|
|
435
|
-
'Z'
|
|
502
|
+
'A' -> 'C' = 2 steps
|
|
503
|
+
'Z' -> 'B' = 3 steps (wraps: Z -> ' ' -> A -> B)
|
|
436
504
|
```
|
|
437
505
|
|
|
438
506
|
This applies to all flap types. Keep the order of your spool in mind when designing update sequences. The closer two keys are in the spool, the faster the transition.
|
|
@@ -442,4 +510,3 @@ This applies to all flap types. Keep the order of your spool in mind when design
|
|
|
442
510
|
- [How a Split-Flap Display Works (YouTube)](https://www.youtube.com/watch?v=UAQJJAQSg_g)
|
|
443
511
|
- [Lit](https://lit.dev/)
|
|
444
512
|
- [Scott Bezek's open-source split-flap hardware](https://github.com/scottbez1/splitflap)
|
|
445
|
-
- [@ybhrdwj on X](https://x.com/ybhrdwj/status/2037110274696896687) - the tweet that started this :)
|
|
@@ -1,18 +1,56 @@
|
|
|
1
|
-
"use strict";var
|
|
2
|
-
${this.spools.
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
"use strict";var l=require("lit"),p=require("lit/decorators.js"),n=Object.defineProperty,u=Object.getOwnPropertyDescriptor,a=(d,t,e,s)=>{for(var r=s>1?void 0:s?u(t,e):t,o=d.length-1,i;o>=0;o--)(i=d[o])&&(r=(s?i(t,e,r):i(r))||r);return s&&r&&n(t,e,r),r};exports.SplitFlapBoard=class extends l.LitElement{constructor(){super(...arguments),this.spools=[],this.grid=[],this.speed=60,this.variant="minimal",this.visibleSideCount=-1,this._pendingSettle=!1}updated(t){super.updated(t),(t.has("spools")||t.has("grid"))&&(this._pendingSettle=!0,queueMicrotask(()=>this._checkAllSettled()))}_getSpoolEls(){return Array.from(this.renderRoot.querySelectorAll("split-flap-spool"))}_checkAllSettled(){if(!this._pendingSettle)return;const t=this._getSpoolEls();if(t.length===0){this._pendingSettle=!1,this._dispatchBoardSettled([]);return}t.every(e=>e.isSettled)&&(this._pendingSettle=!1,this._dispatchBoardSettled(t))}_dispatchBoardSettled(t){const e=this._getCurrentGrid(t);this.dispatchEvent(new CustomEvent("board-settled",{detail:{grid:e},bubbles:!0,composed:!0}))}_getCurrentGrid(t){let e=0;return this.spools.map(s=>s.map(()=>{var r,o;const i=(o=(r=t[e])==null?void 0:r.currentValue)!=null?o:"";return e+=1,i}))}render(){return l.html`
|
|
2
|
+
${this.spools.map((t,e)=>l.html`
|
|
3
|
+
<div class="board-row" style="z-index: ${e+1}">
|
|
4
|
+
${t.map((s,r)=>{var o,i;return l.html`
|
|
5
|
+
<split-flap-spool
|
|
6
|
+
.flaps=${s}
|
|
7
|
+
.value=${(i=(o=this.grid[e])==null?void 0:o[r])!=null?i:""}
|
|
8
|
+
.speed=${this.speed}
|
|
9
|
+
.variant=${this.variant}
|
|
10
|
+
.visibleSideCount=${this.visibleSideCount}
|
|
11
|
+
@settled=${()=>this._checkAllSettled()}
|
|
12
|
+
></split-flap-spool>
|
|
13
|
+
`})}
|
|
14
|
+
</div>
|
|
15
|
+
`)}
|
|
16
|
+
`}},exports.SplitFlapBoard.styles=l.css`
|
|
13
17
|
:host {
|
|
14
|
-
display: inline-
|
|
15
|
-
|
|
18
|
+
display: inline-flex;
|
|
19
|
+
position: relative;
|
|
20
|
+
flex-direction: column;
|
|
16
21
|
box-sizing: border-box;
|
|
22
|
+
box-shadow:
|
|
23
|
+
inset 14px 0 18px rgba(0, 0, 0, 0.55),
|
|
24
|
+
inset -14px 0 18px rgba(0, 0, 0, 0.55);
|
|
25
|
+
border-radius: var(--sfb-board-radius, 8px);
|
|
26
|
+
background: var(--sfb-board-bg, #1c1c1c);
|
|
27
|
+
padding: var(--sfb-board-padding, 10px);
|
|
17
28
|
}
|
|
18
|
-
|
|
29
|
+
|
|
30
|
+
:host::after {
|
|
31
|
+
position: absolute;
|
|
32
|
+
z-index: 10000;
|
|
33
|
+
inset: 0;
|
|
34
|
+
border: 10px solid var(--sfb-board-bg, #1c1c1c);
|
|
35
|
+
border-radius: var(--sfb-board-radius, 8px);
|
|
36
|
+
pointer-events: none;
|
|
37
|
+
content: '';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.board-row {
|
|
41
|
+
display: flex;
|
|
42
|
+
position: relative;
|
|
43
|
+
gap: var(--sfb-gap, 3px);
|
|
44
|
+
padding-block: 12px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.board-row::before {
|
|
48
|
+
position: absolute;
|
|
49
|
+
z-index: 1;
|
|
50
|
+
inset: 0 0 auto 0;
|
|
51
|
+
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.65);
|
|
52
|
+
background: var(--sfb-board-bg, #1c1c1c);
|
|
53
|
+
height: 10px;
|
|
54
|
+
content: '';
|
|
55
|
+
}
|
|
56
|
+
`,a([p.property({type:Array})],exports.SplitFlapBoard.prototype,"spools",2),a([p.property({type:Array})],exports.SplitFlapBoard.prototype,"grid",2),a([p.property({type:Number})],exports.SplitFlapBoard.prototype,"speed",2),a([p.property({type:String})],exports.SplitFlapBoard.prototype,"variant",2),a([p.property({type:Number})],exports.SplitFlapBoard.prototype,"visibleSideCount",2),exports.SplitFlapBoard=a([p.customElement("split-flap-board")],exports.SplitFlapBoard);
|
package/dist/cjs/lib/board.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var u=require("../spools/presets.js"),y=Object.defineProperty,i=Object.getOwnPropertySymbols,b=Object.prototype.hasOwnProperty,g=Object.prototype.propertyIsEnumerable,s=(r,t,e)=>t in r?y(r,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):r[t]=e,c=(r,t)=>{for(var e in t||(t={}))b.call(t,e)&&s(r,e,t[e]);if(i)for(var e of i(t))g.call(t,e)&&s(r,e,t[e]);return r};function h(r,t,e){const o=v(r)?r:[r];return Array.from({length:e},()=>Array.from({length:t},(a,l)=>{var n;return(n=o[l%o.length])!=null?n:u.charSpool}))}function m(r,t){const e=[],o=[];for(const a of r){const l=O(a),n=A(l.text,t).split(""),f=l.bg!=null||l.color!=null;e.push(f?S(t,l):p(u.charSpool,t)),o.push(n)}return{spools:e,grid:o}}function v(r){return Array.isArray(r[0])}function p(r,t){return Array.from({length:t},()=>r)}function d(r,t){return r.type!=="char"?r:c(c(c({},r),t.bg!=null?{bg:t.bg}:{}),t.color!=null?{color:t.color}:{})}function O(r){return typeof r=="string"?{text:r}:r}function A(r,t){return r.toUpperCase().padEnd(t," ").slice(0,t)}function S(r,t){const e=u.charSpool.map(o=>d(o,t));return p(e,r)}exports.fromLines=m,exports.spoolGrid=h;
|
package/dist/cjs/lib/flap.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";function
|
|
1
|
+
"use strict";function c(e){var r,t;switch(e.type){case"char":case"color":return(r=e.key)!=null?r:e.value;case"image":return(t=e.key)!=null?t:e.src;case"custom":return e.key}}exports.getFlapKey=c;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use strict";var o=require("lit"),r=require("lit/decorators.js"),S=require("./presets.js"),
|
|
1
|
+
"use strict";var o=require("lit"),r=require("lit/decorators.js"),S=require("./presets.js"),u=require("../lib/flap.js");require("./SplitFlapSpoolMinimal.js"),require("./SplitFlapSpoolRealistic.js");var v=Object.defineProperty,h=Object.getOwnPropertyDescriptor,i=(s,e,t,p)=>{for(var l=p>1?void 0:p?h(e,t):e,a=s.length-1,n;a>=0;a--)(n=s[a])&&(l=(p?n(e,t,l):n(l))||l);return p&&l&&v(e,t,l),l};exports.SplitFlapSpool=class extends o.LitElement{constructor(){super(...arguments),this.variant="minimal",this.value=" ",this.flaps=S.charSpool,this.speed=60,this.visibleSideCount=-1}get currentValue(){var e,t;return(t=(e=this._getActiveSpool())==null?void 0:e.currentValue)!=null?t:this.flaps[0]!=null?u.getFlapKey(this.flaps[0]):void 0}get isSettled(){var e,t;return(t=(e=this._getActiveSpool())==null?void 0:e.isSettled)!=null?t:!0}hasKey(e){var t,p;return(p=(t=this._getActiveSpool())==null?void 0:t.hasKey(e))!=null?p:this.flaps.some(l=>u.getFlapKey(l)===e)}_getActiveSpool(){return this.renderRoot.querySelector("split-flap-spool-minimal, split-flap-spool-realistic")}_renderVariant(){return this.variant==="realistic"?o.html`
|
|
2
2
|
<split-flap-spool-realistic
|
|
3
3
|
.value=${this.value}
|
|
4
4
|
.flaps=${this.flaps}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
.flaps=${this.flaps}
|
|
12
12
|
.speed=${this.speed}
|
|
13
13
|
></split-flap-spool-minimal>
|
|
14
|
-
`}},exports.SplitFlapSpool.styles=o.css`
|
|
14
|
+
`}render(){return this._renderVariant()}},exports.SplitFlapSpool.styles=o.css`
|
|
15
15
|
:host {
|
|
16
16
|
display: contents;
|
|
17
17
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
"use strict";var n=require("lit"),
|
|
1
|
+
"use strict";var n=require("lit"),p=require("lit/decorators.js"),x=require("lit/directives/style-map.js"),I=require("./presets.js"),o=require("../lib/flap.js"),g=Object.defineProperty,f=Object.getOwnPropertySymbols,y=Object.prototype.hasOwnProperty,T=Object.prototype.propertyIsEnumerable,v=(s,e,t)=>e in s?g(s,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):s[e]=t,_=(s,e)=>{for(var t in e||(e={}))y.call(e,t)&&v(s,t,e[t]);if(f)for(var t of f(e))T.call(e,t)&&v(s,t,e[t]);return s},u=(s,e,t,r)=>{for(var i=void 0,a=s.length-1,h;a>=0;a--)(h=s[a])&&(i=h(e,t,i)||i);return i&&g(e,t,i),i};class l extends n.LitElement{constructor(){super(...arguments),this.value=" ",this.flaps=I.charSpool,this.speed=60,this._currentIndex=0,this._prevIndex=0,this._stepping=!1,this._targetIndex=-1,this._stepTimer=null,this._animTimer=null,this._animEndsAt=0}get _animDur(){return Math.max(Math.floor(this.speed*.85),1)}updated(e){super.updated(e),e.has("flaps")&&this._syncIndicesToFlaps(e.get("flaps")),(e.has("value")||e.has("flaps"))&&this._startStepping()}disconnectedCallback(){super.disconnectedCallback(),this._clearTimers()}get currentFlap(){var e;return(e=this.flaps[this._currentIndex])!=null?e:this.flaps[0]}get currentValue(){return this.currentFlap!=null?o.getFlapKey(this.currentFlap):void 0}get isSettled(){return!this._stepping&&this._targetIndex===-1&&this._stepTimer==null&&this._animTimer==null}hasKey(e){return this._findFlapIndex(e)>=0}_startStepping(){if(!this.flaps.length){this._resetForEmptyFlaps();return}const e=this._findFlapIndex(this.value),t=this._isAnimating();if(e===-1){this._targetIndex=-1,t&&this._scheduleAdvanceOrSettle(this._getRemainingAnimationTime());return}if(this._targetIndex=e,this._currentIndex===this._targetIndex){t?this._scheduleAdvanceOrSettle(this._getRemainingAnimationTime()):this._targetIndex=-1;return}t||this._doStep()}_doStep(){if(!this.flaps.length){this._resetForEmptyFlaps();return}const e=this._getNextIndex();this._prevIndex=this._currentIndex,this._currentIndex=e,this._stepping=!0,this._animEndsAt=Date.now()+this._animDur,this._animTimer!=null&&clearTimeout(this._animTimer),this._animTimer=setTimeout(()=>{this._stepping=!1,this._animTimer=null,this._animEndsAt=0},this._animDur);const t=this._currentIndex!==this._targetIndex?this.speed:this._getRemainingAnimationTime();this._scheduleAdvanceOrSettle(t)}_scheduleAdvanceOrSettle(e){this._stepTimer!=null&&clearTimeout(this._stepTimer),this._stepTimer=setTimeout(()=>{if(this._stepTimer=null,this._targetIndex!==-1&&this._currentIndex!==this._targetIndex){this._doStep();return}this._finishSettling()},Math.max(e,0))}_finishSettling(){if(this._stepping){this._scheduleAdvanceOrSettle(this._getRemainingAnimationTime());return}this._targetIndex=-1;const e=this.currentValue;e!=null&&this.dispatchEvent(new CustomEvent("settled",{detail:{value:e},bubbles:!0,composed:!0}))}_findFlapIndex(e){return this.flaps.findIndex(t=>o.getFlapKey(t)===e)}_getNextIndex(){return(this._currentIndex+1)%this.flaps.length}_isAnimating(){return this._stepping||this._stepTimer!=null}_getRemainingAnimationTime(){return this._stepping?Math.max(this._animEndsAt-Date.now(),1):0}_resetForEmptyFlaps(){this._clearTimers(),this._targetIndex=-1,this._currentIndex=0,this._prevIndex=0,this._stepping=!1}_syncIndicesToFlaps(e){var t,r;if(!this.flaps.length){this._resetForEmptyFlaps();return}if(e==null||!e.length){this._currentIndex=0,this._prevIndex=0,this._targetIndex=-1,this._clearTimers(),this._stepping=!1;return}const i=(t=e[this._currentIndex])!=null?t:e[0],a=(r=e[this._prevIndex])!=null?r:i,h=i!=null?o.getFlapKey(i):void 0,d=a!=null?o.getFlapKey(a):h;this._clearTimers(),this._stepping=!1,this._targetIndex=-1;const c=h!=null?this._findFlapIndex(h):-1,m=d!=null?this._findFlapIndex(d):-1;this._currentIndex=c>=0?c:0,this._prevIndex=m>=0?m:this._currentIndex}_clearTimers(){this._stepTimer!=null&&(clearTimeout(this._stepTimer),this._stepTimer=null),this._animTimer!=null&&(clearTimeout(this._animTimer),this._animTimer=null),this._animEndsAt=0}_renderHalf(e,t){var r;switch(e.type){case"char":return n.html`
|
|
2
2
|
<div
|
|
3
3
|
class="char-inner"
|
|
4
|
-
style=${
|
|
4
|
+
style=${x.styleMap(_(_(_(_(_({},e.color!=null?{color:e.color}:{}),e.bg!=null?{background:e.bg}:{}),e.fontSize!=null?{fontSize:e.fontSize}:{}),e.fontFamily!=null?{fontFamily:e.fontFamily}:{}),e.fontWeight!=null?{fontWeight:e.fontWeight}:{}))}
|
|
5
5
|
>
|
|
6
6
|
${e.value}
|
|
7
7
|
</div>
|
|
@@ -13,4 +13,4 @@
|
|
|
13
13
|
<div class="half top flipping">${this._renderHalf(r,"top")}</div>
|
|
14
14
|
<div class="half bottom flipping">${this._renderHalf(t,"bottom")}</div>
|
|
15
15
|
`:n.nothing}
|
|
16
|
-
`}}
|
|
16
|
+
`}}u([p.property({type:String})],l.prototype,"value"),u([p.property({type:Array})],l.prototype,"flaps"),u([p.property({type:Number})],l.prototype,"speed"),u([p.state()],l.prototype,"_currentIndex"),u([p.state()],l.prototype,"_prevIndex"),u([p.state()],l.prototype,"_stepping"),exports.SplitFlapSpoolBase=l;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use strict";var e=require("lit"),
|
|
1
|
+
"use strict";var e=require("lit"),l=require("lit/decorators.js"),s=require("./SplitFlapSpoolBase.js"),n=(t,a,p,f)=>{for(var o=a,i=t.length-1,r;i>=0;i--)(r=t[i])&&(o=r(o)||o);return o};exports.SplitFlapSpoolMinimal=class extends s.SplitFlapSpoolBase{render(){return e.html`
|
|
2
2
|
<div class="spool" style="--_anim-dur: ${this._animDur}ms">${this._renderCard()}</div>
|
|
3
3
|
`}},exports.SplitFlapSpoolMinimal.styles=e.css`
|
|
4
4
|
:host {
|
|
@@ -19,23 +19,23 @@
|
|
|
19
19
|
position: absolute;
|
|
20
20
|
right: 0;
|
|
21
21
|
left: 0;
|
|
22
|
-
background: var(--sfb-bg, #111);
|
|
22
|
+
background: var(--sfb-flap-bg, #111);
|
|
23
23
|
height: 50%;
|
|
24
24
|
overflow: hidden;
|
|
25
|
-
color: var(--sfb-color, #f5f0e0);
|
|
25
|
+
color: var(--sfb-flap-color, #f5f0e0);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
.half.top {
|
|
29
29
|
top: 0;
|
|
30
30
|
border-bottom: 1px solid var(--sfb-fold-color, #0a0a0a);
|
|
31
|
-
border-top-right-radius: var(--sfb-
|
|
32
|
-
border-top-left-radius: var(--sfb-
|
|
31
|
+
border-top-right-radius: var(--sfb-flap-radius, 4px);
|
|
32
|
+
border-top-left-radius: var(--sfb-flap-radius, 4px);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
.half.bottom {
|
|
36
36
|
bottom: 0;
|
|
37
|
-
border-bottom-right-radius: var(--sfb-
|
|
38
|
-
border-bottom-left-radius: var(--sfb-
|
|
37
|
+
border-bottom-right-radius: var(--sfb-flap-radius, 4px);
|
|
38
|
+
border-bottom-left-radius: var(--sfb-flap-radius, 4px);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
.half.flipping {
|
|
@@ -118,4 +118,4 @@
|
|
|
118
118
|
inset: 0;
|
|
119
119
|
overflow: hidden;
|
|
120
120
|
}
|
|
121
|
-
`,exports.SplitFlapSpoolMinimal=n([
|
|
121
|
+
`,exports.SplitFlapSpoolMinimal=n([l.customElement("split-flap-spool-minimal")],exports.SplitFlapSpoolMinimal);
|
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var p=require("lit"),f=require("lit/decorators.js"),d=require("lit/directives/ref.js"),u=require("lit/directives/style-map.js");require("./presets.js");var v=require("../lib/spool-layout.js"),m=require("./SplitFlapSpoolBase.js"),b=Object.defineProperty,g=Object.getOwnPropertyDescriptor,h=(n,e,r,t)=>{for(var a=t>1?void 0:t?g(e,r):e,s=n.length-1,i;s>=0;s--)(i=n[s])&&(a=(t?i(e,r,a):i(a))||a);return t&&a&&b(e,r,a),a},x=(n,e,r)=>new Promise((t,a)=>{var s=l=>{try{o(r.next(l))}catch(c){a(c)}},i=l=>{try{o(r.throw(l))}catch(c){a(c)}},o=l=>l.done?t(l.value):Promise.resolve(l.value).then(s,i);o((r=r.apply(n,e)).next())});exports.SplitFlapSpoolRealistic=class extends m.SplitFlapSpoolBase{constructor(){super(...arguments),this.visibleSideCount=-1,this._slotRef=d.createRef(),this._wrapResetTimer=null,this._skipNextIndexAnimation=!1}firstUpdated(){this._syncSlotState()}updated(e){var r;const t=this._currentIndex,a=this._prevIndex;super.updated(e),e.has("flaps")&&this._handleFlapChange(t,a),e.has("_currentIndex")&&this._handleIndexChange((r=e.get("_currentIndex"))!=null?r:0)}disconnectedCallback(){super.disconnectedCallback(),this._clearWrapTimer()}_clearWrapTimer(){this._wrapResetTimer!=null&&(clearTimeout(this._wrapResetTimer),this._wrapResetTimer=null)}_syncSlotState(){const e=this._slotRef.value;e!=null&&(e.style.setProperty("--current-character-index",String(this._currentIndex)),e.style.setProperty("--_flip-dur","0ms"))}_handleFlapChange(e,r){this._clearWrapTimer();const t=this._currentIndex!==e||this._prevIndex!==r;this._skipNextIndexAnimation=t,this._syncSlotState()}_handleIndexChange(e){if(this._skipNextIndexAnimation){this._skipNextIndexAnimation=!1,this._syncSlotState();return}this._animateStep(e,this._currentIndex)}_waitForPositionReset(){return new Promise(e=>{requestAnimationFrame(()=>{requestAnimationFrame(()=>e())})})}_isWrapForwardStep(e,r){return e===this.flaps.length-1&&r===0}_animateStep(e,r){return x(this,null,function*(){const t=this._slotRef.value;if(t==null)return;this._clearWrapTimer(),t.style.setProperty("--_flip-dur","0ms"),t.style.setProperty("--current-character-index",String(e)),yield this._waitForPositionReset();const a=this._isWrapForwardStep(e,r),s=a?this.flaps.length:r;t.style.setProperty("--_flip-dur",`${this._animDur}ms`),t.style.setProperty("--current-character-index",String(s)),a&&(this._wrapResetTimer=setTimeout(()=>{t.style.setProperty("--_flip-dur","0ms"),t.style.setProperty("--current-character-index","0"),this._wrapResetTimer=null,this.requestUpdate()},this._animDur))})}_getRenderCenter(){return this.flaps.length>0&&this._prevIndex===this.flaps.length-1&&this._currentIndex===0&&(this._stepping||this._wrapResetTimer!=null)?this.flaps.length:this._currentIndex}render(){const e=this._getRenderCenter(),r=v.getRenderedFlaps(this.flaps,this._currentIndex,{visibleSideCount:this.visibleSideCount,renderCenter:e});return p.html`
|
|
2
2
|
<div
|
|
3
3
|
${d.ref(this._slotRef)}
|
|
4
4
|
class="slot"
|
|
5
5
|
style=${u.styleMap({"--total":String(this.flaps.length)})}
|
|
6
6
|
>
|
|
7
|
-
${
|
|
8
|
-
<div
|
|
7
|
+
${r.map(({flap:t,actualIndex:a,renderedIndex:s})=>{const i=this.flaps.length>2&&this.flaps.length%2===0&&Math.abs(s-e)===this.flaps.length/2;return p.html`
|
|
8
|
+
<div
|
|
9
|
+
class="character ${i?"is-background":""}"
|
|
10
|
+
style="--index: ${s}"
|
|
11
|
+
data-index=${a}
|
|
12
|
+
>
|
|
9
13
|
<div class="flap">${this._renderHalf(t,"top")}</div>
|
|
10
14
|
<div class="flap" aria-hidden="true">${this._renderHalf(t,"bottom")}</div>
|
|
11
15
|
</div>
|
|
12
|
-
`)}
|
|
16
|
+
`})}
|
|
13
17
|
</div>
|
|
14
|
-
`}},exports.SplitFlapSpoolRealistic.styles=
|
|
18
|
+
`}},exports.SplitFlapSpoolRealistic.styles=p.css`
|
|
15
19
|
:host {
|
|
16
20
|
display: inline-block;
|
|
17
21
|
perspective: var(--sfb-perspective, 400px);
|
|
@@ -69,6 +73,11 @@
|
|
|
69
73
|
pointer-events: none;
|
|
70
74
|
}
|
|
71
75
|
|
|
76
|
+
.character.is-background {
|
|
77
|
+
opacity: 0;
|
|
78
|
+
z-index: 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
72
81
|
.flap {
|
|
73
82
|
position: relative;
|
|
74
83
|
transform-style: preserve-3d;
|
|
@@ -76,13 +85,16 @@
|
|
|
76
85
|
transition: transform var(--_flip-dur, 0ms) cubic-bezier(0.25, 0, 0.5, 1);
|
|
77
86
|
will-change: transform;
|
|
78
87
|
box-sizing: border-box;
|
|
88
|
+
box-shadow:
|
|
89
|
+
0 2px 6px rgba(0, 0, 0, 0.7),
|
|
90
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
|
79
91
|
border: 1px solid var(--sfb-flap-border, #2a2a2a);
|
|
80
|
-
border-radius: var(--sfb-
|
|
81
|
-
background: var(--sfb-bg, #111);
|
|
92
|
+
border-radius: var(--sfb-flap-radius, 5px);
|
|
93
|
+
background: var(--sfb-flap-bg, #111);
|
|
82
94
|
width: var(--sfb-spool-width, 1em);
|
|
83
95
|
height: calc(var(--sfb-spool-height, 2em) / 2);
|
|
84
96
|
overflow: hidden;
|
|
85
|
-
color: var(--sfb-color, #f5f0e0);
|
|
97
|
+
color: var(--sfb-flap-color, #f5f0e0);
|
|
86
98
|
line-height: 1;
|
|
87
99
|
}
|
|
88
100
|
|
|
@@ -141,4 +153,4 @@
|
|
|
141
153
|
justify-content: center;
|
|
142
154
|
align-items: center;
|
|
143
155
|
}
|
|
144
|
-
`,h([
|
|
156
|
+
`,h([f.property({type:Number})],exports.SplitFlapSpoolRealistic.prototype,"visibleSideCount",2),exports.SplitFlapSpoolRealistic=h([f.customElement("split-flap-spool-realistic")],exports.SplitFlapSpoolRealistic);
|
|
@@ -1,18 +1,56 @@
|
|
|
1
|
-
import{css as
|
|
2
|
-
${this.spools.
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
import{css as b,LitElement as u,html as n}from"lit";import{property as d,customElement as c}from"lit/decorators.js";var h=Object.defineProperty,g=Object.getOwnPropertyDescriptor,a=(t,e,p,s)=>{for(var r=s>1?void 0:s?g(e,p):e,i=t.length-1,l;i>=0;i--)(l=t[i])&&(r=(s?l(e,p,r):l(r))||r);return s&&r&&h(e,p,r),r};let o=class extends u{constructor(){super(...arguments),this.spools=[],this.grid=[],this.speed=60,this.variant="minimal",this.visibleSideCount=-1,this._pendingSettle=!1}updated(t){super.updated(t),(t.has("spools")||t.has("grid"))&&(this._pendingSettle=!0,queueMicrotask(()=>this._checkAllSettled()))}_getSpoolEls(){return Array.from(this.renderRoot.querySelectorAll("split-flap-spool"))}_checkAllSettled(){if(!this._pendingSettle)return;const t=this._getSpoolEls();if(t.length===0){this._pendingSettle=!1,this._dispatchBoardSettled([]);return}t.every(e=>e.isSettled)&&(this._pendingSettle=!1,this._dispatchBoardSettled(t))}_dispatchBoardSettled(t){const e=this._getCurrentGrid(t);this.dispatchEvent(new CustomEvent("board-settled",{detail:{grid:e},bubbles:!0,composed:!0}))}_getCurrentGrid(t){let e=0;return this.spools.map(p=>p.map(()=>{var s,r;const i=(r=(s=t[e])==null?void 0:s.currentValue)!=null?r:"";return e+=1,i}))}render(){return n`
|
|
2
|
+
${this.spools.map((t,e)=>n`
|
|
3
|
+
<div class="board-row" style="z-index: ${e+1}">
|
|
4
|
+
${t.map((p,s)=>{var r,i;return n`
|
|
5
|
+
<split-flap-spool
|
|
6
|
+
.flaps=${p}
|
|
7
|
+
.value=${(i=(r=this.grid[e])==null?void 0:r[s])!=null?i:""}
|
|
8
|
+
.speed=${this.speed}
|
|
9
|
+
.variant=${this.variant}
|
|
10
|
+
.visibleSideCount=${this.visibleSideCount}
|
|
11
|
+
@settled=${()=>this._checkAllSettled()}
|
|
12
|
+
></split-flap-spool>
|
|
13
|
+
`})}
|
|
14
|
+
</div>
|
|
15
|
+
`)}
|
|
16
|
+
`}};o.styles=b`
|
|
13
17
|
:host {
|
|
14
|
-
display: inline-
|
|
15
|
-
|
|
18
|
+
display: inline-flex;
|
|
19
|
+
position: relative;
|
|
20
|
+
flex-direction: column;
|
|
16
21
|
box-sizing: border-box;
|
|
22
|
+
box-shadow:
|
|
23
|
+
inset 14px 0 18px rgba(0, 0, 0, 0.55),
|
|
24
|
+
inset -14px 0 18px rgba(0, 0, 0, 0.55);
|
|
25
|
+
border-radius: var(--sfb-board-radius, 8px);
|
|
26
|
+
background: var(--sfb-board-bg, #1c1c1c);
|
|
27
|
+
padding: var(--sfb-board-padding, 10px);
|
|
17
28
|
}
|
|
18
|
-
|
|
29
|
+
|
|
30
|
+
:host::after {
|
|
31
|
+
position: absolute;
|
|
32
|
+
z-index: 10000;
|
|
33
|
+
inset: 0;
|
|
34
|
+
border: 10px solid var(--sfb-board-bg, #1c1c1c);
|
|
35
|
+
border-radius: var(--sfb-board-radius, 8px);
|
|
36
|
+
pointer-events: none;
|
|
37
|
+
content: '';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.board-row {
|
|
41
|
+
display: flex;
|
|
42
|
+
position: relative;
|
|
43
|
+
gap: var(--sfb-gap, 3px);
|
|
44
|
+
padding-block: 12px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.board-row::before {
|
|
48
|
+
position: absolute;
|
|
49
|
+
z-index: 1;
|
|
50
|
+
inset: 0 0 auto 0;
|
|
51
|
+
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.65);
|
|
52
|
+
background: var(--sfb-board-bg, #1c1c1c);
|
|
53
|
+
height: 10px;
|
|
54
|
+
content: '';
|
|
55
|
+
}
|
|
56
|
+
`,a([d({type:Array})],o.prototype,"spools",2),a([d({type:Array})],o.prototype,"grid",2),a([d({type:Number})],o.prototype,"speed",2),a([d({type:String})],o.prototype,"variant",2),a([d({type:Number})],o.prototype,"visibleSideCount",2),o=a([c("split-flap-board")],o);export{o as SplitFlapBoard};
|
package/dist/esm/lib/board.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{charSpool as
|
|
1
|
+
import{charSpool as u}from"../spools/presets.js";var y=Object.defineProperty,f=Object.getOwnPropertySymbols,b=Object.prototype.hasOwnProperty,g=Object.prototype.propertyIsEnumerable,i=(r,t,e)=>t in r?y(r,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):r[t]=e,c=(r,t)=>{for(var e in t||(t={}))b.call(t,e)&&i(r,e,t[e]);if(f)for(var e of f(t))g.call(t,e)&&i(r,e,t[e]);return r};function m(r,t,e){const o=v(r)?r:[r];return Array.from({length:e},()=>Array.from({length:t},(a,n)=>{var l;return(l=o[n%o.length])!=null?l:u}))}function h(r,t){const e=[],o=[];for(const a of r){const n=d(a),l=A(n.text,t).split(""),s=n.bg!=null||n.color!=null;e.push(s?j(t,n):p(u,t)),o.push(l)}return{spools:e,grid:o}}function v(r){return Array.isArray(r[0])}function p(r,t){return Array.from({length:t},()=>r)}function O(r,t){return r.type!=="char"?r:c(c(c({},r),t.bg!=null?{bg:t.bg}:{}),t.color!=null?{color:t.color}:{})}function d(r){return typeof r=="string"?{text:r}:r}function A(r,t){return r.toUpperCase().padEnd(t," ").slice(0,t)}function j(r,t){const e=u.map(o=>O(o,t));return p(e,r)}export{h as fromLines,m as spoolGrid};
|
package/dist/esm/lib/flap.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
function r
|
|
1
|
+
function t(r){var e,c;switch(r.type){case"char":case"color":return(e=r.key)!=null?e:r.value;case"image":return(c=r.key)!=null?c:r.src;case"custom":return r.key}}export{t as getFlapKey};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{css as
|
|
1
|
+
import{css as h,LitElement as m,html as u}from"lit";import{property as o,customElement as d}from"lit/decorators.js";import{charSpool as c}from"./presets.js";import{getFlapKey as v}from"../lib/flap.js";import"./SplitFlapSpoolMinimal.js";import"./SplitFlapSpoolRealistic.js";var f=Object.defineProperty,y=Object.getOwnPropertyDescriptor,p=(e,t,i,s)=>{for(var r=s>1?void 0:s?y(t,i):t,a=e.length-1,n;a>=0;a--)(n=e[a])&&(r=(s?n(t,i,r):n(r))||r);return s&&r&&f(t,i,r),r};let l=class extends m{constructor(){super(...arguments),this.variant="minimal",this.value=" ",this.flaps=c,this.speed=60,this.visibleSideCount=-1}get currentValue(){var e,t;return(t=(e=this._getActiveSpool())==null?void 0:e.currentValue)!=null?t:this.flaps[0]!=null?v(this.flaps[0]):void 0}get isSettled(){var e,t;return(t=(e=this._getActiveSpool())==null?void 0:e.isSettled)!=null?t:!0}hasKey(e){var t,i;return(i=(t=this._getActiveSpool())==null?void 0:t.hasKey(e))!=null?i:this.flaps.some(s=>v(s)===e)}_getActiveSpool(){return this.renderRoot.querySelector("split-flap-spool-minimal, split-flap-spool-realistic")}_renderVariant(){return this.variant==="realistic"?u`
|
|
2
2
|
<split-flap-spool-realistic
|
|
3
3
|
.value=${this.value}
|
|
4
4
|
.flaps=${this.flaps}
|
|
@@ -11,8 +11,8 @@ import{css as m,LitElement as h,html as u}from"lit";import{property as o,customE
|
|
|
11
11
|
.flaps=${this.flaps}
|
|
12
12
|
.speed=${this.speed}
|
|
13
13
|
></split-flap-spool-minimal>
|
|
14
|
-
`}};l.styles=
|
|
14
|
+
`}render(){return this._renderVariant()}};l.styles=h`
|
|
15
15
|
:host {
|
|
16
16
|
display: contents;
|
|
17
17
|
}
|
|
18
|
-
`,p([o({type:String})],l.prototype,"variant",2),p([o({type:String})],l.prototype,"value",2),p([o({type:Array})],l.prototype,"flaps",2),p([o({type:Number})],l.prototype,"speed",2),p([o({type:Number})],l.prototype,"visibleSideCount",2),l=p([
|
|
18
|
+
`,p([o({type:String})],l.prototype,"variant",2),p([o({type:String})],l.prototype,"value",2),p([o({type:Array})],l.prototype,"flaps",2),p([o({type:Number})],l.prototype,"speed",2),p([o({type:Number})],l.prototype,"visibleSideCount",2),l=p([d("split-flap-spool")],l);export{l as SplitFlapSpool};
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import{LitElement as
|
|
1
|
+
import{LitElement as T,html as h,nothing as f}from"lit";import{property as _,state as d}from"lit/decorators.js";import{styleMap as y}from"lit/directives/style-map.js";import{charSpool as F}from"./presets.js";import{getFlapKey as o}from"../lib/flap.js";var v=Object.defineProperty,x=Object.getOwnPropertySymbols,b=Object.prototype.hasOwnProperty,S=Object.prototype.propertyIsEnumerable,I=(s,t,e)=>t in s?v(s,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):s[t]=e,u=(s,t)=>{for(var e in t||(t={}))b.call(t,e)&&I(s,e,t[e]);if(x)for(var e of x(t))S.call(t,e)&&I(s,e,t[e]);return s},p=(s,t,e,n)=>{for(var i=void 0,l=s.length-1,a;l>=0;l--)(a=s[l])&&(i=a(t,e,i)||i);return i&&v(t,e,i),i};class r extends T{constructor(){super(...arguments),this.value=" ",this.flaps=F,this.speed=60,this._currentIndex=0,this._prevIndex=0,this._stepping=!1,this._targetIndex=-1,this._stepTimer=null,this._animTimer=null,this._animEndsAt=0}get _animDur(){return Math.max(Math.floor(this.speed*.85),1)}updated(t){super.updated(t),t.has("flaps")&&this._syncIndicesToFlaps(t.get("flaps")),(t.has("value")||t.has("flaps"))&&this._startStepping()}disconnectedCallback(){super.disconnectedCallback(),this._clearTimers()}get currentFlap(){var t;return(t=this.flaps[this._currentIndex])!=null?t:this.flaps[0]}get currentValue(){return this.currentFlap!=null?o(this.currentFlap):void 0}get isSettled(){return!this._stepping&&this._targetIndex===-1&&this._stepTimer==null&&this._animTimer==null}hasKey(t){return this._findFlapIndex(t)>=0}_startStepping(){if(!this.flaps.length){this._resetForEmptyFlaps();return}const t=this._findFlapIndex(this.value),e=this._isAnimating();if(t===-1){this._targetIndex=-1,e&&this._scheduleAdvanceOrSettle(this._getRemainingAnimationTime());return}if(this._targetIndex=t,this._currentIndex===this._targetIndex){e?this._scheduleAdvanceOrSettle(this._getRemainingAnimationTime()):this._targetIndex=-1;return}e||this._doStep()}_doStep(){if(!this.flaps.length){this._resetForEmptyFlaps();return}const t=this._getNextIndex();this._prevIndex=this._currentIndex,this._currentIndex=t,this._stepping=!0,this._animEndsAt=Date.now()+this._animDur,this._animTimer!=null&&clearTimeout(this._animTimer),this._animTimer=setTimeout(()=>{this._stepping=!1,this._animTimer=null,this._animEndsAt=0},this._animDur);const e=this._currentIndex!==this._targetIndex?this.speed:this._getRemainingAnimationTime();this._scheduleAdvanceOrSettle(e)}_scheduleAdvanceOrSettle(t){this._stepTimer!=null&&clearTimeout(this._stepTimer),this._stepTimer=setTimeout(()=>{if(this._stepTimer=null,this._targetIndex!==-1&&this._currentIndex!==this._targetIndex){this._doStep();return}this._finishSettling()},Math.max(t,0))}_finishSettling(){if(this._stepping){this._scheduleAdvanceOrSettle(this._getRemainingAnimationTime());return}this._targetIndex=-1;const t=this.currentValue;t!=null&&this.dispatchEvent(new CustomEvent("settled",{detail:{value:t},bubbles:!0,composed:!0}))}_findFlapIndex(t){return this.flaps.findIndex(e=>o(e)===t)}_getNextIndex(){return(this._currentIndex+1)%this.flaps.length}_isAnimating(){return this._stepping||this._stepTimer!=null}_getRemainingAnimationTime(){return this._stepping?Math.max(this._animEndsAt-Date.now(),1):0}_resetForEmptyFlaps(){this._clearTimers(),this._targetIndex=-1,this._currentIndex=0,this._prevIndex=0,this._stepping=!1}_syncIndicesToFlaps(t){var e,n;if(!this.flaps.length){this._resetForEmptyFlaps();return}if(t==null||!t.length){this._currentIndex=0,this._prevIndex=0,this._targetIndex=-1,this._clearTimers(),this._stepping=!1;return}const i=(e=t[this._currentIndex])!=null?e:t[0],l=(n=t[this._prevIndex])!=null?n:i,a=i!=null?o(i):void 0,m=l!=null?o(l):a;this._clearTimers(),this._stepping=!1,this._targetIndex=-1;const c=a!=null?this._findFlapIndex(a):-1,g=m!=null?this._findFlapIndex(m):-1;this._currentIndex=c>=0?c:0,this._prevIndex=g>=0?g:this._currentIndex}_clearTimers(){this._stepTimer!=null&&(clearTimeout(this._stepTimer),this._stepTimer=null),this._animTimer!=null&&(clearTimeout(this._animTimer),this._animTimer=null),this._animEndsAt=0}_renderHalf(t,e){var n;switch(t.type){case"char":return h`
|
|
2
2
|
<div
|
|
3
3
|
class="char-inner"
|
|
4
|
-
style=${
|
|
4
|
+
style=${y(u(u(u(u(u({},t.color!=null?{color:t.color}:{}),t.bg!=null?{background:t.bg}:{}),t.fontSize!=null?{fontSize:t.fontSize}:{}),t.fontFamily!=null?{fontFamily:t.fontFamily}:{}),t.fontWeight!=null?{fontWeight:t.fontWeight}:{}))}
|
|
5
5
|
>
|
|
6
6
|
${t.value}
|
|
7
7
|
</div>
|
|
8
|
-
`;case"color":return
|
|
8
|
+
`;case"color":return h`<div class="color-fill" style="background: ${t.value}"></div>`;case"image":return h`<img class="image-fill" src=${t.src} alt=${(n=t.alt)!=null?n:""} />`;case"custom":return h`<div class="custom-fill">${t[e]}</div>`}}_getFlaps(){var t;const e=this.currentFlap;return e==null?null:{current:e,prev:(t=this.flaps[this._prevIndex])!=null?t:e}}_renderCard(){const t=this._getFlaps();if(t==null)return f;const{current:e,prev:n}=t,i=this._stepping?n:e;return h`
|
|
9
9
|
<div class="half top">${this._renderHalf(e,"top")}</div>
|
|
10
10
|
<div class="half bottom">${this._renderHalf(i,"bottom")}</div>
|
|
11
11
|
|
|
12
|
-
${this._stepping?
|
|
13
|
-
<div class="half top flipping">${this._renderHalf(
|
|
12
|
+
${this._stepping?h`
|
|
13
|
+
<div class="half top flipping">${this._renderHalf(n,"top")}</div>
|
|
14
14
|
<div class="half bottom flipping">${this._renderHalf(e,"bottom")}</div>
|
|
15
|
-
`:
|
|
16
|
-
`}}
|
|
15
|
+
`:f}
|
|
16
|
+
`}}p([_({type:String})],r.prototype,"value"),p([_({type:Array})],r.prototype,"flaps"),p([_({type:Number})],r.prototype,"speed"),p([d()],r.prototype,"_currentIndex"),p([d()],r.prototype,"_prevIndex"),p([d()],r.prototype,"_stepping");export{r as SplitFlapSpoolBase};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{css as l,html as s}from"lit";import{customElement as
|
|
1
|
+
import{css as l,html as s}from"lit";import{customElement as f}from"lit/decorators.js";import{SplitFlapSpoolBase as n}from"./SplitFlapSpoolBase.js";var p=(i,a,m,d)=>{for(var o=a,r=i.length-1,e;r>=0;r--)(e=i[r])&&(o=e(o)||o);return o};let t=class extends n{render(){return s`
|
|
2
2
|
<div class="spool" style="--_anim-dur: ${this._animDur}ms">${this._renderCard()}</div>
|
|
3
3
|
`}};t.styles=l`
|
|
4
4
|
:host {
|
|
@@ -19,23 +19,23 @@ import{css as l,html as s}from"lit";import{customElement as n}from"lit/decorator
|
|
|
19
19
|
position: absolute;
|
|
20
20
|
right: 0;
|
|
21
21
|
left: 0;
|
|
22
|
-
background: var(--sfb-bg, #111);
|
|
22
|
+
background: var(--sfb-flap-bg, #111);
|
|
23
23
|
height: 50%;
|
|
24
24
|
overflow: hidden;
|
|
25
|
-
color: var(--sfb-color, #f5f0e0);
|
|
25
|
+
color: var(--sfb-flap-color, #f5f0e0);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
.half.top {
|
|
29
29
|
top: 0;
|
|
30
30
|
border-bottom: 1px solid var(--sfb-fold-color, #0a0a0a);
|
|
31
|
-
border-top-right-radius: var(--sfb-
|
|
32
|
-
border-top-left-radius: var(--sfb-
|
|
31
|
+
border-top-right-radius: var(--sfb-flap-radius, 4px);
|
|
32
|
+
border-top-left-radius: var(--sfb-flap-radius, 4px);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
.half.bottom {
|
|
36
36
|
bottom: 0;
|
|
37
|
-
border-bottom-right-radius: var(--sfb-
|
|
38
|
-
border-bottom-left-radius: var(--sfb-
|
|
37
|
+
border-bottom-right-radius: var(--sfb-flap-radius, 4px);
|
|
38
|
+
border-bottom-left-radius: var(--sfb-flap-radius, 4px);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
.half.flipping {
|
|
@@ -118,4 +118,4 @@ import{css as l,html as s}from"lit";import{customElement as n}from"lit/decorator
|
|
|
118
118
|
inset: 0;
|
|
119
119
|
overflow: hidden;
|
|
120
120
|
}
|
|
121
|
-
`,t=p([
|
|
121
|
+
`,t=p([f("split-flap-spool-minimal")],t);export{t as SplitFlapSpoolMinimal};
|
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
import{css as h,html as p}from"lit";import{property as u,customElement as m}from"lit/decorators.js";import{createRef as v,ref as b}from"lit/directives/ref.js";import{styleMap as
|
|
1
|
+
import{css as h,html as p}from"lit";import{property as u,customElement as m}from"lit/decorators.js";import{createRef as v,ref as b}from"lit/directives/ref.js";import{styleMap as g}from"lit/directives/style-map.js";import"./presets.js";import{getRenderedFlaps as x}from"../lib/spool-layout.js";import{SplitFlapSpoolBase as _}from"./SplitFlapSpoolBase.js";var y=Object.defineProperty,w=Object.getOwnPropertyDescriptor,d=(e,r,t,s)=>{for(var a=s>1?void 0:s?w(r,t):r,i=e.length-1,l;i>=0;i--)(l=e[i])&&(a=(s?l(r,t,a):l(a))||a);return s&&a&&y(r,t,a),a},S=(e,r,t)=>new Promise((s,a)=>{var i=n=>{try{c(t.next(n))}catch(f){a(f)}},l=n=>{try{c(t.throw(n))}catch(f){a(f)}},c=n=>n.done?s(n.value):Promise.resolve(n.value).then(i,l);c((t=t.apply(e,r)).next())});let o=class extends _{constructor(){super(...arguments),this.visibleSideCount=-1,this._slotRef=v(),this._wrapResetTimer=null,this._skipNextIndexAnimation=!1}firstUpdated(){this._syncSlotState()}updated(e){var r;const t=this._currentIndex,s=this._prevIndex;super.updated(e),e.has("flaps")&&this._handleFlapChange(t,s),e.has("_currentIndex")&&this._handleIndexChange((r=e.get("_currentIndex"))!=null?r:0)}disconnectedCallback(){super.disconnectedCallback(),this._clearWrapTimer()}_clearWrapTimer(){this._wrapResetTimer!=null&&(clearTimeout(this._wrapResetTimer),this._wrapResetTimer=null)}_syncSlotState(){const e=this._slotRef.value;e!=null&&(e.style.setProperty("--current-character-index",String(this._currentIndex)),e.style.setProperty("--_flip-dur","0ms"))}_handleFlapChange(e,r){this._clearWrapTimer();const t=this._currentIndex!==e||this._prevIndex!==r;this._skipNextIndexAnimation=t,this._syncSlotState()}_handleIndexChange(e){if(this._skipNextIndexAnimation){this._skipNextIndexAnimation=!1,this._syncSlotState();return}this._animateStep(e,this._currentIndex)}_waitForPositionReset(){return new Promise(e=>{requestAnimationFrame(()=>{requestAnimationFrame(()=>e())})})}_isWrapForwardStep(e,r){return e===this.flaps.length-1&&r===0}_animateStep(e,r){return S(this,null,function*(){const t=this._slotRef.value;if(t==null)return;this._clearWrapTimer(),t.style.setProperty("--_flip-dur","0ms"),t.style.setProperty("--current-character-index",String(e)),yield this._waitForPositionReset();const s=this._isWrapForwardStep(e,r),a=s?this.flaps.length:r;t.style.setProperty("--_flip-dur",`${this._animDur}ms`),t.style.setProperty("--current-character-index",String(a)),s&&(this._wrapResetTimer=setTimeout(()=>{t.style.setProperty("--_flip-dur","0ms"),t.style.setProperty("--current-character-index","0"),this._wrapResetTimer=null,this.requestUpdate()},this._animDur))})}_getRenderCenter(){return this.flaps.length>0&&this._prevIndex===this.flaps.length-1&&this._currentIndex===0&&(this._stepping||this._wrapResetTimer!=null)?this.flaps.length:this._currentIndex}render(){const e=this._getRenderCenter(),r=x(this.flaps,this._currentIndex,{visibleSideCount:this.visibleSideCount,renderCenter:e});return p`
|
|
2
2
|
<div
|
|
3
3
|
${b(this._slotRef)}
|
|
4
4
|
class="slot"
|
|
5
|
-
style=${
|
|
5
|
+
style=${g({"--total":String(this.flaps.length)})}
|
|
6
6
|
>
|
|
7
|
-
${
|
|
8
|
-
<div
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
${r.map(({flap:t,actualIndex:s,renderedIndex:a})=>{const i=this.flaps.length>2&&this.flaps.length%2===0&&Math.abs(a-e)===this.flaps.length/2;return p`
|
|
8
|
+
<div
|
|
9
|
+
class="character ${i?"is-background":""}"
|
|
10
|
+
style="--index: ${a}"
|
|
11
|
+
data-index=${s}
|
|
12
|
+
>
|
|
13
|
+
<div class="flap">${this._renderHalf(t,"top")}</div>
|
|
14
|
+
<div class="flap" aria-hidden="true">${this._renderHalf(t,"bottom")}</div>
|
|
11
15
|
</div>
|
|
12
|
-
`)}
|
|
16
|
+
`})}
|
|
13
17
|
</div>
|
|
14
18
|
`}};o.styles=h`
|
|
15
19
|
:host {
|
|
@@ -69,6 +73,11 @@ import{css as h,html as p}from"lit";import{property as u,customElement as m}from
|
|
|
69
73
|
pointer-events: none;
|
|
70
74
|
}
|
|
71
75
|
|
|
76
|
+
.character.is-background {
|
|
77
|
+
opacity: 0;
|
|
78
|
+
z-index: 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
72
81
|
.flap {
|
|
73
82
|
position: relative;
|
|
74
83
|
transform-style: preserve-3d;
|
|
@@ -76,13 +85,16 @@ import{css as h,html as p}from"lit";import{property as u,customElement as m}from
|
|
|
76
85
|
transition: transform var(--_flip-dur, 0ms) cubic-bezier(0.25, 0, 0.5, 1);
|
|
77
86
|
will-change: transform;
|
|
78
87
|
box-sizing: border-box;
|
|
88
|
+
box-shadow:
|
|
89
|
+
0 2px 6px rgba(0, 0, 0, 0.7),
|
|
90
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
|
79
91
|
border: 1px solid var(--sfb-flap-border, #2a2a2a);
|
|
80
|
-
border-radius: var(--sfb-
|
|
81
|
-
background: var(--sfb-bg, #111);
|
|
92
|
+
border-radius: var(--sfb-flap-radius, 5px);
|
|
93
|
+
background: var(--sfb-flap-bg, #111);
|
|
82
94
|
width: var(--sfb-spool-width, 1em);
|
|
83
95
|
height: calc(var(--sfb-spool-height, 2em) / 2);
|
|
84
96
|
overflow: hidden;
|
|
85
|
-
color: var(--sfb-color, #f5f0e0);
|
|
97
|
+
color: var(--sfb-flap-color, #f5f0e0);
|
|
86
98
|
line-height: 1;
|
|
87
99
|
}
|
|
88
100
|
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import { LitElement, type TemplateResult } from 'lit';
|
|
2
|
-
import './
|
|
3
|
-
import type { TSpool } from './types';
|
|
2
|
+
import type { TSplitFlapVariant, TSpool } from './types';
|
|
4
3
|
export declare class SplitFlapBoard extends LitElement {
|
|
5
4
|
static readonly styles: import("lit").CSSResult;
|
|
6
|
-
/** 2-D grid of spool configs
|
|
5
|
+
/** 2-D grid of spool configs; defines what each position can show. Row-major order. */
|
|
7
6
|
spools: TSpool[][];
|
|
8
|
-
/** 2-D grid of target keys
|
|
7
|
+
/** 2-D grid of target keys; defines what each position currently shows. Row-major order. */
|
|
9
8
|
grid: string[][];
|
|
10
9
|
/** Flip speed in ms, forwarded to every child spool. */
|
|
11
10
|
speed: number;
|
|
12
11
|
/** Visual variant forwarded to every child spool. */
|
|
13
|
-
variant:
|
|
12
|
+
variant: TSplitFlapVariant;
|
|
14
13
|
/** Number of visible drum sides forwarded to every child spool (-1 = default). */
|
|
15
14
|
visibleSideCount: number;
|
|
16
15
|
/** True while at least one spool is still animating toward its target. */
|
|
@@ -19,6 +18,7 @@ export declare class SplitFlapBoard extends LitElement {
|
|
|
19
18
|
private _getSpoolEls;
|
|
20
19
|
private _checkAllSettled;
|
|
21
20
|
private _dispatchBoardSettled;
|
|
21
|
+
private _getCurrentGrid;
|
|
22
22
|
render(): TemplateResult;
|
|
23
23
|
}
|
|
24
24
|
declare global {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TSpool } from '../types';
|
|
1
|
+
import type { TBoardData, TLineInput, TSpool } from '../types';
|
|
2
2
|
/**
|
|
3
3
|
* Fill a uniform 2-D spool grid.
|
|
4
4
|
*
|
|
@@ -19,12 +19,4 @@ export declare function spoolGrid(spool: TSpool | TSpool[], cols: number, rows:
|
|
|
19
19
|
* @param lines - Text lines to display.
|
|
20
20
|
* @param cols - Board width in columns.
|
|
21
21
|
*/
|
|
22
|
-
export declare function fromLines(lines: TLineInput[], cols: number):
|
|
23
|
-
spools: TSpool[][];
|
|
24
|
-
grid: string[][];
|
|
25
|
-
};
|
|
26
|
-
export type TLineInput = string | {
|
|
27
|
-
text: string;
|
|
28
|
-
bg?: string;
|
|
29
|
-
color?: string;
|
|
30
|
-
};
|
|
22
|
+
export declare function fromLines(lines: TLineInput[], cols: number): TBoardData;
|
package/dist/types/lib/flap.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { LitElement, type TemplateResult } from 'lit';
|
|
2
|
-
import type { TSpool } from '../types';
|
|
2
|
+
import type { TSplitFlapVariant, TSpool } from '../types';
|
|
3
3
|
import './SplitFlapSpoolMinimal';
|
|
4
4
|
import './SplitFlapSpoolRealistic';
|
|
5
5
|
export declare class SplitFlapSpool extends LitElement {
|
|
6
6
|
static readonly styles: import("lit").CSSResult;
|
|
7
|
-
variant:
|
|
7
|
+
variant: TSplitFlapVariant;
|
|
8
8
|
value: string;
|
|
9
9
|
flaps: TSpool;
|
|
10
10
|
speed: number;
|
|
@@ -16,6 +16,7 @@ export declare class SplitFlapSpool extends LitElement {
|
|
|
16
16
|
/** Returns true when the given key exists in the loaded flaps. */
|
|
17
17
|
hasKey(value: string): boolean;
|
|
18
18
|
private _getActiveSpool;
|
|
19
|
+
private _renderVariant;
|
|
19
20
|
render(): TemplateResult;
|
|
20
21
|
}
|
|
21
22
|
declare global {
|
|
@@ -36,13 +36,16 @@ export declare abstract class SplitFlapSpoolBase extends LitElement {
|
|
|
36
36
|
private _doStep;
|
|
37
37
|
private _scheduleAdvanceOrSettle;
|
|
38
38
|
private _finishSettling;
|
|
39
|
-
private
|
|
39
|
+
private _findFlapIndex;
|
|
40
|
+
private _getNextIndex;
|
|
41
|
+
private _isAnimating;
|
|
42
|
+
private _getRemainingAnimationTime;
|
|
40
43
|
private _resetForEmptyFlaps;
|
|
41
44
|
private _syncIndicesToFlaps;
|
|
42
45
|
private _clearTimers;
|
|
43
|
-
/** Renders
|
|
46
|
+
/** Renders one flap half. Subclass stylesheets must define `.char-inner`, `.color-fill`, `.image-fill`, `.custom-fill`. */
|
|
44
47
|
protected _renderHalf(flap: TFlap, half: 'top' | 'bottom'): TemplateResult;
|
|
45
|
-
/** Returns current and previous flap for
|
|
48
|
+
/** Returns current and previous flap for render. Null when flaps is empty. */
|
|
46
49
|
protected _getFlaps(): {
|
|
47
50
|
current: TFlap;
|
|
48
51
|
prev: TFlap;
|
|
@@ -24,10 +24,10 @@ export declare class SplitFlapSpoolRealistic extends SplitFlapSpoolBase {
|
|
|
24
24
|
disconnectedCallback(): void;
|
|
25
25
|
private _clearWrapTimer;
|
|
26
26
|
private _syncSlotState;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
private _handleFlapChange;
|
|
28
|
+
private _handleIndexChange;
|
|
29
|
+
private _waitForPositionReset;
|
|
30
|
+
private _isWrapForwardStep;
|
|
31
31
|
private _animateStep;
|
|
32
32
|
private _getRenderCenter;
|
|
33
33
|
render(): TemplateResult;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { TemplateResult } from 'lit';
|
|
2
|
+
export type TSplitFlapVariant = 'minimal' | 'realistic';
|
|
2
3
|
export type TSpool = TFlap[];
|
|
4
|
+
export type TGrid = string[][];
|
|
3
5
|
export type TFlap = TFlapChar | TFlapColor | TFlapImage | TFlapCustom;
|
|
4
6
|
export interface TFlapChar {
|
|
5
7
|
type: 'char';
|
|
@@ -28,3 +30,19 @@ export interface TFlapCustom {
|
|
|
28
30
|
top: TemplateResult;
|
|
29
31
|
bottom: TemplateResult;
|
|
30
32
|
}
|
|
33
|
+
export interface TBoardData {
|
|
34
|
+
spools: TSpool[][];
|
|
35
|
+
grid: TGrid;
|
|
36
|
+
}
|
|
37
|
+
export interface TLineConfig {
|
|
38
|
+
text: string;
|
|
39
|
+
bg?: string;
|
|
40
|
+
color?: string;
|
|
41
|
+
}
|
|
42
|
+
export type TLineInput = string | TLineConfig;
|
|
43
|
+
export interface TSpoolSettledDetail {
|
|
44
|
+
value: string;
|
|
45
|
+
}
|
|
46
|
+
export interface TBoardSettledDetail {
|
|
47
|
+
grid: TGrid;
|
|
48
|
+
}
|