Architecture & Core Concept
Slime is modeled as a viscous fluid surface with trailing strokes: When the user drags across the canvas: 1. Each move pushes a stroke point with { x, y, prevPoint, life: 1.0, force } 2. Points are connected by thick rounded lines: ctx.lineCap = 'round', ctx.lineWidth = 42 * life 3. Over time, point life decays (p.life -= dt * 0.34), causing slime stretches to settle back 4. A static radial shine gradient overlays the slime, giving it an authentic wet, glossy sheen 5. Taps spawn concentric circular ripples that expand and fade.
Key Game Mechanics Taught:
- Continuous pointer stroke tracking and previous-point line interpolation
- Radial specular lighting gradients creating a wet glossy appearance
- Decaying stroke alpha and width modeling fluid relaxation
- Concentric ripple wave propagation physics
- Low-frequency squish sound synthesis
Step-by-Step Implementation
Create Wet Glossy Base with Radial Gradients
Layering a soft off-center radial gradient over a vibrant two-color linear gradient creates the illusion of translucent, shiny gel.
function drawGlossyBackground() {
const grad = ctx.createLinearGradient(0, 0, width, height);
grad.addColorStop(0, '#c084fc'); // Pastel purple
grad.addColorStop(1, '#f472b6'); // Pastel pink
ctx.fillStyle = grad;
ctx.fillRect(0, 0, width, height);
// Specular sheen light reflection
const shine = ctx.createRadialGradient(width * 0.25, height * 0.16, 5, width * 0.25, height * 0.16, width * 0.7);
shine.addColorStop(0, 'rgba(255, 255, 255, 0.30)');
shine.addColorStop(1, 'rgba(255, 255, 255, 0)');
ctx.fillStyle = shine;
ctx.fillRect(0, 0, width, height);
}
Record & Interpolate Pointer Strokes
Store pointer coordinates with links to the previous point. This guarantees smooth stroke lines even when rapid mouse movements skip pixels.
let lastPoint = null;
const strokes = [];
function recordPoint(x, y, force = 1) {
const p = { x, y, life: 1.0, force };
if (lastPoint) {
p.prev = lastPoint;
}
strokes.push(p);
lastPoint = p;
if (strokes.length > 160) strokes.shift();
}
Render Viscous Deformation and Ripples
Iterate through strokes, drawing thick semi-transparent highlights and soft drop shadows that decay smoothly over time.
function renderSlime(dt) {
strokes.forEach(p => p.life -= dt * 0.34); // Settle slime
strokes = strokes.filter(p => p.life > 0);
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
strokes.forEach(p => {
const alpha = Math.min(0.34, p.life * 0.4);
ctx.strokeStyle = `rgba(255, 255, 255, ${alpha})`;
ctx.lineWidth = 42 * p.force * p.life + 12;
if (p.prev) {
ctx.beginPath();
ctx.moveTo(p.prev.x, p.prev.y);
ctx.lineTo(p.x, p.y);
ctx.stroke();
}
});
}
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>Interactive Slime Demo</title>
<style>body{margin:0;background:#1e1b2e;display:flex;justify-content:center;align-items:center;height:100vh;touch-action:none;}canvas{background:#a855f7;border-radius:16px;box-shadow:0 10px 30px rgba(0,0,0,0.3);}</style>
</head><body><canvas id="c" width="700" height="500"></canvas>
<script>
const c = document.querySelector('#c'), ctx = c.getContext('2d');
let down = false, lastPoint = null, strokes = [], ripples = [], last = performance.now();
c.onpointerdown = e => {
down = true; lastPoint = null;
const r = c.getBoundingClientRect();
addPoint(e.clientX - r.left, e.clientY - r.top, 1.4);
ripples.push({ x: e.clientX - r.left, y: e.clientY - r.top, r: 10, life: 1 });
};
c.onpointermove = e => {
if(!down) return;
const r = c.getBoundingClientRect();
addPoint(e.clientX - r.left, e.clientY - r.top);
};
window.onpointerup = () => down = false;
function addPoint(x, y, force = 1) {
const p = { x, y, life: 1, force, prev: lastPoint };
strokes.push(p); lastPoint = p;
if(strokes.length > 140) strokes.shift();
}
function loop(t) {
const dt = Math.min((t-last)/1000, 0.04); last = t;
const grad = ctx.createLinearGradient(0,0,700,500);
grad.addColorStop(0, '#c084fc'); grad.addColorStop(1, '#f472b6');
ctx.fillStyle = grad; ctx.fillRect(0,0,700,500);
strokes.forEach(p => p.life -= dt * 0.35);
strokes = strokes.filter(p => p.life > 0);
ctx.lineCap = 'round'; ctx.lineJoin = 'round';
strokes.forEach(p => {
ctx.strokeStyle = `rgba(255,255,255,${p.life * 0.35})`;
ctx.lineWidth = 40 * p.life + 10;
if(p.prev) {
ctx.beginPath(); ctx.moveTo(p.prev.x, p.prev.y); ctx.lineTo(p.x, p.y); ctx.stroke();
}
});
ripples.forEach(q => {
q.r += 70 * dt; q.life -= dt * 0.9;
ctx.strokeStyle = `rgba(255,255,255,${q.life * 0.5})`;
ctx.lineWidth = 3; ctx.beginPath(); ctx.arc(q.x, q.y, q.r, 0, 7); ctx.stroke();
});
ripples = ripples.filter(q => q.life > 0);
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.