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:
wilandwil authored and GitHub committed 2024-05-30 17:14:04 -04:00
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>
);
};
+5
View File
@@ -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 = {
+74 -8
View File
@@ -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();
}
+11 -1
View File
@@ -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",
+2
View File
@@ -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>
+3 -6
View File
@@ -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` }}
/>
);
};
@@ -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",
}
+11 -6
View File
@@ -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(
@@ -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;
};
+4 -1
View File
@@ -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("");
+2 -1
View File
@@ -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";