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,确保整个页面可以有过渡效果。