@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.
Files changed (254) hide show
  1. package/README.md +14 -1
  2. package/dist/{ContentContainer-DNx460y8.d.ts → ContentContainer-BXUlIf18.d.ts} +6 -2
  3. package/dist/{Doc-BeSeH2iX.d.ts → Doc-BBVGq1_9.d.ts} +922 -273
  4. package/dist/{ImageEditDoc-BTIvw0oq.d.ts → ImageEditDoc-FuyGtt6o.d.ts} +11 -4
  5. package/dist/{chunk-ZTX4PFFT.js → chunk-2OIBZYKP.js} +75 -2
  6. package/dist/chunk-2OIBZYKP.js.map +1 -0
  7. package/dist/chunk-2UQOHYE2.js +45 -0
  8. package/dist/chunk-2UQOHYE2.js.map +1 -0
  9. package/dist/chunk-6ATE2PSM.js +344 -0
  10. package/dist/chunk-6ATE2PSM.js.map +1 -0
  11. package/dist/{chunk-57CVQCFX.js → chunk-A4O7GIWE.js} +74 -23
  12. package/dist/chunk-A4O7GIWE.js.map +1 -0
  13. package/dist/{chunk-5WFRKURX.js → chunk-DLXZMS5K.js} +4 -2
  14. package/dist/chunk-DLXZMS5K.js.map +1 -0
  15. package/dist/chunk-ET53IIEP.js +1 -0
  16. package/dist/chunk-ET53IIEP.js.map +1 -0
  17. package/dist/chunk-HWVFJAAH.js +362 -0
  18. package/dist/chunk-HWVFJAAH.js.map +1 -0
  19. package/dist/{chunk-2TMAWB4Y.js → chunk-IMSCRLLV.js} +1 -1
  20. package/dist/chunk-IMSCRLLV.js.map +1 -0
  21. package/dist/{chunk-B4EEON3N.js → chunk-KKNUBQ6Y.js} +536 -103
  22. package/dist/chunk-KKNUBQ6Y.js.map +1 -0
  23. package/dist/chunk-LDQ2HJIX.js +596 -0
  24. package/dist/chunk-LDQ2HJIX.js.map +1 -0
  25. package/dist/chunk-LH7I6SH7.js +7208 -0
  26. package/dist/chunk-LH7I6SH7.js.map +1 -0
  27. package/dist/chunk-PGGGKYWG.js +1 -0
  28. package/dist/chunk-PGGGKYWG.js.map +1 -0
  29. package/dist/{chunk-7UDSRZKG.js → chunk-Q3ROPT5H.js} +35 -14
  30. package/dist/chunk-Q3ROPT5H.js.map +1 -0
  31. package/dist/chunk-QE7TWGVT.js +178 -0
  32. package/dist/chunk-QE7TWGVT.js.map +1 -0
  33. package/dist/chunk-RUDYOTA6.js +345 -0
  34. package/dist/chunk-RUDYOTA6.js.map +1 -0
  35. package/dist/{chunk-K32VJONL.js → chunk-T5UK6YOB.js} +41 -2
  36. package/dist/chunk-T5UK6YOB.js.map +1 -0
  37. package/dist/{chunk-3K5OG6KI.js → chunk-TEEEILMP.js} +412 -186
  38. package/dist/chunk-TEEEILMP.js.map +1 -0
  39. package/dist/{chunk-FR2RBTKO.js → chunk-TQWLI6S2.js} +93 -27
  40. package/dist/chunk-TQWLI6S2.js.map +1 -0
  41. package/dist/{chunk-WMOI7FN3.js → chunk-UY7KGQ5R.js} +12 -2
  42. package/dist/{chunk-WMOI7FN3.js.map → chunk-UY7KGQ5R.js.map} +1 -1
  43. package/dist/{chunk-3E5F2XMR.js → chunk-VWUFZ6ZG.js} +29 -2
  44. package/dist/chunk-VWUFZ6ZG.js.map +1 -0
  45. package/dist/chunk-ZLQKNOZQ.js +1 -0
  46. package/dist/chunk-ZLQKNOZQ.js.map +1 -0
  47. package/dist/doc/index.d.ts +5 -5
  48. package/dist/doc/index.js +135 -8
  49. package/dist/generate/index.d.ts +3 -74
  50. package/dist/generate/index.js +2 -7
  51. package/dist/icons/index.d.ts +3 -26
  52. package/dist/icons/index.js +13 -0
  53. package/dist/icons/inlineIconMarker.d.ts +1 -0
  54. package/dist/icons/inlineIconMarker.js +15 -0
  55. package/dist/icons/inlineIconMarker.js.map +1 -0
  56. package/dist/imageEdit/index.d.ts +12 -6
  57. package/dist/imageEdit/index.js +4 -1
  58. package/dist/index.d.ts +14 -11
  59. package/dist/index.js +265 -37
  60. package/dist/inlineIconMarker-CiE8Hlvz.d.ts +59 -0
  61. package/dist/jsonForm/index.d.ts +42 -1
  62. package/dist/jsonForm/index.js +9 -1
  63. package/dist/markdown/index.d.ts +228 -3
  64. package/dist/markdown/index.js +42 -7
  65. package/dist/recommend/index.d.ts +1 -1
  66. package/dist/recommend/index.js +3 -2
  67. package/dist/schemas/index.d.ts +28 -8
  68. package/dist/schemas/index.js +36 -8
  69. package/dist/spatial/index.d.ts +9 -4
  70. package/dist/spatial/index.js +1 -1
  71. package/dist/storage/index.d.ts +2 -2
  72. package/dist/storage/index.js +1 -1
  73. package/dist/story/index.d.ts +1021 -41
  74. package/dist/story/index.js +135 -8
  75. package/dist/{themeLibrary-BHjJ0Qx6.d.ts → themeLibrary-CehcJhzz.d.ts} +1 -1
  76. package/dist/timing/index.js +3 -5
  77. package/dist/transform/index.d.ts +48 -10
  78. package/dist/transform/index.js +17 -481
  79. package/dist/transform/index.js.map +1 -1
  80. package/dist/{types-BdZkdJ3z.d.ts → types-DlAZ7MW4.d.ts} +115 -1
  81. package/dist/versions/index.d.ts +1 -1
  82. package/package.json +6 -1
  83. package/src/__tests__/animationUtils.test.ts +40 -0
  84. package/src/__tests__/annotationCoercion.test.ts +243 -0
  85. package/src/__tests__/applyRenderStyle.test.ts +146 -0
  86. package/src/__tests__/atmosphereLayers.test.ts +108 -0
  87. package/src/__tests__/attrTokens.test.ts +162 -0
  88. package/src/__tests__/contentExtractor.test.ts +60 -0
  89. package/src/__tests__/coverBlock.test.ts +46 -1
  90. package/src/__tests__/customTemplateSample.test.ts +144 -0
  91. package/src/__tests__/customTemplatesFrontmatter.test.ts +236 -0
  92. package/src/__tests__/customThemesFrontmatter.test.ts +145 -0
  93. package/src/__tests__/diagramBlock.test.ts +122 -0
  94. package/src/__tests__/diagramLayout.test.ts +134 -0
  95. package/src/__tests__/drawing.test.ts +109 -0
  96. package/src/__tests__/drawingBlock.test.ts +124 -0
  97. package/src/__tests__/drawingLayout.test.ts +171 -0
  98. package/src/__tests__/geohash.test.ts +6 -5
  99. package/src/__tests__/getLayers.test.ts +6 -2
  100. package/src/__tests__/getLayersFallback.test.ts +103 -0
  101. package/src/__tests__/imageEditExportSvg.test.ts +70 -0
  102. package/src/__tests__/imageEditVersions.test.ts +72 -0
  103. package/src/__tests__/inlineIconMarker.test.ts +36 -0
  104. package/src/__tests__/inputDescriptors.test.ts +162 -0
  105. package/src/__tests__/jsonForm.tokens.test.ts +84 -0
  106. package/src/__tests__/layoutBlock.test.ts +83 -0
  107. package/src/__tests__/layoutLayout.test.ts +169 -0
  108. package/src/__tests__/markdown.test.ts +87 -0
  109. package/src/__tests__/markdownSanitize.test.ts +79 -0
  110. package/src/__tests__/markdownToDoc.test.ts +391 -2
  111. package/src/__tests__/mediaAnnotations.test.ts +91 -0
  112. package/src/__tests__/mediaSchedule.test.ts +118 -0
  113. package/src/__tests__/pandocAttrParse.test.ts +187 -0
  114. package/src/__tests__/resolveDocTheme.test.ts +55 -0
  115. package/src/__tests__/rootExports.test.ts +43 -0
  116. package/src/__tests__/shapeGeometry.test.ts +89 -0
  117. package/src/__tests__/shapeVocabulary.test.ts +133 -0
  118. package/src/__tests__/structuredData.test.ts +249 -0
  119. package/src/__tests__/templateAnnotationParse.test.ts +108 -0
  120. package/src/__tests__/templateMetadata.test.ts +31 -0
  121. package/src/__tests__/templates.test.ts +257 -4
  122. package/src/__tests__/themeCompile.test.ts +42 -0
  123. package/src/__tests__/themeValidator.test.ts +6 -0
  124. package/src/__tests__/transformV2.test.ts +137 -0
  125. package/src/__tests__/transitionNormalize.test.ts +61 -0
  126. package/src/__tests__/validateDoc.test.ts +260 -0
  127. package/src/doc/annotationBlocks.ts +117 -0
  128. package/src/doc/customTemplatesFrontmatter.ts +314 -0
  129. package/src/doc/customThemesFrontmatter.ts +95 -0
  130. package/src/doc/docToMarkdown.ts +104 -5
  131. package/src/doc/getLayers.ts +49 -17
  132. package/src/doc/index.ts +47 -1
  133. package/src/doc/markdownToDoc.ts +411 -13
  134. package/src/doc/mediaAnnotations.ts +100 -0
  135. package/src/doc/resolveDocTheme.ts +55 -0
  136. package/src/doc/standaloneAnnotation.ts +58 -0
  137. package/src/doc/structuredData.ts +301 -0
  138. package/src/doc/templateInputs.ts +240 -0
  139. package/src/doc/templates/__tests__/customTemplate.test.ts +170 -0
  140. package/src/doc/templates/__tests__/customTemplateRegistry.test.ts +110 -0
  141. package/src/doc/templates/accentImage.ts +30 -6
  142. package/src/doc/templates/captionUtils.ts +23 -0
  143. package/src/doc/templates/comparisonBar.ts +33 -18
  144. package/src/doc/templates/coverBlock.ts +35 -5
  145. package/src/doc/templates/customTemplate.ts +51 -0
  146. package/src/doc/templates/dataTable.ts +37 -19
  147. package/src/doc/templates/dateEvent.ts +41 -20
  148. package/src/doc/templates/definitionCard.ts +26 -18
  149. package/src/doc/templates/diagramBlock.ts +204 -0
  150. package/src/doc/templates/diagramLayout.ts +159 -0
  151. package/src/doc/templates/drawingBlock.ts +325 -0
  152. package/src/doc/templates/drawingLayout.ts +539 -0
  153. package/src/doc/templates/factCard.ts +54 -26
  154. package/src/doc/templates/fallbackBlock.ts +123 -0
  155. package/src/doc/templates/featureBlock.ts +20 -13
  156. package/src/doc/templates/fullBleedQuote.ts +18 -8
  157. package/src/doc/templates/imageWithCaption.ts +30 -15
  158. package/src/doc/templates/index.ts +225 -46
  159. package/src/doc/templates/inputDescriptors.ts +432 -0
  160. package/src/doc/templates/layoutBlock.ts +43 -0
  161. package/src/doc/templates/layoutLayout.ts +243 -0
  162. package/src/doc/templates/listBlock.ts +56 -30
  163. package/src/doc/templates/mapBlock.ts +15 -12
  164. package/src/doc/templates/metadata.ts +120 -0
  165. package/src/doc/templates/persistentLayers.ts +129 -11
  166. package/src/doc/templates/photoGrid.ts +22 -34
  167. package/src/doc/templates/pullQuote.ts +30 -12
  168. package/src/doc/templates/quoteBlock.ts +54 -29
  169. package/src/doc/templates/sectionHeader.ts +25 -11
  170. package/src/doc/templates/statHighlight.ts +39 -22
  171. package/src/doc/templates/titleBlock.ts +59 -21
  172. package/src/doc/templates/tokens/__tests__/resolveTokens.test.ts +487 -0
  173. package/src/doc/templates/tokens/resolveTokens.ts +453 -0
  174. package/src/doc/templates/twoColumn.ts +48 -30
  175. package/src/doc/templates/videoPullQuote.ts +22 -12
  176. package/src/doc/templates/videoWithCaption.ts +21 -15
  177. package/src/doc/utils/animationUtils.ts +267 -3
  178. package/src/doc/utils/applyRenderStyle.ts +113 -0
  179. package/src/doc/utils/imageTreatment.ts +70 -0
  180. package/src/doc/utils/nearest.ts +59 -0
  181. package/src/doc/utils/shapeGeometry.ts +464 -0
  182. package/src/doc/utils/themeUtils.ts +75 -1
  183. package/src/doc/validate.ts +575 -0
  184. package/src/generate/contentExtractor.ts +148 -35
  185. package/src/generate/index.ts +0 -3
  186. package/src/icons/index.ts +8 -0
  187. package/src/icons/inlineIconMarker.ts +73 -0
  188. package/src/imageEdit/export.ts +85 -2
  189. package/src/imageEdit/index.ts +1 -0
  190. package/src/imageEdit/persistence.ts +51 -0
  191. package/src/index.ts +2 -0
  192. package/src/jsonForm/index.ts +2 -0
  193. package/src/jsonForm/tokens.ts +78 -0
  194. package/src/markdown/annotationCoercion.ts +295 -0
  195. package/src/markdown/attrTokens.ts +241 -0
  196. package/src/markdown/convert.ts +229 -26
  197. package/src/markdown/index.ts +37 -0
  198. package/src/markdown/sanitize.ts +318 -0
  199. package/src/markdown/stringify.ts +96 -6
  200. package/src/markdown/types.ts +66 -0
  201. package/src/markdown/utils.ts +111 -2
  202. package/src/recommend/templates.ts +24 -0
  203. package/src/schemas/BlockTemplates.ts +133 -4
  204. package/src/schemas/CustomTemplates.ts +126 -0
  205. package/src/schemas/Doc.ts +387 -65
  206. package/src/schemas/ImageEditDoc.ts +11 -4
  207. package/src/schemas/Media.ts +169 -0
  208. package/src/schemas/Theme.ts +23 -1
  209. package/src/schemas/Transitions.ts +224 -0
  210. package/src/schemas/colorUtils.ts +33 -0
  211. package/src/schemas/fontStacks.ts +5 -2
  212. package/src/schemas/index.ts +3 -0
  213. package/src/schemas/themeCompile.ts +24 -5
  214. package/src/schemas/themeValidator.ts +49 -11
  215. package/src/schemas/themes/cinematic.json +69 -12
  216. package/src/schemas/themes/documentary.json +58 -11
  217. package/src/schemas/themes/gezellig.json +60 -11
  218. package/src/schemas/themes/magazine.json +54 -9
  219. package/src/schemas/themes/morning-light.json +52 -9
  220. package/src/schemas/themes/standard-dark.json +48 -13
  221. package/src/schemas/themes/standard.json +44 -12
  222. package/src/schemas/themes/tech-dark.json +65 -12
  223. package/src/spatial/Geohash.ts +52 -21
  224. package/src/storage/ContentContainer.ts +6 -2
  225. package/src/transform/applyTransform.ts +43 -4
  226. package/src/transform/index.ts +1 -0
  227. package/src/transform/registry.ts +44 -13
  228. package/src/transform/styles/dataDriven.ts +2 -1
  229. package/src/transform/styles/documentary.ts +2 -1
  230. package/src/transform/styles/magazine.ts +3 -1
  231. package/src/transform/styles/minimal.ts +2 -1
  232. package/src/transform/styles/narrative.ts +3 -1
  233. package/src/transform/templateSelector.ts +87 -11
  234. package/src/transform/types.ts +35 -2
  235. package/dist/chunk-2TMAWB4Y.js.map +0 -1
  236. package/dist/chunk-3E5F2XMR.js.map +0 -1
  237. package/dist/chunk-3K5OG6KI.js.map +0 -1
  238. package/dist/chunk-4X3JQXNM.js +0 -35
  239. package/dist/chunk-4X3JQXNM.js.map +0 -1
  240. package/dist/chunk-57CVQCFX.js.map +0 -1
  241. package/dist/chunk-5WFRKURX.js.map +0 -1
  242. package/dist/chunk-7UDSRZKG.js.map +0 -1
  243. package/dist/chunk-B4EEON3N.js.map +0 -1
  244. package/dist/chunk-FR2RBTKO.js.map +0 -1
  245. package/dist/chunk-H3AXU4MJ.js +0 -179
  246. package/dist/chunk-H3AXU4MJ.js.map +0 -1
  247. package/dist/chunk-K32VJONL.js.map +0 -1
  248. package/dist/chunk-KSWJR66U.js +0 -3706
  249. package/dist/chunk-KSWJR66U.js.map +0 -1
  250. package/dist/chunk-MYIH7FWD.js +0 -145
  251. package/dist/chunk-MYIH7FWD.js.map +0 -1
  252. package/dist/chunk-ZTX4PFFT.js.map +0 -1
  253. package/src/__tests__/slideshowGenerator.test.ts +0 -128
  254. package/src/generate/slideshowGenerator.ts +0 -273
