/* * ANIMATION STORYBOARD * * 0ms old course run starts, all weeks reset * 260ms next week activates, student log receives one consequence * 1200ms active week reaches exam signal, learner state updates * 2400ms run ends with diagnosis visible in course file */ const TIMING = { oldCourseStep: 260, feedbackDelay: 900 }; const $ = (selector, root = document) => root.querySelector(selector); const $$ = (selector, root = document) => Array.from(root.querySelectorAll(selector)); const course = { outcome: null, practice: null, assessment: null, feedback: null, opening: null, classMoves: {}, memory: new Set() }; const labels = { outcome: { vague: "understand trees, graphs, and hashing", usable: "choose and justify a data structure for a stated constraint", tooBig: "design industrial-scale storage systems from scratch" }, practice: { lecture: "lecture examples only", compare: "compare structures for a constraint", debug: "debug a bad choice in pairs" }, assessment: { definitions: "definition-heavy exam", justify: "scenario justification task", project: "mini-project with design memo" }, feedback: { late: "after the midterm", quick: "same-class comparison feedback", revision: "revision attempt after feedback" }, opening: { rules: "grading rules first", mystery: "real design failure first", support: "outcome, practice, and help route first" }, move: { hook: "opening problem", predict: "prediction", pair: "pair talk", quiz: "quick quiz", paper: "minute paper" } }; function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); } function setText(selector, text) { const node = typeof selector === "string" ? $(selector) : selector; if (node) node.textContent = text; } function initProgress() { const bar = $(".progress-line span"); const update = () => { const max = document.documentElement.scrollHeight - window.innerHeight; const percent = max <= 0 ? 0 : (window.scrollY / max) * 100; bar.style.width = `${clamp(percent, 0, 100)}%`; }; update(); window.addEventListener("scroll", update, { passive: true }); } function initNav() { const links = $$(".topbar nav a"); const byId = new Map(links.map((link) => [link.getAttribute("href").slice(1), link])); const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (!entry.isIntersecting) return; links.forEach((link) => link.classList.remove("active")); const link = byId.get(entry.target.id); if (link) link.classList.add("active"); }); }, { rootMargin: "-36% 0px -54% 0px", threshold: 0.01 } ); links.forEach((link) => { const section = $(link.getAttribute("href")); if (section) observer.observe(section); }); } function initWeeks() { const track = $("#weekTrack"); if (!track) return; track.replaceChildren(); for (let i = 1; i <= 12; i += 1) { const week = document.createElement("div"); week.className = "week"; week.textContent = `W${i}`; track.append(week); } } function runOldCourse() { const weeks = $$(".week"); const log = $("#studentLog"); const entries = [ "Week 2: Maya copies the list implementation. It works, but she cannot explain the design choice.", "Week 5: Trees arrive. The course has moved on before her earlier misconception is visible.", "Week 8: The midterm asks definitions. Her effort shifts toward remembering names.", "Week 12: She passes many recall items. Transfer is still fragile." ]; weeks.forEach((week) => week.classList.remove("active")); log.replaceChildren(); setText(log.appendChild(document.createElement("p")), "The run starts from the topic list."); let i = 0; const timer = window.setInterval(() => { if (i > 0) weeks[i - 1]?.classList.remove("active"); if (i >= weeks.length) { window.clearInterval(timer); updateLearnerNotes("diagnosed"); updateCourseFile(); return; } weeks[i].classList.add("active"); if ([1, 4, 7, 11].includes(i)) { const p = document.createElement("p"); p.textContent = entries.shift(); log.append(p); } i += 1; }, TIMING.oldCourseStep); } function outcomeFeedback(kind) { const node = $("#outcomeFeedback"); const messages = { vague: [ "This is a topic in disguise.", "It does not tell you what students must produce, so assessment will drift." ], usable: [ "This can design the course.", "It names a student action, a judgment, and a realistic course-level capability." ], tooBig: [ "This overpromises.", "Ambition helps only when students can practice the component skills inside the course." ] }; const [title, detail] = messages[kind]; node.innerHTML = `${title}${detail}`; } function initOutcomes() { $$("#outcomeChoices .choice-card").forEach((button) => { button.addEventListener("click", () => { $$("#outcomeChoices .choice-card").forEach((item) => item.classList.toggle("selected", item === button)); course.outcome = button.dataset.outcome; outcomeFeedback(course.outcome); updateLearnerNotes("outcome"); updateCourseFile(); updateAlignmentMap(); updatePlan(); }); }); } function initAlignmentControls() { $$("#practiceChoices button").forEach((button) => { button.addEventListener("click", () => { $$("#practiceChoices button").forEach((item) => item.classList.toggle("selected", item === button)); course.practice = button.dataset.practice; updateAlignmentMap(); updateLearnerNotes("alignment"); updateCourseFile(); updatePlan(); }); }); $$("#assessmentChoices button").forEach((button) => { button.addEventListener("click", () => { $$("#assessmentChoices button").forEach((item) => item.classList.toggle("selected", item === button)); course.assessment = button.dataset.assessment; updateAlignmentMap(); updateLearnerNotes("alignment"); updateCourseFile(); updatePlan(); }); }); } function updateAlignmentMap() { const outcome = course.outcome ? labels.outcome[course.outcome] : "not written"; const practice = course.practice ? labels.practice[course.practice] : "listening"; const assessment = course.assessment ? labels.assessment[course.assessment] : "recall exam"; const feedback = course.feedback ? labels.feedback[course.feedback] : "after exam"; setText("#mapOutcome", shorten(outcome, 24)); setText("#mapPractice", shorten(practice, 24)); setText("#mapAssessment", shorten(assessment, 24)); setText("#mapFeedback", shorten(feedback, 24)); const goodOutcome = course.outcome === "usable"; const goodPractice = course.practice === "compare" || course.practice === "debug"; const goodAssessment = course.assessment === "justify" || course.assessment === "project"; const goodFeedback = course.feedback === "quick" || course.feedback === "revision"; $(".wire-a")?.classList.toggle("strong", goodOutcome); $(".wire-b")?.classList.toggle("strong", goodPractice); $(".wire-c")?.classList.toggle("strong", goodAssessment); $(".wire-d")?.classList.toggle("strong", goodFeedback); $(".wire-c")?.classList.toggle("warn", course.assessment === "definitions"); const hidden = $("#hiddenCourse"); if (!course.assessment || course.assessment === "definitions") { hidden.classList.remove("good"); hidden.innerHTML = "The hidden course is still recall.Students will protect marks by memorizing definitions."; } else if (goodOutcome && goodPractice && goodAssessment) { hidden.classList.add("good"); hidden.innerHTML = "The hidden course now matches the stated course.Students can see that practice and marks both point toward justified choice."; } else { hidden.classList.remove("good"); hidden.innerHTML = "The signals are mixed.One corner has improved, but students still receive competing messages about effort."; } } function shorten(text, limit) { return text.length > limit ? `${text.slice(0, limit - 3)}...` : text; } function initClassBuilder() { const palette = $$("#movePalette button"); const slotRow = $("#slotRow"); let selectedMove = null; palette.forEach((button) => { button.addEventListener("click", () => { selectedMove = button.dataset.move; palette.forEach((item) => item.classList.toggle("selected", item === button)); }); }); slotRow.replaceChildren(); [0, 10, 20, 30, 40, 50].forEach((minute) => { const slot = document.createElement("button"); slot.type = "button"; slot.className = "slot"; slot.dataset.minute = String(minute); slot.textContent = `${minute} min`; slot.addEventListener("click", () => { if (!selectedMove) { setText("#classFeedback", "Choose a move first, then place it in the hour."); return; } course.classMoves[minute] = selectedMove; slot.classList.add("filled"); slot.textContent = `${minute}: ${labels.move[selectedMove]}`; updateAttention(); updateLearnerNotes("class"); updateCourseFile(); updatePlan(); }); slotRow.append(slot); }); updateAttention(); } function attentionAt(minute) { const placed = Object.entries(course.classMoves) .map(([key, value]) => ({ minute: Number(key), move: value })) .sort((a, b) => a.minute - b.minute); const previous = placed.filter((item) => item.minute <= minute).pop(); const since = previous ? minute - previous.minute : minute; const reset = previous && since < 5 ? 11 : 0; const endLift = minute > 54 ? (minute - 54) * 2 : 0; return clamp(86 - since * 2.15 + reset + endLift, 26, 94); } function updateAttention() { const path = $("#attentionCurve"); const markers = $("#attentionMarkers"); if (!path || !markers) return; const points = []; for (let minute = 0; minute <= 60; minute += 2) { const x = 42 + (minute / 60) * 700; const y = 232 - (attentionAt(minute) / 100) * 190; points.push(`${x},${y}`); } path.setAttribute("d", `M ${points.join(" L ")}`); markers.replaceChildren(); Object.entries(course.classMoves).forEach(([minute, move]) => { const x = 42 + (Number(minute) / 60) * 700; const y = 232 - (attentionAt(Number(minute)) / 100) * 190; const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle"); circle.setAttribute("cx", x); circle.setAttribute("cy", y); circle.setAttribute("r", 8); const text = document.createElementNS("http://www.w3.org/2000/svg", "text"); text.setAttribute("x", x); text.setAttribute("y", y - 15); text.setAttribute("text-anchor", "middle"); text.textContent = labels.move[move]; markers.append(circle, text); }); const moves = Object.values(course.classMoves); const hasHook = moves.includes("hook"); const thinkingMoves = moves.filter((move) => ["predict", "pair", "quiz", "paper"].includes(move)).length; if (hasHook && thinkingMoves >= 2) { setText("#classFeedback", "This hour now asks students to predict, explain, or retrieve before the idea goes cold."); } else if (thinkingMoves >= 1) { setText("#classFeedback", "Good start. Add one more thinking move so the middle of the hour does not sag."); } else { setText("#classFeedback", "Place an opening problem and two short thinking moves."); } } function initFeedbackLab() { const bars = $("#persistenceBars"); bars.replaceChildren(); ["same day", "one week", "midterm"].forEach((label) => { const row = document.createElement("div"); row.className = "persistence-row"; row.innerHTML = `${label}
`; bars.append(row); }); updatePersistence("late"); $$(".feedback-choices button").forEach((button) => { button.addEventListener("click", () => { $$(".feedback-choices button").forEach((item) => item.classList.toggle("selected", item === button)); course.feedback = button.dataset.feedback; updatePersistence(course.feedback); updateAlignmentMap(); updateLearnerNotes("feedback"); updateCourseFile(); updatePlan(); }); }); } function updatePersistence(kind) { const levels = { late: [80, 72, 58], quick: [58, 30, 18], revision: [62, 24, 10] }; $$(".bar-shell span").forEach((bar, index) => { bar.style.setProperty("--amount", `${levels[kind][index]}%`); bar.style.background = kind === "late" ? "var(--coral)" : "var(--teal)"; }); } function initOpening() { $$("#openingChoices button").forEach((button) => { button.addEventListener("click", () => { $$("#openingChoices button").forEach((item) => item.classList.toggle("selected", item === button)); course.opening = button.dataset.opening; const response = $("#motivationResponse"); if (course.opening === "rules") { response.classList.remove("good"); response.innerHTML = "Maya hears that marks matter.Value, expectancy, and support are still mostly implicit."; } else if (course.opening === "mystery") { response.classList.add("good"); response.innerHTML = "Maya sees value first.The opening gives the outcome a reason to exist before naming the machinery."; } else { response.classList.add("good"); response.innerHTML = "Maya sees the route.The opening joins value with expectancy: here is the goal, the practice path, and where help lives."; } updateLearnerNotes("motivation"); updateCourseFile(); updatePlan(); }); }); } function initMemoryChecks() { $$(".memory-check").forEach((check) => { $$("button", check).forEach((button) => { button.addEventListener("click", () => { $$("button", check).forEach((item) => item.classList.remove("correct", "incorrect")); const isRight = button.dataset.answer === "right"; button.classList.add(isRight ? "correct" : "incorrect"); const feedback = $(".check-feedback", check); if (isRight) { course.memory.add(check.dataset.check); feedback.textContent = "Settled. This will show up in the course file."; } else { feedback.textContent = "Not yet. Try the option that changes what the teacher designs next."; } updateCourseFile(); }); }); }); } function updateLearnerNotes(stage) { const notes = { diagnosed: [ "She can repeat definitions after lecture.", "She waits until the exam to find out what counts.", "She cannot yet transfer the ideas to a new problem." ], outcome: [ course.outcome === "usable" ? "She can see the course is about justified choices, not names alone." : "The destination is still hard to recognize from a student's seat.", "She needs practice that matches the promise.", "The exam will decide what she actually studies." ], alignment: [ course.practice === "compare" || course.practice === "debug" ? "She has a place to practice choosing, not just hearing." : "She is still mostly watching someone else reason.", course.assessment === "justify" || course.assessment === "project" ? "Marks now reward the capability the outcome names." : "The visible reward still points toward recall.", "Alignment is becoming a message she can read." ], class: [ Object.values(course.classMoves).length >= 3 ? "She has to predict, explain, and retrieve during class." : "The class has started to ask for thinking, but the rhythm is still thin.", "Short tasks make confusion visible before the exam.", "Attention is being used as a design material." ], feedback: [ course.feedback === "late" ? "Her misconception survives until the midterm." : "Her misconception gets interrupted while it is still fixable.", "Feedback now changes the next attempt, not just the grade record.", "Formative work has become part of teaching." ], motivation: [ course.opening === "rules" ? "She hears the grading system first." : "She sees why the outcome matters before the syllabus details.", course.opening === "support" ? "She also sees how effort can succeed." : "Support still needs to be named clearly.", "Motivation is being designed, not wished for." ] }; const list = $("#learnerNotes"); list.replaceChildren(); notes[stage].forEach((note) => { const item = document.createElement("li"); item.textContent = note; list.append(item); }); } function designLevels() { let use = 22; let confidence = 18; let transfer = 14; if (course.outcome === "usable") { use += 18; transfer += 16; } else if (course.outcome) { use += 6; } if (course.practice === "compare" || course.practice === "debug") { use += 16; transfer += 14; } if (course.assessment === "justify" || course.assessment === "project") { use += 16; confidence += 14; transfer += 12; } if (Object.values(course.classMoves).length >= 3) { confidence += 14; use += 10; } if (course.feedback === "quick" || course.feedback === "revision") { confidence += 18; transfer += 10; } if (course.opening === "mystery" || course.opening === "support") { confidence += 10; use += 8; } return { use: clamp(use, 8, 96), confidence: clamp(confidence, 8, 96), transfer: clamp(transfer, 8, 96) }; } function updateCourseFile() { setText("#spineOutcome", course.outcome ? labels.outcome[course.outcome] : "not yet written"); setText("#spinePractice", course.practice ? labels.practice[course.practice] : "mostly listening"); setText("#spineAssessment", course.assessment ? labels.assessment[course.assessment] : "midterm plus final"); setText("#spineFeedback", course.feedback ? labels.feedback[course.feedback] : "late and scarce"); setText("#spineMotivation", course.opening ? labels.opening[course.opening] : "grades carry the message"); const levels = designLevels(); $("#meterUse").style.setProperty("--fill", `${levels.use}%`); $("#meterConfidence").style.setProperty("--fill", `${levels.confidence}%`); $("#meterTransfer").style.setProperty("--fill", `${levels.transfer}%`); const memory = $("#memoryTrace"); memory.replaceChildren(); if (course.memory.size === 0) { const span = document.createElement("span"); span.textContent = "0 checks settled"; memory.append(span); } else { course.memory.forEach((check) => { const span = document.createElement("span"); span.className = "settled"; span.textContent = check; memory.append(span); }); } const aligned = course.outcome === "usable" && (course.practice === "compare" || course.practice === "debug") && (course.assessment === "justify" || course.assessment === "project") && (course.feedback === "quick" || course.feedback === "revision"); if (aligned) { setText("#fileStatus", "The course now gives students one coherent message."); } else if (course.outcome || course.practice || course.assessment || course.feedback || course.opening) { setText("#fileStatus", "The course is improving, but at least one signal still needs work."); } else { setText("#fileStatus", "Topic coverage is still driving the course."); } } function updatePlan() { const moves = Object.entries(course.classMoves) .sort((a, b) => Number(a[0]) - Number(b[0])) .map(([minute, move]) => `${minute} min: ${labels.move[move]}`); const plan = `Data Structures I - first class redesign Learning outcome ${course.outcome ? labels.outcome[course.outcome] : "Write one observable course outcome before class."} Opening ${course.opening ? labels.opening[course.opening] : "Open by showing why the outcome matters to a real design problem."} In-class practice ${moves.length ? moves.join("\n") : "0 min: opening problem\n15 min: prediction\n30 min: think-pair-share\n45 min: one-minute quiz"} Assessment signal ${course.assessment ? labels.assessment[course.assessment] : "Use a scenario task that asks students to justify a structure."} Feedback route ${course.feedback ? labels.feedback[course.feedback] : "Give quick formative feedback before the first high-stakes exam."} Teacher check Students should leave knowing what they are trying to learn, how they will practice it, how it will be assessed, and where to get help.`; setText("#finalPlan", plan); } function initCopy() { $("#copyPlan")?.addEventListener("click", async () => { try { await navigator.clipboard.writeText($("#finalPlan").textContent); setText("#copyStatus", "Copied."); } catch { const range = document.createRange(); range.selectNodeContents($("#finalPlan")); const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); setText("#copyStatus", "Selected the plan text."); } }); } function initReset() { $$("[data-action='reset']").forEach((button) => { button.addEventListener("click", () => { course.outcome = null; course.practice = null; course.assessment = null; course.feedback = null; course.opening = null; course.classMoves = {}; course.memory.clear(); $$(".selected, .correct, .incorrect, .filled").forEach((node) => node.classList.remove("selected", "correct", "incorrect", "filled")); $$(".slot").forEach((slot) => { slot.textContent = `${slot.dataset.minute} min`; }); updateLearnerNotes("diagnosed"); updateAlignmentMap(); updateAttention(); updatePersistence("late"); updateCourseFile(); updatePlan(); setText("#classFeedback", "Place an opening problem and two short thinking moves."); $("#outcomeFeedback").innerHTML = "No outcome chosen yet.The course still has topics, not a destination."; $("#motivationResponse").innerHTML = "Maya is waiting to see what this course values.The opening has not yet told her why effort will be worth it."; $("#hiddenCourse").innerHTML = "The hidden course is still recall.Students will protect marks by memorizing definitions."; }); }); } document.addEventListener("DOMContentLoaded", () => { initProgress(); initNav(); initWeeks(); initOutcomes(); initAlignmentControls(); initClassBuilder(); initFeedbackLab(); initOpening(); initMemoryChecks(); initCopy(); initReset(); $("#runOldCourse")?.addEventListener("click", runOldCourse); updateLearnerNotes("diagnosed"); updateAlignmentMap(); updateCourseFile(); updatePlan(); });