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%">
|
||||
<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>
|
||||
# OpenCut (Legacy)
|
||||
|
||||
## 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.
|
||||
|
||||
<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)
|
||||
|
||||
---
|
||||
|
||||

|
||||
The rewrite is happening at [opencut-app/opencut](https://github.com/opencut-app/opencut).
|
||||
|
||||
@@ -19,6 +19,17 @@ import {
|
||||
} from "@/preview/preview-snap";
|
||||
import type { TCanvasSize } from "@/project/types";
|
||||
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 type {
|
||||
ElementRef,
|
||||
@@ -50,7 +61,12 @@ interface PendingGesture extends CapturedPointerState {
|
||||
interface DragElementSnapshot {
|
||||
readonly trackId: 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 {
|
||||
@@ -204,8 +220,10 @@ function movedPastDragThreshold({
|
||||
|
||||
function toDragElementSnapshots({
|
||||
elementsWithTracks,
|
||||
currentTime,
|
||||
}: {
|
||||
elementsWithTracks: Array<{ track: TimelineTrack; element: TimelineElement }>;
|
||||
currentTime: number;
|
||||
}): DragElementSnapshot[] {
|
||||
const isVisualTrackedElement = (value: {
|
||||
track: TimelineTrack;
|
||||
@@ -215,11 +233,107 @@ function toDragElementSnapshots({
|
||||
|
||||
return elementsWithTracks
|
||||
.filter(isVisualTrackedElement)
|
||||
.map(({ track, element }) => ({
|
||||
trackId: track.id,
|
||||
elementId: element.id,
|
||||
initialTransform: element.transform,
|
||||
}));
|
||||
.map(({ track, element }) => {
|
||||
const localTime = mediaTime({
|
||||
ticks: getElementLocalTime({
|
||||
timelineTime: currentTime,
|
||||
elementStartTime: element.startTime,
|
||||
elementDuration: element.duration,
|
||||
}),
|
||||
});
|
||||
return {
|
||||
trackId: track.id,
|
||||
elementId: element.id,
|
||||
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 {
|
||||
@@ -489,6 +603,7 @@ export class PreviewInteractionController {
|
||||
elementsWithTracks: this.deps.timeline.getElementsWithTracks({
|
||||
elements: dragSelection,
|
||||
}),
|
||||
currentTime: this.deps.scene.getCurrentTime(),
|
||||
});
|
||||
|
||||
if (draggableElements.length === 0) {
|
||||
@@ -536,8 +651,8 @@ export class PreviewInteractionController {
|
||||
const deltaY = currentPos.y - drag.origin.y;
|
||||
|
||||
const proposedPosition = {
|
||||
x: firstElement.initialTransform.position.x + deltaX,
|
||||
y: firstElement.initialTransform.position.y + deltaY,
|
||||
x: firstElement.initialResolvedTransform.position.x + deltaX,
|
||||
y: firstElement.initialResolvedTransform.position.y + deltaY,
|
||||
};
|
||||
|
||||
const shouldSnap = !this.deps.input.isShiftHeld();
|
||||
@@ -560,23 +675,21 @@ export class PreviewInteractionController {
|
||||
this.deps.preview.onSnapLinesChange?.(activeLines);
|
||||
|
||||
const deltaSnappedX =
|
||||
snappedPosition.x - firstElement.initialTransform.position.x;
|
||||
snappedPosition.x - firstElement.initialResolvedTransform.position.x;
|
||||
const deltaSnappedY =
|
||||
snappedPosition.y - firstElement.initialTransform.position.y;
|
||||
snappedPosition.y - firstElement.initialResolvedTransform.position.y;
|
||||
|
||||
this.deps.timeline.previewElements(
|
||||
drag.elements.map(({ trackId, elementId, initialTransform }) => ({
|
||||
trackId,
|
||||
elementId,
|
||||
updates: {
|
||||
transform: {
|
||||
...initialTransform,
|
||||
position: {
|
||||
x: initialTransform.position.x + deltaSnappedX,
|
||||
y: initialTransform.position.y + deltaSnappedY,
|
||||
},
|
||||
drag.elements.map((snapshot) => ({
|
||||
trackId: snapshot.trackId,
|
||||
elementId: snapshot.elementId,
|
||||
updates: buildDragPositionUpdates({
|
||||
snapshot,
|
||||
position: {
|
||||
x: snapshot.initialResolvedTransform.position.x + deltaSnappedX,
|
||||
y: snapshot.initialResolvedTransform.position.y + deltaSnappedY,
|
||||
},
|
||||
},
|
||||
}),
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user