Imperative API: Pixel Art
This demo showcases drawing simple pictures using Excel grid paper.
A 50x50 grid is provided for drawing basic shapes and patterns.
Implementation Guide
Section titled “Implementation Guide”Imperative Sheet Access with useSheetRef and useStoreRef
Section titled “Imperative Sheet Access with useSheetRef and useStoreRef”useSheetRef and useStoreRef give you imperative access to the internal sheet state and Redux-like store outside the normal React data flow.
const sheetRef = useSheetRef();const storeRef = useStoreRef();
// Pass them as props to GridSheet<GridSheet sheetRef={sheetRef} storeRef={storeRef} ... />After the grid mounts, sheetRef.current exposes { sheet, apply } and storeRef.current exposes { store, dispatch }.
Reading the Current Selection with clip
Section titled “Reading the Current Selection with clip”clip(store) returns the currently selected area as { top, bottom, left, right }. This is the entry point for any operation that acts on the user’s selection.
const area = clip(store);if (!area) return;
for (let row = area.top; row <= area.bottom; row++) { for (let col = area.left; col <= area.right; col++) { const cellAddress = p2a({ y: row, x: col }); diff[cellAddress] = { style: { backgroundColor: selectedColor } }; }}p2a({ y, x }) converts a zero-based coordinate into a cell address string like "C3".
Writing Style Changes Imperatively with sheet.update
Section titled “Writing Style Changes Imperatively with sheet.update”To apply changes programmatically (e.g., from a button click), build a diff object keyed by cell address and call sheet.update({ diff }). Pass the result to apply to commit it to the store.
const { sheet, apply } = sheetRef.current;const diff: CellsByAddressType = { A1: { style: { backgroundColor: '#FF0000' } }, B1: { style: { backgroundColor: '#FF0000' } },};apply(sheet.update({ diff }));This pattern lets you drive cell mutations from outside the grid — for example, from a color picker or a fill button.
Guaranteeing Grid Size with ensured
Section titled “Guaranteeing Grid Size with ensured”By default, GridSheet only allocates rows/columns for cells that have data. Use the ensured option in buildInitialCells to reserve a minimum grid size regardless of data.
buildInitialCells({ cells: { defaultRow: { height: 25 }, defaultCol: { width: 25 }, ...toCells(heartColors), }, ensured: { numRows: 50, numCols: 50, },})This is essential for a blank drawing canvas where the user should be able to click any cell even before any data exists.
Persisting Cell Styles to localStorage
Section titled “Persisting Cell Styles to localStorage”To save only the relevant data (background colors), iterate over the sheet’s cell range, read cell.style.backgroundColor, and store a compact mapping of address → color.
const coloredCells: { [key: string]: string } = {};for (let row = sheet.top; row <= sheet.bottom; row++) { for (let col = sheet.left; col <= sheet.right; col++) { const cell = sheet.getCell({ y: row, x: col }); if (cell?.style?.backgroundColor) { coloredCells[p2a({ y: row, x: col })] = cell.style.backgroundColor; } }}localStorage.setItem('demo3', JSON.stringify({ cells: coloredCells }));On mount, the saved map is read once and painted onto the sheet with sheet.update + apply (initialCells is initial-only, so later changes go through the sheet API). The Reset button uses the same repaint to restore the heart in place — no page reload, and it can be undone with Ctrl/Cmd+Z.
const paint = (colors: { [address: string]: string }) => { const { sheet, apply } = sheetRef.current; const diff: CellsByAddressType = {}; for (let y = 1; y <= 50; y++) { for (let x = 1; x <= 50; x++) { const address = p2a({ y, x }); diff[address] = { style: colors[address] ? { backgroundColor: colors[address] } : {} }; } } apply(sheet.update({ diff }));};Auto-saving on Every Change with onChange
Section titled “Auto-saving on Every Change with onChange”useSpellbook accepts an onChange callback that fires after every cell mutation. Wiring the save logic there means the drawing is persisted automatically without requiring an explicit save button.
const book = useSpellbook({ onChange: saveData,});Initial Pixel Art from ASCII Art
Section titled “Initial Pixel Art from ASCII Art”The heart shown on first load is written as a compact ASCII-art array and converted into CellsByAddressType, where each key is a cell address and the value sets a backgroundColor.
const HEART = [ '..RR.........RR..', '.RppR.......RppR.', 'RppppR.....RppppR', // ... '........R........',];const PIXEL_COLORS = { R: 'red', p: 'pink' };
const heartColors = {};HEART.forEach((line, i) => { [...line].forEach((ch, j) => { if (PIXEL_COLORS[ch]) { heartColors[p2a({ y: i + 3, x: j + 1 })] = PIXEL_COLORS[ch]; // starts at A3 } });});
const toCells = (colors) => Object.fromEntries(Object.entries(colors).map(([address, color]) => [address, { style: { backgroundColor: color } }]));The resulting cells are spread directly into the cells option of buildInitialCells, making it trivial to define any initial pixel art pattern.