@@ -0,0 +1,464 @@
1
+ /**
2
+ * shapeGeometry — pure SVG geometry for the drawing/diagram shape vocabulary.
3
+ *
4
+ * Three concerns, all deterministic and dependency-free:
5
+ * - `shapePath(kind, x, y, w, h)` → an SVG path `d` for a named shape inscribed
6
+ * in a bounding box. Returns null for the natively-rendered primitives
7
+ * (rect / circle / line / text / path) so the caller can keep using a
8
+ * `ShapeLayer` for those; everything else becomes a computed `PathLayer`.
9
+ * - `markerPath(style, dir)` → the `<marker>` path for a line end-style, in the
10
+ * standard 0–10 marker viewBox (shared by the SSR `PathLayer` renderer and the
11
+ * editor's edge renderer).
12
+ * - `connectorPath(routing, start, end)` + `clipEndpoints(a, b)` → the line/edge
13
+ * geometry between two shapes (straight / orthogonal / curved), lifted from the
14
+ * diagram template so drawings and diagrams draw identical connectors.
15
+ */
16
+
17
+ import type { MarkerStyle } from '../../schemas/Doc.js';
18
+
19
+ // ============================================
20
+ // Shapes
21
+ // ============================================
22
+
23
+ /** Connector routing styles. */
24
+ export type ConnectorRouting = 'straight' | 'orthogonal' | 'curved';
25
+
26
+ /**
27
+ * Shape kinds rendered as computed paths (everything except the primitives the
28
+ * renderer draws natively). Used by templates to decide ShapeLayer vs PathLayer.
29
+ */
30
+ export const PATH_SHAPE_KINDS = new Set([
31
+ 'triangle',
32
+ 'right-triangle',
33
+ 'diamond',
34
+ 'pentagon',
35
+ 'hexagon',
36
+ 'octagon',
37
+ 'star',
38
+ 'star4',
39
+ 'star6',
40
+ 'parallelogram',
41
+ 'trapezoid',
42
+ 'plus',
43
+ 'chevron',
44
+ 'arrow-right',
45
+ 'arrow-left',
46
+ 'arrow-up',
47
+ 'arrow-down',
48
+ 'double-arrow',
49
+ 'callout',
50
+ 'cylinder',
51
+ 'cloud',
52
+ 'heart',
53
+ 'lightning',
54
+ ]);
55
+
56
+ /** Round to 1 decimal for compact, stable path output. */
57
+ function r(n: number): number {
58
+ return Math.round(n * 10) / 10;
59
+ }
60
+
61
+ /** Close a list of points into an `M … L … Z` polygon path. */
62
+ function poly(points: ReadonlyArray<readonly [number, number]>): string {
63
+ if (points.length === 0) return '';
64
+ const [first, ...rest] = points;
65
+ const head = `M ${r(first[0])} ${r(first[1])}`;
66
+ const tail = rest.map(([x, y]) => `L ${r(x)} ${r(y)}`).join(' ');
67
+ return `${head} ${tail} Z`;
68
+ }
69
+
70
+ /** Regular polygon points inscribed in the box ellipse, `rotation` in radians. */
71
+ function regular(
72
+ cx: number,
73
+ cy: number,
74
+ rx: number,
75
+ ry: number,
76
+ sides: number,
77
+ rotation: number,
78
+ ): Array<[number, number]> {
79
+ const pts: Array<[number, number]> = [];
80
+ for (let i = 0; i < sides; i++) {
81
+ const a = rotation + (i * 2 * Math.PI) / sides;
82
+ pts.push([cx + rx * Math.cos(a), cy + ry * Math.sin(a)]);
83
+ }
84
+ return pts;
85
+ }
86
+
87
+ /** Star points alternating outer/inner radius. */
88
+ function star(
89
+ cx: number,
90
+ cy: number,
91
+ rx: number,
92
+ ry: number,
93
+ spikes: number,
94
+ innerRatio: number,
95
+ ): Array<[number, number]> {
96
+ const pts: Array<[number, number]> = [];
97
+ const rot = -Math.PI / 2; // first point up
98
+ for (let i = 0; i < spikes * 2; i++) {
99
+ const outer = i % 2 === 0;
100
+ const a = rot + (i * Math.PI) / spikes;
101
+ const fx = outer ? rx : rx * innerRatio;
102
+ const fy = outer ? ry : ry * innerRatio;
103
+ pts.push([cx + fx * Math.cos(a), cy + fy * Math.sin(a)]);
104
+ }
105
+ return pts;
106
+ }
107
+
108
+ /**
109
+ * SVG path `d` for a named shape inscribed in `[x, y, w, h]`, or null when the
110
+ * kind is rendered natively (rect / circle / line / text / path / arrow).
111
+ */
112
+ export function shapePath(kind: string, x: number, y: number, w: number, h: number): string | null {
113
+ const cx = x + w / 2;
114
+ const cy = y + h / 2;
115
+ const rx = w / 2;
116
+ const ry = h / 2;
117
+ const right = x + w;
118
+ const bottom = y + h;
119
+
120
+ switch (kind) {
121
+ case 'triangle':
122
+ return poly([
123
+ [cx, y],
124
+ [right, bottom],
125
+ [x, bottom],
126
+ ]);
127
+ case 'right-triangle':
128
+ return poly([
129
+ [x, y],
130
+ [x, bottom],
131
+ [right, bottom],
132
+ ]);
133
+ case 'diamond':
134
+ return poly([
135
+ [cx, y],
136
+ [right, cy],
137
+ [cx, bottom],
138
+ [x, cy],
139
+ ]);
140
+ case 'pentagon':
141
+ return poly(regular(cx, cy, rx, ry, 5, -Math.PI / 2));
142
+ case 'hexagon': {
143
+ // Flat top/bottom hexagon: inset the left/right vertices.
144
+ const inset = w * 0.25;
145
+ return poly([
146
+ [x + inset, y],
147
+ [right - inset, y],
148
+ [right, cy],
149
+ [right - inset, bottom],
150
+ [x + inset, bottom],
151
+ [x, cy],
152
+ ]);
153
+ }
154
+ case 'octagon': {
155
+ const cut = Math.min(w, h) * 0.29;
156
+ return poly([
157
+ [x + cut, y],
158
+ [right - cut, y],
159
+ [right, y + cut],
160
+ [right, bottom - cut],
161
+ [right - cut, bottom],
162
+ [x + cut, bottom],
163
+ [x, bottom - cut],
164
+ [x, y + cut],
165
+ ]);
166
+ }
167
+ case 'star':
168
+ return poly(star(cx, cy, rx, ry, 5, 0.4));
169
+ case 'star4':
170
+ return poly(star(cx, cy, rx, ry, 4, 0.4));
171
+ case 'star6':
172
+ return poly(star(cx, cy, rx, ry, 6, 0.5));
173
+ case 'parallelogram': {
174
+ const off = w * 0.25;
175
+ return poly([
176
+ [x + off, y],
177
+ [right, y],
178
+ [right - off, bottom],
179
+ [x, bottom],
180
+ ]);
181
+ }
182
+ case 'trapezoid': {
183
+ const off = w * 0.25;
184
+ return poly([
185
+ [x + off, y],
186
+ [right - off, y],
187
+ [right, bottom],
188
+ [x, bottom],
189
+ ]);
190
+ }
191
+ case 'plus': {
192
+ const t = Math.min(w, h) / 3; // arm thickness
193
+ const lx = x + (w - t) / 2;
194
+ const rxp = lx + t;
195
+ const ty = y + (h - t) / 2;
196
+ const byp = ty + t;
197
+ return poly([
198
+ [lx, y],
199
+ [rxp, y],
200
+ [rxp, ty],
201
+ [right, ty],
202
+ [right, byp],
203
+ [rxp, byp],
204
+ [rxp, bottom],
205
+ [lx, bottom],
206
+ [lx, byp],
207
+ [x, byp],
208
+ [x, ty],
209
+ [lx, ty],
210
+ ]);
211
+ }
212
+ case 'chevron': {
213
+ const notch = w * 0.25;
214
+ return poly([
215
+ [x, y],
216
+ [right - notch, y],
217
+ [right, cy],
218
+ [right - notch, bottom],
219
+ [x, bottom],
220
+ [x + notch, cy],
221
+ ]);
222
+ }
223
+ case 'arrow-right':
224
+ return blockArrow(x, y, w, h, 'right');
225
+ case 'arrow-left':
226
+ return blockArrow(x, y, w, h, 'left');
227
+ case 'arrow-up':
228
+ return blockArrow(x, y, w, h, 'up');
229
+ case 'arrow-down':
230
+ return blockArrow(x, y, w, h, 'down');
231
+ case 'double-arrow': {
232
+ const head = w * 0.3;
233
+ const bodyT = h * 0.4;
234
+ const ty = cy - bodyT / 2;
235
+ const by = cy + bodyT / 2;
236
+ return poly([
237
+ [x, cy],
238
+ [x + head, y],
239
+ [x + head, ty],
240
+ [right - head, ty],
241
+ [right - head, y],
242
+ [right, cy],
243
+ [right - head, bottom],
244
+ [right - head, by],
245
+ [x + head, by],
246
+ [x + head, bottom],
247
+ ]);
248
+ }
249
+ case 'callout': {
250
+ // Speech bubble: rounded-ish body (square corners for path simplicity)
251
+ // with a tail off the lower-left.
252
+ const bodyH = h * 0.78;
253
+ const bb = y + bodyH;
254
+ const tailX = x + w * 0.25;
255
+ return `M ${r(x)} ${r(y)} L ${r(right)} ${r(y)} L ${r(right)} ${r(bb)} L ${r(tailX + w * 0.12)} ${r(bb)} L ${r(tailX)} ${r(bottom)} L ${r(tailX - w * 0.02)} ${r(bb)} L ${r(x)} ${r(bb)} Z`;
256
+ }
257
+ case 'cylinder': {
258
+ const ell = h * 0.16; // ellipse half-height
259
+ return (
260
+ `M ${r(x)} ${r(y + ell)} ` +
261
+ `A ${r(rx)} ${r(ell)} 0 0 1 ${r(right)} ${r(y + ell)} ` +
262
+ `L ${r(right)} ${r(bottom - ell)} ` +
263
+ `A ${r(rx)} ${r(ell)} 0 0 1 ${r(x)} ${r(bottom - ell)} ` +
264
+ `Z ` +
265
+ // top ellipse outline
266
+ `M ${r(x)} ${r(y + ell)} ` +
267
+ `A ${r(rx)} ${r(ell)} 0 0 0 ${r(right)} ${r(y + ell)}`
268
+ );
269
+ }
270
+ case 'cloud': {
271
+ // Four bumps along the top + a flat-ish bottom, via arcs.
272
+ const b = h * 0.5;
273
+ return (
274
+ `M ${r(x + w * 0.2)} ${r(bottom)} ` +
275
+ `A ${r(w * 0.2)} ${r(b)} 0 0 1 ${r(x + w * 0.15)} ${r(cy)} ` +
276
+ `A ${r(w * 0.2)} ${r(b)} 0 0 1 ${r(x + w * 0.45)} ${r(y + h * 0.2)} ` +
277
+ `A ${r(w * 0.2)} ${r(b)} 0 0 1 ${r(x + w * 0.8)} ${r(y + h * 0.25)} ` +
278
+ `A ${r(w * 0.18)} ${r(b)} 0 0 1 ${r(x + w * 0.85)} ${r(bottom)} ` +
279
+ `Z`
280
+ );
281
+ }
282
+ case 'heart': {
283
+ const topY = y + h * 0.3;
284
+ return (
285
+ `M ${r(cx)} ${r(bottom)} ` +
286
+ `C ${r(x - w * 0.1)} ${r(cy)} ${r(x)} ${r(y)} ${r(cx)} ${r(topY)} ` +
287
+ `C ${r(right)} ${r(y)} ${r(right + w * 0.1)} ${r(cy)} ${r(cx)} ${r(bottom)} ` +
288
+ `Z`
289
+ );
290
+ }
291
+ case 'lightning':
292
+ return poly([
293
+ [x + w * 0.55, y],
294
+ [x + w * 0.2, y + h * 0.55],
295
+ [x + w * 0.45, y + h * 0.55],
296
+ [x + w * 0.3, bottom],
297
+ [right, y + h * 0.4],
298
+ [x + w * 0.55, y + h * 0.4],
299
+ ]);
300
+ default:
301
+ return null;
302
+ }
303
+ }
304
+
305
+ /** Block arrow (body rectangle + triangular head) pointing in `dir`. */
306
+ function blockArrow(
307
+ x: number,
308
+ y: number,
309
+ w: number,
310
+ h: number,
311
+ dir: 'right' | 'left' | 'up' | 'down',
312
+ ): string {
313
+ const right = x + w;
314
+ const bottom = y + h;
315
+ const cx = x + w / 2;
316
+ const cy = y + h / 2;
317
+ if (dir === 'right' || dir === 'left') {
318
+ const head = w * 0.4;
319
+ const bodyT = h * 0.5;
320
+ const ty = cy - bodyT / 2;
321
+ const by = cy + bodyT / 2;
322
+ if (dir === 'right') {
323
+ return poly([
324
+ [x, ty],
325
+ [right - head, ty],
326
+ [right - head, y],
327
+ [right, cy],
328
+ [right - head, bottom],
329
+ [right - head, by],
330
+ [x, by],
331
+ ]);
332
+ }
333
+ return poly([
334
+ [right, ty],
335
+ [x + head, ty],
336
+ [x + head, y],
337
+ [x, cy],
338
+ [x + head, bottom],
339
+ [x + head, by],
340
+ [right, by],
341
+ ]);
342
+ }
343
+ const head = h * 0.4;
344
+ const bodyT = w * 0.5;
345
+ const lx = cx - bodyT / 2;
346
+ const rxp = cx + bodyT / 2;
347
+ if (dir === 'down') {
348
+ return poly([
349
+ [lx, y],
350
+ [rxp, y],
351
+ [rxp, bottom - head],
352
+ [right, bottom - head],
353
+ [cx, bottom],
354
+ [x, bottom - head],
355
+ [lx, bottom - head],
356
+ ]);
357
+ }
358
+ return poly([
359
+ [lx, bottom],
360
+ [rxp, bottom],
361
+ [rxp, y + head],
362
+ [right, y + head],
363
+ [cx, y],
364
+ [x, y + head],
365
+ [lx, y + head],
366
+ ]);
367
+ }
368
+
369
+ // ============================================
370
+ // End markers
371
+ // ============================================
372
+
373
+ /**
374
+ * The `<marker>` path for a line end-style, in the 0–10 marker viewBox.
375
+ * `dir` selects orientation (`end` points along +x; `start` is mirrored).
376
+ * Returns null for `'none'`. `filled` shapes paint with the stroke color;
377
+ * non-filled (`open`) draw as a stroked outline.
378
+ */
379
+ export function markerPath(
380
+ style: MarkerStyle,
381
+ dir: 'start' | 'end',
382
+ ): { d: string; filled: boolean } | null {
383
+ const mirror = dir === 'start';
384
+ const mx = (n: number) => (mirror ? 10 - n : n);
385
+ const tri = (closed: boolean) => `M ${mx(0)} 0 L ${mx(10)} 5 L ${mx(0)} 10${closed ? ' z' : ''}`;
386
+ switch (style) {
387
+ case 'arrow':
388
+ return { d: tri(true), filled: true };
389
+ case 'open':
390
+ return { d: tri(false), filled: false };
391
+ case 'diamond':
392
+ return { d: 'M 0 5 L 5 0 L 10 5 L 5 10 z', filled: true };
393
+ case 'square':
394
+ return { d: 'M 1 1 L 9 1 L 9 9 L 1 9 z', filled: true };
395
+ case 'circle':
396
+ return { d: 'M 0 5 A 5 5 0 1 0 10 5 A 5 5 0 1 0 0 5 z', filled: true };
397
+ case 'none':
398
+ default:
399
+ return null;
400
+ }
401
+ }
402
+
403
+ // ============================================
404
+ // Connector routing
405
+ // ============================================
406
+
407
+ export interface ClipBox {
408
+ cx: number;
409
+ cy: number;
410
+ rx: number;
411
+ ry: number;
412
+ }
413
+
414
+ /** Intersection of the center-to-center line with `from`'s bounding box. */
415
+ export function clipPoint(from: ClipBox, to: ClipBox): { x: number; y: number } {
416
+ const dx = to.cx - from.cx;
417
+ const dy = to.cy - from.cy;
418
+ if (dx === 0 && dy === 0) return { x: from.cx, y: from.cy };
419
+ const sx = Math.abs(dx) > 0 ? from.rx / Math.abs(dx) : Infinity;
420
+ const sy = Math.abs(dy) > 0 ? from.ry / Math.abs(dy) : Infinity;
421
+ const s = Math.min(sx, sy);
422
+ return { x: from.cx + dx * s, y: from.cy + dy * s };
423
+ }
424
+
425
+ /** Clip both endpoints of an edge to the two shapes it joins. */
426
+ export function clipEndpoints(
427
+ a: ClipBox,
428
+ b: ClipBox,
429
+ ): { start: { x: number; y: number }; end: { x: number; y: number } } {
430
+ return { start: clipPoint(a, b), end: clipPoint(b, a) };
431
+ }
432
+
433
+ /** Build the SVG `d` for a connector between two clipped endpoints. */
434
+ export function connectorPath(
435
+ routing: ConnectorRouting,
436
+ start: { x: number; y: number },
437
+ end: { x: number; y: number },
438
+ ): string {
439
+ if (routing === 'straight') {
440
+ return `M ${r(start.x)} ${r(start.y)} L ${r(end.x)} ${r(end.y)}`;
441
+ }
442
+ if (routing === 'orthogonal') {
443
+ const midX = (start.x + end.x) / 2;
444
+ return `M ${r(start.x)} ${r(start.y)} L ${r(midX)} ${r(start.y)} L ${r(midX)} ${r(end.y)} L ${r(end.x)} ${r(end.y)}`;
445
+ }
446
+ // curved
447
+ const dx = Math.abs(end.x - start.x);
448
+ const cp = Math.max(40, dx / 2);
449
+ return `M ${r(start.x)} ${r(start.y)} C ${r(start.x + cp)} ${r(start.y)}, ${r(end.x - cp)} ${r(end.y)}, ${r(end.x)} ${r(end.y)}`;
450
+ }
451
+
452
+ /** Map a `lineStyle` (solid|dashed|dotted) to an SVG `stroke-dasharray`. */
453
+ export function lineStyleDasharray(style: string | undefined): string | undefined {
454
+ switch (style?.toLowerCase()) {
455
+ case 'dashed':
456
+ case 'dash':
457
+ return '8 6';
458
+ case 'dotted':
459
+ case 'dot':
460
+ return '2 6';
461
+ default:
462
+ return undefined;
463
+ }
464
+ }
@@ -8,8 +8,9 @@
8
8
 
