@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.
Files changed (102) hide show
  1. package/dist/Types-sh2VRxfo.d.ts +15 -0
  2. package/dist/chunk-7UDSRZKG.js +103 -0
  3. package/dist/chunk-7UDSRZKG.js.map +1 -0
  4. package/dist/chunk-O5HCIUAD.js +577 -0
  5. package/dist/chunk-O5HCIUAD.js.map +1 -0
  6. package/dist/chunk-PJ7AODIM.js +3040 -0
  7. package/dist/chunk-PJ7AODIM.js.map +1 -0
  8. package/dist/chunk-QWVRN6I4.js +81 -0
  9. package/dist/chunk-QWVRN6I4.js.map +1 -0
  10. package/dist/chunk-S4F2RY37.js +92 -0
  11. package/dist/chunk-S4F2RY37.js.map +1 -0
  12. package/dist/chunk-URU6QMLY.js +888 -0
  13. package/dist/chunk-URU6QMLY.js.map +1 -0
  14. package/dist/chunk-VJN7UB2Z.js +145 -0
  15. package/dist/chunk-VJN7UB2Z.js.map +1 -0
  16. package/dist/doc/index.d.ts +3 -0
  17. package/dist/doc/index.js +126 -0
  18. package/dist/doc/index.js.map +1 -0
  19. package/dist/index.d.ts +8 -0
  20. package/dist/index.js +217 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/markdown/index.d.ts +237 -0
  23. package/dist/markdown/index.js +33 -0
  24. package/dist/markdown/index.js.map +1 -0
  25. package/dist/schemas/index.d.ts +70 -0
  26. package/dist/schemas/index.js +61 -0
  27. package/dist/schemas/index.js.map +1 -0
  28. package/dist/spatial/index.d.ts +111 -0
  29. package/dist/spatial/index.js +25 -0
  30. package/dist/spatial/index.js.map +1 -0
  31. package/dist/storage/index.d.ts +91 -0
  32. package/dist/storage/index.js +11 -0
  33. package/dist/storage/index.js.map +1 -0
  34. package/dist/story/index.d.ts +691 -0
  35. package/dist/story/index.js +126 -0
  36. package/dist/story/index.js.map +1 -0
  37. package/dist/themeLibrary-DySHPcgj.d.ts +1352 -0
  38. package/dist/types-DHiv_Pnm.d.ts +496 -0
  39. package/package.json +91 -0
  40. package/src/__tests__/animationUtils.test.ts +77 -0
  41. package/src/__tests__/geohash.test.ts +109 -0
  42. package/src/__tests__/getLayers.test.ts +200 -0
  43. package/src/__tests__/haversine.test.ts +63 -0
  44. package/src/__tests__/localForageAdapter.test.ts +155 -0
  45. package/src/__tests__/markdown.test.ts +806 -0
  46. package/src/__tests__/markdownToDoc.test.ts +374 -0
  47. package/src/__tests__/storage.test.ts +60 -0
  48. package/src/__tests__/templates.test.ts +247 -0
  49. package/src/doc/docToMarkdown.ts +98 -0
  50. package/src/doc/getLayers.ts +152 -0
  51. package/src/doc/index.ts +8 -0
  52. package/src/doc/markdownToDoc.ts +277 -0
  53. package/src/doc/templates/accentImage.ts +338 -0
  54. package/src/doc/templates/captionUtils.ts +33 -0
  55. package/src/doc/templates/comparisonBar.ts +140 -0
  56. package/src/doc/templates/coverBlock.ts +156 -0
  57. package/src/doc/templates/dateEvent.ts +122 -0
  58. package/src/doc/templates/definitionCard.ts +129 -0
  59. package/src/doc/templates/factCard.ts +114 -0
  60. package/src/doc/templates/fullBleedQuote.ts +59 -0
  61. package/src/doc/templates/imageWithCaption.ts +167 -0
  62. package/src/doc/templates/index.ts +571 -0
  63. package/src/doc/templates/listBlock.ts +101 -0
  64. package/src/doc/templates/mapBlock.ts +132 -0
  65. package/src/doc/templates/persistentLayers.ts +522 -0
  66. package/src/doc/templates/photoGrid.ts +199 -0
  67. package/src/doc/templates/pullQuote.ts +119 -0
  68. package/src/doc/templates/quoteBlock.ts +112 -0
  69. package/src/doc/templates/sectionHeader.ts +105 -0
  70. package/src/doc/templates/statHighlight.ts +113 -0
  71. package/src/doc/templates/titleBlock.ts +100 -0
  72. package/src/doc/templates/twoColumn.ts +239 -0
  73. package/src/doc/templates/videoPullQuote.ts +128 -0
  74. package/src/doc/templates/videoWithCaption.ts +126 -0
  75. package/src/doc/utils/animationUtils.ts +135 -0
  76. package/src/doc/utils/themeUtils.ts +139 -0
  77. package/src/index.ts +5 -0
  78. package/src/markdown/convert.ts +897 -0
  79. package/src/markdown/htmlParse.ts +212 -0
  80. package/src/markdown/index.ts +110 -0
  81. package/src/markdown/parse.ts +103 -0
  82. package/src/markdown/stringify.ts +122 -0
  83. package/src/markdown/types.ts +605 -0
  84. package/src/markdown/utils.ts +165 -0
  85. package/src/ngeohash.d.ts +11 -0
  86. package/src/schemas/BlockTemplates.ts +706 -0
  87. package/src/schemas/Doc.ts +565 -0
  88. package/src/schemas/LayoutStrategy.ts +178 -0
  89. package/src/schemas/MediaProvider.ts +70 -0
  90. package/src/schemas/Theme.ts +235 -0
  91. package/src/schemas/Types.ts +15 -0
  92. package/src/schemas/Viewport.ts +91 -0
  93. package/src/schemas/index.ts +14 -0
  94. package/src/schemas/themeLibrary.ts +492 -0
  95. package/src/spatial/Geohash.ts +168 -0
  96. package/src/spatial/Haversine.ts +59 -0
  97. package/src/spatial/index.ts +2 -0
  98. package/src/storage/LocalForageAdapter.ts +103 -0
  99. package/src/storage/LocalStorageAdapter.ts +57 -0
  100. package/src/storage/MemoryStorageAdapter.ts +37 -0
  101. package/src/storage/Storage.ts +28 -0
  102. package/src/storage/index.ts +5 -0
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Date Event Template
3
+ *
4
+ * Timeline-style slide with prominent date and description.
5
+ * Supports different moods: neutral, somber, celebratory.
6
+ * Adapts font sizes and positioning for different viewports.
7
+ *
8
+ * Supports optional accent images that appear as tasteful side/bottom strips.
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 { DateEventInput, TemplateContext } from '../../schemas/BlockTemplates.js';
15
+ import { scaledFontSize } from '../../schemas/BlockTemplates.js';
16
+ import { createAccentLayers, getAccentLayout, adjustY, DEFAULT_LAYOUT } from './accentImage.js';
17
+
18
+ const MOOD_COLORS = {
19
+ neutral: { bg: '#1a202c', date: '#63b3ed', text: '#ffffff' },
20
+ somber: { bg: '#1a1a2e', date: '#e53e3e', text: '#a0aec0' },
21
+ celebratory: { bg: '#1a365d', date: '#68d391', text: '#ffffff' },
22
+ };
23
+
24
+ export function dateEvent(input: DateEventInput, context: TemplateContext): Layer[] {
25
+ const { date, description, footer, mood = 'neutral', accentImage } = input;
26
+ const { theme } = context;
27
+ const colors = MOOD_COLORS[mood];
28
+
29
+ // Get layout adjustments if accent image is present
30
+ const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
31
+
32
+ // Scale font sizes — date is the hero element
33
+ const dateFontSize = scaledFontSize(96, context, true);
34
+ const descFontSize = scaledFontSize(30, context, false);
35
+ const footerFontSize = scaledFontSize(26, context, false);
36
+
37
+ const layers: Layer[] = [
38
+ // Background — unique diagonal gradient to differentiate from other dark templates
39
+ {
40
+ type: 'shape',
41
+ id: 'bg',
42
+ content: {
43
+ shape: 'rect',
44
+ fill: `linear-gradient(135deg, ${colors.bg} 0%, #0d1117 100%)`,
45
+ },
46
+ position: { x: 0, y: 0, width: '100%', height: '100%' },
47
+ },
48
+ ];
49
+
50
+ // Add accent image layers (behind text, after background)
51
+ if (accentImage) {
52
+ layers.push(...createAccentLayers(accentImage, input.id));
53
+ }
54
+
55
+ // Date — hero element, much larger
56
+ layers.push({
57
+ type: 'text',
58
+ id: 'date',
59
+ content: {
60
+ text: date,
61
+ style: {
62
+ fontSize: dateFontSize,
63
+ fontWeight: 'bold',
64
+ color: colors.date,
65
+ shadow: !!accentImage,
66
+ },
67
+ },
68
+ position: {
69
+ x: accentLayout.textCenterX,
70
+ y: adjustY('35%', accentLayout),
71
+ anchor: 'center',
72
+ },
73
+ animation: { type: 'fadeIn', duration: 1.5 },
74
+ });
75
+
76
+ // Description - constrained width for proper text wrapping
77
+ layers.push({
78
+ type: 'text',
79
+ id: 'description',
80
+ content: {
81
+ text: description,
82
+ style: {
83
+ fontSize: descFontSize,
84
+ color: colors.text,
85
+ textAlign: 'center',
86
+ lineHeight: 1.8,
87
+ shadow: !!accentImage,
88
+ },
89
+ },
90
+ position: {
91
+ x: accentLayout.textCenterX,
92
+ y: adjustY('58%', accentLayout),
93
+ width: accentLayout.textWidth,
94
+ anchor: 'center',
95
+ },
96
+ animation: { type: 'fadeIn', duration: 2, delay: 1 },
97
+ });
98
+
99
+ // Add footer if provided
100
+ if (footer) {
101
+ layers.push({
102
+ type: 'text',
103
+ id: 'footer',
104
+ content: {
105
+ text: footer,
106
+ style: {
107
+ fontSize: footerFontSize,
108
+ color: theme.colors.textMuted,
109
+ shadow: !!accentImage,
110
+ },
111
+ },
112
+ position: {
113
+ x: accentLayout.textCenterX,
114
+ y: adjustY('82%', accentLayout),
115
+ anchor: 'center',
116
+ },
117
+ animation: { type: 'fadeIn', duration: 1, delay: 3 },
118
+ });
119
+ }
120
+
121
+ return layers;
122
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Definition Card Template
3
+ *
4
+ * Dictionary-style slide with a large term and its definition.
5
+ * Good for explaining local words, place names, or cultural concepts.
6
+ * Supports optional accent images.
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 { DefinitionCardInput, TemplateContext } from '../../schemas/BlockTemplates.js';
13
+ import { scaledFontSize } from '../../schemas/BlockTemplates.js';
14
+ import { resolveColorScheme } from '../utils/themeUtils.js';
15
+ import { createAccentLayers, getAccentLayout, adjustY, DEFAULT_LAYOUT } from './accentImage.js';
16
+ import { createBackgroundLayer } from './captionUtils.js';
17
+
18
+ export function definitionCard(input: DefinitionCardInput, context: TemplateContext): Layer[] {
19
+ const { term, definition, origin, colorScheme = 'blue', accentImage } = input;
20
+ const { theme } = context;
21
+ const colors = resolveColorScheme(context, colorScheme);
22
+
23
+ // Get layout adjustments if accent image is present
24
+ const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
25
+
26
+ const termFontSize = scaledFontSize(72, context, true);
27
+ const defFontSize = scaledFontSize(32, context, false);
28
+ const originFontSize = scaledFontSize(22, context, false);
29
+
30
+ const layers: Layer[] = [
31
+ createBackgroundLayer(
32
+ 'bg',
33
+ `linear-gradient(145deg, #1e2030 0%, ${theme.colors.background} 100%)`,
34
+ ),
35
+ ];
36
+
37
+ // Add accent image layers
38
+ if (accentImage) {
39
+ layers.push(...createAccentLayers(accentImage, input.id));
40
+ }
41
+
42
+ // Term — large, accent-colored
43
+ layers.push({
44
+ type: 'text',
45
+ id: 'term',
46
+ content: {
47
+ text: term,
48
+ style: {
49
+ fontSize: termFontSize,
50
+ fontWeight: 'bold',
51
+ color: colors.text,
52
+ textAlign: 'center',
53
+ shadow: !!accentImage,
54
+ },
55
+ },
56
+ position: {
57
+ x: accentLayout.textCenterX,
58
+ y: adjustY('30%', accentLayout),
59
+ anchor: 'center',
60
+ },
61
+ animation: { type: 'fadeIn', duration: 1.5 },
62
+ });
63
+
64
+ // Horizontal separator line
65
+ layers.push({
66
+ type: 'shape',
67
+ id: 'separator',
68
+ content: {
69
+ shape: 'rect',
70
+ fill: `${colors.text}33`, // accent color at 20% opacity
71
+ },
72
+ position: {
73
+ x: accentLayout.textCenterX,
74
+ y: adjustY('42%', accentLayout),
75
+ width: '30%',
76
+ height: '2px',
77
+ anchor: 'center',
78
+ },
79
+ });
80
+
81
+ // Definition text
82
+ layers.push({
83
+ type: 'text',
84
+ id: 'definition',
85
+ content: {
86
+ text: definition,
87
+ style: {
88
+ fontSize: defFontSize,
89
+ color: theme.colors.text,
90
+ textAlign: 'center',
91
+ lineHeight: 1.6,
92
+ maxLines: 4,
93
+ shadow: !!accentImage,
94
+ },
95
+ },
96
+ position: {
97
+ x: accentLayout.textCenterX,
98
+ y: adjustY('55%', accentLayout),
99
+ width: accentLayout.textWidth,
100
+ anchor: 'center',
101
+ },
102
+ animation: { type: 'fadeIn', duration: 1, delay: 0.8 },
103
+ });
104
+
105
+ // Origin if provided
106
+ if (origin) {
107
+ layers.push({
108
+ type: 'text',
109
+ id: 'origin',
110
+ content: {
111
+ text: origin,
112
+ style: {
113
+ fontSize: originFontSize,
114
+ color: theme.colors.textMuted,
115
+ textAlign: 'center',
116
+ shadow: !!accentImage,
117
+ },
118
+ },
119
+ position: {
120
+ x: accentLayout.textCenterX,
121
+ y: adjustY('78%', accentLayout),
122
+ anchor: 'center',
123
+ },
124
+ animation: { type: 'fadeIn', duration: 0.8, delay: 1.5 },
125
+ });
126
+ }
127
+
128
+ return layers;
129
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Fact Card Template
3
+ *
4
+ * Key fact with explanation and optional source.
5
+ * Useful for presenting important information clearly.
6
+ * Adapts font sizes and positioning for different viewports.
7
+ *
8
+ * Supports optional accent images that appear as tasteful side/bottom strips.
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 { FactCardInput, TemplateContext } from '../../schemas/BlockTemplates.js';
15
+ import { scaledFontSize } from '../../schemas/BlockTemplates.js';
16
+ import { createAccentLayers, getAccentLayout, adjustY, DEFAULT_LAYOUT } from './accentImage.js';
17
+ import { createBackgroundLayer } from './captionUtils.js';
18
+
19
+ export function factCard(input: FactCardInput, context: TemplateContext): Layer[] {
20
+ const { fact, explanation, source, accentImage } = input;
21
+ const { theme } = context;
22
+
23
+ // Get layout adjustments if accent image is present
24
+ const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
25
+
26
+ // Scale font sizes for viewport
27
+ const factFontSize = scaledFontSize(56, context, true);
28
+ const explainFontSize = scaledFontSize(32, context, false);
29
+ const sourceFontSize = scaledFontSize(20, context, false);
30
+
31
+ const layers: Layer[] = [
32
+ createBackgroundLayer(
33
+ 'bg',
34
+ `linear-gradient(170deg, #1e2030 0%, ${theme.colors.background} 100%)`,
35
+ ),
36
+ ];
37
+
38
+ // Add accent image layers (behind text, after background)
39
+ if (accentImage) {
40
+ layers.push(...createAccentLayers(accentImage, input.id));
41
+ }
42
+
43
+ // Fact (main statement)
44
+ layers.push({
45
+ type: 'text',
46
+ id: 'fact',
47
+ content: {
48
+ text: fact,
49
+ style: {
50
+ fontSize: factFontSize,
51
+ fontWeight: 'bold',
52
+ color: theme.colors.text,
53
+ textAlign: 'center',
54
+ lineHeight: 1.4,
55
+ shadow: true,
56
+ },
57
+ },
58
+ position: {
59
+ x: accentLayout.textCenterX,
60
+ y: adjustY('35%', accentLayout),
61
+ width: accentLayout.textWidth,
62
+ anchor: 'center',
63
+ },
64
+ animation: { type: 'fadeIn', duration: 1.5 },
65
+ });
66
+
67
+ // Explanation
68
+ layers.push({
69
+ type: 'text',
70
+ id: 'explanation',
71
+ content: {
72
+ text: explanation,
73
+ style: {
74
+ fontSize: explainFontSize,
75
+ color: theme.colors.textMuted,
76
+ textAlign: 'center',
77
+ lineHeight: 1.5,
78
+ shadow: !!accentImage,
79
+ },
80
+ },
81
+ position: {
82
+ x: accentLayout.textCenterX,
83
+ y: adjustY('60%', accentLayout),
84
+ width: accentLayout.textWidth,
85
+ anchor: 'center',
86
+ },
87
+ animation: { type: 'fadeIn', duration: 1, delay: 0.8 },
88
+ });
89
+
90
+ // Add source if provided
91
+ if (source) {
92
+ layers.push({
93
+ type: 'text',
94
+ id: 'source',
95
+ content: {
96
+ text: source,
97
+ style: {
98
+ fontSize: sourceFontSize,
99
+ color: theme.colors.textMuted,
100
+ textAlign: 'center',
101
+ shadow: !!accentImage,
102
+ },
103
+ },
104
+ position: {
105
+ x: accentLayout.textCenterX,
106
+ y: adjustY('85%', accentLayout),
107
+ anchor: 'center',
108
+ },
109
+ animation: { type: 'fadeIn', duration: 0.8, delay: 1.5 },
110
+ });
111
+ }
112
+
113
+ return layers;
114
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Full Bleed Quote Template
3
+ *
4
+ * Short dramatic text filling the viewport like a movie title card.
5
+ * Designed for punchy text under 60 characters. Uses massive font
6
+ * centered on a dark vignette background.
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 { FullBleedQuoteInput, TemplateContext } from '../../schemas/BlockTemplates.js';
13
+ import { scaledFontSize } from '../../schemas/BlockTemplates.js';
14
+ import { resolveColorScheme } from '../utils/themeUtils.js';
15
+
16
+ export function fullBleedQuote(input: FullBleedQuoteInput, context: TemplateContext): Layer[] {
17
+ const { text, colorScheme = 'blue' } = input;
18
+ const { theme } = context;
19
+ const colors = resolveColorScheme(context, colorScheme);
20
+
21
+ // Massive font for dramatic impact
22
+ const textFontSize = scaledFontSize(120, context, true);
23
+
24
+ return [
25
+ // Background — dark radial vignette for cinematic feel
26
+ {
27
+ type: 'shape',
28
+ id: 'bg',
29
+ content: {
30
+ shape: 'rect',
31
+ fill: `radial-gradient(ellipse at 50% 50%, ${theme.colors.backgroundLight} 0%, #000000 100%)`,
32
+ },
33
+ position: { x: 0, y: 0, width: '100%', height: '100%' },
34
+ },
35
+ // The text — massive, centered
36
+ {
37
+ type: 'text',
38
+ id: 'impact-text',
39
+ content: {
40
+ text,
41
+ style: {
42
+ fontSize: textFontSize,
43
+ fontWeight: 'bold',
44
+ color: colors.text,
45
+ textAlign: 'center',
46
+ lineHeight: 1.2,
47
+ shadow: true,
48
+ },
49
+ },
50
+ position: {
51
+ x: '50%',
52
+ y: '50%',
53
+ anchor: 'center',
54
+ width: '85%',
55
+ },
56
+ animation: { type: 'fadeIn', duration: 1.5 },
57
+ },
58
+ ];
59
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Image With Caption Template
3
+ *
4
+ * Full-screen background image with text overlay.
5
+ * Supports Ken Burns animation effects (zoom, pan).
6
+ * Adapts caption positioning and font sizes 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 { ImageWithCaptionInput, TemplateContext } from '../../schemas/BlockTemplates.js';
13
+ import { scaledFontSize } from '../../schemas/BlockTemplates.js';
14
+ import { cleanCaption } from './captionUtils.js';
15
+ import { mapAmbientMotion } from './accentImage.js';
16
+
17
+ export function imageWithCaption(input: ImageWithCaptionInput, context: TemplateContext): Layer[] {
18
+ const {
19
+ imageSrc,
20
+ imageAlt,
21
+ caption: rawCaption,
22
+ captionPosition: _captionPosition = 'bottom',
23
+ ambientMotion,
24
+ isTitle,
25
+ subtitle,
26
+ imageCredit,
27
+ imageLicense,
28
+ } = input;
29
+ const caption = rawCaption ? cleanCaption(rawCaption) : rawCaption;
30
+ const { theme, layout } = context;
31
+
32
+ // Scale font sizes for viewport
33
+ const captionFontSize = scaledFontSize(36, context, false);
34
+ const titleFontSize = scaledFontSize(96, context, true);
35
+ const subtitleFontSize = scaledFontSize(36, context, false);
36
+
37
+ // Determine animation based on ambientMotion setting
38
+ // Support both new 'ambientMotion' and legacy 'kenBurns' property
39
+ const legacyKenBurns = (input as unknown as Record<string, unknown>).kenBurns as
40
+ | string
41
+ | undefined;
42
+ const motion: string | undefined = ambientMotion || legacyKenBurns;
43
+ const imageAnimation = mapAmbientMotion(motion);
44
+
45
+ const layers: Layer[] = [
46
+ // Background image
47
+ {
48
+ type: 'image',
49
+ id: 'bg-image',
50
+ content: {
51
+ src: imageSrc,
52
+ alt: imageAlt,
53
+ fit: 'cover',
54
+ credit: imageCredit,
55
+ license: imageLicense,
56
+ },
57
+ position: { x: 0, y: 0, width: '100%', height: '100%' },
58
+ animation: imageAnimation,
59
+ },
60
+ ];
61
+
62
+ // TITLE MODE: Large centered title over hero image (like cover slide)
63
+ if (isTitle && caption) {
64
+ // Full gradient overlay for title readability (similar to cover slide)
65
+ layers.push({
66
+ type: 'shape',
67
+ id: 'title-gradient',
68
+ content: {
69
+ shape: 'rect',
70
+ fill: 'linear-gradient(0deg, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0.4) 40%, rgba(0,0,0,0.1) 70%, transparent 100%)',
71
+ },
72
+ position: { x: 0, y: 0, width: '100%', height: '100%' },
73
+ });
74
+
75
+ // Title text - large and centered in lower portion
76
+ layers.push({
77
+ type: 'text',
78
+ id: 'title',
79
+ content: {
80
+ text: caption,
81
+ style: {
82
+ fontSize: titleFontSize,
83
+ fontWeight: 'bold',
84
+ color: theme.colors.text,
85
+ textAlign: 'center',
86
+ shadow: true,
87
+ },
88
+ },
89
+ position: {
90
+ x: '50%',
91
+ y: subtitle ? '70%' : '75%',
92
+ anchor: 'center',
93
+ width: layout.maxTextWidth,
94
+ },
95
+ animation: { type: 'fadeIn', duration: 0.8 },
96
+ });
97
+
98
+ // Optional subtitle
99
+ if (subtitle) {
100
+ layers.push({
101
+ type: 'text',
102
+ id: 'subtitle',
103
+ content: {
104
+ text: subtitle,
105
+ style: {
106
+ fontSize: subtitleFontSize,
107
+ color: theme.colors.textMuted,
108
+ textAlign: 'center',
109
+ lineHeight: 1.5,
110
+ },
111
+ },
112
+ position: {
113
+ x: '50%',
114
+ y: '82%',
115
+ anchor: 'center',
116
+ width: layout.maxTextWidth,
117
+ },
118
+ animation: { type: 'fadeIn', duration: 0.8, delay: 0.2 },
119
+ });
120
+ }
121
+
122
+ return layers;
123
+ }
124
+
125
+ // STANDARD MODE: Caption at bottom
126
+ if (caption) {
127
+ // Subtle gradient at bottom for caption readability (raised above media controls)
128
+ layers.push({
129
+ type: 'shape',
130
+ id: 'caption-gradient',
131
+ content: {
132
+ shape: 'rect',
133
+ fill: 'linear-gradient(0deg, rgba(0,0,0,0.7) 0%, rgba(0,0,0,0.3) 60%, transparent 100%)',
134
+ },
135
+ position: {
136
+ x: 0,
137
+ y: '65%',
138
+ width: '100%',
139
+ height: '35%',
140
+ },
141
+ });
142
+
143
+ // Caption text - raised above media controls
144
+ layers.push({
145
+ type: 'text',
146
+ id: 'caption',
147
+ content: {
148
+ text: caption,
149
+ style: {
150
+ fontSize: captionFontSize,
151
+ color: theme.colors.text,
152
+ textAlign: 'center',
153
+ shadow: true,
154
+ },
155
+ },
156
+ position: {
157
+ x: '50%',
158
+ y: '82%',
159
+ anchor: 'center',
160
+ width: layout.maxTextWidth,
161
+ },
162
+ animation: { type: 'fadeIn', duration: 1.5, delay: 0.5 },
163
+ });
164
+ }
165
+
166
+ return layers;
167
+ }