@bendyline/squisq 1.4.1 → 1.5.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/README.md +14 -1
- package/dist/{ContentContainer-DNx460y8.d.ts → ContentContainer-BXUlIf18.d.ts} +6 -2
- package/dist/{Doc-BeSeH2iX.d.ts → Doc-BBVGq1_9.d.ts} +922 -273
- package/dist/{ImageEditDoc-BTIvw0oq.d.ts → ImageEditDoc-FuyGtt6o.d.ts} +11 -4
- package/dist/{chunk-ZTX4PFFT.js → chunk-2OIBZYKP.js} +75 -2
- package/dist/chunk-2OIBZYKP.js.map +1 -0
- package/dist/chunk-2UQOHYE2.js +45 -0
- package/dist/chunk-2UQOHYE2.js.map +1 -0
- package/dist/chunk-6ATE2PSM.js +344 -0
- package/dist/chunk-6ATE2PSM.js.map +1 -0
- package/dist/{chunk-57CVQCFX.js → chunk-A4O7GIWE.js} +74 -23
- package/dist/chunk-A4O7GIWE.js.map +1 -0
- package/dist/{chunk-5WFRKURX.js → chunk-DLXZMS5K.js} +4 -2
- package/dist/chunk-DLXZMS5K.js.map +1 -0
- package/dist/chunk-ET53IIEP.js +1 -0
- package/dist/chunk-ET53IIEP.js.map +1 -0
- package/dist/chunk-HWVFJAAH.js +362 -0
- package/dist/chunk-HWVFJAAH.js.map +1 -0
- package/dist/{chunk-2TMAWB4Y.js → chunk-IMSCRLLV.js} +1 -1
- package/dist/chunk-IMSCRLLV.js.map +1 -0
- package/dist/{chunk-B4EEON3N.js → chunk-KKNUBQ6Y.js} +536 -103
- package/dist/chunk-KKNUBQ6Y.js.map +1 -0
- package/dist/chunk-LDQ2HJIX.js +596 -0
- package/dist/chunk-LDQ2HJIX.js.map +1 -0
- package/dist/chunk-LH7I6SH7.js +7208 -0
- package/dist/chunk-LH7I6SH7.js.map +1 -0
- package/dist/chunk-PGGGKYWG.js +1 -0
- package/dist/chunk-PGGGKYWG.js.map +1 -0
- package/dist/{chunk-7UDSRZKG.js → chunk-Q3ROPT5H.js} +35 -14
- package/dist/chunk-Q3ROPT5H.js.map +1 -0
- package/dist/chunk-QE7TWGVT.js +178 -0
- package/dist/chunk-QE7TWGVT.js.map +1 -0
- package/dist/chunk-RUDYOTA6.js +345 -0
- package/dist/chunk-RUDYOTA6.js.map +1 -0
- package/dist/{chunk-K32VJONL.js → chunk-T5UK6YOB.js} +41 -2
- package/dist/chunk-T5UK6YOB.js.map +1 -0
- package/dist/{chunk-3K5OG6KI.js → chunk-TEEEILMP.js} +412 -186
- package/dist/chunk-TEEEILMP.js.map +1 -0
- package/dist/{chunk-FR2RBTKO.js → chunk-TQWLI6S2.js} +93 -27
- package/dist/chunk-TQWLI6S2.js.map +1 -0
- package/dist/{chunk-WMOI7FN3.js → chunk-UY7KGQ5R.js} +12 -2
- package/dist/{chunk-WMOI7FN3.js.map → chunk-UY7KGQ5R.js.map} +1 -1
- package/dist/{chunk-3E5F2XMR.js → chunk-VWUFZ6ZG.js} +29 -2
- package/dist/chunk-VWUFZ6ZG.js.map +1 -0
- package/dist/chunk-ZLQKNOZQ.js +1 -0
- package/dist/chunk-ZLQKNOZQ.js.map +1 -0
- package/dist/doc/index.d.ts +5 -5
- package/dist/doc/index.js +135 -8
- package/dist/generate/index.d.ts +3 -74
- package/dist/generate/index.js +2 -7
- package/dist/icons/index.d.ts +3 -26
- package/dist/icons/index.js +13 -0
- package/dist/icons/inlineIconMarker.d.ts +1 -0
- package/dist/icons/inlineIconMarker.js +15 -0
- package/dist/icons/inlineIconMarker.js.map +1 -0
- package/dist/imageEdit/index.d.ts +12 -6
- package/dist/imageEdit/index.js +4 -1
- package/dist/index.d.ts +14 -11
- package/dist/index.js +265 -37
- package/dist/inlineIconMarker-CiE8Hlvz.d.ts +59 -0
- package/dist/jsonForm/index.d.ts +42 -1
- package/dist/jsonForm/index.js +9 -1
- package/dist/markdown/index.d.ts +228 -3
- package/dist/markdown/index.js +42 -7
- package/dist/recommend/index.d.ts +1 -1
- package/dist/recommend/index.js +3 -2
- package/dist/schemas/index.d.ts +28 -8
- package/dist/schemas/index.js +36 -8
- package/dist/spatial/index.d.ts +9 -4
- package/dist/spatial/index.js +1 -1
- package/dist/storage/index.d.ts +2 -2
- package/dist/storage/index.js +1 -1
- package/dist/story/index.d.ts +1021 -41
- package/dist/story/index.js +135 -8
- package/dist/{themeLibrary-BHjJ0Qx6.d.ts → themeLibrary-CehcJhzz.d.ts} +1 -1
- package/dist/timing/index.js +3 -5
- package/dist/transform/index.d.ts +48 -10
- package/dist/transform/index.js +17 -481
- package/dist/transform/index.js.map +1 -1
- package/dist/{types-BdZkdJ3z.d.ts → types-DlAZ7MW4.d.ts} +115 -1
- package/dist/versions/index.d.ts +1 -1
- package/package.json +6 -1
- package/src/__tests__/animationUtils.test.ts +40 -0
- package/src/__tests__/annotationCoercion.test.ts +243 -0
- package/src/__tests__/applyRenderStyle.test.ts +146 -0
- package/src/__tests__/atmosphereLayers.test.ts +108 -0
- package/src/__tests__/attrTokens.test.ts +162 -0
- package/src/__tests__/contentExtractor.test.ts +60 -0
- package/src/__tests__/coverBlock.test.ts +46 -1
- package/src/__tests__/customTemplateSample.test.ts +144 -0
- package/src/__tests__/customTemplatesFrontmatter.test.ts +236 -0
- package/src/__tests__/customThemesFrontmatter.test.ts +145 -0
- package/src/__tests__/diagramBlock.test.ts +122 -0
- package/src/__tests__/diagramLayout.test.ts +134 -0
- package/src/__tests__/drawing.test.ts +109 -0
- package/src/__tests__/drawingBlock.test.ts +124 -0
- package/src/__tests__/drawingLayout.test.ts +171 -0
- package/src/__tests__/geohash.test.ts +6 -5
- package/src/__tests__/getLayers.test.ts +6 -2
- package/src/__tests__/getLayersFallback.test.ts +103 -0
- package/src/__tests__/imageEditExportSvg.test.ts +70 -0
- package/src/__tests__/imageEditVersions.test.ts +72 -0
- package/src/__tests__/inlineIconMarker.test.ts +36 -0
- package/src/__tests__/inputDescriptors.test.ts +162 -0
- package/src/__tests__/jsonForm.tokens.test.ts +84 -0
- package/src/__tests__/layoutBlock.test.ts +83 -0
- package/src/__tests__/layoutLayout.test.ts +169 -0
- package/src/__tests__/markdown.test.ts +87 -0
- package/src/__tests__/markdownSanitize.test.ts +79 -0
- package/src/__tests__/markdownToDoc.test.ts +391 -2
- package/src/__tests__/mediaAnnotations.test.ts +91 -0
- package/src/__tests__/mediaSchedule.test.ts +118 -0
- package/src/__tests__/pandocAttrParse.test.ts +187 -0
- package/src/__tests__/resolveDocTheme.test.ts +55 -0
- package/src/__tests__/rootExports.test.ts +43 -0
- package/src/__tests__/shapeGeometry.test.ts +89 -0
- package/src/__tests__/shapeVocabulary.test.ts +133 -0
- package/src/__tests__/structuredData.test.ts +249 -0
- package/src/__tests__/templateAnnotationParse.test.ts +108 -0
- package/src/__tests__/templateMetadata.test.ts +31 -0
- package/src/__tests__/templates.test.ts +257 -4
- package/src/__tests__/themeCompile.test.ts +42 -0
- package/src/__tests__/themeValidator.test.ts +6 -0
- package/src/__tests__/transformV2.test.ts +137 -0
- package/src/__tests__/transitionNormalize.test.ts +61 -0
- package/src/__tests__/validateDoc.test.ts +260 -0
- package/src/doc/annotationBlocks.ts +117 -0
- package/src/doc/customTemplatesFrontmatter.ts +314 -0
- package/src/doc/customThemesFrontmatter.ts +95 -0
- package/src/doc/docToMarkdown.ts +104 -5
- package/src/doc/getLayers.ts +49 -17
- package/src/doc/index.ts +47 -1
- package/src/doc/markdownToDoc.ts +411 -13
- package/src/doc/mediaAnnotations.ts +100 -0
- package/src/doc/resolveDocTheme.ts +55 -0
- package/src/doc/standaloneAnnotation.ts +58 -0
- package/src/doc/structuredData.ts +301 -0
- package/src/doc/templateInputs.ts +240 -0
- package/src/doc/templates/__tests__/customTemplate.test.ts +170 -0
- package/src/doc/templates/__tests__/customTemplateRegistry.test.ts +110 -0
- package/src/doc/templates/accentImage.ts +30 -6
- package/src/doc/templates/captionUtils.ts +23 -0
- package/src/doc/templates/comparisonBar.ts +33 -18
- package/src/doc/templates/coverBlock.ts +35 -5
- package/src/doc/templates/customTemplate.ts +51 -0
- package/src/doc/templates/dataTable.ts +37 -19
- package/src/doc/templates/dateEvent.ts +41 -20
- package/src/doc/templates/definitionCard.ts +26 -18
- package/src/doc/templates/diagramBlock.ts +204 -0
- package/src/doc/templates/diagramLayout.ts +159 -0
- package/src/doc/templates/drawingBlock.ts +325 -0
- package/src/doc/templates/drawingLayout.ts +539 -0
- package/src/doc/templates/factCard.ts +54 -26
- package/src/doc/templates/fallbackBlock.ts +123 -0
- package/src/doc/templates/featureBlock.ts +20 -13
- package/src/doc/templates/fullBleedQuote.ts +18 -8
- package/src/doc/templates/imageWithCaption.ts +30 -15
- package/src/doc/templates/index.ts +225 -46
- package/src/doc/templates/inputDescriptors.ts +432 -0
- package/src/doc/templates/layoutBlock.ts +43 -0
- package/src/doc/templates/layoutLayout.ts +243 -0
- package/src/doc/templates/listBlock.ts +56 -30
- package/src/doc/templates/mapBlock.ts +15 -12
- package/src/doc/templates/metadata.ts +120 -0
- package/src/doc/templates/persistentLayers.ts +129 -11
- package/src/doc/templates/photoGrid.ts +22 -34
- package/src/doc/templates/pullQuote.ts +30 -12
- package/src/doc/templates/quoteBlock.ts +54 -29
- package/src/doc/templates/sectionHeader.ts +25 -11
- package/src/doc/templates/statHighlight.ts +39 -22
- package/src/doc/templates/titleBlock.ts +59 -21
- package/src/doc/templates/tokens/__tests__/resolveTokens.test.ts +487 -0
- package/src/doc/templates/tokens/resolveTokens.ts +453 -0
- package/src/doc/templates/twoColumn.ts +48 -30
- package/src/doc/templates/videoPullQuote.ts +22 -12
- package/src/doc/templates/videoWithCaption.ts +21 -15
- package/src/doc/utils/animationUtils.ts +267 -3
- package/src/doc/utils/applyRenderStyle.ts +113 -0
- package/src/doc/utils/imageTreatment.ts +70 -0
- package/src/doc/utils/nearest.ts +59 -0
- package/src/doc/utils/shapeGeometry.ts +464 -0
- package/src/doc/utils/themeUtils.ts +75 -1
- package/src/doc/validate.ts +575 -0
- package/src/generate/contentExtractor.ts +148 -35
- package/src/generate/index.ts +0 -3
- package/src/icons/index.ts +8 -0
- package/src/icons/inlineIconMarker.ts +73 -0
- package/src/imageEdit/export.ts +85 -2
- package/src/imageEdit/index.ts +1 -0
- package/src/imageEdit/persistence.ts +51 -0
- package/src/index.ts +2 -0
- package/src/jsonForm/index.ts +2 -0
- package/src/jsonForm/tokens.ts +78 -0
- package/src/markdown/annotationCoercion.ts +295 -0
- package/src/markdown/attrTokens.ts +241 -0
- package/src/markdown/convert.ts +229 -26
- package/src/markdown/index.ts +37 -0
- package/src/markdown/sanitize.ts +318 -0
- package/src/markdown/stringify.ts +96 -6
- package/src/markdown/types.ts +66 -0
- package/src/markdown/utils.ts +111 -2
- package/src/recommend/templates.ts +24 -0
- package/src/schemas/BlockTemplates.ts +133 -4
- package/src/schemas/CustomTemplates.ts +126 -0
- package/src/schemas/Doc.ts +387 -65
- package/src/schemas/ImageEditDoc.ts +11 -4
- package/src/schemas/Media.ts +169 -0
- package/src/schemas/Theme.ts +23 -1
- package/src/schemas/Transitions.ts +224 -0
- package/src/schemas/colorUtils.ts +33 -0
- package/src/schemas/fontStacks.ts +5 -2
- package/src/schemas/index.ts +3 -0
- package/src/schemas/themeCompile.ts +24 -5
- package/src/schemas/themeValidator.ts +49 -11
- package/src/schemas/themes/cinematic.json +69 -12
- package/src/schemas/themes/documentary.json +58 -11
- package/src/schemas/themes/gezellig.json +60 -11
- package/src/schemas/themes/magazine.json +54 -9
- package/src/schemas/themes/morning-light.json +52 -9
- package/src/schemas/themes/standard-dark.json +48 -13
- package/src/schemas/themes/standard.json +44 -12
- package/src/schemas/themes/tech-dark.json +65 -12
- package/src/spatial/Geohash.ts +52 -21
- package/src/storage/ContentContainer.ts +6 -2
- package/src/transform/applyTransform.ts +43 -4
- package/src/transform/index.ts +1 -0
- package/src/transform/registry.ts +44 -13
- package/src/transform/styles/dataDriven.ts +2 -1
- package/src/transform/styles/documentary.ts +2 -1
- package/src/transform/styles/magazine.ts +3 -1
- package/src/transform/styles/minimal.ts +2 -1
- package/src/transform/styles/narrative.ts +3 -1
- package/src/transform/templateSelector.ts +87 -11
- package/src/transform/types.ts +35 -2
- package/dist/chunk-2TMAWB4Y.js.map +0 -1
- package/dist/chunk-3E5F2XMR.js.map +0 -1
- package/dist/chunk-3K5OG6KI.js.map +0 -1
- package/dist/chunk-4X3JQXNM.js +0 -35
- package/dist/chunk-4X3JQXNM.js.map +0 -1
- package/dist/chunk-57CVQCFX.js.map +0 -1
- package/dist/chunk-5WFRKURX.js.map +0 -1
- package/dist/chunk-7UDSRZKG.js.map +0 -1
- package/dist/chunk-B4EEON3N.js.map +0 -1
- package/dist/chunk-FR2RBTKO.js.map +0 -1
- package/dist/chunk-H3AXU4MJ.js +0 -179
- package/dist/chunk-H3AXU4MJ.js.map +0 -1
- package/dist/chunk-K32VJONL.js.map +0 -1
- package/dist/chunk-KSWJR66U.js +0 -3706
- package/dist/chunk-KSWJR66U.js.map +0 -1
- package/dist/chunk-MYIH7FWD.js +0 -145
- package/dist/chunk-MYIH7FWD.js.map +0 -1
- package/dist/chunk-ZTX4PFFT.js.map +0 -1
- package/src/__tests__/slideshowGenerator.test.ts +0 -128
- package/src/generate/slideshowGenerator.ts +0 -273
|
@@ -20,12 +20,15 @@ import type {
|
|
|
20
20
|
GradientBackgroundConfig,
|
|
21
21
|
ImageBackgroundConfig,
|
|
22
22
|
PatternBackgroundConfig,
|
|
23
|
+
VignetteConfig,
|
|
24
|
+
AmbientGradientConfig,
|
|
23
25
|
TitleCaptionConfig,
|
|
24
26
|
CornerBrandingConfig,
|
|
25
27
|
ProgressIndicatorConfig,
|
|
26
28
|
} from '../../schemas/BlockTemplates.js';
|
|
27
29
|
import type { Theme } from '../../schemas/Theme.js';
|
|
28
30
|
import { isPersistentLayerTemplate } from '../../schemas/BlockTemplates.js';
|
|
31
|
+
import { oklchDarken, withAlpha } from '../../schemas/colorUtils.js';
|
|
29
32
|
|
|
30
33
|
// ============================================
|
|
31
34
|
// Gradient Presets
|
|
@@ -35,9 +38,11 @@ const GRADIENT_PRESETS: Record<string, string> = {
|
|
|
35
38
|
'dark-vignette':
|
|
36
39
|
'radial-gradient(ellipse at center, rgba(26,32,44,0.8) 0%, rgba(0,0,0,0.95) 100%)',
|
|
37
40
|
'radial-dark': 'radial-gradient(ellipse at center, #1a202c 0%, #000000 100%)',
|
|
38
|
-
|
|
41
|
+
// Warm presets deepen within their own hue — ending in the shared navy
|
|
42
|
+
// made warm themes drift cold halfway down the frame.
|
|
43
|
+
'warm-sunset': 'linear-gradient(135deg, rgba(124,58,48,0.9) 0%, rgba(46,20,16,0.95) 100%)',
|
|
39
44
|
'cool-blue': 'linear-gradient(135deg, rgba(26,54,93,0.9) 0%, rgba(26,32,44,0.95) 100%)',
|
|
40
|
-
'earth-tones': 'linear-gradient(135deg, rgba(68,51,34,0.9) 0%, rgba(
|
|
45
|
+
'earth-tones': 'linear-gradient(135deg, rgba(68,51,34,0.9) 0%, rgba(30,22,15,0.95) 100%)',
|
|
41
46
|
};
|
|
42
47
|
|
|
43
48
|
// ============================================
|
|
@@ -84,7 +89,9 @@ function expandImageBackground(config: ImageBackgroundConfig): Layer[] {
|
|
|
84
89
|
const layers: Layer[] = [];
|
|
85
90
|
const opacity = config.opacity ?? 0.4;
|
|
86
91
|
|
|
87
|
-
// Base image layer
|
|
92
|
+
// Base image layer. `blur` softens the photo into an atmosphere layer
|
|
93
|
+
// (the ImageLayer renderer over-scans blurred images so soft edges never
|
|
94
|
+
// reveal the frame).
|
|
88
95
|
const imageLayer: Layer = {
|
|
89
96
|
type: 'image',
|
|
90
97
|
id: 'persistent-bg-image',
|
|
@@ -92,6 +99,7 @@ function expandImageBackground(config: ImageBackgroundConfig): Layer[] {
|
|
|
92
99
|
src: config.src,
|
|
93
100
|
alt: 'Background',
|
|
94
101
|
fit: 'cover',
|
|
102
|
+
...(config.blur != null && config.blur > 0 ? { blur: config.blur } : {}),
|
|
95
103
|
},
|
|
96
104
|
position: { x: 0, y: 0, width: '100%', height: '100%' },
|
|
97
105
|
animation: config.ambientMotion
|
|
@@ -122,22 +130,79 @@ function expandImageBackground(config: ImageBackgroundConfig): Layer[] {
|
|
|
122
130
|
* Expand a pattern background config to a Layer.
|
|
123
131
|
*/
|
|
124
132
|
function expandPatternBackground(config: PatternBackgroundConfig): Layer {
|
|
125
|
-
|
|
126
|
-
|
|
133
|
+
// 'noise' is film grain: a full-bleed rect whose paint is replaced by a
|
|
134
|
+
// static feTurbulence field clipped to the rect (see ShapeFilter).
|
|
135
|
+
if (config.pattern === 'noise') {
|
|
136
|
+
return {
|
|
137
|
+
type: 'shape',
|
|
138
|
+
id: 'persistent-grain',
|
|
139
|
+
content: {
|
|
140
|
+
shape: 'rect',
|
|
141
|
+
fill: '#000000',
|
|
142
|
+
filter: { type: 'noise', opacity: config.opacity ?? 0.05 },
|
|
143
|
+
},
|
|
144
|
+
position: { x: 0, y: 0, width: '100%', height: '100%' },
|
|
145
|
+
};
|
|
146
|
+
}
|
|
127
147
|
|
|
128
|
-
//
|
|
129
|
-
|
|
148
|
+
// dots / grid / diagonal render as native SVG <pattern> fills.
|
|
149
|
+
const opacity = config.opacity ?? 0.06;
|
|
130
150
|
return {
|
|
131
151
|
type: 'shape',
|
|
132
152
|
id: 'persistent-bg-pattern',
|
|
133
153
|
content: {
|
|
134
154
|
shape: 'rect',
|
|
135
|
-
|
|
155
|
+
pattern: {
|
|
156
|
+
kind: config.pattern,
|
|
157
|
+
color: config.color ?? '#ffffff',
|
|
158
|
+
size: Math.round(24 * (config.scale ?? 1)),
|
|
159
|
+
opacity,
|
|
160
|
+
},
|
|
136
161
|
},
|
|
137
162
|
position: { x: 0, y: 0, width: '100%', height: '100%' },
|
|
138
163
|
};
|
|
139
164
|
}
|
|
140
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Expand a vignette config: transparent center darkening toward the frame
|
|
168
|
+
* edges. Rides the CSS-gradient rect path in the renderer.
|
|
169
|
+
*/
|
|
170
|
+
function expandVignette(config: VignetteConfig): Layer {
|
|
171
|
+
const strength = Math.max(0, Math.min(1, config.strength ?? 0.3));
|
|
172
|
+
const color = config.color ?? '#000000';
|
|
173
|
+
const edge = withAlpha(color, strength);
|
|
174
|
+
const clear = withAlpha(color, 0);
|
|
175
|
+
return {
|
|
176
|
+
type: 'shape',
|
|
177
|
+
id: 'persistent-vignette',
|
|
178
|
+
content: {
|
|
179
|
+
shape: 'rect',
|
|
180
|
+
fill: `radial-gradient(ellipse at center, ${clear} 55%, ${edge} 100%)`,
|
|
181
|
+
},
|
|
182
|
+
position: { x: 0, y: 0, width: '100%', height: '100%' },
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Expand an ambient drifting gradient: an oversized surface gradient with
|
|
188
|
+
* a very slow Ken Burns loop. Deterministic (no randomness).
|
|
189
|
+
*/
|
|
190
|
+
function expandAmbientGradient(config: AmbientGradientConfig, theme?: Theme): Layer {
|
|
191
|
+
const from = config.from ?? theme?.colors.backgroundLight ?? '#1a202c';
|
|
192
|
+
const to = config.to ?? (theme ? oklchDarken(theme.colors.background, 0.04) : '#0f141d');
|
|
193
|
+
return {
|
|
194
|
+
type: 'shape',
|
|
195
|
+
id: 'persistent-ambient-gradient',
|
|
196
|
+
content: {
|
|
197
|
+
shape: 'rect',
|
|
198
|
+
fill: `linear-gradient(150deg, ${from} 0%, ${to} 100%)`,
|
|
199
|
+
},
|
|
200
|
+
// Oversized so the slow pan never reveals the frame edge.
|
|
201
|
+
position: { x: '-5%', y: '-5%', width: '110%', height: '110%' },
|
|
202
|
+
animation: { type: 'slowZoom', panDirection: 'left', duration: config.duration ?? 40 },
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
141
206
|
// ============================================
|
|
142
207
|
// Overlay Layer Expansion
|
|
143
208
|
// ============================================
|
|
@@ -349,7 +414,7 @@ function expandProgressIndicator(config: ProgressIndicatorConfig): Layer {
|
|
|
349
414
|
/**
|
|
350
415
|
* Expand a single persistent layer (template or raw) to raw Layer(s).
|
|
351
416
|
*/
|
|
352
|
-
export function expandPersistentLayer(layer: PersistentLayer): Layer[] {
|
|
417
|
+
export function expandPersistentLayer(layer: PersistentLayer, theme?: Theme): Layer[] {
|
|
353
418
|
// If already a raw Layer, return as-is
|
|
354
419
|
if (!isPersistentLayerTemplate(layer)) {
|
|
355
420
|
return [layer as Layer];
|
|
@@ -367,6 +432,10 @@ export function expandPersistentLayer(layer: PersistentLayer): Layer[] {
|
|
|
367
432
|
return expandImageBackground(config);
|
|
368
433
|
case 'patternBackground':
|
|
369
434
|
return [expandPatternBackground(config)];
|
|
435
|
+
case 'vignette':
|
|
436
|
+
return [expandVignette(config)];
|
|
437
|
+
case 'ambientGradient':
|
|
438
|
+
return [expandAmbientGradient(config, theme)];
|
|
370
439
|
case 'titleCaption':
|
|
371
440
|
return expandTitleCaption(config);
|
|
372
441
|
case 'cornerBranding':
|
|
@@ -382,13 +451,19 @@ export function expandPersistentLayer(layer: PersistentLayer): Layer[] {
|
|
|
382
451
|
/**
|
|
383
452
|
* Expand all persistent layers in a config to raw Layer arrays.
|
|
384
453
|
*/
|
|
385
|
-
export function expandPersistentLayers(
|
|
454
|
+
export function expandPersistentLayers(
|
|
455
|
+
layers: PersistentLayer[] | undefined,
|
|
456
|
+
theme?: Theme,
|
|
457
|
+
): Layer[] {
|
|
386
458
|
if (!layers || layers.length === 0) {
|
|
387
459
|
return [];
|
|
388
460
|
}
|
|
389
461
|
|
|
390
462
|
// Use reduce+concat instead of flatMap for Coherent GT compatibility (ES2017)
|
|
391
|
-
return layers.reduce<Layer[]>(
|
|
463
|
+
return layers.reduce<Layer[]>(
|
|
464
|
+
(acc, layer) => acc.concat(expandPersistentLayer(layer, theme)),
|
|
465
|
+
[],
|
|
466
|
+
);
|
|
392
467
|
}
|
|
393
468
|
|
|
394
469
|
// ============================================
|
|
@@ -520,3 +595,46 @@ export function getDocStyleConfig(
|
|
|
520
595
|
export function getPersistentLayersFromTheme(theme: Theme): PersistentLayerConfig {
|
|
521
596
|
return theme.persistentLayers ?? {};
|
|
522
597
|
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Resolve the effective persistent-layer config for a doc: the doc's own
|
|
601
|
+
* config wins **wholesale** when present; only docs with no persistent
|
|
602
|
+
* layers inherit the theme's.
|
|
603
|
+
*
|
|
604
|
+
* Doc-wins-wholesale (rather than merging) protects pre-baked documents
|
|
605
|
+
* that carry their own background/branding layers — merging a theme's
|
|
606
|
+
* atmosphere on top of those would double up backgrounds and stack
|
|
607
|
+
* overlays the doc author never saw.
|
|
608
|
+
*/
|
|
609
|
+
export function resolvePersistentLayers(
|
|
610
|
+
doc: { persistentLayers?: PersistentLayerConfig },
|
|
611
|
+
theme: Theme,
|
|
612
|
+
): PersistentLayerConfig | undefined {
|
|
613
|
+
const docConfig = doc.persistentLayers;
|
|
614
|
+
const hasDocLayers =
|
|
615
|
+
!!docConfig &&
|
|
616
|
+
((docConfig.bottomLayers?.length ?? 0) > 0 || (docConfig.topLayers?.length ?? 0) > 0);
|
|
617
|
+
if (hasDocLayers) return docConfig;
|
|
618
|
+
const themeConfig = theme.persistentLayers;
|
|
619
|
+
const hasThemeLayers =
|
|
620
|
+
!!themeConfig &&
|
|
621
|
+
((themeConfig.bottomLayers?.length ?? 0) > 0 || (themeConfig.topLayers?.length ?? 0) > 0);
|
|
622
|
+
return hasThemeLayers ? themeConfig : docConfig;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Compose a block's layers between pre-expanded persistent bottom/top
|
|
627
|
+
* layers, honoring the per-block `useBottomLayer` / `useTopLayer` opt-outs.
|
|
628
|
+
* Single shared implementation for `expandDocBlocks` and `getLayers`.
|
|
629
|
+
*/
|
|
630
|
+
export function wrapWithPersistentLayers(
|
|
631
|
+
layers: Layer[],
|
|
632
|
+
block: { useBottomLayer?: boolean; useTopLayer?: boolean },
|
|
633
|
+
bottomLayers: Layer[],
|
|
634
|
+
topLayers: Layer[],
|
|
635
|
+
): Layer[] {
|
|
636
|
+
if (bottomLayers.length === 0 && topLayers.length === 0) return layers;
|
|
637
|
+
const useBottom = block.useBottomLayer !== false;
|
|
638
|
+
const useTop = block.useTopLayer !== false;
|
|
639
|
+
return [...(useBottom ? bottomLayers : []), ...layers, ...(useTop ? topLayers : [])];
|
|
640
|
+
}
|
|
@@ -17,12 +17,12 @@
|
|
|
17
17
|
|
|
18
18
|
import type { Layer } from '../../schemas/Doc.js';
|
|
19
19
|
import type { PhotoGridInput, TemplateContext } from '../../schemas/BlockTemplates.js';
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
20
|
+
import { getThemeFont, themedFontSize, themedImageTreatment } from '../utils/themeUtils.js';
|
|
21
|
+
import { withAlpha } from '../../schemas/colorUtils.js';
|
|
22
22
|
import { cleanCaption } from './captionUtils.js';
|
|
23
23
|
|
|
24
24
|
/** Gap between images as percentage */
|
|
25
|
-
const GAP =
|
|
25
|
+
const GAP = 1.2;
|
|
26
26
|
|
|
27
27
|
export function photoGrid(input: PhotoGridInput, context: TemplateContext): Layer[] {
|
|
28
28
|
const { images, caption: rawCaption, ambientMotion } = input;
|
|
@@ -33,25 +33,23 @@ export function photoGrid(input: PhotoGridInput, context: TemplateContext): Laye
|
|
|
33
33
|
const caption = rawCaption ? cleanCaption(rawCaption) : rawCaption;
|
|
34
34
|
const { theme, layout } = context;
|
|
35
35
|
|
|
36
|
+
const treatment = themedImageTreatment(context, input.imageTreatment);
|
|
37
|
+
|
|
36
38
|
const layers: Layer[] = [
|
|
37
|
-
//
|
|
39
|
+
// Theme surface background (visible in gaps between images)
|
|
38
40
|
{
|
|
39
41
|
type: 'shape',
|
|
40
42
|
id: 'bg',
|
|
41
|
-
content: { shape: 'rect', fill:
|
|
43
|
+
content: { shape: 'rect', fill: theme.colors.background },
|
|
42
44
|
position: { x: 0, y: 0, width: '100%', height: '100%' },
|
|
43
45
|
},
|
|
44
46
|
];
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
const captionHeight = caption ? (layout.stackColumns ? 18 : 22) : 0;
|
|
48
|
-
const gridHeight = 100 - captionHeight;
|
|
48
|
+
const gridHeight = 100;
|
|
49
49
|
|
|
50
50
|
// Generate image positions based on count; stack vertically in portrait
|
|
51
51
|
const positions = getGridPositions(images.length, gridHeight, layout.stackColumns);
|
|
52
52
|
|
|
53
|
-
const altFontSize = scaledFontSize(24, context, false);
|
|
54
|
-
|
|
55
53
|
for (let i = 0; i < Math.min(images.length, 4); i++) {
|
|
56
54
|
const img = images[i];
|
|
57
55
|
const pos = positions[i];
|
|
@@ -64,24 +62,6 @@ export function photoGrid(input: PhotoGridInput, context: TemplateContext): Laye
|
|
|
64
62
|
position: { x: `${pos.x}%`, y: `${pos.y}%`, width: `${pos.w}%`, height: `${pos.h}%` },
|
|
65
63
|
});
|
|
66
64
|
|
|
67
|
-
// Alt-text label on placeholder
|
|
68
|
-
if (img.alt) {
|
|
69
|
-
layers.push({
|
|
70
|
-
type: 'text',
|
|
71
|
-
id: `grid-alt-${i}`,
|
|
72
|
-
content: {
|
|
73
|
-
text: img.alt,
|
|
74
|
-
style: {
|
|
75
|
-
fontSize: altFontSize,
|
|
76
|
-
fontFamily: getThemeFont(context, 'body'),
|
|
77
|
-
color: theme.colors.textMuted,
|
|
78
|
-
textAlign: 'center' as const,
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
|
-
position: { x: `${pos.x + pos.w / 2}%`, y: `${pos.y + pos.h / 2}%`, anchor: 'center' },
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
65
|
// Image layer (covers placeholder when loaded)
|
|
86
66
|
layers.push({
|
|
87
67
|
type: 'image',
|
|
@@ -92,6 +72,7 @@ export function photoGrid(input: PhotoGridInput, context: TemplateContext): Laye
|
|
|
92
72
|
fit: 'cover',
|
|
93
73
|
credit: img.credit,
|
|
94
74
|
license: img.license,
|
|
75
|
+
...(treatment ? { treatment } : {}),
|
|
95
76
|
},
|
|
96
77
|
position: {
|
|
97
78
|
x: `${pos.x}%`,
|
|
@@ -107,18 +88,23 @@ export function photoGrid(input: PhotoGridInput, context: TemplateContext): Laye
|
|
|
107
88
|
});
|
|
108
89
|
}
|
|
109
90
|
|
|
110
|
-
// Caption
|
|
91
|
+
// Caption in a compact translucent chip above player controls. Long
|
|
92
|
+
// archival/Wikimedia descriptions are intentionally clamped.
|
|
111
93
|
if (caption) {
|
|
112
|
-
const captionFontSize =
|
|
94
|
+
const captionFontSize = themedFontSize(layout.stackColumns ? 24 : 26, context, false);
|
|
95
|
+
const captionY = layout.stackColumns ? '78%' : '74%';
|
|
96
|
+
const captionBgY = layout.stackColumns ? '72%' : '68%';
|
|
97
|
+
const captionHeight = layout.stackColumns ? '13%' : '16%';
|
|
98
|
+
const bg = theme.colors.background;
|
|
113
99
|
|
|
114
100
|
layers.push({
|
|
115
101
|
type: 'shape',
|
|
116
102
|
id: 'caption-bg',
|
|
117
103
|
content: {
|
|
118
104
|
shape: 'rect',
|
|
119
|
-
fill:
|
|
105
|
+
fill: `linear-gradient(90deg, ${withAlpha(bg, 0)}, ${withAlpha(bg, 0.78)} 16%, ${withAlpha(bg, 0.78)} 84%, ${withAlpha(bg, 0)})`,
|
|
120
106
|
},
|
|
121
|
-
position: { x: 0, y:
|
|
107
|
+
position: { x: 0, y: captionBgY, width: '100%', height: captionHeight },
|
|
122
108
|
});
|
|
123
109
|
|
|
124
110
|
layers.push({
|
|
@@ -132,13 +118,15 @@ export function photoGrid(input: PhotoGridInput, context: TemplateContext): Laye
|
|
|
132
118
|
color: theme.colors.text,
|
|
133
119
|
textAlign: 'center',
|
|
134
120
|
shadow: true,
|
|
121
|
+
lineHeight: 1.18,
|
|
122
|
+
maxLines: layout.stackColumns ? 2 : 1,
|
|
135
123
|
},
|
|
136
124
|
},
|
|
137
125
|
position: {
|
|
138
126
|
x: '50%',
|
|
139
|
-
y:
|
|
127
|
+
y: captionY,
|
|
140
128
|
anchor: 'center',
|
|
141
|
-
width: '
|
|
129
|
+
width: '78%',
|
|
142
130
|
},
|
|
143
131
|
animation: { type: 'fadeIn', duration: 1, delay: 0.5 },
|
|
144
132
|
});
|
|
@@ -10,18 +10,28 @@
|
|
|
10
10
|
|
|
11
11
|
import type { Layer } from '../../schemas/Doc.js';
|
|
12
12
|
import type { PullQuoteInput, TemplateContext } from '../../schemas/BlockTemplates.js';
|
|
13
|
-
import {
|
|
14
|
-
|
|
13
|
+
import {
|
|
14
|
+
getThemeFont,
|
|
15
|
+
themedEntrance,
|
|
16
|
+
themedFontSize,
|
|
17
|
+
themedImageTreatment,
|
|
18
|
+
} from '../utils/themeUtils.js';
|
|
19
|
+
import { estimateTextHeight } from './captionUtils.js';
|
|
15
20
|
|
|
16
21
|
export function pullQuote(input: PullQuoteInput, context: TemplateContext): Layer[] {
|
|
17
22
|
const { text, attribution, backgroundImage, ambientMotion } = input;
|
|
23
|
+
const { viewport } = context;
|
|
24
|
+
|
|
25
|
+
const treatment = themedImageTreatment(context, input.imageTreatment);
|
|
18
26
|
|
|
19
27
|
// Guard: backgroundImage is required
|
|
20
28
|
if (!backgroundImage?.src) return [];
|
|
21
29
|
|
|
22
|
-
const quoteFontSize =
|
|
23
|
-
const attrFontSize =
|
|
24
|
-
const decoFontSize =
|
|
30
|
+
const quoteFontSize = themedFontSize(52, context, true);
|
|
31
|
+
const attrFontSize = themedFontSize(26, context, false);
|
|
32
|
+
const decoFontSize = themedFontSize(200, context, true);
|
|
33
|
+
const quoteLineHeight = 1.4;
|
|
34
|
+
const quoteYPct = attribution ? 45 : 50;
|
|
25
35
|
|
|
26
36
|
const layers: Layer[] = [
|
|
27
37
|
// Full-bleed background image
|
|
@@ -34,6 +44,7 @@ export function pullQuote(input: PullQuoteInput, context: TemplateContext): Laye
|
|
|
34
44
|
fit: 'cover',
|
|
35
45
|
credit: backgroundImage.credit,
|
|
36
46
|
license: backgroundImage.license,
|
|
47
|
+
...(treatment ? { treatment } : {}),
|
|
37
48
|
},
|
|
38
49
|
position: { x: 0, y: 0, width: '100%', height: '100%' },
|
|
39
50
|
animation: ambientMotion ? { type: ambientMotion, duration: 15 } : undefined,
|
|
@@ -81,22 +92,29 @@ export function pullQuote(input: PullQuoteInput, context: TemplateContext): Laye
|
|
|
81
92
|
fontFamily: getThemeFont(context, 'title'),
|
|
82
93
|
color: '#ffffff',
|
|
83
94
|
textAlign: 'center',
|
|
84
|
-
lineHeight:
|
|
95
|
+
lineHeight: quoteLineHeight,
|
|
85
96
|
shadow: true,
|
|
86
97
|
},
|
|
87
98
|
},
|
|
88
99
|
position: {
|
|
89
100
|
x: '50%',
|
|
90
|
-
y:
|
|
101
|
+
y: `${quoteYPct}%`,
|
|
91
102
|
anchor: 'center',
|
|
92
|
-
width: '
|
|
103
|
+
width: '72%',
|
|
93
104
|
},
|
|
94
|
-
animation: { type: 'fadeIn', duration: 2 },
|
|
105
|
+
animation: themedEntrance(context, 'text', { type: 'fadeIn', duration: 2 }),
|
|
95
106
|
},
|
|
96
107
|
];
|
|
97
108
|
|
|
98
|
-
// Attribution
|
|
109
|
+
// Attribution \u2014 hangs just below the quote's estimated bottom edge so
|
|
110
|
+
// the two read as one lockup instead of stranding near the bottom.
|
|
99
111
|
if (attribution) {
|
|
112
|
+
const quoteWidthPx = 0.72 * viewport.width;
|
|
113
|
+
const quoteHeightPx = estimateTextHeight(text, quoteFontSize, quoteWidthPx, quoteLineHeight);
|
|
114
|
+
const attrYPct = Math.min(
|
|
115
|
+
82,
|
|
116
|
+
quoteYPct + ((quoteHeightPx / 2 + quoteFontSize * 1.7) / viewport.height) * 100,
|
|
117
|
+
);
|
|
100
118
|
layers.push({
|
|
101
119
|
type: 'text',
|
|
102
120
|
id: 'attribution',
|
|
@@ -105,14 +123,14 @@ export function pullQuote(input: PullQuoteInput, context: TemplateContext): Laye
|
|
|
105
123
|
style: {
|
|
106
124
|
fontSize: attrFontSize,
|
|
107
125
|
fontFamily: getThemeFont(context, 'body'),
|
|
108
|
-
color: 'rgba(255, 255, 255, 0.
|
|
126
|
+
color: 'rgba(255, 255, 255, 0.85)',
|
|
109
127
|
textAlign: 'center',
|
|
110
128
|
shadow: true,
|
|
111
129
|
},
|
|
112
130
|
},
|
|
113
131
|
position: {
|
|
114
132
|
x: '50%',
|
|
115
|
-
y:
|
|
133
|
+
y: `${attrYPct}%`,
|
|
116
134
|
anchor: 'center',
|
|
117
135
|
},
|
|
118
136
|
animation: { type: 'fadeIn', duration: 1, delay: 1.5 },
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Quote Block Template
|
|
3
3
|
*
|
|
4
|
-
* Large centered quote with optional attribution
|
|
5
|
-
*
|
|
4
|
+
* Large centered quote with optional attribution, composed as one
|
|
5
|
+
* vertically-centered lockup: the attribution hangs a fixed distance
|
|
6
|
+
* below the quote's estimated bottom edge instead of being pinned to a
|
|
7
|
+
* far-away layout slot.
|
|
6
8
|
* Adapts font sizes and positioning for different viewports.
|
|
7
9
|
*
|
|
8
10
|
* Supports optional accent images that appear as tasteful side/bottom strips.
|
|
@@ -12,59 +14,72 @@
|
|
|
12
14
|
|
|
13
15
|
import type { Layer } from '../../schemas/Doc.js';
|
|
14
16
|
import type { QuoteBlockInput, TemplateContext } from '../../schemas/BlockTemplates.js';
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
+
import {
|
|
18
|
+
getThemeFont,
|
|
19
|
+
shouldUseShadow,
|
|
20
|
+
themedEntrance,
|
|
21
|
+
themedFontSize,
|
|
22
|
+
themedSurfaceGradient,
|
|
23
|
+
themedImageTreatment,
|
|
24
|
+
} from '../utils/themeUtils.js';
|
|
25
|
+
import { withAlpha } from '../../schemas/colorUtils.js';
|
|
17
26
|
import { createAccentLayers, getAccentLayout, adjustY, DEFAULT_LAYOUT } from './accentImage.js';
|
|
18
|
-
import { createBackgroundLayer } from './captionUtils.js';
|
|
27
|
+
import { createBackgroundLayer, estimateTextHeight } from './captionUtils.js';
|
|
19
28
|
|
|
20
29
|
export function quoteBlock(input: QuoteBlockInput, context: TemplateContext): Layer[] {
|
|
21
30
|
const { quote, attribution, accentImage } = input;
|
|
22
|
-
const { theme,
|
|
31
|
+
const { theme, viewport } = context;
|
|
23
32
|
|
|
24
33
|
// Get layout adjustments if accent image is present
|
|
25
34
|
const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
|
|
26
35
|
|
|
27
36
|
// Scale font sizes for viewport
|
|
28
|
-
const quoteFontSize =
|
|
29
|
-
const attrFontSize =
|
|
37
|
+
const quoteFontSize = themedFontSize(48, context, true);
|
|
38
|
+
const attrFontSize = themedFontSize(26, context, false);
|
|
39
|
+
const quoteLineHeight = Math.max(theme.typography.titleLineHeight ?? 1.4, 1.25);
|
|
30
40
|
|
|
31
41
|
// Decorative quotation mark font size
|
|
32
|
-
const decorativeQuoteFontSize =
|
|
42
|
+
const decorativeQuoteFontSize = themedFontSize(280, context, true);
|
|
33
43
|
|
|
34
|
-
const layers: Layer[] = [
|
|
35
|
-
createBackgroundLayer(
|
|
36
|
-
'bg',
|
|
37
|
-
`linear-gradient(160deg, ${theme.colors.backgroundLight} 0%, #1e2636 100%)`,
|
|
38
|
-
),
|
|
39
|
-
];
|
|
44
|
+
const layers: Layer[] = [createBackgroundLayer('bg', themedSurfaceGradient(context, 160))];
|
|
40
45
|
|
|
41
46
|
// Add accent image layers (behind text, after background)
|
|
42
47
|
if (accentImage) {
|
|
43
|
-
layers.push(
|
|
48
|
+
layers.push(
|
|
49
|
+
...createAccentLayers(
|
|
50
|
+
accentImage,
|
|
51
|
+
input.id,
|
|
52
|
+
themedImageTreatment(context, input.imageTreatment),
|
|
53
|
+
),
|
|
54
|
+
);
|
|
44
55
|
}
|
|
45
56
|
|
|
46
|
-
// Decorative opening quotation mark — oversized, low-opacity behind
|
|
57
|
+
// Decorative opening quotation mark — oversized, low-opacity behind the
|
|
58
|
+
// quote. Derived from the theme text color so it stays a subtle ornament
|
|
59
|
+
// on light and dark surfaces alike (a hard-coded white glyph was
|
|
60
|
+
// invisible on both).
|
|
47
61
|
layers.push({
|
|
48
62
|
type: 'text',
|
|
49
63
|
id: 'deco-quote',
|
|
50
64
|
content: {
|
|
51
|
-
text: '
|
|
65
|
+
text: '“',
|
|
52
66
|
style: {
|
|
53
67
|
fontSize: decorativeQuoteFontSize,
|
|
54
68
|
fontFamily: getThemeFont(context, 'title'),
|
|
55
|
-
color:
|
|
69
|
+
color: withAlpha(theme.colors.text, 0.09),
|
|
56
70
|
textAlign: 'center',
|
|
57
71
|
},
|
|
58
72
|
},
|
|
59
73
|
position: {
|
|
60
74
|
x: accentLayout.textCenterX,
|
|
61
|
-
y: adjustY('
|
|
75
|
+
y: adjustY('26%', accentLayout),
|
|
62
76
|
anchor: 'center',
|
|
63
77
|
},
|
|
64
78
|
});
|
|
65
79
|
|
|
66
|
-
// Quote text
|
|
67
|
-
|
|
80
|
+
// Quote text — optically centered, nudged up slightly when an
|
|
81
|
+
// attribution hangs below it.
|
|
82
|
+
const quoteYPct = attribution ? 45 : 50;
|
|
68
83
|
layers.push({
|
|
69
84
|
type: 'text',
|
|
70
85
|
id: 'quote',
|
|
@@ -75,21 +90,31 @@ export function quoteBlock(input: QuoteBlockInput, context: TemplateContext): La
|
|
|
75
90
|
fontFamily: getThemeFont(context, 'title'),
|
|
76
91
|
color: theme.colors.text,
|
|
77
92
|
textAlign: 'center',
|
|
78
|
-
lineHeight:
|
|
79
|
-
shadow:
|
|
93
|
+
lineHeight: quoteLineHeight,
|
|
94
|
+
shadow: shouldUseShadow(context),
|
|
80
95
|
},
|
|
81
96
|
},
|
|
82
97
|
position: {
|
|
83
98
|
x: accentLayout.textCenterX,
|
|
84
|
-
y: adjustY(
|
|
99
|
+
y: adjustY(`${quoteYPct}%`, accentLayout),
|
|
85
100
|
anchor: 'center',
|
|
86
101
|
width: accentLayout.textWidth,
|
|
87
102
|
},
|
|
88
|
-
animation: { type: 'fadeIn', duration: 2 },
|
|
103
|
+
animation: themedEntrance(context, 'text', { type: 'fadeIn', duration: 2 }),
|
|
89
104
|
});
|
|
90
105
|
|
|
91
|
-
//
|
|
106
|
+
// Attribution hangs just below the quote's estimated bottom edge, so
|
|
107
|
+
// the two read as one lockup instead of the attribution stranding near
|
|
108
|
+
// the bottom of the block.
|
|
92
109
|
if (attribution) {
|
|
110
|
+
const quoteWidthPx = (parseFloat(accentLayout.textWidth) / 100) * viewport.width;
|
|
111
|
+
const quoteHeightPx = estimateTextHeight(quote, quoteFontSize, quoteWidthPx, quoteLineHeight);
|
|
112
|
+
const gapPx = quoteFontSize * 1.7;
|
|
113
|
+
const attrYPct = Math.min(
|
|
114
|
+
82,
|
|
115
|
+
quoteYPct + ((quoteHeightPx / 2 + gapPx) / viewport.height) * 100,
|
|
116
|
+
);
|
|
117
|
+
|
|
93
118
|
layers.push({
|
|
94
119
|
type: 'text',
|
|
95
120
|
id: 'attribution',
|
|
@@ -100,12 +125,12 @@ export function quoteBlock(input: QuoteBlockInput, context: TemplateContext): La
|
|
|
100
125
|
fontFamily: getThemeFont(context, 'body'),
|
|
101
126
|
color: theme.colors.textMuted,
|
|
102
127
|
textAlign: 'center',
|
|
103
|
-
shadow:
|
|
128
|
+
shadow: shouldUseShadow(context),
|
|
104
129
|
},
|
|
105
130
|
},
|
|
106
131
|
position: {
|
|
107
132
|
x: accentLayout.textCenterX,
|
|
108
|
-
y: adjustY(
|
|
133
|
+
y: adjustY(`${attrYPct}%`, accentLayout),
|
|
109
134
|
anchor: 'center',
|
|
110
135
|
},
|
|
111
136
|
animation: { type: 'fadeIn', duration: 1, delay: 1.5 },
|