Wired Magazine Editorial Illustration

Wired Magazine

An interactive editorial illustration to Mattha Busby’s WIRED article, “Human Design Is Blowing Up. Following It Might Make You Leave Your Spouse.” Built with code, the illustration shows a field of eyes that react to the cursor’s hover state, turning interaction into a the human design types for self analysis, emotional projection, and the way belief systems shape how people see themselves and their partners.


let eyes = [];

let eyeScale = 0.75;

const STATE = { OVERVIEW: 0, ACTION: 2 };

let app = {

state: STATE.OVERVIEW,

focusedIndex: -1,

cam: { x:0, y:0, s:1, tx:0, ty:0, ts:1, ease:0.16 },

effect: null,

removeAfter: -1,

vignetteStrength: 0.0

};

const DURS = { run:1400, explode:1200, melt:1800, break:1500, burn:1800 };

const EFFECT_BY_TYPE = {

"MANIFESTOR": "burn",

"MANIFESTING GENERATOR": "run",

"GENERATOR": "explode",

"PROJECTOR": "break",

"REFLECTOR": "melt"

};

function setup() {

createCanvas(1200, 800);

eyes.push(new Eye1_Grid("PROJECTOR"));

eyes.push(new Eye2_Fraser("REFLECTOR"));

eyes.push(new Eye3_Ribbons("MANIFESTOR"));

eyes.push(new Eye4_Generator("GENERATOR"));

eyes.push(new Eye5_ManGen("MANIFESTING GENERATOR"));

for (const e of eyes) e.fx = null;

noSmooth();

toOverviewImmediate();

}

function windowResized() {

toOverviewImmediate();

}

function draw() {

background(0);

const c = app.cam;

c.x = lerp(c.x, c.tx, c.ease);

c.y = lerp(c.y, c.ty, c.ease);

c.s = lerp(c.s, c.ts, c.ease);

const positions = getGridPositions(eyes.length);

handleHoverInteraction(positions);

push();

translate(width/2, height/2);

scale(c.s);

translate(-c.x, -c.y);

for (let i = 0; i < eyes.length; i++) {

const p = positions[i];

if (eyes[i].fx) {

push();

translate(p.x, p.y);

const done = runSecondaryEffect(eyes[i], { x:0, y:0 }, eyes[i].fx);

pop();

if (done) eyes[i].fx = null;

} else {

eyes[i].drawAt(p.x, p.y, eyeScale);

}

}

pop();

}

function handleHoverInteraction(positions) {

const world = screenToWorld(mouseX, mouseY);

const mx = world.x, my = world.y;

for (let i = 0; i < eyes.length; i++) {

const p = positions[i];

const baseW = 240 * eyeScale;

const baseH = baseW * 0.52;

const dx = mx - p.x, dy = my - p.y;

const h = (dxdx) / sq(baseW/2) + (dydy) / sq(baseH/2);

if (h <= 1 && !eyes[i].fx) {

eyes[i].fx = makeEffectFor(i);

}

}

}

function makeEffectFor(index) {

const label = getEyeLabel(index);

const kind = EFFECT_BY_TYPE[label] || "run";

return { kind, t:0, dur: DURS[kind], data:{} };

}

function toOverviewImmediate() {

const bbox = worldBBox();

const fit = fitToScreen(bbox, 0.0);

app.cam.x = app.cam.tx = fit.cx;

app.cam.y = app.cam.ty = fit.cy;

app.cam.s = app.cam.ts = fit.scale;

app.state = STATE.OVERVIEW;

}

function worldBBox() {

const positions = getGridPositions(eyes.length);

if (positions.length === 0) return { minx:0, miny:0, maxx:0, maxy:0, cx:0, cy:0 };

let minx = +Infinity, miny = +Infinity, maxx = -Infinity, maxy = -Infinity;

const w = 240 * eyeScale, h = w * 0.52;

for (const p of positions) {

minx = min(minx, p.x - w/1.8);

maxx = max(maxx, p.x + w/1.8);

miny = min(miny, p.y - h/1.6);

maxy = max(maxy, p.y + h/1.6);

}

return { minx, miny, maxx, maxy, cx:(minx+maxx)/2, cy:(miny+maxy)/2 };

}

function fitToScreen(bbox, padFrac=0.12) {

const w = max(1, bbox.maxx - bbox.minx);

const h = max(1, bbox.maxy - bbox.miny);

const padW = w * padFrac, padH = h * padFrac;

const scaleX = width / (w + padW2);

const scaleY = height / (h + padH2);

const scale = min(scaleX, scaleY);

return { cx:bbox.cx, cy:bbox.cy, scale };

}

function screenToWorld(sx, sy) {

const c = app.cam;

const wx = (sx - width/2) / c.s + c.x;

const wy = (sy - height/2) / c.s + c.y;

return { x: wx, y: wy };

}

function getEyeLabel(i) {

const ph = (eyes[i] && eyes[i].phrase) ? String(eyes[i].phrase).toUpperCase() : "";

if (ph) return ph;

const n = (eyes[i] && eyes[i].constructor && eyes[i].constructor.name) || "";

if (/Eye1_Grid/i.test(n)) return "PROJECTOR";

if (/Eye2_Fraser/i.test(n)) return "REFLECTOR";

if (/Eye3_Ribbons/i.test(n)) return "MANIFESTOR";

if (/Eye4_Generator/i.test(n)) return "GENERATOR";

if (/Eye5_ManGen/i.test(n)) return "MANIFESTING GENERATOR";

return "";

}

