@revideo/create 0.5.10-alpha.1087 → 0.5.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/examples/avatar-with-background/package.json +8 -8
- package/examples/avatar-with-background/src/scenes/example.tsx +1 -0
- package/examples/default/package.json +8 -8
- package/examples/github-stars-celebration/package.json +7 -7
- package/examples/github-stars-celebration/src/scenes/example.tsx +2 -2
- package/examples/google-cloud-run-parallelized/render-orchestrator/package.json +21 -21
- package/examples/google-cloud-run-parallelized/render-worker/package.json +8 -8
- package/examples/marketing-templates/marketing-template/package.json +7 -7
- package/examples/marketing-templates/multiple-videos-in-one/package.json +7 -7
- package/examples/minimal-drag-drop/README.md +27 -0
- package/examples/minimal-drag-drop/next/app/api/render/route.ts +22 -0
- package/examples/minimal-drag-drop/next/app/globals.css +3 -0
- package/examples/minimal-drag-drop/next/app/layout.tsx +25 -0
- package/examples/minimal-drag-drop/next/app/page.tsx +277 -0
- package/examples/minimal-drag-drop/next/next-env.d.ts +5 -0
- package/examples/minimal-drag-drop/next/next.config.mjs +4 -0
- package/examples/minimal-drag-drop/next/package-lock.json +5577 -0
- package/examples/minimal-drag-drop/next/package.json +35 -0
- package/examples/minimal-drag-drop/next/postcss.config.js +6 -0
- package/examples/minimal-drag-drop/next/public/favicon.ico +0 -0
- package/examples/minimal-drag-drop/next/public/next.svg +1 -0
- package/examples/minimal-drag-drop/next/public/vercel.svg +1 -0
- package/examples/minimal-drag-drop/next/tailwind.config.ts +30 -0
- package/examples/minimal-drag-drop/next/tsconfig.json +26 -0
- package/examples/minimal-drag-drop/next/utils/parse.ts +35 -0
- package/examples/minimal-drag-drop/revideo/package-lock.json +3771 -0
- package/examples/minimal-drag-drop/revideo/package.json +24 -0
- package/examples/minimal-drag-drop/revideo/src/global.css +1 -0
- package/examples/minimal-drag-drop/revideo/src/project.meta +28 -0
- package/examples/minimal-drag-drop/revideo/src/project.ts +9 -0
- package/examples/minimal-drag-drop/revideo/src/render.ts +15 -0
- package/examples/minimal-drag-drop/revideo/src/revideo.d.ts +1 -0
- package/examples/minimal-drag-drop/revideo/src/scenes/example.meta +5 -0
- package/examples/minimal-drag-drop/revideo/src/scenes/example.tsx +44 -0
- package/examples/minimal-drag-drop/revideo/tsconfig.json +9 -0
- package/examples/minimal-drag-drop/revideo/vite.config.ts +6 -0
- package/examples/parallelized-aws-lambda/revideo-project/package.json +8 -11
- package/examples/parallelized-aws-lambda/revideo-project/src/lambda.ts +15 -2
- package/examples/parallelized-aws-lambda/revideo-project/src/render.ts +2 -1
- package/examples/parallelized-aws-lambda/revideo-project/src/scenes/example.tsx +1 -0
- package/examples/reddit-post-video/package.json +7 -7
- package/examples/reddit-post-video/src/project.meta +2 -2
- package/examples/rive-explanation-video/package.json +8 -8
- package/examples/saas-template/next/package.json +33 -33
- package/examples/saas-template/revideo/package.json +23 -23
- package/examples/stitching-videos/package.json +6 -6
- package/examples/stitching-videos/src/render.ts +2 -1
- package/examples/three-js-example/package-lock.json +41 -41
- package/examples/three-js-example/package.json +7 -7
- package/examples/youtube-shorts/README.md +7 -7
- package/examples/youtube-shorts/package-lock.json +332 -365
- package/examples/youtube-shorts/package.json +8 -8
- package/examples/youtube-shorts/src/get-assets.ts +34 -0
- package/examples/youtube-shorts/src/render.ts +4 -35
- package/examples/youtube-shorts/src/scenes/example.tsx +25 -12
- package/index.js +122 -16
- package/package.json +9 -9
- package/examples/avatar-with-background/package-lock.json +0 -3223
- package/examples/marketing-templates/marketing-template/package-lock.json +0 -3824
- package/examples/marketing-templates/multiple-videos-in-one/package-lock.json +0 -3824
- package/examples/rive-explanation-video/package-lock.json +0 -3706
- package/examples/stitching-videos/package-lock.json +0 -2801
|
@@ -10,18 +10,18 @@
|
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@deepgram/sdk": "^3.3.0",
|
|
13
|
-
"@revideo/2d": "0.
|
|
14
|
-
"@revideo/cli": "
|
|
15
|
-
"@revideo/core": "0.
|
|
16
|
-
"@revideo/ffmpeg": "0.
|
|
17
|
-
"@revideo/renderer": "0.
|
|
18
|
-
"@revideo/vite-plugin": "0.
|
|
13
|
+
"@revideo/2d": "0.5.10",
|
|
14
|
+
"@revideo/cli": "0.5.10",
|
|
15
|
+
"@revideo/core": "0.5.10",
|
|
16
|
+
"@revideo/ffmpeg": "0.5.10",
|
|
17
|
+
"@revideo/renderer": "0.5.10",
|
|
18
|
+
"@revideo/vite-plugin": "0.5.10",
|
|
19
19
|
"dotenv": "^16.4.5",
|
|
20
20
|
"openai": "^4.39.0"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@revideo/ui": "0.
|
|
23
|
+
"@revideo/ui": "0.5.10",
|
|
24
24
|
"typescript": "5.5.4",
|
|
25
25
|
"vite": "^4.5"
|
|
26
26
|
}
|
|
27
|
-
}
|
|
27
|
+
}
|
|
@@ -9,16 +9,16 @@
|
|
|
9
9
|
"render": "tsc && node dist/render.js"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@revideo/core": "
|
|
13
|
-
"@revideo/2d": "
|
|
14
|
-
"@revideo/renderer": "
|
|
15
|
-
"@revideo/vite-plugin": "
|
|
16
|
-
"@revideo/ffmpeg": "
|
|
12
|
+
"@revideo/core": "0.5.10",
|
|
13
|
+
"@revideo/2d": "0.5.10",
|
|
14
|
+
"@revideo/renderer": "0.5.10",
|
|
15
|
+
"@revideo/vite-plugin": "0.5.10",
|
|
16
|
+
"@revideo/ffmpeg": "0.5.10"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
|
-
"@revideo/ui": "
|
|
20
|
-
"@revideo/cli": "
|
|
19
|
+
"@revideo/ui": "0.5.10",
|
|
20
|
+
"@revideo/cli": "0.5.10",
|
|
21
21
|
"typescript": "5.5.4",
|
|
22
22
|
"vite": "^4.5"
|
|
23
23
|
}
|
|
24
|
-
}
|
|
24
|
+
}
|
|
@@ -9,15 +9,15 @@
|
|
|
9
9
|
"render": "tsc && node dist/render.js"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@revideo/core": "
|
|
13
|
-
"@revideo/2d": "
|
|
14
|
-
"@revideo/renderer": "
|
|
15
|
-
"@revideo/vite-plugin": "
|
|
16
|
-
"@revideo/ffmpeg": "
|
|
12
|
+
"@revideo/core": "0.5.10",
|
|
13
|
+
"@revideo/2d": "0.5.10",
|
|
14
|
+
"@revideo/renderer": "0.5.10",
|
|
15
|
+
"@revideo/vite-plugin": "0.5.10",
|
|
16
|
+
"@revideo/ffmpeg": "0.5.10"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
|
-
"@revideo/ui": "
|
|
20
|
-
"@revideo/cli": "
|
|
19
|
+
"@revideo/ui": "0.5.10",
|
|
20
|
+
"@revideo/cli": "0.5.10",
|
|
21
21
|
"typescript": "5.5.4",
|
|
22
22
|
"vite": "^4.5"
|
|
23
23
|
}
|
|
@@ -29,13 +29,13 @@ export default makeScene2D(function* (view) {
|
|
|
29
29
|
|
|
30
30
|
yield* rocketRef().position([1000, -600], 2, easeInCubic);
|
|
31
31
|
|
|
32
|
-
view.add(
|
|
32
|
+
yield view.add(
|
|
33
33
|
<Txt fontFamily={"Lexend"} fill="white" ref={txtRef} textAlign={"center"} fontSize={60}></Txt>
|
|
34
34
|
)
|
|
35
35
|
|
|
36
36
|
yield* txtRef().text("Thank you for 1,000 Github stars!", 2);
|
|
37
37
|
|
|
38
|
-
view.add(
|
|
38
|
+
yield view.add(
|
|
39
39
|
<Img
|
|
40
40
|
width={'1%'}
|
|
41
41
|
ref={logoRef}
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
2
|
+
"name": "google-cloud-run",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"start": "tsc && node dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"@google-cloud/storage": "^7.11.0",
|
|
10
|
+
"@revideo/2d": "0.5.10",
|
|
11
|
+
"@revideo/core": "0.5.10",
|
|
12
|
+
"@revideo/ffmpeg": "0.5.10",
|
|
13
|
+
"express": "^4.19.2",
|
|
14
|
+
"uuid": "^9.0.1"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/express": "^4.17.21",
|
|
18
|
+
"@types/uuid": "^9.0.8",
|
|
19
|
+
"typescript": "^5.4.5",
|
|
20
|
+
"vite": "^4.5"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -12,13 +12,13 @@
|
|
|
12
12
|
"@ffprobe-installer/ffprobe": "^2.1.2",
|
|
13
13
|
"@google-cloud/functions-framework": "^3.4.0",
|
|
14
14
|
"@google-cloud/storage": "^7.11.0",
|
|
15
|
-
"@revideo/2d": "0.
|
|
16
|
-
"@revideo/cli": "
|
|
17
|
-
"@revideo/core": "0.
|
|
18
|
-
"@revideo/ffmpeg": "0.
|
|
19
|
-
"@revideo/renderer": "0.
|
|
20
|
-
"@revideo/ui": "0.
|
|
21
|
-
"@revideo/vite-plugin": "0.
|
|
15
|
+
"@revideo/2d": "0.5.10",
|
|
16
|
+
"@revideo/cli": "0.5.10",
|
|
17
|
+
"@revideo/core": "0.5.10",
|
|
18
|
+
"@revideo/ffmpeg": "0.5.10",
|
|
19
|
+
"@revideo/renderer": "0.5.10",
|
|
20
|
+
"@revideo/ui": "0.5.10",
|
|
21
|
+
"@revideo/vite-plugin": "0.5.10",
|
|
22
22
|
"@types/express": "^4.17.21",
|
|
23
23
|
"@types/uuid": "^9.0.8",
|
|
24
24
|
"express": "^4.19.2",
|
|
@@ -27,4 +27,4 @@
|
|
|
27
27
|
"uuid": "^9.0.1",
|
|
28
28
|
"vite": "^4.5"
|
|
29
29
|
}
|
|
30
|
-
}
|
|
30
|
+
}
|
|
@@ -9,15 +9,15 @@
|
|
|
9
9
|
"render": "tsc && node dist/render.js"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@revideo/core": "0.
|
|
13
|
-
"@revideo/2d": "0.
|
|
14
|
-
"@revideo/renderer": "0.
|
|
15
|
-
"@revideo/vite-plugin": "0.
|
|
16
|
-
"@revideo/ffmpeg": "0.
|
|
12
|
+
"@revideo/core": "0.5.10",
|
|
13
|
+
"@revideo/2d": "0.5.10",
|
|
14
|
+
"@revideo/renderer": "0.5.10",
|
|
15
|
+
"@revideo/vite-plugin": "0.5.10",
|
|
16
|
+
"@revideo/ffmpeg": "0.5.10"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
|
-
"@revideo/ui": "0.
|
|
20
|
-
"@revideo/cli": "
|
|
19
|
+
"@revideo/ui": "0.5.10",
|
|
20
|
+
"@revideo/cli": "0.5.10",
|
|
21
21
|
"typescript": "5.5.4",
|
|
22
22
|
"vite": "^4.5"
|
|
23
23
|
}
|
|
@@ -9,15 +9,15 @@
|
|
|
9
9
|
"render": "tsc && node dist/render.js"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@revideo/core": "0.
|
|
13
|
-
"@revideo/2d": "0.
|
|
14
|
-
"@revideo/renderer": "0.
|
|
15
|
-
"@revideo/vite-plugin": "0.
|
|
16
|
-
"@revideo/ffmpeg": "0.
|
|
12
|
+
"@revideo/core": "0.5.10",
|
|
13
|
+
"@revideo/2d": "0.5.10",
|
|
14
|
+
"@revideo/renderer": "0.5.10",
|
|
15
|
+
"@revideo/vite-plugin": "0.5.10",
|
|
16
|
+
"@revideo/ffmpeg": "0.5.10"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
|
-
"@revideo/ui": "0.
|
|
20
|
-
"@revideo/cli": "
|
|
19
|
+
"@revideo/ui": "0.5.10",
|
|
20
|
+
"@revideo/cli": "0.5.10",
|
|
21
21
|
"typescript": "5.5.4",
|
|
22
22
|
"vite": "^4.5"
|
|
23
23
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Minimal Drag-and-Drop Example
|
|
2
|
+
|
|
3
|
+
This is an adapted version of the [Revideo](https://github.com/redotvideo/revideo) [NextJS SaaS template](https://github.com/redotvideo/examples/saas-template), demonstrating how you can build drag-and-drop editing functionality with Revideo.
|
|
4
|
+
|
|
5
|
+
## Getting Started
|
|
6
|
+
|
|
7
|
+
First, install the dependencies both in the next/ and the revideo/ directories:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
(cd next && npm install) &&
|
|
11
|
+
(cd revideo && npm install)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Then, serve the Revideo project using the CLI:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
(cd revideo && npx revideo serve)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Now you can start your NextJS app using the following command:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
(cd next && npm run dev)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Open [http://localhost:3000](http://localhost:3000) with your browser to see the
|
|
27
|
+
result. Happy developing!
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const RENDER_URL = 'http://localhost:4000/render';
|
|
2
|
+
|
|
3
|
+
async function getResponse(body: string) {
|
|
4
|
+
return await fetch(RENDER_URL, {
|
|
5
|
+
method: 'POST',
|
|
6
|
+
headers: {
|
|
7
|
+
'Content-Type': 'application/json',
|
|
8
|
+
},
|
|
9
|
+
body,
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function POST(request: Request) {
|
|
14
|
+
const body = await request.json();
|
|
15
|
+
|
|
16
|
+
const response = await getResponse(JSON.stringify(body));
|
|
17
|
+
if (!response.ok) {
|
|
18
|
+
return new Response('Failed to render', {status: 500});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return new Response(response.body, {status: 200});
|
|
22
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type {Metadata} from 'next';
|
|
2
|
+
import {Inter} from 'next/font/google';
|
|
3
|
+
import './globals.css';
|
|
4
|
+
|
|
5
|
+
const inter = Inter({subsets: ['latin']});
|
|
6
|
+
|
|
7
|
+
export const metadata: Metadata = {
|
|
8
|
+
title: 'Create Next App',
|
|
9
|
+
description: 'Generated by create next app',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default function RootLayout({
|
|
13
|
+
children,
|
|
14
|
+
}: Readonly<{
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
}>) {
|
|
17
|
+
return (
|
|
18
|
+
<html lang="en">
|
|
19
|
+
<head>
|
|
20
|
+
<link rel="stylesheet" href="http://localhost:4000/player/project.css" />
|
|
21
|
+
</head>
|
|
22
|
+
<body className={inter.className}>{children}</body>
|
|
23
|
+
</html>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {Player} from '@revideo/player-react';
|
|
4
|
+
import {useState, useEffect} from 'react';
|
|
5
|
+
import {LoaderCircle} from 'lucide-react';
|
|
6
|
+
import { parseStream } from '../utils/parse';
|
|
7
|
+
import {Player as CorePlayer} from '@revideo/core';
|
|
8
|
+
import {Scene2D, Shape} from '@revideo/2d';
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
function Button({
|
|
12
|
+
children,
|
|
13
|
+
loading,
|
|
14
|
+
onClick,
|
|
15
|
+
}: {
|
|
16
|
+
children: React.ReactNode;
|
|
17
|
+
loading: boolean;
|
|
18
|
+
onClick: () => void;
|
|
19
|
+
}) {
|
|
20
|
+
return (
|
|
21
|
+
<button
|
|
22
|
+
className="text-sm flex items-center gap-x-2 rounded-md p-2 bg-gray-200 text-gray-700 hover:bg-gray-300"
|
|
23
|
+
onClick={() => onClick()}
|
|
24
|
+
disabled={loading}
|
|
25
|
+
>
|
|
26
|
+
{loading && <LoaderCircle className="animate-spin h-4 w-4 text-gray-700" />}
|
|
27
|
+
{children}
|
|
28
|
+
</button>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default function Home() {
|
|
33
|
+
const [nodes, setNodes] = useState<Map<string, any>>(new Map([
|
|
34
|
+
['txt', { text: "Text 1", x: 0, y: 0 }]
|
|
35
|
+
]));
|
|
36
|
+
const [draggingNode, setDraggingNode] = useState<Shape | null>(null);
|
|
37
|
+
const [dragOffset, setDragOffset] = useState<{ x: number, y: number } | null>(null);
|
|
38
|
+
const [playerRect, setPlayerRect] = useState<DOMRect | null>(null);
|
|
39
|
+
const [corePlayer, setCorePlayer] = useState<CorePlayer | null>(null);
|
|
40
|
+
const [transformMatrix, setTransformMatrix] = useState<DOMMatrix | null>(null);
|
|
41
|
+
|
|
42
|
+
const [renderLoading, setRenderLoading] = useState(false);
|
|
43
|
+
const [progress, setProgress] = useState(0);
|
|
44
|
+
const [downloadUrl, setDownloadUrl] = useState<string | null>(null);
|
|
45
|
+
|
|
46
|
+
const [playing, setPlaying] = useState<boolean>(false);
|
|
47
|
+
const [dimensions, setDimensions] = useState<{x: number, y: number}>({ x: 1920, y: 1080 });
|
|
48
|
+
const [selectedNode, setSelectedNode] = useState<{
|
|
49
|
+
key: string;
|
|
50
|
+
x: number;
|
|
51
|
+
y: number;
|
|
52
|
+
width: number;
|
|
53
|
+
height: number;
|
|
54
|
+
} | null>(null);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (playerRect) {
|
|
58
|
+
setTransformMatrix(createTransformationMatrix(playerRect, dimensions));
|
|
59
|
+
}
|
|
60
|
+
}, [playerRect, dimensions]);
|
|
61
|
+
|
|
62
|
+
const handlePlayerReady = (player: CorePlayer) => {
|
|
63
|
+
setCorePlayer(player);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const addTextNode = () => {
|
|
67
|
+
setNodes(prevNodes => {
|
|
68
|
+
const newNodes = new Map(prevNodes);
|
|
69
|
+
const newKey = `txt_${newNodes.size+1}`;
|
|
70
|
+
newNodes.set(newKey, { text: `Text ${newNodes.size+1}`, x: 0, y: 0 });
|
|
71
|
+
return newNodes;
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const handlePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
|
|
76
|
+
if (playerRect && transformMatrix) {
|
|
77
|
+
const { x: playerX, y: playerY } = transformPoint(
|
|
78
|
+
event.clientX,
|
|
79
|
+
event.clientY,
|
|
80
|
+
transformMatrix
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
let hitNode = (corePlayer?.playback.currentScene as Scene2D).getNodeByPosition(playerX, playerY) as Shape;
|
|
84
|
+
while (hitNode) {
|
|
85
|
+
// We want to avoid getting TxtLeaf nodes, but only their parent Txt nodes
|
|
86
|
+
if (nodes.has(hitNode.key)) {
|
|
87
|
+
setDraggingNode(hitNode);
|
|
88
|
+
const node = nodes.get(hitNode.key);
|
|
89
|
+
setDragOffset({ x: playerX - node.x, y: playerY - node.y });
|
|
90
|
+
|
|
91
|
+
const position = hitNode.position();
|
|
92
|
+
const { x, y } = transformPoint(position.x+dimensions.x/2, position.y+dimensions.y/2, transformMatrix.inverse());
|
|
93
|
+
|
|
94
|
+
setSelectedNode({
|
|
95
|
+
key: hitNode.key,
|
|
96
|
+
x: x,
|
|
97
|
+
y: y,
|
|
98
|
+
width: selectedNode?.width || hitNode.width() / transformMatrix.a,
|
|
99
|
+
height: selectedNode?.height || hitNode.height() / transformMatrix.d
|
|
100
|
+
});
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
hitNode = hitNode.parent() as Shape;
|
|
104
|
+
}
|
|
105
|
+
setSelectedNode(null);
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const handlePointerMove = (event: React.PointerEvent<HTMLDivElement>) => {
|
|
110
|
+
if (draggingNode && playerRect && transformMatrix) {
|
|
111
|
+
const { x: playerX, y: playerY } = transformPoint(
|
|
112
|
+
event.clientX,
|
|
113
|
+
event.clientY,
|
|
114
|
+
transformMatrix
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
setNodes(prevNodes => {
|
|
118
|
+
const newNodes = new Map(prevNodes);
|
|
119
|
+
const node = newNodes.get(draggingNode.key);
|
|
120
|
+
if (node && dragOffset) {
|
|
121
|
+
node.x = playerX - dragOffset.x;
|
|
122
|
+
node.y = playerY - dragOffset.y;
|
|
123
|
+
|
|
124
|
+
const { x, y } = transformPoint(node.x+dimensions.x/2, node.y+dimensions.y/2, transformMatrix.inverse());
|
|
125
|
+
setSelectedNode({
|
|
126
|
+
key: draggingNode.key,
|
|
127
|
+
x: x,
|
|
128
|
+
y: y,
|
|
129
|
+
width: selectedNode?.width || draggingNode.width() / transformMatrix.a,
|
|
130
|
+
height: selectedNode?.height || draggingNode.height() / transformMatrix.d,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
}
|
|
134
|
+
return newNodes;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
function createTransformationMatrix(
|
|
140
|
+
playerRect: DOMRect,
|
|
141
|
+
dimensions: { x: number; y: number }
|
|
142
|
+
): DOMMatrix {
|
|
143
|
+
const scaleX = dimensions.x / playerRect.width;
|
|
144
|
+
const scaleY = dimensions.y / playerRect.height;
|
|
145
|
+
|
|
146
|
+
const matrix = new DOMMatrix();
|
|
147
|
+
matrix.scaleSelf(scaleX, scaleY);
|
|
148
|
+
matrix.translateSelf(-playerRect.left, -playerRect.top);
|
|
149
|
+
|
|
150
|
+
return matrix;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function transformPoint(
|
|
154
|
+
x: number,
|
|
155
|
+
y: number,
|
|
156
|
+
matrix: DOMMatrix
|
|
157
|
+
): { x: number; y: number } {
|
|
158
|
+
const point = new DOMPoint(x, y);
|
|
159
|
+
const transformedPoint = point.matrixTransform(matrix);
|
|
160
|
+
return { x: transformedPoint.x, y: transformedPoint.y };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Render the video.
|
|
165
|
+
*/
|
|
166
|
+
async function render() {
|
|
167
|
+
setRenderLoading(true);
|
|
168
|
+
const res = await fetch('/api/render', {
|
|
169
|
+
method: 'POST',
|
|
170
|
+
headers: {
|
|
171
|
+
'Content-Type': 'application/json',
|
|
172
|
+
},
|
|
173
|
+
body: JSON.stringify({
|
|
174
|
+
variables: {
|
|
175
|
+
nodes: Object.fromEntries(nodes)
|
|
176
|
+
},
|
|
177
|
+
streamProgress: true,
|
|
178
|
+
}),
|
|
179
|
+
}).catch((e) => console.log(e));
|
|
180
|
+
|
|
181
|
+
if (!res) {
|
|
182
|
+
alert('Failed to render video.');
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const downloadUrl = await parseStream(res.body!.getReader(), (p) => setProgress(p));
|
|
187
|
+
setRenderLoading(false);
|
|
188
|
+
setDownloadUrl(downloadUrl);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<>
|
|
193
|
+
<div className="m-auto p-12 max-w-6xl flex flex-col gap-y-4">
|
|
194
|
+
<div
|
|
195
|
+
className="rounded-lg overflow-hidden w-[100%] py-20 bg-gray-100 flex items-center justify-center"
|
|
196
|
+
onPointerDown={handlePointerDown}
|
|
197
|
+
onPointerMove={handlePointerMove}
|
|
198
|
+
onPointerUp={() => {
|
|
199
|
+
setDraggingNode(null);
|
|
200
|
+
setDragOffset(null);
|
|
201
|
+
}}
|
|
202
|
+
>
|
|
203
|
+
<div className='w-[80%] h-[80%]'>
|
|
204
|
+
<Player
|
|
205
|
+
src="http://localhost:4000/player/"
|
|
206
|
+
controls={false}
|
|
207
|
+
playing={playing}
|
|
208
|
+
width={dimensions.x}
|
|
209
|
+
height={dimensions.y}
|
|
210
|
+
variables={{
|
|
211
|
+
nodes: Object.fromEntries(nodes),
|
|
212
|
+
}}
|
|
213
|
+
onPlayerReady={handlePlayerReady}
|
|
214
|
+
onPlayerResize={(rect: DOMRect) => {
|
|
215
|
+
setPlayerRect(rect);
|
|
216
|
+
}}
|
|
217
|
+
/>
|
|
218
|
+
{(selectedNode && playerRect) && (
|
|
219
|
+
<div
|
|
220
|
+
className="absolute border-2 border-[#5bbad5] pointer-events-none"
|
|
221
|
+
style={{
|
|
222
|
+
left: `${selectedNode.x-selectedNode.width/2}px`,
|
|
223
|
+
top: `${selectedNode.y-selectedNode.height/2}px`,
|
|
224
|
+
width: `${selectedNode.width}px`,
|
|
225
|
+
height: `${selectedNode.height}px`,
|
|
226
|
+
}}
|
|
227
|
+
/>
|
|
228
|
+
)}
|
|
229
|
+
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
<div className="flex gap-x-4">
|
|
233
|
+
{/* Progress bar */}
|
|
234
|
+
<div className="text-sm flex-1 bg-gray-100 rounded-md overflow-hidden">
|
|
235
|
+
<div
|
|
236
|
+
className="text-gray-600 bg-gray-400 h-full flex items-center px-4 transition-all transition-200"
|
|
237
|
+
style={{
|
|
238
|
+
width: `${Math.round(progress * 100)}%`,
|
|
239
|
+
}}
|
|
240
|
+
>
|
|
241
|
+
{Math.round(progress * 100)}%
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
{downloadUrl ? (
|
|
245
|
+
<a
|
|
246
|
+
href={downloadUrl}
|
|
247
|
+
download
|
|
248
|
+
className="text-sm flex items-center gap-x-2 rounded-md p-2 bg-green-200 text-gray-700 hover:bg-gray-300"
|
|
249
|
+
>
|
|
250
|
+
Download video
|
|
251
|
+
</a>
|
|
252
|
+
) : (
|
|
253
|
+
<>
|
|
254
|
+
<Button
|
|
255
|
+
loading={false}
|
|
256
|
+
onClick={() => {
|
|
257
|
+
setPlaying(!playing);
|
|
258
|
+
setSelectedNode(null);
|
|
259
|
+
}}
|
|
260
|
+
>
|
|
261
|
+
{playing ? "Pause" : "Play"}
|
|
262
|
+
</Button>
|
|
263
|
+
|
|
264
|
+
<Button onClick={addTextNode} loading={false}>
|
|
265
|
+
Add Text
|
|
266
|
+
</Button>
|
|
267
|
+
|
|
268
|
+
<Button onClick={() => render()} loading={renderLoading}>
|
|
269
|
+
Render video
|
|
270
|
+
</Button>
|
|
271
|
+
</>
|
|
272
|
+
)}
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
</>
|
|
276
|
+
);
|
|
277
|
+
}
|