vuecssuiant-designantdreactantantd-vueenterprisefrontendui-designvue-antdvue-antd-uivue3vuecomponent
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.
259 lines
9.2 KiB
259 lines
9.2 KiB
import type { ExtractPropTypes, CSSProperties } from 'vue'; |
|
import { computed, defineComponent, onBeforeUnmount, onMounted, shallowRef, watch } from 'vue'; |
|
import { getStyleStr, getPixelRatio, rotateWatermark, reRendering } from './utils'; |
|
import { arrayType, objectType, someType, withInstall } from '../_util/type'; |
|
import { useMutationObserver } from '../_util/hooks/_vueuse/useMutationObserver'; |
|
import { initDefaultProps } from '../_util/props-util'; |
|
import { useToken } from '../theme/internal'; |
|
|
|
/** |
|
* Base size of the canvas, 1 for parallel layout and 2 for alternate layout |
|
* Only alternate layout is currently supported |
|
*/ |
|
const BaseSize = 2; |
|
const FontGap = 3; |
|
|
|
export interface WatermarkFontType { |
|
color?: string; |
|
fontSize?: number | string; |
|
fontWeight?: 'normal' | 'light' | 'weight' | number; |
|
fontStyle?: 'none' | 'normal' | 'italic' | 'oblique'; |
|
fontFamily?: string; |
|
} |
|
export const watermarkProps = () => ({ |
|
zIndex: Number, |
|
rotate: Number, |
|
width: Number, |
|
height: Number, |
|
image: String, |
|
content: someType<string | string[]>([String, Array]), |
|
font: objectType<WatermarkFontType>(), |
|
rootClassName: String, |
|
gap: arrayType<[number, number]>(), |
|
offset: arrayType<[number, number]>(), |
|
}); |
|
export type WatermarkProps = Partial<ExtractPropTypes<ReturnType<typeof watermarkProps>>>; |
|
const Watermark = defineComponent({ |
|
name: 'AWatermark', |
|
inheritAttrs: false, |
|
props: initDefaultProps(watermarkProps(), { |
|
zIndex: 9, |
|
rotate: -22, |
|
font: {}, |
|
gap: [100, 100], |
|
}), |
|
setup(props, { slots, attrs }) { |
|
const [, token] = useToken(); |
|
const containerRef = shallowRef<HTMLDivElement>(); |
|
const watermarkRef = shallowRef<HTMLDivElement>(); |
|
const stopObservation = shallowRef(false); |
|
const gapX = computed(() => props.gap?.[0] ?? 100); |
|
const gapY = computed(() => props.gap?.[1] ?? 100); |
|
const gapXCenter = computed(() => gapX.value / 2); |
|
const gapYCenter = computed(() => gapY.value / 2); |
|
const offsetLeft = computed(() => props.offset?.[0] ?? gapXCenter.value); |
|
const offsetTop = computed(() => props.offset?.[1] ?? gapYCenter.value); |
|
const fontSize = computed(() => props.font?.fontSize ?? token.value.fontSizeLG); |
|
const fontWeight = computed(() => props.font?.fontWeight ?? 'normal'); |
|
const fontStyle = computed(() => props.font?.fontStyle ?? 'normal'); |
|
const fontFamily = computed(() => props.font?.fontFamily ?? 'sans-serif'); |
|
const color = computed(() => props.font?.color ?? token.value.colorFill); |
|
const markStyle = computed(() => { |
|
const markStyle: CSSProperties = { |
|
zIndex: props.zIndex ?? 9, |
|
position: 'absolute', |
|
left: 0, |
|
top: 0, |
|
width: '100%', |
|
height: '100%', |
|
pointerEvents: 'none', |
|
backgroundRepeat: 'repeat', |
|
}; |
|
|
|
/** Calculate the style of the offset */ |
|
let positionLeft = offsetLeft.value - gapXCenter.value; |
|
let positionTop = offsetTop.value - gapYCenter.value; |
|
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; |
|
}); |
|
const destroyWatermark = () => { |
|
if (watermarkRef.value) { |
|
watermarkRef.value.remove(); |
|
watermarkRef.value = undefined; |
|
} |
|
}; |
|
|
|
const appendWatermark = (base64Url: string, markWidth: number) => { |
|
if (containerRef.value && watermarkRef.value) { |
|
stopObservation.value = true; |
|
watermarkRef.value.setAttribute( |
|
'style', |
|
getStyleStr({ |
|
...markStyle.value, |
|
backgroundImage: `url('${base64Url}')`, |
|
backgroundSize: `${(gapX.value + markWidth) * BaseSize}px`, |
|
}), |
|
); |
|
containerRef.value?.append(watermarkRef.value); |
|
// Delayed execution |
|
setTimeout(() => { |
|
stopObservation.value = false; |
|
}); |
|
} |
|
}; |
|
/** |
|
* Get the width and height of the watermark. The default values are as follows |
|
* Image: [120, 64]; Content: It's calculated by content; |
|
*/ |
|
const getMarkSize = (ctx: CanvasRenderingContext2D) => { |
|
let defaultWidth = 120; |
|
let defaultHeight = 64; |
|
const content = props.content; |
|
const image = props.image; |
|
const width = props.width; |
|
const height = props.height; |
|
if (!image && ctx.measureText) { |
|
ctx.font = `${Number(fontSize.value)}px ${fontFamily.value}`; |
|
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] as const; |
|
}; |
|
const fillTexts = ( |
|
ctx: CanvasRenderingContext2D, |
|
drawX: number, |
|
drawY: number, |
|
drawWidth: number, |
|
drawHeight: number, |
|
) => { |
|
const ratio = getPixelRatio(); |
|
const content = props.content; |
|
const mergedFontSize = Number(fontSize.value) * ratio; |
|
ctx.font = `${fontStyle.value} normal ${fontWeight.value} ${mergedFontSize}px/${drawHeight}px ${fontFamily.value}`; |
|
ctx.fillStyle = color.value; |
|
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)); |
|
}); |
|
}; |
|
const renderWatermark = () => { |
|
const canvas = document.createElement('canvas'); |
|
const ctx = canvas.getContext('2d'); |
|
const image = props.image; |
|
const rotate = props.rotate ?? -22; |
|
|
|
if (ctx) { |
|
if (!watermarkRef.value) { |
|
watermarkRef.value = document.createElement('div'); |
|
} |
|
|
|
const ratio = getPixelRatio(); |
|
const [markWidth, markHeight] = getMarkSize(ctx); |
|
const canvasWidth = (gapX.value + markWidth) * ratio; |
|
const canvasHeight = (gapY.value + markHeight) * ratio; |
|
canvas.setAttribute('width', `${canvasWidth * BaseSize}px`); |
|
canvas.setAttribute('height', `${canvasHeight * BaseSize}px`); |
|
|
|
const drawX = (gapX.value * ratio) / 2; |
|
const drawY = (gapY.value * ratio) / 2; |
|
const drawWidth = markWidth * ratio; |
|
const drawHeight = markHeight * ratio; |
|
const rotateX = (drawWidth + gapX.value * ratio) / 2; |
|
const rotateY = (drawHeight + gapY.value * ratio) / 2; |
|
/** Alternate drawing parameters */ |
|
const alternateDrawX = drawX + canvasWidth; |
|
const alternateDrawY = drawY + canvasHeight; |
|
const alternateRotateX = rotateX + canvasWidth; |
|
const alternateRotateY = rotateY + canvasHeight; |
|
|
|
ctx.save(); |
|
rotateWatermark(ctx, rotateX, rotateY, rotate); |
|
|
|
if (image) { |
|
const img = new Image(); |
|
img.onload = () => { |
|
ctx.drawImage(img, drawX, drawY, drawWidth, drawHeight); |
|
/** Draw interleaved pictures after rotation */ |
|
ctx.restore(); |
|
rotateWatermark(ctx, alternateRotateX, alternateRotateY, rotate); |
|
ctx.drawImage(img, alternateDrawX, alternateDrawY, drawWidth, drawHeight); |
|
appendWatermark(canvas.toDataURL(), markWidth); |
|
}; |
|
img.crossOrigin = 'anonymous'; |
|
img.referrerPolicy = 'no-referrer'; |
|
img.src = image; |
|
} else { |
|
fillTexts(ctx, drawX, drawY, drawWidth, drawHeight); |
|
/** Fill the interleaved text after rotation */ |
|
ctx.restore(); |
|
rotateWatermark(ctx, alternateRotateX, alternateRotateY, rotate); |
|
fillTexts(ctx, alternateDrawX, alternateDrawY, drawWidth, drawHeight); |
|
appendWatermark(canvas.toDataURL(), markWidth); |
|
} |
|
} |
|
}; |
|
onMounted(() => { |
|
renderWatermark(); |
|
}); |
|
watch( |
|
() => [props, token.value.colorFill, token.value.fontSizeLG], |
|
() => { |
|
renderWatermark(); |
|
}, |
|
{ |
|
deep: true, |
|
flush: 'post', |
|
}, |
|
); |
|
onBeforeUnmount(() => { |
|
destroyWatermark(); |
|
}); |
|
const onMutate = (mutations: MutationRecord[]) => { |
|
if (stopObservation.value) { |
|
return; |
|
} |
|
mutations.forEach(mutation => { |
|
if (reRendering(mutation, watermarkRef.value)) { |
|
destroyWatermark(); |
|
renderWatermark(); |
|
} |
|
}); |
|
}; |
|
useMutationObserver(containerRef, onMutate, { |
|
attributes: true, |
|
subtree: true, |
|
childList: true, |
|
attributeFilter: ['style', 'class'], |
|
}); |
|
return () => { |
|
return ( |
|
<div |
|
{...attrs} |
|
ref={containerRef} |
|
class={[attrs.class, props.rootClassName]} |
|
style={[{ position: 'relative' }, attrs.style as CSSProperties]} |
|
> |
|
{slots.default?.()} |
|
</div> |
|
); |
|
}; |
|
}, |
|
}); |
|
|
|
export default withInstall(Watermark);
|
|
|