diff --git a/assets/images/grid.png b/assets/images/grid.png new file mode 100644 index 0000000..04da9eb Binary files /dev/null and b/assets/images/grid.png differ diff --git a/assets/images/minus.png b/assets/images/minus.png new file mode 100644 index 0000000..5dbdee3 Binary files /dev/null and b/assets/images/minus.png differ diff --git a/assets/images/plus.png b/assets/images/plus.png new file mode 100644 index 0000000..af56f5b Binary files /dev/null and b/assets/images/plus.png differ diff --git a/assets/scripts/KaplayMap/index.js b/assets/scripts/KaplayMap/index.js new file mode 100644 index 0000000..4125027 --- /dev/null +++ b/assets/scripts/KaplayMap/index.js @@ -0,0 +1,372 @@ +export class KaplayMap { + kp; + opts = { + minZoomLevel: 0, + maxZoomLevel: 3, + dblClickDuration: 0.2, // s + dblClickForgiveness: 30, // px + }; + + uiLayer; + + #zoomLevel = 0; + #scaleTween = null; + + startMousePos = null; + startCamPos = null; + startZoomLevel = null; + prevCamPos = null; + moveVelocity = null; + moveFriction = 0.25; + moveDead = 1; + #moveTween = 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.uiLayer = this.initUI(); + + this.kp.add(this.uiLayer); + + 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 < 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; + + // ||| Completely unintelligible to the average person ||| + // vvv Sorry y'all xwx vvv + + 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(); + + // if (this.prevCamPos != null) + // this.moveVelocity = this.prevCamPos + // .sub(curCamPos) + // .scale(1 / camScale); + } + + 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 + ); + } + } + + if ( + this.moveVelocity != null && + this.moveVelocity?.x != 0 && + this.moveVelocity?.y != 0 + ) + this.kp.camPos(curCamPos.sub(this.moveVelocity?.scale(camScale) ?? 0)); + + this.kp.camPos( + Math.round(this.kp.camPos().x * this.kp.camScale().y) / + this.kp.camScale().y, + Math.round(this.kp.camPos().y * this.kp.camScale().y) / + this.kp.camScale().y + ); + + // ^^^ Completely unintelligible to the average person ^^^ + // ||| Sorry y'all xwx ||| + + if (this.moveVelocity == null) return; + + if ( + this.moveVelocity.x <= this.moveDead && + this.moveVelocity.x >= -this.moveDead + ) + this.moveVelocity.x = 0; + if ( + this.moveVelocity.y <= this.moveDead && + this.moveVelocity.y >= -this.moveDead + ) + this.moveVelocity.y = 0; + + this.moveVelocity = this.moveVelocity.scale(1 - this.moveFriction); + }); + } + + initUI() { + const uiLayer = this.kp.make([ + this.kp.pos(0), + this.kp.fixed(), + this.kp.stay(), + this.kp.z(1000), + ]); + + return uiLayer; + } + + 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() + 100, + height: this.kp.height() + 100, + pos: this.kp.vec2( + Math.floor(this.kp.camPos().x / 100) * 100 - this.kp.width() * 0.5, + Math.floor(this.kp.camPos().y / 100) * 100 - this.kp.height() * 0.5 + ), + }); + }); + + return grid; + } + + clearMouseMode() { + this.lastReleaseTime = null; + this.lastReleasePos = null; + this.lastPressTime = null; + this.lastPressPos = null; + this.prevCamPos = 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; + }); + } + + 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)); + } + + zoomToNoTweenAbs(newZoom, position) { + this.zoomLevel_NoTween = newZoom; + this.kp.camPos(position); + } +} diff --git a/assets/scripts/KaplayMap/zoom.js b/assets/scripts/KaplayMap/zoom.js new file mode 100644 index 0000000..0b4404e --- /dev/null +++ b/assets/scripts/KaplayMap/zoom.js @@ -0,0 +1,60 @@ +export function addZoomButtons(map) { + // Zoom buttons + const plus = map.kp.loadSprite(null, "/assets/images/plus.png", { + sliceX: 2, + }); + + const minus = map.kp.loadSprite(null, "/assets/images/minus.png", { + sliceX: 2, + }); + + const zoomIn = map.kp.make([ + map.kp.sprite(plus), + map.kp.pos(16), + map.kp.scale(2), + map.kp.area(), + map.kp.opacity(1), + "ui", + ]); + const zoomOut = map.kp.make([ + map.kp.sprite(minus), + map.kp.pos(16, 42), + map.kp.scale(2), + map.kp.area(), + map.kp.opacity(1), + "ui", + ]); + + let ziw; + zoomIn.onClick(() => { + map.mouseMode = "ui"; + map.zoomLevel += 1; + zoomIn.frame = 1; + if (ziw?.finish) ziw.finish(); + ziw = map.kp.wait(0.25, () => (zoomIn.frame = 0)); + + map.clearMouseMode(); + }); + + zoomIn.onUpdate(() => { + zoomIn.opacity = map.zoomLevelLimit > 0 ? 0.25 : 1; + }); + + let zow; + zoomOut.onClick(() => { + map.mouseMode = "ui"; + map.zoomLevel -= 1; + zoomOut.frame = 1; + if (zow?.finish) zow.finish(); + zow = map.kp.wait(0.25, () => (zoomOut.frame = 0)); + + map.clearMouseMode(); + }); + + zoomOut.onUpdate(() => { + zoomOut.opacity = map.zoomLevelLimit < 0 ? 0.25 : 1; + }); + + map.uiLayer.add(zoomIn); + map.uiLayer.add(zoomOut); +} diff --git a/assets/scripts/script.js b/assets/scripts/script.js index f5e7510..23b594d 100644 --- a/assets/scripts/script.js +++ b/assets/scripts/script.js @@ -1,160 +1,28 @@ import kaplay from "./modules/kaplay.js"; +import { KaplayMap } from "./KaplayMap/index.js"; +import { addZoomButtons } from "./KaplayMap/zoom.js"; -const map = document.querySelector("map"); - -class KaplayMap { - kp; - opts = { - minZoomLevel: 0, - maxZoomLevel: 3, - }; - - #zoomLevel = 0; - #scaleTween = null; - - startMousePos = null; - startCamPos = null; - prevCamPos = null; - moveVelocity = null; - moveFriction = 0.25; - moveDead = 1; - #moveTween = null; - - constructor(kp, opts) { - this.kp = kp; - this.opts = { - ...this.opts, - ...opts, - }; - - // Zooming - - this.kp.onScroll((delta) => { - const scrollDist = -delta.y / 120; - this.zoomTo(this.zoomLevel + scrollDist, this.kp.mousePos()); - }); - - // Dragging - - this.kp.onUpdate(() => { - const curCamPos = this.kp.camPos(); - const camScale = 1 / this.kp.camScale().y; - - // ||| Completely unintelligible to the average person ||| - // vvv Sorry y'all xwx vvv - - if (this.kp.isMousePressed()) { - this.startMousePos = this.kp.mousePos(); - this.startCamPos = this.kp.camPos(); - } else if (this.kp.isMouseReleased() && this.prevCamPos != null) { - this.moveVelocity = this.prevCamPos.sub(curCamPos).scale(1 / camScale); - } - - if (this.kp.isMouseDown()) { - this.prevCamPos = this.kp.camPos(); - this.kp.camPos( - this.startCamPos.sub( - this.kp.mousePos().sub(this.startMousePos).scale(camScale) - ) - ); - } else if (this.moveVelocity?.x != 0 && this.moveVelocity?.y != 0) - this.kp.camPos(curCamPos.sub(this.moveVelocity?.scale(camScale) ?? 0)); - - // ^^^ Completely unintelligible to the average person ^^^ - // ||| Sorry y'all xwx ||| - - if (this.moveVelocity == null) return; - - if ( - this.moveVelocity.x <= this.moveDead && - this.moveVelocity.x >= -this.moveDead - ) - this.moveVelocity.x = 0; - if ( - this.moveVelocity.y <= this.moveDead && - this.moveVelocity.y >= -this.moveDead - ) - this.moveVelocity.y = 0; - - this.moveVelocity = this.moveVelocity.scale(1 - this.moveFriction); - }); - } - - 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; - }); - } - - zoomTo(newZoom, position) { - const curCamPos = this.kp.camPos(); - const camScale = 1 / this.kp.camScale().y; - - const diff = this.kp.center().sub(position).scale(camScale); - - 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; - }); - } -} +const map = document.querySelector("#map"); const kp = kaplay({ canvas: map, focus: true, loadingScreen: false, - crisp: false, - debug: false, + crisp: true, + // debug: false, + // touchToMouse: false, global: false, maxFPS: 120, + texFilter: "nearest", background: "404040", }); const kaplaymap = new KaplayMap(kp, {}); +kaplaymap.initGrid(); + +addZoomButtons(kaplaymap); + const bean = kp.loadBean(); -kp.add([kp.sprite("bean"), kp.pos(kp.center())]); +kp.add([kp.sprite("bean"), kp.pos(kp.center())]); \ No newline at end of file diff --git a/assets/styles/style.css b/assets/styles/style.css index cd9fcef..3a4b14b 100644 --- a/assets/styles/style.css +++ b/assets/styles/style.css @@ -4,4 +4,8 @@ body, html { width: 100vw; box-sizing: border-box; overflow: hidden; +} + +#map { + cursor: grab; } \ No newline at end of file