@vibecuting/video-project-core 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DESIGN.md +29 -0
- package/README.md +21 -0
- package/package.json +30 -0
- package/src/base/BaseRemotionScene.test.tsx +27 -0
- package/src/base/BaseRemotionScene.tsx +38 -0
- package/src/base/index.ts +4 -0
- package/src/base/scene-identity.test.ts +19 -0
- package/src/base/scene-identity.ts +25 -0
- package/src/base/use-remotion-scene-runtime.test.tsx +35 -0
- package/src/base/use-remotion-scene-runtime.ts +17 -0
- package/src/core/index.test.ts +21 -0
- package/src/core/index.ts +3 -0
- package/src/core/intro/default-intro-bumper.tsx +66 -0
- package/src/core/intro/index.ts +1 -0
- package/src/core/outro/default-outro-bumper.tsx +55 -0
- package/src/core/outro/index.ts +1 -0
- package/src/core/scene/2dscene/default-2d-scene.tsx +52 -0
- package/src/core/scene/2dscene/index.ts +1 -0
- package/src/core/scene/3dscene/default-3d-scene.tsx +54 -0
- package/src/core/scene/3dscene/index.ts +1 -0
- package/src/core/scene/index.ts +4 -0
- package/src/core/scene/slide/default-slide-scene.tsx +51 -0
- package/src/core/scene/slide/index.ts +1 -0
- package/src/core/scene/utils/index.ts +2 -0
- package/src/core/scene/utils/scene-shell.tsx +35 -0
- package/src/core/scene/utils/scene-size.test.ts +22 -0
- package/src/core/scene/utils/scene-size.ts +25 -0
- package/src/index.ts +2 -0
- package/tsconfig.json +5 -0
- package/vitest.config.ts +19 -0
package/DESIGN.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# @vibecuting/video-project-core 设计文档
|
|
2
|
+
|
|
3
|
+
`@vibecuting/video-project-core` 现在只承载视频工程的基础 demo 模板组件。
|
|
4
|
+
它不再负责组件协议、manifest 协议或本地 decorator 协议层。
|
|
5
|
+
|
|
6
|
+
## 职责
|
|
7
|
+
|
|
8
|
+
- 提供 `src/core/*` 下的基础模板组件
|
|
9
|
+
- `intro`
|
|
10
|
+
- `outro`
|
|
11
|
+
- `scene`
|
|
12
|
+
- `scene/2dscene`
|
|
13
|
+
- `scene/3dscene`
|
|
14
|
+
- `scene/slide`
|
|
15
|
+
- `scene/utils`
|
|
16
|
+
- 复用 `@vibecuting/component-project-helper` 的注解和元数据模型
|
|
17
|
+
- 作为视频工程模板与组件工程注解之间的轻量共享层
|
|
18
|
+
|
|
19
|
+
## 不负责什么
|
|
20
|
+
|
|
21
|
+
- 不负责 `src/manifest/*`
|
|
22
|
+
- 不负责 `src/decorators/*`
|
|
23
|
+
- 不负责把视频工程再抽象成独立协议包
|
|
24
|
+
|
|
25
|
+
## 约束
|
|
26
|
+
|
|
27
|
+
- demo 组件只通过 helper 注解暴露元数据
|
|
28
|
+
- package root 只导出 `src/core/*`
|
|
29
|
+
- 旧的 manifest / decorator 测试和实现不再保留
|
package/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# @vibecuting/video-project-core
|
|
2
|
+
|
|
3
|
+
这里放视频工程的基础 demo 模板组件。
|
|
4
|
+
|
|
5
|
+
职责:
|
|
6
|
+
|
|
7
|
+
- 提供 `src/core/*` 下的 demo 组件
|
|
8
|
+
- `intro`
|
|
9
|
+
- `outro`
|
|
10
|
+
- `scene`
|
|
11
|
+
- `scene/2dscene`
|
|
12
|
+
- `scene/3dscene`
|
|
13
|
+
- `scene/slide`
|
|
14
|
+
- `scene/utils`
|
|
15
|
+
- 复用 `@vibecuting/component-project-helper` 的注解和元数据模型
|
|
16
|
+
- 只保留基础场景模板,不再承载本地 `manifest` / `decorators` 协议层
|
|
17
|
+
|
|
18
|
+
常用命令:
|
|
19
|
+
|
|
20
|
+
- `pnpm typecheck`
|
|
21
|
+
- `pnpm test`
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vibecuting/video-project-core",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"types": "src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"registry": "https://registry.npmjs.org",
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"typecheck": "node ../../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/tsc -p tsconfig.json --noEmit",
|
|
16
|
+
"test": "node ../../../node_modules/vitest/vitest.mjs run"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@vibecuting/component-project-helper": "workspace:*",
|
|
20
|
+
"react": "^19.0.0"
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"remotion": "^4.0.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/react": "^19.0.0",
|
|
27
|
+
"@types/react-dom": "^19.0.0",
|
|
28
|
+
"remotion": "4.0.473"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { BaseRemotionScene } from './BaseRemotionScene'
|
|
5
|
+
|
|
6
|
+
describe('BaseRemotionScene', () => {
|
|
7
|
+
it('renders a reusable scene frame', () => {
|
|
8
|
+
const html = renderToStaticMarkup(
|
|
9
|
+
<BaseRemotionScene name="Opening">
|
|
10
|
+
<section>demo</section>
|
|
11
|
+
</BaseRemotionScene>,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
expect(html).toContain('demo')
|
|
15
|
+
expect(html).toContain('data-scene-name="Opening"')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('derives a deterministic id when only name is provided', () => {
|
|
19
|
+
const html = renderToStaticMarkup(
|
|
20
|
+
<BaseRemotionScene name="Opening Scene">
|
|
21
|
+
<section>demo</section>
|
|
22
|
+
</BaseRemotionScene>,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
expect(html).toContain('id="scene-opening-scene"')
|
|
26
|
+
})
|
|
27
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from 'react'
|
|
2
|
+
import { useId } from 'react'
|
|
3
|
+
import { AbsoluteFill } from 'remotion'
|
|
4
|
+
|
|
5
|
+
import { resolveRemotionSceneId } from './scene-identity'
|
|
6
|
+
|
|
7
|
+
export interface BaseRemotionSceneProps {
|
|
8
|
+
id?: string
|
|
9
|
+
name?: string
|
|
10
|
+
className?: string
|
|
11
|
+
style?: CSSProperties
|
|
12
|
+
children?: ReactNode
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function BaseRemotionScene({
|
|
16
|
+
id,
|
|
17
|
+
name,
|
|
18
|
+
className,
|
|
19
|
+
style,
|
|
20
|
+
children,
|
|
21
|
+
}: BaseRemotionSceneProps) {
|
|
22
|
+
const reactId = useId()
|
|
23
|
+
const resolvedId = resolveRemotionSceneId({ id, name, reactId })
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<AbsoluteFill
|
|
27
|
+
id={resolvedId}
|
|
28
|
+
data-scene-name={name}
|
|
29
|
+
className={className}
|
|
30
|
+
style={{
|
|
31
|
+
overflow: 'hidden',
|
|
32
|
+
...style,
|
|
33
|
+
}}
|
|
34
|
+
>
|
|
35
|
+
{children}
|
|
36
|
+
</AbsoluteFill>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { resolveRemotionSceneId } from './scene-identity'
|
|
4
|
+
|
|
5
|
+
describe('resolveRemotionSceneId', () => {
|
|
6
|
+
it('prefers an explicit id', () => {
|
|
7
|
+
expect(resolveRemotionSceneId({ id: 'intro', name: 'Opening', reactId: ':r0:' })).toBe('intro')
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('derives a stable id from name', () => {
|
|
11
|
+
expect(resolveRemotionSceneId({ name: 'Opening Scene', reactId: ':r0:' })).toBe(
|
|
12
|
+
'scene-opening-scene',
|
|
13
|
+
)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('uses a sanitized React id when id and name are omitted', () => {
|
|
17
|
+
expect(resolveRemotionSceneId({ reactId: ':r0:' })).toBe('scene-r0')
|
|
18
|
+
})
|
|
19
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const normalizeSceneIdPart = (value: string) =>
|
|
2
|
+
value
|
|
3
|
+
.trim()
|
|
4
|
+
.toLowerCase()
|
|
5
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
6
|
+
.replace(/^-+|-+$/g, '')
|
|
7
|
+
|
|
8
|
+
export function resolveRemotionSceneId(input: {
|
|
9
|
+
id?: string
|
|
10
|
+
name?: string
|
|
11
|
+
reactId: string
|
|
12
|
+
}) {
|
|
13
|
+
const explicitId = input.id ? normalizeSceneIdPart(input.id) : ''
|
|
14
|
+
if (explicitId) {
|
|
15
|
+
return explicitId
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const namedId = input.name ? normalizeSceneIdPart(input.name) : ''
|
|
19
|
+
if (namedId) {
|
|
20
|
+
return `scene-${namedId}`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const reactId = normalizeSceneIdPart(input.reactId)
|
|
24
|
+
return `scene-${reactId || 'anonymous'}`
|
|
25
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react'
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { useRemotionSceneRuntime } from './use-remotion-scene-runtime'
|
|
5
|
+
|
|
6
|
+
vi.mock('remotion', () => ({
|
|
7
|
+
useCurrentFrame: () => 30,
|
|
8
|
+
useVideoConfig: () => ({
|
|
9
|
+
fps: 30,
|
|
10
|
+
width: 1920,
|
|
11
|
+
height: 1080,
|
|
12
|
+
durationInFrames: 300,
|
|
13
|
+
}),
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
describe('useRemotionSceneRuntime', () => {
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
vi.clearAllMocks()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('returns derived runtime values', () => {
|
|
22
|
+
const { result } = renderHook(() => useRemotionSceneRuntime())
|
|
23
|
+
|
|
24
|
+
expect(result.current).toEqual({
|
|
25
|
+
frame: 30,
|
|
26
|
+
fps: 30,
|
|
27
|
+
width: 1920,
|
|
28
|
+
height: 1080,
|
|
29
|
+
durationInFrames: 300,
|
|
30
|
+
timeInSeconds: 1,
|
|
31
|
+
scaleX: 1,
|
|
32
|
+
scaleY: 1,
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useCurrentFrame, useVideoConfig } from 'remotion'
|
|
2
|
+
|
|
3
|
+
export function useRemotionSceneRuntime() {
|
|
4
|
+
const frame = useCurrentFrame()
|
|
5
|
+
const { fps, width, height, durationInFrames } = useVideoConfig()
|
|
6
|
+
|
|
7
|
+
return {
|
|
8
|
+
frame,
|
|
9
|
+
fps,
|
|
10
|
+
width,
|
|
11
|
+
height,
|
|
12
|
+
durationInFrames,
|
|
13
|
+
timeInSeconds: frame / fps,
|
|
14
|
+
scaleX: width / 1920,
|
|
15
|
+
scaleY: height / 1080,
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { getComponentProjectComponentMetadata } from '@vibecuting/component-project-helper'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
Default2DScene,
|
|
7
|
+
Default3DScene,
|
|
8
|
+
DefaultIntroBumper,
|
|
9
|
+
DefaultOutroBumper,
|
|
10
|
+
DefaultSlideScene,
|
|
11
|
+
} from './index'
|
|
12
|
+
|
|
13
|
+
describe('video-project-core demo exports', () => {
|
|
14
|
+
it('exports helper-annotated demo templates from the core entrypoint', () => {
|
|
15
|
+
expect(getComponentProjectComponentMetadata(DefaultIntroBumper)?.sceneType).toBe('intro')
|
|
16
|
+
expect(getComponentProjectComponentMetadata(DefaultOutroBumper)?.sceneType).toBe('outro')
|
|
17
|
+
expect(getComponentProjectComponentMetadata(Default2DScene)?.aspectRatio).toBe('16:9')
|
|
18
|
+
expect(getComponentProjectComponentMetadata(Default3DScene)?.aspectRatio).toBe('16:9')
|
|
19
|
+
expect(getComponentProjectComponentMetadata(DefaultSlideScene)?.aspectRatio).toBe('16:9')
|
|
20
|
+
})
|
|
21
|
+
})
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
BaseRemotionScene,
|
|
5
|
+
type BaseRemotionSceneProps,
|
|
6
|
+
useRemotionSceneRuntime,
|
|
7
|
+
} from '../../base'
|
|
8
|
+
import {
|
|
9
|
+
VideoComponent,
|
|
10
|
+
defineComponentProjectComponentMetadata,
|
|
11
|
+
} from '@vibecuting/component-project-helper'
|
|
12
|
+
|
|
13
|
+
export interface DefaultIntroBumperProps extends BaseRemotionSceneProps {
|
|
14
|
+
title: string
|
|
15
|
+
subtitle?: string
|
|
16
|
+
accent?: string
|
|
17
|
+
children?: ReactNode
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const componentMetadata = defineComponentProjectComponentMetadata({
|
|
21
|
+
name: 'DefaultIntroBumper',
|
|
22
|
+
description: 'Default remotion intro bumper',
|
|
23
|
+
sourceFile: 'src/core/intro/default-intro-bumper.tsx',
|
|
24
|
+
aspectRatio: '16:9',
|
|
25
|
+
sceneType: 'intro',
|
|
26
|
+
tags: ['intro', 'bumper'],
|
|
27
|
+
propsTypeName: 'DefaultIntroBumperProps',
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
export function DefaultIntroBumper({
|
|
31
|
+
id,
|
|
32
|
+
name,
|
|
33
|
+
title,
|
|
34
|
+
subtitle,
|
|
35
|
+
accent,
|
|
36
|
+
children,
|
|
37
|
+
}: DefaultIntroBumperProps) {
|
|
38
|
+
const runtime = useRemotionSceneRuntime()
|
|
39
|
+
const titleFontSize = Math.round(runtime.height * 0.062)
|
|
40
|
+
const subtitleFontSize = Math.round(runtime.height * 0.024)
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<BaseRemotionScene
|
|
44
|
+
id={id}
|
|
45
|
+
name={name}
|
|
46
|
+
style={{
|
|
47
|
+
background: accent ?? '#0f172a',
|
|
48
|
+
color: '#fff',
|
|
49
|
+
display: 'flex',
|
|
50
|
+
alignItems: 'flex-start',
|
|
51
|
+
justifyContent: 'flex-start',
|
|
52
|
+
padding: Math.round(runtime.height * 0.06),
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
<section style={{ maxWidth: Math.round(runtime.width * 0.72) }}>
|
|
56
|
+
{subtitle ? (
|
|
57
|
+
<p style={{ margin: 0, fontSize: subtitleFontSize, opacity: 0.82 }}>{subtitle}</p>
|
|
58
|
+
) : null}
|
|
59
|
+
<h1 style={{ margin: '16px 0 0', fontSize: titleFontSize, lineHeight: 1.05 }}>{title}</h1>
|
|
60
|
+
{children}
|
|
61
|
+
</section>
|
|
62
|
+
</BaseRemotionScene>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
VideoComponent(componentMetadata)(DefaultIntroBumper)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DefaultIntroBumper } from './default-intro-bumper'
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
BaseRemotionScene,
|
|
5
|
+
type BaseRemotionSceneProps,
|
|
6
|
+
useRemotionSceneRuntime,
|
|
7
|
+
} from '../../base'
|
|
8
|
+
import {
|
|
9
|
+
VideoComponent,
|
|
10
|
+
defineComponentProjectComponentMetadata,
|
|
11
|
+
} from '@vibecuting/component-project-helper'
|
|
12
|
+
|
|
13
|
+
export interface DefaultOutroBumperProps extends BaseRemotionSceneProps {
|
|
14
|
+
title: string
|
|
15
|
+
note?: string
|
|
16
|
+
children?: ReactNode
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const componentMetadata = defineComponentProjectComponentMetadata({
|
|
20
|
+
name: 'DefaultOutroBumper',
|
|
21
|
+
description: 'Default remotion outro bumper',
|
|
22
|
+
sourceFile: 'src/core/outro/default-outro-bumper.tsx',
|
|
23
|
+
aspectRatio: '16:9',
|
|
24
|
+
sceneType: 'outro',
|
|
25
|
+
tags: ['outro', 'bumper'],
|
|
26
|
+
propsTypeName: 'DefaultOutroBumperProps',
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
export function DefaultOutroBumper({ id, name, title, note, children }: DefaultOutroBumperProps) {
|
|
30
|
+
const runtime = useRemotionSceneRuntime()
|
|
31
|
+
const titleFontSize = Math.round(runtime.height * 0.062)
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<BaseRemotionScene
|
|
35
|
+
id={id}
|
|
36
|
+
name={name}
|
|
37
|
+
style={{
|
|
38
|
+
background: '#e2e8f0',
|
|
39
|
+
color: '#0f172a',
|
|
40
|
+
display: 'flex',
|
|
41
|
+
alignItems: 'flex-start',
|
|
42
|
+
justifyContent: 'flex-start',
|
|
43
|
+
padding: Math.round(runtime.height * 0.06),
|
|
44
|
+
}}
|
|
45
|
+
>
|
|
46
|
+
<footer style={{ maxWidth: Math.round(runtime.width * 0.72) }}>
|
|
47
|
+
<h1 style={{ margin: 0, fontSize: titleFontSize, lineHeight: 1.05 }}>{title}</h1>
|
|
48
|
+
{note ? <p style={{ margin: '16px 0 0' }}>{note}</p> : null}
|
|
49
|
+
{children}
|
|
50
|
+
</footer>
|
|
51
|
+
</BaseRemotionScene>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
VideoComponent(componentMetadata)(DefaultOutroBumper)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DefaultOutroBumper } from './default-outro-bumper'
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseRemotionScene,
|
|
3
|
+
type BaseRemotionSceneProps,
|
|
4
|
+
useRemotionSceneRuntime,
|
|
5
|
+
} from '../../../base'
|
|
6
|
+
import {
|
|
7
|
+
VideoComponent,
|
|
8
|
+
defineComponentProjectComponentMetadata,
|
|
9
|
+
} from '@vibecuting/component-project-helper'
|
|
10
|
+
|
|
11
|
+
export interface Default2DSceneProps extends BaseRemotionSceneProps {
|
|
12
|
+
title: string
|
|
13
|
+
subtitle?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const componentMetadata = defineComponentProjectComponentMetadata({
|
|
17
|
+
name: 'Default2DScene',
|
|
18
|
+
description: 'Default remotion 2D scene',
|
|
19
|
+
sourceFile: 'src/core/scene/2dscene/default-2d-scene.tsx',
|
|
20
|
+
aspectRatio: '16:9',
|
|
21
|
+
sceneType: 'scene',
|
|
22
|
+
tags: ['scene', '2d'],
|
|
23
|
+
propsTypeName: 'Default2DSceneProps',
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export function Default2DScene({ id, name, title, subtitle }: Default2DSceneProps) {
|
|
27
|
+
const runtime = useRemotionSceneRuntime()
|
|
28
|
+
const titleFontSize = Math.round(runtime.height * 0.062)
|
|
29
|
+
const subtitleFontSize = Math.round(runtime.height * 0.024)
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<BaseRemotionScene
|
|
33
|
+
id={id}
|
|
34
|
+
name={name}
|
|
35
|
+
style={{
|
|
36
|
+
backgroundColor: '#111827',
|
|
37
|
+
color: '#f8fafc',
|
|
38
|
+
display: 'flex',
|
|
39
|
+
alignItems: 'center',
|
|
40
|
+
justifyContent: 'flex-start',
|
|
41
|
+
padding: Math.round(runtime.height * 0.06),
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
<div style={{ width: '100%', maxWidth: Math.round(runtime.width * 0.72) }}>
|
|
45
|
+
<h1 style={{ margin: 0, fontSize: titleFontSize, lineHeight: 1.05 }}>{title}</h1>
|
|
46
|
+
{subtitle ? <p style={{ margin: '16px 0 0', fontSize: subtitleFontSize }}>{subtitle}</p> : null}
|
|
47
|
+
</div>
|
|
48
|
+
</BaseRemotionScene>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
VideoComponent(componentMetadata)(Default2DScene)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Default2DScene } from './default-2d-scene'
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseRemotionScene,
|
|
3
|
+
type BaseRemotionSceneProps,
|
|
4
|
+
useRemotionSceneRuntime,
|
|
5
|
+
} from '../../../base'
|
|
6
|
+
import {
|
|
7
|
+
VideoComponent,
|
|
8
|
+
defineComponentProjectComponentMetadata,
|
|
9
|
+
} from '@vibecuting/component-project-helper'
|
|
10
|
+
|
|
11
|
+
export interface Default3DSceneProps extends BaseRemotionSceneProps {
|
|
12
|
+
title: string
|
|
13
|
+
depth?: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const componentMetadata = defineComponentProjectComponentMetadata({
|
|
17
|
+
name: 'Default3DScene',
|
|
18
|
+
description: 'Default remotion 3D scene',
|
|
19
|
+
sourceFile: 'src/core/scene/3dscene/default-3d-scene.tsx',
|
|
20
|
+
aspectRatio: '16:9',
|
|
21
|
+
sceneType: 'scene',
|
|
22
|
+
tags: ['scene', '3d'],
|
|
23
|
+
propsTypeName: 'Default3DSceneProps',
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export function Default3DScene({ id, name, title, depth = 240 }: Default3DSceneProps) {
|
|
27
|
+
const runtime = useRemotionSceneRuntime()
|
|
28
|
+
const titleFontSize = Math.round(runtime.height * 0.062)
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<BaseRemotionScene
|
|
32
|
+
id={id}
|
|
33
|
+
name={name}
|
|
34
|
+
style={{
|
|
35
|
+
backgroundColor: '#020617',
|
|
36
|
+
color: '#e2e8f0',
|
|
37
|
+
display: 'flex',
|
|
38
|
+
alignItems: 'center',
|
|
39
|
+
justifyContent: 'flex-start',
|
|
40
|
+
padding: Math.round(runtime.height * 0.06),
|
|
41
|
+
perspective: 1200,
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
<div style={{ transform: `translateZ(${depth / 8}px) rotateY(-18deg)` }}>
|
|
45
|
+
<h1 style={{ margin: 0, fontSize: titleFontSize, lineHeight: 1.05 }}>{title}</h1>
|
|
46
|
+
<p style={{ margin: '16px 0 0', fontSize: Math.round(runtime.height * 0.024), opacity: 0.8 }}>
|
|
47
|
+
Depth {depth}
|
|
48
|
+
</p>
|
|
49
|
+
</div>
|
|
50
|
+
</BaseRemotionScene>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
VideoComponent(componentMetadata)(Default3DScene)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Default3DScene } from './default-3d-scene'
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseRemotionScene,
|
|
3
|
+
type BaseRemotionSceneProps,
|
|
4
|
+
useRemotionSceneRuntime,
|
|
5
|
+
} from '../../../base'
|
|
6
|
+
import {
|
|
7
|
+
VideoComponent,
|
|
8
|
+
defineComponentProjectComponentMetadata,
|
|
9
|
+
} from '@vibecuting/component-project-helper'
|
|
10
|
+
|
|
11
|
+
export interface DefaultSlideSceneProps extends BaseRemotionSceneProps {
|
|
12
|
+
title: string
|
|
13
|
+
body?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const componentMetadata = defineComponentProjectComponentMetadata({
|
|
17
|
+
name: 'DefaultSlideScene',
|
|
18
|
+
description: 'Default remotion slide scene',
|
|
19
|
+
sourceFile: 'src/core/scene/slide/default-slide-scene.tsx',
|
|
20
|
+
aspectRatio: '16:9',
|
|
21
|
+
sceneType: 'scene',
|
|
22
|
+
tags: ['scene', 'slide'],
|
|
23
|
+
propsTypeName: 'DefaultSlideSceneProps',
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export function DefaultSlideScene({ id, name, title, body }: DefaultSlideSceneProps) {
|
|
27
|
+
const runtime = useRemotionSceneRuntime()
|
|
28
|
+
const titleFontSize = Math.round(runtime.height * 0.062)
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<BaseRemotionScene
|
|
32
|
+
id={id}
|
|
33
|
+
name={name}
|
|
34
|
+
style={{
|
|
35
|
+
backgroundColor: '#f8fafc',
|
|
36
|
+
color: '#0f172a',
|
|
37
|
+
display: 'flex',
|
|
38
|
+
alignItems: 'flex-start',
|
|
39
|
+
justifyContent: 'flex-start',
|
|
40
|
+
padding: Math.round(runtime.height * 0.06),
|
|
41
|
+
}}
|
|
42
|
+
>
|
|
43
|
+
<aside style={{ maxWidth: Math.round(runtime.width * 0.72) }}>
|
|
44
|
+
<h1 style={{ margin: 0, fontSize: titleFontSize, lineHeight: 1.05 }}>{title}</h1>
|
|
45
|
+
{body ? <p style={{ margin: '16px 0 0', fontSize: Math.round(runtime.height * 0.024) }}>{body}</p> : null}
|
|
46
|
+
</aside>
|
|
47
|
+
</BaseRemotionScene>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
VideoComponent(componentMetadata)(DefaultSlideScene)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DefaultSlideScene } from './default-slide-scene'
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
import { createSceneShellStyle, type DemoAspectRatio } from './scene-size'
|
|
4
|
+
|
|
5
|
+
export function SceneShell({
|
|
6
|
+
aspectRatio,
|
|
7
|
+
width,
|
|
8
|
+
backgroundColor,
|
|
9
|
+
className,
|
|
10
|
+
style,
|
|
11
|
+
children,
|
|
12
|
+
}: {
|
|
13
|
+
aspectRatio: DemoAspectRatio
|
|
14
|
+
width: number
|
|
15
|
+
backgroundColor: string
|
|
16
|
+
className?: string
|
|
17
|
+
style?: CSSProperties
|
|
18
|
+
children: ReactNode
|
|
19
|
+
}) {
|
|
20
|
+
return (
|
|
21
|
+
<div
|
|
22
|
+
className={className}
|
|
23
|
+
style={{
|
|
24
|
+
...createSceneShellStyle({
|
|
25
|
+
aspectRatio,
|
|
26
|
+
width,
|
|
27
|
+
backgroundColor,
|
|
28
|
+
}),
|
|
29
|
+
...style,
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
{children}
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { createSceneShellStyle, resolveDemoSceneSize } from './scene-size'
|
|
4
|
+
|
|
5
|
+
describe('scene utils', () => {
|
|
6
|
+
it('resolves 16:9 and 9:16 sizes', () => {
|
|
7
|
+
expect(resolveDemoSceneSize('16:9', 1920).height).toBe(1080)
|
|
8
|
+
expect(resolveDemoSceneSize('9:16', 1080).height).toBe(1920)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('builds a centered shell style', () => {
|
|
12
|
+
const style = createSceneShellStyle({
|
|
13
|
+
aspectRatio: '16:9',
|
|
14
|
+
width: 1280,
|
|
15
|
+
backgroundColor: '#0f172a',
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
expect(style.width).toBe(1280)
|
|
19
|
+
expect(style.backgroundColor).toBe('#0f172a')
|
|
20
|
+
expect(style.overflow).toBe('hidden')
|
|
21
|
+
})
|
|
22
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type DemoAspectRatio = '9:16' | '16:9'
|
|
2
|
+
|
|
3
|
+
export function resolveDemoSceneSize(aspectRatio: DemoAspectRatio, width: number) {
|
|
4
|
+
if (aspectRatio === '9:16') {
|
|
5
|
+
return { width, height: Math.round((width * 16) / 9) }
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
return { width, height: Math.round((width * 9) / 16) }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createSceneShellStyle(input: {
|
|
12
|
+
aspectRatio: DemoAspectRatio
|
|
13
|
+
width: number
|
|
14
|
+
backgroundColor: string
|
|
15
|
+
}) {
|
|
16
|
+
const size = resolveDemoSceneSize(input.aspectRatio, input.width)
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
position: 'relative' as const,
|
|
20
|
+
overflow: 'hidden' as const,
|
|
21
|
+
width: size.width,
|
|
22
|
+
height: size.height,
|
|
23
|
+
backgroundColor: input.backgroundColor,
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/index.ts
ADDED
package/tsconfig.json
ADDED
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
test: {
|
|
6
|
+
environment: 'jsdom',
|
|
7
|
+
globals: true,
|
|
8
|
+
include: ['src/**/*.{test,spec}.{ts,tsx}'],
|
|
9
|
+
},
|
|
10
|
+
resolve: {
|
|
11
|
+
alias: {
|
|
12
|
+
'@vibecuting/component-project-helper': path.resolve(
|
|
13
|
+
__dirname,
|
|
14
|
+
'../component-project-helper/src/index.ts',
|
|
15
|
+
),
|
|
16
|
+
'@vibecuting/video-project-core': path.resolve(__dirname, './src/index.ts'),
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
})
|