Files
blog-frontend-v2/src/components/MainpageTypewriter.svelte
2026-04-01 22:54:47 +08:00

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;">&#xFEFF;</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>