mirror of
https://github.com/pengzhanbo/vuepress-theme-plume.git
synced 2026-04-23 10:58:13 +08:00
feat(theme): optimize view transition (#725)
This commit is contained in:
parent
a2d52602d3
commit
1503a20fbe
@ -15,6 +15,8 @@ export default defineThemeConfig({
|
||||
organization: 'pengzhanbo',
|
||||
},
|
||||
|
||||
transition: { appearance: 'circle-clip' },
|
||||
|
||||
social: [
|
||||
{ icon: 'github', link: 'https://github.com/pengzhanbo/vuepress-theme-plume' },
|
||||
{ icon: 'qq', link: 'https://qm.qq.com/q/FbPPoOIscE' },
|
||||
|
||||
@ -686,7 +686,7 @@ interface SidebarItem {
|
||||
* 或配置过渡动画类型
|
||||
* @default 'fade'
|
||||
*/
|
||||
appearance?: boolean | 'fade' | 'circle-clip' | 'horizontal-clip' | 'vertical-clip' | 'skew-clip'
|
||||
appearance?: boolean | 'fade' | 'circle-clip' | 'horizontal-clip' | 'vertical-clip' | 'skew-clip' | 'blinds-vertical' | 'blinds-horizontal' | 'soft-blur-fade' | 'diamond-reveal'
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@ -691,7 +691,7 @@ interface SidebarItem {
|
||||
* or configure the transition animation type.
|
||||
* @default 'fade'
|
||||
*/
|
||||
appearance?: boolean | 'fade' | 'circle-clip' | 'horizontal-clip' | 'vertical-clip' | 'skew-clip'
|
||||
appearance?: boolean | 'fade' | 'circle-clip' | 'horizontal-clip' | 'vertical-clip' | 'skew-clip' | 'blinds-vertical' | 'blinds-horizontal' | 'soft-blur-fade' | 'diamond-reveal'
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import VPSwitch from '@theme/VPSwitch.vue'
|
||||
import { computed, inject, nextTick, ref, watchPostEffect } from 'vue'
|
||||
import { enableTransitions, useData } from '../composables/index.js'
|
||||
import { enableTransitions, resolveTransitionKeyframes, useData } from '../composables/index.js'
|
||||
|
||||
const checked = ref(false)
|
||||
const { theme, isDark } = useData()
|
||||
@ -15,7 +15,7 @@ const transitionMode = computed(() => {
|
||||
return typeof options.appearance === 'string' ? options.appearance : 'fade'
|
||||
})
|
||||
|
||||
const toggleAppearance = inject('toggle-appearance', async ({ clientX: x, clientY: y }: MouseEvent) => {
|
||||
const toggleAppearance = inject('toggle-appearance', async ({ clientX, clientY }: MouseEvent) => {
|
||||
if (!enableTransitions() || transitionMode.value === false) {
|
||||
isDark.value = !isDark.value
|
||||
return
|
||||
@ -26,45 +26,12 @@ const toggleAppearance = inject('toggle-appearance', async ({ clientX: x, client
|
||||
await nextTick()
|
||||
}).ready
|
||||
|
||||
const keyframes: PropertyIndexedKeyframes = {}
|
||||
const mode = transitionMode.value
|
||||
let duration = 400
|
||||
|
||||
if (mode === 'circle-clip') {
|
||||
const clipPath = [
|
||||
`circle(0px at ${x}px ${y}px)`,
|
||||
`circle(${Math.hypot(
|
||||
Math.max(x, innerWidth - x),
|
||||
Math.max(y, innerHeight - y),
|
||||
)}px at ${x}px ${y}px)`,
|
||||
]
|
||||
keyframes.clipPath = isDark.value ? clipPath.reverse() : clipPath
|
||||
}
|
||||
else if (mode === 'horizontal-clip') {
|
||||
const clipPath = [
|
||||
`inset(0px ${innerWidth}px 0px 0px)`,
|
||||
`inset(0px 0px 0px 0px)`,
|
||||
]
|
||||
keyframes.clipPath = isDark.value ? clipPath.reverse() : clipPath
|
||||
}
|
||||
else if (mode === 'vertical-clip') {
|
||||
const clipPath = [
|
||||
`inset(0px 0px ${innerHeight}px 0px)`,
|
||||
`inset(0px 0px 0px 0px)`,
|
||||
]
|
||||
keyframes.clipPath = isDark.value ? clipPath.reverse() : clipPath
|
||||
}
|
||||
else if (mode === 'skew-clip') {
|
||||
const clipPath = [
|
||||
'polygon(0px 0px, 0px 0px, 0px 0px)',
|
||||
`polygon(0px 0px, ${innerWidth * 2}px 0px, 0px ${innerHeight * 2}px)`,
|
||||
]
|
||||
keyframes.clipPath = isDark.value ? clipPath.reverse() : clipPath
|
||||
}
|
||||
else {
|
||||
keyframes.opacity = isDark.value ? [1, 0] : [0, 1]
|
||||
duration = 300
|
||||
}
|
||||
const { keyframes, duration } = resolveTransitionKeyframes(
|
||||
clientX,
|
||||
clientY,
|
||||
transitionMode.value,
|
||||
isDark.value,
|
||||
)
|
||||
|
||||
document.documentElement.animate(
|
||||
keyframes,
|
||||
@ -86,12 +53,7 @@ watchPostEffect(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VPSwitch
|
||||
class="vp-switch-appearance"
|
||||
:title="switchTitle"
|
||||
:aria-checked="checked"
|
||||
@click="toggleAppearance"
|
||||
>
|
||||
<VPSwitch class="vp-switch-appearance" :title="switchTitle" :aria-checked="checked" @click="toggleAppearance">
|
||||
<span class="vpi-sun sun" />
|
||||
<span class="vpi-moon moon" />
|
||||
</VPSwitch>
|
||||
@ -121,13 +83,23 @@ watchPostEffect(() => {
|
||||
</style>
|
||||
|
||||
<style>
|
||||
[data-theme] {
|
||||
will-change: clip-path, filter, opacity;
|
||||
}
|
||||
|
||||
::view-transition-image-pair(root) {
|
||||
isolation: auto;
|
||||
}
|
||||
|
||||
::view-transition-group(root) {
|
||||
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root) {
|
||||
clip-path: none;
|
||||
mix-blend-mode: normal;
|
||||
mask: none;
|
||||
transition: none !important;
|
||||
animation: none !important;
|
||||
}
|
||||
|
||||
@ -33,4 +33,5 @@ export * from './sidebar-data.js'
|
||||
export * from './sidebar.js'
|
||||
export * from './tag-colors.js'
|
||||
export * from './theme-data.js'
|
||||
export * from './view-transition.js'
|
||||
export * from './watermark.js'
|
||||
|
||||
99
theme/src/client/composables/view-transition.ts
Normal file
99
theme/src/client/composables/view-transition.ts
Normal file
@ -0,0 +1,99 @@
|
||||
import type { TransitionOptions } from '../../shared/index.js'
|
||||
|
||||
type TransitionMode = Exclude<TransitionOptions['appearance'], boolean>
|
||||
interface TransitionResult extends PropertyIndexedKeyframes {
|
||||
duration?: number
|
||||
}
|
||||
|
||||
type TransitionStrategy = {
|
||||
[key in NonNullable<TransitionMode>]: (
|
||||
reverse: (effect: string[]) => string[],
|
||||
context: { x: number, y: number, isDark: boolean }
|
||||
) => TransitionResult
|
||||
}
|
||||
|
||||
const strategy: TransitionStrategy = {
|
||||
// 淡入淡出
|
||||
'fade': reverse => ({ opacity: reverse(['0', '1']), duration: 300 }),
|
||||
// 圆形裁剪
|
||||
'circle-clip': (reverse, { x, y }) => ({
|
||||
clipPath: reverse([
|
||||
`circle(0px at ${x}px ${y}px)`,
|
||||
`circle(${Math.hypot(
|
||||
Math.max(x, innerWidth - x),
|
||||
Math.max(y, innerHeight - y),
|
||||
)}px at ${x}px ${y}px)`,
|
||||
]),
|
||||
duration: 650,
|
||||
}),
|
||||
// 横向裁剪
|
||||
'horizontal-clip': reverse => ({
|
||||
clipPath: reverse([
|
||||
`inset(0px ${innerWidth}px 0px 0px)`,
|
||||
`inset(0px 0px 0px 0px)`,
|
||||
]),
|
||||
}),
|
||||
// 纵向裁剪
|
||||
'vertical-clip': reverse => ({
|
||||
clipPath: reverse([
|
||||
`inset(0px 0px ${innerHeight}px 0px)`,
|
||||
`inset(0px 0px 0px 0px)`,
|
||||
]),
|
||||
}),
|
||||
// 倾斜裁剪
|
||||
'skew-clip': reverse => ({
|
||||
clipPath: reverse([
|
||||
'polygon(0px 0px, 0px 0px, 0px 0px)',
|
||||
`polygon(0px 0px, ${innerWidth * 2}px 0px, 0px ${innerHeight * 2}px)`,
|
||||
]),
|
||||
}),
|
||||
|
||||
// 百叶窗效果 上下展开
|
||||
'blinds-vertical': reverse => ({
|
||||
clipPath: reverse([
|
||||
'inset(50% 0% 50% 0%)',
|
||||
'inset(0 0 0 0)',
|
||||
]),
|
||||
}),
|
||||
// 百叶窗效果 左右展开
|
||||
'blinds-horizontal': reverse => ({
|
||||
clipPath: reverse([
|
||||
'polygon(50% 0, 50% 100%, 50% 100%, 50% 0)',
|
||||
'polygon(0 0, 0 100%, 100% 100%, 100% 0)',
|
||||
]),
|
||||
}),
|
||||
// 模糊淡出
|
||||
'soft-blur-fade': reverse => ({
|
||||
opacity: reverse(['0', '1']),
|
||||
filter: reverse(['blur(10px)', 'blur(0px)']),
|
||||
duration: 380,
|
||||
}),
|
||||
// 菱形裁剪展开
|
||||
'diamond-reveal': reverse => ({
|
||||
clipPath: reverse([
|
||||
`polygon(50% 50%, 50% 50%, 50% 50%, 50% 50%)`,
|
||||
`polygon(50% -50%, 150% 50%, 50% 150%, -50% 50%)`,
|
||||
]),
|
||||
duration: 500,
|
||||
}),
|
||||
}
|
||||
|
||||
export function resolveTransitionKeyframes(
|
||||
x: number,
|
||||
y: number,
|
||||
mode: TransitionMode,
|
||||
isDark: boolean,
|
||||
): {
|
||||
keyframes: PropertyIndexedKeyframes
|
||||
duration: number
|
||||
} {
|
||||
if (!mode || !strategy[mode])
|
||||
mode = 'fade'
|
||||
|
||||
const reverse = (effect: string[]): string[] => {
|
||||
return isDark ? effect.reverse() : effect
|
||||
}
|
||||
|
||||
const { duration = 400, ...keyframes } = strategy[mode](reverse, { x, y, isDark })
|
||||
return { keyframes, duration }
|
||||
}
|
||||
@ -13,5 +13,5 @@ export interface TransitionOptions {
|
||||
* 是否启用 深色/浅色 模式切换过渡动画
|
||||
* @default 'fade'
|
||||
*/
|
||||
appearance?: boolean | 'fade' | 'circle-clip' | 'horizontal-clip' | 'vertical-clip' | 'skew-clip'
|
||||
appearance?: boolean | 'fade' | 'circle-clip' | 'horizontal-clip' | 'vertical-clip' | 'skew-clip' | 'blinds-vertical' | 'blinds-horizontal' | 'soft-blur-fade' | 'diamond-reveal'
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user