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
— always set automatically bydrawQuadrille
.origin
— you may override it indrawQuadrille({ options: { origin: ... } })
; defaults areCORNER
in P2D andCENTER
in WEBGL.- The function must not return a value — it should contain only drawing commands (
line
,circle
,rect
, etc.).
This chapter teaches
- Functional programming basics through function values
- A lightweight design-by-contract approach for cell displays
- How to create and fill a quadrille with display functions
- How to use the
options
parameter in cell display functions to add rich visual dynamics
Cell Effects Tic-Tac-Toe
The Tic-Tac-Toe demo below 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 radius
const cx = cellLength / 2, cy = cellLength / 2;
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]
]);
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)
if (winner) {
textSize(32);
fill('yellow');
text(`${winner} wins!`, width / 2, height / 2);
noLoop();
}
}
function mousePressed() {
if (winner) return;
const row = game.mouseRow;
const col = game.mouseCol;
if (game.isValid(row, col) && game.isEmpty(row, col)) {
const current = game.order % 2 === 0 ? x : o;
game.fill(row, col, current);
winner = checkWinner();
if (!winner && game.order === 9) resetGame();
}
}
function keyPressed() {
if (key === 'r' || key === 'R') {
resetGame();
loop();
}
}
function resetGame() {
game = createQuadrille(3, 3);
winner = undefined;
loop();
}
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.Using origin: CENTER
for Simpler Drawing
Setting the drawing origin
to the cell center can make display code shorter and easier to read—especially for symmetric shapes like x
and o
in this example.
Below is the relevant excerpt using origin: CENTER
instead of the default origin: CORNER
in p2d. The rest of the game logic is the same as in [Cell Effects Tic-Tac-Toe](#cell-effects-tic-tac-toe).
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();
}
In the Cell Effects Tic-Tac-Toe demo, update
draw()
to use origin: CENTER
and replace the x()
and o()
functions with these simplified versions. The visual output should be identical, but the code will be cleaner.Using row
and col
Display functions receive row
and col
. You can use them 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 (parameters are passed automatically):
drawQuadrille(game, { options: { origin: CENTER } });
🧩 Try this
- Replace the
x()
ando()
functions from the earlier demo with theserow
/col
-aware versions. - Add global display options like
phase
,speed
, orjitter
, and combine them withrow
andcol
to create radial or checkerboard animation patterns.
References
- Design by contract — a gentle intro via Wikipedia: https://en.wikipedia.org/wiki/Design_by_contract
- p5.js reference for drawing state:
push()
,pop()
,strokeWeight()
,colorMode()
- See also: Aggregated States, Layered Boards, and Timelines for integrating effects with different state strategies.