Skip to content

Commit

Permalink
feat: The watermark is staggered by default (#39464)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
JarvisArt committed Dec 13, 2022
1 parent ff63068 commit ec76041
Show file tree
Hide file tree
Showing 10 changed files with 153 additions and 82 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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>
Expand Down
25 changes: 19 additions & 6 deletions components/watermark/__tests__/__snapshots__/index.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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>
Expand All @@ -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>
Expand All @@ -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>
Expand All @@ -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>
Expand All @@ -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>
`;
23 changes: 19 additions & 4 deletions components/watermark/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});

Expand Down
2 changes: 1 addition & 1 deletion components/watermark/demo/custom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion components/watermark/index.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
123 changes: 63 additions & 60 deletions components/watermark/index.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -47,7 +45,7 @@ const Watermark: React.FC<WatermarkProps> = (props) => {
font = {},
style,
className,
gap = [200, 200],
gap = [100, 100],
offset,
children,
} = props;
Expand All @@ -66,15 +64,14 @@ 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',
left: 0,
top: 0,
width: '100%',
height: '100%',
backgroundSize: `${gapX + markWidth}px`,
pointerEvents: 'none',
backgroundRepeat: 'repeat',
};
Expand All @@ -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) {
Expand All @@ -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();
Expand All @@ -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) {
Expand All @@ -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');
Expand All @@ -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 mergedMarkWidth = markWidth * ratio;
const mergedMarkHeight = markHeight * ratio;
const mergedGapXCenter = (gapX * ratio) / 2;
const mergedGapYCenter = (gapY * ratio) / 2;

/** 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);
const canvasWidth = (gapX + markWidth) * ratio;
const canvasHeight = (gapY + markHeight) * ratio;
canvas.setAttribute('width', `${canvasWidth * BaseSize}px`);
canvas.setAttribute('height', `${canvasHeight * BaseSize}px`);

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;

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);
}
}
Expand Down

0 comments on commit ec76041

Please sign in to comment.