133 lines
4.8 KiB
TypeScript
133 lines
4.8 KiB
TypeScript
import { useCutsceneStore, useSelectedCutscene } from '../../store/cutsceneStore';
|
|
import { useShallow } from 'zustand/react/shallow';
|
|
import { AVAILABLE_IMAGES } from '../../constants/images';
|
|
import { EASING_OPTIONS } from '../../constants/easings';
|
|
import type { ImagePose } from '../../types/cutscene';
|
|
import styles from './RightPanel.module.css';
|
|
|
|
function NumField({ label, value, onChange, min, step }: {
|
|
label: string; value: number; onChange: (v: number) => void; min?: number; step?: number;
|
|
}) {
|
|
return (
|
|
<div className={styles.field}>
|
|
<label>{label}</label>
|
|
<input
|
|
type="number"
|
|
value={value}
|
|
min={min}
|
|
step={step ?? 1}
|
|
onChange={e => onChange(Number(e.target.value))}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function PoseFields({ label, pose, onChange }: {
|
|
label: string;
|
|
pose: ImagePose;
|
|
onChange: (patch: Partial<ImagePose>) => void;
|
|
}) {
|
|
return (
|
|
<div className={styles.poseGroup}>
|
|
<div className={styles.poseTitle}>{label}</div>
|
|
<div className={styles.poseRow}>
|
|
<div className={styles.field}>
|
|
<label>centerX</label>
|
|
<input type="number" value={pose.centerX} step={0.01} min={0} max={1}
|
|
onChange={e => onChange({ centerX: Number(e.target.value) })} />
|
|
</div>
|
|
<div className={styles.field}>
|
|
<label>centerY</label>
|
|
<input type="number" value={pose.centerY} step={0.01} min={0} max={1}
|
|
onChange={e => onChange({ centerY: Number(e.target.value) })} />
|
|
</div>
|
|
<div className={styles.field}>
|
|
<label>scale</label>
|
|
<input type="number" value={pose.scale} step={0.05} min={0.1}
|
|
onChange={e => onChange({ scale: Number(e.target.value) })} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function LayerProperties() {
|
|
const cutscene = useSelectedCutscene();
|
|
const { selectedLayerIndex, updateLayer, updateLayerFrom, updateLayerTo } = useCutsceneStore(useShallow(s => ({
|
|
selectedLayerIndex: s.selectedLayerIndex,
|
|
updateLayer: s.updateLayer,
|
|
updateLayerFrom: s.updateLayerFrom,
|
|
updateLayerTo: s.updateLayerTo,
|
|
})));
|
|
|
|
if (!cutscene || selectedLayerIndex === null) return null;
|
|
const seg = cutscene.imageSegments[selectedLayerIndex];
|
|
if (!seg) return null;
|
|
|
|
return (
|
|
<div className={styles.section}>
|
|
<div className={styles.sectionTitle}>Layer {selectedLayerIndex + 1}</div>
|
|
|
|
<div className={styles.field}>
|
|
<label>Image</label>
|
|
<select
|
|
value={AVAILABLE_IMAGES.includes(seg.path) ? seg.path : '__custom__'}
|
|
onChange={e => {
|
|
if (e.target.value !== '__custom__') updateLayer(selectedLayerIndex, { path: e.target.value });
|
|
}}
|
|
>
|
|
{AVAILABLE_IMAGES.map(img => (
|
|
<option key={img} value={img}>{img.split('/').pop()}</option>
|
|
))}
|
|
{!AVAILABLE_IMAGES.includes(seg.path) && (
|
|
<option value="__custom__">(custom)</option>
|
|
)}
|
|
</select>
|
|
</div>
|
|
|
|
<div className={styles.field}>
|
|
<label>Path (manual)</label>
|
|
<input
|
|
type="text"
|
|
value={seg.path}
|
|
onChange={e => updateLayer(selectedLayerIndex, { path: e.target.value })}
|
|
placeholder="resources/..."
|
|
/>
|
|
</div>
|
|
|
|
<div className={styles.row2}>
|
|
<NumField label="Width" value={seg.width} onChange={v => updateLayer(selectedLayerIndex, { width: v })} min={1} />
|
|
<NumField label="Height" value={seg.height} onChange={v => updateLayer(selectedLayerIndex, { height: v })} min={1} />
|
|
</div>
|
|
|
|
<div className={styles.row2}>
|
|
<NumField label="Start (ms)" value={seg.startMs} onChange={v => updateLayer(selectedLayerIndex, { startMs: v })} min={0} />
|
|
<NumField label="End (ms)" value={seg.endMs} onChange={v => updateLayer(selectedLayerIndex, { endMs: v })} min={0} />
|
|
</div>
|
|
|
|
<div className={styles.row2}>
|
|
<NumField label="Fade in (ms)" value={seg.fadeInMs} onChange={v => updateLayer(selectedLayerIndex, { fadeInMs: v })} min={0} />
|
|
<NumField label="Fade out (ms)" value={seg.fadeOutMs} onChange={v => updateLayer(selectedLayerIndex, { fadeOutMs: v })} min={0} />
|
|
</div>
|
|
|
|
<div className={styles.field}>
|
|
<label>Easing</label>
|
|
<select value={seg.easing} onChange={e => updateLayer(selectedLayerIndex, { easing: e.target.value as typeof seg.easing })}>
|
|
{EASING_OPTIONS.map(e => <option key={e} value={e}>{e}</option>)}
|
|
</select>
|
|
</div>
|
|
|
|
<PoseFields
|
|
label="From"
|
|
pose={seg.from}
|
|
onChange={patch => updateLayerFrom(selectedLayerIndex, patch)}
|
|
/>
|
|
<PoseFields
|
|
label="To"
|
|
pose={seg.to}
|
|
onChange={patch => updateLayerTo(selectedLayerIndex, patch)}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|