@pixel-point/toolcraft 0.0.2

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 (257) hide show
  1. package/LICENSE.md +98 -0
  2. package/README.md +41 -0
  3. package/bin/create-toolcraft-app.mjs +8 -0
  4. package/bin/toolcraft.mjs +8 -0
  5. package/package.json +24 -0
  6. package/scripts/prepare-pack.mjs +29 -0
  7. package/src/cli.mjs +392 -0
  8. package/src/cli.test.mjs +284 -0
  9. package/src/copy-recursive.mjs +86 -0
  10. package/src/generate.mjs +212 -0
  11. package/src/generate.test.mjs +322 -0
  12. package/src/import-map.mjs +14 -0
  13. package/src/package-json.mjs +80 -0
  14. package/src/package-json.test.mjs +67 -0
  15. package/src/rewrite-imports.mjs +85 -0
  16. package/src/rewrite-imports.test.mjs +58 -0
  17. package/templates/runtime/contracts/component-contracts.test.ts +1165 -0
  18. package/templates/runtime/contracts/component-contracts.ts +1340 -0
  19. package/templates/runtime/contracts/decision-contracts.test.ts +206 -0
  20. package/templates/runtime/contracts/decision-contracts.ts +283 -0
  21. package/templates/runtime/contracts/index.test.ts +14 -0
  22. package/templates/runtime/contracts/index.ts +3 -0
  23. package/templates/runtime/contracts/types.ts +56 -0
  24. package/templates/runtime/export/export.test.ts +203 -0
  25. package/templates/runtime/export/export.ts +132 -0
  26. package/templates/runtime/export/index.ts +1 -0
  27. package/templates/runtime/index.ts +14 -0
  28. package/templates/runtime/react/canvas-shell.test.tsx +424 -0
  29. package/templates/runtime/react/canvas-shell.tsx +408 -0
  30. package/templates/runtime/react/control-renderers.ts +31 -0
  31. package/templates/runtime/react/controls-panel.test.tsx +3736 -0
  32. package/templates/runtime/react/controls-panel.tsx +2327 -0
  33. package/templates/runtime/react/curve-geometry.test.ts +70 -0
  34. package/templates/runtime/react/index.ts +15 -0
  35. package/templates/runtime/react/layer-tree.ts +96 -0
  36. package/templates/runtime/react/layers-panel.test.tsx +487 -0
  37. package/templates/runtime/react/layers-panel.tsx +1348 -0
  38. package/templates/runtime/react/media-file.ts +82 -0
  39. package/templates/runtime/react/panel-host-config.ts +80 -0
  40. package/templates/runtime/react/panel-host-geometry.test.ts +66 -0
  41. package/templates/runtime/react/panel-host-geometry.ts +109 -0
  42. package/templates/runtime/react/panel-host-types.ts +74 -0
  43. package/templates/runtime/react/panel-host.test.tsx +102 -0
  44. package/templates/runtime/react/panel-host.tsx +353 -0
  45. package/templates/runtime/react/runtime-public-api.test.tsx +132 -0
  46. package/templates/runtime/react/settings-transfer.test.ts +150 -0
  47. package/templates/runtime/react/settings-transfer.ts +279 -0
  48. package/templates/runtime/react/storage-key-migration.ts +48 -0
  49. package/templates/runtime/react/theme-runtime.tsx +177 -0
  50. package/templates/runtime/react/timeline-panel.test.tsx +668 -0
  51. package/templates/runtime/react/timeline-panel.tsx +2953 -0
  52. package/templates/runtime/react/toolbar-panel.test.tsx +212 -0
  53. package/templates/runtime/react/toolbar-panel.tsx +205 -0
  54. package/templates/runtime/react/toolcraft-app.integration.test.tsx +350 -0
  55. package/templates/runtime/react/toolcraft-app.test.tsx +339 -0
  56. package/templates/runtime/react/toolcraft-app.tsx +81 -0
  57. package/templates/runtime/react/toolcraft-root.test.tsx +347 -0
  58. package/templates/runtime/react/toolcraft-root.tsx +203 -0
  59. package/templates/runtime/react/use-toolcraft.ts +41 -0
  60. package/templates/runtime/schema/define-toolcraft.test.ts +1524 -0
  61. package/templates/runtime/schema/define-toolcraft.ts +1442 -0
  62. package/templates/runtime/schema/keyframe-capability.test.ts +90 -0
  63. package/templates/runtime/schema/keyframe-capability.ts +51 -0
  64. package/templates/runtime/schema/runtime-targets.ts +40 -0
  65. package/templates/runtime/schema/types.ts +370 -0
  66. package/templates/runtime/state/canvas-zoom.ts +8 -0
  67. package/templates/runtime/state/create-template-state.test.ts +242 -0
  68. package/templates/runtime/state/create-template-state.ts +95 -0
  69. package/templates/runtime/state/keyframe-evaluation.test.ts +141 -0
  70. package/templates/runtime/state/keyframe-evaluation.ts +203 -0
  71. package/templates/runtime/state/persistence.test.ts +217 -0
  72. package/templates/runtime/state/persistence.ts +511 -0
  73. package/templates/runtime/state/reducer.test.ts +937 -0
  74. package/templates/runtime/state/reducer.ts +1212 -0
  75. package/templates/runtime/state/timeline-readiness.ts +43 -0
  76. package/templates/runtime/state/types.ts +242 -0
  77. package/templates/runtime/styles.css +125 -0
  78. package/templates/runtime/testing/performance.test.ts +1058 -0
  79. package/templates/runtime/testing/performance.ts +1078 -0
  80. package/templates/starter/AGENTS.md +186 -0
  81. package/templates/starter/LICENSE.md +98 -0
  82. package/templates/starter/NOTICE.md +8 -0
  83. package/templates/starter/docs/toolcraft/README.md +41 -0
  84. package/templates/starter/docs/toolcraft/acceptance-testing.md +205 -0
  85. package/templates/starter/docs/toolcraft/agent-worklog.md +81 -0
  86. package/templates/starter/docs/toolcraft/assembly-workflow.md +206 -0
  87. package/templates/starter/docs/toolcraft/component-rules.md +299 -0
  88. package/templates/starter/docs/toolcraft/custom-controls.md +71 -0
  89. package/templates/starter/docs/toolcraft/decision-contract.md +71 -0
  90. package/templates/starter/docs/toolcraft/performance.md +112 -0
  91. package/templates/starter/docs/toolcraft/renderer-technique.md +48 -0
  92. package/templates/starter/docs/toolcraft/schema-reference.md +265 -0
  93. package/templates/starter/docs/toolcraft/workflow.md +87 -0
  94. package/templates/starter/e2e/app-browser-acceptance.spec.ts +785 -0
  95. package/templates/starter/e2e/app-controls.spec.ts +41 -0
  96. package/templates/starter/e2e/app-performance.spec.ts +326 -0
  97. package/templates/starter/e2e/canvas-handle-helpers.ts +244 -0
  98. package/templates/starter/e2e/performance-helpers.ts +612 -0
  99. package/templates/starter/e2e/product-observable-helpers.ts +170 -0
  100. package/templates/starter/index.html +12 -0
  101. package/templates/starter/package.json +52 -0
  102. package/templates/starter/playwright.config.ts +43 -0
  103. package/templates/starter/scripts/check-ai-skills.mjs +95 -0
  104. package/templates/starter/scripts/check-toolcraft-docs.mjs +159 -0
  105. package/templates/starter/scripts/check-toolcraft-integrity.mjs +232 -0
  106. package/templates/starter/scripts/run-vite-on-free-port.mjs +48 -0
  107. package/templates/starter/scripts/toolcraft-port.mjs +54 -0
  108. package/templates/starter/scripts/toolcraft-port.test.mjs +73 -0
  109. package/templates/starter/src/app/starter-acceptance.test.ts +5959 -0
  110. package/templates/starter/src/app/starter-acceptance.ts +2646 -0
  111. package/templates/starter/src/app/starter-performance.test.ts +1390 -0
  112. package/templates/starter/src/app/starter-performance.ts +12 -0
  113. package/templates/starter/src/app/starter-schema.test.ts +70 -0
  114. package/templates/starter/src/app/starter-schema.ts +15 -0
  115. package/templates/starter/src/main.tsx +18 -0
  116. package/templates/starter/src/router.tsx +16 -0
  117. package/templates/starter/src/routes/index.tsx +7 -0
  118. package/templates/starter/src/routes/root.tsx +19 -0
  119. package/templates/starter/src/styles.css +120 -0
  120. package/templates/starter/tsconfig.json +11 -0
  121. package/templates/starter/vite.config.ts +13 -0
  122. package/templates/ui/components/composites/accordion.tsx +73 -0
  123. package/templates/ui/components/composites/alert-dialog.tsx +190 -0
  124. package/templates/ui/components/composites/alert.tsx +74 -0
  125. package/templates/ui/components/composites/aspect-ratio.tsx +22 -0
  126. package/templates/ui/components/composites/avatar.tsx +98 -0
  127. package/templates/ui/components/composites/badge.tsx +69 -0
  128. package/templates/ui/components/composites/breadcrumb.tsx +106 -0
  129. package/templates/ui/components/composites/card.tsx +91 -0
  130. package/templates/ui/components/composites/combobox.tsx +486 -0
  131. package/templates/ui/components/composites/command.tsx +296 -0
  132. package/templates/ui/components/composites/context-menu.tsx +247 -0
  133. package/templates/ui/components/composites/dialog.tsx +282 -0
  134. package/templates/ui/components/composites/dropdown-menu.tsx +299 -0
  135. package/templates/ui/components/composites/empty.tsx +110 -0
  136. package/templates/ui/components/composites/hover-card.tsx +44 -0
  137. package/templates/ui/components/composites/index.ts +30 -0
  138. package/templates/ui/components/composites/menubar.tsx +214 -0
  139. package/templates/ui/components/composites/navigation-menu.tsx +167 -0
  140. package/templates/ui/components/composites/pagination.tsx +131 -0
  141. package/templates/ui/components/composites/progress.tsx +72 -0
  142. package/templates/ui/components/composites/radio-group.tsx +84 -0
  143. package/templates/ui/components/composites/resizable.tsx +42 -0
  144. package/templates/ui/components/composites/sheet.tsx +153 -0
  145. package/templates/ui/components/composites/sidebar-structural.tsx +310 -0
  146. package/templates/ui/components/composites/sidebar.tsx +431 -0
  147. package/templates/ui/components/composites/sonner.tsx +35 -0
  148. package/templates/ui/components/composites/spinner.tsx +43 -0
  149. package/templates/ui/components/composites/table.tsx +108 -0
  150. package/templates/ui/components/composites/tabs.tsx +83 -0
  151. package/templates/ui/components/control-layout/index.tsx +437 -0
  152. package/templates/ui/components/controls/actions/actions-control.tsx +139 -0
  153. package/templates/ui/components/controls/actions/index.ts +9 -0
  154. package/templates/ui/components/controls/anchor-grid/anchor-grid-control.tsx +107 -0
  155. package/templates/ui/components/controls/anchor-grid/index.ts +4 -0
  156. package/templates/ui/components/controls/boolean/boolean-controls.tsx +79 -0
  157. package/templates/ui/components/controls/boolean/index.ts +4 -0
  158. package/templates/ui/components/controls/channel-mixer/channel-mixer-control.tsx +95 -0
  159. package/templates/ui/components/controls/channel-mixer/index.ts +4 -0
  160. package/templates/ui/components/controls/channel-tabs/channel-tabs.tsx +42 -0
  161. package/templates/ui/components/controls/channel-tabs/index.ts +6 -0
  162. package/templates/ui/components/controls/code-textarea/code-textarea-control.tsx +90 -0
  163. package/templates/ui/components/controls/code-textarea/index.ts +4 -0
  164. package/templates/ui/components/controls/color/color-control.tsx +571 -0
  165. package/templates/ui/components/controls/color/color-picker-popover.tsx +104 -0
  166. package/templates/ui/components/controls/color/index.ts +41 -0
  167. package/templates/ui/components/controls/color/palette-control-data.ts +436 -0
  168. package/templates/ui/components/controls/color/palette-control.tsx +535 -0
  169. package/templates/ui/components/controls/color/style-guide-color-picker-channel-utils.ts +162 -0
  170. package/templates/ui/components/controls/color/style-guide-color-picker-interactions.ts +190 -0
  171. package/templates/ui/components/controls/color/style-guide-color-picker-logic.ts +485 -0
  172. package/templates/ui/components/controls/color/style-guide-color-picker-parts.tsx +710 -0
  173. package/templates/ui/components/controls/color/style-guide-color-picker.tsx +503 -0
  174. package/templates/ui/components/controls/control-types.ts +43 -0
  175. package/templates/ui/components/controls/curves/curve-geometry.ts +355 -0
  176. package/templates/ui/components/controls/curves/curve-graph.tsx +390 -0
  177. package/templates/ui/components/controls/curves/curves-control.tsx +445 -0
  178. package/templates/ui/components/controls/curves/index.ts +6 -0
  179. package/templates/ui/components/controls/file-drop/file-drop-control.tsx +191 -0
  180. package/templates/ui/components/controls/file-drop/index.ts +5 -0
  181. package/templates/ui/components/controls/font-picker/font-catalog.json +15360 -0
  182. package/templates/ui/components/controls/font-picker/font-catalog.ts +116 -0
  183. package/templates/ui/components/controls/font-picker/font-picker-control.tsx +1202 -0
  184. package/templates/ui/components/controls/font-picker/font-preview-loader.ts +336 -0
  185. package/templates/ui/components/controls/font-picker/index.ts +24 -0
  186. package/templates/ui/components/controls/font-picker/use-hover-intent.ts +46 -0
  187. package/templates/ui/components/controls/gradient/gradient-control-utils.ts +190 -0
  188. package/templates/ui/components/controls/gradient/gradient-control.tsx +612 -0
  189. package/templates/ui/components/controls/gradient/gradient-stop-list.tsx +400 -0
  190. package/templates/ui/components/controls/gradient/gradient-toolbar.tsx +152 -0
  191. package/templates/ui/components/controls/gradient/index.ts +4 -0
  192. package/templates/ui/components/controls/image-picker/image-picker-control.tsx +139 -0
  193. package/templates/ui/components/controls/image-picker/index.ts +7 -0
  194. package/templates/ui/components/controls/index.ts +192 -0
  195. package/templates/ui/components/controls/range-input/index.ts +4 -0
  196. package/templates/ui/components/controls/range-input/range-input-control.tsx +173 -0
  197. package/templates/ui/components/controls/range-slider/index.ts +4 -0
  198. package/templates/ui/components/controls/range-slider/range-slider-control.tsx +122 -0
  199. package/templates/ui/components/controls/range-slider/range-slider-value.ts +61 -0
  200. package/templates/ui/components/controls/segmented/index.ts +8 -0
  201. package/templates/ui/components/controls/segmented/segmented-control.tsx +94 -0
  202. package/templates/ui/components/controls/select/index.ts +4 -0
  203. package/templates/ui/components/controls/select/select-control.tsx +223 -0
  204. package/templates/ui/components/controls/slider/index.ts +4 -0
  205. package/templates/ui/components/controls/slider/slider-control.tsx +150 -0
  206. package/templates/ui/components/controls/slider/slider-value.ts +56 -0
  207. package/templates/ui/components/controls/text-input/index.ts +4 -0
  208. package/templates/ui/components/controls/text-input/text-input-control.tsx +158 -0
  209. package/templates/ui/components/controls/use-measured-element-width.ts +42 -0
  210. package/templates/ui/components/controls/vector/index.ts +8 -0
  211. package/templates/ui/components/controls/vector/vector-control.tsx +401 -0
  212. package/templates/ui/components/panel/index.ts +19 -0
  213. package/templates/ui/components/panel/panel-actions.tsx +165 -0
  214. package/templates/ui/components/panel/panel-header.tsx +61 -0
  215. package/templates/ui/components/panel/panel-icon-button.tsx +96 -0
  216. package/templates/ui/components/panel/panel-section.tsx +168 -0
  217. package/templates/ui/components/panel/panel-surface.tsx +206 -0
  218. package/templates/ui/components/panel/panel.tsx +210 -0
  219. package/templates/ui/components/primitives/animated-loader.tsx +61 -0
  220. package/templates/ui/components/primitives/button-group.tsx +134 -0
  221. package/templates/ui/components/primitives/button.tsx +429 -0
  222. package/templates/ui/components/primitives/checkbox.tsx +62 -0
  223. package/templates/ui/components/primitives/editable-slider-value-label.tsx +337 -0
  224. package/templates/ui/components/primitives/field.tsx +225 -0
  225. package/templates/ui/components/primitives/index.ts +82 -0
  226. package/templates/ui/components/primitives/input-group.tsx +298 -0
  227. package/templates/ui/components/primitives/input.tsx +61 -0
  228. package/templates/ui/components/primitives/internal/button-loading.tsx +178 -0
  229. package/templates/ui/components/primitives/label.tsx +16 -0
  230. package/templates/ui/components/primitives/popover.tsx +126 -0
  231. package/templates/ui/components/primitives/portal-layer-context.tsx +33 -0
  232. package/templates/ui/components/primitives/primitive-arrow-icon.tsx +38 -0
  233. package/templates/ui/components/primitives/scroll-fade-logic.ts +441 -0
  234. package/templates/ui/components/primitives/scroll-fade-render.tsx +75 -0
  235. package/templates/ui/components/primitives/scroll-fade-types.ts +41 -0
  236. package/templates/ui/components/primitives/scroll-fade.tsx +72 -0
  237. package/templates/ui/components/primitives/select.tsx +408 -0
  238. package/templates/ui/components/primitives/selection-state.ts +31 -0
  239. package/templates/ui/components/primitives/separator.tsx +21 -0
  240. package/templates/ui/components/primitives/slider/index.ts +4 -0
  241. package/templates/ui/components/primitives/slider/slider-interaction.tsx +96 -0
  242. package/templates/ui/components/primitives/slider/slider-parts.tsx +303 -0
  243. package/templates/ui/components/primitives/slider/slider-reset.ts +152 -0
  244. package/templates/ui/components/primitives/slider/slider-value.ts +114 -0
  245. package/templates/ui/components/primitives/slider/slider.tsx +511 -0
  246. package/templates/ui/components/primitives/switch.tsx +35 -0
  247. package/templates/ui/components/primitives/textarea.tsx +49 -0
  248. package/templates/ui/components/primitives/toggle-group.tsx +114 -0
  249. package/templates/ui/components/primitives/toggle.tsx +46 -0
  250. package/templates/ui/components/primitives/tooltip.tsx +100 -0
  251. package/templates/ui/hooks/use-mobile.ts +21 -0
  252. package/templates/ui/index.ts +31 -0
  253. package/templates/ui/lib/control-outline.ts +3 -0
  254. package/templates/ui/lib/input-control-style.ts +131 -0
  255. package/templates/ui/lib/style-guide-color-utils.ts +111 -0
  256. package/templates/ui/lib/utils.ts +6 -0
  257. package/templates/ui/styles.css +291 -0
