559 lines
17 KiB
JavaScript
559 lines
17 KiB
JavaScript
/* 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
|
|
}
|
|
}
|