修身养性,知行合一

  • 首页
  • 爱码
    • 系统
    • 数据库
    • JavaScript
    • CSharp
    • Python
  • 生活
    • 文化
    • 美食
  • 杂谈
  • 关于
修身养性,知行合一
码字,杂谈
  1. 首页
  2. 爱码
  3. 前端
  4. JavaScript
  5. 正文

实现ElementPlus炫酷的亮暗切换效果

2024年10月15日 3243点热度 6人点赞 1条评论

一直觉得 Element-plus 的亮暗切换很漂亮,最近抽时间研究了一下,技术还是比较新的,甚至在打包的时候,对应 api 还报了找不到声明的问题,也算是小坑。

吐槽一下:前端真是天天卷样式,实在搞不动了

实现原理

我们先来看一下 element-plus 的效果:

分析一下可以看出,要想实现这个效果,至少需要四步:

1、找到点击的位置
2、再找到距离点击位置的最远位置(以上图官方为例,点击的是右上角,那么最远的点应该就是左下角)
3、基于上面的点,画一个大圆
4、让这个圆动起来,实现效果切换~

前面三个都不难,重点就是最后一步,这个我一直以为使用的是蒙版~

实现基础切换

我创建了一个管理框架,并使用了 Element-Plus 和 vueuse。

首先,创建这个 switch 按钮,并实现基础逻辑:

<template>
  <el-switch
    class="XSwitchTheme"
    :model-value="isDark"
    @change="onChange"
    @click.capture="onClick"
  >
    <template #active-action>🌙</template>
    <template #inactive-action>🌞</template>
  </el-switch>
</template>

<script setup lang="ts">
import { useDark } from '@/hooks/useDark';
import { ref } from 'vue';

const { isDark, mode } = useDark();

const pos = ref({ x: 0, y: 0 });
function onClick(e: MouseEvent) {
  pos.value = { x: e.clientX, y: e.clientY };
}

function onChange(val: boolean) {
  mode.value = val ? 'dark' : 'light';
}
</script>

因为使用了 vueuse 的 useColorMode 来管理亮暗模式,所以 swtich 中需要使用只读的方式传值,在 change 事件中去修改它。

useDark 的逻辑也很简单:

import { useColorMode } from '@vueuse/core';
import { computed } from 'vue';

export const useDark = () => {
  const mode = useColorMode({
    emitAuto: true,
    storageKey: "ACTIVE_COLOR_SCHEME",
    disableTransition: false,
    initialValue: 'light'
  });

  return {
    isDark: computed(
      () =>
        (mode.store.value === 'auto' ? mode.system.value : mode.store.value) ===
        'dark'
    ),
    mode
  };
};

这样就可以适配亮暗,并且还可以根据系统自动选择。

现在,我们的页面也可以完成切换效果:

找到远端点,并绘制大圆

在上面的代码中,已经找到了每次点击的位置,现在,我们需要找到对应的远点:

我们要做的是,无论切换按钮在哪里,都可以找到远点,如图示例:

file

也就是:

1、找到 x 轴的较大值
2、找到 y 轴的较大值
3、基于 x 和 y 值,相当于一个直角三角形,求斜边边长,这个边长就是大圆的半径

这里要用到 js 的原生方法 Math.hypot,它可以很方便的求出斜边:

const radius = Math.hypot(
  Math.max(pos.value.x, window.innerWidth - pos.value.x),
  Math.max(pos.value.y, window.innerHeight - pos.value.y)
);

我们有了半径,怎么画圆呢?这里就需要用到 css 提供的一个属性:

  • clip-path:使用裁剪方式创建元素的可显示区域。区域内的部分显示,区域外的隐藏。

有兴趣的朋友可以自行查看 MDN文档

clip-path

比如我们现在在页面上这样写:

clip-path: circle(10% at 50% center);

关于 circle 更多内容可以查看 MDN文档

我们将它挂在文本上:

<h1 class="text-100px" style="clip-path: circle(10% at 50% center)">JEREMYJONE</h1>

看到的效果就是:

file

它将 h1 标签进行了裁切,大小为 10%,位置在 50%、中央的位置,它只展示这个区域的内容。

有了上面关于 circle 的基础,我们可以想象,基于点击位置,画一个上面计算出来的 radius 半径大小的圆。它应该这样写:

`clip-path: circle(${radius}px at ${pos.value.x}px ${pos.value.y}px)`

我们先看一下效果,所以我们直接在切换后,将半径设置为 500px,我们把它画在根节点上:

function onChange(val: boolean) {
  mode.value = val ? 'dark' : 'light';

  // 半径固定设为 500px
  document.documentElement.style.clipPath = `circle(500px at ${pos.value.x}px ${pos.value.y}px)`;
}

可以看到在点击后,圆已经出现了。现在就要实现它的动画。

实现动画

要实现这个动画,就要使用一个比较新的 API 了。

startViewTransition,它开始一个新的视图过渡,并返回一个 ViewTransition 对象来表示它。

说人话,就是在页面发生变化后启动一个动画,并可以通过伪类实现不同效果。直接上用法:

