Mustafa Bakir – Midterm – GALACTIC

As crazy as it sounds, a big inspiration of this sketch is a song by a not very well known band called fairtrade narcotics. Especially the part that starts around  4:10 , as well this video: Instagram

To toggle modes, press space to change to Galaxy and press Enter to change to Membrane

 

GALACTIC is an interactive particle system built around a single mechanic: pressure. Pressure from the mouse, pressure of formation, and pressure of, holding it together. This sketch is built around the state of mind I had when I first discovered the song in 2022. I played it on repeat during very dark times and it was mending my soul. After every successful moment I had at that time, during my college application period, I would play that specific part of the song and it would lift me to galactic levels. The sketch has 3 modes, the charging modes resembles when I put in a lot of effort into something and eventually it works out which is resembled by the explosion. The second state is illustrates discipline by forming the particles into a galaxy. The last is Membrane which represents warmth and support from all my loved ones.

In the previous blog post, I documented how I created the particle system and the architecture of the project. So, I will resume with the documentation from that point and will now talk about the galaxy mode.

The galaxy implementation started with a question I couldn’t immediately answer: how do you turn drifting particles into something that looks like a spiral galaxy without teleporting them there? My first instinct was to assign each particle a slot on a pre-calculated spiral arm and pull it toward that slot. I wrote assignGalaxyTargets(), sorted particles by their angle from center, matched them to sorted target positions, and felt pretty good about it.

function assignGalaxyTargets() {
  let n = particles.length;

  // build target list at fc = 0 (static, for assignment geometry only)
  let targets = [];
  for (let j = 0; j < n; j++) {
    let gi  = j * (GALAXY_TOTAL / n);
    let pos = galaxyOuterPos(gi, 0);
    targets.push({ gi, x: pos.x, y: pos.y,
                   ang: atan2(pos.y - galaxyCY, pos.x - galaxyCX) });
  }

  // sort particles by current angle from galaxy center
  let sortedP = particles
    .map(p => ({ p, ang: atan2(p.y - galaxyCY, p.x - galaxyCX) }))
    .sort((a, b) => a.ang - b.ang);

  // sort targets by their angle
  targets.sort((a, b) => a.ang - b.ang);

  // assign in matched order → minimal travel distance
  for (let j = 0; j < n; j++) {
    sortedP[j].p.galaxyI = targets[j].gi;
  }

  galaxyAssigned = true;
}

 

I lied. It looked awful. Particles on the right side of the canvas were getting assigned to slots on the left and crossing the entire screen to get there. The transition looked like someone had scrambled an egg.

 

The fix was to delete almost all of that code. Instead of pulling particles toward external target positions, I read each particle’s current position every frame, converted it to polar coordinates relative to the galaxy center, and applied two forces directly: a tangential force that spins it into orbit, and a very weak radial spring that nudges it back if it drifts too far inward or outward. Inner particles orbit faster because the tangential speed coefficient scales inversely with radius. Nobody crosses the canvas. Every particle just starts rotating from wherever it already is.

let tanNX = -rdy / r;
let tanNY =  rdx / r;
let orbSpeed = lerp(1.6, 0.25, constrain(r / GALAXY_R_MAX, 0, 1)) * gt;
this.vx += tanNX * orbSpeed * 0.07;
this.vy += tanNY * orbSpeed * 0.07;

The glow for galaxy mode uses the same concentric stroke circle method from a reference I found: loop from d=0 to d=width, stroke each circle with brightness mapped from high to zero outward. The alpha uses a power curve so it falls off quickly at the edges. The trick is running galaxyGlowT on a separate lerp from galaxyT. The particles start moving into orbit immediately when you press Space, but the ambient halo breathes in much slower, at 0.0035 per frame vs 0.018 for the particle forces. You get the orbital motion first, then the light catches up.

The galaxy center follows wherever you release the mouse. This is made so the galaxy forms where the explosion happens so the particles wrap around the galaxy center in a much more neat way instead of always having the galaxy in the center.
One line in mouseReleased():

galaxyCX = smoothX; galaxyCY = smoothY;

like honestly look how cool this looks now

 

 

The third mode came from a reference sketch by professor Jack that drew 1024 noise-driven circles around a fixed ring. Each circle’s radius came from Perlin noise sampled at a position that loops seamlessly around the ring’s circumference without a visible seam, the 999 + cos(angle)*0.5 trick. The output looks like a breathing cell membrane or a pulsar cross-section.

My first implementation was a direct port: 1024 fixed positions on the ring, circles drawn at each one. It worked but the blob had zero relationship to the particles underneath it. It just floated on top like a decal. Press Enter, blob appears. Press Enter again, blob disappears. The particles had nothing to do with any of it.

The version that actually felt right throws out the fixed ring entirely. Instead of iterating 1024 pre-calculated positions, drawMorphOverlay() iterates over the particle array. Each particle draws one circle centered at its own x, y. The noise seed comes from the particle’s live angle relative to morphCX/CY, so each particle carries a stable but slowly shifting petal radius with it as it moves.

