• About Us
  • Privacy Policy
  • Disclaimer
  • Contact Us
AimactGrow
  • Home
  • Technology
  • AI
  • SEO
  • Coding
  • Gaming
  • Cybersecurity
  • Digital marketing
No Result
View All Result
  • Home
  • Technology
  • AI
  • SEO
  • Coding
  • Gaming
  • Cybersecurity
  • Digital marketing
No Result
View All Result
AimactGrow
No Result
View All Result

How To Create Kinetic Picture Animations with React-Three-Fiber

Admin by Admin
July 10, 2025
Home Coding
Share on FacebookShare on Twitter



For the previous few months, I’ve been exploring totally different kinetic movement designs with textual content and pictures. The type seems to be very intriguing, so I made a decision to create some actually cool natural animations utilizing pictures and React Three Fiber.

On this article, we’ll discover ways to create the next animation utilizing Canvas2D and React Three Fiber.

Setting Up the View & Digicam

The digital camera’s area of view (FOV) performs an enormous position on this undertaking. Let’s maintain it very low so it seems to be like an orthographic digital camera. You’ll be able to experiment with totally different views later. I favor utilizing a perspective digital camera over an orthographic one as a result of we are able to all the time attempt totally different FOVs. For extra detailed implementation test supply code.

Setting Up Our 3D Shapes

First, let’s create and position 3D objects that will display our images. For this example, we need to make 2 components:

Billboard.tsx – This is a cylinder that will show our stack of images

'use client';

import { useRef } from 'react';
import * as THREE from 'three';

function Billboard({ radius = 5, ...props }) {
    const ref = useRef(null);

    return (
        
            
            
        
    );
}

Banner.tsx – This is another cylinder that will work like a moving banner

'use client';

import * as THREE from 'three';
import { useRef } from 'react';

function Banner({ radius = 1.6, ...props }) {
    const ref = useRef(null);

    return (
        
            
            
        
    );
}

export default Banner;

Once we have our components ready, we can use them on our page.

Now let’s build the whole shape:

1. Create a wrapper group – We’ll make a group that wraps all our components. This will help us rotate everything together later.

page.jsx

'use client';

import styles from './page.module.scss';
import Billboard from '@/components/webgl/Billboard/Billboard';
import Banner from '@/components/webgl/Banner/Banner';
import { View } from '@/webgl/View';
import { PerspectiveCamera } from '@react-three/drei';

export default function Home() {
    return (
        
    );
}

2. Render Billboard and Banner components in the loop – Inside our group, we’ll create a loop to render our Billboards and Banners multiple times.

page.jsx

'use client';

import styles from './page.module.scss';
import Billboard from '@/components/webgl/Billboard/Billboard';
import Banner from '@/components/webgl/Banner/Banner';
import { View } from '@/webgl/View';
import { PerspectiveCamera } from '@react-three/drei';

export default function Home() {
    return (
        
{Array.from({ length: COUNT }).map((_, index) => [ , , ])}
); }

3. Stack them up – We’ll use the index from our loop and the y place to stack our gadgets on prime of one another. Right here’s the way it seems to be to this point:

web page.jsx

'use consumer';

import kinds from './web page.module.scss';
import Billboard from '@/elements/webgl/Billboard/Billboard';
import Banner from '@/elements/webgl/Banner/Banner';
import { View } from '@/webgl/View';
import { PerspectiveCamera } from '@react-three/drei';

const COUNT = 10;
const GAP = 3.2;

export default perform Residence() {
    return (
        
{Array.from({ length: COUNT }).map((_, index) => [ , , ])}
); }

4. Add some rotation – Let’s rotate issues a bit! First, I’ll hard-code the rotation of our banners to make them extra curved and match properly with the Billboard part. We’ll additionally make the radius a bit greater.

web page.jsx

'use consumer';

import kinds from './web page.module.scss';
import Billboard from '@/elements/webgl/Billboard/Billboard';
import Banner from '@/elements/webgl/Banner/Banner';
import { View } from '@/webgl/View';
import { PerspectiveCamera } from '@react-three/drei';

const COUNT = 10;
const GAP = 3.2;

export default perform Residence() {
    return (
        
{Array.from({ length: COUNT }).map((_, index) => [ , , ])}
); }

5. Tilt the entire thing – Now let’s rotate our total group to make it appear to be the Leaning Tower of Pisa.

web page.jsx

'use consumer';

import kinds from './web page.module.scss';
import Billboard from '@/elements/webgl/Billboard/Billboard';
import Banner from '@/elements/webgl/Banner/Banner';
import { View } from '@/webgl/View';
import { PerspectiveCamera } from '@react-three/drei';

const COUNT = 10;
const GAP = 3.2;

export default perform Residence() {
    return (
        
// <-- rotate the group {Array.from({ length: COUNT }).map((_, index) => [ , , ])}
); }

6. Good! – Our 3D shapes are all arrange. Now we are able to add our pictures to them.

Making a Texture from Our Photos Utilizing Canvas

Right here’s the cool half: we’ll put all our pictures onto a canvas, then use that canvas as a texture on our Billboard form.

To make this simpler, I created some helper features that simplify the entire course of.

getCanvasTexture.js

import * as THREE from 'three';

/**
* Preloads a picture and calculates its dimensions
*/
async perform preloadImage(imageUrl, axis, canvasHeight, canvasWidth) {
    const img = new Picture();

    img.crossOrigin = 'nameless';

    await new Promise((resolve, reject) => {
        img.onload = () => resolve();
        img.onerror = () => reject(new Error(`Did not load picture: ${imageUrl}`));
        img.src = imageUrl;
    });

    const aspectRatio = img.naturalWidth / img.naturalHeight;

    let calculatedWidth;
    let calculatedHeight;

    if (axis === 'x') {
        // Horizontal structure: scale to suit canvasHeight
        calculatedHeight = canvasHeight;
        calculatedWidth = canvasHeight * aspectRatio;
        } else {
        // Vertical structure: scale to suit canvasWidth
        calculatedWidth = canvasWidth;
        calculatedHeight = canvasWidth / aspectRatio;
    }

    return { img, width: calculatedWidth, peak: calculatedHeight };
}

perform calculateCanvasDimensions(imageData, axis, hole, canvasHeight, canvasWidth) {
    if (axis === 'x') {
        const totalWidth = imageData.cut back(
        (sum, knowledge, index) => sum + knowledge.width + (index > 0 ? hole : 0), 0);

        return { totalWidth, totalHeight: canvasHeight };
    } else {
        const totalHeight = imageData.cut back(
        (sum, knowledge, index) => sum + knowledge.peak + (index > 0 ? hole : 0), 0);

        return { totalWidth: canvasWidth, totalHeight };
    }
}

perform setupCanvas(canvasElement, context, dimensions) {
    const { totalWidth, totalHeight } = dimensions;
    const devicePixelRatio = Math.min(window.devicePixelRatio || 1, 2);

    canvasElement.width = totalWidth * devicePixelRatio;
    canvasElement.peak = totalHeight * devicePixelRatio;

    if (devicePixelRatio !== 1) context.scale(devicePixelRatio, devicePixelRatio);

    context.fillStyle = '#ffffff';
    context.fillRect(0, 0, totalWidth, totalHeight);
}

perform drawImages(context, imageData, axis, hole) {
    let currentX = 0;
    let currentY = 0;

    context.save();

    for (const knowledge of imageData) {
        context.drawImage(knowledge.img, currentX, currentY, knowledge.width, knowledge.peak);

        if (axis === 'x') currentX += knowledge.width + hole;
        else currentY += knowledge.peak + hole;
    }

    context.restore();
}

perform createTextureResult(canvasElement, dimensions) {
    const texture = new THREE.CanvasTexture(canvasElement);
    texture.needsUpdate = true;
    texture.wrapS = THREE.RepeatWrapping;
    texture.wrapT = THREE.ClampToEdgeWrapping;
    texture.generateMipmaps = false;
    texture.minFilter = THREE.LinearFilter;
    texture.magFilter = THREE.LinearFilter;

    return {
        texture,
        dimensions: {
            width: dimensions.totalWidth,
            peak: dimensions.totalHeight,
            aspectRatio: dimensions.totalWidth / dimensions.totalHeight,
        },
    };
}

export async perform getCanvasTexture({
    pictures,
    hole = 10,
    canvasHeight = 512,
    canvasWidth = 512,
    canvas,
    ctx,
    axis = 'x',
})  canvasElement.getContext('second');

    if (!context) throw new Error('No context');

    // Preload all pictures in parallel
    const imageData = await Promise.all(
        pictures.map((picture) => preloadImage(picture.url, axis, canvasHeight, canvasWidth))
    );

    // Calculate whole canvas dimensions
    const dimensions = calculateCanvasDimensions(imageData, axis, hole, canvasHeight, canvasWidth);

    // Setup canvas
    setupCanvas(canvasElement, context, dimensions);

    // Draw all pictures
    drawImages(context, imageData, axis, hole);

    // Create and return texture consequence
    return createTextureResult(canvasElement, dimensions)

Then we are able to additionally create a useCollageTexture hook that we are able to simply use in our elements.

