export class KaplayMap { kp; opts = { minZoomLevel: 1, maxZoomLevel: 5, dblClickDuration: 0.2, // s dblClickForgiveness: 100, // px }; uiLayer; #zoomLevel = this.opts.minZoomLevel; #scaleTween = null; startMousePos = null; startCamPos = null; startZoomLevel = null; prevCamPos = null; moveFriction = 0.25; moveDead = 1; #moveTween = null; camBounds = null; lastReleaseTime; lastReleasePos; lastPressTime; lastPressPos; mouseMode = "drag"; fingers = new Map([]); startFingerDiff = null; startFingerCenterPos = null; startFingerZoomLevel = null; constructor(kp, opts) { this.kp = kp; this.opts = { ...this.opts, ...opts, }; this.zoomLevel_NoTween = this.opts.minZoomLevel; this.kp.camPos(0); this.kp.onScroll((delta) => { const scrollDist = -delta.y / 120; this.zoomTo(this.zoomLevel + scrollDist, this.kp.mousePos()); }); // Dragging this.kp.onTouchStart((pos, touch) => { this.fingers.set(touch.identifier, pos); const fingerArr = Array.from(this.fingers.values()); this.startCamPos = this.kp.camPos(); this.startFingerCenterPos = fingerArr[0]; if (this.fingers.size > 1) { this.clearMouseMode(); this.startFingerDiff = fingerArr[0].dist(fingerArr[1]); const fingerDifference = fingerArr[0].sub(fingerArr[1]); this.startFingerCenterPos = fingerArr[1].add( fingerDifference.scale(0.5) ); this.startFingerZoomLevel = this.zoomLevel; this.mouseMode = "pinch"; } else if (this.fingers.size < 2) { this.mouseMode = "drag"; } }); this.kp.onTouchMove((pos, touch) => { this.fingers.set(touch.identifier, pos); }); this.kp.onTouchEnd((pos, touch) => { if (this.fingers.size > 1) { this.clearMouseMode(); } this.fingers.delete(touch.identifier); if (this.fingers.size < 1) { this.checkBounds(); } if (this.fingers.size < 2) { const fingerArr = Array.from(this.fingers.values()); this.startFingerCenterPos = fingerArr[0]; this.startCamPos = this.kp.camPos(); this.startFingerDiff = null; } }); this.kp.onUpdate(() => { const camScale = 1 / this.kp.camScale().y; const fingerArr = Array.from(this.fingers.values()); if (this.fingers.size > 1) { const curFingerDiff = fingerArr[0].dist(fingerArr[1]); // Get centerpoint const fingerDifference = fingerArr[0].sub(fingerArr[1]); const fingerCenter = fingerArr[1].add(fingerDifference.scale(0.5)); this.zoomToNoTweenAbs( this.startFingerZoomLevel + Math.log2(curFingerDiff / this.startFingerDiff), this.startCamPos.sub( fingerCenter.sub(this.startFingerCenterPos).scale(camScale) ) ); } else if (this.fingers.size == 1) { if (this.mouseMode != "zoom") { this.kp.camPos( this.startCamPos.sub( fingerArr[0].sub(this.startFingerCenterPos).scale(camScale) ) ); } } }); this.kp.onUpdate(() => { const curCamPos = this.kp.camPos(); const camScale = 1 / this.kp.camScale().y; if (this.kp.isMousePressed()) { // ignore if no double click if (this.lastPressTime != null && this.lastPressPos != null) { const clickDifference = this.kp.time() - this.lastPressTime; const clickDistance = this.kp.mousePos().dist(this.lastPressPos); // Double-click! if ( clickDifference <= this.opts.dblClickDuration && clickDistance <= this.opts.dblClickForgiveness ) { this.mouseMode = "zoom"; } } if (this.mouseMode == "drag") { this.startMousePos = this.kp.mousePos(); this.startCamPos = this.kp.camPos(); } else if (this.mouseMode == "zoom") { this.startMousePos = this.kp.mousePos(); this.startZoomLevel = this.zoomLevel; } // Set variables this.lastPressTime = this.kp.time(); this.lastPressPos = this.kp.mousePos(); } else if (this.kp.isMouseReleased()) { if (this.mouseMode == "pinch") return; this.mouseMode = "drag"; // ignore if no double click if (this.lastReleaseTime != null && this.lastReleasePos != null) { const clickDifference = this.kp.time() - this.lastReleaseTime; const clickDistance = this.kp.mousePos().dist(this.lastReleasePos); // Double-click! if ( clickDifference <= this.opts.dblClickDuration && clickDistance <= this.opts.dblClickForgiveness ) { this.zoomTo(this.zoomLevel + 1, this.kp.mousePos()); } } // Set variables this.lastReleaseTime = this.kp.time(); this.lastReleasePos = this.kp.mousePos(); // Check bounds this.checkBounds(); } if (this.kp.isMouseDown()) { if (this.mouseMode == "drag") { this.prevCamPos = this.kp.camPos(); this.kp.camPos( this.startCamPos.sub( this.kp.mousePos().sub(this.startMousePos).scale(camScale) ) ); } else if (this.mouseMode == "zoom") { this.zoomToNoTween( this.startZoomLevel + -this.kp.mousePos().sub(this.startMousePos).y / (this.kp.height() / 2), this.startMousePos ); } } }); } initGrid() { const grid = this.kp.loadSprite(null, "/assets/images/grid.png"); this.kp.onDraw(() => { this.kp.drawSprite({ sprite: grid, tiled: true, opacity: 0.25, width: this.kp.width() + 200, height: this.kp.height() + 200, anchor: "center", pos: this.kp.vec2( Math.floor(this.kp.camPos().x / 100) * 100 + 50.5, Math.floor(this.kp.camPos().y / 100) * 100 + 0.5 ), }); }); return grid; } clearMouseMode() { this.lastReleaseTime = null; this.lastReleasePos = null; this.lastPressTime = null; this.lastPressPos = null; this.prevCamPos = null; } checkBounds() { const camPos = this.kp.camPos(); if (this.camBounds == null) return; if (this.kp.testRectPoint(this.camBounds, camPos)) return; const boundsCenter = this.camBounds.center(); const cast = this.camBounds.raycast( camPos, this.kp.Vec2.fromAngle(camPos.angle(boundsCenter) + 180).scale(40000) ); if (cast == null) return; if (this.#moveTween != null) { this.#moveTween.finish(); } this.#moveTween = this.kp.tween( camPos, cast.point, 0.25, this.kp.camPos, this.kp.easings.easeOutQuad ); this.#moveTween.then(() => { this.#moveTween = null; }); } get zoomLevelLimit() { if (this.#zoomLevel == this.opts.minZoomLevel) return -1; if (this.#zoomLevel == this.opts.maxZoomLevel) return 1; return 0; } get zoomLevel() { return this.#zoomLevel; } set zoomLevel(newZoom) { const cameraZoom = this.kp.camScale().y; let addLinear = newZoom; if (addLinear < this.opts.minZoomLevel) addLinear = this.opts.minZoomLevel; if (addLinear > this.opts.maxZoomLevel) addLinear = this.opts.maxZoomLevel; let linearToLog = Math.pow(2, addLinear); this.#zoomLevel = addLinear; if (this.#scaleTween != null) { this.#scaleTween.finish(); } this.#scaleTween = this.kp.tween( cameraZoom, linearToLog, 0.25, this.kp.camScale, this.kp.easings.easeOutQuad ); this.#scaleTween.then(() => { this.#scaleTween = null; }); } set zoomLevel_NoTween(newZoom) { let addLinear = newZoom; if (addLinear < this.opts.minZoomLevel) addLinear = this.opts.minZoomLevel; if (addLinear > this.opts.maxZoomLevel) addLinear = this.opts.maxZoomLevel; let linearToLog = Math.pow(2, addLinear); this.#zoomLevel = addLinear; this.kp.camScale(linearToLog); } zoomTo(newZoom, position) { const curCamPos = this.kp.camPos(); const camScl = 1 / this.kp.camScale().y; const diff = this.kp.center().sub(position).scale(camScl); this.zoomLevel = newZoom; let newZoomLog = 1 / Math.pow(2, newZoom); const newDiff = this.kp.center().sub(position).scale(newZoomLog); if (newZoom < this.opts.minZoomLevel) return; if (newZoom > this.opts.maxZoomLevel) return; if (this.#moveTween != null) { this.#moveTween.finish(); } this.#moveTween = this.kp.tween( curCamPos, curCamPos.sub(diff).add(newDiff), 0.25, this.kp.camPos, this.kp.easings.easeOutQuad ); this.#moveTween.then(() => { this.#moveTween = null; this.checkBounds(); }); } zoomToAbs(newZoom, position) { const curCamPos = this.kp.camPos(); this.zoomLevel = newZoom; if (this.#moveTween != null) { this.#moveTween.finish(); } this.#moveTween = this.kp.tween( curCamPos, position, 0.25, this.kp.camPos, this.kp.easings.easeOutQuad ); this.#moveTween.then(() => { this.#moveTween = null; this.checkBounds(); }); } zoomToNoTween(newZoom, position) { const curCamPos = this.kp.camPos(); const camScl = 1 / this.kp.camScale().y; const diff = this.kp.center().sub(position).scale(camScl); this.zoomLevel_NoTween = newZoom; let newZoomLog = 1 / Math.pow(2, newZoom); const newDiff = this.kp.center().sub(position).scale(newZoomLog); if (newZoom < this.opts.minZoomLevel) return; if (newZoom > this.opts.maxZoomLevel) return; this.kp.camPos(curCamPos.sub(diff).add(newDiff)); this.checkBounds(); } zoomToNoTweenAbs(newZoom, position) { this.zoomLevel_NoTween = newZoom; this.kp.camPos(position); this.checkBounds(); } }