vue2 水印效果
效果图展示
Watermark
参数 |
说明 |
类型 |
默认值 |
width |
水印的宽度,content 的默认值为自身的宽度 |
number |
120 |
height |
水印的高度,content 的默认值为自身的高度 |
number |
64 |
rotate |
水印绘制时,旋转的角度,单位 ° |
number |
-22 |
zIndex |
追加的水印元素的 z-index |
number |
9 |
image |
图片源,建议导出 2 倍或 3 倍图,优先级高 |
string |
- |
content |
水印文字内容 |
string | string[] |
- |
font |
文字样式 |
Font |
Font |
gap |
水印之间的间距 |
[number, number] |
[100, 100] |
clockwise |
顺时针旋转 |
boolean |
true |
opacity |
水印的透明度 0~1 |
number |
1 |
offset |
水印距离容器左上角的偏移量,默认为 gap/2 |
[number, number] |
[gap[0]/2, gap[1]/2] |
Font
参数 |
说明 |
类型 |
默认值 |
color |
字体颜色 |
string |
rgba(0,0,0,.15) |
fontSize |
字体大小 |
number |
16 |
fontWeight |
字体粗细 |
normal | light | weight | number |
normal |
fontFamily |
字体类型 |
string |
sans-serif |
fontStyle |
字体样式 |
none | normal | italic | oblique |
normal |
使用Watermark 组件
<!--
* Copyright ©
* #
* @author: zw
* @date: 2023-05-11
-->
<template>
<el-row v-parent-height>
<el-col :span="8" class="bg-blue-50 h-80%">
<el-card class="h-full" :body-style="{ margin: 0, padding: 0, height: '100%' }" v-style="{ marginTop: '80px' }">
<el-form :model="form" ref="queryForm" :size="layoutSize" :inline="false" label-width="90px">
<br />
<el-col :span="22">
<template v-for="(item, index) in form.content">
<el-form-item :label="index <= 0 ? '水印内容' : '水印内容 ' + (index + 1)">
<div class="flex items-center">
<el-input class="w-full" v-model="form.content[index]" placeholder="请输入文字信息" />
<el-button v-if="index === 0" class="ml-10" type="text" :size="layoutSize" icon="el-icon-circle-plus-outline" @click="form.content.splice(index + 1, 0, '')" />
<el-button v-if="index > 0" class="ml-10" v-style="{ color: 'red' }" type="text" :size="layoutSize" icon="el-icon-error" @click="form.content.splice(index, 1)" />
</div>
</el-form-item>
</template>
</el-col>
<el-col :span="11">
<el-form-item label="水平间距">
<el-slider v-model="form.gap[0]" :max="180" />
</el-form-item>
</el-col>
<el-col :span="11">
<el-form-item label="垂直间距">
<el-slider v-model="form.gap[1]" :max="180" />
</el-form-item>
</el-col>
<el-col :span="11">
<el-form-item label="水平偏移">
<el-slider v-model="form.offset[0]" :max="180" />
</el-form-item>
</el-col>
<el-col :span="11">
<el-form-item label="垂直偏移">
<el-slider v-model="form.offset[1]" :max="180" />
</el-form-item>
</el-col>
<el-col :span="11">
<el-form-item label="宽度">
<el-slider v-model="form.width" :max="180" />
</el-form-item>
</el-col>
<el-col :span="11">
<el-form-item label="高度">
<el-slider v-model="form.height" :max="180" />
</el-form-item>
</el-col>
<el-col :span="11">
<el-form-item label="顺时针">
<el-switch v-model="form.clockwise" :active-value="true" :inactive-value="false" />
</el-form-item>
</el-col>
<el-col :span="11">
<el-form-item label="旋转角度">
<el-slider v-model="form.rotate" :max="90" />
</el-form-item>
</el-col>
<el-col :span="11">
<el-form-item label="水印层级">
<el-slider v-model="form.zIndex" />
</el-form-item>
</el-col>
<el-col :span="11">
<el-form-item label="透明度">
<el-slider v-model="form.opacity" :format-tooltip="(val) => val / 100" />
</el-form-item>
</el-col>
<el-col :span="22">
<el-form-item label="上传图片">
<ImageUpload v-model="upload" @input="inputChange" :limit="1" />
</el-form-item>
</el-col>
<el-col>
<el-form-item>
<el-button :size="layoutSize" type="info" @click="reset">重置</el-button>
</el-form-item>
</el-col>
</el-form>
</el-card>
</el-col>
<el-col :span="15" class="bg-blue-50 h-full">
<Watermark v-parent-height v-bind="{ ...form, opacity: form.opacity / 100 }">
<el-col class="mt-20 bg-yellow-50 text-gray-600" :span="13" :push="3"> Lorem, ipsum dolor sit amet consectetur adipisicing elit. Laudantium aut animi rerum veritatis? Nulla quas libero, rerum deserunt doloremque aspernatur, animi necessitatibus sunt a dicta nobis repellat odit, saepe maiores. </el-col>
<el-col class="mt-20 bg-yellow-50 text-gray-600" :span="13" :push="3"> Lorem, ipsum dolor sit amet consectetur adipisicing elit. Laudantium aut animi rerum veritatis? Nulla quas libero, rerum deserunt doloremque aspernatur, animi necessitatibus sunt a dicta nobis repellat odit, saepe maiores. </el-col>
<el-col class="mt-20 bg-yellow-50 text-gray-600" :span="13" :push="3"> Lorem, ipsum dolor sit amet consectetur adipisicing elit. Laudantium aut animi rerum veritatis? Nulla quas libero, rerum deserunt doloremque aspernatur, animi necessitatibus sunt a dicta nobis repellat odit, saepe maiores. </el-col>
<el-col class="mt-20 bg-yellow-50 text-gray-600" :span="13" :push="3"> Lorem, ipsum dolor sit amet consectetur adipisicing elit. Laudantium aut animi rerum veritatis? Nulla quas libero, rerum deserunt doloremque aspernatur, animi necessitatibus sunt a dicta nobis repellat odit, saepe maiores. </el-col>
<el-col class="mt-20 bg-yellow-50 text-gray-600" :span="13" :push="3"> Lorem, ipsum dolor sit amet consectetur adipisicing elit. Laudantium aut animi rerum veritatis? Nulla quas libero, rerum deserunt doloremque aspernatur, animi necessitatibus sunt a dicta nobis repellat odit, saepe maiores. </el-col>
</Watermark>
</el-col>
</el-row>
</template>
<script>
import Watermark from '@/components/Watermark'
import ImageUpload from '@/components/ImageUpload'
export default {
name: 'Watermark-template',
components: { Watermark, ImageUpload },
data() {
return {
form: {
content: ['水印内容', '水印内容2', '水印内容3'],
gap: [60, 60],
offset: [100, 100],
width: 120,
height: 64,
rotate: 45,
zIndex: 9,
image: '',
clockwise: false,
opacity: 100,
},
upload: [],
}
},
mounted() {},
methods: {
reset() {
Object.assign(this.$data.form, this.$options.data().form)
},
inputChange(_, list) {
const image = list[0]?.base64
this.form.image = image
},
},
}
</script>
<style lang="css" scoped></style>
Watermark 组件
<!--
* Copyright ©
* #
* @author: zw
* @date: 2023-05-09
-->
<template>
<div class="watermark-container">
<div class="watermark-content">
<slot></slot>
</div>
</div>
</template>
<script>
let rate = 350
let lastClick = Date.now() - rate
const BaseSize = 2
const FontGap = 3
const getPixelRatio = () => window.devicePixelRatio || 1
const toLowercaseSeparator = (key) => key.replace(/([A-Z])/g, '-$1').toLowerCase()
const getStyleStr = (style) =>
Object.keys(style)
.map((key) => `${toLowercaseSeparator(key)}: ${style[key]};`)
.join(' ')
function reRendering(mutation, watermarkElement) {
let flag = false
if (mutation.removedNodes.length) {
flag = Array.from(mutation.removedNodes).some((node) => node === watermarkElement)
}
if (mutation.type === 'attributes' && mutation.target === watermarkElement) {
flag = true
}
return flag
}
export default {
name: 'Watermark',
data() {
return {
watermarkRef: null,
stopObservation: false,
observe: null,
}
},
props: {
zIndex: { type: Number, default: 9 },
rotate: { type: Number, default: -22 },
width: { type: [String, Number], default: 120 },
height: { type: [String, Number], default: 64 },
image: { type: String, default: '' },
content: { type: [String, Array], default: '' },
font: {
type: Object,
default: () => ({
fontSize: 16,
fontFamily: 'sans-serif',
fontStyle: 'normal',
fontWeight: 'normal',
color: 'rgba(0, 0, 0, 0.15)',
}),
},
clockwise: { type: Boolean, default: true },
opacity: { type: Number, default: 1 },
rootClassName: '',
gap: { type: Array, default: () => [20, 20] },
offset: { type: Array, default: () => [100, 100] },
},
mounted() {
this.renderWatermark()
this.$nextTick(() => {
this.observe = this.useMutationObserver(this.$el, this.onMutate, { attributes: true, childList: true, subtree: true })
})
},
methods: {
onMutate(records) {
if (this.stopObservation) return
records.forEach((mutation) => {
if (!reRendering(mutation, this.watermarkRef)) return
this.destroyWatermark()
this.renderWatermark()
})
},
useMutationObserver(target, callback, options) {
const isSupported = typeof MutationObserver !== 'undefined'
if (!isSupported) return false
const observe = new MutationObserver(callback)
observe.observe(target, options)
return observe
},
getMarkSize(ctx) {
const props = this.$props
const { fontSize, fontFamily } = props.font
let defaultWidth
let defaultHeight
const content = props.content
const image = props.image
const width = props.width
const height = props.height
if (!image && ctx.measureText) {
ctx.font = `${Number(fontSize)}px ${fontFamily}`
const contents = Array.isArray(content) ? content : [content]
const widths = contents.map((item) => ctx.measureText(item).width)
defaultWidth = Math.ceil(Math.max(...widths))
defaultHeight = Number(fontSize.value) * contents.length + (contents.length - 1) * FontGap
}
return [width ?? defaultWidth, height ?? defaultHeight]
},
rotateWatermark(ctx, rotateX, rotateY, rotate) {
const direction = this.$props.clockwise ? 1 : -1
ctx.translate(rotateX, rotateY)
ctx.rotate((Math.PI / 180) * Number(rotate) * direction)
ctx.translate(-rotateX, -rotateY)
},
fillTexts(ctx, drawX, drawY, drawWidth, drawHeight) {
const props = this.$props
const { fontSize, fontFamily, fontStyle, fontWeight, color } = props.font
const ratio = getPixelRatio()
const content = props.content
const mergedFontSize = Number(fontSize) * ratio
ctx.font = `${fontStyle} normal ${fontWeight} ${mergedFontSize}px/${drawHeight}px ${fontFamily}`
ctx.fillStyle = color
ctx.textAlign = 'center'
ctx.textBaseline = 'top'
ctx.translate(drawWidth / 2, 0)
const contents = Array.isArray(content) ? content : [content]
contents?.forEach((item, index) => {
ctx.fillText(item ?? '', drawX, drawY + index * (mergedFontSize + FontGap * ratio))
})
},
appendWatermark(base64Url, markWidth) {
if (!this.watermarkRef) return
const props = this.$props
const [gapX, gapY] = props.gap
this.stopObservation = true
const attrs = getStyleStr({ ...this.markStyle, backgroundImage: `url('${base64Url}')`, backgroundSize: `${(gapX + markWidth) * BaseSize}px` })
this.watermarkRef.setAttribute('style', attrs)
this.$el.append(this.watermarkRef)
setTimeout(() => {
this.stopObservation = false
})
},
renderWatermark() {
const props = this.$props
const [gapX, gapY] = props.gap
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
const image = props.image
const rotate = props.rotate
if (!ctx) return false
if (!this.watermarkRef) {
this.watermarkRef = document.createElement('div')
}
const ratio = getPixelRatio()
const [markWidth, markHeight] = this.getMarkSize(ctx)
const canvasWidth = (gapX + markWidth) * ratio
const canvasHeight = (gapY + markHeight) * ratio
canvas.setAttribute('width', `${canvasWidth * BaseSize}px`)
canvas.setAttribute('height', `${canvasHeight * BaseSize}px`)
const drawX = (gapX * ratio) / 2
const drawY = (gapY * ratio) / 2
const drawWidth = markWidth * ratio
const drawHeight = markHeight * ratio
const rotateX = (drawWidth + gapX * ratio) / 2
const rotateY = (drawHeight + gapY * ratio) / 2
const alternateDrawX = drawX + canvasWidth
const alternateDrawY = drawY + canvasHeight
const alternateRotateX = rotateX + canvasWidth
const alternateRotateY = rotateY + canvasHeight
ctx.save()
this.rotateWatermark(ctx, rotateX, rotateY, rotate)
if (image) {
const img = new Image()
img.onload = () => {
ctx.drawImage(img, drawX, drawY, drawWidth, drawHeight)
ctx.restore()
this.rotateWatermark(ctx, alternateRotateX, alternateRotateY, rotate)
ctx.drawImage(img, alternateDrawX, alternateDrawY, drawWidth, drawHeight)
this.appendWatermark(canvas.toDataURL(), markWidth)
}
img.crossOrigin = 'anonymous'
img.referrerPolicy = 'no-referrer'
img.src = image
} else {
this.fillTexts(ctx, drawX, drawY, drawWidth, drawHeight)
ctx.restore()
this.rotateWatermark(ctx, alternateRotateX, alternateRotateY, rotate)
this.fillTexts(ctx, alternateDrawX, alternateDrawY, drawWidth, drawHeight)
this.appendWatermark(canvas.toDataURL(), markWidth)
}
},
destroyWatermark() {
if (!this.watermarkRef) return
this.watermarkRef.remove()
this.watermarkRef = undefined
},
},
computed: {
markStyle() {
const props = this.$props
const [gapX, gapY] = props.gap
const [offsetX, offsetY] = props.offset
const gapXCenter = gapX / 2
const gapYCenter = gapY / 2
const offsetTop = offsetY || gapYCenter
const offsetLeft = offsetX || gapXCenter
const markStyle = {
zIndex: this.zIndex,
opacity: this.opacity,
position: 'absolute',
left: 0,
top: 0,
width: '100%',
height: '100%',
pointerEvents: 'none',
backgroundRepeat: 'repeat',
}
let positionLeft = offsetLeft - gapXCenter
let positionTop = offsetTop - gapYCenter
if (positionLeft > 0) {
markStyle.left = `${positionLeft}px`
markStyle.width = `calc(100% - ${positionLeft}px)`
positionLeft = 0
}
if (positionTop > 0) {
markStyle.top = `${positionTop}px`
markStyle.height = `calc(100% - ${positionTop}px)`
positionTop = 0
}
markStyle.backgroundPosition = `${positionLeft}px ${positionTop}px`
return markStyle
},
},
watch: {
$props: {
handler() {
if (Date.now() - lastClick >= rate) {
this.stopObservation = true
this.renderWatermark()
setTimeout(() => {
this.stopObservation = false
lastClick = Date.now()
})
}
},
deep: true,
},
},
beforeDestroy() {
this.destroyWatermark()
this.observe.disconnect()
this.observe = null
},
}
</script>
<style lang="scss" scoped>
.watermark-container {
position: relative;
.watermark-content {
position: relative;
z-index: 1;
}
}
</style>