From 411ed397e4f5086c3ccf762ac64248805ac024c1 Mon Sep 17 00:00:00 2001
From: Mikey Gower
Date: Tue, 13 Feb 2024 10:57:41 -0500
Subject: [PATCH 01/12] RectangleTexture and createRenderTexture util
---
src/renderers/webgl/textures/ArrowTexture.ts | 23 ++-------
src/renderers/webgl/textures/CircleTexture.ts | 21 ++------
.../webgl/textures/RectangleTexture.ts | 32 ++++++++++++
.../webgl/textures/TextIconTexture.ts | 49 +++++--------------
src/utils/webgl.ts | 28 +++++++++++
5 files changed, 79 insertions(+), 74 deletions(-)
create mode 100644 src/renderers/webgl/textures/RectangleTexture.ts
create mode 100644 src/utils/webgl.ts
diff --git a/src/renderers/webgl/textures/ArrowTexture.ts b/src/renderers/webgl/textures/ArrowTexture.ts
index 7d11d11..6a4448b 100644
--- a/src/renderers/webgl/textures/ArrowTexture.ts
+++ b/src/renderers/webgl/textures/ArrowTexture.ts
@@ -1,7 +1,8 @@
-import { RenderTexture, Graphics, Matrix, MSAA_QUALITY, Renderer as PixiRenderer } from 'pixi.js'
+import { RenderTexture, Graphics, Matrix } from 'pixi.js'
+import { createRenderTexture } from '../../../utils/webgl'
import { MIN_TEXTURE_ZOOM } from '../../../utils/constants'
-import { Texture } from '../../../types'
import { Renderer } from '..'
+import { Texture } from '../../../types'
export default class ArrowTexture implements Texture {
private texture: RenderTexture
@@ -12,23 +13,7 @@ export default class ArrowTexture implements Texture {
.lineTo(this.height * this.scaleFactor, this.width * this.scaleFactor * 0.5)
.lineTo(this.height * this.scaleFactor, -this.width * this.scaleFactor * 0.5)
- this.texture = RenderTexture.create({
- width: graphic.width,
- height: graphic.height,
- multisample: MSAA_QUALITY.HIGH,
- resolution: 2
- })
-
- renderer.app.renderer.render(graphic, {
- renderTexture: this.texture,
- transform: new Matrix(1, 0, 0, 1, 0, graphic.height / 2)
- })
-
- if (renderer.app.renderer instanceof PixiRenderer) {
- renderer.app.renderer.framebuffer.blit()
- }
-
- graphic.destroy(true)
+ this.texture = createRenderTexture(renderer.app, graphic, new Matrix(1, 0, 0, 1, 0, graphic.height / 2))
}
get() {
diff --git a/src/renderers/webgl/textures/CircleTexture.ts b/src/renderers/webgl/textures/CircleTexture.ts
index e1c6b5b..e9f6a07 100644
--- a/src/renderers/webgl/textures/CircleTexture.ts
+++ b/src/renderers/webgl/textures/CircleTexture.ts
@@ -1,4 +1,5 @@
-import { RenderTexture, Graphics, Matrix, MSAA_QUALITY, Renderer as PixiRenderer } from 'pixi.js'
+import { RenderTexture, Graphics, Matrix } from 'pixi.js'
+import { createRenderTexture } from '../../../utils/webgl'
import { MIN_TEXTURE_ZOOM } from '../../../utils/constants'
import { Texture } from '../../../types'
import { Renderer } from '..'
@@ -9,23 +10,7 @@ export default class CircleTexture implements Texture {
constructor(renderer: Renderer) {
const graphic = new Graphics().beginFill(0xffffff).drawCircle(0, 0, this.scaleFactor)
- this.texture = RenderTexture.create({
- width: graphic.width,
- height: graphic.height,
- multisample: MSAA_QUALITY.HIGH,
- resolution: 2
- })
-
- renderer.app.renderer.render(graphic, {
- renderTexture: this.texture,
- transform: new Matrix(1, 0, 0, 1, graphic.width / 2, graphic.height / 2)
- })
-
- if (renderer.app.renderer instanceof PixiRenderer) {
- renderer.app.renderer.framebuffer.blit()
- }
-
- graphic.destroy(true)
+ this.texture = createRenderTexture(renderer.app, graphic, new Matrix(1, 0, 0, 1, graphic.width / 2, graphic.height / 2))
}
get() {
diff --git a/src/renderers/webgl/textures/RectangleTexture.ts b/src/renderers/webgl/textures/RectangleTexture.ts
new file mode 100644
index 0000000..e89382e
--- /dev/null
+++ b/src/renderers/webgl/textures/RectangleTexture.ts
@@ -0,0 +1,32 @@
+import { Graphics, Matrix, RenderTexture } from 'pixi.js'
+import { createRenderTexture } from '../../../utils/webgl'
+import { MIN_TEXTURE_ZOOM } from '../../../utils/constants'
+import { Renderer } from '..'
+import { Texture } from '../../../types'
+
+export default class RectangleTexture implements Texture {
+ scaleFactor: number
+ private texture: RenderTexture
+
+ constructor(renderer: Renderer) {
+ this.scaleFactor = Math.max(renderer.width, renderer.height) * this.minTextureZoom
+
+ const graphic = new Graphics().beginFill(0xffffff).drawRect(0, 0, this.scaleFactor, this.scaleFactor)
+
+ this.texture = createRenderTexture(renderer.app, graphic, new Matrix(1, 0, 0, 1, graphic.width / 2, graphic.height / 2))
+ }
+
+ get() {
+ return this.texture
+ }
+
+ delete() {
+ this.texture.destroy()
+ return undefined
+ }
+
+ // TODO -> make configurable
+ private get minTextureZoom() {
+ return MIN_TEXTURE_ZOOM
+ }
+}
diff --git a/src/renderers/webgl/textures/TextIconTexture.ts b/src/renderers/webgl/textures/TextIconTexture.ts
index 07d7be2..35e3528 100644
--- a/src/renderers/webgl/textures/TextIconTexture.ts
+++ b/src/renderers/webgl/textures/TextIconTexture.ts
@@ -1,13 +1,11 @@
-import { RenderTexture, Text as PixiText, MSAA_QUALITY, Matrix, Renderer as PixiRenderer } from 'pixi.js'
-import { DEFAULT_RESOLUTION, DEFAULT_TEXT_STYLE, MIN_TEXTURE_ZOOM } from '../../../utils/constants'
+import { RenderTexture, Text as PixiText } from 'pixi.js'
+import { createRenderTexture } from '../../../utils/webgl'
import { TextIcon, Texture } from '../../../types'
+import { MIN_TEXTURE_ZOOM } from '../../../utils/constants'
import { Renderer } from '..'
import TextTexture from './TextTexture'
-const getCacheKey = ({ content, style = {} }: TextIcon) => {
- const { color, stroke, fontFamily, fontSize, fontWeight } = { ...style, ...DEFAULT_TEXT_STYLE }
- return [content, color, stroke.color, stroke.width, fontFamily, fontSize, fontWeight].join('-')
-}
+const join = (...args: (string | number)[]) => args.join('-')
export default class TextIconTexture implements Texture {
protected cache: { [key: string]: RenderTexture } = {}
@@ -17,10 +15,16 @@ export default class TextIconTexture implements Texture {
}
get(icon: TextIcon) {
- const key = getCacheKey(icon)
+ const style = new TextTexture(icon.style, { defaultTextStyle: { align: 'center' } })
+ const key = join(icon.content, style.color, style.stroke.color, style.stroke.width, style.fontFamily, style.fontSize, style.fontWeight)
if (this.cache[key] === undefined) {
- this.cache[key] = this.createTexture(icon)
+ style.fontSize = style.fontSize * this.scaleFactor
+
+ const object = new PixiText(icon.content, style.getTextStyle())
+ object.updateText(true)
+
+ this.cache[key] = createRenderTexture(this.renderer.app, object)
}
return this.cache[key]
@@ -39,33 +43,4 @@ export default class TextIconTexture implements Texture {
get scaleFactor() {
return MIN_TEXTURE_ZOOM
}
- get resolution() {
- return DEFAULT_RESOLUTION
- }
-
- private createTexture(icon: TextIcon) {
- const style = new TextTexture(icon.style, { defaultTextStyle: { align: 'center' } })
- style.fontSize = style.fontSize * this.scaleFactor
-
- const object = new PixiText(icon.content, style.getTextStyle())
-
- object.updateText(true)
-
- const renderTexture = RenderTexture.create({
- width: object.width,
- height: object.height,
- multisample: MSAA_QUALITY.HIGH,
- resolution: this.resolution
- })
-
- this.renderer.app.renderer.render(object, { renderTexture, transform: new Matrix() })
-
- if (this.renderer.app.renderer instanceof PixiRenderer) {
- this.renderer.app.renderer.framebuffer.blit()
- }
-
- object.destroy(true)
-
- return renderTexture
- }
}
diff --git a/src/utils/webgl.ts b/src/utils/webgl.ts
new file mode 100644
index 0000000..8f04e7c
--- /dev/null
+++ b/src/utils/webgl.ts
@@ -0,0 +1,28 @@
+import { Application, Renderer, Container, Matrix, RenderTexture, IBaseTextureOptions, MSAA_QUALITY, SCALE_MODES } from 'pixi.js'
+import { DEFAULT_RESOLUTION } from './constants'
+
+export const createRenderTexture = (
+ app: A,
+ graphic: G,
+ transform = new Matrix(),
+ options: IBaseTextureOptions = {}
+) => {
+ const renderTexture = RenderTexture.create({
+ width: graphic.width,
+ height: graphic.height,
+ resolution: DEFAULT_RESOLUTION,
+ multisample: MSAA_QUALITY.HIGH,
+ scaleMode: SCALE_MODES.LINEAR,
+ ...options
+ })
+
+ app.renderer.render(graphic, { renderTexture, transform })
+
+ if (app.renderer instanceof Renderer) {
+ app.renderer.framebuffer.blit()
+ }
+
+ graphic.destroy(true)
+
+ return renderTexture
+}
From 0dc830443b6c6b18609d3233de5ea76f0630c33d Mon Sep 17 00:00:00 2001
From: Mikey Gower
Date: Tue, 13 Feb 2024 11:48:28 -0500
Subject: [PATCH 02/12] standardize RenderObject type
---
src/renderers/webgl/LifecycleManager.ts | 4 +-
src/renderers/webgl/node.ts | 8 +-
src/renderers/webgl/objects/Icon.ts | 4 +-
src/renderers/webgl/objects/circle/Circle.ts | 124 ++++++++++++++++++
.../webgl/objects/circle/CircleStrokes.ts | 118 +++++++++++++++++
src/renderers/webgl/objects/nodeFill.ts | 76 -----------
src/renderers/webgl/objects/nodeStrokes.ts | 10 +-
.../webgl/objects/rectangle/Rectangle.ts | 20 +++
.../objects/rectangle/RectangleStrokes.ts | 20 +++
src/renderers/webgl/objects/text/Text.ts | 4 +-
.../webgl/objects/text/TextHighlight.ts | 4 +-
src/types/internal.ts | 4 +
src/utils/constants.ts | 8 +-
13 files changed, 310 insertions(+), 94 deletions(-)
create mode 100644 src/renderers/webgl/objects/circle/Circle.ts
create mode 100644 src/renderers/webgl/objects/circle/CircleStrokes.ts
delete mode 100644 src/renderers/webgl/objects/nodeFill.ts
create mode 100644 src/renderers/webgl/objects/rectangle/Rectangle.ts
create mode 100644 src/renderers/webgl/objects/rectangle/RectangleStrokes.ts
diff --git a/src/renderers/webgl/LifecycleManager.ts b/src/renderers/webgl/LifecycleManager.ts
index fbdc473..968ca8e 100644
--- a/src/renderers/webgl/LifecycleManager.ts
+++ b/src/renderers/webgl/LifecycleManager.ts
@@ -1,13 +1,13 @@
import { NodeStrokes } from './objects/nodeStrokes'
import { LineSegment } from './objects/lineSegment'
-import { NodeFill } from './objects/nodeFill'
import { Arrow } from './objects/arrow'
import ObjectManager from './objects/ObjectManager'
+import Circle from './objects/circle/Circle'
import Icon from './objects/Icon'
import Text from './objects/text/Text'
export default class LifecycleManager {
- nodes = new ObjectManager(2000)
+ nodes = new ObjectManager(2000)
icons = new ObjectManager(1000)
edges = new ObjectManager(2000)
arrows = new ObjectManager(1000)
diff --git a/src/renderers/webgl/node.ts b/src/renderers/webgl/node.ts
index f03e7b5..1fb44e2 100644
--- a/src/renderers/webgl/node.ts
+++ b/src/renderers/webgl/node.ts
@@ -3,9 +3,9 @@ import { FederatedPointerEvent } from 'pixi.js'
import { NodeStrokes } from './objects/nodeStrokes'
import { NodeHitArea } from './interaction/nodeHitArea'
import { interpolate } from '../../utils/helpers'
-import { NodeFill } from './objects/nodeFill'
import { type Renderer } from '.'
import type { Node } from '../../types'
+import Circle from './objects/circle/Circle'
import Text from './objects/text/Text'
import Icon from './objects/Icon'
@@ -13,7 +13,7 @@ export class NodeRenderer {
node!: Node
x!: number
y!: number
- fill: NodeFill
+ fill: Circle
label?: Text
icon?: Icon
strokes: NodeStrokes
@@ -32,7 +32,7 @@ export class NodeRenderer {
constructor(renderer: Renderer, node: Node) {
this.renderer = renderer
- this.fill = new NodeFill(this.renderer.nodesContainer, this.renderer.circle)
+ this.fill = new Circle(renderer.nodesContainer, renderer.circle, node.style?.color)
this.strokes = new NodeStrokes(this.renderer.nodesContainer, this.renderer.circle, this.fill)
this.hitArea = new NodeHitArea(this.renderer.interactionContainer, this)
this.update(node)
@@ -491,7 +491,7 @@ export class NodeRenderer {
this.x = x
this.y = y
- this.fill.update(this.x, this.y, radius, node.style)
+ this.fill.moveTo(this.x, this.y).resize(radius)
this.strokes.update(this.x, this.y, radius, node.style)
if (this.label) {
diff --git a/src/renderers/webgl/objects/Icon.ts b/src/renderers/webgl/objects/Icon.ts
index fb8667d..5b01b5e 100644
--- a/src/renderers/webgl/objects/Icon.ts
+++ b/src/renderers/webgl/objects/Icon.ts
@@ -1,10 +1,10 @@
import { Container, Sprite, Texture } from 'pixi.js'
-import { IconStyle } from '../../../types'
+import { IconStyle, RenderObject } from '../../../types'
import { equals } from '../../../utils/api'
import AssetManager, { AssetSubscription, FontSubscription } from '../loaders/AssetManager'
import TextIconTexture from '../textures/TextIconTexture'
-export default class Icon {
+export default class Icon implements RenderObject {
mounted = false
private x = 0
diff --git a/src/renderers/webgl/objects/circle/Circle.ts b/src/renderers/webgl/objects/circle/Circle.ts
new file mode 100644
index 0000000..3595c4c
--- /dev/null
+++ b/src/renderers/webgl/objects/circle/Circle.ts
@@ -0,0 +1,124 @@
+import { FillStyle, RenderObject } from '../../../../types'
+import { DEFAULT_FILL_STYLE } from '../../../../utils/constants'
+import { Container, Sprite } from 'pixi.js'
+import CircleTexture from '../../textures/CircleTexture'
+
+export default class Circle implements RenderObject {
+ mounted = false
+
+ private _x = 0
+ private _y = 0
+ private _radius = 0
+
+ private object: Sprite
+ private style: Required = DEFAULT_FILL_STYLE
+
+ constructor(
+ private container: Container,
+ private texture: CircleTexture,
+ color?: string,
+ opacity?: number
+ ) {
+ this.container = container
+ this.texture = texture
+
+ this.style = {
+ color: color ?? this.style.color,
+ opacity: opacity ?? this.style.opacity
+ }
+
+ this.object = this.create()
+ }
+
+ update(color = this.style.color, opacity = this.style.opacity) {
+ if (color !== this.style.color) {
+ this.style.color = color
+ this.object.tint = color
+ }
+
+ if (opacity !== this.style.opacity) {
+ this.style.opacity = opacity
+ this.object.alpha = opacity
+ }
+
+ return this
+ }
+
+ moveTo(x: number, y: number) {
+ if (x !== this.x) {
+ this._x = x
+ this.object.x = x
+ }
+
+ if (y !== this.y) {
+ this._y = y
+ this.object.y = y
+ }
+
+ return this
+ }
+
+ resize(radius: number) {
+ if (this._radius !== radius) {
+ this._radius = radius
+ this.object.scale.set(radius / this.texture.scaleFactor)
+ }
+
+ return this
+ }
+
+ mount() {
+ // TODO - why is mounting/unmouting fill Sprite less efficient?
+ if (!this.mounted) {
+ this.mounted = true
+ this.object.visible = true
+ }
+
+ return this
+ }
+
+ unmount() {
+ if (this.mounted) {
+ this.mounted = false
+ this.object.visible = false
+ }
+
+ return this
+ }
+
+ delete() {
+ this.unmount()
+ this.container.removeChild(this.object)
+ this.object.destroy()
+
+ return undefined
+ }
+
+ getContainerIndex() {
+ return this.container.getChildIndex(this.object)
+ }
+
+ get x() {
+ return this._x
+ }
+
+ get y() {
+ return this._y
+ }
+
+ get radius() {
+ return this._radius
+ }
+
+ private create() {
+ const object = new Sprite(this.texture.get())
+ object.anchor.set(0.5)
+ object.x = this._x
+ object.y = this._y
+ object.visible = this.mounted
+ object.tint = this.style.color
+ object.alpha = this.style.opacity
+ this.container.addChild(object)
+ return object
+ }
+}
diff --git a/src/renderers/webgl/objects/circle/CircleStrokes.ts b/src/renderers/webgl/objects/circle/CircleStrokes.ts
new file mode 100644
index 0000000..6ba9a53
--- /dev/null
+++ b/src/renderers/webgl/objects/circle/CircleStrokes.ts
@@ -0,0 +1,118 @@
+import { FillStyle, RenderObject } from '../../../../types'
+import { DEFAULT_FILL_STYLE } from '../../../../utils/constants'
+import { Container, Sprite } from 'pixi.js'
+import CircleTexture from '../../textures/CircleTexture'
+
+export default class CircleStrokes implements RenderObject {
+ mounted = false
+
+ private _x = 0
+ private _y = 0
+ private _radius = 0
+
+ private object: Sprite
+ private style: Required = DEFAULT_FILL_STYLE
+
+ constructor(
+ private container: Container,
+ private texture: CircleTexture,
+ style?: FillStyle
+ ) {
+ this.container = container
+ this.texture = texture
+
+ if (style) {
+ this.style = { ...DEFAULT_FILL_STYLE, ...style }
+ }
+
+ this.object = this.create()
+ }
+
+ update(color = this.style.color, opacity = this.style.opacity) {
+ if (color !== this.style.color) {
+ this.style.color = color
+ this.object.tint = color
+ }
+
+ if (opacity !== this.style.opacity) {
+ this.style.opacity = opacity
+ this.object.alpha = opacity
+ }
+
+ return this
+ }
+
+ moveTo(x: number, y: number) {
+ if (x !== this.x) {
+ this._x = x
+ this.object.x = x
+ }
+
+ if (y !== this.y) {
+ this._y = y
+ this.object.y = y
+ }
+
+ return this
+ }
+
+ resize(radius: number) {
+ if (this._radius !== radius) {
+ this._radius = radius
+ this.object.scale.set(radius / this.texture.scaleFactor)
+ }
+
+ return this
+ }
+
+ mount() {
+ // TODO - why is mounting/unmouting fill Sprite less efficient?
+ if (!this.mounted) {
+ this.mounted = true
+ this.object.visible = true
+ }
+
+ return this
+ }
+
+ unmount() {
+ if (this.mounted) {
+ this.mounted = false
+ this.object.visible = false
+ }
+
+ return this
+ }
+
+ delete() {
+ this.unmount()
+ this.container.removeChild(this.object)
+ this.object.destroy()
+
+ return undefined
+ }
+
+ get x() {
+ return this._x
+ }
+
+ get y() {
+ return this._y
+ }
+
+ get radius() {
+ return this._radius
+ }
+
+ private create() {
+ const object = new Sprite(this.texture.get())
+ object.anchor.set(0.5)
+ object.x = this._x
+ object.y = this._y
+ object.visible = this.mounted
+ object.tint = this.style.color
+ object.alpha = this.style.opacity
+ this.container.addChild(object)
+ return object
+ }
+}
diff --git a/src/renderers/webgl/objects/nodeFill.ts b/src/renderers/webgl/objects/nodeFill.ts
deleted file mode 100644
index e5a940a..0000000
--- a/src/renderers/webgl/objects/nodeFill.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-import { Container, Sprite } from 'pixi.js'
-import type { NodeStyle } from '../../../types'
-import CircleTexture from '../textures/CircleTexture'
-
-const DEFAULT_NODE_FILL = 0xaaaaaa
-
-export class NodeFill {
- mounted = false
- fill: Sprite // TODO - make private
-
- private container: Container
- private circleTexture: CircleTexture
- private radius?: number
- private style?: NodeStyle
-
- constructor(container: Container, circleTexture: CircleTexture) {
- this.container = container
- this.circleTexture = circleTexture
- this.fill = new Sprite(this.circleTexture.get())
- this.fill.anchor.set(0.5)
- this.fill.visible = false
-
- this.container.addChild(this.fill)
- }
-
- update(x: number, y: number, radius: number, style?: NodeStyle) {
- if ((style?.color ?? DEFAULT_NODE_FILL) !== (this.style?.color ?? DEFAULT_NODE_FILL)) {
- this.fill.tint = style?.color ?? DEFAULT_NODE_FILL
- }
-
- if (radius !== this.radius) {
- this.fill.scale.set(radius / this.circleTexture.scaleFactor)
- this.radius = radius
- }
-
- this.fill.x = x
- this.fill.y = y
-
- this.style = style
-
- return this
- }
-
- mount() {
- if (!this.mounted) {
- // TODO - why is mounting/unmouting fill Sprite less efficient?
- this.fill.visible = true
- // this.container.addChild(this.fill)
- this.mounted = true
- }
-
- return this
- }
-
- unmount() {
- if (this.mounted) {
- this.fill.visible = false
- // this.container.removeChild(this.fill)
- this.mounted = false
- }
-
- return this
- }
-
- delete() {
- this.unmount()
- this.container.removeChild(this.fill)
- this.fill.destroy()
-
- return undefined
- }
-
- getContainerIndex() {
- return this.container.getChildIndex(this.fill)
- }
-}
diff --git a/src/renderers/webgl/objects/nodeStrokes.ts b/src/renderers/webgl/objects/nodeStrokes.ts
index 00243b6..cd6968e 100644
--- a/src/renderers/webgl/objects/nodeStrokes.ts
+++ b/src/renderers/webgl/objects/nodeStrokes.ts
@@ -1,6 +1,5 @@
import { Container, Sprite } from 'pixi.js'
import CircleTexture from '../textures/CircleTexture'
-import { NodeFill } from './nodeFill'
import type { NodeStyle } from '../../../types'
export class NodeStrokes {
@@ -8,12 +7,13 @@ export class NodeStrokes {
radius = 0
sprites?: Sprite[] // TODO - make private
- private container: Container
- private circleTexture: CircleTexture
- private fill: NodeFill
private style?: NodeStyle
- constructor(container: Container, circleTexture: CircleTexture, fill: NodeFill) {
+ constructor(
+ private container: Container,
+ private circleTexture: CircleTexture,
+ private fill: { getContainerIndex: () => number }
+ ) {
this.container = container
this.circleTexture = circleTexture
this.fill = fill
diff --git a/src/renderers/webgl/objects/rectangle/Rectangle.ts b/src/renderers/webgl/objects/rectangle/Rectangle.ts
new file mode 100644
index 0000000..089d210
--- /dev/null
+++ b/src/renderers/webgl/objects/rectangle/Rectangle.ts
@@ -0,0 +1,20 @@
+import { RenderObject } from '../../../../types'
+
+export default class Rectangle implements RenderObject {
+ mounted = false
+
+ moveTo(): this {
+ return this
+ }
+ mount() {
+ this.mounted = true
+ return this
+ }
+ unmount(): this {
+ this.mounted = false
+ return this
+ }
+ delete(): void {
+ this.unmount()
+ }
+}
diff --git a/src/renderers/webgl/objects/rectangle/RectangleStrokes.ts b/src/renderers/webgl/objects/rectangle/RectangleStrokes.ts
new file mode 100644
index 0000000..7c0bb01
--- /dev/null
+++ b/src/renderers/webgl/objects/rectangle/RectangleStrokes.ts
@@ -0,0 +1,20 @@
+import { RenderObject } from '../../../../types'
+
+export default class RectangleStrokes implements RenderObject {
+ mounted = false
+
+ moveTo(): this {
+ return this
+ }
+ mount() {
+ this.mounted = true
+ return this
+ }
+ unmount(): this {
+ this.mounted = false
+ return this
+ }
+ delete(): void {
+ this.unmount()
+ }
+}
diff --git a/src/renderers/webgl/objects/text/Text.ts b/src/renderers/webgl/objects/text/Text.ts
index 06b61dd..b523e39 100644
--- a/src/renderers/webgl/objects/text/Text.ts
+++ b/src/renderers/webgl/objects/text/Text.ts
@@ -1,11 +1,11 @@
-import type { Bounds, TextStyle, TextObject, FontWeight } from '../../../../types'
+import type { Bounds, TextStyle, TextObject, FontWeight, RenderObject } from '../../../../types'
import { BitmapText, Container, Text as PixiText } from 'pixi.js'
import { equals } from '../../../../utils/api'
import TextTexture, { TextTextureOptions } from '../../textures/TextTexture'
import AssetManager, { FontSubscription } from '../../loaders/AssetManager'
import TextHighlight from './TextHighlight'
-export default class Text {
+export default class Text implements RenderObject {
mounted = false
offset = 0
diff --git a/src/renderers/webgl/objects/text/TextHighlight.ts b/src/renderers/webgl/objects/text/TextHighlight.ts
index c96ce70..d7ecba1 100644
--- a/src/renderers/webgl/objects/text/TextHighlight.ts
+++ b/src/renderers/webgl/objects/text/TextHighlight.ts
@@ -1,7 +1,7 @@
import { Container, Sprite, Texture } from 'pixi.js'
-import { FillStyle, TextObject } from '../../../../types'
+import { FillStyle, RenderObject, TextObject } from '../../../../types'
-export default class TextHighlight {
+export default class TextHighlight implements RenderObject {
mounted = false
private x = 0
diff --git a/src/types/internal.ts b/src/types/internal.ts
index d31e657..71a61c1 100644
--- a/src/types/internal.ts
+++ b/src/types/internal.ts
@@ -17,3 +17,7 @@ export interface RenderObjectLifecycle {
unmount(): this
delete(): void
}
+
+export interface RenderObject extends RenderObjectLifecycle {
+ moveTo(...args: number[]): this
+}
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index 8e8bd81..160cdda 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -13,7 +13,13 @@ export const DEFAULT_OPACITY = 1
export const COLORS = {
BLACK: '#000000',
- WHITE: '#FFFFFF'
+ WHITE: '#FFFFFF',
+ GREY: '#AAAAAA'
+}
+
+export const DEFAULT_FILL_STYLE = {
+ color: COLORS.GREY,
+ opacity: DEFAULT_OPACITY
}
export const DEFAULT_TEXT_STYLE = {
From 7d10c9117dca9f3f3435e63f0782e53583142137 Mon Sep 17 00:00:00 2001
From: Mikey Gower
Date: Tue, 13 Feb 2024 13:28:30 -0500
Subject: [PATCH 03/12] replace nodeFill and nodeStrokes with Circle and
CircleStrokes
---
examples/native/src/labels/index.ts | 17 ++-
src/renderers/webgl/LifecycleManager.ts | 4 +-
src/renderers/webgl/node.ts | 110 +++++++++------
src/renderers/webgl/objects/circle/Circle.ts | 25 ++--
.../webgl/objects/circle/CircleStrokes.ts | 128 ++++++++++--------
5 files changed, 165 insertions(+), 119 deletions(-)
diff --git a/examples/native/src/labels/index.ts b/examples/native/src/labels/index.ts
index 5147f0c..87a0dc5 100644
--- a/examples/native/src/labels/index.ts
+++ b/examples/native/src/labels/index.ts
@@ -26,7 +26,10 @@ const TEXT_ICON: Graph.TextIcon = {
const NODE_STYLE: Graph.NodeStyle = {
color: GREEN,
icon: TEXT_ICON,
- stroke: [{ width: 2, color: GREEN_LIGHT }],
+ stroke: [
+ { width: 1, color: '#FFF' },
+ { width: 2, color: GREEN_LIGHT }
+ ],
label: {
position: 'bottom',
fontName: 'NodeLabel',
@@ -39,7 +42,11 @@ const NODE_STYLE: Graph.NodeStyle = {
const NODE_HOVER_STYLE: Graph.NodeStyle = {
color: DARK_GREEN,
icon: IMAGE_ICON,
- stroke: [{ width: 2, color: GREEN_LIGHT }],
+ stroke: [
+ { width: 1, color: '#FFF' },
+ { width: 2, color: GREEN_LIGHT },
+ { width: 1, color: DARK_GREEN }
+ ],
label: {
position: 'bottom',
fontName: 'NodeLabelHover',
@@ -119,7 +126,9 @@ const options: Renderer.Options = {
},
onNodePointerEnter: (event: Renderer.NodePointerEvent) => {
// console.log('node pointer enter', `x: ${event.x}, y: ${event.y}`)
- nodes = nodes.map((node) => (node.id === event.target.id ? { ...node, label: node.label + ' 北京', style: NODE_HOVER_STYLE } : node))
+ nodes = nodes.map((node) =>
+ node.id === event.target.id ? { ...node, radius: 15, label: node.label + ' 北京', style: NODE_HOVER_STYLE } : node
+ )
renderer.update({ nodes, edges, options })
},
onNodeDrag: (event: Renderer.NodeDragEvent) => {
@@ -132,7 +141,7 @@ const options: Renderer.Options = {
onNodePointerLeave: (event: Renderer.NodePointerEvent) => {
// console.log('node pointer leave', `x: ${event.x}, y: ${event.y}`)
nodes = nodes.map((node) =>
- node.id === event.target.id ? { ...node, label: node.label?.slice(0, node.label.length - 3), style: NODE_STYLE } : node
+ node.id === event.target.id ? { ...node, radius: 10, label: node.label?.slice(0, node.label.length - 3), style: NODE_STYLE } : node
)
renderer.update({ nodes, edges, options })
}
diff --git a/src/renderers/webgl/LifecycleManager.ts b/src/renderers/webgl/LifecycleManager.ts
index 968ca8e..2cdce0d 100644
--- a/src/renderers/webgl/LifecycleManager.ts
+++ b/src/renderers/webgl/LifecycleManager.ts
@@ -1,13 +1,13 @@
-import { NodeStrokes } from './objects/nodeStrokes'
import { LineSegment } from './objects/lineSegment'
import { Arrow } from './objects/arrow'
import ObjectManager from './objects/ObjectManager'
+import CircleStrokes from './objects/circle/CircleStrokes'
import Circle from './objects/circle/Circle'
import Icon from './objects/Icon'
import Text from './objects/text/Text'
export default class LifecycleManager {
- nodes = new ObjectManager(2000)
+ nodes = new ObjectManager(2000)
icons = new ObjectManager(1000)
edges = new ObjectManager(2000)
arrows = new ObjectManager(1000)
diff --git a/src/renderers/webgl/node.ts b/src/renderers/webgl/node.ts
index 1fb44e2..5e2d189 100644
--- a/src/renderers/webgl/node.ts
+++ b/src/renderers/webgl/node.ts
@@ -1,22 +1,23 @@
import { DEFAULT_LABEL_STYLE, MIN_LABEL_ZOOM, MIN_INTERACTION_ZOOM, MIN_NODE_STROKE_ZOOM, MIN_NODE_ICON_ZOOM } from '../../utils/constants'
import { FederatedPointerEvent } from 'pixi.js'
-import { NodeStrokes } from './objects/nodeStrokes'
import { NodeHitArea } from './interaction/nodeHitArea'
import { interpolate } from '../../utils/helpers'
import { type Renderer } from '.'
import type { Node } from '../../types'
import Circle from './objects/circle/Circle'
+import CircleStrokes from './objects/circle/CircleStrokes'
import Text from './objects/text/Text'
import Icon from './objects/Icon'
export class NodeRenderer {
- node!: Node
- x!: number
- y!: number
+ node: Node
+ x = 0
+ y = 0
+ radius = 0
fill: Circle
+ strokes: CircleStrokes
label?: Text
icon?: Icon
- strokes: NodeStrokes
private hitArea: NodeHitArea
private renderer: Renderer
@@ -32,13 +33,19 @@ export class NodeRenderer {
constructor(renderer: Renderer, node: Node) {
this.renderer = renderer
- this.fill = new Circle(renderer.nodesContainer, renderer.circle, node.style?.color)
- this.strokes = new NodeStrokes(this.renderer.nodesContainer, this.renderer.circle, this.fill)
+ this.fill = new Circle(renderer.nodesContainer, renderer.circle)
+ this.strokes = new CircleStrokes(renderer.nodesContainer, renderer.circle, this.fill)
this.hitArea = new NodeHitArea(this.renderer.interactionContainer, this)
+ this.node = node
this.update(node)
}
update(node: Node) {
+ this.node = node
+
+ this.fill.update(node.style?.color)
+ this.strokes.update(node.style?.stroke)
+
if (this.label) {
if (node.label === undefined || node.label.trim() === '') {
this.managers.labels.delete(this.label)
@@ -67,9 +74,10 @@ export class NodeRenderer {
const x = node.x ?? 0
const y = node.y ?? 0
+ const radius = node.radius
const xChanged = x !== this.x
const yChanged = y !== this.y
- const radiusChanged = node.radius !== this.node?.radius
+ const radiusChanged = radius !== this.radius
if (
(xChanged || yChanged || radiusChanged) &&
@@ -84,54 +92,56 @@ export class NodeRenderer {
this.interpolateY = interpolate(this.y, y, this.renderer.animateNodePosition)
}
if (radiusChanged && this.renderer.animateNodeRadius) {
- this.interpolateRadius = interpolate(this.y, y, this.renderer.animateNodeRadius)
+ this.interpolateRadius = interpolate(this.radius, radius, this.renderer.animateNodeRadius)
}
} else {
- this.setPosition(node, x, y, node.radius)
+ this.resize(radius).moveTo(x, y)
this.interpolateX = undefined
this.interpolateY = undefined
this.interpolateRadius = undefined
}
- this.node = node
-
return this
}
render(dt: number) {
- let _x: number | undefined
- let _y: number | undefined
- let _radius: number | undefined
+ if (this.interpolateRadius) {
+ const { value, done } = this.interpolateRadius(dt)
- if (this.interpolateX) {
- const { value, done } = this.interpolateX(dt)
- _x = value
+ this.resize(value)
if (done) {
- this.interpolateX = undefined
+ this.interpolateRadius = undefined
+ }
+
+ if (this.label && !this.interpolateX && !this.interpolateY) {
+ this.label.moveTo(this.x, this.y)
}
}
- if (this.interpolateY) {
- const { value, done } = this.interpolateY(dt)
- _y = value
+ if (this.interpolateX || this.interpolateY) {
+ let x = this.x
+ let y = this.y
- if (done) {
- this.interpolateY = undefined
+ if (this.interpolateX) {
+ const { value, done } = this.interpolateX(dt)
+ x = value
+
+ if (done) {
+ this.interpolateX = undefined
+ }
}
- }
- if (this.interpolateRadius) {
- const { value, done } = this.interpolateRadius(dt)
- _radius = value
+ if (this.interpolateY) {
+ const { value, done } = this.interpolateY(dt)
+ y = value
- if (done) {
- this.interpolateRadius = undefined
+ if (done) {
+ this.interpolateY = undefined
+ }
}
- }
- if (_x !== undefined || _y !== undefined || _radius !== undefined) {
- this.setPosition(this.node, _x ?? this.x, _y ?? this.y, _radius ?? this.node.radius)
+ this.moveTo(x, y)
}
const isVisible = this.visible()
@@ -487,20 +497,34 @@ export class NodeRenderer {
this.doubleClick = false
}
- private setPosition(node: Node, x: number, y: number, radius: number) {
- this.x = x
- this.y = y
+ private resize(radius: number) {
+ if (radius !== this.radius) {
+ this.radius = radius
+ this.fill.resize(radius)
+ this.strokes.resize(radius)
- this.fill.moveTo(this.x, this.y).resize(radius)
- this.strokes.update(this.x, this.y, radius, node.style)
+ if (this.label) {
+ this.label.offset = this.strokes.radius
+ }
- if (this.label) {
- this.label.offset = this.strokes.radius
- this.label.moveTo(this.x, this.y)
+ this.hitArea.update(this.x, this.y, radius)
}
- this.icon?.moveTo(this.x, this.y)
- this.hitArea.update(this.x, this.y, radius)
+ return this
+ }
+
+ private moveTo(x: number, y: number) {
+ if (x !== this.x || y !== this.y) {
+ this.x = x
+ this.y = y
+ this.fill.moveTo(x, y)
+ this.strokes.moveTo(x, y)
+ this.label?.moveTo(x, y)
+ this.icon?.moveTo(x, y)
+ this.hitArea.update(x, y, this.radius)
+ }
+
+ return this
}
private visible() {
diff --git a/src/renderers/webgl/objects/circle/Circle.ts b/src/renderers/webgl/objects/circle/Circle.ts
index 3595c4c..87b6e85 100644
--- a/src/renderers/webgl/objects/circle/Circle.ts
+++ b/src/renderers/webgl/objects/circle/Circle.ts
@@ -2,6 +2,7 @@ import { FillStyle, RenderObject } from '../../../../types'
import { DEFAULT_FILL_STYLE } from '../../../../utils/constants'
import { Container, Sprite } from 'pixi.js'
import CircleTexture from '../../textures/CircleTexture'
+import { isNumber } from '../../../../utils/helpers'
export default class Circle implements RenderObject {
mounted = false
@@ -16,21 +17,14 @@ export default class Circle implements RenderObject {
constructor(
private container: Container,
private texture: CircleTexture,
- color?: string,
- opacity?: number
+ index?: number
) {
this.container = container
this.texture = texture
-
- this.style = {
- color: color ?? this.style.color,
- opacity: opacity ?? this.style.opacity
- }
-
- this.object = this.create()
+ this.object = this.create(index)
}
- update(color = this.style.color, opacity = this.style.opacity) {
+ update(color = DEFAULT_FILL_STYLE.color, opacity = DEFAULT_FILL_STYLE.opacity) {
if (color !== this.style.color) {
this.style.color = color
this.object.tint = color
@@ -110,15 +104,22 @@ export default class Circle implements RenderObject {
return this._radius
}
- private create() {
+ private create(index?: number) {
const object = new Sprite(this.texture.get())
+
object.anchor.set(0.5)
object.x = this._x
object.y = this._y
object.visible = this.mounted
object.tint = this.style.color
object.alpha = this.style.opacity
- this.container.addChild(object)
+
+ if (isNumber(index)) {
+ this.container.addChildAt(object, index)
+ } else {
+ this.container.addChild(object)
+ }
+
return object
}
}
diff --git a/src/renderers/webgl/objects/circle/CircleStrokes.ts b/src/renderers/webgl/objects/circle/CircleStrokes.ts
index 6ba9a53..2021375 100644
--- a/src/renderers/webgl/objects/circle/CircleStrokes.ts
+++ b/src/renderers/webgl/objects/circle/CircleStrokes.ts
@@ -1,75 +1,82 @@
-import { FillStyle, RenderObject } from '../../../../types'
-import { DEFAULT_FILL_STYLE } from '../../../../utils/constants'
-import { Container, Sprite } from 'pixi.js'
+import { RenderObject, Stroke } from '../../../../types'
+import { Container } from 'pixi.js'
+import { equals } from '../../../../utils/api'
import CircleTexture from '../../textures/CircleTexture'
+import Circle from './Circle'
export default class CircleStrokes implements RenderObject {
mounted = false
- private _x = 0
- private _y = 0
- private _radius = 0
-
- private object: Sprite
- private style: Required = DEFAULT_FILL_STYLE
+ private x = 0
+ private y = 0
+ private minRadius = 0
+ private maxRadius = 0
+ private objects: Circle[] = []
+ private strokes: Stroke[] = []
constructor(
private container: Container,
private texture: CircleTexture,
- style?: FillStyle
+ private fill: Circle
) {
this.container = container
this.texture = texture
-
- if (style) {
- this.style = { ...DEFAULT_FILL_STYLE, ...style }
- }
-
- this.object = this.create()
+ this.fill = fill
+ this.minRadius = fill.radius
+ this.maxRadius = fill.radius
}
- update(color = this.style.color, opacity = this.style.opacity) {
- if (color !== this.style.color) {
- this.style.color = color
- this.object.tint = color
- }
+ update(strokes: Stroke[] = []) {
+ if (!equals(this.strokes, strokes)) {
+ const isMounted = this.mounted
- if (opacity !== this.style.opacity) {
- this.style.opacity = opacity
- this.object.alpha = opacity
+ this.delete()
+ this.applyStrokes(strokes)
+
+ if (isMounted) {
+ this.mount()
+ }
}
return this
}
moveTo(x: number, y: number) {
- if (x !== this.x) {
- this._x = x
- this.object.x = x
- }
+ const dirty = x !== this.x || y !== this.y
+
+ if (dirty) {
+ this.x = x
+ this.y = y
- if (y !== this.y) {
- this._y = y
- this.object.y = y
+ for (const object of this.objects) {
+ object.moveTo(x, y)
+ }
}
return this
}
resize(radius: number) {
- if (this._radius !== radius) {
- this._radius = radius
- this.object.scale.set(radius / this.texture.scaleFactor)
+ if (radius !== this.minRadius) {
+ this.minRadius = radius
+ this.maxRadius = radius
+
+ for (let i = 0; i < this.strokes.length; i += 1) {
+ this.maxRadius += this.strokes[i].width
+ this.objects[i].resize(this.maxRadius)
+ }
}
return this
}
mount() {
- // TODO - why is mounting/unmouting fill Sprite less efficient?
if (!this.mounted) {
this.mounted = true
- this.object.visible = true
+
+ for (const object of this.objects) {
+ object.mount()
+ }
}
return this
@@ -78,41 +85,46 @@ export default class CircleStrokes implements RenderObject {
unmount() {
if (this.mounted) {
this.mounted = false
- this.object.visible = false
+
+ for (const object of this.objects) {
+ object.unmount()
+ }
}
return this
}
delete() {
- this.unmount()
- this.container.removeChild(this.object)
- this.object.destroy()
+ this.mounted = false
- return undefined
- }
+ for (const object of this.objects) {
+ object.delete()
+ }
- get x() {
- return this._x
- }
+ this.strokes = []
+ this.objects = []
+ this.maxRadius = 0
- get y() {
- return this._y
+ return undefined
}
get radius() {
- return this._radius
+ return this.maxRadius
}
- private create() {
- const object = new Sprite(this.texture.get())
- object.anchor.set(0.5)
- object.x = this._x
- object.y = this._y
- object.visible = this.mounted
- object.tint = this.style.color
- object.alpha = this.style.opacity
- this.container.addChild(object)
- return object
+ private applyStrokes(strokes: Stroke[]) {
+ this.objects = []
+ this.strokes = strokes
+ this.maxRadius = this.minRadius
+
+ const index = this.fill.getContainerIndex()
+
+ for (const { color, width } of strokes) {
+ this.maxRadius += width
+ const object = new Circle(this.container, this.texture, index)
+ this.objects.push(object.update(color).resize(this.maxRadius).moveTo(this.x, this.y))
+ }
+
+ return this
}
}
From a821fcbda65fbbf2b89cbadfe171fde672af09e4 Mon Sep 17 00:00:00 2001
From: Mikey Gower
Date: Tue, 13 Feb 2024 13:48:16 -0500
Subject: [PATCH 04/12] geometry objects: Rectangle and RectangleStrokes
---
src/renderers/webgl/objects/circle/Circle.ts | 24 ++--
.../webgl/objects/circle/CircleStrokes.ts | 13 +-
src/renderers/webgl/objects/nodeStrokes.ts | 105 --------------
.../webgl/objects/rectangle/Rectangle.ts | 110 ++++++++++++++-
.../objects/rectangle/RectangleStrokes.ts | 129 +++++++++++++++++-
5 files changed, 242 insertions(+), 139 deletions(-)
delete mode 100644 src/renderers/webgl/objects/nodeStrokes.ts
diff --git a/src/renderers/webgl/objects/circle/Circle.ts b/src/renderers/webgl/objects/circle/Circle.ts
index 87b6e85..b79cb67 100644
--- a/src/renderers/webgl/objects/circle/Circle.ts
+++ b/src/renderers/webgl/objects/circle/Circle.ts
@@ -7,8 +7,8 @@ import { isNumber } from '../../../../utils/helpers'
export default class Circle implements RenderObject {
mounted = false
- private _x = 0
- private _y = 0
+ private x = 0
+ private y = 0
private _radius = 0
private object: Sprite
@@ -40,12 +40,12 @@ export default class Circle implements RenderObject {
moveTo(x: number, y: number) {
if (x !== this.x) {
- this._x = x
+ this.x = x
this.object.x = x
}
if (y !== this.y) {
- this._y = y
+ this.y = y
this.object.y = y
}
@@ -65,7 +65,7 @@ export default class Circle implements RenderObject {
// TODO - why is mounting/unmouting fill Sprite less efficient?
if (!this.mounted) {
this.mounted = true
- this.object.visible = true
+ this.object.visible = this.mounted
}
return this
@@ -74,7 +74,7 @@ export default class Circle implements RenderObject {
unmount() {
if (this.mounted) {
this.mounted = false
- this.object.visible = false
+ this.object.visible = this.mounted
}
return this
@@ -92,14 +92,6 @@ export default class Circle implements RenderObject {
return this.container.getChildIndex(this.object)
}
- get x() {
- return this._x
- }
-
- get y() {
- return this._y
- }
-
get radius() {
return this._radius
}
@@ -108,8 +100,8 @@ export default class Circle implements RenderObject {
const object = new Sprite(this.texture.get())
object.anchor.set(0.5)
- object.x = this._x
- object.y = this._y
+ object.x = this.x
+ object.y = this.y
object.visible = this.mounted
object.tint = this.style.color
object.alpha = this.style.opacity
diff --git a/src/renderers/webgl/objects/circle/CircleStrokes.ts b/src/renderers/webgl/objects/circle/CircleStrokes.ts
index 2021375..c4da0f1 100644
--- a/src/renderers/webgl/objects/circle/CircleStrokes.ts
+++ b/src/renderers/webgl/objects/circle/CircleStrokes.ts
@@ -62,8 +62,7 @@ export default class CircleStrokes implements RenderObject {
this.maxRadius = radius
for (let i = 0; i < this.strokes.length; i += 1) {
- this.maxRadius += this.strokes[i].width
- this.objects[i].resize(this.maxRadius)
+ this.objects[i].resize(this.increment(this.strokes[i].width))
}
}
@@ -103,7 +102,7 @@ export default class CircleStrokes implements RenderObject {
this.strokes = []
this.objects = []
- this.maxRadius = 0
+ this.maxRadius = this.minRadius
return undefined
}
@@ -112,6 +111,11 @@ export default class CircleStrokes implements RenderObject {
return this.maxRadius
}
+ private increment(width: number) {
+ this.maxRadius += width
+ return this.maxRadius
+ }
+
private applyStrokes(strokes: Stroke[]) {
this.objects = []
this.strokes = strokes
@@ -120,9 +124,8 @@ export default class CircleStrokes implements RenderObject {
const index = this.fill.getContainerIndex()
for (const { color, width } of strokes) {
- this.maxRadius += width
const object = new Circle(this.container, this.texture, index)
- this.objects.push(object.update(color).resize(this.maxRadius).moveTo(this.x, this.y))
+ this.objects.push(object.update(color).resize(this.increment(width)).moveTo(this.x, this.y))
}
return this
diff --git a/src/renderers/webgl/objects/nodeStrokes.ts b/src/renderers/webgl/objects/nodeStrokes.ts
deleted file mode 100644
index cd6968e..0000000
--- a/src/renderers/webgl/objects/nodeStrokes.ts
+++ /dev/null
@@ -1,105 +0,0 @@
-import { Container, Sprite } from 'pixi.js'
-import CircleTexture from '../textures/CircleTexture'
-import type { NodeStyle } from '../../../types'
-
-export class NodeStrokes {
- mounted = false
- radius = 0
- sprites?: Sprite[] // TODO - make private
-
- private style?: NodeStyle
-
- constructor(
- private container: Container,
- private circleTexture: CircleTexture,
- private fill: { getContainerIndex: () => number }
- ) {
- this.container = container
- this.circleTexture = circleTexture
- this.fill = fill
- }
-
- update(x: number, y: number, radius: number, style?: NodeStyle) {
- if (style?.stroke !== this.style?.stroke) {
- // exit
- const isMounted = this.mounted
- this.delete()
-
- if (style?.stroke?.length) {
- // enter
-
- this.sprites = Array(style.stroke.length)
-
- this.radius = radius
-
- for (let i = 0; i < style.stroke.length; i++) {
- this.radius += style.stroke[i].width
- const circle = new Sprite(this.circleTexture.get())
- circle.anchor.set(0.5)
- circle.scale.set(this.radius / this.circleTexture.scaleFactor)
- circle.tint = style.stroke[i].color
- circle.x = x
- circle.y = y
- this.sprites[i] = circle
- }
- if (isMounted) {
- this.mount()
- }
- }
- } else if (this.sprites && this.style?.stroke) {
- // reposition
- this.radius = radius
-
- for (let i = 0; i < this.sprites.length; i++) {
- this.radius += this.style.stroke[i].width
- const scale = this.radius / this.circleTexture.scaleFactor
- if (scale !== this.sprites[i].scale.x) {
- this.sprites[i].scale.set(this.radius / this.circleTexture.scaleFactor)
- }
- this.sprites[i].x = x
- this.sprites[i].y = y
- }
- }
-
- this.style = style
-
- return this
- }
-
- mount() {
- if (!this.mounted && this.sprites) {
- const strokeContainerIndex = this.fill.getContainerIndex()
-
- for (let i = this.sprites.length - 1; i >= 0; i--) {
- this.container.addChildAt(this.sprites[i], strokeContainerIndex)
- }
- this.mounted = true
- }
-
- return this
- }
-
- unmount() {
- if (this.mounted && this.sprites) {
- for (let i = this.sprites.length - 1; i >= 0; i--) {
- this.container.removeChild(this.sprites[i])
- }
- this.mounted = false
- }
-
- return this
- }
-
- delete() {
- this.radius = 0
- this.unmount()
-
- if (this.sprites) {
- for (const stroke of this.sprites) {
- stroke.destroy()
- }
- }
-
- return undefined
- }
-}
diff --git a/src/renderers/webgl/objects/rectangle/Rectangle.ts b/src/renderers/webgl/objects/rectangle/Rectangle.ts
index 089d210..ddf2474 100644
--- a/src/renderers/webgl/objects/rectangle/Rectangle.ts
+++ b/src/renderers/webgl/objects/rectangle/Rectangle.ts
@@ -1,20 +1,118 @@
-import { RenderObject } from '../../../../types'
+import { Dimensions, FillStyle, RenderObject } from '../../../../types'
+import { Container, Sprite } from 'pixi.js'
+import { DEFAULT_FILL_STYLE } from '../../../../utils/constants'
+import { isNumber } from '../../../../utils/helpers'
+import RectangleTexture from '../../textures/RectangleTexture'
export default class Rectangle implements RenderObject {
mounted = false
- moveTo(): this {
+ private x = 0
+ private y = 0
+ private width = 0
+ private height = 0
+ private object: Sprite
+ private style: Required = DEFAULT_FILL_STYLE
+
+ constructor(
+ private container: Container,
+ private texture: RectangleTexture,
+ index?: number
+ ) {
+ this.container = container
+ this.texture = texture
+ this.object = this.create(index)
+ }
+
+ update(color = DEFAULT_FILL_STYLE.color, opacity = DEFAULT_FILL_STYLE.opacity) {
+ if (color !== this.style.color) {
+ this.style.color = color
+ this.object.tint = color
+ }
+
+ if (opacity !== this.style.opacity) {
+ this.style.opacity = opacity
+ this.object.alpha = opacity
+ }
+
return this
}
+
+ moveTo(x: number, y: number) {
+ if (x !== this.x) {
+ this.x = x
+ this.object.x = x
+ }
+
+ if (y !== this.y) {
+ this.y = y
+ this.object.y = y
+ }
+
+ return this
+ }
+
+ resize({ width, height }: Dimensions) {
+ if (this.width !== width || this.height !== height) {
+ this.width = width
+ this.height = height
+ this.object.scale.set(width / this.texture.scaleFactor, height / this.texture.scaleFactor)
+ }
+
+ return this
+ }
+
mount() {
- this.mounted = true
+ // TODO - why is mounting/unmouting fill Sprite less efficient?
+ if (!this.mounted) {
+ this.mounted = true
+ this.object.visible = this.mounted
+ }
+
return this
}
- unmount(): this {
- this.mounted = false
+
+ unmount() {
+ if (this.mounted) {
+ this.mounted = false
+ this.object.visible = this.mounted
+ }
+
return this
}
- delete(): void {
+
+ delete() {
this.unmount()
+ this.container.removeChild(this.object)
+ this.object.destroy()
+
+ return undefined
+ }
+
+ getContainerIndex() {
+ return this.container.getChildIndex(this.object)
+ }
+
+ get size(): Dimensions {
+ return { width: this.width, height: this.height }
+ }
+
+ private create(index?: number) {
+ const object = new Sprite(this.texture.get())
+
+ object.anchor.set(0.5)
+ object.x = this.x
+ object.y = this.y
+ object.visible = this.mounted
+ object.tint = this.style.color
+ object.alpha = this.style.opacity
+
+ if (isNumber(index)) {
+ this.container.addChildAt(object, index)
+ } else {
+ this.container.addChild(object)
+ }
+
+ return object
}
}
diff --git a/src/renderers/webgl/objects/rectangle/RectangleStrokes.ts b/src/renderers/webgl/objects/rectangle/RectangleStrokes.ts
index 7c0bb01..5113ec9 100644
--- a/src/renderers/webgl/objects/rectangle/RectangleStrokes.ts
+++ b/src/renderers/webgl/objects/rectangle/RectangleStrokes.ts
@@ -1,20 +1,135 @@
-import { RenderObject } from '../../../../types'
+import { Dimensions, RenderObject, Stroke } from '../../../../types'
+import { Container } from 'pixi.js'
+import { equals } from '../../../../utils/api'
+import RectangleTexture from '../../textures/RectangleTexture'
+import Rectangle from './Rectangle'
export default class RectangleStrokes implements RenderObject {
mounted = false
- moveTo(): this {
+ private x = 0
+ private y = 0
+
+ private minSize: Dimensions = { width: 0, height: 0 }
+ private maxSize: Dimensions = { width: 0, height: 0 }
+ private objects: Rectangle[] = []
+ private strokes: Stroke[] = []
+
+ constructor(
+ private container: Container,
+ private texture: RectangleTexture,
+ private fill: Rectangle
+ ) {
+ this.container = container
+ this.texture = texture
+ this.fill = fill
+ this.minSize = this.fill.size
+ this.maxSize = this.minSize
+ }
+
+ update(strokes: Stroke[] = []) {
+ if (!equals(this.strokes, strokes)) {
+ const isMounted = this.mounted
+
+ this.delete()
+ this.applyStrokes(strokes)
+
+ if (isMounted) {
+ this.mount()
+ }
+ }
+
+ return this
+ }
+
+ moveTo(x: number, y: number) {
+ const dirty = x !== this.x || y !== this.y
+
+ if (dirty) {
+ this.x = x
+ this.y = y
+
+ for (const object of this.objects) {
+ object.moveTo(x, y)
+ }
+ }
+
+ return this
+ }
+
+ resize(width: number, height: number) {
+ if (width !== this.minSize.width || height !== this.minSize.height) {
+ this.minSize = { width, height }
+ this.maxSize = this.minSize
+
+ for (let i = 0; i < this.strokes.length; i += 1) {
+ this.objects[i].resize(this.increment(this.strokes[i].width))
+ }
+ }
+
return this
}
+
mount() {
- this.mounted = true
+ if (!this.mounted) {
+ this.mounted = true
+
+ for (const object of this.objects) {
+ object.mount()
+ }
+ }
+
return this
}
- unmount(): this {
- this.mounted = false
+
+ unmount() {
+ if (this.mounted) {
+ this.mounted = false
+
+ for (const object of this.objects) {
+ object.unmount()
+ }
+ }
+
return this
}
- delete(): void {
- this.unmount()
+
+ delete() {
+ this.mounted = false
+
+ for (const object of this.objects) {
+ object.delete()
+ }
+
+ this.strokes = []
+ this.objects = []
+ this.maxSize = this.minSize
+
+ return undefined
+ }
+
+ get size() {
+ return this.maxSize
+ }
+
+ private increment(value: number) {
+ this.maxSize.height += value
+ this.maxSize.width += value
+ return this.maxSize
+ }
+
+ private applyStrokes(strokes: Stroke[]) {
+ this.objects = []
+ this.strokes = strokes
+ this.maxSize = this.minSize
+
+ const index = this.fill.getContainerIndex()
+
+ for (const { color, width } of strokes) {
+ const object = new Rectangle(this.container, this.texture, index)
+ this.objects.push(object.update(color).resize(this.increment(width)).moveTo(this.x, this.y))
+ }
+
+ return this
}
}
From 7aadaf0adbc2fc25dd389f4e4d24bcba9c4d769b Mon Sep 17 00:00:00 2001
From: Mikey Gower
Date: Tue, 13 Feb 2024 15:38:34 -0500
Subject: [PATCH 05/12] LineSegment, LineStrokes for edges
---
examples/native/src/labels/index.ts | 55 +++++--
examples/native/src/perf/index.ts | 4 +-
src/renderers/webgl/LifecycleManager.ts | 9 +-
src/renderers/webgl/edge.ts | 97 ++++++------
src/renderers/webgl/node.ts | 2 +-
src/renderers/webgl/objects/arrow.ts | 81 +++++++---
src/renderers/webgl/objects/circle/Circle.ts | 17 +-
.../webgl/objects/circle/CircleStrokes.ts | 4 +-
.../webgl/objects/line/LineSegment.ts | 109 +++++++++++++
.../webgl/objects/line/LineStrokes.ts | 149 ++++++++++++++++++
src/renderers/webgl/objects/lineSegment.ts | 53 -------
.../webgl/objects/rectangle/Rectangle.ts | 16 +-
.../objects/rectangle/RectangleStrokes.ts | 4 +-
src/renderers/webgl/objects/text/Text.ts | 15 +-
.../webgl/objects/text/TextHighlight.ts | 13 +-
src/renderers/webgl/textures/TextTexture.ts | 3 +-
src/renderers/webgl/utils.ts | 6 +-
src/types/api.ts | 11 +-
src/types/internal.ts | 2 +
src/utils/constants.ts | 23 ++-
20 files changed, 475 insertions(+), 198 deletions(-)
create mode 100644 src/renderers/webgl/objects/line/LineSegment.ts
create mode 100644 src/renderers/webgl/objects/line/LineStrokes.ts
delete mode 100644 src/renderers/webgl/objects/lineSegment.ts
diff --git a/examples/native/src/labels/index.ts b/examples/native/src/labels/index.ts
index 87a0dc5..51f89f6 100644
--- a/examples/native/src/labels/index.ts
+++ b/examples/native/src/labels/index.ts
@@ -57,6 +57,28 @@ const NODE_HOVER_STYLE: Graph.NodeStyle = {
}
}
+const EDGE_STYLE: Graph.EdgeStyle = {
+ arrow: 'both',
+ label: {
+ fontName: 'EdgeLabel',
+ fontFamily: 'Roboto',
+ fontSize: 10,
+ color: DARK_GREEN,
+ margin: 4
+ }
+}
+const EDGE_HOVER_STYLE: Graph.EdgeStyle = {
+ arrow: 'both',
+ stroke: [{ width: 1, color: DARK_GREEN }],
+ label: {
+ fontName: 'EdgeLabel',
+ fontFamily: 'Roboto',
+ fontSize: 10,
+ color: DARK_GREEN,
+ margin: 4
+ }
+}
+
const data = [
'Myriel',
'Napoleon',
@@ -74,22 +96,13 @@ const data = [
const collide = Collide.Layout()
-const edges: Graph.Edge[] = [
+let edges: Graph.Edge[] = [
{
id: '0::1',
source: '0',
target: '1',
label: '0 <- EDGE LABEL -> 1',
- style: {
- arrow: 'both',
- label: {
- fontName: 'EdgeLabel',
- fontFamily: 'Roboto',
- fontSize: 10,
- color: DARK_GREEN,
- margin: 4
- }
- }
+ style: EDGE_STYLE
}
]
@@ -143,6 +156,26 @@ const options: Renderer.Options = {
nodes = nodes.map((node) =>
node.id === event.target.id ? { ...node, radius: 10, label: node.label?.slice(0, node.label.length - 3), style: NODE_STYLE } : node
)
+ renderer.update({ nodes, edges, options })
+ },
+ onEdgePointerEnter: (event: Renderer.EdgePointerEvent) => {
+ edges = edges.map((edge) => {
+ if (edge.id === event.target.id) {
+ return { ...edge, style: EDGE_HOVER_STYLE }
+ }
+ return edge
+ })
+
+ renderer.update({ nodes, edges, options })
+ },
+ onEdgePointerLeave: (event: Renderer.EdgePointerEvent) => {
+ edges = edges.map((edge) => {
+ if (edge.id === event.target.id) {
+ return { ...edge, style: EDGE_STYLE }
+ }
+ return edge
+ })
+
renderer.update({ nodes, edges, options })
}
}
diff --git a/examples/native/src/perf/index.ts b/examples/native/src/perf/index.ts
index 1555127..c56cb85 100644
--- a/examples/native/src/perf/index.ts
+++ b/examples/native/src/perf/index.ts
@@ -53,13 +53,13 @@ const NODE_HOVER_STYLE: Graph.NodeStyle = {
const EDGE_STYLE: Graph.EdgeStyle = {
width: 1,
- stroke: '#aaa',
+ color: '#aaa',
arrow: 'reverse'
}
const EDGE_HOVER_STYLE: Graph.EdgeStyle = {
width: 2,
- stroke: '#f66',
+ color: '#f66',
arrow: 'reverse'
}
diff --git a/src/renderers/webgl/LifecycleManager.ts b/src/renderers/webgl/LifecycleManager.ts
index 2cdce0d..6776b1b 100644
--- a/src/renderers/webgl/LifecycleManager.ts
+++ b/src/renderers/webgl/LifecycleManager.ts
@@ -1,15 +1,16 @@
-import { LineSegment } from './objects/lineSegment'
-import { Arrow } from './objects/arrow'
+import LineSegment from './objects/line/LineSegment'
+import LineStrokes from './objects/line/LineStrokes'
import ObjectManager from './objects/ObjectManager'
import CircleStrokes from './objects/circle/CircleStrokes'
import Circle from './objects/circle/Circle'
+import Arrow from './objects/Arrow'
import Icon from './objects/Icon'
import Text from './objects/text/Text'
export default class LifecycleManager {
- nodes = new ObjectManager(2000)
+ nodes = new ObjectManager(2000)
+ edges = new ObjectManager(2000)
icons = new ObjectManager(1000)
- edges = new ObjectManager(2000)
arrows = new ObjectManager(1000)
labels = new ObjectManager(2000)
interactions = new ObjectManager(2000)
diff --git a/src/renderers/webgl/edge.ts b/src/renderers/webgl/edge.ts
index 3a46e67..d6b6e2b 100644
--- a/src/renderers/webgl/edge.ts
+++ b/src/renderers/webgl/edge.ts
@@ -1,48 +1,49 @@
-import { DEFAULT_LABEL_STYLE, MIN_EDGES_ZOOM, MIN_INTERACTION_ZOOM, MIN_LABEL_ZOOM } from '../../utils/constants'
+import { DEFAULT_ARROW, DEFAULT_LABEL_STYLE, MIN_EDGES_ZOOM, MIN_INTERACTION_ZOOM, MIN_LABEL_ZOOM } from '../../utils/constants'
import { type Renderer } from '.'
import { midPoint } from './utils'
import { movePoint } from './utils'
import { NodeRenderer } from './node'
-import type { ArrowStyle, Edge } from '../../types'
-import { Arrow } from './objects/arrow'
-import { LineSegment } from './objects/lineSegment'
+import type { ArrowStyle, Edge, PointTuple } from '../../types'
+import Arrow from './objects/Arrow'
+import LineSegment from './objects/line/LineSegment'
+import LineStrokes from './objects/line/LineStrokes'
import { FederatedPointerEvent } from 'pixi.js'
import { EdgeHitArea } from './interaction/edgeHitArea'
-import { angle } from '../../utils/api'
+import { angle, distance } from '../../utils/api'
import Text from './objects/text/Text'
-const DEFAULT_EDGE_WIDTH = 1
-const DEFAULT_EDGE_COLOR = 0xaaaaaa
-const DEFAULT_ARROW = 'none'
-
export class EdgeRenderer {
edge!: Edge
source!: NodeRenderer
target!: NodeRenderer
- label?: Text
- renderer: Renderer
- lineSegment: LineSegment
- x0 = 0
- y0 = 0
- x1 = 0
- y1 = 0
- theta = 0
- center: [x: number, y: number] = [0, 0]
- width?: number
- stroke?: string | number
- strokeOpacity?: number
sourceRadius?: number
targetRadius?: number
+ private x0 = 0
+ private y0 = 0
+ private x1 = 0
+ private y1 = 0
+ private length = 0
+ private theta = 0
+ private center: PointTuple = [0, 0]
+ private lineSegment: LineSegment
+ private strokes: LineStrokes
private hitArea: EdgeHitArea
+ private label?: Text
private forwardArrow?: Arrow
private reverseArrow?: Arrow
private doubleClickTimeout: NodeJS.Timeout | undefined
private doubleClick = false
- constructor(renderer: Renderer, edge: Edge, source: NodeRenderer, target: NodeRenderer) {
+ constructor(
+ private renderer: Renderer,
+ edge: Edge,
+ source: NodeRenderer,
+ target: NodeRenderer
+ ) {
this.renderer = renderer
this.lineSegment = new LineSegment(this.renderer.edgesContainer)
+ this.strokes = new LineStrokes(this.renderer.edgesContainer, this.lineSegment)
this.hitArea = new EdgeHitArea(this.renderer.interactionContainer, this)
this.update(edge, source, target)
}
@@ -52,6 +53,9 @@ export class EdgeRenderer {
this.source = source
this.target = target
+ this.lineSegment.update(edge.style?.color, edge.style?.width, edge.style?.opacity)
+ this.strokes.update(edge.style?.stroke)
+
const arrow = edge.style?.arrow ?? DEFAULT_ARROW
if (arrow !== this.arrow) {
@@ -74,6 +78,9 @@ export class EdgeRenderer {
}
}
+ this.forwardArrow?.update(edge.style?.color, edge.style?.opacity)
+ this.reverseArrow?.update(edge.style?.color, edge.style?.opacity)
+
if (this.label) {
if (edge.label === undefined || edge.label.trim() === '') {
this.managers.labels.delete(this.label)
@@ -97,24 +104,14 @@ export class EdgeRenderer {
const isVisible = this.visible(x0, y0, x1, y1)
if (isVisible) {
- const width = this.edge?.style?.width ?? DEFAULT_EDGE_WIDTH
- const stroke = this.edge?.style?.stroke ?? DEFAULT_EDGE_COLOR
- const strokeOpacity = this.edge?.style?.strokeOpacity ?? 1
-
if (
x0 !== this.x0 ||
y0 !== this.y0 ||
x1 !== this.x1 ||
y1 !== this.y1 ||
sourceRadius !== this.sourceRadius ||
- targetRadius !== this.targetRadius ||
- width !== this.width ||
- stroke !== this.stroke ||
- strokeOpacity !== this.strokeOpacity
+ targetRadius !== this.targetRadius
) {
- this.width = width
- this.stroke = stroke
- this.strokeOpacity = strokeOpacity
this.sourceRadius = sourceRadius
this.targetRadius = targetRadius
this.x0 = x0
@@ -131,8 +128,8 @@ export class EdgeRenderer {
const edgePoint = movePoint(x1, y1, this.theta, this.targetRadius + this.forwardArrow.height)
edgeX1 = edgePoint[0]
edgeY1 = edgePoint[1]
- const [arrowX1, arrowY1] = movePoint(x1, y1, this.theta, this.targetRadius)
- this.forwardArrow.update(arrowX1, arrowY1, this.theta, this.stroke, this.strokeOpacity)
+
+ this.forwardArrow.rotate(this.theta).moveTo(...movePoint(x1, y1, this.theta, this.targetRadius))
} else {
const edgePoint = movePoint(x1, y1, this.theta, this.targetRadius)
edgeX1 = edgePoint[0]
@@ -143,8 +140,8 @@ export class EdgeRenderer {
const edgePoint = movePoint(x0, y0, this.theta, -this.sourceRadius - this.reverseArrow.height)
edgeX0 = edgePoint[0]
edgeY0 = edgePoint[1]
- const [arrowX0, arrowY0] = movePoint(x0, y0, this.theta, -this.sourceRadius)
- this.reverseArrow.update(arrowX0, arrowY0, this.theta + Math.PI, this.stroke, this.strokeOpacity)
+
+ this.reverseArrow.rotate(this.theta + Math.PI).moveTo(...movePoint(x0, y0, this.theta, -this.sourceRadius))
} else {
const edgePoint = movePoint(x0, y0, this.theta, -this.sourceRadius)
edgeX0 = edgePoint[0]
@@ -152,15 +149,17 @@ export class EdgeRenderer {
}
this.center = midPoint(edgeX0, edgeY0, edgeX1, edgeY1)
+ this.length = distance(edgeX0, edgeY0, edgeX1, edgeY1)
+
+ // TODO -> draw hitArea/strokes over arrows
+ this.lineSegment.rotate(this.theta).resize(this.length).moveTo(edgeX0, edgeY0)
+ this.strokes.rotate(this.theta).resize(this.length).moveTo(edgeX0, edgeY0)
+ this.hitArea.update(edgeX0, edgeY0, edgeX1, edgeY1, this.strokes.width, this.theta)
if (this.label) {
- this.label.rotation = this.theta
- this.label.moveTo(...this.center)
+ this.label.offset = this.strokes.width
+ this.label.rotate(this.theta).moveTo(...this.center)
}
-
- // TODO - draw hitArea over arrow
- this.lineSegment.update(edgeX0, edgeY0, edgeX1, edgeY1, this.width, this.stroke, this.strokeOpacity)
- this.hitArea.update(edgeX0, edgeY0, edgeX1, edgeY1, this.width, this.theta)
}
}
@@ -181,6 +180,13 @@ export class EdgeRenderer {
this.managers.edges.unmount(this.lineSegment)
}
+ const strokesMounted = this.managers.edges.isMounted(this.strokes)
+ if (isVisible && !strokesMounted) {
+ this.managers.edges.mount(this.strokes)
+ } else if (!isVisible && strokesMounted) {
+ this.managers.edges.unmount(this.strokes)
+ }
+
if (this.forwardArrow) {
const forwardArrowMounted = this.managers.arrows.isMounted(this.forwardArrow)
if (isVisible && !forwardArrowMounted) {
@@ -219,6 +225,7 @@ export class EdgeRenderer {
clearTimeout(this.doubleClickTimeout)
this.managers.edges.delete(this.lineSegment)
+ this.managers.edges.delete(this.strokes)
this.managers.interactions.delete(this.hitArea)
if (this.forwardArrow) {
this.managers.arrows.delete(this.forwardArrow)
@@ -428,8 +435,8 @@ export class EdgeRenderer {
const style = this.edge.style?.label
if (label !== undefined && label.trim() !== '' && this.label === undefined) {
this.label = new Text(this.renderer.assets, this.renderer.labelsContainer, label, style, DEFAULT_LABEL_STYLE)
- this.label.rotation = this.theta
- this.label.moveTo(...this.center)
+ this.label.offset = this.strokes.width
+ this.label.rotate(this.theta).moveTo(...this.center)
}
return this
diff --git a/src/renderers/webgl/node.ts b/src/renderers/webgl/node.ts
index 5e2d189..2a72378 100644
--- a/src/renderers/webgl/node.ts
+++ b/src/renderers/webgl/node.ts
@@ -43,7 +43,7 @@ export class NodeRenderer {
update(node: Node) {
this.node = node
- this.fill.update(node.style?.color)
+ this.fill.update(node.style?.color, node.style?.opacity)
this.strokes.update(node.style?.stroke)
if (this.label) {
diff --git a/src/renderers/webgl/objects/arrow.ts b/src/renderers/webgl/objects/arrow.ts
index 3895ba3..bcf2db5 100644
--- a/src/renderers/webgl/objects/arrow.ts
+++ b/src/renderers/webgl/objects/arrow.ts
@@ -1,38 +1,54 @@
import { Container, Sprite } from 'pixi.js'
+import { FillStyle, RenderObject } from '../../../types'
import ArrowTexture from '../textures/ArrowTexture'
+import { DEFAULT_FILL, DEFAULT_FILL_STYLE, DEFAULT_OPACITY } from '../../../utils/constants'
-export class Arrow {
+export default class Arrow implements RenderObject {
mounted = false
- height: number
- width: number
- private container: Container
- private arrowTexture: ArrowTexture
- private arrow: Sprite
+ private x = 0
+ private y = 0
+ private object: Sprite
+ private style: Required = DEFAULT_FILL_STYLE
- constructor(container: Container, arrowTexture: ArrowTexture) {
+ constructor(
+ private container: Container,
+ private texture: ArrowTexture
+ ) {
this.container = container
- this.arrowTexture = arrowTexture
- this.arrow = new Sprite(this.arrowTexture.get())
- this.height = this.arrowTexture.height
- this.width = this.arrowTexture.width
- this.arrow.anchor.set(0, 0.5)
- this.arrow.scale.set(1 / this.arrowTexture.scaleFactor)
+ this.texture = texture
+ this.object = this.create()
}
- update(x: number, y: number, rotation: number, color: string | number, opacity: number) {
- this.arrow.x = x
- this.arrow.y = y
- this.arrow.rotation = rotation
- this.arrow.tint = color
- this.arrow.alpha = opacity
+ update(color = DEFAULT_FILL, opacity = DEFAULT_OPACITY) {
+ this.style = { color, opacity }
+ this.object.tint = this.style.color
+ this.object.alpha = this.style.opacity
+ return this
+ }
+
+ moveTo(x: number, y: number) {
+ if (x !== this.x) {
+ this.x = x
+ this.object.x = x
+ }
+
+ if (y !== this.y) {
+ this.y = y
+ this.object.y = y
+ }
+
+ return this
+ }
+ rotate(angle: number) {
+ this.object.rotation = angle
return this
}
mount() {
if (!this.mounted) {
- this.container.addChild(this.arrow)
+ this.container.addChild(this.object)
this.mounted = true
}
@@ -41,7 +57,7 @@ export class Arrow {
unmount() {
if (this.mounted) {
- this.container.removeChild(this.arrow)
+ this.container.removeChild(this.object)
this.mounted = false
}
@@ -50,8 +66,29 @@ export class Arrow {
delete() {
this.unmount()
- this.arrow.destroy()
+ this.object.destroy()
return undefined
}
+
+ get width() {
+ return this.texture.width
+ }
+
+ get height() {
+ return this.texture.height
+ }
+
+ private get scale() {
+ return 1 / this.texture.scaleFactor
+ }
+
+ private create() {
+ const object = new Sprite(this.texture.get())
+ object.anchor.set(0, 0.5)
+ object.scale.set(this.scale)
+ object.tint = this.style.color
+ object.alpha = this.style.opacity
+ return object
+ }
}
diff --git a/src/renderers/webgl/objects/circle/Circle.ts b/src/renderers/webgl/objects/circle/Circle.ts
index b79cb67..2a6e974 100644
--- a/src/renderers/webgl/objects/circle/Circle.ts
+++ b/src/renderers/webgl/objects/circle/Circle.ts
@@ -1,5 +1,5 @@
import { FillStyle, RenderObject } from '../../../../types'
-import { DEFAULT_FILL_STYLE } from '../../../../utils/constants'
+import { DEFAULT_FILL, DEFAULT_FILL_STYLE, DEFAULT_OPACITY } from '../../../../utils/constants'
import { Container, Sprite } from 'pixi.js'
import CircleTexture from '../../textures/CircleTexture'
import { isNumber } from '../../../../utils/helpers'
@@ -24,17 +24,10 @@ export default class Circle implements RenderObject {
this.object = this.create(index)
}
- update(color = DEFAULT_FILL_STYLE.color, opacity = DEFAULT_FILL_STYLE.opacity) {
- if (color !== this.style.color) {
- this.style.color = color
- this.object.tint = color
- }
-
- if (opacity !== this.style.opacity) {
- this.style.opacity = opacity
- this.object.alpha = opacity
- }
-
+ update(color = DEFAULT_FILL, opacity = DEFAULT_OPACITY) {
+ this.style = { color, opacity }
+ this.object.tint = this.style.color
+ this.object.alpha = this.style.opacity
return this
}
diff --git a/src/renderers/webgl/objects/circle/CircleStrokes.ts b/src/renderers/webgl/objects/circle/CircleStrokes.ts
index c4da0f1..a6e5983 100644
--- a/src/renderers/webgl/objects/circle/CircleStrokes.ts
+++ b/src/renderers/webgl/objects/circle/CircleStrokes.ts
@@ -123,9 +123,9 @@ export default class CircleStrokes implements RenderObject {
const index = this.fill.getContainerIndex()
- for (const { color, width } of strokes) {
+ for (const { width, color, opacity } of strokes) {
const object = new Circle(this.container, this.texture, index)
- this.objects.push(object.update(color).resize(this.increment(width)).moveTo(this.x, this.y))
+ this.objects.push(object.update(color, opacity).resize(this.increment(width)).moveTo(this.x, this.y))
}
return this
diff --git a/src/renderers/webgl/objects/line/LineSegment.ts b/src/renderers/webgl/objects/line/LineSegment.ts
new file mode 100644
index 0000000..c025fe0
--- /dev/null
+++ b/src/renderers/webgl/objects/line/LineSegment.ts
@@ -0,0 +1,109 @@
+import { Container, Sprite, Texture } from 'pixi.js'
+import { Dimensions, RenderObject, Stroke } from '../../../../types'
+import { DEFAULT_FILL, DEFAULT_OPACITY, DEFAULT_STROKE_STYLE, DEFAULT_STROKE_WIDTH, HALF_PI } from '../../../../utils/constants'
+import { isNumber } from '../../../../utils/helpers'
+
+// TODO -> let LineSegment own arrow rendering
+export default class LineSegment implements RenderObject {
+ mounted = false
+
+ private x = 0
+ private y = 0
+ private length = 0
+ private object: Sprite
+ private style: Required = DEFAULT_STROKE_STYLE
+
+ constructor(private container: Container) {
+ this.container = container
+ this.object = this.create()
+ }
+
+ update(color = DEFAULT_FILL, width = DEFAULT_STROKE_WIDTH, opacity = DEFAULT_OPACITY) {
+ this.style = { color, width, opacity }
+ this.object.tint = color
+ this.object.width = width
+ this.object.alpha = opacity
+
+ return this
+ }
+
+ rotate(angle: number) {
+ this.object.rotation = angle + HALF_PI
+ return this
+ }
+
+ resize(length: number) {
+ if (length !== this.length) {
+ this.length = length
+ this.object.height = length
+ }
+
+ return this
+ }
+
+ moveTo(x: number, y: number) {
+ if (x !== this.x) {
+ this.x = x
+ this.object.x = x
+ }
+ if (y !== this.y) {
+ this.y = y
+ this.object.y = y
+ }
+
+ return this
+ }
+
+ mount(index?: number) {
+ if (!this.mounted) {
+ this.mounted = true
+
+ if (isNumber(index)) {
+ this.container.addChildAt(this.object, index)
+ } else {
+ this.container.addChild(this.object)
+ }
+ }
+
+ return this
+ }
+
+ unmount() {
+ if (this.mounted) {
+ this.mounted = false
+ this.container.removeChild(this.object)
+ }
+
+ return this
+ }
+
+ delete() {
+ this.unmount()
+ this.object.destroy()
+
+ return undefined
+ }
+
+ getContainerIndex() {
+ if (this.mounted) {
+ return this.container.getChildIndex(this.object)
+ }
+
+ return -1
+ }
+
+ get size(): Dimensions {
+ return { width: this.style.width, height: this.length }
+ }
+
+ private create() {
+ const object = new Sprite(Texture.WHITE)
+ object.tint = this.style.color
+ object.alpha = this.style.opacity
+ object.width = this.style.width
+ object.height = this.length
+ object.anchor.set(0.5, 0)
+
+ return object
+ }
+}
diff --git a/src/renderers/webgl/objects/line/LineStrokes.ts b/src/renderers/webgl/objects/line/LineStrokes.ts
new file mode 100644
index 0000000..49dbfeb
--- /dev/null
+++ b/src/renderers/webgl/objects/line/LineStrokes.ts
@@ -0,0 +1,149 @@
+import { RenderObject, Stroke } from '../../../../types'
+import { Container } from 'pixi.js'
+import { equals } from '../../../../utils/api'
+import LineSegment from './LineSegment'
+
+export default class LineStrokes implements RenderObject {
+ mounted = false
+
+ private x = 0
+ private y = 0
+ private length = 0
+ private angle = 0
+ private minWidth = 0
+ private maxWidth = 0
+ private objects: LineSegment[] = []
+ private strokes: Stroke[] = []
+
+ constructor(
+ private container: Container,
+ private fill: LineSegment
+ ) {
+ this.container = container
+ this.fill = fill
+ this.minWidth = this.fill.size.width
+ this.maxWidth = this.minWidth
+ }
+
+ update(strokes: Stroke[] = []) {
+ if (!equals(this.strokes, strokes)) {
+ const isMounted = this.mounted
+
+ this.delete()
+ this.applyStrokes(strokes)
+
+ if (isMounted) {
+ this.mount()
+ }
+ } else if (this.fill.size.width !== this.minWidth) {
+ this.minWidth = this.fill.size.width
+ this.maxWidth = this.minWidth
+
+ for (let i = 0; i < this.strokes.length; i += 1) {
+ const { color, width, opacity } = this.strokes[i]
+ this.objects[i].update(color, this.increment(width), opacity)
+ }
+ }
+
+ return this
+ }
+
+ moveTo(x: number, y: number) {
+ const dirty = x !== this.x || y !== this.y
+
+ if (dirty) {
+ this.x = x
+ this.y = y
+
+ for (const object of this.objects) {
+ object.moveTo(x, y)
+ }
+ }
+
+ return this
+ }
+
+ resize(length: number) {
+ if (length !== this.length) {
+ this.length = length
+
+ for (const object of this.objects) {
+ object.resize(length)
+ }
+ }
+
+ return this
+ }
+
+ rotate(angle: number) {
+ if (angle !== this.angle) {
+ this.angle = angle
+ for (const object of this.objects) {
+ object.rotate(angle)
+ }
+ }
+ return this
+ }
+
+ mount() {
+ if (!this.mounted) {
+ this.mounted = true
+
+ const index = this.fill.getContainerIndex()
+ for (const object of this.objects) {
+ object.mount(index)
+ }
+ }
+
+ return this
+ }
+
+ unmount() {
+ if (this.mounted) {
+ this.mounted = false
+
+ for (const object of this.objects) {
+ object.unmount()
+ }
+ }
+
+ return this
+ }
+
+ delete() {
+ this.mounted = false
+
+ for (const object of this.objects) {
+ object.delete()
+ }
+
+ this.strokes = []
+ this.objects = []
+ this.maxWidth = this.minWidth
+
+ return undefined
+ }
+
+ get width() {
+ return this.maxWidth
+ }
+
+ private increment(value: number) {
+ this.maxWidth += value
+ return this.maxWidth
+ }
+
+ private applyStrokes(strokes: Stroke[]) {
+ this.objects = []
+ this.strokes = strokes
+ this.minWidth = this.fill.size.width
+ this.maxWidth = this.minWidth
+
+ for (const { color, width, opacity } of strokes) {
+ const object = new LineSegment(this.container)
+ this.objects.push(object.update(color, this.increment(width), opacity).rotate(this.angle).resize(this.length).moveTo(this.x, this.y))
+ }
+
+ return this
+ }
+}
diff --git a/src/renderers/webgl/objects/lineSegment.ts b/src/renderers/webgl/objects/lineSegment.ts
deleted file mode 100644
index a8f5596..0000000
--- a/src/renderers/webgl/objects/lineSegment.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import { Container, Sprite, Texture } from 'pixi.js'
-import { HALF_PI } from '../utils'
-import { angle, distance } from '../../../utils/api'
-
-export class LineSegment {
- mounted = false
-
- private container: Container
- private lineSegment: Sprite
-
- constructor(container: Container) {
- this.container = container
- this.lineSegment = new Sprite(Texture.WHITE)
- }
-
- update(x0: number, y0: number, x1: number, y1: number, width: number, color: string | number, opacity: number) {
- this.lineSegment.x = x0
- this.lineSegment.y = y0
- this.lineSegment.width = width
- this.lineSegment.height = distance(x0, y0, x1, y1)
- this.lineSegment.rotation = angle(x0, y0, x1, y1) + HALF_PI
- this.lineSegment.tint = color
- this.lineSegment.alpha = opacity
- this.lineSegment.anchor.set(0.5, 0)
-
- return this
- }
-
- mount() {
- if (!this.mounted) {
- this.container.addChild(this.lineSegment)
- this.mounted = true
- }
-
- return this
- }
-
- unmount() {
- if (this.mounted) {
- this.container.removeChild(this.lineSegment)
- this.mounted = false
- }
-
- return this
- }
-
- delete() {
- this.unmount()
- this.lineSegment.destroy()
-
- return undefined
- }
-}
diff --git a/src/renderers/webgl/objects/rectangle/Rectangle.ts b/src/renderers/webgl/objects/rectangle/Rectangle.ts
index ddf2474..4221982 100644
--- a/src/renderers/webgl/objects/rectangle/Rectangle.ts
+++ b/src/renderers/webgl/objects/rectangle/Rectangle.ts
@@ -1,6 +1,6 @@
import { Dimensions, FillStyle, RenderObject } from '../../../../types'
import { Container, Sprite } from 'pixi.js'
-import { DEFAULT_FILL_STYLE } from '../../../../utils/constants'
+import { DEFAULT_FILL, DEFAULT_FILL_STYLE, DEFAULT_OPACITY } from '../../../../utils/constants'
import { isNumber } from '../../../../utils/helpers'
import RectangleTexture from '../../textures/RectangleTexture'
@@ -24,16 +24,10 @@ export default class Rectangle implements RenderObject {
this.object = this.create(index)
}
- update(color = DEFAULT_FILL_STYLE.color, opacity = DEFAULT_FILL_STYLE.opacity) {
- if (color !== this.style.color) {
- this.style.color = color
- this.object.tint = color
- }
-
- if (opacity !== this.style.opacity) {
- this.style.opacity = opacity
- this.object.alpha = opacity
- }
+ update(color = DEFAULT_FILL, opacity = DEFAULT_OPACITY) {
+ this.style = { color, opacity }
+ this.object.tint = color
+ this.object.alpha = opacity
return this
}
diff --git a/src/renderers/webgl/objects/rectangle/RectangleStrokes.ts b/src/renderers/webgl/objects/rectangle/RectangleStrokes.ts
index 5113ec9..69e67ec 100644
--- a/src/renderers/webgl/objects/rectangle/RectangleStrokes.ts
+++ b/src/renderers/webgl/objects/rectangle/RectangleStrokes.ts
@@ -125,9 +125,9 @@ export default class RectangleStrokes implements RenderObject {
const index = this.fill.getContainerIndex()
- for (const { color, width } of strokes) {
+ for (const { width, color, opacity } of strokes) {
const object = new Rectangle(this.container, this.texture, index)
- this.objects.push(object.update(color).resize(this.increment(width)).moveTo(this.x, this.y))
+ this.objects.push(object.update(color, opacity).resize(this.increment(width)).moveTo(this.x, this.y))
}
return this
diff --git a/src/renderers/webgl/objects/text/Text.ts b/src/renderers/webgl/objects/text/Text.ts
index b523e39..8acff5e 100644
--- a/src/renderers/webgl/objects/text/Text.ts
+++ b/src/renderers/webgl/objects/text/Text.ts
@@ -1,4 +1,4 @@
-import type { Bounds, TextStyle, TextObject, FontWeight, RenderObject } from '../../../../types'
+import type { Bounds, TextStyle, TextObject, FontWeight, RenderObject, PointTuple } from '../../../../types'
import { BitmapText, Container, Text as PixiText } from 'pixi.js'
import { equals } from '../../../../utils/api'
import TextTexture, { TextTextureOptions } from '../../textures/TextTexture'
@@ -169,15 +169,10 @@ export default class Text implements RenderObject {
this.font = undefined
}
- get rotation() {
- return this.object.rotation
- }
-
- set rotation(rotation: number) {
+ rotate(rotation: number) {
this.object.rotation = rotation
- if (this.highlight) {
- this.highlight.rotation = rotation
- }
+ this.highlight?.rotate(rotation)
+ return this
}
get rect() {
@@ -264,7 +259,7 @@ export default class Text implements RenderObject {
return this
}
- private offsetPosition(x: number, y: number, offset: number): [x: number, y: number] {
+ private offsetPosition(x: number, y: number, offset: number): PointTuple {
switch (this.style.position) {
case 'bottom':
return [x, y + offset]
diff --git a/src/renderers/webgl/objects/text/TextHighlight.ts b/src/renderers/webgl/objects/text/TextHighlight.ts
index d7ecba1..b5d6b33 100644
--- a/src/renderers/webgl/objects/text/TextHighlight.ts
+++ b/src/renderers/webgl/objects/text/TextHighlight.ts
@@ -84,6 +84,11 @@ export default class TextHighlight implements RenderObject {
return undefined
}
+ rotate(rotation: number) {
+ this.object.rotation = rotation
+ return this
+ }
+
get text() {
return this.textObject
}
@@ -96,14 +101,6 @@ export default class TextHighlight implements RenderObject {
return this.object.anchor
}
- get rotation() {
- return this.object.rotation
- }
-
- set rotation(rotation: number) {
- this.object.rotation = rotation
- }
-
private create() {
const object = Sprite.from(Texture.WHITE)
object.width = this.width
diff --git a/src/renderers/webgl/textures/TextTexture.ts b/src/renderers/webgl/textures/TextTexture.ts
index 9d09e9b..a19048e 100644
--- a/src/renderers/webgl/textures/TextTexture.ts
+++ b/src/renderers/webgl/textures/TextTexture.ts
@@ -3,6 +3,7 @@ import { TextStyle as PixiTextStyle, ITextStyle as IPixiTextStyle, BitmapFont, I
import { DEFAULT_HIGHLIGHT_STYLE, DEFAULT_TEXT_STYLE, MIN_TEXTURE_ZOOM, DEFAULT_RESOLUTION } from '../../../utils/constants'
import { isNumber } from '../../../utils/helpers'
import { equals } from '../../../utils/api'
+import { PointTuple } from '../../../types'
export type TextTextureOptions = {
defaultTextStyle?: Omit
@@ -116,7 +117,7 @@ export default class TextTexture {
}
}
- get anchor(): [x: number, y: number] {
+ get anchor(): PointTuple {
switch (this.position) {
case 'bottom':
return [0.5, 0]
diff --git a/src/renderers/webgl/utils.ts b/src/renderers/webgl/utils.ts
index f96be92..412adaf 100644
--- a/src/renderers/webgl/utils.ts
+++ b/src/renderers/webgl/utils.ts
@@ -1,5 +1,5 @@
/* eslint-disable no-console */
-import type { Node } from '../../types'
+import type { Node, PointTuple } from '../../types'
export const HALF_PI = Math.PI / 2
@@ -19,11 +19,11 @@ export const logUnknownEdgeError = (source: Node | undefined, target: Node | und
}
}
-export const movePoint = (x: number, y: number, angle: number, distance: number): [x: number, y: number] => [
+export const movePoint = (x: number, y: number, angle: number, distance: number): PointTuple => [
x + Math.cos(angle) * distance,
y + Math.sin(angle) * distance
]
-export const midPoint = (x0: number, y0: number, x1: number, y1: number): [x: number, y: number] => [(x0 + x1) / 2, (y0 + y1) / 2]
+export const midPoint = (x0: number, y0: number, x1: number, y1: number): PointTuple => [(x0 + x1) / 2, (y0 + y1) / 2]
export const length = (x0: number, y0: number, x1: number, y1: number) => Math.hypot(x1 - x0, y1 - y0)
diff --git a/src/types/api.ts b/src/types/api.ts
index 78aaae5..4adbed3 100644
--- a/src/types/api.ts
+++ b/src/types/api.ts
@@ -8,7 +8,7 @@ export type Viewport = { x: number; y: number; zoom: number }
// style
export type FillStyle = { color: string; opacity?: number }
-export type Stroke = { color: string; width: number }
+export type Stroke = FillStyle & { width: number }
export type FontWeight = 'normal' | 'bold' | 'bolder' | 'lighter' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'
@@ -58,8 +58,7 @@ export type TextIcon = IconBase<'textIcon'> & {
export type IconStyle = ImageIcon | TextIcon
// nodes
-export type NodeStyle = {
- color?: string
+export type NodeStyle = Partial & {
icon?: IconStyle
stroke?: Stroke[]
badge?: {
@@ -96,10 +95,8 @@ export type EdgeLabelStyle = LabelStyle & {
position?: Exclude
}
-export type EdgeStyle = {
- width?: number
- stroke?: string
- strokeOpacity?: number
+export type EdgeStyle = Partial & {
+ stroke?: Stroke[]
arrow?: ArrowStyle
label?: EdgeLabelStyle
}
diff --git a/src/types/internal.ts b/src/types/internal.ts
index 71a61c1..cfc3fe1 100644
--- a/src/types/internal.ts
+++ b/src/types/internal.ts
@@ -6,6 +6,8 @@ export type Extend = {
export type TextObject = PixiText | BitmapText
+export type PointTuple = [x: number, y: number]
+
export type Texture = {
get(...args: unknown[]): RenderTexture
delete(): void
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index 160cdda..75843c8 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -1,3 +1,9 @@
+// math
+export const HALF_PI = Math.PI / 2
+export const TWO_PI = Math.PI * 2
+export const THREE_HALF_PI = HALF_PI * 3
+export const RADIANS_PER_DEGREE = Math.PI / 180
+
// zoom limits
// TODO - extends to renderer options
export const MIN_LABEL_ZOOM = 0.25
@@ -8,20 +14,29 @@ export const MIN_EDGES_ZOOM = 0.1
export const MIN_TEXTURE_ZOOM = 3
// style
-export const DEFAULT_RESOLUTION = 2
-export const DEFAULT_OPACITY = 1
-
export const COLORS = {
BLACK: '#000000',
WHITE: '#FFFFFF',
GREY: '#AAAAAA'
}
+export const DEFAULT_RESOLUTION = 2
+export const DEFAULT_FILL = COLORS.GREY
+export const DEFAULT_OPACITY = 1
+export const DEFAULT_STROKE_WIDTH = 1
+export const DEFAULT_ARROW = 'none'
+
export const DEFAULT_FILL_STYLE = {
- color: COLORS.GREY,
+ color: DEFAULT_FILL,
opacity: DEFAULT_OPACITY
}
+export const DEFAULT_STROKE_STYLE = {
+ color: DEFAULT_FILL,
+ opacity: DEFAULT_OPACITY,
+ width: DEFAULT_STROKE_WIDTH
+}
+
export const DEFAULT_TEXT_STYLE = {
margin: 2,
fontSize: 10,
From 3eb1d1e7669bba87e4515f2631567fea149ff3bc Mon Sep 17 00:00:00 2001
From: Mikey Gower
Date: Wed, 14 Feb 2024 10:17:07 -0500
Subject: [PATCH 06/12] begin implementing AnnotationsRenderer
---
examples/native/index.html | 3 +
.../native/src/annotations/annotations.html | 14 ++
examples/native/src/annotations/index.ts | 79 +++++++
src/renderers/webgl/LifecycleManager.ts | 6 +
.../webgl/annotations/AnnotationsRenderer.ts | 67 ++++++
.../annotations/CircleAnnotationRenderer.ts | 48 ++++
.../RectangleAnnotationRenderer.ts | 221 ++++++++++++++++++
src/renderers/webgl/edge.ts | 14 +-
src/renderers/webgl/index.ts | 15 +-
src/renderers/webgl/node.ts | 10 +-
.../webgl/objects/rectangle/Rectangle.ts | 6 +-
.../objects/rectangle/RectangleStrokes.ts | 16 +-
src/renderers/webgl/textures/TextTexture.ts | 2 +-
src/types/internal.ts | 2 +
src/utils/constants.ts | 6 +-
15 files changed, 489 insertions(+), 20 deletions(-)
create mode 100644 examples/native/src/annotations/annotations.html
create mode 100644 examples/native/src/annotations/index.ts
create mode 100644 src/renderers/webgl/annotations/AnnotationsRenderer.ts
create mode 100644 src/renderers/webgl/annotations/CircleAnnotationRenderer.ts
create mode 100644 src/renderers/webgl/annotations/RectangleAnnotationRenderer.ts
diff --git a/examples/native/index.html b/examples/native/index.html
index fae189c..9cad059 100644
--- a/examples/native/index.html
+++ b/examples/native/index.html
@@ -16,6 +16,9 @@ Native Examples
Label Styles
+
+ Annotations
+
+
+
+
diff --git a/examples/native/src/annotations/annotations.html b/examples/native/src/annotations/annotations.html
new file mode 100644
index 0000000..f9c4e1e
--- /dev/null
+++ b/examples/native/src/annotations/annotations.html
@@ -0,0 +1,14 @@
+
+
+