lazy-widgets
Version:
Typescript retained mode GUI for the HTML canvas API
282 lines • 11.8 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { damageField, layoutField, damageLayoutArrayField } from '../decorators/FlagFields.js';
import { Widget } from './Widget.js';
import { DynMsg } from '../core/Strings.js';
import { urlToBackingMediaSource } from '../helpers/BackingMediaSource.js';
import { BackingMediaWrapper } from '../helpers/BackingMediaWrapper.js';
import { BackingMediaEventType } from '../helpers/BackingMediaEventType.js';
// TODO rename this to MediaFit
/**
* The image fitting mode for {@link Icon} widgets; describes how an image is
* transformed if its dimensions don't match the output dimensions.
*
* @category Widget
*/
export var IconFit;
(function (IconFit) {
/**
* The image will be scaled down or up such that at least an axis of the
* image has the same dimensions as the widget, and the entire image is
* visible, preserving the aspect ratio of the image. The default image
* fitting mode.
*/
IconFit[IconFit["Contain"] = 0] = "Contain";
/**
* Similar to {@link IconFit.Contain}, except parts of the image can be cut
* off so that all parts of the widget are covered by the image.
*/
IconFit[IconFit["Cover"] = 1] = "Cover";
/**
* The image will be forced to have the same size as the widget by
* stretching or squishing it.
*/
IconFit[IconFit["Fill"] = 2] = "Fill";
})(IconFit || (IconFit = {}));
// TODO rename this to Media, and all related properties
/**
* A widget which displays a given image.
*
* @category Widget
*/
export class Icon extends Widget {
constructor(image, properties) {
var _a, _b, _c, _d, _e, _f;
super(properties);
/** Has the user already been warned about the broken media? */
this.warnedBroken = false;
/** The current media rotation in radians. */
this.rotation = 0;
/**
* The wanted width. If null, the media's width will be used, taking
* {@link Icon#viewBox} into account.
*/
this.imageWidth = null;
/**
* The wanted height. If null, the media's height will be used, taking
* {@link Icon#viewBox} into account.
*/
this.imageHeight = null;
/** Horizontal offset. */
this.offsetX = 0;
/** Vertical offset. */
this.offsetY = 0;
/** Actual image width. */
this.actualWidth = 0;
/** Actual image height. */
this.actualHeight = 0;
this.onMediaEvent = (ev) => {
switch (ev) {
case BackingMediaEventType.Dirty:
this.markWholeAsDirty();
break;
case BackingMediaEventType.Resized:
this._layoutDirty = true;
break;
}
};
if (typeof image === 'string') {
image = urlToBackingMediaSource(image)[0];
}
this._media = image === null ? null : new BackingMediaWrapper(image);
this.rotation = (_a = properties === null || properties === void 0 ? void 0 : properties.rotation) !== null && _a !== void 0 ? _a : 0;
this.viewBox = (_b = properties === null || properties === void 0 ? void 0 : properties.viewBox) !== null && _b !== void 0 ? _b : null;
this.imageWidth = (_c = properties === null || properties === void 0 ? void 0 : properties.width) !== null && _c !== void 0 ? _c : null;
this.imageHeight = (_d = properties === null || properties === void 0 ? void 0 : properties.height) !== null && _d !== void 0 ? _d : null;
this.fit = (_e = properties === null || properties === void 0 ? void 0 : properties.fit) !== null && _e !== void 0 ? _e : IconFit.Contain;
this.mediaPadding = (_f = properties === null || properties === void 0 ? void 0 : properties.mediaPadding) !== null && _f !== void 0 ? _f : { left: 0, right: 0, top: 0, bottom: 0 };
}
handleAttachment() {
if (this._media) {
this._media.addEventListener(this.onMediaEvent);
}
}
handleDetachment() {
if (this._media) {
this._media.removeEventListener(this.onMediaEvent);
}
}
/**
* The image or video used by this Icon. Sets {@link Icon#_media} if
* changed.
*
* If getting, returns {@link Icon#_media}.
*/
set image(image) {
if (image !== this._media) {
if (this._media && this.attached) {
this._media.removeEventListener(this.onMediaEvent);
}
this._media = image === null ? null : new BackingMediaWrapper(image);
if (this._media && this.attached) {
this._media.addEventListener(this.onMediaEvent);
}
}
}
get image() {
return this._media === null ? null : this._media.source;
}
handleResolveDimensions(minWidth, maxWidth, minHeight, maxHeight) {
const pad = this.mediaPadding;
const hPad = pad.left + pad.right;
const vPad = pad.top + pad.bottom;
// Find dimensions
let wantedWidth = this.imageWidth;
if (wantedWidth === null) {
if (this._media === null) {
wantedWidth = 0;
}
else if (this.viewBox === null) {
wantedWidth = this._media.width;
}
else {
wantedWidth = this.viewBox[2];
}
}
this.idealWidth = Math.max(Math.min(wantedWidth + hPad, maxWidth), minWidth);
let idealWidthNoPad = Math.max(this.idealWidth - hPad, 0);
let wantedHeight = this.imageHeight;
if (wantedHeight === null) {
if (this._media === null) {
wantedHeight = 0;
}
else if (this.viewBox === null) {
wantedHeight = this._media.height;
}
else {
wantedHeight = this.viewBox[3];
}
}
this.idealHeight = Math.max(Math.min(wantedHeight + vPad, maxHeight), minHeight);
let idealHeightNoPad = Math.max(this.idealHeight - vPad, 0);
// Find offset and actual image dimensions (preserving aspect ratio)
switch (this.fit) {
case IconFit.Contain:
case IconFit.Cover:
{
if (this._media === null) {
// XXX fallback for no media
this.actualWidth = idealWidthNoPad;
this.actualHeight = idealHeightNoPad;
}
else {
const widthRatio = wantedWidth === 0 ? 0 : idealWidthNoPad / wantedWidth;
const heightRatio = wantedHeight === 0 ? 0 : idealHeightNoPad / wantedHeight;
let scale;
if (this.fit === IconFit.Contain) {
scale = Math.min(widthRatio, heightRatio);
}
else {
scale = Math.max(widthRatio, heightRatio);
}
this.actualWidth = wantedWidth * scale;
this.actualHeight = wantedHeight * scale;
}
if (this.fit === IconFit.Contain) {
this.idealWidth = Math.max(Math.min(this.actualWidth + hPad, maxWidth), minWidth);
this.idealHeight = Math.max(Math.min(this.actualHeight + vPad, maxHeight), minHeight);
idealWidthNoPad = Math.max(this.idealWidth - hPad, 0);
idealHeightNoPad = Math.max(this.idealHeight - vPad, 0);
}
this.offsetX = Math.round((idealWidthNoPad - this.actualWidth) / 2 + pad.left);
this.offsetY = Math.round((idealHeightNoPad - this.actualHeight) / 2 + pad.top);
break;
}
case IconFit.Fill:
this.actualWidth = idealWidthNoPad;
this.actualHeight = idealHeightNoPad;
this.offsetX = pad.left;
this.offsetY = pad.top;
break;
default:
throw new Error(DynMsg.INVALID_ENUM(this.fit, 'IconFit', 'fit'));
}
}
handlePainting(_dirtyRects) {
let actualImage = this._media ? this._media.canvasImageSource : null;
// Translate, rotate and clip if rotation is not 0
let tdx = this.x + this.offsetX, tdy = this.y + this.offsetY;
const rotated = this.rotation !== 0;
const needsClip = rotated || this.fit === IconFit.Cover;
const ctx = this.viewport.context;
if (needsClip) {
ctx.save();
ctx.beginPath();
ctx.rect(this.x, this.y, this.width, this.height);
ctx.clip();
if (rotated) {
ctx.translate(this.x + this.offsetX + this.actualWidth / 2, this.y + this.offsetY + this.actualHeight / 2);
tdx = -this.actualWidth / 2;
tdy = -this.actualHeight / 2;
ctx.rotate(this.rotation);
}
}
// Draw image, with viewBox if it is not null
if (actualImage) {
try {
if (this.viewBox === null) {
ctx.drawImage(actualImage, tdx, tdy, this.actualWidth, this.actualHeight);
}
else {
ctx.drawImage(actualImage, ...this.viewBox, tdx, tdy, this.actualWidth, this.actualHeight);
}
}
catch (err) {
// HACK even though complete is true, the image might be in a broken
// state, which is not easy to detect. to prevent a crash, catch the
// exception and log as a warning
if (!this.warnedBroken) {
this.warnedBroken = true;
console.error(err);
console.warn("Failed to paint image to Icon widget. Are you using an invalid URL? This warning won't be shown again");
}
actualImage = null;
}
}
// Draw fallback colour
if (!actualImage) {
const fill = this.mediaFallbackFill;
if (fill !== 'transparent') {
ctx.fillStyle = fill;
ctx.fillRect(tdx, tdy, this.actualWidth, this.actualHeight);
}
}
// Revert transformation
if (needsClip) {
ctx.restore();
}
}
}
Icon.autoXML = {
name: 'icon',
inputConfig: [
{
mode: 'value',
name: 'image',
validator: 'nullable:image-source'
}
]
};
__decorate([
damageField
], Icon.prototype, "rotation", void 0);
__decorate([
damageLayoutArrayField(true)
], Icon.prototype, "viewBox", void 0);
__decorate([
layoutField
], Icon.prototype, "imageWidth", void 0);
__decorate([
layoutField
], Icon.prototype, "imageHeight", void 0);
__decorate([
damageField
], Icon.prototype, "fit", void 0);
__decorate([
layoutField
], Icon.prototype, "mediaPadding", void 0);
//# sourceMappingURL=Icon.js.map