Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master'
Browse files Browse the repository at this point in the history
  • Loading branch information
zepumph committed Nov 6, 2020
2 parents b05bd60 + 029a883 commit a316047
Show file tree
Hide file tree
Showing 15 changed files with 314 additions and 21 deletions.
5 changes: 4 additions & 1 deletion js/display/CanvasBlock.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Matrix3 from '../../../dot/js/Matrix3.js';
import Vector2 from '../../../dot/js/Vector2.js';
import Poolable from '../../../phet-core/js/Poolable.js';
import cleanArray from '../../../phet-core/js/cleanArray.js';
import platform from '../../../phet-core/js/platform.js';
import scenery from '../scenery.js';
import CanvasContextWrapper from '../util/CanvasContextWrapper.js';
import Features from '../util/Features.js';
Expand Down Expand Up @@ -323,7 +324,9 @@ class CanvasBlock extends FittedBlock {
bottomWrapper.context.setTransform( 1, 0, 0, 1, 0, 0 );

const filters = node._filters;
let canUseInternalFilter = Features.canvasFilter;
// We need to fall back to a different filter behavior with Chrome, since it over-darkens otherwise with the
// built-in feature.
let canUseInternalFilter = Features.canvasFilter && !platform.chromium;
for ( let j = 0; j < filters.length; j++ ) {
// If we use context.filter, it's equivalent to checking DOM compatibility on all of them.
canUseInternalFilter = canUseInternalFilter && filters[ j ].isDOMCompatible();
Expand Down
23 changes: 22 additions & 1 deletion js/nodes/Node.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ import arrayDifference from '../../../phet-core/js/arrayDifference.js';
import deprecationWarning from '../../../phet-core/js/deprecationWarning.js';
import inherit from '../../../phet-core/js/inherit.js';
import merge from '../../../phet-core/js/merge.js';
import platform from '../../../phet-core/js/platform.js';
import PhetioObject from '../../../tandem/js/PhetioObject.js';
import BooleanIO from '../../../tandem/js/types/BooleanIO.js';
import IOType from '../../../tandem/js/types/IOType.js';
Expand Down Expand Up @@ -3306,12 +3307,32 @@ inherit( PhetioObject, Node, {
* Sets the non-opacity filters for this Node.
* @public
*
* The default is an empty array (no filters). It should be an array of Filter objects, which will be effectively
* applied in-order on this Node (and its subtree), and will be applied BEFORE opacity/clipping.
*
* NOTE: Some filters may decrease performance (and this may be platform-specific). Please read documentation for each
* filter before using.
*
* Typical filter types to use are:
* - Brightness
* - Contrast
* - DropShadow (EXPERIMENTAL)
* - GaussianBlur (EXPERIMENTAL)
* - Grayscale (Grayscale.FULL for the full effect)
* - HueRotate
* - Invert (Invert.FULL for the full effect)
* - Saturate
* - Sepia (Sepia.FULL for the full effect)
*
* Filter.js has more information in general on filters.
*
* @param {Array.<Filter>} filters
*/
setFilters: function( filters ) {
assert && assert( Array.isArray( filters ), 'filters should be an array' );
assert && assert( _.every( filters, filter => filter instanceof Filter ), 'filters should consist of Filter objects only' );

// We re-use the same array internally, so we don't reference a potentially-mutable array from outside.
this._filters.length = 0;
this._filters.push( ...filters );

Expand Down Expand Up @@ -4421,7 +4442,7 @@ inherit( PhetioObject, Node, {

if ( child._filters.length ) {
// Filters shouldn't be too often, so less concerned about the GC here (and this is so much easier to read).
if ( Features.canvasFilter && _.every( child._filters, filter => filter.isDOMCompatible() ) ) {
if ( Features.canvasFilter && !platform.chromium && _.every( child._filters, filter => filter.isDOMCompatible() ) ) {
wrapper.context.filter = child._filters.map( filter => filter.getCSSFilterString() ).join( ' ' );
}
else {
Expand Down
5 changes: 4 additions & 1 deletion js/util/Brightness.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import ColorMatrixFilter from './ColorMatrixFilter.js';

class Brightness extends ColorMatrixFilter {
/**
* @param {number} amount
* @param {number} amount - How bright to be, from 0 (dark), 1 (normal), or larger values to brighten
*/
constructor( amount ) {
assert && assert( typeof amount === 'number', 'Brightness amount should be a number' );
Expand Down Expand Up @@ -54,5 +54,8 @@ class Brightness extends ColorMatrixFilter {
}
}

// @public {Brightness} - Fully darkens the content
Brightness.BLACKEN = new Brightness( 0 );

scenery.register( 'Brightness', Brightness );
export default Brightness;
57 changes: 46 additions & 11 deletions js/util/ColorMatrixFilter.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,33 @@
*/

import toSVGNumber from '../../../dot/js/toSVGNumber.js';
import platform from '../../../phet-core/js/platform.js';
import scenery from '../scenery.js';
import CanvasContextWrapper from './CanvasContextWrapper.js';
import Filter from './Filter.js';
import Utils from './Utils.js';

const isImageDataSupported = Utils.supportsImageDataCanvasFilter();
const useFakeGamma = platform.chromium;

class ColorMatrixFilter extends Filter {
/**
* NOTE: It is possible but not generally recommended to create custom ColorMatrixFilter types. They should be
* compatible with Canvas and SVG, HOWEVER any WebGL/DOM content cannot work with those custom filters, and any
* combination of multiple SVG or Canvas elements will ALSO not work (since there is no CSS filter function that can
* do arbitrary color matrix operations). This means that performance will likely be reduced UNLESS all content is
* within a single SVG block.
*
* Please prefer the named subtypes where possible.
*
* The resulting color is the result of the matrix multiplication:
*
* [ m00 m01 m02 m03 m04 ] [ r ]
* [ m10 m11 m12 m13 m14 ] [ g ]
* [ m20 m21 m22 m23 m24 ] * [ b ]
* [ m30 m31 m32 m33 m34 ] [ a ]
* [ 1 ]
*
* @param {number} m00
* @param {number} m01
* @param {number} m02
Expand All @@ -40,7 +58,7 @@ class ColorMatrixFilter extends Filter {
constructor( m00, m01, m02, m03, m04,
m10, m11, m12, m13, m14,
m20, m21, m22, m23, m24,
m30, m31, m32, m33, m34 ) {
m30, m31, m32, m33, m34 ) {

assert && assert( typeof m00 === 'number' && isFinite( m00 ), 'm00 should be a finite number' );
assert && assert( typeof m01 === 'number' && isFinite( m01 ), 'm01 should be a finite number' );
Expand Down Expand Up @@ -134,16 +152,33 @@ class ColorMatrixFilter extends Filter {
for ( let i = 0; i < size; i++ ) {
const index = i * 4;

const r = imageData.data[ index + 0 ];
const g = imageData.data[ index + 1 ];
const b = imageData.data[ index + 2 ];
const a = imageData.data[ index + 3 ];

// Clamp/round should be done by the UInt8Array, we don't do it here for performance reasons.
imageData.data[ index + 0 ] = r * this.m00 + g * this.m01 + b * this.m02 + a * this.m03 + this.m04;
imageData.data[ index + 1 ] = r * this.m10 + g * this.m11 + b * this.m12 + a * this.m13 + this.m14;
imageData.data[ index + 2 ] = r * this.m20 + g * this.m21 + b * this.m22 + a * this.m23 + this.m24;
imageData.data[ index + 3 ] = r * this.m30 + g * this.m31 + b * this.m32 + a * this.m33 + this.m34;
if ( useFakeGamma ) {
// Gamma-corrected version, which seems to match SVG/DOM
// Eek, this seems required for chromium Canvas to have a standard behavior?
const gamma = 1.45;
const r = Math.pow( imageData.data[ index + 0 ] / 255, gamma );
const g = Math.pow( imageData.data[ index + 1 ] / 255, gamma );
const b = Math.pow( imageData.data[ index + 2 ] / 255, gamma );
const a = Math.pow( imageData.data[ index + 3 ] / 255, gamma );

// Clamp/round should be done by the UInt8Array, we don't do it here for performance reasons.
imageData.data[ index + 0 ] = 255 * Math.pow( r * this.m00 + g * this.m01 + b * this.m02 + a * this.m03 + this.m04, 1 / gamma );
imageData.data[ index + 1 ] = 255 * Math.pow( r * this.m10 + g * this.m11 + b * this.m12 + a * this.m13 + this.m14, 1 / gamma );
imageData.data[ index + 2 ] = 255 * Math.pow( r * this.m20 + g * this.m21 + b * this.m22 + a * this.m23 + this.m24, 1 / gamma );
imageData.data[ index + 3 ] = 255 * Math.pow( r * this.m30 + g * this.m31 + b * this.m32 + a * this.m33 + this.m34, 1 / gamma );
}
else {
const r = imageData.data[ index + 0 ];
const g = imageData.data[ index + 1 ];
const b = imageData.data[ index + 2 ];
const a = imageData.data[ index + 3 ];

// Clamp/round should be done by the UInt8Array, we don't do it here for performance reasons.
imageData.data[ index + 0 ] = r * this.m00 + g * this.m01 + b * this.m02 + a * this.m03 + this.m04;
imageData.data[ index + 1 ] = r * this.m10 + g * this.m11 + b * this.m12 + a * this.m13 + this.m14;
imageData.data[ index + 2 ] = r * this.m20 + g * this.m21 + b * this.m22 + a * this.m23 + this.m24;
imageData.data[ index + 3 ] = r * this.m30 + g * this.m31 + b * this.m32 + a * this.m33 + this.m34;
}
}

wrapper.context.putImageData( imageData, 0, 0 );
Expand Down
5 changes: 4 additions & 1 deletion js/util/Contrast.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import ColorMatrixFilter from './ColorMatrixFilter.js';

class Contrast extends ColorMatrixFilter {
/**
* @param {number} amount
* @param {number} amount - The amount of the effect, from 0 (gray), 1 (normal), or above for high-contrast
*/
constructor( amount ) {
assert && assert( typeof amount === 'number', 'Contrast amount should be a number' );
Expand Down Expand Up @@ -54,5 +54,8 @@ class Contrast extends ColorMatrixFilter {
}
}

// @public {Contrast} - Turns the content gray
Contrast.GRAY = new Contrast( 0 );

scenery.register( 'Contrast', Contrast );
export default Contrast;
2 changes: 2 additions & 0 deletions js/util/DropShadow.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
/**
* DropShadow filter
*
* EXPERIMENTAL! DO not use in production code yet
*
* TODO: preventFit OR handle bounds increase (or both)
*
* @author Jonathan Olson <[email protected]>
Expand Down
28 changes: 28 additions & 0 deletions js/util/Filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,27 @@
/**
* Base type for filters
*
* Filters have different ways of being applied, depending on what the platform supports AND what content is below.
* These different ways have potentially different performance characteristics, and potentially quality differences.
*
* The current ways are:
* - DOM element with CSS filter specified (can include mixed content and WebGL underneath, and this is used as a
* general fallback). NOTE: General color matrix support is NOT provided under this, we only have specific named
* filters that can be used.
* - SVG filter elements (which are very flexible, a combination of filters may be combined into SVG filter elements).
* This only works if ALL of the content under the filter(s) can be placed in one SVG element, so a layerSplit or
* non-SVG content can prevent this from being used.
* - Canvas filter attribute (similar to DOM CSS). Similar to DOM CSS, but not as accelerated (requires applying the
* filter by drawing into another Canvas). Chromium-based browsers seem to have issues with the color space used,
* so this can't be used on that platform. Additionally, this only works if ALL the content under the filter(s) can
* be placed in one Canvas, so a layerSplit or non-SVG content can prevent this from being used.
* - Canvas ImageData. This is a fallback where we directly get, manipulate, and set pixel data in a Canvas (with the
* corresponding performance hit that it takes to CPU-process every pixel). Additionally, this only works if ALL the
* content under the filter(s) can be placed in one Canvas, so a layerSplit or non-SVG content can prevent this from
* being used.
*
* Some filters may have slightly different appearances depending on the browser/platform/renderer.
*
* @author Jonathan Olson <[email protected]>
*/

Expand Down Expand Up @@ -113,6 +134,7 @@ class Filter {
}

/**
* Applies a color matrix effect into an existing SVG filter.
* @public
*
* @param {string} matrixValues
Expand All @@ -122,9 +144,15 @@ class Filter {
*/
static applyColorMatrix( matrixValues, svgFilter, inName, resultName ) {
const feColorMatrix = document.createElementNS( svgns, 'feColorMatrix' );

feColorMatrix.setAttribute( 'type', 'matrix' );
feColorMatrix.setAttribute( 'values', matrixValues );
feColorMatrix.setAttribute( 'in', inName );

// Since the DOM effects are done with sRGB and we can't manipulate that, we'll instead adjust SVG to apply the
// effects in sRGB so that we have consistency
feColorMatrix.setAttribute( 'color-interpolation-filters', 'sRGB' );

if ( resultName ) {
feColorMatrix.setAttribute( 'result', resultName );
}
Expand Down
2 changes: 2 additions & 0 deletions js/util/GaussianBlur.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
/**
* GaussianBlur filter
*
* EXPERIMENTAL! DO not use in production code yet
*
* TODO: preventFit OR handle bounds increase (or both)
*
* @author Jonathan Olson <[email protected]>
Expand Down
7 changes: 6 additions & 1 deletion js/util/Grayscale.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import ColorMatrixFilter from './ColorMatrixFilter.js';

class Grayscale extends ColorMatrixFilter {
/**
* @param {number} [amount]
* @param {number} [amount] - The amount of the effect, from 0 (none) to 1 (full)
*/
constructor( amount = 1 ) {
assert && assert( typeof amount === 'number', 'Grayscale amount should be a number' );
Expand All @@ -22,6 +22,11 @@ class Grayscale extends ColorMatrixFilter {

const n = 1 - amount;

// https://drafts.fxtf.org/filter-effects/#grayscaleEquivalent
// (0.2126 + 0.7874 * [1 - amount]) (0.7152 - 0.7152 * [1 - amount]) (0.0722 - 0.0722 * [1 - amount]) 0 0
// (0.2126 - 0.2126 * [1 - amount]) (0.7152 + 0.2848 * [1 - amount]) (0.0722 - 0.0722 * [1 - amount]) 0 0
// (0.2126 - 0.2126 * [1 - amount]) (0.7152 - 0.7152 * [1 - amount]) (0.0722 + 0.9278 * [1 - amount]) 0 0
// 0 0 0 1 0
super(
0.2126 + 0.7874 * n, 0.7152 - 0.7152 * n, 0.0722 - 0.0722 * n, 0, 0,
0.2126 - 0.2126 * n, 0.7152 + 0.2848 * n, 0.0722 - 0.0722 * n, 0, 0,
Expand Down
2 changes: 1 addition & 1 deletion js/util/HueRotate.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import ColorMatrixFilter from './ColorMatrixFilter.js';

class HueRotate extends ColorMatrixFilter {
/**
* @param {number} amount - In radians
* @param {number} amount - In radians, the amount of hue to color-shift
*/
constructor( amount ) {
assert && assert( typeof amount === 'number', 'HueRotate amount should be a number' );
Expand Down
5 changes: 4 additions & 1 deletion js/util/Invert.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import Filter from './Filter.js';

class Invert extends Filter {
/**
* @param {number} [amount]
* @param {number} [amount] - The amount of the effect, from 0 (none) to 1 (full)
*/
constructor( amount = 1 ) {
assert && assert( typeof amount === 'number', 'Invert amount should be a number' );
Expand Down Expand Up @@ -50,5 +50,8 @@ class Invert extends Filter {
}
}

// @public {Invert}
Invert.FULL = new Invert( 1 );

scenery.register( 'Invert', Invert );
export default Invert;
4 changes: 3 additions & 1 deletion js/util/Opacity.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import Filter from './Filter.js';

class Opacity extends Filter {
/**
* @param {number} amount
* NOTE: Generally prefer setting a Node's opacity, unless this is required for stacking of filters.
*
* @param {number} amount - The amount of opacity, from 0 (invisible) to 1 (fully visible)
*/
constructor( amount ) {
assert && assert( typeof amount === 'number', 'Opacity amount should be a number' );
Expand Down
2 changes: 1 addition & 1 deletion js/util/Saturate.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import ColorMatrixFilter from './ColorMatrixFilter.js';

class Saturate extends ColorMatrixFilter {
/**
* @param {number} amount
* @param {number} amount - The amount of the effect, from 0 (no saturation), 1 (normal), or higher to over-saturate
*/
constructor( amount ) {
assert && assert( typeof amount === 'number', 'Saturate amount should be a number' );
Expand Down
5 changes: 4 additions & 1 deletion js/util/Sepia.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import ColorMatrixFilter from './ColorMatrixFilter.js';

class Sepia extends ColorMatrixFilter {
/**
* @param {number} [amount]
* @param {number} [amount] - The amount of the effect, from 0 (none) to 1 (full sepia)
*/
constructor( amount = 1 ) {
assert && assert( typeof amount === 'number', 'Sepia amount should be a number' );
Expand Down Expand Up @@ -55,5 +55,8 @@ class Sepia extends ColorMatrixFilter {
}
}

// @public {Sepia}
Sepia.FULL = new Sepia( 1 );

scenery.register( 'Sepia', Sepia );
export default Sepia;
Loading

0 comments on commit a316047

Please sign in to comment.