iwmlib/lib/pixi/maps/scatter.js

618 lines
19 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 != null) {
if (displayObject.dontBlockScatter && displayObject.parent != null) {
displayObject = interactionManager.hitTest(local, displayObject.parent)
}
if (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 {
constructor(displayObject, renderer, opts = {}) {
/*
Currently the submaps are only working on one scale to
avoid recalculations of the shown map.
Therfore we force the scatter to not be scaleable.
*/
Object.assign(opts, {
minScale: 1,
maxScale: 1,
startScale: 1,
overdoScaling: 1,
scalable: false,
resizable: true
})
super(displayObject, renderer, opts)
this.onResize = opts.onResize
}
/**
* Adds mouse wheel resizing to the submap.
*
* @param {MouseEvent} event - Event containing the mouse data.
* @memberof SubmapScatter
*/
onMouseWheel(event) {
super.onMouseWheel(event)
if (this.onResize) {
const zoomFactor = 0.001
let zoom = 1 + zoomFactor * event.deltaY
let anchor = {
x: event.offsetX,
y: event.offsetY
}
this.onResize(anchor, zoom)
}
}
/**
* Overwrites the gesture to get access to the interaction pivot and scaling information.
* This calls the resize method to resize the submap.
*
* @private
* @param {interaction} interaction - Interaction element containing various informations about the touch interaction.
* @memberof SubmapScatter
*/
gesture(interaction) {
super.gesture(interaction)
if (this.onResize) {
let delta = interaction.delta()
this.onResize(delta.about, delta.zoom)
}
}
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)
this.displayObject.on('added', this._applyInitialCover)
this._applyInitialCover()
}
}
_applyInitialCover() {
if (this.debug) console.log('ApplyInitialCover: ', parent)
if (this.displayObject.parent)
this.forceCover(this.displayObject.parent.width, this.displayObject.parent.height)
}
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) {
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
}
}