Umi4/Max实现主题切换和过渡效果!

Umi4/Max实现主题切换和过渡效果!

在工作中现在其实很常见的就是主题切换,主题切换挺简单的,但是如果让实现主题切换实现一个过渡的效果还是有点难度的。接下来我们就实现umi4中的主题切换和过渡动画效果,这个其实我也是参照的别人的,在其上面做了一些改动,如果有什么问题希望大家可以交流一下。

封装切换主题的组件,避免代码冗余

这里使用到了纯html+css画出的一个主题切换的组件,以下是封装的代码,只是简单的实现,都是加了TS版本的 ,创建component/SetTheme/index.tsx

javascript 复制代码
import { crop, toCanvas } from '@/utils/setTheme';
import { useAntdConfigSetter } from '@umijs/max'; // 引入设置antdConfig的配置
import { theme } from 'antd';
import { memo, useRef, useState } from 'react';
import './index.css';
const Index = () => {
  const [flag, setFlag] = useState<'dark' | 'light'>('dark');
  const setAntdConfig = useAntdConfigSetter();
  const targetRef = useRef<HTMLLabelElement>(null);
  const setTheme = () => {
    let them = flag === 'light' ? 'dark' : 'light';
    setFlag(them as 'dark' | 'light');
    toCanvas(document.getElementById('root') as HTMLDivElement).then(
      (canvas) => {
        document
          .getElementById('root')
          ?.appendChild(canvas as HTMLCanvasElement);
        crop(
          canvas as HTMLCanvasElement,
          targetRef.current as HTMLLabelElement,
          {
            reverse: flag === 'dark',
          },
        ).then((canvas) => {
          //绘制结束后删除canvas
          document
            .getElementById('root')
            ?.removeChild(canvas as HTMLCanvasElement);
        });
        setAntdConfig({
          theme: {
            algorithm: [
              flag === 'dark' ? theme.darkAlgorithm : theme.defaultAlgorithm,
            ],
          },
        });
      },
    );
  };
  return (
    <>
      <label className="switch" ref={targetRef} onChange={setTheme}>
        <span className="sun">
          <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
            <g fill="#ffd43b">
              <circle r="5" cy="12" cx="12"></circle>
              <path d="m21 13h-1a1 1 0 0 1 0-2h1a1 1 0 0 1 0 2zm-17 0h-1a1 1 0 0 1 0-2h1a1 1 0 0 1 0 2zm13.66-5.66a1 1 0 0 1 -.66-.29 1 1 0 0 1 0-1.41l.71-.71a1 1 0 1 1 1.41 1.41l-.71.71a1 1 0 0 1 -.75.29zm-12.02 12.02a1 1 0 0 1 -.71-.29 1 1 0 0 1 0-1.41l.71-.66a1 1 0 0 1 1.41 1.41l-.71.71a1 1 0 0 1 -.7.24zm6.36-14.36a1 1 0 0 1 -1-1v-1a1 1 0 0 1 2 0v1a1 1 0 0 1 -1 1zm0 17a1 1 0 0 1 -1-1v-1a1 1 0 0 1 2 0v1a1 1 0 0 1 -1 1zm-5.66-14.66a1 1 0 0 1 -.7-.29l-.71-.71a1 1 0 0 1 1.41-1.41l.71.71a1 1 0 0 1 0 1.41 1 1 0 0 1 -.71.29zm12.02 12.02a1 1 0 0 1 -.7-.29l-.66-.71a1 1 0 0 1 1.36-1.36l.71.71a1 1 0 0 1 0 1.41 1 1 0 0 1 -.71.24z"></path>
            </g>
          </svg>
        </span>
        <span className="moon">
          <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512">
            <path d="m223.5 32c-123.5 0-223.5 100.3-223.5 224s100 224 223.5 224c60.6 0 115.5-24.2 155.8-63.4 5-4.9 6.3-12.5 3.1-18.7s-10.1-9.7-17-8.5c-9.8 1.7-19.8 2.6-30.1 2.6-96.9 0-175.5-78.8-175.5-176 0-65.8 36-123.1 89.3-153.3 6.1-3.5 9.2-10.5 7.7-17.3s-7.3-11.9-14.3-12.5c-6.3-.5-12.6-.8-19-.8z"></path>
          </svg>
        </span>
        <input type="checkbox" className="input" />
        <span className="slider"></span>
      </label>
    </>
  );
};

export default memo(Index);

css的样式

css 复制代码
.switch {
  font-size: 17px;
  position: relative;
  display: inline-block;
  width: 64px;
  height: 34px;
}

.switch input {
  opacity: 0;
  width: 0;
  height: 0;
}

.slider {
  position: absolute;
  cursor: pointer;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: #73c0fc;
  transition: 0.4s;
  border-radius: 30px;
}

.slider:before {
  position: absolute;
  content: '';
  height: 30px;
  width: 30px;
  border-radius: 20px;
  left: 2px;
  bottom: 2px;
  z-index: 2;
  background-color: #e8e8e8;
  transition: 0.4s;
}

.sun svg {
  position: absolute;
  top: 6px;
  left: 36px;
  z-index: 1;
  width: 24px;
  height: 24px;
}

.moon svg {
  fill: #73c0fc;
  position: absolute;
  top: 5px;
  left: 5px;
  z-index: 1;
  width: 24px;
  height: 24px;
}

/* .switch:hover */
.sun svg {
  animation: rotate 15s linear infinite;
}

@keyframes rotate {
  0% {
    transform: rotate(0);
  }

  100% {
    transform: rotate(360deg);
  }
}

/* .switch:hover */
.moon svg {
  animation: tilt 5s linear infinite;
}

