Как стать автором
Обновить
81.65
SimbirSoft
Лидер в разработке современных ИТ-решений на заказ

Встраивание WebGL в HTML-страницу с помощью Three.JS

Уровень сложностиСложный
Время на прочтение15 мин
Количество просмотров5.5K

demo, github 

Приветствую! Я Алексей, frontend‑разработчик в SimbirSoft. Вы, возможно, видели потрясающие веб‑сайты, представленные на www.awwwards.com. Это онлайн‑каталог лучших веб‑сайтов, который оценивает и награждает творческие и инновационные работы веб‑дизайнеров и разработчиков. На этом сайте можно найти множество примеров креативного веб‑дизайна, анимаций и визуальных эффектов. Такие удивительные анимации обычно разрабатываются с использованием WebGL. Эта технология позволяет более свободно и творчески подходить к созданию впечатляющих визуальных эффектов без ущерба для производительности. Для работы с WebGL используются такие библиотеки, как Three.js, PIXIJS или BABYLON, которые также популярны при создании игр.

В данной статье мы рассмотрим совмещение WebGL‑анимации с прокруткой страницы HTML, используя библиотеку Three.js. Работа с ней во многом схожа с работой 3D‑редактора (3ds Max, Maya, Blender и т. д.). Для получения результата в виде картинки или анимации необходимо создать сцену, поместить в нее камеру, создать примитив (геометрию или 3D‑модель), создать источник освещения и запустить процесс рендеринга.

Эта статья будет полезна middle и senior фронтенд‑разработчикам, которые хотят ознакомиться с Three. В статье очень мало теории и вводных материалов, акцент сделан на практической части. Если вы совсем не знаете, как работает Three.js и шейдеры, рекомендую вначале ознакомиться с этой технологией, а после вернуться к статье.

Как это будет работать на базовом уровне

У нас есть HTML‑страница со скрытыми картинками. Блок canvas абсолютно позиционирован относительно body и находится за HTML‑страницей. Внутри сцены расположены плоскости, на которых будут отображаться наши картинки (те же самые, что и на HTML‑странице). Текстуры картинок будут реализованы с помощью вершинных и пиксельных шейдеров, благодаря этому мы сможем добавлять различные эффекты при наведении мыши. Для получения размеров и координат картинок мы вызовем функцию getBoundingClientRect и применим эти данные к нашим плоскостям в сцене, что позволит разместить их соответствующим образом и сделать их такого же размера, как и картинки. При прокрутке также будем получать значение скролла и двигать наши плоскости вверх.

Настройка проекта

Для сборки я буду использовать vite. Как установить vite, можно почитать тут. Команда 

npm create vite@latest app --template vanilla

создаст базовый шаблон для разработки на JavaScript. Сразу установим Three.js, выполнив команду:

npm install three

Чтобы свободно импортировать шейдеры, необходимо установить библиотеку vite-plugin-glsl, устанавливаем библиотеку с помощью команды:

npm i vite-plugin-glsl --save-dev

Теперь создаем файл vite.config.js и прописываем там конфиг:

// vite.config.js

import glsl from 'vite-plugin-glsl';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [glsl()]
});

Согласно этой библиотеке, структура проекта должна выглядеть следующим образом:

Структура проекта
Структура проекта

Не забудьте поменять путь до файла main.js в файле index.html, который должен находиться в папке src.

Базовые настройки сцены

В нашей сцене будет установлена перспективная камера. Чтобы камера захватывала всю ширину и высоту экрана, рассчитываем угол обзора fov:

А теперь небольшой урок школьной тригонометрии :) Камера расположена на расстоянии 1000 px от плоскости (на рисунке показана как canvas), на которой будут располагаться наши картинки. Арктангенс fov/2 будет равен (отношение противолежащего катета к прилежащему) window.innerHeight / 2 / perspective. Чтобы получить угол в градусах, умножим удвоенное значение на 180 и разделим на PI:

Если вы будете делать свой проект из моего репозитория на github, то вам следует закомментировать класс main в css, чтобы в дальнейшем все совпадало.

Изначально скроем все наши картинки, установив им значение непрозрачности (opacity: 0). Для тега canvas зададим следующие стили:

Создадим класс Sketch и вызовем экземпляр этого класса: 

файл:src/main.js

import "/src/assets/style.scss";
import * as THREE from 'three';


class Sketch {
    constructor() {
        this.body = document.querySelector('body');
        this.createScene();
        this.createCamera();
        this.createMesh();
        this.initRenderer();
        this.render();
    }


