split-flap-board 0.0.1 → 0.0.4

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.
Files changed (38) hide show
  1. package/README.md +452 -1
  2. package/dist/cjs/SplitFlapBoard.js +61 -0
  3. package/dist/cjs/index.js +1 -7
  4. package/dist/cjs/lib/board.js +1 -0
  5. package/dist/cjs/lib/flap.js +1 -0
  6. package/dist/cjs/lib/spool-layout.js +1 -0
  7. package/dist/cjs/spools/SplitFlapSpool.js +18 -0
  8. package/dist/cjs/spools/SplitFlapSpoolBase.js +16 -0
  9. package/dist/cjs/spools/SplitFlapSpoolMinimal.js +121 -0
  10. package/dist/cjs/spools/SplitFlapSpoolRealistic.js +156 -0
  11. package/dist/cjs/spools/presets.js +1 -0
  12. package/dist/esm/SplitFlapBoard.js +61 -0
  13. package/dist/esm/index.js +1 -5
  14. package/dist/esm/lib/board.js +1 -0
  15. package/dist/esm/lib/flap.js +1 -0
  16. package/dist/esm/lib/spool-layout.js +1 -0
  17. package/dist/esm/spools/SplitFlapSpool.js +18 -0
  18. package/dist/esm/spools/SplitFlapSpoolBase.js +16 -0
  19. package/dist/esm/spools/SplitFlapSpoolMinimal.js +121 -0
  20. package/dist/esm/spools/SplitFlapSpoolRealistic.js +156 -0
  21. package/dist/esm/spools/presets.js +1 -0
  22. package/dist/types/SplitFlapBoard.d.ts +28 -0
  23. package/dist/types/index.d.ts +4 -2
  24. package/dist/types/lib/board.d.ts +30 -0
  25. package/dist/types/lib/flap.d.ts +3 -0
  26. package/dist/types/lib/index.d.ts +3 -0
  27. package/dist/types/lib/spool-layout.d.ts +16 -0
  28. package/dist/types/spools/SplitFlapSpool.d.ts +25 -0
  29. package/dist/types/spools/SplitFlapSpoolBase.d.ts +52 -0
  30. package/dist/types/spools/SplitFlapSpoolMinimal.d.ts +11 -0
  31. package/dist/types/spools/SplitFlapSpoolRealistic.d.ts +39 -0
  32. package/dist/types/spools/index.d.ts +5 -0
  33. package/dist/types/spools/presets.d.ts +4 -0
  34. package/dist/types/types.d.ts +30 -0
  35. package/package.json +13 -4
  36. package/dist/cjs/index.js.map +0 -1
  37. package/dist/esm/index.js.map +0 -1
  38. package/dist/types/index.d.ts.map +0 -1