let ang = atan2(p.y - morphCY, p.x - morphCX);
let nX  = 999 + cos(ang) * 0.5 + cos(lp * TWO_PI) * 0.5;
let nY  = 999 + sin(ang) * 0.5 + sin(lp * TWO_PI) * 0.5;
let r   = map(noise(nX, nY, 555), 0, 1, height / 18, height / 2.2);

The rendered circle size scales by mt * p.life * proximity. Proximity is how close the particle sits to the ring. Particles clustered at the ring draw full circles. Particles still traveling inward draw small faint ones. When you activate morph mode, the blob coalesces as particles converge. When you deactivate it, the blob tears apart as particles scatter outward, circles traveling with them. The disintegration happens at the particle level, not as a fading overlay.

The core glow stopped rendering at a fixed point too. It now computes the centroid of all particles within 2x the ring radius and renders there. The glow radius scales by count / particles.length, so a sparse ring is dim and a dense ring is bright. The light follows the mass.

 

Originally I had Space and Enter both cycling through modes in sequence: bio to galaxy to membrane and back. That made no sense for how I actually wanted to use it. Space now toggles bio and galaxy. Enter toggles bio and membrane. If you’re in galaxy and press Enter, galaxyT starts lerping back to zero while morphT starts lerping toward one simultaneously. The cross-fade between two non-bio modes works automatically because both lerps run every frame regardless of which mode is active.

if (keyCode === 32) {
  currentMode = (currentMode === 1) ? 0 : 1;
} else if (keyCode === 13) {
  currentMode = (currentMode === 2) ? 0 : 2;
  if (currentMode === 2) morphAssigned = false;
}

morphAssigned = false triggers the angle re-sort on the next frame, which maps current particle positions to evenly spaced ring angles in angular order. Same fix as the galaxy crossing problem: sort particles by angle from center, sort targets by angle, zip them in order. Nobody crosses the ring.

The sketch now has three fully functional modes with smooth bidirectional transitions. The galaxy holds its own as a visual. The membrane is the most satisfying of the three to toggle in and out of because the disintegration is legible. You can watch individual particles drag pieces of the blob away as they scatter.

I still haven’t solved the performance question on lower-end hardware. The membrane mode in particular runs 80 draw calls per particle at full opacity in additive blending, which is not nothing. My next steps are profiling this properly and figuring out whether the p5 web editor deployment is going to survive it. I’m cautiously optimistic but I’ve been cautiously optimistic before.

I faced many challenges throughout this project. I will list a couple below.

  • The trails of the particles
  • The explosion not being strong enough
  • The behavior of pulling particles
  • Performance issues
  • The behavior of particles explosion
  • The Texture of galaxy (scrapped idea)

and honestly I could go on for days.

The thing that worked the best for me is that I started very early and made a lot of progress early so I had time to play around with ideas. Like the galaxy texture idea for example from the last post, I had time to implement it and also scrap it because of performance issues. I also tried to write some shader code but honestly that went horribly and I didn’t want to learn all of that because the risk margin was high. Say I did learn it and spend days trying to perfect it and end up scrapping the idea. I also didn’t want to generate the whole shaders thing with AI, I actually wanted to at least undestand whats going on.

The most prominent issue was how the prints were going to look like as I didn’t know how to beautifully integrate trails as they looked very odd. I played around with the transparency of the background with many values until I got the sweet spot. My initial 3 modes were attract, condense, and explode but that wouldn’t be conveyed well with the prints so I switched to the modes we have right now.

Reflection

Honestly the user experience is in a better place than I expected it to be at this stage. The core loop, hold to charge, release to detonate, turned out to be one of those interactions that people understand immediately without any instructions, bur I can’t say the same about pressing Enter and Space to toggle around between modes haha. I’ve watched a few people pick it up cold and within thirty seconds they’re already testing how long they can hold before releasing. That’s a good sign. When an interaction teaches itself that quickly, you’ve probably found something worth keeping.

The three modes add a layer of depth that I wasn’t sure would land. Galaxy mode feels the most coherent because the visual logic is obvious: particles orbit a center, a halo breathes outward, the whole thing rotates slowly. Membrane mode is more abstract and I think some people will find it confusing on first contact. The blob emerging from particle convergence reads as intentional once you’ve seen it a few times, but the first time it happens it might just look like a bug. That’s a documentation problem as much as a design problem. A very subtle UI hint, maybe a faint key label in the corner, might do enough work there without breaking the aesthetic.

The transition speeds feel right in galaxy and a little slow in membrane. When you press Enter to leave membrane mode, the blob takes long enough to dissolve that it starts feeling like lag rather than a designed dissolve. I want to tighten the MORPH_LERP value and see if a slightly faster exit reads better while keeping the entrance speed the same. Entering slow, leaving fast, might be the right rhythm for that mode.

Performance is the thing I’m least settled about. On my machine in VS Code the sketch runs clean. The membrane mode specifically concerns me because it runs one draw call per particle per frame in additive blending, and additive blending is expensive in ways that only become obvious at 600 particles so it’s a little bit slower there.

