mirror of
https://github.com/element-plus/element-plus.git
synced 2024-11-21 01:02:59 +08:00
feat(components): watermark component (#14236)
* feat(components): watermark component be able to set text,multi-text,image as watermark * docs(components): update image watermark example update the image for dark mode
This commit is contained in:
parent
0a46c24da9
commit
7916200ba4
@ -323,6 +323,10 @@
|
||||
{
|
||||
"link": "/divider",
|
||||
"text": "Divider"
|
||||
},
|
||||
{
|
||||
"link": "/watermark",
|
||||
"text": "Watermark"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
75
docs/en-US/component/watermark.md
Normal file
75
docs/en-US/component/watermark.md
Normal file
@ -0,0 +1,75 @@
|
||||
---
|
||||
title: Watermark
|
||||
lang: en-US
|
||||
---
|
||||
|
||||
# Watermark
|
||||
|
||||
Add specific text or patterns to the page.
|
||||
|
||||
## Basic usage
|
||||
|
||||
The most basic usage
|
||||
|
||||
:::demo
|
||||
|
||||
watermark/basic
|
||||
|
||||
:::
|
||||
|
||||
## Multi-line watermark
|
||||
|
||||
Use "content" to set an array of strings to specify multi-line text watermark content.
|
||||
|
||||
:::demo
|
||||
|
||||
watermark/multi-line
|
||||
|
||||
:::
|
||||
|
||||
## Image watermark
|
||||
|
||||
Specify the image address via 'image'. To ensure that the image is high definition and not stretched, set the width and height, and upload at least twice the width and height of the logo image address.
|
||||
|
||||
:::demo
|
||||
|
||||
watermark/image
|
||||
|
||||
:::
|
||||
|
||||
## Custom configuration
|
||||
|
||||
Preview the watermark effect by configuring custom parameters.
|
||||
|
||||
:::demo
|
||||
|
||||
watermark/custom
|
||||
|
||||
:::
|
||||
|
||||
## Watermark API
|
||||
|
||||
### Watermark Attributes
|
||||
|
||||
|
||||
| Name | Description | Type | Default |
|
||||
| ------- | ----------------------------------------------------------------------------------------------- | ------------------ | -------------------------- |
|
||||
| width | The width of the watermark, the default value of `content` is its own width | number | 120 |
|
||||
| height | The height of the watermark, the default value of `content` is its own height | number | 64 |
|
||||
| rotate | When the watermark is drawn, the rotation Angle, unit `°` | number | -22 |
|
||||
| zIndex | The z-index of the appended watermark element | number | 9 |
|
||||
| image | Image source, it is recommended to export 2x or 3x image, high priority | string | - |
|
||||
| content | Watermark text content | string \| string[] | - |
|
||||
| font | Text style | [Font](#font) | [Font](#font) |
|
||||
| gap | The spacing between watermarks | \[number, number\] | \[100, 100\] |
|
||||
| offset | The offset of the watermark from the upper left corner of the container. The default is `gap/2` | \[number, number\] | \[gap\[0\]/2, gap\[1\]/2\] |
|
||||
|
||||
### Font
|
||||
|
||||
| Name | Description | Type | Default |
|
||||
| ---------- | ----------- | ---------------------------------------------------- | --------------- |
|
||||
| color | font color | string | rgba(0,0,0,.15) |
|
||||
| fontSize | font size | number | 16 |
|
||||
| fontWeight | font weight | ^[enum]`'normal \| 'light' \| 'weight' \| number` | normal |
|
||||
| fontFamily | font family | string | sans-serif |
|
||||
| fontStyle | font style | ^[enum]`'none' \| 'normal' \| 'italic' \| 'oblique'` | normal |
|
5
docs/examples/watermark/basic.vue
Normal file
5
docs/examples/watermark/basic.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<el-watermark>
|
||||
<div style="height: 500px" />
|
||||
</el-watermark>
|
||||
</template>
|
103
docs/examples/watermark/custom.vue
Normal file
103
docs/examples/watermark/custom.vue
Normal file
@ -0,0 +1,103 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive } from 'vue'
|
||||
|
||||
const config = reactive({
|
||||
content: 'Element Plus',
|
||||
font: {
|
||||
fontSize: 16,
|
||||
color: 'rgba(0, 0, 0, 0.15)',
|
||||
},
|
||||
zIndex: -1,
|
||||
rotate: -22,
|
||||
gap: [100, 100] as [number, number],
|
||||
offset: [] as unknown as [number, number],
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="wrapper">
|
||||
<el-watermark
|
||||
class="watermark"
|
||||
:content="config.content"
|
||||
:font="config.font"
|
||||
:z-index="config.zIndex"
|
||||
:rotate="config.rotate"
|
||||
:gap="config.gap"
|
||||
:offset="config.offset"
|
||||
>
|
||||
<div class="demo">
|
||||
<h1>Element Plus</h1>
|
||||
<h2>a Vue 3 based component library for designers and developers</h2>
|
||||
<img src="/images/hamburger.png" alt="示例图片" />
|
||||
</div>
|
||||
</el-watermark>
|
||||
<el-form
|
||||
class="form"
|
||||
:model="config"
|
||||
label-position="top"
|
||||
label-width="50px"
|
||||
>
|
||||
<el-form-item label="Content">
|
||||
<el-input v-model="config.content" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Color">
|
||||
<el-color-picker v-model="config.font.color" show-alpha />
|
||||
</el-form-item>
|
||||
<el-form-item label="FontSize">
|
||||
<el-slider v-model="config.font.fontSize" />
|
||||
</el-form-item>
|
||||
<el-form-item label="zIndex">
|
||||
<el-slider v-model="config.zIndex" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Rotate">
|
||||
<el-slider v-model="config.rotate" :min="-180" :max="180" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Gap">
|
||||
<el-space>
|
||||
<el-input-number v-model="config.gap[0]" controls-position="right" />
|
||||
<el-input-number v-model="config.gap[1]" controls-position="right" />
|
||||
</el-space>
|
||||
</el-form-item>
|
||||
<el-form-item label="Offset">
|
||||
<el-space>
|
||||
<el-input-number
|
||||
v-model="config.offset[0]"
|
||||
placeholder="offsetLeft"
|
||||
controls-position="right"
|
||||
/>
|
||||
<el-input-number
|
||||
v-model="config.offset[1]"
|
||||
placeholder="offsetTop"
|
||||
controls-position="right"
|
||||
/>
|
||||
</el-space>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.wrapper {
|
||||
display: flex;
|
||||
}
|
||||
.watermark {
|
||||
display: flex;
|
||||
flex: auto;
|
||||
}
|
||||
.demo {
|
||||
flex: auto;
|
||||
}
|
||||
.form {
|
||||
width: 330px;
|
||||
margin-left: 20px;
|
||||
border-left: 1px solid #eee;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
img {
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
9
docs/examples/watermark/image.vue
Normal file
9
docs/examples/watermark/image.vue
Normal file
@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<el-watermark
|
||||
:width="130"
|
||||
:height="30"
|
||||
image="https://element-plus.org/images/element-plus-logo.svg"
|
||||
>
|
||||
<div style="height: 500px" />
|
||||
</el-watermark>
|
||||
</template>
|
5
docs/examples/watermark/multi-line.vue
Normal file
5
docs/examples/watermark/multi-line.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<el-watermark :content="['Element+', 'Element Plus']">
|
||||
<div style="height: 500px" />
|
||||
</el-watermark>
|
||||
</template>
|
@ -0,0 +1,3 @@
|
||||
// Vitest Snapshot v1
|
||||
|
||||
exports[`Watermark.vue > create 1`] = `"<div style=\\"position: relative;\\" class=\\"watermark\\">Rem is the best girl</div>"`;
|
22
packages/components/watermark/__tests__/watermark.test.tsx
Normal file
22
packages/components/watermark/__tests__/watermark.test.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import Watermark from '../src/watermark.vue'
|
||||
|
||||
const AXIOM = 'Rem is the best girl'
|
||||
|
||||
describe('Watermark.vue', () => {
|
||||
it('create', () => {
|
||||
const wrapper = mount(() => (
|
||||
<Watermark class="watermark">{AXIOM}</Watermark>
|
||||
))
|
||||
|
||||
expect(wrapper.classes()).toContain('watermark')
|
||||
expect(wrapper.html()).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('slots', () => {
|
||||
const wrapper = mount(() => <Watermark>{AXIOM}</Watermark>)
|
||||
|
||||
expect(wrapper.text()).toContain(AXIOM)
|
||||
})
|
||||
})
|
7
packages/components/watermark/index.ts
Normal file
7
packages/components/watermark/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { withInstall } from '@element-plus/utils'
|
||||
import Watermark from './src/watermark.vue'
|
||||
|
||||
export const ElWatermark = withInstall(Watermark)
|
||||
export default ElWatermark
|
||||
|
||||
export * from './src/watermark'
|
145
packages/components/watermark/src/useClips.ts
Normal file
145
packages/components/watermark/src/useClips.ts
Normal file
@ -0,0 +1,145 @@
|
||||
import type { WatermarkProps } from './watermark'
|
||||
|
||||
export const FontGap = 3
|
||||
|
||||
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
|
||||
): [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 } = font
|
||||
const mergedFontSize = Number(fontSize) * ratio
|
||||
|
||||
ctx.font = `${fontStyle} normal ${fontWeight} ${mergedFontSize}px/${height}px ${fontFamily}`
|
||||
ctx.fillStyle = color
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'top'
|
||||
const contents = Array.isArray(content) ? content : [content]
|
||||
contents?.forEach((item, index) => {
|
||||
ctx.fillText(
|
||||
item ?? '',
|
||||
contentWidth / 2,
|
||||
index * (mergedFontSize + 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
|
||||
}
|
37
packages/components/watermark/src/utils.ts
Normal file
37
packages/components/watermark/src/utils.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import type { CSSProperties } from 'vue'
|
||||
|
||||
/** converting camel-cased strings to be lowercase and link it with Separato */
|
||||
export function toLowercaseSeparator(key: string) {
|
||||
return key.replace(/([A-Z])/g, '-$1').toLowerCase()
|
||||
}
|
||||
|
||||
export function getStyleStr(style: CSSProperties): string {
|
||||
return Object.keys(style)
|
||||
.map(
|
||||
(key) =>
|
||||
`${toLowercaseSeparator(key)}: ${style[key as keyof CSSProperties]};`
|
||||
)
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
/** Returns the ratio of the device's physical pixel resolution to the css pixel resolution */
|
||||
export function getPixelRatio() {
|
||||
return window.devicePixelRatio || 1
|
||||
}
|
||||
|
||||
/** Whether to re-render the watermark */
|
||||
export const reRendering = (
|
||||
mutation: MutationRecord,
|
||||
watermarkElement?: HTMLElement
|
||||
) => {
|
||||
let flag = false
|
||||
// Whether to delete the watermark node
|
||||
if (mutation.removedNodes.length && watermarkElement) {
|
||||
flag = Array.from(mutation.removedNodes).includes(watermarkElement)
|
||||
}
|
||||
// Whether the watermark dom property value has been modified
|
||||
if (mutation.type === 'attributes' && mutation.target === watermarkElement) {
|
||||
flag = true
|
||||
}
|
||||
return flag
|
||||
}
|
70
packages/components/watermark/src/watermark.ts
Normal file
70
packages/components/watermark/src/watermark.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import { buildProps, definePropType } from '@element-plus/utils'
|
||||
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
import type Watermark from './watermark.vue'
|
||||
|
||||
export interface WatermarkFontType {
|
||||
color?: string
|
||||
fontSize?: number | string
|
||||
fontWeight?: 'normal' | 'light' | 'weight' | number
|
||||
fontStyle?: 'none' | 'normal' | 'italic' | 'oblique'
|
||||
fontFamily?: string
|
||||
}
|
||||
|
||||
export const watermarkProps = buildProps({
|
||||
/**
|
||||
* @description The z-index of the appended watermark element
|
||||
*/
|
||||
zIndex: {
|
||||
type: Number,
|
||||
default: 9,
|
||||
},
|
||||
/**
|
||||
* @description The rotation angle of the watermark
|
||||
*/
|
||||
rotate: {
|
||||
type: Number,
|
||||
default: -22,
|
||||
},
|
||||
/**
|
||||
* @description The width of the watermark
|
||||
*/
|
||||
width: Number,
|
||||
/**
|
||||
* @description The height of the watermark
|
||||
*/
|
||||
height: Number,
|
||||
/**
|
||||
* @description Image source, it is recommended to export 2x or 3x image, high priority (support base64 format)
|
||||
*/
|
||||
image: String,
|
||||
/**
|
||||
* @description Watermark text content
|
||||
*/
|
||||
content: {
|
||||
type: definePropType<string | string[]>([String, Array]),
|
||||
default: 'Element Plus',
|
||||
},
|
||||
/**
|
||||
* @description Text style
|
||||
*/
|
||||
font: {
|
||||
type: definePropType<WatermarkFontType>(Object),
|
||||
},
|
||||
/**
|
||||
* @description The spacing between watermarks
|
||||
*/
|
||||
gap: {
|
||||
type: definePropType<[number, number]>(Array),
|
||||
default: () => [100, 100],
|
||||
},
|
||||
/**
|
||||
* @description The offset of the watermark from the upper left corner of the container. The default is gap/2
|
||||
*/
|
||||
offset: {
|
||||
type: definePropType<[number, number]>(Array),
|
||||
},
|
||||
} as const)
|
||||
|
||||
export type WatermarkProps = ExtractPropTypes<typeof watermarkProps>
|
||||
export type WatermarkInstance = InstanceType<typeof Watermark>
|
225
packages/components/watermark/src/watermark.vue
Normal file
225
packages/components/watermark/src/watermark.vue
Normal file
@ -0,0 +1,225 @@
|
||||
<template>
|
||||
<div ref="containerRef" :style="[style]">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
computed,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
ref,
|
||||
shallowRef,
|
||||
watch,
|
||||
} from 'vue'
|
||||
import { useMutationObserver } from '@vueuse/core'
|
||||
import { watermarkProps } from './watermark'
|
||||
import { getPixelRatio, getStyleStr, reRendering } from './utils'
|
||||
import useClips, { FontGap } from './useClips'
|
||||
import type { WatermarkProps } from './watermark'
|
||||
import type { CSSProperties } from 'vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'ElWatermark',
|
||||
})
|
||||
|
||||
const style: CSSProperties = {
|
||||
position: 'relative',
|
||||
}
|
||||
|
||||
const props = defineProps(watermarkProps)
|
||||
const color = computed(() => props.font?.color ?? 'rgba(0,0,0,.15)')
|
||||
const fontSize = computed(() => props.font?.fontSize ?? 16)
|
||||
const fontWeight = computed(() => props.font?.fontWeight ?? 'normal')
|
||||
const fontStyle = computed(() => props.font?.fontStyle ?? 'normal')
|
||||
const fontFamily = computed(() => props.font?.fontFamily ?? 'sans-serif')
|
||||
|
||||
const gapX = computed(() => props.gap[0])
|
||||
const gapY = computed(() => props.gap[1])
|
||||
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 getMarkStyle = () => {
|
||||
const markStyle: CSSProperties = {
|
||||
zIndex: props.zIndex,
|
||||
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 containerRef = shallowRef<HTMLDivElement | null>(null)
|
||||
const watermarkRef = shallowRef<HTMLDivElement>()
|
||||
const stopObservation = ref(false)
|
||||
|
||||
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({
|
||||
...getMarkStyle(),
|
||||
backgroundImage: `url('${base64Url}')`,
|
||||
backgroundSize: `${Math.floor(markWidth)}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 image = props.image
|
||||
const content = props.content
|
||||
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 sizes = contents.map((item) => {
|
||||
const metrics = ctx.measureText(item!)
|
||||
|
||||
return [
|
||||
metrics.width,
|
||||
metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent,
|
||||
]
|
||||
})
|
||||
defaultWidth = Math.ceil(Math.max(...sizes.map((size) => size[0])))
|
||||
defaultHeight =
|
||||
Math.ceil(Math.max(...sizes.map((size) => size[1]))) * contents.length +
|
||||
(contents.length - 1) * FontGap
|
||||
}
|
||||
return [width ?? defaultWidth, height ?? defaultHeight] as const
|
||||
}
|
||||
|
||||
const getClips = useClips()
|
||||
|
||||
const renderWatermark = () => {
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
const image = props.image
|
||||
const content = props.content
|
||||
const rotate = props.rotate
|
||||
|
||||
if (ctx) {
|
||||
if (!watermarkRef.value) {
|
||||
watermarkRef.value = document.createElement('div')
|
||||
}
|
||||
|
||||
const ratio = getPixelRatio()
|
||||
const [markWidth, markHeight] = getMarkSize(ctx)
|
||||
|
||||
const drawCanvas = (
|
||||
drawContent?: NonNullable<WatermarkProps['content']> | HTMLImageElement
|
||||
) => {
|
||||
const [textClips, clipWidth] = getClips(
|
||||
drawContent || '',
|
||||
rotate,
|
||||
ratio,
|
||||
markWidth,
|
||||
markHeight,
|
||||
{
|
||||
color: color.value,
|
||||
fontSize: fontSize.value,
|
||||
fontStyle: fontStyle.value,
|
||||
fontWeight: fontWeight.value,
|
||||
fontFamily: fontFamily.value,
|
||||
},
|
||||
gapX.value,
|
||||
gapY.value
|
||||
)
|
||||
|
||||
appendWatermark(textClips, clipWidth)
|
||||
}
|
||||
|
||||
if (image) {
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
drawCanvas(img)
|
||||
}
|
||||
img.onerror = () => {
|
||||
drawCanvas(content)
|
||||
}
|
||||
img.crossOrigin = 'anonymous'
|
||||
img.referrerPolicy = 'no-referrer'
|
||||
img.src = image
|
||||
} else {
|
||||
drawCanvas(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
renderWatermark()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props,
|
||||
() => {
|
||||
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,
|
||||
})
|
||||
</script>
|
0
packages/components/watermark/style/index.ts
Normal file
0
packages/components/watermark/style/index.ts
Normal file
@ -102,6 +102,7 @@ import { ElTree } from '@element-plus/components/tree'
|
||||
import { ElTreeSelect } from '@element-plus/components/tree-select'
|
||||
import { ElTreeV2 } from '@element-plus/components/tree-v2'
|
||||
import { ElUpload } from '@element-plus/components/upload'
|
||||
import { ElWatermark } from '@element-plus/components/watermark'
|
||||
|
||||
import type { Plugin } from 'vue'
|
||||
|
||||
@ -204,4 +205,5 @@ export default [
|
||||
ElTreeSelect,
|
||||
ElTreeV2,
|
||||
ElUpload,
|
||||
ElWatermark,
|
||||
] as Plugin[]
|
||||
|
1
typings/components.d.ts
vendored
1
typings/components.d.ts
vendored
@ -97,6 +97,7 @@ declare module '@vue/runtime-core' {
|
||||
ElDescriptionsItem: typeof import('../packages/element-plus')['ElDescriptionsItem']
|
||||
ElResult: typeof import('../packages/element-plus')['ElResult']
|
||||
ElSelectV2: typeof import('../packages/element-plus')['ElSelectV2']
|
||||
ElWatermark: typeof import('../packages/element-plus')['ElWatermark']
|
||||
}
|
||||
|
||||
interface ComponentCustomProperties {
|
||||
|
Loading…
Reference in New Issue
Block a user