useCollageTexture.jsx

import { useState, useEffect, useCallback } from 'react';
import { getCanvasTexture } from '@/webgl/helpers/getCanvasTexture';

export perform useCollageTexture(pictures, choices = {}) {
const [textureResults, setTextureResults] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);

const { hole = 0, canvasHeight = 512, canvasWidth = 512, axis = 'x' } = choices;

const createTexture = useCallback(async () => {
    attempt {
        setIsLoading(true);
        setError(null);

        const consequence = await getCanvasTexture({
            pictures,
            hole,
            canvasHeight,
            canvasWidth,
            axis,
        });

        setTextureResults(consequence);

    } catch (err) {
        setError(err instanceof Error ? err : new Error('Did not create texture'));
    } lastly {
        setIsLoading(false);
    }
}, [images, gap, canvasHeight, canvasWidth, axis]);

    useEffect(() => {
        if (pictures.size > 0) createTexture();
    }, [images.length, createTexture]);

    return  null,
        dimensions: textureResults?.dimensions ;
}

Including the Canvas to Our Billboard

Now let’s use our useCollageTexture hook on our web page. We’ll create some easy loading logic. It takes a second to fetch all the photographs and put them onto the canvas. Then we’ll go our texture and dimensions of canvas into the Billboard part.

web page.jsx

'use consumer';

import kinds from './web page.module.scss';
import Billboard from '@/elements/webgl/Billboard/Billboard';
import Banner from '@/elements/webgl/Banner/Banner';
import Loader from '@/elements/ui/modules/Loader/Loader';
import pictures from '@/knowledge/pictures';
import { View } from '@/webgl/View';
import { PerspectiveCamera } from '@react-three/drei';
import { useCollageTexture } from '@/hooks/useCollageTexture';

const COUNT = 10;
const GAP = 3.2;

export default perform Residence() {
    const { texture, dimensions, isLoading } = useCollageTexture(pictures); // <-- getting the feel and dimensions from the useCollageTexture hook

    if (isLoading) return ; // <-- displaying the loader when the feel is loading

    return (
        
{Array.from({ length: COUNT }).map((_, index) => [ , , ])}
); }

Contained in the Billboard part, we have to correctly map this texture to ensure every part suits accurately. The width of our canvas will match the circumference of the cylinder, and we’ll heart the y place of the feel. This fashion, all the photographs maintain their decision and don’t get squished or stretched.

Billboard.jsx

'use consumer';

import * as THREE from 'three';
import { useRef } from 'react';  

perform setupCylinderTextureMapping(texture, dimensions, radius, peak) {
    const cylinderCircumference = 2 * Math.PI * radius;
    const cylinderHeight = peak;
    const cylinderAspectRatio = cylinderCircumference / cylinderHeight;

    if (dimensions.aspectRatio > cylinderAspectRatio) {
        // Canvas is wider than cylinder proportionally
        texture.repeat.x = cylinderAspectRatio / dimensions.aspectRatio;
        texture.repeat.y = 1;
        texture.offset.x = (1 - texture.repeat.x) / 2;
    } else {
        // Canvas is taller than cylinder proportionally
        texture.repeat.x = 1;
        texture.repeat.y = dimensions.aspectRatio / cylinderAspectRatio;
    }

    // Middle the feel
    texture.offset.y = (1 - texture.repeat.y) / 2;
}

perform Billboard({ texture, dimensions, radius = 5, ...props }) {
    const ref = useRef(null);

    setupCylinderTextureMapping(texture, dimensions, radius, 2);

    return (
        
            
            
        
    );
}

export default Billboard;

Now let’s animate them using the useFrame hook. The trick to animating these images is to just move the X offset of the texture. This gives us the effect of a rotating mesh, when really we’re just moving the texture offset.

Billboard.jsx

'use client';

import * as THREE from 'three';
import { useRef } from 'react';
import { useFrame } from '@react-three/fiber';  

function setupCylinderTextureMapping(texture, dimensions, radius, height) {
    const cylinderCircumference = 2 * Math.PI * radius;
    const cylinderHeight = height;
    const cylinderAspectRatio = cylinderCircumference / cylinderHeight;

    if (dimensions.aspectRatio > cylinderAspectRatio) {
        // Canvas is wider than cylinder proportionally
        texture.repeat.x = cylinderAspectRatio / dimensions.aspectRatio;
        texture.repeat.y = 1;
        texture.offset.x = (1 - texture.repeat.x) / 2;
    } else {
        // Canvas is taller than cylinder proportionally
        texture.repeat.x = 1;
        texture.repeat.y = dimensions.aspectRatio / cylinderAspectRatio;
    }

    // Center the texture
    texture.offset.y = (1 - texture.repeat.y) / 2;
}

