>({});
20 |
21 | // Get the slide elements in the template by name (starting with 'Slide-')
22 | const slideElements = useMemo(() => {
23 | return props.currentState?.elements.filter((element) => element.source.name?.startsWith('Slide-'));
24 | }, [props.currentState]);
25 |
26 | return (
27 |
28 |
29 |
30 |
31 | Intro
32 | ensureElementVisibility(props.preview, 'Title', 1.5)}
35 | onChange={(e) => setPropertyValue(props.preview, 'Title', e.target.value, modificationsRef.current)}
36 | />
37 | ensureElementVisibility(props.preview, 'Tagline', 1.5)}
40 | onChange={(e) => setPropertyValue(props.preview, 'Tagline', e.target.value, modificationsRef.current)}
41 | />
42 | ensureElementVisibility(props.preview, 'Start-Text', 1.5)}
45 | onChange={(e) => setPropertyValue(props.preview, 'Start-Text', e.target.value, modificationsRef.current)}
46 | />
47 |
48 |
49 |
50 | Outro
51 | ensureElementVisibility(props.preview, 'Final-Text', 1.5)}
54 | onChange={(e) => setPropertyValue(props.preview, 'Final-Text', e.target.value, modificationsRef.current)}
55 | />
56 |
57 |
58 | {slideElements?.map((slideElement, i) => {
59 | const transitionAnimation = slideElement.source.animations.find((animation: any) => animation.transition);
60 |
61 | const nestedElements = props.preview.getElements(slideElement);
62 | const textElement = nestedElements.find((element) => element.source.name?.endsWith('-Text'));
63 | const imageElement = nestedElements.find((element) => element.source.name?.endsWith('-Image'));
64 |
65 | return (
66 |
67 | Slide {i + 1}
68 | {textElement && (
69 |
70 | ensureElementVisibility(props.preview, textElement.source.name, 1.5)}
73 | onChange={(e) =>
74 | setPropertyValue(props.preview, textElement.source.name, e.target.value, modificationsRef.current)
75 | }
76 | />
77 | ensureElementVisibility(props.preview, textElement.source.name, 1.5)}
79 | onChange={(e) =>
80 | setTextStyle(props.preview, textElement.source.name, e.target.value, modificationsRef.current)
81 | }
82 | >
83 | Block Text
84 | Rounded Text
85 |
86 | ensureElementVisibility(props.preview, slideElement.source.name, 0.5)}
89 | onChange={(e) => setSlideTransition(props.preview, slideElement.source.name, e.target.value)}
90 | >
91 | Fade Transition
92 | Circle Wipe Transition
93 |
94 | {imageElement && (
95 |
96 | {[
97 | 'https://creatomate-static.s3.amazonaws.com/demo/harshil-gudka-77zGnfU_SFU-unsplash.jpg',
98 | 'https://creatomate-static.s3.amazonaws.com/demo/samuel-ferrara-1527pjeb6jg-unsplash.jpg',
99 | 'https://creatomate-static.s3.amazonaws.com/demo/simon-berger-UqCnDyc_3vA-unsplash.jpg',
100 | ].map((url) => (
101 | {
105 | await ensureElementVisibility(props.preview, imageElement.source.name, 1.5);
106 | await setPropertyValue(
107 | props.preview,
108 | imageElement.source.name,
109 | url,
110 | modificationsRef.current,
111 | );
112 | }}
113 | />
114 | ))}
115 |
116 | )}
117 |
118 | )}
119 |
120 | );
121 | })}
122 |
123 | addSlide(props.preview)} style={{ width: '100%' }}>
124 | Add Slide
125 |
126 |
127 | );
128 | };
129 |
130 | const Group = styled.div`
131 | margin: 20px 0;
132 | padding: 20px;
133 | background: #f5f7f8;
134 | border-radius: 5px;
135 | `;
136 |
137 | const GroupTitle = styled.div`
138 | margin-bottom: 15px;
139 | font-weight: 600;
140 | `;
141 |
142 | const ImageOptions = styled.div`
143 | display: flex;
144 | margin: 20px -10px 0 -10px;
145 | `;
146 |
147 | // Updates the provided modifications object
148 | const setPropertyValue = async (
149 | preview: Preview,
150 | selector: string,
151 | value: string,
152 | modifications: Record,
153 | ) => {
154 | if (value.trim()) {
155 | // If a non-empty value is passed, update the modifications based on the provided selector
156 | modifications[selector] = value;
157 | } else {
158 | // If an empty value is passed, remove it from the modifications map, restoring its default
159 | delete modifications[selector];
160 | }
161 |
162 | // Set the template modifications
163 | await preview.setModifications(modifications);
164 | };
165 |
166 | // Sets the text styling properties
167 | // For a full list of text properties, refer to: https://creatomate.com/docs/json/elements/text-element
168 | const setTextStyle = async (preview: Preview, selector: string, style: string, modifications: Record) => {
169 | if (style === 'block-text') {
170 | modifications[`${selector}.background_border_radius`] = '0%';
171 | } else if (style === 'rounded-text') {
172 | modifications[`${selector}.background_border_radius`] = '50%';
173 | }
174 |
175 | await preview.setModifications(modifications);
176 | };
177 |
178 | // Jumps to a time position where the provided element is visible
179 | const ensureElementVisibility = async (preview: Preview, elementName: string, addTime: number) => {
180 | // Find element by name
181 | const element = preview.getElements().find((element) => element.source.name === elementName);
182 | if (element) {
183 | // Set playback time
184 | await preview.setTime(element.globalTime + addTime);
185 | }
186 | };
187 |
188 | // Sets the animation of a slide element
189 | const setSlideTransition = async (preview: Preview, slideName: string, type: string) => {
190 | // Make sure to clone the state as it's immutable
191 | const mutatedState = deepClone(preview.state);
192 |
193 | // Find element by name
194 | const element = preview.getElements(mutatedState).find((element) => element.source.name === slideName);
195 | if (element) {
196 | // Set the animation property
197 | // Refer to: https://creatomate.com/docs/json/elements/common-properties
198 | element.source.animations = [
199 | {
200 | type,
201 | duration: 1,
202 | transition: true,
203 | },
204 | ];
205 |
206 | // Update the video source
207 | // Refer to: https://creatomate.com/docs/json/introduction
208 | await preview.setSource(preview.getSource(mutatedState));
209 | }
210 | };
211 |
212 | const addSlide = async (preview: Preview) => {
213 | // Get the video source
214 | // Refer to: https://creatomate.com/docs/json/introduction
215 | const source = preview.getSource();
216 |
217 | // Delete the 'duration' and 'time' property values to make each element (Slide-1, Slide-2, etc.) autosize on the timeline
218 | delete source.duration;
219 | for (const element of source.elements) {
220 | delete element.time;
221 | }
222 |
223 | // Find the last slide element (e.g. Slide-3)
224 | const lastSlideIndex = source.elements.findLastIndex((element: any) => element.name?.startsWith('Slide-'));
225 | if (lastSlideIndex !== -1) {
226 | const slideName = `Slide-${lastSlideIndex}`;
227 |
228 | // Create a new slide
229 | const newSlideSource = createSlide(slideName, `This is the text caption for newly added slide ${lastSlideIndex}.`);
230 |
231 | // Insert the new slide
232 | source.elements.splice(lastSlideIndex + 1, 0, newSlideSource);
233 |
234 | // Update the video source
235 | await preview.setSource(source);
236 |
237 | // Jump to the time at which the text element is visible
238 | await ensureElementVisibility(preview, `${slideName}-Text`, 1.5);
239 |
240 | // Scroll to the bottom of the settings panel
241 | const panel = document.querySelector('#panel');
242 | if (panel) {
243 | panel.scrollTop = panel.scrollHeight;
244 | }
245 | }
246 | };
247 |
248 | const createSlide = (slideName: string, caption: string) => {
249 | // This is the JSON of a new slide. It is based on existing slides in the "Image Slideshow w/ Intro and Outro" template.
250 | // Refer to: https://creatomate.com/docs/json/introduction
251 | return {
252 | name: slideName,
253 | type: 'composition',
254 | track: 1,
255 | duration: 4,
256 | animations: [
257 | {
258 | type: 'fade',
259 | duration: 1,
260 | transition: true,
261 | },
262 | ],
263 | elements: [
264 | {
265 | name: `${slideName}-Image`,
266 | type: 'image',
267 | animations: [
268 | {
269 | easing: 'linear',
270 | type: 'scale',
271 | fade: false,
272 | scope: 'element',
273 | end_scale: '130%',
274 | start_scale: '100%',
275 | },
276 | ],
277 | source: 'https://creatomate-static.s3.amazonaws.com/demo/samuel-ferrara-1527pjeb6jg-unsplash.jpg',
278 | },
279 | {
280 | name: `${slideName}-Text`,
281 | type: 'text',
282 | time: 0.5,
283 | duration: 3.5,
284 | y: '83.3107%',
285 | width: '70%',
286 | height: '10%',
287 | x_alignment: '50%',
288 | y_alignment: '100%',
289 | fill_color: '#ffffff',
290 | animations: [
291 | {
292 | time: 'start',
293 | duration: 1,
294 | easing: 'quadratic-out',
295 | type: 'text-slide',
296 | scope: 'split-clip',
297 | split: 'line',
298 | direction: 'up',
299 | background_effect: 'scaling-clip',
300 | },
301 | {
302 | easing: 'linear',
303 | type: 'scale',
304 | fade: false,
305 | scope: 'element',
306 | y_anchor: '100%',
307 | end_scale: '130%',
308 | start_scale: '100%',
309 | },
310 | ],
311 | text: caption,
312 | font_family: 'Roboto Condensed',
313 | font_weight: '700',
314 | background_color: 'rgba(220,171,94,1)',
315 | background_x_padding: '80%',
316 | },
317 | ],
318 | };
319 | };
320 |
--------------------------------------------------------------------------------