diff --git a/.agents/skills/add-effect/SKILL.md b/.agents/skills/add-effect/SKILL.md index 943478d4a4b..f19e21c6b4c 100644 --- a/.agents/skills/add-effect/SKILL.md +++ b/.agents/skills/add-effect/SKILL.md @@ -1,6 +1,6 @@ --- name: add-effect -description: Add a new effect to @remotion/effects, including implementation, package exports, docs, demos, preview images, tests, formatting, and builds. +description: Add a new effect to @remotion/effects, including implementation, package exports, docs, demos, preview images, Remotion skill updates, tests, formatting, and builds. --- # Add a new `@remotion/effects` effect @@ -323,7 +323,19 @@ Commit the generated `packages/docs/static/generated/articles-docs-effects-=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw=="], + "react-router": ["react-router@7.15.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-HW9vYwuM8f4yx66Izy8xfrzCM+SBJluoZcCbww9A1TySax11S5Vgw6fi3ZjMONw9J4gQwngL7PzkyIpJJpJ7RQ=="], "react-router-config": ["react-router-config@5.1.1", "", { "dependencies": { "@babel/runtime": "7.27.1" }, "peerDependencies": { "react": "19.0.0", "react-router": "5.3.4" } }, "sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg=="], @@ -9350,8 +9350,14 @@ "@react-router/dev/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "@react-router/express/react-router": ["react-router@7.12.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw=="], + + "@react-router/node/react-router": ["react-router@7.12.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw=="], + "@react-router/serve/express": ["express@4.21.2", "", { "dependencies": { "accepts": "1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "1.0.5", "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "2.0.0", "escape-html": "1.0.3", "etag": "1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "1.1.2", "on-finished": "2.4.1", "parseurl": "1.3.3", "path-to-regexp": "0.1.12", "proxy-addr": "2.0.7", "qs": "6.13.0", "range-parser": "1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "1.6.18", "utils-merge": "1.0.1", "vary": "1.1.2" } }, "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA=="], + "@react-router/serve/react-router": ["react-router@7.12.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw=="], + "@react-three/drei/hls.js": ["hls.js@1.5.19", "", {}, "sha512-C020dKWEJcyvLnrqsFKW4q6D/6IEzKWdhktIS5bgoyEFE8lHgrFBq4RIngdy113abJOlIruhv8qjg7UX8hwxOw=="], "@react-three/drei/react": ["react@19.0.0", "", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="], @@ -10960,12 +10966,12 @@ "react-remove-scroll-bar/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "react-router/cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], - "react-router-config/react": ["react@19.0.0", "", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="], "react-router-config/react-router": ["react-router@5.3.4", "", { "dependencies": { "@babel/runtime": "7.27.1", "history": "4.10.1", "hoist-non-react-statics": "3.3.2", "loose-envify": "1.4.0", "path-to-regexp": "1.8.0", "prop-types": "15.8.1", "react-is": "16.13.1", "tiny-invariant": "1.3.3", "tiny-warning": "1.0.3" }, "peerDependencies": { "react": "19.0.0" } }, "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA=="], + "react-router-dom/react-router": ["react-router@7.12.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw=="], + "react-style-singleton/react": ["react@19.0.0", "", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="], "react-style-singleton/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -12860,6 +12866,10 @@ "@react-router/dev/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + "@react-router/express/react-router/cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], + + "@react-router/node/react-router/cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], + "@react-router/serve/express/cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="], "@react-router/serve/express/cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="], @@ -12868,6 +12878,8 @@ "@react-router/serve/express/path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="], + "@react-router/serve/react-router/cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], + "@react-three/drei/react-dom/scheduler": ["scheduler@0.25.0", "", {}, "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="], "@remix-run/dev/@babel/core/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "7.27.1", "js-tokens": "4.0.0", "picocolors": "1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], @@ -14048,6 +14060,8 @@ "react-router-config/react-router/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + "react-router-dom/react-router/cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], + "react-use-measure/react-dom/scheduler": ["scheduler@0.25.0", "", {}, "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="], "read-pkg-up/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], diff --git a/packages/animated-emoji/package.json b/packages/animated-emoji/package.json index cb38b6c9c50..e6644578c27 100644 --- a/packages/animated-emoji/package.json +++ b/packages/animated-emoji/package.json @@ -1,6 +1,6 @@ { "name": "@remotion/animated-emoji", - "version": "4.0.471", + "version": "4.0.472", "main": "dist/cjs/index.js", "types": "dist/cjs/index.d.ts", "module": "dist/esm/index.mjs", diff --git a/packages/animation-utils/package.json b/packages/animation-utils/package.json index b7a06ef5c18..cb74481bbdc 100644 --- a/packages/animation-utils/package.json +++ b/packages/animation-utils/package.json @@ -7,7 +7,7 @@ "name": "Chetan Karwa", "email": "cbkarwa@gmail.com" }, - "version": "4.0.471", + "version": "4.0.472", "description": "Helpers for animating CSS properties", "main": "./dist/index.js", "module": "./dist/index.mjs", diff --git a/packages/astro-example/package.json b/packages/astro-example/package.json index c2ab776d1ee..3b51c3436b0 100644 --- a/packages/astro-example/package.json +++ b/packages/astro-example/package.json @@ -5,7 +5,7 @@ "name": "@remotion/astro-example", "private": true, "type": "module", - "version": "4.0.471", + "version": "4.0.472", "scripts": { "dev": "astro dev", "start": "astro dev", diff --git a/packages/babel-loader/package.json b/packages/babel-loader/package.json index e540d7b0cf1..fc9b59f8e36 100644 --- a/packages/babel-loader/package.json +++ b/packages/babel-loader/package.json @@ -3,7 +3,7 @@ "url": "https://github.com/remotion-dev/remotion/tree/main/packages/babel-loader" }, "name": "@remotion/babel-loader", - "version": "4.0.471", + "version": "4.0.472", "description": "Babel loader for Remotion", "main": "dist/index.js", "scripts": { diff --git a/packages/brand/package.json b/packages/brand/package.json index db912548a32..cb05cdc1848 100644 --- a/packages/brand/package.json +++ b/packages/brand/package.json @@ -3,7 +3,7 @@ "url": "https://github.com/remotion-dev/remotion/tree/main/packages/brand" }, "name": "@remotion/brand", - "version": "4.0.471", + "version": "4.0.472", "private": true, "sideEffects": [ "*.css" diff --git a/packages/bugs/package.json b/packages/bugs/package.json index c9b9123a85c..dbdbb376345 100644 --- a/packages/bugs/package.json +++ b/packages/bugs/package.json @@ -4,5 +4,5 @@ }, "name": "bugs", "private": true, - "version": "4.0.471" + "version": "4.0.472" } diff --git a/packages/bundler/package.json b/packages/bundler/package.json index 69f250fe224..2c7e7b57965 100644 --- a/packages/bundler/package.json +++ b/packages/bundler/package.json @@ -3,7 +3,7 @@ "url": "https://github.com/remotion-dev/remotion/tree/main/packages/bundler" }, "name": "@remotion/bundler", - "version": "4.0.471", + "version": "4.0.472", "description": "Bundle Remotion compositions using Webpack", "main": "dist/index.js", "bugs": { diff --git a/packages/captions/package.json b/packages/captions/package.json index fff090db569..7365e919d56 100644 --- a/packages/captions/package.json +++ b/packages/captions/package.json @@ -3,7 +3,7 @@ "url": "https://github.com/remotion-dev/remotion/tree/main/packages/captions" }, "name": "@remotion/captions", - "version": "4.0.471", + "version": "4.0.472", "description": "Primitives for dealing with captions", "main": "dist/index.js", "bugs": { diff --git a/packages/cli/package.json b/packages/cli/package.json index b2a7be31a19..be675e11f05 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -3,7 +3,7 @@ "url": "https://github.com/remotion-dev/remotion/tree/main/packages/cli" }, "name": "@remotion/cli", - "version": "4.0.471", + "version": "4.0.472", "description": "Control Remotion features using the `npx remotion` command", "main": "dist/index.js", "bin": { diff --git a/packages/cloudrun/container/package.json b/packages/cloudrun/container/package.json index c0a54103c36..7e787f5e2b1 100644 --- a/packages/cloudrun/container/package.json +++ b/packages/cloudrun/container/package.json @@ -1,6 +1,6 @@ { "name": "cloud-run-render", - "version": "4.0.471", + "version": "4.0.472", "description": "Render media and stills on GCP Cloud Run", "main": "dist/index.js", "scripts": { diff --git a/packages/cloudrun/package.json b/packages/cloudrun/package.json index 0753281f083..9f5392cbdf7 100644 --- a/packages/cloudrun/package.json +++ b/packages/cloudrun/package.json @@ -3,7 +3,7 @@ "url": "https://github.com/remotion-dev/remotion/tree/main/packages/cloudrun" }, "name": "@remotion/cloudrun", - "version": "4.0.471", + "version": "4.0.472", "description": "Render Remotion videos on Google Cloud Run", "main": "dist/index.js", "scripts": { diff --git a/packages/codex-plugin/package.json b/packages/codex-plugin/package.json index ffd586918b4..13d20705728 100644 --- a/packages/codex-plugin/package.json +++ b/packages/codex-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@remotion/codex-plugin", - "version": "4.0.471", + "version": "4.0.472", "private": true, "type": "module", "repository": { diff --git a/packages/compositor-darwin-arm64/package.json b/packages/compositor-darwin-arm64/package.json index 7006fde083f..0d90d45e879 100644 --- a/packages/compositor-darwin-arm64/package.json +++ b/packages/compositor-darwin-arm64/package.json @@ -2,7 +2,7 @@ "repository": { "url": "https://github.com/remotion-dev/remotion/tree/main/packages/compositor-darwin-arm64" }, - "version": "4.0.471", + "version": "4.0.472", "name": "@remotion/compositor-darwin-arm64", "os": [ "darwin" diff --git a/packages/compositor-darwin-arm64/remotion b/packages/compositor-darwin-arm64/remotion index ba1e17cfdff..8253f04a35a 100755 Binary files a/packages/compositor-darwin-arm64/remotion and b/packages/compositor-darwin-arm64/remotion differ diff --git a/packages/compositor-darwin-x64/package.json b/packages/compositor-darwin-x64/package.json index 3e2f1fb8bed..b2185b905d2 100644 --- a/packages/compositor-darwin-x64/package.json +++ b/packages/compositor-darwin-x64/package.json @@ -2,7 +2,7 @@ "repository": { "url": "https://github.com/remotion-dev/remotion/tree/main/packages/compositor-darwin-x64" }, - "version": "4.0.471", + "version": "4.0.472", "name": "@remotion/compositor-darwin-x64", "os": [ "darwin" diff --git a/packages/compositor-darwin-x64/remotion b/packages/compositor-darwin-x64/remotion index 83b7c3e35d8..906d383cd0a 100755 Binary files a/packages/compositor-darwin-x64/remotion and b/packages/compositor-darwin-x64/remotion differ diff --git a/packages/compositor-linux-arm64-gnu/package.json b/packages/compositor-linux-arm64-gnu/package.json index 369458fb9d1..e3b86806a89 100644 --- a/packages/compositor-linux-arm64-gnu/package.json +++ b/packages/compositor-linux-arm64-gnu/package.json @@ -2,7 +2,7 @@ "repository": { "url": "https://github.com/remotion-dev/remotion/tree/main/packages/compositor-linux-arm64-gnu" }, - "version": "4.0.471", + "version": "4.0.472", "name": "@remotion/compositor-linux-arm64-gnu", "os": [ "linux" diff --git a/packages/compositor-linux-arm64-gnu/remotion b/packages/compositor-linux-arm64-gnu/remotion index 3d1a88ad236..14991846bdc 100755 Binary files a/packages/compositor-linux-arm64-gnu/remotion and b/packages/compositor-linux-arm64-gnu/remotion differ diff --git a/packages/compositor-linux-arm64-musl/package.json b/packages/compositor-linux-arm64-musl/package.json index 6b89e46d9c3..068fc0696b0 100644 --- a/packages/compositor-linux-arm64-musl/package.json +++ b/packages/compositor-linux-arm64-musl/package.json @@ -2,7 +2,7 @@ "repository": { "url": "https://github.com/remotion-dev/remotion/tree/main/packages/compositor-linux-arm64-musl" }, - "version": "4.0.471", + "version": "4.0.472", "name": "@remotion/compositor-linux-arm64-musl", "os": [ "linux" diff --git a/packages/compositor-linux-arm64-musl/remotion b/packages/compositor-linux-arm64-musl/remotion index 10e01ccf932..9a0ee575532 100755 Binary files a/packages/compositor-linux-arm64-musl/remotion and b/packages/compositor-linux-arm64-musl/remotion differ diff --git a/packages/compositor-linux-x64-gnu/package.json b/packages/compositor-linux-x64-gnu/package.json index 7cc12ac0431..fe00183f64c 100644 --- a/packages/compositor-linux-x64-gnu/package.json +++ b/packages/compositor-linux-x64-gnu/package.json @@ -2,7 +2,7 @@ "repository": { "url": "https://github.com/remotion-dev/remotion/tree/main/packages/compositor-linux-x64-gnu" }, - "version": "4.0.471", + "version": "4.0.472", "name": "@remotion/compositor-linux-x64-gnu", "os": [ "linux" diff --git a/packages/compositor-linux-x64-gnu/remotion b/packages/compositor-linux-x64-gnu/remotion index e204dc6010f..10880cc74d6 100755 Binary files a/packages/compositor-linux-x64-gnu/remotion and b/packages/compositor-linux-x64-gnu/remotion differ diff --git a/packages/compositor-linux-x64-musl/package.json b/packages/compositor-linux-x64-musl/package.json index 5e154c183d4..d4534536584 100644 --- a/packages/compositor-linux-x64-musl/package.json +++ b/packages/compositor-linux-x64-musl/package.json @@ -2,7 +2,7 @@ "repository": { "url": "https://github.com/remotion-dev/remotion/tree/main/packages/compositor-linux-x64-musl" }, - "version": "4.0.471", + "version": "4.0.472", "name": "@remotion/compositor-linux-x64-musl", "os": [ "linux" diff --git a/packages/compositor-linux-x64-musl/remotion b/packages/compositor-linux-x64-musl/remotion index 5fb28fbdf06..eca3dcd80c4 100755 Binary files a/packages/compositor-linux-x64-musl/remotion and b/packages/compositor-linux-x64-musl/remotion differ diff --git a/packages/compositor-win32-x64-msvc/package.json b/packages/compositor-win32-x64-msvc/package.json index 5e0ab33b342..4156202c14f 100644 --- a/packages/compositor-win32-x64-msvc/package.json +++ b/packages/compositor-win32-x64-msvc/package.json @@ -2,7 +2,7 @@ "repository": { "url": "https://github.com/remotion-dev/remotion/tree/main/packages/compositor-win32-x64-msvc" }, - "version": "4.0.471", + "version": "4.0.472", "name": "@remotion/compositor-win32-x64-msvc", "os": [ "win32" diff --git a/packages/compositor/package.json b/packages/compositor/package.json index 607da893066..5167c76de6c 100644 --- a/packages/compositor/package.json +++ b/packages/compositor/package.json @@ -3,7 +3,7 @@ "url": "https://github.com/remotion-dev/remotion/tree/main/packages/compositor" }, "name": "@remotion/compositor", - "version": "4.0.471", + "version": "4.0.472", "description": "Rust binary for Remotion", "scripts": { "build-all": "bun build.ts --all" diff --git a/packages/convert/package.json b/packages/convert/package.json index ab8a5e9eb2f..fcc3f9804b9 100644 --- a/packages/convert/package.json +++ b/packages/convert/package.json @@ -1,6 +1,6 @@ { "name": "@remotion/convert", - "version": "4.0.471", + "version": "4.0.472", "private": true, "type": "module", "author": "Jonny Burger , Hunain Ahmed ", diff --git a/packages/core/package.json b/packages/core/package.json index 586aa45f8ce..39fb54af36f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -3,7 +3,7 @@ "url": "https://github.com/remotion-dev/remotion/tree/main/packages/core" }, "name": "remotion", - "version": "4.0.471", + "version": "4.0.472", "description": "Make videos programmatically", "main": "dist/cjs/index.js", "types": "dist/cjs/index.d.ts", diff --git a/packages/core/src/CompositionManagerContext.tsx b/packages/core/src/CompositionManagerContext.tsx index 13b0bf37466..5760902427c 100644 --- a/packages/core/src/CompositionManagerContext.tsx +++ b/packages/core/src/CompositionManagerContext.tsx @@ -55,6 +55,7 @@ export type CompositionManagerSetters = { name: string, parent: string | null, nonce: NonceHistory, + stack: string | null, ) => void; unregisterFolder: (name: string, parent: string | null) => void; setCanvasContent: React.Dispatch>; diff --git a/packages/core/src/CompositionManagerProvider.tsx b/packages/core/src/CompositionManagerProvider.tsx index 9acf3d2a2ab..18253d6ae6f 100644 --- a/packages/core/src/CompositionManagerProvider.tsx +++ b/packages/core/src/CompositionManagerProvider.tsx @@ -79,7 +79,12 @@ export const CompositionManagerProvider = ({ }, []); const registerFolder = useCallback( - (name: string, parent: string | null, nonce: NonceHistory) => { + ( + name: string, + parent: string | null, + nonce: NonceHistory, + stack: string | null, + ) => { setFolders((prevFolders) => { return [ ...prevFolders, @@ -87,6 +92,7 @@ export const CompositionManagerProvider = ({ name, parent, nonce, + stack, }, ]; }); diff --git a/packages/core/src/Folder.tsx b/packages/core/src/Folder.tsx index 035a803ba89..605dd18bdc2 100644 --- a/packages/core/src/Folder.tsx +++ b/packages/core/src/Folder.tsx @@ -10,6 +10,7 @@ export type TFolder = { name: string; parent: string | null; nonce: NonceHistory; + stack: string | null; }; type FolderContextType = { @@ -29,10 +30,12 @@ export const FolderContext = createContext({ export const Folder: FC<{ readonly name: string; readonly children: React.ReactNode; -}> = ({name, children}) => { +}> = (props) => { + const {name, children} = props; const parent = useContext(FolderContext); const {registerFolder, unregisterFolder} = useContext(CompositionSetters); const nonce = useNonce(); + const stack = (props as {stack?: string}).stack ?? null; validateFolderName(name); @@ -49,7 +52,7 @@ export const Folder: FC<{ }, [name, parentName]); useEffect(() => { - registerFolder(name, parentName, nonce.get()); + registerFolder(name, parentName, nonce.get(), stack); return () => { unregisterFolder(name, parentName); @@ -61,6 +64,7 @@ export const Folder: FC<{ registerFolder, unregisterFolder, nonce, + stack, ]); return ( diff --git a/packages/core/src/HtmlInCanvas.tsx b/packages/core/src/HtmlInCanvas.tsx index 15f4de921d5..fc85cbbaa82 100644 --- a/packages/core/src/HtmlInCanvas.tsx +++ b/packages/core/src/HtmlInCanvas.tsx @@ -185,6 +185,7 @@ export type HtmlInCanvasOnPaintParams = { readonly canvas: OffscreenCanvas; readonly element: HTMLDivElement; readonly elementImage: ElementImage; + readonly pixelDensity: number; }; // Memoize the support check across the session — neither the platform @@ -225,6 +226,8 @@ export type HtmlInCanvasOnInit = ( params: HtmlInCanvasOnPaintParams, ) => HtmlInCanvasOnInitCleanup | Promise; +export type HtmlInCanvasPixelDensity = number; + function assertHtmlInCanvasDimensions(width: unknown, height: unknown): void { if (typeof width !== 'number' || typeof height !== 'number') { throw new Error( @@ -245,6 +248,44 @@ function assertHtmlInCanvasDimensions(width: unknown, height: unknown): void { } } +function resolveHtmlInCanvasPixelDensity( + pixelDensity: HtmlInCanvasPixelDensity | undefined, +): number { + if (pixelDensity === undefined) { + return 1; + } + + if ( + typeof pixelDensity !== 'number' || + !Number.isFinite(pixelDensity) || + pixelDensity <= 0 + ) { + throw new Error( + `HtmlInCanvas: \`pixelDensity\` must be a positive finite number. Received: ${String(pixelDensity)}.`, + ); + } + + return pixelDensity; +} + +const resizeOffscreenCanvas = ({ + offscreen, + width, + height, +}: { + offscreen: OffscreenCanvas; + width: number; + height: number; +}) => { + if (offscreen.width !== width) { + offscreen.width = width; + } + + if (offscreen.height !== height) { + offscreen.height = height; + } +}; + const defaultOnPaint: HtmlInCanvasOnPaint = ({ canvas, element, @@ -284,6 +325,7 @@ export type HtmlInCanvasProps = Omit< readonly children: React.ReactNode; readonly onPaint?: HtmlInCanvasOnPaint; readonly onInit?: HtmlInCanvasOnInit; + readonly pixelDensity?: HtmlInCanvasPixelDensity; }; /* eslint-enable react/require-default-props */ @@ -296,6 +338,7 @@ type HtmlInCanvasContentProps = { readonly children: React.ReactNode; readonly onPaint: HtmlInCanvasOnPaint | undefined; readonly onInit: HtmlInCanvasOnInit | undefined; + readonly pixelDensity: HtmlInCanvasPixelDensity | undefined; readonly controls: SequenceControls | undefined; readonly style: React.CSSProperties | undefined; }; @@ -305,7 +348,17 @@ const HtmlInCanvasContent = forwardRef< HtmlInCanvasContentProps >( ( - {width, height, effects, children, onPaint, onInit, controls, style}, + { + width, + height, + effects, + children, + onPaint, + onInit, + pixelDensity, + controls, + style, + }, ref, ) => { const isInsideAncestorHtmlInCanvas = useContext( @@ -313,6 +366,9 @@ const HtmlInCanvasContent = forwardRef< ); assertHtmlInCanvasDimensions(width, height); + const resolvedPixelDensity = resolveHtmlInCanvasPixelDensity(pixelDensity); + const canvasWidth = Math.ceil(width * resolvedPixelDensity); + const canvasHeight = Math.ceil(height * resolvedPixelDensity); const {continueRender, cancelRender} = useDelayRender(); if (!isHtmlInCanvasSupported()) { @@ -322,7 +378,7 @@ const HtmlInCanvasContent = forwardRef< const canvas2dRef = useRef(null); const offscreenRef = useRef(null); const divRef = useRef(null); - const canvasSizeKey = `${width}x${height}`; + const canvasSizeKey = `${width}x${height}@${resolvedPixelDensity}`; const setLayoutCanvasRef = useCallback( (node: HTMLCanvasElement | null) => { @@ -369,8 +425,11 @@ const HtmlInCanvasContent = forwardRef< ); } - offscreen.width = width; - offscreen.height = height; + resizeOffscreenCanvas({ + offscreen, + width: canvasWidth, + height: canvasHeight, + }); try { const placeholderCanvas = canvas2dRef.current; @@ -404,6 +463,7 @@ const HtmlInCanvasContent = forwardRef< canvas: offscreen, element, elementImage: initImage!, + pixelDensity: resolvedPixelDensity, }); if (typeof cleanup !== 'function') { throw new Error( @@ -426,22 +486,30 @@ const HtmlInCanvasContent = forwardRef< canvas: offscreen, element, elementImage: elImage!, + pixelDensity: resolvedPixelDensity, }); await runEffectChain({ - state: chainState.get(width, height)!, + state: chainState.get(canvasWidth, canvasHeight)!, source: offscreen, effects: effectsRef.current, output: offscreen, - width, - height, + width: canvasWidth, + height: canvasHeight, }); continueRender(handle); } catch (error) { cancelRender(error); } - }, [chainState, continueRender, cancelRender, width, height]); + }, [ + canvasHeight, + canvasWidth, + chainState, + continueRender, + cancelRender, + resolvedPixelDensity, + ]); // Transfer control once per layout canvas instance, then listen for paint on // the placeholder (capture) while drawing on the linked offscreen surface. @@ -455,8 +523,11 @@ const HtmlInCanvasContent = forwardRef< const offscreen = placeholder.transferControlToOffscreen(); offscreenRef.current = offscreen; - offscreen.width = width; - offscreen.height = height; + resizeOffscreenCanvas({ + offscreen, + width: canvasWidth, + height: canvasHeight, + }); initializedRef.current = false; unmountedRef.current = false; @@ -471,7 +542,7 @@ const HtmlInCanvasContent = forwardRef< onInitCleanupRef.current?.(); onInitCleanupRef.current = null; }; - }, [onPaintCb, cancelRender, width, height]); + }, [onPaintCb, cancelRender, canvasWidth, canvasHeight]); const onPaintChangedRef = useRef(false); useLayoutEffect(() => { @@ -515,6 +586,14 @@ const HtmlInCanvasContent = forwardRef< }; }, [width, height]); + const canvasStyle = useMemo(() => { + return { + width, + height, + ...(style ?? {}), + }; + }, [height, style, width]); + if (isInsideAncestorHtmlInCanvas) { throw new Error( ' effects cannot be nested together. Chrome will only display the outer effect. Consider merging the effects into one if you can.', @@ -526,9 +605,9 @@ const HtmlInCanvasContent = forwardRef<
{children} @@ -555,6 +634,7 @@ const HtmlInCanvasInner = forwardRef< children, onPaint, onInit, + pixelDensity, _experimentalControls: controls, style, durationInFrames, @@ -603,6 +683,7 @@ const HtmlInCanvasInner = forwardRef< effects={effects} onPaint={onPaint} onInit={onInit} + pixelDensity={pixelDensity} controls={controls} style={style} > diff --git a/packages/core/src/SequenceManager.tsx b/packages/core/src/SequenceManager.tsx index 6302647a81d..ccf5f900bc1 100644 --- a/packages/core/src/SequenceManager.tsx +++ b/packages/core/src/SequenceManager.tsx @@ -3,6 +3,7 @@ import type {TSequence} from './CompositionManager.js'; import type { CanUpdateSequencePropStatus, CodeValues, + DragOverrideValue, DragOverrides, EffectDragOverrides, GetDragOverrides, @@ -40,14 +41,14 @@ export type VisualModeSetters = { setDragOverrides: ( nodePath: SequencePropsSubscriptionKey, key: string, - value: unknown, + value: DragOverrideValue, ) => void; clearDragOverrides: (nodePath: SequencePropsSubscriptionKey) => void; setEffectDragOverrides: ( nodePath: SequencePropsSubscriptionKey, effectIndex: number, key: string, - value: unknown, + value: DragOverrideValue, ) => void; clearEffectDragOverrides: ( nodePath: SequencePropsSubscriptionKey, @@ -165,7 +166,11 @@ export const SequenceManagerProvider: React.FC<{ const [codeValues, setCodeValuesMapState] = useState({}); const setDragOverrides = useCallback( - (nodePath: SequencePropsSubscriptionKey, key: string, value: unknown) => { + ( + nodePath: SequencePropsSubscriptionKey, + key: string, + value: DragOverrideValue, + ) => { setControlOverrides((prev) => ({ ...prev, [makeSequencePropsSubscriptionKey(nodePath)]: { @@ -198,7 +203,7 @@ export const SequenceManagerProvider: React.FC<{ nodePath: SequencePropsSubscriptionKey, effectIndex: number, key: string, - value: unknown, + value: DragOverrideValue, ) => { setEffectDragOverridesState((prev) => { const mapKey = effectDragOverridesKey(nodePath, effectIndex); diff --git a/packages/core/src/effects/Solid.tsx b/packages/core/src/effects/Solid.tsx index b83a30a6c6c..6dcc1f6558d 100644 --- a/packages/core/src/effects/Solid.tsx +++ b/packages/core/src/effects/Solid.tsx @@ -38,6 +38,25 @@ type OptionalProps = { readonly effects: EffectsProp; readonly className: string | undefined; readonly style: React.CSSProperties | undefined; + readonly pixelDensity: number | undefined; +}; + +const resolveSolidPixelDensity = (pixelDensity: number | undefined): number => { + if (pixelDensity === undefined) { + return 1; + } + + if ( + typeof pixelDensity !== 'number' || + !Number.isFinite(pixelDensity) || + pixelDensity <= 0 + ) { + throw new Error( + `: \`pixelDensity\` must be a positive finite number. Received: ${String(pixelDensity)}.`, + ); + } + + return pixelDensity; }; type InnerSolidProps = MandatoryProps & @@ -86,11 +105,16 @@ const SolidInner: React.FC< effects = [], className, style, + pixelDensity, overrideId, reference, }) => { const {delayRender, continueRender, cancelRender} = useDelayRender(); + const resolvedPixelDensity = resolveSolidPixelDensity(pixelDensity); + const canvasWidth = Math.ceil(width * resolvedPixelDensity); + const canvasHeight = Math.ceil(height * resolvedPixelDensity); + const [outputCanvas, setOutputCanvas] = useState( null, ); @@ -156,12 +180,12 @@ const SolidInner: React.FC< } runEffectChain({ - state: chainState.get(width, height)!, + state: chainState.get(canvasWidth, canvasHeight)!, source: sourceCanvas, effects: memoizedEffects, output: outputCanvas, - width, - height, + width: canvasWidth, + height: canvasHeight, }) .then((completed) => { if (completed) { @@ -180,21 +204,29 @@ const SolidInner: React.FC< outputCanvas, sourceCanvas, chainState, - width, - height, + canvasWidth, + canvasHeight, delayRender, continueRender, cancelRender, memoizedEffects, ]); + const canvasStyle = useMemo(() => { + return { + width, + height, + ...(style ?? {}), + }; + }, [height, style, width]); + return ( ); }; @@ -222,6 +254,7 @@ const SolidOuter = forwardRef< from, hidden, showInTimeline, + pixelDensity, ...props }, ref, @@ -261,6 +294,7 @@ const SolidOuter = forwardRef< className={className} style={style} effects={effects} + pixelDensity={pixelDensity} /> ); diff --git a/packages/core/src/effects/use-memoized-effects.ts b/packages/core/src/effects/use-memoized-effects.ts index b8ee79efff7..3609f751070 100644 --- a/packages/core/src/effects/use-memoized-effects.ts +++ b/packages/core/src/effects/use-memoized-effects.ts @@ -1,4 +1,5 @@ import {useContext, useRef} from 'react'; +import {resolveDragOverrideValue} from '../get-effective-visual-mode-value.js'; import {OverrideIdsToNodePathsGettersContext} from '../sequence-node-path.js'; import type { CannotUpdateEffectReason, @@ -12,7 +13,12 @@ import { VisualModeCodeValuesContext, VisualModeDragOverridesContext, } from '../SequenceManager.js'; -import type {CanUpdateSequencePropStatus, CodeValues} from '../use-schema.js'; +import {useCurrentFrame} from '../use-current-frame.js'; +import { + type CanUpdateSequencePropStatus, + type CodeValues, + type DragOverrideValue, +} from '../use-schema.js'; import type { EffectDefinition, EffectDefinitionAndStack, @@ -23,10 +29,12 @@ const mergeOverrides = ({ descriptor, codeOverrides, dragOverrides, + frame, }: { descriptor: EffectDescriptor; codeOverrides: Record | null; - dragOverrides: Record | null; + dragOverrides: Record | null; + frame: number; }): {params: unknown; effectKey: string} => { if (!codeOverrides && !dragOverrides) { return {params: descriptor.params, effectKey: descriptor.effectKey}; @@ -46,7 +54,13 @@ const mergeOverrides = ({ if (dragOverrides) { for (const [key, value] of Object.entries(dragOverrides)) { - merged[key] = value; + const resolved = resolveDragOverrideValue({ + dragOverrideValue: value, + frame, + }); + if (resolved.type === 'resolved') { + merged[key] = resolved.value; + } } } @@ -169,6 +183,7 @@ export const useMemoizedEffects = ({ const {codeValues} = useContext(VisualModeCodeValuesContext); const {getEffectDragOverrides} = useContext(VisualModeDragOverridesContext); + const frame = useCurrentFrame(); const {overrideIdToNodePathMappings} = useContext( OverrideIdsToNodePathsGettersContext, @@ -206,6 +221,7 @@ export const useMemoizedEffects = ({ descriptor, codeOverrides, dragOverrides, + frame, }); return {descriptor, params, effectKey}; diff --git a/packages/core/src/get-effective-visual-mode-value.ts b/packages/core/src/get-effective-visual-mode-value.ts index 34d6ee4a843..68bfabef585 100644 --- a/packages/core/src/get-effective-visual-mode-value.ts +++ b/packages/core/src/get-effective-visual-mode-value.ts @@ -1,18 +1,77 @@ -import type {CanUpdateSequencePropStatusStatic} from './use-schema'; +import {interpolateKeyframedStatus} from './interpolate-keyframed-status'; +import type { + CanUpdateSequencePropStatusKeyframed, + CanUpdateSequencePropStatusStatic, + DragOverrideValue, +} from './use-schema'; + +export type ResolvedDragOverrideValue = + | { + readonly type: 'none'; + } + | { + readonly type: 'resolved'; + readonly value: unknown; + }; + +export const resolveDragOverrideValue = ({ + dragOverrideValue, + frame, +}: { + dragOverrideValue: DragOverrideValue | undefined; + frame: number | null; +}): ResolvedDragOverrideValue => { + if (dragOverrideValue === undefined) { + return {type: 'none'}; + } + + if (dragOverrideValue.type === 'static') { + return {type: 'resolved', value: dragOverrideValue.value}; + } + + if (frame === null) { + return {type: 'none'}; + } + + const interpolated = interpolateKeyframedStatus({ + frame, + status: dragOverrideValue.status, + }); + if (interpolated === null) { + return {type: 'none'}; + } + + return {type: 'resolved', value: interpolated}; +}; export const getEffectiveVisualModeValue = ({ codeValue, dragOverrideValue, defaultValue, + frame = null, shouldResortToDefaultValueIfUndefined = false, }: { - codeValue: CanUpdateSequencePropStatusStatic; - dragOverrideValue: unknown; + codeValue: + | CanUpdateSequencePropStatusStatic + | CanUpdateSequencePropStatusKeyframed; + dragOverrideValue: DragOverrideValue | undefined; defaultValue: unknown; + frame?: number | null; shouldResortToDefaultValueIfUndefined: boolean; }) => { - if (dragOverrideValue !== undefined) { - return dragOverrideValue; + const dragOverride = resolveDragOverrideValue({ + dragOverrideValue, + frame, + }); + if (dragOverride.type === 'resolved' && dragOverride.value !== undefined) { + return dragOverride.value; + } + + if (codeValue.status === 'keyframed' && frame !== null) { + return interpolateKeyframedStatus({ + frame, + status: codeValue, + }); } if ( diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 21a6f56ec39..12ca8ac28e5 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -13,6 +13,7 @@ import type { } from './CompositionManager.js'; import type {DelayRenderScope} from './delay-render.js'; import {addSequenceStackTraces} from './enable-sequence-stack-traces.js'; +import {Folder, type TFolder} from './Folder.js'; import type {StaticFile} from './get-static-files.js'; import {useIsPlayer} from './is-player.js'; import type {LogLevel} from './log.js'; @@ -21,6 +22,8 @@ import {Null} from './Null.js'; import type {ProResProfile} from './prores-profile.js'; import type {PixelFormat, VideoImageFormat} from './render-types.js'; import type { + ArrayFieldSchema, + ArrayItemFieldSchema, SequenceFieldSchema, SequenceSchema, } from './sequence-field-schema.js'; @@ -156,6 +159,7 @@ export { type HtmlInCanvasOnInit, type HtmlInCanvasOnInitCleanup, type HtmlInCanvasOnPaint, + type HtmlInCanvasPixelDensity, } from './HtmlInCanvas.js'; export type { HtmlInCanvasOnPaintParams, @@ -229,6 +233,7 @@ export { useCurrentScale, } from './use-current-scale'; export {useDelayRender} from './use-delay-render'; +export {usePixelDensity} from './use-pixel-density'; export {useRemotionEnvironment} from './use-remotion-environment.js'; export * from './use-video-config.js'; export * from './version.js'; @@ -300,9 +305,11 @@ export const Config = new Proxy(proxyObj, { Sequence.displayName = 'Sequence'; addSequenceStackTraces(Sequence); addSequenceStackTraces(Composition); +addSequenceStackTraces(Folder); export type _InternalTypes = { AnyComposition: AnyComposition; + TFolder: TFolder; BundleCompositionState: BundleCompositionState; BundleState: BundleState; VideoConfigWithSerializedProps: VideoConfigWithSerializedProps; @@ -314,6 +321,8 @@ export type _InternalTypes = { export type { AnyComposition, + ArrayFieldSchema, + ArrayItemFieldSchema, DelayRenderScope, LoopDisplay, SequenceControls, diff --git a/packages/core/src/internals.ts b/packages/core/src/internals.ts index 14b527541a2..3b44e378b62 100644 --- a/packages/core/src/internals.ts +++ b/packages/core/src/internals.ts @@ -73,7 +73,10 @@ import { getFlatSchemaWithAllKeys, } from './flatten-schema.js'; import {getAssetDisplayName} from './get-asset-file-name.js'; -import {getEffectiveVisualModeValue} from './get-effective-visual-mode-value.js'; +import { + getEffectiveVisualModeValue, + resolveDragOverrideValue, +} from './get-effective-visual-mode-value.js'; import { getPreviewDomElement, REMOTION_STUDIO_CONTAINER_ELEMENT, @@ -122,6 +125,8 @@ import { sequenceSchema, sequenceStyleSchema, sequenceVisualStyleSchema, + type ArrayFieldSchema, + type ArrayItemFieldSchema, type SequenceFieldSchema, type SequenceSchema, type VisibleFieldSchema, @@ -184,10 +189,12 @@ import { useBasicMediaInTimeline, useMediaInTimeline, } from './use-media-in-timeline.js'; +import {PixelDensityContext} from './use-pixel-density.js'; import type { - CanUpdateSequencePropStatusStatic, CanUpdateSequencePropStatusFalse, CanUpdateSequencePropStatusKeyframed, + CanUpdateSequencePropStatusStatic, + DragOverrideValue, GetCodeValues, GetDragOverrides, GetEffectCodeValues, @@ -195,6 +202,9 @@ import type { } from './use-schema.js'; import { computeEffectiveSchemaValuesDotNotation, + getStaticDragOverrideValue, + makeKeyframedDragOverride, + makeStaticDragOverride, type CanUpdateSequencePropStatus, type CodeValues, type DragOverrides, @@ -212,6 +222,10 @@ import { invalidCompositionErrorMessage, isCompositionIdValid, } from './validation/validate-composition-id.js'; +import { + invalidFolderNameErrorMessage, + isFolderNameValid, +} from './validation/validate-folder-name.js'; import {DurationsContextProvider} from './video/duration-state.js'; import {InnerOffthreadVideo} from './video/OffthreadVideo.js'; import {isIosSafari} from './video/video-fragment.js'; @@ -297,8 +311,10 @@ export const Internals = { SharedAudioTagsContext, SharedAudioTagsContextProvider, invalidCompositionErrorMessage, + invalidFolderNameErrorMessage, calculateMediaDuration, isCompositionIdValid, + isFolderNameValid, getPreviewDomElement, compositionsRef, portalNode, @@ -333,6 +349,7 @@ export const Internals = { BufferingContextReact, getComponentsToAddStacksTo, CurrentScaleContext, + PixelDensityContext, PreviewSizeContext, calculateScale, validateRenderAsset, @@ -372,6 +389,10 @@ export const Internals = { createWebGL2ContextError, computeEffectiveSchemaValuesDotNotation, interpolateKeyframedStatus, + makeStaticDragOverride, + makeKeyframedDragOverride, + resolveDragOverrideValue, + getStaticDragOverrideValue, OverrideIdsToNodePathsGettersContext, OverrideIdsToNodePathsSettersContext, findPropsToDelete, @@ -384,21 +405,24 @@ export const Internals = { } as const; export type { + ArrayFieldSchema, + ArrayItemFieldSchema, CannotUpdateSequenceReason, CanUpdateEffectPropsResponse, CanUpdateEffectPropsResponseFalse, CanUpdateEffectPropsResponseTrue, - CanUpdateSequencePropStatusStatic, CanUpdateSequencePropsResponse, CanUpdateSequencePropsResponseFalse, CanUpdateSequencePropsResponseTrue, CanUpdateSequencePropStatus, CanUpdateSequencePropStatusFalse, CanUpdateSequencePropStatusKeyframed, + CanUpdateSequencePropStatusStatic, CodeValues, CompositionManagerContext, CompProps, DragOverrides, + DragOverrideValue, EffectDragOverrides, GetCodeValues, GetDragOverrides, diff --git a/packages/core/src/interpolate-keyframed-status.ts b/packages/core/src/interpolate-keyframed-status.ts index c8b42b354dd..5cb49427652 100644 --- a/packages/core/src/interpolate-keyframed-status.ts +++ b/packages/core/src/interpolate-keyframed-status.ts @@ -28,8 +28,9 @@ export const interpolateKeyframedStatus = ({ return null; } - const inputRange = keyframes.map((k) => k.frame); - const outputs = keyframes.map((k) => k.value); + const sortedKeyframes = [...keyframes].sort((a, b) => a.frame - b.frame); + const inputRange = sortedKeyframes.map((k) => k.frame); + const outputs = sortedKeyframes.map((k) => k.value); if (interpolationFunction === 'interpolateColors') { if (!outputs.every((v) => typeof v === 'string')) { diff --git a/packages/core/src/sequence-field-schema.ts b/packages/core/src/sequence-field-schema.ts index 7d536a4eac2..59c3b5959bf 100644 --- a/packages/core/src/sequence-field-schema.ts +++ b/packages/core/src/sequence-field-schema.ts @@ -82,6 +82,67 @@ export type EnumFieldSchema = { keyframable?: boolean; }; +export type NumberArrayItemSchema = Omit< + NumberFieldSchema, + 'default' | 'description' | 'hiddenFromList' | 'keyframable' +>; + +export type BooleanArrayItemSchema = Omit< + BooleanFieldSchema, + 'default' | 'description' | 'keyframable' +>; + +export type RotationCssArrayItemSchema = Omit< + RotationCssFieldSchema, + 'default' | 'description' | 'keyframable' +>; + +export type RotationDegreesArrayItemSchema = Omit< + RotationDegreesFieldSchema, + 'default' | 'description' | 'keyframable' +>; + +export type TranslateArrayItemSchema = Omit< + TranslateFieldSchema, + 'default' | 'description' | 'keyframable' +>; + +export type UvCoordinateArrayItemSchema = Omit< + UvCoordinateFieldSchema, + 'default' | 'description' | 'keyframable' +>; + +export type ColorArrayItemSchema = Omit< + ColorFieldSchema, + 'default' | 'description' | 'keyframable' +>; + +export type EnumArrayItemSchema = { + type: 'enum'; + variants: readonly string[]; +}; + +export type ArrayItemFieldSchema = + | NumberArrayItemSchema + | BooleanArrayItemSchema + | RotationCssArrayItemSchema + | RotationDegreesArrayItemSchema + | TranslateArrayItemSchema + | UvCoordinateArrayItemSchema + | ColorArrayItemSchema + | EnumArrayItemSchema; + +export type ArrayFieldSchema = { + type: 'array'; + item: ArrayItemFieldSchema; + default: readonly unknown[] | undefined; + minLength?: number; + maxLength?: number; + newItemDefault: unknown; + description?: string; + keyframable?: false; +}; + export type VisibleFieldSchema = | NumberFieldSchema | BooleanFieldSchema @@ -91,6 +152,7 @@ export type VisibleFieldSchema = | ScaleFieldSchema | UvCoordinateFieldSchema | ColorFieldSchema + | ArrayFieldSchema | EnumFieldSchema; export type SequenceFieldSchema = VisibleFieldSchema | HiddenFieldSchema; diff --git a/packages/core/src/test/drag-override-value.test.ts b/packages/core/src/test/drag-override-value.test.ts new file mode 100644 index 00000000000..088790b907d --- /dev/null +++ b/packages/core/src/test/drag-override-value.test.ts @@ -0,0 +1,119 @@ +import {expect, test} from 'bun:test'; +import { + getEffectiveVisualModeValue, + resolveDragOverrideValue, +} from '../get-effective-visual-mode-value'; +import { + computeEffectiveSchemaValuesDotNotation, + makeKeyframedDragOverride, + type CanUpdateSequencePropStatusKeyframed, +} from '../use-schema'; + +const makeKeyframedStatus = (): CanUpdateSequencePropStatusKeyframed => ({ + status: 'keyframed', + codeValue: undefined, + interpolationFunction: 'interpolate', + keyframes: [ + {frame: 0, value: 2}, + {frame: 60, value: 4}, + ], + easing: ['linear'], + clamping: {left: 'extend', right: 'extend'}, + posterize: undefined, +}); + +test('makeKeyframedDragOverride inserts a new keyframe and preserves easing length', () => { + const override = makeKeyframedDragOverride({ + status: makeKeyframedStatus(), + frame: 30, + value: 3, + }); + + expect(override).toEqual({ + type: 'keyframed', + status: { + ...makeKeyframedStatus(), + keyframes: [ + {frame: 0, value: 2}, + {frame: 30, value: 3}, + {frame: 60, value: 4}, + ], + easing: ['linear', 'linear'], + }, + }); + + expect( + resolveDragOverrideValue({dragOverrideValue: override, frame: 30}), + ).toEqual({ + type: 'resolved', + value: 3, + }); +}); + +test('makeKeyframedDragOverride replaces an existing keyframe without changing easing length', () => { + const override = makeKeyframedDragOverride({ + status: makeKeyframedStatus(), + frame: 60, + value: 5, + }); + + expect(override).toEqual({ + type: 'keyframed', + status: { + ...makeKeyframedStatus(), + keyframes: [ + {frame: 0, value: 2}, + {frame: 60, value: 5}, + ], + easing: ['linear'], + }, + }); +}); + +test('computeEffectiveSchemaValuesDotNotation resolves keyframed drag overrides at the current frame', () => { + const status = makeKeyframedStatus(); + const {merged} = computeEffectiveSchemaValuesDotNotation({ + schema: { + opacity: { + type: 'number', + default: 1, + hiddenFromList: false, + }, + }, + currentValue: {opacity: 2}, + overrideValues: { + opacity: makeKeyframedDragOverride({ + status, + frame: 30, + value: 3, + }), + }, + propStatus: { + opacity: status, + }, + frame: 30, + }); + + expect(merged.opacity).toBe(3); +}); + +test('getEffectiveVisualModeValue resolves keyframed drag overrides at the source frame', () => { + const status = makeKeyframedStatus(); + + expect( + getEffectiveVisualModeValue({ + codeValue: { + status: 'static', + codeValue: 2, + }, + dragOverrideValue: makeKeyframedDragOverride({ + status, + frame: 30, + value: 3, + }), + defaultValue: 1, + frame: 30, + shouldResortToDefaultValueIfUndefined: true, + }), + ).toBe(3); +}); diff --git a/packages/core/src/test/html-in-canvas.test.tsx b/packages/core/src/test/html-in-canvas.test.tsx index 49f8c472dc4..712c03ab560 100644 --- a/packages/core/src/test/html-in-canvas.test.tsx +++ b/packages/core/src/test/html-in-canvas.test.tsx @@ -2,6 +2,7 @@ import {afterEach, expect, test} from 'bun:test'; import {cleanup, render, waitFor} from '@testing-library/react'; import React, {useCallback, useMemo} from 'react'; import type {TSequence} from '../CompositionManager.js'; +import type {HtmlInCanvasOnPaintParams} from '../HtmlInCanvas.js'; import {HtmlInCanvas} from '../HtmlInCanvas.js'; import {Internals} from '../internals.js'; import type {SequenceManagerContext} from '../SequenceManager.js'; @@ -13,18 +14,57 @@ import { } from '../SequenceManager.js'; import {WrapSequenceContext} from './wrap-sequence-context.js'; -const stub2dContext = () => ({ - canvas: null as unknown as HTMLCanvasElement, - reset: () => undefined, - drawElementImage: () => new DOMMatrix(), - getImageData: () => ({ - data: new Uint8ClampedArray(4), - width: 1, - height: 1, - }), - putImageData: () => undefined, +class TestDOMMatrix { + private readonly scaleX: number; + private readonly scaleY: number; + + public constructor(scaleX = 1, scaleY = 1) { + this.scaleX = scaleX; + this.scaleY = scaleY; + } + + public scale(x: number, y: number) { + return new TestDOMMatrix(this.scaleX * x, this.scaleY * y); + } + + public multiply(other: TestDOMMatrix) { + return new TestDOMMatrix( + this.scaleX * other.scaleX, + this.scaleY * other.scaleY, + ); + } + + public toString() { + return `matrix(${this.scaleX}, 0, 0, ${this.scaleY}, 0, 0)`; + } +} + +Object.defineProperty(globalThis, 'DOMMatrix', { + configurable: true, + value: TestDOMMatrix, }); +const stub2dContext = () => { + let currentTransform = new DOMMatrix(); + + return { + canvas: null as unknown as HTMLCanvasElement, + reset: () => { + currentTransform = new DOMMatrix(); + }, + scale: (x: number, y: number) => { + currentTransform = currentTransform.scale(x, y); + }, + drawElementImage: () => currentTransform, + getImageData: () => ({ + data: new Uint8ClampedArray(4), + width: 1, + height: 1, + }), + putImageData: () => undefined, + }; +}; + Object.defineProperties(HTMLCanvasElement.prototype, { getContext: { configurable: true, @@ -220,3 +260,73 @@ test(' keeps refs current when the canvas remounts', async () => { expect(canvasRef.current).toBe(nextCanvas); expect(registeredSequences[0]?.refForOutline?.current).toBe(nextCanvas); }); + +test(' can use a higher backing density', async () => { + let paintParams: HtmlInCanvasOnPaintParams | undefined; + + const {container} = render( + undefined}> + { + paintParams = params; + }} + > +
Test
+
+
, + ); + + await waitFor(() => { + expect(container.querySelector('canvas')?.getAttribute('width')).toBe( + '100', + ); + }); + + const canvas = container.querySelector('canvas')!; + expect(canvas.getAttribute('height')).toBe('100'); + expect(canvas.style.width).toBe('50px'); + expect(canvas.style.height).toBe('50px'); + + canvas.dispatchEvent(new Event('paint')); + + await waitFor(() => { + expect(paintParams).not.toBeUndefined(); + }); + + if (!paintParams) { + throw new Error('Expected paint params to be captured'); + } + + expect(paintParams.canvas.width).toBe(100); + expect(paintParams.canvas.height).toBe(100); + expect(paintParams.pixelDensity).toBe(2); +}); + +test(' does not apply pixel density to the live DOM transform', async () => { + const {container} = render( + undefined}> + +
Test
+
+
, + ); + + await waitFor(() => { + expect(container.querySelector('canvas')?.getAttribute('width')).toBe( + '100', + ); + }); + + const canvas = container.querySelector('canvas')!; + canvas.dispatchEvent(new Event('paint')); + + const htmlInCanvasElement = canvas.querySelector('div'); + await waitFor(() => { + expect(htmlInCanvasElement?.style.transform).toBe( + new DOMMatrix().toString(), + ); + }); +}); diff --git a/packages/core/src/test/interpolate-keyframed-status.test.ts b/packages/core/src/test/interpolate-keyframed-status.test.ts index 74d04a16425..52e423c2fe5 100644 --- a/packages/core/src/test/interpolate-keyframed-status.test.ts +++ b/packages/core/src/test/interpolate-keyframed-status.test.ts @@ -20,6 +20,26 @@ test('interpolates linear numeric keyframes', () => { expect(result).toBe(50); }); +test('sorts keyframes before interpolating numeric values', () => { + const result = interpolateKeyframedStatus({ + frame: 75, + status: { + status: 'keyframed', + codeValue: undefined, + interpolationFunction: 'interpolate', + keyframes: [ + {frame: 100, value: 100}, + {frame: 50, value: 50}, + {frame: 0, value: 0}, + ], + easing: ['linear', 'linear'], + clamping: {left: 'extend', right: 'extend'}, + posterize: undefined, + }, + }); + expect(result).toBe(75); +}); + test('clamps when extrapolation is clamp', () => { const result = interpolateKeyframedStatus({ frame: 120, diff --git a/packages/core/src/test/offthread-video.test.tsx b/packages/core/src/test/offthread-video.test.tsx index 95b0d528b5e..956fbac4012 100644 --- a/packages/core/src/test/offthread-video.test.tsx +++ b/packages/core/src/test/offthread-video.test.tsx @@ -1,5 +1,5 @@ import {afterEach, describe, expect, test} from 'bun:test'; -import {cleanup, render} from '@testing-library/react'; +import {cleanup, fireEvent, render} from '@testing-library/react'; import type React from 'react'; import {SharedAudioContextProvider} from '../audio/shared-audio-tags.js'; import {RemotionEnvironmentContext} from '../remotion-environment-context.js'; @@ -198,6 +198,37 @@ describe('OffthreadVideo render correctly with props', () => { expect(container.querySelector('video')?.preservesPitch).toBe(false); }); + test('It should forward native props on OffthreadVideo', () => { + let clicks = 0; + + const {container} = render( + + { + clicks++; + }} + role="img" + src="https://example.com/test.mp4" + tabIndex={0} + title="Preview video" + /> + , + ); + + const video = container.querySelector('video'); + + expect(video?.getAttribute('aria-label')).toBe('Video preview'); + expect(video?.getAttribute('data-testid')).toBe('offthread-video'); + expect(video?.getAttribute('role')).toBe('img'); + expect(video?.getAttribute('tabindex')).toBe('0'); + expect(video?.getAttribute('title')).toBe('Preview video'); + + fireEvent.click(video as HTMLVideoElement); + expect(clicks).toBe(1); + }); + test('It should reject invalid preservePitch values on OffthreadVideo', () => { expect(() => render( diff --git a/packages/core/src/test/solid.test.tsx b/packages/core/src/test/solid.test.tsx index dbc9a2b602f..ffcc02006e3 100644 --- a/packages/core/src/test/solid.test.tsx +++ b/packages/core/src/test/solid.test.tsx @@ -88,3 +88,39 @@ test(' accepts an empty effects array', () => { expect(container.querySelector('canvas')).not.toBeNull(); }); + +test(' scales the backing canvas by pixelDensity while keeping logical CSS size', () => { + const {container} = render( + + + , + ); + + const canvas = container.querySelector('canvas'); + expect(canvas?.getAttribute('width')).toBe('240'); + expect(canvas?.getAttribute('height')).toBe('160'); + expect(canvas?.style.width).toBe('120px'); + expect(canvas?.style.height).toBe('80px'); +}); + +test(' rounds up the backing canvas for fractional pixelDensity', () => { + const {container} = render( + + + , + ); + + const canvas = container.querySelector('canvas'); + expect(canvas?.getAttribute('width')).toBe('152'); + expect(canvas?.getAttribute('height')).toBe('75'); +}); + +test(' throws for an invalid pixelDensity', () => { + expect(() => + render( + + + , + ), + ).toThrow(/`pixelDensity` must be a positive finite number/); +}); diff --git a/packages/core/src/test/timeline-position-state.test.ts b/packages/core/src/test/timeline-position-state.test.ts new file mode 100644 index 00000000000..47de6026777 --- /dev/null +++ b/packages/core/src/test/timeline-position-state.test.ts @@ -0,0 +1,10 @@ +import {expect, test} from 'bun:test'; +import {clampFrameToCompositionRange} from '../timeline-position-state'; + +test('clamps timeline frames to the composition range', () => { + expect(clampFrameToCompositionRange(-1, 100)).toBe(0); + expect(clampFrameToCompositionRange(0, 100)).toBe(0); + expect(clampFrameToCompositionRange(50, 100)).toBe(50); + expect(clampFrameToCompositionRange(99, 100)).toBe(99); + expect(clampFrameToCompositionRange(100, 100)).toBe(99); +}); diff --git a/packages/core/src/test/use-pixel-density.test.tsx b/packages/core/src/test/use-pixel-density.test.tsx new file mode 100644 index 00000000000..b668f109e68 --- /dev/null +++ b/packages/core/src/test/use-pixel-density.test.tsx @@ -0,0 +1,76 @@ +import {afterEach, expect, test} from 'bun:test'; +import {cleanup, render} from '@testing-library/react'; +import {CanUseRemotionHooksProvider} from '../CanUseRemotionHooks.js'; +import {PixelDensityContext, usePixelDensity} from '../use-pixel-density.js'; + +afterEach(cleanup); + +const setDevicePixelRatio = (value: number) => { + Object.defineProperty(window, 'devicePixelRatio', { + configurable: true, + value, + }); +}; + +test('usePixelDensity() reads the browser pixel density in a Remotion context', () => { + setDevicePixelRatio(2.5); + let pixelDensity = Number.NaN; + + const Component = () => { + pixelDensity = usePixelDensity(); + return null; + }; + + render( + + + , + ); + + expect(pixelDensity).toBe(2.5); +}); + +test('usePixelDensity() prefers an explicit pixel density context', () => { + setDevicePixelRatio(1); + let pixelDensity = Number.NaN; + + const Component = () => { + pixelDensity = usePixelDensity(); + return null; + }; + + render( + + + + + , + ); + + expect(pixelDensity).toBe(0.5); +}); + +test('usePixelDensity() throws outside a Remotion context', () => { + const Component = () => { + usePixelDensity(); + return null; + }; + + expect(() => render()).toThrow( + /usePixelDensity\(\) was called outside of a Remotion context/, + ); +}); + +test('usePixelDensity() can be called outside Remotion with dontThrowIfOutsideOfRemotion', () => { + setDevicePixelRatio(3); + let pixelDensity = Number.NaN; + + const Component = () => { + pixelDensity = usePixelDensity({dontThrowIfOutsideOfRemotion: true}); + return null; + }; + + render(); + + expect(pixelDensity).toBe(3); +}); diff --git a/packages/core/src/timeline-position-state.ts b/packages/core/src/timeline-position-state.ts index b8c4737327e..145bca0cecd 100644 --- a/packages/core/src/timeline-position-state.ts +++ b/packages/core/src/timeline-position-state.ts @@ -47,6 +47,13 @@ export const getFrameForComposition = (composition: string) => { return window.remotion_initialFrame ?? 0; }; +export const clampFrameToCompositionRange = ( + frame: number, + durationInFrames: number, +) => { + return Math.max(0, Math.min(Math.max(0, durationInFrames - 1), frame)); +}; + const useTimelinePositionFromContext = ( state: TimelineContextValue, ): number => { @@ -63,7 +70,7 @@ const useTimelinePositionFromContext = ( state.frame[videoConfig.id] ?? (env.isPlayer ? 0 : getFrameForComposition(videoConfig.id)); - return Math.min(videoConfig.durationInFrames - 1, unclamped); + return clampFrameToCompositionRange(unclamped, videoConfig.durationInFrames); }; export const useTimelineContext = (): TimelineContextValue => { diff --git a/packages/core/src/use-pixel-density.ts b/packages/core/src/use-pixel-density.ts new file mode 100644 index 00000000000..57904496ec5 --- /dev/null +++ b/packages/core/src/use-pixel-density.ts @@ -0,0 +1,42 @@ +import React, {useContext} from 'react'; +import {CanUseRemotionHooks} from './CanUseRemotionHooks.js'; + +type Options = { + dontThrowIfOutsideOfRemotion?: boolean; +}; + +export const PixelDensityContext = React.createContext(null); + +const getBrowserPixelDensity = () => { + if (typeof window === 'undefined') { + return 1; + } + + return window.devicePixelRatio || 1; +}; + +/* + * @description Retrieves the current pixel density. In previews, this corresponds to `window.devicePixelRatio`. During renders, this corresponds to the `scale` option. + * @see [Documentation](https://www.remotion.dev/docs/use-pixel-density) + */ +export const usePixelDensity = (options?: Options): number => { + const pixelDensity = useContext(PixelDensityContext); + const canUseRemotionHooks = useContext(CanUseRemotionHooks); + + if (pixelDensity !== null) { + return pixelDensity; + } + + if (canUseRemotionHooks || options?.dontThrowIfOutsideOfRemotion) { + return getBrowserPixelDensity(); + } + + throw new Error( + [ + 'usePixelDensity() was called outside of a Remotion context.', + 'This hook can only be called in a component that is being rendered by Remotion.', + 'If you want this hook to return the browser pixel density outside of Remotion, pass {dontThrowIfOutsideOfRemotion: true} as an option.', + 'If you think you called this hook in a Remotion component, make sure all versions of Remotion are aligned.', + ].join('\n'), + ); +}; diff --git a/packages/core/src/use-schema.ts b/packages/core/src/use-schema.ts index a7ca5590e0d..db9e2f0b17b 100644 --- a/packages/core/src/use-schema.ts +++ b/packages/core/src/use-schema.ts @@ -1,5 +1,8 @@ import {findPropsToDelete} from './find-props-to-delete.js'; -import {getEffectiveVisualModeValue} from './get-effective-visual-mode-value.js'; +import { + getEffectiveVisualModeValue, + resolveDragOverrideValue, +} from './get-effective-visual-mode-value.js'; import {interpolateKeyframedStatus} from './interpolate-keyframed-status.js'; import type {ExtrapolateType} from './interpolate.js'; import type { @@ -56,8 +59,21 @@ export type CanUpdateSequencePropStatus = | CanUpdateSequencePropStatusKeyframed | CanUpdateSequencePropStatusFalse; -export type DragOverrides = Record>; -export type EffectDragOverrides = Record>; +export type DragOverrideValue = + | { + readonly type: 'static'; + readonly value: unknown; + } + | { + readonly type: 'keyframed'; + readonly status: CanUpdateSequencePropStatusKeyframed; + }; + +export type DragOverrides = Record>; +export type EffectDragOverrides = Record< + string, + Record +>; export type CodeValues = Record; export type GetCodeValues = ( @@ -76,7 +92,60 @@ export type GetDragOverrides = ( export type GetEffectDragOverrides = ( nodePath: SequencePropsSubscriptionKey, effectIndex: number, -) => Record; +) => Record; + +export const makeStaticDragOverride = (value: unknown): DragOverrideValue => { + return {type: 'static', value}; +}; + +export const makeKeyframedDragOverride = ({ + status, + frame, + value, +}: { + status: CanUpdateSequencePropStatusKeyframed; + frame: number; + value: unknown; +}): DragOverrideValue => { + const existingIndex = status.keyframes.findIndex( + (keyframe) => keyframe.frame === frame, + ); + const keyframes = + existingIndex === -1 + ? [...status.keyframes, {frame, value}].sort( + (first, second) => first.frame - second.frame, + ) + : status.keyframes.map((keyframe, index) => + index === existingIndex ? {frame, value} : keyframe, + ); + const easing = [...status.easing]; + while (easing.length < keyframes.length - 1) { + easing.push('linear'); + } + + if (easing.length > keyframes.length - 1) { + easing.length = keyframes.length - 1; + } + + return { + type: 'keyframed', + status: { + ...status, + keyframes, + easing, + }, + }; +}; + +export const getStaticDragOverrideValue = ( + dragOverrideValue: DragOverrideValue | undefined, +): unknown => { + if (dragOverrideValue?.type !== 'static') { + return undefined; + } + + return dragOverrideValue.value; +}; export const isKeyframedStatus = ( status: CanUpdateSequencePropStatus | null, @@ -117,7 +186,7 @@ export const computeEffectiveSchemaValuesDotNotation = ({ }: { schema: SequenceSchema; currentValue: Record; - overrideValues: Record; + overrideValues: Record; propStatus: Record | undefined; frame: number | null; }): {merged: Record; propsToDelete: Set} => { @@ -135,14 +204,24 @@ export const computeEffectiveSchemaValuesDotNotation = ({ if (codeValueStatus === null) { value = currentValue[key]; } else if (isKeyframedStatus(codeValueStatus)) { - if (frame !== null) { - const interpolated = interpolateKeyframedStatus({ + if (field?.type === 'array' || field?.keyframable === false) { + value = currentValue[key]; + } else { + const dragOverride = resolveDragOverrideValue({ + dragOverrideValue: overrideValues[key], frame, - status: codeValueStatus, }); - value = interpolated ?? currentValue[key]; - } else { - value = currentValue[key]; + if (dragOverride.type === 'resolved') { + value = dragOverride.value; + } else if (frame !== null) { + const interpolated = interpolateKeyframedStatus({ + frame, + status: codeValueStatus, + }); + value = interpolated ?? currentValue[key]; + } else { + value = currentValue[key]; + } } } else if (codeValueStatus.status === 'computed') { value = currentValue[key]; @@ -151,6 +230,7 @@ export const computeEffectiveSchemaValuesDotNotation = ({ codeValue: codeValueStatus, dragOverrideValue: overrideValues[key], defaultValue: field?.default, + frame, shouldResortToDefaultValueIfUndefined: false, }); } diff --git a/packages/core/src/version.ts b/packages/core/src/version.ts index 84075bd32d0..6bd05af37bf 100644 --- a/packages/core/src/version.ts +++ b/packages/core/src/version.ts @@ -5,4 +5,4 @@ * @see [Documentation](https://remotion.dev/docs/version) * @returns {string} The current version of the remotion package */ -export const VERSION = '4.0.471'; +export const VERSION = '4.0.472'; diff --git a/packages/core/src/video/OffthreadVideo.tsx b/packages/core/src/video/OffthreadVideo.tsx index 4ccbbd40e33..1b743f35d2d 100644 --- a/packages/core/src/video/OffthreadVideo.tsx +++ b/packages/core/src/video/OffthreadVideo.tsx @@ -139,11 +139,9 @@ export const OffthreadVideo: React.FC = ({ acceptableTimeShiftInSeconds, allowAmplificationDuringRender, audioStreamIndex, - className, crossOrigin, delayRenderRetries, delayRenderTimeoutInMilliseconds, - id, loopVolumeCurveBehavior, muted, name, @@ -167,6 +165,7 @@ export const OffthreadVideo: React.FC = ({ stack, startFrom, imageFormat, + ...props }) => { if (imageFormat) { throw new TypeError( @@ -179,11 +178,9 @@ export const OffthreadVideo: React.FC = ({ acceptableTimeShiftInSeconds={acceptableTimeShiftInSeconds} allowAmplificationDuringRender={allowAmplificationDuringRender ?? true} audioStreamIndex={audioStreamIndex ?? 0} - className={className} crossOrigin={crossOrigin} delayRenderRetries={delayRenderRetries} delayRenderTimeoutInMilliseconds={delayRenderTimeoutInMilliseconds} - id={id} loopVolumeCurveBehavior={loopVolumeCurveBehavior ?? 'repeat'} muted={muted ?? false} name={name} @@ -209,6 +206,7 @@ export const OffthreadVideo: React.FC = ({ trimBefore={trimBefore} useWebAudioApi={useWebAudioApi ?? false} volume={volume} + {...props} /> ); }; diff --git a/packages/core/src/video/props.ts b/packages/core/src/video/props.ts index 8474679ba8b..74b0c95c5b6 100644 --- a/packages/core/src/video/props.ts +++ b/packages/core/src/video/props.ts @@ -72,9 +72,7 @@ type MandatoryOffthreadVideoProps = { }; type OptionalOffthreadVideoProps = { - className: string | undefined; name: string | undefined; - id: string | undefined; style: React.CSSProperties | undefined; volume: VolumeProp | undefined; playbackRate: number; @@ -102,14 +100,26 @@ type OptionalOffthreadVideoProps = { audioStreamIndex: number; }; +type NativeOffthreadVideoProps = Omit< + React.HTMLAttributes, + | keyof MandatoryOffthreadVideoProps + | keyof OptionalOffthreadVideoProps + | keyof CommonVideoProps + | keyof DeprecatedOffthreadVideoProps + | 'onError' +> & + Record<`data-${string}`, string | undefined>; + export type AllOffthreadVideoProps = MandatoryOffthreadVideoProps & OptionalOffthreadVideoProps & - CommonVideoProps; + CommonVideoProps & + NativeOffthreadVideoProps; export type RemotionOffthreadVideoProps = MandatoryOffthreadVideoProps & Partial & Partial & - Partial; + Partial & + NativeOffthreadVideoProps; export type OnVideoFrame = ( frame: CanvasImageSource, diff --git a/packages/create-video/package.json b/packages/create-video/package.json index bd6998cc5f8..3d0687d8024 100644 --- a/packages/create-video/package.json +++ b/packages/create-video/package.json @@ -3,7 +3,7 @@ "url": "https://github.com/remotion-dev/remotion/tree/main/packages/create-video" }, "name": "create-video", - "version": "4.0.471", + "version": "4.0.472", "description": "Create a new Remotion project", "main": "dist/index.js", "bin": { diff --git a/packages/design/package.json b/packages/design/package.json index 628a4d64a54..774456febb4 100644 --- a/packages/design/package.json +++ b/packages/design/package.json @@ -1,6 +1,6 @@ { "name": "@remotion/design", - "version": "4.0.471", + "version": "4.0.472", "main": "dist/index.js", "types": "dist/index.d.ts", "module": "dist/esm/index.mjs", diff --git a/packages/discord-poster/package.json b/packages/discord-poster/package.json index be1dec753b9..1e92753e3b8 100644 --- a/packages/discord-poster/package.json +++ b/packages/discord-poster/package.json @@ -3,7 +3,7 @@ "url": "https://github.com/remotion-dev/remotion/tree/main/packages/discord-poster" }, "name": "@remotion/discord-poster", - "version": "4.0.471", + "version": "4.0.472", "license": "SEE LICENSE IN LICENSE.md", "type": "module", "scripts": { diff --git a/packages/docs/components/effects-demos/get-default-props-from-schema.ts b/packages/docs/components/effects-demos/get-default-props-from-schema.ts index d1a9553377a..794862011a0 100644 --- a/packages/docs/components/effects-demos/get-default-props-from-schema.ts +++ b/packages/docs/components/effects-demos/get-default-props-from-schema.ts @@ -43,6 +43,14 @@ export const getDefaultValueFromSchema = ( return [0.5, 0.5] as const; } + if (field.type === 'array') { + if (field.default !== undefined) { + return field.default; + } + + return Array.from({length: field.minLength ?? 0}, () => field.newItemDefault); + } + return undefined; }; diff --git a/packages/docs/components/effects-demos/registry.ts b/packages/docs/components/effects-demos/registry.ts index 5669e112258..a59f51543f5 100644 --- a/packages/docs/components/effects-demos/registry.ts +++ b/packages/docs/components/effects-demos/registry.ts @@ -396,6 +396,7 @@ export const effectsDemos: EffectsDemoType[] = [ schema: starburstEffectSchema, initialValues: { rays: 16, + colors: ['#ff6600', '#ffff00'], }, }, { diff --git a/packages/docs/components/effects-demos/schema-control.tsx b/packages/docs/components/effects-demos/schema-control.tsx index c81cc9b2536..58f165a82e9 100644 --- a/packages/docs/components/effects-demos/schema-control.tsx +++ b/packages/docs/components/effects-demos/schema-control.tsx @@ -1,5 +1,5 @@ import React, {useMemo} from 'react'; -import type {SequenceFieldSchema} from 'remotion'; +import type {ArrayFieldSchema, SequenceFieldSchema} from 'remotion'; import styles from '../demos/styles.module.css'; import {getDefaultValueFromSchema} from './get-default-props-from-schema'; @@ -61,6 +61,18 @@ const getStringValue = (value: unknown, fallback: unknown): string => { return typeof fallback === 'string' ? fallback : ''; }; +const getStringOrNumberValue = (value: unknown, fallback: unknown): string => { + if (typeof value === 'string' || typeof value === 'number') { + return String(value); + } + + if (typeof fallback === 'string' || typeof fallback === 'number') { + return String(fallback); + } + + return ''; +}; + const getUvValue = ( value: unknown, field: Extract, @@ -76,6 +88,324 @@ const getUvValue = ( return getDefaultValueFromSchema(field) as readonly [number, number]; }; +const getArrayValue = ( + value: unknown, + field: ArrayFieldSchema, +): readonly unknown[] => { + if (Array.isArray(value)) { + return value; + } + + const fallback = getDefaultValueFromSchema(field); + return Array.isArray(fallback) ? fallback : []; +}; + +const getArrayItemLabel = (index: number): string => String(index); + +const getArrayItemKey = (value: unknown, index: number): string => { + return `${JSON.stringify(value)}-${index}`; +}; + +const getArrayItemValue = ( + value: unknown, + field: ArrayFieldSchema, +): unknown => { + if (value !== undefined) { + return value; + } + + return field.newItemDefault; +}; + +const isUvCoordinateValue = ( + value: unknown, +): value is readonly [number, number] => { + return ( + Array.isArray(value) && + value.length === 2 && + value.every((item) => typeof item === 'number') + ); +}; + +const makeArrayItemFieldSchema = ({ + field, + value, +}: { + readonly field: ArrayFieldSchema; + readonly value: unknown; +}): SequenceFieldSchema => { + const defaultValue = getArrayItemValue(value, field); + const {item} = field; + + if (item.type === 'number') { + return { + ...item, + default: typeof defaultValue === 'number' ? defaultValue : undefined, + hiddenFromList: false, + }; + } + + if (item.type === 'rotation-degrees') { + return { + ...item, + default: typeof defaultValue === 'number' ? defaultValue : undefined, + }; + } + + if (item.type === 'boolean') { + return { + ...item, + default: Boolean(defaultValue), + }; + } + + if (item.type === 'uv-coordinate') { + return { + ...item, + default: isUvCoordinateValue(defaultValue) ? defaultValue : undefined, + }; + } + + if ( + item.type === 'rotation-css' || + item.type === 'translate' || + item.type === 'color' + ) { + return { + ...item, + default: typeof defaultValue === 'string' ? defaultValue : undefined, + }; + } + + if (item.type === 'enum') { + return { + type: 'enum', + default: + typeof defaultValue === 'string' + ? defaultValue + : (item.variants[0] ?? ''), + variants: Object.fromEntries( + item.variants.map((variant) => [variant, {}]), + ), + }; + } + + throw new Error( + `Unsupported array item field: ${JSON.stringify(item satisfies never)}`, + ); +}; + +const SchemaControlInput = ({ + fieldKey, + field, + value, + setValue, + inputStyle, + textInputStyle, +}: { + readonly fieldKey: string; + readonly field: SequenceFieldSchema; + readonly value: unknown; + readonly setValue: (value: unknown) => void; + readonly inputStyle: React.CSSProperties; + readonly textInputStyle: React.CSSProperties; +}) => { + const numberRange = + field.type === 'number' || field.type === 'rotation-degrees' + ? getDemoNumberRange(fieldKey, field) + : null; + + if (field.type === 'array') { + return ( +
+ {getArrayValue(value, field).map((itemValue, index, array) => { + const itemField = makeArrayItemFieldSchema({ + field, + value: itemValue, + }); + const updateAtIndex = (nextItemValue: unknown) => { + setValue( + array.map((previousItem, itemIndex) => + itemIndex === index ? nextItemValue : previousItem, + ), + ); + }; + + const canRemove = array.length > (field.minLength ?? 0); + + return ( +
+ {getArrayItemLabel(index)} + + +
+ ); + })} + +
+ ); + } + + if (field.type === 'number' || field.type === 'rotation-degrees') { + return numberRange ? ( + setValue(Number(e.target.value))} + /> + ) : ( + setValue(Number(e.target.value))} + /> + ); + } + + if (field.type === 'uv-coordinate') { + return ( + <> + + setValue([ + Number(e.target.value), + getUvValue(value, field)[1], + ] as const) + } + /> + + setValue([ + getUvValue(value, field)[0], + Number(e.target.value), + ] as const) + } + /> + + ); + } + + if (field.type === 'enum') { + return ( +
+ +
+ ); + } + + if (field.type === 'boolean') { + return ( + { + setValue(event.target.checked); + }} + style={check} + checked={value as boolean} + type="checkbox" + /> + ); + } + + if (field.type === 'color') { + return ( + setValue(e.target.value)} + value={getStringValue(value, getDefaultValueFromSchema(field))} + /> + ); + } + + if (field.type === 'rotation-css' || field.type === 'translate') { + return ( + setValue(e.target.value)} + style={textInputStyle} + value={getStringValue(value, getDefaultValueFromSchema(field))} + /> + ); + } + + if (field.type === 'scale') { + return ( + setValue(e.target.value)} + style={textInputStyle} + value={getStringOrNumberValue(value, getDefaultValueFromSchema(field))} + /> + ); + } + + if (field.type === 'hidden') { + return null; + } + + throw new Error( + `Unsupported field: ${JSON.stringify(field satisfies never)}`, + ); +}; + export const SchemaControl = ({ fieldKey, field, @@ -117,67 +447,20 @@ export const SchemaControl = ({ } const label = field.description ?? fieldKey; - const numberRange = - field.type === 'number' || field.type === 'rotation-degrees' - ? getDemoNumberRange(fieldKey, field) - : null; return ( ); diff --git a/packages/docs/docs/animatedimage.mdx b/packages/docs/docs/animatedimage.mdx index 6372961b36b..46b149596a3 100644 --- a/packages/docs/docs/animatedimage.mdx +++ b/packages/docs/docs/animatedimage.mdx @@ -48,6 +48,10 @@ Remote images need to support [CORS](https://developer.mozilla.org/en-US/docs/We ::: +### `effects?` + +Apply [effects](/docs/effects/api) to each frame after it has been drawn to the canvas. + ### `width?` The display width. diff --git a/packages/docs/docs/canvasimage.mdx b/packages/docs/docs/canvasimage.mdx index f5b6ec030e0..3e66a309c2e 100644 --- a/packages/docs/docs/canvasimage.mdx +++ b/packages/docs/docs/canvasimage.mdx @@ -33,6 +33,10 @@ The URL of the image. Can be a remote URL or a local file path from [`staticFile Remote images need to support [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) because the image is drawn onto a canvas. +### `effects?` + +An array of Remotion [effects](/docs/effects/api) to apply to the image after it has been drawn to the source canvas. + ### `width?` The canvas width in pixels. @@ -51,10 +55,6 @@ Must be one of these values: - `'contain'`: The image is scaled to fit the canvas, while aspect ratio is maintained. - `'cover'`: The image completely fills the canvas and maintains its aspect ratio. It will be cropped if necessary. -### `effects?` - -An array of Remotion [effects](/docs/effects/api) to apply to the image after it has been drawn to the source canvas. - ### `className?` A class name to apply to the `` element. diff --git a/packages/docs/docs/effects.mdx b/packages/docs/docs/effects.mdx index bd409f6777d..586e22f64e4 100644 --- a/packages/docs/docs/effects.mdx +++ b/packages/docs/docs/effects.mdx @@ -14,12 +14,13 @@ Pixels of canvas-based components can be manipulated using effects. The following components support effects: - [``](/docs/solid) -- [``](/docs/html-in-canvas) +- [``](/docs/remotion/html-in-canvas) - [`