footprint-storydeck 0.9.0 → 0.10.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 +33 -2
- package/README.md +11 -0
- package/deck.d.ts +12 -2
- package/dist/deck/compile.js +17 -3
- package/dist/deck/config.js +3 -1
- package/dist/deck/explore.js +88 -0
- package/dist/deck/init.js +1 -0
- package/dist/deck/page.js +8 -4
- package/dist/deck/parse.js +11 -2
- package/dist/deck/review.js +4 -0
- package/dist/deck/transitions.js +161 -0
- package/package.json +1 -1
- package/plugin/skills/storydeck/SKILL.md +4 -1
- package/site/public/deck-stage.js +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.10.0 — a part arrives with a change of picture, and a deck a viewer can explore
|
|
4
|
+
|
|
5
|
+
- **A part arrives with a change of picture: `data-enter`.** `<section … data-enter="push" data-enter-from="right">`
|
|
6
|
+
plays one of the editor's changes StoryReel's films use (footprint-story/motion's table: the same names, settings,
|
|
7
|
+
lengths and eases) as the deck steps into the part: cut, fade, dip, wipe, split, iris, push, slide, whip or zoom,
|
|
8
|
+
each with its settings as `data-enter-*` (`from`, `line`, `at` as "x y" in slide px or "centre", `color`) and
|
|
9
|
+
`data-enter-seconds` / `data-enter-ease`. The build samples each distinct change once into keyframes for both slides
|
|
10
|
+
(30 a second, the same curves a film draws); the page plays them with Web Animations only on a forward step into the
|
|
11
|
+
part — a jump, a step back, reduced motion and motion off land at once — keeping the slide it leaves in view until
|
|
12
|
+
it ends, and a video steps them like any animation. A dip shows its colour behind both slides (black unless
|
|
13
|
+
`data-enter-color` says otherwise): `<deck-stage>` now paints what shows behind its slides with `--deck-canvas` (white,
|
|
14
|
+
as before, when unset). A change a page cannot play (clock, page, through, match: they need a canvas), a setting the
|
|
15
|
+
change does not take and a value out of range are refused at build, naming the part and the fix. `review` says how
|
|
16
|
+
each part arrives. While a change plays the frame clips both slides (a push never crosses the letterbox: `<deck-stage>`
|
|
17
|
+
clips its canvas while it carries `data-deck-changing`), the slide being left keeps its loops running, and printing
|
|
18
|
+
puts everything back first. Settings are read as a browser reads the tag. A deck without `data-enter` carries none
|
|
19
|
+
of it (no keyframes, rule or runtime); the `<deck-stage>` every page carries gained the two hooks, and draws as
|
|
20
|
+
before without them.
|
|
21
|
+
|
|
22
|
+
- **Explore: a viewer clicks a piece to go to the click about it.** With `"explore": true` in deck.json, a click on a
|
|
23
|
+
piece goes to the click that is about it — the first click whose `data-on` names it (`gw+`'s gw too), or, when none
|
|
24
|
+
does, the first that takes it in through a `name+` — and a "Back to click N" control (or Backspace) returns, a step
|
|
25
|
+
at a time, as a browser's Back does (to where the viewer was before each jump, whatever they stepped through since).
|
|
26
|
+
Pieces that lead somewhere get a pointer and a ring on hover; a piece whose click is the one shown, or one that
|
|
27
|
+
cannot be seen, leads nowhere; a link or control inside a piece keeps its click; a drag (the dev page's Alt-drag) is
|
|
28
|
+
never a click. On a touch screen a tap on a piece that leads somewhere explores — tap beside the pieces to step. The
|
|
29
|
+
pieces are reached by pointer, not by keyboard: arrow keys still step through the clicks in order. The presenter
|
|
30
|
+
window does not explore (it follows the room's deck). The authored order never changes: explore is a way through the
|
|
31
|
+
same clicks. A deck without `"explore"` carries none of it (its look comes with it, not in deck.css).
|
|
32
|
+
|
|
3
33
|
## 0.9.0 — living system diagrams: loops, wires between pieces, a wire's label, halo and running packets
|
|
4
34
|
|
|
5
35
|
- **A wire between pieces: `data-arrow="client to server"`.** The wire runs from the edge of one piece's box to the
|
|
@@ -45,8 +75,9 @@
|
|
|
45
75
|
step back keeps it running (a static arrival no longer zeroes a loop, only entrances). A loop never runs under
|
|
46
76
|
reduced motion (switched live), in print, or with motion off: there the piece rests in its own style. A piece that
|
|
47
77
|
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
|
|
49
|
-
before.
|
|
78
|
+
of the piece's list. Loop CSS is drawn only for decks that use loops, so a deck without one gains none; the
|
|
79
|
+
`<deck-stage>` every page carries changed, and a deck without loops draws as before. (Corrected after release: this
|
|
80
|
+
line first said such a deck builds byte for byte as before — its page carries the changed stage.)
|
|
50
81
|
- **Motion off: a still deck.** `?motion=off` after the page's address, or `<deck-stage data-motion="off">` (switched
|
|
51
82
|
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
83
|
time.
|
package/README.md
CHANGED
|
@@ -408,6 +408,17 @@ npx footprint-storydeck build my-talk # my-talk/index.html — one page, eve
|
|
|
408
408
|
follow the part on screen as well, select the active slide in the same rule:
|
|
409
409
|
`[data-theme="ledger"], :root:has([data-deck-active][data-theme="ledger"]) { … }`. `check` warns about a theme
|
|
410
410
|
that theme.css never selects (a misspelt name changes nothing), and `review` names each click's theme.
|
|
411
|
+
- **A part arrives with a change of picture.** `<section … data-enter="push" data-enter-from="right">` plays one of
|
|
412
|
+
the editor's changes StoryReel's films use — cut, fade, dip, wipe, split, iris, push, slide, whip, zoom — with its
|
|
413
|
+
settings as `data-enter-*` (`from`, `line`, `at` "x y" or "centre", `color` — a dip goes through black unless it
|
|
414
|
+
says otherwise — `seconds`, `ease`), as the deck steps into the part; the frame clips both slides while it plays. Only on a forward step: a jump or a step back lands at once, and so does every step under reduced
|
|
415
|
+
motion or with motion off. `review` says how each part arrives.
|
|
416
|
+
- **Explore: click a piece, go to the click about it.** `"explore": true` in deck.json lets a viewer wander the
|
|
417
|
+
deck: a click on a piece goes to the click that is about it (the first whose `data-on` names it, else the first that
|
|
418
|
+
takes it in through a `name+`), and "Back to click N" (or Backspace) returns, as a browser's Back does. Pieces that
|
|
419
|
+
lead somewhere show a pointer and a ring on hover; on a touch screen a tap on one explores (tap beside the pieces to
|
|
420
|
+
step), and the keyboard keeps stepping in order. The order you wrote is
|
|
421
|
+
untouched — explore is a way through the same clicks, for a deck left open on a screen or sent to someone.
|
|
411
422
|
- **Read first, look last.** A reviewer — a person, or a model that pays for every screenshot — reads the deck as text:
|
|
412
423
|
`outline` (a line per click), `review --click 14` (its headline and words, what it lights, greys, brings in and moves,
|
|
413
424
|
its route, its notes), `check` (what to fix, each with its click and its fix: a headline over 12 words, pieces that
|
package/deck.d.ts
CHANGED
|
@@ -29,6 +29,8 @@ export interface DeckConfig {
|
|
|
29
29
|
readonly deckData: string | null;
|
|
30
30
|
/** What `storydeck check` asks: words in a map click's headline (12), on a plain slide (40). */
|
|
31
31
|
readonly checks: { readonly maxWords: number; readonly maxSlideWords: number };
|
|
32
|
+
/** A viewer may click a piece to go to the click about it, and come back (false). */
|
|
33
|
+
readonly explore: boolean;
|
|
32
34
|
/** The keys deck.json wrote (a file it names must be there; a default may be absent). */
|
|
33
35
|
readonly given: readonly string[];
|
|
34
36
|
}
|
|
@@ -37,10 +39,12 @@ export function readConfig(json: string | object): DeckConfig;
|
|
|
37
39
|
|
|
38
40
|
/** One click on a map, as written: its attributes (focus-html · readStep reads them), headline (HTML) and notes. */
|
|
39
41
|
export interface DeckStepSource { readonly attrs: string; readonly headline: string; readonly notes: readonly string[] }
|
|
42
|
+
/** How a part arrives, as written: data-enter and its data-enter-* settings, as text (compile reads them against the shared table). */
|
|
43
|
+
export interface DeckEnterSource { readonly type: string; readonly given: Readonly<Record<string, string>> }
|
|
40
44
|
/** A part of the deck: a plain slide (map null) or clicks told on a map. */
|
|
41
|
-
export type DeckPart =
|
|
45
|
+
export type DeckPart = { readonly theme: string | null; readonly enter: DeckEnterSource | null } & (
|
|
42
46
|
| { readonly label: string; readonly kicker: string; readonly map: null; readonly body: string; readonly notes: readonly string[] }
|
|
43
|
-
| { readonly label: string; readonly kicker: string; readonly map: string; readonly steps: readonly DeckStepSource[] };
|
|
47
|
+
| { readonly label: string; readonly kicker: string; readonly map: string; readonly steps: readonly DeckStepSource[] });
|
|
44
48
|
/**
|
|
45
49
|
* The clicks file read into parts, scanned once. A step holds only its <h2> and notes (anything else is refused, naming
|
|
46
50
|
* where it goes); a <section> or <step> left open, or opened inside another, is refused.
|
|
@@ -80,6 +84,8 @@ export type DeckClick = {
|
|
|
80
84
|
readonly notes: readonly string[];
|
|
81
85
|
/** What loops on the click (on a map: on the pieces it shows). */
|
|
82
86
|
readonly loops: readonly DeckLoop[];
|
|
87
|
+
/** How the click's part arrives, on its first click (null elsewhere, or when it just appears): footprint-story/motion's change, read. */
|
|
88
|
+
readonly enter: { readonly type: string; readonly seconds: number; readonly ease: string; readonly p: Readonly<Record<string, unknown>> } | null;
|
|
83
89
|
} & (
|
|
84
90
|
| { readonly kind: 'plain'; readonly map: null; readonly text: string }
|
|
85
91
|
| { readonly kind: 'map'; readonly map: string; readonly step: FocusClick; readonly look: FocusLook; readonly stepIndex: number; readonly steps: number }
|
|
@@ -98,6 +104,10 @@ export interface CompiledDeck {
|
|
|
98
104
|
readonly clicks: readonly DeckClick[];
|
|
99
105
|
/** The named loops the clicks show (data-loop values), drawn into the page's CSS once each. */
|
|
100
106
|
readonly loops: readonly string[];
|
|
107
|
+
/** With deck.json "explore": the map each click is on (null on a plain slide), and where each piece leads on its map — the first click whose data-on names it, else the first that takes it in through a name+ (0-based). */
|
|
108
|
+
readonly explore: { readonly slides: readonly (string | null)[]; readonly targets: Readonly<Record<string, Readonly<Record<string, number>>>> } | null;
|
|
109
|
+
/** The parts' entrances as the page plays them: each distinct change sampled once (kinds), and its key per slide — or null when no part has one. */
|
|
110
|
+
readonly transitions: { readonly kinds: Readonly<Record<string, unknown>>; readonly enter: readonly (string | null)[] } | null;
|
|
101
111
|
readonly maps: Readonly<Record<string, { readonly pieces: readonly FocusPiece[] }>>;
|
|
102
112
|
}
|
|
103
113
|
/** The deck's clicks planned (each map's state carried click to click) and drawn as <deck-stage> slides. */
|
package/dist/deck/compile.js
CHANGED
|
@@ -2,6 +2,8 @@ import { focusStep } from "../focus.js";
|
|
|
2
2
|
import { readPieces, readStep, drawFocus, wrapStage, slideClass, focusOverlay } from "../focusHtml.js";
|
|
3
3
|
import { applyTransforms, icons as drawIcons } from "./transforms.js";
|
|
4
4
|
import { loopsIn } from "./loops.js";
|
|
5
|
+
import { exploreTargets } from "./explore.js";
|
|
6
|
+
import { readEnter, transitionTable } from "./transitions.js";
|
|
5
7
|
import { plainText } from "./parse.js";
|
|
6
8
|
import { esc, stripComments } from "./html.js";
|
|
7
9
|
import { readAttributes, tagsWith } from "../pieceRules.js";
|
|
@@ -41,6 +43,7 @@ function planClicks(parts, maps, { area, canvas, icons }) {
|
|
|
41
43
|
let lastMap = null;
|
|
42
44
|
const clicks = [];
|
|
43
45
|
for (const part of parts) {
|
|
46
|
+
const enter = readEnter(part.enter, `"${part.label}"`, { frame: canvas });
|
|
44
47
|
if (!part.map) {
|
|
45
48
|
refuseOwnNumbers(`"${part.label}"`, part.body);
|
|
46
49
|
let inner;
|
|
@@ -49,7 +52,7 @@ function planClicks(parts, maps, { area, canvas, icons }) {
|
|
|
49
52
|
} catch (e) {
|
|
50
53
|
throw new Error(`"${part.label}": ${e.message}`, { cause: e });
|
|
51
54
|
}
|
|
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 });
|
|
55
|
+
clicks.push({ label: part.label, part, kind: "plain", inner, enter, loops: loopsIn(drawIcons(part.body, icons), `"${part.label}"`), notes: part.notes, headline: "", step: 0, steps: 1 });
|
|
53
56
|
lastMap = null;
|
|
54
57
|
continue;
|
|
55
58
|
}
|
|
@@ -64,7 +67,7 @@ function planClicks(parts, maps, { area, canvas, icons }) {
|
|
|
64
67
|
}
|
|
65
68
|
map.state = r.state;
|
|
66
69
|
lastMap = map;
|
|
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 });
|
|
70
|
+
clicks.push({ label: `${part.label} \xB7 ${k + 1}`, part, kind: "map", map, step, look: r.look, enter: k === 0 ? enter : null, loops: shownLoops(map, r.look), headline: s.headline, notes: s.notes, stepIndex: k, steps: part.steps.length });
|
|
68
71
|
});
|
|
69
72
|
}
|
|
70
73
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -118,10 +121,21 @@ function compileDeck(source, { dev = false } = {}) {
|
|
|
118
121
|
headline: plainText(c.headline),
|
|
119
122
|
notes: c.notes,
|
|
120
123
|
loops: c.loops,
|
|
124
|
+
enter: c.enter,
|
|
121
125
|
...c.kind === "map" ? { step: c.step, look: c.look, stepIndex: c.stepIndex, steps: c.steps } : { text: plainText(c.inner) }
|
|
122
126
|
}));
|
|
123
127
|
const loops = [...new Set(planned.flatMap((c) => c.loops.map((l) => l.value)))].filter(Boolean).sort();
|
|
124
|
-
return {
|
|
128
|
+
return {
|
|
129
|
+
config,
|
|
130
|
+
title: config.title,
|
|
131
|
+
themeCss: source.themeCss ?? "",
|
|
132
|
+
slides,
|
|
133
|
+
clicks,
|
|
134
|
+
loops,
|
|
135
|
+
explore: config.explore ? exploreTargets(planned) : null,
|
|
136
|
+
transitions: transitionTable(planned.map((c) => c.enter), config.canvas),
|
|
137
|
+
maps: Object.fromEntries(Object.values(maps).map((m) => [m.name, { pieces: m.pieces }]))
|
|
138
|
+
};
|
|
125
139
|
}
|
|
126
140
|
export {
|
|
127
141
|
compileDeck
|
package/dist/deck/config.js
CHANGED
|
@@ -9,7 +9,7 @@ const DEFAULTS = Object.freeze({
|
|
|
9
9
|
out: "index.html",
|
|
10
10
|
checks: Object.freeze({ maxWords: 12, maxSlideWords: 40 })
|
|
11
11
|
});
|
|
12
|
-
const KEYS = ["title", "description", "lang", "footer", "area", "canvas", "clicks", "theme", "icons", "maps", "out", "post", "deckData", "checks"];
|
|
12
|
+
const KEYS = ["title", "description", "lang", "footer", "area", "canvas", "clicks", "theme", "icons", "maps", "out", "post", "deckData", "checks", "explore"];
|
|
13
13
|
const CHECK_KEYS = ["maxWords", "maxSlideWords"];
|
|
14
14
|
const isText = (v) => typeof v === "string" && v.trim().length > 0;
|
|
15
15
|
const numbers = (v, n) => Array.isArray(v) && v.length === n && v.every((x) => typeof x === "number" && Number.isFinite(x));
|
|
@@ -43,6 +43,7 @@ function readConfig(json) {
|
|
|
43
43
|
return v;
|
|
44
44
|
};
|
|
45
45
|
const maxWords = count("maxWords"), maxSlideWords = count("maxSlideWords");
|
|
46
|
+
if (raw.explore !== void 0 && typeof raw.explore !== "boolean") throw new Error(`deck.json "explore" is true or false, not ${JSON.stringify(raw.explore)}`);
|
|
46
47
|
return Object.freeze({
|
|
47
48
|
title: raw.title.trim(),
|
|
48
49
|
description: raw.description ?? "",
|
|
@@ -58,6 +59,7 @@ function readConfig(json) {
|
|
|
58
59
|
post: raw.post === void 0 ? null : deckPath(raw.post, "post"),
|
|
59
60
|
deckData: raw.deckData === void 0 ? null : deckPath(raw.deckData, "deckData"),
|
|
60
61
|
checks: Object.freeze({ maxWords, maxSlideWords }),
|
|
62
|
+
explore: raw.explore === true,
|
|
61
63
|
given: Object.freeze(Object.keys(raw))
|
|
62
64
|
// the keys deck.json wrote: a file it names must be there; a default may be absent
|
|
63
65
|
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { expandNames } from "../focus.js";
|
|
2
|
+
function exploreTargets(planned) {
|
|
3
|
+
const targets = {}, through = {}, table = () => /* @__PURE__ */ Object.create(null);
|
|
4
|
+
planned.forEach((c, i) => {
|
|
5
|
+
if (c.kind !== "map" || !c.step.on) return;
|
|
6
|
+
const own = targets[c.map.name] ??= table(), near = through[c.map.name] ??= table();
|
|
7
|
+
for (const name of c.step.on.map((x) => expandNames(c.map.pieces, [x])[0])) if (name !== "*" && !Object.hasOwn(own, name)) own[name] = i;
|
|
8
|
+
for (const name of expandNames(c.map.pieces, c.step.on)) if (name !== "*" && !Object.hasOwn(near, name)) near[name] = i;
|
|
9
|
+
});
|
|
10
|
+
for (const [map, near] of Object.entries(through)) for (const [name, i] of Object.entries(near)) if (!Object.hasOwn(targets[map], name)) targets[map][name] = i;
|
|
11
|
+
return { slides: planned.map((c) => c.kind === "map" ? c.map.name : null), targets };
|
|
12
|
+
}
|
|
13
|
+
const exploreData = (explore) => `<script type="application/json" id="deck-explore">${JSON.stringify(explore).replace(/</g, "\\u003c")}<\/script>`;
|
|
14
|
+
const EXPLORE_CSS = `:where(deck-stage[data-explore] .sd-leads) { cursor: pointer; }
|
|
15
|
+
:where(deck-stage[data-explore] .sd-leads:hover) { outline: 3px solid var(--sd-accent, var(--accent-ink, #0b6e99)); outline-offset: 6px; }
|
|
16
|
+
:where(.explore-back) {
|
|
17
|
+
position: fixed; left: 12px; top: 12px; z-index: 10; padding: 8px 14px; border-radius: 20px; cursor: pointer;
|
|
18
|
+
border: 1.5px solid var(--line, #c9d0d9); background: var(--bg-elev, #f6f8fa); color: var(--fg, #14181f);
|
|
19
|
+
font: 600 14px/1 var(--sans, system-ui, sans-serif);
|
|
20
|
+
}
|
|
21
|
+
@media print { :where(.explore-back) { display: none; } }`;
|
|
22
|
+
function exploreRuntime() {
|
|
23
|
+
const stage = document.querySelector("deck-stage[data-explore]"), data = document.getElementById("deck-explore");
|
|
24
|
+
if (!stage || !data || window.name === "deck-presenter" || /[?&]_snthumb=/.test(location.search)) return;
|
|
25
|
+
const { slides, targets } = JSON.parse(data.textContent), back = [];
|
|
26
|
+
const button = document.createElement("button");
|
|
27
|
+
button.type = "button";
|
|
28
|
+
button.className = "explore-back";
|
|
29
|
+
button.hidden = true;
|
|
30
|
+
document.body.appendChild(button);
|
|
31
|
+
const shown = () => [...stage.children].filter((s) => s.tagName === "SECTION")[stage.index];
|
|
32
|
+
const unseen = ".sd-gone, .sd-hide, .sd-twin-off";
|
|
33
|
+
const controls = 'a[href], button, input, select, textarea, summary, label, video[controls], audio[controls], [role="button"], [onclick], [contenteditable]:not([contenteditable="false" i])';
|
|
34
|
+
const leads = (piece) => {
|
|
35
|
+
if (piece.closest(unseen)) return null;
|
|
36
|
+
const own = targets[slides[stage.index]] ?? {};
|
|
37
|
+
const to = piece.getAttribute("data-k").trim().split(/\s+/).map((k) => Object.hasOwn(own, k) ? own[k] : void 0).find((i) => i !== void 0);
|
|
38
|
+
return to === void 0 || to === stage.index ? null : to;
|
|
39
|
+
};
|
|
40
|
+
const mark = () => {
|
|
41
|
+
document.querySelectorAll(".sd-leads").forEach((p) => p.classList.remove("sd-leads"));
|
|
42
|
+
for (const p of shown().querySelectorAll("[data-k]")) if (leads(p) !== null) p.classList.add("sd-leads");
|
|
43
|
+
button.hidden = !back.length;
|
|
44
|
+
if (back.length) button.textContent = `\u2190 Back to click ${back.at(-1) + 1}`;
|
|
45
|
+
};
|
|
46
|
+
const go = (i) => {
|
|
47
|
+
stage.goTo(i);
|
|
48
|
+
mark();
|
|
49
|
+
};
|
|
50
|
+
const goBack = () => {
|
|
51
|
+
if (back.length) go(back.pop());
|
|
52
|
+
};
|
|
53
|
+
let down = null;
|
|
54
|
+
document.addEventListener("pointerdown", (e) => {
|
|
55
|
+
down = [e.clientX, e.clientY];
|
|
56
|
+
}, true);
|
|
57
|
+
document.addEventListener("click", (e) => {
|
|
58
|
+
const dragged = down && Math.hypot(e.clientX - down[0], e.clientY - down[1]) > 4;
|
|
59
|
+
down = null;
|
|
60
|
+
if (e.altKey || e.button !== 0 || dragged || !stage.isConnected) return;
|
|
61
|
+
const piece = e.target.closest?.("[data-k]");
|
|
62
|
+
if (!piece || !shown().contains(piece) || e.target.closest(controls)) return;
|
|
63
|
+
const to = leads(piece);
|
|
64
|
+
if (to === null) return;
|
|
65
|
+
e.preventDefault();
|
|
66
|
+
back.push(stage.index);
|
|
67
|
+
go(to);
|
|
68
|
+
}, true);
|
|
69
|
+
button.addEventListener("click", (e) => {
|
|
70
|
+
e.stopPropagation();
|
|
71
|
+
button.blur();
|
|
72
|
+
goBack();
|
|
73
|
+
});
|
|
74
|
+
window.addEventListener("keydown", (e) => {
|
|
75
|
+
if (e.key !== "Backspace" || !back.length || !stage.isConnected || e.defaultPrevented || e.metaKey || e.ctrlKey || e.altKey || e.shiftKey) return;
|
|
76
|
+
if (/^(INPUT|TEXTAREA|SELECT)$/.test(e.target?.tagName) || e.target?.isContentEditable) return;
|
|
77
|
+
e.preventDefault();
|
|
78
|
+
goBack();
|
|
79
|
+
});
|
|
80
|
+
stage.addEventListener("slidechange", mark);
|
|
81
|
+
mark();
|
|
82
|
+
}
|
|
83
|
+
export {
|
|
84
|
+
EXPLORE_CSS,
|
|
85
|
+
exploreData,
|
|
86
|
+
exploreRuntime,
|
|
87
|
+
exploreTargets
|
|
88
|
+
};
|
package/dist/deck/init.js
CHANGED
|
@@ -29,6 +29,7 @@ const CLICKS = `<!--
|
|
|
29
29
|
The deck, click by click.
|
|
30
30
|
<section data-label="\u2026" data-kicker="\u2026"> one part of the story; without data-map it is one plain slide
|
|
31
31
|
<section \u2026 data-theme="ledger"> optional: its slides carry data-theme, so theme.css can dress that part
|
|
32
|
+
<section \u2026 data-enter="push" data-enter-from="right"> optional: how the part arrives (cut fade dip wipe split iris push slide whip zoom)
|
|
32
33
|
<section \u2026 data-map="map"> a part told on map.html: every <step> inside is ONE click
|
|
33
34
|
<step data-in="\u2026" data-out="\u2026" data-on="\u2026" data-focus="\u2026">
|
|
34
35
|
data-in pieces that come in (they stay) data-out pieces that leave
|
package/dist/deck/page.js
CHANGED
|
@@ -2,6 +2,8 @@ import { focusRuntime } from "../focusHtml.js";
|
|
|
2
2
|
import { notesData, notesRuntime, listenRuntime } from "../narrationHtml.js";
|
|
3
3
|
import { esc, scriptSafe, styleSafe } from "./html.js";
|
|
4
4
|
import { loopCss } from "./loops.js";
|
|
5
|
+
import { EXPLORE_CSS, exploreData, exploreRuntime } from "./explore.js";
|
|
6
|
+
import { TRANSITION_CSS, transitionData, transitionRuntime } from "./transitions.js";
|
|
5
7
|
function themeRuntime() {
|
|
6
8
|
const root = document.documentElement, btn = document.querySelector(".theme-toggle");
|
|
7
9
|
const set = (dark) => {
|
|
@@ -34,9 +36,9 @@ const TOGGLE = '<button class="theme-toggle" type="button" aria-label="Switch th
|
|
|
34
36
|
function deckPage(compiled, assets, { dev = null } = {}) {
|
|
35
37
|
const { config, slides } = compiled;
|
|
36
38
|
const loops = loopCss(compiled.loops ?? []);
|
|
37
|
-
const css = [...assets.css, ...loops ? [loops] : [], compiled.themeCss ?? ""].join("\n");
|
|
39
|
+
const css = [...assets.css, ...loops ? [loops] : [], ...compiled.explore ? [EXPLORE_CSS] : [], ...compiled.transitions ? [TRANSITION_CSS] : [], compiled.themeCss ?? ""].join("\n");
|
|
38
40
|
const script = dev?.nonce ? `<script nonce="${esc(dev.nonce)}">` : "<script>";
|
|
39
|
-
const runtimes = [focusRuntime, notesRuntime, listenRuntime, themeRuntime].map((fn) => `${script}(${scriptSafe(fn)})();<\/script>`);
|
|
41
|
+
const runtimes = [focusRuntime, notesRuntime, listenRuntime, themeRuntime, ...compiled.explore ? [exploreRuntime] : [], ...compiled.transitions ? [transitionRuntime] : []].map((fn) => `${script}(${scriptSafe(fn)})();<\/script>`);
|
|
40
42
|
return `<!doctype html>
|
|
41
43
|
<html lang="${esc(config.lang)}">
|
|
42
44
|
<head>
|
|
@@ -51,12 +53,14 @@ ${config.description ? `<meta name="description" content="${esc(config.descripti
|
|
|
51
53
|
<style>${styleSafe(css)}</style>
|
|
52
54
|
</head>
|
|
53
55
|
<body>
|
|
54
|
-
<deck-stage width="${config.canvas[0]}" height="${config.canvas[1]}" no-rail>
|
|
56
|
+
<deck-stage width="${config.canvas[0]}" height="${config.canvas[1]}" no-rail${compiled.explore ? " data-explore" : ""}>
|
|
55
57
|
${slides.map((s) => s.html).join("\n")}
|
|
56
58
|
</deck-stage>
|
|
57
59
|
${TOGGLE}
|
|
58
60
|
<!-- deck-stage.js, focus and narration runtimes: footprint-storydeck ${esc(assets.version)} (MIT) -->
|
|
59
|
-
${notesData(slides.map((s) => s.notes))}
|
|
61
|
+
${notesData(slides.map((s) => s.notes))}${compiled.explore ? `
|
|
62
|
+
${exploreData(compiled.explore)}` : ""}${compiled.transitions ? `
|
|
63
|
+
${transitionData(compiled.transitions)}` : ""}
|
|
60
64
|
${script}${scriptSafe(assets.stageJs)}<\/script>
|
|
61
65
|
${runtimes.join("\n")}
|
|
62
66
|
${dev ? `${script}(${scriptSafe(dev.client)})();<\/script>
|
package/dist/deck/parse.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { closing, elements, stripComments } from "./html.js";
|
|
2
|
+
import { readAttributes } from "../pieceRules.js";
|
|
2
3
|
const NOTES_CLASS = /\sclass=(["'])(?:[^"']*\s)?notes(?:\s[^"']*)?\1/;
|
|
3
4
|
const BLOCK = /* @__PURE__ */ new Set([
|
|
4
5
|
"br",
|
|
@@ -106,6 +107,13 @@ function readMapStep(label, k, attrs, inner) {
|
|
|
106
107
|
}
|
|
107
108
|
return { attrs, headline, notes };
|
|
108
109
|
}
|
|
110
|
+
function enterOf(attrs) {
|
|
111
|
+
const read = readAttributes(attrs) ?? [], type = read.find((a) => a.name === "data-enter");
|
|
112
|
+
if (!type) return null;
|
|
113
|
+
const given = {};
|
|
114
|
+
for (const a of read) if (a.name.startsWith("data-enter-") && a.name.length > 11) given[a.name.slice(11)] ??= a.value;
|
|
115
|
+
return { type: type.value.trim(), given };
|
|
116
|
+
}
|
|
109
117
|
function parseClicks(html) {
|
|
110
118
|
const source = stripComments(html);
|
|
111
119
|
const parts = [];
|
|
@@ -121,10 +129,11 @@ function parseClicks(html) {
|
|
|
121
129
|
const kicker = attrOf(attrs, "data-kicker") ?? "";
|
|
122
130
|
const map = attrOf(attrs, "data-map");
|
|
123
131
|
const theme = themeOf(label, attrs);
|
|
132
|
+
const enter = enterOf(attrs);
|
|
124
133
|
if (map === null) {
|
|
125
134
|
if (/<step\b/i.test(body)) throw new Error(`${where(label)} has <step> clicks but no data-map \u2014 name the map they are told on (data-map="map")`);
|
|
126
135
|
const { html: inner, notes } = takeNotes(body);
|
|
127
|
-
parts.push({ label, kicker, theme, map: null, body: inner, notes });
|
|
136
|
+
parts.push({ label, kicker, theme, enter, map: null, body: inner, notes });
|
|
128
137
|
continue;
|
|
129
138
|
}
|
|
130
139
|
if (!map.trim()) throw new Error(`${where(label)}: data-map is empty \u2014 name a map (a file in the deck, e.g. data-map="map")`);
|
|
@@ -137,7 +146,7 @@ function parseClicks(html) {
|
|
|
137
146
|
if (!steps.length) throw new Error(`${where(label)} is told on map "${map}" but has no <step> \u2014 a map part is one click per <step>`);
|
|
138
147
|
const outside = plainText(takeNotes(without(body, steps)).html);
|
|
139
148
|
if (outside) throw new Error(`${where(label)}: "${outside.slice(0, 60)}" is outside every <step> \u2014 put a headline or notes inside the step they belong to`);
|
|
140
|
-
parts.push({ label, kicker, theme, map: map.trim(), steps: steps.map(({ attrs: a, inner }, k) => readMapStep(label, k, a, inner)) });
|
|
149
|
+
parts.push({ label, kicker, theme, enter, map: map.trim(), steps: steps.map(({ attrs: a, inner }, k) => readMapStep(label, k, a, inner)) });
|
|
141
150
|
}
|
|
142
151
|
if (!parts.length) throw new Error('the clicks file has no <section> \u2014 a deck is one <section data-label="\u2026"> per part');
|
|
143
152
|
return parts;
|
package/dist/deck/review.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { wordCount } from "./parse.js";
|
|
2
|
+
import { enterText } from "./transitions.js";
|
|
2
3
|
const nameOf = (pieces, i) => pieces[i].keys[0];
|
|
3
4
|
const unique = (names) => [...new Set(names)];
|
|
4
5
|
const list = (names, max = 14) => names.length ? `${names.slice(0, max).join(", ")}${names.length > max ? `, \u2026 (${names.length - max} more)` : ""}` : "\u2014";
|
|
5
6
|
const themed = (c) => c.theme ? ` \xB7 theme ${c.theme}` : "";
|
|
6
7
|
const quote = (s, max = 160) => s.length > max ? `"${s.slice(0, max - 1)}\u2026"` : `"${s}"`;
|
|
8
|
+
const enterLine = (c) => c.enter ? `enters by ${enterText(c.enter)} (on a forward step)` : null;
|
|
7
9
|
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;
|
|
8
10
|
function pieceGroups(click, pieces) {
|
|
9
11
|
const by = (pred) => unique(click.look.pieces.map((p, i) => pred(p) ? nameOf(pieces, i) : null).filter(Boolean));
|
|
@@ -60,6 +62,7 @@ function reviewClick(compiled, which, { maxWords = compiled.config?.checks?.maxW
|
|
|
60
62
|
if (c.kind === "plain") {
|
|
61
63
|
lines.push(`click ${c.n} of ${compiled.clicks.length} \xB7 "${c.label}" (plain slide)${themed(c)}`);
|
|
62
64
|
lines.push(`text (${words(wordCount(c.text))}): ${quote(c.text, 300)}`);
|
|
65
|
+
if (enterLine(c)) lines.push(enterLine(c));
|
|
63
66
|
if (loopsLine(c)) lines.push(loopsLine(c));
|
|
64
67
|
} else {
|
|
65
68
|
lines.push(`click ${c.n} of ${compiled.clicks.length} \xB7 "${c.label}" (map "${c.map}", step ${c.stepIndex + 1} of ${c.steps})${themed(c)}`);
|
|
@@ -84,6 +87,7 @@ function reviewClick(compiled, which, { maxWords = compiled.config?.checks?.maxW
|
|
|
84
87
|
lines.push(`this click: ${changes.length ? changes.join(" \xB7 ") : "nothing changes on the map"}`);
|
|
85
88
|
const motion = motionOf(c.look);
|
|
86
89
|
if (motion.length) lines.push(`motion: ${motion.join(" \xB7 ")}`);
|
|
90
|
+
if (enterLine(c)) lines.push(enterLine(c));
|
|
87
91
|
if (loopsLine(c)) lines.push(loopsLine(c));
|
|
88
92
|
}
|
|
89
93
|
const noteWords = c.notes.reduce((sum, p) => sum + wordCount(p), 0);
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { easeNamed, readTransition } from "footprint-story/motion";
|
|
2
|
+
const CSS_TRANSITIONS = Object.freeze(["cut", "fade", "dip", "wipe", "split", "iris", "push", "slide", "whip", "zoom"]);
|
|
3
|
+
const FPS = 30;
|
|
4
|
+
const r4 = (v) => +(+v).toFixed(4) + 0;
|
|
5
|
+
const pct = (v) => `${r4(v * 100)}%`;
|
|
6
|
+
const UNIT = { left: [-1, 0], right: [1, 0], top: [0, -1], bottom: [0, 1] };
|
|
7
|
+
const REST = { opacity: 1, transform: "translate(0%, 0%) scale(1)", filter: "blur(0px)" };
|
|
8
|
+
const IRIS_UNIT = Math.SQRT1_2;
|
|
9
|
+
function framesAt(type, p, e, [w, h]) {
|
|
10
|
+
const keep = { ...REST };
|
|
11
|
+
if (type === "fade") return [keep, { ...REST, opacity: r4(e) }];
|
|
12
|
+
if (type === "dip") return [{ ...REST, opacity: r4(Math.max(0, 1 - 2 * e)) }, { ...REST, opacity: r4(Math.max(0, 2 * e - 1)) }];
|
|
13
|
+
if (type === "wipe") {
|
|
14
|
+
const hidden = pct(1 - e), from = p.from;
|
|
15
|
+
const inset = from === "left" ? `inset(0% ${hidden} 0% 0%)` : from === "right" ? `inset(0% 0% 0% ${hidden})` : from === "top" ? `inset(0% 0% ${hidden} 0%)` : `inset(${hidden} 0% 0% 0%)`;
|
|
16
|
+
return [keep, { ...REST, clipPath: inset }];
|
|
17
|
+
}
|
|
18
|
+
if (type === "split") {
|
|
19
|
+
const half = pct((1 - e) / 2);
|
|
20
|
+
return [keep, { ...REST, clipPath: p.line === "horizontal" ? `inset(${half} 0% ${half} 0%)` : `inset(0% ${half} 0% ${half})` }];
|
|
21
|
+
}
|
|
22
|
+
if (type === "iris") {
|
|
23
|
+
const [x, y] = p.at, far = Math.max(...[[0, 0], [w, 0], [0, h], [w, h]].map(([cx, cy]) => Math.hypot(cx - x, cy - y)));
|
|
24
|
+
return [keep, { ...REST, clipPath: `circle(${pct(e * far / (Math.hypot(w, h) * IRIS_UNIT))} at ${pct(x / w)} ${pct(y / h)})` }];
|
|
25
|
+
}
|
|
26
|
+
if (type === "push" || type === "whip") {
|
|
27
|
+
const [dx, dy] = UNIT[p.from], blur = type === "whip" ? `blur(${r4(4 * e * (1 - e) * 0.02 * w)}px)` : "blur(0px)";
|
|
28
|
+
return [{ ...REST, transform: `translate(${pct(-dx * e)}, ${pct(-dy * e)}) scale(1)`, filter: blur }, { ...REST, transform: `translate(${pct(dx * (1 - e))}, ${pct(dy * (1 - e))}) scale(1)`, filter: blur }];
|
|
29
|
+
}
|
|
30
|
+
if (type === "slide") {
|
|
31
|
+
const [dx, dy] = UNIT[p.from];
|
|
32
|
+
return [keep, { ...REST, transform: `translate(${pct(dx * (1 - e))}, ${pct(dy * (1 - e))}) scale(1)` }];
|
|
33
|
+
}
|
|
34
|
+
const origin = `${pct(p.at[0] / w)} ${pct(p.at[1] / h)}`;
|
|
35
|
+
return [
|
|
36
|
+
{ ...REST, opacity: r4(1 - e), transform: `translate(0%, 0%) scale(${r4(1 + 0.6 * e)})`, transformOrigin: origin },
|
|
37
|
+
{ ...REST, transform: `translate(0%, 0%) scale(${r4(1 + 0.25 * (1 - e))})`, transformOrigin: origin }
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
const ON_TOP = { fade: "arriving", dip: "arriving", wipe: "arriving", split: "arriving", iris: "arriving", push: "arriving", whip: "arriving", slide: "arriving", zoom: "leaving", cut: "arriving" };
|
|
41
|
+
function transitionKeyframes(t, frame) {
|
|
42
|
+
if (t.type === "cut") return { type: "cut", seconds: 0, on: "arriving", backdrop: null, leaving: [], arriving: [], options: { duration: 0, easing: "linear", fill: "both" } };
|
|
43
|
+
const ease = easeNamed(t.ease, `${t.type}: ease`), steps = Math.max(2, Math.ceil(t.seconds * FPS)), leaving = [], arriving = [];
|
|
44
|
+
for (let i = 0; i <= steps; i++) {
|
|
45
|
+
const [a, b] = framesAt(t.type, t.p, ease(i / steps), frame), offset = r4(i / steps);
|
|
46
|
+
leaving.push({ offset, ...a });
|
|
47
|
+
arriving.push({ offset, ...b });
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
type: t.type,
|
|
51
|
+
seconds: t.seconds,
|
|
52
|
+
on: ON_TOP[t.type],
|
|
53
|
+
backdrop: t.type === "dip" ? t.p.color : null,
|
|
54
|
+
leaving,
|
|
55
|
+
arriving,
|
|
56
|
+
options: { duration: r4(t.seconds * 1e3), easing: "linear", fill: "both" }
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function readEnter(enter, where, { frame = [1920, 1080] } = {}) {
|
|
60
|
+
if (!enter) return null;
|
|
61
|
+
const who = `${where}: data-enter="${enter.type}"`;
|
|
62
|
+
if (!CSS_TRANSITIONS.includes(enter.type)) {
|
|
63
|
+
let known = true;
|
|
64
|
+
try {
|
|
65
|
+
readTransition(enter.type, who);
|
|
66
|
+
} catch {
|
|
67
|
+
known = false;
|
|
68
|
+
}
|
|
69
|
+
throw new Error(`${who} \u2014 ${known ? "a page cannot play it (it needs a canvas, as StoryReel draws it); " : ""}a part arrives by ${CSS_TRANSITIONS.join(", ")}`);
|
|
70
|
+
}
|
|
71
|
+
const params = readTransition(enter.type, who, { frame }).p, out = { type: enter.type };
|
|
72
|
+
for (const [name, text] of Object.entries(enter.given)) {
|
|
73
|
+
if (name !== "seconds" && name !== "ease" && !Object.hasOwn(params, name)) {
|
|
74
|
+
const takes = ["seconds", "ease", ...Object.keys(params)].map((k) => `data-enter-${k}`).join(", ");
|
|
75
|
+
throw new Error(`${who} takes ${takes} \u2014 not data-enter-${name}`);
|
|
76
|
+
}
|
|
77
|
+
const point = Array.isArray(params[name]), centre = point && text.trim() === "centre";
|
|
78
|
+
const value = name === "seconds" ? Number(text) : centre ? [frame[0] / 2, frame[1] / 2] : point ? text.trim().split(/[\s,]+/).map(Number) : text.trim();
|
|
79
|
+
if (name === "seconds" && !Number.isFinite(value)) throw new Error(`${who}: data-enter-seconds is a number of seconds, not "${text}"`);
|
|
80
|
+
if (point && !centre && (value.length !== 2 || value.some((n) => !Number.isFinite(n)))) throw new Error(`${who}: data-enter-${name} is a point, "x y" in slide px (or "centre"), not "${text}"`);
|
|
81
|
+
out[name] = value;
|
|
82
|
+
}
|
|
83
|
+
const t = readTransition(out, who, { frame });
|
|
84
|
+
return { type: t.type, seconds: t.seconds, ease: t.ease, p: t.p };
|
|
85
|
+
}
|
|
86
|
+
const enterText = (t) => `${t.type}${Object.entries(t.p).map(([k, v]) => ` ${k} ${Array.isArray(v) ? v.join(" ") : v}`).join("")}, ${t.type === "cut" ? "at once" : `${t.seconds} s`}`;
|
|
87
|
+
function transitionTable(enters, frame) {
|
|
88
|
+
if (!enters.some(Boolean)) return null;
|
|
89
|
+
const kinds = {}, enter = enters.map((t) => {
|
|
90
|
+
if (!t) return null;
|
|
91
|
+
const key = JSON.stringify([t.type, t.seconds, t.ease, t.p]);
|
|
92
|
+
kinds[key] ??= transitionKeyframes(t, frame);
|
|
93
|
+
return key;
|
|
94
|
+
});
|
|
95
|
+
return { kinds, enter };
|
|
96
|
+
}
|
|
97
|
+
const transitionData = (table) => `<script type="application/json" id="deck-transitions">${JSON.stringify(table).replace(/</g, "\\u003c")}<\/script>`;
|
|
98
|
+
const TRANSITION_CSS = "deck-stage > [data-sd-leaving] { visibility: visible; opacity: 1; }";
|
|
99
|
+
function transitionRuntime() {
|
|
100
|
+
const stage = document.querySelector("deck-stage"), data = document.getElementById("deck-transitions");
|
|
101
|
+
if (!stage || !data) return;
|
|
102
|
+
const { kinds, enter } = JSON.parse(data.textContent);
|
|
103
|
+
let playing = null;
|
|
104
|
+
const settle = () => {
|
|
105
|
+
if (!playing) return;
|
|
106
|
+
const p = playing;
|
|
107
|
+
playing = null;
|
|
108
|
+
p.anims.forEach((a) => a.cancel());
|
|
109
|
+
p.leaving.removeAttribute("data-sd-leaving");
|
|
110
|
+
if (p.looped && !p.leaving.hasAttribute("data-deck-active") && !p.leaving.hasAttribute("data-deck-under")) p.leaving.removeAttribute("data-deck-loop");
|
|
111
|
+
p.leaving.style.zIndex = p.z[0];
|
|
112
|
+
p.arriving.style.zIndex = p.z[1];
|
|
113
|
+
if (p.bg !== null) stage.style.setProperty("--deck-canvas", p.bg);
|
|
114
|
+
stage.removeAttribute("data-deck-changing");
|
|
115
|
+
};
|
|
116
|
+
const calm = () => !!window.matchMedia?.("(prefers-reduced-motion: reduce)").matches || stage.getAttribute("data-motion") === "off";
|
|
117
|
+
window.addEventListener("beforeprint", settle);
|
|
118
|
+
stage.addEventListener("slidechange", (e) => {
|
|
119
|
+
const { index, previousIndex, slide, previousSlide } = e.detail;
|
|
120
|
+
if (index === previousIndex) return;
|
|
121
|
+
settle();
|
|
122
|
+
const t = kinds[enter[index]];
|
|
123
|
+
if (!t || t.type === "cut" || !slide || !previousSlide || index < previousIndex || calm() || slide.hasAttribute("data-deck-static")) return;
|
|
124
|
+
const all = [...stage.children].filter((s) => s.tagName === "SECTION");
|
|
125
|
+
if (!all.slice(previousIndex + 1, index).every((s) => s.hasAttribute("data-deck-skip")) || typeof slide.animate !== "function") return;
|
|
126
|
+
const over = t.on === "leaving";
|
|
127
|
+
const p = {
|
|
128
|
+
anims: [],
|
|
129
|
+
leaving: previousSlide,
|
|
130
|
+
arriving: slide,
|
|
131
|
+
z: [previousSlide.style.zIndex, slide.style.zIndex],
|
|
132
|
+
bg: t.backdrop ? stage.style.getPropertyValue("--deck-canvas") : null,
|
|
133
|
+
looped: false
|
|
134
|
+
};
|
|
135
|
+
previousSlide.setAttribute("data-sd-leaving", "");
|
|
136
|
+
if (slide.hasAttribute("data-deck-loop") && !previousSlide.hasAttribute("data-deck-loop")) {
|
|
137
|
+
previousSlide.setAttribute("data-deck-loop", "");
|
|
138
|
+
p.looped = true;
|
|
139
|
+
}
|
|
140
|
+
previousSlide.style.zIndex = over ? "2" : "1";
|
|
141
|
+
slide.style.zIndex = over ? "1" : "2";
|
|
142
|
+
if (t.backdrop) stage.style.setProperty("--deck-canvas", t.backdrop);
|
|
143
|
+
stage.setAttribute("data-deck-changing", "");
|
|
144
|
+
p.anims = [previousSlide.animate(t.leaving, t.options), slide.animate(t.arriving, t.options)];
|
|
145
|
+
playing = p;
|
|
146
|
+
const done = () => {
|
|
147
|
+
if (playing === p) settle();
|
|
148
|
+
};
|
|
149
|
+
p.anims[1].finished.then(done, done);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
export {
|
|
153
|
+
CSS_TRANSITIONS,
|
|
154
|
+
TRANSITION_CSS,
|
|
155
|
+
enterText,
|
|
156
|
+
readEnter,
|
|
157
|
+
transitionData,
|
|
158
|
+
transitionKeyframes,
|
|
159
|
+
transitionRuntime,
|
|
160
|
+
transitionTable
|
|
161
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "footprint-storydeck",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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",
|
|
@@ -65,7 +65,10 @@ a map, `<i data-k="w" class="wire pkt" data-arrow="client to server"></i>` is a
|
|
|
65
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
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
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);
|
|
68
|
+
data-speed="5"` (packets that keep running along it on the slide shown — loops); `"explore": true` in deck.json lets a
|
|
69
|
+
viewer click a piece to go to the click about it (Back returns); `<section data-enter="push" data-enter-from="right">`
|
|
70
|
+
makes a part arrive with one of StoryReel's changes (cut, fade, dip, wipe, split, iris, push, slide, whip, zoom; on a
|
|
71
|
+
forward step only); and
|
|
69
72
|
`<i class="ic" data-icon="name"></i>` is icons/name.svg. A piece that comes in lights unless the click is `data-quiet`
|
|
70
73
|
(`check` warns: `lit-on-entry`). Motion that keeps going is a loop: `data-loop="pulse"` (or `float`, `marquee`, with a
|
|
71
74
|
pace: `"float 4s"`) on a piece, or `data-loop` alone with the piece's own `--loop: spin 6s linear infinite` in
|
|
@@ -200,9 +200,12 @@
|
|
|
200
200
|
position: relative;
|
|
201
201
|
transform-origin: center center;
|
|
202
202
|
flex-shrink: 0;
|
|
203
|
-
|
|
203
|
+
/* what shows behind the slides (a change of slide that fades both — a dip — shows it); --deck-canvas from the page */
|
|
204
|
+
background: var(--deck-canvas, #fff);
|
|
204
205
|
will-change: transform;
|
|
205
206
|
}
|
|
207
|
+
/* while a change of slide plays (the page sets data-deck-changing), a slide moving in or out stays inside the frame */
|
|
208
|
+
:host([data-deck-changing]) .canvas { overflow: hidden; }
|
|
206
209
|
|
|
207
210
|
/* Slides live in light DOM (via <slot>) so authored CSS still applies.
|
|
208
211
|
We absolutely position each slotted child to stack them. */
|