Canvases that remember
Persist a canvas run with sprout.state so the kid resumes where they left off, and carry a durable checkpoint across days with sprout.journey. Merge-defaults authoring, sprout.resumed, sprout.log, HARD rules.
Canvas Memory lets a canvas remember where the kid was. Write the run's durable state into sprout.state and the SDK auto-persists it as the kid works , no save button, assignment IS the save. When the kid closes the app mid-activity and reopens the canvas, their progress is already there , they continue where they left off.
How it works
On reopen the host seeds sprout.state with the saved snapshot before your canvas code runs, so it is already correct at your first line , no async wait, no polling. sprout.resumed is a boolean, accurate at that first line, telling you whether this run is a resume.
Author with merge-defaults
Read sprout.state, default-fill the fields that are missing with ??=, then mutate. Never replace the whole object , assigning sprout.state = {…} wipes a resumed run.
const S = sprout.state; // already the saved snapshot on resume, {} on a fresh start
S.step ??= 0; // default-fill ONLY what's missing , never overwrite
S.answers ??= {};
// …now mutate freely; every change auto-persists.
S.answers.q1 = "blue";
S.step = 1;Fresh vs resume with sprout.resumed
Branch on sprout.resumed for the genuine fresh-vs-resume cases: welcome-back vs intro, a first-time bonus, an opening animation, schema migration (key a _v version field and migrate from the resumed snapshot), or analytics.
if (sprout.resumed) {
goToStep(sprout.state.step); // returning mid-run , rebuild UI from restored state
} else {
showIntro(); // brand-new run , intro / tutorial / first-time bonus
}Rules
- Merge-defaults, never replace. Default-fill missing fields (
sprout.state.x ??= default) and mutate. A wholesalesprout.state = {…}clobbers a resumed run. - Durable state only. Answers, current step, progress, score-so-far. Keep volatile / derived / animation state OUT (cursor, tween frames, hover) , every write persists, so volatile writes bloat the run.
- JSON-serializable values only. No functions, DOM nodes,
Date,Map,Set, they're stripped on save. - No PII / identifiers in
sprout.state. Activity data only. - Always wire it , every canvas. A canvas that ignores
sprout.staterestarts the kid from scratch on every reopen, which we never want.
sprout.restore() is a deprecated back-compat alias (returns the snapshot when resuming, else null) , prefer sprout.resumed. sprout.save() is a manual-flush escape hatch you rarely need , assignment already persists.Across days: sprout.journey
sprout.state resumes the current run. For memory that survives across runs and across days: a level, unlocked words, a mastery map: use sprout.journey. The kid finishes Tuesday on level 3; when the task offers a new play on Wednesday, the canvas reads the journey and picks up at level 3.
// Read where this kid is. ALWAYS default-fill: get() resolves {} on a
// first run AND when the family hasn't enabled durable state.
const j = await sprout.journey.get();
const level = j.level ?? 1;
const stars = j.stars ?? 0;
// …the kid plays, clears level 3…
// Save the WHOLE checkpoint (replace-only, ≤ 64 KB). Never rejects:
// branch on the result. Save BEFORE sprout.complete().
const res = await sprout.journey.save({ level: 4, stars: 12 });
if (!res.ok) {
// res.error: 'too-large' | 'disabled' | 'not-task-linked'.
// Degrade gracefully: the canvas still works, just without cross-day memory.
}Journey rules: replace-only (save overwrites the whole blob, keep it a small "where are they" object), save before you complete (so the checkpoint is durable before the run freezes), and history does not go here: a growing list of past attempts belongs in sprout.log.
The record: sprout.log
sprout.log(entry) appends one entry to the run's record: what happened, for a parent or the system to review later. Fire-and-forget; the host batches and caps it, so a chatty canvas is bounded automatically.
sprout.log("level 3 cleared");
sprout.log({ event: "hint_used", card: 7 });The three-line model: sprout.state = this sitting · sprout.journey = this kid's journey (durable, cross-day) · sprout.log = what happened, for the record. Full contract on the Canvas SDK reference.
Related reading
Interactive canvas Canvas Canvas SDK