0xc3e1…9515

All memos sent from and to 0xc3e1…9515.

site.image(fillPersistentLayer, 0, 0) finalComposite.push() finalComposite.translate(0, maskOffset) finalComposite.image(eyesBlockingCache, 0, 0) finalComposite.pop() const eyeOpacity = _cachedEyeOpacity if (eyeOpacity > 0) { finalComposite.push() finalComposite.translate(0, maskOffset) if (TRAITS.iris === "Omnia") { const omniaCol = _cachedOmniaCol || getOmniaColor() finalComposite.tint(red(omniaCol), green(omniaCol), blue(omniaCol), eyeOpacity * 255) finalComposite.image(omniaEyesBase, 0, 0) } else { finalComposite.tint(255, eyeOpacity * 255) finalComposite.image(eyesCache, 0, 0) } finalComposite.pop() finalComposite.noTint() } } orbitBuffer.clear() orbitBuffer.background(0, 0, 0, 0) for (let orb of orbitingElements) { orb.display(orbitBuffer, false) } finalComposite.image(orbitBuffer, 0, 0) finalShadedBuffer.push() finalShadedBuffer.rectMode(CORNER) finalShadedBuffer.shader(asciiShader) asciiShader.setUniform('u_texture', finalComposite) asciiShader.setUniform('u_resolution', [width, height]) if (!freezeMode) _cachedShaderTime = millis() / 1000.0 asciiShader.setUniform('u_time', _cachedShaderTime) asciiShader.setUniform('u_gridX', _tierGridDensity) asciiShader.setUniform('u_symbolSize', PARAMS.symbolSize) asciiShader.setUniform('u_forceMonochrome', TRAITS.forceMonochrome) asciiShader.setUniform('u_applyNoise', _tierNoiseOverride !== null ? _tierNoiseOverride : TRAITS.applyNoise) asciiShader.setUniform('u_symbolOffset', PARAMS.symbolOffset) asciiShader.setUniform('u_vocabSize', PARAMS.vocabSize) asciiShader.setUniform('u_bwMode', bwMode) finalShadedBuffer.rect(-width / 2, -height / 2, width, height) finalShadedBuffer.pop() image(finalShadedBuffer, 0, 0) // Snapshot hero frame: once the composition has settled, freeze (capture UA only) // and signal Cloudflare the render is ready. No-op for live viewers. if (!_snapshotMarked && window.$art && window.$art.captureMode && frameCount >= SNAPSHOT_FRAME) { _snapshotMarked = true window.$art.snapshot(() => { freezeMode = true }) } } // ─── PIXEL-PERFECT EXPORT ─── // Capture exactly what's on screen. The live canvas already holds the displayed // frame with its real grid, resolution, Perlin-noise term, and coarse symbol // rasterization — so we grab it (rather than re-rendering the shader, which would // resolve thin pattern features at a finer per-cell resolution and drift from the // live look). Crop the top 1:1 square and integer-upscale with smoothing off: a // whole-number scale turns every source pixel into a clean N×N block, staying // pixel-identical to the iframe with no blur. const EXPORT_TARGET_PX = 2048 // aim for ~2048 px; actual = srcSquare × integer scale function renderPixelPerfectSquare() { const src = document.querySelector('canvas') // live displayed 2D canvas const cropSize = Math.min(src.width, src.height) // top square side (= canvas width) const scale = Math.max(1, Math.round(EXPORT_TARGET_PX / cropSize)) const outSize = cropSize * scale const out = document.createElement('canvas') out.width = outSize out.height = outSize const ctx = out.getContext('2d') ctx.imageSmoothingEnabled = false // nearest-neighbor → crisp blocks ctx.drawImage(src, 0, 0, cropSize, cropSize, 0, 0, outSize, outSize) return out } function keyPressed() { if (key === 's' || key === 'S') { const out = renderPixelPerfectSquare() const a = document.createElement('a') a.href = out.toDataURL('image/png') a.download = 'SATARI_' + CURRENT_SEED + '_' + out.width + '.png' a.click() return false } if (key === 'n' || key === 'N') { // Restart current piece from frame 0 — same seed, replay animation resetAndRegenerate() if (DEBUG) console.log(`%c↻ RESTART — same seed (${CURRENT_SEED}), replayed from frame 0`, 'color:#FFD700; font-weight:bold;') return false } if (key === ' ') { freezeMode = !freezeMode if (DEBUG) console.log(`%c${freezeMode ? '❄ FROZEN' : '▶ THAWED'}`, freezeMode ? 'color:#00FFFF; background:#003; font-weight:bold; padding:2px 8px;' : 'color:#39FF14; font-weight:bold;') return false } if (key === 'Escape') { freezeMode = false if (DEBUG) console.log(`%c⟲ RESET — all modes cleared`, 'color:#FFF; background:#333; font-weight:bold; padding:2px 8px;') return false } } </script> <!-- ============================================================ --> <!-- satarimustdie.com capture bridge --> <!-- ============================================================ --> <!-- Listens for postMessage requests from the parent frame and --> <!-- returns PNG data URLs of the live canvas. Used by the site's --> <!-- PNG and GIF save buttons. Restored after the v5 renderer --> <!-- rebuild dropped the original capture handlers. --> <script> (function () { window.addEventListener('message', function (event) { var data = event.data || {}; if (data.action !== 'captureSnapshot') return; var requestId = data.requestId; function respond(success, payload) { var msg = { action: 'snapshotCaptured', requestId: requestId, success: success }; if (success) msg.dataUrl = payload; else msg.error = payload; if (event.source && event.source.postMessage) { event.source.postMessage(msg, '*'); } else { window.parent.postMessage(msg, '*'); } } try { // Pixel-perfect master: reuse the same exact-live-match capture as the 's' key. // renderPixelPerfectSquare() is a top-level function in the sketch script and shares // this page's global scope. Output is a fixed pixel-perfect square, so the requested // options.size is ignored. if (typeof renderPixelPerfectSquare !== 'function') { respond(false, 'renderer not ready'); return; } var out = renderPixelPerfectSquare(); respond(true, out.toDataURL('image/png')); } catch (err) { respond(false, (err && err.message) || 'capture failed'); } }); })(); </script> </body> </html>
() pg.translate(-hollowX, hollowY) pg.beginShape() pg.vertex(-hollowWidth * 0.35, -hollowHeight * 0.5) pg.vertex(hollowWidth * 0.35, -hollowHeight * 0.5) pg.bezierVertex( hollowWidth * 0.45, -hollowHeight * 0.2, hollowWidth * 0.5, hollowHeight * 0.2, hollowWidth * 0.4, hollowHeight * 0.5 ) pg.vertex(-hollowWidth * 0.4, hollowHeight * 0.5) pg.bezierVertex( -hollowWidth * 0.5, hollowHeight * 0.2, -hollowWidth * 0.45, -hollowHeight * 0.2, -hollowWidth * 0.35, -hollowHeight * 0.5 ) pg.endShape(CLOSE) pg.pop() } else { pg.push() pg.translate(hollowX, hollowY) pg.beginShape() pg.vertex(-hollowWidth * 0.35, -hollowHeight * 0.5) pg.vertex(hollowWidth * 0.35, -hollowHeight * 0.5) pg.bezierVertex( hollowWidth * 0.45, -hollowHeight * 0.2, hollowWidth * 0.5, hollowHeight * 0.2, hollowWidth * 0.4, hollowHeight * 0.5 ) pg.vertex(-hollowWidth * 0.4, hollowHeight * 0.5) pg.bezierVertex( -hollowWidth * 0.5, hollowHeight * 0.2, -hollowWidth * 0.45, -hollowHeight * 0.2, -hollowWidth * 0.35, -hollowHeight * 0.5 ) pg.endShape(CLOSE) pg.pop() } } else { pg.push() pg.translate(-hollowX, hollowY) pg.beginShape() pg.vertex(-hollowWidth * 0.35, -hollowHeight * 0.5) pg.vertex(hollowWidth * 0.35, -hollowHeight * 0.5) pg.bezierVertex( hollowWidth * 0.45, -hollowHeight * 0.2, hollowWidth * 0.5, hollowHeight * 0.2, hollowWidth * 0.4, hollowHeight * 0.5 ) pg.vertex(-hollowWidth * 0.4, hollowHeight * 0.5) pg.bezierVertex( -hollowWidth * 0.5, hollowHeight * 0.2, -hollowWidth * 0.45, -hollowHeight * 0.2, -hollowWidth * 0.35, -hollowHeight * 0.5 ) pg.endShape(CLOSE) pg.pop() pg.push() pg.translate(hollowX, hollowY) pg.beginShape() pg.vertex(-hollowWidth * 0.35, -hollowHeight * 0.5) pg.vertex(hollowWidth * 0.35, -hollowHeight * 0.5) pg.bezierVertex( hollowWidth * 0.45, -hollowHeight * 0.2, hollowWidth * 0.5, hollowHeight * 0.2, hollowWidth * 0.4, hollowHeight * 0.5 ) pg.vertex(-hollowWidth * 0.4, hollowHeight * 0.5) pg.bezierVertex( -hollowWidth * 0.5, hollowHeight * 0.2, -hollowWidth * 0.45, -hollowHeight * 0.2, -hollowWidth * 0.35, -hollowHeight * 0.5 ) pg.endShape(CLOSE) pg.pop() } } } function drawOrnaments(pg, m) { if (m.ornament === "Bare") return pg.push() pg.noStroke() pg.fill(255) if (m.ornament === "Links") { const earringSize = m.w * 0.08 const earringX = m.w * 0.40 const earringY = m.h * 0.15 pg.circle(-earringX, earringY, earringSize) pg.circle(earringX, earringY, earringSize) const smallSize1 = earringSize * 0.65 const smallSize2 = earringSize * 0.5 const smallSize3 = earringSize * 0.35 const dropSpacing = earringSize * 0.55 pg.circle(-earringX, earringY + dropSpacing, smallSize1) pg.circle(-earringX, earringY + dropSpacing * 1.8, smallSize2) pg.circle(-earringX, earringY + dropSpacing * 2.5, smallSize3) pg.circle(earringX, earringY + dropSpacing, smallSize1) pg.circle(earringX, earringY + dropSpacing * 1.8, smallSize2) pg.circle(earringX, earringY + dropSpacing * 2.5, smallSize3) } else if (m.ornament === "Amp") { pg.rectMode(CENTER) const markWidth = m.w * 0.03 const markHeight = m.h * 0.15 const markX = m.w * 0.38 const markY = 0 pg.rect(-markX, markY, markWidth, markHeight) pg.rect(markX, markY, markWidth, markHeight) } else if (m.ornament === "Crest") { const spikeCount = 5 const baseWidth = m.w * 0.1 const spacing = m.w * 0.15 const maskTopY = -m.h * 0.4 const heights = [ m.h * 0.16, m.h * 0.22, m.h * 0.26, m.h * 0.22, m.h * 0.16 ] const verticalOffsets = [50, 0, 0, 0, 50] pg.stroke(0, 0, 0) pg.strokeWeight(m.w * 0.015) for (let i = 0; i < spikeCount; i++) { const x = (i - (spikeCount - 1) / 2) * spacing const spikeHeight = heights[i] const baseY = maskTopY + verticalOffsets[i] const spikeY = baseY - spikeHeight const isOuter = (i === 0 || i === spikeCount - 1) const angleOffset = isOuter ? (i === 0 ? baseWidth * 0.3 : -baseWidth * 0.3) : 0 pg.triangle( x, spikeY, x - baseWidth / 2 + (i === 0 ? angleOffset : 0), baseY, x + baseWidth / 2 + (i === spikeCount - 1 ? angleOffset : 0), baseY ) const connectorHeight = m.h * 0.10 const topWidth = baseWidth * 0.9 const bottomWidth = baseWidth * 0.6 pg.beginShape() pg.vertex(x - baseWidth / 2 + (i === 0 ? angleOffset : 0), baseY) pg.vertex(x + baseWidth / 2 + (i === spikeCount - 1 ? angleOffset : 0), baseY) pg.vertex(x + bottomWidth / 2, baseY + connectorHeight) pg.vertex(x - bottomWidth / 2, baseY + connectorHeight) pg.endShape(CLOSE) } pg.noStroke() } else if (m.ornament === "Bestia") { pg.fill(0, 0, 10) const hornBaseY = -m.h * 0.25 const hornSpacing = m.w * 0.25 const hornHeight = m.h * 0.35 const hornWidth = m.w * 0.12 for (let side of [-1, 1]) { pg.push() pg.translate(hornSpacing * side, hornBaseY) pg.scale(side, 1) pg.rotate(-0.25) pg.beginShape() pg.vertex(hornWidth * 0.3, 0) pg.vertex(-hornWidth * 0.3, 0) pg.bezierVertex( -hornWidth * 0.5, -hornHeight * 0.15, -hornWidth * 0.7, -hornHeight * 0.25, -hornWidth * 0.6, -hornHeight * 0.35 ) pg.vertex(-hornWidth * 0.9, -hornHeight * 0.32) pg.vertex(-hornWidth * 0.55, -hornHeight * 0.45) pg.bezierVertex( -hornWidth * 0.65, -hornHeight * 0.55, -hornWidth * 0.6, -hornHeight * 0.65, -hornWidth * 0.45, -hornHeight * 0.72 ) pg.vertex(-hornWidth * 0.75, -hornHeight * 0.70) pg.vertex(-hornWidth * 0.35, -hornHeight * 0.82) pg.bezierVertex( -hornWidth * 0.3, -hornHeight * 0.92, -hornWidth * 0.1, -hornHeight * 0.98, hornWidth * 0.05, -hornHeight ) pg.bezierVertex( hornWidth * 0.15, -hornHeight * 0.75, hornWidth * 0.2, -hornHeight * 0.45, hornWidth * 0.3, 0 ) pg.endShape(CLOSE) pg.pop() } } else if (m.ornament === "Root") { const tuskWidth = m.w * 0.15 const tuskHeight = m.h * 0.4 const tuskSpacing = m.w * 0.20 const tuskY = m.h * 0.22 pg.push() pg.translate(-tuskSpacing, tuskY) pg.beginShape() pg.vertex(-tuskWidth * 0.35, 0) pg.bezierVertex( -tuskWidth * 0.25, tuskHeight * 0.6, -tuskWidth * 0.05, tuskHeight * 0.8, 0, tuskHeight ) pg.bezierVertex( tuskWidth * 0.35, tuskHeight * 0.8, tuskWidth * 0.5, tuskHeight * 0.6, tuskWidth * 0.05, 0 ) pg.endShape(CLOSE) pg.pop() pg.push() pg.translate(tuskSpacing, tuskY) pg.beginShape() pg.vertex(-tuskWidth * 0.35, 0) pg.bezierVertex( -tuskWidth * 0.25, tuskHeight * 0.6, -tuskWidth * 0.15, tuskHeight * 0.8, 0, tuskHeight ) pg.bezierVertex( tuskWidth * 0.15, tuskHeight * 0.8, tuskWidth * 0.25, tuskHeight * 0.6, tuskWidth * 0.35, 0 ) pg.endShape(CLOSE) pg.pop() } else if (m.ornament === "Gauge") { const gaugeSize = m.w * 0.12 const gaugeX = m.w * 0.42 const gaugeY = m.h * 0.12 pg.circle(-gaugeX, gaugeY, gaugeSize) pg.circle(gaugeX, gaugeY, gaugeSize) pg.push() pg.erase() const holeSize = gaugeSize * 0.5 pg.circle(-gaugeX, gaugeY, holeSize) pg.circle(gaugeX, gaugeY, holeSize) pg.noErase() pg.pop() } else if (m.ornament === "Thorns") { const spikeCount = 3 const spikeWidth = m.w * 0.05 const spikeHeight = m.h * 0.08 const earX = m.w * 0.42 const startY = m.h * 0.05 const spacing = m.h * 0.1 for (let side of [-1, 1]) { for (let i = 0; i < spikeCount; i++) { const x = earX * side const y = startY + i * spacing pg.push() pg.translate(x, y) pg.triangle( 0, -spikeHeight * 0.5, -spikeWidth * 0.5 * side, spikeHeight * 0.5, spikeWidth * 0.5 * side, spikeHeight * 0.5 ) pg.pop() } } } else if (m.ornament === "Tandem") { const earringSize = m.w * 0.06 const earringX = m.w * 0.44 const earringY1 = m.h * 0.10 const earringY2 = m.h * 0.20 pg.circle(-earringX, earringY1, earringSize) pg.circle(earringX, earringY1, earringSize) pg.circle(-earringX, earringY2, earringSize * 0.8) pg.circle(earringX, earringY2, earringSize * 0.8) } else if (m.ornament === "Spike") { const spikeWidth = m.w * 0.12 const spikeHeight = m.h * 0.22 const chinY = m.h * 0.32 pg.push() pg.translate(0, chinY) pg.triangle( -spikeWidth / 2, 0, spikeWidth / 2, 0, 0, spikeHeight ) pg.pop() } pg.pop() } const vertSrc = ` attribute vec3 aPosition; attribute vec2 aTexCoord; varying vec2 vTexCoord; void main() { vTexCoord = vec2(aTexCoord.x, 1.0 - aTexCoord.y); vec4 positionVec4 = vec4(aPosition, 1.0); positionVec4.xy = positionVec4.xy * 2.0 - 1.0; gl_Position = positionVec4; } `; const fragDigitalStitchOptimized = ` #ifdef GL_ES precision mediump float; #endif varying vec2 vTexCoord; uniform float u_time; uniform vec2 u_resolution; uniform sampler2D u_texture; uniform float u_gridX; uniform bool u_forceMonochrome; uniform bool u_applyNoise; uniform float u_symbolOffset; uniform float u_vocabSize; uniform float u_symbolSize; uniform bool u_bwMode; float hash21(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); } // Simple 2D Perlin-style noise function float noise2D(vec2 p) { vec2 i = floor(p); vec2 f = fract(p); // Smooth interpolation f = f * f * (3.0 - 2.0 * f); // Four corners float a = hash21(i); float b = hash21(i + vec2(1.0, 0.0)); float c = hash21(i + vec2(0.0, 1.0)); float d = hash21(i + vec2(1.0, 1.0)); // Bilinear interpolation return mix(mix(a, b, f.x), mix(c, d, f.x), f.y); } // Convert RGB to HSB vec3 rgb2hsb(vec3 c) { vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0); vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g)); vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r)); float d = q.x - min(q.w, q.y); float e = 1.0e-10; return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x); } // Convert HSB to RGB vec3 hsb2rgb(vec3 c) { vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); } float getCharPattern(float shapeId, vec2 cellUV, float brightness){ vec2 p = (cellUV - 0.5) / u_symbolSize; int id = int(shapeId); if (id == 0) { // Horizontal bar return step(abs(p.y), 0.28); } if (id == 1) { // Vertical bar return step(abs(p.x), 0.28); } if (id == 2) { // Filled circle return step(length(p), 0.48); } if (id == 3) { // Square return step(max(abs(p.x), abs(p.y)), 0.45); } if (id == 4) { // Diamond return step(abs(p.x) + abs(p.y), 0.65); } if (id == 5) { // Thick cross return max(step(abs(p.y), 0.22), step(abs(p.x), 0.22)); } if (id == 6) { // Ring float ring = length(p); return step(ring, 0.48) * (1.0 - step(ring, 0.12)); } if (id == 7) { // Checkerboard return mod(floor(cellUV.x*2.0) + floor(cellUV.y*2.0), 2.0); } if (id == 8) { // Small circle return step(length(p), 0.38); } if (id == 9) { // X return step(abs(abs(p.x) - abs(p.y)), 0.18); } if (id == 10) { // Rounded quad vec2 corner = abs(p) - vec2(0.22); return step(length(corner), 0.22); } if (id == 11) { // Thin cross return max(step(abs(p.y), 0.18), step(abs(p.x), 0.18)); } if (id == 12) { // Heart float lobeL = step(length(p - vec2(-0.15, 0.13)), 0.20); float lobeR = step(length(p - vec2(0.15, 0.13)), 0.20); float tri = step(abs(p.x) - (p.y + 0.35) * 0.73, 0.0) * step(-0.35, p.y) * step(p.y, 0.13); return min(lobeL + lobeR + tri, 1.0); } if (id == 13) { // 5-petal flower float a = atan(p.y, p.x); float r = length(p); return step(r, abs(cos(a * 2.5)) * 0.15 + 0.25); } if (id == 14) { // Triangle up return step(abs(p.x) * 0.577 + p.y, 0.25) * step(-p.y, 0.35); } if (id == 15) { // Triangle down return step(abs(p.x) * 0.577 - p.y, 0.25) * step(p.y, 0.35); } if (id == 16) { // Arrow up float shaft = step(abs(p.x), 0.1) * step(p.y, 0.1) * step(-0.3, p.y); float head = step(abs(p.x) - (0.3 - p.y), 0.0) * step(0.1, p.y) * step(p.y, 0.3); return max(shaft, head); } if (id == 17) { // Arrow right float shaft = step(abs(p.y), 0.1) * step(p.x, 0.1) * step(-0.3, p.x); float head = step(abs(p.y) - (0.3 - p.x), 0.0) * step(0.1, p.x) * step(p.x, 0.3); return max(shaft, head); } if (id == 18) { // Hexagon vec2 q = abs(p); return step(q.x * 0.866 + q.y * 0.5, 0.4); } if (id == 19) { // Dot return step(length(p), 0.12); } if (id == 20) { // Two circles return max(step(length(p - vec2(-0.15, 0.0)), 0.15), step(length(p - vec2(0.15, 0.0)), 0.15)); } if (id == 21) { // Grid return max(step(0.7, fract(cellUV.x * 3.0)), step(0.7, fract(cellUV.y * 3.0))); } if (id == 22) { // Diagonal / return step(0.7, fract((cellUV.x + cellUV.y) * 5.0)); } if (id == 23) { // Diagonal \\ return step(0.7, fract((cellUV.x - cellUV.y) * 5.0)); } if (id == 24) { // Cross with dots float plus = max(step(abs(p.y), 0.12), step(abs(p.x), 0.12)); float dots = step(length(p - vec2(0.3, 0.3)), 0.08) + step(length(p - vec2(-0.3, 0.3)), 0.08) + step(length(p - vec2(0.3, -0.3)), 0.08) + step(length(p - vec2(-0.3, -0.3)), 0.08); return min(plus + dots, 1.0); } if (id == 25) { // Four corner dots return max(max(step(length(p - vec2(-0.2, -0.2)), 0.15), step(length(p - vec2(0.2, -0.2)), 0.15)), max(step(length(p - vec2(-0.2, 0.2)), 0.15), step(length(p - vec2(0.2, 0.2)), 0.15))); } if (id == 26) { // Quincunx float center = step(length(p), 0.15); float pts = step(length(p - vec2(0.0, 0.25)), 0.12) + step(length(p - vec2(0.25, 0.0)), 0.12) + step(length(p - vec2(0.0, -0.25)), 0.12) + step(length(p - vec2(-0.25, 0.0)), 0.12); return min(center + pts, 1.0); } if (id == 27) { // Octagon vec2 q = abs(p); return step(max(q.x * 0.707 + q.y * 0.707, max(q.x, q.y)), 0.4); } if (id == 28) { // Vertical stripes return step(0.6, fract(cellUV.x * 4.0)); } if (id == 29) { // Horizontal stripes return step(0.6, fract(cellUV.y * 4.0)); } if (id == 30) { // Concentric rings float r = length(p); return step(0.7, fract(r * 10.0)); } if (id == 31) { // Crescent float outer = step(length(p), 0.38); float inner = step(length(p - vec2(0.18, 0.0)), 0.32); return outer * (1.0 - inner); } if (id == 32) { // 5-pointed star float a = atan(p.y, p.x) + 1.5708; float r = length(p); return step(r, cos(floor(0.5 + a / 1.2566) * 1.2566 - a) * 0.45); } if (id == 33) { // Scouts [REPLACED] float a = atan(p.y, p.x); float r = length(p); float star = step(r, abs(cos(a * 4.0)) * 0.32 + 0.05); float diamond = step(abs(p.x) + abs(p.y), 0.10); return max(star, diamond); } if (id == 34) { // Bowtie [REPLACED] return step(abs(p.x) * 0.5 - abs(p.y), 0.0) * step(abs(p.x), 0.4) * step(abs(p.y), 0.35); } if (id == 35) { // Rook float body = step(abs(p.x), 0.22) * step(abs(p.y), 0.32); float notch1 = step(abs(p.x + 0.12), 0.04) * step(p.y, 0.35) * step(0.22, p.y); float notch2 = step(abs(p.x - 0.12), 0.04) * step(p.y, 0.35) * step(0.22, p.y); return body * (1.0 - notch1) * (1.0 - notch2); } if (id == 36) { // Circuit node [REPLACED] float c = step(length(p), 0.1); float l1 = step(abs(p.y), 0.03) * step(abs(p.x), 0.4); float l2 = step(abs(p.x), 0.03) * step(abs(p.y), 0.4); return min(c + l1 + l2, 1.0); } if (id == 37) { // Resistor [REPLACED] float l1 = step(abs(p.y), 0.03) * step(abs(p.x + 0.3), 0.1); float l2 = step(abs(p.y), 0.03) * step(abs(p.x - 0.3), 0.1); float box = step(abs(p.y), 0.1) * step(abs(p.x), 0.2); return min(l1 + l2 + box, 1.0); } if (id == 38) { // Trace L [REPLACED] float h = step(abs(p.y), 0.03) * step(p.x, 0.4) * step(-0.03, p.x); float v = step(abs(p.x), 0.03) * step(p.y, 0.03) * step(-0.4, p.y); return max(h, v); } if (id == 39) { // Lightning vec2 q = p * 2.5; float b1 = step(abs(q.x - 0.2), 0.18) * step(q.y, 0.8) * step(0.0, q.y); float b2 = step(abs(q.x + 0.1), 0.25) * step(q.y, 0.15) * step(-0.15, q.y); float b3 = step(abs(q.x + 0.2), 0.18) * step(q.y, -0.0) * step(-0.8, q.y); return min(b1 + b2 + b3, 1.0); } if (id == 40) { // Hourglass float w = min(abs(p.y) * 0.9 + 0.03, 0.28); return step(abs(p.x), w) * step(abs(p.y), 0.38); } if (id == 41) { // Infinity float l1 = length(p - vec2(-0.18, 0.0)); float l2 = length(p - vec2(0.18, 0.0)); return max(step(l1, 0.2) * (1.0 - step(l1, 0.08)), step(l2, 0.2) * (1.0 - step(l2, 0.08))); } if (id == 42) { // Wave crest [REPLACED] return step(abs(p.y - sin(p.x * 8.0) * 0.1), 0.06) * step(abs(p.x), 0.4); } if (id == 43) { // Eye float almond = step(abs(p.x) * 0.5 + abs(p.y), 0.22); return max(almond * 0.7, step(length(p), 0.08)); } if (id == 44) { // Skull float head = step(length(p - vec2(0.0, 0.08)), 0.26); float jaw = step(abs(p.x), 0.14) * step(abs(p.y + 0.2), 0.1); float eyeL = 1.0 - step(length(p - vec2(-0.09, 0.12)), 0.07); float eyeR = 1.0 - step(length(p - vec2(0.09, 0.12)), 0.07); return min(head + jaw, 1.0) * eyeL * eyeR; } if (id == 45) { // Spade float lobeL = step(length(p - vec2(-0.15, -0.05)), 0.20); float lobeR = step(length(p - vec2(0.15, -0.05)), 0.20); float tri = step(abs(p.x) - (0.32 - p.y) * 0.95, 0.0) * step(-0.05, p.y) * step(p.y, 0.32); float stem = step(abs(p.x), 0.07) * step(p.y, -0.22) * step(-0.34, p.y); return min(lobeL + lobeR + tri + stem, 1.0); } if (id == 46) { // Club float c1 = step(length(p - vec2(0.0, 0.18)), 0.16); float c2 = step(length(p - vec2(-0.16, -0.05)), 0.16); float c3 = step(length(p - vec2(0.16, -0.05)), 0.16); float center = step(length(p - vec2(0.0, 0.03)), 0.10); float stem = step(abs(p.x), 0.07) * step(p.y, -0.05) * step(-0.34, p.y); return min(c1 + c2 + c3 + center + stem, 1.0); } if (id == 47) { // Trefoil float c1 = step(length(p - vec2(0.0, 0.15)), 0.14); float c2 = step(length(p - vec2(-0.13, -0.05)), 0.14); float c3 = step(length(p - vec2(0.13, -0.05)), 0.14); return min(c1 + c2 + c3, 1.0); } if (id == 48) { // Peace sign float r = length(p); float ring = step(r, 0.35) * (1.0 - step(r, 0.27)); float inside = step(r, 0.30); float vert = step(abs(p.x), 0.04) * inside; float legL = step(abs(p.x + p.y * 0.65), 0.04) * step(p.y, 0.0) * inside; float legR = step(abs(p.x - p.y * 0.65), 0.04) * step(p.y, 0.0) * inside; return min(ring + vert + legL + legR, 1.0); } if (id == 49) { // Warriors [REPLACED] float armH = step(abs(p.y), max(0.0, 0.12 - abs(p.x) * 0.32)) * step(abs(p.x), 0.38); float armV = step(abs(p.x), max(0.0, 0.12 - abs(p.y) * 0.32)) * step(abs(p.y), 0.38); float diamond = step(abs(p.x) + abs(p.y), 0.13); return max(max(armH, armV), diamond); } if (id == 50) { // Nobles [REPLACED] float d1 = step(abs(p.x) + abs(p.y), 0.12); float d2 = step(abs(p.x) + abs(p.y - 0.26), 0.10); float d3 = step(abs(p.x) + abs(p.y + 0.26), 0.10); float d4 = step(abs(p.x - 0.26) + abs(p.y), 0.10); float d5 = step(abs(p.x + 0.26) + abs(p.y), 0.10); return min(d1 + d2 + d3 + d4 + d5, 1.0); } if (id == 51) { // Conflux [REPLACED] float c = 0.866; float s = 0.5; vec2 q1 = vec2((p.x - 0.10) * c + (p.y - 0.08) * s, -(p.x - 0.10) * s + (p.y - 0.08) * c); vec2 q2 = vec2((p.x + 0.10) * c + (p.y + 0.08) * s, -(p.x + 0.10) * s + (p.y + 0.08) * c); float r1 = step(max(abs(q1.x), abs(q1.y)), 0.14); float r2 = step(max(abs(q2.x), abs(q2.y)), 0.14); return min(r1 + r2, 1.0); } return step(max(abs(p.x), abs(p.y)), 0.2); // fallback } void main(){ vec2 uv = vTexCoord; float GRID_X = max(20.0, u_gridX); float aspect = u_resolution.x / u_resolution.y; vec2 gridCount = vec2(GRID_X, GRID_X / aspect); vec2 gridUV = uv * gridCount; vec2 cellID = floor(gridUV); vec2 cellUV = fract(gridUV); vec2 cellCenter = (cellID + 0.5) / gridCount; vec4 cellColor = texture2D(u_texture, cellCenter); if (cellColor.a < 0.01) { gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0); return; } // Enhanced brightness calculation with better contrast float cellBri = dot(cellColor.rgb, vec3(0.299, 0.587, 0.114)); cellBri = pow(cellBri, 0.8); // Gamma adjustment for better distribution // Conditionally apply Perlin noise texture // Slow-moving organic texture across the surface if (u_applyNoise) { vec2 noiseCoord = cellCenter * u_resolution * 0.003 + u_time * 0.05; float noiseVal = noise2D(noiseCoord); // Apply subtle noise variation to brightness (affects pattern selection) cellBri = cellBri + (noiseVal - 0.5) * 0.12; } cellBri = clamp(cellBri, 0.0, 1.0); // Symbol selection — brightness picks an index within the active vocabulary, // which maps to a contiguous block of slots starting at u_symbolOffset. // u_vocabSize controls how many distinct symbols a piece uses (1, 2, 5, 10, or 52). float TYPES = 52.0; float vocab = max(u_vocabSize, 1.0); float idx = floor(cellBri * vocab); idx = min(idx, vocab - 1.0); // clamp top edge (cellBri = 1.0 case) float shapeId = mod(u_symbolOffset + idx, TYPES); // Get the pattern mask float mask = getCharPattern(shapeId, cellUV, cellBri); mask = clamp(mask, 0.0, 1.0); // Convert cell color to HSB vec3 hsbColor = rgb2hsb(cellColor.rgb); // Check if this is a grayscale/desaturated color (like the form) float isGrayscale = step(hsbColor.y, 0.15); // Saturation threshold — lower = more cells use chromatic path // OVERRIDE: Force monochrome mode if requested if (u_forceMonochrome) { isGrayscale = 1.0; } vec3 bgColor, symbolColor; if (isGrayscale > 0.5 || u_forceMonochrome) { // MONOCHROMATIC MODE - use single hue with varying sat/brightness float baseHue = hsbColor.x; if (u_forceMonochrome) { // For monotone palettes: create monochromatic scheme bgColor = hsb2rgb(vec3(baseHue, hsbColor.y * 0.3, hsbColor.z * 1.2)); symbolColor = hsb2rgb(vec3(baseHue, hsbColor.y * 0.9, hsbColor.z * 0.7)); } else { // For form/grayscale elements: true grayscale bgColor = vec3(hsbColor.z * 1.15); symbolColor = vec3(hsbColor.z * 0.6); } } else { // COMPLEMENTARY COLOR MODE (for vibrant mask fills) // Create background color - slightly adjusted version of original bgColor = hsb2rgb(vec3(hsbColor.x, hsbColor.y * 0.8, hsbColor.z * 1.2)); // Create symbol color - complementary hue float symbolHue = mod(hsbColor.x + 0.5, 1.0); // Vary symbol color based on cell position for more variety float cellHash = hash21(cellID); float hueVariation = (cellHash - 0.5) * 0.2; symbolHue = mod(symbolHue + hueVariation, 1.0); // Choose saturation and brightness for symbol float symbolSat = mix(0.7, 1.0, cellHash); float symbolBri = cellBri > 0.5 ? 0.3 : 0.9; symbolColor = hsb2rgb(vec3(symbolHue, symbolSat, symbolBri)); } // Mix background and symbol colors vec3 finalColor = mix(bgColor, symbolColor, mask); // Ink mode — pure black & white, hard luminance threshold (Etched palette). if (u_bwMode) { float lum = dot(finalColor, vec3(0.299, 0.587, 0.114)); finalColor = lum > 0.5 ? vec3(1.0) : vec3(0.0); } gl_FragColor = vec4(finalColor, cellColor.a); } ` function logTraits() { if (!DEBUG) return console.log('%c╔═══════════════════════════════════════════════════════════╗', 'color: #FF00FF;') console.log('%c║ SATARI TRAITS ║', 'color: #FF00FF; font-weight: bold; font-size: 16px;') console.log('%c╚═══════════════════════════════════════════════════════════╝', 'color: #FF00FF;') console.log(`%c SEED: ${CURRENT_SEED}`, 'color: #FFD700; font-weight: bold; font-size: 14px;') console.log('') console.log('%c ─── PRIMARY ───', 'color: #00FFFF; font-weight: bold;') console.log(`%c Form: %c${TRAITS.displayedArchetype}`, 'color: #888;', 'color: #FFF; font-weight: bold;') console.log(`%c House: %c${TRAITS.house}`, 'color: #888;', 'color: #FFF; font-weight: bold;') console.log(`%c Class: %c${TRAITS.role}`, 'color: #888;', 'color: #FFF; font-weight: bold;') console.log(`%c Palette: %c${TRAITS.paletteName}${TRAITS._basePalette ? ' (over ' + TRAITS._basePalette + ')' : ''}`, 'color: #888;', 'color: #FFF; font-weight: bold;') console.log(`%c Realm: %c${TRAITS.realm}`, 'color: #888;', 'color: #FFF; font-weight: bold;') console.log(`%c Aura: %c${TRAITS.aura}`, 'color: #888;', 'color: #FFF; font-weight: bold;') console.log('') console.log('%c ─── IDENTITY ───', 'color: #00FFFF; font-weight: bold;') console.log(`%c Doctrine: %c${TRAITS.doctrine}`, 'color: #888;', 'color: #FFF;') console.log(`%c Spacecraft: %c${TRAITS.spacecraft}`, 'color: #888;', 'color: #FFF;') console.log('') console.log('%c ─── FACE ───', 'color: #00FFFF; font-weight: bold;') console.log(`%c Sight: %c${TRAITS.sight}`, 'color: #888;', 'color: #FFF;') console.log(`%c Iris: %c${TRAITS.iris}`, 'color: #888;', 'color: #FFF;') console.log(`%c Voice: %c${TRAITS.voice}`, 'color: #888;', 'color: #FFF;') console.log(`%c Scent: %c${TRAITS.scent}`, 'color: #888;', 'color: #FFF;') console.log(`%c Hollows: %c${TRAITS.hollows}`, 'color: #888;', 'color: #FFF;') console.log(`%c Ornament: %c${TRAITS.ornament}`, 'color: #888;', 'color: #FFF;') console.log('') console.log('%c ─── ANIMATION ───', 'color: #00FFFF; font-weight: bold;') console.log(`%c Fill: %c${TRAITS.fillVariant}`, 'color: #888;', 'color: #FFF;') console.log(`%c Blend: %c${TRAITS.blendSchema}`, 'color: #888;', 'color: #FFF;') console.log(`%c Origin: %c${TRAITS.originTopology}`, 'color: #888;', 'color: #FFF;') console.log(`%c Spawn: %c${TRAITS.spawnRhythm}`, 'color: #888;', 'color: #FFF;') console.log('') console.log('%c ─── CONTROLS ───', 'color: #888;') console.log('%c N = Restart | S = Save PNG | Space = Freeze | Esc = Reset Modes', 'color: #666; font-size: 11px;') console.log('%c═══════════════════════════════════════════════════════════════', 'color: #FF00FF;') } function resetAndRegenerate() { fillAnimations = [] orbitingElements = [] masks = [] maskBoundsCache = [] globalBounds = null eyesFullyOn = false regenStartFrame = frameCount // Track when this regeneration started nextSpawnFrame = 0 _breathStartMillis = null if (animationPool) { animationPool.clear() } if (fillPersistentLayer) { fillPersistentLayer.clear() } applySeed() TRAITS = buildTraits() bwMode = TRAITS.paletteName === "Etched" PARAMS = resolveParams(TRAITS) logTraits() layoutMasks() calculateMaskBounds() isDirty = true drawStaticLayers() initTopology() initOrbitingElements() } // Exposed for external tools (e.g. the rater): swap to a new seed without // reloading the page. Keeps the same shader/canvas/buffers — only re-rolls // traits and rebuilds layout. Much faster than an iframe reload. window.regenerateWithSeed = function (newSeed) { CURRENT_SEED = newSeed >>> 0 resetAndRegenerate() } function getTierForWidth(displayWidth) { let tier = QUALITY_TIERS[0] for (const t of QUALITY_TIERS) { if (displayWidth >= t.minWidth) tier = t } return tier } function getDisplayWidth() { const canvas = document.querySelector('canvas') if (canvas) return canvas.getBoundingClientRect().width return window.innerWidth } function resizeAllBuffers(w, h) { // The render canvas is off-DOM (the DOM holds the square display canvas), // so reach it via the shim global rather than querySelector. const mainCanvas = _mainCanvas if (mainCanvas) { mainCanvas.width = w mainCanvas.height = h width = w height = h _designScale = w / 750 if (_mainPG) { _mainPG.width = w _mainPG.height = h _mainPG.canvas = mainCanvas _mainPG.drawingContext = mainCanvas.getContext('2d') _mainPG.drawingContext.imageSmoothingEnabled = false window.drawingContext = _mainPG.drawingContext } } if (_displayCanvas) { _displayCanvas.width = w _displayCanvas.height = w // Resizing resets 2d context state — restore nearest-neighbor blits. _displayCtx.imageSmoothingEnabled = false } staticLayerCache = createGraphics(w, h) staticLayerCache.pixelDensity(1) staticLayerCache.colorMode(HSB, 360, 100, 100, 100) formPart1Cache = createGraphics(w, h) formPart1Cache.pixelDensity(1) formPart1Cache.colorMode(HSB, 360, 100, 100, 100) formPart2Cache = createGraphics(w, h) formPart2Cache.pixelDensity(1) formPart2Cache.colorMode(HSB, 360, 100, 100, 100) maskSilhouetteCache = createGraphics(w, h) maskSilhouetteCache.pixelDensity(1) fillBuffer = createGraphics(w, h) fillBuffer.pixelDensity(1) fillBuffer.colorMode(HSB, 360, 100, 100, 100) fillPersistentLayer = createGraphics(w, h) fillPersistentLayer.pixelDensity(1) fillPersistentLayer.colorMode(HSB, 360, 100, 100, 100) eyesCache = createGraphics(w, h) eyesCache.pixelDensity(1) eyesCache.colorMode(HSB, 360, 100, 100, 100) eyesBlockingCache = createGraphics(w, h) eyesBlockingCache.pixelDensity(1) eyesBlockingCache.colorMode(HSB, 360, 100, 100, 100) omniaEyesBase = createGraphics(w, h) omniaEyesBase.pixelDensity(1) omniaEyesBase.colorMode(HSB, 360, 100, 100, 100) orbitBuffer = createGraphics(w, h) orbitBuffer.pixelDensity(1) orbitBuffer.colorMode(HSB, 360, 100, 100, 100) meltBuffer = createGraphics(w, h) meltBuffer.pixelDensity(1) meltBuffer.colorMode(HSB, 360, 100, 100, 100) corruptBuffer = createGraphics(w, h) corruptBuffer.pixelDensity(1) corruptBuffer.colorMode(HSB, 360, 100, 100, 100) clipBuffer = createGraphics(w, h) clipBuffer.pixelDensity(1) clipBuffer.colorMode(HSB, 360, 100, 100, 100) finalComposite = createGraphics(w, h) finalComposite.pixelDensity(1) finalComposite.colorMode(HSB, 360, 100, 100, 100) finalShadedBuffer = createGraphics(w, h, WEBGL) finalShadedBuffer.pixelDensity(1) finalShadedBuffer.noStroke() finalShadedBuffer.rectMode(CENTER) asciiShader = finalShadedBuffer.createShader(vertSrc, fragDigitalStitchOptimized) } function applyTier(tier) { const prevTier = _currentTier _currentTier = tier _tierMaxAnims = tier.maxAnims _tierGridDensity = tier.gridCols _tierNoiseOverride = tier.noise ? null : false frameRate(tier.fps) if (!prevTier || prevTier.canvasW !== tier.canvasW) { const newW = tier.canvasW const newH = floor(newW * ASPECT_RATIO) if (width !== newW && width > 0) { resizeAllBuffers(newW, newH) resetAndRegenerate() } } } function initTierObserver() { // Capture is a one-shot static render. Skip the tier/visibility observers: // the visibility observer in particular flips _running back on and restarts // the draw loop when the canvas is on-screen, which would undo the capture // freeze and resume animation (trail/persist-buffer shimmer, etc.). if (typeof window.$art !== 'undefined' && window.$art.captureMode) return if (typeof ResizeObserver === 'undefined') return const canvas = document.querySelector('canvas') if (!canvas) return let _resizeTimeout = null const observer = new ResizeObserver(() => { clearTimeout(_resizeTimeout) _resizeTimeout = setTimeout(() => { const displayW = canvas.getBoundingClientRect().width const tier = getTierForWidth(displayW) if (!_currentTier || tier.canvasW !== _currentTier.canvasW) { _pendingTier = tier // Defer — applied at start of next draw() } }, 150) }) observer.observe(canvas) if (typeof IntersectionObserver !== 'undefined') { const visObserver = new IntersectionObserver((entries) => { _running = entries[0].isIntersecting if (_running) requestAnimationFrame(_loop) }, { threshold: 0.05 }) visObserver.observe(canvas) } } function setup() { readSeed() applySeed() const initialTier = getTierForWidth(getDisplayWidth()) _currentTier = initialTier _tierMaxAnims = initialTier.maxAnims _tierGridDensity = initialTier.gridCols _tierNoiseOverride = initialTier.noise ? null : false const canvasW = initialTier.canvasW let canvas = createCanvas(canvasW, floor(canvasW * ASPECT_RATIO)) pixelDensity(1) frameRate(initialTier.fps) colorMode(HSB, 360, 100, 100, 100) drawingContext.imageSmoothingEnabled = false _designScale = width / 750 animationPool = new AnimationPool() TRAITS = buildTraits() bwMode = TRAITS.paletteName === "Etched" PARAMS = resolveParams(TRAITS) staticLayerCache = createGraphics(width, height) staticLayerCache.pixelDensity(1) staticLayerCache.colorMode(HSB, 360, 100, 100, 100) formPart1Cache = createGraphics(width, height) formPart1Cache.pixelDensity(1) formPart1Cache.colorMode(HSB, 360, 100, 100, 100) formPart2Cache = createGraphics(width, height) formPart2Cache.pixelDensity(1) formPart2Cache.colorMode(HSB, 360, 100, 100, 100) maskSilhouetteCache = createGraphics(width, height) maskSilhouetteCache.pixelDensity(1) fillBuffer = createGraphics(width, height) fillBuffer.pixelDensity(1) fillBuffer.colorMode(HSB, 360, 100, 100, 100) fillPersistentLayer = createGraphics(width, height) fillPersistentLayer.pixelDensity(1) fillPersistentLayer.colorMode(HSB, 360, 100, 100, 100) eyesCache = createGraphics(width, height) eyesCache.pixelDensity(1) eyesCache.colorMode(HSB, 360, 100, 100, 100) eyesBlockingCache = createGraphics(width, height) eyesBlockingCache.pixelDensity(1) eyesBlockingCache.colorMode(HSB, 360, 100, 100, 100) omniaEyesBase = createGraphics(width, height) omniaEyesBase.pixelDensity(1) omniaEyesBase.colorMode(HSB, 360, 100, 100, 100) orbitBuffer = createGraphics(width, height) orbitBuffer.pixelDensity(1) orbitBuffer.colorMode(HSB, 360, 100, 100, 100) meltBuffer = createGraphics(width, height) meltBuffer.pixelDensity(1) meltBuffer.colorMode(HSB, 360, 100, 100, 100) corruptBuffer = createGraphics(width, height) corruptBuffer.pixelDensity(1) corruptBuffer.colorMode(HSB, 360, 100, 100, 100) clipBuffer = createGraphics(width, height) clipBuffer.pixelDensity(1) clipBuffer.colorMode(HSB, 360, 100, 100, 100) finalComposite = createGraphics(width, height) finalComposite.pixelDensity(1) finalComposite.colorMode(HSB, 360, 100, 100, 100) finalShadedBuffer = createGraphics(width, height, WEBGL) finalShadedBuffer.pixelDensity(1) finalShadedBuffer.noStroke() finalShadedBuffer.rectMode(CENTER) asciiShader = finalShadedBuffer.createShader(vertSrc, fragDigitalStitchOptimized) layoutMasks() calculateMaskBounds() drawStaticLayers() initTopology() initOrbitingElements() logTraits() initTierObserver() } function getBackgroundColor() { const bgType = PARAMS.background if (bgType === "Halo" || bgType === "Void") { return color(0, 0, 75) } else if (bgType === "Aero") { return color(200, 15, 85) } else if (bgType === "Rose") { return color(330, 25, 90) } else if (bgType === "Mint") { return color(150, 20, 88) } else if (bgType === "Mist") { return color(270, 18, 88) } else if (bgType === "Rise") { return color(20, 30, 90) } else if (bgType === "Fallen") { return color(0, 100, 90) } else { return color(PARAMS.palette[0]) } } function drawStaticLayers() { if (!isDirty) return staticLayerCache.clear() drawBackground(staticLayerCache, PARAMS.background) if (isTwoPartArchetype(TRAITS.archetype)) { formPart2Cache.clear() formPart2Cache.background(0, 0, 0, 0) drawArchetypePart2(formPart2Cache, TRAITS.count, TRAITS.archetype) formPart1Cache.clear() formPart1Cache.background(0, 0, 0, 0) drawArchetypePart1(formPart1Cache, TRAITS.count, TRAITS.archetype) } else { formPart2Cache.clear() formPart2Cache.background(0, 0, 0, 0) drawArchetype(formPart2Cache, TRAITS.count, TRAITS.archetype) formPart1Cache.clear() formPart1Cache.background(0, 0, 0, 0) } eyesCache.clear() eyesCache.background(0, 0, 0, 0) for (const m of masks) { drawMaskEyes(eyesCache, m) } if (TRAITS.iris === "Omnia") { omniaEyesBase.clear() omniaEyesBase.background(0, 0, 0, 0) for (const m of masks) { drawMaskEyesOmniaBase(omniaEyesBase, m) } } eyesBlockingCache.clear() eyesBlockingCache.background(0, 0, 0, 0) for (const m of masks) { drawMaskEyesBlocking(eyesBlockingCache, m) } maskSilhouetteCache.clear() maskSilhouetteCache.noStroke() maskSilhouetteCache.fill(255) for (const m of masks) drawMaskSilhouette(maskSilhouetteCache, m) isDirty = false } function stableRemove(arr, index) { arr.splice(index, 1) } function draw() { if (_pendingTier) { applyTier(_pendingTier) _pendingTier = null } fillPersistentLayer.drawingContext.globalCompositeOperation = 'source-over' fillPersistentLayer.drawingContext.globalAlpha = 1.0 clipBuffer.drawingContext.globalCompositeOperation = 'source-over' clipBuffer.drawingContext.globalAlpha = 1.0 meltBuffer.drawingContext.globalCompositeOperation = 'source-over' meltBuffer.drawingContext.globalAlpha = 1.0 corruptBuffer.drawingContext.globalCompositeOperation = 'source-over' corruptBuffer.drawingContext.globalAlpha = 1.0 fillBuffer.drawingContext.globalCompositeOperation = 'source-over' fillBuffer.drawingContext.globalAlpha = 1.0 finalComposite.drawingContext.globalCompositeOperation = 'source-over' finalComposite.drawingContext.globalAlpha = 1.0 orbitBuffer.drawingContext.globalCompositeOperation = 'source-over' orbitBuffer.drawingContext.globalAlpha = 1.0 if (!freezeMode) { for (let i = fillAnimations.length - 1; i >= 0; i--) { fillAnimations[i].update() if (fillAnimations[i].isDone()) { animationPool.release(fillAnimations[i]) stableRemove(fillAnimations, i) } } for (let orb of orbitingElements) { orb.update() } if (PARAMS.aura === "Radiant") { starburstRotation += 0.0025 } updateSkyfall() updateMelt() updateCorrupt() updateVent() } if (!freezeMode) { _cachedPart2Offset = formBreathOffsetPart2() _cachedPart1Offset = formBreathOffsetPart1() _cachedMaskOffset = maskBreathOffset() } const part2Offset = _cachedPart2Offset const part1Offset = _cachedPart1Offset const maskOffset = _cachedMaskOffset fillPersistentLayer.clear() fillPersistentLayer.background(0, 0, 0, 0) for (let s of fillAnimations) { fillBuffer.clear() s.display(fillBuffer) fillPersistentLayer.image(fillBuffer, 0, 0) } if (!freezeMode) spawner() if (PARAMS.aura !== "CorruptMask") { const clipCtx = clipBuffer.drawingContext clipBuffer.clear() clipCtx.save() clipCtx.translate(0, maskOffset) clipCtx.drawImage(maskSilhouetteCache.canvas, 0, 0) clipCtx.restore() fillPersistentLayer.push() fillPersistentLayer.drawingContext.save() fillPersistentLayer.drawingContext.globalCompositeOperation = 'destination-in' fillPersistentLayer.drawingContext.drawImage(clipBuffer.canvas, 0, 0) fillPersistentLayer.drawingContext.restore() fillPersistentLayer.pop() } orbitBuffer.clear() orbitBuffer.background(0, 0, 0, 0) if (PARAMS.aura === "Radiant") { drawStarburst(orbitBuffer) } if (PARAMS.aura === "Skyfall") { drawSkyfall(orbitBuffer) } if (PARAMS.aura === "Vent") { drawVent(orbitBuffer) } if (PARAMS.aura === "Kintsugi") { drawCrack(orbitBuffer) } for (let orb of orbitingElements) { orb.display(orbitBuffer, true) } if (!freezeMode) { _cachedEyeOpacity = getEyeOpacity() if (TRAITS.iris === "Omnia") _cachedOmniaCol = getOmniaColor() } finalComposite.clear() finalComposite.background(getBackgroundColor()) if (PARAMS.aura === "CorruptRealm") { corruptBuffer.clear() corruptBuffer.background(0, 0, 0, 0) corruptBuffer.image(staticLayerCache, 0, 0) for (let slice of corruptSlices) { if (slice.active) { corruptBuffer.noStroke() corruptBuffer.fill(0, 0, 0, 0) corruptBuffer.blendMode(REPLACE) corruptBuffer.rect(0, slice.y, width, slice.sliceHeight) corruptBuffer.blendMode(BLEND) let sliceImg = staticLayerCache.get(0, slice.y, width, slice.sliceHeight) corruptBuffer.image(sliceImg, slice.offset, slice.y) } } finalComposite.image(corruptBuffer, 0, 0) } else { finalComposite.image(staticLayerCache, 0, 0) } finalComposite.image(orbitBuffer, 0, 0) finalComposite.push() finalComposite.translate(0, part2Offset) finalComposite.image(formPart2Cache, 0, 0) finalComposite.pop() finalComposite.push() finalComposite.translate(0, part1Offset) finalComposite.image(formPart1Cache, 0, 0) finalComposite.pop() if (PARAMS.aura === "Melt") { meltBuffer.clear() meltBuffer.background(0, 0, 0, 0) drawMelt(meltBuffer) meltBuffer.push() meltBuffer.drawingContext.save() meltBuffer.drawingContext.globalCompositeOperation = 'destination-out' meltBuffer.push() meltBuffer.translate(0, maskOffset) meltBuffer.image(eyesBlockingCache, 0, 0) meltBuffer.pop() meltBuffer.drawingContext.restore() meltBuffer.pop() finalComposite.image(meltBuffer, 0, 0) } if (PARAMS.aura === "CorruptMask") { meltBuffer.clear() meltBuffer.background(0, 0, 0, 0) meltBuffer.image(fillPersistentLayer, 0, 0) meltBuffer.push() meltBuffer.drawingContext.save() meltBuffer.drawingContext.globalCompositeOperation = 'destination-in' meltBuffer.push() meltBuffer.translate(0, maskOffset) meltBuffer.image(maskSilhouetteCache, 0, 0) meltBuffer.pop() meltBuffer.drawingContext.restore() meltBuffer.pop() corruptBuffer.clear() corruptBuffer.background(0, 0, 0, 0) corruptBuffer.image(meltBuffer, 0, 0) corruptBuffer.push() corruptBuffer.translate(0, maskOffset) corruptBuffer.image(eyesBlockingCache, 0, 0) corruptBuffer.pop() const eyeOpacity = _cachedEyeOpacity if (eyeOpacity > 0) { corruptBuffer.push() corruptBuffer.translate(0, maskOffset) if (TRAITS.iris === "Omnia") { const omniaCol = _cachedOmniaCol || getOmniaColor() corruptBuffer.tint(red(omniaCol), green(omniaCol), blue(omniaCol), eyeOpacity * 255) corruptBuffer.image(omniaEyesBase, 0, 0) } else { corruptBuffer.tint(255, eyeOpacity * 255) corruptBuffer.image(eyesCache, 0, 0) } corruptBuffer.pop() corruptBuffer.noTint() } for (let slice of corruptSlices) { if (slice.active) { corruptBuffer.noStroke() corruptBuffer.fill(0, 0, 0, 0) corruptBuffer.blendMode(REPLACE) corruptBuffer.rect(slice.sliceX, slice.y, slice.sliceWidth, slice.sliceHeight) corruptBuffer.blendMode(BLEND) let sliceImg = fillPersistentLayer.get(slice.sliceX, slice.y, slice.sliceWidth, slice.sliceHeight) corruptBuffer.image(sliceImg, slice.sliceX + slice.offset, slice.y) } } finalComposite.image(corruptBuffer, 0, 0) } else { finalCompo
let cp2y = y - 134.8 * scaleY x += 130.5 * scaleX y -= 193.7 * scaleY g.bezierVertex(cp1x, cp1y, cp2x, cp2y, x, y) cp1x = x + 64.9 * scaleX cp1y = y - 76.8 * scaleY cp2x = x + 78.7 * scaleX cp2y = y - 250.3 * scaleY x += 100.8 * scaleX y -= 385.7 * scaleY g.bezierVertex(cp1x, cp1y, cp2x, cp2y, x, y) cp1x = x + 17.8 * scaleX cp1y = y - 109.2 * scaleY cp2x = x + 134.3 * scaleX cp2y = y - 225.2 * scaleY x += 272.5 * scaleX y -= 225.3 * scaleY g.bezierVertex(cp1x, cp1y, cp2x, cp2y, x, y) cp1x = 2 * x - cp2x cp1y = 2 * y - cp2y cp2x = x + 244.5 * scaleX cp2y = y + 116 * scaleY x += 263.8 * scaleX y += 265.3 * scaleY g.bezierVertex(cp1x, cp1y, cp2x, cp2y, x, y) cp1x = x + 42.3 * scaleX cp1y = y + 274.2 * scaleY cp2x = x + 91.2 * scaleX cp2y = y + 343 * scaleY x += 91.2 * scaleX y += 343 * scaleY g.bezierVertex(cp1x, cp1y, cp2x, cp2y, x, y) cp1x = x + 56.5 * scaleX cp1y = y + 79.5 * scaleY cp2x = x + 143.4 * scaleX cp2y = y + 199.4 * scaleY x += 143.4 * scaleX y += 199.4 * scaleY g.bezierVertex(cp1x, cp1y, cp2x, cp2y, x, y) y += 115.4 * scaleY g.vertex(x, y) cp1x = x + 0 * scaleX cp1y = y + 0 * scaleY cp2x = x - 1000.9 * scaleX cp2y = y + 2.7 * scaleY x -= 1000.9 * scaleX y += 2.7 * scaleY g.bezierVertex(cp1x, cp1y, cp2x, cp2y, x, y) x -= 1.2 * scaleX y -= 120.9 * scaleY g.vertex(x, y) g.endShape(CLOSE) } function drawDuxArchetype(g, scaleX, scaleY) { g.beginShape() g.vertex(531.5 * scaleX, 60.4 * scaleY) g.vertex(500.1 * scaleX, 10.9 * scaleY) g.vertex(472.5 * scaleX, 61.4 * scaleY) g.vertex(502.9 * scaleX, 88 * scaleY) g.endShape(CLOSE) g.beginShape() g.vertex(854.6 * scaleX, 792.0 * scaleY) g.vertex(748.3 * scaleX, 660.7 * scaleY) g.vertex(765.0 * scaleX, 506.9 * scaleY) g.vertex(841.7 * scaleX, 493.9 * scaleY) g.vertex(841.0 * scaleX, 492.7 * scaleY) g.vertex(849.8 * scaleX, 364.1 * scaleY) g.vertex(800.7 * scaleX, 423.5 * scaleY) g.vertex(768.8 * scaleX, 368.6 * scaleY) g.vertex(886.0 * scaleX, 238.3 * scaleY) g.vertex(859.1 * scaleX, 103.2 * scaleY) g.vertex(805.6 * scaleX, 202.7 * scaleY) g.vertex(560.1 * scaleX, 91.5 * scaleY) g.vertex(504.5 * scaleX, 111.0 * scaleY) g.vertex(440.9 * scaleX, 91.5 * scaleY) g.vertex(214.2 * scaleX, 190.9 * scaleY) g.vertex(145.1 * scaleX, 101.5 * scaleY) g.vertex(130.9 * scaleX, 232.4 * scaleY) g.vertex(239.3 * scaleX, 363.3 * scaleY) g.vertex(214.6 * scaleX, 411.3 * scaleY) g.vertex(161.5 * scaleX, 353.0 * scaleY) g.vertex(165.4 * scaleX, 506.8 * scaleY) g.vertex(164.1 * scaleX, 509.4 * scaleY) g.vertex(239.7 * scaleX, 516.1 * scaleY) g.vertex(261.4 * scaleX, 661.0 * scaleY) g.vertex(157.7 * scaleX, 794.8 * scaleY) g.vertex(-0.8 * scaleX, 825.7 * scaleY) g.vertex(0.0 * scaleX, 999.8 * scaleY) g.vertex(1000.3 * scaleX, 1000.0 * scaleY) g.vertex(1000.3 * scaleX, 820.2 * scaleY) g.endShape(CLOSE) } function drawBulwarkArchetype(g, scaleX, scaleY) { g.beginShape() g.vertex(958.3 * scaleX, 833.3 * scaleY) g.vertex(916.6 * scaleX, 795.8 * scaleY) g.vertex(962.3 * scaleX, 696.3 * scaleY) g.vertex(857.8 * scaleX, 742.9 * scaleY) g.vertex(838.9 * scaleX, 725.8 * scaleY) g.vertex(870.4 * scaleX, 657.3 * scaleY) g.vertex(798.4 * scaleX, 689.4 * scaleY) g.vertex(762.8 * scaleX, 657.3 * scaleY) g.vertex(801.8 * scaleX, 557.6 * scaleY) g.vertex(762.8 * scaleX, 512.4 * scaleY) g.vertex(826.2 * scaleX, 384.2 * scaleY) g.vertex(762.8 * scaleX, 326.4 * scaleY) g.vertex(852.5 * scaleX, 175.7 * scaleY) g.vertex(752.8 * scaleX, 102.5 * scaleY) g.vertex(253.8 * scaleX, 102.5 * scaleY) g.vertex(154.7 * scaleX, 175.7 * scaleY) g.vertex(243.8 * scaleX, 326.4 * scaleY) g.vertex(182.6 * scaleX, 384.2 * scaleY) g.vertex(243.8 * scaleX, 518.2 * scaleY) g.vertex(208.7 * scaleX, 557.6 * scaleY) g.vertex(243.8 * scaleX, 657.7 * scaleY) g.vertex(193.9 * scaleX, 704.4 * scaleY) g.vertex(132.8 * scaleX, 676.3 * scaleY) g.vertex(159.6 * scaleX, 736.5 * scaleY) g.vertex(144.5 * scaleX, 750.5 * scaleY) g.vertex(43.6 * scaleX, 704.2 * scaleY) g.vertex(87.9 * scaleX, 803.5 * scaleY) g.vertex(39 * scaleX, 849.3 * scaleY) g.vertex(0.1 * scaleX, 858.4 * scaleY) g.vertex(-1.2 * scaleX, 1001.1 * scaleY) g.vertex(1000.3 * scaleX, 1001.4 * scaleY) g.vertex(1000.4 * scaleX, 846.4 * scaleY) g.endShape(CLOSE) } function drawWardenArchetype(g, scaleX, scaleY) { g.beginShape() g.vertex(960 * scaleX, 662.3 * scaleY) g.vertex(960.8 * scaleX, 661.7 * scaleY) g.vertex(958.3 * scaleX, 661.7 * scaleY) g.vertex(909.1 * scaleX, 510 * scaleY) g.vertex(880.7 * scaleX, 654.7 * scaleY) g.vertex(859 * scaleX, 587.7 * scaleY) g.vertex(835.7 * scaleX, 659.4 * scaleY) g.vertex(790.1 * scaleX, 622 * scaleY) g.vertex(762.8 * scaleX, 528.5 * scaleY) g.vertex(776.2 * scaleX, 384.2 * scaleY) g.vertex(762.8 * scaleX, 326.4 * scaleY) g.vertex(742.5 * scaleX, 175.7 * scaleY) g.vertex(642.8 * scaleX, 102.5 * scaleY) g.vertex(363.8 * scaleX, 102.5 * scaleY) g.vertex(264.7 * scaleX, 175.7 * scaleY) g.vertex(243.8 * scaleX, 326.4 * scaleY) g.vertex(232.6 * scaleX, 384.2 * scaleY) g.vertex(243.8 * scaleX, 528.5 * scaleY) g.vertex(219.8 * scaleX, 622 * scaleY) g.vertex(178.4 * scaleX, 660.9 * scaleY) g.vertex(177.8 * scaleX, 660.9 * scaleY) g.vertex(155.2 * scaleX, 591.5 * scaleY) g.vertex(132.4 * scaleX, 661.8 * scaleY) g.vertex(131.6 * scaleX, 661.9 * scaleY) g.vertex(87.8 * scaleX, 510 * scaleY) g.vertex(51.7 * scaleX, 661.9 * scaleY) g.vertex(0.1 * scaleX, 673.5 * scaleY) g.vertex(-1.2 * scaleX, 1001.1 * scaleY) g.vertex(1000.3 * scaleX, 1001.4 * scaleY) g.vertex(1000.7 * scaleX, 673.5 * scaleY) g.endShape(CLOSE) } function drawShadeArchetype(g, scaleX, scaleY) { g.beginShape() g.vertex(909 * scaleX, 623.5 * scaleY) g.vertex(844 * scaleX, 651.3 * scaleY) g.vertex(707.2 * scaleX, 181.5 * scaleY) g.vertex(546.4 * scaleX, 106.5 * scaleY) g.vertex(509.6 * scaleX, 69.7 * scaleY) g.vertex(474.1 * scaleX, 105.2 * scaleY) g.vertex(309.5 * scaleX, 179.3 * scaleY) g.vertex(184.8 * scaleX, 651.3 * scaleY) g.vertex(124 * scaleX, 623.5 * scaleY) g.vertex(0.3 * scaleX, 988.8 * scaleY) g.vertex(0.5 * scaleX, 1003 * scaleY) g.vertex(1000 * scaleX, 1002.6 * scaleY) g.vertex(1001.2 * scaleX, 987.1 * scaleY) g.endShape(CLOSE) } function drawBruteArchetype(g, scaleX, scaleY) { g.beginShape() // Starting point g.vertex(940.1 * scaleX, 779.9 * scaleY) // l21.8-23.7 -> 961.9, 756.2 g.vertex(961.9 * scaleX, 756.2 * scaleY) // l-49.4,17.8 -> 912.5, 774 g.vertex(912.5 * scaleX, 774 * scaleY) // l-27.7-5.9 -> 884.8, 768.1 g.vertex(884.8 * scaleX, 768.1 * scaleY) // l29.2-44.7 -> 914, 723.4 g.vertex(914 * scaleX, 723.4 * scaleY) // l-92,31.3 -> 822, 754.7 g.vertex(822 * scaleX, 754.7 * scaleY) // l-12-2.6 -> 810, 752.1 g.vertex(810 * scaleX, 752.1 * scaleY) // l76.6-74 -> 886.6, 678.1 g.vertex(886.6 * scaleX, 678.1 * scaleY) // l-138.5,68.6 -> 748.1, 746.7 g.vertex(748.1 * scaleX, 746.7 * scaleY) // h.1 (tiny horizontal) g.vertex(748.2 * scaleX, 746.7 * scaleY) // c0,0-77.4,37.6-77.4,37.6 -> 670.8, 784.3 g.vertex(670.8 * scaleX, 784.3 * scaleY) // l41.5-73.4 -> 712.3, 710.9 g.vertex(712.3 * scaleX, 710.9 * scaleY) // l45.1-426.7 -> 757.4, 284.2 g.vertex(757.4 * scaleX, 284.2 * scaleY) // c77,23.1,137.3,227.8,137.3,149 (curve to 894.7, 433.2 but end at 894.7, 284.2) g.bezierVertex(834.4 * scaleX, 307.3 * scaleY, 894.7 * scaleX, 512 * scaleY, 894.7 * scaleX, 433.2 * scaleY) // c0-58.3-24.2-142.6-61.6-200.8 -> 833.1, 232.4 g.bezierVertex(894.7 * scaleX, 374.9 * scaleY, 870.5 * scaleX, 290.6 * scaleY, 833.1 * scaleX, 232.4 * scaleY) // c.6,0,1.1,0,1.7,0 -> 834.8, 232.4 g.vertex(834.8 * scaleX, 232.4 * scaleY) // c59.1,0,107.1-84.6,107.1-189.5 -> 941.9, 42.9 g.bezierVertex(893.9 * scaleX, 232.4 * scaleY, 941.9 * scaleX, 147.8 * scaleY, 941.9 * scaleX, 42.9 * scaleY) // s-37.6,72-87.3,101.1 (smooth curve) -> 854.6, 144 g.bezierVertex(941.9 * scaleX, 114.9 * scaleY, 904.3 * scaleX, 144 * scaleY, 854.6 * scaleX, 144 * scaleY) // l-219.5-27.7 -> 635.1, 116.3 g.vertex(635.1 * scaleX, 116.3 * scaleY) // l-274.6-.2 -> 360.5, 116.1 g.vertex(360.5 * scaleX, 116.1 * scaleY) // l-195.3,31.1 -> 165.2, 147.2 g.vertex(165.2 * scaleX, 147.2 * scaleY) // C112.4,126,71.5-11.5,71.5,43.1 (absolute curve) g.bezierVertex(112.4 * scaleX, 126 * scaleY, 71.5 * scaleX, -11.5 * scaleY, 71.5 * scaleX, 43.1 * scaleY) // s35.4,156.6,82.9,182.8 -> 154.4, 225.9 g.bezierVertex(71.5 * scaleX, 97.7 * scaleY, 106.9 * scaleX, 199.7 * scaleY, 154.4 * scaleX, 225.9 * scaleY) // c-38.2,48.5-63.1,127.7-63.1,217.5 -> 91.3, 443.4 g.bezierVertex(116.2 * scaleX, 274.4 * scaleY, 91.3 * scaleX, 353.6 * scaleY, 91.3 * scaleX, 443.4 * scaleY) // c0,83.5,67.7-151.1,151.1-151.1 -> 242.4, 292.3 g.bezierVertex(91.3 * scaleX, 526.9 * scaleY, 159 * scaleX, 292.3 * scaleY, 242.4 * scaleX, 292.3 * scaleY) // c3.3,0,6.6-.2,9.9-.6 -> 252.3, 291.7 g.vertex(252.3 * scaleX, 291.7 * scaleY) // l.4.6 -> 252.7, 292.3 g.vertex(252.7 * scaleX, 292.3 * scaleY) // l49.9,418.8 -> 302.6, 711.1 g.vertex(302.6 * scaleX, 711.1 * scaleY) // l40.7,75 -> 343.3, 786.1 g.vertex(343.3 * scaleX, 786.1 * scaleY) // l-123.5-52.2 -> 219.8, 733.9 g.vertex(219.8 * scaleX, 733.9 * scaleY) // l-126.3-68.6 -> 93.5, 665.3 g.vertex(93.5 * scaleX, 665.3 * scaleY) // l72.4,76.7 -> 165.9, 742 g.vertex(165.9 * scaleX, 742 * scaleY) // l-9.3,1.4 -> 156.6, 743.4 g.vertex(156.6 * scaleX, 743.4 * scaleY) // l-88.1-32.9 -> 68.5, 710.5 g.vertex(68.5 * scaleX, 710.5 * scaleY) // l25.2,42.3 -> 93.7, 752.8 g.vertex(93.7 * scaleX, 752.8 * scaleY) // l-32.5,4.9 -> 61.2, 757.7 g.vertex(61.2 * scaleX, 757.7 * scaleY) // l-36.4-14.4 -> 24.8, 743.3 g.vertex(24.8 * scaleX, 743.3 * scaleY) // l14.8,17.6 -> 39.6, 760.9 g.vertex(39.6 * scaleX, 760.9 * scaleY) // l-42.9,6.4 -> -3.3, 767.3 (clamp to 0) g.vertex(0 * scaleX, 767.3 * scaleY) // l1.6,231.7 -> 1.6, 999 g.vertex(1.6 * scaleX, 999 * scaleY) // h1001.1 -> 1002.7, 999 g.vertex(1002.7 * scaleX, 999 * scaleY) // v-206.5 -> 1002.7, 792.5 g.vertex(1002.7 * scaleX, 792.5 * scaleY) // l-59.4-12.7 -> 943.3, 779.8 (close to start) g.vertex(943.3 * scaleX, 779.8 * scaleY) g.endShape(CLOSE) } function drawEmperorArchetype(g, scaleX, scaleY) { // New Emperor form from updated SVG g.beginShape() // Start at 936.5, 608.1 g.vertex(936.5 * scaleX, 608.1 * scaleY) // Line to (936.5-142.7, 608.1-77.7) = (793.8, 530.4) g.vertex(793.8 * scaleX, 530.4 * scaleY) // The path continues with curves, but we'll approximate with vertices // c.5-2.5.8-5 .8-7.7 (relative curve, small adjustment) g.vertex(794.6 * scaleX, 522.7 * scaleY) // c0-10.8-4.9-20.5-12.5-27 g.vertex(782.1 * scaleX, 495.7 * scaleY) // l73.7-316.8 (line to relative) g.vertex(855.8 * scaleX, 178.9 * scaleY) // s-133.6-106.3-171.4-106 (smooth curve approximated) g.vertex(684.4 * scaleX, 72.9 * scaleY) // c-4.7-.7-5.6,7.6-3.8,9.5 (small curve) g.vertex(680.6 * scaleX, 82.4 * scaleY) // l61.5,65.5 g.vertex(742.1 * scaleX, 147.9 * scaleY) // l-17.4,7.6 g.vertex(724.7 * scaleX, 155.5 * scaleY) // c-18.2-4.2-42.9-7.7-71.7-10.6 g.vertex(653.0 * scaleX, 144.9 * scaleY) // l13.7-34.7 g.vertex(666.7 * scaleX, 110.2 * scaleY) // c-.6-3.2-3.3-5-6.3-4 (small adjustment) g.vertex(660.4 * scaleX, 106.2 * scaleY) // c-1.4.4-14.9,17.1-17.2,19.9 g.vertex(643.2 * scaleX, 126.1 * scaleY) // c-4.6,5.5-9.1,11.1-13.6,16.7 g.vertex(629.6 * scaleX, 142.8 * scaleY) // c-16-1.3-33-2.3-50.5-3.2 g.vertex(579.1 * scaleX, 139.6 * scaleY) // l32.7-82.7 g.vertex(611.8 * scaleX, 56.9 * scaleY) // c-.6-3.2-3.3-5-6.3-4 g.vertex(605.5 * scaleX, 52.9 * scaleY) // c-1.4.4-14.9,17.1-17.2,19.9 g.vertex(588.3 * scaleX, 72.8 * scaleY) // c-17.7,21.5-35.1,43.3-52.4,65.2 g.vertex(535.9 * scaleX, 138.0 * scaleY) // c-7.7-.2-15.4-.4-23.1-.5 g.vertex(512.8 * scaleX, 137.5 * scaleY) // l-13.5-38.5 g.vertex(499.3 * scaleX, 99.0 * scaleY) // c-1.4-4.1-7.3-4.1-8.6,0 g.vertex(490.7 * scaleX, 99.0 * scaleY) // l-12.6,38.2 g.vertex(478.1 * scaleX, 137.2 * scaleY) // c-7.6,0-15.1,0-22.6.2 g.vertex(455.5 * scaleX, 137.4 * scaleY) // c-20.8-22.1-41.5-44.3-62.6-66 g.vertex(392.9 * scaleX, 71.4 * scaleY) // c-2.6-2.7-17.7-18.4-19.2-18.8 g.vertex(373.7 * scaleX, 52.6 * scaleY) // c-3.3-.8-5.9,1.2-6.4,4.4 g.vertex(367.3 * scaleX, 57.0 * scaleY) // l39.7,81.8 g.vertex(407.0 * scaleX, 138.8 * scaleY) // c-19.1.8-37.5,2-54.6,3.5 g.vertex(352.4 * scaleX, 142.3 * scaleY) // c-4.6-4.8-9.1-9.5-13.7-14.2 g.vertex(338.7 * scaleX, 128.1 * scaleY) // c-2.6-2.7-17.7-18.4-19.2-18.8 g.vertex(319.5 * scaleX, 109.3 * scaleY) // c-3.3-.8-5.9,1.2-6.4,4.4 g.vertex(313.1 * scaleX, 113.7 * scaleY) // l15.2,31.2 g.vertex(328.3 * scaleX, 144.9 * scaleY) // c-22.5,2.7-41.9,6.2-56.7,10.5 g.vertex(271.6 * scaleX, 155.4 * scaleY) // l-16.9-7.5 g.vertex(254.7 * scaleX, 147.9 * scaleY) // l61.6-65.4 g.vertex(316.3 * scaleX, 82.5 * scaleY) // c1.8-1.9,1-10.2-3.7-9.5 g.vertex(312.6 * scaleX, 73.0 * scaleY) // c-78.6,24.3-171.4,105.7-171.4,105.7 g.vertex(141.2 * scaleX, 178.7 * scaleY) // l72.3,302.4 g.vertex(213.5 * scaleX, 481.1 * scaleY) // c-8.3,6.5-13.6,16.5-13.6,27.9 g.vertex(199.9 * scaleX, 509.0 * scaleY) // s.9,8.9,2.4,12.9 g.vertex(202.3 * scaleX, 521.9 * scaleY) // l-143.1,82.9 g.vertex(59.2 * scaleX, 604.8 * scaleY) // L-.7,834.6 (absolute line) g.vertex(-0.7 * scaleX, 834.6 * scaleY) // l1.6,166.5 g.vertex(0.9 * scaleX, 1001.1 * scaleY) // h1001.1 (horizontal line) g.vertex(1002.0 * scaleX, 1001.1 * scaleY) // v-172.3 (vertical line up) g.vertex(1002.0 * scaleX, 828.8 * scaleY) // l-65.5-220.7 g.vertex(936.5 * scaleX, 608.1 * scaleY) g.endShape(CLOSE) } function drawArmatusArchetype(g, scaleX, scaleY) { // Armatus form - angular armor-like silhouette with crown spikes g.beginShape() // Start at right side: 817.9, 763.3 g.vertex(817.9 * scaleX, 763.3 * scaleY) // l-130.2-633.3 (diagonal line up-left to shoulder) g.vertex(687.7 * scaleX, 130.0 * scaleY) // v-51.2 (vertical line up) g.vertex(687.7 * scaleX, 78.8 * scaleY) // l-22.2-10 (crown spike 1) g.vertex(665.5 * scaleX, 68.8 * scaleY) // v53.8 g.vertex(665.5 * scaleX, 122.6 * scaleY) // c-5.2-1.4-10.7-2.7-16.6-3.9 g.vertex(648.9 * scaleX, 118.7 * scaleY) // v-31.3 g.vertex(648.9 * scaleX, 87.4 * scaleY) // l-22.2-10 (crown spike 2) g.vertex(626.7 * scaleX, 77.4 * scaleY) // v37.3 g.vertex(626.7 * scaleX, 114.7 * scaleY) // c-5.4-.8-10.9-1.6-16.6-2.4 g.vertex(610.1 * scaleX, 112.3 * scaleY) // v-15 g.vertex(610.1 * scaleX, 97.3 * scaleY) // l-22.2-10 (crown spike 3) g.vertex(587.9 * scaleX, 87.3 * scaleY) // v22.5 g.vertex(587.9 * scaleX, 109.8 * scaleY) // c-5.4-.5-10.8-1-16.3-1.5 g.vertex(571.6 * scaleX, 108.3 * scaleY) // v-11 g.vertex(571.6 * scaleX, 97.3 * scaleY) // l-22.9-10 (crown spike 4) g.vertex(548.7 * scaleX, 87.3 * scaleY) // v19.5 g.vertex(548.7 * scaleX, 106.8 * scaleY) // c-5.4-.3-10.8-.6-16.2-.8 g.vertex(532.5 * scaleX, 106.0 * scaleY) // v-8.7 g.vertex(532.5 * scaleX, 97.3 * scaleY) // l-22.2-10 (crown spike 5) g.vertex(510.3 * scaleX, 87.3 * scaleY) // v18.1 g.vertex(510.3 * scaleX, 105.4 * scaleY) // c-5.5,0-11.1-.2-16.6-.2 g.vertex(493.7 * scaleX, 105.2 * scaleY) // v-17.9 g.vertex(493.7 * scaleX, 87.3 * scaleY) // l-22.1,10 (crown spike 6 - going back up) g.vertex(471.6 * scaleX, 97.3 * scaleY) // v8.1 g.vertex(471.6 * scaleX, 105.4 * scaleY) // c-5.6.1-11.2.3-16.7.5 g.vertex(454.9 * scaleX, 105.9 * scaleY) // v-18.6 g.vertex(454.9 * scaleX, 87.3 * scaleY) // l-21.8,10 (crown spike 7) g.vertex(433.1 * scaleX, 97.3 * scaleY) // v9.8 g.vertex(433.1 * scaleX, 107.1 * scaleY) // c-5.8.4-11.5.8-17,1.3 g.vertex(416.1 * scaleX, 108.4 * scaleY) // v-21.1 g.vertex(416.1 * scaleX, 87.3 * scaleY) // l-22.2,10 (crown spike 8) g.vertex(393.9 * scaleX, 97.3 * scaleY) // v13.5 g.vertex(393.9 * scaleX, 110.8 * scaleY) // c-5.7.7-11.3,1.6-16.6,2.5 g.vertex(377.3 * scaleX, 113.3 * scaleY) // v-36 g.vertex(377.3 * scaleX, 77.3 * scaleY) // l-22.2,10 (crown spike 9) g.vertex(355.1 * scaleX, 87.3 * scaleY) // v30.4 g.vertex(355.1 * scaleX, 117.7 * scaleY) // c-5.9,1.4-11.5,2.9-16.6,4.6 g.vertex(338.5 * scaleX, 122.3 * scaleY) // v-53.6 g.vertex(338.5 * scaleX, 68.7 * scaleY) // l-22.2,10 (crown spike 10) g.vertex(316.3 * scaleX, 78.7 * scaleY) // v62.9 g.vertex(316.3 * scaleX, 141.6 * scaleY) // l-157.9,618.3 (diagonal line down-left to bottom left) g.vertex(158.4 * scaleX, 759.9 * scaleY) // L-1.3,969.7 (line to bottom left corner area) g.vertex(-1.3 * scaleX, 969.7 * scaleY) // l1.6,29.3 g.vertex(0.3 * scaleX, 999.0 * scaleY) // h1001.1 (horizontal line across bottom) g.vertex(1001.4 * scaleX, 999.0 * scaleY) // v-30.1 g.vertex(1001.4 * scaleX, 968.9 * scaleY) // l-183.5-205.6 (diagonal line back to start) g.vertex(817.9 * scaleX, 763.3 * scaleY) g.endShape(CLOSE) } function drawTitanArchetype(g, scaleX, scaleY, maskCount) { g.beginShape() if (maskCount === "Conflux") { let x = 1000.2 * scaleX let y = 776.4 * scaleY g.vertex(x, y) let cp1x = x - 22.5 * scaleX let cp1y = y - 31.3 * scaleY let cp2x = x - 45.3 * scaleX let cp2y = y - 63.4 * scaleY x -= 65.5 * scaleX y -= 91.7 * scaleY g.bezierVertex(cp1x, cp1y, cp2x, cp2y, x, y) cp1x = x - 48.9 * scaleX cp1y = y - 68.7 * scaleY cp2x = x - 71.9 * scaleX cp2y = y - 193.7 * scaleY x -= 91.2 * scaleX y -= 343 * scaleY g.bezierVertex(cp1x, cp1y, cp2x, cp2y, x, y) cp1x = x - 19.3 * scaleX cp1y = y - 149.2 * scaleY cp2x = x - 176.1 * scaleX cp2y = y - 274.1 * scaleY x -= 360.8 * scaleX y -= 274.1 * scaleY g.bezierVertex(cp1x, cp1y, cp2x, cp2y, x, y) cp1x = 2 * x - cp2x cp1y = 2 * y - cp2y cp2x = x - 314.1 * scaleX cp2y = y + 124.9 * scaleY x -= 332 * scaleX y += 234.2 * scaleY g.bezierVertex(cp1x, cp1y, cp2x, cp2y, x, y) cp1x = x - 22.1 * scaleX cp1y = y + 135.4 * scaleY cp2x = x - 35.9 * scaleX cp2y = y + 308.9 * scaleY x -= 100.8 * scaleX y += 385.7 * scaleY g.bezierVertex(cp1x, cp1y, cp2x, cp2y, x, y) cp1x = 32 * scaleX cp1y = 708.7 * scaleY cp2x = 16.7 * scaleX cp2y = 728.9 * scaleY x = 0 * scaleX y = 751.7 * scaleY g.bezierVertex(cp1x, cp1y, cp2x, cp2y, x, y) x -= 0.2 * scaleX y += 248.5 * scaleY g.vertex(x, y) x += 1000.2 * scaleX y -= 0.3 * scaleY g.vertex(x, y) x += 0.3 * scaleX y -= 223.5 * scaleY g.vertex(x, y) } else { let x = -2.3 * scaleX let y = 881.6 * scaleY g.vertex(x, y) let cp1x = x + 34.5 * scaleX let cp1y = y - 70 * scaleY let cp2x = x + 80.7 * scaleX let cp2y = y - 134.8 * scaleY x += 130.5 * scaleX y -= 193.7 * scaleY g.bezierVertex(cp1x, cp1y, cp2x, cp2y, x, y) cp1x = x + 64.9 * scaleX cp1y = y - 76.8 * scaleY cp2x = x + 78.7 * scaleX cp2y = y - 250.3 * scaleY x += 100.8 * scaleX y -= 385.7 * scaleY g.bezierVertex(cp1x, cp1y, cp2x, cp2y, x, y) cp1x = x + 17.8 * scaleX cp1y = y - 109.2 * scaleY cp2x = x + 134.3 * scaleX cp2y = y - 225.2 * scaleY x += 272.5 * scaleX y -= 225.3 * scaleY g.bezierVertex(cp1x, cp1y, cp2x, cp2y, x, y) cp1x = 2 * x - cp2x cp1y = 2 * y - cp2y cp2x = x + 244.5 * scaleX cp2y = y + 116 * scaleY x += 263.8 * scaleX y += 265.3 * scaleY g.bezierVertex(cp1x, cp1y, cp2x, cp2y, x, y) cp1x = x + 42.3 * scaleX cp1y = y + 274.2 * scaleY cp2x = x + 91.2 * scaleX cp2y = y + 343 * scaleY x += 91.2 * scaleX y += 343 * scaleY g.bezierVertex(cp1x, cp1y, cp2x, cp2y, x, y) cp1x = x + 56.5 * scaleX cp1y = y + 79.5 * scaleY cp2x = x + 143.4 * scaleX cp2y = y + 199.4 * scaleY x += 143.4 * scaleX y += 199.4 * scaleY g.bezierVertex(cp1x, cp1y, cp2x, cp2y, x, y) y += 115.4 * scaleY g.vertex(x, y) cp1x = x + 0 * scaleX cp1y = y + 0 * scaleY cp2x = x - 1000.9 * scaleX cp2y = y + 2.7 * scaleY x -= 1000.9 * scaleX y += 2.7 * scaleY g.bezierVertex(cp1x, cp1y, cp2x, cp2y, x, y) x -= 1.2 * scaleX y -= 120.9 * scaleY g.vertex(x, y) } g.endShape(CLOSE) } /* ========================= MASK LAYOUT ========================= */ function layoutMasks() { const count = TRAITS.count const HEAD_ZONE_H = width // head zone is a square region at top, width x width const cx = width / 2, cy = HEAD_ZONE_H / 2 // v2: center in head zone, not full canvas const baseW = width * 0.62 const baseH = baseW * 1.18 if (count === "Mono") { masks.push(makeMask(cx, cy, baseW, baseH, 0, 0)) } else if (count === "Conflux") { const maskWidth = baseW * 0.86 const gap = maskWidth * 0.36 masks.push(makeMask(cx - gap / 2, cy, maskWidth, baseH * 0.86, -0.03, 0)) masks.push(makeMask(cx + gap / 2, cy, maskWidth, baseH * 0.86, 0.03, 1)) } } function makeMask(x, y, w, h, rot, maskIndex) { return { x, y, w, h, rot, maskIndex, edgeStyle: TRAITS.edgeStyle, sight: TRAITS.sight, voice: TRAITS.voice, scent: TRAITS.scent, ornament: TRAITS.ornament, hollows: TRAITS.hollows, } } function drawMaskEyesBlocking(pg, m) { pg.push() pg.translate(m.x, m.y) pg.rotate(PARAMS.axisLock ? 0 : m.rot) const rx = m.w * 0.22 const ry = m.h * 0.13 const ly = -m.h * 0.12 const left = { x: -m.w * 0.18, y: ly } const right = { x: m.w * 0.18, y: ly } pg.noStroke() pg.fill(0, 0, 10) if (TRAITS.iris === "Null" || TRAITS.iris === "Fury" || TRAITS.iris === "Plasma" || TRAITS.iris === "Omnia") { const strokeW = m.w * 0.03 if (m.sight === "Mono") { const center = { x: 0, y: ly } drawEye(pg, center, rx, ry, m.sight, strokeW) } else if (m.sight === "Verge") { drawVerticalAlmondTotal(pg, rx, ry, ly, m.w, strokeW) } else { drawEye(pg, left, rx, ry, m.sight, strokeW) drawEye(pg, right, rx, ry, m.sight, strokeW) } } else if (TRAITS.iris === "Abyss") { if (m.sight === "Mono") { const center = { x: 0, y: ly } drawEye(pg, center, rx, ry, m.sight, 0) } else if (m.sight === "Verge") { drawVerticalAlmondTotal(pg, rx, ry, ly, m.w, 0) } else { drawEye(pg, left, rx, ry, m.sight, 0) drawEye(pg, right, rx, ry, m.sight, 0) } } pg.pop() } function drawMaskEyes(pg, m) { pg.push() pg.translate(m.x, m.y) pg.rotate(PARAMS.axisLock ? 0 : m.rot) const rx = m.w * 0.22 const ry = m.h * 0.13 const ly = -m.h * 0.12 const left = { x: -m.w * 0.18, y: ly } const right = { x: m.w * 0.18, y: ly } pg.noStroke() if (TRAITS.iris === "Null") { pg.fill(0, 0, 100) if (m.sight === "Mono") { const center = { x: 0, y: ly } drawEye(pg, center, rx, ry, m.sight, 0) } else if (m.sight === "Verge") { drawVerticalAlmondTotal(pg, rx, ry, ly, m.w, 0) } else { drawEye(pg, left, rx, ry, m.sight, 0) drawEye(pg, right, rx, ry, m.sight, 0) } } else if (TRAITS.iris === "Fury") { pg.fill(0, 100, 100) if (m.sight === "Mono") { const center = { x: 0, y: ly } drawEye(pg, center, rx, ry, m.sight, 0) } else if (m.sight === "Verge") { drawVerticalAlmondTotal(pg, rx, ry, ly, m.w, 0) } else { drawEye(pg, left, rx, ry, m.sight, 0) drawEye(pg, right, rx, ry, m.sight, 0) } } else if (TRAITS.iris === "Plasma") { pg.fill(185, 100, 100) if (m.sight === "Mono") { const center = { x: 0, y: ly } drawEye(pg, center, rx, ry, m.sight, 0) } else if (m.sight === "Verge") { drawVerticalAlmondTotal(pg, rx, ry, ly, m.w, 0) } else { drawEye(pg, left, rx, ry, m.sight, 0) drawEye(pg, right, rx, ry, m.sight, 0) } } else if (TRAITS.iris === "Omnia") { const cycleSpeed = 0.08 const palette = PARAMS.palette const complementaryHues = palette.map(c => { const col = color(c) const h = hue(col) return (h + 180) % 360 }) const cyclePosition = (millis() * cycleSpeed) % (complementaryHues.length * 100) const colorIndex = floor(cyclePosition / 100) % complementaryHues.length const nextIndex = (colorIndex + 1) % complementaryHues.length const blend = (cyclePosition % 100) / 100 const currentHue = lerp(complementaryHues[colorIndex], complementaryHues[nextIndex], blend) pg.fill(currentHue, 100, 100) if (m.sight === "Mono") { const center = { x: 0, y: ly } drawEye(pg, center, rx, ry, m.sight, 0) } else if (m.sight === "Verge") { drawVerticalAlmondTotal(pg, rx, ry, ly, m.w, 0) } else { drawEye(pg, left, rx, ry, m.sight, 0) drawEye(pg, right, rx, ry, m.sight, 0) } } else if (TRAITS.iris === "Abyss") { const strokeW = m.w * 0.03 pg.fill(0, 0, 100) if (m.sight === "Mono") { const center = { x: 0, y: ly } drawEye(pg, center, rx, ry, m.sight, strokeW) } else if (m.sight === "Verge") { drawVerticalAlmondTotal(pg, rx, ry, ly, m.w, strokeW) } else { drawEye(pg, left, rx, ry, m.sight, strokeW) drawEye(pg, right, rx, ry, m.sight, strokeW) } pg.erase() if (m.sight === "Mono") { const center = { x: 0, y: ly } drawEye(pg, center, rx, ry, m.sight, 0) } else if (m.sight === "Verge") { drawVerticalAlmondTotal(pg, rx, ry, ly, m.w, 0) } else { drawEye(pg, left, rx, ry, m.sight, 0) drawEye(pg, right, rx, ry, m.sight, 0) } pg.noErase() } pg.pop() } function drawMaskEyesOmniaBase(pg, m) { pg.push() pg.translate(m.x, m.y) pg.rotate(PARAMS.axisLock ? 0 : m.rot) const rx = m.w * 0.22 const ry = m.h * 0.13 const ly = -m.h * 0.12 const left = { x: -m.w * 0.18, y: ly } const right = { x: m.w * 0.18, y: ly } pg.noStroke() pg.fill(0, 0, 100) // White fill - will be tinted if (m.sight === "Mono") { const center = { x: 0, y: ly } drawEye(pg, center, rx, ry, m.sight, 0) } else if (m.sight === "Verge") { drawVerticalAlmondTotal(pg, rx, ry, ly, m.w, 0) } else { drawEye(pg, left, rx, ry, m.sight, 0) drawEye(pg, right, rx, ry, m.sight, 0) } pg.pop() } function getOmniaColor() { const cycleSpeed = 0.08 const palette = PARAMS.palette const complementaryHues = palette.map(c => { const col = color(c) const h = hue(col) return (h + 180) % 360 }) const cyclePosition = (millis() * cycleSpeed) % (complementaryHues.length * 100) const colorIndex = floor(cyclePosition / 100) % complementaryHues.length const nextIndex = (colorIndex + 1) % complementaryHues.length const blend = (cyclePosition % 100) / 100 const currentHue = lerp(complementaryHues[colorIndex], complementaryHues[nextIndex], blend) return color(currentHue, 100, 100) } function drawMaskSilhouette(pg, m) { pg.push() pg.translate(m.x, m.y) pg.rotate(PARAMS.axisLock ? 0 : m.rot) pg.noStroke() pg.fill(255) drawMaskShape(pg, m) drawOrnaments(pg, m) drawTusks(pg, m) drawEyes(pg, m) pg.push() pg.erase() carveMouth(pg, m) carveNose(pg, m) carveCutouts(pg, m) pg.noErase() pg.pop() pg.pop() } function drawMaskShape(pg, m) { const pts = silhouettePoints(m.w, m.h, m.edgeStyle) if (m.edgeStyle === "Curved") { pg.beginShape() const first = pts[0], last = pts[pts.length - 1] pg.curveVertex(last.x, last.y) for (const p of pts) pg.curveVertex(p.x, p.y) pg.curveVertex(first.x, first.y) pg.curveVertex(pts[1].x, pts[1].y) pg.endShape(CLOSE) } else { pg.beginShape() for (const p of pts) pg.vertex(p.x, p.y) pg.endShape(CLOSE) } } function stylizeEdge(p, style, snap) { if (style === "Faceted") { return { x: round(p.x / snap) * snap, y: round(p.y / snap) * snap } } if (style === "Straight") { const ax = abs(p.x) > abs(p.y) ? { x: p.x, y: round(p.y / snap) * snap } : { x: round(p.x / snap) * snap, y: p.y } return ax } return p } function silhouettePoints(w, h, edgeStyle) { const bw = w * 0.5, bh = h * 0.5 let base = [ { x: 0, y: -bh * 0.92 }, { x: bw * 0.45, y: -bh * 0.78 }, { x: bw * 0.68, y: -bh * 0.35 }, { x: bw * 0.72, y: bh * 0.05 }, { x: bw * 0.58, y: bh * 0.55 }, { x: 0, y: bh * 0.72 }, { x: -bw * 0.58, y: bh * 0.55 }, { x: -bw * 0.72, y: bh * 0.05 }, { x: -bw * 0.68, y: -bh * 0.35 }, { x: -bw * 0.45, y: -bh * 0.78 }, ] const snap = min(w, h) * (edgeStyle === "Faceted" ? 0.05 : 0.035) base = base.map((p) => stylizeEdge(p, edgeStyle, snap)) return base } function drawTusks(pg, m) { if (m.voice !== "Downfall") return const y = m.h * 0.27 const tuskTopWidth = m.w * 0.12 const tuskTipWidth = m.w * 0.03 const tuskHeight = m.h * 0.25 const tuskSpacing = m.w * 0.18 pg.push() pg.fill(255) pg.noStroke() pg.push() pg.translate(-tuskSpacing, y) pg.beginShape() pg.vertex(-tuskTopWidth * 0.5, 0) pg.bezierVertex( -tuskTopWidth * 0.5, tuskHeight * 0.3, -tuskTipWidth * 0.8, tuskHeight * 0.7, -tuskTipWidth * 0.3, tuskHeight ) pg.vertex(0, tuskHeight * 0.95) pg.bezierVertex( tuskTipWidth * 0.3, tuskHeight * 0.7, tuskTopWidth * 0.3, tuskHeight * 0.3, tuskTopWidth * 0.5, 0 ) pg.endShape(CLOSE) pg.pop() pg.push() pg.translate(tuskSpacing, y) pg.beginShape() pg.vertex(-tuskTopWidth * 0.5, 0) pg.bezierVertex( -tuskTopWidth * 0.3, tuskHeight * 0.3, -tuskTipWidth * 0.3, tuskHeight * 0.7, 0, tuskHeight * 0.95 ) pg.vertex(tuskTipWidth * 0.3, tuskHeight) pg.bezierVertex( tuskTipWidth * 0.8, tuskHeight * 0.7, tuskTopWidth * 0.5, tuskHeight * 0.3, tuskTopWidth * 0.5, 0 ) pg.endShape(CLOSE) pg.pop() pg.pop() } function drawEyes(pg, m) { let eyeColor, strokeColor, strokeW; if (TRAITS.iris === "Null") { eyeColor = 255; strokeColor = 0; strokeW = m.w * 0.015; } else if (TRAITS.iris === "Abyss") { eyeColor = 0; strokeColor = 255; strokeW = m.w * 0.012; } else { eyeColor = 255; strokeColor = 0; strokeW = m.w * 0.012; } const rx = m.w * 0.22 const ry = m.h * 0.13 const ly = -m.h * 0.12 const left = { x: -m.w * 0.18, y: ly } const right = { x: m.w * 0.18, y: ly } pg.push() pg.noStroke() pg.fill(strokeColor) if (m.sight === "Mono") { const center = { x: 0, y: ly } drawEye(pg, center, rx, ry, m.sight, strokeW) } else if (m.sight === "Verge") { drawVerticalAlmondTotal(pg, rx, ry, ly, m.w, strokeW) } else { drawEye(pg, left, rx, ry, m.sight, strokeW) drawEye(pg, right, rx, ry, m.sight, strokeW) } pg.fill(eyeColor) if (m.sight === "Mono") { const center = { x: 0, y: ly } drawEye(pg, center, rx, ry, m.sight, 0) } else if (m.sight === "Verge") { drawVerticalAlmondTotal(pg, rx, ry, ly, m.w, 0) } else { drawEye(pg, left, rx, ry, m.sight, 0) drawEye(pg, right, rx, ry, m.sight, 0) } pg.pop() } function drawVerticalAlmondTotal(pg, rx, ry, ly, maskWidth, strokeW = 0) { const almondWidth = rx * 0.5 const almondHeight = ry * 1.8 const spacing = maskWidth * 0.15 const w = almondWidth + strokeW * 2 const h = almondHeight + strokeW * 2 for (let pos of [-spacing, 0, spacing]) { pg.push() pg.translate(pos, ly) const width = pos === 0 ? w * 1.1 : w const height = pos === 0 ? h : h pg.beginShape() pg.vertex(0, -height / 2) pg.bezierVertex( width * 0.5, -height * 0.3, width * 0.5, height * 0.3, 0, height / 2 ) pg.bezierVertex( -width * 0.5, height * 0.3, -width * 0.5, -height * 0.3, 0, -height / 2 ) pg.endShape(CLOSE) pg.pop() } } function drawEye(pg, c, rx, ry, type, strokeW = 0) { pg.push() pg.translate(c.x, c.y) pg.rectMode(CENTER) const hasPupil = type.includes("with Pupil") && TRAITS.count !== "Conflux" if (type === "Core") { pg.ellipse(0, 0, rx * 1.0 + strokeW * 2, ry * 2 + strokeW * 2) if (hasPupil && strokeW === 0) { pg.push() pg.fill(0, 0, 0) const pupilWidth = min(rx, ry) * 0.1 const pupilHeight = ry * 1.6 pg.ellipse(0, -ry * 0.25, pupilWidth, pupilHeight) pg.pop() } } else if (type === "Lune") { pg.push() const crescentRadius = ry * 0.7 + strokeW const cutoutRadius = ry * 0.95 const cutoutOffset = ry * 0.4 pg.beginShape() for (let a = 0; a <= PI; a += PI / 20) { pg.vertex(cos(a) * crescentRadius, sin(a) * crescentRadius - cutoutOffset) } for (let a = PI; a >= 0; a -= PI / 20) { pg.vertex(cos(a) * cutoutRadius, sin(a) * cutoutRadius - cutoutOffset) } pg.endShape(CLOSE) pg.pop() } else if (type === "Teardrop") { pg.push() const tearWidth = rx * 0.9 + strokeW const tearHeight = ry * 1.4 + strokeW const pointOffset = tearWidth * 0.7 const pointDir = c.x < 0 ? 1 : -1 pg.beginShape() pg.vertex(pointDir * pointOffset, 0) pg.bezierVertex( pointDir * pointOffset * 0.3, -tearHeight * 0.5, -pointDir * tearWidth * 0.3, -tearHeight * 0.6, -pointDir * tearWidth * 0.5, 0 ) pg.bezierVertex( -pointDir * tearWidth * 0.3, tearHeight * 0.6, pointDir * pointOffset * 0.3, tearHeight * 0.5, pointDir * pointOffset, 0 ) pg.endShape(CLOSE) pg.pop() } else if (type === "Mono") { const visorWidth = rx * 2.0 + strokeW * 2 const visorHeight = ry * 0.8 + strokeW * 2 const visorRadius = visorHeight * 0.4 pg.rect(0, 0, visorWidth, visorHeight, visorRadius) } else if (type === "Echo") { const ovalWidth = rx * 1.2 + strokeW * 2 const ovalHeight = ry * 0.35 + strokeW * 2 const gap = ry * 0.5 pg.ellipse(0, -gap, ovalWidth, ovalHeight) pg.ellipse(0, 0, ovalWidth * 1.1, ovalHeight) pg.ellipse(0, gap, ovalWidth, ovalHeight) } else if (type === "Slate") { const rectWidth = rx * 1.0 + strokeW * 2 const rectHeight = ry * 0.4 + strokeW * 2 const gap = ry * 0.9 const cornerRadius = rectHeight pg.rect(0, -gap, rectWidth, rectHeight, cornerRadius) pg.rect(0, 0, rectWidth * 1.0, rectHeight, rectHeight * 1.0) pg.rect(0, gap, rectWidth, rectHeight, rectHeight * 0.5) } else if (type === "Vert") { const segments = 5 const segWidth = rx * 0.33 + strokeW * 2 const totalHeight = ry * 1.6 const segHeight = totalHeight / segments + strokeW * 2 const gap = segHeight * 0.1 for (let i = 0; i < segments; i++) { const y = (i - (segments - 1) / 2) * (totalHeight / segments) pg.ellipse(0, y, segWidth, segHeight - gap) } } else if (type === "Signal") { const dashes = 3 const dashWidth = rx * 1.2 / dashes + strokeW * 2 const dashHeight = ry * 0.6 + strokeW * 2 const gap = dashWidth * 0.25 for (let i = 0; i < dashes; i++) { const x = (i - (dashes - 1) / 2) * (rx * 1.2 / dashes) pg.rect(x, 0, dashWidth - gap, dashHeight, dashHeight * 0.2) } } else if (type === "Pierce") { const dots = 1 const dotSize = min(rx, ry) * 0.75 + strokeW * 2 const spacing = rx * 0.5 for (let i = 0; i < dots; i++) { const x = (i - (dots - 1) / 2) * spacing pg.circle(x, 0, dotSize) } } else if (type === "Cross") { const crossWidth = rx * 0.3 + strokeW * 2 const crossHeight = ry * 1.6 + strokeW * 2 pg.rect(0, 0, crossWidth, crossHeight) pg.rect(0, 0, crossHeight * 0.6, crossWidth) } else if (type === "Cut") { const slitCount = 3 const slitWidth = rx * 0.7 + strokeW * 2 const slitHeight = ry * 0.2 + strokeW * 2 const gap = ry * 0.6 for (let i = 0; i < slitCount; i++) { const y = (i - (slitCount - 1) / 2) * gap pg.rect(0, y, slitWidth, slitHeight) } } else { pg.rect(0, 0, rx * 1.2 + strokeW * 2, ry * 0.4 + strokeW * 2, ry * 0.2) } pg.pop() } function carveMouth(pg, m) { const y = m.h * 0.22 if (TRAITS.count === "Conflux") { pg.push() const translateX = m.maskIndex === 0 ? m.w * 0.18 : -m.w * 0.18 pg.translate(translateX, 0) } pg.rectMode(CENTER) if (m.voice === "Still") { pg.rect(0, y, m.w * 0.35, m.h * 0.03) } else if (m.voice === "Prowl") { pg.ellipse(0, y, m.w * 0.09, m.h * 0.4) } else if (m.voice === "Forge") { pg.rect(0, y, m.w * 0.28, m.h * 0.06, m.h * 0.01) } else if (m.voice === "Sunder") { const segmentCount = 4 const totalWidth = m.w * 0.25 const segmentWidth = totalWidth / segmentCount const segmentHeight = m.h * 0.04 const gap = segmentWidth * 0.15 for (let i = 0; i < segmentCount; i++) { const x = (i - (segmentCount - 1) / 2) * segmentWidth pg.rect(x, y, segmentWidth - gap, segmentHeight, segmentHeight * 0.1) } } else if (m.voice === "Hollow") { pg.rect(0, y, m.w * 0.35, m.h * 0.02) } else if (m.voice === "Devour") { pg.beginShape() const points = 12 const width = m.w * 0.4 const height = m.h * 0.1 for (let i = 0; i <= points; i++) { const x = map(i, 0, points, -width / 2, width / 2) const yOffset = (i % 2 === 0) ? -height / 2 : height / 2 pg.vertex(x, y + yOffset) } pg.endShape() pg.strokeWeight(m.h * 0.01) pg.noFill() pg.beginShape() for (let i = 0; i <= points; i++) { const x = map(i, 0, points, -width / 2, width / 2) const yOffset = (i % 2 === 0) ? -height / 2 : height / 2 pg.vertex(x, y + yOffset) } pg.endShape() pg.noStroke() pg.fill(255) } else if (m.voice === "Viper") { const fangWidth = m.w * 0.06 const fangHeight = m.h * 0.08 const fangSpacing = m.w * 0.15 pg.push() pg.translate(-fangSpacing, y) pg.triangle(0, fangHeight, -fangWidth / 2, 0, fangWidth / 2, 0) pg.pop() pg.push() pg.translate(fangSpacing, y) pg.triangle(0, fangHeight, -fangWidth / 2, 0, fangWidth / 2, 0) pg.pop() } else if (m.voice === "Bind") { const mouthWidth = m.w * 0.35 const mouthHeight = m.h * 0.015 pg.rect(0, y, mouthWidth, mouthHeight) const stitchCount = 6 const stitchHeight = m.h * 0.03 const stitchWidth = m.w * 0.01 for (let i = 0; i < stitchCount; i++) { const x = (i - (stitchCount - 1) / 2) * (mouthWidth / (stitchCount - 1)) pg.rect(x, y, stitchWidth, stitchHeight) } } else if (m.voice === "Downfall") { pg.rect(0, y, m.w * 0.25, m.h * 0.015) } else if (m.voice === "Stun") { const mouthWidth = m.h * 0.12 const mouthHeight = m.h * 0.06 pg.push() pg.translate(0, y + mouthHeight) pg.beginShape() for (let a = PI; a <= TWO_PI; a += PI / 20) { const x = cos(a) * mouthWidth / 2 const yOffset = sin(a) * mouthHeight pg.vertex(x, yOffset) } pg.vertex(mouthWidth / 2, 0) pg.vertex(-mouthWidth / 2, 0) pg.endShape(CLOSE) pg.pop() } else if (m.voice === "Grate") { const barCount = 4 const barWidth = m.w * 0.3 const barHeight = m.h * 0.03 const spacing = m.h * 0.04 for (let i = 0; i < barCount; i++) { const barY = y + (i - (barCount - 1) / 2) * spacing pg.rect(0, barY, barWidth, barHeight) } } else if (m.voice === "Ease") { pg.push() pg.translate(0, y) const waveWidth = m.w * 0.4 const waveHeight = m.h * 0.025 pg.beginShape() const steps = 30 for (let i = 0; i <= steps; i++) { const t = i / steps const x = map(t, 0, 1, -waveWidth / 2, waveWidth / 2) const waveY = sin(t * PI * 3) * waveHeight pg.vertex(x, waveY) } for (let i = steps; i >= 0; i--) { const t = i / steps const x = map(t, 0, 1, -waveWidth / 2, waveWidth / 2) const waveY = sin(t * PI * 3) * waveHeight + m.h * 0.015 pg.vertex(x, waveY) } pg.endShape(CLOSE) pg.pop() } else { pg.rect(0, y, m.w * 0.45, m.h * 0.018) } if (TRAITS.count === "Conflux") { pg.pop() } } function carveNose(pg, m) { if (m.scent === "Sealed") return const y = m.h * 0.05 + 20 if (TRAITS.count === "Conflux") { pg.push() const translateX = m.maskIndex === 0 ? m.w * 0.18 : -m.w * 0.18 pg.translate(translateX, 0) } pg.rectMode(CENTER) if (m.scent === "Aero") { pg.rect(0, y, m.w * 0.05, m.h * 0.07) } else if (m.scent === "Sego") { const segments = 3 const totalHeight = m.h * 0.12 const segHeight = totalHeight / segments const gap = segHeight * 0.25 for (let i = 0; i < segments; i++) { const segY = y + (i - (segments - 1) / 2) * segHeight pg.rect(0, segY, m.w * 0.05, segHeight - gap) } } else if (m.scent === "Duos") { const dotSize = m.w * 0.025 const spacing = m.w * 0.04 pg.circle(-spacing / 2, y, dotSize) pg.circle(spacing / 2, y, dotSize) } else if (m.scent === "Jewel") { pg.push() pg.translate(0, y) const size = m.w * 0.04 pg.quad(0, -size, size, 0, 0, size, -size, 0) pg.pop() } else if (m.scent === "Dot") { const dotSize = m.w * 0.03 pg.circle(0, y, dotSize) } else if (m.scent === "Aexo") { pg.push() pg.translate(0, y) pg.triangle(0, m.h * 0.04, -m.w * 0.03, -m.h * 0.04, m.w * 0.03, -m.h * 0.04) pg.pop() } else if (m.scent === "Tri") { const dotSize = m.w * 0.02 const spacing = m.h * 0.04 pg.circle(0, y - spacing, dotSize) pg.circle(0, y, dotSize) pg.circle(0, y + spacing, dotSize) } if (TRAITS.count === "Conflux") { pg.pop() } } function carveCutouts(pg, m) { if (m.hollows === "Whole") return if (m.hollows === "Cleave") { if (TRAITS.count === "Conflux") { const isLeftMask = m.x < width / 2 if (isLeftMask) { pg.ellipse(-m.w * 0.28, m.h * 0.08, m.w * 0.08, m.h * 0.08) } else { pg.ellipse(m.w * 0.28, m.h * 0.08, m.w * 0.08, m.h * 0.08) } } else { pg.ellipse(-m.w * 0.28, m.h * 0.08, m.w * 0.08, m.h * 0.08) pg.ellipse(m.w * 0.28, m.h * 0.08, m.w * 0.08, m.h * 0.08) } } else if (m.hollows === "Mark") { pg.ellipse(0, -m.h * 0.32, m.w * 0.08, m.h * 0.08) } else if (m.hollows === "Maw") { pg.rectMode(CENTER) const hollowX = m.w * 0.30 const hollowY = m.h * 0.18 const hollowWidth = m.w * 0.18 const hollowHeight = m.h * 0.28 if (TRAITS.count === "Conflux") { const isLeftMask = m.maskIndex === 0 if (isLeftMask) { pg.push
nstrainToMaskBounds(x, y, vx, vy) { if (!globalBounds) return { x, y, vx, vy } let newX = x let newY = y let newVx = vx let newVy = vy const bounce = 0.7 if (x < globalBounds.minX) { newX = globalBounds.minX newVx = Math.abs(vx) * bounce } else if (x > globalBounds.maxX) { newX = globalBounds.maxX newVx = -Math.abs(vx) * bounce } if (y < globalBounds.minY) { newY = globalBounds.minY newVy = Math.abs(vy) * bounce } else if (y > globalBounds.maxY) { newY = globalBounds.maxY newVy = -Math.abs(vy) * bounce } return { x: newX, y: newY, vx: newVx, vy: newVy } } function initTopology() { const getConstrainedOrigin = () => { if (maskBoundsCache.length === 0) { return { x: width / 2, y: height / 2 } } return getRandomPointInMasks() } if (TRAITS.originTopology === "Clustered Burst") { originCenters = [getConstrainedOrigin()] } else if (TRAITS.originTopology === "Dual-Core") { if (maskBoundsCache.length >= 2) { const bounds1 = maskBoundsCache[0] const bounds2 = maskBoundsCache[maskBoundsCache.length > 1 ? 1 : 0] originCenters = [ { x: bounds1.centerX + random(-60, 60), y: bounds1.centerY + random(-60, 60) }, { x: bounds2.centerX + random(-60, 60), y: bounds2.centerY + random(-60, 60) } ] } else { const pt = getConstrainedOrigin() originCenters = [ { x: pt.x - 80, y: pt.y }, { x: pt.x + 80, y: pt.y } ] } } else if (TRAITS.originTopology === "Radial Drift") { originCenters = [getConstrainedOrigin()] } else if (TRAITS.originTopology === "Tidal Sweep") { tidalAngle = random(-PI / 9, PI / 9) } else if (TRAITS.originTopology === "Void Edge") { if (maskBoundsCache.length > 0) { const bounds = random(maskBoundsCache) let side = floor(random(4)) if (side === 0) originCenters = [{ x: bounds.centerX, y: bounds.minY }] else if (side === 1) originCenters = [{ x: bounds.maxX, y: bounds.centerY }] else if (side === 2) originCenters = [{ x: bounds.centerX, y: bounds.maxY }] else originCenters = [{ x: bounds.minX, y: bounds.centerY }] } else { originCenters = [{ x: width / 2, y: height / 2 }] } } else { originCenters = [getConstrainedOrigin()] } } function spawner() { const maxCap = min(_tierMaxAnims, PARAMS.maxSupernovas) const activeCount = fillAnimations.filter(a => a.averageOpacity() > 20).length if (activeCount >= maxCap) return if (frameCount >= nextSpawnFrame) { spawnFillAnimation() if (PARAMS.spawnMode === "Clockwork") { nextSpawnFrame = frameCount + PARAMS.baseSpawnEvery } else if (PARAMS.spawnMode === "Poisson") { nextSpawnFrame = frameCount + floor(randomGaussian(PARAMS.baseSpawnEvery, 5)) } else if (PARAMS.spawnMode === "BurstRest") { if (random() < 0.2) { const remaining = maxCap - fillAnimations.filter(a => a.averageOpacity() > 20).length const burstCount = min(3, max(0, remaining)) for (let i = 0; i < burstCount; i++) spawnFillAnimation() nextSpawnFrame = frameCount + PARAMS.baseSpawnEvery * 2 } else { nextSpawnFrame = frameCount + PARAMS.baseSpawnEvery } } else { nextSpawnFrame = frameCount + PARAMS.baseSpawnEvery } } } function spawnFillAnimation() { let sx, sy if (TRAITS.originTopology === "Tidal Sweep") { const sweep = (frameCount / 120.0) % 1.0 const pt = getRandomPointInMasks() sx = lerp(pt.x - 100, pt.x + 100, sweep) sy = pt.y + sin(tidalAngle) * 80 const constrained = constrainToMaskBounds(sx, sy, 0, 0) sx = constrained.x sy = constrained.y } else { const origin = random(originCenters) sx = origin.x + randomGaussian(0, 40) sy = origin.y + randomGaussian(0, 40) const constrained = constrainToMaskBounds(sx, sy, 0, 0) sx = constrained.x sy = constrained.y } const variant = PARAMS.fillVariant let animation if (variant === "Sparkles") { const sparkleCount = fillAnimations.filter(a => a instanceof Sparkles).length if (sparkleCount >= 6) return } if (variant === "Supernova") { const count = floor(random(PARAMS.rectRange[0], PARAMS.rectRange[1])) animation = animationPool.acquire('Supernova', sx, sy, PARAMS.palette, count, PARAMS.sizeRange, TRAITS) } else if (variant === "ExpandingCircles") { animation = animationPool.acquire('ExpandingCircles', sx, sy, PARAMS.palette, TRAITS) } else if (variant === "ExpandingSquares") { animation = animationPool.acquire('ExpandingSquares', sx, sy, PARAMS.palette, TRAITS) } else if (variant === "VerticalStripes") { animation = animationPool.acquire('VerticalStripes', sx, sy, PARAMS.palette, TRAITS) } else if (variant === "Gradient") { animation = animationPool.acquire('GradientFill', sx, sy, PARAMS.palette, TRAITS) } else if (variant === "WavyLines") { animation = animationPool.acquire('WavyLines', sx, sy, PARAMS.palette, TRAITS) } else if (variant === "Sparkles") { animation = animationPool.acquire('Sparkles', sx, sy, PARAMS.palette, TRAITS) } else if (variant === "HorizontalStripes") { animation = animationPool.acquire('HorizontalStripes', sx, sy, PARAMS.palette, TRAITS) } else if (variant === "VoronoiCells") { animation = animationPool.acquire('VoronoiCells', sx, sy, PARAMS.palette, TRAITS) } else if (variant === "Shockwave") { animation = animationPool.acquire('Shockwave', sx, sy, PARAMS.palette, TRAITS) } if (animation) { fillAnimations.push(animation) } } function drawBackground(g, bgType) { g.push() g.noStroke() if (bgType === "Halo") { g.background(0, 0, 75) } else if (bgType === "Aero") { g.background(200, 15, 85) } else if (bgType === "Rose") { g.background(330, 25, 90) } else if (bgType === "Mint") { g.background(150, 20, 88) } else if (bgType === "Mist") { g.background(270, 18, 88) } else if (bgType === "Rise") { g.background(20, 30, 90) } else if (bgType === "Fallen") { g.background(0, 100, 90) } else if (bgType === "Shift") { const colors = PARAMS.palette const c1 = g.color(colors[0]) const c2 = g.color(colors[colors.length - 1]) const ctx = g.drawingContext const grad = ctx.createLinearGradient(0, 0, 0, g.height) grad.addColorStop(0, g.color(colors[0]).toString()) grad.addColorStop(1, g.color(colors[colors.length - 1]).toString()) ctx.fillStyle = grad ctx.fillRect(0, 0, g.width, g.height) } else if (bgType === "Radiant") { const colors = PARAMS.palette const centerX = g.width / 2 const centerY = g.height / 2 const radius = Math.sqrt(centerX * centerX + centerY * centerY) const ctx = g.drawingContext const grad = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, radius) grad.addColorStop(0, g.color(colors[0]).toString()) grad.addColorStop(1, g.color(colors[colors.length - 1]).toString()) ctx.fillStyle = grad ctx.fillRect(0, 0, g.width, g.height) } else if (bgType === "Facet") { const colors = PARAMS.palette const centerX = g.width / 2 const centerY = g.height / 2 const maxDist = centerX + centerY const stepSize = 4 for (let y = 0; y < g.height; y += stepSize) { for (let x = 0; x < g.width; x += stepSize) { const d = Math.abs(x - centerX) + Math.abs(y - centerY) const inter = constrain(d / maxDist, 0, 1) const c1 = g.color(colors[0]) const c2 = g.color(colors[colors.length - 1]) const gradColor = g.lerpColor(c1, c2, inter) g.fill(gradColor) g.noStroke() g.rect(x, y, stepSize, stepSize) } } } else if (bgType === "Ascend") { const colors = PARAMS.palette const maxDist = g.width + g.height const stepSize = 4 for (let y = 0; y < g.height; y += stepSize) { for (let x = 0; x < g.width; x += stepSize) { const d = x + y const inter = constrain(d / maxDist, 0, 1) const c1 = g.color(colors[0]) const c2 = g.color(colors[colors.length - 1]) const gradColor = g.lerpColor(c1, c2, inter) g.fill(gradColor) g.noStroke() g.rect(x, y, stepSize, stepSize) } } } else if (bgType === "Descend") { const colors = PARAMS.palette const maxDist = g.width + g.height const stepSize = 4 for (let y = 0; y < g.height; y += stepSize) { for (let x = 0; x < g.width; x += stepSize) { const d = (g.width - x) + (g.height - y) const inter = constrain(d / maxDist, 0, 1) const c1 = g.color(colors[0]) const c2 = g.color(colors[colors.length - 1]) const gradColor = g.lerpColor(c1, c2, inter) g.fill(gradColor) g.noStroke() g.rect(x, y, stepSize, stepSize) } } } else { g.background(0, 0, 100) } g.pop() } function drawArchetype(g, maskCount, archetype) { g.push() g.noStroke() g.fill(0, 0, 10) const scaleX = g.width / 1000 const scaleY = g.width / 1000 // v2: width-based to preserve proportions on portrait canvas const styleToUse = TRAITS.displayedArchetype if (styleToUse === "Helios") { drawHeliosArchetype(g, scaleX, scaleY) } else if (styleToUse === "Guard") { drawGuardArchetype(g, scaleX, scaleY) } else if (styleToUse === "Riven") { drawRivenArchetype(g, scaleX, scaleY) } else if (styleToUse === "Rogue") { drawRogueArchetype(g, scaleX, scaleY) } else if (styleToUse === "Apex") { drawApexArchetype(g, scaleX, scaleY) } else if (styleToUse === "Goliath") { drawGoliathArchetype(g, scaleX, scaleY) } else if (styleToUse === "Sentinel") { drawSentinelArchetype(g, scaleX, scaleY) } else if (styleToUse === "Colossus") { drawColossusArchetype(g, scaleX, scaleY) } else if (styleToUse === "Ferox") { drawFeroxArchetype(g, scaleX, scaleY) } else if (styleToUse === "Seeker") { drawSeekerArchetype(g, scaleX, scaleY) } else if (styleToUse === "Arbiter") { drawArbiterArchetype(g, scaleX, scaleY) } else if (styleToUse === "Nomad") { drawNomadArchetype(g, scaleX, scaleY) } else if (styleToUse === "Prime") { drawPrimeArchetype(g, scaleX, scaleY) } else if (styleToUse === "Dux") { drawDuxArchetype(g, scaleX, scaleY) } else if (styleToUse === "Bulwark") { drawBulwarkArchetype(g, scaleX, scaleY) } else if (styleToUse === "Warden") { drawWardenArchetype(g, scaleX, scaleY) } else if (styleToUse === "Shade") { drawShadeArchetype(g, scaleX, scaleY) } else if (styleToUse === "Brute") { drawBruteArchetype(g, scaleX, scaleY) } else if (styleToUse === "Emperor") { drawEmperorArchetype(g, scaleX, scaleY) } else if (styleToUse === "Armatus") { drawArmatusArchetype(g, scaleX, scaleY) } else { drawTitanArchetype(g, scaleX, scaleY, maskCount) } g.noStroke() if (styleToUse === "Apex") { const botY = g.height + 50 const slopeL = (112.2 - 265.6) / (1020 - 233.3) const slopeR = (866.2 - 716.8) / (1020 - 236.9) const topY = 980 const lxTop = 112.2 + (topY - 1020) * slopeL const rxTop = 866.2 + (topY - 1020) * slopeR const lxBot = 112.2 + (botY / scaleY - 1020) * slopeL const rxBot = 866.2 + (botY / scaleY - 1020) * slopeR g.beginShape() g.vertex(lxTop * scaleX, topY * scaleY) g.vertex(rxTop * scaleX, topY * scaleY) g.vertex(rxBot * scaleX, botY) g.vertex(lxBot * scaleX, botY) g.endShape(CLOSE) } else { g.rect(-20, 990 * scaleY, g.width + 40, g.height) } g.pop() } function drawArchetypePart1(g, maskCount, archetype) { g.push() g.noStroke() g.fill(0, 0, 8) const scaleX = g.width / 1000 const scaleY = g.width / 1000 // v2: width-based if (archetype === "Blade") { drawBladePart1(g, scaleX, scaleY) } else if (archetype === "Havoc") { drawHavocPart1(g, scaleX, scaleY) } else if (archetype === "Vanta") { drawVantaPart1(g, scaleX, scaleY) } g.noStroke() g.rect(-20, 990 * scaleY, g.width + 40, g.height) g.pop() } function drawArchetypePart2(g, maskCount, archetype) { g.push() g.noStroke() g.fill(0, 0, 8) const scaleX = g.width / 1000 const scaleY = g.width / 1000 // v2: width-based if (archetype === "Blade") { drawBladePart2(g, scaleX, scaleY) } else if (archetype === "Havoc") { drawHavocPart2(g, scaleX, scaleY) } else if (archetype === "Vanta") { drawVantaPart2(g, scaleX, scaleY) } g.noStroke() g.rect(-20, 990 * scaleY, g.width + 40, g.height) g.pop() } /* ========================= EXISTING ARCHETYPE SHAPES ========================= */ function drawSentinelArchetype(g, scaleX, scaleY) { g.beginShape() g.vertex(877.1 * scaleX, 739.2 * scaleY) g.vertex(762.9 * scaleX, 357.1 * scaleY) g.vertex(800.5 * scaleX, 235.7 * scaleY) g.vertex(687 * scaleX, 22.2 * scaleY) g.vertex(687 * scaleX, 224.8 * scaleY) g.vertex(667.1 * scaleX, 177.1 * scaleY) g.vertex(497 * scaleX, 121.3 * scaleY) g.vertex(326.9 * scaleX, 177.1 * scaleY) g.vertex(310.1 * scaleX, 217.5 * scaleY) g.vertex(310.1 * scaleX, 22.2 * scaleY) g.vertex(196.6 * scaleX, 235.7 * scaleY) g.vertex(236.3 * scaleX, 348 * scaleY) g.vertex(144.8 * scaleX, 734.9 * scaleY) g.vertex(-0.6 * scaleX, 838.8 * scaleY) g.vertex(-0.6 * scaleX, 1020 * scaleY) g.vertex(1000.8 * scaleX, 1020 * scaleY) g.vertex(1000.8 * scaleX, 838.8 * scaleY) g.endShape(CLOSE) } function drawColossusArchetype(g, scaleX, scaleY) { g.beginShape() g.vertex(976.9 * scaleX, 643.4 * scaleY) g.vertex(976.9 * scaleX, 608.4 * scaleY) g.vertex(950.9 * scaleX, 608.4 * scaleY) g.vertex(950.9 * scaleX, 639.4 * scaleY) g.vertex(922.8 * scaleX, 635 * scaleY) g.vertex(922.8 * scaleX, 578.4 * scaleY) g.vertex(896.8 * scaleX, 578.4 * scaleY) g.vertex(896.8 * scaleX, 631 * scaleY) g.vertex(868.7 * scaleX, 626.7 * scaleY) g.vertex(868.7 * scaleX, 578.4 * scaleY) g.vertex(842.7 * scaleX, 578.4 * scaleY) g.vertex(842.7 * scaleX, 622.6 * scaleY) g.vertex(820.7 * scaleX, 619.2 * scaleY) g.vertex(819 * scaleX, 262.8 * scaleY) g.vertex(709.2 * scaleX, 119.6 * scaleY) g.vertex(696 * scaleX, 119.6 * scaleY) g.vertex(696 * scaleX, 89.3 * scaleY) g.vertex(669.9 * scaleX, 89.3 * scaleY) g.vertex(669.9 * scaleX, 119.6 * scaleY) g.vertex(641.9 * scaleX, 119.6 * scaleY) g.vertex(641.9 * scaleX, 89.3 * scaleY) g.vertex(615.8 * scaleX, 89.3 * scaleY) g.vertex(615.8 * scaleX, 119.6 * scaleY) g.vertex(587.8 * scaleX, 119.6 * scaleY) g.vertex(587.8 * scaleX, 89.3 * scaleY) g.vertex(561.7 * scaleX, 89.3 * scaleY) g.vertex(561.7 * scaleX, 119.6 * scaleY) g.vertex(533.7 * scaleX, 119.6 * scaleY) g.vertex(533.7 * scaleX, 89.3 * scaleY) g.vertex(507.6 * scaleX, 89.3 * scaleY) g.vertex(507.6 * scaleX, 119.6 * scaleY) g.vertex(479.6 * scaleX, 119.6 * scaleY) g.vertex(479.6 * scaleX, 89.3 * scaleY) g.vertex(453.5 * scaleX, 89.3 * scaleY) g.vertex(453.5 * scaleX, 119.6 * scaleY) g.vertex(425.5 * scaleX, 119.6 * scaleY) g.vertex(425.5 * scaleX, 89.3 * scaleY) g.vertex(399.4 * scaleX, 89.3 * scaleY) g.vertex(399.4 * scaleX, 119.6 * scaleY) g.vertex(371.4 * scaleX, 119.6 * scaleY) g.vertex(371.4 * scaleX, 89.3 * scaleY) g.vertex(345.3 * scaleX, 89.3 * scaleY) g.vertex(345.3 * scaleX, 119.6 * scaleY) g.vertex(317.3 * scaleX, 119.6 * scaleY) g.vertex(317.3 * scaleX, 89.3 * scaleY) g.vertex(291.2 * scaleX, 89.3 * scaleY) g.vertex(291.2 * scaleX, 119.6 * scaleY) g.vertex(276.2 * scaleX, 119.6 * scaleY) g.vertex(183.2 * scaleX, 251.3 * scaleY) g.vertex(176.8 * scaleX, 617.3 * scaleY) g.vertex(158.8 * scaleX, 620.4 * scaleY) g.vertex(158.8 * scaleX, 578.4 * scaleY) g.vertex(132.7 * scaleX, 578.4 * scaleY) g.vertex(132.7 * scaleX, 624.9 * scaleY) g.vertex(104.7 * scaleX, 629.7 * scaleY) g.vertex(104.7 * scaleX, 578.4 * scaleY) g.vertex(78.6 * scaleX, 578.4 * scaleY) g.vertex(78.6 * scaleX, 634.1 * scaleY) g.vertex(50.6 * scaleX, 638.9 * scaleY) g.vertex(50.6 * scaleX, 608.4 * scaleY) g.vertex(24.5 * scaleX, 608.4 * scaleY) g.vertex(24.5 * scaleX, 643.4 * scaleY) g.vertex(0.3 * scaleX, 647.5 * scaleY) g.vertex(-0.3 * scaleX, 1020 * scaleY) g.vertex(1001.9 * scaleX, 1020 * scaleY) g.vertex(1003.3 * scaleX, 647.5 * scaleY) g.endShape(CLOSE) } function drawRivenArchetype(g, scaleX, scaleY) { g.beginShape() g.vertex(910 * scaleX, 849.7 * scaleY) g.vertex(890 * scaleX, 668.1 * scaleY) g.vertex(781.4 * scaleX, 778.1 * scaleY) g.vertex(801.4 * scaleX, 454.1 * scaleY) g.vertex(731.4 * scaleX, 207.8 * scaleY) g.vertex(650.2 * scaleX, 97.8 * scaleY) g.vertex(501.3 * scaleX, 77.3 * scaleY) g.vertex(343.9 * scaleX, 96.7 * scaleY) g.vertex(262.7 * scaleX, 207.8 * scaleY) g.vertex(192.7 * scaleX, 454.1 * scaleY) g.vertex(212.7 * scaleX, 778.1 * scaleY) g.vertex(104.1 * scaleX, 668.1 * scaleY) g.vertex(84.1 * scaleX, 849.7 * scaleY) g.vertex(1.3 * scaleX, 879.7 * scaleY) g.vertex(1.3 * scaleX, 1020 * scaleY) g.vertex(1002 * scaleX, 1020 * scaleY) g.vertex(1002 * scaleX, 879.7 * scaleY) g.endShape(CLOSE) } function drawRogueArchetype(g, scaleX, scaleY) { g.beginShape() let x = 931.5 * scaleX let y = 820.2 * scaleY g.vertex(x, y) x += -68.6 * scaleX y += -18.2 * scaleY g.vertex(x, y) x += -30 * scaleX y += -9.3 * scaleY g.vertex(x, y) let cp1x = x + (-31.3 * scaleX) let cp1y = y + (-9.7 * scaleY) let cp2x = x + (-53.5 * scaleX) let cp2y = y + (-37.4 * scaleY) x += -56.4 * scaleX y += -70 * scaleY g.bezierVertex(cp1x, cp1y, cp2x, cp2y, x, y) x += -33.8 * scaleX y += -385.5 * scaleY g.vertex(x, y) x += 115 * scaleX y += -253.1 * scaleY g.vertex(x, y) x += -146.9 * scaleX y += 91.6 * scaleY g.vertex(x, y) x += -209.7 * scaleX y += -100 * scaleY g.vertex(x, y) x += -210.1 * scaleX y += 105.2 * scaleY g.vertex(x, y) x += -152.7 * scaleX y += -93.8 * scaleY g.vertex(x, y) x += 112.4 * scaleX y += 253.6 * scaleY g.vertex(x, y) x += -33.9 * scaleX y += 386.8 * scaleY g.vertex(x, y) cp1x = x + (-2.6 * scaleX) cp1y = y + (30.1 * scaleY) cp2x = x + (-21.9 * scaleX) cp2y = y + (56.2 * scaleY) x += -49.9 * scaleX y += 67.6 * scaleY g.bezierVertex(cp1x, cp1y, cp2x, cp2y, x, y) x += -26.6 * scaleX y += 10.9 * scaleY g.vertex(x, y) x += -70.6 * scaleX y += 22.2 * scaleY g.vertex(x, y) x = -1 * scaleX y = 878.2 * scaleY g.vertex(x, y) y = 1001 * scaleY g.vertex(x, y) x = 1001 * scaleX g.vertex(x, y) y = 820.2 * scaleY g.vertex(x, y) x = 931.5 * scaleX g.vertex(x, y) g.endShape(CLOSE) } function drawApexArchetype(g, scaleX, scaleY) { g.beginShape() g.vertex(112.2 * scaleX, 1020 * scaleY) g.vertex(265.6 * scaleX, 233.3 * scaleY) g.vertex(404.5 * scaleX, 138.3 * scaleY) g.vertex(589.8 * scaleX, 138.3 * scaleY) g.vertex(716.8 * scaleX, 236.9 * scaleY) g.vertex(866.2 * scaleX, 1020 * scaleY) g.endShape(CLOSE) g.beginShape() g.vertex(739.6 * scaleX, 241.2 * scaleY) g.vertex(848.7 * scaleX, 241.2 * scaleY) g.vertex(756.4 * scaleX, 333.5 * scaleY) g.endShape(CLOSE) g.beginShape() g.vertex(222.9 * scaleX, 333.4 * scaleY) g.vertex(243.4 * scaleX, 241.6 * scaleY) g.vertex(122.5 * scaleX, 241.6 * scaleY) g.endShape(CLOSE) g.beginShape() g.vertex(448.4 * scaleX, 64.6 * scaleY) g.vertex(499.5 * scaleX, 115.7 * scaleY) g.vertex(549.6 * scaleX, 65.6 * scaleY) g.endShape(CLOSE) } function drawGoliathArchetype(g, scaleX, scaleY) { g.beginShape() g.vertex(892.4 * scaleX, 679.5 * scaleY) g.vertex(882.5 * scaleX, 621.9 * scaleY) g.vertex(906.4 * scaleX, 493.9 * scaleY) g.vertex(852.8 * scaleX, 432.7 * scaleY) g.vertex(810.4 * scaleX, 200.7 * scaleY) g.vertex(489.9 * scaleX, 53.8 * scaleY) g.vertex(180.9 * scaleX, 179.7 * scaleY) g.vertex(145.4 * scaleX, 420.6 * scaleY) g.vertex(87.6 * scaleX, 490 * scaleY) g.vertex(115.6 * scaleX, 623.3 * scaleY) g.vertex(108.4 * scaleX, 671.9 * scaleY) g.vertex(-2.2 * scaleX, 757.7 * scaleY) g.vertex(-0.5 * scaleX, 1020 * scaleY) g.vertex(1001.6 * scaleX, 1020 * scaleY) g.vertex(1003 * scaleX, 757.7 * scaleY) g.endShape(CLOSE) } function drawHeliosArchetype(g, scaleX, scaleY) { g.beginShape() g.vertex(927.9 * scaleX, 722.9 * scaleY) g.vertex(950.4 * scaleX, 677.8 * scaleY) g.vertex(899.3 * scaleX, 670.3 * scaleY) g.vertex(892.6 * scaleX, 654.5 * scaleY) g.vertex(920.4 * scaleX, 600.4 * scaleY) g.vertex(858.2 * scaleX, 598.5 * scaleY) g.vertex(850.5 * scaleX, 586.1 * scaleY) g.vertex(868.5 * scaleX, 559.1 * scaleY) g.vertex(834.6 * scaleX, 555.1 * scaleY) g.vertex(824.9 * scaleX, 537.3 * scaleY) g.vertex(771.6 * scaleX, 537.3 * scaleY) g.vertex(771.6 * scaleX, 276.1 * scaleY) g.vertex(961.4 * scaleX, 223.1 * scaleY) g.vertex(741.1 * scaleX, 218.9 * scaleY) g.vertex(724.4 * scaleX, 198.3 * scaleY) g.vertex(847 * scaleX, 114.7 * scaleY) g.vertex(651.7 * scaleX, 157.3 * scaleY) g.vertex(635.9 * scaleX, 148.8 * scaleY) g.vertex(651.9 * scaleX, 77.4 * scaleY) g.vertex(550.2 * scaleX, 137.9 * scaleY) g.vertex(507.9 * scaleX, 41.4 * scaleY) g.vertex(473.6 * scaleX, 137.9 * scaleY) g.vertex(373.9 * scaleX, 78.9 * scaleY) g.vertex(394 * scaleX, 151.3 * scaleY) g.vertex(379 * scaleX, 156.6 * scaleY) g.vertex(174.4 * scaleX, 120.3 * scaleY) g.vertex(298.7 * scaleX, 194.7 * scaleY) g.vertex(279.4 * scaleX, 209.2 * scaleY) g.vertex(40.2 * scaleX, 212.9 * scaleY) g.vertex(234.8 * scaleX, 273.4 * scaleY) g.vertex(234.8 * scaleX, 542.4 * scaleY) g.vertex(179.2 * scaleX, 542.4 * scaleY) g.vertex(164.6 * scaleX, 565.8 * scaleY) g.vertex(132.7 * scaleX, 568.2 * scaleY) g.vertex(145.4 * scaleX, 596.4 * scaleY) g.vertex(135 * scaleX, 612.9 * scaleY) g.vertex(75.7 * scaleX, 609.3 * scaleY) g.vertex(101.8 * scaleX, 665.8 * scaleY) g.vertex(90.9 * scaleX, 679.9 * scaleY) g.vertex(46.4 * scaleX, 678.8 * scaleY) g.vertex(60.5 * scaleX, 727.7 * scaleY) g.vertex(-1.4 * scaleX, 843.6 * scaleY) g.vertex(-.5 * scaleX, 1020 * scaleY) g.vertex(1002 * scaleX, 1020 * scaleY) g.vertex(1002 * scaleX, 842.6 * scaleY) g.endShape(CLOSE) } function drawSeekerArchetype(g, scaleX, scaleY) { g.beginShape() g.vertex(908 * scaleX, 821.3 * scaleY) g.vertex(824.9 * scaleX, 751.3 * scaleY) g.vertex(744.9 * scaleX, 453.1 * scaleY) g.vertex(871.4 * scaleX, 247.8 * scaleY) g.vertex(871.4 * scaleX, 247.9 * scaleY) g.vertex(896.7 * scaleX, 222.6 * scaleY) g.vertex(871.4 * scaleX, 197.3 * scaleY) g.vertex(846.2 * scaleX, 222.6 * scaleY) g.vertex(864.9 * scaleX, 241.4 * scaleY) g.vertex(826.5 * scaleX, 226 * scaleY) g.vertex(826.5 * scaleX, 201.2 * scaleY) g.vertex(808.5 * scaleX, 201.2 * scaleY) g.vertex(808.5 * scaleX, 218.8 * scaleY) g.vertex(788.5 * scaleX, 210.8 * scaleY) g.vertex(788.5 * scaleX, 185.4 * scaleY) g.vertex(770.5 * scaleX, 185.4 * scaleY) g.vertex(770.5 * scaleX, 203.6 * scaleY) g.vertex(750.5 * scaleX, 195.6 * scaleY) g.vertex(750.5 * scaleX, 167.2 * scaleY) g.vertex(732.5 * scaleX, 167.2 * scaleY) g.vertex(732.5 * scaleX, 188.5 * scaleY) g.vertex(708.5 * scaleX, 178.9 * scaleY) g.vertex(708.5 * scaleX, 148.2 * scaleY) g.vertex(690.5 * scaleX, 148.2 * scaleY) g.vertex(690.5 * scaleX, 171.7 * scaleY) g.vertex(667.3 * scaleX, 162.4 * scaleY) g.vertex(667.3 * scaleX, 132.4 * scaleY) g.vertex(649.3 * scaleX, 132.4 * scaleY) g.vertex(649.3 * scaleX, 155.3 * scaleY) g.vertex(600.6 * scaleX, 135.8 * scaleY) g.vertex(620.1 * scaleX, 116.2 * scaleY) g.vertex(594.8 * scaleX, 91 * scaleY) g.vertex(569.6 * scaleX, 116.2 * scaleY) g.vertex(587.9 * scaleX, 134.6 * scaleY) g.vertex(431.5 * scaleX, 134.6 * scaleY) g.vertex(449.2 * scaleX, 116.9 * scaleY) g.vertex(423.9 * scaleX, 91.7 * scaleY) g.vertex(398.6 * scaleX, 116.9 * scaleY) g.vertex(418.8 * scaleX, 137.2 * scaleY) g.vertex(373 * scaleX, 156.3 * scaleY) g.vertex(373 * scaleX, 137.4 * scaleY) g.vertex(355 * scaleX, 137.4 * scaleY) g.vertex(355 * scaleX, 163.8 * scaleY) g.vertex(330.5 * scaleX, 174 * scaleY) g.vertex(330.5 * scaleX, 156.9 * scaleY) g.vertex(312.5 * scaleX, 156.9 * scaleY) g.vertex(312.5 * scaleX, 181.5 * scaleY) g.vertex(287.9 * scaleX, 191.7 * scaleY) g.vertex(287.9 * scaleX, 171.5 * scaleY) g.vertex(269.9 * scaleX, 171.5 * scaleY) g.vertex(269.9 * scaleX, 199.2 * scaleY) g.vertex(245.3 * scaleX, 209.5 * scaleY) g.vertex(245.3 * scaleX, 186.1 * scaleY) g.vertex(227.3 * scaleX, 186.1 * scaleY) g.vertex(227.3 * scaleX, 217 * scaleY) g.vertex(202.8 * scaleX, 227.2 * scaleY) g.vertex(202.8 * scaleX, 210.7 * scaleY) g.vertex(184.8 * scaleX, 210.7 * scaleY) g.vertex(184.8 * scaleX, 234.7 * scaleY) g.vertex(132.2 * scaleX, 256.6 * scaleY) g.vertex(153.3 * scaleX, 235.6 * scaleY) g.vertex(128 * scaleX, 210.3 * scaleY) g.vertex(102.7 * scaleX, 235.6 * scaleY) g.vertex(126.3 * scaleX, 259.1 * scaleY) g.vertex(126.2 * scaleX, 259.1 * scaleY) g.vertex(252.4 * scaleX, 450.3 * scaleY) g.vertex(155.9 * scaleX, 751.3 * scaleY) g.vertex(84.3 * scaleX, 821.3 * scaleY) g.vertex(-2.7 * scaleX, 921.3 * scaleY) g.vertex(-2.7 * scaleX, 1002.6 * scaleY) g.vertex(1002.8 * scaleX, 1002.6 * scaleY) g.vertex(1002.8 * scaleX, 921.3 * scaleY) g.endShape(CLOSE) } function drawArbiterArchetype(g, scaleX, scaleY) { g.beginShape() g.vertex(914.1 * scaleX, 840.2 * scaleY) g.vertex(982 * scaleX, 799.5 * scaleY) g.vertex(982 * scaleX, 731.1 * scaleY) g.vertex(743.6 * scaleX, 731.1 * scaleY) g.vertex(868.6 * scaleX, 646.4 * scaleY) g.vertex(868.6 * scaleX, 582.2 * scaleY) g.vertex(785.3 * scaleX, 582.2 * scaleY) g.vertex(918.6 * scaleX, 491 * scaleY) g.vertex(918.6 * scaleX, 456.8 * scaleY) g.vertex(771.3 * scaleX, 456.8 * scaleY) g.vertex(858.6 * scaleX, 391.1 * scaleY) g.vertex(858.6 * scaleX, 356.9 * scaleY) g.vertex(760.7 * scaleX, 326.9 * scaleY) g.vertex(827.1 * scaleX, 222.3 * scaleY) g.vertex(827.1 * scaleX, 188.1 * scaleY) g.vertex(725.2 * scaleX, 188.1 * scaleY) g.vertex(752.3 * scaleX, 124.9 * scaleY) g.vertex(752.3 * scaleX, 76.1 * scaleY) g.vertex(752 * scaleX, 76.1 * scaleY) g.vertex(752 * scaleX, 56.2 * scaleY) g.vertex(745.4 * scaleX, 56.2 * scaleY) g.vertex(738.7 * scaleX, 62.5 * scaleY) g.vertex(734.1 * scaleX, 68.1 * scaleY) g.vertex(734.1 * scaleX, 69.7 * scaleY) g.vertex(734.1 * scaleX, 71.7 * scaleY) g.vertex(734.1 * scaleX, 73.8 * scaleY) g.vertex(734.1 * scaleX, 76.1 * scaleY) g.vertex(683.5 * scaleX, 76.1 * scaleY) g.vertex(683.5 * scaleX, 56.2 * scaleY) g.vertex(676.9 * scaleX, 56.2 * scaleY) g.vertex(670.3 * scaleX, 62.5 * scaleY) g.vertex(665.6 * scaleX, 68.1 * scaleY) g.vertex(665.6 * scaleX, 69.7 * scaleY) g.vertex(665.6 * scaleX, 71.7 * scaleY) g.vertex(665.6 * scaleX, 73.8 * scaleY) g.vertex(665.6 * scaleX, 76.1 * scaleY) g.vertex(615 * scaleX, 76.1 * scaleY) g.vertex(615 * scaleX, 56.2 * scaleY) g.vertex(608.4 * scaleX, 56.2 * scaleY) g.vertex(601.8 * scaleX, 62.5 * scaleY) g.vertex(597.1 * scaleX, 68.1 * scaleY) g.vertex(597.1 * scaleX, 69.7 * scaleY) g.vertex(597.1 * scaleX, 71.7 * scaleY) g.vertex(597.1 * scaleX, 73.8 * scaleY) g.vertex(597.1 * scaleX, 76.1 * scaleY) g.vertex(546.5 * scaleX, 76.1 * scaleY) g.vertex(546.5 * scaleX, 56.2 * scaleY) g.vertex(539.9 * scaleX, 56.2 * scaleY) g.vertex(533.3 * scaleX, 62.5 * scaleY) g.vertex(528.6 * scaleX, 68.1 * scaleY) g.vertex(528.6 * scaleX, 69.7 * scaleY) g.vertex(528.6 * scaleX, 71.7 * scaleY) g.vertex(528.6 * scaleX, 73.8 * scaleY) g.vertex(528.6 * scaleX, 76.1 * scaleY) g.vertex(478 * scaleX, 76.1 * scaleY) g.vertex(478 * scaleX, 56.2 * scaleY) g.vertex(471.4 * scaleX, 56.2 * scaleY) g.vertex(464.8 * scaleX, 62.5 * scaleY) g.vertex(460.1 * scaleX, 68.1 * scaleY) g.vertex(460.1 * scaleX, 69.7 * scaleY) g.vertex(460.1 * scaleX, 71.7 * scaleY) g.vertex(460.1 * scaleX, 73.8 * scaleY) g.vertex(460.1 * scaleX, 76.1 * scaleY) g.vertex(409.5 * scaleX, 76.1 * scaleY) g.vertex(409.5 * scaleX, 56.2 * scaleY) g.vertex(402.9 * scaleX, 56.2 * scaleY) g.vertex(396.3 * scaleX, 62.5 * scaleY) g.vertex(391.6 * scaleX, 68.1 * scaleY) g.vertex(391.6 * scaleX, 69.7 * scaleY) g.vertex(391.6 * scaleX, 71.7 * scaleY) g.vertex(391.6 * scaleX, 73.8 * scaleY) g.vertex(391.6 * scaleX, 76.1 * scaleY) g.vertex(341 * scaleX, 76.1 * scaleY) g.vertex(341 * scaleX, 56.2 * scaleY) g.vertex(334.4 * scaleX, 56.2 * scaleY) g.vertex(327.8 * scaleX, 62.5 * scaleY) g.vertex(323.1 * scaleX, 68.1 * scaleY) g.vertex(323.1 * scaleX, 69.7 * scaleY) g.vertex(323.1 * scaleX, 71.7 * scaleY) g.vertex(323.1 * scaleX, 73.8 * scaleY) g.vertex(323.1 * scaleX, 76.1 * scaleY) g.vertex(272.5 * scaleX, 76.1 * scaleY) g.vertex(272.5 * scaleX, 56.2 * scaleY) g.vertex(265.9 * scaleX, 56.2 * scaleY) g.vertex(259.3 * scaleX, 62.5 * scaleY) g.vertex(254.6 * scaleX, 68.1 * scaleY) g.vertex(254.6 * scaleX, 69.7 * scaleY) g.vertex(254.6 * scaleX, 71.7 * scaleY) g.vertex(254.6 * scaleX, 73.8 * scaleY) g.vertex(254.6 * scaleX, 84.3 * scaleY) g.vertex(254.6 * scaleX, 86.9 * scaleY) g.vertex(254.6 * scaleX, 89.1 * scaleY) g.vertex(254.6 * scaleX, 92.5 * scaleY) g.vertex(254.8 * scaleX, 92.7 * scaleY) g.vertex(254.8 * scaleX, 124.9 * scaleY) g.vertex(288 * scaleX, 188.1 * scaleY) g.vertex(174 * scaleX, 188.1 * scaleY) g.vertex(174 * scaleX, 222.3 * scaleY) g.vertex(247.1 * scaleX, 326.9 * scaleY) g.vertex(145.5 * scaleX, 356.9 * scaleY) g.vertex(145.5 * scaleX, 391.1 * scaleY) g.vertex(237.9 * scaleX, 456.8 * scaleY) g.vertex(105.5 * scaleX, 456.8 * scaleY) g.vertex(105.5 * scaleX, 491 * scaleY) g.vertex(223.5 * scaleX, 582.2 * scaleY) g.vertex(135.5 * scaleX, 582.2 * scaleY) g.vertex(135.5 * scaleX, 646.4 * scaleY) g.vertex(269.2 * scaleX, 731.1 * scaleY) g.vertex(26.9 * scaleX, 731.1 * scaleY) g.vertex(26.9 * scaleX, 799.5 * scaleY) g.vertex(94.3 * scaleX, 840.2 * scaleY) g.vertex(-1 * scaleX, 840.2 * scaleY) g.vertex(-1 * scaleX, 1020 * scaleY) g.vertex(1002 * scaleX, 1020 * scaleY) g.vertex(1002 * scaleX, 840.2 * scaleY) g.endShape(CLOSE) } function drawNomadArchetype(g, scaleX, scaleY) { g.beginShape() g.vertex(936.4 * scaleX, 825.3 * scaleY) g.vertex(885 * scaleX, 839.9 * scaleY) g.vertex(748.1 * scaleX, 666 * scaleY) g.vertex(853.7 * scaleX, 728.5 * scaleY) g.vertex(918.3 * scaleX, 621.6 * scaleY) g.vertex(764.8 * scaleX, 552.2 * scaleY) g.vertex(764.2 * scaleX, 531.1 * scaleY) g.vertex(851.4 * scaleX, 549.2 * scaleY) g.vertex(878.6 * scaleX, 494 * scaleY) g.vertex(764.2 * scaleX, 434.5 * scaleY) g.vertex(764.4 * scaleX, 339.5 * scaleY) g.vertex(869.1 * scaleX, 372.6 * scaleY) g.vertex(928.9 * scaleX, 262.8 * scaleY) g.vertex(563.9 * scaleX, 101.2 * scaleY) g.vertex(444.7 * scaleX, 101.2 * scaleY) g.vertex(69.1 * scaleX, 262.8 * scaleY) g.vertex(151.9 * scaleX, 369.5 * scaleY) g.vertex(241.6 * scaleX, 330.7 * scaleY) g.vertex(240.1 * scaleX, 442.1 * scaleY) g.vertex(129.1 * scaleX, 498.7 * scaleY) g.vertex(153.8 * scaleX, 554.8 * scaleY) g.vertex(240.3 * scaleX, 536.9 * scaleY) g.vertex(239.4 * scaleX, 561.5 * scaleY) g.vertex(97.9 * scaleX, 611.4 * scaleY) g.vertex(151.3 * scaleX, 729.4 * scaleY) g.vertex(261.1 * scaleX, 666.4 * scaleY) g.vertex(146.8 * scaleX, 846.6 * scaleY) g.vertex(99.4 * scaleX, 833.8 * scaleY) g.vertex(1.5 * scaleX, 856.5 * scaleY) g.vertex(-1.4 * scaleX, 1020 * scaleY) g.vertex(1002 * scaleX, 1020 * scaleY) g.vertex(1002 * scaleX, 845.1 * scaleY) g.endShape(CLOSE) } function drawGuardArchetype(g, scaleX, scaleY) { g.beginShape() g.vertex(982.7 * scaleX, 850.9 * scaleY) g.vertex(982.7 * scaleX, 822.2 * scaleY) g.vertex(949.8 * scaleX, 822.2 * scaleY) g.vertex(949.8 * scaleX, 840.5 * scaleY) g.vertex(939.8 * scaleX, 837.3 * scaleY) g.vertex(939.8 * scaleX, 780.2 * scaleY) g.vertex(906.9 * scaleX, 780.2 * scaleY) g.vertex(906.9 * scaleX, 826.9 * scaleY) g.vertex(892.3 * scaleX, 822.3 * scaleY) g.vertex(892.3 * scaleX, 750.2 * scaleY) g.vertex(859.4 * scaleX, 750.2 * scaleY) g.vertex(859.4 * scaleX, 811.9 * scaleY) g.vertex(843 * scaleX, 806.6 * scaleY) g.vertex(843 * scaleX, 723.2 * scaleY) g.vertex(810.1 * scaleX, 723.2 * scaleY) g.vertex(810.1 * scaleX, 796.2 * scaleY) g.vertex(793.6 * scaleX, 791 * scaleY) g.vertex(793.6 * scaleX, 693.2 * scaleY) g.vertex(760.7 * scaleX, 693.2 * scaleY) g.vertex(760.7 * scaleX, 780.6 * scaleY) g.vertex(738.4 * scaleX, 773.5 * scaleY) g.vertex(738.4 * scaleX, 354.6 * scaleY) g.vertex(781.1 * scaleX, 354.6 * scaleY) g.vertex(781.1 * scaleX, 321.7 * scaleY) g.vertex(738.4 * scaleX, 321.7 * scaleY) g.vertex(738.4 * scaleX, 303.6 * scaleY) g.vertex(813.1 * scaleX, 303.6 * scaleY) g.vertex(813.1 * scaleX, 270.7 * scaleY) g.vertex(738.4 * scaleX, 270.7 * scaleY) g.vertex(738.4 * scaleX, 254.3 * scaleY) g.vertex(843.1 * scaleX, 254.3 * scaleY) g.vertex(843.1 * scaleX, 221.4 * scaleY) g.vertex(738.4 * scaleX, 221.4 * scaleY) g.vertex(738.4 * scaleX, 204.9 * scaleY) g.vertex(871.1 * scaleX, 204.9 * scaleY) g.vertex(871.1 * scaleX, 172 * scaleY) g.vertex(738.4 * scaleX, 172 * scaleY) g.vertex(738.4 * scaleX, 155.6 * scaleY) g.vertex(901.1 * scaleX, 155.6 * scaleY) g.vertex(901.1 * scaleX, 122.7 * scaleY) g.vertex(95.5 * scaleX, 122.7 * scaleY) g.vertex(95.5 * scaleX, 155.6 * scaleY) g.vertex(258.3 * scaleX, 155.6 * scaleY) g.vertex(258.3 * scaleX, 172 * scaleY) g.vertex(125.5 * scaleX, 172 * scaleY) g.vertex(125.5 * scaleX, 204.9 * scaleY) g.vertex(258.3 * scaleX, 204.9 * scaleY) g.vertex(258.3 * scaleX, 221.4 * scaleY) g.vertex(153.5 * scaleX, 221.4 * scaleY) g.vertex(153.5 * scaleX, 254.3 * scaleY) g.vertex(258.3 * scaleX, 254.3 * scaleY) g.vertex(258.3 * scaleX, 270.7 * scaleY) g.vertex(183.5 * scaleX, 270.7 * scaleY) g.vertex(183.5 * scaleX, 303.6 * scaleY) g.vertex(258.3 * scaleX, 303.6 * scaleY) g.vertex(258.3 * scaleX, 321.7 * scaleY) g.vertex(215.5 * scaleX, 321.7 * scaleY) g.vertex(215.5 * scaleX, 354.6 * scaleY) g.vertex(258.3 * scaleX, 354.6 * scaleY) g.vertex(258.3 * scaleX, 766 * scaleY) g.vertex(241 * scaleX, 771.7 * scaleY) g.vertex(241 * scaleX, 690.3 * scaleY) g.vertex(208.1 * scaleX, 690.3 * scaleY) g.vertex(208.1 * scaleX, 782.6 * scaleY) g.vertex(191.6 * scaleX, 788 * scaleY) g.vertex(191.6 * scaleX, 720.3 * scaleY) g.vertex(158.7 * scaleX, 720.3 * scaleY) g.vertex(158.7 * scaleX, 798.8 * scaleY) g.vertex(142.3 * scaleX, 804.2 * scaleY) g.vertex(142.3 * scaleX, 749.3 * scaleY) g.vertex(109.4 * scaleX, 749.3 * scaleY) g.vertex(109.4 * scaleX, 815.1 * scaleY) g.vertex(92.9 * scaleX, 820.5 * scaleY) g.vertex(92.9 * scaleX, 779.3 * scaleY) g.vertex(60 * scaleX, 779.3 * scaleY) g.vertex(60 * scaleX, 831.4 * scaleY) g.vertex(47 * scaleX, 835.7 * scaleY) g.vertex(47 * scaleX, 815.2 * scaleY) g.vertex(14.1 * scaleX, 815.2 * scaleY) g.vertex(14.1 * scaleX, 846.5 * scaleY) g.vertex(-1.9 * scaleX, 852.1 * scaleY) g.vertex(-1.7 * scaleX, 1020 * scaleY) g.vertex(1002 * scaleX, 1020 * scaleY) g.vertex(1002 * scaleX, 856.4 * scaleY) g.endShape(CLOSE) } function drawBladePart1(g, scaleX, scaleY) { g.beginShape() g.vertex(666.3 * scaleX, 784.6 * scaleY) g.vertex(756.6 * scaleX, 733.6 * scaleY) g.vertex(741.7 * scaleX, 397.7 * scaleY) g.vertex(772.7 * scaleX, 351.3 * scaleY) g.vertex(736.4 * scaleX, 324.8 * scaleY) g.vertex(722.6 * scaleX, 136.6 * scaleY) g.vertex(615.3 * scaleX, 184.5 * scaleY) g.vertex(492.1 * scaleX, 132 * scaleY) g.vertex(353.6 * scaleX, 179.3 * scaleY) g.vertex(269.7 * scaleX, 127.6 * scaleY) g.vertex(257.9 * scaleX, 315 * scaleY) g.vertex(218.5 * scaleX, 342.3 * scaleY) g.vertex(254.2 * scaleX, 387.5 * scaleY) g.vertex(234.3 * scaleX, 721.5 * scaleY) g.vertex(327.2 * scaleX, 780.8 * scaleY) g.vertex(327.1 * scaleX, 873.6 * scaleY) g.vertex(666.7 * scaleX, 875.5 * scaleY) g.endShape(CLOSE) } function drawBladePart2(g, scaleX, scaleY) { g.beginShape() g.vertex(912 * scaleX, 847.9 * scaleY) g.vertex(891.9 * scaleX, 666.3 * scaleY) g.vertex(782.8 * scaleX, 776.3 * scaleY) g.vertex(212 * scaleX, 776.3 * scaleY) g.vertex(102.9 * scaleX, 666.3 * scaleY) g.vertex(82.8 * scaleX, 847.9 * scaleY) g.vertex(-.3 * scaleX, 877.9 * scaleY) g.vertex(-.3 * scaleX, 1020 * scaleY) g.vertex(1001.1 * scaleX, 1020 * scaleY) g.vertex(1001.1 * scaleX, 877.9 * scaleY) g.endShape(CLOSE) } function drawHavocPart1(g, scaleX, scaleY) { g.beginShape() g.vertex(776.4 * scaleX, 394.9 * scaleY) g.vertex(776.4 * scaleX, 328.4 * scaleY) g.vertex(758.9 * scaleX, 307 * scaleY) g.vertex(919.4 * scaleX, 72.7 * scaleY) g.vertex(655.5 * scaleX, 180.1 * scaleY) g.vertex(640.8 * scaleX, 162.1 * scaleY) g.vertex(584.8 * scaleX, 186 * scaleY) g.vertex(527.6 * scaleX, 162.1 * scaleY) g.vertex(497.7 * scaleX, 96.4 * scaleY) g.vertex(469.3 * scaleX, 162.1 * scaleY) g.vertex(412 * scaleX, 184.8 * scaleY) g.vertex(361.7 * scaleX, 162.1 * scaleY) g.vertex(346.9 * scaleX, 180.3 * scaleY) g.vertex(77.2 * scaleX, 75.2 * scaleY) g.vertex(241.9 * scaleX, 309.4 * scaleY) g.vertex(226.8 * scaleX, 327.9 * scaleY) g.vertex(226.8 * scaleX, 387.7 * scaleY) g.vertex(181.9 * scaleX, 432.6 * scaleY) g.vertex(226.8 * scaleX, 507.4 * scaleY) g.vertex(181.9 * scaleX, 552.3 * scaleY) g.bezierVertex( 181.9 * scaleX, 552.3 * scaleY, 228.1 * scaleX, 618 * scaleY, 229.3 * scaleX, 626.4 * scaleY ) g.vertex(228.4 * scaleX, 625.5 * scaleY) g.vertex(182 * scaleX, 671.9 * scaleY) g.vertex(274 * scaleX, 743.5 * scaleY) g.vertex(274 * scaleX, 743.2 * scaleY) g.bezierVertex( 323.4 * scaleX, 816.2 * scaleY, 407 * scaleX, 864.1 * scaleY, 501.8 * scaleX, 864.1 * scaleY ) g.bezierVertex( 590.2 * scaleX, 864.1 * scaleY, 668.8 * scaleX, 822.4 * scaleY, 719.1 * scaleX, 757.6 * scaleY ) g.vertex(819.5 * scaleX, 677 * scaleY) g.vertex(773.5 * scaleX, 631 * scaleY) g.bezierVertex( 775 * scaleX, 621 * scaleY, 819.5 * scaleX, 557.4 * scaleY, 819.5 * scaleX, 557.4 * scaleY ) g.vertex(776.7 * scaleX, 514.6 * scaleY) g.vertex(819.5 * scaleX, 437.8 * scaleY) g.vertex(776.7 * scaleX, 395 * scaleY) g.endShape(CLOSE) } function drawHavocPart2(g, scaleX, scaleY) { g.beginShape() g.vertex(985.3 * scaleX, 881.1 * scaleY) g.vertex(985.3 * scaleX, 880.8 * scaleY) g.bezierVertex( 985.3 * scaleX, 880.8 * scaleY, 921 * scaleX, 844.1 * scaleY, 895.5 * scaleX, 808.7 * scaleY ) g.vertex(952.6 * scaleX, 725.3 * scaleY) g.vertex(811 * scaleX, 782.9 * scaleY) g.vertex(809.9 * scaleX, 782.9 * scaleY) g.vertex(845.2 * scaleX, 731.3 * scaleY) g.vertex(718.5 * scaleX, 782.9 * scaleY) g.vertex(291.8 * scaleX, 782.9 * scaleY) g.vertex(167.3 * scaleX, 734.4 * scaleY) g.vertex(191.6 * scaleX, 769 * scaleY) g.vertex(72 * scaleX, 722.4 * scaleY) g.vertex(128.8 * scaleX, 803.2 * scaleY) g.bezierVertex( 106.4 * scaleX, 840.3 * scaleY, 35.5 * scaleX, 880.9 * scaleY, 35.5 * scaleX, 880.9 * scaleY ) g.bezierVertex( 35.5 * scaleX, 880.9 * scaleY, 0.7 * scaleX, 895.1 * scaleY, 0.7 * scaleX, 895.1 * scaleY ) g.vertex(-1.8 * scaleX, 1020 * scaleY) g.vertex(999.5 * scaleX, 1020 * scaleY) g.vertex(1002 * scaleX, 890.1 * scaleY) g.vertex(985.2 * scaleX, 881.2 * scaleY) g.endShape(CLOSE) } function drawFeroxArchetype(g, scaleX, scaleY) { g.beginShape() g.vertex(820.4 * scaleX, 703.4 * scaleY) g.vertex(793.4 * scaleX, 570.9 * scaleY) g.vertex(809.6 * scaleX, 510.6 * scaleY) g.vertex(878.1 * scaleX, 669.3 * scaleY) g.vertex(883.3 * scaleX, 343.3 * scaleY) g.vertex(790 * scaleX, 316.2 * scaleY) g.vertex(922.5 * scaleX, 297.5 * scaleY) g.vertex(870.4 * scaleX, 234.3 * scaleY) g.vertex(811.9 * scaleX, 219.4 * scaleY) g.vertex(756.8 * scaleX, 112 * scaleY) g.vertex(653.8 * scaleX, 160 * scaleY) g.vertex(620.2 * scaleX, 78.7 * scaleY) g.vertex(543.4 * scaleX, 94.3 * scaleY) g.vertex(495.4 * scaleX, 16.2 * scaleY) g.vertex(460.6 * scaleX, 94.9 * scaleY) g.vertex(389.4 * scaleX, 78.7 * scaleY) g.vertex(363.4 * scaleX, 154.1 * scaleY) g.vertex(260.4 * scaleX, 109.5 * scaleY) g.vertex(206.8 * scaleX, 216 * scaleY) g.vertex(127 * scaleX, 234.3 * scaleY) g.vertex(70.9 * scaleX, 297.5 * scaleY) g.vertex(205.7 * scaleX, 316.2 * scaleY) g.vertex(121.4 * scaleX, 339.3 * scaleY) g.vertex(130.5 * scaleX, 675.3 * scaleY) g.vertex(186.1 * scaleX, 507.6 * scaleY) g.vertex(205.2 * scaleX, 570.9 * scaleY) g.vertex(177 * scaleX, 703.4 * scaleY) g.vertex(0.1 * scaleX, 782.6 * scaleY) g.vertex(-0.2 * scaleX, 1020 * scaleY) g.vertex(1000.2 * scaleX, 1020 * scaleY) g.vertex(1000.2 * scaleX, 781.3 * scaleY) g.endShape(CLOSE) } function drawVantaPart1(g, scaleX, scaleY) { g.beginShape() g.vertex(719.2 * scaleX, 202 * scaleY) g.vertex(719.3 * scaleX, 201.9 * scaleY) g.vertex(719.2 * scaleX, 201.9 * scaleY) g.vertex(717.4 * scaleX, 181 * scaleY) g.vertex(691 * scaleX, 173.7 * scaleY) g.vertex(579.5 * scaleX, 62.1 * scaleY) g.vertex(579.5 * scaleX, 110.1 * scaleY) g.vertex(624.9 * scaleX, 155.6 * scaleY) g.vertex(528.3 * scaleX, 129.1 * scaleY) g.vertex(566.7 * scaleX, 90.6 * scaleY) g.vertex(495 * scaleX, 18.9 * scaleY) g.vertex(423.2 * scaleX, 90.6 * scaleY) g.vertex(459.7 * scaleX, 127.2 * scaleY) g.vertex(371.9 * scaleX, 151.4 * scaleY) g.vertex(413.9 * scaleX, 109.6 * scaleY) g.vertex(413.9 * scaleX, 61.6 * scaleY) g.vertex(305.3 * scaleX, 169.8 * scaleY) g.vertex(265.1 * scaleX, 181 * scaleY) g.vertex(227 * scaleX, 830.8 * scaleY) g.vertex(773 * scaleX, 840.5 * scaleY) g.endShape(CLOSE) } function drawVantaPart2(g, scaleX, scaleY) { g.beginShape() g.vertex(1002 * scaleX, 880 * scaleY) g.vertex(1002 * scaleX, 878.9 * scaleY) g.vertex(1002 * scaleX, 878.3 * scaleY) g.vertex(1002 * scaleX, 878 * scaleY) g.vertex(928.5 * scaleX, 808.2 * scaleY) g.vertex(905.8 * scaleX, 831 * scaleY) g.vertex(866.8 * scaleX, 792 * scaleY) g.vertex(851.6 * scaleX, 807.2 * scaleY) g.vertex(822.2 * scaleX, 777.8 * scaleY) g.vertex(808.5 * scaleX, 791.6 * scaleY) g.vertex(781.2 * scaleX, 776.5 * scaleY) g.vertex(211.1 * scaleX, 776.5 * scaleY) g.vertex(186.6 * scaleX, 790.1 * scaleY) g.vertex(172.4 * scaleX, 775.8 * scaleY) g.vertex(142 * scaleX, 806.2 * scaleY) g.vertex(123.8 * scaleX, 788 * scaleY) g.vertex(89.9 * scaleX, 821.9 * scaleY) g.vertex(72.2 * scaleX, 804.2 * scaleY) g.vertex(0.4 * scaleX, 875.9 * scaleY) g.vertex(1.8 * scaleX, 877.3 * scaleY) g.vertex(-0.9 * scaleX, 878.3 * scaleY) g.vertex(-0.9 * scaleX, 1020 * scaleY) g.vertex(1002 * scaleX, 1020 * scaleY) g.vertex(1002 * scaleX, 881 * scaleY) g.endShape(CLOSE) } function drawPrimeArchetype(g, scaleX, scaleY) { g.beginShape() let x = -2.3 * scaleX let y = 881.6 * scaleY g.vertex(x, y) let cp1x = x + 34.5 * scaleX let cp1y = y - 70 * scaleY let cp2x = x + 80.7 * scaleX
g.rect(0, 0, r.width, r.height) g.pop() } } isDone() { if (this.rectangles.length === 0) return true return this.fadedCount >= this.rectangles.length } averageOpacity() { if (this.startedCount === 0) return 100 let sum = 0 for (let r of this.rectangles) { if (r.started) sum += r.opacity } return sum / this.startedCount } } class Shockwave extends FillAnimation { constructor(x, y, palette, traits) { super(x, y, palette, traits) this.palette = palette this.rectangles = [] this._initRects(x, y, palette) } _initRects(x, y, palette) { const numRects = floor(random(3, 30)) for (let i = 0; i < numRects; i++) { const finalWidth = random(150, 1100) * _designScale const finalHeight = random(150, 1100) * _designScale const duration = random(80, 250) / FILL_ANIMATION_SPEED_MULT const startDelay = floor(random(0, 80)) const originalSW = random(8, 150) * _designScale this.rectangles.push({ x: x, y: y, width: 0, height: 0, targetWidth: finalWidth, targetHeight: finalHeight, growthRateWidth: finalWidth / duration, growthRateHeight: finalHeight / duration, color: random(palette), sw: originalSW, originalSW: originalSW, delay: startDelay, started: false, done: false, blendMode: this.getBlend(random() < 0.2 ? DIFFERENCE : BLEND), }) } } reset(x, y, palette, traits) { super.reset(x, y, palette, traits) this.palette = palette this.rectangles = [] this._initRects(x, y, palette) } update() { super.update() for (let r of this.rectangles) { if (this.age < r.delay) continue if (!r.started) r.started = true r.width += r.growthRateWidth r.height += r.growthRateHeight const sizeProgress = max(r.width / r.targetWidth, r.height / r.targetHeight) if (sizeProgress >= 0.6) { const ease = pow((sizeProgress - 0.6) / 0.4, 2) r.sw = max(0, r.originalSW * (1 - ease)) } if (!r.done && r.width >= r.targetWidth && r.height >= r.targetHeight && r.sw <= 0) { r.done = true } } } display(g) { for (let r of this.rectangles) { if (!r.started || r.sw <= 0) continue g.push() g.blendMode(r.blendMode) g.stroke(r.color) g.strokeWeight(r.sw) g.noFill() g.rectMode(CENTER) const adjW = max(0, r.width - r.sw) const adjH = max(0, r.height - r.sw) g.rect(r.x, r.y, adjW, adjH) g.pop() } } isDone() { if (this.rectangles.length === 0) return true return this.rectangles.every(r => r.done) } averageOpacity() { if (this.rectangles.length === 0) return 0 let visible = 0 for (let r of this.rectangles) { if (r.started && r.sw > 0) visible++ } return (visible / this.rectangles.length) * 100 } } class ExpandingCircles extends FillAnimation { constructor(x, y, palette, traits) { super(x, y, palette, traits) this.circles = [] const count = floor(random(1, 7)) for (let i = 0; i < count; i++) { this.circles.push({ radius: random(10, 30) * _designScale, maxRadius: random(500, 900) * _designScale, speed: random(0.375, 3.0) * FILL_ANIMATION_SPEED_MULT, strokeWeight: random(2, 6) * _designScale, color: random(palette), opacity: 100, blendMode: this.getBlend(i % 2 === 0 ? DIFFERENCE : ADD), maxLife: floor(random(60, 350)), age: 0 }) } } reset(x, y, palette, traits) { super.reset(x, y, palette, traits) this.circles = [] const count = floor(random(1, 3)) for (let i = 0; i < count; i++) { this.circles.push({ radius: random(10, 20) * _designScale, maxRadius: random(500, 900) * _designScale, speed: random(0.375, 3.0) * FILL_ANIMATION_SPEED_MULT, strokeWeight: random(2, 6) * _designScale, color: random(palette), opacity: 100, blendMode: this.getBlend(i % 2 === 0 ? DIFFERENCE : ADD), maxLife: floor(random(60, 350)), age: 0 }) } } update() { super.update() for (let c of this.circles) { c.age++ const selfFading = c.age > c.maxLife c.radius += c.speed * (selfFading ? 2 : 1) const fadeProgress = c.radius / c.maxRadius const t = constrain(map(fadeProgress, 0.2, 1.1, 0, 1), 0, 1) c.opacity = max(0, 100 * (1 - t)) } } display(g) { g.push() g.noFill() for (let c of this.circles) { if (c.opacity <= 0) continue g.push() g.blendMode(c.blendMode) const col = g.color(c.color) col.setAlpha(c.opacity) g.stroke(col) g.strokeWeight(c.strokeWeight) g.noFill() g.circle(this.x, this.y, c.radius * 2) g.pop() } g.pop() } isDone() { if (this.circles.length === 0) return true return this.circles.every(c => c.opacity <= 0) } averageOpacity() { if (this.circles.length === 0) return 0 return this.circles.reduce((sum, c) => sum + c.opacity, 0) / this.circles.length } } class ExpandingSquares extends FillAnimation { constructor(x, y, palette, traits) { super(x, y, palette, traits) this.fadeInDuration = 60 this.squares = [] this.persistMode = (traits.expandingPersist === 'persist') this.persistentLayer = null this.rotationMode = traits.expandingRotation || 'rotating' this.fixedRotation = this.rotationMode === 'static' ? random([0, QUARTER_PI, HALF_PI, QUARTER_PI * 3]) : 0 const count = floor(random(1, 7)) for (let i = 0; i < count; i++) { this.squares.push({ size: random(10, 30) * _designScale, maxSize: random(500, 900) * _designScale, speed: random(0.375, 3.0) * FILL_ANIMATION_SPEED_MULT, strokeWeight: random(2, 6) * _designScale, width: random(0.5, 1.5), height: random(0.5, 1.5), rotation: this.rotationMode === 'rotating' ? random(TWO_PI) : this.fixedRotation, rotSpeed: this.rotationMode === 'rotating' ? random(-0.02, 0.02) : 0, color: random(palette), opacity: 100, blendMode: this.getBlend(i % 2 === 0 ? DIFFERENCE : ADD), maxLife: floor(random(60, 350)), age: 0 }) } } reset(x, y, palette, traits) { super.reset(x, y, palette, traits) this.squares = [] this.persistMode = (traits.expandingPersist === 'persist') this.rotationMode = traits.expandingRotation || 'rotating' this._fadeoutStart = undefined if (!this.fixedRotation && this.rotationMode === 'static') { this.fixedRotation = random([0, QUARTER_PI, HALF_PI, QUARTER_PI * 3]) } const count = floor(random(1, 3)) if (this.persistMode && !this.persistentLayer) { this.persistentLayer = createGraphics(width, height) this.persistentLayer.clear() } else if (!this.persistMode && this.persistentLayer) { this.persistentLayer.remove() this.persistentLayer = null } else if (this.persistMode && this.persistentLayer) { this.persistentLayer.clear() } for (let i = 0; i < count; i++) { this.squares.push({ size: random(10, 20), maxSize: random(500, 900) * _designScale, speed: random(0.375, 3.0) * FILL_ANIMATION_SPEED_MULT, strokeWeight: random(2, 6) * _designScale, width: random(0.5, 1.5), height: random(0.5, 1.5), rotation: this.rotationMode === 'rotating' ? random(TWO_PI) : this.fixedRotation, rotSpeed: this.rotationMode === 'rotating' ? random(-0.02, 0.02) : 0, color: random(palette), opacity: 100, blendMode: this.getBlend(i % 2 === 0 ? DIFFERENCE : ADD), maxLife: floor(random(60, 350)), age: 0 }) } } update() { super.update() for (let s of this.squares) { s.age++ const selfFading = s.age > s.maxLife s.size += s.speed * (selfFading ? 2 : 1) const fadeProgress = s.size / s.maxSize const t = constrain(map(fadeProgress, 0.2, 1.1, 0, 1), 0, 1) s.opacity = max(0, 100 * (1 - t)) if (this.persistMode && s.size >= s.maxSize) { s.opacity = max(0, s.opacity - (selfFading ? 0.8 : 0.5)) } } } display(g) { g.push() const visualOpacity = this.getVisualOpacity() if (this.persistMode && this.persistentLayer) { if (visualOpacity < 255) { g.tint(255, visualOpacity) } g.image(this.persistentLayer, 0, 0) } g.noFill() for (let s of this.squares) { if (s.opacity <= 0) continue const targetBuffer = (this.persistMode && this.persistentLayer) ? this.persistentLayer : g targetBuffer.push() targetBuffer.blendMode(s.blendMode) targetBuffer.stroke(s.color) targetBuffer.strokeWeight(s.strokeWeight) targetBuffer.noFill() if (!this.persistMode) { const c = color(s.color) c.setAlpha(s.opacity * (visualOpacity / 255)) targetBuffer.stroke(c) } targetBuffer.translate(this.x, this.y) targetBuffer.rotate(s.rotation) targetBuffer.rectMode(CENTER) targetBuffer.rect(0, 0, s.size * s.width * 2, s.size * s.height * 2) targetBuffer.pop() if (this.persistMode && this.persistentLayer && targetBuffer === this.persistentLayer) { g.push() g.blendMode(s.blendMode) const c = color(s.color) c.setAlpha(visualOpacity) g.stroke(c) g.strokeWeight(s.strokeWeight) g.noFill() g.translate(this.x, this.y) g.rotate(s.rotation) g.rectMode(CENTER) g.rect(0, 0, s.size * s.width * 2, s.size * s.height * 2) g.pop() } } g.pop() } getVisualOpacity() { const anyAlive = this.squares.some(s => s.opacity > 0) if (anyAlive) { this._fadeoutStart = undefined return 255 } if (!this._fadeoutStart) this._fadeoutStart = this.age const elapsed = this.age - this._fadeoutStart const fadeDuration = this.persistMode ? 100 : 1 // Only persistent layer needs fadeout return max(0, map(elapsed, 0, fadeDuration, 255, 0)) } isDone() { return this.getVisualOpacity() <= 0 } averageOpacity() { const squareOpacity = this.squares.length === 0 ? 0 : this.squares.reduce((sum, s) => sum + s.opacity, 0) / this.squares.length if (squareOpacity > 0) return squareOpacity return this.getVisualOpacity() / 255 * 100 } } class VerticalStripes extends FillAnimation { constructor(x, y, palette, traits) { super(x, y, palette, traits) this.stripes = [] this.reset(x, y, palette, traits) } reset(x, y, palette, traits) { super.reset(x, y, palette, traits) this.stripes = [] const count = floor(random(4, 10)) const spacing = width / count for (let i = 0; i < count; i++) { this.stripes.push({ x: i * spacing + random(-spacing * 0.3, spacing * 0.3), y: random(-height * 0.2, height * 0.2), width: random(spacing * 0.05, spacing * 0.5), height: 0, maxHeight: height * random(1, 7), speed: random(3, 10) * FILL_ANIMATION_SPEED_MULT, color: random(palette), opacity: 100, blendMode: this.getBlend([DIFFERENCE, ADD, SCREEN][i % 3]), maxLife: floor(random(60, 300)), age: 0, selfFading: false }) } } update() { super.update() for (let s of this.stripes) { s.age++ if (!s.selfFading && s.age > s.maxLife) { s.selfFading = true } s.height = min(s.height + s.speed * (s.selfFading ? 1.5 : 1), s.maxHeight) if (s.selfFading || s.height >= s.maxHeight) { s.opacity = max(0, s.opacity - (s.selfFading ? 0.8 : 0.3)) } } } display(g) { g.push() g.noStroke() for (let s of this.stripes) { if (s.opacity <= 0) continue g.push() g.blendMode(s.blendMode) const c = g.color(s.color) c.setAlpha(s.opacity) g.fill(c) g.rectMode(CENTER) g.rect(s.x, s.y, s.width, s.height) g.pop() } g.pop() } isDone() { if (this.stripes.length === 0) return true return this.stripes.every(s => s.opacity <= 0) } averageOpacity() { if (this.stripes.length === 0) return 0 return this.stripes.reduce((sum, s) => sum + s.opacity, 0) / this.stripes.length } } class HorizontalStripes extends FillAnimation { constructor(x, y, palette, traits) { super(x, y, palette, traits) this.stripes = [] this.reset(x, y, palette, traits) } reset(x, y, palette, traits) { super.reset(x, y, palette, traits) this.stripes = [] const verticalBuffer = 0.25 const baseHeight = globalBounds ? (globalBounds.maxY - globalBounds.minY) : height const baseMinY = globalBounds ? globalBounds.minY : 0 const baseMaxY = globalBounds ? globalBounds.maxY : height const maskMinY = baseMinY - baseHeight * verticalBuffer const maskMaxY = baseMaxY + baseHeight * verticalBuffer const maskHeight = maskMaxY - maskMinY const maskMinX = globalBounds ? globalBounds.minX : 0 const maskMaxX = globalBounds ? globalBounds.maxX : width const maskWidth = maskMaxX - maskMinX const count = floor(random(5, 15)) const spacing = maskHeight / count const widthMode = random(['wide', 'narrow', 'variable']) for (let i = 0; i < count; i++) { let stripeHeight if (widthMode === 'wide') { stripeHeight = spacing * random(0.7, 1.2) } else if (widthMode === 'narrow') { stripeHeight = spacing * random(0.05, 0.25) } else { stripeHeight = random(spacing * 0.1, spacing * 1.3) } this.stripes.push({ x: random(maskMinX - maskWidth * 0.3, maskMinX + maskWidth * 1.5), y: maskMinY + i * spacing + random(-spacing * 0.3, spacing * 0.3), width: 0, maxWidth: maskWidth * random(1.2, 2.0), height: stripeHeight, speed: random(2, 4.5) * FILL_ANIMATION_SPEED_MULT, color: random(palette), opacity: 100, blendMode: this.getBlend([DIFFERENCE, ADD, MULTIPLY][i % 3]), maxLife: floor(random(60, 300)), age: 0, selfFading: false }) } } update() { super.update() for (let s of this.stripes) { s.age++ if (!s.selfFading && s.age > s.maxLife) { s.selfFading = true } s.width = min(s.width + s.speed * (s.selfFading ? 1.5 : 1), s.maxWidth) if (s.selfFading || s.width >= s.maxWidth) { s.opacity = max(0, s.opacity - (s.selfFading ? 0.8 : 0.3)) } } } display(g) { g.push() g.noStroke() for (let s of this.stripes) { if (s.opacity <= 0) continue g.push() g.blendMode(s.blendMode) const c = g.color(s.color) c.setAlpha(s.opacity) g.fill(c) g.rectMode(CENTER) g.rect(s.x, s.y, s.width, s.height) g.pop() } g.pop() } isDone() { if (this.stripes.length === 0) return true return this.stripes.every(s => s.opacity <= 0) } averageOpacity() { if (this.stripes.length === 0) return 0 return this.stripes.reduce((sum, s) => sum + s.opacity, 0) / this.stripes.length } } class GradientFill extends FillAnimation { constructor(x, y, palette, traits) { super(x, y, palette, traits) this.fadeInDuration = 60 this.radius = 0 this.prevRadius = 0 this.maxRadius = max(width, height) * 1.5 this.speed = random(1.5, 3) * FILL_ANIMATION_SPEED_MULT this.colors = [] this.persistentLayer = createGraphics(width, height) this.angle = random(TWO_PI) this.lifetime = 0 this.fadeStartTime = null const colorCount = floor(random(3, 6)) for (let i = 0; i < colorCount; i++) { this.colors.push(random(palette)) } } reset(x, y, palette, traits) { super.reset(x, y, palette, traits) this.radius = 0 this.prevRadius = 0 this.maxRadius = max(width, height) * 1.5 this.speed = random(1.5, 3) * FILL_ANIMATION_SPEED_MULT this.colors = [] this.angle = random(TWO_PI) this.lifetime = 0 this.fadeStartTime = null if (!this.persistentLayer) { this.persistentLayer = createGraphics(width, height) } this.persistentLayer.clear() const colorCount = floor(random(3, 20)) for (let i = 0; i < colorCount; i++) { this.colors.push(random(palette)) } } update() { super.update() this.lifetime++ this.prevRadius = this.radius this.radius = min(this.radius + this.speed, this.maxRadius) if (this.radius >= this.maxRadius && this.fadeStartTime === null) { this.fadeStartTime = this.lifetime } if (this.radius > this.prevRadius && this.radius < this.maxRadius) { const pl = this.persistentLayer pl.push() pl.noStroke() pl.blendMode(BLEND) const r = this.radius const t = r / this.maxRadius const colorIndex = floor(t * (this.colors.length - 1)) const nextColorIndex = min(colorIndex + 1, this.colors.length - 1) const localT = (t * (this.colors.length - 1)) % 1 const c = lerpColor(color(this.colors[colorIndex]), color(this.colors[nextColorIndex]), localT) pl.noFill() pl.stroke(c) pl.strokeWeight(this.speed + 1) pl.circle(this.x, this.y, r * 2) pl.pop() } } getVisualOpacity() { if (this.fadeStartTime === null) { return 255 } const fadeTime = 150 const timeSinceFade = this.lifetime - this.fadeStartTime return max(0, map(timeSinceFade, 0, fadeTime, 255, 0)) } display(g) { g.push() const opacity = this.getVisualOpacity() if (opacity < 255) { g.tint(255, opacity) } g.image(this.persistentLayer, 0, 0) g.pop() } isDone() { return this.getVisualOpacity() <= 0 } averageOpacity() { return this.getVisualOpacity() / 255 * 100 } } class WavyLines extends FillAnimation { constructor(x, y, palette, traits) { super(x, y, palette, traits) this.fadeInDuration = 60 this.lines = [] this.trailLayer = null // Decaying trail buffer — always active this.directionMode = traits.wavyDirection || 'chaos' this.noiseOffset = random(100) // Unique noise offset per instance this.diagonalAngle = null // Locked per instance for diagonal mode const lineCount = floor(random(10, 24)) for (let i = 0; i < lineCount; i++) { const orientation = this.pickOrientation() this.lines.push(this.createLine(orientation, palette, i)) } } pickOrientation() { const mode = this.directionMode if (mode === 'horizontal-clash') { return random() < 0.5 ? 'horizontal-right' : 'horizontal-left' } if (mode === 'chaos') { const dirs = ['horizontal-right', 'horizontal-left', 'vertical-down', 'vertical-up', 'diagonal-down-right', 'diagonal-down-left', 'diagonal-up-right', 'diagonal-up-left'] return random(dirs) } return mode } createLine(orientation, palette, index) { const fullSpan = max(width, height) * 1.5 // Extended for diagonal lines const crossSpan = orientation === 'horizontal' ? height : width const spanType = random() let startT, endT if (spanType < 0.3) { startT = 0 endT = 1 } else if (spanType < 0.6) { startT = 0 endT = random(0.3, 0.8) } else if (spanType < 0.8) { startT = random(0.2, 0.7) endT = 1 } else { startT = random(0.1, 0.4) endT = random(0.6, 0.9) } const baseFreq = random(1.5, 4) const harmonics = [ { freq: baseFreq, amp: random(20, 10) * _designScale, phase: random(TWO_PI) }, { freq: baseFreq * random(1.8, 2.5), amp: random(8, 25) * _designScale, phase: random(TWO_PI) }, { freq: baseFreq * random(0.3, 0.6), amp: random(15, 40) * _designScale, phase: random(TWO_PI) }, ] const noiseSeedX = random(100) const noiseSeedY = random(1000) const numSegments = 40 const baseOffset = -random(100, 300) const baseSpeed = random(0.2, 2.8) * FILL_ANIMATION_SPEED_MULT const segments = [] for (let i = 0; i <= numSegments; i++) { const t = i / numSegments const speedVariation = random(0.4, 1.6) // Some segments faster, some slower segments.push({ offset: baseOffset, speed: baseSpeed * speedVariation, speedNoiseSeed: random(1000), }) } let angle, moveAngle if (orientation === 'horizontal-right') { angle = 0 moveAngle = PI / 2 // Move downward (perpendicular) } else if (orientation === 'horizontal-left') { angle = 0 moveAngle = -PI / 2 // Move upward (perpendicular) } else if (orientation === 'vertical-down') { angle = PI / 2 moveAngle = 0 // Move rightward (perpendicular) } else if (orientation === 'vertical-up') { angle = PI / 2 moveAngle = PI // Move leftward (perpendicular) } else if (orientation === 'diagonal-down-right') { angle = random(PI / 6, PI / 3) // 30-60 degrees moveAngle = angle + PI / 2 // Perpendicular movement } else if (orientation === 'diagonal-down-left') { angle = random(2 * PI / 3, 5 * PI / 6) // 120-150 degrees moveAngle = angle + PI / 2 } else if (orientation === 'diagonal-up-right') { angle = random(-PI / 3, -PI / 6) // -30 to -60 degrees moveAngle = angle + PI / 2 } else if (orientation === 'diagonal-up-left') { angle = random(-5 * PI / 6, -2 * PI / 3) // -120 to -150 degrees moveAngle = angle + PI / 2 } else { angle = random(TWO_PI) moveAngle = angle + PI / 2 } const startX = random(-width * 0.3, width * 1.3) const startY = random(-height * 0.3, height * 1.3) return { startX: startX, startY: startY, angle: angle, // Direction the line extends moveAngle: moveAngle, // Direction the line travels baseOffset: baseOffset, maxOffset: random(800, 2000) * _designScale, // Travel distance before starting to check exit baseSpeed: baseSpeed, segments: segments, harmonics: harmonics, thickness: random(3, 25) * _designScale, thicknessVariation: random(0.3, 10.8) * _designScale, color: random(palette), opacity: 100, isFading: false, // Once true, line commits to fading out (no flicker) blendMode: this.getBlend([DIFFERENCE, ADD, MULTIPLY][index % 3]), orientation: orientation, // Keep for reference startT: startT, endT: endT, noiseSeedX: noiseSeedX, noiseSeedY: noiseSeedY, noiseScale: random(0.003, 0.08), noiseAmp: random(15, 40) * _designScale, dripFactor: random(0.5, 1.5), phaseSpeed: random(0.01, 0.04), birthTime: this.age || 0, maxLife: floor(random(300, 700)), // 12-29 seconds at 24fps fadeInDuration: random(30, 60), speedEvolution: random(0.005, 0.02), speedNoiseScale: random(0.01, 0.03), lineLength: random(width * 0.8, width * 1.4), // How long the line extends } } reset(x, y, palette, traits) { super.reset(x, y, palette, traits) this.lines = [] this.directionMode = traits.wavyDirection || 'chaos' this.noiseOffset = random(1000) if (!this.trailLayer) { this.trailLayer = createGraphics(width, height) this.trailLayer.colorMode(HSB, 360, 100, 100, 100) } this.trailLayer.clear() this._trailDecayStart = undefined this._fadeoutStart = undefined const lineCount = floor(random(10, 24)) for (let i = 0; i < lineCount; i++) { const orientation = this.pickOrientation() this.lines.push(this.createLine(orientation, palette, i)) } } getWaveDisplacement(l, t, time) { const pos = t * l.lineLength let wave = 0 for (const h of l.harmonics) { const animatedPhase = h.phase + time * l.phaseSpeed wave += sin(pos * 0.02 * h.freq + animatedPhase) * h.amp } const noiseVal = noise( l.noiseSeedX + pos * l.noiseScale, l.noiseSeedY + time * 0.01 + this.noiseOffset ) const noiseDisplacement = (noiseVal - 0.5) * 2 * l.noiseAmp const ampModulation = noise(l.noiseSeedX + 100, t * 3) * 0.5 + 0.5 return (wave * ampModulation + noiseDisplacement) * l.dripFactor } getThicknessAt(l, t) { const thickNoise = noise(l.noiseSeedX + 200, t * 4) const variation = 1 - l.thicknessVariation + thickNoise * l.thicknessVariation * 2 return l.thickness * variation } update() { super.update() const time = this.age const padding = 100 const minX = -padding const maxX = width + padding const minY = -padding const maxY = height + padding for (let l of this.lines) { let maxSegmentOffset = -Infinity let minSegmentOffset = Infinity for (let i = 0; i < l.segments.length; i++) { const seg = l.segments[i] const t = i / (l.segments.length - 1) const speedNoise = noise( seg.speedNoiseSeed + time * l.speedEvolution, t * 3 ) const dynamicSpeedMult = 0.3 + speedNoise * 1.4 seg.offset += seg.speed * dynamicSpeedMult maxSegmentOffset = max(maxSegmentOffset, seg.offset) minSegmentOffset = min(minSegmentOffset, seg.offset) } l.currentMaxOffset = maxSegmentOffset l.currentMinOffset = minSegmentOffset let isOffCanvas = true const numSegments = l.segments.length - 1 for (let i = 0; i <= numSegments && isOffCanvas; i++) { const t = i / numSegments if (t < l.startT || t > l.endT) continue const seg = l.segments[i] const wave = this.getWaveDisplacement(l, t, time) const alongLine = t * l.lineLength - l.lineLength / 2 const baseX = l.startX + cos(l.angle) * alongLine const baseY = l.startY + sin(l.angle) * alongLine const px = baseX + cos(l.moveAngle) * (seg.offset + wave) const py = baseY + sin(l.moveAngle) * (seg.offset + wave) if (px >= minX && px <= maxX && py >= minY && py <= maxY) { isOffCanvas = false } } const lineAge = this.age - l.birthTime const pastMaxLife = lineAge > l.maxLife if (l.isFading) { l.opacity = max(0, l.opacity - 0.15) } else if (isOffCanvas && maxSegmentOffset > 0) { l.isFading = true l.opacity = max(0, l.opacity - 0.15) } else if (pastMaxLife) { l.isFading = true l.opacity = max(0, l.opacity - 0.25) } } } isDone() { return this.getVisualOpacity() <= 0 } getVisualOpacity() { const anyAlive = this.lines.some(l => l.opacity > 0) if (anyAlive) { this._fadeoutStart = undefined return 255 } if (!this._fadeoutStart) this._fadeoutStart = this.age const elapsed = this.age - this._fadeoutStart const fadeDuration = 150 // ~6 seconds at 24fps return max(0, map(elapsed, 0, fadeDuration, 255, 0)) } averageOpacity() { return this.getVisualOpacity() / 255 * 100 } display(g) { g.push() const visualOpacity = this.getVisualOpacity() if (this.trailLayer) { this.trailLayer.push() this.trailLayer.blendMode(BLEND) this.trailLayer.noStroke() this.trailLayer.fill(0, 0, 0, 3) // ~3% decay per frame this.trailLayer.rect(0, 0, width, height) this.trailLayer.pop() } const time = this.age for (let l of this.lines) { const lineAge = time - l.birthTime const fadeInAlpha = min(1, lineAge / l.fadeInDuration) const effectiveOpacity = l.opacity * fadeInAlpha * (visualOpacity / 255) if (effectiveOpacity <= 0) continue const numSegments = l.segments.length - 1 const segmentData = [] for (let i = 0; i < numSegments; i++) { const t1 = i / numSegments const t2 = (i + 1) / numSegments if (t2 < l.startT || t1 > l.endT) continue const segT1 = max(t1, l.startT) const segT2 = min(t2, l.endT) let edgeFade = 1 const edgeWidth = 0.1 if (segT1 < l.startT + edgeWidth) { edgeFade *= map(segT1, l.startT, l.startT + edgeWidth, 0, 1) } if (segT2 > l.endT - edgeWidth) { edgeFade *= map(segT2, l.endT - edgeWidth, l.endT, 1, 0) } const midT = (segT1 + segT2) / 2 const thickness = this.getThicknessAt(l, midT) const segIdx1 = floor(t1 * numSegments) const segIdx2 = min(segIdx1 + 1, numSegments) const localT = (t1 * numSegments) - segIdx1 const offset1 = lerp(l.segments[segIdx1].offset, l.segments[segIdx2].offset, localT) const segIdx3 = floor(t2 * numSegments) const segIdx4 = min(segIdx3 + 1, numSegments) const localT2 = (t2 * numSegments) - segIdx3 const offset2 = lerp(l.segments[segIdx3].offset, l.segments[segIdx4].offset, localT2) const alongLine1 = segT1 * l.lineLength - l.lineLength / 2 const alongLine2 = segT2 * l.lineLength - l.lineLength / 2 const baseX1 = l.startX + cos(l.angle) * alongLine1 const baseY1 = l.startY + sin(l.angle) * alongLine1 const baseX2 = l.startX + cos(l.angle) * alongLine2 const baseY2 = l.startY + sin(l.angle) * alongLine2 const wave1 = this.getWaveDisplacement(l, segT1, time) const wave2 = this.getWaveDisplacement(l, segT2, time) const x1 = baseX1 + cos(l.moveAngle) * (offset1 + wave1) const y1 = baseY1 + sin(l.moveAngle) * (offset1 + wave1) const x2 = baseX2 + cos(l.moveAngle) * (offset2 + wave2) const y2 = baseY2 + sin(l.moveAngle) * (offset2 + wave2) segmentData.push({ x1, y1, x2, y2, thickness, edgeFade }) } if (this.trailLayer) { this.trailLayer.push() this.trailLayer.blendMode(l.blendMode) this.trailLayer.noFill() for (const seg of segmentData) { const trailColor = color(l.color) trailColor.setAlpha(effectiveOpacity * seg.edgeFade * 0.5) // Half opacity for trail this.trailLayer.stroke(trailColor) this.trailLayer.strokeWeight(seg.thickness) this.trailLayer.line(seg.x1, seg.y1, seg.x2, seg.y2) } this.trailLayer.pop() } g.push() g.blendMode(l.blendMode) g.noFill() for (const seg of segmentData) { const coreColor = color(l.color) coreColor.setAlpha(effectiveOpacity * seg.edgeFade) g.stroke(coreColor) g.strokeWeight(seg.thickness) g.line(seg.x1, seg.y1, seg.x2, seg.y2) } g.pop() } if (this.trailLayer) { g.push() if (visualOpacity < 255) { g.tint(255, visualOpacity) } g.image(this.trailLayer, 0, 0) g.pop() } g.pop() } } class Sparkles extends FillAnimation { constructor(x, y, palette, traits) { super(x, y, palette, traits) this.fadeInDuration = 60 this.sparklePaths = [] this.lifetime = 0 this.maxLifetime = random(120, 250) this.maxActivePaths = 40 this.palette = palette this.persistentLayer = null } reset(x, y, palette, traits) { super.reset(x, y, palette, traits) this.sparklePaths = [] this.lifetime = 0 this.maxLifetime = random(120, 250) this.palette = palette if (!this.persistentLayer) { this.persistentLayer = createGraphics(width, height) } this.persistentLayer.clear() for (let i = 0; i < this.maxActivePaths; i++) { this.sparklePaths.push(this.createSparkle()) } } createSparkle() { const angle = random(TWO_PI) const speed = random(1, 3) * FILL_ANIMATION_SPEED_MULT return { x: this.x, y: this.y, vx: cos(angle) * speed, vy: sin(angle) * speed, size: random(10, 50) * _designScale, color: random(this.palette), life: random(0, 80), maxLife: random(60, 180) } } resetSparkle(s) { const angle = random(TWO_PI) const speed = random(1, 3) * FILL_ANIMATION_SPEED_MULT s.x = this.x s.y = this.y s.vx = cos(angle) * speed s.vy = sin(angle) * speed s.size = random(10, 50) * _designScale s.color = random(this.palette) s.life = 0 s.maxLife = random(60, 180) } update() { super.update() this.lifetime++ const stillSpawning = this.lifetime < this.maxLifetime const pl = this.persistentLayer pl.push() pl.noStroke() pl.blendMode(BLEND) const bounds = globalBounds const hasB = !!bounds const bounce = 0.7 for (let i = 0; i < this.sparklePaths.length; i++) { const s = this.sparklePaths[i] pl.fill(s.color) pl.circle(s.x, s.y, s.size) let x = s.x + s.vx let y = s.y + s.vy if (hasB) { if (x < bounds.minX) { x = bounds.minX; s.vx = Math.abs(s.vx) * bounce } else if (x > bounds.maxX) { x = bounds.maxX; s.vx = -Math.abs(s.vx) * bounce } if (y < bounds.minY) { y = bounds.minY; s.vy = Math.abs(s.vy) * bounce } else if (y > bounds.maxY) { y = bounds.maxY; s.vy = -Math.abs(s.vy) * bounce } } s.x = x s.y = y s.life++ if (s.life > s.maxLife) { if (stillSpawning) { this.resetSparkle(s) } } } pl.pop() } display(g) { g.push() const visualOpacity = this.getVisualOpacity() if (visualOpacity < 255) { g.tint(255, visualOpacity) } g.image(this.persistentLayer, 0, 0) g.pop() } getVisualOpacity() { if (this.lifetime < this.maxLifetime) { return 255 } const fadeTime = 150 const timeSinceEnd = this.lifetime - this.maxLifetime return max(0, map(timeSinceEnd, 0, fadeTime, 255, 0)) } isDone() { return this.getVisualOpacity() <= 0 } averageOpacity() { return this.getVisualOpacity() / 255 * 100 } } class VoronoiCells extends FillAnimation { constructor(x, y, palette, traits) { super(x, y, palette, traits) this.cells = [] const cellCount = floor(random(4, 10)) for (let i = 0; i < cellCount; i++) { const angle = random(TWO_PI) const dist = random(20, 80) * _designScale this.cells.push({ x: this.x + cos(angle) * dist, y: this.y + sin(angle) * dist, vx: cos(angle) * random(0.1, 0.4) * FILL_ANIMATION_SPEED_MULT, vy: sin(angle) * random(0.1, 0.4) * FILL_ANIMATION_SPEED_MULT, radius: random(15, 35) * _designScale, targetRadius: random(60, 120) * _designScale, color: random(palette), opacity: 100, blendMode: this.getBlend([DIFFERENCE, ADD, MULTIPLY][i % 3]), growthSpeed: random(0.3, 0.8) * FILL_ANIMATION_SPEED_MULT, maxLife: floor(random(80, 400)), age: 0, selfFading: false }) } } reset(x, y, palette, traits) { super.reset(x, y, palette, traits) this.cells = [] const cellCount = floor(random(4, 10)) for (let i = 0; i < cellCount; i++) { const angle = random(TWO_PI) const dist = random(20, 80) * _designScale this.cells.push({ x: this.x + cos(angle) * dist, y: this.y + sin(angle) * dist, vx: cos(angle) * random(0.1, 0.4) * FILL_ANIMATION_SPEED_MULT, vy: sin(angle) * random(0.1, 0.4) * FILL_ANIMATION_SPEED_MULT, radius: random(15, 35) * _designScale, targetRadius: random(60, 120) * _designScale, color: random(palette), opacity: 100, blendMode: this.getBlend([DIFFERENCE, ADD, MULTIPLY][i % 3]), growthSpeed: random(0.3, 0.8) * FILL_ANIMATION_SPEED_MULT, maxLife: floor(random(80, 400)), age: 0, selfFading: false }) } } update() { super.update() for (let c of this.cells) { c.age++ if (!c.selfFading && c.age > c.maxLife) { c.selfFading = true } if (c.radius < c.targetRadius) { c.radius += c.growthSpeed } let newX = c.x + c.vx let newY = c.y + c.vy const constrained = constrainToMaskBounds(newX, newY, c.vx, c.vy) c.x = constrained.x c.y = constrained.y c.vx = constrained.vx c.vy = constrained.vy const pulse = sin(this.age * 0.05) * 3 c.displayRadius = c.radius + pulse if (c.selfFading) { c.opacity = max(0, c.opacity - 0.6) } } } display(g) { for (let c of this.cells) { if (c.opacity <= 0) continue g.push() g.blendMode(c.blendMode) const col = g.color(c.color) col.setAlpha(c.opacity) g.fill(col) g.noStroke() g.circle(c.x, c.y, c.displayRadius * 2) g.pop() } } isDone() { if (this.cells.length === 0) return true return this.cells.every(c => c.opacity <= 0) } averageOpacity() { if (this.cells.length === 0) return 0 return this.cells.reduce((sum, c) => sum + c.opacity, 0) / this.cells.length } } class AnimationPool { constructor() { this.pools = { Supernova: [], ExpandingCircles: [], ExpandingSquares: [], VerticalStripes: [], HorizontalStripes: [], GradientFill: [], WavyLines: [], Sparkles: [], VoronoiCells: [], Shockwave: [], } this.maxPoolSize = 100 } createNew(type) { switch (type) { case 'Supernova': return new Supernova(0, 0, [], 0, [0, 0], {}) case 'ExpandingCircles': return new ExpandingCircles(0, 0, [], {}) case 'ExpandingSquares': return new ExpandingSquares(0, 0, [], {}) case 'VerticalStripes': return new VerticalStripes(0, 0, [], {}) case 'HorizontalStripes': return new HorizontalStripes(0, 0, [], {}) case 'GradientFill': return new GradientFill(0, 0, [], {}) case 'WavyLines': return new WavyLines(0, 0, [], {}) case 'Sparkles': return new Sparkles(0, 0, [], {}) case 'VoronoiCells': return new VoronoiCells(0, 0, [], {}) case 'Shockwave': return new Shockwave(0, 0, [], {}) default: return null } } acquire(type, x, y, palette, ...extraArgs) { const pool = this.pools[type] let obj = pool.pop() if (!obj) { obj = this.createNew(type) } obj.reset(x, y, palette, ...extraArgs) return obj } release(obj) { const type = obj.constructor.name const pool = this.pools[type] if (pool && pool.length < this.maxPoolSize) { if (obj.persistentLayer) { obj.persistentLayer.clear() } if (obj.trailLayer) { obj.trailLayer.clear() } pool.push(obj) } else { if (obj.persistentLayer) { obj.persistentLayer.remove() obj.persistentLayer = null } if (obj.trailLayer) { obj.trailLayer.remove() obj.trailLayer = null } } } clear() { Object.keys(this.pools).forEach(key => { this.pools[key] = [] }) } } let animationPool let TRAITS, PARAMS let fillAnimations = [] let orbitingElements = [] let starburstRotation = 0 let nextSpawnFrame = 0 let originCenters = [] let tidalAngle = 0 let eyesFlickerStart = 30 let eyesFlickerDuration = 35 let eyesFullyOn = false let regenStartFrame = 0 let staticLayerCache let formPart1Cache let formPart2Cache let maskSilhouetteCache let fillBuffer let fillPersistentLayer // Composites all current fill animations per frame let eyesCache let eyesBlockingCache let omniaEyesBase let orbitBuffer let meltBuffer let corruptBuffer let finalComposite let finalShadedBuffer let asciiShader let clipBuffer // temp buffer for mask clipping compositing let masks = [] let maskBoundsCache = [] let globalBounds = null let isDirty = true function breathWave(t, hz) { const cycleTime = 1.0 / hz const phase = (t % cycleTime) / cycleTime if (phase < 0.65) { const p = phase / 0.65 const sine = Math.sin(p * Math.PI) return Math.pow(sine, 0.7) } else { return 0.0 } } function getBreathTime() { if (_breathStartMillis === null) _breathStartMillis = millis() return (millis() - _breathStartMillis) / 1000.0 } function formBreathOffsetPart2() { const t = getBreathTime() const wave = breathWave(t, FORM_BREATH_HZ) const amp = TRAITS.form === "Armatus" ? FORM_BREATH_AMP * 0.5 : FORM_BREATH_AMP return wave * amp } function formBreathOffsetPart1() { const t = getBreathTime() const delayedT = t - BREATH_DELAY_PART1 const wave = breathWave(delayedT, FORM_BREATH_HZ) const amp = TRAITS.form === "Armatus" ? FORM_BREATH_AMP * 0.5 : FORM_BREATH_AMP return wave * amp } function maskBreathOffset() { const t = getBreathTime() const delayedT = t - BREATH_DELAY_MASK const wave = breathWave(delayedT, FORM_BREATH_HZ) const amp = TRAITS.form === "Armatus" ? FORM_BREATH_AMP * 0.5 : FORM_BREATH_AMP return wave * amp } function getEyeOpacity() { if (eyesFullyOn) return 1.0 const relativeFrame = frameCount - regenStartFrame const flickerEnd = eyesFlickerStart + eyesFlickerDuration if (relativeFrame < eyesFlickerStart) { return 0.0 } else if (relativeFrame >= flickerEnd) { eyesFullyOn = true return 1.0 } else { const flickerProgress = (relativeFrame - eyesFlickerStart) / eyesFlickerDuration const flickerSpeed = map(flickerProgress, 0, 1, 8, 2) const baseOpacity = map(flickerProgress, 0, 1, 0.3, 1.0) const flicker = noise(frameCount * flickerSpeed) > 0.4 ? 1.0 : 0.0 return baseOpacity * flicker } } function calculateMaskBounds() { maskBoundsCache = [] for (const m of masks) { const padding = 20 const bounds = { minX: m.x - m.w / 2 + padding, maxX: m.x + m.w / 2 - padding, minY: m.y - m.h / 2 + padding, maxY: m.y + m.h / 2 - padding, centerX: m.x, centerY: m.y, w: m.w, h: m.h, mask: m } maskBoundsCache.push(bounds) } calculateGlobalBounds() } function calculateGlobalBounds() { if (maskBoundsCache.length === 0) { globalBounds = null return } let minX = Infinity, maxX = -Infinity let minY = Infinity, maxY = -Infinity for (const bounds of maskBoundsCache) { minX = min(minX, bounds.minX) maxX = max(maxX, bounds.maxX) minY = min(minY, bounds.minY) maxY = max(maxY, bounds.maxY) } globalBounds = { minX: minX, maxX: maxX, minY: minY, maxY: maxY, centerX: (minX + maxX) / 2, centerY: (minY + maxY) / 2 } } function getRandomPointInMasks() { if (maskBoundsCache.length === 0) return { x: width / 2, y: height / 2 } const bounds = random(maskBoundsCache) return { x: random(bounds.minX, bounds.maxX), y: random(bounds.minY, bounds.maxY) } } function co
ood", 10], ["Signal", 10], ["Dual", 10], ["Stark", 10], ]) let eyeColor = pickWeightedWith(rEyeColor, [ ["Null", 15], ["Abyss", 80], ["Fury", 15], ["Plasma", 15], ["Omnia", 5], ]) // monochromeList — palettes that force the shader into single-hue tonal mode. // Trimmed based on rating data: Sage/Petal/Crimson/Amber removed (these scored // poorly as mono; they'll now go through the full complementary-color path). const monochromeList = [ "Lilac", "Abyss", "Canopy", "Haze", "Current", "Riptide", "Stark" ] // ─── ARCHETYPE PICKER ─── // 24 total: 21 Mono (weight 27 each, ~4.523% each) + 3 Conflux (weight 10 each, ~1.675% each) // Each archetype maps to one House (Codex/Forge/Helix) × one Role (Leader/Noble/Guard/Spy/ // Warrior/Scout/Chaos/Conflux). See HOUSE_BY_ARCHETYPE and ROLE_BY_ARCHETYPE below. let archetype = pickWeightedWith(rArch, [ // Leader role ["Vanta", 27], ["Dux", 27], ["Emperor", 27], // Noble role ["Seeker", 27], ["Arbiter", 27], ["Armatus", 27], // Guard role ["Warden", 27], ["Bulwark", 27], ["Guard", 27], // Spy role ["Shade", 27], ["Prime", 27], ["Apex", 27], // Warrior role ["Sentinel", 27], ["Riven", 27], ["Blade", 27], // Scout role ["Rogue", 27], ["Ferox", 27], ["Nomad", 27], // Chaos role ["Brute", 27], ["Havoc", 27], ["Helios", 27], // Conflux role (rare) ["Colossus", 10], ["Titan", 10], ["Goliath", 10], ]) const displayedArchetype = archetype // ─── HOUSE × ROLE LOOKUP ─── // Every archetype belongs to exactly one House (Codex/Forge/Helix) and one Role // (Leader/Noble/Guard/Spy/Warrior/Scout/Chaos/Conflux). 24 archetypes = 8 roles × 3 houses. // Codex (Red) — Intelligence, Wisdom, Science // Forge (Green) — Strength, brute force, physical power // Helix (Blue) — Power (political, spiritual) const HOUSE_BY_ARCHETYPE = { // Codex Vanta: "Codex", Seeker: "Codex", Warden: "Codex", Shade: "Codex", Sentinel: "Codex", Rogue: "Codex", Brute: "Codex", Colossus: "Codex", // Forge Dux: "Forge", Arbiter: "Forge", Bulwark: "Forge", Prime: "Forge", Riven: "Forge", Ferox: "Forge", Havoc: "Forge", Titan: "Forge", // Helix Emperor: "Helix", Armatus: "Helix", Guard: "Helix", Apex: "Helix", Blade: "Helix", Nomad: "Helix", Helios: "Helix", Goliath: "Helix", } const ROLE_BY_ARCHETYPE = { Vanta: "Leader", Dux: "Leader", Emperor: "Leader", Seeker: "Noble", Arbiter: "Noble", Armatus: "Noble", Warden: "Guard", Bulwark: "Guard", Guard: "Guard", Shade: "Spy", Prime: "Spy", Apex: "Spy", Sentinel: "Warrior", Riven: "Warrior", Blade: "Warrior", Rogue: "Scout", Ferox: "Scout", Nomad: "Scout", Brute: "Chaos", Havoc: "Chaos", Helios: "Chaos", Colossus: "Conflux", Titan: "Conflux", Goliath: "Conflux", } const house = HOUSE_BY_ARCHETYPE[archetype] const role = ROLE_BY_ARCHETYPE[archetype] // Count is derived: Conflux archetypes are the only Conflux-count pieces. const count = role === "Conflux" ? "Conflux" : "Mono" let eyes = pickWeightedWith(rEye, [ ["Core", 10], ["Echo", 10], ["Verge", 10], ["Slate", 10], ["Vert", 10], ["Signal", 10], ["Pierce", 10], ["Cross", 10], ["Cut", 10], ["Lune", 10], ["Mono", 10], ["Teardrop", 10], ]) let orbitingElements = pickWeightedWith(rOrbit, [ ["Calm", 10], ["Radiant", 10], ["Skyfall", 10], ["Melt", 10], ["CorruptMask", 10], ["CorruptRealm", 10], ["Seeds", 10], ["Motes", 10], ["Prisms", 10], ["Vent", 10], ["Kintsugi", 10], ]) let mouth = pickWeightedWith(rMou, [ ["Still", 10], ["Prowl", 10], ["Forge", 10], ["Sunder", 10], ["Hollow", 10], ["Devour", 10], ["Viper", 10], ["Bind", 10], ["Downfall", 10], ["Stun", 10], ["Grate", 10], ["Ease", 10], ]) let nose = pickWeightedWith(rNose, [ ["Sealed", 10], ["Aero", 10], ["Sego", 10], ["Duos", 10], ["Jewel", 10], ["Dot", 10], ["Aexo", 10], ["Tri", 10], ]) let ornament = pickWeightedWith(rOrn, [ ["Bare", 10], ["Links", 10], ["Amp", 10], ["Crest", 10], ["Bestia", 10], ["Root", 10], ["Gauge", 10], ["Thorns", 10], ["Tandem", 10], ["Spike", 10] ]) let hollows = pickWeightedWith(rCut, [ ["Whole", 10], ["Cleave", 10], ["Mark", 10], ["Maw", 10], ]) let realm = pickWeightedWith(rRealm, [ ["Halo", 2], ["Aero", 2], ["Rose", 2], ["Mint", 2], ["Mist", 1], ["Rise", 1], ["Fallen", 10], ["Shift", 10], ["Radiant", 10], ["Facet", 10], ["Ascend", 10], ["Descend", 10], ]) let edgeStyle = pickWeightedWith(rEdge, [ ["Curved", 35], ["Straight", 25], ["Faceted", 30], ]) let orientation = pickWeightedWith(rOr, [ ["Axis-locked", 80], ["Free", 20], ]) let fillVariant = pickWeightedWith(rFillVariant, [ ["Supernova", 10], ["ExpandingCircles", 10], ["ExpandingSquares", 10], ["VerticalStripes", 10], ["Gradient", 10], ["HorizontalStripes", 10], ["WavyLines", 10], ["Sparkles", 10], ["VoronoiCells", 10], ["Shockwave", 10], ]) if (paletteName === "Wraith" && eyeColor === "Null") { eyeColor = rEyeColor() < 2147483648 ? "Abyss" : "Fury" } if (paletteName === "Crimson" && eyeColor === "Fury") { eyeColor = rEyeColor() < 2147483648 ? "Null" : "Abyss" } if (archetype === "Helios" && orbitingElements === "Radiant") { orbitingElements = rOrbit() < 2147483648 ? "Seeds" : "Calm" } if (count === "Conflux" && eyes === "Mono") { eyes = rEye() < 2147483648 ? "Core" : "Slate" } if (mouth === "Prowl") { nose = "Sealed" } if (nose === "Sego" && mouth === "Grate") { nose = rNose() < 2147483648 ? "Aero" : "Sealed" } // ─── ETCHED PALETTE OVERRIDE ─── // 5% chance the rolled palette gets replaced by "Etched" — a B&W ink treatment. // Original palette is stored on _basePalette so render logic can still // dispatch the underlying palette's behavior (Wraith void, Crimson-specific // archetype tweaks, etc). // Threshold: 0.05 * 2^32 = 214748365 (integer-RNG parity for Solidity). let _basePalette = null if (rInkMode() < 214748365) { _basePalette = paletteName paletteName = "Etched" } const bestiaCompatible = ["Havoc", "Sentinel", "Apex", "Rogue", "Riven", "Guard", "Nomad", "Blade", "Prime"] if (ornament === "Bestia" && !bestiaCompatible.includes(displayedArchetype)) { ornament = "Bare" } if (ornament === "Crest" && displayedArchetype === "Vanta") { ornament = "Bare" } // ─── NON-VISUAL TRAIT — Spacecraft ─── // Doctrine is not rolled here — it is derived from the kill counts in // applyMutations() (0 kills at fresh mint → Idle). // Spacecraft: the vessel a citizen arrived on. Weights chosen to hit 54.9 / 20.1 / 13.8 / 11.3 %. const spacecraft = pickWeightedWith(rSpacecraft, [ ["Vessel", 549], ["Cruiser", 201], ["Scout", 138], ["Undocumented", 113], ]) const result = { archetype: archetype, displayedArchetype: displayedArchetype, house: house, role: role, edgeStyle: edgeStyle, iris: eyeColor, paletteName, _basePalette, sight: eyes, voice: mouth, scent: nose, ornament: ornament, hollows: hollows, realm: realm, orientation: orientation, count: count, doctrine: "Idle", // fresh-mint default; derived from kill counts in applyMutations() spacecraft: spacecraft, aura: orbitingElements, orbitalSpeed: pickWeightedWith(rOrbitSpeed, [ ["Slow", 1], ["Medium", 1], ["Fast", 1], ]), originTopology: pickWeightedWith(rOrigin, [ ["Clustered Burst", 40], ["Dual-Core", 22], ["Radial Drift", 16], ["Tidal Sweep", 12], ["Void Edge", 8], ["Singular", 2] ]), growthProfile: pickWeightedWith(rGrowth, [ ["Bilateral", 45], ["Aniso-X", 18], ["Aniso-Y", 18], ["Eased-Then-Coast", 14], ["Breathing", 5] ]), blendSchema: pickWeightedWith(rBlend, [ ["NoBlend", 50], ["MostlyDIFF", 28], ["AltDiffAdd", 9], ["PowerTrio", 9], ["Nocturne", 3], ["InvertedNight", 1] ]), sizeEnvelope: pickWeightedWith(rSize, [ ["Compact", 33], ["Medium", 66], ["Titanic", 33] ]), gridDensity: 92, symbolSize: pickWeightedWith(rSymbolSize, [ [1.1, 70], // standard — dominant [0.95, 15], // tight glyphs — rare [1.3, 15], // bold glyphs — rare ]), spawnRhythm: pickWeightedWith(rSpawn, [ ["Clockwork", 45], ["Poisson", 35], ["BurstRest", 20] ]), fillVariant: fillVariant, wavyDirection: pickWeightedWith(rWavyDir, [ ["horizontal", 40], ["vertical", 40], ["both", 20], ]), expandingRotation: pickWeightedWith(rExpandRotation, [ ["rotating", 20], ["static", 80], ]), expandingPersist: pickWeightedWith(rExpandPersist, [ ["fade", 20], ["persist", 80], ]), forceMonochrome: monochromeList.includes(paletteName), applyNoise: rApplyNoise() < 3006477107, // 70% chance noise is applied (0.7 * 2^32) } // ─── APPLY POST-MINT MUTATIONS ─── // After hash-derived traits are built, overlay any mutations from the // kill-mechanic state. Mutations may modify visual traits (palette, iris, etc.) // and bump counters (slain, remembering). For fresh-mint state, mutations are // all zero/null and this is effectively a no-op. const mutations = getMutations() applyMutations(result, mutations) // ─── ART BLOCKS FEATURES ─── // Public traits exposed to Art Blocks for on-chain metadata. Art Blocks // reads window.$features after the render completes and writes each entry // as a marketplace trait. Keep this list curated — only collector-facing traits. // All other entries on `result` are render-only. if (typeof window !== "undefined") { window.$features = { "Archetype": result.displayedArchetype, "House": result.house, "Class": result.role, "Energy": ({ ExpandingCircles: "Pulse", ExpandingSquares: "Lattice", VerticalStripes: "Beam", Gradient: "Bleed", HorizontalStripes: "Strata", WavyLines: "Frequency", Sparkles: "Spark", VoronoiCells: "Plasma", }[result.fillVariant]) || result.fillVariant, "Sight": result.sight, "Iris": result.iris, "Voice": result.voice, "Scent": result.scent, "Ornament": result.ornament, "Aura": ({ CorruptMask: "Broken", CorruptRealm: "Fragments" }[result.aura]) || result.aura, "Palette": result.paletteName, "Realm": result.realm, "Doctrine": result.doctrine, "Spacecraft": result.spacecraft, "Slain": result.slain.toString(), "Remembering": result.remembering.toString(), } // If embedded in a parent (e.g. the kill tester), post features up. // No-op when not embedded (window.parent === window). if (window.parent && window.parent !== window) { try { window.parent.postMessage({ type: "satari-features", features: window.$features }, "*") } catch (e) { /* ignore */ } } // Persist the public traits into a hidden #art-traits element so they survive in a // captured HTML snapshot (Cloudflare Browser Rendering). Idempotent — also covers // resetAndRegenerate, which rebuilds traits. if (window.$art) window.$art.setTraits(window.$features) } return result } // Returns the rendering palette name — looks through Etched override. // Use this anywhere render logic needs to dispatch on the underlying palette // (Wraith voids, Crimson-specific archetype tweaks, etc). function effectivePaletteName() { return (TRAITS && (TRAITS._basePalette || TRAITS.paletteName)) || null } function resolveParams(T) { const palettesByName = { "Flux": ["#110033", "#00FFFF", "#39FF14", "#FFFF00", "#FF4500"], "Acid": ["#EEFF00", "#FF00AA", "#00FFD5", "#CCFF00", "#FF6B00"], "Surge": ["#00F0FF", "#00FFA3", "#00FFEF", "#23F0C7", "#00D4FF"], "Infra": ["#FF0055", "#FF1493", "#FF6B00", "#110033", "#FF4D7E"], "Toxin": ["#39FF14", "#00FF41", "#00FF85", "#B6FF00", "#110033"], "Auric": ["#FFD700", "#FF00F5", "#FFA500", "#FF1493", "#110033"], "Jazz": ["#00CED1", "#FF1493", "#7B68EE", "#00FFFF", "#FF69B4"], "Corrupt": ["#00FF41", "#FF00F5", "#00FFFF", "#39FF14", "#FF0090"], "Vapor": ["#FF6EC7", "#01CDFE", "#B967FF", "#05FFA1", "#110033"], "Tropic": ["#FFD700", "#FF8C42", "#FFA07A", "#FF1493", "#110033"], "Wraith": ["#FFFFFF", "#F5F5F5", "#EBEBEB", "#E0E0E0", "#D6D6D6"], "Stark": ["#FFFFFF", "#FFFFFF", "#FFFFFF", "#FFFFFF", "#FFFFFF"], "Riptide": ["#004466", "#003366", "#115588", "#2277AA", "#44AAFF"], "Petal": ["#330011", "#551133", "#882255", "#BB4477", "#FF66AA"], "Sage": ["#114422", "#110033", "#228855", "#44BB77", "#66FFAA"], "Lilac": ["#2D1B4E", "#221155", "#442288", "#6644BB", "#9966FF"], "Crimson": ["#330000", "#660000", "#990000", "#CC0000", "#FF0000"], "Abyss": ["#001a33", "#003366", "#004d99", "#0066CC", "#0080FF"], "Canopy": ["#0d2b0d", "#1a4d1a", "#2d7a2d", "#3fa03f", "#52cc52"], "Amber": ["#1a0a00", "#ff6600", "#ffaa00", "#ffdd00", "#ffe566"], "Haze": ["#1a0033", "#330066", "#4d0099", "#6600cc", "#8000ff"], "Current": ["#0d0d33", "#1a1a66", "#2626cc", "#3333ff", "#6666ff"], "Flare": ["#FF0000", "#FF4500", "#FF8C00", "#FFD700", "#FFFF00"], "Coast": ["#00FFFF", "#00BFFF", "#0080FF", "#4040FF", "#8000FF"], "Flora": ["#9ACD32", "#00FF7F", "#00FA9A", "#00CED1", "#00BFFF"], "Astra": ["#4169E1", "#8A2BE2", "#9400D3", "#BA55D3", "#DA70D6"], "Dune": ["#FF4500", "#FF6347", "#FF7F50", "#FFA500", "#FFB347"], "Polar": ["#00CED1", "#20B2AA", "#48D1CC", "#7FFFD4", "#AFEEEE"], "Clash": ["#FF4500", "#00FFFF", "#FF6347"], "Royal": ["#8000FF", "#FFD700"], "Axis": ["#00FF7F", "#FF00FF", "#00FA9A"], "Blood": ["#8B0000", "#00CED1"], "Signal": ["#FF00FF", "#00FF00", "#FF0080"], "Dual": ["#FF1493", "#00FF88"], } let background = T.realm // Etched preserves the underlying palette behavior — check _basePalette first. const effectivePalette = T._basePalette || T.paletteName if (effectivePalette === "Wraith") background = "Void" if (effectivePalette === "Stark") background = "Void" const isPureBW = effectivePalette === "Wraith" || effectivePalette === "Stark" const canvasSize = width const scaleFactor = 750 / 700 // Fixed to design-width ratio — grid density is visual, not pixel-dependent const adjustedDensity = T.gridDensity / scaleFactor let maxSupernovas = 20 let rectRange = [1, 1] let sizeRange = { Compact: [100 * _designScale, 520 * _designScale], Medium: [300 * _designScale, 570 * _designScale], Titanic: [400 * _designScale, 640 * _designScale] }[T.sizeEnvelope] const baseSpawnEvery = 12 const axisLock = T.orientation === "Axis-locked" const symbolRNG = makeTraitRNG("symbolOffset") const symbolOffset = floor(symbolRNG() * 52 / 4294967296) const vocabRNG = makeTraitRNG("symbolVocab") // Production weights — full vocabulary dominates, restricted vocabs are rare moments. const vocabSize = pickWeightedWith(vocabRNG, [ [52, 90], // Full vocabulary — all 52 symbols (~90%) [10, 5], // Restricted — 10 contiguous symbols (~5%) [5, 3], // Tight — 5 contiguous symbols (~3%) [2, 1], // Binary — 2 alternating symbols (~1%) [1, 1], // Monosymbol — single symbol throughout (~1%) ]) return { palette: palettesByName[effectivePalette], background, axisLock, isPureBW, maxSupernovas, rectRange, sizeRange, gridDensity: adjustedDensity, symbolSize: T.symbolSize, spawnMode: T.spawnRhythm, baseSpawnEvery, fillVariant: T.fillVariant, aura: T.aura, orbitalSpeed: T.orbitalSpeed, symbolOffset, vocabSize, } } function isTwoPartArchetype(archetypeName) { const twoPartArchetypes = [ "Blade", "Havoc", "Vanta" ] return twoPartArchetypes.includes(archetypeName) } class OrbitingElement { constructor(type, centerX, centerY, palette, sharedTilt, speedSetting) { this.type = type this.centerX = centerX this.centerY = centerY this.palette = palette this.angle = random(TWO_PI) this.radiusX = random(300, 380) * _designScale this.radiusY = random(120, 180) * _designScale let speedRange if (speedSetting === "Slow") { speedRange = [0.0125, 0.03] } else if (speedSetting === "Fast") { speedRange = [0.0375, 0.075] } else { speedRange = [0.025, 0.05] } this.speed = random(speedRange[0], speedRange[1]) this.tilt = sharedTilt this.size = random(12, 35) * _designScale this.color = random(palette) this.rotation = random(TWO_PI) this.rotSpeed = random(-0.05, 0.05) this.trailLength = random() < 0.7 ? 0 : floor(random(2, 5)) this.trail = [] } update() { if (this.trailLength > 0) { this.trail.push({ angle: this.angle, rotation: this.rotation }) while (this.trail.length > this.trailLength) { this.trail.shift() } } this.angle += this.speed this.rotation += this.rotSpeed } getPosition(angle = this.angle) { const x = this.centerX + cos(angle) * this.radiusX const y = this.centerY + sin(angle) * this.radiusY * cos(this.tilt) const z = sin(angle) * this.radiusY * sin(this.tilt) return { x, y, z } } display(g, behind) { const pos = this.getPosition() const isMainBehind = pos.z < 0 if (behind !== isMainBehind) return if (this.trailLength > 0 && this.trail.length > 0) { for (let i = 0; i < this.trail.length; i++) { const trailPos = this.getPosition(this.trail[i].angle) const scale = map(abs(trailPos.z), 0, this.radiusY, 1.0, 0.6) const trailFade = map(i, 0, this.trail.length - 1, 0.2, 0.8) const opacity = map(abs(trailPos.z), 0, this.radiusY, 255, 100) * trailFade g.push() g.translate(trailPos.x, trailPos.y) g.rotate(this.trail[i].rotation) this.drawShapeWithOpacity(g, scale * (0.5 + trailFade * 0.5), opacity) g.pop() } } const scale = map(abs(pos.z), 0, this.radiusY, 1.0, 0.6) const opacity = map(abs(pos.z), 0, this.radiusY, 255, 100) g.push() g.translate(pos.x, pos.y) g.rotate(this.rotation) this.drawShapeWithOpacity(g, scale, opacity) g.pop() } drawShapeWithOpacity(g, scale, opacity) { const c = g.color(this.color) c.setAlpha(opacity) g.stroke(0, 0, 0) g.strokeWeight(3 * scale * _designScale) g.fill(c) if (this.type === "Seeds") { g.circle(0, 0, this.size * 2 * scale) } else if (this.type === "Prisms") { const s = this.size * scale g.rectMode(CENTER) g.rect(0, 0, s * 2, s * 2) } else if (this.type === "Motes") { g.circle(0, 0, this.size * scale) } } } function initOrbitingElements() { orbitingElements = [] starburstRotation = 0 if (PARAMS.aura === "Calm") { // no-op } else if (PARAMS.aura === "Radiant") { const starburstRNG = makeTraitRNG("starburstInit") starburstRotation = starburstRNG() / 4294967296 * TWO_PI } else if (PARAMS.aura === "Skyfall") { initSkyfall() } else if (PARAMS.aura === "Melt") { initMelt() } else if (PARAMS.aura === "CorruptMask" || PARAMS.aura === "CorruptRealm") { initCorrupt() } else if (PARAMS.aura === "Vent") { initVent() } else if (PARAMS.aura === "Kintsugi") { initCrack() } else if (PARAMS.aura === "Seeds" || PARAMS.aura === "Motes" || PARAMS.aura === "Prisms") { // Random orbital elements of the given type (count 3-19). const cx = width / 2 const cy = headCenterY() const tiltRNG = makeTraitRNG("orbitTilt") const sharedTilt = (tiltRNG() / 4294967296) * 1.6 - 0.8 const countRNG = makeTraitRNG("orbitCount") const count = 3 + Math.floor((countRNG() / 4294967296) * 17) // 3-19 for (let i = 0; i < count; i++) { orbitingElements.push( new OrbitingElement(PARAMS.aura, cx, cy, PARAMS.palette, sharedTilt, PARAMS.orbitalSpeed) ) } } } function drawStarburst(g) { g.push() g.translate(width / 2, headCenterY()) // v2: center on head zone g.rotate(starburstRotation) const _starburstRNG = makeTraitRNG("starburst") const starburstRNG = () => _starburstRNG() / 4294967296 const rayCount = floor(starburstRNG() * 16 + 24) const innerRadius = min(width, height) * 0.15 const outerRadius = max(width, height) * 1.65 const rayThickness = (starburstRNG() * 50 + 18) * _designScale const colorIndex = floor(starburstRNG() * PARAMS.palette.length) const starburstColor = PARAMS.palette[colorIndex] g.fill(starburstColor) g.stroke(0, 0, 0) g.strokeWeight(5 * _designScale) for (let i = 0; i < rayCount; i++) { const angle = (TWO_PI / rayCount) * i const x1 = cos(angle) * innerRadius const y1 = sin(angle) * innerRadius const x2 = cos(angle) * outerRadius const y2 = sin(angle) * outerRadius const lengthVariation = starburstRNG() * 0.15 + 0.85 const tipX = x2 * lengthVariation const tipY = y2 * lengthVariation const perpAngle = angle + HALF_PI const baseHalfWidth = rayThickness / 2 g.beginShape() g.vertex(x1 + cos(perpAngle) * baseHalfWidth, y1 + sin(perpAngle) * baseHalfWidth) g.vertex(tipX, tipY) g.vertex(x1 - cos(perpAngle) * baseHalfWidth, y1 - sin(perpAngle) * baseHalfWidth) g.endShape(CLOSE) } g.pop() } class SkyfallParticle { constructor(palette, scatter = false) { this.reset(palette, scatter) } reset(palette, scatter = false) { this.x = random(width) this.y = scatter ? random(-height * 0.2, height) : random(-height * 0.2, -50 * _designScale) this.speed = random(5, 10) * _designScale this.length = random(50, 300) * _designScale this.thickness = random(2, 5) * _designScale this.color = random(palette) this.opacity = scatter ? random(50, 255) : random(150, 255) this.fadeSpeed = random(1.25, 5) } update() { this.y += this.speed this.opacity -= this.fadeSpeed if (this.y > height + 100 || this.opacity <= 0) { this.reset(PARAMS.palette) } } display(g) { if (this.opacity <= 0) return const c = g.color(this.color) c.setAlpha(this.opacity) g.push() g.stroke(c) g.strokeWeight(this.thickness) g.line(this.x, this.y, this.x, this.y + this.length) g.noStroke() g.fill(c) g.circle(this.x, this.y, this.thickness * 3) g.pop() } } let skyfallParticles = [] function initSkyfall() { skyfallParticles = [] if (PARAMS.aura === "Skyfall") { const count = floor(random(15, 80)) for (let i = 0; i < count; i++) { skyfallParticles.push(new SkyfallParticle(PARAMS.palette, true)) } } } function drawSkyfall(g) { if (PARAMS.aura === "Skyfall") { for (let p of skyfallParticles) { p.display(g) } } } function updateSkyfall() { if (PARAMS.aura === "Skyfall") { for (let p of skyfallParticles) { p.update() } } } // ────────────────────────────────────────────────────────────── // VENT — rising particles from bottom, thicker than Skyfall, meandering ascent // Particles spawn at random x-positions at the bottom, drift upward with a sine // meander, fade out in the top third. Hard alpha (no additive blend). // ────────────────────────────────────────────────────────────── class VentParticle { constructor(palette, scatter = false) { this.reset(palette, scatter) } reset(palette, scatter = false) { this.x = random(width) this.baseX = this.x // track origin for sine meander this.y = scatter ? random(0, height) : random(height, height + 100) this.speed = random(1.5, 3.5) * _designScale // slower than Skyfall (rises, not falls fast) this.size = random(4, 10) * _designScale // thicker than Skyfall (Skyfall is 2-5) this.color = random(palette) this.opacity = 220 + random(35) // hard alpha — opaque this.meanderAmp = random(8, 22) * _designScale this.meanderFreq = random(0.012, 0.028) this.meanderPhase = random(TWO_PI) this.life = 0 // tracks time alive for meander progression } update() { this.life += 1 this.y -= this.speed // Horizontal meander as a sine of life-time this.x = this.baseX + sin(this.life * this.meanderFreq + this.meanderPhase) * this.meanderAmp // Fade out gently in the top third if (this.y < height * 0.33) { this.opacity -= 2.5 } if (this.y < -50 || this.opacity <= 0) { this.reset(PARAMS.palette) } } display(g) { if (this.opacity <= 0) return const c = g.color(this.color) c.setAlpha(this.opacity) g.push() g.noStroke() g.fill(c) g.circle(this.x, this.y, this.size) g.pop() } } let ventParticles = [] function initVent() { ventParticles = [] if (PARAMS.aura === "Vent") { const count = 70 // denser than Skyfall (Skyfall picks 15-80, Vent fixed 70) for (let i = 0; i < count; i++) { ventParticles.push(new VentParticle(PARAMS.palette, true)) } } } function drawVent(g) { if (PARAMS.aura === "Vent") { for (let p of ventParticles) p.display(g) } } function updateVent() { if (PARAMS.aura === "Vent") { for (let p of ventParticles) p.update() } } // ────────────────────────────────────────────────────────────── // KINTSUGI — 1-3 jagged fissures across canvas, behind form layer. // Each crack is a polyline path. Color rotates through palette at 4x Omnia speed // (Omnia = 0.08, so Kintsugi = 0.32). All cracks share the same palette but each // has its own phase offset so they don't all flash the same color simultaneously. // ────────────────────────────────────────────────────────────── let crackPaths = [] function initCrack() { crackPaths = [] if (PARAMS.aura !== "Kintsugi") return const _crackRNG = makeTraitRNG("crackInit") const crackRNG = () => _crackRNG() / 4294967296 const crackCount = 4 + floor(crackRNG() * 5) // 4-8 cracks for (let i = 0; i < crackCount; i++) { const path = [] // Each crack travels from one edge to another. Pick start and end edges. // 4 edges: 0=top, 1=right, 2=bottom, 3=left const startEdge = floor(crackRNG() * 4) let endEdge = floor(crackRNG() * 4) while (endEdge === startEdge) endEdge = floor(crackRNG() * 4) const edgeToPoint = (edge, t) => { if (edge === 0) return { x: t * width, y: 0 } if (edge === 1) return { x: width, y: t * height } if (edge === 2) return { x: t * width, y: height } return { x: 0, y: t * height } } const start = edgeToPoint(startEdge, crackRNG()) const end = edgeToPoint(endEdge, crackRNG()) // 5-9 jagged segments between start and end const segments = 5 + floor(crackRNG() * 5) const jitterMag = min(width, height) * 0.08 path.push(start) for (let s = 1; s < segments; s++) { const t = s / segments const baseX = lerp(start.x, end.x, t) const baseY = lerp(start.y, end.y, t) // Perpendicular jitter const dx = end.x - start.x const dy = end.y - start.y const len = sqrt(dx * dx + dy * dy) || 1 const perpX = -dy / len const perpY = dx / len const jitter = (crackRNG() * 2 - 1) * jitterMag path.push({ x: baseX + perpX * jitter, y: baseY + perpY * jitter }) } path.push(end) crackPaths.push({ points: path, phaseOffset: crackRNG() * TWO_PI, // each crack at different palette phase thickness: (2 + crackRNG() * 3) * _designScale, }) } } function getCrackColor(phaseOffset) { // 4x Omnia speed (Omnia = 0.08, Kintsugi = 0.32). Uses palette colors directly // (not complementary like Omnia). const cycleSpeed = 0.32 const palette = PARAMS.palette const cyclePos = (millis() * cycleSpeed + phaseOffset * 100) % (palette.length * 100) const idx = floor(cyclePos / 100) % palette.length const next = (idx + 1) % palette.length const blend = (cyclePos % 100) / 100 const c1 = color(palette[idx]) const c2 = color(palette[next]) return lerpColor(c1, c2, blend) } function drawCrack(g) { if (PARAMS.aura !== "Kintsugi") return for (const crack of crackPaths) { const col = getCrackColor(crack.phaseOffset) g.push() g.stroke(col) g.strokeWeight(crack.thickness) g.noFill() g.beginShape() for (const p of crack.points) { g.vertex(p.x, p.y) } g.endShape() g.pop() } } let meltStreams = [] function initMeltStreams() { meltStreams = [] const numStreams = floor(random(2, 5)) const totalWidth = random(50, 200) * _designScale for (let i = 0; i < numStreams; i++) { const streamWidth = totalWidth / numStreams * random(0.5, 1.5) meltStreams.push({ centerX: width / 2 + random(-totalWidth / 2, totalWidth / 2), width: streamWidth }) } } class MeltParticle { constructor(palette, scatter = false) { this.assignStream() this.reset(palette, scatter) } assignStream() { if (meltStreams.length > 0) { this.stream = random(meltStreams) } else { this.stream = { centerX: width / 2, width: 100 * _designScale } } } reset(palette, scatter = false) { this.assignStream() this.x = this.stream.centerX + random(-this.stream.width / 2, this.stream.width / 2) const startY = globalBounds ? globalBounds.centerY : height / 2 this.y = scatter ? random(startY, height * 1.2) : startY + random(-20, 20) * _designScale this.speed = random(3, 8) * _designScale this.targetLength = random(30, 200) * _designScale this.targetThickness = random(1, 12) * _designScale this.currentLength = scatter ? random(0, this.targetLength) : 0 this.currentThickness = scatter ? random(0, this.targetThickness) : 0 this.growthRate = random(2, 6) * _designScale this.color = random(palette) this.opacity = scatter ? random(80, 255) : random(180, 255) this.fadeSpeed = random(0.8, 3) this.startDelay = scatter ? 0 : floor(random(0, 60)) this.waiting = !scatter this.maxTravelY = startY + random(150 * _designScale, height * 0.8) } update() { if (this.waiting) { this.startDelay-- if (this.startDelay <= 0) { this.waiting = false } return } this.y += this.speed this.opacity -= this.fadeSpeed this.currentLength = min(this.currentLength + this.growthRate, this.targetLength) this.currentThickness = min(this.currentThickness + this.growthRate * 0.1, this.targetThickness) if (this.y > this.maxTravelY - 100 * _designScale) { this.opacity -= this.fadeSpeed * 2 } if (this.y > this.maxTravelY || this.opacity <= 0) { this.reset(PARAMS.palette) } } display(g) { if (this.waiting || this.opacity <= 0 || this.currentThickness < 0.1) return const c = g.color(this.color) c.setAlpha(this.opacity) g.push() g.stroke(c) g.strokeWeight(this.currentThickness) g.line(this.x, this.y, this.x, this.y + this.currentLength) g.noStroke() g.fill(c) g.circle(this.x, this.y, this.currentThickness * 2.5) g.pop() } } let meltParticles = [] let meltDelayFrames = 50 let meltFrameCount = 0 function initMelt() { meltParticles = [] meltFrameCount = 0 if (PARAMS.aura === "Melt") { initMeltStreams() const count = floor(random(30, 90)) for (let i = 0; i < count; i++) { meltParticles.push(new MeltParticle(PARAMS.palette, false)) } } } function drawMelt(g) { if (PARAMS.aura === "Melt" && meltFrameCount >= meltDelayFrames) { for (let p of meltParticles) { p.display(g) } } } function updateMelt() { if (PARAMS.aura === "Melt") { meltFrameCount++ if (meltFrameCount >= meltDelayFrames) { for (let p of meltParticles) { p.update() } } } } class CorruptSlice { constructor(forMask = false) { this.forMask = forMask this.reset() } reset() { this.y = random(height) this.sliceHeight = this.forMask ? random(2, 15) * _designScale : random(5, 60) * _designScale this.sliceWidth = random(80, 250) * _designScale this.sliceX = width / 2 - this.sliceWidth / 2 + random(-30, 30) * _designScale this.offset = random(-80, 80) * _designScale this.duration = floor(random(2, 8)) this.age = 0 this.active = false } trigger() { this.reset() this.active = true } update() { if (this.active) { this.age++ if (this.age >= this.duration) { this.active = false } } } } let corruptSlices = [] let corruptTimer = 0 let corruptBurstMode = false let corruptBurstCount = 0 function initCorrupt() { corruptSlices = [] corruptTimer = 0 corruptBurstMode = false corruptBurstCount = 0 const isMask = PARAMS.aura === "CorruptMask" const poolSize = isMask ? 40 : 15 for (let i = 0; i < poolSize; i++) { corruptSlices.push(new CorruptSlice(isMask)) } } function updateCorrupt() { if (PARAMS.aura !== "CorruptMask" && PARAMS.aura !== "CorruptRealm") return const isMask = PARAMS.aura === "CorruptMask" corruptTimer++ for (let slice of corruptSlices) { slice.update() } if (corruptBurstMode) { corruptBurstCount-- if (corruptBurstCount <= 0) { corruptBurstMode = false } else if (random() < 0.7) { triggerRandomSlice() } } const triggerChance = isMask ? 0.15 : 0.02 const burstChance = isMask ? 0.5 : 0.3 if (!corruptBurstMode && random() < triggerChance) { if (random() < burstChance) { corruptBurstMode = true corruptBurstCount = floor(random(isMask ? 10 : 5, isMask ? 30 : 20)) } else { triggerRandomSlice() } } } function triggerRandomSlice() { for (let slice of corruptSlices) { if (!slice.active) { slice.trigger() break } } } class FillAnimation { constructor(x, y, palette, traits) { this.x = x this.y = y this.palette = palette this.traits = traits this.age = 0 this.fadeInDuration = 24 // ~1 second at 24fps } update() { this.age++ } reset(x, y, palette, traits) { this.x = x this.y = y this.palette = palette this.traits = traits this.age = 0 } display(g) { } getBlend(requestedMode) { if (!this.traits || !this.traits.blendSchema) return BLEND const schema = this.traits.blendSchema if (schema === "NoBlend") return BLEND if (schema === "MostlyDIFF") return DIFFERENCE if (schema === "AltDiffAdd") return requestedMode if (schema === "PowerTrio") return requestedMode if (schema === "Nocturne") { if (requestedMode === ADD || requestedMode === SCREEN) return MULTIPLY return requestedMode } if (schema === "InvertedNight") { if (requestedMode === MULTIPLY) return SCREEN return requestedMode } return BLEND } isDone() { return false } averageOpacity() { return 100 } } class Supernova extends FillAnimation { constructor(x, y, palette, count, sizeRange, traits) { super(x, y, palette, traits) this.count = count this.sizeRange = sizeRange this.rectangles = [] this.startedCount = 0 this.fadedCount = 0 this.shouldRotate = random() > 0.8 const layers = Math.ceil(count / 8) const rectsPerLayer = Math.ceil(count / layers) let rectIndex = 0 for (let layer = 0; layer < layers && rectIndex < count; layer++) { const numInLayer = Math.min(rectsPerLayer, count - rectIndex) for (let i = 0; i < numInLayer; i++) { const angle = (TWO_PI / numInLayer) * i + random(-0.2, 0.2) const speed = (this.shouldRotate ? random(0.005, 0.015) : random(0.015, 0.025)) * FILL_ANIMATION_SPEED_MULT const startDist = random(10, 40) const sx = this.x + cos(angle) * startDist const sy = this.y + sin(angle) * startDist const layerSizeMod = layers > 1 ? map(layer, 0, layers - 1, 1.2, 0.8) : 1.0 const w = random(sizeRange[0], sizeRange[1]) * layerSizeMod const h = random(sizeRange[0], sizeRange[1]) * layerSizeMod const col = random(palette) const fadeRate = random(1.0, 2.0) const blendMode = this.pickBlend(rectIndex) this.rectangles.push({ x: sx, y: sy, vx: cos(angle) * speed, vy: sin(angle) * speed, width: 0, height: 0, baseWidth: w, baseHeight: h, color: col, opacity: 100, fadeRate, blendMode, rotation: this.shouldRotate ? angle + HALF_PI : 0, rotSpeed: this.shouldRotate ? random(-0.005, 0.005) : 0, growthPhase: random(TWO_PI), layer: layer, angle: angle, delay: rectIndex * 6, started: false, faded: false, maxLife: floor(random(80, 400)), selfFading: false }) rectIndex++ } } } pickBlend(i) { return this.getBlend(i % 2 === 0 ? DIFFERENCE : ADD) } reset(x, y, palette, count, sizeRange, traits) { super.reset(x, y, palette, traits) this.count = count this.sizeRange = sizeRange this.rectangles = [] this.startedCount = 0 this.fadedCount = 0 this.shouldRotate = random() > 0.8 // 20% chance, matching constructor const layers = Math.ceil(count / 8) const rectsPerLayer = Math.ceil(count / layers) let rectIndex = 0 for (let layer = 0; layer < layers && rectIndex < count; layer++) { const numInLayer = Math.min(rectsPerLayer, count - rectIndex) for (let i = 0; i < numInLayer; i++) { const angle = (TWO_PI / numInLayer) * i + random(-0.2, 0.2) const speed = (this.shouldRotate ? random(0.015, 0.045) : random(0.025, 0.075)) * FILL_ANIMATION_SPEED_MULT const startDist = random(1, 4) const sx = this.x + cos(angle) * startDist const sy = this.y + sin(angle) * startDist const layerSizeMod = layers > 1 ? map(layer, 0, layers - 1, 1.2, 0.8) : 1.0 const w = random(sizeRange[0], sizeRange[1]) * layerSizeMod const h = random(sizeRange[0], sizeRange[1]) * layerSizeMod const col = random(palette) const fadeRate = random(1.0, 2.0) const blendMode = this.pickBlend(rectIndex) this.rectangles.push({ x: sx, y: sy, vx: cos(angle) * speed, vy: sin(angle) * speed, width: 0, height: 0, baseWidth: w, baseHeight: h, color: col, opacity: 100, fadeRate, blendMode, rotation: this.shouldRotate ? angle + HALF_PI : 0, rotSpeed: this.shouldRotate ? random(-0.005, 0.005) : 0, growthPhase: random(TWO_PI), layer: layer, angle: angle, delay: rectIndex * 6, started: false, faded: false, maxLife: floor(random(80, 400)), selfFading: false }) rectIndex++ } } } update() { super.update() for (let r of this.rectangles) { if (this.age < r.delay) continue if (!r.started) { r.started = true r.startAge = this.age this.startedCount++ } let newX = r.x + r.vx let newY = r.y + r.vy const constrained = constrainToMaskBounds(newX, newY, r.vx, r.vy) r.x = constrained.x r.y = constrained.y r.vx = constrained.vx r.vy = constrained.vy r.rotation += r.rotSpeed const rectAge = r.started ? (this.age - r.startAge) : 0 if (!r.selfFading && rectAge > r.maxLife) { r.selfFading = true } if (r.selfFading) { r.opacity = max(0, r.opacity - r.fadeRate) } if (!r.faded && r.opacity <= 0) { r.faded = true this.fadedCount++ } this.applyGrowthProfile(r) } } applyGrowthProfile(r) { const profile = this.traits.growthProfile const rectAge = r.started ? (this.age - r.startAge) : 0 const expansionDuration = 90 let baseScale = 1.0 if (rectAge < expansionDuration) { const t = rectAge / expansionDuration baseScale = 1 - pow(1 - t, 3) } if (profile === "Bilateral") { const scale = baseScale * (1.0 + sin(rectAge * 0.05 + r.growthPhase) * 0.15) r.width = r.baseWidth * scale r.height = r.baseHeight * scale } else if (profile === "Aniso-X") { const scaleX = baseScale * (1.0 + sin(rectAge * 0.05 + r.growthPhase) * 0.3) const scaleY = baseScale * (1.0 + sin(rectAge * 0.05 + r.growthPhase) * 0.05) r.width = r.baseWidth * scaleX r.height = r.baseHeight * scaleY } else if (profile === "Aniso-Y") { const scaleX = baseScale * (1.0 + sin(rectAge * 0.05 + r.growthPhase) * 0.05) const scaleY = baseScale * (1.0 + sin(rectAge * 0.05 + r.growthPhase) * 0.3) r.width = r.baseWidth * scaleX r.height = r.baseHeight * scaleY } else if (profile === "Eased-Then-Coast") { const easeDuration = 120 if (rectAge < easeDuration) { const t = rectAge / easeDuration const easeOut = 1 - pow(1 - t, 3) const scale = baseScale * (1.0 + easeOut * 0.4) r.width = r.baseWidth * scale r.height = r.baseHeight * scale } else { r.width = r.baseWidth * 1.4 r.height = r.baseHeight * 1.4 } } else if (profile === "Breathing") { const breathFreq = 0.05 const breathDepth = 0.5 const breathPhase = sin(rectAge * breathFreq + r.growthPhase) const scale = baseScale * (1.0 + breathPhase * breathDepth) r.width = r.baseWidth * scale r.height = r.baseHeight * scale } else { r.width = r.baseWidth * baseScale r.height = r.baseHeight * baseScale } } display(g) { for (let r of this.rectangles) { if (!r.started || r.opacity <= 0) continue g.push() g.blendMode(r.blendMode) g.translate(r.x, r.y) g.rotate(r.rotation) const c = g.color(r.color) c.setAlpha(r.opacity) g.fill(c) g.noStroke() g.rectMode(CENTER)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <title>SATARI</title> <meta name="viewport" content="width=device-width, initial-scale=1" /> <!-- SATARI ENGINE — zero-dependency p5 shim --> <script> /* ================================================================ MICRO-P5: minimal p5.js-compatible engine for SATARI Provides: canvas, draw loop, graphics buffers, HSB colors, shapes, blend modes, seeded RNG, noise, WebGL shader support ================================================================ */ // ─── CONSTANTS ─── var PI = Math.PI, TWO_PI = PI * 2, HALF_PI = PI / 2, QUARTER_PI = PI / 4 var TAU = TWO_PI var CLOSE = 'close' var CENTER = 1, CORNER = 0 var HSB = 'hsb', RGB = 'rgb' var WEBGL = 'webgl' var ROUND = 'round', SQUARE = 'butt', PROJECT = 'square' var BLEND = 'source-over', ADD = 'lighter', MULTIPLY = 'multiply' var SCREEN = 'screen', DIFFERENCE = 'difference', EXCLUSION = 'exclusion' var OVERLAY = 'overlay', REPLACE = 'copy' // ─── MATH GLOBALS ─── var { floor, ceil, round, abs, min, max, sin, cos, tan, atan2, sqrt, pow, log, exp, sign } = Math function lerp(a, b, t) { return a + (b - a) * t } function map(v, a1, b1, a2, b2) { return a2 + (v - a1) / (b1 - a1) * (b2 - a2) } function constrain(v, lo, hi) { return max(lo, min(hi, v)) } // ─── SEEDED RNG ─── var _rngState = 12345 function _rngNext() { let t = (_rngState += 0x6D2B79F5) t = Math.imul(t ^ (t >>> 15), t | 1) t ^= t + Math.imul(t ^ (t >>> 7), t | 61) return ((t ^ (t >>> 14)) >>> 0) / 4294967296 } function randomSeed(s) { _rngState = s >>> 0 } function random(a, b) { const r = _rngNext() if (a === undefined) return r if (b === undefined) { if (Array.isArray(a)) return a[floor(r * a.length)] return r * a } return a + r * (b - a) } function randomGaussian(m, sd) { const u1 = _rngNext(), u2 = _rngNext() return (m || 0) + (sd || 1) * sqrt(-2 * log(u1 || 1e-10)) * cos(TWO_PI * u2) } // ─── PERLIN NOISE ─── var _noiseState = 0 var _noisePerm = new Uint8Array(512) function noiseSeed(s) { _noiseState = s const prev = _rngState; _rngState = s >>> 0 for (let i = 0; i < 256; i++) _noisePerm[i] = i for (let i = 255; i > 0; i--) { const j = floor(_rngNext() * (i + 1)) const tmp = _noisePerm[i]; _noisePerm[i] = _noisePerm[j]; _noisePerm[j] = tmp } for (let i = 0; i < 256; i++) _noisePerm[i + 256] = _noisePerm[i] _rngState = prev } function _fade(t) { return t * t * t * (t * (t * 6 - 15) + 10) } function _ngrad(hash, x) { return (hash & 1) ? -x : x } function noise(x, y, z) { x = x || 0; y = y || 0; z = z || 0 const X = floor(x) & 255, Y = floor(y) & 255, Z = floor(z) & 255 x -= floor(x); y -= floor(y); z -= floor(z) const u = _fade(x), v = _fade(y), w = _fade(z) const A = _noisePerm[X] + Y, B = _noisePerm[X + 1] + Y const AA = _noisePerm[A] + Z, AB = _noisePerm[A + 1] + Z const BA = _noisePerm[B] + Z, BB = _noisePerm[B + 1] + Z return (lerp( lerp(lerp(_ngrad(_noisePerm[AA], x), _ngrad(_noisePerm[BA], x - 1), u), lerp(_ngrad(_noisePerm[AB], x), _ngrad(_noisePerm[BB], x - 1), u), v), lerp(lerp(_ngrad(_noisePerm[AA + 1], x), _ngrad(_noisePerm[BA + 1], x - 1), u), lerp(_ngrad(_noisePerm[AB + 1], x), _ngrad(_noisePerm[BB + 1], x - 1), u), v), w ) + 1) / 2 // map to 0-1 } noiseSeed(0) // init permutation table // ─── COLOR SYSTEM ─── function _hsbToCSS(h, s, b, a) { h = ((h % 360) + 360) % 360 s = constrain(s, 0, 100) / 100 b = constrain(b, 0, 100) / 100 a = constrain(a !== undefined ? a : 100, 0, 100) / 100 const k = n => (n + h / 60) % 6 const f = n => b * (1 - s * max(0, min(k(n), 4 - k(n), 1))) return `rgba(${round(f(5) * 255)},${round(f(3) * 255)},${round(f(1) * 255)},${a})` } function _parseHex(hex) { const r = parseInt(hex.slice(1, 3), 16) const g = parseInt(hex.slice(3, 5), 16) const b = parseInt(hex.slice(5, 7), 16) return [r, g, b, 255] } function _rgbToHSB(r, g, b) { r /= 255; g /= 255; b /= 255 const mx = max(r, g, b), mn = min(r, g, b), d = mx - mn let h = 0, s = mx === 0 ? 0 : d / mx if (d !== 0) { if (mx === r) h = ((g - b) / d + 6) % 6 else if (mx === g) h = (b - r) / d + 2 else h = (r - g) / d + 4 h *= 60 } return [h, s * 100, mx * 100] } // Color object — stores RGBA internally class SColor { constructor(r, g, b, a) { this._r = r; this._g = g; this._b = b; this._a = a } toString() { return `rgba(${this._r},${this._g},${this._b},${this._a / 255})` } setAlpha(a) { this._a = constrain(round(a / 100 * 255), 0, 255) } } window.SColor = SColor function color(a, b, c, d) { if (a instanceof SColor) return a if (typeof a === 'string') { const [r, g, bl, al] = _parseHex(a) return new SColor(r, g, bl, al) } // HSB mode: (h, s, b [, a]) if (c !== undefined) { const css = _hsbToCSS(a, b, c, d !== undefined ? d : 100) const m = css.match(/[\d.]+/g) return new SColor(+m[0], +m[1], +m[2], round(+m[3] * 255)) } // single value → grayscale brightness const v = constrain(a, 0, 100) const bri = round(v / 100 * 255) if (b !== undefined) { // (gray, alpha) return new SColor(bri, bri, bri, round(constrain(b, 0, 100) / 100 * 255)) } return new SColor(bri, bri, bri, 255) } function red(c) { return c._r } function green(c) { return c._g } function blue(c) { return c._b } function hue(c) { return _rgbToHSB(c._r, c._g, c._b)[0] } function lerpColor(c1, c2, t) { return new SColor( round(lerp(c1._r, c2._r, t)), round(lerp(c1._g, c2._g, t)), round(lerp(c1._b, c2._b, t)), round(lerp(c1._a, c2._a, t)) ) } // ─── RESOLVE COLOR ARGS → CSS STRING ─── function _resolveColor(args) { if (args.length === 0) return null const a0 = args[0] if (a0 instanceof SColor) return a0.toString() if (typeof a0 === 'string') return a0 if (args.length >= 3) return _hsbToCSS(args[0], args[1], args[2], args[3]) // single value = grayscale brightness (0-100 in HSB), or >100 clamps to white const bri = constrain(a0, 0, 255) const v = round(bri / 100 * 255) const av = args[1] !== undefined ? constrain(args[1], 0, 100) / 100 : 1 return `rgba(${min(v, 255)},${min(v, 255)},${min(v, 255)},${av})` } // ─── GRAPHICS BUFFER ─── class PGraphics { constructor(w, h, mode) { this.width = w this.height = h this._isWebGL = (mode === WEBGL) this.canvas = document.createElement('canvas') this.canvas.width = w this.canvas.height = h if (this._isWebGL) { this._gl = this.canvas.getContext('webgl', { premultipliedAlpha: false, preserveDrawingBuffer: true }) this.drawingContext = this._gl this._setupGL() } else { this.drawingContext = this.canvas.getContext('2d') this.drawingContext.imageSmoothingEnabled = false } this._rectMode = CORNER this._fillCSS = '#000000' this._strokeCSS = null this._strokeW = 1 this._doFill = true this._doStroke = true this._tintAlpha = 1 this._tintColor = null this._erasing = false this._shapeVerts = null this._shapeBeziers = null this._activeShader = null } // ── GL setup ── _setupGL() { const gl = this._gl // p5 vertex shader expects aPosition in 0-1 range (maps to clip space via *2-1) const verts = new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0]) const texCoords = new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]) this._quadPos = gl.createBuffer() gl.bindBuffer(gl.ARRAY_BUFFER, this._quadPos) gl.bufferData(gl.ARRAY_BUFFER, verts, gl.STATIC_DRAW) this._quadTex = gl.createBuffer() gl.bindBuffer(gl.ARRAY_BUFFER, this._quadTex) gl.bufferData(gl.ARRAY_BUFFER, texCoords, gl.STATIC_DRAW) this._inputTex = gl.createTexture() } createShader(vertSrc, fragSrc) { const gl = this._gl function compile(type, src) { const s = gl.createShader(type) gl.shaderSource(s, src) gl.compileShader(s) if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) { console.error('Shader compile error:', gl.getShaderInfoLog(s)) } return s } const vs = compile(gl.VERTEX_SHADER, vertSrc) const fs = compile(gl.FRAGMENT_SHADER, fragSrc) const prog = gl.createProgram() gl.attachShader(prog, vs) gl.attachShader(prog, fs) gl.linkProgram(prog) if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) { console.error('Shader link error:', gl.getProgramInfoLog(prog)) } return { _prog: prog, _gl: gl, _uniforms: {}, setUniform(name, val) { gl.useProgram(prog) let loc = this._uniforms[name] if (!loc) { loc = gl.getUniformLocation(prog, name); this._uniforms[name] = loc } if (loc === null) return if (val instanceof PGraphics || (val && val.canvas)) { // Texture gl.activeTexture(gl.TEXTURE0) const pgTex = this._gl._boundTex || this._gl.createTexture() this._gl._boundTex = pgTex gl.bindTexture(gl.TEXTURE_2D, pgTex) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, val.canvas || val) gl.uniform1i(loc, 0) } else if (Array.isArray(val)) { if (val.length === 2) gl.uniform2f(loc, val[0], val[1]) else if (val.length === 3) gl.uniform3f(loc, val[0], val[1], val[2]) else if (val.length === 4) gl.uniform4f(loc, val[0], val[1], val[2], val[3]) } else if (typeof val === 'boolean') { gl.uniform1i(loc, val ? 1 : 0) } else { gl.uniform1f(loc, val) } } } } shader(s) { this._activeShader = s } // Draw fullscreen quad with current shader rect(x, y, w, h) { if (this._isWebGL && this._activeShader) { const gl = this._gl const prog = this._activeShader._prog gl.useProgram(prog) gl.viewport(0, 0, this.width, this.height) const posLoc = gl.getAttribLocation(prog, 'aPosition') if (posLoc >= 0) { gl.bindBuffer(gl.ARRAY_BUFFER, this._quadPos) gl.enableVertexAttribArray(posLoc) gl.vertexAttribPointer(posLoc, 3, gl.FLOAT, false, 0, 0) } const texLoc = gl.getAttribLocation(prog, 'aTexCoord') if (texLoc >= 0) { gl.bindBuffer(gl.ARRAY_BUFFER, this._quadTex) gl.enableVertexAttribArray(texLoc) gl.vertexAttribPointer(texLoc, 2, gl.FLOAT, false, 0, 0) } gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4) return } // 2D rect — use path-based fill to match p5's behavior const ctx = this.drawingContext let rx = x, ry = y if (this._rectMode === CENTER) { rx = x - w / 2; ry = y - h / 2 } // Optimization: REPLACE + transparent fill = clearRect (safer across browsers) if (ctx.globalCompositeOperation === 'copy' && this._fillCSS === 'rgba(0,0,0,0)') { ctx.clearRect(rx, ry, w, h) } else { ctx.beginPath() ctx.rect(rx, ry, w, h) if (this._doFill) { ctx.fillStyle = this._fillCSS; ctx.fill() } if (this._doStroke && this._strokeCSS) { ctx.strokeStyle = this._strokeCSS; ctx.lineWidth = this._strokeW; ctx.stroke() } } } // ── 2D Drawing ── pixelDensity() { return 1 } colorMode() { } // noop — always HSB internally noStroke() { this._doStroke = false; this._strokeCSS = null } noFill() { this._doFill = false } fill(...args) { this._doFill = true this._fillCSS = _resolveColor(args) } stroke(...args) { this._doStroke = true this._strokeCSS = _resolveColor(args) } strokeWeight(w) { this._strokeW = w; this.drawingContext.lineWidth = w } blendMode(m) { if (!this._isWebGL) this.drawingContext.globalCompositeOperation = m } rectMode(m) { this._rectMode = m } background(...args) { if (this._isWebGL) return const ctx = this.drawingContext ctx.save() ctx.setTransform(1, 0, 0, 1, 0, 0) if (args.length >= 4 && args[0] === 0 && args[1] === 0 && args[2] === 0 && args[3] === 0) { ctx.clearRect(0, 0, this.width, this.height) } else if (args[0] instanceof SColor) { ctx.fillStyle = args[0].toString() ctx.fillRect(0, 0, this.width, this.height) } else { ctx.fillStyle = _resolveColor(args) ctx.fillRect(0, 0, this.width, this.height) } ctx.restore() } clear() { if (this._isWebGL) { const gl = this._gl; gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT) } else { const ctx = this.drawingContext; ctx.save(); ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.clearRect(0, 0, this.width, this.height); ctx.restore() } } push() { if (!this._isWebGL) { this.drawingContext.save() this._stateStack = this._stateStack || [] this._stateStack.push({ fill: this._fillCSS, stroke: this._strokeCSS, doFill: this._doFill, doStroke: this._doStroke, strokeW: this._strokeW, rectMode: this._rectMode, erasing: this._erasing, tintAlpha: this._tintAlpha, tintColor: this._tintColor }) } } pop() { if (!this._isWebGL) { this.drawingContext.restore() if (this._stateStack && this._stateStack.length) { const s = this._stateStack.pop() this._fillCSS = s.fill; this._strokeCSS = s.stroke; this._doFill = s.doFill; this._doStroke = s.doStroke; this._strokeW = s.strokeW; this._rectMode = s.rectMode; this._erasing = s.erasing; this._tintAlpha = s.tintAlpha; this._tintColor = s.tintColor } } } translate(x, y) { if (!this._isWebGL) this.drawingContext.translate(x, y) } rotate(a) { if (!this._isWebGL) this.drawingContext.rotate(a) } scale(x, y) { if (!this._isWebGL) this.drawingContext.scale(x, y !== undefined ? y : x) } // Erase mode erase() { this._erasing = true this._prevComposite = this.drawingContext.globalCompositeOperation this.drawingContext.globalCompositeOperation = 'destination-out' } noErase() { this._erasing = false this.drawingContext.globalCompositeOperation = this._prevComposite || 'source-over' } // ── Shapes ── beginShape() { this._shapeVerts = []; this._firstVert = true } vertex(x, y) { if (!this._shapeVerts) return this._shapeVerts.push({ type: 'v', x, y }) } bezierVertex(x2, y2, x3, y3, x4, y4) { if (!this._shapeVerts) return this._shapeVerts.push({ type: 'b', x2, y2, x3, y3, x4, y4 }) } curveVertex(x, y) { if (!this._shapeVerts) return this._shapeVerts.push({ type: 'c', x, y }) } endShape(mode) { if (!this._shapeVerts || this._shapeVerts.length === 0) return const ctx = this.drawingContext ctx.beginPath() // Separate handling for curve vertices const allCurve = this._shapeVerts.every(v => v.type === 'c') if (allCurve && this._shapeVerts.length >= 4) { this._drawCatmullRom(ctx, this._shapeVerts.map(v => ({ x: v.x, y: v.y }))) } else { let first = true for (const v of this._shapeVerts) { if (v.type === 'v') { if (first) { ctx.moveTo(v.x, v.y); first = false } else ctx.lineTo(v.x, v.y) } else if (v.type === 'b') { ctx.bezierCurveTo(v.x2, v.y2, v.x3, v.y3, v.x4, v.y4) } } } if (mode === CLOSE || mode === 'close') ctx.closePath() if (this._doFill) { ctx.fillStyle = this._fillCSS; ctx.fill() } if (this._doStroke && this._strokeCSS) { ctx.strokeStyle = this._strokeCSS; ctx.lineWidth = this._strokeW; ctx.stroke() } this._shapeVerts = null } _drawCatmullRom(ctx, pts) { // Catmull-Rom → cubic bezier conversion // p5 curveVertex expects: first and last points are control-only if (pts.length < 4) return ctx.moveTo(pts[1].x, pts[1].y) for (let i = 0; i < pts.length - 3; i++) { const p0 = pts[i], p1 = pts[i + 1], p2 = pts[i + 2], p3 = pts[i + 3] ctx.bezierCurveTo( p1.x + (p2.x - p0.x) / 6, p1.y + (p2.y - p0.y) / 6, p2.x - (p3.x - p1.x) / 6, p2.y - (p3.y - p1.y) / 6, p2.x, p2.y ) } } ellipse(x, y, w, h) { if (h === undefined) h = w const ctx = this.drawingContext ctx.beginPath() ctx.ellipse(x, y, w / 2, h / 2, 0, 0, TWO_PI) if (this._doFill) { ctx.fillStyle = this._fillCSS; ctx.fill() } if (this._doStroke && this._strokeCSS) { ctx.strokeStyle = this._strokeCSS; ctx.lineWidth = this._strokeW; ctx.stroke() } } circle(x, y, d) { this.ellipse(x, y, d, d) } line(x1, y1, x2, y2) { const ctx = this.drawingContext ctx.beginPath() ctx.moveTo(x1, y1) ctx.lineTo(x2, y2) if (this._doStroke && this._strokeCSS) { ctx.strokeStyle = this._strokeCSS; ctx.lineWidth = this._strokeW; ctx.stroke() } } triangle(x1, y1, x2, y2, x3, y3) { const ctx = this.drawingContext ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.lineTo(x3, y3); ctx.closePath() if (this._doFill) { ctx.fillStyle = this._fillCSS; ctx.fill() } if (this._doStroke && this._strokeCSS) { ctx.strokeStyle = this._strokeCSS; ctx.lineWidth = this._strokeW; ctx.stroke() } } quad(x1, y1, x2, y2, x3, y3, x4, y4) { const ctx = this.drawingContext ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.lineTo(x3, y3); ctx.lineTo(x4, y4); ctx.closePath() if (this._doFill) { ctx.fillStyle = this._fillCSS; ctx.fill() } if (this._doStroke && this._strokeCSS) { ctx.strokeStyle = this._strokeCSS; ctx.lineWidth = this._strokeW; ctx.stroke() } } // ── Image / tint ── image(src, x, y, w, h) { if (!src) return const ctx = this.drawingContext const prevAlpha = ctx.globalAlpha // ALWAYS set globalAlpha — prevent stale values from leaking across operations ctx.globalAlpha = this._tintAlpha // Handle slice references from get() — avoids creating temp canvases if (src._isSlice) { ctx.drawImage(src.canvas, src._sx, src._sy, src._sw, src._sh, x || 0, y || 0, w || src._sw, h || src._sh) ctx.globalAlpha = prevAlpha return } const srcCanvas = (src instanceof PGraphics) ? src.canvas : src if (this._tintColor && this._tintAlpha > 0) { // Color tint: draw to temp, multiply with color if (!this._tintCanvas || this._tintCanvas.width !== this.width || this._tintCanvas.height !== this.height) { this._tintCanvas = document.createElement('canvas') this._tintCanvas.width = this.width; this._tintCanvas.height = this.height this._tintCtx = this._tintCanvas.getContext('2d') } const tc = this._tintCtx tc.clearRect(0, 0, this.width, this.height) tc.globalCompositeOperation = 'source-over' tc.globalAlpha = 1.0 tc.drawImage(srcCanvas, x || 0, y || 0, w || srcCanvas.width || this.width, h || srcCanvas.height || this.height) tc.globalCompositeOperation = 'source-atop' tc.fillStyle = this._tintColor tc.fillRect(0, 0, this.width, this.height) ctx.drawImage(this._tintCanvas, 0, 0) } else if (w !== undefined && h !== undefined) { ctx.drawImage(srcCanvas, x || 0, y || 0, w, h) } else { ctx.drawImage(srcCanvas, x || 0, y || 0) } ctx.globalAlpha = prevAlpha } tint(...args) { if (args.length === 2 && args[0] === 255) { // tint(255, alpha) — just alpha this._tintAlpha = args[1] / 255 this._tintColor = null } else if (args.length === 4) { // tint(r, g, b, a) this._tintColor = `rgb(${args[0]},${args[1]},${args[2]})` this._tintAlpha = args[3] / 255 } else if (args.length === 1) { this._tintAlpha = args[0] / 255 this._tintColor = null } } noTint() { this._tintAlpha = 1; this._tintColor = null } // p5's get(x,y,w,h) — return lightweight slice reference // Avoids creating a new canvas for every call (critical for corrupt effect performance) get(x, y, w, h) { const srcCanvas = this.canvas return { canvas: srcCanvas, _isSlice: true, _sx: x, _sy: y, _sw: w, _sh: h, width: w, height: h } } // p5 color() on buffer instance color(...args) { return color(...args) } lerpColor(c1, c2, t) { return lerpColor(c1, c2, t) } // Cleanup — p5 removes from DOM, we just release the canvas remove() { if (this._isWebGL && this._gl) { const ext = this._gl.getExtension('WEBGL_lose_context') if (ext) ext.loseContext() } this.canvas.width = 0 this.canvas.height = 0 } } window.PGraphics = PGraphics // ─── MAIN CANVAS + LIFECYCLE ─── var _mainPG = null var _mainCanvas = null var _displayCanvas = null var _displayCtx = null var width = 0, height = 0 var frameCount = 0 var _targetFPS = 60 var _frameDuration = 1000 / 60 var _lastFrame = 0 var _running = true var key = '' // When null, millis() reads real wall-clock time (live views). The capture // fast-forward (see the DOMContentLoaded handler) sets this to a virtual // accumulator so millis() and frameCount advance in lockstep. var _virtualClock = null function createCanvas(w, h) { _mainPG = new PGraphics(w, h) _mainCanvas = _mainPG.canvas _mainCanvas.id = 'defaultCanvas0' // The 5:7 render canvas stays off-DOM. The DOM shows a square display // canvas holding the top 1:1 crop (blitted in _presentFrame), so // right-click copy / drag-out yields the same square the viewer sees. _displayCanvas = document.createElement('canvas') _displayCanvas.width = w _displayCanvas.height = w _displayCtx = _displayCanvas.getContext('2d') _displayCtx.imageSmoothingEnabled = false document.body.appendChild(_displayCanvas) width = w; height = h _mainPG.drawingContext.imageSmoothingEnabled = false window.drawingContext = _mainPG.drawingContext return { elt: _mainCanvas, canvas: _mainCanvas } } // Present the finished frame: copy the top square of the offscreen render // canvas onto the DOM display canvas. function _presentFrame() { if (!_displayCtx || !_mainCanvas) return _displayCtx.drawImage(_mainCanvas, 0, 0, width, width, 0, 0, width, width) } function createGraphics(w, h, mode) { return new PGraphics(w, h, mode) } function pixelDensity() { return 1 } function frameRate(fps) { _targetFPS = fps; _frameDuration = 1000 / fps } function colorMode() { } // HSB always function millis() { return _virtualClock === null ? performance.now() : _virtualClock } // Global drawing proxies — delegate to main canvas function background(...a) { _mainPG.background(...a) } function image(...a) { _mainPG.image(...a) } function fill(...a) { _mainPG.fill(...a) } function noFill() { _mainPG.noFill() } function stroke(...a) { _mainPG.stroke(...a) } function noStroke() { _mainPG.noStroke() } function strokeWeight(w) { _mainPG.strokeWeight(w) } function push() { _mainPG.push() } function pop() { _mainPG.pop() } function translate(x, y) { _mainPG.translate(x, y) } function rotate(a) { _mainPG.rotate(a) } function blendMode(m) { _mainPG.blendMode(m) } function rectMode(m) { _mainPG.rectMode(m) } function beginShape() { _mainPG.beginShape() } function vertex(x, y) { _mainPG.vertex(x, y) } function endShape(m) { _mainPG.endShape(m) } function rect(x, y, w, h) { _mainPG.rect(x, y, w, h) } function ellipse(x, y, w, h) { _mainPG.ellipse(x, y, w, h) } function circle(x, y, d) { _mainPG.circle(x, y, d) } function line(x1, y1, x2, y2) { _mainPG.line(x1, y1, x2, y2) } // ─── DRAW LOOP ─── function _loop(ts) { if (!_running) return if (ts - _lastFrame >= _frameDuration) { _lastFrame = ts try { if (typeof draw === 'function') { frameCount++; draw(); _presentFrame() } } catch (e) { console.error('Draw error:', e) } } requestAnimationFrame(_loop) } window.addEventListener('DOMContentLoaded', () => { try { if (typeof setup === 'function') setup() } catch (e) { console.error('Setup error:', e) } // Capture fast-forward: under the capture UA, advance straight to the // snapshot frame synchronously instead of waiting ~4s of real-time RAF. // The particle systems accumulate per frame, so we still run every step — // just back-to-back with no throttle. The virtual clock ticks +one frame // per step so millis()-driven motion (breathing, crack cycle) lands at the // same phase a live viewer sees at the snapshot frame, deterministically // and with no RAF jitter. The draw loop's own freeze/ready logic fires on // the final step. Live/collector views skip this branch and animate normally. if (typeof window.$art !== 'undefined' && window.$art.captureMode) { const target = (typeof SNAPSHOT_FRAME === 'number') ? SNAPSHOT_FRAME : 96 _virtualClock = 0 while (frameCount < target && typeof draw === 'function') { _virtualClock += _frameDuration frameCount++ try { draw() } catch (e) { console.error('Draw error:', e) } } _presentFrame() if (typeof DEBUG !== 'undefined' && DEBUG) { console.log(`%c⏩ CAPTURE — fast-forwarded to frame ${frameCount}, frozen`, 'color:#00FFFF; font-weight:bold;') } // The canvas now holds the fully-rendered hero frame. Stop here and do // NOT restart RAF: freezeMode gates the update/breath code but not the // per-frame frameCount++, so any uncached display-path read of // frameCount/millis()/random() (e.g. eye flicker, aura shimmer) would // keep animating on subsequent ticks. Leaving the loop off makes the // snapshot a genuinely static single frame. _running = false return } requestAnimationFrame(_loop) }) window.addEventListener('keydown', (e) => { key = e.key if (e.key === ' ' || e.key === 'Escape') e.preventDefault() if (typeof keyPressed === 'function') keyPressed() }) </script> <style> html, body { margin: 0; padding: 0; height: 100%; width: 100%; background: #0b0b0b; overflow: hidden; display: flex; align-items: center; justify-content: center; } canvas { display: block; width: min(100vw, 100vh) !important; height: min(100vw, 100vh) !important; image-rendering: pixelated; image-rendering: crisp-edges; -webkit-image-rendering: pixelated; } </style> </head> <body> <!-- TL Gen Art — rendering pipeline only. Satari owns its own seeding/RNG; this provides just the snapshot + traits DOM hooks that survive in a captured HTML snapshot (Cloudflare Browser Rendering). No seeding/randomness here. --> <script> const $art = (function () { function writeHiddenJSON(id, data) { let el = document.getElementById(id); if (!el) { el = document.createElement("script"); el.type = "application/json"; // not executed, not rendered, survives in HTML el.id = id; (document.body || document.documentElement).appendChild(el); } el.textContent = JSON.stringify(data); return el; } let currentTraits = null; // { Palette: "Sunset", Layers: 5 } -> OpenSea [{trait_type, value}, ...] function setTraits(traits) { if (!traits || typeof traits !== "object" || Array.isArray(traits)) throw new Error("$art.setTraits: expects a plain object of { name: value }"); currentTraits = Object.keys(traits).map((k) => ({ trait_type: k, value: traits[k] })); writeHiddenJSON("art-traits", currentTraits); return currentTraits; } // Capture environment detection. Cloudflare Browser Rendering is configured to send // this sentinel user agent; live collector views never match. Keep in sync with infra. const CAPTURE_UA = "tl-gen-art"; const captureMode = typeof navigator !== "undefined" && new RegExp(CAPTURE_UA).test(navigator.userAgent || ""); // Append a hidden #art-snapshot-ready marker so Cloudflare can waitForSelector on it. // Optional freeze callback runs ONLY under the capture UA (live views keep animating). function snapshot(onCapture) { if (captureMode && typeof onCapture === "function") onCapture(); let el = document.getElementById("art-snapshot-ready"); if (!el) { el = document.createElement("div"); el.id = "art-snapshot-ready"; el.style.display = "none"; (document.body || document.documentElement).appendChild(el); } return el; } return { setTraits, getTraits: () => currentTraits, snapshot, captureMode }; })(); window.$art = $art; </script> <script> let CURRENT_SEED = null const DEBUG = true // Set false for production — disables console logging let freezeMode = false // Snapshot: under the capture UA, settle to this frame, freeze, then mark ready. // 96 frames = the same 4s of formation (at the live 24fps) a viewer perceives. // Live views reach it in real time; the capture path fast-forwards through the // 96 accumulating frames synchronously on a virtual clock (see the // DOMContentLoaded handler), so both frame-count structure and millis()-driven // motion land at exactly the 4-second phase — instantly and deterministically. let _snapshotMarked = false const SNAPSHOT_FRAME = 96 let bwMode = false // Driven by Etched palette trait; set in setup/regenerate. const ASPECT_RATIO = 1.4 // 5:7 internal render — displayed as 1:1 top crop let FORM_BREATH_HZ = 0.4 let FORM_BREATH_AMP = 10 let BREATH_DELAY_PART1 = 0.30 let BREATH_DELAY_MASK = 0.20 let FILL_ANIMATION_SPEED_MULT = 2.2 const QUALITY_TIERS = [ { minWidth: 0, canvasW: 200, fps: 6, maxAnims: 5, gridCols: 40, noise: false }, { minWidth: 200, canvasW: 300, fps: 12, maxAnims: 10, gridCols: 60, noise: false }, { minWidth: 400, canvasW: 400, fps: 18, maxAnims: 15, gridCols: 75, noise: true }, { minWidth: 600, canvasW: 500, fps: 24, maxAnims: 20, gridCols: 86, noise: true }, ] let _currentTier = null let _tierMaxAnims = 20 let _tierGridDensity = 86 let _tierNoiseOverride = null // null = use trait value, false = force off let _pendingTier = null // Deferred tier change — applied at start of next frame function headCenterY() { return width / 2 } // center of head zone let _designScale = 1 // width / 750 — used to scale pixel values to current canvas size let _cachedPart2Offset = 0 let _cachedPart1Offset = 0 let _cachedMaskOffset = 0 let _cachedEyeOpacity = 0 let _cachedOmniaCol = null let _cachedShaderTime = 0 let _breathStartMillis = null function seedFromString(str) { let h = 2166136261 >>> 0 for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i) h = Math.imul(h, 16777619) } h += h << 13 h ^= h >>> 7 h += h << 3 h ^= h >>> 17 h += h << 5 return h >>> 0 } function randomU32() { if (window.crypto && crypto.getRandomValues) { const a = new Uint32Array(1) crypto.getRandomValues(a) return a[0] >>> 0 } return (Math.random() * 0xffffffff) >>> 0 } // ─── SEED SOURCE ─── // Priority chain: // 1. ?tokenId=<n>&blockhash=<hex> (production render — both required) // 2. ?seed=<uint32> (manual preview / debugging) // 3. randomU32() (local fallback) function readSeed() { const params = new URLSearchParams(window.location.search) // 1. Production render: seed deterministically from the token id + block hash // injected via query params. Both must be present. We hash the combined // string into the existing uint32 RNG pipeline so the same // (tokenId, blockhash) pair always yields the same seed. if (params.has("tokenId") && params.has("blockhash")) { const tokenId = params.get("tokenId") const blockhash = params.get("blockhash") CURRENT_SEED = seedFromString(tokenId + "_" + blockhash) return } // 2. Manual override via URL query. if (params.has("seed")) { CURRENT_SEED = Number(params.get("seed")) >>> 0 return } // 3. Random fallback for local preview. CURRENT_SEED = randomU32() } function applySeed() { randomSeed(CURRENT_SEED) noiseSeed(CURRENT_SEED) } function mulberry32(a) { return () => { let t = (a += 0x6d2b79f5) | 0 t = Math.imul(t ^ (t >>> 15), t | 1) t ^= t + Math.imul(t ^ (t >>> 7), t | 61) return (t ^ (t >>> 14)) >>> 0 // raw uint32, not float } } function makeTraitRNG(tag) { return mulberry32(seedFromString(tag + "_" + CURRENT_SEED)) } function pickWeightedWith(rng, table) { const raw = rng() let sum = 0 for (const [, w] of table) sum += w let r = Math.floor(raw * sum / 4294967296), acc = 0 for (const [v, w] of table) { acc += w if (r < acc) return v } return table.at(-1)[0] } // ─── POST-MINT MUTATIONS ─── // Pieces accumulate state after mint via the kill mechanic. Mutations are // fetched from external storage (Transient Labs custom tooling at production; // returns zeros when unavailable for local preview / fresh-mint state). // Shape: // { // killCount: <int>, // total kills → Slain (every kill ticks +1) // rememberingCount: <int>, // trait-inheritance kills → Remembering // sameHouseKillCount: <int>, // in-house kills → derives Doctrine (with killCount) // palette?, energy?, sight?, iris?, scent?, voice?, ornament? // } // Mutation keys accepted as individual, readable URL params (dev / sacrifice // preview), e.g. ?killCount=1&rememberingCount=1&sameHouseKillCount=2&palette=Toxin // Graftable traits: Palette, Aura, Energy, Sight, Iris, Scent, Voice, Ornament. // killCount / rememberingCount / sameHouseKillCount are automatic kill outcomes, // not stealable traits. Doctrine is derived from killCount + sameHouseKillCount (see below). const MUT_PARAM_KEYS = ["killCount", "rememberingCount", "sameHouseKillCount", "palette", "aura", "energy", "sight", "iris", "scent", "voice", "ornament"] function getMutations() { // Priority order: // 1. tokenData.mutations (production: on-chain values via Transient Labs) // 2. individual ?killCount=&scent=&palette=… params (readable dev/sacrifice overrides) // 3. fresh-mint defaults (zeros) if (typeof tokenData !== "undefined" && tokenData && tokenData.mutations) { return _normalizeMutations(tokenData.mutations) } // URL-param fallbacks (dev/tester only; production never uses these paths). if (typeof window !== "undefined" && window.location && window.location.search) { const usp = new URLSearchParams(window.location.search) // 2. Flat per-trait params. _normalizeMutations coerces the string values // ("1" -> 1 via | 0; trait strings pass through unchanged). if (MUT_PARAM_KEYS.some((k) => usp.has(k))) { const flat = {} for (const k of MUT_PARAM_KEYS) { if (usp.has(k)) flat[k] = usp.get(k) } return _normalizeMutations(flat) } } return _normalizeMutations({}) } // Canonical values for graftable traits, so override values are accepted // case-insensitively (e.g. sight=mono → "Mono") — the renderer matches these // strings exactly. Keep in sync with the trait tables in buildTraits(). Energy // and Aura carry a few internal names the renderer keys on; MUT_PUBLISHED_TO_INTERNAL // (below) also accepts their collector-facing names. Unknown values pass through. const MUT_CANONICAL = { palette: ["Flux", "Acid", "Surge", "Infra", "Toxin", "Auric", "Jazz", "Corrupt", "Vapor", "Tropic", "Wraith", "Riptide", "Petal", "Sage", "Lilac", "Crimson", "Abyss", "Canopy", "Amber", "Haze", "Current", "Flare", "Coast", "Flora", "Astra", "Dune", "Polar", "Clash", "Royal", "Axis", "Blood", "Signal", "Dual", "Stark", "Etched"], aura: ["Calm", "Radiant", "Skyfall", "Melt", "CorruptMask", "CorruptRealm", "Seeds", "Motes", "Prisms", "Vent", "Kintsugi"], energy: ["Supernova", "ExpandingCircles", "ExpandingSquares", "VerticalStripes", "Gradient", "HorizontalStripes", "WavyLines", "Sparkles", "VoronoiCells", "Shockwave"], sight: ["Core", "Echo", "Verge", "Slate", "Vert", "Signal", "Pierce", "Cross", "Cut", "Lune", "Mono", "Teardrop"], iris: ["Null", "Abyss", "Fury", "Plasma", "Omnia"], scent: ["Sealed", "Aero", "Sego", "Duos", "Jewel", "Dot", "Aexo", "Tri"], voice: ["Still", "Prowl", "Forge", "Sunder", "Hollow", "Devour", "Viper", "Bind", "Downfall", "Stun", "Grate", "Ease"], ornament: ["Bare", "Links", "Amp", "Crest", "Bestia", "Root", "Gauge", "Thorns", "Tandem", "Spike"], // Doctrine is not an input trait — it is derived from killCount + sameHouseKillCount. } // Energy and Aura are published to collectors under names that differ from // the renderer's internal values. Production passes the collector-facing // (on-chain) names, so accept those here and translate to the internal // names the renderer keys on. Internal names still resolve via MUT_CANONICAL. const MUT_PUBLISHED_TO_INTERNAL = { energy: { Pulse: "ExpandingCircles", Lattice: "ExpandingSquares", Beam: "VerticalStripes", Bleed: "Gradient", Strata: "HorizontalStripes", Frequency: "WavyLines", Spark: "Sparkles", Plasma: "VoronoiCells", }, aura: { Broken: "CorruptMask", Fragments: "CorruptRealm" }, } function _canonMut(trait, value) { if (typeof value !== "string") return value const aliases = MUT_PUBLISHED_TO_INTERNAL[trait] if (aliases) { const hit = Object.keys(aliases).find((k) => k.toLowerCase() === value.toLowerCase()) if (hit) return aliases[hit] } const list = MUT_CANONICAL[trait] if (!list) return value const lc = value.toLowerCase() return list.find((v) => v.toLowerCase() === lc) || value } function _normalizeMutations(m) { return { // killCount / rememberingCount / sameHouseKillCount are the only accepted // counter params (production sends exactly these). String values from // URLSearchParams ("2") coerce to int via `| 0`; absent → 0. slain: (m.killCount | 0) >>> 0, remembering: (m.rememberingCount | 0) >>> 0, sameHouseKillCount: (m.sameHouseKillCount | 0) >>> 0, palette: _canonMut("palette", m.palette) || null, aura: _canonMut("aura", m.aura) || null, energy: _canonMut("energy", m.energy) || null, sight: _canonMut("sight", m.sight) || null, iris: _canonMut("iris", m.iris) || null, scent: _canonMut("scent", m.scent) || null, voice: _canonMut("voice", m.voice) || null, ornament: _canonMut("ornament", m.ornament) || null, } } // Doctrine is derived from kill counts (not a graftable trait). With no kills it // is Idle. Otherwise it reflects the balance between out-of-house and in-house // kills: diff = differentHouseKillCount - sameHouseKillCount. // diff >= +5 → Crusader, >= +2 → Devoted, |diff| < 2 → Sovereign, // <= -2 → Dissenting, <= -5 → Heretic. function doctrineFromKills(killCount, sameHouseKillCount) { if (killCount <= 0) return "Idle" const differentHouseKillCount = killCount - sameHouseKillCount const diff = differentHouseKillCount - sameHouseKillCount if (diff >= 5) return "Crusader" if (diff >= 2) return "Devoted" if (diff <= -5) return "Heretic" if (diff <= -2) return "Dissenting" return "Sovereign" } // Apply mutation overlay to a freshly-built traits object. Respects compatibility // rules: incompatible inheritances are marked "(inert)" in the trait field so // they show in metadata but don't break render assumptions. function applyMutations(result, mut) { result.slain = mut.slain result.remembering = mut.remembering // Palette: re-graft as-is (including Etched). // Palette graft: when adopting Etched, the underlying _basePalette stays as // the killer's own original base (preserves their original ash beneath the // new Etched skin). When grafting a non-Etched palette, the killer's // _basePalette is cleared (since they're no longer Etched). if (mut.palette) { if (mut.palette === "Etched") { // Adopt Etched override. _basePalette = whatever killer already had as // their underlying palette: either their existing _basePalette (if they // were already Etched), or their current paletteName (if they weren't). if (!result._basePalette) { result._basePalette = result.paletteName } result.paletteName = "Etched" } else { // Adopt a non-Etched palette: clear any Etched state, set new palette. result._basePalette = null result.paletteName = mut.palette } } // Energy → fillVariant if (mut.energy) result.fillVariant = mut.energy // Sight, Iris, Scent, Voice: free swaps if (mut.sight) result.sight = mut.sight if (mut.iris) result.iris = mut.iris if (mut.scent) result.scent = mut.scent // Aura: free swap. Note: the Helios+Radiant override is a renderer invariant — // grafting Radiant onto a Helios killer converts to Seeds or Calm here too, // matching mint-time behavior. if (mut.aura) { let newAura = mut.aura if (result.displayedArchetype === "Helios" && newAura === "Radiant") { // Deterministic flip — use the seed's existing entropy via a fresh tag. const _r = makeTraitRNG("auraGraftHeliosRadiant") newAura = _r() < 2147483648 ? "Seeds" : "Calm" } result.aura = newAura } if (mut.voice) result.voice = mut.voice // Doctrine: derived from kill counts (Idle when no kills; otherwise the balance // of out-of-house vs in-house kills — see doctrineFromKills). result.doctrine = doctrineFromKills(mut.slain, mut.sameHouseKillCount) // Ornament: respect bestiaCompatible / Vanta-Crest rules if (mut.ornament) { const bestiaCompatible = ["Havoc", "Sentinel", "Apex", "Rogue", "Riven", "Guard", "Nomad", "Blade", "Prime"] if (mut.ornament === "Bestia" && !bestiaCompatible.includes(result.displayedArchetype)) { result.ornament = "Bestia (inert)" } else if (mut.ornament === "Crest" && result.displayedArchetype === "Vanta") { result.ornament = "Crest (inert)" } else { result.ornament = mut.ornament } } return result } function buildTraits() { const rArch = makeTraitRNG("archetype") const rPal = makeTraitRNG("palette") const rEye = makeTraitRNG("sight") const rMou = makeTraitRNG("voice") const rNose = makeTraitRNG("nose") const rOrn = makeTraitRNG("ornament") const rCut = makeTraitRNG("cutouts") const rRealm = makeTraitRNG("realm") const rOr = makeTraitRNG("orient") const rEdge = makeTraitRNG("edgeStyle") const rEyeColor = makeTraitRNG("eyeColor") const rOrigin = makeTraitRNG("originTopology") const rGrowth = makeTraitRNG("growthProfile") const rBlend = makeTraitRNG("blendSchema") const rSize = makeTraitRNG("sizeEnvelope") const rSpawn = makeTraitRNG("spawnRhythm") const rFillVariant = makeTraitRNG("fillVariant") const rOrbit = makeTraitRNG("orbitingElements") const rOrbitSpeed = makeTraitRNG("orbitalSpeed") const rWavyDir = makeTraitRNG("wavyDirection") const rExpandRotation = makeTraitRNG("expandingRotation") const rExpandPersist = makeTraitRNG("expandingPersist") const rApplyNoise = makeTraitRNG("applyNoise") const rInkMode = makeTraitRNG("inkMode") const rSymbolSize = makeTraitRNG("symbolSize") const rSpacecraft = makeTraitRNG("spacecraft") let paletteName = pickWeightedWith(rPal, [ ["Flux", 10], ["Acid", 10], ["Surge", 10], ["Infra", 10], ["Toxin", 10], ["Auric", 10], ["Jazz", 10], ["Corrupt", 10], ["Vapor", 10], ["Tropic", 10], ["Wraith", 10], ["Riptide", 10], ["Petal", 10], ["Sage", 10], ["Lilac", 10], ["Crimson", 10], ["Abyss", 10], ["Canopy", 10], ["Amber", 10], ["Haze", 10], ["Current", 10], ["Flare", 10], ["Coast", 10], ["Flora", 10], ["Astra", 10], ["Dune", 10], ["Polar", 10], ["Clash", 10], ["Royal", 10], ["Axis", 10], ["Bl