function runSecondaryEffect(eyeObj, localPos, ef) {

ef.t += deltaTime;

const u = constrain(ef.t / ef.dur, 0, 1);

push();

translate(localPos.x, localPos.y);

colorMode(RGB, 255, 255, 255, 255);

switch (ef.kind) {

case "run": {

const dist = 600;

const off = easeInOutCubic(u) * dist;

push();

translate(off, -off*0.25);

eyeObj.drawAt(0, 0, eyeScale);

stroke(255); strokeWeight(2);

const step = sin(u * TWO_PI * 6);

line(-60, 60, -60 + 20*step, 90);

line(-20, 60, -20 - 20*step, 90);

pop();

break;

}

case "explode": {

eyeObj.drawAt(0, 0, eyeScale);

noStroke();

const N = 48;

for (let i = 0; i < N; i++) {

const a = (i/N) * TWO_PI;

const r = map(u, 0, 1, 10, 360);

const x = cos(a) * r, y = sin(a) * r * 0.52;

fill(255, map(1-u, 0, 1, 0, 220));

circle(x, y, 4 + (1-u)*10);

}

break;

}

case "melt": {

const squish = 1 + u*1.1;

const sag = u * 100;

push();

scale(1, squish);

translate(0, sag);

eyeObj.drawAt(0, 0, eyeScale);

noStroke(); fill(255, 170);

for (let i=0;i<5;i++){

const dx = -70 + i*35 + sin((i+u*8))*8;

const dy = u*u*140 + sin((i*0.8+u*10))*6;

rect(dx, dy, 10, 50*(1-u), 6);

}

pop();

noStroke(); fill(255, 85);

ellipse(0, 180, 240*u, 70*u);

break;

}

case "break": {

const sep = easeInOutCubic(u) * 200;

const angle = radians(8) * u;

const w = 240*eyeScale, h = w*0.52;

const g = drawingContext;

g.save(); g.beginPath();

g.rect(-w/2-10, -h-10, w/2+20, h*2+20); g.clip();

push(); translate(-sep/2, u*26); rotate(-angle);

eyeObj.drawAt(0, 0, eyeScale);

pop(); g.restore();

g.save(); g.beginPath();

g.rect(0, -h-10, w/2+10, h*2+20); g.clip();

push(); translate(sep/2, u*26); rotate(angle);

eyeObj.drawAt(0, 0, eyeScale);

pop(); g.restore();

break;

}

case "burn": {

eyeObj.drawAt(0, 0, eyeScale);

const flames = 24;

for (let i=0;i<flames;i++){

const a = (i/flames)*TWO_PI;

const r = 60 + noise(i*0.2, u*2)*110;

const x = cos(a)*r, y = sin(a)*r*0.52 - u*110;

noStroke(); fill(255, 120 + 100*sin(i+u*6), 30, 200*(1-u*0.6));

circle(x, y, 36*(1-u));

}

noStroke(); fill(0, 130*u);

const w = 240*eyeScale, h = w*0.52;

ellipse(0, 0, w*1.05, h*1.05);

break;

}

}

pop();

return ef.t >= ef.dur;

}

function easeInOutCubic(x) { return x < 0.5 ? 4 * xxx : 1 - pow(-2*x + 2, 3)/2; }

