core/probes/image.probe.ts
core/probes/image.probe.ts is a file in Bane's Lab Social Share. 34 lines of code and 9 definitions.
import type { CardFinding, CardSpec } from "#types/card.types";
import { MILLISECONDS, SEAM_TOLERANCE } from "#configuration/constants/card.constants";
import { ANIMATION_OUT_OF_STEP } from "#configuration/strings/card.strings";
import { join } from "node:path";
import sharp from "sharp";
export const loopDurationOf = async function loopDurationOf(location: string): Promise<number> {
const { delay } = await sharp(location, { animated: true }).metadata();
return (delay ?? []).reduce((sum, value) => sum + value, 0);
};
const inStep = function inStep(cardMs: number, loopMs: number): boolean {
if (loopMs <= 0) {
return true;
}
const cycles = cardMs / loopMs;
return Math.abs(cycles - Math.round(cycles)) <= SEAM_TOLERANCE && Math.round(cycles) >= 1;
};
export const animationFindings = async function animationFindings(
specs: readonly CardSpec[],
publicRoot: string,
): Promise<readonly CardFinding[]> {
const checks = specs
.filter((spec) => spec.timeline.loop && spec.timeline.frames > 1)
.flatMap((spec) =>
spec.layers.flatMap((layer) => (layer.kind === "animation" ? [{ source: layer.source, spec }] : [])),
);
const results = await Promise.all(
checks.map(async ({ source, spec }) => {
const cardMs = (spec.timeline.frames / spec.timeline.fps) * MILLISECONDS;
const loopMs = await loopDurationOf(join(publicRoot, source));
return inStep(cardMs, loopMs) ? null : { card: spec.id, message: ANIMATION_OUT_OF_STEP };
}),
);
return results.filter((result): result is CardFinding => result !== null);
};