    get viewport() {
        const width = window.innerWidth;
        const height = window.innerHeight;
        const aspectRatio = width / height;
        return { width, height, aspectRatio };
    }


    createScene() {
        this.scene = new THREE.Scene();
    }


    createCamera() {
        const perspective = 1000;
        const fov = (180 * (2 * Math.atan(window.innerHeight / 2 / perspective))) / Math.PI;
        this.camera = new THREE.PerspectiveCamera(fov, this.viewport.aspectRatio, 1, 1000)
        this.camera.position.set(0, 0, perspective);
    }


    createMesh() {
        const geometry = new THREE.PlaneGeometry( 250, 250, 10, 10 );
        const material = new THREE.MeshBasicMaterial({ color: 0x00ff00, wireframe: true });
        const mesh = new THREE.Mesh(geometry, material);
        this.scene.add(mesh);
    }


    onWindowResize() {        
        this.camera.aspect = this.viewport.aspectRatio;
        this.createCamera();
        this.camera.updateProjectionMatrix();
        this.renderer.setSize(this.viewport.width, this.viewport.height);
    }


    initRenderer() {
        this.renderer = new THREE.WebGL1Renderer({ antialias: true, alpha: true });
        this.renderer.setSize(this.viewport.width, this.viewport.height);
        this.renderer.setPixelRatio(window.devicePixelRatio);
        this.body.appendChild(this.renderer.domElement);
    }


    render() {
        this.renderer.render(this.scene, this.camera);
        requestAnimationFrame(this.render.bind(this));
    }
}


new Sketch();

Описание методов класса Sketch:

viewport — получаем размеры и соотношение сторон экрана;

createScene — создаем сцену;

createCamera — создаем камеру с расчетными параметрами;

createMesh — создаем материал, геометрическую плоскость и добавляем ее в нашу сцену;

THREE.PlaneGeometry (250, 250, 10, 10) — задаем размер плоскости 250×250 и 10×10 размер полигональной сетки деления плоскости по горизонтали и вертикали;

THREE.MeshBasicMaterial({ color: 0×00ff00, wireframe: true }) — задаем цвет плоскости в формате Hex Color. wireframe: true — отображение плоскости в виде полигональной сетки (позже мы это исправим);

onWindowResize — функция для обновления параметров рендера при изменении размеров экрана;

initRenderer — инициализация рендера, при изменении размеров экрана будет вызывать функцию onWindowResize;

setPixelRatio — устанавливает соотношение пикселей устройства, чтобы предотвратить размытие выходного холста. Возможно, на устройствах mac retina это значение нужно будет изменить, чтобы анимация не тормозила.

render — рекурсивная функция, которая будет запускать саму себя для обновления рендера сцены.

Страница должны выглядеть так:

Замена картинок на Three.js плоскости

Для начала доработаем наш класс Sketch.

файл: src/main.js

сlass Sketch{
    constructor(){
        this.body = document.querySelector('body');

        this.images = [...document.querySelectorAll('img')];
        this.meshItems = [];

        this.createScene();
        this.createCamera();
        this.createMesh();
        this.initRenderer();
        this.render();

    }

    initRenderer() {
        window.addEventListener('resize', this.onWindowResize.bind(this), false);
        this.renderer = new THREE.WebGL1Renderer({ antialias: true, alpha: true });
        this.renderer.setSize(this.viewport.width, this.viewport.height);
        this.renderer.setPixelRatio(window.devicePixelRatio);
        this.body.appendChild(this.renderer.domElement);
    }
	...
    createMesh() {
        // const geometry = new THREE.PlaneGeometry( 250, 250, 10, 10 );
        // const material = new THREE.MeshBasicMaterial({ color: 0x00ff00, wireframe: true });
        // const mesh = new THREE.Mesh(geometry, material);
        // this.scene.add(mesh);
        this.images.map((image) => {
            const meshItem = new MeshItem(image, this.scene);
            this.meshItems.push(meshItem);
        })
    }
     ...
    render(){
        for(let i = 0; i < this.meshItems.length; i++){
            this.meshItems[i].render();
        }
        this.renderer.render(this.scene, this.camera);
        requestAnimationFrame(this.render.bind(this));
    }
}

this.images — получаем массив картинок;

this.meshItems — изначально равен пустому массиву;