9
9
  import type { TemplateContext } from '../../schemas/BlockTemplates.js';
10
10
  import type { ThemeColorScheme, RenderStyle } from '../../schemas/Theme.js';
11
- import type { AnimationType } from '../../schemas/Doc.js';
11
+ import type { Animation, AnimationType, ImageTreatment } from '../../schemas/Doc.js';
12
12
  import { resolveFontFamily } from '../../schemas/fontStacks.js';
13
+ import { oklchDarken, withAlpha } from '../../schemas/colorUtils.js';
13
14
 
14
15
  // ============================================
15
16
  // Color Scheme Resolution
@@ -123,6 +124,30 @@ export function getDefaultAnimation(
123
124
  return layerType === 'text' ? rs.defaultTextAnimation : rs.defaultImageAnimation;
124
125
  }
125
126
 
127
+ /**
128
+ * Theme-aware entrance animation for a template layer.
129
+ *
130
+ * The theme's `renderStyle.defaultTextAnimation` / `defaultImageAnimation`
131
+ * overrides the entrance *type*; the template keeps its authored duration,
132
+ * delay, and stagger. Templates opt in per layer:
133
+ *
134
+ * ```ts
135
+ * animation: themedEntrance(context, 'text', { type: 'fadeIn', duration: 2 })
136
+ * ```
137
+ *
138
+ * `slowZoom` is excluded — it's an ambient treatment handled by the
139
+ * `applyRenderStyleToLayers` post-pass, not an entrance.
140
+ */
141
+ export function themedEntrance(
142
+ context: TemplateContext,
143
+ layerType: 'text' | 'image',
144
+ fallback: Animation,
145
+ ): Animation {
146
+ const type = getDefaultAnimation(context, layerType);
147
+ if (!type || type === 'slowZoom' || type === fallback.type) return fallback;
148
+ return { ...fallback, type };
149
+ }
150
+
126
151
  // ============================================
