@vibecuting/video-project-core 0.1.30 → 0.1.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -3
- package/src/base/utils/background-audio-layer.tsx +1 -1
- package/src/base/utils/voice-captions-foreground.tsx +0 -1
- package/src/core/scene/photo/3dvr-photo/default-3dvr-photo-scene/default-3dvr-photo-scene.tsx +1 -1
- package/src/core/scene/photo/effects-photo/default-effects-photo-scene/default-effects-photo-scene.tsx +1 -1
- package/src/lint/index.ts +2 -0
- package/src/lint/scene-convention-lint.test.ts +86 -0
- package/src/lint/scene-convention-lint.ts +247 -0
- package/src/storybook/index.ts +19 -0
- package/src/storybook/remotion-preview.test.tsx +53 -0
- package/src/storybook/remotion-preview.tsx +45 -15
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibecuting/video-project-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.31",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"types": "src/index.ts",
|
|
8
8
|
"exports": {
|
|
9
|
-
".": "./src/index.ts"
|
|
9
|
+
".": "./src/index.ts",
|
|
10
|
+
"./storybook": "./src/storybook/index.ts",
|
|
11
|
+
"./storybook/remotion-preview": "./src/storybook/remotion-preview.tsx",
|
|
12
|
+
"./lint": "./src/lint/index.ts"
|
|
10
13
|
},
|
|
11
14
|
"publishConfig": {
|
|
12
15
|
"registry": "https://registry.npmjs.org",
|
|
@@ -22,7 +25,7 @@
|
|
|
22
25
|
"test": "pnpm exec vitest run"
|
|
23
26
|
},
|
|
24
27
|
"dependencies": {
|
|
25
|
-
"@vibecuting/component-project-helper": "0.1.
|
|
28
|
+
"@vibecuting/component-project-helper": "0.1.31",
|
|
26
29
|
"@remotion/transitions": "4.0.473",
|
|
27
30
|
"mermaid": "^11.12.2",
|
|
28
31
|
"react": "^19.0.0",
|
|
@@ -79,7 +79,7 @@ export const VideoBackgroundAudioLayer = ({
|
|
|
79
79
|
>
|
|
80
80
|
<Audio
|
|
81
81
|
src={resolveVideoLayerAssetSource(cue.source)}
|
|
82
|
-
volume={(frame) => getVideoLayerCueVolume(cue, frame)}
|
|
82
|
+
volume={(frame: number) => getVideoLayerCueVolume(cue, frame)}
|
|
83
83
|
/>
|
|
84
84
|
</Sequence>
|
|
85
85
|
))}
|
package/src/core/scene/photo/3dvr-photo/default-3dvr-photo-scene/default-3dvr-photo-scene.tsx
CHANGED
|
@@ -89,7 +89,7 @@ export const Default3DVRPhotoScene = VideoComponent(componentMetadata)(
|
|
|
89
89
|
const { fps } = useVideoConfig()
|
|
90
90
|
const portrait = runtime.height > runtime.width
|
|
91
91
|
const animated = sceneConfig.sceneType === 'animation'
|
|
92
|
-
const entrance = sceneConfig.sceneType === 'animation' ? spring({ frame
|
|
92
|
+
const entrance = sceneConfig.sceneType === 'animation' ? spring({ frame: Math.max(0, frame - 4), fps, config: { damping: 18, mass: 0.8, stiffness: 120 } }) : 1
|
|
93
93
|
const orbit = animated ? Math.sin((frame / fps) * Math.PI * 0.55) : 0
|
|
94
94
|
const cameraYaw = yaw + orbit * 4 + (1 - entrance) * (portrait ? -3 : -4)
|
|
95
95
|
const cameraPitch = pitch + Math.sin((frame / fps) * Math.PI * 0.35) * 1.5 + (1 - entrance) * 1.6
|
|
@@ -149,7 +149,7 @@ export const DefaultEffectsPhotoScene = VideoComponent(componentMetadata)(
|
|
|
149
149
|
config: { damping: 22, mass: 0.8, stiffness: 130 },
|
|
150
150
|
})
|
|
151
151
|
: 1
|
|
152
|
-
const reveal = sceneConfig.sceneType === 'animation' ? spring({ frame
|
|
152
|
+
const reveal = sceneConfig.sceneType === 'animation' ? spring({ frame: Math.max(0, frame - 6), fps, config: { damping: 20, mass: 0.7, stiffness: 150 } }) : 1
|
|
153
153
|
const focusDrift = sceneConfig.sceneType === 'animation' ? Math.sin((frame / fps) * Math.PI * 0.75) : 0
|
|
154
154
|
const scale = 1.01 + (1 - animationProgress) * 0.06 + focusDrift * 0.008
|
|
155
155
|
const translateX = (1 - animationProgress) * (portrait ? -8 : -12) + focusDrift * (portrait ? 8 : 12)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from 'vitest'
|
|
6
|
+
|
|
7
|
+
import { lintSceneConvention } from './scene-convention-lint'
|
|
8
|
+
|
|
9
|
+
function writeSceneFixture(rootDir: string) {
|
|
10
|
+
const folder = path.join(rootDir, 'src/components/default-component-scene')
|
|
11
|
+
const sharedFolder = path.join(rootDir, 'src/components/_shared')
|
|
12
|
+
fs.mkdirSync(folder, { recursive: true })
|
|
13
|
+
fs.mkdirSync(sharedFolder, { recursive: true })
|
|
14
|
+
|
|
15
|
+
fs.writeFileSync(
|
|
16
|
+
path.join(folder, 'default-component-scene.tsx'),
|
|
17
|
+
[
|
|
18
|
+
"import { z } from 'zod'",
|
|
19
|
+
"export const componentMetadata = { sourceFile: 'src/components/default-component-scene/default-component-scene.tsx' }",
|
|
20
|
+
"export const DefaultComponentScene = defineSceneComponent({ propsSchema: z.object({ title: z.string() }), component: function DefaultComponentScene() { useRemotionSceneRuntime(); return <SceneMotionLayer /> } })",
|
|
21
|
+
'',
|
|
22
|
+
].join('\n'),
|
|
23
|
+
)
|
|
24
|
+
fs.writeFileSync(
|
|
25
|
+
path.join(folder, 'default-component-scene-Landscape.tsx'),
|
|
26
|
+
"export const componentMetadata = { sourceFile: 'src/components/default-component-scene/default-component-scene-Landscape.tsx', aspectRatio: '16:9' }\n",
|
|
27
|
+
)
|
|
28
|
+
fs.writeFileSync(
|
|
29
|
+
path.join(folder, 'default-component-scene-Horizontal.tsx'),
|
|
30
|
+
"export const componentMetadata = { sourceFile: 'src/components/default-component-scene/default-component-scene-Horizontal.tsx', aspectRatio: '9:16' }\n",
|
|
31
|
+
)
|
|
32
|
+
fs.writeFileSync(
|
|
33
|
+
path.join(folder, 'default-component-scene.stories.tsx'),
|
|
34
|
+
"const meta = { title: 'components/default-component-scene/DefaultComponentSceneLandscape' }\nexport default meta\nexport const Default = { parameters: { scenePreviewAspectRatio: '16:9' } }\n",
|
|
35
|
+
)
|
|
36
|
+
fs.writeFileSync(
|
|
37
|
+
path.join(folder, 'default-component-scene.animated.stories.tsx'),
|
|
38
|
+
"const meta = { title: 'components/default-component-scene/DefaultComponentSceneLandscape' }\nexport default meta\nexport const Animated = { parameters: { scenePreviewAspectRatio: '16:9', scenePreviewMode: 'animation' } }\n",
|
|
39
|
+
)
|
|
40
|
+
fs.writeFileSync(
|
|
41
|
+
path.join(folder, 'default-component-scene-Horizontal.stories.tsx'),
|
|
42
|
+
"const meta = { title: 'components/default-component-scene/DefaultComponentSceneHorizontal' }\nexport default meta\nexport const Default = { parameters: { scenePreviewAspectRatio: '9:16' } }\n",
|
|
43
|
+
)
|
|
44
|
+
fs.writeFileSync(
|
|
45
|
+
path.join(folder, 'default-component-scene-Horizontal.animated.stories.tsx'),
|
|
46
|
+
"const meta = { title: 'components/default-component-scene/DefaultComponentSceneHorizontal' }\nexport default meta\nexport const Animated = { parameters: { scenePreviewAspectRatio: '9:16', scenePreviewMode: 'animation' } }\n",
|
|
47
|
+
)
|
|
48
|
+
fs.writeFileSync(path.join(folder, 'default-component-scene.test.tsx'), "import test from 'node:test'\ntest('ok', () => {})\n")
|
|
49
|
+
fs.writeFileSync(path.join(folder, 'index.ts'), "export * from './default-component-scene'\n")
|
|
50
|
+
fs.writeFileSync(path.join(sharedFolder, 'scene-frame.tsx'), 'export function SceneFrame() { return null }\n')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe('scene convention lint', () => {
|
|
54
|
+
it('passes on the expected folder structure', () => {
|
|
55
|
+
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'scene-convention-lint-'))
|
|
56
|
+
writeSceneFixture(rootDir)
|
|
57
|
+
|
|
58
|
+
const result = lintSceneConvention({
|
|
59
|
+
rootDir,
|
|
60
|
+
sourceRoot: 'src/components',
|
|
61
|
+
titleRoot: 'components',
|
|
62
|
+
sharedFolderName: '_shared',
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
expect(result.errors).toEqual([])
|
|
66
|
+
expect(result.checkedSceneFolders).toContain('src/components/default-component-scene')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('reports missing animation mode', () => {
|
|
70
|
+
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'scene-convention-lint-missing-animation-'))
|
|
71
|
+
writeSceneFixture(rootDir)
|
|
72
|
+
fs.writeFileSync(
|
|
73
|
+
path.join(rootDir, 'src/components/default-component-scene/default-component-scene.animated.stories.tsx'),
|
|
74
|
+
"const meta = { title: 'components/default-component-scene/DefaultComponentSceneLandscape' }\nexport default meta\nexport const Animated = { parameters: { scenePreviewAspectRatio: '16:9' } }\n",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
const result = lintSceneConvention({
|
|
78
|
+
rootDir,
|
|
79
|
+
sourceRoot: 'src/components',
|
|
80
|
+
titleRoot: 'components',
|
|
81
|
+
sharedFolderName: '_shared',
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
expect(result.errors.some((error) => error.includes("scenePreviewMode: 'animation'"))).toBe(true)
|
|
85
|
+
})
|
|
86
|
+
})
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
export type SceneConventionLintOptions = {
|
|
5
|
+
rootDir: string
|
|
6
|
+
sourceRoot: string
|
|
7
|
+
titleRoot: string
|
|
8
|
+
checkedFolderPrefix?: string
|
|
9
|
+
sharedFolderName?: string
|
|
10
|
+
fix?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type SceneConventionLintResult = {
|
|
14
|
+
checkedSceneFolders: string[]
|
|
15
|
+
checkedStoryFiles: string[]
|
|
16
|
+
errors: string[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const toPosix = (value: string) => value.split(path.sep).join('/')
|
|
20
|
+
|
|
21
|
+
function toRootRelative(rootDir: string, file: string) {
|
|
22
|
+
if (path.isAbsolute(file)) {
|
|
23
|
+
return toPosix(path.relative(rootDir, file))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return toPosix(file).replace(/^\.\//, '')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveRootPath(rootDir: string, file: string) {
|
|
30
|
+
return path.resolve(rootDir, toRootRelative(rootDir, file))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readText(rootDir: string, file: string) {
|
|
34
|
+
return fs.readFileSync(resolveRootPath(rootDir, file), 'utf8')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function writeText(rootDir: string, file: string, text: string) {
|
|
38
|
+
fs.writeFileSync(resolveRootPath(rootDir, file), text)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readStoryTitle(text: string) {
|
|
42
|
+
const titleMatch = text.match(/const\s+meta\s*=\s*{[\s\S]*?title:\s*['"`]([^'"`]+)['"`]/m)
|
|
43
|
+
return titleMatch?.[1] ?? ''
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function replaceStoryTitlePrefix(text: string, expectedPrefix: string) {
|
|
47
|
+
return text.replace(
|
|
48
|
+
/(const\s+meta\s*=\s*{[\s\S]*?title:\s*['"`])([^'"`]+)(['"`])/m,
|
|
49
|
+
(_match, before: string, currentTitle: string, after: string) => {
|
|
50
|
+
const titleLabel = currentTitle.includes('/') ? currentTitle.split('/').filter(Boolean).at(-1) : currentTitle
|
|
51
|
+
return `${before}${expectedPrefix}${titleLabel}${after}`
|
|
52
|
+
},
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function collectFiles(rootDir: string, dir: string, suffix: string) {
|
|
57
|
+
const absoluteDir = resolveRootPath(rootDir, dir)
|
|
58
|
+
const results: string[] = []
|
|
59
|
+
const stack = [absoluteDir]
|
|
60
|
+
|
|
61
|
+
while (stack.length > 0) {
|
|
62
|
+
const current = stack.pop()
|
|
63
|
+
if (!current || !fs.existsSync(current)) {
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
68
|
+
const fullPath = path.join(current, entry.name)
|
|
69
|
+
if (entry.isDirectory()) {
|
|
70
|
+
stack.push(fullPath)
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
if (entry.isFile() && entry.name.endsWith(suffix)) {
|
|
74
|
+
results.push(toRootRelative(rootDir, fullPath))
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return results.sort()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function collectSceneFolders(options: SceneConventionLintOptions) {
|
|
83
|
+
const sourceRootPath = resolveRootPath(options.rootDir, options.sourceRoot)
|
|
84
|
+
const sharedFolderName = options.sharedFolderName ?? '_shared'
|
|
85
|
+
|
|
86
|
+
if (!fs.existsSync(sourceRootPath)) {
|
|
87
|
+
return []
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return fs
|
|
91
|
+
.readdirSync(sourceRootPath, { withFileTypes: true })
|
|
92
|
+
.filter((entry) => entry.isDirectory())
|
|
93
|
+
.filter((entry) => entry.name !== sharedFolderName)
|
|
94
|
+
.filter((entry) => !options.checkedFolderPrefix || entry.name.startsWith(options.checkedFolderPrefix))
|
|
95
|
+
.map((entry) => toRootRelative(options.rootDir, path.join(sourceRootPath, entry.name)))
|
|
96
|
+
.sort()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function lintSceneFolder(options: SceneConventionLintOptions, folder: string) {
|
|
100
|
+
const slug = path.posix.basename(folder)
|
|
101
|
+
const errors: string[] = []
|
|
102
|
+
const requiredFiles = [
|
|
103
|
+
`${slug}.tsx`,
|
|
104
|
+
`${slug}-Landscape.tsx`,
|
|
105
|
+
`${slug}-Horizontal.tsx`,
|
|
106
|
+
`${slug}.stories.tsx`,
|
|
107
|
+
`${slug}.animated.stories.tsx`,
|
|
108
|
+
`${slug}-Horizontal.stories.tsx`,
|
|
109
|
+
`${slug}-Horizontal.animated.stories.tsx`,
|
|
110
|
+
`${slug}.test.tsx`,
|
|
111
|
+
'index.ts',
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
for (const fileName of requiredFiles) {
|
|
115
|
+
const requiredPath = `${folder}/${fileName}`
|
|
116
|
+
if (!fs.existsSync(resolveRootPath(options.rootDir, requiredPath))) {
|
|
117
|
+
errors.push(`${folder}: missing ${fileName}`)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const filesToCheck = [
|
|
122
|
+
`${folder}/${slug}.tsx`,
|
|
123
|
+
`${folder}/${slug}-Landscape.tsx`,
|
|
124
|
+
`${folder}/${slug}-Horizontal.tsx`,
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
for (const file of filesToCheck) {
|
|
128
|
+
if (!fs.existsSync(resolveRootPath(options.rootDir, file))) {
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const text = readText(options.rootDir, file)
|
|
133
|
+
const sourceFileMatch = text.match(/sourceFile:\s*['"`]([^'"`]+)['"`]/)
|
|
134
|
+
if (!sourceFileMatch) {
|
|
135
|
+
errors.push(`${file}: missing componentMetadata.sourceFile`)
|
|
136
|
+
} else if (sourceFileMatch[1] !== file) {
|
|
137
|
+
errors.push(`${file}: sourceFile must be "${file}", got "${sourceFileMatch[1]}"`)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const baseFile = `${folder}/${slug}.tsx`
|
|
142
|
+
if (fs.existsSync(resolveRootPath(options.rootDir, baseFile))) {
|
|
143
|
+
const text = readText(options.rootDir, baseFile)
|
|
144
|
+
if (!/defineSceneComponent\s*\(/.test(text)) {
|
|
145
|
+
errors.push(`${baseFile}: base scene must use defineSceneComponent(...)`)
|
|
146
|
+
}
|
|
147
|
+
if (!/propsSchema:\s*z\.object\s*\(/.test(text)) {
|
|
148
|
+
errors.push(`${baseFile}: base scene must colocate propsSchema with z.object(...)`)
|
|
149
|
+
}
|
|
150
|
+
if (!/useRemotionSceneRuntime\s*\(/.test(text)) {
|
|
151
|
+
errors.push(`${baseFile}: base scene must use useRemotionSceneRuntime()`)
|
|
152
|
+
}
|
|
153
|
+
if (!/SceneMotionLayer|SceneMotionStack|useSceneMotion/.test(text)) {
|
|
154
|
+
errors.push(`${baseFile}: scene animation must use SceneMotionLayer, SceneMotionStack, or useSceneMotion`)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const landscapeFile = `${folder}/${slug}-Landscape.tsx`
|
|
159
|
+
if (
|
|
160
|
+
fs.existsSync(resolveRootPath(options.rootDir, landscapeFile)) &&
|
|
161
|
+
!/aspectRatio:\s*['"]16:9['"]/.test(readText(options.rootDir, landscapeFile))
|
|
162
|
+
) {
|
|
163
|
+
errors.push(`${landscapeFile}: Landscape variant must set aspectRatio: '16:9'`)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const horizontalFile = `${folder}/${slug}-Horizontal.tsx`
|
|
167
|
+
if (
|
|
168
|
+
fs.existsSync(resolveRootPath(options.rootDir, horizontalFile)) &&
|
|
169
|
+
!/aspectRatio:\s*['"]9:16['"]/.test(readText(options.rootDir, horizontalFile))
|
|
170
|
+
) {
|
|
171
|
+
errors.push(`${horizontalFile}: Horizontal variant must set aspectRatio: '9:16'`)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return errors
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function lintStoryFile(options: SceneConventionLintOptions, storyFile: string) {
|
|
178
|
+
const normalized = toRootRelative(options.rootDir, storyFile)
|
|
179
|
+
const text = readText(options.rootDir, normalized)
|
|
180
|
+
const errors: string[] = []
|
|
181
|
+
const storyDir = path.posix.dirname(normalized.slice(`${options.sourceRoot}/`.length))
|
|
182
|
+
const expectedPrefix = `${options.titleRoot}/${storyDir}/`
|
|
183
|
+
const title = readStoryTitle(text)
|
|
184
|
+
const isAnimatedStory = normalized.endsWith('.animated.stories.tsx')
|
|
185
|
+
const isHorizontalStory = /-Horizontal(\.animated)?\.stories\.tsx$/.test(normalized)
|
|
186
|
+
const isLandscapeStory = !isHorizontalStory
|
|
187
|
+
|
|
188
|
+
if (!title.startsWith(expectedPrefix)) {
|
|
189
|
+
if (options.fix) {
|
|
190
|
+
writeText(options.rootDir, normalized, replaceStoryTitlePrefix(text, expectedPrefix))
|
|
191
|
+
} else {
|
|
192
|
+
errors.push(`${normalized}: story title must start with "${expectedPrefix}"`)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (isAnimatedStory && !/scenePreviewMode:\s*['"]animation['"]/.test(text)) {
|
|
197
|
+
errors.push(`${normalized}: animated stories must set scenePreviewMode: 'animation'`)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (isLandscapeStory && !/scenePreviewAspectRatio:\s*['"]16:9['"]/.test(text)) {
|
|
201
|
+
errors.push(`${normalized}: landscape stories must set scenePreviewAspectRatio: '16:9'`)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (isHorizontalStory && !/scenePreviewAspectRatio:\s*['"]9:16['"]/.test(text)) {
|
|
205
|
+
errors.push(`${normalized}: horizontal stories must set scenePreviewAspectRatio: '9:16'`)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return errors
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function lintSharedFolder(options: SceneConventionLintOptions) {
|
|
212
|
+
const sharedFolderName = options.sharedFolderName ?? '_shared'
|
|
213
|
+
const sharedRoot = `${options.sourceRoot}/${sharedFolderName}`
|
|
214
|
+
const sharedRootPath = resolveRootPath(options.rootDir, sharedRoot)
|
|
215
|
+
const errors: string[] = []
|
|
216
|
+
|
|
217
|
+
if (!fs.existsSync(sharedRootPath)) {
|
|
218
|
+
return errors
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
for (const file of collectFiles(options.rootDir, sharedRoot, '.tsx')) {
|
|
222
|
+
const text = readText(options.rootDir, file)
|
|
223
|
+
if (/componentMetadata|defineComponentProjectComponentMetadata|defineScenePluginMetadata/.test(text)) {
|
|
224
|
+
errors.push(`${file}: shared files must not declare publishable component metadata`)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return errors
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function lintSceneConvention(options: SceneConventionLintOptions): SceneConventionLintResult {
|
|
232
|
+
const sceneFolders = collectSceneFolders(options)
|
|
233
|
+
const storyFiles = collectFiles(options.rootDir, options.sourceRoot, '.stories.tsx').filter(
|
|
234
|
+
(file) => !file.includes(`/${options.sharedFolderName ?? '_shared'}/`),
|
|
235
|
+
)
|
|
236
|
+
const errors = [
|
|
237
|
+
...sceneFolders.flatMap((folder) => lintSceneFolder(options, folder)),
|
|
238
|
+
...storyFiles.flatMap((file) => lintStoryFile(options, file)),
|
|
239
|
+
...lintSharedFolder(options),
|
|
240
|
+
]
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
checkedSceneFolders: sceneFolders,
|
|
244
|
+
checkedStoryFiles: storyFiles,
|
|
245
|
+
errors,
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { SceneViewport } from './SceneViewport'
|
|
2
|
+
export { createSceneStoryVariants } from './scene-story'
|
|
3
|
+
export {
|
|
4
|
+
AbsoluteFill,
|
|
5
|
+
Audio,
|
|
6
|
+
Easing,
|
|
7
|
+
HtmlInCanvas,
|
|
8
|
+
Img,
|
|
9
|
+
RemotionPreviewProvider,
|
|
10
|
+
Sequence,
|
|
11
|
+
interpolate,
|
|
12
|
+
isHtmlInCanvasSupported,
|
|
13
|
+
random,
|
|
14
|
+
spring,
|
|
15
|
+
staticFile,
|
|
16
|
+
useCurrentFrame,
|
|
17
|
+
useVideoConfig,
|
|
18
|
+
} from './remotion-preview'
|
|
19
|
+
export type { RemotionPreviewValue } from './remotion-preview'
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { render } from '@testing-library/react'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
AbsoluteFill,
|
|
6
|
+
Easing,
|
|
7
|
+
Img,
|
|
8
|
+
RemotionPreviewProvider,
|
|
9
|
+
Sequence,
|
|
10
|
+
interpolate,
|
|
11
|
+
random,
|
|
12
|
+
spring,
|
|
13
|
+
staticFile,
|
|
14
|
+
useCurrentFrame,
|
|
15
|
+
useVideoConfig,
|
|
16
|
+
} from './remotion-preview'
|
|
17
|
+
|
|
18
|
+
describe('storybook remotion preview shim', () => {
|
|
19
|
+
it('exports spring, interpolate and helpers', () => {
|
|
20
|
+
expect(spring({ frame: 30, fps: 30 })).toBeGreaterThan(0)
|
|
21
|
+
expect(interpolate(0.5, [0, 1], [10, 20])).toBe(15)
|
|
22
|
+
expect(random('seed')).toBeGreaterThanOrEqual(0)
|
|
23
|
+
expect(staticFile('/foo.png')).toBe('/foo.png')
|
|
24
|
+
expect(Easing.linear(0.5)).toBe(0.5)
|
|
25
|
+
expect(AbsoluteFill).toBeTypeOf('function')
|
|
26
|
+
expect(Sequence).toBeTypeOf('function')
|
|
27
|
+
expect(Img).toBeTypeOf('function')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('provides frame and video config through the provider', () => {
|
|
31
|
+
const Probe = () => {
|
|
32
|
+
const frame = useCurrentFrame()
|
|
33
|
+
const config = useVideoConfig()
|
|
34
|
+
return (
|
|
35
|
+
<div>
|
|
36
|
+
frame:{frame}
|
|
37
|
+
width:{config.width}
|
|
38
|
+
height:{config.height}
|
|
39
|
+
</div>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const { getByText } = render(
|
|
44
|
+
<RemotionPreviewProvider value={{ frame: 12, fps: 30, width: 720, height: 1280, durationInFrames: 150 }}>
|
|
45
|
+
<Probe />
|
|
46
|
+
</RemotionPreviewProvider>,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
expect(getByText(/frame:12/)).toBeTruthy()
|
|
50
|
+
expect(getByText(/width:720/)).toBeTruthy()
|
|
51
|
+
expect(getByText(/height:1280/)).toBeTruthy()
|
|
52
|
+
})
|
|
53
|
+
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { CSSProperties, ReactNode } from 'react'
|
|
2
2
|
import { createContext, useContext } from 'react'
|
|
3
3
|
|
|
4
|
-
type RemotionPreviewValue = {
|
|
4
|
+
export type RemotionPreviewValue = {
|
|
5
5
|
frame: number
|
|
6
6
|
fps: number
|
|
7
7
|
width: number
|
|
@@ -60,9 +60,9 @@ type SpringConfig = {
|
|
|
60
60
|
delay?: number
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
function
|
|
64
|
-
if (value <
|
|
65
|
-
if (value >
|
|
63
|
+
function clamp(value: number, min: number, max: number) {
|
|
64
|
+
if (value < min) return min
|
|
65
|
+
if (value > max) return max
|
|
66
66
|
return value
|
|
67
67
|
}
|
|
68
68
|
|
|
@@ -84,19 +84,13 @@ export function spring({
|
|
|
84
84
|
const eased = 1 - Math.exp(-progress * (damping / Math.max(0.001, mass)))
|
|
85
85
|
const wobble = Math.sin(progress * Math.PI * (stiffness / 55)) * 0.015
|
|
86
86
|
|
|
87
|
-
return
|
|
87
|
+
return clamp(eased + wobble, 0, 1)
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
type Extrapolate = 'clamp' | 'extend' | 'identity'
|
|
91
91
|
|
|
92
92
|
type EasingFunction = (value: number) => number
|
|
93
93
|
|
|
94
|
-
function clamp(value: number, min: number, max: number) {
|
|
95
|
-
if (value < min) return min
|
|
96
|
-
if (value > max) return max
|
|
97
|
-
return value
|
|
98
|
-
}
|
|
99
|
-
|
|
100
94
|
function wrapInOut(easing: EasingFunction): EasingFunction {
|
|
101
95
|
return (value) => {
|
|
102
96
|
if (value <= 0.5) {
|
|
@@ -197,6 +191,22 @@ export function Img({
|
|
|
197
191
|
return <img {...rest} src={src} alt={alt} style={style} />
|
|
198
192
|
}
|
|
199
193
|
|
|
194
|
+
export function Audio({
|
|
195
|
+
src,
|
|
196
|
+
volume,
|
|
197
|
+
style,
|
|
198
|
+
...rest
|
|
199
|
+
}: {
|
|
200
|
+
src: string
|
|
201
|
+
volume?: number | ((frame: number) => number)
|
|
202
|
+
style?: CSSProperties
|
|
203
|
+
[key: string]: unknown
|
|
204
|
+
}) {
|
|
205
|
+
void volume
|
|
206
|
+
|
|
207
|
+
return <audio {...rest} src={src} style={style} />
|
|
208
|
+
}
|
|
209
|
+
|
|
200
210
|
export const HTML_IN_CANVAS_UNSUPPORTED_MESSAGE =
|
|
201
211
|
'HtmlInCanvas is not available in Storybook preview, falling back to the DOM renderer.'
|
|
202
212
|
|
|
@@ -216,10 +226,7 @@ type HtmlInCanvasProps = {
|
|
|
216
226
|
style?: CSSProperties
|
|
217
227
|
}
|
|
218
228
|
|
|
219
|
-
function HtmlInCanvasFallback({
|
|
220
|
-
children,
|
|
221
|
-
style,
|
|
222
|
-
}: HtmlInCanvasProps) {
|
|
229
|
+
function HtmlInCanvasFallback({ children, style }: HtmlInCanvasProps) {
|
|
223
230
|
return (
|
|
224
231
|
<div
|
|
225
232
|
style={{
|
|
@@ -264,3 +271,26 @@ export function AbsoluteFill({
|
|
|
264
271
|
</div>
|
|
265
272
|
)
|
|
266
273
|
}
|
|
274
|
+
|
|
275
|
+
export function Sequence({
|
|
276
|
+
children,
|
|
277
|
+
}: {
|
|
278
|
+
from?: number
|
|
279
|
+
durationInFrames?: number
|
|
280
|
+
children?: ReactNode
|
|
281
|
+
}) {
|
|
282
|
+
return <>{children}</>
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function random(seed?: string | number) {
|
|
286
|
+
const value = String(seed ?? 0)
|
|
287
|
+
let hash = 0
|
|
288
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
289
|
+
hash = (hash * 31 + value.charCodeAt(index)) >>> 0
|
|
290
|
+
}
|
|
291
|
+
return (hash % 10000) / 10000
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function staticFile(path: string) {
|
|
295
|
+
return path
|
|
296
|
+
}
|