/** * A grid items to fullscreen transition * @module GridToFullscreenEffect * @author Daniel Velasquez * @version 1.0.0 */ class GridToFullscreenEffect { /** Initializes instance variables. @param {HTMLDivElement} container - Container of the webGL canvas. @param {HTMLDivElement} itemsWrapper - Container of the grid items. @param {object} options - A configuration object. */ constructor(container, itemsWrapper, options = {}) { this.container = container; this.itemsWrapper = itemsWrapper; this.initialised = false; this.camera = null; this.scene = null; this.renderer = null; options.scrollContainer = options.scrollContainer || null; options.timing = options.timing || {}; options.timing.type = options.timing.type || "sameEnd"; options.timing.sections = options.timing.sections || 1; options.timing.latestStart = options.timing.latestStart || 0.5; options.timing.duration = options.timing.duration || 1; options.transformation = options.transformation || {}; options.transformation.type = options.transformation.type || "none"; options.transformation.props = options.transformation.props || {}; options.activation.type = options.activation.type || "topLeft"; options.seed = options.seed || 0; options.easings = options.easings || {}; options.easings.toFullscreen = options.easings.toFullscreen || Power0.easeNone; options.easings.toGrid = options.easings.toGrid || Power0.easeNone; options.flipBeizerControls = options.flipBeizerControls || {}; options.flipBeizerControls.c0 = options.flipBeizerControls.c0 || {}; options.flipBeizerControls.c0.x = options.flipBeizerControls.c0.x || 0.5; options.flipBeizerControls.c0.y = options.flipBeizerControls.c0.y || 0.5; options.flipBeizerControls.c1 = options.flipBeizerControls.c1 || {}; options.flipBeizerControls.c1.x = options.flipBeizerControls.c1.x || 0.5; options.flipBeizerControls.c1.y = options.flipBeizerControls.c1.y || 0.5; this.options = options; this.uniforms = { uImage: new THREE.Uniform(null), uImageRes: new THREE.Uniform(new THREE.Vector2(1, 1)), uImageLarge: new THREE.Uniform(null), uImageLargeRes: new THREE.Uniform(new THREE.Vector2(1, 1)), // Calculated Uniforms uProgress: new THREE.Uniform(0), uMeshScale: new THREE.Uniform(new THREE.Vector2(1, 1)), uPlaneCenter: new THREE.Uniform(new THREE.Vector2(0, 0)), uViewSize: new THREE.Uniform(new THREE.Vector2(1, 1)), uScaleToViewSize: new THREE.Uniform(new THREE.Vector2(1, 1)), uClosestCorner: new THREE.Uniform(0), uMouse: new THREE.Uniform(new THREE.Vector2(0, 0)), // Option Uniforms uSeed: new THREE.Uniform(options.seed), uProgressByParts: new THREE.Uniform(options.timing.type === "sections"), uActivationParts: new THREE.Uniform(options.timing.sections), uSyncLatestStart: new THREE.Uniform(options.timing.latestStart), uBeizerControls: new THREE.Uniform( new THREE.Vector4( options.flipBeizerControls.c0.x, options.flipBeizerControls.c0.y, options.flipBeizerControls.c1.x, options.flipBeizerControls.c1.y ) ) }; this.textures = []; this.currentImageIndex = -1; this.isFullscreen = false; this.isAnimating = false; this.onResize = this.onResize = this.onResize.bind(this); } resetUniforms() { this.uniforms.uMeshScale.value = new THREE.Vector2(1, 1); this.uniforms.uPlaneCenter.value = new THREE.Vector2(0, 0); this.uniforms.uScaleToViewSize.value = new THREE.Vector2(1, 1); this.uniforms.uClosestCorner.value = 0; this.uniforms.uMouse.value = new THREE.Vector2(0, 0); this.uniforms.uImage.value = null; this.uniforms.uImageRes.value = new THREE.Vector2(1, 1); this.uniforms.uImageLarge.value = null; this.uniforms.uImageLargeRes.value = new THREE.Vector2(1, 1); const mesh = this.mesh; mesh.scale.x = 0.00001; mesh.scale.y = 0.00001; mesh.position.x = 0; mesh.position.y = 0; } /** An image @typedef {Object} ImageObject @property {HTMLImageElement} element - Html element of image @property {Image} image - Image object An set of small and large image @typedef {Object} ImageSet @property {ImageObject} small - Small image element @property {ImageObject} small - Large image element */ /** Creates the textures for the plane and sets them if needed. @param {imageSet[]} images - Small and large images of grid items. */ createTextures(images) { const textures = []; for (let i = 0; i < images.length; i++) { const imageSet = images[i]; const largeTexture = new THREE.Texture(imageSet.large.image); // So It doesnt get resized to the power of 2 largeTexture.generateMipmaps = false; largeTexture.wrapS = largeTexture.wrapT = THREE.ClampToEdgeWrapping; largeTexture.minFilter = THREE.LinearFilter; largeTexture.needsUpdate = true; const smallTexture = new THREE.Texture(imageSet.small.image); smallTexture.generateMipmaps = false; smallTexture.wrapS = smallTexture.wrapT = THREE.ClampToEdgeWrapping; smallTexture.minFilter = THREE.LinearFilter; smallTexture.needsUpdate = true; const textureSet = { large: { element: imageSet.large.element, texture: largeTexture }, small: { element: imageSet.small.element, texture: smallTexture } }; textures.push(textureSet); } this.textures = textures; this.setCurrentTextures(); } /** Sets the correct textures to the uniforms. And renders if not in a loop */ setCurrentTextures() { if (this.currentImageIndex === -1) return; const textureSet = this.textures[this.currentImageIndex]; this.uniforms.uImage.value = textureSet.small.texture; this.uniforms.uImageRes.value.x = textureSet.small.texture.image.naturalWidth; this.uniforms.uImageRes.value.y = textureSet.small.texture.image.naturalHeight; this.uniforms.uImageLarge.value = textureSet.large.texture; this.uniforms.uImageLargeRes.value.x = textureSet.large.texture.image.naturalWidth; this.uniforms.uImageLargeRes.value.y = textureSet.large.texture.image.naturalHeight; if (!this.isAnimating) { this.render(); } } /** Initiates THREEJS objects and adds listeners to the items */ init() { this.renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true }); this.renderer.setPixelRatio(window.devicePixelRatio); this.renderer.setSize(window.innerWidth, window.innerHeight); this.container.appendChild(this.renderer.domElement); this.scene = new THREE.Scene(); this.camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.1, 10000 ); this.camera.position.z = 50; this.camera.lookAt = this.scene.position; const viewSize = this.getViewSize(); this.uniforms.uViewSize.value = new THREE.Vector2( viewSize.width, viewSize.height ); const segments = 128; var geometry = new THREE.PlaneBufferGeometry(1, 1, segments, segments); function isFunction(functionToCheck) { return ( functionToCheck && {}.toString.call(functionToCheck) === "[object Function]" ); } const transformation = isFunction(this.options.transformation.type) ? this.options.transformation.type(this.options.transformation.props) : transformations[this.options.transformation.type]( this.options.transformation.props ); const shaders = generateShaders( activations[this.options.activation.type], transformation ); var material = new THREE.ShaderMaterial({ uniforms: this.uniforms, vertexShader: shaders.vertex, fragmentShader: shaders.fragment, side: THREE.DoubleSide }); this.mesh = new THREE.Mesh(geometry, material); this.scene.add(this.mesh); window.addEventListener("resize", this.onResize); if (this.options.scrollContainer) { this.options.scrollContainer.addEventListener("scroll", ev => { this.recalculateUniforms(ev); }); } for (let i = 0; i < this.itemsWrapper.children.length; i++) { const image = this.itemsWrapper.children[i].children[0]; image.addEventListener("mousedown", this.createOnMouseDown(i)); } } /** Creates a listener that sends item to fullscreen when activated. @return {function} Event listener */ createOnMouseDown(itemIndex) { return ev => { this.toFullscreen(itemIndex, ev); }; } /* Tweens the plane to grid position if on fullscreen */ toGrid() { if (!this.isFullscreen || this.isAnimating) return; this.isAnimating = true; if (this.options.onToGridStart) this.options.onToGridStart({ index: this.currentImageIndex }); this.tween = TweenLite.to( this.uniforms.uProgress, this.options.timing.duration, { value: 0, ease: this.options.easings.toGrid, onUpdate: () => { this.render(); }, onComplete: () => { this.isAnimating = false; this.isFullscreen = false; this.itemsWrapper.style.zIndex = 1; this.container.style.zIndex = 0; this.resetUniforms(); this.render(); if (this.options.onToGridFinish) this.options.onToGridFinish({ index: -1, lastIndex: this.currentImageIndex }); this.currentImageIndex = -1; } } ); } recalculateUniforms(ev) { if (this.currentImageIndex === -1) return; const rect = this.itemsWrapper.children[ this.currentImageIndex ].children[0].getBoundingClientRect(); const mouseNormalized = { x: (ev.clientX - rect.left) / rect.width, // Allways invert Y coord y: 1 - (ev.clientY - rect.top) / rect.height }; const xIndex = rect.left > window.innerWidth - (rect.left + rect.width); const yIndex = rect.top > window.innerHeight - (rect.top + rect.height); const closestCorner = xIndex * 2 + yIndex; this.uniforms.uClosestCorner.value = closestCorner; this.uniforms.uMouse.value = new THREE.Vector2( mouseNormalized.x, mouseNormalized.y ); const viewSize = this.getViewSize(); const widthViewUnit = (rect.width * viewSize.width) / window.innerWidth; const heightViewUnit = (rect.height * viewSize.height) / window.innerHeight; // x and y are based on top left of screen. While ThreeJs is on the center const xViewUnit = (rect.left * viewSize.width) / window.innerWidth - viewSize.width / 2; const yViewUnit = (rect.top * viewSize.height) / window.innerHeight - viewSize.height / 2; const geometry = this.mesh.geometry; const mesh = this.mesh; // // The plain's size is initially 1. So the scale is the new size mesh.scale.x = widthViewUnit; mesh.scale.y = heightViewUnit; // // Move the object by its top left corner, not the center let x = xViewUnit + widthViewUnit / 2; let y = -yViewUnit - heightViewUnit / 2; // geometry.translate(x, y, 0); mesh.position.x = x; mesh.position.y = y; // Used to scale the plane from the center // divided by scale so when later scaled it looks fine this.uniforms.uPlaneCenter.value.x = x / widthViewUnit; this.uniforms.uPlaneCenter.value.y = y / heightViewUnit; this.uniforms.uMeshScale.value.x = widthViewUnit; this.uniforms.uMeshScale.value.y = heightViewUnit; this.uniforms.uScaleToViewSize.value.x = viewSize.width / widthViewUnit - 1; this.uniforms.uScaleToViewSize.value.y = viewSize.height / heightViewUnit - 1; } toFullscreen(itemIndex, ev) { if (this.isFullscreen || this.isAnimating) return; this.isAnimating = true; this.currentImageIndex = itemIndex; this.recalculateUniforms(ev); if (this.textures[itemIndex]) { const textureSet = this.textures[itemIndex]; this.uniforms.uImage.value = textureSet.small.texture; this.uniforms.uImageRes.value.x = textureSet.small.texture.image.naturalWidth; this.uniforms.uImageRes.value.y = textureSet.small.texture.image.naturalHeight; this.uniforms.uImageLarge.value = textureSet.large.texture; this.uniforms.uImageLargeRes.value.x = textureSet.large.texture.image.naturalWidth; this.uniforms.uImageLargeRes.value.y = textureSet.large.texture.image.naturalHeight; } this.itemsWrapper.style.zIndex = 0; this.container.style.zIndex = 2; if (this.options.onToFullscreenStart) this.options.onToFullscreenStart({ index: this.currentImageIndex }); this.tween = TweenLite.to( this.uniforms.uProgress, this.options.timing.duration, { value: 1, ease: this.options.easings.toFullscreen, onUpdate: () => { this.render(); }, onComplete: () => { this.isAnimating = false; this.isFullscreen = true; if (this.options.onToFullscreenFinish) this.options.onToFullscreenFinish({ index: this.currentImageIndex }); } } ); } /** Gives the width and height of the current camera's view. @typedef {Object} Size @property {number} width @property {number} height @return {Size} The size of the camera's view */ getViewSize() { const fovInRadians = (this.camera.fov * Math.PI) / 180; const height = Math.abs( this.camera.position.z * Math.tan(fovInRadians / 2) * 2 ); return { width: height * this.camera.aspect, height }; } /** Renders ThreeJS to the canvas. */ render() { this.renderer.render(this.scene, this.camera); } /** Resize Event Listener. Updates anything that uses the window's size. @param {Event} ev resize event */ onResize(ev) { this.camera.aspect = window.innerWidth / window.innerHeight; this.camera.updateProjectionMatrix(); this.renderer.setSize(window.innerWidth, window.innerHeight); if (this.currentImageIndex > -1) { this.recalculateUniforms(ev); this.render(); } } } var activations = { corners: ` float getActivation(vec2 uv){ float top = (1.-uv.y); float right = uv.x; float bottom = uv.y; float left = 1.- uv.x; return top *0.333333 + (right * 0.333333 + (right * bottom)*0.666666 ); } `, topLeft: ` float getActivation(vec2 uv){ return (+uv.x-uv.y+1.)/2.; } `, sides: ` float getActivation(vec2 uv){ return min(uv.x, 1.-uv.x) * 2.; } `, left: ` float getActivation(vec2 uv){ return uv.x; } `, top: ` float getActivation(vec2 uv){ return 1. - uv.y; } `, bottom: ` float getActivation(vec2 uv){ return uv.y; } `, bottomStep: ` float getActivation(vec2 uv){ return uv.y; } `, sinX: ` float getActivation(vec2 uv){ return sin(uv.x * 3.14); } `, center: ` float getActivation(vec2 uv){ float maxDistance = distance(vec2(0.),vec2(0.5)); float dist = distance(vec2(0.), uv-0.5); return smoothstep(0.,maxDistance,dist); } `, mouse: ` float getActivation(vec2 uv){ float maxDistance = distance(uMouse, 1.-floor(uMouse+0.5)); float dist = smoothstep(0.,maxDistance,distance(uMouse,uv)); return dist; } `, closestCorner: ` float getActivation(vec2 uv){ float y = mod(uClosestCorner,2.) *2. -1.; float x = (floor(uClosestCorner /2.)*2.-1.)*-1.; float xAct = abs(min(0.,x)) + uv.x * x; float yAct = abs(min(0.,y)) + uv.y * y; return (xAct+yAct)/2.; } `, closestSide: ` float getActivation(vec2 uv){ float y = mod(uClosestCorner,2.) *2. -1.; float x = (floor(uClosestCorner /2.)*2.-1.)*-1.; float xAct = abs(min(0.,x)) + uv.x * x; float yAct = abs(min(0.,y)) + uv.y * y; return (xAct+yAct)/2.; } ` }; function ensureFloat(num) { let stringed = num.toString(); const dotIndex = stringed.indexOf("."); if (dotIndex === -1) { stringed += "."; } return stringed; } const transformations = { none: () => null, flipX: () => { // Only works with sync ending return ` float beizerProgress = cubicBezier(vertexProgress, uBeizerControls.x,uBeizerControls.y, uBeizerControls.z,uBeizerControls.w); float flippedX = -transformedPos.x; transformedPos.x = mix (transformedPos.x, flippedX,beizerProgress ); // Flip texture on flipped sections // float activationAtX0 = getActivation(vec2(0.,transformedUV.y)); // float activationAtX1 = getActivation(vec2(1.,transformedUV.y)); // float syncDifference = // activationAtX1 * uSyncLatestStart - activationAtX0 * uSyncLatestStart; float syncDifference = uSyncLatestStart; // Flip the controls because who knows why // But it works exactly // Multiply by aspect ratio to account for mesh scaling float aspectRatio = (uMeshScale.x / uMeshScale.y); float stepFormula = 0.5 - (syncDifference * uSyncLatestStart * uSyncLatestStart) * aspectRatio; transformedUV.x = mix(transformedUV.x,1.-transformedUV.x, step(stepFormula,beizerProgress)); `; }, simplex: props => { let seed = ensureFloat(props.seed || 0); let amplitudeX = ensureFloat(props.amplitudeX || 0.5); let amplitudeY = ensureFloat(props.amplitudeY || 0.5); let frequencyX = ensureFloat(props.frequencyX || 1); let frequencyY = ensureFloat(props.frequencyY || 0.75); let progressLimit = ensureFloat(props.progressLimit || 0.5); return ` float simplexProgress = min(clamp((vertexProgress) / ${progressLimit},0.,1.),clamp((1.-vertexProgress) / (1.-${progressLimit}),0.,1.)); simplexProgress = smoothstep(0.,1.,simplexProgress); float noiseX = snoise(vec2(transformedPos.x +uSeed, transformedPos.y + uSeed + simplexProgress * 1.) * ${frequencyX} ) ; float noiseY = snoise(vec2(transformedPos.y +uSeed, transformedPos.x + uSeed + simplexProgress * 1.) * ${frequencyY}) ; transformedPos.x += ${amplitudeX} * noiseX * simplexProgress; transformedPos.y += ${amplitudeY} * noiseY * simplexProgress; `; }, wavy: props => { const seed = ensureFloat(props.seed || 0); const amplitude = ensureFloat(props.amplitude || 0.5); const frequency = ensureFloat(props.frequency || 4); return ` float limit = 0.5; float wavyProgress = min(clamp((vertexProgress) / limit,0.,1.),clamp((1.-vertexProgress) / (1.-limit),0.,1.)); float dist = length(transformedPos.xy); float angle = atan(transformedPos.x,transformedPos.y); float nextDist = dist * (${amplitude} * (sin(angle * ${frequency} + ${seed}) /2.+0.5)+ 1.); transformedPos.x = mix(transformedPos.x,sin(angle) * nextDist , wavyProgress); transformedPos.y = mix(transformedPos.y,cos(angle) * nextDist, wavyProgress); `; }, circle: props => { return ` float limit = 0.5; float circleProgress = min(clamp((vertexProgress) / limit,0.,1.),clamp((1.-vertexProgress) / (1.-limit),0.,1.)); float maxDistance = 0.5; float dist = length(transformedPos.xy); float nextDist = min(maxDistance,dist); float overload = step(maxDistance,dist); float angle = atan(transformedPos.x,transformedPos.y); transformedPos.x = mix(transformedPos.x,sin(angle) * nextDist , circleProgress ); transformedPos.y = mix(transformedPos.y,cos(angle) * nextDist, circleProgress); transformedPos.z += -0.5 * overload * circleProgress; `; } }; var vertexUniforms = ` uniform float uProgress; uniform vec2 uScaleToViewSize; uniform vec2 uPlaneCenter; uniform vec2 uMeshScale; uniform vec2 uMouse; uniform vec2 uViewSize; uniform float uClosestCorner; // Option Uniforms uniform float uSeed; uniform vec4 uBeizerControls; uniform float uSyncLatestStart; uniform float uActivationParts; uniform bool uProgressByParts; varying vec2 vUv; varying vec2 scale; varying float vProgress; `; function generateShaders(activation, transform) { var vertex = ` ${vertexUniforms} ${cubicBeizer} ${simplex} ${quadraticBezier} ${activation} float linearStep(float edge0, float edge1, float val) { float x = clamp( (val - edge0) / (edge1 - edge0),0.,1.); return x; } void main(){ vec3 pos = position.xyz; vec2 newUV = uv; float activation = getActivation(uv); // Everything ends at the same time float startAt = activation * uSyncLatestStart; float vertexProgress = smoothstep(startAt,1.,uProgress); if(uProgressByParts){ // Vertex end by parts float activationPart = 1./uActivationParts; float activationPartDuration = 1./(uActivationParts+1.); float progressStart = (activation / activationPart) * activationPartDuration; float progressEnd = min(progressStart + activationPartDuration,1.); vertexProgress = linearStep(progressStart,progressEnd,uProgress); } vec3 transformedPos = pos; vec2 transformedUV = uv; ${transform ? transform : ""} pos = transformedPos; newUV = transformedUV; // Scale // uScaleToViewSize scale = vec2( 1. + uScaleToViewSize * vertexProgress ); // Since we are moving the mesh not the geometry the geometry is in the center vec2 flippedPos = vec2( (- pos.x) , (- pos.y ) ); pos.xy *= scale; // Move to center // Mesh moves it into position. Shader moves it to the center pos.y += -uPlaneCenter.y * vertexProgress; pos.x += -uPlaneCenter.x * vertexProgress; // Move slightly to the front pos.z += vertexProgress; gl_Position = projectionMatrix * modelViewMatrix * vec4(pos,1.); vProgress = vertexProgress; vUv = newUV; } `; var fragment = ` uniform float uProgress; uniform sampler2D uImage; uniform vec2 uImageRes; uniform sampler2D uImageLarge; uniform vec2 uImageLargeRes; uniform vec2 uMeshScale; varying vec2 vUv; varying float vProgress; varying vec2 scale; vec2 preserveAspectRatioSlice(vec2 uv, vec2 planeSize, vec2 imageSize ){ vec2 ratio = vec2( min((planeSize.x / planeSize.y) / (imageSize.x / imageSize.y), 1.0), min((planeSize.y / planeSize.x) / (imageSize.y / imageSize.x), 1.0) ); vec2 sliceUvs = vec2( uv.x * ratio.x + (1.0 - ratio.x) * 0.5, uv.y * ratio.y + (1.0 - ratio.y) * 0.5 ); return sliceUvs; } void main(){ vec2 uv = vUv; vec2 scaledPlane = uMeshScale * scale; vec2 smallImageUV = preserveAspectRatioSlice(uv, scaledPlane, uImageRes); vec3 color = texture2D(uImage,smallImageUV).xyz; if(vProgress > 0.){ vec2 largeImageUV = preserveAspectRatioSlice(uv, scaledPlane, uImageLargeRes); color = mix(color,texture2D(uImageLarge,largeImageUV).xyz, vProgress ); } gl_FragColor = vec4(color,1.); } `; return { fragment, vertex }; } var cubicBeizer = ` // Helper functions: float slopeFromT (float t, float A, float B, float C){ float dtdx = 1.0/(3.0*A*t*t + 2.0*B*t + C); return dtdx; } float xFromT (float t, float A, float B, float C, float D){ float x = A*(t*t*t) + B*(t*t) + C*t + D; return x; } float yFromT (float t, float E, float F, float G, float H){ float y = E*(t*t*t) + F*(t*t) + G*t + H; return y; } float cubicBezier (float x, float a, float b, float c, float d){ float y0a = 0.00; // initial y float x0a = 0.00; // initial x float y1a = b; // 1st influence y float x1a = a; // 1st influence x float y2a = d; // 2nd influence y float x2a = c; // 2nd influence x float y3a = 1.00; // final y float x3a = 1.00; // final x float A = x3a - 3.*x2a + 3.*x1a - x0a; float B = 3.*x2a - 6.*x1a + 3.*x0a; float C = 3.*x1a - 3.*x0a; float D = x0a; float E = y3a - 3.*y2a + 3.*y1a - y0a; float F = 3.*y2a - 6.*y1a + 3.*y0a; float G = 3.*y1a - 3.*y0a; float H = y0a; // Solve for t given x (using Newton-Raphelson), then solve for y given t. // Assume for the first guess that t = x. float currentt = x; const int nRefinementIterations = 5; for (int i=0; i < nRefinementIterations; i++){ float currentx = xFromT (currentt, A,B,C,D); float currentslope = slopeFromT (currentt, A,B,C); currentt -= (currentx - x)*(currentslope); currentt = clamp(currentt, 0.,1.); } float y = yFromT (currentt, E,F,G,H); return y; } `; var simplex = ` vec3 permute(vec3 x) { return mod(((x*34.0)+1.0)*x, 289.0); } float snoise(vec2 v){ const vec4 C = vec4(0.211324865405187, 0.366025403784439, -0.577350269189626, 0.024390243902439); vec2 i = floor(v + dot(v, C.yy) ); vec2 x0 = v - i + dot(i, C.xx); vec2 i1; i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0); vec4 x12 = x0.xyxy + C.xxzz; x12.xy -= i1; i = mod(i, 289.0); vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 )) + i.x + vec3(0.0, i1.x, 1.0 )); vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0); m = m*m ; m = m*m ; vec3 x = 2.0 * fract(p * C.www) - 1.0; vec3 h = abs(x) - 0.5; vec3 ox = floor(x + 0.5); vec3 a0 = x - ox; m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h ); vec3 g; g.x = a0.x * x0.x + h.x * x0.y; g.yz = a0.yz * x12.xz + h.yz * x12.yw; return 130.0 * dot(m, g); } `; var quadraticBezier = ` float quadraticBezier (float x, float a, float b){ // adapted from BEZMATH.PS (1993) // by Don Lancaster, SYNERGETICS Inc. // http://www.tinaja.com/text/bezmath.html float epsilon = 0.00001; a = max(0., min(1., a)); b = max(0., min(1., b)); if (a == 0.5){ a += epsilon; } // solve t from x (an inverse operation) float om2a = 1. - 2.*a; float t = (sqrt(a*a + om2a*x) - a)/om2a; float y = (1.-2.*b)*(t*t) + (2.*b)*t; return y; } `;