Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor user interactions. Fixes #2 #9

Merged
merged 2 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 19 additions & 2 deletions src/components/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export class Cache {
keys.forEach((key) => { this.#cache[key] = new Cache() })
}

add (key, item) {
set (key, item) {
if (this.#hasKeys && !this.#keys.includes(key)) {
throw new Error(`Invalid key: ${key}`)
}
Expand All @@ -22,7 +22,24 @@ export class Cache {
return key ? this.#cache[key] : this.#cache
}

remove (key) {
keys (key) {
return Object.keys(this.#get(key))
}

length (key) {
return this.keys(key).length
}

unset (key) {
delete this.#cache[key]
}

values (key) {
return Object.values(this.#get(key))
}

#get (key) {
const value = this.get(key)
return value instanceof Cache ? value.get() : value
}
}
11 changes: 11 additions & 0 deletions src/components/debug.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { params } from './util'

const console = window.console = window.console || { debug: function () {} }
const consoleDebug = console.debug

export function debug (debug) {
console.debug = debug ? consoleDebug : function () {}
}

// Silence debug logging by default
debug(params.has('debug') ?? false)
30 changes: 0 additions & 30 deletions src/components/eventListener.js

This file was deleted.

27 changes: 27 additions & 0 deletions src/components/eventListeners.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export class EventListeners {
#events = []
#options = { element: document }

constructor (options = {}) {
this.#options = Object.assign(this.#options, options)
}

add (events, options = {}) {
this.#events = this.#events.concat(events.map((event) => {
event = Object.assign({}, this.#options, options, event)
if (!event.type) {
throw new Error('Event type is required')
}
if (event.context) {
event.handler = event.handler.bind(event.context)
}
event.element.addEventListener(event.type, event.handler, event.options)
return event
}))
}

remove () {
this.#events.forEach((event) => event.element.removeEventListener(event.type, event.handler))
this.#events = []
}
}
14 changes: 14 additions & 0 deletions src/components/feedback.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Puzzle } from './puzzle'

const container = document.getElementById('feedback-container')
const help = document.getElementById('help')

document.getElementById('feedback').addEventListener('click', () => {
help.setAttribute('open', 'true')
container.scrollIntoView(true)
})

const doorbellOptions = window.doorbellOptions
document.addEventListener(Puzzle.Events.Updated, (event) => {
doorbellOptions.properties.puzzleId = event.detail.state.getId()
})
6 changes: 6 additions & 0 deletions src/components/infoDialog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const dialog = document.getElementById('dialog')
document.getElementById('info').addEventListener('click', () => {
if (!dialog.open) {
dialog.showModal()
}
})
177 changes: 177 additions & 0 deletions src/components/interact.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import paper, { Point } from 'paper'
import { Cache } from './cache'
import { EventListeners } from './eventListeners'

export class Interact {
#bounds
#cache = new Cache(Object.values(Interact.CacheKeys))
#element
#eventListener = new EventListeners({ context: this })
#offset

constructor (element) {
this.#bounds = element.getBoundingClientRect()
this.#element = element
this.#offset = new Point(this.#bounds.left, this.#bounds.top)
this.#eventListener.add([
{ type: 'pointercancel', handler: this.onPointerUp },
{ type: 'pointerdown', handler: this.onPointerDown },
{ type: 'pointerleave', handler: this.onPointerUp },
{ type: 'pointermove', handler: this.onPointerMove },
{ type: 'pointerout', handler: this.onPointerUp },
{ type: 'pointerup', handler: this.onPointerUp },
{ type: 'wheel', handler: this.onMouseWheel, options: { passive: false } }
], { element })
}

onMouseWheel (event) {
event.preventDefault()
this.#zoom(new Point(event.offsetX, event.offsetY), event.deltaY, 1.05)
}

onPan (event) {
const point = this.#getPoint(event)
const pan = this.#getGesture(Interact.GestureKeys.Pan)

if (!pan) {
this.#setGesture(Interact.GestureKeys.Pan, { from: point })
return
}

const center = pan.from.subtract(point).add(paper.view.center)

// Allow a little wiggle room to prevent panning on tap
if (paper.view.center.subtract(center).length > 1) {
if (!document.body.classList.contains('grab')) {
document.body.classList.add('grab')
}

// Center on the cursor
paper.view.center = center
}
}

onPinch (events) {
const pointer0 = events[0]
const pointer1 = events[1]

const delta = Math.abs(pointer0.clientX - pointer1.clientX)
const pinch = this.#getGesture(Interact.GestureKeys.Pinch)

if (!pinch) {
const point0 = new Point(pointer0.clientX, pointer0.clientY)
const point1 = new Point(pointer1.clientX, pointer1.clientY)
const vector = point1.subtract(point0).divide(2)
const center = point1.subtract(vector).subtract(this.#offset)
this.#setGesture(Interact.GestureKeys.Pinch, { center, delta })
return
}

if (delta > 1) {
this.#zoom(pinch.center, pinch.delta - delta, 1.01)
}

pinch.delta = delta
}

onPointerDown (event) {
this.#cache.get(Interact.CacheKeys.Down).set(event.pointerId, event)
}

onPointerMove (event) {
const down = this.#cache.get(Interact.CacheKeys.Down).get(event.pointerId)
if (!down) {
// Ignore events until there is a pointer down event
return
}

// For some reason pointermove fires on mobile even if there was no movement
const diff = this.#getPoint(event).subtract(this.#getPoint(down)).length
if (diff > 1) {
this.#cache.get(Interact.CacheKeys.Move).set(event.pointerId, event)

const events = this.#cache.get(Interact.CacheKeys.Move).values()
if (events.length === 2) {
this.onPinch(events)
} else {
this.onPan(event)
}
}
}

onPointerUp (event) {
const down = this.#cache.get(Interact.CacheKeys.Down).get(event.pointerId)
if (!down) {
return
}

if (
this.#cache.length(Interact.CacheKeys.Down) === 1 &&
!this.#cache.get(Interact.CacheKeys.Move).get(event.pointerId)
) {
this.onTap(down)
}

document.body.classList.remove('grab')

this.#cache.get(Interact.CacheKeys.Down).unset(event.pointerId)
this.#cache.get(Interact.CacheKeys.Move).unset(event.pointerId)
this.#cache.get(Interact.CacheKeys.Gesture).unset(Interact.GestureKeys.Pan)

if (this.#cache.length(Interact.CacheKeys.Move) < 2) {
this.#cache.get(Interact.CacheKeys.Gesture).unset(Interact.GestureKeys.Pinch)
}
}

onTap (event) {
const point = this.#getPoint(event)
this.#element.dispatchEvent(new CustomEvent(Interact.GestureKeys.Tap, { detail: { event, point } }))
}

#getGesture (key) {
return this.#cache.get(Interact.CacheKeys.Gesture).get(key)
}

#getPoint (event) {
return paper.view.viewToProject(new Point(event.clientX, event.clientY).subtract(this.#offset))
}

#setGesture (key, value) {
this.#cache.get(Interact.CacheKeys.Gesture).set(key, value)
}

#zoom (point, delta, factor) {
const zoom = delta < 0 ? paper.view.zoom * factor : paper.view.zoom / factor

// Don't allow zooming too far in or out
if (zoom > 2 || zoom < 0.5) {
return
}

// Convert the touch point from the view coordinate space to the project coordinate space
const touchPoint = paper.view.viewToProject(point)
const touchOffset = touchPoint.subtract(paper.view.center)

// Adjust center towards cursor location
const zoomOffset = touchPoint
.subtract(touchOffset.multiply(paper.view.zoom / zoom))
.subtract(paper.view.center)

paper.view.zoom = zoom
paper.view.center = paper.view.center.add(zoomOffset)
}

