@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.7",
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-pagefind": "0.4.15",
57
- "vitepress-plugin-announcement": "0.1.5",
58
- "vitepress-plugin-rss": "0.3.2",
59
- "@sugarat/theme-shared": "0.0.6"
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 { useBlogConfig, useGlobalAuthor, useHomeConfig } from '../composables/config/blog'
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
- transform: rotate(666turn);
43
- transition-duration: 59s;
44
- transition-timing-function: cubic-bezier(.34, 0, .84, 1)
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
+ }