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
+63 -8
View File
@@ -1,4 +1,5 @@
import { SOURCE_COLORS } from "./model.js";
import { sourcePositionAtTime } from "/packages/acoustics-core/trajectory.js";
function finitePlane(plane) {
return [plane.xMin, plane.xMax, plane.yMin, plane.yMax].every(Number.isFinite) &&
@@ -45,9 +46,50 @@ function drawGrid(context, width, height, plane) {
context.textAlign = "left";
}
function drawSource(context, source, index, selected, plane, width, height) {
if (!source.position?.every(Number.isFinite)) return;
const point = coordinateToPixel(source.position, plane, width, height);
function drawTrajectory(context, source, index, selected, selectedKeyframeIndex, plane, width, height) {
if (!Array.isArray(source.trajectory) || source.trajectory.length < 2) return;
const points = source.trajectory.map(({ position }) => coordinateToPixel(position, plane, width, height));
const color = SOURCE_COLORS[index % SOURCE_COLORS.length];
context.save();
context.strokeStyle = color;
context.globalAlpha = selected ? 0.85 : 0.28;
context.lineWidth = selected ? 2.5 : 1.5;
context.setLineDash([7, 5]);
context.beginPath();
points.forEach((point, pointIndex) => {
if (pointIndex === 0) context.moveTo(point.x, point.y);
else context.lineTo(point.x, point.y);
});
context.stroke();
context.setLineDash([]);
points.forEach((point, pointIndex) => {
context.globalAlpha = selected ? 1 : 0.42;
context.fillStyle = color;
context.beginPath();
context.arc(point.x, point.y, pointIndex === selectedKeyframeIndex && selected ? 6 : 4, 0, Math.PI * 2);
context.fill();
if (pointIndex === selectedKeyframeIndex && selected) {
context.strokeStyle = "#fff";
context.lineWidth = 2;
context.beginPath();
context.arc(point.x, point.y, 9, 0, Math.PI * 2);
context.stroke();
}
});
context.restore();
}
function positionAtPreviewTime(source, previewTime) {
try {
return sourcePositionAtTime(source, previewTime);
} catch {
return source.position;
}
}
function drawSource(context, source, position, index, selected, plane, width, height) {
if (!position?.every(Number.isFinite)) return;
const point = coordinateToPixel(position, plane, width, height);
if (point.x < -30 || point.x > width + 30 || point.y < -30 || point.y > height + 30) return;
const color = SOURCE_COLORS[index % SOURCE_COLORS.length];
context.save();
@@ -81,8 +123,8 @@ export function createSceneCanvas(canvas, emptyElement, badgeElement, onMove) {
const context = canvas.getContext("2d");
let current = null;
function render(scene, image, plane, selectedSourceId) {
current = { scene, image, plane, selectedSourceId };
function render(scene, image, plane, selectedSourceId, previewTime = 0, selectedKeyframeIndex = 0) {
current = { scene, image, plane, selectedSourceId, previewTime, selectedKeyframeIndex };
const rect = canvas.getBoundingClientRect();
const width = Math.max(1, rect.width);
const height = Math.max(1, rect.height);
@@ -111,12 +153,22 @@ export function createSceneCanvas(canvas, emptyElement, badgeElement, onMove) {
context.fillRect(0, 0, width, height);
}
scene.sources.forEach((source, index) => {
drawSource(context, source, index, source.id === selectedSourceId, plane, width, height);
drawTrajectory(
context, source, index, source.id === selectedSourceId,
source.id === selectedSourceId ? selectedKeyframeIndex : -1,
plane, width, height,
);
});
scene.sources.forEach((source, index) => {
drawSource(
context, source, positionAtPreviewTime(source, previewTime), index,
source.id === selectedSourceId, plane, width, height,
);
});
emptyElement.classList.toggle("is-hidden", Boolean(image));
badgeElement.textContent =
`x ${plane.xMin.toFixed(2)}…${plane.xMax.toFixed(2)} m · ` +
`y ${plane.yMin.toFixed(2)}…${plane.yMax.toFixed(2)} m`;
`y ${plane.yMin.toFixed(2)}…${plane.yMax.toFixed(2)} m · t ${previewTime.toFixed(2)} s`;
}
canvas.addEventListener("pointerdown", (event) => {
@@ -128,7 +180,10 @@ export function createSceneCanvas(canvas, emptyElement, badgeElement, onMove) {
});
new ResizeObserver(() => {
if (current) render(current.scene, current.image, current.plane, current.selectedSourceId);
if (current) render(
current.scene, current.image, current.plane, current.selectedSourceId,
current.previewTime, current.selectedKeyframeIndex,
);
}).observe(canvas);
return { render };