From de556f0e6acf8d3c83b22e88b0669601330b806a Mon Sep 17 00:00:00 2001 From: JarvisArt <1120886013@qq.com> Date: Fri, 2 Dec 2022 20:46:08 +0800 Subject: [PATCH 1/7] feat: New Component Watermark * docs: add watermark docs * docs: add watermark demo * test: add watermark test * test: add watermark snapshot * chore: add jest-canvas-mock --- .jest.js | 2 +- .../__snapshots__/index.test.ts.snap | 1 + components/index.tsx | 2 + .../__snapshots__/demo-extend.test.ts.snap | 62 +++++ .../__tests__/__snapshots__/demo.test.ts.snap | 62 +++++ .../__snapshots__/index.test.tsx.snap | 71 ++++++ .../watermark/__tests__/demo-extend.test.ts | 3 + components/watermark/__tests__/demo.test.ts | 3 + components/watermark/__tests__/image.test.ts | 5 + components/watermark/__tests__/index.test.tsx | 70 ++++++ components/watermark/demo/basic.md | 7 + components/watermark/demo/basic.tsx | 10 + components/watermark/demo/image.md | 7 + components/watermark/demo/image.tsx | 14 ++ components/watermark/demo/multi-line.md | 7 + components/watermark/demo/multi-line.tsx | 10 + components/watermark/demo/z-index.md | 7 + components/watermark/demo/z-index.tsx | 44 ++++ components/watermark/index.en-US.md | 51 ++++ components/watermark/index.tsx | 234 ++++++++++++++++++ components/watermark/index.zh-CN.md | 52 ++++ components/watermark/useMutationObserver.ts | 45 ++++ package.json | 1 + tests/__snapshots__/index.test.ts.snap | 1 + 24 files changed, 770 insertions(+), 1 deletion(-) create mode 100644 components/watermark/__tests__/__snapshots__/demo-extend.test.ts.snap create mode 100644 components/watermark/__tests__/__snapshots__/demo.test.ts.snap create mode 100644 components/watermark/__tests__/__snapshots__/index.test.tsx.snap create mode 100644 components/watermark/__tests__/demo-extend.test.ts create mode 100644 components/watermark/__tests__/demo.test.ts create mode 100644 components/watermark/__tests__/image.test.ts create mode 100644 components/watermark/__tests__/index.test.tsx create mode 100644 components/watermark/demo/basic.md create mode 100644 components/watermark/demo/basic.tsx create mode 100644 components/watermark/demo/image.md create mode 100644 components/watermark/demo/image.tsx create mode 100644 components/watermark/demo/multi-line.md create mode 100644 components/watermark/demo/multi-line.tsx create mode 100644 components/watermark/demo/z-index.md create mode 100644 components/watermark/demo/z-index.tsx create mode 100644 components/watermark/index.en-US.md create mode 100644 components/watermark/index.tsx create mode 100644 components/watermark/index.zh-CN.md create mode 100644 components/watermark/useMutationObserver.ts diff --git a/.jest.js b/.jest.js index 2af90f6d8f6a..679d0b161aa7 100644 --- a/.jest.js +++ b/.jest.js @@ -34,7 +34,7 @@ function getTestRegex(libDir) { module.exports = { verbose: true, testEnvironment: 'jsdom', - setupFiles: ['./tests/setup.js'], + setupFiles: ['./tests/setup.js', 'jest-canvas-mock'], setupFilesAfterEnv: ['./tests/setupAfterEnv.ts'], moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'md'], modulePathIgnorePatterns: ['/_site/'], diff --git a/components/__tests__/__snapshots__/index.test.ts.snap b/components/__tests__/__snapshots__/index.test.ts.snap index 740ef46f1fa8..d174610afb88 100644 --- a/components/__tests__/__snapshots__/index.test.ts.snap +++ b/components/__tests__/__snapshots__/index.test.ts.snap @@ -65,6 +65,7 @@ exports[`antd exports modules correctly 1`] = ` "TreeSelect", "Typography", "Upload", + "Watermark", "message", "notification", "theme", diff --git a/components/index.tsx b/components/index.tsx index 9043a3add8a9..d2118d9bf6bd 100644 --- a/components/index.tsx +++ b/components/index.tsx @@ -149,4 +149,6 @@ export { default as Typography } from './typography'; export type { TypographyProps } from './typography'; export { default as Upload } from './upload'; export type { UploadFile, UploadProps } from './upload'; +export { default as Watermark } from './watermark'; +export type { WatermarkProps } from './watermark'; export { default as version } from './version'; diff --git a/components/watermark/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/watermark/__tests__/__snapshots__/demo-extend.test.ts.snap new file mode 100644 index 000000000000..d397a83e1ed3 --- /dev/null +++ b/components/watermark/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -0,0 +1,62 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders ./components/watermark/demo/basic.tsx extend context correctly 1`] = ` +
+
+
+`; + +exports[`renders ./components/watermark/demo/image.tsx extend context correctly 1`] = ` +
+
+
+`; + +exports[`renders ./components/watermark/demo/multi-line.tsx extend context correctly 1`] = ` +
+
+
+`; + +exports[`renders ./components/watermark/demo/z-index.tsx extend context correctly 1`] = ` +
+
+
+ The light-speed iteration of the digital world makes products more complex. However, human consciousness and attention resources are limited. Facing this design contradiction, the pursuit of natural interaction will be the consistent direction of Ant Design. +
+
+ Natural user cognition: According to cognitive psychology, about 80% of external information is obtained through visual channels. The most important visual elements in the interface design, including layout, colors, illustrations, icons, etc., should fully absorb the laws of nature, thereby reducing the user's cognitive cost and bringing authentic and smooth feelings. In some scenarios, opportunely adding other sensory channels such as hearing, touch can create a richer and more natural product experience. +
+
+ Natural user behavior: In the interaction with the system, the designer should fully understand the relationship between users, system roles, and task objectives, and also contextually organize system functions and services. At the same time, a series of methods such as behavior analysis, artificial intelligence and sensors could be applied to assist users to make effective decisions and reduce extra operations of users, to save users' mental and physical resources and make human-computer interaction more natural. +
+
+ 示例图片 +
+`; diff --git a/components/watermark/__tests__/__snapshots__/demo.test.ts.snap b/components/watermark/__tests__/__snapshots__/demo.test.ts.snap new file mode 100644 index 000000000000..662f93cfc6fb --- /dev/null +++ b/components/watermark/__tests__/__snapshots__/demo.test.ts.snap @@ -0,0 +1,62 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders ./components/watermark/demo/basic.tsx correctly 1`] = ` +
+
+
+`; + +exports[`renders ./components/watermark/demo/image.tsx correctly 1`] = ` +
+
+
+`; + +exports[`renders ./components/watermark/demo/multi-line.tsx correctly 1`] = ` +
+
+
+`; + +exports[`renders ./components/watermark/demo/z-index.tsx correctly 1`] = ` +
+
+
+ The light-speed iteration of the digital world makes products more complex. However, human consciousness and attention resources are limited. Facing this design contradiction, the pursuit of natural interaction will be the consistent direction of Ant Design. +
+
+ Natural user cognition: According to cognitive psychology, about 80% of external information is obtained through visual channels. The most important visual elements in the interface design, including layout, colors, illustrations, icons, etc., should fully absorb the laws of nature, thereby reducing the user's cognitive cost and bringing authentic and smooth feelings. In some scenarios, opportunely adding other sensory channels such as hearing, touch can create a richer and more natural product experience. +
+
+ Natural user behavior: In the interaction with the system, the designer should fully understand the relationship between users, system roles, and task objectives, and also contextually organize system functions and services. At the same time, a series of methods such as behavior analysis, artificial intelligence and sensors could be applied to assist users to make effective decisions and reduce extra operations of users, to save users' mental and physical resources and make human-computer interaction more natural. +
+
+ 示例图片 +
+`; diff --git a/components/watermark/__tests__/__snapshots__/index.test.tsx.snap b/components/watermark/__tests__/__snapshots__/index.test.tsx.snap new file mode 100644 index 000000000000..89e435bb5107 --- /dev/null +++ b/components/watermark/__tests__/__snapshots__/index.test.tsx.snap @@ -0,0 +1,71 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Watermark Image watermark snapshot 1`] = ` +
+
+
+
+
+`; + +exports[`Watermark MutationObserver should work properly 1`] = ` +
+
+
+`; + +exports[`Watermark Observe the modification of data-watermark-id 1`] = ` +
+
+
+
+
+`; + +exports[`Watermark The offset should be correct 1`] = ` +
+
+
+
+
+`; + +exports[`Watermark The watermark should render successfully 1`] = ` +
+
+
+
+
+`; + +exports[`Watermark rtl render component should be rendered correctly in RTL direction 1`] = ` +
+`; diff --git a/components/watermark/__tests__/demo-extend.test.ts b/components/watermark/__tests__/demo-extend.test.ts new file mode 100644 index 000000000000..5c97653ae588 --- /dev/null +++ b/components/watermark/__tests__/demo-extend.test.ts @@ -0,0 +1,3 @@ +import { extendTest } from '../../../tests/shared/demoTest'; + +extendTest('watermark'); diff --git a/components/watermark/__tests__/demo.test.ts b/components/watermark/__tests__/demo.test.ts new file mode 100644 index 000000000000..60b75d580286 --- /dev/null +++ b/components/watermark/__tests__/demo.test.ts @@ -0,0 +1,3 @@ +import demoTest from '../../../tests/shared/demoTest'; + +demoTest('watermark'); diff --git a/components/watermark/__tests__/image.test.ts b/components/watermark/__tests__/image.test.ts new file mode 100644 index 000000000000..04fae6f308d0 --- /dev/null +++ b/components/watermark/__tests__/image.test.ts @@ -0,0 +1,5 @@ +import { imageDemoTest } from '../../../tests/shared/imageTest'; + +describe('Watermark image', () => { + imageDemoTest('watermark'); +}); diff --git a/components/watermark/__tests__/index.test.tsx b/components/watermark/__tests__/index.test.tsx new file mode 100644 index 000000000000..8c8915b45ad2 --- /dev/null +++ b/components/watermark/__tests__/index.test.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import Watermark from '..'; +import mountTest from '../../../tests/shared/mountTest'; +import rtlTest from '../../../tests/shared/rtlTest'; +import { render } from '../../../tests/utils'; + +describe('Watermark', () => { + mountTest(Watermark); + rtlTest(Watermark); + + const mockSrcSet = jest.spyOn(Image.prototype, 'src', 'set'); + + beforeAll(() => { + mockSrcSet.mockImplementation(function fn() { + if (this.onload) { + this.onload(); + } + }); + }); + + afterAll(() => { + mockSrcSet.mockRestore(); + }); + + it('The watermark should render successfully', () => { + const { container } = render(); + expect(container.querySelector('.watermark div')).toBeTruthy(); + expect(container).toMatchSnapshot(); + }); + + it('The offset should be correct', () => { + const { container } = render( + , + ); + const target = container.querySelector('.watermark div') as HTMLDivElement; + 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(container).toMatchSnapshot(); + }); + + it('Image watermark snapshot', () => { + const { container } = render( + , + ); + expect(container).toMatchSnapshot(); + }); + + it('MutationObserver should work properly', () => { + const { container } = render(); + const target = container.querySelector('.watermark div') as HTMLDivElement; + target.style.zIndex = '0'; + target.remove(); + expect(container).toMatchSnapshot(); + }); + + it('Observe the modification of data-watermark-id', () => { + const { container } = render( + , + ); + const target = container.querySelector('.watermark div') as HTMLDivElement; + target.setAttribute('data-watermark-id', 'jfei'); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/components/watermark/demo/basic.md b/components/watermark/demo/basic.md new file mode 100644 index 000000000000..c2120a5571c8 --- /dev/null +++ b/components/watermark/demo/basic.md @@ -0,0 +1,7 @@ +## zh-CN + +最简单的用法。 + +## en-US + +The most basic usage. diff --git a/components/watermark/demo/basic.tsx b/components/watermark/demo/basic.tsx new file mode 100644 index 000000000000..98828425d068 --- /dev/null +++ b/components/watermark/demo/basic.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { Watermark } from 'antd'; + +const App: React.FC = () => ( + +
+ +); + +export default App; diff --git a/components/watermark/demo/image.md b/components/watermark/demo/image.md new file mode 100644 index 000000000000..a1ace81b1464 --- /dev/null +++ b/components/watermark/demo/image.md @@ -0,0 +1,7 @@ +## zh-CN + +通过 `image` 指定图片地址。为保证图片高清且不被拉伸,请设置 width 和 height, 并上传至少两倍的宽高的 logo 图片地址。 + +## en-US + +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. diff --git a/components/watermark/demo/image.tsx b/components/watermark/demo/image.tsx new file mode 100644 index 000000000000..53b0664dea0b --- /dev/null +++ b/components/watermark/demo/image.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { Watermark } from 'antd'; + +const App: React.FC = () => ( + +
+ +); + +export default App; diff --git a/components/watermark/demo/multi-line.md b/components/watermark/demo/multi-line.md new file mode 100644 index 000000000000..1207f337976c --- /dev/null +++ b/components/watermark/demo/multi-line.md @@ -0,0 +1,7 @@ +## zh-CN + +通过 `content` 设置 字符串数组 指定多行文字水印内容。 + +## en-US + +Use 'content' to set a string array to specify multi-line text watermark content. diff --git a/components/watermark/demo/multi-line.tsx b/components/watermark/demo/multi-line.tsx new file mode 100644 index 000000000000..b97592ba12ae --- /dev/null +++ b/components/watermark/demo/multi-line.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { Watermark } from 'antd'; + +const App: React.FC = () => ( + +
+ +); + +export default App; diff --git a/components/watermark/demo/z-index.md b/components/watermark/demo/z-index.md new file mode 100644 index 000000000000..ee397be7b311 --- /dev/null +++ b/components/watermark/demo/z-index.md @@ -0,0 +1,7 @@ +## zh-CN + +zIndex 默认值为 9,通过设置更大的 `zIndex` 堆叠在元素的顶级。 + +## en-US + +zIndex defaults to 9, by setting a larger `zIndex` to stack at the top level of the element. diff --git a/components/watermark/demo/z-index.tsx b/components/watermark/demo/z-index.tsx new file mode 100644 index 000000000000..1000c94f280f --- /dev/null +++ b/components/watermark/demo/z-index.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { Watermark, Typography } from 'antd'; + +const { Paragraph } = Typography; + +const App: React.FC = () => ( + + + + The light-speed iteration of the digital world makes products more complex. However, human + consciousness and attention resources are limited. Facing this design contradiction, the + pursuit of natural interaction will be the consistent direction of Ant Design. + + + Natural user cognition: According to cognitive psychology, about 80% of external information + is obtained through visual channels. The most important visual elements in the interface + design, including layout, colors, illustrations, icons, etc., should fully absorb the laws + of nature, thereby reducing the user's cognitive cost and bringing authentic and smooth + feelings. In some scenarios, opportunely adding other sensory channels such as hearing, + touch can create a richer and more natural product experience. + + + Natural user behavior: In the interaction with the system, the designer should fully + understand the relationship between users, system roles, and task objectives, and also + contextually organize system functions and services. At the same time, a series of methods + such as behavior analysis, artificial intelligence and sensors could be applied to assist + users to make effective decisions and reduce extra operations of users, to save users' + mental and physical resources and make human-computer interaction more natural. + + + 示例图片 + +); + +export default App; diff --git a/components/watermark/index.en-US.md b/components/watermark/index.en-US.md new file mode 100644 index 000000000000..f7f9d2d62294 --- /dev/null +++ b/components/watermark/index.en-US.md @@ -0,0 +1,51 @@ +--- +category: Components +group: Other +title: Watermark +cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*wr1ISY50SyYAAAAAAAAAAAAADrJ8AQ/original +demo: + cols: 1 +--- + +Add specific text or patterns to the page. + +## When To Use + +- Use when the page needs to be watermarked to identify the copyright. +- Suitable for preventing information theft. + +## Examples + + +Basic +Multi-line watermark +Image watermark +High priority + +## API + +### Watermark + +| Property | Description | Type | Default | Version | +| --- | --- | --- | --- | --- | +| width | The width of the watermark | number | 120 | | +| height | The height of the watermark | 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) | | +| style | Container layer style | React.CSSProperties | - | | +| className | The className of the container layer | string | - | | +| gap | The spacing between watermarks | \[number, number\] | \[200, 200\] | | +| 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 + +| 参数 | 说明 | 类型 | 默认值 | 版本 | +| ---------- | ----------- | ------------------------------------------- | --------------- | ---- | +| color | font color | string | rgba(0,0,0,.15) | | +| fontSize | font size | number | 16 | | +| fontWeight | font weight | `normal` \| `light` \| `weight` \| number | normal | | +| fontFamily | font family | string | sans-serif | | +| fontStyle | font style | `none` \| `normal` \| `italic` \| `oblique` | normal | | diff --git a/components/watermark/index.tsx b/components/watermark/index.tsx new file mode 100644 index 000000000000..50ea2dba240d --- /dev/null +++ b/components/watermark/index.tsx @@ -0,0 +1,234 @@ +import React, { useEffect, useMemo, useRef } from 'react'; +import useMutationObserver from './useMutationObserver'; + +const getStyleStr = (style: Record): string => { + let styleStr = ''; + Object.keys(style).forEach((item) => { + const key = item.replace(/([A-Z])/g, '-$1').toLowerCase(); + styleStr += `${key}: ${style[item]}; `; + }); + return styleStr; +}; + +export interface WatermarkProps { + zIndex?: number; + rotate?: number; + width?: number; + height?: number; + image?: string; + content?: string | string[]; + font?: { + color?: string; + fontSize?: number | string; + fontWeight?: 'normal' | 'light' | 'weight' | number; + fontStyle?: 'none' | 'normal' | 'italic' | 'oblique'; + fontFamily?: string; + }; + style?: React.CSSProperties; + className?: string; + gap?: [number, number]; + offset?: [number, number]; + children?: React.ReactNode; +} + +const Watermark: React.FC = (props) => { + const { + /** + * The antd content layer zIndex is basically below 10 + * https://github.com/ant-design/ant-design/blob/6192403b2ce517c017f9e58a32d58774921c10cd/components/style/themes/default.less#L335 + */ + zIndex = 9, + rotate = -22, + width = 120, + height = 64, + image, + content, + font = {}, + style, + className, + gap = [200, 200], + offset, + children, + } = props; + + const { + color = 'rgba(0,0,0,.15)', + fontSize = 16, + fontWeight = 'normal', + fontStyle = 'normal', + fontFamily = 'sans-serif', + } = font; + + const [gapX, gapY] = gap; + + /** Calculate the style of the offset */ + const offsetStyle = useMemo(() => { + const gapXCenter = gapX / 2; + const gapYCenter = gapY / 2; + const offsetLeft = offset?.[0] ?? gapXCenter; + const offsetTop = offset?.[1] ?? gapYCenter; + const mergedOffsetStyle: React.CSSProperties = {}; + let mergedOffsetLeft = offsetLeft - gapXCenter; + let mergedOffsetTop = offsetTop - gapYCenter; + + if (mergedOffsetLeft > 0) { + mergedOffsetStyle.left = `${mergedOffsetLeft}px`; + mergedOffsetStyle.width = `calc(100% - ${mergedOffsetLeft}px)`; + mergedOffsetLeft = 0; + } + if (mergedOffsetTop > 0) { + mergedOffsetStyle.top = `${mergedOffsetTop}px`; + mergedOffsetStyle.height = `calc(100% - ${mergedOffsetTop}px)`; + mergedOffsetTop = 0; + } + + return { + ...mergedOffsetStyle, + backgroundPosition: `${mergedOffsetLeft}px ${mergedOffsetTop}px`, + }; + }, [offset, gap]); + + const markStyle: React.CSSProperties = { + zIndex, + position: 'absolute', + left: 0, + top: 0, + width: '100%', + height: '100%', + backgroundSize: `${gapX + width}px`, + pointerEvents: 'none', + backgroundRepeat: 'repeat', + ...offsetStyle, + }; + + const containerRef = useRef(null); + const watermarkDom = useRef(); + const { WATERMARK_ID_NAME, watermarkId, createObserver, destroyObserver } = useMutationObserver(); + + const destroyWatermark = () => { + if (watermarkDom.current) { + watermarkDom.current.remove(); + watermarkDom.current = undefined; + } + }; + + const reRendering = (mutation: MutationRecord) => { + let flag = false; + // Whether to delete the watermark node + if (mutation.removedNodes.length) { + mutation.removedNodes.forEach((node) => { + if ((node as HTMLDivElement).getAttribute?.(WATERMARK_ID_NAME) === watermarkId) { + flag = true; + } + }); + } + // Whether the watermark dom property value has been modified + if ( + mutation.type === 'attributes' && + ((mutation.target as HTMLDivElement).getAttribute?.(WATERMARK_ID_NAME) === watermarkId || + mutation.attributeName === WATERMARK_ID_NAME) + ) { + flag = true; + } + return flag; + }; + + const appendWatermark = (base64Url: string) => { + if (containerRef.current && watermarkDom.current) { + destroyObserver(); + watermarkDom.current.setAttribute(WATERMARK_ID_NAME, watermarkId); + watermarkDom.current.setAttribute( + 'style', + getStyleStr({ + ...markStyle, + backgroundImage: `url('${base64Url}')`, + }), + ); + containerRef.current?.append(watermarkDom.current); + createObserver(containerRef.current, (mutations) => { + mutations.forEach((mutation) => { + if (reRendering(mutation)) { + destroyWatermark(); + // eslint-disable-next-line @typescript-eslint/no-use-before-define + renderWatermark(); + } + }); + }); + } + }; + + const renderWatermark = () => { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + if (ctx) { + if (!watermarkDom.current) { + watermarkDom.current = document.createElement('div'); + } + + const ratio = window.devicePixelRatio; + const canvasWidth = `${(gapX + width) * ratio}px`; + const canvasHeight = `${(gapY + height) * ratio}px`; + + canvas.setAttribute('width', canvasWidth); + canvas.setAttribute('height', canvasHeight); + + const markWidth = width * ratio; + const markHeight = height * ratio; + const gapXCenter = (gapX * ratio) / 2; + const gapYCenter = (gapY * ratio) / 2; + + /** Rotate with the canvas as the center point */ + const centerX = (markWidth + gapX * ratio) / 2; + const centerY = (markHeight + gapY * ratio) / 2; + ctx.translate(centerX, centerY); + ctx.rotate((Math.PI / 180) * Number(rotate)); + ctx.translate(-centerX, -centerY); + + if (image) { + const img = new Image(); + img.onload = () => { + ctx.drawImage(img, gapXCenter, gapYCenter, markWidth, markHeight); + appendWatermark(canvas.toDataURL()); + }; + img.crossOrigin = 'anonymous'; + img.referrerPolicy = 'no-referrer'; + img.src = image; + } else if (content) { + const markSize = Number(fontSize) * ratio; + const fontGap = ratio * 3; + ctx.font = `${fontStyle} normal ${fontWeight} ${markSize}px/${markHeight}px ${fontFamily}`; + ctx.fillStyle = color; + ctx.textAlign = 'center'; + ctx.translate(markWidth / 2, markSize); + if (Array.isArray(content)) { + content?.forEach((item: string, index: number) => + ctx.fillText(item, gapXCenter, gapYCenter + index * (markSize + fontGap)), + ); + } else { + ctx.fillText(content, gapXCenter, gapYCenter); + } + appendWatermark(canvas.toDataURL()); + } + } + }; + + useEffect(() => { + renderWatermark(); + }, [rotate, zIndex, width, height, image, content, font, style, className, gap]); + + return ( +
+ {children} +
+ ); +}; + +export default Watermark; diff --git a/components/watermark/index.zh-CN.md b/components/watermark/index.zh-CN.md new file mode 100644 index 000000000000..8b4f29efa8fd --- /dev/null +++ b/components/watermark/index.zh-CN.md @@ -0,0 +1,52 @@ +--- +category: Components +subtitle: 水印 +group: 其他 +title: Watermark +cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*wr1ISY50SyYAAAAAAAAAAAAADrJ8AQ/original +demo: + cols: 1 +--- + +给页面的某个区域加上水印。 + +## 何时使用 + +- 页面需要添加水印标识版权时使用。 +- 适用于防止信息盗用。 + +## 代码演示 + + +基本 +多行水印 +图片水印 +高优先级 + +## API + +### Watermark + +| 参数 | 说明 | 类型 | 默认值 | 版本 | +| --- | --- | --- | --- | --- | +| width | 水印的宽度 | number | 120 | | +| height | 水印的高度 | number | 64 | | +| rotate | 水印绘制时,旋转的角度,单位 `°` | number | -22 | | +| zIndex | 追加的水印元素的 z-index | number | 9 | | +| image | 图片源,建议导出 2 倍或 3 倍图,优先级高 | string | - | | +| content | 水印文字内容 | string \| string[] | - | | +| font | 文字样式 | [Font](#Font) | [Font](#Font) | | +| style | 容器层的样式 | React.CSSProperties | - | | +| className | 容器层的类名 | string | - | | +| gap | 水印之间的间距 | \[number, number\] | \[200, 200\] | | +| offset | 水印距离容器左上角的偏移量,默认为 `gap/2` | \[number, number\] | \[gap\[0\]/2, gap\[1\]/2\] | | + +### Font + +| 参数 | 说明 | 类型 | 默认值 | 版本 | +| ---------- | -------- | ------------------------------------------- | --------------- | ---- | +| color | 字体颜色 | string | rgba(0,0,0,.15) | | +| fontSize | 字体大小 | number | 16 | | +| fontWeight | 字体粗细 | `normal` \| `light` \| `weight` \| number | normal | | +| fontFamily | 字体类型 | string | sans-serif | | +| fontStyle | 字体样式 | `none` \| `normal` \| `italic` \| `oblique` | normal | | diff --git a/components/watermark/useMutationObserver.ts b/components/watermark/useMutationObserver.ts new file mode 100644 index 000000000000..54ddc4e35d9b --- /dev/null +++ b/components/watermark/useMutationObserver.ts @@ -0,0 +1,45 @@ +import { useEffect, useRef } from 'react'; + +const getRandomId = () => { + let randomId = `${Math.random().toString(36).substring(2, 6)}-${Date.now()}`; + if (process.env.NODE_ENV === 'test') { + randomId = '7mte-1669650061677'; + } + return randomId; +}; + +const WATERMARK_ID_NAME = 'data-watermark-id'; + +export default function useMutationObserver() { + const instance = useRef(); + const watermarkId = useRef(getRandomId()); + + const destroyObserver = () => { + if (instance.current) { + instance.current.takeRecords(); + instance.current.disconnect(); + instance.current = undefined; + } + }; + + const createObserver = (target: Node, callback: MutationCallback) => { + if (MutationObserver) { + destroyObserver(); + instance.current = new MutationObserver(callback); + instance.current.observe(target, { + childList: true, + subtree: true, + attributeFilter: ['style', 'class', WATERMARK_ID_NAME], + }); + } + }; + + useEffect(() => () => destroyObserver(), []); + + return { + WATERMARK_ID_NAME, + watermarkId: watermarkId.current, + createObserver, + destroyObserver, + }; +} diff --git a/package.json b/package.json index 76f5d5fa03b6..2fa8faf3410b 100644 --- a/package.json +++ b/package.json @@ -241,6 +241,7 @@ "isomorphic-fetch": "^3.0.0", "jest": "^29.0.0", "jest-axe": "^7.0.0", + "jest-canvas-mock": "^2.4.0", "jest-environment-jsdom": "^29.0.1", "jest-environment-node": "^29.0.0", "jest-image-snapshot": "^6.0.0", diff --git a/tests/__snapshots__/index.test.ts.snap b/tests/__snapshots__/index.test.ts.snap index b867fa54bcd4..9240e277a4f0 100644 --- a/tests/__snapshots__/index.test.ts.snap +++ b/tests/__snapshots__/index.test.ts.snap @@ -65,6 +65,7 @@ exports[`antd dist files exports modules correctly 1`] = ` "TreeSelect", "Typography", "Upload", + "Watermark", "message", "notification", "theme", From 6a159d35190074905e86ce9b7d1363b07f757e54 Mon Sep 17 00:00:00 2001 From: JarvisArt <1120886013@qq.com> Date: Sun, 4 Dec 2022 19:59:56 +0800 Subject: [PATCH 2/7] feat: Watermark calculates the width and height of content by default * docs: update docs * docs: update demo * test: update snapshot --- .../__snapshots__/demo-extend.test.ts.snap | 1286 ++++++++++++++++- .../__tests__/__snapshots__/demo.test.ts.snap | 784 +++++++++- .../__snapshots__/index.test.tsx.snap | 13 +- components/watermark/demo/custom.md | 7 + components/watermark/demo/custom.tsx | 167 +++ components/watermark/demo/z-index.md | 7 - components/watermark/demo/z-index.tsx | 44 - components/watermark/index.en-US.md | 6 +- components/watermark/index.tsx | 69 +- components/watermark/index.zh-CN.md | 6 +- 10 files changed, 2245 insertions(+), 144 deletions(-) create mode 100644 components/watermark/demo/custom.md create mode 100644 components/watermark/demo/custom.tsx delete mode 100644 components/watermark/demo/z-index.md delete mode 100644 components/watermark/demo/z-index.tsx diff --git a/components/watermark/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/watermark/__tests__/__snapshots__/demo-extend.test.ts.snap index d397a83e1ed3..e5dc9abb7590 100644 --- a/components/watermark/__tests__/__snapshots__/demo-extend.test.ts.snap +++ b/components/watermark/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -10,17 +10,1266 @@ exports[`renders ./components/watermark/demo/basic.tsx extend context correctly
`; -exports[`renders ./components/watermark/demo/image.tsx extend context correctly 1`] = ` +exports[`renders ./components/watermark/demo/custom.tsx extend context correctly 1`] = `
+ style="position:relative" + > +
+
+ The light-speed iteration of the digital world makes products more complex. However, human consciousness and attention resources are limited. Facing this design contradiction, the pursuit of natural interaction will be the consistent direction of Ant Design. +
+
+ Natural user cognition: According to cognitive psychology, about 80% of external information is obtained through visual channels. The most important visual elements in the interface design, including layout, colors, illustrations, icons, etc., should fully absorb the laws of nature, thereby reducing the user's cognitive cost and bringing authentic and smooth feelings. In some scenarios, opportunely adding other sensory channels such as hearing, touch can create a richer and more natural product experience. +
+
+ Natural user behavior: In the interaction with the system, the designer should fully understand the relationship between users, system roles, and task objectives, and also contextually organize system functions and services. At the same time, a series of methods such as behavior analysis, artificial intelligence and sensors could be applied to assist users to make effective decisions and reduce extra operations of users, to save users' mental and physical resources and make human-computer interaction more natural. +
+
+ 示例图片 +
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ +
+