Ahoj lidi, doufám, že se máte dobře! 🙂
Jsem zpět po zveřejnění druhé části této série o tom, jak začít s ThreeJS bez bolesti.
Pokud jste to ještě neudělali, můžete si první a druhý díl přečíst zde 👇🏼
Malá rekapitulace
Ve druhé části jsme viděli, jak animovat krychli, jak změnit její geometrii a jak změnit její materiál. Dostali jsme se k této krásné 3D animaci:
Konečný kód použitý k dosažení tohoto efektu je následující:
// script.js
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight);
camera.position.z = 3;
scene.add(camera);
const textureLoader = new THREE.TextureLoader();
const matcapTexture = textureLoader.load("https://bruno-simon.com/prismic/matcaps/3.png");
const geometry = new THREE.TorusKnotGeometry(0.5, 0.2, 200, 30);
const material = new THREE.MeshMatcapMaterial({ matcap: matcapTexture });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
const animate = function () {
window.requestAnimationFrame(animate);
mesh.rotation.x += 0.01;
mesh.rotation.y += 0.01;
renderer.render( scene, camera );
};
animate();
document.body.appendChild(renderer.domElement);
V této poslední části uvidíme, jak zajistit, aby naše plátno reagovalo, jak plynule animovat kameru a jak vložit nějaké HTML věci, aby byly mnohem reprezentativnější jako sekce nadpisů. Stránku upravíme tak, aby vypadala takto:https://th3wall-threejs.netlify.app
Nechte to reagovat
Pokud si v prohlížeči prohlédneme výsledek kódu poskytnutého v malé rekapitulaci zde, mohli bychom jasně vidět, že plátno nereaguje.
Jak to tedy můžeme udělat responzivní ?
Nejprve musíme přidat posluchač událostí v okně 'změnit velikost' metoda:
window.addEventListener('resize', () => {
})
Pak musíme zacházet s kamerou.
V našem posluchači událostí musíme aktualizovat aspekt kamery a uděláme to tak, že jí poskytneme poměr mezi vnitřní šířkou okna a vnitřní výškou:
//Update the camera
camera.aspect = window.innerWidth / window.innerHeight;
Pokaždé, když aktualizujeme parametr kamery, měli bychom jej sdělit kameře.
"updateProjectionMatrix " je funkcí PerspectiveCamera který aktualizuje projekční matici kamery. Musí být voláno po každé změně parametrů. (viz to v ThreeJS docS)
Takže na kameře nazýváme tuto metodu:
camera.updateProjectionMatrix();
Poslední věcí, kterou musíte udělat, je předat nové velikosti výřezů do rendereru:
renderer.setSize(window.innerWidth, window.innerHeight);
a máme hotovo! Nyní naše plátno plně reaguje a můžeme to ověřit změnou velikosti obrazovky.
Zde je úplná funkce posluchače událostí:
window.addEventListener('resize', () => {
//Update the camera
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
//Update the renderer
renderer.setSize(window.innerWidth, window.innerHeight);
})
Animace kamery založené na poloze myši
Nyní, když jsme našemu plátnu a objektu přidali schopnost reagovat, je čas vnést na scénu nějaké pohyby.
Uděláme klasickou animaci:pokud pohneme myší vlevo kamera se přesune doleva , pokud pohneme myší vpravo kamera se přesune doprava a totéž platí pro pohyby nahoru a dolů.
Nejprve musíme vědět, kde je kurzor .
Můžeme uložit pozici kurzoru v proměnné:
const cursor = { x: 0, y: 0 };
Kdykoli se myš pohne, zobrazí se x a y hodnoty kurzoru budou aktualizovány. Takže přidáme posluchač události na mousemove :
window.addEventListener('mousemove', event => {
// update cursor values
});
Uvnitř posluchače získáme pozici kurzoru pomocí vanilla JS, opravdu snadno. událost parametr obsahuje pozici kurzoru na X -osa a na Y -osa:
cursor.x = event.clientX;
cursor.y = event.clientY;
Zaznamenáním hodnot kurzoru můžeme vidět souřadnice, které jdou od 0 (vlevo nahoře) po maximální šířku a výšku výřezu (vpravo dole). Ale hodnoty, které chceme mít, jsou normalizované hodnoty, které jdou od 0 do 1 .
Toho dosáhneme vydělením hodnoty kurzoru aktuální šířkou/výškou výřezu :
cursor.x = event.clientX / window.innerWidth;
cursor.y = event.clientY / window.innerHeight;
Nyní, když máme hodnoty, které se pohybují od 0 do 1, můžeme přidat malý geniální trik od Bruna :odečteme 0,5 od každé hodnoty kurzoru .
cursor.x = event.clientX / window.innerWidth - 0.5;
cursor.y = event.clientY / window.innerHeight - 0.5;
Proč?
Protože tímto způsobem (můžete se podívat na graf níže) mít 0 ve středu , pozitivní hodnoty půjdou na +0,5 a negativní hodnoty půjdou na -0,5
Nyní, když jsme zakódovali aktualizaci pro hodnoty kurzoru, musíme současně pohybovat kamerou.
Uvnitř animace funkce, která se provádí pomocí requestAnimationFrame, uložíme hodnoty kurzoru do dvou proměnných:
const cameraX = cursor.x;
const cameraY = cursor.y;
Pozici kamery přiřadíme tyto dvě hodnoty:
camera.position.x = cameraX;
camera.position.y = cameraY;
Jak můžeme vidět při náhledu výsledku, kamera se pohybuje podivně, když se pohybujeme vertikálně. Pokud se posunu nahoru , kamera se přesune dolů a pokud se posunu dolů , kamera se posune nahoru .
To je způsobeno problémem na ose Y :
- v ThreeJS osa Y je kladná jdou nahoru;
- v event.clientY osa Y je kladná jde dolů;
Obvykle je osa Y kladná směrem nahoru, ale to může záviset na softwaru/technologii, kterou používáme.
Abych tuto nepříjemnost napravil, dám a - (mínus) uvnitř fotoaparátuY úkol:
const cameraX = cursor.x;
const cameraY = - cursor.y; // <-- This has changed
Nyní, když se podíváme na náhled, můžeme konečně vidět správný pohyb kamery na svislé ose
Přidejte do animací náběh
Nyní přidáme nějaké usnadnění k animacím:znovu vytvoříme slavnou lehkost animace.
Cílem je posunout X (nebo Y) směrem k cíli ne přímo k němu, ale pouze o 1/10 délky cíle. A opakováním výpočtu 1/10 na každém dalším snímku se 1/10 zmenšuje a zmenšuje... To reprodukuje klasickou animaci náběhu/doběhu.
Potřebujeme vypočítat delta mezi skutečnou polohou (kameraX/Y ) a cíl (camera.position.x/y ), pak toto delta číslo vydělíme 10.
To bude přidáno ke každému snímku k hodnotám polohy kamery.
Abychom tedy mohli použít tento výpočet, musíme upravit přiřazení polohy kamery takto:
camera.position.x += (cameraX - camera.position.x) / 10;
camera.position.y += (cameraY - camera.position.y) / 10;
Nyní si můžete vychutnat skutečnou hladkost!
Nastavení rozvržení
V tuto chvíli potřebujeme pouze nastavit HTML a CSS naší vstupní stránky.
Nejprve můžeme otevřít index.html soubor, který jsme vytvořili v první části.
Můžeme přidat název třídy "tři" na <body>
tag a v něm následující struktura:
<!-- index.html -->
<section class="content">
<h2 class="content__title">Hi, I'm Davide</h2>
<p class="content__subtitle">I'm a Front End Developer <br />I'm playing with ThreeJS for the very first time. </p>
<div class="content__link--wrp">
<a class="content__link" href="https://github.com/Th3Wall">
<svg class="content__link--icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewbox="0 0 55 56">
<g clip-path="url(#a)">
<path fill="#fff" fill-rule="evenodd" d="M27.5.5387C12.3063.5387 0 12.8449 0 28.0387c0 12.1687 7.8719 22.4469 18.8031 26.0906 1.375.2406 1.8907-.5844 1.8907-1.3062 0-.6532-.0344-2.8188-.0344-5.1219-6.9094 1.2719-8.6969-1.6844-9.2469-3.2313-.3094-.7906-1.65-3.2312-2.8187-3.8843-.9626-.5156-2.3376-1.7875-.0344-1.8219 2.1656-.0344 3.7125 1.9937 4.2281 2.8187 2.475 4.1594 6.4281 2.9907 8.0094 2.2688.2406-1.7875.9625-2.9906 1.7531-3.6781-6.1187-.6875-12.5125-3.0594-12.5125-13.5782 0-2.9906 1.0656-5.4656 2.8188-7.3906-.275-.6875-1.2375-3.5062.275-7.2875 0 0 2.3031-.7219 7.5625 2.8188 2.1999-.6188 4.5375-.9282 6.875-.9282 2.3374 0 4.675.3094 6.875.9282 5.2593-3.575 7.5625-2.8188 7.5625-2.8188 1.5125 3.7813.55 6.6.275 7.2875 1.7531 1.925 2.8187 4.3656 2.8187 7.3906 0 10.5532-6.4281 12.8907-12.5469 13.5782.9969.8593 1.8563 2.5093 1.8563 5.0875 0 3.6781-.0344 6.6344-.0344 7.5625 0 .7218.5156 1.5812 1.8906 1.3062A27.5454 27.5454 0 0 0 55 28.0387c0-15.1938-12.3062-27.5-27.5-27.5Z" clip-rule="evenodd"></path>
</g>
<defs>
<clippath id="a">
<path fill="#fff" d="M0 0h55v55H0z" transform="translate(0 .5387)"></path>
</clippath>
</defs>
</svg>
<span class="content__link--text">Th3Wall</span>
</a>
<a class="content__link" href="https://twitter.com/Th3Wall25">
<svg class="content__link--icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewbox="0 0 55 46">
<path fill="#fff" d="M54.8923 6.0116a22.9167 22.9167 0 0 1-6.474 1.776 11.3622 11.3622 0 0 0 4.9569-6.2402c-2.1794 1.272-4.5948 2.1978-7.166 2.7134a11.2752 11.2752 0 0 0-18.5074 3.0528 11.2754 11.2754 0 0 0-.706 7.2184C17.6229 14.0897 9.3202 9.5866 3.7583 2.785a11.0506 11.0506 0 0 0-1.5262 5.6718c0 3.9188 1.9937 7.3631 5.0141 9.3867a11.2384 11.2384 0 0 1-5.1058-1.4117v.1375a11.2821 11.2821 0 0 0 9.0429 11.0619 11.449 11.449 0 0 1-5.0691.1948 11.3113 11.3113 0 0 0 10.5508 7.8306 22.6124 22.6124 0 0 1-13.9837 4.824c-.8938 0-1.7853-.0527-2.6813-.1536a32.0718 32.0718 0 0 0 17.3181 5.0623c20.7465 0 32.0788-17.1783 32.0788-32.0489 0-.4813 0-.9625-.0344-1.4438A22.7684 22.7684 0 0 0 55 6.0574l-.1077-.0458Z"></path>
</svg>
<span class="content__link--text">Th3Wall25</span>
</a>
</div>
</section>
Nyní potřebujete stylingovou část:vložím sem css vygenerovaný z mého kódu SCSS. Musíte jej vložit do styles.css soubor:
/* --- styles.css --- */
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700;800;900&display=swap");
html {
font-size: 16px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: auto;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial,
sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
scroll-behavior: smooth;
}
body {
position: relative;
overflow-x: hidden;
margin: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-family: "Poppins", sans-serif;
font-size: 1rem;
font-weight: 400;
background-color: #fff;
color: #000;
text-align: center;
}
h1,
h2,
h3,
h4,
h5,
h6,
p {
margin: 0;
}
.three {
position: relative;
overflow: hidden;
width: 100vw;
min-height: 100vh;
height: 100%;
}
.three .content {
position: absolute;
top: 50%;
left: 5%;
transform: translateY(-50%);
margin-top: 1rem;
display: flex;
flex-direction: column;
justify-content: center;
text-align: left;
mix-blend-mode: difference;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.three .content__title {
font-size: 26px;
font-weight: 800;
background: linear-gradient(270deg, #ffb04f 40%, #ff8961, #ff50b8, #cb5eee);
color: #9d8eee;
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
-webkit-box-decoration-break: clone;
}
.three .content__subtitle {
margin-bottom: 1.5rem;
font-size: 14px;
color: #fff;
}
.three .content__link {
display: inline-flex;
align-items: center;
color: inherit;
}
.three .content__link:last-child {
margin-left: 1rem;
}
.three .content__link:hover .content__link--icon,
.three .content__link:hover .content__link--text {
opacity: 0.65;
transform: translateX(5px);
}
.three .content__link--wrp {
display: flex;
align-items: center;
}
.three .content__link--icon {
width: 100%;
max-width: 1.5rem;
transition: all 0.4s cubic-bezier(0.6, -0.05, 0.01, 0.99);
}
.three .content__link--text {
margin-left: 0.5rem;
display: block;
text-decoration: underline;
font-size: 14px;
color: #fff;
transition: all 0.4s cubic-bezier(0.6, -0.05, 0.01, 0.99);
}
@media (min-width: 768px) {
.three .content__title {
letter-spacing: -0.1rem;
}
.three .content__link:last-child {
margin-left: 2rem;
}
.three .content__link--icon {
max-width: 2.5rem;
}
.three .content__link--text {
margin-left: 1rem;
font-size: 16px;
}
}
@media (min-width: 1450px) {
.three .content__title {
font-size: 62px;
}
.three .content__subtitle {
font-size: 28px;
}
.three .content__link--text {
font-size: 22px;
}
}
Jakmile bude vše na svém místě, měli bychom mít výsledek, který vypadá takto:
Jak vidíme, objekt je vycentrován a mnohem lépe by seděl vpravo, aby se nekřížil s textem vlevo.
Abychom ji mohli přesunout, musíme upravit cameraX uvnitř funkce animace:
const cameraX = cursor.x; //Before
const cameraX = cursor.x -1; //After
Protože jsme chtěli posunout objekt vpravo, odečetli jsme 1 do kamery, takže bude mít vždy offset 1.
Přidání sekvenovaných vstupů pomocí GSAP
Jsme na samém konci a jako konec chceme pomocí GSAP animovat vstup prvků na stránce.
Abychom mohli animovat náš plovoucí objekt, musíme změnit způsob připevnění plátna k tělu .
V tuto chvíli je plátno automaticky připojeno k tělu pomocí ThreeJS, ale potřebujeme animovat prvek plátna při načítání, takže jej musíme mít na stránce při načítání.
Uvnitř souboru index.html vedle <section class="content">
vloženo do posledního odstavce, musíme plátno vložit ručně a dát mu id nebo název třídy:
<canvas id="world"></canvas>
V tomto okamžiku můžeme deklarovat proměnné pro každý prvek, který chceme animovat:
const canvas = document.querySelector("#world");
const title = document.querySelector(".content__title");
const subtitle = document.querySelector(".content__subtitle");
const buttons = document.querySelectorAll(".content__link");
Vezmeme proměnnou canvas a předáme ji jako parametr rendereru takto:
const renderer = new THREE.WebGLRenderer({
canvas: canvas
});
Nyní, když vykreslovací modul ví, co má zobrazit, můžeme tento řádek odstranit:
document.body.appendChild(renderer.domElement);
Potom musíme předat dva parametry materiálu aby mohl být transparentní:
- transparentní:true
- neprůhlednost:0
a nastavíme je uvnitř prohlášení o materiálu
const material = new THREE.MeshMatcapMaterial({
matcap: matcapTexture,
transparent: true,
opacity: 0
});
Nyní musíme nainstalovat GSAP a pomocí NPM můžeme zadat následující příkaz:
Po instalaci jej můžeme importovat nad náš script.js soubor:
import { gsap } from "gsap";
a můžeme deklarovat klasickou časovou osu, jako je tato:
const tl = gsap.timeline({paused: true, delay: 0.8, easing: "Back.out(2)"});
tl.from(title, {opacity: 0, y: 20})
.from(subtitle, {opacity: 0, y: 20}, "-=.3")
.from(buttons,
{stagger: {each: 0.2, from: "start"}, opacity: 0, y: 20},
"-=.3"
)
.to(material, {opacity: 1}, "-=.2");
Jako úplně poslední krok zavoláme spuštění přehrávání na časové ose po funkci animace.
tl.play();
Mise splněna! Gratulujeme! 🥳 🎉 👏
Závěrečná rekapitulace
Zde ponechávám úplné konečné znění script.js blok kódu, abyste se na něj mohli lépe podívat:
// script.js
import * as THREE from "three";
import { gsap } from "gsap";
const canvas = document.querySelector("#world");
const title = document.querySelector(".content__title");
const subtitle = document.querySelector(".content__subtitle");
const buttons = document.querySelectorAll(".content__link");
const cursor = { x: 0, y: 0 };
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight);
camera.position.z = 3;
scene.add(camera);
const textureLoader = new THREE.TextureLoader();
const matcapTexture = textureLoader.load("https://bruno-simon.com/prismic/matcaps/3.png");
const geometry = new THREE.TorusKnotGeometry(0.5, 0.2, 200, 30);
const material = new THREE.MeshMatcapMaterial({ matcap: matcapTexture, transparent: true, opacity: 0 });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
const renderer = new THREE.WebGLRenderer({ canvas: canvas });
renderer.setSize(window.innerWidth, window.innerHeight);
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
})
window.addEventListener('mousemove', (_e) => {
cursor.x = _e.clientX / window.innerWidth - 0.5;
cursor.y = _e.clientY / window.innerHeight - 0.5;
});
const tl = gsap.timeline({ paused: true, delay: 0.8, easing: "Back.out(2)" });
tl.from(title, {opacity: 0, y: 20})
.from(subtitle, {opacity: 0, y: 20}, "-=.3")
.from(buttons, {stagger: {each: 0.2, from: "start"}, opacity: 0, y: 20}, "-=.3")
.to(material, { opacity: 1 }, "-=.2");
const animate = function () {
window.requestAnimationFrame(animate);
mesh.rotation.x += 0.01;
mesh.rotation.y += 0.01;
const cameraX = cursor.x -1;
const cameraY = - cursor.y;
camera.position.x += (cameraX - camera.position.x) / 10;
camera.position.y += (cameraY - camera.position.y) / 10;
renderer.render( scene, camera );
};
animate();
tl.play();
Závěr
Opravdu doufám, že tato minisérie pomohla vám a co nejvíce lidem a možná inspirovala jako Bruna Simona mě, když jsem poprvé viděl rozhovor.
Dejte mi prosím vědět, jestli jste ocenili článek a celou minisérii.
Můžete mě sledovat na Twitteru, GitHubu a Hashnode.
Děkuji za přečtení!
Th3Wall