▶ Hold the mouse button inside the canvas — watch the code come to life
Code Walkthrough
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.
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;
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.
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); }
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.
// ── 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; }
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()
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
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.
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 }
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.
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 } }
Key Concepts
| 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 |
Complete Source Code
The full sketch in p5.js global mode — ready to paste into the p5.js editor and make it your own.