Codrops 원본
Shuffling Typography Animation
텍스트 · MIT
갤러리 안에서는 화면을 벗어나거나 움직임을 멈추면 예제도 닫힙니다. 다시 재생할 때는 처음부터 시작해요.
SOURCE FILES
원본 코드 읽기
수집한 원본 소스와 실행 안내를 함께 제공합니다.
src/css/base.css
*,
*::after,
*::before {
box-sizing: border-box;
}
:root {
font-size: 18px;
--color-text: #fff;
--color-bg: #000;
--color-link: #61dca3;
--color-link-hover: #fff;
}
body {
margin: 0;
color: var(--color-text);
background-color: var(--color-bg);
font-family: ballinger-mono, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-transform: uppercase;
}
/* Page Loader */
.js .loading::before,
.js .loading::after {
content: '';
position: fixed;
z-index: 1000;
}
.js .loading::before {
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--color-bg);
}
.js .loading::after {
top: 50%;
left: 50%;
width: 60px;
height: 60px;
margin: -30px 0 0 -30px;
border-radius: 50%;
opacity: 0.4;
background: var(--color-link);
animation: loaderAnim 0.7s linear infinite alternate forwards;
}
@keyframes loaderAnim {
to {
opacity: 1;
transform: scale3d(0.5,0.5,1);
}
}
a {
text-decoration: none;
color: var(--color-link);
outline: none;
cursor: pointer;
}
a:not(.frame__title-back) {
white-space: nowrap;
overflow: hidden;
position: relative;
}
a:hover {
color: var(--color-link-hover);
outline: none;
}
/* Better focus styles from https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible */
a:focus {
/* Provide a fallback style for browsers
that don't support :focus-visible */
outline: none;
background: lightgrey;
}
a:focus:not(:focus-visible) {
/* Remove the focus indicator on mouse-focus for browsers
that do support :focus-visible */
background: transparent;
}
a:focus-visible {
/* Draw a very noticeable focus style for
keyboard-focus on browsers that do support
:focus-visible */
outline: 2px solid red;
background: transparent;
}
.unbutton {
background: none;
border: 0;
padding: 0;
margin: 0;
font: inherit;
cursor: pointer;
}
.unbutton:focus {
outline: none;
}
main {
height: 100vh;
display: flex;
flex-direction: column;
}
.frame {
color: var(--color-title);
padding: 1rem 2rem;
margin-bottom: 3rem;
grid-gap: 2rem;
align-items: start;
text-transform: lowercase;
font-size: 0.75rem;
}
.frame a:not(.frame__title-back)::before {
content: '';
height: 1px;
width: 100%;
background: currentColor;
position: absolute;
top: 90%;
transition: transform 0.3s;
transform-origin: 0% 50%;
}
.frame a:not(.frame__title-back):hover::before {
transform: scaleX(0);
transform-origin: 100% 50%;
}
.frame__title {
grid-area: title;
display: flex;
}
.frame__title-main {
font-size: inherit;
margin: 0;
font-weight: inherit;
}
.frame__title-back {
position: relative;
display: flex;
align-items: flex-end;
}
.frame__title-back span {
display: none;
}
.frame__title-back svg {
fill: currentColor;
}
.frame__prev {
grid-area: prev;
align-self: start;
}
.ascii {
color: var(--color-link);
opacity: 0.5;
position: fixed;
right: 0;
top: -1rem;
pointer-events: none;
}
.content {
display: grid;
gap: 1rem;
max-width: 830px;
padding: 7vh 2rem 0;
}
dt {
font-weight: bold;
}
dd {
margin: 0;
}
.effects {
margin-top: auto;
padding: 2rem;
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.effects button {
border: 0;
color: inherit;
white-space: nowrap;
background: none;
text-transform: inherit;
font: inherit;
cursor: pointer;
color: var(--color-link);
font-size: 0.75rem;
text-transform: lowercase;
}
.effects button::before {
content: "[";
margin-right: 0.5rem;
display: inline-block;
}
.effects button::after {
content: "]";
margin-left: 0.5rem;
display: inline-block;
}
.effects button:hover {
color: var(--color-link-hover);
}
.splitting .words .word {
white-space: nowrap;
}
@media screen and (min-width: 53em) {
.frame {
display: grid;
grid-template-columns: auto auto 1fr;
grid-template-rows: auto;
grid-template-areas: 'title prev sponsor';
justify-content: start;
}
.content {
grid-template-columns: 15ch 1fr;
}
}
src/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>Type Shuffle Animation | Codrops</title>
<meta name="description" content="A shuffling letter animation with a variety of effects. " />
<meta name="keywords" content="type animation, terminal, typography, monospace, letter animation, gsap" />
<meta name="author" content="Codrops" />
<link rel="shortcut icon" href="favicon.ico">
<link rel="stylesheet" href="https://use.typekit.net/biu0hfr.css">
<link rel="stylesheet" type="text/css" href="css/base.css" />
<script src="https://ajax.googleapis.com/ajax/libs/webfont/1.6.26/webfont.js"></script>
<script>
document.documentElement.className = "js";
var supportsCssVars = function() { var e, t = document.createElement("style"); return t.innerHTML = "root: { --tmp-var: bold; }", document.head.appendChild(t), e = !!(window.CSS && window.CSS.supports && window.CSS.supports("font-weight", "var(--tmp-var)")), t.parentNode.removeChild(t), e };
supportsCssVars() || alert("Please view this demo in a modern browser that supports CSS Variables.");
</script>
<script src="//tympanus.net/codrops/adpacks/analytics.js"></script>
</head>
<body class="">
<main>
<pre class="ascii">
.' .*' `*. `. ; / .' .*'`-.
.' .' \ \ : / .*' `-.
/ / , ', -. `.
/ / _.._ : '. .-*""*-. `. \
/ .' .-' `-. '/ .' `-. \ \
, ,' .' .*""*-.\,: ,.-*"*-. `. .
/ .' .' ` ` `. `.
' / / `. \ . `
, . / \ \ ` ;
: , \ : \ ` :
; . \ . \ \ |
| ' : , . \ ; ;:
|: ; ` \ : ;|
|| | ; . ; ; ;;
;: | | ` ; | ||
L.\ | | .d$$s. .s$$b.. ; : |.:J
/ __`'; | *' `*Tb._ _.dP*' `* | | :__ \
..' .`.: | `*Ts' `sP*' ; | '. `,,
; / , ; .+s**s. `. .s**s+. :_;-.' \ :
: , /:; : \ *ss* \ ; / *ss* / +: \ . ;
.` : : , .+s$$$s+. .+s$$$s+. .* ; ; ',
\ *. ;*d$P*"$$$T$b ,+**+, d$P*"$$$T$b* .* /
\ ; ::$; +:$$$:$$;* *:$; +:$$$:$$; : /
: T$b._$$$d$P T$b._$$$d$P ;
`._.; ; `*T$$$P*' `*T$$$P*' :._.'
|; : ' | `.
;: \ :. , : `. \
` \ `._ `*.__.*' ' \ \
. *--*' "" , ; .
\ / . :
\ .+*"*--*"*+. / ` |
`. :._.--..--._.; .'; ; : ;
;. `. .' .' | | ,
: `. `*----*' .' | | :
| `. .' | : .
: `. .' : ' / `.
/ `-. .-' /`. / .' `.
/ `****' . `. \
.' , ' \ \ \
_.' / : . ,
_.-*' `. : : | : :
`-. . ; |
`-. .-` , ;
`. `. .* .' `. `. .' ,
`. `- ' .' `. .'
`. .' `+._ / `.
`. .' : / . \
`. .' | : ; .
`..' : ; ` ;
. : ; :
\ ` / ;
`. \ /
`-.\ .'
</pre>
<div class="frame">
<div class="frame__title">
<h1 class="frame__title-main">Type Shuffle Animation based on <a href="http://lcd.ertdfgcvb.xyz/">LCD 1</a></h1>
<a aria-label="Back to the article" class="frame__title-back" href="https://tympanus.net/codrops/?p=">
<span class="oh__inner">Back to the article</span>
<svg width="18px" height="18px" viewBox="0 0 24 24">
<path vector-effect="non-scaling-stroke" d="M18.25 15.5a.75.75 0 00.75-.75v-9a.75.75 0 00-.75-.75h-9a.75.75 0 000 1.5h7.19L6.22 16.72a.75.75 0 101.06 1.06L17.5 7.56v7.19c0 .414.336.75.75.75z"></path>
</svg>
</a>
</div>
<a class="frame__prev" href="https://tympanus.net/Tutorials/DiceRoller/">Previous demo</a>
</div>
<dl class="content">
<dt>Name</dt>
<dd>Aria McDonald</dd>
<dt>Profession</dt>
<dd>Creative Web Developer</dd>
<dt>Bio</dt>
<dd>5 years experience - HTML, CSS, JavaScript. Passion for creativity in the digital space. Problem solver. Hiker, guitar player, culinary enthusiast. Constantly seeking new challenges, growth opportunities.Bringing imaginative ideas to life. Skilled in modern web development frameworks such as React and Angular. Strong understanding of UI/UX design principles and ability to create visually appealing and usable websites.</dd>
<dt>Projects</dt>
<dd>Eco Explorer, SkyBridge, SparkSail</dd>
<dt>Awards</dt>
<dd>Best User Experience Design, Webby Awards 2021</dd>
</dl>
<div class="effects">
<button data-fx="1">Effect 1</button>
<button data-fx="2">Effect 2</button>
<button data-fx="3">Effect 3</button>
<button data-fx="4">Effect 4</button>
<button data-fx="5">Effect 5</button>
<button data-fx="6">Effect 6</button>
</div>
</main>
<script src="https://tympanus.net/codrops/adpacks/cda_sponsor.js"></script>
<script type="module" src="js/index.js"></script>
</body>
</html>함께 쓰는 파일 6개 보기
src/js/index.js
import { preloadFonts } from './utils';
import { TypeShuffle } from './typeShuffle';
preloadFonts('biu0hfr').then(() => {
document.body.classList.remove('loading');
const textElement = document.querySelector('.content');
const ts = new TypeShuffle(textElement);
ts.trigger('fx1');
[...document.querySelectorAll('.effects > button')].forEach(button => {
button.addEventListener('click', () => {
ts.trigger(`fx${button.dataset.fx}`);
});
});
});src/js/typeShuffle.js
import 'splitting/dist/splitting.css';
import 'splitting/dist/splitting-cells.css';
import Splitting from 'splitting';
import { randomNumber } from './utils';
/**
* Class representing one line
*/
class Line {
// line position
position = -1;
// cells/chars
cells = [];
/**
* Constructor.
* @param {Element} DOM_el - the char element (<span>)
*/
constructor(linePosition) {
this.position = linePosition;
}
}
/**
* Class representing one cell/char
*/
class Cell {
// DOM elements
DOM = {
// the char element (<span>)
el: null,
};
// cell position
position = -1;
// previous cell position
previousCellPosition = -1;
// original innerHTML
original;
// current state/innerHTML
state;
color;
originalColor;
// cached values
cache;
/**
* Constructor.
* @param {Element} DOM_el - the char element (<span>)
*/
constructor(DOM_el, {
position,
previousCellPosition
} = {}) {
this.DOM.el = DOM_el;
this.original = this.DOM.el.innerHTML;
this.state = this.original;
this.color = this.originalColor = getComputedStyle(document.documentElement).getPropertyValue('--color-text');
this.position = position;
this.previousCellPosition = previousCellPosition;
}
/**
* @param {string} value
*/
set(value) {
this.state = value;
this.DOM.el.innerHTML = this.state;
}
}
/**
* Class representing the TypeShuffle object
*/
export class TypeShuffle {
// DOM elements
DOM = {
// the main text element
el: null,
};
// array of Line objs
lines = [];
// array of letters and symbols
lettersAndSymbols = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '!', '@', '#', '$', '&', '*', '(', ')', '-', '_', '+', '=', '/', '[', ']', '{', '}', ';', ':', '<', '>', ',', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
// effects and respective methods
effects = {
'fx1': () => this.fx1(),
'fx2': () => this.fx2(),
'fx3': () => this.fx3(),
'fx4': () => this.fx4(),
'fx5': () => this.fx5(),
'fx6': () => this.fx6(),
};
totalChars = 0;
/**
* Constructor.
* @param {Element} DOM_el - main text element
*/
constructor(DOM_el) {
this.DOM.el = DOM_el;
// Apply Splitting (two times to have lines, words and chars)
const results = Splitting({
target: this.DOM.el,
by: 'lines'
})
results.forEach(s => Splitting({ target: s.words }));
// for every line
for (const [linePosition, lineArr] of results[0].lines.entries()) {
// create a new Line
const line = new Line(linePosition);
let cells = [];
let charCount = 0;
// for every word of each line
for (const word of lineArr) {
// for every character of each line
for (const char of [...word.querySelectorAll('.char')]) {
cells.push(
new Cell(char, {
position: charCount,
previousCellPosition: charCount === 0 ? -1 : charCount-1
})
);
++charCount;
}
}
line.cells = cells;
this.lines.push(line);
this.totalChars += charCount;
}
// TODO
// window.addEventListener('resize', () => this.resize());
}
/**
* clear all the cells chars
*/
clearCells() {
for (const line of this.lines) {
for (const cell of line.cells) {
cell.set(' ');
}
}
}
/**
*
* @returns {string} a random char from this.lettersAndSymbols
*/
getRandomChar() {
return this.lettersAndSymbols[Math.floor(Math.random() * this.lettersAndSymbols.length)];
}
/**
* Effect 1 - clear cells and animate each line cells (delays per line and per cell)
*/
fx1() {
// max iterations for each cell to change the current value
const MAX_CELL_ITERATIONS = 45;
let finished = 0;
// clear all cells values
this.clearCells();
// cell's loop animation
// each cell will change its value MAX_CELL_ITERATIONS times
const loop = (line, cell, iteration = 0) => {
// cache the previous value
cell.cache = cell.state;
// set back the original cell value if at the last iteration
if ( iteration === MAX_CELL_ITERATIONS-1 ) {
cell.set(cell.original);
++finished;
if ( finished === this.totalChars ) {
this.isAnimating = false;
}
}
// if the cell is the first one in its line then generate a random char
else if ( cell.position === 0 ) {
// show specific characters for the first 9 iterations (looks cooler)
cell.set(iteration < 9 ?
['*', '-', '\u0027', '\u0022'][Math.floor(Math.random() * 4)] :
this.getRandomChar());
}
// get the cached value of the previous cell.
// This will result in the illusion that the chars are sliding from left to right
else {
cell.set(line.cells[cell.previousCellPosition].cache);
}
// doesn't count if it's an empty space
if ( cell.cache != ' ' ) {
++iteration;
}
// repeat...
if ( iteration < MAX_CELL_ITERATIONS ) {
setTimeout(() => loop(line, cell, iteration), 15);
}
};
// set delays for each cell animation
for (const line of this.lines) {
for (const cell of line.cells) {
setTimeout(() => loop(line, cell), (line.position+1)*200);
}
}
}
fx2() {
const MAX_CELL_ITERATIONS = 20;
let finished = 0;
const loop = (line, cell, iteration = 0) => {
if ( iteration === MAX_CELL_ITERATIONS-1 ) {
cell.set(cell.original);
cell.DOM.el.style.opacity = 0;
setTimeout(() => {
cell.DOM.el.style.opacity = 1;
}, 300);
++finished;
if ( finished === this.totalChars ) {
this.isAnimating = false;
}
}
else {
cell.set(this.getRandomChar());
}
++iteration;
if ( iteration < MAX_CELL_ITERATIONS ) {
setTimeout(() => loop(line, cell, iteration), 40);
}
};
for (const line of this.lines) {
for (const cell of line.cells) {
setTimeout(() => loop(line, cell), (cell.position+1)*30);
}
}
}
fx3() {
const MAX_CELL_ITERATIONS = 10;
let finished = 0;
this.clearCells();
const loop = (line, cell, iteration = 0) => {
if ( iteration === MAX_CELL_ITERATIONS-1 ) {
cell.set(cell.original);
++finished;
if ( finished === this.totalChars ) {
this.isAnimating = false;
}
}
else {
cell.set(this.getRandomChar());
}
++iteration;
if ( iteration < MAX_CELL_ITERATIONS ) {
setTimeout(() => loop(line, cell, iteration), 80);
}
};
for (const line of this.lines) {
for (const cell of line.cells) {
setTimeout(() => loop(line, cell), randomNumber(0,2000));
}
}
}
fx4() {
const MAX_CELL_ITERATIONS = 30;
let finished = 0;
this.clearCells();
const loop = (line, cell, iteration = 0) => {
cell.cache = cell.state;
if ( iteration === MAX_CELL_ITERATIONS-1 ) {
cell.set(cell.original);
++finished;
if ( finished === this.totalChars ) {
this.isAnimating = false;
}
}
else if ( cell.position === 0 ) {
cell.set(['*',':'][Math.floor(Math.random() * 2)]);
}
else {
cell.set(line.cells[cell.previousCellPosition].cache);
}
if ( cell.cache != ' ' ) {
++iteration;
}
if ( iteration < MAX_CELL_ITERATIONS ) {
setTimeout(() => loop(line, cell, iteration), 15);
}
};
for (const line of this.lines) {
for (const cell of line.cells) {
setTimeout(() => loop(line, cell), Math.abs(this.lines.length/2-line.position)*400);
}
}
}
fx5() {
// max iterations for each cell to change the current value
const MAX_CELL_ITERATIONS = 30;
let finished = 0;
this.clearCells();
const loop = (line, cell, iteration = 0) => {
cell.cache = {'state': cell.state, 'color': cell.color};
if ( iteration === MAX_CELL_ITERATIONS-1 ) {
cell.color = cell.originalColor;
cell.DOM.el.style.color = cell.color;
cell.set(cell.original);
++finished;
if ( finished === this.totalChars ) {
this.isAnimating = false;
}
}
else if ( cell.position === 0 ) {
cell.color = ['#3e775d', '#61dca3', '#61b3dc'][Math.floor(Math.random() * 3)]
cell.DOM.el.style.color = cell.color
cell.set(iteration < 9 ?
['*', '-', '\u0027', '\u0022'][Math.floor(Math.random() * 4)] :
this.getRandomChar());
}
else {
cell.set(line.cells[cell.previousCellPosition].cache.state);
cell.color = line.cells[cell.previousCellPosition].cache.color
cell.DOM.el.style.color = cell.color
}
if ( cell.cache.state != ' ' ) {
++iteration;
}
if ( iteration < MAX_CELL_ITERATIONS ) {
setTimeout(() => loop(line, cell, iteration), 10);
}
};
for (const line of this.lines) {
for (const cell of line.cells) {
setTimeout(() => loop(line, cell), (line.position+1)*200);
}
}
}
fx6() {
// max iterations for each cell to change the current value
const MAX_CELL_ITERATIONS = 15;
let finished = 0;
const loop = (line, cell, iteration = 0) => {
cell.cache = {'state': cell.state, 'color': cell.color};
if ( iteration === MAX_CELL_ITERATIONS-1 ) {
cell.set(cell.original);
cell.color = cell.originalColor;
cell.DOM.el.style.color = cell.color;
++finished;
if ( finished === this.totalChars ) {
this.isAnimating = false;
}
}
else {
cell.set(this.getRandomChar());
cell.color = ['#2b4539', '#61dca3', '#61b3dc'][Math.floor(Math.random() * 3)]
cell.DOM.el.style.color = cell.color
}
++iteration;
if ( iteration < MAX_CELL_ITERATIONS ) {
setTimeout(() => loop(line, cell, iteration), randomNumber(30,110));
}
};
for (const line of this.lines) {
for (const cell of line.cells) {
setTimeout(() => loop(line, cell), (line.position+1)*80);
}
}
}
/**
* call the right effect method (defined in this.effects)
* @param {string} effect - effect type
*/
trigger(effect = 'fx1') {
if ( !(effect in this.effects) || this.isAnimating ) return;
this.isAnimating = true;
this.effects[effect]();
}
}src/js/utils.js
// Preload images
const preloadFonts = id => {
return new Promise((resolve) => {
WebFont.load({
typekit: {
id: id
},
active: resolve
});
});
};
const randomNumber = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
export {
preloadFonts,
randomNumber
}Media credits and license evidence실행 안내·자료
README.md
## License
[MIT](LICENSE)
Made with :blue_heart: by [Codrops](http://www.codrops.com)
LICENSE실행 안내·자료
MIT License
Copyright (c) 2009 - 2022 [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실행 안내·자료
splitting@1.1.0 — LICENSE.md
MIT License
Copyright (c) 2018 Stephen Shaw
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
이 장면을 만드는 원리
이전 셀의 글자를 전달하는 셔플
시각적인 글자 분할과 사용자에게 한 글자로 보이는 단위를 구분합니다.
- 이 예제에서는
- 첫 셔플은 각 셀의 이전 값을 cache로 저장하고 다음 셀이 그 값을 가져오게 합니다. 마지막 반복에서는 original로 복귀하며 공백은 반복 횟수 계산에서 다르게 취급합니다.
코드와 함께 확인하기
코드에서 찾기
셀별 timeout 반복과 원문 복원을 관리합니다.
직접 해보기
공백이 많은 문장·빈 문구·복합 문자를 사용하고 실행 중 제거합니다.
살펴볼 변화원문 복귀와 잠금 해제, 남은 timeout을 확인하고 문자 이동을 논리 텍스트 변경으로 오인하지 않게 해야 합니다.
