G GAMESRICH
Anti-Stress Games · Intermediate

How to Build an Interactive Slime Simulator: Slime Santai

Create viscous fluids with interpolated trailing strokes, specular shine gradients, and squish sound design.

By GamesRich Engineering 8 min read Zero External Libraries

Play the Live Game First

Experience the mechanics firsthand to understand what you are building.

PLAY LIVE GAME ↗

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:

Step-by-Step Implementation

1

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.

Step 1 Implementation
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);
}
2

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.

Step 2 Implementation
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();
}
3

Render Viscous Deformation and Ripples

Iterate through strokes, drawing thick semi-transparent highlights and soft drop shadows that decay smoothly over time.

Step 3 Implementation
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:

Complete Runnable Game (index.html)
<!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: