setcpm(112/4) // ========================================== // SOL LEWITT / SOUND IS GEOMETRY 9.0 // ATTRACTION & RESONANCE // // 8 ARCS / 28 COMBINATIONS // 112 BARS / APPROX. 4 MINUTES // // CELLO + WARM PAD // GEOMETRIC ATTRACTION // REAL ARC INTERSECTIONS // SOFT VIBRAPHONE RESONANCES // MONOCHROME VISUALIZATION // ========================================== // 01 - MASTER SOUND SETTINGS const melodyVolume = 2 const brightness = 0.65 const soundPoints = 16 // Strength of the interaction between arcs const attractionStrength = 1 // Volume of intersection accents const resonanceVolume = 0.035 // Main instruments const instrumentA = 'gm_cello' const instrumentB = 'gm_pad_warm' // Quiet third voice for intersections const resonanceInstrument = 'gm_vibraphone' // 02 - EIGHT GEOMETRIC ARCS const arcs = [ {cx:0,cy:0,r:1,a:0,b:Math.PI/2}, {cx:1,cy:0,r:1,a:Math.PI/2,b:Math.PI}, {cx:1,cy:1,r:1,a:Math.PI,b:1.5*Math.PI}, {cx:0,cy:1,r:1,a:-Math.PI/2,b:0}, {cx:0.5,cy:0,r:0.5,a:0,b:Math.PI}, {cx:1,cy:0.5,r:0.5,a:Math.PI/2,b:1.5*Math.PI}, {cx:0.5,cy:1,r:0.5,a:Math.PI,b:2*Math.PI}, {cx:0,cy:0.5,r:0.5,a:-Math.PI/2,b:Math.PI/2} ] // 03 - BASIC GEOMETRY function clamp(x,min,max) { return Math.max(min,Math.min(max,x)) } function pointOnArc(arc,t) { const angle = arc.a+(arc.b-arc.a)*t return { x:arc.cx+arc.r*Math.cos(angle), y:arc.cy+arc.r*Math.sin(angle) } } // 04 - GENERATE 28 UNIQUE PAIRS const pairs=[] for(let i=0;i<8;i++) { for(let j=i+1;j<8;j++) { pairs.push([i,j]) } } // ========================================== // NEW MECHANIC 1 // GEOMETRIC ATTRACTION // ========================================== // 05 - DISTANCE BETWEEN TWO MOVING POINTS function arcDistance(idA,idB,t) { const a=pointOnArc(arcs[idA],t) const b=pointOnArc(arcs[idB],t) return Math.hypot( a.x-b.x, a.y-b.y ) } // 06 - ATTRACTION VALUE // 0 = arcs far apart // 1 = arcs very close function arcAttraction(idA,idB,t) { const distance=arcDistance(idA,idB,t) const closeness=clamp( (0.90-distance)/0.70, 0,1 ) return closeness*attractionStrength } // ========================================== // NEW MECHANIC 2 // REAL GEOMETRIC INTERSECTIONS // ========================================== // 07 - POSITION ALONG AN ARC // Returns t from 0 to 1 or null function arcParameter(arc,x,y) { const angle=Math.atan2( y-arc.cy, x-arc.cx ) for(let turn=-2;turn<=2;turn++) { const adjusted= angle+turn*2*Math.PI const t= (adjusted-arc.a)/(arc.b-arc.a) if(t>=-0.0001 && t<=1.0001) { return clamp(t,0,1) } } return null } // 08 - INTERSECTION OF TWO CIRCULAR ARCS function arcIntersections(idA,idB) { const a=arcs[idA] const b=arcs[idB] const dx=b.cx-a.cx const dy=b.cy-a.cy const d=Math.hypot(dx,dy) const eps=0.000001 // No intersection or coincident circles if(da.r+b.r+eps) return [] if(d1 || y<0 || y>1) { continue } const tA=arcParameter(a,x,y) const tB=arcParameter(b,x,y) if(tA===null || tB===null) { continue } // Exclude endpoints and corner contacts // Only real interior encounters count if( tA<=0.04 || tA>=0.96 || tB<=0.04 || tB>=0.96 ) { continue } const duplicate=results.some(p=> Math.hypot(p.x-x,p.y-y)a.time-b.time ) } // 09 - INTERSECTION RESONANCE // One quiet musical accent per valid crossing function resonanceOfPair(idA,idB) { const crossings=arcIntersections(idA,idB) if(crossings.length===0) { return silence } // One 16-step grid across four bars const steps=Array.from( {length:16}, ()=>silence ) for(const crossing of crossings) { const index=clamp( Math.floor(crossing.time*16), 0,15 ) // Higher intersections get a higher note const pitch= crossing.y<0.34 ? 'e5' : crossing.y<0.67 ? 'c5' : 'a4' steps[index]=note(pitch) } return seq(...steps) .s(resonanceInstrument) .attack(0.015) .decay(0.65) .sustain(0.12) .release(1.1) .lpf(1700) .room(0.55) .gain(resonanceVolume) .pan(0.5) .orbit(10) .slow(4) } // ========================================== // THE GEOMETRIC MUSICAL VOICES // ========================================== // 10 - SOUND OF AN ARC // Cello and Warm Pad retain the same melody // Attraction only changes their relationship function soundOfArc(id,partner,section,voice) { const arc=arcs[id] const count=soundPoints const points=Array.from( {length:count}, (_,i)=>{ const t=(i+0.5)/count const p=pointOnArc(arc,t) const angle= arc.a+(arc.b-arc.a)*t const motion=Math.abs( Math.cos(angle) ) const distance=Math.hypot( p.x-0.5, p.y-0.5 ) // NEW: relationship with the other arc const attraction=arcAttraction( id,partner,t ) return { p,motion,distance,attraction } } ) // PITCH // Eight melodic movements across 16 steps const pitches=points.map((data,i)=>{ const t= (Math.floor(i/2)*2+1)/count const p=pointOnArc(arc,t) return clamp( Math.round((1-p.y)*5), 0,5 ) }) // PAN // When arcs approach, the voices move // towards the center of the stereo field const pans=points.map(({p,attraction})=>{ const naturalPan=clamp( 0.16+p.x*0.68, 0,1 ) const convergence= 1-0.70*attraction return 0.5+ (naturalPan-0.5)*convergence }) // FILTER // Sound becomes slightly warmer // when the two lines approach const filters=points.map( ({motion,attraction})=>{ const base=voice===0 ? 520+motion*650 : 740+motion*1100 return base*brightness* (1-0.18*attraction) } ) // DEPTH // Distant arcs have more space // Nearby arcs sound more intimate const depths=points.map( ({distance,attraction})=>{ const base= 0.18+ clamp(distance/0.71,0,1)*0.27 return clamp( base-0.12*attraction, 0.08,0.65 ) } ) // EMOTIONAL DEVELOPMENT const intensity= section<4 ? 0.55 : section<14 ? 0.85 : section<20 ? 1.00 : section<23 ? 0.50 : section<26 ? 1.10 : 0.40 // DYNAMICS // A slight lift when the lines approach const volumes=points.map( ({p,attraction})=>{ const base= ( 0.045+ clamp(1-p.y,0,1)*0.014 ) *intensity *melodyVolume *(voice===0 ? 1 : 0.85) return base* (1+0.09*attraction) } ) // FINAL GEOMETRIC INSTRUMENT return n(seq(...pitches)) .scale( voice===0 ? 'A2:minor:pentatonic' : 'A3:minor:pentatonic' ) // Your chosen pair .s( voice===0 ? instrumentA : instrumentB ) // Geometry + interaction .pan(seq(...pans)) .lpf(seq(...filters)) .room(seq(...depths)) // Smooth articulation .attack(0.10) .decay(0.30) .sustain(0.50) .release(0.55) .gain(seq(...volumes)) .orbit(voice+2) .slow(4) } // 11 - BUILD ALL 28 MUSICAL SCENES const scenes=pairs.map(([a,b],i)=>[ 4, stack( // First geometric voice soundOfArc(a,b,i,0), // Second geometric voice soundOfArc(b,a,i,1), // NEW: intersection resonance resonanceOfPair(a,b) ) ]) // ========================================== // MONOCHROME VISUAL SYSTEM // ========================================== // 12 - TILE LAYOUT // 7 columns x 4 rows, no gaps function tileLayout(index,X,Y,S) { return { x:X+(index%7)*S, y:Y+Math.floor(index/7)*S, size:S } } // 13 - GEOMETRY TO WALL COORDINATES function wallPoint(index,slot,t,X,Y,S) { const id=pairs[index][slot] const p=pointOnArc(arcs[id],t) const tile=tileLayout( index,X,Y,S ) return { x:tile.x+p.x*S, y:tile.y+p.y*S } } // 14 - DRAW INDEPENDENT WHITE ARCS function drawTrack( ctx,slot,count,active,progress, finished,X,Y,S ) { if(count<=0) return ctx.beginPath() for(let i=0;i=112 const active=Math.min( 27,Math.floor(elapsed/4) ) const progress=finished ? 1 : clamp( (elapsed-active*4)/4, 0,1 ) const visibleCount=finished ? 28 : active+1 // BLACK BACKGROUND ctx.clearRect(0,0,W,H) ctx.fillStyle='#000000' ctx.fillRect(0,0,W,H) // SEAMLESS WALL const header=H*0.19 const footer=H*0.14 const margin=W*0.035 const cell=Math.min( (W-2*margin)/7, (H-header-footer)/4 ) const gridW=cell*7 const gridH=cell*4 const X=(W-gridW)/2 const Y=header+ (H-header-footer-gridH)/2 // TITLE ctx.textAlign='left' ctx.textBaseline='alphabetic' ctx.fillStyle='#FFFFFF' ctx.font='bold '+ Math.round(cell*0.21)+ 'px monospace' ctx.fillText( 'SOL LEWITT / SOUND IS GEOMETRY', X,Y-cell*0.66 ) // CURRENT COMBINATION const current=pairs[active] const status=finished ? 'COMPOSITION COMPLETE' : 'NOW DRAWING '+ (current[0]+1)+'/'+(current[1]+1) ctx.font= Math.round(cell*0.13)+ 'px monospace' ctx.fillText( status, X,Y-cell*0.24 ) ctx.textAlign='right' ctx.fillText( (finished?28:active+1)+' / 28', X+gridW, Y-cell*0.24 ) // BLACK WALL ctx.fillStyle='#000000' ctx.fillRect(X,Y,gridW,gridH) // NEARLY INVISIBLE GRID ctx.strokeStyle= 'rgba(255,255,255,0.05)' ctx.lineWidth=0.75 ctx.beginPath() for(let i=0;i<=7;i++) { const x=X+i*cell ctx.moveTo(x,Y) ctx.lineTo(x,Y+gridH) } for(let j=0;j<=4;j++) { const y=Y+j*cell ctx.moveTo(X,y) ctx.lineTo(X+gridW,y) } ctx.stroke() // ACTIVE CELL if(!finished) { const tile=tileLayout( active,X,Y,cell ) ctx.strokeStyle= 'rgba(255,255,255,0.18)' ctx.lineWidth=1 ctx.strokeRect( tile.x+0.5, tile.y+0.5, cell-1, cell-1 ) } // FRACTION LABELS pairs.forEach((pair,i)=>{ const tile=tileLayout( i,X,Y,cell ) const label= (pair[0]+1)+'/'+(pair[1]+1) const currentCell= !finished && i===active const fontSize=Math.max( 8, Math.round(cell*0.085) ) ctx.font=fontSize+'px monospace' const textW= ctx.measureText(label).width const padX=cell*0.02 const padY=cell*0.012 const boxW=textW+padX*2 const boxH=fontSize+padY*2 const labelX= tile.x+cell*0.02 const labelY= tile.y+cell*0.02 // Small black backing ctx.fillStyle='#000000' ctx.fillRect( labelX,labelY, boxW,boxH ) // White label ctx.textAlign='left' ctx.textBaseline='alphabetic' ctx.fillStyle='#FFFFFF' ctx.fillText( label, labelX+padX, labelY+padY+fontSize*0.82 ) // Underline current fraction if(currentCell) { ctx.fillStyle='#FFFFFF' ctx.fillRect( labelX, labelY+boxH+cell*0.008, boxW, Math.max(1,cell*0.008) ) } }) // GEOMETRY ALWAYS ON TOP drawTrack( ctx,0,visibleCount, active,progress,finished, X,Y,cell ) drawTrack( ctx,1,visibleCount, active,progress,finished, X,Y,cell ) // PROGRESS BAR const barY= Y+gridH+cell*0.29 const barH=Math.max( 2,cell*0.035 ) ctx.fillStyle= 'rgba(255,255,255,0.12)' ctx.fillRect( X,barY,gridW,barH ) ctx.fillStyle='#FFFFFF' ctx.fillRect( X,barY, gridW*(elapsed/112), barH ) // FOOTER ctx.textAlign='center' ctx.fillStyle='#FFFFFF' ctx.font= Math.round(cell*0.12)+ 'px monospace' ctx.fillText( finished ? '28 COMBINATIONS / ONE COMPLETE WALL' : '8 ARCS / 28 COMBINATIONS', W/2, barY+cell*0.25 ) ctx.textAlign='left' ctx.textBaseline='alphabetic' } // ========================================== // AUDIO PERFORMANCE // ========================================== // 16 - GEOMETRIC MELODIES // + ATTRACTION + INTERSECTION RESONANCE // + VISUALIZATION $: arrange(...scenes) .onPaint((ctx,time)=>{ drawComposition(time) }) // 17 - EMOTIONAL HARMONIC FIELD $: note( "<[a3,c4,e4,b4] [f3,a3,c4,e4] [c4,e4,g4,b4] [g3,b3,d4,e4]>/4" ) .s('triangle') .attack(0.6) .sustain(0.45) .release(1.8) .lpf(1350) .room(0.65) .gain(0.055) .orbit(4) // 18 - DEEP WARM BASS $: note("/4") .struct("x ~ x [~ x] x ~ x ~") .s('triangle') .attack(0.08) .decay(0.4) .sustain(0.18) .release(0.25) .lpf(250) .gain(0.18) .mask("<0!8 1!96 0!8>") .orbit(5) // 19 - SOFT TR808 KICK $: s("bd*4") .bank('RolandTR808') .lpf(420) .gain(0.31) .mask("<0!4 1!104 0!4>") .orbit(6) // 20 - DUSTY BACKBEAT $: s("~ sd ~ sd") .bank('RolandTR808') .lpf(1400) .gain(0.10) .room(0.22) .mask("<0!8 1!100 0!4>") .orbit(7) // 21 - DELICATE HI HATS $: s("~ hh ~ hh ~ hh ~ hh") .bank('RolandTR808') .lpf(2900) .gain(0.065) .late(0.012) .mask("<0!12 1!96 0!4>") .orbit(8) // 22 - FINAL EMOTIONAL CHORD $: arrange( [108,silence], [ 4, note('[a3,c4,e4,b4]') .s('triangle') .attack(1.2) .release(5) .lpf(1300) .room(0.85) .gain(0.11) .slow(4) .orbit(9) ] )