memoscan
← all memos

Memo 0xab3c9506…06970d on Ethereum

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)