The one thing I genuinely would love to add is audio. Not full sound design, something minimal. A low frequency hum that rises in pitch as charge builds, a short percussive hit on release scaled to the explosion tier. The sketch is very silent right now and I think sound would close a gap in the experience that visuals alone can’t. The charge accumulation in particular has this tension that wants a corresponding audio texture.

The naming situation I mentioned at the start, Melancholic Bioluminescence sounding like a Spotify playlist, has not resolved itself. If anything the addition of galaxy mode and the membrane makes the name less accurate. The name now is GALACTIC

REFERENCES

p5.js Web Editor | 20260211-decoding-nature-w4-blob-example

p5.js Web Editor | galaxy

How would you generate a nebula/galaxy image using p5.js ? (e.g something like the following image) : r/generative

Inigo Quilez :: computer graphics, maths, shaders, fractals, demoscene

Instagram Video 

Also yes, AI helped with a lot of the math again. The Keplerian orbital speed scaling, the seamless noise ring sampling, the proximity weighting in the blob. I understand what all of it does now though, which I count as a win. I use AI not do my work, but as a tool that helps me get to what I want as a mentor. I think I am very satisified with this output as I built the architecture, I build the algorithms, I designed everything beforehand and when things felt stuck I used AI as my mentor. I think the section where I used AI the most is filling in a lot of values to for things cus I couldn’t get values that felt nice. Here’s an example below.

class Particle {
  constructor(x, y) {
    this.pos = createVector(x, y);
    this.vel = createVector(0, 0);
    this.nox = random(10000);
    this.noy = random(10000);
    this.ns = random(0.0015, 0.004);
    this.driftSpd = random(0.5, 1.2);
    this.baseSize = random(1.8, 4);
    this.size = this.baseSize;
    this.baseHue = random(228, 288);
    this.hue = this.baseHue;
    this.sat = random(55, 85);
    this.bri = random(75, 100);
    this.alpha = random(40, 70);
    this.maxAlpha = this.alpha;
    this.life = 1;
    this.dead = false;
    this.wobAmp = random(0.3, 0.9);
    this.wobFreq = random(2, 4.5);
    this.orbSpd = random(0.015, 0.04) * (random() > 0.5 ? 1 : -1);
    this.drag = random(0.93, 0.97);
    this.explSpd = random(0.6, 1.4);
    this.rotDrift = random(-0.35, 0.35);
    this.absorbed = false;
    this.trailTimer = 0;
    this.suctionTrailTimer = 0;
    this.behavior = BEHAVE_RADIAL;
    this.spiralDir = random() > 0.5 ? 1 : -1;
    this.spiralTight = random(0.03, 0.09);
    this.boomerangTimer = 0;
    this.boomerangPeak = random(0.3, 0.5);
    this.flutterFreqX = random(5, 12);
    this.flutterFreqY = random(5, 12);
    this.flutterAmp = random(2, 6);
    this.cometTrailRate = 0;
    this.explodeOrigin = createVector(0, 0);
    this.morphAngle = random(TWO_PI);
  }

 

Midterm progress – Mustafa Bakir

My main inspiration came from this video: Instagram

 

For this project, I chased the feeling of something between a pulsar and a deep-sea creature. The working title became Melancholic Bioluminescence, which sounds like a Spotify playlist but the fun thing about creating projects is having full authority and ownership over everything and its my sketch so I’ll name it that.

The core interaction is simple and satisfying to say out loud: hold your mouse down, energy accumulates, release it to detonate. Hold longer, bigger explosion. That’s the entire loop. What makes it interesting is the texture of the accumulation. Particles spiral inward like a black hole, and the glow resembles energy accumulating within that blackhole.

Before writing a single line, I sketched the architecture on paper. The system has three major layers of responsibility: the global state (are we holding? are we exploding? what’s the charge level?), the particle population (a pool of objects that each manage their own physics), and the vfx (trails, embers, glow pulses, which are short-lived visual elements that don’t need the full particle class). I accumulated my notes and compiled them into a beautiful psuedocode that I can follow, this is me abusing what I learned taking Data Structures and honestly desgining the system beforehand works for me really well

please check the pdf for the psuedcode because there’s this ANNOYING issue that no matter whats the scale of the screenshot I am uploading its always so blurry and small.

decoding_naturfe (1)

I also want to disclose that AI helped me with many mathematical sections within this sketch, I wouldn’t be able to understand the math or get around it on my own I think. But I promise my usage is not excessive or dependent and I actually use it to learn haha

Anyway, I started writing attributes for the particle class and oh boy they added up QUICKLY. Here’s a snippet. I tried assigning random values manually but it was very very hard to find the sweet spot for everything so I used some help from AI to assign the proper values to those attributes and I tweaked them a little bit and got really good results.

class Particle {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.vx = 0; this.vy = 0;
    this.nox = random(10000);
    this.noy = random(10000);
    this.ns  = random(0.0015, 0.004);
    this.driftSpd = random(0.5, 1.2);

    this.baseSize = random(1.8, 4);
    this.size     = this.baseSize;

    this.baseHue = random(228, 288);
    this.hue     = this.baseHue;
    this.sat     = random(55, 85);
    this.bri     = random(75, 100);
    this.alpha   = random(40, 70);
    this.maxAlpha = this.alpha;

