@createjs/easeljs
Version:
The Easel JavaScript library provides a full, hierarchical display list, a core interaction model, and helper classes to make working with the HTML5 Canvas element much easier. Part of the CreateJS suite of libraries.
513 lines (459 loc) • 19.3 kB
JavaScript
/**
* @license BitmapCache
* Visit http://createjs.com/ for documentation, updates and examples.
*
* Copyright (c) 2017 gskinner.com, inc.
*
* 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.
*/
import Filter from "./Filter";
import Rectangle from "../geom/Rectangle";
import StageGL from "../display/StageGL";
/**
* The BitmapCache is an internal representation of all the cache properties and logic required in order to "cache"
* an object. This information and functionality used to be located on a {@link easeljs.DisplayObject#cache}
* method in {@link easeljs.DisplayObject}, but was moved to its own class.
*
* Caching in this context is purely visual, and will render the DisplayObject out into an image to be used instead
* of the object. The actual cache itself is still stored on the target with the {@link easeljs.DisplayObject#cacheCanvas}.
*
* Working with a singular image like a {@link easeljs.Bitmap}, there is little benefit to performing
* a cache operation, as it is already a single image. Caching is best done on containers that have multiple complex
* parts that do not change often, so that rendering the image will improve overall rendering speed. A cached object
* will not visually update until explicitly told to do so with a call to {@link easeljs.Stage#update},
* much like a Stage. If a cache is being updated every frame, it is likely not improving rendering performance.
* Caches are best used when updates will be sparse.
*
* Caching is also a co-requisite for applying filters to prevent expensive filters running constantly without need.
* The BitmapCache is also responsible for applying filters to objects, and reads each {@link easeljs.Filter}.
* Real-time Filters are not recommended when dealing with a Context2D canvas if performance is a concern. For best
* performance and to still allow for some visual effects, use a {{#crossLink "DisplayObject/compositeOperation:property"}}{{/crossLink}}
* when possible.
*
* @memberof easeljs
* @extends easeljs.Filter
*/
export default class BitmapCache extends Filter {
constructor () {
super();
/**
* Width of the cache relative to the target object.
* @protected
* @type {Number}
* @default undefined
*/
this.width = undefined;
/**
* Height of the cache relative to the target object.
* @protected
* @type {Number}
* @default undefined
*/
this.height = undefined;
/**
* Horizontal position of the cache relative to the target's origin.
* @protected
* @type {Number}
* @default undefined
*/
this.x = undefined;
/**
* Vertical position of the cache relative to target's origin.
* @protected
* @type {Number}
* @default undefined
*/
this.y = undefined;
/**
* The internal scale of the cache image, does not affects display size. This is useful to both increase and
* decrease render quality. Objects with increased scales are more likely to look good when scaled up. Objects
* with decreased scales can save on rendering performance.
* @protected
* @type {Number}
* @default 1
*/
this.scale = 1;
/**
* The relative offset of the {@link easeljs.BitmapCache#x} position, used for drawing
* into the cache with the correct offsets. Re-calculated every update call before drawing.
* @protected
* @type {Number}
* @default 0
*/
this.offX = 0;
/**
* The relative offset of the {@link easeljs.BitmapCache#y} position, used for drawing
* into the cache with the correct offsets. Re-calculated every update call before drawing.
* @protected
* @type {Number}
* @default 0
*/
this.offY = 0;
/**
* Track how many times the cache has been updated, mostly used for preventing duplicate cacheURLs. This can be
* useful to see if a cache has been updated.
* @type {Number}
* @default 0
*/
this.cacheID = 0;
/**
* Relative offset of the x position, used for drawing the cache into other scenes.
* Re-calculated every update call before drawing.
* @protected
* @type {Number}
* @default 0
* @todo Is this description right? Its the same as offX.
*/
this._filterOffX = 0;
/**
* Relative offset of the y position, used for drawing into the cache into other scenes.
* Re-calculated every update call before drawing.
* @protected
* @type {Number}
* @default 0
* @todo Is this description right? Its the same as offY.
*/
this._filterOffY = 0;
/**
* The cacheID when a DataURL was requested.
* @protected
* @type {Number}
* @default 0
*/
this._cacheDataURLID = 0;
/**
* The cache's DataURL, generated on-demand using the getter.
* @protected
* @type {String}
* @default null
*/
this._cacheDataURL = null;
/**
* Internal tracking of final bounding width, approximately `width*scale;` however, filters can complicate the actual value.
* @protected
* @type {Number}
* @default 0
*/
this._drawWidth = 0;
/**
* Internal tracking of final bounding height, approximately `height*scale;` however, filters can complicate the actual value.
* @protected
* @type {Number}
* @default 0
*/
this._drawHeight = 0;
/**
* Internal tracking of the last requested bounds, may happen repeadtedly so stored to avoid object creation.
* @protected
* @type {easeljs.Rectangle}
* @default easeljs.Rectangle
*/
this._boundRect = new Rectangle();
}
/**
* Returns the bounds that surround all applied filters. This relies on each filter to describe how it changes bounds.
* @param {easeljs.DisplayObject} target The object to check the filter bounds for.
* @param {easeljs.Rectangle} [output] Calculated bounds will be applied to this rect.
* @return {easeljs.Rectangle}
* @static
*/
static getFilterBounds (target, output = new Rectangle()) {
let filters = target.filters;
let filterCount = filters && filters.length;
if (!!filterCount <= 0) { return output; }
for (let i=0; i<filterCount; i++) {
let f = filters[i];
if (!f || !f.getBounds) { continue; }
let test = f.getBounds();
if (!test) { continue; }
if (i==0) {
output.setValues(test.x, test.y, test.width, test.height);
} else {
output.extend(test.x, test.y, test.width, test.height);
}
}
return output;
}
/**
* Directly called via {@link easeljs.DisplayObject#cache}. Creates and sets properties needed
* for a cache to function, and performs the initial update.
* @param {easeljs.DisplayObject} target The DisplayObject this cache is linked to.
* @param {Number} [x=0] The x coordinate origin for the cache region.
* @param {Number} [y=0] The y coordinate origin for the cache region.
* @param {Number} [width=1] The width of the cache region.
* @param {Number} [height=1] The height of the cache region.
* @param {Number} [scale=1] The scale at which the cache will be created. For example, if you cache a vector shape
* using `myShape.cache(0,0,100,100,2)`, then the resulting cacheCanvas will be 200x200 pixels. This lets you scale
* and rotate cached elements with greater fidelity.
* @param {Object} [options] When using things like {@link easeljs.StageGL} there may be
* extra caching opportunities or requirements.
*/
define (target, x = 0, y = 0, width = 1, height = 1, scale = 1, options) {
if (!target) { throw "No symbol to cache"; }
this._options = options;
this._useWebGL = options !== undefined;
this.target = target;
this.width = width >= 1 ? width : 1;
this.height = height >= 1 ? height : 1;
this.x = x;
this.y = y;
this.scale = scale;
this.update();
}
/**
* Directly called via {@link easeljs.DisplayObject#updateCache}, but also internally. This
* has the dual responsibility of making sure the surface is ready to be drawn to, and performing the draw. For
* full details of each behaviour, check the protected functions {@link easeljs.BitmapCache#_updateSurface}
* and {@link easeljs.BitmapCache#_drawToCache} respectively.
* @param {String} [compositeOperation] The DisplayObject this cache is linked to.
*/
update (compositeOperation) {
if (!this.target) { throw "define() must be called before update()"; }
let filterBounds = BitmapCache.getFilterBounds(this.target);
let surface = this.target.cacheCanvas;
this._drawWidth = Math.ceil(this.width*this.scale) + filterBounds.width;
this._drawHeight = Math.ceil(this.height*this.scale) + filterBounds.height;
if (!surface || this._drawWidth != surface.width || this._drawHeight != surface.height) {
this._updateSurface();
}
this._filterOffX = filterBounds.x;
this._filterOffY = filterBounds.y;
this.offX = this.x*this.scale + this._filterOffX;
this.offY = this.y*this.scale + this._filterOffY;
this._drawToCache(compositeOperation);
this.cacheID = this.cacheID?this.cacheID+1:1;
}
/**
* Reset and release all the properties and memory associated with this cache.
*/
release () {
let stage = this.target.stage;
if (this._useWebGL && this._webGLCache) {
// if it isn't cache controlled clean up after yourself
if (!this._webGLCache.isCacheControlled) {
if (this.__lastRT) { this.__lastRT = undefined; }
if (this.__rtA) { this._webGLCache._killTextureObject(this.__rtA); }
if (this.__rtB) { this._webGLCache._killTextureObject(this.__rtB); }
if (this.target && this.target.cacheCanvas) { this._webGLCache._killTextureObject(this.target.cacheCanvas); }
}
// set the context to none and let the garbage collector get the rest when the canvas itself gets removed
this._webGLCache = false;
} else if (stage instanceof StageGL) {
stage.releaseTexture(this.target.cacheCanvas);
}
this.target = this.target.cacheCanvas = null;
this.cacheID = this._cacheDataURLID = this._cacheDataURL = undefined;
this.width = this.height = this.x = this.y = this.offX = this.offY = 0;
this.scale = 1;
}
/**
* Returns a data URL for the cache, or `null` if this display object is not cached.
* Uses {@link easeljs.BitmapCache#cacheID} to ensure a new data URL is not generated if the
* cache has not changed.
* @return {String} The image data url for the cache.
*/
getCacheDataURL () {
let cacheCanvas = this.target && this.target.cacheCanvas;
if (!cacheCanvas) { return null; }
if (this.cacheID != this._cacheDataURLID) {
this._cacheDataURLID = this.cacheID;
this._cacheDataURL = cacheCanvas.toDataURL?cacheCanvas.toDataURL():null; // incase function is
}
return this._cacheDataURL;
}
/**
* Use context2D drawing commands to display the cache canvas being used.
* @param {CanvasRenderingContext2D} ctx The context to draw into.
* @return {Boolean} Whether the draw was handled successfully.
*/
draw (ctx) {
if (!this.target) { return false; }
ctx.drawImage(
this.target.cacheCanvas,
this.x + (this._filterOffX/this.scale),
this.y + (this._filterOffY/this.scale),
this._drawWidth/this.scale,
this._drawHeight/this.scale
);
return true;
}
/**
* Determine the bounds of the shape in local space.
* @returns {easeljs.Rectangle}
*/
getBounds () {
const scale = this.scale;
return this._boundRect.setValue(
this._filterOffX/scale,
this._filterOffY/scale,
this.width/scale,
this.height/scale
);
}
/**
* Basic context2D caching works by creating a new canvas element and setting its physical size. This function will
* create and or size the canvas as needed.
* @protected
*/
_updateSurface () {
let surface;
if (!this._useWebGL) {
surface = this.target.cacheCanvas;
// create it if it's missing
if (!surface) {
surface = this.target.cacheCanvas = window.createjs&&createjs.createCanvas?createjs.createCanvas():document.createElement("canvas");
}
// now size it
surface.width = this._drawWidth;
surface.height = this._drawHeight;
// skip the webgl-only updates
return;
}
// create it if it's missing
if (!this._webGLCache) {
if (this._options.useGL === "stage") {
if(!(this.target.stage != null && this.target.stage.isWebGL)) {
throw `Cannot use 'stage' for cache because the object's parent stage is ${this.target.stage != null ? "non WebGL." : "not set, please addChild to the correct stage."}`;
}
this.target.cacheCanvas = true; // will be replaced with RenderTexture, temporary positive value for old "isCached" checks
this._webGLCache = this.target.stage;
} else if (this._options.useGL === "new") {
this.target.cacheCanvas = document.createElement("canvas"); // we can turn off autopurge because we wont be making textures here
this._webGLCache = new StageGL(this.target.cacheCanvas, {antialias: true, transparent: true, autoPurge: -1});
this._webGLCache.isCacheControlled = true; // use this flag to control stage sizing and final output
} else {
throw "Invalid option provided to useGL, expected ['stage', 'new', StageGL, undefined], got "+ this._options.useGL;
}
}
// now size render surfaces
let stageGL = this._webGLCache;
surface = this.target.cacheCanvas;
// if we have a dedicated stage we've gotta size it
if (stageGL.isCacheControlled) {
surface.width = this._drawWidth;
surface.height = this._drawHeight;
stageGL.updateViewport(this._drawWidth, this._drawHeight);
}
if (this.target.filters) {
// with filters we can't tell how many we'll need but the most we'll ever need is two, so make them now
stageGL.getTargetRenderTexture(this.target, this._drawWidth,this._drawHeight);
stageGL.getTargetRenderTexture(this.target, this._drawWidth,this._drawHeight);
} else if (!stageGL.isCacheControlled) {
// without filters then we only need one RenderTexture, and that's only if its not a dedicated stage
stageGL.getTargetRenderTexture(this.target, this._drawWidth,this._drawHeight);
}
}
/**
* Perform the cache draw out for context 2D now that the setup properties have been performed.
* @protected
*/
_drawToCache (compositeOperation) {
let target = this.target;
let surface = target.cacheCanvas;
let webGL = this._webGLCache;
if (!this._useWebGL || !webGL) {
let ctx = surface.getContext("2d");
if (!compositeOperation) {
ctx.clearRect(0, 0, this._drawWidth+1, this._drawHeight+1);
}
ctx.save();
ctx.globalCompositeOperation = compositeOperation;
ctx.setTransform(this.scale,0,0,this.scale, -this._filterOffX,-this._filterOffY);
ctx.translate(-this.x, -this.y);
target.draw(ctx, true);
ctx.restore();
if (target.filters && target.filters.length) {
this._applyFilters(target);
}
surface._invalid = true;
return;
}
this._webGLCache.cacheDraw(target, target.filters, this);
// NOTE: we may of swapped around which element the surface is, so we re-fetch it
surface = this.target.cacheCanvas;
surface.width = this._drawWidth;
surface.height = this._drawHeight;
surface._invalid = true;
}
/**
* Work through every filter and apply its individual transformation to it.
* @protected
*/
_applyFilters () {
let surface = this.target.cacheCanvas;
let filters = this.target.filters;
let w = this._drawWidth;
let h = this._drawHeight;
// setup
let data = surface.getContext("2d").getImageData(0,0, w,h);
// apply
let l = filters.length;
for (let i=0; i<l; i++) {
filters[i]._applyFilter(data);
}
//done
surface.getContext("2d").putImageData(data, 0,0);
}
}
/**
* Functionality injected to {@link easeljs.BitmapCache}. Ensure StageGL is loaded after all other
* standard EaselJS classes are loaded but before making any DisplayObject instances for injection to take full effect.
* Replaces the context2D cache draw with the option for WebGL or context2D drawing.
* If options is set to "true" a StageGL is created and contained on the object for use when rendering a cache.
* If options is a StageGL instance it will not create an instance but use the one provided.
* If possible it is best to provide the StageGL instance that is a parent to this DisplayObject for performance reasons.
* A StageGL cache does not infer the ability to draw objects a StageGL cannot currently draw,
* i.e. do not use a WebGL context cache when caching a Shape, Text, etc.
*
* You can make your own StageGL and have it render to a canvas if you set ".isCacheControlled" to true on your stage.
* You may wish to create your own StageGL instance to control factors like background color/transparency, AA, and etc.
* You must set "options" to its own stage if you wish to use the fast Render Textures available only to StageGLs.
* If you use WebGL cache on a container with Shapes you will have to cache each shape individually before the container,
* otherwise the WebGL cache will not render the shapes.
*
* @name easeljs.BitmapCache#cache
*
* @example <caption>WebGL cache with 2d context</caption>
* let stage = new Stage();
* let bmp = new Bitmap(src);
* bmp.cache(0, 0, bmp.width, bmp.height, 1, true); // no StageGL to use, so make one
* let shape = new Shape();
* shape.graphics.clear().fill("red").drawRect(0,0,20,20);
* shape.cache(0, 0, 20, 20, 1); // cannot use WebGL cache
*
* @example <caption>WebGL cache with WebGL context</caption>
* let stageGL = new StageGL();
* let bmp = new Bitmap(src);
* bmp.cache(0, 0, bmp.width, bmp.height, 1, stageGL); // use our StageGL to cache
* let shape = new Shape();
* shape.graphics.clear().fill("red").drawRect(0,0,20,20);
* shape.cache(0, 0, 20, 20, 1); // cannot use WebGL cache
*
* @param {Number} x The x coordinate origin for the cache region.
* @param {Number} y The y coordinate origin for the cache region.
* @param {Number} width The width of the cache region.
* @param {Number} height The height of the cache region.
* @param {Number} [scale=1] The scale at which the cache will be created. For example, if you cache a vector shape using
* myShape.cache(0,0,100,100,2) then the resulting cacheCanvas will be 200x200 px. This lets you scale and rotate
* cached elements with greater fidelity.
* @param {Boolean | easeljs.StageGL} [options] Select whether to use context 2D, or WebGL rendering, and whether to make a new stage instance or use an existing one.
*/