// 调用 startViewTransition 实现动画
document.startViewTransition(() => { mode.value = val? 'dark' : 'light' }).ready.then(() => {
  document.documentElement.animate(
    {
      // 圆,从半径为0,到半径最大,然后让其实现动画,也就实现了圆自动展开的效果
      clipPath: [
        `circle(0px at ${pos.value.x}px ${pos.value.y}px)`,
        `circle(${radius}px at ${pos.value.x}px ${pos.value.y}px)`
      ]
    },
    {
      duration: 600,
      pseudoElement: '::view-transition-new(root)'
    }
  )
})

这里的 ::view-transition-new(root) 伪类是应用在 startViewTransition API 中的,具体可以看 W3C技术规范

现在我们就可以看到动画效果了:

但是,效果并不明显,这是因为这个伪类有默认效果,我们需要重置它:

::view-transition-new(root),
::view-transition-old(root) {
  animation: none;
  mix-blend-mode: normal;
}

这样就看到效果了。

实现反向效果

反向效果的实现就很简单了,只需要将其动画翻转即可:

function onChange(val: boolean) {
  const setTheme = () => {
    mode.value = val ? 'dark' : 'light';
  };

  const doAnimate = () => {
    const radius = Math.hypot(
      Math.max(pos.value.x, window.innerWidth - pos.value.x),
      Math.max(pos.value.y, window.innerHeight - pos.value.y)
    );

    const clipPath = [
      `circle(0px at ${pos.value.x}px ${pos.value.y}px)`,
      `circle(${radius}px at ${pos.value.x}px ${pos.value.y}px)`
    ];

    document.documentElement.animate(
      // 通过 val 值判断展示方向
      { clipPath: val ? clipPath.reverse() : clipPath },
      {
        duration: 600,
        pseudoElement: val
          // 这里一样,通过 val 值判断方向
          ? '::view-transition-old(root)'
          : '::view-transition-new(root)'
      }
    );
  };

  document.startViewTransition
    ? document.startViewTransition(setTheme).ready.then(doAnimate)
    : setTheme();
}

这里有一个细节,就是反向的时候,我们仍然需要添加一个 css,否则 old 会因为层级不够,而看不到效果,因为 new 默认永远在 old 上面:

.dark::view-transition-old(root) {
  z-index: 9999999999;
}

需要注意,添加的时候要挂载 dark 属性,否则变亮时,也会看不到效果了。

至此,我们就完成了整个效果。

完整代码

<template>
  <el-switch
    class="XSwitchTheme"
    :model-value="isDark"
    @change="onChange"
    @click.capture="onClick"
  >
    <template #active-action>🌙</template>
    <template #inactive-action>🌞</template>
  </el-switch>
</template>

<script setup lang="ts">
import { useDark } from '@/hooks/useDark';
import { ref } from 'vue';

const { isDark, mode } = useDark();

const pos = ref({ x: 0, y: 0 });
function onClick(e: MouseEvent) {
  pos.value = { x: e.clientX, y: e.clientY };
}

function onChange(val: boolean) {
  const setTheme = () => {
    mode.value = val ? 'dark' : 'light';
  };

  const doAnimate = () => {
    const radius = Math.hypot(
      Math.max(pos.value.x, window.innerWidth - pos.value.x),
      Math.max(pos.value.y, window.innerHeight - pos.value.y)
    );

    const clipPath = [
      `circle(0px at ${pos.value.x}px ${pos.value.y}px)`,
      `circle(${radius}px at ${pos.value.x}px ${pos.value.y}px)`
    ];

    document.documentElement.animate(
      { clipPath: val ? clipPath.reverse() : clipPath },
      {
        duration: 600,
        pseudoElement: val
          ? '::view-transition-old(root)'
          : '::view-transition-new(root)'
      }
    );
  };

  document.startViewTransition
    ? document.startViewTransition(setTheme).ready.then(doAnimate)
    : setTheme();
}
</script>

<style>
.XSwitchTheme {
  .el-switch__action {
    background-color: transparent;
  }
}

::view-transition-new(root),
::view-transition-old(root) {
  animation: none;
  mix-blend-mode: normal;
}
.dark::view-transition-old(root) {
  z-index: 9999999999;
}
</style>
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可
标签: JavaScript vue
最后更新:2024年10月15日

jeremyjone

这个人很懒,什么都没留下

打赏 点赞
< 上一篇
下一篇 >

文章评论

  • 米優

    老師!!謝謝你的教學!!我成功應用到自己網站中了
    多虧有您!

    2025年5月7日
    回复
  • 取消回复

    文章目录
    • 实现原理
    • 实现基础切换
    • 找到远端点,并绘制大圆
      • clip-path
    • 实现动画
      • 实现反向效果
    • 完整代码
    最新 热点 随机
    最新 热点 随机
    node-sass 的安装 解决端口被占的问题 vue3 组件 Props 的声明方式 给 div 添加选中状态 请求的取消 rgb 颜色小数兼容问题
    GIT删除指定的某次版本提交 美化PowerShell(含WindowsTerminal和VSCode终端) rgb 颜色小数兼容问题 docker 自动更新 windows 无法登录便签、OneNote等应用 MySql中timestamp创建日期的时区问题

    (っ•̀ω•́)っ✎⁾⁾ 开心每一天

    COPYRIGHT © 2021 jeremyjone.com. ALL RIGHTS RESERVED.

    THEME KRATOS MADE BY VTROIS

    京ICP备19012859号-1

    京公网安备 11010802028585号