From 5c26bd8de972a111566329f245a12016d0b87570 Mon Sep 17 00:00:00 2001 From: "yuqi.pyq" Date: Thu, 29 Sep 2022 11:03:44 +0800 Subject: [PATCH] feat: support applying CSS Transfrom on container #1161 --- packages/g-lite/src/Canvas.ts | 2 + .../src/css/parser/__tests__/color.spec.ts | 23 +++-- packages/g-lite/src/plugins/EventPlugin.ts | 30 +++++-- packages/g-lite/src/types.ts | 5 ++ packages/site/docs/api/canvas.en.md | 13 +++ packages/site/docs/api/canvas.zh.md | 11 +++ packages/site/examples/canvas/demo/meta.json | 7 ++ .../canvas/demo/supports-css-transform.js | 89 +++++++++++++++++++ 8 files changed, 164 insertions(+), 16 deletions(-) create mode 100644 packages/site/examples/canvas/demo/supports-css-transform.js diff --git a/packages/g-lite/src/Canvas.ts b/packages/g-lite/src/Canvas.ts index cf231a76f..4ee28d39f 100644 --- a/packages/g-lite/src/Canvas.ts +++ b/packages/g-lite/src/Canvas.ts @@ -157,6 +157,7 @@ export class Canvas extends EventTarget implements ICanvas { createImage, supportsPointerEvents, supportsTouchEvents, + supportsCSSTransform, isTouchEvent, isMouseEvent, } = config; @@ -212,6 +213,7 @@ export class Canvas extends EventTarget implements ICanvas { background: background || 'transparent', createImage, document, + supportsCSSTransform, }); this.initDefaultCamera(canvasWidth, canvasHeight); diff --git a/packages/g-lite/src/css/parser/__tests__/color.spec.ts b/packages/g-lite/src/css/parser/__tests__/color.spec.ts index 10ca246a5..01f1c9f78 100644 --- a/packages/g-lite/src/css/parser/__tests__/color.spec.ts +++ b/packages/g-lite/src/css/parser/__tests__/color.spec.ts @@ -239,15 +239,20 @@ describe('Property Color', () => { expect((result[1].value as RadialGradient).steps[2].offset.toString()).to.be.eqls('100%'); // TODO: multiple gradients, use 0 as 0% - result = parseColor(`radial-gradient(circle at 50% 0%, - rgba(255,0,0,.5), - rgba(255,0,0,0) 70.71%), - radial-gradient(circle at 6.7% 75%, - rgba(0,0,255,.5), - rgba(0,0,255,0) 70.71%), - radial-gradient(circle at 93.3% 75%, - rgba(0,255,0,.5), - rgba(0,255,0,0) 70.71%)`) as CSSGradientValue[]; + // result = parseColor(`radial-gradient(circle at 50% 0%, + // rgba(255,0,0,.5), + // rgba(255,0,0,0) 70.71%), + // radial-gradient(circle at 6.7% 75%, + // rgba(0,0,255,.5), + // rgba(0,0,255,0) 70.71%), + // radial-gradient(circle at 93.3% 75%, + // rgba(0,255,0,.5), + // rgba(0,255,0,0) 70.71%)`) as CSSGradientValue[]; + + // result = parseColor( + // `radial-gradient(circle 480px at 256px 496px, rgb(196, 217, 245) 0%, rgb(50, 80, 176) 50%, rgb(41, 47, 117) 100%)`, + // ); + // console.log(result); }); it('should parse legacy linear gradient color correctly', () => { diff --git a/packages/g-lite/src/plugins/EventPlugin.ts b/packages/g-lite/src/plugins/EventPlugin.ts index 414cd33a2..251321abb 100644 --- a/packages/g-lite/src/plugins/EventPlugin.ts +++ b/packages/g-lite/src/plugins/EventPlugin.ts @@ -1,5 +1,5 @@ import { inject, singleton } from 'mana-syringe'; -import { isUndefined } from '@antv/util'; +import { isNil, isUndefined } from '@antv/util'; import type { FederatedMouseEvent, ICanvas } from '../dom'; import { FederatedPointerEvent } from '../dom/FederatedPointerEvent'; import { FederatedWheelEvent } from '../dom/FederatedWheelEvent'; @@ -146,6 +146,26 @@ export class EventPlugin implements RenderingPlugin { this.setCursor(this.eventService.cursor); }; + private getViewportXY(nativeEvent: PointerEvent | WheelEvent) { + let x: number; + let y: number; + /** + * Should account for CSS Transform applied on container. + * @see https://github.com/antvis/G/issues/1161 + * @see https://developer.mozilla.org/zh-CN/docs/Web/API/MouseEvent/offsetX + */ + const { offsetX, offsetY, clientX, clientY } = nativeEvent; + if (this.canvasConfig.supportsCSSTransform && !isNil(offsetX) && !isNil(offsetY)) { + x = offsetX; + y = offsetY; + } else { + const point = this.eventService.client2Viewport(new Point(clientX, clientY)); + x = point.x; + y = point.y; + } + return { x, y }; + } + private bootstrapEvent( event: FederatedPointerEvent, nativeEvent: PointerEvent, @@ -167,9 +187,7 @@ export class EventPlugin implements RenderingPlugin { event.twist = nativeEvent.twist; this.transferMouseData(event, nativeEvent); - const { x, y } = this.eventService.client2Viewport( - new Point(nativeEvent.clientX, nativeEvent.clientY), - ); + const { x, y } = this.getViewportXY(nativeEvent); event.viewport.x = x; event.viewport.y = y; const { x: canvasX, y: canvasY } = this.eventService.viewport2Canvas(event.viewport); @@ -201,9 +219,7 @@ export class EventPlugin implements RenderingPlugin { event.deltaY = nativeEvent.deltaY; event.deltaZ = nativeEvent.deltaZ; - const { x, y } = this.eventService.client2Viewport( - new Point(nativeEvent.clientX, nativeEvent.clientY), - ); + const { x, y } = this.getViewportXY(nativeEvent); event.viewport.x = x; event.viewport.y = y; const { x: canvasX, y: canvasY } = this.eventService.viewport2Canvas(event.viewport); diff --git a/packages/g-lite/src/types.ts b/packages/g-lite/src/types.ts index 2ce2b816b..7aee9d37a 100644 --- a/packages/g-lite/src/types.ts +++ b/packages/g-lite/src/types.ts @@ -435,6 +435,11 @@ export interface CanvasConfig { isTouchEvent?: (event: InteractivePointerEvent) => event is TouchEvent; isMouseEvent?: (event: InteractivePointerEvent) => event is MouseEvent; + /** + * Should we account for CSS Transform applied on container? + */ + supportsCSSTransform?: boolean; + /** * 画布宽度 */ diff --git a/packages/site/docs/api/canvas.en.md b/packages/site/docs/api/canvas.en.md index 7aa3d8bde..e6a724323 100644 --- a/packages/site/docs/api/canvas.en.md +++ b/packages/site/docs/api/canvas.en.md @@ -183,6 +183,19 @@ const canvas = new Canvas({ }); ``` +## supportsCSSTransform + +Optional. 是否支持在容器上应用 CSS Transform 的情况下确保交互事件坐标转换正确。 + +Whether or not CSS Transform is supported on the container to ensure that the interaction event coordinates are transformed correctly. + +In this [example](/en/examples/canvas#supports-css-transform), we have enlarged the container by a factor of 1.1, and with this configuration enabled, mouse movement over the circle changes the mouse style correctly. + +```js +const $wrapper = document.getElementById('container'); +$wrapper.style.transform = 'scale(1.1)'; +``` + ## supportsPointerEvents Optional. Whether PointerEvent is supported or not, the default will use `! !globalThis.PointerEvent`. If `false` is passed, the event listener plugin will not listen for PointerEvent such as `pointerdown`. diff --git a/packages/site/docs/api/canvas.zh.md b/packages/site/docs/api/canvas.zh.md index 46757ad6f..b094fa1eb 100644 --- a/packages/site/docs/api/canvas.zh.md +++ b/packages/site/docs/api/canvas.zh.md @@ -187,6 +187,17 @@ const canvas = new Canvas({ }); ``` +## supportsCSSTransform + +可选。是否支持在容器上应用 CSS Transform 的情况下确保交互事件坐标转换正确。 + +在该 [示例](/zh/examples/canvas#supports-css-transform) 中,我们将容器放大了 1.1 倍,开启该配置项后,鼠标移动到圆上可以正确变化鼠标样式: + +```js +const $wrapper = document.getElementById('container'); +$wrapper.style.transform = 'scale(1.1)'; +``` + ## supportsPointerEvents 可选。是否支持 PointerEvent,默认将使用 `!!globalThis.PointerEvent` 判断。如果传入 `false`,事件监听插件将不会监听例如 `pointerdown` 等 PointerEvent。 diff --git a/packages/site/examples/canvas/demo/meta.json b/packages/site/examples/canvas/demo/meta.json index b7770db85..4aa679850 100644 --- a/packages/site/examples/canvas/demo/meta.json +++ b/packages/site/examples/canvas/demo/meta.json @@ -63,6 +63,13 @@ "zh": "通过 API 方式完成拾取", "en": "Use picking API" } + }, + { + "filename": "supports-css-transform.js", + "title": { + "zh": "支持在容器上应用 CSS Transform", + "en": "Support applying CSS Transform on container" + } } ] } diff --git a/packages/site/examples/canvas/demo/supports-css-transform.js b/packages/site/examples/canvas/demo/supports-css-transform.js new file mode 100644 index 000000000..fd302dbf7 --- /dev/null +++ b/packages/site/examples/canvas/demo/supports-css-transform.js @@ -0,0 +1,89 @@ +import { Canvas, CanvasEvent, Circle } from '@antv/g'; +import { Renderer as CanvasRenderer } from '@antv/g-canvas'; +import { Renderer as CanvaskitRenderer } from '@antv/g-canvaskit'; +import { Renderer as SVGRenderer } from '@antv/g-svg'; +import { Renderer as WebGLRenderer } from '@antv/g-webgl'; +import { Renderer as WebGPURenderer } from '@antv/g-webgpu'; +import * as lil from 'lil-gui'; +import Stats from 'stats.js'; + +const $wrapper = document.getElementById('container'); +$wrapper.style.transform = 'scale(1.1)'; + +// create a renderer +const canvasRenderer = new CanvasRenderer(); +const svgRenderer = new SVGRenderer(); +const webglRenderer = new WebGLRenderer(); +const webgpuRenderer = new WebGPURenderer(); +const canvaskitRenderer = new CanvaskitRenderer({ + wasmDir: '/', +}); + +// create a canvas +const canvas = new Canvas({ + container: 'container', + width: 600, + height: 500, + renderer: canvasRenderer, + supportsCSSTransform: true, +}); + +// create a circle +const circle = new Circle({ + style: { + cx: 300, + cy: 200, + r: 100, + fill: '#1890FF', + stroke: '#F04864', + lineWidth: 4, + shadowColor: 'black', + shadowBlur: 20, + cursor: 'pointer', + }, +}); + +canvas.addEventListener(CanvasEvent.READY, () => { + // add a circle to canvas + canvas.appendChild(circle); +}); + +// stats +const stats = new Stats(); +stats.showPanel(0); +const $stats = stats.dom; +$stats.style.position = 'absolute'; +$stats.style.left = '0px'; +$stats.style.top = '0px'; +$wrapper.appendChild($stats); +canvas.addEventListener(CanvasEvent.AFTER_RENDER, () => { + if (stats) { + stats.update(); + } +}); + +// GUI +const gui = new lil.GUI({ autoPlace: false }); +$wrapper.appendChild(gui.domElement); +const rendererFolder = gui.addFolder('renderer'); +const rendererConfig = { + renderer: 'canvas', +}; +rendererFolder + .add(rendererConfig, 'renderer', ['canvas', 'svg', 'webgl', 'webgpu', 'canvaskit']) + .onChange((rendererName) => { + let renderer; + if (rendererName === 'canvas') { + renderer = canvasRenderer; + } else if (rendererName === 'svg') { + renderer = svgRenderer; + } else if (rendererName === 'webgl') { + renderer = webglRenderer; + } else if (rendererName === 'webgpu') { + renderer = webgpuRenderer; + } else if (rendererName === 'canvaskit') { + renderer = canvaskitRenderer; + } + canvas.setRenderer(renderer); + }); +rendererFolder.open();