Architecture & Core Concept
Unlike competitive games, toddler sensory activities prioritize immediate tactile joy with zero penalty. Every tap generates a visual burst (expanding icon + exploding sparkles) paired with a calming musical note. The audio frequency maps to screen width: notes = [262, 330, 392, 523, 659, 784] // C Major pentatonic Because all notes belong to the pentatonic scale, any combination of taps sounds musical and pleasing.
Key Game Mechanics Taught:
- Full-screen responsive canvas scaling with devicePixelRatio awareness
- Harmonious pentatonic scale audio synthesis (no harsh discords)
- Radial sparkle and growing icon particle dynamics
- Pointer events handling unified touch, pen, and mouse taps
- Parental hold-to-exit protection pattern
Step-by-Step Implementation
Full-Screen Canvas with High DPI Support
Keep canvas graphics sharp on Retina displays by multiplying width and height by devicePixelRatio while scaling CSS width to 100vw/100vh.
function resizeCanvas() {
const dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = window.innerWidth * dpr;
canvas.height = window.innerHeight * dpr;
canvas.style.width = window.innerWidth + 'px';
canvas.style.height = window.innerHeight + 'px';
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
window.addEventListener('resize', resizeCanvas);
Pentatonic Note Web Audio Synthesis
Avoid harsh notes by restricting synthesis to a pentatonic scale. The horizontal touch position determines the note pitch.
let audioCtx;
const pentatonicNotes = [262, 330, 392, 523, 659, 784]; // C4 to G5
function playChime(x) {
audioCtx ||= new (window.AudioContext || window.webkitAudioContext)();
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
const noteIndex = Math.floor((x / window.innerWidth) * pentatonicNotes.length);
osc.frequency.value = pentatonicNotes[noteIndex] || 440;
osc.type = 'sine'; // Soft, warm wave
gain.gain.setValueAtTime(0.055, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.3);
osc.connect(gain).connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + 0.35);
}
Expanding Icon & Sparkle Burst
When a pointer touches the screen, push a main expanding icon and 24 scattering sparkles into the particle simulation list.
function createBurst(x, y) {
playChime(x);
things.push({
x, y,
icon: ['★','♥','●','🐟','🌸'][Math.floor(Math.random() * 5)],
grow: 0,
life: 1.0,
color: '#ffd447'
});
for (let i = 0; i < 20; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = 70 + Math.random() * 200;
sparkles.push({
x, y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
life: 0.7,
size: 4 + Math.random() * 6
});
}
}
Complete Standalone Source Code
Here is the full, self-contained implementation. Save this as an index.html file on your computer and open it in any web browser to run your game immediately:
<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>Budak Kecik Sensory Demo</title>
<style>body{margin:0;overflow:hidden;background:#8066f4;touch-action:none;}</style>
</head><body><canvas id="c"></canvas>
<script>
const c = document.querySelector('#c'), ctx = c.getContext('2d');
let things = [], sparkles = [];
function resize() { c.width = innerWidth; c.height = innerHeight; }
window.onresize = resize; resize();
let audio;
function beep(x) {
audio ||= new (window.AudioContext || window.webkitAudioContext)();
const o = audio.createOscillator(), g = audio.createGain();
const notes = [262, 330, 392, 523, 659, 784];
o.frequency.value = notes[Math.floor((x/innerWidth)*notes.length)] || 440;
o.type = 'sine';
g.gain.setValueAtTime(0.05, audio.currentTime);
g.gain.exponentialRampToValueAtTime(0.001, audio.currentTime + 0.28);
o.connect(g).connect(audio.destination);
o.start(); o.stop(audio.currentTime + 0.3);
}
function tap(x, y) {
beep(x);
things.push({ x, y, icon:['★','♥','●','🌸'][Math.floor(Math.random()*4)], grow:0, life:1 });
for(let i=0;i<16;i++){
const a = Math.random()*Math.PI*2, s = 80+Math.random()*150;
sparkles.push({ x, y, vx:Math.cos(a)*s, vy:Math.sin(a)*s, life:0.6 });
}
}
window.addEventListener('pointerdown', e => tap(e.clientX, e.clientY));
let last = performance.now();
function loop(t) {
const dt = Math.min((t-last)/1000, 0.03); last = t;
things.forEach(q => { q.life -= dt; q.grow += (1 - q.grow) * dt * 8; });
things = things.filter(q => q.life > 0);
sparkles.forEach(s => { s.x += s.vx*dt; s.y += s.vy*dt; s.vy += 150*dt; s.life -= dt; });
sparkles = sparkles.filter(s => s.life > 0);
ctx.fillStyle = '#8066f4'; ctx.fillRect(0,0,innerWidth,innerHeight);
things.forEach(q => {
ctx.save(); ctx.translate(q.x, q.y); ctx.scale(q.grow, q.grow);
ctx.font = 'bold 64px sans-serif'; ctx.fillStyle = '#ffd447';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(q.icon, 0, 0); ctx.restore();
});
ctx.fillStyle = '#fff';
sparkles.forEach(s => {
ctx.beginPath(); ctx.arc(s.x, s.y, 5, 0, 7); ctx.fill();
});
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
</script>
</body></html>
Ideas to Expand Your Game
Now that you have the core mechanics running, consider adding:
- Local High Scores: Use
localStorage.setItem()andlocalStorage.getItem()to persist personal bests. - Visual Polish: Add screen shake on impacts by displacing
ctx.translate(shakeX, shakeY). - Mobile Touch Controls: Bind on-screen directional buttons using
onpointerdownandonpointerup.