class Eye1_Grid {

constructor(eyelidPhrase = "") {

this.fx = null;

this.blinkAmt = 0;

this.eyeCX = 0;

this.eyeCY = 0;

this.phrase = eyelidPhrase;

this.timeOffset = random(4000);

this.blinkPeriod = 7000;

this.blinkDuration = 650;

this.blinkEase = 0.1;

this.moveEase = 0.14;

this.hitPad = 1.08;

this.SLIT_FRACTION = 0.04;

this.GRIDCFG = {

color: { r: 209, g: 0, b: 239 },

alphaGlow: 0.08, alphaMid: 0.18, alphaCore: 0.55,

lengthScale: 1.25, nVertical: 18, nHorizontal: 28,

perspGamma: 2.0, speed: 0.40

};

}

drawAt(x, y, s = 1) {

push();

translate(x, y);

scale(s);

colorMode(RGB, 255, 255, 255, 1);

strokeCap(ROUND);

strokeJoin(ROUND);

noFill();

const base = 240;

const eyeW = base;

const eyeH = base * 0.52;

const pupilR = eyeH * 0.24;

const mx = mouseX - x;

const my = mouseY - y;

const insideEye = (mx * mx) / sq((eyeW / 2) * this.hitPad) + (my * my) / sq((eyeH / 2) * this.hitPad) <= 1;

let targetBlink = autoBlink(millis() + this.timeOffset, this.blinkPeriod, this.blinkDuration);

if (insideEye && !this.fx) targetBlink = 1;

this.blinkAmt = lerp(this.blinkAmt, targetBlink, this.blinkEase);

const easedLid = easeInOutCubic(constrain(this.blinkAmt, 0, 1));

const maxOffset = eyeH * 0.18;

let tx = mx, ty = my;

const mLen = Math.sqrt(tx * tx + ty * ty);

if (mLen > maxOffset && mLen > 0) { const k = maxOffset / mLen; tx *= k; ty *= k; }

this.eyeCX = lerp(this.eyeCX, tx, this.moveEase);

this.eyeCY = lerp(this.eyeCY, ty, this.moveEase);

withEyeClip(eyeW, eyeH, () => {

push();

translate(this.eyeCX, this.eyeCY);

this.drawAllFourWedges(eyeW, eyeH, { reverse: false, scale: 1.0 });

push();

drawingContext.save();

drawingContext.beginPath();

drawingContext.arc(0, 0, pupilR, 0, TWO_PI);

drawingContext.clip();

this.drawAllFourWedges(eyeW, eyeH, { reverse: true, scale: 0.65 });

drawingContext.restore();

noFill();

stroke(220, 220, 220, 1);

strokeWeight(1.8);

circle(0, 0, pupilR * 2);

pop();

this.drawTopLid(eyeW, eyeH, easedLid, color(134, 29, 194), insideEye);

const yEdge = lidEdgeY(eyeH, easedLid, insideEye, this.SLIT_FRACTION);

const ctx = drawingContext;

ctx.save();

ctx.beginPath();

ctx.rect(-eyeW / 2 - 2, -eyeH, eyeW + 4, yEdge + eyeH);

ctx.clip();

fill(255); noStroke(); textAlign(CENTER, CENTER);

textSize(eyeH * 0.14);

text(this.phrase, 0, -eyeH * 0.07);

ctx.restore();

pop();

});

noFill();

stroke(255);

strokeWeight(1.5);

drawEyeOutline(eyeW, eyeH);

pop();

}

drawAllFourWedges(eyeW, eyeH, options) {

const reverse = !!options.reverse;

const scaleVal = (options.scale == null) ? 1.0 : options.scale;

const L = Math.max(eyeW, eyeH) * this.GRIDCFG.lengthScale * scaleVal;

const t = millis() * 0.001;

const cycle = t * this.GRIDCFG.speed * (reverse ? -1 : 1);

const right = createVector(1, 0), left = createVector(-1, 0);

const up = createVector(0, -1), down = createVector(0, 1);

this.drawWedgeGrid(right, up, L, cycle);

this.drawWedgeGrid(right, down, L, cycle);

this.drawWedgeGrid(left, down, L, cycle);

this.drawWedgeGrid(left, up, L, cycle);

}

drawWedgeGrid(dirA, dirB, L, cycle) {

dirA = dirA.copy().normalize();

dirB = dirB.copy().normalize();

const farA = p5.Vector.mult(dirA, L);

const farB = p5.Vector.mult(dirB, L);

for (let i = 0; i <= this.GRIDCFG.nVertical; i++) {

const v = i / this.GRIDCFG.nVertical;

const edgePoint = p5.Vector.lerp(farA, farB, v);

this.drawGlowLine(0, 0, edgePoint.x, edgePoint.y);

}

const total = this.GRIDCFG.nHorizontal + 8;

for (let k = -4; k < total - 4; k++) {

const f = frac((k + cycle) / this.GRIDCFG.nHorizontal);

const tt = 1 - Math.pow(1 - f, this.GRIDCFG.perspGamma);

const pA = p5.Vector.mult(dirA, tt * L);

const pB = p5.Vector.mult(dirB, tt * L);

this.drawGlowLine(pA.x, pA.y, pB.x, pB.y);

}

}

drawGlowLine(x1, y1, x2, y2) {

const c = this.GRIDCFG.color;

stroke(c.r, c.g, c.b, this.GRIDCFG.alphaGlow); strokeWeight(5.5); line(x1, y1, x2, y2);

stroke(c.r, c.g, c.b, this.GRIDCFG.alphaMid); strokeWeight(3.0); line(x1, y1, x2, y2);

stroke(c.r, c.g, c.b, this.GRIDCFG.alphaCore); strokeWeight(1.6); line(x1, y1, x2, y2);

}

drawTopLid(eyeW, eyeH, blinkAmt, fillCol, isHover) {

if (blinkAmt <= 0.0001) return;

const yEdge = lidEdgeY(eyeH, blinkAmt, isHover, this.SLIT_FRACTION);

noStroke(); fill(fillCol);

rectMode(CORNERS);

rect(-eyeW / 2 - 5, -height, eyeW / 2 + 5, yEdge);

}

}

