← Back to Speeches

Source Code Walkthrough • Fire Breathing Dragon

How the Dragon Works

▶  Hold the mouse button inside the canvas — watch the code come to life

Six Sections, One Dragon

1
Global State
Variables that live for the entire sketch lifetime

Three variables live at the top level — outside any function — so every part of the sketch can access them at any time.

particles is an array that grows and shrinks every frame: new fire particles are pushed in when the mouse is held, and dead ones are spliced out.

dragonX and dragonY set the dragon’s anchor point on the canvas. Because translate(dragonX, dragonY) shifts the coordinate origin to this position before anything is drawn, you can move the entire dragon by changing just these two numbers.

💡 Keep state at the smallest scope that works. These three variables must be global because both draw() and the Particle class need them.
// ── GLOBAL STATE ─────────────────────────────
// Array of live fire particles.
// New ones are added each frame while the mouse
// is pressed; finished ones are removed.
let particles = [];

// Dragon anchor — the origin point around which
// all dragon geometry is drawn.
// Changing these moves the whole dragon instantly.
let dragonX = 150;
let dragonY = 200;
2
setup()
Runs once — creates the canvas

p5.js calls setup() exactly once when the sketch first loads. Its only job here is to create a 400×400 pixel canvas where everything will be drawn.

createCanvas(w, h) returns a canvas element and inserts it into the page. Every subsequent draw call paints on this surface.

💡 In a full sketch you might also set frameRate(), load assets, or position the canvas here. Keep setup() lean — it runs before the user sees anything.
// ── SETUP ────────────────────────────────────
// Called once on page load.
function setup() {
  createCanvas(400, 400);
}
3
isMouseInCanvas()
A helper function that keeps interaction inside the canvas boundary

When a p5.js sketch is embedded inside a webpage, the page itself can still receive mouse events. Without this guard, holding the mouse button anywhere on the page — scrollbars, buttons, text — would trigger the jaw animation and fire, even when the cursor is nowhere near the dragon.

isMouseInCanvas() checks whether the current mouse coordinates fall inside the canvas bounds. p5.js provides mouseX and mouseY relative to the canvas origin, and width / height for its dimensions. If the mouse is outside the canvas, both values will be out of range.

The function is called alongside mouseIsPressed in every place that responds to the mouse — the jaw variables in draw() and the fire-spawn guard — using a simple && (AND) condition.

💡 This is a great example of extracting a helper function: the same four-comparison test is needed in multiple places. Writing it once and calling it by name is cleaner, easier to read, and means you only have to fix it in one place if the logic ever changes.
// ── BOUNDS CHECK ─────────────────────────────
// Returns true only when the mouse cursor is
// actually inside the canvas area.
// Prevents clicks elsewhere on the page from
// triggering the jaw / fire unexpectedly.
function isMouseInCanvas() {
  return mouseX >= 0 && mouseX <= width
      && mouseY >= 0 && mouseY <= height;
}
4
draw() — The Dragon Body
Seven layered shapes + coordinate transforms for the moving jaw

draw() runs ~60 times per second. Every frame starts by painting the dark background over the previous frame, then redraws the dragon from scratch.

Coordinate transform — the first thing inside draw() is push() followed by translate(dragonX, dragonY). This moves the canvas’ origin to the dragon’s anchor, so every draw command that follows uses dragon-relative coordinates. The outer pop() at the end restores the origin for the fire system.

Animated jaw — the lower jaw uses a second nested push()/pop() block: translate(15, 15 + jawDown) moves the origin to the hinge point on the cheek, then rotate(jawAngle) pivots the jaw around that exact point. Without the translate first, the jaw would spin from the top-left corner of its bounding box — the original bug.

Both jaw variables use a ternary that checks mouseIsPressed && isMouseInCanvas(). The extra guard ensures the jaw only animates when the cursor is actually over the canvas — not when a mouse button is held elsewhere on the page.

The seven body parts draw in back-to-front order so each shape sits correctly on top of the ones beneath it.

💡 push() / pop() rule: every push() must have a matching pop(). They save and restore the entire drawing state: position, rotation, fill, stroke, and weight.
// ── DRAW ─────────────────────────────────────
// Runs ~60× per second.
function draw() {
  background(15, 20, 30);  // repaint dark each frame

  // ── DRAGON ───────────────────────────────────
  push();
  translate(dragonX, dragonY); // move origin to anchor

  // Jaw animation: opens 5px down, rotates 11° when clicked inside canvas
  let jawDown  = mouseIsPressed && isMouseInCanvas() ? 5   : 0;
  let jawAngle = mouseIsPressed && isMouseInCanvas() ? 0.2 : 0;  // radians

  // 1. Horns — drawn behind the head
  fill(200, 180, 150);
  noStroke();
  triangle(-10, -20,  -5, -45,  10, -20);
  triangle(-25, -15, -25, -40, -10, -15);

  // 2. Head — dark green ellipse
  fill(34, 139, 34);
  stroke(0, 100, 0);
  strokeWeight(2);
  ellipse(0, 0, 80, 70);

  // 3. Lower jaw — hinged at (15, 15) on the cheek
  push();
  translate(15, 15 + jawDown); // move origin to hinge point
  rotate(jawAngle);           // rotate around that point
  rect(0, -5, 55, 18, 5);      // rect drawn from the pivot
  pop();

  // 4. Upper snout — fixed, drawn over the jaw
  rect(10, -10, 60, 25, 10);

  // 5. Eye — three nested ellipses: black / red / black pupil
  fill(0);   noStroke();  ellipse(15, -10, 15, 18);
  fill(255, 0, 0);           ellipse(18, -10,  8, 12);
  fill(0);                    ellipse(20, -10,  3,  7);

  // 6. Nostril
  fill(20, 60, 20);  noStroke();  ellipse(60, 5, 6, 4);

  // 7. Ear / fin — semi-transparent light green
  fill(144, 238, 144, 150);
  noStroke();
  triangle(-35, 0, -50, -20, -35, -20);

  pop(); // restore origin from outer translate
5
draw() — The Fire System
Spawning, updating, and blending particles each frame

The fire system runs after the dragon body, so flames draw on top of the snout.

Spawning — while the mouse is held inside the canvas, seven new Particle objects are pushed into the array each frame (60×/s × 7 = up to 420 particles/s, capped by their fade speed). The spawn guard uses mouseIsPressed && isMouseInCanvas() so that holding a mouse button on the page outside the canvas doesn’t fill the array with particles the viewer can’t see.

Traversal trick — the loop counts backward from the end of the array. This lets splice(i, 1) remove a dead particle without skipping the next element — a classic array-removal pattern.

Additive blending — blendMode(ADD) makes overlapping pixels add their RGB values instead of covering each other. Where many particles overlap, colors accumulate toward white-hot. This is how real fire — and real light — behaves. blendMode(BLEND) resets to normal before the next frame.

💡 Always reset blendMode() after use. Additive blending left active will corrupt every other draw call in the same frame.
  // ── FIRE SYSTEM ──────────────────────────────
  // Spawn 7 new particles per frame while pressed inside canvas
  if (mouseIsPressed && isMouseInCanvas()) {
    for (let i = 0; i < 7; i++) {
      particles.push(
        new Particle(dragonX + 75, dragonY + 19)
      );
    }
  }

  // Additive blending: overlapping particles
  // add brightness → realistic fire glow
  blendMode(ADD);

  // Traverse backward so splice() doesn't skip elements
  for (let i = particles.length - 1; i >= 0; i--) {
    particles[i].update();   // move, fade, shrink
    particles[i].show();     // draw with glow
    if (particles[i].finished()) {
      particles.splice(i, 1); // remove from array
    }
  }

  blendMode(BLEND); // reset before next frame
}
6
The Particle Class
OOP — one class, hundreds of independent instances

Each fire ember is an instance of the Particle class. The class bundles the data (position, velocity, color, size, alpha) and the behavior (update, show, finished) into one reusable blueprint.

constructor — randomizes each particle at birth: velocity in both axes, starting size, and the green channel of the fire color (red and blue are fixed at 255 and 0). This randomness is what makes the fire look organic rather than mechanical.

update() — called each frame. Moves by velocity, decreases opacity by 8, shrinks size by 5%. Three lines produce motion, fade, and taper simultaneously.

show() — draws a filled circle, then uses the native Canvas API’s shadowBlur to add a soft glow. The shadow is reset to 0 immediately after to avoid bleeding onto other shapes.

finished() — returns true when the particle has faded out or shrunk to nothing, signaling draw() to remove it.

💡 Why a class? You could write this with parallel arrays (xArr, yArr, alphaArr…) but the class makes each particle self-contained. Adding a new property like rotation means one change in one place.
// ── PARTICLE CLASS ───────────────────────────
// One blueprint for every fire ember.
class Particle {

  constructor(x, y) {
    this.x     = x;
    this.y     = y;
    this.vx    = random(3, 7);    // rightward drift
    this.vy    = random(-2, 2);  // random vertical scatter
    this.alpha = 255;             // starts fully opaque
    this.size  = random(15, 30); // random ember diameter
    this.r     = 255;             // full red always
    this.g     = random(50, 150);// random orange-yellow
    this.b     = 0;               // no blue
  }

  // Returns true when the particle should be removed
  finished() {
    return this.alpha < 0 || this.size < 1;
  }

  // Move, fade, and shrink each frame
  update() {
    this.x     += this.vx;
    this.y     += this.vy;
    this.alpha -= 8;         // fade out linearly
    this.size  *= 0.95;      // shrink by 5% each frame
  }

  // Draw the ember with a soft glow
  show() {
    noStroke();
    fill(this.r, this.g, this.b, this.alpha);

    // Native Canvas API: soft shadow = glow effect
    drawingContext.shadowBlur  = 15;
    drawingContext.shadowColor =
      `rgb(${this.r},${this.g},${this.b})`;

    ellipse(this.x, this.y, this.size);

    drawingContext.shadowBlur = 0; // always reset
  }
}

What This Sketch Teaches

Concept Where It Appears Why It Matters
push() / pop() Outer dragon block; jaw hinge block Save & restore the entire drawing state so transforms on one part don’t affect other parts
translate() + rotate() Dragon anchor; jaw hinge Move the coordinate origin before rotating so pivots happen at the right point — not the canvas corner
Particle systems (OOP) Particle class + particles[] array Spawn many objects with randomized behavior to simulate emergent natural phenomena (fire, rain, smoke)
blendMode(ADD) Fire rendering loop Overlapping colors add brightness; used in real rendering engines for fire, neon, and light
Reverse-loop splice pattern Particle cleanup loop Traverse an array backwards so removing an element with splice(i,1) doesn’t skip the next index
drawingContext.shadowBlur Particle.show() Access the native Canvas 2D API directly from p5.js to get effects p5 doesn’t expose natively
Helper function + bounds check isMouseInCanvas() — jaw variables & fire spawn guard Extract repeated logic into a named function; prevents mouse events on the surrounding page from triggering sketch interactions

Copy & Modify the Dragon

The full sketch in p5.js global mode — ready to paste into the p5.js editor and make it your own.

dragon.js — p5.js (global mode)