diff --git a/src/controls/PixelPainterControls.js b/src/controls/PixelPainterControls.js index ff1228e..efd18f7 100644 --- a/src/controls/PixelPainterControls.js +++ b/src/controls/PixelPainterControls.js @@ -4,7 +4,6 @@ * @flow */ -import Hammer from 'hammerjs'; import keycode from 'keycode'; import { @@ -28,100 +27,108 @@ import { getOffsetOfPixel, } from '../core/utils'; -let store = null; +class PixelPlainterControls { + constructor(renderer, viewport: HTMLCanvasElement, curStore) { + this.store = curStore; + this.renderer = renderer; + this.viewport = viewport; -function onKeyPress(event: KeyboardEvent) { - // ignore key presses if modal is open or chat is used - if (event.target.nodeName === 'INPUT' - || event.target.nodeName === 'TEXTAREA' - ) { - return; + this.onMouseDown = this.onMouseDown.bind(this); + this.onKeyPress = this.onKeyPress.bind(this); + this.onAuxClick = this.onAuxClick.bind(this); + this.onMouseOut = this.onMouseOut.bind(this); + this.onMouseMove = this.onMouseMove.bind(this); + this.onWheel = this.onWheel.bind(this); + this.onMouseUp = this.onMouseUp.bind(this); + this.onTouchStart = this.onTouchStart.bind(this); + this.onTouchEnd = this.onTouchEnd.bind(this); + this.onTouchMove = this.onTouchMove.bind(this); + + this.clickTabStartView = [0, 0]; + this.clickTabStartTime = 0; + this.clickTabStartCoords = [0, 0]; + this.startTabDist = 50; + this.startTabScale = this.store.getState().scale; + this.isMultiTab = false; + this.isMouseDown = false; + + document.addEventListener('keydown', this.onKeyPress, false); + viewport.addEventListener('auxclick', this.onAuxClick, false); + viewport.addEventListener('mousedown', this.onMouseDown, false); + viewport.addEventListener('mousemove', this.onMouseMove, false); + viewport.addEventListener('mouseup', this.onMouseUp, false); + viewport.addEventListener('wheel', this.onWheel, false); + viewport.addEventListener('touchstart', this.onTouchStart, false); + viewport.addEventListener('touchend', this.onTouchEnd, false); + viewport.addEventListener('touchmove', this.onTouchMove, false); + viewport.addEventListener('mouseout', this.onMouseOut, false); } - switch (keycode(event)) { - case 'up': - case 'w': - store.dispatch(moveNorth()); - break; - case 'left': - case 'a': - store.dispatch(moveWest()); - break; - case 'down': - case 's': - store.dispatch(moveSouth()); - break; - case 'right': - case 'd': - store.dispatch(moveEast()); - break; - /* - case 'space': - if ($viewport) $viewport.click(); - return; - */ - case '+': - case 'e': - store.dispatch(zoomIn()); - break; - case '-': - case 'q': - store.dispatch(zoomOut()); - break; - default: + dispose() { + document.removeEventListener('keydown', this.onKeyPress, false); } -} -export function initControls(renderer, viewport: HTMLCanvasElement, curStore) { - store = curStore; - viewport.onmousemove = ({ clientX, clientY }: MouseEvent) => { - const state = store.getState(); - const screenCoor = screenToWorld(state, viewport, [clientX, clientY]); - store.dispatch(setHover(screenCoor)); - }; - viewport.onmouseout = () => { - store.dispatch(unsetHover()); - }; - viewport.onwheel = ({ deltaY }: WheelEvent) => { - const state = store.getState(); - const { hover } = state.gui; - let zoompoint = null; - if (hover) { - zoompoint = hover; - } - if (deltaY < 0) { - store.dispatch(zoomIn(zoompoint)); - } - if (deltaY > 0) { - store.dispatch(zoomOut(zoompoint)); - } - store.dispatch(onViewFinishChange()); - }; - viewport.onauxclick = ({ which, clientX, clientY }: MouseEvent) => { - // middle mouse button - if (which !== 2) { - return; - } - const state = store.getState(); - if (state.canvas.scale < 3) { - return; - } - const coords = screenToWorld(state, viewport, [clientX, clientY]); - const clrIndex = renderer.getColorIndexOfPixel(...coords); - if (clrIndex === null) { - return; - } - store.dispatch(selectColor(clrIndex)); - }; + onMouseDown(event: MouseEvent) { + event.preventDefault(); - // fingers controls on touch - const hammertime = new Hammer(viewport); - hammertime.get('pan').set({ direction: Hammer.DIRECTION_ALL }); - hammertime.get('swipe').set({ direction: Hammer.DIRECTION_ALL }); - // Zoom-in Zoom-out in touch devices - hammertime.get('pinch').set({ enable: true }); + if (event.button === 0) { + this.isMouseDown = true; + const { clientX, clientY } = event; + this.clickTabStartTime = Date.now(); + this.clickTabStartCoords = [clientX, clientY]; + this.clickTabStartView = this.store.getState().canvas.view; + const { viewport } = this; + setTimeout(() => { + viewport.style.cursor = 'move'; + }, 300); + } + } - hammertime.on('tap', ({ center }) => { + onMouseUp(event: MouseEvent) { + event.preventDefault(); + + if (event.button === 0) { + this.isMouseDown = false; + const { clientX, clientY } = event; + const { clickTabStartCoords, clickTabStartTime } = this; + const coordsDiff = [ + clickTabStartCoords[0] - clientX, + clickTabStartCoords[1] - clientY, + ]; + // thresholds for single click / holding + if (clickTabStartTime > Date.now() - 300 + && coordsDiff[0] < 10 && coordsDiff[1] < 10) { + PixelPlainterControls.placePixel( + this.store, + this.viewport, + this.renderer, + [clientX, clientY], + ); + } + this.viewport.style.cursor = 'auto'; + } + } + + static getTouchCenter(event: TouchEvent) { + switch (event.touches.length) { + case 1: { + const { pageX, pageY } = event.touches[0]; + return [pageX, pageY]; + } + case 2: { + const pageX = Math.floor(0.5 + * (event.touches[0].pageX + event.touches[1].pageX)); + const pageY = Math.floor(0.5 + * (event.touches[0].pageY + event.touches[1].pageY)); + return [pageX, pageY]; + } + default: + break; + } + return null; + } + + static placePixel(store, viewport, renderer, center) { const state = store.getState(); const { autoZoomIn } = state.gui; const { placeAllowed } = state.user; @@ -133,8 +140,7 @@ export function initControls(renderer, viewport: HTMLCanvasElement, curStore) { if (isHistoricalView) return; - const { x, y } = center; - const cell = screenToWorld(state, viewport, [x, y]); + const cell = screenToWorld(state, viewport, center); if (autoZoomIn && scale < 8) { store.dispatch(setViewCoordinates(cell)); @@ -142,7 +148,7 @@ export function initControls(renderer, viewport: HTMLCanvasElement, curStore) { return; } - // don't allow placing of pixel just on low zoomlevels + // allow placing of pixel just on low zoomlevels if (scale < 3) return; if (!placeAllowed) return; @@ -156,61 +162,218 @@ export function initControls(renderer, viewport: HTMLCanvasElement, curStore) { selectedColor, )); } - }); + } - const initialState: State = store.getState(); - [window.lastPosX, window.lastPosY] = initialState.canvas.view; - let lastScale = initialState.canvas.scale; - hammertime.on( - 'panstart pinchstart pan pinch panend pinchend', - ({ - type, deltaX, deltaY, scale, - }) => { - viewport.style.cursor = 'move'; // like google maps - const { scale: viewportScale } = store.getState().canvas; + static getMultiTouchDistance(event: TouchEvent) { + if (event.touches.length < 2) { + return 1; + } + const a = event.touches[0]; + const b = event.touches[1]; + return Math.sqrt( + (b.pageX - a.pageX) ** 2 + (b.pageY - a.pageY) ** 2, + ); + } - // pinch start - if (type === 'pinchstart') { - store.dispatch(unsetHover()); - lastScale = viewportScale; + onTouchStart(event: TouchEvent) { + event.preventDefault(); + + this.clickTabStartTime = Date.now(); + this.clickTabStartCoords = PixelPlainterControls.getTouchCenter(event); + const state = this.store.getState(); + this.clickTabStartView = state.canvas.view; + + if (event.touches.length > 1) { + this.startTabScale = state.canvas.scale; + this.startTabDist = PixelPlainterControls.getMultiTouchDistance(event); + this.isMultiTab = true; + } else { + this.isMultiTab = false; + } + } + + onTouchEnd(event: TouchEvent) { + event.preventDefault(); + + if (event.changedTouches.length < 2) { + const { pageX, pageY } = event.changedTouches[0]; + const { clickTabStartCoords, clickTabStartTime } = this; + const coordsDiff = [ + clickTabStartCoords[0] - pageX, + clickTabStartCoords[1] - pageY, + ]; + // thresholds for single click / holding + if (clickTabStartTime > Date.now() - 300 + && coordsDiff[0] < 10 && coordsDiff[1] < 10) { + const { store, viewport } = this; + PixelPlainterControls.placePixel( + store, + viewport, + this.renderer, + [pageX, pageY], + ); + setTimeout(() => { + store.dispatch(unsetHover()); + }, 500); } + } + } - // panstart - if (type === 'panstart') { - store.dispatch(unsetHover()); - const { view: initView } = store.getState().canvas; - [window.lastPosX, window.lastPosY] = initView; - } + onTouchMove(event: TouchEvent) { + event.preventDefault(); - // pinch - if (type === 'pinch') { - store.dispatch(setScale(lastScale * scale)); - } + const multiTouch = (event.touches.length > 1); + // pan + const [clientX, clientY] = PixelPlainterControls.getTouchCenter(event); + const { store } = this; + const state = store.getState(); + if (this.isMultiTab !== multiTouch) { + // if one finger got lifted or added, reset clickTabStart + this.isMultiTab = multiTouch; + this.clickTabStartCoords = [clientX, clientY]; + this.clickTabStartView = state.canvas.view; + this.startTabDist = PixelPlainterControls.getMultiTouchDistance(event); + this.startTabScale = state.canvas.scale; + } else { // pan + const { clickTabStartView, clickTabStartCoords } = this; + const [lastPosX, lastPosY] = clickTabStartView; + + const deltaX = clientX - clickTabStartCoords[0]; + const deltaY = clientY - clickTabStartCoords[1]; + const { scale } = state.canvas; store.dispatch(setViewCoordinates([ - window.lastPosX - (deltaX / viewportScale), - window.lastPosY - (deltaY / viewportScale), + lastPosX - (deltaX / scale), + lastPosY - (deltaY / scale), ])); - // pinch end - if (type === 'pinchend') { - lastScale = viewportScale; + // pinch + if (multiTouch) { + const a = event.touches[0]; + const b = event.touches[1]; + const { startTabDist, startTabScale } = this; + const dist = Math.sqrt( + (b.pageX - a.pageX) ** 2 + (b.pageY - a.pageY) ** 2, + ); + const pinchScale = dist / startTabDist; + store.dispatch(setScale(startTabScale * pinchScale)); } + } + } - // panend - if (type === 'panend') { - store.dispatch(onViewFinishChange()); - const { view } = store.getState().canvas; - [window.lastPosX, window.lastPosY] = view; - viewport.style.cursor = 'auto'; - } - }, - ); + onWheel(event: MouseEvent) { + const { deltaY } = event; + const { store } = this; + const state = store.getState(); + const { hover } = state.gui; + let zoompoint = null; + if (hover) { + zoompoint = hover; + } + if (deltaY < 0) { + store.dispatch(zoomIn(zoompoint)); + } + if (deltaY > 0) { + store.dispatch(zoomOut(zoompoint)); + } + store.dispatch(onViewFinishChange()); + } - document.addEventListener('keydown', onKeyPress, false); + onMouseMove(event: MouseEvent) { + event.preventDefault(); + + const { clientX, clientY } = event; + const { store, isMouseDown } = this; + const state = store.getState(); + if (isMouseDown) { + const { clickTabStartView, clickTabStartCoords } = this; + const [lastPosX, lastPosY] = clickTabStartView; + const deltaX = clientX - clickTabStartCoords[0]; + const deltaY = clientY - clickTabStartCoords[1]; + + const { scale } = state.canvas; + store.dispatch(setViewCoordinates([ + lastPosX - (deltaX / scale), + lastPosY - (deltaY / scale), + ])); + } else { + const screenCoor = screenToWorld( + state, + this.viewport, + [clientX, clientY], + ); + store.dispatch(setHover(screenCoor)); + } + } + + onMouseOut() { + const { store } = this; + store.dispatch(unsetHover()); + } + + onAuxClick(event: MouseEvent) { + const { which, clientX, clientY } = event; + const { store } = this; + // middle mouse button + if (which !== 2) { + return; + } + event.preventDefault(); + const state = store.getState(); + if (state.canvas.scale < 3) { + return; + } + const coords = screenToWorld(state, this.viewport, [clientX, clientY]); + const clrIndex = this.renderer.getColorIndexOfPixel(...coords); + if (clrIndex === null) { + return; + } + store.dispatch(selectColor(clrIndex)); + } + + onKeyPress(event: KeyboardEvent) { + // ignore key presses if modal is open or chat is used + if (event.target.nodeName === 'INPUT' + || event.target.nodeName === 'TEXTAREA' + ) { + return; + } + const { store } = this; + + switch (keycode(event)) { + case 'up': + case 'w': + store.dispatch(moveNorth()); + break; + case 'left': + case 'a': + store.dispatch(moveWest()); + break; + case 'down': + case 's': + store.dispatch(moveSouth()); + break; + case 'right': + case 'd': + store.dispatch(moveEast()); + break; + /* + case 'space': + if ($viewport) $viewport.click(); + return; + */ + case '+': + case 'e': + store.dispatch(zoomIn()); + break; + case '-': + case 'q': + store.dispatch(zoomOut()); + break; + default: + } + } } -export function removeControls() { - document.removeEventListener('keydown', onKeyPress, false); -} +export default PixelPlainterControls; diff --git a/src/ui/Renderer2D.js b/src/ui/Renderer2D.js index 7e8cfe1..aeab6e4 100644 --- a/src/ui/Renderer2D.js +++ b/src/ui/Renderer2D.js @@ -18,10 +18,7 @@ import { renderPlaceholder, renderPotatoPlaceholder, } from './render2Delements'; -import { - initControls, - removeControls, -} from '../controls/PixelPainterControls'; +import PixelPainterControls from '../controls/PixelPainterControls'; import ChunkLoader from './ChunkLoader2D'; @@ -90,7 +87,7 @@ class Renderer { } destructor() { - removeControls(this.viewport); + this.controls.dispose(); window.removeEventListener('resize', this.resizeHandle); this.viewport.remove(); } @@ -120,8 +117,8 @@ class Renderer { canvasSize, } = state.canvas; this.updateCanvasData(state); - initControls(this, this.viewport, store); this.updateScale(viewscale, canvasMaxTiledZoom, view, canvasSize); + this.controls = new PixelPainterControls(this, this.viewport, store); } updateCanvasData(state: State) {