Приветствую! Я Алексей, 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 и шейдеры, рекомендую вначале ознакомиться с этой технологией, а после вернуться к статье.
![](https://habrastorage.org/getpro/habr/upload_files/d7c/980/221/d7c9802219f73ec39bf823cc88a609ef.png)
Как это будет работать на базовом уровне
У нас есть HTML‑страница со скрытыми картинками. Блок canvas абсолютно позиционирован относительно body и находится за HTML‑страницей. Внутри сцены расположены плоскости, на которых будут отображаться наши картинки (те же самые, что и на HTML‑странице). Текстуры картинок будут реализованы с помощью вершинных и пиксельных шейдеров, благодаря этому мы сможем добавлять различные эффекты при наведении мыши. Для получения размеров и координат картинок мы вызовем функцию getBoundingClientRect и применим эти данные к нашим плоскостям в сцене, что позволит разместить их соответствующим образом и сделать их такого же размера, как и картинки. При прокрутке также будем получать значение скролла и двигать наши плоскости вверх.
![](https://habrastorage.org/getpro/habr/upload_files/1ca/fdf/4b1/1cafdf4b16636d49d15f14a2e75a6796.png)
Настройка проекта
Для сборки я буду использовать 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()]
});
Согласно этой библиотеке, структура проекта должна выглядеть следующим образом:
![Структура проекта Структура проекта](https://habrastorage.org/getpro/habr/upload_files/15a/910/4b6/15a9104b60a6e8c8f29d453fff07f249.jpg)
Не забудьте поменять путь до файла main.js в файле index.html, который должен находиться в папке src.
Базовые настройки сцены
В нашей сцене будет установлена перспективная камера. Чтобы камера захватывала всю ширину и высоту экрана, рассчитываем угол обзора fov:
![](https://habrastorage.org/getpro/habr/upload_files/dde/02b/afb/dde02bafb7eca3067c15bfa58f342f29.jpg)
А теперь небольшой урок школьной тригонометрии :) Камера расположена на расстоянии 1000 px от плоскости (на рисунке показана как canvas), на которой будут располагаться наши картинки. Арктангенс fov/2 будет равен (отношение противолежащего катета к прилежащему) window.innerHeight / 2 / perspective. Чтобы получить угол в градусах, умножим удвоенное значение на 180 и разделим на PI:
![](https://habrastorage.org/getpro/habr/upload_files/6c6/0b7/641/6c60b7641d1e27cfb61cb000a73a80ff.jpg)
Если вы будете делать свой проект из моего репозитория на 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 — рекурсивная функция, которая будет запускать саму себя для обновления рендера сцены.
Страница должны выглядеть так:
![](https://habrastorage.org/getpro/habr/upload_files/0c8/82f/094/0c882f094859a654e04ce7596e9438d3.jpg)
Замена картинок на 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 — обновляет координаты и размеры картинок и устанавливает соответствующие размеры и координаты плоскости.
Должно получиться так:
![](https://habrastorage.org/getpro/habr/upload_files/0fc/ad1/3d2/0fcad13d2dcb41e4067e0a2a51343b57.gif)
При прокрутке страницы можно заметить, что наши плоскости немного запаздывают за картинками (при прокрутке за картинками видны наши зеленые плоскости). На анимации я сделал картинки видимыми (opacity: 1), чтобы эффект был лучше виден. Нужно сделать плавную прокрутку на странице, чтобы избавиться от этого.
![](https://habrastorage.org/getpro/habr/upload_files/624/a81/ecb/624a81ecbf7a20637f4871fd6329b66c.gif)
Добавляем плавную прокрутку на страницу
При скролле значения прокрутки изменяются не последовательно 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
![](https://habrastorage.org/getpro/habr/upload_files/f1d/11f/f9f/f1d11ff9f55013831e466b82f043c2c3.jpg)
файл:src/assets/style.scss
![](https://habrastorage.org/getpro/habr/upload_files/a4a/f9b/646/a4af9b64698fdaa750864f6506c8d1ee.jpg)
Отображение картинок с помощью шейдеров
Добавим шейдеры которые будут отвечать нашу анимацию. Создадим файл 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;
![](https://habrastorage.org/getpro/habr/upload_files/df5/f4a/1d0/df5f4a1d0fb254441d66bcc1ff5af1ce.jpg)
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 не совпадает, то возвращаем параметры шейдера в первоначальное состояние;
Наглядный пример анимации без линейной интерполяции, если бы мы сразу присваивали значение напрямую и плавное завершение анимации с функцией линейной интерполяции.
Без линейной интерполяции:
![](https://habrastorage.org/getpro/habr/upload_files/613/930/d12/613930d123f35534e315e12f2c5b6825.gif)
С линейной интерполяцией:
![](https://habrastorage.org/getpro/habr/upload_files/b3d/9b3/9ee/b3d9b39ee92f6642f330b3c161c60cc3.gif)
Заключение
В этой статье мы рассмотрели пример встраивания WebGL в HTML‑страницу, с синхронизацией по прокрутке. Картинки заменены на плоскости Three.js, мы также можем управлять расположением наших картинок, используя CSS (посмотрите, как представленное демо работает на мобильной версии экрана). Благодаря использованию 3D‑графики и шейдеров можно делать различные анимации, что дает больше возможностей для творческой реализации. Единственное ограничение — это ваше воображение и уровень мастерства:)
В качестве самостоятельного домашнего задания можете сделать курсор, который следует за движением мыши вот так:
![](https://habrastorage.org/getpro/habr/upload_files/eea/9e5/f9d/eea9e5f9d9100cb853ec9f5d2dcda4a7.gif)
Спасибо за внимание!
Полезные материалы о frontend-разработке мы также публикуем в наших соцсетях – ВКонтакте и Telegram.
politon
И так же слушаем, как скрипят подшипники взлетающего кулера ))
Имхо. Рановато для внедрения в web. Еще относительно мала доля использования 8-ядерных процессоров. 4 ядра будут на пределе, про два вообще моветон))
SimbirSoft_frontend Автор
Добрый день! Производительность Tree.js-приложений больше зависит от видеокарты, основные вычисления производятся именно там. Это нормально, что кулер начинает шуметь — нагрузка немного возрастает, поэтому веб-страница при открытии напоминает видеоигру. В идеале необходимо тестировать WebGL-страницы на производительность — ее можно оптимизировать настройками. Для слабых компьютеров можно отключить совсем. Статистика использования Three.js растет с каждым годом. Также предлагаю посмотреть другие сайты, использующие Three.js.
Статистика загрузок насчитывает порядка миллиона в неделю. И это только Three.js, но существует множество других библиотек. Для сравнения у Vue 3-4 миллиона загрузок в неделю, React — 15-17 миллионов, но эти два фреймворка общего назначения. На мой взгляд, использование WebGl в веб-разработке будет расти с каждым годом по мере подготовки специалистов и роста производительности компьютеров.