Case 3: Pixel Art Drawing Interface
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 sync 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 }, ...getSavedData(), }, 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 load, reconstruct CellsByAddressType from the saved map and pass it into buildInitialCells as spread cells.
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 as CellsByAddressType
Section titled “Initial Pixel Art as CellsByAddressType”The heart shape pre-drawn on first load is defined as a plain CellsByAddressType object, where each key is a cell address and the value sets a backgroundColor.
const myHeart: CellsByAddressType = { C3: { style: { backgroundColor: 'red' } }, D3: { style: { backgroundColor: 'red' } }, // ... B5: { style: { backgroundColor: 'pink' } }, // ...};This object is spread directly into the cells option of buildInitialCells, making it trivial to define any initial pixel art pattern.