Jak snadno začít s ThreeJS – část 3

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