js-draw
Version:
Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript.
157 lines (156 loc) • 5.44 kB
JavaScript
import { Rect2, Mat33 } from '@js-draw/math';
import { assertIsNumber, assertIsNumberArray } from '../util/assertions.mjs';
import AbstractComponent from './AbstractComponent.mjs';
import waitForImageLoaded from '../util/waitForImageLoaded.mjs';
/**
* Represents a raster image.
*
* **Example: Adding images**:
* [[include:doc-pages/inline-examples/adding-an-image-and-data-urls.md]]
*/
export default class ImageComponent extends AbstractComponent {
constructor(image) {
super('image-component');
this.image = {
...image,
label: image.label ??
image.image.getAttribute('alt') ??
image.image.getAttribute('aria-label') ??
undefined,
};
const isHTMLImageElem = (elem) => {
return elem.getAttribute('src') !== undefined;
};
if (isHTMLImageElem(image.image) && !image.image.complete) {
image.image.onload = () => this.recomputeBBox();
}
this.recomputeBBox();
}
getImageRect() {
return new Rect2(0, 0, this.image.image.width, this.image.image.height);
}
recomputeBBox() {
this.contentBBox = this.getImageRect();
this.contentBBox = this.contentBBox.transformedBoundingBox(this.image.transform);
}
/**
* Load from an image. Waits for the image to load if incomplete.
*
* The image, `elem`, must not [taint](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image#security_and_tainted_canvases)
* an HTMLCanvasElement when rendered.
*/
static async fromImage(elem, transform) {
await waitForImageLoaded(elem);
let width, height;
if (typeof elem.width === 'number' &&
typeof elem.height === 'number' &&
elem.width !== 0 &&
elem.height !== 0) {
width = elem.width;
height = elem.height;
}
else {
width = elem.clientWidth;
height = elem.clientHeight;
}
let image;
let url = elem.src ?? '';
if (!url.startsWith('data:image/')) {
// Convert to a data URL:
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(elem, 0, 0, canvas.width, canvas.height);
url = canvas.toDataURL();
image = canvas;
}
else {
image = new Image();
image.src = url;
image.width = width;
image.height = height;
}
image.setAttribute('alt', elem.getAttribute('alt') ?? '');
image.setAttribute('aria-label', elem.getAttribute('aria-label') ?? '');
return new ImageComponent({
image,
base64Url: url,
transform: transform,
});
}
render(canvas, _visibleRect) {
canvas.startObject(this.contentBBox);
canvas.drawImage(this.image);
canvas.endObject(this.getLoadSaveData());
}
// A *very* rough estimate of how long it takes to render this component
getProportionalRenderingTime() {
// Estimate: Equivalent to a stroke with 10 segments.
return 10;
}
intersects(lineSegment) {
const rect = this.getImageRect();
const edges = rect.getEdges().map((edge) => edge.transformedBy(this.image.transform));
for (const edge of edges) {
if (edge.intersects(lineSegment)) {
return true;
}
}
return false;
}
applyTransformation(affineTransfm) {
this.image.transform = affineTransfm.rightMul(this.image.transform);
this.recomputeBBox();
}
description(localizationTable) {
return this.image.label
? localizationTable.imageNode(this.image.label)
: localizationTable.unlabeledImageNode;
}
getAltText() {
return this.image.label;
}
// The base64 image URL of this image.
getURL() {
return this.image.base64Url;
}
getTransformation() {
return this.image.transform;
}
createClone() {
return new ImageComponent({
...this.image,
});
}
serializeToJSON() {
return {
src: this.image.base64Url,
label: this.image.label,
// Store the width and height for bounding box computations while the image is loading.
width: this.image.image.width,
height: this.image.image.height,
transform: this.image.transform.toArray(),
};
}
static deserializeFromJSON(data) {
if (!(typeof data.src === 'string')) {
throw new Error(`${data} has invalid format! Expected src property.`);
}
assertIsNumberArray(data.transform);
assertIsNumber(data.width);
assertIsNumber(data.height);
const image = new Image();
image.src = data.src;
image.width = data.width;
image.height = data.height;
const transform = new Mat33(...data.transform);
return new ImageComponent({
image: image,
base64Url: data.src,
label: data.label,
transform,
});
}
}
AbstractComponent.registerComponent('image-component', ImageComponent.deserializeFromJSON);