Codrops 원본
How to Create Responsive and SEO-friendly WebGL Text
텍스트 · MIT
갤러리 안에서는 화면을 벗어나거나 움직임을 멈추면 예제도 닫힙니다. 다시 재생할 때는 처음부터 시작해요.
SOURCE FILES
원본 코드 읽기
수집한 원본 소스와 실행 안내를 함께 제공합니다.
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;
}
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
});
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.
BEHIND THE EXAMPLE
이 장면을 만드는 원리
DOM 문구와 WebGL 글자의 동기 범위
시각적인 글자 분할과 사용자에게 한 글자로 보이는 단위를 구분합니다.
- 이 예제에서는
- WebGLText는 element.innerText로 메시 문구를 만들고 계산 스타일·bounds에서 글꼴과 위치를 구합니다. resize는 스타일과 bounds를 다시 읽는 별도 경로입니다.
코드와 함께 확인하기
코드에서 찾기
createMeshWebGLText.tsDOM 문자열을 Troika Text의 text에 전달합니다.
직접 해보기
문구·굵기·폭을 각각 바꾸고 한글·이모지와 DOM 읽기 경로를 비교합니다.
살펴볼 변화메시가 실제 문구 변경을 따라가는지와 시각 글꼴의 지원 범위를 확인해야 하며 검색 가능성·접근성은 제목만으로 증명되지 않습니다.
