feat: The watermark is staggered by default (#39464)

* chore: watermark add utils

* feat: Support watermark interleaved layout

* docs: add watermark docs

* docs: add watermark demo

* test: add watermark test

* test: add watermark snapshot

* feat: The watermark is staggered by default
This commit is contained in:
JarvisArt 2022-12-13 15:47:57 +08:00 committed by GitHub
parent ff63068e47
commit ec76041584
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 151 additions and 80 deletions

View File

@ -904,14 +904,14 @@ exports[`renders ./components/watermark/demo/custom.tsx extend context correctly
class="ant-input-number-input-wrap"
>
<input
aria-valuenow="200"
aria-valuenow="100"
autocomplete="off"
class="ant-input-number-input"
id="gap_0"
placeholder="gapX"
role="spinbutton"
step="1"
value="200"
value="100"
/>
</div>
</div>
@ -1005,14 +1005,14 @@ exports[`renders ./components/watermark/demo/custom.tsx extend context correctly
class="ant-input-number-input-wrap"
>
<input
aria-valuenow="200"
aria-valuenow="100"
autocomplete="off"
class="ant-input-number-input"
id="gap_1"
placeholder="gapY"
role="spinbutton"
step="1"
value="200"
value="100"
/>
</div>
</div>

View File

@ -402,14 +402,14 @@ exports[`renders ./components/watermark/demo/custom.tsx correctly 1`] = `
class="ant-input-number-input-wrap"
>
<input
aria-valuenow="200"
aria-valuenow="100"
autocomplete="off"
class="ant-input-number-input"
id="gap_0"
placeholder="gapX"
role="spinbutton"
step="1"
value="200"
value="100"
/>
</div>
</div>
@ -503,14 +503,14 @@ exports[`renders ./components/watermark/demo/custom.tsx correctly 1`] = `
class="ant-input-number-input-wrap"
>
<input
aria-valuenow="200"
aria-valuenow="100"
autocomplete="off"
class="ant-input-number-input"
id="gap_1"
placeholder="gapY"
role="spinbutton"
step="1"
value="200"
value="100"
/>
</div>
</div>

View File

@ -6,7 +6,20 @@ exports[`Watermark Image watermark snapshot 1`] = `
style="position: relative;"
>
<div
style="z-index: 9; position: absolute; left: 0; top: 0; width: 100%; height: 100%; background-size: 320px; pointer-events: none; background-repeat: repeat; background-position: 0px 0px; background-image: url('data:image/png;base64,00');"
style="z-index: 9; position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; background-repeat: repeat; background-position: 0px 0px; background-image: url('data:image/png;base64,00'); background-size: 440px;"
/>
</div>
</div>
`;
exports[`Watermark Interleaved watermark backgroundSize is correct 1`] = `
<div>
<div
class="watermark"
style="position: relative;"
>
<div
style="z-index: 9; position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; background-repeat: repeat; background-position: 0px 0px; background-image: url('data:image/png;base64,00'); background-size: 600px;"
/>
</div>
</div>
@ -19,7 +32,7 @@ exports[`Watermark MutationObserver should work properly 1`] = `
style="position: relative;"
>
<div
style="z-index: 9; position: absolute; left: 0; top: 0; width: 100%; height: 100%; background-size: 216px; pointer-events: none; background-repeat: repeat; background-position: 0px 0px; background-image: url('data:image/png;base64,00');"
style="z-index: 9; position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; background-repeat: repeat; background-position: 0px 0px; background-image: url('data:image/png;base64,00'); background-size: 232px;"
/>
</div>
</div>
@ -32,7 +45,7 @@ exports[`Watermark Observe the modification of style 1`] = `
style="position: relative;"
>
<div
style="z-index: 9; position: absolute; left: 0; top: 0; width: 100%; height: 100%; background-size: 216px; pointer-events: none; background-repeat: repeat; background-position: -300px -300px; background-image: url('data:image/png;base64,00');"
style="z-index: 9; position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; background-repeat: repeat; background-position: -250px -250px; background-image: url('data:image/png;base64,00'); background-size: 232px;"
/>
</div>
</div>
@ -45,7 +58,7 @@ exports[`Watermark The offset should be correct 1`] = `
style="position: relative;"
>
<div
style="z-index: 9; position: absolute; left: 100px; top: 100px; width: calc(100% - 100px); height: calc(100% - 100px); background-size: 214px; pointer-events: none; background-repeat: repeat; background-position: 0px 0px; background-image: url('data:image/png;base64,00');"
style="z-index: 9; position: absolute; left: 150px; top: 150px; width: calc(100% - 150px); height: calc(100% - 150px); pointer-events: none; background-repeat: repeat; background-position: 0px 0px; background-image: url('data:image/png;base64,00'); background-size: 228px;"
/>
</div>
</div>
@ -58,7 +71,7 @@ exports[`Watermark The watermark should render successfully 1`] = `
style="position: relative;"
>
<div
style="z-index: 9; position: absolute; left: 0; top: 0; width: 100%; height: 100%; background-size: 210px; pointer-events: none; background-repeat: repeat; background-position: 0px 0px; background-image: url('data:image/png;base64,00');"
style="z-index: 9; position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; background-repeat: repeat; background-position: 0px 0px; background-image: url('data:image/png;base64,00'); background-size: 220px;"
/>
</div>
</div>
@ -69,7 +82,7 @@ exports[`Watermark rtl render component should be rendered correctly in RTL dire
style="position: relative;"
>
<div
style="z-index: 9; position: absolute; left: 0; top: 0; width: 100%; height: 100%; background-size: 200px; pointer-events: none; background-repeat: repeat; background-position: 0px 0px; background-image: url('data:image/png;base64,00');"
style="z-index: 9; position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; background-repeat: repeat; background-position: 0px 0px; background-image: url('data:image/png;base64,00'); background-size: 200px;"
/>
</div>
`;

View File

@ -35,10 +35,25 @@ describe('Watermark', () => {
/>,
);
const target = container.querySelector<HTMLDivElement>('.watermark div');
expect(target?.style.left).toBe('100px');
expect(target?.style.top).toBe('100px');
expect(target?.style.width).toBe('calc(100% - 100px)');
expect(target?.style.height).toBe('calc(100% - 100px)');
expect(target?.style.left).toBe('150px');
expect(target?.style.top).toBe('150px');
expect(target?.style.width).toBe('calc(100% - 150px)');
expect(target?.style.height).toBe('calc(100% - 150px)');
expect(container).toMatchSnapshot();
});
it('Interleaved watermark backgroundSize is correct', () => {
const { container } = render(
<Watermark
className="watermark"
width={200}
height={200}
content="Ant Design"
gap={[100, 100]}
/>,
);
const target = container.querySelector<HTMLDivElement>('.watermark div');
expect(target?.style.backgroundSize).toBe('600px');
expect(container).toMatchSnapshot();
});

View File

@ -49,7 +49,7 @@ const App: React.FC = () => {
fontSize: 16,
zIndex: 11,
rotate: -22,
gap: [200, 200] as [number, number],
gap: [100, 100] as [number, number],
offset: undefined,
});
const { content, color, fontSize, zIndex, rotate, gap, offset } = config;

View File

@ -35,7 +35,7 @@ Add specific text or patterns to the page.
| 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\] | \[200, 200\] | |
| 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

View File

@ -1,16 +1,14 @@
import React, { useEffect, useRef } from 'react';
import useMutationObserver from './useMutationObserver';
import { getStyleStr, getPixelRatio, rotateWatermark } from './utils';
/**
* 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;
const getStyleStr = (style: React.CSSProperties): string => {
const styleArr = Object.keys(style).map((item: keyof React.CSSProperties) => {
const key = item.replace(/([A-Z])/g, '-$1').toLowerCase();
return `${key}: ${style[item]};`;
});
return styleArr.join(' ');
};
export interface WatermarkProps {
zIndex?: number;
rotate?: number;
@ -47,7 +45,7 @@ const Watermark: React.FC<WatermarkProps> = (props) => {
font = {},
style,
className,
gap = [200, 200],
gap = [100, 100],
offset,
children,
} = props;
@ -66,7 +64,7 @@ const Watermark: React.FC<WatermarkProps> = (props) => {
const offsetLeft = offset?.[0] ?? gapXCenter;
const offsetTop = offset?.[1] ?? gapYCenter;
const getMarkStyle = (markWidth: number) => {
const getMarkStyle = () => {
const markStyle: React.CSSProperties = {
zIndex,
position: 'absolute',
@ -74,7 +72,6 @@ const Watermark: React.FC<WatermarkProps> = (props) => {
top: 0,
width: '100%',
height: '100%',
backgroundSize: `${gapX + markWidth}px`,
pointerEvents: 'none',
backgroundRepeat: 'repeat',
};
@ -99,7 +96,7 @@ const Watermark: React.FC<WatermarkProps> = (props) => {
const containerRef = useRef<HTMLDivElement>(null);
const watermarkRef = useRef<HTMLDivElement>();
const { createObserver, destroyObserver } = useMutationObserver();
const { createObserver, destroyObserver, reRendering } = useMutationObserver();
const destroyWatermark = () => {
if (watermarkRef.current) {
@ -108,33 +105,21 @@ const Watermark: React.FC<WatermarkProps> = (props) => {
}
};
const reRendering = (mutation: MutationRecord) => {
let flag = false;
// Whether to delete the watermark node
if (mutation.removedNodes.length) {
flag = Array.from(mutation.removedNodes).some((node) => node === watermarkRef.current);
}
// Whether the watermark dom property value has been modified
if (mutation.type === 'attributes' && mutation.target === watermarkRef.current) {
flag = true;
}
return flag;
};
const appendWatermark = (base64Url: string, markWidth: number) => {
if (containerRef.current && watermarkRef.current) {
destroyObserver();
watermarkRef.current.setAttribute(
'style',
getStyleStr({
...getMarkStyle(markWidth),
...getMarkStyle(),
backgroundImage: `url('${base64Url}')`,
backgroundSize: `${(gapX + markWidth) * BaseSize}px`,
}),
);
containerRef.current?.append(watermarkRef.current);
createObserver(containerRef.current, (mutations) => {
mutations.forEach((mutation) => {
if (reRendering(mutation)) {
if (reRendering(mutation, watermarkRef.current)) {
destroyWatermark();
// eslint-disable-next-line @typescript-eslint/no-use-before-define
renderWatermark();
@ -148,7 +133,7 @@ const Watermark: React.FC<WatermarkProps> = (props) => {
* 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): readonly [number, number] => {
const getMarkSize = (ctx: CanvasRenderingContext2D) => {
let defaultWidth = 120;
let defaultHeight = 64;
if (!image && ctx.measureText) {
@ -161,6 +146,26 @@ const Watermark: React.FC<WatermarkProps> = (props) => {
return [width ?? defaultWidth, height ?? defaultHeight] as const;
};
const fillTexts = (
ctx: CanvasRenderingContext2D,
drawX: number,
drawY: number,
drawWidth: number,
drawHeight: number,
) => {
const ratio = getPixelRatio();
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));
});
};
const renderWatermark = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
@ -170,49 +175,47 @@ const Watermark: React.FC<WatermarkProps> = (props) => {
watermarkRef.current = document.createElement('div');
}
const ratio = window.devicePixelRatio || 1;
const ratio = getPixelRatio();
const [markWidth, markHeight] = getMarkSize(ctx);
const canvasWidth = `${(gapX + markWidth) * ratio}px`;
const canvasHeight = `${(gapY + markHeight) * ratio}px`;
canvas.setAttribute('width', canvasWidth);
canvas.setAttribute('height', canvasHeight);
const canvasWidth = (gapX + markWidth) * ratio;
const canvasHeight = (gapY + markHeight) * ratio;
canvas.setAttribute('width', `${canvasWidth * BaseSize}px`);
canvas.setAttribute('height', `${canvasHeight * BaseSize}px`);
const mergedMarkWidth = markWidth * ratio;
const mergedMarkHeight = markHeight * ratio;
const mergedGapXCenter = (gapX * ratio) / 2;
const mergedGapYCenter = (gapY * ratio) / 2;
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;
/** Alternate drawing parameters */
const alternateDrawX = drawX + canvasWidth;
const alternateDrawY = drawY + canvasHeight;
const alternateRotateX = rotateX + canvasWidth;
const alternateRotateY = rotateY + canvasHeight;
/** Rotate with the canvas as the center point */
const centerX = (mergedMarkWidth + gapX * ratio) / 2;
const centerY = (mergedMarkHeight + gapY * ratio) / 2;
ctx.translate(centerX, centerY);
ctx.rotate((Math.PI / 180) * Number(rotate));
ctx.translate(-centerX, -centerY);
ctx.save();
rotateWatermark(ctx, rotateX, rotateY, rotate);
if (image) {
const img = new Image();
img.onload = () => {
ctx.drawImage(img, mergedGapXCenter, mergedGapYCenter, mergedMarkWidth, mergedMarkHeight);
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 {
const mergedFontSize = Number(fontSize) * ratio;
ctx.font = `${fontStyle} normal ${fontWeight} ${mergedFontSize}px/${mergedMarkHeight}px ${fontFamily}`;
ctx.fillStyle = color;
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.translate(mergedMarkWidth / 2, 0);
const contents = Array.isArray(content) ? content : [content];
contents?.forEach((item, index) => {
ctx.fillText(
item ?? '',
mergedGapXCenter,
mergedGapYCenter + index * (mergedFontSize + FontGap * ratio),
);
});
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);
}
}

View File

@ -36,7 +36,7 @@ demo:
| image | 图片源,建议导出 2 倍或 3 倍图,优先级高 | string | - | |
| content | 水印文字内容 | string \| string[] | - | |
| font | 文字样式 | [Font](#Font) | [Font](#Font) | |
| gap | 水印之间的间距 | \[number, number\] | \[200, 200\] | |
| gap | 水印之间的间距 | \[number, number\] | \[100, 100\] | |
| offset | 水印距离容器左上角的偏移量,默认为 `gap/2` | \[number, number\] | \[gap\[0\]/2, gap\[1\]/2\] | |
### Font

View File

@ -25,5 +25,18 @@ export default function useMutationObserver() {
useEffect(() => destroyObserver, []);
return { createObserver, destroyObserver };
const reRendering = (mutation: MutationRecord, watermarkElement?: HTMLElement) => {
let flag = false;
// Whether to delete the watermark node
if (mutation.removedNodes.length) {
flag = Array.from(mutation.removedNodes).some((node) => node === watermarkElement);
}
// Whether the watermark dom property value has been modified
if (mutation.type === 'attributes' && mutation.target === watermarkElement) {
flag = true;
}
return flag;
};
return { createObserver, destroyObserver, reRendering };
}

View File

@ -0,0 +1,27 @@
/** 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: React.CSSProperties): string {
return Object.keys(style)
.map((key: keyof React.CSSProperties) => `${toLowercaseSeparator(key)}: ${style[key]};`)
.join(' ');
}
/** Returns the ratio of the device's physical pixel resolution to the css pixel resolution */
export function getPixelRatio() {
return window.devicePixelRatio || 1;
}
/** Rotate with the watermark as the center point */
export function rotateWatermark(
ctx: CanvasRenderingContext2D,
rotateX: number,
rotateY: number,
rotate: number,
) {
ctx.translate(rotateX, rotateY);
ctx.rotate((Math.PI / 180) * Number(rotate));
ctx.translate(-rotateX, -rotateY);
}