[BUG] Emit stroked versions of the contours for ai images #17
@@ -158,16 +158,28 @@ export async function processCreation(id: number): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/** 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({
|
||||
|
||||
+28
-4
@@ -48,9 +48,12 @@ function round(n: number): number {
|
||||
}
|
||||
|
||||
export interface LessonArt {
|
||||
outline: string; // one <path> — the main outline (big shapes)
|
||||
details: string; // one <path> — interior details (may be "")
|
||||
full: string; // one <path> — the whole line art (color template)
|
||||
outline: string; // one filled <path> — the main outline (big shapes), for guides/reference
|
||||
details: string; // one filled <path> — interior details (may be "")
|
||||
full: string; // one filled <path> — 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<LessonArt> {
|
||||
@@ -70,6 +73,21 @@ export async function pngToLessonArt(png: Buffer): Promise<LessonArt> {
|
||||
const wrap = (subs: string[]) =>
|
||||
subs.length ? `<path d="${subs.join(" ")}" transform="${transform}" fill="${STROKE}" fill-rule="evenodd"/>` : "";
|
||||
|
||||
// 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(`<path d="${d}" transform="${transform}" fill="none" stroke="${STROKE}" stroke-width="${sw}" stroke-linecap="round" stroke-linejoin="round"/>`);
|
||||
}
|
||||
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<LessonArt> {
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user