    this.life    = 1;
    this.dead    = false;
    this.wobAmp  = random(0.3, 0.9);
    this.wobFreq = random(2, 4.5);
    this.orbSpd  = random(0.015, 0.04) * (random() > 0.5 ? 1 : -1);
    this.drag    = random(0.93, 0.97);
    this.explSpd = random(0.6, 1.4);
    this.rotDrift = random(-0.35, 0.35);
    this.absorbed = false;
    this.trailTimer       = 0;
    this.suctionTrailTimer = 0;

    this.behavior     = BEHAVE_RADIAL;
    this.spiralDir    = random() > 0.5 ? 1 : -1;
    this.spiralTight  = random(0.03, 0.09);
    this.boomerangTimer = 0;
    this.boomerangPeak  = random(0.3, 0.5);
    this.flutterFreqX = random(5, 12);
    this.flutterFreqY = random(5, 12);
    this.flutterAmp   = random(2, 6);
    this.cometTrailRate = 0;
    this.explodeOriginX = 0;
    this.explodeOriginY = 0;
  }

 

A useful frame for interactive generative art is the state machine. This sketch has three primary states that produce visually distinct experiences, and the transitions between them are where most of the design work happened.

Idle state: No mouse interaction. 80 particles drift across the canvas on Perlin noise. Each particle has its own noise offset, frequency, and speed. The result is slow, organic, slightly hypnotic. The palette sits in the 228-288 HSB hue range (blue through violet) and particles breathe gently at a rate of 2 cycles per second. This is the sketch’s resting face, and it needs to be beautiful enough to watch on its own.

Charging state: Mouse held. New particles spawn at the edge of the screen and get pulled toward the cursor which acts as an attractor. Spawn rate accelerates from 1/frame to 18/frame as charge approaches maximum. The vortex arms appear past 8% charge: three logarithmic spirals that rotate faster as charge builds, drawn with beginShape()/vertex() and per-vertex stroke colors that fade toward the outer edge. The glow orb grows around the cursor. Screen rumble starts at 60% charge. Particles near the cursor compress and brighten. The hue of nearby particles shifts toward 305, a hot magenta-violet. Every visual element does the same narrative work: energy is accumulating.

Explosion state: Mouse released. This is tiered across four discrete levels (0 through 3) based on charge thresholds at 0.25, 0.55, and 0.85. Tier 0 is a gentle push. Tier 3 is a white-flash, screen-shake, 800-pixel-radius detonation that spawns up to 70 child particles from split candidates nearest the blast origin. Each particle in blast range gets a force vector calculated from distance falloff (pow(1 - d/blastRadius, 2)), a random rotation drift, and a behavior assignment weighted by proximity to center and charge level. The explosion duration scales with charge, from 1.4 seconds to 4 seconds. The slowdown at high charge gives full-tier explosions a cinematic quality: the cloud expands, holds, then dissipates.

The variation space this produces is wide. A quick series of light taps creates a dotted constellation. Holding in one place while moving slightly creates smeared, comet-like trails. A patient full charge, held long enough to feel the rumble, produces a different kind of satisfaction.

the scariest things about this project were two things braided together: performance under additive blending with 700+ particles, and making the multi-behavior explosion feel coherent rather than random.

Additive blending (blendMode(ADD)) is visually spectacular. Overlapping particles bloom into white rather than muddy brown. The cost is real though: each ellipse composites against everything underneath it. With three ellipses per particle (the outer glow halo, the mid-glow body, and the bright core), plus trail objects, plus embers, a naive implementation at 700 particles hits framerate problems fast. The risk was a beautiful system running at 20fps. I ran many optimization processes but then I migrated to VS code which was MUCH smoother but I don’t know how smart that is going to be because in the end I’m gonna have to embed the sketch in p5.js web editor so it wouldn’t make sense or a difference that it runs smoothy on my device but its laggy on the website.

The mitigation strategy relies on hard caps with graceful degradation. Particles cap at 600 during charging and 700 overall. Trails cap at 1200 objects. Embers die slowly at life -= 0.003 per frame, about 333 frames of life. The three-ellipse draw call per particle uses deliberately low-resolution sizes: the outer halo is s*4, the body s*2, the core s*0.6, where s is typically 1.8-4 pixels. The glow effect comes from accumulation of tiny translucent shapes. The drawGlow() function uses 50 layered ellipses for the cursor glow, each with an alpha under 4, nearly invisible on their own.

For the behavior system, the risk was that five different particle behaviors during explosion would read as a mess of conflicting physics. To test this, I implemented behaviors one at a time and ran the explosion at full charge with only that behavior active, watching whether each produced a readable visual signature. BEHAVE_COMET needed the highest speed and the lowest drag (0.99 vs the standard 0.93-0.97) to produce visible streaks. BEHAVE_BOOMERANG needed the timer offset: if the return force kicked in immediately, particles just wobbled. They needed to actually leave the origin first. BEHAVE_FLUTTER was the most unpredictable and required the dampening multiplier (vx *= 0.985) to prevent runaway acceleration from the oscillating force.

The assignBehavior() method’s probability table weights behavior by charge level and proximity to blast center. Close-in particles at high charge get COMET and SPIRAL; far particles get RADIAL and FLUTTER. This creates a natural visual structure: a dense bright core of fast-moving comets surrounded by a slowly oscillating outer cloud. The explosion has a center and a periphery, which reads as physically plausible even though the physics are entirely invented.

The remaining uncertainty heading into the midterm is whether the vortex spiral arm rendering, which uses nested beginShape()/endShape() with per-vertex stroke calls, holds up on lower-end hardware. The core mechanic, the charge-and-release loop, and the explosion tier system all feel solid. The scary part is mostly tamed.

But there comes another problem. I don’t like the black background so I decided to create a galaxy background. I had a rough idea how to make it but I had to do some research.

Guess what, all of those links were useless. I found nothing of help but I didn’t want to give up. so I found this reddit post and I found this page  and I follwed the principles and methods in the article to create something cool.

I tried to upload the gif of the animated image but I got this error so I will just upload a screenshot unfortunately.

I really dont know why the resolution is so low my monitor is 4k resolution and honestly it’s too late for me to worry about this. Anyway, I would love to go with the galaxy design but unfortunately it lags like HELL even on VS code, maybe I’ll book office hours and see how I can troubleshoot this.

my next steps for this is to figure out the background and also try to replicate the main inspiration video because right now everything feels flat and I am starting to hate it.

 

Assignment 4: Harmonic Motion

My inspiration comes from Memo Akten’s Simple Harmonic Motion 8-9.  I wanted to build something that felt alive on screen. Waves have always fascinated me, specifically the way two simple oscillations combine into something unexpectedly complex. I used the principles of adding waves together such that: peak + peak = higher peak, trough + trough = lower trough, and peak + trough cancel each other.

Please move mouse around and click around the sketch for special effects.

I started with the simplest possible thing: a single sine wave drawn as dots across the canvas.

I added a second wave with a different frequency and a phase offset of PI, so it starts on the opposite side of the cycle from wave one. The two waves drift in and out of sync as time moves forward. Wave two has a slightly higher frequency and a smaller amplitude, so it has its own distinct character.

This is where things got interesting. I added a third wave that is the sum of the first two. When the two source waves push in the same direction, the result amplifies. When they oppose each other, they cancel out. The yellow interference wave is the visual record of that conversation between the two.

let y3 = y1 + y2;
stroke(255, 220, 80);
strokeWeight(5);
point(x, height / 2 + y3);

 

I replaced the solid background() call with a semi-transparent rectangle drawn each frame. The old frames fade slowly instead of disappearing instantly.

fill(10, 10, 20, 25);
noStroke();
rect(0, 0, width, height);

I replaced fixed amps with values that oscillate slowly over time using a second, slower sine function.

let amp1 = map(sin(t * 0.3), -1, 1, 40, 130);
let amp2 = map(sin(t * 0.17 + PI), -1, 1, 30, 110);

The two amplitudes breathe at different rates, so they are never in sync with each other. The interference wave gets dramatically more expressive as a result, going nearly flat at times and spiking wide at others.

 

I switched to HSB color mode and tied the hue and brightness of each point to its position in the wave cycle.

let hue1 = map(sin(x * 0.01 + t * 0.5), -1, 1, 160, 220);
let bright1 = map(abs(y1), 0, amp1, 60, 100);

I expanded from two base waves to four, each with its own frequency, speed, phase, and breathing amplitude. I also added two partial interference sums alongside the full sum of all four.

The frequency ratios across the four waves are close to harmonic but slightly off, so the pattern never fully repeats. There is always something new happening somewhere on the canvas.

let yA    = ys[0] + ys[1];
let yB    = ys[2] + ys[3];
let yFull = ys[0] + ys[1] + ys[2] + ys[3];

 

At this point the draw() loop was getting hard to read. I pulled the repeating logic into dedicated functions. All wave parameters moved into a waveDefs array of objects.  I also moved the array definition inside setup().

 

function waveY(x, def, amp) { ... }
function breathingAmp(i) { ... }
function driftHue(x, hueMin, hueMax, freqX, freqT) { ... }
function drawWavePoint(x, y, centerY, hue, sat, maxAmp, weight, alpha) { ... }
function drawSumPoint(x, y, centerY, maxDisplace, hueMin, hueMax, sat, weight, alpha) { ... }
 This is the highlight of code im proud of.
  ampPhases = [0, PI, HALF_PI, PI / 3];

