{
    "id": "bhaskar/v-prism",
    "kind": "component",
    "title": "V-Prism",
    "summary": "Glass prism that splits a beam aimed by your pointer into a glowing spectrum, with Snell-law refraction and a lens flare.",
    "author": "Admin",
    "stars": 0,
    "url": "https://ownlyplans.com/bhaskar/v-prism/",
    "stack": [
        "CSS",
        "HTML",
        "JavaScript"
    ],
    "tools": [
        "claude-code",
        "cursor",
        "lovable"
    ],
    "difficulty": "beginner",
    "license": "mit",
    "version": "1.0",
    "overview": "Glass prism that splits a beam aimed by your pointer into a glowing spectrum, with Snell-law refraction and a lens flare. Ported to plain HTML, CSS and JavaScript from ObsidianUI (github.com/Atharvsinh-codez/ObsidianUI), which is released under the MIT License. The original copyright notice is kept in the code.",
    "steps": [],
    "files": [],
    "install": "",
    "how_to_use": "Use the component code (html, css, js, or react) directly, or run the install command.",
    "component": {
        "category": "Backgrounds",
        "html": "<!--\n  Ported to plain HTML/CSS/JS for OwnlyPlans from ObsidianUI (https://github.com/Atharvsinh-codez/ObsidianUI)\n  MIT License — Copyright (c) 2026 ObsidianUI\n  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated\n  documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation\n  the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to\n  permit persons to whom the Software is furnished to do so, subject to the following conditions:\n  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED.\n-->\n<div class=\"op-v-prism\" role=\"img\" aria-label=\"A glass prism splitting a beam of light into a spectrum. Move or drag to aim the beam.\">\n  <canvas class=\"op-vp-canvas\"></canvas>\n  <div class=\"op-vp-hint\">move to aim the light</div>\n</div>",
        "css": "/* Ported from ObsidianUI (MIT License, Copyright (c) 2026 ObsidianUI) */\n.op-v-prism{position:relative;width:100%;height:520px;overflow:hidden;background:#000;touch-action:none;cursor:crosshair}\n.op-vp-canvas{position:absolute;inset:0;display:block;width:100%;height:100%}\n.op-vp-hint{pointer-events:none;position:absolute;bottom:24px;left:50%;transform:translateX(-50%);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.6rem;text-transform:uppercase;letter-spacing:.2em;color:rgba(255,255,255,.22)}",
        "js": "(function(){\n  // Same settings as the original dark preset; rendering is an original 2D take on the effect.\n  var IOR=1.5,GLOW=2.5,BLOOM=.5;\n  function snell(theta,ior){var s=Math.sin(theta)/ior;s=Math.max(-1,Math.min(1,s));return theta-Math.asin(s)}\n  document.querySelectorAll('.op-v-prism').forEach(function(root){\n    var cv=root.querySelector('canvas'),ctx=cv.getContext('2d'),W=0,H=0,dpr=1,reduce=matchMedia('(prefers-reduced-motion: reduce)').matches;\n    var aim={x:-1,y:.35},cur={x:-1,y:.35},t0=performance.now(),idle=true;\n    function resize(){dpr=Math.min(devicePixelRatio||1,2);W=root.clientWidth;H=root.clientHeight;cv.width=W*dpr;cv.height=H*dpr;ctx.setTransform(dpr,0,0,dpr,0,0)}\n    resize();new ResizeObserver(resize).observe(root);\n    function prism(){var cx=W/2,cy=H/2+(W<=640?H*.08:0),r=Math.min(W,H)*.2;return[0,1,2].map(function(i){var a=-Math.PI/2+i*2*Math.PI/3;return{x:cx+Math.cos(a)*r,y:cy+Math.sin(a)*r}}).concat([{x:cx,y:cy,r:r}])}\n    function hit(o,d,p){var best=null;for(var i=0;i<3;i++){var a=p[i],b=p[(i+1)%3],ex=b.x-a.x,ey=b.y-a.y,den=d.x*ey-d.y*ex;if(Math.abs(den)<1e-6)continue;\n      var t=((a.x-o.x)*ey-(a.y-o.y)*ex)/den,u=((a.x-o.x)*d.y-(a.y-o.y)*d.x)/den;if(t>0&&u>=0&&u<=1&&(!best||t<best.t)){var nx=ey,ny=-ex,l=Math.hypot(nx,ny);best={t:t,x:o.x+d.x*t,y:o.y+d.y*t,nx:nx/l,ny:ny/l}}}return best}\n    function glow(x,y,r,a){var g=ctx.createRadialGradient(x,y,0,x,y,r);g.addColorStop(0,'rgba(255,255,255,'+a+')');g.addColorStop(.25,'rgba(255,248,235,'+a*.35+')');g.addColorStop(1,'rgba(255,255,255,0)');ctx.fillStyle=g;ctx.beginPath();ctx.arc(x,y,r,0,Math.PI*2);ctx.fill()}\n    function frame(now){\n      var t=(now-t0)/1000;\n      if(idle&&!reduce){aim.x=-Math.cos(t*.25)*1;aim.y=.35+Math.sin(t*.4)*.25}\n      var k=reduce?1:.12;cur.x+=(aim.x-cur.x)*k;cur.y+=(aim.y-cur.y)*k;\n      ctx.globalCompositeOperation='source-over';ctx.fillStyle='#000';ctx.fillRect(0,0,W,H);\n      var p=prism(),c=p[3],dl=Math.hypot(cur.x,cur.y)||1,d={x:-cur.x/dl,y:-cur.y/dl},o={x:c.x-d.x*Math.max(W,H),y:c.y-d.y*Math.max(W,H)};\n      var h=hit(o,d,p);ctx.globalCompositeOperation='lighter';\n      // Incident beam\n      var end=h||{x:c.x+d.x*Math.max(W,H),y:c.y+d.y*Math.max(W,H)};\n      ctx.lineCap='round';[[10,.04],[4,.12],[1.6,.9]].forEach(function(l){ctx.strokeStyle='rgba(235,235,235,'+l[1]+')';ctx.lineWidth=l[0];ctx.beginPath();ctx.moveTo(o.x,o.y);ctx.lineTo(end.x,end.y);ctx.stroke()});\n      if(h){\n        // Refraction: incidence angle against the inward normal, Snell deviation exaggerated x6 like the original.\n        var nx=h.nx,ny=h.ny;if(nx*d.x+ny*d.y>0){nx=-nx;ny=-ny}\n        var din=Math.atan2(d.y,d.x),nin=Math.atan2(-ny,-nx),th=Math.atan2(Math.sin(din-nin),Math.cos(din-nin));\n        var dev=Math.max(-1.1,Math.min(1.1,snell(th,IOR)*6)),j=din-dev,exit={x:c.x,y:c.y},len=Math.hypot(W,H),spread=.34;\n        for(var i=0;i<60;i++){var f=i/59,ang=j+(f-.5)*spread,hue=f*280,a=(.035+.02*Math.sin(t*2+f*6))*GLOW*.5;\n          var g=ctx.createLinearGradient(exit.x,exit.y,exit.x+Math.cos(ang)*len*.7,exit.y+Math.sin(ang)*len*.7);g.addColorStop(0,'hsla('+hue+',100%,60%,'+a*1.6+')');g.addColorStop(.6,'hsla('+hue+',100%,55%,'+a+')');g.addColorStop(1,'hsla('+hue+',100%,50%,0)');\n          ctx.fillStyle=g;ctx.beginPath();ctx.moveTo(exit.x,exit.y);ctx.lineTo(exit.x+Math.cos(ang-.006)*len,exit.y+Math.sin(ang-.006)*len);ctx.lineTo(exit.x+Math.cos(ang+.006)*len,exit.y+Math.sin(ang+.006)*len);ctx.closePath();ctx.fill()}\n        ctx.strokeStyle='rgba(255,255,255,.35)';ctx.lineWidth=2;ctx.beginPath();ctx.moveTo(h.x,h.y);ctx.lineTo(exit.x,exit.y);ctx.stroke();\n        glow(h.x,h.y,90*(1+BLOOM),.55);glow(h.x,h.y,22,.9);\n        ctx.save();ctx.translate(h.x,h.y);ctx.rotate(din+Math.PI/2);var sg=ctx.createLinearGradient(-160,0,160,0);sg.addColorStop(0,'rgba(255,255,255,0)');sg.addColorStop(.5,'rgba(255,255,255,.12)');sg.addColorStop(1,'rgba(255,255,255,0)');ctx.fillStyle=sg;ctx.fillRect(-160,-1.5,320,3);ctx.restore();\n      }\n      // Glass crystal: solid tinted body, rounded corners, inner facet lines and a specular rim, standing in for the original's transmissive 3D prism.\n      ctx.globalCompositeOperation='source-over';\n      function tri(pts,rad){ctx.beginPath();for(var i=0;i<3;i++){var a=pts[i],b=pts[(i+1)%3],q=pts[(i+2)%3];var m1={x:a.x+(q.x-a.x)*.5,y:a.y+(q.y-a.y)*.5};if(i===0)ctx.moveTo((a.x+b.x)/2,(a.y+b.y)/2);ctx.arcTo(b.x,b.y,(b.x+q.x)/2,(b.y+q.y)/2,rad)}ctx.closePath()}\n      var rr=c.r*.12;tri(p,rr);\n      var fg=ctx.createLinearGradient(p[2].x,p[0].y,p[1].x,p[1].y);fg.addColorStop(0,'rgba(190,205,230,.20)');fg.addColorStop(.45,'rgba(120,130,150,.10)');fg.addColorStop(1,'rgba(200,210,235,.18)');ctx.fillStyle=fg;ctx.fill();\n      ctx.save();tri(p,rr);ctx.clip();\n      var cx=c.x,cy=c.y;ctx.strokeStyle='rgba(255,255,255,.10)';ctx.lineWidth=1;for(var i=0;i<3;i++){ctx.beginPath();ctx.moveTo(cx,cy);ctx.lineTo(p[i].x,p[i].y);ctx.stroke()}\n      var sh=ctx.createLinearGradient(p[0].x,p[0].y,cx,cy+c.r);sh.addColorStop(0,'rgba(255,255,255,.22)');sh.addColorStop(.4,'rgba(255,255,255,0)');ctx.fillStyle=sh;ctx.fillRect(cx-c.r,p[0].y,c.r*2,c.r*2);\n      ctx.restore();\n      tri(p,rr);ctx.strokeStyle='rgba(255,255,255,.28)';ctx.lineWidth=1;ctx.stroke();\n      ctx.globalCompositeOperation='lighter';tri(p,rr);ctx.strokeStyle='rgba(210,225,255,.07)';ctx.lineWidth=8;ctx.stroke();\n      requestAnimationFrame(frame)}\n    function aimAt(e){var r=root.getBoundingClientRect(),p=prism(),c=p[3];var x=e.clientX-r.left-c.x,y=e.clientY-r.top-c.y;if(Math.hypot(x,y)<4)return;aim.x=x;aim.y=y;idle=false}\n    root.addEventListener('pointermove',aimAt);root.addEventListener('pointerdown',function(e){root.setPointerCapture(e.pointerId);aimAt(e)});\n    root.addEventListener('pointerleave',function(){idle=true;t0=performance.now()-Math.acos(Math.max(-1,Math.min(1,-cur.x/(Math.hypot(cur.x,cur.y)||1))))/.25*1000});\n    requestAnimationFrame(frame);\n  });\n})();",
        "react": "",
        "dependencies": [],
        "install": "",
        "licence_notice": ""
    }
}