A camera plan can look orderly in a spreadsheet while asking two cues to control position during the same few frames. Catching that conflict before handoff saves the reviewer from reconstructing timing from prose. This Node.js check takes a small JSON shot plan and reports one overlapping pair for each cue that starts while an earlier cue on its track is still active.
1. Give each cue a track and a half-open window
Use integer milliseconds measured from the start of the shot. A cue owns its start time and releases its track at its end time. With this convention, a pan ending at 3000 and a follow starting at 3000 share a boundary and can both pass.
The example has two tracks, position and zoom. A zoom can overlap a position cue under this local rule. Two position cues cannot overlap. Set the track vocabulary to the controls your editor or renderer actually treats as independent.
A plan also carries one positive shot duration. Each cue must fit inside it and have a unique ID, so the output can identify the source row that needs a change.
2. Keep the earlier cue that ends latest
Sort each track by start time. When starts tie, put the longer window first. Keep the earlier cue with the greatest end time as you scan. If the next start is below that end, the intersection begins at the next start and ends at the smaller of the two ends.
This extra bit of state matters when windows nest. A short cue can finish before the next cue starts while both are still inside a longer pan. Comparing only neighboring rows would miss the second conflict.
Save the following as cue-windows.mjs. Sorting happens on fresh per-track arrays, so the caller's plan retains its original row order.
export function checkCueWindows(plan) {
const { durationMs, cues } = plan ?? {};
if (!Number.isSafeInteger(durationMs) || durationMs <= 0 ||
!Array.isArray(cues) || cues.length === 0) {
throw new Error('Expected positive durationMs and a nonempty cues array');
}
const ids = new Set();
const tracks = new Map();
for (const cue of cues) {
const { id, track, startMs, endMs } = cue ?? {};
if (typeof id !== 'string' || id.trim() === '' || ids.has(id) ||
!['position', 'zoom'].includes(track) ||
!Number.isSafeInteger(startMs) || !Number.isSafeInteger(endMs) ||
startMs < 0 || startMs >= endMs || endMs > durationMs) {
throw new Error('Invalid cue or repeated cue ID');
}
ids.add(id);
if (!tracks.has(track)) tracks.set(track, []);
tracks.get(track).push({ id, startMs, endMs });
}
const conflicts = [];
for (const [track, windows] of tracks) {
windows.sort((a, b) => a.startMs - b.startMs || b.endMs - a.endMs);
let latestEnd;
for (const cue of windows) {
if (latestEnd && cue.startMs < latestEnd.endMs) {
conflicts.push({
track,
earlierId: latestEnd.id,
laterId: cue.id,
startMs: cue.startMs,
endMs: Math.min(cue.endMs, latestEnd.endMs)
});
}
if (!latestEnd || cue.endMs > latestEnd.endMs) latestEnd = cue;
}
}
return conflicts;
}
The returned earlierId identifies the earlier entry in the scan, including ties. The output gives a witness for each conflicting arrival. An editor can use that witness to locate a collision quickly. A tool that needs every overlapping pair would need to keep all active cues.
3. Try a six-second shot with a nested conflict
Save this unsorted fixture as shot.json. The values are synthetic planning data for a six-second shot.
{
"durationMs": 6000,
"cues": [
{
"id": "follow",
"track": "position",
"startMs": 3000,
"endMs": 6000
},
{
"id": "zoom",
"track": "zoom",
"startMs": 500,
"endMs": 1500
},
{
"id": "pan",
"track": "position",
"startMs": 0,
"endMs": 3000
},
{
"id": "nudge",
"track": "position",
"startMs": 500,
"endMs": 900
},
{
"id": "drift",
"track": "position",
"startMs": 1600,
"endMs": 2200
}
]
}
The pan covers 0 through 3000 milliseconds, with 3000 excluded. It overlaps nudge from 500 to 900 and drift from 1600 to 2200. The nudge has already ended by the time drift starts, so drift is the case that exposes a neighbor-only check.
The zoom can run during the pan because it owns a separate track. The follow cue starts at the exact instant the pan releases position.
4. Make the check usable from a terminal
Save this as check.mjs beside the module. It reads a local JSON file and prints conflict windows with both IDs.
import { readFile } from 'node:fs/promises';
import { checkCueWindows } from './cue-windows.mjs';
try {
if (process.argv.length !== 3) throw new Error('Usage: node check.mjs plan.json');
const plan = JSON.parse(await readFile(process.argv[2], 'utf8'));
const conflicts = checkCueWindows(plan);
console.log(JSON.stringify({ conflicts }, null, 2));
process.exitCode = conflicts.length ? 1 : 0;
} catch (error) {
console.error(error.message);
process.exitCode = 2;
}
node check.mjs shot.json
Exit 0 means the plan passed. Exit 1 means the JSON parsed and the plan contains same-track conflicts. Exit 2 covers input, file, and validation errors. Keep these cases separate so an editor can distinguish a timing change from a malformed plan.
5. Repair the timing and run it again
First decide whether the pan or the smaller moves should own the position track. If the pan and follow are the intended sequence, remove nudge and drift. If either smaller move needs to survive, split or shorten the pan deliberately and check the resulting plan again.
For the fixture above, keeping follow, zoom, and pan yields an empty conflict list. Changing the shot duration to 1 millisecond while keeping the original cues makes the plan fail validation.
Local verification on Node.js 24.20.0 covered 17 assertions, including touching edges, nested windows, tie ordering, a new cue extending the active end, separate tracks, invalid bounds, duplicate IDs and input order. Separate CLI runs verified exit codes 0, 1 and 2.
6. Keep the timing check at the handoff boundary
Use the JSON plan when a teammate needs stable cue IDs and precise timings. Keep visual intent alongside it in the shot brief, including what the camera should reveal and where it should settle.
A passing report confirms this timing policy for the plan. Camera feel, easing, framing, and model adherence still require review of the actual shot. The examples here exercise local JSON and integer arithmetic.
AI assistance was used to draft the tutorial and code. Verification used synthetic plans and local CLI runs.
Source adaptation: PixMind's Hailuo video generator guide. Its camera direction examples motivated the shot handoff check.
JavaScript reference: MDN Array.prototype.sort().
About the author
- The PixMind team shares practical AI image and video workflows. PixMind brings text-to-image, reference-based creation, editing, and video tools into one workspace. Website: https://www.pixmind.io/
Comments
Join the discussionNo comments yet. Be the first to comment.