From 8a3870fc3162471f244faddce8be2f2935a71962 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E7=88=B1=E5=90=83=E7=99=BD=E8=90=9D?= =?UTF-8?q?=E5=8D=9C?= Date: Tue, 8 Aug 2023 16:48:26 +0800 Subject: [PATCH] feat: Watermark support nest Modal & Drawer (#44104) * docs: add demo * refactor: init * refactor: of it * refactor: simple content * chore: unique func * chore: refactor * chore: support modal watermark * feat: support nest watermark * test: add test case * chore: fix lint * chore: bump rc-image * test: add test case * refactor: use same func --- components/_util/wave/useWave.ts | 2 +- components/anchor/Anchor.tsx | 2 +- components/drawer/index.tsx | 6 + components/dropdown/dropdown.tsx | 4 +- components/menu/menu.tsx | 2 +- components/modal/Modal.tsx | 7 + .../__snapshots__/demo-extend.test.ts.snap | 44 +++ .../__tests__/__snapshots__/demo.test.ts.snap | 38 +++ components/watermark/__tests__/index.test.tsx | 32 ++ components/watermark/context.ts | 33 ++ components/watermark/demo/portal.md | 7 + components/watermark/demo/portal.tsx | 50 +++ components/watermark/index.en-US.md | 1 + components/watermark/index.tsx | 286 ++++++------------ components/watermark/index.zh-CN.md | 1 + components/watermark/useContent.tsx | 156 ++++++++++ components/watermark/useRafDebounce.tsx | 26 ++ components/watermark/useWatermark.tsx | 61 ++++ components/watermark/utils.ts | 6 +- package.json | 10 +- 20 files changed, 571 insertions(+), 203 deletions(-) create mode 100644 components/watermark/context.ts create mode 100644 components/watermark/demo/portal.md create mode 100644 components/watermark/demo/portal.tsx create mode 100644 components/watermark/useContent.tsx create mode 100644 components/watermark/useRafDebounce.tsx create mode 100644 components/watermark/useWatermark.tsx diff --git a/components/_util/wave/useWave.ts b/components/_util/wave/useWave.ts index 788141ebb7ad..24bee62e2243 100644 --- a/components/_util/wave/useWave.ts +++ b/components/_util/wave/useWave.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -import useEvent from 'rc-util/lib/hooks/useEvent'; +import { useEvent } from 'rc-util'; import raf from 'rc-util/lib/raf'; import showWaveEffect from './WaveEffect'; import { ConfigContext } from '../../config-provider'; diff --git a/components/anchor/Anchor.tsx b/components/anchor/Anchor.tsx index 0a8e510fcab7..af5436e79a8b 100644 --- a/components/anchor/Anchor.tsx +++ b/components/anchor/Anchor.tsx @@ -1,5 +1,5 @@ import classNames from 'classnames'; -import useEvent from 'rc-util/lib/hooks/useEvent'; +import { useEvent } from 'rc-util'; import * as React from 'react'; import scrollIntoView from 'scroll-into-view-if-needed'; diff --git a/components/drawer/index.tsx b/components/drawer/index.tsx index 8b737f885d4f..b71ba51e56e9 100644 --- a/components/drawer/index.tsx +++ b/components/drawer/index.tsx @@ -16,6 +16,7 @@ import DrawerPanel from './DrawerPanel'; // CSSINJS import { NoCompactStyle } from '../space/Compact'; import useStyle from './style'; +import { usePanelRef } from '../watermark/context'; const SizeTypes = ['default', 'large'] as const; type sizeType = typeof SizeTypes[number]; @@ -137,6 +138,10 @@ const Drawer: React.FC & { motionDeadline: 500, }); + // ============================ Refs ============================ + // Select `ant-modal-content` by `panelRef` + const panelRef = usePanelRef(); + // =========================== Render =========================== return wrapSSR( @@ -157,6 +162,7 @@ const Drawer: React.FC & { rootClassName={drawerClassName} getContainer={getContainer} afterOpenChange={afterOpenChange ?? afterVisibleChange} + panelRef={panelRef} > diff --git a/components/dropdown/dropdown.tsx b/components/dropdown/dropdown.tsx index ca92efd35b03..c9250140a33f 100644 --- a/components/dropdown/dropdown.tsx +++ b/components/dropdown/dropdown.tsx @@ -1,7 +1,7 @@ import RightOutlined from '@ant-design/icons/RightOutlined'; import classNames from 'classnames'; import RcDropdown from 'rc-dropdown'; -import useEvent from 'rc-util/lib/hooks/useEvent'; +import { useEvent } from 'rc-util'; import useMergedState from 'rc-util/lib/hooks/useMergedState'; import omit from 'rc-util/lib/omit'; import * as React from 'react'; @@ -28,7 +28,7 @@ const Placements = [ 'bottom', ] as const; -type Placement = (typeof Placements)[number]; +type Placement = typeof Placements[number]; type DropdownPlacement = Exclude; type OverlayFunc = () => React.ReactElement; diff --git a/components/menu/menu.tsx b/components/menu/menu.tsx index 2bc36bf1852a..6a1e67f7472d 100644 --- a/components/menu/menu.tsx +++ b/components/menu/menu.tsx @@ -2,7 +2,7 @@ import EllipsisOutlined from '@ant-design/icons/EllipsisOutlined'; import classNames from 'classnames'; import type { MenuProps as RcMenuProps, MenuRef as RcMenuRef } from 'rc-menu'; import RcMenu from 'rc-menu'; -import useEvent from 'rc-util/lib/hooks/useEvent'; +import { useEvent } from 'rc-util'; import omit from 'rc-util/lib/omit'; import * as React from 'react'; import { forwardRef } from 'react'; diff --git a/components/modal/Modal.tsx b/components/modal/Modal.tsx index 14974f4cbe44..7a5138ab5f2d 100644 --- a/components/modal/Modal.tsx +++ b/components/modal/Modal.tsx @@ -12,6 +12,7 @@ import { NoCompactStyle } from '../space/Compact'; import type { ModalProps, MousePosition } from './interface'; import { Footer, renderCloseIcon } from './shared'; import useStyle from './style'; +import { usePanelRef } from '../watermark/context'; let mousePosition: MousePosition; @@ -103,6 +104,11 @@ const Modal: React.FC = (props) => { true, ); + // ============================ Refs ============================ + // Select `ant-modal-content` by `panelRef` + const panelRef = usePanelRef(`.${prefixCls}-content`); + + // =========================== Render =========================== return wrapSSR( @@ -124,6 +130,7 @@ const Modal: React.FC = (props) => { maskTransitionName={getTransitionName(rootPrefixCls, 'fade', props.maskTransitionName)} className={classNames(hashId, className, modal?.className)} style={{ ...modal?.style, ...style }} + panelRef={panelRef} /> , diff --git a/components/watermark/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/watermark/__tests__/__snapshots__/demo-extend.test.ts.snap index b9505885a6e7..3892d5837c0d 100644 --- a/components/watermark/__tests__/__snapshots__/demo-extend.test.ts.snap +++ b/components/watermark/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -1233,3 +1233,47 @@ exports[`renders components/watermark/demo/multi-line.tsx extend context correct `; exports[`renders components/watermark/demo/multi-line.tsx extend context correctly 2`] = `[]`; + +exports[`renders components/watermark/demo/portal.tsx extend context correctly 1`] = ` +Array [ +
+
+ +
+
+ +
+
, +
+
+
, +] +`; + +exports[`renders components/watermark/demo/portal.tsx extend context correctly 2`] = `[]`; diff --git a/components/watermark/__tests__/__snapshots__/demo.test.ts.snap b/components/watermark/__tests__/__snapshots__/demo.test.ts.snap index 2f0ff2f26fc1..d11fb0d591eb 100644 --- a/components/watermark/__tests__/__snapshots__/demo.test.ts.snap +++ b/components/watermark/__tests__/__snapshots__/demo.test.ts.snap @@ -795,3 +795,41 @@ exports[`renders components/watermark/demo/multi-line.tsx correctly 1`] = ` />
`; + +exports[`renders components/watermark/demo/portal.tsx correctly 1`] = ` +Array [ +
+
+ +
+
+ +
+
, +
, +] +`; diff --git a/components/watermark/__tests__/index.test.tsx b/components/watermark/__tests__/index.test.tsx index be45ffb33b0c..5ccf993c33ef 100644 --- a/components/watermark/__tests__/index.test.tsx +++ b/components/watermark/__tests__/index.test.tsx @@ -1,5 +1,7 @@ import React from 'react'; import Watermark from '..'; +import Modal from '../../modal'; +import Drawer from '../../drawer'; import mountTest from '../../../tests/shared/mountTest'; import rtlTest from '../../../tests/shared/rtlTest'; import { render, waitFakeTimer, waitFor } from '../../../tests/utils'; @@ -94,4 +96,34 @@ describe('Watermark', () => { await waitFor(() => expect(target).toBeTruthy()); expect(container).toMatchSnapshot(); }); + + describe('nest component', () => { + function test(name: string, children: React.ReactNode, getWatermarkElement: () => Node) { + it(name, async () => { + const { rerender } = render({children}); + await waitFakeTimer(); + + const watermark = getWatermarkElement(); + + expect(watermark).toHaveStyle({ + zIndex: '9', + }); + + // Not crash when children removed + rerender(); + }); + } + + test( + 'Modal', + , + () => document.body.querySelector('.ant-modal-content')!.lastChild!, + ); + + test( + 'Drawer', + , + () => document.body.querySelector('.ant-drawer-content')!.lastChild!, + ); + }); }); diff --git a/components/watermark/context.ts b/components/watermark/context.ts new file mode 100644 index 000000000000..d30a8add955b --- /dev/null +++ b/components/watermark/context.ts @@ -0,0 +1,33 @@ +import { useEvent } from 'rc-util'; +import * as React from 'react'; + +export interface WatermarkContextProps { + add: (ele: HTMLElement) => void; + remove: (ele: HTMLElement) => void; +} + +function voidFunc() {} + +const WatermarkContext = React.createContext({ + add: voidFunc, + remove: voidFunc, +}); + +export function usePanelRef(panelSelector?: string) { + const watermark = React.useContext(WatermarkContext); + + const panelEleRef = React.useRef(); + const panelRef = useEvent((ele: HTMLElement | null) => { + if (ele) { + const innerContentEle = panelSelector ? ele.querySelector(panelSelector)! : ele; + watermark.add(innerContentEle); + panelEleRef.current = innerContentEle; + } else { + watermark.remove(panelEleRef.current!); + } + }); + + return panelRef; +} + +export default WatermarkContext; diff --git a/components/watermark/demo/portal.md b/components/watermark/demo/portal.md new file mode 100644 index 000000000000..7ce87a489aaa --- /dev/null +++ b/components/watermark/demo/portal.md @@ -0,0 +1,7 @@ +## zh-CN + +在 Modal 与 Drawer 中使用。 + +## en-US + +Use in Modal and Drawer. diff --git a/components/watermark/demo/portal.tsx b/components/watermark/demo/portal.tsx new file mode 100644 index 000000000000..cfb1223fa751 --- /dev/null +++ b/components/watermark/demo/portal.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { Watermark, Modal, Drawer, Button, Space } from 'antd'; + +const placeholder = ( +
+ A mock height +
+); + +const App: React.FC = () => { + const [showModal, setShowModal] = React.useState(false); + const [showDrawer, setShowDrawer] = React.useState(false); + + const closeModal = () => setShowModal(false); + const closeDrawer = () => setShowDrawer(false); + + return ( + <> + + + + + + + + {placeholder} + + + {placeholder} + + + + ); +}; + +export default App; diff --git a/components/watermark/index.en-US.md b/components/watermark/index.en-US.md index 59aceaa47880..1856b28341a2 100644 --- a/components/watermark/index.en-US.md +++ b/components/watermark/index.en-US.md @@ -22,6 +22,7 @@ Add specific text or patterns to the page. Multi-line watermark Image watermark Custom configuration +Modal or Drawer ## API diff --git a/components/watermark/index.tsx b/components/watermark/index.tsx index 43d99510728f..f2d3ad270c8b 100644 --- a/components/watermark/index.tsx +++ b/components/watermark/index.tsx @@ -1,15 +1,13 @@ -import MutateObserver from '@rc-component/mutate-observer'; +import { useMutateObserver } from '@rc-component/mutate-observer'; import classNames from 'classnames'; -import React, { useEffect, useRef } from 'react'; -import { getPixelRatio, getStyleStr, reRendering, rotateWatermark } from './utils'; +import React, { useEffect } from 'react'; +import { reRendering } from './utils'; import theme from '../theme'; - -/** - * 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; +import useWatermark from './useWatermark'; +import useRafDebounce from './useRafDebounce'; +import useContent from './useContent'; +import WatermarkContext from './context'; +import type { WatermarkContextProps } from './context'; export interface WatermarkProps { zIndex?: number; @@ -33,6 +31,14 @@ export interface WatermarkProps { children?: React.ReactNode; } +/** + * Only return `next` when size changed. + * This is only used for elements compare, not a shallow equal! + */ +function getSizeDiff(prev: Set, next: Set) { + return prev.size === next.size ? prev : next; +} + const Watermark: React.FC = (props) => { const { /** @@ -68,8 +74,8 @@ const Watermark: React.FC = (props) => { const offsetLeft = offset?.[0] ?? gapXCenter; const offsetTop = offset?.[1] ?? gapYCenter; - const getMarkStyle = () => { - const markStyle: React.CSSProperties = { + const markStyle = React.useMemo(() => { + const mergedStyle: React.CSSProperties = { zIndex, position: 'absolute', left: 0, @@ -84,197 +90,74 @@ const Watermark: React.FC = (props) => { let positionLeft = offsetLeft - gapXCenter; let positionTop = offsetTop - gapYCenter; if (positionLeft > 0) { - markStyle.left = `${positionLeft}px`; - markStyle.width = `calc(100% - ${positionLeft}px)`; + mergedStyle.left = `${positionLeft}px`; + mergedStyle.width = `calc(100% - ${positionLeft}px)`; positionLeft = 0; } if (positionTop > 0) { - markStyle.top = `${positionTop}px`; - markStyle.height = `calc(100% - ${positionTop}px)`; + mergedStyle.top = `${positionTop}px`; + mergedStyle.height = `calc(100% - ${positionTop}px)`; positionTop = 0; } - markStyle.backgroundPosition = `${positionLeft}px ${positionTop}px`; - - return markStyle; - }; - - const containerRef = useRef(null); - const watermarkRef = useRef(); - const stopObservation = useRef(false); - - const destroyWatermark = () => { - if (watermarkRef.current) { - watermarkRef.current.remove(); - watermarkRef.current = undefined; - } - }; - - const appendWatermark = (base64Url: string, markWidth: number) => { - if (containerRef.current && watermarkRef.current) { - stopObservation.current = true; - watermarkRef.current.setAttribute( - 'style', - getStyleStr({ - ...getMarkStyle(), - backgroundImage: `url('${base64Url}')`, - backgroundSize: `${(gapX + markWidth) * BaseSize}px`, - }), - ); - containerRef.current?.append(watermarkRef.current); - // Delayed execution - setTimeout(() => { - stopObservation.current = false; - }); - } - }; + mergedStyle.backgroundPosition = `${positionLeft}px ${positionTop}px`; - /** - * 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; - if (!image && ctx.measureText) { - ctx.font = `${Number(fontSize)}px ${fontFamily}`; - 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) * contents.length + (contents.length - 1) * FontGap; - } - return [width ?? defaultWidth, height ?? defaultHeight] as const; - }; + return mergedStyle; + }, [zIndex, offsetLeft, gapXCenter, offsetTop, gapYCenter]); - 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 [container, setContainer] = React.useState(); - const drawText = ( - canvas: HTMLCanvasElement, - ctx: CanvasRenderingContext2D, - drawX: number, - drawY: number, - drawWidth: number, - drawHeight: number, - alternateRotateX: number, - alternateRotateY: number, - alternateDrawX: number, - alternateDrawY: number, - markWidth: number, - ) => { - 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); - }; + // Used for nest case like Modal, Drawer + const [subElements, setSubElements] = React.useState(new Set()); - const renderWatermark = () => { - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); + // Nest elements should also support watermark + const targetElements = React.useMemo(() => { + const list = container ? [container] : []; + return [...list, ...Array.from(subElements)]; + }, [container, subElements]); - if (ctx) { - if (!watermarkRef.current) { - watermarkRef.current = document.createElement('div'); - } + // ============================ Content ============================= + const [watermarkInfo, setWatermarkInfo] = React.useState<[base64: string, contentWidth: number]>( + null!, + ); - const ratio = getPixelRatio(); - const [markWidth, markHeight] = getMarkSize(ctx); - const canvasWidth = (gapX + markWidth) * ratio; - const canvasHeight = (gapY + markHeight) * ratio; - canvas.setAttribute('width', `${canvasWidth * BaseSize}px`); - canvas.setAttribute('height', `${canvasHeight * BaseSize}px`); + // Generate new Watermark content + const renderWatermark = useContent( + { + ...props, + rotate, + gap, + }, + (base64, contentWidth) => { + setWatermarkInfo([base64, contentWidth]); + }, + ); - 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; + const syncWatermark = useRafDebounce(renderWatermark); - ctx.save(); - rotateWatermark(ctx, rotateX, rotateY, rotate); + // ============================= Effect ============================= + // Append watermark to the container + const [appendWatermark, removeWatermark, isWatermarkEle] = useWatermark(markStyle, gapX); - 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.onerror = () => - drawText( - canvas, - ctx, - drawX, - drawY, - drawWidth, - drawHeight, - alternateRotateX, - alternateRotateY, - alternateDrawX, - alternateDrawY, - markWidth, - ); - img.crossOrigin = 'anonymous'; - img.referrerPolicy = 'no-referrer'; - img.src = image; - } else { - drawText( - canvas, - ctx, - drawX, - drawY, - drawWidth, - drawHeight, - alternateRotateX, - alternateRotateY, - alternateDrawX, - alternateDrawY, - markWidth, - ); - } + useEffect(() => { + if (watermarkInfo) { + targetElements.forEach((holder) => { + appendWatermark(watermarkInfo[0], watermarkInfo[1], holder); + }); } - }; + }, [watermarkInfo, targetElements]); + // ============================ Observe ============================= const onMutate = (mutations: MutationRecord[]) => { - if (stopObservation.current) { - return; - } mutations.forEach((mutation) => { - if (reRendering(mutation, watermarkRef.current)) { - destroyWatermark(); - renderWatermark(); + if (reRendering(mutation, isWatermarkEle)) { + syncWatermark(); } }); }; - useEffect(renderWatermark, [ + useMutateObserver(targetElements, onMutate); + + useEffect(syncWatermark, [ rotate, zIndex, width, @@ -292,16 +175,39 @@ const Watermark: React.FC = (props) => { offsetTop, ]); + // ============================ Context ============================= + const watermarkContext = React.useMemo( + () => ({ + add: (ele) => { + setSubElements((prev) => { + const clone = new Set(prev); + clone.add(ele); + return getSizeDiff(prev, clone); + }); + }, + remove: (ele) => { + removeWatermark(ele); + + setSubElements((prev) => { + const clone = new Set(prev); + clone.delete(ele); + + return getSizeDiff(prev, clone); + }); + }, + }), + [], + ); + + // ============================= Render ============================= return ( - -
- {children} -
-
+
+ {children} +
); }; diff --git a/components/watermark/index.zh-CN.md b/components/watermark/index.zh-CN.md index 78f3c0601231..0cb3867d0576 100644 --- a/components/watermark/index.zh-CN.md +++ b/components/watermark/index.zh-CN.md @@ -23,6 +23,7 @@ demo: 多行水印 图片水印 自定义配置 +Modal 与 Drawer ## API diff --git a/components/watermark/useContent.tsx b/components/watermark/useContent.tsx new file mode 100644 index 000000000000..705bd8b7d512 --- /dev/null +++ b/components/watermark/useContent.tsx @@ -0,0 +1,156 @@ +import type { WatermarkProps } from '.'; +import useToken from '../theme/useToken'; +import { BaseSize, FontGap } from './useWatermark'; +import { getPixelRatio, rotateWatermark } from './utils'; + +export default function useContent( + props: Pick & + Required>, + callback: (base64Url: string, markWidth: number) => void, +) { + const { rotate, width, height, image, content, font = {}, gap } = props; + + const [, token] = useToken(); + + const { + color = token.colorFill, + fontSize = token.fontSizeLG, + fontWeight = 'normal', + fontStyle = 'normal', + fontFamily = 'sans-serif', + } = font; + + const [gapX, gapY] = gap; + + /** + * 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; + if (!image && ctx.measureText) { + ctx.font = `${Number(fontSize)}px ${fontFamily}`; + 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) * 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 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 drawText = ( + canvas: HTMLCanvasElement, + ctx: CanvasRenderingContext2D, + drawX: number, + drawY: number, + drawWidth: number, + drawHeight: number, + alternateRotateX: number, + alternateRotateY: number, + alternateDrawX: number, + alternateDrawY: number, + markWidth: number, + ) => { + 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); + callback(canvas.toDataURL(), markWidth); + }; + + const renderWatermark = () => { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + if (ctx) { + const ratio = getPixelRatio(); + const [markWidth, markHeight] = getMarkSize(ctx); + 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, drawX, drawY, drawWidth, drawHeight); + /** Draw interleaved pictures after rotation */ + ctx.restore(); + rotateWatermark(ctx, alternateRotateX, alternateRotateY, rotate); + ctx.drawImage(img, alternateDrawX, alternateDrawY, drawWidth, drawHeight); + callback(canvas.toDataURL(), markWidth); + }; + img.onerror = () => + drawText( + canvas, + ctx, + drawX, + drawY, + drawWidth, + drawHeight, + alternateRotateX, + alternateRotateY, + alternateDrawX, + alternateDrawY, + markWidth, + ); + img.crossOrigin = 'anonymous'; + img.referrerPolicy = 'no-referrer'; + img.src = image; + } else { + drawText( + canvas, + ctx, + drawX, + drawY, + drawWidth, + drawHeight, + alternateRotateX, + alternateRotateY, + alternateDrawX, + alternateDrawY, + markWidth, + ); + } + } + }; + + return renderWatermark; +} diff --git a/components/watermark/useRafDebounce.tsx b/components/watermark/useRafDebounce.tsx new file mode 100644 index 000000000000..6936dd14f520 --- /dev/null +++ b/components/watermark/useRafDebounce.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import raf from 'rc-util/lib/raf'; +import { useEvent } from 'rc-util'; + +/** + * Callback will only execute last one for each raf + */ +export default function useRafDebounce(callback: VoidFunction) { + const executeRef = React.useRef(false); + const rafRef = React.useRef(); + + const wrapperCallback = useEvent(callback); + + return () => { + if (executeRef.current) { + return; + } + + executeRef.current = true; + wrapperCallback(); + + rafRef.current = raf(() => { + executeRef.current = false; + }); + }; +} diff --git a/components/watermark/useWatermark.tsx b/components/watermark/useWatermark.tsx new file mode 100644 index 000000000000..ded73e570049 --- /dev/null +++ b/components/watermark/useWatermark.tsx @@ -0,0 +1,61 @@ +import * as React from 'react'; +import { getStyleStr } from './utils'; + +/** + * Base size of the canvas, 1 for parallel layout and 2 for alternate layout + * Only alternate layout is currently supported + */ +export const BaseSize = 2; +export const FontGap = 3; + +export type AppendWatermark = ( + base64Url: string, + markWidth: number, + container: HTMLElement, +) => void; + +export default function useWatermark( + markStyle: React.CSSProperties, + gapX: number, +): [ + appendWatermark: AppendWatermark, + removeWatermark: (container: HTMLElement) => void, + isWatermarkEle: (ele: Node) => boolean, +] { + const [watermarkMap] = React.useState(() => new Map()); + + const appendWatermark = (base64Url: string, markWidth: number, container: HTMLElement) => { + if (container) { + if (!watermarkMap.get(container)) { + const newWatermarkEle = document.createElement('div'); + watermarkMap.set(container, newWatermarkEle); + } + + const watermarkEle = watermarkMap.get(container)!; + + watermarkEle.setAttribute( + 'style', + getStyleStr({ + ...markStyle, + backgroundImage: `url('${base64Url}')`, + backgroundSize: `${(gapX + markWidth) * BaseSize}px`, + }), + ); + container.append(watermarkEle); + } + }; + + const removeWatermark = (container: HTMLElement) => { + const watermarkEle = watermarkMap.get(container); + + if (watermarkEle && container) { + container.removeChild(watermarkEle); + } + + watermarkMap.delete(container); + }; + + const isWatermarkEle = (ele: any) => Array.from(watermarkMap.values()).includes(ele); + + return [appendWatermark, removeWatermark, isWatermarkEle]; +} diff --git a/components/watermark/utils.ts b/components/watermark/utils.ts index 84103b59c533..e0fdc07a9bf2 100644 --- a/components/watermark/utils.ts +++ b/components/watermark/utils.ts @@ -27,14 +27,14 @@ export function rotateWatermark( } /** Whether to re-render the watermark */ -export const reRendering = (mutation: MutationRecord, watermarkElement?: HTMLElement) => { +export const reRendering = (mutation: MutationRecord, isWatermarkEle: (ele: any) => boolean) => { let flag = false; // Whether to delete the watermark node if (mutation.removedNodes.length) { - flag = Array.from(mutation.removedNodes).some((node) => node === watermarkElement); + flag = Array.from(mutation.removedNodes).some((node) => isWatermarkEle(node)); } // Whether the watermark dom property value has been modified - if (mutation.type === 'attributes' && mutation.target === watermarkElement) { + if (mutation.type === 'attributes' && isWatermarkEle(mutation.target)) { flag = true; } return flag; diff --git a/package.json b/package.json index 721cb2e92601..b2c3f3883af8 100644 --- a/package.json +++ b/package.json @@ -116,7 +116,7 @@ "@babel/runtime": "^7.18.3", "@ctrl/tinycolor": "^3.6.0", "@rc-component/color-picker": "~1.4.0", - "@rc-component/mutate-observer": "^1.0.0", + "@rc-component/mutate-observer": "^1.1.0", "@rc-component/tour": "~1.8.1", "@rc-component/trigger": "^1.15.0", "classnames": "^2.2.6", @@ -126,11 +126,11 @@ "rc-cascader": "~3.14.0", "rc-checkbox": "~3.1.0", "rc-collapse": "~3.7.0", - "rc-dialog": "~9.1.0", - "rc-drawer": "~6.2.0", + "rc-dialog": "~9.2.0", + "rc-drawer": "~6.4.1", "rc-dropdown": "~4.1.0", "rc-field-form": "~1.36.0", - "rc-image": "~7.1.0", + "rc-image": "~7.2.0", "rc-input": "~1.1.0", "rc-input-number": "~8.0.2", "rc-mentions": "~2.5.0", @@ -154,7 +154,7 @@ "rc-tree": "~5.7.6", "rc-tree-select": "~5.11.0", "rc-upload": "~4.3.0", - "rc-util": "^5.32.0", + "rc-util": "^5.36.0", "scroll-into-view-if-needed": "^3.0.3", "throttle-debounce": "^5.0.0" },