@keyframes tilt {
  0% {
    transform: rotate(0deg);
  }

  25% {
    transform: rotate(-10deg);
  }

  75% {
    transform: rotate(10deg);
  }

  100% {
    transform: rotate(0deg);
  }
}

.input:checked + .slider {
  background-color: #183153;
}

.input:focus + .slider {
  box-shadow: 0 0 1px #183153;
}

.input:checked + .slider:before {
  transform: translateX(30px);
}

封装过渡效果的方法

utils/setTheme.ts 这里是利用了 html2canvas的插件,可以将dom元素转换为cavans的功能,然后再画布上进行一些操作,最后是使用了原型销毁获取点击位置的函数实现了过渡的播放

/* 复制代码
import html2canvas from 'html2canvas';
export function toCanvas(el: HTMLDivElement) {
    return new Promise(resolve => {
        // 转换为图片
        const rect = el.getBoundingClientRect();
        html2canvas(el, {
            logging: false, // 禁用日志输出  
            scale: 1, // 放大截图两倍  
            useCORS: true, // 如果需要跨域资源,启用这个选项  
            width: rect.width, // 指定截图的宽度  
            height: rect.height // 指定截图的高度  
        }).then(canvas => {
            const base64Image = canvas.toDataURL('image/png').replace('image/png', 'image/octet-stream');
            const rect = el.getBoundingClientRect();
            //canvas样式设置(位置大小)
            canvas.style.position = "fixed"
            canvas.style.left = rect.left + "px"
            canvas.style.top = rect.top + "px"
            canvas.width = rect.width
            canvas.height = rect.height
            canvas.style.width = rect.width + 'px'
            canvas.style.height = rect.height + 'px'
            const context = canvas.getContext('2d');
            //创建一个Image元素,并将其源设置为转换后的URI
            const img = new Image();
            img.src = base64Image;
            // 当图像加载完成时,将图像绘制到 canvas 上
            img.onload = () => {
                context!.drawImage(img, 0, 0);
                setTimeout(() => {
                    resolve(canvas)
                })
            }
        })
    })
}

export function easeInOutQuint(elapsed: number, initialValue: number, amountOfChange: number, duration: number) {

    if ((elapsed /= duration / 2) < 1) {
        return amountOfChange / 2 * elapsed * elapsed * elapsed * elapsed * elapsed + initialValue;
    }
    return amountOfChange / 2 * ((elapsed -= 2) * elapsed * elapsed * elapsed * elapsed + 2) + initialValue;
}

export function easeInOutQuart(elapsed: number, initialValue: number, amountOfChange: number, duration: number) {
    if ((elapsed /= duration / 2) < 1) {
        return amountOfChange / 2 * elapsed * elapsed * elapsed * elapsed + initialValue;
    }
    return -amountOfChange / 2 * ((elapsed -= 2) * elapsed * elapsed * elapsed - 2) + initialValue;
}

function getMousePos(canvas: HTMLCanvasElement, evt: HTMLLabelElement) {
    const rect = canvas.getBoundingClientRect();
    return {
        x: ((evt.offsetLeft - rect.left) / (rect.right - rect.left) * canvas.width),
        y: ((evt.offsetTop - rect.top) / (rect.bottom - rect.top) * canvas.height)
    };
}

function getMaxRadius(canvas: HTMLCanvasElement) {
    return Math.sqrt(Math.pow(canvas.width, 2) + Math.pow(canvas.height, 2));
}

export const crop = (canvas: HTMLCanvasElement, initialPosition: HTMLLabelElement, { reverse = false }) => {
    const ctx = canvas.getContext('2d');
    const { x, y } = getMousePos(canvas, initialPosition);
    const maxRadius = getMaxRadius(canvas);
    return new Promise(resolve => {
        let progress = 0;
        const duration = 60;
        ctx!.fillStyle = 'rgba(255, 255, 255, 1)';
        ctx!.globalCompositeOperation = reverse ? 'destination-in' : 'destination-out';
        function draw() {
            let radius;
            if (reverse) {
                radius = easeInOutQuint(progress, maxRadius, -maxRadius, duration);

            } else {
                radius = easeInOutQuart(progress, 0, maxRadius, duration);

            }
            ctx!.beginPath();
            ctx!.arc(x, y, radius, 0, Math.PI * 2, false);
            ctx!.fill();
            progress++;

            if (progress < duration) {
                requestAnimationFrame(draw);
            } else {
                resolve(canvas);
            }
        }

        draw();
    })
}

这里解释一下我为什么选择root这个根节点,因为我是后台管理的系统 所以选择了root根节点作为图片展示的dom,确保整个页面可以有过渡效果。

最后就可以查看自己的页面效果了

相关推荐
老坛0018 分钟前
2025决策延迟的椭圆算子分析:锐减协同工具的谱间隙优化
前端
老坛0019 分钟前
从记录到预测:2025新一代预算工具如何通过AI实现前瞻性资金管理
前端
今禾11 分钟前
" 当Base64遇上Blob,图像转换不再神秘,让你的网页瞬间变身魔法画布! "
前端·数据可视化
华科云商xiao徐16 分钟前
高性能小型爬虫语言与代码示例
前端·爬虫
十盒半价17 分钟前
深入理解 React useEffect:从基础到实战的全攻略
前端·react.js·trae
攀登的牵牛花17 分钟前
Electron+Vue+Python全栈项目打包实战指南
前端·electron·全栈
iccb101318 分钟前
我是如何实现在线客服系统的极致稳定性与安全性的
前端·javascript·后端
一大树19 分钟前
Vue3祖孙组件通信方法总结
前端·vue.js
不要进入那温驯的良夜20 分钟前
跨平台UI自动化-Appium
前端
海底火旺20 分钟前
以一个简单的React应用理解数据绑定的重要性
前端·css·react.js