@vibecuting/video-project-core 0.1.27 → 0.1.29

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 (54) hide show
  1. package/package.json +2 -2
  2. package/src/base/BaseRemotionScene.tsx +4 -3
  3. package/src/core/index.test.ts +40 -0
  4. package/src/core/scene/2dscene/default-2d-scene-Horizontal.stories.tsx +1 -1
  5. package/src/core/scene/2dscene/default-2d-scene.stories.tsx +1 -1
  6. package/src/core/scene/3dscene/default-3d-scene-Horizontal.stories.tsx +1 -1
  7. package/src/core/scene/3dscene/default-3d-scene.stories.tsx +1 -1
  8. package/src/core/scene/index.ts +3 -0
  9. package/src/core/scene/photo/3dvr-photo/default-3dvr-photo-scene-Horizontal.stories.tsx +46 -0
  10. package/src/core/scene/photo/3dvr-photo/default-3dvr-photo-scene-Horizontal.tsx +20 -0
  11. package/src/core/scene/photo/3dvr-photo/default-3dvr-photo-scene-Landscape.tsx +20 -0
  12. package/src/core/scene/photo/3dvr-photo/default-3dvr-photo-scene.stories.tsx +46 -0
  13. package/src/core/scene/photo/3dvr-photo/default-3dvr-photo-scene.tsx +255 -0
  14. package/src/core/scene/photo/3dvr-photo/index.ts +3 -0
  15. package/src/core/scene/photo/default-live-photo-scene-Horizontal.stories.tsx +33 -0
  16. package/src/core/scene/photo/default-live-photo-scene-Horizontal.tsx +20 -0
  17. package/src/core/scene/photo/default-live-photo-scene-Landscape.tsx +20 -0
  18. package/src/core/scene/photo/default-live-photo-scene.stories.tsx +33 -0
  19. package/src/core/scene/photo/default-live-photo-scene.tsx +432 -0
  20. package/src/core/scene/photo/default-photo-strip-scene-Horizontal.tsx +20 -0
  21. package/src/core/scene/photo/default-photo-strip-scene-Landscape.tsx +20 -0
  22. package/src/core/scene/photo/default-photo-strip-scene.stories.tsx +38 -0
  23. package/src/core/scene/photo/default-photo-strip-scene.tsx +19 -0
  24. package/src/core/scene/photo/default-photo-wall-scene-Horizontal.tsx +20 -0
  25. package/src/core/scene/photo/default-photo-wall-scene-Landscape.tsx +20 -0
  26. package/src/core/scene/photo/default-photo-wall-scene.stories.tsx +38 -0
  27. package/src/core/scene/photo/default-photo-wall-scene.tsx +19 -0
  28. package/src/core/scene/photo/effects-photo/default-effects-photo-scene-Horizontal.stories.tsx +44 -0
  29. package/src/core/scene/photo/effects-photo/default-effects-photo-scene-Horizontal.tsx +20 -0
  30. package/src/core/scene/photo/effects-photo/default-effects-photo-scene-Landscape.tsx +20 -0
  31. package/src/core/scene/photo/effects-photo/default-effects-photo-scene.stories.tsx +44 -0
  32. package/src/core/scene/photo/effects-photo/default-effects-photo-scene.tsx +352 -0
  33. package/src/core/scene/photo/effects-photo/index.ts +3 -0
  34. package/src/core/scene/photo/index.ts +25 -0
  35. package/src/core/scene/photo/photo-storybook.test.tsx +112 -0
  36. package/src/core/scene/slide/default-slide-scene-Horizontal.animated.stories.tsx +1 -1
  37. package/src/core/scene/slide/default-slide-scene-Horizontal.stories.tsx +1 -1
  38. package/src/core/scene/slide/default-slide-scene.animated.stories.tsx +1 -1
  39. package/src/core/scene/slide/default-slide-scene.stories.tsx +1 -1
  40. package/src/core/scene/video/default-video-scene-Horizontal.stories.tsx +26 -0
  41. package/src/core/scene/video/default-video-scene-Horizontal.tsx +20 -0
  42. package/src/core/scene/video/default-video-scene-Landscape.tsx +20 -0
  43. package/src/core/scene/video/default-video-scene.stories.tsx +26 -0
  44. package/src/core/scene/video/default-video-scene.tsx +283 -0
  45. package/src/core/scene/video/default-video-split-scene-Horizontal.tsx +20 -0
  46. package/src/core/scene/video/default-video-split-scene-Landscape.tsx +20 -0
  47. package/src/core/scene/video/default-video-split-scene.stories.tsx +41 -0
  48. package/src/core/scene/video/default-video-split-scene.tsx +19 -0
  49. package/src/core/scene/video/default-video-wall-scene-Horizontal.tsx +20 -0
  50. package/src/core/scene/video/default-video-wall-scene-Landscape.tsx +20 -0
  51. package/src/core/scene/video/default-video-wall-scene.stories.tsx +40 -0
  52. package/src/core/scene/video/default-video-wall-scene.tsx +19 -0
  53. package/src/core/scene/video/index.ts +9 -0
  54. package/src/storybook/remotion-preview.tsx +112 -6
