Gravity Ink

Gravity Ink

In this physically accurate puzzle-drawing game, you are given dynamic pen-drawn lines and special ink physics to draw your way through mind-bending levels. Use unique ink types like Bouncy and Sticky to guide a gravity-affected ball through obstacles, collecting stars and navigating through portals in an ever-evolving, futuristic world.

Helpful?

Case trace

1 creative leap

Step 1 — The master promptCursorSonnet 4.5

Build a fully playable Gravity Painter game as a single React component (.jsx) using Matter.js for physics.

Tech Stack

  • React with hooks (useState, useEffect, useRef, useCallback)

  • Matter.js from CDN: https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js — import via useEffect script injection or assume it's available as window.Matter

  • All rendering done on a <canvas> ref — no external CSS files, all styles inline or via a <style> tag injected in the component

  • Single file, default export, no required props

Game Phases (managed via useState)

  • 'menu' — title screen with level select and instructions

  • 'drawing' — player draws shapes before simulation

  • 'simulating' — physics runs, ball drops

  • 'result' — level outcome shown (win/lose, stars, ink used)

Drawing Phase

  • Canvas ref captures mouse/touch events for freehand drawing

  • Strokes are stored as arrays of points in a drawnShapes ref

  • A toolbar rendered as React JSX (not on canvas) sits beside or above the canvas with:

    • Mode buttons — Solid, Ramp, Eraser (active mode stored in state)

    • Material buttons — Normal, Bouncy, Sticky, Slippery — each with a distinct color

    • Ink budget bar — a <div> progress bar depleting as stroke length accumulates

    • "Drop Ball" button — triggers phase transition to 'simulating'

    • "Clear" button — resets drawnShapes ref and redraws canvas

Physics Simulation

  • On phase change to 'simulating', convert drawnShapes into Matter.js static bodies and add to the world

  • Ball spawns at a fixed top-center position as a dynamic Matter.Bodies.circle

  • Use Matter.Runner and Matter.Events.on(engine, 'afterUpdate') inside a useEffect to drive the render loop

  • Each material maps to Matter.js body options:

    • Normal → { restitution: 0.4, friction: 0.5 }

    • Bouncy → { restitution: 0.95, friction: 0.1 }

    • Sticky → { restitution: 0.1, friction: 0.9 }

    • Slippery → { restitution: 0.3, friction: 0 }

  • Cleanup Matter.js engine and runner in useEffect return function to prevent memory leaks

Level Data

  • Define 6 levels as a plain JS array of objects outside the component:

    js

    const LEVELS = [
    { id: 1, name: 'First Drop', parInk: 200, goal: {x: 0.8, y: 0.85}, hazards: [], gravityY: 1 },
    // ...
    ]

  • Each level defines: goal position (as fraction of canvas size), hazard positions/types, gravity scale, wind force, par ink value, and collectible star positions

  • Current level index stored in state, increments on win

Canvas Rendering (useEffect watching phase + engine updates)

  • Drawing phase: render strokes live as player draws, color-coded by material

  • Simulation phase: clear and redraw each frame —

    • Draw all static shape bodies (player drawings)

    • Draw hazards (red spikes, voids)

    • Draw collectible stars (pulsing yellow circles)

    • Draw goal zone (glowing green circle with animated ring)

    • Draw ball with a fading trail (store last N positions in a ref)

    • Draw faint grid during drawing phase only

HUD (JSX overlay, not canvas)

  • Level name and number

  • Ink budget as an animated <div> bar

  • Star count ⭐ 0 / 3

  • Phase-appropriate action button: "Drop Ball" during drawing, "Retry" and "Next Level" on result screen

  • Result card rendered as JSX overlay on top of canvas when phase is 'result'

Result Phase

  • Triggered when: ball reaches goal zone (win) or ball exits canvas bounds (lose)

  • Show a centered JSX card with: win/lose status, stars collected, ink used vs par, two buttons

  • On "Retry" — reset Matter.js world, clear drawn shapes, go back to 'drawing' phase

  • On "Next Level" — load next level config, reset world and shapes, go to 'drawing'

Polish

  • Ball trail: store last 20 positions in a ref, draw as fading circles each frame

  • Goal zone: animated pulsing ring drawn on canvas each frame using a frameCount ref

  • Particle burst on goal reached: store particles in a ref, update and draw them each frame

  • Hazard spikes: drawn as triangles on canvas, destroy ball on collision via Matter.js collision events

  • Smooth 60fps loop driven by requestAnimationFrame stored in a ref and cancelled on cleanup

  • Dark background (#0f0f1a), neon accent palette: ball = #60d0ff, goal = #40ff90, hazard = #ff4060

Published 3w ago

Category

Code

Tools used

Cursor

Details

Tech stack

ReactTailwind

Tags

physicspuzzle2dgravity

More from Oni linn

Shards of Her — The Woman Behind the Masks

Shards of Her — The Woman Behind the Masks

A surreal woman holding a cracked porcelain mask in front of her face, her real face partially visible and emotionless, fragments floating, …

Adobe Firefly
Horizon's peak

Horizon's peak

Compose a cinematic orchestral piece inspired by the grandeur of nature. Start with soft ambient textures (wind, distant echoes) and gradual…

Flow Music
Paymark Fintech Landing Page — Dark Luxury UI

Paymark Fintech Landing Page — Dark Luxury UI

Build the hero section only for a fintech landing page called PAYMARK.Background: Near-black (#0d0c0c) with a warm ember/copper radial glow …

StitchDesktop

View similar

Animated Envelope

Animated Envelope

Create the HTML structure for an animated envelope component. Use nested divs for the envelope parts: back-fold, letter, top-fold (flap), bo…

CursorHTMLCSS
TypeBreaker

TypeBreaker

Build a fully playable Brick Breaker game in a single HTML/CSS/JS file with the following features:Pre-Game ScreenDisplay a centered input f…

v0
Liquid Magnetic Button

Liquid Magnetic Button

I want to build a button component in React that behaves like a magnet — when the cursor is within 100px of the button, the button smoothly …

ClaudeReactTailwind
Smart Typewriter Component

Smart Typewriter Component

Write a React hook useTypewriter(phrases, options) that types each phrase, pauses, backspaces it, then loops. Add ±30% jitter so it does not…

ClaudeReact
Animated Gradient Mesh Hero

Animated Gradient Mesh Hero

Create a full-viewport div with an animated CSS mesh gradient background. Use 4 radial gradients in purple, cyan, rose, and amber. The gradi…

v0ClaudeReact
3D Bohr Atom Model — CSS Animation

3D Bohr Atom Model — CSS Animation

Requirements:A central glowing nucleus positioned in the middle.Three elliptical or circular orbits arranged in 3D space using CSS transform…

Claude
Gravity Ink — Vibe5