2021年,是时候用mask-image来展示你炫酷的渐变图标了

3,634 阅读3分钟

what is mask-image

mask-image CSS属性用于设置元素上遮罩层的图像。

MDN是这样来表述mask-image的。关于它的兼容性,你可以在这里看到Can i use mask-image

img_can_use

  • 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,你可以制造出各种意想不到的效果: img_gradient

下面基于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,对于其他类型的图标并不适用,不过也足以应对大部分场景了。

最后,如果觉得文章对你有用,点赞,评论,让更多的人看到,谢谢。

参考文献