mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 17:38:10 +00:00
Merge branch 'test' into feature/loading-saving-scene
This commit is contained in:
commit
580bc86d7e
46 files changed
+1565
-575
No files matched your search
@@ -0,0 +1,89 @@
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { Button } from "~/components/Button";
|
||||
import { Fragment } from "react";
|
||||
|
||||
interface Props {
|
||||
text: string;
|
||||
title: string;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onOk?: () => void;
|
||||
okText?: string;
|
||||
okColor?: string;
|
||||
onCancel?: () => void;
|
||||
cancelText?: string;
|
||||
}
|
||||
|
||||
export const ConfirmationModal = ({
|
||||
text,
|
||||
title,
|
||||
open,
|
||||
onClose,
|
||||
onOk,
|
||||
okText = "OK",
|
||||
okColor = "bg-brand-success",
|
||||
onCancel,
|
||||
cancelText = "Cancel",
|
||||
}: Props) => {
|
||||
return (
|
||||
<Transition appear show={open} as={Fragment}>
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black/40" />
|
||||
</Transition.Child>
|
||||
<div className="fixed inset-0 flex w-screen items-center justify-center p-4">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-xl border border-ui-panel-border bg-ui-panel p-5 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title
|
||||
as="h4"
|
||||
className="mb-4 text-xl font-bold text-white"
|
||||
>
|
||||
{title}
|
||||
</Dialog.Title>
|
||||
|
||||
<div className="mt-2">{text}</div>
|
||||
|
||||
<div className="mt-6 flex justify-end gap-2">
|
||||
{!!onCancel && (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="rounded-lg px-3 py-2"
|
||||
variant="secondary"
|
||||
>
|
||||
{cancelText}
|
||||
</Button>
|
||||
)}
|
||||
{!!onOk && (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onOk}
|
||||
className={[okColor, "rounded-lg px-3 py-2"].join(" ")}
|
||||
>
|
||||
{okText}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
);
|
||||
};
|
||||
@@ -1,16 +1,31 @@
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { H4 } from ".";
|
||||
|
||||
|
||||
interface ItemPickerProps extends React.ImgHTMLAttributes<HTMLImageElement> {
|
||||
label: string
|
||||
selected: boolean;
|
||||
onSelected : (val:string)=>void;
|
||||
}
|
||||
|
||||
export const ItemPicker = ({
|
||||
label,
|
||||
selected = false,
|
||||
onSelected,
|
||||
...imgProps
|
||||
}:ItemPickerProps) => {
|
||||
|
||||
const handleSelected = ()=>{
|
||||
onSelected(label);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative border border-ui-border rounded-lg overflow-hidden">
|
||||
<div
|
||||
className={twMerge("relative border-2 rounded-lg overflow-hidden cursor-pointer transition-colors ease-in-out", selected ? "border-brand-primary" : "border-ui-border")}
|
||||
onClick={handleSelected}
|
||||
>
|
||||
<img className="aspect-square min-w-20" {...imgProps} />
|
||||
<div className="bg-gradient-to-t from-gray-700 to-transparent absolute top-0 left-0 w-full h-full"/>
|
||||
<H4 className="absolute bottom-0 left-2 drop-shadow-md">{label}</H4>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
interface LoadingDotsProps {
|
||||
className?: string;
|
||||
show?: boolean;
|
||||
transition?: boolean;
|
||||
transitionDuration?: number;
|
||||
};
|
||||
|
||||
interface LoadingDotsInnerProps {
|
||||
className?: string;
|
||||
show?: boolean;
|
||||
transition?: boolean;
|
||||
transitionDuration?: number;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const LoadingDotsTyping = (props: LoadingDotsProps)=>{
|
||||
return (
|
||||
<LoadingDots {...props}>
|
||||
<div className="dot-typing"></div>
|
||||
</LoadingDots>
|
||||
)
|
||||
}
|
||||
export const LoadingDotsBricks = (props: LoadingDotsProps)=>{
|
||||
return (
|
||||
<LoadingDots {...props}>
|
||||
<div className="dot-bricks"></div>
|
||||
</LoadingDots>
|
||||
)
|
||||
}
|
||||
|
||||
function LoadingDots({
|
||||
className,
|
||||
show=true,
|
||||
transition = false,
|
||||
transitionDuration = 1000,
|
||||
children,
|
||||
}: LoadingDotsInnerProps){
|
||||
const [state, setState] = useState<'loading'|'loaded'|'completed'>('loading');
|
||||
const transitionClasses =
|
||||
(transition ? `transition-opacity duration-${transitionDuration} `: "")
|
||||
+ (state === 'loading' ? 'opacity-100' : 'opacity-0');
|
||||
|
||||
const classNames = twMerge("w-full h-full flex justify-center items-center bg-ui-background ",transitionClasses, className);
|
||||
useEffect(()=>{
|
||||
if(!show){
|
||||
if(transition){
|
||||
setState((curr)=>{
|
||||
if (curr === 'loading') return 'loaded'
|
||||
else return curr;
|
||||
});
|
||||
setTimeout(()=>setState('completed'), 2000);
|
||||
} else {
|
||||
setState('completed');
|
||||
}
|
||||
}
|
||||
}, [show, transition]);
|
||||
|
||||
if(state!== 'completed'){
|
||||
return(
|
||||
<div className={classNames}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}else{
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,11 +7,18 @@ export const Textarea = ({
|
||||
}:TextareaHTMLAttributes<HTMLTextAreaElement>) => {
|
||||
return(
|
||||
<textarea
|
||||
{...rest}
|
||||
|
||||
className={twMerge(
|
||||
"rounded-lg bg-ui-controls border border-ui-panel-border py-2 px-4"
|
||||
,className
|
||||
)}
|
||||
style={{
|
||||
outline: "2px solid transparent",
|
||||
transition: "outline-color 0.15s ease-in-out",
|
||||
}}
|
||||
onFocus={(e) => (e.currentTarget.style.outlineColor = "#e66462")}
|
||||
onBlur={(e) => (e.currentTarget.style.outlineColor = "transparent")}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { Input } from "./Input";
|
||||
import { H4 } from "./H4";
|
||||
import { ItemPicker } from "./ItemPicker";
|
||||
import { Textarea } from "./Textarea";
|
||||
import { LoadingDotsBricks, LoadingDotsTyping } from "./LoadingDots";
|
||||
|
||||
export {
|
||||
Button,
|
||||
@@ -16,4 +17,6 @@ export {
|
||||
ItemPicker,
|
||||
Textarea,
|
||||
Input,
|
||||
LoadingDotsBricks,
|
||||
LoadingDotsTyping,
|
||||
};
|
||||
@@ -1,128 +0,0 @@
|
||||
import { TrackContext } from "~/contexts/TrackContext/TrackContext";
|
||||
import { ReactNode, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import useUpdateCharacters from "~/contexts/TrackContext/utils/useUpdateCharacters";
|
||||
import useUpdateCamera from "~/contexts/TrackContext/utils/useUpdateCamera";
|
||||
import useUpdateAudio from "~/contexts/TrackContext/utils/useUpdateAudio";
|
||||
import useUpdateObject from "~/contexts/TrackContext/utils/useUpdateObject";
|
||||
import { AnimationClip, AudioClip } from "~/models/track";
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const TrackProvider = ({ children }: Props) => {
|
||||
const { characters, updateCharacters, toggleLipSyncMute } =
|
||||
useUpdateCharacters();
|
||||
const { camera, updateCamera } = useUpdateCamera();
|
||||
const { audio, updateAudio, toggleAudioMute } = useUpdateAudio();
|
||||
const { objects, updateObject } = useUpdateObject();
|
||||
const [selectedClip, setSelectedClip] = useState<string | null>(null);
|
||||
|
||||
const selectClip = useCallback((clipId: string | null) => {
|
||||
setSelectedClip(clipId);
|
||||
}, []);
|
||||
|
||||
const [state, setState] = useState<{
|
||||
dragType: "animations" | "lipSync" | null;
|
||||
dragId: string | null;
|
||||
}>({ dragType: null, dragId: null });
|
||||
const [animationClips, setAnimationClips] = useState<AnimationClip[]>([]);
|
||||
const [audioClips, setAudioClips] = useState<AudioClip[]>([]);
|
||||
|
||||
const startDrag = useCallback(
|
||||
(type: "animations" | "lipSync", id: string) => {
|
||||
setState({ dragId: id, dragType: type });
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const endDrag = useCallback(() => {
|
||||
setState({ dragId: null, dragType: null });
|
||||
}, []);
|
||||
|
||||
const [time, setTime] = useState(0);
|
||||
const updateCurrentTime = useCallback((newTime: number) => {
|
||||
setTime(newTime);
|
||||
}, []);
|
||||
|
||||
const [canDrop, setCanDrop] = useState(false);
|
||||
const [scale, setScale] = useState(1);
|
||||
const [length, setLength] = useState(1);
|
||||
|
||||
const fullWidth = useMemo(() => {
|
||||
return length * 60 * 4 * scale;
|
||||
}, [length, scale]);
|
||||
|
||||
useEffect(() => {
|
||||
setAnimationClips([
|
||||
{
|
||||
id: "ani-id1",
|
||||
offset: 0,
|
||||
length: 124,
|
||||
name: "ani 11",
|
||||
},
|
||||
]);
|
||||
setAudioClips([]);
|
||||
setScale(1);
|
||||
setLength(12);
|
||||
}, []);
|
||||
|
||||
const values = useMemo(() => {
|
||||
return {
|
||||
characters,
|
||||
camera,
|
||||
audio,
|
||||
objects,
|
||||
updateCharacters,
|
||||
updateCamera,
|
||||
updateAudio,
|
||||
updateObject,
|
||||
selectClip,
|
||||
selectedClip,
|
||||
toggleLipSyncMute,
|
||||
toggleAudioMute,
|
||||
dragType: state.dragType,
|
||||
dragId: state.dragId,
|
||||
animationClips,
|
||||
audioClips,
|
||||
startDrag,
|
||||
endDrag,
|
||||
scale,
|
||||
currentTime: time,
|
||||
length,
|
||||
updateCurrentTime,
|
||||
canDrop,
|
||||
setCanDrop,
|
||||
fullWidth,
|
||||
};
|
||||
}, [
|
||||
characters,
|
||||
updateCharacters,
|
||||
selectClip,
|
||||
selectedClip,
|
||||
camera,
|
||||
updateCamera,
|
||||
audio,
|
||||
updateAudio,
|
||||
objects,
|
||||
updateObject,
|
||||
toggleLipSyncMute,
|
||||
toggleAudioMute,
|
||||
state.dragId,
|
||||
state.dragType,
|
||||
animationClips,
|
||||
audioClips,
|
||||
startDrag,
|
||||
endDrag,
|
||||
canDrop,
|
||||
updateCurrentTime,
|
||||
setCanDrop,
|
||||
time,
|
||||
fullWidth,
|
||||
length,
|
||||
scale,
|
||||
]);
|
||||
return (
|
||||
<TrackContext.Provider value={values}>{children}</TrackContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,66 +0,0 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { AudioGroup } from "~/models/track";
|
||||
|
||||
export default function useUpdateAudio() {
|
||||
const [audio, setAudio] = useState<AudioGroup>({
|
||||
id: "AU1",
|
||||
muted: false,
|
||||
clips: [
|
||||
{
|
||||
id: "AU1-1",
|
||||
length: 200,
|
||||
offset: 0,
|
||||
name: "audio 1",
|
||||
},
|
||||
{
|
||||
id: "AU1-2",
|
||||
length: 180,
|
||||
offset: 300,
|
||||
name: "audio 2",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const updateAudio = useCallback(
|
||||
({
|
||||
id,
|
||||
offset,
|
||||
length,
|
||||
}: {
|
||||
id: string;
|
||||
length: number;
|
||||
offset: number;
|
||||
}) => {
|
||||
setAudio((oldAudio) => {
|
||||
const newClips = [...oldAudio.clips];
|
||||
const clipIndex = newClips.findIndex((row) => row.id === id);
|
||||
if (clipIndex === -1) {
|
||||
return { ...oldAudio };
|
||||
}
|
||||
const clip = newClips[clipIndex];
|
||||
clip.offset = offset;
|
||||
clip.length = length;
|
||||
return {
|
||||
...oldAudio,
|
||||
clips: newClips,
|
||||
};
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const toggleAudioMute = useCallback(() => {
|
||||
setAudio((oldAudio) => {
|
||||
return {
|
||||
...oldAudio,
|
||||
muted: !oldAudio.muted,
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
audio,
|
||||
updateAudio,
|
||||
toggleAudioMute,
|
||||
};
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { CharacterGroup } from "~/models/track";
|
||||
|
||||
export default function useUpdateCharacters() {
|
||||
const [characters, setCharacters] = useState<CharacterGroup[]>([
|
||||
{
|
||||
id: "CH1",
|
||||
muted: false,
|
||||
animationClips: [
|
||||
{
|
||||
id: "CH1-A1",
|
||||
length: 200,
|
||||
offset: 0,
|
||||
name: "ani 1",
|
||||
},
|
||||
{
|
||||
id: "CH1-A2",
|
||||
length: 180,
|
||||
offset: 300,
|
||||
name: "ani 2",
|
||||
},
|
||||
],
|
||||
positionClips: [
|
||||
{
|
||||
id: "CH1-P1",
|
||||
length: 200,
|
||||
offset: 100,
|
||||
name: "pos 1",
|
||||
},
|
||||
{
|
||||
id: "CH1-P2",
|
||||
length: 180,
|
||||
offset: 500,
|
||||
name: "pos 2",
|
||||
},
|
||||
],
|
||||
lipSyncClips: [
|
||||
{
|
||||
id: "CH1-L1",
|
||||
length: 400,
|
||||
offset: 150,
|
||||
name: "sync 1",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
const updateCharacters = useCallback(
|
||||
({
|
||||
type,
|
||||
id,
|
||||
offset,
|
||||
length,
|
||||
}: {
|
||||
type: "animations" | "positions" | "lipSync";
|
||||
id: string;
|
||||
length: number;
|
||||
offset: number;
|
||||
}) => {
|
||||
if (type === "animations") {
|
||||
setCharacters((oldCharacters) => {
|
||||
return oldCharacters.map((character) => {
|
||||
const newAnimationClips = [...character.animationClips];
|
||||
const clipIndex = newAnimationClips.findIndex(
|
||||
(row) => row.id === id,
|
||||
);
|
||||
if (clipIndex === -1) {
|
||||
return { ...character };
|
||||
}
|
||||
const clip = newAnimationClips[clipIndex];
|
||||
clip.offset = offset;
|
||||
clip.length = length;
|
||||
return {
|
||||
...character,
|
||||
animationClips: newAnimationClips,
|
||||
};
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (type === "positions") {
|
||||
setCharacters((oldCharacters) => {
|
||||
return oldCharacters.map((character) => {
|
||||
const newPositionClips = [...character.positionClips];
|
||||
const clipIndex = newPositionClips.findIndex(
|
||||
(row) => row.id === id,
|
||||
);
|
||||
if (clipIndex === -1) {
|
||||
return { ...character };
|
||||
}
|
||||
const clip = newPositionClips[clipIndex];
|
||||
clip.offset = offset;
|
||||
clip.length = length;
|
||||
return {
|
||||
...character,
|
||||
positionClips: newPositionClips,
|
||||
};
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (type === "lipSync") {
|
||||
setCharacters((oldCharacters) => {
|
||||
return oldCharacters.map((character) => {
|
||||
const newLipSyncClips = [...character.lipSyncClips];
|
||||
const clipIndex = newLipSyncClips.findIndex((row) => row.id === id);
|
||||
if (clipIndex === -1) {
|
||||
return { ...character };
|
||||
}
|
||||
const clip = newLipSyncClips[clipIndex];
|
||||
clip.offset = offset;
|
||||
clip.length = length;
|
||||
return {
|
||||
...character,
|
||||
lipSyncClips: newLipSyncClips,
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const toggleLipSyncMute = useCallback((characterId: string) => {
|
||||
setCharacters((oldCharacters) => {
|
||||
return oldCharacters.map((character) => ({
|
||||
...character,
|
||||
muted:
|
||||
character.id === characterId ? !character.muted : character.muted,
|
||||
}));
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
characters,
|
||||
updateCharacters,
|
||||
toggleLipSyncMute,
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import React, { useCallback, useContext } from "react";
|
||||
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
|
||||
|
||||
interface LowerPanelPropsI {
|
||||
timelineHeight: number;
|
||||
@@ -6,10 +7,22 @@ interface LowerPanelPropsI {
|
||||
}
|
||||
|
||||
export const LowerPanel = ({ children, timelineHeight }: LowerPanelPropsI) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const handleOpen = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
// const [open, setOpen] = useState(false);
|
||||
const { setOverTimeline, updateCurrentTime, scale } =
|
||||
useContext(TrackContext);
|
||||
|
||||
const onTimelineClick = useCallback(
|
||||
(event: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (event.button === 0) {
|
||||
updateCurrentTime((Math.round(event.clientX) - 92) / 4 / scale);
|
||||
}
|
||||
},
|
||||
[updateCurrentTime, scale],
|
||||
);
|
||||
|
||||
// const handleOpen = () => {
|
||||
// setOpen(true);
|
||||
// };
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -20,6 +33,11 @@ export const LowerPanel = ({ children, timelineHeight }: LowerPanelPropsI) => {
|
||||
"bg-ui-panel",
|
||||
].join(" ")}
|
||||
style={{ height: timelineHeight }}
|
||||
onPointerOver={() => {
|
||||
setOverTimeline(true);
|
||||
}}
|
||||
onPointerLeave={() => setOverTimeline(false)}
|
||||
onPointerDown={onTimelineClick}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PageEnigmaComponent } from "./PageEnigmaComponent";
|
||||
import { TrackProvider } from "~/contexts/TrackContext/TrackProvider";
|
||||
import { TrackProvider } from "~/pages/PageEnigma/contexts/TrackContext/TrackProvider";
|
||||
|
||||
export const PageEnigma = () => {
|
||||
return (
|
||||
|
||||
@@ -1,92 +1,92 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useRef, useReducer } from "react";
|
||||
|
||||
import { faSparkles } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
import { Button } from "~/components";
|
||||
import { Button, LoadingDotsBricks } from "~/components";
|
||||
import { TopBarHelmet } from "~/modules/TopBarHelmet/TopBarHelmet";
|
||||
import { SidePanel } from "~/modules/SidePanel";
|
||||
import { Controls3D } from "./comps/Controls3D";
|
||||
import { ControlsTopButtons } from "./comps/ControlsTopButtons";
|
||||
import { ControlsVideo } from "./comps/ControlsVideo";
|
||||
import { PreviewWindow } from "./comps/PreviewWindow";
|
||||
import { PreviewEngineCamera } from "./comps/PreviewEngineCamera";
|
||||
import { Timeline } from "./comps/Timeline";
|
||||
import { SidePanelTabs } from "./comps/SidePanelTabs";
|
||||
|
||||
import { EngineProvider } from "~/contexts/EngineProvider";
|
||||
import { EngineProvider } from "./contexts/EngineProvider";
|
||||
import { AppUIProvider } from "./contexts/AppUiContext";
|
||||
import { reducer, initialState, ACTION_TYPES } from "./reducer";
|
||||
import { VIEW_MODES } from "./reducer/types";
|
||||
import { ViewSideBySide } from "./comps/ViewSideBySide";
|
||||
|
||||
export const PageEnigmaComponent = () => {
|
||||
const [appUiState, dispatchAppUiState] = useReducer(reducer, initialState);
|
||||
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
|
||||
const [timelineHeight, setTimelineHeight] = useState(260);
|
||||
const timelineRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const updateTimelineHeight = useCallback(() => {
|
||||
if (timelineRef.current) {
|
||||
setTimelineHeight(timelineRef.current.offsetHeight);
|
||||
}
|
||||
}, []);
|
||||
|
||||
//for updating timeline/engine div height (for resizing)
|
||||
useEffect(() => {
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
for (let entry of entries) {
|
||||
updateTimelineHeight();
|
||||
}
|
||||
});
|
||||
|
||||
if (timelineRef.current) {
|
||||
observer.observe(timelineRef.current);
|
||||
}
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [updateTimelineHeight]);
|
||||
|
||||
useEffect(()=>{
|
||||
setTimeout(()=>dispatchAppUiState({
|
||||
type: ACTION_TYPES.HIDE_EDITOR_LOADER
|
||||
}), 3000);
|
||||
},[]);
|
||||
return (
|
||||
<div>
|
||||
<TopBarHelmet>
|
||||
<Button icon={faSparkles}>Generate Movie</Button>
|
||||
</TopBarHelmet>
|
||||
|
||||
<EngineProvider>
|
||||
<TopBarHelmet>
|
||||
<Button icon={faSparkles}>Generate Movie</Button>
|
||||
</TopBarHelmet>
|
||||
<div style={{ height: "calc(100vh - 68px)" }}>
|
||||
{/* Engine section/side panel */}
|
||||
<div
|
||||
id="CanvasUiWrapper"
|
||||
className="flex"
|
||||
// style={{ height: `calc(100% - ${timelineHeight}px)` }}
|
||||
style={{ height: `calc(100% - 260px` }}
|
||||
>
|
||||
<div className="relative w-full overflow-hidden bg-gray-400">
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
id="video-scene"
|
||||
width="1280px"
|
||||
height="720px"
|
||||
/>
|
||||
<AppUIProvider value={[appUiState, dispatchAppUiState]} >
|
||||
<div style={{ height: "calc(100vh - 68px)" }}>
|
||||
{/* Engine section/side panel */}
|
||||
<div
|
||||
id="CanvasUiWrapper"
|
||||
className="flex"
|
||||
style={{ height: `calc(100% - ${appUiState.timelineHeight}px)` }}
|
||||
// style={{ height: `calc(100% - 260px` }}
|
||||
>
|
||||
<div className="relative w-full overflow-hidden bg-transparent">
|
||||
<div className={(appUiState.viewMode === VIEW_MODES.SIDE_BY_SIDE) ? 'invisible' : ''}>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
id="video-scene"
|
||||
width="1280px"
|
||||
height="720px"
|
||||
/>
|
||||
|
||||
{/* Top controls */}
|
||||
<div className="absolute left-0 top-0 w-full">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<ControlsTopButtons />
|
||||
<Controls3D />
|
||||
{/* Top controls */}
|
||||
<div className="absolute left-0 top-0 w-full">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<ControlsTopButtons />
|
||||
<Controls3D />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom controls */}
|
||||
<div className="absolute bottom-0 left-0 w-full">
|
||||
<PreviewEngineCamera />
|
||||
<ControlsVideo />
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
appUiState.viewMode === VIEW_MODES.SIDE_BY_SIDE &&
|
||||
<ViewSideBySide />
|
||||
}
|
||||
<LoadingDotsBricks
|
||||
className="absolute top-0 left-0"
|
||||
show={appUiState.showEditorLoader}
|
||||
transition
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Bottom controls */}
|
||||
<div className="absolute bottom-0 left-0 w-full">
|
||||
<PreviewWindow />
|
||||
<ControlsVideo />
|
||||
</div>
|
||||
|
||||
{/* Side panel */}
|
||||
<SidePanel>
|
||||
<SidePanelTabs />
|
||||
</SidePanel>
|
||||
</div>
|
||||
|
||||
{/* Side panel */}
|
||||
<SidePanel>
|
||||
<SidePanelTabs />
|
||||
</SidePanel>
|
||||
{/* Timeline */}
|
||||
<Timeline timelineHeight={appUiState.timelineHeight} />
|
||||
</div>
|
||||
|
||||
{/* Timeline */}
|
||||
<Timeline timelineHeight={timelineHeight} />
|
||||
</div>
|
||||
</AppUIProvider>
|
||||
</EngineProvider>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
fa3dSphere,
|
||||
} from "@awesome.me/kit-fde2be5eb0/icons/kit/custom";
|
||||
import { ButtonIcon, ButtonIconSelect } from "~/components";
|
||||
import { EngineContext } from "~/contexts/EngineContext";
|
||||
import { EngineContext } from "../../contexts/EngineContext";
|
||||
import { useContext } from "react";
|
||||
|
||||
export const Controls3D = () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useContext, useState } from "react";
|
||||
import { Button, H4, Input } from "~/components";
|
||||
import { ButtonDialogue } from "~/modules/ButtonDialogue";
|
||||
import { EngineContext } from "~/contexts/EngineContext";
|
||||
import { EngineContext } from "../../contexts/EngineContext";
|
||||
|
||||
export const ControlsTopButtons = () => {
|
||||
const editorEngine = useContext(EngineContext);
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useContext, useEffect, useState } from "react"
|
||||
import { EngineContext } from "../../contexts/EngineContext"
|
||||
|
||||
import { LoadingDotsTyping } from "~/components";
|
||||
export const PreviewEngineCamera = ()=>{
|
||||
const editorEngine = useContext(EngineContext);
|
||||
//take data from egine context
|
||||
|
||||
const [showLoader, setShowLoader] = useState<boolean>(true);
|
||||
useEffect(()=>{
|
||||
setTimeout(()=>setShowLoader(false), 5000);
|
||||
},[]);
|
||||
return (
|
||||
<div
|
||||
id="preview-engine-camera"
|
||||
className="absolute bottom-0 w-30 m-4"
|
||||
>
|
||||
<div className="relative">
|
||||
<div
|
||||
className="text-white bg-ui-panel pt-1 px-2 pb-3 -z-10 rounded-t-lg -mb-2 w-fit"
|
||||
>
|
||||
<p>Camera View</p>
|
||||
</div>
|
||||
<div className="relative rounded-lg border border-white box overflow-hidden">
|
||||
|
||||
<img
|
||||
//shoot that datat from engine context to this image
|
||||
className="aspect-video max-h-40"
|
||||
src="/resources/uiAssets/video_player_placeholder.gif"
|
||||
/>
|
||||
<div className="absolute w-full h-full top-0 left-0">
|
||||
<LoadingDotsTyping show={showLoader} transition/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { useContext } from "react"
|
||||
import { EngineContext } from "~/contexts/EngineContext"
|
||||
|
||||
export const PreviewWindow = ()=>{
|
||||
const editorEngine = useContext(EngineContext);
|
||||
//take data from egine context
|
||||
|
||||
return (
|
||||
<div
|
||||
id="preview-window"
|
||||
className="absolute bottom-0 w-30 m-4"
|
||||
>
|
||||
<div className="relative">
|
||||
<div
|
||||
id="preview-window-label"
|
||||
className="text-white bg-ui-panel pt-1 px-2 pb-3 -z-10 rounded-t-lg -mb-2 w-fit"
|
||||
>
|
||||
<p>Camera View</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-white box overflow-hidden">
|
||||
<img
|
||||
//shoot that datat from engine context to this image
|
||||
className="aspect-video max-h-40"
|
||||
src="/resources/uiAssets/video_player_placeholder.gif"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useContext, useEffect, useState } from "react";
|
||||
import { BaseClip } from "~/models/track";
|
||||
import { TrackContext } from "~/contexts/TrackContext/TrackContext";
|
||||
import { BaseClip } from "~/pages/PageEnigma/models/track";
|
||||
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
|
||||
|
||||
interface Props {
|
||||
clip: BaseClip;
|
||||
@@ -8,8 +8,16 @@ interface Props {
|
||||
}
|
||||
|
||||
export const ClipElement = ({ clip, type }: Props) => {
|
||||
const { startDrag, dragId, endDrag, scale, canDrop, setCanDrop } =
|
||||
useContext(TrackContext);
|
||||
const {
|
||||
startDrag,
|
||||
dragId,
|
||||
endDrag,
|
||||
scale,
|
||||
canDrop,
|
||||
setCanDrop,
|
||||
overTimeline,
|
||||
dropId,
|
||||
} = useContext(TrackContext);
|
||||
const [initPosition, setInitPosition] = useState<{
|
||||
initX: number;
|
||||
initY: number;
|
||||
@@ -36,6 +44,8 @@ export const ClipElement = ({ clip, type }: Props) => {
|
||||
|
||||
const onMouseMove = (event: MouseEvent) => {
|
||||
if (dragId) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
const deltaX = event.clientX - initX;
|
||||
const deltaY = event.clientY - initY;
|
||||
setCurrPosition({ x: deltaX, y: deltaY });
|
||||
@@ -55,20 +65,24 @@ export const ClipElement = ({ clip, type }: Props) => {
|
||||
const onPointerDown = useCallback(
|
||||
(event: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (event.button === 0) {
|
||||
const box = document.getElementById(`ani-clip-${clip.id}`);
|
||||
const position = box!.getBoundingClientRect();
|
||||
|
||||
startDrag(type, clip.id);
|
||||
setCurrPosition({
|
||||
x: 0,
|
||||
y: 0,
|
||||
x: position.x,
|
||||
y: position.y + position.height + 1,
|
||||
});
|
||||
setInitPosition({
|
||||
initX: position.x,
|
||||
initY: position.y + position.height + 1,
|
||||
});
|
||||
setInitPosition({ initY: event.clientY, initX: event.clientX });
|
||||
setCanDrop(false);
|
||||
}
|
||||
},
|
||||
[type, clip.id, startDrag, setCanDrop],
|
||||
);
|
||||
|
||||
// console.log("clip", canDrop);
|
||||
|
||||
return (
|
||||
<div key={clip.id} className="relative h-16 w-16">
|
||||
<div
|
||||
@@ -82,16 +96,17 @@ export const ClipElement = ({ clip, type }: Props) => {
|
||||
<div
|
||||
id={`ani-dnd-${clip.id}`}
|
||||
className={[
|
||||
"absolute h-8 p-2",
|
||||
"absolute p-2",
|
||||
"rounded-lg",
|
||||
!canDrop ? "bg-brand-primary" : "bg-brand-secondary-700",
|
||||
dragId ? "block" : "hidden",
|
||||
].join(" ")}
|
||||
style={{
|
||||
top: y,
|
||||
left: x,
|
||||
top: overTimeline ? y + 24 : y,
|
||||
left: overTimeline ? x : x,
|
||||
zIndex: 5000,
|
||||
width: clip.length * 4 * scale,
|
||||
width: overTimeline ? clip.length * 4 * scale : 64,
|
||||
height: overTimeline ? 32 : 64,
|
||||
}}
|
||||
onPointerDown={onPointerDown}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useContext } from "react";
|
||||
import { ClipElement } from "./ClipElement";
|
||||
import { TrackContext } from "~/contexts/TrackContext/TrackContext";
|
||||
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
|
||||
|
||||
export const ClipElements = () => {
|
||||
const { animationClips } = useContext(TrackContext);
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
|
||||
import {
|
||||
H4,
|
||||
ItemPicker,
|
||||
Textarea
|
||||
} from "~/components"
|
||||
|
||||
|
||||
export const TabStyling = ()=>{
|
||||
const handlePickingStyle = (picked:string)=>{
|
||||
console.log(`Picked style: ${picked}`);
|
||||
}
|
||||
|
||||
return(
|
||||
<>
|
||||
<H4>Select Base Style</H4>
|
||||
<div className="flex gap-2 my-2">
|
||||
<ItemPicker
|
||||
label="Anime"
|
||||
onClick={()=>handlePickingStyle("anime")}
|
||||
src="/resources/avatars/0.webp"
|
||||
/>
|
||||
<ItemPicker
|
||||
label="Pixel"
|
||||
onClick={()=>handlePickingStyle("pixel")}
|
||||
src="/resources/avatars/1.webp"
|
||||
/>
|
||||
<ItemPicker
|
||||
label="Pixar"
|
||||
onClick={()=>handlePickingStyle("pixar")}
|
||||
src="/resources/avatars/2.webp"
|
||||
/>
|
||||
<ItemPicker
|
||||
label="Stylized"
|
||||
onClick={()=>handlePickingStyle("stylized")}
|
||||
src="/resources/avatars/3.webp"
|
||||
/>
|
||||
</div>
|
||||
<H4>Enter a Prompt</H4>
|
||||
<div className="flex gap-2 my-2">
|
||||
<Textarea className="w-full" />
|
||||
</div>
|
||||
<H4>Negative Prompt</H4>
|
||||
<div className="flex gap-2 my-2">
|
||||
<Textarea
|
||||
className="w-full"
|
||||
placeholder="Type here to filter out the things you don't want in the scene
|
||||
"/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { useContext, useState } from 'react';
|
||||
|
||||
import {
|
||||
Button,
|
||||
H4,
|
||||
ItemPicker,
|
||||
Textarea
|
||||
} from "~/components"
|
||||
|
||||
import { AppUiContext } from '../../contexts/AppUiContext';
|
||||
import { ACTION_TYPES } from '../../reducer';
|
||||
import { VIEW_MODES } from '../../reducer/types';
|
||||
|
||||
export const TabStylization = ()=>{
|
||||
const [appUiState, dispatchAppUiState] = useContext(AppUiContext);
|
||||
const [ selection, setSelection ] = useState<string>("Anime");
|
||||
const handlePickingStylizer = (picked:string)=>{
|
||||
console.log(`Picked style: ${picked}`);
|
||||
setSelection(picked);
|
||||
}
|
||||
|
||||
|
||||
const handleChangeViewMode = ()=>{
|
||||
if(dispatchAppUiState!==null){
|
||||
dispatchAppUiState({
|
||||
type: ACTION_TYPES.ON_CHANGE_VIEW_MODE,
|
||||
payload: {
|
||||
viewMode: appUiState?.viewMode === VIEW_MODES.SIDE_BY_SIDE
|
||||
? VIEW_MODES.EDITOR : VIEW_MODES.SIDE_BY_SIDE,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
return(
|
||||
<>
|
||||
<H4>Select Base Stylizer</H4>
|
||||
<div className="flex gap-2 my-2">
|
||||
<ItemPicker
|
||||
label="Anime"
|
||||
selected={(selection === "Anime")}
|
||||
onSelected={handlePickingStylizer}
|
||||
src="/resources/avatars/0.webp"
|
||||
/>
|
||||
<ItemPicker
|
||||
label="Pixel"
|
||||
selected={selection === "Pixel"}
|
||||
onSelected={handlePickingStylizer}
|
||||
src="/resources/avatars/1.webp"
|
||||
/>
|
||||
<ItemPicker
|
||||
label="Pixar"
|
||||
selected={selection === "Pixar"}
|
||||
onSelected={handlePickingStylizer}
|
||||
src="/resources/avatars/2.webp"
|
||||
/>
|
||||
<ItemPicker
|
||||
label="Stylized"
|
||||
selected={selection === "Stylized"}
|
||||
onSelected={handlePickingStylizer}
|
||||
src="/resources/avatars/3.webp"
|
||||
/>
|
||||
</div>
|
||||
<H4>Enter a Prompt</H4>
|
||||
<div className="flex gap-2 my-2">
|
||||
<Textarea
|
||||
className="w-full h-32"
|
||||
placeholder="Type here to describe your scene"
|
||||
/>
|
||||
</div>
|
||||
<H4>Negative Prompt</H4>
|
||||
<div className="flex gap-2 my-2">
|
||||
<Textarea
|
||||
className="w-full h-32"
|
||||
placeholder="Type here to filter out the things you don't want in the scene
|
||||
"/>
|
||||
</div>
|
||||
<div className="flex gap-2 mt-6 justify-center">
|
||||
<Button onClick={handleChangeViewMode}>
|
||||
{
|
||||
appUiState?.viewMode === VIEW_MODES.SIDE_BY_SIDE
|
||||
? "Back to Scene" : "Preview Side by Side"
|
||||
}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Tabs } from "~/modules/Tabs";
|
||||
import { TabStyling } from "./TabStyling";
|
||||
import { TabStylization} from "./TabStylization";
|
||||
import { ClipElements } from "./ClipElements";
|
||||
|
||||
export const SidePanelTabs = () => {
|
||||
@@ -19,8 +19,8 @@ export const SidePanelTabs = () => {
|
||||
children: <p>Audio Tab</p>,
|
||||
},
|
||||
{
|
||||
header: "Styling",
|
||||
children: <TabStyling />,
|
||||
header: "Stylization",
|
||||
children: <TabStylization />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useContext } from "react";
|
||||
import { TrackContext } from "~/contexts/TrackContext/TrackContext";
|
||||
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
|
||||
import { Track } from "~/pages/PageEnigma/comps/Timeline/Track";
|
||||
|
||||
export const Audio = () => {
|
||||
@@ -9,19 +9,20 @@ export const Audio = () => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="bg-audio-groupBg block rounded-lg pb-5 pl-2 pr-4"
|
||||
className="block rounded-lg bg-audio-groupBg pb-5 pl-2 pr-4"
|
||||
style={{ width: fullWidth + 90 }}
|
||||
>
|
||||
<div className="prevent-select mb-5 pt-2 text-xs font-medium text-white">
|
||||
Global Audio
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col gap-4">
|
||||
<Track
|
||||
id={audio!.id}
|
||||
clips={clips}
|
||||
title="Global Audio Track"
|
||||
updateClip={updateAudio}
|
||||
style="audio"
|
||||
muted={audio?.muted}
|
||||
muted={audio!.muted}
|
||||
toggleMute={toggleAudioMute}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useContext } from "react";
|
||||
import { TrackContext } from "~/contexts/TrackContext/TrackContext";
|
||||
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
|
||||
import { Track } from "~/pages/PageEnigma/comps/Timeline/Track";
|
||||
|
||||
export const Camera = () => {
|
||||
@@ -8,12 +8,13 @@ export const Camera = () => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="bg-camera-groupBg block rounded-lg pb-5 pl-2 pr-4"
|
||||
className="block rounded-lg bg-camera-groupBg pb-5 pl-2 pr-4"
|
||||
style={{ width: fullWidth + 90 }}
|
||||
>
|
||||
<div className="mb-5 pt-2 text-xs font-medium text-white">Camera</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col gap-4">
|
||||
<Track
|
||||
id={camera!.id}
|
||||
clips={clips}
|
||||
title="Camera Position/Rotation"
|
||||
style="camera"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useContext, useMemo } from "react";
|
||||
import { TrackContext } from "~/contexts/TrackContext/TrackContext";
|
||||
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
|
||||
import { Track } from "~/pages/PageEnigma/comps/Timeline/Track";
|
||||
|
||||
function buildUpdaters(
|
||||
@@ -61,8 +61,9 @@ export const Character = ({ characterId }: Props) => {
|
||||
<div className="prevent-select mb-5 pt-2 text-xs font-medium text-white">
|
||||
Character
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col gap-4">
|
||||
<Track
|
||||
id={character.id}
|
||||
clips={animationClips}
|
||||
title="Animation"
|
||||
updateClip={updateClipAnimations}
|
||||
@@ -70,6 +71,7 @@ export const Character = ({ characterId }: Props) => {
|
||||
type="animations"
|
||||
/>
|
||||
<Track
|
||||
id={character.id}
|
||||
clips={positionClips}
|
||||
title="Character Position/Rotation"
|
||||
updateClip={updateClipPosition}
|
||||
@@ -77,6 +79,7 @@ export const Character = ({ characterId }: Props) => {
|
||||
type="positions"
|
||||
/>
|
||||
<Track
|
||||
id={character.id}
|
||||
clips={lipSyncClips}
|
||||
title="Lipsync Audio Track"
|
||||
updateClip={updateClipLipSync}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useContext } from "react";
|
||||
import { TrackContext } from "~/contexts/TrackContext/TrackContext";
|
||||
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
|
||||
import { Track } from "~/pages/PageEnigma/comps/Timeline/Track";
|
||||
|
||||
export const Objects = () => {
|
||||
@@ -7,15 +7,16 @@ export const Objects = () => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="bg-objects-groupBg block rounded-lg pb-5 pl-2 pr-4"
|
||||
className="block rounded-lg bg-objects-groupBg pb-5 pl-2 pr-4"
|
||||
style={{ width: fullWidth + 90 }}
|
||||
>
|
||||
<div className="prevent-select mb-5 pt-2 text-xs font-medium text-white">
|
||||
Objects
|
||||
</div>
|
||||
{objects.objects.map((object) => (
|
||||
<div key={object.id} className="flex flex-col gap-2">
|
||||
<div key={object.id} className="flex flex-col gap-4">
|
||||
<Track
|
||||
id={object.id}
|
||||
clips={object.clips}
|
||||
title="Mask Position/Rotation"
|
||||
updateClip={updateObject}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Fragment, useContext } from "react";
|
||||
import { TrackContext } from "~/contexts/TrackContext/TrackContext";
|
||||
import { Fragment, useCallback, useContext, useEffect, useState } from "react";
|
||||
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
|
||||
import { LowerPanel } from "~/modules/LowerPanel";
|
||||
import { Character } from "./Character";
|
||||
|
||||
@@ -9,18 +9,63 @@ import { Objects } from "./Objects";
|
||||
import { useMouseEventsAnimation } from "./utils/useMouseEventsAnimation";
|
||||
import { faSortDown } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { ConfirmationModal } from "~/components/ConfirmationModal";
|
||||
|
||||
interface Props {
|
||||
timelineHeight: number;
|
||||
}
|
||||
|
||||
export const Timeline = ({ timelineHeight }: Props) => {
|
||||
const { characters, objects, scale, length } = useContext(TrackContext);
|
||||
const {
|
||||
characters,
|
||||
objects,
|
||||
scale,
|
||||
length,
|
||||
selectedClip,
|
||||
currentTime,
|
||||
deleteCharacterClip,
|
||||
deleteAudioClip,
|
||||
deleteCameraClip,
|
||||
deleteObjectClip,
|
||||
} = useContext(TrackContext);
|
||||
const { onPointerDown, time } = useMouseEventsAnimation();
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
|
||||
const sectionWidth = 60 * 4 * scale;
|
||||
const fullHeight =
|
||||
characters.length * 200 + objects.objects.length * 80 + 248 + 96;
|
||||
characters.length * 268 + objects.objects.length * 60 + 300 + 96;
|
||||
|
||||
const onDeleteAsk = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
if (["Backspace", "Delete"].indexOf(event.key) > -1 && selectedClip) {
|
||||
setDialogOpen(true);
|
||||
}
|
||||
},
|
||||
[selectedClip],
|
||||
);
|
||||
|
||||
const displayTime = time === -1 ? currentTime : time;
|
||||
|
||||
const onDelete = useCallback(() => {
|
||||
deleteCharacterClip(selectedClip!);
|
||||
deleteCameraClip(selectedClip!);
|
||||
deleteAudioClip(selectedClip!);
|
||||
deleteObjectClip(selectedClip!);
|
||||
}, [
|
||||
selectedClip,
|
||||
deleteAudioClip,
|
||||
deleteCharacterClip,
|
||||
deleteCameraClip,
|
||||
deleteObjectClip,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("keydown", onDeleteAsk);
|
||||
|
||||
return () => {
|
||||
document.addEventListener("keydown", onDeleteAsk);
|
||||
};
|
||||
}, [onDeleteAsk]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -44,7 +89,7 @@ export const Timeline = ({ timelineHeight }: Props) => {
|
||||
00:{index < 10 ? "0" + index.toString() : index.toString()}
|
||||
</div>
|
||||
<div
|
||||
className="bg-ui-divider absolute block h-full"
|
||||
className="absolute block h-full bg-ui-divider"
|
||||
style={{
|
||||
width: 1,
|
||||
left: index * sectionWidth + 88,
|
||||
@@ -60,7 +105,7 @@ export const Timeline = ({ timelineHeight }: Props) => {
|
||||
00:{length < 10 ? "0" + length.toString() : length.toString()}
|
||||
</div>
|
||||
<div
|
||||
className="bg-ui-divider absolute block h-full"
|
||||
className="absolute block h-full bg-ui-divider"
|
||||
style={{
|
||||
width: 1,
|
||||
left: length * sectionWidth + 88,
|
||||
@@ -84,7 +129,7 @@ export const Timeline = ({ timelineHeight }: Props) => {
|
||||
</div>
|
||||
<div
|
||||
className="absolute text-brand-primary"
|
||||
style={{ top: 8, left: time * 4 * scale + 88 }}
|
||||
style={{ top: 8, left: displayTime * 4 * scale + 88 }}
|
||||
onPointerDown={onPointerDown}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
@@ -102,6 +147,19 @@ export const Timeline = ({ timelineHeight }: Props) => {
|
||||
/>
|
||||
</div>
|
||||
</LowerPanel>
|
||||
<ConfirmationModal
|
||||
title="Delete Clip"
|
||||
text="Are you sure you want to delete the selected clip?"
|
||||
open={dialogOpen}
|
||||
onClose={() => setDialogOpen(false)}
|
||||
onOk={() => {
|
||||
onDelete();
|
||||
setDialogOpen(false);
|
||||
}}
|
||||
okText="Delete"
|
||||
okColor="bg-brand-primary"
|
||||
onCancel={() => setDialogOpen(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,11 +1,12 @@
|
||||
import { TrackClip } from "~/pages/PageEnigma/comps/Timeline/TrackClip";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faVolume, faVolumeSlash } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { BaseClip } from "~/models/track";
|
||||
import { useContext } from "react";
|
||||
import { TrackContext } from "~/contexts/TrackContext/TrackContext";
|
||||
import { BaseClip } from "~/pages/PageEnigma/models/track";
|
||||
import { PointerEvent, useContext } from "react";
|
||||
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
clips: BaseClip[];
|
||||
title: string;
|
||||
style: "character" | "audio" | "camera" | "objects";
|
||||
@@ -16,6 +17,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const Track = ({
|
||||
id,
|
||||
clips,
|
||||
toggleMute,
|
||||
updateClip,
|
||||
@@ -24,7 +26,8 @@ export const Track = ({
|
||||
style,
|
||||
type,
|
||||
}: Props) => {
|
||||
const { length, setCanDrop, dragType } = useContext(TrackContext);
|
||||
const { length, scale, setCanDrop, dragType, setDropId, setDropOffset } =
|
||||
useContext(TrackContext);
|
||||
const trackType = type ?? style;
|
||||
|
||||
function onPointerOver() {
|
||||
@@ -32,6 +35,19 @@ export const Track = ({
|
||||
return;
|
||||
}
|
||||
setCanDrop(true);
|
||||
setDropId(id);
|
||||
}
|
||||
|
||||
function onPointerMove(event: PointerEvent<HTMLDivElement>) {
|
||||
if (dragType !== trackType) {
|
||||
return;
|
||||
}
|
||||
const track = document.getElementById(`track-${trackType}-${id}`);
|
||||
if (!track) {
|
||||
return;
|
||||
}
|
||||
const position = track.getBoundingClientRect();
|
||||
setDropOffset((event.clientX - position.x) / 4 / scale);
|
||||
}
|
||||
function onPointerLeave() {
|
||||
if (dragType !== trackType) {
|
||||
@@ -43,9 +59,11 @@ export const Track = ({
|
||||
return (
|
||||
<div className="pl-16">
|
||||
<div
|
||||
className={`rounded=lg relative mt-4 block h-9 w-full bg-${style}-unselected`}
|
||||
id={`track-${trackType}-${id}`}
|
||||
className={`relative mt-4 block h-9 w-full rounded-lg bg-${style}-unselected`}
|
||||
onPointerOver={onPointerOver}
|
||||
onPointerLeave={onPointerLeave}
|
||||
onPointerMove={onPointerMove}
|
||||
>
|
||||
{clips.map((clip, index) => (
|
||||
<TrackClip
|
||||
@@ -66,14 +84,14 @@ export const Track = ({
|
||||
</div>
|
||||
{!!toggleMute && (
|
||||
<button
|
||||
className="absolute text-xs text-white"
|
||||
style={{ top: 6, left: -20 }}
|
||||
className="text-md absolute text-white transition-colors duration-100 hover:text-white/80"
|
||||
style={{ top: 6, left: -28 }}
|
||||
onClick={toggleMute}
|
||||
>
|
||||
{muted ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faVolumeSlash}
|
||||
className="text-brand-primary"
|
||||
className="text-brand-primary transition-colors duration-100 hover:text-brand-primary/80"
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faVolume} />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMouseEventsClip } from "./utils/useMouseEventsClip";
|
||||
import { BaseClip } from "~/models/track";
|
||||
import { TrackContext } from "~/contexts/TrackContext/TrackContext";
|
||||
import { BaseClip } from "~/pages/PageEnigma/models/track";
|
||||
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
|
||||
import { useContext, useState } from "react";
|
||||
|
||||
interface Props {
|
||||
@@ -51,7 +51,7 @@ export const TrackClip = ({ clip, min, max, style, updateClip }: Props) => {
|
||||
"rounded-l-lg",
|
||||
"block h-full",
|
||||
clip.id === selectedClip
|
||||
? "border border-b-2 border-l-2 border-r-0 border-t-2 border-white"
|
||||
? "border border-b-2 border-l-2 border-r-0 border-t-2 border-white focus-visible:outline-0"
|
||||
: "",
|
||||
].join(" ")}
|
||||
style={{ width: 15, left: offset * 4 * scale, cursor: "w-resize" }}
|
||||
@@ -63,7 +63,7 @@ export const TrackClip = ({ clip, min, max, style, updateClip }: Props) => {
|
||||
...classes,
|
||||
"block h-full",
|
||||
clip.id === selectedClip
|
||||
? "border border-b-2 border-l-0 border-r-0 border-t-2 border-white"
|
||||
? "border border-b-2 border-l-0 border-r-0 border-t-2 border-white focus-visible:outline-0"
|
||||
: "",
|
||||
].join(" ")}
|
||||
style={{
|
||||
@@ -80,7 +80,7 @@ export const TrackClip = ({ clip, min, max, style, updateClip }: Props) => {
|
||||
"rounded-r-lg",
|
||||
"block h-full",
|
||||
clip.id === selectedClip
|
||||
? "border border-b-2 border-l-0 border-r-2 border-t-2 border-white"
|
||||
? "border border-b-2 border-l-0 border-r-2 border-t-2 border-white focus-visible:outline-0"
|
||||
: "",
|
||||
].join(" ")}
|
||||
style={{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { TrackContext } from "~/contexts/TrackContext/TrackContext";
|
||||
import React, { useCallback, useContext, useEffect, useState } from "react";
|
||||
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
|
||||
|
||||
export const useMouseEventsAnimation = () => {
|
||||
const { currentTime, updateCurrentTime, length, scale } =
|
||||
@@ -7,7 +7,7 @@ export const useMouseEventsAnimation = () => {
|
||||
const [isActive, setIsActive] = useState("");
|
||||
const [clientX, setClientX] = useState(0);
|
||||
|
||||
const [time, setTime] = useState(currentTime);
|
||||
const [time, setTime] = useState(-1);
|
||||
|
||||
useEffect(() => {
|
||||
const max = length * 60 * 4 * scale;
|
||||
@@ -16,12 +16,15 @@ export const useMouseEventsAnimation = () => {
|
||||
if (isActive) {
|
||||
updateCurrentTime(Math.round(time));
|
||||
setIsActive("");
|
||||
setTime(-1);
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseMove = (event: MouseEvent) => {
|
||||
const delta = (event.clientX - clientX) / 4 / scale + currentTime;
|
||||
if (isActive === "drag") {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
if (delta < 0 || delta > max) {
|
||||
return;
|
||||
}
|
||||
@@ -37,15 +40,16 @@ export const useMouseEventsAnimation = () => {
|
||||
window.removeEventListener("pointerup", onPointerUp);
|
||||
window.removeEventListener("pointermove", onMouseMove);
|
||||
};
|
||||
}, [clientX, isActive, length, updateCurrentTime, scale, time]);
|
||||
}, [clientX, isActive, length, updateCurrentTime, scale, time, currentTime]);
|
||||
|
||||
return {
|
||||
onPointerDown: (event: React.PointerEvent<HTMLDivElement>) => {
|
||||
onPointerDown: useCallback((event: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (event.button === 0) {
|
||||
setClientX(event.clientX);
|
||||
setIsActive("drag");
|
||||
setTime(currentTime);
|
||||
}
|
||||
},
|
||||
}, []),
|
||||
time,
|
||||
};
|
||||
};
|
||||
@@ -5,8 +5,8 @@ import React, {
|
||||
useEffect,
|
||||
useRef,
|
||||
} from "react";
|
||||
import { BaseClip } from "~/models/track";
|
||||
import { TrackContext } from "~/contexts/TrackContext/TrackContext";
|
||||
import { BaseClip } from "~/pages/PageEnigma/models/track";
|
||||
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
|
||||
|
||||
export const useMouseEventsClip = (
|
||||
clip: BaseClip,
|
||||
@@ -89,6 +89,8 @@ export const useMouseEventsClip = (
|
||||
type: string,
|
||||
) => {
|
||||
if (event.button === 0) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
clientX.current = event.clientX;
|
||||
isActive.current = type;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
import { LoadingDotsBricks } from "~/components";
|
||||
|
||||
export const ViewSideBySide = ()=>{
|
||||
const [showLoader, setShowLoader] = useState(true);
|
||||
useEffect(()=>{
|
||||
setTimeout(()=>setShowLoader(false), 1000);
|
||||
},[ ]);
|
||||
|
||||
return (
|
||||
<div
|
||||
id="view-side-by-side"
|
||||
className="absolute top-0 left-0 w-full h-full flex justify-center items-center gap-2 p-2"
|
||||
>
|
||||
<div className="flex flex-col justify-center items-center">
|
||||
<div className="text-white">
|
||||
<label>Raw Preview</label>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<img
|
||||
//shoot that datat from engine context to this image
|
||||
className="aspect-video max-h-150"
|
||||
src="/resources/uiAssets/video_player_placeholder.gif"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col justify-center items-center">
|
||||
<div className="text-white">
|
||||
<label>Stylized Preview</label>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<img
|
||||
//shoot that datat from engine context to this image
|
||||
className="aspect-video max-h-150"
|
||||
src="/resources/uiAssets/video_player_placeholder.gif"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute top-0 left-0 w-full h-full">
|
||||
<LoadingDotsBricks show={showLoader} transition/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { createContext, Dispatch, ReactNode } from "react";
|
||||
import { State, Action } from "../reducer";
|
||||
|
||||
export const AppUiContext = createContext<[State|null, Dispatch<Action>|null]>([null,null]);
|
||||
|
||||
export const AppUIProvider = ({
|
||||
value,
|
||||
children,
|
||||
}:{
|
||||
value:[State, Dispatch<Action>],
|
||||
children: ReactNode
|
||||
})=>{
|
||||
return (
|
||||
<AppUiContext.Provider value={value}>{children}</AppUiContext.Provider>
|
||||
);
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
+100
-23
@@ -3,78 +3,155 @@ import {
|
||||
AnimationClip,
|
||||
AudioClip,
|
||||
AudioGroup,
|
||||
BaseClip,
|
||||
CameraGroup,
|
||||
CharacterGroup,
|
||||
ObjectGroup,
|
||||
} from "~/models/track";
|
||||
} from "~/pages/PageEnigma/models/track";
|
||||
|
||||
export const TrackContext = createContext<{
|
||||
// timeline characters and it's functions
|
||||
characters: CharacterGroup[];
|
||||
camera: CameraGroup | null;
|
||||
audio: AudioGroup | null;
|
||||
objects: ObjectGroup;
|
||||
selectedClip: string | null;
|
||||
updateCharacters: (options: {
|
||||
type: "animations" | "positions" | "lipSync";
|
||||
id: string;
|
||||
length: number;
|
||||
offset: number;
|
||||
}) => void;
|
||||
toggleLipSyncMute: (characterId: string) => void;
|
||||
addCharacterAnimation: (options: {
|
||||
clipId: string;
|
||||
characterId: string;
|
||||
animationClips: BaseClip[];
|
||||
offset: number;
|
||||
}) => void;
|
||||
addCharacterAudio: (options: {
|
||||
clipId: string;
|
||||
characterId: string;
|
||||
audioClips: BaseClip[];
|
||||
offset: number;
|
||||
}) => void;
|
||||
selectCharacterClip: (clipId: string) => void;
|
||||
deleteCharacterClip: (clipId: string) => void;
|
||||
|
||||
// timeline camera group
|
||||
camera: CameraGroup | null;
|
||||
updateCamera: (options: {
|
||||
id: string;
|
||||
length: number;
|
||||
offset: number;
|
||||
}) => void;
|
||||
selectCameraClip: (clipId: string) => void;
|
||||
deleteCameraClip: (clipId: string) => void;
|
||||
|
||||
// timeline global audio group
|
||||
audio: AudioGroup | null;
|
||||
updateAudio: (options: {
|
||||
id: string;
|
||||
length: number;
|
||||
offset: number;
|
||||
}) => void;
|
||||
toggleAudioMute: () => void;
|
||||
addGlobalAudio: (
|
||||
clipId: string,
|
||||
animationClips: BaseClip[],
|
||||
offset: number,
|
||||
) => void;
|
||||
selectAudioClip: (clipId: string) => void;
|
||||
deleteAudioClip: (clipId: string) => void;
|
||||
|
||||
// timeline objects group
|
||||
objects: ObjectGroup;
|
||||
updateObject: (options: {
|
||||
id: string;
|
||||
length: number;
|
||||
offset: number;
|
||||
}) => void;
|
||||
selectObjectClip: (clipId: string) => void;
|
||||
deleteObjectClip: (clipId: string) => void;
|
||||
|
||||
// current - only select one item - will be replaced
|
||||
selectedClip: string | null;
|
||||
selectClip: (clipId: string) => void;
|
||||
toggleLipSyncMute: (characterId: string) => void;
|
||||
toggleAudioMute: () => void;
|
||||
|
||||
// sidebar clips
|
||||
animationClips: AnimationClip[];
|
||||
audioClips: AudioClip[];
|
||||
|
||||
// drag and drop
|
||||
dragType: "animations" | "lipSync" | null;
|
||||
dragId: string | null;
|
||||
startDrag: (type: "animations" | "lipSync", id: string) => void;
|
||||
endDrag: () => void;
|
||||
scale: number;
|
||||
currentTime: number;
|
||||
length: number;
|
||||
updateCurrentTime: (time: number) => void;
|
||||
canDrop: boolean;
|
||||
setCanDrop: (can: boolean) => void;
|
||||
overTimeline: boolean;
|
||||
setOverTimeline: (over: boolean) => void;
|
||||
dropId: string;
|
||||
setDropId: (id: string) => void;
|
||||
dropOffset: number;
|
||||
setDropOffset: (offset: number) => void;
|
||||
|
||||
// scale of timeline displa
|
||||
scale: number;
|
||||
|
||||
// current time position
|
||||
currentTime: number;
|
||||
updateCurrentTime: (time: number) => void;
|
||||
|
||||
// total length of the film clip
|
||||
length: number;
|
||||
|
||||
// computed width length * 60 * 4 * scale
|
||||
fullWidth: number;
|
||||
}>({
|
||||
characters: [],
|
||||
camera: null,
|
||||
audio: null,
|
||||
objects: { id: "", objects: [] },
|
||||
selectedClip: null,
|
||||
updateCharacters: () => {},
|
||||
updateCamera: () => {},
|
||||
updateAudio: () => {},
|
||||
updateObject: () => {},
|
||||
selectClip: () => {},
|
||||
toggleLipSyncMute: () => {},
|
||||
addCharacterAnimation: () => {},
|
||||
addCharacterAudio: () => {},
|
||||
selectCharacterClip: () => {},
|
||||
deleteCharacterClip: () => {},
|
||||
|
||||
camera: null,
|
||||
updateCamera: () => {},
|
||||
selectCameraClip: () => {},
|
||||
deleteCameraClip: () => {},
|
||||
|
||||
audio: null,
|
||||
updateAudio: () => {},
|
||||
toggleAudioMute: () => {},
|
||||
selectAudioClip: () => {},
|
||||
addGlobalAudio: () => {},
|
||||
deleteAudioClip: () => {},
|
||||
|
||||
objects: { id: "", objects: [] },
|
||||
updateObject: () => {},
|
||||
selectObjectClip: () => {},
|
||||
deleteObjectClip: () => {},
|
||||
|
||||
selectedClip: null,
|
||||
selectClip: () => {},
|
||||
|
||||
animationClips: [],
|
||||
audioClips: [],
|
||||
|
||||
dragType: null,
|
||||
dragId: null,
|
||||
startDrag: () => {},
|
||||
endDrag: () => {},
|
||||
scale: 1,
|
||||
currentTime: 0,
|
||||
length: 12,
|
||||
updateCurrentTime: () => {},
|
||||
dropId: "",
|
||||
setDropId: () => {},
|
||||
canDrop: false,
|
||||
setCanDrop: () => {},
|
||||
overTimeline: false,
|
||||
setOverTimeline: () => {},
|
||||
dropOffset: 0,
|
||||
setDropOffset: () => {},
|
||||
|
||||
scale: 1,
|
||||
currentTime: 0,
|
||||
updateCurrentTime: () => {},
|
||||
length: 12,
|
||||
fullWidth: 0,
|
||||
});
|
||||
@@ -0,0 +1,122 @@
|
||||
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
|
||||
import { ReactNode, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import useUpdateCharacters from "~/pages/PageEnigma/contexts/TrackContext/utils/useUpdateCharacters";
|
||||
import useUpdateCamera from "~/pages/PageEnigma/contexts/TrackContext/utils/useUpdateCamera";
|
||||
import useUpdateAudio from "~/pages/PageEnigma/contexts/TrackContext/utils/useUpdateAudio";
|
||||
import useUpdateObject from "~/pages/PageEnigma/contexts/TrackContext/utils/useUpdateObject";
|
||||
import { AnimationClip, AudioClip } from "~/pages/PageEnigma/models/track";
|
||||
import useUpdateDragDrop from "~/pages/PageEnigma/contexts/TrackContext/utils/useUpdateDragDrop";
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const TrackProvider = ({ children }: Props) => {
|
||||
const characters = useUpdateCharacters();
|
||||
const camera = useUpdateCamera();
|
||||
const audio = useUpdateAudio();
|
||||
const objects = useUpdateObject();
|
||||
|
||||
const { endDrag, ...dragDrop } = useUpdateDragDrop();
|
||||
|
||||
const [selectedClip, setSelectedClip] = useState<string | null>(null);
|
||||
const selectClip = useCallback((clipId: string | null) => {
|
||||
setSelectedClip(clipId);
|
||||
}, []);
|
||||
|
||||
const [animationClips, setAnimationClips] = useState<AnimationClip[]>([]);
|
||||
const [audioClips, setAudioClips] = useState<AudioClip[]>([]);
|
||||
|
||||
const [scale, setScale] = useState(1);
|
||||
const [length, setLength] = useState(1);
|
||||
const [time, setTime] = useState(0);
|
||||
const updateCurrentTime = useCallback((newTime: number) => {
|
||||
setTime(newTime);
|
||||
console.log("message", {
|
||||
action: "UpdateCurrentTime",
|
||||
id: "",
|
||||
data: { currentTime: newTime },
|
||||
});
|
||||
}, []);
|
||||
|
||||
// cross group functions
|
||||
const dropClip = useCallback(() => {
|
||||
const { canDrop, dragType, dropId, dragId, dropOffset } = dragDrop;
|
||||
if (canDrop) {
|
||||
if (dragType === "animations") {
|
||||
characters.addCharacterAnimation({
|
||||
clipId: dragId!,
|
||||
characterId: dropId,
|
||||
animationClips,
|
||||
offset: dropOffset,
|
||||
});
|
||||
}
|
||||
}
|
||||
endDrag();
|
||||
}, [dragDrop, animationClips, characters, endDrag]);
|
||||
|
||||
const fullWidth = useMemo(() => {
|
||||
return length * 60 * 4 * scale;
|
||||
}, [length, scale]);
|
||||
|
||||
useEffect(() => {
|
||||
setAnimationClips([
|
||||
{
|
||||
id: "ani-id1",
|
||||
offset: 0,
|
||||
length: 124,
|
||||
name: "ani 11",
|
||||
},
|
||||
]);
|
||||
setAudioClips([]);
|
||||
setScale(1);
|
||||
setLength(12);
|
||||
}, []);
|
||||
|
||||
const values = useMemo(() => {
|
||||
return {
|
||||
...characters,
|
||||
...camera,
|
||||
...audio,
|
||||
...objects,
|
||||
|
||||
selectClip,
|
||||
selectedClip,
|
||||
|
||||
...dragDrop,
|
||||
endDrag: dropClip,
|
||||
|
||||
animationClips,
|
||||
audioClips,
|
||||
|
||||
scale,
|
||||
currentTime: time,
|
||||
updateCurrentTime,
|
||||
length,
|
||||
fullWidth,
|
||||
};
|
||||
}, [
|
||||
characters,
|
||||
camera,
|
||||
audio,
|
||||
objects,
|
||||
|
||||
selectClip,
|
||||
selectedClip,
|
||||
|
||||
dragDrop,
|
||||
dropClip,
|
||||
|
||||
animationClips,
|
||||
audioClips,
|
||||
|
||||
updateCurrentTime,
|
||||
time,
|
||||
fullWidth,
|
||||
length,
|
||||
scale,
|
||||
]);
|
||||
return (
|
||||
<TrackContext.Provider value={values}>{children}</TrackContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,129 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { AudioGroup, BaseClip } from "~/pages/PageEnigma/models/track";
|
||||
|
||||
export default function useUpdateAudio() {
|
||||
const [audio, setAudio] = useState<AudioGroup>({
|
||||
id: "AU1",
|
||||
muted: false,
|
||||
clips: [
|
||||
{
|
||||
id: "AU1-1",
|
||||
length: 200,
|
||||
offset: 0,
|
||||
name: "audio 1",
|
||||
},
|
||||
{
|
||||
id: "AU1-2",
|
||||
length: 180,
|
||||
offset: 300,
|
||||
name: "audio 2",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const updateAudio = useCallback(
|
||||
({
|
||||
id,
|
||||
offset,
|
||||
length,
|
||||
}: {
|
||||
id: string;
|
||||
length: number;
|
||||
offset: number;
|
||||
}) => {
|
||||
setAudio((oldAudio) => {
|
||||
const newClips = [...oldAudio.clips];
|
||||
const clipIndex = newClips.findIndex((row) => row.id === id);
|
||||
if (clipIndex === -1) {
|
||||
return { ...oldAudio };
|
||||
}
|
||||
const clip = newClips[clipIndex];
|
||||
clip.offset = offset;
|
||||
clip.length = length;
|
||||
return {
|
||||
...oldAudio,
|
||||
clips: newClips,
|
||||
};
|
||||
});
|
||||
console.log("message", {
|
||||
action: "UpdateGlobalAudio",
|
||||
id,
|
||||
data: { offset, length },
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const addGlobalAudio = useCallback(
|
||||
(dragId: string, audioClips: BaseClip[], offset: number) => {
|
||||
const clip = audioClips.find((row) => row.id === dragId);
|
||||
if (!clip) {
|
||||
return;
|
||||
}
|
||||
|
||||
setAudio((oldAudio) => {
|
||||
return {
|
||||
...oldAudio,
|
||||
clips: [...oldAudio.clips, { ...clip, offset }],
|
||||
};
|
||||
});
|
||||
console.log("message", {
|
||||
action: "UpdateGlobalAudio",
|
||||
id: dragId,
|
||||
data: { offset },
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const toggleAudioMute = useCallback(() => {
|
||||
setAudio((oldAudio) => {
|
||||
return {
|
||||
...oldAudio,
|
||||
muted: !oldAudio.muted,
|
||||
};
|
||||
});
|
||||
console.log("message", {
|
||||
action: "ToggleGlobalAudioMute",
|
||||
id: "",
|
||||
});
|
||||
}, []);
|
||||
|
||||
const selectAudioClip = useCallback((clipId: string) => {
|
||||
setAudio((oldAudio) => {
|
||||
return {
|
||||
...oldAudio,
|
||||
clips: [
|
||||
...oldAudio.clips.map((clip) => {
|
||||
return {
|
||||
...clip,
|
||||
selected: clip.id === clipId ? !clip.selected : clip.selected,
|
||||
};
|
||||
}),
|
||||
],
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const deleteAudioClip = useCallback((clipId: string) => {
|
||||
setAudio((oldAudio) => {
|
||||
return {
|
||||
...oldAudio,
|
||||
clips: [...oldAudio.clips.filter((clip) => clip.id !== clipId)],
|
||||
};
|
||||
});
|
||||
console.log("message", {
|
||||
action: "UpdateGlobalAudio",
|
||||
id: clipId,
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
audio,
|
||||
updateAudio,
|
||||
toggleAudioMute,
|
||||
selectAudioClip,
|
||||
addGlobalAudio,
|
||||
deleteAudioClip,
|
||||
};
|
||||
}
|
||||
+28
-1
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { CameraGroup } from "~/models/track";
|
||||
import { CameraGroup } from "~/pages/PageEnigma/models/track";
|
||||
|
||||
export default function useUpdateCamera() {
|
||||
const [camera, setCamera] = useState<CameraGroup>({
|
||||
@@ -48,8 +48,35 @@ export default function useUpdateCamera() {
|
||||
[],
|
||||
);
|
||||
|
||||
const selectCameraClip = useCallback((clipId: string) => {
|
||||
setCamera((oldCamera) => {
|
||||
return {
|
||||
...oldCamera,
|
||||
clips: [
|
||||
...oldCamera.clips.map((clip) => {
|
||||
return {
|
||||
...clip,
|
||||
selected: clip.id === clipId ? !clip.selected : clip.selected,
|
||||
};
|
||||
}),
|
||||
],
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const deleteCameraClip = useCallback((clipId: string) => {
|
||||
setCamera((oldCamera) => {
|
||||
return {
|
||||
...oldCamera,
|
||||
clips: [...oldCamera.clips.filter((clip) => clip.id !== clipId)],
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
camera,
|
||||
updateCamera,
|
||||
selectCameraClip,
|
||||
deleteCameraClip,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { BaseClip, CharacterGroup } from "~/pages/PageEnigma/models/track";
|
||||
|
||||
export default function useUpdateCharacters() {
|
||||
const [characters, setCharacters] = useState<CharacterGroup[]>([
|
||||
{
|
||||
id: "CH1",
|
||||
muted: false,
|
||||
animationClips: [],
|
||||
positionClips: [],
|
||||
lipSyncClips: [
|
||||
{
|
||||
id: "CH1-L1",
|
||||
length: 400,
|
||||
offset: 150,
|
||||
name: "sync 1",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
const updateCharacters = useCallback(
|
||||
({
|
||||
type,
|
||||
id,
|
||||
offset,
|
||||
length,
|
||||
}: {
|
||||
type: "animations" | "positions" | "lipSync";
|
||||
id: string;
|
||||
length: number;
|
||||
offset: number;
|
||||
}) => {
|
||||
if (type === "animations") {
|
||||
setCharacters((oldCharacters) => {
|
||||
return oldCharacters.map((character) => {
|
||||
const newAnimationClips = [...character.animationClips];
|
||||
const clipIndex = newAnimationClips.findIndex(
|
||||
(row) => row.id === id,
|
||||
);
|
||||
if (clipIndex === -1) {
|
||||
return { ...character };
|
||||
}
|
||||
const clip = newAnimationClips[clipIndex];
|
||||
clip.offset = offset;
|
||||
clip.length = length;
|
||||
return {
|
||||
...character,
|
||||
animationClips: newAnimationClips,
|
||||
};
|
||||
});
|
||||
});
|
||||
console.log("message", {
|
||||
action: "UpdateAnimation",
|
||||
id,
|
||||
data: { offset, length },
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (type === "positions") {
|
||||
setCharacters((oldCharacters) => {
|
||||
return oldCharacters.map((character) => {
|
||||
const newPositionClips = [...character.positionClips];
|
||||
const clipIndex = newPositionClips.findIndex(
|
||||
(row) => row.id === id,
|
||||
);
|
||||
if (clipIndex === -1) {
|
||||
return { ...character };
|
||||
}
|
||||
const clip = newPositionClips[clipIndex];
|
||||
clip.offset = offset;
|
||||
clip.length = length;
|
||||
return {
|
||||
...character,
|
||||
positionClips: newPositionClips,
|
||||
};
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (type === "lipSync") {
|
||||
setCharacters((oldCharacters) => {
|
||||
return oldCharacters.map((character) => {
|
||||
const newLipSyncClips = [...character.lipSyncClips];
|
||||
const clipIndex = newLipSyncClips.findIndex((row) => row.id === id);
|
||||
if (clipIndex === -1) {
|
||||
return { ...character };
|
||||
}
|
||||
const clip = newLipSyncClips[clipIndex];
|
||||
clip.offset = offset;
|
||||
clip.length = length;
|
||||
return {
|
||||
...character,
|
||||
lipSyncClips: newLipSyncClips,
|
||||
};
|
||||
});
|
||||
});
|
||||
console.log("message", {
|
||||
action: "UpdateLipSync",
|
||||
id,
|
||||
data: { offset, length },
|
||||
});
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const addCharacterAnimation = useCallback(
|
||||
({
|
||||
clipId,
|
||||
characterId,
|
||||
animationClips,
|
||||
offset,
|
||||
}: {
|
||||
clipId: string;
|
||||
characterId: string;
|
||||
animationClips: BaseClip[];
|
||||
offset: number;
|
||||
}) => {
|
||||
const clip = animationClips.find((row) => row.id === clipId);
|
||||
if (!clip) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCharacters((oldCharacters) => {
|
||||
return oldCharacters.map((character) => {
|
||||
if (character.id !== characterId) {
|
||||
return { ...character };
|
||||
}
|
||||
return {
|
||||
...character,
|
||||
animationClips: [...character.animationClips, { ...clip, offset }],
|
||||
};
|
||||
});
|
||||
});
|
||||
console.log("message", {
|
||||
action: "AddAnimation",
|
||||
id: clipId,
|
||||
data: { offset },
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const addCharacterAudio = useCallback(
|
||||
({
|
||||
clipId,
|
||||
characterId,
|
||||
audioClips,
|
||||
offset,
|
||||
}: {
|
||||
clipId: string;
|
||||
characterId: string;
|
||||
audioClips: BaseClip[];
|
||||
offset: number;
|
||||
}) => {
|
||||
const clip = audioClips.find((row) => row.id === clipId);
|
||||
if (!clip) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCharacters((oldCharacters) => {
|
||||
return oldCharacters.map((character) => {
|
||||
if (character.id !== characterId) {
|
||||
return { ...character };
|
||||
}
|
||||
return {
|
||||
...character,
|
||||
lipSyncClips: [...character.lipSyncClips, { ...clip, offset }],
|
||||
};
|
||||
});
|
||||
});
|
||||
console.log("message", {
|
||||
action: "AddLipSync",
|
||||
id: clipId,
|
||||
data: { offset },
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const toggleLipSyncMute = useCallback((characterId: string) => {
|
||||
setCharacters((oldCharacters) => {
|
||||
return oldCharacters.map((character) => ({
|
||||
...character,
|
||||
muted:
|
||||
character.id === characterId ? !character.muted : character.muted,
|
||||
}));
|
||||
});
|
||||
console.log("message", {
|
||||
action: "ToggleLipSync",
|
||||
id: characterId,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const selectCharacterClip = useCallback((clipId: string) => {
|
||||
setCharacters((oldCharacters) => {
|
||||
return [
|
||||
...oldCharacters.map((character) => ({
|
||||
...character,
|
||||
animationClips: character.animationClips.map((clip) => ({
|
||||
...clip,
|
||||
selected: clip.id === clipId ? !clip.selected : clip.selected,
|
||||
})),
|
||||
positionClips: character.positionClips.map((clip) => ({
|
||||
...clip,
|
||||
selected: clip.id === clipId ? !clip.selected : clip.selected,
|
||||
})),
|
||||
lipSyncClips: character.lipSyncClips.map((clip) => ({
|
||||
...clip,
|
||||
selected: clip.id === clipId ? !clip.selected : clip.selected,
|
||||
})),
|
||||
})),
|
||||
];
|
||||
});
|
||||
}, []);
|
||||
|
||||
const deleteCharacterClip = useCallback((clipId: string) => {
|
||||
setCharacters((oldCharacters) => {
|
||||
return [
|
||||
...oldCharacters.map((character) => ({
|
||||
...character,
|
||||
animationClips: character.animationClips.filter(
|
||||
(clip) => clip.id !== clipId,
|
||||
),
|
||||
positionClips: character.positionClips.filter(
|
||||
(clip) => clip.id !== clipId,
|
||||
),
|
||||
lipSyncClips: character.lipSyncClips.filter(
|
||||
(clip) => clip.id !== clipId,
|
||||
),
|
||||
})),
|
||||
];
|
||||
});
|
||||
console.log("message", {
|
||||
action: "DeleteCharacterClip",
|
||||
id: clipId,
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
characters,
|
||||
updateCharacters,
|
||||
toggleLipSyncMute,
|
||||
addCharacterAnimation,
|
||||
addCharacterAudio,
|
||||
selectCharacterClip,
|
||||
deleteCharacterClip,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
export default function useUpdateDragDrop() {
|
||||
const [state, setState] = useState<{
|
||||
dragType: "animations" | "lipSync" | null;
|
||||
dragId: string | null;
|
||||
}>({ dragType: null, dragId: null });
|
||||
const startDrag = useCallback(
|
||||
(type: "animations" | "lipSync", id: string) => {
|
||||
setState({ dragId: id, dragType: type });
|
||||
},
|
||||
[],
|
||||
);
|
||||
const [canDrop, setCanDrop] = useState(false);
|
||||
const [overTimeline, setOverTimeline] = useState(false);
|
||||
|
||||
const endDrag = useCallback(() => {
|
||||
setState({ dragId: null, dragType: null });
|
||||
}, []);
|
||||
|
||||
const [dropId, setDropId] = useState("");
|
||||
const [dropOffset, setDropOffset] = useState(0);
|
||||
|
||||
return {
|
||||
dragId: state.dragId,
|
||||
dragType: state.dragType,
|
||||
startDrag,
|
||||
endDrag,
|
||||
canDrop,
|
||||
setCanDrop,
|
||||
overTimeline,
|
||||
setOverTimeline,
|
||||
dropId,
|
||||
setDropId,
|
||||
dropOffset,
|
||||
setDropOffset,
|
||||
};
|
||||
}
|
||||
+30
-1
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { ObjectGroup } from "~/models/track";
|
||||
import { ObjectGroup } from "~/pages/PageEnigma/models/track";
|
||||
|
||||
export default function useUpdateObject() {
|
||||
const [objects, setObjects] = useState<ObjectGroup>({
|
||||
@@ -46,8 +46,37 @@ export default function useUpdateObject() {
|
||||
[],
|
||||
);
|
||||
|
||||
const selectObjectClip = useCallback((clipId: string) => {
|
||||
setObjects((oldObjectGroup) => {
|
||||
return {
|
||||
...oldObjectGroup,
|
||||
objects: oldObjectGroup.objects.map((object) => ({
|
||||
...object,
|
||||
clips: object.clips.map((clip) => ({
|
||||
...clip,
|
||||
selected: clip.id === clipId ? !clip.selected : clip.selected,
|
||||
})),
|
||||
})),
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const deleteObjectClip = useCallback((clipId: string) => {
|
||||
setObjects((oldObjectGroup) => {
|
||||
return {
|
||||
...oldObjectGroup,
|
||||
objects: oldObjectGroup.objects.map((object) => ({
|
||||
...object,
|
||||
clips: object.clips.filter((clip) => clip.id !== clipId),
|
||||
})),
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
objects,
|
||||
updateObject,
|
||||
selectObjectClip,
|
||||
deleteObjectClip,
|
||||
};
|
||||
}
|
||||
@@ -3,6 +3,7 @@ export interface BaseClip {
|
||||
name: string;
|
||||
offset: number;
|
||||
length: number;
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
export interface AnimationClip extends BaseClip {}
|
||||
@@ -0,0 +1,15 @@
|
||||
import {State, Action, ACTION_TYPES, VIEW_MODES} from './types';
|
||||
import {reducer} from './reducer';
|
||||
|
||||
const initialState:State = {
|
||||
timelineHeight: 260,
|
||||
viewMode: VIEW_MODES.EDITOR,
|
||||
showEditorLoader: true
|
||||
}
|
||||
|
||||
export type {State, Action};
|
||||
export {
|
||||
ACTION_TYPES,
|
||||
initialState,
|
||||
reducer,
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { State, Action, ACTION_TYPES} from './types';
|
||||
|
||||
export function reducer(state: State, action: Action): State {
|
||||
switch(action.type){
|
||||
case ACTION_TYPES.SHOW_EDITOR_LOADER:{
|
||||
return{
|
||||
...state,
|
||||
showEditorLoader: true,
|
||||
}
|
||||
}
|
||||
case ACTION_TYPES.HIDE_EDITOR_LOADER:{
|
||||
return{
|
||||
...state,
|
||||
showEditorLoader: false,
|
||||
}
|
||||
}
|
||||
case ACTION_TYPES.ON_TIMELINE_RESIZE:{
|
||||
return{
|
||||
...state,
|
||||
timelineHeight: action.payload.timelineHeight,
|
||||
}
|
||||
}
|
||||
case ACTION_TYPES.ON_CHANGE_VIEW_MODE:{
|
||||
console.log(action);
|
||||
return{
|
||||
...state,
|
||||
viewMode: action.payload.viewMode
|
||||
}
|
||||
}
|
||||
default:{
|
||||
return state; //no change
|
||||
}
|
||||
}// end switch
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
export enum VIEW_MODES {
|
||||
EDITOR = 'editor',
|
||||
SIDE_BY_SIDE = 'side-by-side'
|
||||
};
|
||||
|
||||
type ViewModes = VIEW_MODES.EDITOR | VIEW_MODES.SIDE_BY_SIDE;
|
||||
|
||||
export type State = {
|
||||
viewMode: ViewModes;
|
||||
timelineHeight: number;
|
||||
showEditorLoader: boolean;
|
||||
};
|
||||
|
||||
|
||||
export enum ACTION_TYPES {
|
||||
ON_TIMELINE_RESIZE = "on_timeline_resize",
|
||||
ON_CHANGE_VIEW_MODE = "on_change_view_mode",
|
||||
SHOW_EDITOR_LOADER = "show_editor_loader",
|
||||
HIDE_EDITOR_LOADER = "hide_editor_loader",
|
||||
};
|
||||
|
||||
export type Action =
|
||||
| {type: ACTION_TYPES.SHOW_EDITOR_LOADER,}
|
||||
| {type: ACTION_TYPES.HIDE_EDITOR_LOADER,}
|
||||
| {
|
||||
type: ACTION_TYPES.ON_TIMELINE_RESIZE,
|
||||
payload: {
|
||||
timelineHeight: number
|
||||
}
|
||||
}
|
||||
| {
|
||||
type: ACTION_TYPES.ON_CHANGE_VIEW_MODE,
|
||||
payload: {
|
||||
viewMode: ViewModes
|
||||
}
|
||||
}
|
||||
;
|
||||
+41
-2
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { LinksFunction } from "@remix-run/deno";
|
||||
import {
|
||||
Links,
|
||||
@@ -12,6 +12,7 @@ import normalizeCss from "./styles/normalize.css?url";
|
||||
import tailwindCss from "./styles/tailwind.css?url";
|
||||
import baseCss from "./styles/base.css?url";
|
||||
|
||||
import { LoadingDotsBricks } from "~/components";
|
||||
import { TopBar } from "./modules/TopBar";
|
||||
import { TopBarInnerContext } from "~/contexts/TopBarInner";
|
||||
|
||||
@@ -45,7 +46,13 @@ export const links: LinksFunction = () => [
|
||||
|
||||
export default function App() {
|
||||
const [topBarInnerComponent, setTopBarInnerComponent] =
|
||||
useState<JSX.Element | null>(null);
|
||||
useState<JSX.Element | null>(null);
|
||||
const [isLoaded, setIsLoaded] = useState<'loading'|'loaded'|'completed'>('loading');
|
||||
useEffect(()=>{
|
||||
setIsLoaded('loaded');
|
||||
setTimeout(()=>setIsLoaded('completed'), 2000);
|
||||
},[]);
|
||||
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
@@ -56,6 +63,15 @@ export default function App() {
|
||||
<Links />
|
||||
</head>
|
||||
<body className="overflow-hidden bg-ui-background">
|
||||
{isLoaded !== 'completed' &&
|
||||
<CompleteTakeoverLoadingScreen
|
||||
className={
|
||||
isLoaded === 'loading'
|
||||
? "transition-opacity duration-1000 opacity-100"
|
||||
: "transition-opacity duration-1000 opacity-0"
|
||||
}
|
||||
/>
|
||||
}
|
||||
<TopBarInnerContext.Provider
|
||||
value={{
|
||||
TopBarInner: topBarInnerComponent,
|
||||
@@ -72,3 +88,26 @@ export default function App() {
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
function CompleteTakeoverLoadingScreen({className}:{className:string}){
|
||||
return(
|
||||
<div
|
||||
id='complete-takeover-loading-screen'
|
||||
className={className}
|
||||
style={{
|
||||
backgroundColor: '#1a1a27',
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: 9999,
|
||||
}}
|
||||
>
|
||||
<LoadingDotsBricks />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -35,3 +35,115 @@ body {
|
||||
user-select: none; /* Standard syntax */
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
/*!
|
||||
* three-dots - v0.3.2
|
||||
* CSS loading animations made with single element
|
||||
* https://nzbin.github.io/three-dots/
|
||||
*
|
||||
* Copyright (c) 2018 nzbin
|
||||
* Released under MIT License
|
||||
*/
|
||||
|
||||
/**
|
||||
* ==============================================
|
||||
* Dot Typing
|
||||
* ==============================================
|
||||
*/
|
||||
.dot-typing {
|
||||
position: relative;
|
||||
left: -9999px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
background-color: #393948;
|
||||
color: #393948;
|
||||
box-shadow: 9984px 0 0 0 #393948, 9999px 0 0 0 #393948, 10014px 0 0 0 #393948;
|
||||
animation: dot-typing 1.5s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes dot-typing {
|
||||
0% {
|
||||
box-shadow: 9984px 0 0 0 #393948, 9999px 0 0 0 #393948, 10014px 0 0 0 #393948;
|
||||
}
|
||||
16.667% {
|
||||
box-shadow: 9984px -10px 0 0 #393948, 9999px 0 0 0 #393948, 10014px 0 0 0 #393948;
|
||||
}
|
||||
33.333% {
|
||||
box-shadow: 9984px 0 0 0 #393948, 9999px 0 0 0 #393948, 10014px 0 0 0 #393948;
|
||||
}
|
||||
50% {
|
||||
box-shadow: 9984px 0 0 0 #393948, 9999px -10px 0 0 #393948, 10014px 0 0 0 #393948;
|
||||
}
|
||||
66.667% {
|
||||
box-shadow: 9984px 0 0 0 #393948, 9999px 0 0 0 #393948, 10014px 0 0 0 #393948;
|
||||
}
|
||||
83.333% {
|
||||
box-shadow: 9984px 0 0 0 #393948, 9999px 0 0 0 #393948, 10014px -10px 0 0 #393948;
|
||||
}
|
||||
100% {
|
||||
box-shadow: 9984px 0 0 0 #393948, 9999px 0 0 0 #393948, 10014px 0 0 0 #393948;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ==============================================
|
||||
* Dot Bricks
|
||||
* ==============================================
|
||||
*/
|
||||
.dot-bricks {
|
||||
position: relative;
|
||||
top: 8px;
|
||||
left: -9999px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
background-color: #393948;
|
||||
color: #393948;
|
||||
box-shadow: 9991px -16px 0 0 #393948, 9991px 0 0 0 #393948, 10007px 0 0 0 #393948;
|
||||
animation: dot-bricks 2s infinite ease;
|
||||
}
|
||||
|
||||
@keyframes dot-bricks {
|
||||
0% {
|
||||
box-shadow: 9991px -16px 0 0 #393948, 9991px 0 0 0 #393948, 10007px 0 0 0 #393948;
|
||||
}
|
||||
8.333% {
|
||||
box-shadow: 10007px -16px 0 0 #393948, 9991px 0 0 0 #393948, 10007px 0 0 0 #393948;
|
||||
}
|
||||
16.667% {
|
||||
box-shadow: 10007px -16px 0 0 #393948, 9991px -16px 0 0 #393948, 10007px 0 0 0 #393948;
|
||||
}
|
||||
25% {
|
||||
box-shadow: 10007px -16px 0 0 #393948, 9991px -16px 0 0 #393948, 9991px 0 0 0 #393948;
|
||||
}
|
||||
33.333% {
|
||||
box-shadow: 10007px 0 0 0 #393948, 9991px -16px 0 0 #393948, 9991px 0 0 0 #393948;
|
||||
}
|
||||
41.667% {
|
||||
box-shadow: 10007px 0 0 0 #393948, 10007px -16px 0 0 #393948, 9991px 0 0 0 #393948;
|
||||
}
|
||||
50% {
|
||||
box-shadow: 10007px 0 0 0 #393948, 10007px -16px 0 0 #393948, 9991px -16px 0 0 #393948;
|
||||
}
|
||||
58.333% {
|
||||
box-shadow: 9991px 0 0 0 #393948, 10007px -16px 0 0 #393948, 9991px -16px 0 0 #393948;
|
||||
}
|
||||
66.666% {
|
||||
box-shadow: 9991px 0 0 0 #393948, 10007px 0 0 0 #393948, 9991px -16px 0 0 #393948;
|
||||
}
|
||||
75% {
|
||||
box-shadow: 9991px 0 0 0 #393948, 10007px 0 0 0 #393948, 10007px -16px 0 0 #393948;
|
||||
}
|
||||
83.333% {
|
||||
box-shadow: 9991px -16px 0 0 #393948, 10007px 0 0 0 #393948, 10007px -16px 0 0 #393948;
|
||||
}
|
||||
91.667% {
|
||||
box-shadow: 9991px -16px 0 0 #393948, 9991px 0 0 0 #393948, 10007px -16px 0 0 #393948;
|
||||
}
|
||||
100% {
|
||||
box-shadow: 9991px -16px 0 0 #393948, 9991px 0 0 0 #393948, 10007px 0 0 0 #393948;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user