В методе createMesh удалим ранее существовавший код и вставим цикл, который будет обходить массив изображений. Создадим экземпляр класса MeshItem для каждой фотографии и разместим его в массиве this.meshItems. Присвоим каждой картинке уникальный id, соответствующий ее индексу в массиве. Для метода render добавим цикл, который будет проходить по массиву изображений и делать их перерендер на каждом этапе requestAnimationFrame;

Все остальные методы в классе Sketch останутся без изменений.

Далее создадим класс MeshItem:

файл: src/main.js

class MeshItem{
    constructor(element, scene){
        this.element = element;
        this.scene = scene;
        this.offset = new THREE.Vector2(0,0);
        this.sizes = new THREE.Vector2(0,0);
        this.createMesh();
    }

    getDimensions(){
        const {width, height, top, left} = this.element.getBoundingClientRect();
        this.sizes.set(width, height);
        this.offset.set(left - window.innerWidth / 2 + width / 2, -top + window.innerHeight / 2 - height / 2);
    }

    createMesh(){
        const geometry = new THREE.PlaneGeometry( 1, 1, 10, 10 );
        const material = new THREE.MeshBasicMaterial({ color: 0x00ff00, wireframe: true });
        this.mesh = new THREE.Mesh(geometry, material);
        this.scene.add(this.mesh);
    }

    render(){
        this.getDimensions();
        this.mesh.position.set(this.offset.x, this.offset.y, 0);
        this.mesh.scale.set(this.sizes.x, this.sizes.y, 1);
    }
}

THREE.Vector2 — класс, представляющий двумерный вектор. Точка в 2D пространстве (то есть положение на плоскости) или размер плоскости, как у нас. Экземпляр Vector2 создаст объект с полями (x, y), зададим им начальные значения (0,0);

this.offset — координаты расположения плоскости;

this.sizes — размер плоскости;

getDimensions — задает размеры и координаты плоскостям в соответствии с размерами и координатами картинок. В THREE JS координаты расположения объектов задается не как в браузере от верхнего левого угла, а от координат x=0, y=0, z=0 (соответствующих центру экрана в нашем случае) до центральной точки объекта.

render — обновляет координаты и размеры картинок и устанавливает соответствующие размеры и координаты плоскости.

Должно получиться так:

При прокрутке страницы можно заметить, что наши плоскости немного запаздывают за картинками (при прокрутке за картинками видны наши зеленые плоскости). На анимации я сделал картинки видимыми (opacity: 1), чтобы эффект был лучше виден. Нужно сделать плавную прокрутку на странице, чтобы избавиться от этого.

Добавляем плавную прокрутку на страницу

При скролле значения прокрутки изменяются не последовательно 1,2,3,4,5, а с пропуском некоторых тактов для увеличения производительности. Из-за этого в нашей анимации плоскости не всегда успевают за картинками. 

Плавный скролл можно сделать по-разному, я сделаю через свойство transform: translate3d.

файл: src/main.js

import "/src/assets/style.scss";
import * as THREE from 'three';


const lerp = (start, end, t) => {
    return start * (1 - t) + end * t;
};

class Sketch {
    constructor() {
        this.body = document.querySelector('body');


        this.images = [...document.querySelectorAll('img')];
        this.meshItems = [];


        this.scrollable = document.querySelector(".smooth-scroll");
        this.current = 0;
        this.target = 0;
        this.ease = 0.065;


        this.createScene();
        this.createCamera();
        this.createMesh();
        this.initRenderer();
        this.render();
    }


    get viewport() {
        const width = window.innerWidth;
        const height = window.innerHeight;
        const aspectRatio = width / height;
        return { width, height, aspectRatio };
    }


    smoothScroll = () => {
        this.target = window.scrollY;
        this.current = lerp(this.current, this.target, this.ease);
        this.scrollable.style.transform = `translate3d(0,${-this.current}px, 0)`;
    };


    createScene() {
        this.scene = new THREE.Scene();
    }


    createCamera() {
        const perspective = 1000;
        const fov = (180 * (2 * Math.atan(window.innerHeight / 2 / perspective))) / Math.PI;
        this.camera = new THREE.PerspectiveCamera(fov, this.viewport.aspectRatio, 1, 1000)
        this.camera.position.set(0, 0, perspective);
    }


    createMesh() {
        this.images.map((image) => {
            const meshItem = new MeshItem(image, this.scene);
            this.meshItems.push(meshItem);
        })
        document.body.style.height = `${this.scrollable.getBoundingClientRect().height}px`;
    }