class Eye2_Fraser {

constructor(eyelidPhrase = "") {

this.fx = null;

this.blinkAmt = 0;

this.eyeCX = 0;

this.eyeCY = 0;

this.phrase = eyelidPhrase;

this.timeOffset = random(4000);

this.blinkPeriod = 7000;

this.blinkDuration = 650;

this.blinkEase = 0.1;

this.moveEase = 0.14; this.hitPad = 1.08; this.SLIT_FRACTION = 0.04;

this.FRASER = { rStep: 6.0, aStep: 0.10, m: 6.0, k: 0.28, omega: 1.2, bands: 8, hueBase: 290, hueSpan: 320, sat: 90, briHi: 100, briLo: 60, alpha: 0.95, tilt: 12 * Math.PI/180, expand: 1.08 };

this.FRASER_PUPIL = { rStep: 3.5, aStep: 0.08, m: 6.0, k: 0.30, omega: -1.4, bands: 9, hueBase: 290, hueSpan: 320, sat: 90, briHi: 100, briLo: 55, alpha: 0.95, tilt: 10 * Math.PI/180, expand: 1.08 };

}

drawAt(x, y, s = 1) {

push();

translate(x, y);

scale(s);

colorMode(HSB, 360, 100, 100, 1);

noStroke();

const base = 240;

const eyeW = base;

const eyeH = base * 0.52;

const pupilR = eyeH * 0.20;

const mx = mouseX - x;

const my = mouseY - y;

const insideEye = (mx * mx) / sq((eyeW / 2) * this.hitPad) + (my * my) / sq((eyeH / 2) * this.hitPad) <= 1;

let targetBlink = autoBlink(millis() + this.timeOffset, this.blinkPeriod, this.blinkDuration);

if (insideEye && !this.fx) targetBlink = 1;

this.blinkAmt = lerp(this.blinkAmt, targetBlink, this.blinkEase);

const easedLid = easeInOutCubic(constrain(this.blinkAmt, 0, 1));

const maxOffset = eyeH * 0.18;

let tx = mx, ty = my;

const mLen = Math.sqrt(tx * tx + ty * ty);

if (mLen > maxOffset && mLen > 0) { const k = maxOffset / mLen; tx *= k; ty *= k; }

this.eyeCX = lerp(this.eyeCX, tx, this.moveEase);

this.eyeCY = lerp(this.eyeCY, ty, this.moveEase);

withEyeClip(eyeW, eyeH, () => {

push();

translate(this.eyeCX, this.eyeCY);

this.drawFraserTilesField(eyeW, eyeH, pupilR, this.FRASER, 0.65, 0.60);

pop();

push();

translate(this.eyeCX, this.eyeCY);

withCircleClip(0, 0, pupilR * 1.005, () => {

this.drawFraserTilesField(pupilR*2.2, pupilR*2.2, 0, this.FRASER_PUPIL, 1.10, 0.0);

});

pop();

push();

translate(this.eyeCX, this.eyeCY);

noFill();

stroke(0, 0, 85, 1);

strokeWeight(1.6);

circle(0, 0, pupilR * 2);

pop();

this.drawTopLid(eyeW, eyeH, easedLid, color(278, 77, 76, 1), insideEye);

const yEdge = lidEdgeY(eyeH, easedLid, insideEye, this.SLIT_FRACTION);

const ctx = drawingContext;

ctx.save();

ctx.beginPath();

ctx.rect(-eyeW / 2 - 2, -eyeH, eyeW + 4, yEdge + eyeH);

ctx.clip();

fill(0, 0, 100); noStroke(); textAlign(CENTER, CENTER);

textSize(eyeH * 0.14);

text(this.phrase, 0, -eyeH * 0.07);

ctx.restore();

});

noFill();

stroke(255);

strokeWeight(1.5);

drawEyeOutline(eyeW, eyeH);

pop();

}

drawFraserTilesField(bboxW, bboxH, innerRadius, cfg, coverFactor, innerFactor) {

const maxR = Math.min(bboxW, bboxH) * (coverFactor || 0.95);

const t = millis() * 0.001;

const rot = cfg.omega * t;

const startR = Math.max(innerRadius * (innerFactor || 0.60), 0);

for (let r = startR; r <= maxR; r += cfg.rStep) {

const r2 = Math.min(r + cfg.rStep, maxR);

const rMid = (r + r2) * 0.5;

for (let a0 = 0; a0 < Math.PI2; a0 += cfg.aStep) {

const a1 = a0 + cfg.aStep;

const aMid = (a0 + a1) * 0.5;

const p = cfg.m * (aMid + rot) + cfg.k * rMid;

const s = Math.sin(p);

const bands = Math.max(2, Math.floor(cfg.bands));

let idx = Math.floor((0.5 + 0.5 * s) * bands);

if (idx >= bands) idx = bands - 1;

const hue = (cfg.hueBase + (cfg.hueSpan * idx / (bands - 1))) % 360;

const bri = lerp(cfg.briLo, cfg.briHi, 0.5 + 0.5s);

fill(hue, cfg.sat, bri, cfg.alpha);

const erx = Math.cos(aMid), ery = Math.sin(aMid);

const etx = -Math.sin(aMid), ety = Math.cos(aMid);

const hr = (r2 - r) * 0.5 * cfg.expand;

const ht = (a1 - a0) * rMid * 0.5 * cfg.expand;

const c = Math.cos(cfg.tilt), sT = Math.sin(cfg.tilt);

const ux = cerx + sTetx, uy = cery + sTety;

const vx = -sTerx + cetx, vy = -sTery + cety;

const px = erx * rMid, py = ery * rMid;

beginShape();

vertex(px + ux * hr, py + uy * hr);

vertex(px + vx * ht, py + vy * ht);

vertex(px - ux * hr, py - uy * hr);

vertex(px - vx * ht, py - vy * ht);

endShape(CLOSE);

}

}

}

drawTopLid(eyeW, eyeH, blinkAmt, fillCol, isHover) {

if (blinkAmt <= 0.0001) return;

const yEdge = lidEdgeY(eyeH, blinkAmt, isHover, this.SLIT_FRACTION);

noStroke(); fill(fillCol);

rectMode(CORNERS);

rect(-eyeW / 2 - 5, -height, eyeW / 2 + 5, yEdge);

}

}

