0x77b35947…cc7dsent to0x2758442e…81ae·#25,448,164·view on Etherscan
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>