  waveDefs = [
    { freqX: 0.020, freqT: 1.0, phase: 0,        ampMin: 40, ampMax: 130, hueMin: 160, hueMax: 220, weight: 3 },
    { freqX: 0.035, freqT: 1.5, phase: PI,        ampMin: 30, ampMax: 110, hueMin: 260, hueMax: 320, weight: 3 },
    { freqX: 0.055, freqT: 0.7, phase: HALF_PI,   ampMin: 20, ampMax:  80, hueMin: 100, hueMax: 160, weight: 2 },
    { freqX: 0.013, freqT: 2.0, phase: PI / 4,    ampMin: 15, ampMax:  60, hueMin:  20, hueMax:  60, weight: 2 },
  ];
}

function waveY(x, def, amp) {
  return sin(x * def.freqX + t * def.freqT + def.phase) * amp;
}

function breathingAmp(i) {
  return map(sin(t * ampSpeeds[i] + ampPhases[i]), -1, 1, waveDefs[i].ampMin, waveDefs[i].ampMax);
}

function driftHue(x, hueMin, hueMax, freqX, freqT) {
  return map(sin(x * freqX + t * freqT), -1, 1, hueMin, hueMax);
}

function drawWavePoint(x, y, hue, sat, maxAmp, weight, alpha) {
  let bright = map(abs(y), 0, maxAmp, 55, 100);
  stroke(hue, sat, bright, alpha);
  strokeWeight(weight);
  point(x, height / 2 + y);
}

