@sugarat/theme 0.5.7 → 0.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/node.d.ts
CHANGED
|
@@ -186,6 +186,27 @@ declare namespace Theme {
|
|
|
186
186
|
* @default false
|
|
187
187
|
*/
|
|
188
188
|
blogInfoCollapsible?: boolean;
|
|
189
|
+
/**
|
|
190
|
+
* 旋转的配置
|
|
191
|
+
*/
|
|
192
|
+
hoverSpin?: boolean | HoverSpinConfig;
|
|
193
|
+
}
|
|
194
|
+
interface HoverSpinConfig {
|
|
195
|
+
/**
|
|
196
|
+
* 旋转的加速度
|
|
197
|
+
* @default 180
|
|
198
|
+
*/
|
|
199
|
+
accel?: number;
|
|
200
|
+
/**
|
|
201
|
+
* 旋转的最大角速度
|
|
202
|
+
* @default 2160
|
|
203
|
+
*/
|
|
204
|
+
maxVel?: number;
|
|
205
|
+
/**
|
|
206
|
+
* 缓停时长
|
|
207
|
+
* @default 1500
|
|
208
|
+
*/
|
|
209
|
+
decelDuration?: number;
|
|
189
210
|
}
|
|
190
211
|
interface ArticleConfig {
|
|
191
212
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sugarat/theme",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.8",
|
|
4
4
|
"description": "简约风的 Vitepress 博客主题,sugarat vitepress blog theme",
|
|
5
5
|
"author": "sugar",
|
|
6
6
|
"license": "MIT",
|
|
@@ -53,10 +53,10 @@
|
|
|
53
53
|
"vitepress-plugin-group-icons": "1.2.4",
|
|
54
54
|
"vitepress-plugin-mermaid": "2.0.13",
|
|
55
55
|
"vitepress-plugin-tabs": "0.2.0",
|
|
56
|
-
"vitepress-plugin-
|
|
57
|
-
"
|
|
58
|
-
"vitepress-plugin-
|
|
59
|
-
"
|
|
56
|
+
"vitepress-plugin-rss": "0.3.3",
|
|
57
|
+
"@sugarat/theme-shared": "0.0.6",
|
|
58
|
+
"vitepress-plugin-pagefind": "0.4.16",
|
|
59
|
+
"vitepress-plugin-announcement": "0.1.5"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@element-plus/icons-vue": "^2.3.1",
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { useData, withBase } from 'vitepress'
|
|
3
|
-
import { computed } from 'vue'
|
|
4
|
-
import {
|
|
3
|
+
import { computed, ref } from 'vue'
|
|
4
|
+
import { useGlobalAuthor, useHomeConfig } from '../composables/config/blog'
|
|
5
|
+
import { useHoverSpin } from '../hooks/useHoverSpin'
|
|
5
6
|
|
|
6
7
|
const home = useHomeConfig()
|
|
7
8
|
const { frontmatter, site } = useData()
|
|
@@ -20,11 +21,15 @@ const logo = computed(() =>
|
|
|
20
21
|
?? '/logo.png'
|
|
21
22
|
)
|
|
22
23
|
const show = computed(() => author.value || logo.value)
|
|
24
|
+
|
|
25
|
+
const imgRef = ref(null)
|
|
26
|
+
|
|
27
|
+
useHoverSpin(imgRef, home?.value?.hoverSpin)
|
|
23
28
|
</script>
|
|
24
29
|
|
|
25
30
|
<template>
|
|
26
31
|
<div v-if="show" class="blog-author">
|
|
27
|
-
<img v-if="logo" :src="withBase(logo)" alt="avatar">
|
|
32
|
+
<img v-if="logo" ref="imgRef" :src="withBase(logo)" alt="avatar">
|
|
28
33
|
<p v-if="author">
|
|
29
34
|
{{ author }}
|
|
30
35
|
</p>
|
|
@@ -42,12 +47,7 @@ const show = computed(() => author.value || logo.value)
|
|
|
42
47
|
height: 100px;
|
|
43
48
|
border-radius: 50%;
|
|
44
49
|
background-color: rgba(var(--bg-gradient-home));
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
img:hover {
|
|
48
|
-
transform: rotate(666turn);
|
|
49
|
-
transition-duration: 59s;
|
|
50
|
-
transition-timing-function: cubic-bezier(.34, 0, .84, 1)
|
|
50
|
+
cursor: pointer;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
p {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { useData, withBase } from 'vitepress'
|
|
3
|
-
import { computed } from 'vue'
|
|
3
|
+
import { computed, ref } from 'vue'
|
|
4
4
|
import { useHomeConfig } from '../composables/config/blog'
|
|
5
|
+
import { useHoverSpin } from '../hooks/useHoverSpin'
|
|
5
6
|
|
|
6
7
|
const home = useHomeConfig()
|
|
7
8
|
const { frontmatter, site } = useData()
|
|
@@ -13,11 +14,14 @@ const logo = computed(() =>
|
|
|
13
14
|
?? '/logo.png'
|
|
14
15
|
)
|
|
15
16
|
const alwaysHide = computed(() => frontmatter.value.blog?.minScreenAvatar === false)
|
|
17
|
+
|
|
18
|
+
const imgRef = ref(null)
|
|
19
|
+
useHoverSpin(imgRef, home?.value?.hoverSpin)
|
|
16
20
|
</script>
|
|
17
21
|
|
|
18
22
|
<template>
|
|
19
23
|
<div v-show="!alwaysHide" class="blog-home-header-avatar">
|
|
20
|
-
<img :src="withBase(logo)" alt="avatar">
|
|
24
|
+
<img ref="imgRef" :src="withBase(logo)" alt="avatar">
|
|
21
25
|
</div>
|
|
22
26
|
</template>
|
|
23
27
|
|
|
@@ -36,13 +40,14 @@ const alwaysHide = computed(() => frontmatter.value.blog?.minScreenAvatar === fa
|
|
|
36
40
|
background-color: transparent;
|
|
37
41
|
border: 5px solid rgba(var(--bg-gradient-home));
|
|
38
42
|
box-sizing: border-box;
|
|
43
|
+
cursor: pointer;
|
|
39
44
|
}
|
|
40
45
|
|
|
41
|
-
img:hover {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
+
// img:hover {
|
|
47
|
+
// transform: rotate(666turn);
|
|
48
|
+
// transition-duration: 59s;
|
|
49
|
+
// transition-timing-function: cubic-bezier(.34, 0, .84, 1)
|
|
50
|
+
// }
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
@media screen and (min-width: 768px) {
|
|
@@ -191,6 +191,28 @@ export namespace Theme {
|
|
|
191
191
|
* @default false
|
|
192
192
|
*/
|
|
193
193
|
blogInfoCollapsible?: boolean
|
|
194
|
+
/**
|
|
195
|
+
* 旋转的配置
|
|
196
|
+
*/
|
|
197
|
+
hoverSpin?: boolean | HoverSpinConfig
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export interface HoverSpinConfig {
|
|
201
|
+
/**
|
|
202
|
+
* 旋转的加速度
|
|
203
|
+
* @default 180
|
|
204
|
+
*/
|
|
205
|
+
accel?: number
|
|
206
|
+
/**
|
|
207
|
+
* 旋转的最大角速度
|
|
208
|
+
* @default 2160
|
|
209
|
+
*/
|
|
210
|
+
maxVel?: number
|
|
211
|
+
/**
|
|
212
|
+
* 缓停时长
|
|
213
|
+
* @default 1500
|
|
214
|
+
*/
|
|
215
|
+
decelDuration?: number
|
|
194
216
|
}
|
|
195
217
|
|
|
196
218
|
export interface ArticleConfig {
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { type Ref, onMounted, onUnmounted } from 'vue'
|
|
2
|
+
import type { Theme } from '../composables/config/index'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 元素悬停旋转
|
|
6
|
+
* @param elRef 元素引用
|
|
7
|
+
* @param accel 加速度
|
|
8
|
+
* @param maxVel 最大角速度
|
|
9
|
+
* @param decelDuration 缓停时长
|
|
10
|
+
*/
|
|
11
|
+
export function useHoverSpin(elRef: Ref<HTMLElement | null>, hoverSpinConfig?: Theme.HoverSpinConfig | boolean) {
|
|
12
|
+
const { accel = 180, maxVel = 2160, decelDuration = 1500 } = hoverSpinConfig === true ? {} : hoverSpinConfig || {}
|
|
13
|
+
let rafId = 0
|
|
14
|
+
let lastTs = 0
|
|
15
|
+
let rotation = 0
|
|
16
|
+
let velocity = 0
|
|
17
|
+
let decelStart = 0
|
|
18
|
+
let decelStartRotation = 0
|
|
19
|
+
let decelTargetRotation = 0
|
|
20
|
+
let decelBias = 0
|
|
21
|
+
let isDecel = false
|
|
22
|
+
let hovering = false
|
|
23
|
+
|
|
24
|
+
function animate(ts: number) {
|
|
25
|
+
if (!lastTs)
|
|
26
|
+
lastTs = ts
|
|
27
|
+
const dt = (ts - lastTs) / 1000
|
|
28
|
+
lastTs = ts
|
|
29
|
+
|
|
30
|
+
if (hovering) {
|
|
31
|
+
isDecel = false
|
|
32
|
+
decelStart = 0
|
|
33
|
+
velocity = Math.min(velocity + accel * dt, maxVel)
|
|
34
|
+
rotation += velocity * dt
|
|
35
|
+
}
|
|
36
|
+
else if (isDecel) {
|
|
37
|
+
const t = Math.min((ts - decelStart) / decelDuration, 1)
|
|
38
|
+
const durSec = decelDuration / 1000
|
|
39
|
+
const base = velocity * durSec * (t - t * t / 2)
|
|
40
|
+
const smooth = 3 * t * t - 2 * t * t * t
|
|
41
|
+
rotation = decelStartRotation + base + decelBias * smooth
|
|
42
|
+
if (t >= 1) {
|
|
43
|
+
decelTargetRotation = 0
|
|
44
|
+
decelStartRotation = 0
|
|
45
|
+
decelBias = 0
|
|
46
|
+
rotation = decelTargetRotation
|
|
47
|
+
velocity = 0
|
|
48
|
+
isDecel = false
|
|
49
|
+
if (rafId) {
|
|
50
|
+
cancelAnimationFrame(rafId)
|
|
51
|
+
rafId = 0
|
|
52
|
+
}
|
|
53
|
+
const el2 = elRef.value as HTMLElement | null
|
|
54
|
+
if (el2)
|
|
55
|
+
el2.style.transform = `rotate(${rotation}deg)`
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const el = elRef.value as HTMLElement | null
|
|
61
|
+
if (el)
|
|
62
|
+
el.style.transform = `rotate(${rotation}deg)`
|
|
63
|
+
if (hovering || isDecel) {
|
|
64
|
+
rafId = requestAnimationFrame(animate)
|
|
65
|
+
}
|
|
66
|
+
else if (rafId) {
|
|
67
|
+
cancelAnimationFrame(rafId)
|
|
68
|
+
rafId = 0
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function onEnter() {
|
|
73
|
+
hovering = true
|
|
74
|
+
if (!rafId) {
|
|
75
|
+
lastTs = 0
|
|
76
|
+
rafId = requestAnimationFrame(animate)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function onLeave() {
|
|
81
|
+
hovering = false
|
|
82
|
+
isDecel = true
|
|
83
|
+
decelStart = performance.now()
|
|
84
|
+
decelStartRotation = rotation
|
|
85
|
+
const durSec = decelDuration / 1000
|
|
86
|
+
const S = velocity * durSec / 2
|
|
87
|
+
const minTarget = decelStartRotation + S
|
|
88
|
+
decelTargetRotation = Math.ceil(minTarget / 360) * 360
|
|
89
|
+
decelBias = (decelTargetRotation - decelStartRotation) - S
|
|
90
|
+
lastTs = 0
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
onMounted(() => {
|
|
94
|
+
if (hoverSpinConfig === false)
|
|
95
|
+
return
|
|
96
|
+
const el = elRef.value
|
|
97
|
+
if (!el)
|
|
98
|
+
return
|
|
99
|
+
el.addEventListener('mouseenter', onEnter)
|
|
100
|
+
el.addEventListener('mouseleave', onLeave)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
onUnmounted(() => {
|
|
104
|
+
if (hoverSpinConfig === false)
|
|
105
|
+
return
|
|
106
|
+
const el = elRef.value
|
|
107
|
+
if (el) {
|
|
108
|
+
el.removeEventListener('mouseenter', onEnter)
|
|
109
|
+
el.removeEventListener('mouseleave', onLeave)
|
|
110
|
+
}
|
|
111
|
+
if (rafId)
|
|
112
|
+
cancelAnimationFrame(rafId)
|
|
113
|
+
})
|
|
114
|
+
}
|