主页框架
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-03-31 22:54:14 +08:00
parent 36a6ff369d
commit a87c6faa1e
11 changed files with 1267 additions and 20 deletions

View File

@ -52,4 +52,46 @@
}
</script>
<button onclick={click}>没用的按钮 | {clicks + clicksPending}</button>
<button class="useless-button" onclick={click}>
<span class="name">到此一游</span>
<span class="number">{clicks + clicksPending}</span>
</button>
<style>
.useless-button {
display: block;
background-color: var(--color-bg-n);
font-size: 1.25rem;
cursor: pointer;
border-radius: 1rem;
overflow: hidden;
transition: transform ease 0.1s;
&:hover {
transform: scale(1.02);
}
&:active {
transform: scale(0.97);
}
& > .name {
display: inline-block;
font-weight: 300;
padding-inline-start: 1rem;
padding-inline-end: 0.5rem;
padding-block: 0.5rem;
}
& > .number {
display: inline-block;
padding-inline-start: 0.75rem;
padding-inline-end: 0.75rem;
padding-block: 0.5rem;
background-color: var(--color-green);
color: var(--color-bg-n);
font-weight: 600;
}
}
</style>

View File

@ -0,0 +1,244 @@
<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"] },
'钻研手工开发',
],
},
]
// 状态机定义
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>