├── .gitignore ├── 1-new-project ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── README.md ├── components │ └── Main.tsx ├── next.config.js ├── package-lock.json ├── package.json ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── api │ │ └── videos.ts │ └── index.tsx ├── public │ └── favicon.ico ├── styles │ ├── Home.module.css │ └── globals.css └── tsconfig.json ├── 2-basic-scene ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── README.md ├── components │ └── Main.tsx ├── next.config.js ├── package-lock.json ├── package.json ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── api │ │ └── videos.ts │ └── index.tsx ├── public │ └── favicon.ico ├── styles │ ├── Home.module.css │ └── globals.css ├── tsconfig.json └── utility │ └── getBasicComposition.ts ├── 3-live-editing ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── README.md ├── components │ └── Main.tsx ├── next.config.js ├── package-lock.json ├── package.json ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── api │ │ └── videos.ts │ └── index.tsx ├── public │ └── favicon.ico ├── styles │ ├── Home.module.css │ └── globals.css ├── tsconfig.json └── utility │ └── getBasicComposition.ts ├── 4-play-and-pause ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── README.md ├── components │ └── Main.tsx ├── next.config.js ├── package-lock.json ├── package.json ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── api │ │ └── videos.ts │ └── index.tsx ├── public │ └── favicon.ico ├── styles │ ├── Home.module.css │ └── globals.css ├── tsconfig.json └── utility │ └── getBasicComposition.ts ├── 5-state-management ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── README.md ├── components │ └── Main.tsx ├── next.config.js ├── package-lock.json ├── package.json ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── api │ │ └── videos.ts │ └── index.tsx ├── public │ └── favicon.ico ├── styles │ ├── Home.module.css │ └── globals.css ├── tsconfig.json └── utility │ └── getBasicComposition.ts ├── 6-interactivity ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── README.md ├── components │ ├── Main.tsx │ └── ProgressControl.tsx ├── next.config.js ├── package-lock.json ├── package.json ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── api │ │ └── videos.ts │ └── index.tsx ├── public │ └── favicon.ico ├── styles │ ├── Home.module.css │ └── globals.css ├── tsconfig.json └── utility │ └── getBasicComposition.ts ├── 7-advanced-mutation ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── README.md ├── components │ └── Main.tsx ├── next.config.js ├── package-lock.json ├── package.json ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── api │ │ └── videos.ts │ └── index.tsx ├── public │ └── favicon.ico ├── styles │ ├── Home.module.css │ └── globals.css ├── tsconfig.json └── utility │ ├── addSlide.ts │ ├── createSlide.ts │ ├── ensureElementVisibility.ts │ └── getSlideshowComposition.ts ├── 8-final-project ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── README.md ├── components │ ├── CreateButton.tsx │ ├── Main.tsx │ └── SettingsPanel.tsx ├── next.config.js ├── package-lock.json ├── package.json ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── api │ │ └── videos.ts │ └── index.tsx ├── public │ └── favicon.ico ├── styles │ ├── Home.module.css │ └── globals.css ├── tsconfig.json └── utility │ ├── addSlide.ts │ ├── createSlide.ts │ ├── deepClone.ts │ ├── ensureElementVisibility.ts │ ├── finishVideo.tsx │ ├── setPropertyValue.ts │ ├── setSlideTransition.ts │ ├── setTextStyle.tsx │ └── useWindowWidth.ts ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | -------------------------------------------------------------------------------- /1-new-project/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /1-new-project/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | 37 | # JetBrains editors 38 | /.idea 39 | -------------------------------------------------------------------------------- /1-new-project/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "trailingComma": "all", 4 | "tabWidth": 2, 5 | "semi": true, 6 | "singleQuote": true 7 | } 8 | -------------------------------------------------------------------------------- /1-new-project/README.md: -------------------------------------------------------------------------------- 1 | # Video Editor Tutorial – New Project 2 | 3 | This example is part of this article: [How to Build a Video Editor in JavaScript](https://creatomate.com/blog/how-to-build-a-video-editor-in-javascript) 4 | 5 | --- 6 | 7 | Run it with the following command: 8 | 9 | ```bash 10 | npm install && npm run dev 11 | ``` 12 | 13 | You can also try it online using StackBlitz (Chrome and Edge only): 14 | 15 | [![Run on StackBlitz](https://user-images.githubusercontent.com/44575638/199058604-b6e5e08a-cdfd-451a-8ce9-ab7355b22786.svg)](https://stackblitz.com/github/creatomate/video-editor-tutorial/tree/main/1-new-project) 16 | -------------------------------------------------------------------------------- /1-new-project/components/Main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Inter } from 'next/font/google'; 3 | import styles from '@/styles/Home.module.css'; 4 | 5 | const inter = Inter({ subsets: ['latin'] }); 6 | 7 | export const Main: React.FC = () => { 8 | return ( 9 |
10 | This is the empty starter project we will use for the tutorial. 11 |
12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /1-new-project/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | }; 5 | 6 | module.exports = nextConfig; 7 | -------------------------------------------------------------------------------- /1-new-project/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "video-editor", 3 | "version": "1.0.0", 4 | "author": "Creatomate", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "next dev", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint" 11 | }, 12 | "dependencies": { 13 | "@creatomate/preview": "^1.5.0", 14 | "@types/node": "20.4.5", 15 | "@types/react": "18.2.16", 16 | "@types/react-dom": "18.2.7", 17 | "creatomate": "^1.1.0", 18 | "eslint": "8.45.0", 19 | "eslint-config-next": "13.4.12", 20 | "modern-normalize": "2.0.0", 21 | "next": "^13.5.6", 22 | "prettier": "3.0.0", 23 | "react": "18.2.0", 24 | "react-dom": "18.2.0", 25 | "typescript": "5.1.6" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /1-new-project/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from 'next/app' 2 | import '@/styles/globals.css' 3 | 4 | export default function App({ Component, pageProps }: AppProps) { 5 | return 6 | } 7 | -------------------------------------------------------------------------------- /1-new-project/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document'; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /1-new-project/pages/api/videos.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | 3 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 4 | res.status(200).json({}); 5 | } 6 | -------------------------------------------------------------------------------- /1-new-project/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import { Main } from '@/components/Main'; 3 | 4 | export default function Home() { 5 | return ( 6 | <> 7 | 8 | Create Next App 9 | 10 | 11 | 12 | 13 |
14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /1-new-project/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Creatomate/video-editor-tutorial/4163bdc73a29f76d27e5717fbb2b1ee72dfa4ca9/1-new-project/public/favicon.ico -------------------------------------------------------------------------------- /1-new-project/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | width: 100vw; 3 | height: 100vh; 4 | display: flex; 5 | flex-direction: column; 6 | } 7 | -------------------------------------------------------------------------------- /1-new-project/styles/globals.css: -------------------------------------------------------------------------------- 1 | @import '@/node_modules/modern-normalize/modern-normalize.css'; 2 | 3 | html { 4 | background-color: #e4e7eb; 5 | user-select: none; 6 | } 7 | 8 | body { 9 | font-size: 18px; 10 | line-height: 1.5; 11 | } 12 | 13 | button { 14 | padding: 10px 15px; 15 | border: none; 16 | background-color: #0065eb; 17 | border-radius: 5px; 18 | color: #fff; 19 | font-size: 16px; 20 | font-weight: 500; 21 | cursor: pointer; 22 | } 23 | 24 | input, 25 | textarea { 26 | display: block; 27 | margin: 5px 0; 28 | padding: 15px; 29 | width: 100%; 30 | border: 1px solid #b3bfcc; 31 | border-radius: 5px; 32 | outline: none; 33 | resize: none; 34 | } 35 | 36 | textarea { 37 | height: 75px; 38 | } 39 | 40 | input:focus, 41 | textarea:focus { 42 | background-color: #e9f4fc; 43 | border-color: #005aff; 44 | } 45 | 46 | select { 47 | display: block; 48 | margin: 5px 0; 49 | padding: 10px 15px; 50 | width: 100%; 51 | background-color: #fff; 52 | border: 1px solid #b3bfcc; 53 | border-radius: 5px; 54 | outline: none; 55 | 56 | /* Arrow */ 57 | appearance: none; 58 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 10'%3E%3Cpath d='m0.993 2.02 5.25 5.25c0.966 0.966 2.534 0.966 3.5-0l5.264-5.264' fill='none' stroke='%23000' stroke-width='2px'/%3E%3C/svg%3E"); 59 | background-repeat: no-repeat; 60 | background-position: right 12px top 50%; 61 | background-size: 16px auto; 62 | } 63 | 64 | select:focus { 65 | background-color: #e9f4fc; 66 | border-color: #005aff; 67 | } 68 | 69 | *::-webkit-scrollbar { 70 | width: 8px; 71 | height: 8px; 72 | } 73 | 74 | *::-webkit-scrollbar-track { 75 | background-color: #dedede; 76 | } 77 | 78 | *::-webkit-scrollbar-thumb { 79 | background-color: #c0c0c0; 80 | } 81 | 82 | *::-webkit-scrollbar-corner { 83 | background-color: rgba(0, 0, 0, 0); 84 | } 85 | -------------------------------------------------------------------------------- /1-new-project/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "paths": { 18 | "@/*": ["./*"] 19 | } 20 | }, 21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /2-basic-scene/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /2-basic-scene/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | 37 | # JetBrains editors 38 | /.idea 39 | -------------------------------------------------------------------------------- /2-basic-scene/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "trailingComma": "all", 4 | "tabWidth": 2, 5 | "semi": true, 6 | "singleQuote": true 7 | } 8 | -------------------------------------------------------------------------------- /2-basic-scene/README.md: -------------------------------------------------------------------------------- 1 | # Video Editor Tutorial – Basic Scene 2 | 3 | This example is part of this article: [How to Build a Video Editor in JavaScript](https://creatomate.com/blog/how-to-build-a-video-editor-in-javascript) 4 | 5 | --- 6 | 7 | Run it with the following command: 8 | 9 | ```bash 10 | npm install && npm run dev 11 | ``` 12 | 13 | You can also try it online using StackBlitz (Chrome and Edge only): 14 | 15 | [![Run on StackBlitz](https://user-images.githubusercontent.com/44575638/199058604-b6e5e08a-cdfd-451a-8ce9-ab7355b22786.svg)](https://stackblitz.com/github/creatomate/video-editor-tutorial/tree/main/2-basic-scene) 16 | -------------------------------------------------------------------------------- /2-basic-scene/components/Main.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react'; 2 | import { Inter } from 'next/font/google'; 3 | import { Preview } from '@creatomate/preview'; 4 | import { getBasicComposition } from '@/utility/getBasicComposition'; 5 | import styles from '@/styles/Home.module.css'; 6 | 7 | const inter = Inter({ subsets: ['latin'] }); 8 | 9 | export const Main: React.FC = () => { 10 | const [isLoading, setIsLoading] = useState(true); 11 | 12 | // Reference to the preview 13 | const previewRef = useRef(); 14 | 15 | const setUpPreview = (htmlElement: HTMLDivElement) => { 16 | // Clean up an older instance of the preview SDK 17 | if (previewRef.current) { 18 | previewRef.current.dispose(); 19 | previewRef.current = undefined; 20 | } 21 | 22 | // Initialize a preview. Make sure you provide your own public token, which can be found in your dashboard under Project Settings 23 | const preview = new Preview(htmlElement, 'player', 'public-0x6hcqpfhrhw16d67ogth7ry'); 24 | 25 | preview.onReady = async () => { 26 | // Once the SDK is ready, create a basic video scene 27 | await preview.setSource(getBasicComposition()); 28 | 29 | // Skip to 2 seconds into the video 30 | await preview.setTime(2); 31 | 32 | setIsLoading(false); 33 | }; 34 | 35 | previewRef.current = preview; 36 | }; 37 | 38 | return ( 39 |
40 | {isLoading && 'Loading...'} 41 | 42 |
{ 45 | if (htmlElement && htmlElement !== previewRef.current?.element) { 46 | setUpPreview(htmlElement); 47 | } 48 | }} 49 | /> 50 |
51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /2-basic-scene/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | }; 5 | 6 | module.exports = nextConfig; 7 | -------------------------------------------------------------------------------- /2-basic-scene/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "video-editor", 3 | "version": "1.0.0", 4 | "author": "Creatomate", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "next dev", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint" 11 | }, 12 | "dependencies": { 13 | "@creatomate/preview": "^1.5.0", 14 | "@types/node": "20.4.5", 15 | "@types/react": "18.2.16", 16 | "@types/react-dom": "18.2.7", 17 | "creatomate": "^1.1.0", 18 | "eslint": "8.45.0", 19 | "eslint-config-next": "13.4.12", 20 | "modern-normalize": "2.0.0", 21 | "next": "^13.5.6", 22 | "prettier": "3.0.0", 23 | "react": "18.2.0", 24 | "react-dom": "18.2.0", 25 | "typescript": "5.1.6" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /2-basic-scene/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from 'next/app' 2 | import '@/styles/globals.css' 3 | 4 | export default function App({ Component, pageProps }: AppProps) { 5 | return 6 | } 7 | -------------------------------------------------------------------------------- /2-basic-scene/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document'; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /2-basic-scene/pages/api/videos.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | 3 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 4 | res.status(200).json({}); 5 | } 6 | -------------------------------------------------------------------------------- /2-basic-scene/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import { Main } from '@/components/Main'; 3 | 4 | export default function Home() { 5 | return ( 6 | <> 7 | 8 | Create Next App 9 | 10 | 11 | 12 | 13 |
14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /2-basic-scene/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Creatomate/video-editor-tutorial/4163bdc73a29f76d27e5717fbb2b1ee72dfa4ca9/2-basic-scene/public/favicon.ico -------------------------------------------------------------------------------- /2-basic-scene/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | width: 100vw; 3 | height: 100vh; 4 | display: flex; 5 | flex-direction: column; 6 | } 7 | 8 | .container { 9 | width: 100%; 10 | height: 100%; 11 | max-width: 720px; 12 | max-height: 720px; 13 | margin: auto; 14 | } 15 | -------------------------------------------------------------------------------- /2-basic-scene/styles/globals.css: -------------------------------------------------------------------------------- 1 | @import '@/node_modules/modern-normalize/modern-normalize.css'; 2 | 3 | html { 4 | background-color: #e4e7eb; 5 | user-select: none; 6 | } 7 | 8 | body { 9 | font-size: 18px; 10 | line-height: 1.5; 11 | } 12 | 13 | button { 14 | padding: 10px 15px; 15 | border: none; 16 | background-color: #0065eb; 17 | border-radius: 5px; 18 | color: #fff; 19 | font-size: 16px; 20 | font-weight: 500; 21 | cursor: pointer; 22 | } 23 | 24 | input, 25 | textarea { 26 | display: block; 27 | margin: 5px 0; 28 | padding: 15px; 29 | width: 100%; 30 | border: 1px solid #b3bfcc; 31 | border-radius: 5px; 32 | outline: none; 33 | resize: none; 34 | } 35 | 36 | textarea { 37 | height: 75px; 38 | } 39 | 40 | input:focus, 41 | textarea:focus { 42 | background-color: #e9f4fc; 43 | border-color: #005aff; 44 | } 45 | 46 | select { 47 | display: block; 48 | margin: 5px 0; 49 | padding: 10px 15px; 50 | width: 100%; 51 | background-color: #fff; 52 | border: 1px solid #b3bfcc; 53 | border-radius: 5px; 54 | outline: none; 55 | 56 | /* Arrow */ 57 | appearance: none; 58 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 10'%3E%3Cpath d='m0.993 2.02 5.25 5.25c0.966 0.966 2.534 0.966 3.5-0l5.264-5.264' fill='none' stroke='%23000' stroke-width='2px'/%3E%3C/svg%3E"); 59 | background-repeat: no-repeat; 60 | background-position: right 12px top 50%; 61 | background-size: 16px auto; 62 | } 63 | 64 | select:focus { 65 | background-color: #e9f4fc; 66 | border-color: #005aff; 67 | } 68 | 69 | *::-webkit-scrollbar { 70 | width: 8px; 71 | height: 8px; 72 | } 73 | 74 | *::-webkit-scrollbar-track { 75 | background-color: #dedede; 76 | } 77 | 78 | *::-webkit-scrollbar-thumb { 79 | background-color: #c0c0c0; 80 | } 81 | 82 | *::-webkit-scrollbar-corner { 83 | background-color: rgba(0, 0, 0, 0); 84 | } 85 | -------------------------------------------------------------------------------- /2-basic-scene/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "paths": { 18 | "@/*": ["./*"] 19 | } 20 | }, 21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /2-basic-scene/utility/getBasicComposition.ts: -------------------------------------------------------------------------------- 1 | // This is the source code for a simple video with two text elements and a background image. 2 | // Learn more about this here: https://creatomate.com/docs/json/introduction 3 | // If you want to edit it, copy-paste it into the template editor: https://creatomate.com/docs/template-editor/source-editor 4 | export function getBasicComposition() { 5 | return { 6 | output_format: 'mp4', 7 | width: 1920, 8 | height: 1080, 9 | elements: [ 10 | { 11 | id: '48734f5c-8c90-41ac-a059-e949e733936b', 12 | name: 'Main-Image', 13 | type: 'image', 14 | track: 1, 15 | time: 0, 16 | color_overlay: 'rgba(0,0,0,0.25)', 17 | animations: [ 18 | { 19 | easing: 'linear', 20 | type: 'scale', 21 | fade: false, 22 | scope: 'element', 23 | end_scale: '130%', 24 | start_scale: '100%', 25 | }, 26 | ], 27 | source: 'https://creatomate.com/files/assets/5bc5ed6f-26e6-4c3a-8d03-1b169dc7f983.jpg', 28 | }, 29 | { 30 | id: '72ec46a3-610c-4b46-86ef-c9bbc337f012', 31 | name: 'Tagline', 32 | type: 'text', 33 | track: 2, 34 | time: 1, 35 | duration: 2.5, 36 | y: '73.71%', 37 | width: '69.79%', 38 | height: '12.56%', 39 | x_alignment: '50%', 40 | fill_color: '#ffffff', 41 | animations: [ 42 | { 43 | time: 'start', 44 | duration: 1, 45 | easing: 'quadratic-out', 46 | type: 'text-slide', 47 | scope: 'split-clip', 48 | split: 'word', 49 | direction: 'up', 50 | }, 51 | ], 52 | text: 'Enter your tagline here', 53 | font_family: 'Oswald', 54 | font_weight: '600', 55 | text_transform: 'uppercase', 56 | }, 57 | { 58 | id: '04b59bd6-b9df-439f-9586-2d5095c9f959', 59 | name: 'Title', 60 | type: 'text', 61 | track: 3, 62 | time: 0, 63 | y: '41.41%', 64 | width: '69.79%', 65 | height: '47.61%', 66 | x_alignment: '50%', 67 | fill_color: '#ffffff', 68 | animations: [ 69 | { 70 | time: 'start', 71 | duration: 1, 72 | easing: 'quadratic-out', 73 | type: 'text-appear', 74 | split: 'line', 75 | }, 76 | ], 77 | text: 'Lorem ipsum dolor sit amet', 78 | font_family: 'Oswald', 79 | font_weight: '600', 80 | text_transform: 'uppercase', 81 | }, 82 | ], 83 | }; 84 | } 85 | -------------------------------------------------------------------------------- /3-live-editing/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /3-live-editing/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | 37 | # JetBrains editors 38 | /.idea 39 | -------------------------------------------------------------------------------- /3-live-editing/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "trailingComma": "all", 4 | "tabWidth": 2, 5 | "semi": true, 6 | "singleQuote": true 7 | } 8 | -------------------------------------------------------------------------------- /3-live-editing/README.md: -------------------------------------------------------------------------------- 1 | # Video Editor Tutorial – Live Editing 2 | 3 | This example is part of this article: [How to Build a Video Editor in JavaScript](https://creatomate.com/blog/how-to-build-a-video-editor-in-javascript) 4 | 5 | --- 6 | 7 | Run it with the following command: 8 | 9 | ```bash 10 | npm install && npm run dev 11 | ``` 12 | 13 | You can also try it online using StackBlitz (Chrome and Edge only): 14 | 15 | [![Run on StackBlitz](https://user-images.githubusercontent.com/44575638/199058604-b6e5e08a-cdfd-451a-8ce9-ab7355b22786.svg)](https://stackblitz.com/github/creatomate/video-editor-tutorial/tree/main/3-live-editing) 16 | -------------------------------------------------------------------------------- /3-live-editing/components/Main.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react'; 2 | import { Inter } from 'next/font/google'; 3 | import { Preview } from '@creatomate/preview'; 4 | import { getBasicComposition } from '@/utility/getBasicComposition'; 5 | import styles from '@/styles/Home.module.css'; 6 | 7 | const inter = Inter({ subsets: ['latin'] }); 8 | 9 | export const Main: React.FC = () => { 10 | const [isLoading, setIsLoading] = useState(true); 11 | 12 | // Reference to the preview 13 | const previewRef = useRef(); 14 | 15 | const setUpPreview = (htmlElement: HTMLDivElement) => { 16 | // Clean up an older instance of the preview SDK 17 | if (previewRef.current) { 18 | previewRef.current.dispose(); 19 | previewRef.current = undefined; 20 | } 21 | 22 | // Initialize a preview. Make sure you provide your own public token, which can be found in your dashboard under Project Settings 23 | const preview = new Preview(htmlElement, 'player', 'public-0x6hcqpfhrhw16d67ogth7ry'); 24 | 25 | preview.onReady = async () => { 26 | // Once the SDK is ready, create a basic video scene 27 | await preview.setSource(getBasicComposition()); 28 | 29 | // Skip to 2 seconds into the video 30 | await preview.setTime(2); 31 | 32 | setIsLoading(false); 33 | }; 34 | 35 | previewRef.current = preview; 36 | }; 37 | 38 | const applyTextValue = async (value: string) => { 39 | // Change the 'Title' element to the provided text value 40 | // For more information: https://creatomate.com/docs/api/rest-api/the-modifications-object 41 | await previewRef.current?.setModifications({ 42 | Title: value, 43 | }); 44 | }; 45 | 46 | return ( 47 |
48 |
49 | {isLoading && 'Loading...'} 50 | 51 |
{ 54 | if (htmlElement && htmlElement !== previewRef.current?.element) { 55 | setUpPreview(htmlElement); 56 | } 57 | }} 58 | /> 59 | 60 |
61 | { 65 | await applyTextValue(e.target.value); 66 | }} 67 | /> 68 |
69 |
70 |
71 | ); 72 | }; 73 | -------------------------------------------------------------------------------- /3-live-editing/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | }; 5 | 6 | module.exports = nextConfig; 7 | -------------------------------------------------------------------------------- /3-live-editing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "video-editor", 3 | "version": "1.0.0", 4 | "author": "Creatomate", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "next dev", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint" 11 | }, 12 | "dependencies": { 13 | "@creatomate/preview": "^1.5.0", 14 | "@types/node": "20.4.5", 15 | "@types/react": "18.2.16", 16 | "@types/react-dom": "18.2.7", 17 | "creatomate": "^1.1.0", 18 | "eslint": "8.45.0", 19 | "eslint-config-next": "13.4.12", 20 | "modern-normalize": "2.0.0", 21 | "next": "^13.5.6", 22 | "prettier": "3.0.0", 23 | "react": "18.2.0", 24 | "react-dom": "18.2.0", 25 | "typescript": "5.1.6" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /3-live-editing/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from 'next/app' 2 | import '@/styles/globals.css' 3 | 4 | export default function App({ Component, pageProps }: AppProps) { 5 | return 6 | } 7 | -------------------------------------------------------------------------------- /3-live-editing/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document'; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /3-live-editing/pages/api/videos.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | 3 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 4 | res.status(200).json({}); 5 | } 6 | -------------------------------------------------------------------------------- /3-live-editing/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import { Main } from '@/components/Main'; 3 | 4 | export default function Home() { 5 | return ( 6 | <> 7 | 8 | Create Next App 9 | 10 | 11 | 12 | 13 |
14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /3-live-editing/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Creatomate/video-editor-tutorial/4163bdc73a29f76d27e5717fbb2b1ee72dfa4ca9/3-live-editing/public/favicon.ico -------------------------------------------------------------------------------- /3-live-editing/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | width: 100vw; 3 | height: 100vh; 4 | display: flex; 5 | flex-direction: column; 6 | } 7 | 8 | .center { 9 | width: 100%; 10 | height: 100%; 11 | max-width: 720px; 12 | max-height: 720px; 13 | margin: auto; 14 | display: flex; 15 | flex-direction: column; 16 | } 17 | 18 | .container { 19 | flex: 1; 20 | } 21 | 22 | .controls { 23 | display: flex; 24 | justify-content: center; 25 | } 26 | -------------------------------------------------------------------------------- /3-live-editing/styles/globals.css: -------------------------------------------------------------------------------- 1 | @import '@/node_modules/modern-normalize/modern-normalize.css'; 2 | 3 | html { 4 | background-color: #e4e7eb; 5 | user-select: none; 6 | } 7 | 8 | body { 9 | font-size: 18px; 10 | line-height: 1.5; 11 | } 12 | 13 | button { 14 | padding: 10px 15px; 15 | border: none; 16 | background-color: #0065eb; 17 | border-radius: 5px; 18 | color: #fff; 19 | font-size: 16px; 20 | font-weight: 500; 21 | cursor: pointer; 22 | } 23 | 24 | input, 25 | textarea { 26 | display: block; 27 | margin: 5px 0; 28 | padding: 15px; 29 | width: 100%; 30 | border: 1px solid #b3bfcc; 31 | border-radius: 5px; 32 | outline: none; 33 | resize: none; 34 | } 35 | 36 | textarea { 37 | height: 75px; 38 | } 39 | 40 | input:focus, 41 | textarea:focus { 42 | background-color: #e9f4fc; 43 | border-color: #005aff; 44 | } 45 | 46 | select { 47 | display: block; 48 | margin: 5px 0; 49 | padding: 10px 15px; 50 | width: 100%; 51 | background-color: #fff; 52 | border: 1px solid #b3bfcc; 53 | border-radius: 5px; 54 | outline: none; 55 | 56 | /* Arrow */ 57 | appearance: none; 58 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 10'%3E%3Cpath d='m0.993 2.02 5.25 5.25c0.966 0.966 2.534 0.966 3.5-0l5.264-5.264' fill='none' stroke='%23000' stroke-width='2px'/%3E%3C/svg%3E"); 59 | background-repeat: no-repeat; 60 | background-position: right 12px top 50%; 61 | background-size: 16px auto; 62 | } 63 | 64 | select:focus { 65 | background-color: #e9f4fc; 66 | border-color: #005aff; 67 | } 68 | 69 | *::-webkit-scrollbar { 70 | width: 8px; 71 | height: 8px; 72 | } 73 | 74 | *::-webkit-scrollbar-track { 75 | background-color: #dedede; 76 | } 77 | 78 | *::-webkit-scrollbar-thumb { 79 | background-color: #c0c0c0; 80 | } 81 | 82 | *::-webkit-scrollbar-corner { 83 | background-color: rgba(0, 0, 0, 0); 84 | } 85 | -------------------------------------------------------------------------------- /3-live-editing/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "paths": { 18 | "@/*": ["./*"] 19 | } 20 | }, 21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /3-live-editing/utility/getBasicComposition.ts: -------------------------------------------------------------------------------- 1 | // This is the source code for a simple video with two text elements and a background image. 2 | // Learn more about this here: https://creatomate.com/docs/json/introduction 3 | // If you want to edit it, copy-paste it into the template editor: https://creatomate.com/docs/template-editor/source-editor 4 | export function getBasicComposition() { 5 | return { 6 | output_format: 'mp4', 7 | width: 1920, 8 | height: 1080, 9 | elements: [ 10 | { 11 | id: '48734f5c-8c90-41ac-a059-e949e733936b', 12 | name: 'Main-Image', 13 | type: 'image', 14 | track: 1, 15 | time: 0, 16 | color_overlay: 'rgba(0,0,0,0.25)', 17 | animations: [ 18 | { 19 | easing: 'linear', 20 | type: 'scale', 21 | fade: false, 22 | scope: 'element', 23 | end_scale: '130%', 24 | start_scale: '100%', 25 | }, 26 | ], 27 | source: 'https://creatomate.com/files/assets/5bc5ed6f-26e6-4c3a-8d03-1b169dc7f983.jpg', 28 | }, 29 | { 30 | id: '72ec46a3-610c-4b46-86ef-c9bbc337f012', 31 | name: 'Tagline', 32 | type: 'text', 33 | track: 2, 34 | time: 1, 35 | duration: 2.5, 36 | y: '73.71%', 37 | width: '69.79%', 38 | height: '12.56%', 39 | x_alignment: '50%', 40 | fill_color: '#ffffff', 41 | animations: [ 42 | { 43 | time: 'start', 44 | duration: 1, 45 | easing: 'quadratic-out', 46 | type: 'text-slide', 47 | scope: 'split-clip', 48 | split: 'word', 49 | direction: 'up', 50 | }, 51 | ], 52 | text: 'Enter your tagline here', 53 | font_family: 'Oswald', 54 | font_weight: '600', 55 | text_transform: 'uppercase', 56 | }, 57 | { 58 | id: '04b59bd6-b9df-439f-9586-2d5095c9f959', 59 | name: 'Title', 60 | type: 'text', 61 | track: 3, 62 | time: 0, 63 | y: '41.41%', 64 | width: '69.79%', 65 | height: '47.61%', 66 | x_alignment: '50%', 67 | fill_color: '#ffffff', 68 | animations: [ 69 | { 70 | time: 'start', 71 | duration: 1, 72 | easing: 'quadratic-out', 73 | type: 'text-appear', 74 | split: 'line', 75 | }, 76 | ], 77 | text: 'Lorem ipsum dolor sit amet', 78 | font_family: 'Oswald', 79 | font_weight: '600', 80 | text_transform: 'uppercase', 81 | }, 82 | ], 83 | }; 84 | } 85 | -------------------------------------------------------------------------------- /4-play-and-pause/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /4-play-and-pause/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | 37 | # JetBrains editors 38 | /.idea 39 | -------------------------------------------------------------------------------- /4-play-and-pause/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "trailingComma": "all", 4 | "tabWidth": 2, 5 | "semi": true, 6 | "singleQuote": true 7 | } 8 | -------------------------------------------------------------------------------- /4-play-and-pause/README.md: -------------------------------------------------------------------------------- 1 | # Video Editor Tutorial – Play and Pause 2 | 3 | This example is part of this article: [How to Build a Video Editor in JavaScript](https://creatomate.com/blog/how-to-build-a-video-editor-in-javascript) 4 | 5 | --- 6 | 7 | Run it with the following command: 8 | 9 | ```bash 10 | npm install && npm run dev 11 | ``` 12 | 13 | You can also try it online using StackBlitz (Chrome and Edge only): 14 | 15 | [![Run on StackBlitz](https://user-images.githubusercontent.com/44575638/199058604-b6e5e08a-cdfd-451a-8ce9-ab7355b22786.svg)](https://stackblitz.com/github/creatomate/video-editor-tutorial/tree/main/4-play-and-pause) 16 | -------------------------------------------------------------------------------- /4-play-and-pause/components/Main.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react'; 2 | import { Inter } from 'next/font/google'; 3 | import { Preview } from '@creatomate/preview'; 4 | import { getBasicComposition } from '@/utility/getBasicComposition'; 5 | import styles from '@/styles/Home.module.css'; 6 | 7 | const inter = Inter({ subsets: ['latin'] }); 8 | 9 | export const Main: React.FC = () => { 10 | const [isLoading, setIsLoading] = useState(true); 11 | 12 | // Reference to the preview 13 | const previewRef = useRef(); 14 | 15 | const setUpPreview = (htmlElement: HTMLDivElement) => { 16 | // Clean up an older instance of the preview SDK 17 | if (previewRef.current) { 18 | previewRef.current.dispose(); 19 | previewRef.current = undefined; 20 | } 21 | 22 | // Initialize a preview. Make sure you provide your own public token, which can be found in your dashboard under Project Settings 23 | const preview = new Preview(htmlElement, 'player', 'public-0x6hcqpfhrhw16d67ogth7ry'); 24 | 25 | preview.onReady = async () => { 26 | // Once the SDK is ready, create a basic video scene 27 | await preview.setSource(getBasicComposition()); 28 | 29 | // Skip to 2 seconds into the video 30 | await preview.setTime(2); 31 | 32 | setIsLoading(false); 33 | }; 34 | 35 | previewRef.current = preview; 36 | }; 37 | 38 | const playVideo = async () => { 39 | await previewRef.current?.play(); 40 | }; 41 | 42 | const pauseVideo = async () => { 43 | await previewRef.current?.pause(); 44 | }; 45 | 46 | return ( 47 |
48 |
49 | {isLoading && 'Loading...'} 50 | 51 |
{ 54 | if (htmlElement && htmlElement !== previewRef.current?.element) { 55 | setUpPreview(htmlElement); 56 | } 57 | }} 58 | /> 59 | 60 |
61 | 68 |   69 | 76 |
77 |
78 |
79 | ); 80 | }; 81 | -------------------------------------------------------------------------------- /4-play-and-pause/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | }; 5 | 6 | module.exports = nextConfig; 7 | -------------------------------------------------------------------------------- /4-play-and-pause/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "video-editor", 3 | "version": "1.0.0", 4 | "author": "Creatomate", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "next dev", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint" 11 | }, 12 | "dependencies": { 13 | "@creatomate/preview": "^1.5.0", 14 | "@types/node": "20.4.5", 15 | "@types/react": "18.2.16", 16 | "@types/react-dom": "18.2.7", 17 | "creatomate": "^1.1.0", 18 | "eslint": "8.45.0", 19 | "eslint-config-next": "13.4.12", 20 | "modern-normalize": "2.0.0", 21 | "next": "^13.5.6", 22 | "prettier": "3.0.0", 23 | "react": "18.2.0", 24 | "react-dom": "18.2.0", 25 | "typescript": "5.1.6" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /4-play-and-pause/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from 'next/app' 2 | import '@/styles/globals.css' 3 | 4 | export default function App({ Component, pageProps }: AppProps) { 5 | return 6 | } 7 | -------------------------------------------------------------------------------- /4-play-and-pause/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document'; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /4-play-and-pause/pages/api/videos.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | 3 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 4 | res.status(200).json({}); 5 | } 6 | -------------------------------------------------------------------------------- /4-play-and-pause/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import { Main } from '@/components/Main'; 3 | 4 | export default function Home() { 5 | return ( 6 | <> 7 | 8 | Create Next App 9 | 10 | 11 | 12 | 13 |
14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /4-play-and-pause/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Creatomate/video-editor-tutorial/4163bdc73a29f76d27e5717fbb2b1ee72dfa4ca9/4-play-and-pause/public/favicon.ico -------------------------------------------------------------------------------- /4-play-and-pause/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | width: 100vw; 3 | height: 100vh; 4 | display: flex; 5 | flex-direction: column; 6 | } 7 | 8 | .center { 9 | width: 100%; 10 | height: 100%; 11 | max-width: 720px; 12 | max-height: 720px; 13 | margin: auto; 14 | display: flex; 15 | flex-direction: column; 16 | } 17 | 18 | .container { 19 | flex: 1; 20 | } 21 | 22 | .controls { 23 | display: flex; 24 | justify-content: center; 25 | } 26 | -------------------------------------------------------------------------------- /4-play-and-pause/styles/globals.css: -------------------------------------------------------------------------------- 1 | @import '@/node_modules/modern-normalize/modern-normalize.css'; 2 | 3 | html { 4 | background-color: #e4e7eb; 5 | user-select: none; 6 | } 7 | 8 | body { 9 | font-size: 18px; 10 | line-height: 1.5; 11 | } 12 | 13 | button { 14 | padding: 10px 15px; 15 | border: none; 16 | background-color: #0065eb; 17 | border-radius: 5px; 18 | color: #fff; 19 | font-size: 16px; 20 | font-weight: 500; 21 | cursor: pointer; 22 | } 23 | 24 | input, 25 | textarea { 26 | display: block; 27 | margin: 5px 0; 28 | padding: 15px; 29 | width: 100%; 30 | border: 1px solid #b3bfcc; 31 | border-radius: 5px; 32 | outline: none; 33 | resize: none; 34 | } 35 | 36 | textarea { 37 | height: 75px; 38 | } 39 | 40 | input:focus, 41 | textarea:focus { 42 | background-color: #e9f4fc; 43 | border-color: #005aff; 44 | } 45 | 46 | select { 47 | display: block; 48 | margin: 5px 0; 49 | padding: 10px 15px; 50 | width: 100%; 51 | background-color: #fff; 52 | border: 1px solid #b3bfcc; 53 | border-radius: 5px; 54 | outline: none; 55 | 56 | /* Arrow */ 57 | appearance: none; 58 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 10'%3E%3Cpath d='m0.993 2.02 5.25 5.25c0.966 0.966 2.534 0.966 3.5-0l5.264-5.264' fill='none' stroke='%23000' stroke-width='2px'/%3E%3C/svg%3E"); 59 | background-repeat: no-repeat; 60 | background-position: right 12px top 50%; 61 | background-size: 16px auto; 62 | } 63 | 64 | select:focus { 65 | background-color: #e9f4fc; 66 | border-color: #005aff; 67 | } 68 | 69 | *::-webkit-scrollbar { 70 | width: 8px; 71 | height: 8px; 72 | } 73 | 74 | *::-webkit-scrollbar-track { 75 | background-color: #dedede; 76 | } 77 | 78 | *::-webkit-scrollbar-thumb { 79 | background-color: #c0c0c0; 80 | } 81 | 82 | *::-webkit-scrollbar-corner { 83 | background-color: rgba(0, 0, 0, 0); 84 | } 85 | -------------------------------------------------------------------------------- /4-play-and-pause/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "paths": { 18 | "@/*": ["./*"] 19 | } 20 | }, 21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /4-play-and-pause/utility/getBasicComposition.ts: -------------------------------------------------------------------------------- 1 | // This is the source code for a simple video with two text elements and a background image. 2 | // Learn more about this here: https://creatomate.com/docs/json/introduction 3 | // If you want to edit it, copy-paste it into the template editor: https://creatomate.com/docs/template-editor/source-editor 4 | export function getBasicComposition() { 5 | return { 6 | output_format: 'mp4', 7 | width: 1920, 8 | height: 1080, 9 | elements: [ 10 | { 11 | id: '48734f5c-8c90-41ac-a059-e949e733936b', 12 | name: 'Main-Image', 13 | type: 'image', 14 | track: 1, 15 | time: 0, 16 | color_overlay: 'rgba(0,0,0,0.25)', 17 | animations: [ 18 | { 19 | easing: 'linear', 20 | type: 'scale', 21 | fade: false, 22 | scope: 'element', 23 | end_scale: '130%', 24 | start_scale: '100%', 25 | }, 26 | ], 27 | source: 'https://creatomate.com/files/assets/5bc5ed6f-26e6-4c3a-8d03-1b169dc7f983.jpg', 28 | }, 29 | { 30 | id: '72ec46a3-610c-4b46-86ef-c9bbc337f012', 31 | name: 'Tagline', 32 | type: 'text', 33 | track: 2, 34 | time: 1, 35 | duration: 2.5, 36 | y: '73.71%', 37 | width: '69.79%', 38 | height: '12.56%', 39 | x_alignment: '50%', 40 | fill_color: '#ffffff', 41 | animations: [ 42 | { 43 | time: 'start', 44 | duration: 1, 45 | easing: 'quadratic-out', 46 | type: 'text-slide', 47 | scope: 'split-clip', 48 | split: 'word', 49 | direction: 'up', 50 | }, 51 | ], 52 | text: 'Enter your tagline here', 53 | font_family: 'Oswald', 54 | font_weight: '600', 55 | text_transform: 'uppercase', 56 | }, 57 | { 58 | id: '04b59bd6-b9df-439f-9586-2d5095c9f959', 59 | name: 'Title', 60 | type: 'text', 61 | track: 3, 62 | time: 0, 63 | y: '41.41%', 64 | width: '69.79%', 65 | height: '47.61%', 66 | x_alignment: '50%', 67 | fill_color: '#ffffff', 68 | animations: [ 69 | { 70 | time: 'start', 71 | duration: 1, 72 | easing: 'quadratic-out', 73 | type: 'text-appear', 74 | split: 'line', 75 | }, 76 | ], 77 | text: 'Lorem ipsum dolor sit amet', 78 | font_family: 'Oswald', 79 | font_weight: '600', 80 | text_transform: 'uppercase', 81 | }, 82 | ], 83 | }; 84 | } 85 | -------------------------------------------------------------------------------- /5-state-management/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /5-state-management/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | 37 | # JetBrains editors 38 | /.idea 39 | -------------------------------------------------------------------------------- /5-state-management/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "trailingComma": "all", 4 | "tabWidth": 2, 5 | "semi": true, 6 | "singleQuote": true 7 | } 8 | -------------------------------------------------------------------------------- /5-state-management/README.md: -------------------------------------------------------------------------------- 1 | # Video Editor Tutorial – State Management 2 | 3 | This example is part of this article: [How to Build a Video Editor in JavaScript](https://creatomate.com/blog/how-to-build-a-video-editor-in-javascript) 4 | 5 | --- 6 | 7 | Run it with the following command: 8 | 9 | ```bash 10 | npm install && npm run dev 11 | ``` 12 | 13 | You can also try it online using StackBlitz (Chrome and Edge only): 14 | 15 | [![Run on StackBlitz](https://user-images.githubusercontent.com/44575638/199058604-b6e5e08a-cdfd-451a-8ce9-ab7355b22786.svg)](https://stackblitz.com/github/creatomate/video-editor-tutorial/tree/main/5-state-management) 16 | -------------------------------------------------------------------------------- /5-state-management/components/Main.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react'; 2 | import { Inter } from 'next/font/google'; 3 | import { Preview } from '@creatomate/preview'; 4 | import { getBasicComposition } from '@/utility/getBasicComposition'; 5 | import styles from '@/styles/Home.module.css'; 6 | 7 | const inter = Inter({ subsets: ['latin'] }); 8 | 9 | export const Main: React.FC = () => { 10 | const [isLoading, setIsLoading] = useState(true); 11 | const [counter, setCounter] = useState(1); 12 | 13 | const [value, setValue] = useState(); 14 | 15 | // Reference to the preview 16 | const previewRef = useRef(); 17 | 18 | const setUpPreview = (htmlElement: HTMLDivElement) => { 19 | // Clean up an older instance of the preview SDK 20 | if (previewRef.current) { 21 | previewRef.current.dispose(); 22 | previewRef.current = undefined; 23 | } 24 | 25 | // Initialize a preview. Make sure you provide your own public token, which can be found in your dashboard under Project Settings 26 | const preview = new Preview(htmlElement, 'player', 'public-0x6hcqpfhrhw16d67ogth7ry'); 27 | 28 | preview.onReady = async () => { 29 | // Once the SDK is ready, create a basic video scene 30 | await preview.setSource(getBasicComposition()); 31 | 32 | // Skip to 2 seconds into the video 33 | await preview.setTime(2); 34 | 35 | setIsLoading(false); 36 | }; 37 | 38 | preview.onStateChange = async (state) => { 39 | // Find title element 40 | const element = preview.findElement((element) => element.source.name === 'Title'); 41 | if (element) { 42 | setValue(element.source.text); 43 | } 44 | }; 45 | 46 | previewRef.current = preview; 47 | }; 48 | 49 | const changeTitle = async (value: string) => { 50 | // Note we're using 'applyModifications' instead of 'setModifications' for this example to show the undo/redo feature! 51 | await previewRef.current?.applyModifications({ 52 | Title: value, 53 | }); 54 | }; 55 | 56 | return ( 57 |
58 |
59 | {isLoading && 'Loading...'} 60 | 61 |
{ 64 | if (htmlElement && htmlElement !== previewRef.current?.element) { 65 | setUpPreview(htmlElement); 66 | } 67 | }} 68 | /> 69 | 70 | {value &&
State: {value}
} 71 | 72 |
73 | 81 |   82 | 89 |   90 | 97 |
98 |
99 |
100 | ); 101 | }; 102 | -------------------------------------------------------------------------------- /5-state-management/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | }; 5 | 6 | module.exports = nextConfig; 7 | -------------------------------------------------------------------------------- /5-state-management/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "video-editor", 3 | "version": "1.0.0", 4 | "author": "Creatomate", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "next dev", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint" 11 | }, 12 | "dependencies": { 13 | "@creatomate/preview": "^1.5.0", 14 | "@types/node": "20.4.5", 15 | "@types/react": "18.2.16", 16 | "@types/react-dom": "18.2.7", 17 | "creatomate": "^1.1.0", 18 | "eslint": "8.45.0", 19 | "eslint-config-next": "13.4.12", 20 | "modern-normalize": "2.0.0", 21 | "next": "^13.5.6", 22 | "prettier": "3.0.0", 23 | "react": "18.2.0", 24 | "react-dom": "18.2.0", 25 | "typescript": "5.1.6" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /5-state-management/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from 'next/app' 2 | import '@/styles/globals.css' 3 | 4 | export default function App({ Component, pageProps }: AppProps) { 5 | return 6 | } 7 | -------------------------------------------------------------------------------- /5-state-management/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document'; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /5-state-management/pages/api/videos.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | 3 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 4 | res.status(200).json({}); 5 | } 6 | -------------------------------------------------------------------------------- /5-state-management/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import { Main } from '@/components/Main'; 3 | 4 | export default function Home() { 5 | return ( 6 | <> 7 | 8 | Create Next App 9 | 10 | 11 | 12 | 13 |
14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /5-state-management/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Creatomate/video-editor-tutorial/4163bdc73a29f76d27e5717fbb2b1ee72dfa4ca9/5-state-management/public/favicon.ico -------------------------------------------------------------------------------- /5-state-management/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | width: 100vw; 3 | height: 100vh; 4 | display: flex; 5 | flex-direction: column; 6 | } 7 | 8 | .center { 9 | width: 100%; 10 | height: 100%; 11 | max-width: 720px; 12 | max-height: 720px; 13 | margin: auto; 14 | display: flex; 15 | flex-direction: column; 16 | } 17 | 18 | .container { 19 | flex: 1; 20 | } 21 | 22 | .controls { 23 | display: flex; 24 | justify-content: center; 25 | } 26 | -------------------------------------------------------------------------------- /5-state-management/styles/globals.css: -------------------------------------------------------------------------------- 1 | @import '@/node_modules/modern-normalize/modern-normalize.css'; 2 | 3 | html { 4 | background-color: #e4e7eb; 5 | user-select: none; 6 | } 7 | 8 | body { 9 | font-size: 18px; 10 | line-height: 1.5; 11 | } 12 | 13 | button { 14 | padding: 10px 15px; 15 | border: none; 16 | background-color: #0065eb; 17 | border-radius: 5px; 18 | color: #fff; 19 | font-size: 16px; 20 | font-weight: 500; 21 | cursor: pointer; 22 | } 23 | 24 | input, 25 | textarea { 26 | display: block; 27 | margin: 5px 0; 28 | padding: 15px; 29 | width: 100%; 30 | border: 1px solid #b3bfcc; 31 | border-radius: 5px; 32 | outline: none; 33 | resize: none; 34 | } 35 | 36 | textarea { 37 | height: 75px; 38 | } 39 | 40 | input:focus, 41 | textarea:focus { 42 | background-color: #e9f4fc; 43 | border-color: #005aff; 44 | } 45 | 46 | select { 47 | display: block; 48 | margin: 5px 0; 49 | padding: 10px 15px; 50 | width: 100%; 51 | background-color: #fff; 52 | border: 1px solid #b3bfcc; 53 | border-radius: 5px; 54 | outline: none; 55 | 56 | /* Arrow */ 57 | appearance: none; 58 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 10'%3E%3Cpath d='m0.993 2.02 5.25 5.25c0.966 0.966 2.534 0.966 3.5-0l5.264-5.264' fill='none' stroke='%23000' stroke-width='2px'/%3E%3C/svg%3E"); 59 | background-repeat: no-repeat; 60 | background-position: right 12px top 50%; 61 | background-size: 16px auto; 62 | } 63 | 64 | select:focus { 65 | background-color: #e9f4fc; 66 | border-color: #005aff; 67 | } 68 | 69 | *::-webkit-scrollbar { 70 | width: 8px; 71 | height: 8px; 72 | } 73 | 74 | *::-webkit-scrollbar-track { 75 | background-color: #dedede; 76 | } 77 | 78 | *::-webkit-scrollbar-thumb { 79 | background-color: #c0c0c0; 80 | } 81 | 82 | *::-webkit-scrollbar-corner { 83 | background-color: rgba(0, 0, 0, 0); 84 | } 85 | -------------------------------------------------------------------------------- /5-state-management/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "paths": { 18 | "@/*": ["./*"] 19 | } 20 | }, 21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /5-state-management/utility/getBasicComposition.ts: -------------------------------------------------------------------------------- 1 | // This is the source code for a simple video with two text elements and a background image. 2 | // Learn more about this here: https://creatomate.com/docs/json/introduction 3 | // If you want to edit it, copy-paste it into the template editor: https://creatomate.com/docs/template-editor/source-editor 4 | export function getBasicComposition() { 5 | return { 6 | output_format: 'mp4', 7 | width: 1920, 8 | height: 1080, 9 | elements: [ 10 | { 11 | id: '48734f5c-8c90-41ac-a059-e949e733936b', 12 | name: 'Main-Image', 13 | type: 'image', 14 | track: 1, 15 | time: 0, 16 | color_overlay: 'rgba(0,0,0,0.25)', 17 | animations: [ 18 | { 19 | easing: 'linear', 20 | type: 'scale', 21 | fade: false, 22 | scope: 'element', 23 | end_scale: '130%', 24 | start_scale: '100%', 25 | }, 26 | ], 27 | source: 'https://creatomate.com/files/assets/5bc5ed6f-26e6-4c3a-8d03-1b169dc7f983.jpg', 28 | }, 29 | { 30 | id: '72ec46a3-610c-4b46-86ef-c9bbc337f012', 31 | name: 'Tagline', 32 | type: 'text', 33 | track: 2, 34 | time: 1, 35 | duration: 2.5, 36 | y: '73.71%', 37 | width: '69.79%', 38 | height: '12.56%', 39 | x_alignment: '50%', 40 | fill_color: '#ffffff', 41 | animations: [ 42 | { 43 | time: 'start', 44 | duration: 1, 45 | easing: 'quadratic-out', 46 | type: 'text-slide', 47 | scope: 'split-clip', 48 | split: 'word', 49 | direction: 'up', 50 | }, 51 | ], 52 | text: 'Enter your tagline here', 53 | font_family: 'Oswald', 54 | font_weight: '600', 55 | text_transform: 'uppercase', 56 | }, 57 | { 58 | id: '04b59bd6-b9df-439f-9586-2d5095c9f959', 59 | name: 'Title', 60 | type: 'text', 61 | track: 3, 62 | time: 0, 63 | y: '41.41%', 64 | width: '69.79%', 65 | height: '47.61%', 66 | x_alignment: '50%', 67 | fill_color: '#ffffff', 68 | animations: [ 69 | { 70 | time: 'start', 71 | duration: 1, 72 | easing: 'quadratic-out', 73 | type: 'text-appear', 74 | split: 'line', 75 | }, 76 | ], 77 | text: 'Lorem ipsum dolor sit amet', 78 | font_family: 'Oswald', 79 | font_weight: '600', 80 | text_transform: 'uppercase', 81 | }, 82 | ], 83 | }; 84 | } 85 | -------------------------------------------------------------------------------- /6-interactivity/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /6-interactivity/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | 37 | # JetBrains editors 38 | /.idea 39 | -------------------------------------------------------------------------------- /6-interactivity/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "trailingComma": "all", 4 | "tabWidth": 2, 5 | "semi": true, 6 | "singleQuote": true 7 | } 8 | -------------------------------------------------------------------------------- /6-interactivity/README.md: -------------------------------------------------------------------------------- 1 | # Video Editor Tutorial – Interactivity 2 | 3 | This example is part of this article: [How to Build a Video Editor in JavaScript](https://creatomate.com/blog/how-to-build-a-video-editor-in-javascript) 4 | 5 | --- 6 | 7 | Run it with the following command: 8 | 9 | ```bash 10 | npm install && npm run dev 11 | ``` 12 | 13 | You can also try it online using StackBlitz (Chrome and Edge only): 14 | 15 | [![Run on StackBlitz](https://user-images.githubusercontent.com/44575638/199058604-b6e5e08a-cdfd-451a-8ce9-ab7355b22786.svg)](https://stackblitz.com/github/creatomate/video-editor-tutorial/tree/main/6-interactivity) 16 | -------------------------------------------------------------------------------- /6-interactivity/components/Main.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react'; 2 | import { Inter } from 'next/font/google'; 3 | import { Preview } from '@creatomate/preview'; 4 | import { ProgressControl } from '@/components/ProgressControl'; 5 | import { getBasicComposition } from '@/utility/getBasicComposition'; 6 | import styles from '@/styles/Home.module.css'; 7 | 8 | const inter = Inter({ subsets: ['latin'] }); 9 | 10 | export const Main: React.FC = () => { 11 | const [isLoading, setIsLoading] = useState(true); 12 | const [counter, setCounter] = useState(1); 13 | 14 | // Reference to the preview 15 | const previewRef = useRef(); 16 | 17 | const setUpPreview = (htmlElement: HTMLDivElement) => { 18 | // Clean up an older instance of the preview SDK 19 | if (previewRef.current) { 20 | previewRef.current.dispose(); 21 | previewRef.current = undefined; 22 | } 23 | 24 | // Initialize a preview. Make sure you provide your own public token, which can be found in your dashboard under Project Settings 25 | const preview = new Preview(htmlElement, 'interactive', 'public-0x6hcqpfhrhw16d67ogth7ry'); 26 | 27 | preview.onReady = async () => { 28 | // Once the SDK is ready, create a basic video scene 29 | await preview.setSource(getBasicComposition()); 30 | 31 | // Set the zoom mode so the user can pan, zoom in and out, but always keep the canvas in the middle 32 | await preview.setZoom('centered'); 33 | 34 | // Skip to 2 seconds into the video 35 | await preview.setTime(2); 36 | 37 | setIsLoading(false); 38 | }; 39 | 40 | previewRef.current = preview; 41 | }; 42 | 43 | const changeTitle = async (value: string) => { 44 | // Note we're using 'applyModifications' instead of 'setModifications' for this example to show the undo/redo feature! 45 | await previewRef.current?.applyModifications({ 46 | Title: value, 47 | }); 48 | }; 49 | 50 | return ( 51 |
52 | {isLoading && 'Loading...'} 53 | 54 |
{ 57 | if (htmlElement && htmlElement !== previewRef.current?.element) { 58 | setUpPreview(htmlElement); 59 | } 60 | }} 61 | /> 62 | 63 | {previewRef.current && } 64 | 65 |
66 | 74 |   75 | 82 |   83 | 90 |
91 |
92 | 99 |   100 | 107 |   108 | 115 |   116 | 127 |
128 |
129 | 136 |   137 | 144 |   145 | 152 |   153 | 160 |
161 |
162 | ); 163 | }; 164 | -------------------------------------------------------------------------------- /6-interactivity/components/ProgressControl.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useRef, useState } from 'react'; 2 | import { Preview } from '@creatomate/preview'; 3 | import { DraggableCore } from 'react-draggable'; 4 | import throttle from 'lodash/throttle'; 5 | import styles from '@/styles/Home.module.css'; 6 | 7 | interface ProgressControlProps { 8 | preview: Preview; 9 | } 10 | 11 | export const ProgressControl: React.FC = (props) => { 12 | const trackRef = useRef(null); 13 | const handleRef = useRef(null); 14 | 15 | // The current time of the video 16 | const [currentTime, setCurrentTime] = useState(0); 17 | const currentTrackProgress = props.preview.state?.duration ? currentTime / props.preview.state.duration : 0; 18 | 19 | // Listen for time changes 20 | useEffect(() => { 21 | props.preview.onTimeChange = (time) => { 22 | setCurrentTime(time); 23 | }; 24 | }, [props.preview]); 25 | 26 | // Sets the current time 27 | const setTime = async (time: number) => { 28 | await props.preview.setTime(time); 29 | }; 30 | 31 | // Throttle the 'setTime' function to 15 milliseconds as mouse events are not throttled by default 32 | // eslint-disable-next-line react-hooks/exhaustive-deps 33 | const setTimeThrottled = useCallback(throttle(setTime, 15), []); 34 | 35 | // This is where data is stored while dragging the mouse 36 | const [dragContext, setDragContext] = useState<{ startX: number; startTime: number }>(); 37 | 38 | return ( 39 |
40 |
41 | { 44 | // Set the current X pixel position and current time when dragging starts 45 | setDragContext({ 46 | startX: data.x, 47 | startTime: currentTime, 48 | }); 49 | }} 50 | onDrag={(e, data) => { 51 | if (props.preview.state && trackRef.current && dragContext) { 52 | // Width of the track element in pixels 53 | const trackWidth = trackRef.current.clientWidth; 54 | 55 | // Track progress from 0 to 1 56 | const trackProgress = (data.x - dragContext.startX) / trackWidth; 57 | 58 | // Track progress in seconds 59 | const trackProgressSeconds = Math.min( 60 | Math.max(dragContext.startTime + props.preview.state.duration * trackProgress, 0), 61 | props.preview.state.duration - 0.001, 62 | ); 63 | 64 | // Set the time progress 65 | setTimeThrottled(trackProgressSeconds); 66 | } 67 | }} 68 | onStop={() => { 69 | setDragContext(undefined); 70 | }} 71 | > 72 |
77 | 78 |
79 | ); 80 | }; 81 | -------------------------------------------------------------------------------- /6-interactivity/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | }; 5 | 6 | module.exports = nextConfig; 7 | -------------------------------------------------------------------------------- /6-interactivity/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "video-editor", 3 | "version": "1.0.0", 4 | "author": "Creatomate", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "next dev", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint" 11 | }, 12 | "dependencies": { 13 | "@creatomate/preview": "^1.5.0", 14 | "@types/lodash": "4.14.196", 15 | "@types/node": "20.4.5", 16 | "@types/react": "18.2.16", 17 | "@types/react-dom": "18.2.7", 18 | "creatomate": "^1.1.0", 19 | "eslint": "8.45.0", 20 | "eslint-config-next": "13.4.12", 21 | "lodash": "4.17.21", 22 | "modern-normalize": "2.0.0", 23 | "next": "^13.5.6", 24 | "prettier": "3.0.0", 25 | "react": "18.2.0", 26 | "react-dom": "18.2.0", 27 | "react-draggable": "4.4.5", 28 | "typescript": "5.1.6" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /6-interactivity/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from 'next/app' 2 | import '@/styles/globals.css' 3 | 4 | export default function App({ Component, pageProps }: AppProps) { 5 | return 6 | } 7 | -------------------------------------------------------------------------------- /6-interactivity/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document'; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /6-interactivity/pages/api/videos.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | 3 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 4 | res.status(200).json({}); 5 | } 6 | -------------------------------------------------------------------------------- /6-interactivity/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import { Main } from '@/components/Main'; 3 | 4 | export default function Home() { 5 | return ( 6 | <> 7 | 8 | Create Next App 9 | 10 | 11 | 12 | 13 |
14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /6-interactivity/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Creatomate/video-editor-tutorial/4163bdc73a29f76d27e5717fbb2b1ee72dfa4ca9/6-interactivity/public/favicon.ico -------------------------------------------------------------------------------- /6-interactivity/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | width: 100vw; 3 | height: 100vh; 4 | display: flex; 5 | flex-direction: column; 6 | } 7 | 8 | .container { 9 | width: 100%; 10 | height: 100%; 11 | } 12 | 13 | .controls { 14 | display: flex; 15 | justify-content: center; 16 | } 17 | 18 | .progressControl { 19 | margin: 0 50px; 20 | position: relative; 21 | height: 50px; 22 | } 23 | 24 | .progressControlTrack { 25 | position: absolute; 26 | top: 50%; 27 | width: 100%; 28 | height: 5px; 29 | transform: translateY(-50%); 30 | background-color: rgba(0, 0, 0, 10%); 31 | } 32 | 33 | .progressControlHandle { 34 | position: absolute; 35 | top: 50%; 36 | width: 20px; 37 | height: 20px; 38 | transform: translateY(-50%); 39 | background-color: #005aff; 40 | border-radius: 100%; 41 | cursor: ew-resize; 42 | } 43 | -------------------------------------------------------------------------------- /6-interactivity/styles/globals.css: -------------------------------------------------------------------------------- 1 | @import '@/node_modules/modern-normalize/modern-normalize.css'; 2 | 3 | html { 4 | background-color: #e4e7eb; 5 | user-select: none; 6 | } 7 | 8 | body { 9 | font-size: 18px; 10 | line-height: 1.5; 11 | } 12 | 13 | button { 14 | padding: 10px 15px; 15 | border: none; 16 | background-color: #0065eb; 17 | border-radius: 5px; 18 | color: #fff; 19 | font-size: 16px; 20 | font-weight: 500; 21 | cursor: pointer; 22 | } 23 | 24 | input, 25 | textarea { 26 | display: block; 27 | margin: 5px 0; 28 | padding: 15px; 29 | width: 100%; 30 | border: 1px solid #b3bfcc; 31 | border-radius: 5px; 32 | outline: none; 33 | resize: none; 34 | } 35 | 36 | textarea { 37 | height: 75px; 38 | } 39 | 40 | input:focus, 41 | textarea:focus { 42 | background-color: #e9f4fc; 43 | border-color: #005aff; 44 | } 45 | 46 | select { 47 | display: block; 48 | margin: 5px 0; 49 | padding: 10px 15px; 50 | width: 100%; 51 | background-color: #fff; 52 | border: 1px solid #b3bfcc; 53 | border-radius: 5px; 54 | outline: none; 55 | 56 | /* Arrow */ 57 | appearance: none; 58 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 10'%3E%3Cpath d='m0.993 2.02 5.25 5.25c0.966 0.966 2.534 0.966 3.5-0l5.264-5.264' fill='none' stroke='%23000' stroke-width='2px'/%3E%3C/svg%3E"); 59 | background-repeat: no-repeat; 60 | background-position: right 12px top 50%; 61 | background-size: 16px auto; 62 | } 63 | 64 | select:focus { 65 | background-color: #e9f4fc; 66 | border-color: #005aff; 67 | } 68 | 69 | *::-webkit-scrollbar { 70 | width: 8px; 71 | height: 8px; 72 | } 73 | 74 | *::-webkit-scrollbar-track { 75 | background-color: #dedede; 76 | } 77 | 78 | *::-webkit-scrollbar-thumb { 79 | background-color: #c0c0c0; 80 | } 81 | 82 | *::-webkit-scrollbar-corner { 83 | background-color: rgba(0, 0, 0, 0); 84 | } 85 | -------------------------------------------------------------------------------- /6-interactivity/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "paths": { 18 | "@/*": ["./*"] 19 | } 20 | }, 21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /6-interactivity/utility/getBasicComposition.ts: -------------------------------------------------------------------------------- 1 | // This is the source code for a simple video with two text elements and a background image. 2 | // Learn more about this here: https://creatomate.com/docs/json/introduction 3 | // If you want to edit it, copy-paste it into the template editor: https://creatomate.com/docs/template-editor/source-editor 4 | export function getBasicComposition() { 5 | return { 6 | output_format: 'mp4', 7 | width: 1920, 8 | height: 1080, 9 | elements: [ 10 | { 11 | id: '48734f5c-8c90-41ac-a059-e949e733936b', 12 | name: 'Main-Image', 13 | type: 'image', 14 | track: 1, 15 | time: 0, 16 | color_overlay: 'rgba(0,0,0,0.25)', 17 | animations: [ 18 | { 19 | easing: 'linear', 20 | type: 'scale', 21 | fade: false, 22 | scope: 'element', 23 | end_scale: '130%', 24 | start_scale: '100%', 25 | }, 26 | ], 27 | source: 'https://creatomate.com/files/assets/5bc5ed6f-26e6-4c3a-8d03-1b169dc7f983.jpg', 28 | }, 29 | { 30 | id: '72ec46a3-610c-4b46-86ef-c9bbc337f012', 31 | name: 'Tagline', 32 | type: 'text', 33 | track: 2, 34 | time: 1, 35 | duration: 2.5, 36 | y: '73.71%', 37 | width: '69.79%', 38 | height: '12.56%', 39 | x_alignment: '50%', 40 | fill_color: '#ffffff', 41 | animations: [ 42 | { 43 | time: 'start', 44 | duration: 1, 45 | easing: 'quadratic-out', 46 | type: 'text-slide', 47 | scope: 'split-clip', 48 | split: 'word', 49 | direction: 'up', 50 | }, 51 | ], 52 | text: 'Enter your tagline here', 53 | font_family: 'Oswald', 54 | font_weight: '600', 55 | text_transform: 'uppercase', 56 | }, 57 | { 58 | id: '04b59bd6-b9df-439f-9586-2d5095c9f959', 59 | name: 'Title', 60 | type: 'text', 61 | track: 3, 62 | time: 0, 63 | y: '41.41%', 64 | width: '69.79%', 65 | height: '47.61%', 66 | x_alignment: '50%', 67 | fill_color: '#ffffff', 68 | animations: [ 69 | { 70 | time: 'start', 71 | duration: 1, 72 | easing: 'quadratic-out', 73 | type: 'text-appear', 74 | split: 'line', 75 | }, 76 | ], 77 | text: 'Lorem ipsum dolor sit amet', 78 | font_family: 'Oswald', 79 | font_weight: '600', 80 | text_transform: 'uppercase', 81 | }, 82 | ], 83 | }; 84 | } 85 | -------------------------------------------------------------------------------- /7-advanced-mutation/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /7-advanced-mutation/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | 37 | # JetBrains editors 38 | /.idea 39 | -------------------------------------------------------------------------------- /7-advanced-mutation/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "trailingComma": "all", 4 | "tabWidth": 2, 5 | "semi": true, 6 | "singleQuote": true 7 | } 8 | -------------------------------------------------------------------------------- /7-advanced-mutation/README.md: -------------------------------------------------------------------------------- 1 | # Video Editor Tutorial – Advanced Mutation 2 | 3 | This example is part of this article: [How to Build a Video Editor in JavaScript](https://creatomate.com/blog/how-to-build-a-video-editor-in-javascript) 4 | 5 | --- 6 | 7 | Run it with the following command: 8 | 9 | ```bash 10 | npm install && npm run dev 11 | ``` 12 | 13 | You can also try it online using StackBlitz (Chrome and Edge only): 14 | 15 | [![Run on StackBlitz](https://user-images.githubusercontent.com/44575638/199058604-b6e5e08a-cdfd-451a-8ce9-ab7355b22786.svg)](https://stackblitz.com/github/creatomate/video-editor-tutorial/tree/main/7-advanced-mutation) 16 | -------------------------------------------------------------------------------- /7-advanced-mutation/components/Main.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react'; 2 | import { Inter } from 'next/font/google'; 3 | import { Preview } from '@creatomate/preview'; 4 | import { getSlideshowComposition } from '@/utility/getSlideshowComposition'; 5 | import styles from '@/styles/Home.module.css'; 6 | import { addSlide } from '@/utility/addSlide'; 7 | 8 | const inter = Inter({ subsets: ['latin'] }); 9 | 10 | export const Main: React.FC = () => { 11 | const [isLoading, setIsLoading] = useState(true); 12 | 13 | // Reference to the preview 14 | const previewRef = useRef(); 15 | 16 | const setUpPreview = (htmlElement: HTMLDivElement) => { 17 | // Clean up an older instance of the preview SDK 18 | if (previewRef.current) { 19 | previewRef.current.dispose(); 20 | previewRef.current = undefined; 21 | } 22 | 23 | // Initialize a preview. Make sure you provide your own public token, which can be found in your dashboard under Project Settings 24 | const preview = new Preview(htmlElement, 'player', 'public-0x6hcqpfhrhw16d67ogth7ry'); 25 | 26 | preview.onReady = async () => { 27 | // This is the source code of "Image Slideshow w/ Intro and Outro" template under the "Storytelling" category 28 | // See: https://user-images.githubusercontent.com/44575638/227714779-31292519-3a75-40a4-8c3f-549e28100a48.jpg 29 | await preview.setSource(getSlideshowComposition()); 30 | 31 | setIsLoading(false); 32 | }; 33 | 34 | previewRef.current = preview; 35 | }; 36 | 37 | return ( 38 |
39 |
40 | {isLoading && 'Loading...'} 41 | 42 |
{ 45 | if (htmlElement && htmlElement !== previewRef.current?.element) { 46 | setUpPreview(htmlElement); 47 | } 48 | }} 49 | /> 50 | 51 |
52 | 61 |
62 |
63 |
64 | ); 65 | }; 66 | -------------------------------------------------------------------------------- /7-advanced-mutation/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | }; 5 | 6 | module.exports = nextConfig; 7 | -------------------------------------------------------------------------------- /7-advanced-mutation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "video-editor", 3 | "version": "1.0.0", 4 | "author": "Creatomate", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "next dev", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint" 11 | }, 12 | "dependencies": { 13 | "@creatomate/preview": "^1.5.0", 14 | "@types/node": "20.4.5", 15 | "@types/react": "18.2.16", 16 | "@types/react-dom": "18.2.7", 17 | "creatomate": "^1.1.0", 18 | "eslint": "8.45.0", 19 | "eslint-config-next": "13.4.12", 20 | "modern-normalize": "2.0.0", 21 | "next": "^13.5.6", 22 | "prettier": "3.0.0", 23 | "react": "18.2.0", 24 | "react-dom": "18.2.0", 25 | "typescript": "5.1.6" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /7-advanced-mutation/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from 'next/app' 2 | import '@/styles/globals.css' 3 | 4 | export default function App({ Component, pageProps }: AppProps) { 5 | return 6 | } 7 | -------------------------------------------------------------------------------- /7-advanced-mutation/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document'; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /7-advanced-mutation/pages/api/videos.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | 3 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 4 | res.status(200).json({}); 5 | } 6 | -------------------------------------------------------------------------------- /7-advanced-mutation/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import { Main } from '@/components/Main'; 3 | 4 | export default function Home() { 5 | return ( 6 | <> 7 | 8 | Create Next App 9 | 10 | 11 | 12 | 13 |
14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /7-advanced-mutation/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Creatomate/video-editor-tutorial/4163bdc73a29f76d27e5717fbb2b1ee72dfa4ca9/7-advanced-mutation/public/favicon.ico -------------------------------------------------------------------------------- /7-advanced-mutation/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | width: 100vw; 3 | height: 100vh; 4 | display: flex; 5 | flex-direction: column; 6 | } 7 | 8 | .center { 9 | width: 100%; 10 | height: 100%; 11 | max-width: 720px; 12 | max-height: 720px; 13 | margin: auto; 14 | display: flex; 15 | flex-direction: column; 16 | } 17 | 18 | .container { 19 | flex: 1; 20 | } 21 | 22 | .controls { 23 | display: flex; 24 | justify-content: center; 25 | } 26 | -------------------------------------------------------------------------------- /7-advanced-mutation/styles/globals.css: -------------------------------------------------------------------------------- 1 | @import '@/node_modules/modern-normalize/modern-normalize.css'; 2 | 3 | html { 4 | background-color: #e4e7eb; 5 | user-select: none; 6 | } 7 | 8 | body { 9 | font-size: 18px; 10 | line-height: 1.5; 11 | } 12 | 13 | button { 14 | padding: 10px 15px; 15 | border: none; 16 | background-color: #0065eb; 17 | border-radius: 5px; 18 | color: #fff; 19 | font-size: 16px; 20 | font-weight: 500; 21 | cursor: pointer; 22 | } 23 | 24 | input, 25 | textarea { 26 | display: block; 27 | margin: 5px 0; 28 | padding: 15px; 29 | width: 100%; 30 | border: 1px solid #b3bfcc; 31 | border-radius: 5px; 32 | outline: none; 33 | resize: none; 34 | } 35 | 36 | textarea { 37 | height: 75px; 38 | } 39 | 40 | input:focus, 41 | textarea:focus { 42 | background-color: #e9f4fc; 43 | border-color: #005aff; 44 | } 45 | 46 | select { 47 | display: block; 48 | margin: 5px 0; 49 | padding: 10px 15px; 50 | width: 100%; 51 | background-color: #fff; 52 | border: 1px solid #b3bfcc; 53 | border-radius: 5px; 54 | outline: none; 55 | 56 | /* Arrow */ 57 | appearance: none; 58 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 10'%3E%3Cpath d='m0.993 2.02 5.25 5.25c0.966 0.966 2.534 0.966 3.5-0l5.264-5.264' fill='none' stroke='%23000' stroke-width='2px'/%3E%3C/svg%3E"); 59 | background-repeat: no-repeat; 60 | background-position: right 12px top 50%; 61 | background-size: 16px auto; 62 | } 63 | 64 | select:focus { 65 | background-color: #e9f4fc; 66 | border-color: #005aff; 67 | } 68 | 69 | *::-webkit-scrollbar { 70 | width: 8px; 71 | height: 8px; 72 | } 73 | 74 | *::-webkit-scrollbar-track { 75 | background-color: #dedede; 76 | } 77 | 78 | *::-webkit-scrollbar-thumb { 79 | background-color: #c0c0c0; 80 | } 81 | 82 | *::-webkit-scrollbar-corner { 83 | background-color: rgba(0, 0, 0, 0); 84 | } 85 | -------------------------------------------------------------------------------- /7-advanced-mutation/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "paths": { 18 | "@/*": ["./*"] 19 | } 20 | }, 21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /7-advanced-mutation/utility/addSlide.ts: -------------------------------------------------------------------------------- 1 | import { Preview } from '@creatomate/preview'; 2 | import { createSlide } from '@/utility/createSlide'; 3 | import { ensureElementVisibility } from '@/utility/ensureElementVisibility'; 4 | 5 | export async function addSlide(preview: Preview) { 6 | // Get the video source 7 | // Refer to: https://creatomate.com/docs/json/introduction 8 | const source = preview.getSource(); 9 | 10 | // Delete the 'duration' and 'time' property values to make each element (Slide-1, Slide-2, etc.) autosize on the timeline 11 | delete source.duration; 12 | for (const element of source.elements) { 13 | delete element.time; 14 | } 15 | 16 | // Find the last slide element (e.g. Slide-3) 17 | const lastSlideIndex = source.elements.findLastIndex((element: any) => element.name?.startsWith('Slide-')); 18 | if (lastSlideIndex !== -1) { 19 | const slideName = `Slide-${lastSlideIndex}`; 20 | 21 | // Create a new slide 22 | const newSlideSource = createSlide(slideName, `This is the text caption for newly added slide ${lastSlideIndex}.`); 23 | 24 | // Insert the new slide 25 | source.elements.splice(lastSlideIndex + 1, 0, newSlideSource); 26 | 27 | // Update the video source 28 | await preview.setSource(source); 29 | 30 | // Jump to the time at which the text element is visible 31 | await ensureElementVisibility(preview, `${slideName}-Text`, 1.5); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /7-advanced-mutation/utility/createSlide.ts: -------------------------------------------------------------------------------- 1 | export function createSlide(slideName: string, caption: string) { 2 | // This is the JSON of a new slide. It is based on existing slides in the "Image Slideshow w/ Intro and Outro" template 3 | // Refer to: https://creatomate.com/docs/json/introduction 4 | return { 5 | name: slideName, 6 | type: 'composition', 7 | track: 1, 8 | duration: 4, 9 | animations: [ 10 | { 11 | type: 'fade', 12 | duration: 1, 13 | transition: true, 14 | }, 15 | ], 16 | elements: [ 17 | { 18 | name: `${slideName}-Image`, 19 | type: 'image', 20 | animations: [ 21 | { 22 | easing: 'linear', 23 | type: 'scale', 24 | fade: false, 25 | scope: 'element', 26 | end_scale: '130%', 27 | start_scale: '100%', 28 | }, 29 | ], 30 | source: 'https://creatomate-static.s3.amazonaws.com/demo/samuel-ferrara-1527pjeb6jg-unsplash.jpg', 31 | }, 32 | { 33 | name: `${slideName}-Text`, 34 | type: 'text', 35 | time: 0.5, 36 | duration: 3.5, 37 | y: '83.3107%', 38 | width: '70%', 39 | height: '10%', 40 | x_alignment: '50%', 41 | y_alignment: '100%', 42 | fill_color: '#ffffff', 43 | animations: [ 44 | { 45 | time: 'start', 46 | duration: 1, 47 | easing: 'quadratic-out', 48 | type: 'text-slide', 49 | scope: 'split-clip', 50 | split: 'line', 51 | direction: 'up', 52 | background_effect: 'scaling-clip', 53 | }, 54 | { 55 | easing: 'linear', 56 | type: 'scale', 57 | fade: false, 58 | scope: 'element', 59 | y_anchor: '100%', 60 | end_scale: '130%', 61 | start_scale: '100%', 62 | }, 63 | ], 64 | text: caption, 65 | font_family: 'Roboto Condensed', 66 | font_weight: '700', 67 | background_color: 'rgba(220,171,94,1)', 68 | background_x_padding: '80%', 69 | }, 70 | ], 71 | }; 72 | } 73 | -------------------------------------------------------------------------------- /7-advanced-mutation/utility/ensureElementVisibility.ts: -------------------------------------------------------------------------------- 1 | import { Preview } from '@creatomate/preview'; 2 | 3 | // Jumps to a time position where the provided element is visible 4 | export async function ensureElementVisibility(preview: Preview, elementName: string, addTime: number) { 5 | // Find element by name 6 | const element = preview.getElements().find((element) => element.source.name === elementName); 7 | if (element) { 8 | // Set playback time 9 | await preview.setTime(element.globalTime + addTime); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /7-advanced-mutation/utility/getSlideshowComposition.ts: -------------------------------------------------------------------------------- 1 | // This is the source code of "Image Slideshow w/ Intro and Outro" template under the "Storytelling" category 2 | // See: https://user-images.githubusercontent.com/44575638/227714779-31292519-3a75-40a4-8c3f-549e28100a48.jpg 3 | export function getSlideshowComposition() { 4 | return { 5 | output_format: 'mp4', 6 | width: 1280, 7 | height: 720, 8 | duration: 20.8339, 9 | snapshot_time: 7.42, 10 | fill_color: '#ffffff', 11 | elements: [ 12 | { 13 | id: '770d2703-f404-4f0f-ac74-14dcec14f0e4', 14 | type: 'composition', 15 | track: 1, 16 | time: 0, 17 | duration: 3.5, 18 | clip: true, 19 | elements: [ 20 | { 21 | id: '48734f5c-8c90-41ac-a059-e949e733936b', 22 | name: 'Main-Image', 23 | type: 'image', 24 | track: 1, 25 | time: 0, 26 | dynamic: true, 27 | color_overlay: 'rgba(0,0,0,0.25)', 28 | animations: [ 29 | { 30 | easing: 'linear', 31 | type: 'scale', 32 | fade: false, 33 | scope: 'element', 34 | end_scale: '130%', 35 | start_scale: '100%', 36 | }, 37 | ], 38 | source: 'https://creatomate.com/files/assets/5bc5ed6f-26e6-4c3a-8d03-1b169dc7f983.jpg', 39 | }, 40 | { 41 | id: '72ec46a3-610c-4b46-86ef-c9bbc337f012', 42 | name: 'Tagline', 43 | type: 'text', 44 | track: 2, 45 | time: 1, 46 | duration: 2.5, 47 | dynamic: true, 48 | y: '73.7108%', 49 | width: '69.7955%', 50 | height: '12.5699%', 51 | x_alignment: '50%', 52 | fill_color: '#ffffff', 53 | animations: [ 54 | { 55 | time: 'start', 56 | duration: 1, 57 | easing: 'quadratic-out', 58 | type: 'text-slide', 59 | scope: 'split-clip', 60 | split: 'word', 61 | direction: 'up', 62 | }, 63 | ], 64 | text: 'Enter your tagline here', 65 | font_family: 'Oswald', 66 | font_weight: '600', 67 | text_transform: 'uppercase', 68 | }, 69 | { 70 | id: '04b59bd6-b9df-439f-9586-2d5095c9f959', 71 | name: 'Title', 72 | type: 'text', 73 | track: 3, 74 | time: 0, 75 | dynamic: true, 76 | y: '41.4148%', 77 | width: '69.7955%', 78 | height: '47.6103%', 79 | x_alignment: '50%', 80 | fill_color: '#ffffff', 81 | animations: [ 82 | { 83 | time: 'start', 84 | duration: 1, 85 | easing: 'quadratic-out', 86 | type: 'text-appear', 87 | split: 'line', 88 | }, 89 | ], 90 | text: 'Lorem ipsum dolor sit amet', 91 | font_family: 'Oswald', 92 | font_weight: '600', 93 | text_transform: 'uppercase', 94 | }, 95 | ], 96 | }, 97 | { 98 | id: '8504a124-1814-4e5d-af8b-6b4573d1a618', 99 | type: 'composition', 100 | track: 1, 101 | time: 2.8, 102 | duration: 3.5, 103 | fill_color: 'rgba(231,180,52,1)', 104 | clip: true, 105 | animations: [ 106 | { 107 | time: 'start', 108 | duration: 0.7, 109 | easing: 'cubic-out', 110 | transition: true, 111 | type: 'slide', 112 | fade: false, 113 | direction: '180°', 114 | }, 115 | ], 116 | elements: [ 117 | { 118 | id: '7f7e1331-941a-4b6f-89b7-cdc5372406cf', 119 | name: 'Start-Text', 120 | type: 'text', 121 | track: 1, 122 | time: 0, 123 | dynamic: true, 124 | width: '52.2433%', 125 | height: '35.6373%', 126 | x_alignment: '50%', 127 | y_alignment: '50%', 128 | fill_color: '#ffffff', 129 | animations: [ 130 | { 131 | easing: 'linear', 132 | type: 'scale', 133 | fade: false, 134 | scope: 'element', 135 | track: 0, 136 | start_scale: '130%', 137 | }, 138 | { 139 | time: 'end', 140 | duration: 3, 141 | easing: 'quadratic-out', 142 | type: 'text-typewriter', 143 | typing_start: 0, 144 | typing_duration: 1.5, 145 | }, 146 | ], 147 | text: 'A second and longer text here ✌️', 148 | font_family: 'Oswald', 149 | font_weight: '600', 150 | text_transform: 'uppercase', 151 | }, 152 | ], 153 | }, 154 | { 155 | id: '072d2b97-9117-4848-87f5-34a390fa6f3e', 156 | name: 'Slide-1', 157 | type: 'composition', 158 | track: 1, 159 | time: 5.3, 160 | duration: 4, 161 | animations: [ 162 | { 163 | time: 'start', 164 | duration: 1, 165 | transition: true, 166 | type: 'circular-wipe', 167 | x_anchor: '0%', 168 | y_anchor: '0%', 169 | }, 170 | ], 171 | elements: [ 172 | { 173 | id: 'bf5d61e2-d850-4400-a951-293ec3c22d7d', 174 | name: 'Slide-1-Image', 175 | type: 'image', 176 | track: 1, 177 | time: 0, 178 | dynamic: true, 179 | animations: [ 180 | { 181 | easing: 'linear', 182 | type: 'scale', 183 | fade: false, 184 | scope: 'element', 185 | start_scale: '130%', 186 | }, 187 | ], 188 | source: 'https://creatomate.com/files/assets/63dfc7e7-8621-4779-b471-e4098783eaa2.jpg', 189 | }, 190 | { 191 | id: 'b57524d8-d032-4c91-9bd1-de22ba74ebe9', 192 | name: 'Slide-1-Text', 193 | type: 'text', 194 | track: 2, 195 | time: 0.5, 196 | duration: 3.5, 197 | dynamic: true, 198 | x: '4.9383%', 199 | y: '88.3107%', 200 | width: '75%', 201 | height: '10%', 202 | x_anchor: '0%', 203 | y_anchor: '100%', 204 | y_alignment: '100%', 205 | fill_color: '#ffffff', 206 | animations: [ 207 | { 208 | time: 'start', 209 | duration: 1, 210 | easing: 'quadratic-out', 211 | type: 'text-slide', 212 | scope: 'split-clip', 213 | split: 'line', 214 | background_effect: 'scaling-clip', 215 | }, 216 | { 217 | easing: 'linear', 218 | type: 'scale', 219 | fade: false, 220 | scope: 'element', 221 | x_anchor: '0%', 222 | y_anchor: '100%', 223 | end_scale: '130%', 224 | start_scale: '100%', 225 | }, 226 | ], 227 | text: 'Enter a text for the first slide. ⛱️', 228 | font_family: 'Roboto Condensed', 229 | font_weight: '700', 230 | background_color: 'rgba(220,171,94,1)', 231 | background_x_padding: '80%', 232 | }, 233 | ], 234 | }, 235 | { 236 | id: 'b18841ca-11f3-4b9f-997d-634e462ce42b', 237 | name: 'Slide-2', 238 | type: 'composition', 239 | track: 1, 240 | time: 8.3, 241 | duration: 4, 242 | animations: [ 243 | { 244 | time: 0, 245 | duration: 1, 246 | transition: true, 247 | type: 'fade', 248 | }, 249 | ], 250 | elements: [ 251 | { 252 | id: '0184e92a-1ab8-4b82-b130-bc7f43736f7c', 253 | name: 'Slide-2-Image', 254 | type: 'image', 255 | track: 1, 256 | time: 0, 257 | dynamic: true, 258 | animations: [ 259 | { 260 | easing: 'linear', 261 | type: 'scale', 262 | fade: false, 263 | scope: 'element', 264 | track: 0, 265 | end_scale: '130%', 266 | start_scale: '100%', 267 | }, 268 | ], 269 | source: 'https://creatomate.com/files/assets/5e62bfc9-060a-4a27-aba0-aecdc49215b7.jpg', 270 | }, 271 | { 272 | id: 'ea2eb10d-3618-4c54-af51-da5f5dace080', 273 | name: 'Slide-2-Text', 274 | type: 'text', 275 | track: 2, 276 | time: 0.5, 277 | duration: 3.5, 278 | dynamic: true, 279 | y: '83.3107%', 280 | width: '70%', 281 | height: '10%', 282 | x_alignment: '50%', 283 | y_alignment: '100%', 284 | fill_color: '#ffffff', 285 | animations: [ 286 | { 287 | time: 'start', 288 | duration: 1, 289 | easing: 'quadratic-out', 290 | type: 'text-slide', 291 | scope: 'split-clip', 292 | split: 'line', 293 | direction: 'up', 294 | background_effect: 'scaling-clip', 295 | }, 296 | { 297 | easing: 'linear', 298 | type: 'scale', 299 | fade: false, 300 | scope: 'element', 301 | y_anchor: '100%', 302 | end_scale: '130%', 303 | start_scale: '100%', 304 | }, 305 | ], 306 | text: 'Enter a text for the second slide. 🌊', 307 | font_family: 'Roboto Condensed', 308 | font_weight: '700', 309 | background_color: 'rgba(220,171,94,1)', 310 | background_x_padding: '80%', 311 | }, 312 | ], 313 | }, 314 | { 315 | id: '347d5c69-6667-4e8c-9275-508af70bf5bd', 316 | name: 'Slide-3', 317 | type: 'composition', 318 | track: 1, 319 | time: 11.3, 320 | duration: 4, 321 | animations: [ 322 | { 323 | time: 0, 324 | duration: 1, 325 | transition: true, 326 | type: 'fade', 327 | }, 328 | ], 329 | elements: [ 330 | { 331 | id: '9d8e0fe9-e4db-47f1-ad81-9e033bb7c6cf', 332 | name: 'Slide-3-Image', 333 | type: 'image', 334 | track: 1, 335 | time: 0, 336 | dynamic: true, 337 | animations: [ 338 | { 339 | easing: 'linear', 340 | transition: true, 341 | type: 'scale', 342 | fade: false, 343 | scope: 'element', 344 | start_scale: '130%', 345 | }, 346 | ], 347 | source: 'https://creatomate.com/files/assets/0ae5625b-8c8d-498c-9f35-fb50797efbd1.jpg', 348 | }, 349 | { 350 | id: '04c19c44-e718-4dca-bb5f-a6b2b940b74c', 351 | name: 'Slide-3-Text', 352 | type: 'text', 353 | track: 2, 354 | time: 0.5, 355 | duration: 3.5, 356 | x: '95.0617%', 357 | y: '88.3107%', 358 | width: '70%', 359 | height: '10%', 360 | x_anchor: '100%', 361 | y_anchor: '100%', 362 | x_alignment: '100%', 363 | y_alignment: '100%', 364 | fill_color: '#ffffff', 365 | animations: [ 366 | { 367 | time: 'start', 368 | duration: 1, 369 | easing: 'quadratic-out', 370 | type: 'text-slide', 371 | scope: 'split-clip', 372 | split: 'line', 373 | direction: 'left', 374 | background_effect: 'scaling-clip', 375 | }, 376 | { 377 | easing: 'linear', 378 | type: 'scale', 379 | fade: false, 380 | scope: 'element', 381 | x_anchor: '100%', 382 | y_anchor: '100%', 383 | end_scale: '130%', 384 | start_scale: '100%', 385 | }, 386 | ], 387 | text: 'Enter a text for the third slide. 🌴', 388 | font_family: 'Roboto Condensed', 389 | font_weight: '700', 390 | background_color: 'rgba(220,171,94,1)', 391 | background_x_padding: '80%', 392 | }, 393 | ], 394 | }, 395 | { 396 | id: 'f114638f-0676-4825-b040-d55a751e1d30', 397 | type: 'composition', 398 | track: 1, 399 | time: 14.3, 400 | duration: 4, 401 | fill_color: 'rgba(75,175,201,1)', 402 | animations: [ 403 | { 404 | time: 'start', 405 | duration: 1, 406 | transition: true, 407 | type: 'circular-wipe', 408 | x_anchor: '100%', 409 | y_anchor: '100%', 410 | }, 411 | ], 412 | elements: [ 413 | { 414 | id: 'bd29910d-fc10-463e-af94-e70f082025fe', 415 | name: 'Final-Text', 416 | type: 'text', 417 | track: 1, 418 | time: 0, 419 | dynamic: true, 420 | width: '69.7955%', 421 | height: '47.6103%', 422 | x_alignment: '50%', 423 | y_alignment: '50%', 424 | fill_color: '#ffffff', 425 | animations: [ 426 | { 427 | time: 0.5, 428 | duration: 1.5009, 429 | easing: 'quadratic-out', 430 | type: 'text-spin', 431 | split: 'letter', 432 | x_anchor: '100%', 433 | y_anchor: '100%', 434 | direction: 'left', 435 | }, 436 | ], 437 | text: 'Your Call To Action Here', 438 | font_family: 'Oswald', 439 | font_weight: '600', 440 | text_transform: 'uppercase', 441 | }, 442 | ], 443 | }, 444 | { 445 | id: '27f3241f-c6a1-4b29-9c6f-1537dda720f9', 446 | type: 'composition', 447 | track: 1, 448 | time: 17.3339, 449 | duration: 3.5, 450 | animations: [ 451 | { 452 | time: 'start', 453 | duration: 0.9661, 454 | easing: 'back-out', 455 | transition: true, 456 | type: 'slide', 457 | fade: false, 458 | direction: '180°', 459 | }, 460 | ], 461 | elements: [ 462 | { 463 | id: 'def71fed-c745-4f97-a9b9-7f4280b039ce', 464 | type: 'shape', 465 | track: 1, 466 | time: 0, 467 | x: '30.6505%', 468 | width: '7.4114%', 469 | height: '14.4045%', 470 | aspect_ratio: 0.8134, 471 | x_anchor: '50%', 472 | y_anchor: '50%', 473 | fill_color: '#000000', 474 | path: 'M 56.4198 14.5533 L 56.4198 0 L 0 0 L 0 100 L 56.4198 100 L 56.4198 85.4467 C 44.8629 85.4467 33.7773 81.71 25.6055 75.0633 C 17.4337 68.4167 12.8437 59.4 12.8437 50 C 12.8437 40.6 17.4337 31.5833 25.6055 24.9367 C 33.7773 18.29 44.8629 14.5533 56.4198 14.5533 Z M 56.4198 14.5533 L 56.4198 85.4467 C 67.9767 85.4467 79.0623 81.71 87.2341 75.0633 C 95.4059 68.4167 100 59.4 100 50 C 100 40.6 95.4059 31.5833 87.2341 24.9367 C 79.0623 18.29 67.9767 14.5533 56.4198 14.5533 Z', 475 | }, 476 | { 477 | id: 'c2edc7b3-db26-474e-aac7-3ab72dcce3c7', 478 | type: 'text', 479 | track: 2, 480 | time: 0.5, 481 | x: '54.3111%', 482 | width: '37.4883%', 483 | height: '14.4045%', 484 | x_alignment: '50%', 485 | y_alignment: '50%', 486 | animations: [ 487 | { 488 | time: 'start', 489 | duration: 0.9, 490 | easing: 'quadratic-out', 491 | type: 'text-slide', 492 | scope: 'split-clip', 493 | split: 'line', 494 | distance: '200%', 495 | }, 496 | ], 497 | text: 'Your Logo', 498 | font_family: 'Roboto', 499 | font_weight: '700', 500 | letter_spacing: '-19%', 501 | line_height: '96%', 502 | text_wrap: false, 503 | }, 504 | ], 505 | }, 506 | ], 507 | }; 508 | } 509 | -------------------------------------------------------------------------------- /8-final-project/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /8-final-project/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | 37 | # JetBrains editors 38 | /.idea 39 | -------------------------------------------------------------------------------- /8-final-project/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "trailingComma": "all", 4 | "tabWidth": 2, 5 | "semi": true, 6 | "singleQuote": true 7 | } 8 | -------------------------------------------------------------------------------- /8-final-project/README.md: -------------------------------------------------------------------------------- 1 | # Video Preview Demo 2 | 3 | Add video rendering to your web apps! Seamlessly integrate our video renderer into your software and provide your users with video editing functionality – right in the browser. 4 | 5 | This is a demo application showing how a dynamic video can be previewed in the browser using the [Preview SDK](https://creatomate.com/javascript-video-sdk). The code can be used as a basis for creating your own video editor applications using Creatomate's API. 6 | 7 | ## Demo 8 | 9 | Try it out live: https://video-preview-demo.vercel.app 10 | 11 | The **Create Video** button is disabled in the live demo as this requires an API key. To run the example with your own API key, follow the instructions below. 12 | 13 | ## Usage 14 | 15 | ### Running this demo application 16 | 17 | This demo uses a video template from your account. The example code demonstrates a few features that require a specific template, so be sure to follow the instructions carefully: 18 | 19 | 1. Create a free account [here](https://creatomate.com/sign-in). 20 | 2. Go to your project settings, and copy your **API Key** and **Public Token** under *Programmatic Access*:

![Screenshot](https://user-images.githubusercontent.com/44575638/227715496-5ae23468-c047-4ab8-beb2-e21b6c65d74b.png)

21 | 3. In your dashboard, go to **My Templates**, click **New**, go to the **Storytelling** category, and choose the **"Image Slideshow w/ Intro and Outro"** template, then click **Create Template**:

![Screenshot](https://user-images.githubusercontent.com/44575638/227714779-31292519-3a75-40a4-8c3f-549e28100a48.jpg)

22 | 4. From the address bar, copy the ID of the newly created template:

![Screenshot](https://user-images.githubusercontent.com/44575638/227736758-f9d522c3-3bbb-4b7b-92c7-e004e9dc16e5.png)

23 | 5. Create a new file called `.env.local` in the root of the project, providing the **API Key**, **Public Token**, and **Template ID** from the previous steps: 24 | 25 | ``` 26 | CREATOMATE_API_KEY=... 27 | NEXT_PUBLIC_VIDEO_PLAYER_TOKEN=... 28 | NEXT_PUBLIC_TEMPLATE_ID=... 29 | ``` 30 | 31 | 6. Install all NPM dependencies using the following command: 32 | 33 | ```bash 34 | npm install 35 | ``` 36 | 37 | 7. The demo can then be run using: 38 | 39 | ```bash 40 | npm run dev 41 | ``` 42 | 43 | 8. Now visit the URL that is displayed in your console, which is by default: `http://localhost:3000` 44 | 45 | ### Using this code in your own projects 46 | 47 | Install the Preview SDK using the following command: 48 | 49 | ```bash 50 | npm install @creatomate/preview 51 | ``` 52 | 53 | Please refer to [App.tsx](https://github.com/Creatomate/video-preview-demo/blob/main/components/App.tsx) to see an example of how to initialize the SDK. 54 | 55 | ## Issues & Comments 56 | 57 | Feel free to contact us if you encounter any issues with this demo or Creatomate API at [support@creatomate.com](mailto:support@creatomate.com). 58 | 59 | ## License 60 | 61 | This demo is licensed under the MIT license. Please refer to the [LICENSE](https://github.com/Creatomate/video-preview-demo/blob/main/LICENSE) for more information. 62 | -------------------------------------------------------------------------------- /8-final-project/components/CreateButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Preview } from '@creatomate/preview'; 3 | import { finishVideo } from '@/utility/finishVideo'; 4 | import styles from '@/styles/Home.module.css'; 5 | 6 | interface CreateButtonProps { 7 | preview: Preview; 8 | } 9 | 10 | export const CreateButton: React.FC = (props) => { 11 | const [isRendering, setIsRendering] = useState(false); 12 | const [render, setRender] = useState(); 13 | 14 | if (isRendering) { 15 | return ( 16 | 19 | ); 20 | } 21 | 22 | if (render) { 23 | return ( 24 | 34 | ); 35 | } 36 | 37 | return ( 38 | 60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /8-final-project/components/Main.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react'; 2 | import { Inter } from 'next/font/google'; 3 | import { Preview, PreviewState } from '@creatomate/preview'; 4 | import { SettingsPanel } from '@/components/SettingsPanel'; 5 | import { useWindowWidth } from '@/utility/useWindowWidth'; 6 | import styles from '@/styles/Home.module.css'; 7 | 8 | const inter = Inter({ subsets: ['latin'] }); 9 | 10 | export const Main: React.FC = () => { 11 | // React Hook to update the component when the window width changes 12 | const windowWidth = useWindowWidth(); 13 | 14 | // Video aspect ratio that can be calculated once the video is loaded 15 | const [videoAspectRatio, setVideoAspectRatio] = useState(); 16 | 17 | // Reference to the preview 18 | const previewRef = useRef(); 19 | 20 | // Current state of the preview 21 | const [isReady, setIsReady] = useState(false); 22 | const [isLoading, setIsLoading] = useState(true); 23 | const [currentState, setCurrentState] = useState(); 24 | 25 | // This sets up the video player in the provided HTML DIV element 26 | const setUpPreview = (htmlElement: HTMLDivElement) => { 27 | if (previewRef.current) { 28 | previewRef.current.dispose(); 29 | previewRef.current = undefined; 30 | } 31 | 32 | // Initialize a preview 33 | const preview = new Preview(htmlElement, 'player', process.env.NEXT_PUBLIC_VIDEO_PLAYER_TOKEN!); 34 | 35 | // Once the SDK is ready, load a template from our project 36 | preview.onReady = async () => { 37 | await preview.loadTemplate(process.env.NEXT_PUBLIC_TEMPLATE_ID!); 38 | setIsReady(true); 39 | }; 40 | 41 | preview.onLoad = () => { 42 | setIsLoading(true); 43 | }; 44 | 45 | preview.onLoadComplete = () => { 46 | setIsLoading(false); 47 | }; 48 | 49 | // Listen for state changes of the preview 50 | preview.onStateChange = (state) => { 51 | setCurrentState(state); 52 | setVideoAspectRatio(state.width / state.height); 53 | }; 54 | 55 | previewRef.current = preview; 56 | }; 57 | 58 | return ( 59 |
60 |
61 |
{ 64 | if (htmlElement && htmlElement !== previewRef.current?.element) { 65 | setUpPreview(htmlElement); 66 | } 67 | }} 68 | style={{ 69 | height: 70 | videoAspectRatio && windowWidth && windowWidth < 768 ? window.innerWidth / videoAspectRatio : undefined, 71 | }} 72 | /> 73 |
74 | 75 |
76 | {isReady && ( 77 |
78 | 79 |
80 | )} 81 |
82 | 83 | {isLoading &&
Loading...
} 84 |
85 | ); 86 | }; 87 | -------------------------------------------------------------------------------- /8-final-project/components/SettingsPanel.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useMemo, useRef } from 'react'; 2 | import { Preview, PreviewState } from '@creatomate/preview'; 3 | import { CreateButton } from '@/components/CreateButton'; 4 | import styles from '@/styles/Home.module.css'; 5 | import { setPropertyValue } from '@/utility/setPropertyValue'; 6 | import { setTextStyle } from '@/utility/setTextStyle'; 7 | import { addSlide } from '@/utility/addSlide'; 8 | import { setSlideTransition } from '@/utility/setSlideTransition'; 9 | import { ensureElementVisibility } from '@/utility/ensureElementVisibility'; 10 | 11 | interface SettingsPanelProps { 12 | preview: Preview; 13 | currentState?: PreviewState; 14 | } 15 | 16 | export const SettingsPanel: React.FC = (props) => { 17 | // In this variable, we store the modifications that are applied to the template 18 | // Refer to: https://creatomate.com/docs/api/rest-api/the-modifications-object 19 | const modificationsRef = useRef>({}); 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 |