This demo illustrates new capabilities of the WebGL mode in the next major upcoming version of p5.quadrille.js, currently under development. It showcases how Platonic solids can be stored in quadrille cells and rendered using either immediate or retained mode with the p5.platonic library.

Platonic Cells

Platonic cells are cell functions (cellFn) that implement the filling of Platonic solid cells in a quadrille game.

Retained Mode

(mouse click to clear/add Platonic solids, drag to navigate; press s (or c) to save)

code
'use strict'
let cnv
let game, solids
let font
let images = []
const r = 5, c = 5, l = Quadrille.cellLength

function preload() {
  font = loadFont('noto_sans.ttf')
  for (let i = 1; i <= 10; i++) {
    images.push(loadImage(`p${i}.jpg`))
  }
}

function setup() {
  cnv = createCanvas(r * l, c * l, WEBGL)
  solids = createQuadrille(r, c)
  const args = [l / 2, true, ['yellow', 'blue', 'red',
                              'cyan', 'magenta', 'yellow']]
  visitQuadrille(solids, (row, col) => solids.fill(row, col,
                                       platonicGeometry(...args)))
  game = createQuadrille(5, 5)
  visitQuadrille(game, (row, col) => game.fill(row, col, random(images)))
  game.rand(15).rand(10, cellFn)
}

function draw() {
  background('Gold')
  drawQuadrille(game, {
    textFont: font,
    origin: CORNER,
    options: { origin: CENTER }
  })
}

function cellFn({ row, col }) {
  push()
  background('black')
  stroke('lime')
  fill('blue')
  rotateX(millis() * 0.001)
  rotateY(millis() * 0.001)
  rotateZ(millis() * 0.001)
  model(solids.read(row, col))
  pop()
}

function mouseClicked() {
  const row = game.mouseRow
  const col = game.mouseCol
  game.isEmpty(row, col) ? game.fill(row, col, cellFn) : game.clear(row, col)
}

function keyPressed() {
  key === 's' && game.toImage('game.png', {
    textFont: font,
    origin: CORNER,
    options: { origin: CENTER }
  })
  key === 'c' && save(cnv, 'platonic_cells.png')
}

const mouseWheel = () => false

Retained mode rendering of the Platonic cells requires the geometry to be built and stored in a separate solids quadrille in setup:

function setup() {
  // ...
  solids = createQuadrille(r, c)
  const args = [l / 2, true, ['yellow', 'blue', 'red',
                              'cyan', 'magenta', 'yellow']]
  // When no specific Platonic solid is passed to the p5.platonic 
  // platonicGeometry function (as done here), it returns a p5.Geometry 
  // instance of a random one. See: https://bit.ly/3XcQYUs
  visitQuadrille(solids, (row, col) => solids.fill(row, col,
                                       platonicGeometry(...args)))
}

The cellFn rendering procedure reads the solid located at (row, col) in the solids quadrille and renders it after applying some rotations, in retained mode with p5 model:

function cellFn({ row, col }) {
  push()
  background('black')
  stroke('lime')
  fill('blue')
  rotateX(millis() * 0.001)
  rotateY(millis() * 0.001)
  rotateZ(millis() * 0.001)
  model(solids.read(row, col))
  pop()
}

Note that the cellFn may be handled by the quadrille like any other value:

game.fill(row, col, cellFn)

Retained mode is the fastest approach but it requires the geometry to be computed in setup so that it is set in GPU memory only once.

Immediate Mode

(mouse click to clear/add Platonic solids, drag to navigate; press s to save)

code
'use strict'
let cnv
let game
let font
let images = []
const r = 5, c = 5, l = Quadrille.cellLength

function preload() {
  font = loadFont('noto_sans.ttf')
  for (let i = 1; i <= 10; i++) {
    images.push(loadImage(`p${i}.jpg`))
  }
}