class Eye3_Ribbons {

constructor(eyelidPhrase = "") {

this.fx = null;

this.blinkAmt = 0;

this.eyeCX = 0;

this.eyeCY = 0;

this.starRot = 0;

this.phrase = eyelidPhrase;

this.timeOffset = random(4000);

this.blinkPeriod = 7000;

this.blinkDuration = 650;

this.blinkEase = 0.1;

this.moveEase = 0.14; this.hitPad = 1.08; this.SLIT_FRACTION = 0.04;

}

drawAt(x, y, s = 1) {

push();

translate(x, y);

scale(s);

colorMode(RGB);

strokeJoin(ROUND);

strokeCap(ROUND);

noStroke();

const base = 240;

const eyeW = base;

const eyeH = base * 0.52;

const pupilR = eyeH * 0.36;

const starOuter = eyeH * 0.28;

const starInner = starOuter * 0.46;

const mx = mouseX - x;

const my = mouseY - y;

const insideEye = (mx*mx)/sq((eyeW/2)*this.hitPad) + (my*my)/sq((eyeH/2)*this.hitPad) <= 1;

let targetBlink = autoBlink(millis() + this.timeOffset, this.blinkPeriod, this.blinkDuration);

if (insideEye && !this.fx) targetBlink = 1;

this.blinkAmt = lerp(this.blinkAmt, targetBlink, this.blinkEase);

const easedLid = easeInOutCubic(constrain(this.blinkAmt, 0, 1));

const maxOffset = eyeH * 0.18;

let tx = mx, ty = my;

const mLen = sqrt(tx*tx + ty*ty);

if (mLen > maxOffset && mLen > 0) { const k = maxOffset/mLen; tx *= k; ty *= k; }

this.eyeCX = lerp(this.eyeCX, tx, this.moveEase);

this.eyeCY = lerp(this.eyeCY, ty, this.moveEase);

this.starRot = lerpAngle(this.starRot, atan2(my, mx), this.moveEase * 0.9);

withEyeClip(eyeW, eyeH, () => {

const rs = [

{ fill: color('#F20045'), alpha: 150, baseAmp: 10, waveFreq: 0.018, timeSpeed: 1.8, wStart: 6, wEnd: 52, wobbleAmt: 0.22, wobbleFreq: 0.8, wobbleTime: 0.9, len: 260, steps: 80, seedShift: 1111 },

{ fill: color('#861DC2'), alpha: 120, baseAmp: 12, waveFreq: 0.014, timeSpeed: 2.6, wStart: 5, wEnd: 60, wobbleAmt: 0.28, wobbleFreq: 1.1, wobbleTime: 0.7, len: 270, steps: 85, seedShift: 2222 },

{ fill: color('#20D6FF'), alpha: 110, baseAmp: 8, waveFreq: 0.022, timeSpeed: 1.4, wStart: 4, wEnd: 46, wobbleAmt: 0.18, wobbleFreq: 0.9, wobbleTime: 1.1, len: 250, steps: 70, seedShift: 3333 },

{ fill: color('#FFE14A'), alpha: 90, baseAmp: 14, waveFreq: 0.016, timeSpeed: 2.1, wStart: 7, wEnd: 58, wobbleAmt: 0.25, wobbleFreq: 0.7, wobbleTime: 1.0, len: 280, steps: 90, seedShift: 4444 }

];

for (const r of rs) {

this.drawContrailRibbon(this.eyeCX, this.eyeCY, r.len, r.steps, r.baseAmp, r.waveFreq, r.timeSpeed, r.wStart, r.wEnd, r.wobbleAmt, r.wobbleFreq, r.wobbleTime, r.fill, r.alpha, r.seedShift);

this.drawContrailRibbon(this.eyeCX, this.eyeCY, -r.len, r.steps, r.baseAmp, r.waveFreq, r.timeSpeed, r.wStart, r.wEnd, r.wobbleAmt, r.wobbleFreq, r.wobbleTime, r.fill, r.alpha, r.seedShift);

}

push();

translate(this.eyeCX, this.eyeCY);

stroke(200); strokeWeight(1.5); fill(0);

circle(0, 0, pupilR * 2);

noFill(); stroke(255); strokeWeight(2);

push();

rotate(this.starRot);

drawStar(0, 0, starOuter, starInner, 5);

scale(0.6);

drawStar(0, 0, starOuter, starInner, 5);

pop(); pop();

this.drawTopLid(eyeW, eyeH, easedLid, color('#861DC2'), insideEye);

const yEdge = lidEdgeY(eyeH, easedLid, insideEye, this.SLIT_FRACTION);

const ctx = drawingContext;

ctx.save();

ctx.beginPath();

ctx.rect(-eyeW / 2 - 2, -eyeH, eyeW + 4, yEdge + eyeH);

ctx.clip();

fill(255); noStroke(); textAlign(CENTER, CENTER);

textSize(eyeH * 0.14);

text(this.phrase, 0, -eyeH * 0.07);

ctx.restore();

});

stroke(255); strokeWeight(1.5); noFill();

drawEyeOutline(eyeW, eyeH);

pop();

}

drawContrailRibbon(px, py, len, steps, baseAmp, waveFreq, timeSpeed, wStart, wEnd, wobbleAmt, wobbleFreq, wobbleTime, colorFill, alpha, seedShift) {

let ptsTop = [], ptsBot = [];

const time = millis() * 0.001;

const centerAt = (u) => {

const x = px - (1 - u) * len;

const y = py + sin((x * waveFreq) + time * timeSpeed) * baseAmp + sin((x * waveFreq * 0.53) + time * 1.3) * (baseAmp * 0.22);

return createVector(x, y);

};

const du = 1 / steps;

for (let i = 0; i <= steps; i++) {

const u = i * du;

const p = centerAt(u);

const p2 = centerAt(min(u + du, 1));

const tx = p2.x - p.x, ty = p2.y - p.y;

const mag = max(1e-6, sqrt(txtx + tyty));

const nx = -ty / mag, ny = tx / mag;

let w = lerp(wStart, wEnd, u);

const n1 = noise(u * wobbleFreq, time * wobbleTime) * 2 - 1;

const n2 = noise((u + (seedShift||0)) * wobbleFreq, time * wobbleTime) * 2 - 1;

w *= 1 + wobbleAmt * constrain((n1 + n2) * 0.5, -1, 1);

ptsTop.push(createVector(p.x + nx * w * 0.5, p.y + ny * w * 0.5));

ptsBot.push(createVector(p.x - nx * w * 0.5, p.y - ny * w * 0.5));

}

push();

fill(red(colorFill), green(colorFill), blue(colorFill), alpha || 120);

beginShape();

for (const pt of ptsTop) vertex(pt.x, pt.y);

for (let j = ptsBot.length - 1; j >= 0; j--) vertex(ptsBot[j].x, ptsBot[j].y);

endShape(CLOSE);

pop();

}

drawTopLid(eyeW, eyeH, blinkAmt, fillCol, isHover) {

if (blinkAmt <= 0.0001) return;

const yEdge = lidEdgeY(eyeH, blinkAmt, isHover, this.SLIT_FRACTION);

noStroke(); fill(fillCol);

rectMode(CORNERS);

rect(-eyeW / 2 - 5, -height, eyeW / 2 + 5, yEdge);

}

}

