Codrops 원본

How to Create Responsive and SEO-friendly WebGL Text

텍스트 · MIT

텍스트 더 보기
ORIGINAL PREVIEW
How to Create Responsive and SEO-friendly WebGL Text 정적 미리보기

갤러리 안에서는 화면을 벗어나거나 움직임을 멈추면 예제도 닫힙니다. 다시 재생할 때는 처음부터 시작해요.

큰 화면으로 보기 새 탭

SOURCE FILES

원본 코드 읽기

19개 파일

수집한 원본 소스와 실행 안내를 함께 제공합니다.

index.html
파일 저장

<!DOCTYPE html>
<html lang="en" class="no-js">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Responsive and Accessible WebGL Text With Three.js and Troika | Demo | Codrops</title>
    <link rel="shortcut icon" href="https://tympanus.net/favicon/favicon.ico" />
    <link rel="stylesheet" type="text/css" href="src/css/base.css" />
    <link rel="stylesheet" type="text/css" href="src/css/scroll.css" />
    <link rel="stylesheet" type="text/css" href="src/css/styles.css" />
    <script>
      document.documentElement.className = 'js';
    </script>
    <script src="//tympanus.net/codrops/adpacks/analytics.js"></script>
  </head>

  <body class="loading">
    <main>
      <header class="frame">
        <h1 class="frame__title">Responsive and Accessible WebGL Text</h1>
        <a class="frame__back" href="https://tympanus.net/codrops/?p=92085">Read the article</a>
        <a class="frame__github" href="https://github.com/ehaakana/codrops-text-demo">Code</a>
        <a class="frame__archive" href="https://tympanus.net/codrops/demos/">All demos</a>
        <nav class="frame__tags">
          <a href="https://tympanus.net/codrops/demos/?tag=scroll">#scroll</a>
          <a href="https://tympanus.net/codrops/demos/?tag=three-js">#three.js</a>
          <a href="https://tympanus.net/codrops/demos/?tag=webgl">#webgl</a>
          <a href="https://tympanus.net/codrops/demos/?tag=typography">#typography</a>
        </nav>
      </header>
      <div class="content">
        <div class="container">
          <section class="section__heading">
            <h3 data-animation="webgl-text" class="text__2">THREE.JS</h3>
            <h2 data-animation="webgl-text" class="text__1">RESPONSIVE AND ACCESSIBLE TEXT</h2>
          </section>
          <section class="section__main__content">
            <p data-animation="webgl-text" class="text__2">
              THIS TEXT IS STYLED TO LOOK LIKE A TYPICAL BLOCK OF TEXT ON A STANDARD WEBSITE. BUT UNDER THE SURFACE,
              IT’S BEING RENDERED WITH WEBGL INSTEAD OF TRADITIONAL HTML.
            </p>
            <p data-animation="webgl-text" class="text__2">
              THIS OPENS THE DOOR TO CUSTOM SHADER EFFECTS AND INTERACTIONS THAT GO BEYOND WHAT’S POSSIBLE WITH
              TRADITIONAL HTML.
            </p>
            <p data-animation="webgl-text" class="text__2">
              WE KEEP THE UNDERYLING HTML STRUCTURE PRESENT IN THE DOM. RATHER THAN CREATING MESHES DIRECTLY IN
              THREE.JS, THE SCENE IS BUILT BY READING FROM THE EXISTING HTML CONTENT. THIS WAY, SCREEN READERS, SEARCH
              ENGINES, AND OTHER TOOLS CAN STILL INTERPRET THE PAGE AS EXPECTED.
            </p>
          </section>
          <section class="section__footer">
            <p data-animation="webgl-text" class="text__3">NOW GO CRAZY WITH THE SHADERS :)</p>
          </section>
        </div>
      </div>
    </main>
    <script src="https://tympanus.net/codrops/adpacks/cda_sponsor.js"></script>
    <script type="module" src="/src/js/main.ts"></script>
  </body>
</html>
src/css/base.css
파일 저장

*,
*::after,
*::before {
  box-sizing: border-box;
}

:root {
  font-size: 12px;
  --color-text: #fff;
  --color-bg: #000;
  --color-link: #fff;
  --color-link-hover: #fff;
  --page-padding: 1.5rem;
}

