Graph/scatterplot improvements (#893)

* many graph/scatterplot fixes: 722, 882, 758, 886

* fix e2e test breakage caused by graph transform work

* convert webgl point shaders to use flags rather than explicit point size

* remove unused packages from dependencies

* fix typo in regl prop name

* factor common code into util helper file

* legibility tweaks
This commit is contained in:
Bruce Martin
2019-08-22 20:56:05 -07:00
committed by GitHub
parent 28c4d28308
commit 0f36b49840
9 changed files with 708 additions and 536 deletions
+141 -51
View File
@@ -1,66 +1,156 @@
// jshint esversion: 6
const createCamera = require("orbit-camera");
const createScroll = require("scroll-speed");
const mp = require("mouse-position");
const mb = require("mouse-pressed");
const key = require("key-pressed");
import { vec2, mat3 } from "gl-matrix";
const panSpeed = 1.0; // changed from 0.4 to 1.0 per issue #722
const scaleSpeed = 0.5;
const scaleMax = 3;
const scaleMin = 1.03;
const scaleMax = 3.0;
const scaleMin = 0.5;
function attachCamera(canvas, opts) {
opts = opts || {};
opts.pan = opts.pan !== false;
opts.scale = opts.scale !== false;
opts.rotate = opts.rotate !== false;
// private
const scratch0 = new Float32Array(16);
const scratch1 = new Float32Array(16);
const scroll = createScroll(canvas, opts.scale);
const mbut = mb(canvas, opts.rotate);
const mpos = mp(canvas);
const camera = createCamera([0, 0, 1], [0, 0, -1], [0, 1, 0]);
class Camera {
constructor(canvas) {
this.prevEvent = {
clientX: 0,
clientY: 0,
type: 0
};
this.canvas = canvas;
this.viewMatrix = mat3.create();
this.viewMatrixInv = mat3.create();
}
camera.tick = tick;
view() {
return this.viewMatrix;
}
return camera;
invView() {
return this.viewMatrixInv;
}
function tick() {
const ctrl = key("<control>") || key("<alt>");
const alt = key("<shift>");
const { height, width } = canvas;
distance() {
return this.viewMatrix[0];
}
if (opts.rotate && mbut.left && ctrl && !alt) {
camera.rotate(
[mpos.x / width - 0.5, mpos.y / height - 0.5],
[mpos.prevX / width - 0.5, mpos.prevY / height - 0.5]
);
pan(dx, dy) {
mat3.translate(this.viewMatrix, this.viewMatrix, [dx, dy]);
mat3.invert(this.viewMatrixInv, this.viewMatrix);
}
zoomAt(d, x = 0, y = 0) {
/*
Set camera zoom at [x,y]
*/
const dClamped =
Math.max(Math.min(d * this.viewMatrix[0], scaleMax), scaleMin) /
this.viewMatrix[0];
if (dClamped === 1) return;
mat3.translate(this.viewMatrix, this.viewMatrix, [x, y]);
mat3.scale(this.viewMatrix, this.viewMatrix, [dClamped, dClamped]);
mat3.translate(this.viewMatrix, this.viewMatrix, [-x, -y]);
mat3.invert(this.viewMatrixInv, this.viewMatrix);
}
/*
Event handling
*/
flush(e) {
this.prevEvent.type = e.type;
this.prevEvent.clientX = e.clientX;
this.prevEvent.clientY = e.clientY;
}
localPosition(target, canvasX, canvasY, projectionInvTF) {
/*
Convert mouse position to local
*/
const { height, width } = target;
const targetRect = target.getBoundingClientRect();
canvasX -= targetRect.left;
canvasY -= targetRect.top;
const pos = vec2.fromValues(
2 * (canvasX / width) - 1,
-2 * (canvasY / height) + 1
);
if (projectionInvTF) {
vec2.transformMat3(pos, pos, projectionInvTF);
}
vec2.transformMat3(pos, pos, this.invView());
return pos;
}
if ((opts.pan && mbut.right) || (mbut.left && !ctrl && !alt)) {
camera.pan([
((panSpeed * (mpos[0] - mpos.prev[0])) / width) * camera.distance,
((panSpeed * (mpos[1] - mpos.prev[1])) / height) * camera.distance
]);
mousePan(e, projectionTF) {
const projectionInvTF = mat3.invert(scratch0, projectionTF);
const pos = this.localPosition(
this.canvas,
e.clientX,
e.clientY,
projectionInvTF
);
const prev = this.localPosition(
this.canvas,
this.prevEvent.clientX,
this.prevEvent.clientY,
projectionInvTF
);
const delta = vec2.sub(scratch1, pos, prev);
this.pan(delta[0], delta[1]);
return true;
}
wheelZoom(e, projectionTF) {
const { height } = this.canvas;
const { deltaY, deltaMode, clientX, clientY } = e;
const scale = scaleSpeed * (deltaMode === 1 ? 12 : 1) * (deltaY || 0);
const projectionInvTF = mat3.invert(scratch0, projectionTF);
const pos = this.localPosition(
this.canvas,
clientX,
clientY,
projectionInvTF
);
this.zoomAt(1 / Math.exp(scale / height), pos[0], pos[1]);
return true;
}
handleEvent(e, projectionTF) {
/*
process the event, and return true if camera view changed
*/
let viewChanged = false;
switch (e.type) {
case "mousemove": {
/* eslint-disable no-bitwise */
if (e.buttons & 0x1) {
viewChanged = this.mousePan(e, projectionTF);
}
/* eslint-enable no-bitwise */
this.flush(e);
break;
}
case "wheel": {
viewChanged = this.wheelZoom(e, projectionTF);
this.flush(e);
break;
}
default:
// noop
break;
}
if (opts.scale && scroll[1]) {
camera.distance *= Math.exp((scroll[1] * scaleSpeed) / height);
}
if (opts.scale && (mbut.middle || (mbut.left && !ctrl && alt))) {
const d = mpos.y - mpos.prevY;
if (!d) return;
camera.distance *= Math.exp(d / height);
}
if (camera.distance > scaleMax) camera.distance = scaleMax;
if (camera.distance < scaleMin) camera.distance = scaleMin;
scroll.flush();
mpos.flush();
return viewChanged;
}
}
function attachCamera(canvas) {
return new Camera(canvas);
}
export default attachCamera;