class Eye4_Generator {

constructor(eyelidPhrase = "GENERATOR") {

this.fx = null;

this.blinkPeriod = 7000;

this.blinkDuration = 650;

this.blinkEase = 0.1;

this.moveEase = 0.14;

this.hitPad = 1.08; this.SLIT_FRACTION = 0.04;

this.blinkAmt = 0; this.eyeCX = 0; this.eyeCY = 0;

this.base = 240; this.phrase = eyelidPhrase;

this.timeOffset = random(4000);

this.pulseSpeed = 0.85; this.gearTeeth = 36; this.gearSpeed = 0.12;

}

drawAt(x, y, s = 1) {

push();

translate(x, y);

scale(s);

colorMode(RGB);

textAlign(CENTER, CENTER);

noStroke();

const eyeW = this.base;

const eyeH = this.base * 0.52;

const pupilR = eyeH * 0.24;

const mx = mouseX - x;

const my = mouseY - y;

const insideEye = (mx * mx) / sq((eyeW / 2) * this.hitPad) + (my * my) / sq((eyeH / 2) * this.hitPad) <= 1;

let targetBlink = autoBlink(millis() + this.timeOffset, this.blinkPeriod, this.blinkDuration);

if (insideEye && !this.fx) targetBlink = 1;

this.blinkAmt = lerp(this.blinkAmt, targetBlink, this.blinkEase);

const easedLid = easeInOutCubic(constrain(this.blinkAmt, 0, 1));

const maxOffset = eyeH * 0.18;

let tx = mx, ty = my;

const mLen = sqrt(tx * tx + ty * ty);

if (mLen > maxOffset && mLen > 0) { const k = maxOffset / mLen; tx *= k; ty *= k; }

this.eyeCX = lerp(this.eyeCX, tx, this.moveEase);

this.eyeCY = lerp(this.eyeCY, ty, this.moveEase);

withEyeClip(eyeW, eyeH, () => {

push();

translate(this.eyeCX * 0.25, this.eyeCY * 0.25);

const t = millis() * 0.001 * this.pulseSpeed;

for (let i = 0; i < 3; i++) {

const ph = t + i * 0.33;

const r = eyeH * (0.25 + 0.22 * (0.5 + 0.5 * sin(TWO_PI * ph)));

const a = 160 - i * 50;

noFill();

stroke(255, 180, 60, a); strokeWeight(18 - i * 6); ellipse(0, 0, r * 2.2, r * 2.2 * (eyeH / eyeW));

stroke(220, 60, 30, a * 0.8); strokeWeight(8 - i * 2); ellipse(0, 0, r * 1.8, r * 1.8 * (eyeH / eyeW));

}

pop();

push();

translate(this.eyeCX * 0.18, this.eyeCY * 0.18);

rotate(millis() * 0.001 * TWO_PI * this.gearSpeed);

const gearR = eyeH * 0.34;

noFill(); stroke(255, 180); strokeWeight(1.2);

beginShape();

for (let i = 0; i < this.gearTeeth; i++) {

const a = (i / this.gearTeeth) * TWO_PI;

const rr = i % 2 === 0 ? gearR * 1.04 : gearR * 0.92;

vertex(cos(a) * rr, sin(a) * rr);

}

endShape(CLOSE);

pop();

push();

translate(this.eyeCX, this.eyeCY);

noFill(); stroke(255); strokeWeight(1.2); circle(0, 0, pupilR * 2);

noStroke(); fill(0); circle(0, 0, pupilR * 1.66);

pop();

this.drawTopLid(eyeW, eyeH, easedLid, color(196, 74, 36), insideEye);

const yEdge = lidEdgeY(eyeH, easedLid, insideEye, this.SLIT_FRACTION);

const ctx = drawingContext;

ctx.save();

ctx.beginPath();

ctx.rect(-eyeW / 2 - 2, -eyeH, eyeW + 4, yEdge + eyeH);

ctx.clip();

fill(255); noStroke(); textAlign(CENTER, CENTER);

textSize(eyeH * 0.14);

text(this.phrase, 0, -eyeH * 0.07);

ctx.restore();

});

stroke(255); strokeWeight(1.5); noFill();

drawEyeOutline(eyeW, eyeH);

pop();

}

drawTopLid(eyeW, eyeH, blinkAmt, fillCol, isHover) {

if (blinkAmt <= 0.0001) return;

const yEdge = lidEdgeY(eyeH, blinkAmt, isHover, this.SLIT_FRACTION);

noStroke(); fill(fillCol);

rectMode(CORNERS);

rect(-eyeW / 2 - 5, -height, eyeW / 2 + 5, yEdge);

}

}