function setup() {
  cnv = createCanvas(r * l, c * l, WEBGL)
  game = createQuadrille(r, c)
  visitQuadrille(game, (row, col) => 
                 game.fill(row, col, random() < 0.5 ? 
                                                createCellFn() :
                                                random(images)))
  game.rand(10)
}

function draw() {
  background('DeepSkyBlue')
  drawQuadrille(game, {
    outline: 'magenta',
    textFont: font,
    origin: CORNER,
    options: { origin: CENTER }
  })
}

function createCellFn() {
  const solid = random([tetrahedron, hexahedron, octahedron,
                        dodecahedron, icosahedron])
  return function cellFn() {
    push()
    background('black')
    stroke('lime')
    fill('red')
    rotateX(millis() * 0.001)
    rotateY(millis() * 0.001)
    rotateZ(millis() * 0.001)
    solid(Quadrille.cellLength / 2)
    pop()
  }
}

function mouseClicked() {
  const row = game.mouseRow
  const col = game.mouseCol
  game.isEmpty(row, col) ? game.fill(row, col, createCellFn()) :
                           game.clear(row, col)
}

function keyPressed() {
  key === 's' && game.toImage('game.png', {
    outline: 'magenta',
    textFont: font,
    origin: CORNER,
    options: { origin: CENTER }
  })
  key === 'c' && save(cnv, 'platonic_cells.png')
}

const mouseWheel = () => false

Immediate mode requires a cellFn function creator to create and return a random Platonic solid function:

function createCellFn() {
  // The Platonic functions tetrahedron, hexahedron, etc.,
  // are defined in the p5.platonic library. See: https://bit.ly/4bMaxYg
  const solid = random([tetrahedron, hexahedron, octahedron,
                        dodecahedron, icosahedron])
  return function cellFn() {
    push()
    background('black')
    stroke('lime')
    fill('red')
    rotateX(millis() * 0.001)
    rotateY(millis() * 0.001)
    rotateZ(millis() * 0.001)
    solid(Quadrille.cellLength / 2)
    pop()
  }
}

The function value returned by the creator is then stored in the game quadrille, just like any other data type:

game.fill(row, col, createCellFn())

Note that createCellFn is referred to as a higher-order function.

Immediate mode rendering is less performant than retained mode as it requires constantly transferring the Platonic solid geometry data once per frame to the GPU.

Cell Functions

Cell functions offer all the drawing features of the main canvas, extending beyond just displaying Platonic solids. Since each cell can incorporate complex WebGL content, a wide range of creative possibilities opens up, such as:

  • Interactive Animations: Embedding animated sequences within individual cells, making each cell an interactive component.
  • Textured Surfaces: Applying custom textures or procedural shaders to each cell for intricate visual effects.
  • Miniature Scenes: Creating miniature 3D scenes or models within each cell, turning the grid into a collection of diverse, detailed environments.
  • Dynamic Data Visualization: Visualizing dynamic data within cells, where each cell could represent a different data point or metric with its unique graphical representation.

Leveraging p5.Framebuffer for these features allows for more advanced rendering techniques and enhanced performance compared to p5.Graphics.

Future Work

WebGL in p5.quadrille.js requires at least:

  • Implement cell picking, i.e., make mouseRow and mouseCol work when navigating the scene, such as with orbitControl.
  • Improve the parameterization of the cellFn (e.g., to support cell camera interaction), as it currently only supports row and col (as shown above), and origin to define where to place the reference frame within each cell drawing, either CENTER (default) or CORNER.
  • Use WebGL mode to enhance the performance of cell sorting.
  • Properly document the new displayFunction functionality, which leverages the capabilities of p5.Framebuffer.

p5.quadrille.js API References

  • createQuadrille: Creates a new quadrille instance.
  • visitQuadrille: Visits each cell in a quadrille and applies a function.
  • drawQuadrille: Renders the quadrille on the canvas.
  • fill: Fills a cell with a specified content.
  • clear: Clears the content of a cell.
  • isEmpty: Checks if a cell is empty.
  • toImage: Converts the quadrille to an image.