@quartic/bokehjs
Version:
Interactive, novel data visualization
908 lines (798 loc) • 36 kB
text/coffeescript
import * as gloo2 from "gloo2"
import {logger} from "core/logging"
import {color2rgba} from "core/util/color"
import {BaseGLGlyph, line_width, attach_float, attach_color} from "./base"
class DashAtlas
constructor: (gl) ->
# Init texture
# Init with solid line (index 0 is reserved for this)
get_atlas_data: (pattern) ->
key = pattern.join('-')
findex_period = @_atlas[key]
if findex_period is undefined
[data, period] = @make_pattern(pattern)
return @_atlas[key]
make_pattern: (pattern) ->
# A pattern is defined as on/off sequence of segments
# It must be a multiple of 2
if pattern.length > 1 and pattern.length % 2
pattern = pattern.concat(pattern)
# Period is sum of elements
period = 0
for v in pattern
period += v
# Find all start and end of on-segment only
C = []; c = 0
for i in [0...pattern.length+2] by 2
a = Math.max(0.0001, pattern[i % pattern.length])
b = Math.max(0.0001, pattern[(i+1) % pattern.length])
C.push.apply(C, [c, c + a]) # == extend
c += a + b
# Build pattern
n = @_width
Z = new Float32Array(n * 4)
for i in [0...n]
x = period * i / (n-1)
# get index at min - index = np.argmin(abs(C-(x)))
index = 0; val_at_index = 1e16
for j in [0...C.length]
val = Math.abs(C[j]-x)
if val < val_at_index
index = j; val_at_index = val
if index % 2 == 0
dash_type = if (x <= C[index]) then +1 else 0
dash_start = C[index]; dash_end = C[index+1]
else
dash_type = if (x > C[index]) then -1 else 0
dash_start = C[index-1]; dash_end = C[index]
Z[i*4+0] = C[index]
Z[i*4+1] = dash_type
Z[i*4+2] = dash_start
Z[i*4+3] = dash_end
return [Z, period]
export class LineGLGlyph extends BaseGLGlyph
GLYPH: 'line'
JOINS:
'miter': 0, 'round': 1, 'bevel': 2
CAPS:
'': 0, 'none': 0, '.': 0,
'round': 1, ')': 1, '(': 1, 'o': 1,
'triangle in': 2, '<': 2,
'triangle out': 3, '>': 3,
'square': 4, '[': 4, ']': 4, '=': 4,
'butt': 5, '|': 5
VERT: """
precision mediump float;
const float PI = 3.14159265358979323846264;
const float THETA = 15.0 * 3.14159265358979323846264/180.0;
uniform float u_pixel_ratio;
uniform vec2 u_canvas_size, u_offset;
uniform vec2 u_scale_aspect;
uniform float u_scale_length;
uniform vec4 u_color;
uniform float u_antialias;
uniform float u_length;
uniform float u_linewidth;
uniform float u_dash_index;
uniform float u_closed;
attribute vec2 a_position;
attribute vec4 a_tangents;
attribute vec2 a_segment;
attribute vec2 a_angles;
attribute vec2 a_texcoord;
varying vec4 v_color;
varying vec2 v_segment;
varying vec2 v_angles;
varying vec2 v_texcoord;
varying vec2 v_miter;
varying float v_length;
varying float v_linewidth;
float cross(in vec2 v1, in vec2 v2)
{
return v1.x*v2.y - v1.y*v2.x;
}
float signed_distance(in vec2 v1, in vec2 v2, in vec2 v3)
{
return cross(v2-v1,v1-v3) / length(v2-v1);
}
void rotate( in vec2 v, in float alpha, out vec2 result )
{
float c = cos(alpha);
float s = sin(alpha);
result = vec2( c*v.x - s*v.y,
s*v.x + c*v.y );
}
void main()
{
bool closed = (u_closed > 0.0);
// Attributes and uniforms to varyings
v_color = u_color;
v_linewidth = u_linewidth;
v_segment = a_segment * u_scale_length;
v_length = u_length * u_scale_length;
// Scale to map to pixel coordinates. The original algorithm from the paper
// assumed isotropic scale. We obviously do not have this.
vec2 abs_scale_aspect = abs(u_scale_aspect);
vec2 abs_scale = u_scale_length * abs_scale_aspect;
// Correct angles for aspect ratio
vec2 av;
av = vec2(1.0, tan(a_angles.x)) / abs_scale_aspect;
v_angles.x = atan(av.y, av.x);
av = vec2(1.0, tan(a_angles.y)) / abs_scale_aspect;
v_angles.y = atan(av.y, av.x);
// Thickness below 1 pixel are represented using a 1 pixel thickness
// and a modified alpha
v_color.a = min(v_linewidth, v_color.a);
v_linewidth = max(v_linewidth, 1.0);
// If color is fully transparent we just will discard the fragment anyway
if( v_color.a <= 0.0 ) {
gl_Position = vec4(0.0,0.0,0.0,1.0);
return;
}
// This is the actual half width of the line
float w = ceil(u_antialias+v_linewidth)/2.0;
vec2 position = (a_position + u_offset) * abs_scale;
vec2 t1 = normalize(a_tangents.xy * abs_scale_aspect); // note the scaling for aspect ratio here
vec2 t2 = normalize(a_tangents.zw * abs_scale_aspect);
float u = a_texcoord.x;
float v = a_texcoord.y;
vec2 o1 = vec2( +t1.y, -t1.x);
vec2 o2 = vec2( +t2.y, -t2.x);
// This is a join
// ----------------------------------------------------------------
if( t1 != t2 ) {
float angle = atan (t1.x*t2.y-t1.y*t2.x, t1.x*t2.x+t1.y*t2.y); // Angle needs recalculation for some reason
vec2 t = normalize(t1+t2);
vec2 o = vec2( + t.y, - t.x);
if ( u_dash_index > 0.0 )
{
// Broken angle
// ----------------------------------------------------------------
if( (abs(angle) > THETA) ) {
position += v * w * o / cos(angle/2.0);
float s = sign(angle);
if( angle < 0.0 ) {
if( u == +1.0 ) {
u = v_segment.y + v * w * tan(angle/2.0);
if( v == 1.0 ) {
position -= 2.0 * w * t1 / sin(angle);
u -= 2.0 * w / sin(angle);
}
} else {
u = v_segment.x - v * w * tan(angle/2.0);
if( v == 1.0 ) {
position += 2.0 * w * t2 / sin(angle);
u += 2.0*w / sin(angle);
}
}
} else {
if( u == +1.0 ) {
u = v_segment.y + v * w * tan(angle/2.0);
if( v == -1.0 ) {
position += 2.0 * w * t1 / sin(angle);
u += 2.0 * w / sin(angle);
}
} else {
u = v_segment.x - v * w * tan(angle/2.0);
if( v == -1.0 ) {
position -= 2.0 * w * t2 / sin(angle);
u -= 2.0*w / sin(angle);
}
}
}
// Continuous angle
// ------------------------------------------------------------
} else {
position += v * w * o / cos(angle/2.0);
if( u == +1.0 ) u = v_segment.y;
else u = v_segment.x;
}
}
// Solid line
// --------------------------------------------------------------------
else
{
position.xy += v * w * o / cos(angle/2.0);
if( angle < 0.0 ) {
if( u == +1.0 ) {
u = v_segment.y + v * w * tan(angle/2.0);
} else {
u = v_segment.x - v * w * tan(angle/2.0);
}
} else {
if( u == +1.0 ) {
u = v_segment.y + v * w * tan(angle/2.0);
} else {
u = v_segment.x - v * w * tan(angle/2.0);
}
}
}
// This is a line start or end (t1 == t2)
// ------------------------------------------------------------------------
} else {
position += v * w * o1;
if( u == -1.0 ) {
u = v_segment.x - w;
position -= w * t1;
} else {
u = v_segment.y + w;
position += w * t2;
}
}
// Miter distance
// ------------------------------------------------------------------------
vec2 t;
vec2 curr = a_position * abs_scale;
if( a_texcoord.x < 0.0 ) {
vec2 next = curr + t2*(v_segment.y-v_segment.x);
rotate( t1, +v_angles.x/2.0, t);
v_miter.x = signed_distance(curr, curr+t, position);
rotate( t2, +v_angles.y/2.0, t);
v_miter.y = signed_distance(next, next+t, position);
} else {
vec2 prev = curr - t1*(v_segment.y-v_segment.x);
rotate( t1, -v_angles.x/2.0,t);
v_miter.x = signed_distance(prev, prev+t, position);
rotate( t2, -v_angles.y/2.0,t);
v_miter.y = signed_distance(curr, curr+t, position);
}
if (!closed && v_segment.x <= 0.0) {
v_miter.x = 1e10;
}
if (!closed && v_segment.y >= v_length)
{
v_miter.y = 1e10;
}
v_texcoord = vec2( u, v*w );
// Calculate position in device coordinates. Note that we
// already scaled with abs scale above.
vec2 normpos = position * sign(u_scale_aspect);
normpos += 0.5; // make up for Bokeh's offset
normpos /= u_canvas_size / u_pixel_ratio; // in 0..1
gl_Position = vec4(normpos*2.0-1.0, 0.0, 1.0);
gl_Position.y *= -1.0;
}
"""
FRAG_: """
// Fragment shader that can be convenient during debugging to show the line skeleton.
precision mediump float;
uniform vec4 u_color;
void main () {
gl_FragColor = u_color;
}
"""
FRAG: """
precision mediump float;
const float PI = 3.14159265358979323846264;
const float THETA = 15.0 * 3.14159265358979323846264/180.0;
uniform sampler2D u_dash_atlas;
uniform vec2 u_linecaps;
uniform float u_miter_limit;
uniform float u_linejoin;
uniform float u_antialias;
uniform float u_dash_phase;
uniform float u_dash_period;
uniform float u_dash_index;
uniform vec2 u_dash_caps;
uniform float u_closed;
varying vec4 v_color;
varying vec2 v_segment;
varying vec2 v_angles;
varying vec2 v_texcoord;
varying vec2 v_miter;
varying float v_length;
varying float v_linewidth;
// Compute distance to cap ----------------------------------------------------
float cap( int type, float dx, float dy, float t, float linewidth )
{
float d = 0.0;
dx = abs(dx);
dy = abs(dy);
if (type == 0) discard; // None
else if (type == 1) d = sqrt(dx*dx+dy*dy); // Round
else if (type == 3) d = (dx+abs(dy)); // Triangle in
else if (type == 2) d = max(abs(dy),(t+dx-abs(dy))); // Triangle out
else if (type == 4) d = max(dx,dy); // Square
else if (type == 5) d = max(dx+t,dy); // Butt
return d;
}
// Compute distance to join -------------------------------------------------
float join( in int type, in float d, in vec2 segment, in vec2 texcoord, in vec2 miter,
in float linewidth )
{
// texcoord.x is distance from start
// texcoord.y is distance from centerline
// segment.x and y indicate the limits (as for texcoord.x) for this segment
float dx = texcoord.x;
// Round join
if( type == 1 ) {
if (dx < segment.x) {
d = max(d,length( texcoord - vec2(segment.x,0.0)));
//d = length( texcoord - vec2(segment.x,0.0));
} else if (dx > segment.y) {
d = max(d,length( texcoord - vec2(segment.y,0.0)));
//d = length( texcoord - vec2(segment.y,0.0));
}
}
// Bevel join
else if ( type == 2 ) {
if (dx < segment.x) {
vec2 x = texcoord - vec2(segment.x,0.0);
d = max(d, max(abs(x.x), abs(x.y)));
} else if (dx > segment.y) {
vec2 x = texcoord - vec2(segment.y,0.0);
d = max(d, max(abs(x.x), abs(x.y)));
}
/* Original code for bevel which does not work for us
if( (dx < segment.x) || (dx > segment.y) )
d = max(d, min(abs(x.x),abs(x.y)));
*/
}
return d;
}
void main()
{
// If color is fully transparent we just discard the fragment
if( v_color.a <= 0.0 ) {
discard;
}
// Test if dash pattern is the solid one (0)
bool solid = (u_dash_index == 0.0);
// Test if path is closed
bool closed = (u_closed > 0.0);
vec4 color = v_color;
float dx = v_texcoord.x;
float dy = v_texcoord.y;
float t = v_linewidth/2.0-u_antialias;
float width = 1.0; //v_linewidth; original code had dashes scale with line width, we do not
float d = 0.0;
vec2 linecaps = u_linecaps;
vec2 dash_caps = u_dash_caps;
float line_start = 0.0;
float line_stop = v_length;
// Apply miter limit; fragments too far into the miter are simply discarded
if( (dx < v_segment.x) || (dx > v_segment.y) ) {
float into_miter = max(v_segment.x - dx, dx - v_segment.y);
if (into_miter > u_miter_limit*v_linewidth/2.0)
discard;
}
// Solid line --------------------------------------------------------------
if( solid ) {
d = abs(dy);
if( (!closed) && (dx < line_start) ) {
d = cap( int(u_linecaps.x), abs(dx), abs(dy), t, v_linewidth );
}
else if( (!closed) && (dx > line_stop) ) {
d = cap( int(u_linecaps.y), abs(dx)-line_stop, abs(dy), t, v_linewidth );
}
else {
d = join( int(u_linejoin), abs(dy), v_segment, v_texcoord, v_miter, v_linewidth );
}
// Dash line --------------------------------------------------------------
} else {
float segment_start = v_segment.x;
float segment_stop = v_segment.y;
float segment_center= (segment_start+segment_stop)/2.0;
float freq = u_dash_period*width;
float u = mod( dx + u_dash_phase*width, freq);
vec4 tex = texture2D(u_dash_atlas, vec2(u/freq, u_dash_index)) * 255.0 -10.0; // conversion to int-like
float dash_center= tex.x * width;
float dash_type = tex.y;
float _start = tex.z * width;
float _stop = tex.a * width;
float dash_start = dx - u + _start;
float dash_stop = dx - u + _stop;
// Compute extents of the first dash (the one relative to v_segment.x)
// Note: this could be computed in the vertex shader
if( (dash_stop < segment_start) && (dash_caps.x != 5.0) ) {
float u = mod(segment_start + u_dash_phase*width, freq);
vec4 tex = texture2D(u_dash_atlas, vec2(u/freq, u_dash_index)) * 255.0 -10.0; // conversion to int-like
dash_center= tex.x * width;
//dash_type = tex.y;
float _start = tex.z * width;
float _stop = tex.a * width;
dash_start = segment_start - u + _start;
dash_stop = segment_start - u + _stop;
}
// Compute extents of the last dash (the one relatives to v_segment.y)
// Note: This could be computed in the vertex shader
else if( (dash_start > segment_stop) && (dash_caps.y != 5.0) ) {
float u = mod(segment_stop + u_dash_phase*width, freq);
vec4 tex = texture2D(u_dash_atlas, vec2(u/freq, u_dash_index)) * 255.0 -10.0; // conversion to int-like
dash_center= tex.x * width;
//dash_type = tex.y;
float _start = tex.z * width;
float _stop = tex.a * width;
dash_start = segment_stop - u + _start;
dash_stop = segment_stop - u + _stop;
}
// This test if the we are dealing with a discontinuous angle
bool discontinuous = ((dx < segment_center) && abs(v_angles.x) > THETA) ||
((dx >= segment_center) && abs(v_angles.y) > THETA);
//if( dx < line_start) discontinuous = false;
//if( dx > line_stop) discontinuous = false;
float d_join = join( int(u_linejoin), abs(dy),
v_segment, v_texcoord, v_miter, v_linewidth );
// When path is closed, we do not have room for linecaps, so we make room
// by shortening the total length
if (closed) {
line_start += v_linewidth/2.0;
line_stop -= v_linewidth/2.0;
}
// We also need to take antialias area into account
//line_start += u_antialias;
//line_stop -= u_antialias;
// Check is dash stop is before line start
if( dash_stop <= line_start ) {
discard;
}
// Check is dash start is beyond line stop
if( dash_start >= line_stop ) {
discard;
}
// Check if current dash start is beyond segment stop
if( discontinuous ) {
// Dash start is beyond segment, we discard
if( (dash_start > segment_stop) ) {
discard;
//gl_FragColor = vec4(1.0,0.0,0.0,.25); return;
}
// Dash stop is before segment, we discard
if( (dash_stop < segment_start) ) {
discard; //gl_FragColor = vec4(0.0,1.0,0.0,.25); return;
}
// Special case for round caps (nicer with this)
if( dash_caps.x == 1.0 ) {
if( (u > _stop) && (dash_stop > segment_stop ) && (abs(v_angles.y) < PI/2.0)) {
discard;
}
}
// Special case for round caps (nicer with this)
if( dash_caps.y == 1.0 ) {
if( (u < _start) && (dash_start < segment_start ) && (abs(v_angles.x) < PI/2.0)) {
discard;
}
}
// Special case for triangle caps (in & out) and square
// We make sure the cap stop at crossing frontier
if( (dash_caps.x != 1.0) && (dash_caps.x != 5.0) ) {
if( (dash_start < segment_start ) && (abs(v_angles.x) < PI/2.0) ) {
float a = v_angles.x/2.0;
float x = (segment_start-dx)*cos(a) - dy*sin(a);
float y = (segment_start-dx)*sin(a) + dy*cos(a);
if( x > 0.0 ) discard;
// We transform the cap into square to avoid holes
dash_caps.x = 4.0;
}
}
// Special case for triangle caps (in & out) and square
// We make sure the cap stop at crossing frontier
if( (dash_caps.y != 1.0) && (dash_caps.y != 5.0) ) {
if( (dash_stop > segment_stop ) && (abs(v_angles.y) < PI/2.0) ) {
float a = v_angles.y/2.0;
float x = (dx-segment_stop)*cos(a) - dy*sin(a);
float y = (dx-segment_stop)*sin(a) + dy*cos(a);
if( x > 0.0 ) discard;
// We transform the caps into square to avoid holes
dash_caps.y = 4.0;
}
}
}
// Line cap at start
if( (dx < line_start) && (dash_start < line_start) && (dash_stop > line_start) ) {
d = cap( int(linecaps.x), dx-line_start, dy, t, v_linewidth);
}
// Line cap at stop
else if( (dx > line_stop) && (dash_stop > line_stop) && (dash_start < line_stop) ) {
d = cap( int(linecaps.y), dx-line_stop, dy, t, v_linewidth);
}
// Dash cap left - dash_type = -1, 0 or 1, but there may be roundoff errors
else if( dash_type < -0.5 ) {
d = cap( int(dash_caps.y), abs(u-dash_center), dy, t, v_linewidth);
if( (dx > line_start) && (dx < line_stop) )
d = max(d,d_join);
}
// Dash cap right
else if( dash_type > 0.5 ) {
d = cap( int(dash_caps.x), abs(dash_center-u), dy, t, v_linewidth);
if( (dx > line_start) && (dx < line_stop) )
d = max(d,d_join);
}
// Dash body (plain)
else {// if( dash_type > -0.5 && dash_type < 0.5) {
d = abs(dy);
}
// Line join
if( (dx > line_start) && (dx < line_stop)) {
if( (dx <= segment_start) && (dash_start <= segment_start)
&& (dash_stop >= segment_start) ) {
d = d_join;
// Antialias at outer border
float angle = PI/2.+v_angles.x;
float f = abs( (segment_start - dx)*cos(angle) - dy*sin(angle));
d = max(f,d);
}
else if( (dx > segment_stop) && (dash_start <= segment_stop)
&& (dash_stop >= segment_stop) ) {
d = d_join;
// Antialias at outer border
float angle = PI/2.+v_angles.y;
float f = abs((dx - segment_stop)*cos(angle) - dy*sin(angle));
d = max(f,d);
}
else if( dx < (segment_start - v_linewidth/2.)) {
discard;
}
else if( dx > (segment_stop + v_linewidth/2.)) {
discard;
}
}
else if( dx < (segment_start - v_linewidth/2.)) {
discard;
}
else if( dx > (segment_stop + v_linewidth/2.)) {
discard;
}
}
// Distance to border ------------------------------------------------------
d = d - t;
if( d < 0.0 ) {
gl_FragColor = color;
} else {
d /= u_antialias;
gl_FragColor = vec4(color.rgb, exp(-d*d)*color.a);
}
}
"""
init: () ->
gl = @gl
# keep track, so we know when we need to update segment data
# The program
# Buffers
# Dash atlas
draw: (indices, mainGlyph, trans) ->
mainGlGlyph = mainGlyph.glglyph
if mainGlGlyph.data_changed
if not (isFinite(trans.dx) and isFinite(trans.dy))
return # not sure why, but it happens on init sometimes (#4367)
mainGlGlyph._baked_offset = [trans.dx, trans.dy] # float32 precision workaround; used in _bake() and below
mainGlGlyph._set_data()
mainGlGlyph.data_changed = false
if @visuals_changed
# Decompose x-y scale into scalar scale and aspect-vector.
sx = trans.sx; sy = trans.sy;
scale_length = Math.sqrt(sx * sx + sy * sy)
sx /= scale_length; sy /= scale_length
# Do we need to re-calculate segment data and cumsum?
if Math.abs(@_scale_aspect - (sy / sx)) > Math.abs(1e-3 * @_scale_aspect)
mainGlGlyph._update_scale(sx, sy)
# Select buffers from main glyph
# (which may be this glyph but maybe not if this is a (non)selection glyph)
#
# Handle transformation to device coordinates
baked_offset = mainGlGlyph._baked_offset
if @I_triangles.length < 65535
# Data is small enough to draw in one pass
# @prog.draw(@gl.LINE_STRIP, @index_buffer) # Use this to draw the line skeleton
else
# Work around the limit that the indexbuffer must be uint16. We draw in chunks.
# First collect indices in chunks
indices = @I_triangles
nvertices = @I_triangles.length
chunksize = 64008 # 65536 max. 64008 is divisible by 12
chunks = []
for i in [0...Math.ceil(nvertices/chunksize)]
chunks.push([])
for i in [0...indices.length]
uint16_index = indices[i] % chunksize
chunk = Math.floor(indices[i] / chunksize)
chunks[chunk].push(uint16_index)
# Then draw each chunk
for chunk in [0...chunks.length]
these_indices = new Uint16Array(chunks[chunk])
offset = chunk * chunksize * 4
if these_indices.length == 0
continue
# The actual drawing
_set_data: () ->
_set_visuals: () ->
color = color2rgba(@glyph.visuals.line.line_color.value(), @glyph.visuals.line.line_alpha.value())
cap = @CAPS[@glyph.visuals.line.line_cap.value()]
join = @JOINS[@glyph.visuals.line.line_join.value()]
# Smaller aa-region to obtain crisper images
# 10 should be a good value
# https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-miterlimit
dash_pattern = @glyph.visuals.line.line_dash.value()
dash_index = 0; dash_period = 1
if dash_pattern.length
[dash_index, dash_period] = @dash_atlas.get_atlas_data(dash_pattern)
# 0 means solid line
# We dont do closed lines
_bake: () ->
# This is what you get if you port 50 lines of numpy code to JS.
# V_segment is handled in another method, because it depends on the aspect
# ratio of the scale (The original paper/code assumed isotropic scaling).
#
# Buffer dtype from the Python implementation:
#
# self.vtype = np.dtype( [('a_position', 'f4', 2),
# ('a_segment', 'f4', 2),
# ('a_angles', 'f4', 2),
# ('a_tangents', 'f4', 4),
# ('a_texcoord', 'f4', 2) ])
# Init array of implicit shape nx2
n = @nvertices
_x = new Float64Array(@glyph._x)
_y = new Float64Array(@glyph._y)
# Init vertex data
V_position = Vp = new Float32Array(n*2)
#V_segment = new Float32Array(n*2) # Done later
V_angles = new Float32Array(n*2)
V_tangents = Vt = new Float32Array(n*4) # mind the 4!
V_texcoord = new Float32Array(n*2)
# Position
for i in [0...n]
V_position[i*2+0] = _x[i] + @_baked_offset[0]
V_position[i*2+1] = _y[i] + @_baked_offset[1]
# Tangents & norms (need tangents to calculate segments based on scale)
for i in [0...n-1]
T[i*2+0] = Vp[(i+1)*2+0] - Vp[i*2+0]
T[i*2+1] = Vp[(i+1)*2+1] - Vp[i*2+1]
for i in [0...n-1]
# V['a_tangents'][+1:, :2] = T
V_tangents[(i+1)*4+0] = T[i*2+0]
V_tangents[(i+1)*4+1] = T[i*2+1]
# V['a_tangents'][:-1, 2:] = T
V_tangents[i*4+2] = T[i*2+0]
V_tangents[i*4+3] = T[i*2+1]
# V['a_tangents'][0 , :2] = T[0]
V_tangents[0*4+0] = T[0]
V_tangents[0*4+1] = T[1]
# V['a_tangents'][ -1, 2:] = T[-1]
V_tangents[(n-1)*4+2] = T[(n-2)*2+0]
V_tangents[(n-1)*4+3] = T[(n-2)*2+1]
# Angles
A = new Float32Array(n)
for i in [0...n]
A[i] = Math.atan2(Vt[i*4+0]*Vt[i*4+3] - Vt[i*4+1]*Vt[i*4+2],
Vt[i*4+0]*Vt[i*4+2] + Vt[i*4+1]*Vt[i*4+3])
for i in [0...n-1]
V_angles[i*2+0] = A[i]
V_angles[i*2+1] = A[i+1]
# Step 1: A -- B -- C => A -- B, B' -- C
# Repeat our array 4 times
m = 4 * n - 4
# mind the 4!
o = 2
#
# Arg, we really need an ndarray thing in JS :/
for i in [0...n] # all nodes on the line
for j in [0...4] # the four quad vertices
for k in [0...2] # xy
V_position2[(i*4+j-o)*2+k] = V_position[i*2+k]
V_angles2[(i*4+j)*2+k] = V_angles[i*2+k] # no offset
for k in [0...4]
V_tangents2[(i*4+j-o)*4+k] = V_tangents[i*4+k]
for i in [0..n]
V_texcoord2[(i*4+0)*2+0] = -1
V_texcoord2[(i*4+1)*2+0] = -1
V_texcoord2[(i*4+2)*2+0] = +1
V_texcoord2[(i*4+3)*2+0] = +1
#
V_texcoord2[(i*4+0)*2+1] = -1
V_texcoord2[(i*4+1)*2+1] = +1
V_texcoord2[(i*4+2)*2+1] = -1
V_texcoord2[(i*4+3)*2+1] = +1
# Indices
#I = np.resize( np.array([0,1,2,1,2,3], dtype=np.uint32), (n-1)*(2*3))
#I += np.repeat( 4*np.arange(n-1), 6)
ni = (n-1) * 6
# Order of indices is such that drawing as line_strip reveals the line skeleton
# Might have implications on culling, if we ever turn that on.
# Order in paper was: 0 1 2 1 2 3
for i in [0...n]
I[i*6+0] = 0 + 4*i
I[i*6+1] = 1 + 4*i
I[i*6+2] = 3 + 4*i
I[i*6+3] = 2 + 4*i
I[i*6+4] = 0 + 4*i
I[i*6+5] = 3 + 4*i
_update_scale: (sx, sy) ->
# Update segment data and cumsum so the length along the line has the
# scale aspect ratio in it. In the vertex shader we multiply with the
# "isotropic part" of the scale.
n = @nvertices
m = 4 * n - 4
# Prepare arrays
T = @tangents
N = new Float32Array(n-1)
V_segment = new Float32Array(n*2) # Elements are initialized with 0
# Calculate vector lengths - with scale aspect ratio taken into account
for i in [0...n-1]
N[i] = Math.sqrt(Math.pow(T[i*2+0] * sx, 2) + Math.pow(T[i*2+1] * sy, 2))
# Calculate Segments
cumsum = 0
for i in [0...n-1]
cumsum += N[i]
V_segment[(i+1)*2+0] = cumsum
V_segment[i*2+1] = cumsum
# Upscale (same loop as in _bake())
for i in [0...n]
for j in [0...4]
for k in [0...2]
V_segment2[(i*4+j)*2+k] = V_segment[i*2+k]
# Update
# L[-1] in Nico's code