diff --git a/Sources/Common/Core/Math/index.d.ts b/Sources/Common/Core/Math/index.d.ts index 95e7dea8b8a..e8ed0daa4ba 100755 --- a/Sources/Common/Core/Math/index.d.ts +++ b/Sources/Common/Core/Math/index.d.ts @@ -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. diff --git a/Sources/Common/Core/Math/index.js b/Sources/Common/Core/Math/index.js index 31055a0c2eb..88c8f7f8aa1 100644 --- a/Sources/Common/Core/Math/index.js +++ b/Sources/Common/Core/Math/index.js @@ -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]; } } } diff --git a/Sources/Rendering/Core/ColorTransferFunction/CssFilters.d.ts b/Sources/Rendering/Core/ColorTransferFunction/CssFilters.d.ts new file mode 100644 index 00000000000..540c8a89a20 --- /dev/null +++ b/Sources/Rendering/Core/ColorTransferFunction/CssFilters.d.ts @@ -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; diff --git a/Sources/Rendering/Core/ColorTransferFunction/CssFilters.js b/Sources/Rendering/Core/ColorTransferFunction/CssFilters.js new file mode 100644 index 00000000000..5d3fbe1e83b --- /dev/null +++ b/Sources/Rendering/Core/ColorTransferFunction/CssFilters.js @@ -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); +}