body {
  margin: 0;
  color: var(--color-text);
  background-color: var(--color-bg);
  font-family: ui-monospace, monospace;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

@media (scripting: enabled) {
  .loading {
    &::before,
    &::after {
      content: "";
      position: fixed;
      z-index: 10000;
    }

    &::before {
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: var(--color-bg);
    }

    &::after {
      top: 50%;
      left: 50%;
      width: 100px;
      height: 1px;
      margin: 0 0 0 -50px;
      background: var(--color-link);
      animation: loaderAnim 1.5s ease-in-out infinite alternate forwards;
    }
  }
}

@keyframes loaderAnim {
  0% {
    transform: scaleX(0);
    transform-origin: 0% 50%;
  }

  50% {
    transform: scaleX(1);
    transform-origin: 0% 50%;
  }

  50.1% {
    transform: scaleX(1);
    transform-origin: 100% 50%;
  }

  100% {
    transform: scaleX(0);
    transform-origin: 100% 50%;
  }
}

a {
  text-decoration: none;
  color: var(--color-link);
  outline: none;
  cursor: pointer;

  &:hover {
    text-decoration: underline;
    color: var(--color-link-hover);
  }

  &:focus {
    outline: none;
    background: lightgrey;

    &:not(:focus-visible) {
      background: transparent;
    }

    &:focus-visible {
      outline: 2px solid red;
      background: transparent;
    }
  }
}

.frame {
  padding: 3rem var(--page-padding) 0;
  display: grid;
  z-index: 1000;
  position: relative;
  grid-row-gap: 1rem;
  grid-column-gap: 2rem;
  pointer-events: none;
  justify-items: start;
  grid-template-columns: auto auto;
  grid-template-areas:
    "title"
    "back"
    "archive"
    "github"
    "demos"
    "tags"
    "sponsor";

  #cdawrap {
    justify-self: start;
    grid-area: sponsor;
  }

  a,
  button {
    pointer-events: auto;
  }

  .frame__title {
    grid-area: title;
    font-size: inherit;
    margin: 0;
  }

  .frame__back {
    grid-area: back;
    justify-self: start;
  }

  .frame__archive {
    grid-area: archive;
    justify-self: start;
  }

  .frame__github {
    grid-area: github;
  }

  .frame__tags {
    grid-area: tags;
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
  }

  .frame__demos {
    grid-area: demos;
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
  }

  @media screen and (min-width: 53em) {
    padding: var(--page-padding);
    height: 100%;
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    grid-template-columns: auto auto auto auto 1fr;
    grid-template-rows: auto auto;
    align-content: space-between;
    grid-template-areas:
      "title back github archive demos"
      "tags tags tags sponsor sponsor";

    .frame__tags {
      align-self: end;
    }

    .frame__demos,
    #cdawrap {
      justify-self: end;
      text-align: right;
      max-width: 300px;
    }
  }
}

.content {
  padding: var(--page-padding);
  display: flex;
  flex-direction: column;
  width: 100vw;
  position: relative;

  @media screen and (min-width: 53em) {
    min-height: 100vh;
    justify-content: center;
    align-items: center;
  }
}
함께 쓰는 파일 17개 보기
src/css/scroll.css
파일 저장

html.lenis,
html.lenis body {
  height: auto;
}
.lenis.lenis-smooth [data-lenis-prevent] {
  overscroll-behavior: contain;
}
.lenis.lenis-stopped {
  overflow: clip;
}
.lenis.lenis-smooth iframe {
  pointer-events: none;
}
src/css/styles.css
파일 저장

:root {
  --clr-text: #fdcdf9;
  --clr-selection: rgba(255, 156, 245, 0.3);
  --clr-background: #212720;
}

