@bendyline/squisq 0.1.1
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/dist/Types-sh2VRxfo.d.ts +15 -0
- package/dist/chunk-7UDSRZKG.js +103 -0
- package/dist/chunk-7UDSRZKG.js.map +1 -0
- package/dist/chunk-O5HCIUAD.js +577 -0
- package/dist/chunk-O5HCIUAD.js.map +1 -0
- package/dist/chunk-PJ7AODIM.js +3040 -0
- package/dist/chunk-PJ7AODIM.js.map +1 -0
- package/dist/chunk-QWVRN6I4.js +81 -0
- package/dist/chunk-QWVRN6I4.js.map +1 -0
- package/dist/chunk-S4F2RY37.js +92 -0
- package/dist/chunk-S4F2RY37.js.map +1 -0
- package/dist/chunk-URU6QMLY.js +888 -0
- package/dist/chunk-URU6QMLY.js.map +1 -0
- package/dist/chunk-VJN7UB2Z.js +145 -0
- package/dist/chunk-VJN7UB2Z.js.map +1 -0
- package/dist/doc/index.d.ts +3 -0
- package/dist/doc/index.js +126 -0
- package/dist/doc/index.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +217 -0
- package/dist/index.js.map +1 -0
- package/dist/markdown/index.d.ts +237 -0
- package/dist/markdown/index.js +33 -0
- package/dist/markdown/index.js.map +1 -0
- package/dist/schemas/index.d.ts +70 -0
- package/dist/schemas/index.js +61 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/spatial/index.d.ts +111 -0
- package/dist/spatial/index.js +25 -0
- package/dist/spatial/index.js.map +1 -0
- package/dist/storage/index.d.ts +91 -0
- package/dist/storage/index.js +11 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/story/index.d.ts +691 -0
- package/dist/story/index.js +126 -0
- package/dist/story/index.js.map +1 -0
- package/dist/themeLibrary-DySHPcgj.d.ts +1352 -0
- package/dist/types-DHiv_Pnm.d.ts +496 -0
- package/package.json +91 -0
- package/src/__tests__/animationUtils.test.ts +77 -0
- package/src/__tests__/geohash.test.ts +109 -0
- package/src/__tests__/getLayers.test.ts +200 -0
- package/src/__tests__/haversine.test.ts +63 -0
- package/src/__tests__/localForageAdapter.test.ts +155 -0
- package/src/__tests__/markdown.test.ts +806 -0
- package/src/__tests__/markdownToDoc.test.ts +374 -0
- package/src/__tests__/storage.test.ts +60 -0
- package/src/__tests__/templates.test.ts +247 -0
- package/src/doc/docToMarkdown.ts +98 -0
- package/src/doc/getLayers.ts +152 -0
- package/src/doc/index.ts +8 -0
- package/src/doc/markdownToDoc.ts +277 -0
- package/src/doc/templates/accentImage.ts +338 -0
- package/src/doc/templates/captionUtils.ts +33 -0
- package/src/doc/templates/comparisonBar.ts +140 -0
- package/src/doc/templates/coverBlock.ts +156 -0
- package/src/doc/templates/dateEvent.ts +122 -0
- package/src/doc/templates/definitionCard.ts +129 -0
- package/src/doc/templates/factCard.ts +114 -0
- package/src/doc/templates/fullBleedQuote.ts +59 -0
- package/src/doc/templates/imageWithCaption.ts +167 -0
- package/src/doc/templates/index.ts +571 -0
- package/src/doc/templates/listBlock.ts +101 -0
- package/src/doc/templates/mapBlock.ts +132 -0
- package/src/doc/templates/persistentLayers.ts +522 -0
- package/src/doc/templates/photoGrid.ts +199 -0
- package/src/doc/templates/pullQuote.ts +119 -0
- package/src/doc/templates/quoteBlock.ts +112 -0
- package/src/doc/templates/sectionHeader.ts +105 -0
- package/src/doc/templates/statHighlight.ts +113 -0
- package/src/doc/templates/titleBlock.ts +100 -0
- package/src/doc/templates/twoColumn.ts +239 -0
- package/src/doc/templates/videoPullQuote.ts +128 -0
- package/src/doc/templates/videoWithCaption.ts +126 -0
- package/src/doc/utils/animationUtils.ts +135 -0
- package/src/doc/utils/themeUtils.ts +139 -0
- package/src/index.ts +5 -0
- package/src/markdown/convert.ts +897 -0
- package/src/markdown/htmlParse.ts +212 -0
- package/src/markdown/index.ts +110 -0
- package/src/markdown/parse.ts +103 -0
- package/src/markdown/stringify.ts +122 -0
- package/src/markdown/types.ts +605 -0
- package/src/markdown/utils.ts +165 -0
- package/src/ngeohash.d.ts +11 -0
- package/src/schemas/BlockTemplates.ts +706 -0
- package/src/schemas/Doc.ts +565 -0
- package/src/schemas/LayoutStrategy.ts +178 -0
- package/src/schemas/MediaProvider.ts +70 -0
- package/src/schemas/Theme.ts +235 -0
- package/src/schemas/Types.ts +15 -0
- package/src/schemas/Viewport.ts +91 -0
- package/src/schemas/index.ts +14 -0
- package/src/schemas/themeLibrary.ts +492 -0
- package/src/spatial/Geohash.ts +168 -0
- package/src/spatial/Haversine.ts +59 -0
- package/src/spatial/index.ts +2 -0
- package/src/storage/LocalForageAdapter.ts +103 -0
- package/src/storage/LocalStorageAdapter.ts +57 -0
- package/src/storage/MemoryStorageAdapter.ts +37 -0
- package/src/storage/Storage.ts +28 -0
- package/src/storage/index.ts +5 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Title Block Template
|
|
3
|
+
*
|
|
4
|
+
* Large title with optional subtitle for doc intros.
|
|
5
|
+
* Centered text with fade-in animations.
|
|
6
|
+
* Adapts font sizes and positioning for different viewports.
|
|
7
|
+
*
|
|
8
|
+
* This is shared code used by both site and efb-app doc renderers.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Layer } from '../../schemas/Doc.js';
|
|
12
|
+
import type { TitleBlockInput, TemplateContext } from '../../schemas/BlockTemplates.js';
|
|
13
|
+
import { scaledFontSize } from '../../schemas/BlockTemplates.js';
|
|
14
|
+
|
|
15
|
+
export function titleBlock(input: TitleBlockInput, context: TemplateContext): Layer[] {
|
|
16
|
+
const { title, subtitle, backgroundColor } = input;
|
|
17
|
+
const { theme, layout } = context;
|
|
18
|
+
|
|
19
|
+
// Scale font sizes for viewport
|
|
20
|
+
const titleFontSize = scaledFontSize(96, context, true);
|
|
21
|
+
const subtitleFontSize = scaledFontSize(36, context, false);
|
|
22
|
+
|
|
23
|
+
const baseBg = backgroundColor || theme.colors.primary;
|
|
24
|
+
|
|
25
|
+
const layers: Layer[] = [
|
|
26
|
+
// Background — radial gradient for depth instead of flat color
|
|
27
|
+
{
|
|
28
|
+
type: 'shape',
|
|
29
|
+
id: 'bg',
|
|
30
|
+
content: {
|
|
31
|
+
shape: 'rect',
|
|
32
|
+
fill: `radial-gradient(ellipse at 50% 40%, ${baseBg} 0%, ${theme.colors.background} 100%)`,
|
|
33
|
+
},
|
|
34
|
+
position: { x: 0, y: 0, width: '100%', height: '100%' },
|
|
35
|
+
},
|
|
36
|
+
// Subtle decorative line above title
|
|
37
|
+
{
|
|
38
|
+
type: 'shape',
|
|
39
|
+
id: 'accent-line',
|
|
40
|
+
content: {
|
|
41
|
+
shape: 'rect',
|
|
42
|
+
fill: 'rgba(255, 255, 255, 0.2)',
|
|
43
|
+
},
|
|
44
|
+
position: {
|
|
45
|
+
x: '40%',
|
|
46
|
+
y: subtitle ? '28%' : '38%',
|
|
47
|
+
width: '20%',
|
|
48
|
+
height: '2px',
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
// Title
|
|
52
|
+
{
|
|
53
|
+
type: 'text',
|
|
54
|
+
id: 'title',
|
|
55
|
+
content: {
|
|
56
|
+
text: title,
|
|
57
|
+
style: {
|
|
58
|
+
fontSize: titleFontSize,
|
|
59
|
+
fontWeight: 'bold',
|
|
60
|
+
color: theme.colors.text,
|
|
61
|
+
textAlign: 'center',
|
|
62
|
+
shadow: true,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
position: {
|
|
66
|
+
x: '50%',
|
|
67
|
+
y: subtitle ? layout.primaryY : '50%',
|
|
68
|
+
anchor: 'center',
|
|
69
|
+
width: layout.maxTextWidth,
|
|
70
|
+
},
|
|
71
|
+
animation: { type: 'fadeIn', duration: 2 },
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
// Add subtitle if provided
|
|
76
|
+
if (subtitle) {
|
|
77
|
+
layers.push({
|
|
78
|
+
type: 'text',
|
|
79
|
+
id: 'subtitle',
|
|
80
|
+
content: {
|
|
81
|
+
text: subtitle,
|
|
82
|
+
style: {
|
|
83
|
+
fontSize: subtitleFontSize,
|
|
84
|
+
color: theme.colors.textMuted,
|
|
85
|
+
textAlign: 'center',
|
|
86
|
+
lineHeight: 1.5,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
position: {
|
|
90
|
+
x: '50%',
|
|
91
|
+
y: layout.secondaryY,
|
|
92
|
+
anchor: 'center',
|
|
93
|
+
width: layout.maxTextWidth,
|
|
94
|
+
},
|
|
95
|
+
animation: { type: 'fadeIn', duration: 1.5, delay: 1 },
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return layers;
|
|
100
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two Column Template
|
|
3
|
+
*
|
|
4
|
+
* Side-by-side comparison of two items.
|
|
5
|
+
* Each column has a label and optional sublabel.
|
|
6
|
+
* Adapts font sizes and positioning for different viewports.
|
|
7
|
+
*
|
|
8
|
+
* Portrait mode: Stacks columns vertically (top/bottom) instead of side-by-side.
|
|
9
|
+
*
|
|
10
|
+
* This is shared code used by both site and efb-app doc renderers.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { Layer } from '../../schemas/Doc.js';
|
|
14
|
+
import type { TwoColumnInput, TemplateContext } from '../../schemas/BlockTemplates.js';
|
|
15
|
+
import { scaledFontSize } from '../../schemas/BlockTemplates.js';
|
|
16
|
+
import { resolveColorScheme } from '../utils/themeUtils.js';
|
|
17
|
+
import { getTwoColumnPositions } from '../../schemas/LayoutStrategy.js';
|
|
18
|
+
|
|
19
|
+
export function twoColumn(input: TwoColumnInput, context: TemplateContext): Layer[] {
|
|
20
|
+
const { left, right, header, leftColor = 'green', rightColor = 'blue' } = input;
|
|
21
|
+
|
|
22
|
+
// Guard: required fields may be absent when generated from sparse markdown
|
|
23
|
+
if (!left?.label || !right?.label) return [];
|
|
24
|
+
|
|
25
|
+
const { theme, layout, orientation } = context;
|
|
26
|
+
const leftColors = resolveColorScheme(context, leftColor);
|
|
27
|
+
const rightColors = resolveColorScheme(context, rightColor);
|
|
28
|
+
|
|
29
|
+
// Get column positions based on orientation
|
|
30
|
+
const positions = getTwoColumnPositions(orientation);
|
|
31
|
+
const isStacked = layout.stackColumns;
|
|
32
|
+
|
|
33
|
+
// Column width ~42% for side-by-side, ~85% for stacked
|
|
34
|
+
const columnWidth = isStacked ? '85%' : '42%';
|
|
35
|
+
|
|
36
|
+
// Calculate font sizes based on viewport
|
|
37
|
+
const headerFontSize = scaledFontSize(32, context, false);
|
|
38
|
+
const labelFontSize = scaledFontSize(44, context, true);
|
|
39
|
+
const sublabelFontSize = scaledFontSize(22, context, false);
|
|
40
|
+
|
|
41
|
+
const layers: Layer[] = [
|
|
42
|
+
// Background — subtle gradient to add depth
|
|
43
|
+
{
|
|
44
|
+
type: 'shape',
|
|
45
|
+
id: 'bg',
|
|
46
|
+
content: {
|
|
47
|
+
shape: 'rect',
|
|
48
|
+
fill: `linear-gradient(135deg, ${theme.colors.background} 0%, #16202e 100%)`,
|
|
49
|
+
},
|
|
50
|
+
position: { x: 0, y: 0, width: '100%', height: '100%' },
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
if (!isStacked) {
|
|
55
|
+
// Left column background panel
|
|
56
|
+
layers.push({
|
|
57
|
+
type: 'shape',
|
|
58
|
+
id: 'left-panel',
|
|
59
|
+
content: {
|
|
60
|
+
shape: 'rect',
|
|
61
|
+
fill: 'rgba(255, 255, 255, 0.04)',
|
|
62
|
+
},
|
|
63
|
+
position: { x: '3%', y: '15%', width: '44%', height: '70%' },
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Right column background panel
|
|
67
|
+
layers.push({
|
|
68
|
+
type: 'shape',
|
|
69
|
+
id: 'right-panel',
|
|
70
|
+
content: {
|
|
71
|
+
shape: 'rect',
|
|
72
|
+
fill: 'rgba(255, 255, 255, 0.04)',
|
|
73
|
+
},
|
|
74
|
+
position: { x: '53%', y: '15%', width: '44%', height: '70%' },
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Header position adjusts based on layout
|
|
79
|
+
const headerY = isStacked ? '12%' : '22%';
|
|
80
|
+
|
|
81
|
+
// Add header if provided
|
|
82
|
+
if (header) {
|
|
83
|
+
layers.push({
|
|
84
|
+
type: 'text',
|
|
85
|
+
id: 'header',
|
|
86
|
+
content: {
|
|
87
|
+
text: header,
|
|
88
|
+
style: {
|
|
89
|
+
fontSize: headerFontSize,
|
|
90
|
+
color: theme.colors.textMuted,
|
|
91
|
+
textAlign: 'center',
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
position: { x: '50%', y: headerY, anchor: 'center' },
|
|
95
|
+
animation: { type: 'fadeIn', duration: 0.8 },
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Calculate Y positions based on layout mode and header presence
|
|
100
|
+
let leftY: string;
|
|
101
|
+
let leftSublabelY: string;
|
|
102
|
+
let rightY: string;
|
|
103
|
+
let rightSublabelY: string;
|
|
104
|
+
|
|
105
|
+
if (isStacked) {
|
|
106
|
+
// Portrait: stacked vertically
|
|
107
|
+
leftY = header ? '30%' : '25%';
|
|
108
|
+
leftSublabelY = header ? '40%' : '35%';
|
|
109
|
+
rightY = header ? '60%' : '55%';
|
|
110
|
+
rightSublabelY = header ? '70%' : '65%';
|
|
111
|
+
} else {
|
|
112
|
+
// Landscape/square: side by side — vertically centered in panels (15%-85%)
|
|
113
|
+
const baseY = header ? '47%' : '45%';
|
|
114
|
+
const sublabelOffset = header ? '58%' : '56%';
|
|
115
|
+
leftY = baseY;
|
|
116
|
+
leftSublabelY = sublabelOffset;
|
|
117
|
+
rightY = baseY;
|
|
118
|
+
rightSublabelY = sublabelOffset;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Left column (or top in portrait)
|
|
122
|
+
layers.push({
|
|
123
|
+
type: 'text',
|
|
124
|
+
id: 'left-label',
|
|
125
|
+
content: {
|
|
126
|
+
text: left.label,
|
|
127
|
+
style: {
|
|
128
|
+
fontSize: labelFontSize,
|
|
129
|
+
fontWeight: 'bold',
|
|
130
|
+
color: leftColors.text,
|
|
131
|
+
textAlign: 'center',
|
|
132
|
+
lineHeight: 1.3,
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
position: {
|
|
136
|
+
x: positions.left.x,
|
|
137
|
+
y: leftY,
|
|
138
|
+
anchor: 'center',
|
|
139
|
+
width: columnWidth,
|
|
140
|
+
},
|
|
141
|
+
animation: { type: 'fadeIn', duration: 1 },
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (left.sublabel) {
|
|
145
|
+
layers.push({
|
|
146
|
+
type: 'text',
|
|
147
|
+
id: 'left-sublabel',
|
|
148
|
+
content: {
|
|
149
|
+
text: left.sublabel,
|
|
150
|
+
style: {
|
|
151
|
+
fontSize: sublabelFontSize,
|
|
152
|
+
color: theme.colors.textMuted,
|
|
153
|
+
textAlign: 'center',
|
|
154
|
+
lineHeight: 1.4,
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
position: {
|
|
158
|
+
x: positions.left.x,
|
|
159
|
+
y: leftSublabelY,
|
|
160
|
+
anchor: 'center',
|
|
161
|
+
width: columnWidth,
|
|
162
|
+
},
|
|
163
|
+
animation: { type: 'fadeIn', duration: 0.8, delay: 0.3 },
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Visual connector between columns (bold arrow)
|
|
168
|
+
const connectorFontSize = scaledFontSize(120, context, true);
|
|
169
|
+
const connectorY = isStacked ? '48%' : '50%'; // Centered in viewport
|
|
170
|
+
const connectorSymbol = isStacked ? '\u2193' : '\u2192'; // ↓ for stacked, → for side-by-side
|
|
171
|
+
|
|
172
|
+
layers.push({
|
|
173
|
+
type: 'text',
|
|
174
|
+
id: 'connector',
|
|
175
|
+
content: {
|
|
176
|
+
text: connectorSymbol,
|
|
177
|
+
style: {
|
|
178
|
+
fontSize: connectorFontSize,
|
|
179
|
+
fontWeight: 'bold',
|
|
180
|
+
color: 'rgba(255, 255, 255, 0.6)',
|
|
181
|
+
textAlign: 'center',
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
position: {
|
|
185
|
+
x: '50%',
|
|
186
|
+
y: connectorY,
|
|
187
|
+
anchor: 'center',
|
|
188
|
+
},
|
|
189
|
+
animation: { type: 'fadeIn', duration: 0.6, delay: 0.4 },
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Right column (or bottom in portrait)
|
|
193
|
+
layers.push({
|
|
194
|
+
type: 'text',
|
|
195
|
+
id: 'right-label',
|
|
196
|
+
content: {
|
|
197
|
+
text: right.label,
|
|
198
|
+
style: {
|
|
199
|
+
fontSize: labelFontSize,
|
|
200
|
+
fontWeight: 'bold',
|
|
201
|
+
color: rightColors.text,
|
|
202
|
+
textAlign: 'center',
|
|
203
|
+
lineHeight: 1.3,
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
position: {
|
|
207
|
+
x: positions.right.x,
|
|
208
|
+
y: rightY,
|
|
209
|
+
anchor: 'center',
|
|
210
|
+
width: columnWidth,
|
|
211
|
+
},
|
|
212
|
+
animation: { type: 'fadeIn', duration: 1, delay: 0.5 },
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
if (right.sublabel) {
|
|
216
|
+
layers.push({
|
|
217
|
+
type: 'text',
|
|
218
|
+
id: 'right-sublabel',
|
|
219
|
+
content: {
|
|
220
|
+
text: right.sublabel,
|
|
221
|
+
style: {
|
|
222
|
+
fontSize: sublabelFontSize,
|
|
223
|
+
color: theme.colors.textMuted,
|
|
224
|
+
textAlign: 'center',
|
|
225
|
+
lineHeight: 1.4,
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
position: {
|
|
229
|
+
x: positions.right.x,
|
|
230
|
+
y: rightSublabelY,
|
|
231
|
+
anchor: 'center',
|
|
232
|
+
width: columnWidth,
|
|
233
|
+
},
|
|
234
|
+
animation: { type: 'fadeIn', duration: 0.8, delay: 0.8 },
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return layers;
|
|
239
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video Pull Quote Template
|
|
3
|
+
*
|
|
4
|
+
* Quote text over a video clip background with dark overlay.
|
|
5
|
+
* Cinematic alternative to pullQuote when a video clip is available.
|
|
6
|
+
* Combines the visual dynamism of live video with the text focus of quoteBlock.
|
|
7
|
+
*
|
|
8
|
+
* The video plays muted — narration audio is the only sound track.
|
|
9
|
+
*
|
|
10
|
+
* This is shared code used by both site and efb-app doc renderers.
|
|
11
|
+
*
|
|
12
|
+
* Related Files:
|
|
13
|
+
* - shared/story/templates/pullQuote.ts — image-based equivalent
|
|
14
|
+
* - schemas/StoryScript.ts — VideoLayer type
|
|
15
|
+
* - site/src/components/story/layers/VideoLayer.tsx — rendering component
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { Layer } from '../../schemas/Doc.js';
|
|
19
|
+
import type { VideoPullQuoteInput, TemplateContext } from '../../schemas/BlockTemplates.js';
|
|
20
|
+
import { scaledFontSize } from '../../schemas/BlockTemplates.js';
|
|
21
|
+
|
|
22
|
+
export function videoPullQuote(input: VideoPullQuoteInput, context: TemplateContext): Layer[] {
|
|
23
|
+
const { text, attribution, backgroundVideo } = input;
|
|
24
|
+
|
|
25
|
+
// Guard: backgroundVideo is required
|
|
26
|
+
if (!backgroundVideo?.src) return [];
|
|
27
|
+
|
|
28
|
+
const quoteFontSize = scaledFontSize(52, context, true);
|
|
29
|
+
const attrFontSize = scaledFontSize(24, context, false);
|
|
30
|
+
const decoFontSize = scaledFontSize(200, context, true);
|
|
31
|
+
|
|
32
|
+
const layers: Layer[] = [
|
|
33
|
+
// Full-bleed background video clip
|
|
34
|
+
{
|
|
35
|
+
type: 'video',
|
|
36
|
+
id: 'bg-video',
|
|
37
|
+
content: {
|
|
38
|
+
src: backgroundVideo.src,
|
|
39
|
+
posterSrc: backgroundVideo.posterSrc,
|
|
40
|
+
alt: backgroundVideo.alt,
|
|
41
|
+
fit: 'cover',
|
|
42
|
+
clipStart: backgroundVideo.clipStart,
|
|
43
|
+
clipEnd: backgroundVideo.clipEnd,
|
|
44
|
+
credit: backgroundVideo.credit,
|
|
45
|
+
license: backgroundVideo.license,
|
|
46
|
+
},
|
|
47
|
+
position: { x: 0, y: 0, width: '100%', height: '100%' },
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
// Dark overlay for text readability
|
|
51
|
+
{
|
|
52
|
+
type: 'shape',
|
|
53
|
+
id: 'overlay',
|
|
54
|
+
content: {
|
|
55
|
+
shape: 'rect',
|
|
56
|
+
fill: 'rgba(0, 0, 0, 0.55)',
|
|
57
|
+
},
|
|
58
|
+
position: { x: 0, y: 0, width: '100%', height: '100%' },
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
// Decorative quotation mark
|
|
62
|
+
{
|
|
63
|
+
type: 'text',
|
|
64
|
+
id: 'deco-quote',
|
|
65
|
+
content: {
|
|
66
|
+
text: '\u201C',
|
|
67
|
+
style: {
|
|
68
|
+
fontSize: decoFontSize,
|
|
69
|
+
color: 'rgba(255, 255, 255, 0.08)',
|
|
70
|
+
textAlign: 'center',
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
position: {
|
|
74
|
+
x: '50%',
|
|
75
|
+
y: '20%',
|
|
76
|
+
anchor: 'center',
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
// Quote text
|
|
81
|
+
{
|
|
82
|
+
type: 'text',
|
|
83
|
+
id: 'quote-text',
|
|
84
|
+
content: {
|
|
85
|
+
text,
|
|
86
|
+
style: {
|
|
87
|
+
fontSize: quoteFontSize,
|
|
88
|
+
color: '#ffffff',
|
|
89
|
+
textAlign: 'center',
|
|
90
|
+
lineHeight: 1.5,
|
|
91
|
+
shadow: true,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
position: {
|
|
95
|
+
x: '50%',
|
|
96
|
+
y: attribution ? '45%' : '50%',
|
|
97
|
+
anchor: 'center',
|
|
98
|
+
width: '80%',
|
|
99
|
+
},
|
|
100
|
+
animation: { type: 'fadeIn', duration: 2 },
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
// Attribution
|
|
105
|
+
if (attribution) {
|
|
106
|
+
layers.push({
|
|
107
|
+
type: 'text',
|
|
108
|
+
id: 'attribution',
|
|
109
|
+
content: {
|
|
110
|
+
text: `\u2014 ${attribution}`,
|
|
111
|
+
style: {
|
|
112
|
+
fontSize: attrFontSize,
|
|
113
|
+
color: 'rgba(255, 255, 255, 0.7)',
|
|
114
|
+
textAlign: 'center',
|
|
115
|
+
shadow: true,
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
position: {
|
|
119
|
+
x: '50%',
|
|
120
|
+
y: '72%',
|
|
121
|
+
anchor: 'center',
|
|
122
|
+
},
|
|
123
|
+
animation: { type: 'fadeIn', duration: 1, delay: 1.5 },
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return layers;
|
|
128
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video With Caption Template
|
|
3
|
+
*
|
|
4
|
+
* Full-screen background video clip with text overlay. Mirrors the structure of
|
|
5
|
+
* imageWithCaption but uses a VideoLayer instead of an ImageLayer. The video
|
|
6
|
+
* plays muted — narration audio is the only sound track.
|
|
7
|
+
*
|
|
8
|
+
* Adapts caption positioning and font sizes for different viewports.
|
|
9
|
+
*
|
|
10
|
+
* This is shared code used by both site and efb-app doc renderers.
|
|
11
|
+
*
|
|
12
|
+
* Related Files:
|
|
13
|
+
* - shared/story/templates/imageWithCaption.ts — image equivalent
|
|
14
|
+
* - schemas/StoryScript.ts — VideoLayer type
|
|
15
|
+
* - site/src/components/story/layers/VideoLayer.tsx — rendering component
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { Layer } from '../../schemas/Doc.js';
|
|
19
|
+
import type { VideoWithCaptionInput, TemplateContext } from '../../schemas/BlockTemplates.js';
|
|
20
|
+
import { scaledFontSize } from '../../schemas/BlockTemplates.js';
|
|
21
|
+
import { cleanCaption } from './captionUtils.js';
|
|
22
|
+
|
|
23
|
+
export function videoWithCaption(input: VideoWithCaptionInput, context: TemplateContext): Layer[] {
|
|
24
|
+
const {
|
|
25
|
+
videoSrc,
|
|
26
|
+
posterSrc,
|
|
27
|
+
videoAlt,
|
|
28
|
+
clipStart,
|
|
29
|
+
clipEnd,
|
|
30
|
+
sourceDuration,
|
|
31
|
+
caption: rawCaption,
|
|
32
|
+
captionPosition = 'bottom',
|
|
33
|
+
videoCredit,
|
|
34
|
+
videoLicense,
|
|
35
|
+
} = input;
|
|
36
|
+
const caption = rawCaption ? cleanCaption(rawCaption) : rawCaption;
|
|
37
|
+
const { theme, layout } = context;
|
|
38
|
+
|
|
39
|
+
const captionFontSize = scaledFontSize(36, context, false);
|
|
40
|
+
const creditFontSize = scaledFontSize(16, context, false);
|
|
41
|
+
|
|
42
|
+
const layers: Layer[] = [
|
|
43
|
+
// Background video clip
|
|
44
|
+
{
|
|
45
|
+
type: 'video',
|
|
46
|
+
id: 'bg-video',
|
|
47
|
+
content: {
|
|
48
|
+
src: videoSrc,
|
|
49
|
+
posterSrc,
|
|
50
|
+
alt: videoAlt,
|
|
51
|
+
fit: 'cover',
|
|
52
|
+
clipStart,
|
|
53
|
+
clipEnd,
|
|
54
|
+
sourceDuration,
|
|
55
|
+
credit: videoCredit,
|
|
56
|
+
license: videoLicense,
|
|
57
|
+
},
|
|
58
|
+
position: { x: 0, y: 0, width: '100%', height: '100%' },
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
// Caption text at bottom with gradient for readability
|
|
63
|
+
if (caption) {
|
|
64
|
+
layers.push({
|
|
65
|
+
type: 'shape',
|
|
66
|
+
id: 'caption-gradient',
|
|
67
|
+
content: {
|
|
68
|
+
shape: 'rect',
|
|
69
|
+
fill: 'linear-gradient(0deg, rgba(0,0,0,0.7) 0%, rgba(0,0,0,0.3) 60%, transparent 100%)',
|
|
70
|
+
},
|
|
71
|
+
position: {
|
|
72
|
+
x: 0,
|
|
73
|
+
y: captionPosition === 'top' ? 0 : '65%',
|
|
74
|
+
width: '100%',
|
|
75
|
+
height: '35%',
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const captionY =
|
|
80
|
+
captionPosition === 'top' ? '15%' : captionPosition === 'center' ? '50%' : '82%';
|
|
81
|
+
|
|
82
|
+
layers.push({
|
|
83
|
+
type: 'text',
|
|
84
|
+
id: 'caption',
|
|
85
|
+
content: {
|
|
86
|
+
text: caption,
|
|
87
|
+
style: {
|
|
88
|
+
fontSize: captionFontSize,
|
|
89
|
+
color: theme.colors.text,
|
|
90
|
+
textAlign: 'center',
|
|
91
|
+
shadow: true,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
position: {
|
|
95
|
+
x: '50%',
|
|
96
|
+
y: captionY,
|
|
97
|
+
anchor: 'center',
|
|
98
|
+
width: layout.maxTextWidth,
|
|
99
|
+
},
|
|
100
|
+
animation: { type: 'fadeIn', duration: 1.5, delay: 0.5 },
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Small credit text in bottom-right corner
|
|
105
|
+
if (videoCredit) {
|
|
106
|
+
layers.push({
|
|
107
|
+
type: 'text',
|
|
108
|
+
id: 'credit',
|
|
109
|
+
content: {
|
|
110
|
+
text: videoCredit,
|
|
111
|
+
style: {
|
|
112
|
+
fontSize: creditFontSize,
|
|
113
|
+
color: 'rgba(255, 255, 255, 0.5)',
|
|
114
|
+
textAlign: 'right',
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
position: {
|
|
118
|
+
x: '96%',
|
|
119
|
+
y: '96%',
|
|
120
|
+
anchor: 'bottom-right',
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return layers;
|
|
126
|
+
}
|