function Billboard({ texture, dimensions, radius = 5, ...props }) {
    const ref = useRef(null);

    setupCylinderTextureMapping(texture, dimensions, radius, 2);

    useFrame((state, delta) => {
        if (texture) texture.offset.x += delta * 0.001;
    });

    return (
        
            
            
        
    );
}

export default Billboard;

I think it would look even better if we made the back of the images a little darker. To do this, I created MeshImageMaterial – it’s just an extension of MeshBasicMaterial that makes our backface a bit darker.

MeshImageMaterial.js

import * as THREE from 'three';
import { extend } from '@react-three/fiber';

export class MeshImageMaterial extends THREE.MeshBasicMaterial {
    constructor(parameters = {}) {
        super(parameters);
        this.setValues(parameters);
    }

    onBeforeCompile = (shader) => {
        shader.fragmentShader = shader.fragmentShader.replace(
            '#include ',
            /* glsl */ `#include 
            if (!gl_FrontFacing) {
            vec3 blackCol = vec3(0.0);
            diffuseColor.rgb = mix(diffuseColor.rgb, blackCol, 0.7);
            }
            `
        );
    };
}

extend({ MeshImageMaterial });

Billboard.jsx

'use client';

import * as THREE from 'three';
import { useRef } from 'react';
import { useFrame } from '@react-three/fiber';
import '@/webgl/materials/MeshImageMaterial';

function setupCylinderTextureMapping(texture, dimensions, radius, height) {
    const cylinderCircumference = 2 * Math.PI * radius;
    const cylinderHeight = height;
    const cylinderAspectRatio = cylinderCircumference / cylinderHeight;

    if (dimensions.aspectRatio > cylinderAspectRatio) {
        // Canvas is wider than cylinder proportionally
        texture.repeat.x = cylinderAspectRatio / dimensions.aspectRatio;
        texture.repeat.y = 1;
        texture.offset.x = (1 - texture.repeat.x) / 2;
    } else {
        // Canvas is taller than cylinder proportionally
        texture.repeat.x = 1;
        texture.repeat.y = dimensions.aspectRatio / cylinderAspectRatio;
    }

    // Center the texture
    texture.offset.y = (1 - texture.repeat.y) / 2;
}

function Billboard({ texture, dimensions, radius = 5, ...props }) {
    const ref = useRef(null);

    setupCylinderTextureMapping(texture, dimensions, radius, 2);

    useFrame((state, delta) => {
        if (texture) texture.offset.x += delta * 0.001;
    });

    return (
        
            
            
        
    );
}

export default Billboard;

And now we have our images moving around cylinders. Next, we’ll focus on banners (or marquees, whatever you prefer).

Adding Texture to the Banner

The last thing we need to fix is our Banner component. I wrapped it with this texture. Feel free to take it and edit it however you want, but remember to keep the proper dimensions of the texture.

We simply import our texture using the useTexture hook, map it onto our material, and animate the texture offset just like we did in our Billboard component.

Billboard.jsx

'use client';

import * as THREE from 'three';
import bannerTexture from '@/assets/images/banner.jpg';
import { useTexture } from '@react-three/drei';
import { useFrame } from '@react-three/fiber';
import { useRef } from 'react';

function Banner({ radius = 1.6, ...props }) {
    const ref = useRef(null);

    const texture = useTexture(bannerTexture.src);
    texture.wrapS = texture.wrapT = THREE.RepeatWrapping;

    useFrame((state, delta) => {
        if (!ref.current) return;
        const material = ref.current.material;
        if (material.map) material.map.offset.x += delta / 30;
    });

    return (
        
            
            
        
    );
}

export default Banner;

Nice! Now we have something cool, but I think it would look even cooler if we replaced the backface with something different. Maybe a gradient? For this, I created another extension of MeshBasicMaterial called MeshBannerMaterial. As you probably guessed, we just put a gradient on the backface. That’s it! Let’s use it in our Banner component.

We replace the MeshBasicMaterial with MeshBannerMaterial and now it looks like this!

MeshBannerMaterial.js

import * as THREE from 'three';
import { extend } from '@react-three/fiber';

export class MeshBannerMaterial extends THREE.MeshBasicMaterial {
    constructor(parameters = {}) {
        super(parameters);
        this.setValues(parameters);

        this.backfaceRepeatX = 1.0;

        if (parameters.backfaceRepeatX !== undefined)

        this.backfaceRepeatX = parameters.backfaceRepeatX;
    }

