( frames: MotionFrame[], within: string | undefined, maxStaticSec: number, )
| 162 | } |
| 163 | |
| 164 | function keepsMoving( |
| 165 | frames: MotionFrame[], |
| 166 | within: string | undefined, |
| 167 | maxStaticSec: number, |
| 168 | ): LayoutIssue[] { |
| 169 | // "*" is reserved for whole-canvas scope; motionSpec.ts rejects it as a user-supplied withinSelector. |
| 170 | const scope = within ?? "*"; |
| 171 | if (within && frames.every((frame) => !frame.liveness[scope])) { |
| 172 | return [missingIssue(within, 0)]; |
| 173 | } |
| 174 | |
| 175 | const issues: LayoutIssue[] = []; |
| 176 | const first = frames[0]; |
| 177 | if (!first) return issues; |
| 178 | let runStart = first.time; |
| 179 | let runSig = first.liveness[scope] ?? ""; |
| 180 | const flush = (endTime: number) => { |
| 181 | const span = endTime - runStart; |
| 182 | if (span > maxStaticSec) { |
| 183 | issues.push({ |
| 184 | code: "motion_frozen", |
| 185 | severity: "error", |
| 186 | time: runStart, |
| 187 | selector: within ?? "composition", |
| 188 | message: `nothing moves${within ? ` within ${within}` : ""} between ${round(runStart)}s and ${round(endTime)}s (${round(span)}s static) — should keep moving`, |
| 189 | rect: ZERO_RECT, |
| 190 | fixHint: |
| 191 | "Add or extend motion so no shot freezes for this long, or shorten the static hold.", |
| 192 | }); |
| 193 | } |
| 194 | }; |
| 195 | |
| 196 | let lastTime = runStart; |
| 197 | for (const frame of frames.slice(1)) { |
| 198 | lastTime = frame.time; |
| 199 | const sig = frame.liveness[scope] ?? ""; |
| 200 | if (sig !== runSig) { |
| 201 | flush(frame.time); |
| 202 | runStart = frame.time; |
| 203 | runSig = sig; |
| 204 | } |
| 205 | } |
| 206 | flush(lastTime); |
| 207 | return issues; |
| 208 | } |
| 209 | |
| 210 | /** |
| 211 | * Evaluate motion assertions against the dense `element × time` matrix. |
no test coverage detected