create-substrate 0.1.3 → 0.1.5

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 (39) hide show
  1. package/README.md +53 -0
  2. package/dist/index.js +1 -1
  3. package/dist/scaffold.d.ts.map +1 -1
  4. package/dist/scaffold.js +68 -4
  5. package/dist/scaffold.js.map +1 -1
  6. package/dist/surfaces/3d-scene.d.ts.map +1 -1
  7. package/dist/surfaces/3d-scene.js +414 -67
  8. package/dist/surfaces/3d-scene.js.map +1 -1
  9. package/dist/surfaces/animation.d.ts.map +1 -1
  10. package/dist/surfaces/animation.js +95 -103
  11. package/dist/surfaces/animation.js.map +1 -1
  12. package/dist/surfaces/blank.d.ts.map +1 -1
  13. package/dist/surfaces/blank.js +28 -16
  14. package/dist/surfaces/blank.js.map +1 -1
  15. package/dist/surfaces/canvas-2d.d.ts.map +1 -1
  16. package/dist/surfaces/canvas-2d.js +369 -92
  17. package/dist/surfaces/canvas-2d.js.map +1 -1
  18. package/dist/surfaces/data-vis.d.ts.map +1 -1
  19. package/dist/surfaces/data-vis.js +262 -98
  20. package/dist/surfaces/data-vis.js.map +1 -1
  21. package/dist/surfaces/image-gen.d.ts.map +1 -1
  22. package/dist/surfaces/image-gen.js +354 -92
  23. package/dist/surfaces/image-gen.js.map +1 -1
  24. package/dist/surfaces/node-editor.d.ts.map +1 -1
  25. package/dist/surfaces/node-editor.js +559 -114
  26. package/dist/surfaces/node-editor.js.map +1 -1
  27. package/package.json +4 -2
  28. package/skills/3d-scene/SKILL.md +6 -0
  29. package/skills/animation/SKILL.md +6 -0
  30. package/skills/canvas-2d/SKILL.md +7 -1
  31. package/skills/composing-panels/SKILL.md +14 -8
  32. package/skills/create-custom-tool/SKILL.md +6 -0
  33. package/skills/data-visualisation/SKILL.md +6 -0
  34. package/skills/image-generation/SKILL.md +6 -0
  35. package/skills/install-components/SKILL.md +199 -0
  36. package/skills/scaffold-playground/SKILL.md +12 -6
  37. package/skills/substrate-controls/SKILL.md +1 -3
  38. package/skills/theming/SKILL.md +6 -0
  39. package/skills/wire-interactions/SKILL.md +6 -0
