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: "",
};
}
}