class Eye5_ManGen {

constructor(eyelidPhrase = "MANIFESTING GENERATOR") {

this.fx = null;

this.blinkPeriod = 7000;

this.blinkDuration = 650;

this.blinkEase = 0.1;

this.moveEase = 0.14;

this.hitPad = 1.08; this.SLIT_FRACTION = 0.04;

this.blinkAmt = 0; this.eyeCX = 0; this.eyeCY = 0;

this.base = 240; this.phrase = eyelidPhrase;

this.timeOffset = random(4000);

this.flowSpeed = 0.30; this.sparkRate = 0.035; this.sparks = [];

}

drawAt(x, y, s = 1) {

push();

translate(x, y);

scale(s);

colorMode(RGB);

textAlign(CENTER, CENTER);

noStroke();

const eyeW = this.base;

const eyeH = this.base * 0.52;

const pupilR = eyeH * 0.24;

const mx = mouseX - x;

const my = mouseY - y;

const insideEye = (mx * mx) / sq((eyeW / 2) * this.hitPad) + (my * my) / sq((eyeH / 2) * this.hitPad) <= 1;

let targetBlink = autoBlink(millis() + this.timeOffset, this.blinkPeriod, this.blinkDuration);

if (insideEye && !this.fx) targetBlink = 1;

this.blinkAmt = lerp(this.blinkAmt, targetBlink, this.blinkEase);

const easedLid = easeInOutCubic(constrain(this.blinkAmt, 0, 1));

const maxOffset = eyeH * 0.18;

let tx = mx, ty = my;

const mLen = sqrt(tx * tx + ty * ty);

if (mLen > maxOffset && mLen > 0) { const k = maxOffset / mLen; tx *= k; ty *= k; }

this.eyeCX = lerp(this.eyeCX, tx, this.moveEase);

this.eyeCY = lerp(this.eyeCY, ty, this.moveEase);

withEyeClip(eyeW, eyeH, () => {

this.renderFlowAndSparks(eyeW, eyeH, 1.0);

push();

translate(this.eyeCX, this.eyeCY);

noFill(); stroke(230); strokeWeight(1.4); circle(0, 0, pupilR * 2);

noStroke(); fill(0); circle(0, 0, pupilR * 1.72);

pop();

const innerScale = 0.56;

const ctx = drawingContext;

ctx.save();

ctx.beginPath();

ctx.arc(this.eyeCX, this.eyeCY, pupilR * 0.98, 0, TWO_PI);

ctx.clip();

push();

translate(this.eyeCX, this.eyeCY);

scale(innerScale);

translate(-this.eyeCX, -this.eyeCY);

this.renderFlowAndSparks(eyeW, eyeH, innerScale);

pop();

ctx.restore();

this.drawTopLid(eyeW, eyeH, easedLid, color(134, 29, 194), insideEye);

const yEdge = lidEdgeY(eyeH, easedLid, insideEye, this.SLIT_FRACTION);

const ctx2 = drawingContext;

ctx2.save();

ctx2.beginPath();

ctx2.rect(-eyeW/2 - 2, -eyeH, eyeW + 4, yEdge + eyeH);

ctx2.clip();

fill(255); noStroke(); textAlign(CENTER, CENTER);

textSize(eyeH * 0.12);

text(this.phrase, 0, -eyeH * 0.07);

ctx2.restore();

});

stroke(255); strokeWeight(1.5); noFill();

drawEyeOutline(eyeW, eyeH);

pop();

}

drawTopLid(eyeW, eyeH, blinkAmt, fillCol, isHover) {

if (blinkAmt <= 0.0001) return;

const yEdge = lidEdgeY(eyeH, blinkAmt, isHover, this.SLIT_FRACTION);

noStroke(); fill(fillCol);

rectMode(CORNERS);

rect(-eyeW / 2 - 5, -height, eyeW / 2 + 5, yEdge);

}

renderFlowAndSparks(eyeW, eyeH, scaleMul = 1.0) {

push();

translate(this.eyeCX * 0.25 * scaleMul, this.eyeCY * 0.25 * scaleMul);

const t = millis() * 0.001 * this.flowSpeed;

for (let k = 0; k < 3; k++) {

const rot = t + k * TWO_PI / 3;

this.ribbon(rot, eyeW * 0.42 * scaleMul, eyeH * 0.26 * scaleMul, color(209, 0, 239, 120));

this.ribbon(rot + 0.25, eyeW * 0.36 * scaleMul, eyeH * 0.22 * scaleMul, color(255, 160, 40, 110));

}

pop();

this.maybeSpawnSpark(eyeW, eyeH);

this.updateSparks(scaleMul);

}

ribbon(rot, rx, ry, col) {

push();

rotate(rot);

noFill(); stroke(col); strokeWeight(2);

beginShape();

for (let i = 0; i <= 80; i++) {

const u = i / 80;

const a = lerp(-PI * 0.8, PI * 0.8, u);

const wob = 0.25 * sin(3 * a + rot * 2.0);

const x = cos(a) * rx * (1 + 0.08 * wob);

const y = sin(a) * ry * (1 - 0.08 * wob);

curveVertex(x, y);

}

endShape();

pop();

}

maybeSpawnSpark(eyeW, eyeH) {

if (random() < this.sparkRate) {

const a = random(TWO_PI);

const rx = eyeW * 0.46, ry = eyeH * 0.46;

this.sparks.push({

x: cos(a) * rx * 0.6, y: sin(a) * ry * 0.6,

vx: random(-1.4, 1.4), vy: random(-1.0, 1.0),

life: 1.0, hue: random() < 0.5 ? 'mag' : 'amb'

});

if (this.sparks.length > 80) this.sparks.shift();

}

}

updateSparks(scaleMul = 1.0) {

for (let s of this.sparks) {

const len = (10 + 20 * (1 - s.life)) * scaleMul;

stroke(s.hue === 'mag' ? color(209, 0, 239, 200) : color(255, 160, 40, 200));

strokeWeight(1.6 * scaleMul);

line(s.x * scaleMul, s.y * scaleMul, (s.x + s.vx * len), (s.y + s.vy * len));

noStroke(); fill(255, 230);

circle(s.x * scaleMul + s.vx * len, s.y * scaleMul + s.vy * len, (2.6 + 1.8 * (1 - s.life)) * scaleMul);

if (scaleMul === 1.0) { s.x += s.vx * 2.0; s.y += s.vy * 2.0; s.life *= 0.90; }

}

if (scaleMul === 1.0) this.sparks = this.sparks.filter(s => s.life > 0.06);

}

}

