diff --git a/src/components/entity/AnimationResource.tsx b/src/components/entity/AnimationResource.tsx index 7b8cf88..fe574c9 100644 --- a/src/components/entity/AnimationResource.tsx +++ b/src/components/entity/AnimationResource.tsx @@ -5,7 +5,7 @@ import { StoreContext } from "@/store"; import { observer } from "mobx-react"; import { MdDelete } from "react-icons/md"; import { - Animation, + TAnimation, FadeInAnimation, FadeOutAnimation, SlideDirection, @@ -22,7 +22,7 @@ const ANIMATION_TYPE_TO_LABEL: Record = { breath: "Breath", }; export type AnimationResourceProps = { - animation: Animation; + animation: TAnimation; }; export const AnimationResource = observer((props: AnimationResourceProps) => { const store = React.useContext(StoreContext); @@ -40,13 +40,13 @@ export const AnimationResource = observer((props: AnimationResourceProps) => { {props.animation.type === "fadeIn" || - props.animation.type === "fadeOut" ? ( + props.animation.type === "fadeOut" ? ( ) : null} {props.animation.type === "slideIn" || - props.animation.type === "slideOut" ? ( + props.animation.type === "slideOut" ? ( diff --git a/src/store/Entity/AnimationHandler.ts b/src/store/Entity/AnimationHandler.ts new file mode 100644 index 0000000..e807a61 --- /dev/null +++ b/src/store/Entity/AnimationHandler.ts @@ -0,0 +1,352 @@ +import { fabric } from "fabric"; +import anime, { get } from "animejs"; +import { + EditorElement, + SlideDirection, + TAnimation, + TextEditorElement, +} from "../../types"; +import { FabricUitls } from "../../utils/fabric-utils"; +import { TimerHandler } from "./TimerHandler"; +import { observable, action, makeObservable, computed } from "mobx"; +export class AnimationHandler extends TimerHandler{ + canvas: fabric.Canvas | null; + animationTimeLine: anime.AnimeTimelineInstance; + animations: TAnimation[]; + editorElements: EditorElement[]; + constructor() { + super(); + this.canvas = null; + this.animationTimeLine = anime.timeline(); + this.animations = []; + this.editorElements = []; + makeObservable(this, { + canvas: observable, + animationTimeLine: observable, + animations: observable, + editorElements: observable, + addAnimation: action, + updateAnimation: action, + refreshAnimations: action, + removeAnimation: action, + }); + } + addAnimation(animation: TAnimation) { + this.animations = [...this.animations, animation]; + this.refreshAnimations(); + } + updateAnimation(id: string, animation: TAnimation) { + const index = this.animations.findIndex((a) => a.id === id); + this.animations[index] = animation; + this.refreshAnimations(); + } + refreshAnimations() { + anime.remove(this.animationTimeLine); + this.animationTimeLine = anime.timeline({ + duration: this.maxTime, + autoplay: false, + }); + for (let i = 0; i < this.animations.length; i++) { + const animation = this.animations[i]; + const editorElement = this.editorElements.find( + (element) => element.id === animation.targetId + ); + const fabricObject = editorElement?.fabricObject; + if (!editorElement || !fabricObject) { + continue; + } + fabricObject.clipPath = undefined; + const animationParams = (opacity: number[]) => { + return { + opacity: opacity, + duration: animation.duration, + targets: fabricObject, + easing: "linear", + }; + }; + switch (animation.type) { + case "fadeIn": { + this.animationTimeLine.add( + animationParams([0,1]), + editorElement.timeFrame.start + ); + break; + } + case "fadeOut": { + this.animationTimeLine.add( + animationParams([1,0]), + editorElement.timeFrame.end - animation.duration + ); + break; + } + case "slideIn": { + let { direction, startPosition, targetPosition } = this.slidePozition( + animation, + editorElement, + 0 + ); + [startPosition, targetPosition] = [targetPosition, startPosition]; + if (animation.properties.useClipPath) { + const clipRectangle = FabricUitls.getClipMaskRect( + editorElement, + 50 + ); + fabricObject.set("clipPath", clipRectangle); + } + if ( + editorElement.type === "text" && + animation.properties.textType === "character" + ) { + if (editorElement.fabricObject instanceof fabric.Text) { + this.canvas?.remove(...editorElement.properties.splittedTexts); + // @ts-ignore + editorElement.properties.splittedTexts = + getTextObjectsPartitionedByCharacters( + editorElement.fabricObject, + editorElement + ); + } + editorElement.properties.splittedTexts.forEach((textObject) => { + this.canvas!.add(textObject); + }); + const duration = animation.duration / 2; + const delay = + duration / editorElement.properties.splittedTexts.length; + for ( + let i = 0; + i < editorElement.properties.splittedTexts.length; + i++ + ) { + const splittedText = editorElement.properties.splittedTexts[i]; + const offset = { + left: splittedText.left! - editorElement.placement.x, + top: splittedText.top! - editorElement.placement.y, + }; + + this.animationTimeLine.add( + { + left: [ + startPosition.left + offset.left, + targetPosition.left && targetPosition.left + offset.left, + ], + top: [ + startPosition.top! + offset.top, + targetPosition.top && targetPosition.top + offset.top, + ], + delay: i * delay, + duration: duration, + targets: splittedText, + }, + editorElement.timeFrame.start + ); + } + this.animationTimeLine.add( + { + opacity: [1, 0], + duration: 1, + targets: fabricObject, + easing: "linear", + }, + editorElement.timeFrame.start + ); + this.animationTimeLine.add( + { + opacity: [0, 1], + duration: 1, + targets: fabricObject, + easing: "linear", + }, + editorElement.timeFrame.start + animation.duration + ); + + this.animationTimeLine.add( + { + opacity: [0, 1], + duration: 1, + targets: editorElement.properties.splittedTexts, + easing: "linear", + }, + editorElement.timeFrame.start + ); + this.animationTimeLine.add( + { + opacity: [1, 0], + duration: 1, + targets: editorElement.properties.splittedTexts, + easing: "linear", + }, + editorElement.timeFrame.start + animation.duration + ); + } + this.animationTimeLine.add( + { + left: [startPosition.left, targetPosition.left], + top: [startPosition.top, targetPosition.top], + duration: animation.duration, + targets: fabricObject, + easing: "linear", + }, + editorElement.timeFrame.start + ); + break; + } + case "slideOut": { + const { direction, startPosition, targetPosition } = + this.slidePozition(animation, editorElement, -100); + if (animation.properties.useClipPath) { + const clipRectangle = FabricUitls.getClipMaskRect( + editorElement, + 50 + ); + fabricObject.set("clipPath", clipRectangle); + } + this.animationTimeLine.add( + this.animationFadePattern( + startPosition, + targetPosition as TAnimationDirection, + animation, + fabricObject + ), + editorElement.timeFrame.end - animation.duration + ); + break; + } + case "breathe": { + const itsSlideInAnimation = this.animations.find( + (a) => a.targetId === animation.targetId && a.type === "slideIn" + ); + const itsSlideOutAnimation = this.animations.find( + (a) => a.targetId === animation.targetId && a.type === "slideOut" + ); + const timeEndOfSlideIn = itsSlideInAnimation + ? editorElement.timeFrame.start + itsSlideInAnimation.duration + : editorElement.timeFrame.start; + const timeStartOfSlideOut = itsSlideOutAnimation + ? editorElement.timeFrame.end - itsSlideOutAnimation.duration + : editorElement.timeFrame.end; + if (timeEndOfSlideIn > timeStartOfSlideOut) { + continue; + } + const duration = timeStartOfSlideOut - timeEndOfSlideIn; + const easeFactor = 4; + const suitableTimeForHeartbeat = ((1000 * 60) / 72) * easeFactor; + const upScale = 1.05; + const currentScaleX = fabricObject.scaleX ?? 1; + const currentScaleY = fabricObject.scaleY ?? 1; + const finalScaleX = currentScaleX * upScale; + const finalScaleY = currentScaleY * upScale; + const totalHeartbeats = Math.floor( + duration / suitableTimeForHeartbeat + ); + if (totalHeartbeats < 1) { + continue; + } + const keyframes = []; + for (let i = 0; i < totalHeartbeats; i++) { + keyframes.push({ scaleX: finalScaleX, scaleY: finalScaleY }); + keyframes.push({ scaleX: currentScaleX, scaleY: currentScaleY }); + } + + this.animationTimeLine.add( + { + duration: duration, + targets: fabricObject, + keyframes, + easing: "linear", + loop: true, + }, + timeEndOfSlideIn + ); + + break; + } + } + } + } + private slidePozition( + animation: TAnimation, + editorElement: EditorElement | undefined, + height: number + ) { + const direction = (animation.properties as { direction: SlideDirection }) + .direction; + const startPosition: any = { + left: editorElement?.placement.x, + top: editorElement?.placement.y, + }; + const targetPosition = { + left: + direction === "left" + ? -editorElement!.placement.width + : direction === "right" + ? this.canvas?.width + : editorElement!.placement.x, + top: + direction === "top" + ? height - editorElement!.placement.height + : direction === "bottom" + ? this.canvas?.height + : editorElement!.placement.y, + }; + return { direction, startPosition, targetPosition }; + } + private animationFadePattern( + startPosition: TAnimationDirection, + targetPosition: TAnimationDirection, + animation: any, + fabricObject: any + ) { + return { + left: [startPosition.left, targetPosition.left], + top: [startPosition.top, targetPosition.top], + duration: animation.duration, + targets: fabricObject, + easing: "linear", + }; + } + removeAnimation(id: string) { + this.animations = this.animations.filter( + (animation) => animation.id !== id + ); + this.refreshAnimations(); + } +} + +type TAnimationDirection = { + left: number; + top: number; +}; +function getTextObjectsPartitionedByCharacters( + textObject: fabric.Text, + element: TextEditorElement +): fabric.Text[] { + let copyCharsObjects: fabric.Text[] = []; + // replace all line endings with blank + const characters = (textObject.text ?? "") + .split("") + .filter((m) => m !== "\n"); + const charObjects = textObject.__charBounds; + if (!charObjects) return []; + const charObjectFixed = charObjects + .map((m, index) => m.slice(0, m.length - 1).map((m) => ({ m, index }))) + .flat(); + const lineHeight = textObject.getHeightOfLine(0); + for (let i = 0; i < characters.length; i++) { + if (!charObjectFixed[i]) continue; + const { m: charObject, index: lineIndex } = charObjectFixed[i]; + const char = characters[i]; + const scaleX = textObject.scaleX ?? 1; + const scaleY = textObject.scaleY ?? 1; + const charTextObject = new fabric.Text(char, { + left: charObject.left * scaleX + element.placement.x, + scaleX: scaleX, + scaleY: scaleY, + top: lineIndex * lineHeight * scaleY + element.placement.y, + fontSize: textObject.fontSize, + fontWeight: textObject.fontWeight, + fill: "#fff", + }); + copyCharsObjects.push(charTextObject); + } + return copyCharsObjects; +} diff --git a/src/store/Entity/ElementHandler.ts b/src/store/Entity/ElementHandler.ts new file mode 100644 index 0000000..0e896a1 --- /dev/null +++ b/src/store/Entity/ElementHandler.ts @@ -0,0 +1,326 @@ +import { fabric } from "fabric"; +import { EditorElement, Effect, Placement } from "../../types"; +import { isHtmlImageElement, isHtmlVideoElement } from "../../utils"; +import { AnimationHandler } from "./AnimationHandler"; +import { observable, action, makeObservable, computed } from "mobx"; +import { isEditorImageElement, isEditorVideoElement } from "../Store"; +export class ElementHandler extends AnimationHandler{ + selectedElement: EditorElement | null; + backgroundColor: string; + audios: string[]; + videos: string[]; + images: string[]; + constructor() { + super(); + this.videos = []; + this.images = []; + this.audios = []; + this.selectedElement = null; + this.backgroundColor = '#111111'; + makeObservable(this, { + selectedElement: observable, + backgroundColor: observable, + audios: observable, + videos: observable, + images: observable, + addVideoResource: action, + addAudioResource: action, + addImageResource: action, + addEditorElement: action, + setEditorElements: action, + setSelectedElement: action, + updateSelectedElement: action, + updateEditorElement: action, + refreshElements: action, + updateTimeTo: action, + updateEffect: action, + removeEditorElement:action + }); + } + updateEffect(id: string, effect: Effect) { + const index = this.editorElements.findIndex((element) => element.id === id); + const element = this.editorElements[index]; + if (isEditorVideoElement(element) || isEditorImageElement(element)) { + element.properties.effect = effect; + } + this.refreshElements(); + } + setBackgroundColor(backgroundColor: string) { + this.backgroundColor = backgroundColor; + if (this.canvas) { + this.canvas.backgroundColor = backgroundColor; + } + } + removeEditorElement(id: string) { + this.setEditorElements(this.editorElements.filter( + (editorElement) => editorElement.id !== id + )); + this.refreshElements(); + } + addVideoResource(video: string) { + this.videos = [...this.videos, video]; + } + addAudioResource(audio: string) { + this.audios = [...this.audios, audio]; + } + addImageResource(image: string) { + this.images = [...this.images, image]; + } + addEditorElement(editorElement: EditorElement) { + this.setEditorElements([...this.editorElements, editorElement]); + this.refreshElements(); + this.setSelectedElement( + this.editorElements[this.editorElements.length - 1] + ); + } + setEditorElements(editorElements: EditorElement[]) { + this.editorElements = editorElements; + this.updateSelectedElement(); + this.refreshElements(); + // this.refreshAnimations(); + } + updateSelectedElement() { + this.selectedElement = + this.editorElements.find( + (element) => element.id === this.selectedElement?.id + ) ?? null; + } + setSelectedElement(selectedElement: EditorElement | null) { + this.selectedElement = selectedElement; + if (this.canvas) { + if (selectedElement?.fabricObject) + this.canvas.setActiveObject(selectedElement.fabricObject); + else this.canvas.discardActiveObject(); + } + } + updateEditorElement(editorElement: EditorElement) { + this.setEditorElements( + this.editorElements.map((element) => + element.id === editorElement.id ? editorElement : element + ) + ); + } + refreshElements() { + const store = this; + if (!store.canvas) return; + const canvas = store.canvas; + store.canvas.remove(...store.canvas.getObjects()); + for (let index = 0; index < store.editorElements.length; index++) { + const element = store.editorElements[index]; + switch (element.type) { + case "video": { + console.log("elementid", element.properties.elementId); + if (document.getElementById(element.properties.elementId) === null) + continue; + const videoElement = document.getElementById( + element.properties.elementId + ); + if (!isHtmlVideoElement(videoElement)) continue; + // const filters = []; + // if (element.properties.effect?.type === "blackAndWhite") { + // filters.push(new fabric.Image.filters.Grayscale()); + // } + const videoObject = new fabric.CoverVideo(videoElement, { + name: element.id, + left: element.placement.x, + top: element.placement.y, + width: element.placement.width, + height: element.placement.height, + scaleX: element.placement.scaleX, + scaleY: element.placement.scaleY, + angle: element.placement.rotation, + objectCaching: false, + selectable: true, + lockUniScaling: true, + // filters: filters, + // @ts-ignore + customFilter: element.properties.effect.type, + }); + + element.fabricObject = videoObject; + element.properties.imageObject = videoObject; + videoElement.width = 100; + videoElement.height = + (videoElement.videoHeight * 100) / videoElement.videoWidth; + canvas.add(videoObject); + canvas.on("object:modified", function (e) { + if (!e.target) return; + const target = e.target; + if (target != videoObject) return; + const placement = element.placement; + const newPlacement: Placement = { + ...placement, + x: target.left ?? placement.x, + y: target.top ?? placement.y, + rotation: target.angle ?? placement.rotation, + width: + target.width && target.scaleX + ? target.width * target.scaleX + : placement.width, + height: + target.height && target.scaleY + ? target.height * target.scaleY + : placement.height, + scaleX: 1, + scaleY: 1, + }; + const newElement = { + ...element, + placement: newPlacement, + }; + store.updateEditorElement(newElement); + }); + break; + } + + case "image": { + if (document.getElementById(element.properties.elementId) == null) + continue; + const imageElement = document.getElementById( + element.properties.elementId + ); + if (!isHtmlImageElement(imageElement)) continue; + // const filters = []; + // if (element.properties.effect?.type === "blackAndWhite") { + // filters.push(new fabric.Image.filters.Grayscale()); + // } + const imageObject = new fabric.CoverImage(imageElement, { + name: element.id, + left: element.placement.x, + top: element.placement.y, + angle: element.placement.rotation, + objectCaching: false, + selectable: true, + lockUniScaling: true, + // filters + // @ts-ignore + customFilter: element.properties.effect.type, + }); + // imageObject.applyFilters(); + element.fabricObject = imageObject; + element.properties.imageObject = imageObject; + const image = { + w: imageElement.naturalWidth, + h: imageElement.naturalHeight, + }; + + imageObject.width = image.w; + imageObject.height = image.h; + imageElement.width = image.w; + imageElement.height = image.h; + imageObject.scaleToHeight(image.w); + imageObject.scaleToWidth(image.h); + const toScale = { + x: element.placement.width / image.w, + y: element.placement.height / image.h, + }; + imageObject.scaleX = toScale.x * element.placement.scaleX; + imageObject.scaleY = toScale.y * element.placement.scaleY; + canvas.add(imageObject); + canvas.on("object:modified", function (e) { + if (!e.target) return; + const target = e.target; + if (target != imageObject) return; + const placement = element.placement; + let fianlScale = 1; + if (target.scaleX && target.scaleX > 0) { + fianlScale = target.scaleX / toScale.x; + } + const newPlacement: Placement = { + ...placement, + x: target.left ?? placement.x, + y: target.top ?? placement.y, + rotation: target.angle ?? placement.rotation, + scaleX: fianlScale, + scaleY: fianlScale, + }; + const newElement = { + ...element, + placement: newPlacement, + }; + store.updateEditorElement(newElement); + }); + break; + } + case "audio": { + break; + } + case "text": { + const textObject = new fabric.Textbox(element.properties.text, { + name: element.id, + left: element.placement.x, + top: element.placement.y, + scaleX: element.placement.scaleX, + scaleY: element.placement.scaleY, + width: element.placement.width, + height: element.placement.height, + angle: element.placement.rotation, + fontSize: element.properties.fontSize, + fontWeight: element.properties.fontWeight, + objectCaching: false, + selectable: true, + lockUniScaling: true, + fill: "#ffffff", + }); + element.fabricObject = textObject; + canvas.add(textObject); + canvas.on("object:modified", function (e) { + if (!e.target) return; + const target = e.target; + if (target != textObject) return; + const placement = element.placement; + const newPlacement: Placement = { + ...placement, + x: target.left ?? placement.x, + y: target.top ?? placement.y, + rotation: target.angle ?? placement.rotation, + width: target.width ?? placement.width, + height: target.height ?? placement.height, + scaleX: target.scaleX ?? placement.scaleX, + scaleY: target.scaleY ?? placement.scaleY, + }; + const newElement = { + ...element, + placement: newPlacement, + properties: { + ...element.properties, + // @ts-ignore + text: target?.text, + }, + }; + store.updateEditorElement(newElement); + }); + break; + } + default: { + throw new Error("Not implemented"); + } + } + if (element.fabricObject) { + element.fabricObject.on("selected", function (e) { + store.setSelectedElement(element); + }); + } + } + const selectedEditorElement = store.selectedElement; + if (selectedEditorElement && selectedEditorElement.fabricObject) { + canvas.setActiveObject(selectedEditorElement.fabricObject); + } + this.refreshAnimations(); + this.updateTimeTo(this.currentTimeInMs); + store.canvas.renderAll(); + } + updateTimeTo(newTime: number) { + this.setCurrentTimeInMs(newTime); + this.animationTimeLine.seek(newTime); + if (this.canvas) { + this.canvas.backgroundColor = this.backgroundColor; + } + this.editorElements.forEach((e) => { + if (!e.fabricObject) return; + const isInside = + e.timeFrame.start <= newTime && newTime <= e.timeFrame.end; + e.fabricObject.visible = isInside; + }); + } +} diff --git a/src/store/Entity/MediaHandler.ts b/src/store/Entity/MediaHandler.ts new file mode 100644 index 0000000..d3ec8c0 --- /dev/null +++ b/src/store/Entity/MediaHandler.ts @@ -0,0 +1,170 @@ +import { EditorElement, TimeFrame } from "@/types"; +import { + getUid, + isHtmlAudioElement, + isHtmlImageElement, + isHtmlVideoElement, +} from "../../utils"; +import { MediaPlayer } from "./MediaPlayer"; +import { action, makeObservable } from "mobx"; +export class MediaHandler extends MediaPlayer { + constructor() { + super(); + makeObservable(this, { + addAudio: action, + addVideo: action, + addImage: action, + addText: action, + updateEditorElementTimeFrame: action, + }); + } + addVideo(index: number) { + const videoElement = document.getElementById(`video-${index}`); + if (!isHtmlVideoElement(videoElement)) { + return; + } + const videoDurationMs = videoElement.duration * 1000; + const aspectRatio = videoElement.videoWidth / videoElement.videoHeight; + const id = getUid(); + this.addEditorElement({ + id, + name: `Media(video) ${index + 1}`, + type: "video", + placement: { + x: 0, + y: 0, + width: 100 * aspectRatio, + height: 100, + rotation: 0, + scaleX: 1, + scaleY: 1, + }, + timeFrame: { + start: 0, + end: videoDurationMs, + }, + properties: { + elementId: `video-${id}`, + src: videoElement.src, + effect: { + type: "none", + }, + }, + }); + } + + addImage(index: number) { + const imageElement = document.getElementById(`image-${index}`); + if (!isHtmlImageElement(imageElement)) { + return; + } + const aspectRatio = imageElement.naturalWidth / imageElement.naturalHeight; + const id = getUid(); + this.addEditorElement({ + id, + name: `Media(image) ${index + 1}`, + type: "image", + placement: { + x: 0, + y: 0, + width: 100 * aspectRatio, + height: 100, + rotation: 0, + scaleX: 1, + scaleY: 1, + }, + timeFrame: { + start: 0, + end: this.maxTime, + }, + properties: { + elementId: `image-${id}`, + src: imageElement.src, + effect: { + type: "none", + }, + }, + }); + } + + addAudio(index: number) { + const audioElement = document.getElementById(`audio-${index}`); + if (!isHtmlAudioElement(audioElement)) { + return; + } + const audioDurationMs = audioElement.duration * 1000; + const id = getUid(); + this.addEditorElement({ + id, + name: `Media(audio) ${index + 1}`, + type: "audio", + placement: { + x: 0, + y: 0, + width: 100, + height: 100, + rotation: 0, + scaleX: 1, + scaleY: 1, + }, + timeFrame: { + start: 0, + end: audioDurationMs, + }, + properties: { + elementId: `audio-${id}`, + src: audioElement.src, + }, + }); + } + addText(options: { text: string; fontSize: number; fontWeight: number }) { + const id = getUid(); + const index = this.editorElements.length; + this.addEditorElement({ + id, + name: `Text ${index + 1}`, + type: "text", + placement: { + x: 0, + y: 0, + width: 100, + height: 100, + rotation: 0, + scaleX: 1, + scaleY: 1, + }, + timeFrame: { + start: 0, + end: this.maxTime, + }, + properties: { + text: options.text, + fontSize: options.fontSize, + fontWeight: options.fontWeight, + splittedTexts: [], + }, + }); + } + updateEditorElementTimeFrame( + editorElement: EditorElement, + timeFrame: Partial + ) { + if (timeFrame.start != undefined && timeFrame.start < 0) { + timeFrame.start = 0; + } + if (timeFrame.end != undefined && timeFrame.end > this.maxTime) { + timeFrame.end = this.maxTime; + } + const newEditorElement = { + ...editorElement, + timeFrame: { + ...editorElement.timeFrame, + ...timeFrame, + }, + }; + this.updateVideoElements(); + this.updateAudioElements(); + this.updateEditorElement(newEditorElement); + this.refreshAnimations(); + } +} diff --git a/src/store/Entity/MediaPlayer.ts b/src/store/Entity/MediaPlayer.ts new file mode 100644 index 0000000..ee22bd0 --- /dev/null +++ b/src/store/Entity/MediaPlayer.ts @@ -0,0 +1,100 @@ +import { getUid, isHtmlAudioElement, isHtmlVideoElement } from "../../utils"; +import { ElementHandler } from "./ElementHandler"; +import { AudioEditorElement, VideoEditorElement } from "../../types"; +import { observable, action, makeObservable, computed } from "mobx"; +export class MediaPlayer extends ElementHandler { + playing: boolean; + constructor() { + super(); + this.playing = false; + makeObservable(this, { + playing: observable, + setPlaying: action, + playFrames: action, + updateVideoElements: action, + updateAudioElements: action, + + handleSeek: action, + }); + } + + setPlaying(playing: boolean) { + this.playing = playing; + this.updateVideoElements(); + this.updateAudioElements(); + if (playing) { + this.startedTime = Date.now(); + this.startedTimePlay = this.currentTimeInMs; + requestAnimationFrame(() => { + this.playFrames(); + }); + } + } + updateAudioElements() { + this.editorElements + .filter( + (element): element is AudioEditorElement => element.type === "audio" + ) + .forEach((element) => { + const audio = document.getElementById(element.properties.elementId); + if (isHtmlAudioElement(audio)) { + const audioTime = + (this.currentTimeInMs - element.timeFrame.start) / 1000; + audio.currentTime = audioTime; + if (this.playing) { + audio.play(); + } else { + audio.pause(); + } + } + }); + } + startedTime = 0; + startedTimePlay = 0; + + playFrames() { + if (!this.playing) { + return; + } + const elapsedTime = Date.now() - this.startedTime; + const newTime = this.startedTimePlay + elapsedTime; + this.updateTimeTo(newTime); + if (newTime > this.maxTime) { + this.currentKeyFrame = 0; + this.setPlaying(false); + } else { + requestAnimationFrame(() => { + this.playFrames(); + }); + } + } + + + handleSeek(seek: number) { + if (this.playing) { + this.setPlaying(false); + } + this.updateTimeTo(seek); + this.updateVideoElements(); + this.updateAudioElements(); + } + updateVideoElements() { + this.editorElements + .filter( + (element): element is VideoEditorElement => element.type === "video" + ) + .forEach((element) => { + const video = document.getElementById(element.properties.elementId); + if (isHtmlVideoElement(video)) { + const videoTime = + (this.currentTimeInMs - element.timeFrame.start) / 1000; + video.currentTime = videoTime; + if (this.playing) { + video.play(); + } else { + video.pause(); + } + } + }); + } +} diff --git a/src/store/Entity/TimerHandler.ts b/src/store/Entity/TimerHandler.ts new file mode 100644 index 0000000..b0bc6ac --- /dev/null +++ b/src/store/Entity/TimerHandler.ts @@ -0,0 +1,29 @@ +import { observable, action, makeObservable, computed } from "mobx"; +export class TimerHandler { + currentKeyFrame: number; + fps: number; + maxTime: number; + constructor() { + this.fps = 60; + this.currentKeyFrame = 0; + this.maxTime = 30 * 1000; + makeObservable(this, { + currentKeyFrame: observable, + fps: observable, + maxTime: observable, + setCurrentTimeInMs: action, + setMaxTime: action, + currentTimeInMs: computed, + }); + } + get currentTimeInMs() { + return (this.currentKeyFrame * 1000) / this.fps; + } + + setCurrentTimeInMs(time: number) { + this.currentKeyFrame = Math.floor((time / 1000) * this.fps); + } + setMaxTime(maxTime: number) { + this.maxTime = maxTime; + } +} diff --git a/src/store/Store.ts b/src/store/Store.ts index 6dc4bf3..94b0ec8 100644 --- a/src/store/Store.ts +++ b/src/store/Store.ts @@ -1,66 +1,42 @@ -import { makeAutoObservable } from 'mobx'; -import { fabric } from 'fabric'; -import { getUid, isHtmlAudioElement, isHtmlImageElement, isHtmlVideoElement } from '@/utils'; -import anime, { get } from 'animejs'; -import { MenuOption, EditorElement, Animation, TimeFrame, VideoEditorElement, AudioEditorElement, Placement, ImageEditorElement, Effect, TextEditorElement } from '../types'; -import { FabricUitls } from '@/utils/fabric-utils'; -import { FFmpeg } from '@ffmpeg/ffmpeg'; -import { toBlobURL } from '@ffmpeg/util'; - -export class Store { - canvas: fabric.Canvas | null - - backgroundColor: string; - +import { observable, action, makeObservable, computed } from "mobx"; +import { fabric } from "fabric"; +import anime, { get } from "animejs"; +import { + MenuOption, + EditorElement, + TAnimation, + VideoEditorElement, + AudioEditorElement, + ImageEditorElement, + TextEditorElement, +} from "../types"; +import { FFmpeg } from "@ffmpeg/ffmpeg"; +import { toBlobURL } from "@ffmpeg/util"; +import { MediaHandler } from "./Entity/MediaHandler"; +export class Store extends MediaHandler { selectedMenuOption: MenuOption; - audios: string[] - videos: string[] - images: string[] - editorElements: EditorElement[] - selectedElement: EditorElement | null; - - maxTime: number - animations: Animation[] - animationTimeLine: anime.AnimeTimelineInstance; - playing: boolean; - - currentKeyFrame: number; - fps: number; - - possibleVideoFormats: string[] = ['mp4', 'webm']; - selectedVideoFormat: 'mp4' | 'webm'; + possibleVideoFormats: string[] = ["mp4", "webm"]; + selectedVideoFormat: "mp4" | "webm"; constructor() { - this.canvas = null; - this.videos = []; - this.images = []; - this.audios = []; - this.editorElements = []; - this.backgroundColor = '#111111'; - this.maxTime = 30 * 1000; - this.playing = false; - this.currentKeyFrame = 0; - this.selectedElement = null; - this.fps = 60; - this.animations = []; - this.animationTimeLine = anime.timeline(); - this.selectedMenuOption = 'Video'; - this.selectedVideoFormat = 'mp4'; - makeAutoObservable(this); - } - - get currentTimeInMs() { - return this.currentKeyFrame * 1000 / this.fps; - } - - setCurrentTimeInMs(time: number) { - this.currentKeyFrame = Math.floor(time / 1000 * this.fps); + super(); + this.selectedMenuOption = "Video"; + this.selectedVideoFormat = "mp4"; + makeObservable(this, { + selectedMenuOption: observable, + possibleVideoFormats: observable, + selectedVideoFormat: observable, + setVideos: action, + setCanvas: action, + setSelectedMenuOption: action, + setVideoFormat: action, + saveCanvasToVideoWithAudio: action, + saveCanvasToVideoWithAudioWebmMp4: action, + }); } - - setSelectedMenuOption(selectedMenuOption: MenuOption) { - this.selectedMenuOption = selectedMenuOption; + setVideos(videos: string[]) { + this.videos = videos; } - setCanvas(canvas: fabric.Canvas | null) { this.canvas = canvas; if (canvas) { @@ -68,518 +44,8 @@ export class Store { } } - setBackgroundColor(backgroundColor: string) { - this.backgroundColor = backgroundColor; - if (this.canvas) { - this.canvas.backgroundColor = backgroundColor; - } - } - - updateEffect(id: string, effect: Effect) { - const index = this.editorElements.findIndex((element) => element.id === id); - const element = this.editorElements[index]; - if (isEditorVideoElement(element) || isEditorImageElement(element)) { - element.properties.effect = effect; - } - this.refreshElements(); - } - - setVideos(videos: string[]) { - this.videos = videos; - } - - addVideoResource(video: string) { - this.videos = [...this.videos, video]; - } - addAudioResource(audio: string) { - this.audios = [...this.audios, audio]; - } - addImageResource(image: string) { - this.images = [...this.images, image]; - } - - addAnimation(animation: Animation) { - this.animations = [...this.animations, animation]; - this.refreshAnimations(); - } - updateAnimation(id: string, animation: Animation) { - const index = this.animations.findIndex((a) => a.id === id); - this.animations[index] = animation; - this.refreshAnimations(); - } - - refreshAnimations() { - anime.remove(this.animationTimeLine); - this.animationTimeLine = anime.timeline({ - duration: this.maxTime, - autoplay: false, - }); - for (let i = 0; i < this.animations.length; i++) { - const animation = this.animations[i]; - const editorElement = this.editorElements.find((element) => element.id === animation.targetId); - const fabricObject = editorElement?.fabricObject; - if (!editorElement || !fabricObject) { - continue; - } - fabricObject.clipPath = undefined; - switch (animation.type) { - case "fadeIn": { - this.animationTimeLine.add({ - opacity: [0, 1], - duration: animation.duration, - targets: fabricObject, - easing: 'linear', - }, editorElement.timeFrame.start); - break; - } - case "fadeOut": { - this.animationTimeLine.add({ - opacity: [1, 0], - duration: animation.duration, - targets: fabricObject, - easing: 'linear', - }, editorElement.timeFrame.end - animation.duration); - break - } - case "slideIn": { - const direction = animation.properties.direction; - const targetPosition = { - left: editorElement.placement.x, - top: editorElement.placement.y, - } - const startPosition = { - left: (direction === "left" ? - editorElement.placement.width : direction === "right" ? this.canvas?.width : editorElement.placement.x), - top: (direction === "top" ? - editorElement.placement.height : direction === "bottom" ? this.canvas?.height : editorElement.placement.y), - } - if (animation.properties.useClipPath) { - const clipRectangle = FabricUitls.getClipMaskRect(editorElement, 50); - fabricObject.set('clipPath', clipRectangle) - } - if (editorElement.type === "text" && animation.properties.textType === "character") { - this.canvas?.remove(...editorElement.properties.splittedTexts) - // @ts-ignore - editorElement.properties.splittedTexts = getTextObjectsPartitionedByCharacters(editorElement.fabricObject, editorElement); - editorElement.properties.splittedTexts.forEach((textObject) => { - this.canvas!.add(textObject); - }) - const duration = animation.duration / 2; - const delay = duration / editorElement.properties.splittedTexts.length; - for (let i = 0; i < editorElement.properties.splittedTexts.length; i++) { - const splittedText = editorElement.properties.splittedTexts[i]; - const offset = { - left: splittedText.left! - editorElement.placement.x, - top: splittedText.top! - editorElement.placement.y - } - this.animationTimeLine.add({ - left: [startPosition.left! + offset.left, targetPosition.left + offset.left], - top: [startPosition.top! + offset.top, targetPosition.top + offset.top], - delay: i * delay, - duration: duration, - targets: splittedText, - }, editorElement.timeFrame.start); - } - this.animationTimeLine.add({ - opacity: [1, 0], - duration: 1, - targets: fabricObject, - easing: 'linear', - }, editorElement.timeFrame.start); - this.animationTimeLine.add({ - opacity: [0, 1], - duration: 1, - targets: fabricObject, - easing: 'linear', - }, editorElement.timeFrame.start + animation.duration); - - this.animationTimeLine.add({ - opacity: [0, 1], - duration: 1, - targets: editorElement.properties.splittedTexts, - easing: 'linear', - }, editorElement.timeFrame.start); - this.animationTimeLine.add({ - opacity: [1, 0], - duration: 1, - targets: editorElement.properties.splittedTexts, - easing: 'linear', - }, editorElement.timeFrame.start + animation.duration); - } - this.animationTimeLine.add({ - left: [startPosition.left, targetPosition.left], - top: [startPosition.top, targetPosition.top], - duration: animation.duration, - targets: fabricObject, - easing: 'linear', - }, editorElement.timeFrame.start); - break - } - case "slideOut": { - const direction = animation.properties.direction; - const startPosition = { - left: editorElement.placement.x, - top: editorElement.placement.y, - } - const targetPosition = { - left: (direction === "left" ? - editorElement.placement.width : direction === "right" ? this.canvas?.width : editorElement.placement.x), - top: (direction === "top" ? -100 - editorElement.placement.height : direction === "bottom" ? this.canvas?.height : editorElement.placement.y), - } - if (animation.properties.useClipPath) { - const clipRectangle = FabricUitls.getClipMaskRect(editorElement, 50); - fabricObject.set('clipPath', clipRectangle) - } - this.animationTimeLine.add({ - left: [startPosition.left, targetPosition.left], - top: [startPosition.top, targetPosition.top], - duration: animation.duration, - targets: fabricObject, - easing: 'linear', - }, editorElement.timeFrame.end - animation.duration); - break - } - case "breathe": { - const itsSlideInAnimation = this.animations.find((a) => a.targetId === animation.targetId && (a.type === "slideIn")); - const itsSlideOutAnimation = this.animations.find((a) => a.targetId === animation.targetId && (a.type === "slideOut")); - const timeEndOfSlideIn = itsSlideInAnimation ? editorElement.timeFrame.start + itsSlideInAnimation.duration : editorElement.timeFrame.start; - const timeStartOfSlideOut = itsSlideOutAnimation ? editorElement.timeFrame.end - itsSlideOutAnimation.duration : editorElement.timeFrame.end; - if (timeEndOfSlideIn > timeStartOfSlideOut) { - continue; - } - const duration = timeStartOfSlideOut - timeEndOfSlideIn; - const easeFactor = 4; - const suitableTimeForHeartbeat = 1000 * 60 / 72 * easeFactor - const upScale = 1.05; - const currentScaleX = fabricObject.scaleX ?? 1; - const currentScaleY = fabricObject.scaleY ?? 1; - const finalScaleX = currentScaleX * upScale; - const finalScaleY = currentScaleY * upScale; - const totalHeartbeats = Math.floor(duration / suitableTimeForHeartbeat); - if (totalHeartbeats < 1) { - continue; - } - const keyframes = []; - for (let i = 0; i < totalHeartbeats; i++) { - keyframes.push({ scaleX: finalScaleX, scaleY: finalScaleY }); - keyframes.push({ scaleX: currentScaleX, scaleY: currentScaleY }); - } - - this.animationTimeLine.add({ - duration: duration, - targets: fabricObject, - keyframes, - easing: 'linear', - loop: true - }, timeEndOfSlideIn); - - break - } - } - } - } - - removeAnimation(id: string) { - this.animations = this.animations.filter( - (animation) => animation.id !== id - ); - this.refreshAnimations(); - } - - setSelectedElement(selectedElement: EditorElement | null) { - this.selectedElement = selectedElement; - if (this.canvas) { - if (selectedElement?.fabricObject) - this.canvas.setActiveObject(selectedElement.fabricObject); - else - this.canvas.discardActiveObject(); - } - } - updateSelectedElement() { - this.selectedElement = this.editorElements.find((element) => element.id === this.selectedElement?.id) ?? null; - } - - setEditorElements(editorElements: EditorElement[]) { - this.editorElements = editorElements; - this.updateSelectedElement(); - this.refreshElements(); - // this.refreshAnimations(); - } - - updateEditorElement(editorElement: EditorElement) { - this.setEditorElements(this.editorElements.map((element) => - element.id === editorElement.id ? editorElement : element - )); - } - - updateEditorElementTimeFrame(editorElement: EditorElement, timeFrame: Partial) { - if (timeFrame.start != undefined && timeFrame.start < 0) { - timeFrame.start = 0; - } - if (timeFrame.end != undefined && timeFrame.end > this.maxTime) { - timeFrame.end = this.maxTime; - } - const newEditorElement = { - ...editorElement, - timeFrame: { - ...editorElement.timeFrame, - ...timeFrame, - } - } - this.updateVideoElements(); - this.updateAudioElements(); - this.updateEditorElement(newEditorElement); - this.refreshAnimations(); - } - - - addEditorElement(editorElement: EditorElement) { - this.setEditorElements([...this.editorElements, editorElement]); - this.refreshElements(); - this.setSelectedElement(this.editorElements[this.editorElements.length - 1]); - } - - removeEditorElement(id: string) { - this.setEditorElements(this.editorElements.filter( - (editorElement) => editorElement.id !== id - )); - this.refreshElements(); - } - - setMaxTime(maxTime: number) { - this.maxTime = maxTime; - } - - - setPlaying(playing: boolean) { - this.playing = playing; - this.updateVideoElements(); - this.updateAudioElements(); - if (playing) { - this.startedTime = Date.now(); - this.startedTimePlay = this.currentTimeInMs - requestAnimationFrame(() => { - this.playFrames(); - }); - } - } - - startedTime = 0; - startedTimePlay = 0; - - playFrames() { - if (!this.playing) { - return; - } - const elapsedTime = Date.now() - this.startedTime; - const newTime = this.startedTimePlay + elapsedTime; - this.updateTimeTo(newTime); - if (newTime > this.maxTime) { - this.currentKeyFrame = 0; - this.setPlaying(false); - } else { - requestAnimationFrame(() => { - this.playFrames(); - }); - } - } - updateTimeTo(newTime: number) { - this.setCurrentTimeInMs(newTime); - this.animationTimeLine.seek(newTime); - if (this.canvas) { - this.canvas.backgroundColor = this.backgroundColor; - } - this.editorElements.forEach( - e => { - if (!e.fabricObject) return; - const isInside = e.timeFrame.start <= newTime && newTime <= e.timeFrame.end; - e.fabricObject.visible = isInside; - } - ) - } - - handleSeek(seek: number) { - if (this.playing) { - this.setPlaying(false); - } - this.updateTimeTo(seek); - this.updateVideoElements(); - this.updateAudioElements(); - } - - addVideo(index: number) { - const videoElement = document.getElementById(`video-${index}`) - if (!isHtmlVideoElement(videoElement)) { - return; - } - const videoDurationMs = videoElement.duration * 1000; - const aspectRatio = videoElement.videoWidth / videoElement.videoHeight; - const id = getUid(); - this.addEditorElement( - { - id, - name: `Media(video) ${index + 1}`, - type: "video", - placement: { - x: 0, - y: 0, - width: 100 * aspectRatio, - height: 100, - rotation: 0, - scaleX: 1, - scaleY: 1, - }, - timeFrame: { - start: 0, - end: videoDurationMs, - }, - properties: { - elementId: `video-${id}`, - src: videoElement.src, - effect: { - type: "none", - } - }, - }, - ); - } - - addImage(index: number) { - const imageElement = document.getElementById(`image-${index}`) - if (!isHtmlImageElement(imageElement)) { - return; - } - const aspectRatio = imageElement.naturalWidth / imageElement.naturalHeight; - const id = getUid(); - this.addEditorElement( - { - id, - name: `Media(image) ${index + 1}`, - type: "image", - placement: { - x: 0, - y: 0, - width: 100 * aspectRatio, - height: 100, - rotation: 0, - scaleX: 1, - scaleY: 1, - }, - timeFrame: { - start: 0, - end: this.maxTime, - }, - properties: { - elementId: `image-${id}`, - src: imageElement.src, - effect: { - type: "none", - } - }, - }, - ); - } - - addAudio(index: number) { - const audioElement = document.getElementById(`audio-${index}`) - if (!isHtmlAudioElement(audioElement)) { - return; - } - const audioDurationMs = audioElement.duration * 1000; - const id = getUid(); - this.addEditorElement( - { - id, - name: `Media(audio) ${index + 1}`, - type: "audio", - placement: { - x: 0, - y: 0, - width: 100, - height: 100, - rotation: 0, - scaleX: 1, - scaleY: 1, - }, - timeFrame: { - start: 0, - end: audioDurationMs, - }, - properties: { - elementId: `audio-${id}`, - src: audioElement.src, - } - }, - ); - - } - addText(options: { - text: string, - fontSize: number, - fontWeight: number, - }) { - const id = getUid(); - const index = this.editorElements.length; - this.addEditorElement( - { - id, - name: `Text ${index + 1}`, - type: "text", - placement: { - x: 0, - y: 0, - width: 100, - height: 100, - rotation: 0, - scaleX: 1, - scaleY: 1, - }, - timeFrame: { - start: 0, - end: this.maxTime, - }, - properties: { - text: options.text, - fontSize: options.fontSize, - fontWeight: options.fontWeight, - splittedTexts: [], - }, - }, - ); - } - - updateVideoElements() { - this.editorElements.filter( - (element): element is VideoEditorElement => - element.type === "video" - ) - .forEach((element) => { - const video = document.getElementById(element.properties.elementId); - if (isHtmlVideoElement(video)) { - const videoTime = (this.currentTimeInMs - element.timeFrame.start) / 1000; - video.currentTime = videoTime; - if (this.playing) { - video.play(); - } else { - video.pause(); - } - } - }) - } - updateAudioElements() { - this.editorElements.filter( - (element): element is AudioEditorElement => - element.type === "audio" - ) - .forEach((element) => { - const audio = document.getElementById(element.properties.elementId); - if (isHtmlAudioElement(audio)) { - const audioTime = (this.currentTimeInMs - element.timeFrame.start) / 1000; - audio.currentTime = audioTime; - if (this.playing) { - audio.play(); - } else { - audio.pause(); - } - } - }) + setSelectedMenuOption(selectedMenuOption: MenuOption) { + this.selectedMenuOption = selectedMenuOption; } // saveCanvasToVideo() { // const video = document.createElement("video"); @@ -609,7 +75,7 @@ export class Store { // } - setVideoFormat(format: 'mp4' | 'webm') { + setVideoFormat(format: "mp4" | "webm") { this.selectedVideoFormat = format; } @@ -618,14 +84,16 @@ export class Store { } saveCanvasToVideoWithAudioWebmMp4() { - console.log('modified') - let mp4 = this.selectedVideoFormat === 'mp4' + console.log("modified"); + let mp4 = this.selectedVideoFormat === "mp4"; const canvas = document.getElementById("canvas") as HTMLCanvasElement; const stream = canvas.captureStream(30); - const audioElements = this.editorElements.filter(isEditorAudioElement) + const audioElements = this.editorElements.filter(isEditorAudioElement); const audioStreams: MediaStream[] = []; audioElements.forEach((audio) => { - const audioElement = document.getElementById(audio.properties.elementId) as HTMLAudioElement; + const audioElement = document.getElementById( + audio.properties.elementId + ) as HTMLAudioElement; let ctx = new AudioContext(); let sourceNode = ctx.createMediaElementSource(audioElement); let dest = ctx.createMediaStreamDestination(); @@ -648,33 +116,44 @@ export class Store { mediaRecorder.ondataavailable = function (e) { chunks.push(e.data); console.log("data available"); - }; mediaRecorder.onstop = async function (e) { const blob = new Blob(chunks, { type: "video/webm" }); if (mp4) { // lets use ffmpeg to convert webm to mp4 - const data = new Uint8Array(await (blob).arrayBuffer()); + const data = new Uint8Array(await blob.arrayBuffer()); const ffmpeg = new FFmpeg(); - const baseURL = "https://unpkg.com/@ffmpeg/core@0.12.2/dist/umd" + const baseURL = "https://unpkg.com/@ffmpeg/core@0.12.2/dist/umd"; await ffmpeg.load({ - coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'), - wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'), + coreURL: await toBlobURL( + `${baseURL}/ffmpeg-core.js`, + "text/javascript" + ), + wasmURL: await toBlobURL( + `${baseURL}/ffmpeg-core.wasm`, + "application/wasm" + ), // workerURL: await toBlobURL(`${baseURL}/ffmpeg-core.worker.js`, 'text/javascript'), }); - await ffmpeg.writeFile('video.webm', data); - await ffmpeg.exec(["-y", "-i", "video.webm", "-c", "copy", "video.mp4"]); + await ffmpeg.writeFile("video.webm", data); + await ffmpeg.exec([ + "-y", + "-i", + "video.webm", + "-c", + "copy", + "video.mp4", + ]); // await ffmpeg.exec(["-y", "-i", "video.webm", "-c:v", "libx264", "video.mp4"]); - const output = await ffmpeg.readFile('video.mp4'); + const output = await ffmpeg.readFile("video.mp4"); const outputBlob = new Blob([output], { type: "video/mp4" }); const outputUrl = URL.createObjectURL(outputBlob); const a = document.createElement("a"); a.download = "video.mp4"; a.href = outputUrl; a.click(); - } else { const url = URL.createObjectURL(blob); const a = document.createElement("a"); @@ -688,222 +167,10 @@ export class Store { mediaRecorder.stop(); }, this.maxTime); video.remove(); - }) - } - - refreshElements() { - const store = this; - if (!store.canvas) return; - const canvas = store.canvas; - store.canvas.remove(...store.canvas.getObjects()); - for (let index = 0; index < store.editorElements.length; index++) { - const element = store.editorElements[index]; - switch (element.type) { - case "video": { - console.log("elementid", element.properties.elementId); - if (document.getElementById(element.properties.elementId) == null) - continue; - const videoElement = document.getElementById( - element.properties.elementId - ); - if (!isHtmlVideoElement(videoElement)) continue; - // const filters = []; - // if (element.properties.effect?.type === "blackAndWhite") { - // filters.push(new fabric.Image.filters.Grayscale()); - // } - const videoObject = new fabric.CoverVideo(videoElement, { - name: element.id, - left: element.placement.x, - top: element.placement.y, - width: element.placement.width, - height: element.placement.height, - scaleX: element.placement.scaleX, - scaleY: element.placement.scaleY, - angle: element.placement.rotation, - objectCaching: false, - selectable: true, - lockUniScaling: true, - // filters: filters, - // @ts-ignore - customFilter: element.properties.effect.type, - }); - - element.fabricObject = videoObject; - element.properties.imageObject = videoObject; - videoElement.width = 100; - videoElement.height = - (videoElement.videoHeight * 100) / videoElement.videoWidth; - canvas.add(videoObject); - canvas.on("object:modified", function (e) { - if (!e.target) return; - const target = e.target; - if (target != videoObject) return; - const placement = element.placement; - const newPlacement: Placement = { - ...placement, - x: target.left ?? placement.x, - y: target.top ?? placement.y, - rotation: target.angle ?? placement.rotation, - width: - target.width && target.scaleX - ? target.width * target.scaleX - : placement.width, - height: - target.height && target.scaleY - ? target.height * target.scaleY - : placement.height, - scaleX: 1, - scaleY: 1, - }; - const newElement = { - ...element, - placement: newPlacement, - }; - store.updateEditorElement(newElement); - }); - break; - } - case "image": { - if (document.getElementById(element.properties.elementId) == null) - continue; - const imageElement = document.getElementById( - element.properties.elementId - ); - if (!isHtmlImageElement(imageElement)) continue; - // const filters = []; - // if (element.properties.effect?.type === "blackAndWhite") { - // filters.push(new fabric.Image.filters.Grayscale()); - // } - const imageObject = new fabric.CoverImage(imageElement, { - name: element.id, - left: element.placement.x, - top: element.placement.y, - angle: element.placement.rotation, - objectCaching: false, - selectable: true, - lockUniScaling: true, - // filters - // @ts-ignore - customFilter: element.properties.effect.type, - }); - // imageObject.applyFilters(); - element.fabricObject = imageObject; - element.properties.imageObject = imageObject; - const image = { - w: imageElement.naturalWidth, - h: imageElement.naturalHeight, - }; - - imageObject.width = image.w; - imageObject.height = image.h; - imageElement.width = image.w; - imageElement.height = image.h; - imageObject.scaleToHeight(image.w); - imageObject.scaleToWidth(image.h); - const toScale = { - x: element.placement.width / image.w, - y: element.placement.height / image.h, - }; - imageObject.scaleX = toScale.x * element.placement.scaleX; - imageObject.scaleY = toScale.y * element.placement.scaleY; - canvas.add(imageObject); - canvas.on("object:modified", function (e) { - if (!e.target) return; - const target = e.target; - if (target != imageObject) return; - const placement = element.placement; - let fianlScale = 1; - if (target.scaleX && target.scaleX > 0) { - fianlScale = target.scaleX / toScale.x; - } - const newPlacement: Placement = { - ...placement, - x: target.left ?? placement.x, - y: target.top ?? placement.y, - rotation: target.angle ?? placement.rotation, - scaleX: fianlScale, - scaleY: fianlScale, - }; - const newElement = { - ...element, - placement: newPlacement, - }; - store.updateEditorElement(newElement); - }); - break; - } - case "audio": { - break; - } - case "text": { - const textObject = new fabric.Textbox(element.properties.text, { - name: element.id, - left: element.placement.x, - top: element.placement.y, - scaleX: element.placement.scaleX, - scaleY: element.placement.scaleY, - width: element.placement.width, - height: element.placement.height, - angle: element.placement.rotation, - fontSize: element.properties.fontSize, - fontWeight: element.properties.fontWeight, - objectCaching: false, - selectable: true, - lockUniScaling: true, - fill: "#ffffff", - }); - element.fabricObject = textObject; - canvas.add(textObject); - canvas.on("object:modified", function (e) { - if (!e.target) return; - const target = e.target; - if (target != textObject) return; - const placement = element.placement; - const newPlacement: Placement = { - ...placement, - x: target.left ?? placement.x, - y: target.top ?? placement.y, - rotation: target.angle ?? placement.rotation, - width: target.width ?? placement.width, - height: target.height ?? placement.height, - scaleX: target.scaleX ?? placement.scaleX, - scaleY: target.scaleY ?? placement.scaleY, - }; - const newElement = { - ...element, - placement: newPlacement, - properties: { - ...element.properties, - // @ts-ignore - text: target?.text, - }, - }; - store.updateEditorElement(newElement); - }); - break; - } - default: { - throw new Error("Not implemented"); - } - } - if (element.fabricObject) { - element.fabricObject.on("selected", function (e) { - store.setSelectedElement(element); - }); - } - } - const selectedEditorElement = store.selectedElement; - if (selectedEditorElement && selectedEditorElement.fabricObject) { - canvas.setActiveObject(selectedEditorElement.fabricObject); - } - this.refreshAnimations(); - this.updateTimeTo(this.currentTimeInMs); - store.canvas.renderAll(); + }); } - } - export function isEditorAudioElement( element: EditorElement ): element is AudioEditorElement { @@ -921,14 +188,20 @@ export function isEditorImageElement( return element.type === "image"; } - -function getTextObjectsPartitionedByCharacters(textObject: fabric.Text, element: TextEditorElement): fabric.Text[] { +function getTextObjectsPartitionedByCharacters( + textObject: fabric.Text, + element: TextEditorElement +): fabric.Text[] { let copyCharsObjects: fabric.Text[] = []; // replace all line endings with blank - const characters = (textObject.text ?? "").split('').filter((m) => m !== '\n'); + const characters = (textObject.text ?? "") + .split("") + .filter((m) => m !== "\n"); const charObjects = textObject.__charBounds; if (!charObjects) return []; - const charObjectFixed = charObjects.map((m, index) => m.slice(0, m.length - 1).map(m => ({ m, index }))).flat(); + const charObjectFixed = charObjects + .map((m, index) => m.slice(0, m.length - 1).map((m) => ({ m, index }))) + .flat(); const lineHeight = textObject.getHeightOfLine(0); for (let i = 0; i < characters.length; i++) { if (!charObjectFixed[i]) continue; @@ -937,15 +210,15 @@ function getTextObjectsPartitionedByCharacters(textObject: fabric.Text, element: const scaleX = textObject.scaleX ?? 1; const scaleY = textObject.scaleY ?? 1; const charTextObject = new fabric.Text(char, { - left: charObject.left * scaleX + (element.placement.x), + left: charObject.left * scaleX + element.placement.x, scaleX: scaleX, scaleY: scaleY, - top: lineIndex * lineHeight * scaleY + (element.placement.y), + top: lineIndex * lineHeight * scaleY + element.placement.y, fontSize: textObject.fontSize, fontWeight: textObject.fontWeight, - fill: '#fff', + fill: "#fff", }); copyCharsObjects.push(charTextObject); } return copyCharsObjects; -} \ No newline at end of file +} diff --git a/src/types.ts b/src/types.ts index f8247d0..c2094f1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -92,7 +92,7 @@ export type SlideOutAnimation = AnimationBase<"slideOut", { textType:SlideTextType, }>; -export type Animation = +export type TAnimation = FadeInAnimation | FadeOutAnimation | SlideInAnimation