memoscan
← all memos

Memo 0x4a7cda92…c69e18 on Ethereum

r in 3D");for(var n=this.infos.length-1;0<=n;--n){var s=this.infos[n];if(s.index+e<o){r=(t=s).imageData;break}}if(!t){try{r=new ImageData(this.width,this.height)}catch(e){var i=document.getElementsByTagName("canvas")[0],a=!i,l=(i||((i=document.createElement("canvas")).style.display="none",document.body.appendChild(i)),i.getContext("2d"));l&&(r=l.createImageData(this.width,this.height)),a&&document.body.removeChild(i)}this.infos.push(t={index:0,imageData:r})}l=t.index;return t.index+=e,r._dirty=!0,{imageData:r,index:l}}}]),r}();function z(e,t,r,o,n){var s=e.imageData.data,e=4*e.index++;s[e++]=t,s[e++]=r,s[e++]=o,s[+e]=n}var H=Math.sqrt(3),M=function(){function t(e){G(this,t),this.font=e,this.strokeImageInfos=new l(64,64),this.colDimImageInfos=new l(64,64),this.rowDimImageInfos=new l(64,64),this.colCellImageInfos=new l(64,64),this.rowCellImageInfos=new l(64,64),this.glyphInfos={}}return V(t,[{key:"getGlyphInfo",value:function(e){var t=this.glyphInfos[e.index];if(!t){var r=e.getBoundingBox(),o=r.x1,n=r.y1,s=r.x2-o,i=r.y2-n,a=e.path.commands;if(0==s||0==i||!a.length)return this.glyphInfos[e.index]={};for(var u=[],c=[],d=[],l=8;0<=l;--l)d.push([]);for(l=8;0<=l;--l)c.push([]);var h,f,p,m,D=function(){function n(e,t,r,o){G(this,n),this.p0=e,this.c0=t,this.c1=r,this.p1=o}return V(n,[{key:"toQuadratic",value:function(){return{x:this.p0.x,y:this.p0.y,x1:this.p1.x,y1:this.p1.y,cx:(3*(this.c0.x+this.c1.x)-(this.p0.x+this.p1.x))/4,cy:(3*(this.c0.y+this.c1.y)-(this.p0.y+this.p1.y))/4}}},{key:"quadError",value:function(){return B.default.Vector.sub(B.default.Vector.sub(this.p1,this.p0),B.default.Vector.mult(B.default.Vector.sub(this.c1,this.c0),3)).mag()/2}},{key:"split",value:function(e){var t=B.default.Vector.lerp(this.p0,this.c0,e),r=B.default.Vector.lerp(this.c0,this.c1,e),o=B.default.Vector.lerp(t,r,e),r=(this.c1=B.default.Vector.lerp(this.c1,this.p1,e),this.c0=B.default.Vector.lerp(r,this.c1,e),B.default.Vector.lerp(o,this.c0,e)),e=new n(this.p0,t,o,r);return this.p0=r,e}},{key:"splitInflections",value:function(){var e=B.default.Vector.sub(this.c0,this.p0),t=B.default.Vector.sub(B.default.Vector.sub(this.c1,this.c0),e),r=B.default.Vector.sub(B.default.Vector.sub(B.default.Vector.sub(this.p1,this.c1),e),B.default.Vector.mult(t,2)),o=[],n=t.x*r.y-t.y*r.x;return 0!==n&&0<=(e=(r=e.x*r.y-e.y*r.x)*r-4*n*(e.x*t.y-e.y*t.x))&&(n<0&&(n=-n,r=-r,0),e=(-r+(t=Math.sqrt(e)))/(2*n),0<(r=(-r-t)/(2*n))&&r<1&&(o.push(this.split(r)),e=1-(1-e)/(1-r)),0<e&&e<1&&o.push(this.split(e))),o.push(this),o}}]),n}(),y=!0,g=!1,v=void 0;try{for(var b,_=a[Symbol.iterator]();!(y=(b=_.next()).done);y=!0){var j=b.value,x=(j.x-o)/s,w=(j.y-n)/i;if(!U(h,f,x,w)){switch(j.type){case"M":p=x,m=w;break;case"L":I(h,f,x,w);break;case"Q":var S=(j.x1-o)/s,T=(j.y1-n)/i;L([h,x,S],[f,w,T],{x:h,y:f,cx:S,cy:T});break;case"Z":U(h,f,p,m)?u.push({x:h,y:f}):(I(h,f,p,m),u.push({x:p,y:m}));break;case"C":for(var E=function(e,t,r,o,n,s,i,a){var e=new D(new B.default.Vector(e,t),new B.default.Vector(r,o),new B.default.Vector(n,s),new B.default.Vector(i,a)).splitInflections(),l=[],u=30/H,c=!0,t=!1,r=void 0;try{for(var d,h=e[Symbol.iterator]();!(c=(d=h.next()).done);c=!0){for(var f=d.value,p=[],m=void 0;!(.125<=(m=u/f.quadError()));){var y=Math.pow(m,1/3),g=f.split(y),v=f.split(1-y/(1-y));l.push(g),p.push(f),f=v}m<1&&l.push(f.split(.5)),l.push(f),Array.prototype.push.apply(l,p.reverse())}}catch(e){t=!0,r=e}finally{try{c||null==h.return||h.return()}finally{if(t)throw r}}return l}(h,f,(j.x1-o)/s,(j.y1-n)/i,(j.x2-o)/s,(j.y2-n)/i,x,w),M=0;M<E.length;M++){var k=E[M].toQuadratic();L([k.x,k.x1,k.cx],[k.y,k.y1,k.cy],k)}break;default:throw new Error("unknown command type: ".concat(j.type))}h=x,f=w}}}catch(e){g=!0,v=e}finally{try{y||null==_.return||_.return()}finally{if(g)throw v}}for(var O=u.length,C=this.strokeImageInfos.findImage(O),F=C.index,P=0;P<O;++P){var A=u[P];z(C,R(A.x),R(A.y),R(A.cx),R(A.cy))}(t=this.glyphInfos[e.index]={glyph:e,uGlyphRect:[r.x1,-r.y1,r.x2,-r.y2],strokeImageInfo:C,strokes:u,colInfo:N(d,this.colDimImageInfos,this.colCellImageInfos),rowInfo:N(c,this.rowDimImageInfos,this.rowCellImageInfos)}).uGridOffset=[t.colInfo.dimOffset,t.rowInfo.dimOffset]}return t;function L(e,t,r){var o=u.length;function n(e,t,r){for(var o=e.length;0<o--;){var n=e[o];n<t&&(t=n),r<n&&(r=n)}return{min:t,max:r}}u.push(r);for(var r=n(e,1,0),e=Math.max(Math.floor(9*r.min-.5),0),s=Math.min(Math.ceil(9*r.max+.5),9),i=e;i<s;++i)d[i].push(o);for(var r=n(t,1,0),e=Math.max(Math.floor(9*r.min-.5),0),a=Math.min(Math.ceil(9*r.max+.5),9),l=e;l<a;++l)c[l].push(o)}function R(e){return(e=(r=255)*e)<(t=0)?t:r<e?r:e;var t,r}function I(e,t,r,o){L([e,r],[t,o],{x:e,y:t,cx:(e+r)/2,cy:(t+o)/2})}function U(e,t,r,o){return Math.abs(r-e)<1e-5&&Math.abs(o-t)<1e-5}function N(e,t,r){for(var o=e.length,n=t.findImage(o),t=n.index,s=0,i=0;i<o;++i)s+=e[i].length;for(var a=r.findImage(s),l=0;l<o;++l){var u=e[l],c=u.length,d=a.index;z(n,d>>7,127&d,c>>7,127&c);for(var h=0;h<c;++h){var f=u[h]+F;z(a,f>>7,127&f,0,0)}}return{cellImageInfo:a,dimOffset:t,dimImageInfo:n}}}}]),t}();B.default.RendererGL.prototype._renderText=function(e,t,r,o,n){if(this._textFont&&"string"!=typeof this._textFont){if(!(n<=o)&&this._doFill){if(this._isOpenType()){e.push();var n=this._doStroke,s=this.drawMode,i=(this._doStroke=!1,this.drawMode=E.TEXTURE,this._textFont.font),a=(a=this._textFont._fontInfo)||(this._textFont._fontInfo=new M(i)),r=this._textFont._handleAlignment(this,t,r,o),o=this._textSize/i.unitsPerEm,l=(this.translate(r.x,r.y,0),this.scale(o,o,1),this.GL),r=!this._defaultFontShader,u=this._getFontShader(),c=(u.init(),u.bindShader(),r&&(u.setUniform("uGridImageSize",[64,64]),u.setUniform("uCellsImageSize",[64,64]),u.setUniform("uStrokeImageSize",[64,64]),u.setUniform("uGridSize",[9,9])),this._applyColorBlend(this.curFillColor),this.retainedMode.geometry.glyph),d=(c||((o=this._textGeom=new B.default.Geometry(1,1,function(){for(var e=0;e<=1;e++)for(var t=0;t<=1;t++)this.vertices.push(new B.default.Vector(t,e,0)),this.uvs.push(t,e)})).computeFaces().computeNormals(),c=this.createBuffers("glyph",o)),!0),r=!1,o=void 0;try{for(var h,f=this.retainedMode.buffers.text[Symbol.iterator]();!(d=(h=f.next()).done);d=!0)h.value._prepareBuffer(c,u)}catch(e){r=!0,o=e}finally{try{d||null==f.return||f.return()}finally{if(r)throw o}}this._bindBuffer(c.indexBuffer,l.ELEMENT_ARRAY_BUFFER),u.setUniform("uMaterialColor",this.curFillColor),l.pixelStorei(l.UNPACK_PREMULTIPLY_ALPHA_WEBGL,!1);try{var p=0,m=null,y=i.stringToGlyphs(t),g=!0,v=!1,b=void 0;try{for(var _,j=y[Symbol.iterator]();!(g=(_=j.next()).done);g=!0){var x,w,S=_.value,T=(m&&(p+=i.getKerningValue(m,S)),a.getGlyphInfo(S));T.uGlyphRect&&(x=T.rowInfo,w=T.colInfo,u.setUniform("uSamplerStrokes",T.strokeImageInfo.imageData),u.setUniform("uSamplerRowStrokes",x.cellImageInfo.imageData),u.setUniform("uSamplerRows",x.dimImageInfo.imageData),u.setUniform("uSamplerColStrokes",w.cellImageInfo.imageData),u.setUniform("uSamplerCols",w.dimImageInfo.imageData),u.setUniform("uGridOffset",T.uGridOffset),u.setUniform("uGlyphRect",T.uGlyphRect),u.setUniform("uGlyphOffset",p),u.bindTextures(),l.drawElements(l.TRIANGLES,6,this.GL.UNSIGNED_SHORT,0)),p+=S.advanceWidth,m=S}}catch(e){v=!0,b=e}finally{try{g||null==j.return||j.return()}finally{if(v)throw b}}}finally{u.unbindShader(),this._doStroke=n,this.drawMode=s,l.pixelStorei(l.UNPACK_PREMULTIPLY_ALPHA_WEBGL,!0),e.pop()}}else console.log("WEBGL: only Opentype (.otf) and Truetype (.ttf) fonts are supported. Make sure to set the font using textFont() before drawing text.");return e}}else console.log("WEBGL: you must load and set a font before drawing text. See `loadFont` and `textFont` for more details.")}},{"../core/constants":272,"../core/main":283,"./p5.RendererGL.Retained":340,"./p5.Shader":342,"core-js/modules/es.array.iterator":165,"core-js/modules/es.object.get-own-property-descriptor":186,"core-js/modules/es.object.to-string":190,"core-js/modules/es.regexp.exec":195,"core-js/modules/es.string.iterator":200,"core-js/modules/es.string.split":206,"core-js/modules/es.string.sub":208,"core-js/modules/es.symbol":212,"core-js/modules/es.symbol.description":210,"core-js/modules/es.symbol.iterator":211,"core-js/modules/es.weak-map":244,"core-js/modules/web.dom-collections.iterator":246}]},{},[267])(267)});</script> <!-- 2. tl-gen-art: window.$art, seeded synchronously at load from URL mint params --> <script>/** * Generative art random helpers with p5-style configurable seeding. * * Reads mint params from the URL: tokenId, blockhash, txHash, minter * (each falls back to a random value so the script runs locally). * * tokenId + blockhash are ALWAYS part of the seed (uniqueness + * unpredictability). minter, txHash, gasPrice, and gasUsed are opt-in. * Re-seeding resets the random stream deterministically, like p5's randomSeed(). * * $art.seedFrom(); // tokenId + blockhash (default) * $art.seedFrom("minter"); // + minter * $art.seedFrom("gasPrice", "gasUsed"); // + gas fields * * A `seed` URL param is a curated, full override: when present it becomes the * complete seed verbatim and seedFrom(...) defers to it (contract-injected for * collector-curated drops). Absent locally, seeding composes from the fields. * * Snapshot + traits (for Cloudflare Browser Rendering): * $art.setTraits({ Palette: "Sunset", Layers: 5 }); // hidden #art-traits JSON * $art.snapshot(); // hidden #art-snapshot-ready marker * $art.snapshot(() => noLoop()); // + freeze the frame under capture * * For animated pieces, pass snapshot() a freeze callback. It runs ONLY under the * snapshot capture user agent (live collector views never freeze), letting the * screenshot grab the frame that was live at call time. Making that frame * reproducible is the artist's job: call snapshot() at a deterministic point. */ const $art = (function () { const params = new URLSearchParams(window.location.search); const get = (key, fb) => { const v = params.get(key); return v === null || v === "" ? fb : v; }; const randHex = (len) => "0x" + Array.from({ length: len }, () => "0123456789abcdef"[(Math.random() * 16) | 0]).join(""); // Raw mint values (with local-dev fallbacks). "0" tokenId is preserved. const mint = { tokenId: get("tokenId", Math.floor(Math.random() * 10000).toString()), blockhash: get("blockhash", randHex(64)), txHash: get("txHash", randHex(64)), minter: get("minter", randHex(40)), gasPrice: get("gasPrice", Math.floor(Math.random() * 1e11).toString()), // ~wei gasUsed: get("gasUsed", (21000 + Math.floor(Math.random() * 1e6)).toString()), seed: get("seed", null), // curated full-seed override; null = compose from fields }; // ---- seed -> PRNG ---- function xmur3(str) { let h = 1779033703 ^ str.length; for (let i = 0; i < str.length; i++) { h = Math.imul(h ^ str.charCodeAt(i), 3432918353); h = (h << 13) | (h >>> 19); } return function () { h = Math.imul(h ^ (h >>> 16), 2246822507); h = Math.imul(h ^ (h >>> 13), 3266489909); return (h ^= h >>> 16) >>> 0; }; } function sfc32(a, b, c, d) { return function () { a |= 0; b |= 0; c |= 0; d |= 0; const t = (((a + b) | 0) + d) | 0; d = (d + 1) | 0; a = b ^ (b >>> 9); b = (c + (c << 3)) | 0; c = (c << 21) | (c >>> 11); c = (c + t) | 0; return (t >>> 0) / 4294967296; // [0, 1) }; } // ---- seed composition ---- const FIELD_TAGS = { tokenId: "tid", blockhash: "blk", txHash: "tx", minter: "min", gasPrice: "gp", gasUsed: "gu", }; const REQUIRED = ["tokenId", "blockhash"]; function composeSeed(extraFields) { // required fields first, then opt-ins; de-duplicated, order preserved const ordered = [...new Set([...REQUIRED, ...extraFields])]; return ordered .map((f) => { if (!(f in FIELD_TAGS)) throw new Error("$art: unknown seed field '" + f + "'"); return FIELD_TAGS[f] + ":" + String(mint[f]).toLowerCase(); }) .join("|"); } // Mutable generator so re-seeding resets the stream (p5 semantics). let rand; let currentSeed; function applySeed(seedString) { currentSeed = seedString; const next = xmur3(seedString); rand = sfc32(next(), next(), next(), next()); for (let i = 0; i < 15; i++) rand(); // warm-up } // ---- p5-style seeding ---- // Curated full-seed override from the `seed` URL param (null when absent). const seedOverride = mint.seed; // Seed from chosen mint fields (tokenId + blockhash always included). // If a `seed` URL param is present it is the complete seed verbatim and // wins over the field composition (curated drops). Resets the random stream. // Returns the seed string used. function seedFrom(...extraFields) { const composed = composeSeed(extraFields); // validates fields, may throw applySeed(seedOverride !== null ? seedOverride : composed); return currentSeed; } // Default seed on load: the two required fields. seedFrom(); // ---- random helpers (draw from the current stream) ---- // random() -> [0,1) | random(max) -> [0,max) | random(min,max) -> [min,max) function random(...args) { let min = 0, max = 1; if (args.length === 1) max = args[0]; else if (args.length >= 2) { min = args[0]; max = args[1]; } return min + (max - min) * rand(); } // Inclusive of both ends. randomInt(max) -> [0,max] | randomInt(min,max) -> [min,max] function randomInt(...args) { let min = 0, max = 1; if (args.length === 1) max = args[0]; else if (args.length >= 2) { min = args[0]; max = args[1]; } return Math.floor(random(min, max + 1)); } function randomBool(p = 0.5) { return random() < p; } function randomElement(array) { if (!array || array.length === 0) return undefined; return array[randomInt(0, array.length - 1)]; } // ---- snapshot + traits (hidden DOM, survives in the HTML snapshot) ---- // Write a hidden, non-rendered JSON element (idempotent by id). 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}, ...] // Emitted into a hidden #art-traits element for the snapshot HTML. 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 the snapshot infra's userAgent setting. const CAPTURE_UA = "tl-gen-art"; const captureMode = typeof navigator !== "undefined" && new RegExp(CAPTURE_UA).test(navigator.userAgent || ""); // Signal that the canvas is fully drawn: append a hidden marker so Cloudflare's // Browser Rendering API can waitForSelector("#art-snapshot-ready"). Idempotent. // // For animated pieces, pass a freeze callback (e.g. () => noLoop(), or one that // cancels your rAF loop). It runs ONLY under the capture user agent, so the // screenshot captures the frame live at call time while collectors keep their // animation. Reproducibility of that frame is the artist's responsibility. 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 { mint, seedFrom, getSeed: () => currentSeed, random, randomInt, randomBool, randomElement, setTraits, getTraits: () => currentTraits, snapshot, captureMode, }; })(); window.$art = $art; </script> <!-- 3. Cinética engine (bundled ES modules, single scope) --> <script> const CinéticaEngine = (function(){ /* ===== engine/lib/prng.js ===== */ // Deterministic pseudo-random number generation. // // The whole collection is reproducible from a single seed: same seed -> same // artwork, forever. We use the well-known, fast, public-domain sfc32 generator, // seeded by hashing an arbitrary string with xmur3. Both are standard ~15-line // snippets used widely in generative-art / Art Blocks scripts. // xmur3: string -> a function that emits successive 32-bit hash values. // Used to derive the four 32-bit seeds sfc32 needs from any input string. function xmur3(str) { let h = 1779033703 ^ str.length for (let i = 0; i < str.length; i++) { h = Math.imul(h ^ str.charCodeAt(i), 3432918353) h = (h << 13) | (h >>> 19) } return function () { h = Math.imul(h ^ (h >>> 16), 2246822507) h = Math.imul(h ^ (h >>> 13), 3266489909) h ^= h >>> 16 return h >>> 0 } } // Derive four 32-bit unsigned integer seeds from a string. function seedFromString(str) { const next = xmur3(String(str)) return [next(), next(), next(), next()] } // sfc32 (Simple Fast Counter): four 32-bit seeds -> a () => float in [0, 1). function sfc32(a, b, c, d) { a >>>= 0 b >>>= 0 c >>>= 0 d >>>= 0 return function () { a |= 0 b |= 0 c |= 0 d |= 0 const t = (((a + b) | 0) + d) | 0 d = (d + 1) | 0 a = b ^ (b >>> 9) b = (c + (c << 3)) | 0 c = (c << 21) | (c >>> 11) c = (c + t) | 0 return (t >>> 0) / 4294967296 } } /* ===== engine/lib/hash.js ===== */ // Hash normalization. // // Mint platforms inject a seed in different shapes: Art Blocks provides a // `tokenData.hash` like "0x7c9f..."; other platforms may pass a plain string or // a VRF-derived value. We normalize any of them into a single canonical seed // string that feeds the PRNG. Normalization must be stable: the same logical // input always yields the same canonical string, so the artwork never changes. function normalizeHash(input) { if (input === null || input === undefined) { throw new Error('normalizeHash: seed is required') } const s = String(input).trim().toLowerCase() if (s.length === 0) { throw new Error('normalizeHash: seed must not be empty') } return s } /* ===== engine/lib/rng-stream.js ===== */ // The RNG stream — the single source of all randomness for one artwork. // // `makeRng(hash)` builds one sfc32 generator from the normalized hash and wraps // it with convenience helpers. Every decision in the engine (traits, palette, // composition, motion) draws from this one ordered stream, so the entire piece // is reproducible from its hash. IMPORTANT for determinism: draws must always // happen in the same order across runs — never branch the draw order on // non-deterministic input. function makeRng(hashInput) { const seed = normalizeHash(hashInput) const rand = sfc32(...seedFromString(seed)) const api = { // Raw float in [0, 1). next() { return rand() }, // Float in [a, b). range(a, b) { return a + rand() * (b - a) }, // Integer in [a, b] inclusive. int(a, b) { return a + Math.floor(rand() * (b - a + 1)) }, // Uniform pick from an array. pick(arr) { return arr[Math.floor(rand() * arr.length)] }, // Weighted pick. `table` is an array of [value, weight] pairs. weighted(table) { let total = 0 for (const [, w] of table) total += w let r = rand() * total for (const [value, w] of table) { r -= w if (r < 0) return value } return table[table.length - 1][0] }, // True with probability p (default 0.5). bool(p = 0.5) { return rand() < p }, // Alias for bool, reads naturally as a rarity gate: rng.chance(0.05). chance(p) { return rand() < p }, } return api } /* ===== engine/lib/color.js ===== */ // Small pure color helpers shared by the renderers. Kept dependency-free so the // on-chain sketch stays self-contained. function hexToRgb(hex) { const h = hex.replace('#', '') return { r: parseInt(h.slice(0, 2), 16), g: parseInt(h.slice(2, 4), 16), b: parseInt(h.slice(4, 6), 16), } } /* ===== engine/lib/palettes.js ===== */ // The Venezuelan Heritage Palette Family. // // Each palette is rooted in a real Venezuelan place, creature, tradition, or // natural feature. The flag tricolor is ONE palette among many — the collection // salutes the country without becoming a flag poster. Palette is a headline // trait; `weight` controls how often each appears (iconic ones common, the // more specialized studies rarer, so a palette becomes something to collect). // // `colors` are the blade/line hues. `bg` is the ground the kinetic structure // sits on — it matters as much as the colors, because Cruz-Diez's optical // effect comes from how narrow bands interact with each other and the field. // ELEGANCE RULE (the palette family's reason): every palette is a chromatic // homage — to a kinetic master or to Venezuela itself — and ALL obey one rule so // the collection reads as a single refined body of work: // • a DEEP, slightly-tinted ground (never flat black) for richness + depth // • a restrained set of hues with a clear VALUE LADDER (lights / mids / darks) // so every piece has dimension, not just flat poster color // • saturated primaries pulled a touch toward painterly (silk, pigment, dusk) // rather than screen-RGB — gallery-grade harmony, and it lets the holographic // lenticular shift read as luminous rather than garish. // Tricolor (the flag) is the hero anchor; the rest are a TIGHT curated family. // Curated to 6 — cohesion (Tricolor dominant = unmistakably Venezuelan) plus the // variety worth keeping (the master's own colors, a crisp B&W op study, the // magenta bloom, a cool range, modernist energy). Every palette earns its place. const PALETTES = [ { key: 'tricolor', name: 'Tricolor', // THE HERO / ANCHOR — Venezuela's flag refined into silk. Weighted highest so // the collection reads unmistakably Venezuelan. amber-gold / royal cobalt / // carmine / cream, on midnight blue + near-black for depth. weight: 30, bg: '#070a16', colors: ['#e8b73a', '#1442a6', '#c0303a', '#f0ead6', '#0e1b3c', '#121214'], }, { key: 'cruzdiez', name: 'Cruz-Díez', // The master's own vocabulary (red+green pair, yellow/red/blue triad, black + // white as essential elements), pulled toward painterly pigment. The homage, // made literal in color. weight: 16, bg: '#0a0a0b', colors: ['#d23a2e', '#138a5a', '#1f5fae', '#e8b43c', '#efece2', '#16161a'], }, { key: 'tinta', name: 'Tinta', // Pure black & white op-art study (ink). The most austere, gallery-severe // colorway — Soto's monochrome vibration / Cruz-Diez's black-white-as-essential. // Crisp value ladder of greys for depth; reads as the "fine print" of the set. weight: 14, bg: '#0a0a0a', colors: ['#f7f7f4', '#0d0d0d', '#cfcfca', '#3a3a3a', '#9a9a96', '#1c1c1c'], }, { key: 'caribe', name: 'Caribe', // Caribbean coast — refined teal/sea with a coral accent. The cool range. weight: 14, bg: '#04181f', colors: ['#2aa7b3', '#0e6a7d', '#e87a63', '#ecdcc0', '#0a3a45', '#86d8cf'], }, { key: 'orquidea', name: 'Orquídea', // Flor de mayo — magenta/lilac/cream over deep plum. The bloom. weight: 14, bg: '#140a13', colors: ['#c0457f', '#9069ac', '#e6c8d8', '#f1ebe2', '#5e3760', '#2a1426'], }, { key: 'canaima', name: 'Canaima', // Amazon / jungle / Salto Ángel — deep greens & blacks. Tannin-dark river, // canopy emerald, light-through-leaves moss, waterfall mist/cream, a warm // jasper-ochre of the tepui rock face. The deep-nature register of the set. weight: 12, bg: '#050b07', colors: ['#0f5132', '#3f8f4e', '#cfe0c4', '#b07a2e', '#0a2a1a', '#020503'], }, ] const BY_KEY = new Map(PALETTES.map((p) => [p.key, p])) function getPalette(key) { const p = BY_KEY.get(key) if (!p) throw new Error(`getPalette: unknown palette "${key}"`) return p } // Weighted deterministic pick from the family, driven by the artwork's rng. function pickPalette(rng) { const table = PALETTES.map((p) => [p.key, p.weight]) return getPalette(rng.weighted(table)) } /* ===== engine/lib/noise.js ===== */ // Fractal noise (fBm) — the math behind the collection's signature warp. // // A small, dependency-free 1D value-noise + fractional Brownian motion. Pure and // deterministic: noise1(x) and fbm1(x, ...) always return the same value for the // same input, so the warped artwork is reproducible from its hash. Smooth // (smoothstep-interpolated) so the warp flows without kinks. // hash a number -> pseudo-random value in [0,1), stable across machines. function hash1(n) { const s = Math.sin(n * 127.1) * 43758.5453 return s - Math.floor(s) } // smooth 1D value noise in [0,1). function noise1(x) { const xi = Math.floor(x) const xf = x - xi const u = xf * xf * (3 - 2 * xf) // smoothstep return hash1(xi) * (1 - u) + hash1(xi + 1) * u } // fractional Brownian motion: sum of `octaves` noise layers at doubling // frequency and halving amplitude. Returns ~[0,1). function fbm1(x, octaves = 5) { let v = 0 let amp = 0.5 let freq = 1 let norm = 0 for (let o = 0; o < octaves; o++) { v += amp * noise1(x * freq) norm += amp freq *= 2 amp *= 0.5 } return v / norm } /* ===== engine/lib/traits.js ===== */ // Trait derivation — the hash becomes the artwork's DNA. // // deriveTraits(hash) builds ONE rng stream and draws the HEADLINE traits in a // FIXED ORDER (order is part of the contract — append new traits at the end, // never insert in the middle). These headline traits drive the composition and // are what the platform reads for rarity (see features.js). Finer per-cell // decisions (which master fills each cell, its sub-type, density, etc.) are // derived deterministically inside the renderer from per-cell sub-seeds, so the // whole piece is still a pure function of the hash. // // Identity: an homage to Venezuelan kinetic art (Cruz-Diez / Soto / Gego), with // JN Silva's sacred-geometry composition as the signature. Hybrid-dominant. // MODE = which master (or hybrid). Hybrid is the STAR (most common); pure // single-master homages are the rarer, special tier. const MODE_WEIGHTS = [ ['hybrid', 58], // the star — masters in dialogue across the cells ['cruzdiez', 18], ['soto', 14], ['gego', 10], ] // GEOMETRY ENGINE = the JN Silva compositional signature (quiet background // variety — how the canvas is divided). const ENGINE_WEIGHTS = [ ['medium', 40], // golden cells (nested), depth 4 ['deep', 26], // deeper golden nesting ['mandala', 18], // 4-quadrant mirror symmetry ['single', 16], // single field (no subdivision) ] // FOCAL EVENT (per piece; many get none). Radial-fill events (singularity, // interference, spiral) are GATED to nested compositions at render time so they // never become a full-canvas sunburst — see compose.js. const EVENT_WEIGHTS = [ ['none', 26], ['singularity', 14], ['interference', 10], ['rings', 12], ['squares', 12], ['implosion', 12], ['spiral', 8], ['moire', 6], ] // Events that read badly when they fill a canvas-dominating cell. const RADIAL_FILL_EVENTS = ['singularity', 'interference', 'spiral'] // What a radial-fill event downgrades to when it can't nest safely. const SAFE_EVENT_WEIGHTS = [ ['none', 30], ['rings', 22], ['squares', 22], ['implosion', 16], ['moire', 10], ] // Soto color treatment (only meaningful for Soto cells): authentic monochrome // field (color reserved for floating element) vs full color. const SOTO_COLOR_WEIGHTS = [ ['color', 64], ['mono', 24], ['trait', 12], // mixed per cell ] function deriveTraits(hash) { const rng = makeRng(hash) // --- FIXED DRAW ORDER (do not reorder) --- const mode = rng.weighted(MODE_WEIGHTS) const engine = rng.weighted(ENGINE_WEIGHTS) const paletteObj = pickPalette(rng) // headline event (gated later in the renderer once cell sizes are known) const event = rng.weighted(EVENT_WEIGHTS) // global continuous render params (continuous = key to uniqueness) const iridescence = Math.round(rng.range(0.35, 0.7) * 1000) / 1000 const geo = Math.round(rng.range(0.55, 0.9) * 1000) / 1000 // geometry strength const phase = rng.range(0, 1) // global color-sequence phase const sweepSpeed = Math.round(rng.range(3.4, 5.6) * 100) / 100 // lenticular tempo const sotoColor = rng.weighted(SOTO_COLOR_WEIGHTS) return { mode, engine, palette: paletteObj.key, event, iridescence, geo, phase, sweepSpeed, sotoColor, } } // Re-pick a safe event when a radial-fill event would dominate the canvas. Pure // + deterministic from the same hash (uses a distinct sub-stream). function safeEvent(hash) { return makeRng(hash + ':event2').weighted(SAFE_EVENT_WEIGHTS) } /* ===== engine/lib/features.js ===== */ // Platform features / rarity output. // // Mint platforms (Art Blocks, Transient Labs, fxhash) read a `features` object // to compute and display trait rarity. Their contract: every value must be a // STRING. This maps the headline traits into the human-readable labels collectors // see ("Master: Hybrid", "Palette: Tricolor", "Composition: Mandala"). const MODE_LABEL = { hybrid: 'Hybrid', cruzdiez: 'Cruz-Díez', soto: 'Soto', gego: 'Gego', } const ENGINE_LABEL = { medium: 'Golden Cells', deep: 'Deep Nesting', mandala: 'Mandala', single: 'Single Field', } const EVENT_LABEL = { none: 'None', singularity: 'Singularity', interference: 'Interference', rings: 'Concentric Rings', spiral: 'Spiral', squares: 'Nested Squares', implosion: 'Implosion', moire: 'Moiré', } // Bucket the continuous lenticular tempo into a coarse, collectible label. function tempoLabel(speed) { if (speed < 4.0) return 'Calm' if (speed < 4.9) return 'Living' return 'Vivid' } function buildFeatures(traits) { return { Master: MODE_LABEL[traits.mode] || traits.mode, Palette: getPalette(traits.palette).name, Composition: ENGINE_LABEL[traits.engine] || traits.engine, Event: EVENT_LABEL[traits.event] || traits.event, Tempo: tempoLabel(traits.sweepSpeed), Iridescence: traits.iridescence < 0.5 ? 'Subtle' : 'Luminous', } } /* ===== engine/compose.js ===== */ // engine/compose.js — the FULL Cinetica composition system, framework-free. // // SINGLE SOURCE OF TRUTH ported from the approved Composition Studio // (gallery/compose.html, tag studio-v1). Imported by engine/render.js. // Given a p5 instance, the piece traits, its palette, the hash, and time t, it // renders the complete artwork: geometry engine -> cells -> per-cell master // (Cruz-Diez/Soto/Gego/hybrid) -> holographic lenticular + sheen -> gated event. // Pure & deterministic: same hash + same t -> identical frame. const PHI = 1.6180339887, INVPHI = 0.6180339887 const FIB = [1,1,2,3,5,8,13,21,34] // The golden "power points": where the phi-lines of a cell cross. Picking the // radial-event epicenter from these (per-hash) gives variety while keeping the // placement sacred-geometry-justified (never an arbitrary spot). Includes the // 4 corners-of-phi + the center. function goldenNode(w, h, rng){ const xs=[w*INVPHI, w*(1-INVPHI), w*0.5] const ys=[h*INVPHI, h*(1-INVPHI), h*0.5] // anchor on a golden power point... let x = rng.weighted([[xs[0],5],[xs[1],5],[xs[2],2]]) let y = rng.weighted([[ys[0],5],[ys[1],5],[ys[2],2]]) // ...then continuous jitter around it so the epicenter is effectively // never identical between pieces (kills the "only ~9 positions" bug) while // staying near the sacred-geometry anchor. x += (rng.next()-0.5)*w*0.18 y += (rng.next()-0.5)*h*0.18 return {cx:x, cy:y} } // Pick the focal cell. For RADIAL events (singularity/interference/spiral/rings/ // squares) prefer a cell that does NOT dominate the canvas (<=45%), nearest the // golden node among those — so the event has a real nested home and the gate // rarely needs to downgrade it. Falls back to nearest-node if none qualify. function pickFocal(cells, W, H, preferSmall){ const gx=W*INVPHI, gy=H*INVPHI const near=(c)=>Math.hypot(c.x+c.w/2-gx, c.y+c.h/2-gy) let pool=cells.map((c,idx)=>({c,idx})) if(preferSmall){ const small=pool.filter(({c})=>c.w*c.h <= W*H*0.45) if(small.length) pool=small } let best=1e9, focal=pool.length?pool[0].idx:0 pool.forEach(({c,idx})=>{ const d=near(c); if(d<best){best=d;focal=idx} }) return focal } function rgb2hsl({r,g,b}){r/=255;g/=255;b/=255;const mx=Math.max(r,g,b),mn=Math.min(r,g,b);let h,s,l=(mx+mn)/2;if(mx===mn){h=s=0;}else{const d=mx-mn;s=l>0.5?d/(2-mx-mn):d/(mx+mn);switch(mx){case r:h=(g-b)/d+(g<b?6:0);break;case g:h=(b-r)/d+2;break;default:h=(r-g)/d+4;}h/=6;}return{h:h*360,s,l};} function hsl2rgb(h,s,l){ h=(((h%360)+360)%360)/360;s=Math.max(0,Math.min(1,s));l=Math.max(0,Math.min(1,l)); if(s===0){const v=Math.round(l*255);return[v,v,v];} const q=l<0.5?l*(1+s):l+s-l*s,p=2*l-q,f=(t)=>{if(t<0)t+=1;if(t>1)t-=1;if(t<1/6)return p+(q-p)*6*t;if(t<1/2)return q;if(t<2/3)return p+(q-p)*(2/3-t)*6;return p;}; return[Math.round(f(h+1/3)*255),Math.round(f(h)*255),Math.round(f(h-1/3)*255)]; } function goldenCells(x,y,w,h,depth,rng,geo){ if(depth<=0 || w<24 || h<24) return [{x,y,w,h,depth}] // probability of splitting scales with geometry strength if(rng.next() > 0.45 + geo*0.5) return [{x,y,w,h,depth}] const horizontalCut = (w>=h) // cut the longer axis -> golden rectangles const ratio = rng.bool() ? INVPHI : 1-INVPHI const cells=[] if(horizontalCut){ const wl=w*ratio cells.push(...goldenCells(x,y,wl,h,depth-1,rng,geo)) cells.push(...goldenCells(x+wl,y,w-wl,h,depth-1,rng,geo)) } else { const ht=h*ratio cells.push(...goldenCells(x,y,w,ht,depth-1,rng,geo)) cells.push(...goldenCells(x,y+ht,w,h-ht,depth-1,rng,geo)) } return cells } // MANDALA (mirror): build golden cells in one quadrant, then mirror to all // four — radial bilateral symmetry, the most overtly 'sacred' layout. function mandalaCells(W,H,rng,geo){ const hw=W/2, hh=H/2 const q=goldenCells(0,0,hw,hh,3,rng,geo) const out=[] q.forEach(c=>{ out.push({x:c.x, y:c.y, w:c.w, h:c.h}) // TL out.push({x:W-c.x-c.w, y:c.y, w:c.w, h:c.h}) // TR (mirror x) out.push({x:c.x, y:H-c.y-c.h, w:c.w, h:c.h}) // BL (mirror y) out.push({x:W-c.x-c.w, y:H-c.y-c.h, w:c.w, h:c.h}) // BR (mirror xy) }) return out } // Fibonacci-spaced band edges across length L: band widths follow a slice of // the Fibonacci sequence (the rhythm itself is sacred-geometric), repeated. function fibEdges(L, baseCount, rng, geo){ // geo=0 -> near-uniform; geo=1 -> strong fibonacci rhythm const start = Math.floor(rng.next()*3) const pattern = FIB.slice(start, start+4) // e.g. [2,3,5,8] const patSum = pattern.reduce((a,c)=>a+c,0) const uniformW = L/baseCount const edges=[0]; let x=0, idx=0 const cycleW = uniformW * 4 // a full fib cycle ~ 4 uniform bands wide while(x < L){ const fibW = (pattern[idx%pattern.length]/patSum) * cycleW const w = uniformW*(1-geo) + fibW*geo x += Math.max(2, w); edges.push(Math.min(L,x)); idx++ } return edges } function renderCell(p, c, o){ const {horizontal,event}=o p.push(); p.translate(c.x,c.y) // CLIP to this cell: radial events overscan past the cell; clipping stops // them bleeding into neighbors (which is what made neighbor grid-lines // appear to cross the event). drawingContext is the raw 2D ctx; we MUST // restore() it on the single exit path below so the clip never leaks. const dc=p.drawingContext, clipped = event!=='none' if(clipped){ dc.save(); dc.beginPath(); dc.rect(0,0,c.w,c.h); dc.clip() } // FOCAL EVENTS are STRUCTURAL — they change how the cell's own blades are // generated, never drawn over the top. if(event==='implosion') drawImplosion(p, c, o) else if(event==='singularity') drawSingularity(p, c, o, false) else if(event==='interference') drawSingularity(p, c, o, true) else if(event==='rings' || event==='squares'){ // EMBEDDED medallion: rings/squares never dominate. Draw the cell's own // kinetic blade field first, then a small rings/squares medallion within // it (sized to a fraction of the cell) so it reads as an accent that // belongs to the piece, not a standalone target/box. drawBands(p, c, o, {}) const med = Math.min(c.w,c.h) * (0.32 + makeRng(o.cellSeed+':med').next()*0.18) // 32-50% const mo = {...o, medallion: med} if(event==='rings') drawRings(p, c, mo); else drawSquares(p, c, mo) } else if(event==='spiral') drawSpiral(p, c, o) else { drawBands(p, c, o, {}) if(event==='moire'){ // SOTO homage: a second, slightly-offset blade field -> optical moiré. drawBands(p, c, {...o, phase:o.phase+0.5, horizontal:!horizontal}, {alpha: 150, freqMul: 1.07}) } } if(clipped) dc.restore() p.pop() } // Standard band fill for a cell (vertical or horizontal), holographic color. function drawBands(p, c, o, opt={}){ const {baseHues,seqLen,phase,irid,t,sweepSpeed,horizontal}=o const negative=opt.negative, alpha=opt.alpha??255, freqMul=opt.freqMul??1 const L = horizontal ? c.h : c.w const baseCount = Math.max(6, Math.round(L/(6+ (o.cellDensity||10))) * freqMul) const edges = fibEdges(L, baseCount, makeRng(o.cellSeed+':bands'), o.geo) const cycle = baseHues.length for(let b=0;b<edges.length-1;b++){ const a0=edges[b], a1=edges[b+1] const slot=Math.floor(b + phase*seqLen) let A=baseHues[((slot%cycle)+cycle)%cycle], B=baseHues[((slot+1)%cycle+cycle)%cycle] if(negative){ // invert lightness + rotate hue 180 -> the "void" cell A={h:A.h+180,s:A.s,l:1-A.l}; B={h:B.h+180,s:B.s,l:1-B.l} } const [ar,ag,ab]=hsl2rgb(A.h,A.s,A.l), [br,bg,bb]=hsl2rgb(B.h,B.s,B.l) const along=(a0/Math.max(1,L)) const wave=Math.sin(t*sweepSpeed*0.16 + along*3.2 + phase*6.28)*0.5+0.5 const k=wave*wave*(3-2*wave); const m=Math.max(0,Math.min(1,0.5+(k-0.5)*(0.6+1.4*irid))) p.fill(Math.round(ar*(1-m)+br*m),Math.round(ag*(1-m)+bg*m),Math.round(ab*(1-m)+bb*m), alpha) if(horizontal){ p.rect(0,a0,c.w,a1-a0) } else { p.rect(a0,0,a1-a0,c.h) } const r=Math.max(0.5,(a1-a0)*0.10) p.fill(0,0,0,55*alpha/255); if(horizontal)p.rect(0,a0,c.w,r); else p.rect(a0,0,r,c.h) p.fill(255,255,255,38*alpha/255); if(horizontal)p.rect(0,a0+r,c.w,r*0.6); else p.rect(a0+r,0,r*0.6,c.h) } } // EVENT: recursive implosion — the cell subdivides (golden) into ever-smaller // sub-cells toward a golden node, each filled with blades. A fractal vortex of // escalating density, built entirely from cells+blades. function drawImplosion(p, c, o){ ;(function rec(x,y,w,h,d,idx){ if(d<=0 || w<6 || h<6){ const sub={x,y,w,h}; const so={...o, horizontal:(d%2===0), phase:o.phase+idx*0.12, cellSeed:o.cellSeed+':i'+idx, cellDensity:4+d} p.push(); p.translate(x,y); drawBands(p,{x:0,y:0,w,h},so,{}); p.pop(); return } // keep one golden sub-rect, recurse into the other (the vortex) const cutW = w>=h const keep = INVPHI if(cutW){ const wl=w*keep const sub={x,y,w:wl,h}; const so={...o,horizontal:(d%2===0),phase:o.phase+d*0.15,cellSeed:o.cellSeed+':k'+d,cellDensity:3+d} p.push(); p.translate(x,y); drawBands(p,{x:0,y:0,w:wl,h},so,{}); p.pop() rec(x+wl,y,w-wl,h,d-1,idx+1) } else { const ht=h*keep const so={...o,horizontal:(d%2===0),phase:o.phase+d*0.15,cellSeed:o.cellSeed+':k'+d,cellDensity:3+d} p.push(); p.translate(x,y); drawBands(p,{x:0,y:0,w,h:ht},so,{}); p.pop() rec(x,y+ht,w,h-ht,d-1,idx+1) } })(0,0,c.w,c.h,7,0) } // EVENT: orientation singularity — blades fan radially from a golden node. // Filled wedges (not stroked lines). interference=true overlays a second // counter-rotating fan -> radial moiré buzz (a kinetic Soto-flavored burst). function drawSingularity(p, c, o, interference){ const sr=makeRng(o.cellSeed+':sing') // per-piece structural entropy const {cx,cy}=goldenNode(c.w,c.h,makeRng(o.cellSeed+':node')) // Per-piece structural params (this is what makes every singularity unique, // not one of ~9 fixed looks): ray count, color cadence, rotation dir/speed, // color start, and wedge-width rhythm. const rays = 48 + Math.floor(sr.next()*200) // 48..248 const cadence = 1 + Math.floor(sr.next()*4) // colors step every 1-4 rays const colStart = Math.floor(sr.next()*4) const rotDir = sr.bool()?1:-1 const rotSpd = 0.025 + sr.next()*0.06 const pulse = sr.next()<0.45 ? (1+Math.floor(sr.next()*4)) : 0 // 0 = even wedges, else fib-ish pulse const fan=(rot,alpha)=>{ const {baseHues,seqLen,phase,irid,t,sweepSpeed}=o const R=Math.hypot(Math.max(cx,c.w-cx),Math.max(cy,c.h-cy))*1.15 const cycle=baseHues.length p.noStroke() for(let i=0;i<rays;i++){ // optional uneven wedge widths (a slow sinusoidal pulse around the dial) const wmul = pulse? (0.6 + 0.5*(0.5+0.5*Math.sin(i/rays*Math.PI*2*pulse))) : 1 const a0=(i/rays)*Math.PI*2+rot, a1=((i+wmul)/rays)*Math.PI*2+rot const slot=Math.floor(i/cadence + colStart + phase*seqLen) const A=baseHues[((slot%cycle)+cycle)%cycle] const wave=Math.sin(t*sweepSpeed*0.16+i*0.3+phase*6.28)*0.5+0.5 const [cr,cg,cb]=hsl2rgb(A.h,A.s,Math.min(1,A.l+wave*0.18*irid)) p.fill(cr,cg,cb,alpha) p.triangle(cx,cy,cx+Math.cos(a0)*R,cy+Math.sin(a0)*R,cx+Math.cos(a1)*R,cy+Math.sin(a1)*R) p.fill(0,0,0,40*alpha/255) p.triangle(cx,cy,cx+Math.cos(a0)*R,cy+Math.sin(a0)*R,cx+Math.cos(a0+0.004)*R,cy+Math.sin(a0+0.004)*R) } } fan(o.t*rotSpd*rotDir, 255) if(interference) fan(-o.t*rotSpd*rotDir + 0.013, 140) // counter-rotating -> radial moiré } // EVENT: concentric rings — color bands radiating from a golden node (target). // MOTION: the color sequence flows continuously INWARD over time (each ring // takes the next color), so the target appears to pulse toward the center. // Drawn largest-first as filled circles; each smaller circle covers the // previous, leaving a ring. Continuous flow = perfectly smooth. function drawRings(p, c, o){ const {baseHues,seqLen,phase,irid,t}=o const {cx,cy}=goldenNode(c.w,c.h,makeRng(o.cellSeed+':node')) // EMBEDDED: when o.medallion is set, the target is a small disc of that // radius centered on the golden node (drawn over the cell's blade field), // not the whole cell. Otherwise fill the cell (legacy/full). const sr=makeRng(o.cellSeed+':rings') // per-piece entropy const R = o.medallion ? o.medallion/2 : Math.hypot(Math.max(cx,c.w-cx),Math.max(cy,c.h-cy))*1.2 const cycle=baseHues.length; p.noStroke() const NR = 6 + Math.floor(sr.next()*16) // 6..22 rings (per-piece) const cadence = 1 + Math.floor(sr.next()*3) // color steps every 1-3 rings const colStart = Math.floor(sr.next()*4) const dir = sr.bool()?1:-1 // flow in or out const ringW=R/NR const g = t*0.5*dir, gi=Math.floor(g), gf=g-gi if(o.medallion){ // clip to the medallion disc so it has a crisp circular edge const dc=p.drawingContext; dc.save(); dc.beginPath(); dc.arc(cx,cy,R,0,Math.PI*2); dc.clip() } for(let i=0;i<=NR+1;i++){ const rr = R - (i - gf)*ringW if(rr<=0 || rr>R+ringW) continue const band=i+gi; const slot=Math.floor(band/cadence + colStart + phase*seqLen) const A=baseHues[((slot%cycle)+cycle)%cycle] const lift=(0.5+0.5*Math.sin(band*0.7 + phase*6.28))*0.16*irid const [cr,cg,cb]=hsl2rgb(A.h,A.s,Math.min(1,A.l+lift)) p.fill(cr,cg,cb); p.circle(cx,cy, rr*2) p.fill(0,0,0,45); p.circle(cx,cy, (rr-Math.max(0.6,ringW*0.12))*2) } if(o.medallion){ p.drawingContext.restore() // crisp rim so the medallion reads as an inset element p.noFill(); p.stroke(0,0,0,120); p.strokeWeight(2); p.circle(cx,cy,R*2) p.stroke(255,255,255,50); p.strokeWeight(1); p.circle(cx,cy,R*2-2); p.noStroke() } } // EVENT: spiral blade-flow — wedges whose angular width + hue progress along a // golden spiral (a kinetic swirl), built from filled wedges rotating outward. function drawSpiral(p, c, o){ const {baseHues,seqLen,phase,irid,t,sweepSpeed}=o const sr=makeRng(o.cellSeed+':spiral') // per-piece entropy const {cx,cy}=goldenNode(c.w,c.h,makeRng(o.cellSeed+':node')) const R=Math.hypot(Math.max(cx,c.w-cx),Math.max(cy,c.h-cy))*1.2 const cycle=baseHues.length; p.noStroke() const arms=2+Math.floor(sr.next()*5) // 2..6 arms const turns=2+sr.next()*3 // 2..5 turns const growth=1.05+sr.next()*0.5 // spiral tightness const steps=200 const dir=sr.bool()?1:-1 const rot=t*(0.04+sr.next()*0.05)*dir for(let a=0;a<arms;a++){ for(let s=0;s<steps;s++){ const u=s/steps const ang = u*Math.PI*2*turns + a*(Math.PI*2/arms) + rot const rad = R*Math.pow(u,1/growth) const slot=Math.floor(s*0.12 + a + phase*seqLen); const A=baseHues[((slot%cycle)+cycle)%cycle] const wave=Math.sin(t*sweepSpeed*0.16 + s*0.1 + phase*6.28)*0.5+0.5 const [cr,cg,cb]=hsl2rgb(A.h,A.s,Math.min(1,A.l+wave*0.18*irid)) const w=Math.max(2, R*0.05*(1-u)+3) p.fill(cr,cg,cb) p.circle(cx+Math.cos(ang)*rad, cy+Math.sin(ang)*rad, w) } } } // EVENT: nested squares — concentric squares receding to the center (the // light-box, Cruz-Diez ref 2). MOTION: colors flow inward over time so the // box appears to recede/pulse. Even steps for more depth layers. function drawSquares(p, c, o){ const {baseHues,seqLen,phase,irid,t}=o const sr=makeRng(o.cellSeed+':squares') // per-piece entropy const {cx,cy}=goldenNode(c.w,c.h,makeRng(o.cellSeed+':node')) const cycle=baseHues.length; p.noStroke() const NL=7+Math.floor(sr.next()*14) // 7..21 layers (per-piece) const cadence=1+Math.floor(sr.next()*3) const colStart=Math.floor(sr.next()*4) const dir=sr.bool()?1:-1 // EMBEDDED: medallion -> a small nested-square box centered on the node, // over the cell's blade field. Otherwise fill the cell. const maxR = o.medallion ? o.medallion/2 : Math.max(cx,c.w-cx,cy,c.h-cy)*1.1 const step=maxR/NL const ar = o.medallion?1:c.w/Math.max(c.w,c.h), arh = o.medallion?1:c.h/Math.max(c.w,c.h) const g=t*0.5*dir, gi=Math.floor(g), gf=g-gi if(o.medallion){ const dc=p.drawingContext; dc.save(); dc.beginPath() dc.rect(cx-maxR,cy-maxR,maxR*2,maxR*2); dc.clip() } for(let i=NL+1;i>=0;i--){ const r=(i - gf)*step; const w=r*2*ar, h=r*2*arh if(w<=2||h<=2) continue const band=i+gi; const slot=Math.floor(band/cadence + colStart + phase*seqLen) const A=baseHues[((slot%cycle)+cycle)%cycle] const lift=(0.5+0.5*Math.sin(band*0.7 + phase*6.28))*0.16*irid const [cr,cg,cb]=hsl2rgb(A.h,A.s,Math.min(1,A.l+lift)) p.fill(cr,cg,cb); p.rect(cx-w/2, cy-h/2, w, h) p.fill(0,0,0,40); p.rect(cx-w/2, cy-h/2, w, Math.max(1,step*0.12)) } if(o.medallion){ p.drawingContext.restore() p.noFill(); p.stroke(0,0,0,