Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a CSS helper for CSS filters #3009

Merged
merged 5 commits into from
Feb 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Sources/Common/Core/Math/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -471,9 +471,9 @@ export function multiply3x3_mat3(a_3x3: Matrix3x3, b_3x3: Matrix3x3, out_3x3: Ma
* @param {Number} colA
* @param {Number} rowB
* @param {Number} colB
* @param {Matrix} out_rowXcol
* @param {Matrix} outRowAColB
*/
export function multiplyMatrix(a: Matrix, b: Matrix, rowA: number, colA: number, rowB: number, colB: number, out_rowXcol: Matrix): void;
export function multiplyMatrix(a: Matrix, b: Matrix, rowA: number, colA: number, rowB: number, colB: number, outRowAColB: Matrix): void;

/**
* Transpose a 3x3 matrix.
Expand Down
6 changes: 3 additions & 3 deletions Sources/Common/Core/Math/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,7 @@ export function multiply3x3_mat3(a_3x3, b_3x3, out_3x3) {
}
}

export function multiplyMatrix(a, b, rowA, colA, rowB, colB, out_rowXcol) {
export function multiplyMatrix(a, b, rowA, colA, rowB, colB, outRowAColB) {
// we need colA == rowB
if (colA !== rowB) {
vtkErrorMacro('Number of columns of A must match number of rows of B.');
Expand All @@ -623,10 +623,10 @@ export function multiplyMatrix(a, b, rowA, colA, rowB, colB, out_rowXcol) {
for (let i = 0; i < rowA; i++) {
// output col
for (let j = 0; j < colB; j++) {
out_rowXcol[i * colB + j] = 0;
outRowAColB[i * colB + j] = 0;
// sum for this point
for (let k = 0; k < colA; k++) {
out_rowXcol[i * colB + j] += copyA[i * colA + k] * copyB[j + colB * k];
outRowAColB[i * colB + j] += copyA[i * colA + k] * copyB[j + colB * k];
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/Filters/General/ScalarToRGBA/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ function vtkScalarToRGBA(publicAPI, model) {

const rgba = [0, 0, 0, 0];
const data = scalars.getData();
const rgbaArray = new Uint8Array(data.length * 4);
const rgbaArray = new Uint8ClampedArray(data.length * 4);
let offset = 0;
for (let idx = 0; idx < data.length; idx++) {
const x = data[idx];
Expand Down
2 changes: 1 addition & 1 deletion Sources/Rendering/Core/AxesActor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ function shiftDataset(ds, axis) {

function addColor(ds, r, g, b) {
const size = ds.getPoints().getData().length;
const rgbArray = new Uint8Array(size);
const rgbArray = new Uint8ClampedArray(size);
let offset = 0;

while (offset < size) {
Expand Down
109 changes: 109 additions & 0 deletions Sources/Rendering/Core/ColorTransferFunction/CssFilters.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* A helper file to transform RGBA points using CSS filters equivalent
* The equivalents of CSS filters using SVG filters can be found here:
* https://www.w3.org/TR/filter-effects-1/#ShorthandEquivalents
* For each SVG filter, you can look for the maths behind it on the same page:
* https://www.w3.org/TR/filter-effects-1/#FilterPrimitivesOverview
*
* For example, the saturate filter equivalent is here:
* https://www.w3.org/TR/filter-effects-1/#saturateEquivalent
* And the maths behind the feColorMatrix of type saturate is here:
* https://www.w3.org/TR/filter-effects-1/#ref-for-attr-valuedef-type-saturate
*
* The transforms are done using matrices of size 5 by 5. They are row major
* as in vtkMath. The vectors representing the RGBA points uses
* [R, G, B, A, 1] vectors, with each channel between 0 and 1.
*/

import { Matrix, Vector3 } from "../../../types";

export const luminanceWeights: Vector3;
export type FilterMatrix = Matrix;

/**
* Create a new filter matrix
* This is a 5x5 row major array
* Use applyFilter() function to use it
* It is NOT the identity
*/
export function createCSSFilter(): FilterMatrix;

/**
* Convert a filter to an identity matrix or create a new identity filter
* @param outFilter If specified, the outFilter is converted to identity filter
*/
export function createIdentityFilter(outFilter?: FilterMatrix): FilterMatrix;

/**
* Combine two filters into a single filter
* 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
*/
export function combineFilters(baseFilter: FilterMatrix, newFilter: FilterMatrix, outFilter?: FilterMatrix): 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)
* @param b The blue channel (between 0 and 1)
* @param a The optional alpha channel (between 0 and 1), defaults to 1
* @returns A vector of size 4 [r, g, b, a]
*/
export function applyFilter(filter: FilterMatrix, r: number, g: number, b: number, a?: number): [number, number, number, number];

