Created
August 20, 2025 23:00
-
-
Save hugoferreira/97d96cd125d749418bb44b381d8e8370 to your computer and use it in GitHub Desktop.
Quantum Optical Lab
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>Quantum Optical Lab</title> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.min.js"></script> | |
| <style> | |
| body { margin: 0; padding: 0; overflow: hidden; background: #000; } | |
| canvas { display: block; } | |
| </style> | |
| </head> | |
| <body> | |
| <script> | |
| // -------------------------------------- | |
| // Utilities | |
| // -------------------------------------- | |
| // Add reflect method to p5.Vector (not built-in) | |
| p5.Vector.prototype.reflect = function(normal) { return this.copy().sub(p5.Vector.mult(normal, 2 * this.dot(normal))); }; | |
| // Bitmask color model (fixes composite filter math) | |
| // R=1, G=2, B=4; combinations via bitwise OR | |
| const COLOR_MASKS = { black:0, red:1, green:2, blue:4, yellow:3, cyan:6, magenta:5, white:7 }; | |
| const MASK_TO_NAME = { 0:"black", 1:"red", 2:"green", 3:"yellow", 4:"blue", 5:"magenta", 6:"cyan", 7:"white" }; | |
| const MASK_TO_RGB = { | |
| 0: [0,0,0], | |
| 1: [255,100,100], // red | |
| 2: [100,255,100], // green | |
| 3: [255,255,100], // yellow (R|G) | |
| 4: [100,100,255], // blue | |
| 5: [255,100,255], // magenta (R|B) | |
| 6: [100,255,255], // cyan (G|B) | |
| 7: [255,255,255] // white | |
| }; | |
| function nameToMask(name){ return COLOR_MASKS[name] ?? 7; } | |
| function maskToName(mask){ return MASK_TO_NAME[mask & 7]; } | |
| // -------------------------------------- | |
| // Colors (UI) | |
| // -------------------------------------- | |
| let COLORS = {}; | |
| function setupColors(){ | |
| COLORS = { | |
| bg: color(25, 40, 70), | |
| gridFine: color(60, 90, 140, 80), | |
| gridMajor: color(80, 120, 180, 120), | |
| beamStd: color(160, 160, 160, 180), | |
| beamHot: color(184, 115, 51, 180), | |
| plate: color(100, 100, 100, 200), | |
| hole: color(25, 40, 70), | |
| mirrorBody: color(200, 220, 240, 120), | |
| mirrorSurface: color(255, 255, 255, 180), | |
| mirrorSelected: color(255, 255, 100, 150), | |
| prismBody: color(180, 200, 255, 100), | |
| prismRefraction: color(220, 230, 255, 60), | |
| blackHoleCore: color(0, 0, 0, 200), | |
| blackHoleOuter: color(50, 0, 100, 100), | |
| whiteHoleCore: color(255, 255, 255, 200), | |
| whiteHoleOuter: color(255, 250, 200, 100) | |
| }; | |
| } | |
| // -------------------------------------- | |
| // Filter definitions (with bitmasks) | |
| // -------------------------------------- | |
| const FILTER_TYPES = { | |
| red: { passes:["red"], col:[255,0,0,100], label:"R", passMask: COLOR_MASKS.red }, | |
| green: { passes:["green"], col:[0,255,0,100], label:"G", passMask: COLOR_MASKS.green }, | |
| blue: { passes:["blue"], col:[0,0,255,100], label:"B", passMask: COLOR_MASKS.blue }, | |
| yellow: { passes:["red","green"], col:[255,255,0,100], label:"Y", passMask: (COLOR_MASKS.red|COLOR_MASKS.green) }, | |
| cyan: { passes:["green","blue"], col:[0,255,255,100], label:"C", passMask: (COLOR_MASKS.green|COLOR_MASKS.blue) }, | |
| magenta: { passes:["red","blue"], col:[255,0,255,100], label:"M", passMask: (COLOR_MASKS.red|COLOR_MASKS.blue) } | |
| }; | |
| // -------------------------------------- | |
| // Elements | |
| // -------------------------------------- | |
| class Mirror { | |
| constructor(start, end, rotSpeed=0){ | |
| this.type='mirror'; | |
| this.baseStart=start; this.baseEnd=end; this.rotSpeed=rotSpeed; this.angle=0; | |
| this.isRotating=rotSpeed!==0; this.isSelected=false; | |
| this.baseCenter=p5.Vector.lerp(start,end,0.5); this.halfLen=p5.Vector.dist(start,end)/2; | |
| this.updateGeometry(); | |
| } | |
| updateGeometry(){ | |
| if(this.isRotating){ | |
| let dir=p5.Vector.fromAngle(this.angle,this.halfLen); | |
| this.start=p5.Vector.sub(this.baseCenter,dir); this.end=p5.Vector.add(this.baseCenter,dir); | |
| } else { this.start=this.baseStart.copy(); this.end=this.baseEnd.copy(); } | |
| this.center=p5.Vector.lerp(this.start,this.end,0.5); this.len=p5.Vector.dist(this.start,this.end); | |
| let d=p5.Vector.sub(this.end,this.start); this.normal=createVector(-d.y,d.x).normalize(); | |
| } | |
| update(){ if(this.isRotating && !this.isSelected){ this.angle+=this.rotSpeed; this.updateGeometry(); } } | |
| moveTo(pos){ let off=p5.Vector.sub(pos,this.center); this.baseCenter.add(off); if(!this.isRotating){ this.baseStart.add(off); this.baseEnd.add(off);} this.updateGeometry(); } | |
| contains(p){ let d=p5.Vector.sub(this.end,this.start); let f=p5.Vector.sub(this.start,p); let a=d.dot(d); let b=2*f.dot(d); let c=f.dot(f)-25; let disc=b*b-4*a*c; if(disc<0) return false; disc=sqrt(disc); let t1=(-b-disc)/(2*a); let t2=(-b+disc)/(2*a); return (t1>=0&&t1<=1)||(t2>=0&&t2<=1)||(t1<0&&t2>1); } | |
| intersectRay(origin,dir,maxDist){ let d=p5.Vector.sub(this.end,this.start); let det=dir.x*d.y - dir.y*d.x; if(abs(det)<1e-4) return null; let toStart=p5.Vector.sub(this.start,origin); let t=(toStart.x*d.y - toStart.y*d.x)/det; let s=(toStart.x*dir.y - toStart.y*dir.x)/det; if(t>0.001 && t<=maxDist && s>=0 && s<=1){ let pt=p5.Vector.add(origin,p5.Vector.mult(dir,t)); let n=this.normal.copy(); if(n.dot(dir)>0) n.mult(-1); return {point:pt, distance:t, normal:n, element:this}; } return null; } | |
| render(){ push(); translate(this.center.x,this.center.y); rotate(atan2(this.end.y-this.start.y,this.end.x-this.start.x)); | |
| fill(this.isSelected?COLORS.mirrorSelected: this.isRotating?color(150,255,150,120):COLORS.mirrorBody); | |
| stroke(this.isSelected?color(255,255,0):color(150,170,190)); strokeWeight(this.isSelected?2:1); rectMode(CENTER); rect(0,0,this.len,6); | |
| stroke(this.isSelected?color(255,255,0):COLORS.mirrorSurface); strokeWeight(this.isSelected?3:2); line(-this.len/2,0,this.len/2,0); | |
| if(this.isRotating && !this.isSelected){ noFill(); stroke(100,255,100,100); strokeWeight(1); arc(0,0,20,20,0,abs(this.rotSpeed)*100);} pop(); } | |
| } | |
| class Prism { | |
| constructor(center,size=30,angle=0){ this.type='prism'; this.center=center; this.size=size; this.angle=angle; this.isSelected=false; this.dispersion={1:-0.15, 2:0.0, 4:0.15}; this.updateGeometry(); } | |
| updateGeometry(){ this.vertices=[]; this.edges=[]; this.normals=[]; for(let i=0;i<3;i++){ let a=this.angle+TWO_PI*i/3-PI/2; this.vertices.push(p5.Vector.add(this.center,p5.Vector.fromAngle(a,this.size))); } for(let i=0;i<3;i++){ let j=(i+1)%3; this.edges.push({start:this.vertices[i],end:this.vertices[j]}); let d=p5.Vector.sub(this.vertices[j],this.vertices[i]); this.normals.push(createVector(-d.y,d.x).normalize()); } } | |
| update(){} | |
| moveTo(pos){ this.center=pos; this.updateGeometry(); } | |
| rotate(da){ this.angle+=da; this.updateGeometry(); } | |
| contains(p){ const sign=(p,a,b)=> (p.x-b.x)*(a.y-b.y) - (a.x-b.x)*(p.y-b.y); let d1=sign(p,this.vertices[0],this.vertices[1]); let d2=sign(p,this.vertices[1],this.vertices[2]); let d3=sign(p,this.vertices[2],this.vertices[0]); let hasNeg=(d1<0)||(d2<0)||(d3<0); let hasPos=(d1>0)||(d2>0)||(d3>0); return !(hasNeg&&hasPos); } | |
| intersectRay(origin,dir,maxDist){ let closest=null; for(let i=0;i<3;i++){ let e=this.edges[i]; let d=p5.Vector.sub(e.end,e.start); let det=dir.x*d.y - dir.y*d.x; if(abs(det)<1e-4) continue; let toStart=p5.Vector.sub(e.start,origin); let t=(toStart.x*d.y - toStart.y*d.x)/det; let s=(toStart.x*dir.y - toStart.y*dir.x)/det; if(t>0.001 && t<=maxDist && s>=0 && s<=1){ if(!closest || t<closest.distance){ let pt=p5.Vector.add(origin,p5.Vector.mult(dir,t)); let n=this.normals[i].copy(); if(n.dot(dir)>0) n.mult(-1); closest={point:pt,distance:t,normal:n,element:this}; } } } return closest; } | |
| refract(dir, colorMask){ | |
| // Split any multi-component color into separate primary beams (like a dispersive prism) | |
| const components=[1,2,4].filter(bit=> (colorMask & bit)); | |
| return components.map(bit=> ({ | |
| direction: p5.Vector.fromAngle(dir.heading() + (this.dispersion[bit]||0)), | |
| colorMask: bit | |
| })); | |
| } | |
| render(){ fill(this.isSelected?COLORS.mirrorSelected:COLORS.prismBody); stroke(this.isSelected?color(255,255,0):color(140,160,200)); strokeWeight(this.isSelected?2:1); | |
| beginShape(); for(let v of this.vertices) vertex(v.x,v.y); endShape(CLOSE); | |
| if(!this.isSelected){ noStroke(); fill(COLORS.prismRefraction); beginShape(); for(let v of this.vertices){ let vv=p5.Vector.lerp(v,this.center,0.2); vertex(vv.x,vv.y);} endShape(CLOSE);} } | |
| } | |
| class Filter { | |
| constructor(center,type='red',size=20,angle=0){ this.type='filter'; this.center=center; this.filterType=type; this.size=size; this.angle=angle; this.isSelected=false; this.def=FILTER_TYPES[type]; this.updateGeometry(); } | |
| updateGeometry(){ this.vertices=[]; let corners=[ createVector(-this.size,-this.size/3), createVector(this.size,-this.size/3), createVector(this.size,this.size/3), createVector(-this.size,this.size/3) ]; for(let c of corners){ c.rotate(this.angle); this.vertices.push(p5.Vector.add(this.center,c)); } } | |
| update(){} | |
| moveTo(pos){ this.center=pos; this.updateGeometry(); } | |
| rotate(da){ this.angle+=da; this.updateGeometry(); } | |
| contains(p){ let local=p5.Vector.sub(p,this.center); local.rotate(-this.angle); return abs(local.x)<=this.size+5 && abs(local.y)<=this.size/3+5; } | |
| intersectRay(origin,dir,maxDist){ let closest=null; for(let i=0;i<4;i++){ let j=(i+1)%4; let d=p5.Vector.sub(this.vertices[j],this.vertices[i]); let det=dir.x*d.y - dir.y*d.x; if(abs(det)<1e-4) continue; let toStart=p5.Vector.sub(this.vertices[i],origin); let t=(toStart.x*d.y - toStart.y*d.x)/det; let s=(toStart.x*dir.y - toStart.y*dir.x)/det; if(t>0.001 && t<=maxDist && s>=0 && s<=1){ if(!closest || t<closest.distance){ let pt=p5.Vector.add(origin,p5.Vector.mult(dir,t)); closest={point:pt,distance:t,element:this}; } } } return closest; } | |
| filterLight(colorMask){ | |
| const passMask = this.def.passMask; // which components the filter passes | |
| const outMask = colorMask & passMask; // intersection = what survives | |
| if(outMask===0) return [{ colorMask: colorMask, continues:false }]; // fully blocked | |
| return [{ colorMask: outMask, continues:true }]; // possibly reduced (composite->primary) | |
| } | |
| render(){ push(); translate(this.center.x,this.center.y); rotate(this.angle); | |
| const [r,g,b,a] = this.def.col; fill(r,g,b, this.isSelected?150:a); stroke(this.isSelected?255:200); strokeWeight(this.isSelected?2:1); rectMode(CENTER); rect(0,0,this.size*2,this.size*2/3); | |
| fill(255); noStroke(); textAlign(CENTER,CENTER); textSize(14); textStyle(BOLD); text(this.def.label,0,0); pop(); } | |
| } | |
| class QuantumHole { | |
| constructor(center,isBlack=true,linked=null){ this.type='quantumHole'; this.center=center; this.isBlack=isBlack; this.linked=linked; this.radius=20; this.isSelected=false; this.rotAngle=0; this.particleAngle=0; } | |
| setLinked(hole){ this.linked=hole; } | |
| update(){ this.rotAngle += this.isBlack?0.02:-0.02; this.particleAngle+=0.05; } | |
| moveTo(pos){ this.center=pos; } | |
| contains(p){ return p5.Vector.dist(this.center,p) <= this.radius+5; } | |
| intersectRay(origin,dir,maxDist){ let toCenter=p5.Vector.sub(this.center,origin); let projLen=toCenter.dot(dir); if(projLen<0||projLen>maxDist) return null; let projPt=p5.Vector.add(origin,p5.Vector.mult(dir,projLen)); let distToCenter=p5.Vector.dist(projPt,this.center); if(distToCenter>this.radius) return null; let halfChord=sqrt(this.radius*this.radius - distToCenter*distToCenter); let t=projLen - halfChord; if(t<0.001||t>maxDist) return null; return { point:p5.Vector.add(origin,p5.Vector.mult(dir,t)), distance:t, element:this }; } | |
| teleport(dir,colorMask){ if(!this.linked) return null; return { position:p5.Vector.add(this.linked.center,p5.Vector.mult(dir,this.linked.radius+2)), direction:dir, colorMask }; } | |
| render(){ push(); translate(this.center.x,this.center.y); | |
| if(this.isBlack){ for(let i=3;i>0;i--){ fill(50,0,100,this.isSelected?150:100*(4-i)/3); noStroke(); circle(0,0,this.radius*2*(1+i*0.3)); } | |
| push(); rotate(this.rotAngle); noFill(); strokeWeight(2); for(let i=0;i<3;i++){ stroke(150,100,255,100-i*20); arc(0,0,this.radius*3,this.radius*1.5,i*PI/3, i*PI/3+PI/2);} pop(); | |
| fill(COLORS.blackHoleCore); noStroke(); circle(0,0,this.radius*2); | |
| } else { push(); rotate(this.rotAngle); noFill(); strokeWeight(2); for(let i=0;i<4;i++){ stroke(200,220,255,100-i*20); arc(0,0,this.radius*3,this.radius*1.5,i*PI/4, i*PI/4+PI/3);} pop(); for(let i=3;i>0;i--){ fill(255,250,200,this.isSelected?150:100*(4-i)/4); noStroke(); circle(0,0,this.radius*2*(1+i*0.4)); } | |
| fill(COLORS.whiteHoleCore); noStroke(); circle(0,0,this.radius*2); noFill(); stroke(255,255,255,100); strokeWeight(1); for(let i=0;i<6;i++){ let a=this.particleAngle+i*PI/3; line(cos(a)*this.radius, sin(a)*this.radius, cos(a)*(this.radius+10), sin(a)*(this.radius+10)); } | |
| } | |
| if(this.isSelected){ noFill(); stroke(255,255,0); strokeWeight(2); circle(0,0,this.radius*2.5); } pop(); } | |
| } | |
| // -------------------------------------- | |
| // Laser system | |
| // -------------------------------------- | |
| class LaserSystem { | |
| trace(origin, elements){ | |
| let beams=[{ pos:origin.copy(), dir:origin.copy().normalize(), colorMask: COLOR_MASKS.white, segments:[], teleports:0 }]; | |
| let allSegments=[]; | |
| while(beams.length>0){ | |
| let beam=beams.shift(); | |
| for(let bounce=0; bounce<30; bounce++){ | |
| let maxDist=this.getScreenExit(beam.pos,beam.dir); | |
| let closest=null; | |
| for(let el of elements){ let hit=el.intersectRay(beam.pos,beam.dir,maxDist); if(hit && hit.distance>0.1 && (!closest || hit.distance<closest.distance)){ closest=hit; } } | |
| if(closest){ | |
| beam.segments.push({ start:beam.pos.copy(), end:closest.point.copy(), colorMask: beam.colorMask }); | |
| if(closest.element.type==='quantumHole'){ | |
| if(beam.teleports<5 && closest.element.isBlack){ let tp=closest.element.teleport(beam.dir,beam.colorMask); if(tp){ beam.pos=tp.position; beam.dir=tp.direction; beam.teleports++; } else break; } else break; | |
| } else if(closest.element.type==='prism' && beams.length<10){ | |
| const refracted=closest.element.refract(beam.dir, beam.colorMask); | |
| for(let r of refracted){ beams.push({ pos:p5.Vector.add(closest.point,p5.Vector.mult(r.direction,2)), dir:r.direction, colorMask:r.colorMask, segments:[], teleports:beam.teleports }); } | |
| break; | |
| } else if(closest.element.type==='filter'){ | |
| const filtered=closest.element.filterLight(beam.colorMask)[0]; | |
| if(filtered.continues){ beam.pos=p5.Vector.add(closest.point,p5.Vector.mult(beam.dir,1)); beam.colorMask=filtered.colorMask; } | |
| else break; | |
| } else { // mirror | |
| beam.pos=p5.Vector.add(closest.point,p5.Vector.mult(closest.normal,1)); | |
| beam.dir=beam.dir.reflect(closest.normal).normalize(); | |
| } | |
| } else { | |
| let endPt=p5.Vector.add(beam.pos,p5.Vector.mult(beam.dir,maxDist)); | |
| if(maxDist>0.1){ beam.segments.push({ start:beam.pos.copy(), end:endPt, colorMask: beam.colorMask }); } | |
| break; | |
| } | |
| } | |
| allSegments.push(...beam.segments); | |
| } | |
| return allSegments; | |
| } | |
| getScreenExit(pos,dir){ let bounds=225; let t=[]; if(abs(dir.x)>1e-4){ t.push((bounds-pos.x)/dir.x); t.push((-bounds-pos.x)/dir.x);} if(abs(dir.y)>1e-4){ t.push((bounds-pos.y)/dir.y); t.push((-bounds-pos.y)/dir.y);} let valid=t.filter(v=>v>0.01); return valid.length>0? Math.min(...valid):1000; } | |
| render(segments){ blendMode(ADD); for(let seg of segments){ const rgb=MASK_TO_RGB[seg.colorMask]; for(let i=0;i<4;i++){ let w=[12,6,3,1][i]; let a=[40,80,160,200][i]; stroke(rgb[0],rgb[1],rgb[2],a); strokeWeight(w); line(seg.start.x,seg.start.y,seg.end.x,seg.end.y); } } blendMode(BLEND); } | |
| } | |
| // -------------------------------------- | |
| // Octagon source | |
| // -------------------------------------- | |
| class Octagon { | |
| constructor(radius){ this.radius=radius; this.rotation=0; this.vertices=[]; for(let i=0;i<8;i++){ this.vertices.push(p5.Vector.fromAngle(TWO_PI*i/8, radius)); } } | |
| update(){ this.rotation += TWO_PI/(8*120); } | |
| getHotVertex(){ return this.vertices[1].copy().rotate(this.rotation); } | |
| render(){ push(); rotate(this.rotation); | |
| // Plate | |
| fill(COLORS.plate); stroke(70); strokeWeight(1.5); circle(0,0,this.radius*1.7); | |
| // Center bolt | |
| fill(80); stroke(50); strokeWeight(1); circle(0,0,12); stroke(40); line(-5,0,5,0); | |
| // Beams around | |
| for(let i=0;i<8;i++){ | |
| let isHot = i===0 || i===1; let v1=this.vertices[i]; let v2=this.vertices[(i+1)%8]; | |
| push(); translate((v1.x+v2.x)/2,(v1.y+v2.y)/2); rotate(atan2(v2.y-v1.y,v2.x-v1.x)); | |
| fill(isHot?COLORS.beamHot:COLORS.beamStd); stroke(isHot?120:80); strokeWeight(1); rectMode(CENTER); rect(0,0,p5.Vector.dist(v1,v2)+7,14,7); | |
| fill(COLORS.hole); stroke(200); strokeWeight(0.5); for(let j=-1;j<=1;j++){ circle(j*p5.Vector.dist(v1,v2)/3,0,6); } pop(); | |
| } | |
| pop(); } | |
| } | |
| // -------------------------------------- | |
| // Main | |
| // -------------------------------------- | |
| let octagon, laser, elements, selected, dragOffset, isRotating; | |
| function setup(){ | |
| createCanvas(900,900); setupColors(); octagon=new Octagon(40); laser=new LaserSystem(); | |
| // Quantum hole pairs | |
| let bh1=new QuantumHole(createVector(-120,-80),true); let wh1=new QuantumHole(createVector(120,80),false); bh1.setLinked(wh1); wh1.setLinked(bh1); | |
| let bh2=new QuantumHole(createVector(80,-120),true); let wh2=new QuantumHole(createVector(-80,120),false); bh2.setLinked(wh2); wh2.setLinked(bh2); | |
| elements=[ | |
| bh1, wh1, bh2, wh2, | |
| new Prism(createVector(50,-50),25,PI/6), | |
| new Filter(createVector(-60,-140),'red'), | |
| new Filter(createVector(0,-140),'green'), | |
| new Filter(createVector(60,-140),'blue'), | |
| new Filter(createVector(-100,50),'yellow',20,PI/4), | |
| new Filter(createVector(140,50),'cyan',20,-PI/4), | |
| new Filter(createVector(0,180),'magenta',20,PI/2), | |
| new Mirror(createVector(100,-60),createVector(140,-40)), | |
| new Mirror(createVector(-140,-40),createVector(-100,-60)), | |
| new Mirror(createVector(100,20),createVector(120,-20)), | |
| new Mirror(createVector(-120,-20),createVector(-100,20)), | |
| new Mirror(createVector(-20,140),createVector(20,140)), | |
| new Mirror(createVector(180,0),createVector(210,0),0.02), | |
| new Mirror(createVector(-210,0),createVector(-180,0),0.015) | |
| ]; | |
| } | |
| function draw(){ | |
| background(COLORS.bg); | |
| // Grid | |
| stroke(COLORS.gridFine); strokeWeight(0.5); | |
| for(let i=0;i<=width;i+=20){ line(i,0,i,height); line(0,i,width,i); } | |
| stroke(COLORS.gridMajor); strokeWeight(1); | |
| for(let i=0;i<=width;i+=100){ line(i,0,i,height); line(0,i,width,i); } | |
| push(); translate(width/2,height/2); scale(2); | |
| octagon.update(); for(let el of elements) el.update(); for(let el of elements) el.render(); | |
| let segments=laser.trace(octagon.getHotVertex(), elements); laser.render(segments); octagon.render(); | |
| pop(); | |
| // Instructions | |
| fill(255,200); noStroke(); textAlign(LEFT); textSize(12); | |
| text('Drag elements • Right-click prisms/filters to rotate', 10, 20); | |
| text('Black holes teleport to white holes • Filters: RGB (primary) YCM (secondary)', 10, 38); | |
| } | |
| function mousePressed(){ | |
| let world=createVector((mouseX-width/2)/2,(mouseY-height/2)/2); | |
| if(mouseButton===RIGHT){ | |
| for(let i=elements.length-1;i>=0;i--){ if(elements[i].contains && elements[i].contains(world)){ if(elements[i].type==='prism' || elements[i].type==='filter'){ selected=elements[i]; isRotating=true; dragOffset=p5.Vector.sub(world,selected.center); return; } } } | |
| } else { | |
| for(let i=elements.length-1;i>=0;i--){ if(elements[i].contains && elements[i].contains(world)){ selected=elements[i]; selected.isSelected=true; isRotating=false; dragOffset=p5.Vector.sub(world,selected.center); return; } } | |
| if(selected){ selected.isSelected=false; selected=null; } | |
| } | |
| } | |
| function mouseDragged(){ if(!selected) return; let world=createVector((mouseX-width/2)/2,(mouseY-height/2)/2); if(isRotating && (selected.type==='prism' || selected.type==='filter')){ let a1=dragOffset.heading(); let a2=p5.Vector.sub(world,selected.center).heading(); selected.rotate(a2-a1); dragOffset=p5.Vector.sub(world,selected.center); } else if(!isRotating){ selected.moveTo(p5.Vector.sub(world,dragOffset)); } } | |
| function mouseReleased(){ if(selected){ selected.isSelected=false; isRotating=false; } } | |
| document.addEventListener('contextmenu', e=>e.preventDefault()); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment