feat: edit moving source trajectories

This commit is contained in:
Codex
2026-08-28 16:04:55 +09:00
parent acb36bf15d
commit a20b55e8e2
8 changed files with 499 additions and 15 deletions
+112
View File
@@ -7,11 +7,123 @@ export const DEFAULT_VIEW_PLANE = Object.freeze({
export const SOURCE_COLORS = ["#65f2bf", "#58c7e8", "#ffc56e", "#ff8f87", "#be9bff", "#8ee36b"];
export const MAX_TRAJECTORY_KEYFRAMES = 4096;
function copyPosition(position) {
return position.map((coordinate) => Number(coordinate));
}
function clamp(value, minimum, maximum) {
return Math.max(minimum, Math.min(maximum, value));
}
export function synchronizeSourcePosition(source) {
if (Array.isArray(source.trajectory) && source.trajectory.length > 0) {
source.position = copyPosition(source.trajectory[0].position);
}
return source.position;
}
export function createDefaultTrajectory(source, sceneDuration, plane = DEFAULT_VIEW_PLANE) {
const start = copyPosition(source.position);
const width = Number.isFinite(plane?.xMax - plane?.xMin) && plane.xMax > plane.xMin
? plane.xMax - plane.xMin
: DEFAULT_VIEW_PLANE.xMax - DEFAULT_VIEW_PLANE.xMin;
const minimum = Number.isFinite(plane?.xMin) ? plane.xMin : DEFAULT_VIEW_PLANE.xMin;
const maximum = Number.isFinite(plane?.xMax) ? plane.xMax : DEFAULT_VIEW_PLANE.xMax;
const offset = Math.max(0.1, width * 0.25);
const rightRoom = maximum - start[0];
const leftRoom = start[0] - minimum;
const direction = rightRoom >= offset || rightRoom >= leftRoom ? 1 : -1;
const endX = clamp(start[0] + direction * offset, minimum, maximum);
return [
{ time: 0, position: start },
{ time: sceneDuration, position: [endX, start[1], start[2]] },
];
}
export function enableSourceTrajectory(source, sceneDuration, plane = DEFAULT_VIEW_PLANE) {
if (!Array.isArray(source.trajectory) || source.trajectory.length < 2) {
source.trajectory = createDefaultTrajectory(source, sceneDuration, plane);
}
synchronizeSourcePosition(source);
return source.trajectory;
}
export function disableSourceTrajectory(source) {
synchronizeSourcePosition(source);
delete source.trajectory;
}
export function insertTrajectoryKeyframe(source) {
if (!Array.isArray(source.trajectory) || source.trajectory.length < 2 ||
source.trajectory.length >= MAX_TRAJECTORY_KEYFRAMES) {
return -1;
}
let leftIndex = 0;
let largestGap = -Infinity;
for (let index = 0; index < source.trajectory.length - 1; index += 1) {
const gap = source.trajectory[index + 1].time - source.trajectory[index].time;
if (gap > largestGap) {
largestGap = gap;
leftIndex = index;
}
}
if (!(largestGap > 0)) return -1;
const left = source.trajectory[leftIndex];
const right = source.trajectory[leftIndex + 1];
source.trajectory.splice(leftIndex + 1, 0, {
time: left.time + largestGap / 2,
position: left.position.map((coordinate, index) =>
coordinate + (right.position[index] - coordinate) / 2,
),
});
synchronizeSourcePosition(source);
return leftIndex + 1;
}
export function removeTrajectoryKeyframe(source, index) {
if (!Array.isArray(source.trajectory) || source.trajectory.length <= 2 ||
!Number.isInteger(index) || index < 0 || index >= source.trajectory.length) return false;
source.trajectory.splice(index, 1);
synchronizeSourcePosition(source);
return true;
}
export function trajectoryTimeBounds(source, index, sceneDuration) {
const trajectory = source.trajectory;
const duration = Number.isFinite(sceneDuration) && sceneDuration > 0 ? sceneDuration : 0;
const gap = Math.max(1e-9, duration * 1e-9);
return {
minimum: index === 0 ? 0 : trajectory[index - 1].time + gap,
maximum: index === trajectory.length - 1 ? duration : trajectory[index + 1].time - gap,
};
}
export function updateTrajectoryKeyframeTime(source, index, requestedTime, sceneDuration) {
if (!Number.isFinite(requestedTime) || !source.trajectory?.[index]) return source.trajectory?.[index]?.time;
const { minimum, maximum } = trajectoryTimeBounds(source, index, sceneDuration);
source.trajectory[index].time = clamp(requestedTime, minimum, maximum);
return source.trajectory[index].time;
}
export function scaleSceneTrajectories(scene, previousDuration, nextDuration) {
if (!Number.isFinite(previousDuration) || previousDuration <= 0 ||
!Number.isFinite(nextDuration) || nextDuration <= 0) return;
const scale = nextDuration / previousDuration;
for (const source of scene.sources) {
if (!Array.isArray(source.trajectory)) continue;
source.trajectory.forEach((keyframe) => { keyframe.time *= scale; });
synchronizeSourcePosition(source);
}
}
export function prepareSceneForEditing(scene) {
const prepared = structuredClone(scene);
prepared.assets ??= [];
prepared.ambient ??= [];
prepared.mix ??= { normalization: "none", targetPeak: 0.95 };
prepared.sources.forEach(synchronizeSourcePosition);
return prepared;
}