Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 1503a20

Browse files
authored
feat(theme): optimize view transition (#725)
1 parent a2d5260 commit 1503a20

File tree

7 files changed

+124
-50
lines changed

7 files changed

+124
-50
lines changed

docs/.vuepress/plume.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export default defineThemeConfig({
1515
organization: 'pengzhanbo',
1616
},
1717

18+
transition: { appearance: 'circle-clip' },
19+
1820
social: [
1921
{ icon: 'github', link: 'https://github.com/pengzhanbo/vuepress-theme-plume' },
2022
{ icon: 'qq', link: 'https://qm.qq.com/q/FbPPoOIscE' },

docs/config/theme.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -686,7 +686,7 @@ interface SidebarItem {
686686
* 或配置过渡动画类型
687687
* @default 'fade'
688688
*/
689-
appearance?: boolean | 'fade' | 'circle-clip' | 'horizontal-clip' | 'vertical-clip' | 'skew-clip'
689+
appearance?: boolean | 'fade' | 'circle-clip' | 'horizontal-clip' | 'vertical-clip' | 'skew-clip' | 'blinds-vertical' | 'blinds-horizontal' | 'soft-blur-fade' | 'diamond-reveal'
690690
}
691691
```
692692

docs/en/config/theme.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -691,7 +691,7 @@ interface SidebarItem {
691691
* or configure the transition animation type.
692692
* @default 'fade'
693693
*/
694-
appearance?: boolean | 'fade' | 'circle-clip' | 'horizontal-clip' | 'vertical-clip' | 'skew-clip'
694+
appearance?: boolean | 'fade' | 'circle-clip' | 'horizontal-clip' | 'vertical-clip' | 'skew-clip' | 'blinds-vertical' | 'blinds-horizontal' | 'soft-blur-fade' | 'diamond-reveal'
695695
}
696696
```
697697

theme/src/client/components/VPSwitchAppearance.vue

Lines changed: 19 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script lang="ts" setup>
22
import VPSwitch from '@theme/VPSwitch.vue'
33
import { computed, inject, nextTick, ref, watchPostEffect } from 'vue'
4-
import { enableTransitions, useData } from '../composables/index.js'
4+
import { enableTransitions, resolveTransitionKeyframes, useData } from '../composables/index.js'
55
66
const checked = ref(false)
77
const { theme, isDark } = useData()
@@ -15,7 +15,7 @@ const transitionMode = computed(() => {
1515
return typeof options.appearance === 'string' ? options.appearance : 'fade'
1616
})
1717
18-
const toggleAppearance = inject('toggle-appearance', async ({ clientX: x, clientY: y }: MouseEvent) => {
18+
const toggleAppearance = inject('toggle-appearance', async ({ clientX, clientY }: MouseEvent) => {
1919
if (!enableTransitions() || transitionMode.value === false) {
2020
isDark.value = !isDark.value
2121
return
@@ -26,45 +26,12 @@ const toggleAppearance = inject('toggle-appearance', async ({ clientX: x, client
2626
await nextTick()
2727
}).ready
2828
29-
const keyframes: PropertyIndexedKeyframes = {}
30-
const mode = transitionMode.value
31-
let duration = 400
32-
33-
if (mode === 'circle-clip') {
34-
const clipPath = [
35-
`circle(0px at ${x}px ${y}px)`,
36-
`circle(${Math.hypot(
37-
Math.max(x, innerWidth - x),
38-
Math.max(y, innerHeight - y),
39-
)}px at ${x}px ${y}px)`,
40-
]
41-
keyframes.clipPath = isDark.value ? clipPath.reverse() : clipPath
42-
}
43-
else if (mode === 'horizontal-clip') {
44-
const clipPath = [
45-
`inset(0px ${innerWidth}px 0px 0px)`,
46-
`inset(0px 0px 0px 0px)`,
47-
]
48-
keyframes.clipPath = isDark.value ? clipPath.reverse() : clipPath
49-
}
50-
else if (mode === 'vertical-clip') {
51-
const clipPath = [
52-
`inset(0px 0px ${innerHeight}px 0px)`,
53-
`inset(0px 0px 0px 0px)`,
54-
]
55-
keyframes.clipPath = isDark.value ? clipPath.reverse() : clipPath
56-
}
57-
else if (mode === 'skew-clip') {
58-
const clipPath = [
59-
'polygon(0px 0px, 0px 0px, 0px 0px)',
60-
`polygon(0px 0px, ${innerWidth * 2}px 0px, 0px ${innerHeight * 2}px)`,
61-
]
62-
keyframes.clipPath = isDark.value ? clipPath.reverse() : clipPath
63-
}
64-
else {
65-
keyframes.opacity = isDark.value ? [1, 0] : [0, 1]
66-
duration = 300
67-
}
29+
const { keyframes, duration } = resolveTransitionKeyframes(
30+
clientX,
31+
clientY,
32+
transitionMode.value,
33+
isDark.value,
34+
)
6835
6936
document.documentElement.animate(
7037
keyframes,
@@ -86,12 +53,7 @@ watchPostEffect(() => {
8653
</script>
8754

8855
<template>
89-
<VPSwitch
90-
class="vp-switch-appearance"
91-
:title="switchTitle"
92-
:aria-checked="checked"
93-
@click="toggleAppearance"
94-
>
56+
<VPSwitch class="vp-switch-appearance" :title="switchTitle" :aria-checked="checked" @click="toggleAppearance">
9557
<span class="vpi-sun sun" />
9658
<span class="vpi-moon moon" />
9759
</VPSwitch>
@@ -121,13 +83,23 @@ watchPostEffect(() => {
12183
</style>
12284

12385
<style>
86+
[data-theme] {
87+
will-change: clip-path, filter, opacity;
88+
}
89+
12490
::view-transition-image-pair(root) {
12591
isolation: auto;
12692
}
12793
94+
::view-transition-group(root) {
95+
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
96+
}
97+
12898
::view-transition-old(root),
12999
::view-transition-new(root) {
100+
clip-path: none;
130101
mix-blend-mode: normal;
102+
mask: none;
131103
transition: none !important;
132104
animation: none !important;
133105
}

theme/src/client/composables/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,5 @@ export * from './sidebar-data.js'
3333
export * from './sidebar.js'
3434
export * from './tag-colors.js'
3535
export * from './theme-data.js'
36+
export * from './view-transition.js'
3637
export * from './watermark.js'
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import type { TransitionOptions } from '../../shared/index.js'
2+
3+
type TransitionMode = Exclude<TransitionOptions['appearance'], boolean>
4+
interface TransitionResult extends PropertyIndexedKeyframes {
5+
duration?: number
6+
}
7+
8+
type TransitionStrategy = {
9+
[key in NonNullable<TransitionMode>]: (
10+
reverse: (effect: string[]) => string[],
11+
context: { x: number, y: number, isDark: boolean }
12+
) => TransitionResult
13+
}
14+
15+
const strategy: TransitionStrategy = {
16+
// 淡入淡出
17+
'fade': reverse => ({ opacity: reverse(['0', '1']), duration: 300 }),
18+
// 圆形裁剪
19+
'circle-clip': (reverse, { x, y }) => ({
20+
clipPath: reverse([
21+
`circle(0px at ${x}px ${y}px)`,
22+
`circle(${Math.hypot(
23+
Math.max(x, innerWidth - x),
24+
Math.max(y, innerHeight - y),
25+
)}px at ${x}px ${y}px)`,
26+
]),
27+
duration: 650,
28+
}),
29+
// 横向裁剪
30+
'horizontal-clip': reverse => ({
31+
clipPath: reverse([
32+
`inset(0px ${innerWidth}px 0px 0px)`,
33+
`inset(0px 0px 0px 0px)`,
34+
]),
35+
}),
36+
// 纵向裁剪
37+
'vertical-clip': reverse => ({
38+
clipPath: reverse([
39+
`inset(0px 0px ${innerHeight}px 0px)`,
40+
`inset(0px 0px 0px 0px)`,
41+
]),
42+
}),
43+
// 倾斜裁剪
44+
'skew-clip': reverse => ({
45+
clipPath: reverse([
46+
'polygon(0px 0px, 0px 0px, 0px 0px)',
47+
`polygon(0px 0px, ${innerWidth * 2}px 0px, 0px ${innerHeight * 2}px)`,
48+
]),
49+
}),
50+
51+
// 百叶窗效果 上下展开
52+
'blinds-vertical': reverse => ({
53+
clipPath: reverse([
54+
'inset(50% 0% 50% 0%)',
55+
'inset(0 0 0 0)',
56+
]),
57+
}),
58+
// 百叶窗效果 左右展开
59+
'blinds-horizontal': reverse => ({
60+
clipPath: reverse([
61+
'polygon(50% 0, 50% 100%, 50% 100%, 50% 0)',
62+
'polygon(0 0, 0 100%, 100% 100%, 100% 0)',
63+
]),
64+
}),
65+
// 模糊淡出
66+
'soft-blur-fade': reverse => ({
67+
opacity: reverse(['0', '1']),
68+
filter: reverse(['blur(10px)', 'blur(0px)']),
69+
duration: 380,
70+
}),
71+
// 菱形裁剪展开
72+
'diamond-reveal': reverse => ({
73+
clipPath: reverse([
74+
`polygon(50% 50%, 50% 50%, 50% 50%, 50% 50%)`,
75+
`polygon(50% -50%, 150% 50%, 50% 150%, -50% 50%)`,
76+
]),
77+
duration: 500,
78+
}),
79+
}
80+
81+
export function resolveTransitionKeyframes(
82+
x: number,
83+
y: number,
84+
mode: TransitionMode,
85+
isDark: boolean,
86+
): {
87+
keyframes: PropertyIndexedKeyframes
88+
duration: number
89+
} {
90+
if (!mode || !strategy[mode])
91+
mode = 'fade'
92+
93+
const reverse = (effect: string[]): string[] => {
94+
return isDark ? effect.reverse() : effect
95+
}
96+
97+
const { duration = 400, ...keyframes } = strategy[mode](reverse, { x, y, isDark })
98+
return { keyframes, duration }
99+
}

theme/src/shared/features/transition.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@ export interface TransitionOptions {
1313
* 是否启用 深色/浅色 模式切换过渡动画
1414
* @default 'fade'
1515
*/
16-
appearance?: boolean | 'fade' | 'circle-clip' | 'horizontal-clip' | 'vertical-clip' | 'skew-clip'
16+
appearance?: boolean | 'fade' | 'circle-clip' | 'horizontal-clip' | 'vertical-clip' | 'skew-clip' | 'blinds-vertical' | 'blinds-horizontal' | 'soft-blur-fade' | 'diamond-reveal'
1717
}

0 commit comments

Comments
 (0)