function drawSumPoint(x, y, maxDisplace, hueMin, hueMax, sat, weight, alpha) {
  let hue = map(y, -maxDisplace, maxDisplace, hueMin, hueMax);
  let bright = map(abs(y), 0, maxDisplace, 60, 100);
  stroke(hue, sat, bright, alpha);
  strokeWeight(weight);
  point(x, height / 2 + y);
}

I added a mousePull() function that bends each wave point toward the cursor. The pull strength falls off with distance, reaching zero at 200 pixels away. Moving the mouse slowly across the canvas bends the waves toward it in a way that feels physical. The effect fades naturally so it never feels abrupt. The final step was a Ripple class. Clicking spawns a ring that expands outward from the click position and displaces any wave point it passes through. The ring has a bandwidth of 40 pixels. Points inside that band get displaced, everything outside it is untouched. The ripple fades as it expands and gets removed from the array once it exceeds its maximum radius. Multiple ripples can coexist and their displacements stack on top of each other.

function mousePull(x, baseY, centerY) {
  let dist = sqrt(dx * dx + dy * dy);
  let influence = constrain(map(dist, 0, 200, 60, 0), 0, 60);
  return dy * (influence / max(dist, 1)) * -1;
}


class Ripple {
  constructor(x, y) {
    this.radius = 0;
    this.speed  = 4;
    this.maxRadius = 300;
    this.strength  = 55;
  }

  influence(px, py) {
    let ring = abs(dist - this.radius);
    if (ring > 40) return 0;
    let fade    = map(this.radius, 0, this.maxRadius, 1, 0);
    let falloff = map(ring, 0, 40, 1, 0);
    return sin(ring * 0.3) * this.strength * fade * falloff;
  }
}

 

For future improvements, I want to make the waves responsive to music/notes. I think this can be done by using Fast Fourier Transform which I’ve used and written a paper about before in one of my INTRO TO IM projects. it would work by processing the whole audio file once then storing the data in a queue and mapping each frequency band to an intersection between waves. I really wanna try this but it seems a little bit challenging so I didnt implement it in this sketch.

Assignment 3 – Kaleidoscope

This sketch is a kaleidoscopic illustration of movers and attractors. My main inspiration is a combination of the picture above and a mix of effects I used in one of my motion graphics/VFX projects. I wanted to recreate a sample that captures both feelings at once.

Black White Kaleidoscope Background Abstract Animated

 

 

I started this sketch by placing circular attractors as shown in the image.

I wrote code that creates a circular arrangement of attractor points around a center position.

  for (let i = 0; i < numAttractors; i++) {
    let angle = (TWO_PI / numAttractors) * i;
    let x = centerX + cos(angle) * attractorRadius;
    let y = centerY + sin(angle) * attractorRadius;
    attractors.push(new Attractor(x, y, 20));
  }
}

This loop divides the circle into segments, each iteration multiplies by i to get the angle for that specific attractor. For x and y positions, I use polar-to-cartesian coordinate conversion as shown in the code.

Then I added movers and I applied the forces to the movers such that their position is affected by the attractors.

For this effect I take inspiration from this code.

So basically, the kaleidoscope effect happens because every single object gets drawn (numAttractors) times instead of just once. The trick is that before drawing each copy, the code rotates the entire coordinate system around the center of the canvas by increments of 60 degrees (since 360 deg ÷ 6 = 60 deg). It does this by moving the origin to the canvas center, rotating, then moving it back, and finally drawing the object at its actual position. But because the whole coordinate system has been rotated, what you actually see is the object appearing in six different spots arranged symmetrically around the center, like looking through a kaleidoscope. The cool part is that the physics simulation itself, only happens once for each object, but visually you see six reflections of everything. So when a mover orbits an attractor, all six of its mirror images orbit simultaneously.

 

For example, the number of movers in this sketch is 1, but it gets reflected 5 times

 

Here’s the code responsible for the kaliedoscope effect

for (let i = 0; i < symmetryFold; i++) {
  push();
  translate(width / 2, height / 2);      // move origin to center
  rotate((TWO_PI / symmetryFold) * i);   // rotate by incremental angle
  translate(-width / 2, -height / 2);    // move origin back
  circle(this.position.x, this.position.y, size);  // draw at original position
  pop();
}

