webglimpse
Version:
Webglimpse is a data visualization library for the web.
658 lines • 332 kB
JavaScript
/*
* Copyright (c) 2014, Metron, Inc.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import { Color, darker } from '../color';
import { Pane, isLeftMouseDown } from '../core';
import { TimelineLaneArray, effectiveEdges_PMILLIS } from './timeline_lanes';
import { concatLines, GL, ensureCapacityFloat32, hasval, isNumber } from '../util/util';
import { indexNearest } from '../util/sorted_arrays';
import { Program, Attribute, Uniform1f } from '../shader';
import { newDynamicBuffer } from '../buffer';
import { putQuadXys, putRgbas, putQuadRgbas, putUpperRightTriangleXys, putLowerRightTriangleXys, putLowerLeftTriangleXys, putUpperLeftTriangleXys, varyingColor_FRAGSHADER } from '../misc';
import { TextureRenderer, Texture2D } from '../texture';
import { newTextTextureCache2 } from '../text';
export function newEventsRowPaneFactory(eventsRowOpts) {
// Pane Factory
return function (drawable, timeAxis, dataAxis, model, row, ui, options) {
const rowTopPadding = (hasval(eventsRowOpts) && hasval(eventsRowOpts.rowTopPadding) ? eventsRowOpts.rowTopPadding : 6);
const rowBottomPadding = (hasval(eventsRowOpts) && hasval(eventsRowOpts.rowBottomPadding) ? eventsRowOpts.rowBottomPadding : 6);
const laneHeight = (hasval(eventsRowOpts) && hasval(eventsRowOpts.laneHeight) ? eventsRowOpts.laneHeight : 33);
const painterFactories = (hasval(eventsRowOpts) && hasval(eventsRowOpts.painterFactories) ? eventsRowOpts.painterFactories : []);
const allowMultipleLanes = (hasval(eventsRowOpts) && hasval(eventsRowOpts.allowMultipleLanes) ? eventsRowOpts.allowMultipleLanes : true);
const timelineFont = options.timelineFont;
const timelineFgColor = options.timelineFgColor;
const draggableEdgeWidth = options.draggableEdgeWidth;
const snapToDistance = options.snapToDistance;
const rowUi = ui.rowUi(row.rowGuid);
const input = ui.input;
const selection = ui.selection;
const lanes = new TimelineLaneArray(model, row, ui, allowMultipleLanes);
const timeAtCoords_PMILLIS = function (viewport, i) {
return timeAxis.tAtFrac_PMILLIS(viewport.xFrac(i));
};
const timeAtPointer_PMILLIS = function (ev) {
return timeAtCoords_PMILLIS(ev.paneViewport, ev.i);
};
const eventAtCoords = function (viewport, i, j) {
const laneNum = Math.floor((viewport.jEnd - j - rowTopPadding) / laneHeight);
const time_PMILLIS = timeAtCoords_PMILLIS(viewport, i);
return lanes.eventAt(laneNum, time_PMILLIS);
};
const eventAtPointer = function (ev) {
return eventAtCoords(ev.paneViewport, ev.i, ev.j);
};
const isInsideAnEvent = function (viewport, i, j) {
return hasval(eventAtCoords(viewport, i, j));
};
// Create pane
//
const layout = {
updatePrefSize: function (parentPrefSize) {
parentPrefSize.h = rowTopPadding + rowBottomPadding + Math.max(1, lanes.length) * laneHeight;
parentPrefSize.w = null;
}
};
const rowContentPane = new Pane(layout, true, isInsideAnEvent);
rowUi.addPane('content', rowContentPane);
const painterOptions = { timelineFont: timelineFont, timelineFgColor: timelineFgColor, rowTopPadding: rowTopPadding, rowBottomPadding: rowBottomPadding, laneHeight: laneHeight };
for (let n = 0; n < painterFactories.length; n++) {
const createPainter = painterFactories[n];
rowContentPane.addPainter(createPainter(drawable, timeAxis, lanes, ui, painterOptions));
}
const redraw = function () {
drawable.redraw();
};
row.eventGuids.valueAdded.on(redraw);
row.eventGuids.valueMoved.on(redraw);
row.eventGuids.valueRemoved.on(redraw);
const watchEventAttrs = function (eventGuid) {
model.event(eventGuid).attrsChanged.on(redraw);
};
row.eventGuids.forEach(watchEventAttrs);
row.eventGuids.valueAdded.on(watchEventAttrs);
const removeRedraw = function (eventGuid) {
model.event(eventGuid).attrsChanged.off(redraw);
};
row.eventGuids.valueRemoved.on(removeRedraw);
// Used by both sets of listeners to know whether an event-drag is in progress
let eventDragMode = null;
// Hook up input notifications
//
let recentMouseMove = null;
rowContentPane.mouseMove.on(function (ev) {
input.mouseMove.fire(ev);
if (!eventDragMode) {
input.timeHover.fire(timeAtPointer_PMILLIS(ev), ev);
input.rowHover.fire(row, ev);
input.eventHover.fire(eventAtPointer(ev), ev);
}
recentMouseMove = ev;
});
rowContentPane.mouseExit.on(function (ev) {
input.mouseExit.fire(ev);
if (!eventDragMode) {
input.timeHover.fire(null, ev);
input.rowHover.fire(null, ev);
input.eventHover.fire(null, ev);
}
recentMouseMove = null;
});
const uiMillisPerPxChanged = function () {
if (!eventDragMode && recentMouseMove != null) {
const ev = recentMouseMove;
input.timeHover.fire(timeAtPointer_PMILLIS(ev), ev);
input.eventHover.fire(eventAtPointer(ev), ev);
}
};
ui.millisPerPx.changed.on(uiMillisPerPxChanged);
rowContentPane.mouseUp.on(function (ev) {
input.mouseUp.fire(ev);
});
rowContentPane.mouseDown.on(function (ev) {
input.mouseDown.fire(ev);
});
rowContentPane.mouseWheel.on(options.mouseWheelListener);
rowContentPane.contextMenu.on(function (ev) {
input.contextMenu.fire(ev);
});
// Begin event-drag
//
let eventDragEvents = [];
let eventDragOffsets_MILLIS = {};
let eventDragSnapTimes_PMILLIS = [];
// Event-edges are draggable for events at least this wide
const minEventWidthForEdgeDraggability = 3 * draggableEdgeWidth;
// When dragging an event-edge, the event cannot be made narrower than this
//
// Needs to be greater than minEventWidthForEdgeDraggability -- by enough to
// cover floating-point precision loss -- so a user can't accidentally make
// an event so narrow that it can't easily be widened again.
//
const minEventWidthWhenDraggingEdge = minEventWidthForEdgeDraggability + 1;
function allUserEditable(events) {
for (let n = 0; n < events.length; n++) {
if (!events[n].userEditable) {
return false;
}
}
return true;
}
function chooseEventDragMode(timelineUi, mouseTime_PMILLIS, timelineEventDragEvents) {
if (timelineEventDragEvents.length === 0) {
// If no events are selected, then we don't have any to drag
return null;
}
else if (!allUserEditable(timelineEventDragEvents)) {
// If any selected event is not user-editable, don't allow editing
return 'undraggable';
}
else if (timelineEventDragEvents.length > 1) {
// If more than one event is selected, don't allow edge dragging
return 'center';
}
else if (timelineEventDragEvents.length === 1) {
const event = timelineEventDragEvents[0];
const pxPerMilli = 1 / timelineUi.millisPerPx.value;
const eventWidth = (event.end_PMILLIS - event.start_PMILLIS) * pxPerMilli;
if (eventWidth < minEventWidthForEdgeDraggability) {
// If event isn't very wide, don't try to allow edge dragging
return 'center';
}
else {
const mouseOffset = (mouseTime_PMILLIS - event.start_PMILLIS) * pxPerMilli;
if (mouseOffset < draggableEdgeWidth) {
// If mouse is near the left edge, drag the event's start-time
return 'start';
}
else if (mouseOffset < eventWidth - draggableEdgeWidth) {
// If mouse is in the center, drag the whole event
return 'center';
}
else {
// If mouse is near the right edge, drag the event's end-time
return 'end';
}
}
}
else {
// Should never get here, because we have clauses above for length === 0, length === 1, and length > 1
return null;
}
}
const updateCursor = function () {
if (!eventDragMode) {
const mouseCursors = { 'center': 'default', 'start': 'w-resize', 'end': 'e-resize', 'undraggable': 'default' };
const hoveredTime_PMILLIS = selection.hoveredTime_PMILLIS.value;
// if a multi-selection has been made, update the cursor based on all the events in the multi-selection
if (selection.selectedEvents.length > 1) {
rowContentPane.mouseCursor = mouseCursors[chooseEventDragMode(ui, hoveredTime_PMILLIS, selection.selectedEvents.toArray())];
}
else {
const hoveredEvent = selection.hoveredEvent.value;
const hoveredEvents = (hasval(hoveredEvent) ? [hoveredEvent] : []);
rowContentPane.mouseCursor = mouseCursors[chooseEventDragMode(ui, hoveredTime_PMILLIS, hoveredEvents)];
}
}
};
ui.millisPerPx.changed.on(updateCursor);
selection.hoveredTime_PMILLIS.changed.on(updateCursor);
selection.hoveredEvent.changed.on(updateCursor);
rowContentPane.mouseDown.on(function (ev) {
if (isLeftMouseDown(ev.mouseEvent)) {
const eventDragEventsSet = selection.selectedEvents;
eventDragEvents = eventDragEventsSet.toArray();
eventDragMode = chooseEventDragMode(ui, timeAtPointer_PMILLIS(ev), eventDragEvents);
eventDragSnapTimes_PMILLIS = new Array();
const numSnapTimes = 0;
const allEventGuids = row.eventGuids;
for (let n = 0; n < allEventGuids.length; n++) {
const eventGuid = allEventGuids.valueAt(n);
if (!eventDragEventsSet.hasId(eventGuid)) {
const event = model.event(eventGuid);
eventDragSnapTimes_PMILLIS.push(event.start_PMILLIS);
eventDragSnapTimes_PMILLIS.push(event.end_PMILLIS);
}
}
eventDragSnapTimes_PMILLIS.sort();
}
});
function findSnapShift_MILLIS(t_PMILLIS, maxShift_MILLIS) {
const i = indexNearest(eventDragSnapTimes_PMILLIS, t_PMILLIS);
if (i >= 0) {
const shift_MILLIS = eventDragSnapTimes_PMILLIS[i] - t_PMILLIS;
if (Math.abs(shift_MILLIS) <= maxShift_MILLIS) {
return shift_MILLIS;
}
}
return null;
}
// Compute (and remember) the pointer time, for use by the event-drag listeners below
//
let eventDragPointer_PMILLIS = null;
const updateEventDragPointer = function (ev) {
if (isLeftMouseDown(ev.mouseEvent) && eventDragMode) {
eventDragPointer_PMILLIS = timeAtPointer_PMILLIS(ev);
}
};
rowContentPane.mouseDown.on(updateEventDragPointer);
rowContentPane.mouseMove.on(updateEventDragPointer);
// Dragging event-center
//
const grabEventCenter = function () {
if (eventDragMode === 'center') {
for (let n = 0; n < eventDragEvents.length; n++) {
const event = eventDragEvents[n];
eventDragOffsets_MILLIS[event.eventGuid] = eventDragPointer_PMILLIS - event.start_PMILLIS;
}
// If this is a simple click-and-release, leave the mouse-cursor alone --
// but once we can tell that it's actually a drag, change to a drag cursor
//
const beginDrag = function () {
rowContentPane.mouseCursor = 'move';
};
rowContentPane.mouseMove.on(beginDrag);
const pendingBeginDrag = setTimeout(beginDrag, 300);
const endDrag = function () {
clearTimeout(pendingBeginDrag);
rowContentPane.mouseMove.off(beginDrag);
rowContentPane.mouseUp.off(endDrag);
};
rowContentPane.mouseUp.on(endDrag);
}
};
rowContentPane.mouseDown.on(grabEventCenter);
const dragEventCenter = function () {
if (eventDragMode === 'center') {
const maxSnapShift_MILLIS = snapToDistance * (timeAxis.tSize_MILLIS / rowContentPane.viewport.w);
let snapShift_MILLIS = null;
for (let n = 0; n < eventDragEvents.length; n++) {
const event = eventDragEvents[n];
const newStart_PMILLIS = (eventDragPointer_PMILLIS - eventDragOffsets_MILLIS[event.eventGuid]);
const newEnd_PMILLIS = event.end_PMILLIS + (newStart_PMILLIS - event.start_PMILLIS);
const eventStartSnapShift_MILLIS = findSnapShift_MILLIS(newStart_PMILLIS, maxSnapShift_MILLIS);
if (hasval(eventStartSnapShift_MILLIS)) {
if (!hasval(snapShift_MILLIS) || Math.abs(eventStartSnapShift_MILLIS) < Math.abs(snapShift_MILLIS)) {
snapShift_MILLIS = eventStartSnapShift_MILLIS;
}
}
const eventEndSnapShift_MILLIS = findSnapShift_MILLIS(newEnd_PMILLIS, maxSnapShift_MILLIS);
if (hasval(eventEndSnapShift_MILLIS)) {
if (!hasval(snapShift_MILLIS) || Math.abs(eventEndSnapShift_MILLIS) < Math.abs(snapShift_MILLIS)) {
snapShift_MILLIS = eventEndSnapShift_MILLIS;
}
}
}
if (!hasval(snapShift_MILLIS)) {
snapShift_MILLIS = 0;
}
for (let n = 0; n < eventDragEvents.length; n++) {
const event = eventDragEvents[n];
const newStart_PMILLIS = eventDragPointer_PMILLIS - eventDragOffsets_MILLIS[event.eventGuid] + snapShift_MILLIS;
const newEnd_PMILLIS = event.end_PMILLIS + (newStart_PMILLIS - event.start_PMILLIS);
event.setInterval(newStart_PMILLIS, newEnd_PMILLIS);
}
}
};
rowContentPane.mouseMove.on(dragEventCenter);
// Dragging event-start
//
const grabEventStart = function () {
if (eventDragMode === 'start') {
for (let n = 0; n < eventDragEvents.length; n++) {
const event = eventDragEvents[n];
eventDragOffsets_MILLIS[event.eventGuid] = eventDragPointer_PMILLIS - event.start_PMILLIS;
}
}
};
rowContentPane.mouseDown.on(grabEventStart);
const dragEventStart = function () {
if (eventDragMode === 'start') {
const wMin_MILLIS = minEventWidthWhenDraggingEdge * timeAxis.vSize / rowContentPane.viewport.w;
const maxSnapShift_MILLIS = snapToDistance * (timeAxis.tSize_MILLIS / rowContentPane.viewport.w);
let snapShift_MILLIS = null;
for (let n = 0; n < eventDragEvents.length; n++) {
const event = eventDragEvents[n];
const newStart_PMILLIS = eventDragPointer_PMILLIS - eventDragOffsets_MILLIS[event.eventGuid];
const eventSnapShift_MILLIS = findSnapShift_MILLIS(newStart_PMILLIS, maxSnapShift_MILLIS);
if (hasval(eventSnapShift_MILLIS)) {
if (!hasval(snapShift_MILLIS) || Math.abs(eventSnapShift_MILLIS) < Math.abs(snapShift_MILLIS)) {
snapShift_MILLIS = eventSnapShift_MILLIS;
}
}
}
if (!hasval(snapShift_MILLIS)) {
snapShift_MILLIS = 0;
}
for (let n = 0; n < eventDragEvents.length; n++) {
const event = eventDragEvents[n];
const newStart_PMILLIS = eventDragPointer_PMILLIS - eventDragOffsets_MILLIS[event.eventGuid] + snapShift_MILLIS;
event.start_PMILLIS = Math.trunc(Math.min(event.end_PMILLIS - wMin_MILLIS, newStart_PMILLIS));
}
}
};
rowContentPane.mouseMove.on(dragEventStart);
timeAxis.limitsChanged.on(dragEventStart);
// Dragging event-end
//
const grabEventEnd = function () {
if (eventDragMode === 'end') {
for (let n = 0; n < eventDragEvents.length; n++) {
const event = eventDragEvents[n];
eventDragOffsets_MILLIS[event.eventGuid] = eventDragPointer_PMILLIS - event.end_PMILLIS;
}
}
};
rowContentPane.mouseDown.on(grabEventEnd);
const dragEventEnd = function () {
if (eventDragMode === 'end') {
const wMin_MILLIS = minEventWidthWhenDraggingEdge * timeAxis.vSize / rowContentPane.viewport.w;
const maxSnapShift_MILLIS = snapToDistance * (timeAxis.tSize_MILLIS / rowContentPane.viewport.w);
let snapShift_MILLIS = null;
for (let n = 0; n < eventDragEvents.length; n++) {
const event = eventDragEvents[n];
const newEnd_PMILLIS = eventDragPointer_PMILLIS - eventDragOffsets_MILLIS[event.eventGuid];
const eventSnapShift_MILLIS = findSnapShift_MILLIS(newEnd_PMILLIS, maxSnapShift_MILLIS);
if (hasval(eventSnapShift_MILLIS)) {
if (!hasval(snapShift_MILLIS) || Math.abs(eventSnapShift_MILLIS) < Math.abs(snapShift_MILLIS)) {
snapShift_MILLIS = eventSnapShift_MILLIS;
}
}
}
if (!hasval(snapShift_MILLIS)) {
snapShift_MILLIS = 0;
}
for (let n = 0; n < eventDragEvents.length; n++) {
const event = eventDragEvents[n];
const newEnd_PMILLIS = eventDragPointer_PMILLIS - eventDragOffsets_MILLIS[event.eventGuid] + snapShift_MILLIS;
event.end_PMILLIS = Math.trunc(Math.max(event.start_PMILLIS + wMin_MILLIS, newEnd_PMILLIS));
}
}
};
rowContentPane.mouseMove.on(dragEventEnd);
timeAxis.limitsChanged.on(dragEventEnd);
// Finish event-drag
//
rowContentPane.mouseUp.on(function (ev) {
eventDragEvents = [];
eventDragOffsets_MILLIS = {};
eventDragSnapTimes_PMILLIS = [];
eventDragPointer_PMILLIS = null;
eventDragMode = null;
});
rowContentPane.dispose.on(function () {
lanes.dispose();
timeAxis.limitsChanged.off(dragEventEnd);
timeAxis.limitsChanged.off(dragEventStart);
ui.millisPerPx.changed.off(uiMillisPerPxChanged);
ui.millisPerPx.changed.off(updateCursor);
selection.hoveredTime_PMILLIS.changed.off(updateCursor);
selection.hoveredEvent.changed.off(updateCursor);
row.eventGuids.valueAdded.off(redraw);
row.eventGuids.valueMoved.off(redraw);
row.eventGuids.valueRemoved.off(redraw);
row.eventGuids.valueRemoved.off(removeRedraw);
row.eventGuids.valueAdded.off(watchEventAttrs);
row.eventGuids.forEach(function (eventGuid) {
model.event(eventGuid).attrsChanged.off(redraw);
});
});
return rowContentPane;
};
}
function eventLimitsPainterHelper(limitsOpts, drawable, timeAxis, lanes, ui, options) {
const rowTopPadding = options.rowTopPadding;
const rowBottomPadding = options.rowBottomPadding;
const laneHeight = options.laneHeight;
const lineColor = (hasval(limitsOpts) && hasval(limitsOpts.lineColor) ? limitsOpts.lineColor : new Color(1, 0, 0, 1));
const lineThickness = (hasval(limitsOpts) && hasval(limitsOpts.lineThickness) ? limitsOpts.lineThickness : 2.5);
const xyFrac_vColor_VERTSHADER = concatLines(' ', ' attribute vec2 a_XyFrac; ', ' attribute vec4 a_Color; ', ' ', ' varying vec4 v_Color; ', ' ', ' void main( ) { ', ' gl_Position = vec4( ( -1.0 + 2.0*a_XyFrac ), 0.0, 1.0 ); ', ' v_Color = a_Color; ', ' } ', ' ');
const program = new Program(xyFrac_vColor_VERTSHADER, varyingColor_FRAGSHADER);
const a_XyFrac = new Attribute(program, 'a_XyFrac');
const a_Color = new Attribute(program, 'a_Color');
let xys = new Float32Array(0);
const xysBuffer = newDynamicBuffer();
let rgbas = new Float32Array(0);
const rgbasBuffer = newDynamicBuffer();
return {
paint(indexXys, indexRgbas, gl, viewport) {
if (indexXys > 0) {
gl.blendFuncSeparate(GL.SRC_ALPHA, GL.ONE_MINUS_SRC_ALPHA, GL.ONE, GL.ONE_MINUS_SRC_ALPHA);
gl.enable(GL.BLEND);
program.use(gl);
xysBuffer.setData(xys.subarray(0, indexXys));
a_XyFrac.setDataAndEnable(gl, xysBuffer, 2, GL.FLOAT);
rgbasBuffer.setData(rgbas.subarray(0, indexRgbas));
a_Color.setDataAndEnable(gl, rgbasBuffer, 4, GL.FLOAT);
gl.drawArrays(GL.TRIANGLES, 0, Math.floor(indexXys / 2));
a_Color.disable(gl);
a_XyFrac.disable(gl);
program.endUse(gl);
}
},
ensureCapacity: function (eventCount) {
const numVertices = (6 * 3 /* triangles */ * eventCount);
xys = ensureCapacityFloat32(xys, 2 * numVertices);
rgbas = ensureCapacityFloat32(rgbas, 4 * numVertices);
},
fillEvent: function (laneIndex, eventIndex, indexXys, indexRgbas, viewport) {
const lane = lanes.lane(laneIndex);
const event = lane.event(eventIndex);
const wLine = lineThickness / viewport.w;
const hLine = lineThickness / viewport.h;
const jTop = rowTopPadding + (laneIndex) * laneHeight;
const yTop = (viewport.h - jTop) / viewport.h;
const jBottom = rowTopPadding + (laneIndex + 1) * laneHeight;
const yBottom = (viewport.h - jBottom) / viewport.h;
const yMid = (yTop + yBottom) / 2;
const xLeft = hasval(event.startLimit_PMILLIS) ? timeAxis.tFrac(event.startLimit_PMILLIS) : 0;
const xRight = hasval(event.endLimit_PMILLIS) ? timeAxis.tFrac(event.endLimit_PMILLIS) : 1;
indexXys = putQuadXys(xys, indexXys, xLeft, xRight, yMid - hLine / 2, yMid + hLine / 2);
indexXys = putQuadXys(xys, indexXys, xLeft, xLeft - wLine, yTop, yBottom);
indexXys = putQuadXys(xys, indexXys, xRight, xRight + wLine, yTop, yBottom);
indexRgbas = putRgbas(rgbas, indexRgbas, lineColor, 18);
return { indexXys: indexXys, indexRgbas: indexRgbas };
}
};
}
export function newEventLimitsPainterFactory(limitOpts) {
// Painter Factory
return function (drawable, timeAxis, lanes, ui, options) {
const helper = eventLimitsPainterHelper(limitOpts, drawable, timeAxis, lanes, ui, options);
// Painter
return function (gl, viewport) {
const selectedEvents = ui.selection.selectedEvents;
// XXX Instead of estimating the number of events we will need to draw ahead of time
// XXX (difficult because selected events may be present in multiple lanes, so
// XXX selectedEvents.length might not be sufficient) just make enough space for all events.
// XXX Potentially quite inefficient with lots of events (and few selected events).
helper.ensureCapacity(lanes.numEvents);
let indexXys = 0;
let indexRgbas = 0;
for (let l = 0; l < lanes.length; l++) {
const lane = lanes.lane(l);
for (let e = 0; e < lane.length; e++) {
const event = lane.event(e);
// check whether the event is selected and has limits defined
if (selectedEvents.hasId(event.eventGuid) && (hasval(event.startLimit_PMILLIS) || hasval(event.endLimit_PMILLIS))) {
const indexes = helper.fillEvent(l, e, indexXys, indexRgbas, viewport);
indexXys = indexes.indexXys;
indexRgbas = indexes.indexRgbas;
}
}
}
helper.paint(indexXys, indexRgbas, gl, viewport);
};
};
}
export var JointType;
(function (JointType) {
JointType[JointType["BEVEL"] = 0] = "BEVEL";
JointType[JointType["MITER"] = 1] = "MITER";
})(JointType || (JointType = {}));
export var FillPattern;
(function (FillPattern) {
FillPattern[FillPattern["solid"] = 0] = "solid";
FillPattern[FillPattern["stripe"] = 1] = "stripe";
FillPattern[FillPattern["gradient"] = 2] = "gradient";
})(FillPattern || (FillPattern = {}));
function eventStripedBarPainterHelper(barOpts, drawable, timeAxis, lanes, ui, options) {
const rowTopPadding = options.rowTopPadding;
const rowBottomPadding = options.rowBottomPadding;
const laneHeight = options.laneHeight;
const topMargin = (hasval(barOpts) && hasval(barOpts.topMargin) ? barOpts.topMargin : 1.2);
const bottomMargin = (hasval(barOpts) && hasval(barOpts.bottomMargin) ? barOpts.bottomMargin : 1.2);
const borderThickness = (hasval(barOpts) && hasval(barOpts.borderThickness) ? barOpts.borderThickness : 2);
const cornerType = (hasval(barOpts) && hasval(barOpts.cornerType) ? barOpts.cornerType : JointType.BEVEL);
const defaultColor = (hasval(barOpts) && hasval(barOpts.defaultColor) ? barOpts.defaultColor : options.timelineFgColor.withAlphaTimes(0.4));
const defaultColorSecondary = new Color(1, 1, 1, 1);
const minimumVisibleWidth = (hasval(barOpts) && hasval(barOpts.minimumVisibleWidth) ? barOpts.minimumVisibleWidth : 0);
const stripeWidth = (hasval(barOpts) && hasval(barOpts.stripeWidth) ? barOpts.stripeWidth : 5);
const stripeSecondaryWidth = (hasval(barOpts) && hasval(barOpts.stripeSecondaryWidth) ? barOpts.stripeSecondaryWidth : 5);
const stripeSlant = (hasval(barOpts) && hasval(barOpts.stripeSlant) ? barOpts.stripeSlant : 1);
const featherWidth = (hasval(barOpts) && hasval(barOpts.featherWidth) ? barOpts.featherWidth : 2);
const selection = ui.selection;
const xyFrac_vColor_VERTSHADER = concatLines(' ', ' attribute vec2 a_XyFrac; ', ' attribute vec4 a_Color; ', ' attribute vec4 a_ColorSecondary; ', ' attribute vec2 a_relativeXy; ', ' attribute float a_fillPattern; ', ' ', ' varying vec4 v_Color; ', ' varying vec4 v_ColorSecondary; ', ' varying vec2 v_relativeXy; ', ' varying float v_fillPattern; ', ' ', ' void main( ) { ', ' gl_Position = vec4( ( -1.0 + 2.0*a_XyFrac ), 0.0, 1.0 ); ', ' v_Color = a_Color; ', ' v_ColorSecondary = a_ColorSecondary; ', ' v_relativeXy = a_relativeXy; ', ' v_fillPattern = a_fillPattern; ', ' } ', ' ');
const fillPattern_FRAGSHADER = concatLines(' #define PI 3.1415926535897932384626433832795 ', ' ', ' precision lowp float; ', ' // the width in pixels of the first color stripe ', ' uniform float u_stripeWidth; ', ' // the width in pixels of the second color stripe ', ' uniform float u_stripeSecondaryWidth; ', ' // the slant of the stipes: 0 = horizontal, 1 = 45 degrees ', ' uniform float u_slant; ', ' // width in pixels of the antialiasing of the slant ', ' uniform float u_featherWidth; ', ' ', ' varying vec4 v_Color; ', ' varying vec4 v_ColorSecondary; ', ' varying vec2 v_relativeXy; ', ' varying float v_fillPattern; ', ' ', ' void pattern_stripe( ) { ', ' float stripeWidthTotal = u_stripeWidth + u_stripeSecondaryWidth; ', ' ', ' // calculate the value indicating where we are in the stripe pattern ', ' float stripeCoord = mod( v_relativeXy.x + u_slant * v_relativeXy.y , stripeWidthTotal ); ', ' ', ' // we are in the feather region beween the two stripes ', ' if ( stripeCoord < u_featherWidth ) { ', ' float diff = stripeCoord / u_featherWidth; ', ' gl_FragColor = vec4 ( v_Color.xyz * diff + (1.0-diff) * v_ColorSecondary.xyz, 1.0 ); ', ' } ', ' // we are in the color 1 stripe ', ' else if ( stripeCoord < u_stripeWidth ) { ', ' gl_FragColor = v_Color; ', ' } ', ' // we are the feather region between the two stripes ', ' else if ( stripeCoord < u_stripeWidth + u_featherWidth ) { ', ' float diff = ( stripeCoord - u_stripeWidth ) / u_featherWidth; ', ' gl_FragColor = vec4 ( v_Color.xyz * (1.0-diff) + diff * v_ColorSecondary.xyz, 1.0 ); ', ' } ', ' // we are in the color 2 stripe ', ' else { ', ' gl_FragColor = v_ColorSecondary; ', ' } ', ' } ', ' ', ' void pattern_gradient( ) { ', ' float stripeWidthTotal = u_stripeWidth + u_stripeSecondaryWidth; ', ' ', ' // calculate the value indicating where we are in the stripe pattern ', ' float stripeCoord = mod( v_relativeXy.x + u_slant * v_relativeXy.y , stripeWidthTotal ); ', ' ', ' float weightedCoord; ', ' if ( stripeCoord < u_stripeWidth ) { ', ' float slope = PI / u_stripeWidth; ', ' weightedCoord = slope * stripeCoord; ', ' } ', ' else { ', ' float slope = PI / u_stripeSecondaryWidth; ', ' weightedCoord = PI + slope * ( stripeCoord - u_stripeWidth ); ', ' } ', ' ', ' // sin wave domain: [0, stripeWidthTotal ] range: [0, 1] ', ' float frac = sin( weightedCoord ) * 2.0 - 1.0; ', ' ', ' // mix primary and secondary colors based on gradient fraction ', ' gl_FragColor = mix( v_Color, v_ColorSecondary, frac ); ', ' } ', ' ', ' void pattern_solid( ) { ', ' gl_FragColor = v_Color; ', ' } ', ' ', ' void main( ) { ', ' if ( v_fillPattern == 1.0 ) { ', ' pattern_stripe( ); ', ' } ', ' else if ( v_fillPattern == 2.0 ) { ', ' pattern_gradient( ); ', ' } ', ' else { ', ' pattern_solid( ); ', ' } ', ' } ', ' ', ' ', ' ');
const program = new Program(xyFrac_vColor_VERTSHADER, fillPattern_FRAGSHADER);
const a_XyFrac = new Attribute(program, 'a_XyFrac');
const a_Color = new Attribute(program, 'a_Color');
const a_ColorSecondary = new Attribute(program, 'a_ColorSecondary');
const a_relativeXy = new Attribute(program, 'a_relativeXy');
const a_fillPattern = new Attribute(program, 'a_fillPattern');
const u_stripeWidth = new Uniform1f(program, 'u_stripeWidth');
const u_stripeSecondaryWidth = new Uniform1f(program, 'u_stripeSecondaryWidth');
const u_slant = new Uniform1f(program, 'u_slant');
const u_featherWidth = new Uniform1f(program, 'u_featherWidth');
let xys = new Float32Array(0);
const xysBuffer = newDynamicBuffer();
let rgbas = new Float32Array(0);
const rgbasBuffer = newDynamicBuffer();
let rgbasSecondary = new Float32Array(0);
const rgbasSecondaryBuffer = newDynamicBuffer();
let relativeXys = new Float32Array(0);
const relativeXysBuffer = newDynamicBuffer();
let fillPattern = new Float32Array(0);
const fillPatternBuffer = newDynamicBuffer();
return {
paint(indexXys, indexRgbas, gl, viewport, indexRelativeXys, indexFillPattern) {
if (indexXys === 0 || indexRgbas === 0) {
return;
}
gl.blendFuncSeparate(GL.SRC_ALPHA, GL.ONE_MINUS_SRC_ALPHA, GL.ONE, GL.ONE_MINUS_SRC_ALPHA);
gl.enable(GL.BLEND);
program.use(gl);
u_slant.setData(gl, stripeSlant);
u_stripeWidth.setData(gl, stripeWidth);
u_stripeSecondaryWidth.setData(gl, stripeSecondaryWidth);
u_featherWidth.setData(gl, featherWidth);
xysBuffer.setData(xys.subarray(0, indexXys));
a_XyFrac.setDataAndEnable(gl, xysBuffer, 2, GL.FLOAT);
rgbasBuffer.setData(rgbas.subarray(0, indexRgbas));
a_Color.setDataAndEnable(gl, rgbasBuffer, 4, GL.FLOAT);
rgbasSecondaryBuffer.setData(rgbasSecondary.subarray(0, indexRgbas));
a_ColorSecondary.setDataAndEnable(gl, rgbasSecondaryBuffer, 4, GL.FLOAT);
relativeXysBuffer.setData(relativeXys.subarray(0, indexRelativeXys));
a_relativeXy.setDataAndEnable(gl, relativeXysBuffer, 2, GL.FLOAT);
fillPatternBuffer.setData(fillPattern.subarray(0, indexFillPattern));
a_fillPattern.setDataAndEnable(gl, fillPatternBuffer, 1, GL.FLOAT);
gl.drawArrays(GL.TRIANGLES, 0, Math.floor(indexXys / 2));
a_Color.disable(gl);
a_XyFrac.disable(gl);
a_ColorSecondary.disable(gl);
a_fillPattern.disable(gl);
a_relativeXy.disable(gl);
program.endUse(gl);
},
ensureCapacity: function (eventCount) {
const numVertices = (6 * (1 /*quads*/)) * eventCount;
xys = ensureCapacityFloat32(xys, 2 * numVertices);
rgbas = ensureCapacityFloat32(rgbas, 4 * numVertices);
rgbasSecondary = ensureCapacityFloat32(rgbasSecondary, 4 * numVertices);
relativeXys = ensureCapacityFloat32(relativeXys, 2 * numVertices);
fillPattern = ensureCapacityFloat32(fillPattern, numVertices);
},
fillEvent: function (laneIndex, eventIndex, indexXys, indexRgbas, viewport, indexRelativeXys, indexFillPattern) {
const lane = lanes.lane(laneIndex);
const event = lane.event(eventIndex);
const wBorder = borderThickness / viewport.w;
const hBorder = borderThickness / viewport.h;
const _topMargin = hasval(event.topMargin) ? event.topMargin : topMargin;
const _bottomMargin = hasval(event.bottomMargin) ? event.bottomMargin : bottomMargin;
const jTop = rowTopPadding + (laneIndex) * laneHeight + _topMargin;
const yTop = (viewport.h - jTop) / viewport.h;
const jBottom = rowTopPadding + (laneIndex + 1) * laneHeight - _bottomMargin;
const yBottom = (viewport.h - jBottom) / viewport.h;
const xLeft = timeAxis.tFrac(event.start_PMILLIS);
const xRight = timeAxis.tFrac(event.end_PMILLIS);
const xWidthPixels = viewport.w * (xRight - xLeft);
const yHeightPixels = jTop - jBottom;
if (!(xRight < 0 || xLeft > 1) && xWidthPixels > minimumVisibleWidth) {
// Fill
let fillColor = (event.bgColor || defaultColor);
let fillColorSecondary = (event.bgSecondaryColor || defaultColorSecondary);
if (event === selection.hoveredEvent.value) {
fillColor = darker(fillColor, 0.8);
fillColorSecondary = darker(fillColorSecondary, 0.8);
}
indexXys = putQuadXys(xys, indexXys, xLeft + wBorder, xRight - wBorder, yTop - hBorder, yBottom + hBorder);
const startIndex = indexRgbas;
putQuadRgbas(rgbas, startIndex, fillColor);
indexRgbas = putQuadRgbas(rgbasSecondary, startIndex, fillColorSecondary);
// create a quad with relative coordinates
indexRelativeXys = putQuadXys(relativeXys, indexRelativeXys, 0.0, xWidthPixels, 0.0, yHeightPixels);
// Set the fillPatternValue per vertex of the quad
const fillPatternValue = event.fillPattern;
fillPattern[indexFillPattern++] = fillPatternValue;
fillPattern[indexFillPattern++] = fillPatternValue;
fillPattern[indexFillPattern++] = fillPatternValue;
fillPattern[indexFillPattern++] = fillPatternValue;
fillPattern[indexFillPattern++] = fillPatternValue;
fillPattern[indexFillPattern++] = fillPatternValue;
}
return { indexXys: indexXys, indexRgbas: indexRgbas, indexRelativeXys: indexRelativeXys, indexFillPattern: indexFillPattern };
}
};
}
function eventDashedBorderPainterHelper(barOpts, drawable, timeAxis, lanes, ui, options) {
const rowTopPadding = options.rowTopPadding;
const rowBottomPadding = options.rowBottomPadding;
const laneHeight = options.laneHeight;
const topMargin = (hasval(barOpts) && hasval(barOpts.topMargin) ? barOpts.topMargin : 1.2);
const bottomMargin = (hasval(barOpts) && hasval(barOpts.bottomMargin) ? barOpts.bottomMargin : 1.2);
const borderThickness = (hasval(barOpts) && hasval(barOpts.borderThickness) ? barOpts.borderThickness : 2);
const cornerType = (hasval(barOpts) && hasval(barOpts.cornerType) ? barOpts.cornerType : JointType.BEVEL);
const defaultColor = (hasval(barOpts) && hasval(barOpts.defaultColor) ? barOpts.defaultColor : options.timelineFgColor.withAlphaTimes(0.4));
const defaultBorderColor = (hasval(barOpts) && hasval(barOpts.defaultBorderColor) ? barOpts.defaultBorderColor : null);
const selectedBorderColor = (hasval(barOpts) && hasval(barOpts.selectedBorderColor) ? barOpts.selectedBorderColor : null);
const minimumVisibleWidth = (hasval(barOpts) && hasval(barOpts.minimumVisibleWidth) ? barOpts.minimumVisibleWidth : 0);
const dashLength = (hasval(barOpts) && hasval(barOpts.dashLength) ? barOpts.dashLength : 5);
const defaultSecondaryColor = new Color(0, 0, 0, 0);
const selection = ui.selection;
const dashedBorder_VERTSHADER = concatLines(' ', ' attribute vec2 a_XyFrac; ', ' attribute vec4 a_Color; ', ' attribute vec4 a_SecondaryColor; ', ' attribute float a_LengthSoFar; ', ' ', ' varying vec4 v_Color; ', ' varying vec4 v_SecondaryColor; ', ' varying float f_LengthSoFar; ', ' ', ' void main( ) { ', ' gl_Position = vec4( ( -1.0 + 2.0*a_XyFrac ), 0.0, 1.0 ); ', ' v_Color = a_Color; ', ' v_SecondaryColor = a_SecondaryColor; ', ' f_LengthSoFar = a_LengthSoFar; ', ' } ', ' ');
const varyingBorder_FRAGSHADER = concatLines(' ', ' precision lowp float; ', ' varying vec4 v_Color; ', ' varying vec4 v_SecondaryColor; ', ' varying float f_LengthSoFar; ', ' //dashes are u_DashLength_PX pixels long ', ' uniform float u_DashLength_PX; ', ' ', ' void main( ) { ', ' gl_FragColor = v_Color; ', ' ', ' if (f_LengthSoFar > 0.0) { ', ' float mod = mod(f_LengthSoFar, u_DashLength_PX * 2.0); ', ' float alpha = 1.0; ', ' if ( mod < u_DashLength_PX ) { ', ' gl_FragColor = v_SecondaryColor; ', ' }