mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdb4dff755 | ||
|
|
6d96674dc2 | ||
|
|
0d35499356 |
@@ -1,167 +1,5 @@
|
|||||||
<table width="100%">
|
# OpenCut (Legacy)
|
||||||
<tr>
|
|
||||||
<td align="left" width="120">
|
|
||||||
<img src="apps/web/public/logos/opencut/icon.svg" alt="OpenCut Logo" width="100" />
|
|
||||||
</td>
|
|
||||||
<td align="right">
|
|
||||||
<h1>OpenCut</h1>
|
|
||||||
<h3 style="margin-top: -10px;">A free, open-source video editor for web, desktop, and mobile.</h3>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
## Sponsors
|
This is the original OpenCut codebase. It's archived and no longer maintained.
|
||||||
|
|
||||||
Thanks to [Vercel](https://vercel.com?utm_source=github-opencut&utm_campaign=oss) and [fal.ai](https://fal.ai?utm_source=github-opencut&utm_campaign=oss) for their support of open-source software.
|
The rewrite is happening at [opencut-app/opencut](https://github.com/opencut-app/opencut).
|
||||||
|
|
||||||
<a href="https://vercel.com/oss">
|
|
||||||
<img alt="Vercel OSS Program" src="https://vercel.com/oss/program-badge.svg" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="https://fal.ai">
|
|
||||||
<img alt="Powered by fal.ai" src="https://img.shields.io/badge/Powered%20by-fal.ai-000000?style=flat&logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDJMMTMuMDkgOC4yNkwyMCAxMEwxMy4wOSAxNS43NEwxMiAyMkwxMC45MSAxNS43NEw0IDEwTDEwLjkxIDguMjZMMTIgMloiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
## Why?
|
|
||||||
|
|
||||||
- **Privacy**: Your videos stay on your device
|
|
||||||
- **Free features**: Most basic CapCut features are now paywalled
|
|
||||||
- **Simple**: People want editors that are easy to use - CapCut proved that
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
- `apps/web/`: Next.js web application
|
|
||||||
- `apps/desktop/`: Native desktop app built with GPUI (in progress)
|
|
||||||
- `rust/`: Platform-agnostic core: GPU compositor, effects, masks, and WASM bindings. We're actively migrating business logic here from TypeScript.
|
|
||||||
- `docs/`: Architecture and subsystem documentation
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- [Bun](https://bun.sh/docs/installation)
|
|
||||||
- [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/)
|
|
||||||
|
|
||||||
> **Note:** Docker is optional but recommended for running the local database and Redis. If you only want to work on frontend features, you can skip it.
|
|
||||||
|
|
||||||
### Setup
|
|
||||||
|
|
||||||
1. Fork and clone the repository
|
|
||||||
|
|
||||||
2. Copy the environment file:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Unix/Linux/Mac
|
|
||||||
cp apps/web/.env.example apps/web/.env.local
|
|
||||||
|
|
||||||
# Windows PowerShell
|
|
||||||
Copy-Item apps/web/.env.example apps/web/.env.local
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Start the database and Redis:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose up -d db redis serverless-redis-http
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Install dependencies and start the dev server:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bun install
|
|
||||||
bun dev:web
|
|
||||||
```
|
|
||||||
|
|
||||||
The application will be available at [http://localhost:3000](http://localhost:3000).
|
|
||||||
|
|
||||||
The `.env.example` has sensible defaults that match the Docker Compose config — it should work out of the box.
|
|
||||||
|
|
||||||
### Desktop setup
|
|
||||||
|
|
||||||
Desktop is opt-in. If you're only working on the web app, skip this entirely.
|
|
||||||
|
|
||||||
If you want to get ready for `apps/desktop`, see [`apps/desktop/README.md`](apps/desktop/README.md). It's a two-step setup: Rust toolchain first, then desktop native dependencies.
|
|
||||||
|
|
||||||
### Local WASM development
|
|
||||||
|
|
||||||
Only needed if you're editing `rust/wasm` and want the web app to use your local build instead of the published package.
|
|
||||||
|
|
||||||
**Prerequisites** — install these once before anything else:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Rust toolchain
|
|
||||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
|
||||||
|
|
||||||
# build the WASM package
|
|
||||||
cargo install wasm-pack
|
|
||||||
|
|
||||||
# reruns the build on file changes, used by bun dev:wasm
|
|
||||||
cargo install cargo-watch
|
|
||||||
```
|
|
||||||
|
|
||||||
1. Build the package once from the repo root:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bun run build:wasm
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Register the generated package for linking:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd rust/wasm/pkg
|
|
||||||
bun link
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Link `apps/web` to the local package:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd apps/web
|
|
||||||
bun link opencut-wasm
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Rebuild on changes while you work:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bun dev:wasm
|
|
||||||
```
|
|
||||||
|
|
||||||
To switch `apps/web` back to the published package, run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd apps/web
|
|
||||||
bun add opencut-wasm
|
|
||||||
```
|
|
||||||
|
|
||||||
### Self-Hosting with Docker
|
|
||||||
|
|
||||||
To run everything (including a production build of the app) in Docker:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
The app will be available at [http://localhost:3100](http://localhost:3100).
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
We welcome contributions! While we're actively developing and refactoring certain areas, there are plenty of opportunities to contribute effectively.
|
|
||||||
|
|
||||||
**🎯 Focus areas:** Timeline functionality, project management, performance, bug fixes, and UI improvements outside the preview panel.
|
|
||||||
|
|
||||||
**⚠️ Avoid for now:** Preview panel enhancements (fonts, stickers, effects) and export functionality - we're refactoring these with a new binary rendering approach.
|
|
||||||
|
|
||||||
See our [Contributing Guide](.github/CONTRIBUTING.md) for detailed setup instructions, development guidelines, and complete focus area guidance.
|
|
||||||
|
|
||||||
**Quick start for contributors:**
|
|
||||||
|
|
||||||
- Fork the repo and clone locally
|
|
||||||
- Follow the setup instructions in CONTRIBUTING.md
|
|
||||||
- Working on `apps/desktop`? See [`apps/desktop/README.md`](apps/desktop/README.md) for setup
|
|
||||||
- Create a feature branch and submit a PR
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
[MIT LICENSE](LICENSE)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||

|
|
||||||
|
|||||||
@@ -19,6 +19,17 @@ import {
|
|||||||
} from "@/preview/preview-snap";
|
} from "@/preview/preview-snap";
|
||||||
import type { TCanvasSize } from "@/project/types";
|
import type { TCanvasSize } from "@/project/types";
|
||||||
import type { Transform } from "@/rendering";
|
import type { Transform } from "@/rendering";
|
||||||
|
import { resolveTransformAtTime } from "@/rendering/animation-values";
|
||||||
|
import {
|
||||||
|
getElementLocalTime,
|
||||||
|
hasKeyframesForPath,
|
||||||
|
upsertElementKeyframe,
|
||||||
|
} from "@/animation";
|
||||||
|
import type {
|
||||||
|
AnimationPropertyPath,
|
||||||
|
ElementAnimations,
|
||||||
|
} from "@/animation/types";
|
||||||
|
import { mediaTime, type MediaTime } from "@/wasm";
|
||||||
import { isVisualElement } from "@/timeline/element-utils";
|
import { isVisualElement } from "@/timeline/element-utils";
|
||||||
import type {
|
import type {
|
||||||
ElementRef,
|
ElementRef,
|
||||||
@@ -50,7 +61,12 @@ interface PendingGesture extends CapturedPointerState {
|
|||||||
interface DragElementSnapshot {
|
interface DragElementSnapshot {
|
||||||
readonly trackId: string;
|
readonly trackId: string;
|
||||||
readonly elementId: string;
|
readonly elementId: string;
|
||||||
readonly initialTransform: Transform;
|
readonly initialBaseTransform: Transform;
|
||||||
|
readonly initialResolvedTransform: Transform;
|
||||||
|
readonly animations: ElementAnimations | undefined;
|
||||||
|
readonly localTime: MediaTime;
|
||||||
|
readonly hasAnimatedPositionX: boolean;
|
||||||
|
readonly hasAnimatedPositionY: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DraggingGesture extends CapturedPointerState {
|
interface DraggingGesture extends CapturedPointerState {
|
||||||
@@ -204,8 +220,10 @@ function movedPastDragThreshold({
|
|||||||
|
|
||||||
function toDragElementSnapshots({
|
function toDragElementSnapshots({
|
||||||
elementsWithTracks,
|
elementsWithTracks,
|
||||||
|
currentTime,
|
||||||
}: {
|
}: {
|
||||||
elementsWithTracks: Array<{ track: TimelineTrack; element: TimelineElement }>;
|
elementsWithTracks: Array<{ track: TimelineTrack; element: TimelineElement }>;
|
||||||
|
currentTime: number;
|
||||||
}): DragElementSnapshot[] {
|
}): DragElementSnapshot[] {
|
||||||
const isVisualTrackedElement = (value: {
|
const isVisualTrackedElement = (value: {
|
||||||
track: TimelineTrack;
|
track: TimelineTrack;
|
||||||
@@ -215,11 +233,107 @@ function toDragElementSnapshots({
|
|||||||
|
|
||||||
return elementsWithTracks
|
return elementsWithTracks
|
||||||
.filter(isVisualTrackedElement)
|
.filter(isVisualTrackedElement)
|
||||||
.map(({ track, element }) => ({
|
.map(({ track, element }) => {
|
||||||
|
const localTime = mediaTime({
|
||||||
|
ticks: getElementLocalTime({
|
||||||
|
timelineTime: currentTime,
|
||||||
|
elementStartTime: element.startTime,
|
||||||
|
elementDuration: element.duration,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
return {
|
||||||
trackId: track.id,
|
trackId: track.id,
|
||||||
elementId: element.id,
|
elementId: element.id,
|
||||||
initialTransform: element.transform,
|
initialBaseTransform: element.transform,
|
||||||
}));
|
initialResolvedTransform: resolveTransformAtTime({
|
||||||
|
baseTransform: element.transform,
|
||||||
|
animations: element.animations,
|
||||||
|
localTime,
|
||||||
|
}),
|
||||||
|
animations: element.animations,
|
||||||
|
localTime,
|
||||||
|
hasAnimatedPositionX: hasKeyframesForPath({
|
||||||
|
animations: element.animations,
|
||||||
|
propertyPath: "transform.positionX",
|
||||||
|
}),
|
||||||
|
hasAnimatedPositionY: hasKeyframesForPath({
|
||||||
|
animations: element.animations,
|
||||||
|
propertyPath: "transform.positionY",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function upsertPositionKeyframe({
|
||||||
|
animations,
|
||||||
|
propertyPath,
|
||||||
|
localTime,
|
||||||
|
value,
|
||||||
|
}: {
|
||||||
|
animations: ElementAnimations | undefined;
|
||||||
|
propertyPath: AnimationPropertyPath;
|
||||||
|
localTime: MediaTime;
|
||||||
|
value: number;
|
||||||
|
}): ElementAnimations | undefined {
|
||||||
|
return upsertElementKeyframe({
|
||||||
|
animations,
|
||||||
|
propertyPath,
|
||||||
|
time: localTime,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDragPositionUpdates({
|
||||||
|
snapshot,
|
||||||
|
position,
|
||||||
|
}: {
|
||||||
|
snapshot: DragElementSnapshot;
|
||||||
|
position: Point;
|
||||||
|
}): Partial<VisualElement> {
|
||||||
|
const hasAnimatedPosition =
|
||||||
|
snapshot.hasAnimatedPositionX || snapshot.hasAnimatedPositionY;
|
||||||
|
const hasStaticPosition =
|
||||||
|
!snapshot.hasAnimatedPositionX || !snapshot.hasAnimatedPositionY;
|
||||||
|
let animations = snapshot.animations;
|
||||||
|
const updates: Partial<VisualElement> = {};
|
||||||
|
|
||||||
|
if (snapshot.hasAnimatedPositionX) {
|
||||||
|
animations = upsertPositionKeyframe({
|
||||||
|
animations,
|
||||||
|
propertyPath: "transform.positionX",
|
||||||
|
localTime: snapshot.localTime,
|
||||||
|
value: position.x,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.hasAnimatedPositionY) {
|
||||||
|
animations = upsertPositionKeyframe({
|
||||||
|
animations,
|
||||||
|
propertyPath: "transform.positionY",
|
||||||
|
localTime: snapshot.localTime,
|
||||||
|
value: position.y,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasAnimatedPosition) {
|
||||||
|
updates.animations = animations;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasStaticPosition) {
|
||||||
|
updates.transform = {
|
||||||
|
...snapshot.initialBaseTransform,
|
||||||
|
position: {
|
||||||
|
x: snapshot.hasAnimatedPositionX
|
||||||
|
? snapshot.initialBaseTransform.position.x
|
||||||
|
: position.x,
|
||||||
|
y: snapshot.hasAnimatedPositionY
|
||||||
|
? snapshot.initialBaseTransform.position.y
|
||||||
|
: position.y,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return updates;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PreviewInteractionController {
|
export class PreviewInteractionController {
|
||||||
@@ -489,6 +603,7 @@ export class PreviewInteractionController {
|
|||||||
elementsWithTracks: this.deps.timeline.getElementsWithTracks({
|
elementsWithTracks: this.deps.timeline.getElementsWithTracks({
|
||||||
elements: dragSelection,
|
elements: dragSelection,
|
||||||
}),
|
}),
|
||||||
|
currentTime: this.deps.scene.getCurrentTime(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (draggableElements.length === 0) {
|
if (draggableElements.length === 0) {
|
||||||
@@ -536,8 +651,8 @@ export class PreviewInteractionController {
|
|||||||
const deltaY = currentPos.y - drag.origin.y;
|
const deltaY = currentPos.y - drag.origin.y;
|
||||||
|
|
||||||
const proposedPosition = {
|
const proposedPosition = {
|
||||||
x: firstElement.initialTransform.position.x + deltaX,
|
x: firstElement.initialResolvedTransform.position.x + deltaX,
|
||||||
y: firstElement.initialTransform.position.y + deltaY,
|
y: firstElement.initialResolvedTransform.position.y + deltaY,
|
||||||
};
|
};
|
||||||
|
|
||||||
const shouldSnap = !this.deps.input.isShiftHeld();
|
const shouldSnap = !this.deps.input.isShiftHeld();
|
||||||
@@ -560,23 +675,21 @@ export class PreviewInteractionController {
|
|||||||
this.deps.preview.onSnapLinesChange?.(activeLines);
|
this.deps.preview.onSnapLinesChange?.(activeLines);
|
||||||
|
|
||||||
const deltaSnappedX =
|
const deltaSnappedX =
|
||||||
snappedPosition.x - firstElement.initialTransform.position.x;
|
snappedPosition.x - firstElement.initialResolvedTransform.position.x;
|
||||||
const deltaSnappedY =
|
const deltaSnappedY =
|
||||||
snappedPosition.y - firstElement.initialTransform.position.y;
|
snappedPosition.y - firstElement.initialResolvedTransform.position.y;
|
||||||
|
|
||||||
this.deps.timeline.previewElements(
|
this.deps.timeline.previewElements(
|
||||||
drag.elements.map(({ trackId, elementId, initialTransform }) => ({
|
drag.elements.map((snapshot) => ({
|
||||||
trackId,
|
trackId: snapshot.trackId,
|
||||||
elementId,
|
elementId: snapshot.elementId,
|
||||||
updates: {
|
updates: buildDragPositionUpdates({
|
||||||
transform: {
|
snapshot,
|
||||||
...initialTransform,
|
|
||||||
position: {
|
position: {
|
||||||
x: initialTransform.position.x + deltaSnappedX,
|
x: snapshot.initialResolvedTransform.position.x + deltaSnappedX,
|
||||||
y: initialTransform.position.y + deltaSnappedY,
|
y: snapshot.initialResolvedTransform.position.y + deltaSnappedY,
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
}),
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user