@codellyson/framely-cli 0.1.0
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/commands/compositions.js +135 -0
- package/commands/preview.js +889 -0
- package/commands/render.js +295 -0
- package/commands/still.js +165 -0
- package/index.js +93 -0
- package/package.json +60 -0
- package/studio/App.css +605 -0
- package/studio/App.jsx +185 -0
- package/studio/CompositionsView.css +399 -0
- package/studio/CompositionsView.jsx +327 -0
- package/studio/PropsEditor.css +195 -0
- package/studio/PropsEditor.tsx +176 -0
- package/studio/RenderDialog.tsx +476 -0
- package/studio/ShareDialog.tsx +200 -0
- package/studio/index.ts +19 -0
- package/studio/player/Player.css +199 -0
- package/studio/player/Player.jsx +355 -0
- package/studio/styles/design-system.css +592 -0
- package/studio/styles/dialogs.css +420 -0
- package/studio/templates/AnimatedGradient.jsx +99 -0
- package/studio/templates/InstagramStory.jsx +172 -0
- package/studio/templates/LowerThird.jsx +139 -0
- package/studio/templates/ProductShowcase.jsx +162 -0
- package/studio/templates/SlideTransition.jsx +211 -0
- package/studio/templates/SocialIntro.jsx +122 -0
- package/studio/templates/SubscribeAnimation.jsx +186 -0
- package/studio/templates/TemplateCard.tsx +58 -0
- package/studio/templates/TemplateFilters.tsx +97 -0
- package/studio/templates/TemplatePreviewDialog.tsx +196 -0
- package/studio/templates/TemplatesMarketplace.css +686 -0
- package/studio/templates/TemplatesMarketplace.tsx +172 -0
- package/studio/templates/TextReveal.jsx +134 -0
- package/studio/templates/UseTemplateDialog.tsx +154 -0
- package/studio/templates/index.ts +45 -0
- package/utils/browser.js +188 -0
- package/utils/codecs.js +200 -0
- package/utils/logger.js +35 -0
- package/utils/props.js +42 -0
- package/utils/render.js +447 -0
- package/utils/validate.js +148 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate, spring } from '@codellyson/framely';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Clean Lower Third Template
|
|
5
|
+
*/
|
|
6
|
+
export function LowerThird({
|
|
7
|
+
name = 'John Doe',
|
|
8
|
+
title = 'CEO & Founder',
|
|
9
|
+
social = '@johndoe',
|
|
10
|
+
accentColor = '#3b82f6',
|
|
11
|
+
}) {
|
|
12
|
+
const frame = useCurrentFrame();
|
|
13
|
+
const { fps, durationInFrames } = useVideoConfig();
|
|
14
|
+
|
|
15
|
+
// Entry animation
|
|
16
|
+
const entrySpring = spring({ frame, fps, config: { damping: 15, stiffness: 120 } });
|
|
17
|
+
|
|
18
|
+
// Line animation
|
|
19
|
+
const lineWidth = interpolate(frame, [0, 25], [0, 100], { extrapolateRight: 'clamp' });
|
|
20
|
+
|
|
21
|
+
// Text slide in
|
|
22
|
+
const nameX = interpolate(entrySpring, [0, 1], [-300, 0]);
|
|
23
|
+
const nameOpacity = interpolate(frame, [5, 20], [0, 1], { extrapolateRight: 'clamp' });
|
|
24
|
+
|
|
25
|
+
const titleX = interpolate(frame, [15, 35], [-200, 0], { extrapolateRight: 'clamp' });
|
|
26
|
+
const titleOpacity = interpolate(frame, [15, 30], [0, 1], { extrapolateRight: 'clamp' });
|
|
27
|
+
|
|
28
|
+
// Exit animation
|
|
29
|
+
const exitStart = durationInFrames - 30;
|
|
30
|
+
const exitProgress = interpolate(frame, [exitStart, durationInFrames], [0, 1], {
|
|
31
|
+
extrapolateLeft: 'clamp',
|
|
32
|
+
extrapolateRight: 'clamp',
|
|
33
|
+
});
|
|
34
|
+
const exitX = interpolate(exitProgress, [0, 1], [0, -400]);
|
|
35
|
+
const exitOpacity = interpolate(exitProgress, [0, 0.5], [1, 0], { extrapolateRight: 'clamp' });
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<AbsoluteFill style={{ background: 'transparent' }}>
|
|
39
|
+
{/* Lower third container */}
|
|
40
|
+
<div
|
|
41
|
+
style={{
|
|
42
|
+
position: 'absolute',
|
|
43
|
+
bottom: 80,
|
|
44
|
+
left: 60,
|
|
45
|
+
transform: `translateX(${exitX}px)`,
|
|
46
|
+
opacity: exitOpacity,
|
|
47
|
+
}}
|
|
48
|
+
>
|
|
49
|
+
{/* Background shape */}
|
|
50
|
+
<div
|
|
51
|
+
style={{
|
|
52
|
+
position: 'relative',
|
|
53
|
+
padding: '16px 24px',
|
|
54
|
+
background: 'rgba(0, 0, 0, 0.85)',
|
|
55
|
+
backdropFilter: 'blur(10px)',
|
|
56
|
+
borderRadius: 4,
|
|
57
|
+
borderLeft: `4px solid ${accentColor}`,
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
{/* Accent line animation */}
|
|
61
|
+
<div
|
|
62
|
+
style={{
|
|
63
|
+
position: 'absolute',
|
|
64
|
+
top: 0,
|
|
65
|
+
left: 0,
|
|
66
|
+
width: `${lineWidth}%`,
|
|
67
|
+
height: 2,
|
|
68
|
+
background: `linear-gradient(90deg, ${accentColor}, transparent)`,
|
|
69
|
+
}}
|
|
70
|
+
/>
|
|
71
|
+
|
|
72
|
+
{/* Name */}
|
|
73
|
+
<h2
|
|
74
|
+
style={{
|
|
75
|
+
margin: 0,
|
|
76
|
+
fontSize: 28,
|
|
77
|
+
fontWeight: 600,
|
|
78
|
+
color: '#fff',
|
|
79
|
+
transform: `translateX(${nameX}px)`,
|
|
80
|
+
opacity: nameOpacity,
|
|
81
|
+
letterSpacing: '0.02em',
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
{name}
|
|
85
|
+
</h2>
|
|
86
|
+
|
|
87
|
+
{/* Title */}
|
|
88
|
+
<p
|
|
89
|
+
style={{
|
|
90
|
+
margin: '4px 0 0',
|
|
91
|
+
fontSize: 16,
|
|
92
|
+
fontWeight: 400,
|
|
93
|
+
color: accentColor,
|
|
94
|
+
transform: `translateX(${titleX}px)`,
|
|
95
|
+
opacity: titleOpacity,
|
|
96
|
+
textTransform: 'uppercase',
|
|
97
|
+
letterSpacing: '0.1em',
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
100
|
+
{title}
|
|
101
|
+
</p>
|
|
102
|
+
|
|
103
|
+
{/* Social handle */}
|
|
104
|
+
{social && (
|
|
105
|
+
<p
|
|
106
|
+
style={{
|
|
107
|
+
margin: '8px 0 0',
|
|
108
|
+
fontSize: 14,
|
|
109
|
+
fontWeight: 400,
|
|
110
|
+
color: 'rgba(255, 255, 255, 0.6)',
|
|
111
|
+
transform: `translateX(${titleX}px)`,
|
|
112
|
+
opacity: titleOpacity * 0.8,
|
|
113
|
+
}}
|
|
114
|
+
>
|
|
115
|
+
{social}
|
|
116
|
+
</p>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
{/* Decorative element */}
|
|
121
|
+
<div
|
|
122
|
+
style={{
|
|
123
|
+
position: 'absolute',
|
|
124
|
+
right: -20,
|
|
125
|
+
top: '50%',
|
|
126
|
+
transform: 'translateY(-50%)',
|
|
127
|
+
width: 40,
|
|
128
|
+
height: 40,
|
|
129
|
+
border: `2px solid ${accentColor}`,
|
|
130
|
+
borderRadius: '50%',
|
|
131
|
+
opacity: interpolate(frame, [30, 45], [0, 0.5], { extrapolateRight: 'clamp' }),
|
|
132
|
+
}}
|
|
133
|
+
/>
|
|
134
|
+
</div>
|
|
135
|
+
</AbsoluteFill>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export default LowerThird;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate, spring } from '@codellyson/framely';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Product Showcase Template
|
|
5
|
+
*/
|
|
6
|
+
export function ProductShowcase({
|
|
7
|
+
productName = 'Product Name',
|
|
8
|
+
tagline = 'Amazing features await',
|
|
9
|
+
price = '$99.99',
|
|
10
|
+
ctaText = 'Shop Now',
|
|
11
|
+
brandColor = '#10b981',
|
|
12
|
+
}) {
|
|
13
|
+
const frame = useCurrentFrame();
|
|
14
|
+
const { fps, width, height } = useVideoConfig();
|
|
15
|
+
|
|
16
|
+
// Product placeholder (circle representing product)
|
|
17
|
+
const productSpring = spring({ frame, fps, config: { damping: 12, stiffness: 100 } });
|
|
18
|
+
const productScale = interpolate(productSpring, [0, 1], [0, 1]);
|
|
19
|
+
const productRotate = interpolate(frame, [0, 180], [0, 360]);
|
|
20
|
+
|
|
21
|
+
// Text animations
|
|
22
|
+
const nameY = interpolate(frame, [30, 50], [50, 0], { extrapolateRight: 'clamp' });
|
|
23
|
+
const nameOpacity = interpolate(frame, [30, 45], [0, 1], { extrapolateRight: 'clamp' });
|
|
24
|
+
|
|
25
|
+
const taglineY = interpolate(frame, [40, 60], [30, 0], { extrapolateRight: 'clamp' });
|
|
26
|
+
const taglineOpacity = interpolate(frame, [40, 55], [0, 1], { extrapolateRight: 'clamp' });
|
|
27
|
+
|
|
28
|
+
// Price pop
|
|
29
|
+
const priceSpring = spring({ frame: frame - 70, fps, config: { damping: 10, stiffness: 200 } });
|
|
30
|
+
const priceScale = interpolate(priceSpring, [0, 1], [0, 1]);
|
|
31
|
+
|
|
32
|
+
// CTA button
|
|
33
|
+
const ctaSpring = spring({ frame: frame - 100, fps, config: { damping: 15, stiffness: 150 } });
|
|
34
|
+
const ctaScale = interpolate(ctaSpring, [0, 1], [0, 1]);
|
|
35
|
+
const ctaY = interpolate(ctaSpring, [0, 1], [30, 0]);
|
|
36
|
+
|
|
37
|
+
// Pulse effect on CTA
|
|
38
|
+
const ctaPulse = frame > 120 ? 1 + Math.sin((frame - 120) * 0.15) * 0.05 : 1;
|
|
39
|
+
|
|
40
|
+
// Decorative circles
|
|
41
|
+
const circles = [
|
|
42
|
+
{ size: 300, delay: 0, x: '20%', y: '30%' },
|
|
43
|
+
{ size: 200, delay: 10, x: '80%', y: '60%' },
|
|
44
|
+
{ size: 150, delay: 20, x: '70%', y: '20%' },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<AbsoluteFill
|
|
49
|
+
style={{
|
|
50
|
+
background: `linear-gradient(135deg, #0f0f0f 0%, #1a1a1a 50%, ${brandColor}22 100%)`,
|
|
51
|
+
display: 'flex',
|
|
52
|
+
flexDirection: 'column',
|
|
53
|
+
alignItems: 'center',
|
|
54
|
+
justifyContent: 'center',
|
|
55
|
+
overflow: 'hidden',
|
|
56
|
+
}}
|
|
57
|
+
>
|
|
58
|
+
{/* Background circles */}
|
|
59
|
+
{circles.map((circle, i) => {
|
|
60
|
+
const circleScale = interpolate(frame - circle.delay, [0, 40], [0, 1], {
|
|
61
|
+
extrapolateLeft: 'clamp',
|
|
62
|
+
extrapolateRight: 'clamp',
|
|
63
|
+
});
|
|
64
|
+
return (
|
|
65
|
+
<div
|
|
66
|
+
key={i}
|
|
67
|
+
style={{
|
|
68
|
+
position: 'absolute',
|
|
69
|
+
left: circle.x,
|
|
70
|
+
top: circle.y,
|
|
71
|
+
width: circle.size,
|
|
72
|
+
height: circle.size,
|
|
73
|
+
borderRadius: '50%',
|
|
74
|
+
border: `2px solid ${brandColor}33`,
|
|
75
|
+
transform: `translate(-50%, -50%) scale(${circleScale})`,
|
|
76
|
+
}}
|
|
77
|
+
/>
|
|
78
|
+
);
|
|
79
|
+
})}
|
|
80
|
+
|
|
81
|
+
{/* Product placeholder */}
|
|
82
|
+
<div
|
|
83
|
+
style={{
|
|
84
|
+
width: Math.min(width, height) * 0.35,
|
|
85
|
+
height: Math.min(width, height) * 0.35,
|
|
86
|
+
borderRadius: '50%',
|
|
87
|
+
background: `linear-gradient(135deg, ${brandColor}, ${brandColor}88)`,
|
|
88
|
+
boxShadow: `0 20px 60px ${brandColor}44`,
|
|
89
|
+
transform: `scale(${productScale}) rotate(${productRotate}deg)`,
|
|
90
|
+
display: 'flex',
|
|
91
|
+
alignItems: 'center',
|
|
92
|
+
justifyContent: 'center',
|
|
93
|
+
marginBottom: 30,
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
<span style={{ fontSize: 60, filter: 'grayscale(1) brightness(10)' }}>📦</span>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
{/* Product name */}
|
|
100
|
+
<h1
|
|
101
|
+
style={{
|
|
102
|
+
fontSize: Math.min(width, height) * 0.07,
|
|
103
|
+
fontWeight: 700,
|
|
104
|
+
color: '#fff',
|
|
105
|
+
margin: 0,
|
|
106
|
+
transform: `translateY(${nameY}px)`,
|
|
107
|
+
opacity: nameOpacity,
|
|
108
|
+
textAlign: 'center',
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
{productName}
|
|
112
|
+
</h1>
|
|
113
|
+
|
|
114
|
+
{/* Tagline */}
|
|
115
|
+
<p
|
|
116
|
+
style={{
|
|
117
|
+
fontSize: Math.min(width, height) * 0.035,
|
|
118
|
+
color: 'rgba(255, 255, 255, 0.7)',
|
|
119
|
+
margin: '10px 0 20px',
|
|
120
|
+
transform: `translateY(${taglineY}px)`,
|
|
121
|
+
opacity: taglineOpacity,
|
|
122
|
+
textAlign: 'center',
|
|
123
|
+
}}
|
|
124
|
+
>
|
|
125
|
+
{tagline}
|
|
126
|
+
</p>
|
|
127
|
+
|
|
128
|
+
{/* Price */}
|
|
129
|
+
<div
|
|
130
|
+
style={{
|
|
131
|
+
fontSize: Math.min(width, height) * 0.08,
|
|
132
|
+
fontWeight: 700,
|
|
133
|
+
color: brandColor,
|
|
134
|
+
transform: `scale(${priceScale})`,
|
|
135
|
+
marginBottom: 20,
|
|
136
|
+
}}
|
|
137
|
+
>
|
|
138
|
+
{price}
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
{/* CTA Button */}
|
|
142
|
+
<button
|
|
143
|
+
style={{
|
|
144
|
+
padding: '16px 40px',
|
|
145
|
+
fontSize: Math.min(width, height) * 0.035,
|
|
146
|
+
fontWeight: 600,
|
|
147
|
+
color: '#fff',
|
|
148
|
+
background: brandColor,
|
|
149
|
+
border: 'none',
|
|
150
|
+
borderRadius: 50,
|
|
151
|
+
transform: `scale(${ctaScale * ctaPulse}) translateY(${ctaY}px)`,
|
|
152
|
+
boxShadow: `0 10px 30px ${brandColor}66`,
|
|
153
|
+
cursor: 'pointer',
|
|
154
|
+
}}
|
|
155
|
+
>
|
|
156
|
+
{ctaText}
|
|
157
|
+
</button>
|
|
158
|
+
</AbsoluteFill>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export default ProductShowcase;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate, spring } from '@codellyson/framely';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Slide Transition Template
|
|
5
|
+
* Smooth slide transitions for presentations
|
|
6
|
+
*/
|
|
7
|
+
export function SlideTransition({
|
|
8
|
+
transitionType = 'slide',
|
|
9
|
+
direction = 'left',
|
|
10
|
+
fromContent = 'Slide 1',
|
|
11
|
+
toContent = 'Slide 2',
|
|
12
|
+
backgroundColor = '#1a1a2e',
|
|
13
|
+
slideColor = '#16213e',
|
|
14
|
+
textColor = '#ffffff',
|
|
15
|
+
}) {
|
|
16
|
+
const frame = useCurrentFrame();
|
|
17
|
+
const { fps, width, durationInFrames } = useVideoConfig();
|
|
18
|
+
|
|
19
|
+
const midpoint = durationInFrames / 2;
|
|
20
|
+
const progress = frame / durationInFrames;
|
|
21
|
+
|
|
22
|
+
// Transition animation
|
|
23
|
+
const transitionSpring = spring({
|
|
24
|
+
frame: frame - midpoint / 2,
|
|
25
|
+
fps,
|
|
26
|
+
config: { damping: 15, stiffness: 100 },
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Calculate slide positions based on transition type
|
|
30
|
+
const getTransform = (isOutgoing) => {
|
|
31
|
+
const springValue = isOutgoing ? transitionSpring : 1 - transitionSpring;
|
|
32
|
+
|
|
33
|
+
switch (transitionType) {
|
|
34
|
+
case 'slide': {
|
|
35
|
+
const slideOffset = interpolate(springValue, [0, 1], [0, 100]);
|
|
36
|
+
if (direction === 'left') {
|
|
37
|
+
return isOutgoing
|
|
38
|
+
? `translateX(${-slideOffset}%)`
|
|
39
|
+
: `translateX(${100 - slideOffset}%)`;
|
|
40
|
+
} else if (direction === 'right') {
|
|
41
|
+
return isOutgoing
|
|
42
|
+
? `translateX(${slideOffset}%)`
|
|
43
|
+
: `translateX(${-100 + slideOffset}%)`;
|
|
44
|
+
} else if (direction === 'up') {
|
|
45
|
+
return isOutgoing
|
|
46
|
+
? `translateY(${-slideOffset}%)`
|
|
47
|
+
: `translateY(${100 - slideOffset}%)`;
|
|
48
|
+
} else {
|
|
49
|
+
return isOutgoing
|
|
50
|
+
? `translateY(${slideOffset}%)`
|
|
51
|
+
: `translateY(${-100 + slideOffset}%)`;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
case 'zoom': {
|
|
56
|
+
const zoomScale = isOutgoing
|
|
57
|
+
? interpolate(springValue, [0, 1], [1, 0.5])
|
|
58
|
+
: interpolate(springValue, [0, 1], [1.5, 1]);
|
|
59
|
+
const zoomOpacity = isOutgoing
|
|
60
|
+
? interpolate(springValue, [0, 1], [1, 0])
|
|
61
|
+
: interpolate(springValue, [0, 1], [0, 1]);
|
|
62
|
+
return { transform: `scale(${zoomScale})`, opacity: zoomOpacity };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
case 'fade': {
|
|
66
|
+
const fadeOpacity = isOutgoing
|
|
67
|
+
? interpolate(springValue, [0, 1], [1, 0])
|
|
68
|
+
: interpolate(springValue, [0, 1], [0, 1]);
|
|
69
|
+
return { opacity: fadeOpacity };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
case 'flip': {
|
|
73
|
+
const flipRotation = isOutgoing
|
|
74
|
+
? interpolate(springValue, [0, 1], [0, -90])
|
|
75
|
+
: interpolate(springValue, [0, 1], [90, 0]);
|
|
76
|
+
const flipOpacity = Math.abs(flipRotation) > 85 ? 0 : 1;
|
|
77
|
+
return {
|
|
78
|
+
transform: `perspective(1000px) rotateY(${flipRotation}deg)`,
|
|
79
|
+
opacity: flipOpacity,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
default:
|
|
84
|
+
return {};
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const outgoingTransform = getTransform(true);
|
|
89
|
+
const incomingTransform = getTransform(false);
|
|
90
|
+
|
|
91
|
+
// Slide content component
|
|
92
|
+
const SlideContent = ({ content, style }) => (
|
|
93
|
+
<div
|
|
94
|
+
style={{
|
|
95
|
+
position: 'absolute',
|
|
96
|
+
inset: 0,
|
|
97
|
+
display: 'flex',
|
|
98
|
+
alignItems: 'center',
|
|
99
|
+
justifyContent: 'center',
|
|
100
|
+
background: slideColor,
|
|
101
|
+
...(typeof style === 'string' ? { transform: style } : style),
|
|
102
|
+
}}
|
|
103
|
+
>
|
|
104
|
+
<div
|
|
105
|
+
style={{
|
|
106
|
+
fontSize: width * 0.06,
|
|
107
|
+
fontWeight: 700,
|
|
108
|
+
color: textColor,
|
|
109
|
+
textAlign: 'center',
|
|
110
|
+
padding: 40,
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
{content}
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
{/* Decorative corners */}
|
|
117
|
+
{['top-left', 'top-right', 'bottom-left', 'bottom-right'].map((corner) => (
|
|
118
|
+
<div
|
|
119
|
+
key={corner}
|
|
120
|
+
style={{
|
|
121
|
+
position: 'absolute',
|
|
122
|
+
width: 60,
|
|
123
|
+
height: 60,
|
|
124
|
+
borderColor: textColor,
|
|
125
|
+
borderStyle: 'solid',
|
|
126
|
+
borderWidth: 0,
|
|
127
|
+
opacity: 0.3,
|
|
128
|
+
...(corner.includes('top') ? { top: 40 } : { bottom: 40 }),
|
|
129
|
+
...(corner.includes('left') ? { left: 40 } : { right: 40 }),
|
|
130
|
+
...(corner.includes('top') && corner.includes('left') && {
|
|
131
|
+
borderTopWidth: 3,
|
|
132
|
+
borderLeftWidth: 3,
|
|
133
|
+
}),
|
|
134
|
+
...(corner.includes('top') && corner.includes('right') && {
|
|
135
|
+
borderTopWidth: 3,
|
|
136
|
+
borderRightWidth: 3,
|
|
137
|
+
}),
|
|
138
|
+
...(corner.includes('bottom') && corner.includes('left') && {
|
|
139
|
+
borderBottomWidth: 3,
|
|
140
|
+
borderLeftWidth: 3,
|
|
141
|
+
}),
|
|
142
|
+
...(corner.includes('bottom') && corner.includes('right') && {
|
|
143
|
+
borderBottomWidth: 3,
|
|
144
|
+
borderRightWidth: 3,
|
|
145
|
+
}),
|
|
146
|
+
}}
|
|
147
|
+
/>
|
|
148
|
+
))}
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// Progress indicator
|
|
153
|
+
const progressWidth = interpolate(progress, [0, 1], [0, 100]);
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<AbsoluteFill style={{ background: backgroundColor, overflow: 'hidden' }}>
|
|
157
|
+
{/* Outgoing slide */}
|
|
158
|
+
<SlideContent
|
|
159
|
+
content={fromContent}
|
|
160
|
+
style={typeof outgoingTransform === 'string' ? outgoingTransform : outgoingTransform}
|
|
161
|
+
isActive={frame < midpoint}
|
|
162
|
+
/>
|
|
163
|
+
|
|
164
|
+
{/* Incoming slide */}
|
|
165
|
+
<SlideContent
|
|
166
|
+
content={toContent}
|
|
167
|
+
style={typeof incomingTransform === 'string' ? incomingTransform : incomingTransform}
|
|
168
|
+
isActive={frame >= midpoint}
|
|
169
|
+
/>
|
|
170
|
+
|
|
171
|
+
{/* Progress bar at bottom */}
|
|
172
|
+
<div
|
|
173
|
+
style={{
|
|
174
|
+
position: 'absolute',
|
|
175
|
+
bottom: 0,
|
|
176
|
+
left: 0,
|
|
177
|
+
right: 0,
|
|
178
|
+
height: 4,
|
|
179
|
+
background: 'rgba(255,255,255,0.1)',
|
|
180
|
+
}}
|
|
181
|
+
>
|
|
182
|
+
<div
|
|
183
|
+
style={{
|
|
184
|
+
width: `${progressWidth}%`,
|
|
185
|
+
height: '100%',
|
|
186
|
+
background: textColor,
|
|
187
|
+
opacity: 0.5,
|
|
188
|
+
}}
|
|
189
|
+
/>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
{/* Transition type label */}
|
|
193
|
+
<div
|
|
194
|
+
style={{
|
|
195
|
+
position: 'absolute',
|
|
196
|
+
top: 20,
|
|
197
|
+
right: 20,
|
|
198
|
+
fontSize: 14,
|
|
199
|
+
color: textColor,
|
|
200
|
+
opacity: 0.5,
|
|
201
|
+
fontFamily: 'monospace',
|
|
202
|
+
textTransform: 'uppercase',
|
|
203
|
+
}}
|
|
204
|
+
>
|
|
205
|
+
{transitionType} • {direction}
|
|
206
|
+
</div>
|
|
207
|
+
</AbsoluteFill>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export default SlideTransition;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate, spring } from '@codellyson/framely';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Modern Social Intro Template
|
|
5
|
+
* Clean intro animation for social media videos
|
|
6
|
+
*/
|
|
7
|
+
export function SocialIntro({
|
|
8
|
+
title = 'Your Title Here',
|
|
9
|
+
subtitle = 'Subtitle text',
|
|
10
|
+
accentColor = '#6366f1',
|
|
11
|
+
}) {
|
|
12
|
+
const frame = useCurrentFrame();
|
|
13
|
+
const { fps, width, height } = useVideoConfig();
|
|
14
|
+
|
|
15
|
+
// Background gradient animation
|
|
16
|
+
const gradientProgress = interpolate(frame, [0, 60], [0, 1], { extrapolateRight: 'clamp' });
|
|
17
|
+
|
|
18
|
+
// Title animation
|
|
19
|
+
const titleSpring = spring({ frame, fps, config: { damping: 15, stiffness: 100 } });
|
|
20
|
+
const titleY = interpolate(titleSpring, [0, 1], [100, 0]);
|
|
21
|
+
const titleOpacity = interpolate(frame, [0, 20], [0, 1], { extrapolateRight: 'clamp' });
|
|
22
|
+
|
|
23
|
+
// Subtitle animation (delayed)
|
|
24
|
+
const subtitleSpring = spring({ frame: frame - 15, fps, config: { damping: 15, stiffness: 100 } });
|
|
25
|
+
const subtitleY = interpolate(subtitleSpring, [0, 1], [50, 0]);
|
|
26
|
+
const subtitleOpacity = interpolate(frame, [15, 35], [0, 1], { extrapolateRight: 'clamp' });
|
|
27
|
+
|
|
28
|
+
// Accent line animation
|
|
29
|
+
const lineWidth = interpolate(frame, [30, 60], [0, 200], { extrapolateRight: 'clamp' });
|
|
30
|
+
|
|
31
|
+
// Exit animation
|
|
32
|
+
const exitProgress = interpolate(frame, [70, 90], [0, 1], { extrapolateRight: 'clamp' });
|
|
33
|
+
const scale = interpolate(exitProgress, [0, 1], [1, 0.8]);
|
|
34
|
+
const overallOpacity = interpolate(exitProgress, [0, 1], [1, 0]);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<AbsoluteFill
|
|
38
|
+
style={{
|
|
39
|
+
background: `linear-gradient(135deg, #0a0a0f ${gradientProgress * 30}%, ${accentColor}22 100%)`,
|
|
40
|
+
display: 'flex',
|
|
41
|
+
flexDirection: 'column',
|
|
42
|
+
alignItems: 'center',
|
|
43
|
+
justifyContent: 'center',
|
|
44
|
+
transform: `scale(${scale})`,
|
|
45
|
+
opacity: overallOpacity,
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
{/* Background particles */}
|
|
49
|
+
<div style={{ position: 'absolute', inset: 0, overflow: 'hidden' }}>
|
|
50
|
+
{[...Array(20)].map((_, i) => {
|
|
51
|
+
const delay = i * 3;
|
|
52
|
+
const particleY = interpolate(
|
|
53
|
+
frame - delay,
|
|
54
|
+
[0, 90],
|
|
55
|
+
[height + 50, -50],
|
|
56
|
+
{ extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }
|
|
57
|
+
);
|
|
58
|
+
const particleX = (i * 97) % width;
|
|
59
|
+
const size = 4 + (i % 3) * 2;
|
|
60
|
+
return (
|
|
61
|
+
<div
|
|
62
|
+
key={i}
|
|
63
|
+
style={{
|
|
64
|
+
position: 'absolute',
|
|
65
|
+
left: particleX,
|
|
66
|
+
top: particleY,
|
|
67
|
+
width: size,
|
|
68
|
+
height: size,
|
|
69
|
+
borderRadius: '50%',
|
|
70
|
+
background: accentColor,
|
|
71
|
+
opacity: 0.3,
|
|
72
|
+
}}
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
75
|
+
})}
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
{/* Content */}
|
|
79
|
+
<div style={{ textAlign: 'center', zIndex: 1 }}>
|
|
80
|
+
<h1
|
|
81
|
+
style={{
|
|
82
|
+
fontSize: Math.min(width, height) * 0.08,
|
|
83
|
+
fontWeight: 700,
|
|
84
|
+
color: '#fff',
|
|
85
|
+
margin: 0,
|
|
86
|
+
transform: `translateY(${titleY}px)`,
|
|
87
|
+
opacity: titleOpacity,
|
|
88
|
+
textShadow: `0 4px 30px ${accentColor}66`,
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
{title}
|
|
92
|
+
</h1>
|
|
93
|
+
|
|
94
|
+
{/* Accent line */}
|
|
95
|
+
<div
|
|
96
|
+
style={{
|
|
97
|
+
width: lineWidth,
|
|
98
|
+
height: 4,
|
|
99
|
+
background: `linear-gradient(90deg, transparent, ${accentColor}, transparent)`,
|
|
100
|
+
margin: '20px auto',
|
|
101
|
+
borderRadius: 2,
|
|
102
|
+
}}
|
|
103
|
+
/>
|
|
104
|
+
|
|
105
|
+
<p
|
|
106
|
+
style={{
|
|
107
|
+
fontSize: Math.min(width, height) * 0.035,
|
|
108
|
+
fontWeight: 400,
|
|
109
|
+
color: 'rgba(255, 255, 255, 0.7)',
|
|
110
|
+
margin: 0,
|
|
111
|
+
transform: `translateY(${subtitleY}px)`,
|
|
112
|
+
opacity: subtitleOpacity,
|
|
113
|
+
}}
|
|
114
|
+
>
|
|
115
|
+
{subtitle}
|
|
116
|
+
</p>
|
|
117
|
+
</div>
|
|
118
|
+
</AbsoluteFill>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export default SocialIntro;
|