feat(elements): add image shape support radius, adpat elements

This commit is contained in:
antv 2024-08-05 21:28:31 +08:00
parent 9c5b855816
commit ab3f3e2644
11 changed files with 225 additions and 6 deletions

View File

@ -0,0 +1,32 @@
import { Graph } from '@antv/g6';
const avatar = 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*_Do9Tq7MxFQAAAAAAAAAAAAADmJ7AQ/original';
const logo = 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*AzSISZeq81IAAAAAAAAAAAAADmJ7AQ/original';
export const elementNodeAvatar: TestCase = async (context) => {
const graph = new Graph({
...context,
data: {
nodes: [
{
id: 'node-1',
type: 'circle',
style: { iconSrc: avatar, iconWidth: 30, iconHeight: 30, iconRadius: 15, size: 40 },
},
{
id: 'node-2',
type: 'image',
style: { src: avatar, size: 40, radius: 20, iconSrc: logo, iconWidth: 40, iconHeight: 40, iconRadius: 20 },
},
],
},
layout: {
type: 'grid',
},
behaviors: ['drag-element', 'drag-canvas', 'zoom-canvas'],
});
await graph.render();
return graph;
};

View File

@ -41,6 +41,7 @@ export { elementEdgeQuadratic } from './element-edge-quadratic';
export { elementEdgeSize } from './element-edge-size';
export { elementLabelBackground } from './element-label-background';
export { elementLabelOversized } from './element-label-oversized';
export { elementNodeAvatar } from './element-node-avatar';
export { elementNodeBadges } from './element-node-badges';
export { elementNodeCircle } from './element-node-circle';
export { elementNodeDiamond } from './element-node-diamond';

View File

@ -0,0 +1,42 @@
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500" style="background: transparent; position: absolute; outline: none;" color-interpolation-filters="sRGB" tabindex="1">
<defs>
<path id="g-svg-9" fill="none" d="M 250,110 l 0,0 a 15,15,0,0,1,15,15 l 0,0 a 15,15,0,0,1,-15,15 l 0,0 a 15,15,0,0,1,-15,-15 l 0,0 a 15,15,0,0,1,15,-15 z" x="235" y="110" width="30" height="30"/>
<clipPath id="clip-path-9-8" transform="matrix(1,0,0,1,-250,-125)">
<use href="#g-svg-9"/>
</clipPath>
<path id="g-svg-14" fill="none" d="M 250,355 l 0,0 a 20,20,0,0,1,20,20 l 0,0 a 20,20,0,0,1,-20,20 l 0,0 a 20,20,0,0,1,-20,-20 l 0,0 a 20,20,0,0,1,20,-20 z" x="230" y="355" width="40" height="40"/>
<clipPath id="clip-path-14-11" transform="matrix(1,0,0,1,-250,-375)">
<use href="#g-svg-14"/>
</clipPath>
<path id="g-svg-15" fill="none" d="M 250,355 l 0,0 a 20,20,0,0,1,20,20 l 0,0 a 20,20,0,0,1,-20,20 l 0,0 a 20,20,0,0,1,-20,-20 l 0,0 a 20,20,0,0,1,20,-20 z" x="230" y="355" width="40" height="40"/>
<clipPath id="clip-path-15-13" transform="matrix(1,0,0,1,-250,-375)">
<use href="#g-svg-15"/>
</clipPath>
</defs>
<g >
<g fill="none">
<g fill="none">
<g fill="none" x="250" y="125" transform="matrix(1,0,0,1,250,125)">
<g>
<circle fill="rgba(23,131,255,1)" class="key" stroke-width="0" stroke="rgba(0,0,0,1)" r="20"/>
</g>
<g fill="none" class="icon" width="30" height="30">
<g clip-path="url(#clip-path-9-8)">
<image fill="rgba(255,255,255,1)" preserveAspectRatio="none" x="-15" y="-15" href="https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*_Do9Tq7MxFQAAAAAAAAAAAAADmJ7AQ/original" class="icon" width="30" height="30"/>
</g>
</g>
</g>
<g fill="none" x="250" y="375" transform="matrix(1,0,0,1,250,375)">
<g clip-path="url(#clip-path-14-11)">
<image fill="rgba(23,131,255,1)" preserveAspectRatio="none" x="-20" y="-20" href="https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*_Do9Tq7MxFQAAAAAAAAAAAAADmJ7AQ/original" class="key" stroke-width="0" stroke="rgba(0,0,0,1)" width="40" height="40"/>
</g>
<g fill="none" class="icon" width="40" height="40">
<g clip-path="url(#clip-path-15-13)">
<image fill="rgba(255,255,255,1)" preserveAspectRatio="none" x="-20" y="-20" href="https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*AzSISZeq81IAAAAAAAAAAAAADmJ7AQ/original" class="icon" width="40" height="40"/>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -13,4 +13,19 @@ declare global {
const content: string;
export default content;
}
export module '*.png' {
const content: string;
export default content;
}
export module '*.jpg' {
const content: string;
export default content;
}
export module '*.jpeg' {
const content: string;
export default content;
}
}

