SVG基本介绍
SVG:可缩放矢量图形,矢量图意味着图像能够被无限放大而不失真或者降低质量,并且可以方便地修改内容。SVG代码是XML语言进行描述,可以结合CSS以及JAVASCRIPT进行交互。
SVG有一些预定义的形状元素,可以供开发者使用
矩形 <rect>
圆形 <circle>
椭圆 <ellipse>
线 <line>
折线 <polyline>
多边形 <polygon>
路径 <path>
基本用法:
<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
<rect width="300" height="100" style="fill:rgb(0,0,255);stroke-width:1;stroke:rgb(0,0,0)" />
</svg>
SVG动画
SVG除了可绘制矢量图形之外,更重要的特点是动画。其自带的<animate>元素,也可以实现动画开发。相比CSS ANIMATION, SVG动画能被更好地“操控”, 丰富动画的表现形式。但如果是更复杂的动画,还是需要使用或借助其他库来完成。
以下场景的动画,可以优先考虑使用SVG动画:
- 描边动画:沿着一个固定的路径去填充、描边
- 路径动画:绕着一个路径去运动
描边动画
很常见的一种描边动画就是圆环倒计时,随着时间,圆环中的蓝色部分逐渐填满整个圆环,或者消失直至整个圆环都是灰色。
import React, { useRef, useEffect } from 'react';
const animateRef = useRef(null);
// 动画重新开始
// animateRef.current?.beginElement();
//
// 监听动画结束
useEffect(() => {
animateRef?.current?.addEventListener('endEvent', () => {
handleClose();
});
return () => {
animateRef?.current?.removeEventListener('endEvent', () => {
handleClose();
});
};
}, [handleClose]);
return <svg width="37" height="37" preserveAspectRatio="xMinYMin meet" viewBox="0,0,37,37">
<circle cx="18" cy="18" r="17" strokeWidth="3" stroke={closeColor} fill="none" transform="matrix(0,-1,1,0,0,36)" strokeOpacity="0.2" />
<circle cx="18" cy="18" r="17" strokeWidth="3" stroke={closeColor} fill="none" transform="matrix(0,-1,1,0,0,36)" strokeDasharray="0 106" fillOpacity="1">
<animate attributeName="stroke-dasharray" begin="0s" dur="8s" from="106" to="0" fill="freeze" fillOpacity="0" ref={animateRef} />
</circle>
</svg>
<svg>标签定义了一个宽高为37单位的svg区域,其中视图区域viewBox从(0,0)开始,宽高37个单位。 但在实际效果中会发现viewBox被svg viewport截掉,因此需要设置一个缩放属性,即preserveAspectRatio
preserveAspectRatio="xMinYMin meet"
preserveAspectRatio属性的值由两个值组成,“xMidYMid” 和 “meet”。 第1个值表示,viewBox如何与SVG viewport对齐;第2个值表示,如何维持高宽比。
第一个值的组合方式:x(Min/Mid/Max)Y(Min/Mid/Max) Min表示靠近原点(0,0)对齐,Mid表示svg viewport中心对齐,Max则是远离原点的对齐。前面的x/Y表示x/Y轴的对齐方式。 xMinYMin即:viewport和viewBox左边对齐,viewport和viewBox上边缘对齐。
第二值的选择有:meet/slice/none meet:保持纵横比缩放viewBox适应viewport slice: 保持纵横比同时比例小的方向放大填满viewport(通常会被截) none: 扭曲纵横比以充分适应viewport
分享一位大佬的文章,充分解释了preserveAspectRatio属性值以及示例: 理解SVG viewport,viewBox,preserveAspectRatio缩放
第一个<circle>元素实现了半透明部分的圆环,样式半径为17,粗细为3单位,透明度为0.2。 第二个<circle>元素实现了白色部分的圆环,样式半径为17,粗细为3单位,透明度为1。
因为圆环的动画起点是从0度逆时针开始,想要调整角度/顺逆时针动画可以通过transform属性或者css scale进行翻转。
因此路径动画的实现主要是通过包裹一个<animate />元素,在<animate />元素中定义动画的变化
路径动画
一个元素(物体)沿着特定的路径(轨迹)进行运动。 使用<animationMotion>元素,定义一个path, 就可以使一个元素沿着路径运动。
import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
const point = [
[178, 565],
[281, 530],
[386, 511],
[526, 551],
[600, 506],
[605, 376],
[632, 280],
[591, 220],
[576, 160],
[476, 118],
[371, 171],
[256, 163],
[126, 168],
[124, 218],
[103, 288],
[71, 402],
[105, 488]
];
export function SvgAnimation() {
const line = point.reduce((pre, cur,i) => {
return i === 0 ? `M ${cur[0]} ${cur[1]}` : `${pre} L ${cur[0]} ${cur[1]}`;
}, '');
const [boatPath, setBoatPath] = useState(`${line} Z`);
return (
<svg width="100%" height="100%" viewBox="0 0 720 600" preserveAspectRatio="xMinYMin meet">
<rect x="0" y="0" width="40" height="40" fill="rgba(0, 255, 255, 0.5)">
<animateMotion
dur="10s"
rotate="auto" // 在路径移动的同时,是否旋转元素
fill="freeze"
restart="whenNotActive" // 动画执行完成了才能再重新开始动画
>
<mpath xlinkHref="#boat-path"/> // 复用路径
</animateMotion>
</rect>
<path id="boat-path" d={boatPath} fill="none" stroke="green" />
</svg>
);
}
代码里react元素就会沿着path定义的路径移动
其中的rotate属性,用于定义元素相对于移动路线的方向
- auto:元素会根据路径的方向自动旋转
- auto-reverse: 元素会根据路径的方向加上180度的角度自动旋转
- 某个数字: 元素以这个数字为旋转角度沿着路径移动
restart属性:值有always/whenNotActive/never
- always: 可以中断动画重新开始
- whenNotActive: 动画结束才能重新开始
- never: 动画都不能重新开始
控制移动的路径动画
上一个路径动画的例子是自执行的动画,如果想要通过js控制svg元素沿着路径1/n步移动,可以参考下面的代码:
import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
export const point = [
[178, 565],
[281, 530],
[386, 511],
[526, 551],
[600, 506],
[605, 376],
[632, 280],
[591, 220],
[576, 160],
[476, 118],
[371, 171],
[256, 163],
[126, 168],
[124, 218],
[103, 288],
[71, 402],
[105, 488]
];
export function SvgDemo(props) {
const { defaultPosIdx = 0, onBoatMoveFinish} = props;
const line = point.reduce((pre, cur,i) => {
return i === 0 ? `M ${cur[0]} ${cur[1]}` : `${pre} L ${cur[0]} ${cur[1]}`;
}, '');
const _boatPoints = point.map(item => { // 将点对为正下方
return [item[0] - 50, item[1] - 80];
});
const boatline = point.reduce((pre, cur,i) => {
return i === 0 ? `M ${cur[0]} ${cur[1]}` : `${pre} L ${cur[0]} ${cur[1]}`;
}, '');
const realPath = `${boatline} Z`;
const boatPath = `${line} Z`;
const [posIdx, setPosIdx] = useState(defaultPosIdx);
const [boatPosition, setBoatPosition] = useState({ x: _boatPoints?.[defaultPosIdx]?.[0], y: _boatPoints?.[defaultPosIdx]?.[1] });
// const [idx, setIdx] = useState(0);
const originPosition = useRef(defaultPosIdx);
const destination = useRef(defaultPosIdx);
const times = useRef(0);
const isInit = useRef(true);
const animateXRef = useRef(null);
const animateYRef = useRef(null);
const prevIdx = useMemo(() => {
if (isInit.current) {
isInit.current = false;
return defaultPosIdx;
}
return posIdx === 0 ? _boatPoints.length - 1 : posIdx - 1;
}, [posIdx, defaultPosIdx]);
const runBoatAnimation = useCallback(() => {
if (!times.current) {
onBoatMoveFinish?.(posIdx);
return;
}
times.current -= 1;
setBoatPosition(() => {
const nextIdx = (posIdx + 1) % _boatPoints.length;
const nextPos = _boatPoints?.[nextIdx];
const newPosition = { x: nextPos[0], y: nextPos[1] };
animateYRef?.current?.beginElement();
animateXRef?.current?.beginElement();
return newPosition;
});
setPosIdx((pre) => {
return (pre + 1) % _boatPoints.length;
});
}, [posIdx, _boatPoints, onBoatMoveFinish]);
const moveBoat = (step) => {
originPosition.current = posIdx;
times.current = step;
const des = (originPosition.current + times.current) % _boatPoints.length;
destination.current = des;
runBoatAnimation();
};
useEffect(() => {
animateXRef?.current?.addEventListener('endEvent', runBoatAnimation);
return () => {
animateXRef?.current?.removeEventListener('endEvent', runBoatAnimation);
};
}, [runBoatAnimation]);
return (
<div className="list-container">
<button onClick={() => {moveBoat(6)}}>next</button>
<svg width="100%" height="100%" viewBox="0 0 720 600" preserveAspectRatio="xMinYMin meet">
<image
x={boatPosition.x}
y={boatPosition.y}
width="100"
height="100"
fill="rgba(0, 255, 255, 0.5)"
xlinkHref="https://picnew13.photophoto.cn/20181228/shiliangshouhuikatonglunchuan-31308405_1.jpg"
>
<animate
ref={animateXRef}
attributeName="x"
from={_boatPoints[prevIdx]?.[0]}
to={_boatPoints[posIdx]?.[0]}
fill="freeze"
repeatCount="1"
dur="3s" // 这里可以修改动画持续时间
/>
<animate
ref={animateYRef}
attributeName="y"
from={_boatPoints[prevIdx]?.[1]}
to={_boatPoints[posIdx]?.[1]}
fill="freeze"
dur="3s" // 这里可以修改动画持续时间
/>
</image>
<path d={boatPath} fill="none" stroke="green"/>
<path d={realPath} fill="none" stroke="none"/>
</svg>
</div>
)
}