/* ES Lint */ /* globals PIXI, requestAnimationFrame, performance, app*/ import { DisplayObjectScatter, ScatterContainer } from '../scatter.js' import { Points } from '../../utils.js' import { EventHandler } from './utils.js' /** * The AdvancedScatterContainer extends the ScatterContainer, but * uses the findTargetNew instead of the findTarget method, which takes interactive elements * into account, when calculating the target. */ export class AdvancedScatterContainer extends ScatterContainer { applyToChildScattersRecursively(parent, root, applyFunc) { for (let child of parent.children) { if (child.scatter) { applyFunc(child, root) this.applyToChildScattersRecursively(child, root, applyFunc) } } } /** * The update method was adjusted to draw bounds and polygpns */ update(dt) { this.clear() this.lineStyle(1, 0x0000ff) if (this.showBounds) { this.applyToChildScattersRecursively(this, this, (child, root) => { let position = child.worldTransform.apply(new PIXI.Point(0, 0)) this.drawShape(child.scatter.bounds) let center = { x: child.scatter.width / 2 / child.scatter.scale, y: child.scatter.height / 2 / child.scatter.scale } center = child.worldTransform.apply(center) this.drawCircle(center.x, center.y, 4) this.drawCircle(position.x, position.y, 4) }) this.lineStyle(2, 0x0000ff) this.drawShape(this.bounds) } if (this.showPolygon) { this.lineStyle(2, 0xff0000) for (let child of this.children) { if (child.scatter) { let polygon = child.scatter.polygon let shape = new PIXI.Polygon(polygon.flatAbsolutePoints()) if ( shape.points[0] !== shape.points[shape.points.length - 2] || shape.points[1] !== shape.points[shape.points.length - 1] ) { shape.points.push(shape.points[0], shape.points[1]) } this.drawShape(shape) } } } if (this.showTouches) { let current = this.delegate.interaction.current for (let [key, point] of current.entries()) { let local = this.mapPositionToPoint(point) this.drawCircle(local.x, local.y, 12) } } } get width() { return this.bounds.width } get height() { return this.bounds.width } findTarget(event, local, global) { // UO: still problematic. Does not find non interactive elements // which are needed for some stylus applications if (event.claimedByScatter) { return null } this.hitScatter = null let interactionManager = this.renderer.plugins.interaction let displayObject = interactionManager.hitTest(local, this) if (displayObject.dontBlockScatter && displayObject.parent != null) { displayObject = interactionManager.hitTest(local, displayObject.parent) } if (displayObject != null && displayObject.scatter != null) this.hitScatter = displayObject.scatter if (this.claimEvents) event.claimedByScatter = this.hitScatter return this.hitScatter } } export class RigidScatterContainer extends AdvancedScatterContainer { constructor(width, height, renderer, opts) { super(renderer, opts) this._width = width this._height = height } resize(width, height) { this._width = width this._height = height } get width() { return this._width } get height() { return this._height } _calculateBounds() { this._bounds.clear() let bounds = new PIXI.Bounds() bounds.minX = this.parent.position.x bounds.minY = this.parent.position.y bounds.maxX = this.parent.position.x + this.width bounds.maxY = this.parent.position.y + this.height this._bounds.addBounds(bounds) } } export class RigidContainer extends PIXI.Graphics { constructor(width, height, { onResize = null } = {}) { super() if (!width || !height) { console.log( `Width (${width}) or height (${height}) not set! If this is intended use another PIXI.DisplayObject instead.`, this ) } this._width = width this._height = height this.beginFill(0xffffff, 0.3) this.drawRect(0, 0, this.width, this.height) this.onResize = new EventHandler('resize', { listeners: onResize ? [onResize] : [] }) } /** * Resizes the scattercontainer to the provided dimensions. * * @param {number} width - Target width of the resize. * @param {number} height - Target height of the resize. * @memberof RigidContainer */ resize(width, height) { this._width = width this._height = height this._updateHitArea() this.onResize.call(this, 'resize', { x: width, y: height }) } _updateHitArea() { // this.hitArea = new PIXI.Rectangle(0,0, this.width, this.height) } get width() { return this._width } get height() { return this._height } } /** * Specialization of the DisplayObjectScatter with special behavior needed for maps. */ class AdvancedScatter extends DisplayObjectScatter { /** * Animates the throw and ensures that the map is always visible. This * is different from the standard behavior. * * @param {*} time * @memberof AdvancedScatter */ animateThrow(time) { // In rare cases animateThrow is called when the displayObject is already removed if (this.displayObject.parent == null) { return } //Moved this outside, as the time continously increases, //when the object is not thrown. if (this.velocity != null) { let dt = this._throwDeltaTime() // This seems to just correct the velocity when its calculated wrong. // In what case can it get bigger? Velocity tends to always shrink, when no force is added. let d = this._getThrowDelta(dt) this._move(d) this.onDragUpdate(d) if (dt == 0 || this.needsAnimation()) { requestAnimationFrame(this.animateThrow.bind(this)) return } else { if (this.isOutside()) { requestAnimationFrame(this.animateThrow.bind(this)) return } } } this.onDragComplete() } get parent() { // Returns the parent is a scatter get the parent scatter. // If it's a scatter container, get the displayObject. // Other cases are not allowed. return this.displayObject.parent != null && this.displayObject.parent.scatter != null ? this.displayObject.parent.scatter : this.displayObject.parent } _getThrowDelta(dt) { this.velocity = this.nextVelocity(this.velocity) if (this.velocity != null) { return Points.multiplyScalar(this.velocity, dt) } return { x: 0, y: 0 } } freeze({ translatable = false, scalable = false, rotatable = false, movableX = false, movableY = false } = {}) { this.translatable = translatable this.scalable = scalable this.rotatable = rotatable this.movableX = movableX this.movableY = movableY } unfreeze({ translatable = true, scalable = true, rotatable = true, movableX = true, movableY = true } = {}) { this.freeze({ translatable, scalable, rotatable: false, movableX, movableY }) } } export class SubmapScatter extends DisplayObjectScatter { get width() { return this.displayObject.width * this.displayObject.scale.x } get height() { return this.displayObject.height * this.displayObject.scale.y } freeze({ translatable = false, scalable = false, rotatable = false, movableX = false, movableY = false } = {}) { this.translatable = translatable this.scalable = scalable this.rotatable = rotatable this.movableX = movableX this.movableY = movableY } unfreeze({ translatable = true, scalable = true, rotatable = true, movableX = true, movableY = true } = {}) { this.freeze({ translatable, scalable, rotatable: false, movableX, movableY }) } onZoomed(about) { super.onZoomed(about) } } /** * TODO * A scatter which width does not increase when child elements are added. */ export class CompactScatter extends AdvancedScatter {} /** * A problem with PIXI Objects is, that the bounding box changes * with child elements. Therefore the sized scatter has a rigid size, that is not * changed by elements, but only with their width and height property. */ export class CoverScatter extends AdvancedScatter { constructor(a, b, opts) { super(a, b, opts) opts = Object.assign( { debug: false, cover: true, boundaries: { min: { x: 0, y: 0 }, max: { x: 1, y: 1 } } }, opts ) this.debug = opts.debug this.cover = opts.cover this._boundaries = opts.boundaries if (this.debug) { this.debugGraphics = new PIXI.Graphics() this.displayObject.addChild(this.debugGraphics) this.debugGraphics.lineStyle(0.2, 0x00ff00) this.debugGraphics.drawRect( this.width * this.boundaries.min.x, this.height * this.boundaries.min.y, this.width * (this.boundaries.max.x - this.boundaries.min.x), this.height * (this.boundaries.max.y - this.boundaries.min.y) ) this.debugGraphics.endFill() } // if (this.cover) { // // The reference to the element handler needs to be stored, // // that we can remove it later on. // this._applyInitialCover = this._applyInitialCover.bind(this) // } } // _applyInitialCover() { // if (this.debug) console.log('ApplyInitialCover: ', parent) // if (this.displayObject.parent) // this.forceCover(this.displayObject.parent.width, this.displayObject.parent.height) // else { // this.displayObject.on('added', eventHandler) // } // } get boundaries() { if (this._boundaries) return this._boundaries else return { min: { x: 0, y: 0 }, max: { x: 1, y: 1 } } } transform(translate, zoom, rotate, anchor) { if (this.cover) { if (!this.parent) { return new PIXI.Rectangle(0, 0, window.innerWidth, window.innerHeight) } /** * If the scaling violates the minCoverScale, the zoom is adjusted to perfectly match * the minCoverScale. */ let minCoverScale = this.calculateMinCoverScale(this.parent.width, this.parent.height) let { zoom: fixedZoom, scale } = this.calculateScale(zoom) if (scale < minCoverScale) { zoom = minCoverScale / this.scale } } super.transform(translate, zoom, rotate, anchor) if (this.cover) { let postTranslate = this.outsideBoundaries() super.transform(postTranslate, 1, 0, { x: 0, y: 0 }) } } outsideBoundaries(movement = { x: 0, y: 0 }, { boundaries = this.boundaries } = {}) { if (this.parent == null) { return { x: 0, y: 0 } } let left = -(this.displayObject.position.x + movement.x + this.width * boundaries.min.x) let right = -left + this.width * (boundaries.max.x - boundaries.min.x) - this.parent.width let top = -(this.displayObject.position.y + movement.y + this.height * boundaries.min.y) let bot = -top + this.height * (boundaries.max.y - boundaries.min.y) - this.parent.height // Helper function to limit an Axis inside a container, or 'glue' it // to the lower side, if the size is smaller than the size of the // container. function limitAxis(low, high, contentSize, containerSize) { let val = 0 if (low < 0) { val = low } else if (high < 0) { if (contentSize > containerSize) { // ... and the element is bigger as the container // - reset it to the containersize. val = -high } else { // ... and the size is not high enough, then // 'glue' it to the low axis. val = low } } return val } // Use the helper function to correct the movement to cover x and y. let correctionX = limitAxis(left, right, this.width * (boundaries.max.x - boundaries.min.x), this.parent.width) let correctionY = limitAxis(top, bot, this.height * (boundaries.max.y - boundaries.min.y), this.parent.height) let fixedMovement = { x: correctionX == 0 ? movement.x : correctionX + movement.x, y: correctionY == 0 ? movement.y : correctionY + movement.y } return fixedMovement } calculateMinCoverScale(width, height) { let scale = 0 if (!(this.width == 0 && this.height == 0)) { let actualWidth = this.width / this.scale let actualHeight = this.height / this.scale let boundaryWidth = (this.boundaries.max.x - this.boundaries.min.x) * actualWidth let boundaryHeight = (this.boundaries.max.y - this.boundaries.min.y) * actualHeight let coverWidth = width / boundaryWidth let coverHeight = height / boundaryHeight scale = Math.max(coverWidth, coverHeight) } return scale } _getThrowDelta(dt) { let delta = super._getThrowDelta(dt) if (this.cover) { delta = this.outsideBoundaries(delta, { boundaries: this.boundaries }) } return delta } containerChanged(width, height) { if (this.cover) this.forceCover(width, height) } forceCover() { this.requestScale(this.scale) let translate = this.outsideBoundaries() this.transform(translate, 1, 0, { x: 0, y: 0 }) } requestScale(scale) { if (this.scalable && this.parent != null) { if (this.cover) { let minCoverScale = this.calculateMinCoverScale(this.parent.width, this.parent.height) if (scale < minCoverScale) { console.error('USE MIN COVER SCALE', minCoverScale, scale) scale = minCoverScale } } this.scale = scale } } requestFocus(point) { let allowedMovement = this.outsideBoundaries(Points.subtract(point, this.position)) this.transform(allowedMovement, 1, 0, { x: 0, y: 0 }) } } /** * The MapObjectScatter somewhat inverts the behaviour of * regular throwables. Regular throwables should bounce of the wall, when they * overlap to a certain amount. Maps on the otherhand are intended to overlap with the * scatter container heavily to hide irrelevant parts of the map without revealing * any background. * * @class * @extends DisplayObjectScatter */ export class MapObjectScatter extends CoverScatter { /** * x=null, y=null, minScale=0.1, maxScale=1.0, startScale=1.0, autoBringToFront=true, translatable=true, scalable=true, rotatable=true, resizable=false, movableX=true, movableY=true, throwVisibility=44, throwDamping = 0.95, autoThrow=true, rotationDegrees=null, rotation=null, onTransform = null } */ constructor(displayObject, renderer, opts = {}) { opts = Object.assign( { debug: false, cover: false, startScale: 1, boundaries: { min: { x: 0, y: 0 }, max: { x: 1, y: 1 } }, autoBringToFront: false }, opts ) super(displayObject, renderer, opts) if (!renderer) { console.error('Renderer was not set!') return } this.cover = opts.cover } moveTo() { super.moveTo(...arguments) } lock() { this.rotatable = false this.moveable = false this.scaleable = false } unlock() { this.rotatable = true this.moveable = true this.scaleable = true } getWorldScatter() { return this.parent } }