webgl-heatmap/webgl-heatmap.coffee
2013-06-08 18:25:43 +00:00

995 lines
34 KiB
CoffeeScript

nukeVendorPrefix = ->
if window.WebGLRenderingContext?
vendors = ['WEBKIT', 'MOZ', 'MS', 'O']
vendorRe = /^WEBKIT_(.*)|MOZ_(.*)|MS_(.*)|O_(.*)/
getExtension = WebGLRenderingContext.prototype.getExtension
WebGLRenderingContext.prototype.getExtension = (name) ->
match = name.match vendorRe
if match != null
name = match[1]
extobj = getExtension.call @, name
if extobj == null
for vendor in vendors
extobj = getExtension.call @, vendor + '_' + name
if extobj != null
return extobj
return null
else
return extobj
getSupportedExtensions = WebGLRenderingContext.prototype.getSupportedExtensions
WebGLRenderingContext.prototype.getSupportedExtensions = ->
supported = getSupportedExtensions.call @
result = []
for extension in supported
match = extension.match vendorRe
if match != null
extension = match[1]
if extension not in result
result.push extension
return result
textureFloatShims = ->
createSourceCanvas = ->
canvas = document.createElement 'canvas'
canvas.width = 2
canvas.height = 2
ctx = canvas.getContext '2d'
imageData = ctx.getImageData(0, 0, 2, 2)
imageData.data.set(new Uint8ClampedArray([
0,0,0,0,
255,255,255,255,
0,0,0,0,
255,255,255,255,
]))
ctx.putImageData(imageData, 0, 0)
return canvas
createSourceCanvas()
checkFloatLinear = (gl, sourceType) ->
## drawing program ##
program = gl.createProgram()
vertexShader = gl.createShader(gl.VERTEX_SHADER)
gl.attachShader(program, vertexShader)
gl.shaderSource(vertexShader, '''
attribute vec2 position;
void main(){
gl_Position = vec4(position, 0.0, 1.0);
}
''')
gl.compileShader(vertexShader)
if not gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)
throw gl.getShaderInfoLog(vertexShader)
fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)
gl.attachShader(program, fragmentShader)
gl.shaderSource(fragmentShader, '''
uniform sampler2D source;
void main(){
gl_FragColor = texture2D(source, vec2(1.0, 1.0));
}
''')
gl.compileShader(fragmentShader)
if not gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)
throw gl.getShaderInfoLog(fragmentShader)
gl.linkProgram(program)
if not gl.getProgramParameter(program, gl.LINK_STATUS)
throw gl.getProgramInfoLog(program)
gl.useProgram(program)
cleanup = ->
gl.deleteShader(fragmentShader)
gl.deleteShader(vertexShader)
gl.deleteProgram(program)
gl.deleteBuffer(buffer)
gl.deleteTexture(source)
gl.deleteTexture(target)
gl.deleteFramebuffer(framebuffer)
gl.bindBuffer(gl.ARRAY_BUFFER, null)
gl.useProgram(null)
gl.bindTexture(gl.TEXTURE_2D, null)
gl.bindFramebuffer(gl.FRAMEBUFFER, null)
## target FBO ##
target = gl.createTexture()
gl.bindTexture(gl.TEXTURE_2D, target)
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
2, 2,
0,
gl.RGBA,
gl.UNSIGNED_BYTE,
null,
)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
framebuffer = gl.createFramebuffer()
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer)
gl.framebufferTexture2D(
gl.FRAMEBUFFER,
gl.COLOR_ATTACHMENT0,
gl.TEXTURE_2D,
target,
0
)
## source texture ##
sourceCanvas = createSourceCanvas()
source = gl.createTexture()
gl.bindTexture(gl.TEXTURE_2D, source)
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
gl.RGBA,
sourceType,
sourceCanvas,
)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
## create VBO ##
vertices = new Float32Array([
1, 1,
-1, 1,
-1, -1,
1, 1,
-1, -1,
1, -1,
])
buffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW)
positionLoc = gl.getAttribLocation(program, 'position')
sourceLoc = gl.getUniformLocation(program, 'source')
gl.enableVertexAttribArray(positionLoc)
gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0)
gl.uniform1i(sourceLoc, 0)
gl.drawArrays(gl.TRIANGLES, 0, 6)
readBuffer = new Uint8Array(4*4)
gl.readPixels(0, 0, 2, 2, gl.RGBA, gl.UNSIGNED_BYTE, readBuffer)
result = Math.abs(readBuffer[0] - 127) < 10
cleanup()
return result
checkTexture = (gl, targetType) ->
target = gl.createTexture()
gl.bindTexture(gl.TEXTURE_2D, target)
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
2, 2,
0,
gl.RGBA,
targetType,
null,
)
if gl.getError() == 0
gl.deleteTexture(target)
return true
else
gl.deleteTexture(target)
return false
checkColorBuffer = (gl, targetType) ->
target = gl.createTexture()
gl.bindTexture(gl.TEXTURE_2D, target)
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
2, 2,
0,
gl.RGBA,
targetType,
null,
)
framebuffer = gl.createFramebuffer()
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer)
gl.framebufferTexture2D(
gl.FRAMEBUFFER,
gl.COLOR_ATTACHMENT0,
gl.TEXTURE_2D,
target,
0
)
check = gl.checkFramebufferStatus(gl.FRAMEBUFFER)
gl.deleteTexture(target)
gl.deleteFramebuffer(framebuffer)
gl.bindTexture(gl.TEXTURE_2D, null)
gl.bindFramebuffer(gl.FRAMEBUFFER, null)
if check == gl.FRAMEBUFFER_COMPLETE
return true
else
return false
shimExtensions = []
shimLookup = {}
unshimExtensions = []
checkSupport = ->
canvas = document.createElement 'canvas'
gl = null
try
gl = canvas.getContext 'experimental-webgl'
if(gl == null)
gl = canvas.getContext 'webgl'
if gl?
singleFloatExt = gl.getExtension 'OES_texture_float'
if singleFloatExt == null
if checkTexture gl, gl.FLOAT
singleFloatTexturing = true
shimExtensions.push 'OES_texture_float'
shimLookup.OES_texture_float = {shim:true}
else
singleFloatTexturing = false
unshimExtensions.push 'OES_texture_float'
else
if checkTexture gl, gl.FLOAT
singleFloatTexturing = true
shimExtensions.push 'OES_texture_float'
else
singleFloatTexturing = false
unshimExtensions.push 'OES_texture_float'
if singleFloatTexturing
extobj = gl.getExtension 'WEBGL_color_buffer_float'
if extobj == null
if checkColorBuffer gl, gl.FLOAT
shimExtensions.push 'WEBGL_color_buffer_float'
shimLookup.WEBGL_color_buffer_float = {
shim: true
RGBA32F_EXT: 0x8814
RGB32F_EXT: 0x8815
FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE_EXT: 0x8211
UNSIGNED_NORMALIZED_EXT: 0x8C17
}
else
unshimExtensions.push 'WEBGL_color_buffer_float'
else
if checkColorBuffer gl, gl.FLOAT
shimExtensions.push 'WEBGL_color_buffer_float'
else
unshimExtensions.push 'WEBGL_color_buffer_float'
extobj = gl.getExtension 'OES_texture_float_linear'
if extobj == null
if checkFloatLinear gl, gl.FLOAT
shimExtensions.push 'OES_texture_float_linear'
shimLookup.OES_texture_float_linear = {shim:true}
else
unshimExtensions.push 'OES_texture_float_linear'
else
if checkFloatLinear gl, gl.FLOAT
shimExtensions.push 'OES_texture_float_linear'
else
unshimExtensions.push 'OES_texture_float_linear'
halfFloatExt = gl.getExtension 'OES_texture_half_float'
if halfFloatExt == null
if checkTexture(gl, 0x8D61)
halfFloatTexturing = true
shimExtensions.push 'OES_texture_half_float'
halfFloatExt = shimLookup.OES_texture_half_float = {
HALF_FLOAT_OES: 0x8D61
shim:true
}
else
halfFloatTexturing = false
unshimExtensions.push 'OES_texture_half_float'
else
if checkTexture(gl, halfFloatExt.HALF_FLOAT_OES)
halfFloatTexturing = true
shimExtensions.push 'OES_texture_half_float'
else
halfFloatTexturing = false
unshimExtensions.push 'OES_texture_half_float'
if halfFloatTexturing
extobj = gl.getExtension 'EXT_color_buffer_half_float'
if extobj == null
if checkColorBuffer gl, halfFloatExt.HALF_FLOAT_OES
shimExtensions.push 'EXT_color_buffer_half_float'
shimLookup.EXT_color_buffer_half_float = {
shim: true
RGBA16F_EXT: 0x881A
RGB16F_EXT: 0x881B
FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE_EXT: 0x8211
UNSIGNED_NORMALIZED_EXT: 0x8C17
}
else
unshimExtensions.push 'EXT_color_buffer_half_float'
else
if checkColorBuffer gl, halfFloatExt.HALF_FLOAT_OES
shimExtensions.push 'EXT_color_buffer_half_float'
else
unshimExtensions.push 'EXT_color_buffer_half_float'
extobj = gl.getExtension 'OES_texture_half_float_linear'
if extobj == null
if checkFloatLinear gl, halfFloatExt.HALF_FLOAT_OES
shimExtensions.push 'OES_texture_half_float_linear'
shimLookup.OES_texture_half_float_linear = {shim:true}
else
unshimExtensions.push 'OES_texture_half_float_linear'
else
if checkFloatLinear gl, halfFloatExt.HALF_FLOAT_OES
shimExtensions.push 'OES_texture_half_float_linear'
else
unshimExtensions.push 'OES_texture_half_float_linear'
if window.WebGLRenderingContext?
checkSupport()
unshimLookup = {}
for name in unshimExtensions
unshimLookup[name] = true
getExtension = WebGLRenderingContext.prototype.getExtension
WebGLRenderingContext.prototype.getExtension = (name) ->
extobj = shimLookup[name]
if extobj == undefined
if unshimLookup[name]
return null
else
return getExtension.call @, name
else
return extobj
getSupportedExtensions = WebGLRenderingContext.prototype.getSupportedExtensions
WebGLRenderingContext.prototype.getSupportedExtensions = ->
supported = getSupportedExtensions.call(@)
result = []
for extension in supported
if unshimLookup[extension] == undefined
result.push(extension)
for extension in shimExtensions
if extension not in result
result.push extension
return result
WebGLRenderingContext.prototype.getFloatExtension = (spec) ->
spec.prefer ?= ['half']
spec.require ?= []
spec.throws ?= true
singleTexture = @getExtension 'OES_texture_float'
halfTexture = @getExtension 'OES_texture_half_float'
singleFramebuffer = @getExtension 'WEBGL_color_buffer_float'
halfFramebuffer = @getExtension 'EXT_color_buffer_half_float'
singleLinear = @getExtension 'OES_texture_float_linear'
halfLinear = @getExtension 'OES_texture_half_float_linear'
single = {
texture: singleTexture != null
filterable: singleLinear != null
renderable: singleFramebuffer != null
score: 0
precision: 'single'
half: false
single: true
type: @FLOAT
}
half = {
texture: halfTexture != null
filterable: halfLinear != null
renderable: halfFramebuffer != null
score: 0
precision: 'half'
half: true
single: false
type: halfTexture?.HALF_FLOAT_OES ? null
}
candidates = []
if single.texture
candidates.push(single)
if half.texture
candidates.push(half)
result = []
for candidate in candidates
use = true
for name in spec.require
if candidate[name] == false
use = false
if use
result.push candidate
for candidate in result
for preference, i in spec.prefer
importance = Math.pow 2, spec.prefer.length - i - 1
if candidate[preference]
candidate.score += importance
result.sort (a, b) ->
if a.score == b.score then 0
else if a.score < b.score then 1
else if a.score > b.score then -1
if result.length == 0
if throws
throw 'No floating point texture support that is ' + spec.require.join(', ')
else
return null
else
result = result[0]
return {
filterable: result.filterable
renderable: result.renderable
type: result.type
precision: result.precision
}
nukeVendorPrefix()
textureFloatShims()
class Shader
constructor: (@gl, {vertex, fragment}) ->
@program = @gl.createProgram()
@vs = @gl.createShader @gl.VERTEX_SHADER
@fs = @gl.createShader @gl.FRAGMENT_SHADER
@gl.attachShader @program, @vs
@gl.attachShader @program, @fs
@compileShader @vs, vertex
@compileShader @fs, fragment
@link()
@value_cache = {}
@uniform_cache = {}
@attribCache = {}
attribLocation: (name) ->
location = @attribCache[name]
if location is undefined
location = @attribCache[name] = @gl.getAttribLocation @program, name
return location
compileShader: (shader, source) ->
@gl.shaderSource shader, source
@gl.compileShader shader
if not @gl.getShaderParameter shader, @gl.COMPILE_STATUS
throw "Shader Compile Error: #{@gl.getShaderInfoLog(shader)}"
link: ->
@gl.linkProgram @program
if not @gl.getProgramParameter @program, @gl.LINK_STATUS
throw "Shader Link Error: #{@gl.getProgramInfoLog(@program)}"
use: ->
@gl.useProgram @program
return @
uniformLoc: (name) ->
location = @uniform_cache[name]
if location is undefined
location = @uniform_cache[name] = @gl.getUniformLocation @program, name
return location
int: (name, value) ->
cached = @value_cache[name]
if cached != value
@value_cache[name] = value
loc = @uniformLoc name
@gl.uniform1i loc, value if loc
return @
vec2: (name, a, b) ->
loc = @uniformLoc name
@gl.uniform2f loc, a, b if loc
return @
float: (name, value) ->
cached = @value_cache[name]
if cached != value
@value_cache[name] = value
loc = @uniformLoc name
@gl.uniform1f loc, value if loc
return @
class Framebuffer
constructor: (@gl) ->
@buffer = @gl.createFramebuffer()
destroy: ->
@gl.deleteFRamebuffer @buffer
bind: ->
@gl.bindFramebuffer @gl.FRAMEBUFFER, @buffer
return @
unbind: ->
@gl.bindFramebuffer @gl.FRAMEBUFFER, null
return @
check: ->
result = @gl.checkFramebufferStatus @gl.FRAMEBUFFER
switch result
when @gl.FRAMEBUFFER_UNSUPPORTED
throw 'Framebuffer is unsupported'
when @gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT
throw 'Framebuffer incomplete attachment'
when @gl.FRAMEBUFFER_INCOMPLETE_DIMENSIONS
throw 'Framebuffer incomplete dimensions'
when @gl.FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT
throw 'Framebuffer incomplete missing attachment'
return @
color: (texture) ->
@gl.framebufferTexture2D @gl.FRAMEBUFFER, @gl.COLOR_ATTACHMENT0, texture.target, texture.handle, 0
@check()
return @
depth: (buffer) ->
@gl.framebufferRenderbuffer @gl.FRAMEBUFFER, @gl.DEPTH_ATTACHMENT, @gl.RENDERBUFFER, buffer.id
@check()
return @
destroy: ->
@gl.deleteFramebuffer @buffer
class Texture
constructor: (@gl, params={}) ->
@channels = @gl[(params.channels ? 'rgba').toUpperCase()]
if typeof(params.type) == 'number'
@type = params.type
else
@type = @gl[(params.type ? 'unsigned_byte').toUpperCase()]
switch @channels
when @gl.RGBA then @chancount = 4
when @gl.RGB then @chancount = 3
when @gl.LUMINANCE_ALPHA then @chancount = 2
else @chancount = 1
@target = @gl.TEXTURE_2D
@handle = @gl.createTexture()
destroy: ->
@gl.deleteTexture @handle
bind: (unit=0) ->
if unit > 15
throw 'Texture unit too large: ' + unit
@gl.activeTexture @gl.TEXTURE0+unit
@gl.bindTexture @target, @handle
return @
setSize: (@width, @height) ->
@gl.texImage2D @target, 0, @channels, @width, @height, 0, @channels, @type, null
return @
upload: (data) ->
@width = data.width
@height = data.height
@gl.texImage2D @target, 0, @channels, @channels, @type, data
return @
linear: ->
@gl.texParameteri @target, @gl.TEXTURE_MAG_FILTER, @gl.LINEAR
@gl.texParameteri @target, @gl.TEXTURE_MIN_FILTER, @gl.LINEAR
return @
nearest: ->
@gl.texParameteri @target, @gl.TEXTURE_MAG_FILTER, @gl.NEAREST
@gl.texParameteri @target, @gl.TEXTURE_MIN_FILTER, @gl.NEAREST
return @
clampToEdge: ->
@gl.texParameteri @target, @gl.TEXTURE_WRAP_S, @gl.CLAMP_TO_EDGE
@gl.texParameteri @target, @gl.TEXTURE_WRAP_T, @gl.CLAMP_TO_EDGE
return @
repeat: ->
@gl.texParameteri @target, @gl.TEXTURE_WRAP_S, @gl.REPEAT
@gl.texParameteri @target, @gl.TEXTURE_WRAP_T, @gl.REPEAT
return @
class Node
constructor: (@gl, @width, @height) ->
floatExt = @gl.getFloatExtension require: ['renderable']
@texture = new Texture(@gl, type:floatExt.type).bind(0).setSize(@width, @height).nearest().clampToEdge()
@fbo = new Framebuffer(@gl).bind().color(@texture).unbind()
use: -> @fbo.bind()
bind: (unit) -> @texture.bind(unit)
end: -> @fbo.unbind()
resize: (@width, @height) ->
@texture.bind(0).setSize(@width, @height)
vertexShaderBlit = '''
attribute vec4 position;
varying vec2 texcoord;
void main(){
texcoord = position.xy*0.5+0.5;
gl_Position = position;
}
'''
fragmentShaderBlit = '''
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp int;
precision highp float;
#else
precision mediump int;
precision mediump float;
#endif
uniform sampler2D source;
varying vec2 texcoord;
'''
class Heights
constructor: (@heatmap, @gl, @width, @height) ->
@shader = new Shader @gl,
vertex: '''
attribute vec4 position, intensity;
varying vec2 off, dim;
varying float vIntensity;
uniform vec2 viewport;
void main(){
dim = abs(position.zw);
off = position.zw;
vec2 pos = position.xy + position.zw;
vIntensity = intensity.x;
gl_Position = vec4((pos/viewport)*2.0-1.0, 0.0, 1.0);
}
'''
fragment: '''
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp int;
precision highp float;
#else
precision mediump int;
precision mediump float;
#endif
varying vec2 off, dim;
varying float vIntensity;
void main(){
float falloff = (1.0 - smoothstep(0.0, 1.0, length(off/dim)));
float intensity = falloff*vIntensity;
gl_FragColor = vec4(intensity);
}
'''
@clampShader = new Shader @gl,
vertex: vertexShaderBlit
fragment: fragmentShaderBlit + '''
uniform float low, high;
void main(){
gl_FragColor = vec4(clamp(texture2D(source, texcoord).rgb, low, high), 1.0);
}
'''
@multiplyShader = new Shader @gl,
vertex: vertexShaderBlit
fragment: fragmentShaderBlit + '''
uniform float value;
void main(){
gl_FragColor = vec4(texture2D(source, texcoord).rgb*value, 1.0);
}
'''
@blurShader = new Shader @gl,
vertex: vertexShaderBlit
fragment: fragmentShaderBlit + '''
uniform vec2 viewport;
void main(){
vec4 result = vec4(0.0);
for(int x=-1; x<=1; x++){
for(int y=-1; y<=1; y++){
vec2 off = vec2(x,y)/viewport;
//float factor = 1.0 - smoothstep(0.0, 1.5, length(off));
float factor = 1.0;
result += vec4(texture2D(source, texcoord+off).rgb*factor, factor);
}
}
gl_FragColor = vec4(result.rgb/result.w, 1.0);
}
'''
@nodeBack = new Node @gl, @width, @height
@nodeFront = new Node @gl, @width, @height
@vertexBuffer = @gl.createBuffer()
@vertexSize = 8
@maxPointCount = 1024*10
@vertexBufferData = new Float32Array @maxPointCount*@vertexSize*6
@vertexBufferViews = []
for i in [0...@maxPointCount]
@vertexBufferViews.push new Float32Array(@vertexBufferData.buffer, 0, i*@vertexSize*6)
@bufferIndex = 0
@pointCount = 0
resize: (@width, @height) ->
@nodeBack.resize @width, @height
@nodeFront.resize @width, @height
update: ->
if @pointCount > 0
@gl.enable @gl.BLEND
@nodeFront.use()
@gl.bindBuffer @gl.ARRAY_BUFFER, @vertexBuffer
@gl.bufferData @gl.ARRAY_BUFFER, @vertexBufferViews[@pointCount], @gl.STREAM_DRAW
positionLoc = @shader.attribLocation('position')
intensityLoc = @shader.attribLocation('intensity')
@gl.enableVertexAttribArray 1
@gl.vertexAttribPointer(positionLoc, 4, @gl.FLOAT, false, 8*4, 0*4)
@gl.vertexAttribPointer(intensityLoc, 4, @gl.FLOAT, false, 8*4, 4*4)
@shader.use().vec2('viewport', @width, @height)
@gl.drawArrays @gl.TRIANGLES, 0, @pointCount*6
@gl.disableVertexAttribArray 1
@pointCount = 0
@bufferIndex = 0
@nodeFront.end()
@gl.disable @gl.BLEND
clear: ->
@nodeFront.use()
@gl.clearColor(0,0,0,1)
@gl.clear(@gl.COLOR_BUFFER_BIT)
@nodeFront.end()
clamp: (min, max) ->
@gl.bindBuffer @gl.ARRAY_BUFFER, @heatmap.quad
@gl.vertexAttribPointer(0, 4, @gl.FLOAT, false, 0, 0)
@nodeFront.bind(0)
@nodeBack.use()
@clampShader.use().int('source', 0).float('low', min).float('high', max)
@gl.drawArrays @gl.TRIANGLES, 0, 6
@nodeBack.end()
@swap()
multiply: (value) ->
@gl.bindBuffer @gl.ARRAY_BUFFER, @heatmap.quad
@gl.vertexAttribPointer(0, 4, @gl.FLOAT, false, 0, 0)
@nodeFront.bind(0)
@nodeBack.use()
@multiplyShader.use().int('source', 0).float('value', value)
@gl.drawArrays @gl.TRIANGLES, 0, 6
@nodeBack.end()
@swap()
blur: ->
@gl.bindBuffer @gl.ARRAY_BUFFER, @heatmap.quad
@gl.vertexAttribPointer(0, 4, @gl.FLOAT, false, 0, 0)
@nodeFront.bind(0)
@nodeBack.use()
@blurShader.use().int('source', 0).vec2('viewport', @width, @height)
@gl.drawArrays @gl.TRIANGLES, 0, 6
@nodeBack.end()
@swap()
swap: ->
tmp = @nodeFront
@nodeFront = @nodeBack
@nodeBack = tmp
addVertex: (x, y, xs, ys, intensity) ->
@vertexBufferData[@bufferIndex++] = x
@vertexBufferData[@bufferIndex++] = y
@vertexBufferData[@bufferIndex++] = xs
@vertexBufferData[@bufferIndex++] = ys
@vertexBufferData[@bufferIndex++] = intensity
@vertexBufferData[@bufferIndex++] = intensity
@vertexBufferData[@bufferIndex++] = intensity
@vertexBufferData[@bufferIndex++] = intensity
addPoint: (x, y, size=50, intensity=0.2) ->
if @pointCount >= @maxPointCount - 1
@update()
#if @pointCount < @maxPointCount
y = @height - y
s = size/2
@addVertex x, y, -s, -s, intensity
@addVertex x, y, +s, -s, intensity
@addVertex x, y, -s, +s, intensity
@addVertex x, y, -s, +s, intensity
@addVertex x, y, +s, -s, intensity
@addVertex x, y, +s, +s, intensity
@pointCount += 1
class WebGLHeatmap
constructor: ({@canvas, @width, @height, intensityToAlpha, gradientTexture, alphaRange}={}) ->
@canvas = document.createElement('canvas') unless @canvas
try
@gl = @canvas.getContext('experimental-webgl', depth:false, antialias:false)
if @gl == null
@gl = @canvas.getContext('webgl', depth:false, antialias:false)
if @gl == null
throw 'WebGL not supported'
catch error
throw 'WebGL not supported'
if window.WebGLDebugUtils?
console.log 'debugging mode'
@gl = WebGLDebugUtils.makeDebugContext @gl, (err, funcName, args) ->
throw WebGLDebugUtils.glEnumToString(err) + " was caused by call to: " + funcName
@gl.enableVertexAttribArray 0
@gl.blendFunc @gl.ONE, @gl.ONE
if gradientTexture
textureGradient = @gradientTexture = new Texture(@gl, channels:'rgba').bind(0).setSize(2, 2).nearest().clampToEdge()
if typeof gradientTexture == 'string'
image = new Image()
image.onload = ->
textureGradient.bind().upload(image)
image.src = gradientTexture
else
if gradientTexture.width > 0 and gradientTexture.height > 0
textureGradient.upload(gradientTexture)
else
gradientTexture.onload = ->
textureGradient.upload(gradientTexture)
getColorFun = '''
uniform sampler2D gradientTexture;
vec3 getColor(float intensity){
return texture2D(gradientTexture, vec2(intensity, 0.0)).rgb;
}
'''
else
textureGradient = null
getColorFun = '''
vec3 getColor(float intensity){
vec3 blue = vec3(0.0, 0.0, 1.0);
vec3 cyan = vec3(0.0, 1.0, 1.0);
vec3 green = vec3(0.0, 1.0, 0.0);
vec3 yellow = vec3(1.0, 1.0, 0.0);
vec3 red = vec3(1.0, 0.0, 0.0);
vec3 color = (
fade(-0.25, 0.25, intensity)*blue +
fade(0.0, 0.5, intensity)*cyan +
fade(0.25, 0.75, intensity)*green +
fade(0.5, 1.0, intensity)*yellow +
smoothstep(0.75, 1.0, intensity)*red
);
return color;
}
'''
intensityToAlpha ?= true
if intensityToAlpha
[alphaStart, alphaEnd] = alphaRange ? [0,1]
output = """
vec4 alphaFun(vec3 color, float intensity){
float alpha = smoothstep(#{alphaStart.toFixed(8)}, #{alphaEnd.toFixed(8)}, intensity);
return vec4(color*alpha, alpha);
}
"""
else
output = '''
vec4 alphaFun(vec3 color, float intensity){
return vec4(color, 1.0);
}
'''
@shader = new Shader @gl,
vertex: vertexShaderBlit
fragment: fragmentShaderBlit + """
float linstep(float low, float high, float value){
return clamp((value-low)/(high-low), 0.0, 1.0);
}
float fade(float low, float high, float value){
float mid = (low+high)*0.5;
float range = (high-low)*0.5;
float x = 1.0 - clamp(abs(mid-value)/range, 0.0, 1.0);
return smoothstep(0.0, 1.0, x);
}
#{getColorFun}
#{output}
void main(){
float intensity = smoothstep(0.0, 1.0, texture2D(source, texcoord).r);
vec3 color = getColor(intensity);
gl_FragColor = alphaFun(color, intensity);
}
"""
@width ?= @canvas.offsetWidth or 2
@height ?= @canvas.offsetHeight or 2
@canvas.width = @width
@canvas.height = @height
@gl.viewport(0, 0, @width, @height)
@quad = @gl.createBuffer()
@gl.bindBuffer @gl.ARRAY_BUFFER, @quad
quad = new Float32Array([
-1, -1, 0, 1,
1, -1, 0, 1,
-1, 1, 0, 1,
-1, 1, 0, 1,
1, -1, 0, 1,
1, 1, 0, 1,
])
@gl.bufferData @gl.ARRAY_BUFFER, quad, @gl.STATIC_DRAW
@gl.bindBuffer @gl.ARRAY_BUFFER, null
@heights = new Heights @, @gl, @width, @height
adjustSize: ->
canvasWidth = @canvas.offsetWidth or 2
canvasHeight = @canvas.offsetHeight or 2
if @width isnt canvasWidth or @height isnt canvasHeight
@gl.viewport 0, 0, canvasWidth, canvasHeight
@canvas.width = canvasWidth
@canvas.height = canvasHeight
@width = canvasWidth
@height = canvasHeight
@heights.resize @width, @height
display: ->
@gl.bindBuffer @gl.ARRAY_BUFFER, @quad
@gl.vertexAttribPointer(0, 4, @gl.FLOAT, false, 0, 0)
@heights.nodeFront.bind(0)
if @gradientTexture
@gradientTexture.bind(1)
@shader.use().int('source', 0).int('gradientTexture', 1)
@gl.drawArrays @gl.TRIANGLES, 0, 6
update: ->
@heights.update()
clear: -> @heights.clear()
clamp: (min=0, max=1) -> @heights.clamp(min, max)
multiply: (value=0.95) -> @heights.multiply(value)
blur: -> @heights.blur()
addPoint: (x, y, size, intensity) -> @heights.addPoint x, y, size, intensity
window.createWebGLHeatmap = (params) -> new WebGLHeatmap(params)