diff --git a/js/nodes/Imageable.ts b/js/nodes/Imageable.ts index a59132aa9..4bd157746 100644 --- a/js/nodes/Imageable.ts +++ b/js/nodes/Imageable.ts @@ -62,8 +62,63 @@ export type Mipmap = { updateCanvas?: () => void; }[]; +/** + * The available ways to specify an image as an input to Imageable. See onImagePropertyChange() for parsing logic. + * We support a few different 'image' types that can be passed in: + * + * HTMLImageElement - A normal HTML . If it hasn't been fully loaded yet, Scenery will take care of adding a + * listener that will update Scenery with its width/height (and load its data) when the image is fully loaded. + * NOTE that if you just created the , it probably isn't loaded yet, particularly in Safari. If the Image + * node is constructed with an that hasn't fully loaded, it will have a width and height of 0, which may + * cause issues if you are using bounds for layout. Please see initialWidth/initialHeight notes below. + * + * URL - Provide a {string}, and Scenery will assume it is a URL. This can be a normal URL, or a data URI, both will + * work. Please note that this has the same loading-order issues as using HTMLImageElement, but that it's almost + * always guaranteed to not have a width/height when you create the Image node. Note that data URI support for + * formats depends on the browser - only JPEG and PNG are supported broadly. Please see initialWidth/initialHeight + * notes below. + * Additionally, note that if a URL is provided, accessing image.getImage() or image.image will result not in the + * original URL (currently), but with the automatically created HTMLImageElement. + * + * HTMLCanvasElement - It's possible to pass an HTML5 Canvas directly into the Image node. It will immediately be + * aware of the width/height (bounds) of the Canvas, but NOTE that the Image node will not listen to Canvas size + * changes. It is assumed that after you pass in a Canvas to an Image node that it will not be modified further. + * Additionally, the Image node will only be rendered using Canvas or WebGL if a Canvas is used as input. + * + * Mipmap data structure - Image supports a mipmap data structure that provides rasterized mipmap levels. The 'top' + * level (level 0) is the entire full-size image, and every other level is twice as small in every direction + * (~1/4 the pixels), rounding dimensions up. This is useful for browsers that display the image badly if the + * image is too large. Instead, Scenery will dynamically pick the most appropriate size of the image to use, + * which improves the image appearance. + * The passed in 'image' should be an Array of mipmap objects of the format: + * { + * img: {HTMLImageElement}, // preferably preloaded, but it isn't required + * url: {string}, // URL (usually a data URL) for the image level + * width: {number}, // width of the mipmap level, in pixels + * height: {number} // height of the mipmap level, in pixels, + * canvas: {HTMLCanvasElement} // Canvas element containing the image data for the img. + * [updateCanvas]: {function} // If available, should be called before using the Canvas directly. + * } + * At least one level is required (level 0), and each mipmap level corresponds to the index in the array, e.g.: + * [ + * level 0 (full size, e.g. 100x64) + * level 1 (half size, e.g. 50x32) + * level 2 (quarter size, e.g. 25x16) + * level 3 (eighth size, e.g. 13x8 - note the rounding up) + * ... + * level N (single pixel, e.g. 1x1 - this is the smallest level permitted, and there should only be one) + * ] + * Additionally, note that (currently) image.getImage() will return the HTMLImageElement from the first level, + * not the mipmap data. + * + * Also note that if the underlying image (like Canvas data) has changed, it is recommended to call + * invalidateImage() instead of changing the image reference (calling setImage() multiple times) + */ export type ImageableImage = string | HTMLImageElement | HTMLCanvasElement | Mipmap; +// The output image type from parsing the input "ImageableImage", see onImagePropertyChange() +type ParsedImage = HTMLImageElement | HTMLCanvasElement | null; + export type ImageableOptions = { image?: ImageableImage; imageProperty?: TReadOnlyProperty; @@ -80,8 +135,8 @@ export type ImageableOptions = { const Imageable = ( type: SuperType ) => { // eslint-disable-line @typescript-eslint/explicit-module-boundary-types return class ImageableMixin extends type { - // (scenery-internal) Internal stateful value, see setImage() - public _image: HTMLImageElement | HTMLCanvasElement | null; + // (scenery-internal) Internal stateful value, see onImagePropertyChange() + public _image: ParsedImage; // For imageProperty private readonly _imageProperty: TinyForwardingProperty; @@ -161,59 +216,8 @@ const Imageable = ( type: SuperType ) => { // esl } /** - * Sets the current image to be displayed by this Image node. - * - * We support a few different 'image' types that can be passed in: - * - * HTMLImageElement - A normal HTML . If it hasn't been fully loaded yet, Scenery will take care of adding a - * listener that will update Scenery with its width/height (and load its data) when the image is fully loaded. - * NOTE that if you just created the , it probably isn't loaded yet, particularly in Safari. If the Image - * node is constructed with an that hasn't fully loaded, it will have a width and height of 0, which may - * cause issues if you are using bounds for layout. Please see initialWidth/initialHeight notes below. - * - * URL - Provide a {string}, and Scenery will assume it is a URL. This can be a normal URL, or a data URI, both will - * work. Please note that this has the same loading-order issues as using HTMLImageElement, but that it's almost - * always guaranteed to not have a width/height when you create the Image node. Note that data URI support for - * formats depends on the browser - only JPEG and PNG are supported broadly. Please see initialWidth/initialHeight - * notes below. - * Additionally, note that if a URL is provided, accessing image.getImage() or image.image will result not in the - * original URL (currently), but with the automatically created HTMLImageElement. - * TODO: return the original input https://github.com/phetsims/scenery/issues/1581 - * - * HTMLCanvasElement - It's possible to pass an HTML5 Canvas directly into the Image node. It will immediately be - * aware of the width/height (bounds) of the Canvas, but NOTE that the Image node will not listen to Canvas size - * changes. It is assumed that after you pass in a Canvas to an Image node that it will not be modified further. - * Additionally, the Image node will only be rendered using Canvas or WebGL if a Canvas is used as input. - * - * Mipmap data structure - Image supports a mipmap data structure that provides rasterized mipmap levels. The 'top' - * level (level 0) is the entire full-size image, and every other level is twice as small in every direction - * (~1/4 the pixels), rounding dimensions up. This is useful for browsers that display the image badly if the - * image is too large. Instead, Scenery will dynamically pick the most appropriate size of the image to use, - * which improves the image appearance. - * The passed in 'image' should be an Array of mipmap objects of the format: - * { - * img: {HTMLImageElement}, // preferably preloaded, but it isn't required - * url: {string}, // URL (usually a data URL) for the image level - * width: {number}, // width of the mipmap level, in pixels - * height: {number} // height of the mipmap level, in pixels, - * canvas: {HTMLCanvasElement} // Canvas element containing the image data for the img. - * [updateCanvas]: {function} // If available, should be called before using the Canvas directly. - * } - * At least one level is required (level 0), and each mipmap level corresponds to the index in the array, e.g.: - * [ - * level 0 (full size, e.g. 100x64) - * level 1 (half size, e.g. 50x32) - * level 2 (quarter size, e.g. 25x16) - * level 3 (eighth size, e.g. 13x8 - note the rounding up) - * ... - * level N (single pixel, e.g. 1x1 - this is the smallest level permitted, and there should only be one) - * ] - * Additionally, note that (currently) image.getImage() will return the HTMLImageElement from the first level, - * not the mipmap data. - * TODO: return the original input + * Sets the current image to be displayed by this Image node. See ImageableImage for details on provided image value. * - * Also note that if the underlying image (like Canvas data) has changed, it is recommended to call - * invalidateImage() instead of changing the image reference (calling setImage() multiple times) */ public setImage( image: ImageableImage ): this { assert && assert( image, 'image should be available' ); @@ -225,16 +229,16 @@ const Imageable = ( type: SuperType ) => { // esl public set image( value: ImageableImage ) { this.setImage( value ); } - public get image(): HTMLImageElement | HTMLCanvasElement { return this.getImage(); } + public get image(): ParsedImage { return this.getImage(); } /** * Returns the current image's representation as either a Canvas or img element. * * NOTE: If a URL or mipmap data was provided, this currently doesn't return the original input to setImage(), but - * instead provides the mapped result (or first mipmap level's image). - * TODO: return the original result instead. https://github.com/phetsims/scenery/issues/1581 + * instead provides the mapped result (or first mipmap level's image). If you need the original, use + * imageProperty instead. */ - public getImage(): HTMLImageElement | HTMLCanvasElement { + public getImage(): ParsedImage { assert && assert( this._image !== null ); return this._image!; @@ -345,11 +349,11 @@ const Imageable = ( type: SuperType ) => { // esl * function. This may trigger bounds changes, even if the previous and next image (and image dimensions) * are the same. * - * @param image - See setImage()'s documentation + * @param image - See ImageableImage's type documentation * @param width - Initial width of the image. See setInitialWidth() for more documentation * @param height - Initial height of the image. See setInitialHeight() for more documentation */ - public setImageWithSize( image: string | HTMLImageElement | HTMLCanvasElement | Mipmap, width: number, height: number ): this { + public setImageWithSize( image: ImageableImage, width: number, height: number ): this { // First, setImage(), as it will reset the initial width and height this.setImage( image ); @@ -934,7 +938,7 @@ const Imageable = ( type: SuperType ) => { // esl * @param width - logical width of the image * @param height - logical height of the image */ -Imageable.getHitTestData = ( image: HTMLImageElement | HTMLCanvasElement, width: number, height: number ): ImageData | null => { +Imageable.getHitTestData = ( image: Exclude, width: number, height: number ): ImageData | null => { // If the image isn't loaded yet, we don't want to try loading anything if ( !( ( 'naturalWidth' in image ? image.naturalWidth : 0 ) || image.width ) || !( ( 'naturalHeight' in image ? image.naturalHeight : 0 ) || image.height ) ) { return null; diff --git a/js/nodes/Path.ts b/js/nodes/Path.ts index bcd6f2cb7..84a223212 100644 --- a/js/nodes/Path.ts +++ b/js/nodes/Path.ts @@ -10,7 +10,7 @@ import Bounds2 from '../../../dot/js/Bounds2.js'; import { Shape } from '../../../kite/js/imports.js'; import Matrix3 from '../../../dot/js/Matrix3.js'; import Vector2 from '../../../dot/js/Vector2.js'; -import { CanvasContextWrapper, CanvasSelfDrawable, Instance, TPathDrawable, Node, NodeOptions, Paint, Paintable, PAINTABLE_DRAWABLE_MARK_FLAGS, PAINTABLE_OPTION_KEYS, PaintableOptions, PathCanvasDrawable, PathSVGDrawable, Renderer, scenery, SVGSelfDrawable } from '../imports.js'; +import { CanvasContextWrapper, CanvasSelfDrawable, Instance, Node, NodeOptions, Paint, Paintable, PAINTABLE_DRAWABLE_MARK_FLAGS, PAINTABLE_OPTION_KEYS, PaintableOptions, PathCanvasDrawable, PathSVGDrawable, Renderer, scenery, SVGSelfDrawable, TPathDrawable } from '../imports.js'; import optionize, { combineOptions } from '../../../phet-core/js/optionize.js'; import TReadOnlyProperty, { isTReadOnlyProperty } from '../../../axon/js/TReadOnlyProperty.js'; import WithRequired from '../../../phet-core/js/types/WithRequired.js'; @@ -31,32 +31,43 @@ const DEFAULT_OPTIONS = { export type PathBoundsMethod = 'accurate' | 'unstroked' | 'tightPadding' | 'safePadding' | 'none'; +/** + * The valid parameter types are: + * - Shape: (from Kite), normally used. + * - string: Uses the SVG Path format, see https://www.w3.org/TR/SVG/paths.html (the PATH part of ). + * This will immediately be converted to a Shape object when set, and getShape() or equivalents will return + * the parsed Shape instance instead of the original string. See "ParsedShape" + * - null: Indicates that there is no Shape, and nothing is drawn. Usually used as a placeholder. + * + * NOTE: Be aware of the potential for memory leaks. If a Shape is not marked as immutable (with makeImmutable()), + * Path will add a listener so that it is updated when the Shape itself changes. If there is a listener + * added, keeping a reference to the Shape will also keep a reference to the Path object (and thus whatever + * Nodes are connected to the Path). For now, set path.shape = null if you need to release the reference + * that the Shape would have, or call dispose() on the Path if it is not needed anymore. + */ +type InputShape = Shape | string | null; + +/** + * See InputShape for details, but this type differs in that it only supports a Shape, and any "string" data will + * be parsed into a Shape instance. + */ +type ParsedShape = Shape | null; + +// Provide these as an option. type SelfOptions = { + /** * This sets the shape of the Path, which determines the shape of its appearance. It should generally not be called - * on Path subtypes like Line, Rectangle, etc. - * - * NOTE: When you create a Path with a shape in the constructor, this function will be called. - * - * The valid parameter types are: - * - Shape: (from Kite), normally used. - * - string: Uses the SVG Path format, see https://www.w3.org/TR/SVG/paths.html (the PATH part of ). - * This will immediately be converted to a Shape object, and getShape() or equivalents will return the new - * Shape object instead of the original string. - * - null: Indicates that there is no Shape, and nothing is drawn. Usually used as a placeholder. + * on Path subtypes like Line, Rectangle, etc. See InputShape for details about what to provide for the shape. * - * NOTE: Be aware of the potential for memory leaks. If a Shape is not marked as immutable (with makeImmutable()), - * Path will add a listener so that it is updated when the Shape itself changes. If there is a listener - * added, keeping a reference to the Shape will also keep a reference to the Path object (and thus whatever - * Nodes are connected to the Path). For now, set path.shape = null if you need to release the reference - * that the Shape would have, or call dispose() on the Path if it is not needed anymore. + * NOTE: When you create a Path with a shape in the constructor, this setter will be called (don't overload the option). */ - shape?: Shape | string | null; + shape?: InputShape; /** * Similar to `shape`, but allows setting the shape as a Property. */ - shapeProperty?: TReadOnlyProperty; + shapeProperty?: TReadOnlyProperty; /** * Sets the bounds method for the Path. This determines how our (self) bounds are computed, and can particularly @@ -86,14 +97,14 @@ export default class Path extends Paintable( Node ) { // so it is best to not have to compute it on changes. // NOTE: Please use hasShape() to determine if we are actually drawing things, as it is subtype-safe. // (scenery-internal) - public _shape: Shape | null; + public _shape: ParsedShape; // For shapeProperty - private readonly _shapeProperty: TinyForwardingProperty; + private readonly _shapeProperty: TinyForwardingProperty; // This stores a stroked copy of the Shape which is lazily computed. This can be required for computing bounds // of a Shape with a stroke. - private _strokedShape: Shape | null; + private _strokedShape: ParsedShape; // (scenery-internal) public _boundsMethod: PathBoundsMethod; @@ -113,11 +124,11 @@ export default class Path extends Paintable( Node ) { * - shape: The actual Shape (or a string representing an SVG path, or null). * - boundsMethod: Determines how the bounds of a shape are determined. * - * @param shape - The initial Shape to display. See setShape() for more details and documentation. + * @param shape - The initial Shape to display. See onShapePropertyChange() for more details and documentation. * @param [providedOptions] - Path-specific options are documented in PATH_OPTION_KEYS above, and can be provided * along-side options for Node */ - public constructor( shape: Shape | string | null | TReadOnlyProperty, providedOptions?: PathOptions ) { + public constructor( shape: InputShape | TReadOnlyProperty, providedOptions?: PathOptions ) { assert && assert( providedOptions === undefined || Object.getPrototypeOf( providedOptions ) === Object.prototype, 'Extra prototype on Node options object is a code smell' ); @@ -141,7 +152,7 @@ export default class Path extends Paintable( Node ) { super(); // We'll initialize this by mutating. - this._shapeProperty = new TinyForwardingProperty( null, false, this.onShapePropertyChange.bind( this ) ); + this._shapeProperty = new TinyForwardingProperty( null, false, this.onShapePropertyChange.bind( this ) ); this._shape = DEFAULT_OPTIONS.shape; this._strokedShape = null; @@ -154,7 +165,7 @@ export default class Path extends Paintable( Node ) { this.mutate( options ); } - public setShape( shape: Shape | string | null ): this { + public setShape( shape: InputShape ): this { assert && assert( shape === null || typeof shape === 'string' || shape instanceof Shape, 'A path\'s shape should either be null, a string, or a Shape' ); @@ -163,9 +174,9 @@ export default class Path extends Paintable( Node ) { return this; } - public set shape( value: Shape | string | null ) { this.setShape( value ); } + public set shape( value: InputShape ) { this.setShape( value ); } - public get shape(): Shape | null { return this.getShape(); } + public get shape(): ParsedShape { return this.getShape(); } /** * Returns the shape that was set for this Path (or for subtypes like Line and Rectangle, will return an immutable @@ -174,11 +185,12 @@ export default class Path extends Paintable( Node ) { * It is best to generally assume modifications to the Shape returned is not supported. If there is no shape * currently, null will be returned. */ - public getShape(): Shape | null { + public getShape(): ParsedShape { + assert && assert( this.shapeProperty.value === this._shape ); return this._shape; } - private onShapePropertyChange( shape: Shape | string | null ): void { + private onShapePropertyChange( shape: InputShape ): void { assert && assert( shape === null || typeof shape === 'string' || shape instanceof Shape, 'A path\'s shape should either be null, a string, or a Shape' ); @@ -189,7 +201,7 @@ export default class Path extends Paintable( Node ) { } if ( typeof shape === 'string' ) { - // be content with setShape always invalidating the shape? + // be content with onShapePropertyChange always invalidating the shape? shape = new Shape( shape ); } this._shape = shape; @@ -205,19 +217,19 @@ export default class Path extends Paintable( Node ) { /** * See documentation for Node.setVisibleProperty, except this is for the shape */ - public setShapeProperty( newTarget: TReadOnlyProperty | null ): this { - return this._shapeProperty.setTargetProperty( this, null, newTarget as TProperty ); + public setShapeProperty( newTarget: TReadOnlyProperty | null ): this { + return this._shapeProperty.setTargetProperty( this, null, newTarget as TProperty ); } - public set shapeProperty( property: TReadOnlyProperty | null ) { this.setShapeProperty( property ); } + public set shapeProperty( property: TReadOnlyProperty | null ) { this.setShapeProperty( property ); } - public get shapeProperty(): TProperty { return this.getShapeProperty(); } + public get shapeProperty(): TProperty { return this.getShapeProperty(); } /** * Like Node.getVisibleProperty(), but for the shape. Note this is not the same as the Property provided in - * setImageProperty. Thus is the nature of TinyForwardingProperty. + * setShapeProperty. Thus is the nature of TinyForwardingProperty. */ - public getShapeProperty(): TProperty { + public getShapeProperty(): TProperty { return this._shapeProperty; }