├── 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 | 
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 |
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 |
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 | }
--------------------------------------------------------------------------------