@hypit/hypit 0.1.14 → 0.2.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/README.md +2 -2
- package/dist/public/browser-capture.d.ts +9 -3
- package/dist/public/caption.d.ts +2 -0
- package/dist/public/narrative.d.ts +2 -0
- package/dist/public/runtime-kit.d.ts +2 -2
- package/dist/public/speech.d.ts +2 -0
- package/examples/semantic-composition/packages/responsive-explainer/README.md +1 -1
- package/package.json +1 -1
- package/packages/browser-capture/README.md +9 -2
- package/packages/browser-capture/package.json +5 -0
- package/packages/browser-capture/src/browser.ts +22 -8
- package/packages/browser-capture/src/index.ts +8 -2
- package/packages/caption/README.md +1 -1
- package/packages/caption/src/display.ts +1 -0
- package/packages/caption-fine/README.md +19 -5
- package/packages/caption-fine/package.json +1 -0
- package/packages/caption-fine/src/joined-box.ts +114 -0
- package/packages/caption-fine/src/manifest.ts +1 -1
- package/packages/caption-fine/src/render.ts +99 -92
- package/packages/caption-fine/src/spacing.ts +8 -61
- package/packages/caption-fine-studio/src/index.ts +2 -4
- package/packages/cli/README.md +3 -2
- package/packages/cli/src/arguments.ts +2 -2
- package/packages/cli/src/command.ts +1 -1
- package/packages/cli/src/commands/environment.ts +9 -2
- package/packages/cli/src/machine-view.ts +1 -1
- package/packages/cli/src/output.ts +1 -0
- package/packages/credential-store-os/src/store.ts +5 -1
- package/packages/narrative/README.md +3 -1
- package/packages/narrative/src/schema.ts +1 -0
- package/packages/narrative/src/types.ts +2 -0
- package/packages/provider-whisperx-local/README.md +26 -10
- package/packages/provider-whisperx-local/src/activation.ts +10 -1
- package/packages/provider-whisperx-local/src/program.ts +8 -4
- package/packages/runtime-host-node/src/index.ts +1 -0
- package/packages/runtime-kit/README.md +6 -2
- package/packages/runtime-kit/src/index.ts +2 -2
- package/packages/runtime-local/README.md +6 -1
- package/packages/runtime-local/src/host.ts +2 -0
- package/packages/runtime-local/src/index.ts +1 -0
- package/packages/runtime-local/src/programs.ts +53 -17
- package/packages/script/README.md +81 -34
- package/packages/script/bin/migrate-0.2.mjs +90 -0
- package/packages/script/src/edit.ts +28 -60
- package/packages/script/src/lexical.ts +59 -130
- package/packages/script/src/manifest.ts +11 -11
- package/packages/script/src/narrative.ts +4 -2
- package/packages/script/src/parser.ts +140 -193
- package/packages/script/src/types.ts +2 -3
- package/packages/studio/src/ui/code.ts +1 -1
- package/packages/studio/src/ui/main.ts +2 -2
- package/packages/studio/src/ui/markers.ts +3 -3
- package/packages/studio/src/ui/selection.ts +1 -1
- package/packages/studio/src/ui/syntax.ts +42 -21
- package/packages/temporal-markup/README.md +1 -1
- package/packages/video-cli/README.md +9 -5
- package/packages/video-cli/src/capture.ts +17 -2
- package/packages/video-cli/src/creation.ts +4 -6
- package/packages/video-cli/src/media.ts +10 -3
- package/packages/whisperx/README.md +56 -3
- package/packages/whisperx/src/component.ts +2 -5
- package/packages/whisperx/src/evidence.ts +4 -3
- package/packages/whisperx/src/index.ts +1 -0
- package/packages/whisperx/src/manifest.ts +2 -3
- package/packages/whisperx/src/surface.ts +3 -6
- package/packages/whisperx/src/types.ts +9 -1
- package/packages/yt-dlp/README.md +9 -3
- package/packages/yt-dlp/package.json +2 -1
- package/packages/yt-dlp/src/download.ts +10 -24
- package/packages/yt-dlp/src/environment.ts +36 -0
- package/packages/yt-dlp/src/index.ts +1 -0
- package/services/whisperx/README.md +45 -12
- package/services/whisperx/src/hypit_whisperx_service/application.py +3 -0
- package/services/whisperx/src/hypit_whisperx_service/check.py +7 -0
- package/services/whisperx/src/hypit_whisperx_service/config.py +7 -0
- package/services/whisperx/src/hypit_whisperx_service/engine.py +21 -5
- package/services/whisperx/src/hypit_whisperx_service/models.py +131 -0
- package/services/whisperx/src/hypit_whisperx_service/prepare.py +21 -3
- package/services/whisperx/src/hypit_whisperx_service/resources.py +19 -3
- package/services/yt-dlp/README.md +14 -15
- package/services/yt-dlp/pyproject.toml +1 -1
- package/services/yt-dlp/uv.lock +513 -2
package/README.md
CHANGED
|
@@ -110,7 +110,7 @@ Drop in a video and your agent clones the whole workflow — or describe what yo
|
|
|
110
110
|
|
|
111
111
|
## Stay Tuned with Us
|
|
112
112
|
|
|
113
|
-
Star
|
|
113
|
+
Star Hypit to bookmark the project. To receive release notifications, select **Watch → Custom → Releases** on GitHub.
|
|
114
114
|
|
|
115
115
|