/**
* A generic linear filter
* See svg equivalent for parameters and a specification
* https://www.w3.org/TR/filter-effects-1/#attr-valuedef-type-linear
* @param slope
* @param intercept
* @param outFilter Optional output, a new filter is created if not specified
*/
export function createLinearFilter(slope: number, intercept: number, outFilter?: FilterMatrix): FilterMatrix;

/**
* A contrast filter
* See css/svg equivalent for parameters and a specification
* https://www.w3.org/TR/filter-effects-1/#contrastEquivalent
* https://www.w3.org/TR/filter-effects-1/#attr-valuedef-type-linear
* @param contrast
* @param outFilter Optional output, a new filter is created if not specified
*/
export function createContrastFilter(contrast: number, outFilter?: FilterMatrix): FilterMatrix;

/**
* A saturate filter
* See css/svg equivalent for parameters and a specification
* https://www.w3.org/TR/filter-effects-1/#saturateEquivalent
* https://www.w3.org/TR/filter-effects-1/#ref-for-attr-valuedef-type-saturate
* @param saturate
* @param outFilter Optional output, a new filter is created if not specified
*/
export function createSaturateFilter(saturate: number, outFilter?: FilterMatrix): FilterMatrix;

/**
* A brightness filter
* See css/svg equivalent for parameters and a specification
* https://www.w3.org/TR/filter-effects-1/#brightnessEquivalent
* https://www.w3.org/TR/filter-effects-1/#attr-valuedef-type-linear
* @param brightness
* @param outFilter Optional output, a new filter is created if not specified
*/
export function createBrightnessFilter(brightness: number, outFilter?: FilterMatrix): FilterMatrix;

/**
* An invert filter
* See css/svg equivalent for parameters and a specification
* https://www.w3.org/TR/filter-effects-1/#invertEquivalent
* https://www.w3.org/TR/filter-effects-1/#attr-valuedef-type-table
* @param invert
* @param outFilter Optional output, a new filter is created if not specified
*/
export function createInvertFilter(invert: number, outFilter?: FilterMatrix): FilterMatrix;
110 changes: 110 additions & 0 deletions Sources/Rendering/Core/ColorTransferFunction/CssFilters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/**
* A helper file to transform RGBA points using CSS filters equivalent
* The equivalents of CSS filters using SVG filters can be found here:
* https://www.w3.org/TR/filter-effects-1/#ShorthandEquivalents
* For each SVG filter, you can look for the maths behind it on the same page:
* https://www.w3.org/TR/filter-effects-1/#FilterPrimitivesOverview
*
* For example, the saturate filter equivalent is here:
* https://www.w3.org/TR/filter-effects-1/#saturateEquivalent
* And the maths behind the feColorMatrix of type saturate is here:
* https://www.w3.org/TR/filter-effects-1/#ref-for-attr-valuedef-type-saturate
*
* The transforms are done using matrices of size 5 by 5. They are row major
* as in vtkMath. The vectors representing the RGBA points uses
* [R, G, B, A, 1] vectors, with each channel between 0 and 1.
*/

import { identity, multiplyMatrix } from 'vtk.js/Sources/Common/Core/Math';

export const luminanceWeights = [0.213, 0.715, 0.072];

export function createCSSFilter() {
return new Array(25);
}

export function createIdentityFilter(outFilter = createCSSFilter()) {
return identity(5, outFilter);
}

export function combineFilters(
baseFilter,
newFilter,
outFilter = createCSSFilter()
) {
multiplyMatrix(newFilter, baseFilter, 5, 5, 5, 5, outFilter);
return outFilter;
}

export function applyFilter(filter, r, g, b, a = 1) {
const vec = [r, g, b, a, 1];
multiplyMatrix(filter, vec, 5, 5, 5, 1, vec);
// 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(
slope,
intercept,
outFilter = createCSSFilter()
) {
createIdentityFilter(outFilter);
for (let row = 0; row < 3; ++row) {
outFilter[row * 5 + row] = slope;
outFilter[row * 5 + 4] = intercept;
}
return outFilter;
}

// https://www.w3.org/TR/filter-effects-1/#contrastEquivalent
// https://www.w3.org/TR/filter-effects-1/#attr-valuedef-type-linear
export function createContrastFilter(contrast, outFilter = createCSSFilter()) {
const slope = contrast;
const intercept = -(0.5 * contrast) + 0.5;
bruyeret marked this conversation as resolved.
Show resolved Hide resolved
return createLinearFilter(slope, intercept, outFilter);
}

