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 — set automatically by drawQuadrille.
  • origin — defaults: CORNER in P2D, CENTER in WEBGL; override via drawQuadrille({ 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';
}
All interactivity (mouse and keyboard) comes from the Aggregated States chapter—only the display functions x() and o() are new.

Try this

  • Make x() and o() 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();
}
Try this Switch your Tic-Tac-Toe sketch to 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() and o() functions from the earlier demo with the row/col-aware versions shown here.
  • Add global display options such as phase, speed, or jitter, and combine them with row and col 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

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.