2 changed files with 207 additions and 0 deletions
@ -0,0 +1,115 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
/* SongLoading — 水墨晕染加载 §7 |
||||
|
* 墨点如滴入宣纸般扩散、敛去,合成层动效(transform/opacity only) |
||||
|
* mask: 全屏遮罩模式(弹层加载) */ |
||||
|
withDefaults( |
||||
|
defineProps<{ |
||||
|
visible?: boolean |
||||
|
mask?: boolean |
||||
|
text?: string |
||||
|
}>(), |
||||
|
{ visible: true, mask: false, text: '墨迹未干' } |
||||
|
) |
||||
|
defineOptions({ name: 'SongLoading' }) |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div v-if="visible" class="song-loading" :class="{ 'song-loading--mask': mask }"> |
||||
|
<div class="song-loading__inner"> |
||||
|
<div class="song-loading__inks" aria-hidden="true"> |
||||
|
<i class="song-loading__ink" /> |
||||
|
<i class="song-loading__ink" /> |
||||
|
<i class="song-loading__ink" /> |
||||
|
</div> |
||||
|
<div class="song-loading__abs" aria-hidden="true" /> |
||||
|
<span v-if="text" class="song-loading__text caption">{{ text }}</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped> |
||||
|
.song-loading { |
||||
|
display: grid; |
||||
|
place-items: center; |
||||
|
padding: 40px 0; |
||||
|
} |
||||
|
|
||||
|
.song-loading--mask { |
||||
|
position: fixed; |
||||
|
inset: 0; |
||||
|
z-index: 950; |
||||
|
background: var(--song-bg-elevated); |
||||
|
opacity: 0.96; |
||||
|
} |
||||
|
|
||||
|
.song-loading__inner { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
gap: 18px; |
||||
|
} |
||||
|
|
||||
|
.song-loading__inks { |
||||
|
position: relative; |
||||
|
width: 64px; |
||||
|
height: 64px; |
||||
|
} |
||||
|
|
||||
|
.song-loading__ink { |
||||
|
position: absolute; |
||||
|
left: 50%; |
||||
|
top: 50%; |
||||
|
width: 40px; |
||||
|
height: 40px; |
||||
|
border-radius: 50%; |
||||
|
background: radial-gradient(circle, var(--song-primary) 0%, rgba(79, 120, 184, 0) 68%); |
||||
|
opacity: 0; |
||||
|
transform: translate(-50%, -50%) scale(0.3); |
||||
|
animation: song-ink-soak 2.4s var(--song-ease) infinite; |
||||
|
will-change: transform, opacity; |
||||
|
} |
||||
|
|
||||
|
.song-loading__ink:nth-child(2) { |
||||
|
animation-delay: 0.8s; |
||||
|
} |
||||
|
|
||||
|
.song-loading__ink:nth-child(3) { |
||||
|
animation-delay: 1.6s; |
||||
|
} |
||||
|
|
||||
|
.song-loading__abs { |
||||
|
position: absolute; |
||||
|
left: 50%; |
||||
|
top: 50%; |
||||
|
width: 52px; |
||||
|
height: 52px; |
||||
|
margin: -26px 0 0 -26px; |
||||
|
border: 1px solid var(--song-border-strong); |
||||
|
border-radius: 50%; |
||||
|
} |
||||
|
|
||||
|
.song-loading__text { |
||||
|
letter-spacing: 0.4em; |
||||
|
color: var(--song-text-secondary); |
||||
|
} |
||||
|
|
||||
|
@keyframes song-ink-soak { |
||||
|
0% { |
||||
|
transform: translate(-50%, -50%) scale(0.3); |
||||
|
opacity: 0; |
||||
|
} |
||||
|
|
||||
|
35% { |
||||
|
opacity: 0.85; |
||||
|
} |
||||
|
|
||||
|
80% { |
||||
|
opacity: 0; |
||||
|
} |
||||
|
|
||||
|
100% { |
||||
|
transform: translate(-50%, -50%) scale(1.15); |
||||
|
opacity: 0; |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,92 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
/* SongSeal — 章印(钤印意象),SVG 自绘图形。 |
||||
|
* 方印 ≤4 字(田字排布),圆印单字(连珠双圈)。 |
||||
|
* 颜色走 --song-seal(印泥砂红),rotate 模拟手钤微妙倾斜。 */ |
||||
|
import { computed } from 'vue' |
||||
|
|
||||
|
const props = withDefaults( |
||||
|
defineProps<{ |
||||
|
text?: string |
||||
|
round?: boolean |
||||
|
size?: number |
||||
|
rotate?: number |
||||
|
}>(), |
||||
|
{ text: '檐下', round: false, size: 76, rotate: -2 } |
||||
|
) |
||||
|
defineOptions({ name: 'SongSeal' }) |
||||
|
|
||||
|
const chars = computed(() => props.text.slice(0, props.round ? 1 : 4).split('')) |
||||
|
const fd = computed<number[]>(() => { |
||||
|
const n = chars.value.length |
||||
|
if (n <= 1) return [0] |
||||
|
if (n === 2) return [0, 1] |
||||
|
return [0, 1, 2, 3] |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<svg |
||||
|
class="song-seal" |
||||
|
:width="size" |
||||
|
:height="size" |
||||
|
:viewBox="round ? '0 0 64 64' : '0 0 72 72'" |
||||
|
role="img" |
||||
|
:aria-label="`印章:${text}`" |
||||
|
:style="{ transform: `rotate(${rotate}deg)` }" |
||||
|
> |
||||
|
<!-- 方印:田字界格 + 2×2 文字 --> |
||||
|
<g v-if="!round" fill="none" stroke="currentColor"> |
||||
|
<rect x="2" y="2" width="68" height="68" rx="3" /> |
||||
|
<line v-if="fd.length > 1" x1="36" y1="2" x2="36" y2="70" /> |
||||
|
<line v-if="fd.length > 2" x1="2" y1="36" x2="70" y2="36" /> |
||||
|
</g> |
||||
|
<!-- 圆印:连珠双圈 + 单字 --> |
||||
|
<g v-else fill="none" stroke="currentColor"> |
||||
|
<circle cx="32" cy="32" r="28" /> |
||||
|
<circle cx="32" cy="32" r="21" /> |
||||
|
</g> |
||||
|
<g |
||||
|
v-if="!round" |
||||
|
fill="currentColor" |
||||
|
fill-opacity="0.88" |
||||
|
font-family="var(--song-font-serif)" |
||||
|
font-weight="600" |
||||
|
text-anchor="middle" |
||||
|
dominant-baseline="central" |
||||
|
> |
||||
|
<text |
||||
|
v-for="(c, i) in chars" |
||||
|
:key="i" |
||||
|
:x="36 + (i % 2 === 0 ? -18 : 18)" |
||||
|
:y="36 + (i < 2 ? -18 : 18)" |
||||
|
opacity="0.72" |
||||
|
>{{ c }}</text> |
||||
|
</g> |
||||
|
<text |
||||
|
v-else |
||||
|
x="32" |
||||
|
y="32" |
||||
|
fill="currentColor" |
||||
|
fill-opacity="0.88" |
||||
|
font-family="var(--song-font-serif)" |
||||
|
font-weight="600" |
||||
|
font-size="22" |
||||
|
text-anchor="middle" |
||||
|
dominant-baseline="central" |
||||
|
>{{ chars[0] }}</text> |
||||
|
</svg> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped> |
||||
|
.song-seal { |
||||
|
color: var(--song-seal, #a84635); |
||||
|
display: block; |
||||
|
opacity: 0.9; |
||||
|
transition: transform var(--song-dur-2) var(--song-ease); |
||||
|
} |
||||
|
|
||||
|
.song-seal:hover { |
||||
|
opacity: 1; |
||||
|
transform: rotate(-1deg) !important; |
||||
|
} |
||||
|
</style> |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue