# Animated lines with WebGL

**Important notes:**

- This sample shows experimental functionality, please read the documentation carefully before using it in a product.
- This sample is written for expert developers familiar with WebGL and hardware-accelerated rendering.

This sample demonstrates how to implement an animatedtrail (or flow) effect for polyline graphics. This description assumes familiarity with WebGL and custom WebGL layer views. It's similar to the Custom WebGL layer view sample, which triangulates points into quads. This sample instead uses *polyline triangulation*.

The `updatePositions()`

method has been modified to convert thegeometry of polyline graphics into triangle meshes. Special *per-vertex* attributes are computed and stored on the GPU and thenused in conjunction with *per-frame* uniform values to implement an efficient animation system that can support thousands of trailsat the same time.

In this sample we tessellated polylines using custom code; version 4.14 of the ArcGIS API introduces generic tessellation routines that the developer can use to tessellate arbitrary geometries; see the SDK tessellation sample for more details.

## Creating the mesh

WebGL draw calls operate on geometric primitives. While in WebGL there is support for the `gl.LINES`

primitive, this is not very flexibleand cannot be used to implement advanced effects such as the one showcased in this sample. The most flexible primitive is the indexedtriangle list, which can be rendered using `gl.drawElements(gl.TRIANGLES, ...)`

. The sample application renders all polylines in theview using one single such call. To feed the rasterization we need to set up appropriate vertex attributes and an index buffer.

### Triangulating lines

A polyline can be represented using triangles. A simple triangulation scheme is the one in which every vertex of the original polylineis *extruded* into two GPU vertices in opposite directions. Groups of four GPU vertices are then connected using two trianglesfor a total of six indices.

The geometry of the extrusion is exemplified by the figure below. Let `α`

be the angle of a turn, and `w`

the widthof the polyline on screen, in pixels. The extrusion direction forms an angle of `α / 2`

with each of the normalsto the segments; the amount of the extrusion is `w / (2 cos(α / 2))`

.

We use vector algebra to study vertex extrusion. Let's consider three consecutive vertices `a`

, `b`

and `c`

of a polyline andsuppose that we want to compute the *offset* vector that drives the extrusion of point `b`

. Let's consider segments `ab`

and `bc`

separately. We compute the vector that goes from `a`

to `b`

as `Δ1 = b - a`

; let `(dx, dy)`

= `Δ1 / ||Δ1||`

be the normalizedvector that expresses the direction of segment `ab`

; then `n1 = (-dy, dx)`

is the normal to the segment. Normal `n2`

can becomputed in the same way for segment `bc`

. The direction of the offset vector can then be computed as the normalized average of`n1`

and `n2`

, *i.e.* `offsetDir = (n1 + n2) / ||n1 + n2||`

.

We have characterized the direction of the extrusion, but not its amount; intuitively, a sharp turn should result in a larger extrusionthan a shallow one; also, we should extrude more if we want the polyline to appear thicker. Let's normalize the width of thepolyline to `2`

so that its edges just touch the tips of the normal vectors. Consider the right triangle that has `n1`

(or `n2`

) and `offset`

assides. It is easy to see that `offset cos(α / 2)`

must equal `1`

because the normal is unit length; from this we can conclude thelength of the offset vector is exactly `1 / cos(α / 2)`

. For a polyline of width `w`

we have to scale the offset by a factor of`w / 2`

, which leads to the `w / (2 cos(α / 2))`

factor we introduced at the beginning of this section.

Each polyline graphic is processed once, and the extrusion information is captured in attributes stored in a vertex buffer. Theextruded vertices are connected into triangles using an index buffer. These generated buffers drive the rasterization process anddo not need to be regenerated at each frame. We regenerate them only when the view becomes stationary to account for the limited precision offloating-point numbers on GPUs. See Custom WebGL layer view for a discussion of this technique.

### Vertex format

Each vertex of the original polyline results in two GPU vertices. These have a *position* attribute, which is the same as the vertexthey originate from and use the same units of the spatial reference. GPU vertices also have an *offset* attribute, which is a unit vector thatpoints toward the extruded GPU vertex. The two offset vectors in a pair are opposite of each other. Note that we store _normalized_offset vectors. These are the offsets that would make a polyline appear having width equal to `2`

pixels. The vertex shader is responsiblefor scaling the offset attribute so the polyline renders thicker.

To apply a texture to the line, we need to build a UV space on it. This requires two new attributes:

*distance*- Defines a coordinate that increases*along*the line. It is measured in the spatial reference units and starts a 0 on the first vertex and increases witheach polyline segment.*side*- This is set to`+1`