127
152
  // Style Helpers
128
153
  // ============================================
@@ -142,6 +167,55 @@ export function getOverlayOpacity(context: TemplateContext): number {
142
167
  return context.theme.style.overlayOpacity ?? 0.5;
143
168
  }
144
169
 
170
+ /**
171
+ * Resolve the effective photographic treatment for a block's imagery.
172
+ *
173
+ * Precedence: the block's `imageTreatment` override ('none' opts out, a
174
+ * type forces that grade) → the theme's `style.imageTreatment` → none.
175
+ * Duotone tints default to the theme primary. Templates pass the result
176
+ * straight into `ImageLayer.content.treatment`.
177
+ */
178
+ export function themedImageTreatment(
179
+ context: TemplateContext,
180
+ override?: 'none' | 'mono' | 'duotone' | 'warm' | 'cool',
181
+ ): ImageTreatment | undefined {
182
+ if (override === 'none') return undefined;
183
+ const themeTreatment = context.theme.style.imageTreatment;
184
+ const base: ImageTreatment | undefined = override
185
+ ? { type: override, strength: themeTreatment?.strength }
186
+ : themeTreatment;
187
+ if (!base || base.type === 'none') return undefined;
188
+ if (base.type === 'duotone' && !base.color) {
189
+ return { ...base, color: context.theme.colors.primary };
190
+ }
191
+ return base;
192
+ }
193
+
194
+ /**
195
+ * Theme-derived surface gradient for text-first templates.
196
+ *
197
+ * Runs from the theme's alternate surface into a slightly deepened
198
+ * background, staying inside the theme's own hue. Templates must use
199
+ * this instead of hard-coding a dark endpoint (`#0f1520`, `#1e2030`, …):
200
+ * hard-coded navy endpoints made light themes render dark panels and
201
+ * warm themes drift cold halfway down the block.
202
+ */
203
+ export function themedSurfaceGradient(context: TemplateContext, angleDeg = 160): string {
204
+ const { background, backgroundLight } = context.theme.colors;
205
+ return `linear-gradient(${angleDeg}deg, ${backgroundLight} 0%, ${oklchDarken(background, 0.04)} 100%)`;
206
+ }
207
+
208
+ /**
209
+ * Translucent overlay tint derived from the theme background, for text
210
+ * bands/scrims painted over imagery. Light themes get a light scrim with
211
+ * dark theme text on top; dark themes get the familiar dark scrim —
212
+ * `theme.colors.text` stays legible on it by construction.
213
+ */
214
+ export function themedScrim(context: TemplateContext, alpha?: number): string {
215
+ const a = alpha ?? Math.max(0.55, getOverlayOpacity(context));
216
+ return withAlpha(context.theme.colors.background, a);
217
+ }
218
+
145
219
  // ============================================
146
220
  // Template Hint Access
147
221
  // ============================================