diff --git a/Sources/Rendering/Core/ColorTransferFunction/CssFilters.d.ts b/Sources/Rendering/Core/ColorTransferFunction/CssFilters.d.ts index 3a6d959db06..32a4c0e557a 100644 --- a/Sources/Rendering/Core/ColorTransferFunction/CssFilters.d.ts +++ b/Sources/Rendering/Core/ColorTransferFunction/CssFilters.d.ts @@ -36,7 +36,10 @@ export function createIdentityFilter(outFilter?: FilterMatrix): FilterMatrix; /** * Combine two filters into a single filter - * The order matters + * Warning: it is NOT an operation inspired by CSS filters + * For this, apply filters one by one using applyFilter + * The clamping step is not applied between each filter when the filters are combined + * The order of the filters matters * @param baseFilter The first filter that will be applied * @param newFilter The second filter that will be applied * @param outFilter An optional filter that will contain the combined filter @@ -45,6 +48,7 @@ export function combineFilters(baseFilter: FilterMatrix, newFilter: FilterMatrix /** * Apply a filter to a rgb(a) point + * It is a multiplication by the matrix and a clamping * @param filter The filter * @param r The red channel (between 0 and 1) * @param g The green channel (between 0 and 1) diff --git a/Sources/Rendering/Core/ColorTransferFunction/CssFilters.js b/Sources/Rendering/Core/ColorTransferFunction/CssFilters.js index 37c9821f238..2b39761c8c7 100644 --- a/Sources/Rendering/Core/ColorTransferFunction/CssFilters.js +++ b/Sources/Rendering/Core/ColorTransferFunction/CssFilters.js @@ -39,7 +39,19 @@ export function combineFilters( export function applyFilter(filter, r, g, b, a = 1) { const vec = [r, g, b, a, 1]; multiplyMatrix(filter, vec, 5, 5, 5, 1, vec); - return vec.slice(0, 4); + // Clamp R, G, B, A + const output = new Array(4); + for (let i = 0; i < 4; ++i) { + const value = vec[i]; + if (value < 0) { + output[i] = 0; + } else if (value > 1) { + output[i] = 1; + } else { + output[i] = value; + } + } + return output; } export function createLinearFilter( diff --git a/Sources/Rendering/Core/ColorTransferFunction/test/testCssFilters.js b/Sources/Rendering/Core/ColorTransferFunction/test/testCssFilters.js new file mode 100644 index 00000000000..8846657542c --- /dev/null +++ b/Sources/Rendering/Core/ColorTransferFunction/test/testCssFilters.js @@ -0,0 +1,65 @@ +import test from 'tape'; + +import * as CssFilters from 'vtk.js/Sources/Rendering/Core/ColorTransferFunction/CssFilters'; + +test('Test CssFilters identity', (t) => { + const color = [Math.random(), Math.random(), -1, 2]; + const identity = CssFilters.createIdentityFilter(); + const output = CssFilters.applyFilter(identity, ...color); + t.deepEqual(color, output, 'Apply identity filter'); + t.end(); +}); + +test('Test CssFilters brightness', (t) => { + const color = [0.1, 0.5, 0.9, 0.2]; + const halfBright = CssFilters.createBrightnessFilter(0.5); + const output = CssFilters.applyFilter(halfBright, ...color); + t.deepEqual([0.05, 0.25, 0.45, 0.2], output, 'Apply brightness filter'); + t.end(); +}); + +test('Fuzzy test all CssFilters', (t) => { + const canvas = document.createElement('canvas'); + canvas.width = 1; + canvas.height = 1; + const ctx = canvas.getContext('2d', { willReadFrequently: true }); + + const randomChannel = () => Math.floor(Math.random() * 256); + + const numberOfRepetitions = 1000; + for (let repetition = 0; repetition < numberOfRepetitions; repetition++) { + const contrast = 2 * Math.random(); + const saturate = 2 * Math.random(); + const brightness = 2 * Math.random(); + const invert = Math.random(); + + const color = [randomChannel(), randomChannel(), randomChannel(), 255]; + + // Apply filters one by one (don't combine as it is not equivalent) + let filtersOutput = color.map((channel) => channel / 255); + const contrastFilter = CssFilters.createContrastFilter(contrast); + filtersOutput = CssFilters.applyFilter(contrastFilter, ...filtersOutput); + const saturateFilter = CssFilters.createSaturateFilter(saturate); + filtersOutput = CssFilters.applyFilter(saturateFilter, ...filtersOutput); + const brightnessFilter = CssFilters.createBrightnessFilter(brightness); + filtersOutput = CssFilters.applyFilter(brightnessFilter, ...filtersOutput); + const invertFilter = CssFilters.createInvertFilter(invert); + filtersOutput = CssFilters.applyFilter(invertFilter, ...filtersOutput); + + // Reference: canvas 2D context filters + ctx.fillStyle = `rgb(${color[0]} ${color[1]} ${color[2]})`; + ctx.filter = `contrast(${contrast}) saturate(${saturate}) brightness(${brightness}) invert(${invert})`; + ctx.beginPath(); + ctx.fillRect(0, 0, canvas.width, canvas.height); + const canvasPixel = ctx.getImageData(0, 0, 1, 1).data; + const referenceOutput = [...canvasPixel].map((x) => x / 255); + + for (let channel = 0; channel < 4; ++channel) { + const error = referenceOutput[channel] - filtersOutput[channel]; + // The error depends on the values of each filter + // For example, using a color of [127.5, 127.5, 128.49] with a saturate of 1000 creates big errors + t.assert(Math.abs(error) <= 7 / 255); + } + } + t.end(); +});