You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
577 lines
15 KiB
577 lines
15 KiB
2 years ago
|
<!--
|
||
|
* @Author: weisheng
|
||
|
* @Date: 2023-04-05 21:32:56
|
||
|
* @LastEditTime: 2023-05-15 11:29:31
|
||
|
* @LastEditors: weisheng
|
||
|
* @Description: 水印组件
|
||
|
* @FilePath: \fant-mini-plus\src\uni_modules\fant-mini-plus\components\hd-water-mark\hd-water-mark.vue
|
||
|
* 记得注释
|
||
|
-->
|
||
|
<template>
|
||
|
<view :class="rootClass" :style="rootStyle">
|
||
|
<canvas
|
||
|
v-if="!canvasOffScreenable && showCanvas"
|
||
|
type="2d"
|
||
|
:style="{ height: canvasHeight + 'px', width: canvasWidth + 'px', visibility: 'hidden' }"
|
||
|
:canvas-id="canvasId"
|
||
|
:id="canvasId"
|
||
|
/>
|
||
|
</view>
|
||
|
</template>
|
||
|
|
||
|
<script lang="ts">
|
||
|
export default {
|
||
|
// 将自定义节点设置成虚拟的,更加接近Vue组件的表现,可以去掉微信小程序自定义组件多出的最外层标签
|
||
|
options: {
|
||
|
virtualHost: true
|
||
|
}
|
||
|
}
|
||
|
</script>
|
||
|
|
||
|
<script lang="ts" setup>
|
||
|
import { computed, onMounted, ref, watch, nextTick } from 'vue'
|
||
|
import { CommonUtil } from '../..'
|
||
|
|
||
|
/**
|
||
|
* WaterMark 水印
|
||
|
*/
|
||
|
interface Props {
|
||
|
// 显示内容
|
||
|
content?: string
|
||
|
// 显示图片的地址
|
||
|
image?: string
|
||
|
// 图片高度
|
||
|
imageHeight?: number
|
||
|
// 图片高度
|
||
|
imageWidth?: number
|
||
|
// X轴间距,单位px
|
||
|
gutterX?: number
|
||
|
// Y轴间距,单位px
|
||
|
gutterY?: number
|
||
|
// canvas画布宽度,单位px
|
||
|
width?: number
|
||
|
// canvas画布高度,单位px
|
||
|
height?: number
|
||
|
// 是否为全屏水印
|
||
|
fullScreen?: boolean
|
||
|
// 水印字体颜色
|
||
|
color?: string
|
||
|
// 水印字体大小,单位px
|
||
|
size?: number
|
||
|
// 水印字体样式(仅微信和h5支持),可能的值:normal、italic、oblique
|
||
|
fontStyle?: string
|
||
|
// 水印字体的粗细(仅微信和h5支持)
|
||
|
fontWeight?: number | string
|
||
|
// 水印字体系列(仅微信和h5支持)
|
||
|
fontFamily?: string
|
||
|
// 水印旋转角度
|
||
|
rotate?: number
|
||
|
// 自定义层级
|
||
|
zIndex?: number
|
||
|
// 自定义透明度,取值 0~1
|
||
|
opacity?: number
|
||
|
}
|
||
|
|
||
|
const props = withDefaults(defineProps<Props>(), {
|
||
|
// 显示内容
|
||
|
content: '',
|
||
|
// 显示图片
|
||
|
image: '',
|
||
|
// 图片高度
|
||
|
imageHeight: 64,
|
||
|
// 图片高度
|
||
|
imageWidth: 120,
|
||
|
// X轴间距
|
||
|
gutterX: 24,
|
||
|
// Y轴间距
|
||
|
gutterY: 48,
|
||
|
// canvas画布宽度,单位px
|
||
|
width: 120,
|
||
|
// canvas画布高度,单位px
|
||
|
height: 64,
|
||
|
// 是否为全屏水印
|
||
|
fullScreen: true,
|
||
|
// 水印字体颜色
|
||
|
color: '#1a1a1a',
|
||
|
// 水印字体大小
|
||
|
size: 14,
|
||
|
// 水印字体样式,可能的值:normal、italic、oblique
|
||
|
fontStyle: 'normal',
|
||
|
// 水印字体的粗细
|
||
|
fontWeight: 'normal',
|
||
|
// 水印字体系列
|
||
|
fontFamily: 'PingFang SC',
|
||
|
// 水印旋转角度
|
||
|
rotate: -25,
|
||
|
// 自定义层级
|
||
|
zIndex: 1100,
|
||
|
// 自定义透明度,取值 0~1
|
||
|
opacity: 0.5
|
||
|
})
|
||
|
|
||
|
watch(
|
||
|
() => props,
|
||
|
() => {
|
||
|
doReset()
|
||
|
},
|
||
|
{ deep: true }
|
||
|
)
|
||
|
|
||
|
const canvasId = ref<string>(`water${CommonUtil.s4()}`) // canvas 组件的唯一标识符
|
||
|
const waterMarkUrl = ref<string>('') // canvas生成base64水印
|
||
|
const canvasOffScreenable = ref<boolean>(uni.canIUse('createOffscreenCanvas') && Boolean(uni.createOffscreenCanvas)) // 是否可以使用离屏canvas
|
||
|
const pixelRatio = ref<number>(uni.getSystemInfoSync().pixelRatio) // 像素比
|
||
|
const canvasHeight = ref<number>((props.height + props.gutterY) * pixelRatio.value) // canvas画布高度
|
||
|
const canvasWidth = ref<number>((props.width + props.gutterX) * pixelRatio.value) // canvas画布宽度
|
||
|
const showCanvas = ref<boolean>(true) // 是否展示canvas
|
||
|
|
||
|
/**
|
||
|
* 水印css类
|
||
|
*/
|
||
|
const rootClass = computed(() => {
|
||
|
let classess: string = 'hd-water-mark'
|
||
|
if (props.fullScreen) {
|
||
|
classess = `${classess} hd-water-mark-full-screen`
|
||
|
}
|
||
|
return classess
|
||
|
})
|
||
|
|
||
|
/**
|
||
|
* 水印样式
|
||
|
*/
|
||
|
const rootStyle = computed(() => {
|
||
|
const style: Record<string, string | number> = {
|
||
|
opacity: props.opacity,
|
||
|
backgroundSize: CommonUtil.addUnit(props.width + props.gutterX, 'px')
|
||
|
}
|
||
|
if (waterMarkUrl.value) {
|
||
|
style['backgroundImage'] = `url('${waterMarkUrl.value}')`
|
||
|
}
|
||
|
return CommonUtil.style(style)
|
||
|
})
|
||
|
|
||
|
onMounted(() => {
|
||
|
doInit()
|
||
|
})
|
||
|
|
||
|
function doReset() {
|
||
|
showCanvas.value = true
|
||
|
canvasHeight.value = (props.height + props.gutterY) * pixelRatio.value
|
||
|
canvasWidth.value = (props.width + props.gutterX) * pixelRatio.value
|
||
|
nextTick(() => {
|
||
|
doInit()
|
||
|
})
|
||
|
}
|
||
|
|
||
|
function doInit() {
|
||
|
// #ifdef H5
|
||
|
// h5使用document.createElement创建canvas,不用展示canvas标签
|
||
|
showCanvas.value = false
|
||
|
// #endif
|
||
|
const { width, height, color, size, fontStyle, fontWeight, fontFamily, content, rotate, gutterX, gutterY, image, imageHeight, imageWidth } = props
|
||
|
|
||
|
// 创建水印
|
||
|
createWaterMark(width, height, color, size, fontStyle, fontWeight, fontFamily, content, rotate, gutterX, gutterY, image, imageHeight, imageWidth)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 创建水印图片
|
||
|
* @param width canvas宽度
|
||
|
* @param height canvas高度
|
||
|
* @param color canvas字体颜色
|
||
|
* @param size canvas字体大小
|
||
|
* @param fontStyle canvas字体样式
|
||
|
* @param fontWeight canvas字体字重
|
||
|
* @param fontFamily canvas字体系列
|
||
|
* @param content canvas内容
|
||
|
* @param rotate 倾斜角度
|
||
|
* @param gutterX X轴间距
|
||
|
* @param gutterY Y轴间距
|
||
|
* @param image canvas图片
|
||
|
* @param imageHeight canvas图片高度
|
||
|
* @param imageWidth canvas图片宽度
|
||
|
*/
|
||
|
function createWaterMark(
|
||
|
width: number,
|
||
|
height: number,
|
||
|
color: string,
|
||
|
size: number,
|
||
|
fontStyle: string,
|
||
|
fontWeight: number | string,
|
||
|
fontFamily: string,
|
||
|
content: string,
|
||
|
rotate: number,
|
||
|
gutterX: number,
|
||
|
gutterY: number,
|
||
|
image: string,
|
||
|
imageHeight: number,
|
||
|
imageWidth: number
|
||
|
) {
|
||
|
const canvasHeight = (height + gutterY) * pixelRatio.value
|
||
|
const canvasWidth = (width + gutterX) * pixelRatio.value
|
||
|
const contentWidth = width * pixelRatio.value
|
||
|
const contentHeight = height * pixelRatio.value
|
||
|
const fontSize = size * pixelRatio.value
|
||
|
// #ifndef H5
|
||
|
if (canvasOffScreenable.value) {
|
||
|
createOffscreenCanvas(
|
||
|
canvasHeight,
|
||
|
canvasWidth,
|
||
|
contentWidth,
|
||
|
contentHeight,
|
||
|
rotate,
|
||
|
fontSize,
|
||
|
fontFamily,
|
||
|
fontStyle,
|
||
|
fontWeight,
|
||
|
color,
|
||
|
content,
|
||
|
image,
|
||
|
imageHeight,
|
||
|
imageWidth
|
||
|
)
|
||
|
} else {
|
||
|
createCanvas(canvasHeight, contentWidth, rotate, fontSize, color, content, image, imageHeight, imageWidth)
|
||
|
}
|
||
|
// #endif
|
||
|
// #ifdef H5
|
||
|
createH5Canvas(
|
||
|
canvasHeight,
|
||
|
canvasWidth,
|
||
|
contentWidth,
|
||
|
contentHeight,
|
||
|
rotate,
|
||
|
fontSize,
|
||
|
fontFamily,
|
||
|
fontStyle,
|
||
|
fontWeight,
|
||
|
color,
|
||
|
content,
|
||
|
image,
|
||
|
imageHeight,
|
||
|
imageWidth
|
||
|
)
|
||
|
// #endif
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 创建离屏canvas
|
||
|
* @param canvasHeight canvas高度
|
||
|
* @param canvasWidth canvas宽度
|
||
|
* @param contentWidth 内容宽度
|
||
|
* @param contentHeight 内容高度
|
||
|
* @param rotate 内容倾斜角度
|
||
|
* @param fontSize 字体大小
|
||
|
* @param fontFamily 字体系列
|
||
|
* @param fontStyle 字体样式
|
||
|
* @param fontWeight 字体字重
|
||
|
* @param color 字体颜色
|
||
|
* @param content 内容
|
||
|
* @param image canvas图片
|
||
|
* @param imageHeight canvas图片高度
|
||
|
* @param imageWidth canvas图片宽度
|
||
|
*/
|
||
|
function createOffscreenCanvas(
|
||
|
canvasHeight: number,
|
||
|
canvasWidth: number,
|
||
|
contentWidth: number,
|
||
|
contentHeight: number,
|
||
|
rotate: number,
|
||
|
fontSize: number,
|
||
|
fontFamily: string,
|
||
|
fontStyle: string,
|
||
|
fontWeight: string | number,
|
||
|
color: string,
|
||
|
content: string,
|
||
|
image: string,
|
||
|
imageHeight: number,
|
||
|
imageWidth: number
|
||
|
) {
|
||
|
// 创建离屏canvas
|
||
|
const canvas: any = uni.createOffscreenCanvas({ height: canvasHeight, width: canvasWidth, type: '2d' })
|
||
|
const ctx: any = canvas.getContext('2d')
|
||
|
if (ctx) {
|
||
|
if (image) {
|
||
|
const img = canvas.createImage() as HTMLImageElement
|
||
|
drawImageOffScreen(ctx, img, image, imageHeight, imageWidth, rotate, contentWidth, contentHeight, canvas)
|
||
|
} else {
|
||
|
drawTextOffScreen(ctx, content, contentWidth, contentHeight, rotate, fontSize, fontFamily, fontStyle, fontWeight, color, canvas)
|
||
|
}
|
||
|
} else {
|
||
|
console.error('无法获取canvas上下文,请确认当前环境是否支持canvas')
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 非H5创建canvas
|
||
|
* 不支持创建离屏canvas时调用
|
||
|
* @param contentHeight 内容高度
|
||
|
* @param contentWidth 内容宽度
|
||
|
* @param rotate 内容倾斜角度
|
||
|
* @param fontSize 字体大小
|
||
|
* @param color 字体颜色
|
||
|
* @param content 内容
|
||
|
* @param image canvas图片
|
||
|
* @param imageHeight canvas图片高度
|
||
|
* @param imageWidth canvas图片宽度
|
||
|
*/
|
||
|
function createCanvas(
|
||
|
contentHeight: number,
|
||
|
contentWidth: number,
|
||
|
rotate: number,
|
||
|
fontSize: number,
|
||
|
color: string,
|
||
|
content: string,
|
||
|
image: string,
|
||
|
imageHeight: number,
|
||
|
imageWidth: number
|
||
|
) {
|
||
|
const ctx = uni.createCanvasContext(canvasId.value)
|
||
|
if (ctx) {
|
||
|
if (image) {
|
||
|
drawImageOnScreen(ctx, image, imageHeight, imageWidth, rotate, contentWidth, contentHeight)
|
||
|
} else {
|
||
|
drawTextOnScreen(ctx, content, contentWidth, rotate, fontSize, color)
|
||
|
}
|
||
|
} else {
|
||
|
console.error('无法获取canvas上下文,请确认当前环境是否支持canvas')
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* h5创建canvas
|
||
|
* @param canvasHeight canvas高度
|
||
|
* @param canvasWidth canvas宽度
|
||
|
* @param contentWidth 水印内容宽度
|
||
|
* @param contentHeight 水印内容高度
|
||
|
* @param rotate 水印内容倾斜角度
|
||
|
* @param fontSize 水印字体大小
|
||
|
* @param fontFamily 水印字体系列
|
||
|
* @param fontStyle 水印字体样式
|
||
|
* @param fontWeight 水印字体字重
|
||
|
* @param color 水印字体颜色
|
||
|
* @param content 水印内容
|
||
|
*/
|
||
|
function createH5Canvas(
|
||
|
canvasHeight: number,
|
||
|
canvasWidth: number,
|
||
|
contentWidth: number,
|
||
|
contentHeight: number,
|
||
|
rotate: number,
|
||
|
fontSize: number,
|
||
|
fontFamily: string,
|
||
|
fontStyle: string,
|
||
|
fontWeight: string | number,
|
||
|
color: string,
|
||
|
content: string,
|
||
|
image: string,
|
||
|
imageHeight: number,
|
||
|
imageWidth: number
|
||
|
) {
|
||
|
const canvas = document.createElement('canvas')
|
||
|
const ctx = canvas.getContext('2d')
|
||
|
canvas.setAttribute('width', `${canvasWidth}px`)
|
||
|
canvas.setAttribute('height', `${canvasHeight}px`)
|
||
|
if (ctx) {
|
||
|
if (image) {
|
||
|
const img = new Image()
|
||
|
drawImageOffScreen(ctx, img, image, imageHeight, imageWidth, rotate, contentWidth, contentHeight, canvas)
|
||
|
} else {
|
||
|
drawTextOffScreen(ctx, content, contentWidth, contentHeight, rotate, fontSize, fontFamily, fontStyle, fontWeight, color, canvas)
|
||
|
}
|
||
|
} else {
|
||
|
console.error('无法获取canvas上下文,请确认当前环境是否支持canvas')
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 绘制离屏文字canvas
|
||
|
* @param ctx canvas上下文
|
||
|
* @param content 水印内容
|
||
|
* @param contentWidth 水印宽度
|
||
|
* @param contentHeight 水印高度
|
||
|
* @param rotate 水印内容倾斜角度
|
||
|
* @param fontSize 水印字体大小
|
||
|
* @param fontFamily 水印字体系列
|
||
|
* @param fontStyle 水印字体样式
|
||
|
* @param fontWeight 水印字体字重
|
||
|
* @param color 水印字体颜色
|
||
|
* @param canvas canvas实例
|
||
|
*/
|
||
|
function drawTextOffScreen(
|
||
|
ctx: CanvasRenderingContext2D,
|
||
|
content: string,
|
||
|
contentWidth: number,
|
||
|
contentHeight: number,
|
||
|
rotate: number,
|
||
|
fontSize: number,
|
||
|
fontFamily: string,
|
||
|
fontStyle: string,
|
||
|
fontWeight: string | number,
|
||
|
color: string,
|
||
|
canvas: HTMLCanvasElement
|
||
|
) {
|
||
|
ctx.textBaseline = 'middle'
|
||
|
ctx.textAlign = 'center'
|
||
|
ctx.translate(contentWidth / 2, contentWidth / 2)
|
||
|
ctx.rotate((Math.PI / 180) * rotate)
|
||
|
ctx.font = `${fontStyle} normal ${fontWeight} ${fontSize}px/${contentHeight}px ${fontFamily}`
|
||
|
ctx.fillStyle = color
|
||
|
ctx.fillText(content, 0, 0)
|
||
|
ctx.restore()
|
||
|
waterMarkUrl.value = canvas.toDataURL()
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 绘制在屏文字canvas
|
||
|
* @param ctx canvas上下文
|
||
|
* @param content 水印内容
|
||
|
* @param contentWidth 水印宽度
|
||
|
* @param rotate 水印内容倾斜角度
|
||
|
* @param fontSize 水印字体大小
|
||
|
* @param color 水印字体颜色
|
||
|
*/
|
||
|
function drawTextOnScreen(ctx: UniApp.CanvasContext, content: string, contentWidth: number, rotate: number, fontSize: number, color: string) {
|
||
|
ctx.setTextBaseline('middle')
|
||
|
ctx.setTextAlign('center')
|
||
|
ctx.translate(contentWidth / 2, contentWidth / 2)
|
||
|
ctx.rotate((Math.PI / 180) * rotate)
|
||
|
ctx.setFillStyle(color)
|
||
|
ctx.setFontSize(fontSize)
|
||
|
ctx.fillText(content, 0, 0)
|
||
|
ctx.restore()
|
||
|
ctx.draw()
|
||
|
// #ifdef MP-DINGTALK
|
||
|
// 钉钉小程序的canvasToTempFilePath接口与其他平台不一样
|
||
|
;(ctx as any).toTempFilePath({
|
||
|
success(res) {
|
||
|
showCanvas.value = false
|
||
|
waterMarkUrl.value = res.filePath
|
||
|
}
|
||
|
})
|
||
|
// #endif
|
||
|
// #ifndef MP-DINGTALK
|
||
|
uni.canvasToTempFilePath({
|
||
|
canvasId: canvasId.value,
|
||
|
success: (res) => {
|
||
|
showCanvas.value = false
|
||
|
waterMarkUrl.value = res.tempFilePath
|
||
|
}
|
||
|
})
|
||
|
// #endif
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 绘制离屏图片canvas
|
||
|
* @param ctx canvas上下文
|
||
|
* @param img 水印图片对象
|
||
|
* @param image 水印图片地址
|
||
|
* @param imageHeight 水印图片高度
|
||
|
* @param imageWidth 水印图片宽度
|
||
|
* @param rotate 水印内容倾斜角度
|
||
|
* @param contentWidth 水印宽度
|
||
|
* @param contentHeight 水印高度
|
||
|
* @param canvas canvas实例
|
||
|
*/
|
||
|
async function drawImageOffScreen(
|
||
|
ctx: CanvasRenderingContext2D,
|
||
|
img: HTMLImageElement,
|
||
|
image: string,
|
||
|
imageHeight: number,
|
||
|
imageWidth: number,
|
||
|
rotate: number,
|
||
|
contentWidth: number,
|
||
|
contentHeight: number,
|
||
|
canvas: HTMLCanvasElement
|
||
|
) {
|
||
|
ctx.translate(contentWidth / 2, contentHeight / 2)
|
||
|
ctx.rotate((Math.PI / 180) * Number(rotate))
|
||
|
img.crossOrigin = 'anonymous'
|
||
|
img.referrerPolicy = 'no-referrer'
|
||
|
|
||
|
img.src = CommonUtil.setUrlParams(image, {
|
||
|
timestamp: `${new Date().getTime()}`
|
||
|
}) // 要加载的图片 url
|
||
|
img.onload = () => {
|
||
|
ctx.drawImage(
|
||
|
img,
|
||
|
(-imageWidth * pixelRatio.value) / 2,
|
||
|
(-imageHeight * pixelRatio.value) / 2,
|
||
|
imageWidth * pixelRatio.value,
|
||
|
imageHeight * pixelRatio.value
|
||
|
)
|
||
|
ctx.restore()
|
||
|
waterMarkUrl.value = canvas.toDataURL()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 绘制在屏图片canvas
|
||
|
* @param ctx canvas上下文
|
||
|
* @param image 水印图片地址
|
||
|
* @param imageHeight 水印图片高度
|
||
|
* @param imageWidth 水印图片宽度
|
||
|
* @param rotate 水印内容倾斜角度
|
||
|
* @param contentWidth 水印宽度
|
||
|
* @param contentHeight 水印高度
|
||
|
*/
|
||
|
function drawImageOnScreen(
|
||
|
ctx: UniApp.CanvasContext,
|
||
|
image: string,
|
||
|
imageHeight: number,
|
||
|
imageWidth: number,
|
||
|
rotate: number,
|
||
|
contentWidth: number,
|
||
|
contentHeight: number
|
||
|
) {
|
||
|
ctx.translate(contentWidth / 2, contentHeight / 2)
|
||
|
ctx.rotate((Math.PI / 180) * Number(rotate))
|
||
|
|
||
|
ctx.drawImage(
|
||
|
image,
|
||
|
(-imageWidth * pixelRatio.value) / 2,
|
||
|
(-imageHeight * pixelRatio.value) / 2,
|
||
|
imageWidth * pixelRatio.value,
|
||
|
imageHeight * pixelRatio.value
|
||
|
)
|
||
|
ctx.restore()
|
||
|
ctx.draw(false, () => {
|
||
|
// #ifdef MP-DINGTALK
|
||
|
// 钉钉小程序的canvasToTempFilePath接口与其他平台不一样
|
||
|
;(ctx as any).toTempFilePath({
|
||
|
success(res) {
|
||
|
showCanvas.value = false
|
||
|
waterMarkUrl.value = res.filePath
|
||
|
}
|
||
|
})
|
||
|
// #endif
|
||
|
// #ifndef MP-DINGTALK
|
||
|
uni.canvasToTempFilePath({
|
||
|
canvasId: canvasId.value,
|
||
|
success: (res) => {
|
||
|
showCanvas.value = false
|
||
|
waterMarkUrl.value = res.tempFilePath
|
||
|
}
|
||
|
})
|
||
|
// #endif
|
||
|
})
|
||
|
}
|
||
|
</script>
|
||
|
|
||
|
<style lang="scss" scoped>
|
||
|
.hd-water-mark {
|
||
|
position: absolute;
|
||
|
opacity: 0.1;
|
||
|
z-index: 1010;
|
||
|
left: 0;
|
||
|
right: 0;
|
||
|
top: 0;
|
||
|
bottom: 0;
|
||
|
pointer-events: none;
|
||
|
background-repeat: repeat;
|
||
|
&-full-screen {
|
||
|
position: fixed;
|
||
|
}
|
||
|
}
|
||
|
</style>
|