@kitware/vtk.js
Version:
Visualization Toolkit for the Web
1,056 lines (985 loc) • 37 kB
JavaScript
import { m as macro } from '../../macros2.js';
import { F as arrayRange } from '../../Common/Core/Math/index.js';
import WebworkerPromise from 'webworker-promise';
import { W as WorkerFactory } from '../../_virtual/rollup-plugin-worker-loader__module_Sources/Interaction/Widgets/PiecewiseGaussianWidget/ComputeHistogram.worker.js';
/* eslint-disable no-continue */
// ----------------------------------------------------------------------------
// Global structures
// ----------------------------------------------------------------------------
const MIN_GAUSSIAN_WIDTH = 0.001;
const ACTION_TO_CURSOR = {
adjustPosition: '-webkit-grab',
adjustHeight: 'row-resize',
adjustBias: 'crosshair',
adjustWidth: 'col-resize',
adjustZoom: 'col-resize'
};
const TOUCH_CLICK = [];
// ----------------------------------------------------------------------------
// Global methods
// ----------------------------------------------------------------------------
const ACTIONS = {
adjustPosition(x, y, _ref) {
let {
originalXY,
gaussian,
originalGaussian
} = _ref;
const xOffset = originalGaussian.position - originalXY[0];
gaussian.position = x + xOffset;
return true;
},
adjustHeight(x, y, _ref2) {
let {
model,
gaussian
} = _ref2;
gaussian.height = 1 - y;
gaussian.height = Math.min(1, Math.max(model.gaussianMinimumHeight, gaussian.height));
return true;
},
adjustBias(x, y, _ref3) {
let {
originalXY,
gaussian,
originalGaussian
} = _ref3;
gaussian.xBias = originalGaussian.xBias - (originalXY[0] - x) / gaussian.height;
gaussian.yBias = originalGaussian.yBias + 4 * (originalXY[1] - y) / gaussian.height;
// Clamps
gaussian.xBias = Math.max(-1, Math.min(1, gaussian.xBias));
gaussian.yBias = Math.max(0, Math.min(2, gaussian.yBias));
return true;
},
adjustWidth(x, y, _ref4) {
let {
originalXY,
gaussian,
originalGaussian,
gaussianSide
} = _ref4;
gaussian.width = gaussianSide < 0 ? originalGaussian.width - (originalXY[0] - x) : originalGaussian.width + (originalXY[0] - x);
if (gaussian.width < MIN_GAUSSIAN_WIDTH) {
gaussian.width = MIN_GAUSSIAN_WIDTH;
}
return true;
},
adjustZoom(x, y, _ref5) {
let {
rangeZoom,
publicAPI
} = _ref5;
const delta = rangeZoom[1] - rangeZoom[0];
const absNormX = (x - rangeZoom[0]) / delta;
const minDelta = Math.abs(absNormX - rangeZoom[0]);
const maxDelta = Math.abs(absNormX - rangeZoom[1]);
const meanDelta = Math.abs(absNormX - 0.5 * (rangeZoom[0] + rangeZoom[1]));
if (meanDelta < Math.min(minDelta, maxDelta)) {
const halfDelta = delta * 0.5;
rangeZoom[0] = Math.min(Math.max(absNormX - halfDelta, 0), rangeZoom[1] - 0.1);
rangeZoom[1] = Math.max(Math.min(absNormX + halfDelta, 1), rangeZoom[0] + 0.1);
} else if (minDelta < maxDelta) {
rangeZoom[0] = Math.min(Math.max(absNormX, 0), rangeZoom[1] - 0.1);
} else {
rangeZoom[1] = Math.max(Math.min(absNormX, 1), rangeZoom[0] + 0.1);
}
publicAPI.invokeZoomChange(rangeZoom);
// The opacity did not changed
return false;
}
};
// ----------------------------------------------------------------------------
function computeOpacities(gaussians) {
let sampling = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 256;
const opacities = [];
while (opacities.length < sampling) {
opacities.push(0);
}
let count = gaussians.length;
while (count--) {
const {
position,
height,
width,
xBias,
yBias
} = gaussians[count];
for (let i = 0; i < sampling; i++) {
const x = i / (sampling - 1);
// clamp non-zero values to pos +/- width
if (x > position + width || x < position - width) {
if (opacities[i] < 0.0) {
opacities[i] = 0.0;
}
continue;
}
// non-zero width
const correctedWidth = width < MIN_GAUSSIAN_WIDTH ? MIN_GAUSSIAN_WIDTH : width;
// translate the original x to a new x based on the xbias
let x0 = 0;
if (xBias === 0 || x === position + xBias) {
x0 = x;
} else if (x > position + xBias) {
if (correctedWidth === xBias) {
x0 = position;
} else {
x0 = position + (x - position - xBias) * (correctedWidth / (correctedWidth - xBias));
}
} else if (-correctedWidth === xBias) {
// (x < pos+xBias)
x0 = position;
} else {
x0 = position - (x - position - xBias) * (correctedWidth / (correctedWidth + xBias));
}
// center around 0 and normalize to -1,1
const x1 = (x0 - position) / correctedWidth;
// do a linear interpolation between:
// a gaussian and a parabola if 0 < yBias <1
// a parabola and a step function if 1 < yBias <2
const h0a = Math.exp(-(4 * x1 * x1));
const h0b = 1.0 - x1 * x1;
const h0c = 1.0;
let h1;
if (yBias < 1) {
h1 = yBias * h0b + (1 - yBias) * h0a;
} else {
h1 = (2 - yBias) * h0b + (yBias - 1) * h0c;
}
const h2 = height * h1;
// perform the MAX over different gaussians, not the sum
if (h2 > opacities[i]) {
opacities[i] = h2;
}
}
}
return opacities;
}
// ----------------------------------------------------------------------------
function applyGaussianToPiecewiseFunction(gaussians, sampling, rangeToUse, piecewiseFunction) {
const opacities = computeOpacities(gaussians, sampling);
const nodes = [];
const delta = (rangeToUse[1] - rangeToUse[0]) / (opacities.length - 1);
const midpoint = 0.5;
const sharpness = 0;
for (let index = 0; index < opacities.length; index++) {
const x = rangeToUse[0] + delta * index;
const y = opacities[index];
nodes.push({
x,
y,
midpoint,
sharpness
});
}
piecewiseFunction.setNodes(nodes);
}
// ----------------------------------------------------------------------------
function drawChart(ctx, area, values) {
let style = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {
lineWidth: 1,
strokeStyle: '#000'
};
const verticalScale = area[3];
const horizontalScale = area[2] / (values.length - 1);
const fill = !!style.fillStyle;
const offset = verticalScale + area[1];
ctx.lineWidth = style.lineWidth;
ctx.strokeStyle = style.strokeStyle;
ctx.beginPath();
ctx.moveTo(area[0], area[1] + area[3]);
for (let index = 0; index < values.length; index++) {
ctx.lineTo(area[0] + index * horizontalScale, Math.max(area[1], offset - values[index] * verticalScale));
}
if (fill) {
ctx.fillStyle = style.fillStyle;
ctx.lineTo(area[0] + area[2], area[1] + area[3]);
if (style.clip) {
ctx.clip();
return;
}
ctx.fill();
}
ctx.stroke();
}
// ----------------------------------------------------------------------------
function updateColorCanvas(colorTransferFunction, width, rangeToUse, canvas) {
const workCanvas = canvas || document.createElement('canvas');
workCanvas.setAttribute('width', width);
workCanvas.setAttribute('height', 256);
const ctx = workCanvas.getContext('2d');
const rgba = colorTransferFunction.getUint8Table(rangeToUse[0], rangeToUse[1], width, 4);
const pixelsArea = ctx.getImageData(0, 0, width, 256);
for (let lineIdx = 0; lineIdx < 256; lineIdx++) {
pixelsArea.data.set(rgba, lineIdx * 4 * width);
}
const nbValues = 256 * width * 4;
const lineSize = width * 4;
for (let i = 3; i < nbValues; i += 4) {
pixelsArea.data[i] = 255 - Math.floor(i / lineSize);
}
ctx.putImageData(pixelsArea, 0, 0);
return workCanvas;
}
// ----------------------------------------------------------------------------
function updateColorCanvasFromImage(img, width, canvas) {
const workCanvas = canvas || document.createElement('canvas');
workCanvas.setAttribute('width', width);
workCanvas.setAttribute('height', 256);
const ctx = workCanvas.getContext('2d');
ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, width, 256);
return workCanvas;
}
// ----------------------------------------------------------------------------
function normalizeCoordinates(x, y, subRectangeArea) {
let zoomRange = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : [0, 1];
return [zoomRange[0] + (x - subRectangeArea[0]) / subRectangeArea[2] * (zoomRange[1] - zoomRange[0]), (y - subRectangeArea[1]) / subRectangeArea[3]];
}
// ----------------------------------------------------------------------------
function findGaussian(x, gaussians) {
const distances = gaussians.map(g => Math.abs(g.position - x));
const min = Math.min(...distances);
return distances.indexOf(min);
}
// ----------------------------------------------------------------------------
function createListener(callback) {
let preventDefault = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
return e => {
const {
offsetX,
offsetY
} = e;
if (preventDefault) {
e.preventDefault();
}
callback(offsetX, offsetY);
};
}
// ----------------------------------------------------------------------------
function createTouchClickListener() {
const id = TOUCH_CLICK.length;
for (var _len = arguments.length, callbacks = new Array(_len), _key = 0; _key < _len; _key++) {
callbacks[_key] = arguments[_key];
}
TOUCH_CLICK.push({
callbacks,
timeout: 0,
deltaT: 200,
count: 0,
ready: false
});
return id;
}
// ----------------------------------------------------------------------------
function processTouchClicks() {
TOUCH_CLICK.filter(t => t.ready).forEach(touchHandle => {
touchHandle.callbacks.forEach(callback => {
if (callback.touches === touchHandle.touches && callback.clicks === touchHandle.count) {
callback.action(...touchHandle.singleTouche);
}
});
// Clear state
touchHandle.ts = 0;
touchHandle.count = 0;
touchHandle.touches = 0;
touchHandle.ready = false;
});
}
// ----------------------------------------------------------------------------
function createTouchListener(id, callback) {
let nbTouches = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 1;
let preventDefault = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true;
return e => {
const targetBounds = e.target.getBoundingClientRect();
const relativeTouches = Array.prototype.map.call(e.touches, t => [t.pageX - targetBounds.left, t.pageY - targetBounds.top]);
const singleTouche = relativeTouches.reduce((a, b) => [a[0] + b[0], a[1] + b[1]], [0, 0]).map(v => v / e.touches.length);
if (e.type === 'touchstart') {
clearTimeout(TOUCH_CLICK[id].timeout);
TOUCH_CLICK[id].ts = e.timeStamp;
TOUCH_CLICK[id].singleTouche = singleTouche;
TOUCH_CLICK[id].touches = e.touches.length;
} else if (e.type === 'touchmove') {
TOUCH_CLICK[id].ts = 0;
TOUCH_CLICK[id].count = 0;
TOUCH_CLICK[id].ready = false;
} else if (e.type === 'touchend') {
if (e.timeStamp - TOUCH_CLICK[id].ts < TOUCH_CLICK[id].deltaT) {
TOUCH_CLICK[id].count += 1;
TOUCH_CLICK[id].ready = true;
if (preventDefault) {
e.preventDefault();
}
TOUCH_CLICK[id].timeout = setTimeout(processTouchClicks, TOUCH_CLICK[id].deltaT);
} else {
TOUCH_CLICK[id].ready = false;
}
}
if (e.touches.length === nbTouches) {
callback(...singleTouche);
if (preventDefault) {
e.preventDefault();
}
}
};
}
// ----------------------------------------------------------------------------
function listenerSelector(condition, ok, ko) {
return e => condition() ? ok(e) : ko(e);
}
// ----------------------------------------------------------------------------
function rescaleArray(array, focusArea) {
if (!focusArea) {
return array;
}
const maxIdx = array.length - 1;
const idxRange = focusArea.map(v => Math.round(v * maxIdx));
return array.slice(idxRange[0], idxRange[1] + 1);
}
// ----------------------------------------------------------------------------
function rescaleValue(value, focusArea) {
if (!focusArea) {
return value;
}
return (value - focusArea[0]) / (focusArea[1] - focusArea[0]);
}
// ----------------------------------------------------------------------------
// Static API
// ----------------------------------------------------------------------------
const STATIC = {
applyGaussianToPiecewiseFunction,
computeOpacities,
createListener,
drawChart,
findGaussian,
listenerSelector,
normalizeCoordinates
};
// ----------------------------------------------------------------------------
// vtkPiecewiseGaussianWidget methods
// ----------------------------------------------------------------------------
function vtkPiecewiseGaussianWidget(publicAPI, model) {
// Set our className
model.classHierarchy.push('vtkPiecewiseGaussianWidget');
if (!model.canvas) {
model.canvas = document.createElement('canvas');
}
publicAPI.setContainer = el => {
if (model.container && model.container !== el) {
model.container.removeChild(model.canvas);
}
if (model.container !== el) {
model.container = el;
if (model.container) {
model.container.appendChild(model.canvas);
}
publicAPI.modified();
}
};
publicAPI.setGaussians = gaussians => {
if (model.gaussians === gaussians) {
return;
}
model.gaussians = gaussians;
model.opacities = computeOpacities(model.gaussians, model.piecewiseSize);
publicAPI.invokeOpacityChange(publicAPI);
publicAPI.modified();
};
publicAPI.addGaussian = (position, height, width, xBias, yBias) => {
const nextIndex = model.gaussians.length;
model.gaussians.push({
position,
height,
width,
xBias,
yBias
});
model.opacities = computeOpacities(model.gaussians, model.piecewiseSize);
publicAPI.invokeOpacityChange(publicAPI);
publicAPI.modified();
return nextIndex;
};
publicAPI.removeGaussian = index => {
model.gaussians.splice(index, 1);
model.opacities = computeOpacities(model.gaussians, model.piecewiseSize);
publicAPI.invokeOpacityChange(publicAPI);
publicAPI.modified();
};
publicAPI.setSize = (width, height) => {
model.canvas.setAttribute('width', width);
model.canvas.setAttribute('height', height);
if (model.size[0] !== width || model.size[1] !== height) {
model.size = [width, height];
model.colorCanvasMTime = 0;
publicAPI.modified();
}
};
publicAPI.updateStyle = style => {
model.style = {
...model.style,
...style
};
publicAPI.modified();
};
// Method used to compute and show data distribution in the background.
// When an array with many components is used, you can provide additional
// information to choose which component you want to extract the histogram
// from.
publicAPI.setDataArray = function (array) {
let {
numberOfBinToConsiders = 1,
numberOfBinsToSkip = 1,
numberOfComponents = 1,
component = 0
} = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
model.histogram = null;
model.histogramArray = array;
model.dataRange = arrayRange(array, component, numberOfComponents);
const [min, max] = model.dataRange;
const maxNumberOfWorkers = 4;
let arrayStride = Math.floor(array.length / maxNumberOfWorkers) || 1;
arrayStride += arrayStride % numberOfComponents;
let arrayIndex = 0;
const workerChunks = [];
const workers = [];
while (arrayIndex < array.length) {
const worker = new WorkerFactory();
workers.push(worker);
const workerPromise = new WebworkerPromise(worker);
const arrayStart = arrayIndex;
const arrayEnd = Math.min(arrayIndex + arrayStride, array.length - 1);
const subArray = new array.constructor(array.slice(arrayStart, arrayEnd + 1));
workerChunks.push(workerPromise.postMessage({
array: subArray,
component,
numberOfComponents,
min,
max,
numberOfBins: model.numberOfBins
}, [subArray.buffer]));
arrayIndex += arrayStride;
}
Promise.all(workerChunks).then(subHistograms => {
workers.forEach(worker => worker.terminate());
model.histogram = new Float32Array(model.numberOfBins);
model.histogram.fill(0);
subHistograms.forEach(subHistogram => {
for (let i = 0, len = subHistogram.length; i < len; ++i) {
model.histogram[i] += subHistogram[i];
}
});
// Smart Rescale Histogram
const sampleSize = Math.min(numberOfBinToConsiders, model.histogram.length - numberOfBinsToSkip);
const sortedArray = Array.from(model.histogram);
sortedArray.sort((a, b) => Number(a) - Number(b));
for (let i = 0; i < numberOfBinsToSkip; i++) {
sortedArray.pop();
}
while (sortedArray.length > sampleSize) {
sortedArray.shift();
}
const mean = sortedArray.reduce((a, b) => a + b, 0) / sampleSize;
for (let i = 0, len = model.histogram.length; i < len; ++i) {
model.histogram[i] /= mean;
}
publicAPI.modified();
setTimeout(publicAPI.render, 0);
});
publicAPI.modified();
};
publicAPI.onClick = (x, y) => {
const [xNormalized, yNormalized] = normalizeCoordinates(x, y, model.graphArea, model.enableRangeZoom ? model.rangeZoom : null);
if (xNormalized < 0 && model.style.iconSize > 1) {
// Control buttons
const delta = model.style.iconSize + model.style.padding;
let offset = delta;
let buttonIdx = 0;
while (y > offset) {
buttonIdx += 1;
offset += delta;
}
switch (buttonIdx) {
case 0:
{
const gaussianIdx = publicAPI.addGaussian(0, 1, 0.1, 0, 0);
const gaussian = model.gaussians[gaussianIdx];
const originalGaussian = {
...gaussian
};
const action = ACTIONS.adjustPosition;
model.activeGaussian = gaussianIdx;
model.selectedGaussian = gaussianIdx;
// Fake active action
macro.setImmediate(() => {
publicAPI.onDown(x, y);
model.dragAction = {
originalXY: [0, 0],
action,
gaussian,
originalGaussian
};
});
break;
}
case 1:
{
if (model.selectedGaussian !== -1) {
publicAPI.removeGaussian(model.selectedGaussian);
}
break;
}
default:
{
model.selectedGaussian = -1;
model.dragAction = null;
}
}
} else if (xNormalized < 0 || xNormalized > 1 || yNormalized < 0 || yNormalized > 1) {
model.selectedGaussian = -1;
model.dragAction = null;
} else {
const newSelected = findGaussian(xNormalized, model.gaussians);
if (newSelected !== model.selectedGaussian) {
model.selectedGaussian = newSelected;
publicAPI.modified();
}
}
return true;
};
publicAPI.onHover = (x, y) => {
// Determines the interaction region size for adjusting the Gaussian's
// height.
const tolerance = 20 / model.canvas.height;
const [xNormalized, yNormalized] = normalizeCoordinates(x, y, model.graphArea, model.enableRangeZoom ? model.rangeZoom : null);
const [xNormalizedAbs] = normalizeCoordinates(x, y, model.graphArea);
const newActive = xNormalized < 0 ? model.selectedGaussian : findGaussian(xNormalized, model.gaussians);
model.canvas.style.cursor = 'default';
const gaussian = model.gaussians[newActive];
if (model.enableRangeZoom && xNormalizedAbs >= 0 && y < model.graphArea[1] - 6 // circle radius
) {
const thirdDelta = (model.rangeZoom[1] - model.rangeZoom[0]) / 3;
if (xNormalizedAbs < model.rangeZoom[0] + thirdDelta || xNormalizedAbs > model.rangeZoom[1] - thirdDelta) {
model.canvas.style.cursor = ACTION_TO_CURSOR.adjustZoom;
} else {
model.canvas.style.cursor = ACTION_TO_CURSOR.adjustPosition;
}
model.dragAction = {
rangeZoom: model.rangeZoom,
action: ACTIONS.adjustZoom
};
} else if (gaussian && xNormalizedAbs >= 0) {
const invY = 1 - yNormalized;
let actionName = null;
if (invY > gaussian.height + tolerance) {
actionName = 'adjustPosition';
} else if (invY > gaussian.height - tolerance) {
if (Math.abs(xNormalized - gaussian.position) < tolerance) {
actionName = 'adjustHeight';
} else {
actionName = 'adjustPosition';
}
} else if (invY > gaussian.height * 0.5 + tolerance) {
actionName = 'adjustPosition';
} else if (invY > gaussian.height * 0.5 - tolerance) {
if (Math.abs(xNormalized - gaussian.position) < tolerance) {
actionName = 'adjustBias';
} else {
actionName = 'adjustPosition';
}
} else if (invY > tolerance) {
actionName = 'adjustPosition';
} else {
actionName = 'adjustWidth';
}
model.canvas.style.cursor = ACTION_TO_CURSOR[actionName];
const action = ACTIONS[actionName];
const originalGaussian = {
...gaussian
};
model.dragAction = {
originalXY: [xNormalized, yNormalized],
action,
gaussian,
originalGaussian
};
}
if (newActive !== model.activeGaussian) {
model.activeGaussian = newActive;
publicAPI.modified();
}
return true;
};
publicAPI.onDown = (x, y) => {
if (!model.mouseIsDown) {
publicAPI.invokeAnimation(true);
}
model.mouseIsDown = true;
const xNormalized = normalizeCoordinates(x, y, model.graphArea, model.enableRangeZoom ? model.rangeZoom : null)[0];
const newSelected = findGaussian(xNormalized, model.gaussians);
model.gaussianSide = 0;
const gaussian = model.gaussians[newSelected];
if (gaussian) {
model.gaussianSide = gaussian.position - xNormalized;
}
if (newSelected !== model.selectedGaussian && xNormalized > 0) {
model.selectedGaussian = newSelected;
publicAPI.modified();
}
return true;
};
publicAPI.onDrag = (x, y) => {
if (model.dragAction) {
const [xNormalized, yNormalized] = normalizeCoordinates(x, y, model.graphArea, model.enableRangeZoom ? model.rangeZoom : null);
const {
action
} = model.dragAction;
if (action(xNormalized, yNormalized, {
gaussianSide: model.gaussianSide,
model,
publicAPI,
...model.dragAction
})) {
model.opacities = computeOpacities(model.gaussians, model.piecewiseSize);
publicAPI.invokeOpacityChange(publicAPI, true);
}
publicAPI.modified();
}
return true;
};
publicAPI.onUp = (x, y) => {
if (model.mouseIsDown) {
publicAPI.invokeAnimation(false);
}
model.mouseIsDown = false;
return true;
};
publicAPI.onLeave = (x, y) => {
publicAPI.onUp(x, y);
model.canvas.style.cursor = 'default';
model.activeGaussian = -1;
publicAPI.modified();
return true;
};
publicAPI.onAddGaussian = (x, y) => {
const [xNormalized, yNormalized] = normalizeCoordinates(x, y, model.graphArea, model.enableRangeZoom ? model.rangeZoom : null);
if (xNormalized >= 0) {
publicAPI.addGaussian(xNormalized, 1 - yNormalized, 0.1, 0, 0);
}
return true;
};
publicAPI.onRemoveGaussian = (x, y) => {
const xNormalized = normalizeCoordinates(x, y, model.graphArea, model.enableRangeZoom ? model.rangeZoom : null)[0];
const newSelected = findGaussian(xNormalized, model.gaussians);
if (xNormalized >= 0 && newSelected !== -1) {
publicAPI.removeGaussian(newSelected);
}
return true;
};
publicAPI.bindMouseListeners = () => {
if (!model.listeners) {
const isDown = () => !!model.mouseIsDown;
const touchId = createTouchClickListener({
clicks: 1,
touches: 1,
action: publicAPI.onClick
}, {
clicks: 2,
touches: 1,
action: publicAPI.onAddGaussian
}, {
clicks: 2,
touches: 2,
action: publicAPI.onRemoveGaussian
});
model.listeners = {
mousemove: listenerSelector(isDown, createListener(publicAPI.onDrag), createListener(publicAPI.onHover)),
dblclick: createListener(publicAPI.onAddGaussian),
contextmenu: createListener(publicAPI.onRemoveGaussian),
click: createListener(publicAPI.onClick),
mouseup: createListener(publicAPI.onUp),
mousedown: createListener(publicAPI.onDown),
mouseout: createListener(publicAPI.onLeave),
touchstart: createTouchListener(touchId, macro.chain(publicAPI.onHover, publicAPI.onDown)),
touchmove: listenerSelector(isDown, createTouchListener(touchId, publicAPI.onDrag), createTouchListener(touchId, publicAPI.onHover)),
touchend: createTouchListener(touchId, publicAPI.onUp, 0) // touchend have 0 touch event...
};
Object.keys(model.listeners).forEach(eventType => {
model.canvas.addEventListener(eventType, model.listeners[eventType], false);
});
}
};
publicAPI.unbindMouseListeners = () => {
if (model.listeners) {
Object.keys(model.listeners).forEach(eventType => {
model.canvas.removeEventListener(eventType, model.listeners[eventType]);
});
delete model.listeners;
}
};
publicAPI.render = () => {
const ctx = model.canvas.getContext('2d');
ctx.imageSmoothingEnabled = true;
const [width, height] = model.size;
const offset = model.style.padding;
const graphArea = [Math.floor(model.style.iconSize + offset), Math.floor(offset), Math.ceil(width - 2 * offset - model.style.iconSize), Math.ceil(height - 2 * offset)];
const zoomControlHeight = model.style.zoomControlHeight;
if (model.enableRangeZoom) {
graphArea[1] += Math.floor(zoomControlHeight);
graphArea[3] -= Math.floor(zoomControlHeight);
}
model.graphArea = graphArea;
// Clear canvas
ctx.clearRect(0, 0, width, height);
ctx.lineJoin = 'round';
ctx.fillStyle = model.style.backgroundColor;
ctx.fillRect(...graphArea);
if (model.style.iconSize > 1) {
// Draw icons
// +
const halfSize = Math.round(model.style.iconSize / 2 - model.style.strokeWidth);
const center = Math.round(halfSize + offset + model.style.strokeWidth);
ctx.beginPath();
ctx.lineWidth = model.style.buttonStrokeWidth;
ctx.strokeStyle = model.style.buttonStrokeColor;
ctx.arc(center - offset / 2, center, halfSize, 0, 2 * Math.PI, false);
ctx.fillStyle = model.style.buttonFillColor;
ctx.fill();
ctx.stroke();
ctx.moveTo(center - halfSize + model.style.strokeWidth + 2 - offset / 2, center);
ctx.lineTo(center + halfSize - model.style.strokeWidth - 2 - offset / 2, center);
ctx.stroke();
ctx.moveTo(center - offset / 2, center - halfSize + model.style.strokeWidth + 2);
ctx.lineTo(center - offset / 2, center + halfSize - model.style.strokeWidth - 2);
ctx.stroke();
// -
if (model.selectedGaussian === -1) {
ctx.fillStyle = model.style.buttonDisableFillColor;
ctx.lineWidth = model.style.buttonDisableStrokeWidth;
ctx.strokeStyle = model.style.buttonDisableStrokeColor;
} else {
ctx.fillStyle = model.style.buttonFillColor;
ctx.lineWidth = model.style.buttonStrokeWidth;
ctx.strokeStyle = model.style.buttonStrokeColor;
}
ctx.beginPath();
ctx.arc(center - offset / 2, center + offset / 2 + model.style.iconSize, halfSize, 0, 2 * Math.PI, false);
ctx.fill();
ctx.stroke();
ctx.moveTo(center - halfSize + model.style.strokeWidth + 2 - offset / 2, center + offset / 2 + model.style.iconSize);
ctx.lineTo(center + halfSize - model.style.strokeWidth - 2 - offset / 2, center + offset / 2 + model.style.iconSize);
ctx.stroke();
}
// Draw histogram
if (model.histogram) {
drawChart(ctx, graphArea, rescaleArray(model.histogram, model.rangeZoom), {
lineWidth: 1,
strokeStyle: model.style.histogramColor,
fillStyle: model.style.histogramColor
});
}
// Draw gaussians
drawChart(ctx, graphArea, rescaleArray(model.opacities, model.enableRangeZoom && model.rangeZoom), {
lineWidth: model.style.strokeWidth,
strokeStyle: model.style.strokeColor
});
// Draw color function if any
if (model.colorTransferFunction && model.colorTransferFunction.getSize()) {
const rangeToUse = model.dataRange || model.colorTransferFunction.getMappingRange();
if (!model.colorCanvas || model.colorCanvasMTime !== model.colorTransferFunction.getMTime()) {
model.colorCanvasMTime = model.colorTransferFunction.getMTime();
model.colorCanvas = updateColorCanvas(model.colorTransferFunction, graphArea[2], rangeToUse, model.colorCanvas);
}
ctx.save();
drawChart(ctx, graphArea, rescaleArray(model.opacities, model.enableRangeZoom && model.rangeZoom), {
lineWidth: 1,
strokeStyle: 'rgba(0,0,0,0)',
fillStyle: 'rgba(0,0,0,1)',
clip: true
});
// Draw the correct portion of the color BG image
if (model.enableRangeZoom) {
ctx.drawImage(model.colorCanvas, model.rangeZoom[0] * graphArea[2], 0, graphArea[2], graphArea[3], graphArea[0], graphArea[1], graphArea[2] / (model.rangeZoom[1] - model.rangeZoom[0]), graphArea[3]);
} else {
ctx.drawImage(model.colorCanvas, graphArea[0], graphArea[1]);
}
ctx.restore();
} else if (model.backgroundImage) {
model.colorCanvas = updateColorCanvasFromImage(model.backgroundImage, graphArea[2], model.colorCanvas);
ctx.save();
drawChart(ctx, graphArea, rescaleArray(model.opacities, model.enableRangeZoom && model.rangeZoom), {
lineWidth: 1,
strokeStyle: 'rgba(0,0,0,0)',
fillStyle: 'rgba(0,0,0,1)',
clip: true
});
ctx.drawImage(model.colorCanvas, graphArea[0], graphArea[1]);
ctx.restore();
}
// Draw zoomed area
if (model.enableRangeZoom) {
ctx.fillStyle = model.style.zoomControlColor;
ctx.beginPath();
ctx.rect(graphArea[0] + model.rangeZoom[0] * graphArea[2], 0, (model.rangeZoom[1] - model.rangeZoom[0]) * graphArea[2], zoomControlHeight);
ctx.fill();
}
// Draw active gaussian
const activeGaussian = model.gaussians[model.activeGaussian] || model.gaussians[model.selectedGaussian];
if (activeGaussian) {
const activeOpacities = computeOpacities([activeGaussian], graphArea[2]);
drawChart(ctx, graphArea, rescaleArray(activeOpacities, model.enableRangeZoom && model.rangeZoom), {
lineWidth: model.style.activeStrokeWidth,
strokeStyle: model.style.activeColor
});
// Draw controls
const xCenter = graphArea[0] + rescaleValue(activeGaussian.position, model.enableRangeZoom && model.rangeZoom) * graphArea[2];
const yTop = graphArea[1] + (1 - activeGaussian.height) * graphArea[3];
const yMiddle = graphArea[1] + (1 - 0.5 * activeGaussian.height) * graphArea[3];
const yBottom = graphArea[1] + graphArea[3];
let widthInPixel = activeGaussian.width * graphArea[2];
if (model.enableRangeZoom) {
widthInPixel /= model.rangeZoom[1] - model.rangeZoom[0];
}
ctx.lineWidth = model.style.handleWidth;
ctx.strokeStyle = model.style.handleColor;
ctx.fillStyle = model.style.backgroundColor;
ctx.beginPath();
ctx.moveTo(xCenter, graphArea[1] + (1 - activeGaussian.height) * graphArea[3]);
ctx.lineTo(xCenter, graphArea[1] + graphArea[3]);
ctx.stroke();
// Height
ctx.beginPath();
ctx.arc(xCenter, yTop, 6, 0, 2 * Math.PI);
ctx.fill();
ctx.stroke();
// Bias
const radius = Math.min(widthInPixel * 0.1, activeGaussian.height * graphArea[3] * 0.2);
ctx.beginPath();
ctx.rect(xCenter - radius, yMiddle - radius, radius * 2, radius * 2);
ctx.fill();
ctx.stroke();
ctx.beginPath();
// Width
const sliderWidth = widthInPixel * 0.8;
ctx.rect(xCenter - sliderWidth, yBottom - 5, 2 * sliderWidth, 10);
ctx.fill();
ctx.stroke();
}
};
publicAPI.getOpacityNodes = dataRange => {
const rangeToUse = dataRange || model.dataRange;
const delta = (rangeToUse[1] - rangeToUse[0]) / (model.opacities.length - 1);
const nodes = [];
const midpoint = 0.5;
const sharpness = 0;
for (let index = 0; index < model.opacities.length; index++) {
const x = rangeToUse[0] + delta * index;
const y = model.opacities[index];
nodes.push({
x,
y,
midpoint,
sharpness
});
}
return nodes;
};
publicAPI.applyOpacity = (piecewiseFunction, dataRange) => {
const nodes = publicAPI.getOpacityNodes(dataRange);
piecewiseFunction.setNodes(nodes);
};
publicAPI.getOpacityRange = dataRange => {
const rangeToUse = dataRange || model.dataRange;
const delta = (rangeToUse[1] - rangeToUse[0]) / (model.opacities.length - 1);
let minIndex = model.opacities.length - 1;
let maxIndex = 0;
for (let index = 0; index < model.opacities.length; index++) {
if (model.opacities[index] > 0) {
minIndex = Math.min(minIndex, index);
}
if (model.opacities[index] > 0) {
maxIndex = Math.max(maxIndex, index);
}
}
return [rangeToUse[0] + minIndex * delta, rangeToUse[0] + maxIndex * delta];
};
const enableZoom = publicAPI.setEnableRangeZoom;
publicAPI.setEnableRangeZoom = v => {
const change = enableZoom(v);
if (change) {
model.colorCanvasMTime = 0;
model.rangeZoom = [0, 1];
}
return change;
};
const rangeZoom = publicAPI.setRangeZoom;
publicAPI.setRangeZoom = function () {
const change = rangeZoom(...arguments);
if (change) {
model.colorCanvasMTime = 0;
}
return change;
};
// Trigger rendering for any modified event
publicAPI.onModified(() => publicAPI.render());
publicAPI.setSize(...model.size);
}
// ----------------------------------------------------------------------------
// Object factory
// ----------------------------------------------------------------------------
const DEFAULT_VALUES = {
histogram: [],
numberOfBins: 256,
histogramArray: null,
dataRange: [0, 1],
gaussians: [],
opacities: [],
size: [600, 300],
piecewiseSize: 256,
colorCanvasMTime: 0,
gaussianMinimumHeight: 0.05,
style: {
backgroundColor: 'rgba(255, 255, 255, 1)',
histogramColor: 'rgba(200, 200, 200, 0.5)',
strokeColor: 'rgb(0, 0, 0)',
activeColor: 'rgb(0, 0, 150)',
buttonDisableFillColor: 'rgba(255, 255, 255, 0.5)',
buttonDisableStrokeColor: 'rgba(0, 0, 0, 0.5)',
buttonStrokeColor: 'rgba(0, 0, 0, 1)',
buttonFillColor: 'rgba(255, 255, 255, 1)',
handleColor: 'rgb(0, 150, 0)',
strokeWidth: 2,
activeStrokeWidth: 3,
buttonStrokeWidth: 1.5,
handleWidth: 3,
iconSize: 20,
padding: 10,
zoomControlHeight: 10,
zoomControlColor: '#999'
},
activeGaussian: -1,
selectedGaussian: -1,
enableRangeZoom: true,
rangeZoom: [0, 1] // normalized value
};
// ----------------------------------------------------------------------------
function extend(publicAPI, model) {
let initialValues = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
Object.assign(model, DEFAULT_VALUES, initialValues);
// Object methods
macro.obj(publicAPI, model);
macro.setGet(publicAPI, model, ['piecewiseSize', 'numberOfBins', 'colorTransferFunction', 'backgroundImage', 'enableRangeZoom', 'gaussianMinimumHeight']);
macro.setGetArray(publicAPI, model, ['rangeZoom'], 2);
macro.get(publicAPI, model, ['size', 'canvas', 'gaussians']);
macro.event(publicAPI, model, 'opacityChange');
macro.event(publicAPI, model, 'animation');
macro.event(publicAPI, model, 'zoomChange');
// Object specific methods
vtkPiecewiseGaussianWidget(publicAPI, model);
}
// ----------------------------------------------------------------------------
const newInstance = macro.newInstance(extend, 'vtkPiecewiseGaussianWidget');
// ----------------------------------------------------------------------------
var vtkPiecewiseGaussianWidget$1 = {
newInstance,
extend,
...STATIC
};
export { STATIC, vtkPiecewiseGaussianWidget$1 as default, extend, newInstance };