feat(worksheets): implement unique place value colors for 1-6 digit problems
Added 6 unique pastel colors (ones through hundred-thousands) with dynamic color lookup using array indexing instead of hardcoded ternaries. Colors: - Ones: Light blue (rgb(227, 242, 253)) - Tens: Light green (rgb(232, 245, 233)) - Hundreds: Light yellow (rgb(255, 249, 196)) - Thousands: Light pink/rose (rgb(255, 228, 225)) - Ten-thousands: Light purple/lavender (rgb(243, 229, 245)) - Hundred-thousands: Light peach/orange (rgb(255, 239, 213)) All place value rendering (carry boxes, addends, answer boxes, ten-frames) now uses place-colors.at(i) for dynamic lookup. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
d9b54a736c
commit
65e272c570
|
|
@ -18,10 +18,13 @@ export interface DisplayOptions {
|
|||
*/
|
||||
export function generateTypstHelpers(cellSize: number): string {
|
||||
return String.raw`
|
||||
// Place value colors (light pastels)
|
||||
#let color-ones = rgb(227, 242, 253) // Light blue
|
||||
#let color-tens = rgb(232, 245, 233) // Light green
|
||||
#let color-hundreds = rgb(255, 249, 196) // Light yellow
|
||||
// Place value colors (light pastels) - unique color per place value
|
||||
#let color-ones = rgb(227, 242, 253) // Light blue (ones)
|
||||
#let color-tens = rgb(232, 245, 233) // Light green (tens)
|
||||
#let color-hundreds = rgb(255, 249, 196) // Light yellow (hundreds)
|
||||
#let color-thousands = rgb(255, 228, 225) // Light pink/rose (thousands)
|
||||
#let color-ten-thousands = rgb(243, 229, 245) // Light purple/lavender (ten-thousands)
|
||||
#let color-hundred-thousands = rgb(255, 239, 213) // Light peach/orange (hundred-thousands)
|
||||
#let color-none = white // No color
|
||||
|
||||
// Ten-frame helper - stacked 2 frames vertically, sized to fit cell width
|
||||
|
|
@ -93,92 +96,201 @@ export function generateTypstHelpers(cellSize: number): string {
|
|||
* Generate Typst function for rendering problem stack/grid
|
||||
* This is the SINGLE SOURCE OF TRUTH for problem rendering layout
|
||||
* Used by both full worksheets and preview examples
|
||||
*
|
||||
* @param cellSize Size of each digit cell in inches
|
||||
* @param maxDigits Maximum number of digits in any problem on this page (1-6)
|
||||
*/
|
||||
export function generateProblemStackFunction(cellSize: number): string {
|
||||
export function generateProblemStackFunction(cellSize: number, maxDigits: number = 3): string {
|
||||
const cellSizeIn = `${cellSize}in`
|
||||
const cellSizePt = cellSize * 72
|
||||
|
||||
// Generate place value color assignments (unique color per place value)
|
||||
// Index 0 = ones, 1 = tens, 2 = hundreds, 3 = thousands, 4 = ten-thousands, 5 = hundred-thousands
|
||||
const placeColors = [
|
||||
'color-ones', // 0: ones (light blue)
|
||||
'color-tens', // 1: tens (light green)
|
||||
'color-hundreds', // 2: hundreds (light yellow)
|
||||
'color-thousands', // 3: thousands (light pink/rose)
|
||||
'color-ten-thousands', // 4: ten-thousands (light purple/lavender)
|
||||
'color-hundred-thousands', // 5: hundred-thousands (light peach/orange)
|
||||
]
|
||||
|
||||
return String.raw`
|
||||
// Problem rendering function for addition worksheets
|
||||
// Returns the stack/grid structure for rendering a single 2-digit addition problem
|
||||
// Problem rendering function for addition worksheets (supports 1-${maxDigits} digit problems)
|
||||
// Returns the stack/grid structure for rendering a single addition problem
|
||||
// Per-problem display flags: show-carries, show-answers, show-colors, show-ten-frames, show-numbers
|
||||
#let problem-stack(a, b, aT, aO, bT, bO, index-or-none, show-carries, show-answers, show-colors, show-ten-frames, show-numbers) = {
|
||||
#let problem-stack(a, b, index-or-none, show-carries, show-answers, show-colors, show-ten-frames, show-numbers) = {
|
||||
// Place value colors array for dynamic lookup (index 0 = ones, 1 = tens, ...)
|
||||
let place-colors = (${placeColors.join(', ')})
|
||||
|
||||
// Extract digits dynamically based on problem size
|
||||
let max-digits = ${maxDigits}
|
||||
// Allow one extra digit for potential overflow (e.g., 99999 + 99999 = 199998)
|
||||
let max-extraction = max-digits + 1
|
||||
let a-digits = ()
|
||||
let b-digits = ()
|
||||
let temp-a = a
|
||||
let temp-b = b
|
||||
|
||||
// Extract digits from right to left (ones, tens, hundreds, ...)
|
||||
for i in range(0, max-extraction) {
|
||||
a-digits.push(calc.rem(temp-a, 10))
|
||||
b-digits.push(calc.rem(temp-b, 10))
|
||||
temp-a = calc.floor(temp-a / 10)
|
||||
temp-b = calc.floor(temp-b / 10)
|
||||
}
|
||||
|
||||
// Find highest non-zero digit position for each number (for leading zero detection)
|
||||
let a-highest = 0
|
||||
let b-highest = 0
|
||||
for i in range(0, max-extraction) {
|
||||
if a-digits.at(i) > 0 { a-highest = i }
|
||||
if b-digits.at(i) > 0 { b-highest = i }
|
||||
}
|
||||
|
||||
// Actual number of digits in this specific problem (not the page max)
|
||||
// Must consider the sum, which may have one more digit than the addends (e.g., 99 + 1 = 100)
|
||||
let sum = a + b
|
||||
let sum-highest = 0
|
||||
let temp-sum = sum
|
||||
for i in range(0, max-extraction) {
|
||||
if calc.rem(temp-sum, 10) > 0 { sum-highest = i }
|
||||
temp-sum = calc.floor(temp-sum / 10)
|
||||
}
|
||||
let actual-digits = calc.max(calc.max(a-highest, b-highest), sum-highest) + 1
|
||||
|
||||
// Generate column list dynamically based on actual digits in this problem
|
||||
// Column list: [+sign column (0.5em), ...actual-digits × cellSizeIn]
|
||||
let column-list = (0.5em,)
|
||||
for i in range(0, actual-digits) {
|
||||
column-list.push(${cellSizeIn})
|
||||
}
|
||||
|
||||
// Show problem number (only if problem numbers are enabled)
|
||||
let problem-number-display = if show-numbers and index-or-none != none {
|
||||
align(top + left)[
|
||||
#box(inset: (left: 0.08in, top: 0.05in))[
|
||||
#text(size: ${(cellSizePt * 0.6).toFixed(1)}pt, weight: "bold", font: "New Computer Modern Math")[\##(index-or-none + 1).]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
stack(
|
||||
dir: ttb,
|
||||
spacing: 0pt,
|
||||
if show-numbers and index-or-none != none {
|
||||
align(top + left)[
|
||||
#box(inset: (left: 0.08in, top: 0.05in))[
|
||||
#text(size: ${(cellSizePt * 0.6).toFixed(1)}pt, weight: "bold", font: "New Computer Modern Math")[\##(index-or-none + 1).]
|
||||
]
|
||||
]
|
||||
},
|
||||
problem-number-display,
|
||||
grid(
|
||||
columns: (0.5em, ${cellSizeIn}, ${cellSizeIn}, ${cellSizeIn}),
|
||||
columns: column-list,
|
||||
gutter: 0pt,
|
||||
|
||||
[],
|
||||
// Hundreds carry box: shows carry FROM tens (green) TO hundreds (yellow)
|
||||
if show-carries {
|
||||
if show-colors {
|
||||
diagonal-split-box(${cellSizeIn}, color-tens, color-hundreds)
|
||||
} else {
|
||||
box(width: ${cellSizeIn}, height: ${cellSizeIn}, stroke: 0.5pt)[]
|
||||
}
|
||||
} else { v(${cellSizeIn}) },
|
||||
// Tens carry box: shows carry FROM ones (blue) TO tens (green)
|
||||
if show-carries {
|
||||
if show-colors {
|
||||
diagonal-split-box(${cellSizeIn}, color-ones, color-tens)
|
||||
} else {
|
||||
box(width: ${cellSizeIn}, height: ${cellSizeIn}, stroke: 0.5pt)[]
|
||||
}
|
||||
} else { v(${cellSizeIn}) },
|
||||
[],
|
||||
// Carry boxes row (one per place value, right to left)
|
||||
[], // Empty cell for + sign column
|
||||
..for i in range(0, actual-digits).rev() {
|
||||
// DEBUG: Show which place values get carry boxes and why
|
||||
let show-carry = show-carries and i > 0
|
||||
|
||||
[],
|
||||
[],
|
||||
box(width: ${cellSizeIn}, height: ${cellSizeIn}, fill: if show-colors { color-tens } else { color-none })[#align(center + horizon)[#text(size: ${(cellSizePt * 0.8).toFixed(1)}pt)[#str(aT)]]],
|
||||
box(width: ${cellSizeIn}, height: ${cellSizeIn}, fill: if show-colors { color-ones } else { color-none })[#align(center + horizon)[#text(size: ${(cellSizePt * 0.8).toFixed(1)}pt)[#str(aO)]]],
|
||||
if show-carry {
|
||||
// Carry box: shows carry FROM position i-1 TO position i
|
||||
let source-color = place-colors.at(i - 1) // Color of source place value
|
||||
let dest-color = place-colors.at(i) // Color of destination place value
|
||||
|
||||
if show-colors {
|
||||
(box(width: ${cellSizeIn}, height: ${cellSizeIn})[
|
||||
#diagonal-split-box(${cellSizeIn}, source-color, dest-color)
|
||||
],)
|
||||
} else {
|
||||
(box(width: ${cellSizeIn}, height: ${cellSizeIn}, stroke: 0.5pt)[],)
|
||||
}
|
||||
} else {
|
||||
// No carry box for this position (i == 0 or show-carries is false)
|
||||
(box(width: ${cellSizeIn}, height: ${cellSizeIn})[
|
||||
#v(${cellSizeIn})
|
||||
],)
|
||||
}
|
||||
},
|
||||
|
||||
// First addend row (right to left: ones, tens, hundreds, ...)
|
||||
[], // Empty cell for + sign column
|
||||
..for i in range(0, actual-digits).rev() {
|
||||
let digit = a-digits.at(i)
|
||||
let place-color = place-colors.at(i) // Dynamic color lookup by place value
|
||||
let fill-color = if show-colors { place-color } else { color-none }
|
||||
|
||||
// Hide leading zeros (zeros higher than the highest non-zero digit)
|
||||
(box(width: ${cellSizeIn}, height: ${cellSizeIn}, fill: fill-color)[
|
||||
#align(center + horizon)[
|
||||
#if i <= a-highest [
|
||||
#text(size: ${(cellSizePt * 0.8).toFixed(1)}pt)[#str(digit)]
|
||||
] else [
|
||||
#h(0pt)
|
||||
]
|
||||
]
|
||||
],)
|
||||
},
|
||||
|
||||
// Second addend row with + sign (right to left)
|
||||
box(width: ${cellSizeIn}, height: ${cellSizeIn})[#align(center + horizon)[#text(size: ${(cellSizePt * 0.8).toFixed(1)}pt)[+]]],
|
||||
[],
|
||||
box(width: ${cellSizeIn}, height: ${cellSizeIn}, fill: if show-colors { color-tens } else { color-none })[#align(center + horizon)[#text(size: ${(cellSizePt * 0.8).toFixed(1)}pt)[#str(bT)]]],
|
||||
box(width: ${cellSizeIn}, height: ${cellSizeIn}, fill: if show-colors { color-ones } else { color-none })[#align(center + horizon)[#text(size: ${(cellSizePt * 0.8).toFixed(1)}pt)[#str(bO)]]],
|
||||
..for i in range(0, actual-digits).rev() {
|
||||
let digit = b-digits.at(i)
|
||||
let place-color = place-colors.at(i) // Dynamic color lookup by place value
|
||||
let fill-color = if show-colors { place-color } else { color-none }
|
||||
|
||||
// Hide leading zeros (zeros higher than the highest non-zero digit)
|
||||
(box(width: ${cellSizeIn}, height: ${cellSizeIn}, fill: fill-color)[
|
||||
#align(center + horizon)[
|
||||
#if i <= b-highest [
|
||||
#text(size: ${(cellSizePt * 0.8).toFixed(1)}pt)[#str(digit)]
|
||||
] else [
|
||||
#h(0pt)
|
||||
]
|
||||
]
|
||||
],)
|
||||
},
|
||||
|
||||
// Line row
|
||||
[],
|
||||
line(length: ${cellSizeIn}, stroke: heavy-stroke),
|
||||
line(length: ${cellSizeIn}, stroke: heavy-stroke),
|
||||
line(length: ${cellSizeIn}, stroke: heavy-stroke),
|
||||
[], // Empty cell for + sign column
|
||||
..for i in range(0, actual-digits) {
|
||||
(line(length: ${cellSizeIn}, stroke: heavy-stroke),)
|
||||
},
|
||||
|
||||
// Ten-frames row with overlaid line on top
|
||||
// Show ten-frames for any place value that has regrouping (or all if show-ten-frames-for-all)
|
||||
..if show-ten-frames {
|
||||
let carry = if (aO + bO) >= 10 { 1 } else { 0 }
|
||||
let tens-regroup = (aT + bT + carry) >= 10
|
||||
let ones-regroup = (aO + bO) >= 10
|
||||
let needs-ten-frames = show-ten-frames-for-all or tens-regroup or ones-regroup
|
||||
// Check which place values need ten-frames (only for actual digits in this problem)
|
||||
let carry = 0
|
||||
let regrouping-places = ()
|
||||
for i in range(0, actual-digits) {
|
||||
let digit-sum = a-digits.at(i) + b-digits.at(i) + carry
|
||||
if digit-sum >= 10 {
|
||||
regrouping-places.push(i)
|
||||
carry = 1
|
||||
} else {
|
||||
carry = 0
|
||||
}
|
||||
}
|
||||
|
||||
let needs-ten-frames = show-ten-frames-for-all or regrouping-places.len() > 0
|
||||
|
||||
if needs-ten-frames {
|
||||
(
|
||||
[],
|
||||
[], // Empty cell for hundreds column
|
||||
if show-ten-frames-for-all or tens-regroup {
|
||||
box(width: ${cellSizeIn}, height: ${cellSizeIn} * 0.8)[
|
||||
#align(center + top)[#ten-frames-stacked(${cellSizeIn} * 0.90, if show-colors { color-hundreds } else { color-none }, if show-colors { color-tens } else { color-none })]
|
||||
#place(top, line(length: ${cellSizeIn} * 0.90, stroke: heavy-stroke))
|
||||
]
|
||||
h(2.5pt)
|
||||
} else {
|
||||
v(${cellSizeIn} * 0.8)
|
||||
},
|
||||
if show-ten-frames-for-all or ones-regroup {
|
||||
box(width: ${cellSizeIn}, height: ${cellSizeIn} * 0.8)[
|
||||
#align(center + top)[#ten-frames-stacked(${cellSizeIn} * 0.90, if show-colors { color-tens } else { color-none }, if show-colors { color-ones } else { color-none })]
|
||||
#place(top, line(length: ${cellSizeIn} * 0.90, stroke: heavy-stroke))
|
||||
]
|
||||
} else {
|
||||
v(${cellSizeIn} * 0.8)
|
||||
[], // Empty cell for + sign column
|
||||
// Show ten-frames for any place value that needs regrouping
|
||||
..for i in range(0, actual-digits).rev() {
|
||||
let shows-frame = show-ten-frames-for-all or (i in regrouping-places)
|
||||
if shows-frame {
|
||||
// Show ten-frame for this place value
|
||||
// Top frame: carry destination (next higher place value)
|
||||
// Bottom frame: current place value overflow
|
||||
let top-color = if i + 1 < actual-digits { place-colors.at(i + 1) } else { color-none }
|
||||
let bottom-color = place-colors.at(i)
|
||||
|
||||
(box(width: ${cellSizeIn}, height: ${cellSizeIn} * 0.8)[
|
||||
#align(center + top)[#ten-frames-stacked(${cellSizeIn} * 0.90, if show-colors { top-color } else { color-none }, if show-colors { bottom-color } else { color-none })]
|
||||
#place(top, line(length: ${cellSizeIn} * 0.90, stroke: heavy-stroke))
|
||||
],)
|
||||
} else {
|
||||
(v(${cellSizeIn} * 0.8),)
|
||||
}
|
||||
},
|
||||
)
|
||||
} else {
|
||||
|
|
@ -188,11 +300,22 @@ export function generateProblemStackFunction(cellSize: number): string {
|
|||
()
|
||||
},
|
||||
|
||||
// Answer boxes
|
||||
[],
|
||||
if show-answers { box(width: ${cellSizeIn}, height: ${cellSizeIn}, stroke: 0.5pt, fill: if show-colors { color-hundreds } else { color-none })[] } else { v(${cellSizeIn}) },
|
||||
if show-answers { box(width: ${cellSizeIn}, height: ${cellSizeIn}, stroke: 0.5pt, fill: if show-colors { color-tens } else { color-none })[] } else { v(${cellSizeIn}) },
|
||||
if show-answers { box(width: ${cellSizeIn}, height: ${cellSizeIn}, stroke: 0.5pt, fill: if show-colors { color-ones } else { color-none })[] } else { v(${cellSizeIn}) },
|
||||
// Answer boxes (one per digit column, only for actual digits including sum)
|
||||
[], // Empty cell for + sign column
|
||||
..for i in range(0, actual-digits).rev() {
|
||||
let place-color = place-colors.at(i) // Dynamic color lookup by place value
|
||||
let fill-color = if show-colors { place-color } else { color-none }
|
||||
let show-answer = show-answers
|
||||
|
||||
if show-answer {
|
||||
(box(width: ${cellSizeIn}, height: ${cellSizeIn}, stroke: 0.5pt, fill: fill-color)[],)
|
||||
} else {
|
||||
// No answer box (show-answers is false)
|
||||
(box(width: ${cellSizeIn}, height: ${cellSizeIn})[
|
||||
#v(${cellSizeIn})
|
||||
],)
|
||||
}
|
||||
},
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue