Created
March 19, 2026 20:00
-
-
Save krhoyt/4d7a3d44275bee9f7f853e30488306d3 to your computer and use it in GitHub Desktop.
WebGL Transitions Carousel
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
| import { WebGLTransitionFactory } from './WebGLTransitionFactory.js'; | |
| import { transitionLibrary } from './transition-library.js'; | |
| const IMAGE_URLS = [ | |
| 'https://picsum.photos/id/1015/1200/800', | |
| 'https://picsum.photos/id/1025/1200/800', | |
| 'https://picsum.photos/id/1035/1200/800', | |
| 'https://picsum.photos/id/1043/1200/800' | |
| ]; | |
| // Time the shader animation itself runs. | |
| const TRANSITION_DURATION = 2000; | |
| // Time to wait after one transition completes before starting the next. | |
| const TRANSITION_DELAY = 3000; | |
| function modulo(index, length) { | |
| return ((index % length) + length) % length; | |
| } | |
| async function preloadImages(factory, urls) { | |
| const uniqueUrls = [...new Set(urls)]; | |
| const loadedPairs = await Promise.all( | |
| uniqueUrls.map(async (url) => [url, await factory.loadImage(url)]) | |
| ); | |
| return new Map(loadedPairs); | |
| } | |
| async function main() { | |
| const canvas = document.getElementById('glcanvas'); | |
| const controls = document.getElementById('controls'); | |
| const durationInput = document.getElementById('duration-input'); | |
| const playButton = document.getElementById('play-button'); | |
| const transitions = new WebGLTransitionFactory(canvas, { | |
| duration: TRANSITION_DURATION, | |
| bgcolor: [0.0, 0.0, 0.0, 1.0] | |
| }); | |
| transitions.registerMany(transitionLibrary); | |
| const availableTransitionNames = transitionLibrary.map((transition) => transition.name); | |
| if (IMAGE_URLS.length === 0) { | |
| throw new Error('IMAGE_URLS must contain at least one image URL.'); | |
| } | |
| if (availableTransitionNames.length === 0) { | |
| throw new Error('transitionLibrary must contain at least one transition.'); | |
| } | |
| const imageCache = await preloadImages(transitions, IMAGE_URLS); | |
| let currentImageIndex = 0; | |
| let currentTransitionIndex = 0; | |
| let carouselTimeoutId = null; | |
| let isCarouselRunning = true; | |
| async function showImagePair(fromIndex, toIndex) { | |
| const normalizedFromIndex = modulo(fromIndex, IMAGE_URLS.length); | |
| const normalizedToIndex = modulo(toIndex, IMAGE_URLS.length); | |
| const fromUrl = IMAGE_URLS[normalizedFromIndex]; | |
| const toUrl = IMAGE_URLS[normalizedToIndex]; | |
| const fromImage = imageCache.get(fromUrl); | |
| const toImage = imageCache.get(toUrl); | |
| if (!fromImage || !toImage) { | |
| throw new Error('One or more carousel images failed to preload.'); | |
| } | |
| transitions.setTextures(fromImage, toImage); | |
| } | |
| function applyCurrentTransition() { | |
| const transitionName = | |
| availableTransitionNames[modulo(currentTransitionIndex, availableTransitionNames.length)]; | |
| transitions.setTransition(transitionName); | |
| } | |
| async function advanceCarousel() { | |
| if (!isCarouselRunning) return; | |
| const nextImageIndex = modulo(currentImageIndex + 1, IMAGE_URLS.length); | |
| await showImagePair(currentImageIndex, nextImageIndex); | |
| applyCurrentTransition(); | |
| transitions.playOnce(); | |
| currentImageIndex = nextImageIndex; | |
| currentTransitionIndex = modulo( | |
| currentTransitionIndex + 1, | |
| availableTransitionNames.length | |
| ); | |
| carouselTimeoutId = window.setTimeout(() => { | |
| advanceCarousel().catch((err) => { | |
| console.error(err); | |
| }); | |
| }, transitions.runtimeState.duration + TRANSITION_DELAY); | |
| } | |
| function stopCarousel() { | |
| isCarouselRunning = false; | |
| if (carouselTimeoutId !== null) { | |
| window.clearTimeout(carouselTimeoutId); | |
| carouselTimeoutId = null; | |
| } | |
| } | |
| function startCarousel() { | |
| stopCarousel(); | |
| isCarouselRunning = true; | |
| advanceCarousel().catch((err) => { | |
| console.error(err); | |
| }); | |
| } | |
| // Initial state: show the first image transitioning to the second. | |
| await showImagePair(0, IMAGE_URLS.length > 1 ? 1 : 0); | |
| applyCurrentTransition(); | |
| transitions.mountButtons(controls); | |
| transitions.start(); | |
| transitions.playOnce(); | |
| durationInput.value = String(TRANSITION_DURATION); | |
| durationInput.addEventListener('change', () => { | |
| transitions.setDuration(durationInput.value); | |
| startCarousel(); | |
| }); | |
| durationInput.addEventListener('keydown', (event) => { | |
| if (event.key === 'Enter') { | |
| transitions.setDuration(durationInput.value); | |
| startCarousel(); | |
| } | |
| }); | |
| playButton.addEventListener('click', () => { | |
| transitions.setDuration(durationInput.value); | |
| startCarousel(); | |
| }); | |
| window.addEventListener('beforeunload', () => { | |
| stopCarousel(); | |
| transitions.stop(); | |
| }); | |
| // Start the automated carousel loop after the initial transition. | |
| carouselTimeoutId = window.setTimeout(() => { | |
| advanceCarousel().catch((err) => { | |
| console.error(err); | |
| }); | |
| }, transitions.runtimeState.duration + TRANSITION_DELAY); | |
| window.transitions = transitions; | |
| window.startCarousel = startCarousel; | |
| window.stopCarousel = stopCarousel; | |
| // Optional convenience helpers | |
| window.setAngularSpeed = (value) => { | |
| transitions.setTransitionParams('angular', { | |
| speed: Number(value) | |
| }); | |
| }; | |
| window.setCrossZoomIntensity = (value) => { | |
| transitions.setTransitionParams('cross-zoom', { | |
| intensity: Number(value) | |
| }); | |
| }; | |
| window.setDirection = (x, y) => { | |
| transitions.setTransitionParams('slide-wrap', { | |
| direction: [Number(x), Number(y)] | |
| }); | |
| }; | |
| window.setStripeCount = (value) => { | |
| transitions.setTransitionParams('vertical-stripes', { | |
| count: Number(value) | |
| }); | |
| }; | |
| window.setStripeSmoothness = (value) => { | |
| transitions.setTransitionParams('vertical-stripes', { | |
| smoothness: Number(value) | |
| }); | |
| }; | |
| window.setGridSize = (x, y) => { | |
| transitions.setTransitionParams('random-squares', { | |
| size: [Number(x), Number(y)] | |
| }); | |
| }; | |
| window.setDots = (value) => { | |
| transitions.setTransitionParams('dot-reveal', { | |
| dots: Number(value) | |
| }); | |
| }; | |
| window.setCenter = (x, y) => { | |
| transitions.setTransitionParams('dot-reveal', { | |
| center: [Number(x), Number(y)] | |
| }); | |
| }; | |
| window.setDoorway = (partial) => { | |
| transitions.setTransitionParams('doorway', partial); | |
| }; | |
| window.setHexSteps = (value) => { | |
| transitions.setTransitionParams('hexagonalize', { | |
| steps: Number(value) | |
| }); | |
| }; | |
| window.setHorizontalHexagons = (value) => { | |
| transitions.setTransitionParams('hexagonalize', { | |
| horizontalHexagons: Number(value) | |
| }); | |
| }; | |
| window.setPageCurl = (partial) => { | |
| transitions.setTransitionParams('page-curl', partial); | |
| }; | |
| window.setRadialStartingAngle = (value) => { | |
| transitions.setTransitionParams('radial', { | |
| startingAngle: Number(value) | |
| }); | |
| }; | |
| window.setSwap = (partial) => { | |
| transitions.setTransitionParams('swap', partial); | |
| }; | |
| window.setSquaresWire = (partial) => { | |
| transitions.setTransitionParams('squares-wire', partial); | |
| }; | |
| } | |
| main().catch((err) => { | |
| console.error(err); | |
| document.body.innerHTML = | |
| '<pre style="color:white;padding:16px;">' + err.message + '</pre>'; | |
| }); |
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" /> | |
| <title>WebGL Transition Library</title> | |
| <style> | |
| html, body { | |
| margin: 0; | |
| height: 100%; | |
| background: #111; | |
| overflow: hidden; | |
| font-family: sans-serif; | |
| } | |
| canvas { | |
| display: block; | |
| width: 100vw; | |
| height: 100vh; | |
| } | |
| .controls { | |
| position: fixed; | |
| top: 12px; | |
| left: 12px; | |
| z-index: 10; | |
| display: flex; | |
| gap: 8px; | |
| padding: 8px; | |
| border-radius: 8px; | |
| background: rgba(0, 0, 0, 0.45); | |
| color: white; | |
| flex-wrap: wrap; | |
| align-items: center; | |
| max-width: calc(100vw - 24px); | |
| } | |
| button, | |
| input { | |
| font: 14px/1.2 sans-serif; | |
| } | |
| label { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| white-space: nowrap; | |
| } | |
| input[type="number"] { | |
| width: 90px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="controls" id="controls"> | |
| <label> | |
| Duration (ms) | |
| <input id="duration-input" type="number" min="1" step="100" value="2000" /> | |
| </label> | |
| <button id="play-button" type="button">Play</button> | |
| </div> | |
| <canvas id="glcanvas"></canvas> | |
| <script type="module" src="./app.js"></script> | |
| </body> | |
| </html> |
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
| export const transitionLibrary = [ | |
| { | |
| name: 'angular', | |
| label: 'Angular', | |
| defaults: { | |
| speed: 2.0 | |
| }, | |
| uniforms: { | |
| speed: { type: '1f', getValue: (params) => params.speed } | |
| }, | |
| glsl: ` | |
| vec4 transition(vec2 uv) { | |
| vec2 p = uv; | |
| float circPos = atan(p.y - 0.5, p.x - 0.5) + progress * speed; | |
| float modPos = mod(circPos, 3.1415 / 4.0); | |
| float signedValue = sign(progress - modPos); | |
| return mix( | |
| getToColor(p), | |
| getFromColor(p), | |
| step(signedValue, 0.5) | |
| ); | |
| } | |
| ` | |
| }, | |
| { | |
| name: 'crossfade', | |
| label: 'Crossfade', | |
| defaults: {}, | |
| uniforms: {}, | |
| glsl: ` | |
| vec4 transition(vec2 p) { | |
| return mix(getFromColor(p), getToColor(p), progress); | |
| } | |
| ` | |
| }, | |
| { | |
| name: 'cross-zoom', | |
| label: 'Cross Zoom', | |
| defaults: { | |
| intensity: 0.1 | |
| }, | |
| uniforms: { | |
| intensity: { type: '1f', getValue: (params) => params.intensity } | |
| }, | |
| glsl: ` | |
| const int passes = 6; | |
| vec4 transition(vec2 uv) { | |
| vec4 c1 = vec4(0.0); | |
| vec4 c2 = vec4(0.0); | |
| float disp = intensity * (0.5 - distance(0.5, progress)); | |
| for (int xi = 0; xi < passes; xi++) { | |
| float x = float(xi) / float(passes) - 0.5; | |
| for (int yi = 0; yi < passes; yi++) { | |
| float y = float(yi) / float(passes) - 0.5; | |
| vec2 v = vec2(x, y); | |
| float d = disp; | |
| c1 += getFromColor(uv + d * v); | |
| c2 += getToColor(uv + d * v); | |
| } | |
| } | |
| c1 /= float(passes * passes); | |
| c2 /= float(passes * passes); | |
| return mix(c1, c2, progress); | |
| } | |
| ` | |
| }, | |
| { | |
| name: 'doorway', | |
| label: 'Doorway', | |
| defaults: { | |
| reflection: 0.4, | |
| perspective: 0.4, | |
| depth: 3.0 | |
| }, | |
| uniforms: { | |
| reflection: { type: '1f', getValue: (params) => params.reflection }, | |
| perspective: { type: '1f', getValue: (params) => params.perspective }, | |
| depth: { type: '1f', getValue: (params) => params.depth } | |
| }, | |
| glsl: ` | |
| const vec4 black = vec4(0.0, 0.0, 0.0, 1.0); | |
| const vec2 boundMin = vec2(0.0, 0.0); | |
| const vec2 boundMax = vec2(1.0, 1.0); | |
| bool inBounds(vec2 p) { | |
| return all(lessThan(boundMin, p)) && all(lessThan(p, boundMax)); | |
| } | |
| vec2 project(vec2 p) { | |
| return p * vec2(1.0, -1.2) + vec2(0.0, -0.02); | |
| } | |
| vec4 bgColor(vec2 p, vec2 pto) { | |
| vec4 c = black; | |
| pto = project(pto); | |
| if (inBounds(pto)) { | |
| c += mix( | |
| black, | |
| getToColor(pto), | |
| reflection * mix(1.0, 0.0, pto.y) | |
| ); | |
| } | |
| return c; | |
| } | |
| vec4 transition(vec2 p) { | |
| vec2 pfr = vec2(-1.0); | |
| vec2 pto = vec2(-1.0); | |
| float middleSlit = 2.0 * abs(p.x - 0.5) - progress; | |
| if (middleSlit > 0.0) { | |
| pfr = p + (p.x > 0.5 ? -1.0 : 1.0) * vec2(0.5 * progress, 0.0); | |
| float d = 1.0 / (1.0 + perspective * progress * (1.0 - middleSlit)); | |
| pfr.y -= d / 2.0; | |
| pfr.y *= d; | |
| pfr.y += d / 2.0; | |
| } | |
| float size = mix(1.0, depth, 1.0 - progress); | |
| pto = (p - vec2(0.5)) * vec2(size) + vec2(0.5); | |
| if (inBounds(pfr)) { | |
| return getFromColor(pfr); | |
| } else if (inBounds(pto)) { | |
| return getToColor(pto); | |
| } else { | |
| return bgColor(p, pto); | |
| } | |
| } | |
| ` | |
| }, | |
| { | |
| name: 'slide-wrap', | |
| label: 'Slide Wrap', | |
| defaults: { | |
| direction: [0.0, 1.0] | |
| }, | |
| uniforms: { | |
| direction: { type: '2f', getValue: (params) => params.direction } | |
| }, | |
| glsl: ` | |
| vec4 transition(vec2 p) { | |
| vec2 shifted = p + progress * sign(direction); | |
| vec2 wrapped = fract(shifted); | |
| float inBounds = | |
| step(0.0, shifted.y) * | |
| step(shifted.y, 1.0) * | |
| step(0.0, shifted.x) * | |
| step(shifted.x, 1.0); | |
| return mix( | |
| getToColor(wrapped), | |
| getFromColor(wrapped), | |
| inBounds | |
| ); | |
| } | |
| ` | |
| }, | |
| { | |
| name: 'swap', | |
| label: 'Swap', | |
| defaults: { | |
| reflection: 0.4, | |
| perspective: 0.2, | |
| depth: 3.0 | |
| }, | |
| uniforms: { | |
| reflection: { type: '1f', getValue: (params) => params.reflection }, | |
| perspective: { type: '1f', getValue: (params) => params.perspective }, | |
| depth: { type: '1f', getValue: (params) => params.depth } | |
| }, | |
| glsl: ` | |
| const vec4 black = vec4(0.0, 0.0, 0.0, 1.0); | |
| const vec2 boundMin = vec2(0.0, 0.0); | |
| const vec2 boundMax = vec2(1.0, 1.0); | |
| bool inBounds(vec2 p) { | |
| return all(lessThan(boundMin, p)) && all(lessThan(p, boundMax)); | |
| } | |
| vec2 project(vec2 p) { | |
| return p * vec2(1.0, -1.2) + vec2(0.0, -0.02); | |
| } | |
| vec4 bgColor(vec2 p, vec2 pfr, vec2 pto) { | |
| vec4 c = black; | |
| pfr = project(pfr); | |
| if (inBounds(pfr)) { | |
| c += mix( | |
| black, | |
| getFromColor(pfr), | |
| reflection * mix(1.0, 0.0, pfr.y) | |
| ); | |
| } | |
| pto = project(pto); | |
| if (inBounds(pto)) { | |
| c += mix( | |
| black, | |
| getToColor(pto), | |
| reflection * mix(1.0, 0.0, pto.y) | |
| ); | |
| } | |
| return c; | |
| } | |
| vec4 transition(vec2 p) { | |
| vec2 pfr = vec2(-1.0); | |
| vec2 pto = vec2(-1.0); | |
| float size = mix(1.0, depth, progress); | |
| float persp = perspective * progress; | |
| pfr = (p + vec2(0.0, -0.5)) * | |
| vec2( | |
| size / (1.0 - perspective * progress), | |
| size / (1.0 - size * persp * p.x) | |
| ) + vec2(0.0, 0.5); | |
| size = mix(1.0, depth, 1.0 - progress); | |
| persp = perspective * (1.0 - progress); | |
| pto = (p + vec2(-1.0, -0.5)) * | |
| vec2( | |
| size / (1.0 - perspective * (1.0 - progress)), | |
| size / (1.0 - size * persp * (0.5 - p.x)) | |
| ) + vec2(1.0, 0.5); | |
| if (progress < 0.5) { | |
| if (inBounds(pfr)) { | |
| return getFromColor(pfr); | |
| } | |
| if (inBounds(pto)) { | |
| return getToColor(pto); | |
| } | |
| } | |
| if (inBounds(pto)) { | |
| return getToColor(pto); | |
| } | |
| if (inBounds(pfr)) { | |
| return getFromColor(pfr); | |
| } | |
| return bgColor(p, pfr, pto); | |
| } | |
| ` | |
| }, | |
| { | |
| name: 'squares-wire', | |
| label: 'Squares Wire', | |
| defaults: { | |
| squares: [10.0, 10.0], | |
| direction: [1.0, -0.5], | |
| smoothness: 1.6 | |
| }, | |
| uniforms: { | |
| squares: { type: '2f', getValue: (params) => params.squares }, | |
| direction: { type: '2f', getValue: (params) => params.direction }, | |
| smoothness: { type: '1f', getValue: (params) => params.smoothness } | |
| }, | |
| glsl: ` | |
| const vec2 center = vec2(0.5, 0.5); | |
| vec4 transition(vec2 p) { | |
| vec2 v = normalize(direction); | |
| v /= abs(v.x) + abs(v.y); | |
| float d = v.x * center.x + v.y * center.y; | |
| float offset = smoothness; | |
| float pr = smoothstep( | |
| -offset, | |
| 0.0, | |
| v.x * p.x + v.y * p.y - (d - 0.5 + progress * (1.0 + offset)) | |
| ); | |
| vec2 squarep = fract(p * squares); | |
| vec2 squaremin = vec2(pr / 2.0); | |
| vec2 squaremax = vec2(1.0 - pr / 2.0); | |
| float a = | |
| (1.0 - step(progress, 0.0)) * | |
| step(squaremin.x, squarep.x) * | |
| step(squaremin.y, squarep.y) * | |
| step(squarep.x, squaremax.x) * | |
| step(squarep.y, squaremax.y); | |
| return mix(getFromColor(p), getToColor(p), a); | |
| } | |
| ` | |
| }, | |
| { | |
| name: 'vertical-stripes', | |
| label: 'Vertical Stripes', | |
| defaults: { | |
| count: 10.0, | |
| smoothness: 0.5 | |
| }, | |
| uniforms: { | |
| count: { type: '1f', getValue: (params) => params.count }, | |
| smoothness: { type: '1f', getValue: (params) => params.smoothness } | |
| }, | |
| glsl: ` | |
| vec4 transition(vec2 p) { | |
| float pr = smoothstep( | |
| -smoothness, | |
| 0.0, | |
| p.x - progress * (1.0 + smoothness) | |
| ); | |
| float s = step(pr, fract(count * p.x)); | |
| return mix(getFromColor(p), getToColor(p), s); | |
| } | |
| ` | |
| }, | |
| { | |
| name: 'radial', | |
| label: 'Radial', | |
| defaults: { | |
| startingAngle: 90.0 | |
| }, | |
| uniforms: { | |
| startingAngle: { type: '1f', getValue: (params) => params.startingAngle } | |
| }, | |
| glsl: ` | |
| #define PI 3.141592653589 | |
| vec4 transition(vec2 uv) { | |
| float offset = startingAngle * PI / 180.0; | |
| float angle = atan(uv.y - 0.5, uv.x - 0.5) + offset; | |
| float normalizedAngle = (angle + PI) / (2.0 * PI); | |
| normalizedAngle = normalizedAngle - floor(normalizedAngle); | |
| return mix( | |
| getFromColor(uv), | |
| getToColor(uv), | |
| step(normalizedAngle, progress) | |
| ); | |
| } | |
| ` | |
| }, | |
| { | |
| name: 'random-squares', | |
| label: 'Random Squares', | |
| defaults: { | |
| size: [10.0, 10.0], | |
| smoothness: 0.5 | |
| }, | |
| uniforms: { | |
| size: { type: '2f', getValue: (params) => params.size }, | |
| smoothness: { type: '1f', getValue: (params) => params.smoothness } | |
| }, | |
| glsl: ` | |
| float rand(vec2 co) { | |
| return fract(sin(dot(co.xy, vec2(12.9898, 78.233))) * 43758.5453); | |
| } | |
| vec4 transition(vec2 p) { | |
| float r = rand(floor(size * p)); | |
| float m = smoothstep( | |
| 0.0, | |
| -smoothness, | |
| r - (progress * (1.0 + smoothness)) | |
| ); | |
| return mix(getFromColor(p), getToColor(p), m); | |
| } | |
| ` | |
| }, | |
| { | |
| name: 'dot-reveal', | |
| label: 'Dot Reveal', | |
| defaults: { | |
| dots: 20.0, | |
| center: [0.0, 0.0] | |
| }, | |
| uniforms: { | |
| dots: { type: '1f', getValue: (params) => params.dots }, | |
| center: { type: '2f', getValue: (params) => params.center } | |
| }, | |
| glsl: ` | |
| vec4 transition(vec2 p) { | |
| float d = max(distance(p, center), 0.0001); | |
| bool nextImage = | |
| distance(fract(p * dots), vec2(0.5, 0.5)) < | |
| (progress / d); | |
| return nextImage ? getToColor(p) : getFromColor(p); | |
| } | |
| ` | |
| }, | |
| { | |
| name: 'hexagonalize', | |
| label: 'Hexagonalize', | |
| defaults: { | |
| steps: 50.0, | |
| horizontalHexagons: 20.0 | |
| }, | |
| uniforms: { | |
| steps: { type: '1f', getValue: (params) => params.steps }, | |
| horizontalHexagons: { type: '1f', getValue: (params) => params.horizontalHexagons } | |
| }, | |
| glsl: ` | |
| struct Hexagon { | |
| float q; | |
| float r; | |
| float s; | |
| }; | |
| Hexagon createHexagon(float q, float r) { | |
| Hexagon hex; | |
| hex.q = q; | |
| hex.r = r; | |
| hex.s = -q - r; | |
| return hex; | |
| } | |
| Hexagon roundHexagon(Hexagon hex) { | |
| float q = floor(hex.q + 0.5); | |
| float r = floor(hex.r + 0.5); | |
| float s = floor(hex.s + 0.5); | |
| float deltaQ = abs(q - hex.q); | |
| float deltaR = abs(r - hex.r); | |
| float deltaS = abs(s - hex.s); | |
| if (deltaQ > deltaR && deltaQ > deltaS) { | |
| q = -r - s; | |
| } else if (deltaR > deltaS) { | |
| r = -q - s; | |
| } else { | |
| s = -q - r; | |
| } | |
| return createHexagon(q, r); | |
| } | |
| Hexagon hexagonFromPoint(vec2 point, float size) { | |
| point.y /= ratio; | |
| point = (point - 0.5) / size; | |
| float q = (sqrt(3.0) / 3.0) * point.x + (-1.0 / 3.0) * point.y; | |
| float r = 2.0 / 3.0 * point.y; | |
| Hexagon hex = createHexagon(q, r); | |
| return roundHexagon(hex); | |
| } | |
| vec2 pointFromHexagon(Hexagon hex, float size) { | |
| float x = (sqrt(3.0) * hex.q + (sqrt(3.0) / 2.0) * hex.r) * size + 0.5; | |
| float y = ((3.0 / 2.0) * hex.r) * size + 0.5; | |
| return vec2(x, y * ratio); | |
| } | |
| vec4 transition(vec2 p) { | |
| float dist = 2.0 * min(progress, 1.0 - progress); | |
| if (steps > 0.0) { | |
| dist = ceil(dist * steps) / steps; | |
| } | |
| float size = (sqrt(3.0) / 3.0) * dist / horizontalHexagons; | |
| vec2 point = dist > 0.0 | |
| ? pointFromHexagon(hexagonFromPoint(p, size), size) | |
| : p; | |
| return mix(getFromColor(point), getToColor(point), progress); | |
| } | |
| ` | |
| }, | |
| { | |
| name: 'cylinder-curl', | |
| label: 'Cylinder Curl', | |
| defaults: {}, | |
| uniforms: {}, | |
| glsl: ` | |
| const float MIN_AMOUNT = -0.16; | |
| const float MAX_AMOUNT = 1.5; | |
| const float PI = 3.141592653589793; | |
| const float scale = 512.0; | |
| const float sharpness = 3.0; | |
| const float angle = 100.0 * PI / 180.0; | |
| const float cylinderRadius = 1.0 / PI / 2.0; | |
| vec3 hitPoint(float hitAngle, float yc, vec3 point, mat3 rrotation) { | |
| float hitPointValue = hitAngle / (2.0 * PI); | |
| point.y = hitPointValue; | |
| return rrotation * point; | |
| } | |
| vec4 antiAlias(vec4 color1, vec4 color2, float distanc) { | |
| distanc *= scale; | |
| if (distanc < 0.0) return color2; | |
| if (distanc > 2.0) return color1; | |
| float dd = pow(1.0 - distanc / 2.0, sharpness); | |
| return ((color2 - color1) * dd) + color1; | |
| } | |
| float distanceToEdge(vec3 point) { | |
| float dx = abs(point.x > 0.5 ? 1.0 - point.x : point.x); | |
| float dy = abs(point.y > 0.5 ? 1.0 - point.y : point.y); | |
| if (point.x < 0.0) dx = -point.x; | |
| if (point.x > 1.0) dx = point.x - 1.0; | |
| if (point.y < 0.0) dy = -point.y; | |
| if (point.y > 1.0) dy = point.y - 1.0; | |
| if ((point.x < 0.0 || point.x > 1.0) && (point.y < 0.0 || point.y > 1.0)) { | |
| return sqrt(dx * dx + dy * dy); | |
| } | |
| return min(dx, dy); | |
| } | |
| vec4 seeThrough( | |
| float yc, | |
| vec2 p, | |
| mat3 rotation, | |
| mat3 rrotation, | |
| float cylinderAngle | |
| ) { | |
| float hitAngle = PI - (acos(yc / cylinderRadius) - cylinderAngle); | |
| vec3 point = hitPoint(hitAngle, yc, rotation * vec3(p, 1.0), rrotation); | |
| if (yc <= 0.0 && (point.x < 0.0 || point.y < 0.0 || point.x > 1.0 || point.y > 1.0)) { | |
| return getToColor(p); | |
| } | |
| if (yc > 0.0) return getFromColor(p); | |
| vec4 color = getFromColor(point.xy); | |
| vec4 tcolor = vec4(0.0); | |
| return antiAlias(color, tcolor, distanceToEdge(point)); | |
| } | |
| vec4 seeThroughWithShadow( | |
| float yc, | |
| vec2 p, | |
| vec3 point, | |
| mat3 rotation, | |
| mat3 rrotation, | |
| float amount, | |
| float cylinderAngle | |
| ) { | |
| float shadow = distanceToEdge(point) * 30.0; | |
| shadow = (1.0 - shadow) / 3.0; | |
| if (shadow < 0.0) { | |
| shadow = 0.0; | |
| } else { | |
| shadow *= amount; | |
| } | |
| vec4 shadowColor = seeThrough(yc, p, rotation, rrotation, cylinderAngle); | |
| shadowColor.r -= shadow; | |
| shadowColor.g -= shadow; | |
| shadowColor.b -= shadow; | |
| return shadowColor; | |
| } | |
| vec4 backside(float yc, vec3 point) { | |
| vec4 color = getFromColor(point.xy); | |
| float gray = (color.r + color.b + color.g) / 15.0; | |
| gray += (8.0 / 10.0) * ( | |
| pow(1.0 - abs(yc / cylinderRadius), 2.0 / 10.0) / 2.0 + (5.0 / 10.0) | |
| ); | |
| color.rgb = vec3(gray); | |
| return color; | |
| } | |
| vec4 behindSurface( | |
| vec2 p, | |
| float yc, | |
| vec3 point, | |
| mat3 rrotation, | |
| float amount, | |
| float cylinderAngle | |
| ) { | |
| float shado = (1.0 - ((-cylinderRadius - yc) / amount * 7.0)) / 6.0; | |
| shado *= 1.0 - abs(point.x - 0.5); | |
| yc = (-cylinderRadius - cylinderRadius - yc); | |
| float hitAngle = (acos(yc / cylinderRadius) + cylinderAngle) - PI; | |
| point = hitPoint(hitAngle, yc, point, rrotation); | |
| if ( | |
| yc < 0.0 && | |
| point.x >= 0.0 && | |
| point.y >= 0.0 && | |
| point.x <= 1.0 && | |
| point.y <= 1.0 && | |
| (hitAngle < PI || amount > 0.5) | |
| ) { | |
| shado = 1.0 - ( | |
| sqrt(pow(point.x - 0.5, 2.0) + pow(point.y - 0.5, 2.0)) / (71.0 / 100.0) | |
| ); | |
| shado *= pow(-yc / cylinderRadius, 3.0); | |
| shado *= 0.5; | |
| } else { | |
| shado = 0.0; | |
| } | |
| return vec4(getToColor(p).rgb - shado, 1.0); | |
| } | |
| vec4 transition(vec2 p) { | |
| float amount = progress * (MAX_AMOUNT - MIN_AMOUNT) + MIN_AMOUNT; | |
| float cylinderCenter = amount; | |
| float cylinderAngle = 2.0 * PI * amount; | |
| float c = cos(-angle); | |
| float s = sin(-angle); | |
| mat3 rotation = mat3( | |
| c, s, 0.0, | |
| -s, c, 0.0, | |
| -0.801, 0.8900, 1.0 | |
| ); | |
| c = cos(angle); | |
| s = sin(angle); | |
| mat3 rrotation = mat3( | |
| c, s, 0.0, | |
| -s, c, 0.0, | |
| 0.9850, 0.985, 1.0 | |
| ); | |
| vec3 point = rotation * vec3(p, 1.0); | |
| float yc = point.y - cylinderCenter; | |
| if (yc < -cylinderRadius) { | |
| return behindSurface(p, yc, point, rrotation, amount, cylinderAngle); | |
| } | |
| if (yc > cylinderRadius) { | |
| return getFromColor(p); | |
| } | |
| float hitAngle = (acos(yc / cylinderRadius) + cylinderAngle) - PI; | |
| float hitAngleMod = mod(hitAngle, 2.0 * PI); | |
| if ( | |
| (hitAngleMod > PI && amount < 0.5) || | |
| (hitAngleMod > PI / 2.0 && amount < 0.0) | |
| ) { | |
| return seeThrough(yc, p, rotation, rrotation, cylinderAngle); | |
| } | |
| point = hitPoint(hitAngle, yc, point, rrotation); | |
| if (point.x < 0.0 || point.y < 0.0 || point.x > 1.0 || point.y > 1.0) { | |
| return seeThroughWithShadow(yc, p, point, rotation, rrotation, amount, cylinderAngle); | |
| } | |
| vec4 color = backside(yc, point); | |
| vec4 otherColor; | |
| if (yc < 0.0) { | |
| float shado = 1.0 - ( | |
| sqrt(pow(point.x - 0.5, 2.0) + pow(point.y - 0.5, 2.0)) / 0.71 | |
| ); | |
| shado *= pow(-yc / cylinderRadius, 3.0); | |
| shado *= 0.5; | |
| otherColor = vec4(0.0, 0.0, 0.0, shado); | |
| } else { | |
| otherColor = getFromColor(p); | |
| } | |
| color = antiAlias(color, otherColor, cylinderRadius - abs(yc)); | |
| vec4 cl = seeThroughWithShadow(yc, p, point, rotation, rrotation, amount, cylinderAngle); | |
| float dist = distanceToEdge(point); | |
| return antiAlias(color, cl, dist); | |
| } | |
| ` | |
| }, | |
| { | |
| name: 'page-curl', | |
| label: 'Page Curl', | |
| defaults: { | |
| persp: 0.7, | |
| unzoom: 0.3, | |
| reflection: 0.4, | |
| floating: 3.0 | |
| }, | |
| uniforms: { | |
| persp: { type: '1f', getValue: (params) => params.persp }, | |
| unzoom: { type: '1f', getValue: (params) => params.unzoom }, | |
| reflection: { type: '1f', getValue: (params) => params.reflection }, | |
| floating: { type: '1f', getValue: (params) => params.floating } | |
| }, | |
| glsl: ` | |
| vec2 project(vec2 p) { | |
| return p * vec2(1.0, -1.2) + vec2(0.0, -floating / 100.0); | |
| } | |
| bool inBounds(vec2 p) { | |
| return all(lessThan(vec2(0.0), p)) && all(lessThan(p, vec2(1.0))); | |
| } | |
| vec4 bgColor(vec2 p, vec2 pfr, vec2 pto) { | |
| vec4 c = vec4(0.0, 0.0, 0.0, 1.0); | |
| pfr = project(pfr); | |
| if (inBounds(pfr)) { | |
| c += mix(vec4(0.0), getFromColor(pfr), reflection * mix(1.0, 0.0, pfr.y)); | |
| } | |
| pto = project(pto); | |
| if (inBounds(pto)) { | |
| c += mix(vec4(0.0), getToColor(pto), reflection * mix(1.0, 0.0, pto.y)); | |
| } | |
| return c; | |
| } | |
| vec2 xskew(vec2 p, float perspValue, float center) { | |
| float x = mix(p.x, 1.0 - p.x, center); | |
| return ( | |
| ( | |
| vec2( | |
| x, | |
| (p.y - 0.5 * (1.0 - perspValue) * x) / | |
| (1.0 + (perspValue - 1.0) * x) | |
| ) - vec2(0.5 - distance(center, 0.5), 0.0) | |
| ) * vec2( | |
| 0.5 / distance(center, 0.5) * (center < 0.5 ? 1.0 : -1.0), | |
| 1.0 | |
| ) + vec2(center < 0.5 ? 0.0 : 1.0, 0.0) | |
| ); | |
| } | |
| vec4 transition(vec2 op) { | |
| float t = clamp(progress, 0.0001, 0.9999); | |
| float uz = unzoom * 2.0 * (0.5 - distance(0.5, t)); | |
| vec2 p = -uz * 0.5 + (1.0 + uz) * op; | |
| vec2 fromP = xskew( | |
| (p - vec2(t, 0.0)) / vec2(1.0 - t, 1.0), | |
| 1.0 - mix(t, 0.0, persp), | |
| 0.0 | |
| ); | |
| vec2 toP = xskew( | |
| p / vec2(t, 1.0), | |
| mix(pow(t, 2.0), 1.0, persp), | |
| 1.0 | |
| ); | |
| if (inBounds(fromP)) { | |
| return getFromColor(fromP); | |
| } else if (inBounds(toP)) { | |
| return getToColor(toP); | |
| } | |
| return bgColor(op, fromP, toP); | |
| } | |
| ` | |
| } | |
| ]; |
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
| export class WebGLTransitionFactory { | |
| static vertexSource = ` | |
| attribute vec2 a_position; | |
| attribute vec2 a_uv; | |
| varying vec2 v_uv; | |
| void main() { | |
| v_uv = a_uv; | |
| gl_Position = vec4(a_position, 0.0, 1.0); | |
| } | |
| `; | |
| static fragmentHeader = ` | |
| precision mediump float; | |
| uniform sampler2D u_from; | |
| uniform sampler2D u_to; | |
| uniform float progress; | |
| uniform float ratio; | |
| uniform vec4 bgcolor; | |
| varying vec2 v_uv; | |
| vec4 getFromColor(vec2 p) { | |
| return texture2D(u_from, p); | |
| } | |
| vec4 getToColor(vec2 p) { | |
| return texture2D(u_to, p); | |
| } | |
| `; | |
| static sharedUniforms = { | |
| u_from: { type: '1i' }, | |
| u_to: { type: '1i' }, | |
| progress: { type: '1f' }, | |
| ratio: { type: '1f' }, | |
| bgcolor: { type: '4f' } | |
| }; | |
| constructor(canvas, options = {}) { | |
| this.canvas = canvas; | |
| this.gl = canvas.getContext('webgl'); | |
| if (!this.gl) { | |
| throw new Error('WebGL not supported'); | |
| } | |
| this.runtimeState = { | |
| duration: options.duration ?? 2000, | |
| bgcolor: options.bgcolor ?? [0.0, 0.0, 0.0, 1.0] | |
| }; | |
| this.transitionDefs = []; | |
| this.transitions = []; | |
| this.transitionMap = new Map(); | |
| this.transitionOverrides = new Map(); | |
| this.active = null; | |
| this.activeParams = {}; | |
| this.startTime = null; | |
| this.playing = true; | |
| this.rafId = null; | |
| this.buffer = null; | |
| this.textureFrom = null; | |
| this.textureTo = null; | |
| this._initGeometry(); | |
| this._render = this._render.bind(this); | |
| } | |
| _initGeometry() { | |
| const gl = this.gl; | |
| const vertices = new Float32Array([ | |
| -1, -1, 0, 0, | |
| 1, -1, 1, 0, | |
| -1, 1, 0, 1, | |
| -1, 1, 0, 1, | |
| 1, -1, 1, 0, | |
| 1, 1, 1, 1 | |
| ]); | |
| this.buffer = gl.createBuffer(); | |
| gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer); | |
| gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); | |
| } | |
| _createShader(type, source) { | |
| const gl = this.gl; | |
| const shader = gl.createShader(type); | |
| gl.shaderSource(shader, source); | |
| gl.compileShader(shader); | |
| if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { | |
| const info = gl.getShaderInfoLog(shader); | |
| gl.deleteShader(shader); | |
| throw new Error('Shader compile error: ' + info + '\n\n' + source); | |
| } | |
| return shader; | |
| } | |
| _createProgram(vertexSource, fragmentSource) { | |
| const gl = this.gl; | |
| const vs = this._createShader(gl.VERTEX_SHADER, vertexSource); | |
| const fs = this._createShader(gl.FRAGMENT_SHADER, fragmentSource); | |
| const program = gl.createProgram(); | |
| gl.attachShader(program, vs); | |
| gl.attachShader(program, fs); | |
| gl.linkProgram(program); | |
| if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { | |
| const info = gl.getProgramInfoLog(program); | |
| gl.deleteProgram(program); | |
| throw new Error('Program link error: ' + info); | |
| } | |
| return program; | |
| } | |
| _buildFragmentSource(def) { | |
| const uniformLines = Object.entries(def.uniforms || {}) | |
| .map(([name, spec]) => { | |
| const glslTypeMap = { | |
| '1f': 'float', | |
| '1i': 'int', | |
| '2f': 'vec2', | |
| '3f': 'vec3', | |
| '4f': 'vec4' | |
| }; | |
| return `uniform ${glslTypeMap[spec.type]} ${name};`; | |
| }) | |
| .join('\n'); | |
| return ` | |
| ${WebGLTransitionFactory.fragmentHeader} | |
| ${uniformLines} | |
| ${def.glsl} | |
| void main() { | |
| gl_FragColor = transition(v_uv); | |
| } | |
| `; | |
| } | |
| _setUniform(location, type, value) { | |
| if (location === null) return; | |
| const gl = this.gl; | |
| switch (type) { | |
| case '1f': | |
| gl.uniform1f(location, value); | |
| break; | |
| case '1i': | |
| gl.uniform1i(location, value); | |
| break; | |
| case '2f': | |
| gl.uniform2f(location, value[0], value[1]); | |
| break; | |
| case '3f': | |
| gl.uniform3f(location, value[0], value[1], value[2]); | |
| break; | |
| case '4f': | |
| gl.uniform4f(location, value[0], value[1], value[2], value[3]); | |
| break; | |
| default: | |
| throw new Error('Unsupported uniform type: ' + type); | |
| } | |
| } | |
| _bindAttributes(locations) { | |
| const gl = this.gl; | |
| gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer); | |
| const stride = 4 * Float32Array.BYTES_PER_ELEMENT; | |
| if (locations.a_position !== -1) { | |
| gl.enableVertexAttribArray(locations.a_position); | |
| gl.vertexAttribPointer(locations.a_position, 2, gl.FLOAT, false, stride, 0); | |
| } | |
| if (locations.a_uv !== -1) { | |
| gl.enableVertexAttribArray(locations.a_uv); | |
| gl.vertexAttribPointer( | |
| locations.a_uv, | |
| 2, | |
| gl.FLOAT, | |
| false, | |
| stride, | |
| 2 * Float32Array.BYTES_PER_ELEMENT | |
| ); | |
| } | |
| } | |
| _resizeCanvas() { | |
| const gl = this.gl; | |
| const dpr = Math.min(window.devicePixelRatio || 1, 2); | |
| const width = Math.floor(this.canvas.clientWidth * dpr); | |
| const height = Math.floor(this.canvas.clientHeight * dpr); | |
| if (this.canvas.width !== width || this.canvas.height !== height) { | |
| this.canvas.width = width; | |
| this.canvas.height = height; | |
| } | |
| gl.viewport(0, 0, this.canvas.width, this.canvas.height); | |
| } | |
| _mergeParams(defaults, overrides) { | |
| return { | |
| ...structuredClone(defaults || {}), | |
| ...(overrides || {}) | |
| }; | |
| } | |
| _createTransition(def) { | |
| const gl = this.gl; | |
| const fragmentSource = this._buildFragmentSource(def); | |
| const program = this._createProgram( | |
| WebGLTransitionFactory.vertexSource, | |
| fragmentSource | |
| ); | |
| const locations = { | |
| a_position: gl.getAttribLocation(program, 'a_position'), | |
| a_uv: gl.getAttribLocation(program, 'a_uv') | |
| }; | |
| for (const name of Object.keys(WebGLTransitionFactory.sharedUniforms)) { | |
| locations[name] = gl.getUniformLocation(program, name); | |
| } | |
| for (const name of Object.keys(def.uniforms || {})) { | |
| locations[name] = gl.getUniformLocation(program, name); | |
| } | |
| return { | |
| name: def.name, | |
| label: def.label || def.name, | |
| defaults: structuredClone(def.defaults || {}), | |
| program, | |
| locations, | |
| bind: (runtimeState, params, runtime) => { | |
| this._setUniform(locations.u_from, '1i', 0); | |
| this._setUniform(locations.u_to, '1i', 1); | |
| this._setUniform(locations.progress, '1f', runtime.progress); | |
| this._setUniform(locations.ratio, '1f', runtime.ratio); | |
| this._setUniform(locations.bgcolor, '4f', runtimeState.bgcolor); | |
| for (const [name, spec] of Object.entries(def.uniforms || {})) { | |
| const value = spec.getValue(params, runtime, runtimeState); | |
| this._setUniform(locations[name], spec.type, value); | |
| } | |
| } | |
| }; | |
| } | |
| register(def) { | |
| if (!def || !def.name) { | |
| throw new Error('Transition definition must include a name.'); | |
| } | |
| if (this.transitionMap.has(def.name)) { | |
| throw new Error(`Transition "${def.name}" is already registered.`); | |
| } | |
| const transition = this._createTransition(def); | |
| this.transitionDefs.push(def); | |
| this.transitions.push(transition); | |
| this.transitionMap.set(transition.name, transition); | |
| if (!this.active) { | |
| this.active = transition; | |
| this.activeParams = this._mergeParams( | |
| transition.defaults, | |
| this.transitionOverrides.get(transition.name) | |
| ); | |
| } | |
| return this; | |
| } | |
| registerMany(definitions) { | |
| definitions.forEach((def) => this.register(def)); | |
| return this; | |
| } | |
| setTransition(indexOrName) { | |
| let next = null; | |
| if (typeof indexOrName === 'number') { | |
| if (indexOrName >= 0 && indexOrName < this.transitions.length) { | |
| next = this.transitions[indexOrName]; | |
| } | |
| } else { | |
| next = this.transitionMap.get(indexOrName) || null; | |
| } | |
| if (!next) return this; | |
| this.active = next; | |
| this.activeParams = this._mergeParams( | |
| next.defaults, | |
| this.transitionOverrides.get(next.name) | |
| ); | |
| this.playOnce(); | |
| return this; | |
| } | |
| setTransitionParams(name, partial) { | |
| const transition = this.transitionMap.get(name); | |
| if (!transition) return this; | |
| const previous = this.transitionOverrides.get(name) || {}; | |
| this.transitionOverrides.set(name, { ...previous, ...partial }); | |
| if (this.active && this.active.name === name) { | |
| this.activeParams = this._mergeParams( | |
| this.active.defaults, | |
| this.transitionOverrides.get(name) | |
| ); | |
| } | |
| return this; | |
| } | |
| getTransitionParams(name) { | |
| const transition = this.transitionMap.get(name); | |
| if (!transition) return null; | |
| return this._mergeParams( | |
| transition.defaults, | |
| this.transitionOverrides.get(name) | |
| ); | |
| } | |
| setDuration(ms) { | |
| const value = Number(ms); | |
| if (!Number.isFinite(value) || value <= 0) return this; | |
| this.runtimeState.duration = value; | |
| return this; | |
| } | |
| setBackgroundColor(r, g, b, a = 1.0) { | |
| this.runtimeState.bgcolor = [r, g, b, a]; | |
| return this; | |
| } | |
| playOnce(durationOverride) { | |
| if (durationOverride !== undefined) { | |
| this.setDuration(durationOverride); | |
| } | |
| this.startTime = null; | |
| this.playing = true; | |
| return this; | |
| } | |
| async loadImage(url) { | |
| return new Promise((resolve, reject) => { | |
| const img = new Image(); | |
| img.crossOrigin = 'anonymous'; | |
| img.onload = () => resolve(img); | |
| img.onerror = () => reject(new Error('Failed to load image: ' + url)); | |
| img.src = url; | |
| }); | |
| } | |
| _createTexture(image, textureUnit) { | |
| const gl = this.gl; | |
| const texture = gl.createTexture(); | |
| gl.activeTexture(gl.TEXTURE0 + textureUnit); | |
| gl.bindTexture(gl.TEXTURE_2D, texture); | |
| gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); | |
| 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_MIN_FILTER, gl.LINEAR); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); | |
| gl.texImage2D( | |
| gl.TEXTURE_2D, | |
| 0, | |
| gl.RGBA, | |
| gl.RGBA, | |
| gl.UNSIGNED_BYTE, | |
| image | |
| ); | |
| return texture; | |
| } | |
| setTextures(fromImage, toImage) { | |
| this.textureFrom = this._createTexture(fromImage, 0); | |
| this.textureTo = this._createTexture(toImage, 1); | |
| return this; | |
| } | |
| async setTexturesFromUrls(fromUrl, toUrl) { | |
| const [fromImage, toImage] = await Promise.all([ | |
| this.loadImage(fromUrl), | |
| this.loadImage(toUrl) | |
| ]); | |
| this.setTextures(fromImage, toImage); | |
| return this; | |
| } | |
| mountButtons(container) { | |
| container.querySelectorAll('[data-transition-button="true"]').forEach((node) => { | |
| node.remove(); | |
| }); | |
| this.transitions.forEach((transition, index) => { | |
| const button = document.createElement('button'); | |
| button.type = 'button'; | |
| button.textContent = transition.label; | |
| button.dataset.transitionButton = 'true'; | |
| button.addEventListener('click', () => this.setTransition(index)); | |
| container.appendChild(button); | |
| }); | |
| return this; | |
| } | |
| start() { | |
| if (!this.active) { | |
| throw new Error('No transitions registered.'); | |
| } | |
| if (!this.rafId) { | |
| this.rafId = requestAnimationFrame(this._render); | |
| } | |
| return this; | |
| } | |
| stop() { | |
| if (this.rafId) { | |
| cancelAnimationFrame(this.rafId); | |
| this.rafId = null; | |
| } | |
| return this; | |
| } | |
| _render(time) { | |
| const gl = this.gl; | |
| this._resizeCanvas(); | |
| if (this.playing && this.startTime === null) { | |
| this.startTime = time; | |
| } | |
| let progress = 0.0; | |
| if (this.playing) { | |
| const elapsed = time - this.startTime; | |
| progress = Math.min(elapsed / this.runtimeState.duration, 1.0); | |
| if (progress >= 1.0) { | |
| this.playing = false; | |
| } | |
| } else { | |
| progress = 1.0; | |
| } | |
| const runtime = { | |
| progress, | |
| ratio: this.canvas.width / this.canvas.height | |
| }; | |
| gl.clearColor( | |
| this.runtimeState.bgcolor[0], | |
| this.runtimeState.bgcolor[1], | |
| this.runtimeState.bgcolor[2], | |
| this.runtimeState.bgcolor[3] | |
| ); | |
| gl.clear(gl.COLOR_BUFFER_BIT); | |
| gl.useProgram(this.active.program); | |
| this._bindAttributes(this.active.locations); | |
| this.active.bind(this.runtimeState, this.activeParams, runtime); | |
| gl.drawArrays(gl.TRIANGLES, 0, 6); | |
| this.rafId = requestAnimationFrame(this._render); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment