From 3cde2c74c37f0d487708450b3b12be6c78576e62 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 28 Nov 2025 19:51:47 +0000 Subject: [PATCH 1/5] chore: initialize preset generation From d52ded5b80c07c450df848da02ecb88537300577 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 28 Nov 2025 19:57:11 +0000 Subject: [PATCH 2/5] feat: add 3D Carousel Spin Transition (validation failed - needs review) Generated from issue #315 --- .../generated/carousel-3d-spin-transition.ts | 321 ++++++++++++++++++ 1 file changed, 321 insertions(+) create mode 100644 apps/mediamake/components/editor/presets/registry/generated/carousel-3d-spin-transition.ts diff --git a/apps/mediamake/components/editor/presets/registry/generated/carousel-3d-spin-transition.ts b/apps/mediamake/components/editor/presets/registry/generated/carousel-3d-spin-transition.ts new file mode 100644 index 000000000..f48823fb0 --- /dev/null +++ b/apps/mediamake/components/editor/presets/registry/generated/carousel-3d-spin-transition.ts @@ -0,0 +1,321 @@ +/** + * 3D Carousel Spin Transition Preset + * + * This preset creates a 3D cylindrical carousel transition effect where images rotate + * around a virtual drum axis. The outgoing image rotates away to the left (rotateY -90deg) + * while the incoming image enters from the right (rotateY 90deg), both with synchronized + * X-axis translation and depth scaling to create a realistic cylindrical rotation illusion. + * + * Features: + * - **3D Perspective Transform**: Uses perspective(1000px) on container for 3D depth + * - **Synchronized Rotation**: Both images appear mounted on the same rotating drum + * - **Depth Illusion**: Subtle scale reduction (to 0.9) at rotation midpoint enhances depth + * - **Smooth Overlap**: 0.8s transition period with controlled z-index layering + * - **Cylindrical Effect**: Combined rotateY and translateX creates barrel rotation + * + * Use cases: + * - Creating immersive 3D image transitions + * - Building carousel-style photo galleries + * - Adding depth to slideshow presentations + * - Creating engaging product showcase transitions + */ + +import z from 'zod'; +import type { + PresetMetadata, + PresetOutput, + PresetPassedProps, +} from '../../types'; +import type { RenderableComponentData } from '@microfox/datamotion'; + +const presetParams = z.object({ + media1: z + .object({ + src: z.string().describe('Source URL of outgoing media (image or video)'), + type: z + .enum(['image', 'video']) + .optional() + .describe('Media type (auto-detected if omitted)'), + duration: z.number().describe('Duration of outgoing media in seconds'), + }) + .describe('Outgoing media configuration'), + media2: z + .object({ + src: z.string().describe('Source URL of incoming media (image or video)'), + type: z + .enum(['image', 'video']) + .optional() + .describe('Media type (auto-detected if omitted)'), + duration: z.number().describe('Duration of incoming media in seconds'), + }) + .describe('Incoming media configuration'), + overlapDuration: z + .number() + .min(0.1) + .max(5) + .default(0.8) + .describe('Duration of transition overlap in seconds (both images visible)'), + perspective: z + .number() + .min(500) + .max(2000) + .default(1000) + .describe('Perspective distance in pixels for 3D transform'), + rotationAngle: z + .number() + .min(45) + .max(180) + .default(90) + .describe('Rotation angle in degrees for carousel spin'), + translateDistance: z + .number() + .min(50) + .max(500) + .default(200) + .describe('Horizontal translation distance in pixels for depth effect'), + scaleAtMidpoint: z + .number() + .min(0.5) + .max(1) + .default(0.9) + .describe('Scale value at rotation midpoint (creates depth illusion)'), +}); + +type PresetParams = z.infer; + +const presetExecution = ( + params: PresetParams, + props: PresetPassedProps, +): PresetOutput => { + const { + media1, + media2, + overlapDuration, + perspective, + rotationAngle, + translateDistance, + scaleAtMidpoint, + } = params; + + // Helper: Determine component ID based on media type or extension + const getMediaComponentId = ( + src: string, + type?: 'image' | 'video', + ): 'ImageAtom' | 'VideoAtom' => { + if (type === 'video') return 'VideoAtom'; + if (type === 'image') return 'ImageAtom'; + + // Auto-detect from extension + if (src.match(/\.(mp4|webm|mov|avi|mkv|flv|wmv)$/i)) return 'VideoAtom'; + return 'ImageAtom'; + }; + + const media1ComponentId = getMediaComponentId(media1.src, media1.type); + const media2ComponentId = getMediaComponentId(media2.src, media2.type); + + // Calculate total duration (sum minus overlap) + const totalDuration = media1.duration + media2.duration - overlapDuration; + + // Timing calculations + const outgoingStart = 0; + const outgoingDuration = media1.duration; + const outgoingEffectStart = media1.duration - overlapDuration; + const outgoingEffectDuration = overlapDuration; + + const incomingStart = media1.duration - overlapDuration; + const incomingDuration = media2.duration; + const incomingEffectStart = 0; // Relative to incoming image start + const incomingEffectDuration = overlapDuration; + + // Build outgoing media atom + const outgoingMedia: RenderableComponentData = { + id: 'outgoing-image', + type: 'atom', + componentId: media1ComponentId, + data: { + src: media1.src, + className: 'absolute inset-0 w-full h-full object-cover', + style: { + zIndex: 1, + }, + }, + context: { + timing: { + start: outgoingStart, + duration: outgoingDuration, + }, + }, + effects: [], + }; + + // Build incoming media atom + const incomingMedia: RenderableComponentData = { + id: 'incoming-image', + type: 'atom', + componentId: media2ComponentId, + data: { + src: media2.src, + className: 'absolute inset-0 w-full h-full object-cover', + style: { + zIndex: 2, + }, + }, + context: { + timing: { + start: incomingStart, + duration: incomingDuration, + }, + }, + effects: [], + }; + + // Outgoing opacity effect + const outgoingOpacityEffect = { + id: 'outgoing-opacity-effect', + componentId: 'generic', + data: { + type: 'ease-in-out' as const, + start: outgoingEffectStart, + duration: outgoingEffectDuration, + mode: 'provider' as const, + targetIds: ['outgoing-image'], + ranges: [ + { key: 'opacity', val: 1, prog: 0 }, + { key: 'opacity', val: 0, prog: 1 }, + ], + }, + }; + + // Outgoing transform effect (rotation + translation + scale) + const outgoingTransformEffect = { + id: 'outgoing-transform-effect', + componentId: 'generic', + data: { + type: 'ease-in-out' as const, + start: outgoingEffectStart, + duration: outgoingEffectDuration, + mode: 'provider' as const, + targetIds: ['outgoing-image'], + ranges: [ + { key: 'rotateY', val: 0, prog: 0 }, + { key: 'rotateY', val: -rotationAngle, prog: 1 }, + { key: 'translateX', val: 0, prog: 0 }, + { key: 'translateX', val: -translateDistance, prog: 1 }, + { key: 'scale', val: 1, prog: 0 }, + { key: 'scale', val: scaleAtMidpoint, prog: 0.5 }, + { key: 'scale', val: 1, prog: 1 }, + ], + }, + }; + + // Incoming opacity effect + const incomingOpacityEffect = { + id: 'incoming-opacity-effect', + componentId: 'generic', + data: { + type: 'ease-in-out' as const, + start: incomingEffectStart, + duration: incomingEffectDuration, + mode: 'provider' as const, + targetIds: ['incoming-image'], + ranges: [ + { key: 'opacity', val: 0, prog: 0 }, + { key: 'opacity', val: 1, prog: 1 }, + ], + }, + }; + + // Incoming transform effect (rotation + translation + scale) + const incomingTransformEffect = { + id: 'incoming-transform-effect', + componentId: 'generic', + data: { + type: 'ease-in-out' as const, + start: incomingEffectStart, + duration: incomingEffectDuration, + mode: 'provider' as const, + targetIds: ['incoming-image'], + ranges: [ + { key: 'rotateY', val: rotationAngle, prog: 0 }, + { key: 'rotateY', val: 0, prog: 1 }, + { key: 'translateX', val: translateDistance, prog: 0 }, + { key: 'translateX', val: 0, prog: 1 }, + { key: 'scale', val: scaleAtMidpoint, prog: 0 }, + { key: 'scale', val: 1, prog: 1 }, + ], + }, + }; + + // Attach effects to media atoms + outgoingMedia.effects = [outgoingOpacityEffect, outgoingTransformEffect]; + incomingMedia.effects = [incomingOpacityEffect, incomingTransformEffect]; + + // Root container with perspective + const rootContainer: RenderableComponentData = { + id: 'carousel-3d-spin-container', + type: 'layout', + componentId: 'BaseLayout', + data: { + containerProps: { + className: 'absolute inset-0', + style: { + perspective: `${perspective}px`, + perspectiveOrigin: '50% 50%', + }, + }, + }, + context: { + timing: { + start: 0, + duration: totalDuration, + }, + }, + childrenData: [outgoingMedia, incomingMedia], + }; + + return { + output: { + childrenData: [rootContainer] as RenderableComponentData[], + }, + options: { + attachedToId: 'BaseScene', + }, + }; +}; + +const presetMetadata: PresetMetadata = { + id: 'carousel-3d-spin-transition', + title: '3D Carousel Spin Transition', + description: + 'A 3D cylindrical carousel transition effect that rotates images around a virtual drum axis. The outgoing image rotates away to the left (rotateY -90deg) while the incoming image enters from the right (rotateY 90deg), both with synchronized X-axis translation and depth scaling to create a realistic cylindrical rotation illusion. Uses perspective transforms and controlled z-index layering during an 0.8s overlap period.', + type: 'predefined', + presetType: 'children', + tags: ['transition', '3d', 'carousel', 'rotation', 'cylindrical'], + defaultInputParams: { + media1: { + src: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1920&h=1080&fit=crop', + type: 'image', + duration: 5, + }, + media2: { + src: 'https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=1920&h=1080&fit=crop', + type: 'image', + duration: 5, + }, + overlapDuration: 0.8, + perspective: 1000, + rotationAngle: 90, + translateDistance: 200, + scaleAtMidpoint: 0.9, + }, + dependencies: { + presets: [], + helpers: [], + }, +}; + +export const carousel3dSpinTransitionPreset = { + metadata: presetMetadata, + presetFunction: presetExecution.toString(), + presetParams: z.toJSONSchema(presetParams) as any, +}; From b863b082666e6dde5ec96829933731c7c955b84b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 28 Nov 2025 19:57:12 +0000 Subject: [PATCH 3/5] feat: add Gallery Carousel 3D Transition (validation failed - needs review) Generated from issue #315 --- .../gallery-carousel-3d-transition.ts | 274 ++++++++++++++++++ 1 file changed, 274 insertions(+) create mode 100644 apps/mediamake/components/editor/presets/registry/generated/gallery-carousel-3d-transition.ts diff --git a/apps/mediamake/components/editor/presets/registry/generated/gallery-carousel-3d-transition.ts b/apps/mediamake/components/editor/presets/registry/generated/gallery-carousel-3d-transition.ts new file mode 100644 index 000000000..2ff95cb7a --- /dev/null +++ b/apps/mediamake/components/editor/presets/registry/generated/gallery-carousel-3d-transition.ts @@ -0,0 +1,274 @@ +/** + * Gallery Carousel 3D Transition Preset + * + * This preset creates a horizontal gallery carousel transition with a 3D perspective shift + * that mimics a physical rotating display case. Images rotate on the Y-axis with scaling + * and motion blur effects, creating an elegant cylindrical gallery space illusion. + * + * Features: + * - 3D perspective transform with preserve-3d style + * - Cylindrical rotation with Y-axis transforms + * - Motion blur during transition peak + * - Scale animation (0.85 to 1.0) + * - Organic vertical offset movement + * - Smooth 1-second overlap period + * + * Use cases: + * - Creating elegant image transitions + * - Building photo gallery presentations + * - Showcasing product images with depth + * - Creating cinematic slideshow effects + */ + +import z from 'zod'; +import type { + PresetMetadata, + PresetOutput, + PresetPassedProps, +} from '../../types'; +import type { RenderableComponentData } from '@microfox/datamotion'; + +const presetParams = z.object({ + media1: z.object({ + src: z.string().describe('Source URL of the first image'), + duration: z.number().describe('Duration of the first image in seconds'), + }), + media2: z.object({ + src: z.string().describe('Source URL of the second image'), + duration: z.number().describe('Duration of the second image in seconds'), + }), + transitionDuration: z + .number() + .default(1.0) + .describe('Duration of the transition overlap in seconds'), +}); + +type PresetParams = z.infer; + +const presetExecution = ( + params: PresetParams, + props: PresetPassedProps, +): PresetOutput => { + const { media1, media2, transitionDuration } = params; + + // Calculate container duration (sum of media durations minus overlap) + const containerDuration = media1.duration + media2.duration - transitionDuration; + + // Outgoing image (media1) - slides left and rotates + const outgoingImage: RenderableComponentData = { + id: 'gallery-outgoing-image', + type: 'atom', + componentId: 'ImageAtom', + data: { + src: media1.src, + fit: 'cover', + className: 'absolute inset-0', + style: { + zIndex: 10, + }, + }, + context: { + timing: { + start: 0, + duration: media1.duration, + }, + }, + effects: [ + // Opacity fade out + { + id: 'outgoing-opacity-effect', + componentId: 'generic', + data: { + type: 'linear', + start: media1.duration - transitionDuration, + duration: transitionDuration, + mode: 'provider', + targetIds: ['gallery-outgoing-image'], + ranges: [ + { key: 'opacity', val: 1, prog: 0 }, + { key: 'opacity', val: 0.3, prog: 0.5 }, + { key: 'opacity', val: 0, prog: 1 }, + ], + }, + }, + // Transform: rotateY + translateX + scale + { + id: 'outgoing-transform-effect', + componentId: 'generic', + data: { + type: 'ease-in-out', + start: media1.duration - transitionDuration, + duration: transitionDuration, + mode: 'provider', + targetIds: ['gallery-outgoing-image'], + ranges: [ + { key: 'rotateY', val: 0, prog: 0 }, + { key: 'rotateY', val: -45, prog: 1 }, + { key: 'translateX', val: 0, prog: 0 }, + { key: 'translateX', val: -50, prog: 1 }, + { key: 'scale', val: 1, prog: 0 }, + { key: 'scale', val: 0.85, prog: 1 }, + ], + }, + }, + // Blur effect (0px -> 3px -> 0px) + { + id: 'outgoing-blur-effect', + componentId: 'generic', + data: { + type: 'ease-in-out', + start: media1.duration - transitionDuration, + duration: transitionDuration, + mode: 'provider', + targetIds: ['gallery-outgoing-image'], + ranges: [ + { key: 'blur', val: 0, prog: 0 }, + { key: 'blur', val: 3, prog: 0.5 }, + { key: 'blur', val: 0, prog: 1 }, + ], + }, + }, + ], + }; + + // Incoming image (media2) - enters from right with opposite rotation + const incomingImage: RenderableComponentData = { + id: 'gallery-incoming-image', + type: 'atom', + componentId: 'ImageAtom', + data: { + src: media2.src, + fit: 'cover', + className: 'absolute inset-0', + style: { + zIndex: 20, + }, + }, + context: { + timing: { + start: media1.duration - transitionDuration, + duration: media2.duration, + }, + }, + effects: [ + // Opacity fade in + { + id: 'incoming-opacity-effect', + componentId: 'generic', + data: { + type: 'linear', + start: 0, + duration: transitionDuration, + mode: 'provider', + targetIds: ['gallery-incoming-image'], + ranges: [ + { key: 'opacity', val: 0, prog: 0 }, + { key: 'opacity', val: 0.3, prog: 0.5 }, + { key: 'opacity', val: 1, prog: 1 }, + ], + }, + }, + // Transform: rotateY + translateX + scale + translateY + { + id: 'incoming-transform-effect', + componentId: 'generic', + data: { + type: 'ease-in-out', + start: 0, + duration: transitionDuration, + mode: 'provider', + targetIds: ['gallery-incoming-image'], + ranges: [ + { key: 'rotateY', val: 45, prog: 0 }, + { key: 'rotateY', val: 0, prog: 1 }, + { key: 'translateX', val: 50, prog: 0 }, + { key: 'translateX', val: 0, prog: 1 }, + { key: 'scale', val: 0.85, prog: 0 }, + { key: 'scale', val: 1, prog: 1 }, + { key: 'translateY', val: -10, prog: 0 }, + { key: 'translateY', val: 0, prog: 1 }, + ], + }, + }, + // Blur effect (3px -> 0px) + { + id: 'incoming-blur-effect', + componentId: 'generic', + data: { + type: 'ease-in-out', + start: 0, + duration: transitionDuration, + mode: 'provider', + targetIds: ['gallery-incoming-image'], + ranges: [ + { key: 'blur', val: 3, prog: 0 }, + { key: 'blur', val: 0, prog: 1 }, + ], + }, + }, + ], + }; + + // Root container with 3D perspective + const rootContainer: RenderableComponentData = { + id: 'gallery-carousel-container', + type: 'layout', + componentId: 'BaseLayout', + data: { + containerProps: { + className: 'absolute inset-0 overflow-hidden', + style: { + perspective: '800px', + transformStyle: 'preserve-3d', + }, + }, + }, + context: { + timing: { + start: 0, + duration: containerDuration, + }, + }, + childrenData: [outgoingImage, incomingImage], + }; + + return { + output: { + childrenData: [rootContainer] as RenderableComponentData[], + }, + options: { + attachedToId: 'BaseScene', + }, + }; +}; + +const presetMetadata: PresetMetadata = { + id: 'gallery-carousel-3d-transition', + title: 'Gallery Carousel 3D Transition', + description: + 'Horizontal gallery carousel transition with 3D perspective shift that mimics a physical rotating display case. Features preserve-3d transform style, cylindrical rotation with Y-axis transforms, motion blur, and organic vertical offset movement.', + type: 'predefined', + presetType: 'children', + tags: ['transition', 'gallery', 'carousel', '3d', 'rotation', 'perspective'], + defaultInputParams: { + media1: { + src: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1920&h=1080&fit=crop', + duration: 5, + }, + media2: { + src: 'https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=1920&h=1080&fit=crop', + duration: 5, + }, + transitionDuration: 1.0, + }, + dependencies: { + presets: [], + helpers: [], + }, +}; + +export const galleryCarousel3dTransitionPreset = { + metadata: presetMetadata, + presetFunction: presetExecution.toString(), + presetParams: z.toJSONSchema(presetParams) as any, +}; From 710cec3c3a4a22d352ac1f52341c271c5fc2ef43 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 28 Nov 2025 19:57:12 +0000 Subject: [PATCH 4/5] feat: add Cylindrical Rotation Carousel Transition (validation failed - needs review) Succeeded after 1 retry Generated from issue #315 --- .../cylindrical-rotation-carousel.ts | 347 ++++++++++++++++++ 1 file changed, 347 insertions(+) create mode 100644 apps/mediamake/components/editor/presets/registry/generated/cylindrical-rotation-carousel.ts diff --git a/apps/mediamake/components/editor/presets/registry/generated/cylindrical-rotation-carousel.ts b/apps/mediamake/components/editor/presets/registry/generated/cylindrical-rotation-carousel.ts new file mode 100644 index 000000000..4c100ad7e --- /dev/null +++ b/apps/mediamake/components/editor/presets/registry/generated/cylindrical-rotation-carousel.ts @@ -0,0 +1,347 @@ +/** + * Cylindrical Rotation Carousel Transition Preset + * + * A dynamic cylindrical rotation carousel with exaggerated perspective for YouTube-style impact. + * This transition features fast 0.5 second overlap with aggressive rotation angles (rotateY -120deg + * for outgoing, 120deg to 0 for incoming) to create a dramatic spinning effect. + * + * Features: + * - Fast 0.5s overlap with aggressive rotation angles + * - RotateX tilt (±5deg) during transition for dynamic 3D feel + * - Scale punch effect where incoming overshoots to 1.05 before settling + * - Closer perspective (600px) for dramatic depth distortion + * - Subtle drop shadow that intensifies during rotation + * - Outgoing uses ease-out timing, incoming uses ease-in-out for smooth landing + * + * Use cases: + * - Creating dramatic transitions between media items + * - Building engaging video sequences with 3D effects + * - Adding cinematic rotation effects to slideshows + * - Creating YouTube-style impact transitions + */ + +import z from 'zod'; +import type { + PresetMetadata, + PresetOutput, + PresetPassedProps, +} from '../../types'; +import type { RenderableComponentData } from '@microfox/datamotion'; + +const presetParams = z.object({ + media1: z.object({ + src: z.string().describe('Source URL of outgoing media (image or video)'), + type: z.enum(['image', 'video']).describe('Media type'), + duration: z.number().describe('Duration of outgoing media in seconds'), + }), + media2: z.object({ + src: z.string().describe('Source URL of incoming media (image or video)'), + type: z.enum(['image', 'video']).describe('Media type'), + duration: z.number().describe('Duration of incoming media in seconds'), + }), + transitionDuration: z + .number() + .default(0.5) + .describe('Duration of transition overlap in seconds (default: 0.5s)'), + perspective: z + .number() + .default(600) + .describe('Perspective distance in pixels for 3D effect (default: 600px)'), + rotationAngle: z + .number() + .default(120) + .describe('Rotation angle in degrees (default: 120deg)'), + tiltAngle: z + .number() + .default(5) + .describe('RotateX tilt angle in degrees (default: 5deg)'), + scaleOvershoot: z + .number() + .default(1.05) + .describe('Scale overshoot value for punch effect (default: 1.05)'), + shadowIntensity: z + .number() + .default(0.4) + .describe('Drop shadow intensity (0-1, default: 0.4)'), +}); + +type PresetParams = z.infer; + +const presetExecution = ( + params: PresetParams, + props: PresetPassedProps, +): PresetOutput => { + const { + media1, + media2, + transitionDuration, + perspective, + rotationAngle, + tiltAngle, + scaleOvershoot, + shadowIntensity, + } = params; + + // Calculate BaseLayout duration + const baseLayoutDuration = + media1.duration + media2.duration - transitionDuration; + + // Determine component IDs based on media type + const media1ComponentId = media1.type === 'video' ? 'VideoAtom' : 'ImageAtom'; + const media2ComponentId = media2.type === 'video' ? 'VideoAtom' : 'ImageAtom'; + + // Calculate timing values + const outgoingEffectStart = media1.duration - transitionDuration; + const incomingStart = media1.duration - transitionDuration; + const incomingDuration = media2.duration; + + // Shadow configuration + const maxShadowBlur = 40; + const maxShadowOffset = 20; + const shadowColor = `rgba(0,0,0,${shadowIntensity})`; + + const childrenData: RenderableComponentData[] = [ + // Outgoing media + { + id: 'outgoing-media', + type: 'atom', + componentId: media1ComponentId, + data: { + src: media1.src, + className: 'w-full h-full object-cover', + style: { + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: '100%', + zIndex: 5, + }, + }, + context: { + timing: { + start: 0, + duration: media1.duration, + }, + }, + effects: [ + // Opacity fade out + { + id: 'outgoing-opacity', + componentId: 'generic', + data: { + type: 'ease-out', + start: outgoingEffectStart, + duration: transitionDuration, + mode: 'provider', + targetIds: ['outgoing-media'], + ranges: [ + { key: 'opacity', val: 1, prog: 0 }, + { key: 'opacity', val: 0, prog: 1 }, + ], + }, + }, + // Transform: rotateY(-120deg), rotateX(5deg), scale(0.7) + { + id: 'outgoing-transform', + componentId: 'generic', + data: { + type: 'ease-out', + start: outgoingEffectStart, + duration: transitionDuration, + mode: 'provider', + targetIds: ['outgoing-media'], + ranges: [ + { key: 'rotateY', val: '0deg', prog: 0 }, + { key: 'rotateY', val: `${-rotationAngle}deg`, prog: 1 }, + { key: 'rotateX', val: '0deg', prog: 0 }, + { key: 'rotateX', val: `${tiltAngle}deg`, prog: 1 }, + { key: 'scale', val: 1, prog: 0 }, + { key: 'scale', val: 0.7, prog: 1 }, + ], + }, + }, + // Box shadow effect + { + id: 'outgoing-shadow', + componentId: 'generic', + data: { + type: 'ease-out', + start: outgoingEffectStart, + duration: transitionDuration, + mode: 'provider', + targetIds: ['outgoing-media'], + ranges: [ + { key: 'boxShadow', val: '0 0 0 rgba(0,0,0,0)', prog: 0 }, + { + key: 'boxShadow', + val: `0 ${maxShadowOffset}px ${maxShadowBlur}px ${shadowColor}`, + prog: 0.5, + }, + { key: 'boxShadow', val: '0 0 0 rgba(0,0,0,0)', prog: 1 }, + ], + }, + }, + ], + } as RenderableComponentData, + // Incoming media + { + id: 'incoming-media', + type: 'atom', + componentId: media2ComponentId, + data: { + src: media2.src, + className: 'w-full h-full object-cover', + style: { + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: '100%', + zIndex: 10, + }, + }, + context: { + timing: { + start: incomingStart, + duration: incomingDuration, + }, + }, + effects: [ + // Opacity fade in + { + id: 'incoming-opacity', + componentId: 'generic', + data: { + type: 'ease-in-out', + start: 0, + duration: transitionDuration, + mode: 'provider', + targetIds: ['incoming-media'], + ranges: [ + { key: 'opacity', val: 0, prog: 0 }, + { key: 'opacity', val: 1, prog: 1 }, + ], + }, + }, + // Transform with overshoot: rotateY(120deg -> 0), rotateX(-5deg -> 0), scale(0.7 -> 1.05 -> 1) + { + id: 'incoming-transform', + componentId: 'generic', + data: { + type: 'ease-in-out', + start: 0, + duration: 0.7, + mode: 'provider', + targetIds: ['incoming-media'], + ranges: [ + { key: 'rotateY', val: `${rotationAngle}deg`, prog: 0 }, + { key: 'rotateY', val: '0deg', prog: 0.7 }, + { key: 'rotateY', val: '0deg', prog: 1 }, + { key: 'rotateX', val: `${-tiltAngle}deg`, prog: 0 }, + { key: 'rotateX', val: '0deg', prog: 0.7 }, + { key: 'rotateX', val: '0deg', prog: 1 }, + { key: 'scale', val: 0.7, prog: 0 }, + { key: 'scale', val: scaleOvershoot, prog: 0.7 }, + { key: 'scale', val: 1, prog: 1 }, + ], + }, + }, + // Box shadow effect + { + id: 'incoming-shadow', + componentId: 'generic', + data: { + type: 'ease-in-out', + start: 0, + duration: transitionDuration, + mode: 'provider', + targetIds: ['incoming-media'], + ranges: [ + { key: 'boxShadow', val: '0 0 0 rgba(0,0,0,0)', prog: 0 }, + { + key: 'boxShadow', + val: `0 ${maxShadowOffset}px ${maxShadowBlur}px ${shadowColor}`, + prog: 0.5, + }, + { + key: 'boxShadow', + val: `0 10px 20px rgba(0,0,0,${shadowIntensity * 0.5})`, + prog: 1, + }, + ], + }, + }, + ], + } as RenderableComponentData, + ]; + + const rootContainer: RenderableComponentData = { + id: 'cylindrical-rotation-carousel-container', + type: 'layout', + componentId: 'BaseLayout', + data: { + containerProps: { + className: 'absolute inset-0', + style: { + perspective: `${perspective}px`, + perspectiveOrigin: '50% 50%', + }, + }, + }, + context: { + timing: { + start: 0, + duration: baseLayoutDuration, + }, + }, + childrenData, + }; + + return { + output: { + childrenData: [rootContainer] as RenderableComponentData[], + }, + options: { + attachedToId: 'BaseScene', + }, + }; +}; + +const presetMetadata: PresetMetadata = { + id: 'cylindrical-rotation-carousel', + title: 'Cylindrical Rotation Carousel Transition', + description: + 'A dynamic cylindrical rotation carousel with exaggerated perspective for YouTube-style impact. Features fast 0.5s overlap with aggressive rotation angles (rotateY ±120deg), rotateX tilt (±5deg), scale punch effect (1.05 overshoot), and intensifying drop shadow for 3D cylinder illusion. Perspective set to 600px for dramatic depth distortion.', + type: 'predefined', + presetType: 'children', + tags: ['transition', 'carousel', 'rotation', '3d', 'cylindrical', 'youtube'], + defaultInputParams: { + media1: { + src: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1920&h=1080&fit=crop', + type: 'image', + duration: 3, + }, + media2: { + src: 'https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=1920&h=1080&fit=crop', + type: 'image', + duration: 3, + }, + transitionDuration: 0.5, + perspective: 600, + rotationAngle: 120, + tiltAngle: 5, + scaleOvershoot: 1.05, + shadowIntensity: 0.4, + }, + dependencies: { + presets: [], + helpers: [], + }, +}; + +export const cylindricalRotationCarouselPreset = { + metadata: presetMetadata, + presetFunction: presetExecution.toString(), + presetParams: z.toJSONSchema(presetParams) as any, +}; From 5033f7c2be682a6dd737d16babde784f199c9abc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 28 Nov 2025 19:57:13 +0000 Subject: [PATCH 5/5] feat: add Perspective Carousel Spin Transition (validation failed - needs review) Succeeded after 1 retry Generated from issue #315 --- .../perspective-carousel-spin-transition.ts | 277 ++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 apps/mediamake/components/editor/presets/registry/generated/perspective-carousel-spin-transition.ts diff --git a/apps/mediamake/components/editor/presets/registry/generated/perspective-carousel-spin-transition.ts b/apps/mediamake/components/editor/presets/registry/generated/perspective-carousel-spin-transition.ts new file mode 100644 index 000000000..63ea7af23 --- /dev/null +++ b/apps/mediamake/components/editor/presets/registry/generated/perspective-carousel-spin-transition.ts @@ -0,0 +1,277 @@ +/** + * Perspective Carousel Spin Transition + * + * Attention-grabbing transition featuring compound 3D rotations (rotateY + rotateZ) + * with a zoom pulse at the midpoint and vignette overlay. Creates a cylindrical + * motion path with snappy, professional cubic-bezier easing optimized for YouTube. + * + * Features: + * - Compound rotation: rotateY (-90deg primary spin) + rotateZ (15deg secondary tilt) + * - Zoom pulse: Both images scale to 1.1 at transition midpoint + * - 0.7s overlap with cubic-bezier(0.4, 0, 0.2, 1) easing + * - Vignette radial gradient overlay that intensifies during transition + * - 3D perspective container (700px) with preserved transforms + * + * Use cases: + * - YouTube content transitions with high visual impact + * - Attention-grabbing media transitions for social content + * - Professional video montages with 3D effects + * - Creating cylindrical motion paths between media items + */ + +import z from 'zod'; +import type { + PresetMetadata, + PresetOutput, + PresetPassedProps, +} from '../../types'; +import type { RenderableComponentData } from '@microfox/datamotion'; + +const presetParams = z.object({ + media1: z.object({ + src: z.string().describe('Source URL of the outgoing media'), + type: z.enum(['image', 'video']).describe('Media type'), + duration: z.number().describe('Duration in seconds'), + }).describe('Outgoing media configuration'), + media2: z.object({ + src: z.string().describe('Source URL of the incoming media'), + type: z.enum(['image', 'video']).describe('Media type'), + duration: z.number().describe('Duration in seconds'), + }).describe('Incoming media configuration'), + overlapDuration: z + .number() + .default(0.7) + .describe('Duration of transition overlap in seconds (default: 0.7)'), +}); + +type PresetParams = z.infer; + +const presetExecution = ( + params: PresetParams, + props: PresetPassedProps, +): PresetOutput => { + const { media1, media2, overlapDuration } = params; + + // Calculate total duration (sum of media durations minus overlap) + const totalDuration = media1.duration + media2.duration - overlapDuration; + + // Determine component IDs based on media type + const media1ComponentId = media1.type === 'video' ? 'VideoAtom' : 'ImageAtom'; + const media2ComponentId = media2.type === 'video' ? 'VideoAtom' : 'ImageAtom'; + + // Outgoing media starts at 0, lasts for its full duration + const outgoingStart = 0; + const outgoingDuration = media1.duration; + + // Incoming media starts before outgoing ends (overlap), extended by overlap + const incomingStart = media1.duration - overlapDuration; + const incomingDuration = media2.duration; + + // Transition effect timing (for outgoing: starts at end minus overlap) + const outgoingEffectStart = outgoingDuration - overlapDuration; + + const childrenData: RenderableComponentData[] = [ + // Outgoing media (zIndex 10) + { + id: 'outgoing-media', + type: 'atom', + componentId: media1ComponentId, + data: { + src: media1.src, + className: 'absolute inset-0 object-cover', + style: { + zIndex: 10, + }, + }, + context: { + timing: { + start: outgoingStart, + duration: outgoingDuration, + }, + }, + effects: [ + // Outgoing spiral: opacity [1, 0], transform rotateY(0→-90deg) rotateZ(0→15deg) scale(1→0.6) + { + id: 'outgoing-spiral', + componentId: 'generic', + data: { + type: 'cubic-bezier(0.4, 0, 0.2, 1)', + start: outgoingEffectStart, // Relative to outgoing media start + duration: overlapDuration, + mode: 'provider', + targetIds: ['outgoing-media'], + ranges: [ + // Opacity fade out + { key: 'opacity', val: 1, prog: 0 }, + { key: 'opacity', val: 0, prog: 1 }, + // Compound rotation + scale + // rotateY: 0deg → -90deg + { key: 'rotateY', val: 0, prog: 0 }, + { key: 'rotateY', val: -90, prog: 1 }, + // rotateZ: 0deg → 15deg + { key: 'rotateZ', val: 0, prog: 0 }, + { key: 'rotateZ', val: 15, prog: 1 }, + // scale: 1 → 0.6 + { key: 'scale', val: 1, prog: 0 }, + { key: 'scale', val: 0.6, prog: 1 }, + ], + }, + }, + ], + } as RenderableComponentData, + // Incoming media (zIndex 20) + { + id: 'incoming-media', + type: 'atom', + componentId: media2ComponentId, + data: { + src: media2.src, + className: 'absolute inset-0 object-cover', + style: { + zIndex: 20, + }, + }, + context: { + timing: { + start: incomingStart, + duration: incomingDuration, + }, + }, + effects: [ + // Incoming spiral: opacity [0, 1], transform rotateY(90deg→0→0) rotateZ(-15deg→0→0) scale(0.6→1.1→1) + // Keyframes at 0%, 70%, 100% + { + id: 'incoming-spiral', + componentId: 'generic', + data: { + type: 'cubic-bezier(0.4, 0, 0.2, 1)', + start: 0, // Relative to incoming media start + duration: overlapDuration, + mode: 'provider', + targetIds: ['incoming-media'], + ranges: [ + // Opacity fade in + { key: 'opacity', val: 0, prog: 0 }, + { key: 'opacity', val: 1, prog: 0.7 }, + { key: 'opacity', val: 1, prog: 1 }, + // rotateY: 90deg → 0deg (at 70%) → 0deg (at 100%) + { key: 'rotateY', val: 90, prog: 0 }, + { key: 'rotateY', val: 0, prog: 0.7 }, + { key: 'rotateY', val: 0, prog: 1 }, + // rotateZ: -15deg → 0deg (at 70%) → 0deg (at 100%) + { key: 'rotateZ', val: -15, prog: 0 }, + { key: 'rotateZ', val: 0, prog: 0.7 }, + { key: 'rotateZ', val: 0, prog: 1 }, + // scale: 0.6 → 1.1 (at 70%) → 1 (at 100%) + { key: 'scale', val: 0.6, prog: 0 }, + { key: 'scale', val: 1.1, prog: 0.7 }, + { key: 'scale', val: 1, prog: 1 }, + ], + }, + }, + ], + } as RenderableComponentData, + // Vignette overlay (zIndex 30) + { + id: 'vignette-overlay', + type: 'atom', + componentId: 'HTMLBlockAtom', + data: { + html: '
', + className: 'absolute inset-0', + style: { + zIndex: 30, + }, + }, + context: { + timing: { + start: incomingStart, // Starts with incoming media + duration: overlapDuration, + }, + }, + effects: [ + // Vignette opacity pulse: 0 → 0.3 → 0 during transition + { + id: 'vignette-pulse', + componentId: 'generic', + data: { + type: 'cubic-bezier(0.4, 0, 0.2, 1)', + start: 0, // Relative to vignette start + duration: overlapDuration, + mode: 'provider', + targetIds: ['vignette-overlay'], + ranges: [ + { key: 'opacity', val: 0, prog: 0 }, + { key: 'opacity', val: 0.3, prog: 0.5 }, + { key: 'opacity', val: 0, prog: 1 }, + ], + }, + }, + ], + } as RenderableComponentData, + ]; + + const rootContainer: RenderableComponentData = { + id: 'perspective-carousel-spin-container', + type: 'layout', + componentId: 'BaseLayout', + data: { + containerProps: { + className: 'absolute inset-0 overflow-hidden', + style: { + perspective: '700px', + transformStyle: 'preserve-3d', + }, + }, + }, + context: { + timing: { + start: 0, + duration: totalDuration, + }, + }, + childrenData, + }; + + return { + output: { + childrenData: [rootContainer] as RenderableComponentData[], + }, + options: { + attachedToId: 'BaseScene', + }, + }; +}; + +const presetMetadata: PresetMetadata = { + id: 'perspective-carousel-spin-transition', + title: 'Perspective Carousel Spin Transition', + description: + 'Attention-grabbing YouTube transition with compound 3D rotations (rotateY + rotateZ), zoom pulse at midpoint, and vignette overlay. Features cylindrical motion path with cubic-bezier easing for professional, snappy transitions.', + type: 'predefined', + presetType: 'children', + tags: ['transition', 'perspective', '3d', 'carousel', 'spin', 'youtube', 'attention-grabbing'], + defaultInputParams: { + media1: { + src: 'https://example.com/video1.mp4', + type: 'video', + duration: 5, + }, + media2: { + src: 'https://example.com/video2.mp4', + type: 'video', + duration: 5, + }, + overlapDuration: 0.7, + }, + dependencies: { + presets: [], + helpers: [], + }, +}; + +export const perspectiveCarouselSpinTransitionPreset = { + metadata: presetMetadata, + presetFunction: presetExecution.toString(), + presetParams: z.toJSONSchema(presetParams) as any, +};