mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
add transform + few fixes
feat: transform feat: batch command fix: slider undo fix: timeline zoom perf refactor: make updateElement support multiple
This commit is contained in:
@@ -1,22 +1,11 @@
|
||||
import type { CanvasRenderer } from "../canvas-renderer";
|
||||
import { BaseNode } from "./base-node";
|
||||
import { VisualNode, type VisualNodeParams } from "./visual-node";
|
||||
|
||||
const IMAGE_EPSILON = 1 / 1000;
|
||||
|
||||
export interface ImageNodeParams {
|
||||
export interface ImageNodeParams extends VisualNodeParams {
|
||||
url: string;
|
||||
duration: number;
|
||||
timeOffset: number;
|
||||
trimStart: number;
|
||||
trimEnd: number;
|
||||
x?: number;
|
||||
y?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
opacity?: number;
|
||||
}
|
||||
|
||||
export class ImageNode extends BaseNode<ImageNodeParams> {
|
||||
export class ImageNode extends VisualNode<ImageNodeParams> {
|
||||
private image?: HTMLImageElement;
|
||||
private readyPromise: Promise<void>;
|
||||
|
||||
@@ -36,18 +25,6 @@ export class ImageNode extends BaseNode<ImageNodeParams> {
|
||||
});
|
||||
}
|
||||
|
||||
private getImageTime(time: number) {
|
||||
return time - this.params.timeOffset + this.params.trimStart;
|
||||
}
|
||||
|
||||
private isInRange(time: number) {
|
||||
const imageTime = this.getImageTime(time);
|
||||
return (
|
||||
imageTime >= this.params.trimStart - IMAGE_EPSILON &&
|
||||
imageTime < this.params.trimStart + this.params.duration
|
||||
);
|
||||
}
|
||||
|
||||
async render({ renderer, time }: { renderer: CanvasRenderer; time: number }) {
|
||||
await super.render({ renderer, time });
|
||||
|
||||
@@ -61,40 +38,14 @@ export class ImageNode extends BaseNode<ImageNodeParams> {
|
||||
return;
|
||||
}
|
||||
|
||||
renderer.context.save();
|
||||
const mediaW = this.image.naturalWidth || renderer.width;
|
||||
const mediaH = this.image.naturalHeight || renderer.height;
|
||||
|
||||
if (this.params.opacity !== undefined) {
|
||||
renderer.context.globalAlpha = this.params.opacity;
|
||||
}
|
||||
|
||||
if (
|
||||
this.params.x !== undefined &&
|
||||
this.params.y !== undefined &&
|
||||
this.params.width !== undefined &&
|
||||
this.params.height !== undefined
|
||||
) {
|
||||
renderer.context.drawImage(
|
||||
this.image,
|
||||
this.params.x,
|
||||
this.params.y,
|
||||
this.params.width,
|
||||
this.params.height,
|
||||
);
|
||||
} else {
|
||||
const mediaW = this.image.naturalWidth || renderer.width;
|
||||
const mediaH = this.image.naturalHeight || renderer.height;
|
||||
const containScale = Math.min(
|
||||
renderer.width / mediaW,
|
||||
renderer.height / mediaH,
|
||||
);
|
||||
const drawW = mediaW * containScale;
|
||||
const drawH = mediaH * containScale;
|
||||
const drawX = (renderer.width - drawW) / 2;
|
||||
const drawY = (renderer.height - drawH) / 2;
|
||||
|
||||
renderer.context.drawImage(this.image, drawX, drawY, drawW, drawH);
|
||||
}
|
||||
|
||||
renderer.context.restore();
|
||||
this.renderVisual({
|
||||
renderer,
|
||||
source: this.image,
|
||||
sourceWidth: mediaW,
|
||||
sourceHeight: mediaH,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,12 @@
|
||||
import type { CanvasRenderer } from "../canvas-renderer";
|
||||
import { BaseNode, type BaseNodeParams } from "./base-node";
|
||||
import type { Transform } from "@/types/timeline";
|
||||
import { VisualNode, type VisualNodeParams } from "./visual-node";
|
||||
|
||||
const STICKER_EPSILON = 1 / 1000;
|
||||
|
||||
export type StickerNodeParams = BaseNodeParams & {
|
||||
export interface StickerNodeParams extends VisualNodeParams {
|
||||
iconName: string;
|
||||
duration: number;
|
||||
timeOffset: number;
|
||||
trimStart: number;
|
||||
trimEnd: number;
|
||||
transform: Transform;
|
||||
opacity: number;
|
||||
color?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class StickerNode extends BaseNode<StickerNodeParams> {
|
||||
export class StickerNode extends VisualNode<StickerNodeParams> {
|
||||
private image?: HTMLImageElement;
|
||||
private readyPromise: Promise<void>;
|
||||
|
||||
@@ -40,18 +31,6 @@ export class StickerNode extends BaseNode<StickerNodeParams> {
|
||||
});
|
||||
}
|
||||
|
||||
private getStickerTime(time: number) {
|
||||
return time - this.params.timeOffset + this.params.trimStart;
|
||||
}
|
||||
|
||||
private isInRange(time: number) {
|
||||
const stickerTime = this.getStickerTime(time);
|
||||
return (
|
||||
stickerTime >= this.params.trimStart - STICKER_EPSILON &&
|
||||
stickerTime < this.params.trimStart + this.params.duration
|
||||
);
|
||||
}
|
||||
|
||||
async render({ renderer, time }: { renderer: CanvasRenderer; time: number }) {
|
||||
await super.render({ renderer, time });
|
||||
|
||||
@@ -65,23 +44,11 @@ export class StickerNode extends BaseNode<StickerNodeParams> {
|
||||
return;
|
||||
}
|
||||
|
||||
const { transform, opacity } = this.params;
|
||||
const size = 200 * transform.scale;
|
||||
const x = renderer.width / 2 + transform.position.x - size / 2;
|
||||
const y = renderer.height / 2 + transform.position.y - size / 2;
|
||||
|
||||
renderer.context.save();
|
||||
renderer.context.globalAlpha = opacity;
|
||||
|
||||
if (transform.rotate !== 0) {
|
||||
const centerX = x + size / 2;
|
||||
const centerY = y + size / 2;
|
||||
renderer.context.translate(centerX, centerY);
|
||||
renderer.context.rotate((transform.rotate * Math.PI) / 180);
|
||||
renderer.context.translate(-centerX, -centerY);
|
||||
}
|
||||
|
||||
renderer.context.drawImage(this.image, x, y, size, size);
|
||||
renderer.context.restore();
|
||||
this.renderVisual({
|
||||
renderer,
|
||||
source: this.image,
|
||||
sourceWidth: 200,
|
||||
sourceHeight: 200,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,14 @@
|
||||
import type { CanvasRenderer } from "../canvas-renderer";
|
||||
import { BaseNode } from "./base-node";
|
||||
import { VisualNode, type VisualNodeParams } from "./visual-node";
|
||||
import { videoCache } from "@/services/video-cache/service";
|
||||
|
||||
const VIDEO_EPSILON = 1 / 1000;
|
||||
|
||||
export interface VideoNodeParams {
|
||||
export interface VideoNodeParams extends VisualNodeParams {
|
||||
url: string;
|
||||
file: File;
|
||||
mediaId: string;
|
||||
duration: number;
|
||||
timeOffset: number;
|
||||
trimStart: number;
|
||||
trimEnd: number;
|
||||
x?: number;
|
||||
y?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
opacity?: number;
|
||||
}
|
||||
|
||||
export class VideoNode extends BaseNode<VideoNodeParams> {
|
||||
private getVideoTime(time: number) {
|
||||
return time - this.params.timeOffset + this.params.trimStart;
|
||||
}
|
||||
|
||||
private isInRange(time: number) {
|
||||
const videoTime = this.getVideoTime(time);
|
||||
return (
|
||||
videoTime >= this.params.trimStart - VIDEO_EPSILON &&
|
||||
videoTime < this.params.trimStart + this.params.duration
|
||||
);
|
||||
}
|
||||
|
||||
export class VideoNode extends VisualNode<VideoNodeParams> {
|
||||
async render({ renderer, time }: { renderer: CanvasRenderer; time: number }) {
|
||||
await super.render({ renderer, time });
|
||||
|
||||
@@ -39,7 +16,7 @@ export class VideoNode extends BaseNode<VideoNodeParams> {
|
||||
return;
|
||||
}
|
||||
|
||||
const videoTime = this.getVideoTime(time);
|
||||
const videoTime = this.getLocalTime(time);
|
||||
const frame = await videoCache.getFrameAt({
|
||||
mediaId: this.params.mediaId,
|
||||
file: this.params.file,
|
||||
@@ -47,36 +24,12 @@ export class VideoNode extends BaseNode<VideoNodeParams> {
|
||||
});
|
||||
|
||||
if (frame) {
|
||||
renderer.context.save();
|
||||
|
||||
if (this.params.opacity !== undefined) {
|
||||
renderer.context.globalAlpha = this.params.opacity;
|
||||
}
|
||||
|
||||
if (
|
||||
this.params.x !== undefined &&
|
||||
this.params.y !== undefined &&
|
||||
this.params.width !== undefined &&
|
||||
this.params.height !== undefined
|
||||
) {
|
||||
renderer.context.drawImage(
|
||||
frame.canvas,
|
||||
this.params.x,
|
||||
this.params.y,
|
||||
this.params.width,
|
||||
this.params.height,
|
||||
);
|
||||
} else {
|
||||
renderer.context.drawImage(
|
||||
frame.canvas,
|
||||
0,
|
||||
0,
|
||||
renderer.width,
|
||||
renderer.height,
|
||||
);
|
||||
}
|
||||
|
||||
renderer.context.restore();
|
||||
this.renderVisual({
|
||||
renderer,
|
||||
source: frame.canvas,
|
||||
sourceWidth: frame.canvas.width,
|
||||
sourceHeight: frame.canvas.height,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { CanvasRenderer } from "../canvas-renderer";
|
||||
import { BaseNode } from "./base-node";
|
||||
import type { Transform } from "@/types/timeline";
|
||||
|
||||
const VISUAL_EPSILON = 1 / 1000;
|
||||
|
||||
export interface VisualNodeParams {
|
||||
duration: number;
|
||||
timeOffset: number;
|
||||
trimStart: number;
|
||||
trimEnd: number;
|
||||
transform: Transform;
|
||||
opacity: number;
|
||||
}
|
||||
|
||||
export abstract class VisualNode<
|
||||
Params extends VisualNodeParams = VisualNodeParams,
|
||||
> extends BaseNode<Params> {
|
||||
protected getLocalTime(time: number): number {
|
||||
return time - this.params.timeOffset + this.params.trimStart;
|
||||
}
|
||||
|
||||
protected isInRange(time: number): boolean {
|
||||
const localTime = this.getLocalTime(time);
|
||||
return (
|
||||
localTime >= this.params.trimStart - VISUAL_EPSILON &&
|
||||
localTime < this.params.trimStart + this.params.duration
|
||||
);
|
||||
}
|
||||
|
||||
protected renderVisual({
|
||||
renderer,
|
||||
source,
|
||||
sourceWidth,
|
||||
sourceHeight,
|
||||
}: {
|
||||
renderer: CanvasRenderer;
|
||||
source: CanvasImageSource;
|
||||
sourceWidth: number;
|
||||
sourceHeight: number;
|
||||
}): void {
|
||||
renderer.context.save();
|
||||
|
||||
const { transform, opacity } = this.params;
|
||||
const containScale = Math.min(
|
||||
renderer.width / sourceWidth,
|
||||
renderer.height / sourceHeight,
|
||||
);
|
||||
const scaledWidth = sourceWidth * containScale * transform.scale;
|
||||
const scaledHeight = sourceHeight * containScale * transform.scale;
|
||||
const x = renderer.width / 2 + transform.position.x - scaledWidth / 2;
|
||||
const y = renderer.height / 2 + transform.position.y - scaledHeight / 2;
|
||||
|
||||
renderer.context.globalAlpha = opacity;
|
||||
|
||||
if (transform.rotate !== 0) {
|
||||
const centerX = x + scaledWidth / 2;
|
||||
const centerY = y + scaledHeight / 2;
|
||||
renderer.context.translate(centerX, centerY);
|
||||
renderer.context.rotate((transform.rotate * Math.PI) / 180);
|
||||
renderer.context.translate(-centerX, -centerY);
|
||||
}
|
||||
|
||||
renderer.context.drawImage(source, x, y, scaledWidth, scaledHeight);
|
||||
renderer.context.restore();
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,8 @@ export function buildScene(params: BuildSceneParams) {
|
||||
timeOffset: element.startTime,
|
||||
trimStart: element.trimStart,
|
||||
trimEnd: element.trimEnd,
|
||||
transform: element.transform,
|
||||
opacity: element.opacity,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -75,6 +77,8 @@ export function buildScene(params: BuildSceneParams) {
|
||||
timeOffset: element.startTime,
|
||||
trimStart: element.trimStart,
|
||||
trimEnd: element.trimEnd,
|
||||
transform: element.transform,
|
||||
opacity: element.opacity,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user