function withEyeClip(eyeW, eyeH, drawFn) {

const w = eyeW, h = eyeH;

const L = -w / 2, R = w / 2;

const ctx = drawingContext;

ctx.save();

ctx.beginPath();

ctx.moveTo(L, 0);

ctx.bezierCurveTo(L + w * 0.25, -h * 0.55, R - w * 0.25, -h * 0.55, R, 0);

ctx.bezierCurveTo(R - w * 0.25, h * 0.55, L + w * 0.25, h * 0.55, L, 0);

ctx.closePath();

ctx.clip();

drawFn();

ctx.restore();

}

function withCircleClip(cx, cy, r, drawFn) {

const ctx = drawingContext;

ctx.save();

ctx.beginPath();

ctx.arc(cx, cy, r, 0, Math.PI * 2, false);

ctx.closePath();

ctx.clip();

drawFn();

ctx.restore();

}

function drawEyeOutline(eyeW, eyeH) {

const w = eyeW, h = eyeH;

const L = -w / 2, R = w / 2;

beginShape();

vertex(L, 0);

bezierVertex(L + w * 0.25, -h * 0.55, R - w * 0.25, -h * 0.55, R, 0);

bezierVertex(R - w * 0.25, h * 0.55, L + w * 0.25, h * 0.55, L, 0);

endShape(CLOSE);

}

function lidEdgeY(eyeH, blinkAmt, isHover, SLIT_FRACTION = 0.04) {

const openY = -eyeH * 0.65;

let closedY;

if (isHover) {

const slitPx = eyeH * SLIT_FRACTION;

closedY = bottomMidY(eyeH) - slitPx;

} else {

closedY = -eyeH * 0.05;

}

return lerp(openY, closedY, constrain(blinkAmt, 0, 1));

}

function autoBlink(nowMs, periodMs, durMs) {

const t = nowMs % periodMs;

if (t < durMs) return t / durMs;

if (t > periodMs - durMs) return (periodMs - t) / durMs;

return 0;

}

function bottomMidY(h) {

const p0 = 0, p1 = h * 0.55, p2 = h * 0.55, p3 = 0;

const u = 0.5, uu = (1 - u);

return uu * uu * uu * p0 + 3 * uu * uu * u * p1 + 3 * uu * u * u * p2 + u * u * u * p3;

}

function drawStar(cx, cy, rOuter, rInner, points=5) {

beginShape();

for (let i = 0; i < points * 2; i++) {

const a = (PI / points) * i - HALF_PI;

const r = (i % 2 === 0) ? rOuter : rInner;

vertex(cx + cos(a) * r, cy + sin(a) * r);

}

endShape(CLOSE);

}

function frac(x) { return x - Math.floor(x); }

function lerpAngle(a, b, t) {

let diff = (b - a + PI) % (TWO_PI) - PI;

return a + diff * t;

}

function getGridPositions(n) {

const pos = [];

if (n >= 1) pos.push({ x: width * 0.25, y: height * 0.33 });

if (n >= 2) pos.push({ x: width * 0.50, y: height * 0.33 });

if (n >= 3) pos.push({ x: width * 0.75, y: height * 0.33 });

if (n >= 4) pos.push({ x: width * 0.37, y: height * 0.66 });

if (n >= 5) pos.push({ x: width * 0.63, y: height * 0.66 });

return pos.slice(0, n);

}

WIRED Editorial Illustration is a live editorial illustration created for the Mattha Busby’s article, “Human Design Is Blowing Up. Following It Might Make You Leave Your Spouse.” The project explores how Human Design and similar belief systems can influence identity, relationships, and the way people interpret themselves through external frameworks. Using P5.JS I coded and designed a set of eyes that respond to the cursor’s hover state. As the viewer moves through the piece, the eyes react back, creating a feeling of being observed, analyzed, or emotionally projected onto. This interaction became a visual metaphor for the article’s themes of self analysis, compatibility, and the pressure of being read through a system.

Client

Wired Magazine

Work

Wired Magazine Editorial Illustration

Year

2025