aac-chatbot / frontend /src /lib /airTemplates.ts
shwetangisingh's picture
imultimodal sensing → real stylistic constraints
af222c8
// Default air-writing template bank.
// Each template is a normalised 32-point [x, y] trajectory (coords in [0, 1]).
// Matched against live trajectories via DTW in AirWriter.recognise.
// To add a new template: pick a distinctive *single-stroke* shape,
// sample ~32 evenly-spaced points from stroke start → end, normalise
// x/y into [0, 1], and add an entry to DEFAULT_AIR_TEMPLATES.
//
// DTW quality tips:
// - Stick to single-stroke shapes. Multi-stroke shapes (like an X) look
// like a teleport to DTW and will mis-match.
// - Shapes should be distinctive in direction and extent — a small
// check-mark and a big slash look similar after normalisation.
function linear(from: [number, number], to: [number, number], n: number): [number, number][] {
const out: [number, number][] = [];
for (let i = 0; i < n; i++) {
const t = i / (n - 1);
out.push([from[0] + t * (to[0] - from[0]), from[1] + t * (to[1] - from[1])]);
}
return out;
}
function concat(...segs: [number, number][][]): [number, number][] {
const out: [number, number][] = [];
for (const s of segs) out.push(...s);
return resample(out, 32);
}
function resample(pts: [number, number][], n: number): [number, number][] {
if (pts.length < 2) return pts;
const out: [number, number][] = [];
for (let i = 0; i < n; i++) {
const t = (i / (n - 1)) * (pts.length - 1);
const lo = Math.floor(t);
const hi = Math.min(lo + 1, pts.length - 1);
const frac = t - lo;
out.push([
pts[lo][0] + frac * (pts[hi][0] - pts[lo][0]),
pts[lo][1] + frac * (pts[hi][1] - pts[lo][1]),
]);
}
return out;
}
// check-mark: short down-right, then long up-right → affirmation
const YES: [number, number][] = concat(
linear([0.0, 0.5], [0.35, 1.0], 12),
linear([0.35, 1.0], [1.0, 0.0], 20)
);
// question-mark: curve over the top, then down to the dot → clarifying
const QUESTION: [number, number][] = concat(
linear([0.1, 0.25], [0.5, 0.0], 8),
linear([0.5, 0.0], [0.9, 0.25], 8),
linear([0.9, 0.25], [0.5, 0.55], 8),
linear([0.5, 0.55], [0.5, 1.0], 8)
);
// zig-zag wave across the top → greeting
const HI: [number, number][] = concat(
linear([0.0, 0.0], [0.25, 1.0], 8),
linear([0.25, 1.0], [0.5, 0.0], 8),
linear([0.5, 0.0], [0.75, 1.0], 8),
linear([0.75, 1.0], [1.0, 0.0], 8)
);
// straight vertical line bottom→top → "help" (raise hand / SOS mental model)
const HELP: [number, number][] = linear([0.5, 1.0], [0.5, 0.0], 32);
// horizontal line left→right → "done" (close / finish)
const DONE: [number, number][] = linear([0.0, 0.5], [1.0, 0.5], 32);
// plus-sign-ish as a single stroke: long down, backtrack up, then across → "more"
// mimics drawing "+" as one continuous stroke (down, back, right)
const MORE: [number, number][] = concat(
linear([0.5, 0.0], [0.5, 1.0], 12),
linear([0.5, 1.0], [0.5, 0.5], 6),
linear([0.5, 0.5], [1.0, 0.5], 14)
);
// single wave (down-up-down-up smooth) → "water" (fluid/ocean mental model)
const WATER: [number, number][] = concat(
linear([0.0, 0.5], [0.2, 0.9], 6),
linear([0.2, 0.9], [0.4, 0.1], 8),
linear([0.4, 0.1], [0.6, 0.9], 8),
linear([0.6, 0.9], [0.8, 0.1], 8),
linear([0.8, 0.1], [1.0, 0.5], 2)
);
// square/box (traced as one stroke) → "stop"
// start top-left, go right, down, left, up — closing the box
const STOP: [number, number][] = concat(
linear([0.0, 0.0], [1.0, 0.0], 8),
linear([1.0, 0.0], [1.0, 1.0], 8),
linear([1.0, 1.0], [0.0, 1.0], 8),
linear([0.0, 1.0], [0.0, 0.0], 8)
);
export const DEFAULT_AIR_TEMPLATES: Map<string, [number, number][]> = new Map([
["yes", YES],
["?", QUESTION],
["hi", HI],
["help", HELP],
["done", DONE],
["more", MORE],
["water", WATER],
["stop", STOP],
]);