@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 +26 -0
- package/README.md +154 -2
- package/dist/activation.d.ts +8 -0
- package/dist/activation.js +30 -0
- package/dist/component.d.ts +37 -0
- package/dist/component.js +34 -0
- package/dist/fragment.d.ts +4 -0
- package/dist/fragment.js +84 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +9 -0
- package/dist/joined-box.d.ts +6 -0
- package/dist/joined-box.js +114 -0
- package/dist/manifest.d.ts +854 -0
- package/dist/manifest.js +387 -0
- package/dist/recipe.d.ts +18 -0
- package/dist/recipe.js +116 -0
- package/dist/render.d.ts +8 -0
- package/dist/render.js +1043 -0
- package/dist/schedule.d.ts +12 -0
- package/dist/schedule.js +135 -0
- package/dist/spacing.d.ts +7 -0
- package/dist/spacing.js +13 -0
- package/dist/studio.d.ts +7 -0
- package/dist/studio.js +175 -0
- package/dist/style.d.ts +8 -0
- package/dist/style.js +340 -0
- package/dist/surface.d.ts +3 -0
- package/dist/surface.js +171 -0
- package/dist/types.d.ts +163 -0
- package/dist/types.js +1 -0
- package/package.json +22 -4
- package/preview/Track.png +0 -0
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
|
-
#
|
|
1
|
+
# `@hypit/caption-fine`
|
|
2
2
|
|
|
3
|
-
|
|
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,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;
|
package/dist/fragment.js
ADDED
|
@@ -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
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
`;
|