simscript
Version:
A Discrete Event Simulation Library in TypeScript
524 lines (523 loc) • 19.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Animation = void 0;
const queue_1 = require("./queue");
const entity_1 = require("./entity");
const util_1 = require("./util");
const _DEFAULT_ENTITY_ICON = '⚫', _DEFAULT_SPLINE_TENSION = 1;
class Animation {
constructor(sim, animationHost, options) {
this._queues = new Map();
this._disabled = false;
this._rotateEntities = false;
this._toQueueEnd = true;
this._entities = new Map();
this._host = util_1.getElement(animationHost);
this._scene = this._host;
if (this.hostTag == 'X3D') {
this._scene = this._host.querySelector('scene');
const x3dom = window['x3dom'];
if (x3dom != null && !animationHost.offsetHeight) {
requestAnimationFrame(() => x3dom.reload());
}
}
this._sim = sim;
sim.timeNowChanged.addEventListener(this.updateDisplay, this);
sim.stateChanged.addEventListener(this.updateDisplay, this);
sim.yieldInterval = 30;
this._lastUpdate = 0;
this._entities = new Map();
if (options) {
util_1.setOptions(this, options);
}
}
get hostElement() {
return this._host;
}
get hostTag() {
return this._host.tagName.toUpperCase();
}
get isThreeD() {
const tag = this.hostTag;
return tag == 'X3D' || tag == 'A-SCENE';
}
get sceneElement() {
return this._scene;
}
get getEntityHtml() {
return this._getEntityHtml;
}
set getEntityHtml(value) {
this._getEntityHtml = value;
}
get updateEntityElement() {
return this._updateEntityElement;
}
set updateEntityElement(value) {
this._updateEntityElement = value;
}
get rotateEntities() {
return this._rotateEntities;
}
set rotateEntities(value) {
this._rotateEntities = value;
}
get animateToQueueEnd() {
return this._toQueueEnd;
}
set animateToQueueEnd(value) {
this._toQueueEnd = value;
}
get disabled() {
return this._disabled;
}
set disabled(value) {
if (value != this.disabled) {
this._disabled = value;
if (!value) {
this.updateDisplay();
}
}
}
get queues() {
return this._queueArray;
}
set queues(value) {
this._queueArray = value;
value.forEach(item => {
let aq = new AnimatedQueue(this, item);
this._queues.set(aq._q, aq);
});
if (this._lastUpdate) {
this._lastUpdate = -1;
this.updateDisplay();
}
}
updateDisplay() {
if (this._disabled) {
return;
}
const host = this._host, sim = this._sim, rc = host.getBoundingClientRect();
if (rc.height != this._height || rc.width != this._width) {
this._height = rc.height;
this._width = rc.width;
for (let aq of this._queues.values()) {
aq._ptStart = null;
}
}
this._entities.forEach(ae => {
ae._inUse = false;
});
for (let aq of this._queues.values()) {
aq._draw();
}
sim._fec.forEach(item => {
if (item.options.path && item.timeDue != null) {
const ae = this._getAnimatedEntity(item.e);
const path = item.options.path, tension = path.tension != null ? path.tension : _DEFAULT_SPLINE_TENSION, radius = path.radius, points = [];
path.queues.forEach((q, index) => {
const aq = this._queues.get(q);
util_1.assert(aq != null, 'Queue missing animation info');
points.push(this._toQueueEnd && path.queues.length == 2 && index > 0
? aq._ptEnd || aq._getStart()
: aq._getStart());
});
if (radius) {
applyRadius(points, radius);
}
ae.updateIcon();
const start = item.timeStart, finish = item.timeDue, pct = 1 - (finish - this._sim.timeNow) / (finish - start), [pos, angle] = interpolatePath(points, tension, pct);
ae._drawAt(pos, this.rotateEntities ? angle : 0);
}
});
this._entities.forEach((ae, key) => {
if (!ae._inUse) {
this._entities.delete(key);
let e = ae._element;
if (e && e.parentElement) {
e.remove();
}
}
});
this._lastUpdate = sim.timeNow;
}
_getAnimatedEntity(e) {
let ae = this._entities.get(e);
if (!ae) {
ae = new AnimatedEntity(this, e);
this._entities.set(e, ae);
}
return ae;
}
}
exports.Animation = Animation;
class AnimatedQueue {
constructor(anim, options) {
this._angle = 0;
this._pt1 = new util_1.Point();
this._pt2 = new util_1.Point();
this._anim = anim;
this._q = options.queue;
this._max = options.max;
this._element = util_1.getElement(options.element);
this._elementEnd = options.endElement ? util_1.getElement(options.endElement) : null;
this._angle = (options.angle || 0);
this._stackEntities = options.stackEntities;
util_1.assert(this._q instanceof queue_1.Queue, 'q parameter should be a Queue');
}
_getStart() {
if (!this._ptStart) {
this._ptStart = this._getElementPosition(this._element);
}
return this._ptStart;
}
_getEnd() {
if (!this._ptEnd) {
this._ptEnd = this._getElementPosition(this._elementEnd || this._element);
}
return this._ptEnd;
}
_getElementPosition(e) {
const anim = this._anim;
switch (anim.hostTag) {
case 'X3D':
return new BoundingBox(e).center;
case 'A-SCENE':
return util_1.Point.clone(e.object3D.position);
case 'SVG':
const rcSvg = e.getBBox();
return new util_1.Point(rcSvg.x + rcSvg.width / 2, rcSvg.y + rcSvg.height / 2, 0);
default:
const rcHost = anim.hostElement.getBoundingClientRect(), rcEl = e.getBoundingClientRect();
return new util_1.Point(rcEl.left - rcHost.left + rcEl.width / 2, rcEl.top - rcHost.top + rcEl.height / 2, 0);
}
}
_draw() {
const anim = this._anim, q = this._q;
if (!q.pop) {
this._ptEnd = null;
return;
}
if (!this._customPositions && q.lastChange < anim._lastUpdate && anim.hostTag != 'A-SCENE') {
for (let item of this._q.items.values()) {
const ae = anim._entities.get(item.entity);
if (ae) {
ae._inUse = true;
}
}
return;
}
let pt = util_1.Point.clone(this._getStart());
this._ptEnd = null;
let cnt = 0;
for (let item of q.items.values()) {
if (this._max != null && cnt >= this._max) {
break;
}
const e = item.entity, ae = anim._getAnimatedEntity(e);
ae.updateIcon();
const getPos = e.getAnimationPosition;
if (getPos != entity_1.Entity.prototype.getAnimationPosition) {
const start = util_1.Point.copy(this._pt1, this._getStart()), end = util_1.Point.copy(this._pt2, this._getEnd()), pos = getPos.call(e, q, start, end);
if (pos != null) {
this._customPositions = true;
ae._drawAt(pos.position, pos.angle);
continue;
}
}
const angle = this._angle * (anim.isThreeD ? -1 : +1), rad = -angle / 180 * Math.PI, sin = Math.sin(rad), cos = -Math.cos(rad);
const stack = this._stackEntities, hWid = stack ? 0 : ae._sz.x * cos / 2, hHei = stack ? 0 : (anim.rotateEntities ? ae._sz.x : ae._sz.y) * sin / 2;
pt.x += hWid;
pt.y += hHei;
ae._drawAt(pt, angle);
pt.x += hWid;
pt.y += hHei;
cnt++;
}
this._ptEnd = pt;
}
}
class AnimatedEntity {
constructor(anim, entity) {
this._anim = anim;
this._entity = entity;
this._inUse = false;
let e;
switch (anim.hostTag) {
case 'X3D':
e = document.createElement('transform');
break;
case 'A-SCENE':
e = document.createElement('a-entity');
break;
case 'SVG':
e = document.createElementNS('http://www.w3.org/2000/svg', 'g');
e.style.opacity = '0';
break;
default:
e = document.createElement('div');
e.style.opacity = '0';
break;
}
e.classList.add('ss-entity');
this._element = e;
this.updateIcon(true);
anim.sceneElement.appendChild(e);
switch (anim.hostTag) {
case 'X3D':
const sz = new BoundingBox(e).size;
this._sz = {
x: sz.x,
y: sz.y,
z: sz.z
};
break;
case 'A-SCENE':
this._sz = new util_1.Point();
requestAnimationFrame(() => {
const model = e.object3D, box = new THREE.Box3().setFromObject(model);
this._sz = {
x: box.max.x - box.min.x,
y: box.max.y - box.min.y,
z: box.max.z - box.min.z
};
});
break;
case 'SVG':
{
const rc = e.getBBox();
this._sz = {
x: rc.width,
y: rc.height
};
this._offset = {
x: rc.x + rc.width / 2,
y: rc.y + rc.height / 2
};
}
break;
default:
{
const rc = e.getBoundingClientRect();
this._sz = {
x: rc.width,
y: rc.height
};
this._offset = {
x: rc.width / 2,
y: rc.height / 2
};
}
break;
}
}
_getEntityHtml() {
const getEntity = this._anim.getEntityHtml;
return getEntity
? getEntity(this._entity)
: _DEFAULT_ENTITY_ICON;
}
updateIcon(creating) {
const updateFn = creating ? null : this._anim.updateEntityElement;
if (updateFn) {
updateFn(this._entity, this._element);
}
else {
const html = this._getEntityHtml();
if (html != this._html) {
this._element.innerHTML = this._html = html;
}
}
}
_drawAt(pt, angle) {
const anim = this._anim, e = this._element, s = e.style;
switch (anim.hostTag) {
case 'X3D':
e.setAttribute('translation', `${pt.x} ${pt.y} ${pt.z || 0}`);
e.setAttribute('rotation', `0 0 1 ${anim.rotateEntities ? angle / 180 * Math.PI : 0}`);
break;
case 'A-SCENE':
const model = e.object3D;
model.position.set(pt.x, pt.y, pt.z);
model.rotation.set(0, 0, angle && anim.rotateEntities ? angle / 180 * Math.PI : 0);
break;
case 'SVG':
default:
const p = new util_1.Point(pt.x - this._offset.x, pt.y - this._offset.y);
let transform = `translate(${Math.round(p.x)}px, ${Math.round(p.y)}px)`;
if (angle && anim.rotateEntities) {
transform += `rotate(${angle}deg) `;
}
s.transform = transform;
s.opacity = '';
break;
}
this._inUse = true;
}
}
function interpolatePath(pts, tension, t) {
const ptDist = util_1.Point.distance, ptAng = util_1.Point.angle, ptInter = util_1.Point.interpolate, getPoint = (pts, index) => pts[util_1.clamp(index, 0, pts.length - 1)];
t = util_1.clamp(t, 0, 1);
tension = util_1.clamp(tension, 0, 1);
if (pts.length < 3) {
return pts.length == 2
? [ptInter(pts[0], pts[1], t), util_1.Point.angle(pts[0], pts[1])]
: [pts[0], 0];
}
let len = 0;
for (let i = 0; i < pts.length - 1; i++) {
len += ptDist(pts[i], pts[i + 1]);
}
const pos = t * len;
let idx = -1;
let pctSeg = -1;
len = 0;
for (let i = 0; i < pts.length - 1; i++) {
const segLen = ptDist(pts[i], pts[i + 1]);
if (len + segLen >= pos) {
idx = i;
pctSeg = (pos - len) / segLen;
break;
}
len += segLen;
}
let p0 = pts[idx];
let p1 = getPoint(pts, idx + 1);
const ptLin = ptInter(p0, p1, pctSeg), angLin = ptAng(p0, p1);
if (tension == 0) {
return [ptLin, angLin];
}
let p2;
if (pctSeg >= .5) {
p1 = getPoint(pts, idx + 1);
p0 = ptInter(pts[idx], p1, .5);
p2 = ptInter(p1, getPoint(pts, idx + 2), .5);
}
else {
p1 = pts[idx];
p0 = ptInter(getPoint(pts, idx - 1), p1, .5);
p2 = ptInter(pts[idx], getPoint(pts, idx + 1), .5);
}
if ((idx == 0 && !ptDist(p0, p1)) || (idx == pts.length - 2 && !ptDist(p1, p2))) {
return [ptLin, angLin];
}
const d1 = ptDist(p0, p1), d2 = ptDist(p1, p2), posSpl = (pos - len) + d1 * (pctSeg >= .5 ? -1 : +1), pctSpl = posSpl / (d1 + d2);
const [ptSpl, angSpl] = interpolateSpline(p0, p1, p2, pctSpl);
return [
ptInter(ptLin, ptSpl, tension),
tension > 0.1 ? angSpl : angLin
];
}
function interpolateSpline(p0, p1, p2, t) {
const c0 = (1 - t) * (1 - t), c1 = 2 * (1 - t) * t, c2 = t * t, pt = {
x: c0 * p0.x + c1 * p1.x + c2 * p2.x,
y: c0 * p0.y + c1 * p1.y + c2 * p2.y,
z: c0 * p0.z + c1 * p1.z + c2 * p2.z
};
const a0 = 2 * (t - 1), a1 = 2 * (1 - 2 * t), a2 = 2 * t, dx = a0 * p0.x + a1 * p1.x + a2 * p2.x, dy = a0 * p0.y + a1 * p1.y + a2 * p2.y, ang = Math.atan2(dy, dx) * 180 / Math.PI;
return [pt, ang];
}
class BoundingBox {
constructor(e) {
this.center = new util_1.Point();
this.size = new util_1.Point();
const g = e.querySelectorAll('shape>:not(appearance)');
if (g.length == 0) {
this.applyGeometry(e);
this.applyTransforms(e);
}
else {
this.applyGeometry(g[0]);
this.applyTransforms(g[0]);
for (let i = 1; i < g.length; i++) {
this.merge(new BoundingBox(g[i]));
}
}
}
applyGeometry(e) {
const sz = this.size;
switch (e.tagName) {
case 'BOX':
sz.x = sz.y = sz.z = 2;
let atts = getAttributes(e, 'size');
if (atts && atts.length >= 3) {
sz.x = atts[0];
sz.y = atts[1];
sz.z = atts[2];
}
break;
case 'CONE':
sz.x = sz.z = 2 * Math.max(getAttribute(e, 'topRadius', 0), getAttribute(e, 'BottomRadius', 0));
sz.y = getAttribute(e, 'height', 0);
break;
case 'CYLINDER':
sz.x = sz.y = 2 * getAttribute(e, 'radius', 0);
sz.z = getAttribute(e, 'height', 0);
break;
case 'SPHERE':
sz.x = sz.y = sz.z = 2 * getAttribute(e, 'radius', 1);
break;
default:
console.error('skipping unknown geometry', e.tagName);
break;
}
}
applyTransforms(el) {
let e = el.closest('transform');
while (e != null) {
const t = getAttributes(e, 'translation');
if (t && t.length >= 3) {
const c = this.center;
c.x += t[0];
c.y += t[1];
c.z += t[2];
}
const s = getAttributes(e, 'scale');
if (s && s.length >= 3) {
const sz = this.size;
sz.x *= s[0];
sz.y *= s[1];
sz.z *= s[2];
}
const p = e.parentElement;
e = p ? p.closest('transform') : null;
}
}
merge(box) {
const c = this.center, sz = this.size, bc = box.center, bsz = box.size;
const min = new util_1.Point(Math.min(c.x - sz.x / 2, bc.x - bsz.x / 2), Math.min(c.y - sz.y / 2, bc.y - bsz.y / 2), Math.min(c.z - sz.z / 2, bc.z - bsz.z / 2));
const max = new util_1.Point(Math.max(c.x + sz.x / 2, bc.x + bsz.x / 2), Math.max(c.y + sz.y / 2, bc.y + bsz.y / 2), Math.max(c.z + sz.z / 2, bc.z + bsz.z / 2));
c.x = (min.x + max.x) / 2;
c.y = (min.y + max.y) / 2;
c.z = (min.z + max.z) / 2;
sz.x = max.x - min.x;
sz.y = max.y - min.y;
sz.z = max.y - min.y;
}
}
function getAttributes(e, attName) {
const att = e.getAttribute(attName), atts = att ? att.split(/\s*,\s*|\s+/) : null;
return atts ? atts.map(item => parseFloat(item)) : null;
}
function getAttribute(e, attName, defVal) {
const att = e.getAttribute(attName);
return att ? parseFloat(att) : defVal;
}
function applyRadius(pts, radius) {
if (radius > 0 && pts.length > 2) {
for (let i = pts.length - 2; i >= 0; i--) {
const start = pts[i], end = pts[i + 1], len = util_1.Point.distance(start, end);
if (len > 2 * radius) {
let idx = i + 1;
if (i > 0) {
const pt = util_1.Point.interpolate(start, end, radius / len);
pts.splice(idx++, 0, pt);
}
if (i < pts.length - 2) {
const pt = util_1.Point.interpolate(start, end, 1 - (len - radius) / len);
pts.splice(idx, 0, pt);
}
}
}
}
}