Files
element-plus/packages/components/watermark/src/useClips.ts
SuQingyao 5b1bde20fe feat(components): [watermark] add fontGap prop (#22342)
* feat(components): [watermark] add fontGap property

* docs(components): [watermark] add fontGap option

* docs(components): [watermark] add version annotation to fontGap property
2025-09-29 14:25:07 +08:00

165 lines
4.5 KiB
TypeScript

import { isArray } from '@element-plus/utils'
import type { WatermarkProps } from './watermark'
// [alignRatio, spaceRatio]
const TEXT_ALIGN_RATIO_MAP = {
left: [0, 0.5],
start: [0, 0.5],
center: [0.5, 0],
right: [1, -0.5],
end: [1, -0.5],
} as const
function prepareCanvas(
width: number,
height: number,
ratio = 1
): [
ctx: CanvasRenderingContext2D,
canvas: HTMLCanvasElement,
realWidth: number,
realHeight: number,
] {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')!
const realWidth = width * ratio
const realHeight = height * ratio
canvas.setAttribute('width', `${realWidth}px`)
canvas.setAttribute('height', `${realHeight}px`)
ctx.save()
return [ctx, canvas, realWidth, realHeight]
}
/**
* Get the clips of text content.
* This is a lazy hook function since SSR no need this
*/
export default function useClips() {
// Get single clips
function getClips(
content: NonNullable<WatermarkProps['content']> | HTMLImageElement,
rotate: number,
ratio: number,
width: number,
height: number,
font: Required<NonNullable<WatermarkProps['font']>>,
gapX: number,
gapY: number,
space: number
): [dataURL: string, finalWidth: number, finalHeight: number] {
// ================= Text / Image =================
const [ctx, canvas, contentWidth, contentHeight] = prepareCanvas(
width,
height,
ratio
)
if (content instanceof HTMLImageElement) {
// Image
ctx.drawImage(content, 0, 0, contentWidth, contentHeight)
} else {
// Text
const {
color,
fontSize,
fontStyle,
fontWeight,
fontFamily,
textAlign,
textBaseline,
} = font
const mergedFontSize = Number(fontSize) * ratio
ctx.font = `${fontStyle} normal ${fontWeight} ${mergedFontSize}px/${height}px ${fontFamily}`
ctx.fillStyle = color
ctx.textAlign = textAlign
ctx.textBaseline = textBaseline
const contents = isArray(content) ? content : [content]
contents?.forEach((item, index) => {
const [alignRatio, spaceRatio] = TEXT_ALIGN_RATIO_MAP[textAlign]
ctx.fillText(
item ?? '',
contentWidth * alignRatio + space * spaceRatio,
index * (mergedFontSize + font.fontGap * ratio)
)
})
}
// ==================== Rotate ====================
const angle = (Math.PI / 180) * Number(rotate)
const maxSize = Math.max(width, height)
const [rCtx, rCanvas, realMaxSize] = prepareCanvas(maxSize, maxSize, ratio)
// Copy from `ctx` and rotate
rCtx.translate(realMaxSize / 2, realMaxSize / 2)
rCtx.rotate(angle)
if (contentWidth > 0 && contentHeight > 0) {
rCtx.drawImage(canvas, -contentWidth / 2, -contentHeight / 2)
}
// Get boundary of rotated text
function getRotatePos(x: number, y: number) {
const targetX = x * Math.cos(angle) - y * Math.sin(angle)
const targetY = x * Math.sin(angle) + y * Math.cos(angle)
return [targetX, targetY]
}
let left = 0
let right = 0
let top = 0
let bottom = 0
const halfWidth = contentWidth / 2
const halfHeight = contentHeight / 2
const points = [
[0 - halfWidth, 0 - halfHeight],
[0 + halfWidth, 0 - halfHeight],
[0 + halfWidth, 0 + halfHeight],
[0 - halfWidth, 0 + halfHeight],
]
points.forEach(([x, y]) => {
const [targetX, targetY] = getRotatePos(x, y)
left = Math.min(left, targetX)
right = Math.max(right, targetX)
top = Math.min(top, targetY)
bottom = Math.max(bottom, targetY)
})
const cutLeft = left + realMaxSize / 2
const cutTop = top + realMaxSize / 2
const cutWidth = right - left
const cutHeight = bottom - top
// ================ Fill Alternate ================
const realGapX = gapX * ratio
const realGapY = gapY * ratio
const filledWidth = (cutWidth + realGapX) * 2
const filledHeight = cutHeight + realGapY
const [fCtx, fCanvas] = prepareCanvas(filledWidth, filledHeight)
function drawImg(targetX = 0, targetY = 0) {
fCtx.drawImage(
rCanvas,
cutLeft,
cutTop,
cutWidth,
cutHeight,
targetX,
targetY,
cutWidth,
cutHeight
)
}
drawImg()
drawImg(cutWidth + realGapX, -cutHeight / 2 - realGapY / 2)
drawImg(cutWidth + realGapX, +cutHeight / 2 + realGapY / 2)
return [fCanvas.toDataURL(), filledWidth / ratio, filledHeight / ratio]
}
return getClips
}