@@ -0,0 +1,432 @@
1
+ import type { ReactNode } from 'react'
2
+ import { cloneElement, isValidElement } from 'react'
3
+
4
+ import { z } from 'zod'
5
+
6
+ import { VideoComponent, defineScenePluginMetadata } from '@vibecuting/component-project-helper'
7
+ import { spring, useCurrentFrame, useVideoConfig } from 'remotion'
8
+
9
+ import { BaseRemotionScene, SceneMotionLayer, type BaseRemotionSceneProps, useRemotionSceneRuntime, useSceneConfig } from '../../../base'
10
+ import { defineSceneComponent } from '../../../plugins/scene-component'
11
+
12
+ export interface DefaultLivePhotoSceneProps extends BaseRemotionSceneProps {
13
+ sceneId?: string
14
+ sceneName?: string
15
+ title?: string
16
+ subtitle?: string
17
+ badge?: string
18
+ image: ReactNode
19
+ images?: ReactNode[]
20
+ variant?: 'live' | 'wall' | 'strip'
21
+ accent?: string
22
+ secondaryAccent?: string
23
+ textColor?: string
24
+ }
25
+
26
+ export const componentMetadata = defineScenePluginMetadata({
27
+ resourceKind: 'scene',
28
+ name: 'DefaultLivePhotoScene',
29
+ description: 'Apple-style live photo scene with a subtle push-in and luminous overlay',
30
+ sourceFile: 'src/core/scene/photo/default-live-photo-scene.tsx',
31
+ pluginKey: 'core.photo.live-photo',
32
+ tags: ['scene', 'photo', 'live-photo', 'image', 'apple'],
33
+ aspectRatio: '16:9',
34
+ sceneType: 'scene',
35
+ sceneFamily: 'custom',
36
+ rootLayout: 'absolute-fill',
37
+ propsTypeName: 'DefaultLivePhotoSceneProps',
38
+ })
39
+
40
+ function LivePhotoBadge({ label }: { label: string }) {
41
+ return (
42
+ <div
43
+ style={{
44
+ display: 'inline-flex',
45
+ alignItems: 'center',
46
+ gap: 8,
47
+ border: '1px solid rgba(255,255,255,0.18)',
48
+ background: 'rgba(2,6,23,0.6)',
49
+ color: '#f8fafc',
50
+ padding: '8px 12px',
51
+ letterSpacing: '0.22em',
52
+ textTransform: 'uppercase',
53
+ fontSize: 11,
54
+ lineHeight: 1,
55
+ }}
56
+ >
57
+ <span
58
+ aria-hidden="true"
59
+ style={{
60
+ width: 8,
61
+ height: 8,
62
+ borderRadius: 9999,
63
+ background: 'rgba(239,68,68,0.95)',
64
+ boxShadow: '0 0 0 4px rgba(239,68,68,0.14)',
65
+ }}
66
+ />
67
+ {label}
68
+ </div>
69
+ )
70
+ }
71
+
72
+ function renderCoverMedia(node: ReactNode) {
73
+ if (isValidElement(node)) {
74
+ const existingStyle = (node.props as { style?: Record<string, unknown> }).style ?? {}
75
+ return cloneElement(node, {
76
+ ...(node.props as Record<string, unknown>),
77
+ style: {
78
+ ...existingStyle,
79
+ width: '100%',
80
+ height: '100%',
81
+ objectFit: 'cover',
82
+ display: 'block',
83
+ minWidth: 0,
84
+ minHeight: 0,
85
+ },
86
+ })
87
+ }
88
+
89
+ return <div style={{ width: '100%', height: '100%', minWidth: 0, minHeight: 0 }}>{node}</div>
90
+ }
91
+
92
+ export const DefaultLivePhotoScene = VideoComponent(componentMetadata)(
93
+ defineSceneComponent({
94
+ family: 'custom',
95
+ propsSchema: z.object({
96
+ sceneId: z.string().optional(),
97
+ sceneName: z.string().optional(),
98
+ title: z.string().optional(),
99
+ subtitle: z.string().optional(),
100
+ badge: z.string().optional(),
101
+ image: z.any(),
102
+ images: z.array(z.any()).optional(),
103
+ variant: z.enum(['live', 'wall', 'strip']).optional(),
104
+ accent: z.string().optional(),
105
+ secondaryAccent: z.string().optional(),
106
+ textColor: z.string().optional(),
107
+ }),
108
+ component: function DefaultLivePhotoScene({
109
+ id,
110
+ name,
111
+ sceneId,
112
+ sceneName,
113
+ title,
114
+ subtitle,
115
+ badge,
116
+ image,
117
+ images,
118
+ variant = 'live',
119
+ accent,
120
+ secondaryAccent,
121
+ textColor,
122
+ }: DefaultLivePhotoSceneProps) {
123
+ const runtime = useRemotionSceneRuntime()
124
+ const sceneConfig = useSceneConfig()
125
+ const frame = useCurrentFrame()
126
+ const { fps } = useVideoConfig()
127
+ const portrait = runtime.height > runtime.width
128
+ const animationProgress =
129
+ sceneConfig.sceneType === 'animation'
130
+ ? spring({
131
+ frame,
132
+ fps,
133
+ config: {
134
+ damping: 18,
135
+ mass: 0.8,
136
+ stiffness: 110,
137
+ },
138
+ })
139
+ : 1
140
+ const titleSize = Math.round(runtime.height * (portrait ? 0.034 : 0.04))
141
+ const subtitleSize = Math.round(runtime.height * 0.017)
142
+ const padding = Math.round(runtime.height * (portrait ? 0.012 : 0.014))
143
+ const liveMediaHeight = Math.max(240, Math.floor(runtime.height * (portrait ? 0.84 : 0.86)))
144
+ const revealFrame = 12
145
+ const liveBreath = sceneConfig.sceneType === 'animation' ? Math.sin((frame / fps) * Math.PI * 0.85) * 0.012 : 0
146
+ const liveFocus = sceneConfig.sceneType === 'animation' ? 1 - animationProgress : 0
147
+ const revealEase = sceneConfig.sceneType === 'animation' ? spring({ frame: Math.max(0, frame - revealFrame), fps, config: { damping: 22, mass: 0.7, stiffness: 170 } }) : 1
148
+ const imageScale = sceneConfig.sceneType === 'animation' ? 1.06 + revealEase * 0.24 + liveBreath : 1
149
+ const imageShiftY = sceneConfig.sceneType === 'animation' ? revealEase * 32 + liveBreath * 72 : 0
150
+ const imageShiftX = sceneConfig.sceneType === 'animation' ? (portrait ? -10 : -18) * revealEase + liveBreath * (portrait ? 18 : 28) : 0
151
+ const backgroundScale = sceneConfig.sceneType === 'animation' ? 1.28 + revealEase * 0.12 : 1.16
152
+ const spotlightOpacity = sceneConfig.sceneType === 'animation' ? 0.16 + revealEase * 0.12 : 0.08
153
+ const vignetteOpacity = sceneConfig.sceneType === 'animation' ? 0.42 + revealEase * 0.18 : 0.28
154
+ const imageSet = (images?.length ? images : [image]).slice(0, 4)
155
+
156
+ return (
157
+ <BaseRemotionScene
158
+ id={id ?? sceneId}
159
+ name={name ?? sceneName ?? title}
160
+ style={{
161
+ background:
162
+ portrait
163
+ ? `linear-gradient(180deg, ${accent ?? '#050505'} 0%, #020617 45%, #000000 100%)`
164
+ : `radial-gradient(circle at 50% 28%, ${secondaryAccent ?? 'rgba(255,255,255,0.08)'} 0%, ${accent ?? '#020617'} 48%, #000000 100%)`,
165
+ color: textColor ?? '#f8fafc',
166
+ padding,
167
+ }}
168
+ >
169
+ <section
170
+ style={{
171
+ width: '100%',
172
+ height: '100%',
173
+ display: 'flex',
174
+ flexDirection: 'column',
175
+ gap: portrait ? 14 : 18,
176
+ minWidth: 0,
177
+ minHeight: 0,
178
+ }}
179
+ >
180
+ {variant === 'wall' ? (
181
+ <div
182
+ style={{
183
+ position: 'relative',
184
+ width: '100%',
185
+ height: '100%',
186
+ aspectRatio: portrait ? '9 / 16' : '16 / 9',
187
+ minWidth: 0,
188
+ minHeight: 0,
189
+ overflow: 'hidden',
190
+ border: '1px solid rgba(255,255,255,0.1)',
191
+ background: 'rgba(2,6,23,0.84)',
192
+ display: 'grid',
193
+ gridTemplateColumns: portrait ? '1fr 1fr' : '1.2fr 0.8fr',
194
+ gridTemplateRows: portrait ? '1fr 1fr' : '1fr 1fr',
195
+ gap: 6,
196
+ padding,
197
+ }}
198
+ >
199
+ {imageSet.map((item, index) => (
200
+ <SceneMotionLayer key={`wall-${index}`} variant="media" delayIndex={index}>
201
+ <div
202
+ style={{
203
+ width: '100%',
204
+ height: '100%',
205
+ minHeight: 0,
206
+ overflow: 'hidden',
207
+ border: '1px solid rgba(255,255,255,0.06)',
208
+ background: 'rgba(15,23,42,0.88)',
209
+ transform: `scale(${1 + (1 - animationProgress) * 0.018})`,
210
+ }}
211
+ >
212
+ {renderCoverMedia(item)}
213
+ </div>
214
+ </SceneMotionLayer>
215
+ ))}
216
+ <div style={{ position: 'absolute', inset: padding, display: 'flex', justifyContent: 'space-between', alignItems: 'start', pointerEvents: 'none' }}>
217
+ <LivePhotoBadge label={badge ?? 'Photo Wall'} />
218
+ <div style={{ fontSize: Math.round(runtime.height * 0.013), letterSpacing: '0.24em', textTransform: 'uppercase', opacity: 0.58 }}>
219
+ {imageSet.length} frames
220
+ </div>
221
+ </div>
222
+ </div>
223
+ ) : variant === 'strip' ? (
224
+ <div
225
+ style={{
226
+ position: 'relative',
227
+ minWidth: 0,
228
+ minHeight: 0,
229
+ overflow: 'hidden',
230
+ border: '1px solid rgba(255,255,255,0.14)',
231
+ background: 'rgba(2,6,23,0.84)',
232
+ display: 'grid',
233
+ gridTemplateRows: 'auto 1fr auto',
234
+ gap: 12,
235
+ padding,
236
+ flex: 1,
237
+ }}
238
+ >
239
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12 }}>
240
+ <LivePhotoBadge label={badge ?? 'Photo Strip'} />
241
+ <div style={{ fontSize: Math.round(runtime.height * 0.013), letterSpacing: '0.2em', textTransform: 'uppercase', opacity: 0.56 }}>
242
+ Live sequence
243
+ </div>
244
+ </div>
245
+ <div
246
+ style={{
247
+ display: 'grid',
248
+ gridTemplateColumns: 'repeat(3, minmax(0, 1fr))',
249
+ gap: 6,
250
+ minWidth: 0,
251
+ minHeight: 0,
252
+ }}
253
+ >
254
+ {imageSet.map((item, index) => (
255
+ <SceneMotionLayer key={`strip-${index}`} variant="media" delayIndex={index}>
256
+ <div
257
+ style={{
258
+ width: '100%',
259
+ height: '100%',
260
+ minHeight: 0,
261
+ overflow: 'hidden',
262
+ border: '1px solid rgba(255,255,255,0.06)',
263
+ background: 'rgba(15,23,42,0.88)',
264
+ transform: `translate3d(0, ${(1 - animationProgress) * 8}px, 0) scale(${1 + (1 - animationProgress) * 0.02})`,
265
+ }}
266
+ >
267
+ {renderCoverMedia(item)}
268
+ </div>
269
+ </SceneMotionLayer>
270
+ ))}
271
+ </div>
272
+ <div style={{ display: 'grid', gap: 8 }}>
273
+ {title ? (
274
+ <SceneMotionLayer variant="hero-title">
275
+ <div style={{ fontSize: titleSize * 0.82, fontWeight: 800, letterSpacing: '-0.06em', lineHeight: 0.95 }}>{title}</div>
276
+ </SceneMotionLayer>
277
+ ) : null}
278
+ {subtitle ? <div style={{ fontSize: subtitleSize, lineHeight: 1.45, opacity: 0.84 }}>{subtitle}</div> : null}
279
+ </div>
280
+ </div>
281
+ ) : (
282
+ <div
283
+ style={{
284
+ position: 'relative',
285
+ width: '100%',
286
+ height: liveMediaHeight,
287
+ minHeight: liveMediaHeight,
288
+ minWidth: 0,
289
+ overflow: 'hidden',
290
+ border: '1px solid rgba(255,255,255,0.1)',
291
+ background: 'rgba(2,6,23,0.84)',
292
+ flex: 1,
293
+ }}
294
+ >
295
+ <SceneMotionLayer
296
+ variant="media"
297
+ style={{
298
+ position: 'absolute',
299
+ inset: '-10%',
300
+ opacity: sceneConfig.sceneType === 'animation' ? 0.48 : 0.2,
301
+ filter: 'blur(32px) saturate(1.16) brightness(0.92)',
302
+ transform: `translate3d(${imageShiftX * 0.28}px, ${imageShiftY * 0.18}px, 0) scale(${backgroundScale})`,
303
+ }}
304
+ >
305
+ <div
306
+ style={{
307
+ width: '100%',
308
+ height: '100%',
309
+ transform: `translate3d(${imageShiftX * 0.4}px, ${imageShiftY * 0.32}px, 0) scale(${imageScale + 0.14})`,
310
+ transformOrigin: 'center center',
311
+ }}
312
+ >
313
+ {renderCoverMedia(image)}
314
+ </div>
315
+ </SceneMotionLayer>
316
+
317
+ <SceneMotionLayer
318
+ variant="media"
319
+ style={{
320
+ position: 'absolute',
321
+ inset: 0,
322
+ transform: `translate3d(${imageShiftX}px, ${imageShiftY}px, 0) scale(${imageScale})`,
323
+ }}
324
+ >
325
+ {renderCoverMedia(image)}
326
+ </SceneMotionLayer>
327
+
328
+ <div
329
+ aria-hidden="true"
330
+ style={{
331
+ position: 'absolute',
332
+ inset: 0,
333
+ background:
334
+ sceneConfig.sceneType === 'animation'
335
+ ? `radial-gradient(circle at 50% 20%, rgba(255,255,255,${spotlightOpacity}) 0%, rgba(255,255,255,0.04) 18%, rgba(2,6,23,0.05) 34%, rgba(2,6,23,${vignetteOpacity}) 100%)`
336
+ : 'linear-gradient(180deg, rgba(2,6,23,0.08) 0%, rgba(2,6,23,0.14) 38%, rgba(2,6,23,0.62) 100%)',
337
+ pointerEvents: 'none',
338
+ }}
339
+ />
340
+
341
+ <div
342
+ aria-hidden="true"
343
+ style={{
344
+ position: 'absolute',
345
+ inset: 0,
346
+ border: '1px solid rgba(255,255,255,0.08)',
347
+ pointerEvents: 'none',
348
+ }}
349
+ />
350
+
351
+ <div
352
+ style={{
353
+ position: 'absolute',
354
+ top: padding,
355
+ left: padding,
356
+ display: 'grid',
357
+ gap: 8,
358
+ zIndex: 2,
359
+ }}
360
+ >
361
+ <LivePhotoBadge label={badge ?? (sceneConfig.sceneType === 'animation' ? 'Live Photo' : 'Photo')} />
362
+ </div>
363
+
364
+ <div
365
+ style={{
366
+ position: 'absolute',
367
+ left: padding,
368
+ right: padding,
369
+ bottom: padding,
370
+ display: 'grid',
371
+ gap: 8,
372
+ zIndex: 2,
373
+ }}
374
+ >
375
+ {title ? (
376
+ <SceneMotionLayer variant="hero-title">
377
+ <div style={{ fontSize: titleSize, fontWeight: 800, letterSpacing: '-0.06em', lineHeight: 0.95 }}>
378
+ {title}
379
+ </div>
380
+ </SceneMotionLayer>
381
+ ) : null}
382
+ {subtitle ? (
383
+ <SceneMotionLayer variant="hero-copy" delayIndex={1}>
384
+ <div style={{ fontSize: subtitleSize, lineHeight: 1.45, opacity: 0.84, maxWidth: portrait ? '88%' : '56%' }}>
385
+ {subtitle}
386
+ </div>
387
+ </SceneMotionLayer>
388
+ ) : null}
389
+ </div>
390
+ </div>
391
+ )}
392
+
393
+ {variant === 'live' ? null : (
394
+ <div
395
+ style={{
396
+ display: 'flex',
397
+ justifyContent: 'space-between',
398
+ alignItems: 'center',
399
+ gap: 16,
400
+ minWidth: 0,
401
+ }}
402
+ >
403
+ <div
404
+ style={{
405
+ fontSize: Math.round(runtime.height * 0.014),
406
+ letterSpacing: '0.24em',
407
+ textTransform: 'uppercase',
408
+ opacity: 0.6,
409
+ minWidth: 0,
410
+ }}
411
+ >
412
+ {sceneConfig.sceneType === 'animation' ? 'Animation preview' : 'Static preview'}
413
+ </div>
414
+ <div
415
+ style={{
416
+ fontSize: Math.round(runtime.height * 0.014),
417
+ letterSpacing: '0.18em',
418
+ textTransform: 'uppercase',
419
+ opacity: 0.46,
420
+ minWidth: 0,
421
+ }}
422
+ >
423
+ {sceneConfig.sceneInFrames} frames
424
+ </div>
425
+ </div>
426
+ )}
427
+ </section>
428
+ </BaseRemotionScene>
429
+ )
430
+ },
431
+ }),
432
+ )
@@ -0,0 +1,20 @@
1
+ import type { ComponentProps } from 'react'
2
+
3
+ import { VideoComponent, type ScenePluginMetadata } from '@vibecuting/component-project-helper'
4
+
5
+ import { DefaultPhotoStripScene, componentMetadata as baseComponentMetadata } from './default-photo-strip-scene'
6
+
7
+ export const componentMetadata: ScenePluginMetadata = {
8
+ ...baseComponentMetadata,
9
+ name: 'DefaultPhotoStripSceneHorizontal',
10
+ description: 'Apple-style photo strip scene (Portrait 9:16 variant)',
11
+ sourceFile: 'src/core/scene/photo/default-photo-strip-scene-Horizontal.tsx',
12
+ aspectRatio: '9:16',
13
+ tags: Array.from(new Set([...(baseComponentMetadata.tags ?? []), 'horizontal'])),
14
+ }
15
+
16
+ export const DefaultPhotoStripSceneHorizontal = VideoComponent(componentMetadata)(function DefaultPhotoStripSceneHorizontal(
17
+ props: ComponentProps<typeof DefaultPhotoStripScene>,
18
+ ) {
19
+ return <DefaultPhotoStripScene {...props} />
20
+ })
@@ -0,0 +1,20 @@
1
+ import type { ComponentProps } from 'react'
2
+
3
+ import { VideoComponent, type ScenePluginMetadata } from '@vibecuting/component-project-helper'
4
+
5
+ import { DefaultPhotoStripScene, componentMetadata as baseComponentMetadata } from './default-photo-strip-scene'
6
+
7
+ export const componentMetadata: ScenePluginMetadata = {
8
+ ...baseComponentMetadata,
9
+ name: 'DefaultPhotoStripSceneLandscape',
10
+ description: 'Apple-style photo strip scene (Landscape 16:9 variant)',
11
+ sourceFile: 'src/core/scene/photo/default-photo-strip-scene-Landscape.tsx',
12
+ aspectRatio: '16:9',
13
+ tags: Array.from(new Set([...(baseComponentMetadata.tags ?? []), 'landscape'])),
14
+ }
15
+
16
+ export const DefaultPhotoStripSceneLandscape = VideoComponent(componentMetadata)(function DefaultPhotoStripSceneLandscape(
17
+ props: ComponentProps<typeof DefaultPhotoStripScene>,
18
+ ) {
19
+ return <DefaultPhotoStripScene {...props} />
20
+ })
@@ -0,0 +1,38 @@
1
+ import { DefaultPhotoStripSceneHorizontal } from './default-photo-strip-scene-Horizontal'
2
+ import { DefaultPhotoStripSceneLandscape } from './default-photo-strip-scene-Landscape'
3
+
4
+ const photoUrl = 'https://heyaai-statics-1342239856.cos.ap-guangzhou.myqcloud.com/tmp/premium_photo-1779404552087-0f905162376f-121.jpeg'
5
+ const image = <img src={photoUrl} alt="Photo strip" />
6
+
7
+ const args = {
8
+ title: 'Three Beats',
9
+ subtitle: 'A tight strip of stills for storytelling, chapter markers, or memory sequencing.',
10
+ badge: 'Strip',
11
+ image,
12
+ images: [image, image, image],
13
+ }
14
+
15
+ const meta = {
16
+ title: 'core/scene/photo/DefaultPhotoStripSceneLandscape',
17
+ component: DefaultPhotoStripSceneLandscape,
18
+ args,
19
+ }
20
+
21
+ export default meta
22
+
23
+ export const Default = {
24
+ render: (storyArgs: typeof args) => <DefaultPhotoStripSceneLandscape {...storyArgs} />,
25
+ parameters: { scenePreviewAspectRatio: '16:9' },
26
+ }
27
+ export const Animated = {
28
+ render: (storyArgs: typeof args) => <DefaultPhotoStripSceneLandscape {...storyArgs} />,
29
+ parameters: { scenePreviewAspectRatio: '16:9', scenePreviewMode: 'animation' },
30
+ }
31
+ export const Horizontal = {
32
+ render: (storyArgs: typeof args) => <DefaultPhotoStripSceneHorizontal {...storyArgs} />,
33
+ parameters: { scenePreviewAspectRatio: '9:16' },
34
+ }
35
+ export const HorizontalAnimated = {
36
+ render: (storyArgs: typeof args) => <DefaultPhotoStripSceneHorizontal {...storyArgs} />,
37
+ parameters: { scenePreviewAspectRatio: '9:16', scenePreviewMode: 'animation' },
38
+ }
@@ -0,0 +1,19 @@
1
+ import type { ComponentProps } from 'react'
2
+
3
+ import { VideoComponent, type ScenePluginMetadata } from '@vibecuting/component-project-helper'
4
+
5
+ import { DefaultLivePhotoScene, componentMetadata as baseComponentMetadata } from './default-live-photo-scene'
6
+
7
+ export const componentMetadata: ScenePluginMetadata = {
8
+ ...baseComponentMetadata,
9
+ name: 'DefaultPhotoStripScene',
10
+ description: 'Apple-style photo strip scene',
11
+ sourceFile: 'src/core/scene/photo/default-photo-strip-scene.tsx',
12
+ tags: Array.from(new Set([...(baseComponentMetadata.tags ?? []), 'strip', 'sequence'])),
13
+ }
14
+
15
+ export const DefaultPhotoStripScene = VideoComponent(componentMetadata)(function DefaultPhotoStripScene(
16
+ props: ComponentProps<typeof DefaultLivePhotoScene>,
17
+ ) {
18
+ return <DefaultLivePhotoScene {...props} variant="strip" />
19
+ })
@@ -0,0 +1,20 @@
1
+ import type { ComponentProps } from 'react'
2
+
3
+ import { VideoComponent, type ScenePluginMetadata } from '@vibecuting/component-project-helper'
4
+
5
+ import { DefaultPhotoWallScene, componentMetadata as baseComponentMetadata } from './default-photo-wall-scene'
6
+
7
+ export const componentMetadata: ScenePluginMetadata = {
8
+ ...baseComponentMetadata,
9
+ name: 'DefaultPhotoWallSceneHorizontal',
10
+ description: 'Apple-style photo wall scene (Portrait 9:16 variant)',
11
+ sourceFile: 'src/core/scene/photo/default-photo-wall-scene-Horizontal.tsx',
12
+ aspectRatio: '9:16',
13
+ tags: Array.from(new Set([...(baseComponentMetadata.tags ?? []), 'horizontal'])),
14
+ }
15
+
16
+ export const DefaultPhotoWallSceneHorizontal = VideoComponent(componentMetadata)(function DefaultPhotoWallSceneHorizontal(
17
+ props: ComponentProps<typeof DefaultPhotoWallScene>,
18
+ ) {
19
+ return <DefaultPhotoWallScene {...props} />
20
+ })
@@ -0,0 +1,20 @@
1
+ import type { ComponentProps } from 'react'
2
+
3
+ import { VideoComponent, type ScenePluginMetadata } from '@vibecuting/component-project-helper'
4
+
5
+ import { DefaultPhotoWallScene, componentMetadata as baseComponentMetadata } from './default-photo-wall-scene'
6
+
7
+ export const componentMetadata: ScenePluginMetadata = {
8
+ ...baseComponentMetadata,
9
+ name: 'DefaultPhotoWallSceneLandscape',
10
+ description: 'Apple-style photo wall scene (Landscape 16:9 variant)',
11
+ sourceFile: 'src/core/scene/photo/default-photo-wall-scene-Landscape.tsx',
12
+ aspectRatio: '16:9',
13
+ tags: Array.from(new Set([...(baseComponentMetadata.tags ?? []), 'landscape'])),
14
+ }
15
+
16
+ export const DefaultPhotoWallSceneLandscape = VideoComponent(componentMetadata)(function DefaultPhotoWallSceneLandscape(
17
+ props: ComponentProps<typeof DefaultPhotoWallScene>,
18
+ ) {
19
+ return <DefaultPhotoWallScene {...props} />
20
+ })
@@ -0,0 +1,38 @@
1
+ import { DefaultPhotoWallSceneHorizontal } from './default-photo-wall-scene-Horizontal'
2
+ import { DefaultPhotoWallSceneLandscape } from './default-photo-wall-scene-Landscape'
3
+
4
+ const photoUrl = 'https://heyaai-statics-1342239856.cos.ap-guangzhou.myqcloud.com/tmp/premium_photo-1779404552087-0f905162376f-121.jpeg'
5
+ const image = <img src={photoUrl} alt="Photo wall" />
6
+
7
+ const args = {
8
+ title: 'Four Moments',
9
+ subtitle: 'A photo wall that can duplicate or mix multiple stills with a soft cinematic drift.',
10
+ badge: 'Wall',
11
+ image,
12
+ images: [image, image, image, image],
13
+ }
14
+
15
+ const meta = {
16
+ title: 'core/scene/photo/DefaultPhotoWallSceneLandscape',
17
+ component: DefaultPhotoWallSceneLandscape,
18
+ args,
19
+ }
20
+
21
+ export default meta
22
+
23
+ export const Default = {
24
+ render: (storyArgs: typeof args) => <DefaultPhotoWallSceneLandscape {...storyArgs} />,
25
+ parameters: { scenePreviewAspectRatio: '16:9' },
26
+ }
27
+ export const Animated = {
28
+ render: (storyArgs: typeof args) => <DefaultPhotoWallSceneLandscape {...storyArgs} />,
29
+ parameters: { scenePreviewAspectRatio: '16:9', scenePreviewMode: 'animation' },
30
+ }
31
+ export const Horizontal = {
32
+ render: (storyArgs: typeof args) => <DefaultPhotoWallSceneHorizontal {...storyArgs} />,
33
+ parameters: { scenePreviewAspectRatio: '9:16' },
34
+ }
35
+ export const HorizontalAnimated = {
36
+ render: (storyArgs: typeof args) => <DefaultPhotoWallSceneHorizontal {...storyArgs} />,
37
+ parameters: { scenePreviewAspectRatio: '9:16', scenePreviewMode: 'animation' },
38
+ }
@@ -0,0 +1,19 @@
1
+ import type { ComponentProps } from 'react'
2
+
3
+ import { VideoComponent, type ScenePluginMetadata } from '@vibecuting/component-project-helper'
4
+
5
+ import { DefaultLivePhotoScene, componentMetadata as baseComponentMetadata } from './default-live-photo-scene'
6
+
7
+ export const componentMetadata: ScenePluginMetadata = {
8
+ ...baseComponentMetadata,
9
+ name: 'DefaultPhotoWallScene',
10
+ description: 'Apple-style photo wall scene',
11
+ sourceFile: 'src/core/scene/photo/default-photo-wall-scene.tsx',
12
+ tags: Array.from(new Set([...(baseComponentMetadata.tags ?? []), 'wall', 'collage'])),
13
+ }
14
+
15
+ export const DefaultPhotoWallScene = VideoComponent(componentMetadata)(function DefaultPhotoWallScene(
16
+ props: ComponentProps<typeof DefaultLivePhotoScene>,
17
+ ) {
18
+ return <DefaultLivePhotoScene {...props} variant="wall" />
19
+ })
@@ -0,0 +1,44 @@
1
+ import { DefaultEffectsPhotoSceneHorizontal } from './default-effects-photo-scene-Horizontal'
2
+
3
+ const photoUrl = 'https://heyaai-statics-1342239856.cos.ap-guangzhou.myqcloud.com/tmp/premium_photo-1779404552087-0f905162376f-121.jpeg'
4
+
5
+ const meta = {
6
+ title: 'core/scene/photo/effects-photo/DefaultEffectsPhotoSceneHorizontal',
7
+ component: DefaultEffectsPhotoSceneHorizontal,
8
+ args: {
9
+ title: 'Golden Echo',
10
+ subtitle: 'Layered photo grading with glow, grain, warmth and a cinematic vignette.',
11
+ badge: 'Effects Photo',
12
+ imageSrc: photoUrl,
13
+ effects: [
14
+ { type: 'glow', intensity: 0.55 },
15
+ { type: 'vignette', intensity: 0.65 },
16
+ { type: 'grain', intensity: 0.12 },
17
+ { type: 'warmth', intensity: 0.18, hue: '#f59e0b' },
18
+ ],
19
+ accent: '#020617',
20
+ secondaryAccent: '#7c3aed',
21
+ },
22
+ }
23
+
24
+ export default meta
25
+
26
+ export const Default = {
27
+ parameters: {
28
+ scenePreviewAspectRatio: '9:16',
29
+ },
30
+ }
31
+
32
+ export const Animated = {
33
+ parameters: {
34
+ scenePreviewAspectRatio: '9:16',
35
+ scenePreviewMode: 'animation',
36
+ },
37
+ }
38
+
39
+ export const Both = {
40
+ parameters: {
41
+ scenePreviewAspectRatio: '9:16',
42
+ scenePreviewMode: 'both',
43
+ },
44
+ }