Haunted wobble
~5 minMake the ghost shiver in place while it waits to be clicked.
// in draw(), just before drawGhost():
ghost.x += random(-2, 2);
ghost.y += random(-2, 2);Guide
This guide assumes you've never used p5.js. You'll learn how drawing works, draw your own ghost, make it move, and finish with Ghost Hunt: the same base game everyone starts from.
p5.js is a JavaScript library for drawing and animating in the browser. You get a canvas, a rectangle of pixels, and simple functions like ellipse() and rect() to draw on it.
Every p5.js sketch has two special functions:
setup() runs once when the sketch starts. Make the canvas here.draw() runs again and again, about 60 times a second. Each run is one frame, like a page in a flipbook.Open editor.p5js.org, delete the starter code, and paste this:
function setup() {
createCanvas(480, 360); // make a 480 x 360 pixel canvas
}
function draw() {
background(15, 10, 25); // paint the whole canvas dark purple
}Every point on the canvas is an (x, y) pair. (0, 0) is the top-left corner. x grows to the right, y grows downward. On a 480 × 360 canvas, the middle is (240, 180). Inside your sketch, width and height hold those sizes for you.
Drawing in p5.js works like painting: pick a color, then draw a shape with it. Shapes drawn later go on top.
background(r, g, b) fills the whole canvas. Calling it at the start of draw() wipes the last frame.fill(r, g, b) sets the color for the next shapes. Each number is 0-255: red, green, blue.noStroke() turns off outlines.ellipse(x, y, w, h) draws a circle or oval centered at (x, y).rect(x, y, w, h) draws a rectangle from its top-left corner.function setup() {
createCanvas(480, 360);
}
function draw() {
background(15, 10, 25);
noStroke(); // no outlines on the shapes below
fill(255, 240, 200); // pale yellow
ellipse(400, 60, 50, 50); // a moon: x, y, width, height
fill(40, 30, 50); // dark purple
rect(0, 300, 480, 60); // the ground: x, y, width, height
}400, 60. Change the fill numbers to make a blood-red moon.Now make a drawing of your own. Put the drawing inside a function with x and y parameters. Then you can draw the ghost anywhere, as many times as you want, with one line.
Every shape inside uses x and y as its starting point, so the eyes move with the body.
function draw() {
background(15, 10, 25);
drawGhost(240, 180); // draw a ghost in the middle
drawGhost(100, 80); // and another one, top left
}
function drawGhost(x, y) {
noStroke();
fill(220, 255, 230); // minty white body
ellipse(x, y, 60, 60);
fill(20); // almost-black eyes
ellipse(x - 12, y - 5, 10);
ellipse(x + 12, y - 5, 10);
}ellipse(), give it a hat with rect(), or change its color. Keep the body about 60 pixels wide: the click check later expects that.random(a, b) gives you a different number between a and b every time you call it. That's the key to anything unpredictable.
First, store the ghost's position in a variable so it can change between frames. { x: 240, y: 180 } is an object: one variable holding both numbers.
Nudge the ghost a random amount every frame. It shivers and wanders around like it's possessed.
let ghost = { x: 240, y: 180 };
function draw() {
background(15, 10, 25);
ghost.x += random(-3, 3); // drift a little left or right
ghost.y += random(-3, 3); // drift a little up or down
drawGhost(ghost.x, ghost.y);
}This is what Ghost Hunt does. Every 60 frames (about one second), the ghost jumps to a random spot. frameCount counts frames since the start, and % gives the remainder of a division, so frameCount % 60 === 0 is true once every 60 frames.
let ghost; let spawnEvery = 60; // frames between jumps (60 frames = about 1 second) function setup() { createCanvas(480, 360); spawnGhost(); } function draw() { background(15, 10, 25); if (frameCount % spawnEvery === 0) spawnGhost(); // every 60th frame drawGhost(ghost.x, ghost.y); } function spawnGhost() { ghost = { x: random(40, width - 40), y: random(40, height - 40) }; }
40 and width - 40 keep the ghost from spawning half off the edge. Keep your drawGhost() function from step 3 at the bottom of the file.p5.js calls mousePressed() every time you click. mouseX and mouseY hold where the click happened.
dist() measures the distance between two points. The ghost is 60 pixels wide, so its radius is 30: a click closer than 30 pixels to its center is a hit. An if statement runs code only when that's true.
let score = 0;
function mousePressed() {
// how far was the click from the ghost's center?
if (dist(mouseX, mouseY, ghost.x, ghost.y) < 30) {
score++; // hit! add a point
spawnGhost(); // and move the ghost somewhere new
}
}
// at the end of draw():
fill(255);
textSize(16);
text('Score: ' + score, 12, 26);millis() is how many milliseconds have passed since the sketch started (1000 ms = 1 second). Save it when the round begins, and subtract to see how long the round has lasted.
let duration = 30; // seconds per round let startTime; // in setup(): startTime = millis(); // in draw(), before drawing the ghost: let elapsed = (millis() - startTime) / 1000; let timeLeft = max(0, duration - elapsed); textAlign(RIGHT, TOP); text('Time: ' + ceil(timeLeft), width - 12, 10);
One last idea: a state variable decides what the screen shows. 'start' shows a title, 'play' runs the game, 'over' shows your score. A click moves between them.
Here's the complete game. Replace everything in the editor with it:
let ghost;
let score = 0;
let duration = 30;
let startTime;
let state = 'start'; // 'start' | 'play' | 'over'
let spawnEvery = 60;
function setup() {
createCanvas(480, 360);
spawnGhost();
}
function draw() {
background(15, 10, 25);
if (state === 'start') {
textAlign(CENTER, CENTER);
fill(255);
textSize(22);
text('Click to start', width / 2, height / 2);
return;
}
if (state === 'over') {
textAlign(CENTER, CENTER);
fill(255);
textSize(22);
text('Game over\nScore: ' + score + '\nClick to retry', width / 2, height / 2);
return;
}
let elapsed = (millis() - startTime) / 1000;
let timeLeft = max(0, duration - elapsed);
if (timeLeft <= 0) {
state = 'over';
return;
}
if (frameCount % spawnEvery === 0) spawnGhost();
drawGhost(ghost.x, ghost.y);
textAlign(LEFT, TOP);
fill(255);
textSize(16);
text('Score: ' + score, 12, 10);
textAlign(RIGHT, TOP);
text('Time: ' + ceil(timeLeft), width - 12, 10);
}
function mousePressed() {
if (state === 'start' || state === 'over') {
state = 'play';
score = 0;
startTime = millis();
spawnGhost();
return;
}
if (dist(mouseX, mouseY, ghost.x, ghost.y) < 30) {
score++;
spawnGhost();
}
}
function spawnGhost() {
ghost = { x: random(40, width - 40), y: random(40, height - 40) };
}
function drawGhost(x, y) {
noStroke();
fill(220, 255, 230);
ellipse(x, y, 60, 60);
fill(20);
ellipse(x - 12, y - 5, 10);
ellipse(x + 12, y - 5, 10);
}Short functions you can drop into your game. Pick 1-3 that sound fun. Want to see them all combined? Check the full demo.
Make the ghost shiver in place while it waits to be clicked.
// in draw(), just before drawGhost():
ghost.x += random(-2, 2);
ghost.y += random(-2, 2);Give the ghost a speed and bounce it off the walls instead of sitting still.
// in spawnGhost(), after making the ghost: ghost.vx = random(-2, 2); ghost.vy = random(-2, 2); // in draw(), before drawGhost(): ghost.x += ghost.vx; ghost.y += ghost.vy; if (ghost.x < 30 || ghost.x > width - 30) ghost.vx *= -1; if (ghost.y < 30 || ghost.y > height - 30) ghost.vy *= -1;
A second thing to draw. Swap it in for the ghost, or use it as a bonus target.
function drawPumpkin(x, y) {
noStroke();
fill(255, 140, 40); // orange body
ellipse(x, y, 64, 52);
fill(60, 140, 60); // green stem
rect(x - 4, y - 34, 8, 12);
fill(30); // spooky face
triangle(x - 16, y - 4, x - 8, y - 12, x - 2, y - 4);
triangle(x + 2, y - 4, x + 8, y - 12, x + 16, y - 4);
rect(x - 12, y + 8, 24, 5);
}A fourth number in fill() is transparency. Make the ghost fade in and out.
// in drawGhost(), replace the body fill with:
let alpha = map(sin(frameCount * 0.05), -1, 1, 80, 255);
fill(220, 255, 230, alpha);Shrink the time between jumps as the round goes on.
// in draw(), replacing the fixed spawnEvery check: let progress = constrain(elapsed / duration, 0, 1); let interval = floor(lerp(60, 20, progress)); // 60 frames → 20 frames if (frameCount % interval === 0) spawnGhost();
Tiny dots fly out from the ghost when you catch it, then fade away.
let particles = [];
function burst(x, y) {
for (let i = 0; i < 10; i++) {
let a = random(TWO_PI);
particles.push({ x: x, y: y, vx: cos(a) * 2, vy: sin(a) * 2, life: 1 });
}
}
// call burst(ghost.x, ghost.y) on a hit, then in draw():
for (let i = particles.length - 1; i >= 0; i--) {
let pt = particles[i];
pt.x += pt.vx;
pt.y += pt.vy;
pt.life -= 0.04;
if (pt.life <= 0) { particles.splice(i, 1); continue; }
noStroke();
fill(220, 255, 230, pt.life * 255);
ellipse(pt.x, pt.y, 5);
}Read the full requirements once more, then submit your share link.