-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
Refactored all interactions to use pointer events instead of mouse and touch events. Implemented pinch zooming for multi-touch enabled devices and re-implemented pan support for pointer events. The `EventListener` class has been refactored to allow defining an element per event. It also allows defining options on initialization or when adding events. Events specific to Puzzle were moved out of index and into the Puzzle class. Other miscellaneous interactions were better organized into other files. Modifier selection now provides haptic feedback on touch devices. The concept of toggling a modifier has been introduced, which can allow for the toggling of modifier state based on interaction. For example, middle clicking on a modifier with a mouse will toggle it as will triggering a pointer down on a modifier followed by a pointer leave. For now, this behavior is only used to toggle the rotation direction of the rotate modifier, but it could be used for other things in the future. The header/footer drop-shadows now correctly always overlay the canvas area.
- Loading branch information
Showing
26 changed files
with
537 additions
and
403 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 = [] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.