├── static ├── .gitkeep ├── .DS_Store └── textures │ ├── 01.webp │ ├── 02.webp │ ├── 03.webp │ └── 04.webp ├── .DS_Store ├── src ├── .DS_Store ├── favicon.ico ├── shaders │ ├── vertex.glsl │ └── fragment.glsl ├── MeshItem.js ├── stylesheet │ ├── style.css │ └── base.css ├── js │ └── Loader.js ├── index.html └── script.js ├── .gitignore ├── package.json ├── vite.config.js ├── README.md └── LICENSE /static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arrlindii/Shader-Image-Transition/HEAD/.DS_Store -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arrlindii/Shader-Image-Transition/HEAD/src/.DS_Store -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arrlindii/Shader-Image-Transition/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /static/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arrlindii/Shader-Image-Transition/HEAD/static/.DS_Store -------------------------------------------------------------------------------- /static/textures/01.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arrlindii/Shader-Image-Transition/HEAD/static/textures/01.webp -------------------------------------------------------------------------------- /static/textures/02.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arrlindii/Shader-Image-Transition/HEAD/static/textures/02.webp -------------------------------------------------------------------------------- /static/textures/03.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arrlindii/Shader-Image-Transition/HEAD/static/textures/03.webp -------------------------------------------------------------------------------- /static/textures/04.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arrlindii/Shader-Image-Transition/HEAD/static/textures/04.webp -------------------------------------------------------------------------------- /src/shaders/vertex.glsl: -------------------------------------------------------------------------------- 1 | varying vec2 vUv; 2 | 3 | void main(){ 4 | vec4 modelPosition = modelMatrix * vec4(position, 1.0); 5 | vec4 viewPosition = viewMatrix * modelPosition; 6 | vec4 clipPosition = projectionMatrix * viewPosition; 7 | 8 | gl_Position = clipPosition; 9 | 10 | // varrying 11 | vUv = uv; 12 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-shader-image-reveal", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build" 9 | }, 10 | "devDependencies": { 11 | "vite": "^5.3.3", 12 | "vite-plugin-glsl": "^1.3.1", 13 | "vite-plugin-restart": "^0.4.1" 14 | }, 15 | "dependencies": { 16 | "fontfaceobserver": "^2.3.0", 17 | "gsap": "^3.12.5", 18 | "lil-gui": "^0.19.2", 19 | "three": "^0.166.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import restart from 'vite-plugin-restart' 2 | import glsl from 'vite-plugin-glsl' 3 | 4 | export default { 5 | root: 'src/', 6 | publicDir: '../static/', 7 | base: './', 8 | server: 9 | { 10 | host: true, 11 | open: !('SANDBOX_URL' in process.env || 'CODESANDBOX_HOST' in process.env) 12 | }, 13 | build: 14 | { 15 | outDir: '../dist', 16 | emptyOutDir: true, 17 | sourcemap: true 18 | }, 19 | plugins: 20 | [ 21 | restart({ restart: [ '../static/**', ] }), 22 | glsl() 23 | ], 24 | } -------------------------------------------------------------------------------- /src/MeshItem.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import vertexShader from './shaders/vertex.glsl' 3 | import fragmentShader from './shaders/fragment.glsl' 4 | 5 | export default function(w, h) { 6 | const geometry = new THREE.PlaneGeometry(w, h, 128, 128) 7 | 8 | const material = new THREE.ShaderMaterial({ 9 | vertexShader: vertexShader, 10 | fragmentShader: fragmentShader, 11 | uniforms: { 12 | uProgress: new THREE.Uniform(0.0), 13 | uSize: new THREE.Uniform(new THREE.Vector2(w, h)), 14 | uTexture: new THREE.Uniform(), 15 | }, 16 | }) 17 | 18 | const mesh = new THREE.Mesh(geometry, material) 19 | return mesh 20 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shader Image Transition 2 | 3 | Demo for the tutorial on creating dynamic image transitions with WebGL shaders, exploring techniques like circle SDFs, noise patterns, smooth merging, and texture integration. 4 | 5 | ![Shader Image Transition](https://tympanus.net/codrops/wp-content/uploads/2025/01/imagereveal_feat.png) 6 | 7 | [Article on Codrops](https://tympanus.net/codrops/?p=85782) 8 | 9 | [Demo](https://tympanus.net/Tutorials/ShaderImageReveal/) 10 | 11 | ## Installation 12 | 13 | Install dependencies: 14 | 15 | ``` 16 | npm install 17 | ``` 18 | 19 | Compile the code for development and start a local server: 20 | 21 | ``` 22 | npm run dev 23 | ``` 24 | 25 | Create the build: 26 | 27 | ``` 28 | npm run build 29 | ``` 30 | 31 | ## License 32 | [MIT](LICENSE) 33 | -------------------------------------------------------------------------------- /src/stylesheet/style.css: -------------------------------------------------------------------------------- 1 | * 2 | { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | html { 8 | background-color: #000000; 9 | } 10 | 11 | .webgl 12 | { 13 | position: fixed; 14 | top: 0; 15 | left: 0; 16 | outline: none; 17 | } 18 | 19 | 20 | .section 21 | { 22 | height: 100vh; 23 | position: relative; 24 | } 25 | 26 | .heading { 27 | text-transform: uppercase; 28 | mix-blend-mode: difference; 29 | position: fixed; 30 | bottom: 1rem; 31 | right: 2rem; 32 | font-size: 12vmin; 33 | font-weight: 500; 34 | } 35 | 36 | /* hide scrollbar */ 37 | html { scrollbar-width: none; } /* Firefox */ 38 | body { -ms-overflow-style: none; } /* IE and Edge */ 39 | body::-webkit-scrollbar, body::-webkit-scrollbar-button { display: none; } /* Chrome */ 40 | /* end hide scrollbar */ 41 | -------------------------------------------------------------------------------- /src/js/Loader.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import image1 from '../../static/textures/01.webp' 3 | import image2 from '../../static/textures/02.webp' 4 | import image3 from '../../static/textures/03.webp' 5 | import image4 from '../../static/textures/04.webp' 6 | 7 | export default class Loader { 8 | loadTextures(onComplete) { 9 | const textureLoader = new THREE.TextureLoader() 10 | let loadCount = 0 11 | const imageSources = [image1, image2, image3, image4] 12 | const textures = [] 13 | 14 | imageSources.forEach((src, index) => { 15 | const onLoad = (texture) => { 16 | textures[index] = texture 17 | 18 | loadCount += 1 19 | if (loadCount == imageSources.length) { 20 | onComplete(textures) 21 | } 22 | } 23 | textureLoader.load(src, onLoad) 24 | }) 25 | } 26 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Arlind Aliu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Shader Image Reveal | Demo | Codrops 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |

Shader Image Reveal by Arlind Aliu

21 | Read the tutorial 22 | GitHub 23 | All demos 24 | 27 |
28 |
29 | 30 |

Image Reveal

31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | 40 |
41 |
42 | 43 | -------------------------------------------------------------------------------- /src/shaders/fragment.glsl: -------------------------------------------------------------------------------- 1 | varying vec2 vUv; 2 | uniform float uProgress; 3 | uniform vec2 uSize; 4 | uniform sampler2D uTexture; 5 | #define PI 3.1415926538 6 | 7 | 8 | float noise(vec2 point) { 9 | float frequency = 1.0; 10 | float angle = atan(point.y,point.x) + uProgress * PI; 11 | 12 | float w0 = (cos(angle * frequency) + 1.0) / 2.0; // normalize [0 - 1] 13 | float w1 = (sin(2.*angle * frequency) + 1.0) / 2.0; // normalize [0 - 1] 14 | float w2 = (cos(3.*angle * frequency) + 1.0) / 2.0; // normalize [0 - 1] 15 | float wave = (w0 + w1 + w2) / 3.0; // normalize [0 - 1] 16 | return wave; 17 | } 18 | 19 | float softMax(float a, float b, float k) { 20 | return log(exp(k * a) + exp(k * b)) / k; 21 | } 22 | 23 | float softMin(float a, float b, float k) { 24 | return -softMax(-a, -b, k); 25 | } 26 | 27 | float circleSDF(vec2 pos, float rad) { 28 | float a = sin(uProgress * 0.2) * 0.25; // range -0.25 - 0.25 29 | float amt = 0.5 + a; 30 | float circle = length(pos); 31 | circle += noise(pos) * rad * amt; 32 | return circle; 33 | } 34 | 35 | float radialCircles(vec2 p, float o, float count) { 36 | vec2 offset = vec2(o, o); 37 | 38 | float angle = (2. * PI)/count; 39 | float s = round(atan(p.y, p.x)/angle); 40 | float an = angle * s; 41 | vec2 q = vec2(offset.x * cos(an), offset.y * sin(an)); 42 | vec2 pos = p - q; 43 | float circle = circleSDF(pos, 15.0); 44 | return circle; 45 | } 46 | 47 | void main() { 48 | vec4 bg = vec4(vec3(0.0), 0.0); 49 | vec4 texture = texture2D(uTexture,vUv); 50 | vec2 coords = vUv * uSize; 51 | vec2 o1 = vec2(0.5) * uSize; 52 | 53 | float t = pow(uProgress, 2.5); // easing 54 | float radius = uSize.x / 2.0; 55 | float rad = t * radius; 56 | float c1 = circleSDF(coords - o1, rad); 57 | 58 | vec2 p = (vUv - 0.5) * uSize; 59 | float r1 = radialCircles(p, 0.2 * uSize.x, 3.0); 60 | float r2 = radialCircles(p, 0.25 * uSize.x, 3.0); 61 | float r3 = radialCircles(p, 0.45 * uSize.x, 5.0); 62 | 63 | float k = 50.0 / uSize.x; 64 | float circle = softMin(c1, r1, k); 65 | circle = softMin(circle, r2, k); 66 | circle = softMin(circle, r3, k); 67 | 68 | circle = step(circle, rad); 69 | vec4 color = mix(bg, texture, circle); 70 | gl_FragColor = color; 71 | } -------------------------------------------------------------------------------- /src/script.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import gsap from 'gsap' 3 | import MeshItem from './MeshItem' 4 | import Loader from './js/Loader' 5 | 6 | /** 7 | * Setup 8 | */ 9 | const canvas = document.querySelector('canvas.webgl') 10 | const scene = new THREE.Scene() 11 | 12 | const sizes = { 13 | width: window.innerWidth, 14 | height: window.innerHeight 15 | } 16 | 17 | window.addEventListener('resize', () => 18 | { 19 | sizes.width = window.innerWidth 20 | sizes.height = window.innerHeight 21 | camera.aspect = sizes.width / sizes.height 22 | camera.updateProjectionMatrix() 23 | renderer.setSize(sizes.width, sizes.height) 24 | renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) 25 | }) 26 | 27 | const camera = new THREE.PerspectiveCamera(35, sizes.width / sizes.height, 0.1, 2000) 28 | camera.position.set(0, 0, 1600) 29 | scene.add(camera) 30 | 31 | const renderer = new THREE.WebGLRenderer({ 32 | canvas: canvas, 33 | alpha: true, 34 | }) 35 | renderer.setSize(sizes.width, sizes.height) 36 | renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) 37 | 38 | /** 39 | * Objects 40 | */ 41 | const material = new THREE.MeshBasicMaterial(0xff0000) 42 | const objectsDistance = sizes.height; 43 | 44 | const mesh1 = MeshItem(1000, 667) 45 | const mesh2 = MeshItem(450, 581) 46 | const mesh3 = MeshItem(553, 600) 47 | const mesh4 = MeshItem(600, 600) 48 | 49 | const sectionMeshes = [mesh1, mesh2, mesh3, mesh4] 50 | 51 | for (let i = 0; i < sectionMeshes.length; i++) { 52 | sectionMeshes[i].position.y = -objectsDistance * i 53 | } 54 | 55 | scene.add(mesh1, mesh2, mesh3, mesh4) 56 | 57 | const loader = new Loader() 58 | loader.loadTextures((textures) => { 59 | document.body.classList.remove("loading"); 60 | 61 | for (let i = 0; i < sectionMeshes.length; i++) { 62 | sectionMeshes[i].material.uniforms.uTexture.value = textures[i] 63 | } 64 | 65 | observeScroll() 66 | const section = Math.round(scrollY / sizes.height) 67 | onSectionEnter(section) 68 | tick() 69 | }) 70 | 71 | 72 | /** 73 | * Scroll 74 | */ 75 | let scrollY = window.scrollY 76 | let currentSection = -1 77 | 78 | 79 | // shift 80 | let shift = 0 81 | 82 | const observeScroll = () => { 83 | window.addEventListener('scroll', () => { 84 | scrollY = window.scrollY 85 | 86 | let newSection = scrollY / sizes.height 87 | newSection = Math.round(newSection) 88 | 89 | if (currentSection != newSection) { 90 | shift += newSection - currentSection 91 | currentSection = newSection 92 | onSectionEnter(newSection) 93 | } 94 | }) 95 | } 96 | 97 | const onSectionEnter = (section) => { 98 | gsap.to( 99 | sectionMeshes[section].material.uniforms.uProgress, 100 | { 101 | duration: 3.0, 102 | value: 1.0, 103 | } 104 | ) 105 | } 106 | 107 | /** 108 | * Tick 109 | */ 110 | const clock = new THREE.Clock() 111 | let time = 0 112 | let targetPosY = -scrollY 113 | 114 | const lerp = (a, b, t) => { 115 | return a + (b - a) * t; 116 | } 117 | 118 | const tick = () => 119 | { 120 | const elapsedTime = clock.getElapsedTime() 121 | const deltaTime = elapsedTime - time 122 | time = elapsedTime 123 | 124 | targetPosY = lerp(targetPosY, -scrollY, 0.1) 125 | camera.position.y = targetPosY 126 | renderer.render(scene, camera) 127 | window.requestAnimationFrame(tick) 128 | } -------------------------------------------------------------------------------- /src/stylesheet/base.css: -------------------------------------------------------------------------------- 1 | /* Codrops Template */ 2 | *, 3 | *::after, 4 | *::before { 5 | box-sizing: border-box; 6 | } 7 | 8 | :root { 9 | font-size: 14px; 10 | --color-text: #fff; 11 | --color-bg: #000; 12 | --color-link: #afafaf; 13 | --color-link-hover: #fff; 14 | --page-padding: 1.5rem; 15 | } 16 | 17 | body { 18 | margin: 0; 19 | color: var(--color-text); 20 | background-color: var(--color-bg); 21 | font-family: "Instrument Sans", Helvetica, serif; 22 | font-optical-sizing: auto; 23 | font-weight: 400; 24 | font-style: normal; 25 | font-variation-settings: "wdth" 100; 26 | -webkit-font-smoothing: antialiased; 27 | -moz-osx-font-smoothing: grayscale; 28 | } 29 | 30 | @media (scripting: enabled) { 31 | .loading { 32 | &::before, 33 | &::after { 34 | content: ''; 35 | position: fixed; 36 | z-index: 10000; 37 | } 38 | 39 | &::before { 40 | top: 0; 41 | left: 0; 42 | width: 100%; 43 | height: 100%; 44 | background: var(--color-bg); 45 | } 46 | 47 | &::after { 48 | top: 50%; 49 | left: 50%; 50 | width: 100px; 51 | height: 1px; 52 | margin: 0 0 0 -50px; 53 | background: var(--color-link); 54 | animation: loaderAnim 1.5s ease-in-out infinite alternate forwards; 55 | } 56 | 57 | @keyframes loaderAnim { 58 | 0% { 59 | transform: scaleX(0); 60 | transform-origin: 0% 50%; 61 | } 62 | 50% { 63 | transform: scaleX(1); 64 | transform-origin: 0% 50%; 65 | } 66 | 50.1% { 67 | transform: scaleX(1); 68 | transform-origin: 100% 50%; 69 | } 70 | 100% { 71 | transform: scaleX(0); 72 | transform-origin: 100% 50%; 73 | } 74 | } 75 | } 76 | } 77 | 78 | a { 79 | text-decoration: none; 80 | color: var(--color-link); 81 | outline: none; 82 | cursor: pointer; 83 | 84 | &:hover { 85 | text-decoration: underline; 86 | color: var(--color-link-hover); 87 | } 88 | 89 | &:focus { 90 | outline: none; 91 | background: lightgrey; 92 | 93 | &:not(:focus-visible) { 94 | background: transparent; 95 | } 96 | 97 | &:focus-visible { 98 | outline: 2px solid red; 99 | background: transparent; 100 | } 101 | } 102 | } 103 | 104 | .frame { 105 | 106 | padding: 3rem var(--page-padding) 0; 107 | display: grid; 108 | z-index: 1000; 109 | position: relative; 110 | grid-row-gap: 1rem; 111 | grid-column-gap: 2rem; 112 | pointer-events: none; 113 | justify-items: start; 114 | grid-template-columns: auto auto; 115 | grid-template-areas: 116 | 'title' 117 | 'back' 118 | 'archive' 119 | 'github' 120 | 'tags' 121 | 'sponsor'; 122 | 123 | #cdawrap { 124 | justify-self: start; 125 | grid-area: sponsor; 126 | } 127 | 128 | a, 129 | button { 130 | pointer-events: auto; 131 | } 132 | 133 | .frame__title { 134 | grid-area: title; 135 | font-size: inherit; 136 | margin: 0; 137 | } 138 | 139 | .frame__back { 140 | grid-area: back; 141 | justify-self: start; 142 | } 143 | 144 | .frame__archive { 145 | grid-area: archive; 146 | justify-self: start; 147 | } 148 | 149 | .frame__github { 150 | grid-area: github; 151 | } 152 | 153 | .frame__tags { 154 | grid-area: tags; 155 | display: flex; 156 | flex-wrap: wrap; 157 | gap: 1rem; 158 | } 159 | 160 | .frame__demos { 161 | grid-area: demos; 162 | display: flex; 163 | flex-wrap: wrap; 164 | gap: 1rem; 165 | } 166 | 167 | @media screen and (min-width: 53em) { 168 | padding: var(--page-padding); 169 | height: 100%; 170 | position: fixed; 171 | top: 0; 172 | left: 0; 173 | width: 100%; 174 | grid-template-columns: auto auto auto auto 1fr; 175 | grid-template-rows: auto auto; 176 | align-content: space-between; 177 | grid-template-areas: 178 | 'title back github archive tags' 179 | 'sponsor sponsor sponsor ... ...'; 180 | 181 | .frame__tags { 182 | justify-self: end; 183 | } 184 | 185 | .frame__demos, 186 | #cdawrap { 187 | align-self: end; 188 | max-width: 300px; 189 | } 190 | } 191 | } 192 | 193 | .content { 194 | padding: var(--page-padding); 195 | display: flex; 196 | flex-direction: column; 197 | width: 100vw; 198 | position: relative; 199 | 200 | @media screen and (min-width: 53em) { 201 | min-height: 100vh; 202 | justify-content: center; 203 | align-items: center; 204 | } 205 | } 206 | --------------------------------------------------------------------------------