Intro 구현할 때 뒤로 밀어두었던 타이핑 효과를 추가하였다.
# 계획
1. 오픈 소스 이용
1.1 react-type-animation
가장 먼저 고려하였던건 'react-type-animation'이다.
https://www.npmjs.com/package/react-type-animation
react-type-animation
Customizable React typing animation component based on typical.. Latest version: 3.1.0, last published: 2 months ago. Start using react-type-animation in your project by running `npm i react-type-animation`. There are 7 other projects in the npm registry u
www.npmjs.com
ReactComponent 형태로 사용 가능한 점과 sequence로 텍스트, 딜레이, 콜백 등을 실행할 수 있는 점이 편리해보였다.
예시 (https://react-type-animation.netlify.app/examples)
<TypeAnimation
sequence={[
// Same substring at the start will only be typed out once, initially
'We produce food for Mice',
1000, // wait 1s before replacing "Mice" with "Hamsters"
'We produce food for Hamsters',
1000,
'We produce food for Guinea Pigs',
1000,
'We produce food for Chinchillas',
1000
]}
wrapper="span"
speed={50}
style={{ fontSize: '2em', display: 'inline-block' }}
repeat={Infinity}
/>
하지만 나는 동적으로 타이핑 효과를 조절하고 싶었으나 그건 불가능해 보여서 패스.
1.2 typed.js
동적으로 컨트롤할 수 있는 오픈 소스를 찾다 발견한 것이 'typed.js'이다.
https://mattboldt.com/demos/typed-js/
JavaScript Animated Typing with Typed.js | by Matt Boldt
Another demo made with love by Matt Boldt. Installation # With NPM npm install typed.js # With Yarn yarn add typed.js # With Bower bower install typed.js Setup var typed = new Typed('.element', { strings: ["First sentence.", "Second sentence."], typeSpeed:
mattboldt.com
react-type-animation보다 사용법은 살짝 더 복잡하지만 Typed 객체를 통해 동적으로 컨트롤 가능하다.
예시 (https://mattboldt.github.io/typed.js/)
var typed = new Typed("#typed", {
stringsElement: '#typed-strings',
typeSpeed: 0,
backSpeed: 0,
backDelay: 500,
startDelay: 1000,
loop: false,
onBegin: function(self) { prettyLog('onBegin ' + self) },
onComplete: function(self) { prettyLog('onCmplete ' + self) },
preStringTyped: function(pos, self) { prettyLog('preStringTyped ' + pos + ' ' + self); },
onStringTyped: function(pos, self) { prettyLog('onStringTyped ' + pos + ' ' + self) },
onLastStringBackspaced: function(self) { prettyLog('onLastStringBackspaced ' + self) },
onTypingPaused: function(pos, self) { prettyLog('onTypingPaused ' + pos + ' ' + self) },
onTypingResumed: function(pos, self) { prettyLog('onTypingResumed ' + pos + ' ' + self) },
onReset: function(self) { prettyLog('onReset ' + self) },
onStop: function(pos, self) { prettyLog('onStop ' + pos + ' ' + self) },
onStart: function(pos, self) { prettyLog('onStart ' + pos + ' ' + self) },
onDestroy: function(self) { prettyLog('onDestroy ' + self) }
});
다만 나는 필요 시 텍스트를 backsapce 형식으로 지워줄 method가 필요한데, 그런 방법이 보이지 않는다.
결국에 이 방법도 패스.
2. 직접 구현
결국 내가 원하는대로 하려면 직접 구현이 답이다.
다행히 타이핑 효과는 구현하기 어려운 기능이 아니므로 직접 구현하여 사용하기로 하였다.
# 구현
내가 원하는 결과물에 대해 글로 설명하기보단 직접 보는 게 이해가 빠르다.
따라서 최종 결과물부터 보도록 하자.
Typing Object
Typing 효과를 구현할 객체를 생성하였다.
생성자의 파라미터로 효과를 적용할 element와 여러 옵션을 받을 수 있게 구성하였다.
export default class Typing {
constructor(
element: HTMLElement | null,
options: {
str: string;
speed?: number;
backSpeed?: number;
preDelay?: number;
postDelay?: number;
showCursor?: boolean;
removeCursorAtFinish?: boolean;
}
) {
this.element = element;
this.str = options.str || '';
this.speed = options.speed || 20;
this.backSpeed = options.backSpeed || 20;
this.preDelay = options.preDelay || 0;
this.postDelay = options.postDelay || 0;
this.showCursor =
options.showCursor === undefined ? true : options.showCursor;
this.removeCursorAtFinish = options.removeCursorAtFinish || false;
this.curIdx = 0;
}
element: HTMLSpanElement | null;
str: string;
speed: number; // char per second
backSpeed: number; // char per second
preDelay: number; // ms
postDelay: number; // ms
showCursor: boolean;
removeCursorAtFinish: boolean;
curIdx: number;
timeoutID?: number;
// ...
}
멤버 변수 중 curIdx와 timeoutID는 내부적으로 사용하기 위한 변수이다.
내가 필요한 기능은 글자 쓰기, 지우기, 다시 시작이다.
각각을 start, clear, restart method로 구현하였다.
export default class Typing {
// ...
async start() {
// preDelay
await this.wait(this.preDelay);
// write
if (this.showCursor) this.addCursor('typing-cursor');
while (this.curIdx < this.str.length) {
await this.write();
}
if (this.showCursor) {
this.removeCursor();
this.addCursor('waiting-cursor');
}
// postDelay
await this.wait(this.postDelay);
if (this.removeCursorAtFinish) this.removeCursor();
return new Promise((res) => res(''));
}
async clear() {
clearTimeout(this.timeoutID);
if (this.showCursor) this.addCursor('typing-cursor');
while (this.curIdx > 0) {
await this.backspace();
}
if (this.showCursor) {
this.removeCursor();
this.addCursor('waiting-cursor');
}
if (this.removeCursorAtFinish) this.removeCursor();
}
async restart() {
clearTimeout(this.timeoutID);
if (this.showCursor) this.addCursor('typing-cursor');
while (this.curIdx < this.str.length) {
await this.write();
}
if (this.showCursor) {
this.removeCursor();
this.addCursor('waiting-cursor');
}
await this.wait(this.postDelay);
if (this.removeCursorAtFinish) this.removeCursor();
}
// ...
}
start 함수는 여러 Typing 객체를 chaining하여 사용할 수 있도록 promise를 반환하도록 하였다.
각 method에서 내부적으로 사용되는 함수들은 다음과 같다.
export default class Typing {
// ...
async write() {
if (!this.element) return false;
this.curIdx += 1;
this.element.innerText = this.str.slice(0, this.curIdx);
await this.wait(1000 / this.speed);
}
async backspace() {
if (!this.element) return false;
this.curIdx -= 1;
this.element.innerText = this.str.slice(0, this.curIdx);
await this.wait(1000 / this.backSpeed);
}
wait(ms: number) {
return new Promise((res) => {
this.timeoutID = window.setTimeout(res, ms);
});
}
addCursor(cursor: string) {
this.element?.classList.add(cursor);
}
removeCursor() {
this.element?.classList.remove('typing-cursor');
this.element?.classList.remove('waiting-cursor');
}
}
쓰고 지우는 동작은 innerText를 대체하는 방식으로 구현하였고, 현재 진행상황을 저장하기 위해 curIdx 멤버 변수를 사용한다.
start 실행 중 clear 실행 등의 상황에 대처하기 위해 wait 함수에서 setTimeout ID를 저장해놓았다가 다른 method 호출 시 clearTimeout하도록 하였다.
cursor의 경우에는 typing 중 항상 커서가 표시되기 위한 'typing-cursor' class와 대기 중 깜박이기 위한 'waiting-cursor'를 CSS로 추가하여 사용하였다.
.typing-cursor {
position: relative;
}
.typing-cursor:after {
position: absolute;
content: '|';
}
.waiting-cursor {
position: relative;
}
.waiting-cursor:after {
position: absolute;
content: '|';
animation: cursor 1s infinite step-start;
}
@keyframes cursor {
50% {
opacity: 0;
}
}
이로서 구현은 끝이다.
다만, restart가 어떤 경우에 사용되는지 의문을 가질 수 있을 것 같아 마지막으로 해당 케이스의 예시를 남겨둔다.
welcome page에서 mouse left button hold 중 mouse up이 들어왔을 때 문구를 다시 띄워주기 위해 사용된다.
'Projects > Portfolio' 카테고리의 다른 글
Portfolio v2 - 0. 사전 계획 (0) | 2023.12.13 |
---|---|
Next.js 포트폴리오 페이지 제작기 - 8. 배포 (0) | 2023.08.29 |
Next.js 포트폴리오 페이지 제작기 - 6. Contact & Navigation (0) | 2023.08.09 |
Next.js 포트폴리오 페이지 제작기 - 5. Projects part (0) | 2023.08.05 |
Next.js 포트폴리오 페이지 제작기 - 4. About part (0) | 2023.08.04 |