矢量图形
不同与传统的位图(PNG、JPG ...), 矢量图是一种面向对象的图像。矢量文件中的图形元素称为对象。每个对象都是一个自成一体的实体,它具有颜色、形状、轮廓、大小和屏幕位置等属性。
SVG
基本结构
下图是一个svg的基本结构
<svg width='100' heiight='100' xmlns='http://wwww.w3.org/2000/svg'>
<title>SVG</title>
<desc>Desc of SVG</desc>
<!-- ...draw sth !-->
</svg>
我们可以利用图形和属性来组合成一个矢量图
基本图形:<rect>、<circle>、<ellipse>、<line>、<polyline>、<polygon>
基本属性:fill、stroke、stroke-width、transform
具体可参考 MDN-SVG
接下来我们就可以随意组合图形了!
<svg width="500" height="500" xmlns="http://wwww.w3.org/2000/svg">
<circle
cx="250"
cy="80"
r="50"
style="stroke: black; fill: aquamarine"
/>
<rect
x="200"
y="130"
width="100"
height="100"
style="stroke: black; fill: blue"
/>
</svg>
分组
除了直接使用基础元素,还可以用<g>标签来定义自己的分组,比如我们想给上面的图形加个眼睛。
<svg width="500" height="500" xmlns="http://wwww.w3.org/2000/svg">
<circle
cx="250"
cy="80"
r="50"
style="stroke: black; fill: aquamarine"
/>
<rect
x="200"
y="130"
width="100"
height="100"
style="stroke: black; fill: blue"
/>
<g id="eye">
<circle cx="230" cy="70" r="5" style="stroke: black; fill: black" />
</g>
<use xlink:href="#eye" transform="translate(40 0)"></use>
</svg>
<g>定义了一个可复用的分组,用<use>可以深度克隆一份分组元素,并且可以重新定义元素样式
路径
<path>大致可以理解成通过描述一个点的运动路径,来绘制图像。首先需要了解以下命令
- M = moveto - 移动到新的位置
- L = lineto - 从当前坐标画一条直线到一个新坐标
- H = horizontal lineto - 画一条水平线到新坐标
- V = vertical lineto - 画一条垂直线到新坐标
- C = curveto - 贝塞尔曲线
- S = smooth curveto
- Q = quadratic Bézier curve
- T = smooth quadratic Bézier curveto
- A = elliptical Arc
- Z = closepath - 关闭当前路径
注:大写为绝对路径,小写为相对路径(相对于上一个M/m)
尝试给上面的小人加个鼻子
<svg width="500" height="500" xmlns="http://wwww.w3.org/2000/svg">
<circle
cx="250"
cy="80"
r="50"
style="stroke: black; fill: aquamarine"
/>
<rect
x="200"
y="130"
width="100"
height="100"
style="stroke: black; fill: blue"
/>
<g id="eye">
<circle cx="230" cy="70" r="5" style="stroke: black; fill: black" />
</g>
<use xlink:href="#eye" transform="translate(40 0)"></use>
<!-- 加个鼻子 !-->
<path style="fill: #000" d="M 250 85 l -5 10 l 10 0 Z" />
</svg>
来解读一下 M 250 85 l -5 10 l 10 0 Z
M 250 85:绝对路径,标识起点是绝对坐标(250,85)
l -5 10: 相对于上一个坐标的路径,即(245,95)
l 10 0: 相对于上一个坐标的路径,即(255,95)
效果:
defs
上面我们说到<g>和<use>搭配可以做到元素的复用,但在定义<g>时,就已经完成了一次绘制,如果我们想仅仅定义而不马上使用,可以使用<defs>
<svg width="500" height="500" xmlns="http://wwww.w3.org/2000/svg">
<circle
cx="250"
cy="80"
r="50"
style="stroke: black; fill: aquamarine"
/>
<rect
x="200"
y="130"
width="100"
height="100"
style="stroke: black; fill: blue"
/>
<!-- 定义模板 !-->
<defs>
<g id="eye">
<circle r="5" cx="5" cy="5" style="stroke: black; fill: black" />
</g>
</defs>
<!-- 使用模板模板 !-->
<use x="225" y="70" xlink:href="#eye"></use>
<use x="265" y="70" xlink:href="#eye"></use>
<!-- 加个鼻子 !-->
<path style="fill: #000" d="M 250 85 l -5 10 l 10 0 Z" />
</svg>
symbol
<symbol>有点像<g>+ <defs>的结合体
<svg width="500" height="500" xmlns="http://wwww.w3.org/2000/svg">
<circle
cx="250"
cy="80"
r="50"
style="stroke: black; fill: aquamarine"
/>
<rect
x="200"
y="130"
width="100"
height="100"
style="stroke: black; fill: blue"
/>
<!-- symbol 定义模板 !-->
<symbol id="eye">
<circle r="5" cx="5" cy="5" style="stroke: black; fill: black" />
</symbol>
<!-- 使用模板模板 !-->
<use x="225" y="70" xlink:href="#eye"></use>
<use x="265" y="70" xlink:href="#eye"></use>
<!-- 加个鼻子 !-->
<path style="fill: #000" d="M 250 85 l -5 10 l 10 0 Z" />
</svg>
viewBox
viewBox 简单来说,可以看作一个虚拟的画布
先看常规的svg
<svg style="width:200px; height:400px">
<circle cx="200" cy="200" r="200" fill="#fdd" stroke="none"></circle>
</svg>
这时cx、cy、r的单位都是绝对像素px,为了装下这个circle,svg的宽高必须不小于400px, 像上面宽度只有200px,就会出现显示不全的情况。这只是一个circle,当svg里写了一堆path,结果发现因为外部宽度不足而导致展示不全,这问题就麻烦了。
因此有了viewBox
<svg style="width:200px; height:400px" viewBox="0 0 400 400">
<circle cx="200" cy="200" r="200" fill="#fdd" stroke="none"></circle>
</svg>
viewBox有四个参数,一般只用到后两个,即虚拟画布的宽高
添加了viewBox后,cx、cy、r的单位就变成了相对viewBox的相对单位,只要保证不超过viewBox的宽高即可,而svg实际的宽高,我们可以自行在外部定义或给svg写内联style。
<div class="icon-class">
<svg viewBox="0 0 400 400">
<circle cx="200" cy="200" r="200" fill="#fdd" stroke="none"></circle>
</svg>
</div>
<style type="text/css">
.icon-class{
width: 1em;
height: 1em;
}
</style>
svg变色
color方案
- 修改svg的fill属性
fill: currentColor(currentColor会取当前color颜色) - 通过修改color属性即可
svg {
fill: currentColor;
color: red;
}
实现自定义svg组件
这一部分我们将参考antd-mobile的icon组件实现,来理解一下如何使用svg来封装icon
基础实现
首先来看看icon组件的核心代码,掐去不重要的部分
const Icon = (props) => {
useEffect(() => {
loadSprite(); // important
}, []);
const { type, className, size, ...restProps } = props;
const cls = classnames(
className,
'au-icon',
`au-icon-${type}`,
`au-icon-${size}`
);
return (
<svg className={cls} {...(restProps as any)}>
<use xlinkHref={`#${type}`} />
</svg>
);
};
最重要的部分应该就是loadSprite方法的执行了,继续看
loadSprite
// svg 列表
const icons: { [key: string]: string } = {
check:
'<svg viewBox="0 0 44 44"><path fill-rule="evenodd" d="M34.538 8L38 11.518 17.808 32 8 22.033l3.462-3.518 6.346 6.45z"/></svg>',
plus:
'<svg viewBox="0 0 30 30"><path d="M14 14H0v2h14v14h2V16h14v-2H16V0h-2v14z" fill-rule="evenodd"/></svg>',
minus:
'<svg viewBox="0 0 30 2"><path d="M0 0h30v2H0z" fill-rule="evenodd"/></svg>',
}
// 装载svg
const svgSprite = (contents: string) => `
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
id="__ANTD_MOBILE_SVG_SPRITE_NODE__"
style="display:none;overflow:hidden;width:0;height:0"
>
<defs>
${contents}
</defs>
</svg>
`;
// 渲染svg
const renderSvgSprite = () => {
const symbols = Object.keys(icons)
.map(iconName => {
const svgContent = icons[iconName].split('svg')[1];
return `<symbol id=${iconName}${svgContent}symbol>`;
})
.join('');
return svgSprite(symbols);
};
// 加载svg
const loadSprite = () => {
if (!document) {
return;
}
const existing = document.getElementById('__ANTD_MOBILE_SVG_SPRITE_NODE__');
const mountNode = document.body;
if (!existing) {
mountNode.insertAdjacentHTML('afterbegin', renderSvgSprite());
}
};
简单来说,loadSprite 就是定义好一堆<symbol>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
id="__ANTD_MOBILE_SVG_SPRITE_NODE__"
style="display:none;overflow:hidden;width:0;height:0"
>
<defs>
<symbol class="check" viewBox="0 0 1024 1024" id="icon名">
<path d="...."></path>
</symbol>
<symbol class="plus" viewBox="0 0 1024 1024" id="icon名">
<path d="...."></path>
</symbol>
<symbol class="minus" viewBox="0 0 1024 1024" id="icon名">
<path d="...."></path>
</symbol>
</defs>
</svg>
并插入到html中,外部使用<use>即可调用
这个Icon组件的缺点就很明显了,所有的icon都是手动写到代码中的,扩展性差,事实上,利用webpack我们可以很简单的引入自定义icon并使用。
svg-sprite-loader
svg-sprite-loader 的作用就是把所有svg塞到一个个symbol中
大致步骤:webpack批量读取svg => loader将svg文件塞到symbol中 => 组件引用
1.添加依赖
yarn add svg-sprite-loader
2.添加icons目录, 添加svg文件(icons/svg)和入口(icons/index)
入口inedx
// @ts-ignore
const req = require.context('./svg', true, /\.svg$/);
const requireAll = (requireContext) => requireContext.keys().map(requireContext);
requireAll(req);
这是为了读取所有svg文件到webpack中。
3.添加webpack配置,在config-overrides.js中
module.exports = override(
addWebpackModuleRule({
test: /\.svg$/,
include: [resolve('src/icons')], // svg文件目录
use: [
{
loader: 'svg-sprite-loader',
options: { symbolId: 'icon-[name]' }, // 引用的时候<use xlink:href="#icon-文件名"></use>
},
],
})
)
这时,loader已经把读取到的svg文件都塞到symbol中了,这时候已经可以这样引用了
<svg>
<use xlink:href="#icon-check"></use>
</svg>
但是每次都要写use,依然不够优雅,且拓展性不好,因此需要封装一个组件
4.封装组件
import React from 'react';
import PropTypes from 'prop-types';
import './svg-icon.less';
const SvgIcon = ({ iconClass, className, color }) => {
const styleExternalIcon = {
mask: `url(${iconClass}) no-repeat 50% 50%`,
WebkitMask: `url(${iconClass}) no-repeat 50% 50%`,
};
const isExternal = (path) => /^(https?:|mailto:|tel:)/.test(path);
const svgClass = className ? 'svg-icon ' + className : 'svg-icon';
const iconName = `#icon-${iconClass}`;
return (
<div>
{isExternal(iconClass) ? (
<div style={styleExternalIcon} className={`svg-external-icon ${svgClass}`} />
) : (
<svg color={color} className={svgClass} aria-hidden="true">
<use xlinkHref={iconName} />
</svg>
)}
</div>
);
};
SvgIcon.propTypes = {
iconClass: PropTypes.string.isRequired,
className: PropTypes.string,
};
export default SvgIcon;
然后我们就可以这样用了
<SvgIcon color="red" iconClass="check"></SvgIcon>
注:想要实现传递color变色需要删掉svg中的fill属性
用SVG玩一玩动画
一个圆形进度条
首先了解两个属性
stroke-dasharray
用于描述一段虚线的排列方式
当 stroke-dasharray 只取一个值时,标识每段虚线之间的间隔
上图就是长度为600的直线stroke-dashoffset分别取20,30,40,60时的表现
当然也可以取多个值
stroke-dasharray = '20, 10' :虚线长20, 间距10,=> 重复 虚线长20, 间距10
stroke-dasharray = '20, 10, 5' :虚线长20, 间距10 => 虚线长5, 间距20 => 虚线10, 间距5 => 循环
然后我们要记住一种特殊情况,stroke-dasharray等于线长的情况,即一段虚线就等于线长,这看起来就是一条实线(下面会用到)
<line
x1="0" y1="0" x2="50" y2="0"
stroke="#000"
stroke-width="5"
stroke-dasharray="50"
/>
stroke-dashoffset
即虚线开始的偏移量,不管往哪边便宜,由于stroke-dasharray是循环的,所以超过边界的部分都会循环到头/尾部
直线进度
我们将实现一个这样的效果
<style>
.progress {
animation: rotate 1500ms linear both;
}
@keyframes rotate {
from {
stroke-dashoffset: 150;
}
to {
stroke-dashoffset: 0;
}
}
</style>
<div style="width: 300px;">
<svg viewBox="0 0 150 150">
<line
class="progress"
x1="0"
y1="0"
x2="150"
y2="0"
stroke="red"
stroke-width="20"
stroke-dasharray="150"
stroke-dashoffset="150"
/>
</svg>
</div>
关键点:
1.stroke-dasharray="150" x2="150":上面已经说到,让虚线间隔和线长相等,看起来就像一个实线,但这个实线后面的150,是一段空白,因为间隔是150
2.stroke-dashoffset="150":让线段在开始时向左偏移150,这看起来线段是不见了,是因为看到的是后面的150空白间隔
3.通过动画让.stroke-dashoffset 从150回到0,线段就慢慢出现了
让进度条变成圆形
上面所说的技巧在<circle>中同样适用,stroke-dashoffset的初始值设为周长
<style>
.progress {
animation: rotate 1500ms linear both;
animation-delay: 2s;
}
@keyframes rotate {
from {
stroke-dashoffset: 471;
}
to {
stroke-dashoffset: 0;
}
}
</style>
<div style="width: 300px;">
<svg class="circle-step" viewBox="0 0 150 150">
<circle
class="progress"
r="70"
cy="75"
cx="75"
stroke-width="8"
stroke="#1593FF"
stroke-linejoin="round"
stroke-linecap="round"
fill="none"
transform = "rotate(-90,75,75)"
stroke-dashoffset="0px"
stroke-dasharray="471" // 471 = 75 * 2 * 3.14
/>
</svg>
</div>
transform = "rotate(-90,75,75)" :由于circle中stroke的起始点在3点钟方向,因此如果需要从12点开始,需要做一个90°的逆时针翻转, (75,75)是指定旋转中心坐标为圆心
效果:
把 keyframes 稍微改改也可以变成一个loading !
@keyframes rotate {
from {
stroke-dashoffset: 471;
}
to {
stroke-dashoffset: -471;
}
}
更简单的写法 有没有发现用css3写麻烦得很,用svg自带的animate可以更方便的完成
<div style="width: 300px;">
<svg class="circle-step" viewBox="0 0 150 150">
<circle
r="70"
cy="75"
cx="75"
stroke-width="8"
stroke="#EAEFF4"
stroke-linejoin="round"
stroke-linecap="round"
fill="none"
/>
<circle
class="progress"
r="70"
cy="75"
cx="75"
stroke-width="8"
stroke="#1593FF"
stroke-linejoin="round"
stroke-linecap="round"
fill="none"
transform="rotate(-90,75,75)"
stroke-dashoffset="471"
stroke-dasharray="471"
>
<animate
attributeName="stroke-dashoffset"
attributeType="XML"
from="471"
to="0"
begin="500ms"
dur="2000ms"
fill="freeze"
/>
</circle>
</svg>
</div>
animate在下面会详细介绍
做成组件
import React, { useRef, useImperativeHandle } from 'react';
export interface IProps {
process: number; // 进度 0 - 100
color: string; // 进度条颜色
duration: number; // 持续时间
delay: number; // 延迟开始时间
auto?: boolean; //是否自动执行
}
interface animateRefProps extends SVGAnimateElement {
beginElement: () => void;
}
export interface forwardRefProps {
excute: () => void;
}
/**
* React.forwardRef<useImperativeHandle Props, 接收的props>
*/
const CircleStep = React.forwardRef<forwardRefProps, IProps>((props, ref) => {
const {
process,
color = '#1593FF',
delay = 1500,
duration = 1000,
auto = true,
} = props;
// animate DOM
const animateRef = useRef<animateRefProps>();
/**
* 手动触发
*/
const excute = () => {
animateRef.current.beginElement();
};
/**
* 通过ref暴露
*/
useImperativeHandle(ref, () => ({
excute: excute,
}));
return (
<div className="cs-wrap">
<svg className="circle-step" viewBox="0 0 150 150">
<circle
r="70"
cy="75"
cx="75"
stroke="#EAEFF4"
style={{
strokeWidth: 8,
strokeLinejoin: 'round',
strokeLinecap: 'round',
}}
fill="none"
/>
<circle
className="progress"
r="70"
cy="75"
cx="75"
style={{
strokeWidth: 8,
strokeLinejoin: 'round',
strokeLinecap: 'round',
strokeDashoffset: 471,
strokeDasharray: 471,
}}
stroke={color}
fill="none"
transform="rotate(-90,75,75)"
>
<animate
ref={animateRef}
attributeName="stroke-dashoffset"
attributeType="XML"
from="471"
to={`${471 - (process / 100) * 471}`}
begin={auto ? `${delay}ms` : `indefinite`}
dur={`${duration}ms`}
fill="freeze"
/>
</circle>
</svg>
</div>
);
});
export default CircleStep;
该组件中,begin若设为indefinite, 该animate不会执行,通过animate dom的beginElement方法可触发执行,并且可以重复触发。
animate
举个简单的例子
<div style="width: 500px; height: 500px">
<svg viewBox="0 0 400 400">
<circle cx="200" cy="200" r="200" stroke="black" fill="none">
<animate attributeName="r" attributeType="XML" from="200" to="50" begin="0s" dur="3s" fill="freeze" />
</rect>
</svg>
</div>
上图展示了如何描述一个缩小的circle, 下面是常用的animate属性
attributeName
定义发生变化的元素属性名
attributeType
当attributeType="XML"时,attributeName被认为是XML的属性;当attributeType="CSS"时,attributeName被认为是css的属性;不指定attributeType时,默认为"auto",会先将attributeName作为css的属性,如果无效,再将attributeName作为XML的属性。
from & to & by
from和to分别定义发生变化的属性的初始值和终止值。from可缺省,表示初始值即为父元素相应的属性值。可用by替换to,表示变化偏移量。可以理解为to = from + by。
begin & dur & end
begin定义动画开始时间;dur定义动画所需时间;end定义动画终止时间。时间单位h:小时;min:分钟;s:秒;ms:毫秒。默认时间单位为s
fill
当fill="freeze"时,动画终止时,发生变化的元素属性值停留在动画终止时的状态;当fill="remove"时,动画终止时,发生变化的元素属性值回复到动画起始时的状态。fill属性默认值为remove。 允许在同一个元素内嵌入多个