View File

@ -0,0 +1,11 @@
import { elementNodeAvatar } from '@@/demos';
import { createDemoGraph } from '@@/utils';
describe('element label oversized', () => {
it('render', async () => {
const graph = await createDemoGraph(elementNodeAvatar);
await expect(graph).toMatchSnapshot(__filename);
graph.destroy();
});
});

View File

@ -24,6 +24,7 @@ import { effect } from '../effect';
import type { BaseNodeStyleProps } from '../nodes';
import { BaseNode } from '../nodes';
import { Icon, IconStyleProps } from '../shapes';
import { connectImage, dispatchPositionChange } from '../shapes/image';
/**
* <zh/>
@ -156,6 +157,7 @@ export abstract class BaseCombo<S extends BaseComboStyleProps = BaseComboStylePr
@effect((self, attributes) => self.getCollapsedMarkerStyle(attributes))
protected drawCollapsedMarkerShape(attributes: Required<S>, container: Group): void {
this.upsert('collapsed-marker', Icon, this.getCollapsedMarkerStyle(attributes), container);
connectImage(this);
}
protected getCollapsedMarkerStyle(attributes: Required<S>): IconStyleProps | false {
@ -232,6 +234,7 @@ export abstract class BaseCombo<S extends BaseComboStyleProps = BaseComboStylePr
// Sync combo position to model
const { x, y } = comboStyle;
this.context.model.syncComboDatum({ id: this.id, style: { x, y } });
dispatchPositionChange(this);
}
public render(attributes: Required<S>, container: Group = this) {

View File

@ -30,6 +30,7 @@ import { BaseElement } from '../base-element';
import { effect } from '../effect';
import type { BadgeStyleProps, IconStyleProps, LabelStyleProps } from '../shapes';
import { Badge, Icon, Label } from '../shapes';
import { connectImage, dispatchPositionChange } from '../shapes/image';
/**
* <zh/>
@ -374,6 +375,7 @@ export abstract class BaseNode<S extends BaseNodeStyleProps = BaseNodeStyleProps
@effect((self, attributes) => self.getIconStyle(attributes))
protected drawIconShape(attributes: Required<S>, container: Group): void {
this.upsert('icon', Icon, this.getIconStyle(attributes), container);
connectImage(this);
}
@effect((self, attributes) => self.getBadgesStyle(attributes))
@ -427,6 +429,13 @@ export abstract class BaseNode<S extends BaseNodeStyleProps = BaseNodeStyleProps
this.drawPortShapes(attributes, container);
}
public update(attr?: Partial<S>): void {
super.update(attr);
if (attr && ('x' in attr || 'y' in attr || 'z' in attr)) {
dispatchPositionChange(this);
}
}
protected onframe() {
this.drawBadgeShapes(this.parsedAttributes, this);
this.drawLabelShape(this.parsedAttributes, this);
@ -434,9 +443,12 @@ export abstract class BaseNode<S extends BaseNodeStyleProps = BaseNodeStyleProps
}
/**
* <zh/>
*
* @param context
* @param shape
* <en/> Get the bounding box of the graphic in the off-screen canvas
* @param context - <zh/> <en/> Runtime context
* @param shape - <zh/> <en/> Graphic instance
* @returns <zh/> <en/> Graphic bounding box
*/
function getBoundsInOffscreen(context: RuntimeContext, shape: DisplayObject) {
if (!context) return shape.getLocalBounds();

View File

@ -1,10 +1,12 @@
import type { DisplayObjectConfig, RectStyleProps as GRectStyleProps, Group } from '@antv/g';
import { Image as GImage, ImageStyleProps as GImageStyleProps, Rect as GRect } from '@antv/g';
import { ImageStyleProps as GImageStyleProps, Rect as GRect } from '@antv/g';
import { ICON_SIZE_RATIO } from '../../constants/element';
import { subStyleProps } from '../../utils/prefix';
import { mergeOptions } from '../../utils/style';
import { add } from '../../utils/vector';
import type { IconStyleProps } from '../shapes';
import { Image as ImageShape } from '../shapes';
import { connectImage, dispatchPositionChange } from '../shapes/image';
import type { BaseNodeStyleProps } from './base-node';
import { BaseNode } from './base-node';
@ -79,11 +81,20 @@ export class Image extends BaseNode<ImageStyleProps> {
: false;
}
protected drawKeyShape(attributes: Required<ImageStyleProps>, container: Group): GImage | undefined {
return this.upsert('key', GImage, this.getKeyStyle(attributes), container);
protected drawKeyShape(attributes: Required<ImageStyleProps>, container: Group): ImageShape | undefined {
const image = this.upsert('key', ImageShape, this.getKeyStyle(attributes), container);
connectImage(this);
return image;
}
protected drawHaloShape(attributes: Required<ImageStyleProps>, container: Group): void {
this.upsert('halo', GRect, this.getHaloStyle(attributes), container);
}
public update(attr?: Partial<ImageStyleProps>): void {
super.update(attr);
if (attr && ('x' in attr || 'y' in attr || 'z' in attr)) {
dispatchPositionChange(this);
}
}
}

View File

@ -1,6 +1,8 @@
import { DisplayObjectConfig, Image as GImage, Text as GText, Group, ImageStyleProps, TextStyleProps } from '@antv/g';
import { DisplayObjectConfig, Text as GText, Group, TextStyleProps } from '@antv/g';
import type { BaseShapeStyleProps } from './base-shape';
import { BaseShape } from './base-shape';
import type { ImageStyleProps } from './image';
import { Image as GImage } from './image';
/**
* <zh/>

View File

@ -0,0 +1,88 @@
import type { DisplayObject, DisplayObjectConfig, ImageStyleProps as GImageStyleProps } from '@antv/g';
import { ElementEvent, Image as GImage, Rect as GRect } from '@antv/g';
import { getAncestorShapes } from '../../utils/shape';
export interface ImageStyleProps extends GImageStyleProps {
/**
* <zh/>
*
* <en/> Radius of the rounded corner
*/
radius?: number | number[];
}
export class Image extends GImage {
constructor(options: DisplayObjectConfig<ImageStyleProps>) {
super(options);
current = this;
this.isMutationObserved = true;
this.addEventListener(ElementEvent.MOUNTED, this.onMounted);
this.addEventListener(ElementEvent.ATTR_MODIFIED, this.onAttrModified);
}
private onMounted = () => {
this.handleRadius();
};
private onAttrModified = () => {
this.handleRadius();
};
public handleRadius() {
const { radius, clipPath, width = 0, height = 0 } = this.attributes as ImageStyleProps;
if (radius && width && height) {
const [x, y] = this.getBounds().min;
const clipPathStyle = { x, y, radius, width, height };
if (clipPath) {
Object.assign(this.parsedStyle.clipPath!.style, clipPathStyle);
} else {
const rect = new GRect({ style: clipPathStyle });
this.style.clipPath = rect;
}
} else {
if (clipPath) this.style.clipPath = null;
}
}
}
const ImagesWeakMap = new WeakMap<DisplayObject, Image[]>();
let current: Image | null = null;
/**
* <zh/> g clipPath Image clipPath
*
* connectImage dispatchPositionChange
*
* g
*
* <en/> Since g clipPath does not support relative positions, when the position of the affected element changes, the Image needs to be notified to update the clipPath.
*
* Use connectImage to create an association between the shape and the image, and combine it with the dispatchPositionChange method to trigger the update.
*
* This is a temporary, black-box solution, and if g supports relative positions in the future, this method will be removed.
* @param target - <zh/> <en/> Target element
*/
export const connectImage = (target: DisplayObject) => {
if (current && getAncestorShapes(current).includes(target)) {
const images = ImagesWeakMap.get(target);
if (images) {
if (!images.includes(current)) images.push(current);
} else ImagesWeakMap.set(target, [current]);
}
};
/**
* <zh/>
*
* <en/> Trigger the associated image to update its position
* @param target - <zh/> <en/> Target element
*/
export const dispatchPositionChange = (target: DisplayObject) => {
const image = ImagesWeakMap.get(target);
if (image) {
image.forEach((i) => i.handleRadius());
}
};

View File

@ -8,11 +8,13 @@ export { Badge } from './badge';
export { BaseShape } from './base-shape';
export { Contour } from './contour';
export { Icon } from './icon';
export { Image } from './image';
export { Label } from './label';
export type { BadgeStyleProps } from './badge';
export type { BaseShapeStyleProps } from './base-shape';
export type { ContourStyleProps } from './contour';
export type { IconStyleProps } from './icon';
export type { ImageStyleProps } from './image';
export type { LabelStyleProps } from './label';
export type { PolygonStyleProps } from './polygon';