stage-js
Version:
2D HTML5 Rendering and Layout
665 lines (538 loc) • 15.3 kB
text/typescript
import { Matrix, Vec2Value } from "../common/matrix";
import { uid } from "../common/uid";
import type { Component } from "./component";
/**
* @hidden @deprecated
* - 'in-pad': same as 'contain'
* - 'in': similar to 'contain' without centering
* - 'out-crop': same as 'cover'
* - 'out': similar to 'cover' without centering
*/
export type LegacyFitMode = "in" | "out" | "out-crop" | "in-pad";
/**
* - 'contain': contain within the provided space, maintain aspect ratio
* - 'cover': cover the provided space, maintain aspect ratio
* - 'fill': fill provided space without maintaining aspect ratio
*/
export type FitMode = "contain" | "cover" | "fill" | LegacyFitMode;
/** @internal */
export function isValidFitMode(value: string) {
return (
value &&
(value === "cover" ||
value === "contain" ||
value === "fill" ||
value === "in" ||
value === "in-pad" ||
value === "out" ||
value === "out-crop")
);
}
/** @internal */ let iid = 0;
/** @hidden */
export interface Pinned {
pin(pin: object): this;
pin(key: string, value: any): this;
pin(key: string): any;
size(w: number, h: number): this;
width(): number;
width(w: number): this;
height(): number;
height(h: number): this;
offset(a: Vec2Value): this;
offset(a: number, b: number): this;
rotate(a: number): this;
skew(a: Vec2Value): this;
skew(a: number, b: number): this;
scale(a: Vec2Value): this;
scale(a: number, b: number): this;
alpha(a: number, ta?: number): this;
}
export class Pin {
/** @internal */ uid = "pin:" + uid();
/** @internal */ _owner: Component;
// todo: maybe this should be a getter instead?
/** @internal */ _parent: Pin | null;
/** @internal */ _relativeMatrix: Matrix;
/** @internal */ _absoluteMatrix: Matrix;
/** @internal */ _x: number;
/** @internal */ _y: number;
/** @internal */ _unscaled_width: number;
/** @internal */ _unscaled_height: number;
/** @internal */ _width: number;
/** @internal */ _height: number;
/** @internal */ _textureAlpha: number;
/** @internal */ _alpha: number;
/** @internal */ _scaleX: number;
/** @internal */ _scaleY: number;
/** @internal */ _skewX: number;
/** @internal */ _skewY: number;
/** @internal */ _rotation: number;
/** @internal */ _pivoted: boolean;
/** @internal */ _pivotX: number;
/** @internal */ _pivotY: number;
/** @internal */ _handled: boolean;
/** @internal */ _handleX: number;
/** @internal */ _handleY: number;
/** @internal */ _aligned: boolean;
/** @internal */ _alignX: number;
/** @internal */ _alignY: number;
/** @internal */ _offsetX: number;
/** @internal */ _offsetY: number;
/** @internal */ _boxX: number;
/** @internal */ _boxY: number;
/** @internal */ _boxWidth: number;
/** @internal */ _boxHeight: number;
/** @internal */ _ts_transform: number;
/** @internal */ _ts_translate: number;
/** @internal */ _ts_matrix: number;
/** @internal */ _mo_handle: number;
/** @internal */ _mo_align: number;
/** @internal */ _mo_abs: number;
/** @internal */ _mo_rel: number;
/** @internal */ _directionX = 1;
/** @internal */ _directionY = 1;
/** @internal */
constructor(owner: Component) {
this._owner = owner;
this._parent = null;
// relative to parent
this._relativeMatrix = new Matrix();
// relative to stage
this._absoluteMatrix = new Matrix();
this.reset();
}
reset() {
this._textureAlpha = 1;
this._alpha = 1;
this._width = 0;
this._height = 0;
this._scaleX = 1;
this._scaleY = 1;
this._skewX = 0;
this._skewY = 0;
this._rotation = 0;
// scale/skew/rotate center
this._pivoted = false;
// todo: this used to be null
this._pivotX = 0;
this._pivotY = 0;
// self pin point
this._handled = false;
this._handleX = 0;
this._handleY = 0;
// parent pin point
this._aligned = false;
this._alignX = 0;
this._alignY = 0;
// as seen by parent px
this._offsetX = 0;
this._offsetY = 0;
this._boxX = 0;
this._boxY = 0;
this._boxWidth = this._width;
this._boxHeight = this._height;
// TODO: also set for owner
this._ts_translate = ++iid;
this._ts_transform = ++iid;
this._ts_matrix = ++iid;
}
/** @internal */
_update() {
this._parent = this._owner._parent && this._owner._parent._pin;
// if handled and transformed then be translated
if (this._handled && this._mo_handle != this._ts_transform) {
this._mo_handle = this._ts_transform;
this._ts_translate = ++iid;
}
if (this._aligned && this._parent && this._mo_align != this._parent._ts_transform) {
this._mo_align = this._parent._ts_transform;
this._ts_translate = ++iid;
}
return this;
}
toString() {
return this._owner + " (" + (this._parent ? this._parent._owner : null) + ")";
}
// TODO: ts fields require refactoring
absoluteMatrix() {
this._update();
const ts = Math.max(
this._ts_transform,
this._ts_translate,
this._parent ? this._parent._ts_matrix : 0,
);
if (this._mo_abs == ts) {
return this._absoluteMatrix;
}
this._mo_abs = ts;
const abs = this._absoluteMatrix;
abs.reset(this.relativeMatrix());
this._parent && abs.concat(this._parent._absoluteMatrix);
this._ts_matrix = ++iid;
return abs;
}
relativeMatrix() {
this._update();
const ts = Math.max(
this._ts_transform,
this._ts_translate,
this._parent ? this._parent._ts_transform : 0,
);
if (this._mo_rel == ts) {
return this._relativeMatrix;
}
this._mo_rel = ts;
const rel = this._relativeMatrix;
rel.identity();
if (this._pivoted) {
rel.translate(-this._pivotX * this._width, -this._pivotY * this._height);
}
rel.scale(this._scaleX * this._directionX, this._scaleY * this._directionY);
rel.skew(this._skewX, this._skewY);
rel.rotate(this._rotation);
if (this._pivoted) {
rel.translate(this._pivotX * this._width, this._pivotY * this._height);
}
// calculate effective box
if (this._pivoted) {
// origin
this._boxX = 0;
this._boxY = 0;
this._boxWidth = this._width;
this._boxHeight = this._height;
} else {
// aabb
let p;
let q;
if ((rel.a > 0 && rel.c > 0) || (rel.a < 0 && rel.c < 0)) {
p = 0;
q = rel.a * this._width + rel.c * this._height;
} else {
p = rel.a * this._width;
q = rel.c * this._height;
}
if (p > q) {
this._boxX = q;
this._boxWidth = p - q;
} else {
this._boxX = p;
this._boxWidth = q - p;
}
if ((rel.b > 0 && rel.d > 0) || (rel.b < 0 && rel.d < 0)) {
p = 0;
q = rel.b * this._width + rel.d * this._height;
} else {
p = rel.b * this._width;
q = rel.d * this._height;
}
if (p > q) {
this._boxY = q;
this._boxHeight = p - q;
} else {
this._boxY = p;
this._boxHeight = q - p;
}
}
this._x = this._offsetX;
this._y = this._offsetY;
this._x -= this._boxX + this._handleX * this._boxWidth * this._directionX;
this._y -= this._boxY + this._handleY * this._boxHeight * this._directionY;
if (this._aligned && this._parent) {
this._parent.relativeMatrix();
this._x += this._alignX * this._parent._width;
this._y += this._alignY * this._parent._height;
}
rel.translate(this._x, this._y);
return this._relativeMatrix;
}
/** @internal */
get(key: string) {
if (typeof getters[key] === "function") {
return getters[key](this);
}
}
// TODO: Use defineProperty instead? What about multi-field pinning?
/** @internal */
set(a, b?) {
if (typeof a === "string") {
if (typeof setters[a] === "function" && typeof b !== "undefined") {
setters[a](this, b);
}
} else if (typeof a === "object") {
for (b in a) {
if (typeof setters[b] === "function" && typeof a[b] !== "undefined") {
setters[b](this, a[b], a);
}
}
}
if (this._owner) {
this._owner._ts_pin = ++iid;
this._owner.touch();
}
return this;
}
// todo: should this be public?
/** @internal */
fit(width: number | null, height: number | null, mode?: FitMode) {
this._ts_transform = ++iid;
if (mode === "contain") {
mode = "in-pad";
}
if (mode === "cover") {
mode = "out-crop";
}
if (typeof width === "number") {
this._scaleX = width / this._unscaled_width;
this._width = this._unscaled_width;
}
if (typeof height === "number") {
this._scaleY = height / this._unscaled_height;
this._height = this._unscaled_height;
}
if (typeof width === "number" && typeof height === "number" && typeof mode === "string") {
if (mode === "fill") {
} else if (mode === "out" || mode === "out-crop") {
this._scaleX = this._scaleY = Math.max(this._scaleX, this._scaleY);
} else if (mode === "in" || mode === "in-pad") {
this._scaleX = this._scaleY = Math.min(this._scaleX, this._scaleY);
}
if (mode === "out-crop" || mode === "in-pad") {
this._width = width / this._scaleX;
this._height = height / this._scaleY;
}
}
}
}
/** @internal */ const getters = {
alpha: function (pin: Pin) {
return pin._alpha;
},
textureAlpha: function (pin: Pin) {
return pin._textureAlpha;
},
width: function (pin: Pin) {
return pin._width;
},
height: function (pin: Pin) {
return pin._height;
},
boxWidth: function (pin: Pin) {
return pin._boxWidth;
},
boxHeight: function (pin: Pin) {
return pin._boxHeight;
},
// scale : function(pin: Pin) {
// },
scaleX: function (pin: Pin) {
return pin._scaleX;
},
scaleY: function (pin: Pin) {
return pin._scaleY;
},
// skew : function(pin: Pin) {
// },
skewX: function (pin: Pin) {
return pin._skewX;
},
skewY: function (pin: Pin) {
return pin._skewY;
},
rotation: function (pin: Pin) {
return pin._rotation;
},
// pivot : function(pin: Pin) {
// },
pivotX: function (pin: Pin) {
return pin._pivotX;
},
pivotY: function (pin: Pin) {
return pin._pivotY;
},
// offset : function(pin: Pin) {
// },
offsetX: function (pin: Pin) {
return pin._offsetX;
},
offsetY: function (pin: Pin) {
return pin._offsetY;
},
// align : function(pin: Pin) {
// },
alignX: function (pin: Pin) {
return pin._alignX;
},
alignY: function (pin: Pin) {
return pin._alignY;
},
// handle : function(pin: Pin) {
// },
handleX: function (pin: Pin) {
return pin._handleX;
},
handleY: function (pin: Pin) {
return pin._handleY;
},
};
type ResizeParams = {
resizeMode: FitMode;
resizeWidth: number;
resizeHeight: number;
};
type ScaleParams = {
scaleMode: FitMode;
scaleWidth: number;
scaleHeight: number;
};
/** @internal */ const setters = {
alpha: function (pin: Pin, value: number) {
pin._alpha = value;
},
textureAlpha: function (pin: Pin, value: number) {
pin._textureAlpha = value;
},
width: function (pin: Pin, value: number) {
pin._unscaled_width = value;
pin._width = value;
pin._ts_transform = ++iid;
},
height: function (pin: Pin, value: number) {
pin._unscaled_height = value;
pin._height = value;
pin._ts_transform = ++iid;
},
scale: function (pin: Pin, value: number) {
pin._scaleX = value;
pin._scaleY = value;
pin._ts_transform = ++iid;
},
scaleX: function (pin: Pin, value: number) {
pin._scaleX = value;
pin._ts_transform = ++iid;
},
scaleY: function (pin: Pin, value: number) {
pin._scaleY = value;
pin._ts_transform = ++iid;
},
skew: function (pin: Pin, value: number) {
pin._skewX = value;
pin._skewY = value;
pin._ts_transform = ++iid;
},
skewX: function (pin: Pin, value: number) {
pin._skewX = value;
pin._ts_transform = ++iid;
},
skewY: function (pin: Pin, value: number) {
pin._skewY = value;
pin._ts_transform = ++iid;
},
rotation: function (pin: Pin, value: number) {
pin._rotation = value;
pin._ts_transform = ++iid;
},
pivot: function (pin: Pin, value: number) {
pin._pivotX = value;
pin._pivotY = value;
pin._pivoted = true;
pin._ts_transform = ++iid;
},
pivotX: function (pin: Pin, value: number) {
pin._pivotX = value;
pin._pivoted = true;
pin._ts_transform = ++iid;
},
pivotY: function (pin: Pin, value: number) {
pin._pivotY = value;
pin._pivoted = true;
pin._ts_transform = ++iid;
},
offset: function (pin: Pin, value: number) {
pin._offsetX = value;
pin._offsetY = value;
pin._ts_translate = ++iid;
},
offsetX: function (pin: Pin, value: number) {
pin._offsetX = value;
pin._ts_translate = ++iid;
},
offsetY: function (pin: Pin, value: number) {
pin._offsetY = value;
pin._ts_translate = ++iid;
},
align: function (pin: Pin, value: number) {
this.alignX(pin, value);
this.alignY(pin, value);
},
alignX: function (pin: Pin, value: number) {
pin._alignX = value;
pin._aligned = true;
pin._ts_translate = ++iid;
this.handleX(pin, value);
},
alignY: function (pin: Pin, value: number) {
pin._alignY = value;
pin._aligned = true;
pin._ts_translate = ++iid;
this.handleY(pin, value);
},
handle: function (pin: Pin, value: number) {
this.handleX(pin, value);
this.handleY(pin, value);
},
handleX: function (pin: Pin, value: number) {
pin._handleX = value;
pin._handled = true;
pin._ts_translate = ++iid;
},
handleY: function (pin: Pin, value: number) {
pin._handleY = value;
pin._handled = true;
pin._ts_translate = ++iid;
},
resizeMode: function (pin: Pin, value: FitMode, all: ResizeParams) {
if (all) {
if (value == "in") {
value = "in-pad";
} else if (value == "out") {
value = "out-crop";
}
pin.fit(all.resizeWidth, all.resizeHeight, value);
}
},
resizeWidth: function (pin: Pin, value: number, all: ResizeParams) {
if (!all || !all.resizeMode) {
pin.fit(value, null);
}
},
resizeHeight: function (pin: Pin, value: number, all: ResizeParams) {
if (!all || !all.resizeMode) {
pin.fit(null, value);
}
},
scaleMode: function (pin: Pin, value: FitMode, all: ScaleParams) {
if (all) {
pin.fit(all.scaleWidth, all.scaleHeight, value);
}
},
scaleWidth: function (pin: Pin, value: number, all: ScaleParams) {
if (!all || !all.scaleMode) {
pin.fit(value, null);
}
},
scaleHeight: function (pin: Pin, value: number, all: ScaleParams) {
if (!all || !all.scaleMode) {
pin.fit(null, value);
}
},
matrix: function (pin: Pin, value: Matrix) {
this.scaleX(pin, value.a);
this.skewX(pin, value.c / value.d);
this.skewY(pin, value.b / value.a);
this.scaleY(pin, value.d);
this.offsetX(pin, value.e);
this.offsetY(pin, value.f);
this.rotation(pin, 0);
},
};