├── src ├── index.css ├── main.jsx ├── shaders │ ├── includes │ │ ├── coverUV.glsl │ │ └── perlin3dNoise.glsl │ └── imageReveal │ │ ├── vertex.glsl │ │ └── fragment.glsl ├── components │ ├── CodropsOverlay.jsx │ └── RevealImage.jsx └── App.jsx ├── public ├── favicon.ico └── img │ ├── THUMBNAIL.gif │ └── textureupscaled.webp ├── postcss.config.js ├── tailwind.config.js ├── vite.config.js ├── .gitignore ├── index.html ├── package.json ├── LICENSE ├── README.md └── eslint.config.js /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colindmg/r3f-image-reveal-effect/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/img/THUMBNAIL.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colindmg/r3f-image-reveal-effect/HEAD/public/img/THUMBNAIL.gif -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/img/textureupscaled.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colindmg/r3f-image-reveal-effect/HEAD/public/img/textureupscaled.webp -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | }; 9 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react-swc"; 2 | import { defineConfig } from "vite"; 3 | import glsl from "vite-plugin-glsl"; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react(), glsl()], 8 | }); 9 | -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App.jsx' 4 | import './index.css' 5 | 6 | createRoot(document.getElementById('root')).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/shaders/includes/coverUV.glsl: -------------------------------------------------------------------------------- 1 | /*------------------------------ 2 | Background Cover UV 3 | -------------------------------- 4 | u = basic UV 5 | s = plane size 6 | i = image size 7 | ------------------------------*/ 8 | vec2 CoverUV(vec2 u, vec2 s, vec2 i) { 9 | float rs = s.x / s.y; // Aspect plane size 10 | float ri = i.x / i.y; // Aspect image size 11 | vec2 st = rs < ri ? vec2(i.x * s.y / i.y, s.y) : vec2(s.x, i.y * s.x / i.x); // New st 12 | vec2 o = (rs < ri ? vec2((st.x - s.x) / 2.0, 0.0) : vec2(0.0, (st.y - s.y) / 2.0)) / st; // Offset 13 | return u * s / st + o; 14 | } -------------------------------------------------------------------------------- /src/shaders/imageReveal/vertex.glsl: -------------------------------------------------------------------------------- 1 | uniform float uProgress; 2 | 3 | varying vec2 vUv; 4 | 5 | void main() 6 | { 7 | vec3 newPosition = position; 8 | 9 | // Calculate the distance to the center of our plane 10 | float distanceToCenter = distance(vec2(0.5), uv); 11 | 12 | // Wave effect 13 | float wave = (1.0 - uProgress) * sin(distanceToCenter * 20.0 - uProgress * 5.0); 14 | 15 | // Apply the wave effect to the position Z 16 | newPosition.z += wave; 17 | 18 | // FINAL POSITION 19 | gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0); 20 | 21 | // VARYINGS 22 | vUv = uv; 23 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Image Reveal Effect with R3F & GLSL | Demo | Codrops 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-boilerplate", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@react-three/drei": "^9.114.6", 14 | "@react-three/fiber": "^8.17.10", 15 | "leva": "^0.9.35", 16 | "motion": "^11.11.17", 17 | "react": "^18.3.1", 18 | "react-dom": "^18.3.1", 19 | "three": "^0.169.0" 20 | }, 21 | "devDependencies": { 22 | "@eslint/js": "^9.11.1", 23 | "@types/react": "^18.3.10", 24 | "@types/react-dom": "^18.3.0", 25 | "@vitejs/plugin-react-swc": "^3.5.0", 26 | "autoprefixer": "^10.4.20", 27 | "eslint": "^9.11.1", 28 | "eslint-plugin-react": "^7.37.0", 29 | "eslint-plugin-react-hooks": "^5.1.0-rc.0", 30 | "eslint-plugin-react-refresh": "^0.4.12", 31 | "globals": "^15.9.0", 32 | "postcss": "^8.4.47", 33 | "tailwindcss": "^3.4.14", 34 | "vite": "^5.4.8", 35 | "vite-plugin-glsl": "^1.3.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2009 - 2024 [Codrops](https://tympanus.net/codrops) 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shader based Reveal Effect with React Three Fiber 2 | 3 | _A simple yet pretty cool image reveal effect made with React-Three-Fiber/Drei and animated with FramerMotion._ 4 | 5 | ![Thumbnail](./public/img/THUMBNAIL.gif) 6 | 7 | [Article on Codrops](https://tympanus.net/codrops/?p=83030) 8 | 9 | [Demo](https://tympanus.net/Tutorials/R3FImageReveal/) 10 | 11 | ## Installation 12 | 13 | ```bash 14 | npm install 15 | ``` 16 | 17 | ## Build and Run 18 | 19 | ```bash 20 | npm run dev 21 | ``` 22 | 23 | or 24 | 25 | ```bash 26 | npm run build 27 | ``` 28 | 29 | ## Credits 30 | 31 | - Image generated with [Midjourney](https://www.midjourney.com/) 32 | 33 | ## Misc 34 | 35 | Follow Colin: [X](http://www.x.com/colindmg), [Linkedin](https://www.linkedin.com/in/colindmg/), [GitHub](https://github.com/colindmg) 36 | 37 | Follow Codrops: [Bluesky](https://bsky.app/profile/codrops.bsky.social), [Facebook](http://www.facebook.com/codrops), [GitHub](https://github.com/codrops), [Instagram](https://www.instagram.com/codropsss/), [X](http://www.x.com/codrops) 38 | 39 | ## License 40 | 41 | [MIT](LICENSE) 42 | 43 | Made with :blue_heart: by [Colin](http://www.x.com/colindmg) & [Codrops](http://www.codrops.com) 44 | -------------------------------------------------------------------------------- /src/shaders/imageReveal/fragment.glsl: -------------------------------------------------------------------------------- 1 | uniform sampler2D uTexture; 2 | uniform float uTime; 3 | uniform float uProgress; 4 | uniform vec2 uRes; 5 | uniform vec2 uImageRes; 6 | 7 | varying vec2 vUv; 8 | 9 | #include ../includes/perlin3dNoise.glsl 10 | #include ../includes/coverUV.glsl 11 | 12 | void main() 13 | { 14 | // New UV to prevent image stretching on fullscreen mode 15 | vec2 newUv = CoverUV(vUv, uRes, uImageRes); 16 | 17 | // Displace the UV 18 | vec2 displacedUv = vUv + cnoise(vec3(vUv * 5.0, uTime * 0.1)); 19 | 20 | // Perlin noise 21 | float strength = cnoise(vec3(displacedUv * 5.0, uTime * 0.2 )); 22 | 23 | // Radial gradient 24 | float radialGradient = distance(vUv, vec2(0.5)) * 12.5 - 7.0 * uProgress; 25 | strength += radialGradient; 26 | 27 | // Clamp the value from 0 to 1 & invert it 28 | strength = clamp(strength, 0.0, 1.0); 29 | strength = 1.0 - strength; 30 | 31 | // Apply texture 32 | vec3 textureColor = texture2D(uTexture, newUv).rgb; 33 | 34 | // Opacity animation 35 | float opacityProgress = smoothstep(0.0, 0.7, uProgress); 36 | 37 | // FINAL COLOR 38 | gl_FragColor = vec4(textureColor, strength * opacityProgress); 39 | } -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import react from "eslint-plugin-react"; 3 | import reactHooks from "eslint-plugin-react-hooks"; 4 | import reactRefresh from "eslint-plugin-react-refresh"; 5 | import globals from "globals"; 6 | 7 | export default [ 8 | { ignores: ["dist"] }, 9 | { 10 | files: ["**/*.{js,jsx}"], 11 | languageOptions: { 12 | ecmaVersion: 2020, 13 | globals: globals.browser, 14 | parserOptions: { 15 | ecmaVersion: "latest", 16 | ecmaFeatures: { jsx: true }, 17 | sourceType: "module", 18 | }, 19 | }, 20 | settings: { react: { version: "18.3" } }, 21 | plugins: { 22 | react, 23 | "react-hooks": reactHooks, 24 | "react-refresh": reactRefresh, 25 | }, 26 | rules: { 27 | ...js.configs.recommended.rules, 28 | ...react.configs.recommended.rules, 29 | ...react.configs["jsx-runtime"].rules, 30 | ...reactHooks.configs.recommended.rules, 31 | "react/jsx-no-target-blank": "off", 32 | "react/no-unknown-property": "off", 33 | "react-refresh/only-export-components": [ 34 | "warn", 35 | { allowConstantExport: true }, 36 | ], 37 | }, 38 | }, 39 | ]; 40 | -------------------------------------------------------------------------------- /src/components/CodropsOverlay.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | 3 | const CodropsOverlay = ({ isDarkMode }) => { 4 | return ( 5 |
11 |
12 |
13 |

R3F Shader Reveal Effect

14 | 15 | Article 16 | 17 | 18 | All demos 19 | 20 | 24 | GitHub 25 | 26 |
27 | 28 | 60 |
61 |
62 | ); 63 | }; 64 | 65 | CodropsOverlay.propTypes = { 66 | isDarkMode: PropTypes.bool, 67 | }; 68 | 69 | export default CodropsOverlay; 70 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import { Canvas } from "@react-three/fiber"; 2 | import { animate } from "motion"; 3 | import { useMotionValue } from "motion/react"; 4 | import { useState } from "react"; 5 | import CodropsOverlay from "./components/CodropsOverlay"; 6 | import RevealImage from "./components/RevealImage"; 7 | 8 | function App() { 9 | // FULLSCREEN MODE 10 | const [isFullScreen, setIsFullScreen] = useState(false); 11 | const handleFullScreen = () => setIsFullScreen(!isFullScreen); 12 | 13 | // DARK/LIGHT MODE 14 | const [isDarkMode, setIsDarkMode] = useState(false); 15 | const handleDarkMode = () => setIsDarkMode(!isDarkMode); 16 | 17 | // REVEAL PROGRESS ANIMATION 18 | const [isRevealed, setIsRevealed] = useState(true); 19 | const revealProgress = useMotionValue(1); 20 | 21 | const handleReveal = () => { 22 | animate(revealProgress, isRevealed ? 0 : 1, { 23 | duration: 1.5, 24 | ease: "easeInOut", 25 | }); 26 | setIsRevealed(!isRevealed); 27 | }; 28 | 29 | return ( 30 | <> 31 | 39 | 44 | 45 |
46 | 52 | 58 | 64 |
65 | 66 | 67 | 68 | ); 69 | } 70 | 71 | export default App; 72 | -------------------------------------------------------------------------------- /src/components/RevealImage.jsx: -------------------------------------------------------------------------------- 1 | import { shaderMaterial, useAspect, useTexture } from "@react-three/drei"; 2 | import { extend, useFrame, useThree } from "@react-three/fiber"; 3 | import PropTypes from "prop-types"; 4 | import { useEffect, useRef } from "react"; 5 | import * as THREE from "three"; 6 | import imageRevealFragmentShader from "../shaders/imageReveal/fragment.glsl"; 7 | import imageRevealVertexShader from "../shaders/imageReveal/vertex.glsl"; 8 | 9 | const ImageRevealMaterial = shaderMaterial( 10 | { 11 | uTexture: new THREE.Texture(), 12 | uTime: 0, 13 | uProgress: 0, 14 | uImageRes: new THREE.Vector2(1.0, 1.0), 15 | uRes: new THREE.Vector2(1.0, 1.0), 16 | }, 17 | imageRevealVertexShader, 18 | imageRevealFragmentShader, 19 | (self) => { 20 | self.transparent = true; 21 | } 22 | ); 23 | 24 | extend({ ImageRevealMaterial }); 25 | 26 | const RevealImage = ({ 27 | imageTexture, 28 | revealProgress, 29 | isFullScreen = false, 30 | }) => { 31 | const materialRef = useRef(); 32 | 33 | // LOADING TEXTURE & HANDLING ASPECT RATIO 34 | const texture = useTexture(imageTexture, (loadedTexture) => { 35 | if (materialRef.current) { 36 | materialRef.current.uTexture = loadedTexture; 37 | } 38 | }); 39 | const { width, height } = texture.image; 40 | const scale = useAspect(width, height, 0.2); 41 | 42 | // GETTING VIEWPORT SIZE 43 | const { viewport } = useThree(); 44 | const fullScreenScale = [viewport.width, viewport.height, 1]; 45 | 46 | // UPDATING UNIFORMS ON RESIZE TO MAINTAIN ASPECT RATIO 47 | useEffect(() => { 48 | const viewportScale = [viewport.width, viewport.height, 1]; 49 | 50 | if (materialRef.current) { 51 | materialRef.current.uRes.set( 52 | isFullScreen ? viewportScale[0] : scale[0], 53 | isFullScreen ? viewportScale[1] : scale[1] 54 | ); 55 | materialRef.current.uImageRes.set(width, height); 56 | } 57 | }, [isFullScreen, scale, viewport.width, viewport.height, width, height]); 58 | 59 | // UPDATING UNIFORMS 60 | useFrame(({ clock }) => { 61 | if (materialRef.current) { 62 | materialRef.current.uTime = clock.elapsedTime; 63 | materialRef.current.uProgress = revealProgress.get(); 64 | } 65 | }); 66 | 67 | return ( 68 | 69 | 70 | 71 | 72 | ); 73 | }; 74 | 75 | export default RevealImage; 76 | 77 | RevealImage.propTypes = { 78 | imageTexture: PropTypes.string.isRequired, 79 | revealProgress: PropTypes.object.isRequired, 80 | isFullScreen: PropTypes.bool, 81 | }; 82 | -------------------------------------------------------------------------------- /src/shaders/includes/perlin3dNoise.glsl: -------------------------------------------------------------------------------- 1 | // Classic Perlin 3D Noise 2 | // by Stefan Gustavson 3 | // 4 | vec4 permute(vec4 x){ return mod(((x*34.0)+1.0)*x, 289.0); } 5 | vec4 taylorInvSqrt(vec4 r){ return 1.79284291400159 - 0.85373472095314 * r; } 6 | vec3 fade(vec3 t) { return t*t*t*(t*(t*6.0-15.0)+10.0); } 7 | 8 | float cnoise(vec3 P) 9 | { 10 | vec3 Pi0 = floor(P); // Integer part for indexing 11 | vec3 Pi1 = Pi0 + vec3(1.0); // Integer part + 1 12 | Pi0 = mod(Pi0, 289.0); 13 | Pi1 = mod(Pi1, 289.0); 14 | vec3 Pf0 = fract(P); // Fractional part for interpolation 15 | vec3 Pf1 = Pf0 - vec3(1.0); // Fractional part - 1.0 16 | vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x); 17 | vec4 iy = vec4(Pi0.yy, Pi1.yy); 18 | vec4 iz0 = Pi0.zzzz; 19 | vec4 iz1 = Pi1.zzzz; 20 | 21 | vec4 ixy = permute(permute(ix) + iy); 22 | vec4 ixy0 = permute(ixy + iz0); 23 | vec4 ixy1 = permute(ixy + iz1); 24 | 25 | vec4 gx0 = ixy0 / 7.0; 26 | vec4 gy0 = fract(floor(gx0) / 7.0) - 0.5; 27 | gx0 = fract(gx0); 28 | vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0); 29 | vec4 sz0 = step(gz0, vec4(0.0)); 30 | gx0 -= sz0 * (step(0.0, gx0) - 0.5); 31 | gy0 -= sz0 * (step(0.0, gy0) - 0.5); 32 | 33 | vec4 gx1 = ixy1 / 7.0; 34 | vec4 gy1 = fract(floor(gx1) / 7.0) - 0.5; 35 | gx1 = fract(gx1); 36 | vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1); 37 | vec4 sz1 = step(gz1, vec4(0.0)); 38 | gx1 -= sz1 * (step(0.0, gx1) - 0.5); 39 | gy1 -= sz1 * (step(0.0, gy1) - 0.5); 40 | 41 | vec3 g000 = vec3(gx0.x,gy0.x,gz0.x); 42 | vec3 g100 = vec3(gx0.y,gy0.y,gz0.y); 43 | vec3 g010 = vec3(gx0.z,gy0.z,gz0.z); 44 | vec3 g110 = vec3(gx0.w,gy0.w,gz0.w); 45 | vec3 g001 = vec3(gx1.x,gy1.x,gz1.x); 46 | vec3 g101 = vec3(gx1.y,gy1.y,gz1.y); 47 | vec3 g011 = vec3(gx1.z,gy1.z,gz1.z); 48 | vec3 g111 = vec3(gx1.w,gy1.w,gz1.w); 49 | 50 | vec4 norm0 = taylorInvSqrt(vec4(dot(g000, g000), dot(g010, g010), dot(g100, g100), dot(g110, g110))); 51 | g000 *= norm0.x; 52 | g010 *= norm0.y; 53 | g100 *= norm0.z; 54 | g110 *= norm0.w; 55 | vec4 norm1 = taylorInvSqrt(vec4(dot(g001, g001), dot(g011, g011), dot(g101, g101), dot(g111, g111))); 56 | g001 *= norm1.x; 57 | g011 *= norm1.y; 58 | g101 *= norm1.z; 59 | g111 *= norm1.w; 60 | 61 | float n000 = dot(g000, Pf0); 62 | float n100 = dot(g100, vec3(Pf1.x, Pf0.yz)); 63 | float n010 = dot(g010, vec3(Pf0.x, Pf1.y, Pf0.z)); 64 | float n110 = dot(g110, vec3(Pf1.xy, Pf0.z)); 65 | float n001 = dot(g001, vec3(Pf0.xy, Pf1.z)); 66 | float n101 = dot(g101, vec3(Pf1.x, Pf0.y, Pf1.z)); 67 | float n011 = dot(g011, vec3(Pf0.x, Pf1.yz)); 68 | float n111 = dot(g111, Pf1); 69 | 70 | vec3 fade_xyz = fade(Pf0); 71 | vec4 n_z = mix(vec4(n000, n100, n010, n110), vec4(n001, n101, n011, n111), fade_xyz.z); 72 | vec2 n_yz = mix(n_z.xy, n_z.zw, fade_xyz.y); 73 | float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x); 74 | 75 | return 2.2 * n_xyz; 76 | } --------------------------------------------------------------------------------