let programs = { } ;
let buffers = { } ;
let textures = { } ;
let timers = { } ;
let config = {
bytes _per _quad : 28 ,
w : 32 ,
h : 32 ,
padding : 2 ,
predefined _colors : {
'Np' : [ 75 , 62 , 143 ] ,
'F' : [ 62 , 123 , 143 ] ,
'Pd' : [ 61 , 142 , 88 ] ,
'Dc' : [ 109 , 143 , 61 ] ,
'Rn' : [ 143 , 102 , 61 ] ,
'Ds' : [ 142 , 61 , 95 ] ,
'Sc' : [ 115 , 61 , 143 ] ,
'Is' : [ 61 , 81 , 143 ] ,
'Rr' : [ 61 , 143 , 129 ] ,
'X' : [ 68 , 143 , 61 ] ,
'Rw' : [ 142 , 142 , 61 ] ,
'Cm' : [ 61 , 81 , 143 ] ,
'Mt' : [ 142 , 142 , 61 ] ,
'Ma' : [ 143 , 68 , 61 ] ,
} ,
limit : - 1 ,
zoom _delta : 0.05 ,
raster _texture _size : 4096 ,
max _zoom _level : 0 ,
} ;
let canvas = null ;
let gl = null ;
let gpu _timer _ext = null ;
let offset = { x : 0 , y : 0 } ;
let moving = false ;
let zoom = 1 ;
let zoom _target = 1.0 ;
let zoom _level = 0 ;
let zoom _screenp = { 'x' : 0 , 'y' : 0 } ;
let last _frame _dt = 0 ;
let last _frame _ts = 0 ;
let raster _tile = { 'x' : 0 , 'y' : 0 } ;
let spacedown = false ;
const tquad _vs _src = ` #version 300 es
in vec2 a _pos ;
in vec2 a _size ;
in vec4 a _color ;
in vec2 a _uv ;
uniform vec2 u _res ;
uniform vec2 u _translation ;
uniform float u _scale ;
uniform vec2 u _textile ;
uniform vec2 u _tile ;
out vec4 v _color ;
out vec2 v _uv ;
void main ( ) {
int vertex _index = gl _VertexID % 6 ;
vec2 corner ;
vec2 uv ;
vec2 inset = vec2 ( 1.0 ) ;
// "Fix" for zero-width stages
if ( a _size . x < 0.1 ) {
inset = vec2 ( 0.0 ) ;
}
vec2 cycles = a _size / u _tile ;
vec2 tt = u _textile / u _tile ;
if ( vertex _index == 0 ) {
// "top left" aka "p1"
corner = a _pos ;
uv = a _uv ;
} else if ( vertex _index == 1 || vertex _index == 5 ) {
// "top right" aka "p2"
corner = a _pos + vec2 ( a _size . x , 0 ) ;
uv = a _uv + vec2 ( u _textile . x * cycles . x , 0 ) ;
} else if ( vertex _index == 2 || vertex _index == 4 ) {
// "bottom left" aka "p3"
corner = a _pos + vec2 ( 0 , a _size . y ) ;
uv = a _uv + vec2 ( 0 , u _textile . y * cycles . y ) ;
} else {
// "bottom right" aka "p4"
corner = a _pos + a _size ;
uv = a _uv + u _textile * cycles ;
}
vec2 screen02 = ( corner . xy * vec2 ( u _scale ) + u _translation ) / u _res * 2.0 ;
screen02 . y = 2.0 - screen02 . y ;
v _color = a _color ;
v _uv = uv ;
gl _Position = vec4 ( screen02 - 1.0 , 1.0 , 1.0 ) ;
}
` ;
const tquad _fs _src = ` #version 300 es
precision highp float ;
in vec4 v _color ;
in vec2 v _uv ;
uniform sampler2D u _texture ;
uniform float u _fade ;
layout ( location = 0 ) out vec4 FragColor ;
void main ( ) {
vec4 text = texture ( u _texture , v _uv ) ;
text . a = min ( min ( text . a , v _color . a ) , u _fade ) ;
FragColor = vec4 ( text . rgb * text . a + v _color . rgb , 1.0 ) ;
}
` ;
function schedule _draw ( animation = false ) {
if ( ! timers . raf ) {
window . requestAnimationFrame ( ( ts ) => draw ( ts , animation ) ) ;
timers . raf = true ;
}
}
function draw ( ts , animation ) {
const dt = ts - last _frame _ts ;
const cpu _before = performance . now ( ) ;
const width = window . innerWidth ;
const height = window . innerHeight ;
last _frame _ts = ts ;
let query = null ;
if ( gpu _timer _ext !== null ) {
query = gl . createQuery ( ) ;
gl . beginQuery ( gpu _timer _ext . TIME _ELAPSED _EXT , query ) ;
}
gl . viewport ( 0 , 0 , canvas . width , canvas . height ) ;
gl . clearColor ( 0.11 , 0.11 , 0.11 , 1 ) ;
gl . clear ( gl . COLOR _BUFFER _BIT ) ;
let quads = { 'count' : 0 } ;
if ( '0' in traces ) {
quads = traces [ '0' ] . geo ;
}
const clipped = clip ( quads ) ;
if ( clipped . count > 0 ) {
const program = programs [ 'quad' ] ;
const fade = Math . max ( 0 , Math . min ( 1.25 * zoom - 0.25 , 1 ) ) ;
gl . useProgram ( program . program ) ;
gl . bindBuffer ( gl . ARRAY _BUFFER , buffers [ 'b_packed' ] ) ;
if ( ! clipped . uploaded ) {
gl . bufferData ( gl . ARRAY _BUFFER , clipped . count * config . bytes _per _quad , gl . STATIC _DRAW ) ;
gl . bufferSubData ( gl . ARRAY _BUFFER , 0 , clipped . pos ) ;
gl . bufferSubData ( gl . ARRAY _BUFFER , clipped . pos . byteLength , clipped . size ) ;
gl . bufferSubData ( gl . ARRAY _BUFFER , clipped . pos . byteLength + clipped . size . byteLength , clipped . color ) ;
gl . bufferSubData ( gl . ARRAY _BUFFER , clipped . pos . byteLength + clipped . size . byteLength + clipped . color . byteLength , clipped . uv ) ;
clipped . uploaded = true ;
}
gl . uniform2f ( program . locations [ 'u_res' ] , canvas . width , canvas . height ) ;
gl . uniform2f ( program . locations [ 'u_translation' ] , offset . x , offset . y ) ;
gl . uniform1f ( program . locations [ 'u_scale' ] , zoom ) ;
gl . uniform2f ( program . locations [ 'u_textile' ] , config . w / config . raster _texture _size , config . h / config . raster _texture _size ) ;
gl . uniform1i ( program . locations [ 'u_texture' ] , textures [ 'raster' ] ) ;
gl . uniform2f ( program . locations [ 'u_tile' ] , config . w , config . h ) ;
gl . uniform1f ( program . locations [ 'u_fade' ] , fade ) ;
gl . enableVertexAttribArray ( program . locations [ 'a_pos' ] ) ;
gl . enableVertexAttribArray ( program . locations [ 'a_size' ] ) ;
gl . enableVertexAttribArray ( program . locations [ 'a_color' ] ) ;
gl . enableVertexAttribArray ( program . locations [ 'a_uv' ] ) ;
gl . vertexAttribPointer ( program . locations [ 'a_pos' ] , 2 , gl . FLOAT , false , 2 * 4 , 0 ) ;
gl . vertexAttribPointer ( program . locations [ 'a_size' ] , 2 , gl . FLOAT , false , 2 * 4 , clipped . pos . byteLength ) ;
gl . vertexAttribPointer ( program . locations [ 'a_color' ] , 4 , gl . UNSIGNED _BYTE , true , 4 * 1 , clipped . pos . byteLength + clipped . size . byteLength ) ;
gl . vertexAttribPointer ( program . locations [ 'a_uv' ] , 2 , gl . FLOAT , false , 2 * 4 , clipped . pos . byteLength + clipped . size . byteLength + clipped . color . byteLength ) ;
gl . vertexAttribDivisor ( program . locations [ 'a_pos' ] , 1 ) ;
gl . vertexAttribDivisor ( program . locations [ 'a_size' ] , 1 ) ;
gl . vertexAttribDivisor ( program . locations [ 'a_color' ] , 1 ) ;
gl . vertexAttribDivisor ( program . locations [ 'a_uv' ] , 1 ) ;
gl . bindTexture ( gl . TEXTURE _2D , textures [ 'raster' ] ) ;
gl . drawArraysInstanced ( gl . TRIANGLES , 0 , 6 , clipped . count ) ;
gl . vertexAttribDivisor ( program . locations [ 'a_pos' ] , 0 ) ;
gl . vertexAttribDivisor ( program . locations [ 'a_size' ] , 0 ) ;
gl . vertexAttribDivisor ( program . locations [ 'a_color' ] , 0 ) ;
gl . vertexAttribDivisor ( program . locations [ 'a_uv' ] , 0 ) ;
}
if ( gpu _timer _ext ) {
gl . endQuery ( gpu _timer _ext . TIME _ELAPSED _EXT ) ;
const next _tick = ( ) => {
if ( query ) {
const available = gl . getQueryParameter ( query , gl . QUERY _RESULT _AVAILABLE ) ;
const disjoint = gl . getParameter ( gpu _timer _ext . GPU _DISJOINT _EXT ) ;
if ( available && ! disjoint ) {
const timeElapsed = gl . getQueryParameter ( query , gl . QUERY _RESULT ) ;
console . log ( 'Last GPU Frametime: ' + Math . round ( timeElapsed / 10000 ) / 100 + 'ms' ) ;
}
if ( available || disjoint ) {
gl . deleteQuery ( query ) ;
query = null ;
} else if ( ! available ) {
setTimeout ( next _tick , 0 ) ;
}
}
}
setTimeout ( next _tick , 0 ) ;
}
const cpu _after = performance . now ( ) ;
timers . raf = false ;
console . log ( 'Last CPU Frametime: ' + Math . round ( ( cpu _after - cpu _before ) * 100 ) / 100 + 'ms' ) ;
if ( zoom _target != zoom ) {
update _canvas _zoom ( zoom , zoom _target , animation ? dt : last _frame _dt ) ;
schedule _draw ( true ) ;
}
last _frame _dt = dt ;
}
function update _canvas _zoom ( current , target , dt ) {
const rate = Math . min ( 1.0 , dt / 16.66 * 0.3 ) ;
if ( Math . abs ( 1.0 - current / target ) > 0.01 ) {
zoom = current + ( target - current ) * rate ;
} else {
zoom = target ;
}
// https://gist.github.com/aolo2/a373363419bd5a9283977ab9f8841f78
const zc = zoom _screenp ;
offset . x = zc . x - ( zc . x - offset . x ) * zoom / current ;
offset . y = zc . y - ( zc . y - offset . y ) * zoom / current ;
}
function init _webgl ( ) {
canvas = document . querySelector ( '#c' ) ;
gl = canvas . getContext ( 'webgl2' ) ;
gpu _timer _ext = gl . getExtension ( 'EXT_disjoint_timer_query_webgl2' ) ;
if ( gpu _timer _ext === null ) {
gpu _timer _ext = gl . getExtension ( 'EXT_disjoint_timer_query' ) ;
}
const quad _vs = create _shader ( gl , gl . VERTEX _SHADER , tquad _vs _src ) ;
const quad _fs = create _shader ( gl , gl . FRAGMENT _SHADER , tquad _fs _src ) ;
programs = {
'quad' : create _program ( gl , quad _vs , quad _fs ) ,
} ;
buffers = {
'b_packed' : gl . createBuffer ( ) ,
} ;
textures = {
'raster' : gl . createTexture ( ) ,
} ;
const zeroes = new Uint8Array ( config . raster _texture _size * config . raster _texture _size * 4 ) ;
gl . bindTexture ( gl . TEXTURE _2D , textures [ 'raster' ] ) ;
gl . texParameteri ( gl . TEXTURE _2D , gl . TEXTURE _MIN _FILTER , gl . LINEAR ) ;
gl . texParameteri ( gl . TEXTURE _2D , gl . TEXTURE _MAG _FILTER , gl . NEAREST ) ;
gl . texImage2D ( gl . TEXTURE _2D , 0 , gl . RGBA , config . raster _texture _size , config . raster _texture _size , 0 , gl . RGBA , gl . UNSIGNED _BYTE , zeroes ) ; // fill the whole texture once with zeroes to kill a warning about a partial upload
const resize _canvas = ( entries ) => {
// https://www.khronos.org/webgl/wiki/HandlingHighDPI
const entry = entries [ 0 ] ;
let width ;
let height ;
if ( entry . devicePixelContentBoxSize ) {
width = entry . devicePixelContentBoxSize [ 0 ] . inlineSize ;
height = entry . devicePixelContentBoxSize [ 0 ] . blockSize ;
} else if ( entry . contentBoxSize ) {
// fallback for Safari that will not always be correct
width = Math . round ( entry . contentBoxSize [ 0 ] . inlineSize * devicePixelRatio ) ;
height = Math . round ( entry . contentBoxSize [ 0 ] . blockSize * devicePixelRatio ) ;
}
canvas . width = width ;
canvas . height = height ;
schedule _draw ( ) ;
}
const resize _observer = new ResizeObserver ( resize _canvas ) ;
resize _observer . observe ( canvas ) ;
}
function create _shader ( gl , type , source ) {
const shader = gl . createShader ( type ) ;
gl . shaderSource ( shader , source ) ;
gl . compileShader ( shader ) ;
if ( gl . getShaderParameter ( shader , gl . COMPILE _STATUS ) ) {
return shader ;
}
console . error ( type , ':' , gl . getShaderInfoLog ( shader ) ) ;
gl . deleteShader ( shader ) ;
}
function create _program ( gl , vs , fs ) {
const program = gl . createProgram ( ) ;
gl . attachShader ( program , vs ) ;
gl . attachShader ( program , fs ) ;
gl . linkProgram ( program ) ;
if ( gl . getProgramParameter ( program , gl . LINK _STATUS ) ) {
// src: tiny-sdf
// https://github.com/mapbox/tiny-sdf
const wrapper = { program } ;
const num _attrs = gl . getProgramParameter ( program , gl . ACTIVE _ATTRIBUTES ) ;
const num _uniforms = gl . getProgramParameter ( program , gl . ACTIVE _UNIFORMS ) ;
wrapper . locations = { } ;
for ( let i = 0 ; i < num _attrs ; i ++ ) {
const attribute = gl . getActiveAttrib ( program , i ) ;
wrapper . locations [ attribute . name ] = gl . getAttribLocation ( program , attribute . name ) ;
}
for ( let i = 0 ; i < num _uniforms ; i ++ ) {
const uniform = gl . getActiveUniform ( program , i ) ;
wrapper . locations [ uniform . name ] = gl . getUniformLocation ( program , uniform . name ) ;
}
return wrapper ;
}
console . error ( 'link:' , gl . getProgramInfoLog ( program ) ) ;
gl . deleteProgram ( program ) ;
}