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
This commit is contained in:
二货爱吃白萝卜 2023-08-08 16:48:26 +08:00 committed by GitHub
parent 4fe27ba027
commit 8a3870fc31
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 575 additions and 207 deletions

View File

@ -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';

View File

@ -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';

View File

@ -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<DrawerProps> & {
motionDeadline: 500,
});
// ============================ Refs ============================
// Select `ant-modal-content` by `panelRef`
const panelRef = usePanelRef();
// =========================== Render ===========================
return wrapSSR(
<NoCompactStyle>
@ -157,6 +162,7 @@ const Drawer: React.FC<DrawerProps> & {
rootClassName={drawerClassName}
getContainer={getContainer}
afterOpenChange={afterOpenChange ?? afterVisibleChange}
panelRef={panelRef}
>
<DrawerPanel prefixCls={prefixCls} {...rest} onClose={onClose} />
</RcDrawer>

View File

@ -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<Placement, 'topCenter' | 'bottomCenter'>;
type OverlayFunc = () => React.ReactElement;

View File

@ -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';

View File

@ -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<ModalProps> = (props) => {
true,
);
// ============================ Refs ============================
// Select `ant-modal-content` by `panelRef`
const panelRef = usePanelRef(`.${prefixCls}-content`);
// =========================== Render ===========================
return wrapSSR(
<NoCompactStyle>
<NoFormStyle status override>
@ -124,6 +130,7 @@ const Modal: React.FC<ModalProps> = (props) => {
maskTransitionName={getTransitionName(rootPrefixCls, 'fade', props.maskTransitionName)}
className={classNames(hashId, className, modal?.className)}
style={{ ...modal?.style, ...style }}
panelRef={panelRef}
/>
</NoFormStyle>
</NoCompactStyle>,

View File

@ -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 [
<div
class="ant-space ant-space-horizontal ant-space-align-center"
>
<div
class="ant-space-item"
style="margin-right: 8px;"
>
<button
class="ant-btn ant-btn-default"
type="button"
>
<span>
Show Modal
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-default"
type="button"
>
<span>
Show Drawer
</span>
</button>
</div>
</div>,
<div
class=""
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: 220px;"
/>
</div>,
]
`;
exports[`renders components/watermark/demo/portal.tsx extend context correctly 2`] = `[]`;

View File

@ -795,3 +795,41 @@ exports[`renders components/watermark/demo/multi-line.tsx correctly 1`] = `
/>
</div>
`;
exports[`renders components/watermark/demo/portal.tsx correctly 1`] = `
Array [
<div
class="ant-space ant-space-horizontal ant-space-align-center"
>
<div
class="ant-space-item"
style="margin-right:8px"
>
<button
class="ant-btn ant-btn-default"
type="button"
>
<span>
Show Modal
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-default"
type="button"
>
<span>
Show Drawer
</span>
</button>
</div>
</div>,
<div
class=""
style="position:relative"
/>,
]
`;

View File

@ -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(<Watermark className="test">{children}</Watermark>);
await waitFakeTimer();
const watermark = getWatermarkElement();
expect(watermark).toHaveStyle({
zIndex: '9',
});
// Not crash when children removed
rerender(<Watermark className="test" />);
});
}
test(
'Modal',
<Modal open />,
() => document.body.querySelector('.ant-modal-content')!.lastChild!,
);
test(
'Drawer',
<Drawer open />,
() => document.body.querySelector('.ant-drawer-content')!.lastChild!,
);
});
});

View File

@ -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<WatermarkContextProps>({
add: voidFunc,
remove: voidFunc,
});
export function usePanelRef(panelSelector?: string) {
const watermark = React.useContext(WatermarkContext);
const panelEleRef = React.useRef<HTMLElement>();
const panelRef = useEvent((ele: HTMLElement | null) => {
if (ele) {
const innerContentEle = panelSelector ? ele.querySelector<HTMLElement>(panelSelector)! : ele;
watermark.add(innerContentEle);
panelEleRef.current = innerContentEle;
} else {
watermark.remove(panelEleRef.current!);
}
});
return panelRef;
}
export default WatermarkContext;

View File

@ -0,0 +1,7 @@
## zh-CN
在 Modal 与 Drawer 中使用。
## en-US
Use in Modal and Drawer.

View File

@ -0,0 +1,50 @@
import React from 'react';
import { Watermark, Modal, Drawer, Button, Space } from 'antd';
const placeholder = (
<div
style={{
height: 300,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
background: 'rgba(150, 150, 150, 0.2)',
}}
>
A mock height
</div>
);
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 (
<>
<Space>
<Button onClick={() => setShowModal(true)}>Show Modal</Button>
<Button onClick={() => setShowDrawer(true)}>Show Drawer</Button>
</Space>
<Watermark content="Ant Design">
<Modal
destroyOnClose
open={showModal}
title="Modal"
onCancel={closeModal}
onOk={closeModal}
>
{placeholder}
</Modal>
<Drawer destroyOnClose open={showDrawer} title="Drawer" onClose={closeDrawer}>
{placeholder}
</Drawer>
</Watermark>
</>
);
};
export default App;

View File

@ -22,6 +22,7 @@ Add specific text or patterns to the page.
<code src="./demo/multi-line.tsx">Multi-line watermark</code>
<code src="./demo/image.tsx">Image watermark</code>
<code src="./demo/custom.tsx">Custom configuration</code>
<code src="./demo/portal.tsx">Modal or Drawer</code>
## API

View File

@ -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<T>(prev: Set<T>, next: Set<T>) {
return prev.size === next.size ? prev : next;
}
const Watermark: React.FC<WatermarkProps> = (props) => {
const {
/**
@ -68,8 +74,8 @@ const Watermark: React.FC<WatermarkProps> = (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<WatermarkProps> = (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`;
mergedStyle.backgroundPosition = `${positionLeft}px ${positionTop}px`;
return markStyle;
};
return mergedStyle;
}, [zIndex, offsetLeft, gapXCenter, offsetTop, gapYCenter]);
const containerRef = useRef<HTMLDivElement>(null);
const watermarkRef = useRef<HTMLDivElement>();
const stopObservation = useRef(false);
const [container, setContainer] = React.useState<HTMLDivElement | null>();
const destroyWatermark = () => {
if (watermarkRef.current) {
watermarkRef.current.remove();
watermarkRef.current = undefined;
}
};
// Used for nest case like Modal, Drawer
const [subElements, setSubElements] = React.useState(new Set<HTMLElement>());
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;
// Nest elements should also support watermark
const targetElements = React.useMemo(() => {
const list = container ? [container] : [];
return [...list, ...Array.from(subElements)];
}, [container, subElements]);
// ============================ Content =============================
const [watermarkInfo, setWatermarkInfo] = React.useState<[base64: string, contentWidth: number]>(
null!,
);
// Generate new Watermark content
const renderWatermark = useContent(
{
...props,
rotate,
gap,
},
(base64, contentWidth) => {
setWatermarkInfo([base64, contentWidth]);
},
);
const syncWatermark = useRafDebounce(renderWatermark);
// ============================= Effect =============================
// Append watermark to the container
const [appendWatermark, removeWatermark, isWatermarkEle] = useWatermark(markStyle, gapX);
useEffect(() => {
if (watermarkInfo) {
targetElements.forEach((holder) => {
appendWatermark(watermarkInfo[0], watermarkInfo[1], holder);
});
}
};
/**
* 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);
appendWatermark(canvas.toDataURL(), markWidth);
};
const renderWatermark = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (ctx) {
if (!watermarkRef.current) {
watermarkRef.current = document.createElement('div');
}
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);
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,
);
}
}
};
}, [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<WatermarkProps> = (props) => {
offsetTop,
]);
// ============================ Context =============================
const watermarkContext = React.useMemo<WatermarkContextProps>(
() => ({
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 (
<MutateObserver onMutate={onMutate}>
<div
ref={containerRef}
className={classNames(className, rootClassName)}
style={{ position: 'relative', ...style }}
>
{children}
</div>
</MutateObserver>
<div
ref={setContainer}
className={classNames(className, rootClassName)}
style={{ position: 'relative', ...style }}
>
<WatermarkContext.Provider value={watermarkContext}>{children}</WatermarkContext.Provider>
</div>
);
};

View File

@ -23,6 +23,7 @@ demo:
<code src="./demo/multi-line.tsx">多行水印</code>
<code src="./demo/image.tsx">图片水印</code>
<code src="./demo/custom.tsx">自定义配置</code>
<code src="./demo/portal.tsx">Modal 与 Drawer</code>
## API

View File

@ -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<WatermarkProps, 'width' | 'height' | 'image' | 'content' | 'font'> &
Required<Pick<WatermarkProps, 'rotate' | 'gap'>>,
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;
}

View File

@ -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<number>();
const wrapperCallback = useEvent(callback);
return () => {
if (executeRef.current) {
return;
}
executeRef.current = true;
wrapperCallback();
rafRef.current = raf(() => {
executeRef.current = false;
});
};
}

View File

@ -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<HTMLElement, HTMLDivElement>());
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];
}

View File

@ -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;

View File

@ -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"
},