@cesarvenzac/closeup 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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +127 -2
  3. package/dist/actions.d.ts +27 -0
  4. package/dist/actions.js +13 -0
  5. package/dist/capture.d.ts +28 -0
  6. package/dist/capture.js +33 -0
  7. package/dist/compositor.d.ts +62 -0
  8. package/dist/compositor.js +164 -0
  9. package/dist/cursor.d.ts +49 -0
  10. package/dist/cursor.js +152 -0
  11. package/dist/cursors.d.ts +11 -0
  12. package/dist/cursors.js +24 -0
  13. package/dist/frame.d.ts +86 -0
  14. package/dist/frame.js +78 -0
  15. package/dist/hover.d.ts +23 -0
  16. package/dist/hover.js +89 -0
  17. package/dist/index.d.ts +24 -0
  18. package/dist/index.js +37 -0
  19. package/dist/path.d.ts +41 -0
  20. package/dist/path.js +65 -0
  21. package/dist/player.d.ts +39 -0
  22. package/dist/player.js +140 -0
  23. package/dist/pointer.d.ts +30 -0
  24. package/dist/pointer.js +134 -0
  25. package/dist/script.d.ts +75 -0
  26. package/dist/script.js +94 -0
  27. package/dist/selector.d.ts +22 -0
  28. package/dist/selector.js +83 -0
  29. package/dist/settings.d.ts +61 -0
  30. package/dist/settings.js +59 -0
  31. package/dist/shortcut.d.ts +23 -0
  32. package/dist/shortcut.js +48 -0
  33. package/dist/sounds.d.ts +22 -0
  34. package/dist/sounds.js +94 -0
  35. package/dist/stage.d.ts +12 -0
  36. package/dist/stage.js +53 -0
  37. package/dist/store.d.ts +4 -0
  38. package/dist/store.js +22 -0
  39. package/dist/take.d.ts +69 -0
  40. package/dist/take.js +220 -0
  41. package/dist/ui/editor.d.ts +90 -0
  42. package/dist/ui/editor.js +612 -0
  43. package/dist/ui/icons.d.ts +28 -0
  44. package/dist/ui/icons.js +28 -0
  45. package/dist/ui/panel.d.ts +83 -0
  46. package/dist/ui/panel.js +739 -0
  47. package/dist/ui/styles.d.ts +6 -0
  48. package/dist/ui/styles.js +183 -0
  49. package/package.json +36 -4
