Files

135 lines
4.8 KiB
TypeScript

import { useLayoutEffect, useRef, useState } from 'react'
import { ImagePlus, Plus } from 'lucide-react'
import { imagePathsFromDataTransfer, imagePathsFromFiles } from '../lib/keyframe-drop'
import { lastFrameFromDuration, type DraggedFrame } from '../lib/keyframe-timeline'
import { applyKeyframeImagePaths, type KeyframeItem } from '../lib/multi-keyframe'
import { KeyframeTimeline } from './KeyframeTimeline'
interface MultiKeyframePanelProps {
keyframes: readonly KeyframeItem[]
duration: number | null
fps: number
maxCount: number
playheadFrame: number
onPlayheadChange: (frameIndex: number) => void
onChange: (keyframes: KeyframeItem[]) => void
onDragFrameChange?: (drag: DraggedFrame | null) => void
}
export function MultiKeyframePanel({
keyframes,
duration,
fps,
maxCount,
playheadFrame,
onPlayheadChange,
onChange,
onDragFrameChange,
}: MultiKeyframePanelProps) {
const fileInputRef = useRef<HTMLInputElement>(null)
const replaceIdRef = useRef<string | null>(null)
const [isDragOver, setIsDragOver] = useState(false)
const lastFrame = duration == null ? null : lastFrameFromDuration(duration, fps)
const addEnabled = lastFrame !== null && keyframes.length < maxCount
useLayoutEffect(() => {
if (lastFrame === null) onDragFrameChange?.(null)
}, [lastFrame, onDragFrameChange])
const applyPaths = (paths: string[], replaceId: string | null = null) => {
onChange(applyKeyframeImagePaths({
keyframes,
paths,
replaceId,
lastFrame,
preferredFrame: playheadFrame,
maxCount,
}))
}
const openFilePicker = (replaceId: string | null) => {
replaceIdRef.current = replaceId
fileInputRef.current?.click()
}
return (
<div className="border-b border-zinc-800/60 px-2 pb-2 pt-2">
<div className="mb-1.5 flex items-center justify-between">
<div className="flex items-center gap-1.5 text-[11px] font-medium text-zinc-300">
<ImagePlus className="h-3.5 w-3.5 text-zinc-500" />
<span>Keyframes</span>
<span className="text-zinc-600">{keyframes.length}/{maxCount}</span>
</div>
<button
type="button"
disabled={!addEnabled}
title={keyframes.length >= maxCount
? `You can place up to ${maxCount} keyframes`
: lastFrame === null
? 'Choose a duration to place keyframes'
: 'Add keyframes at the playhead'}
className="flex h-6 items-center gap-1 rounded-md border border-dashed border-zinc-700 px-2 text-[10px] text-zinc-400 hover:border-zinc-500 hover:text-zinc-200 disabled:cursor-not-allowed disabled:opacity-40"
onClick={() => openFilePicker(null)}
onDragOver={(event) => {
event.preventDefault()
if (addEnabled) setIsDragOver(true)
}}
onDragLeave={() => setIsDragOver(false)}
onDrop={(event) => {
event.preventDefault()
setIsDragOver(false)
applyPaths(imagePathsFromDataTransfer(event.dataTransfer))
}}
>
<Plus className="h-3 w-3" />
<span className={isDragOver ? 'text-blue-300' : undefined}>Add keyframe</span>
</button>
</div>
{lastFrame === null ? (
<div className="flex h-12 items-center justify-center rounded-lg border border-dashed border-zinc-700 text-[10px] text-zinc-500">
Choose a duration to place keyframes
</div>
) : (
<KeyframeTimeline
keyframes={keyframes}
fps={fps}
lastFrame={lastFrame}
playheadFrame={Math.min(playheadFrame, lastFrame)}
onPlayheadChange={onPlayheadChange}
onDragFrameChange={onDragFrameChange}
onFrameChange={(id, frameIndex) => {
onChange(keyframes.map((keyframe) => (
keyframe.id === id ? { ...keyframe, frameIndex } : keyframe
)))
onPlayheadChange(frameIndex)
}}
onStrengthChange={(id, strength) => {
onChange(keyframes.map((keyframe) => (
keyframe.id === id ? { ...keyframe, strength } : keyframe
)))
}}
onReplaceRequest={(id) => openFilePicker(id)}
onDelete={(id) => onChange(keyframes.filter((keyframe) => keyframe.id !== id))}
onImagesDrop={(dataTransfer, replaceId) => {
applyPaths(imagePathsFromDataTransfer(dataTransfer), replaceId)
}}
/>
)}
<input
ref={fileInputRef}
type="file"
accept="image/*"
multiple
className="hidden"
onChange={(event) => {
applyPaths(imagePathsFromFiles(event.target.files ?? []), replaceIdRef.current)
replaceIdRef.current = null
event.target.value = ''
}}
/>
</div>
)
}