diff --git a/examples/demo4.html b/examples/demo4.html new file mode 100644 index 0000000..ed242e2 --- /dev/null +++ b/examples/demo4.html @@ -0,0 +1,79 @@ + + + + + Nested transform tests + + + + + + +
+
+ + + + + \ No newline at end of file diff --git a/src/css/index.ts b/src/css/index.ts index 44b9714..7604a1b 100644 --- a/src/css/index.ts +++ b/src/css/index.ts @@ -75,6 +75,7 @@ import {counterIncrement} from './property-descriptors/counter-increment'; import {counterReset} from './property-descriptors/counter-reset'; import {quotes} from './property-descriptors/quotes'; import {boxShadow} from './property-descriptors/box-shadow'; +import {filter} from './property-descriptors/filter'; export class CSSParsedDeclaration { backgroundClip: ReturnType; @@ -103,6 +104,7 @@ export class CSSParsedDeclaration { boxShadow: ReturnType; color: Color; display: ReturnType; + filter: ReturnType; float: ReturnType; fontFamily: ReturnType; fontSize: LengthPercentage; @@ -168,6 +170,7 @@ export class CSSParsedDeclaration { this.boxShadow = parse(boxShadow, declaration.boxShadow); this.color = parse(color, declaration.color); this.display = parse(display, declaration.display); + this.filter = parse(filter, declaration.filter); this.float = parse(float, declaration.cssFloat); this.fontFamily = parse(fontFamily, declaration.fontFamily); this.fontSize = parse(fontSize, declaration.fontSize); diff --git a/src/css/property-descriptors/filter.ts b/src/css/property-descriptors/filter.ts new file mode 100644 index 0000000..08eb312 --- /dev/null +++ b/src/css/property-descriptors/filter.ts @@ -0,0 +1,68 @@ +import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor'; +import {CSSValue, isIdentWithValue, CSSFunction, isCSSFunction} from '../syntax/parser'; +import {isLength, Length} from '../types/length'; + +export interface Filter { + contrast: Length | null; + 'hue-rotate': Length | null; + grayscale: Length | null; + brightness: Length | null; + blur: Length | null; + invert: Length | null; + saturate: Length | null; + sepia: Length | null; +} + +export const filter: IPropertyListDescriptor = { + name: 'filter', + initialValue: 'none', + prefix: true, + type: PropertyDescriptorParsingType.LIST, + parse: (tokens: CSSValue[]) => { + if (tokens.length === 1 && isIdentWithValue(tokens[0], 'none')) { + return null; + } + + const filter: Filter = { + contrast: null, + 'hue-rotate': null, + grayscale: null, + brightness: null, + blur: null, + invert: null, + saturate: null, + sepia: null + }; + + let hasFilter: boolean = false; + + tokens.filter(isCSSFunction).forEach((token: CSSFunction) => { + switch (token.name) { + case 'contrast': + case 'hue-rotate': + case 'grayscale': + case 'brightness': + case 'blur': + case 'invert': + case 'saturate': + case 'sepia': + for (let index = 0; index < token.values.length; index++) { + const value: CSSValue = token.values[index]; + if (isLength(value)) { + hasFilter = true; + filter[token.name] = value; + } + } + break; + default: + break; + } + }); + + if (hasFilter) { + return filter; + } else { + return null; + } + } +}; diff --git a/src/css/property-descriptors/text-stroke-color.ts b/src/css/property-descriptors/text-stroke-color.ts index 2a5d410..ce4e0a9 100644 --- a/src/css/property-descriptors/text-stroke-color.ts +++ b/src/css/property-descriptors/text-stroke-color.ts @@ -1,9 +1,9 @@ import {IPropertyTypeValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor'; export const textStrokeColor: IPropertyTypeValueDescriptor = { - name: `-webkit-text-stroke-color`, + name: `text-stroke-color`, initialValue: 'transparent', - prefix: false, + prefix: true, type: PropertyDescriptorParsingType.TYPE_VALUE, format: 'color' }; diff --git a/src/css/property-descriptors/text-stroke-width.ts b/src/css/property-descriptors/text-stroke-width.ts index 2130296..a4861ec 100644 --- a/src/css/property-descriptors/text-stroke-width.ts +++ b/src/css/property-descriptors/text-stroke-width.ts @@ -1,8 +1,8 @@ import {IPropertyTypeValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor'; export const textStrokeWidth: IPropertyTypeValueDescriptor = { - name: '-webkit-text-stroke-width', + name: 'text-stroke-width', initialValue: '0', type: PropertyDescriptorParsingType.TYPE_VALUE, - prefix: false, + prefix: true, format: 'length-percentage' }; diff --git a/src/css/syntax/parser.ts b/src/css/syntax/parser.ts index 83b8a6c..0acc3bc 100644 --- a/src/css/syntax/parser.ts +++ b/src/css/syntax/parser.ts @@ -141,6 +141,7 @@ export class Parser { } } +export const isCSSFunction = (token: CSSValue): token is CSSFunction => token.type === TokenType.FUNCTION; export const isDimensionToken = (token: CSSValue): token is DimensionToken => token.type === TokenType.DIMENSION_TOKEN; export const isNumberToken = (token: CSSValue): token is NumberValueToken => token.type === TokenType.NUMBER_TOKEN; export const isIdentToken = (token: CSSValue): token is StringValueToken => token.type === TokenType.IDENT_TOKEN; diff --git a/src/css/types/color.ts b/src/css/types/color.ts index ebc17c1..1115973 100644 --- a/src/css/types/color.ts +++ b/src/css/types/color.ts @@ -302,3 +302,156 @@ export const COLORS: {[key: string]: Color} = { YELLOW: 0xffff00ff, YELLOWGREEN: 0x9acd32ff }; + +export interface RGBColor { + r: number; + g: number; + b: number; +} + +export const contrastRGB = (rgb: RGBColor, value: number): RGBColor => { + if (value < 0) value = 0; + else if (value > 1) value = 1; + return { + r: Math.max(0, Math.min(255, value * (rgb.r - 128) + 128)), + g: Math.max(0, Math.min(255, value * (rgb.g - 128) + 128)), + b: Math.max(0, Math.min(255, value * (rgb.b - 128) + 128)) + }; +}; + +export const grayscaleRGB = (rgb: RGBColor, value: number, mode?: string | null) => { + var gray = 0; + //different grayscale algorithms + switch (mode) { + case 'average': + gray = (rgb.r + rgb.g + rgb.b) / 3; + break; + case 'luma:BT601': + gray = rgb.r * 0.299 + rgb.g * 0.587 + rgb.b * 0.114; + break; + case 'desaturation': + gray = (Math.max(rgb.r, rgb.g, rgb.b) + Math.max(rgb.r, rgb.g, rgb.b)) / 2; + break; + case 'decompsition:max': + gray = Math.max(rgb.r, rgb.g, rgb.b); + break; + case 'decompsition:min': + gray = Math.min(rgb.r, rgb.g, rgb.b); + break; + case 'luma:BT709': + default: + gray = rgb.r * 0.2126 + rgb.g * 0.7152 + rgb.b * 0.0722; + break; + } + rgb.r = value * (gray - rgb.r) + rgb.r; + rgb.g = value * (gray - rgb.g) + rgb.g; + rgb.b = value * (gray - rgb.b) + rgb.b; + return rgb; +}; + +export const brightnessRGB = (rgb: RGBColor, value: number): RGBColor => { + if (value < 0) value = 0; + return { + r: Math.max(0, Math.min(255, rgb.r * value)), + g: Math.max(0, Math.min(255, rgb.g * value)), + b: Math.max(0, Math.min(255, rgb.b * value)) + }; +}; + +export const invertRGB = (rgb: RGBColor, value: number): RGBColor => { + return { + r: value * (255 - 2 * rgb.r) + rgb.r, + g: value * (255 - 2 * rgb.g) + rgb.g, + b: value * (255 - 2 * rgb.b) + rgb.b + }; +}; + +export const sepiaRGB = (rgb: RGBColor, value: number): RGBColor => { + if (value < 0) value = 0; + else if (value > 1) value = 1; + return { + r: value * Math.min(255, rgb.r * 0.393 + rgb.g * 0.769 + rgb.b * 0.189 - rgb.r) + rgb.r, + g: value * Math.min(255, rgb.r * 0.349 + rgb.g * 0.686 + rgb.b * 0.168 - rgb.g) + rgb.g, + b: value * Math.min(255, rgb.r * 0.272 + rgb.g * 0.534 + rgb.b * 0.131 - rgb.b) + rgb.b + }; +}; + +export const hueRotateRGB = (rgb: RGBColor, value: number): RGBColor => { + while (value < 0) value += 360; + while (value > 360) value -= 360; + rgb2hsl(rgb); + rgb.r += value; + if (rgb.r < 0) rgb.r += 360; + if (rgb.r > 359) rgb.r -= 360; + hsl2rgb(rgb); + return rgb; +}; + +export const saturateRGB = (rgb: RGBColor, value: number): RGBColor => { + if (value < 0) value = 0; + rgb2hsl(rgb); + rgb.g *= value; + if (rgb.g > 100) rgb.g = 100; + hsl2rgb(rgb); + return rgb; +}; + +function rgb2hsl(rgb: RGBColor) { + rgb.r = Math.max(0, Math.min(255, rgb.r)) / 255; + rgb.g = Math.max(0, Math.min(255, rgb.g)) / 255; + rgb.b = Math.max(0, Math.min(255, rgb.b)) / 255; + let h, l; + let M = Math.max(rgb.r, rgb.g, rgb.b); + let m = Math.min(rgb.r, rgb.g, rgb.b); + let d = M - m; + if (d == 0) h = 0; + else if (M == rgb.r) h = ((rgb.g - rgb.b) / d) % 6; + else if (M == rgb.g) h = (rgb.b - rgb.r) / d + 2; + else h = (rgb.r - rgb.g) / d + 4; + h = Math.round(h * 60); + if (h < 0) h += 360; + rgb.r = h; + l = (M + m) / 2; + if (d == 0) rgb.g = 0; + else rgb.g = (d / (1 - Math.abs(2 * l - 1))) * 100; + rgb.b = l * 100; +} + +function hsl2rgb(rgb: RGBColor) { + rgb.r = Math.max(0, Math.min(359, rgb.r)); + rgb.g = Math.max(0, Math.min(100, rgb.g)) / 100; + rgb.b = Math.max(0, Math.min(100, rgb.b)) / 100; + let C = (1 - Math.abs(2 * rgb.b - 1)) * rgb.g; + let h = rgb.r / 60; + let X = C * (1 - Math.abs((h % 2) - 1)); + let l = rgb.b; + rgb.r = 0; + rgb.g = 0; + rgb.b = 0; + if (h >= 0 && h < 1) { + rgb.r = C; + rgb.g = X; + } else if (h >= 1 && h < 2) { + rgb.r = X; + rgb.g = C; + } else if (h >= 2 && h < 3) { + rgb.g = C; + rgb.b = X; + } else if (h >= 3 && h < 4) { + rgb.g = X; + rgb.b = C; + } else if (h >= 4 && h < 5) { + rgb.r = X; + rgb.b = C; + } else { + rgb.r = C; + rgb.b = X; + } + let m = l - C / 2; + rgb.r += m; + rgb.g += m; + rgb.b += m; + rgb.r = Math.round(rgb.r * 255.0); + rgb.g = Math.round(rgb.g * 255.0); + rgb.b = Math.round(rgb.b * 255.0); +} diff --git a/src/css/types/functions/stackBlur.ts b/src/css/types/functions/stackBlur.ts new file mode 100644 index 0000000..20724dd --- /dev/null +++ b/src/css/types/functions/stackBlur.ts @@ -0,0 +1,1057 @@ +/* eslint-disable no-bitwise -- used for calculations */ +/* eslint-disable unicorn/prefer-query-selector -- aiming at + backward-compatibility */ +/** + * StackBlur - a fast almost Gaussian Blur For Canvas + * + * In case you find this class useful - especially in commercial projects - + * I am not totally unhappy for a small donation to my PayPal account + * mario@quasimondo.de + * + * Or support me on flattr: + * {@link https://flattr.com/thing/72791/StackBlur-a-fast-almost-Gaussian-Blur-Effect-for-CanvasJavascript}. + * + * @module StackBlur + * @author Mario Klingemann + * Contact: mario@quasimondo.com + * Website: {@link http://www.quasimondo.com/StackBlurForCanvas/StackBlurDemo.html} + * Twitter: @quasimondo + * + * @copyright (c) 2010 Mario Klingemann + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +const mulTable = [ + 512, + 512, + 456, + 512, + 328, + 456, + 335, + 512, + 405, + 328, + 271, + 456, + 388, + 335, + 292, + 512, + 454, + 405, + 364, + 328, + 298, + 271, + 496, + 456, + 420, + 388, + 360, + 335, + 312, + 292, + 273, + 512, + 482, + 454, + 428, + 405, + 383, + 364, + 345, + 328, + 312, + 298, + 284, + 271, + 259, + 496, + 475, + 456, + 437, + 420, + 404, + 388, + 374, + 360, + 347, + 335, + 323, + 312, + 302, + 292, + 282, + 273, + 265, + 512, + 497, + 482, + 468, + 454, + 441, + 428, + 417, + 405, + 394, + 383, + 373, + 364, + 354, + 345, + 337, + 328, + 320, + 312, + 305, + 298, + 291, + 284, + 278, + 271, + 265, + 259, + 507, + 496, + 485, + 475, + 465, + 456, + 446, + 437, + 428, + 420, + 412, + 404, + 396, + 388, + 381, + 374, + 367, + 360, + 354, + 347, + 341, + 335, + 329, + 323, + 318, + 312, + 307, + 302, + 297, + 292, + 287, + 282, + 278, + 273, + 269, + 265, + 261, + 512, + 505, + 497, + 489, + 482, + 475, + 468, + 461, + 454, + 447, + 441, + 435, + 428, + 422, + 417, + 411, + 405, + 399, + 394, + 389, + 383, + 378, + 373, + 368, + 364, + 359, + 354, + 350, + 345, + 341, + 337, + 332, + 328, + 324, + 320, + 316, + 312, + 309, + 305, + 301, + 298, + 294, + 291, + 287, + 284, + 281, + 278, + 274, + 271, + 268, + 265, + 262, + 259, + 257, + 507, + 501, + 496, + 491, + 485, + 480, + 475, + 470, + 465, + 460, + 456, + 451, + 446, + 442, + 437, + 433, + 428, + 424, + 420, + 416, + 412, + 408, + 404, + 400, + 396, + 392, + 388, + 385, + 381, + 377, + 374, + 370, + 367, + 363, + 360, + 357, + 354, + 350, + 347, + 344, + 341, + 338, + 335, + 332, + 329, + 326, + 323, + 320, + 318, + 315, + 312, + 310, + 307, + 304, + 302, + 299, + 297, + 294, + 292, + 289, + 287, + 285, + 282, + 280, + 278, + 275, + 273, + 271, + 269, + 267, + 265, + 263, + 261, + 259 +]; + +const shgTable = [ + 9, + 11, + 12, + 13, + 13, + 14, + 14, + 15, + 15, + 15, + 15, + 16, + 16, + 16, + 16, + 17, + 17, + 17, + 17, + 17, + 17, + 17, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 18, + 19, + 19, + 19, + 19, + 19, + 19, + 19, + 19, + 19, + 19, + 19, + 19, + 19, + 19, + 20, + 20, + 20, + 20, + 20, + 20, + 20, + 20, + 20, + 20, + 20, + 20, + 20, + 20, + 20, + 20, + 20, + 20, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 21, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 22, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 23, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24, + 24 +]; + +/** + * @param {ImageData} imageData + * @param {Integer} width + * @param {Integer} height + * @param {Float} radius + * @returns {ImageData} + */ +function processImageDataRGBA(imageData: ImageData, width: number, height: number, radius: number) { + const pixels = imageData.data; + + const div = 2 * radius + 1; + // const w4 = width << 2; + const widthMinus1 = width - 1; + const heightMinus1 = height - 1; + const radiusPlus1 = radius + 1; + const sumFactor = (radiusPlus1 * (radiusPlus1 + 1)) / 2; + + const stackStart: BlurStack = new BlurStack(); + let stack = stackStart; + let stackEnd; + for (let i = 1; i < div; i++) { + stack = stack.next = new BlurStack(); + if (i === radiusPlus1) { + stackEnd = stack; + } + } + stack.next = stackStart; + + let stackIn: BlurStack, + stackOut = null, + yw = 0, + yi = 0; + + const mulSum = mulTable[radius]; + const shgSum = shgTable[radius]; + + for (let y = 0; y < height; y++) { + stack = stackStart; + + const pr = pixels[yi], + pg = pixels[yi + 1], + pb = pixels[yi + 2], + pa = pixels[yi + 3]; + + for (let i = 0; i < radiusPlus1; i++) { + stack.r = pr; + stack.g = pg; + stack.b = pb; + stack.a = pa; + if (stack.next) stack = stack.next; + } + + let rInSum = 0, + gInSum = 0, + bInSum = 0, + aInSum = 0, + rOutSum = radiusPlus1 * pr, + gOutSum = radiusPlus1 * pg, + bOutSum = radiusPlus1 * pb, + aOutSum = radiusPlus1 * pa, + rSum = sumFactor * pr, + gSum = sumFactor * pg, + bSum = sumFactor * pb, + aSum = sumFactor * pa; + + for (let i = 1; i < radiusPlus1; i++) { + const p = yi + ((widthMinus1 < i ? widthMinus1 : i) << 2); + + const r = pixels[p], + g = pixels[p + 1], + b = pixels[p + 2], + a = pixels[p + 3]; + + const rbs = radiusPlus1 - i; + rSum += (stack.r = r) * rbs; + gSum += (stack.g = g) * rbs; + bSum += (stack.b = b) * rbs; + aSum += (stack.a = a) * rbs; + + rInSum += r; + gInSum += g; + bInSum += b; + aInSum += a; + + if (stack.next) stack = stack.next; + } + + stackIn = stackStart; + stackOut = stackEnd; + for (let x = 0; x < width; x++) { + const paInitial = (aSum * mulSum) >> shgSum; + pixels[yi + 3] = paInitial; + if (paInitial !== 0) { + const a = 255 / paInitial; + pixels[yi] = ((rSum * mulSum) >> shgSum) * a; + pixels[yi + 1] = ((gSum * mulSum) >> shgSum) * a; + pixels[yi + 2] = ((bSum * mulSum) >> shgSum) * a; + } else { + pixels[yi] = pixels[yi + 1] = pixels[yi + 2] = 0; + } + + rSum -= rOutSum; + gSum -= gOutSum; + bSum -= bOutSum; + aSum -= aOutSum; + + rOutSum -= stackIn.r; + gOutSum -= stackIn.g; + bOutSum -= stackIn.b; + aOutSum -= stackIn.a; + + let p = x + radius + 1; + p = (yw + (p < widthMinus1 ? p : widthMinus1)) << 2; + + rInSum += stackIn.r = pixels[p]; + gInSum += stackIn.g = pixels[p + 1]; + bInSum += stackIn.b = pixels[p + 2]; + aInSum += stackIn.a = pixels[p + 3]; + + rSum += rInSum; + gSum += gInSum; + bSum += bInSum; + aSum += aInSum; + + if (stackIn.next) stackIn = stackIn.next; + + if (stackOut) { + const {r, g, b, a} = stackOut; + + rOutSum += r; + gOutSum += g; + bOutSum += b; + aOutSum += a; + + rInSum -= r; + gInSum -= g; + bInSum -= b; + aInSum -= a; + + stackOut = stackOut.next; + } + + yi += 4; + } + yw += width; + } + + for (let x = 0; x < width; x++) { + yi = x << 2; + + let pr = pixels[yi], + pg = pixels[yi + 1], + pb = pixels[yi + 2], + pa = pixels[yi + 3], + rOutSum = radiusPlus1 * pr, + gOutSum = radiusPlus1 * pg, + bOutSum = radiusPlus1 * pb, + aOutSum = radiusPlus1 * pa, + rSum = sumFactor * pr, + gSum = sumFactor * pg, + bSum = sumFactor * pb, + aSum = sumFactor * pa; + + stack = stackStart; + + for (let i = 0; i < radiusPlus1; i++) { + stack.r = pr; + stack.g = pg; + stack.b = pb; + stack.a = pa; + if (stack.next) stack = stack.next; + } + + let yp = width; + + let gInSum = 0, + bInSum = 0, + aInSum = 0, + rInSum = 0; + for (let i = 1; i <= radius; i++) { + yi = (yp + x) << 2; + + const rbs = radiusPlus1 - i; + rSum += (stack.r = pr = pixels[yi]) * rbs; + gSum += (stack.g = pg = pixels[yi + 1]) * rbs; + bSum += (stack.b = pb = pixels[yi + 2]) * rbs; + aSum += (stack.a = pa = pixels[yi + 3]) * rbs; + + rInSum += pr; + gInSum += pg; + bInSum += pb; + aInSum += pa; + + if (stack.next) stack = stack.next; + + if (i < heightMinus1) { + yp += width; + } + } + + yi = x; + stackIn = stackStart; + stackOut = stackEnd; + for (let y = 0; y < height; y++) { + let p = yi << 2; + pixels[p + 3] = pa = (aSum * mulSum) >> shgSum; + if (pa > 0) { + pa = 255 / pa; + pixels[p] = ((rSum * mulSum) >> shgSum) * pa; + pixels[p + 1] = ((gSum * mulSum) >> shgSum) * pa; + pixels[p + 2] = ((bSum * mulSum) >> shgSum) * pa; + } else { + pixels[p] = pixels[p + 1] = pixels[p + 2] = 0; + } + + rSum -= rOutSum; + gSum -= gOutSum; + bSum -= bOutSum; + aSum -= aOutSum; + + rOutSum -= stackIn.r; + gOutSum -= stackIn.g; + bOutSum -= stackIn.b; + aOutSum -= stackIn.a; + + p = (x + ((p = y + radiusPlus1) < heightMinus1 ? p : heightMinus1) * width) << 2; + + rSum += rInSum += stackIn.r = pixels[p]; + gSum += gInSum += stackIn.g = pixels[p + 1]; + bSum += bInSum += stackIn.b = pixels[p + 2]; + aSum += aInSum += stackIn.a = pixels[p + 3]; + + if (stackIn.next) stackIn = stackIn.next; + + if (stackOut) { + rOutSum += pr = stackOut.r; + gOutSum += pg = stackOut.g; + bOutSum += pb = stackOut.b; + aOutSum += pa = stackOut.a; + + rInSum -= pr; + gInSum -= pg; + bInSum -= pb; + aInSum -= pa; + + stackOut = stackOut.next; + } + + yi += width; + } + } + return imageData; +} + +/** + * @param {ImageData} imageData + * @param {Integer} topX + * @param {Integer} topY + * @param {Integer} width + * @param {Integer} height + * @param {Float} radius + * @returns {ImageData} + */ +function processImageDataRGB(imageData: ImageData, width: number, height: number, radius: number) { + const pixels = imageData.data; + + const div = 2 * radius + 1; + // const w4 = width << 2; + const widthMinus1 = width - 1; + const heightMinus1 = height - 1; + const radiusPlus1 = radius + 1; + const sumFactor = (radiusPlus1 * (radiusPlus1 + 1)) / 2; + + const stackStart = new BlurStack(); + let stack = stackStart; + let stackEnd; + for (let i = 1; i < div; i++) { + stack = stack.next = new BlurStack(); + if (i === radiusPlus1) { + stackEnd = stack; + } + } + stack.next = stackStart; + let stackIn = null; + let stackOut = null; + + const mulSum = mulTable[radius]; + const shgSum = shgTable[radius]; + + let p, rbs; + let yw = 0, + yi = 0; + + for (let y = 0; y < height; y++) { + let pr = pixels[yi], + pg = pixels[yi + 1], + pb = pixels[yi + 2], + rOutSum = radiusPlus1 * pr, + gOutSum = radiusPlus1 * pg, + bOutSum = radiusPlus1 * pb, + rSum = sumFactor * pr, + gSum = sumFactor * pg, + bSum = sumFactor * pb; + + stack = stackStart; + + for (let i = 0; i < radiusPlus1; i++) { + stack.r = pr; + stack.g = pg; + stack.b = pb; + if (stack.next) stack = stack.next; + } + + let rInSum = 0, + gInSum = 0, + bInSum = 0; + for (let i = 1; i < radiusPlus1; i++) { + p = yi + ((widthMinus1 < i ? widthMinus1 : i) << 2); + rSum += (stack.r = pr = pixels[p]) * (rbs = radiusPlus1 - i); + gSum += (stack.g = pg = pixels[p + 1]) * rbs; + bSum += (stack.b = pb = pixels[p + 2]) * rbs; + + rInSum += pr; + gInSum += pg; + bInSum += pb; + + if (stack.next) stack = stack.next; + } + + stackIn = stackStart; + stackOut = stackEnd; + for (let x = 0; x < width; x++) { + pixels[yi] = (rSum * mulSum) >> shgSum; + pixels[yi + 1] = (gSum * mulSum) >> shgSum; + pixels[yi + 2] = (bSum * mulSum) >> shgSum; + + rSum -= rOutSum; + gSum -= gOutSum; + bSum -= bOutSum; + + if (stackIn && stackOut) { + rOutSum -= stackIn.r; + gOutSum -= stackIn.g; + bOutSum -= stackIn.b; + + p = (yw + ((p = x + radius + 1) < widthMinus1 ? p : widthMinus1)) << 2; + + rInSum += stackIn.r = pixels[p]; + gInSum += stackIn.g = pixels[p + 1]; + bInSum += stackIn.b = pixels[p + 2]; + + rSum += rInSum; + gSum += gInSum; + bSum += bInSum; + + stackIn = stackIn.next; + + rOutSum += pr = stackOut.r; + gOutSum += pg = stackOut.g; + bOutSum += pb = stackOut.b; + + rInSum -= pr; + gInSum -= pg; + bInSum -= pb; + + stackOut = stackOut.next; + } + + yi += 4; + } + yw += width; + } + + for (let x = 0; x < width; x++) { + yi = x << 2; + let pr = pixels[yi], + pg = pixels[yi + 1], + pb = pixels[yi + 2], + rOutSum = radiusPlus1 * pr, + gOutSum = radiusPlus1 * pg, + bOutSum = radiusPlus1 * pb, + rSum = sumFactor * pr, + gSum = sumFactor * pg, + bSum = sumFactor * pb; + + stack = stackStart; + + for (let i = 0; i < radiusPlus1; i++) { + stack.r = pr; + stack.g = pg; + stack.b = pb; + if (stack.next) stack = stack.next; + } + + let rInSum = 0, + gInSum = 0, + bInSum = 0; + for (let i = 1, yp = width; i <= radius; i++) { + yi = (yp + x) << 2; + + rSum += (stack.r = pr = pixels[yi]) * (rbs = radiusPlus1 - i); + gSum += (stack.g = pg = pixels[yi + 1]) * rbs; + bSum += (stack.b = pb = pixels[yi + 2]) * rbs; + + rInSum += pr; + gInSum += pg; + bInSum += pb; + + if (stack.next) stack = stack.next; + + if (i < heightMinus1) { + yp += width; + } + } + + yi = x; + stackIn = stackStart; + stackOut = stackEnd; + for (let y = 0; y < height; y++) { + p = yi << 2; + pixels[p] = (rSum * mulSum) >> shgSum; + pixels[p + 1] = (gSum * mulSum) >> shgSum; + pixels[p + 2] = (bSum * mulSum) >> shgSum; + + rSum -= rOutSum; + gSum -= gOutSum; + bSum -= bOutSum; + + if (stackIn && stackOut) { + rOutSum -= stackIn.r; + gOutSum -= stackIn.g; + bOutSum -= stackIn.b; + + p = (x + ((p = y + radiusPlus1) < heightMinus1 ? p : heightMinus1) * width) << 2; + + rSum += rInSum += stackIn.r = pixels[p]; + gSum += gInSum += stackIn.g = pixels[p + 1]; + bSum += bInSum += stackIn.b = pixels[p + 2]; + + stackIn = stackIn.next; + + rOutSum += pr = stackOut.r; + gOutSum += pg = stackOut.g; + bOutSum += pb = stackOut.b; + + rInSum -= pr; + gInSum -= pg; + bInSum -= pb; + + stackOut = stackOut.next; + } + + yi += width; + } + } + + return imageData; +} + +/** + * + */ +export class BlurStack { + /** + * Set properties. + */ + r: number; + g: number; + b: number; + a: number; + next?: BlurStack; + + constructor() { + this.r = 0; + this.g = 0; + this.b = 0; + this.a = 0; + } +} + +export const stackBlurImage = ( + imageData: ImageData, + width: number, + height: number, + radius: number, + blurAlphaChannel: number +): ImageData => { + if (isNaN(radius) || radius < 1) { + return imageData; + } + radius |= 0; + + if (blurAlphaChannel) { + return processImageDataRGBA(imageData, width, height, radius); + } else { + return processImageDataRGB(imageData, width, height, radius); + } +}; diff --git a/src/render/canvas/canvas-renderer.ts b/src/render/canvas/canvas-renderer.ts index 3430e3a..2ae6851 100644 --- a/src/render/canvas/canvas-renderer.ts +++ b/src/render/canvas/canvas-renderer.ts @@ -1,5 +1,17 @@ import {ElementPaint, parseStackingContexts, StackingContext} from '../stacking-context'; -import {asString, Color, isTransparent} from '../../css/types/color'; +import { + asString, + Color, + isTransparent, + RGBColor, + contrastRGB, + hueRotateRGB, + grayscaleRGB, + brightnessRGB, + invertRGB, + saturateRGB, + sepiaRGB +} from '../../css/types/color'; import {Logger} from '../../core/logger'; import {ElementContainer} from '../../dom/element-container'; import {BORDER_STYLE} from '../../css/property-descriptors/border-style'; @@ -38,6 +50,8 @@ import {TextareaElementContainer} from '../../dom/elements/textarea-element-cont import {SelectElementContainer} from '../../dom/elements/select-element-container'; import {IFrameElementContainer} from '../../dom/replaced-elements/iframe-element-container'; import {TextShadow} from '../../css/property-descriptors/text-shadow'; +import {Filter} from '../../css/property-descriptors/filter'; +import {stackBlurImage} from '../../css/types/functions/stackBlur'; export type RenderConfigurations = RenderOptions & { backgroundColor: Color | null; @@ -248,7 +262,8 @@ export class CanvasRenderer { renderReplacedElement( container: ReplacedElementContainer, curves: BoundCurves, - image: HTMLImageElement | HTMLCanvasElement + image: HTMLImageElement | HTMLCanvasElement, + filter?: Filter | null | undefined ) { if (image && container.intrinsicWidth > 0 && container.intrinsicHeight > 0) { const box = contentBox(container); @@ -267,6 +282,49 @@ export class CanvasRenderer { box.width, box.height ); + if (filter) { + try { + let imageData = this.ctx.getImageData(box.left, box.top, box.width, box.height); + for (let _j = 0; _j < imageData.height; _j++) { + for (let _i = 0; _i < imageData.width; _i++) { + let index = _j * 4 * imageData.width + _i * 4; + let rgb: RGBColor = { + r: imageData.data[index], + g: imageData.data[index + 1], + b: imageData.data[index + 2] + }; + if (filter.contrast) { + rgb = contrastRGB(rgb, filter.contrast.number); + } + if (filter['hue-rotate']) { + rgb = hueRotateRGB(rgb, filter['hue-rotate'].number); + } + if (filter.grayscale) { + rgb = grayscaleRGB(rgb, filter['grayscale'].number, 'luma:BT709'); + } + if (filter.brightness) { + rgb = brightnessRGB(rgb, filter['brightness'].number); + } + if (filter.invert) { + rgb = invertRGB(rgb, filter['invert'].number); + } + if (filter.saturate) { + rgb = saturateRGB(rgb, filter.saturate.number); + } + if (filter.sepia) { + rgb = sepiaRGB(rgb, filter.sepia.number); + } + imageData.data[index] = rgb.r; + imageData.data[index + 1] = rgb.g; + imageData.data[index + 2] = rgb.b; + } + } + if (filter.blur) { + imageData = stackBlurImage(imageData, box.width, box.height, filter.blur.number * 2.2, 1); + } + this.ctx.putImageData(imageData, box.left, box.top); + } catch (error) {} + } this.ctx.restore(); } } @@ -283,7 +341,7 @@ export class CanvasRenderer { if (container instanceof ImageElementContainer) { try { const image = await this.options.cache.match(container.src); - this.renderReplacedElement(container, curves, image); + this.renderReplacedElement(container, curves, image, styles.filter); } catch (e) { Logger.getInstance(this.options.id).error(`Error loading image ${container.src}`); }