From cdb3a6d49b9cdeb60a94f33587d24eebeb7ae2b7 Mon Sep 17 00:00:00 2001 From: Toni Date: Tue, 20 Feb 2024 11:48:29 +0100 Subject: [PATCH] New: Add support for handling device pixel ratio (#45) * Chore: Move container and canvas creation from the view to the renderer * New: Add devicePixelRatio render property and handler * Fix: Add default DPR for older browsers * Chore: Remove useless check for automatic DPR --- docs/view-default.md | 35 +++++--- docs/view-map.md | 31 +++++-- src/renderer/canvas/canvas-renderer.ts | 117 +++++++++++++++++++++---- src/renderer/factory.ts | 16 +--- src/renderer/shared.ts | 20 +++-- src/renderer/webgl/webgl-renderer.ts | 49 +++++++++-- src/utils/html.utils.ts | 35 ++++++++ src/views/orb-map-view.ts | 55 ++++-------- src/views/orb-view.ts | 58 ++++-------- 9 files changed, 272 insertions(+), 144 deletions(-) diff --git a/docs/view-default.md b/docs/view-default.md index 598dae6..12ea901 100644 --- a/docs/view-default.md +++ b/docs/view-default.md @@ -63,6 +63,7 @@ interface IOrbViewSettings { }; // For canvas rendering and events render: { + devicePixelRatio: number | null; fps: number; minZoom: number; maxZoom: number; @@ -74,6 +75,7 @@ interface IOrbViewSettings { contextAlphaOnEvent: number; contextAlphaOnEventIsEnabled: boolean; backgroundColor: Color | string | null; + areCollapsedContainerDimensionsAllowed: boolean; }; // For select and hover look-and-feel strategy: { @@ -90,7 +92,6 @@ interface IOrbViewSettings { isOutOfBoundsDragEnabled: boolean; areCoordinatesRounded: boolean; isSimulationAnimated: boolean; - areCollapsedContainerDimensionsAllowed: boolean; } ``` @@ -138,6 +139,7 @@ const defaultSettings = { }, }, render: { + devicePixelRatio: window.devicePixelRatio, fps: 60, minZoom: 0.25, maxZoom: 8, @@ -149,6 +151,7 @@ const defaultSettings = { contextAlphaOnEvent: 0.3, contextAlphaOnEventIsEnabled: true, backgroundColor: null, + areCollapsedContainerDimensionsAllowed: false, }, strategy: { isDefaultSelectEnabled: true, @@ -162,7 +165,6 @@ const defaultSettings = { isOutOfBoundsDragEnabled: false, areCoordinatesRounded: true, isSimulationAnimated: true, - areCollapsedContainerDimensionsAllowed: false; } ``` @@ -269,6 +271,26 @@ Here you can use your original properties to indicate which ones represent your Optional property `render` has several rendering options that you can tweak. Read more about them on [Styling guide](./styles.md). +#### Property `render.devicePixelRatio` + +`devicePixelRatio` is useful when dealing with the difference between rendering on a standard +display versus a HiDPI or Retina display, which uses more screen pixels to draw the same +objects, resulting in a sharper image. ([Reference: MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio)). +Orb will listen for `devicePixelRatio` changes and handles them by default. You can override the +value with a settings property `render.devicePixelRatio`. Once a custom value is provided, Orb will +stop listening for `devicePixelRatio` changes. +If you want to return automatic `devicePixelRatio` handling, just set `render.devicePixelRatio` +to `null`. + +#### Property `render.areCollapsedContainerDimensionsAllowed` + +Enables setting the dimensions of the Orb container element to zero. +If the container element of Orb has collapsed dimensions (`width: 0;` or `height: 0;`), +Orb will expand the container by setting the values to `100%`. +If that doesn't work (the parent of the container also has collapsed dimensions), +Orb will set an arbitrary fixed dimension to the container. +Disabled by default (`false`). + ### Property `strategy` The optional property `strategy` has two properties that you can enable/disable: @@ -362,15 +384,6 @@ Shows the process of simulation where the nodes are moved by the physics engine converge to a stable position. If disabled, the graph will suddenly appear in its final position. Enabled by default (`true`). -### Property `areCollapsedContainerDimensionsAllowed` - -Enables setting the dimensions of the Orb container element to zero. -If the container element of Orb has collapsed dimensions (`width: 0;` or `height: 0;`), -Orb will expand the container by setting the values to `100%`. -If that doesn't work (the parent of the container also has collapsed dimensions), -Orb will set an arbitrary fixed dimension to the container. -Disabled by default (`false`). - ## Settings The above settings of the `OrbView` can be defined on view initialization, but also anytime diff --git a/docs/view-map.md b/docs/view-map.md index f5dc6c9..26c1cb8 100644 --- a/docs/view-map.md +++ b/docs/view-map.md @@ -116,6 +116,7 @@ interface IOrbMapViewSettings { getGeoPosition(node: INode): { lat: number; lng: number } | undefined; // For canvas rendering and events render: { + devicePixelRatio: number | null; fps: number; minZoom: number; maxZoom: number; @@ -147,6 +148,7 @@ The default settings that `OrbMapView` uses is: ```typescript const defaultSettings = { render: { + devicePixelRatio: window.devicePixelRatio, fps: 60, minZoom: 0.25, maxZoom: 8, @@ -191,6 +193,26 @@ Optional property `map` has two properties that you can set which are: Optional property `render` has several rendering options that you can tweak. Read more about them on [Styling guide](./styles.md). +#### Property `render.devicePixelRatio` + +`devicePixelRatio` is useful when dealing with the difference between rendering on a standard +display versus a HiDPI or Retina display, which uses more screen pixels to draw the same +objects, resulting in a sharper image. ([Reference: MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio)). +Orb will listen for `devicePixelRatio` changes and handles them by default. You can override the +value with a settings property `render.devicePixelRatio`. Once a custom value is provided, Orb will +stop listening for `devicePixelRatio` changes. +If you want to return automatic `devicePixelRatio` handling, just set `render.devicePixelRatio` +to `null`. + +#### Property `render.areCollapsedContainerDimensionsAllowed` + +Enables setting the dimensions of the Orb container element to zero. +If the container element of Orb has collapsed dimensions (`width: 0;` or `height: 0;`), +Orb will expand the container by setting the values to `100%`. +If that doesn't work (the parent of the container also has collapsed dimensions), +Orb will set an arbitrary fixed dimension to the container. +Disabled by default (`false`). + ### Property `strategy` The optional property `strategy` has two properties that you can enable/disable: @@ -243,15 +265,6 @@ orb.events.on(OrbEventType.MOUSE_CLICK, (event) => { }); ``` -### Property `areCollapsedContainerDimensionsAllowed` - -Enables setting the dimensions of the Orb container element to zero. -If the container element of Orb has collapsed dimensions (`width: 0;` or `height: 0;`), -Orb will expand the container by setting the values to `100%`. -If that doesn't work (the parent of the container also has collapsed dimensions), -Orb will set an arbitrary fixed dimension to the container. -Disabled by default (`false`). - ## Settings The above settings of `OrbMapView` can be defined on view initialization, but also anytime after the diff --git a/src/renderer/canvas/canvas-renderer.ts b/src/renderer/canvas/canvas-renderer.ts index 0625c9b..9265d93 100644 --- a/src/renderer/canvas/canvas-renderer.ts +++ b/src/renderer/canvas/canvas-renderer.ts @@ -18,6 +18,14 @@ import { import { throttle } from '../../utils/function.utils'; import { getThrottleMsFromFPS } from '../../utils/math.utils'; import { copyObject } from '../../utils/object.utils'; +import { + appendCanvas, + IObserveDPRUnsubscribe, + observeDevicePixelRatioChanges, + setupContainer, +} from '../../utils/html.utils'; +import { OrbError } from '../../exceptions'; +import { isNumber } from '../../utils/type.utils'; const DEBUG = false; const DEBUG_RED = '#FF5733'; @@ -26,12 +34,16 @@ const DEBUG_BLUE = '#3383FF'; const DEBUG_PINK = '#F333FF'; export class CanvasRenderer extends Emitter implements IRenderer { + private readonly _container: HTMLElement; + private readonly _canvas: HTMLCanvasElement; + private _resizeObs: ResizeObserver; + // Contains the HTML5 Canvas element which is used for drawing nodes and edges. private readonly _context: CanvasRenderingContext2D; // Width and height of the canvas. Used for clearing - public width: number; - public height: number; + private _width: number; + private _height: number; private _settings: IRendererSettings; // Includes translation (pan) in the x and y direction @@ -45,23 +57,58 @@ export class CanvasRenderer extends Em private _isInitiallyRendered = false; private _throttleRender: (graph: IGraph) => void; + private _dprObserveUnsubscribe?: IObserveDPRUnsubscribe; - constructor(context: CanvasRenderingContext2D, settings?: Partial) { + constructor(container: HTMLElement, settings?: Partial) { super(); + setupContainer(container, settings?.areCollapsedContainerDimensionsAllowed); + this._container = container; + this._canvas = appendCanvas(container); + + const context = this._canvas.getContext('2d'); + if (!context) { + throw new OrbError('Failed to create Canvas context.'); + } + this._context = context; - this.width = DEFAULT_RENDERER_WIDTH; - this.height = DEFAULT_RENDERER_HEIGHT; + this._width = DEFAULT_RENDERER_WIDTH; + this._height = DEFAULT_RENDERER_HEIGHT; this.transform = zoomIdentity; this._settings = { ...DEFAULT_RENDERER_SETTINGS, ...settings, }; + // Resize the canvas based on the dimensions of its parent container
. + this._resizeObs = new ResizeObserver(() => this._resize()); + this._resizeObs.observe(this._container); + this._resize(); + + if (!isNumber(settings?.devicePixelRatio)) { + this._dprObserveUnsubscribe = observeDevicePixelRatioChanges(() => this._resize()); + } + this._throttleRender = throttle((graph: IGraph) => { this._render(graph); }, getThrottleMsFromFPS(this._settings.fps)); } + get width(): number { + return this._width; + } + + get height(): number { + return this._height; + } + + get container(): HTMLElement { + return this._container; + } + + get canvas(): HTMLCanvasElement { + return this._canvas; + } + get isInitiallyRendered(): boolean { return this._isInitiallyRendered; } @@ -72,6 +119,9 @@ export class CanvasRenderer extends Em setSettings(settings: Partial) { const isFpsChanged = settings.fps && settings.fps !== this._settings.fps; + const previousDprValue = this._settings.devicePixelRatio; + const newDprValue = settings.devicePixelRatio; + this._settings = { ...this._settings, ...settings, @@ -82,6 +132,17 @@ export class CanvasRenderer extends Em this._render(graph); }, getThrottleMsFromFPS(this._settings.fps)); } + + // Change DPR from automatic to manual handling or change DPR value manually + if (!isNumber(previousDprValue) && isNumber(newDprValue)) { + this._dprObserveUnsubscribe?.(); + this._resize(); + } + + // Change DPR from manual to automatic handling + if (isNumber(previousDprValue) && newDprValue === null) { + this._dprObserveUnsubscribe = observeDevicePixelRatioChanges(() => this._resize()); + } } render(graph: IGraph) { @@ -97,30 +158,30 @@ export class CanvasRenderer extends Em const renderStartedAt = Date.now(); // Clear drawing. - this._context.clearRect(0, 0, this.width, this.height); + this._context.clearRect(0, 0, this._width, this._height); if (this._settings.backgroundColor) { this._context.fillStyle = this._settings.backgroundColor.toString(); - this._context.fillRect(0, 0, this.width, this.height); + this._context.fillRect(0, 0, this._width, this._height); } this._context.save(); if (DEBUG) { this._context.lineWidth = 3; this._context.fillStyle = DEBUG_RED; - this._context.fillRect(0, 0, this.width, this.height); + this._context.fillRect(0, 0, this._width, this._height); } // Apply any scaling (zoom) or translation (pan) transformations. this._context.translate(this.transform.x, this.transform.y); if (DEBUG) { this._context.fillStyle = DEBUG_BLUE; - this._context.fillRect(0, 0, this.width, this.height); + this._context.fillRect(0, 0, this._width, this._height); } this._context.scale(this.transform.k, this.transform.k); if (DEBUG) { this._context.fillStyle = DEBUG_GREEN; - this._context.fillRect(0, 0, this.width, this.height); + this._context.fillRect(0, 0, this._width, this._height); } // Move coordinates (0, 0) to canvas center. @@ -129,11 +190,11 @@ export class CanvasRenderer extends Em // relative to (0, 0), so any source mouse event position needs to take this // offset into account. (Handled in getMousePos()) if (this._isOriginCentered) { - this._context.translate(this.width / 2, this.height / 2); + this._context.translate(this._width / 2, this._height / 2); } if (DEBUG) { this._context.fillStyle = DEBUG_PINK; - this._context.fillRect(0, 0, this.width, this.height); + this._context.fillRect(0, 0, this._width, this._height); } this.drawObjects(graph.getEdges()); @@ -195,6 +256,23 @@ export class CanvasRenderer extends Em } } + private _resize() { + const dpr = this._settings.devicePixelRatio || window.devicePixelRatio || 1; + + const containerSize = this._container.getBoundingClientRect(); + this._canvas.style.width = `${containerSize.width}px`; + this._canvas.style.height = `${containerSize.height}px`; + this._canvas.width = containerSize.width * dpr; + this._canvas.height = containerSize.height * dpr; + + // Normalize coordinate system to use CSS pixels + this._context.scale(dpr, dpr); + + this._width = containerSize.width; + this._height = containerSize.height; + this.emit(RenderEventType.RESIZE, undefined); + } + private drawObject(obj: INode | IEdge, options?: Partial | Partial) { if (isNode(obj)) { drawNode(this._context, obj, options); @@ -207,7 +285,7 @@ export class CanvasRenderer extends Em this.transform = zoomIdentity; // Clear drawing. - this._context.clearRect(0, 0, this.width, this.height); + this._context.clearRect(0, 0, this._width, this._height); this._context.save(); } @@ -252,8 +330,8 @@ export class CanvasRenderer extends Em // simulation coordinates (O) when dragging and hovering nodes. const [x, y] = this.transform.invert([canvasPoint.x, canvasPoint.y]); return { - x: x - this.width / 2, - y: y - this.height / 2, + x: x - this._width / 2, + y: y - this._height / 2, }; } @@ -264,7 +342,7 @@ export class CanvasRenderer extends Em */ getSimulationViewRectangle(): IRectangle { const topLeftPosition = this.getSimulationPosition({ x: 0, y: 0 }); - const bottomRightPosition = this.getSimulationPosition({ x: this.width, y: this.height }); + const bottomRightPosition = this.getSimulationPosition({ x: this._width, y: this._height }); return { x: topLeftPosition.x, y: topLeftPosition.y, @@ -276,4 +354,11 @@ export class CanvasRenderer extends Em translateOriginToCenter() { this._isOriginCentered = true; } + + destroy(): void { + this._resizeObs.unobserve(this._container); + this._dprObserveUnsubscribe?.(); + this.removeAllListeners(); + this._canvas.outerHTML = ''; + } } diff --git a/src/renderer/factory.ts b/src/renderer/factory.ts index 9cc76a9..4fd245c 100644 --- a/src/renderer/factory.ts +++ b/src/renderer/factory.ts @@ -1,28 +1,18 @@ import { CanvasRenderer } from './canvas/canvas-renderer'; import { IRenderer, IRendererSettings, RendererType } from './shared'; import { WebGLRenderer } from './webgl/webgl-renderer'; -import { OrbError } from '../exceptions'; import { INodeBase } from '../models/node'; import { IEdgeBase } from '../models/edge'; export class RendererFactory { static getRenderer( - canvas: HTMLCanvasElement, + container: HTMLElement, type: RendererType = RendererType.CANVAS, settings?: Partial, ): IRenderer { if (type === RendererType.WEBGL) { - const context = canvas.getContext('webgl2'); - if (!context) { - throw new OrbError('Failed to create WebGL context.'); - } - return new WebGLRenderer(context, settings); + return new WebGLRenderer(container, settings); } - - const context = canvas.getContext('2d'); - if (!context) { - throw new OrbError('Failed to create Canvas context.'); - } - return new CanvasRenderer(context, settings); + return new CanvasRenderer(container, settings); } } diff --git a/src/renderer/shared.ts b/src/renderer/shared.ts index fe945b0..a5719bc 100644 --- a/src/renderer/shared.ts +++ b/src/renderer/shared.ts @@ -11,11 +11,13 @@ export enum RendererType { } export enum RenderEventType { + RESIZE = 'resize', RENDER_START = 'render-start', RENDER_END = 'render-end', } export interface IRendererSettings { + devicePixelRatio: number | null; fps: number; minZoom: number; maxZoom: number; @@ -27,6 +29,7 @@ export interface IRendererSettings { contextAlphaOnEvent: number; contextAlphaOnEventIsEnabled: boolean; backgroundColor: Color | string | null; + areCollapsedContainerDimensionsAllowed: boolean; } export interface IRendererSettingsInit extends IRendererSettings { @@ -34,31 +37,30 @@ export interface IRendererSettingsInit extends IRendererSettings { } export type RendererEvents = { + [RenderEventType.RESIZE]: undefined; [RenderEventType.RENDER_START]: undefined; [RenderEventType.RENDER_END]: { durationMs: number }; }; export interface IRenderer extends IEmitter { - // Width and height of the canvas. Used for clearing. - width: number; - height: number; - // Includes translation (pan) in the x and y direction // as well as scaling (level of zoom). transform: ZoomTransform; + // Width and height of the canvas + get width(): number; + get height(): number; + get container(): HTMLElement; + get canvas(): HTMLCanvasElement; get isInitiallyRendered(): boolean; getSettings(): IRendererSettings; - setSettings(settings: Partial): void; render(graph: IGraph): void; - reset(): void; getFitZoomTransform(graph: IGraph): ZoomTransform; - getSimulationPosition(canvasPoint: IPosition): IPosition; /** @@ -69,9 +71,12 @@ export interface IRenderer extends IEm getSimulationViewRectangle(): IRectangle; translateOriginToCenter(): void; + + destroy(): void; } export const DEFAULT_RENDERER_SETTINGS: IRendererSettings = { + devicePixelRatio: null, fps: 60, minZoom: 0.25, maxZoom: 8, @@ -83,6 +88,7 @@ export const DEFAULT_RENDERER_SETTINGS: IRendererSettings = { contextAlphaOnEvent: 0.3, contextAlphaOnEventIsEnabled: true, backgroundColor: null, + areCollapsedContainerDimensionsAllowed: false, }; export const DEFAULT_RENDERER_WIDTH = 640; diff --git a/src/renderer/webgl/webgl-renderer.ts b/src/renderer/webgl/webgl-renderer.ts index 9f7115f..52ba3a1 100644 --- a/src/renderer/webgl/webgl-renderer.ts +++ b/src/renderer/webgl/webgl-renderer.ts @@ -13,28 +13,58 @@ import { IRendererSettings, } from '../shared'; import { copyObject } from '../../utils/object.utils'; +import { appendCanvas, setupContainer } from '../../utils/html.utils'; +import { OrbError } from '../../exceptions'; export class WebGLRenderer extends Emitter implements IRenderer { + private readonly _container: HTMLElement; + private readonly _canvas: HTMLCanvasElement; + // Contains the HTML5 Canvas element which is used for drawing nodes and edges. private readonly _context: WebGL2RenderingContext; - width: number; - height: number; + private _width: number; + private _height: number; private _settings: IRendererSettings; transform: ZoomTransform; - constructor(context: WebGL2RenderingContext, settings?: Partial) { + constructor(container: HTMLElement, settings?: Partial) { super(); - this._context = context; - console.log('context', this._context); + setupContainer(container, settings?.areCollapsedContainerDimensionsAllowed); + this._container = container; + this._canvas = appendCanvas(container); + const context = this._canvas.getContext('webgl2'); - this.width = DEFAULT_RENDERER_WIDTH; - this.height = DEFAULT_RENDERER_HEIGHT; + if (!context) { + throw new OrbError('Failed to create WebGL context.'); + } + + this._context = context; + this._width = DEFAULT_RENDERER_WIDTH; + this._height = DEFAULT_RENDERER_HEIGHT; this.transform = zoomIdentity; this._settings = { ...DEFAULT_RENDERER_SETTINGS, ...settings, }; + + console.log('context', this._context); + } + + get width(): number { + return this._width; + } + + get height(): number { + return this._height; + } + + get container(): HTMLElement { + return this._container; + } + + get canvas(): HTMLCanvasElement { + return this._canvas; } get isInitiallyRendered(): boolean { @@ -78,4 +108,9 @@ export class WebGLRenderer extends Emi translateOriginToCenter(): void { throw new Error('Method not implemented.'); } + + destroy(): void { + this.removeAllListeners(); + this._canvas.outerHTML = ''; + } } diff --git a/src/utils/html.utils.ts b/src/utils/html.utils.ts index 0b02415..deef5b1 100644 --- a/src/utils/html.utils.ts +++ b/src/utils/html.utils.ts @@ -48,3 +48,38 @@ export const isCollapsedDimension = (dimension: string | null | undefined) => { return collapsedDimensionRegex.test(dimension); }; + +export const appendCanvas = (container: HTMLElement): HTMLCanvasElement => { + const canvas = document.createElement('canvas'); + canvas.style.position = 'absolute'; + canvas.style.top = '0'; + canvas.style.left = '0'; + container.appendChild(canvas); + return canvas; +}; + +export type IObserveDPRCallback = (devicePixelRatio: number) => void; +export type IObserveDPRUnsubscribe = () => void; + +export const observeDevicePixelRatioChanges = (callback: IObserveDPRCallback): IObserveDPRUnsubscribe => { + let currentDpr = window.devicePixelRatio; + let unsubscribe: IObserveDPRUnsubscribe = () => { + return; + }; + + const listenForDPRChanges = () => { + unsubscribe(); + + const media = matchMedia(`(resolution: ${currentDpr}dppx)`); + media.addEventListener('change', listenForDPRChanges); + unsubscribe = () => media.removeEventListener('change', listenForDPRChanges); + + if (window.devicePixelRatio !== currentDpr) { + currentDpr = window.devicePixelRatio; + callback(currentDpr); + } + }; + + listenForDPRChanges(); + return () => unsubscribe(); +}; diff --git a/src/views/orb-map-view.ts b/src/views/orb-map-view.ts index 83f994a..01211ab 100644 --- a/src/views/orb-map-view.ts +++ b/src/views/orb-map-view.ts @@ -9,7 +9,6 @@ import { copyObject } from '../utils/object.utils'; import { OrbEmitter, OrbEventType } from '../events'; import { IRenderer, RendererType, RenderEventType, IRendererSettingsInit, IRendererSettings } from '../renderer/shared'; import { RendererFactory } from '../renderer/factory'; -import { setupContainer } from '../utils/html.utils'; import { getDefaultGraphStyle } from '../models/style'; import { isBoolean } from '../utils/type.utils'; @@ -67,14 +66,11 @@ export type IOrbMapViewSettingsUpdate export class OrbMapView implements IOrbView> { private _container: HTMLElement; - private _resizeObs: ResizeObserver; private _graph: IGraph; private _events: OrbEmitter; private _strategy: IEventStrategy; private _settings: IOrbMapViewSettings; - - private _canvas: HTMLCanvasElement; private _map: HTMLDivElement; private readonly _renderer: IRenderer; @@ -116,12 +112,12 @@ export class OrbMapView implements IOr isDefaultHoverEnabled: this._settings.strategy.isDefaultHoverEnabled ?? false, }); - setupContainer(this._container); - this._canvas = this._initCanvas(); - this._map = this._initMap(); - try { - this._renderer = RendererFactory.getRenderer(this._canvas, settings?.render?.type, this._settings.render); + this._renderer = RendererFactory.getRenderer( + this._container, + settings?.render?.type, + this._settings.render, + ); } catch (error: any) { this._container.textContent = error.message; throw error; @@ -129,12 +125,18 @@ export class OrbMapView implements IOr this._renderer.on(RenderEventType.RENDER_END, (data) => { this._events.emit(OrbEventType.RENDER_END, data); }); + this._renderer.on(RenderEventType.RESIZE, () => { + if (this._renderer.isInitiallyRendered) { + this._leaflet.invalidateSize(false); + this._renderer.render(this._graph); + } + }); + this._settings.render = this._renderer.getSettings(); + this._renderer.canvas.style.zIndex = '2'; + this._renderer.canvas.style.pointerEvents = 'none'; - // Resize the canvas based on the dimensions of it's parent container
. - this._resizeObs = new ResizeObserver(() => this._handleResize()); - this._resizeObs.observe(this._container); - this._handleResize(); + this._map = this._initMap(); this._leaflet = this._initLeaflet(); // Setting up leaflet map tile @@ -208,23 +210,10 @@ export class OrbMapView implements IOr } destroy() { - this._resizeObs.unobserve(this._container); - this._renderer.removeAllListeners(); + this._renderer.destroy(); this._leaflet.off(); this._leaflet.remove(); this._leaflet.getContainer().outerHTML = ''; - this._canvas.outerHTML = ''; - } - - private _initCanvas() { - const canvas = document.createElement('canvas'); - canvas.style.position = 'absolute'; - canvas.style.width = '100%'; - canvas.style.zIndex = '2'; - canvas.style.pointerEvents = 'none'; - - this._container.appendChild(canvas); - return canvas; } private _initMap() { @@ -442,18 +431,6 @@ export class OrbMapView implements IOr } } - private _handleResize() { - const containerSize = this._container.getBoundingClientRect(); - this._canvas.width = containerSize.width; - this._canvas.height = containerSize.height; - this._renderer.width = containerSize.width; - this._renderer.height = containerSize.height; - if (this._renderer.isInitiallyRendered) { - this._leaflet.invalidateSize(false); - this._renderer.render(this._graph); - } - } - private _handleTileChange() { const newTile: ILeafletMapTile = this._settings.map.tile; diff --git a/src/views/orb-view.ts b/src/views/orb-view.ts index 932cb6a..d4832b6 100644 --- a/src/views/orb-view.ts +++ b/src/views/orb-view.ts @@ -18,7 +18,6 @@ import { copyObject } from '../utils/object.utils'; import { OrbEmitter, OrbEventType } from '../events'; import { IRenderer, RenderEventType, IRendererSettingsInit, IRendererSettings } from '../renderer/shared'; import { RendererFactory } from '../renderer/factory'; -import { setupContainer } from '../utils/html.utils'; import { SimulatorEventType } from '../simulator/shared'; import { getDefaultGraphStyle } from '../models/style'; import { isBoolean } from '../utils/type.utils'; @@ -38,7 +37,6 @@ export interface IOrbViewSettings { isOutOfBoundsDragEnabled: boolean; areCoordinatesRounded: boolean; isSimulationAnimated: boolean; - areCollapsedContainerDimensionsAllowed: boolean; } export type IOrbViewSettingsInit = Omit< @@ -48,12 +46,10 @@ export type IOrbViewSettingsInit = Omi export class OrbView implements IOrbView> { private _container: HTMLElement; - private _resizeObs: ResizeObserver; private _graph: IGraph; private _events: OrbEmitter; private _strategy: IEventStrategy; private _settings: IOrbViewSettings; - private _canvas: HTMLCanvasElement; private readonly _renderer: IRenderer; private readonly _simulator: ISimulator; @@ -83,7 +79,6 @@ export class OrbView implements IOrbVi isOutOfBoundsDragEnabled: false, areCoordinatesRounded: true, isSimulationAnimated: true, - areCollapsedContainerDimensionsAllowed: false, ...settings, simulation: { isPhysicsEnabled: false, @@ -109,11 +104,12 @@ export class OrbView implements IOrbVi isDefaultHoverEnabled: this._settings.strategy.isDefaultHoverEnabled ?? false, }); - setupContainer(this._container, this._settings.areCollapsedContainerDimensionsAllowed); - this._canvas = this._initCanvas(); - try { - this._renderer = RendererFactory.getRenderer(this._canvas, settings?.render?.type, this._settings.render); + this._renderer = RendererFactory.getRenderer( + this._container, + settings?.render?.type, + this._settings.render, + ); } catch (error: any) { this._container.textContent = error.message; throw error; @@ -124,22 +120,23 @@ export class OrbView implements IOrbVi this._renderer.on(RenderEventType.RENDER_END, (data) => { this._events.emit(OrbEventType.RENDER_END, data); }); + this._renderer.on(RenderEventType.RESIZE, () => { + if (this._renderer.isInitiallyRendered) { + this._renderer.render(this._graph); + } + }); + this._renderer.translateOriginToCenter(); this._settings.render = this._renderer.getSettings(); - // Resize the canvas based on the dimensions of its parent container
. - this._resizeObs = new ResizeObserver(() => this._handleResize()); - this._resizeObs.observe(this._container); - this._handleResize(); - this._d3Zoom = zoom() .scaleExtent([this._renderer.getSettings().minZoom, this._renderer.getSettings().maxZoom]) .on('zoom', this.zoomed); - select(this._canvas) + select(this._renderer.canvas) .call( drag() - .container(this._canvas) + .container(this._renderer.canvas) .subject(this.dragSubject) .on('start', this.dragStarted) .on('drag', this.dragged) @@ -266,7 +263,7 @@ export class OrbView implements IOrbVi recenter(onRendered?: () => void) { const fitZoomTransform = this._renderer.getFitZoomTransform(this._graph); - select(this._canvas) + select(this._renderer.canvas) .transition() .duration(this._settings.zoomFitTransitionMs) .ease(easeLinear) @@ -278,10 +275,8 @@ export class OrbView implements IOrbVi } destroy() { - this._resizeObs.unobserve(this._container); - this._renderer.removeAllListeners(); + this._renderer.destroy(); this._simulator.terminate(); - this._canvas.outerHTML = ''; } dragSubject = (event: D3DragEvent>) => { @@ -367,7 +362,7 @@ export class OrbView implements IOrbVi }; getCanvasMousePosition(event: MouseEvent): IPosition { - const rect = this._canvas.getBoundingClientRect(); + const rect = this._renderer.canvas.getBoundingClientRect(); let x = event.clientX ?? event.pageX ?? event.x; let y = event.clientY ?? event.pageY ?? event.y; @@ -542,27 +537,6 @@ export class OrbView implements IOrbVi } }; - private _initCanvas(): HTMLCanvasElement { - const canvas = document.createElement('canvas'); - canvas.style.position = 'absolute'; - canvas.style.top = '0'; - canvas.style.left = '0'; - - this._container.appendChild(canvas); - return canvas; - } - - private _handleResize() { - const containerSize = this._container.getBoundingClientRect(); - this._canvas.width = containerSize.width; - this._canvas.height = containerSize.height; - this._renderer.width = containerSize.width; - this._renderer.height = containerSize.height; - if (this._renderer.isInitiallyRendered) { - this._renderer.render(this._graph); - } - } - private _startSimulation() { const nodePositions = this._graph.getNodePositions(); const edgePositions = this._graph.getEdgePositions();