package/README.md CHANGED
@@ -1 +1,452 @@
1
- # Split Flap Board
1
+ <h1 align="center">
2
+ <img src="https://raw.githubusercontent.com/builder-group/community/develop/packages/split-flap-board/.github/banner.svg" alt="split-flap-board banner">
3
+ </h1>
4
+
5
+ <p align="left">
6
+ <a href="https://github.com/builder-group/community/blob/develop/LICENSE">
7
+ <img src="https://img.shields.io/github/license/builder-group/community.svg?label=license&style=flat&colorA=293140&colorB=FDE200" alt="GitHub License"/>
8
+ </a>
9
+ <a href="https://www.npmjs.com/package/split-flap-board">
10
+ <img src="https://img.shields.io/bundlephobia/minzip/split-flap-board.svg?label=minzipped%20size&style=flat&colorA=293140&colorB=FDE200" alt="NPM bundle minzipped size"/>
11
+ </a>
12
+ <a href="https://www.npmjs.com/package/split-flap-board">
13
+ <img src="https://img.shields.io/npm/dt/split-flap-board.svg?label=downloads&style=flat&colorA=293140&colorB=FDE200" alt="NPM total downloads"/>
14
+ </a>
15
+ <a href="https://discord.gg/w4xE3bSjhQ">
16
+ <img src="https://img.shields.io/discord/795291052897992724.svg?label=&logo=discord&logoColor=000000&color=293140&labelColor=FDE200" alt="Join Discord"/>
17
+ </a>
18
+ </p>
19
+
20
+ Web component that simulates a split-flap display, the mechanical boards found in airports and train stations. Built with [Lit](https://lit.dev/), works in any framework or plain HTML.
21
+
22
+ ## How a Split-Flap Display Works
23
+
24
+ > [How a Split-Flap Display Works (YouTube)](https://www.youtube.com/watch?v=UAQJJAQSg_g)
25
+
26
+ A split-flap display (also called a "Solari board") works through purely mechanical means:
27
+
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 a different character on the back.
29
+ - 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 at manufacture.
31
+ - A **hall effect sensor** and magnet on the spool give the controller a consistent home position, so it always knows which character is showing even after a power cycle.
32
+
33
+ In code, each `<split-flap-spool>` mirrors this: it holds a sequence of **flaps** and steps forward through them to reach a target key, never backward.
34
+
35
+ ## Core Concepts
36
+
37
+ ### Flap
38
+
39
+ A flap is one card on the spool, the atomic unit of content. Four built-in types:
40
+
41
+ ```ts
42
+ // Character (default)
43
+ { type: 'char'; value: string; color?: string; bg?: string; fontSize?: string; fontFamily?: string; fontWeight?: string }
44
+
45
+ // Solid color
46
+ { type: 'color'; value: string } // any CSS color
47
+
48
+ // Image
49
+ { type: 'image'; src: string; alt?: string }
50
+
51
+ // Custom - top and bottom halves rendered independently (Lit TemplateResult)
52
+ { type: 'custom'; key: string; top: TemplateResult; bottom: TemplateResult }
53
+ ```
54
+
55
+ The `key` field is optional on all types except `custom`. When omitted it defaults to the natural identifier: `value` for char and color, `src` for image.
56
+
57
+ Char flaps default to `2rem` monospace bold. Override per-flap via the object, or rebuild the spool with a new `fontSize` to change all at once:
58
+
59
+ ```ts
60
+ const bigSpool = charSpool.map((f) =>
61
+ f.type === 'char' ? { ...f, fontSize: '3rem', fontWeight: '400' } : f
62
+ );
63
+ spool.flaps = bigSpool;
64
+ ```
65
+
66
+ ### Spool
67
+
68
+ A spool is an ordered array of flaps, the sequence a `<split-flap-spool>` steps through. Define it once, reference it anywhere.
69
+
70
+ ```ts
71
+ type TSpool = TFlap[];
72
+ ```
73
+
74
+ The library ships built-in spools:
75
+
76
+ ```ts
77
+ import { charSpool, numericSpool } from 'split-flap-board';
78
+
79
+ // charSpool → [' ', A-Z, 0-9, . - / :]
80
+ // numericSpool → [' ', 0-9]
81
+ ```
82
+
83
+ Custom spools are just arrays:
84
+
85
+ ```ts
86
+ const statusSpool: TSpool = [
87
+ { type: 'color', value: '#111', key: 'off' },
88
+ { type: 'color', value: '#16a34a', key: 'green' },
89
+ { type: 'color', value: '#dc2626', key: 'red' },
90
+ { type: 'color', value: '#f59e0b', key: 'yellow' }
91
+ ];
92
+ ```
93
+
94
+ You can mix flap types within a spool:
95
+
96
+ ```ts
97
+ const mixedSpool: TSpool = [
98
+ { type: 'char', value: ' ' },
99
+ { type: 'image', src: '/icons/check.svg', key: 'check' },
100
+ { type: 'color', value: '#16a34a', key: 'green' },
101
+ { type: 'char', value: '!' }
102
+ ];
103
+ ```
104
+
105
+ ### Spools grid vs. target grid
106
+
107
+ A board has two separate grids:
108
+
109
+ - **`spools`** - a `TSpool[][]` defining what each spool unit CAN show. Typically set once at init.
110
+ - **`grid`** - a `string[][]` of target keys defining what each unit SHOWS right now. Updated freely at runtime.
111
+
112
+ ```ts
113
+ import { charSpool, spoolGrid } from 'split-flap-board';
114
+
115
+ // spoolGrid(spool, cols, rows) fills a uniform TSpool[][]
116
+ board.spools = spoolGrid(charSpool, 10, 3);
117
+
118
+ // per-column: pass an array of spools, one per column (shorter arrays repeat)
119
+ board.spools = spoolGrid([charSpool, charSpool, statusSpool], 3, 2);
120
+
121
+ // fully custom: build the 2D array directly
122
+ board.spools = [
123
+ [charSpool, charSpool, statusSpool],
124
+ [charSpool, charSpool, statusSpool]
125
+ ];
126
+
127
+ // grid: target keys, updated freely at runtime
128
+ board.grid = [
129
+ ['H', 'E', 'green'],
130
+ ['L', 'O', 'red']
131
+ ];
132
+ ```
133
+
134
+ Board dimensions are inferred from `spools`.
135
+
136
+ ## Usage
137
+
138
+ ### Single spool
139
+
140
+ ```html
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
+ ```
177
+
178
+ ```js
179
+ import { fromLines } from 'split-flap-board';
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
193
+ board.grid = [['G', 'O', 'O', 'D', 'B', 'Y', 'E', ' ', ' ', ' ', ' ']];
194
+ ```
195
+
196
+ ### Multi-row board
197
+
198
+ ```js
199
+ import { fromLines } from 'split-flap-board';
200
+
201
+ const { spools, grid } = fromLines(
202
+ ['BA123 LHR 18:30 BOARDING', 'LH456 FRA 19:15 ON TIME ', 'AF789 CDG 19:45 DELAYED '],
203
+ 26
204
+ );
205
+
206
+ board.spools = spools;
207
+ board.grid = grid;
208
+ ```
209
+
210
+ ### Mixed spools per column
211
+
212
+ ```js
213
+ import { charSpool, spoolGrid } from 'split-flap-board';
214
+
215
+ const statusSpool = [
216
+ { type: 'color', value: '#111', key: 'off' },
217
+ { type: 'color', value: '#16a34a', key: 'green' },
218
+ { type: 'color', value: '#dc2626', key: 'red' }
219
+ ];
220
+
221
+ board.spools = spoolGrid([charSpool, charSpool, charSpool, charSpool, statusSpool], 5, 2);
222
+
223
+ board.grid = [
224
+ ['G', 'A', 'T', 'E', 'green'],
225
+ ['B', '1', '2', '3', 'red']
226
+ ];
227
+ ```
228
+
229
+ ### Colored rows
230
+
231
+ ```js
232
+ import { fromLines } from 'split-flap-board';
233
+
234
+ const { spools, grid } = fromLines(
235
+ [
236
+ { text: 'BA123 LHR BOARDING', bg: '#16a34a', color: '#fff' },
237
+ { text: 'LH456 FRA ON TIME ' },
238
+ { text: 'AF789 CDG DELAYED ', bg: '#dc2626', color: '#fff' }
239
+ ],
240
+ 26
241
+ );
242
+
243
+ board.spools = spools;
244
+ board.grid = grid;
245
+ ```
246
+
247
+ ### React
248
+
249
+ ```tsx
250
+ import { useEffect, useRef } from 'react';
251
+ import { fromLines } from 'split-flap-board';
252
+
253
+ declare global {
254
+ namespace JSX {
255
+ interface IntrinsicElements {
256
+ 'split-flap-spool': React.HTMLAttributes<HTMLElement> & { value?: string; variant?: string };
257
+ 'split-flap-board': React.HTMLAttributes<HTMLElement>;
258
+ }
259
+ }
260
+ }
261
+
262
+ export function DeparturesBoard() {
263
+ const ref = useRef<HTMLElement>(null);
264
+
265
+ useEffect(() => {
266
+ if (ref.current == null) return;
267
+ const { spools, grid } = fromLines(['DEPARTURES'], 10);
268
+ (ref.current as any).spools = spools;
269
+ (ref.current as any).grid = grid;
270
+ }, []);
271
+
272
+ return <split-flap-board ref={ref} />;
273
+ }
274
+ ```
275
+
276
+ ## API Reference
277
+
278
+ ### `<split-flap-spool>`
279
+
280
+ | Property | Type | Default | Description |
281
+ | ------------------ | -------------------------- | ----------- | ------------------------------------------------------------------------------ |
282
+ | `variant` | `'minimal' \| 'realistic'` | `'minimal'` | Which visual variant to render. |
283
+ | `value` | `string` | `' '` | Target flap key. Steps forward through the flaps until it reaches this key. |
284
+ | `flaps` | `TSpool` | `charSpool` | The ordered sequence of flaps this spool holds. |
285
+ | `speed` | `number` | `60` | Milliseconds per flap step. |
286
+ | `visibleSideCount` | `number` | `-1` | Realistic variant only. Limits how many flaps render on each side of the drum. |
287
+
288
+ #### Variants
289
+
290
+ | Variant | Element | Description |
291
+ | ------------- | ------------------------------ | ---------------------------------------------------------------- |
292
+ | `'minimal'` | `<split-flap-spool-minimal>` | Clean card renderer that only draws the active flap. |
293
+ | `'realistic'` | `<split-flap-spool-realistic>` | 3D drum renderer that places multiple flaps around the cylinder. |
294
+
295
+ The variant elements can also be used directly if you prefer not to use the wrapper.
296
+
297
+ ### `<split-flap-board>`
298
+
299
+ | Property | Type | Default | Description |
300
+ | ------------------ | -------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------- |
301
+ | `spools` | `TSpool[][]` | — | 2D spool configuration, one per cell. Board dimensions are inferred from this. Always assign a new array reference to update. |
302
+ | `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. |
304
+ | `variant` | `'minimal' \| 'realistic'` | `'minimal'` | Visual variant forwarded to every child spool. |
305
+ | `visibleSideCount` | `number` | `-1` | Forwarded to child spools. Only affects the `realistic` variant. |
306
+
307
+ ### Events
308
+
309
+ #### `<split-flap-spool>`
310
+
311
+ | Event | Detail | Description |
312
+ | --------- | ------------------- | --------------------------------------------------------------------------------- |
313
+ | `settled` | `{ value: string }` | Fired when the spool becomes idle. `value` is the flap key it actually landed on. |
314
+
315
+ #### `<split-flap-board>`
316
+
317
+ | Event | Detail | Description |
318
+ | --------------- | ---------------------- | --------------------------------------------------------------------- |
319
+ | `board-settled` | `{ grid: string[][] }` | Fired when every rendered spool is idle for the current board inputs. |
320
+
321
+ ```js
322
+ const spool = document.querySelector('split-flap-spool');
323
+ spool.addEventListener('settled', (e) => console.log('landed on', e.detail.value));
324
+ board.addEventListener('board-settled', (e) => console.log('board done', e.detail.grid));
325
+ ```
326
+
327
+ ### `spoolGrid(spool, cols, rows)`
328
+
329
+ ```ts
330
+ function spoolGrid(spool: TSpool | TSpool[], cols: number, rows: number): TSpool[][];
331
+ ```
332
+
333
+ Creates a `TSpool[][]` for use with `board.spools`.
334
+
335
+ ```ts
336
+ // Uniform - same spool for every cell
337
+ spoolGrid(charSpool, 10, 3);
338
+
339
+ // Per-column - pass an array where index = column; shorter arrays repeat
340
+ spoolGrid([charSpool, charSpool, statusSpool], 3, 2);
341
+ ```
342
+
343
+ ### `fromLines(lines, cols)`
344
+
345
+ ```ts
346
+ function fromLines(
347
+ lines: (string | { text: string; bg?: string; color?: string })[],
348
+ cols: number
349
+ ): { spools: TSpool[][]; grid: string[][] };
350
+ ```
351
+
352
+ Creates a char `spools` grid and a `grid` of target keys from an array of strings. Each string is one row, padded with spaces or truncated to `cols`. Rows with `bg`/`color` get those values baked into their char flaps.
353
+
354
+ ```ts
355
+ const { spools, grid } = fromLines(
356
+ ['HELLO WORLD', { text: 'BOARDING', bg: '#16a34a', color: '#fff' }],
357
+ 11
358
+ );
359
+
360
+ board.spools = spools;
361
+ board.grid = grid;
362
+ ```
363
+
364
+ ### CSS Custom Properties
365
+
366
+ Set these on the board to theme all spools at once, or override on individual spools via CSS selectors.
367
+
368
+ ```css
369
+ /* Board panel */
370
+ split-flap-board {
371
+ --sfb-board-bg: #1c1c1c; /* panel and frame background */
372
+ --sfb-board-padding: 10px; /* inset spacing around the cell grid */
373
+ --sfb-board-radius: 8px; /* corner radius of the panel itself */
374
+ --sfb-gap: 3px; /* gap between spool cells */
375
+ }
376
+
377
+ /* Shared (flap) */
378
+ split-flap-board {
379
+ --sfb-flap-bg: #111; /* flap background */
380
+ --sfb-flap-color: #f5f0e0; /* flap text color */
381
+ --sfb-flap-radius: 4px; /* corner radius on each flap */
382
+ }
383
+
384
+ /* Minimal variant */
385
+ split-flap-board {
386
+ --sfb-spool-width: 1.2em; /* explicit cell width */
387
+ --sfb-spool-height: 2em; /* explicit cell height */
388
+ --sfb-fold-color: #0a0a0a; /* center crease color */
389
+ }
390
+
391
+ /* Realistic variant */
392
+ split-flap-board {
393
+ --sfb-spool-width: 1em; /* flap width; defaults to 1× font-size */
394
+ --sfb-spool-height: 2em; /* flap height; defaults to 2× font-size */
395
+ --sfb-drum-radius: 0px; /* cylinder radius; 0 keeps the flip flat */
396
+ --sfb-crease: 1px; /* gap between the two flap halves */
397
+ --sfb-perspective: 400px; /* CSS perspective depth */
398
+ --sfb-view-transform: none; /* e.g. rotateY(-30deg) */
399
+ --sfb-max-step-angle: 1turn; /* per-step angle cap; 8deg tightens small spools */
400
+ --sfb-flap-border: #2a2a2a; /* border on each flap card; separates adjacent cells */
401
+ }
402
+
403
+ /* Per-spool override */
404
+ split-flap-spool.highlight {
405
+ --sfb-flap-bg: #16a34a;
406
+ --sfb-flap-color: #fff;
407
+ }
408
+ ```
409
+
410
+ ## Behavior
411
+
412
+ ### Initial state
413
+
414
+ Before `value` is set, a `<split-flap-spool>` shows the first flap in its sequence (index 0). For `charSpool` that is a space. This mirrors the physical home position the hall effect sensor establishes on power-up.
415
+
416
+ ### Animation
417
+
418
+ Each flap step plays a fold animation where the top half falls away, revealing the next card underneath. The animation duration is derived from `speed` so it always fits within one step interval. No separate property is needed.
419
+
420
+ ### Unknown key
421
+
422
+ 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 that happens during an in-flight animation, the current flip finishes and `settled` reports the flap the spool actually landed on.
423
+
424
+ ### Retargeting during motion
425
+
426
+ 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.
427
+
428
+ ### Spool changes during motion
429
+
430
+ 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.
431
+
432
+ ### Grid size mismatch
433
+
434
+ 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.
435
+
436
+ ### Forward-Only Stepping
437
+
438
+ Because a spool only rotates forward, the number of steps depends on the distance ahead in the spool, wrapping around if needed.
439
+
440
+ ```
441
+ 'A' → 'C' = 2 steps
442
+ 'Z' → 'B' = 3 steps (wraps: Z → ' ' → A → B)
443
+ ```
444
+
445
+ 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.
446
+
447
+ ## Resources & References
448
+
449
+ - [How a Split-Flap Display Works (YouTube)](https://www.youtube.com/watch?v=UAQJJAQSg_g)
450
+ - [Lit](https://lit.dev/)
451
+ - [Scott Bezek's open-source split-flap hardware](https://github.com/scottbez1/splitflap)
452
+ - [@ybhrdwj on X](https://x.com/ybhrdwj/status/2037110274696896687) - the tweet that started this :)
@@ -0,0 +1,61 @@
1
+ "use strict";var p=require("lit"),i=require("lit/decorators.js");require("./spools/SplitFlapSpool.js");var n=Object.defineProperty,b=Object.getOwnPropertyDescriptor,l=(d,e,r,s)=>{for(var t=s>1?void 0:s?b(e,r):e,o=d.length-1,a;o>=0;o--)(a=d[o])&&(t=(s?a(e,r,t):a(t))||t);return s&&t&&n(e,r,t),t};exports.SplitFlapBoard=class extends p.LitElement{constructor(){super(...arguments),this.spools=[],this.grid=[],this.speed=60,this.variant="minimal",this.visibleSideCount=-1,this._pendingSettle=!1}updated(e){super.updated(e),(e.has("spools")||e.has("grid"))&&(this._pendingSettle=!0,Promise.resolve().then(()=>this._checkAllSettled()))}_getSpoolEls(){return Array.from(this.renderRoot.querySelectorAll("split-flap-spool"))}_checkAllSettled(){if(!this._pendingSettle)return;const e=this._getSpoolEls();e.length!==0&&e.every(r=>r.isSettled)&&(this._pendingSettle=!1,this._dispatchBoardSettled(e))}_dispatchBoardSettled(e){let r=0;const s=this.spools.map(t=>t.map(()=>{var o,a;return(a=(o=e[r++])==null?void 0:o.currentValue)!=null?a:""}));this.dispatchEvent(new CustomEvent("board-settled",{detail:{grid:s},bubbles:!0,composed:!0}))}render(){return p.html`
2
+ ${this.spools.map((e,r)=>p.html`
3
+ <div class="board-row" style="z-index: ${r+1}">
4
+ ${e.map((s,t)=>{var o,a;return p.html`
5
+ <split-flap-spool
6
+ .flaps=${s}
7
+ .value=${(a=(o=this.grid[r])==null?void 0:o[t])!=null?a:""}
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=p.css`
17
+ :host {
18
+ display: inline-flex;
19
+ position: relative;
20
+ flex-direction: column;
21
+ box-sizing: border-box;
22
+ /* Inset shadows for side rails; render below all children so frame bars
23
+ * appear at the same visual level. */
24
+ box-shadow:
25
+ inset 14px 0 18px rgba(0, 0, 0, 0.55),
26
+ inset -14px 0 18px rgba(0, 0, 0, 0.55);
27
+ border-radius: var(--sfb-board-radius, 8px);
28
+ background: var(--sfb-board-bg, #1c1c1c);
29
+ padding: var(--sfb-board-padding, 10px);
30
+ }
31
+
32
+ /* Absolute overlay so the outer frame ring adds no layout space. */
33
+ :host::after {
34
+ position: absolute;
35
+ z-index: 10000;
36
+ inset: 0;
37
+ border: 10px solid var(--sfb-board-bg, #1c1c1c);
38
+ border-radius: var(--sfb-board-radius, 8px);
39
+ pointer-events: none;
40
+ content: '';
41
+ }
42
+
43
+ /* Frame bar caps the top padding of each row. z-index increases per row in
44
+ * render() so each bar sits above the drum content of the row above it. */
45
+ .board-row {
46
+ display: flex;
47
+ position: relative;
48
+ gap: var(--sfb-gap, 3px);
49
+ padding-block: 12px;
50
+ }
51
+
52
+ .board-row::before {
53
+ position: absolute;
54
+ z-index: 1;
55
+ inset: 0 0 auto 0;
56
+ box-shadow: 0 3px 10px rgba(0, 0, 0, 0.65);
57
+ background: var(--sfb-board-bg, #1c1c1c);
58
+ height: 10px;
59
+ content: '';
60
+ }
61
+ `,l([i.property({type:Array})],exports.SplitFlapBoard.prototype,"spools",2),l([i.property({type:Array})],exports.SplitFlapBoard.prototype,"grid",2),l([i.property({type:Number})],exports.SplitFlapBoard.prototype,"speed",2),l([i.property({type:String})],exports.SplitFlapBoard.prototype,"variant",2),l([i.property({type:Number})],exports.SplitFlapBoard.prototype,"visibleSideCount",2),exports.SplitFlapBoard=l([i.customElement("split-flap-board")],exports.SplitFlapBoard);
package/dist/cjs/index.js CHANGED
@@ -1,7 +1 @@
1
- 'use strict';
2
-
3
- function helloWorld() {
4
- }
5
-
6
- exports.helloWorld = helloWorld;
7
- //# sourceMappingURL=index.js.map
1
+ "use strict";var l=require("./lib/board.js"),o=require("./lib/flap.js"),e=require("./lib/spool-layout.js"),i=require("./SplitFlapBoard.js"),r=require("./spools/presets.js"),p=require("./spools/SplitFlapSpool.js"),t=require("./spools/SplitFlapSpoolBase.js"),a=require("./spools/SplitFlapSpoolMinimal.js"),S=require("./spools/SplitFlapSpoolRealistic.js");exports.fromLines=l.fromLines,exports.spoolGrid=l.spoolGrid,exports.getFlapKey=o.getFlapKey,exports.getMaxVisibleSideCount=e.getMaxVisibleSideCount,exports.getRenderedFlaps=e.getRenderedFlaps,exports.getSignedCircularOffset=e.getSignedCircularOffset,Object.defineProperty(exports,"SplitFlapBoard",{enumerable:!0,get:function(){return i.SplitFlapBoard}}),exports.charSpool=r.charSpool,exports.colorSpool=r.colorSpool,exports.numericSpool=r.numericSpool,Object.defineProperty(exports,"SplitFlapSpool",{enumerable:!0,get:function(){return p.SplitFlapSpool}}),exports.SplitFlapSpoolBase=t.SplitFlapSpoolBase,Object.defineProperty(exports,"SplitFlapSpoolMinimal",{enumerable:!0,get:function(){return a.SplitFlapSpoolMinimal}}),Object.defineProperty(exports,"SplitFlapSpoolRealistic",{enumerable:!0,get:function(){return S.SplitFlapSpoolRealistic}});
@@ -0,0 +1 @@
1
+ "use strict";var i=require("../spools/presets.js"),g=Object.defineProperty,h=Object.getOwnPropertySymbols,m=Object.prototype.hasOwnProperty,v=Object.prototype.propertyIsEnumerable,y=(t,r,e)=>r in t?g(t,r,{enumerable:!0,configurable:!0,writable:!0,value:e}):t[r]=e,u=(t,r)=>{for(var e in r||(r={}))m.call(r,e)&&y(t,e,r[e]);if(h)for(var e of h(r))v.call(r,e)&&y(t,e,r[e]);return t};function d(t,r,e){const o=t.length>0&&Array.isArray(t[0])?t:Array.from({length:r},()=>t);return Array.from({length:e},()=>Array.from({length:r},(l,a)=>{var p;return(p=o[a%o.length])!=null?p:i.charSpool}))}function A(t,r){const e=[],o=[];for(const l of t){const a=typeof l=="object",p=a?l.text:l,n=a?l.bg:void 0,s=a?l.color:void 0,f=p.toUpperCase().padEnd(r," ").slice(0,r).split("");if(n!=null||s!=null){const b=f.map(()=>i.charSpool.map(c=>c.type!=="char"?c:u(u(u({},c),n!=null?{bg:n}:{}),s!=null?{color:s}:{})));e.push(b)}else e.push(Array.from({length:r},()=>i.charSpool));o.push(f)}return{spools:e,grid:o}}exports.fromLines=A,exports.spoolGrid=d;
@@ -0,0 +1 @@
1
+ "use strict";function r(e){if(e.key!=null)return e.key;switch(e.type){case"char":case"color":return e.value;case"image":return e.src;case"custom":return e.key}}exports.getFlapKey=r;
@@ -0,0 +1 @@
1
+ "use strict";function s(n,t,e){if(e<=0)return 0;const a=(n-t+e)%e;if(a===0)return 0;const i=a-e;return Math.abs(i)<=Math.abs(a)?i:a}function u(n,t){const e=Math.floor(n/2);return t==null||t<0?e:Math.min(Math.floor(t),e)}function f(n,t,{visibleSideCount:e,renderCenter:a=t}={}){const i=u(n.length,e);return n.map((r,d)=>{const l=s(d,t,n.length);return{flap:r,actualIndex:d,offset:l,renderedIndex:a+l}}).filter(({offset:r})=>Math.abs(r)<=i).sort((r,d)=>r.renderedIndex-d.renderedIndex).map(({flap:r,actualIndex:d,renderedIndex:l})=>({flap:r,actualIndex:d,renderedIndex:l}))}exports.getMaxVisibleSideCount=u,exports.getRenderedFlaps=f,exports.getSignedCircularOffset=s;
@@ -0,0 +1,18 @@
1
+ "use strict";var o=require("lit"),r=require("lit/decorators.js"),S=require("./presets.js"),n=require("../lib/flap.js");require("./SplitFlapSpoolMinimal.js"),require("./SplitFlapSpoolRealistic.js");var v=Object.defineProperty,h=Object.getOwnPropertyDescriptor,i=(s,t,e,p)=>{for(var l=p>1?void 0:p?h(t,e):t,a=s.length-1,u;a>=0;a--)(u=s[a])&&(l=(p?u(t,e,l):u(l))||l);return p&&l&&v(t,e,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 t,e;return(e=(t=this._getActiveSpool())==null?void 0:t.currentValue)!=null?e:this.flaps[0]!=null?n.getFlapKey(this.flaps[0]):void 0}get isSettled(){var t,e;return(e=(t=this._getActiveSpool())==null?void 0:t.isSettled)!=null?e:!0}hasKey(t){var e,p;return(p=(e=this._getActiveSpool())==null?void 0:e.hasKey(t))!=null?p:this.flaps.some(l=>n.getFlapKey(l)===t)}_getActiveSpool(){return this.renderRoot.querySelector("split-flap-spool-minimal, split-flap-spool-realistic")}render(){return this.variant==="realistic"?o.html`
2
+ <split-flap-spool-realistic
3
+ .value=${this.value}
4
+ .flaps=${this.flaps}
5
+ .speed=${this.speed}
6
+ .visibleSideCount=${this.visibleSideCount}
7
+ ></split-flap-spool-realistic>
8
+ `:o.html`
9
+ <split-flap-spool-minimal
10
+ .value=${this.value}
11
+ .flaps=${this.flaps}
12
+ .speed=${this.speed}
13
+ ></split-flap-spool-minimal>
14
+ `}},exports.SplitFlapSpool.styles=o.css`
15
+ :host {
16
+ display: contents;
17
+ }
18
+ `,i([r.property({type:String})],exports.SplitFlapSpool.prototype,"variant",2),i([r.property({type:String})],exports.SplitFlapSpool.prototype,"value",2),i([r.property({type:Array})],exports.SplitFlapSpool.prototype,"flaps",2),i([r.property({type:Number})],exports.SplitFlapSpool.prototype,"speed",2),i([r.property({type:Number})],exports.SplitFlapSpool.prototype,"visibleSideCount",2),exports.SplitFlapSpool=i([r.customElement("split-flap-spool")],exports.SplitFlapSpool);
@@ -0,0 +1,16 @@
1
+ "use strict";var n=require("lit"),u=require("lit/decorators.js"),y=require("lit/directives/style-map.js"),I=require("./presets.js"),l=require("../lib/flap.js"),f=Object.defineProperty,v=Object.getOwnPropertySymbols,T=Object.prototype.hasOwnProperty,F=Object.prototype.propertyIsEnumerable,x=(s,e,t)=>e in s?f(s,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):s[e]=t,o=(s,e)=>{for(var t in e||(e={}))T.call(e,t)&&x(s,t,e[t]);if(v)for(var t of v(e))F.call(e,t)&&x(s,t,e[t]);return s},_=(s,e,t,r)=>{for(var i=void 0,h=s.length-1,p;h>=0;h--)(p=s[h])&&(i=p(e,t,i)||i);return i&&f(e,t,i),i};class a 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?l.getFlapKey(this.currentFlap):void 0}get isSettled(){return!this._stepping&&this._targetIndex===-1&&this._stepTimer==null&&this._animTimer==null}hasKey(e){return this.flaps.some(t=>l.getFlapKey(t)===e)}_startStepping(){if(!this.flaps.length){this._resetForEmptyFlaps();return}const e=this.flaps.findIndex(r=>l.getFlapKey(r)===this.value),t=this._stepping||this._stepTimer!=null;if(e===-1){this._targetIndex=-1,t&&this._scheduleAdvanceOrSettle(this._getRemainingAnimTime());return}if(this._targetIndex=e,this._currentIndex===this._targetIndex){t?this._scheduleAdvanceOrSettle(this._getRemainingAnimTime()):this._targetIndex=-1;return}t||this._doStep()}_doStep(){if(!this.flaps.length){this._resetForEmptyFlaps();return}const e=(this._currentIndex+1)%this.flaps.length;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._getRemainingAnimTime();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._getRemainingAnimTime());return}this._targetIndex=-1;const e=this.currentValue;e!=null&&this.dispatchEvent(new CustomEvent("settled",{detail:{value:e},bubbles:!0,composed:!0}))}_getRemainingAnimTime(){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],h=(r=e[this._prevIndex])!=null?r:i,p=i!=null?l.getFlapKey(i):void 0,c=h!=null?l.getFlapKey(h):p;this._clearTimers(),this._stepping=!1,this._targetIndex=-1;const m=p!=null?this.flaps.findIndex(d=>l.getFlapKey(d)===p):-1,g=c!=null?this.flaps.findIndex(d=>l.getFlapKey(d)===c):-1;this._currentIndex=m>=0?m: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(e,t){var r;switch(e.type){case"char":return n.html`
2
+ <div
3
+ class="char-inner"
4
+ style=${y.styleMap(o(o(o(o(o({},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
+ >
6
+ ${e.value}
7
+ </div>
8
+ `;case"color":return n.html`<div class="color-fill" style="background: ${e.value}"></div>`;case"image":return n.html`<img class="image-fill" src=${e.src} alt=${(r=e.alt)!=null?r:""} />`;case"custom":return n.html`<div class="custom-fill">${e[t]}</div>`}}_getFlaps(){var e;const t=this.currentFlap;return t==null?null:{current:t,prev:(e=this.flaps[this._prevIndex])!=null?e:t}}_renderCard(){const e=this._getFlaps();if(e==null)return n.nothing;const{current:t,prev:r}=e,i=this._stepping?r:t;return n.html`
9
+ <div class="half top">${this._renderHalf(t,"top")}</div>
10
+ <div class="half bottom">${this._renderHalf(i,"bottom")}</div>
11
+
12
+ ${this._stepping?n.html`
13
+ <div class="half top flipping">${this._renderHalf(r,"top")}</div>
14
+ <div class="half bottom flipping">${this._renderHalf(t,"bottom")}</div>
15
+ `:n.nothing}
16
+ `}}_([u.property({type:String})],a.prototype,"value"),_([u.property({type:Array})],a.prototype,"flaps"),_([u.property({type:Number})],a.prototype,"speed"),_([u.state()],a.prototype,"_currentIndex"),_([u.state()],a.prototype,"_prevIndex"),_([u.state()],a.prototype,"_stepping"),exports.SplitFlapSpoolBase=a;