    onWindowResize() {
        document.body.style.height = `${this.scrollable.getBoundingClientRect().height}px`;
        this.camera.aspect = this.viewport.aspectRatio;
        this.createCamera();
        this.camera.updateProjectionMatrix();
        this.renderer.setSize(this.viewport.width, this.viewport.height);
    }


    initRenderer() {
        window.addEventListener('resize', this.onWindowResize.bind(this), false);
        this.renderer = new THREE.WebGL1Renderer({ antialias: true, alpha: true });
        this.renderer.setSize(this.viewport.width, this.viewport.height);
        this.renderer.setPixelRatio(window.devicePixelRatio);
        this.body.appendChild(this.renderer.domElement);
    }


    render() {
        this.smoothScroll();
        for(let i = 0; i < this.meshItems.length; i++){
            this.meshItems[i].render();
        }
        this.renderer.render(this.scene, this.camera);
        requestAnimationFrame(this.render.bind(this));
    }
}




new Sketch();

С помощью translate3d будем сдвигать блок с классом smooth‑scroll вверх.

this.target — количество пикселей в документе, которые были пролистаны на данный момент от начальной позиции;

lerp — функция линейной интерполяции;

this.current — текущее значение прокрутки;

this.ease — коэффициент линейной интерполяции для прокрутки;

this.scrollable.getBoundingClientRect().height — будет задавать значение высоты тегу body равное значению высоты контента.

CSS свойства, которые прописаны для блоков html, body, main, scrollable тоже влияют на качество и плавность прокрутки. В HTML-документе это выглядит так:

файл: index.html

файл:src/assets/style.scss

Отображение картинок с помощью шейдеров

Добавим шейдеры которые будут отвечать нашу анимацию. Создадим файл vertex.glsl в папке glsl:

файл:src/glsl/vertex.glsl

uniform sampler2D uTexture;
uniform vec2 uOffset;
uniform vec2 u_mouse;
uniform float u_time;
varying vec2 vUv;

#define PI 3.14

vec3 deformationCurve(vec3 position, vec2 uv, vec2 offset) {
   position.y = position.y + (sin(uv.x * PI) * offset.y);
   return position;
}

void main() {
   vUv = uv;
   float noise = 1. - sin(4. * uv.x + u_mouse.x * 90.) / 30.;
   vec3 newPosition = deformationCurve(position, uv , uOffset + noise * 0.02 * sin(u_time));
   gl_Position = projectionMatrix * modelViewMatrix * vec4( newPosition, 1.0 );
}

Создадим файл fragment.gls lв папке glsl:

файл:src/glsl/fragment.glsl

uniform sampler2D uTexture;
uniform float uAlpha;
uniform vec2 uOffset;
varying vec2 vUv;
uniform vec2 u_mouse;
uniform float u_time;

vec3 rgbShift(sampler2D textureImage, vec2 uv, vec2 offset) {

   float noise = 1. - sin(2. * sin(uv.x * u_mouse.x) * 20. + u_time * 0.6) / 30.;
   vec3 rgb = texture2D(textureImage, uv * noise).rgb;
   rgb.b = texture2D(textureImage, uv * noise + sin(u_mouse.x * u_time - u_time * 0.3)*0.015).b;
   rgb.r = texture2D(textureImage, uv * noise - sin(u_mouse.x * u_time + u_time * 0.3)*-0.02).r;
   return vec3(rgb);
}

void main() {
   vec3 color = rgbShift(uTexture, vUv, uOffset);
   gl_FragColor = vec4(color, uAlpha);
}

Импортируем шейдеры:

файл: src/main.js

import "/src/assets/style.scss";


import * as THREE from 'three';
import vertex from './glsl/vertex.glsl';
import fragment from './glsl/fragment.glsl';

В этой статье не будет описания работы с шейдерами. Тема достаточно объемная и в рамках одной статьи не получится сделать это качественно. Для изучения шейдеров можно воспользоваться The Book of Shaders – это пошаговое руководство по абстрактной и сложной вселенной фрагментных шейдеров. 

Создадим новый материал для нашей плоскости:

файл:src/main.js класс:MeshItem

    createMesh() {
        const geometry = new THREE.PlaneGeometry(1, 1, 10, 10);
        const imageTexture = new THREE.TextureLoader().load(this.element.src);
        imageTexture.minFilter = THREE.LinearFilter;
        this.uniforms = {
            uTexture: { value: imageTexture },
            uOffset: { value: new THREE.Vector2(0.0, 0.0) },
            uAlpha: { value: 1.0 },
            u_mouse: { type: "v2", value: new THREE.Vector2() },
            u_time: { type: "f", value: 0.0 },
        };


        const material = new THREE.ShaderMaterial({
            uniforms: this.uniforms,
            vertexShader: vertex,
            fragmentShader: fragment,
            transparent: true,
            //wireframe: true,
            side: THREE.DoubleSide
        })


        this.mesh = new THREE.Mesh(geometry, material);
        this.scene.add(this.mesh);
    }

imageTexture — асинхронно загружает нашу текстуру по указанному пути;

THREE.LinearFilter — нужен для правильного отображения асинхронно загружаемых текстур;

this.uniforms — специальное свойство Three.js, через которое можно передавать параметры в шейдеры;

uTexture — в это поле передается текстура;

uOffset — двумерный вектор, в который будем передавать параметры;

uAlpha — значение альфа канала. 0 — прозрачный, 1 — непрозрачный;

u_mouse — в это поле будут записываться координаты (x и y) положения мыши;

u_time — в это поле будет записываться значение счетчика времени.

Создадим новый материал и передадим в него параметры вершинных и вертексных шейдеров, значение юниформы, установим значение непрозрачности  и сделаем материал двухсторонним:

файл: src/main.js класс: Sketch

class Sketch {
     imageLoaded(url) {
        return new Promise(function (resolve, reject) {
            var img = new Image();
            img.onload = function () {
                resolve(url);
            };
            img.onerror = function () {
                reject(url);
            };
            img.src = url;
        });
    }


    createMesh() {
        const imagesLoaded = this.images.map((image) => {
            const meshItem = new MeshItem(image, this.scene);
            this.meshItems.push(meshItem);
            return this.imageLoaded(image.src);
        })


        Promise.all(imagesLoaded).then(() => {
            document.body.style.height = ${this.scrollable.getBoundingClientRect().height}px`;
        })
    }
}


new Sketch();

imageLoaded — проверяем, загружена ли картинка и возвращает промис;

Promise.all(imagesLoaded) — если все картинки загружены, то обновляем значение высоты body (если этого не сделать, то высота прокрутки рассчитается до загрузки картинок, и значение высоты будет меньше).

Делаем Hover-эффект при наведении курсора на картинку 

файл: src/main.js  класс: MeshItem

class Sketch {
    constructor() {
        this.body = document.querySelector('body');


        this.images = [...document.querySelectorAll('img')];
       
        this.scrollable = document.querySelector(".smooth-scroll");
        this.current = 0;
        this.target = 0;
        this.ease = 0.065;


        this.meshItems = [];
        this.planeItems = [];


        this.mouseCoordinates = new THREE.Vector2();
        this.raycaster = new THREE.Raycaster();
        this.selectMesh = null;
        this.onMouse();
        this.onTouchMove();


        this.createScene()
        this.createCamera();
        this.createMesh();
        this.initRenderer();
        this.render();        
    }


    onTouchMove() {
        document.addEventListener("touchmove", (event) => {
            const x = (event?.touches[0].clientX / window.innerWidth) * 2 - 1;
            const y = -(event?.touches[0].clientY / window.innerHeight) * 2 + 1;
            this.mouseCoordinates = { x, y: window.innerWidth > 450 ? y : 0. };
        })
    }


    onMouse() {
        document.addEventListener("mousemove", (event) => {
            const x = (event.clientX / window.innerWidth) * 2 - 1;
            const y = -(event.clientY / window.innerHeight) * 2 + 1;
            this.mouseCoordinates = { x, y }
        })
    }


    createMesh() {
        const imagesLoaded = this.images.map((image) => {
            const meshItem = new MeshItem(image, this.scene);
            this.meshItems.push(meshItem);
            return this.imageLoaded(image.src);
        })


        Promise.all(imagesLoaded).then(() => {
            document.body.style.height = ${this.scrollable.getBoundingClientRect().height}px`;
        })


        this.scene.traverse((item) => {
            if (item.isMesh) {
                this.planeItems.push(item);
            }
        })
    }


    render() {
        this.smoothScroll();
        this.raycaster.setFromCamera(this.mouseCoordinates, this.camera);
        const intersects = this.raycaster.intersectObjects(this.planeItems, true);
        if (intersects.length > 0) {
            this.selectMesh = intersects[0].object;
        } else {
            if (this.selectMesh !== null) {
                this.selectMesh = null;
            }
        }
        const velocity = (this.target - this.current);
        for (let i = 0; i < this.meshItems.length; i++) {
            this.meshItems[i].render(velocity, this.mouseCoordinates, this.selectMesh);
        }
        this.renderer.render(this.scene, this.camera);
        requestAnimationFrame(this.render.bind(this));
    }
}


