UNPKG

@kitware/vtk.js

Version:

Visualization Toolkit for the Web

1,056 lines (985 loc) 37 kB
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 };