or`-1`

depending on the edge of the extruded polyline the GPU vertex belongs to.

We want every line to be of a different color. We could use a uniform to store the color, but this would prevent us from drawing all linesusing a single call. Therefore, we encode the color as a 32-bit RGBA attribute.

### The triangulation algorithm

The polyline triangulation algorithm is implemented in the `updatePositions()`

method and assumes thatall graphics stored on the layer have polyline geometries.

We start by counting the number of vertices and indices required, which we do by iterating on all graphics. For simplicity, we onlytriangulate the first path of each graphic. A polyline with `N`

vertices has `N - 1`

segments. For each vertex we need 2GPU vertices extruded in opposite directions. For each segment we need two triangles (i.e. 6 indices).

We allocate an `ArrayBuffer`

that has enough space for `vtxCount`

vertices. Every vertex has 6 floating-point values and a 4 bytes color, so we need a total of `7 * vtxCount * 4`

bytes. We create two views to this area of memory; one is used to write floating-point values, and the other is used to write colors. We also allocate the index memory and contextually create an unsigned short view to it.

Then we start computing and writing the vertex attributes. Writing to vertex and index memory happensat the locations pointed by two cursors, starting at zero.

Then we iterate on all graphics, considering only the first path of each graphic. For each path we need to run the triangulationalgorithm. We start by initializing a variable `s`

with an empty triangulation state, `{}`

; this state will be mutated as the individual pointsthat form a path are processed. Every time that we finish processing a graphic, we reset the state `s`

to `{}`

before we processthe next one.

To process the current vertex `p`

we first check the state `s`

. If this is not the firstiteration for this path, we'll already have the `s.current`

vertex and use it to compute the `delta`

as `p - s.current`

. Then we compute the length of the segment as the norm of `delta`

(the normalized `delta`

is the direction of the segment).By rotating this direction by 90° we obtain the normal to the segment.

For the first and last vertex of the polyline, the segment normal is the offset vectorthat determines the extrusion. For all other intermediate vertices, the normals of thetwo segments that share a vertex must be averaged, normalized, and scaled by the inverse of thecosine of half the angle between the normals. This can be computed as the dot productbetween a segment normal and the normalized (but still unscaled) offset. The previous segment normalis retrieved from the triangulation state as `s.normal`

.

The computed values are then written to attribute buffers. We use the floating-point view to writeposition, offset, distance and side, and we use the unsigned byte view to write the color.

After we have emitted at least four vertices, we can start emitting indices. At every iteration we emit six indices - two trianglesconnecting the two extruded GPU vertices just computed to the ones that were computed in the previous iteration.

Before continuing on to the next iteration, we need to make the latest point, the latest computed normal, the current ones, and incrementthe distance by the length of the segment that was processed by this iteration.

There are two special cases that we have only briefly mentioned:

- The offset of the first vertex is equal to the first computed normal (i.e.
`var normal = [-s.direction[1], s.direction[0]]`

) because there is no previous normal to average it. - Conversely, the offset of the last vertex is equal to the last normal computed when processing the last segment and is recovered by the state (i.e.
`s.normal`

).

To help you understand the triangulation logic, we prepared a pen that showcases theinner working of the algorithm in the context of a simple Canvas2D app.

## Creating the trail effect using shaders

In the present section we briefly discuss the vertex and fragment shader that implement the colored trail effect.

### Vertex shader

The original vertex is transformed by the *transform* matrix, which is determined by map center, scale, and rotation. Then the vertexis extruded by adding the offset vector scaled and rotated by the *extrude* matrix, which includes a half line width factor butis insensitive to map scale so that lines do not get larger as the view is zoomed in. Finally, the extruded vector is transformedby the *display* matrix, which is determined by the size of the viewport. The distance, side, and color attributes are passedunchanged to the fragment shader.

The interpolator will cause the fragments `(v_distance, v_side)`

pair to vary smoothly so that `v_side`

equals `0`

on the centerline while `v_distance`

is `0`

at one end of the polyline and equals the length of the polyline in map units at the other end.

### Fragment shader

The fragment shader computes two opacity factors, `a1`

and `a2`

, based on the position of the fragment *along* and _across_the line respectively. This information is captured by the interpolated `(v_distance, v_side)`

pair as detailed in the previousparagraph. Factor `a1`

is dependent on the current time and the distance associated to the current fragment, while `a2`

is such that the line is more opaque near the centerline and more transparent near the edges. We use a `mod`

operation so that the trails repeat spatially along each line, and each line never runs out of trails to display.