memoscan
← all memos

Memo 0x941a4c84…5d3826 on Ethereum

120); p.strokeWeight(2); p.rect(cx-maxR,cy-maxR,maxR*2,maxR*2) p.stroke(255,255,255,50); p.strokeWeight(1); p.rect(cx-maxR+2,cy-maxR+2,maxR*2-4,maxR*2-4); p.noStroke() } } // ========================= SOTO MODE ========================= // Jesús Rafael Soto — one engine, swappable floating layer. Every Soto work = // a dense periodic FIELD + a sparser FLOATING element above it; the optical // buzz comes from the frequency/parallax mismatch between the two. Sub-types: // vibration — thin rods/wires over a fine line screen // escritura — looping handwritten wire scribble over b/w stripes // suspended — solid color squares floating over the striped ground // Color rule (when o.sotoMono): field is monochrome b/w/grey, color reserved // for the floating element (authentic Soto). Otherwise full color. function drawSoto(p, c, o){ const cr=makeRng(o.cellSeed+':soto') const sub = o.sotoSub || cr.pick(['vibration','escritura','suspended']) // the shared dense vertical line screen (the ground) sotoScreen(p, c, o, cr) if(sub==='vibration') sotoVibration(p, c, o, cr) else if(sub==='escritura') sotoEscritura(p, c, o, cr) else sotoSuspended(p, c, o, cr) } // dense fine vertical line screen. Monochrome when sotoMono, else palette hue. function sotoScreen(p, c, o, cr){ const {baseHues,seqLen,phase,irid,t,sweepSpeed}=o const W=c.w,H=c.h; p.noStroke() const lines=Math.max(36,Math.round(W/3)); const lw=W/lines const hueA=baseHues[Math.floor(cr.next()*seqLen)] for(let i=0;i<lines;i++){ const wave=Math.sin(t*sweepSpeed*0.16+(i/lines)*3+phase*6.28)*0.5+0.5 let r,g,b if(o.sotoMono){ const v=Math.round((0.32+wave*0.12*irid+(i%2?0:0.05))*255); r=g=b=v } else { [r,g,b]=hsl2rgb(hueA.h,hueA.s*0.7, hueA.l*0.5+wave*0.12*irid+(i%2?0:0.04)) } p.fill(r,g,b,200); p.rect(i*lw,0,lw*0.55,H) } } // VIBRATION: thin straight/bent rods at irregular angles, each with a soft // offset shadow -> they buzz against the screen. function sotoVibration(p, c, o, cr){ const {baseHues,seqLen,irid,t}=o; const W=c.w,H=c.h const rods=4+Math.floor(cr.next()*7) const amp=Math.min(W,H)*0.05 // visible sway, fraction of cell for(let i=0;i<rods;i++){ const bx=cr.next()*W, by=cr.next()*H, len=H*(0.2+cr.next()*0.5) const baseAng=(cr.next()-0.5)*1.2 + (cr.next()<0.5?Math.PI/2:0) // the rod SWAYS: position drifts and the angle wobbles -> it buzzes const drift=Math.sin(t*0.9+i*1.3)*amp const ang=baseAng + Math.sin(t*1.2+i)*0.10 const x0=bx+drift, y0=by, x1=x0+Math.cos(ang)*len, y1=y0+Math.sin(ang)*len const hueB=baseHues[Math.floor(cr.next()*seqLen)] const [r,g,b]=o.sotoMono?[235,235,235]:hsl2rgb(hueB.h,hueB.s,Math.min(1,hueB.l+0.12)) // shadow offset grows with sway -> parallax buzz against the screen const sh=2+Math.abs(Math.sin(t*0.9+i*1.3))*2 p.stroke(0,0,0,90); p.strokeWeight(3.2); p.line(x0+sh,y0,x1+sh,y1) p.stroke(r,g,b,235); p.strokeWeight(2.2); p.line(x0,y0,x1,y1) } p.noStroke() } // ESCRITURA: a few continuous looping "handwriting" strokes over the screen, // fading where they cross the dense lines (dematerialize into vibration). function sotoEscritura(p, c, o, cr){ const {baseHues,seqLen,t}=o; const W=c.w,H=c.h const strokes=2+Math.floor(cr.next()*2) p.noFill() for(let s=0;s<strokes;s++){ const hueB=baseHues[Math.floor(cr.next()*seqLen)] const [r,g,b]=o.sotoMono?[240,240,240]:hsl2rgb(hueB.h,hueB.s,Math.min(1,hueB.l+0.1)) const cy=H*(0.25+cr.next()*0.5), amp=H*(0.08+cr.next()*0.14) const loops=5+Math.floor(cr.next()*6), drift=Math.sin(t*0.4+s)*3 // shadow then stroke for(let pass=0;pass<2;pass++){ if(pass===0){p.stroke(0,0,0,70);p.strokeWeight(3)} else {p.stroke(r,g,b,230);p.strokeWeight(2)} const off=pass===0?2:0 p.beginShape() for(let x=0;x<=W;x+=4){ const u=x/W const y=cy + Math.sin(u*loops*6.28 + s)*amp + Math.sin(u*loops*2.1+t*0.3)*amp*0.5 p.curveVertex(x+drift+off, y+off) } p.endShape() } } p.noStroke() } // SUSPENDED: solid color squares floating over the striped ground (color = the // levitating object), each with a faint frame + drop shadow. function sotoSuspended(p, c, o, cr){ const {baseHues,seqLen,irid,t}=o; const W=c.w,H=c.h const forms=1+Math.floor(cr.next()*3) for(let f=0;f<forms;f++){ const fw=W*(0.16+cr.next()*0.3), fh=fw*(0.7+cr.next()*0.7) const fx=cr.next()*(W-fw), fy=cr.next()*(H-fh) const hueB=baseHues[Math.floor(cr.next()*seqLen)] // the form floats more: drifts in BOTH axes + the shadow gap breathes const driftX=Math.sin(t*0.7+f*1.4)*Math.min(W,H)*0.04 const driftY=Math.cos(t*0.55+f)*Math.min(W,H)*0.02 const sh=5+Math.abs(Math.sin(t*0.7+f*1.4))*5 const [r,g,b]=hsl2rgb(hueB.h,hueB.s,Math.min(1,hueB.l+0.05)) const fxx=fx+driftX, fyy=fy+driftY p.noStroke(); p.fill(0,0,0,80); p.rect(fxx+sh,fyy+sh,fw,fh) // shadow -> levitation p.fill(r,g,b,235); p.rect(fxx,fyy,fw,fh) // the solid color plane // a faint moiré disturbance: thin darker lines across the square p.fill(0,0,0,30); const n=Math.max(5,Math.round(fw/6)) for(let i=0;i<n;i++) p.rect(fxx+i*(fw/n), fyy, (fw/n)*0.4, fh) p.noFill(); p.stroke(255,255,255,40); p.strokeWeight(1); p.rect(fxx,fyy,fw,fh); p.noStroke() } } // ========================= GEGO MODE ========================= // Gego — line networks along an order↔disorder spectrum. Sub-types: // reticularea — perturbed Delaunay-ish triangular net + dark variable-valence // node dots + an offset shadow copy (the "second drawing") // chorros — a hanging cascade of segments fanning from a top anchor // dibujos — sparse: a few kinked open polylines on near-empty ground // All built from STRAIGHT short chords; joints read as dots; curves emergent. function drawGego(p, c, o){ const cr=makeRng(o.cellSeed+':gego') const sub = o.gegoSub || cr.pick(['reticularea','chorros','dibujos']) if(sub==='chorros') gegoChorros(p,c,o,cr) else if(sub==='dibujos') gegoDibujos(p,c,o,cr) else gegoReticularea(p,c,o,cr) } function gegoHue(o,cr){ const h=o.baseHues[Math.floor(cr.next()*o.seqLen)]; return h } function gegoStroke(p,o,h,t,i,alpha){ const wave=Math.sin(t*0.16*o.sweepSpeed+i*0.2+o.phase*6.28)*0.5+0.5 const [r,g,b]=hsl2rgb(h.h,h.s,Math.min(1,h.l+wave*0.2*o.irid)); p.stroke(r,g,b,alpha) } // RETICULÁREA: jittered points -> connect each to nearest neighbors (irregular // triangular web), dark dots at nodes, + offset low-opacity shadow duplicate. function gegoReticularea(p, c, o, cr){ const {t}=o; const W=c.w,H=c.h const gx=W*INVPHI, gy=H*INVPHI const count=Math.max(24,Math.round(W*H/2400)) const nodes=[] // the net BREATHES: each node sways on its own little orbit -> the whole // web ripples and the shadow shifts. Amplitude scales with cell size so // it's clearly alive (Gego's nets visibly move/cast moving shadows). const sway=Math.min(W,H)*0.03 for(let i=0;i<count;i++){ let px=cr.next()*W, py=cr.next()*H px+=(gx-px)*0.12*cr.next(); py+=(gy-py)*0.12*cr.next() // organic pull px+=Math.sin(t*0.7+i*0.7)*sway; py+=Math.cos(t*0.6+i*0.5)*sway nodes.push({x:px,y:py}) } const h=gegoHue(o,makeRng(o.cellSeed+':gego:reticularea:color')); const lw=Math.max(0.8,Math.min(W,H)*0.004) // edges: nearest 3 neighbors -> triangular-ish net const edges=[] for(let i=0;i<nodes.length;i++){ const d=[]; for(let j=0;j<nodes.length;j++){if(i===j)continue;d.push([Math.hypot(nodes[i].x-nodes[j].x,nodes[i].y-nodes[j].y),j])} d.sort((a,b)=>a[0]-b[0]); const k=2+Math.floor(cr.next()*2) for(let n=0;n<k&&n<d.length;n++){const j=d[n][1]; if(j>i)edges.push([i,j])} } // shadow ("second drawing") — offset, faint p.noFill(); p.strokeWeight(lw); p.stroke(0,0,0,60) edges.forEach(([i,j])=>p.line(nodes[i].x+4,nodes[i].y+4,nodes[j].x+4,nodes[j].y+4)) // the net p.strokeWeight(lw) edges.forEach(([i,j],e)=>{ gegoStroke(p,o,h,t,e,215); p.line(nodes[i].x,nodes[i].y,nodes[j].x,nodes[j].y) }) // dark node dots (twisted joints) p.noStroke() nodes.forEach(nd=>{ p.fill(10,10,10,220); p.circle(nd.x,nd.y,lw*3.2) const [r,g,b]=hsl2rgb(h.h,h.s,Math.min(1,h.l+0.2)); p.fill(r,g,b,180); p.circle(nd.x,nd.y,lw*1.6) }) } // CHORROS: a cascade — many short segments falling/fanning from a top anchor, // chained into kinked vertical paths, denser knots in pockets. function gegoChorros(p, c, o, cr){ const {t}=o; const W=c.w,H=c.h const anchorX=W*(0.3+cr.next()*0.4), anchorY=H*0.04 const streams=10+Math.floor(cr.next()*14) const h=gegoHue(o,cr); const lw=Math.max(0.7,Math.min(W,H)*0.0035) for(let s=0;s<streams;s++){ let x=anchorX+(cr.next()-0.5)*W*0.06, y=anchorY const spread=(cr.next()-0.5)*1.4 // how far this stream fans const segs=14+Math.floor(cr.next()*16) const pts=[{x,y}] for(let i=0;i<segs;i++){ const fall=H/segs*(0.7+cr.next()*0.7) x += spread*fall*0.5 + (cr.next()-0.5)*8 + Math.sin(t*0.9+s+i*0.35)*Math.min(W,H)*0.018 y += fall pts.push({x,y}); if(y>H)break } // shadow p.noFill(); p.strokeWeight(lw); p.stroke(0,0,0,55) p.beginShape(); pts.forEach(pt=>p.vertex(pt.x+3,pt.y+3)); p.endShape() gegoStroke(p,o,h,t,s,210); p.beginShape(); pts.forEach(pt=>p.vertex(pt.x,pt.y)); p.endShape() // node dots at kinks p.noStroke(); pts.forEach((pt,i)=>{ if(i%3===0){p.fill(10,10,10,200);p.circle(pt.x,pt.y,lw*2.6)} }) } } // DIBUJOS SIN PAPEL: sparse — a handful of kinked open polylines on near-empty // ground, big negative space, faint drop shadows. The minimal end. function gegoDibujos(p, c, o, cr){ const {t}=o; const W=c.w,H=c.h const strokes=3+Math.floor(cr.next()*5) const h=gegoHue(o,cr); const lw=Math.max(1.0,Math.min(W,H)*0.005) for(let s=0;s<strokes;s++){ let x=W*(0.15+cr.next()*0.7), y=H*(0.15+cr.next()*0.7) const kinks=2+Math.floor(cr.next()*4); const pts=[{x,y}] for(let k=0;k<kinks;k++){ const ang=cr.next()*6.28, len=Math.min(W,H)*(0.1+cr.next()*0.22) x+=Math.cos(ang)*len + Math.sin(t*0.8+s+k)*Math.min(W,H)*0.02 y+=Math.sin(ang)*len + Math.cos(t*0.7+s+k)*Math.min(W,H)*0.02 pts.push({x,y}) } p.noFill(); p.strokeWeight(lw); p.stroke(0,0,0,55) p.beginShape(); pts.forEach(pt=>p.vertex(pt.x+3,pt.y+3)); p.endShape() gegoStroke(p,o,h,t,s,220); p.beginShape(); pts.forEach(pt=>p.vertex(pt.x,pt.y)); p.endShape() p.noStroke(); pts.forEach(pt=>{ p.fill(10,10,10,200); p.circle(pt.x,pt.y,lw*2.4) }) } } // ============================================================================ // renderComposition — the production entry. Reads from the hash-derived `traits` // (engine/lib/traits.js) and paints the full Cinética composition. // ============================================================================ function renderComposition(p, { traits, hash, width, height }, t = 0) { hash = normalizeHash(hash) // canonical form so per-cell seeds are stable const palette = getPalette(traits.palette) const rng = makeRng(hash) const W = width || p.width, H = height || p.height const baseHues = palette.colors.slice(0, 4).map(hexToRgb).map(rgb2hsl) const seqLen = baseHues.length const phase = traits.phase const irid = traits.iridescence const geo = traits.geo const sweepSpeed = traits.sweepSpeed p.background(palette.bg) p.noStroke() // GEOMETRY ENGINE (the Silva signature) let cells switch (traits.engine) { case 'single': cells = [{ x: 0, y: 0, w: W, h: H, depth: 0 }]; break case 'deep': cells = goldenCells(0, 0, W, H, 6, rng, geo); break case 'mandala':cells = mandalaCells(W, H, rng, geo); break case 'medium': default: cells = goldenCells(0, 0, W, H, 4, rng, geo); break } // event from traits. For radial/embedded events, pick the focal cell from // NON-dominating cells so the event has a real nested home (this is what lets // spiral/singularity actually render instead of always being gated). let pieceEvent = traits.event const preferSmall = RADIAL_FILL_EVENTS.includes(pieceEvent) || pieceEvent === 'rings' || pieceEvent === 'squares' const focal = pickFocal(cells, W, H, preferSmall) // GATE: last-resort — if even the chosen focal cell dominates (single field), // downgrade a radial-fill event to a composition-safe one. if (RADIAL_FILL_EVENTS.includes(pieceEvent)) { const fc = cells[focal] const fillsCanvas = !fc || (fc.w * fc.h) > (W * H * 0.45) || cells.length <= 2 if (fillsCanvas) pieceEvent = safeEvent(hash) } // render each cell in its master mode cells.forEach((c, idx) => { const cr = makeRng(hash + ':' + idx) const horizontal = cr.next() < (0.30 + geo * 0.20) const isFocal = (idx === focal && pieceEvent !== 'none') const cellDensity = 5 + cr.next() * 10 let cellMode = traits.mode if (traits.mode === 'hybrid') cellMode = cr.weighted([['cruzdiez', 5], ['soto', 3], ['gego', 2]]) const sotoMono = (traits.sotoColor === 'mono') || (traits.sotoColor === 'trait' && cr.bool(0.5)) const o = { baseHues, seqLen, phase: phase + cr.next(), irid, geo, t, sweepSpeed, horizontal, event: isFocal ? pieceEvent : 'none', cellSeed: hash + ':' + idx, cellDensity, mode: cellMode, palette, sotoSub: null, gegoSub: null, sotoMono, } if (cellMode === 'soto' || cellMode === 'gego') { p.push(); p.translate(c.x, c.y) if (cellMode === 'soto') drawSoto(p, { x: 0, y: 0, w: c.w, h: c.h }, o) else drawGego(p, { x: 0, y: 0, w: c.w, h: c.h }, o) p.pop() } else { renderCell(p, c, o) } }) // nested-cell outlines (skip focal cell so events don't get grid bleed) if (geo > 0.3) { p.noFill() cells.forEach((c, idx) => { if (idx === focal && pieceEvent !== 'none') return p.stroke(0, 0, 0, 40); p.strokeWeight(1); p.rect(c.x, c.y, c.w, c.h) p.stroke(255, 255, 255, 18); p.rect(c.x + 1.2, c.y + 1.2, c.w - 2.4, c.h - 2.4) }) p.noStroke() } } /* ===== engine/render.js ===== */ // Shared render entry — the single draw path used by the mint sketch. // // Given a hash: derive traits, resolve palette/features, and render the full // Cinética composition (engine/compose.js) — geometry engine, per-cell masters // (Cruz-Diez / Soto / Gego / hybrid), holographic lenticular + sheen, gated event. // Collection aspect ratio — 2:3 portrait (gallery-canvas proportions). // Single source of truth: sketch, gallery, and stress all derive their canvas // dimensions from this so what you curate matches what mints. const ASPECT = { w: 2, h: 3 } const LOGICAL_CANVAS = { w: 1200, h: 1800 } // Given the longer/available pixel budget, return [width, height] at 2:3. // `box` is the max square-ish space available; we fit a 2:3 portrait inside it. function canvasDims(maxW, maxH) { // fit a 2:3 rect inside (maxW, maxH) let h = maxH let w = (h * ASPECT.w) / ASPECT.h if (w > maxW) { w = maxW h = (w * ASPECT.h) / ASPECT.w } return [Math.round(w), Math.round(h)] } // Build the full render context for a hash (traits + palette + features). function makePiece(hash) { const traits = deriveTraits(hash) const palette = getPalette(traits.palette) const features = buildFeatures(traits) return { hash, traits, palette, features } } // Paint one frame of a piece onto a p5 instance at time t (seconds). function renderFrame(p, piece, t = 0) { const sx = p.width / LOGICAL_CANVAS.w const sy = p.height / LOGICAL_CANVAS.h p.push() p.scale(sx, sy) renderComposition(p, { traits: piece.traits, hash: piece.hash, width: LOGICAL_CANVAS.w, height: LOGICAL_CANVAS.h, }, t) p.pop() } return { makePiece, renderFrame, canvasDims }; })(); </script> <!-- 4. Sketch: seed from $art, render animated, hand traits + snapshot to TL --> <script> (function(){ const { makePiece, renderFrame, canvasDims } = CinéticaEngine; // SEED. tokenId + blockhash always included; minter ties the piece to the // collector. A `seed=` URL param (curated drops) overrides everything in art.js. $art.seedFrom('minter'); const SEED = $art.getSeed(); // BUILD. Our engine derives ALL traits/composition deterministically from the // seed string (art.js uses the same xmur3+sfc32 PRNG, so randomness is the seed). const piece = makePiece(SEED); window.features = piece.features; // platform rarity readers window.cinetica = { makePiece: makePiece, piece: piece, seed: SEED }; // debug/inspection $art.setTraits(piece.features); // -> mint metadata (#art-traits) // RENDER — ANIMATED. The draw loop runs continuously so collectors see the // living kinetic motion. Only the marketplace thumbnail freezes a fixed frame. const FREEZE_T = 0; let snapped = false; new p5(function(p){ p.setup = function(){ const d = canvasDims(window.innerWidth, window.innerHeight); p.createCanvas(d[0], d[1]); p.pixelDensity(Math.min(2, window.devicePixelRatio || 1)); }; p.draw = function(){ const t = $art.captureMode ? FREEZE_T : p.millis() / 1000; renderFrame(p, piece, t); if(!snapped){ snapped = true; $art.snapshot(function(){ p.noLoop(); }); } }; p.windowResized = function(){ const d = canvasDims(window.innerWidth, window.innerHeight); p.resizeCanvas(d[0], d[1]); }; }); })(); </script> </body> </html>