mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-03 00:47:53 +00:00
Fix a number of issues
This commit is contained in:
1 parent
7f4fa98259
commit
f3d7fe6ea5
21 files changed
+968
-351
No files matched your search
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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
@@ -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 {}
|
||||
@@ -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(" ")}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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
@@ -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;
|
||||
Reference in new issue
Block a user