263 lines
6.5 KiB
Svelte
263 lines
6.5 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from 'svelte'
|
|
|
|
// 基础定义部分
|
|
type WriterStage = { text: string; editing: string[] } | string
|
|
type WriterWord = { stages: WriterStage[] }
|
|
|
|
// 辅助函数
|
|
const typeCharacters = (base: string, chars: string) => [
|
|
base,
|
|
...chars.split('').map((_, i) => base + chars.slice(0, i + 1)),
|
|
]
|
|
|
|
const words: WriterWord[] = [
|
|
{
|
|
stages: [
|
|
{ text: '', editing: ['x', 'xi', 'xih', "xi'hr"] },
|
|
{ text: '喜欢', editing: ['y', 'yb', 'ybl'] },
|
|
...typeCharacters('喜欢音', 'MAD'),
|
|
],
|
|
},
|
|
{
|
|
stages: [
|
|
{ text: '', editing: ['x', 'xi', 'xih', "xi'hr"] },
|
|
...typeCharacters('喜欢', 'YTPMV'),
|
|
],
|
|
},
|
|
{
|
|
stages: [
|
|
{ text: '', editing: ['x', 'xi', 'xih', "xi'hr"] },
|
|
{ text: '喜欢', editing: ['u', 'ui', 'uij', "ui'jt"] },
|
|
{ text: '喜欢视觉', editing: ['y', 'yi', 'yiu', "yi'uu"] },
|
|
'喜欢视觉艺术',
|
|
],
|
|
},
|
|
{
|
|
stages: [
|
|
{ text: '', editing: ['x', 'xt', 'xtx', "xt'xi"] },
|
|
{ text: '学习', editing: ['q', 'qm', 'qmd', "qm'dr"] },
|
|
{ text: '学习前端', editing: ['k', 'kd', 'kdf', "kd'fa"] },
|
|
'学习前端开发',
|
|
],
|
|
},
|
|
{
|
|
stages: [
|
|
{ text: '', editing: ['x', 'xt', 'xtx', "xt'xi"] },
|
|
{ text: '学习', editing: ['h', 'hz', "hz'd", "hz'dr"] },
|
|
{ text: '学习后端', editing: ['k', 'kd', 'kdf', "kd'fa"] },
|
|
'学习后端开发',
|
|
],
|
|
},
|
|
{
|
|
stages: [
|
|
{ text: '', editing: ['x', 'xt', 'xtx', "xt'xi"] },
|
|
{
|
|
text: '学习',
|
|
editing: ['q', 'qm', 'qmr', "qm'ru", "qm'ru'u", "qm'ru'ui"],
|
|
},
|
|
'学习嵌入式',
|
|
],
|
|
},
|
|
{
|
|
stages: [
|
|
{ text: '', editing: ['x', 'xt', 'xtx', "xt'xi"] },
|
|
...typeCharacters('学习', 'DevOps'),
|
|
],
|
|
},
|
|
{
|
|
stages: [
|
|
{ text: '', editing: ['z', 'zr', "zr'y", "zr'yj"] },
|
|
{ text: '钻研', editing: ['h', 'hy', 'hyd'] },
|
|
{ text: '钻研混', editing: ['m', 'mu'] },
|
|
'钻研混母',
|
|
],
|
|
},
|
|
{
|
|
stages: [
|
|
{ text: '', editing: ['z', 'zr', "zr'y", "zr'yj"] },
|
|
{ text: '钻研', editing: ['u', 'uz', "uz'g", "uz'gs"] },
|
|
{ text: '钻研手工', editing: ['k', 'kd', 'kdf', "kd'fa"] },
|
|
'钻研手工开发',
|
|
],
|
|
},
|
|
{
|
|
stages: [
|
|
{ text: '', editing: ['b', 'bw', "bw'v", "bw'vj"] },
|
|
{ text: '备战', editing: ['k', 'kc', "kc'y", "kc'yj"] },
|
|
'备战考研',
|
|
],
|
|
},
|
|
]
|
|
|
|
// 状态机定义
|
|
type StatusMachine = {
|
|
wipWord: number
|
|
wipStage: number
|
|
wipEditing: number
|
|
countdown: number
|
|
}
|
|
let status = $state<StatusMachine>({
|
|
wipWord: 0,
|
|
wipStage: 0,
|
|
wipEditing: 0,
|
|
countdown: 0,
|
|
})
|
|
|
|
// --- 显示 ---
|
|
/** 当前正在处理的词语 */
|
|
let wipWord = $derived(words[status.wipWord])
|
|
|
|
/** 当前正在处理的词语的阶段。如果是最后一个阶段(也就是删除阶段),则取最后
|
|
* 一个元素。
|
|
*/
|
|
let wipStage = $derived(
|
|
wipWord.stages[Math.min(status.wipStage, wipWord.stages.length - 1)],
|
|
)
|
|
|
|
/** 当前的文本(未经过裁剪的,不含输入法未上屏部分) */
|
|
let currentText = $derived(
|
|
typeof wipStage == 'string' ? wipStage : wipStage.text,
|
|
)
|
|
|
|
/** 如果在最后一个阶段,则根据进度去裁剪文本 */
|
|
let currentTextSliced = $derived(
|
|
status.wipStage >= wipWord.stages.length
|
|
? currentText.slice(0, currentText.length - status.wipEditing)
|
|
: currentText,
|
|
)
|
|
|
|
/** 输入法待上屏区域 */
|
|
let currentEditing = $derived(
|
|
typeof wipStage == 'string' || status.wipEditing <= 0
|
|
? ''
|
|
: wipStage.editing[status.wipEditing - 1],
|
|
)
|
|
|
|
// 主要的状态更新逻辑
|
|
const DELAY_STEP_WORD = 250
|
|
const DELAY_STEP_STAGE = 80
|
|
const DELAY_STEP_EDIT = 80
|
|
const DELAY_STEP_DELETE = 50
|
|
const DELAY_DISPLAY_HOLD = 3000
|
|
|
|
// 调试用,如果发现动画有问题,可以在这里调慢动画
|
|
const DELAY_GLOBAL_FACTOR = 1
|
|
|
|
function step(status: StatusMachine, dt: number): StatusMachine {
|
|
if (status.countdown > 0) {
|
|
return {
|
|
...status,
|
|
countdown: status.countdown - dt / DELAY_GLOBAL_FACTOR,
|
|
}
|
|
}
|
|
|
|
if (typeof wipStage == 'object') {
|
|
if (status.wipEditing < wipStage.editing.length) {
|
|
return {
|
|
...status,
|
|
wipEditing: status.wipEditing + 1,
|
|
countdown: status.countdown + DELAY_STEP_EDIT,
|
|
}
|
|
}
|
|
} else if (status.wipStage >= wipWord.stages.length) {
|
|
// 此时执行删除操作
|
|
if (status.wipEditing < currentText.length) {
|
|
return {
|
|
...status,
|
|
wipEditing: status.wipEditing + 1,
|
|
countdown: status.countdown + DELAY_STEP_DELETE,
|
|
}
|
|
}
|
|
}
|
|
|
|
if (status.wipStage < wipWord.stages.length) {
|
|
return {
|
|
...status,
|
|
wipStage: status.wipStage + 1,
|
|
wipEditing: 0,
|
|
countdown:
|
|
status.wipStage == wipWord.stages.length - 1
|
|
? DELAY_DISPLAY_HOLD
|
|
: DELAY_STEP_STAGE,
|
|
}
|
|
}
|
|
|
|
return {
|
|
...status,
|
|
wipWord: (status.wipWord + 1) % words.length,
|
|
wipStage: 0,
|
|
wipEditing: 0,
|
|
countdown: DELAY_STEP_WORD,
|
|
}
|
|
}
|
|
|
|
// 时间循环
|
|
onMount(() => {
|
|
let enabled = true
|
|
let lastTime = Date.now()
|
|
|
|
function _step() {
|
|
if (!enabled) return
|
|
|
|
let currentTime = Date.now()
|
|
|
|
status = step(status, currentTime - lastTime)
|
|
lastTime = currentTime
|
|
|
|
requestAnimationFrame(_step)
|
|
}
|
|
|
|
_step()
|
|
|
|
return () => {
|
|
enabled = false
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<span class="typewriter"
|
|
><span style="user-select: none;"></span><span class="text"
|
|
>{currentTextSliced}</span
|
|
><span class="edit">{currentEditing}</span></span
|
|
>
|
|
|
|
<style>
|
|
@keyframes blink {
|
|
from,
|
|
to {
|
|
opacity: 1;
|
|
}
|
|
50% {
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
.typewriter {
|
|
display: inline-block;
|
|
position: relative;
|
|
background-color: var(--color-bg-1);
|
|
padding: 0.25rem 0.5rem;
|
|
margin: 0 0.25rem;
|
|
border-radius: 0.5rem;
|
|
|
|
& > .edit {
|
|
text-decoration: underline;
|
|
/* font-family: var(--font-mono); */
|
|
}
|
|
|
|
&::after {
|
|
--gap-ud: 0.5rem;
|
|
|
|
content: '';
|
|
position: absolute;
|
|
inset-inline-end: 0.5rem;
|
|
inset-block-start: var(--gap-ud);
|
|
block-size: calc(100% - var(--gap-ud) * 2);
|
|
inline-size: 2px;
|
|
background-color: var(--color-fg-0);
|
|
animation: blink 1s step-end infinite;
|
|
}
|
|
}
|
|
</style>
|