mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-06 10:11:31 +00:00
Vertical Video and more aspect ratio options (#441)
_**WAITING FOR THE SERVER UPDATE FOR END-TO-END TESTING**_ - created ui and queue communication for switching camera aspect ratio done - editor's render camera read queue to switch camera aspect ratio - letterbox now available in camera view mode for a better preview of the aspect ratios - updated ui to match figma design after design update - vertical camera styling updates (by bflat) - finalized our render resolutions at different aspect ratio --------- Co-authored-by: wil <wil@storyteller.ai>
This commit is contained in:
1 parent
1c132d3423
commit
b3b30fdf49
25 files changed
+603
-120
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { Fragment, useState } from "react";
|
||||
import { ButtonHTMLAttributes, Fragment, useState } from "react";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import {
|
||||
IconDefinition,
|
||||
@@ -6,14 +6,20 @@ import {
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Button, ButtonProps, TransitionDialogue } from "~/components";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
type UnionedButtonProps = { label?: string } & ButtonProps;
|
||||
|
||||
interface ButtonDropdownProps {
|
||||
interface ButtonDropdownProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
label: string;
|
||||
icon?: IconDefinition;
|
||||
align?: "left" | "right";
|
||||
showSelected?: boolean;
|
||||
options: Array<{
|
||||
label: string;
|
||||
className?: string;
|
||||
icon?: IconDefinition;
|
||||
selected?: boolean;
|
||||
description?: string;
|
||||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
@@ -32,9 +38,12 @@ interface ButtonDropdownProps {
|
||||
}
|
||||
|
||||
export const ButtonDropdown = ({
|
||||
className,
|
||||
label,
|
||||
options,
|
||||
icon,
|
||||
align = "left",
|
||||
showSelected,
|
||||
}: ButtonDropdownProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedOptionIndex, setSelectedOptionIndex] = useState<number | null>(
|
||||
@@ -69,7 +78,12 @@ export const ButtonDropdown = ({
|
||||
<div className="relative">
|
||||
<Menu as="div" className="inline-block text-left">
|
||||
<Menu.Button as="div">
|
||||
<Button icon={faChevronDown} iconFlip={true} variant="secondary">
|
||||
<Button
|
||||
className={className}
|
||||
icon={faChevronDown}
|
||||
iconFlip={true}
|
||||
variant="secondary"
|
||||
>
|
||||
{icon ? <FontAwesomeIcon icon={icon} /> : null}
|
||||
{label}
|
||||
</Button>
|
||||
@@ -85,7 +99,10 @@ export const ButtonDropdown = ({
|
||||
>
|
||||
<Menu.Items
|
||||
static
|
||||
className="absolute left-0 mt-1 w-max divide-y divide-gray-100 overflow-hidden rounded-lg bg-brand-secondary py-1.5 focus:outline-none"
|
||||
className={twMerge(
|
||||
"absolute z-20 mt-1 w-max divide-y divide-gray-100 overflow-hidden rounded-lg bg-brand-secondary py-1.5 shadow-xl focus:outline-none",
|
||||
align === "left" ? "left-0" : "right-0",
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
{options.map((option, index) => (
|
||||
@@ -97,16 +114,51 @@ export const ButtonDropdown = ({
|
||||
{({ active }) => (
|
||||
<button
|
||||
disabled={option.disabled}
|
||||
className={`duration-50 bg-brand-secondary font-medium text-white transition-all ${
|
||||
active ? "bg-ui-controls-button/60" : ""
|
||||
} ${option.disabled ? "pointer-events-none opacity-40" : ""} group flex w-full items-center py-1.5 pl-7 pr-4 text-sm`.trim()}
|
||||
className={twMerge(
|
||||
"duration-50 bg-brand-secondary font-medium text-white transition-all",
|
||||
active ? "bg-ui-controls-button/60" : "",
|
||||
option.disabled
|
||||
? "pointer-events-none opacity-40"
|
||||
: "",
|
||||
"group flex w-full items-center py-1.5 pl-7 pr-4 text-sm",
|
||||
option.className,
|
||||
)}
|
||||
onClick={() => handleOptionClick(index)}
|
||||
>
|
||||
<div className="flex w-full">
|
||||
<div className="flex w-full items-center">
|
||||
{option.icon && (
|
||||
<FontAwesomeIcon
|
||||
icon={option.icon}
|
||||
className="mr-2"
|
||||
/>
|
||||
)}
|
||||
<div className="grow text-start">{option.label}</div>
|
||||
<div className="ml-10 font-normal text-white/75">
|
||||
{option.description && option.description}
|
||||
</div>
|
||||
{showSelected && (
|
||||
<>
|
||||
{option.selected ? (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
className="ml-3 flex h-5"
|
||||
>
|
||||
<path
|
||||
opacity="1"
|
||||
d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c-9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"
|
||||
fill="#FC6B68"
|
||||
/>
|
||||
<path
|
||||
d="M369 175c-9.4 9.4-9.4 24.6 0 33.9L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c-9.4-9.4 24.6-9.4 33.9 0z"
|
||||
fill="#FFFFFF"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<div className="w-8" />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -9,7 +9,7 @@ interface ButtonIconProps extends FontAwesomeIconProps {
|
||||
icon: IconDefinition;
|
||||
onClick: () => void;
|
||||
className?: string;
|
||||
hoverFill?: boolean;
|
||||
bgFill?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
@@ -18,13 +18,13 @@ export const ButtonIcon = ({
|
||||
size,
|
||||
onClick,
|
||||
className: propsClassName,
|
||||
hoverFill = false,
|
||||
bgFill = false,
|
||||
disabled,
|
||||
...rest
|
||||
}: ButtonIconProps) => {
|
||||
const className = twMerge(
|
||||
"flex h-8 w-8 items-center justify-center rounded-lg transition-all duration-150",
|
||||
hoverFill
|
||||
"box-content flex h-8 w-8 items-center justify-center rounded-lg transition-all duration-150",
|
||||
bgFill
|
||||
? "bg-ui-controls-button hover:bg-ui-controls-button/[0.75]"
|
||||
: "bg-transparent hover:bg-ui-panel/[0.4]",
|
||||
disabled && "opacity-50 hover:bg-transparent",
|
||||
@@ -33,7 +33,7 @@ export const ButtonIcon = ({
|
||||
|
||||
return (
|
||||
<button className={className} onClick={onClick} disabled={disabled}>
|
||||
<FontAwesomeIcon icon={icon} size={size} {...rest}/>
|
||||
<FontAwesomeIcon icon={icon} size={size} {...rest} />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -9,6 +9,11 @@ import { ToastTypes } from "~/enums";
|
||||
import { toasts, deleteToast } from "~/signals";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
export type ToastDataType = {
|
||||
type: ToastTypes;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export const Toaster = () => {
|
||||
useSignals();
|
||||
const TITLES = {
|
||||
|
||||
@@ -18,7 +18,11 @@ import { TimeLine } from "./timeline.js";
|
||||
import { LipSyncEngine } from "./lip_sync_engine.js";
|
||||
import { AnimationEngine } from "./animation_engine.js";
|
||||
import { PointerLockControls } from "three/addons/controls/PointerLockControls.js";
|
||||
import { ClipGroup, EditorStates } from "~/pages/PageEnigma/enums";
|
||||
import {
|
||||
ClipGroup,
|
||||
EditorStates,
|
||||
CameraAspectRatio,
|
||||
} from "~/pages/PageEnigma/enums";
|
||||
import { AssetType } from "~/enums";
|
||||
import { XYZ } from "../datastructures/common";
|
||||
import Queue from "~/pages/PageEnigma/Queue/Queue";
|
||||
@@ -52,6 +56,8 @@ class Editor {
|
||||
activeScene: Scene;
|
||||
camera: THREE.PerspectiveCamera | null = null;
|
||||
render_camera: THREE.PerspectiveCamera | null = null;
|
||||
render_camera_aspect_ratio: CameraAspectRatio =
|
||||
CameraAspectRatio.HORIZONTAL_16_9;
|
||||
renderer: THREE.WebGLRenderer | undefined;
|
||||
rawRenderer: THREE.WebGLRenderer | undefined;
|
||||
clock: THREE.Clock | undefined;
|
||||
@@ -135,6 +141,7 @@ class Editor {
|
||||
save_manager: SaveManager;
|
||||
|
||||
generation_options: GenerationOptions;
|
||||
|
||||
constructor() {
|
||||
console.log(
|
||||
"If you see this message twice! then it rendered twice, if you see it once it's all good.",
|
||||
@@ -184,8 +191,9 @@ class Editor {
|
||||
this.selectedCanvas = false;
|
||||
// Audio Engine Test.
|
||||
|
||||
this.render_width = 1280;
|
||||
this.render_height = 720;
|
||||
this.render_camera_aspect_ratio = CameraAspectRatio.HORIZONTAL_16_9;
|
||||
this.render_width = this.getRenderDimensions().width;
|
||||
this.render_height = this.getRenderDimensions().height;
|
||||
|
||||
this.audio_engine = new AudioEngine();
|
||||
this.emotion_engine = new EmotionEngine(this.version);
|
||||
@@ -230,7 +238,32 @@ class Editor {
|
||||
lipSync: false,
|
||||
};
|
||||
}
|
||||
|
||||
getRenderDimensions() {
|
||||
switch (this.render_camera_aspect_ratio) {
|
||||
case CameraAspectRatio.HORIZONTAL_16_9: {
|
||||
return {
|
||||
width: 1280,
|
||||
height: 720,
|
||||
aspectRatio: 16 / 9,
|
||||
};
|
||||
}
|
||||
case CameraAspectRatio.VERTICAL_9_16: {
|
||||
return {
|
||||
width: 720,
|
||||
height: 1280,
|
||||
aspectRatio: 9 / 16,
|
||||
};
|
||||
}
|
||||
case CameraAspectRatio.SQUARE_1_1:
|
||||
default: {
|
||||
return {
|
||||
width: 1080,
|
||||
height: 1080,
|
||||
aspectRatio: 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
isEmpty(value: string | null) {
|
||||
return value === null || value.trim().length === 0;
|
||||
}
|
||||
@@ -266,6 +299,23 @@ class Editor {
|
||||
updateCamViewCanvas(newCanvas: HTMLCanvasElement) {
|
||||
this.canvasRenderCamReference = newCanvas;
|
||||
}
|
||||
|
||||
changeRenderCameraAspectRatio(newAspectRatio: CameraAspectRatio) {
|
||||
this.render_camera_aspect_ratio = newAspectRatio;
|
||||
const { width, height, aspectRatio } = this.getRenderDimensions();
|
||||
this.render_width = width;
|
||||
this.render_height = height;
|
||||
if (this.render_camera) {
|
||||
this.render_camera.aspect = aspectRatio;
|
||||
this.render_camera.updateProjectionMatrix();
|
||||
}
|
||||
|
||||
Queue.publish({
|
||||
queueName: QueueNames.FROM_ENGINE,
|
||||
action: fromEngineActions.CAMERA_ASPECT_RATIO_CHANGED,
|
||||
data: this.render_camera_aspect_ratio,
|
||||
});
|
||||
}
|
||||
initialize({
|
||||
sceneToken,
|
||||
editorCanvasEl,
|
||||
@@ -397,6 +447,8 @@ class Editor {
|
||||
}
|
||||
this.control.addEventListener("change", this.renderScene.bind(this));
|
||||
this.control.addEventListener("dragging-changed", (event: any) => {
|
||||
//TODO: any should be the following
|
||||
// (event: THREE.Event<"dragging-changed", TransformControls>) => {
|
||||
if (this.orbitControls == undefined) {
|
||||
return;
|
||||
}
|
||||
@@ -683,8 +735,23 @@ class Editor {
|
||||
|
||||
if (this.rendering && this.rawRenderer && this.clock && this.renderer) {
|
||||
if (this.recorder === undefined && this.render_camera) {
|
||||
this.rawRenderer.setSize(1024, 576);
|
||||
this.render_camera.aspect = 1024 / 576;
|
||||
const width =
|
||||
this.render_camera_aspect_ratio === CameraAspectRatio.HORIZONTAL_16_9
|
||||
? 1024
|
||||
: this.render_camera_aspect_ratio ===
|
||||
CameraAspectRatio.VERTICAL_9_16
|
||||
? 576
|
||||
: 1000;
|
||||
const height =
|
||||
this.render_camera_aspect_ratio === CameraAspectRatio.HORIZONTAL_16_9
|
||||
? 576
|
||||
: this.render_camera_aspect_ratio ===
|
||||
CameraAspectRatio.VERTICAL_9_16
|
||||
? 1024
|
||||
: 1000;
|
||||
|
||||
this.rawRenderer.setSize(width, height);
|
||||
this.render_camera.aspect = width / height;
|
||||
}
|
||||
|
||||
this.render_timer += this.clock.getDelta();
|
||||
@@ -950,8 +1017,7 @@ class Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
//this.renderer.setSize(this.render_width, this.render_height);
|
||||
this.render_camera.aspect = this.render_width / this.render_height;
|
||||
this.render_camera.aspect = this.getRenderDimensions().aspectRatio;
|
||||
this.render_camera.updateProjectionMatrix();
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,11 @@ import { QueueNames } from "../Queue/QueueNames";
|
||||
import { toEngineActions } from "../Queue/toEngineActions";
|
||||
import { fromEngineActions } from "../Queue/fromEngineActions";
|
||||
import { AssetType } from "~/enums";
|
||||
import { ClipGroup, ClipType } from "~/pages/PageEnigma/enums";
|
||||
import {
|
||||
CameraAspectRatio,
|
||||
ClipGroup,
|
||||
ClipType,
|
||||
} from "~/pages/PageEnigma/enums";
|
||||
import { Keyframe, MediaItem, UpdateTime } from "~/pages/PageEnigma/models";
|
||||
import Editor from "~/pages/PageEnigma/Editor/editor";
|
||||
import EmotionEngine from "./emotion_engine";
|
||||
@@ -179,6 +183,12 @@ export class TimeLine {
|
||||
this.editorEngine.generateVideo();
|
||||
break;
|
||||
}
|
||||
case toEngineActions.CHANGE_CAMERA_ASPECT_RATIO: {
|
||||
this.editorEngine.changeRenderCameraAspectRatio(
|
||||
data.data as CameraAspectRatio,
|
||||
);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
console.log("Action Not Wired", action);
|
||||
}
|
||||
|
||||
@@ -211,8 +211,8 @@ export class VideoGeneration {
|
||||
"anullsrc", // This adds a silent audio track
|
||||
"-max_muxing_queue_size",
|
||||
"999999",
|
||||
"-vf",
|
||||
"select=gte(n\\,1),scale=1024:576", // scale=1024:576
|
||||
// "-vf",
|
||||
// "select=gte(n\\,1),scale=1024:576", // scale=1024:576
|
||||
"-c:v",
|
||||
"libx264", // Specify video codec (optional, but recommended for MP4)
|
||||
"-c:a",
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from "~/pages/PageEnigma/signals";
|
||||
import { EditorCanvas } from "./comps/EngineCanvases";
|
||||
import { SceneContainer } from "./comps/SceneContainer";
|
||||
import { AspectRatioMenu } from "./comps/AspectRatioMenu";
|
||||
|
||||
export const PageEditor = () => {
|
||||
useSignals();
|
||||
@@ -80,6 +81,7 @@ export const PageEditor = () => {
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<ControlsTopButtons />
|
||||
<Controls3D />
|
||||
<AspectRatioMenu />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { fromEngineActions } from "~/pages/PageEnigma/Queue/fromEngineActions";
|
||||
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
|
||||
|
||||
import { CameraAspectRatio } from "../enums";
|
||||
import {
|
||||
QueueClip,
|
||||
QueueKeyframe,
|
||||
@@ -11,12 +12,7 @@ import { toTimelineActions } from "./toTimelineActions";
|
||||
|
||||
import { ClipUI } from "../datastructures/clips/clip_ui";
|
||||
import { GenerationOptions } from "../models/generationOptions";
|
||||
import { ToastTypes } from "~/enums";
|
||||
|
||||
export type ToastDataType = {
|
||||
type: ToastTypes;
|
||||
message: string;
|
||||
};
|
||||
import { ToastDataType } from "~/components";
|
||||
|
||||
export type UnionedActionTypes =
|
||||
| fromEngineActions
|
||||
@@ -31,6 +27,7 @@ export type UnionedDataTypes =
|
||||
| MediaItem
|
||||
| ToastDataType
|
||||
| GenerationOptions
|
||||
| CameraAspectRatio
|
||||
| null;
|
||||
|
||||
export type QueueSubscribeType = {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export enum fromEngineActions {
|
||||
ADD_OBJECT = "add_object",
|
||||
CAMERA_ASPECT_RATIO_CHANGED = "camera_aspect_ratio_changed",
|
||||
// data = CameraAspectRatio
|
||||
DELETE_OBJECT = "delete_object",
|
||||
DESELECT_OBJECT = "deselect_object",
|
||||
RESET_TIMELINE = "reset_timeline",
|
||||
|
||||
@@ -27,6 +27,10 @@ export enum toEngineActions {
|
||||
// data=QueueClip
|
||||
TOGGLE_CAMERA_STATE = "toggle_camera",
|
||||
// data=null
|
||||
|
||||
CHANGE_CAMERA_ASPECT_RATIO = "change_camera_aspect_ratio",
|
||||
// data=CamperaAspectRatio
|
||||
|
||||
UNMUTE = "unmute",
|
||||
// data={version, type, group, object_uuid} if lipsync
|
||||
// data={version, type, group} if global_audio
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import {
|
||||
faRectangle,
|
||||
faRectangleVertical,
|
||||
faSquare,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { CameraAspectRatio } from "~/pages/PageEnigma/enums";
|
||||
import { cameraAspectRatio } from "~/pages/PageEnigma/signals";
|
||||
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
|
||||
import Queue from "~/pages/PageEnigma/Queue/Queue";
|
||||
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
|
||||
import { ButtonDropdown } from "~/components";
|
||||
|
||||
export const AspectRatioMenu = () => {
|
||||
useSignals();
|
||||
const buttonIcon =
|
||||
cameraAspectRatio.value === CameraAspectRatio.HORIZONTAL_16_9
|
||||
? faRectangle
|
||||
: cameraAspectRatio.value === CameraAspectRatio.VERTICAL_9_16
|
||||
? faRectangleVertical
|
||||
: faSquare;
|
||||
const buttonText =
|
||||
cameraAspectRatio.value === CameraAspectRatio.HORIZONTAL_16_9
|
||||
? "16:9 Horizontal"
|
||||
: cameraAspectRatio.value === CameraAspectRatio.VERTICAL_9_16
|
||||
? "9:16 Vertical"
|
||||
: "1:1 Squared";
|
||||
|
||||
const handleChangeAspectRatio = (newRatio: CameraAspectRatio) => {
|
||||
Queue.publish({
|
||||
queueName: QueueNames.TO_ENGINE,
|
||||
action: toEngineActions.CHANGE_CAMERA_ASPECT_RATIO,
|
||||
data: newRatio,
|
||||
});
|
||||
};
|
||||
return (
|
||||
<div className="absolute right-0 top-0 m-2 flex flex-col items-end">
|
||||
<ButtonDropdown
|
||||
label={`${buttonText}`}
|
||||
className="shadow-xl"
|
||||
icon={buttonIcon}
|
||||
align="right"
|
||||
showSelected={true}
|
||||
options={[
|
||||
{
|
||||
label: "16:9",
|
||||
icon: faRectangle,
|
||||
className: "pl-4",
|
||||
description: "Horizontal",
|
||||
selected:
|
||||
cameraAspectRatio.value === CameraAspectRatio.HORIZONTAL_16_9,
|
||||
onClick: () =>
|
||||
handleChangeAspectRatio(CameraAspectRatio.HORIZONTAL_16_9),
|
||||
},
|
||||
{
|
||||
label: "9:16",
|
||||
icon: faRectangleVertical,
|
||||
className: "pl-4",
|
||||
description: "Vertical",
|
||||
selected:
|
||||
cameraAspectRatio.value === CameraAspectRatio.VERTICAL_9_16,
|
||||
onClick: () =>
|
||||
handleChangeAspectRatio(CameraAspectRatio.VERTICAL_9_16),
|
||||
},
|
||||
{
|
||||
label: "1:1",
|
||||
icon: faSquare,
|
||||
className: "pl-4",
|
||||
description: "Squared",
|
||||
selected: cameraAspectRatio.value === CameraAspectRatio.SQUARE_1_1,
|
||||
onClick: () =>
|
||||
handleChangeAspectRatio(CameraAspectRatio.SQUARE_1_1),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./AspectRatioMenu";
|
||||
@@ -71,7 +71,7 @@ export const ControlsTopButtons = () => {
|
||||
title: sceneTitleInput,
|
||||
});
|
||||
}
|
||||
}, [sceneTitleInput]);
|
||||
}, [sceneTitleInput, editorEngine]);
|
||||
|
||||
const handleButtonNewFromTemplate = () => {
|
||||
handleButtonLoadScene();
|
||||
@@ -119,6 +119,7 @@ export const ControlsTopButtons = () => {
|
||||
<ButtonDropdown
|
||||
label="File"
|
||||
icon={faFile}
|
||||
className="shadow-xl"
|
||||
options={[
|
||||
{
|
||||
label: "New scene",
|
||||
@@ -246,6 +247,7 @@ export const ControlsTopButtons = () => {
|
||||
buttonProps={{
|
||||
variant: "secondary",
|
||||
label: "Help",
|
||||
className: "shadow-xl",
|
||||
}}
|
||||
dialogProps={{
|
||||
className: "max-w-6xl w-auto",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useContext, useEffect } from "react";
|
||||
import { useCallback, useContext } from "react";
|
||||
import { EngineContext } from "../contexts/EngineContext";
|
||||
|
||||
export const EditorCanvas = () => {
|
||||
@@ -23,7 +23,7 @@ export const EditorCanvas = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const CameraViewCanvas = () => {
|
||||
export const CameraViewCanvas = ({ className }: { className?: string }) => {
|
||||
const editorEngine = useContext(EngineContext);
|
||||
|
||||
const canvasCallbackRef = useCallback(
|
||||
@@ -40,5 +40,7 @@ export const CameraViewCanvas = () => {
|
||||
[editorEngine],
|
||||
);
|
||||
|
||||
return <canvas ref={canvasCallbackRef} id="camera-view" />;
|
||||
return (
|
||||
<canvas className={className} ref={canvasCallbackRef} id="camera-view" />
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,116 @@
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Button, ButtonIcon, Tooltip } from "~/components";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faBlinds,
|
||||
faBlindsRaised,
|
||||
faCameraViewfinder,
|
||||
faSpinnerThird,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import Queue from "~/pages/PageEnigma/Queue/Queue";
|
||||
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
|
||||
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
|
||||
import {
|
||||
cameraAspectRatio,
|
||||
editorState,
|
||||
} from "~/pages/PageEnigma/signals/engine";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import {
|
||||
editorLetterBox,
|
||||
sidePanelHeight,
|
||||
toggleEditorLetterBox,
|
||||
} from "../../signals";
|
||||
import { CameraAspectRatio, EditorStates } from "~/pages/PageEnigma/enums";
|
||||
import { CameraViewCanvas } from "../EngineCanvases";
|
||||
|
||||
export const PreviewEngineCamera = () => {
|
||||
useSignals();
|
||||
|
||||
if (editorState.value === EditorStates.PREVIEW) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleButtonCameraView = () => {
|
||||
Queue.publish({
|
||||
queueName: QueueNames.TO_ENGINE,
|
||||
action: toEngineActions.TOGGLE_CAMERA_STATE,
|
||||
data: null,
|
||||
});
|
||||
};
|
||||
|
||||
const getScale = () => {
|
||||
const height = sidePanelHeight.value;
|
||||
const scaleHeight = height < 480 ? height / 480 : 1;
|
||||
return scaleHeight;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
id="preview-engine-camera"
|
||||
className="absolute bottom-0 m-3 origin-bottom-left shadow-lg"
|
||||
style={{ transform: `scale(${getScale()})` }}
|
||||
>
|
||||
<div className="relative">
|
||||
<div
|
||||
className={twMerge(
|
||||
"-z-10 flex w-full flex-wrap items-center rounded-t-lg bg-ui-controls p-1.5 text-white",
|
||||
cameraAspectRatio.value !== CameraAspectRatio.VERTICAL_9_16
|
||||
? "h-11 w-72 justify-between"
|
||||
: "h-20 w-44 justify-center",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={twMerge(
|
||||
"ms-1 flex grow items-center gap-2",
|
||||
cameraAspectRatio.value === CameraAspectRatio.VERTICAL_9_16 &&
|
||||
"-ms-1 justify-center",
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCameraViewfinder} className="text-sm" />
|
||||
<p className="mt-[2px] text-sm font-medium">Camera View</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1.5">
|
||||
{editorState.value === EditorStates.CAMERA_VIEW && (
|
||||
<Tooltip content="Toggle Letterbox" position={"top"}>
|
||||
<ButtonIcon
|
||||
icon={editorLetterBox.value ? faBlinds : faBlindsRaised}
|
||||
onClick={() => toggleEditorLetterBox()}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="action"
|
||||
onClick={handleButtonCameraView}
|
||||
className="rounded-md px-2 py-1 text-sm"
|
||||
>
|
||||
{editorState.value === EditorStates.EDIT
|
||||
? "Enter Camera View"
|
||||
: "Exit View"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={twMerge(
|
||||
"relative overflow-hidden rounded-b-lg border border-gray-600",
|
||||
cameraAspectRatio.value === CameraAspectRatio.HORIZONTAL_16_9
|
||||
? "aspect-[16/9]"
|
||||
: cameraAspectRatio.value === CameraAspectRatio.VERTICAL_9_16
|
||||
? "aspect-[9/16]"
|
||||
: cameraAspectRatio.value === CameraAspectRatio.SQUARE_1_1
|
||||
? "aspect-[1/1]"
|
||||
: "aspect-video",
|
||||
)}
|
||||
>
|
||||
<div className="flex h-full w-full items-center justify-center bg-ui-panel">
|
||||
<FontAwesomeIcon icon={faSpinnerThird} size={"3x"} spin />
|
||||
</div>
|
||||
<div className="absolute left-0 top-0 h-full w-full">
|
||||
<CameraViewCanvas className="h-full w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,72 +1 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button, LoadingDotsTyping } from "~/components";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faCameraViewfinder } from "@fortawesome/pro-solid-svg-icons";
|
||||
import Queue from "~/pages/PageEnigma/Queue/Queue";
|
||||
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
|
||||
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
|
||||
import { editorState } from "~/pages/PageEnigma/signals/engine";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { sidePanelHeight } from "../../signals";
|
||||
import { EditorStates } from "~/pages/PageEnigma/enums";
|
||||
import { CameraViewCanvas } from "../EngineCanvases";
|
||||
|
||||
export const PreviewEngineCamera = () => {
|
||||
useSignals();
|
||||
const [showLoader, setShowLoader] = useState<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => setShowLoader(false), 1000);
|
||||
}, []);
|
||||
|
||||
if (editorState.value === EditorStates.PREVIEW) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleButtonCameraView = () => {
|
||||
Queue.publish({
|
||||
queueName: QueueNames.TO_ENGINE,
|
||||
action: toEngineActions.TOGGLE_CAMERA_STATE,
|
||||
data: null,
|
||||
});
|
||||
};
|
||||
|
||||
const getScale = () => {
|
||||
const height = sidePanelHeight.value;
|
||||
const scaleHeight = height < 480 ? height / 480 : 1;
|
||||
return scaleHeight;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
id="preview-engine-camera"
|
||||
className="absolute bottom-0 m-3 origin-bottom-left shadow-lg"
|
||||
style={{ transform: `scale(${getScale()})` }}
|
||||
>
|
||||
<div className="relative">
|
||||
<div className="-z-10 flex w-full items-center justify-between rounded-t-lg bg-ui-controls p-1.5 text-white">
|
||||
<div className="ms-1 flex grow items-center gap-2">
|
||||
<FontAwesomeIcon icon={faCameraViewfinder} className="text-sm" />
|
||||
<p className="mt-[2px] text-sm font-medium">Camera View</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="action"
|
||||
onClick={handleButtonCameraView}
|
||||
className="rounded-md px-2 py-1 text-sm"
|
||||
>
|
||||
{editorState.value === EditorStates.EDIT
|
||||
? "Enter Camera View"
|
||||
: "Exit Camera View"}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="box relative overflow-hidden rounded-b-lg border border-gray-600">
|
||||
<CameraViewCanvas />
|
||||
<div className="absolute left-0 top-0 h-full w-full">
|
||||
<LoadingDotsTyping isShowing={showLoader} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export * from "./PreviewEngineCamera";
|
||||
@@ -0,0 +1,88 @@
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { EditorStates } from "~/pages/PageEnigma/enums";
|
||||
import { editorState, cameraAspectRatio } from "~/pages/PageEnigma/signals";
|
||||
|
||||
import {
|
||||
calcMatteWidth,
|
||||
calcMatteHeight,
|
||||
getMatteOrientation,
|
||||
MatteOrientation,
|
||||
} from "./helpers";
|
||||
|
||||
export const Letterbox = ({
|
||||
isShowing,
|
||||
width,
|
||||
height,
|
||||
}: {
|
||||
isShowing: boolean;
|
||||
width: number;
|
||||
height: number;
|
||||
}) => {
|
||||
useSignals();
|
||||
|
||||
if (editorState.value !== EditorStates.CAMERA_VIEW || !isShowing) {
|
||||
return;
|
||||
}
|
||||
|
||||
const matteOri = getMatteOrientation({
|
||||
camAspect: cameraAspectRatio.value,
|
||||
width,
|
||||
height,
|
||||
});
|
||||
|
||||
const matteWidth = calcMatteWidth({
|
||||
matteOri,
|
||||
camAspect: cameraAspectRatio.value,
|
||||
width,
|
||||
height,
|
||||
});
|
||||
|
||||
const matteHeight = calcMatteHeight({
|
||||
matteOri,
|
||||
camAspect: cameraAspectRatio.value,
|
||||
width,
|
||||
height,
|
||||
});
|
||||
console.log(matteOri, "matteOri");
|
||||
console.log(matteWidth, "matteWidth");
|
||||
console.log(matteHeight, "matteHeight");
|
||||
return (
|
||||
<div
|
||||
id="letterbox"
|
||||
className={twMerge(
|
||||
"absolute left-0 top-0 flex h-full w-full justify-between",
|
||||
matteOri === MatteOrientation.TOP_BOTTOM ? "flex-col" : null,
|
||||
)}
|
||||
>
|
||||
<Matte matteOri={matteOri} width={matteWidth} height={matteHeight} />
|
||||
<Matte matteOri={matteOri} width={matteWidth} height={matteHeight} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Matte = ({
|
||||
matteOri,
|
||||
width,
|
||||
height,
|
||||
}: {
|
||||
matteOri: MatteOrientation;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}) => {
|
||||
if (matteOri === MatteOrientation.TOP_BOTTOM) {
|
||||
return (
|
||||
<div
|
||||
className="h-20 w-full bg-brand-secondary/50 brightness-75"
|
||||
style={{ height: `${height}px` }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="h-full w-80 bg-brand-secondary/50 brightness-75"
|
||||
style={{ width: `${width}px` }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+17
-6
@@ -1,17 +1,26 @@
|
||||
import { useCallback, useContext } from "react";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { EngineContext } from "../contexts/EngineContext";
|
||||
|
||||
import { EngineContext } from "~/pages/PageEnigma/contexts/EngineContext";
|
||||
import { pageHeight, pageWidth } from "~/signals";
|
||||
import {
|
||||
timelineHeight,
|
||||
sidePanelWidth,
|
||||
sidePanelVisible,
|
||||
editorLetterBox,
|
||||
} from "~/pages/PageEnigma/signals";
|
||||
|
||||
import { Letterbox } from "./Letterbox";
|
||||
|
||||
export const SceneContainer = ({ children }: { children: React.ReactNode }) => {
|
||||
useSignals();
|
||||
const editorEngine = useContext(EngineContext);
|
||||
|
||||
const containerWidth =
|
||||
pageWidth.value - (sidePanelVisible.value ? sidePanelWidth.value : 0) - 84;
|
||||
|
||||
const containerHeight = pageHeight.value - timelineHeight.value - 68;
|
||||
|
||||
const callbackRef = useCallback(
|
||||
(node: HTMLDivElement) => {
|
||||
if (node && editorEngine && editorEngine.setSceneContainer) {
|
||||
@@ -27,14 +36,16 @@ export const SceneContainer = ({ children }: { children: React.ReactNode }) => {
|
||||
id="video-scene-container"
|
||||
className="relative"
|
||||
style={{
|
||||
width:
|
||||
pageWidth.value -
|
||||
(sidePanelVisible.value ? sidePanelWidth.value : 0) -
|
||||
84,
|
||||
height: pageHeight.value - timelineHeight.value - 68,
|
||||
width: containerWidth,
|
||||
height: containerHeight,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<Letterbox
|
||||
isShowing={editorLetterBox.value}
|
||||
width={containerWidth}
|
||||
height={containerHeight}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
import { CameraAspectRatio } from "../../enums";
|
||||
|
||||
export enum MatteOrientation {
|
||||
TOP_BOTTOM,
|
||||
LEFT_RIGHT,
|
||||
}
|
||||
|
||||
export const getMatteOrientation = ({
|
||||
camAspect,
|
||||
width,
|
||||
height,
|
||||
}: {
|
||||
camAspect: CameraAspectRatio;
|
||||
width: number;
|
||||
height: number;
|
||||
}): MatteOrientation => {
|
||||
switch (camAspect) {
|
||||
case CameraAspectRatio.SQUARE_1_1: {
|
||||
return width > height
|
||||
? MatteOrientation.LEFT_RIGHT
|
||||
: MatteOrientation.TOP_BOTTOM;
|
||||
}
|
||||
case CameraAspectRatio.VERTICAL_9_16: {
|
||||
return (height / 16) * 9 - width >= 0
|
||||
? MatteOrientation.TOP_BOTTOM
|
||||
: MatteOrientation.LEFT_RIGHT;
|
||||
}
|
||||
case CameraAspectRatio.HORIZONTAL_16_9:
|
||||
default: {
|
||||
return (width / 16) * 9 - height >= 0
|
||||
? MatteOrientation.LEFT_RIGHT
|
||||
: MatteOrientation.TOP_BOTTOM;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const calcMatteWidth = ({
|
||||
matteOri,
|
||||
camAspect,
|
||||
width,
|
||||
height,
|
||||
}: {
|
||||
matteOri: MatteOrientation;
|
||||
camAspect: CameraAspectRatio;
|
||||
width: number;
|
||||
height: number;
|
||||
}): number | undefined => {
|
||||
if (matteOri === MatteOrientation.TOP_BOTTOM) {
|
||||
return undefined;
|
||||
}
|
||||
switch (camAspect) {
|
||||
case CameraAspectRatio.SQUARE_1_1: {
|
||||
return (width - height) / 2;
|
||||
}
|
||||
case CameraAspectRatio.HORIZONTAL_16_9: {
|
||||
return (width - (height / 9) * 16) / 2;
|
||||
}
|
||||
case CameraAspectRatio.VERTICAL_9_16: {
|
||||
return (width - (height / 16) * 9) / 2;
|
||||
}
|
||||
default: {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const calcMatteHeight = ({
|
||||
matteOri,
|
||||
camAspect,
|
||||
width,
|
||||
height,
|
||||
}: {
|
||||
matteOri: MatteOrientation;
|
||||
camAspect: CameraAspectRatio;
|
||||
width: number;
|
||||
height: number;
|
||||
}): number | undefined => {
|
||||
if (matteOri === MatteOrientation.LEFT_RIGHT) {
|
||||
return undefined;
|
||||
}
|
||||
switch (camAspect) {
|
||||
case CameraAspectRatio.SQUARE_1_1: {
|
||||
return (height - width) / 2;
|
||||
}
|
||||
case CameraAspectRatio.HORIZONTAL_16_9: {
|
||||
return (height - (width / 16) * 9) / 2;
|
||||
}
|
||||
case CameraAspectRatio.VERTICAL_9_16: {
|
||||
return (height - (width / 9) * 16) / 2;
|
||||
}
|
||||
default: {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./SceneContainer";
|
||||
@@ -3,3 +3,9 @@ export enum EditorStates {
|
||||
CAMERA_VIEW,
|
||||
PREVIEW,
|
||||
}
|
||||
|
||||
export enum CameraAspectRatio {
|
||||
HORIZONTAL_16_9 = "horizontal_16_9",
|
||||
VERTICAL_9_16 = "vertical_9_16",
|
||||
SQUARE_1_1 = "SQUARE_1_1",
|
||||
}
|
||||
@@ -14,21 +14,22 @@ import {
|
||||
selectedObject,
|
||||
addKeyframe,
|
||||
} from "~/pages/PageEnigma/signals";
|
||||
import Queue, {
|
||||
QueueSubscribeType,
|
||||
ToastDataType,
|
||||
} from "~/pages/PageEnigma/Queue/Queue";
|
||||
import Queue, { QueueSubscribeType } from "~/pages/PageEnigma/Queue/Queue";
|
||||
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
|
||||
|
||||
import { ToastDataType } from "~/components";
|
||||
|
||||
import {
|
||||
MediaItem,
|
||||
QueueKeyframe,
|
||||
UpdateTime,
|
||||
} from "~/pages/PageEnigma/models";
|
||||
import { ClipGroup } from "~/pages/PageEnigma/enums";
|
||||
import { CameraAspectRatio, ClipGroup } from "~/pages/PageEnigma/enums";
|
||||
import { addToast } from "~/signals";
|
||||
import { ToastTypes } from "~/enums";
|
||||
import { toTimelineActions } from "~/pages/PageEnigma/Queue/toTimelineActions";
|
||||
import { ClipUI } from "~/pages/PageEnigma/datastructures/clips/clip_ui";
|
||||
import { cameraAspectRatio } from "../signals/engine";
|
||||
|
||||
const LOADING_FUNCTIONS: Record<ClipGroup, (item: ClipUI) => void> = {
|
||||
[ClipGroup.CHARACTER]: loadCharacterData,
|
||||
@@ -82,11 +83,15 @@ export function useQueueHandler() {
|
||||
addToast(ToastTypes.ERROR, message);
|
||||
break;
|
||||
}
|
||||
case fromEngineActions.CAMERA_ASPECT_RATIO_CHANGED: {
|
||||
cameraAspectRatio.value = data as CameraAspectRatio;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown action ${action}`);
|
||||
}
|
||||
},
|
||||
[deleteObjectOrCharacter],
|
||||
[],
|
||||
);
|
||||
|
||||
const handleToTimelineActions = useCallback(
|
||||
|
||||
+7
@@ -21,3 +21,10 @@ export const hideEditorLoader = () => {
|
||||
// it is important to keep the message so the loader fades out nicely
|
||||
};
|
||||
};
|
||||
|
||||
export const editorLetterBox = signal<boolean>(true);
|
||||
|
||||
export const toggleEditorLetterBox = (newState?: boolean) => {
|
||||
editorLetterBox.value =
|
||||
newState !== undefined ? newState : !editorLetterBox.value;
|
||||
};
|
||||
@@ -1,6 +1,9 @@
|
||||
import { signal } from "@preact/signals-core";
|
||||
import { EditorStates } from "~/pages/PageEnigma/enums";
|
||||
import { CameraAspectRatio, EditorStates } from "~/pages/PageEnigma/enums";
|
||||
|
||||
export const editorState = signal<EditorStates>(EditorStates.EDIT);
|
||||
export const cameraAspectRatio = signal<CameraAspectRatio>(
|
||||
CameraAspectRatio.HORIZONTAL_16_9,
|
||||
);
|
||||
|
||||
export const previewSrc = signal("");
|
||||
@@ -1,7 +1,8 @@
|
||||
export * from "./audioGroup";
|
||||
export * from "./cameraGroup";
|
||||
export * from "./characterGroups";
|
||||
export * from "./editorLoader";
|
||||
export * from "./editorOverlays";
|
||||
export * from "./engine";
|
||||
export * from "./dragAndDrop";
|
||||
export * from "./hotkeys";
|
||||
export * from "./media";
|
||||
|
||||
Reference in new issue
Block a user