mirror of
https://gitee.com/antv/g6.git
synced 2024-12-01 19:28:39 +08:00
feat(elements): add image shape support radius, adpat elements
This commit is contained in:
parent
9c5b855816
commit
ab3f3e2644
32
packages/g6/__tests__/demos/element-node-avatar.ts
Normal file
32
packages/g6/__tests__/demos/element-node-avatar.ts
Normal 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;
|
||||
};
|
@ -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';
|
||||
|
@ -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 |
15
packages/g6/__tests__/types.d.ts
vendored
15
packages/g6/__tests__/types.d.ts
vendored
@ -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;
|
||||
}
|
||||
}
|
||||
|
11
packages/g6/__tests__/unit/elements/nodes/avatar.spec.ts
Normal file
11
packages/g6/__tests__/unit/elements/nodes/avatar.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
@ -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) {
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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/> 图标样式
|
||||
|
88
packages/g6/src/elements/shapes/image.ts
Normal file
88
packages/g6/src/elements/shapes/image.ts
Normal 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());
|
||||
}
|
||||
};
|
@ -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';
|
||||
|
Loading…
Reference in New Issue
Block a user