    onBeforeCompile = (shader) => {
        shader.uniforms.repeatX = { value: this.backfaceRepeatX * 0.1 };
        shader.fragmentShader = shader.fragmentShader
        .replace(
            '#include ',
            /* glsl */ `#include 
            uniform float repeatX;

            vec3 pal( in float t, in vec3 a, in vec3 b, in vec3 c, in vec3 d ) {
                return a + b*cos( 6.28318*(c*t+d) );
            }
            `
        )
        .replace(
            '#include ',
            /* glsl */ `#include 
            if (!gl_FrontFacing) {
            diffuseColor.rgb = pal(vMapUv.x * repeatX, vec3(0.5,0.5,0.5),vec3(0.5,0.5,0.5),vec3(1.0,1.0,1.0),vec3(0.0,0.10,0.20) );
            }
            `
        );
    };
}

extend({ MeshBannerMaterial });

Banner.jsx

'use client';

import * as THREE from 'three';
import bannerTexture from '@/assets/images/banner.jpg';
import { useTexture } from '@react-three/drei';
import { useFrame } from '@react-three/fiber';
import { useRef } from 'react';
import '@/webgl/materials/MeshBannerMaterial';

function Banner({ radius = 1.6, ...props }) {
const ref = useRef(null);

const texture = useTexture(bannerTexture.src);

texture.wrapS = texture.wrapT = THREE.RepeatWrapping;

useFrame((state, delta) => {
    if (!ref.current) return;

    const material = ref.current.material;

    if (material.map) material.map.offset.x += delta / 30;
});

return (
    
        
        
    
);
}

export default Banner;

And now we have it ✨

Check out the demo

You can experiment with this method in lots of ways. For example, I created 2 more examples with shapes I made in Blender, and mapped canvas textures on them. You can check them out here:

Final Words

Check out the final versions of all demos:

I hope you enjoyed this tutorial and learned something new!

Feel free to check out the source code for more details!

Tags: AnimationsCreateimageKineticReactThreeFiber
Admin

Admin

Next Post
‘Honkai Star Rail’ Model 2.5 “Flying Aureus Shot to Lupine Rue” Replace Releases on September tenth, New Trailer Showcased Throughout Livestream – TouchArcade

‘Honkai Star Rail’ Model 2.5 “Flying Aureus Shot to Lupine Rue” Replace Releases on September tenth, New Trailer Showcased Throughout Livestream – TouchArcade

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Recommended.

Managing Cybersecurity Dangers within the Age of AI

Managing Cybersecurity Dangers within the Age of AI

April 25, 2025
Comp AI secures $2.6M pre-seed to disrupt SOC 2 market

Comp AI secures $2.6M pre-seed to disrupt SOC 2 market

August 1, 2025

Trending.

How you can open the Antechamber and all lever places in Blue Prince

How you can open the Antechamber and all lever places in Blue Prince

April 14, 2025
ManageEngine Trade Reporter Plus Vulnerability Allows Distant Code Execution

ManageEngine Trade Reporter Plus Vulnerability Allows Distant Code Execution

June 10, 2025
Expedition 33 Guides, Codex, and Construct Planner

Expedition 33 Guides, Codex, and Construct Planner

April 26, 2025
Important SAP Exploit, AI-Powered Phishing, Main Breaches, New CVEs & Extra

Important SAP Exploit, AI-Powered Phishing, Main Breaches, New CVEs & Extra

April 28, 2025
7 Finest EOR Platforms for Software program Firms in 2025

7 Finest EOR Platforms for Software program Firms in 2025

June 18, 2025

AimactGrow

Welcome to AimactGrow, your ultimate source for all things technology! Our mission is to provide insightful, up-to-date content on the latest advancements in technology, coding, gaming, digital marketing, SEO, cybersecurity, and artificial intelligence (AI).

Categories

  • AI
  • Coding
  • Cybersecurity
  • Digital marketing
  • Gaming
  • SEO
  • Technology

Recent News

10 Movies To Watch After Enjoying Dying Stranding 2

10 Movies To Watch After Enjoying Dying Stranding 2

August 3, 2025
TacticAI: an AI assistant for soccer techniques

TacticAI: an AI assistant for soccer techniques

August 3, 2025
  • About Us
  • Privacy Policy
  • Disclaimer
  • Contact Us

© 2025 https://blog.aimactgrow.com/ - All Rights Reserved

No Result
View All Result
  • Home
  • Technology
  • AI
  • SEO
  • Coding
  • Gaming
  • Cybersecurity
  • Digital marketing

© 2025 https://blog.aimactgrow.com/ - All Rights Reserved