diff --git a/src/client.js b/src/client.js index 07b3e02..95ed7be 100644 --- a/src/client.js +++ b/src/client.js @@ -4,7 +4,10 @@ import { persistStore } from 'redux-persist'; -import createKeyPressHandler from './controls/keypress'; +import { + createKeyDownHandler, + createKeyUpHandler, +} from './controls/keypress'; import { initTimer, urlChange, @@ -57,8 +60,10 @@ persistStore(store, {}, () => { window.name = 'main'; renderApp(document.getElementById('app'), store); - const onKeyPress = createKeyPressHandler(store); - document.addEventListener('keydown', onKeyPress, false); + const onKeyDown = createKeyDownHandler(store); + const onKeyUp = createKeyUpHandler(store); + document.addEventListener('keydown', onKeyDown, false); + document.addEventListener('keyup', onKeyUp, false); // garbage collection setInterval(() => { diff --git a/src/components/buttons/MovementControls.jsx b/src/components/buttons/MovementControls.jsx index 93f0348..02fc145 100644 --- a/src/components/buttons/MovementControls.jsx +++ b/src/components/buttons/MovementControls.jsx @@ -3,29 +3,33 @@ * Menu for WASD keys for mobile users */ -import React from 'react'; -import { useSelector, shallowEqual } from 'react-redux'; +import React, { + useCallback, +} from 'react'; +import { + useSelector, + shallowEqual, + useDispatch, +} from 'react-redux'; -import { getRenderer } from '../../ui/rendererFactory'; +import { + setMoveU, + setMoveV, + setMoveW, +} from '../../store/actions'; const btnStyle = { fontSize: 34, }; -function cancelMovement() { - const renderer = getRenderer(); - renderer.controls.moveU = 0; - renderer.controls.moveV = 0; - renderer.controls.moveW = 0; -} - const MovementControls = () => { - const [pencilEnabled, is3D] = useSelector((state) => [ - state.gui.pencilEnabled, + const [holdPaint, is3D] = useSelector((state) => [ + state.gui.holdPaint, state.canvas.is3D, ], shallowEqual); + const dispatch = useDispatch(); - if (!pencilEnabled && !is3D) { + if (!holdPaint && !is3D) { return null; } @@ -37,25 +41,13 @@ const MovementControls = () => { tabIndex={0} style={{ ...btnStyle, - // left: 46, left: 57, - // bottom: 128, bottom: 139, }} - onMouseDown={() => { - getRenderer().controls.moveV = -1; - }} - onMouseUp={() => { - getRenderer().controls.moveV = 0; - }} - onTouchStart={() => { - getRenderer().controls.moveV = -1; - }} - onTouchEnd={() => { - getRenderer().controls.moveV = 0; - }} - onTouchCancel={cancelMovement} - onMouseLeave={cancelMovement} + onMouseDown={() => dispatch(setMoveV(-1))} + onMouseUp={() => dispatch(setMoveV(0))} + onTouchStart={() => dispatch(setMoveV(-1))} + onTouchEnd={() => dispatch(setMoveV(0))} > ↑ @@ -65,24 +57,13 @@ const MovementControls = () => { tabIndex={0} style={{ ...btnStyle, - // left: 46, left: 57, bottom: 98, }} - onMouseDown={() => { - getRenderer().controls.moveV = 1; - }} - onMouseUp={() => { - getRenderer().controls.moveV = 0; - }} - onTouchStart={() => { - getRenderer().controls.moveV = 1; - }} - onTouchEnd={() => { - getRenderer().controls.moveV = 0; - }} - onTouchCancel={cancelMovement} - onMouseLeave={cancelMovement} + onMouseDown={() => dispatch(setMoveV(1))} + onMouseUp={() => dispatch(setMoveV(0))} + onTouchStart={(event) => dispatch(setMoveV(1))} + onTouchEnd={(event) => dispatch(setMoveV(0))} > ↓ @@ -95,20 +76,10 @@ const MovementControls = () => { left: 16, bottom: 98, }} - onMouseDown={() => { - getRenderer().controls.moveU = -1; - }} - onMouseUp={() => { - getRenderer().controls.moveU = 0; - }} - onTouchStart={() => { - getRenderer().controls.moveU = -1; - }} - onTouchEnd={() => { - getRenderer().controls.moveU = 0; - }} - onTouchCancel={cancelMovement} - onMouseLeave={cancelMovement} + onMouseDown={() => dispatch(setMoveU(-1))} + onMouseUp={() => dispatch(setMoveU(0))} + onTouchStart={() => dispatch(setMoveU(-1))} + onTouchEnd={() => dispatch(setMoveU(0))} > ← @@ -118,24 +89,13 @@ const MovementControls = () => { tabIndex={0} style={{ ...btnStyle, - // left: 76, left: 98, bottom: 98, }} - onMouseDown={() => { - getRenderer().controls.moveU = 1; - }} - onMouseUp={() => { - getRenderer().controls.moveU = 0; - }} - onTouchStart={() => { - getRenderer().controls.moveU = 1; - }} - onTouchEnd={() => { - getRenderer().controls.moveU = 0; - }} - onTouchCancel={cancelMovement} - onMouseLeave={cancelMovement} + onMouseDown={() => dispatch(setMoveU(1))} + onMouseUp={() => dispatch(setMoveU(0))} + onTouchStart={() => dispatch(setMoveU(1))} + onTouchEnd={() => dispatch(setMoveU(0))} > → @@ -145,24 +105,13 @@ const MovementControls = () => { tabIndex={0} style={{ ...btnStyle, - // left: 76, left: 16, bottom: 139, }} - onMouseDown={() => { - getRenderer().controls.moveW = -1; - }} - onMouseUp={() => { - getRenderer().controls.moveW = 0; - }} - onTouchStart={() => { - getRenderer().controls.moveW = -1; - }} - onTouchEnd={() => { - getRenderer().controls.moveW = 0; - }} - onTouchCancel={cancelMovement} - onMouseLeave={cancelMovement} + onMouseDown={() => dispatch(setMoveW(-1))} + onMouseUp={() => dispatch(setMoveW(0))} + onTouchStart={() => dispatch(setMoveW(-1))} + onTouchEnd={() => dispatch(setMoveW(0))} > ↖ @@ -172,24 +121,13 @@ const MovementControls = () => { tabIndex={0} style={{ ...btnStyle, - // left: 76, left: 98, bottom: 139, }} - onMouseDown={() => { - getRenderer().controls.moveW = 1; - }} - onMouseUp={() => { - getRenderer().controls.moveW = 0; - }} - onTouchStart={() => { - getRenderer().controls.moveW = 1; - }} - onTouchEnd={() => { - getRenderer().controls.moveW = 0; - }} - onTouchCancel={cancelMovement} - onMouseLeave={cancelMovement} + onMouseDown={() => dispatch(setMoveW(1))} + onMouseUp={() => dispatch(setMoveW(0))} + onTouchStart={() => dispatch(setMoveW(1))} + onTouchEnd={() => dispatch(setMoveW(0))} > ↘ diff --git a/src/components/buttons/PencilButton.jsx b/src/components/buttons/PencilButton.jsx index 085e51e..ca88b3c 100644 --- a/src/components/buttons/PencilButton.jsx +++ b/src/components/buttons/PencilButton.jsx @@ -7,22 +7,29 @@ import { useSelector, useDispatch } from 'react-redux'; import { TbPencil, TbPencilMinus } from 'react-icons/tb'; import { t } from 'ttag'; -import { togglePencil } from '../../store/actions'; +import { HOLD_PAINT } from '../../core/constants'; +import { selectHoldPaint } from '../../store/actions'; const PencilButton = () => { - const pencilEnabled = useSelector((state) => state.gui.pencilEnabled); + const holdPaint = useSelector((state) => state.gui.holdPaint); const dispatch = useDispatch(); return (
dispatch(togglePencil())} + onClick={() => dispatch(selectHoldPaint( + (holdPaint === HOLD_PAINT.PENCIL) ? HOLD_PAINT.OFF : HOLD_PAINT.PENCIL, + ))} > - {pencilEnabled ? : } + {(holdPaint === HOLD_PAINT.PENCIL) ? : }
); }; diff --git a/src/controls/PixelPainterControls.js b/src/controls/PixelPainterControls.js index a4b0ad5..27bde15 100644 --- a/src/controls/PixelPainterControls.js +++ b/src/controls/PixelPainterControls.js @@ -17,6 +17,9 @@ import { getChunkOfPixel, getOffsetOfPixel, } from '../core/utils'; +import { + HOLD_PAINT, +} from '../core/constants'; class PixelPainterControls { store; @@ -27,28 +30,19 @@ class PixelPainterControls { clickTapStartTime = 0; clickTapStartCoords = [0, 0]; tapStartDist = 50; - // // on mouse: true as long as left mouse button is pressed - // on touch: set to true when one finger touches the screen - // set to false when second finger touches or touch ends isClicking = false; // on touch: true if more than one finger on screen - isMultiTab = false; + isMultiTap = false; + // on touch: true if current tab was ever more than one figher at any time + wasEverMultiTap = false; + // on touch: when painting with holdPaint is active + isTapPainting = false; // on touch: timeout to detect long-press tapTimeout = null; - /* - * if we are shift-hold-painting - * 0: no - * 1: left shift - * 2: right shift - */ - holdPainting = 0; - // if we are moving - moveU = 0; - moveV = 0; - moveW = 0; + // time of last tick prevTime = Date.now(); - // if we are waiting before placing pixel via holdPainting again + // if we are waiting before placing pixel via holdPaint again coolDownDelta = false; constructor(renderer, viewport, store) { @@ -57,8 +51,6 @@ class PixelPainterControls { this.viewport = viewport; this.onMouseDown = this.onMouseDown.bind(this); - this.onKeyDown = this.onKeyDown.bind(this); - this.onKeyUp = this.onKeyUp.bind(this); this.onAuxClick = this.onAuxClick.bind(this); this.onMouseOut = this.onMouseOut.bind(this); this.onMouseMove = this.onMouseMove.bind(this); @@ -68,8 +60,6 @@ class PixelPainterControls { this.onTouchEnd = this.onTouchEnd.bind(this); this.onTouchMove = this.onTouchMove.bind(this); - document.addEventListener('keydown', this.onKeyDown, false); - document.addEventListener('keyup', this.onKeyUp, false); viewport.addEventListener('auxclick', this.onAuxClick, false); viewport.addEventListener('mousedown', this.onMouseDown, false); viewport.addEventListener('mousemove', this.onMouseMove, false); @@ -82,10 +72,8 @@ class PixelPainterControls { viewport.addEventListener('touchcancel', this.onMouseOut, false); } - dispose() { - document.removeEventListener('keydown', this.onKeyDown, false); - document.removeEventListener('keyup', this.onKeyUp, false); - } + // eslint-disable-next-line class-methods-use-this + dispose() {} gotCoolDownDelta(delta) { this.coolDownDelta = true; @@ -129,16 +117,10 @@ class PixelPainterControls { // thresholds for single click / holding if (clickTapStartTime > Date.now() - 250 && coordsDiff[0] < 2 && coordsDiff[1] < 2) { - const cell = screenToWorld( - renderer.view, - renderer.viewscale, - this.viewport, - [clientX, clientY], - ); PixelPainterControls.placePixel( store, renderer, - cell, + this.screenToWorld([clientX, clientY]), ); } this.viewport.style.cursor = 'auto'; @@ -147,22 +129,14 @@ class PixelPainterControls { } static getTouchCenter(event) { - 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; + let x = 0; + let y = 0; + for (const { pageX, pageY } of event.touches) { + x += pageX; + y += pageY; } - return null; + const { length } = event.touches; + return [x / length, y / length]; } /* @@ -171,22 +145,25 @@ class PixelPainterControls { */ static placePixel(store, renderer, cell, colorIndex = null) { const state = store.getState(); - const { autoZoomIn } = state.gui; - const { clrIgnore, isHistoricalView } = state.canvas; + if (state.canvas.isHistoricalView) { + return; + } + const selectedColor = colorIndex + ?? PixelPainterControls.getWantedColor(state, renderer, cell); + if (selectedColor === null) { + return; + } const { viewscale: scale } = renderer; - const selectedColor = (colorIndex === null) - ? state.canvas.selectedColor - : colorIndex; - if (isHistoricalView) return; - - if (autoZoomIn && scale < 8) { + if (state.gui.autoZoomIn && scale < 8) { renderer.updateView([cell[0], cell[1], 12]); return; } // allow placing of pixel just on low zoomlevels - if (scale < 3) return; + if (scale < 3) { + return; + } const curColor = renderer.getColorIndexOfPixel(...cell); if (selectedColor === curColor) { @@ -194,7 +171,7 @@ class PixelPainterControls { } // placing unset pixel - if (selectedColor < clrIgnore) { + if (selectedColor < state.canvas.clrIgnore) { const { palette } = state.canvas; const { rgb } = palette; let clrOffset = selectedColor * 3; @@ -249,35 +226,48 @@ class PixelPainterControls { document.activeElement.blur(); this.renderer.cancelStoreViewInState(); + this.clearTabTimeout(); + this.isTapPainting = false; this.clickTapStartTime = Date.now(); this.clickTapStartCoords = PixelPainterControls.getTouchCenter(event); this.clickTapStartView = this.renderer.view; if (event.touches.length > 1) { this.tapStartDist = PixelPainterControls.getMultiTouchDistance(event); - this.isMultiTab = true; - this.clearTabTimeout(); + this.isMultiTap = true; + this.wasEverMultiTap = true; } else { - this.isClicking = true; - this.isMultiTab = false; - this.tapTimeout = setTimeout(() => { - // check for longer tap to select taped color - PixelPainterControls.selectColor( - this.store, - this.viewport, - this.renderer, - this.clickTapStartCoords, - ); - }, 600); + this.isMultiTap = false; + this.wasEverMultiTap = false; + const state = this.store.getState(); + if (state.gui.holdPaint) { + this.tapTimeout = setTimeout(() => { + this.isTapPainting = true; + PixelPainterControls.placePixel( + this.store, + this.renderer, + this.screenToWorld(this.clickTapStartCoords), + ); + }, 200); + } else { + this.tapTimeout = setTimeout(() => { + // check for longer tap to select taped color + this.selectColorFromScreen(this.clickTapStartCoords); + }, 600); + } } } onTouchEnd(event) { event.preventDefault(); event.stopPropagation(); + if (event.touches.length) { + // still other touches left + return; + } const { store, renderer } = this; - if (event.touches.length === 0 && this.isClicking) { + if (!this.wasEverMultiTap) { const { pageX, pageY } = event.changedTouches[0]; const { clickTapStartCoords, clickTapStartTime } = this; const coordsDiff = [ @@ -286,17 +276,12 @@ class PixelPainterControls { ]; // thresholds for single click / holding if (clickTapStartTime > Date.now() - 580 - && coordsDiff[0] < 2 && coordsDiff[1] < 2) { - const cell = screenToWorld( - renderer.view, - renderer.viewscale, - this.viewport, - [pageX, pageY], - ); + && coordsDiff[0] < 2 && coordsDiff[1] < 2 + ) { PixelPainterControls.placePixel( store, this.renderer, - cell, + this.screenToWorld([pageX, pageY]), ); setTimeout(() => { store.dispatch(unsetHover()); @@ -312,19 +297,34 @@ class PixelPainterControls { event.stopPropagation(); const multiTouch = (event.touches.length > 1); + const state = this.store.getState(); const [clientX, clientY] = PixelPainterControls.getTouchCenter(event); - if (this.isMultiTab !== multiTouch) { + if (this.isMultiTap !== multiTouch) { + this.wasEverMultiTap = true; // if one finger got lifted or added, reset clickTabStart - this.isMultiTab = multiTouch; + this.isMultiTap = multiTouch; this.clickTapStartCoords = [clientX, clientY]; this.clickTapStartView = this.renderer.view; this.tapStartDist = PixelPainterControls.getMultiTouchDistance(event); - } else { - // pan - const { clickTapStartView, clickTapStartCoords } = this; + return; + } + const { clickTapStartView, clickTapStartCoords } = this; + // pinch + if (multiTouch) { + this.clearTabTimeout(); + const a = event.touches[0]; + const b = event.touches[1]; + const dist = Math.sqrt( + (b.pageX - a.pageX) ** 2 + (b.pageY - a.pageY) ** 2, + ); + const pinchScale = dist / this.tapStartDist; + const [x, y] = this.renderer.view; + this.renderer.updateView([x, y, clickTapStartView[2] * pinchScale]); + } + // pan + if (!state.gui.holdPaint || multiTouch) { const [lastPosX, lastPosY] = clickTapStartView; - const deltaX = clientX - clickTapStartCoords[0]; const deltaY = clientY - clickTapStartCoords[1]; if (deltaX > 2 || deltaY > 2) { @@ -335,26 +335,24 @@ class PixelPainterControls { lastPosX - (deltaX / scale), lastPosY - (deltaY / scale), ]); - - // pinch - if (multiTouch) { - this.clearTabTimeout(); - - const a = event.touches[0]; - const b = event.touches[1]; - const { tapStartDist } = this; - const dist = Math.sqrt( - (b.pageX - a.pageX) ** 2 + (b.pageY - a.pageY) ** 2, + } else if (!this.wasEverMultiTap && !this.coolDownDelta) { + // hold paint + if (this.isTapPainting) { + PixelPainterControls.placePixel( + this.store, + this.renderer, + this.screenToWorld([clientX, clientY]), ); - const pinchScale = dist / tapStartDist; - const [x, y] = this.renderer.view; - this.renderer.updateView([x, y, clickTapStartView[2] * pinchScale]); + } else { + // while we are waiting for isTapPainting to trigger track coordinates + this.clickTapStartCoords = [clientX, clientY]; + this.clickTapStartView = this.renderer.view; + this.tapStartDist = PixelPainterControls.getMultiTouchDistance(event); } } } clearTabTimeout() { - this.isClicking = false; if (this.tapTimeout) { clearTimeout(this.tapTimeout); this.tapTimeout = null; @@ -376,6 +374,22 @@ class PixelPainterControls { this.renderer.storeViewInState(); } + holdPaintStarted(immediate) { + // if hold painting is started by keyboard, + // we immeidately have to place, and not just when mousemove starts + if (!immediate) { + return; + } + const { hover } = this.store.getState().canvas; + if (hover) { + PixelPainterControls.placePixel( + this.store, + this.renderer, + hover, + ); + } + } + onWheel(event) { event.preventDefault(); document.activeElement.blur(); @@ -392,6 +406,54 @@ class PixelPainterControls { } } + static getWantedColor(state, renderer, cell) { + if (state.gui.holdPaint === HOLD_PAINT.HISTORY) { + return renderer.getColorIndexOfPixel(...cell, true); + } + return state.canvas.selectedColor; + } + + screenToWorld(screenCoor) { + return screenToWorld( + this.renderer.view, + this.renderer.viewscale, + this.viewport, + screenCoor, + ); + } + + /* + * set hover from screen coordinates + * @param [x, y] screen coordinates + * @return null if hover didn't changed, + * hover if it changed + */ + setHoverFromScrrenCoor(screenCoor) { + const { store } = this; + const state = store.getState(); + const { hover: prevHover } = state.canvas; + const hover = this.screenToWorld(screenCoor); + const [x, y] = hover; + + /* out of bounds check */ + const { canvasSize } = state.canvas; + const maxCoords = canvasSize / 2; + if (x < -maxCoords || x >= maxCoords + || y < -maxCoords || y >= maxCoords + ) { + if (prevHover) { + store.dispatch(unsetHover()); + } + return null; + } + + if (!prevHover || prevHover[0] !== x || prevHover[1] !== y) { + store.dispatch(setHover(hover)); + return hover; + } + return null; + } + onMouseMove(event) { event.preventDefault(); @@ -413,61 +475,18 @@ class PixelPainterControls { lastPosY - (deltaY / viewscale), ]); } else { - const { store } = this; - const state = store.getState(); - const { hover } = state.canvas; - const { view } = renderer; - const screenCoor = screenToWorld( - view, - viewscale, - this.viewport, - [clientX, clientY], - ); - const [x, y] = screenCoor; - - /* out of bounds check */ - const { canvasSize } = state.canvas; - const maxCoords = canvasSize / 2; - if (x < -maxCoords || x >= maxCoords - || y < -maxCoords || y >= maxCoords - ) { - if (hover) { - store.dispatch(unsetHover()); - } + const hover = this.setHoverFromScrrenCoor([clientX, clientY]); + if (!hover) { return; } - - if (!hover || hover[0] !== x || hover[1] !== y) { - store.dispatch(setHover(screenCoor)); - /* shift placing */ - if (!this.coolDownDelta) { - switch (this.holdPainting) { - case 1: { - /* left shift: from selected color */ - PixelPainterControls.placePixel( - store, - this.renderer, - screenCoor, - ); - break; - } - case 2: { - /* right shift: from historical view */ - const colorIndex = this.renderer - .getColorIndexOfPixel(x, y, true); - if (colorIndex !== null) { - PixelPainterControls.placePixel( - store, - this.renderer, - screenCoor, - colorIndex, - ); - } - break; - } - default: - } - } + const state = this.store.getState(); + if (!this.coolDownDelta && state.gui.holdPaint) { + /* hold paint */ + PixelPainterControls.placePixel( + this.store, + this.renderer, + hover, + ); } } } @@ -476,20 +495,15 @@ class PixelPainterControls { const { store, viewport } = this; viewport.style.cursor = 'auto'; store.dispatch(unsetHover()); - this.holdPainting = 0; this.clearTabTimeout(); } - static selectColor(store, viewport, renderer, center) { - if (renderer.viewscale < 3) { + selectColorFromScreen(center) { + const { renderer, store } = this; + if (this.renderer.viewscale < 3) { return; } - const coords = screenToWorld( - renderer.view, - renderer.viewscale, - viewport, - center, - ); + const coords = this.screenToWorld(center); const clrIndex = renderer.getColorIndexOfPixel(...coords); if (clrIndex !== null) { store.dispatch(selectColor(clrIndex)); @@ -503,146 +517,12 @@ class PixelPainterControls { return; } event.preventDefault(); - - PixelPainterControls.selectColor( - this.store, - this.viewport, - this.renderer, - [clientX, clientY], - ); - } - - onKeyUp(event) { - /* - * key locations - */ - switch (event.code) { - case 'ArrowUp': - case 'KeyW': - this.moveV = 0; - return; - case 'ArrowLeft': - case 'KeyA': - this.moveU = 0; - return; - case 'ArrowDown': - case 'KeyS': - this.moveV = 0; - return; - case 'ArrowRight': - case 'KeyD': - this.moveU = 0; - return; - case 'KeyE': - this.moveW = 0; - return; - case 'KeyQ': - this.moveW = 0; - return; - default: - } - - /* - * key char - */ - switch (event.key) { - case 'Shift': - case 'CapsLock': - this.holdPainting = 0; - break; - default: - } - } - - onKeyDown(event) { - // ignore key presses if modal is open or chat is used - if (event.target.nodeName === 'INPUT' - || event.target.nodeName === 'TEXTAREA' - ) { - return; - } - - /* - * key location - */ - switch (event.code) { - case 'ArrowUp': - case 'KeyW': - this.moveV = -1; - return; - case 'ArrowLeft': - case 'KeyA': - this.moveU = -1; - return; - case 'ArrowDown': - case 'KeyS': - this.moveV = 1; - return; - case 'ArrowRight': - case 'KeyD': - this.moveU = 1; - return; - case 'KeyE': - this.moveW = 1; - return; - case 'KeyQ': - this.moveW = -1; - return; - default: - } - - /* - * key char - */ - switch (event.key) { - case '+': - this.zoom(1); - return; - case '-': - this.zoom(-1); - return; - case 'Control': - case 'Shift': { - const { store } = this; - const state = store.getState(); - const { hover } = state.canvas; - if (hover) { - if (event.key === 'Control') { - // ctrl - const clrIndex = this.renderer.getColorIndexOfPixel(...hover); - store.dispatch(selectColor(clrIndex)); - return; - } - if (event.location === KeyboardEvent.DOM_KEY_LOCATION_LEFT) { - // left shift - this.holdPainting = 1; - PixelPainterControls.placePixel(store, this.renderer, hover); - return; - } - if (event.location === KeyboardEvent.DOM_KEY_LOCATION_RIGHT) { - // right shift - this.holdPainting = 2; - const colorIndex = this.renderer - .getColorIndexOfPixel(...hover, true); - if (colorIndex !== null) { - PixelPainterControls.placePixel( - store, - this.renderer, - hover, - colorIndex, - ); - } - } - } - break; - } - default: - } + this.selectColorFromScreen([clientX, clientY]); } update() { let time = Date.now(); - const { moveU, moveV, moveW } = this; + const { moveU, moveV, moveW } = this.store.getState().gui; if (!(moveU || moveV || moveW)) { this.prevTime = time; diff --git a/src/controls/VoxelPainterControls.js b/src/controls/VoxelPainterControls.js index 5c55e82..881ca33 100644 --- a/src/controls/VoxelPainterControls.js +++ b/src/controls/VoxelPainterControls.js @@ -440,7 +440,7 @@ class VoxelPainterControls { return; case 'KeyQ': this.moveW = -1; - return; + default: } } @@ -468,7 +468,7 @@ class VoxelPainterControls { return; case 'KeyQ': this.moveW = 0; - return; + default: } } diff --git a/src/controls/keypress.js b/src/controls/keypress.js index 2120375..eb2f6d6 100644 --- a/src/controls/keypress.js +++ b/src/controls/keypress.js @@ -10,14 +10,72 @@ import { togglePixelNotify, toggleMute, selectCanvas, + selectHoverColor, + selectHoldPaint, + setMoveU, + setMoveV, + setMoveW, } from '../store/actions'; +import { + HOLD_PAINT, +} from '../core/constants'; import { notify, } from '../store/actions/thunks'; -const usedKeys = ['g', 'h', 'x', 'm', 'r', 'p']; +const charKeys = ['g', 'h', 'x', 'm', 'r', 'p', '+', '-']; -function createKeyPressHandler(store) { +export function createKeyUpHandler(store) { + return (event) => { + /* + * key locations + */ + switch (event.code) { + case 'ArrowUp': + case 'KeyW': + store.dispatch(setMoveV(0)); + return; + case 'ArrowLeft': + case 'KeyA': + store.dispatch(setMoveU(0)); + return; + case 'ArrowDown': + case 'KeyS': + store.dispatch(setMoveV(0)); + return; + case 'ArrowRight': + case 'KeyD': + store.dispatch(setMoveU(0)); + return; + case 'KeyE': + store.dispatch(setMoveW(0)); + return; + case 'KeyQ': + store.dispatch(setMoveW(0)); + return; + default: + } + + /* + * key char + */ + switch (event.key) { + case '+': + store.dispatch(setMoveW(0)); + return; + case '-': + store.dispatch(setMoveW(0)); + return; + case 'Shift': + case 'CapsLock': + store.dispatch(selectHoldPaint(HOLD_PAINT.OFF)); + break; + default: + } + }; +} + +export function createKeyDownHandler(store) { return (event) => { // ignore key presses if modal is open or chat is used if (event.target.nodeName === 'INPUT' @@ -44,12 +102,70 @@ function createKeyPressHandler(store) { return; } + /* + * key locations + */ + switch (event.code) { + case 'ArrowUp': + case 'KeyW': + store.dispatch(setMoveV(-1)); + return; + case 'ArrowLeft': + case 'KeyA': + store.dispatch(setMoveU(-1)); + return; + case 'ArrowDown': + case 'KeyS': + store.dispatch(setMoveV(1)); + return; + case 'ArrowRight': + case 'KeyD': + store.dispatch(setMoveU(1)); + return; + case 'KeyE': + store.dispatch(setMoveW(1)); + return; + case 'KeyQ': + store.dispatch(setMoveW(-1)); + return; + default: + } + + /* + * key char + */ + switch (event.key) { + case '+': + store.dispatch(setMoveW(1)); + return; + case '-': + store.dispatch(setMoveW(-1)); + return; + case 'Control': + store.dispatch(selectHoverColor(-1)); + return; + case 'Shift': { + if (event.location === KeyboardEvent.DOM_KEY_LOCATION_LEFT) { + // left shift + store.dispatch(selectHoldPaint(HOLD_PAINT.PENCIL), true); + return; + } + if (event.location === KeyboardEvent.DOM_KEY_LOCATION_RIGHT) { + // right shift + store.dispatch(selectHoldPaint(HOLD_PAINT.HISTORY), true); + return; + } + return; + } + default: + } + /* * if char of key isn't used by a keybind, * we check if the key location is where a * key that is used would be on QWERTY */ - if (!usedKeys.includes(key)) { + if (!charKeys.includes(key)) { key = event.code; if (!key.startsWith('Key')) { return; @@ -98,5 +214,3 @@ function createKeyPressHandler(store) { } }; } - -export default createKeyPressHandler; diff --git a/src/core/constants.js b/src/core/constants.js index e08b0de..4eb35ba 100644 --- a/src/core/constants.js +++ b/src/core/constants.js @@ -54,3 +54,10 @@ export const VIEW_UPDATE_DELAY = 1000; export const MAX_LOADED_CHUNKS = 2000; export const MAX_CHUNK_AGE = 300000; export const GC_INTERVAL = 300000; + +export const HOLD_PAINT = { + OFF: 0, + PENCIL: 1, + HISTORY: 2, + OVERLAY: 3, +}; diff --git a/src/store/actions/index.js b/src/store/actions/index.js index 1129351..b2a1b0f 100644 --- a/src/store/actions/index.js +++ b/src/store/actions/index.js @@ -93,9 +93,17 @@ export function toggleOpenPalette() { }; } -export function togglePencil() { +export function selectHoldPaint(value, immediate) { return { - type: 's/TGL_PENCIL', + type: 's/SELECT_HOLD_PAINT', + value, + immediate, + }; +} + +export function selectHoverColor() { + return { + type: 'SELECT_HOVER_COLOR', }; } @@ -174,6 +182,34 @@ export function setScale(scale, zoompoint) { }; } +export function setMoveU(value) { + return { + type: 's/SET_MOVE_U', + value, + }; +} + +export function setMoveV(value) { + return { + type: 's/SET_MOVE_V', + value, + }; +} + +export function setMoveW(value) { + return { + type: 's/SET_MOVE_W', + value, + }; +} + +export function cancelMove(value) { + return { + type: 's/CANCEL_MOVE', + value, + }; +} + export function requestBigChunk(center) { return { type: 'REQ_BIG_CHUNK', diff --git a/src/store/middleware/rendererHook.js b/src/store/middleware/rendererHook.js index 7ef68c0..c3d5ffe 100644 --- a/src/store/middleware/rendererHook.js +++ b/src/store/middleware/rendererHook.js @@ -10,6 +10,9 @@ import { getRenderer, initRenderer, } from '../../ui/rendererFactory'; +import { + selectColor, +} from '../actions'; export default (store) => (next) => (action) => { const { type } = action; @@ -28,7 +31,14 @@ export default (store) => (next) => (action) => { ); break; } - + case 'SELECT_HOVER_COLOR': { + const renderer = getRenderer(); + const clr = renderer.getPointedColor(); + if (clr !== null) { + store.dispatch(selectColor(clr)); + } + break; + } default: // nothing } @@ -112,7 +122,26 @@ export default (store) => (next) => (action) => { case 's/TGL_HISTORICAL_VIEW': { const renderer = getRenderer(); - renderer.updateView(state.view); + renderer.updateView(state.canvas.view); + break; + } + + case 's/SELECT_HOLD_PAINT': { + if (action.value) { + const renderer = getRenderer(); + renderer.controls?.holdPaintStarted?.(action.immediate); + } + break; + } + + case 's/SET_MOVE_U': + case 's/SET_MOVE_V': + case 's/SET_MOVE_W': + case 's/CANCEL_MOVE': { + if (!action.value) { + const renderer = getRenderer(); + renderer.storeViewInState(); + } break; } diff --git a/src/store/reducers/gui.js b/src/store/reducers/gui.js index 78fb596..dc27bf8 100644 --- a/src/store/reducers/gui.js +++ b/src/store/reducers/gui.js @@ -1,3 +1,5 @@ +import { HOLD_PAINT } from '../../core/constants'; + const initialState = { showGrid: false, showPixelNotify: false, @@ -6,7 +8,6 @@ const initialState = { isLightGrid: false, compactPalette: false, paletteOpen: true, - pencilEnabled: false, mute: false, chatNotify: true, // top-left button menu @@ -15,6 +16,11 @@ const initialState = { onlineCanvas: false, // selected theme style: 'default', + // properties that aren't saved + holdPaint: HOLD_PAINT.OFF, + moveU: 0, + moveV: 0, + moveW: 0, }; @@ -79,13 +85,6 @@ export default function gui( }; } - case 's/TGL_PENCIL': { - return { - ...state, - pencilEnabled: !state.pencilEnabled, - }; - } - case 's/TGL_OPEN_MENU': { return { ...state, @@ -93,6 +92,25 @@ export default function gui( }; } + case 's/TGL_MUTE': + return { + ...state, + mute: !state.mute, + }; + + case 's/TGL_CHAT_NOTIFY': + return { + ...state, + chatNotify: !state.chatNotify, + }; + + case 's/SELECT_HOLD_PAINT': { + return { + ...state, + holdPaint: action.value, + }; + } + case 's/SELECT_STYLE': { const { style } = action; return { @@ -117,22 +135,43 @@ export default function gui( }; } - case 's/TGL_MUTE': + case 's/SET_MOVE_U': { return { ...state, - mute: !state.mute, + moveU: action.value, }; + } - case 's/TGL_CHAT_NOTIFY': + case 's/SET_MOVE_V': { return { ...state, - chatNotify: !state.chatNotify, + moveV: action.value, }; + } + + case 's/SET_MOVE_W': { + return { + ...state, + moveW: action.value, + }; + } + + case 's/CANCEL_MOVE': { + return { + ...state, + moveU: 0, + moveV: 0, + moveW: 0, + }; + } case 'persist/REHYDRATE': return { ...state, - pencilEnabled: false, + holdPaint: HOLD_PAINT.OFF, + moveU: 0, + moveV: 0, + moveW: 0, }; default: diff --git a/src/ui/ChunkLoader2D.js b/src/ui/ChunkLoader2D.js index a3c48f9..d36611f 100644 --- a/src/ui/ChunkLoader2D.js +++ b/src/ui/ChunkLoader2D.js @@ -62,7 +62,7 @@ class ChunkLoader2D extends ChunkLoader { const key = `${this.canvasMaxTiledZoom}:${cx}:${cy}`; const chunk = this.cget(key); if (!chunk) { - return 0; + return null; } return chunk.getColorIndex( getCellInsideChunk(canvasSize, [x, y]), diff --git a/src/ui/Renderer.js b/src/ui/Renderer.js index 951cdbe..d17157a 100644 --- a/src/ui/Renderer.js +++ b/src/ui/Renderer.js @@ -95,6 +95,10 @@ class Renderer { updateCanvasData() {} + getPointedColor() { + return null; + } + isChunkInView() { return true; } diff --git a/src/ui/Renderer2D.js b/src/ui/Renderer2D.js index c0fc74e..71ff57c 100644 --- a/src/ui/Renderer2D.js +++ b/src/ui/Renderer2D.js @@ -166,6 +166,12 @@ class Renderer2D extends Renderer { return this.chunkLoader.getColorIndexOfPixel(cx, cy); } + getPointedColor() { + return this.getColorIndexOfPixel( + ...this.store.getState().canvas.hover, + ); + } + updateView(view, origin) { let [x, y, scale] = view; const state = this.store.getState(); diff --git a/src/ui/Renderer3D.js b/src/ui/Renderer3D.js index a92265c..a2b0e10 100644 --- a/src/ui/Renderer3D.js +++ b/src/ui/Renderer3D.js @@ -35,7 +35,7 @@ import { import { setHover, unsetHover, - selectColor, + selectHoverColor, } from '../store/actions'; import pixelTransferController from './PixelTransferController'; @@ -526,6 +526,40 @@ class Renderer3D extends Renderer { this.updateRollOverMesh(0, 0); } + getPointedColor() { + const { + objects, + raycaster, + mouse, + camera, + } = this; + raycaster.setFromCamera(mouse, camera); + + const intersects = raycaster.intersectObjects(objects); + if (intersects.length <= 0) { + return null; + } + const intersect = intersects[0]; + const target = intersect.point.clone() + .add(intersect.face.normal.multiplyScalar(-0.5)) + .floor() + .addScalar(0.5) + .floor(); + if (target.y < 0) { + return null; + } + if (target.clone().sub(camera.position).length() < 120) { + const cell = target.toArray(); + if (this.chunkLoader) { + const clr = this.chunkLoader.getVoxel(...cell); + if (clr) { + return clr; + } + } + } + return null; + } + placeVoxel(x, y, z, color = null) { const { store, @@ -666,11 +700,8 @@ class Renderer3D extends Renderer { innerHeight, } = window; const { - camera, - objects, - raycaster, - mouse, store, + mouse, } = this; mouse.set( @@ -678,6 +709,18 @@ class Renderer3D extends Renderer { -(clientY / innerHeight) * 2 + 1, ); + if (button === 1) { + // middle mouse button + store.dispatch(selectHoverColor()); + return; + } + + const { + camera, + objects, + raycaster, + } = this; + raycaster.setFromCamera(mouse, camera); const intersects = raycaster.intersectObjects(objects); @@ -695,25 +738,6 @@ class Renderer3D extends Renderer { const [x, y, z] = target.toArray(); this.placeVoxel(x, y, z); } - } else if (button === 1) { - // middle mouse button - const target = intersect.point.clone() - .add(intersect.face.normal.multiplyScalar(-0.5)) - .floor() - .addScalar(0.5) - .floor(); - if (target.y < 0) { - return; - } - if (target.clone().sub(camera.position).length() < 120) { - const cell = target.toArray(); - if (this.chunkLoader) { - const clr = this.chunkLoader.getVoxel(...cell); - if (clr) { - store.dispatch(selectColor(clr)); - } - } - } } else if (button === 2) { // right mouse button const target = intersect.point.clone()