# Vugel
Vugel is a WebGL-powered 2d renderer for Vue 3. It provides an alternative to DOM rendering, with less features, but better performance in terms of element creation and rendering.
Vugel is designed to be part of a 'normal' Vue 3 DOM-based application. This means that you can share the same state (vuex) and properties.
Typical use cases for Vugel include games and charts, or other many-elements cases where runtime performance is essential.
Vugel is based on the tree2d library, which is a high-performance WebGL UI library. Tree2d itself depends on flexbox.js library, which allows you to use flexbox in WebGL.
Vugel does not (directly) allow WebGL based 3d graphics. However, it does support custom shaders.
# Getting started
To get started, first install the Vugel package from NPM:
yarn add vugel
# OR npm install vugel
Vugel has a peer dependency on vue3. Make sure that the vue
(version 3) package is installed!
A repository containing several examples can be found on https://vugel-example.planning.nl.
# Web bundle
Have a look at this codepen on how to include Vugel.
# Webpack config
In order to use Vugel with SFC components, you need a custom vue-loader
, until this PR gets merged.
This can be installed as such:
yarn add git+https://github.com/Planning-nl/vue-loader.git#feat/multiple-compilers-build
# OR npm install git+https://github.com/Planning-nl/vue-loader.git#feat/multiple-compilers-build
Then you need to add the following snippet to the module.rules
array in your webpack.config.js
file:
{
test: /\.vue$/,
use: {
loader: "vue-loader",
options: {
templateCompilers: {
vugel: [ require("vugel"), {} ],
},
},
},
},
# Application
Working with Vugel is just like you'd normally do with HTML elements in Vue3. In general you can use all Vue3 features.
The available tags, events and attributes are different.
Vugel itself provides a component which spawns a HTMLCanvasElement
. To create a canvas with Vugel, you simply use
the vugel
component, as such:
<template>
<vugel :settings="{clearColor: 'black'}" style="width: 500px; height: 500px">
<my-vugel-root-component example-prop="hello world" />
</vugel>
</template>
<script lang="ts">
import { Vugel } from "vugel";
import { MyVugelRootComponent } from "MyVugelRootComponent.vue";
export default {
name: "MyBaseComponent",
components: { Vugel, MyVugelRootComponent },
};
</script>
WARNING
You should only specify your root component(s) (without using v-if
) in the <vugel>
tag, not Vugel tags. Vugel tags should only be placed
in pure Vugel components, because the Vue3 compiler currently doesn't support mixing HTML and Vugel tags within the same template.
WARNING
The vugel tag creates a HTMLCanvasElement
wrapped by a HTMLDivElement
. All styles are applied to this wrapper element.
The wrapper element needs to detect the width and height of the canvas automatically. For this purpose it specifies a
single inline style rule: position:relative
. This can potentially interfere with your own stylesheets.
The available Vugel :settings
are used for creating a new tree2d stage. They define, for example, the background of the
canvas and memory settings.
# Vugel Settings
Name | Type | Description |
---|---|---|
eventsTarget | HTMLElement | Target to bind event listeners to. Defaults to the canvas element |
clearColor | number or string or null | Background color |
gpuPixelsMemory | number | When this amount of pixels is reached, no longer used textures are cleaned up |
bufferMemory | number | Defines the amount of items that can be drawn on a single frame |
defaultFontFace | string[] | The default font face (+ fallbacks) to use |
useImageWorker | boolean | Use off-threaded image parsing (default is true ) |
autostart | boolean | When set to false , you must manually invoke stage.drawFrame() to re-render |
pixelRatio | number | The pixelRatio to use. It is advisable to use 1, 1.5 or 2.0 to avoid artifacts |
canvas2d | boolean | Enables canvas2d mode rendering, even when WebGL is available |
# Vugel components
To use custom SFC components which are fully rendered using Vugel, it is required to annotate those components with the
compiler
attribute:
<template compiler="vugel">
<container>
<rectangle :x="10" :y="10" :w="width" :h="100" color="#ff0000" />
<text :x="10" :y="140" :font-size="10" :font-weight="600">{{ exampleProp }}</text>
</container>
</template>
<script lang="ts">
import { ref } from "vue";
export default {
props: {
exampleProp: { type: String, default: "" },
},
setup() {
const width = ref(0);
setInterval(() => {
width.value++;
}, 20);
return { width };
},
};
</script>
# Nodes
Vugel provides a set of native tags and attributes comparable, but different, to HTML DOM. To view the available properties, please click on the tag name and view the source code of the class (and parent classes). Use the force, read the source!
Many of these tags are covered in the the vugel-examples repository.
# container
Contains other nodes. Comparable to a div
html element.
# picture
Comparable to the img
html element.
Loads and renders an image specified by the src
property.
Make sure that your pictures are local or allow CORS. This is required for WebGL to be able to use them.
# rectangle
A container that has a solid background or linear gradient. The color can be specified using the properties color
.
Gradients can be specified by setting the different corner point colors using color-top
, color-right
, color-bottom
,
color-left
etc.
TIP
These color properties are also available for all other renderable elements, such as image
and text
elements. In
that case it provides texture tinting.
# text
A simple piece of (non-wrapping) text. Several styling-related properties can be specified.
# paragraph
Breaks down the text contents by word and wraps it to the current width of the paragraph.
Note
The paragraph element currently only supports a single piece of text with the same styles.
# styled-retangle
While rectangle is a really lightweight element that is ultra fast to render, it offers features other than linear gradients.
The styled-rectangle
supports rounded edges, stroke and shadows. It needs to be created using a canvas2d and uploaded
to the GPU. This causes a hefty performance penalty (but only upon the first frame that it is shown).
# drawing
Allows you to use a canvas2d rendering context to draw a custom texture. The draw
event must be set with your
custom drawing function with the following type:
export type DrawingFunction = (options: DrawingFunctionOptions) => DrawingResult | Promise<DrawingResult>;
export type DrawingFunctionOptions = {
context: CanvasRenderingContext2D;
w: number;
h: number;
};
export type DrawingResult = {
permanent?: boolean;
renderInfo?: any;
texParams?: Record<GLenum, GLenum>;
texOptions?: {
format?: number;
internalFormat?: number;
type?: GLenum;
};
};
The draw
event will be invoked automatically when the element dimensions change.
You can trigger a manual redraw by invoking the
update()
method on a drawing tag. You can use aref
to obtain a reference to the element first.
# texture
There are advanced use cases where the drawing
tag is not flexible enough. For example if you'd want to upload an
UInt8Array as a WebGL texture source. Or when you want to reuse the same texture object for performance reasons.
The texture
tag allows you to use a tree2d native texture directly.
# svg
Renders an svg file (set by the src
property) for the available width and height.
# grayscale
The contents of this element are rendered in grayscale.
Note
Notice that the contents are pre-rendered to an 'intermediate' texture with the dimensions of this element. This means that if the node has no width or height, nothing will be visible!
# rounded
The contents of this element are rendered in a rounded shape. The corner radius can be specified the radius
property.
An intermediate texture is used, so you should make sure that the element has width and height.
# shader
The contents of this element are rendered using a tree2d native shader.
An intermediate texture is used, so you should make sure that the element has width and height.
# box-blur
The contents of this element are rendered box-blurred. Box blur is a very slight but high-performance blur effect.
# direct-container
In very specific cases you may wish to work around Vue templates/reactivity. Vue is great in almost all use cases, but direct mutations can be faster and offers more fine-grained control.
The direct container, in contrast to a normal container, allows you to add, remove and iterate over children directly
via several child-related methods. You can use the create(type: Constructor<Node>)
method to create nodes that can be
added to this direct container. These nodes are direct-containers themselves, allowing you to add children to them as
well.
For a list of child-mutating methods, see https://github.com/Planning-nl/vugel/blob/master/src/runtime/nodes/direct.ts#L11.
The
direct-container
tag should not contain children in a vue template.
# Layout
Vugel offers basic layouting that uses relative positioning. Additionally, it contains a flexbox layout engine which covers more advanced use cases.
# Positioning
The x
and y
properties can be used to position the node relative to the parent node.
A feature like
position:fixed
is not supported. We advice to use the vue Teleport feature.
Width and height can be set using w
and h
. When not specified (or set to 0
, the width and/or height of the current
texture (picture, text, drawing, styled-rectangle, etc) are used instead.
The mount-x
and mount-y
properties are used to specify how the node should be aligned on the specified x
and y
coordinates. When set to 0
, (x, y)
defines the position of the top-left corner of the node. When set to 1
it
defines the position of the bottom-right corner, and using a value of 0.5
for mount causes the node to be centered on
the (x, y)
coordinates. The mount
property is especially handy to align nodes for which the dimensions are not yet
known beforehand - or are dynamic.
# Relative functions
Like CSS has the calc(..)
expression, Vugel offers something similar known as relative functions. Using
the properties func-x
, func-y
, func-w
and func-h
you can calculate the coordinates and dimensions dynamically.
<rectangle func-x="0.5*w" func-y="0.5*h" func-w="0.25*w" func-h="Math.sqrt(0.25*w+0.5*h)+10" />
All relative functions receive arguments w
and h
, which refer to the parent's (calculated) width and height.
You can either use a string to provide the function body, or refer to an actual function (like
<rectangle :func-x="myFunc" ..
). String functions are usually more convenient. On top of that they are cached and reused, allowing them to be better optimized by the javascript engine.
# Layout-related properties
Property | type | CSS equivalent | Notes |
---|---|---|---|
x | number | Offset | |
y | number | ||
w | number | Size; when 0 then texture's dimensions are inherited | |
h | number | ||
func-x | (w: number, h: number) | When set, overrules the x property and (re)calculates it from the parent dimensions | |
func-y | (w: number, h: number) | ||
func-w | (w: number, h: number) | ||
func-h | (w: number, h: number) | ||
mount | number | Number between 0 and 1, sets both mount-x and mount-y | |
mount-x | number | Number between 0 and 1 | |
mount-y | number | Number between 0 and 1 |
# Layout-related methods
These Node methods can be used to obtain information on the Node's dynamic layout.
Method | Description |
---|---|
getLayoutX() : number | post-layout x position |
getLayoutY() : number | post-layout y position |
getLayoutW() : number | post-layout width |
getLayoutH() : number | post-layout height |
The getLayout methods will return the value that was calculated during the last update cycle (what is currently 'on screen'). Notice that you could manually force an update by invoking
node.stage.root.core.update()
;
# Layout-related events
Event | arg | Description |
---|---|---|
resize | { node: Node; stage: Stage; w: number; h: number } | Called whenever the node changes dimensions |
# Flexbox
Flexbox layouting is also supported. It's almost identical to the CSS Flexbox layout engine, but uses different properties.
# Flex container properties
These properties define the behavior of a flex container.
Property | Type | CSS equivalent | Notes |
---|---|---|---|
flex | true,false | When true, this node behaves as a flex container | |
flex-direction | 'row','row-reverse','column','column-reverse' | flex-direction | |
flex-wrap | true,false | flex-wrap | wrap-reverse is not supported |
flex-align-items | 'flex-start','flex-end','center','stretch' | align-items | baseline not supported |
flex-align-content | 'flex-start','flex-end','center','space-between','space-around','space-evenly','stretch' | align-content | |
flex-justify-content | 'flex-start','flex-end','center','space-between','space-around','space-evenly' | justify-content | |
flex-padding | number | padding | in pixels |
flex-padding-top | number | padding-top | |
flex-padding-left | number | padding-left | |
flex-padding-bottom | number | padding-bottom | |
flex-padding-right | number | padding-right |
When flex layout is active, the
x
andy
properties will be added to the positions calculated by the flexbox layout engine.
When the
w
and/orh
is set to the number0
, it will fit to the contents in those directions.
# Flex item properties
These properties define the behavior of the child items of a flex container.
Property | Type | CSS equivalent | Notes |
---|---|---|---|
flex-item | true,false | When false, this item will not affect the flex layout and will be positioned relatively | |
flex-grow | number | flex-grow | |
flex-shrink | number | flex-shrink | The default value is 0 (in CSS it defaults to 1) |
flex-align-self | 'flex-start','flex-end','center','stretch' | align-self | |
order | Not supported | ||
flex-basis | Not supported (behavior is always as flex-basis:auto ) | ||
min-width | number | min-width | in pixels |
max-width | number | max-width | in pixels |
min-height | number | min-height | in pixels |
max-height | number | max-height | in pixels |
margin | number | margin | in pixels |
margin-top | number | margin-top | |
margin-left | number | margin-left | |
margin-bottom | number | margin-bottom | |
margin-right | number | margin-right |
Notice that nodes with the
visibile
property set tofalse
are ignored in the layout. In contrast, nodes that havevisibile
ontrue
, but thealpha
property set to0
still take up space.
# Layout skipping
The skip-in-layout
property can be used to skip the node while layouting. It's pretty advanced and usually you
won't need it. It can be handy in combination with vue component slots. By setting skip-in-layout
to true on the nodes
between the component root and the slot itself, you could layout the slot children as if they were immediate children of the component.
In this mode, flexbox behaves as if this skipped node was replaced by it's own children. This means that if the node itself was a flex item of a flex container, it's children will now become flex items of that flex container.
It also affects relative layout function arguments (w
or h
). If the parent has skip-in-layout
set to true, the
grandparent's width and height will be used.
# Transforms
All Vugel nodes support a couple of linear transformations, available using a couple of properties:
Property | Type | Default | Notes |
---|---|---|---|
scale | number | 1.0 | Horizontal and vertical scaling |
scale-x | number | 1.0 | Horizontal scaling |
scale-y | number | 1.0 | Vertical scaling |
rotation | number | 0.0 | Rotation (radians) |
pivot | number | 0.5 | Horizontal and vertical pivot position |
pivot-x | number | 0.5 | Horizontal pivot position (0 = left, 0.5 = center, 1 = right) |
pivot-y | number | 0.5 | Vertical pivot position (0 = top, 0.5 = center, 1 = bottom) |
# Textures
All visible tags (picture
, rectangle
, text
, etc.) internally render a texture. You can control several aspects of
these textures and how they are being rendered.
# Visibility & tinting
All textures can be tinted to a specific color. Different corner points or sides can receive different colors to implement linear gradients.
Property | Type | Default | Notes |
---|---|---|---|
alpha | number | 0.0 | Opacity, between 0.0 and 1.0 |
visible | boolean | true | Visibility. When set to false, has same effect as display:none in CSS |
color | number or string | 0xffffffff | Color |
color-left | number or string | 0xffffffff | Left-side color |
color-right | number or string | 0xffffffff | Right-side color |
color-top | number or string | 0xffffffff | Top-side color |
color-bottom | number or string | 0xffffffff | Bottom-side color |
color-top-left | number or string | 0xffffffff | Top-left corner color |
color-top-right | number or string | 0xffffffff | Top-right corner color |
color-bottom-left | number or string | 0xffffffff | Bottom-left corner color |
color-bottom-right | number or string | 0xffffffff | Bottom-right corner color |
Note
The supported color formats are CSS colors, RGB(A), Hex and hexadecimal ARGB. Using hexadecimal ARGB will be the fastest, as it won't require a conversion.
# Texture clipping
Textures can be clipped. This means that, instead of the full texture, only a part of it is used and rendered. This allows you to implement a way of using spritemaps, for example.
Property | Type | Default | Notes |
---|---|---|---|
clip-x | number | 0.0 | Horizontal clipping position (in pixels) |
clip-y | number | 0.0 | Vertical clipping position (in pixels) |
clip-w | number | 0.0 | Horizontal clipping size (in pixels); when set to 0 , the texture width is used |
clip-h | number | 0.0 | Vertical clipping size (in pixels); when set to 0 , the texture height is used |
pixel-ratio | number | 1.0 | The pixel ratio of the texture. This 'scales' the texture. |
Notice that if the clipping region expands the actual texture source's bounds, they are capped on those bounds.
Clipping coordinates are
pixel-ratio
dependent.
# Clipping
The clipping
property results in the same effect as overflow:hidden
in CSS.
In Vugel, clipping is implemented using the WebGL
scissor()
operation. It is incredibly fast, but it doesn't support non-rectangular shapes. This means that theclipping
property won't work when the Vugel node (or one of its ancestors) is rotated. In this caserender-to-texture
can be used, which doesn't have this limitation. It achieves clipping by rendering the node to a separate texture and that makes it a bit slower.
# Z-Index
Defines the stacking order of elements, behaves exactly like its' CSS counterpart. You can use the z-index
property,
which takes a (floating point) number, to define a z-index. Notice that it is also possible to set a negative number, which means
that the texture should be 'behind' the z-index context root's texture (if it has one).
In CSS there are several rules for when a z-index context is created. In Vugel, a node is a z-index context if (and only if):
- non-zero
z-index
is specified force-z-index-context
is set totrue
render-to-texture
is enabled
# Cursor Type
The cursor that's shown in the browser when the mouse hovers a node can be controlled using the cursor-type
property. It takes a string in the same format as the CSS https://developer.mozilla.org/en-US/docs/Web/CSS/cursor property.
# Lifecycle Events
There are a couple of Node lifecycle-related methods available.
Example usage: <container @attach="doSomething">...</container>
.
Name | Event argument | Description |
---|---|---|
attach | { node: Node; stage: Stage } | Node becomes attached to the render tree |
setup | { node: Node; stage: Stage } | Node becomes attached to the render tree for the first time |
detach | { node: Node; stage: Stage } | Node becomes (no longer attached from the render tree) |
enabled | { node: Node; stage: Stage } | Node becomes (attached and visible) |
disabled | { node: Node; stage: Stage } | Node is no longer (attached and visible) |
active | { node: Node; stage: Stage } | Node becomes (attached and visible and within visible bounds) |
inactive | { node: Node; stage: Stage } | Node is no longer (attached and visible and within visible bounds) |
texture-error | { node: Node; stage: Stage; texture: Texture; error: Error } | An error occurred while loading the texture of this node |
texture-loaded | { node: Node; stage: Stage; texture: Texture } | Texture of this node was loaded |
texture-unloaded | { node: Node; stage: Stage; texture: Texture } | Texture of this node was unloaded (garbage collected) |
Visible bounds uses the viewport size and clipping to determine which elements are (partly) on screen. An additional visible bounds margin can be set using the
bounds-margin
property.
# UI Events
Vugel attempts to offer UI Events that are similar to DOM UI events.
However, there are some differences.
Every event has a different type than the original event. It was chosen to do it this way as to be able to ensure that the expected behavior is actually followed.
A VugelEvent
has the following properties:
cancelBubble: boolean
: Set totrue
to cancel event bubbeling.currentTarget: Node | null
: 'This' node (while bubbeling).target: Node | null
: The event target node (while bubbeling).type: string
: The event nameoriginalEvent: Event
: The original DOM Event is available using theoriginalEvent
property.
In HTML you can invoke
event.preventDefault()
to disable the default event. The same can be done in Vugel usingevent.originalEvent.preventDefault()
.
# Mouse events
All mouse events supported by DOM are supported by Vugel, and are translated into their own structure.
Supported mouse events:
mousemove
mouseover
mouseout
mouseenter
mouseleave
Every mouse event will additionally include the following fields:
canvasOffsetX
/canvasOffsetY
: the offset of the mouse w.r.t. the canvaselementOffsetX
/elementOffsetY
: the offset of the mouse w.r.t. the current elementcurrentElement: { offsetX: number; offsetY: number; element: Element; }
: The tree2d target and offset.
To get the relative offset to the event handling node use:
e.currentTarget!.getLocalOffset(e.canvasOffsetX, e.canvasOffsetY)
.
# Touch events
Touch events are not supported in the regular sense. Instead, we provide basic touch support by converting the first touch and translating it to the corresponding mouse event.
# Focus events
Vugel allows one node to be the focused node. The focused node, like in HTML, reveices keypresses.
You may set the focus by obtaining a reference to the node (using template refs) and invoking the focus()
method:
myRef.value.focus()
Supported events:
focusin
focusout
focus
blur
Additional event fields:
relatedTarget: Node | null
: The previous (in case offocusin
,focus
events) or new (focusout
,blur
) focused node.
# Keyboard events
Supported events:
keypress
keydown
keyup
You can find the key-related details in the originalEvent
.