I then made some really interesting changes here that make the pattern way more complex and controlled. I added this clever touch of four “corner attractors” placed at each corner of the canvas with higher mass , which act like gravitational anchors that influence the overall flow and create an effect of repulsion. I also added a visibility toggle (showAttractorsRepeller) so I can hide the attractors if I just want to see the trails. Speaking of trails, I changed the background to fade super slowly with background(0, 1) instead of clearing completely each frame, which creates these motion trails that build up over time. And finally, I made the movers wrap around the screen edges, if one goes off the right side it reappears on the left, which keeps the pattern continuous and prevents movers from escaping the canvas entirely.

Then I hit the jackpot. By changing one of the corner attractors mass, I create really interesting kaleidoscopic effects as shown in the gif below
Then by playing with the number of attractors I achieve really compelling visual effects like this

Sketch

To get the most out of this sketch, please manipulate the parameters I indicated at the top of the code. There’s something so super satisfying with watching those patterns slowly get created.

Assignment 2 documentation – Mustafa Bakir

What inspired this project is my fish, Abdulfattah, a Betta fish that was gifted to me from a very dear person to my heart.

This sketch mimics natural fish movement when it spots food. The user moves the mouse around and upon clicking you essentially drop a fish food pellet, and the fish mimics natural movement and acceleration towards food.

I first started t project by adding a background I found online

Blue water surface template in cartoon style illustration

then I made a rectangle that will later be a fish to start simulating it’s  movement.

then I wrote very simple code with if statements just as a base for my fish movement.

 

then I updated my code to lerp (linear interpolation) towards the mouse.

new_pos= current_pos + (target_pos – current_pos) * speed

 

After that I started implementing dropping fish pellets, I started with creating a simple function to spawn pellets with an array of circles.

