'use strict'; var vertexShaderSource = [ 'precision highp float;', '', 'varying vec4 fragColor;', '', 'attribute vec4 p01_04, p05_08, p09_12, p13_16,', ' p17_20, p21_24, p25_28, p29_32,', ' p33_36, p37_40, p41_44, p45_48,', ' p49_52, p53_56, p57_60, colors;', '', 'uniform mat4 dim0A, dim1A, dim0B, dim1B, dim0C, dim1C, dim0D, dim1D,', ' loA, hiA, loB, hiB, loC, hiC, loD, hiD;', '', 'uniform vec2 resolution, viewBoxPos, viewBoxSize;', 'uniform float maskHeight;', 'uniform float drwLayer; // 0: context, 1: focus, 2: pick', 'uniform vec4 contextColor;', 'uniform sampler2D maskTexture, palette;', '', 'bool isPick = (drwLayer > 1.5);', 'bool isContext = (drwLayer < 0.5);', '', 'const vec4 ZEROS = vec4(0.0, 0.0, 0.0, 0.0);', 'const vec4 UNITS = vec4(1.0, 1.0, 1.0, 1.0);', '', 'float val(mat4 p, mat4 v) {', ' return dot(matrixCompMult(p, v) * UNITS, UNITS);', '}', '', 'float axisY(float ratio, mat4 A, mat4 B, mat4 C, mat4 D) {', ' float y1 = val(A, dim0A) + val(B, dim0B) + val(C, dim0C) + val(D, dim0D);', ' float y2 = val(A, dim1A) + val(B, dim1B) + val(C, dim1C) + val(D, dim1D);', ' return y1 * (1.0 - ratio) + y2 * ratio;', '}', '', 'int iMod(int a, int b) {', ' return a - b * (a / b);', '}', '', 'bool fOutside(float p, float lo, float hi) {', ' return (lo < hi) && (lo > p || p > hi);', '}', '', 'bool vOutside(vec4 p, vec4 lo, vec4 hi) {', ' return (', ' fOutside(p[0], lo[0], hi[0]) ||', ' fOutside(p[1], lo[1], hi[1]) ||', ' fOutside(p[2], lo[2], hi[2]) ||', ' fOutside(p[3], lo[3], hi[3])', ' );', '}', '', 'bool mOutside(mat4 p, mat4 lo, mat4 hi) {', ' return (', ' vOutside(p[0], lo[0], hi[0]) ||', ' vOutside(p[1], lo[1], hi[1]) ||', ' vOutside(p[2], lo[2], hi[2]) ||', ' vOutside(p[3], lo[3], hi[3])', ' );', '}', '', 'bool outsideBoundingBox(mat4 A, mat4 B, mat4 C, mat4 D) {', ' return mOutside(A, loA, hiA) ||', ' mOutside(B, loB, hiB) ||', ' mOutside(C, loC, hiC) ||', ' mOutside(D, loD, hiD);', '}', '', 'bool outsideRasterMask(mat4 A, mat4 B, mat4 C, mat4 D) {', ' mat4 pnts[4];', ' pnts[0] = A;', ' pnts[1] = B;', ' pnts[2] = C;', ' pnts[3] = D;', '', ' for(int i = 0; i < 4; ++i) {', ' for(int j = 0; j < 4; ++j) {', ' for(int k = 0; k < 4; ++k) {', ' if(0 == iMod(', ' int(255.0 * texture2D(maskTexture,', ' vec2(', ' (float(i * 2 + j / 2) + 0.5) / 8.0,', ' (pnts[i][j][k] * (maskHeight - 1.0) + 1.0) / maskHeight', ' ))[3]', ' ) / int(pow(2.0, float(iMod(j * 4 + k, 8)))),', ' 2', ' )) return true;', ' }', ' }', ' }', ' return false;', '}', '', 'vec4 position(bool isContext, float v, mat4 A, mat4 B, mat4 C, mat4 D) {', ' float x = 0.5 * sign(v) + 0.5;', ' float y = axisY(x, A, B, C, D);', ' float z = 1.0 - abs(v);', '', ' z += isContext ? 0.0 : 2.0 * float(', ' outsideBoundingBox(A, B, C, D) ||', ' outsideRasterMask(A, B, C, D)', ' );', '', ' return vec4(', ' 2.0 * (vec2(x, y) * viewBoxSize + viewBoxPos) / resolution - 1.0,', ' z,', ' 1.0', ' );', '}', '', 'void main() {', ' mat4 A = mat4(p01_04, p05_08, p09_12, p13_16);', ' mat4 B = mat4(p17_20, p21_24, p25_28, p29_32);', ' mat4 C = mat4(p33_36, p37_40, p41_44, p45_48);', ' mat4 D = mat4(p49_52, p53_56, p57_60, ZEROS);', '', ' float v = colors[3];', '', ' gl_Position = position(isContext, v, A, B, C, D);', '', ' fragColor =', ' isContext ? vec4(contextColor) :', ' isPick ? vec4(colors.rgb, 1.0) : texture2D(palette, vec2(abs(v), 0.5));', '}' ].join('\n'); var fragmentShaderSource = [ 'precision highp float;', '', 'varying vec4 fragColor;', '', 'void main() {', ' gl_FragColor = fragColor;', '}' ].join('\n'); var maxDim = require('./constants').maxDimensionCount; var Lib = require('../../lib'); // don't change; otherwise near/far plane lines are lost var depthLimitEpsilon = 1e-6; // precision of multiselect is the full range divided into this many parts var maskHeight = 2048; var dummyPixel = new Uint8Array(4); var dataPixel = new Uint8Array(4); var paletteTextureConfig = { shape: [256, 1], format: 'rgba', type: 'uint8', mag: 'nearest', min: 'nearest' }; function ensureDraw(regl) { regl.read({ x: 0, y: 0, width: 1, height: 1, data: dummyPixel }); } function clear(regl, x, y, width, height) { var gl = regl._gl; gl.enable(gl.SCISSOR_TEST); gl.scissor(x, y, width, height); regl.clear({color: [0, 0, 0, 0], depth: 1}); // clearing is done in scissored panel only } function renderBlock(regl, glAes, renderState, blockLineCount, sampleCount, item) { var rafKey = item.key; function render(blockNumber) { var count = Math.min(blockLineCount, sampleCount - blockNumber * blockLineCount); if(blockNumber === 0) { // stop drawing possibly stale glyphs before clearing window.cancelAnimationFrame(renderState.currentRafs[rafKey]); delete renderState.currentRafs[rafKey]; clear(regl, item.scissorX, item.scissorY, item.scissorWidth, item.viewBoxSize[1]); } if(renderState.clearOnly) { return; } item.count = 2 * count; item.offset = 2 * blockNumber * blockLineCount; glAes(item); if(blockNumber * blockLineCount + count < sampleCount) { renderState.currentRafs[rafKey] = window.requestAnimationFrame(function() { render(blockNumber + 1); }); } renderState.drawCompleted = false; } if(!renderState.drawCompleted) { ensureDraw(regl); renderState.drawCompleted = true; } // start with rendering item 0; recursion handles the rest render(0); } function adjustDepth(d) { // WebGL matrix operations use floats with limited precision, potentially causing a number near a border of [0, 1] // to end up slightly outside the border. With an epsilon, we reduce the chance that a line gets clipped by the // near or the far plane. return Math.max(depthLimitEpsilon, Math.min(1 - depthLimitEpsilon, d)); } function palette(unitToColor, opacity) { var result = new Array(256); for(var i = 0; i < 256; i++) { result[i] = unitToColor(i / 255).concat(opacity); } return result; } // Maps the sample index [0...sampleCount - 1] to a range of [0, 1] as the shader expects colors in the [0, 1] range. // but first it shifts the sample index by 0, 8 or 16 bits depending on rgbIndex [0..2] // with the end result that each line will be of a unique color, making it possible for the pick handler // to uniquely identify which line is hovered over (bijective mapping). // The inverse, i.e. readPixel is invoked from 'parcoords.js' function calcPickColor(i, rgbIndex) { return (i >>> 8 * rgbIndex) % 256 / 255; } function makePoints(sampleCount, dims, color) { var points = new Array(sampleCount * (maxDim + 4)); var n = 0; for(var i = 0; i < sampleCount; i++) { for(var k = 0; k < maxDim; k++) { points[n++] = (k < dims.length) ? dims[k].paddedUnitValues[i] : 0.5; } points[n++] = calcPickColor(i, 2); points[n++] = calcPickColor(i, 1); points[n++] = calcPickColor(i, 0); points[n++] = adjustDepth(color[i]); } return points; } function makeVecAttr(vecIndex, sampleCount, points) { var pointPairs = new Array(sampleCount * 8); var n = 0; for(var i = 0; i < sampleCount; i++) { for(var j = 0; j < 2; j++) { for(var k = 0; k < 4; k++) { var q = vecIndex * 4 + k; var v = points[i * 64 + q]; if(q === 63 && j === 0) { v *= -1; } pointPairs[n++] = v; } } } return pointPairs; } function pad2(num) { var s = '0' + num; return s.substr(s.length - 2); } function getAttrName(i) { return (i < maxDim) ? 'p' + pad2(i + 1) + '_' + pad2(i + 4) : 'colors'; } function setAttributes(attributes, sampleCount, points) { for(var i = 0; i <= maxDim; i += 4) { attributes[getAttrName(i)](makeVecAttr(i / 4, sampleCount, points)); } } function emptyAttributes(regl) { var attributes = {}; for(var i = 0; i <= maxDim; i += 4) { attributes[getAttrName(i)] = regl.buffer({usage: 'dynamic', type: 'float', data: new Uint8Array(0)}); } return attributes; } function makeItem( model, leftmost, rightmost, itemNumber, i0, i1, x, y, panelSizeX, panelSizeY, crossfilterDimensionIndex, drwLayer, constraints, plotGlPixelRatio ) { var dims = [[], []]; for(var k = 0; k < 64; k++) { dims[0][k] = (k === i0) ? 1 : 0; dims[1][k] = (k === i1) ? 1 : 0; } x *= plotGlPixelRatio; y *= plotGlPixelRatio; panelSizeX *= plotGlPixelRatio; panelSizeY *= plotGlPixelRatio; var overdrag = model.lines.canvasOverdrag * plotGlPixelRatio; var domain = model.domain; var canvasWidth = model.canvasWidth * plotGlPixelRatio; var canvasHeight = model.canvasHeight * plotGlPixelRatio; var padL = model.pad.l * plotGlPixelRatio; var padB = model.pad.b * plotGlPixelRatio; var layoutHeight = model.layoutHeight * plotGlPixelRatio; var layoutWidth = model.layoutWidth * plotGlPixelRatio; var deselectedLinesColor = model.deselectedLines.color; var deselectedLinesOpacity = model.deselectedLines.opacity; var itemModel = Lib.extendFlat({ key: crossfilterDimensionIndex, resolution: [canvasWidth, canvasHeight], viewBoxPos: [x + overdrag, y], viewBoxSize: [panelSizeX, panelSizeY], i0: i0, i1: i1, dim0A: dims[0].slice(0, 16), dim0B: dims[0].slice(16, 32), dim0C: dims[0].slice(32, 48), dim0D: dims[0].slice(48, 64), dim1A: dims[1].slice(0, 16), dim1B: dims[1].slice(16, 32), dim1C: dims[1].slice(32, 48), dim1D: dims[1].slice(48, 64), drwLayer: drwLayer, contextColor: [ deselectedLinesColor[0] / 255, deselectedLinesColor[1] / 255, deselectedLinesColor[2] / 255, deselectedLinesOpacity !== 'auto' ? deselectedLinesColor[3] * deselectedLinesOpacity : Math.max(1 / 255, Math.pow(1 / model.lines.color.length, 1 / 3)) ], scissorX: (itemNumber === leftmost ? 0 : x + overdrag) + (padL - overdrag) + layoutWidth * domain.x[0], scissorWidth: (itemNumber === rightmost ? canvasWidth - x + overdrag : panelSizeX + 0.5) + (itemNumber === leftmost ? x + overdrag : 0), scissorY: y + padB + layoutHeight * domain.y[0], scissorHeight: panelSizeY, viewportX: padL - overdrag + layoutWidth * domain.x[0], viewportY: padB + layoutHeight * domain.y[0], viewportWidth: canvasWidth, viewportHeight: canvasHeight }, constraints); return itemModel; } function expandedPixelRange(bounds) { var dh = maskHeight - 1; var a = Math.max(0, Math.floor(bounds[0] * dh), 0); var b = Math.min(dh, Math.ceil(bounds[1] * dh), dh); return [ Math.min(a, b), Math.max(a, b) ]; } module.exports = function(canvasGL, d) { // context & pick describe which canvas we're talking about - won't change with new data var isContext = d.context; var isPick = d.pick; var regl = d.regl; var gl = regl._gl; var supportedLineWidth = gl.getParameter(gl.ALIASED_LINE_WIDTH_RANGE); // ensure here that plotGlPixelRatio is within supported range; otherwise regl throws error var plotGlPixelRatio = Math.max( supportedLineWidth[0], Math.min( supportedLineWidth[1], d.viewModel.plotGlPixelRatio ) ); var renderState = { currentRafs: {}, drawCompleted: true, clearOnly: false }; // state to be set by update and used later var model; var vm; var initialDims; var sampleCount; var attributes = emptyAttributes(regl); var maskTexture; var paletteTexture = regl.texture(paletteTextureConfig); var prevAxisOrder = []; update(d); var glAes = regl({ profile: false, blend: { enable: isContext, func: { srcRGB: 'src alpha', dstRGB: 'one minus src alpha', srcAlpha: 1, dstAlpha: 1 // 'one minus src alpha' }, equation: { rgb: 'add', alpha: 'add' }, color: [0, 0, 0, 0] }, depth: { enable: !isContext, mask: true, func: 'less', range: [0, 1] }, // for polygons cull: { enable: true, face: 'back' }, scissor: { enable: true, box: { x: regl.prop('scissorX'), y: regl.prop('scissorY'), width: regl.prop('scissorWidth'), height: regl.prop('scissorHeight') } }, viewport: { x: regl.prop('viewportX'), y: regl.prop('viewportY'), width: regl.prop('viewportWidth'), height: regl.prop('viewportHeight') }, dither: false, vert: vertexShaderSource, frag: fragmentShaderSource, primitive: 'lines', lineWidth: plotGlPixelRatio, attributes: attributes, uniforms: { resolution: regl.prop('resolution'), viewBoxPos: regl.prop('viewBoxPos'), viewBoxSize: regl.prop('viewBoxSize'), dim0A: regl.prop('dim0A'), dim1A: regl.prop('dim1A'), dim0B: regl.prop('dim0B'), dim1B: regl.prop('dim1B'), dim0C: regl.prop('dim0C'), dim1C: regl.prop('dim1C'), dim0D: regl.prop('dim0D'), dim1D: regl.prop('dim1D'), loA: regl.prop('loA'), hiA: regl.prop('hiA'), loB: regl.prop('loB'), hiB: regl.prop('hiB'), loC: regl.prop('loC'), hiC: regl.prop('hiC'), loD: regl.prop('loD'), hiD: regl.prop('hiD'), palette: paletteTexture, contextColor: regl.prop('contextColor'), maskTexture: regl.prop('maskTexture'), drwLayer: regl.prop('drwLayer'), maskHeight: regl.prop('maskHeight') }, offset: regl.prop('offset'), count: regl.prop('count') }); function update(dNew) { model = dNew.model; vm = dNew.viewModel; initialDims = vm.dimensions.slice(); sampleCount = initialDims[0] ? initialDims[0].values.length : 0; var lines = model.lines; var color = isPick ? lines.color.map(function(_, i) {return i / lines.color.length;}) : lines.color; var points = makePoints(sampleCount, initialDims, color); setAttributes(attributes, sampleCount, points); if(!isContext && !isPick) { paletteTexture = regl.texture(Lib.extendFlat({ data: palette(model.unitToColor, 255) }, paletteTextureConfig)); } } function makeConstraints(isContext) { var i, j, k; var limits = [[], []]; for(k = 0; k < 64; k++) { var p = (!isContext && k < initialDims.length) ? initialDims[k].brush.filter.getBounds() : [-Infinity, Infinity]; limits[0][k] = p[0]; limits[1][k] = p[1]; } var len = maskHeight * 8; var mask = new Array(len); for(i = 0; i < len; i++) { mask[i] = 255; } if(!isContext) { for(i = 0; i < initialDims.length; i++) { var u = i % 8; var v = (i - u) / 8; var bitMask = Math.pow(2, u); var dim = initialDims[i]; var ranges = dim.brush.filter.get(); if(ranges.length < 2) continue; // bail if the bounding box based filter is sufficient var prevEnd = expandedPixelRange(ranges[0])[1]; for(j = 1; j < ranges.length; j++) { var nextRange = expandedPixelRange(ranges[j]); for(k = prevEnd + 1; k < nextRange[0]; k++) { mask[k * 8 + v] &= ~bitMask; } prevEnd = Math.max(prevEnd, nextRange[1]); } } } var textureData = { // 8 units x 8 bits = 64 bits, just sufficient for the almost 64 dimensions we support shape: [8, maskHeight], format: 'alpha', type: 'uint8', mag: 'nearest', min: 'nearest', data: mask }; if(maskTexture) maskTexture(textureData); else maskTexture = regl.texture(textureData); return { maskTexture: maskTexture, maskHeight: maskHeight, loA: limits[0].slice(0, 16), loB: limits[0].slice(16, 32), loC: limits[0].slice(32, 48), loD: limits[0].slice(48, 64), hiA: limits[1].slice(0, 16), hiB: limits[1].slice(16, 32), hiC: limits[1].slice(32, 48), hiD: limits[1].slice(48, 64), }; } function renderGLParcoords(panels, setChanged, clearOnly) { var panelCount = panels.length; var i; var leftmost; var rightmost; var lowestX = Infinity; var highestX = -Infinity; for(i = 0; i < panelCount; i++) { if(panels[i].dim0.canvasX < lowestX) { lowestX = panels[i].dim0.canvasX; leftmost = i; } if(panels[i].dim1.canvasX > highestX) { highestX = panels[i].dim1.canvasX; rightmost = i; } } if(panelCount === 0) { // clear canvas here, as the panel iteration below will not enter the loop body clear(regl, 0, 0, model.canvasWidth, model.canvasHeight); } var constraints = makeConstraints(isContext); for(i = 0; i < panelCount; i++) { var p = panels[i]; var i0 = p.dim0.crossfilterDimensionIndex; var i1 = p.dim1.crossfilterDimensionIndex; var x = p.canvasX; var y = p.canvasY; var nextX = x + p.panelSizeX; var plotGlPixelRatio = p.plotGlPixelRatio; if(setChanged || !prevAxisOrder[i0] || prevAxisOrder[i0][0] !== x || prevAxisOrder[i0][1] !== nextX ) { prevAxisOrder[i0] = [x, nextX]; var item = makeItem( model, leftmost, rightmost, i, i0, i1, x, y, p.panelSizeX, p.panelSizeY, p.dim0.crossfilterDimensionIndex, isContext ? 0 : isPick ? 2 : 1, constraints, plotGlPixelRatio ); renderState.clearOnly = clearOnly; var blockLineCount = setChanged ? model.lines.blockLineCount : sampleCount; renderBlock( regl, glAes, renderState, blockLineCount, sampleCount, item ); } } } function readPixel(canvasX, canvasY) { regl.read({ x: canvasX, y: canvasY, width: 1, height: 1, data: dataPixel }); return dataPixel; } function readPixels(canvasX, canvasY, width, height) { var pixelArray = new Uint8Array(4 * width * height); regl.read({ x: canvasX, y: canvasY, width: width, height: height, data: pixelArray }); return pixelArray; } function destroy() { canvasGL.style['pointer-events'] = 'none'; paletteTexture.destroy(); if(maskTexture) maskTexture.destroy(); for(var k in attributes) attributes[k].destroy(); } return { render: renderGLParcoords, readPixel: readPixel, readPixels: readPixels, destroy: destroy, update: update }; };