@@ -0,0 +1,242 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { defineToolcraft } from "../schema/define-toolcraft";
4
+ import { createToolcraftState } from "./create-template-state";
5
+
6
+ describe("createToolcraftState", () => {
7
+ it("initializes values from control defaults by target", () => {
8
+ const app = defineToolcraft({
9
+ canvas: { enabled: true },
10
+ panels: {
11
+ controls: {
12
+ sections: [
13
+ {
14
+ controls: {
15
+ opacity: {
16
+ defaultValue: 75,
17
+ target: "selectedLayer.opacity",
18
+ type: "slider",
19
+ },
20
+ prompt: {
21
+ defaultValue: "",
22
+ target: "generation.prompt",
23
+ type: "textarea",
24
+ },
25
+ },
26
+ },
27
+ ],
28
+ title: "Controls",
29
+ },
30
+ },
31
+ });
32
+
33
+ const state = createToolcraftState(app);
34
+
35
+ expect(state.values).toEqual({
36
+ "generation.prompt": "",
37
+ "selectedLayer.opacity": 75,
38
+ });
39
+ expect(state.defaults).toEqual(state.values);
40
+ expect(state.values).not.toBe(state.defaults);
41
+ });
42
+
43
+ it("initializes editable canvas size control defaults only for editable output apps", () => {
44
+ const app = defineToolcraft({
45
+ canvas: { enabled: true, sizing: { mode: "editable-output" } },
46
+ panels: {
47
+ controls: {
48
+ sections: [],
49
+ title: "Controls",
50
+ },
51
+ },
52
+ });
53
+
54
+ const state = createToolcraftState(app);
55
+
56
+ expect(state.values).toMatchObject({
57
+ "canvas.size.height": 1024,
58
+ "canvas.size.width": 1024,
59
+ });
60
+ });
61
+
62
+ it("initializes canvas state from resolved canvas size", () => {
63
+ const size = { width: 1200, height: 900, unit: "px" } as const;
64
+ const app = defineToolcraft({
65
+ canvas: { enabled: true, size },
66
+ panels: {},
67
+ });
68
+
69
+ const state = createToolcraftState(app);
70
+
71
+ expect(state.canvas.size).toEqual(size);
72
+ expect(state.canvas.zoom).toBe(70);
73
+ });
74
+
75
+ it("preserves seeded canvas width", () => {
76
+ const app = defineToolcraft({
77
+ canvas: { enabled: true },
78
+ panels: {},
79
+ });
80
+
81
+ const state = createToolcraftState(app, {
82
+ canvas: {
83
+ size: { width: 640, height: 900, unit: "px" },
84
+ },
85
+ });
86
+
87
+ expect(state.canvas.size).toEqual({ width: 640, height: 900, unit: "px" });
88
+ });
89
+
90
+ it("initializes default panel offsets", () => {
91
+ const app = defineToolcraft({
92
+ canvas: { enabled: true },
93
+ panels: {},
94
+ });
95
+
96
+ const state = createToolcraftState(app);
97
+
98
+ expect(state.panels).toEqual({
99
+ controls: { offset: { x: 0, y: 0 } },
100
+ layers: { offset: { x: 0, y: 0 } },
101
+ timeline: { offset: { x: 0, y: 0 } },
102
+ toolbar: { offset: { x: 0, y: 0 } },
103
+ });
104
+ });
105
+
106
+ it("initializes media and layer state as empty", () => {
107
+ const app = defineToolcraft({
108
+ canvas: { enabled: true },
109
+ panels: {},
110
+ });
111
+
112
+ const state = createToolcraftState(app);
113
+
114
+ expect(state.layers).toEqual([]);
115
+ expect(state.mediaAssets).toEqual([]);
116
+ expect(state.selectedLayerId).toBeNull();
117
+ });
118
+
119
+ it("accepts seeded runtime layer state", () => {
120
+ const app = defineToolcraft({
121
+ canvas: { enabled: true },
122
+ panels: {},
123
+ });
124
+
125
+ const state = createToolcraftState(app, {
126
+ layers: [
127
+ {
128
+ displayName: "Layer 1",
129
+ id: "layer-1",
130
+ kind: "layer",
131
+ name: "layer-1",
132
+ visible: true,
133
+ },
134
+ ],
135
+ selectedLayerId: "layer-1",
136
+ });
137
+
138
+ expect(state.layers).toHaveLength(1);
139
+ expect(state.layers[0]?.displayName).toBe("Layer 1");
140
+ expect(state.selectedLayerId).toBe("layer-1");
141
+ expect(state.history.undo).toEqual([]);
142
+ });
143
+
144
+ it("creates timeline runtime state", () => {
145
+ const app = defineToolcraft({
146
+ canvas: { enabled: true },
147
+ panels: {},
148
+ });
149
+
150
+ const state = createToolcraftState(app, {
151
+ timeline: {
152
+ currentTimeSeconds: 2,
153
+ durationSeconds: 10,
154
+ isPlaying: false,
155
+ },
156
+ });
157
+
158
+ expect(state.timeline.currentTimeSeconds).toBe(2);
159
+ expect(state.timeline.durationSeconds).toBe(10);
160
+ expect(state.timeline.expanded).toBe(false);
161
+ expect(state.timeline.isLooping).toBe(true);
162
+ expect(state.timeline.isPlaying).toBe(false);
163
+ expect(state.timeline.keyframeGroups).toEqual([]);
164
+ });
165
+
166
+ it("keeps upload-dependent intrinsic-media timelines paused until media exists", () => {
167
+ const app = defineToolcraft({
168
+ canvas: {
169
+ enabled: true,
170
+ sizing: { mode: "intrinsic-media" },
171
+ upload: true,
172
+ },
173
+ panels: {
174
+ timeline: { mode: "playback" },
175
+ },
176
+ });
177
+
178
+ const state = createToolcraftState(app);
179
+
180
+ expect(state.timeline.currentTimeSeconds).toBe(0);
181
+ expect(state.timeline.isPlaying).toBe(false);
182
+ });
183
+
184
+ it("allows upload-dependent intrinsic-media timelines to start ready when media is seeded", () => {
185
+ const app = defineToolcraft({
186
+ canvas: {
187
+ enabled: true,
188
+ sizing: { mode: "intrinsic-media" },
189
+ upload: true,
190
+ },
191
+ panels: {
192
+ timeline: { mode: "playback" },
193
+ },
194
+ });
195
+
196
+ const state = createToolcraftState(app, {
197
+ mediaAssets: [
198
+ {
199
+ dataUrl: "data:image/png;base64,AAAA",
200
+ fileName: "source.png",
201
+ id: "media-1",
202
+ layerId: "layer-1",
203
+ mimeType: "image/png",
204
+ position: { x: 0, y: 0 },
205
+ size: { height: 320, unit: "px", width: 512 },
206
+ },
207
+ ],
208
+ });
209
+
210
+ expect(state.timeline.isPlaying).toBe(true);
211
+ });
212
+
213
+ it("accepts seeded timeline keyframes without making them runtime defaults", () => {
214
+ const app = defineToolcraft({
215
+ canvas: { enabled: true },
216
+ panels: {},
217
+ });
218
+
219
+ const state = createToolcraftState(app, {
220
+ timeline: {
221
+ keyframeGroups: [
222
+ {
223
+ controlId: "opacity",
224
+ keyframes: [
225
+ {
226
+ controlId: "opacity",
227
+ controlLabel: "Opacity",
228
+ easing: { controlPoints: [0.65, 0, 0.35, 1], type: "bezier" },
229
+ id: "opacity-0",
230
+ timeSeconds: 0,
231
+ valueLabel: "Opacity 75%",
232
+ },
233
+ ],
234
+ label: "Opacity",
235
+ },
236
+ ],
237
+ },
238
+ });
239
+
240
+ expect(state.timeline.keyframeGroups.map((group) => group.controlId)).toEqual(["opacity"]);
241
+ });
242
+ });
@@ -0,0 +1,95 @@
1
+ import type { ResolvedToolcraftAppSchema } from "../schema/types";
2
+ import type {
3
+ ToolcraftInitialState,
4
+ ToolcraftState,
5
+ ToolcraftTimelineKeyframeGroup,
6
+ ToolcraftTimelineState,
7
+ } from "./types";
8
+ import { toolcraftCanvasZoomDefault } from "./canvas-zoom";
9
+ import { getMediaReadyTimelineState } from "./timeline-readiness";
10
+
11
+ function cloneTimelineKeyframeGroups(
12
+ keyframeGroups: readonly ToolcraftTimelineKeyframeGroup[],
13
+ ): ToolcraftTimelineKeyframeGroup[] {
14
+ return keyframeGroups.map((group) => ({
15
+ ...group,
16
+ keyframes: group.keyframes.map((keyframe) => ({
17
+ ...keyframe,
18
+ easing:
19
+ keyframe.easing?.type === "bezier"
20
+ ? {
21
+ controlPoints: [...keyframe.easing.controlPoints],
22
+ type: "bezier",
23
+ }
24
+ : keyframe.easing,
25
+ })),
26
+ }));
27
+ }
28
+
29
+ function createDefaultTimelineState(
30
+ timeline?: Partial<ToolcraftTimelineState>,
31
+ ): ToolcraftTimelineState {
32
+ return {
33
+ currentTimeSeconds: 0,
34
+ durationSeconds: 8,
35
+ expanded: false,
36
+ isLooping: true,
37
+ isPlaying: true,
38
+ selectedKeyframeId: null,
39
+ ...timeline,
40
+ keyframeGroups: cloneTimelineKeyframeGroups(timeline?.keyframeGroups ?? []),
41
+ };
42
+ }
43
+
44
+ export function createToolcraftState(
45
+ schema: ResolvedToolcraftAppSchema,
46
+ initialState: ToolcraftInitialState = {},
47
+ ): ToolcraftState {
48
+ const defaults: Record<string, unknown> = {};
49
+ const mediaAssets = initialState.mediaAssets ?? [];
50
+ const timeline = getMediaReadyTimelineState(
51
+ schema,
52
+ createDefaultTimelineState(initialState.timeline),
53
+ mediaAssets,
54
+ );
55
+
56
+ for (const section of schema.panels.controls?.sections ?? []) {
57
+ for (const control of Object.values(section.controls)) {
58
+ defaults[control.target] = control.defaultValue;
59
+ }
60
+ }
61
+
62
+ const panels: ToolcraftState["panels"] = {
63
+ controls: { offset: { x: 0, y: 0 } },
64
+ layers: { offset: { x: 0, y: 0 } },
65
+ timeline: { offset: { x: 0, y: 0 } },
66
+ toolbar: { offset: { x: 0, y: 0 } },
67
+ };
68
+ const initialCanvas = {
69
+ offset: { x: 0, y: 0 },
70
+ size: schema.canvas.size,
71
+ zoom: toolcraftCanvasZoomDefault,
72
+ ...initialState.canvas,
73
+ };
74
+
75
+ return {
76
+ canvas: initialCanvas,
77
+ defaults,
78
+ history: {
79
+ redo: [],
80
+ undo: [],
81
+ },
82
+ layers: initialState.layers ?? [],
83
+ mediaAssets,
84
+ panels: {
85
+ controls: { ...panels.controls, ...initialState.panels?.controls },
86
+ layers: { ...panels.layers, ...initialState.panels?.layers },
87
+ timeline: { ...panels.timeline, ...initialState.panels?.timeline },
88
+ toolbar: { ...panels.toolbar, ...initialState.panels?.toolbar },
89
+ },
90
+ schema,
91
+ selectedLayerId: initialState.selectedLayerId ?? null,
92
+ timeline,
93
+ values: { ...defaults, ...initialState.values },
94
+ };
95
+ }
@@ -0,0 +1,141 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { defineToolcraft } from "../schema/define-toolcraft";
4
+ import { createToolcraftState } from "./create-template-state";
5
+ import {
6
+ evaluateToolcraftTimelineValue,
7
+ evaluateToolcraftTimelineValues,
8
+ } from "./keyframe-evaluation";
9
+
10
+ function createKeyframedState() {
11
+ const app = defineToolcraft({
12
+ canvas: { enabled: true },
13
+ panels: {
14
+ controls: {
15
+ sections: [
16
+ {
17
+ controls: {
18
+ opacity: {
19
+ defaultValue: 0,
20
+ target: "shape.opacity",
21
+ type: "slider",
22
+ },
23
+ position: {
24
+ defaultValue: { x: 0, y: 0 },
25
+ target: "shape.position",
26
+ type: "vector",
27
+ },
28
+ preset: {
29
+ defaultValue: "linear",
30
+ options: [
31
+ { label: "Linear", value: "linear" },
32
+ { label: "Radial", value: "radial" },
33
+ ],
34
+ target: "shape.preset",
35
+ type: "select",
36
+ },
37
+ },
38
+ },
39
+ ],
40
+ title: "Controls",
41
+ },
42
+ timeline: { mode: "keyframes" },
43
+ },
44
+ });
45
+
46
+ return createToolcraftState(app, {
47
+ timeline: {
48
+ currentTimeSeconds: 4,
49
+ durationSeconds: 8,
50
+ keyframeGroups: [
51
+ {
52
+ controlId: "shape.opacity",
53
+ keyframes: [
54
+ {
55
+ controlId: "shape.opacity",
56
+ controlLabel: "Opacity",
57
+ id: "shape.opacity::0",
58
+ timeSeconds: 0,
59
+ value: 0,
60
+ valueLabel: "0%",
61
+ },
62
+ {
63
+ controlId: "shape.opacity",
64
+ controlLabel: "Opacity",
65
+ id: "shape.opacity::8",
66
+ timeSeconds: 8,
67
+ value: 100,
68
+ valueLabel: "100%",
69
+ },
70
+ ],
71
+ label: "Opacity",
72
+ },
73
+ {
74
+ controlId: "shape.position",
75
+ keyframes: [
76
+ {
77
+ controlId: "shape.position",
78
+ controlLabel: "Position",
79
+ id: "shape.position::0",
80
+ timeSeconds: 0,
81
+ value: { x: 0, y: 10 },
82
+ valueLabel: "0.00, 10.00",
83
+ },
84
+ {
85
+ controlId: "shape.position",
86
+ controlLabel: "Position",
87
+ id: "shape.position::8",
88
+ timeSeconds: 8,
89
+ value: { x: 100, y: 30 },
90
+ valueLabel: "100.00, 30.00",
91
+ },
92
+ ],
93
+ label: "Position",
94
+ },
95
+ {
96
+ controlId: "shape.preset",
97
+ keyframes: [
98
+ {
99
+ controlId: "shape.preset",
100
+ controlLabel: "Preset",
101
+ id: "shape.preset::0",
102
+ timeSeconds: 0,
103
+ value: "linear",
104
+ valueLabel: "Linear",
105
+ },
106
+ {
107
+ controlId: "shape.preset",
108
+ controlLabel: "Preset",
109
+ id: "shape.preset::8",
110
+ timeSeconds: 8,
111
+ value: "radial",
112
+ valueLabel: "Radial",
113
+ },
114
+ ],
115
+ label: "Preset",
116
+ },
117
+ ],
118
+ },
119
+ });
120
+ }
121
+
122
+ describe("Toolcraft keyframe evaluation", () => {
123
+ it("evaluates typed keyframe values at the current timeline time", () => {
124
+ const state = createKeyframedState();
125
+ const values = evaluateToolcraftTimelineValues(state);
126
+ const position = values["shape.position"] as { x: number; y: number };
127
+
128
+ expect(values["shape.opacity"]).toBeCloseTo(50, 5);
129
+ expect(position.x).toBeCloseTo(50, 5);
130
+ expect(position.y).toBeCloseTo(20, 5);
131
+ expect(values["shape.preset"]).toBe("linear");
132
+ });
133
+
134
+ it("evaluates a single target and falls back to raw values without typed keyframes", () => {
135
+ const state = createKeyframedState();
136
+
137
+ expect(evaluateToolcraftTimelineValue(state, "shape.opacity", 8)).toBe(100);
138
+ expect(evaluateToolcraftTimelineValue(state, "shape.preset", 8)).toBe("radial");
139
+ expect(evaluateToolcraftTimelineValue(state, "missing.target", 4)).toBeUndefined();
140
+ });
141
+ });
@@ -0,0 +1,203 @@
1
+ import type {
2
+ ToolcraftState,
3
+ ToolcraftTimelineBezierControlPoints,
4
+ ToolcraftTimelineKeyframe,
5
+ ToolcraftTimelineKeyframeEasing,
6
+ ToolcraftTimelineKeyframeGroup,
7
+ } from "./types";
8
+
9
+ const defaultTimelineKeyframeEasing: ToolcraftTimelineKeyframeEasing = {
10
+ controlPoints: [0.65, 0, 0.35, 1],
11
+ type: "bezier",
12
+ };
13
+
14
+ function clampUnit(value: number): number {
15
+ if (!Number.isFinite(value)) {
16
+ return 0;
17
+ }
18
+
19
+ return Math.max(0, Math.min(1, value));
20
+ }
21
+
22
+ function getBezierPoint(
23
+ time: number,
24
+ firstControlPoint: number,
25
+ secondControlPoint: number,
26
+ ): number {
27
+ const inverseTime = 1 - time;
28
+
29
+ return (
30
+ 3 * inverseTime * inverseTime * time * firstControlPoint +
31
+ 3 * inverseTime * time * time * secondControlPoint +
32
+ time * time * time
33
+ );
34
+ }
35
+
36
+ function getBezierYForX(
37
+ progress: number,
38
+ [x1, y1, x2, y2]: ToolcraftTimelineBezierControlPoints,
39
+ ): number {
40
+ let min = 0;
41
+ let max = 1;
42
+ let time = progress;
43
+
44
+ for (let index = 0; index < 30; index += 1) {
45
+ time = (min + max) / 2;
46
+
47
+ if (getBezierPoint(time, x1, x2) < progress) {
48
+ min = time;
49
+ } else {
50
+ max = time;
51
+ }
52
+ }
53
+
54
+ return clampUnit(getBezierPoint(time, y1, y2));
55
+ }
56
+
57
+ function easeProgress(
58
+ progress: number,
59
+ easing: ToolcraftTimelineKeyframeEasing | undefined,
60
+ ): number {
61
+ const clampedProgress = clampUnit(progress);
62
+ const resolvedEasing = easing ?? defaultTimelineKeyframeEasing;
63
+
64
+ if (resolvedEasing.type === "step") {
65
+ return clampedProgress >= 1 ? 1 : 0;
66
+ }
67
+
68
+ return getBezierYForX(clampedProgress, resolvedEasing.controlPoints);
69
+ }
70
+
71
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
72
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
73
+ return false;
74
+ }
75
+
76
+ const prototype = Object.getPrototypeOf(value);
77
+
78
+ return prototype === Object.prototype || prototype === null;
79
+ }
80
+
81
+ function interpolateToolcraftValue(
82
+ fromValue: unknown,
83
+ toValue: unknown,
84
+ progress: number,
85
+ ): unknown {
86
+ if (typeof fromValue === "number" && typeof toValue === "number") {
87
+ return fromValue + (toValue - fromValue) * progress;
88
+ }
89
+
90
+ if (Array.isArray(fromValue) && Array.isArray(toValue) && fromValue.length === toValue.length) {
91
+ return fromValue.map((item, index) =>
92
+ interpolateToolcraftValue(item, toValue[index], progress),
93
+ );
94
+ }
95
+
96
+ if (isPlainRecord(fromValue) && isPlainRecord(toValue)) {
97
+ const fromKeys = Object.keys(fromValue);
98
+ const toKeys = Object.keys(toValue);
99
+
100
+ if (
101
+ fromKeys.length === toKeys.length &&
102
+ fromKeys.every((key) => Object.prototype.hasOwnProperty.call(toValue, key))
103
+ ) {
104
+ return Object.fromEntries(
105
+ fromKeys.map((key) => [
106
+ key,
107
+ interpolateToolcraftValue(fromValue[key], toValue[key], progress),
108
+ ]),
109
+ );
110
+ }
111
+ }
112
+
113
+ return progress >= 1 ? toValue : fromValue;
114
+ }
115
+
116
+ function getKeyframeRuntimeValue(keyframe: ToolcraftTimelineKeyframe): unknown {
117
+ return "value" in keyframe ? keyframe.value : undefined;
118
+ }
119
+
120
+ function getEvaluatedTimelineGroupValue(
121
+ group: ToolcraftTimelineKeyframeGroup,
122
+ timeSeconds: number,
123
+ fallbackValue: unknown,
124
+ ): unknown {
125
+ const keyframes = group.keyframes
126
+ .filter((keyframe) => "value" in keyframe)
127
+ .sort((first, second) => first.timeSeconds - second.timeSeconds);
128
+
129
+ if (keyframes.length === 0) {
130
+ return fallbackValue;
131
+ }
132
+
133
+ const firstKeyframe = keyframes[0];
134
+ const lastKeyframe = keyframes[keyframes.length - 1];
135
+
136
+ if (!firstKeyframe || !lastKeyframe) {
137
+ return fallbackValue;
138
+ }
139
+
140
+ if (timeSeconds <= firstKeyframe.timeSeconds) {
141
+ return getKeyframeRuntimeValue(firstKeyframe);
142
+ }
143
+
144
+ if (timeSeconds >= lastKeyframe.timeSeconds) {
145
+ return getKeyframeRuntimeValue(lastKeyframe);
146
+ }
147
+
148
+ for (let index = 0; index < keyframes.length - 1; index += 1) {
149
+ const fromKeyframe = keyframes[index];
150
+ const toKeyframe = keyframes[index + 1];
151
+
152
+ if (!fromKeyframe || !toKeyframe) {
153
+ continue;
154
+ }
155
+
156
+ if (timeSeconds < fromKeyframe.timeSeconds || timeSeconds > toKeyframe.timeSeconds) {
157
+ continue;
158
+ }
159
+
160
+ const durationSeconds = toKeyframe.timeSeconds - fromKeyframe.timeSeconds;
161
+ const progress = durationSeconds <= 0 ? 1 : (timeSeconds - fromKeyframe.timeSeconds) / durationSeconds;
162
+ const easedProgress = easeProgress(progress, fromKeyframe.easing);
163
+
164
+ return interpolateToolcraftValue(
165
+ getKeyframeRuntimeValue(fromKeyframe),
166
+ getKeyframeRuntimeValue(toKeyframe),
167
+ easedProgress,
168
+ );
169
+ }
170
+
171
+ return fallbackValue;
172
+ }
173
+
174
+ export function evaluateToolcraftTimelineValue(
175
+ state: ToolcraftState,
176
+ target: string,
177
+ timeSeconds = state.timeline.currentTimeSeconds,
178
+ ): unknown {
179
+ const group = state.timeline.keyframeGroups.find((item) => item.controlId === target);
180
+
181
+ if (!group) {
182
+ return state.values[target];
183
+ }
184
+
185
+ return getEvaluatedTimelineGroupValue(group, timeSeconds, state.values[target]);
186
+ }
187
+
188
+ export function evaluateToolcraftTimelineValues(
189
+ state: ToolcraftState,
190
+ timeSeconds = state.timeline.currentTimeSeconds,
191
+ ): Record<string, unknown> {
192
+ const values = { ...state.values };
193
+
194
+ for (const group of state.timeline.keyframeGroups) {
195
+ values[group.controlId] = getEvaluatedTimelineGroupValue(
196
+ group,
197
+ timeSeconds,
198
+ state.values[group.controlId],
199
+ );
200
+ }
201
+
202
+ return values;
203
+ }