new Sketch()

this.mouseCoordinates — двумерный вектор с координатами x, y, куда будут записываться координаты мыши;

Raycaster — специальный класс Three. Используется для выбора объектов в трехмерной сцене;

selectMesh — объект, на который наведена мышь;

onTouchMove, onMouse — функции, которые будут записывать координаты мыши или координаты движения пальца на мобильном в переменную this.mouseCoordinates. Но перед этим мы должны нормализовать координаты мыши в диапазоне от -1 до 1;

this.scene.traverse — будет проходить по массиву сцены и, если это геометрия, то будем ее записывать в this.planeItems;

this.raycaster.setFromCamera(this.mouseCoordinates, this.camera) — передаем координаты мыши и камеру в raycaster;

this.raycaster.intersectObjects — передаем массив с геометрией (this.planeItems) пересечение с которой нужно отслеживать. this.raycaster.intersectObjects вернет отсортированный массив с объектами сцены, которые пересеклись с мышью, начиная от самого ближнего к камере. В нулевой индекс запишется первый объект, который находился ближе к камере, и на который была наведена мышь.

Далее проверяем, есть ли пересечение, и записываем геометрию пересечения в переменную selectMesh. В противном случае обнуляем эту переменную.

this.meshItems[i].render(velocity, this.mouseCoordinates, this.selectMesh) – передаем скорость прокрутки, координаты мыши и выбранную геометрию в метод render-класса meshItems для обновления параметров шейдера:

файл: src/main.js  класс: MeshItem

class MeshItem {
    render(velocity = 0, mouseCoordinates, selectMesh) {
        this.getDimensions();
        this.mesh.position.set(this.offset.x, this.offset.y, 0);
        this.mesh.scale.set(this.sizes.x, this.sizes.y, 1);


        this.uniforms.uOffset.value.set(this.offset.x * 0.5, -(velocity) * 0.0003);


        if (this.mesh.uuid === selectMesh?.uuid) {
            this.uniforms.u_mouse.value.x = lerp(0.0, mouseCoordinates.x, 0.6);
            this.uniforms.u_mouse.value.y = lerp(0.0, mouseCoordinates.y, 0.6);
            this.uniforms.u_time.value += 0.05;
            return;
        }
        this.uniforms.u_mouse.value.x = lerp(this.uniforms.u_mouse.value.x, 0.0, 0.02);
        this.uniforms.u_mouse.value.y = lerp(this.uniforms.u_mouse.value.y, 0.0, 0.02);
        this.uniforms.u_time.value = lerp(this.uniforms.u_time.value, 0.0, 0.02);
    }
}

this.uniforms.uOffset.value.set(this.offset.x * 0.5, -(velocity) * 0.0003) — передаем скорость прокрутки в шейдер;

this.mesh.uuid === selectMesh?.uuid — проверяем, если выбранный uuid геометрии совпадает с uuid текущей геометрии, то обновляем параметры шейдера через функцию линейной интерполяции. Если uuid не совпадает, то возвращаем параметры шейдера в первоначальное состояние;

Наглядный пример анимации без линейной интерполяции, если бы мы сразу присваивали значение напрямую и плавное завершение анимации с функцией линейной интерполяции.

Без линейной интерполяции:

С линейной интерполяцией:

Заключение

В этой статье мы рассмотрели пример встраивания WebGL в HTML‑страницу, с синхронизацией по прокрутке. Картинки заменены на плоскости Three.js, мы также можем управлять расположением наших картинок, используя CSS (посмотрите, как представленное демо работает на мобильной версии экрана). Благодаря использованию 3D‑графики и шейдеров можно делать различные анимации, что дает больше возможностей для творческой реализации. Единственное ограничение — это ваше воображение и уровень мастерства:)

В качестве самостоятельного домашнего задания можете сделать курсор, который следует за движением мыши вот так:

Спасибо за внимание!

Полезные материалы о frontend-разработке мы также публикуем в наших соцсетях – ВКонтакте и Telegram.

Теги:
Хабы:
0
Комментарии2

Публикации

Информация

Сайт
www.simbirsoft.com
Дата регистрации
Дата основания
Численность
1 001–5 000 человек
Местоположение
Россия