UNPKG

simscript

Version:

A Discrete Event Simulation Library in TypeScript

524 lines (523 loc) 19.3 kB
"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 = '&#9899;', _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); } } } } }