static CacheKeys = Object.freeze({
Down: 'down',
Move: 'move',
Gesture: 'gesture'
})

static GestureKeys = Object.freeze({
Pan: 'pan',
Pinch: 'pinch',
Tap: 'tap'
})

static vibratePattern = 25
}
2 changes: 1 addition & 1 deletion src/components/item.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export class Item extends Stateful {
return this.group.parent
}

onClick () {}
onTap () {}

onCollision ({ collisionStep }) {
return collisionStep
Expand Down
4 changes: 2 additions & 2 deletions src/components/items/beam.js
Original file line number Diff line number Diff line change
Expand Up @@ -325,10 +325,10 @@ export class Beam extends Item {
this.addStep(step.copy({
colors: mergeWith.colors,
onAdd: () => {
this.#cache.get(Beam.CacheKeys.MergeWith).add(beam.id, mergeWith)
this.#cache.get(Beam.CacheKeys.MergeWith).set(beam.id, mergeWith)
},
onRemove: () => {
this.#cache.get(Beam.CacheKeys.MergeWith).remove(beam.id)
this.#cache.get(Beam.CacheKeys.MergeWith).unset(beam.id)
},
state: step.state.copy(new StepState.MergeWith(mergeWith))
}))
Expand Down
2 changes: 1 addition & 1 deletion src/components/items/portal.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export class Portal extends movable(rotatable(Item)) {
},
{
beam,
onClick: (puzzle, tile) => {
onTap: (puzzle, tile) => {
const destination = destinations.find((portal) => portal.parent === tile)
if (destination) {
beam.addStep(this.#step(destination, nextStep, portalState))
Expand Down
4 changes: 2 additions & 2 deletions src/components/items/tile.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ export class Tile extends Item {
return state
}

onClick (event) {
onTap (event) {
console.debug(this.coordinates.offset.toString(), this)
this.items.forEach((item) => item.onClick(event))
this.items.forEach((item) => item.onTap(event))
}

onDeselected (selectedTile) {
Expand Down
Loading
Loading