@@ -1,25 +1,18 @@
1
1
  export const animation = {
2
2
  components: [
3
- "toolbar",
4
3
  "panel",
5
4
  "pane",
6
5
  "timeline",
6
+ "animation-store",
7
7
  "easing-curve-editor",
8
- "slider",
8
+ "easing",
9
9
  "number-input",
10
- "history-controls",
11
10
  "status-bar",
12
11
  ],
13
12
  extraDeps: {
14
13
  gsap: "^3.12.0",
15
14
  },
16
- app: () => `import { useState, useRef, useEffect } from "react"
17
- import {
18
- Toolbar,
19
- ToolbarToggleGroup,
20
- ToolbarToggle,
21
- ToolbarSeparator,
22
- } from "@/components/substrate/toolbar"
15
+ app: () => `import { useEffect, useState } from "react"
23
16
  import { Panel } from "@/components/substrate/panel"
24
17
  import {
25
18
  Pane,
@@ -27,18 +20,19 @@ import {
27
20
  PaneLabel,
28
21
  PaneContent,
29
22
  } from "@/components/substrate/pane"
30
- import { Timeline } from "@/components/substrate/timeline"
23
+ import {
24
+ Timeline,
25
+ TimelineRuler,
26
+ TimelinePlayhead,
27
+ TimelineTrack,
28
+ TimelineKeyframe,
29
+ TimelineTransport,
30
+ } from "@/components/substrate/timeline"
31
31
  import { EasingCurveEditor } from "@/components/substrate/easing-curve-editor"
32
32
  import { NumberInput } from "@/components/substrate/number-input"
33
- import { Slider } from "@/components/substrate/slider"
34
- import { HistoryControls } from "@/components/substrate/history-controls"
35
33
  import { StatusBar, StatusBarSection, StatusBarItem } from "@/components/substrate/status-bar"
36
- import {
37
- RiPlayLine,
38
- RiPauseLine,
39
- RiStopLine,
40
- RiSkipBackLine,
41
- } from "@remixicon/react"
34
+ import { useAnimationStore } from "@/stores/substrate/animation-store"
35
+ import { evaluateCubicBezier, type CubicBezier } from "@/lib/substrate/easing"
42
36
 
43
37
  interface Keyframe {
44
38
  id: string
@@ -52,14 +46,15 @@ interface Track {
52
46
  keyframes: Keyframe[]
53
47
  }
54
48
 
55
- const INITIAL_TRACKS: Track[] = [
49
+ // Keyframe times are in seconds, matching the animation store
50
+ const TRACKS: Track[] = [
56
51
  {
57
52
  id: "position-x",
58
53
  label: "Position X",
59
54
  keyframes: [
60
- { id: "k1", time: 0, value: 50 },
61
- { id: "k2", time: 30, value: 300 },
62
- { id: "k3", time: 60, value: 150 },
55
+ { id: "k1", time: 0, value: -150 },
56
+ { id: "k2", time: 2.5, value: 150 },
57
+ { id: "k3", time: 5, value: -150 },
63
58
  ],
64
59
  },
65
60
  {
@@ -67,53 +62,52 @@ const INITIAL_TRACKS: Track[] = [
67
62
  label: "Opacity",
68
63
  keyframes: [
69
64
  { id: "k4", time: 0, value: 100 },
70
- { id: "k5", time: 20, value: 30 },
71
- { id: "k6", time: 50, value: 100 },
65
+ { id: "k5", time: 1.5, value: 30 },
66
+ { id: "k6", time: 4, value: 100 },
72
67
  ],
73
68
  },
74
69
  ]
75
70
 
76
71
  export default function App() {
77
- const [playing, setPlaying] = useState(false)
78
- const [currentFrame, setCurrentFrame] = useState(0)
79
- const [totalFrames] = useState(60)
80
- const [tracks] = useState(INITIAL_TRACKS)
81
- const [easing, setEasing] = useState<[number, number, number, number]>([0.4, 0, 0.2, 1])
82
- const animRef = useRef<number | null>(null)
83
- const boxRef = useRef<HTMLDivElement>(null)
72
+ const duration = useAnimationStore((s) => s.duration)
73
+ const currentTime = useAnimationStore((s) => s.currentTime)
74
+ const isPlaying = useAnimationStore((s) => s.isPlaying)
75
+ const zoomLevel = useAnimationStore((s) => s.zoomLevel)
76
+ const selectedKeyframeIds = useAnimationStore((s) => s.selectedKeyframeIds)
77
+ const selectKeyframe = useAnimationStore((s) => s.selectKeyframe)
78
+ const setCurrentTime = useAnimationStore((s) => s.setCurrentTime)
79
+ const setDuration = useAnimationStore((s) => s.setDuration)
80
+ const [easing, setEasing] = useState<CubicBezier>({ x1: 0.4, y1: 0, x2: 0.2, y2: 1 })
84
81
 
82
+ // Advance the store's clock while playing, looping at the end
85
83
  useEffect(() => {
86
- if (!playing) {
87
- if (animRef.current) cancelAnimationFrame(animRef.current)
88
- return
89
- }
84
+ if (!isPlaying) return
90
85
 
91
- let frame = currentFrame
92
- const tick = () => {
93
- frame = (frame + 1) % totalFrames
94
- setCurrentFrame(frame)
95
- animRef.current = requestAnimationFrame(tick)
96
- }
97
- animRef.current = requestAnimationFrame(tick)
98
- return () => {
99
- if (animRef.current) cancelAnimationFrame(animRef.current)
86
+ let frame = 0
87
+ let last = performance.now()
88
+ const tick = (now: number) => {
89
+ const state = useAnimationStore.getState()
90
+ const next = state.currentTime + (now - last) / 1000
91
+ last = now
92
+ state.setCurrentTime(next >= state.duration ? 0 : next)
93
+ frame = requestAnimationFrame(tick)
100
94
  }
101
- }, [playing])
95
+ frame = requestAnimationFrame(tick)
96
+ return () => cancelAnimationFrame(frame)
97
+ }, [isPlaying])
102
98
 
103
- // Interpolate position and opacity from keyframes
104
- const posX = interpolate(tracks[0].keyframes, currentFrame)
105
- const opacity = interpolate(tracks[1].keyframes, currentFrame) / 100
99
+ const posX = interpolate(TRACKS[0].keyframes, currentTime, easing)
100
+ const opacity = interpolate(TRACKS[1].keyframes, currentTime, easing) / 100
106
101
 
107
102
  return (
108
103
  <div className="flex h-screen flex-col bg-background text-foreground">
109
- <div className="relative flex flex-1 overflow-hidden">
104
+ <div className="flex flex-1 overflow-hidden">
110
105
  {/* Preview stage */}
111
106
  <div className="flex flex-1 items-center justify-center bg-canvas">
112
107
  <div
113
- ref={boxRef}
114
108
  className="size-20 rounded-lg bg-primary"
115
109
  style={{
116
- transform: \`translateX(\${posX - 150}px)\`,
110
+ transform: \`translateX(\${posX}px)\`,
117
111
  opacity,
118
112
  }}
119
113
  />
@@ -130,80 +124,78 @@ export default function App() {
130
124
  </Pane>
131
125
  <Pane>
132
126
  <PaneHeader>
133
- <PaneLabel>Keyframe</PaneLabel>
127
+ <PaneLabel>Playback</PaneLabel>
134
128
  </PaneHeader>
135
129
  <PaneContent>
136
130
  <div className="grid grid-cols-2 gap-2">
137
- <NumberInput label="Frame" value={currentFrame} onChange={setCurrentFrame} min={0} max={totalFrames} />
138
- <div>
139
- <label className="text-xs font-medium">Value</label>
140
- <NumberInput value={0} onChange={() => {}} />
141
- </div>
131
+ <NumberInput
132
+ label="T"
133
+ value={currentTime}
134
+ onChange={setCurrentTime}
135
+ min={0}
136
+ max={duration}
137
+ step={0.1}
138
+ suffix="s"
139
+ />
140
+ <NumberInput
141
+ label="D"
142
+ value={duration}
143
+ onChange={setDuration}
144
+ min={1}
145
+ max={30}
146
+ step={0.5}
147
+ suffix="s"
148
+ />
142
149
  </div>
143
150
  </PaneContent>
144
151
  </Pane>
145
152
  </Panel>
146
153
  </div>
147
154
 
148
- {/* Timeline */}
149
- <div className="h-48 border-t border-border bg-panel">
150
- <Timeline
151
- tracks={tracks}
152
- currentFrame={currentFrame}
153
- totalFrames={totalFrames}
154
- onFrameChange={setCurrentFrame}
155
- />
156
- </div>
157
-
158
- <Toolbar>
159
- <ToolbarToggle
160
- value="back"
161
- tooltip="Go to start"
162
- onPressedChange={() => { setCurrentFrame(0); setPlaying(false) }}
163
- >
164
- <RiSkipBackLine className="size-4" />
165
- </ToolbarToggle>
166
- <ToolbarToggle
167
- value="play"
168
- pressed={playing}
169
- tooltip={playing ? "Pause" : "Play"}
170
- shortcut="Space"
171
- onPressedChange={() => setPlaying(!playing)}
172
- >
173
- {playing ? <RiPauseLine className="size-4" /> : <RiPlayLine className="size-4" />}
174
- </ToolbarToggle>
175
- <ToolbarToggle
176
- value="stop"
177
- tooltip="Stop"
178
- onPressedChange={() => { setPlaying(false); setCurrentFrame(0) }}
179
- >
180
- <RiStopLine className="size-4" />
181
- </ToolbarToggle>
182
- </Toolbar>
155
+ <Timeline>
156
+ <TimelineRuler duration={duration} zoomLevel={zoomLevel} />
157
+ <div className="relative flex-1 overflow-y-auto">
158
+ {TRACKS.map((track) => (
159
+ <TimelineTrack key={track.id} label={track.label}>
160
+ {track.keyframes.map((keyframe) => (
161
+ <TimelineKeyframe
162
+ key={keyframe.id}
163
+ time={keyframe.time}
164
+ duration={duration}
165
+ zoomLevel={zoomLevel}
166
+ selected={selectedKeyframeIds.has(keyframe.id)}
167
+ onSelect={() => selectKeyframe(keyframe.id)}
168
+ />
169
+ ))}
170
+ </TimelineTrack>
171
+ ))}
172
+ <TimelinePlayhead time={currentTime} duration={duration} zoomLevel={zoomLevel} />
173
+ </div>
174
+ <TimelineTransport />
175
+ </Timeline>
183
176
 
184
177
  <StatusBar>
185
178
  <StatusBarSection>
186
- <StatusBarItem>Frame: {currentFrame} / {totalFrames}</StatusBarItem>
187
- <StatusBarItem>State: {playing ? "Playing" : "Stopped"}</StatusBarItem>
188
- </StatusBarSection>
189
- <StatusBarSection>
190
- <HistoryControls size="sm" />
179
+ <StatusBarItem>Time: {currentTime.toFixed(2)}s / {duration.toFixed(2)}s</StatusBarItem>
180
+ <StatusBarItem>State: {isPlaying ? "Playing" : "Stopped"}</StatusBarItem>
191
181
  </StatusBarSection>
192
182
  </StatusBar>
193
183
  </div>
194
184
  )
195
185
  }
196
186
 
197
- function interpolate(keyframes: Keyframe[], frame: number): number {
187
+ // Value at a given time, easing between the surrounding keyframes
188
+ function interpolate(keyframes: Keyframe[], time: number, easing: CubicBezier): number {
198
189
  if (keyframes.length === 0) return 0
199
- if (frame <= keyframes[0].time) return keyframes[0].value
200
- if (frame >= keyframes[keyframes.length - 1].time) return keyframes[keyframes.length - 1].value
190
+ if (time <= keyframes[0].time) return keyframes[0].value
191
+ const last = keyframes[keyframes.length - 1]
192
+ if (time >= last.time) return last.value
201
193
 
202
194
  for (let i = 0; i < keyframes.length - 1; i++) {
203
195
  const a = keyframes[i]
204
196
  const b = keyframes[i + 1]
205
- if (frame >= a.time && frame <= b.time) {
206
- const t = (frame - a.time) / (b.time - a.time)
197
+ if (time >= a.time && time <= b.time) {
198
+ const t = evaluateCubicBezier(easing, (time - a.time) / (b.time - a.time))
207
199
  return a.value + (b.value - a.value) * t
208
200
  }
209
201
  }
@@ -1 +1 @@
1
- {"version":3,"file":"animation.js","sourceRoot":"","sources":["../../src/surfaces/animation.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,SAAS,GAAkB;IACtC,UAAU,EAAE;QACV,SAAS;QACT,OAAO;QACP,MAAM;QACN,UAAU;QACV,qBAAqB;QACrB,QAAQ;QACR,cAAc;QACd,kBAAkB;QAClB,YAAY;KACb;IACD,SAAS,EAAE;QACT,IAAI,EAAE,SAAS;KAChB;IACD,GAAG,EAAE,GAAG,EAAE,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoMZ;CACA,CAAA"}
1
+ {"version":3,"file":"animation.js","sourceRoot":"","sources":["../../src/surfaces/animation.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,SAAS,GAAkB;IACtC,UAAU,EAAE;QACV,OAAO;QACP,MAAM;QACN,UAAU;QACV,iBAAiB;QACjB,qBAAqB;QACrB,QAAQ;QACR,cAAc;QACd,YAAY;KACb;IACD,SAAS,EAAE;QACT,IAAI,EAAE,SAAS;KAChB;IACD,GAAG,EAAE,GAAG,EAAE,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6LZ;CACA,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"blank.d.ts","sourceRoot":"","sources":["../../src/surfaces/blank.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C,eAAO,MAAM,KAAK,EAAE,aAsEnB,CAAA"}
1
+ {"version":3,"file":"blank.d.ts","sourceRoot":"","sources":["../../src/surfaces/blank.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C,eAAO,MAAM,KAAK,EAAE,aAkFnB,CAAA"}
@@ -12,11 +12,14 @@ import {
12
12
  Toolbar,
13
13
  ToolbarToggleGroup,
14
14
  ToolbarToggle,
15
- ToolbarSeparator,
16
15
  } from "@/components/substrate/toolbar"
17
16
  import { Panel } from "@/components/substrate/panel"
18
17
  import { Surface } from "@/components/substrate/surface"
19
- import { EmptyState } from "@/components/substrate/empty-state"
18
+ import {
19
+ EmptyState,
20
+ EmptyStateTitle,
21
+ EmptyStateDescription,
22
+ } from "@/components/substrate/empty-state"
20
23
  import { StatusBar, StatusBarSection, StatusBarItem } from "@/components/substrate/status-bar"
21
24
  import { RiCursorLine, RiHand } from "@remixicon/react"
22
25
 
@@ -27,24 +30,33 @@ export default function App() {
27
30
  <div className="flex h-screen flex-col bg-background text-foreground">
28
31
  <div className="relative flex flex-1 overflow-hidden">
29
32
  <Panel position="left">
30
- <EmptyState
31
- title="Left panel"
32
- description="Add layers, assets, or navigation here"
33
- />
33
+ <EmptyState>
34
+ <EmptyStateTitle>Left panel</EmptyStateTitle>
35
+ <EmptyStateDescription>
36
+ Add layers, assets, or navigation here
37
+ </EmptyStateDescription>
38
+ </EmptyState>
34
39
  </Panel>
35
40
 
36
- <Surface className="flex-1">
37
- <EmptyState
38
- title="Your surface here"
39
- description="Replace this with your rendering engine — canvas, Three.js, timeline, or anything else"
40
- />
41
- </Surface>
41
+ {/* Surface fills its nearest positioned parent */}
42
+ <div className="relative flex-1">
43
+ <Surface>
44
+ <EmptyState>
45
+ <EmptyStateTitle>Your surface here</EmptyStateTitle>
46
+ <EmptyStateDescription>
47
+ Replace this with your rendering engine — canvas, Three.js, timeline, or anything else
48
+ </EmptyStateDescription>
49
+ </EmptyState>
50
+ </Surface>
51
+ </div>
42
52
 
43
53
  <Panel position="right">
44
- <EmptyState
45
- title="Right panel"
46
- description="Add property controls and inspectors here"
47
- />
54
+ <EmptyState>
55
+ <EmptyStateTitle>Right panel</EmptyStateTitle>
56
+ <EmptyStateDescription>
57
+ Add property controls and inspectors here
58
+ </EmptyStateDescription>
59
+ </EmptyState>
48
60
  </Panel>
49
61
  </div>
50
62
 
@@ -1 +1 @@
1
- {"version":3,"file":"blank.js","sourceRoot":"","sources":["../../src/surfaces/blank.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,KAAK,GAAkB;IAClC,UAAU,EAAE;QACV,SAAS;QACT,OAAO;QACP,MAAM;QACN,SAAS;QACT,aAAa;QACb,YAAY;KACb;IACD,GAAG,EAAE,GAAG,EAAE,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4DZ;CACA,CAAA"}
1
+ {"version":3,"file":"blank.js","sourceRoot":"","sources":["../../src/surfaces/blank.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,KAAK,GAAkB;IAClC,UAAU,EAAE;QACV,SAAS;QACT,OAAO;QACP,MAAM;QACN,SAAS;QACT,aAAa;QACb,YAAY;KACb;IACD,GAAG,EAAE,GAAG,EAAE,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwEZ;CACA,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"canvas-2d.d.ts","sourceRoot":"","sources":["../../src/surfaces/canvas-2d.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C,eAAO,MAAM,QAAQ,EAAE,aA8ItB,CAAA"}
1
+ {"version":3,"file":"canvas-2d.d.ts","sourceRoot":"","sources":["../../src/surfaces/canvas-2d.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C,eAAO,MAAM,QAAQ,EAAE,aAmatB,CAAA"}