@font-face {
  font-family: "Humane";
  src: url("/fonts/Humane-Black.ttf") format("truetype");
  font-weight: 900;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Humane";
  src: url("/fonts/Humane-Bold.ttf") format("truetype");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Humane";
  src: url("/fonts/Humane-ExtraBold.ttf") format("truetype");
  font-weight: 800;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Humane";
  src: url("/fonts/Humane-ExtraLight.ttf") format("truetype");
  font-weight: 200;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Humane";
  src: url("/fonts/Humane-Light.ttf") format("truetype");
  font-weight: 300;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Humane";
  src: url("/fonts/Humane-Medium.ttf") format("truetype");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Humane";
  src: url("/fonts/Humane-Regular.ttf") format("truetype");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Humane";
  src: url("/fonts/Humane-SemiBold.ttf") format("truetype");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Humane";
  src: url("/fonts/Humane-Thin.ttf") format("truetype");
  font-weight: 100;
  font-style: normal;
  font-display: swap;
}

body {
  background: var(--clr-background);
}

canvas {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  pointer-events: none;
}

::selection {
  background-color: var(--clr-selection);
  color: var(--clr-text);
}

::-moz-selection {
  background-color: var(--clr-selection);
  color: var(--clr-text);
}

.text__1,
.text__2,
.text__3 {
  color: var(--clr-text);
  text-align: center;
  margin-block-start: 0;
  margin-block-end: 0;
}

.content {
  width: 100%;
  font-family: Humane;
  font-size: 0.825vw;

  @media (max-width: 768px) {
    font-size: 2vw;
  }
}
.container {
  display: flex;
  flex-direction: column;
  align-items: center;

  width: 70em;
  gap: 17.6em;
  padding: 6em 0;

  @media (max-width: 768px) {
    width: 100%;
  }
}

.container section {
  display: flex;
  flex-direction: column;
  align-items: center;
  height: auto;
}

.section__main__content {
  gap: 5.6em;
}

.text__1 {
  font-size: 19.4em;
  font-weight: 700;
  max-width: 45em;

  @media (max-width: 768px) {
    font-size: 13.979em;
  }
}

.text__2 {
  font-size: 4.9em;
  max-width: 7.6em;
  letter-spacing: 0.01em;
}

.text__3 {
  font-size: 13.979em;
  max-width: 2.4em;
}
src/js/classes/Commons.ts
파일 저장

import { PerspectiveCamera, WebGLRenderer, Clock } from "three";

import Lenis from "lenis";

export interface Screen {
  width: number;
  height: number;
  aspect: number;
}

export interface Sizes {
  screen: Screen;
  pixelRatio: number;
}

/**
 * Singleton class for Common stuff.
 * Camera
 * Renderer
 * Lenis
 * Time
 */
export default class Commons {
  private constructor() {}

  private static instance: Commons;

  lenis!: Lenis;
  camera!: PerspectiveCamera;
  renderer!: WebGLRenderer;

  private time: Clock = new Clock();
  elapsedTime!: number;

  sizes: Sizes = {
    screen: {
      width: window.innerWidth,
      height: window.innerHeight,
      aspect: window.innerWidth / window.innerHeight,
    },
    pixelRatio: this.getPixelRatio(),
  };

  private distanceFromCamera: number = 1000;

  /**
   * Function to be called to either create Commons Singleton instance, or to return existing one.
   * TODO AFTER: Call instances init() function.
   * @returns Commons Singleton Instance.
   */
  static getInstance() {
    if (this.instance) return this.instance;

    this.instance = new Commons();
    return this.instance;
  }

  /**
   * Initializes all-things Commons. To be called after instance is set.
   */
  init() {
    this.createLenis();
    this.createCamera();
    this.createRenderer();
  }

  /**
   * Creates Lenis instance.
   * Sets autoRaf to true so we don't have to manually update Lenis on every frame.
   */
  private createLenis() {
    this.lenis = new Lenis({
      autoRaf: true,
      duration: 2,
    });
  }

  /**
   * Creates global camera.
   */
  private createCamera() {
    this.camera = new PerspectiveCamera(
      70,
      this.sizes.screen.aspect,
      200,
      2000
    );
    this.camera.position.z = this.distanceFromCamera;
    this.syncDimensions();
    this.camera.updateProjectionMatrix();
  }

  /**
   * Creates the common WebGLRenderer to be used across the app.
   */
  private createRenderer() {
    this.renderer = new WebGLRenderer({
      alpha: true, // Sets scene background to transparent, so our html body background defines the background color
      antialias: true,
    });

    this.renderer.setSize(this.sizes.screen.width, this.sizes.screen.height);

    this.renderer.setPixelRatio(this.sizes.pixelRatio);

    document.body.appendChild(this.renderer.domElement); // Creating canvas element and appending to body element.
  }

  /**
   * Single source of tbruth to get pixelRatio.
   */
  getPixelRatio() {
    return Math.min(window.devicePixelRatio, 2);
  }

  /**
   * Resize handler function is called from the entry-point (main.ts)
   * Updates the Common screen dimensions.
   * Updates the renderer.
   * Updates the camera.
   */
  onResize() {
    // Updating screen info
    this.sizes.screen = {
      width: window.innerWidth,
      height: window.innerHeight,
      aspect: window.innerWidth / window.innerHeight,
    };
    this.sizes.pixelRatio = this.getPixelRatio();

    // Updating renderer
    this.renderer.setSize(this.sizes.screen.width, this.sizes.screen.height);
    this.renderer.setPixelRatio(this.sizes.pixelRatio);

    //Updating camera
    this.onResizeCamera();
  }

  /**
   * Handler function that is called from onResize handler.
   * Updates the perspective camera with the new adjusted screen dimensions
   */
  private onResizeCamera() {
    this.syncDimensions();
    this.camera.aspect = this.sizes.screen.aspect;
    this.camera.updateProjectionMatrix();
  }

  /**
   * Helper function that is called upon initialization and resize
   * Updates the camera's fov according to the new dimensions such that the window's pixels match with that of WebGL scene
   */
  private syncDimensions() {
    this.camera.fov =
      2 *
      Math.atan(this.sizes.screen.height / 2 / this.distanceFromCamera) *
      (180 / Math.PI);
  }

  /**
   * Update function to be called from entry-point (main.ts)
   */
  update() {
    this.elapsedTime = this.time.getElapsedTime();
  }
}
src/js/classes/PostProcessing.ts
파일 저장

import {
  EffectComposer,
  RenderPass,
  ShaderPass,
} from "three/examples/jsm/Addons.js";

import Commons from "./Commons";
import * as THREE from "three";

import fragmentShader from "../../shaders/postprocessing/postprocessing.frag";
import vertexShader from "../../shaders/postprocessing/postprocessing.vert";

interface Props {
  scene: THREE.Scene;
}

export default class PostProcessing {
  // Scene and utility references
  private commons: Commons;
  private scene: THREE.Scene;

  // EffectComposer and passes
  private composer!: EffectComposer;
  private renderPass!: RenderPass;
  private shiftPass!: ShaderPass;

  // Scroll velocity
  private lerpedVelocity = 0; // Smoothed scroll velocity to be used in postprocessing.
  private lerpFactor = 0.15; // Smoothing factor for lerping the velocity.

  constructor({ scene }: Props) {
    this.commons = Commons.getInstance();

    this.scene = scene;

    this.createComposer();
    this.createPasses(); // Add our render and post-process passes
  }

  /**
   * Creates EffectComposer instance and sets pixel ratio and size.
   */
  private createComposer() {
    this.composer = new EffectComposer(this.commons.renderer);
    this.composer.setPixelRatio(this.commons.sizes.pixelRatio);
    this.composer.setSize(
      this.commons.sizes.screen.width,
      this.commons.sizes.screen.height
    );
  }

  private createPasses() {
    // Creating Render Pass (final output) first
    this.renderPass = new RenderPass(this.scene, this.commons.camera);
    this.composer.addPass(this.renderPass);

    // Creating Post-processing shader for wave and RGB-shift effect
    const shiftShader = {
      uniforms: {
        tDiffuse: { value: null }, // Default input from previous pass
        uVelocity: { value: 0 }, // Scroll velocity input
        uTime: { value: 0 }, // Elapsed time for animated distortion
      },
      vertexShader,
      fragmentShader,
    };

    // Creating the ShaderPass and adding it to the composer
    this.shiftPass = new ShaderPass(shiftShader);
    this.composer.addPass(this.shiftPass);
  }

  /**
   * Resize handler for EffectComposer, called from entry-point.
   */
  onResize() {
    this.composer.setPixelRatio(this.commons.sizes.pixelRatio);
    this.composer.setSize(
      this.commons.sizes.screen.width,
      this.commons.sizes.screen.height
    );
  }

  update() {
    this.shiftPass.uniforms.uTime.value = this.commons.elapsedTime;

    // Reading current velocity form lenis instance.
    const targetVelocity = this.commons.lenis.velocity;

    // We use the lerped velocity as the actual velocity for the shader, just for a smoother experience.
    this.lerpedVelocity +=
      (targetVelocity - this.lerpedVelocity) * this.lerpFactor;

    this.shiftPass.uniforms.uVelocity.value = this.lerpedVelocity;

    this.composer.render();
  }
}
src/js/classes/WebGLText.ts
파일 저장

import Commons from "./Commons";
import * as THREE from "three";

import fragmentShader from "../../shaders/text/text.frag";
import vertexShader from "../../shaders/text/text.vert";

// @ts-ignore
import { Text } from "troika-three-text";

import { inView, animate } from "motion";

interface Props {
  scene: THREE.Scene;
  element: HTMLElement;
}
export default class WebGLText {
  private commons: Commons;

  private scene: THREE.Scene;
  private element: HTMLElement;

  private computedStyle: CSSStyleDeclaration;
  private font!: string; // Path to our .ttf font file.
  private bounds!: DOMRect;
  private color!: THREE.Color;
  private material!: THREE.ShaderMaterial;
  private mesh!: Text;

  // We assign the correct font bard on our element's font weight from here
  private weightToFontMap: Record<string, string> = {
    "900": "/fonts/Humane-Black.ttf",
    "800": "/fonts/Humane-ExtraBold.ttf",
    "700": "/fonts/Humane-Bold.ttf",
    "600": "/fonts/Humane-SemiBold.ttf",
    "500": "/fonts/Humane-Medium.ttf",
    "400": "/fonts/Humane-Regular.ttf",
    "300": "/fonts/Humane-Light.ttf",
    "200": "/fonts/Humane-ExtraLight.ttf",
    "100": "/fonts/Humane-Thin.ttf",
  };

  private y: number = 0; // Scroll-adjusted bounds.top

  private isVisible: boolean = false;

  constructor({ scene, element }: Props) {
    this.commons = Commons.getInstance();

    this.scene = scene;
    this.element = element;

    this.computedStyle = window.getComputedStyle(this.element); // Saving initial computed style.

    this.createFont();
    this.createColor();
    this.createBounds();
    this.createMaterial();
    this.createMesh();
    this.setStaticValues();

    this.scene.add(this.mesh);

    this.element.style.color = "transparent"; // Setting the DOM Element to invisible, so that only WebGLText remains.

    this.addEventListeners(); // Inits visibility tracking for show() and hide()
  }

  private createFont() {
    this.font =
      this.weightToFontMap[this.computedStyle.fontWeight] ||
      "/fonts/Humane-Regular.ttf";
  }

  private createBounds() {
    this.bounds = this.element.getBoundingClientRect();
    this.y = this.bounds.top + this.commons.lenis.actualScroll;
  }

  private createColor() {
    this.color = new THREE.Color(this.computedStyle.color);
  }

  private createMaterial() {
    this.material = new THREE.ShaderMaterial({
      fragmentShader,
      vertexShader,
      uniforms: {
        uProgress: new THREE.Uniform(0),
        uHeight: new THREE.Uniform(this.bounds.height),
        uColor: new THREE.Uniform(this.color),
      },
    });
  }

  private createMesh() {
    this.mesh = new Text();

    this.mesh.text = this.element.innerText; // Always use innerText (not innerHTML or textContent).
    this.mesh.font = this.font;

    this.mesh.anchorX = "0%"; // We set to position it from the left, instead of the center as in traditional ThreeJS/WebGL
    this.mesh.anchorY = "50%";

    this.mesh.material = this.material;
  }