|
|
116
116
|
|
|
@@ -151,7 +151,7 @@ commands and the repository layout.
|
|
|
151
151
|
<td width="220" align="center">
|
|
152
152
|
<a href="https://openagents.org/blog/posts/2026-09-08-pilot-user-program-zh-hypit"><img alt="OpenAgents" src="./docs/public/openagents.png" width="180"></a>
|
|
153
153
|
</td>
|
|
154
|
-
<td>OpenAgents Workspace is a product that helps you manage AI agents, allowing you to invoke agents running on any device (whether on a mobile phone or computer) anytime, anywhere. Agents can collaborate within the platform to help you complete tasks and boost efficiency. It currently supports integration with over 20 types of agents, including Claude, Codex, OpenClaw, and Hermes (
|
|
154
|
+
<td>OpenAgents Workspace is a product that helps you manage AI agents, allowing you to invoke agents running on any device (whether on a mobile phone or computer) anytime, anywhere. Agents can collaborate within the platform to help you complete tasks and boost efficiency. It currently supports integration with over 20 types of agents, including Claude, Codex, OpenClaw, and Hermes (<a href="https://openagents.org">https://openagents.org</a>)</td>
|
|
155
155
|
</tr>
|
|
156
156
|
<tr>
|
|
157
157
|
<td width="220" align="center">
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { LaunchOptions, Browser, Page, ScreenshotOptions, RecordOptions } from 'puppeteer-core';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
type CaptureBrowserOptions = {
|
|
4
|
+
readonly version?: string;
|
|
5
|
+
readonly cacheDirectory?: string;
|
|
6
|
+
readonly downloadBaseUrl?: string;
|
|
7
|
+
};
|
|
8
|
+
declare function captureBrowserExecutablePath(options?: CaptureBrowserOptions): Promise<string>;
|
|
4
9
|
/** Explicit setup, using Puppeteer's cache configuration and upstream browser installer. */
|
|
5
|
-
declare function installCaptureBrowser(): Promise<string>;
|
|
10
|
+
declare function installCaptureBrowser(options?: CaptureBrowserOptions): Promise<string>;
|
|
6
11
|
|
|
7
12
|
type CaptureOptions = {
|
|
13
|
+
readonly browser?: CaptureBrowserOptions;
|
|
8
14
|
/** Ordinary Puppeteer launch options, including channel, executablePath and defaultViewport. */
|
|
9
15
|
readonly launch?: LaunchOptions;
|
|
10
16
|
/** Page operation and navigation timeout; Puppeteer's default applies when omitted. */
|
|
@@ -53,4 +59,4 @@ type CaptureScript = (context: CaptureScriptContext) => Promise<void>;
|
|
|
53
59
|
declare function withCapture(options: CaptureOptions, task: (session: CaptureSession) => Promise<void>, onOutput?: (output: CaptureOutput) => void): Promise<readonly CaptureOutput[]>;
|
|
54
60
|
|
|
55
61
|
export { captureBrowserExecutablePath, installCaptureBrowser, withCapture };
|
|
56
|
-
export type { CaptureOptions, CaptureOutput, CaptureRecording, CaptureScript, CaptureScriptContext, CaptureSession, RecordingCaptureOptions, ScreenshotCaptureOptions };
|
|
62
|
+
export type { CaptureBrowserOptions, CaptureOptions, CaptureOutput, CaptureRecording, CaptureScript, CaptureScriptContext, CaptureSession, RecordingCaptureOptions, ScreenshotCaptureOptions };
|
package/dist/public/caption.d.ts
CHANGED
|
@@ -187,6 +187,8 @@ type NarrativeMoment = {
|
|
|
187
187
|
};
|
|
188
188
|
/** One author-visible word surface. Punctuation owned by the surface is preserved. */
|
|
189
189
|
type CaptionDisplayWord = {
|
|
190
|
+
/** Authored display separator from the preceding word; ignored at a displayed line start. */
|
|
191
|
+
readonly separatorBefore: "" | " ";
|
|
190
192
|
readonly id: string;
|
|
191
193
|
readonly unitId: string;
|
|
192
194
|
readonly segmentId: string;
|
|
@@ -220,6 +220,8 @@ type NarrativeMomentRef = NarrativeMoment & {
|
|
|
220
220
|
};
|
|
221
221
|
/** One author-visible word surface. Punctuation owned by the surface is preserved. */
|
|
222
222
|
type CaptionDisplayWord = {
|
|
223
|
+
/** Authored display separator from the preceding word; ignored at a displayed line start. */
|
|
224
|
+
readonly separatorBefore: "" | " ";
|
|
223
225
|
readonly id: string;
|
|
224
226
|
readonly unitId: string;
|
|
225
227
|
readonly segmentId: string;
|
|
@@ -358,11 +358,11 @@ type ManagedProgramCommand = {
|
|
|
358
358
|
readonly env?: Readonly<Record<string, string>>;
|
|
359
359
|
};
|
|
360
360
|
type ManagedProgramInstallation = {
|
|
361
|
-
/**
|
|
361
|
+
/** Read-only inspection of the selected environment and resources; no downloads or receipts. */
|
|
362
362
|
probe(): Promise<ManagedProgramState>;
|
|
363
363
|
/** Reconcile an installed environment before a cold start, using its package manager's cache. */
|
|
364
364
|
readonly prepareBeforeStart?: boolean;
|
|
365
|
-
/**
|
|
365
|
+
/** Explicit preparation commands, run for missing resources or cold reconciliation above. */
|
|
366
366
|
readonly commands: readonly ManagedProgramCommand[];
|
|
367
367
|
};
|
|
368
368
|
type ManagedProgramState = {
|
package/dist/public/speech.d.ts
CHANGED
|
@@ -291,6 +291,8 @@ type NarrativeMoment = {
|
|
|
291
291
|
};
|
|
292
292
|
/** One author-visible word surface. Punctuation owned by the surface is preserved. */
|
|
293
293
|
type CaptionDisplayWord = {
|
|
294
|
+
/** Authored display separator from the preceding word; ignored at a displayed line start. */
|
|
295
|
+
readonly separatorBefore: "" | " ";
|
|
294
296
|
readonly id: string;
|
|
295
297
|
readonly unitId: string;
|
|
296
298
|
readonly segmentId: string;
|
|
@@ -30,7 +30,7 @@ With prepared Takes, a Canvas and an exact FontStack already available:
|
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
`during` also accepts a Script Selection or Segment; the shared Window syntax supports other authored
|
|
33
|
-
intervals. `reveal` selects a Script Moment, such as `@demonstrate
|
|
33
|
+
intervals. `reveal` selects a Script Moment, such as `@{demonstrate!}` before the relevant word. Rewriting
|
|
34
34
|
the Script or using another delivery changes the projected frame while retaining the layout behavior.
|
|
35
35
|
`transition-frames` is the duration of that change, separate from the scene's lifetime. `stack-order`
|
|
36
36
|
places this scene among other contributions. The caller supplies its title and exact fonts.
|
package/package.json
CHANGED
|
@@ -11,8 +11,13 @@ needs no browser-library installation or private `node_modules` import path.
|
|
|
11
11
|
`hypit capture install-browser` explicitly prepares the package's tested Chrome for Testing revision
|
|
12
12
|
in the browser cache (`PUPPETEER_CACHE_DIR`, or `~/.cache/puppeteer`).
|
|
13
13
|
`captureBrowserExecutablePath()` reports that path.
|
|
14
|
-
The
|
|
15
|
-
the upstream installer.
|
|
14
|
+
The recommended version is declared in this package's `package.json` under `hypit.captureBrowser`.
|
|
15
|
+
`installCaptureBrowser()` uses the upstream installer. Both it and `captureBrowserExecutablePath()`
|
|
16
|
+
accept `{ version, cacheDirectory, downloadBaseUrl }`; capture accepts the same selection as
|
|
17
|
+
`options.browser`. CLI equivalents are `--browser-version`, `--browser-cache` and
|
|
18
|
+
`--browser-download-base-url`. A custom download base must serve the upstream archive layout;
|
|
19
|
+
there is no retry against an unselected source. Use the same version and cache when preparing and
|
|
20
|
+
capturing. Capture never installs a browser, and a missing selected executable is an error. Existing compatible browsers remain selectable through launch options.
|
|
16
21
|
|
|
17
22
|
## Library
|
|
18
23
|
|
|
@@ -26,6 +31,8 @@ file as it finishes. A task failure closes the browser while preserving earlier
|
|
|
26
31
|
`defaultViewport` replaces it. `channel` selects an installed Chrome channel, `executablePath`
|
|
27
32
|
selects a specific executable, and the default uses this package's tested Chrome for Testing.
|
|
28
33
|
`PUPPETEER_CACHE_DIR` selects the cache directory.
|
|
34
|
+
- `browser`: managed browser version/cache selection. It cannot be combined with an explicit
|
|
35
|
+
`launch.channel` or `launch.executablePath`.
|
|
29
36
|
- `timeoutMs`: operation and navigation timeout for the initial page. Omission keeps Puppeteer's
|
|
30
37
|
default; `0` disables that timeout. Scripts can configure other pages themselves.
|
|
31
38
|
- `ffprobePath`: metadata reader for finished recordings; defaults to `ffprobe` on `PATH`.
|
|
@@ -1,19 +1,33 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
1
2
|
import { homedir } from "node:os";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { Browser, computeExecutablePath, install } from "@puppeteer/browsers";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
export type CaptureBrowserOptions = {
|
|
7
|
+
readonly version?: string;
|
|
8
|
+
readonly cacheDirectory?: string;
|
|
9
|
+
readonly downloadBaseUrl?: string;
|
|
10
|
+
};
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
function selection(options: CaptureBrowserOptions) {
|
|
13
|
+
const manifest = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")) as {
|
|
14
|
+
hypit?: { captureBrowser?: { version?: string } };
|
|
15
|
+
};
|
|
16
|
+
const buildId = options.version ?? manifest.hypit?.captureBrowser?.version;
|
|
17
|
+
if (typeof buildId !== "string" || !/^\d+\.\d+\.\d+\.\d+$/u.test(buildId)) throw new Error("Capture browser requires an exact four-part version");
|
|
18
|
+
const cacheDir = options.cacheDirectory ?? process.env.PUPPETEER_CACHE_DIR ?? join(homedir(), ".cache", "puppeteer");
|
|
19
|
+
return { browser: Browser.CHROME, buildId, cacheDir };
|
|
20
|
+
}
|
|
10
21
|
|
|
11
|
-
export async function captureBrowserExecutablePath(): Promise<string> {
|
|
12
|
-
return computeExecutablePath(
|
|
22
|
+
export async function captureBrowserExecutablePath(options: CaptureBrowserOptions = {}): Promise<string> {
|
|
23
|
+
return computeExecutablePath(selection(options));
|
|
13
24
|
}
|
|
14
25
|
|
|
15
26
|
/** Explicit setup, using Puppeteer's cache configuration and upstream browser installer. */
|
|
16
|
-
export async function installCaptureBrowser(): Promise<string> {
|
|
17
|
-
|
|
27
|
+
export async function installCaptureBrowser(options: CaptureBrowserOptions = {}): Promise<string> {
|
|
28
|
+
if (options.downloadBaseUrl !== undefined && !["https:", "http:"].includes(new URL(options.downloadBaseUrl).protocol)) {
|
|
29
|
+
throw new Error("Capture browser download source must use HTTP or HTTPS");
|
|
30
|
+
}
|
|
31
|
+
const installed = await install({ ...selection(options), ...(options.downloadBaseUrl === undefined ? {} : { baseUrl: options.downloadBaseUrl }) });
|
|
18
32
|
return installed.executablePath;
|
|
19
33
|
}
|
|
@@ -10,7 +10,10 @@ import sharp from "sharp";
|
|
|
10
10
|
import { captureBrowserExecutablePath } from "./browser.js";
|
|
11
11
|
export { captureBrowserExecutablePath, installCaptureBrowser } from "./browser.js";
|
|
12
12
|
|
|
13
|
+
export type { CaptureBrowserOptions } from "./browser.js";
|
|
14
|
+
|
|
13
15
|
export type CaptureOptions = {
|
|
16
|
+
readonly browser?: import("./browser.js").CaptureBrowserOptions;
|
|
14
17
|
/** Ordinary Puppeteer launch options, including channel, executablePath and defaultViewport. */
|
|
15
18
|
readonly launch?: LaunchOptions;
|
|
16
19
|
/** Page operation and navigation timeout; Puppeteer's default applies when omitted. */
|
|
@@ -99,10 +102,13 @@ export async function withCapture(
|
|
|
99
102
|
onOutput?: (output: CaptureOutput) => void,
|
|
100
103
|
): Promise<readonly CaptureOutput[]> {
|
|
101
104
|
const launch = { ...options.launch };
|
|
105
|
+
if ((launch.channel || launch.executablePath) && options.browser !== undefined) {
|
|
106
|
+
throw new Error("Choose a managed capture browser or an explicit launch browser, not both");
|
|
107
|
+
}
|
|
102
108
|
if (!launch.channel && !launch.executablePath) {
|
|
103
|
-
launch.executablePath = await captureBrowserExecutablePath();
|
|
109
|
+
launch.executablePath = await captureBrowserExecutablePath(options.browser);
|
|
104
110
|
if (!existsSync(launch.executablePath)) throw new Error(
|
|
105
|
-
`Capture browser is not installed at ${launch.executablePath}. Run hypit capture install-browser, or select an installed compatible browser.`);
|
|
111
|
+
`Capture browser is not installed at ${launch.executablePath}. Run hypit capture install-browser with the same version and cache selection, or select an installed compatible browser.`);
|
|
106
112
|
}
|
|
107
113
|
const browser = await puppeteer.launch({
|
|
108
114
|
defaultViewport: { width: 1280, height: 720, deviceScaleFactor: 1 },
|
|
@@ -7,7 +7,7 @@ Caption has two independent structures: Script organizes displayed words into Cu
|
|
|
7
7
|
choose how those Cues appear. A Use may begin inside a Cue. It changes presentation without changing
|
|
8
8
|
that Cue's text or restarting its word timing.
|
|
9
9
|
|
|
10
|
-
`CaptionDocument` owns displayed words, display/pronunciation associations, word attributes and `||`
|
|
10
|
+
`CaptionDocument` owns displayed words with authored separators, display/pronunciation associations, word attributes and `||`
|
|
11
11
|
breaks. `Timeline` owns placed Takes and measured word times. `temporalizeCaptionDocument(document,
|
|
12
12
|
timeline)` joins their identities and returns `TimedCaptionProjection`: complete Cues with original
|
|
13
13
|
unit times. Segment, speaking-turn and explicit Cue boundaries organize this content. No Style is
|
|
@@ -8,6 +8,7 @@ function assert(condition: unknown, message: string): asserts condition {
|
|
|
8
8
|
export function assertCaptionDocument(value: CaptionDocument): void {
|
|
9
9
|
assert(value.id.length > 0, "CaptionDocument identity is invalid");
|
|
10
10
|
assert(value.units.length > 0 && value.words.length > 0, "CaptionDocument is empty");
|
|
11
|
+
for (const word of value.words) assert(word.separatorBefore === "" || word.separatorBefore === " ", "Caption word must declare its authored separator");
|
|
11
12
|
const words = new Map(value.words.map((word) => [word.id, word]));
|
|
12
13
|
assert(words.size === value.words.length, "CaptionDocument word ids are repeated");
|
|
13
14
|
const units = new Set<string>();
|
|
@@ -82,8 +82,8 @@ caption.primary {
|
|
|
82
82
|
|
|
83
83
|
## Language, spacing and line layout
|
|
84
84
|
|
|
85
|
-
The same Caption pipeline serves
|
|
86
|
-
individual Han characters as Display Words; punctuation stays with its display word. Fine uses
|
|
85
|
+
The same Caption pipeline serves authored text across writing systems. Script emits lexical words
|
|
86
|
+
and individual Han characters as Display Words; punctuation stays with its display word. Fine uses
|
|
87
87
|
those units for timing and active Paint, while authored Cues remain complete reading phrases.
|
|
88
88
|
Dual Text retains its complete alignment unit even when it displays or speaks several words.
|
|
89
89
|
|
|
@@ -94,9 +94,23 @@ explicit authored group, not a renderer-selected segmentation, and does not intr
|
|
|
94
94
|
The underlying character anchors remain available to other Timeline consumers. `wipe` and
|
|
95
95
|
`typewriter` still animate inside the complete unit over its interval when deliberately selected.
|
|
96
96
|
|
|
97
|
-
`word-gap`
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
`word-gap` sizes the separators authored in `CaptionDisplayWord.separatorBefore`. A numeric or
|
|
98
|
+
writing-system boundary does not create a space: `3D` and `3개월` stay joined, while `是的 就是这样`
|
|
99
|
+
keeps its space. Base text, active text, underlines and joined boxes consume the same boundaries.
|
|
100
|
+
For `active-box: trail; active-box-continuity: joined`, Fine measures the complete stationary Cue,
|
|
101
|
+
including the inactive suffix, and joins its painted line rectangles. An over-wide word's internal
|
|
102
|
+
lines are included. Padding and borders extend beyond those rectangles without changing text flow;
|
|
103
|
+
overlapping line backgrounds share one outline and paint translucent color once. Cue motion then
|
|
104
|
+
moves text and decoration together.
|
|
105
|
+
|
|
106
|
+
This decoration uses HyperFrames' existing local browser-program extension, owned by Fine. Its
|
|
107
|
+
measurement copy uses the same exact-font text and available width, is removed synchronously after
|
|
108
|
+
measurement, and is recomputed when a frame is sought (including after fonts finish loading). It
|
|
109
|
+
adds no layout records to Script, Caption, Timeline or Runtime. Other backends must support the
|
|
110
|
+
selected program format explicitly.
|
|
111
|
+
|
|
112
|
+
`letter-spacing` controls glyph tracking. Exact font fallbacks supply the required glyphs; the layout
|
|
113
|
+
does not select a font by language.
|
|
100
114
|
|
|
101
115
|
| Control | Behavior |
|
|
102
116
|
| --- | --- |
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"@hypit/component-kit": "workspace:*",
|
|
16
16
|
"@hypit/composition": "workspace:*",
|
|
17
17
|
"@hypit/elaborator": "workspace:*",
|
|
18
|
+
"@hypit/hyperframes": "workspace:*",
|
|
18
19
|
"@hypit/media": "workspace:*",
|
|
19
20
|
"@hypit/narrative": "workspace:*",
|
|
20
21
|
"@hypit/program-space": "workspace:*",
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fine's joined decoration is measured after browser layout. Its temporary, untransformed copy
|
|
3
|
+
* uses the same exact-font children and content width, so Cue motion cannot distort measurements.
|
|
4
|
+
* Nothing is retained outside the program or written back into Script/Caption/Timeline.
|
|
5
|
+
*/
|
|
6
|
+
export const joinedBoxSetup = String.raw`
|
|
7
|
+
const layout = root.querySelector('[data-fine-box-layout]');
|
|
8
|
+
const layers = [...root.querySelectorAll('[data-caption-active-box="joined"]')];
|
|
9
|
+
const ns = 'http://www.w3.org/2000/svg';
|
|
10
|
+
const paths = layers.map(layer => {
|
|
11
|
+
const svg = document.createElementNS(ns, 'svg');
|
|
12
|
+
svg.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;overflow:visible';
|
|
13
|
+
const path = document.createElementNS(ns, 'path');
|
|
14
|
+
path.setAttribute('fill', data.background);
|
|
15
|
+
path.setAttribute('stroke', data.borderColor);
|
|
16
|
+
path.setAttribute('stroke-width', String(data.borderWidth));
|
|
17
|
+
svg.append(path); layer.append(svg); return path;
|
|
18
|
+
});
|
|
19
|
+
// Union on actual rectangle edges. Tracing only the exterior removes internal borders and
|
|
20
|
+
// paints translucent fills once, including where line padding overlaps the neighbouring line.
|
|
21
|
+
const outline = rectangles => {
|
|
22
|
+
const xs = [...new Set(rectangles.flatMap(r => [r.left, r.right]))].sort((a,b) => a-b);
|
|
23
|
+
const ys = [...new Set(rectangles.flatMap(r => [r.top, r.bottom]))].sort((a,b) => a-b);
|
|
24
|
+
const cells = ys.slice(1).map((_, y) => xs.slice(1).map((_, x) => rectangles.some(r =>
|
|
25
|
+
r.left <= xs[x] && r.right >= xs[x+1] && r.top <= ys[y] && r.bottom >= ys[y+1])));
|
|
26
|
+
const edges = new Map();
|
|
27
|
+
const key = (x,y) => x+','+y;
|
|
28
|
+
const add = (x,y,ex,ey) => {
|
|
29
|
+
const k = key(x,y), list = edges.get(k) || [];
|
|
30
|
+
list.push([ex,ey]); edges.set(k,list);
|
|
31
|
+
};
|
|
32
|
+
for (let y=0;y<cells.length;y++) for (let x=0;x<cells[y].length;x++) {
|
|
33
|
+
if (!cells[y][x]) continue;
|
|
34
|
+
if (!cells[y-1]?.[x]) add(x,y,x+1,y);
|
|
35
|
+
if (!cells[y]?.[x+1]) add(x+1,y,x+1,y+1);
|
|
36
|
+
if (!cells[y+1]?.[x]) add(x+1,y+1,x,y+1);
|
|
37
|
+
if (!cells[y]?.[x-1]) add(x,y+1,x,y);
|
|
38
|
+
}
|
|
39
|
+
const contours = [];
|
|
40
|
+
while (edges.size) {
|
|
41
|
+
const start = edges.keys().next().value.split(',').map(Number);
|
|
42
|
+
const points = []; let current = start, previous;
|
|
43
|
+
do {
|
|
44
|
+
points.push([xs[current[0]],ys[current[1]]]);
|
|
45
|
+
const k = key(...current), next = edges.get(k);
|
|
46
|
+
let index = 0;
|
|
47
|
+
if (next.length > 1 && previous) {
|
|
48
|
+
const dx = current[0]-previous[0], dy = current[1]-previous[1];
|
|
49
|
+
// At a point contact, keep each contour on its own side of the corner.
|
|
50
|
+
index = next.findIndex(p => dx*(p[1]-current[1])-dy*(p[0]-current[0]) > 0);
|
|
51
|
+
if (index < 0) index = 0;
|
|
52
|
+
}
|
|
53
|
+
const target = next.splice(index,1)[0];
|
|
54
|
+
if (!next.length) edges.delete(k);
|
|
55
|
+
previous = current; current = target;
|
|
56
|
+
} while (current[0] !== start[0] || current[1] !== start[1]);
|
|
57
|
+
const corners = points.filter((p,i) => {
|
|
58
|
+
const a = points[(i+points.length-1)%points.length], b = points[(i+1)%points.length];
|
|
59
|
+
return (p[0]-a[0])*(b[1]-p[1]) !== (p[1]-a[1])*(b[0]-p[0]);
|
|
60
|
+
});
|
|
61
|
+
const rounded = corners.map((p,i) => {
|
|
62
|
+
const a = corners[(i+corners.length-1)%corners.length], b = corners[(i+1)%corners.length];
|
|
63
|
+
const before = Math.hypot(p[0]-a[0],p[1]-a[1]), after = Math.hypot(b[0]-p[0],b[1]-p[1]);
|
|
64
|
+
const radius = Math.min(data.radius, before/2, after/2);
|
|
65
|
+
return { p, start: [p[0]+(a[0]-p[0])*radius/before,p[1]+(a[1]-p[1])*radius/before],
|
|
66
|
+
end: [p[0]+(b[0]-p[0])*radius/after,p[1]+(b[1]-p[1])*radius/after], radius,
|
|
67
|
+
turn: (p[0]-a[0])*(b[1]-p[1])-(p[1]-a[1])*(b[0]-p[0]) };
|
|
68
|
+
});
|
|
69
|
+
contours.push(rounded.map((c,i) => (i ? 'L' : 'M')+c.start.join(' ')
|
|
70
|
+
+(c.radius ? 'A'+c.radius+' '+c.radius+' 0 0 '+(c.turn>0 ? 1 : 0)+' '+c.end.join(' ') : '')).join(' ')+'Z');
|
|
71
|
+
}
|
|
72
|
+
return contours.join(' ');
|
|
73
|
+
};
|
|
74
|
+
return () => {
|
|
75
|
+
const measure = layout.cloneNode(true);
|
|
76
|
+
// Exact font declarations are already inline on the typed text children. Copy the inherited
|
|
77
|
+
// flow properties explicitly; the measurement must not inherit the document body's typography.
|
|
78
|
+
const style = getComputedStyle(layout);
|
|
79
|
+
const rootStyle = getComputedStyle(root);
|
|
80
|
+
const offsetX = parseFloat(rootStyle.paddingLeft), offsetY = parseFloat(rootStyle.paddingTop);
|
|
81
|
+
for (const name of ['font-size','line-height','text-align','direction','letter-spacing','word-spacing']) {
|
|
82
|
+
measure.style.setProperty(name,style.getPropertyValue(name));
|
|
83
|
+
}
|
|
84
|
+
measure.style.cssText += ';position:fixed;left:0;top:0;margin:0;padding:0;border:0;box-sizing:content-box;'
|
|
85
|
+
+'width:'+style.width+';height:auto;max-width:none;transform:none;visibility:hidden;pointer-events:none';
|
|
86
|
+
measure.removeAttribute('id');
|
|
87
|
+
for (const child of measure.querySelectorAll('[id]')) child.removeAttribute('id');
|
|
88
|
+
document.body.append(measure);
|
|
89
|
+
let words;
|
|
90
|
+
try {
|
|
91
|
+
const origin = measure.getBoundingClientRect();
|
|
92
|
+
words = [...measure.querySelectorAll('[data-fine-box-word]')].map(word => {
|
|
93
|
+
const range = document.createRange(); range.selectNodeContents(word);
|
|
94
|
+
return [...range.getClientRects()].filter(r => r.width > 0 && r.height > 0).map(r => ({
|
|
95
|
+
left:r.left-origin.left+offsetX, right:r.right-origin.left+offsetX,
|
|
96
|
+
top:r.top-origin.top+offsetY, bottom:r.bottom-origin.top+offsetY,
|
|
97
|
+
}));
|
|
98
|
+
});
|
|
99
|
+
} finally { measure.remove(); }
|
|
100
|
+
const lines = new Map(); let wordIndex = 0;
|
|
101
|
+
for (let index=0;index<paths.length;index++) {
|
|
102
|
+
for (let count=0;count<data.wordCounts[index];count++) for (const rect of words[wordIndex++]) {
|
|
103
|
+
const line = lines.get(rect.top);
|
|
104
|
+
if (!line) lines.set(rect.top,{...rect});
|
|
105
|
+
else { line.left=Math.min(line.left,rect.left); line.right=Math.max(line.right,rect.right);
|
|
106
|
+
line.bottom=Math.max(line.bottom,rect.bottom); }
|
|
107
|
+
}
|
|
108
|
+
paths[index].setAttribute('d',outline([...lines.values()].map(r => ({
|
|
109
|
+
left:r.left-data.paddingX-data.borderWidth/2, right:r.right+data.paddingX+data.borderWidth/2,
|
|
110
|
+
top:r.top-data.paddingY-data.borderWidth/2, bottom:r.bottom+data.paddingY+data.borderWidth/2,
|
|
111
|
+
}))));
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
`;
|
|
@@ -75,7 +75,7 @@ export const captionFineMarkupSurfaces = [
|
|
|
75
75
|
{ name: "letter-spacing", required: false, fallback: "0",
|
|
76
76
|
summary: "Adds pixels of tracking between glyphs." },
|
|
77
77
|
{ name: "word-gap", required: false, fallback: "a quarter of size",
|
|
78
|
-
summary: "Sets pixel
|
|
78
|
+
summary: "Sets the pixel width of authored display separators. Writing-system and numeric boundaries do not create spaces; use letter-spacing for glyph tracking." },
|
|
79
79
|
{ name: "size", required: true,
|
|
80
80
|
summary: "Sets the type size in pixels." },
|
|
81
81
|
{ name: "kerning", required: false, values: ["auto", "normal", "none"], fallback: "auto",
|