what is mask-image
mask-image CSS属性用于设置元素上遮罩层的图像。
MDN是这样来表述mask-image的。关于它的兼容性,你可以在这里看到Can i use mask-image。
- Chrome全系支持
- Firefox v53起
- Safari v4起
到目前为止,我们想要正常地使用该CSS属性,需要做一下兼容,用-webkit-mask-image:
.icon {
width: 24px;
height: 24px;
mask-repeat: no-repeat;
mask-size: cover;
background: black;
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='32' d='M368 368L144 144M368 144L144 368'/%3E%3C/svg%3E");
-webkit-mask-size: cover;
-webkit-mask-repeat: no-repeat;
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='32' d='M368 368L144 144M368 144L144 368'/%3E%3C/svg%3E");
}
why mask-image
看一下市面上的icon解决方案:
-
icon图片 使用img标签,或者作为背景图片
- 优点:直接且方便,不需要任何额外的支持,兼容性最好
- 缺点:多次http请求,且没有任何灵活性,无法满足多场景下的的需求
-
svg symbol 使用use标签,动态添加图标
- 优点:比较灵活,支持多色
- 缺点:需要把图标写入到模版,过于影响开发体验
-
iconfont 基于base64和雪碧图
- 优点:合并为一张图,仅一次http请求;兼容性好;相对灵活
- 缺点:编码为base64之后,图标磁盘占用变大了;base64相对于原始svg,显著影响页面渲染速度;
而使用了mask-image之后,上述问题都不复存在。mask-image十分灵活,它支持一个特别炫酷的特性,就是渐变色(本质是背景叠加),它的表现原理类似于PS中针对图层的“混合选项”=>“渐变叠加”。
我甚至以为,mask-image这个属性就是依据PS的“颜色叠加”、“渐变叠加”来实现的,因为二者的作用,十分相似。
有了mask-image,你可以制造出各种意想不到的效果:
下面基于mask-image分别封装一个小程序组件和React组件。
小程序组件
<!--index.wxml-->
<wxs
src="./index.wxs"
module="wxs"
></wxs>
<view
class="icon_wrap"
style="{{wxs.getWrapStyles({size,bordered,filled,round,borderColor,fillColor})}}"
wx:if="{{visibleWrap}}"
>
<view
class="icon"
style="{{wxs.getStyles({size,color,src})}}"
></view>
</view>
<view
class="icon"
style="{{wxs.getStyles({size,color,src})}}"
wx:else
></view>
// index.wxs
function getWrapStyles (props){
var styles = ''
if (props.bordered) styles += ';border:2rpx solid ' + props.borderColor
if (props.filled) styles += ';background:' + props.fillColor
if (props.round) styles += ';border-radius:50%'
styles += ';width:' + props.size * 1.5 + 'px'
styles += ';height:' + props.size * 1.5 + 'px'
return styles
}
function getStyles (props){
var styles = ''
styles += ';width:' + props.size + 'px'
styles += ';height:' + props.size + 'px'
styles += ';background:' + props.color
styles += ';mask-image:url("' + props.src + '")'
styles += ';-webkit-mask-image:url("' + props.src + '")'
return styles
}
module.exports = {
getWrapStyles: getWrapStyles,
getStyles: getStyles
}
// index.ts
Component({
options: {
//@ts-ignore
pureDataPattern: /^(type)$/
},
properties: {
icon: <{
type: ObjectConstructor
value: { outline: string; filled: string }
}>{
type: Object,
value: {}
},
type: <{
type: StringConstructor
value: 'outline' | 'filled'
}>{
type: String,
value: 'outline'
},
size: {
type: Number,
value: 20
},
color: {
type: String,
value: '#000000'
},
visibleWrap: {
type: Boolean,
value: false
},
bordered: {
type: Boolean,
value: false
},
filled: {
type: Boolean,
value: false
},
round: {
type: Boolean,
value: false
},
borderColor: {
type: String,
value: 'whitesmoke'
},
fillColor: {
type: String,
value: 'whitesmoke'
}
},
observers: {
type (v) {
this.getSrc(this.data.icon[v])
},
icon (v) {
if (!v) return
if (!this.data.type) return
this.getSrc(v[this.data.type])
}
},
data: {
src: '',
height: 20,
width: 20
},
methods: {
getSrc (svg) {
if (!svg) return
this.setData({
src: 'data:image/svg+xml,' + svg.replace(/</g, '%3C').replace(/>/g, '%3E')
})
}
}
})
// index.less
.icon_wrap {
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
}
.icon {
vertical-align: middle;
display: inline-block;
background: black;
mask-repeat: no-repeat;
mask-size: cover;
}
// icon.ts
export const cube = {
outline: `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'><path d='M448 341.37V170.61A32 32 0 00432.11 143l-152-88.46a47.94 47.94 0 00-48.24 0L79.89 143A32 32 0 0064 170.61v170.76A32 32 0 0079.89 369l152 88.46a48 48 0 0048.24 0l152-88.46A32 32 0 00448 341.37z' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='32'/><path fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='32' d='M69 153.99l187 110 187-110M256 463.99v-200'/></svg>`,
filled: `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'><path d='M440.9 136.3a4 4 0 000-6.91L288.16 40.65a64.14 64.14 0 00-64.33 0L71.12 129.39a4 4 0 000 6.91L254 243.88a4 4 0 004.06 0zM54 163.51a4 4 0 00-6 3.49v173.89a48 48 0 0023.84 41.39L234 479.51a4 4 0 006-3.46V274.3a4 4 0 00-2-3.46zM272 275v201a4 4 0 006 3.46l162.15-97.23A48 48 0 00464 340.89V167a4 4 0 00-6-3.45l-184 108a4 4 0 00-2 3.45z'/></svg>`
}
在应用中使用:
- 导入图标
import { cube } from 'icon'
Page({
data:{
icon:{
cube
}
}
})
- 使用组件
<Icon
icon="{{icon.cube}}"
color="black"
size="{{20}}"
type="filled"
></Icon>
通过直接传入svg使用,这样对图标进行按需加载,利于缩减包大小和后续扩展项目。上述这种方式有利有弊:牺牲了运行时内存,但换取的是包大小和渲染效率。
上面的小程序组件经过很少的改动(wxs => computed)就可以用作Vue组件。
React组件
// index.tsx
import { useState, useEffect } from 'react'
import { getWrapStyles, getStyles, getSrc } from './utils'
import styles from './index.less'
interface IProps {
icon: { outline: string; filled?: string }
type?: 'outline' | 'filled'
size?: number
color?: string
visibleWrap?: boolean
bordered?: boolean
filled?: boolean
round?: boolean
borderColor?: string
fillColor?: string
}
const Index = (props: IProps) => {
const {
icon,
type = 'outline',
size = 20,
color = 'black',
visibleWrap,
bordered,
filled,
round,
borderColor = 'whitesmoke',
fillColor = 'whitesmoke'
} = props
const [ state_src, setStateSrc ] = useState('')
useEffect(
() => {
if (!icon) return
if (!type) return
setStateSrc(getSrc(icon[type]))
},
[ icon, type ]
)
return (
<div className={styles._local}>
{visibleWrap ? (
<div
className='icon_wrap'
style={getWrapStyles({
size,
bordered,
filled,
round,
borderColor,
fillColor
})}
>
<div
className='icon'
style={getStyles({ size, color, src: state_src })}
/>
</div>
) : (
<div className='icon' style={getStyles({ size, color, src: state_src })} />
)}
</div>
)
}
export default Index
// index.less
._local {
display: flex;
:global {
.icon_wrap {
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
}
.icon {
vertical-align: middle;
display: inline-block;
background: black;
mask-repeat: no-repeat;
mask-size: cover;
}
}
}
// utils.ts
import { CSSProperties } from 'react'
export const getWrapStyles = ({
size,
bordered,
filled,
round,
borderColor,
fillColor
}): CSSProperties => {
const styles: CSSProperties = {}
if (bordered) styles.border = '2rpx solid ' + borderColor
if (filled) styles.background = fillColor
if (round) styles.borderRadius = '50%'
styles.width = size * 1.5 + 'px'
styles.height = size * 1.5 + 'px'
return styles
}
export const getStyles = ({ size, color, src }): CSSProperties => {
let styles: CSSProperties = {}
styles.width = size + 'px'
styles.height = size + 'px'
styles.background = color
styles.WebkitMaskImage = `url("${src}")`
return styles
}
export const getSrc = (svg: string) => {
if (!svg) return
return 'data:image/svg+xml,' + svg.replace(/</g, '%3C').replace(/>/g, '%3E')
}
注意,目前这种方案仅针对svg,对于其他类型的图标并不适用,不过也足以应对大部分场景了。
最后,如果觉得文章对你有用,点赞,评论,让更多的人看到,谢谢。