  /**
   * Sets static values that don't have to be updated on every frame.
   * This is called at initialization and resize.
   */
  private setStaticValues() {
    const { fontSize, letterSpacing, lineHeight, whiteSpace, textAlign } =
      this.computedStyle;

    const fontSizeNum = window.parseFloat(fontSize);

    this.mesh.fontSize = fontSizeNum;

    this.mesh.textAlign = textAlign;

    // Troika defines letter spacing in em's, so we convert to them
    this.mesh.letterSpacing = parseFloat(letterSpacing) / fontSizeNum;

    // Same with line height
    this.mesh.lineHeight = parseFloat(lineHeight) / fontSizeNum;

    // Important to define maxWidth for the mesh, so that our text doesn't overflow
    this.mesh.maxWidth = this.bounds.width;

    this.mesh.whiteSpace = whiteSpace;
  }

  show() {
    this.isVisible = true;

    animate(
      this.material.uniforms.uProgress,
      { value: 1 },
      { duration: 1.8, ease: [0.25, 1, 0.5, 1] }
    );
  }

  hide() {
    animate(
      this.material.uniforms.uProgress,
      { value: 0 },
      { duration: 1.8, onComplete: () => (this.isVisible = false) }
    );
  }

  onResize() {
    this.computedStyle = window.getComputedStyle(this.element);
    this.createBounds();
    this.setStaticValues();
    this.material.uniforms.uHeight.value = this.bounds.height;
  }

  update() {
    if (this.isVisible) {
      this.mesh.position.y =
        -this.y +
        this.commons.lenis.animatedScroll +
        this.commons.sizes.screen.height / 2 -
        this.bounds.height / 2;

      this.mesh.position.x =
        this.bounds.left - this.commons.sizes.screen.width / 2;
    }
  }

  /**
   * Inits visibility tracking using motion.
   */
  private addEventListeners() {
    inView(this.element, () => {
      this.show();

      return () => this.hide();
    });
  }
}
src/js/main.ts
파일 저장

import Commons from "./classes/Commons";
import * as THREE from "three";
import WebGLText from "./classes/WebGLText";
import PostProcessing from "./classes/PostProcessing";

/**
 * Main entry-point.
 * Creates Commons instance, Postprocessing, Scene & WebGLTexts
 */
class App {
  private commons!: Commons;
  private postProcessing!: PostProcessing;

  private scene!: THREE.Scene;

  private texts!: Array<WebGLText>;

  constructor() {
    document.addEventListener("DOMContentLoaded", async () => {
      await document.fonts.ready; // Important to wait for fonts to load when animating any texts.
      document.body.classList.remove("loading");

      this.commons = Commons.getInstance();
      this.commons.init();

      this.createScene();
      this.createWebGLTexts();
      this.createPostProcessing();
      this.addEventListeners();

      this.update();
    });
  }

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

  private createWebGLTexts() {
    const texts = document.querySelectorAll('[data-animation="webgl-text"]');

    if (texts) {
      this.texts = Array.from(texts).map(
        (el) =>
          new WebGLText({
            element: el as HTMLElement,
            scene: this.scene,
          })
      );
    }
  }

  private createPostProcessing() {
    this.postProcessing = new PostProcessing({ scene: this.scene });
  }

  /**
   * The main raf loop handler of the App
   * The update function to be called on each frame of the browser.
   * Calls update() on Commons, WebGLTexts and Postprocessing
   */
  private update() {
    this.commons.update();

    if (this.texts) {
      this.texts.forEach((el) => el.update());
    }

    // Don't need line below as we're rendering everything using EffectComposer.
    // this.commons.renderer.render(this.scene, this.commons.camera);

    this.postProcessing.update();

    window.requestAnimationFrame(this.update.bind(this));
  }

  private onResize() {
    this.commons.onResize();

    if (this.texts) {
      this.texts.forEach((el) => el.onResize());
    }

    this.postProcessing.onResize();
  }

  private addEventListeners() {
    window.addEventListener("resize", this.onResize.bind(this));
  }
}

export default new App();
src/shaders/postprocessing/postprocessing.frag
파일 저장

uniform sampler2D tDiffuse;
uniform float uVelocity;
uniform float uTime;

varying vec2 vUv;

