Spaces:
Running
Running
| const canvas = document.getElementById('gl'); | |
| const gl = canvas.getContext('webgl'); | |
| if (!gl) { alert('此瀏覽器不支援 WebGL'); } | |
| function resize() { | |
| const dpr = Math.min(window.devicePixelRatio || 1, 2); | |
| const w = Math.floor(window.innerWidth * dpr); | |
| const h = Math.floor((window.innerHeight - document.querySelector('.ui').offsetHeight) * dpr); | |
| canvas.width = w; canvas.height = Math.max(h, 2); | |
| canvas.style.width = '100%'; canvas.style.height = '100%'; | |
| gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); | |
| } | |
| window.addEventListener('resize', resize); | |
| const vertSrc = ` | |
| attribute vec2 aPos; | |
| attribute vec2 aUV; | |
| varying vec2 vUV; | |
| void main(){ | |
| vUV = aUV; | |
| gl_Position = vec4(aPos, 0.0, 1.0); | |
| }`; | |
| // fragment shader(點質量透鏡公式 β = θ - θ_E² θ / |θ|²) | |
| const fragSrc = ` | |
| precision highp float; | |
| varying vec2 vUV; | |
| uniform sampler2D uTex; | |
| uniform vec2 uPosition; | |
| uniform float uThetaE2; | |
| uniform float uRatio; | |
| uniform float uShadowScale; | |
| void main(){ | |
| float eps = 1e-6; | |
| vec2 d = vUV - uPosition; | |
| vec2 d_n = d / vec2(uRatio, 1.0); | |
| float r2 = dot(d_n, d_n) + eps; | |
| vec2 beta_n = d_n - (uThetaE2 * d_n / r2); | |
| vec2 sampleUV = beta_n * vec2(uRatio, 1.0) + uPosition; | |
| vec4 res = texture2D(uTex, clamp(sampleUV, 0.0, 1.0)); | |
| if (length(d_n) < uShadowScale * sqrt(uThetaE2)) { | |
| res.rgb = vec3(0.0); | |
| } | |
| gl_FragColor = res; | |
| }`; | |
| function compile(type, src){ | |
| const sh = gl.createShader(type); | |
| gl.shaderSource(sh, src); | |
| gl.compileShader(sh); | |
| if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) { | |
| throw new Error(gl.getShaderInfoLog(sh) || 'shader compile error'); | |
| } | |
| return sh; | |
| } | |
| const vs = compile(gl.VERTEX_SHADER, vertSrc); | |
| const fs = compile(gl.FRAGMENT_SHADER, fragSrc); | |
| const prog = gl.createProgram(); | |
| gl.attachShader(prog, vs); | |
| gl.attachShader(prog, fs); | |
| gl.linkProgram(prog); | |
| if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) { | |
| throw new Error(gl.getProgramInfoLog(prog) || 'program link error'); | |
| } | |
| gl.useProgram(prog); | |
| const quad = new Float32Array([ | |
| -1, -1, 0, 0, | |
| 1, -1, 1, 0, | |
| -1, 1, 0, 1, | |
| 1, 1, 1, 1, | |
| ]); | |
| const ibo = new Uint16Array([0,1,2,2,1,3]); | |
| const vbo = gl.createBuffer(); | |
| const ebo = gl.createBuffer(); | |
| gl.bindBuffer(gl.ARRAY_BUFFER, vbo); | |
| gl.bufferData(gl.ARRAY_BUFFER, quad, gl.STATIC_DRAW); | |
| gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ebo); | |
| gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, ibo, gl.STATIC_DRAW); | |
| const aPos = gl.getAttribLocation(prog, 'aPos'); | |
| const aUV = gl.getAttribLocation(prog, 'aUV'); | |
| gl.enableVertexAttribArray(aPos); | |
| gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 16, 0); | |
| gl.enableVertexAttribArray(aUV); | |
| gl.vertexAttribPointer(aUV, 2, gl.FLOAT, false, 16, 8); | |
| const uTex = gl.getUniformLocation(prog, 'uTex'); | |
| const uPosition = gl.getUniformLocation(prog, 'uPosition'); | |
| const uThetaE2 = gl.getUniformLocation(prog, 'uThetaE2'); | |
| const uRatio = gl.getUniformLocation(prog, 'uRatio'); | |
| const uShadowScale = gl.getUniformLocation(prog, 'uShadowScale'); | |
| gl.uniform1i(uTex, 0); | |
| let state = { | |
| position: { x: 0.5, y: 0.5 }, | |
| thetaE2: parseFloat(document.getElementById('thetaE').value), | |
| shadowScale: parseFloat(document.getElementById('shadowScale').value) | |
| }; | |
| function updateUniforms(){ | |
| const ratio = gl.drawingBufferWidth / gl.drawingBufferHeight; | |
| gl.uniform2f(uPosition, state.position.x, state.position.y); | |
| gl.uniform1f(uThetaE2, state.thetaE2); | |
| gl.uniform1f(uRatio, ratio); | |
| gl.uniform1f(uShadowScale, state.shadowScale); | |
| } | |
| let texture = gl.createTexture(); | |
| function useTextureFromImage(img){ | |
| gl.bindTexture(gl.TEXTURE_2D, texture); | |
| gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1); | |
| gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); | |
| } | |
| function genStarfield(w=1024, h=512){ | |
| const cvs = document.createElement('canvas'); | |
| cvs.width = w; cvs.height = h; const ctx = cvs.getContext('2d'); | |
| ctx.fillStyle = '#000'; ctx.fillRect(0,0,w,h); | |
| for(let i=0;i<5000;i++){ | |
| const x=Math.random()*w, y=Math.random()*h; const s=Math.random()*1.6+0.2; | |
| const a=Math.random()*0.9+0.1; ctx.fillStyle = `rgba(255,255,255,${a})`; | |
| ctx.fillRect(x, y, s, s); | |
| } | |
| return cvs; | |
| } | |
| useTextureFromImage(genStarfield()); | |
| const imgInput = document.getElementById('img'); | |
| imgInput.addEventListener('change', e => { | |
| const file = e.target.files[0]; if (!file) return; | |
| const img = new Image(); | |
| img.onload = () => useTextureFromImage(img); | |
| img.src = URL.createObjectURL(file); | |
| }); | |
| let dragging = false; | |
| let animate = true; | |
| const chkAnimate = document.getElementById('animate'); | |
| chkAnimate.addEventListener('change', () => animate = chkAnimate.checked); | |
| canvas.addEventListener('mousedown', (e)=>{ dragging = true; move(e); }); | |
| window.addEventListener('mouseup', ()=> dragging = false); | |
| window.addEventListener('mousemove', (e)=>{ if (dragging) move(e); }); | |
| function move(e){ | |
| const rect = canvas.getBoundingClientRect(); | |
| const x = (e.clientX - rect.left) / rect.width; | |
| const y = (e.clientY - rect.top) / rect.height; | |
| state.position.x = x; state.position.y = 1.0 - y; | |
| } | |
| const thetaEEl = document.getElementById('thetaE'); | |
| const shadowEl = document.getElementById('shadowScale'); | |
| thetaEEl.addEventListener('input', ()=> state.thetaE2 = parseFloat(thetaEEl.value)); | |
| shadowEl.addEventListener('input', ()=> state.shadowScale = parseFloat(shadowEl.value)); | |
| document.getElementById('reset').addEventListener('click', ()=>{ | |
| state = { position: {x:0.5, y:0.5}, thetaE2: 0.01, shadowScale: 2.598 }; | |
| thetaEEl.value = state.thetaE2; shadowEl.value = state.shadowScale; chkAnimate.checked = false; animate = false; | |
| }); | |
| resize(); | |
| let t0 = performance.now(); | |
| function render(now){ | |
| if (animate){ | |
| const t = (now - t0) * 0.001; | |
| const r = 0.25; const cx = 0.5, cy = 0.55; | |
| state.position.x = cx + r * Math.cos(t*0.3); | |
| state.position.y = cy + r * Math.sin(t*0.3); | |
| state.thetaE2 = 0.01 + 0.005*Math.sin(t*0.7); | |
| } | |
| updateUniforms(); | |
| gl.clearColor(0,0,0,1); gl.clear(gl.COLOR_BUFFER_BIT); | |
| gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0); | |
| requestAnimationFrame(render); | |
| } | |
| requestAnimationFrame(render); |