diff --git a/src/main.js b/src/main.js index 1b1ee29..a9eac96 100644 --- a/src/main.js +++ b/src/main.js @@ -1,3 +1,6 @@ +const ONE_SECOND = 1000; +const PRESS_RIPPLE = ONE_SECOND / 3; + const DEFAULTS = { board: Tessellate.BOARD_STYLES.HEX, style: Tessellate.DRAW_STYLES.FILL, @@ -5,11 +8,26 @@ const DEFAULTS = { tile: Tessellate.TILE_STYLES.HEX, }; +const pressRipple = function() { + const sinStart = 2 * Math.PI; + const halfPi = Math.PI / 2; + + return pressFactor => Math.sin(sinStart + (pressFactor * halfPi)) + 1; +}(); + +const pressFade = function() { + const halfPi = Math.PI / 2; + + return pressFactor => Math.sin(Math.PI + (pressFactor * halfPi)) + 1; +}(); + class Demo { constructor() { [ 'setOriginTile', - 'onTap', + 'tap', + 'pressStart', + 'press', 'createTile', 'drawTile', 'draw', @@ -21,11 +39,14 @@ class Demo { this.map = {}; this.taps = []; + this.ripples = []; this.setOriginTile(); this.tessellate = new Tessellate(Object.assign({ element: '#container', - tap: this.onTap, + tap: this.tap, + pressStart: this.pressStart, + press: this.press, draw: this.draw, }, this.settings)); } @@ -62,7 +83,7 @@ class Demo { }); } - onTap(tap) { + tap(tap) { const {x, y, z} = tap.tile.getPoint(); console.log(x, y, z); @@ -70,40 +91,77 @@ class Demo { this.map[key].pips = Tessellate.utils.random(1,7); console.log(this.map[key].pips); - -// console.log(tap.tile.getPoint()); -// -// this.taps.push(this.createTile( -// tap.tile.x, -// tap.tile.y, -// -// Tessellate.utils.random(Tessellate.DRAW_STYLES), -// Tessellate.utils.random(Tessellate.TILE_STYLES), -// Tessellate.utils.random(Tessellate.TILE_ORIENTATIONS), -// )); } - createTile(x, y, drawStyle, tileStyle, orientation) { + pressStart(tap) { + this.ripples.push({ + timestamp: tap.event.timeStamp, + + cell: this.createTile({ + x: tap.tile.x, + y: tap.tile.y, + + scale: 1.0, + + drawStyle: Tessellate.DRAW_STYLES.FILL, + tileStyle: Tessellate.TILE_STYLES.CIRCLE, + orientation: Tessellate.TILE_ORIENTATIONS.FLAT, + + red: 255, + green: 127, + blue: 127, + alpha: 0.5, + }) + }); + + this.taps.push(this.createTile({ + x: tap.tile.x, + y: tap.tile.y, + + drawStyle: Tessellate.utils.random(Tessellate.DRAW_STYLES), + tileStyle: Tessellate.utils.random(Tessellate.TILE_STYLES), + orientation: Tessellate.utils.random(Tessellate.TILE_ORIENTATIONS), + })); + } + + press(tap) { + console.log('PRESS END'); + } + + createTile({x, y, + drawStyle, tileStyle, orientation, + scale = Tessellate.utils.random(7, 9) / 10, + red = Tessellate.utils.random(255), + green = Tessellate.utils.random(255), + blue = Tessellate.utils.random(255), + alpha = Tessellate.utils.random(25, 75) / 100, + }) { + return new Tessellate.Cell({ x, y, - scale: Tessellate.utils.random(7, 9) / 10, + scale, drawStyle, tileStyle, orientation, - red: Tessellate.utils.random(255), - green: Tessellate.utils.random(255), - blue: Tessellate.utils.random(255), - alpha: Tessellate.utils.random(25,75)/100 + red, + green, + blue, + alpha, }); } drawTile({x, y, z}, context, scale) { const key = `${ x },${ z != null ? z : y }`; - this.map[key] = this.map[key] || this.createTile(x, y, this.settings.style, this.settings.tile, this.settings.orientation); + this.map[key] = this.map[key] || this.createTile({ + x, y, + drawStyle: this.settings.style, + tileStyle: this.settings.tile, + orientation: this.settings.orientation, + }); const tile = this.map[key]; const pixelPoint = this.tessellate.tileToPixel(x, y, z); @@ -115,6 +173,21 @@ class Demo { draw({context, scale, tilePoints}) { tilePoints.forEach(tilePoint => this.drawTile(tilePoint, context, scale)); + const now = Date.now(); + this.ripples.forEach(({timestamp, cell}) => { + let pressFactor = (now - timestamp) / PRESS_RIPPLE; + pressFactor = pressFactor > 1 ? 1 : pressFactor; + + Object.assign(cell, { + scale: pressRipple(pressFactor), + alpha: pressFade(pressFactor), + }); + + const pixelPoint = this.tessellate.tileToPixel(cell.x, cell.y); + Tessellate.TILES[cell.tileStyle][cell.drawStyle](context, scale, pixelPoint.getX(), pixelPoint.getY(), cell); + }); + this.ripples = this.ripples.filter(ripple => (ripple.timestamp + PRESS_RIPPLE) > now); + this.taps.forEach(cell => { const pixelPoint = this.tessellate.tileToPixel(cell.x, cell.y); Tessellate.TILES[cell.tileStyle][cell.drawStyle](context, scale, pixelPoint.getX(), pixelPoint.getY(), cell); diff --git a/src/onTap.js b/src/onTap.js index 2f53738..956c433 100644 --- a/src/onTap.js +++ b/src/onTap.js @@ -2,7 +2,24 @@ import {noop} from './utils.js'; const DEFAULTS = { + debug: false, + element: document.body, + + desktopPress: false, + + tap: noop, + tapStart: noop, + move: noop, + doubletap: noop, + press: noop, + pressStart: noop, + zoom: noop, + + moveThreshold: 5, + doubletapThreshold: 500, + pressThreshold: 1000, + wheelFactor: -100, }; @@ -10,33 +27,15 @@ export default class OnTap { constructor(settings) { this.settings = Object.assign({}, DEFAULTS, settings); - this.debug = false; this.state = { - clickStartTime: null - }; - - this.actions = { - tap: { - callback: settings.tap || noop - }, - move: { - threshold: settings.moveThreshold || 5, // greater than in pixels - callback: settings.move || noop - }, - doubletap: { - threshold: settings.doubletapThreshold || 500, // less than in milliseconds - callback: settings.doubletap || noop - }, - press: { - threshold: settings.pressThreshold || 1000, // greater than or equal to in milliseconds - callback: settings.press || noop - }, - zoom: { - callback: settings.zoom || noop - }, + tapStartTime: null, }; [ + // TODO: don't set up listeners for these two + 'tapStart', + 'pressStart', + 'mousedown', 'mouseup', 'mousemove', @@ -47,26 +46,40 @@ export default class OnTap { 'touchcancel', 'wheel', - ].map(method => { + ].forEach(method => { this[method] = this[method].bind(this); this.settings.element.addEventListener(method, this[method]); }); } - mousedown(event) { - if (this.debug) console.debug('onTap.mousedown', event); + tapStart(event, mobile = false) { + if (this.settings.debug) console.debug('onTap.tapStart', event); - if (!this.state.clickStartTime) { - this.state.lastX = event.offsetX; - this.state.lastY = event.offsetY; - this.state.clickStartTime = event.timeStamp; + if (!this.state.tapStartTime) { + this.state.tapStartTime = event.timeStamp; + + if (mobile || this.settings.desktopPress) { + clearTimeout(this.state.pressTO); + this.state.pressTO = setTimeout(this.pressStart, this.settings.pressThreshold); + } + + this.settings.tapStart(event); } } + mousedown(event) { + if (this.settings.debug) console.debug('onTap.mousedown', event); + + this.state.lastX = event.offsetX; + this.state.lastY = event.offsetY; + + this.tapStart(event, false); + } + touchstart(event) { event.preventDefault(); - if (this.debug) console.debug('onTap.touchstart', event); + if (this.settings.debug) console.debug('onTap.touchstart', event); const touches = [...event.touches]; event.offsetX = touches.reduce((memo, touch) => memo + touch.pageX, 0) / touches.length; @@ -77,30 +90,44 @@ export default class OnTap { if (event.touches.length > 1) { this.state.pinching = true; + clearTimeout(this.state.pressTO); } - if (!this.state.clickStartTime) { - this.state.clickStartTime = event.timeStamp; - } + this.tapStart(event, true); + } + + pressStart(event = {timeStamp: Date.now(), offsetX: this.state.lastX, offsetY: this.state.lastY}) { + if (this.settings.debug) console.debug('onTap.pressStart', event); + + this.settings.pressStart(event); } mouseup(event) { - if (this.debug) console.debug('onTap.mouseup', event); + if (this.settings.debug) console.debug('onTap.mouseup', event); if (!this.state.moving) { - this.actions.tap.callback(event); + event.duration = event.timeStamp - this.state.tapStartTime; + + if (this.settings.desktopPress && event.duration >= this.settings.pressThreshold) { + this.settings.press(event); + } + else { + clearTimeout(this.state.pressTO); + this.settings.tap(event); + } } this.state.moving = null; this.state.lastX = null; this.state.lastY = null; - this.state.clickStartTime = null; + this.state.tapStartTime = null; + clearTimeout(this.state.pressTO); } touchend(event) { event.preventDefault(); - if (this.debug) console.debug('onTap.touchend', event); + if (this.settings.debug) console.debug('onTap.touchend', event); const touches = [...event.touches]; @@ -117,7 +144,15 @@ export default class OnTap { } if (!(this.state.moving || this.state.pinching)) { - this.actions.tap.callback(event); + event.duration = event.timeStamp - this.state.tapStartTime; + + if (event.duration >= this.settings.pressThreshold) { + this.settings.press(event); + } + else { + clearTimeout(this.state.pressTO); + this.settings.tap(event); + } } if (event.touches.length <= 1) { @@ -129,25 +164,26 @@ export default class OnTap { this.state.moving = null; this.state.lastX = null; this.state.lastY = null; - this.state.clickStartTime = null; + this.state.tapStartTime = null; } } mousemove(event) { - if (this.debug) console.debug('onTap.mousemove', event); + if (this.settings.debug) console.debug('onTap.mousemove', event); - if (this.state.clickStartTime) { + if (this.state.tapStartTime) { if (!this.state.moving) { - if ((Math.abs(event.offsetX - this.state.lastX) > this.actions.move.threshold) - || (Math.abs(event.offsetY - this.state.lastY) > this.actions.move.threshold)) { + if ((Math.abs(event.offsetX - this.state.lastX) > this.settings.moveThreshold) + || (Math.abs(event.offsetY - this.state.lastY) > this.settings.moveThreshold)) { this.state.moving = true; + clearTimeout(this.state.pressTO); } } if (this.state.moving) { event.deltaX = event.offsetX - this.state.lastX, event.deltaY = event.offsetY - this.state.lastY - this.actions.move.callback(event); + this.settings.move(event); this.state.lastX = event.offsetX; this.state.lastY = event.offsetY; @@ -158,9 +194,9 @@ export default class OnTap { touchmove(event) { event.preventDefault(); - if (this.debug) console.debug('onTap.touchmove', event); + if (this.settings.debug) console.debug('onTap.touchmove', event); - if (this.state.clickStartTime) { + if (this.state.tapStartTime) { const touches = [...event.touches]; event.offsetX = touches.reduce((memo, touch) => memo + touch.pageX, 0) / touches.length; event.offsetY = touches.reduce((memo, touch) => memo + touch.pageY, 0) / touches.length; @@ -168,15 +204,16 @@ export default class OnTap { if (this.state.pinching) { event.scaleStep = event.scale / (this.state.lastPinch || 1); - this.actions.zoom.callback(event); + this.settings.zoom(event); this.state.lastPinch = event.scale; } if (!this.state.moving) { - if ((Math.abs(event.offsetX - this.state.lastX) > this.actions.move.threshold) - || (Math.abs(event.offsetY - this.state.lastY) > this.actions.move.threshold)) { + if ((Math.abs(event.offsetX - this.state.lastX) > this.settings.moveThreshold) + || (Math.abs(event.offsetY - this.state.lastY) > this.settings.moveThreshold)) { this.state.moving = true; + clearTimeout(this.state.pressTO); } } @@ -184,7 +221,7 @@ export default class OnTap { event.deltaX = event.offsetX - this.state.lastX, event.deltaY = event.offsetY - this.state.lastY - this.actions.move.callback(event); + this.settings.move(event); this.state.lastX = event.offsetX; this.state.lastY = event.offsetY; @@ -197,11 +234,11 @@ export default class OnTap { } wheel(event) { - if (this.debug) console.debug('onTap.wheel', event); + if (this.settings.debug) console.debug('onTap.wheel', event); event.scaleStep = 1 + (event.deltaY / this.settings.wheelFactor); - this.actions.zoom.callback(event); + this.settings.zoom(event); } } diff --git a/src/tessellate.js b/src/tessellate.js index 8cfe009..63385f9 100644 --- a/src/tessellate.js +++ b/src/tessellate.js @@ -35,10 +35,14 @@ const TILES = { const DEFAULTS = { tile: HEX, board: HEX, - tap: utils.noop, - draw: utils.noop, orientation: FLAT, negativeTiles: true, + + tap: utils.noop, + pressStart: utils.noop, + press: utils.noop, + + draw: utils.noop, }; function selectCartographer(board, orientation) { @@ -72,6 +76,9 @@ export class Tessellate { [ 'checkSettings', 'tap', + 'doubletap', + 'pressStart', + 'press', 'move', 'zoom', 'pixelToTile', @@ -87,14 +94,15 @@ export class Tessellate { draw: this.draw }); - this.onTap = new OnTap({ + this.onTap = new OnTap(Object.assign({ element: this.settings.element, tap: this.tap, doubletap: this.doubletap, - hold: this.hold, + pressStart: this.pressStart, + press: this.press, move: this.move, zoom: this.zoom, - }); + }, funky.pick(this.settings, ['desktopPress', 'moveThreshold', 'doubletapThreshold', 'pressThreshold', 'wheelFactor']))); const cartographer = selectCartographer(this.settings.board, this.settings.orientation); this.cartographer = new cartographer(Object.assign({ @@ -134,6 +142,7 @@ export class Tessellate { } doubletap(event) { + console.log('DOUBLETAP', event); let point = new Point(event.offsetX, event.offsetY); let tile = this.cartographer.pixelToTile(point); @@ -143,11 +152,10 @@ export class Tessellate { tile }; - console.log('DOUBLETAP'); console.log(tap); } - hold(event) { + pressStart(event) { let point = new Point(event.offsetX, event.offsetY); let tile = this.cartographer.pixelToTile(point); @@ -157,8 +165,20 @@ export class Tessellate { tile }; - console.log('HOLD'); - console.log(tap); + this.settings.pressStart(tap); + } + + press(event) { + let point = new Point(event.offsetX, event.offsetY); + let tile = this.cartographer.pixelToTile(point); + + let tap = { + event, + point, + tile + }; + + this.settings.press(tap); } move(event) {