void main() {
    vec2 uv = vUv;
    
    // Calculating wave distortion based on velocity
    float waveAmplitude = uVelocity * 0.0009;
    float waveFrequency = 4.0 + uVelocity * 0.01;
    
    // Applying wave distortion to the UV coordinates
    vec2 waveUv = uv;
    waveUv.x += sin(uv.y * waveFrequency + uTime) * waveAmplitude;
    waveUv.y += sin(uv.x * waveFrequency * 5. + uTime * 0.8) * waveAmplitude;
    
    // Applying the RGB shift to the wave-distorted coordinates
    float r = texture2D(tDiffuse, vec2(waveUv.x, waveUv.y + uVelocity * 0.0005)).r;
    vec2 gb = texture2D(tDiffuse, waveUv).gb;

    gl_FragColor = vec4(r, gb, r);
}
src/shaders/postprocessing/postprocessing.vert
파일 저장

varying vec2 vUv;

void main() {
    vUv = uv;
    
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
src/shaders/text/text.frag
파일 저장

uniform float uProgress;
uniform vec3 uColor;

varying vec2 vUv;


void main() {
    // Calculate the reveal threshold (bottom to top reveal)
    float reveal = 1.0 - vUv.y;
    
    // Discard fragments above the reveal threshold based on progress
    if (reveal > uProgress) discard;

    // Apply the color to the visible parts of the text
    gl_FragColor = vec4(uColor, 1.0);
}
src/shaders/text/text.vert
파일 저장

uniform float uProgress;
uniform float uHeight;

varying vec2 vUv;

void main() {
    vUv = uv;
    
    vec3 transformedPosition = position;
    
    transformedPosition.y -= uHeight * (1.0 - uProgress);
    
    gl_Position = projectionMatrix * modelViewMatrix * vec4(transformedPosition, 1.0);
}
src/shaders.d.ts
파일 저장

declare module "*.frag" {
  const value: string;
  export default value;
}

declare module "*.vert" {
  const value: string;
  export default value;
}

declare module "*.glsl" {
  const value: string;
  export default value;
}
src/troika.d.ts
파일 저장

declare module "troika-three-text" {
  const value: any;
  export default value;
}
src/vite-env.d.ts
파일 저장

/// <reference types="vite/client" />
vite.config.ts
파일 저장

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

export default defineConfig({
  plugins: [
    glsl({
      include: [
        "**/*.glsl",
        "**/*.wgsl",
        "**/*.vert",
        "**/*.frag",
        "**/*.vs",
        "**/*.fs",
      ],
      exclude: undefined,
      // Glob pattern, or array of glob patterns to ignore
      warnDuplicatedImports: true,
      // Warn if the same chunk was imported multiple times
      defaultExtension: "glsl",
      // Shader suffix when no extension is specified
      watch: true,
      // Recompile shader on change
      root: "/",
    }),
  ],
  // config options
});
Media credits and license evidence실행 안내·자료
파일 저장

README.md
## Credits


## License

[MIT](LICENSE)
LICENSE실행 안내·자료
파일 저장

MIT License

Copyright (c) 2009 - 2024 [Codrops](https://tympanus.net/codrops)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Bundled dependency licenses실행 안내·자료
파일 저장

three@0.175.0 — LICENSE
The MIT License

Copyright © 2010-2025 three.js authors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


lenis@1.2.3 — LICENSE
The MIT License

Copyright (c) 2024 darkroom.engineering

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

troika-worker-utils@0.52.0 — LICENSE
MIT License

Copyright (c) 2019 ProtectWise
Copyright (c) 2021 Jason Johnston

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


webgl-sdf-generator@1.1.1 — LICENSE.txt
Copyright (c) 2021 Jason Johnston

MIT License

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


bidi-js@1.1.0 — LICENSE.txt
Copyright (c) 2021 Jason Johnston

MIT License

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


troika-three-utils@0.52.5 — LICENSE
MIT License

Copyright (c) 2019 ProtectWise
Copyright (c) 2021 Jason Johnston

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


troika-three-text@0.52.4 — LICENSE
MIT License

Copyright (c) 2019 ProtectWise
Copyright (c) 2021 Jason Johnston

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


motion@12.6.5 — LICENSE.md
The MIT License (MIT)

Copyright (c) 2024 [Motion](https://motion.dev) B.V.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.