@@ -0,0 +1,61 @@
1
+ /** Every choice a take makes, their defaults, and keeping them between visits. */
2
+ import type { Shape } from "./frame.ts";
3
+ export declare const GRADIENTS: readonly [{
4
+ readonly name: "Dawn";
5
+ readonly from: "#f9d9e8";
6
+ readonly to: "#fdebd3";
7
+ readonly angle: 135;
8
+ }, {
9
+ readonly name: "Mist";
10
+ readonly from: "#e3ecfb";
11
+ readonly to: "#cfdcf1";
12
+ readonly angle: 160;
13
+ }, {
14
+ readonly name: "Sage";
15
+ readonly from: "#e6efe2";
16
+ readonly to: "#cfe0d3";
17
+ readonly angle: 135;
18
+ }, {
19
+ readonly name: "Sand";
20
+ readonly from: "#f6f0e6";
21
+ readonly to: "#e6dcc8";
22
+ readonly angle: 120;
23
+ }, {
24
+ readonly name: "Dusk";
25
+ readonly from: "#3b2a6b";
26
+ readonly to: "#c76b7c";
27
+ readonly angle: 135;
28
+ }, {
29
+ readonly name: "Ink";
30
+ readonly from: "#1d1f24";
31
+ readonly to: "#3a3e47";
32
+ readonly angle: 160;
33
+ }];
34
+ export type BackgroundKind = "none" | "color" | "gradient" | "image";
35
+ export type Settings = {
36
+ mode: "photo" | "video";
37
+ pick: "element" | "region";
38
+ shape: Shape;
39
+ /** Room round what's picked, a share of its size on each side. */
40
+ padding: number;
41
+ background: BackgroundKind;
42
+ color: string;
43
+ gradient: number;
44
+ /** Room between the take's edge and the page, on a background. */
45
+ inset: number;
46
+ radius: number;
47
+ shadow: boolean;
48
+ cursor: boolean;
49
+ cursorSize: number;
50
+ /** A ring spreading from the cursor's tip on each click. */
51
+ ring: boolean;
52
+ /** Who moves the cursor in a video: your mouse, or the page's saved script. */
53
+ moves: "yours" | "script";
54
+ clicks: boolean;
55
+ tabAudio: boolean;
56
+ quality: "standard" | "high";
57
+ };
58
+ export declare const DEFAULTS: Settings;
59
+ /** The saved settings over the defaults, anything unknown or of the wrong type left out. */
60
+ export declare function loadSettings(): Settings;
61
+ export declare function saveSettings(settings: Settings): void;
@@ -0,0 +1,59 @@
1
+ /** Every choice a take makes, their defaults, and keeping them between visits. */
2
+ import { SHAPES } from "./frame.js";
3
+ export const GRADIENTS = [
4
+ { name: "Dawn", from: "#f9d9e8", to: "#fdebd3", angle: 135 },
5
+ { name: "Mist", from: "#e3ecfb", to: "#cfdcf1", angle: 160 },
6
+ { name: "Sage", from: "#e6efe2", to: "#cfe0d3", angle: 135 },
7
+ { name: "Sand", from: "#f6f0e6", to: "#e6dcc8", angle: 120 },
8
+ { name: "Dusk", from: "#3b2a6b", to: "#c76b7c", angle: 135 },
9
+ { name: "Ink", from: "#1d1f24", to: "#3a3e47", angle: 160 },
10
+ ];
11
+ export const DEFAULTS = {
12
+ mode: "video",
13
+ pick: "element",
14
+ shape: "16:9",
15
+ padding: 0.3,
16
+ background: "none",
17
+ color: "#f2f1ee",
18
+ gradient: 0,
19
+ inset: 0.08,
20
+ radius: 18,
21
+ shadow: true,
22
+ cursor: true,
23
+ cursorSize: 48,
24
+ ring: true,
25
+ moves: "yours",
26
+ clicks: true,
27
+ tabAudio: false,
28
+ quality: "high",
29
+ };
30
+ const KEY = "closeup:settings";
31
+ /** The saved settings over the defaults, anything unknown or of the wrong type left out. */
32
+ export function loadSettings() {
33
+ try {
34
+ const saved = JSON.parse(localStorage.getItem(KEY) ?? "{}");
35
+ const settings = { ...DEFAULTS };
36
+ for (const key of Object.keys(DEFAULTS)) {
37
+ if (typeof saved[key] === typeof DEFAULTS[key])
38
+ settings[key] = saved[key];
39
+ }
40
+ if (!(settings.shape in SHAPES))
41
+ settings.shape = DEFAULTS.shape;
42
+ if (!(settings.gradient in GRADIENTS))
43
+ settings.gradient = 0;
44
+ if (settings.moves !== "script")
45
+ settings.moves = "yours";
46
+ return settings;
47
+ }
48
+ catch {
49
+ return { ...DEFAULTS };
50
+ }
51
+ }
52
+ export function saveSettings(settings) {
53
+ try {
54
+ localStorage.setItem(KEY, JSON.stringify(settings));
55
+ }
56
+ catch {
57
+ // Private windows and blocked storage: the settings last as long as the page.
58
+ }
59
+ }
@@ -0,0 +1,23 @@
1
+ /** Keyboard shortcuts as people write them ("Alt+Shift+C"), matched by key position so ⌥ letters still match on a Mac. */
2
+ export type Shortcut = {
3
+ alt: boolean;
4
+ shift: boolean;
5
+ meta: boolean;
6
+ ctrl: boolean;
7
+ code: string;
8
+ };
9
+ /** "Alt+Shift+C" → the modifiers and the key's code ("KeyC"); a digit is "Digit1". Every shortcut needs a modifier. */
10
+ export declare function parseShortcut(text: string): Shortcut;
11
+ type KeyLike = {
12
+ code: string;
13
+ altKey: boolean;
14
+ shiftKey: boolean;
15
+ metaKey: boolean;
16
+ ctrlKey: boolean;
17
+ };
18
+ export declare const matches: (event: KeyLike, shortcut: Shortcut) => boolean;
19
+ /** For showing: "⌥⇧C". */
20
+ export declare function shortcutLabel(shortcut: Shortcut): string;
21
+ /** Typing in a field isn't reaching for a shortcut: there ⌥⇧C types a letter. */
22
+ export declare const typing: (target: EventTarget | null) => boolean;
23
+ export {};
@@ -0,0 +1,48 @@
1
+ /** Keyboard shortcuts as people write them ("Alt+Shift+C"), matched by key position so ⌥ letters still match on a Mac. */
2
+ const MODIFIERS = {
3
+ alt: "alt",
4
+ option: "alt",
5
+ "⌥": "alt",
6
+ shift: "shift",
7
+ "⇧": "shift",
8
+ meta: "meta",
9
+ cmd: "meta",
10
+ command: "meta",
11
+ "⌘": "meta",
12
+ ctrl: "ctrl",
13
+ control: "ctrl",
14
+ "⌃": "ctrl",
15
+ };
16
+ /** "Alt+Shift+C" → the modifiers and the key's code ("KeyC"); a digit is "Digit1". Every shortcut needs a modifier. */
17
+ export function parseShortcut(text) {
18
+ const parts = text.split("+").map((part) => part.trim()).filter(Boolean);
19
+ const shortcut = { alt: false, shift: false, meta: false, ctrl: false, code: "" };
20
+ for (const part of parts) {
21
+ const modifier = MODIFIERS[part.toLowerCase()];
22
+ if (modifier)
23
+ shortcut[modifier] = true;
24
+ else if (/^[a-z]$/i.test(part))
25
+ shortcut.code = `Key${part.toUpperCase()}`;
26
+ else if (/^[0-9]$/.test(part))
27
+ shortcut.code = `Digit${part}`;
28
+ else
29
+ shortcut.code = part;
30
+ }
31
+ if (!shortcut.code)
32
+ throw new Error(`No key in shortcut "${text}"`);
33
+ if (!shortcut.alt && !shortcut.meta && !shortcut.ctrl)
34
+ throw new Error(`Shortcut "${text}" needs ⌥, ⌘ or Ctrl: a bare key would fire while typing`);
35
+ return shortcut;
36
+ }
37
+ export const matches = (event, shortcut) => event.code === shortcut.code &&
38
+ event.altKey === shortcut.alt &&
39
+ event.shiftKey === shortcut.shift &&
40
+ event.metaKey === shortcut.meta &&
41
+ event.ctrlKey === shortcut.ctrl;
42
+ /** For showing: "⌥⇧C". */
43
+ export function shortcutLabel(shortcut) {
44
+ const key = shortcut.code.replace(/^Key|^Digit/, "");
45
+ return `${shortcut.ctrl ? "⌃" : ""}${shortcut.alt ? "⌥" : ""}${shortcut.shift ? "⇧" : ""}${shortcut.meta ? "⌘" : ""}${key}`;
46
+ }
47
+ /** Typing in a field isn't reaching for a shortcut: there ⌥⇧C types a letter. */
48
+ export const typing = (target) => target instanceof HTMLElement && (target.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName));
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The take's sound: the tab's own, if it was shared, a click for each press of the drawn cursor
3
+ * and a softer tick for each key a script types, synthesised (nothing downloaded) and played to
4
+ * the speakers too, so the person recording hears what the video will. All of it mixed into one
5
+ * audio track for the recorder.
6
+ */
7
+ export declare class Mixer {
8
+ readonly context: AudioContext;
9
+ private readonly destination;
10
+ private readonly monitor;
11
+ private readonly noise;
12
+ private sources;
13
+ constructor();
14
+ get track(): MediaStreamTrack;
15
+ /** The tab's own sound, into the recording only: the tab already plays it aloud. */
16
+ add(track: MediaStreamTrack): void;
17
+ /** A trackpad's click: a firmer tick going down, a lighter one coming up. */
18
+ click(down: boolean): void;
19
+ /** A key going down: a short, soft tick, lighter than a click, each a little different. */
20
+ key(): void;
21
+ close(): void;
22
+ }
package/dist/sounds.js ADDED
@@ -0,0 +1,94 @@
1
+ /**
2
+ * The take's sound: the tab's own, if it was shared, a click for each press of the drawn cursor
3
+ * and a softer tick for each key a script types, synthesised (nothing downloaded) and played to
4
+ * the speakers too, so the person recording hears what the video will. All of it mixed into one
5
+ * audio track for the recorder.
6
+ */
7
+ export class Mixer {
8
+ context = new AudioContext();
9
+ destination = this.context.createMediaStreamDestination();
10
+ monitor = this.context.createGain();
11
+ noise;
12
+ sources = [];
13
+ constructor() {
14
+ this.monitor.gain.value = 0.6;
15
+ this.monitor.connect(this.context.destination);
16
+ // Half a second of white noise, the body of every click.
17
+ const length = Math.round(this.context.sampleRate * 0.5);
18
+ this.noise = this.context.createBuffer(1, length, this.context.sampleRate);
19
+ const data = this.noise.getChannelData(0);
20
+ for (let k = 0; k < length; k++)
21
+ data[k] = Math.random() * 2 - 1;
22
+ }
23
+ get track() {
24
+ return this.destination.stream.getAudioTracks()[0];
25
+ }
26
+ /** The tab's own sound, into the recording only: the tab already plays it aloud. */
27
+ add(track) {
28
+ const source = this.context.createMediaStreamSource(new MediaStream([track]));
29
+ source.connect(this.destination);
30
+ this.sources.push(source);
31
+ }
32
+ /** A trackpad's click: a firmer tick going down, a lighter one coming up. */
33
+ click(down) {
34
+ const c = this.context;
35
+ if (c.state === "suspended")
36
+ void c.resume();
37
+ const now = c.currentTime;
38
+ const out = c.createGain();
39
+ out.connect(this.destination);
40
+ out.connect(this.monitor);
41
+ const tick = c.createBufferSource();
42
+ tick.buffer = this.noise;
43
+ const band = c.createBiquadFilter();
44
+ band.type = "bandpass";
45
+ band.frequency.value = down ? 3000 : 4200;
46
+ band.Q.value = 1.1;
47
+ const tickGain = c.createGain();
48
+ const peak = down ? 0.5 : 0.22;
49
+ const length = down ? 0.028 : 0.018;
50
+ tickGain.gain.setValueAtTime(0.0001, now);
51
+ tickGain.gain.exponentialRampToValueAtTime(peak, now + 0.001);
52
+ tickGain.gain.exponentialRampToValueAtTime(0.0001, now + length);
53
+ tick.connect(band).connect(tickGain).connect(out);
54
+ tick.start(now, Math.random() * 0.4, length + 0.01);
55
+ if (down) {
56
+ // A low knock under the tick, felt more than heard.
57
+ const knock = c.createOscillator();
58
+ knock.frequency.setValueAtTime(190, now);
59
+ knock.frequency.exponentialRampToValueAtTime(90, now + 0.04);
60
+ const knockGain = c.createGain();
61
+ knockGain.gain.setValueAtTime(0.0001, now);
62
+ knockGain.gain.exponentialRampToValueAtTime(0.3, now + 0.002);
63
+ knockGain.gain.exponentialRampToValueAtTime(0.0001, now + 0.045);
64
+ knock.connect(knockGain).connect(out);
65
+ knock.start(now);
66
+ knock.stop(now + 0.06);
67
+ }
68
+ }
69
+ /** A key going down: a short, soft tick, lighter than a click, each a little different. */
70
+ key() {
71
+ const c = this.context;
72
+ if (c.state === "suspended")
73
+ void c.resume();
74
+ const now = c.currentTime;
75
+ const tick = c.createBufferSource();
76
+ tick.buffer = this.noise;
77
+ const band = c.createBiquadFilter();
78
+ band.type = "bandpass";
79
+ band.frequency.value = 2200 + Math.random() * 900;
80
+ band.Q.value = 1.4;
81
+ const gain = c.createGain();
82
+ gain.gain.setValueAtTime(0.0001, now);
83
+ gain.gain.exponentialRampToValueAtTime(0.16, now + 0.001);
84
+ gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.022);
85
+ tick.connect(band).connect(gain);
86
+ gain.connect(this.destination);
87
+ gain.connect(this.monitor);
88
+ tick.start(now, Math.random() * 0.4, 0.03);
89
+ }
90
+ close() {
91
+ this.sources.forEach((source) => source.disconnect());
92
+ void this.context.close();
93
+ }
94
+ }
@@ -0,0 +1,12 @@
1
+ import type { Stage } from "./frame.ts";
2
+ /**
3
+ * Puts the page on stage: <body> scaled and moved so the frame fills the middle of the window,
4
+ * the browser drawing it at that size (sharp, not an enlarged capture), scrolling locked. The
5
+ * layout doesn't change, only how big it's drawn. Everything Closeup adds lives outside <body>,
6
+ * so it stays put. Resolves once the zoom has settled; the returned function puts it all back.
7
+ */
8
+ export declare function enterStage(stage: Stage, { animate }?: {
9
+ animate?: boolean | undefined;
10
+ }): Promise<() => Promise<void>>;
11
+ export declare const nextFrame: () => Promise<void>;
12
+ export declare const wait: (ms: number) => Promise<void>;
package/dist/stage.js ADDED
@@ -0,0 +1,53 @@
1
+ /** The zoom into the stage and back out: entering eases out over a moment, leaving is quicker. */
2
+ const ENTER_MS = 420;
3
+ const LEAVE_MS = 260;
4
+ const EASE = "cubic-bezier(0.22, 1, 0.36, 1)";
5
+ /**
6
+ * Puts the page on stage: <body> scaled and moved so the frame fills the middle of the window,
7
+ * the browser drawing it at that size (sharp, not an enlarged capture), scrolling locked. The
8
+ * layout doesn't change, only how big it's drawn. Everything Closeup adds lives outside <body>,
9
+ * so it stays put. Resolves once the zoom has settled; the returned function puts it all back.
10
+ */
11
+ export async function enterStage(stage, { animate = true } = {}) {
12
+ const root = document.documentElement;
13
+ const body = document.body;
14
+ const saved = {
15
+ overflow: root.style.overflow,
16
+ transform: body.style.transform,
17
+ origin: body.style.transformOrigin,
18
+ transition: body.style.transition,
19
+ willChange: body.style.willChange,
20
+ };
21
+ root.style.overflow = "hidden";
22
+ body.style.transformOrigin = "0 0";
23
+ const target = `translate(${stage.tx}px, ${stage.ty}px) scale(${stage.scale})`;
24
+ const reduced = matchMedia("(prefers-reduced-motion: reduce)").matches;
25
+ if (animate && !reduced) {
26
+ body.style.transition = `transform ${ENTER_MS}ms ${EASE}`;
27
+ body.style.transform = "translate(0px, 0px) scale(1)";
28
+ // One frame at the start, so the transition has somewhere to come from.
29
+ await nextFrame();
30
+ body.style.transform = target;
31
+ await wait(ENTER_MS + 30);
32
+ body.style.transition = saved.transition;
33
+ }
34
+ else
35
+ body.style.transform = target;
36
+ // No will-change: with it Chrome may keep drawing at the old scale, and the point is drawing at the new one.
37
+ await nextFrame();
38
+ await nextFrame();
39
+ return async () => {
40
+ if (animate && !reduced) {
41
+ body.style.transition = `transform ${LEAVE_MS}ms ${EASE}`;
42
+ body.style.transform = "translate(0px, 0px) scale(1)";
43
+ await wait(LEAVE_MS + 30);
44
+ }
45
+ root.style.overflow = saved.overflow;
46
+ body.style.transform = saved.transform;
47
+ body.style.transformOrigin = saved.origin;
48
+ body.style.transition = saved.transition;
49
+ body.style.willChange = saved.willChange;
50
+ };
51
+ }
52
+ export const nextFrame = () => new Promise((resolve) => requestAnimationFrame(() => resolve()));
53
+ export const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -0,0 +1,4 @@
1
+ /** Each page's script, kept in the browser by its path, so the scene is there the next time. */
2
+ import { type Script } from "./script.ts";
3
+ export declare function loadScript(path?: string): Script | null;
4
+ export declare function saveScript(script: Script | null, path?: string): void;
package/dist/store.js ADDED
@@ -0,0 +1,22 @@
1
+ /** Each page's script, kept in the browser by its path, so the scene is there the next time. */
2
+ import { parseScript } from "./script.js";
3
+ const keyOf = (path) => `closeup:script:${path}`;
4
+ export function loadScript(path = location.pathname) {
5
+ try {
6
+ return parseScript(JSON.parse(localStorage.getItem(keyOf(path)) ?? "null"));
7
+ }
8
+ catch {
9
+ return null;
10
+ }
11
+ }
12
+ export function saveScript(script, path = location.pathname) {
13
+ try {
14
+ if (script)
15
+ localStorage.setItem(keyOf(path), JSON.stringify(script));
16
+ else
17
+ localStorage.removeItem(keyOf(path));
18
+ }
19
+ catch {
20
+ // Private windows and blocked storage: the script lasts as long as the page.
21
+ }
22
+ }
package/dist/take.d.ts ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * A take, start to end: share this tab, put the picked area on stage, crop the capture to it,
3
+ * draw the cursor (following your mouse, or played from a script), compose the frames on their
4
+ * background, and record them (with the clicks and the tab's sound) or keep one as a photo.
5
+ * Everything is put back afterwards, in order.
6
+ */
7
+ import { DrawnCursor } from "./cursor.ts";
8
+ import { type Rect } from "./frame.ts";
9
+ import { SyntheticPointer } from "./pointer.ts";
10
+ import { type Script } from "./script.ts";
11
+ import { type Settings } from "./settings.ts";
12
+ export type Picked = {
13
+ element: Element | null;
14
+ rect: Rect;
15
+ };
16
+ export type Result = {
17
+ kind: "photo" | "video";
18
+ blob: Blob;
19
+ type: string;
20
+ width: number;
21
+ height: number;
22
+ seconds?: number;
23
+ /** The script a video's cursor played, so the take can be filmed again. */
24
+ script?: Script;
25
+ };
26
+ export type Running = {
27
+ /** The staged frame on screen, for placing the controls round it. */
28
+ screen: Rect;
29
+ started: number;
30
+ /** For a scripted take: settles once the script has played and the cursor has lingered. */
31
+ finished?: Promise<void>;
32
+ stop(): Promise<Result>;
33
+ };
34
+ export declare const toRect: ({ x, y, width, height }: DOMRectReadOnly) => Rect;
35
+ /** What a take films: an element with room round it, a drawn region as drawn, either one to the chosen shape. */
36
+ export declare const frameOf: (rect: Rect, element: Element | null, settings: Settings) => Rect;
37
+ /** The box the capture is cropped to, and a dim veil round it that shows what's filmed (outside the box, so never in it). */
38
+ export declare function frameBox(screen: Rect, host: Element): HTMLDivElement;
39
+ /** Where a take's own layers go (the crop box, the cursor, the hidden video), and which events are Closeup's own. */
40
+ export type Layers = {
41
+ host: HTMLElement;
42
+ ignore: (target: EventTarget | null) => boolean;
43
+ };
44
+ /** The page's element at a point, Closeup's own layers left out. */
45
+ export declare const pageHit: ({ host, ignore }: Layers) => (x: number, y: number) => Element | null;
46
+ /** What plays a script: the drawn cursor, and the pointer the page is told about. */
47
+ export type Actor = {
48
+ cursor: DrawnCursor;
49
+ pointer: SyntheticPointer;
50
+ dispose(): void;
51
+ };
52
+ /**
53
+ * What plays a script, set up over the page: a clear sheet that keeps the real mouse off it (so
54
+ * nothing under it hovers) with the real cursor hidden, the page's hover rules copied for the
55
+ * drawn cursor, the pointer the page is told about, and the cursor. `dispose()` takes it all away,
56
+ * telling the page the pointer left so what it hovers lets go.
57
+ */
58
+ export declare function createActor(layers: Layers, { size, onPress, ring }: {
59
+ size: number;
60
+ onPress?: (down: boolean) => void;
61
+ ring?: boolean;
62
+ }): Actor;
63
+ /** A photo of the picked area, on its background, at the stage's resolution. */
64
+ export declare function takePhoto(picked: Picked, settings: Settings, layers: Layers, image: ImageBitmap | null): Promise<Result>;
65
+ /**
66
+ * Starts recording the picked area, the cursor following your mouse or, given a `script`,
67
+ * playing it; `stop()` ends the take and puts the page back.
68
+ */
69
+ export declare function startVideo(picked: Picked, settings: Settings, layers: Layers, image: ImageBitmap | null, script?: Script | null): Promise<Running>;