regl-scatter2d
Version:
Scatter2d plot built with regl
951 lines (889 loc) • 34.7 kB
JavaScript
'use strict';
function _iterableToArrayLimit(arr, i) {
var _i = null == arr ? null : "undefined" != typeof Symbol && arr[Symbol.iterator] || arr["@@iterator"];
if (null != _i) {
var _s,
_e,
_x,
_r,
_arr = [],
_n = !0,
_d = !1;
try {
if (_x = (_i = _i.call(arr)).next, 0 === i) {
if (Object(_i) !== _i) return;
_n = !1;
} else for (; !(_n = (_s = _x.call(_i)).done) && (_arr.push(_s.value), _arr.length !== i); _n = !0);
} catch (err) {
_d = !0, _e = err;
} finally {
try {
if (!_n && null != _i.return && (_r = _i.return(), Object(_r) !== _r)) return;
} finally {
if (_d) throw _e;
}
}
return _arr;
}
}
function _slicedToArray(arr, i) {
return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest();
}
function _toConsumableArray(arr) {
return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread();
}
function _arrayWithoutHoles(arr) {
if (Array.isArray(arr)) return _arrayLikeToArray(arr);
}
function _arrayWithHoles(arr) {
if (Array.isArray(arr)) return arr;
}
function _iterableToArray(iter) {
if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter);
}
function _unsupportedIterableToArray(o, minLen) {
if (!o) return;
if (typeof o === "string") return _arrayLikeToArray(o, minLen);
var n = Object.prototype.toString.call(o).slice(8, -1);
if (n === "Object" && o.constructor) n = o.constructor.name;
if (n === "Map" || n === "Set") return Array.from(o);
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
}
function _arrayLikeToArray(arr, len) {
if (len == null || len > arr.length) len = arr.length;
for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i];
return arr2;
}
function _nonIterableSpread() {
throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
function _nonIterableRest() {
throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
var rgba = require('color-normalize');
var getBounds = require('array-bounds');
var colorId = require('color-id');
var cluster = require('@plotly/point-cluster');
var extend = require('object-assign');
var glslify = require('glslify');
var pick = require('pick-by-alias');
var updateDiff = require('update-diff');
var flatten = require('flatten-vertex-data');
var ie = require('is-iexplorer');
var f32 = require('to-float32');
var parseRect = require('parse-rect');
var scatter = Scatter;
function Scatter(regl, options) {
var _this = this;
if (!(this instanceof Scatter)) return new Scatter(regl, options);
if (typeof regl === 'function') {
if (!options) options = {};
options.regl = regl;
} else {
options = regl;
regl = null;
}
if (options && options.length) options.positions = options;
regl = options.regl;
// persistent variables
var gl = regl._gl,
paletteTexture,
palette = [],
paletteIds = {},
// state
groups = [],
// textures for marker keys
markerTextures = [null],
markerCache = [null];
var maxColors = 255,
maxSize = 100;
// direct color buffer mode
// IE does not support palette anyways
this.tooManyColors = ie;
// texture with color palette
paletteTexture = regl.texture({
data: new Uint8Array(maxColors * 4),
width: maxColors,
height: 1,
type: 'uint8',
format: 'rgba',
wrapS: 'clamp',
wrapT: 'clamp',
mag: 'nearest',
min: 'nearest'
});
extend(this, {
regl: regl,
gl: gl,
groups: groups,
markerCache: markerCache,
markerTextures: markerTextures,
palette: palette,
paletteIds: paletteIds,
paletteTexture: paletteTexture,
maxColors: maxColors,
maxSize: maxSize,
canvas: gl.canvas
});
this.update(options);
// common shader options
var shaderOptions = {
uniforms: {
constPointSize: !!options.constPointSize,
opacity: regl.prop('opacity'),
paletteSize: function paletteSize(ctx, prop) {
return [_this.tooManyColors ? 0 : maxColors, paletteTexture.height];
},
pixelRatio: regl.context('pixelRatio'),
scale: regl.prop('scale'),
scaleFract: regl.prop('scaleFract'),
translate: regl.prop('translate'),
translateFract: regl.prop('translateFract'),
markerTexture: regl.prop('markerTexture'),
paletteTexture: paletteTexture
},
attributes: {
// FIXME: optimize these parts
x: function x(ctx, prop) {
return prop.xAttr || {
buffer: prop.positionBuffer,
stride: 8,
offset: 0
};
},
y: function y(ctx, prop) {
return prop.yAttr || {
buffer: prop.positionBuffer,
stride: 8,
offset: 4
};
},
xFract: function xFract(ctx, prop) {
return prop.xAttr ? {
constant: [0, 0]
} : {
buffer: prop.positionFractBuffer,
stride: 8,
offset: 0
};
},
yFract: function yFract(ctx, prop) {
return prop.yAttr ? {
constant: [0, 0]
} : {
buffer: prop.positionFractBuffer,
stride: 8,
offset: 4
};
},
size: function size(ctx, prop) {
return prop.size.length ? {
buffer: prop.sizeBuffer,
stride: 2,
offset: 0
} : {
constant: [Math.round(prop.size * 255 / _this.maxSize)]
};
},
borderSize: function borderSize(ctx, prop) {
return prop.borderSize.length ? {
buffer: prop.sizeBuffer,
stride: 2,
offset: 1
} : {
constant: [Math.round(prop.borderSize * 255 / _this.maxSize)]
};
},
colorId: function colorId(ctx, prop) {
return prop.color.length ? {
buffer: prop.colorBuffer,
stride: _this.tooManyColors ? 8 : 4,
offset: 0
} : {
constant: _this.tooManyColors ? palette.slice(prop.color * 4, prop.color * 4 + 4) : [prop.color]
};
},
borderColorId: function borderColorId(ctx, prop) {
return prop.borderColor.length ? {
buffer: prop.colorBuffer,
stride: _this.tooManyColors ? 8 : 4,
offset: _this.tooManyColors ? 4 : 2
} : {
constant: _this.tooManyColors ? palette.slice(prop.borderColor * 4, prop.borderColor * 4 + 4) : [prop.borderColor]
};
},
isActive: function isActive(ctx, prop) {
return prop.activation === true ? {
constant: [1]
} : prop.activation ? prop.activation : {
constant: [0]
};
}
},
blend: {
enable: true,
color: [0, 0, 0, 1],
// photoshop blending
func: {
srcRGB: 'src alpha',
dstRGB: 'one minus src alpha',
srcAlpha: 'one minus dst alpha',
dstAlpha: 'one'
}
},
scissor: {
enable: true,
box: regl.prop('viewport')
},
viewport: regl.prop('viewport'),
stencil: {
enable: false
},
depth: {
enable: false
},
elements: regl.prop('elements'),
count: regl.prop('count'),
offset: regl.prop('offset'),
primitive: 'points'
};
// draw sdf-marker
var markerOptions = extend({}, shaderOptions);
markerOptions.frag = glslify(["precision highp float;\n#define GLSLIFY 1\n\nuniform float opacity;\nuniform sampler2D markerTexture;\n\nvarying vec4 fragColor, fragBorderColor;\nvarying float fragWidth, fragBorderColorLevel, fragColorLevel;\n\nfloat smoothStep(float x, float y) {\n return 1.0 / (1.0 + exp(50.0*(x - y)));\n}\n\nvoid main() {\n float dist = texture2D(markerTexture, gl_PointCoord).r, delta = fragWidth;\n\n // max-distance alpha\n if (dist < 0.003) discard;\n\n // null-border case\n if (fragBorderColorLevel == fragColorLevel || fragBorderColor.a == 0.) {\n float colorAmt = smoothstep(.5 - delta, .5 + delta, dist);\n gl_FragColor = vec4(fragColor.rgb, colorAmt * fragColor.a * opacity);\n }\n else {\n float borderColorAmt = smoothstep(fragBorderColorLevel - delta, fragBorderColorLevel + delta, dist);\n float colorAmt = smoothstep(fragColorLevel - delta, fragColorLevel + delta, dist);\n\n vec4 color = fragBorderColor;\n color.a *= borderColorAmt;\n color = mix(color, fragColor, colorAmt);\n color.a *= opacity;\n\n gl_FragColor = color;\n }\n\n}\n"]);
markerOptions.vert = glslify(["precision highp float;\n#define GLSLIFY 1\n\nattribute float x, y, xFract, yFract;\nattribute float size, borderSize;\nattribute vec4 colorId, borderColorId;\nattribute float isActive;\n\n// `invariant` effectively turns off optimizations for the position.\n// We need this because -fast-math on M1 Macs is re-ordering\n// floating point operations in a way that causes floating point\n// precision limits to put points in the wrong locations.\ninvariant gl_Position;\n\nuniform bool constPointSize;\nuniform float pixelRatio;\nuniform vec2 scale, scaleFract, translate, translateFract, paletteSize;\nuniform sampler2D paletteTexture;\n\nconst float maxSize = 100.;\nconst float borderLevel = .5;\n\nvarying vec4 fragColor, fragBorderColor;\nvarying float fragPointSize, fragBorderRadius, fragWidth, fragBorderColorLevel, fragColorLevel;\n\nfloat pointSizeScale = (constPointSize) ? 2. : pixelRatio;\n\nbool isDirect = (paletteSize.x < 1.);\n\nvec4 getColor(vec4 id) {\n return isDirect ? id / 255. : texture2D(paletteTexture,\n vec2(\n (id.x + .5) / paletteSize.x,\n (id.y + .5) / paletteSize.y\n )\n );\n}\n\nvoid main() {\n // ignore inactive points\n if (isActive == 0.) return;\n\n vec2 position = vec2(x, y);\n vec2 positionFract = vec2(xFract, yFract);\n\n vec4 color = getColor(colorId);\n vec4 borderColor = getColor(borderColorId);\n\n float size = size * maxSize / 255.;\n float borderSize = borderSize * maxSize / 255.;\n\n gl_PointSize = 2. * size * pointSizeScale;\n fragPointSize = size * pixelRatio;\n\n vec2 pos = (position + translate) * scale\n + (positionFract + translateFract) * scale\n + (position + translate) * scaleFract\n + (positionFract + translateFract) * scaleFract;\n\n gl_Position = vec4(pos * 2. - 1., 0., 1.);\n\n fragColor = color;\n fragBorderColor = borderColor;\n fragWidth = 1. / gl_PointSize;\n\n fragBorderColorLevel = clamp(borderLevel - borderLevel * borderSize / size, 0., 1.);\n fragColorLevel = clamp(borderLevel + (1. - borderLevel) * borderSize / size, 0., 1.);\n}\n"]);
this.drawMarker = regl(markerOptions);
// draw circle
var circleOptions = extend({}, shaderOptions);
circleOptions.frag = glslify(["precision highp float;\n#define GLSLIFY 1\n\nvarying vec4 fragColor, fragBorderColor;\nvarying float fragBorderRadius, fragWidth;\n\nuniform float opacity;\n\nfloat smoothStep(float edge0, float edge1, float x) {\n\tfloat t;\n\tt = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);\n\treturn t * t * (3.0 - 2.0 * t);\n}\n\nvoid main() {\n\tfloat radius, alpha = 1.0, delta = fragWidth;\n\n\tradius = length(2.0 * gl_PointCoord.xy - 1.0);\n\n\tif (radius > 1.0 + delta) {\n\t\tdiscard;\n\t}\n\n\talpha -= smoothstep(1.0 - delta, 1.0 + delta, radius);\n\n\tfloat borderRadius = fragBorderRadius;\n\tfloat ratio = smoothstep(borderRadius - delta, borderRadius + delta, radius);\n\tvec4 color = mix(fragColor, fragBorderColor, ratio);\n\tcolor.a *= alpha * opacity;\n\tgl_FragColor = color;\n}\n"]);
circleOptions.vert = glslify(["precision highp float;\n#define GLSLIFY 1\n\nattribute float x, y, xFract, yFract;\nattribute float size, borderSize;\nattribute vec4 colorId, borderColorId;\nattribute float isActive;\n\n// `invariant` effectively turns off optimizations for the position.\n// We need this because -fast-math on M1 Macs is re-ordering\n// floating point operations in a way that causes floating point\n// precision limits to put points in the wrong locations.\ninvariant gl_Position;\n\nuniform bool constPointSize;\nuniform float pixelRatio;\nuniform vec2 paletteSize, scale, scaleFract, translate, translateFract;\nuniform sampler2D paletteTexture;\n\nconst float maxSize = 100.;\n\nvarying vec4 fragColor, fragBorderColor;\nvarying float fragBorderRadius, fragWidth;\n\nfloat pointSizeScale = (constPointSize) ? 2. : pixelRatio;\n\nbool isDirect = (paletteSize.x < 1.);\n\nvec4 getColor(vec4 id) {\n return isDirect ? id / 255. : texture2D(paletteTexture,\n vec2(\n (id.x + .5) / paletteSize.x,\n (id.y + .5) / paletteSize.y\n )\n );\n}\n\nvoid main() {\n // ignore inactive points\n if (isActive == 0.) return;\n\n vec2 position = vec2(x, y);\n vec2 positionFract = vec2(xFract, yFract);\n\n vec4 color = getColor(colorId);\n vec4 borderColor = getColor(borderColorId);\n\n float size = size * maxSize / 255.;\n float borderSize = borderSize * maxSize / 255.;\n\n gl_PointSize = (size + borderSize) * pointSizeScale;\n\n vec2 pos = (position + translate) * scale\n + (positionFract + translateFract) * scale\n + (position + translate) * scaleFract\n + (positionFract + translateFract) * scaleFract;\n\n gl_Position = vec4(pos * 2. - 1., 0., 1.);\n\n fragBorderRadius = 1. - 2. * borderSize / (size + borderSize);\n fragColor = color;\n fragBorderColor = borderColor.a == 0. || borderSize == 0. ? vec4(color.rgb, 0.) : borderColor;\n fragWidth = 1. / gl_PointSize;\n}\n"]);
// polyfill IE
if (ie) {
circleOptions.frag = circleOptions.frag.replace('smoothstep', 'smoothStep');
markerOptions.frag = markerOptions.frag.replace('smoothstep', 'smoothStep');
}
this.drawCircle = regl(circleOptions);
}
// single pass defaults
Scatter.defaults = {
color: 'black',
borderColor: 'transparent',
borderSize: 0,
size: 12,
opacity: 1,
marker: undefined,
viewport: null,
range: null,
pixelSize: null,
count: 0,
offset: 0,
bounds: null,
positions: [],
snap: 1e4
};
// update & redraw
Scatter.prototype.render = function () {
if (arguments.length) {
this.update.apply(this, arguments);
}
this.draw();
return this;
};
// draw all groups or only indicated ones
Scatter.prototype.draw = function () {
var _this2 = this;
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
var groups = this.groups;
// if directly array passed - treat as passes
if (args.length === 1 && Array.isArray(args[0]) && (args[0][0] === null || Array.isArray(args[0][0]))) {
args = args[0];
}
// FIXME: remove once https://github.com/regl-project/regl/issues/474 resolved
this.regl._refresh();
if (args.length) {
for (var i = 0; i < args.length; i++) {
this.drawItem(i, args[i]);
}
}
// draw all passes
else {
groups.forEach(function (group, i) {
_this2.drawItem(i);
});
}
return this;
};
// draw specific scatter group
Scatter.prototype.drawItem = function (id, els) {
var groups = this.groups;
var group = groups[id];
// debug viewport
// let { viewport } = group
// gl.enable(gl.SCISSOR_TEST);
// gl.scissor(viewport.x, viewport.y, viewport.width, viewport.height);
// gl.clearColor(0, 0, 0, .5);
// gl.clear(gl.COLOR_BUFFER_BIT);
if (typeof els === 'number') {
id = els;
group = groups[els];
els = null;
}
if (!(group && group.count && group.opacity)) return;
// draw circles
if (group.activation[0]) {
// TODO: optimize this performance by making groups and regl.this props
this.drawCircle(this.getMarkerDrawOptions(0, group, els));
}
// draw all other available markers
var batch = [];
for (var i = 1; i < group.activation.length; i++) {
if (!group.activation[i] || group.activation[i] !== true && !group.activation[i].data.length) continue;
batch.push.apply(batch, _toConsumableArray(this.getMarkerDrawOptions(i, group, els)));
}
if (batch.length) {
this.drawMarker(batch);
}
};
// get options for the marker ids
Scatter.prototype.getMarkerDrawOptions = function (markerId, group, elements) {
var range = group.range,
tree = group.tree,
viewport = group.viewport,
activation = group.activation,
selectionBuffer = group.selectionBuffer,
count = group.count;
var regl = this.regl;
// direct points
if (!tree) {
// if elements array - draw unclustered points
if (elements) {
return [extend({}, group, {
markerTexture: this.markerTextures[markerId],
activation: activation[markerId],
count: elements.length,
elements: elements,
offset: 0
})];
}
return [extend({}, group, {
markerTexture: this.markerTextures[markerId],
activation: activation[markerId],
offset: 0
})];
}
// clustered points
var batch = [];
var lod = tree.range(range, {
lod: true,
px: [(range[2] - range[0]) / viewport.width, (range[3] - range[1]) / viewport.height]
});
// enable elements by using selection buffer
if (elements) {
var markerActivation = activation[markerId];
var mask = markerActivation.data;
var data = new Uint8Array(count);
for (var i = 0; i < elements.length; i++) {
var id = elements[i];
data[id] = mask ? mask[id] : 1;
}
selectionBuffer.subdata(data);
}
for (var l = lod.length; l--;) {
var _lod$l = _slicedToArray(lod[l], 2),
from = _lod$l[0],
to = _lod$l[1];
batch.push(extend({}, group, {
markerTexture: this.markerTextures[markerId],
activation: elements ? selectionBuffer : activation[markerId],
offset: from,
count: to - from
}));
}
return batch;
};
// update groups options
Scatter.prototype.update = function () {
var _this3 = this;
for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
args[_key2] = arguments[_key2];
}
if (!args.length) return;
// passes are as single array
if (args.length === 1 && Array.isArray(args[0])) args = args[0];
var groups = this.groups,
gl = this.gl,
regl = this.regl,
maxSize = this.maxSize,
maxColors = this.maxColors,
palette = this.palette;
this.groups = groups = args.map(function (options, i) {
var group = groups[i];
if (options === undefined) return group;
if (options === null) options = {
positions: null
};else if (typeof options === 'function') options = {
ondraw: options
};else if (typeof options[0] === 'number') options = {
positions: options
};
// copy options to avoid mutation & handle aliases
options = pick(options, {
positions: 'positions data points',
snap: 'snap cluster lod tree',
size: 'sizes size radius',
borderSize: 'borderSizes borderSize border-size bordersize borderWidth borderWidths border-width borderwidth stroke-width strokeWidth strokewidth outline',
color: 'colors color fill fill-color fillColor',
borderColor: 'borderColors borderColor stroke stroke-color strokeColor',
marker: 'markers marker shape',
range: 'range dataBox databox',
viewport: 'viewport viewPort viewBox viewbox',
opacity: 'opacity alpha transparency',
bounds: 'bound bounds boundaries limits',
tooManyColors: 'tooManyColors palette paletteMode optimizePalette enablePalette'
});
if (options.positions === null) options.positions = [];
if (options.tooManyColors != null) _this3.tooManyColors = options.tooManyColors;
if (!group) {
groups[i] = group = {
id: i,
scale: null,
translate: null,
scaleFract: null,
translateFract: null,
// buffers for active markers
activation: [],
// buffer for filtered markers
selectionBuffer: regl.buffer({
data: new Uint8Array(0),
usage: 'stream',
type: 'uint8'
}),
// buffers with data: it is faster to switch them per-pass
// than provide one congregate buffer
sizeBuffer: regl.buffer({
data: new Uint8Array(0),
usage: 'dynamic',
type: 'uint8'
}),
colorBuffer: regl.buffer({
data: new Uint8Array(0),
usage: 'dynamic',
type: 'uint8'
}),
positionBuffer: regl.buffer({
data: new Uint8Array(0),
usage: 'dynamic',
type: 'float'
}),
positionFractBuffer: regl.buffer({
data: new Uint8Array(0),
usage: 'dynamic',
type: 'float'
})
};
options = extend({}, Scatter.defaults, options);
}
// force update triggers
if (options.positions && !('marker' in options)) {
options.marker = group.marker;
delete group.marker;
}
// updating markers cause recalculating snapping
if (options.marker && !('positions' in options)) {
options.positions = group.positions;
delete group.positions;
}
// global count of points
var hasSize = 0,
hasColor = 0;
updateDiff(group, options, [{
snap: true,
size: function size(s, group) {
if (s == null) s = Scatter.defaults.size;
hasSize += s && s.length ? 1 : 0;
return s;
},
borderSize: function borderSize(s, group) {
if (s == null) s = Scatter.defaults.borderSize;
hasSize += s && s.length ? 1 : 0;
return s;
},
opacity: parseFloat,
// add colors to palette, save references
color: function color(c, group) {
if (c == null) c = Scatter.defaults.color;
c = _this3.updateColor(c);
hasColor++;
return c;
},
borderColor: function borderColor(c, group) {
if (c == null) c = Scatter.defaults.borderColor;
c = _this3.updateColor(c);
hasColor++;
return c;
},
bounds: function bounds(_bounds, group, options) {
if (!('range' in options)) options.range = null;
return _bounds;
},
positions: function positions(_positions, group, options) {
var snap = group.snap;
var positionBuffer = group.positionBuffer,
positionFractBuffer = group.positionFractBuffer,
selectionBuffer = group.selectionBuffer;
// separate buffers for x/y coordinates
if (_positions.x || _positions.y) {
if (_positions.x.length) {
group.xAttr = {
buffer: regl.buffer(_positions.x),
offset: 0,
stride: 4,
count: _positions.x.length
};
} else {
group.xAttr = {
buffer: _positions.x.buffer,
offset: _positions.x.offset * 4 || 0,
stride: (_positions.x.stride || 1) * 4,
count: _positions.x.count
};
}
if (_positions.y.length) {
group.yAttr = {
buffer: regl.buffer(_positions.y),
offset: 0,
stride: 4,
count: _positions.y.length
};
} else {
group.yAttr = {
buffer: _positions.y.buffer,
offset: _positions.y.offset * 4 || 0,
stride: (_positions.y.stride || 1) * 4,
count: _positions.y.count
};
}
group.count = Math.max(group.xAttr.count, group.yAttr.count);
return _positions;
}
_positions = flatten(_positions, 'float64');
var count = group.count = Math.floor(_positions.length / 2);
var bounds = group.bounds = count ? getBounds(_positions, 2) : null;
// if range is not provided updated - recalc it
if (!options.range && !group.range) {
delete group.range;
options.range = bounds;
}
// reset marker
if (!options.marker && !group.marker) {
delete group.marker;
options.marker = null;
}
// build cluster tree if required
if (snap && (snap === true || count > snap)) {
group.tree = cluster(_positions, {
bounds: bounds
});
}
// existing tree instance
else if (snap && snap.length) {
group.tree = snap;
}
if (group.tree) {
var opts = {
primitive: 'points',
usage: 'static',
data: group.tree,
type: 'uint32'
};
if (group.elements) group.elements(opts);else group.elements = regl.elements(opts);
}
// update position buffers
var float_data = f32.float32(_positions);
positionBuffer({
data: float_data,
usage: 'dynamic'
});
var frac_data = f32.fract32(_positions, float_data);
positionFractBuffer({
data: frac_data,
usage: 'dynamic'
});
// expand selectionBuffer
selectionBuffer({
data: new Uint8Array(count),
type: 'uint8',
usage: 'stream'
});
return _positions;
}
}, {
// create marker ids corresponding to known marker textures
marker: function marker(markers, group, options) {
var activation = group.activation;
// reset marker elements
activation.forEach(function (buffer) {
return buffer && buffer.destroy && buffer.destroy();
});
activation.length = 0;
// single sdf marker
if (!markers || typeof markers[0] === 'number') {
var id = _this3.addMarker(markers);
activation[id] = true;
}
// per-point markers use mask buffers to enable markers in vert shader
else {
var markerMasks = [];
for (var _i = 0, l = Math.min(markers.length, group.count); _i < l; _i++) {
var _id = _this3.addMarker(markers[_i]);
if (!markerMasks[_id]) markerMasks[_id] = new Uint8Array(group.count);
// enable marker by default
markerMasks[_id][_i] = 1;
}
for (var _id2 = 0; _id2 < markerMasks.length; _id2++) {
if (!markerMasks[_id2]) continue;
var opts = {
data: markerMasks[_id2],
type: 'uint8',
usage: 'static'
};
if (!activation[_id2]) {
activation[_id2] = regl.buffer(opts);
} else {
activation[_id2](opts);
}
activation[_id2].data = markerMasks[_id2];
}
}
return markers;
},
range: function range(_range, group, options) {
var bounds = group.bounds;
// FIXME: why do we need this?
if (!bounds) return;
if (!_range) _range = bounds;
group.scale = [1 / (_range[2] - _range[0]), 1 / (_range[3] - _range[1])];
group.translate = [-_range[0], -_range[1]];
group.scaleFract = f32.fract(group.scale);
group.translateFract = f32.fract(group.translate);
return _range;
},
viewport: function viewport(vp) {
var rect = parseRect(vp || [gl.drawingBufferWidth, gl.drawingBufferHeight]);
// normalize viewport to the canvas coordinates
// rect.y = gl.drawingBufferHeight - rect.height - rect.y
return rect;
}
}]);
// update size buffer, if needed
if (hasSize) {
var _group = group,
count = _group.count,
size = _group.size,
borderSize = _group.borderSize,
sizeBuffer = _group.sizeBuffer;
var sizes = new Uint8Array(count * 2);
if (size.length || borderSize.length) {
for (var _i2 = 0; _i2 < count; _i2++) {
// we downscale size to allow for fractions
sizes[_i2 * 2] = Math.round((size[_i2] == null ? size : size[_i2]) * 255 / maxSize);
sizes[_i2 * 2 + 1] = Math.round((borderSize[_i2] == null ? borderSize : borderSize[_i2]) * 255 / maxSize);
}
}
sizeBuffer({
data: sizes,
usage: 'dynamic'
});
}
// update color buffer if needed
if (hasColor) {
var _group2 = group,
_count = _group2.count,
color = _group2.color,
borderColor = _group2.borderColor,
colorBuffer = _group2.colorBuffer;
var colors;
// if too many colors - put colors to buffer directly
if (_this3.tooManyColors) {
if (color.length || borderColor.length) {
colors = new Uint8Array(_count * 8);
for (var _i3 = 0; _i3 < _count; _i3++) {
var _colorId = color[_i3];
colors[_i3 * 8] = palette[_colorId * 4];
colors[_i3 * 8 + 1] = palette[_colorId * 4 + 1];
colors[_i3 * 8 + 2] = palette[_colorId * 4 + 2];
colors[_i3 * 8 + 3] = palette[_colorId * 4 + 3];
var borderColorId = borderColor[_i3];
colors[_i3 * 8 + 4] = palette[borderColorId * 4];
colors[_i3 * 8 + 5] = palette[borderColorId * 4 + 1];
colors[_i3 * 8 + 6] = palette[borderColorId * 4 + 2];
colors[_i3 * 8 + 7] = palette[borderColorId * 4 + 3];
}
}
}
// if limited amount of colors - keep palette color picking
// that saves significant memory
else {
if (color.length || borderColor.length) {
// we need slight data increase by 2 due to vec4 borderId in shader
colors = new Uint8Array(_count * 4 + 2);
for (var _i4 = 0; _i4 < _count; _i4++) {
// put color coords in palette texture
if (color[_i4] != null) {
colors[_i4 * 4] = color[_i4] % maxColors;
colors[_i4 * 4 + 1] = Math.floor(color[_i4] / maxColors);
}
if (borderColor[_i4] != null) {
colors[_i4 * 4 + 2] = borderColor[_i4] % maxColors;
colors[_i4 * 4 + 3] = Math.floor(borderColor[_i4] / maxColors);
}
}
}
}
colorBuffer({
data: colors || new Uint8Array(0),
type: 'uint8',
usage: 'dynamic'
});
}
return group;
});
};
// get (and create) marker texture id
Scatter.prototype.addMarker = function (sdf) {
var markerTextures = this.markerTextures,
regl = this.regl,
markerCache = this.markerCache;
var pos = sdf == null ? 0 : markerCache.indexOf(sdf);
if (pos >= 0) return pos;
// convert sdf to 0..255 range
var distArr;
if (sdf instanceof Uint8Array || sdf instanceof Uint8ClampedArray) {
distArr = sdf;
} else {
distArr = new Uint8Array(sdf.length);
for (var i = 0, l = sdf.length; i < l; i++) {
distArr[i] = sdf[i] * 255;
}
}
var radius = Math.floor(Math.sqrt(distArr.length));
pos = markerTextures.length;
markerCache.push(sdf);
markerTextures.push(regl.texture({
channels: 1,
data: distArr,
radius: radius,
mag: 'linear',
min: 'linear'
}));
return pos;
};
// register color to palette, return it's index or list of indexes
Scatter.prototype.updateColor = function (colors) {
var paletteIds = this.paletteIds,
palette = this.palette,
maxColors = this.maxColors;
if (!Array.isArray(colors)) {
colors = [colors];
}
var idx = [];
// if color groups - flatten them
if (typeof colors[0] === 'number') {
var grouped = [];
if (Array.isArray(colors)) {
for (var i = 0; i < colors.length; i += 4) {
grouped.push(colors.slice(i, i + 4));
}
} else {
for (var _i5 = 0; _i5 < colors.length; _i5 += 4) {
grouped.push(colors.subarray(_i5, _i5 + 4));
}
}
colors = grouped;
}
for (var _i6 = 0; _i6 < colors.length; _i6++) {
var color = colors[_i6];
color = rgba(color, 'uint8');
var id = colorId(color, false);
// if new color - save it
if (paletteIds[id] == null) {
var pos = palette.length;
paletteIds[id] = Math.floor(pos / 4);
palette[pos] = color[0];
palette[pos + 1] = color[1];
palette[pos + 2] = color[2];
palette[pos + 3] = color[3];
}
idx[_i6] = paletteIds[id];
}
// detect if too many colors in palette
if (!this.tooManyColors && palette.length > maxColors * 4) this.tooManyColors = true;
// limit max color
this.updatePalette(palette);
// keep static index for single-color property
return idx.length === 1 ? idx[0] : idx;
};
Scatter.prototype.updatePalette = function (palette) {
if (this.tooManyColors) return;
var maxColors = this.maxColors,
paletteTexture = this.paletteTexture;
var requiredHeight = Math.ceil(palette.length * .25 / maxColors);
// pad data
if (requiredHeight > 1) {
palette = palette.slice();
for (var i = palette.length * .25 % maxColors; i < requiredHeight * maxColors; i++) {
palette.push(0, 0, 0, 0);
}
}
// ensure height
if (paletteTexture.height < requiredHeight) {
paletteTexture.resize(maxColors, requiredHeight);
}
// update full data
paletteTexture.subimage({
width: Math.min(palette.length * .25, maxColors),
height: requiredHeight,
data: palette
}, 0, 0);
};
// remove unused stuff
Scatter.prototype.destroy = function () {
this.groups.forEach(function (group) {
group.sizeBuffer.destroy();
group.positionBuffer.destroy();
group.positionFractBuffer.destroy();
group.colorBuffer.destroy();
group.activation.forEach(function (b) {
return b && b.destroy && b.destroy();
});
group.selectionBuffer.destroy();
if (group.elements) group.elements.destroy();
});
this.groups.length = 0;
this.paletteTexture.destroy();
this.markerTextures.forEach(function (txt) {
return txt && txt.destroy && txt.destroy();
});
return this;
};
var extend$1 = require('object-assign');
var reglScatter2d = function reglScatter2d(regl, options) {
var scatter$1 = new scatter(regl, options);
var render = scatter$1.render.bind(scatter$1);
// expose API
extend$1(render, {
render: render,
update: scatter$1.update.bind(scatter$1),
draw: scatter$1.draw.bind(scatter$1),
destroy: scatter$1.destroy.bind(scatter$1),
regl: scatter$1.regl,
gl: scatter$1.gl,
canvas: scatter$1.gl.canvas,
groups: scatter$1.groups,
markers: scatter$1.markerCache,
palette: scatter$1.palette
});
return render;
};
module.exports = reglScatter2d;