Skip to content

Commit

Permalink
feat(CSSHelper): Add CSS filter utilities
Browse files Browse the repository at this point in the history
  • Loading branch information
bruyeret committed Feb 1, 2024
1 parent 0770930 commit 370dbf7
Show file tree
Hide file tree
Showing 4 changed files with 210 additions and 7 deletions.
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} out_rowA_colB
*/
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, out_rowA_colB: Matrix): void;

/**
* Transpose a 3x3 matrix.
Expand Down
17 changes: 12 additions & 5 deletions Sources/Common/Core/Math/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -609,24 +609,31 @@ 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, out_rowA_colB) {
// we need colA == rowB
if (colA !== rowB) {
vtkErrorMacro('Number of columns of A must match number of rows of B.');
}

// If a or b is used to store the result, copying them is required
const copyA = [...a];
const copyB = [...b];
const copyA =
a === out_rowA_colB || (a.buffer && a.buffer === out_rowA_colB.buffer)
? [...a]
: a;
const copyB =
b === out_rowA_colB || (b.buffer && b.buffer === out_rowA_colB.buffer)
? [...b]
: b;
// output matrix is rowA*colB
// output row
for (let i = 0; i < rowA; i++) {
// output col
for (let j = 0; j < colB; j++) {
out_rowXcol[i * colB + j] = 0;
out_rowA_colB[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];
out_rowA_colB[i * colB + j] +=
copyA[i * colA + k] * copyB[j + colB * k];
}
}
}
Expand Down
105 changes: 105 additions & 0 deletions Sources/Rendering/Core/ColorTransferFunction/CssFilters.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* 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 getNewFilter(): 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 getIdentityFilter(outFilter?: FilterMatrix): FilterMatrix;

/**
* Combine two filters into a single filter
* The order 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
* @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 getLinearFilter(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 getContrastFilter(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 getSaturateFilter(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 getBrightnessFilter(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 getInvertFilter(invert: number, outFilter?: FilterMatrix): FilterMatrix;
91 changes: 91 additions & 0 deletions Sources/Rendering/Core/ColorTransferFunction/CssFilters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* 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 getNewFilter() {
return new Array(25);
}

export function getIdentityFilter(outFilter = getNewFilter()) {
return identity(5, outFilter);
}

export function combineFilters(
baseFilter,
newFilter,
outFilter = getNewFilter()
) {
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);
return vec.slice(0, 4);
}

export function getLinearFilter(slope, intercept, outFilter = getNewFilter()) {
getIdentityFilter(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 getContrastFilter(contrast, outFilter = getNewFilter()) {
const slope = contrast;
const intercept = -(0.5 * contrast) + 0.5;
return getLinearFilter(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 getSaturateFilter(saturate, outFilter = getNewFilter()) {
getIdentityFilter(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 getBrightnessFilter(brightness, outFilter = getNewFilter()) {
const slope = brightness;
const intercept = 0;
return getLinearFilter(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 getInvertFilter(invert, outFilter = getNewFilter()) {
const slope = 1 - 2 * invert;
const intercept = invert;
return getLinearFilter(slope, intercept, outFilter);
}

0 comments on commit 370dbf7

Please sign in to comment.