mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
stuff
This commit is contained in:
@@ -36,7 +36,7 @@ export class ImageNode extends BaseNode<ImageNodeParams> {
|
||||
const imageTime = this.getImageTime(time);
|
||||
return (
|
||||
imageTime >= this.params.trimStart - IMAGE_EPSILON &&
|
||||
imageTime < this.params.duration - this.params.trimEnd
|
||||
imageTime < this.params.trimStart + this.params.duration
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { CanvasRenderer } from "../canvas-renderer";
|
||||
import { BaseNode, BaseNodeParams } from "./base-node";
|
||||
import { Transform } from "@/types/timeline";
|
||||
|
||||
const STICKER_EPSILON = 1 / 1000;
|
||||
|
||||
export type StickerNodeParams = BaseNodeParams & {
|
||||
iconName: string;
|
||||
duration: number;
|
||||
timeOffset: number;
|
||||
trimStart: number;
|
||||
trimEnd: number;
|
||||
transform: Transform;
|
||||
opacity: number;
|
||||
color?: string;
|
||||
};
|
||||
|
||||
export class StickerNode extends BaseNode<StickerNodeParams> {
|
||||
private image?: HTMLImageElement;
|
||||
private readyPromise: Promise<void>;
|
||||
|
||||
constructor(params: StickerNodeParams) {
|
||||
super(params);
|
||||
this.readyPromise = this.load();
|
||||
}
|
||||
|
||||
private async load() {
|
||||
this.image = new Image();
|
||||
const color = this.params.color
|
||||
? `&color=${encodeURIComponent(this.params.color)}`
|
||||
: "";
|
||||
const url = `https://api.iconify.design/${this.params.iconName}.svg?width=200&height=200${color}`;
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
this.image!.onload = () => resolve();
|
||||
this.image!.onerror = () =>
|
||||
reject(new Error(`Failed to load sticker: ${this.params.iconName}`));
|
||||
this.image!.src = url;
|
||||
});
|
||||
}
|
||||
|
||||
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 });
|
||||
|
||||
if (!this.isInRange(time)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.readyPromise;
|
||||
|
||||
if (!this.image) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -3,16 +3,15 @@ import { BaseNode } from "./base-node";
|
||||
import { TextElement } from "@/types/timeline";
|
||||
|
||||
export type TextNodeParams = TextElement & {
|
||||
canvasCenter: { x: number; y: number };
|
||||
textBaseline?: CanvasTextBaseline;
|
||||
};
|
||||
|
||||
export class TextNode extends BaseNode<TextNodeParams> {
|
||||
isInRange({ time }: { time: number }) {
|
||||
const visibleDuration =
|
||||
this.params.duration - this.params.trimStart - this.params.trimEnd;
|
||||
return (
|
||||
time >= this.params.startTime &&
|
||||
time < this.params.startTime + visibleDuration
|
||||
time < this.params.startTime + this.params.duration
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,9 +22,12 @@ export class TextNode extends BaseNode<TextNodeParams> {
|
||||
|
||||
renderer.context.save();
|
||||
|
||||
renderer.context.translate(this.params.x, this.params.y);
|
||||
if (this.params.rotation) {
|
||||
renderer.context.rotate((this.params.rotation * Math.PI) / 180);
|
||||
const x = this.params.transform.position.x + this.params.canvasCenter.x;
|
||||
const y = this.params.transform.position.y + this.params.canvasCenter.y;
|
||||
|
||||
renderer.context.translate(x, y);
|
||||
if (this.params.transform.rotate) {
|
||||
renderer.context.rotate((this.params.transform.rotate * Math.PI) / 180);
|
||||
}
|
||||
|
||||
const fontWeight = this.params.fontWeight === "bold" ? "bold" : "normal";
|
||||
|
||||
@@ -54,7 +54,7 @@ export class VideoNode extends BaseNode<VideoNodeParams> {
|
||||
const videoTime = this.getVideoTime(time);
|
||||
return (
|
||||
videoTime >= this.params.trimStart - VIDEO_EPSILON &&
|
||||
videoTime < this.params.duration - this.params.trimEnd
|
||||
videoTime < this.params.trimStart + this.params.duration
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,64 +1,61 @@
|
||||
import { type TimelineTrack } from "@/types/timeline";
|
||||
import { type MediaFile } from "@/types/assets";
|
||||
import type { TimelineTrack, TimelineElement } from "@/types/timeline";
|
||||
import { MediaAsset } from "@/types/assets";
|
||||
import { RootNode } from "./nodes/root-node";
|
||||
import { VideoNode } from "./nodes/video-node";
|
||||
import { ImageNode } from "./nodes/image-node";
|
||||
import { TextNode } from "./nodes/text-node";
|
||||
import { StickerNode } from "./nodes/sticker-node";
|
||||
import { ColorNode } from "./nodes/color-node";
|
||||
import { BlurBackgroundNode } from "./nodes/blur-background-node";
|
||||
import { TBackgroundType } from "@/types/project";
|
||||
import { DEFAULT_BLUR_INTENSITY } from "@/constants/editor-constants";
|
||||
import { TBackground, TCanvasSize } from "@/types/project";
|
||||
import { DEFAULT_BLUR_INTENSITY } from "@/constants/project-constants";
|
||||
|
||||
export type BuildSceneParams = {
|
||||
canvasSize: { width: number; height: number };
|
||||
canvasSize: TCanvasSize;
|
||||
tracks: TimelineTrack[];
|
||||
mediaFiles: MediaFile[];
|
||||
mediaAssets: MediaAsset[];
|
||||
duration: number;
|
||||
backgroundColor?: string;
|
||||
backgroundType?: TBackgroundType;
|
||||
blurIntensity?: number;
|
||||
background: TBackground;
|
||||
};
|
||||
|
||||
export function buildScene(params: BuildSceneParams) {
|
||||
const {
|
||||
tracks,
|
||||
mediaFiles,
|
||||
mediaAssets,
|
||||
duration,
|
||||
canvasSize,
|
||||
backgroundColor,
|
||||
backgroundType,
|
||||
blurIntensity,
|
||||
background
|
||||
} = params;
|
||||
|
||||
const rootNode = new RootNode({ duration });
|
||||
const mediaMap = new Map(mediaFiles.map((m) => [m.id, m]));
|
||||
const mediaMap = new Map(mediaAssets.map((m) => [m.id, m]));
|
||||
|
||||
const elements = tracks
|
||||
.slice()
|
||||
.reverse()
|
||||
.filter((track) => !track.muted)
|
||||
.flatMap((track) => track.elements);
|
||||
.flatMap((track): TimelineElement[] => track.elements);
|
||||
|
||||
const contentNodes = [];
|
||||
|
||||
for (const element of elements) {
|
||||
if (element.type === "media") {
|
||||
const media = mediaMap.get(element.mediaId);
|
||||
if (media && media.file) {
|
||||
if (media.type === "video") {
|
||||
if (element.type === "video" || element.type === "image") {
|
||||
const mediaAsset = mediaMap.get(element.mediaId);
|
||||
if (mediaAsset && mediaAsset.file) {
|
||||
if (mediaAsset.type === "video") {
|
||||
contentNodes.push(
|
||||
new VideoNode({
|
||||
file: media.file,
|
||||
file: mediaAsset.file,
|
||||
duration: element.duration,
|
||||
timeOffset: element.startTime,
|
||||
trimStart: element.trimStart,
|
||||
trimEnd: element.trimEnd,
|
||||
}),
|
||||
);
|
||||
} else if (media.type === "image") {
|
||||
} else if (mediaAsset.type === "image") {
|
||||
contentNodes.push(
|
||||
new ImageNode({
|
||||
file: media.file,
|
||||
file: mediaAsset.file,
|
||||
duration: element.duration,
|
||||
timeOffset: element.startTime,
|
||||
trimStart: element.trimStart,
|
||||
@@ -71,27 +68,40 @@ export function buildScene(params: BuildSceneParams) {
|
||||
}
|
||||
|
||||
if (element.type === "text") {
|
||||
const textElement = element;
|
||||
contentNodes.push(
|
||||
new TextNode({
|
||||
...textElement,
|
||||
x: textElement.x + canvasSize.width / 2,
|
||||
y: textElement.y + canvasSize.height / 2,
|
||||
...element,
|
||||
canvasCenter: { x: canvasSize.width / 2, y: canvasSize.height / 2 },
|
||||
textBaseline: "middle",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (element.type === "sticker") {
|
||||
contentNodes.push(
|
||||
new StickerNode({
|
||||
iconName: element.iconName,
|
||||
duration: element.duration,
|
||||
timeOffset: element.startTime,
|
||||
trimStart: element.trimStart,
|
||||
trimEnd: element.trimEnd,
|
||||
transform: element.transform,
|
||||
opacity: element.opacity,
|
||||
color: element.color,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (backgroundType === "blur") {
|
||||
if (background.type === "blur") {
|
||||
rootNode.add(
|
||||
new BlurBackgroundNode({
|
||||
blurIntensity: blurIntensity ?? DEFAULT_BLUR_INTENSITY,
|
||||
blurIntensity: background.blurIntensity ?? DEFAULT_BLUR_INTENSITY,
|
||||
contentNodes,
|
||||
}),
|
||||
);
|
||||
} else if (backgroundColor && backgroundColor !== "transparent") {
|
||||
rootNode.add(new ColorNode({ color: backgroundColor }));
|
||||
} else if (background.type === "color" && background.color !== "transparent") {
|
||||
rootNode.add(new ColorNode({ color: background.color }));
|
||||
}
|
||||
|
||||
for (const node of contentNodes) {
|
||||
|
||||
Reference in New Issue
Block a user