@kadal/deck 0.0.0-stage → 0.1.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/LICENSE +661 -0
- package/LICENSING.md +40 -0
- package/README.md +170 -2
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/audience/index.d.ts +14 -0
- package/dist/audience/index.js +680 -0
- package/dist/backgrounds/index.d.ts +65 -0
- package/dist/backgrounds/index.js +604 -0
- package/dist/backgrounds/shared.d.ts +7 -0
- package/dist/backgrounds/shared.js +89 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +8 -0
- package/dist/cli/host.js +111 -0
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.js +140 -0
- package/dist/cli/login.js +56 -0
- package/dist/cli/publish.js +159 -0
- package/dist/cli/vite.js +39 -0
- package/dist/compiler/code-meta.d.ts +33 -0
- package/dist/compiler/code-meta.js +81 -0
- package/dist/compiler/index.d.ts +8 -0
- package/dist/compiler/index.js +8 -0
- package/dist/compiler/magic-move.d.ts +26 -0
- package/dist/compiler/magic-move.js +33 -0
- package/dist/compiler/plain-text.js +7 -0
- package/dist/compiler/rehype-code.d.ts +18 -0
- package/dist/compiler/rehype-code.js +66 -0
- package/dist/compiler/rehype-glossary.d.ts +35 -0
- package/dist/compiler/rehype-glossary.js +211 -0
- package/dist/compiler/slots.d.ts +3 -0
- package/dist/compiler/slots.js +36 -0
- package/dist/compiler/split.d.ts +63 -0
- package/dist/compiler/split.js +195 -0
- package/dist/components/Background.d.ts +15 -0
- package/dist/components/Background.js +24 -0
- package/dist/components/Click.d.ts +54 -0
- package/dist/components/Click.js +129 -0
- package/dist/components/Code.d.ts +27 -0
- package/dist/components/Code.js +124 -0
- package/dist/components/Motion.d.ts +25 -0
- package/dist/components/Motion.js +45 -0
- package/dist/components/Sketch.d.ts +44 -0
- package/dist/components/Sketch.js +141 -0
- package/dist/components/Term.d.ts +63 -0
- package/dist/components/Term.js +492 -0
- package/dist/components/bling.d.ts +75 -0
- package/dist/components/bling.js +146 -0
- package/dist/components/content.d.ts +232 -0
- package/dist/components/content.js +637 -0
- package/dist/components/index.d.ts +10 -0
- package/dist/components/index.js +88 -0
- package/dist/core/clicks.d.ts +45 -0
- package/dist/core/clicks.js +75 -0
- package/dist/core/hash.d.ts +33 -0
- package/dist/core/hash.js +49 -0
- package/dist/core/keyboard.d.ts +17 -0
- package/dist/core/keyboard.js +198 -0
- package/dist/core/layout.d.ts +16 -0
- package/dist/core/layout.js +25 -0
- package/dist/core/model.d.ts +259 -0
- package/dist/core/model.js +377 -0
- package/dist/core/navigation.d.ts +74 -0
- package/dist/core/navigation.js +244 -0
- package/dist/core/poster.js +30 -0
- package/dist/core/store.d.ts +20 -0
- package/dist/core/store.js +64 -0
- package/dist/core/touch.js +27 -0
- package/dist/draw/DrawingLayer.js +315 -0
- package/dist/draw/strokes.js +188 -0
- package/dist/export/index.d.ts +68 -0
- package/dist/export/index.js +251 -0
- package/dist/export/run.js +81 -0
- package/dist/glossary/model.d.ts +81 -0
- package/dist/glossary/model.js +156 -0
- package/dist/glossary/parse.d.ts +27 -0
- package/dist/glossary/parse.js +90 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +30 -0
- package/dist/live/LiveProvider.d.ts +20 -0
- package/dist/live/LiveProvider.js +450 -0
- package/dist/live/analytics.d.ts +15 -0
- package/dist/live/analytics.js +103 -0
- package/dist/live/client.d.ts +51 -0
- package/dist/live/client.js +193 -0
- package/dist/live/components.d.ts +64 -0
- package/dist/live/components.js +471 -0
- package/dist/live/context.d.ts +15 -0
- package/dist/live/context.js +28 -0
- package/dist/live/index.d.ts +7 -0
- package/dist/live/index.js +7 -0
- package/dist/live/protocol.d.ts +284 -0
- package/dist/live/protocol.js +42 -0
- package/dist/live/turnstile.js +49 -0
- package/dist/presenter/index.d.ts +20 -0
- package/dist/presenter/index.js +1211 -0
- package/dist/react/AutoAnimate.js +92 -0
- package/dist/react/Backgrounds.d.ts +11 -0
- package/dist/react/Backgrounds.js +154 -0
- package/dist/react/ConfettiLayer.js +85 -0
- package/dist/react/Deck.d.ts +35 -0
- package/dist/react/Deck.js +983 -0
- package/dist/react/Frame.d.ts +16 -0
- package/dist/react/Frame.js +174 -0
- package/dist/react/Hints.d.ts +3 -0
- package/dist/react/Hints.js +201 -0
- package/dist/react/Lightbox.js +54 -0
- package/dist/react/Overview.js +93 -0
- package/dist/react/PrintView.js +128 -0
- package/dist/react/ScrollView.js +128 -0
- package/dist/react/SlideFrame.d.ts +51 -0
- package/dist/react/SlideFrame.js +323 -0
- package/dist/react/Stage.js +151 -0
- package/dist/react/ThemeMenu.d.ts +5 -0
- package/dist/react/ThemeMenu.js +124 -0
- package/dist/react/Zoom.js +23 -0
- package/dist/react/chrome.js +339 -0
- package/dist/react/context.d.ts +165 -0
- package/dist/react/context.js +19 -0
- package/dist/react/hooks.d.ts +33 -0
- package/dist/react/hooks.js +70 -0
- package/dist/react/layouts.d.ts +21 -0
- package/dist/react/layouts.js +104 -0
- package/dist/react/mdx.d.ts +4 -0
- package/dist/react/mdx.js +33 -0
- package/dist/react/terms.d.ts +6 -0
- package/dist/react/terms.js +66 -0
- package/dist/renderers/Island.d.ts +57 -0
- package/dist/renderers/Island.js +223 -0
- package/dist/renderers/contract.d.ts +173 -0
- package/dist/renderers/contract.js +152 -0
- package/dist/renderers/index.d.ts +12 -0
- package/dist/renderers/index.js +16 -0
- package/dist/renderers/react.d.ts +16 -0
- package/dist/renderers/react.js +32 -0
- package/dist/renderers/web-components.d.ts +4 -0
- package/dist/renderers/web-components.js +103 -0
- package/dist/sketch/Canvas.js +49 -0
- package/dist/sketch/dsl.d.ts +131 -0
- package/dist/sketch/dsl.js +281 -0
- package/dist/sketch/index.d.ts +4 -0
- package/dist/sketch/index.js +3 -0
- package/dist/sketch/palette.d.ts +21 -0
- package/dist/sketch/palette.js +66 -0
- package/dist/sketch/render.d.ts +43 -0
- package/dist/sketch/render.js +198 -0
- package/dist/themes/base.css +1704 -0
- package/dist/themes/bling.css +592 -0
- package/dist/themes/glossary.css +317 -0
- package/dist/themes/live.css +1557 -0
- package/dist/themes/minimal.css +83 -0
- package/dist/themes/rla.css +92 -0
- package/dist/themes/sketch.css +121 -0
- package/dist/themes/stack.css +62 -0
- package/dist/vite/index.d.ts +15 -0
- package/dist/vite/index.js +320 -0
- package/package.json +159 -4
- package/tools/verify-island.mjs +150 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { createStore, useStore } from "../core/store.js";
|
|
2
|
+
import { useDeck } from "../react/context.js";
|
|
3
|
+
import { useActions, useLayout, useNav, useUi } from "../react/hooks.js";
|
|
4
|
+
import { Icon } from "../components/bling.js";
|
|
5
|
+
import { useLiveOptional } from "../live/context.js";
|
|
6
|
+
import { EMPTY_ROOM } from "../live/protocol.js";
|
|
7
|
+
import { DRAW_COLORS, DRAW_SIZES, FADE_DWELL, fadeDuration, loadDrawings, newStroke, saveDrawings, segmentsOf, simplify, swatchGradient } from "./strokes.js";
|
|
8
|
+
import { useEffect, useRef, useState } from "react";
|
|
9
|
+
import { createPortal } from "react-dom";
|
|
10
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
+
//#region src/draw/DrawingLayer.tsx
|
|
12
|
+
const noRoom = createStore(EMPTY_ROOM);
|
|
13
|
+
/**
|
|
14
|
+
* Drawing and annotation on the current slide: pen and highlighter, colours, sizes, undo, clear.
|
|
15
|
+
* Strokes are kept in stage coordinates so they line up on every screen size, persisted per
|
|
16
|
+
* deck in localStorage, and — when the room is live — sent to the room so every window shows
|
|
17
|
+
* them. A stylus draws without the mode being on; fingers and the mouse navigate.
|
|
18
|
+
*
|
|
19
|
+
* A stroke drifts through its colour's family as it travels (see `segmentsOf`). Unless the pin
|
|
20
|
+
* is on, a stroke is a scribble: it lingers a few seconds after the pen lifts, then unwinds
|
|
21
|
+
* along its own path — every screen does the same from the moment it saw the stroke, so nothing
|
|
22
|
+
* more needs to cross the wire. Pinned strokes stay, persist, and can be undone or cleared.
|
|
23
|
+
*
|
|
24
|
+
* `inCanvas`: rendered inside a scaled stage canvas (the presenter's current-slide box) rather
|
|
25
|
+
* than over the viewport — it fills the canvas in stage pixels and its toolbar is portalled out.
|
|
26
|
+
*/
|
|
27
|
+
function DrawingLayer({ inCanvas = false } = {}) {
|
|
28
|
+
const deck = useDeck();
|
|
29
|
+
const layout = useLayout();
|
|
30
|
+
const drawing = useUi((s) => s.drawing);
|
|
31
|
+
const { h, v } = useNav((s) => ({
|
|
32
|
+
h: s.h,
|
|
33
|
+
v: s.v
|
|
34
|
+
}));
|
|
35
|
+
const actions = useActions();
|
|
36
|
+
const live = useLiveOptional();
|
|
37
|
+
const slideKey = `${h},${v}`;
|
|
38
|
+
const canDraw = deck.config.drawings.enabled && (!deck.config.drawings.presenterOnly || deck.isPresenter || !live);
|
|
39
|
+
const [local, setLocal] = useState(() => canDraw && deck.config.drawings.persist ? loadDrawings(deck.config.slug || "deck") : {});
|
|
40
|
+
const [tool, setTool] = useState("pen");
|
|
41
|
+
const [color, setColor] = useState(DRAW_COLORS[0]);
|
|
42
|
+
const [size, setSize] = useState(DRAW_SIZES[1]);
|
|
43
|
+
const [keep, setKeep] = useState(false);
|
|
44
|
+
const [current, setCurrent] = useState(null);
|
|
45
|
+
const svg = useRef(null);
|
|
46
|
+
const remote = useStore(live?.client.state ?? noRoom, (s) => s.drawings);
|
|
47
|
+
const seen = useRef(/* @__PURE__ */ new Map());
|
|
48
|
+
const gone = useRef(/* @__PURE__ */ new Set());
|
|
49
|
+
const [progress, setProgress] = useState({});
|
|
50
|
+
const mine = local[slideKey] ?? [];
|
|
51
|
+
const strokes = [...mine, ...(remote[slideKey] ?? []).filter((r) => !mine.some((l) => l.id === r.id))].filter((s) => !gone.current.has(s.id));
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (canDraw && deck.config.drawings.persist) saveDrawings(deck.config.slug || "deck", local);
|
|
54
|
+
}, [
|
|
55
|
+
local,
|
|
56
|
+
canDraw,
|
|
57
|
+
deck.config.drawings.persist,
|
|
58
|
+
deck.config.slug
|
|
59
|
+
]);
|
|
60
|
+
const fadingKey = strokes.filter((s) => s.fade).map((s) => s.id).join(",");
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (!fadingKey) return;
|
|
63
|
+
const fading = strokes.filter((s) => s.fade);
|
|
64
|
+
const now = performance.now();
|
|
65
|
+
for (const s of fading) if (!seen.current.has(s.id)) seen.current.set(s.id, now);
|
|
66
|
+
let raf = 0;
|
|
67
|
+
let timer = null;
|
|
68
|
+
const tick = () => {
|
|
69
|
+
const t = performance.now();
|
|
70
|
+
const next = {};
|
|
71
|
+
const finished = [];
|
|
72
|
+
let unwinding = false;
|
|
73
|
+
let soonest = Number.POSITIVE_INFINITY;
|
|
74
|
+
for (const s of fading) {
|
|
75
|
+
const t0 = seen.current.get(s.id) ?? t;
|
|
76
|
+
const p = (t - t0 - FADE_DWELL) / fadeDuration(s);
|
|
77
|
+
if (p >= 1) finished.push(s.id);
|
|
78
|
+
else if (p > 0) {
|
|
79
|
+
next[s.id] = p;
|
|
80
|
+
unwinding = true;
|
|
81
|
+
} else soonest = Math.min(soonest, t0 + FADE_DWELL - t);
|
|
82
|
+
}
|
|
83
|
+
if (finished.length) {
|
|
84
|
+
for (const id of finished) {
|
|
85
|
+
gone.current.add(id);
|
|
86
|
+
seen.current.delete(id);
|
|
87
|
+
}
|
|
88
|
+
setLocal((d) => {
|
|
89
|
+
const list = d[slideKey] ?? [];
|
|
90
|
+
if (!list.some((s) => finished.includes(s.id))) return d;
|
|
91
|
+
return {
|
|
92
|
+
...d,
|
|
93
|
+
[slideKey]: list.filter((s) => !finished.includes(s.id))
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
live?.client.state.update((st) => {
|
|
97
|
+
const list = st.drawings[slideKey] ?? [];
|
|
98
|
+
if (!list.some((s) => finished.includes(s.id))) return st;
|
|
99
|
+
return {
|
|
100
|
+
...st,
|
|
101
|
+
drawings: {
|
|
102
|
+
...st.drawings,
|
|
103
|
+
[slideKey]: list.filter((s) => !finished.includes(s.id))
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
setProgress((prev) => {
|
|
109
|
+
const a = Object.keys(prev);
|
|
110
|
+
const b = Object.keys(next);
|
|
111
|
+
if (a.length === b.length && a.every((k) => prev[k] === next[k])) return prev;
|
|
112
|
+
return next;
|
|
113
|
+
});
|
|
114
|
+
if (unwinding) raf = requestAnimationFrame(tick);
|
|
115
|
+
else if (Number.isFinite(soonest)) timer = setTimeout(tick, Math.max(16, soonest));
|
|
116
|
+
};
|
|
117
|
+
tick();
|
|
118
|
+
return () => {
|
|
119
|
+
cancelAnimationFrame(raf);
|
|
120
|
+
if (timer) clearTimeout(timer);
|
|
121
|
+
};
|
|
122
|
+
}, [fadingKey, slideKey]);
|
|
123
|
+
const toStage = (e) => {
|
|
124
|
+
const rect = svg.current.getBoundingClientRect();
|
|
125
|
+
return [(e.clientX - rect.left) / rect.width * deck.config.width, (e.clientY - rect.top) / rect.height * deck.config.height];
|
|
126
|
+
};
|
|
127
|
+
const onDown = (e) => {
|
|
128
|
+
if (!canDraw) return;
|
|
129
|
+
if (!drawing && e.pointerType !== "pen") return;
|
|
130
|
+
if (e.button !== 0) return;
|
|
131
|
+
e.target.setPointerCapture?.(e.pointerId);
|
|
132
|
+
const s = newStroke(tool, color, size, !keep);
|
|
133
|
+
s.points.push(toStage(e));
|
|
134
|
+
setCurrent(s);
|
|
135
|
+
};
|
|
136
|
+
const onMove = (e) => {
|
|
137
|
+
if (!current) return;
|
|
138
|
+
setCurrent({
|
|
139
|
+
...current,
|
|
140
|
+
points: [...current.points, toStage(e)]
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
const onUp = () => {
|
|
144
|
+
if (!current) return;
|
|
145
|
+
const done = {
|
|
146
|
+
...current,
|
|
147
|
+
points: simplify(current.points)
|
|
148
|
+
};
|
|
149
|
+
setCurrent(null);
|
|
150
|
+
setLocal((d) => ({
|
|
151
|
+
...d,
|
|
152
|
+
[slideKey]: [...d[slideKey] ?? [], done]
|
|
153
|
+
}));
|
|
154
|
+
if (live && (deck.isPresenter || deck.config.drawings.syncAll)) live.client.send({
|
|
155
|
+
type: "draw",
|
|
156
|
+
slide: slideKey,
|
|
157
|
+
stroke: done
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
const undo = () => {
|
|
161
|
+
setLocal((d) => ({
|
|
162
|
+
...d,
|
|
163
|
+
[slideKey]: (d[slideKey] ?? []).slice(0, -1)
|
|
164
|
+
}));
|
|
165
|
+
if (live && (deck.isPresenter || deck.config.drawings.syncAll)) live.client.send({
|
|
166
|
+
type: "draw:undo",
|
|
167
|
+
slide: slideKey
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
const clear = () => {
|
|
171
|
+
setLocal((d) => ({
|
|
172
|
+
...d,
|
|
173
|
+
[slideKey]: []
|
|
174
|
+
}));
|
|
175
|
+
if (live && (deck.isPresenter || deck.config.drawings.syncAll)) live.client.send({
|
|
176
|
+
type: "draw:clear",
|
|
177
|
+
slide: slideKey
|
|
178
|
+
});
|
|
179
|
+
};
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
const onKey = (e) => {
|
|
182
|
+
if (!drawing) return;
|
|
183
|
+
if (e.code === "KeyC" && e.shiftKey) clear();
|
|
184
|
+
if (e.code === "KeyZ" && (e.ctrlKey || e.metaKey)) undo();
|
|
185
|
+
if (e.code === "Escape") actions.toggleDrawing(false);
|
|
186
|
+
};
|
|
187
|
+
window.addEventListener("keydown", onKey);
|
|
188
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
189
|
+
});
|
|
190
|
+
if (!deck.config.drawings.enabled) return null;
|
|
191
|
+
const toolbar = /* @__PURE__ */ jsxs("div", {
|
|
192
|
+
className: "deck-draw-toolbar",
|
|
193
|
+
role: "toolbar",
|
|
194
|
+
"aria-label": "Drawing tools",
|
|
195
|
+
children: [
|
|
196
|
+
/* @__PURE__ */ jsx("button", {
|
|
197
|
+
type: "button",
|
|
198
|
+
className: tool === "pen" ? "is-on" : "",
|
|
199
|
+
onClick: () => setTool("pen"),
|
|
200
|
+
title: "Pen",
|
|
201
|
+
"aria-label": "Pen",
|
|
202
|
+
children: /* @__PURE__ */ jsx(Icon, { name: "pencil" })
|
|
203
|
+
}),
|
|
204
|
+
/* @__PURE__ */ jsx("button", {
|
|
205
|
+
type: "button",
|
|
206
|
+
className: tool === "highlighter" ? "is-on" : "",
|
|
207
|
+
onClick: () => setTool("highlighter"),
|
|
208
|
+
title: "Highlighter",
|
|
209
|
+
"aria-label": "Highlighter",
|
|
210
|
+
children: /* @__PURE__ */ jsx(Icon, { name: "highlighter" })
|
|
211
|
+
}),
|
|
212
|
+
/* @__PURE__ */ jsx("span", { className: "deck-draw-sep" }),
|
|
213
|
+
DRAW_COLORS.map((c) => /* @__PURE__ */ jsx("button", {
|
|
214
|
+
type: "button",
|
|
215
|
+
className: ["deck-draw-color", color === c ? "is-on" : ""].join(" "),
|
|
216
|
+
onClick: () => setColor(c),
|
|
217
|
+
"aria-label": c,
|
|
218
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
219
|
+
className: "deck-draw-swatch",
|
|
220
|
+
style: { background: swatchGradient(c) }
|
|
221
|
+
})
|
|
222
|
+
}, c)),
|
|
223
|
+
/* @__PURE__ */ jsx("span", { className: "deck-draw-sep" }),
|
|
224
|
+
DRAW_SIZES.map((s) => /* @__PURE__ */ jsx("button", {
|
|
225
|
+
type: "button",
|
|
226
|
+
className: size === s ? "is-on" : "",
|
|
227
|
+
onClick: () => setSize(s),
|
|
228
|
+
title: `Size ${s}`,
|
|
229
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
230
|
+
className: "deck-draw-dot",
|
|
231
|
+
style: {
|
|
232
|
+
width: s + 4,
|
|
233
|
+
height: s + 4
|
|
234
|
+
}
|
|
235
|
+
})
|
|
236
|
+
}, s)),
|
|
237
|
+
/* @__PURE__ */ jsx("span", { className: "deck-draw-sep" }),
|
|
238
|
+
/* @__PURE__ */ jsx("button", {
|
|
239
|
+
type: "button",
|
|
240
|
+
className: keep ? "is-on" : "",
|
|
241
|
+
onClick: () => setKeep((k) => !k),
|
|
242
|
+
title: keep ? "Strokes stay on the slide — click to let them fade" : "Strokes fade after a moment — click to keep them",
|
|
243
|
+
"aria-label": keep ? "Strokes stay" : "Strokes fade",
|
|
244
|
+
children: /* @__PURE__ */ jsx(Icon, { name: keep ? "pin" : "pin-off" })
|
|
245
|
+
}),
|
|
246
|
+
/* @__PURE__ */ jsx("button", {
|
|
247
|
+
type: "button",
|
|
248
|
+
onClick: undo,
|
|
249
|
+
title: "Undo (Ctrl Z)",
|
|
250
|
+
"aria-label": "Undo",
|
|
251
|
+
children: /* @__PURE__ */ jsx(Icon, { name: "undo-2" })
|
|
252
|
+
}),
|
|
253
|
+
/* @__PURE__ */ jsx("button", {
|
|
254
|
+
type: "button",
|
|
255
|
+
onClick: clear,
|
|
256
|
+
title: "Clear (Shift C)",
|
|
257
|
+
"aria-label": "Clear",
|
|
258
|
+
children: /* @__PURE__ */ jsx(Icon, { name: "eraser" })
|
|
259
|
+
}),
|
|
260
|
+
/* @__PURE__ */ jsx("button", {
|
|
261
|
+
type: "button",
|
|
262
|
+
onClick: () => actions.toggleDrawing(false),
|
|
263
|
+
title: "Done (Esc)",
|
|
264
|
+
"aria-label": "Done",
|
|
265
|
+
children: /* @__PURE__ */ jsx(Icon, { name: "check" })
|
|
266
|
+
})
|
|
267
|
+
]
|
|
268
|
+
});
|
|
269
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("svg", {
|
|
270
|
+
ref: svg,
|
|
271
|
+
className: "deck-drawing",
|
|
272
|
+
viewBox: `0 0 ${deck.config.width} ${deck.config.height}`,
|
|
273
|
+
style: inCanvas ? {
|
|
274
|
+
left: 0,
|
|
275
|
+
top: 0,
|
|
276
|
+
width: deck.config.width,
|
|
277
|
+
height: deck.config.height,
|
|
278
|
+
pointerEvents: drawing && canDraw ? "auto" : "none"
|
|
279
|
+
} : {
|
|
280
|
+
left: layout.left,
|
|
281
|
+
top: layout.top,
|
|
282
|
+
width: layout.width,
|
|
283
|
+
height: layout.height,
|
|
284
|
+
pointerEvents: drawing && canDraw ? "auto" : "none"
|
|
285
|
+
},
|
|
286
|
+
onPointerDown: onDown,
|
|
287
|
+
onPointerMove: onMove,
|
|
288
|
+
onPointerUp: onUp,
|
|
289
|
+
onPointerCancel: onUp,
|
|
290
|
+
"aria-hidden": true,
|
|
291
|
+
children: [...strokes, ...current ? [current] : []].map((s) => {
|
|
292
|
+
const p = progress[s.id] ?? 0;
|
|
293
|
+
const from = p > 0 ? Math.floor(p * s.points.length) : 0;
|
|
294
|
+
const wide = s.tool === "highlighter";
|
|
295
|
+
return /* @__PURE__ */ jsx("g", {
|
|
296
|
+
opacity: p > 0 ? 1 - p * .35 : 1,
|
|
297
|
+
style: wide ? { mixBlendMode: "multiply" } : void 0,
|
|
298
|
+
children: segmentsOf(s, from).map((seg, i) => /* @__PURE__ */ jsx("path", {
|
|
299
|
+
d: seg.d,
|
|
300
|
+
stroke: seg.color,
|
|
301
|
+
strokeWidth: wide ? s.size * 3 : s.size,
|
|
302
|
+
strokeOpacity: wide ? .35 : 1,
|
|
303
|
+
strokeLinecap: "round",
|
|
304
|
+
strokeLinejoin: "round",
|
|
305
|
+
fill: "none"
|
|
306
|
+
}, i))
|
|
307
|
+
}, s.id);
|
|
308
|
+
})
|
|
309
|
+
}), drawing && canDraw ? inCanvas ? createPortal(/* @__PURE__ */ jsx("div", {
|
|
310
|
+
className: "deck-draw-toolbar-host",
|
|
311
|
+
children: toolbar
|
|
312
|
+
}), document.body) : toolbar : null] });
|
|
313
|
+
}
|
|
314
|
+
//#endregion
|
|
315
|
+
export { DrawingLayer };
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
//#region src/draw/strokes.ts
|
|
2
|
+
/** A smooth SVG path through a stroke's points: quadratic curves through midpoints. */
|
|
3
|
+
function pathOf(points) {
|
|
4
|
+
if (points.length === 0) return "";
|
|
5
|
+
if (points.length === 1) {
|
|
6
|
+
const [x, y] = points[0];
|
|
7
|
+
return `M ${x} ${y} L ${x + .01} ${y}`;
|
|
8
|
+
}
|
|
9
|
+
let d = `M ${points[0][0]} ${points[0][1]}`;
|
|
10
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
11
|
+
const [x0, y0] = points[i];
|
|
12
|
+
const [x1, y1] = points[i + 1];
|
|
13
|
+
d += ` Q ${x0} ${y0} ${(x0 + x1) / 2} ${(y0 + y1) / 2}`;
|
|
14
|
+
}
|
|
15
|
+
const last = points[points.length - 1];
|
|
16
|
+
d += ` L ${last[0]} ${last[1]}`;
|
|
17
|
+
return d;
|
|
18
|
+
}
|
|
19
|
+
/** Drop points closer than `min` to the previous kept point, so strokes stay light on the wire. */
|
|
20
|
+
function simplify(points, min = 2) {
|
|
21
|
+
const out = [];
|
|
22
|
+
for (const p of points) {
|
|
23
|
+
const last = out[out.length - 1];
|
|
24
|
+
if (!last || Math.hypot(p[0] - last[0], p[1] - last[1]) >= min) out.push(p);
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
function newStroke(tool, color, size, fade = false) {
|
|
29
|
+
return {
|
|
30
|
+
id: `s_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 7)}`,
|
|
31
|
+
tool,
|
|
32
|
+
color,
|
|
33
|
+
size,
|
|
34
|
+
points: [],
|
|
35
|
+
...fade ? { fade: true } : {}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const DRAW_COLORS = [
|
|
39
|
+
"#ef4444",
|
|
40
|
+
"#f59e0b",
|
|
41
|
+
"#22c55e",
|
|
42
|
+
"#3b82f6",
|
|
43
|
+
"#a855f7",
|
|
44
|
+
"#111827",
|
|
45
|
+
"#ffffff"
|
|
46
|
+
];
|
|
47
|
+
const DRAW_SIZES = [
|
|
48
|
+
4,
|
|
49
|
+
8,
|
|
50
|
+
16
|
|
51
|
+
];
|
|
52
|
+
/**
|
|
53
|
+
* Each pen is a small family rather than one flat colour: the swatch shows the family as a
|
|
54
|
+
* gradient, and a stroke drifts through it as it travels — orange through amber and coral and
|
|
55
|
+
* back — so a scribble reads as one gesture with a little life in it. Every screen derives the
|
|
56
|
+
* same colours from the stroke's base colour and points, so nothing extra goes over the wire.
|
|
57
|
+
*/
|
|
58
|
+
const FAMILIES = {
|
|
59
|
+
"#ef4444": [
|
|
60
|
+
"#f43f5e",
|
|
61
|
+
"#ef4444",
|
|
62
|
+
"#fb923c"
|
|
63
|
+
],
|
|
64
|
+
"#f59e0b": [
|
|
65
|
+
"#f97316",
|
|
66
|
+
"#f59e0b",
|
|
67
|
+
"#fbbf24"
|
|
68
|
+
],
|
|
69
|
+
"#22c55e": [
|
|
70
|
+
"#10b981",
|
|
71
|
+
"#22c55e",
|
|
72
|
+
"#84cc16"
|
|
73
|
+
],
|
|
74
|
+
"#3b82f6": [
|
|
75
|
+
"#0ea5e9",
|
|
76
|
+
"#3b82f6",
|
|
77
|
+
"#6366f1"
|
|
78
|
+
],
|
|
79
|
+
"#a855f7": [
|
|
80
|
+
"#8b5cf6",
|
|
81
|
+
"#a855f7",
|
|
82
|
+
"#ec4899"
|
|
83
|
+
],
|
|
84
|
+
"#111827": [
|
|
85
|
+
"#0f172a",
|
|
86
|
+
"#111827",
|
|
87
|
+
"#374151"
|
|
88
|
+
],
|
|
89
|
+
"#ffffff": [
|
|
90
|
+
"#f1f5f9",
|
|
91
|
+
"#ffffff",
|
|
92
|
+
"#e2e8f0"
|
|
93
|
+
]
|
|
94
|
+
};
|
|
95
|
+
function familyOf(color) {
|
|
96
|
+
return FAMILIES[color.toLowerCase()] ?? [
|
|
97
|
+
color,
|
|
98
|
+
color,
|
|
99
|
+
color
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
function swatchGradient(color) {
|
|
103
|
+
const [a, b, c] = familyOf(color);
|
|
104
|
+
return `linear-gradient(135deg, ${a} 0%, ${b} 50%, ${c} 100%)`;
|
|
105
|
+
}
|
|
106
|
+
function hexToRgb(hex) {
|
|
107
|
+
const h = hex.replace("#", "");
|
|
108
|
+
const n = parseInt(h.length === 3 ? [...h].map((c) => c + c).join("") : h, 16);
|
|
109
|
+
return [
|
|
110
|
+
n >> 16 & 255,
|
|
111
|
+
n >> 8 & 255,
|
|
112
|
+
n & 255
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
function mix(a, b, t) {
|
|
116
|
+
const [r1, g1, b1] = hexToRgb(a);
|
|
117
|
+
const [r2, g2, b2] = hexToRgb(b);
|
|
118
|
+
return `rgb(${Math.round(r1 + (r2 - r1) * t)} ${Math.round(g1 + (g2 - g1) * t)} ${Math.round(b1 + (b2 - b1) * t)})`;
|
|
119
|
+
}
|
|
120
|
+
/** The family colour at phase `u` in 0..1: a → b → c → b → a, so the cycle has no seam. */
|
|
121
|
+
function familyAt(color, u) {
|
|
122
|
+
const [a, b, c] = familyOf(color);
|
|
123
|
+
const q = (u % 1 + 1) % 1 * 4;
|
|
124
|
+
if (q < 1) return mix(a, b, q);
|
|
125
|
+
if (q < 2) return mix(b, c, q - 1);
|
|
126
|
+
if (q < 3) return mix(c, b, q - 2);
|
|
127
|
+
return mix(b, a, q - 3);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* A stroke as runs of a few points, each coloured by its distance along the path. Round caps
|
|
131
|
+
* overlap the joins; from `from` on (a point index) the head has already been erased.
|
|
132
|
+
*/
|
|
133
|
+
function segmentsOf(stroke, from = 0, run = 5) {
|
|
134
|
+
const pts = stroke.points;
|
|
135
|
+
if (pts.length === 0) return [];
|
|
136
|
+
const [a, b, c] = familyOf(stroke.color);
|
|
137
|
+
if (a === b && b === c) return [{
|
|
138
|
+
d: pathOf(pts.slice(from)),
|
|
139
|
+
color: stroke.color
|
|
140
|
+
}];
|
|
141
|
+
const out = [];
|
|
142
|
+
let dist = 0;
|
|
143
|
+
const start = Math.max(0, Math.min(from, pts.length - 1));
|
|
144
|
+
for (let i = 1; i <= start; i++) dist += Math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1]);
|
|
145
|
+
for (let i = start; i < pts.length; i += run) {
|
|
146
|
+
const slice = pts.slice(i, Math.min(pts.length, i + run + 1));
|
|
147
|
+
if (slice.length === 0) break;
|
|
148
|
+
let d = dist;
|
|
149
|
+
for (let j = 1; j < slice.length; j++) d += Math.hypot(slice[j][0] - slice[j - 1][0], slice[j][1] - slice[j - 1][1]);
|
|
150
|
+
const mid = (dist + d) / 2;
|
|
151
|
+
out.push({
|
|
152
|
+
d: pathOf(slice),
|
|
153
|
+
color: familyAt(stroke.color, mid / 640)
|
|
154
|
+
});
|
|
155
|
+
dist = d;
|
|
156
|
+
if (slice.length < run + 1) break;
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
/** A fading stroke lingers this long after the pen lifts, then unwinds along its own path. */
|
|
161
|
+
const FADE_DWELL = 5e3;
|
|
162
|
+
/** How long the unwinding takes: roughly the pace it was drawn at, within reason. */
|
|
163
|
+
function fadeDuration(stroke) {
|
|
164
|
+
return Math.min(2400, Math.max(500, stroke.points.length * 9));
|
|
165
|
+
}
|
|
166
|
+
const key = (slug) => `deck.drawings.${slug}`;
|
|
167
|
+
function loadDrawings(slug) {
|
|
168
|
+
try {
|
|
169
|
+
const raw = localStorage.getItem(key(slug));
|
|
170
|
+
const all = raw ? JSON.parse(raw) : {};
|
|
171
|
+
for (const k of Object.keys(all)) all[k] = (all[k] ?? []).filter((s) => !s.fade);
|
|
172
|
+
return all;
|
|
173
|
+
} catch {
|
|
174
|
+
return {};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function saveDrawings(slug, drawings) {
|
|
178
|
+
try {
|
|
179
|
+
const kept = {};
|
|
180
|
+
for (const [k, list] of Object.entries(drawings)) {
|
|
181
|
+
const keep = list.filter((s) => !s.fade);
|
|
182
|
+
if (keep.length) kept[k] = keep;
|
|
183
|
+
}
|
|
184
|
+
localStorage.setItem(key(slug), JSON.stringify(kept));
|
|
185
|
+
} catch {}
|
|
186
|
+
}
|
|
187
|
+
//#endregion
|
|
188
|
+
export { DRAW_COLORS, DRAW_SIZES, FADE_DWELL, fadeDuration, familyAt, familyOf, loadDrawings, newStroke, pathOf, saveDrawings, segmentsOf, simplify, swatchGradient };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
//#region src/export/index.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
* EXPORT — PDF, PNG and PPTX from a running deck, with an optional upload to the host
|
|
5
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
*
|
|
7
|
+
* Drives a headless Chromium over the deck's print view (`?print-pdf`), which lays every slide
|
|
8
|
+
* (and every click step when `pdfSeparateFragments` is on) out as a page at the authored size.
|
|
9
|
+
* PDF comes from the browser's own printer; PNGs are element screenshots of each page; PPTX
|
|
10
|
+
* wraps those PNGs one per slide with the notes attached, so the deck opens in PowerPoint,
|
|
11
|
+
* Keynote and Google Slides looking exactly like the web.
|
|
12
|
+
*
|
|
13
|
+
* Embedded pages cannot be live on paper, so before the build the exporter captures a poster
|
|
14
|
+
* for every iframe slide and `<Iframe>` that names none: a screenshot of the URL at the pane's
|
|
15
|
+
* size, saved under `public/posters/` where the frame shell looks for it (see core/poster.ts).
|
|
16
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
17
|
+
*/
|
|
18
|
+
export interface PosterJob {
|
|
19
|
+
/** The page the frame embeds — what names the poster. */
|
|
20
|
+
url: string;
|
|
21
|
+
/** The page to photograph, when it differs (a state the frame's URL does not carry). */
|
|
22
|
+
capture: string;
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
/** Relative to the deck's `public/`. */
|
|
26
|
+
file: string;
|
|
27
|
+
/** Milliseconds to let the page settle before the shot. */
|
|
28
|
+
wait: number;
|
|
29
|
+
}
|
|
30
|
+
/** Every embedded page in the deck that has no poster of its own, with the size to capture. */
|
|
31
|
+
export declare function posterJobs(deckDir: string, config: {
|
|
32
|
+
width: number;
|
|
33
|
+
height: number;
|
|
34
|
+
}, entry?: string): PosterJob[];
|
|
35
|
+
/**
|
|
36
|
+
* Capture the posters that are missing (or all of them with `refresh`). WebGL pages render
|
|
37
|
+
* through SwiftShader; each page gets its job's `wait` after load for maps and fonts.
|
|
38
|
+
*/
|
|
39
|
+
export declare function capturePosters(deckDir: string, jobs: PosterJob[], { refresh, log }?: {
|
|
40
|
+
refresh?: boolean;
|
|
41
|
+
log?: (line: string) => void;
|
|
42
|
+
}): Promise<number>;
|
|
43
|
+
export interface ExportOptions {
|
|
44
|
+
/** The deck's URL, e.g. http://localhost:4173/showcase/ (with the trailing slash). */
|
|
45
|
+
url: string;
|
|
46
|
+
outDir: string;
|
|
47
|
+
slug: string;
|
|
48
|
+
pdf?: boolean;
|
|
49
|
+
png?: boolean;
|
|
50
|
+
pptx?: boolean;
|
|
51
|
+
/** Device scale for PNG/PPTX captures. */
|
|
52
|
+
scale?: number;
|
|
53
|
+
/** Upload the results to a host (`https://deck.kadal.cc`) using DECK_ADMIN_KEY or a presenter cookie. */
|
|
54
|
+
upload?: string | null;
|
|
55
|
+
adminKey?: string | null;
|
|
56
|
+
log?: (line: string) => void;
|
|
57
|
+
}
|
|
58
|
+
export interface ExportResult {
|
|
59
|
+
files: {
|
|
60
|
+
kind: "pdf" | "png" | "pptx";
|
|
61
|
+
path: string;
|
|
62
|
+
size: number;
|
|
63
|
+
uploaded?: string;
|
|
64
|
+
}[];
|
|
65
|
+
pages: number;
|
|
66
|
+
}
|
|
67
|
+
export declare function exportDeck(options: ExportOptions): Promise<ExportResult>;
|
|
68
|
+
//#endregion
|