0x55969715…1928sent to0x55969715…1928·#22,360,776·view on Etherscan
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>THE PURRMINATORS | Phase 2 Living Geometry Melt</title>
<style>
html,body{margin:0;height:100%;display:flex;justify-content:center;align-items:center;background:#222}
#box{position:relative;width:400px;height:400px;border:2px solid #666;overflow:hidden;background:#000}
#phase1,#melt{position:absolute;top:0;left:0;width:400px;height:400px;border:none}
#melt{z-index:2;display:none;pointer-events:none;image-rendering:pixelated}
#panel{position:absolute;top:6px;left:6px;z-index:3;display:flex;gap:6px}
button{font-size:13px;background:rgba(0,0,0,.55);color:#fff;border:1px solid #888;border-radius:4px;padding:4px 10px;cursor:pointer}
button:disabled{opacity:.4;cursor:default}
</style>
<script src="https://cdn.jsdelivr.net/npm/web3/dist/web3.min.js"></script>
</head>
<body>
<div id="box">
<iframe id="phase1"></iframe>
<canvas id="melt"></canvas>
<div id="panel">
<button id="meltBtn">Melt</button>
<button id="reformBtn" disabled>Re-form</button>
</div>
</div>
<script>
// ── CONSTANTS & GLOBALS ─────────────────────────────────────────────
const TX_PHASE1 = "0x9f1e8063c9277ddd50651472ba8a3e7fe5ea7e8df2f8722b4a0f5646492b55de";
const SIZE = 400, DPR = devicePixelRatio || 1;
let web3, phase1Frame, melt, ctx;
let particles = [], mode = "none", active = false;
let lastTS = 0, tGlobal = 0;
// ── UTILS ─────────────────────────────────────────────────────────────
const rnd = (hi=1, lo=0) => Math.random()*(hi-lo) + lo;
const colouredFamilies = ["red","orange","yellow","green","cyan","blue","magenta"];
const geometryPool = ["circle","fig8","triangle","squircle","spiral","ellipse","star","square","hexagon"];
const geomState = {};
// pick random geometry for each colour family
function mutateAllGeometries(){
colouredFamilies.forEach(fam => {
const type = geometryPool[Math.floor(Math.random()*geometryPool.length)];
switch(type){
case "circle":
geomState[fam]={type,R:rnd(80,120),speed:rnd(0.3,0.1),phase:rnd(0,2*Math.PI)};break;
case "fig8":
geomState[fam]={type,R:rnd(60,90),speed:rnd(0.4,0.15),phase:rnd(0,2*Math.PI)};break;
case "triangle":
geomState[fam]={type,R:rnd(70,100),rotSpeed:rnd(0.5,0.2),phase:rnd(0,2*Math.PI)};break;
case "squircle":
geomState[fam]={type,R:rnd(60,100),rot:rnd(0,2*Math.PI)};break;
case "spiral":
geomState[fam]={type,a:rnd(4,2),b:rnd(3,1),rotSpeed:rnd(0.4,0.15)};break;
case "ellipse":
geomState[fam]={type,rx:rnd(60,100),ry:rnd(40,70),tilt:rnd(0,2*Math.PI),speed:rnd(0.4,0.15)};break;
case "star":
geomState[fam]={type,R:rnd(70,100),r:rnd(25,35),rotSpeed:rnd(0.6,0.2)};break;
case "square":
geomState[fam]={type,S:rnd(120,160),speed:rnd(0.35,0.15),rot:rnd(0,2*Math.PI)};break;
case "hexagon":
geomState[fam]={type,R:rnd(85,115),speed:rnd(0.35,0.15),rot:rnd(0,2*Math.PI)};break;
}
});
}
// compute target geometry point for a fam + time + index
function geomTarget(fam,t,idx){
const s=geomState[fam]; if(!s) return [200,200];
switch(s.type){
case "circle":{
const ang=t*s.speed+s.phase+idx*0.0008;
return [200+Math.cos(ang)*s.R,200+Math.sin(ang)*s.R];
}
case "fig8":{
const ang=t*s.speed+s.phase+idx*0.001;
return [200+s.R*Math.sin(ang),200+s.R*Math.sin(ang)*Math.cos(ang)];
}
case "triangle":{
const ang=(idx%3)*2*Math.PI/3 + s.phase + t*s.rotSpeed;
return [200+Math.cos(ang)*s.R,200+Math.sin(ang)*s.R];
}
case "squircle":{
const ang=t*0.8+s.rot,n=4;
const x=s.R*Math.sign(Math.cos(ang))*Math.pow(Math.abs(Math.cos(ang)),2/n);
const y=s.R*Math.sign(Math.sin(ang))*Math.pow(Math.abs(Math.sin(ang)),2/n);
return [200+x,200+y];
}
case "spiral":{
const ang=t*s.rotSpeed+idx*0.002;
const r=s.a*Math.exp(s.b*ang*0.05);
return [200+r*Math.cos(ang),200+r*Math.sin(ang)];
}
case "ellipse":{
const ang=t*s.speed+idx*0.001;
const x=s.rx*Math.cos(ang), y=s.ry*Math.sin(ang);
const c=Math.cos(s.tilt),si=Math.sin(s.tilt);
return [200+x*c-y*si,200+x*si+y*c];
}
case "star":{
const k=5, ang=(idx%k)*2*Math.PI/k + t*s.rotSpeed;
const r=((idx%k)%2===0)?s.R:s.r;
return [200+Math.cos(ang)*r,200+Math.sin(ang)*r];
}
case "square":{
const per=(t*s.speed+idx*0.0008)%4,half=s.S/2,rot=s.rot;
let lx,ly;
if(per<1){lx=-half+per*s.S;ly=-half}
else if(per<2){lx=half;ly=-half+(per-1)*s.S}
else if(per<3){lx=half-(per-2)*s.S;ly=half}
else{lx=-half;ly=half-(per-3)*s.S}
const c=Math.cos(rot),si=Math.sin(rot);
return [200+lx*c-ly*si,200+lx*si+ly*c];
}
case "hexagon":{
const per=(t*s.speed+idx*0.0008)%6,ang=per*Math.PI/3;
const v1=[Math.cos(ang)*s.R,Math.sin(ang)*s.R],
v2=[Math.cos(ang+Math.PI/3)*s.R,Math.sin(ang+Math.PI/3)*s.R],
f=per%1;
const lx=v1[0]*(1-f)+v2[0]*f, ly=v1[1]*(1-f)+v2[1]*f;
const c=Math.cos(s.rot),si=Math.sin(s.rot);
return [200+lx*c-ly*si,200+lx*si+ly*c];
}
}
return [200,200];
}
// fetch on-chain HTML
async function fetchHtml(tx){
try {
const t = await web3.eth.getTransaction(tx);
if(!t||!t.input) return null;
const hex = t.input.startsWith("0x")?t.input.slice(2):t.input;
const bytes = web3.utils.hexToBytes(hex);
const txt = new TextDecoder().decode(new Uint8Array(bytes));
const m = txt.match(/data:text\/html;base64,([A-Za-z0-9+/=]+)/);
return m? atob(m[1]) : null;
} catch { return null; }
}
// simple hue-based classification
function rgbaToFamily(r,g,b){
const M=Math.max(r,g,b)/255, m=Math.min(r,g,b)/255, d=M-m, L=(M+m)/2;
if(d<0.01) return L<0.15?"black":L>0.85?"white":"grey";
let H;
if(M===r/255) H=((g-b)/d)%6;
else if(M===g/255) H=(b-r)/d+2;
else H=(r-g)/d+4;
H*=60; if(H<0) H+=360;
if(H<20||H>=340) return "red";
if(H<45) return "orange";
if(H<70) return "yellow";
if(H<150) return "green";
if(H<200) return "cyan";
if(H<260) return "blue";
return "magenta";
}
// ── MELT / REFORM ─────────────────────────────────────────────────────
let geomTimer;
function startMelt(){
if(active) return;
const src = phase1Frame.contentDocument?.getElementById("mainCanvas");
if(!src){ alert("Phase-1 canvas not found"); return; }
const data = src.getContext("2d").getImageData(0,0,SIZE,SIZE).data;
particles = [];
for(let y=0;y<SIZE;y++){
for(let x=0;x<SIZE;x++){
const i=(y*SIZE+x)*4;
if(data[i+3]<10) continue;
const r=data[i],g=data[i+1],b=data[i+2],a=data[i+3]/255;
particles.push({
x,y, baseX:x, baseY:y,
vx:(Math.random()-0.5)*40,
vy:(Math.random()-0.5)*40,
rgb:[r,g,b], alpha:a,
fam:rgbaToFamily(r,g,b),
zPhase:Math.random()*2*Math.PI,
size:1,
noiseMag:rnd(6,12),
noiseSpeed:rnd(1.5,0.8),
noisePhase:rnd(0,2*Math.PI)
});
}
}
phase1Frame.style.visibility="hidden";
melt.style.display="block";
meltBtn.disabled = true;
reformBtn.disabled = false;
active=true; mode="melt"; tGlobal=0;
mutateAllGeometries();
geomTimer=setInterval(mutateAllGeometries,4000);
}
function startReform(){
if(mode!=="melt")return;
mode="reform";
clearInterval(geomTimer);
particles.forEach(p=>{
p.vx+=(p.baseX-p.x)*2.2;
p.vy+=(p.baseY-p.y)*2.2;
});
}
// ── INIT ─────────────────────────────────────────────────────────────
window.addEventListener("load",async()=>{
web3=new Web3(new Web3.providers.HttpProvider(
"https://mainnet.infura.io/v3/330c1ac15fc7483b8d9b6f229938d843"
));
phase1Frame=document.getElementById("phase1");
phase1Frame.srcdoc=await fetchHtml(TX_PHASE1)
||"<h2 style='color:#fff;text-align:center;'>phase-1 not found</h2>";
melt=document.getElementById("melt");
melt.width = SIZE*DPR;
melt.height = SIZE*DPR;
melt.style.width = SIZE+"px";
melt.style.height = SIZE+"px";
ctx=melt.getContext("2d");
ctx.scale(DPR,DPR);
ctx.imageSmoothingEnabled=false;
document.getElementById("meltBtn") .onclick=startMelt;
document.getElementById("reformBtn").onclick=startReform;
requestAnimationFrame(loop);
});
// ── PHYSICS & RENDER ─────────────────────────────────────────────────
function physics(dt){
const B={attract:0.22,jitter:60,damping:0.85,depthFreq:4,depthAmp:4,
dustJitter:55,dustAttract:0.18,wakePush:0.7};
let cX=0,cY=0,vX=0,vY=0,nC=0;
particles.forEach(p=>{
if(colouredFamilies.includes(p.fam)){
cX+=p.x; cY+=p.y; vX+=p.vx; vY+=p.vy; nC++;
}
});
if(nC){cX/=nC;cY/=nC;vX/=nC;vY/=nC;}
let settled=0;
particles.forEach((p,i)=>{
if(mode==="melt"){
if(colouredFamilies.includes(p.fam)){
const [gx,gy]=geomTarget(p.fam,tGlobal,i);
const nX=Math.sin(p.noisePhase+tGlobal*p.noiseSpeed)*p.noiseMag;
const nY=Math.cos(p.noisePhase+tGlobal*p.noiseSpeed)*p.noiseMag;
const dx=gx+nX-p.x, dy=gy+nY-p.y;
p.vx+=dx*B.attract; p.vy+=dy*B.attract;
p.vx+=(Math.random()-0.5)*B.jitter*dt;
p.vy+=(Math.random()-0.5)*B.jitter*dt;
p.zPhase+=dt*B.depthFreq;
p.size=1+B.depthAmp*Math.sin(p.zPhase);
} else {
p.vx+=(Math.random()-0.5)*B.dustJitter*dt;
p.vy+=(Math.random()-0.5)*B.dustJitter*dt;
if(nC){
p.vx+=(cX-p.x)*B.dustAttract*dt;
p.vy+=(cY-p.y)*B.dustAttract*dt;
p.vx+=vX*B.wakePush*dt;
p.vy+=vY*B.wakePush*dt;
}
p.zPhase+=dt*B.depthFreq*0.6;
p.size=1+0.5*B.depthAmp*Math.sin(p.zPhase);
}
} else {
const dx=p.baseX-p.x, dy=p.baseY-p.y;
p.vx+=dx*0.25; p.vy+=dy*0.25;
p.size+=(1-p.size)*6*dt;
if(Math.abs(dx)+Math.abs(dy)<0.3) settled++;
}
p.x+=p.vx*dt; p.y+=p.vy*dt;
p.vx*=B.damping; p.vy*=B.damping;
if(p.x<0){p.x=0;p.vx*=-0.6;}
if(p.x>SIZE-1){p.x=SIZE-1;p.vx*=-0.6;}
if(p.y<0){p.y=0;p.vy*=-0.6;}
if(p.y>SIZE-1){p.y=SIZE-1;p.vy*=-0.4;}
});
if(mode==="reform" && settled===particles.length){
active=false; mode="none";
melt.style.display="none";
phase1Frame.style.visibility="visible";
meltBtn.disabled = false;
reformBtn.disabled = true;
}
}
function render(){
if(!active){ ctx.clearRect(0,0,SIZE,SIZE); return; }
ctx.clearRect(0,0,SIZE,SIZE);
particles.forEach(p=>{
// coloured families pulse their hue
if(colouredFamilies.includes(p.fam)){
const hueMap={red:0,orange:30,yellow:60,green:120,cyan:180,blue:240,magenta:300};
const H = (hueMap[p.fam] + tGlobal*30) % 360;
ctx.fillStyle = `hsla(${H},100%,50%,${p.alpha})`;
} else {
const [r,g,b] = p.rgb;
ctx.fillStyle = `rgba(${r},${g},${b},${p.alpha})`;
}
const s = Math.max(1,Math.round(p.size));
ctx.fillRect(Math.round(p.x-s/2),Math.round(p.y-s/2),s,s);
});
}
function loop(ts){
requestAnimationFrame(loop);
if(!lastTS) lastTS=ts;
let dt=(ts-lastTS)/1000; if(dt>0.05) dt=0.05;
lastTS=ts; tGlobal+=dt;
if(active) physics(dt);
render();
}
</script>
</body>
</html>