Cell effects
Cell effects are cell display functions—drawing procedures stored directly in quadrille cells just like any other valid JavaScript value
such as when calling fill(row, col, value)
. Each cell can thus define how it should be drawn, enabling animations, per-cell styling, and interactive visuals.
To behave seamlessly with drawQuadrille
, cell effects should follow this display contract: a cell display function is a function that returns nothing and receives either no parameters or a single options
object:
// A cell effect is a display function stored in a quadrille cell.
// It must return nothing and can be defined with either no parameters
// or a single options object:
({ row, col, origin }) => { /* drawing commands only */ }
// Parameters set by drawQuadrille:
row // Cell's row index (integer)
col // Cell's column index (integer)
origin // Local origin for drawing inside the cell:
// CORNER (default in P2D)
// CENTER (default in WEBGL)
// Can be overridden via drawQuadrille({ options: { origin: ... } })
Key points:
row
andcol
— set automatically bydrawQuadrille
.origin
— defaults:CORNER
in P2D,CENTER
in WEBGL; override viadrawQuadrille({ options: { origin } })
.- The function must not return a value—use drawing commands only (
line
,circle
,rect
, …).
This chapter teaches
- Using functions as values — the basics of functional programming
- A lightweight design-by-contract for cell displays
- Building boards filled with display functions
- Using the
options
object to drive per-cell dynamics
Cell Effects Tic-Tac-Toe
The Tic-Tac-Toe demo uses display functions for x
and o
instead of the value types used in previous chapters—such as strings ('X'
, 'O'
) or images.
code
let patterns1 = [];
let patterns2 = [];
let game;
let resetButton;
let winner;
const cellLength = Quadrille.cellLength;
const baseSize = cellLength * 0.3;
// Animated cross (Player 1)
function x() {
push();
stroke('tomato');
strokeWeight(3 + sin(frameCount * 0.1)); // animate thickness
const animatedSize = cellLength * 0.1 * sin(frameCount * 0.1);
const d = baseSize + animatedSize; // animate size
const cx = cellLength / 2; // center X
const cy = cellLength / 2; // center Y
line(cx - d, cy - d, cx + d, cy + d); // main diagonal
line(cx + d, cy - d, cx - d, cy + d); // anti-diagonal
pop();
}
// Animated circle (Player 2)
function o() {
push();
noFill();
stroke('royalblue');
strokeWeight(3 + sin(frameCount * 0.1)); // animate thickness
const animatedSize = cellLength * 0.1 * sin(frameCount * 0.1);
const r = baseSize + animatedSize; // animate size (radius)
const cx = cellLength / 2; // center X
const cy = cellLength / 2; // center Y
circle(cx, cy, 2 * r); // diameter = 2 * r
pop();
}
function setup() {
createCanvas(300, 300);
resetButton = createButton('reset game');
resetButton.position(10, height + 10);
resetButton.mousePressed(resetGame);
// Initialize board and win patterns
resetGame();
// Win patterns for Player 1 (X)
const diag1 = createQuadrille([
[x],
[null, x],
[null, null, x]
]); // createQuadrille builds a grid from a jagged array of values
const horiz1 = createQuadrille([x, x, x]);
patterns1.push(
diag1,
diag1.clone().reflect(),
horiz1,
horiz1.clone().transpose()
);
// Win patterns for Player 2 (O), cloned and replaced
const diag2 = diag1.clone().replace(x, o);
const horiz2 = horiz1.clone().replace(x, o);
patterns2.push(
diag2,
diag2.clone().reflect(),
horiz2,
horiz2.clone().transpose()
);
}
function draw() {
background('DarkSeaGreen');
drawQuadrille(game); // default origin (p2d: CORNER)
// Show winner message if game is over
if (winner) {
textSize(32);
fill('yellow');
text(`${winner} wins!`, width / 2, height / 2);
noLoop();
}
}
function mousePressed() {
// Ignore clicks if game is over
if (winner) return;
const row = game.mouseRow;
const col = game.mouseCol;
// Place current marker if cell is valid and empty
if (game.isValid(row, col) && game.isEmpty(row, col)) {
const current = game.order % 2 === 0 ? x : o;
game.fill(row, col, current);
// Check for win or restart on draw
winner = checkWinner();
if (!winner && game.order === 9) resetGame();
}
}
// Reset the game with a new empty board
function keyPressed() {
if (key === 'r' || key === 'R') {
resetGame();
loop();
}
}
// Create a new game state and clear winner
function resetGame() {
game = createQuadrille(3, 3);
winner = undefined;
loop();
}
// Search for win conditions using predefined patterns
function checkWinner() {
return patterns1.some(p => game.search(p, true).length > 0) && 'Player 1' ||
patterns2.some(p => game.search(p, true).length > 0) && 'Player 2';
}
x()
and o()
are new.Try this
- Make
x()
ando()
pulse at different rates (e.g.,sin(frameCount*ω)
) and compare readability with and without helper variables.
Using origin: CENTER
for Simpler Drawing
Setting the drawing origin
to the cell center makes the code shorter and cleaner for symmetric shapes like x
and o
.
The snippet below shows the same game as Cell Effects Tic-Tac-Toe, but with origin: CENTER
instead of the default origin: CORNER
in p2d.
draw excerpt
function draw() {
background('DarkSeaGreen');
drawQuadrille(game, { options: { origin: CENTER } });
// ...
}
function x() with CENTER origin
function x() {
push();
stroke('tomato');
strokeWeight(3 + sin(frameCount * 0.1));
const d = baseSize + cellLength * 0.1 * sin(frameCount * 0.1);
line(-d, -d, d, d);
line(d, -d, -d, d);
pop();
}
function o() with CENTER origin
function o() {
push();
noFill();
stroke('royalblue');
strokeWeight(3 + sin(frameCount * 0.1));
const r = baseSize + cellLength * 0.1 * sin(frameCount * 0.1);
circle(0, 0, 2 * r);
pop();
}
origin: CENTER
and replace x()
/o()
with the shorter versions above. The visuals should match while the code gets cleaner.Using row
and col
Display functions automatically receive the row
and col
parameters from drawQuadrille, which you can use to vary animation speed or style across the board.
helper: distanceFromCenter(row, col)
function distanceFromCenter(row, col) {
const center = 1; // for 3×3, center cell is (1,1)
return abs(row - center) + abs(col - center); // Manhattan distance
}
function x({ row, col })
function x({ row, col }) {
push();
const t = frameCount * 0.05 / (1 + distanceFromCenter(row, col));
stroke('tomato');
strokeWeight(2 + sin(t));
const d = baseSize + cellLength * 0.1 * sin(t);
line(-d, -d, d, d);
line(d, -d, -d, d);
pop();
}
function o({ row, col })
function o({ row, col }) {
push();
const t = frameCount * 0.05 / (1 + distanceFromCenter(row, col));
noFill();
stroke('royalblue');
strokeWeight(2 + sin(t));
const r = baseSize + cellLength * 0.1 * sin(t);
circle(0, 0, 2 * r);
pop();
}
Use as before:
drawQuadrille(game, { options: { origin: CENTER } });
Try this
- Replace the
x()
ando()
functions from the earlier demo with therow
/col
-aware versions shown here. - Add global display options such as
phase
,speed
, orjitter
, and combine them withrow
andcol
to create radial or checkerboard animation patterns. - Build an effects toolkit: define small helpers—e.g.,
pulse({speed, amp})
,glow({alpha})
,wiggle({phase})
—that each return a display function. Use closures to capture parameters, then compose them (nest one inside another) to form richer visuals. Finally, swap these effects into your Layered Boards Tic-Tac-Toe to confirm that the contract lets you change the view without altering the game logic.
References
- Design by contract — Overview of contracts in software engineering.
- p5.js Reference — Drawing state essentials:
push
,pop
,strokeWeight
,colorMode
. - Quadrille API (local) — See
drawQuadrille
,fill
,search
, andoptions.origin
notes. - Related chapters — Aggregated States, Layered Boards, Timelines.
Further Reading
- Meyer, B. (1992). Applying Design by Contract. Communications of the ACM. — Clear, accessible intro to contracts; maps nicely to our display-contract idea.
- Perlin, K. (1985). An Image Synthesizer. SIGGRAPH ’85. — Classic paper on procedural textures and smooth animation—great inspiration for richer cell effects.
- Ebert, D., Musgrave, F., Peachey, D., Perlin, K., & Worley, S. (2002). Texturing & Modeling: A Procedural Approach. Morgan Kaufmann. — Comprehensive reference for procedural visuals, patterns, and noise.
- Shiffman, D. (2012). The Nature of Code. Self-published. — Friendly, code-first exploration of motion, oscillation, and systems; many ideas translate directly into per-cell effects.
- Hughes, J. (1990). Why Functional Programming Matters. The Computer Journal. — Influential essay on higher-order functions and composition—the mindset behind treating displays as values.