(() => { const year = document.querySelector("#year"); if (year) year.textContent = String(new Date().getFullYear()); const navigation = document.querySelector("#site-navigation"); function setMetaContent(selector, content) { const element = document.querySelector(selector); if (element && content) element.setAttribute("content", content); } function applySiteMetadata(config) { const site = config && config.site; if (!site || typeof site.name !== "string" || typeof site.url !== "string") return; const homepageTitle = document.querySelector('meta[property="og:title"]'); if (!homepageTitle) return; const name = site.name.trim(); const description = typeof site.description === "string" ? site.description.trim() : ""; let siteUrl; try { siteUrl = new URL(site.url.trim()); if (siteUrl.protocol !== "https:" && siteUrl.protocol !== "http:") return; } catch (error) { return; } if (!name || !siteUrl) return; siteUrl.pathname = siteUrl.pathname.replace(/\/+$/, "") + "/"; siteUrl.search = ""; siteUrl.hash = ""; const canonicalUrl = siteUrl.href; const imageUrl = new URL("assets/images/avatar.jpg", canonicalUrl).href; document.title = name; setMetaContent('meta[name="description"]', description); setMetaContent('meta[property="og:site_name"]', name); homepageTitle.setAttribute("content", name); setMetaContent('meta[property="og:description"]', description); setMetaContent('meta[property="og:url"]', canonicalUrl); setMetaContent('meta[property="og:image"]', imageUrl); const canonical = document.querySelector('link[rel="canonical"]'); if (canonical) canonical.setAttribute("href", canonicalUrl); } function renderConfiguredLinks(config) { if (!navigation || !config || !Array.isArray(config.links)) return; const fragment = document.createDocumentFragment(); config.links.forEach((item) => { if (!item || typeof item.name !== "string" || typeof item.url !== "string") return; const name = item.name.trim(); const url = item.url.trim(); if (!name || !/^https?:\/\//i.test(url)) return; const link = document.createElement("a"); link.className = "destination"; link.href = url; link.target = "_blank"; link.rel = "noopener noreferrer"; link.title = "在新标签页打开"; link.setAttribute("aria-label", `${name}(新标签页打开)`); const label = document.createElement("span"); label.className = "destination-label"; label.textContent = name; const icon = document.createElement("span"); icon.className = "external-icon"; icon.setAttribute("aria-hidden", "true"); icon.textContent = "↗"; link.append(label, icon); fragment.append(link); }); navigation.replaceChildren(fragment); } if (navigation && window.XMLHttpRequest) { try { const request = new XMLHttpRequest(); request.open("GET", "assets/js/site-config.json", true); request.responseType = "json"; request.timeout = 3000; request.onload = function() { if (request.status >= 200 && request.status < 300) { applySiteMetadata(request.response); renderConfiguredLinks(request.response); } }; request.send(); } catch (error) { // Keep the links embedded in the HTML when local-file or network access fails. } } const shaderSources = { "sakura_point_vsh": "uniform mat4 uProjection;\nuniform mat4 uModelview;\nuniform vec3 uResolution;\nuniform vec3 uOffset;\nuniform vec3 uDOF; //x:focus distance, y:focus radius, z:max radius\nuniform vec3 uFade; //x:start distance, y:half distance, z:near fade start\n\n\nattribute vec3 aPosition;\nattribute vec3 aEuler;\nattribute vec2 aMisc; //x:size, y:fade\n\n\nvarying vec3 pposition;\nvarying float psize;\nvarying float palpha;\nvarying float pdist;\n\n\n//varying mat3 rotMat;\nvarying vec3 normX;\nvarying vec3 normY;\nvarying vec3 normZ;\nvarying vec3 normal;\n\n\nvarying float diffuse;\nvarying float specular;\nvarying float rstop;\nvarying float distancefade;\n\n\nvoid main(void) {\n // Projection is based on vertical angle\n vec4 pos = uModelview * vec4(aPosition + uOffset, 1.0);\n gl_Position = uProjection * pos;\n gl_PointSize = aMisc.x * uProjection[1][1] / -pos.z * uResolution.y * 0.5;\n \n pposition = pos.xyz;\n psize = aMisc.x;\n pdist = length(pos.xyz);\n palpha = smoothstep(0.0, 1.0, (pdist - 0.1) / uFade.z);\n \n vec3 elrsn = sin(aEuler);\n vec3 elrcs = cos(aEuler);\n mat3 rotx = mat3(\n 1.0, 0.0, 0.0,\n 0.0, elrcs.x, elrsn.x,\n 0.0, -elrsn.x, elrcs.x\n );\n mat3 roty = mat3(\n elrcs.y, 0.0, -elrsn.y,\n 0.0, 1.0, 0.0,\n elrsn.y, 0.0, elrcs.y\n );\n mat3 rotz = mat3(\n elrcs.z, elrsn.z, 0.0, \n -elrsn.z, elrcs.z, 0.0,\n 0.0, 0.0, 1.0\n );\n mat3 rotmat = rotx * roty * rotz;\n normal = rotmat[2];\n \n mat3 trrotm = mat3(\n rotmat[0][0], rotmat[1][0], rotmat[2][0],\n rotmat[0][1], rotmat[1][1], rotmat[2][1],\n rotmat[0][2], rotmat[1][2], rotmat[2][2]\n );\n normX = trrotm[0];\n normY = trrotm[1];\n normZ = trrotm[2];\n \n const vec3 lit = vec3(0.6917144638660746, 0.6917144638660746, -0.20751433915982237);\n \n float tmpdfs = dot(lit, normal);\n if(tmpdfs < 0.0) {\n normal = -normal;\n tmpdfs = dot(lit, normal);\n }\n diffuse = 0.4 + tmpdfs;\n \n vec3 eyev = normalize(-pos.xyz);\n if(dot(eyev, normal) > 0.0) {\n vec3 hv = normalize(eyev + lit);\n specular = pow(max(dot(hv, normal), 0.0), 20.0);\n }\n else {\n specular = 0.0;\n }\n \n rstop = clamp((abs(pdist - uDOF.x) - uDOF.y) / uDOF.z, 0.0, 1.0);\n rstop = pow(rstop, 0.5);\n //-0.69315 = ln(0.5)\n distancefade = min(1.0, exp((uFade.x - pdist) * 0.69315 / uFade.y));\n}", "sakura_point_fsh": "#ifdef GL_ES\n//precision mediump float;\nprecision highp float;\n#endif\n\n\nuniform vec3 uDOF; //x:focus distance, y:focus radius, z:max radius\nuniform vec3 uFade; //x:start distance, y:half distance, z:near fade start\n\n\nconst vec3 fadeCol = vec3(0.08, 0.03, 0.06);\n\n\nvarying vec3 pposition;\nvarying float psize;\nvarying float palpha;\nvarying float pdist;\n\n\n//varying mat3 rotMat;\nvarying vec3 normX;\nvarying vec3 normY;\nvarying vec3 normZ;\nvarying vec3 normal;\n\n\nvarying float diffuse;\nvarying float specular;\nvarying float rstop;\nvarying float distancefade;\n\n\nfloat ellipse(vec2 p, vec2 o, vec2 r) {\n vec2 lp = (p - o) / r;\n return length(lp) - 1.0;\n}\n\n\nvoid main(void) {\n vec3 p = vec3(gl_PointCoord - vec2(0.5, 0.5), 0.0) * 2.0;\n vec3 d = vec3(0.0, 0.0, -1.0);\n float nd = normZ.z; //dot(-normZ, d);\n if(abs(nd) < 0.0001) discard;\n \n float np = dot(normZ, p);\n vec3 tp = p + d * np / nd;\n vec2 coord = vec2(dot(normX, tp), dot(normY, tp));\n \n //angle = 15 degree\n const float flwrsn = 0.258819045102521;\n const float flwrcs = 0.965925826289068;\n mat2 flwrm = mat2(flwrcs, -flwrsn, flwrsn, flwrcs);\n vec2 flwrp = vec2(abs(coord.x), coord.y) * flwrm;\n \n float r;\n if(flwrp.x < 0.0) {\n r = ellipse(flwrp, vec2(0.065, 0.024) * 0.5, vec2(0.36, 0.96) * 0.5);\n }\n else {\n r = ellipse(flwrp, vec2(0.065, 0.024) * 0.5, vec2(0.58, 0.96) * 0.5);\n }\n \n if(r > rstop) discard;\n \n vec3 col = mix(vec3(1.0, 0.8, 0.75), vec3(1.0, 0.9, 0.87), r);\n float grady = mix(0.0, 1.0, pow(coord.y * 0.5 + 0.5, 0.35));\n col *= vec3(1.0, grady, grady);\n col *= mix(0.8, 1.0, pow(abs(coord.x), 0.3));\n col = col * diffuse + specular;\n \n col = mix(fadeCol, col, distancefade);\n \n float alpha = (rstop > 0.001)? (0.5 - r / (rstop * 2.0)) : 1.0;\n alpha = smoothstep(0.0, 1.0, alpha) * palpha;\n \n gl_FragColor = vec4(col * 0.5, alpha);\n}", "fx_common_vsh": "uniform vec3 uResolution;\nattribute vec2 aPosition;\n\n\nvarying vec2 texCoord;\nvarying vec2 screenCoord;\n\n\nvoid main(void) {\n gl_Position = vec4(aPosition, 0.0, 1.0);\n texCoord = aPosition.xy * 0.5 + vec2(0.5, 0.5);\n screenCoord = aPosition.xy * vec2(uResolution.z, 1.0);\n}", "bg_fsh": "#ifdef GL_ES\n//precision mediump float;\nprecision highp float;\n#endif\n\n\nuniform vec2 uTimes;\n\n\nvarying vec2 texCoord;\nvarying vec2 screenCoord;\n\n\nvoid main(void) {\n vec3 col;\n float c;\n vec2 tmpv = texCoord * vec2(0.8, 1.0) - vec2(0.95, 1.0);\n c = exp(-pow(length(tmpv) * 1.8, 2.0));\n col = mix(vec3(0.02, 0.0, 0.03), vec3(0.96, 0.98, 1.0) * 1.5, c);\n gl_FragColor = vec4(col * 0.5, 1.0);\n}", "fx_brightbuf_fsh": "#ifdef GL_ES\n//precision mediump float;\nprecision highp float;\n#endif\nuniform sampler2D uSrc;\nuniform vec2 uDelta;\n\n\nvarying vec2 texCoord;\nvarying vec2 screenCoord;\n\n\nvoid main(void) {\n vec4 col = texture2D(uSrc, texCoord);\n gl_FragColor = vec4(col.rgb * 2.0 - vec3(0.5), 1.0);\n}", "fx_dirblur_r4_fsh": "#ifdef GL_ES\n//precision mediump float;\nprecision highp float;\n#endif\nuniform sampler2D uSrc;\nuniform vec2 uDelta;\nuniform vec4 uBlurDir; //dir(x, y), stride(z, w)\n\n\nvarying vec2 texCoord;\nvarying vec2 screenCoord;\n\n\nvoid main(void) {\n vec4 col = texture2D(uSrc, texCoord);\n col = col + texture2D(uSrc, texCoord + uBlurDir.xy * uDelta);\n col = col + texture2D(uSrc, texCoord - uBlurDir.xy * uDelta);\n col = col + texture2D(uSrc, texCoord + (uBlurDir.xy + uBlurDir.zw) * uDelta);\n col = col + texture2D(uSrc, texCoord - (uBlurDir.xy + uBlurDir.zw) * uDelta);\n gl_FragColor = col / 5.0;\n}", "fx_common_fsh": "#ifdef GL_ES\n//precision mediump float;\nprecision highp float;\n#endif\nuniform sampler2D uSrc;\nuniform vec2 uDelta;\n\n\nvarying vec2 texCoord;\nvarying vec2 screenCoord;\n\n\nvoid main(void) {\n gl_FragColor = texture2D(uSrc, texCoord);\n}", "pp_final_vsh": "uniform vec3 uResolution;\nattribute vec2 aPosition;\nvarying vec2 texCoord;\nvarying vec2 screenCoord;\nvoid main(void) {\n gl_Position = vec4(aPosition, 0.0, 1.0);\n texCoord = aPosition.xy * 0.5 + vec2(0.5, 0.5);\n screenCoord = aPosition.xy * vec2(uResolution.z, 1.0);\n}", "pp_final_fsh": "#ifdef GL_ES\n//precision mediump float;\nprecision highp float;\n#endif\nuniform sampler2D uSrc;\nuniform sampler2D uBloom;\nuniform vec2 uDelta;\nvarying vec2 texCoord;\nvarying vec2 screenCoord;\nvoid main(void) {\n vec4 srccol = texture2D(uSrc, texCoord) * 2.0;\n vec4 bloomcol = texture2D(uBloom, texCoord);\n vec4 col;\n col = srccol + bloomcol * (vec4(1.0) + srccol);\n col *= smoothstep(1.0, 0.0, pow(length((texCoord - vec2(0.5)) * 2.0), 1.2) * 0.5);\n col = pow(col, vec4(0.45454545454545)); //(1.0 / 2.2)\n \n gl_FragColor = vec4(col.rgb, 1.0);\n gl_FragColor.a = 1.0;\n}", }; // WebGL ES leaves smoothstep with reversed edges undefined. Keep the // original vignette curve while expressing it with a defined operation. shaderSources["pp_final_fsh"] = shaderSources["pp_final_fsh"].replace( "smoothstep(1.0, 0.0, pow(length((texCoord - vec2(0.5)) * 2.0), 1.2) * 0.5)", "1.0 - smoothstep(0.0, 1.0, pow(length((texCoord - vec2(0.5)) * 2.0), 1.2) * 0.5)" ); // Utilities var Vector3 = {}; var Matrix44 = {}; Vector3.create = function(x, y, z) { return {'x':x, 'y':y, 'z':z}; }; Vector3.dot = function (v0, v1) { return v0.x * v1.x + v0.y * v1.y + v0.z * v1.z; }; Vector3.cross = function (v, v0, v1) { v.x = v0.y * v1.z - v0.z * v1.y; v.y = v0.z * v1.x - v0.x * v1.z; v.z = v0.x * v1.y - v0.y * v1.x; }; Vector3.normalize = function (v) { var l = v.x * v.x + v.y * v.y + v.z * v.z; if(l > 0.00001) { l = 1.0 / Math.sqrt(l); v.x *= l; v.y *= l; v.z *= l; } }; Vector3.arrayForm = function(v) { if(v.array) { v.array[0] = v.x; v.array[1] = v.y; v.array[2] = v.z; } else { v.array = new Float32Array([v.x, v.y, v.z]); } return v.array; }; Matrix44.createIdentity = function () { return new Float32Array([1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]); }; Matrix44.loadProjection = function (m, aspect, vdeg, near, far) { var h = near * Math.tan(vdeg * Math.PI / 180.0 * 0.5) * 2.0; var w = h * aspect; m[0] = 2.0 * near / w; m[1] = 0.0; m[2] = 0.0; m[3] = 0.0; m[4] = 0.0; m[5] = 2.0 * near / h; m[6] = 0.0; m[7] = 0.0; m[8] = 0.0; m[9] = 0.0; m[10] = -(far + near) / (far - near); m[11] = -1.0; m[12] = 0.0; m[13] = 0.0; m[14] = -2.0 * far * near / (far - near); m[15] = 0.0; }; Matrix44.loadLookAt = function (m, vpos, vlook, vup) { var frontv = Vector3.create(vpos.x - vlook.x, vpos.y - vlook.y, vpos.z - vlook.z); Vector3.normalize(frontv); var sidev = Vector3.create(1.0, 0.0, 0.0); Vector3.cross(sidev, vup, frontv); Vector3.normalize(sidev); var topv = Vector3.create(1.0, 0.0, 0.0); Vector3.cross(topv, frontv, sidev); Vector3.normalize(topv); m[0] = sidev.x; m[1] = topv.x; m[2] = frontv.x; m[3] = 0.0; m[4] = sidev.y; m[5] = topv.y; m[6] = frontv.y; m[7] = 0.0; m[8] = sidev.z; m[9] = topv.z; m[10] = frontv.z; m[11] = 0.0; m[12] = -(vpos.x * m[0] + vpos.y * m[4] + vpos.z * m[8]); m[13] = -(vpos.x * m[1] + vpos.y * m[5] + vpos.z * m[9]); m[14] = -(vpos.x * m[2] + vpos.y * m[6] + vpos.z * m[10]); m[15] = 1.0; }; // var timeInfo = { 'start':0, 'prev':0, // performance.now() milliseconds 'delta':0, 'elapsed':0 // Number(sec) }; function currentTime() { return (window.performance && window.performance.now) ? window.performance.now() : Date.now(); } // var gl; var renderSpec = { 'width':0, 'height':0, 'aspect':1, 'array':new Float32Array(3), 'halfWidth':0, 'halfHeight':0, 'halfArray':new Float32Array(3), 'pixelRatio':1 // and some render targets. see setViewport() }; renderSpec.setSize = function(w, h) { renderSpec.width = w; renderSpec.height = h; renderSpec.aspect = renderSpec.width / Math.max(1, renderSpec.height); renderSpec.array[0] = renderSpec.width; renderSpec.array[1] = renderSpec.height; renderSpec.array[2] = renderSpec.aspect; renderSpec.halfWidth = Math.max(1, Math.floor(w / 2)); renderSpec.halfHeight = Math.max(1, Math.floor(h / 2)); renderSpec.halfArray[0] = renderSpec.halfWidth; renderSpec.halfArray[1] = renderSpec.halfHeight; renderSpec.halfArray[2] = renderSpec.halfWidth / renderSpec.halfHeight; }; function deleteRenderTarget(rt) { if(!rt) return; if(rt.frameBuffer) gl.deleteFramebuffer(rt.frameBuffer); if(rt.renderBuffer) gl.deleteRenderbuffer(rt.renderBuffer); if(rt.texture) gl.deleteTexture(rt.texture); rt.frameBuffer = null; rt.renderBuffer = null; rt.texture = null; } function createRenderTarget(w, h, withDepth) { w = Math.max(1, Math.floor(w)); h = Math.max(1, Math.floor(h)); var ret = { 'width':w, 'height':h, 'hasDepth':!!withDepth, 'sizeArray':new Float32Array([w, h, w / h]), 'dtxArray':new Float32Array([1.0 / w, 1.0 / h]) }; for(var errorIndex = 0; errorIndex < 16; errorIndex++) { if(gl.getError() === gl.NO_ERROR) break; } ret.frameBuffer = gl.createFramebuffer(); ret.texture = gl.createTexture(); if(!ret.frameBuffer || !ret.texture) { deleteRenderTarget(ret); throw new Error("Unable to allocate WebGL render target."); } try { gl.bindTexture(gl.TEXTURE_2D, ret.texture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); 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); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.bindFramebuffer(gl.FRAMEBUFFER, ret.frameBuffer); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, ret.texture, 0); if(ret.hasDepth) { ret.renderBuffer = gl.createRenderbuffer(); if(!ret.renderBuffer) throw new Error("Unable to allocate WebGL depth buffer."); gl.bindRenderbuffer(gl.RENDERBUFFER, ret.renderBuffer); gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, w, h); gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, ret.renderBuffer); } var allocationError = gl.getError(); if(allocationError !== gl.NO_ERROR) { throw new Error("WebGL render target allocation failed with error 0x" + allocationError.toString(16) + "."); } if(gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) { throw new Error("Incomplete WebGL framebuffer."); } gl.bindTexture(gl.TEXTURE_2D, null); gl.bindRenderbuffer(gl.RENDERBUFFER, null); gl.bindFramebuffer(gl.FRAMEBUFFER, null); return ret; } catch(error) { unbindRenderTargetState(); deleteRenderTarget(ret); throw error; } } function deleteRenderTargetSet(targets) { if(!targets) return; deleteRenderTarget(targets.mainRT); deleteRenderTarget(targets.wHalfRT0); deleteRenderTarget(targets.wHalfRT1); } function unbindRenderTargetState() { // The previous frame can leave these textures bound while a resize is // being committed. Unbind both sampling units before deleting them. gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, null); gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, null); gl.activeTexture(gl.TEXTURE0); gl.bindRenderbuffer(gl.RENDERBUFFER, null); gl.bindFramebuffer(gl.FRAMEBUFFER, null); } function estimateRenderTargetSetBytes(width, height) { var halfWidth = Math.max(1, Math.floor(width / 2)); var halfHeight = Math.max(1, Math.floor(height / 2)); // RGBA8 main color + DEPTH_COMPONENT16 + two RGBA8 half targets, with // headroom for driver alignment and internal depth-buffer padding. var formatBytes = width * height * 6 + halfWidth * halfHeight * 8; return Math.ceil(formatBytes * 1.25); } function createRenderTargetSet(width, height) { var halfWidth = Math.max(1, Math.floor(width / 2)); var halfHeight = Math.max(1, Math.floor(height / 2)); var targets = {}; try { targets.mainRT = createRenderTarget(width, height, true); targets.wHalfRT0 = createRenderTarget(halfWidth, halfHeight, false); targets.wHalfRT1 = createRenderTarget(halfWidth, halfHeight, false); return targets; } catch(error) { unbindRenderTargetState(); deleteRenderTargetSet(targets); throw error; } } function compileShader(shtype, shsrc) { var retsh = gl.createShader(shtype); if(!retsh) { console.error("Unable to create WebGL shader."); return null; } if(shtype === gl.FRAGMENT_SHADER && gl.getShaderPrecisionFormat) { try { var highp = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT); if(!highp || highp.precision === 0) { shsrc = shsrc.replace(/precision\s+highp\s+float\s*;/g, "precision mediump float;"); } } catch(precisionError) { shsrc = shsrc.replace(/precision\s+highp\s+float\s*;/g, "precision mediump float;"); } } gl.shaderSource(retsh, shsrc); gl.compileShader(retsh); if(!gl.getShaderParameter(retsh, gl.COMPILE_STATUS)) { var errlog = gl.getShaderInfoLog(retsh); gl.deleteShader(retsh); console.error(errlog); return null; } return retsh; } function createShader(vtxsrc, frgsrc, uniformlist, attrlist) { var vsh = compileShader(gl.VERTEX_SHADER, vtxsrc); var fsh = compileShader(gl.FRAGMENT_SHADER, frgsrc); if(vsh == null || fsh == null) { if(vsh) gl.deleteShader(vsh); if(fsh) gl.deleteShader(fsh); return null; } var prog = gl.createProgram(); if(!prog) { gl.deleteShader(vsh); gl.deleteShader(fsh); return null; } gl.attachShader(prog, vsh); gl.attachShader(prog, fsh); gl.deleteShader(vsh); gl.deleteShader(fsh); gl.linkProgram(prog); if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) { var errlog = gl.getProgramInfoLog(prog); console.error(errlog); gl.deleteProgram(prog); return null; } if(uniformlist) { prog.uniforms = {}; for(var i = 0; i < uniformlist.length; i++) { prog.uniforms[uniformlist[i]] = gl.getUniformLocation(prog, uniformlist[i]); } } if(attrlist) { prog.attributes = {}; for(var i = 0; i < attrlist.length; i++) { var attr = attrlist[i]; prog.attributes[attr] = gl.getAttribLocation(prog, attr); } } return prog; } function useShader(prog) { if(!prog) return false; gl.useProgram(prog); for(var attr in prog.attributes) { if(prog.attributes[attr] >= 0) gl.enableVertexAttribArray(prog.attributes[attr]); } return true; } function unuseShader(prog) { if(!prog) return; for(var attr in prog.attributes) { if(prog.attributes[attr] >= 0) gl.disableVertexAttribArray(prog.attributes[attr]); } gl.useProgram(null); } ///// var projection = { 'angle':60, 'nearfar':new Float32Array([0.1, 100.0]), 'matrix':Matrix44.createIdentity() }; var camera = { 'position':Vector3.create(0, 0, 100), 'lookat':Vector3.create(0, 0, 0), 'up':Vector3.create(0, 1, 0), 'dof':Vector3.create(10.0, 4.0, 8.0), 'matrix':Matrix44.createIdentity() }; var pointFlower = {}; var meshFlower = {}; var sceneStandBy = false; var BlossomParticle = function () { this.velocity = new Array(3); this.rotation = new Array(3); this.position = new Array(3); this.euler = new Array(3); this.size = 1.0; this.alpha = 1.0; this.zkey = 0.0; }; BlossomParticle.prototype.setVelocity = function (vx, vy, vz) { this.velocity[0] = vx; this.velocity[1] = vy; this.velocity[2] = vz; }; BlossomParticle.prototype.setRotation = function (rx, ry, rz) { this.rotation[0] = rx; this.rotation[1] = ry; this.rotation[2] = rz; }; BlossomParticle.prototype.setPosition = function (nx, ny, nz) { this.position[0] = nx; this.position[1] = ny; this.position[2] = nz; }; BlossomParticle.prototype.setEulerAngles = function (rx, ry, rz) { this.euler[0] = rx; this.euler[1] = ry; this.euler[2] = rz; }; BlossomParticle.prototype.setSize = function (s) { this.size = s; }; BlossomParticle.prototype.update = function (dt, et) { this.position[0] += this.velocity[0] * dt; this.position[1] += this.velocity[1] * dt; this.position[2] += this.velocity[2] * dt; this.euler[0] += this.rotation[0] * dt; this.euler[1] += this.rotation[1] * dt; this.euler[2] += this.rotation[2] * dt; }; function createPointFlowers() { // get point sizes var prm = gl.getParameter(gl.ALIASED_POINT_SIZE_RANGE); renderSpec.pointSize = {'min':prm[0], 'max':prm[1]}; var vtxsrc = shaderSources["sakura_point_vsh"]; var frgsrc = shaderSources["sakura_point_fsh"]; pointFlower.program = createShader( vtxsrc, frgsrc, ['uProjection', 'uModelview', 'uResolution', 'uOffset', 'uDOF', 'uFade'], ['aPosition', 'aEuler', 'aMisc'] ); if(!pointFlower.program) throw new Error("Unable to create Sakura shader program."); useShader(pointFlower.program); pointFlower.offset = new Float32Array([0.0, 0.0, 0.0]); pointFlower.fader = Vector3.create(0.0, 10.0, 0.0); // paramerters: velocity[3], rotate[3] pointFlower.numFlowers = 1600; pointFlower.particles = new Array(pointFlower.numFlowers); pointFlower.initialized = false; // vertex attributes {position[3], euler_xyz[3], size[1]} pointFlower.dataArray = new Float32Array(pointFlower.numFlowers * (3 + 3 + 2)); pointFlower.positionArrayOffset = 0; pointFlower.eulerArrayOffset = pointFlower.numFlowers * 3; pointFlower.miscArrayOffset = pointFlower.numFlowers * 6; pointFlower.buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, pointFlower.buffer); gl.bufferData(gl.ARRAY_BUFFER, pointFlower.dataArray, gl.DYNAMIC_DRAW); gl.bindBuffer(gl.ARRAY_BUFFER, null); unuseShader(pointFlower.program); for(var i = 0; i < pointFlower.numFlowers; i++) { pointFlower.particles[i] = new BlossomParticle(); } } function initPointFlowers() { //area pointFlower.area = Vector3.create(20.0, 20.0, 20.0); pointFlower.area.x = pointFlower.area.y * renderSpec.aspect; pointFlower.fader.x = 10.0; //env fade start pointFlower.fader.y = pointFlower.area.z; //env fade half pointFlower.fader.z = 0.1; //near fade start // A resize changes the camera envelope, not the simulation. Keep the // existing particles so responsive resizes do not visibly restart it. if(pointFlower.initialized) return; //particles var PI2 = Math.PI * 2.0; var tmpv3 = Vector3.create(0, 0, 0); var tmpv = 0; var symmetryrand = function() {return (Math.random() * 2.0 - 1.0);}; for(var i = 0; i < pointFlower.numFlowers; i++) { var tmpprtcl = pointFlower.particles[i]; //velocity tmpv3.x = symmetryrand() * 0.3 + 0.8; tmpv3.y = symmetryrand() * 0.2 - 1.0; tmpv3.z = symmetryrand() * 0.3 + 0.5; Vector3.normalize(tmpv3); tmpv = 2.0 + Math.random() * 1.0; tmpprtcl.setVelocity(tmpv3.x * tmpv, tmpv3.y * tmpv, tmpv3.z * tmpv); //rotation tmpprtcl.setRotation( symmetryrand() * PI2 * 0.5, symmetryrand() * PI2 * 0.5, symmetryrand() * PI2 * 0.5 ); //position tmpprtcl.setPosition( symmetryrand() * pointFlower.area.x, symmetryrand() * pointFlower.area.y, symmetryrand() * pointFlower.area.z ); //euler tmpprtcl.setEulerAngles( Math.random() * Math.PI * 2.0, Math.random() * Math.PI * 2.0, Math.random() * Math.PI * 2.0 ); //size tmpprtcl.setSize(0.9 + Math.random() * 0.1); } pointFlower.initialized = true; } function repeatParticlePosition(prt, cmp, limit) { if(Math.abs(prt.position[cmp]) - prt.size * 0.5 > limit) { // Modulo wrapping also handles a resumed tab or a large time step. var span = limit * 2.0; var wrapped = (prt.position[cmp] + limit) % span; if(wrapped < 0.0) wrapped += span; prt.position[cmp] = wrapped - limit; } } function repeatParticleEuler(prt, cmp, PI2) { prt.euler[cmp] = prt.euler[cmp] % PI2; if(prt.euler[cmp] < 0.0) { prt.euler[cmp] += PI2; } } function compareParticleDepth(p0, p1) { return p0.zkey - p1.zkey; } function renderPointFlowers() { //update var PI2 = Math.PI * 2.0; for(var i = 0; i < pointFlower.numFlowers; i++) { var prtcl = pointFlower.particles[i]; prtcl.update(timeInfo.delta, timeInfo.elapsed); repeatParticlePosition(prtcl, 0, pointFlower.area.x); repeatParticlePosition(prtcl, 1, pointFlower.area.y); repeatParticlePosition(prtcl, 2, pointFlower.area.z); repeatParticleEuler(prtcl, 0, PI2); repeatParticleEuler(prtcl, 1, PI2); repeatParticleEuler(prtcl, 2, PI2); prtcl.alpha = 1.0;//(pointFlower.area.z - prtcl.position[2]) * 0.5; prtcl.zkey = (camera.matrix[2] * prtcl.position[0] + camera.matrix[6] * prtcl.position[1] + camera.matrix[10] * prtcl.position[2] + camera.matrix[14]); } // sort pointFlower.particles.sort(compareParticleDepth); // update data var ipos = pointFlower.positionArrayOffset; var ieuler = pointFlower.eulerArrayOffset; var imisc = pointFlower.miscArrayOffset; for(var i = 0; i < pointFlower.numFlowers; i++) { var prtcl = pointFlower.particles[i]; pointFlower.dataArray[ipos] = prtcl.position[0]; pointFlower.dataArray[ipos + 1] = prtcl.position[1]; pointFlower.dataArray[ipos + 2] = prtcl.position[2]; ipos += 3; pointFlower.dataArray[ieuler] = prtcl.euler[0]; pointFlower.dataArray[ieuler + 1] = prtcl.euler[1]; pointFlower.dataArray[ieuler + 2] = prtcl.euler[2]; ieuler += 3; pointFlower.dataArray[imisc] = prtcl.size; pointFlower.dataArray[imisc + 1] = prtcl.alpha; imisc += 2; } //draw gl.enable(gl.BLEND); //gl.disable(gl.DEPTH_TEST); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); var prog = pointFlower.program; useShader(prog); gl.uniformMatrix4fv(prog.uniforms.uProjection, false, projection.matrix); gl.uniformMatrix4fv(prog.uniforms.uModelview, false, camera.matrix); gl.uniform3fv(prog.uniforms.uResolution, renderSpec.array); gl.uniform3fv(prog.uniforms.uDOF, Vector3.arrayForm(camera.dof)); gl.uniform3fv(prog.uniforms.uFade, Vector3.arrayForm(pointFlower.fader)); gl.bindBuffer(gl.ARRAY_BUFFER, pointFlower.buffer); gl.bufferData(gl.ARRAY_BUFFER, pointFlower.dataArray, gl.DYNAMIC_DRAW); gl.vertexAttribPointer(prog.attributes.aPosition, 3, gl.FLOAT, false, 0, pointFlower.positionArrayOffset * Float32Array.BYTES_PER_ELEMENT); gl.vertexAttribPointer(prog.attributes.aEuler, 3, gl.FLOAT, false, 0, pointFlower.eulerArrayOffset * Float32Array.BYTES_PER_ELEMENT); gl.vertexAttribPointer(prog.attributes.aMisc, 2, gl.FLOAT, false, 0, pointFlower.miscArrayOffset * Float32Array.BYTES_PER_ELEMENT); // doubler for(var i = 1; i < 2; i++) { var zpos = i * -2.0; pointFlower.offset[0] = pointFlower.area.x * -1.0; pointFlower.offset[1] = pointFlower.area.y * -1.0; pointFlower.offset[2] = pointFlower.area.z * zpos; gl.uniform3fv(prog.uniforms.uOffset, pointFlower.offset); gl.drawArrays(gl.POINTS, 0, pointFlower.numFlowers); pointFlower.offset[0] = pointFlower.area.x * -1.0; pointFlower.offset[1] = pointFlower.area.y * 1.0; pointFlower.offset[2] = pointFlower.area.z * zpos; gl.uniform3fv(prog.uniforms.uOffset, pointFlower.offset); gl.drawArrays(gl.POINTS, 0, pointFlower.numFlowers); pointFlower.offset[0] = pointFlower.area.x * 1.0; pointFlower.offset[1] = pointFlower.area.y * -1.0; pointFlower.offset[2] = pointFlower.area.z * zpos; gl.uniform3fv(prog.uniforms.uOffset, pointFlower.offset); gl.drawArrays(gl.POINTS, 0, pointFlower.numFlowers); pointFlower.offset[0] = pointFlower.area.x * 1.0; pointFlower.offset[1] = pointFlower.area.y * 1.0; pointFlower.offset[2] = pointFlower.area.z * zpos; gl.uniform3fv(prog.uniforms.uOffset, pointFlower.offset); gl.drawArrays(gl.POINTS, 0, pointFlower.numFlowers); } //main pointFlower.offset[0] = 0.0; pointFlower.offset[1] = 0.0; pointFlower.offset[2] = 0.0; gl.uniform3fv(prog.uniforms.uOffset, pointFlower.offset); gl.drawArrays(gl.POINTS, 0, pointFlower.numFlowers); gl.bindBuffer(gl.ARRAY_BUFFER, null); unuseShader(prog); gl.enable(gl.DEPTH_TEST); gl.disable(gl.BLEND); } // effects //common util function createEffectProgram(vtxsrc, frgsrc, exunifs, exattrs) { var ret = {}; var unifs = ['uResolution', 'uSrc', 'uDelta']; if(exunifs) { unifs = unifs.concat(exunifs); } var attrs = ['aPosition']; if(exattrs) { attrs = attrs.concat(exattrs); } ret.program = createShader(vtxsrc, frgsrc, unifs, attrs); if(!ret.program) throw new Error("Unable to create post-process shader program."); useShader(ret.program); ret.dataArray = new Float32Array([ -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0 ]); ret.buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, ret.buffer); gl.bufferData(gl.ARRAY_BUFFER, ret.dataArray, gl.STATIC_DRAW); gl.bindBuffer(gl.ARRAY_BUFFER, null); unuseShader(ret.program); return ret; } // basic usage // useEffect(prog, srctex({'texture':texid, 'dtxArray':(f32)[dtx, dty]})); //basic initialize // gl.uniform**(...); //additional uniforms // drawEffect() // unuseEffect(prog) // TEXTURE0 makes src function useEffect(fxobj, srctex) { var prog = fxobj.program; useShader(prog); gl.uniform3fv(prog.uniforms.uResolution, renderSpec.array); if(srctex != null) { gl.uniform2fv(prog.uniforms.uDelta, srctex.dtxArray); gl.uniform1i(prog.uniforms.uSrc, 0); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, srctex.texture); } } function drawEffect(fxobj) { gl.bindBuffer(gl.ARRAY_BUFFER, fxobj.buffer); gl.vertexAttribPointer(fxobj.program.attributes.aPosition, 2, gl.FLOAT, false, 0, 0); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); } function unuseEffect(fxobj) { unuseShader(fxobj.program); } var effectLib = {}; function createEffectLib() { var vtxsrc, frgsrc; //common var cmnvtxsrc = shaderSources["fx_common_vsh"]; //background frgsrc = shaderSources["bg_fsh"]; effectLib.sceneBg = createEffectProgram(cmnvtxsrc, frgsrc, ['uTimes'], null); // make brightpixels buffer frgsrc = shaderSources["fx_brightbuf_fsh"]; effectLib.mkBrightBuf = createEffectProgram(cmnvtxsrc, frgsrc, null, null); // direction blur frgsrc = shaderSources["fx_dirblur_r4_fsh"]; effectLib.dirBlur = createEffectProgram(cmnvtxsrc, frgsrc, ['uBlurDir'], null); //final composite vtxsrc = shaderSources["pp_final_vsh"]; frgsrc = shaderSources["pp_final_fsh"]; effectLib.finalComp = createEffectProgram(vtxsrc, frgsrc, ['uBloom'], null); } // background function createBackground() { //console.log("create background"); } function initBackground() { //console.log("init background"); } function renderBackground() { gl.disable(gl.DEPTH_TEST); useEffect(effectLib.sceneBg, null); gl.uniform2f(effectLib.sceneBg.program.uniforms.uTimes, timeInfo.elapsed, timeInfo.delta); drawEffect(effectLib.sceneBg); unuseEffect(effectLib.sceneBg); gl.enable(gl.DEPTH_TEST); } // post process var postProcess = {}; function createPostProcess() { //console.log("create post process"); } function initPostProcess() { //console.log("init post process"); } function bindRenderTarget(rt, isclear) { gl.bindFramebuffer(gl.FRAMEBUFFER, rt.frameBuffer); gl.viewport(0, 0, rt.width, rt.height); if(isclear) { gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT | (rt.hasDepth ? gl.DEPTH_BUFFER_BIT : 0)); } } function renderPostProcess() { gl.disable(gl.DEPTH_TEST); //make bright buff bindRenderTarget(renderSpec.wHalfRT0, true); useEffect(effectLib.mkBrightBuf, renderSpec.mainRT); drawEffect(effectLib.mkBrightBuf); unuseEffect(effectLib.mkBrightBuf); // make bloom for(var i = 0; i < 2; i++) { var p = 1.5 + 1 * i; var s = 2.0 + 1 * i; bindRenderTarget(renderSpec.wHalfRT1, true); useEffect(effectLib.dirBlur, renderSpec.wHalfRT0); gl.uniform4f(effectLib.dirBlur.program.uniforms.uBlurDir, p, 0.0, s, 0.0); drawEffect(effectLib.dirBlur); unuseEffect(effectLib.dirBlur); bindRenderTarget(renderSpec.wHalfRT0, true); useEffect(effectLib.dirBlur, renderSpec.wHalfRT1); gl.uniform4f(effectLib.dirBlur.program.uniforms.uBlurDir, 0.0, p, 0.0, s); drawEffect(effectLib.dirBlur); unuseEffect(effectLib.dirBlur); } //display gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.viewport(0, 0, renderSpec.width, renderSpec.height); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); useEffect(effectLib.finalComp, renderSpec.mainRT); gl.uniform1i(effectLib.finalComp.program.uniforms.uBloom, 1); gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, renderSpec.wHalfRT0.texture); drawEffect(effectLib.finalComp); unuseEffect(effectLib.finalComp); gl.enable(gl.DEPTH_TEST); } ///// var SceneEnv = {}; function createScene() { createEffectLib(); createBackground(); createPointFlowers(); createPostProcess(); sceneStandBy = true; } function initScene() { initBackground(); initPointFlowers(); initPostProcess(); //camera.position.z = 17.320508; camera.position.z = pointFlower.area.z + projection.nearfar[0]; projection.angle = Math.atan2(pointFlower.area.y, camera.position.z + pointFlower.area.z) * 180.0 / Math.PI * 2.0; Matrix44.loadProjection(projection.matrix, renderSpec.aspect, projection.angle, projection.nearfar[0], projection.nearfar[1]); } function renderScene() { //draw Matrix44.loadLookAt(camera.matrix, camera.position, camera.lookat, camera.up); gl.enable(gl.DEPTH_TEST); //gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.bindFramebuffer(gl.FRAMEBUFFER, renderSpec.mainRT.frameBuffer); gl.viewport(0, 0, renderSpec.mainRT.width, renderSpec.mainRT.height); gl.clearColor(0.005, 0, 0.05, 0); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); renderBackground(); renderPointFlowers(); renderPostProcess(); } ///// var sakuraCanvas = null; var resizeTimer = null; var resizeRequestVersion = 0; var resizeDebounceMs = 140; var normalRenderPixelBudget = 10000000; var constrainedRenderPixelBudget = 6000000; var transactionalPeakByteLimit = 128 * 1024 * 1024; var constrainedPeakByteLimit = 96 * 1024 * 1024; var minimumFallbackPixelRatio = 0.5; function hasCompleteRenderTargetSet() { return !!(renderSpec.mainRT && renderSpec.wHalfRT0 && renderSpec.wHalfRT1); } function getRenderPixelBudget() { var deviceMemory = Number(navigator.deviceMemory); if(isFinite(deviceMemory) && deviceMemory > 0 && deviceMemory <= 4) { return constrainedRenderPixelBudget; } return normalRenderPixelBudget; } function getRenderDimensionLimit() { if(!gl || !gl.getParameter) return Infinity; var maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE) || Infinity; var maxRenderbufferSize = gl.getParameter(gl.MAX_RENDERBUFFER_SIZE) || Infinity; return Math.min(maxTextureSize, maxRenderbufferSize); } function createCanvasSize(cssWidth, cssHeight, pixelRatio, requestedPixelRatio) { var width = Math.max(1, Math.round(cssWidth * pixelRatio)); var height = Math.max(1, Math.round(cssHeight * pixelRatio)); return { 'cssWidth':cssWidth, 'cssHeight':cssHeight, 'width':width, 'height':height, 'pixels':width * height, 'pixelRatio':pixelRatio, 'requestedPixelRatio':requestedPixelRatio }; } function measureCanvasSize() { var d = document.documentElement; var cssWidth = Math.max(1, window.innerWidth || d.clientWidth || 1); var cssHeight = Math.max(1, window.innerHeight || d.clientHeight || 1); var requestedPixelRatio = Math.min(Math.max(window.devicePixelRatio || 1, 1), 2); var budgetRatio = Math.sqrt(getRenderPixelBudget() / (cssWidth * cssHeight)); var dimensionLimit = getRenderDimensionLimit(); var dimensionRatio = Math.min(dimensionLimit / cssWidth, dimensionLimit / cssHeight); var pixelRatio = Math.min(requestedPixelRatio, budgetRatio, dimensionRatio); if(!isFinite(pixelRatio) || pixelRatio <= 0) pixelRatio = 0.25; return createCanvasSize(cssWidth, cssHeight, pixelRatio, requestedPixelRatio); } function getResizeCandidates(canvasSize) { var ratios = []; var floorRatio = canvasSize.pixelRatio < minimumFallbackPixelRatio ? canvasSize.pixelRatio * 0.5 : minimumFallbackPixelRatio; var factors = [1.0, 0.8, 0.6, 0.5]; for(var i = 0; i < factors.length; i++) { var ratio = Math.max(floorRatio, canvasSize.pixelRatio * factors[i]); var duplicate = false; for(var j = 0; j < ratios.length; j++) { if(Math.abs(ratios[j] - ratio) < 0.0001) { duplicate = true; break; } } if(!duplicate) ratios.push(ratio); } return ratios.map(function(ratio) { return createCanvasSize(canvasSize.cssWidth, canvasSize.cssHeight, ratio, canvasSize.requestedPixelRatio); }); } function shouldReleaseBeforeAllocate(previousTargets, width, height) { if(!previousTargets || !previousTargets.mainRT) return false; var deviceMemory = Number(navigator.deviceMemory); var peakLimit = isFinite(deviceMemory) && deviceMemory > 0 && deviceMemory <= 4 ? constrainedPeakByteLimit : transactionalPeakByteLimit; var oldBytes = estimateRenderTargetSetBytes(previousTargets.mainRT.width, previousTargets.mainRT.height); var nextBytes = estimateRenderTargetSetBytes(width, height); return oldBytes + nextBytes > peakLimit; } function commitCanvasSize(canvas, canvasSize) { if(canvas.width !== canvasSize.width) canvas.width = canvasSize.width; if(canvas.height !== canvasSize.height) canvas.height = canvasSize.height; } function onResize() { if(!sakuraCanvas || contextLost) return; if(resizeTimer !== null) window.clearTimeout(resizeTimer); var requestVersion = ++resizeRequestVersion; resizeTimer = window.setTimeout(function() { resizeTimer = null; window.requestAnimationFrame(function() { if(contextLost || requestVersion !== resizeRequestVersion) return; var canvasSize = measureCanvasSize(); if(hasCompleteRenderTargetSet() && renderSpec.width === canvasSize.width && renderSpec.height === canvasSize.height) { renderSpec.pixelRatio = canvasSize.pixelRatio; return; } try { setViewports(canvasSize); if(sceneStandBy) initScene(); if(prefersReducedMotion && sceneStandBy) render(); else if(animating) scheduleAnimationFrame(); } catch(error) { // The canvas backing size was not changed, so its last // composited frame remains visible while a later retry is // requested by another resize or context restoration. console.error("Unable to resize Sakura render targets.", error); if(prefersReducedMotion && sceneStandBy && hasCompleteRenderTargetSet()) render(); } }); }, resizeDebounceMs); } function setViewports(canvasSize) { var width = Math.max(1, canvasSize.width); var height = Math.max(1, canvasSize.height); if(hasCompleteRenderTargetSet() && renderSpec.width === width && renderSpec.height === height) { renderSpec.pixelRatio = canvasSize.pixelRatio; gl.viewport(0, 0, width, height); return false; } var previousTargets = { 'mainRT':renderSpec.mainRT, 'wHalfRT0':renderSpec.wHalfRT0, 'wHalfRT1':renderSpec.wHalfRT1 }; var releaseBeforeAllocate = shouldReleaseBeforeAllocate(previousTargets, width, height); if(releaseBeforeAllocate) { // This path deliberately trades allocation rollback for a bounded // peak. The default framebuffer is kept at its old size until the // replacement set is ready, so the last composited frame remains on // screen during the short resize transaction. gl.finish(); unbindRenderTargetState(); deleteRenderTargetSet(previousTargets); renderSpec.mainRT = null; renderSpec.wHalfRT0 = null; renderSpec.wHalfRT1 = null; } var nextTargets = null; var lastError = null; var candidates = getResizeCandidates(canvasSize); for(var i = 0; i < candidates.length; i++) { try { nextTargets = createRenderTargetSet(candidates[i].width, candidates[i].height); canvasSize = candidates[i]; break; } catch(error) { lastError = error; if(gl.isContextLost && gl.isContextLost()) throw error; } } if(!nextTargets) { if(!releaseBeforeAllocate) { unbindRenderTargetState(); } throw lastError || new Error("Unable to allocate Sakura render targets."); } commitCanvasSize(sakuraCanvas, canvasSize); renderSpec.mainRT = nextTargets.mainRT; renderSpec.wHalfRT0 = nextTargets.wHalfRT0; renderSpec.wHalfRT1 = nextTargets.wHalfRT1; renderSpec.setSize(canvasSize.width, canvasSize.height); renderSpec.pixelRatio = canvasSize.pixelRatio; gl.clearColor(0.2, 0.2, 0.5, 1.0); gl.viewport(0, 0, renderSpec.width, renderSpec.height); if(!releaseBeforeAllocate) { unbindRenderTargetState(); deleteRenderTargetSet(previousTargets); } return true; } function render() { if(!hasCompleteRenderTargetSet() || contextLost) return false; renderScene(); return true; } var animating = true; var contextLost = false; var sakuraInitialized = false; var pageVisible = !document.hidden; var framePending = false; var frameRequestId = null; var frameRequestGeneration = 0; var maximumFrameRate = 120; var minimumFrameInterval = 1000.0 / maximumFrameRate; var frameIntervalTolerance = 0.01; var frameAccumulator = 0; var previousAnimationFrameTime = 0; var reducedMotionQuery = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)') : null; var prefersReducedMotion = !!(reducedMotionQuery && reducedMotionQuery.matches); function resetFrameLimiter(now) { previousAnimationFrameTime = now; frameAccumulator = 0; timeInfo.prev = now; timeInfo.delta = 0; } function shouldRenderFrame(now) { var animationFrameDelta = Math.min(Math.max(now - previousAnimationFrameTime, 0), 250); previousAnimationFrameTime = now; frameAccumulator += animationFrameDelta; if(frameAccumulator + frameIntervalTolerance < minimumFrameInterval) return false; // Preserve fractional time so 144/165 Hz displays converge to 120 FPS // instead of falling to a coarse refresh-rate divisor. frameAccumulator -= minimumFrameInterval; if(frameAccumulator < 0) frameAccumulator = 0; if(frameAccumulator >= minimumFrameInterval) frameAccumulator %= minimumFrameInterval; if(minimumFrameInterval - frameAccumulator < frameIntervalTolerance) frameAccumulator = 0; return true; } function cancelScheduledAnimationFrame() { frameRequestGeneration++; if(frameRequestId !== null && window.cancelAnimationFrame) { window.cancelAnimationFrame(frameRequestId); } frameRequestId = null; framePending = false; } function scheduleAnimationFrame() { if(framePending || !animating || !pageVisible || contextLost || prefersReducedMotion || !hasCompleteRenderTargetSet()) return; framePending = true; var requestGeneration = frameRequestGeneration; frameRequestId = window.requestAnimationFrame(function(timestamp) { if(requestGeneration !== frameRequestGeneration) return; framePending = false; frameRequestId = null; animate(timestamp); }); } function readPageVisibility() { if(typeof document.hidden === "boolean") return !document.hidden; return document.visibilityState !== "hidden"; } function synchronizeAnimationState(reducedMotionOverride) { pageVisible = readPageVisibility(); if(typeof reducedMotionOverride === "boolean") { prefersReducedMotion = reducedMotionOverride; } else if(reducedMotionQuery) { prefersReducedMotion = !!reducedMotionQuery.matches; } if(!pageVisible || contextLost || !animating) { cancelScheduledAnimationFrame(); return; } if(!sceneStandBy || !hasCompleteRenderTargetSet()) return; // A browser can discard a queued frame while a page is hidden or being // restored. Replace any pending request so cold starts cannot retain a // stale framePending flag. Generation guards isolate the old callback. if(framePending) cancelScheduledAnimationFrame(); resetFrameLimiter(currentTime()); if(prefersReducedMotion) { timeInfo.delta = 0; render(); } else { scheduleAnimationFrame(); } } function onReducedMotionChange(event) { var nextPreference = !!event.matches; if(nextPreference === prefersReducedMotion) return; synchronizeAnimationState(nextPreference); } function onPageHide() { pageVisible = false; cancelScheduledAnimationFrame(); } function toggleAnimation(elm) { animating = !animating; if(animating) { synchronizeAnimationState(); } else { cancelScheduledAnimationFrame(); } if(elm) { elm.innerHTML = animating? "Stop":"Start"; } } function stepAnimation() { if(!animating && pageVisible && !contextLost) animate(currentTime(), true); } function animate(timestamp, singleStep) { if(contextLost || !pageVisible || (prefersReducedMotion && !singleStep) || (!animating && !singleStep)) return; var now = typeof timestamp === "number" ? timestamp : currentTime(); if(!timeInfo.start) timeInfo.start = now; if(!singleStep && !shouldRenderFrame(now)) { scheduleAnimationFrame(); return; } timeInfo.elapsed = (now - timeInfo.start) / 1000.0; timeInfo.delta = Math.min(Math.max((now - timeInfo.prev) / 1000.0, 0.0), 0.05); timeInfo.prev = now; render(); if(animating && !prefersReducedMotion) scheduleAnimationFrame(); } function initializeSakura() { if(sakuraInitialized) return; sakuraCanvas = document.getElementById("sakura"); if(!sakuraCanvas) return; sakuraInitialized = true; var initialCanvasSize = null; try { gl = sakuraCanvas.getContext('webgl', { alpha: true, antialias: false, depth: true, powerPreference: 'high-performance' }) || sakuraCanvas.getContext('experimental-webgl'); if(!gl) throw new Error("WebGL is unavailable."); initialCanvasSize = measureCanvasSize(); } catch(e) { console.error(e); sakuraCanvas.hidden = true; return; } window.addEventListener('resize', onResize); sakuraCanvas.addEventListener('webglcontextlost', function(event) { event.preventDefault(); contextLost = true; cancelScheduledAnimationFrame(); resizeRequestVersion++; if(resizeTimer !== null) { window.clearTimeout(resizeTimer); resizeTimer = null; } }, false); sakuraCanvas.addEventListener('webglcontextrestored', function() { contextLost = false; sceneStandBy = false; pointFlower = {}; effectLib = {}; renderSpec.mainRT = null; renderSpec.wHalfRT0 = null; renderSpec.wHalfRT1 = null; try { var restoredCanvasSize = measureCanvasSize(); setViewports(restoredCanvasSize); createScene(); initScene(); timeInfo.start = currentTime(); resetFrameLimiter(timeInfo.start); synchronizeAnimationState(); } catch(error) { contextLost = true; console.error(error); sakuraCanvas.hidden = true; } }, false); try { setViewports(initialCanvasSize); createScene(); initScene(); timeInfo.start = currentTime(); resetFrameLimiter(timeInfo.start); synchronizeAnimationState(); } catch(error) { console.error(error); sakuraCanvas.hidden = true; } } // Set animation-frame fallbacks for older embedded browsers. (function(w) { w.requestAnimationFrame = w.requestAnimationFrame || w.webkitRequestAnimationFrame || w.mozRequestAnimationFrame || function(callback) { return w.setTimeout(callback, 1000 / 60); }; w.cancelAnimationFrame = w.cancelAnimationFrame || w.webkitCancelAnimationFrame || w.mozCancelAnimationFrame || function(requestId) { w.clearTimeout(requestId); }; })(window); if(reducedMotionQuery) { if(reducedMotionQuery.addEventListener) reducedMotionQuery.addEventListener('change', onReducedMotionChange); else if(reducedMotionQuery.addListener) reducedMotionQuery.addListener(onReducedMotionChange); } document.addEventListener('visibilitychange', synchronizeAnimationState); window.addEventListener('pagehide', onPageHide); window.addEventListener('pageshow', synchronizeAnimationState); if(document.readyState === "loading") { document.addEventListener('DOMContentLoaded', initializeSakura, { once:true }); } else { initializeSakura(); } })();