let circles[];

  //in draw
  for (let circle of circles) {
    fill(139, 69, 19); // brown
    noStroke();
    ellipse(circle.x, circle.y, 20, 20);

function mousePressed() {
  circles.push({x: mouseX, y: mouseY});
}

Then I added a sinking motion for the pellets with a slight drift using the sin function to simulate natural waves pushing pellets around as they are sinking with a random angle for variation.

// draw food pellets
for (let i = circles.length - 1; i >= 0; i--) {
  let circle = circles[i];
  
  // make circle float downwards
  circle.y += circle.speed;
  
  // increment the angle for sin
  circle.angle += 0.05;
  
  // subtle drift for food pelletes
  let drift = sin(circle.angle) * 20;
  
  // draw circles
  fill(139, 69, 19);
  noStroke();
  ellipse(circle.x + drift, circle.y, 20, 20);

 

then I edited the code to make the rectangle follow the pellets instead of the mouse. For error handling, the pellets are put in a stack such that the rectangle eats the pellets using a queue as a data structure following the FIFO (first in last out) principle.

 

A challenge I faced:

As shown in the previous gif, there was a problem where the rectangle can’t eat the food because the pellets are drifting and sinking. This is happening because the rectangle is trying to go the position of that circle but it doesn’t account for the drifting and sinking, therefore, its more like it’s tracing the pellet rather than trying to catch it.

The way I tackled this challenge is also a highlight of the code that I’m proud of.

.I fixed this by predicting where each pellet would be 10 frames ahead, accounting for sinking and horizontal drift. I calculated the direction vector and euclidean distance to this predicted target, then normalized it to apply acceleration forces. The velocity builds gradually but it’s not fully smooth yet but I’ll get to that later. I multiplied velocity by 0.92 each frame for friction, cap the maximum speed, then update position. When the fish gets within 30 pixels of a pellet, it gets removed from the queue with shift() and reduce velocity by 70%. This creates a deceleration effect before accelerating toward the next pellet.

function followCircles(){
  // if there are circles, follow the first one following FIFO principle
  if (circles.length > 0) {
    let target = circles[0];
    
    // predict where the pellet will be
    let futureY = target.y + target.speed * 10;
    let futureAngle = target.angle + 0.03 * 10;
    let futureDrift = sin(futureAngle) * 1.5;
    
    let targetX = target.x + futureDrift; // i add the predicted position here so the fish can catch the food
    let targetY = futureY;
    
    // calculate direction to predicted position
    let dx = targetX - x;
    let dy = targetY - y;
    let distance = dist(x, y, targetX, targetY); // calculate the eucilidian distance of the fish and the pellet
    
    // normalize direction and apply acceleration
    if (distance > 0) {
      vx += (dx / distance) * acceleration;
      vy += (dy / distance) * acceleration;
    }
    
    // apply friction for more natural movement
    vx *= friction;
    vy *= friction;
    
    // limit speed
    let speed = dist(0, 0, vx, vy);
    if (speed > maxSpeed) {
      vx = (vx / speed) * maxSpeed;
      vy = (vy / speed) * maxSpeed;
    }
    
    // update position with velocity
    x += vx;
    y += vy;
    
    // check if rectangle is touching the circle 
    let actualDistance = dist(x, y, target.x + target.drift, target.y);
    if (actualDistance < 30) {
      circles.shift(); // eat the circle
      // reduce velocity when eating 
      vx *= 0.3;
      vy *= 0.3;
    }
  } else {
    // slow down when no target
    vx *= 0.9;
    vy *= 0.9;
  }
}

another problem I faced was the fish was getting stuck around the corners but I easily fixed that but implementing a function that lets the fish wander using perlin noise (Thanks professor Jack!!)

Then I added the fish’s sprite from this website.

Future improvements and reflection:

I want to have a simulation of the background such that it actually simulates fluids and looks like water. I also have 3 more fish in the tank that are Abdulfattah’s friends that I want to add, they always steal his food so it would be more realistic that way. Moreover I want to have a bigger canvas so I can add object you naturally find in the ocean or the one’s I have in my fish tank, and an algorithm that makes the fish occasionally hide behind those objects. Lastly, I want an algorithm where  sometimes the fish just rests in the tank doing nothing, which would add more realism to the sketch.

Reading response – Mustafa

This chapter hit me hard. Flake asks the question I’ve been circling around for years: why can’t we predict everything if we understand the parts? The ant colony example nails it. You can study individual ants all day long. You can catalog their behaviors, map their neural pathways, document their castes. You still won’t predict the emergent sophistication of millions of them working together.

I love how Flake frames computation as nature’s language. We’ve been so focused on dissecting things that we forgot to ask “what does this do?” instead of “what is this?” That shift feels enormous. A duck isn’t just feathers and bones. A duck migrates, mates, socializes, adapts. The doing matters as much as the being.

The pdf’s structure fascinates me. Fractals lead to chaos, chaos leads to complex systems, complex systems lead to adaptation. Everything connects. I’ve read excerpts on chaos theory. I’ve read excerpts on evolution. I’ve never seen someone draw the lines between them so clearly. Simple rules, iterated over time, create everything from snowflakes to stock markets.

The timing matters too. Flake wrote this in 1998, right when computers stopped being tools and became laboratories. We could finally simulate systems too complex to solve analytically. That changed everything. Meteorologists, economists, biologists: they all started speaking the same computational language.

What gets me is the humility baked into this approach. Reductionism promised we could know everything by breaking it down far enough. Flake shows us the limits. Some things are incomputable. Some systems defy long-term prediction. Understanding quarks doesn’t help a doctor diagnose patients. Understanding individual neurons doesn’t explain consciousness.

Mustafa Bakir – Assignment 1 – QU4NT0M W4LK

This sketch was inspired by Quantum Cloud by Antoney Gormley 

Sources utilized: (514) Coding Challenge #112: 3D Rendering with Rotation and Projection – YouTube

Concept

I was talking to my friend Youssab William about decision trees, specifically binary decision trees and the thought lingered in my head for a little bit. Then when I started reading the assignment prompt, I read about the 3D walk and say Quantum Cloud. I got an idea of merging the logarithmic growth of binary trees with the random walk on a 3D plane in quadrantal angles. This is where QU4NT0M W4LK was born.

Also, im a big big fan of glitching and RGB offsets as a motion designer and video editor. I include RGB offsets (appropriately) in almost all my projects, so I also loved to add it here.

function draw() {
  background(255);

  // track rotation changes from dragging
  if (mouseIsPressed) {
    let deltaX = mouseX - pmouseX; // built in coords
    let deltaY = mouseY - pmouseY;
    rotY += deltaX * 0.01; // have to manually track rotation cus its not built in
    rotX += deltaY * 0.01;
  }
  
  // calculate rotation delta for RGB offset
  let deltaRotX = rotX - prevRotX;
  let deltaRotY = rotY - prevRotY;
  let deltaZoom = zoom - prevZoom;
  
  // update RGB offsets based on camera rotation and zoom
  blueOffset = lerp(blueOffset, deltaRotY * 500, 0.15);   // horizontal drag -> Blue
  greenOffset = lerp(greenOffset, deltaRotX * 500, 0.15); // vertical drag -> Green
  redOffset = lerp(redOffset, deltaZoom * 100, 0.15);     // zoom -> Red
  
  // decay offsets back to zero when not moving
  blueOffset *= 0.92;
  greenOffset *= 0.92;
  redOffset *= 0.92;
  
  // store previous values
  prevRotX = rotX;
  prevRotY = rotY;
  prevZoom = zoom;

  // apply camera transformations
  scale(zoom);
  rotateX(rotX);
  rotateY(rotY);

I am particularly proud of this segment of the code as everything else was something I have done before. But this camera movement was something I thought was extremely complex. What motivated me to embark on this idea is my recent project in motion graphics with camera movements that I genuinely was proud of, so I wanted to make camera movements I’m proud of using P5 as well. Both instances were something I was dreading before but once I started working on them they turned out to be much much simpler than I thought.

 

Future considerations would optimization. because of the fast growth of the segments, I had to decrease the number of segments. it looks much cooler with more segments, but I assume, i dont know yet, but I assume that the RGB channels are always drawn which technically *3’s the amount of processing. Another thing I wanna try but i’m dreading ins implementing dynamic 3d lighting with shadows and highlights, but maybe in future assignments.

 

Resubmission

 

In this resubmission, I edited the walk such that x walks in the red channel, y walks in the green channel, and blue walks in the blue channel.