@hypit/caption-fine 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 ADDED
@@ -0,0 +1,26 @@
1
+ # Open Source License
2
+
3
+ Hypit is licensed under a modified version of the Apache License 2.0, with the following additional conditions:
4
+
5
+ 1. Hypit may be utilized commercially for your own organization's purposes, including as a rendering or generation backend for applications your organization operates for itself, and as internal tooling within an enterprise. Should any of the conditions below be met, a commercial license must be obtained from the producer:
6
+
7
+ a. Multi-tenant service: Unless explicitly authorized by Hypit.AI in writing, you may not use the Hypit source code, or any derivative work of it, to operate a multi-tenant environment, or to offer Hypit's functionality to third parties as a hosted, managed, or software-as-a-service offering.
8
+ - Tenant Definition: Within the context of Hypit, one tenant corresponds to one workspace — the isolated set of projects, artifacts, credentials, and configuration belonging to a single party. Operating an environment in which two or more parties outside your own organization hold separate workspaces constitutes a multi-tenant service, whether or not a fee is charged.
9
+
10
+ b. Commercial redistribution: Unless explicitly authorized by Hypit.AI in writing, you may not sell, license for a fee, or otherwise supply for commercial gain Hypit or any derivative work of it, whether standalone or as a component of, or bundled with, any product, application, package, or device provided to third parties.
11
+ - Forking, modifying, and publishing the source code of Hypit or a derivative work is permitted, provided the result is made available under the terms of this same license and is not supplied for commercial gain.
12
+
13
+ - Permitted without a commercial license under clauses (a) and (b): obtaining Hypit and running it yourself on your own infrastructure; using it for your own organization's work, including commercial work and work performed for your clients; and single-tenant deployments operated by and for one organization.
14
+
15
+ c. LOGO and copyright information: In the process of using Hypit, you may not remove or modify the name, LOGO, or copyright information presented in the Hypit command-line interface, its generated run reports and manifests, or any user-facing surface derived from them. This restriction is inapplicable to uses of Hypit that do not surface these components to a user.
16
+
17
+ 2. As a contributor, you should agree that:
18
+
19
+ a. The producer can adjust the open-source agreement to be more strict or relaxed as deemed necessary.
20
+ b. Your contributed code may be used for commercial purposes, including but not limited to its cloud business operations.
21
+
22
+ 3. Ownership of output: The producer claims no rights in the content you create with Hypit. Videos, audio, images, manifests, and any other artifacts produced through your use of Hypit belong to you, and this license imposes no conditions on their use — including commercial use. Note that content generated by way of third-party model or service providers may additionally be subject to the terms of those providers.
23
+
24
+ Apart from the specific conditions mentioned above, all other rights and restrictions follow the Apache License 2.0. Detailed information about the Apache License 2.0 can be found at http://www.apache.org/licenses/LICENSE-2.0.
25
+
26
+ © 2026 Hypit.AI
package/README.md CHANGED
@@ -1,3 +1,155 @@
1
- # Temporary Holding Version
1
+ # `@hypit/caption-fine`
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The fine-grained, uniform-flow Caption family. Recipes control placement, typography, Paint,
4
+ active-word treatment and motion. It renders authored Cues whose tokens all obey the same layout,
5
+ type, Paint and motion rules. Spoken time and token order may change the state of that rule; a token
6
+ does not carry a private visual role.
7
+
8
+ ```xml
9
+ <caption-fine:Style id="primary" recipe={recipes.caption.primary} font={caption-font}/>
10
+
11
+ <caption-fine:Caption id="captions-track" document={story.caption} timing={story-captions}
12
+ timeline={speech.timeline} within={vertical.bounds}>
13
+ <caption-fine:Use style={primary}/>
14
+ </caption-fine:Caption>
15
+ ```
16
+
17
+ An optional Region Evidence supplies a moving placement point when the user's reference
18
+ visibly uses head-following Caption or the user asks for that treatment. Ordinary Caption uses the
19
+ Style's placement without regions. Region Evidence is authored numeric input mapped from available picture
20
+ evidence to the composition that consumes it; it is not a face-tracking request:
21
+
22
+ ```xml
23
+ <import as="region" from="@hypit/region-evidence@1"/>
24
+ <region:Evidence id="heads" within={vertical.bounds} timeline={speech.timeline}
25
+ recipe={tracking.heads.default}/>
26
+ <caption-fine:Caption id="captions-track" document={story.caption} timing={story-captions}
27
+ timeline={speech.timeline} within={vertical.bounds} regions={heads}>
28
+ <caption-fine:Use style={primary}/>
29
+ </caption-fine:Caption>
30
+ ```
31
+
32
+ With `regions`, every Cue must carry one Script Role. When Region Evidence contains a measured region for that
33
+ Role and Frame, Fine places the Cue at the region's top center. When that Role has a series but
34
+ the current Frame is `null`, the Cue is not rendered: absence of evidence never becomes a guessed
35
+ position. A Role with no evidence series uses the Style's authored `x` and `y`, so unrelated speakers remain
36
+ ordinary fixed captions. The Style still owns its width and anchors, so `anchor-x: center;
37
+ anchor-y: bottom` puts the Caption immediately above a measured region. The Timeline is finished
38
+ external evidence: Fine does not detect people, associate identities, smooth motion, interpolate
39
+ missing Frames or invoke a Provider. Without `regions`, the ordinary Recipe `x` and `y` behavior is
40
+ unchanged.
41
+
42
+ This keeps placement inspectable and editable. If a tracked face needs padding or an above-head anchor,
43
+ transform the measured numbers while authoring Region Evidence, then give Caption the result. Do
44
+ not hide that transformation in a Provider or ask the Build to rediscover the face.
45
+
46
+ One SVS Recipe freezes three public dimensions:
47
+
48
+ - **Where**: Region position, anchors, extent, block/inline alignment, wrapping and line limits.
49
+ - **How**: the exact font stack, typography, base/active glyph Paint, Cue box and decoration.
50
+ - **When**: the visible lead/tail envelope, handoff, Cue/Atom motion, reveal, Karaoke and loops.
51
+
52
+ An upstream adapter supplies absolute Caption Unit timing. Fine then joins the document's authored
53
+ Cues to that complete timing and produces an explicit visible Schedule. It never splits or merges a
54
+ Cue. Lead and tail never change the source times used by Karaoke. Later Uses mask earlier
55
+ presentation, including Hidden. The final visibility is clipped to the winning Use Window while the
56
+ original Cue envelope and animations are preserved.
57
+
58
+ The Fine Schedule preserves the CaptionTiming's Timeline and document identities. It contains no
59
+ Narrative identity. The renderer rejects any mismatched Timeline or document. Studio may expose
60
+ lead, tail and handoff as ordinary parameter edits, but Cue rectangles remain read-only timing facts.
61
+
62
+ Script segments, turns and `||` organize complete Cues. Timed Uses select presentation without
63
+ changing that grouping. Fine applies uniform rules to the words of a Cue. A caption whose Cue contains structural roles or
64
+ relationships—such as an independently arranged oversized keyword and supporting phrase—can use
65
+ a new project Caption family. Reuse the common [Caption content, timed Uses and timing](../caption/README.md),
66
+ and implement the new schedule and rendering behavior in that package. This is ordinary component
67
+ authorship; different colors or fonts alone can remain Fine Style choices.
68
+
69
+ ```svs
70
+ caption.primary {
71
+ stack-order: 70;
72
+ x: 0.5; y: 0.9; width: 0.84; height: 0.22;
73
+ anchor-x: center; anchor-y: bottom;
74
+ align: center; block-align: end; inline-size: fixed;
75
+ wrap: word;
76
+ size: 58; line-height: 1; fill: #FFFFFF;
77
+ background: #00000000; padding: "0"; radius: 0;
78
+ karaoke: current; active-fill: #FFD54A;
79
+ cue-enter: spring; cue-enter-frames: 4;
80
+ cue-enter-start-scale: 0.75;
81
+ cue-exit: none;
82
+ lead-frames: 4; tail-frames: 4; handoff: cut;
83
+ }
84
+ ```
85
+
86
+ ## Language, spacing and line layout
87
+
88
+ The same Caption pipeline serves authored text across writing systems. Script emits lexical words
89
+ and individual Han characters as Display Words; punctuation stays with its display word. Fine uses
90
+ those units for timing and active Paint, while authored Cues remain complete reading phrases.
91
+ Dual Text retains its complete alignment unit even when it displays or speaks several words.
92
+
93
+ `<组件化|>` authors the same unit as `<组件化|组件化>` without repeating the speech. Fine's `step`
94
+ karaoke, active underline and active box treat that unit together using its first-to-last speech
95
+ interval. Ordinary Chinese characters around it retain their own activation times. This is an
96
+ explicit authored group, not a renderer-selected segmentation, and does not introduce a new Cue.
97
+ The underlying character anchors remain available to other Timeline consumers. `wipe` and
98
+ `typewriter` still animate inside the complete unit over its interval when deliberately selected.
99
+
100
+ `word-gap` sizes the separators authored in `CaptionDisplayWord.separatorBefore`. A numeric or
101
+ writing-system boundary does not create a space: `3D` and `3개월` stay joined, while `是的 就是这样`
102
+ keeps its space. Base text, active text, underlines and joined boxes consume the same boundaries.
103
+ For `active-box: trail; active-box-continuity: joined`, Fine measures the complete stationary Cue,
104
+ including the inactive suffix, and joins its painted line rectangles. An over-wide word's internal
105
+ lines are included. Padding and borders extend beyond those rectangles without changing text flow;
106
+ overlapping line backgrounds share one outline and paint translucent color once. Cue motion then
107
+ moves text and decoration together.
108
+
109
+ This decoration uses the HtmlProgram's local `HtmlVisual` extension, owned by Fine. Its
110
+ measurement copy uses the same exact-font text and available width, is removed synchronously after
111
+ measurement, and is recomputed when a frame is sought (including after fonts finish loading). It
112
+ adds no layout records to Script, Caption, Timeline or Runtime. Other backends must support the
113
+ selected program format explicitly.
114
+
115
+ `letter-spacing` controls glyph tracking. Exact font fallbacks supply the required glyphs; the layout
116
+ does not select a font by language.
117
+
118
+ | Control | Behavior |
119
+ | --- | --- |
120
+ | `width`, `size`, `padding`, `letter-spacing`, `word-gap` | Determine the available space and the text's occupied width. |
121
+ | `wrap: word` | Flows at display-unit boundaries and permits an over-wide word to break. Han units are already characters. |
122
+ | `wrap: grapheme` | Also permits breaking inside a Latin word. |
123
+ | `max-words-per-line` | Optional counted row breaks between complete alignment units. Counts Display Words, normally characters for Chinese; it does not make new Cues. An indivisible Dual Text unit can exceed this count. |
124
+ | `max-lines` | Requires `max-words-per-line`; rejects too many counted rows. It does not measure browser wrapping or guarantee one physical line. |
125
+
126
+ Both count limits are omitted above so the example flows by available width. For a compact
127
+ single-line treatment, author coherent Cues and choose a font, size and width that fit those Cues.
128
+ `karaoke: off` keeps a complete Cue steady; `current` and `trail` follow its timed units, which are
129
+ normally individual characters in Chinese.
130
+
131
+ | Effect | Recipe | Visible behavior |
132
+ | --- | --- | --- |
133
+ | Whole-character emphasis | `karaoke: current; karaoke-transition: step` | The complete current unit takes the active Paint at its start, returning to base Paint at its end. |
134
+ | Accumulating emphasis | `karaoke: trail; karaoke-transition: step` | Each complete unit takes the active Paint at its start and keeps it for the rest of the Cue. |
135
+ | Sweeping fill | `karaoke: trail; karaoke-transition: wipe` | Paint sweeps inside each unit's glyphs during that unit's own time window. |
136
+ | Spoken-unit appearance | `atom-reveal: on-start` | Each complete unit appears at its start. |
137
+ | Typing appearance | `atom-reveal: typewriter` | Whole graphemes appear in sequence within each unit, starting with its first grapheme at the unit's start. A normal Han unit appears as a complete character. |
138
+
139
+ `step` is the default Karaoke transition. Choose it for ordinary Chinese character highlighting;
140
+ `wipe` deliberately produces partial glyph fills. Cue width and duration do not supply a substitute
141
+ clock for either mode. `atom-reveal` controls text appearing, independently of Karaoke recoloring
142
+ already visible text. A Dual Text span shares one timing unit, so its display words activate together
143
+ under `step`; keep pronunciation markup scoped to the name or expression that needs it.
144
+
145
+ The Hypit Skill's Caption craft page owns grouping and visual direction.
146
+
147
+ `font` accepts an exact face or ordered stack. A local file declared through `media:Font` can be the
148
+ primary face or a `<caption-fine:Fallback font={...}/>` child, just like a bundled face. See
149
+ [Media font assets](../media/README.md#font-files) and the independently installed
150
+ [`@hypit/fontsource` adapter](../fontsource/README.md).
151
+
152
+ The package's Studio facet, published as `@hypit/caption-fine/studio`, reads the same schedule and
153
+ authored Use Style references. It presents the actual Cue timing and exposes Use timing and supported
154
+ Style edits in the Inspector. Runtime and authoring support therefore ship as one versioned capability
155
+ rather than as independently versioned packages.
@@ -0,0 +1,8 @@
1
+ export declare const hypitPackage: {
2
+ format: "hypit.package@1";
3
+ modules: {
4
+ manifest: import("@hypit/hypit/protocol").ModuleManifest;
5
+ }[];
6
+ facets: import("@hypit/hypit/facet").Facet[];
7
+ };
8
+ export default hypitPackage;
@@ -0,0 +1,30 @@
1
+ import { createAdmissionPackageFacet } from "@hypit/hypit/admission";
2
+ import { createProducerPackageFacet } from "@hypit/hypit/producer";
3
+ import { createMarkupSurfaceFacet } from "@hypit/hypit/markup";
4
+ import { createStudioCompanionFacet } from "@hypit/studio-companion";
5
+ import { captionFineComponent, captionFineManifest, captionFineModuleRef, decodeFineCaptionStyleSurface, decodeFineCaptionTrackSurface, captionFineMarkupSurfaces, } from "./index.js";
6
+ import { captionFineStudioParameterCompanions, captionFineStudioTrackCompanions } from "./studio.js";
7
+ export const hypitPackage = {
8
+ format: "hypit.package@1",
9
+ modules: [{
10
+ manifest: captionFineManifest,
11
+ }],
12
+ facets: [
13
+ ...[captionFineComponent].flatMap((component) => [createProducerPackageFacet(component), createAdmissionPackageFacet(component)]),
14
+ createMarkupSurfaceFacet({
15
+ module: captionFineModuleRef,
16
+ declaration: captionFineMarkupSurfaces.find((item) => item.name === "style"),
17
+ handler: decodeFineCaptionStyleSurface,
18
+ }),
19
+ createMarkupSurfaceFacet({
20
+ module: captionFineModuleRef,
21
+ declaration: captionFineMarkupSurfaces.find((item) => item.name === "caption"),
22
+ handler: decodeFineCaptionTrackSurface,
23
+ }),
24
+ createStudioCompanionFacet({
25
+ tracks: captionFineStudioTrackCompanions,
26
+ parameters: captionFineStudioParameterCompanions,
27
+ }),
28
+ ],
29
+ };
30
+ export default hypitPackage;
@@ -0,0 +1,37 @@
1
+ export declare const captionFineComponent: {
2
+ producers: ({
3
+ producer: {
4
+ module: {
5
+ readonly name: "@hypit/caption-fine";
6
+ readonly version: "1";
7
+ };
8
+ name: string;
9
+ };
10
+ handler: ({ inputs }: import("@hypit/hypit/producer").ProducerHandlerContext) => {
11
+ outputs: {
12
+ schedule: {
13
+ kind: "inline";
14
+ value: import("@hypit/hypit/protocol").CanonicalValue;
15
+ };
16
+ };
17
+ needs: {};
18
+ };
19
+ } | {
20
+ producer: {
21
+ module: {
22
+ readonly name: "@hypit/caption-fine";
23
+ readonly version: "1";
24
+ };
25
+ name: string;
26
+ };
27
+ handler: ({ inputs }: import("@hypit/hypit/producer").ProducerHandlerContext) => {
28
+ outputs: {
29
+ track: {
30
+ kind: "inline";
31
+ value: import("@hypit/hypit/protocol").CanonicalValue;
32
+ };
33
+ };
34
+ needs: {};
35
+ };
36
+ })[];
37
+ };
@@ -0,0 +1,34 @@
1
+ import { canonicalize } from "@hypit/hypit/protocol";
2
+ import { captionFineProducers } from "./manifest.js";
3
+ import { renderFineCaption } from "./render.js";
4
+ import { scheduleFineCaption } from "./schedule.js";
5
+ function inline(value, subject) {
6
+ if (value?.kind !== "inline")
7
+ throw new Error(`${subject} must be inline`);
8
+ return value.value;
9
+ }
10
+ export const captionFineComponent = {
11
+ producers: [
12
+ {
13
+ producer: captionFineProducers.schedule,
14
+ handler: ({ inputs }) => ({
15
+ outputs: { schedule: { kind: "inline", value: canonicalize(scheduleFineCaption(inline(inputs.timing?.value, "CaptionTiming"), inline(inputs.program?.value, "CaptionProgram"), inline(inputs.document?.value, "CaptionDocument"))) } },
16
+ needs: {},
17
+ }),
18
+ },
19
+ {
20
+ producer: captionFineProducers.render,
21
+ handler: ({ inputs }) => ({
22
+ outputs: { track: { kind: "inline", value: canonicalize(renderFineCaption(inline(inputs.schedule?.value, "FineCaptionSchedule"), inline(inputs.program?.value, "CaptionProgram"), inline(inputs.document?.value, "CaptionDocument"), inline(inputs.timeline?.value, "Timeline"), inline(inputs.within?.value, "SpatialFrame"))) } },
23
+ needs: {},
24
+ }),
25
+ },
26
+ {
27
+ producer: captionFineProducers.renderWithRegions,
28
+ handler: ({ inputs }) => ({
29
+ outputs: { track: { kind: "inline", value: canonicalize(renderFineCaption(inline(inputs.schedule?.value, "FineCaptionSchedule"), inline(inputs.program?.value, "CaptionProgram"), inline(inputs.document?.value, "CaptionDocument"), inline(inputs.timeline?.value, "Timeline"), inline(inputs.within?.value, "SpatialFrame"), inline(inputs.regions?.value, "RegionEvidence"))) } },
30
+ needs: {},
31
+ }),
32
+ },
33
+ ],
34
+ };
@@ -0,0 +1,4 @@
1
+ /** One timing join plus one Style-family render operation, regardless of how many Styles are used. */
2
+ export declare const fineCaptionTrackFragment: import("@hypit/hypit/author").GraphFragment;
3
+ /** The same Caption pipeline with one explicit, external spatial-evidence edge. */
4
+ export declare const fineCaptionRegionEvidenceFragment: import("@hypit/hypit/author").GraphFragment;
@@ -0,0 +1,84 @@
1
+ import { captionTypes } from "@hypit/hypit/caption";
2
+ import { compositionTypes } from "@hypit/hypit/composition";
3
+ import { sealGraphFragment } from "@hypit/hypit/author";
4
+ import { timelineTypes } from "@hypit/hypit/timeline";
5
+ import { regionEvidenceTypes } from "@hypit/hypit/region-evidence";
6
+ import { spatialTypes } from "@hypit/hypit/spatial";
7
+ import { captionFineProducers, captionFineTypes } from "./manifest.js";
8
+ const input = (name) => ({ kind: "fragment-input", name });
9
+ const operation = (id) => ({ kind: "fragment-operation", operation: id });
10
+ /** One timing join plus one Style-family render operation, regardless of how many Styles are used. */
11
+ export const fineCaptionTrackFragment = sealGraphFragment({
12
+ inputs: [
13
+ { name: "document", type: captionTypes.document },
14
+ { name: "timeline", type: timelineTypes.timeline },
15
+ { name: "within", type: spatialTypes.frame },
16
+ { name: "timing", type: captionTypes.timing },
17
+ { name: "program", type: captionTypes.program },
18
+ ],
19
+ operations: [
20
+ {
21
+ id: "caption-fine:schedule",
22
+ producer: captionFineProducers.schedule,
23
+ inputs: {
24
+ timing: input("timing"), program: input("program"), document: input("document"),
25
+ },
26
+ result: { kind: "output", name: "schedule" },
27
+ },
28
+ {
29
+ id: "caption-fine:render",
30
+ producer: captionFineProducers.render,
31
+ inputs: {
32
+ schedule: operation("caption-fine:schedule"), program: input("program"),
33
+ document: input("document"), timeline: input("timeline"), within: input("within"),
34
+ },
35
+ result: { kind: "output", name: "track" },
36
+ },
37
+ ],
38
+ exports: [
39
+ {
40
+ name: "schedule",
41
+ type: captionFineTypes.schedule,
42
+ root: operation("caption-fine:schedule"),
43
+ },
44
+ {
45
+ name: "visual",
46
+ type: compositionTypes.visualTrack,
47
+ root: operation("caption-fine:render"),
48
+ },
49
+ ],
50
+ });
51
+ /** The same Caption pipeline with one explicit, external spatial-evidence edge. */
52
+ export const fineCaptionRegionEvidenceFragment = sealGraphFragment({
53
+ inputs: [
54
+ { name: "document", type: captionTypes.document },
55
+ { name: "timeline", type: timelineTypes.timeline },
56
+ { name: "within", type: spatialTypes.frame },
57
+ { name: "timing", type: captionTypes.timing },
58
+ { name: "program", type: captionTypes.program },
59
+ { name: "regions", type: regionEvidenceTypes.evidence },
60
+ ],
61
+ operations: [
62
+ {
63
+ id: "caption-fine:schedule",
64
+ producer: captionFineProducers.schedule,
65
+ inputs: {
66
+ timing: input("timing"), program: input("program"), document: input("document"),
67
+ },
68
+ result: { kind: "output", name: "schedule" },
69
+ },
70
+ {
71
+ id: "caption-fine:render",
72
+ producer: captionFineProducers.renderWithRegions,
73
+ inputs: {
74
+ schedule: operation("caption-fine:schedule"), program: input("program"),
75
+ document: input("document"), timeline: input("timeline"), within: input("within"), regions: input("regions"),
76
+ },
77
+ result: { kind: "output", name: "track" },
78
+ },
79
+ ],
80
+ exports: [
81
+ { name: "schedule", type: captionFineTypes.schedule, root: operation("caption-fine:schedule") },
82
+ { name: "visual", type: compositionTypes.visualTrack, root: operation("caption-fine:render") },
83
+ ],
84
+ });
@@ -0,0 +1,10 @@
1
+ export { captionFineComponent } from "./component.js";
2
+ export { fineCaptionRegionEvidenceFragment, fineCaptionTrackFragment } from "./fragment.js";
3
+ export { captionFineManifest, captionFineMarkupSurfaces, captionFineModuleRef, captionFineProducers, captionFineTypes } from "./manifest.js";
4
+ export { renderFineCaption } from "./render.js";
5
+ export { assertFineCaptionSchedule, scheduleFineCaption } from "./schedule.js";
6
+ export { fineCaptionOneShotMotions, fineCaptionOptionalRecipeProperties, fineCaptionRecipeSchema, fineCaptionRequiredRecipeProperties, } from "./recipe.js";
7
+ export { assertFineCaptionParameters, FINE_CAPTION_FAMILY, fineCaptionParameters, fineCaptionStyle, } from "./style.js";
8
+ export { decodeFineCaptionStyleSurface, decodeFineCaptionTrackSurface } from "./surface.js";
9
+ export type * from "./types.js";
10
+ export { fineCaptionEditableDefaults } from "./recipe.js";
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ export { captionFineComponent } from "./component.js";
2
+ export { fineCaptionRegionEvidenceFragment, fineCaptionTrackFragment } from "./fragment.js";
3
+ export { captionFineManifest, captionFineMarkupSurfaces, captionFineModuleRef, captionFineProducers, captionFineTypes } from "./manifest.js";
4
+ export { renderFineCaption } from "./render.js";
5
+ export { assertFineCaptionSchedule, scheduleFineCaption } from "./schedule.js";
6
+ export { fineCaptionOneShotMotions, fineCaptionOptionalRecipeProperties, fineCaptionRecipeSchema, fineCaptionRequiredRecipeProperties, } from "./recipe.js";
7
+ export { assertFineCaptionParameters, FINE_CAPTION_FAMILY, fineCaptionParameters, fineCaptionStyle, } from "./style.js";
8
+ export { decodeFineCaptionStyleSurface, decodeFineCaptionTrackSurface } from "./surface.js";
9
+ export { fineCaptionEditableDefaults } from "./recipe.js";
@@ -0,0 +1,6 @@
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 declare const joinedBoxSetup: string;
@@ -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
+ `;