Back to Drone Zone — deep field
SOURCE / PINNED RELEASE

Made of little things.

Drone Zone — deep field

Release
578f277a8be1
Author-recorded commit
a96742b428de…
License
LICENSE
Author’s source reference
nostr://npub1n8ga89w8h6tvwamxusfyzexw8gjy84yxu9rxgnmk955cxtml4ujswzxydd/wss%3A%2F%2Fgit.napplet.soy%2F/n-9753e6cf3ec

Archive hash verified: f7f3bf8ff3482551. The source-to-build association is the author’s claim; it has not been independently rebuilt.

src/tape.ts
interface Burst { start:number; duration:number; row:number; direction:number }

/** Final optical treatment of the complete app surface, including the live UI.
 * The scene supplies the clock: no separate animation loop, timers or authority.
 * Expensive displacement/color separation runs only during a short tracking slip. */
export class TapeEffect {
  private amount=.45;
  private time=0;
  private active=false;
  private nextBurst=2.4;
  private burst:Burst|null=null;
  private seed=94103;
  private noiseFrame=-1;
  private disposed=false;
  private overlay=document.getElementById('tape-overlay')!;
  private noise=document.getElementById('tape-noise')!;
  private roll=document.getElementById('tape-roll')!;
  private dropout=document.getElementById('tape-dropout')!;
  private slider=document.getElementById('tape-wear') as HTMLInputElement;
  private readout=document.getElementById('tape-amount')!;
  private pulseButton=document.getElementById('tape-pulse') as HTMLButtonElement;
  private bandA=document.getElementById('tape-band-a')!;
  private bandB=document.getElementById('tape-band-b')!;
  private displacement=document.getElementById('tape-displacement')!;
  private red=document.getElementById('tape-red')!;
  private cyan=document.getElementById('tape-cyan')!;
  constructor(private root:HTMLElement) {
    // A single embedded grain tile, translated at low frequency instead of
    // regenerating a frame-sized texture on every draw.
    const canvas=document.createElement('canvas');canvas.width=128;canvas.height=128;
    const ctx=canvas.getContext('2d')!;const data=ctx.createImageData(128,128);
    for(let i=0;i<data.data.length;i+=4) {
      const luminance=Math.floor(this.random()*256);
      data.data[i]=data.data[i+1]=data.data[i+2]=luminance;data.data[i+3]=180;
    }
    ctx.putImageData(data,0,0);this.noise.style.backgroundImage=`url("${canvas.toDataURL()}")`;
    this.setAmount(this.amount);
  }
  private random() {this.seed=(this.seed*1664525+1013904223)>>>0;return this.seed/4294967296;}
  setAmount(value:number) {
    if(this.disposed)return;
    const previous=this.amount;this.amount=Number.isFinite(value)?Math.max(0,Math.min(1,value)):.45;
    if(previous===0&&this.amount>0)this.nextBurst=this.time+2.4;
    if(this.amount===0)this.burst=null;
    this.slider.value=String(Math.round(this.amount*100));this.readout.textContent=this.amount?`${Math.round(this.amount*100)}%`:'OFF';
    this.root.style.setProperty('--tape-amount',String(this.amount));
    this.root.style.setProperty('--tape-saturation',String(1-this.amount*.12));
    this.render(this.time,this.active);
  }
  pulse() {
    if(this.disposed||!this.active||this.amount===0||this.burst)return false;
    this.burst={start:this.time,duration:.58,row:.2+this.random()*.55,direction:this.random()<.5?-1:1};
    this.nextBurst=this.time+8+this.random()*10;this.render(this.time,this.active);return true;
  }
  render(time:number,active:boolean) {
    if(this.disposed)return;
    this.time=time;this.active=active;
    const enabled=this.amount>0;
    this.root.dataset.tapeEnabled=String(enabled);
    this.root.dataset.tapePaused=String(!active);
    this.overlay.hidden=!enabled;
    if(!enabled||!active) {
      this.burst=null;this.root.dataset.tapeGlitch='false';this.dropout.style.opacity='0';
      this.pulseButton.disabled=true;this.pulseButton.title=enabled?'Resume motion to trigger a glitch':'Increase tape wear to trigger a glitch';return;
    }
    if(time>=this.nextBurst&&!this.burst)this.pulse();
    const frame=Math.floor(time*10);
    if(frame!==this.noiseFrame) {
      this.noiseFrame=frame;this.noise.style.transform=`translate(${Math.floor(this.random()*80)-40}px,${Math.floor(this.random()*80)-40}px)`;
    }
    this.roll.style.transform=`translateY(${((time*.032)%1)*140-20}vh)`;
    if(this.burst&&time-this.burst.start>=this.burst.duration)this.burst=null;
    this.pulseButton.disabled=!!this.burst;this.pulseButton.title='Trigger a brief tracking slip';
    this.root.dataset.tapeGlitch=String(!!this.burst);
    if(!this.burst){this.dropout.style.opacity='0';return;}
    const age=(time-this.burst.start)/this.burst.duration;
    // A tracking kick followed by a chroma echo. Keeping the two full-surface
    // filters in consecutive phases avoids an expensive chained raster pass.
    const tracking=age<.42,phase=tracking?age/.42:(age-.42)/.58;
    this.root.dataset.tapePhase=tracking?'tracking':'chroma';
    const envelope=Math.sin(Math.PI*phase),strength=this.amount*envelope;
    const width=this.root.clientWidth,height=this.root.clientHeight;
    const row=(this.burst.row+age*.17)*height;
    this.bandA.setAttribute('x','-32');this.bandA.setAttribute('width',String(width+64));
    this.bandA.setAttribute('y',String(row));this.bandA.setAttribute('height',String(3+height*.022));
    this.bandB.setAttribute('x','-32');this.bandB.setAttribute('width',String(width+64));
    this.bandB.setAttribute('y',String((this.burst.row+.22-age*.06)*height));this.bandB.setAttribute('height',String(2+height*.006));
    this.displacement.setAttribute('scale',String((28+Math.sin(age*17)*8)*strength*this.burst.direction));
    this.red.setAttribute('dx',String(5*strength));this.cyan.setAttribute('dx',String(-3*strength));
    this.dropout.style.top=`${row}px`;this.dropout.style.opacity=String(envelope*.24);
  }
  destroy() {
    this.disposed=true;this.burst=null;this.root.dataset.tapeEnabled='false';this.root.dataset.tapeGlitch='false';
    this.overlay.hidden=true;this.noise.style.backgroundImage='none';
  }
}