Fix a number of issues

This commit is contained in:
FromTheLOLAshes committed 2024-03-25 19:59:14 +07:00
1 parent 7f4fa98259
commit f3d7fe6ea5
21 files changed
+968 -351

No files matched your search

-20
View File
@@ -1,20 +0,0 @@
import { createContext } from "react";
import { CharacterTrack } from "~/models/track";
export const TrackContext = createContext<{
characters: CharacterTrack[];
updateCharacters: (options: {
type: "animations" | "positions" | "lipSync";
id: string;
length: number;
offset: number;
}) => void;
selectClip: (options: {
type: "animations" | "positions" | "lipSync";
id: string;
}) => void;
}>({
characters: [],
updateCharacters: () => {},
selectClip: () => {},
});
@@ -0,0 +1,56 @@
import { createContext } from "react";
import {
AudioGroup,
CameraGroup,
CharacterGroup,
ObjectGroup,
} from "~/models/track";
export const TrackContext = createContext<{
characters: CharacterGroup[];
camera: CameraGroup | null;
audio: AudioGroup | null;
objects: ObjectGroup;
selectedClip: string | null;
scale: number;
currentTime: number;
length: number;
updateCharacters: (options: {
type: "animations" | "positions" | "lipSync";
id: string;
length: number;
offset: number;
}) => void;
updateCamera: (options: {
id: string;
length: number;
offset: number;
}) => void;
updateAudio: (options: {
id: string;
length: number;
offset: number;
}) => void;
updateObject: (options: {
id: string;
length: number;
offset: number;
}) => void;
selectClip: (clipId: string) => void;
updateCurrentTime: (time: number) => void;
}>({
characters: [],
camera: null,
audio: null,
objects: { id: "", objects: [] },
selectedClip: null,
scale: 1,
currentTime: 0,
length: 12,
updateCharacters: () => {},
updateCamera: () => {},
updateAudio: () => {},
updateObject: () => {},
selectClip: () => {},
updateCurrentTime: () => {},
});
@@ -0,0 +1,62 @@
import { TrackContext } from "~/contexts/TrackContext/TrackContext";
import { ReactNode, useCallback, 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";
interface Props {
children: ReactNode;
}
export const TrackProvider = ({ children }: Props) => {
const { characters, updateCharacters } = useUpdateCharacters();
const { camera, updateCamera } = useUpdateCamera();
const { audio, updateAudio } = useUpdateAudio();
const { objects, updateObject } = useUpdateObject();
const [selectedClip, setSelectedClip] = useState<string | null>(null);
const [time, setTime] = useState(0);
const selectClip = useCallback((clipId: string | null) => {
setSelectedClip(clipId);
}, []);
const updateCurrentTime = useCallback((newTime: number) => {
setTime(newTime);
}, []);
const values = useMemo(() => {
return {
characters,
camera,
audio,
objects,
scale: 1,
currentTime: time,
length: 12,
updateCharacters,
updateCamera,
updateAudio,
updateObject,
selectClip,
selectedClip,
updateCurrentTime,
};
}, [
characters,
updateCharacters,
selectClip,
selectedClip,
camera,
updateCamera,
audio,
updateAudio,
objects,
updateObject,
time,
updateCurrentTime,
]);
return (
<TrackContext.Provider value={values}>{children}</TrackContext.Provider>
);
};
@@ -0,0 +1,66 @@
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,
mute: !oldAudio,
};
});
}, []);
return {
audio,
updateAudio,
toggleAudioMute,
};
}
@@ -0,0 +1,55 @@
import { useCallback, useState } from "react";
import { CameraGroup } from "~/models/track";
export default function useUpdateCamera() {
const [camera, setCamera] = useState<CameraGroup>({
id: "CA1",
clips: [
{
id: "CA1-1",
length: 200,
offset: 0,
name: "cam 1",
},
{
id: "CA1-2",
length: 180,
offset: 300,
name: "cam 2",
},
],
});
const updateCamera = useCallback(
({
id,
offset,
length,
}: {
id: string;
length: number;
offset: number;
}) => {
setCamera((oldCamera) => {
const newClips = [...oldCamera.clips];
const clipIndex = newClips.findIndex((row) => row.id === id);
if (clipIndex === -1) {
return { ...oldCamera };
}
const clip = newClips[clipIndex];
clip.offset = offset;
clip.length = length;
return {
...oldCamera,
clips: newClips,
};
});
},
[],
);
return {
camera,
updateCamera,
};
}
@@ -0,0 +1,138 @@
import { useCallback, 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 toggleAudioMute = useCallback((characterId: string) => {
setCharacters((oldCharacters) => {
return oldCharacters.map((character) => ({
...character,
muted:
character.id === characterId ? !character.muted : character.muted,
}));
});
}, []);
return {
characters,
updateCharacters,
toggleAudioMute,
};
}
@@ -0,0 +1,53 @@
import { useCallback, useState } from "react";
import { ObjectGroup } from "~/models/track";
export default function useUpdateObject() {
const [objects, setObjects] = useState<ObjectGroup>({
id: "OB1",
objects: [
{
id: "OB1-O1",
clips: [
{
id: "OB1-O1-1",
length: 200,
offset: 0,
name: "obj 1",
},
],
},
],
});
const updateObject = useCallback(
({
id,
offset,
length,
}: {
id: string;
length: number;
offset: number;
}) => {
setObjects((oldObject) => {
return {
id: oldObject.id,
objects: oldObject.objects.map((object) => ({
id: object.id,
clips: object.clips.map((clip) => ({
...clip,
offset: clip.id === id ? offset : clip.offset,
length: clip.id === id ? length : clip.length,
})),
})),
};
});
},
[],
);
return {
objects,
updateObject,
};
}
-133
View File
@@ -1,133 +0,0 @@
import { TrackContext } from "~/contexts/TrackContext";
import { ReactNode, useCallback, useMemo, useState } from "react";
import { CharacterTrack } from "~/models/track";
interface Props {
children: ReactNode;
}
export const TrackProvider = ({ children }: Props) => {
const [characters, setCharacters] = useState<CharacterTrack[]>([
{
id: "t1",
animationClips: [
{
id: "1",
length: 200,
offset: 0,
name: "track 1",
selected: false,
},
{
id: "2",
length: 180,
offset: 300,
name: "track 2",
selected: false,
},
],
positionClips: [],
lipSyncClips: [],
},
]);
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;
}
},
[],
);
const selectClip = useCallback(
({
type,
id,
}: {
type: "animations" | "positions" | "lipSync";
id: string;
}) => {
if (type === "animations") {
setCharacters((oldCharacters) => {
return oldCharacters.map((character) => {
const newAnimationClips = character.animationClips.map((clip) => ({
...clip,
selected: false,
}));
const newPositionClips = character.positionClips.map((clip) => ({
...clip,
selected: false,
}));
const newLipSyncClips = character.lipSyncClips.map((clip) => ({
...clip,
selected: false,
}));
const clipIndex = newAnimationClips.findIndex(
(row) => row.id === id,
);
if (clipIndex === -1) {
return {
...character,
animationClips: newAnimationClips,
positionClips: newPositionClips,
lipSyncClips: newLipSyncClips,
};
}
const clip = newAnimationClips[clipIndex];
clip.selected = true;
return {
...character,
animationClips: newAnimationClips,
positionClips: newPositionClips,
lipSyncClips: newLipSyncClips,
};
});
});
return;
}
},
[],
);
console.log(characters);
const values = useMemo(() => {
return {
characters,
updateCharacters,
selectClip,
};
}, [characters, updateCharacters]);
return (
<TrackContext.Provider value={values}>{children}</TrackContext.Provider>
);
};
+23 -5
View File
@@ -1,11 +1,8 @@
import {Base} from "postcss-selector-parser";
export interface BaseClip {
id: string;
name: string;
offset: number;
length: number;
selected: boolean;
}
export interface AnimationClip extends BaseClip {}
@@ -14,16 +11,37 @@ export interface PositionClip extends BaseClip {}
export interface LipSyncClip extends BaseClip {}
export interface CharacterTrack {
export interface CharacterGroup {
id: string;
muted: boolean;
animationClips: AnimationClip[];
positionClips: PositionClip[];
lipSyncClips: LipSyncClip[];
}
export interface CameraTrack {
export interface CameraGroup {
id: string;
clips: CameraClip[];
}
export interface CameraClip extends BaseClip {}
export interface AudioGroup {
id: string;
muted: boolean;
clips: AudioClip[];
}
export interface AudioClip extends BaseClip {}
export interface ObjectGroup {
id: string;
objects: ObjectTrack[];
}
export interface ObjectTrack {
id: string;
clips: ObjectClip[];
}
export interface ObjectClip extends BaseClip {}
+1 -1
View File
@@ -14,7 +14,7 @@ export const LowerPanel = ({ children }: LowerPanelPropsI) => {
<div
className={[
"absolute bottom-0",
"h-[220px] w-screen overflow-y-auto",
"h-[260px] w-screen overflow-auto",
"border-t border-ui-panel-border",
"bg-ui-panel",
].join(" ")}
+4 -4
View File
@@ -12,8 +12,8 @@ import { ControlsVideo } from "./comps/ControlsVideo";
import { Timeline } from "./comps/Timeline";
import Editor from "./js/editor";
import { TrackContext } from "~/contexts/TrackContext";
import { TrackProvider } from "~/contexts/TrackProvider";
import { TrackContext } from "~/contexts/TrackContext/TrackContext";
import { TrackProvider } from "~/contexts/TrackContext/TrackProvider";
export const PageEnigma = () => {
// const { setTopBarInner } = useContext(TopBarInnerContext) || {};
@@ -128,9 +128,9 @@ export const PageEnigma = () => {
</div>
{/* Timeline */}
<div className="min-h-[220px]" ref={timelineRef}>
<div className="min-h-[260px]" ref={timelineRef}>
<TrackProvider>
<Timeline editorCurrent={editorRef.current} time={20} />
<Timeline editorCurrent={editorRef.current} length={12} />
</TrackProvider>
</div>
</div>
@@ -0,0 +1,45 @@
import { TrackClip } from "./TrackClip";
import { AudioGroup } from "~/models/track";
import { useContext } from "react";
import { TrackContext } from "~/contexts/TrackContext/TrackContext";
export const Audio = () => {
const { audio, updateAudio, length, scale } = useContext(TrackContext);
const fullWidth = length * 60 * 4 * scale;
const { clips } = audio!;
return (
<div
className="bg-audio-groupBg block rounded-lg pl-2 pr-4"
style={{ width: fullWidth + 90 }}
>
<div className="mb-2 text-sm text-white">Global Audio</div>
<div className="flex flex-col gap-2">
<div className="pl-16">
<div className="bg-audio-unselected relative mt-4 block h-8 w-full rounded">
{clips.map((clip, index) => (
<TrackClip
key={clip.id}
min={
index > 0
? clips[index - 1].offset + clips[index - 1].length
: 0
}
max={index < clips.length - 1 ? clips[index + 1].offset : 1000}
style="audio"
updateClip={updateAudio}
clip={clip}
/>
))}
<div
className="absolute text-xs text-white"
style={{ top: 6, left: 4 }}
>
Global Audio Track
</div>
</div>
</div>
</div>
</div>
);
};
+23 -23
View File
@@ -1,41 +1,41 @@
import { TrackClip } from "./TrackClip";
import {CameraTrack, CharacterTrack} from "~/models/track";
import { useContext } from "react";
import { TrackContext } from "~/contexts/TrackContext/TrackContext";
interface Props {
scale: number;
time: number;
camera: CameraTrack;
updateClip: (id: string, offset: number, length: number) => void;
}
export const Camera = () => {
const { camera, updateCamera, length, scale } = useContext(TrackContext);
const fullWidth = length * 60 * 4 * scale;
const { clips } = camera!;
export const Character = ({
camera: { clips },
updateClip,
}: Props) => {
return (
<div className="bg-charcter-groupBg block h-48 w-full rounded-lg p-2">
<div className="mb-2 text-sm text-white">Character</div>
<div
className="bg-camera-groupBg block rounded-lg pl-2 pr-4"
style={{ width: fullWidth + 90 }}
>
<div className="mb-2 text-sm text-white">Camera</div>
<div className="flex flex-col gap-2">
<div className="px-4">
<div className="bg-character-clip relative mt-4 block h-8 w-full rounded">
<div className="pl-16">
<div className="bg-camera-unselected relative mt-4 block h-8 w-full rounded">
{clips.map((clip, index) => (
<TrackClip
key={clip.id}
min={
index > 0
? clips[index - 1].offset +
clips[index - 1].length
? clips[index - 1].offset + clips[index - 1].length
: 0
}
max={
index < clips.length - 1
? clips[index + 1].offset
: 1000
}
updateClip={updateClip}
max={index < clips.length - 1 ? clips[index + 1].offset : 1000}
style="camera"
updateClip={updateCamera}
clip={clip}
/>
))}
<div
className="absolute text-xs text-white"
style={{ top: 6, left: 4 }}
>
Camera Position/Rotation
</div>
</div>
</div>
</div>
@@ -1,23 +1,30 @@
import { TrackClip } from "./TrackClip";
import { CharacterTrack } from "~/models/track";
import { useContext } from "react";
import { TrackContext } from "~/contexts/TrackContext/TrackContext";
interface Props {
scale: number;
time: number;
character: CharacterTrack;
updateClip: (id: string, offset: number, length: number) => void;
characterId: string;
}
export const Character = ({
character: { animationClips },
updateClip,
}: Props) => {
export const Character = ({ characterId }: Props) => {
const { characters, updateCharacters, length, scale } =
useContext(TrackContext);
const fullWidth = length * 60 * 4 * scale;
const character = characters.find((row) => (row.id = characterId));
if (!character) {
return false;
}
const { animationClips, positionClips, lipSyncClips } = character;
return (
<div className="bg-charcter-groupBg block h-48 w-full rounded-lg p-2">
<div className="mb-2 text-sm text-white">Character</div>
<div
className="block rounded-lg bg-character-groupBg pl-2 pr-4"
style={{ width: fullWidth + 90 }}
>
<div className="mb-2 text-xs text-white">Character</div>
<div className="flex flex-col gap-2">
<div className="px-4">
<div className="bg-character-clip relative mt-4 block h-8 w-full rounded">
<div className="pl-16">
<div className="relative mt-4 block h-8 rounded bg-character-unselected">
{animationClips.map((clip, index) => (
<TrackClip
key={clip.id}
@@ -30,19 +37,84 @@ export const Character = ({
max={
index < animationClips.length - 1
? animationClips[index + 1].offset
: 1000
: length * 60 * 4 * scale
}
style="character"
updateClip={(options) =>
updateCharacters({ ...options, type: "animations" })
}
updateClip={updateClip}
clip={clip}
/>
))}
<div
className="absolute text-xs text-white"
style={{ top: 6, left: 4 }}
>
Animation
</div>
</div>
</div>
<div className="px-4">
<div className="relative mt-4 block h-8 w-full rounded bg-brand-secondary-800"></div>
<div className="pl-16">
<div className="relative mt-4 block h-8 w-full rounded bg-character-unselected">
{positionClips.map((clip, index) => (
<TrackClip
key={clip.id}
min={
index > 0
? animationClips[index - 1].offset +
animationClips[index - 1].length
: 0
}
max={
index < animationClips.length - 1
? animationClips[index + 1].offset
: length * 60 * 4 * scale
}
style="character"
updateClip={(options) =>
updateCharacters({ ...options, type: "positions" })
}
clip={clip}
/>
))}
<div
className="absolute text-xs text-white"
style={{ top: 6, left: 4 }}
>
Character Position/Rotation
</div>
</div>
</div>
<div className="px-4">
<div className="relative mt-4 block h-8 w-full rounded bg-brand-secondary-800"></div>
<div className="pl-16">
<div className="relative mt-4 block h-8 w-full rounded bg-character-unselected">
{lipSyncClips.map((clip, index) => (
<TrackClip
key={clip.id}
min={
index > 0
? animationClips[index - 1].offset +
animationClips[index - 1].length
: 0
}
max={
index < animationClips.length - 1
? animationClips[index + 1].offset
: length * 60 * 4 * scale
}
style="character"
updateClip={(options) =>
updateCharacters({ ...options, type: "lipSync" })
}
clip={clip}
/>
))}
<div
className="absolute text-xs text-white"
style={{ top: 6, left: 4 }}
>
Lipsync Audio Track
</div>
</div>
</div>
</div>
</div>
@@ -0,0 +1,50 @@
import { TrackClip } from "./TrackClip";
import { useContext } from "react";
import { TrackContext } from "~/contexts/TrackContext/TrackContext";
export const Objects = () => {
const { objects, updateObject, length, scale } = useContext(TrackContext);
const fullWidth = length * 60 * 4 * scale;
return (
<div
className="block rounded-lg bg-character-groupBg pl-2 pr-4"
style={{ width: fullWidth + 90 }}
>
<div className="mb-2 text-sm text-white">Objects</div>
{objects.objects.map((object) => (
<div key={object.id} className="flex flex-col gap-2">
<div className="pl-16">
<div className="bg-objects-unselected relative mt-4 block h-8 w-full rounded">
{object.clips.map((clip, index) => (
<TrackClip
key={clip.id}
min={
index > 0
? object.clips[index - 1].offset +
object.clips[index - 1].length
: 0
}
max={
index < object.clips.length - 1
? object.clips[index + 1].offset
: 0
}
style="objects"
updateClip={updateObject}
clip={clip}
/>
))}
<div
className="absolute text-xs text-white"
style={{ top: 6, left: 4 }}
>
Mask Position/Rotation
</div>
</div>
</div>
</div>
))}
</div>
);
};
@@ -1,18 +1,21 @@
import { LowerPanel } from "~/modules/LowerPanel";
import Editor from "../../js/editor";
import { Character } from "./Character";
import { useCallback, useContext } from "react";
import { TrackContext } from "~/contexts/TrackContext";
import { Fragment, useContext } from "react";
import { TrackContext } from "~/contexts/TrackContext/TrackContext";
import { Button } from "~/components";
import { Camera } from "./Camera";
import { Audio } from "./Audio";
import { Objects } from "./Objects";
import { useMouseEventsAnimation } from "./utils/useMouseEventsAnimation";
export const Timeline = ({
editorCurrent,
time,
}: {
editorCurrent: Editor | null;
time: number;
}) => {
const { characters, updateCharacters } = useContext(TrackContext);
const { characters, objects, scale, length } = useContext(TrackContext);
const { onPointerDown, time } = useMouseEventsAnimation();
const handleButtonLoad = () => {
document.getElementById("load-upload")?.click();
@@ -22,59 +25,88 @@ export const Timeline = ({
};
const handleButtonPlay = () => {};
const updateClip = useCallback(
(id: string, offset: number, length: number) => {
updateCharacters({ type: "animations", id, offset, length });
},
[],
);
const sectionWidth = 60 * 4 * scale;
const fullHeight =
characters.length * 200 + objects.objects.length * 80 + 248 + 96;
// const width = (window.innerWidth - 40) / 10 - 2;
const width = 1500;
return (
<LowerPanel>
<>
<div className="fixed" style={{ top: 72, left: "calc(100% - 600px)" }}>
<Button onClick={handleButtonLoad}>Load</Button>
<Button onClick={handleButtonRender}>Render</Button>
<Button onClick={handleButtonPlay}>Play</Button>
</div>
<div className="h-10 border-b border-ui-panel-border"></div>
<input
style={{ display: "none" }}
type="file"
id="load-upload"
name="load-upload"
></input>
<div className="relative">
<div className="flex w-full px-10 text-sm text-white">
<div style={{ width }}>0</div>
<div style={{ width }}>1</div>
<div style={{ width }}>2</div>
<div style={{ width }}>3</div>
<div style={{ width }}>4</div>
<div style={{ width }}>5</div>
<div style={{ width }}>6</div>
<div style={{ width }}>7</div>
<div style={{ width }}>8</div>
<div style={{ width }}>9</div>
<div style={{ width: 16 }}>10</div>
<LowerPanel>
<div className="mt-4 flex h-3 text-sm text-white">
{Array(length)
.fill(0)
.map((_, index) => (
<Fragment key={index}>
<div
className="absolute"
style={{ left: index * sectionWidth + 92 }}
>
00:{index < 10 ? "0" + index.toString() : index.toString()}
</div>
<div
className="bg-ui-divider absolute block h-full"
style={{
width: 1,
left: index * sectionWidth + 88,
height: fullHeight,
}}
/>
</Fragment>
))}
<div
className="absolute"
style={{ left: length * sectionWidth + 92 }}
>
00:{length < 10 ? "0" + length.toString() : length.toString()}
</div>
<div
className="bg-ui-divider absolute block h-full"
style={{
width: 1,
left: length * sectionWidth + 88,
height: fullHeight,
}}
/>
</div>
<div className="mr-4 p-4">
<div className="p-4">
{characters.map((character) => (
<Character
key={character.id}
scale={1}
time={0}
character={character}
updateClip={updateClip}
/>
<Character key={character.id} characterId={character.id} />
))}
</div>
<div className="p-4">
<Camera />
</div>
<div className="p-4">
<Audio />
</div>
<div className="p-4">
<Objects />
</div>
<div
className="absolute block h-full bg-brand-primary"
style={{ left: time + 40, top: 0, width: 1 }}
/>
</div>
</LowerPanel>
className="absolute"
style={{ top: 8, left: time * 4 * scale + 88 }}
onPointerDown={onPointerDown}
>
<i
className="fa-solid fa-sort-down font-brand-primary absolute"
style={{ top: 0, left: 0 }}
></i>
<div
className="absolute block bg-brand-primary"
style={{
left: 0,
top: 8,
width: 3,
height: fullHeight,
}}
/>
</div>
</LowerPanel>
</>
);
};
@@ -1,61 +1,86 @@
import { useMouseEvents } from "./utils/useMouseEvents";
import { useMouseEventsClip } from "./utils/useMouseEventsClip";
import { BaseClip } from "~/models/track";
import { TrackContext } from "~/contexts/TrackContext";
import { TrackContext } from "~/contexts/TrackContext/TrackContext";
import { useContext } from "react";
interface Props {
min: number;
max: number;
style: "character" | "camera" | "audio" | "objects";
clip: BaseClip;
updateClip: (id: string, offset: number, length: number) => void;
updateClip: (options: { id: string; offset: number; length: number }) => void;
selected?: boolean;
}
export const TrackClip = ({ clip, min, max, updateClip }: Props) => {
const { selectClip } = useContext(TrackContext);
const {
onPointerDown,
onPointerUp,
onMouseLeave,
onMouseMove,
offset,
length,
} = useMouseEvents(clip, max, min, updateClip);
export const TrackClip = ({ clip, min, max, style, updateClip }: Props) => {
const { selectClip, selectedClip, scale } = useContext(TrackContext);
const { onPointerDown, offset, length } = useMouseEventsClip(
clip,
max,
min,
updateClip,
);
const classes = [
"absolute",
clip.id === selectedClip ? `bg-${style}-selected` : `bg-${style}-clip`,
];
return (
<>
<div
className="absolute text-sm text-white"
style={{ top: -20, left: offset }}
className={[...classes, "px-1", "rounded", "text-sm text-white"].join(
" ",
)}
style={{ top: -21, left: offset * 4 * scale + 2 }}
>
{clip.name}
</div>
<button
className={`bg-character-${clip.selected ? "selected" : "unselected"} absolute block h-full rounded-l`}
style={{ width: 15, left: offset, cursor: "w-resize" }}
className={[
...classes,
"rounded-l",
"block h-full",
clip.id === selectedClip
? "border-b-1 border-t-1 border-l-1 border border-r-0 border-white"
: "",
].join(" ")}
style={{ width: 15, left: offset * 4 * scale, cursor: "w-resize" }}
onPointerDown={(event) => onPointerDown(event, "left")}
onPointerUp={onPointerUp}
onMouseLeave={onMouseLeave}
onMouseMove={onMouseMove}
onClick={() => selectClip({ type: "animations", id: clip.id })}
onClick={() => selectClip(clip.id)}
/>
<button
className={`bg-character-${clip.selected ? "selected" : "unselected"} absolute block h-full`}
style={{ width: length - 30, left: offset + 15, cursor: "move" }}
className={[
...classes,
"block h-full",
clip.id === selectedClip
? "border-b-1 border-t-1 border border-l-0 border-r-0 border-white"
: "",
].join(" ")}
style={{
width: length * 4 * scale - 30,
left: offset * 4 * scale + 15,
cursor: "move",
}}
onPointerDown={(event) => onPointerDown(event, "drag")}
onPointerUp={onPointerUp}
onMouseLeave={onMouseLeave}
onMouseMove={onMouseMove}
onClick={() => selectClip({ type: "animations", id: clip.id })}
onClick={() => selectClip(clip.id)}
/>
<button
className={`bg-character-${clip.selected ? "selected" : "unselected"} absolute block h-full rounded-r`}
style={{ width: 15, left: offset + length - 15, cursor: "e-resize" }}
className={[
...classes,
"rounded-r",
"block h-full",
clip.id === selectedClip
? "border-b-1 border-t-1 border-r-1 border border-l-0 border-white"
: "",
].join(" ")}
style={{
width: 15,
left: offset * 4 * scale + length * 4 * scale - 15,
cursor: "e-resize",
}}
onPointerDown={(event) => onPointerDown(event, "right")}
onPointerUp={onPointerUp}
onMouseLeave={onMouseLeave}
onMouseMove={onMouseMove}
onClick={() => selectClip({ type: "animations", id: clip.id })}
onClick={() => selectClip(clip.id)}
/>
</>
);
@@ -1,65 +0,0 @@
import { useState, PointerEvent, MouseEvent } from "react";
export const useMouseEvents = (
clip: { id: string; offset: number; length: number; name: string },
max: number,
min: number,
updateClip: (id: string, offset: number, length: number) => void,
) => {
const [isActive, setIsActive] = useState("");
const [clientX, setClientX] = useState(0);
const [offset, setOffset] = useState(clip.offset);
const [length, setLength] = useState(clip.length);
return {
onPointerDown: (event: PointerEvent<HTMLButtonElement>, type: string) => {
if (event.button === 0) {
const x = event.clientX - 40;
setClientX(event.clientX);
setIsActive(type);
}
},
onPointerUp: (event: PointerEvent<HTMLButtonElement>) => {
event.stopPropagation();
event.preventDefault();
if (isActive) {
updateClip(clip.id, offset, length);
setIsActive("");
}
},
onMouseLeave: () => {
if (isActive) {
updateClip(clip.id, offset, length);
setIsActive("");
}
},
onMouseMove: (event: MouseEvent) => {
const deltaOffset = event.clientX - clientX + clip.offset;
const delta = event.clientX - clientX;
if (isActive === "drag") {
if (deltaOffset < min || deltaOffset + length > max) {
return;
}
setOffset(deltaOffset);
return;
}
if (isActive === "left") {
if (deltaOffset < min) {
return;
}
setOffset(deltaOffset);
setLength(clip.length - delta);
return;
}
if (isActive === "right") {
if (clip.length + delta < 30 || offset + clip.length + delta > max) {
return;
}
setLength(clip.length + delta);
}
},
length,
offset,
};
};
@@ -0,0 +1,52 @@
import React, { useContext, useEffect, useState } from "react";
import { TrackContext } from "~/contexts/TrackContext/TrackContext";
export const useMouseEventsAnimation = () => {
const { currentTime, updateCurrentTime, length, scale } =
useContext(TrackContext);
const [isActive, setIsActive] = useState("");
const [clientX, setClientX] = useState(0);
const [time, setTime] = useState(currentTime);
console.log("use", currentTime, time);
useEffect(() => {
const max = length * 60 * 4 * scale;
const onPointerUp = () => {
if (isActive) {
updateCurrentTime(Math.round(time));
setIsActive("");
}
};
const onMouseMove = (event: MouseEvent) => {
const delta = (event.clientX - clientX) / 4 / scale + currentTime;
if (isActive === "drag") {
if (delta < 0 || delta > max) {
return;
}
setTime(delta);
return;
}
};
window.addEventListener("pointerup", onPointerUp);
window.addEventListener("pointermove", onMouseMove);
return () => {
window.removeEventListener("pointerup", onPointerUp);
window.removeEventListener("pointermove", onMouseMove);
};
}, [clientX, isActive, length, updateCurrentTime, scale, time]);
return {
onPointerDown: (event: React.PointerEvent<HTMLDivElement>) => {
if (event.button === 0) {
setClientX(event.clientX);
setIsActive("drag");
}
},
time,
};
};
@@ -0,0 +1,79 @@
import React, { useContext, useEffect, useState } from "react";
import { BaseClip } from "~/models/track";
import { TrackContext } from "~/contexts/TrackContext/TrackContext";
export const useMouseEventsClip = (
clip: { id: string; offset: number; length: number; name: string },
max: number,
min: number,
updateClip: (options: { id: string; offset: number; length: number }) => void,
) => {
const [isActive, setIsActive] = useState("");
const [clientX, setClientX] = useState(0);
const [offset, setOffset] = useState(clip.offset);
const [length, setLength] = useState(clip.length);
const { scale } = useContext(TrackContext);
useEffect(() => {
const onPointerUp = () => {
if (isActive) {
updateClip({
id: clip.id,
offset: Math.round(offset),
length: Math.round(length),
} as BaseClip);
setIsActive("");
}
};
const onMouseMove = (event: MouseEvent) => {
const delta = (event.clientX - clientX) / 4 / scale;
const deltaOffset = delta + clip.offset;
if (isActive === "drag") {
if (deltaOffset < min || deltaOffset + length > max) {
return;
}
setOffset(deltaOffset);
return;
}
if (isActive === "left") {
if (deltaOffset < min) {
return;
}
setOffset(deltaOffset);
setLength(clip.length - delta);
return;
}
if (isActive === "right") {
if (clip.length + delta < 30 || offset + clip.length + delta > max) {
return;
}
setLength(clip.length + delta);
}
};
window.addEventListener("pointerup", onPointerUp);
window.addEventListener("pointermove", onMouseMove);
return () => {
window.removeEventListener("pointerup", onPointerUp);
window.removeEventListener("pointermove", onMouseMove);
};
}, [clientX, clip, min, max, isActive, length, offset, updateClip, scale]);
return {
onPointerDown: (
event: React.PointerEvent<HTMLButtonElement>,
type: string,
) => {
if (event.button === 0) {
setClientX(event.clientX);
setIsActive(type);
}
},
length,
offset,
};
};
+33 -1
View File
@@ -85,17 +85,49 @@ export default {
clip: "#7E92DA",
groupBg: "#2B3448",
},
camera: {
unselected: "#466C7C",
selected: "#56BBC1",
clip: "#5F949F",
groupBg: "#2B393E",
},
audio: {
unselected: "#864C68",
selected: "#D460A6",
clip: "#E37BAD",
groupBg: "#382940",
},
objects: {
unselected: "#7C5646",
selected: "#EA8E5A",
clip: "#D49D75",
groupBg: "#372E32",
},
"ui-background": "#1a1a27",
"ui-panel": "#242433",
"ui-panel-border": "#393948",
"ui-controls": "#39394D",
"ui-controls-button": "#676781",
"ui-divider": "#D3D3D3",
gray: colors.gray,
white: colors.white,
},
},
plugins: [],
safelist: ["bg-character-selected", "bg-character-unselected"],
safelist: [
"bg-character-selected",
"bg-character-unselected",
"bg-character-clip",
"bg-camera-selected",
"bg-camera-unselected",
"bg-camera-clip",
"bg-audio-selected",
"bg-audio-unselected",
"bg-audio-clip",
"bg-objects-selected",
"bg-objects-unselected",
"bg-objects-clip",
],
} satisfies Config;