UNPKG

pencil.js

Version:

Nice modular interactive 2D drawing library.

237 lines (210 loc) 6.61 kB
import Rectangle from "@pencil.js/rectangle"; import NetworkEvent from "@pencil.js/network-event"; import OffScreenCanvas from "@pencil.js/offscreen-canvas"; /** * @module Image */ const urlKey = Symbol("_url"); const offscreenKey = Symbol("_offscreen"); /** * Image class * <br><img src="./media/examples/image.png" alt="image demo"/> * @class * @extends {module:Rectangle} */ export default class Image extends Rectangle { /** * Image constructor * @param {PositionDefinition} positionDefinition - Top-left corner of the image * @param {String|Image|HTMLImageElement} source - Link to an image file, another Image instance or the image file itself * @param {ComponentOptions} [options] - Drawing options */ constructor (positionDefinition, source, options) { super(positionDefinition, undefined, undefined, options); /** * @type {HTMLImageElement} */ this.file = null; /** * @type {Boolean} */ this.isLoaded = false; /** * @type {String} * @private */ this[urlKey] = null; /** * @type {{ tint: String, cache: OffScreenCanvas }} * @private */ this[offscreenKey] = {}; this.url = source; } /** * Change the image URL * @param {String|Image|HTMLImageElement} source - Link to an image file, another Image instance or the image file itself */ set url (source) { if (this.url === source) { return; } this.file = null; this.isLoaded = false; this[urlKey] = source; if (source) { const done = (img) => { this[urlKey] = img.src; this.file = img; this.isLoaded = true; this.width = img.width; this.height = img.height; this.fire(new NetworkEvent(NetworkEvent.events.ready, this)); }; if (source instanceof window.HTMLImageElement) { done(source); } else if (source instanceof Image) { if (source.isLoaded) { done(source.file); } else { source.on(NetworkEvent.events.ready, () => done(source.file)); } } else { Image.load(source).then(done).catch(() => { this.fire(new NetworkEvent(NetworkEvent.events.error, this)); }); } } } /** * Get the image URL * @return {String} */ get url () { return this[urlKey]; } /** * Draw it on a context * @param {CanvasRenderingContext2D} ctx - Drawing context * @return {Image} Itself */ makePath (ctx) { if (this.isLoaded) { ctx.save(); const origin = this.getOrigin(); ctx.translate(origin.x, origin.y); const path = new window.Path2D(); this.trace(path); this.path = path; if (this.willFill) { ctx.fill(path); ctx.shadowBlur = 0; } if (this.willStroke) { ctx.stroke(path); } this.draw(ctx); ctx.restore(); } return this; } /** * Draw the image itself * @param {CanvasRenderingContext2D} ctx - Drawing context * @return {Image} Itself */ draw (ctx) { if (this.options.tint) { if (!this[offscreenKey].cache) { this[offscreenKey].cache = OffScreenCanvas.getDrawingContext(this.width, this.height); } const { cache } = this[offscreenKey]; const tintString = this.options.tint.toString(); if (tintString !== this[offscreenKey].tint) { cache.clearRect(0, 0, cache.canvas.width, cache.canvas.height); cache.drawImage(this.file, 0, 0); cache.globalCompositeOperation = "source-atop"; cache.fillStyle = tintString; cache.fillRect(0, 0, cache.canvas.width, cache.canvas.height); this[offscreenKey].tint = tintString; } ctx.drawImage(this[offscreenKey].cache.canvas, 0, 0, this.width, this.height); } else { ctx.drawImage(this.file, 0, 0, this.width, this.height); } return this; } /** * @inheritDoc */ isHover (...args) { if (!this.isLoaded) { return false; } const previous = this.options.fill; this.options.fill = true; const result = super.isHover(...args); this.options.fill = previous; return result; } /** * @inheritDoc */ toJSON () { const json = super.toJSON(); delete json.width; delete json.height; const { url } = this; return { ...json, url, }; } /** * @inheritDoc * @param {Object} definition - Image definition * @return {Image} */ static from (definition) { return new Image(definition.position, definition.url, definition.options); } /** * Promise to load an image file. * @param {String|Array<String>} url - Link or an array of links to image files * @return {Promise<HTMLImageElement>} */ static load (url) { if (Array.isArray(url)) { return Promise.all(url.map(singleUrl => Image.load(singleUrl))); } return new Promise((resolve, reject) => { const img = window.document.createElement("img"); img.crossOrigin = "Anonymous"; img.src = url; img.addEventListener("load", () => resolve(img)); img.addEventListener("error", () => reject(new URIError(`Fail to load ${url}.`))); }); } /** * @typedef {Object} ImageOptions * @extends ComponentOptions * @prop {String|Color} [fill=null] - Color used as background * @prop {String|Color} [tint=null] - Multiply the image pixels with a color * @prop {String} [description=""] - Description of the image (can be used to for better accessibility) */ /** * @type {ImageOptions} */ static get defaultOptions () { return { ...super.defaultOptions, fill: null, tint: null, description: "", }; } }