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