From 8fb4ce8be73cf7828c50762c7f3e5681cc5ad151 Mon Sep 17 00:00:00 2001 From: iamdoubz <> Date: Mon, 29 Jun 2026 13:48:06 -0500 Subject: [PATCH] Emit stroked versions of the contours for ai images --- src/lib/createdLessons.ts | 34 +++++++++++++++++++++++++--------- src/lib/vectorize.ts | 32 ++++++++++++++++++++++++++++---- 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/src/lib/createdLessons.ts b/src/lib/createdLessons.ts index d65000a..3c24662 100644 --- a/src/lib/createdLessons.ts +++ b/src/lib/createdLessons.ts @@ -158,16 +158,28 @@ export async function processCreation(id: number): Promise { } } -/** The stored line art: { outline, details, full }. Tolerates legacy rows that stored a bare SVG string. */ -function lessonArt(row: CreatedLessonRow): { outline: string; details: string; full: string } { +interface StoredArt { outline: string; details: string; full: string; outlineStrokes: string[]; detailStrokes: string[] } + +/** The stored line art. Tolerates legacy rows (bare SVG string, or pre-stroke JSON) by falling back. */ +function lessonArt(row: CreatedLessonRow): StoredArt { const raw = row.template_svg || ""; try { const a = JSON.parse(raw); - if (a && typeof a === "object" && "full" in a) return { outline: a.outline || a.full || "", details: a.details || "", full: a.full || "" }; + if (a && typeof a === "object" && "full" in a) { + const outline = a.outline || a.full || ""; + return { + outline, + details: a.details || "", + full: a.full || "", + // Legacy rows lack stroke arrays → fall back to the filled path (fades in rather than draws). + outlineStrokes: Array.isArray(a.outlineStrokes) && a.outlineStrokes.length ? a.outlineStrokes : outline ? [outline] : [], + detailStrokes: Array.isArray(a.detailStrokes) ? a.detailStrokes : [], + }; + } } catch { /* legacy: a plain SVG string */ } - return { outline: raw, details: "", full: raw }; + return { outline: raw, details: "", full: raw, outlineStrokes: raw ? [raw] : [], detailStrokes: [] }; } /** @@ -177,7 +189,7 @@ function lessonArt(row: CreatedLessonRow): { outline: string; details: string; f */ export function buildCreatedLessons(row: CreatedLessonRow): Lesson[] { const base = 900000 + row.id * 10; - const { outline, details, full } = lessonArt(row); + const { outline, full, outlineStrokes, detailStrokes } = lessonArt(row); const lower = row.subject.toLowerCase(); const common = { level: row.level, @@ -189,6 +201,10 @@ export function buildCreatedLessons(row: CreatedLessonRow): Lesson[] { badgeName: `${row.subject} Artist`, badgeKey: "", }; + // Each stroked chunk is one step, so the pen draws the shape a few strokes at a time. + const toSteps = (strokes: string[], label: string) => + strokes.map((el, i) => ({ n: i + 1, title: `${label} ${i + 1}`, instruction: "Watch the line, then trace it!", tip: "", lines: [el] })); + const lessons: Lesson[] = []; lessons.push({ ...common, @@ -199,9 +215,9 @@ export function buildCreatedLessons(row: CreatedLessonRow): Lesson[] { intro: `Trace the outline of your ${lower}, one line at a time.`, phase: "outline", baseSvg: "", - steps: [{ n: 1, title: "The outline", instruction: `Trace the main outline of your ${lower}.`, tip: "", lines: [outline] }], + steps: toSteps(outlineStrokes, "Outline"), }); - if (details) { + if (detailStrokes.length > 0) { lessons.push({ ...common, sublevel: base + 2, @@ -210,8 +226,8 @@ export function buildCreatedLessons(row: CreatedLessonRow): Lesson[] { subject: `${lower} details`, intro: `Now add the details inside your ${lower}.`, phase: "detail", - baseSvg: outline, // faint outline guide under the new detail lines - steps: [{ n: 1, title: "The details", instruction: "Add the inside details.", tip: "", lines: [details] }], + baseSvg: outline, // faint filled outline as a reference under the new detail lines + steps: toSteps(detailStrokes, "Detail"), }); } lessons.push({ diff --git a/src/lib/vectorize.ts b/src/lib/vectorize.ts index 31a3b83..596bdc9 100644 --- a/src/lib/vectorize.ts +++ b/src/lib/vectorize.ts @@ -48,9 +48,12 @@ function round(n: number): number { } export interface LessonArt { - outline: string; // one — the main outline (big shapes) - details: string; // one — interior details (may be "") - full: string; // one — the whole line art (color template) + outline: string; // one filled — the main outline (big shapes), for guides/reference + details: string; // one filled — interior details (may be "") + full: string; // one filled — the whole line art (color template) + // Stroked, chunked versions for the "watch it drawn" animation (stroke-dashoffset pen effect). + outlineStrokes: string[]; + detailStrokes: string[]; } export async function pngToLessonArt(png: Buffer): Promise { @@ -70,6 +73,21 @@ export async function pngToLessonArt(png: Buffer): Promise { const wrap = (subs: string[]) => subs.length ? `` : ""; + // Stroke width chosen so it renders ~4px on the 400×300 canvas after the transform scale. + const sw = Math.max(2, round(4 / s)); + // Split contours into a few stroked elements so the pen draws the shape in a handful of strokes. + const chunkStroke = (subs: string[], maxSteps: number): string[] => { + if (subs.length === 0) return []; + const n = Math.min(maxSteps, subs.length); + const per = Math.ceil(subs.length / n); + const out: string[] = []; + for (let i = 0; i < subs.length; i += per) { + const d = subs.slice(i, i + per).join(" "); + out.push(``); + } + return out; + }; + let outlineSubs = outline.subs; const outlineSet = new Set(outlineSubs); let detailSubs = full.subs.filter((d) => !outlineSet.has(d)); @@ -80,5 +98,11 @@ export async function pngToLessonArt(png: Buffer): Promise { detailSubs = []; } - return { outline: wrap(outlineSubs), details: wrap(detailSubs), full: wrap(full.subs) }; + return { + outline: wrap(outlineSubs), + details: wrap(detailSubs), + full: wrap(full.subs), + outlineStrokes: chunkStroke(outlineSubs, 4), + detailStrokes: chunkStroke(detailSubs, 3), + }; } -- 2.34.1