// https://www.w3.org/TR/filter-effects-1/#saturateEquivalent
// https://www.w3.org/TR/filter-effects-1/#ref-for-attr-valuedef-type-saturate
export function createSaturateFilter(saturate, outFilter = createCSSFilter()) {
createIdentityFilter(outFilter);
for (let col = 0; col < 3; ++col) {
const columnLuminance = luminanceWeights[col];
const diagonalValue = columnLuminance + (1 - columnLuminance) * saturate;
const nonDiagonalValue = columnLuminance - columnLuminance * saturate;
for (let row = 0; row < 3; ++row) {
outFilter[row * 5 + col] = row === col ? diagonalValue : nonDiagonalValue;
}
}
return outFilter;
}

// https://www.w3.org/TR/filter-effects-1/#brightnessEquivalent
// https://www.w3.org/TR/filter-effects-1/#attr-valuedef-type-linear
export function createBrightnessFilter(
brightness,
outFilter = createCSSFilter()
) {
const slope = brightness;
const intercept = 0;
return createLinearFilter(slope, intercept, outFilter);
}

// https://www.w3.org/TR/filter-effects-1/#invertEquivalent
// https://www.w3.org/TR/filter-effects-1/#attr-valuedef-type-table
export function createInvertFilter(invert, outFilter = createCSSFilter()) {
const slope = 1 - 2 * invert;
const intercept = invert;
return createLinearFilter(slope, intercept, outFilter);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
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);
const expectedOutput = [color[0], color[1], 0, 1];
t.deepEqual(output, expectedOutput, '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
// This is because the CSS filters use uint8 instead of floating point numbers
if (Math.abs(error) > 7 / 255) {
t.fail('Error is too big');
}
}
}
t.end();
});
4 changes: 2 additions & 2 deletions Sources/Rendering/OpenGL/ImageCPRMapper/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ function vtkOpenGLImageCPRMapper(publicAPI, model) {
if (model.colorTextureString !== cfunToString) {
const cWidth = 1024;
const cSize = cWidth * textureHeight * 3;
const cTable = new Uint8Array(cSize);
const cTable = new Uint8ClampedArray(cSize);
let cfun = ppty.getRGBTransferFunction();
if (cfun) {
const tmpTable = new Float32Array(cWidth * 3);
Expand Down Expand Up @@ -287,7 +287,7 @@ function vtkOpenGLImageCPRMapper(publicAPI, model) {
if (model.pwfTextureString !== pwfunToString) {
const pwfWidth = 1024;
const pwfSize = pwfWidth * textureHeight;
const pwfTable = new Uint8Array(pwfSize);
const pwfTable = new Uint8ClampedArray(pwfSize);
let pwfun = ppty.getPiecewiseFunction();
// support case where pwfun is added/removed
model.pwfTexture.releaseGraphicsResources(model._openGLRenderWindow);
Expand Down
4 changes: 2 additions & 2 deletions Sources/Rendering/OpenGL/ImageMapper/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -937,7 +937,7 @@ function vtkOpenGLImageMapper(publicAPI, model) {
if (reBuildC) {
const cWidth = 1024;
const cSize = cWidth * textureHeight * 3;
const cTable = new Uint8Array(cSize);
const cTable = new Uint8ClampedArray(cSize);
if (!model.colorTexture) {
model.colorTexture = vtkOpenGLTexture.newInstance({
resizable: true,
Expand Down Expand Up @@ -1023,7 +1023,7 @@ function vtkOpenGLImageMapper(publicAPI, model) {
if (reBuildPwf) {
const pwfWidth = 1024;
const pwfSize = pwfWidth * textureHeight;
const pwfTable = new Uint8Array(pwfSize);
const pwfTable = new Uint8ClampedArray(pwfSize);
if (!model.pwfTexture) {
model.pwfTexture = vtkOpenGLTexture.newInstance({
resizable: true,
Expand Down
4 changes: 2 additions & 2 deletions Sources/Rendering/OpenGL/ImageResliceMapper/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) {
if (reBuildC) {
const cWidth = 1024;
const cSize = cWidth * textureHeight * 3;
const cTable = new Uint8Array(cSize);
const cTable = new Uint8ClampedArray(cSize);
if (!model.colorTexture) {
model.colorTexture = vtkOpenGLTexture.newInstance();
model.colorTexture.setOpenGLRenderWindow(model._openGLRenderWindow);
Expand Down Expand Up @@ -343,7 +343,7 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) {
if (reBuildPwf) {
const pwfWidth = 1024;
const pwfSize = pwfWidth * textureHeight;
const pwfTable = new Uint8Array(pwfSize);
const pwfTable = new Uint8ClampedArray(pwfSize);
if (!model.pwfTexture) {
model.pwfTexture = vtkOpenGLTexture.newInstance();
model.pwfTexture.setOpenGLRenderWindow(model._openGLRenderWindow);
Expand Down
Loading
Loading