Antigravity × Creative Coding: Building Interactive Art with p5.js, Three.js, and WebGL
A hands-on guide to creative coding with Antigravity — from generative art in p5.js to 3D scenes in Three.js and custom GLSL shaders. Includes working code examples, performance pitfalls, and strategies for publishing and monetizing your work.
For a long time I thought of coding and art as separate worlds.
Writing programs felt like logic. Making art felt like intuition. Whenever I tried to do both at once, one of them pulled me away from the other.
Working with Antigravity changed that, at least a little. I could describe what I wanted in plain language — "I want something that moves like smoke, but controlled" — and get working code back. I could adjust the feel of an animation not by hunting for the right variable name, but by saying "make it more organic." The gap between idea and implementation got smaller.
This guide documents that experience in concrete terms. We'll go from setting up p5.js for generative art, to Three.js for 3D work, to writing custom GLSL shaders with AI assistance. Throughout, the emphasis is on making things that actually run, not just sketches that never quite finish.
Environment Setup: p5.js + Antigravity
Starting a Vite + p5.js Project
For creative coding, simple environments tend to stick. CDN-loaded p5.js works, but Vite gives you hot module replacement — every code change reflects in the browser immediately. For art-making, that real-time feedback loop matters.
Replace main.js with p5 in instance mode. Global mode is fine for quick sketches, but instance mode prevents variable pollution and makes it easier to manage multiple sketches in the same project:
Before diving into code, define your project context in .antigravity/rules. This shifts Antigravity's suggestions from generic developer advice toward thinking like a creative coder:
# Creative Coding Project RulesThis project is for generative and interactive art production.## Priorities- Visual richness > code brevity- Experimental approaches are welcome- Suggestions for animation timing and color palette can include aesthetic judgment- Parameters should be named constants at the top of each file, not magic numbers## Tech Stack- Frontend: Vite + p5.js or Three.js- Shaders: GLSL (WebGL)- Deployment: Cloudflare Pages## Code Style- Comments can be in Japanese or English- Magic numbers must have meaningful names- Parameters should be grouped at the top of the file
With this context file in place, the same prompt — "add some noise to the movement" — returns art-maker suggestions rather than engineering ones.
Generative Art Fundamentals: Noise, Particles, and Trails
Drawing "Living" Flow with Perlin Noise
Perlin noise is one of the most versatile tools in generative art. It produces values that look random but connect smoothly — making it ideal for simulating natural phenomena like smoke, water, and wind.
Telling Antigravity "I want a flow field with particles moving through Perlin noise — blue to purple gradient, particles that leave faint trails" produces a starting implementation. What's more useful is asking follow-up questions: "What happens if I change NOISE_SCALE to 0.01?" gets you an explanation alongside the numerical answer, which builds your own understanding over time.
// Flow field + particle system in p5.jsimport p5 from 'p5'const sketch = (p) => { // ============ Parameters (tune these to change the feel) ============ const PARTICLE_COUNT = 800 // Number of particles const NOISE_SCALE = 0.003 // Noise granularity (smaller = smoother) const TIME_SCALE = 0.0005 // How fast the field evolves const TRAIL_ALPHA = 8 // Trail persistence (lower = longer trails) const SPEED = 2.5 // Particle velocity const HUE_RANGE = [200, 290] // Hue range in HSB (blue to purple) let particles = [] let time = 0 class Particle { constructor() { this.reset() } reset() { this.x = p.random(p.width) this.y = p.random(p.height) this.hue = p.random(HUE_RANGE[0], HUE_RANGE[1]) this.alpha = p.random(60, 100) this.size = p.random(1, 3) this.life = 0 this.maxLife = p.random(150, 400) } update() { // Derive angle from noise value const noiseVal = p.noise( this.x * NOISE_SCALE, this.y * NOISE_SCALE, time ) const angle = noiseVal * p.TWO_PI * 2 this.x += p.cos(angle) * SPEED this.y += p.sin(angle) * SPEED this.life++ // Reset when out of bounds or lifespan exceeded if ( this.x < 0 || this.x > p.width || this.y < 0 || this.y > p.height || this.life > this.maxLife ) { this.reset() } } draw() { const lifeRatio = 1 - this.life / this.maxLife p.stroke(this.hue, 80, 90, this.alpha * lifeRatio) p.strokeWeight(this.size) p.point(this.x, this.y) } } p.setup = () => { p.createCanvas(p.windowWidth, p.windowHeight).parent('app') p.colorMode(p.HSB, 360, 100, 100, 100) p.background(240, 30, 8) for (let i = 0; i < PARTICLE_COUNT; i++) { particles.push(new Particle()) } } p.draw = () => { // Repaint with low alpha to create trails p.fill(240, 30, 8, TRAIL_ALPHA) p.noStroke() p.rect(0, 0, p.width, p.height) particles.forEach(particle => { particle.update() particle.draw() }) time += TIME_SCALE } p.windowResized = () => { p.resizeCanvas(p.windowWidth, p.windowHeight) } p.mousePressed = () => { particles.forEach(p => p.reset()) }}new p5(sketch)
Increase NOISE_SCALE and the flow becomes chaotic; decrease it and you get long, sweeping curves. This kind of parameter tuning — guided by aesthetic preference rather than correctness — is where creative coding lives.
Adding Mouse Interaction
Adding interaction transforms a piece from something you watch into something you touch. Asking Antigravity to "add attraction toward the mouse cursor, with repulsion on click" returns physics-flavored code. The critical detail is the damping coefficient — without it, particles accelerate indefinitely.
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Break through the math and algorithm walls that block generative art progress by working with Antigravity in conversation, arriving at finished pieces with your own artistic voice
✦Learn Three.js 3D scene construction and GLSL shader implementation from scratch, then deploy interactive 3D artwork that runs live in the browser
✦Discover concrete strategies for turning creative coding work into portfolio pieces, digital products, and income streams as an indie developer-artist
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Once p5.js feels familiar, Three.js opens up 3D expression. The learning curve involves new concepts — cameras, lights, materials — but Antigravity handles much of that translation. "Create a rotating icosahedron with a point light from above and wireframe rendering" produces a complete working scene, including the WebGL context setup and animation loop.
Basic Scene Structure
Three.js scenes have three essential components: a Scene (the stage), a Camera (the viewpoint), and a Renderer (what draws the camera's view to the screen). Antigravity can scaffold all three:
npm install three @types/three
import * as THREE from 'three'import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'// Sceneconst scene = new THREE.Scene()scene.background = new THREE.Color(0x0a0a14)// Cameraconst camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000)camera.position.set(0, 0, 5)// Rendererconst renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance'})renderer.setSize(window.innerWidth, window.innerHeight)renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))document.body.appendChild(renderer.domElement)// Orbit controls with dampingconst controls = new OrbitControls(camera, renderer.domElement)controls.enableDamping = true// Geometry and materialconst geometry = new THREE.IcosahedronGeometry(1.5, 1)const material = new THREE.MeshPhongMaterial({ color: 0x6644aa, emissive: 0x220044, wireframe: true, transparent: true, opacity: 0.8})const mesh = new THREE.Mesh(geometry, material)scene.add(mesh)// Lightingscene.add(new THREE.AmbientLight(0xffffff, 0.3))const pointLight = new THREE.PointLight(0x8866ff, 2, 20)pointLight.position.set(3, 3, 3)scene.add(pointLight)// Animation loop with organic vertex deformationlet time = 0function animate() { requestAnimationFrame(animate) time += 0.01 const positions = geometry.attributes.position for (let i = 0; i < positions.count; i++) { const x = positions.getX(i) const y = positions.getY(i) const z = positions.getZ(i) const offset = Math.sin(x * 2 + time) * 0.15 + Math.cos(y * 2 + time * 0.7) * 0.1 const len = Math.sqrt(x * x + y * y + z * z) const scale = (1.5 + offset) / len positions.setXYZ(i, x * scale, y * scale, z * scale) } positions.needsUpdate = true geometry.computeVertexNormals() mesh.rotation.y += 0.003 mesh.rotation.x += 0.001 controls.update() renderer.render(scene, camera)}animate()window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight camera.updateProjectionMatrix() renderer.setSize(window.innerWidth, window.innerHeight)})
Communicating Intent to Antigravity in 3D
With Three.js, how you describe what you want shapes what you get back. Based on experience, these approaches work well:
Sensation-first: "I want something like a nebula — light gathering and dispersing." This gives Antigravity latitude to suggest particle systems, additive blending, and fog.
Reference-first: Paste a CodePen URL and ask "implement something like this in Three.js." The AI can analyze the visual approach from a description of what the piece does.
Problem-first: "The lighting looks wrong after I deform the vertices — the shading has artifacts." This kind of specific technical problem often resolves in one exchange.
The sensation-first approach is the most creative — you're supplying the artistic direction, and Antigravity handles the implementation translation.
GLSL Shaders with AI Assistance
When standard Three.js materials can't produce the effect you're after, GLSL shaders are the answer. This is where many creative coders stall. Antigravity makes it approachable by letting you describe the visual intent in plain language while it handles the shader math.
There are two shader types: the vertex shader (which transforms 3D positions) and the fragment shader (which determines pixel colors). For most generative art effects, the fragment shader is where the interesting work happens.
A Nebula-Like Fragment Shader
Telling Antigravity "create a GLSL fragment shader using fbm noise for a nebula-like effect — deep blues shifting to purple and white — for use as a Three.js ShaderMaterial" produces this:
// Fragment shader: nebula noise with Fractional Brownian Motionuniform float uTime;uniform vec2 uResolution;varying vec2 vUv;// 2D hash function (pseudo-random)vec2 hash22(vec2 p) { p = vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3))); return -1.0 + 2.0 * fract(sin(p) * 43758.5453123);}// Gradient noise (smooth, Perlin-like)float noise(vec2 p) { vec2 i = floor(p); vec2 f = fract(p); // Hermite interpolation for smooth transitions vec2 u = f * f * (3.0 - 2.0 * f); return mix( mix(dot(hash22(i + vec2(0.0, 0.0)), f - vec2(0.0, 0.0)), dot(hash22(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0)), u.x), mix(dot(hash22(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0)), dot(hash22(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0)), u.x), u.y );}// Fractional Brownian Motion — stacks octaves of noise for complexityfloat fbm(vec2 p) { float value = 0.0; float amplitude = 0.5; float frequency = 1.0; for (int i = 0; i < 5; i++) { value += amplitude * noise(p * frequency); frequency *= 2.0; amplitude *= 0.5; } return value;}void main() { vec2 uv = vUv * 3.0; float n = fbm(uv + uTime * 0.1); float n2 = fbm(uv * 1.5 + vec2(n, n) + uTime * 0.07); vec3 colorA = vec3(0.1, 0.1, 0.4); // Deep blue vec3 colorB = vec3(0.5, 0.2, 0.8); // Purple vec3 colorC = vec3(0.9, 0.8, 1.0); // Near-white light vec3 color = mix(colorA, colorB, n); color = mix(color, colorC, pow(n2, 3.0)); // Vignette float vignette = 1.0 - length(vUv - 0.5) * 1.5; color *= max(0.0, vignette); gl_FragColor = vec4(color, 1.0);}
// Using ShaderMaterial in Three.jsconst shaderMaterial = new THREE.ShaderMaterial({ uniforms: { uTime: { value: 0 }, uResolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) } }, vertexShader: ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: fragmentShaderCode,})// In the animation loopshaderMaterial.uniforms.uTime.value += 0.01
When Antigravity proposed this code, I asked what fbm actually does. The answer: "Fractional Brownian Motion stacks the same noise function at different scales — each pass ('octave') uses double the frequency and half the amplitude. This mimics how natural phenomena work: large-scale shapes with fine-grained detail layered on top, like the texture of clouds or mountain terrain." That kind of contextual explanation accelerates understanding in a way that reading shader tutorials alone doesn't.
Common Pitfalls and How Antigravity Helps
Creative coding has a distinct set of failure modes that don't always produce error messages. Here are the ones I've hit most often, and how Antigravity helped resolve them.
Pitfall 1: WebGL Context Loss
If a tab stays in the background long enough, or GPU resources get constrained, the WebGL context can be lost. The symptom is a black canvas — no error in the console. Asking Antigravity to "handle WebGL context loss gracefully with automatic recovery" produces:
Setting renderer.setPixelRatio(window.devicePixelRatio) without a cap means iPhones with a pixel ratio of 3 render 9× as many pixels. For particle-heavy sketches, this destroys frame rate:
// Cap at 2 — captures Retina benefit without the full performance costrenderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
Pitfall 3: Stale Normal Vectors After Vertex Deformation
When you deform geometry vertices in JavaScript, the normals used for lighting calculations stay at their original values. Lighting then looks wrong — strange flat shading on a curved surface. No error message appears.
// Always call this after modifying vertex positionspositions.needsUpdate = truegeometry.computeVertexNormals() // Easy to forget, breaks lighting silently
When I described this to Antigravity as "the shading looks off after vertex deformation, like it's ignoring the new shape," it identified the missing computeVertexNormals() call immediately.
Pitfall 4: Hidden Performance Bottlenecks in p5.js
When a sketch feels sluggish, frameRate() is the starting diagnostic:
When I shared a slow sketch with Antigravity and asked "what's making this run at 20fps?", it walked me through using Chrome DevTools Performance panel, identified that recreating p5.Graphics objects every frame was the culprit, and suggested caching them. The root cause analysis took about five minutes of back-and-forth.
Publishing and Monetizing Your Work
GitHub Pages and Cloudflare Pages
The simplest path: build with Vite, deploy the static output. Cloudflare Pages gives you a free CDN and instant global distribution:
npm run buildnpx wrangler pages deploy dist --project-name my-generative-art
Asking Antigravity to "set up GitHub Actions CI/CD so every push to main deploys to Cloudflare Pages" produces a complete deploy.yml file along with instructions for setting the required API token secrets in your repository.
OpenProcessing and the Creative Coding Community
OpenProcessing hosts p5.js sketches with an active community. Publishing code there gives you feedback from other creative coders and creates visibility that sometimes leads to collaboration or commission work.
High-Resolution Export for Print and Digital Sales
Generative art can become physical or digital products. The key is exporting at resolutions suited for print (300+ DPI at final print size). In p5.js:
// Export at 4K resolution on 'S' keypressp.keyPressed = () => { if (p.key === 's' || p.key === 'S') { // Render to an offscreen buffer at higher resolution const exportGraphics = p.createGraphics(3840, 2160) drawArtwork(exportGraphics, 3840, 2160) // Refactor drawing logic into a function p.save(exportGraphics, `artwork_${Date.now()}.png`) exportGraphics.remove() }}
Platforms for digital sales include BOOTH for downloadable art files and Redbubble and SUZURI for print-on-demand merchandise.
On AI Co-Creation and Artistic Authorship
There's a question that comes up often when people start using AI for creative work: "Did I really make this?"
I've thought about it. My current answer is that it's the same question painters faced with photography, or musicians faced with synthesizers. The tools change; the creative direction doesn't originate from the tool.
What's different with Antigravity is that the tool is conversational. The refinement process — "more organic," "the color is too bright," "I want this rhythm to feel faster" — is a dialogue. And in that dialogue, the aesthetic judgments come from you.
If anything, using Antigravity for creative coding has shifted more of my attention toward what I actually want to make. The implementation friction is lower, which means more time spent on the harder question of what matters aesthetically.
Start with one small p5.js sketch. Something that just runs. Then refine it in conversation with Antigravity, and see where the dialogue takes you.
For readers who want to go deeper on the technical side, The Coding Train by Daniel Shiffman is an excellent resource for generative art foundations — particularly the sections on autonomous agents and noise, which complement what we've covered here.
Advanced Three.js: Post-Processing and Bloom Effects
Once your base Three.js scene is working, post-processing effects can elevate the visual quality dramatically. Bloom — a glow effect where bright areas bleed into surrounding pixels — is particularly effective for generative art with light-emitting particles or luminous geometry.
Antigravity can scaffold the entire post-processing pipeline when you describe what you're after: "I want a bloom effect on the bright parts of the scene — like glowing neon lights in fog."
npm install three/examples/jsm
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js'// Replace direct renderer calls with a composer pipelineconst composer = new EffectComposer(renderer)// First pass: render the scene normallyconst renderPass = new RenderPass(scene, camera)composer.addPass(renderPass)// Second pass: apply bloomconst bloomPass = new UnrealBloomPass( new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, // Bloom strength 0.4, // Bloom radius 0.85 // Bloom threshold (pixels brighter than this glow))composer.addPass(bloomPass)// In the animation loop, call composer.render() instead of renderer.render()function animate() { requestAnimationFrame(animate) controls.update() composer.render() // Not renderer.render()}
The threshold parameter is the key to controlling what glows. Set it to 0 and everything blooms. Set it higher and only genuinely bright elements — like white or highly saturated geometry — will glow. Asking Antigravity to "adjust threshold and strength to feel like a dark ambient music visualizer" gets you a starting point you can tune from.
Additive Blending for Light Accumulation
For particle systems that simulate light sources — stars, fireflies, energy — additive blending makes particles visually accumulate like real light rather than painting over each other:
const particleMaterial = new THREE.PointsMaterial({ color: 0x8866ff, size: 0.05, transparent: true, opacity: 0.8, blending: THREE.AdditiveBlending, // Particles add light rather than covering depthWrite: false, // Required for transparent additive blending to work correctly})
When I first tried additive blending, the particles turned into flat white patches. Asking Antigravity what was wrong, it explained the depthWrite: false requirement — with additive blending enabled, depth buffer writes cause sorting artifacts that make the blending look broken. This is the kind of non-obvious connection that's hard to find by trial and error alone.
Building a Complete Interactive Installation
To bring everything together, here's a pattern for building an interactive piece that uses all three techniques: p5.js particle physics, Three.js 3D rendering, and a GLSL background shader.
The piece: an audio-reactive installation where sound frequencies drive the deformation of a 3D mesh, with a shader background that pulses with the low frequencies.
Audio Input with the Web Audio API
// Get microphone input and analyze frequenciesasync function initAudio() { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) const audioContext = new AudioContext() const source = audioContext.createMediaStreamSource(stream) const analyser = audioContext.createAnalyser() analyser.fftSize = 256 source.connect(analyser) const dataArray = new Uint8Array(analyser.frequencyBinCount) return { analyser, dataArray }}// In the animation loopfunction animate() { requestAnimationFrame(animate) analyser.getByteFrequencyData(dataArray) // Low frequency energy (bass) const bass = dataArray.slice(0, 10).reduce((a, b) => a + b) / (10 * 255) // Mid frequency energy const mid = dataArray.slice(10, 60).reduce((a, b) => a + b) / (50 * 255) // Drive mesh deformation with bass shaderMaterial.uniforms.uBass.value = bass mesh.scale.setScalar(1 + bass * 0.3) // Drive background color shift with mid shaderMaterial.uniforms.uMid.value = mid controls.update() composer.render()}
Connecting Audio to the Shader
Adding uBass and uMid uniforms to the fragment shader lets the background respond to sound:
This piece would have been difficult to build without AI assistance — not because any individual part is impossibly complex, but because the Web Audio API, Three.js uniforms, and GLSL shader inputs have to wire together correctly. Describing the intent to Antigravity ("I want the mesh to pulse with bass and the background to warm with mid-range frequencies") produced the structural scaffold; the aesthetic tuning came from running it and iterating.
Workflow Patterns That Work
After building several pieces with this approach, a few workflow patterns have proven consistently useful:
Prototype in p5.js, refine in Three.js. p5.js is faster for visual experimentation. Once a motion pattern or color scheme feels right, translating it to Three.js adds dimensionality without losing the original aesthetic.
Use named constants for all aesthetic parameters. When Antigravity proposes code, ask it to extract magic numbers into named constants at the top of the file. This makes tuning sessions faster — you're scanning a parameter list rather than hunting through code.
Describe what you want to feel, not just what you want to see. "I want it to feel restless but not chaotic" gives Antigravity more information than "increase the noise speed." Aesthetic language tends to surface better implementation choices.
Version control your explorations. Good generative art happens through iteration. Commit every version that interests you, with a commit message that notes what changed aesthetically. Six months later, that commit history is how you understand your own creative development.
Build a parameters file. When a piece involves many tunable values, put them in a dedicated params.js file that all sketch files import from. Antigravity can then make targeted suggestions — "try these parameter combinations for a calmer feel" — without needing to scan the full implementation.
The craft of creative coding with AI assistance is mostly about learning how to describe what you want clearly enough that the tool can help. That's a skill that develops with practice, and it turns out to be closely related to developing your own artistic voice.
Share
Thank You for Reading
Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.