footprint-storydeck 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/README.md +34 -5
- package/deck.css +19 -2
- package/deck.d.ts +8 -2
- package/dist/deck/check.js +11 -2
- package/dist/deck/compile.js +11 -5
- package/dist/deck/edit.js +3 -2
- package/dist/deck/init.js +10 -6
- package/dist/deck/layout.js +16 -10
- package/dist/deck/loops.js +75 -0
- package/dist/deck/page.js +3 -1
- package/dist/deck/review.js +3 -0
- package/dist/deck/transforms.js +93 -9
- package/dist/focus.js +13 -3
- package/dist/focusHtml.js +2 -2
- package/dist/video.js +16 -1
- package/focus.css +6 -1
- package/focus.d.ts +4 -0
- package/package.json +1 -1
- package/plugin/skills/storydeck/SKILL.md +12 -3
- package/site/public/deck-stage.js +99 -8
- package/video.d.ts +7 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,61 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.0 — living system diagrams: loops, wires between pieces, a wire's label, halo and running packets
|
|
4
|
+
|
|
5
|
+
- **A wire between pieces: `data-arrow="client to server"`.** The wire runs from the edge of one piece's box to the
|
|
6
|
+
edge of the other's (6 px clear), so it follows them: move a box, in the file or by Alt-drag, and its wires come
|
|
7
|
+
along — no four numbers to keep in step. Two or more wires between the same pieces each take a lane (80 px apart at
|
|
8
|
+
most, always inside both boxes), keeping to their left, so a request going right runs above its answer coming back
|
|
9
|
+
whatever the pieces are called. A bend may follow the names (`"gw to db 40"`). Words, not `>`, because a `>` would
|
|
10
|
+
end the tag. A name not placed, a wire to itself and pieces too close for a wire are refused, naming the fix; a
|
|
11
|
+
numbered wire is drawn byte for byte as before. Alt-dragging a wire between pieces is refused with its fix (move one
|
|
12
|
+
of its pieces). The starter deck now writes its four wires this way (they land within 5 px of where its numbers put them).
|
|
13
|
+
- **A wire's label, halo and running packets.** Three attributes on any wire: `data-label="InvokeAgentRuntime"` writes
|
|
14
|
+
its name beside its middle, on its left — above a level wire going right, below one coming back, beside a standing
|
|
15
|
+
one by half its width — in the wire's colour on an edge of the ground (the wire's box grows to hold it, so a wire
|
|
16
|
+
showing itself in shows its label too); class `halo` gives the line and its head a soft glow (`--wire-halo`); and
|
|
17
|
+
`data-packets="2" data-speed="5"` keeps packets running along the curve, evenly spaced, 5 s a run (3 by default;
|
|
18
|
+
1–6 packets, 0.5–60 s), fading in at the start and out before the head (`--packet`, `--packet-glow`). The packets are
|
|
19
|
+
loops, so they run only on the slide shown, in step from click to click, and where loops do not run (reduced motion,
|
|
20
|
+
print, motion off) the first rests at the wire's middle. They are the wire's, not the deck's loops: `review` and
|
|
21
|
+
`check` do not list them. A wire without these is drawn byte for byte as before; packets it cannot run, and a speed
|
|
22
|
+
with no packets, are refused naming the fix. They are read as a browser reads the tag (never from inside another
|
|
23
|
+
attribute's value, in any case, quoted or not); a `>` or `<` in a value is written `>` or `<`, since a tag ends
|
|
24
|
+
at its first `>`. A deck that already put its own `data-label`, `data-packets` or `data-speed` on a wire (for its
|
|
25
|
+
own CSS) now gets them drawn: rename them. A loop an icon file carries is now found by `review` and `check` too.
|
|
26
|
+
- **A piece and what it touches: `name+`.** A wire between pieces carries `data-ends` (the pieces it joins; a wire in
|
|
27
|
+
numbers may carry it too), so a click can name `gw+`: the gateway, the wires with it as an end, and the pieces at
|
|
28
|
+
their other ends — `data-on="gw+"` lights that neighbourhood and greys the rest. It reads anywhere a click names
|
|
29
|
+
pieces (`data-in`, `data-out`, `data-on`, `data-hot`, and the groups a mover moves: `subject`, `aside`, `place`,
|
|
30
|
+
`apart`); `check`'s lit-on-entry reads it the same way, and `review` lists what it lit. A piece actually called `gw+`
|
|
31
|
+
is itself. On a `path` click it lights the piece, then its wires, then what they reach — for a request's own order,
|
|
32
|
+
write the route out (`data-focus-route`), where `+` is not read. `footprint-storydeck/focus` exports `expandNames`.
|
|
33
|
+
|
|
34
|
+
- **Motion that keeps going: loops.** `data-loop="pulse"` on a piece keeps it pulsing while its slide is shown.
|
|
35
|
+
`pulse`, `float` and `marquee` are the endless moves of the motion table StoryReel's films use
|
|
36
|
+
(`footprint-story/motion`), sampled from the same curves and drawn into the page only when a deck names them
|
|
37
|
+
(`data-loop="float 4s"` sets the pace; any other name is refused at build, naming the piece and the loops there are).
|
|
38
|
+
A named loop moves its element by `transform` (a shape inside a drawing about its own middle), so a tag that sets a
|
|
39
|
+
transform of its own is refused, naming the fix: put data-loop on an element inside it. `data-loop` alone runs the
|
|
40
|
+
piece's own animation, `--loop` in theme.css (`.gear { --loop: spin 6s linear infinite; }`); a theme.css rule that
|
|
41
|
+
gives the element its own `animation` wins, so list `var(--loop)` in it to keep both.
|
|
42
|
+
<deck-stage> owns where loops run, so it works in any deck on the stage: only on the slide shown and the layers under
|
|
43
|
+
it (it stamps `data-deck-loop` on them), so a deck of 40 map clicks runs one slide's loops, not 40. Every loop runs
|
|
44
|
+
on one clock (only a data-loop element's animations are set to it: a deck's other animations are left as they were), so a map drawn again for the next click shows each loop where the last click left it, and a jump or a
|
|
45
|
+
step back keeps it running (a static arrival no longer zeroes a loop, only entrances). A loop never runs under
|
|
46
|
+
reduced motion (switched live), in print, or with motion off: there the piece rests in its own style. A piece that
|
|
47
|
+
loops and changes on the same click (comes in, lights up) does both: focus.css keeps the loop as the last animation
|
|
48
|
+
of the piece's list. Loop CSS is drawn only for decks that use loops, so a deck without one builds byte for byte as
|
|
49
|
+
before.
|
|
50
|
+
- **Motion off: a still deck.** `?motion=off` after the page's address, or `<deck-stage data-motion="off">` (switched
|
|
51
|
+
live too): no loops, and every click lands at once, as a jump does — so a screenshot of a click is the same every
|
|
52
|
+
time.
|
|
53
|
+
- **The checks know loops.** `review` lists each click's loops (`loops (2): db pulse, gear (its own --loop)`). `check`
|
|
54
|
+
warns (`loop-unset`) about a `data-loop` that neither theme.css nor its own style gives a `--loop` (nothing would
|
|
55
|
+
loop). `check --layout` reports an endless animation that is not a loop: one running on a slide no one sees is an
|
|
56
|
+
error (`hidden-loop`, with how many and on which pieces, one problem for all the slides it runs on); one shown on a
|
|
57
|
+
click is a warning (`loose-loop`: it starts over at each click and stops on a jump).
|
|
58
|
+
|
|
3
59
|
## 0.8.1 — footprint-narration is now footprint-story
|
|
4
60
|
|
|
5
61
|
- **footprint-narration is now footprint-story.** StoryDeck's shared narration code (shown-vs-spoken text, captions,
|
package/README.md
CHANGED
|
@@ -413,16 +413,45 @@ npx footprint-storydeck build my-talk # my-talk/index.html — one page, eve
|
|
|
413
413
|
its route, its notes), `check` (what to fix, each with its click and its fix: a headline over 12 words, pieces that
|
|
414
414
|
light just by coming in, a click that changes nothing, a piece never shown, clicks without notes, a theme theme.css
|
|
415
415
|
never styles). `check --layout`
|
|
416
|
-
opens the page in a browser, offline, and finds text that spills out of its box
|
|
416
|
+
opens the page in a browser, offline, and finds text that spills out of its box and endless animations that are not
|
|
417
|
+
loops (it needs Playwright installed beside
|
|
417
418
|
StoryDeck: `npm i -D footprint-storydeck playwright-core`; without it, it says so and the other checks run). `mcp` serves the same tools to an AI assistant:
|
|
418
419
|
`claude mcp add storydeck -- npx footprint-storydeck mcp my-talk`.
|
|
419
|
-
- **The helpers a diagram needs.** `<i data-k="w1" class="wire pkt" data-arrow="
|
|
420
|
-
SVG arrow
|
|
421
|
-
|
|
422
|
-
its
|
|
420
|
+
- **The helpers a diagram needs.** `<i data-k="w1" class="wire pkt" data-arrow="client to server"></i>` is a wire (an
|
|
421
|
+
SVG arrow) from the edge of one piece's box to the edge of the other's. It follows them: move a box (in the file, or
|
|
422
|
+
by Alt-drag in the dev page) and its wires go with it. Two wires between the same pieces each take a lane, keeping to
|
|
423
|
+
their left, so a request going right runs above its answer coming back; a bend may follow the names
|
|
424
|
+
(`"gw to db 40"`). `data-arrow="252 352 318 352"` places a wire by hand, in slide px. Class `pkt` adds a packet that
|
|
425
|
+
rides the wire on a `path` click, at its turn — along the curve itself, not a straight line across the bend. A wire
|
|
426
|
+
that comes in shows itself from its start to its tip, its dashes and colours as your theme draws them. A wire between
|
|
427
|
+
pieces knows its ends, so a click can be about a piece and everything it touches: `data-on="gw+"` lights the
|
|
428
|
+
gateway, its wires and the pieces at their other ends, and greys the rest (`review` lists them); it reads in every
|
|
429
|
+
list of names a click has, the groups a mover moves too (`data-focus-apart="gw+: 0 -200"`). A living system
|
|
430
|
+
diagram needs three more things on a wire, each an attribute: `data-label="InvokeAgentRuntime"` writes its name at its
|
|
431
|
+
middle, on its left (a request's above it, its answer's below; in the wire's colour, on an edge of the ground so it
|
|
432
|
+
reads over anything); class `halo` gives it a soft glow (`--wire-halo`); and `data-packets="2" data-speed="5"` keeps
|
|
433
|
+
packets running along it, evenly spaced, 5 s a run (`--packet`, `--packet-glow`). The packets are loops (see below):
|
|
434
|
+
only on the slide shown, in step from click to click, and under reduced motion, in print or with motion off one rests
|
|
435
|
+
at the wire's middle.
|
|
423
436
|
`<i class="ic" data-icon="server"></i>` is `icons/server.svg`. `deck.css` styles the chrome (kicker, headline, footer
|
|
424
437
|
and count), wires, packets, icons and the theme toggle from the token contract, every rule `:where()`, so your
|
|
425
438
|
theme wins.
|
|
439
|
+
- **Motion that keeps going: a loop.** `data-loop="pulse"` on a piece makes it pulse while its slide is shown;
|
|
440
|
+
`pulse`, `float` and `marquee` are the endless moves of the motion table StoryReel's films use
|
|
441
|
+
(`footprint-story/motion`), drawn into the page only when a deck names them (`data-loop="float 4s"` sets the pace).
|
|
442
|
+
`data-loop` alone runs the piece's own animation from theme.css: `.gear { --loop: spin 6s linear infinite; }`. A
|
|
443
|
+
named loop moves its element by `transform` (`marquee` slides it 1600 px left and starts over: a ticker), so it goes
|
|
444
|
+
on an element without a transform of its own — a tag that sets one is refused; one theme.css gives it is replaced
|
|
445
|
+
while the loop runs. A rule of theme.css that gives the element its own `animation` wins over its loop: list
|
|
446
|
+
`var(--loop)` in it to keep both. A
|
|
447
|
+
map is drawn again for every click, yet a loop never starts over: every loop in the deck runs on one clock, so the
|
|
448
|
+
next click shows it where the last one left it, and a jump or a step back keeps it running. A loop runs only on the
|
|
449
|
+
slide shown (a deck of 40 map clicks runs one slide's loops, not 40), and never under reduced motion, in print, or
|
|
450
|
+
with motion off — there the piece rests in its own style. **Motion off** is `?motion=off` after the page's address
|
|
451
|
+
(or `<deck-stage data-motion="off">`): no loops, and every click lands at once, so a screenshot of a click is the same
|
|
452
|
+
every time. `review` lists each click's loops; `check` warns about a `data-loop` that nothing gives a `--loop`, and
|
|
453
|
+
`check --layout` finds an endless animation that is not a loop: one running on a slide no one sees (an error: it
|
|
454
|
+
costs the whole deck), and one that starts over at each click.
|
|
426
455
|
- **A deck is files, never a program — on your machine.** No deck code runs in Node: no build script, no deck module.
|
|
427
456
|
Playwright (1.48 or newer), for `check --layout`, is loaded only from beside StoryDeck — never from the deck folder or
|
|
428
457
|
the folder you run in — and the page is measured offline: no name resolves, the browser context is offline and routed
|
package/deck.css
CHANGED
|
@@ -41,11 +41,11 @@
|
|
|
41
41
|
click it rides the curve once, at the wire's turn: --sd-t runs 0 → 1 and it stands at B(t) = 2(1-t)t·C + t²·E from
|
|
42
42
|
its start (C: --cdx/--cdy, E: --pdx/--pdy, from transforms.js). */
|
|
43
43
|
@property --sd-t { syntax: '<number>'; inherits: false; initial-value: 0; }
|
|
44
|
-
:where(svg.wire .pkt) {
|
|
45
|
-
opacity: 0; fill: var(--sd-glow, currentColor); stroke: none;
|
|
44
|
+
:where(svg.wire :is(.pkt, .run)) {
|
|
46
45
|
transform: translate(calc(var(--cdx) * 2 * (1 - var(--sd-t)) * var(--sd-t) + var(--pdx) * var(--sd-t) * var(--sd-t)),
|
|
47
46
|
calc(var(--cdy) * 2 * (1 - var(--sd-t)) * var(--sd-t) + var(--pdy) * var(--sd-t) * var(--sd-t)));
|
|
48
47
|
}
|
|
48
|
+
:where(svg.wire .pkt) { opacity: 0; fill: var(--sd-glow, currentColor); stroke: none; }
|
|
49
49
|
@keyframes sdPkt { from { --sd-t: 0; } to { --sd-t: 1; } }
|
|
50
50
|
@keyframes sdPktShow { 0%, 100% { opacity: 0; } 15%, 80% { opacity: 1; } }
|
|
51
51
|
/* a wire that comes in shows itself start to tip behind a sweeping soft edge (a mask: a theme's dashes stay), square to
|
|
@@ -70,6 +70,23 @@
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/* a wire's options (transforms.js): its label at its middle, in its colour, on a ground-coloured edge so it reads over
|
|
74
|
+
anything; class "halo", a soft glow; data-packets, packets that keep running along it — loops (deck-stage.js USAGE
|
|
75
|
+
(i)): on the slide shown, on one clock, each fading in at the start and out before the head. Where loops do not run
|
|
76
|
+
(reduced motion, print, motion off) the first packet rests at the wire's middle and the others are hidden. */
|
|
77
|
+
:where(svg.wire text.label) {
|
|
78
|
+
font: 600 18px/1 var(--mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace); letter-spacing: .06em;
|
|
79
|
+
text-transform: uppercase; dominant-baseline: central; fill: currentColor;
|
|
80
|
+
stroke: var(--bg, #ffffff); stroke-width: 6px; stroke-linejoin: round; paint-order: stroke;
|
|
81
|
+
}
|
|
82
|
+
:where(svg.wire.halo path) { filter: drop-shadow(0 0 6px var(--wire-halo, currentColor)); } /* the line and its head, not the label */
|
|
83
|
+
:where(svg.wire .run) {
|
|
84
|
+
--sd-t: .5; fill: var(--packet, var(--sd-glow, currentColor)); stroke: none;
|
|
85
|
+
filter: drop-shadow(0 0 5px var(--packet-glow, var(--packet, var(--sd-glow, currentColor))));
|
|
86
|
+
}
|
|
87
|
+
:where(svg.wire .run + .run) { opacity: 0; }
|
|
88
|
+
@keyframes sdRun { 0% { --sd-t: 0; opacity: 0; } 12%, 85% { opacity: 1; } 100% { --sd-t: 1; opacity: 0; } }
|
|
89
|
+
|
|
73
90
|
/* icons (data-icon): drawn in currentColor */
|
|
74
91
|
:where(svg.ic) { width: 60px; height: 60px; flex: none; fill: none; stroke: currentColor; stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; }
|
|
75
92
|
|
package/deck.d.ts
CHANGED
|
@@ -67,6 +67,8 @@ export interface DeckSource {
|
|
|
67
67
|
}
|
|
68
68
|
export function readDeck(dir: string): DeckSource;
|
|
69
69
|
|
|
70
|
+
/** A loop a click shows (deck-stage: data-loop runs its --loop on the slide shown): its piece's first name (null when the element is not a piece), and its value as written — a named loop ("pulse", "float 4s") or '' for the deck's own (`styled` when its own style sets --loop). */
|
|
71
|
+
export interface DeckLoop { readonly piece: string | null; readonly value: string; readonly styled?: true }
|
|
70
72
|
/** A click of the compiled deck, as review and check read it. */
|
|
71
73
|
export type DeckClick = {
|
|
72
74
|
readonly n: number;
|
|
@@ -76,6 +78,8 @@ export type DeckClick = {
|
|
|
76
78
|
/** The headline as plain text ('' on a plain slide). */
|
|
77
79
|
readonly headline: string;
|
|
78
80
|
readonly notes: readonly string[];
|
|
81
|
+
/** What loops on the click (on a map: on the pieces it shows). */
|
|
82
|
+
readonly loops: readonly DeckLoop[];
|
|
79
83
|
} & (
|
|
80
84
|
| { readonly kind: 'plain'; readonly map: null; readonly text: string }
|
|
81
85
|
| { readonly kind: 'map'; readonly map: string; readonly step: FocusClick; readonly look: FocusLook; readonly stepIndex: number; readonly steps: number }
|
|
@@ -92,6 +96,8 @@ export interface CompiledDeck {
|
|
|
92
96
|
readonly themeCss: string;
|
|
93
97
|
readonly slides: readonly DeckSlide[];
|
|
94
98
|
readonly clicks: readonly DeckClick[];
|
|
99
|
+
/** The named loops the clicks show (data-loop values), drawn into the page's CSS once each. */
|
|
100
|
+
readonly loops: readonly string[];
|
|
95
101
|
readonly maps: Readonly<Record<string, { readonly pieces: readonly FocusPiece[] }>>;
|
|
96
102
|
}
|
|
97
103
|
/** The deck's clicks planned (each map's state carried click to click) and drawn as <deck-stage> slides. */
|
|
@@ -133,10 +139,10 @@ export function formatProblems(problems: readonly DeckProblem[]): string;
|
|
|
133
139
|
/** The browser port the layout check drives (the video's deckStageDriver, with measure). */
|
|
134
140
|
export interface LayoutDriver {
|
|
135
141
|
open(url: string): Promise<void>;
|
|
136
|
-
measure(n: number): Promise<readonly { kind: 'spills' | 'outside'; piece: string | null; text: string; dx: number; dy: number }[]>;
|
|
142
|
+
measure(n: number): Promise<readonly { kind: 'spills' | 'outside' | 'loose-loop' | 'hidden-loop'; piece: string | null; text: string; dx: number; dy: number; slide?: number; count?: number }[]>;
|
|
137
143
|
close(): Promise<unknown> | unknown;
|
|
138
144
|
}
|
|
139
|
-
/** Text that spills out of its box
|
|
145
|
+
/** Text that spills out of its box, lit pieces off the slide, and endless animations that are not loops, measured click by click in a browser. */
|
|
140
146
|
export function layoutCheck(url: string, options: { clicks: readonly DeckClick[]; driver?: LayoutDriver; chromium?: unknown }): Promise<DeckProblem[]>;
|
|
141
147
|
|
|
142
148
|
export interface DeckServer {
|
package/dist/deck/check.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { wordCount } from "./parse.js";
|
|
2
|
+
import { expandNames } from "../focus.js";
|
|
3
|
+
import { LOOP_NAMES } from "./loops.js";
|
|
2
4
|
const names = (pieces, idx) => [...new Set(idx.map((i) => pieces[i].keys[0]))];
|
|
3
5
|
const problem = (level, rule, click, message) => ({ level, rule, click: click?.n ?? null, label: click?.label ?? null, message });
|
|
4
6
|
function words(c, { maxWords, maxSlideWords }) {
|
|
@@ -9,7 +11,7 @@ function words(c, { maxWords, maxSlideWords }) {
|
|
|
9
11
|
}
|
|
10
12
|
function litOnEntry(c, pieces) {
|
|
11
13
|
if (c.kind !== "map" || c.step.on === null || c.step.quiet) return [];
|
|
12
|
-
const on = c.step.on;
|
|
14
|
+
const on = expandNames(pieces, c.step.on);
|
|
13
15
|
if (on.includes("*")) return [];
|
|
14
16
|
const extra = c.look.pieces.map((p, i) => p.state === "on" && (p.change === "enter" || p.change === "arrive") && !pieces[i].keys.some((k) => on.includes(k)) ? i : null).filter((i) => i !== null);
|
|
15
17
|
if (!extra.length) return [];
|
|
@@ -53,6 +55,13 @@ function unstyledThemes(compiled) {
|
|
|
53
55
|
}
|
|
54
56
|
return out;
|
|
55
57
|
}
|
|
58
|
+
function unsetLoops(compiled) {
|
|
59
|
+
if (/--loop\s*:/.test(compiled.themeCss ?? "")) return [];
|
|
60
|
+
const bare = compiled.clicks.flatMap((c) => (c.loops ?? []).filter((l) => l.value === "" && !l.styled).map((l) => ({ c, piece: l.piece ?? "an element" })));
|
|
61
|
+
if (!bare.length) return [];
|
|
62
|
+
const list = [...new Set(bare.map((b) => b.piece))], one = list.length === 1;
|
|
63
|
+
return [problem("warn", "loop-unset", bare[0].c, `${list.join(", ")} ha${one ? "s" : "ve"} data-loop but nothing sets ${one ? "its" : "their"} --loop, so nothing loops \u2014 give ${one ? "it" : "them"} one in theme.css (.gear { --loop: spin 4s linear infinite }), or name a loop: data-loop="${LOOP_NAMES[0]}" (${LOOP_NAMES.join(", ")})`)];
|
|
64
|
+
}
|
|
56
65
|
function checkDeck(compiled) {
|
|
57
66
|
const out = [];
|
|
58
67
|
compiled.clicks.forEach((c, i) => {
|
|
@@ -60,7 +69,7 @@ function checkDeck(compiled) {
|
|
|
60
69
|
if (c.kind === "map") out.push(...litOnEntry(c, compiled.maps[c.map].pieces));
|
|
61
70
|
out.push(...noChange(c, compiled.clicks[i - 1]));
|
|
62
71
|
});
|
|
63
|
-
out.push(...neverShown(compiled), ...missingNotes(compiled), ...unstyledThemes(compiled));
|
|
72
|
+
out.push(...neverShown(compiled), ...missingNotes(compiled), ...unstyledThemes(compiled), ...unsetLoops(compiled));
|
|
64
73
|
return out;
|
|
65
74
|
}
|
|
66
75
|
const failure = (error) => [problem("error", "compile", null, error.message)];
|
package/dist/deck/compile.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { focusStep } from "../focus.js";
|
|
2
2
|
import { readPieces, readStep, drawFocus, wrapStage, slideClass, focusOverlay } from "../focusHtml.js";
|
|
3
|
-
import { applyTransforms } from "./transforms.js";
|
|
3
|
+
import { applyTransforms, icons as drawIcons } from "./transforms.js";
|
|
4
|
+
import { loopsIn } from "./loops.js";
|
|
4
5
|
import { plainText } from "./parse.js";
|
|
5
6
|
import { esc, stripComments } from "./html.js";
|
|
6
7
|
import { readAttributes, tagsWith } from "../pieceRules.js";
|
|
@@ -31,7 +32,10 @@ function prepareMap(name, html, icons, dev) {
|
|
|
31
32
|
}
|
|
32
33
|
const { html: out, pieces } = readPieces(drawn);
|
|
33
34
|
if (!pieces.length) throw new Error(`map "${name}" has no pieces \u2014 a piece is an element with data-k="name" and an inline left/top (width/height)`);
|
|
34
|
-
return { name, html: dev ? numberForDev(out) : out, pieces, state: null };
|
|
35
|
+
return { name, html: dev ? numberForDev(out) : out, pieces, loops: loopsIn(drawIcons(stripComments(html), icons), `map "${name}"`), state: null };
|
|
36
|
+
}
|
|
37
|
+
function shownLoops(map, look) {
|
|
38
|
+
return map.loops.filter((l) => l.piece === null || look.pieces.some((p, i) => p.state !== "gone" && map.pieces[i].keys.includes(l.piece)));
|
|
35
39
|
}
|
|
36
40
|
function planClicks(parts, maps, { area, canvas, icons }) {
|
|
37
41
|
let lastMap = null;
|
|
@@ -45,7 +49,7 @@ function planClicks(parts, maps, { area, canvas, icons }) {
|
|
|
45
49
|
} catch (e) {
|
|
46
50
|
throw new Error(`"${part.label}": ${e.message}`, { cause: e });
|
|
47
51
|
}
|
|
48
|
-
clicks.push({ label: part.label, part, kind: "plain", inner, notes: part.notes, headline: "", step: 0, steps: 1 });
|
|
52
|
+
clicks.push({ label: part.label, part, kind: "plain", inner, loops: loopsIn(drawIcons(part.body, icons), `"${part.label}"`), notes: part.notes, headline: "", step: 0, steps: 1 });
|
|
49
53
|
lastMap = null;
|
|
50
54
|
continue;
|
|
51
55
|
}
|
|
@@ -60,7 +64,7 @@ function planClicks(parts, maps, { area, canvas, icons }) {
|
|
|
60
64
|
}
|
|
61
65
|
map.state = r.state;
|
|
62
66
|
lastMap = map;
|
|
63
|
-
clicks.push({ label: `${part.label} \xB7 ${k + 1}`, part, kind: "map", map, step, look: r.look, headline: s.headline, notes: s.notes, stepIndex: k, steps: part.steps.length });
|
|
67
|
+
clicks.push({ label: `${part.label} \xB7 ${k + 1}`, part, kind: "map", map, step, look: r.look, loops: shownLoops(map, r.look), headline: s.headline, notes: s.notes, stepIndex: k, steps: part.steps.length });
|
|
64
68
|
});
|
|
65
69
|
}
|
|
66
70
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -113,9 +117,11 @@ function compileDeck(source, { dev = false } = {}) {
|
|
|
113
117
|
map: c.kind === "map" ? c.map.name : null,
|
|
114
118
|
headline: plainText(c.headline),
|
|
115
119
|
notes: c.notes,
|
|
120
|
+
loops: c.loops,
|
|
116
121
|
...c.kind === "map" ? { step: c.step, look: c.look, stepIndex: c.stepIndex, steps: c.steps } : { text: plainText(c.inner) }
|
|
117
122
|
}));
|
|
118
|
-
|
|
123
|
+
const loops = [...new Set(planned.flatMap((c) => c.loops.map((l) => l.value)))].filter(Boolean).sort();
|
|
124
|
+
return { config, title: config.title, themeCss: source.themeCss ?? "", slides, clicks, loops, maps: Object.fromEntries(Object.values(maps).map((m) => [m.name, { pieces: m.pieces }])) };
|
|
119
125
|
}
|
|
120
126
|
export {
|
|
121
127
|
compileDeck
|
package/dist/deck/edit.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { attrValue, pieceTags, readAttributes, styleLength, tagsWith } from "../pieceRules.js";
|
|
2
2
|
import { UNSEEN, commentRanges, stripComments } from "./html.js";
|
|
3
|
-
import { wireTags } from "./transforms.js";
|
|
3
|
+
import { wireEnds, wireTags } from "./transforms.js";
|
|
4
4
|
const FEEDBACK = "feedback.md";
|
|
5
5
|
const MAX_MOVE = 5e3;
|
|
6
6
|
const num = (v) => String(Math.round(v * 100) / 100);
|
|
@@ -53,7 +53,8 @@ function moveEdits(html, piece, dx, dy) {
|
|
|
53
53
|
if (browserValueAt(html, piece, "data-k") !== piece.start + 1 + piece.tag.length + piece.before.length + ' data-k="'.length) throw ambiguous();
|
|
54
54
|
const wire = wiresBySource(html).get(piece.start);
|
|
55
55
|
if (wire) {
|
|
56
|
-
const spec = wire.m[1], parts = spec.trim().split(/[\s,]+/), nums = parts.map(Number);
|
|
56
|
+
const spec = wire.m[1], parts = spec.trim().split(/[\s,]+/), nums = parts.map(Number), ends = wireEnds(spec);
|
|
57
|
+
if (ends) throw new Error(`"${piece.name}" runs from "${ends[0]}" to "${ends[1]}" and follows them \u2014 move one of them`);
|
|
57
58
|
if (parts.length !== 4 && parts.length !== 5 || nums.some((n) => !Number.isFinite(n))) {
|
|
58
59
|
throw new Error(`"${piece.name}": its data-arrow is not "x1 y1 x2 y2 [bend]" \u2014 fix it before moving it`);
|
|
59
60
|
}
|
package/dist/deck/init.js
CHANGED
|
@@ -9,16 +9,20 @@ const DECK_JSON = `{
|
|
|
9
9
|
const MAP = `<!--
|
|
10
10
|
The map: one picture the story is told on, in slide pixels (1920 \xD7 1080).
|
|
11
11
|
Every element with data-k="name" and an inline left/top/width/height is a piece a click can bring in, light,
|
|
12
|
-
grey or move. Pieces never nest. <i data-arrow="
|
|
13
|
-
|
|
12
|
+
grey or move. Pieces never nest. <i data-arrow="client to server"> becomes a wire from one piece's edge to the
|
|
13
|
+
other's (it follows them when they move; "x1 y1 x2 y2" in slide px places it by hand; a bend may follow either),
|
|
14
|
+
class "pkt": a packet runs along it on a path click. A wire may carry data-label="its name", class "halo", and
|
|
15
|
+
data-packets="2" data-speed="5" (packets that keep running along it). In a click, "server+" is the server, its wires
|
|
16
|
+
and what they reach. <i class="ic" data-icon="name"> is icons/name.svg. data-loop="pulse" (float, marquee)
|
|
17
|
+
keeps a piece moving while its slide is shown; data-loop alone runs its own --loop from theme.css.
|
|
14
18
|
-->
|
|
15
19
|
<div data-k="client" class="box" style="left:160px;top:420px;width:300px;height:210px"><i class="ic" data-icon="client"></i><b>Client</b><small>a browser</small></div>
|
|
16
|
-
<i data-k="req1" class="wire req pkt" data-arrow="
|
|
20
|
+
<i data-k="req1" class="wire req pkt" data-arrow="client to server"></i>
|
|
17
21
|
<div data-k="server" class="box" style="left:720px;top:420px;width:300px;height:210px"><i class="ic" data-icon="server"></i><b>Server</b><small>an API</small></div>
|
|
18
|
-
<i data-k="req2" class="wire req pkt" data-arrow="
|
|
22
|
+
<i data-k="req2" class="wire req pkt" data-arrow="server to db"></i>
|
|
19
23
|
<div data-k="db" class="box" style="left:1280px;top:420px;width:300px;height:210px"><i class="ic" data-icon="database"></i><b>Database</b><small>where the data lives</small></div>
|
|
20
|
-
<i data-k="ret2" class="wire ret pkt" data-arrow="
|
|
21
|
-
<i data-k="ret1" class="wire ret pkt" data-arrow="
|
|
24
|
+
<i data-k="ret2" class="wire ret pkt" data-arrow="db to server"></i>
|
|
25
|
+
<i data-k="ret1" class="wire ret pkt" data-arrow="server to client"></i>
|
|
22
26
|
<div data-k="time" class="chip" style="left:760px;top:670px;width:220px;height:70px"><b>120 ms</b><small>illustration</small></div>
|
|
23
27
|
`;
|
|
24
28
|
const CLICKS = `<!--
|
package/dist/deck/layout.js
CHANGED
|
@@ -25,20 +25,26 @@ function clickRuns(ns) {
|
|
|
25
25
|
}
|
|
26
26
|
return runs.map(([a, b]) => a === b ? `${a}` : `${a}\u2013${b}`).join(", ");
|
|
27
27
|
}
|
|
28
|
+
const KINDS = {
|
|
29
|
+
spills: (f, where, also) => ["warn", `text spills out of ${where} by ${size(f)} ("${f.text}")${also} \u2014 shorten it, or give the box more room`],
|
|
30
|
+
outside: (f, where, also) => ["warn", `${where} reaches ${size(f)} past the slide's edge ("${f.text}")${also} \u2014 move it in, or zoom less`],
|
|
31
|
+
"loose-loop": (f, where, also) => ["warn", `${where} has an endless animation that is not a loop${also}: it starts again at each click and stops on a jump \u2014 give the element data-loop and its animation as --loop (or name a loop: data-loop="pulse")`],
|
|
32
|
+
"hidden-loop": (f, where, also) => ["error", `${f.count} endless animation${f.count === 1 ? "" : "s"} on ${f.piece} keep${f.count === 1 ? "s" : ""} running while the slide is hidden${also} \u2014 make ${f.count === 1 ? "it a loop" : "each a loop"} (data-loop and its animation as --loop): a loop runs only on the slide shown`]
|
|
33
|
+
};
|
|
34
|
+
const size = ({ dx, dy }) => [dx > 0 && `${Math.round(dx)} px wide`, dy > 0 && `${Math.round(dy)} px tall`].filter(Boolean).join(" and ");
|
|
28
35
|
function layoutProblems(found, clicks) {
|
|
29
36
|
const same = /* @__PURE__ */ new Map();
|
|
30
37
|
for (const f of found) {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
same.
|
|
38
|
+
const at = f.kind === "hidden-loop" ? f.slide : f.n;
|
|
39
|
+
const key = JSON.stringify(f.kind === "hidden-loop" ? [f.kind, f.piece, f.count] : [f.kind, f.piece, f.text, Math.round(f.dx), Math.round(f.dy)]);
|
|
40
|
+
if (!same.has(key)) same.set(key, { ...f, ns: /* @__PURE__ */ new Set() });
|
|
41
|
+
same.get(key).ns.add(at);
|
|
34
42
|
}
|
|
35
|
-
return [...same.values()].sort((a, b) => a.ns[0] - b.ns[0]).map((
|
|
36
|
-
const c = clicks[ns[0] - 1];
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
const message = kind === "spills" ? `text spills out of ${where} by ${by} ("${text}")${also} \u2014 shorten it, or give the box more room` : `${where} reaches ${by} past the slide's edge ("${text}")${also} \u2014 move it in, or zoom less`;
|
|
41
|
-
return { level: "warn", rule: kind === "spills" ? "spills" : "outside", click: ns[0], label: c?.label ?? null, message };
|
|
43
|
+
return [...same.values()].map((f) => ({ ...f, ns: [...f.ns].sort((a, b) => a - b) })).sort((a, b) => a.ns[0] - b.ns[0]).map((f) => {
|
|
44
|
+
const c = clicks[f.ns[0] - 1];
|
|
45
|
+
const also = f.ns.length > 1 ? ` (on clicks ${clickRuns(f.ns)})` : "";
|
|
46
|
+
const [level, message] = KINDS[f.kind](f, f.piece ? `"${f.piece}"` : "an element", also);
|
|
47
|
+
return { level, rule: f.kind, click: f.ns[0], label: c?.label ?? null, message };
|
|
42
48
|
});
|
|
43
49
|
}
|
|
44
50
|
async function layoutCheck(url, { clicks, driver, chromium }) {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { moveAt, movesBy, readMove } from "footprint-story/motion";
|
|
2
|
+
import { readAttributes, tagsWith } from "../pieceRules.js";
|
|
3
|
+
const LOOP_NAMES = Object.freeze(movesBy({ kind: "ambient" }).map((m) => m.name));
|
|
4
|
+
const LOOP_SECONDS = Object.freeze([0.2, 60]);
|
|
5
|
+
const STEPS = 24;
|
|
6
|
+
const FORM = /^([a-z][a-z-]*)(?: (\d+(?:\.\d+)?)s)?$/;
|
|
7
|
+
function readLoop(value, where) {
|
|
8
|
+
if (value === "") return null;
|
|
9
|
+
const m = FORM.exec(value);
|
|
10
|
+
if (!m || !LOOP_NAMES.includes(m[1])) {
|
|
11
|
+
throw new Error(`${where}: data-loop="${value}" is not a loop \u2014 name one of ${LOOP_NAMES.join(", ")}, with its seconds if you like ("float 4s"), or write data-loop alone and give the piece its own --loop in theme.css`);
|
|
12
|
+
}
|
|
13
|
+
const seconds = m[2] === void 0 ? readMove(m[1]).seconds : Number(m[2]);
|
|
14
|
+
if (!(seconds >= LOOP_SECONDS[0] && seconds <= LOOP_SECONDS[1])) {
|
|
15
|
+
throw new Error(`${where}: data-loop="${value}" \u2014 a cycle lasts ${LOOP_SECONDS[0]} to ${LOOP_SECONDS[1]} s`);
|
|
16
|
+
}
|
|
17
|
+
return { name: m[1], seconds };
|
|
18
|
+
}
|
|
19
|
+
function loopsIn(html, where) {
|
|
20
|
+
const out = [];
|
|
21
|
+
for (const t of tagsWith(html, /\sdata-loop(?![\w-])/i)) {
|
|
22
|
+
const attrs = readAttributes(t.before + t.m[0] + t.after);
|
|
23
|
+
if (!attrs) throw new Error(`${where}: <${t.tag}> with data-loop holds a quote never closed before its ">" \u2014 write a ">" inside a value as >`);
|
|
24
|
+
const loop = attrs.find((a) => a.name === "data-loop");
|
|
25
|
+
if (!loop) continue;
|
|
26
|
+
const k = attrs.find((a) => a.name === "data-k")?.value.trim().split(/\s+/)[0] || null, at = k ? `${where}, piece "${k}"` : where;
|
|
27
|
+
readLoop(loop.value, at);
|
|
28
|
+
const style = attrs.find((a) => a.name === "style")?.value ?? "";
|
|
29
|
+
if (loop.value !== "" && (attrs.some((a) => a.name === "transform") || /(?:^|;)\s*transform\s*:/i.test(style))) {
|
|
30
|
+
throw new Error(`${at}: data-loop="${loop.value}" moves the element by its transform, which would replace the one it has \u2014 put data-loop on an element inside it`);
|
|
31
|
+
}
|
|
32
|
+
const styled = loop.value === "" && /(?:^|;)\s*--loop\s*:/.test(style);
|
|
33
|
+
out.push({ piece: k, value: loop.value, ...styled ? { styled } : {} });
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
const r4 = (v) => +(+v).toFixed(4) + 0;
|
|
38
|
+
const TRANSFORM = ["x", "y", "rotate", "scale"];
|
|
39
|
+
function frameCss(v, props) {
|
|
40
|
+
const out = [];
|
|
41
|
+
if (TRANSFORM.some((p) => p in props)) {
|
|
42
|
+
const parts = [];
|
|
43
|
+
if ("x" in props || "y" in props) parts.push(`translate(${r4(v.x)}px, ${r4(v.y)}px)`);
|
|
44
|
+
if ("rotate" in props) parts.push(`rotate(${r4(v.rotate)}deg)`);
|
|
45
|
+
if ("scale" in props) parts.push(`scale(${r4(v.scale)})`);
|
|
46
|
+
out.push(`transform: ${parts.join(" ")}`);
|
|
47
|
+
}
|
|
48
|
+
if ("opacity" in props) out.push(`opacity: ${r4(v.opacity)}`);
|
|
49
|
+
if ("blur" in props) out.push(`filter: blur(${r4(v.blur)}px)`);
|
|
50
|
+
return out.join("; ");
|
|
51
|
+
}
|
|
52
|
+
function keyframes(name) {
|
|
53
|
+
const m = readMove(name), at = (i) => moveAt(m, i / STEPS * m.seconds * (1 - 1e-9), { seconds: m.seconds });
|
|
54
|
+
const frames = Array.from({ length: STEPS + 1 }, (_, i) => `${r4(i / STEPS * 100)}% { ${frameCss(at(i), m.props)}; }`);
|
|
55
|
+
return `@keyframes sd-loop-${name} { ${frames.join(" ")} }`;
|
|
56
|
+
}
|
|
57
|
+
function loopCss(values) {
|
|
58
|
+
const named = [...new Set(values)].filter((v) => v !== "").sort(), names = /* @__PURE__ */ new Set();
|
|
59
|
+
if (!named.length) return "";
|
|
60
|
+
const rules = named.map((v) => {
|
|
61
|
+
const { name, seconds } = readLoop(v, "loopCss");
|
|
62
|
+
names.add(name);
|
|
63
|
+
return `:where([data-loop="${v}"]) { --loop: sd-loop-${name} ${r4(seconds)}s linear infinite; }`;
|
|
64
|
+
});
|
|
65
|
+
const shapes = ':where(svg [data-loop]:not([data-loop=""])) { transform-box: fill-box; transform-origin: center; }';
|
|
66
|
+
return [...[...names].map(keyframes), ...rules, shapes].join("\n");
|
|
67
|
+
}
|
|
68
|
+
export {
|
|
69
|
+
LOOP_NAMES,
|
|
70
|
+
LOOP_SECONDS,
|
|
71
|
+
frameCss,
|
|
72
|
+
loopCss,
|
|
73
|
+
loopsIn,
|
|
74
|
+
readLoop
|
|
75
|
+
};
|
package/dist/deck/page.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { focusRuntime } from "../focusHtml.js";
|
|
2
2
|
import { notesData, notesRuntime, listenRuntime } from "../narrationHtml.js";
|
|
3
3
|
import { esc, scriptSafe, styleSafe } from "./html.js";
|
|
4
|
+
import { loopCss } from "./loops.js";
|
|
4
5
|
function themeRuntime() {
|
|
5
6
|
const root = document.documentElement, btn = document.querySelector(".theme-toggle");
|
|
6
7
|
const set = (dark) => {
|
|
@@ -32,7 +33,8 @@ const THEME_FIRST = "(function(){var d;try{var t=localStorage.getItem('theme');d
|
|
|
32
33
|
const TOGGLE = '<button class="theme-toggle" type="button" aria-label="Switch theme" title="Light / dark (T)"><svg class="sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path stroke-linecap="round" d="M12 2v2M12 20v2M2 12h2M20 12h2M5 5l1.5 1.5M17.5 17.5L19 19M19 5l-1.5 1.5M6.5 17.5L5 19"/></svg><svg class="moon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg></button>';
|
|
33
34
|
function deckPage(compiled, assets, { dev = null } = {}) {
|
|
34
35
|
const { config, slides } = compiled;
|
|
35
|
-
const
|
|
36
|
+
const loops = loopCss(compiled.loops ?? []);
|
|
37
|
+
const css = [...assets.css, ...loops ? [loops] : [], compiled.themeCss ?? ""].join("\n");
|
|
36
38
|
const script = dev?.nonce ? `<script nonce="${esc(dev.nonce)}">` : "<script>";
|
|
37
39
|
const runtimes = [focusRuntime, notesRuntime, listenRuntime, themeRuntime].map((fn) => `${script}(${scriptSafe(fn)})();<\/script>`);
|
|
38
40
|
return `<!doctype html>
|
package/dist/deck/review.js
CHANGED
|
@@ -4,6 +4,7 @@ const unique = (names) => [...new Set(names)];
|
|
|
4
4
|
const list = (names, max = 14) => names.length ? `${names.slice(0, max).join(", ")}${names.length > max ? `, \u2026 (${names.length - max} more)` : ""}` : "\u2014";
|
|
5
5
|
const themed = (c) => c.theme ? ` \xB7 theme ${c.theme}` : "";
|
|
6
6
|
const quote = (s, max = 160) => s.length > max ? `"${s.slice(0, max - 1)}\u2026"` : `"${s}"`;
|
|
7
|
+
const loopsLine = (c) => c.loops?.length ? `loops (${c.loops.length}): ${list(c.loops.map((l) => `${l.piece ?? "an element"} ${l.value || "(its own --loop)"}`), 10)}` : null;
|
|
7
8
|
function pieceGroups(click, pieces) {
|
|
8
9
|
const by = (pred) => unique(click.look.pieces.map((p, i) => pred(p) ? nameOf(pieces, i) : null).filter(Boolean));
|
|
9
10
|
return {
|
|
@@ -59,6 +60,7 @@ function reviewClick(compiled, which, { maxWords = compiled.config?.checks?.maxW
|
|
|
59
60
|
if (c.kind === "plain") {
|
|
60
61
|
lines.push(`click ${c.n} of ${compiled.clicks.length} \xB7 "${c.label}" (plain slide)${themed(c)}`);
|
|
61
62
|
lines.push(`text (${words(wordCount(c.text))}): ${quote(c.text, 300)}`);
|
|
63
|
+
if (loopsLine(c)) lines.push(loopsLine(c));
|
|
62
64
|
} else {
|
|
63
65
|
lines.push(`click ${c.n} of ${compiled.clicks.length} \xB7 "${c.label}" (map "${c.map}", step ${c.stepIndex + 1} of ${c.steps})${themed(c)}`);
|
|
64
66
|
const hw = wordCount(c.headline);
|
|
@@ -82,6 +84,7 @@ function reviewClick(compiled, which, { maxWords = compiled.config?.checks?.maxW
|
|
|
82
84
|
lines.push(`this click: ${changes.length ? changes.join(" \xB7 ") : "nothing changes on the map"}`);
|
|
83
85
|
const motion = motionOf(c.look);
|
|
84
86
|
if (motion.length) lines.push(`motion: ${motion.join(" \xB7 ")}`);
|
|
87
|
+
if (loopsLine(c)) lines.push(loopsLine(c));
|
|
85
88
|
}
|
|
86
89
|
const noteWords = c.notes.reduce((sum, p) => sum + wordCount(p), 0);
|
|
87
90
|
lines.push(c.notes.length ? `notes (${c.notes.length}, ${words(noteWords)}): ${quote(c.notes.join(" "), 240)}` : "notes: \u2014");
|
package/dist/deck/transforms.js
CHANGED
|
@@ -1,16 +1,46 @@
|
|
|
1
1
|
import { stripComments } from "./html.js";
|
|
2
|
+
import { readPieces } from "../focusHtml.js";
|
|
3
|
+
import { readAttributes } from "../pieceRules.js";
|
|
2
4
|
const fixed = (v) => +v.toFixed(1);
|
|
5
|
+
const BETWEEN = /^\s*(\S+)\s+to\s+(\S+)(?:\s+(-?\d+(?:\.\d+)?))?\s*$/;
|
|
6
|
+
const wireEnds = (spec) => {
|
|
7
|
+
const m = BETWEEN.exec(spec);
|
|
8
|
+
return m ? [m[1], m[2]] : null;
|
|
9
|
+
};
|
|
10
|
+
const LANE = 80;
|
|
11
|
+
const GAP = 6;
|
|
12
|
+
const MOST_PACKETS = 6;
|
|
13
|
+
const SPEEDS = [0.5, 60];
|
|
14
|
+
const LABEL = { size: 18, char: 12, clear: 8 };
|
|
15
|
+
function wireOptions(attrs, spec) {
|
|
16
|
+
const where = `wire data-arrow="${spec}"`, read = readAttributes(attrs);
|
|
17
|
+
if (!read) throw new Error(`${where}: a quote in its tag is never closed \u2014 write a ">" or "<" inside a value as > or <`);
|
|
18
|
+
const value = (name) => read.find((a) => a.name === name)?.value;
|
|
19
|
+
const label = value("data-label")?.trim() || null, packets = value("data-packets"), speed = value("data-speed");
|
|
20
|
+
if (packets !== void 0 && !/^[1-9]\d*$/.test(packets.trim())) throw new Error(`${where}: data-packets="${packets}" \u2014 the packets that run along it are a whole number, 1 to ${MOST_PACKETS}`);
|
|
21
|
+
const n = packets === void 0 ? 0 : Number(packets);
|
|
22
|
+
if (n > MOST_PACKETS) throw new Error(`${where}: data-packets="${packets}" \u2014 at most ${MOST_PACKETS} packets run along one wire`);
|
|
23
|
+
if (speed !== void 0 && !n) throw new Error(`${where}: data-speed sets how fast its packets run \u2014 give it data-packets too`);
|
|
24
|
+
const s = speed === void 0 ? 3 : /^\s*\d+(?:\.\d+)?s?\s*$/.test(speed) ? parseFloat(speed) : NaN;
|
|
25
|
+
if (!(s >= SPEEDS[0] && s <= SPEEDS[1])) throw new Error(`${where}: data-speed="${speed}" \u2014 the seconds a packet takes to run the wire, ${SPEEDS[0]} to ${SPEEDS[1]}`);
|
|
26
|
+
return { label, packets: n, speed: s };
|
|
27
|
+
}
|
|
3
28
|
function wireSvg(attrs, spec) {
|
|
4
29
|
const parts = spec.trim().split(/[\s,]+/).map(Number);
|
|
5
30
|
if (parts.length !== 4 && parts.length !== 5 || parts.some((n) => !Number.isFinite(n))) {
|
|
6
|
-
throw new Error(`data-arrow="${spec}": a wire is "x1 y1 x2 y2"
|
|
31
|
+
throw new Error(`data-arrow="${spec}": a wire is the pieces it joins, "from to to" ("client to server"), or "x1 y1 x2 y2" in slide px \u2014 either with an optional bend`);
|
|
7
32
|
}
|
|
8
|
-
const [x1, y1, x2, y2, bend = 0] = parts;
|
|
33
|
+
const [x1, y1, x2, y2, bend = 0] = parts, o = wireOptions(attrs, spec);
|
|
9
34
|
const dx = x2 - x1, dy = y2 - y1, len = Math.hypot(dx, dy) || 1;
|
|
10
35
|
const cx = (x1 + x2) / 2 - dy / len * bend, cy = (y1 + y2) / 2 + dx / len * bend;
|
|
11
36
|
const tl = Math.hypot(x2 - cx, y2 - cy) || 1, ux = (x2 - cx) / tl, uy = (y2 - cy) / tl, hs = 18;
|
|
12
|
-
const
|
|
13
|
-
|
|
37
|
+
const half = o.label ? [o.label.length * LABEL.char / 2 + 4, LABEL.size / 2 + 4] : null;
|
|
38
|
+
let off = half ? Math.abs(dy) / len * half[0] + Math.abs(dx) / len * half[1] + LABEL.clear : 0;
|
|
39
|
+
if (half && bend > 0) off += Math.min(bend / 2, 2 * bend * (Math.abs(dx) / len * half[0] + Math.abs(dy) / len * half[1]) ** 2 / len ** 2);
|
|
40
|
+
const lx = 0.25 * x1 + 0.5 * cx + 0.25 * x2 + dy / len * off, ly = 0.25 * y1 + 0.5 * cy + 0.25 * y2 - dx / len * off;
|
|
41
|
+
const xs = [x1, x2, cx, ...half ? [lx - half[0] + 20, lx + half[0] - 20] : []], ys = [y1, y2, cy, ...half ? [ly - half[1] + 20, ly + half[1] - 20] : []];
|
|
42
|
+
const pad = 20, X = Math.min(...xs) - pad, Y = Math.min(...ys) - pad;
|
|
43
|
+
const W = Math.max(...xs) + pad - X, H = Math.max(...ys) + pad - Y, f = fixed;
|
|
14
44
|
const ex = x2 - ux * hs * 0.8, ey = y2 - uy * hs * 0.8;
|
|
15
45
|
const d = `M${f(x1 - X)} ${f(y1 - Y)} Q${f(cx - X)} ${f(cy - Y)} ${f(ex - X)} ${f(ey - Y)}`;
|
|
16
46
|
const tip = [x2 - X, y2 - Y], back = [tip[0] - ux * hs, tip[1] - uy * hs], nx = -uy * hs * 0.6, ny = ux * hs * 0.6;
|
|
@@ -18,8 +48,11 @@ function wireSvg(attrs, spec) {
|
|
|
18
48
|
let at = attrs.replace(/\sclass="([^"]*)"/, (_, c) => ` class="${/(^|\s)wire(\s|$)/.test(c) ? c : `wire ${c}`}"`);
|
|
19
49
|
if (!/\sclass="/.test(at)) at += ' class="wire"';
|
|
20
50
|
const pkt = /\bpkt\b/.test(at) ? `<rect class="pkt" x="${f(x1 - X - 8)}" y="${f(y1 - Y - 6)}" width="16" height="12" rx="3" style="--cdx:${f(cx - x1)}px;--cdy:${f(cy - y1)}px;--pdx:${f(ex - x1)}px;--pdy:${f(ey - y1)}px"/>` : "";
|
|
51
|
+
const ride = `--cdx:${f(cx - x1)}px;--cdy:${f(cy - y1)}px;--pdx:${f(ex - x1)}px;--pdy:${f(ey - y1)}px`;
|
|
52
|
+
const runs = Array.from({ length: o.packets }, (_, k) => `<circle class="run" data-loop cx="${f(x1 - X)}" cy="${f(y1 - Y)}" r="7" style="${ride};--loop:sdRun ${o.speed}s linear ${+(-k * o.speed / o.packets).toFixed(3) + 0}s infinite"/>`).join("");
|
|
53
|
+
const label = o.label ? `<text class="label" x="${f(lx - X)}" y="${f(ly - Y)}" text-anchor="middle">${o.label}</text>` : "";
|
|
21
54
|
const angle = f(Math.atan2(dx, -dy) * 180 / Math.PI);
|
|
22
|
-
return `<svg${at} style="left:${f(X)}px;top:${f(Y)}px;width:${f(W)}px;height:${f(H)}px;--sd-angle:${angle}deg" viewBox="0 0 ${f(W)} ${f(H)}" aria-hidden="true"><path d="${d}"/>${head}${pkt}</svg>`;
|
|
55
|
+
return `<svg${at} style="left:${f(X)}px;top:${f(Y)}px;width:${f(W)}px;height:${f(H)}px;--sd-angle:${angle}deg" viewBox="0 0 ${f(W)} ${f(H)}" aria-hidden="true"><path d="${d}"/>${head}${pkt}${runs}${label}</svg>`;
|
|
23
56
|
}
|
|
24
57
|
function emptyI(html, rule) {
|
|
25
58
|
const out = [];
|
|
@@ -31,15 +64,65 @@ function emptyI(html, rule) {
|
|
|
31
64
|
}
|
|
32
65
|
function replaceEach(html, found, draw) {
|
|
33
66
|
let out = "", from = 0;
|
|
34
|
-
|
|
35
|
-
out += html.slice(from, f.start) + draw(f);
|
|
67
|
+
found.forEach((f, i) => {
|
|
68
|
+
out += html.slice(from, f.start) + draw(f, i);
|
|
36
69
|
from = f.end;
|
|
37
|
-
}
|
|
70
|
+
});
|
|
38
71
|
return out + html.slice(from);
|
|
39
72
|
}
|
|
73
|
+
function exit(b, [px, py], [ux, uy]) {
|
|
74
|
+
const tx = ux > 0 ? (b.x + b.w - px) / ux : ux < 0 ? (b.x - px) / ux : Infinity;
|
|
75
|
+
const ty = uy > 0 ? (b.y + b.h - py) / uy : uy < 0 ? (b.y - py) / uy : Infinity;
|
|
76
|
+
return Math.min(tx, ty);
|
|
77
|
+
}
|
|
78
|
+
function wiresBetween(html, found) {
|
|
79
|
+
const read = found.map((f) => BETWEEN.exec(f.m[1]));
|
|
80
|
+
if (!read.some(Boolean)) return read;
|
|
81
|
+
const { pieces } = readPieces(html), placed = pieces.filter((p) => p.box && p.box.w > 0 && p.box.h > 0);
|
|
82
|
+
const boxOf = (name, spec) => {
|
|
83
|
+
const boxes = placed.filter((p) => p.keys[0] === name).map((p) => p.box);
|
|
84
|
+
if (!boxes.length) {
|
|
85
|
+
const names = [...new Set(placed.map((p) => p.keys[0]))];
|
|
86
|
+
throw new Error(`data-arrow="${spec}": no piece "${name}" with a box (data-k, and an inline left, top, width and height)${names.length ? ` \u2014 the placed pieces are ${names.slice(0, 20).join(", ")}${names.length > 20 ? ", \u2026" : ""}` : ""}`);
|
|
87
|
+
}
|
|
88
|
+
const x0 = Math.min(...boxes.map((b) => b.x)), y0 = Math.min(...boxes.map((b) => b.y));
|
|
89
|
+
return { x: x0, y: y0, w: Math.max(...boxes.map((b) => b.x + b.w)) - x0, h: Math.max(...boxes.map((b) => b.y + b.h)) - y0 };
|
|
90
|
+
};
|
|
91
|
+
const pairOf = (m) => [m[1], m[2]].sort().join("\0"), lanes = /* @__PURE__ */ new Map();
|
|
92
|
+
read.forEach((m, i) => {
|
|
93
|
+
if (m) lanes.set(pairOf(m), [...lanes.get(pairOf(m)) ?? [], i]);
|
|
94
|
+
});
|
|
95
|
+
for (const [key, ids] of lanes) {
|
|
96
|
+
const along = (i) => read[i][1] === key.split("\0")[0];
|
|
97
|
+
lanes.set(key, [...ids.filter(along), ...ids.filter((i) => !along(i))]);
|
|
98
|
+
}
|
|
99
|
+
return read.map((m, i) => {
|
|
100
|
+
if (!m) return null;
|
|
101
|
+
const spec = found[i].m[1];
|
|
102
|
+
if (m[1] === m[2]) throw new Error(`data-arrow="${spec}": a wire joins two pieces, not "${m[1]}" to itself`);
|
|
103
|
+
const [a, b] = [m[1], m[2]].sort(), A = boxOf(a, spec), B = boxOf(b, spec);
|
|
104
|
+
const ca = [A.x + A.w / 2, A.y + A.h / 2], cb = [B.x + B.w / 2, B.y + B.h / 2], len = Math.hypot(cb[0] - ca[0], cb[1] - ca[1]);
|
|
105
|
+
if (len === 0) throw new Error(`data-arrow="${spec}": "${a}" and "${b}" share a middle \u2014 place them apart, or give the wire numbers`);
|
|
106
|
+
const u = [(cb[0] - ca[0]) / len, (cb[1] - ca[1]) / len], n = [-u[1], u[0]];
|
|
107
|
+
const across = (box) => (Math.abs(n[0]) * box.w + Math.abs(n[1]) * box.h) / 2;
|
|
108
|
+
const lane = lanes.get(pairOf(m)), k = lane.indexOf(i), most = Math.max(0, Math.min(across(A), across(B)) - 8);
|
|
109
|
+
const step = lane.length > 1 ? Math.min(LANE, 2 * most / (lane.length - 1)) : 0, off = (k - (lane.length - 1) / 2) * step;
|
|
110
|
+
const pa = [ca[0] + n[0] * off, ca[1] + n[1] * off], pb = [cb[0] + n[0] * off, cb[1] + n[1] * off];
|
|
111
|
+
const ta = exit(A, pa, u) + GAP, tb = exit(B, pb, [-u[0], -u[1]]) + GAP;
|
|
112
|
+
if (len - ta - tb < 12) throw new Error(`data-arrow="${spec}": "${a}" and "${b}" are too close for a wire between them (they touch or overlap) \u2014 move them apart, or give the wire numbers`);
|
|
113
|
+
const start = [pa[0] + u[0] * ta, pa[1] + u[1] * ta], end = [pb[0] - u[0] * tb, pb[1] - u[1] * tb];
|
|
114
|
+
const [from, to] = m[1] === a ? [start, end] : [end, start];
|
|
115
|
+
return [...from, ...to, Number(m[3] ?? 0)].map(fixed).join(" ");
|
|
116
|
+
});
|
|
117
|
+
}
|
|
40
118
|
const wireTags = (html) => emptyI(html, /\sdata-arrow="([^"<>]+)"/);
|
|
41
119
|
function wires(html) {
|
|
42
|
-
|
|
120
|
+
const found = wireTags(html), between = wiresBetween(html, found);
|
|
121
|
+
return replaceEach(html, found, ({ before, m, after }, i) => {
|
|
122
|
+
if (!between[i]) return wireSvg(before + after, m[1]);
|
|
123
|
+
const [, from, to] = BETWEEN.exec(m[1]);
|
|
124
|
+
return wireSvg(` data-ends="${from} ${to}"${before}${after}`, between[i]);
|
|
125
|
+
});
|
|
43
126
|
}
|
|
44
127
|
function readIcon(name, svg) {
|
|
45
128
|
const text = stripComments(svg).replace(/^\s*<\?xml[^>]*>/, "").trim();
|
|
@@ -66,6 +149,7 @@ export {
|
|
|
66
149
|
applyTransforms,
|
|
67
150
|
icons,
|
|
68
151
|
readIcon,
|
|
152
|
+
wireEnds,
|
|
69
153
|
wireTags,
|
|
70
154
|
wires
|
|
71
155
|
};
|
package/dist/focus.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
const KINDS = ["look", "camera", "mover", "overlay", "order"];
|
|
2
|
+
function expandNames(pieces, list) {
|
|
3
|
+
const own = new Set(pieces.flatMap((p) => p.keys));
|
|
4
|
+
return list.flatMap((x) => {
|
|
5
|
+
if (!x.endsWith("+") || x.length < 2 || own.has(x)) return [x];
|
|
6
|
+
const name = x.slice(0, -1), wires = pieces.filter((p) => p.ends?.includes(name));
|
|
7
|
+
return [.../* @__PURE__ */ new Set([name, ...wires.map((p) => p.keys[0]), ...wires.flatMap((p) => p.ends.filter((e) => e !== name))])];
|
|
8
|
+
});
|
|
9
|
+
}
|
|
2
10
|
function union(boxes) {
|
|
3
11
|
const known = boxes.filter(Boolean);
|
|
4
12
|
if (!known.length) return null;
|
|
@@ -194,7 +202,7 @@ function changeOf(st, was, look, wasLook, entering, before) {
|
|
|
194
202
|
function focusStep(prev, step, deck) {
|
|
195
203
|
const { pieces, strategies = {} } = deck, canvas = deck.canvas ?? { w: 1920, h: 1080 };
|
|
196
204
|
const view = deck.view ?? { s: 1, x: 0, y: 0 }, areaSlide = deck.area ?? [0, 0, canvas.w, canvas.h];
|
|
197
|
-
const names = (l) => (l ?? []).flatMap((x) => String(x).trim().split(/\s+/)).filter(Boolean);
|
|
205
|
+
const names = (l) => expandNames(pieces, (l ?? []).flatMap((x) => String(x).trim().split(/\s+/)).filter(Boolean));
|
|
198
206
|
const ins = names(step.in), outs = names(step.out), on = step.on === void 0 || step.on === null ? null : names(step.on), hot = names(step.hot);
|
|
199
207
|
const has = (i, l) => l.includes("*") || l.some((x) => pieces[i].keys.includes(x));
|
|
200
208
|
const known = (l, what) => {
|
|
@@ -224,14 +232,15 @@ function focusStep(prev, step, deck) {
|
|
|
224
232
|
const subject = union(subjectIdx.map((i) => pieces[i].box)), context = union(contextIdx.map((i) => pieces[i].box)), area = areaOnMap(areaSlide, view);
|
|
225
233
|
const boxOf = (l) => union(all.filter((i) => states[i] !== "gone" && has(i, known(names([].concat(l)), "focus group: "))).map((i) => pieces[i].box));
|
|
226
234
|
const placed = chosen.mover ? chosen.mover.s.place({ subject, context, area, view, canvas, boxOf, options: chosen.mover.options }) ?? {} : {};
|
|
235
|
+
const placedGroups = (placed.groups ?? []).map((x) => ({ ...x, names: names(x.names) }));
|
|
227
236
|
const spring = placed.spring === void 0 || placed.spring === null ? null : readSpring(placed.spring, chosen.mover.name);
|
|
228
237
|
const springOf = /* @__PURE__ */ new Map();
|
|
229
|
-
for (const { names: l, g } of
|
|
238
|
+
for (const { names: l, g } of placedGroups) for (const i of all) if (states[i] !== "gone" && has(i, l)) {
|
|
230
239
|
inGroup.set(i, g ? { g } : "still");
|
|
231
240
|
if (spring && g) springOf.set(i, l.join(" "));
|
|
232
241
|
else springOf.delete(i);
|
|
233
242
|
}
|
|
234
|
-
const springGroups = [...new Map((spring ?
|
|
243
|
+
const springGroups = [...new Map((spring ? placedGroups : []).filter((x) => x.g).map((x) => [x.names.join(" "), [...x.names]]))];
|
|
235
244
|
const groupOf = (i) => {
|
|
236
245
|
const k = inGroup.get(i);
|
|
237
246
|
return k === "subject" ? placed.subject : k === "context" ? placed.context : k?.g ?? null;
|
|
@@ -352,6 +361,7 @@ function planFocus(steps, deck, start = null) {
|
|
|
352
361
|
}
|
|
353
362
|
export {
|
|
354
363
|
FOCUS,
|
|
364
|
+
expandNames,
|
|
355
365
|
focusStep,
|
|
356
366
|
planFocus,
|
|
357
367
|
readFocus
|
package/dist/focusHtml.js
CHANGED
|
@@ -15,8 +15,8 @@ function readPieces(html, { attr = "data-k" } = {}) {
|
|
|
15
15
|
return v !== null && /^[\d.]+$/.test(v) ? Number(v) : null;
|
|
16
16
|
};
|
|
17
17
|
const x = px(style, "left"), y = px(style, "top"), w = px(style, "width") ?? attrPx("width"), h = px(style, "height") ?? attrPx("height");
|
|
18
|
-
const twin = attrOf(at, "data-twin")?.trim().split(/\s+/);
|
|
19
|
-
pieces.push({ keys: k.trim().split(/\s+/), box: x === null || y === null ? null : { x, y, w: w ?? 0, h: h ?? 0 }, ...twin ? { twin } : {} });
|
|
18
|
+
const twin = attrOf(at, "data-twin")?.trim().split(/\s+/), ends = attrOf(at, "data-ends")?.trim().split(/\s+/).filter(Boolean);
|
|
19
|
+
pieces.push({ keys: k.trim().split(/\s+/), box: x === null || y === null ? null : { x, y, w: w ?? 0, h: h ?? 0 }, ...twin ? { twin } : {}, ...ends?.length ? { ends } : {} });
|
|
20
20
|
out += `${html.slice(from, start)}<${tag}${before} ${attr}=${q}${k}${q} data-sd-i="${pieces.length - 1}"${after}>`;
|
|
21
21
|
from = end;
|
|
22
22
|
}
|
package/dist/video.js
CHANGED
|
@@ -270,13 +270,23 @@ function deckStageDriver({ chromium, executablePath, viewport = { width: 1920, h
|
|
|
270
270
|
}
|
|
271
271
|
function measureSlide() {
|
|
272
272
|
const stage = document.querySelector("deck-stage");
|
|
273
|
+
const all = stage ? [...stage.children].filter((s) => s.tagName === "SECTION") : [];
|
|
274
|
+
const endless = (a) => a.effect?.getTiming?.().iterations === Infinity;
|
|
275
|
+
const hidden = /* @__PURE__ */ new Map();
|
|
276
|
+
for (const a of document.getAnimations?.() ?? []) {
|
|
277
|
+
const s = a.effect?.target?.closest?.("deck-stage > section");
|
|
278
|
+
if (!s || s.hasAttribute("data-deck-active") || s.hasAttribute("data-deck-under") || a.playState !== "running" || !endless(a)) continue;
|
|
279
|
+
if (!hidden.has(s)) hidden.set(s, { count: 0, pieces: /* @__PURE__ */ new Set() });
|
|
280
|
+
hidden.get(s).count += 1;
|
|
281
|
+
hidden.get(s).pieces.add(a.effect.target.closest("[data-k]")?.getAttribute("data-k")?.trim().split(/\s+/)[0] ?? a.effect.target.tagName.toLowerCase());
|
|
282
|
+
}
|
|
273
283
|
for (const a of [...document.getAnimations?.() ?? [], ...stage?.shadowRoot?.getAnimations?.() ?? []]) {
|
|
274
284
|
try {
|
|
275
285
|
a.finish();
|
|
276
286
|
} catch {
|
|
277
287
|
}
|
|
278
288
|
}
|
|
279
|
-
const slide =
|
|
289
|
+
const slide = all[stage?.index] ?? null;
|
|
280
290
|
if (!slide) return [];
|
|
281
291
|
const W = Number(stage.getAttribute("width") || 1920), H = Number(stage.getAttribute("height") || 1080);
|
|
282
292
|
const box = slide.getBoundingClientRect(), scale = box.width / W || 1;
|
|
@@ -306,6 +316,11 @@ function measureSlide() {
|
|
|
306
316
|
const dx = Math.max(0, -x0, x1 - W), dy = Math.max(0, -y0, y1 - H);
|
|
307
317
|
if (dx > 2 || dy > 2) out.push({ kind: "outside", piece: pieceOf(el), text: textOf(el), dx: Math.round(dx), dy: Math.round(dy) });
|
|
308
318
|
}
|
|
319
|
+
for (const el of [slide, ...slide.querySelectorAll("*")]) {
|
|
320
|
+
if (el.hasAttribute("data-loop")) continue;
|
|
321
|
+
if (getComputedStyle(el).animationIterationCount.split(",").some((c) => c.trim() === "infinite")) out.push({ kind: "loose-loop", piece: pieceOf(el), text: textOf(el), dx: 0, dy: 0 });
|
|
322
|
+
}
|
|
323
|
+
for (const [s, { count, pieces }] of hidden) out.push({ kind: "hidden-loop", piece: [...pieces].join(", "), text: "", dx: 0, dy: 0, slide: all.indexOf(s) + 1, count });
|
|
309
324
|
return out;
|
|
310
325
|
}
|
|
311
326
|
export {
|
package/focus.css
CHANGED
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
* Every rule is as weak as it can be (the motion gate is a :where()), so a deck's own rule for a piece —
|
|
9
9
|
* its own entrance, say — wins without !important.
|
|
10
10
|
*
|
|
11
|
+
* A piece with a loop (deck-stage: data-loop and its --loop) keeps it while it changes: the loop is the last animation
|
|
12
|
+
* in the piece's list.
|
|
13
|
+
*
|
|
11
14
|
* Tokens (set them in your theme; each has a fallback): --sd-dim-opacity · --sd-hot · --sd-blur ·
|
|
12
15
|
* --sd-veil · --sd-accent (falls back to storydeck's --yellow) · --sd-ease · --sd-move-time · --sd-glow (a path's
|
|
13
16
|
* pulse, falls back to the accent).
|
|
@@ -50,6 +53,7 @@
|
|
|
50
53
|
@property --sd-a-hot { syntax: '*'; inherits: false; }
|
|
51
54
|
@property --sd-a-path { syntax: '*'; inherits: false; }
|
|
52
55
|
@property --sd-a-more { syntax: '*'; inherits: false; } /* one more animation a stylesheet may give a piece, on the piece itself (deck.css: a wire showing itself in) */
|
|
56
|
+
@property --sd-a-loop { syntax: '*'; inherits: false; } /* a piece's loop (deck-stage: data-loop and its --loop), kept while the piece changes */
|
|
53
57
|
@property --sd-turn { syntax: '*'; inherits: false; } /* a piece's moment on a path; never inherited, so 0 off the route */
|
|
54
58
|
@keyframes sdNone {}
|
|
55
59
|
@keyframes sdEnter { from { opacity: 0; scale: calc(var(--sd-s, 1) * .92); } }
|
|
@@ -80,6 +84,7 @@
|
|
|
80
84
|
.sd-move { --sd-a-move: sdMove var(--sd-move-time, 1s) var(--sd-ease, cubic-bezier(.2, .7, .2, 1)) both; }
|
|
81
85
|
.sd-hot-in { --sd-a-hot: sdRing .6s var(--sd-ease, cubic-bezier(.2, .7, .2, 1)) calc(.3s + var(--sd-turn, 0s)) both; } /* on a path, .3s after its turn */
|
|
82
86
|
.sd-focus-new .sd-enter { --sd-delay: .4s; } /* what a click adds waits for its blur */
|
|
87
|
+
:where([data-deck-loop] [data-loop]) { --sd-a-loop: var(--loop); } /* only where deck-stage runs loops */
|
|
83
88
|
/* a path: everything on it has one moment, --sd-turn — its delay, and .4s more under a new blur, so the whole path
|
|
84
89
|
waits for the blur and then runs. A piece on the route enters, arrives or lights then, and glows as the route
|
|
85
90
|
reaches it (on every forward click); a piece stepping aside for its lit twin goes then; the line shows then. */
|
|
@@ -91,7 +96,7 @@
|
|
|
91
96
|
.sd-path.sd-light { --sd-a-state: sdLightPulse .9s ease var(--sd-turn) backwards; --sd-a-path: sdNone 0s; } /* grey until its turn */
|
|
92
97
|
.sd-twin-turn.sd-leave { --sd-a-state: sdLeave .45s ease calc(var(--sd-turn) + var(--sd-twin-lag, 0s)) both; } /* a stylesheet may let a piece step aside later than its twin arrives (deck.css: a wire, while its twin sweeps in) */
|
|
93
98
|
:where([data-deck-active]:not([data-sd-instant]):not([data-deck-static])) :is(.sd-enter, .sd-arrive, .sd-light, .sd-fade, .sd-leave, .sd-relook, .sd-move, .sd-hot-in, .sd-path) {
|
|
94
|
-
animation: var(--sd-a-state, sdNone 0s), var(--sd-a-move, sdNone 0s), var(--sd-a-hot, sdNone 0s), var(--sd-a-path, sdNone 0s), var(--sd-a-more, sdNone 0s); }
|
|
99
|
+
animation: var(--sd-a-state, sdNone 0s), var(--sd-a-move, sdNone 0s), var(--sd-a-hot, sdNone 0s), var(--sd-a-path, sdNone 0s), var(--sd-a-more, sdNone 0s), var(--sd-a-loop, sdNone 0s); }
|
|
95
100
|
:where([data-deck-active]:not([data-sd-instant]):not([data-deck-static])) .sd-path-line { animation: sdArrive .5s ease var(--sd-turn) both; }
|
|
96
101
|
:where([data-deck-active]:not([data-sd-instant]):not([data-deck-static])) .sd-stage.sd-move { animation: sdStage var(--sd-move-time, 1s) var(--sd-ease, cubic-bezier(.2, .7, .2, 1)) both; }
|
|
97
102
|
:where([data-deck-active]:not([data-sd-instant]):not([data-deck-static])) .sd-veil-in-new { animation: sdVeilIn .7s cubic-bezier(.4, 0, .2, 1) both; }
|
package/focus.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface FocusPiece {
|
|
|
12
12
|
readonly box: FocusBox | null;
|
|
13
13
|
/** Names of its twin: while a twin is lit, this piece steps aside (a dashed wire under a solid one). */
|
|
14
14
|
readonly twin?: readonly string[];
|
|
15
|
+
/** A wire's ends: the pieces it joins (data-ends; a wire written "from to to" carries them), for a click's `name+`. */
|
|
16
|
+
readonly ends?: readonly string[];
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
/** A focus as written on a click: a strategy name ('left'), several ('left blur'), or {strategy, …options}. */
|
|
@@ -162,5 +164,7 @@ export type FocusState = { readonly __focusState: true } & Record<string, unknow
|
|
|
162
164
|
export const FOCUS: Readonly<Record<'grey' | 'hide' | 'keep' | 'zoom' | 'left' | 'right' | 'up' | 'down' | 'place' | 'apart' | 'path' | 'blur', FocusStrategy>>;
|
|
163
165
|
export function readFocus(spec: FocusSpec | undefined | null, shared?: Readonly<Record<string, unknown>>): { strategy: string; options: Record<string, unknown> }[];
|
|
164
166
|
export function focusStep(prev: FocusState | null, click: FocusClick, deck: FocusDeck): { state: FocusState; look: FocusLook };
|
|
167
|
+
/** Names as a click writes them, each `name+` read as that piece, the wires with it as an end, and the pieces at their other ends. */
|
|
168
|
+
export function expandNames(pieces: readonly FocusPiece[], names: readonly string[]): string[];
|
|
165
169
|
/** `end` is null only when there are no clicks and no `start`. */
|
|
166
170
|
export function planFocus(clicks: readonly FocusClick[], deck: FocusDeck, start?: FocusState | null): { looks: FocusLook[]; end: FocusState | null };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "footprint-storydeck",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "One source, many lenses — render structured content as a Read article, a Scroll (scrollytelling) story, or a Watch deck; and make a deck from a folder with a live dev loop.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -44,7 +44,7 @@ the project that holds the deck, naming its folder (npx would prefer a StoryDeck
|
|
|
44
44
|
npx footprint-storydeck init my-talk # a starter: deck.json, clicks.html, map.html, theme.css, icons/
|
|
45
45
|
npx footprint-storydeck outline my-talk # the whole deck, a line per click — read this before editing
|
|
46
46
|
npx footprint-storydeck review my-talk --click 3 # one click: headline (words), focus, route, lit, greyed, enters, moves, notes
|
|
47
|
-
npx footprint-storydeck check my-talk # what to fix, each with its click and its fix (--layout: text that spills, in a browser)
|
|
47
|
+
npx footprint-storydeck check my-talk # what to fix, each with its click and its fix (--layout: text that spills and stray endless animations, in a browser)
|
|
48
48
|
npx footprint-storydeck build my-talk # my-talk/index.html, self-contained
|
|
49
49
|
npx footprint-storydeck dev my-talk # for the person: a save reloads on the same click and replays it
|
|
50
50
|
npx footprint-storydeck feedback my-talk # the notes the person left in the dev page — read these first
|
|
@@ -61,9 +61,18 @@ dev page only StoryDeck's own scripts run: a deck's own `<script>` runs in the b
|
|
|
61
61
|
In clicks.html, a `<section data-label data-kicker>` is a plain slide; with `data-map="map"` each `<step>` in it is one
|
|
62
62
|
click on map.html (`data-in`, `data-out`, `data-on`, `data-quiet`, `data-focus` and `data-focus-*`), holding only its
|
|
63
63
|
`<h2>` and `<aside class="notes">` — anything else in a step is refused, naming where it goes (a piece on the map). On
|
|
64
|
-
a map, `<i data-k="w" class="wire pkt" data-arrow="
|
|
64
|
+
a map, `<i data-k="w" class="wire pkt" data-arrow="client to server"></i>` is a wire with a packet, edge to edge
|
|
65
|
+
between the two pieces and following them when they move (prefer it to `"x1 y1 x2 y2"` numbers; a wire each way between
|
|
66
|
+
the same pieces gets its own lane; a bend may follow: `"gw to db 40"`); `data-on="gw+"` is gw, its wires and what they
|
|
67
|
+
reach; a wire takes `data-label="name"` (at its middle, on its left), class `halo` (a glow) and `data-packets="2"
|
|
68
|
+
data-speed="5"` (packets that keep running along it on the slide shown — loops); and
|
|
65
69
|
`<i class="ic" data-icon="name"></i>` is icons/name.svg. A piece that comes in lights unless the click is `data-quiet`
|
|
66
|
-
(`check` warns: `lit-on-entry`).
|
|
70
|
+
(`check` warns: `lit-on-entry`). Motion that keeps going is a loop: `data-loop="pulse"` (or `float`, `marquee`, with a
|
|
71
|
+
pace: `"float 4s"`) on a piece, or `data-loop` alone with the piece's own `--loop: spin 6s linear infinite` in
|
|
72
|
+
theme.css — never an infinite animation of your own (`check --layout` refuses one running on a hidden slide). A named
|
|
73
|
+
loop moves its element by `transform`: put it on an element without a transform of its own (wrap it). A loop
|
|
74
|
+
runs only on the slide shown, on one clock across clicks, and rests in its own style under reduced motion, in print and
|
|
75
|
+
with `?motion=off` (a still deck, for screenshots). For an assistant: `claude mcp add storydeck -- npx footprint-storydeck mcp my-talk`
|
|
67
76
|
gives it outline, review, feedback, check and build as tools.
|
|
68
77
|
|
|
69
78
|
**Focus a diagram, click by click.** Every element with `data-k="name …"` and an inline `left/top`
|
|
@@ -66,6 +66,18 @@
|
|
|
66
66
|
* an opaque background of their own. Decks without data-build behave
|
|
67
67
|
* exactly as before. Rail thumbnails and print show each slide's own
|
|
68
68
|
* authored content (not the composite).
|
|
69
|
+
* (i) loops — an element with data-loop runs the animation in its --loop
|
|
70
|
+
* (e.g. style="--loop: spin 4s linear infinite") as ambient motion: only
|
|
71
|
+
* on the slide shown and the layers under it (the engine stamps
|
|
72
|
+
* data-deck-loop on them), on ONE clock for the whole deck (each
|
|
73
|
+
* loop's animation is set to start at the page's start), so
|
|
74
|
+
* the next slide of the same picture shows every loop where the last
|
|
75
|
+
* one left it, and a jump or a step back keeps them running. Loops
|
|
76
|
+
* never run under prefers-reduced-motion, in print, or with motion
|
|
77
|
+
* off: there the element rests in its own style (its still pose).
|
|
78
|
+
* (j) motion off — <deck-stage data-motion="off">, or ?motion=off in the
|
|
79
|
+
* page's address: no loops, and every arrival lands at once (as a
|
|
80
|
+
* jump does), so a screenshot of a slide is the same every time.
|
|
69
81
|
*
|
|
70
82
|
* Slides are HIDDEN, not unmounted. Non-active slides stay in the DOM with
|
|
71
83
|
* `visibility: hidden` + `opacity: 0`, so their state (videos, iframes,
|
|
@@ -122,7 +134,9 @@
|
|
|
122
134
|
* animate *from* hidden, so print and reduced-motion show content.
|
|
123
135
|
* Gate the animation on [data-deck-active] and the motion query, e.g.
|
|
124
136
|
* `@media (prefers-reduced-motion:no-preference){ [data-deck-active] .x{animation:fade-in .5s both} }`.
|
|
125
|
-
*
|
|
137
|
+
* - Endless motion is a loop (USAGE (i)): data-loop and its --loop, never
|
|
138
|
+
* an infinite animation of your own (that one keeps running on hidden
|
|
139
|
+
* slides, restarts at each slide and stops on a jump).
|
|
126
140
|
*/
|
|
127
141
|
/* END USAGE */
|
|
128
142
|
|
|
@@ -133,6 +147,7 @@
|
|
|
133
147
|
const VALIDATE_ATTR = 'no_overflowing_text,no_overlapping_text,slide_sized_text';
|
|
134
148
|
const FINE_POINTER_MQ = matchMedia('(hover: hover) and (pointer: fine)');
|
|
135
149
|
const NARROW_MQ = matchMedia('(max-width: 640px)');
|
|
150
|
+
const REDUCED_MQ = matchMedia('(prefers-reduced-motion: reduce)');
|
|
136
151
|
// Slide-authored controls that should keep a tap instead of it navigating.
|
|
137
152
|
const INTERACTIVE_SEL = 'a[href], button, input, select, textarea, summary, label, video[controls], audio[controls], [role="button"], [onclick], [tabindex]:not([tabindex^="-"]), [contenteditable]:not([contenteditable="false" i])';
|
|
138
153
|
|
|
@@ -596,7 +611,7 @@
|
|
|
596
611
|
`;
|
|
597
612
|
|
|
598
613
|
class DeckStage extends HTMLElement {
|
|
599
|
-
static get observedAttributes() { return ['width', 'height', 'noscale', 'no-rail']; }
|
|
614
|
+
static get observedAttributes() { return ['width', 'height', 'noscale', 'no-rail', 'data-motion']; }
|
|
600
615
|
|
|
601
616
|
constructor() {
|
|
602
617
|
super();
|
|
@@ -614,6 +629,7 @@
|
|
|
614
629
|
this._onMouseMove = this._onMouseMove.bind(this);
|
|
615
630
|
this._onTap = this._onTap.bind(this);
|
|
616
631
|
this._onMessage = this._onMessage.bind(this);
|
|
632
|
+
this._onMotionChange = this._onMotionChange.bind(this);
|
|
617
633
|
// Capture-phase close so a click anywhere dismisses the menu, but
|
|
618
634
|
// ignore clicks that land inside the menu itself — otherwise the
|
|
619
635
|
// capture handler runs before the menu's own (bubble) handler and
|
|
@@ -636,10 +652,14 @@
|
|
|
636
652
|
// next thumbnails — the rail has no business rendering inside those
|
|
637
653
|
// (wrong scale, and it offsets the stage so the thumb shows a gutter).
|
|
638
654
|
if (/[?&]_snthumb=/.test(location.search)) this.setAttribute('no-rail', '');
|
|
655
|
+
// ?motion=off: a still deck (USAGE (j)), for a screenshot or a person who asks for one
|
|
656
|
+
if (/[?&]motion=off(?:&|$)/.test(location.search)) this.setAttribute('data-motion', 'off');
|
|
639
657
|
this._render();
|
|
640
658
|
this._loadNotes();
|
|
641
659
|
this._syncPrintPageRule();
|
|
642
660
|
this._syncBuildStyleRule();
|
|
661
|
+
this._syncLoopStyleRule();
|
|
662
|
+
if (REDUCED_MQ.addEventListener) REDUCED_MQ.addEventListener('change', this._onMotionChange);
|
|
643
663
|
window.addEventListener('keydown', this._onKey);
|
|
644
664
|
window.addEventListener('resize', this._onResize);
|
|
645
665
|
window.addEventListener('mousemove', this._onMouseMove, { passive: true });
|
|
@@ -658,15 +678,18 @@
|
|
|
658
678
|
this._freezeStyle = document.createElement('style');
|
|
659
679
|
this._freezeStyle.textContent = '*,*::before,*::after{transition-duration:0s !important}';
|
|
660
680
|
document.head.appendChild(this._freezeStyle);
|
|
681
|
+
this._printing = true; // a loop prints its still pose
|
|
661
682
|
this._slides.forEach((s) => {
|
|
662
683
|
s.setAttribute('data-deck-active', '');
|
|
663
684
|
// Print pages each slide standalone — no additive layering, and
|
|
664
685
|
// the under-layer chrome-hide rule must not leak into print.
|
|
665
686
|
s.removeAttribute('data-deck-under');
|
|
666
687
|
s.removeAttribute('data-deck-static');
|
|
688
|
+
s.removeAttribute('data-deck-loop');
|
|
667
689
|
});
|
|
668
690
|
};
|
|
669
691
|
this._onAfterPrint = () => {
|
|
692
|
+
this._printing = false;
|
|
670
693
|
this._applyIndex({ showOverlay: false, broadcast: false });
|
|
671
694
|
if (this._freezeStyle) { this._freezeStyle.remove(); this._freezeStyle = null; }
|
|
672
695
|
};
|
|
@@ -884,6 +907,7 @@
|
|
|
884
907
|
window.removeEventListener('click', this._onDocClick, true);
|
|
885
908
|
window.removeEventListener('beforeprint', this._onBeforePrint);
|
|
886
909
|
window.removeEventListener('afterprint', this._onAfterPrint);
|
|
910
|
+
if (REDUCED_MQ.removeEventListener) REDUCED_MQ.removeEventListener('change', this._onMotionChange);
|
|
887
911
|
if (this._freezeStyle) { this._freezeStyle.remove(); this._freezeStyle = null; }
|
|
888
912
|
this.removeEventListener('click', this._onTap);
|
|
889
913
|
if (this._hideTimer) clearTimeout(this._hideTimer);
|
|
@@ -897,7 +921,8 @@
|
|
|
897
921
|
if (this._onTweakChange) window.removeEventListener('tweakchange', this._onTweakChange);
|
|
898
922
|
}
|
|
899
923
|
|
|
900
|
-
attributeChangedCallback() {
|
|
924
|
+
attributeChangedCallback(name) {
|
|
925
|
+
if (name === 'data-motion') { this._onMotionChange(); return; }
|
|
901
926
|
if (this._canvas) {
|
|
902
927
|
this._canvas.style.width = this.designWidth + 'px';
|
|
903
928
|
this._canvas.style.height = this.designHeight + 'px';
|
|
@@ -1107,7 +1132,9 @@
|
|
|
1107
1132
|
// in connectedCallback that sets data-deck-active on every slide.
|
|
1108
1133
|
'*, *::before, *::after { animation-delay: -99s !important; animation-duration: .001s !important; ' +
|
|
1109
1134
|
'animation-iteration-count: 1 !important; animation-fill-mode: both !important; ' +
|
|
1110
|
-
'animation-play-state: running !important; transition-duration: 0s !important; }
|
|
1135
|
+
'animation-play-state: running !important; transition-duration: 0s !important; } ' +
|
|
1136
|
+
// A loop prints its still pose, even in a print taken without beforeprint (USAGE (i)).
|
|
1137
|
+
'deck-stage [data-loop] { animation: none !important; } }';
|
|
1111
1138
|
}
|
|
1112
1139
|
|
|
1113
1140
|
/** Additive-build support styles. Like the print-page rule, this must
|
|
@@ -1140,13 +1167,72 @@
|
|
|
1140
1167
|
'deck-stage [data-deck-under] [data-chrome] { visibility: hidden !important; }\n' +
|
|
1141
1168
|
// Instant arrival (back-step / deep link / jump): entrance
|
|
1142
1169
|
// animations complete in 0s — fill-mode lands them on their end
|
|
1143
|
-
// state, so the composite appears fully built.
|
|
1144
|
-
|
|
1170
|
+
// state, so the composite appears fully built. A loop is not an
|
|
1171
|
+
// entrance: it keeps running (USAGE (i)).
|
|
1172
|
+
'deck-stage [data-deck-static]:not([data-loop]), deck-stage [data-deck-static] :not([data-loop]) { animation-duration: 0s !important; animation-delay: 0s !important; }\n' +
|
|
1145
1173
|
// Belt for a print snapshot taken without beforeprint having
|
|
1146
1174
|
// cleared the attributes.
|
|
1147
1175
|
'@media print { deck-stage [data-deck-under] [data-chrome] { visibility: visible !important; } }';
|
|
1148
1176
|
}
|
|
1149
1177
|
|
|
1178
|
+
/** Loop styles (USAGE (i)), in the DOCUMENT stylesheet for the same
|
|
1179
|
+
* reason as the build styles. --loop is registered as not inherited,
|
|
1180
|
+
* so a loop's animation never reaches the elements inside it. The rule
|
|
1181
|
+
* is :where() — no weight — so a deck's own rule for the element wins
|
|
1182
|
+
* (a stylesheet that animates pieces can add var(--loop) to its list). */
|
|
1183
|
+
_syncLoopStyleRule() {
|
|
1184
|
+
const id = 'deck-stage-loop-css';
|
|
1185
|
+
let tag = document.getElementById(id);
|
|
1186
|
+
if (!tag) {
|
|
1187
|
+
tag = document.createElement('style');
|
|
1188
|
+
tag.id = id;
|
|
1189
|
+
}
|
|
1190
|
+
(document.body || document.head).appendChild(tag);
|
|
1191
|
+
tag.textContent =
|
|
1192
|
+
'@property --loop { syntax: "*"; inherits: false; }\n' +
|
|
1193
|
+
':where(deck-stage [data-deck-loop] [data-loop]) { animation: var(--loop, none); }';
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
/** Whether loops run: not with motion off, under reduced motion, or in print. */
|
|
1197
|
+
_loopsRun() {
|
|
1198
|
+
return this.getAttribute('data-motion') !== 'off' && !REDUCED_MQ.matches && !this._printing;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
/** data-deck-loop on the slides whose loops run: the one shown and the layers under it. */
|
|
1202
|
+
_syncLoops() {
|
|
1203
|
+
const run = this._loopsRun();
|
|
1204
|
+
this._slides.forEach((s) => {
|
|
1205
|
+
if (run && (s.hasAttribute('data-deck-active') || s.hasAttribute('data-deck-under'))) s.setAttribute('data-deck-loop', '');
|
|
1206
|
+
else s.removeAttribute('data-deck-loop');
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/** One clock for every loop: each loop (an endless animation of a
|
|
1211
|
+
* data-loop element) on a slide whose loops run starts at the page's
|
|
1212
|
+
* start, so the same loop on the next slide (the same picture, drawn
|
|
1213
|
+
* again) goes on where this one is. Nothing else is touched: a slide
|
|
1214
|
+
* without loops is never asked for its animations. */
|
|
1215
|
+
_lockLoops() {
|
|
1216
|
+
for (const s of this._slides) {
|
|
1217
|
+
if (!s.hasAttribute('data-deck-loop') || typeof s.getAnimations !== 'function' || !s.querySelector('[data-loop]')) continue;
|
|
1218
|
+
for (const a of s.getAnimations({ subtree: true })) {
|
|
1219
|
+
if (a.playState === 'paused' || a.startTime === 0 || !a.effect) continue;
|
|
1220
|
+
const t = a.effect.target;
|
|
1221
|
+
if (t && t.hasAttribute && t.hasAttribute('data-loop') && a.effect.getTiming().iterations === Infinity) a.startTime = 0;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
/** Motion turned on or off (data-motion, or the reduced-motion setting): the slide shown keeps its place; with
|
|
1227
|
+
* motion off it lands at once (static), and loops start or stop. */
|
|
1228
|
+
_onMotionChange() {
|
|
1229
|
+
if (!this._slides.length) return;
|
|
1230
|
+
const s = this._slides[this._index];
|
|
1231
|
+
if (s && this.getAttribute('data-motion') === 'off') s.setAttribute('data-deck-static', '');
|
|
1232
|
+
this._syncLoops();
|
|
1233
|
+
this._lockLoops();
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1150
1236
|
_onSlotChange() {
|
|
1151
1237
|
// Self-mutate path already reconciled synchronously and emitted
|
|
1152
1238
|
// slidechange; skip the async slotchange it caused.
|
|
@@ -1255,6 +1341,7 @@
|
|
|
1255
1341
|
else s.removeAttribute('data-deck-active');
|
|
1256
1342
|
});
|
|
1257
1343
|
this._applyChainState(prev, curr);
|
|
1344
|
+
this._syncLoops();
|
|
1258
1345
|
if (this._countEl) this._countEl.textContent = String(curr + 1);
|
|
1259
1346
|
// Follow-scroll on every navigation (init deep-link, keyboard, click,
|
|
1260
1347
|
// tap, external goTo) — the only time we *don't* want the rail to
|
|
@@ -1288,6 +1375,8 @@
|
|
|
1288
1375
|
}
|
|
1289
1376
|
|
|
1290
1377
|
this._prevIndex = curr;
|
|
1378
|
+
// after the listeners: what they changed on the slide is in step too
|
|
1379
|
+
this._lockLoops();
|
|
1291
1380
|
if (showOverlay) this._flashOverlay();
|
|
1292
1381
|
}
|
|
1293
1382
|
|
|
@@ -1317,9 +1406,10 @@
|
|
|
1317
1406
|
const additive = base !== curr;
|
|
1318
1407
|
const sameChain = prev >= 0 && prev < this._slides.length
|
|
1319
1408
|
&& bases[prev] === bases[curr];
|
|
1320
|
-
|
|
1409
|
+
// with motion off every arrival lands at once (USAGE (j))
|
|
1410
|
+
let instant = this.getAttribute('data-motion') === 'off';
|
|
1321
1411
|
if (additive) {
|
|
1322
|
-
instant = !(prev === this._prevPlayable(curr) && prev >= base);
|
|
1412
|
+
instant = instant || !(prev === this._prevPlayable(curr) && prev >= base);
|
|
1323
1413
|
} else if (sameChain && prev > curr) {
|
|
1324
1414
|
instant = true;
|
|
1325
1415
|
}
|
|
@@ -1734,6 +1824,7 @@
|
|
|
1734
1824
|
// but don't let stale nav attributes ride along either).
|
|
1735
1825
|
clone.removeAttribute('data-deck-under');
|
|
1736
1826
|
clone.removeAttribute('data-deck-static');
|
|
1827
|
+
clone.removeAttribute('data-deck-loop');
|
|
1737
1828
|
clone.querySelectorAll('[id]').forEach((el) => el.removeAttribute('id'));
|
|
1738
1829
|
// Neuter heavy media; replace <video> with its poster so the box
|
|
1739
1830
|
// keeps a visual. <iframe>/<audio> become empty placeholders.
|
package/video.d.ts
CHANGED
|
@@ -19,8 +19,12 @@ export interface VideoDriver {
|
|
|
19
19
|
shot(file: string): Promise<unknown>;
|
|
20
20
|
close(): Promise<unknown> | unknown;
|
|
21
21
|
}
|
|
22
|
-
/**
|
|
23
|
-
|
|
22
|
+
/**
|
|
23
|
+
* What a settled click shows that spills: text out of its box, a lit piece (or the chrome) past the slide's edge; and
|
|
24
|
+
* endless animations that are not deck-stage loops: one shown here ('loose-loop'), or running on a slide no one sees
|
|
25
|
+
* ('hidden-loop': `slide`, 1-based, and `count`; `piece` names their pieces).
|
|
26
|
+
*/
|
|
27
|
+
export interface SlideSpill { kind: 'spills' | 'outside' | 'loose-loop' | 'hidden-loop'; piece: string | null; text: string; dx: number; dy: number; slide?: number; count?: number }
|
|
24
28
|
/** The <deck-stage> driver: the video's port, and `measure(n)` — click n as it settles, measured (the layout check). */
|
|
25
29
|
export interface DeckStageDriver extends VideoDriver {
|
|
26
30
|
measure(n: number): Promise<SlideSpill[]>;
|
|
@@ -47,7 +51,7 @@ export function renderVideo(options: {
|
|
|
47
51
|
}): Promise<{ video: string; length: number; cues: number; chapters: Chapters; plan: PlannedClick[] }>;
|
|
48
52
|
/** The driver for storydeck's <deck-stage> pages, with Playwright's `chromium`. */
|
|
49
53
|
export function deckStageDriver(options: { chromium: unknown; executablePath?: string; viewport?: { width: number; height: number }; hide?: string[]; /** keep the page off the network: it loads only its own file and inline data (OFFLINE) */ offline?: boolean }): DeckStageDriver;
|
|
50
|
-
/** Runs in the page: what spills on the slide shown
|
|
54
|
+
/** Runs in the page: what spills on the slide shown (its animations finished first), and endless animations that are not loops (deckStageDriver · measure). */
|
|
51
55
|
export function measureSlide(): SlideSpill[];
|
|
52
56
|
/** How an offline page is run: the browser's launch args and the page context's options (see deckStageDriver's `offline`). */
|
|
53
57
|
export const OFFLINE: { readonly args: readonly string[]; readonly context: { readonly offline: true; readonly serviceWorkers: 'block' } };
|