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 and col — always set automatically by drawQuadrille.
  • origin — you may override it in drawQuadrille({ options: { origin: ... } }); defaults are CORNER in P2D and CENTER 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';
}
ℹ️
All interactivity (mouse and keyboard) comes from the Aggregated States chapter—only the display functions 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();
}
🧩 Try this
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() and o() functions from the earlier demo with these row/col-aware versions.
  • Add global display options like phase, speed, or jitter, and combine them with row and col to create radial or checkerboard animation patterns.

References