go through store with moveUVW and make movement controls work,

experiment with touch controls
This commit is contained in:
HF 2024-01-22 20:49:42 +01:00
parent 236e83694b
commit a14b16247a
14 changed files with 546 additions and 457 deletions

View File

@ -4,7 +4,10 @@
import { persistStore } from 'redux-persist'; import { persistStore } from 'redux-persist';
import createKeyPressHandler from './controls/keypress'; import {
createKeyDownHandler,
createKeyUpHandler,
} from './controls/keypress';
import { import {
initTimer, initTimer,
urlChange, urlChange,
@ -57,8 +60,10 @@ persistStore(store, {}, () => {
window.name = 'main'; window.name = 'main';
renderApp(document.getElementById('app'), store); renderApp(document.getElementById('app'), store);
const onKeyPress = createKeyPressHandler(store); const onKeyDown = createKeyDownHandler(store);
document.addEventListener('keydown', onKeyPress, false); const onKeyUp = createKeyUpHandler(store);
document.addEventListener('keydown', onKeyDown, false);
document.addEventListener('keyup', onKeyUp, false);
// garbage collection // garbage collection
setInterval(() => { setInterval(() => {

View File

@ -3,29 +3,33 @@
* Menu for WASD keys for mobile users * Menu for WASD keys for mobile users
*/ */
import React from 'react'; import React, {
import { useSelector, shallowEqual } from 'react-redux'; useCallback,
} from 'react';
import {
useSelector,
shallowEqual,
useDispatch,
} from 'react-redux';
import { getRenderer } from '../../ui/rendererFactory'; import {
setMoveU,
setMoveV,
setMoveW,
} from '../../store/actions';
const btnStyle = { const btnStyle = {
fontSize: 34, fontSize: 34,
}; };
function cancelMovement() {
const renderer = getRenderer();
renderer.controls.moveU = 0;
renderer.controls.moveV = 0;
renderer.controls.moveW = 0;
}
const MovementControls = () => { const MovementControls = () => {
const [pencilEnabled, is3D] = useSelector((state) => [ const [holdPaint, is3D] = useSelector((state) => [
state.gui.pencilEnabled, state.gui.holdPaint,
state.canvas.is3D, state.canvas.is3D,
], shallowEqual); ], shallowEqual);
const dispatch = useDispatch();
if (!pencilEnabled && !is3D) { if (!holdPaint && !is3D) {
return null; return null;
} }
@ -37,25 +41,13 @@ const MovementControls = () => {
tabIndex={0} tabIndex={0}
style={{ style={{
...btnStyle, ...btnStyle,
// left: 46,
left: 57, left: 57,
// bottom: 128,
bottom: 139, bottom: 139,
}} }}
onMouseDown={() => { onMouseDown={() => dispatch(setMoveV(-1))}
getRenderer().controls.moveV = -1; onMouseUp={() => dispatch(setMoveV(0))}
}} onTouchStart={() => dispatch(setMoveV(-1))}
onMouseUp={() => { onTouchEnd={() => dispatch(setMoveV(0))}
getRenderer().controls.moveV = 0;
}}
onTouchStart={() => {
getRenderer().controls.moveV = -1;
}}
onTouchEnd={() => {
getRenderer().controls.moveV = 0;
}}
onTouchCancel={cancelMovement}
onMouseLeave={cancelMovement}
> >
</div> </div>
@ -65,24 +57,13 @@ const MovementControls = () => {
tabIndex={0} tabIndex={0}
style={{ style={{
...btnStyle, ...btnStyle,
// left: 46,
left: 57, left: 57,
bottom: 98, bottom: 98,
}} }}
onMouseDown={() => { onMouseDown={() => dispatch(setMoveV(1))}
getRenderer().controls.moveV = 1; onMouseUp={() => dispatch(setMoveV(0))}
}} onTouchStart={(event) => dispatch(setMoveV(1))}
onMouseUp={() => { onTouchEnd={(event) => dispatch(setMoveV(0))}
getRenderer().controls.moveV = 0;
}}
onTouchStart={() => {
getRenderer().controls.moveV = 1;
}}
onTouchEnd={() => {
getRenderer().controls.moveV = 0;
}}
onTouchCancel={cancelMovement}
onMouseLeave={cancelMovement}
> >
</div> </div>
@ -95,20 +76,10 @@ const MovementControls = () => {
left: 16, left: 16,
bottom: 98, bottom: 98,
}} }}
onMouseDown={() => { onMouseDown={() => dispatch(setMoveU(-1))}
getRenderer().controls.moveU = -1; onMouseUp={() => dispatch(setMoveU(0))}
}} onTouchStart={() => dispatch(setMoveU(-1))}
onMouseUp={() => { onTouchEnd={() => dispatch(setMoveU(0))}
getRenderer().controls.moveU = 0;
}}
onTouchStart={() => {
getRenderer().controls.moveU = -1;
}}
onTouchEnd={() => {
getRenderer().controls.moveU = 0;
}}
onTouchCancel={cancelMovement}
onMouseLeave={cancelMovement}
> >
</div> </div>
@ -118,24 +89,13 @@ const MovementControls = () => {
tabIndex={0} tabIndex={0}
style={{ style={{
...btnStyle, ...btnStyle,
// left: 76,
left: 98, left: 98,
bottom: 98, bottom: 98,
}} }}
onMouseDown={() => { onMouseDown={() => dispatch(setMoveU(1))}
getRenderer().controls.moveU = 1; onMouseUp={() => dispatch(setMoveU(0))}
}} onTouchStart={() => dispatch(setMoveU(1))}
onMouseUp={() => { onTouchEnd={() => dispatch(setMoveU(0))}
getRenderer().controls.moveU = 0;
}}
onTouchStart={() => {
getRenderer().controls.moveU = 1;
}}
onTouchEnd={() => {
getRenderer().controls.moveU = 0;
}}
onTouchCancel={cancelMovement}
onMouseLeave={cancelMovement}
> >
</div> </div>
@ -145,24 +105,13 @@ const MovementControls = () => {
tabIndex={0} tabIndex={0}
style={{ style={{
...btnStyle, ...btnStyle,
// left: 76,
left: 16, left: 16,
bottom: 139, bottom: 139,
}} }}
onMouseDown={() => { onMouseDown={() => dispatch(setMoveW(-1))}
getRenderer().controls.moveW = -1; onMouseUp={() => dispatch(setMoveW(0))}
}} onTouchStart={() => dispatch(setMoveW(-1))}
onMouseUp={() => { onTouchEnd={() => dispatch(setMoveW(0))}
getRenderer().controls.moveW = 0;
}}
onTouchStart={() => {
getRenderer().controls.moveW = -1;
}}
onTouchEnd={() => {
getRenderer().controls.moveW = 0;
}}
onTouchCancel={cancelMovement}
onMouseLeave={cancelMovement}
> >
</div> </div>
@ -172,24 +121,13 @@ const MovementControls = () => {
tabIndex={0} tabIndex={0}
style={{ style={{
...btnStyle, ...btnStyle,
// left: 76,
left: 98, left: 98,
bottom: 139, bottom: 139,
}} }}
onMouseDown={() => { onMouseDown={() => dispatch(setMoveW(1))}
getRenderer().controls.moveW = 1; onMouseUp={() => dispatch(setMoveW(0))}
}} onTouchStart={() => dispatch(setMoveW(1))}
onMouseUp={() => { onTouchEnd={() => dispatch(setMoveW(0))}
getRenderer().controls.moveW = 0;
}}
onTouchStart={() => {
getRenderer().controls.moveW = 1;
}}
onTouchEnd={() => {
getRenderer().controls.moveW = 0;
}}
onTouchCancel={cancelMovement}
onMouseLeave={cancelMovement}
> >
</div> </div>

View File

@ -7,22 +7,29 @@ import { useSelector, useDispatch } from 'react-redux';
import { TbPencil, TbPencilMinus } from 'react-icons/tb'; import { TbPencil, TbPencilMinus } from 'react-icons/tb';
import { t } from 'ttag'; import { t } from 'ttag';
import { togglePencil } from '../../store/actions'; import { HOLD_PAINT } from '../../core/constants';
import { selectHoldPaint } from '../../store/actions';
const PencilButton = () => { const PencilButton = () => {
const pencilEnabled = useSelector((state) => state.gui.pencilEnabled); const holdPaint = useSelector((state) => state.gui.holdPaint);
const dispatch = useDispatch(); const dispatch = useDispatch();
return ( return (
<div <div
id="pencilbutton" id="pencilbutton"
className={`actionbuttons${pencilEnabled ? ' pressed' : ''}`} className={
`actionbuttons${(holdPaint === HOLD_PAINT.PENCIL) ? ' pressed' : ''}`
}
role="button" role="button"
title={(pencilEnabled) ? t`Disable Pencil` : t`Enable Pencil`} title={(holdPaint === HOLD_PAINT.PENCIL)
? t`Disable Pencil`
: t`Enable Pencil`}
tabIndex={-1} tabIndex={-1}
onClick={() => dispatch(togglePencil())} onClick={() => dispatch(selectHoldPaint(
(holdPaint === HOLD_PAINT.PENCIL) ? HOLD_PAINT.OFF : HOLD_PAINT.PENCIL,
))}
> >
{pencilEnabled ? <TbPencilMinus /> : <TbPencil />} {(holdPaint === HOLD_PAINT.PENCIL) ? <TbPencilMinus /> : <TbPencil />}
</div> </div>
); );
}; };

View File

@ -17,6 +17,9 @@ import {
getChunkOfPixel, getChunkOfPixel,
getOffsetOfPixel, getOffsetOfPixel,
} from '../core/utils'; } from '../core/utils';
import {
HOLD_PAINT,
} from '../core/constants';
class PixelPainterControls { class PixelPainterControls {
store; store;
@ -27,28 +30,19 @@ class PixelPainterControls {
clickTapStartTime = 0; clickTapStartTime = 0;
clickTapStartCoords = [0, 0]; clickTapStartCoords = [0, 0];
tapStartDist = 50; tapStartDist = 50;
//
// on mouse: true as long as left mouse button is pressed // 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; isClicking = false;
// on touch: true if more than one finger on screen // 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 // on touch: timeout to detect long-press
tapTimeout = null; tapTimeout = null;
/* // time of last tick
* 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;
prevTime = Date.now(); 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; coolDownDelta = false;
constructor(renderer, viewport, store) { constructor(renderer, viewport, store) {
@ -57,8 +51,6 @@ class PixelPainterControls {
this.viewport = viewport; this.viewport = viewport;
this.onMouseDown = this.onMouseDown.bind(this); 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.onAuxClick = this.onAuxClick.bind(this);
this.onMouseOut = this.onMouseOut.bind(this); this.onMouseOut = this.onMouseOut.bind(this);
this.onMouseMove = this.onMouseMove.bind(this); this.onMouseMove = this.onMouseMove.bind(this);
@ -68,8 +60,6 @@ class PixelPainterControls {
this.onTouchEnd = this.onTouchEnd.bind(this); this.onTouchEnd = this.onTouchEnd.bind(this);
this.onTouchMove = this.onTouchMove.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('auxclick', this.onAuxClick, false);
viewport.addEventListener('mousedown', this.onMouseDown, false); viewport.addEventListener('mousedown', this.onMouseDown, false);
viewport.addEventListener('mousemove', this.onMouseMove, false); viewport.addEventListener('mousemove', this.onMouseMove, false);
@ -82,10 +72,8 @@ class PixelPainterControls {
viewport.addEventListener('touchcancel', this.onMouseOut, false); viewport.addEventListener('touchcancel', this.onMouseOut, false);
} }
dispose() { // eslint-disable-next-line class-methods-use-this
document.removeEventListener('keydown', this.onKeyDown, false); dispose() {}
document.removeEventListener('keyup', this.onKeyUp, false);
}
gotCoolDownDelta(delta) { gotCoolDownDelta(delta) {
this.coolDownDelta = true; this.coolDownDelta = true;
@ -129,16 +117,10 @@ class PixelPainterControls {
// thresholds for single click / holding // thresholds for single click / holding
if (clickTapStartTime > Date.now() - 250 if (clickTapStartTime > Date.now() - 250
&& coordsDiff[0] < 2 && coordsDiff[1] < 2) { && coordsDiff[0] < 2 && coordsDiff[1] < 2) {
const cell = screenToWorld(
renderer.view,
renderer.viewscale,
this.viewport,
[clientX, clientY],
);
PixelPainterControls.placePixel( PixelPainterControls.placePixel(
store, store,
renderer, renderer,
cell, this.screenToWorld([clientX, clientY]),
); );
} }
this.viewport.style.cursor = 'auto'; this.viewport.style.cursor = 'auto';
@ -147,22 +129,14 @@ class PixelPainterControls {
} }
static getTouchCenter(event) { static getTouchCenter(event) {
switch (event.touches.length) { let x = 0;
case 1: { let y = 0;
const { pageX, pageY } = event.touches[0]; for (const { pageX, pageY } of event.touches) {
return [pageX, pageY]; x += pageX;
} y += 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; const { length } = event.touches;
return [x / length, y / length];
} }
/* /*
@ -171,22 +145,25 @@ class PixelPainterControls {
*/ */
static placePixel(store, renderer, cell, colorIndex = null) { static placePixel(store, renderer, cell, colorIndex = null) {
const state = store.getState(); const state = store.getState();
const { autoZoomIn } = state.gui; if (state.canvas.isHistoricalView) {
const { clrIgnore, isHistoricalView } = state.canvas; return;
}
const selectedColor = colorIndex
?? PixelPainterControls.getWantedColor(state, renderer, cell);
if (selectedColor === null) {
return;
}
const { viewscale: scale } = renderer; const { viewscale: scale } = renderer;
const selectedColor = (colorIndex === null)
? state.canvas.selectedColor
: colorIndex;
if (isHistoricalView) return; if (state.gui.autoZoomIn && scale < 8) {
if (autoZoomIn && scale < 8) {
renderer.updateView([cell[0], cell[1], 12]); renderer.updateView([cell[0], cell[1], 12]);
return; return;
} }
// allow placing of pixel just on low zoomlevels // allow placing of pixel just on low zoomlevels
if (scale < 3) return; if (scale < 3) {
return;
}
const curColor = renderer.getColorIndexOfPixel(...cell); const curColor = renderer.getColorIndexOfPixel(...cell);
if (selectedColor === curColor) { if (selectedColor === curColor) {
@ -194,7 +171,7 @@ class PixelPainterControls {
} }
// placing unset pixel // placing unset pixel
if (selectedColor < clrIgnore) { if (selectedColor < state.canvas.clrIgnore) {
const { palette } = state.canvas; const { palette } = state.canvas;
const { rgb } = palette; const { rgb } = palette;
let clrOffset = selectedColor * 3; let clrOffset = selectedColor * 3;
@ -249,35 +226,48 @@ class PixelPainterControls {
document.activeElement.blur(); document.activeElement.blur();
this.renderer.cancelStoreViewInState(); this.renderer.cancelStoreViewInState();
this.clearTabTimeout();
this.isTapPainting = false;
this.clickTapStartTime = Date.now(); this.clickTapStartTime = Date.now();
this.clickTapStartCoords = PixelPainterControls.getTouchCenter(event); this.clickTapStartCoords = PixelPainterControls.getTouchCenter(event);
this.clickTapStartView = this.renderer.view; this.clickTapStartView = this.renderer.view;
if (event.touches.length > 1) { if (event.touches.length > 1) {
this.tapStartDist = PixelPainterControls.getMultiTouchDistance(event); this.tapStartDist = PixelPainterControls.getMultiTouchDistance(event);
this.isMultiTab = true; this.isMultiTap = true;
this.clearTabTimeout(); this.wasEverMultiTap = true;
} else { } else {
this.isClicking = true; this.isMultiTap = false;
this.isMultiTab = false; this.wasEverMultiTap = false;
this.tapTimeout = setTimeout(() => { const state = this.store.getState();
// check for longer tap to select taped color if (state.gui.holdPaint) {
PixelPainterControls.selectColor( this.tapTimeout = setTimeout(() => {
this.store, this.isTapPainting = true;
this.viewport, PixelPainterControls.placePixel(
this.renderer, this.store,
this.clickTapStartCoords, this.renderer,
); this.screenToWorld(this.clickTapStartCoords),
}, 600); );
}, 200);
} else {
this.tapTimeout = setTimeout(() => {
// check for longer tap to select taped color
this.selectColorFromScreen(this.clickTapStartCoords);
}, 600);
}
} }
} }
onTouchEnd(event) { onTouchEnd(event) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
if (event.touches.length) {
// still other touches left
return;
}
const { store, renderer } = this; const { store, renderer } = this;
if (event.touches.length === 0 && this.isClicking) { if (!this.wasEverMultiTap) {
const { pageX, pageY } = event.changedTouches[0]; const { pageX, pageY } = event.changedTouches[0];
const { clickTapStartCoords, clickTapStartTime } = this; const { clickTapStartCoords, clickTapStartTime } = this;
const coordsDiff = [ const coordsDiff = [
@ -286,17 +276,12 @@ class PixelPainterControls {
]; ];
// thresholds for single click / holding // thresholds for single click / holding
if (clickTapStartTime > Date.now() - 580 if (clickTapStartTime > Date.now() - 580
&& coordsDiff[0] < 2 && coordsDiff[1] < 2) { && coordsDiff[0] < 2 && coordsDiff[1] < 2
const cell = screenToWorld( ) {
renderer.view,
renderer.viewscale,
this.viewport,
[pageX, pageY],
);
PixelPainterControls.placePixel( PixelPainterControls.placePixel(
store, store,
this.renderer, this.renderer,
cell, this.screenToWorld([pageX, pageY]),
); );
setTimeout(() => { setTimeout(() => {
store.dispatch(unsetHover()); store.dispatch(unsetHover());
@ -312,19 +297,34 @@ class PixelPainterControls {
event.stopPropagation(); event.stopPropagation();
const multiTouch = (event.touches.length > 1); const multiTouch = (event.touches.length > 1);
const state = this.store.getState();
const [clientX, clientY] = PixelPainterControls.getTouchCenter(event); 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 // if one finger got lifted or added, reset clickTabStart
this.isMultiTab = multiTouch; this.isMultiTap = multiTouch;
this.clickTapStartCoords = [clientX, clientY]; this.clickTapStartCoords = [clientX, clientY];
this.clickTapStartView = this.renderer.view; this.clickTapStartView = this.renderer.view;
this.tapStartDist = PixelPainterControls.getMultiTouchDistance(event); this.tapStartDist = PixelPainterControls.getMultiTouchDistance(event);
} else { return;
// pan }
const { clickTapStartView, clickTapStartCoords } = this; 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 [lastPosX, lastPosY] = clickTapStartView;
const deltaX = clientX - clickTapStartCoords[0]; const deltaX = clientX - clickTapStartCoords[0];
const deltaY = clientY - clickTapStartCoords[1]; const deltaY = clientY - clickTapStartCoords[1];
if (deltaX > 2 || deltaY > 2) { if (deltaX > 2 || deltaY > 2) {
@ -335,26 +335,24 @@ class PixelPainterControls {
lastPosX - (deltaX / scale), lastPosX - (deltaX / scale),
lastPosY - (deltaY / scale), lastPosY - (deltaY / scale),
]); ]);
} else if (!this.wasEverMultiTap && !this.coolDownDelta) {
// pinch // hold paint
if (multiTouch) { if (this.isTapPainting) {
this.clearTabTimeout(); PixelPainterControls.placePixel(
this.store,
const a = event.touches[0]; this.renderer,
const b = event.touches[1]; this.screenToWorld([clientX, clientY]),
const { tapStartDist } = this;
const dist = Math.sqrt(
(b.pageX - a.pageX) ** 2 + (b.pageY - a.pageY) ** 2,
); );
const pinchScale = dist / tapStartDist; } else {
const [x, y] = this.renderer.view; // while we are waiting for isTapPainting to trigger track coordinates
this.renderer.updateView([x, y, clickTapStartView[2] * pinchScale]); this.clickTapStartCoords = [clientX, clientY];
this.clickTapStartView = this.renderer.view;
this.tapStartDist = PixelPainterControls.getMultiTouchDistance(event);
} }
} }
} }
clearTabTimeout() { clearTabTimeout() {
this.isClicking = false;
if (this.tapTimeout) { if (this.tapTimeout) {
clearTimeout(this.tapTimeout); clearTimeout(this.tapTimeout);
this.tapTimeout = null; this.tapTimeout = null;
@ -376,6 +374,22 @@ class PixelPainterControls {
this.renderer.storeViewInState(); 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) { onWheel(event) {
event.preventDefault(); event.preventDefault();
document.activeElement.blur(); 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) { onMouseMove(event) {
event.preventDefault(); event.preventDefault();
@ -413,61 +475,18 @@ class PixelPainterControls {
lastPosY - (deltaY / viewscale), lastPosY - (deltaY / viewscale),
]); ]);
} else { } else {
const { store } = this; const hover = this.setHoverFromScrrenCoor([clientX, clientY]);
const state = store.getState(); if (!hover) {
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());
}
return; return;
} }
const state = this.store.getState();
if (!hover || hover[0] !== x || hover[1] !== y) { if (!this.coolDownDelta && state.gui.holdPaint) {
store.dispatch(setHover(screenCoor)); /* hold paint */
/* shift placing */ PixelPainterControls.placePixel(
if (!this.coolDownDelta) { this.store,
switch (this.holdPainting) { this.renderer,
case 1: { hover,
/* 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:
}
}
} }
} }
} }
@ -476,20 +495,15 @@ class PixelPainterControls {
const { store, viewport } = this; const { store, viewport } = this;
viewport.style.cursor = 'auto'; viewport.style.cursor = 'auto';
store.dispatch(unsetHover()); store.dispatch(unsetHover());
this.holdPainting = 0;
this.clearTabTimeout(); this.clearTabTimeout();
} }
static selectColor(store, viewport, renderer, center) { selectColorFromScreen(center) {
if (renderer.viewscale < 3) { const { renderer, store } = this;
if (this.renderer.viewscale < 3) {
return; return;
} }
const coords = screenToWorld( const coords = this.screenToWorld(center);
renderer.view,
renderer.viewscale,
viewport,
center,
);
const clrIndex = renderer.getColorIndexOfPixel(...coords); const clrIndex = renderer.getColorIndexOfPixel(...coords);
if (clrIndex !== null) { if (clrIndex !== null) {
store.dispatch(selectColor(clrIndex)); store.dispatch(selectColor(clrIndex));
@ -503,146 +517,12 @@ class PixelPainterControls {
return; return;
} }
event.preventDefault(); event.preventDefault();
this.selectColorFromScreen([clientX, clientY]);
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:
}
} }
update() { update() {
let time = Date.now(); let time = Date.now();
const { moveU, moveV, moveW } = this; const { moveU, moveV, moveW } = this.store.getState().gui;
if (!(moveU || moveV || moveW)) { if (!(moveU || moveV || moveW)) {
this.prevTime = time; this.prevTime = time;

View File

@ -440,7 +440,7 @@ class VoxelPainterControls {
return; return;
case 'KeyQ': case 'KeyQ':
this.moveW = -1; this.moveW = -1;
return;
default: default:
} }
} }
@ -468,7 +468,7 @@ class VoxelPainterControls {
return; return;
case 'KeyQ': case 'KeyQ':
this.moveW = 0; this.moveW = 0;
return;
default: default:
} }
} }

View File

@ -10,14 +10,72 @@ import {
togglePixelNotify, togglePixelNotify,
toggleMute, toggleMute,
selectCanvas, selectCanvas,
selectHoverColor,
selectHoldPaint,
setMoveU,
setMoveV,
setMoveW,
} from '../store/actions'; } from '../store/actions';
import {
HOLD_PAINT,
} from '../core/constants';
import { import {
notify, notify,
} from '../store/actions/thunks'; } 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) => { return (event) => {
// ignore key presses if modal is open or chat is used // ignore key presses if modal is open or chat is used
if (event.target.nodeName === 'INPUT' if (event.target.nodeName === 'INPUT'
@ -44,12 +102,70 @@ function createKeyPressHandler(store) {
return; 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, * if char of key isn't used by a keybind,
* we check if the key location is where a * we check if the key location is where a
* key that is used would be on QWERTY * key that is used would be on QWERTY
*/ */
if (!usedKeys.includes(key)) { if (!charKeys.includes(key)) {
key = event.code; key = event.code;
if (!key.startsWith('Key')) { if (!key.startsWith('Key')) {
return; return;
@ -98,5 +214,3 @@ function createKeyPressHandler(store) {
} }
}; };
} }
export default createKeyPressHandler;

View File

@ -54,3 +54,10 @@ export const VIEW_UPDATE_DELAY = 1000;
export const MAX_LOADED_CHUNKS = 2000; export const MAX_LOADED_CHUNKS = 2000;
export const MAX_CHUNK_AGE = 300000; export const MAX_CHUNK_AGE = 300000;
export const GC_INTERVAL = 300000; export const GC_INTERVAL = 300000;
export const HOLD_PAINT = {
OFF: 0,
PENCIL: 1,
HISTORY: 2,
OVERLAY: 3,
};

View File

@ -93,9 +93,17 @@ export function toggleOpenPalette() {
}; };
} }
export function togglePencil() { export function selectHoldPaint(value, immediate) {
return { 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) { export function requestBigChunk(center) {
return { return {
type: 'REQ_BIG_CHUNK', type: 'REQ_BIG_CHUNK',

View File

@ -10,6 +10,9 @@ import {
getRenderer, getRenderer,
initRenderer, initRenderer,
} from '../../ui/rendererFactory'; } from '../../ui/rendererFactory';
import {
selectColor,
} from '../actions';
export default (store) => (next) => (action) => { export default (store) => (next) => (action) => {
const { type } = action; const { type } = action;
@ -28,7 +31,14 @@ export default (store) => (next) => (action) => {
); );
break; break;
} }
case 'SELECT_HOVER_COLOR': {
const renderer = getRenderer();
const clr = renderer.getPointedColor();
if (clr !== null) {
store.dispatch(selectColor(clr));
}
break;
}
default: default:
// nothing // nothing
} }
@ -112,7 +122,26 @@ export default (store) => (next) => (action) => {
case 's/TGL_HISTORICAL_VIEW': { case 's/TGL_HISTORICAL_VIEW': {
const renderer = getRenderer(); 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; break;
} }

View File

@ -1,3 +1,5 @@
import { HOLD_PAINT } from '../../core/constants';
const initialState = { const initialState = {
showGrid: false, showGrid: false,
showPixelNotify: false, showPixelNotify: false,
@ -6,7 +8,6 @@ const initialState = {
isLightGrid: false, isLightGrid: false,
compactPalette: false, compactPalette: false,
paletteOpen: true, paletteOpen: true,
pencilEnabled: false,
mute: false, mute: false,
chatNotify: true, chatNotify: true,
// top-left button menu // top-left button menu
@ -15,6 +16,11 @@ const initialState = {
onlineCanvas: false, onlineCanvas: false,
// selected theme // selected theme
style: 'default', 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': { case 's/TGL_OPEN_MENU': {
return { return {
...state, ...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': { case 's/SELECT_STYLE': {
const { style } = action; const { style } = action;
return { return {
@ -117,22 +135,43 @@ export default function gui(
}; };
} }
case 's/TGL_MUTE': case 's/SET_MOVE_U': {
return { return {
...state, ...state,
mute: !state.mute, moveU: action.value,
}; };
}
case 's/TGL_CHAT_NOTIFY': case 's/SET_MOVE_V': {
return { return {
...state, ...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': case 'persist/REHYDRATE':
return { return {
...state, ...state,
pencilEnabled: false, holdPaint: HOLD_PAINT.OFF,
moveU: 0,
moveV: 0,
moveW: 0,
}; };
default: default:

View File

@ -62,7 +62,7 @@ class ChunkLoader2D extends ChunkLoader {
const key = `${this.canvasMaxTiledZoom}:${cx}:${cy}`; const key = `${this.canvasMaxTiledZoom}:${cx}:${cy}`;
const chunk = this.cget(key); const chunk = this.cget(key);
if (!chunk) { if (!chunk) {
return 0; return null;
} }
return chunk.getColorIndex( return chunk.getColorIndex(
getCellInsideChunk(canvasSize, [x, y]), getCellInsideChunk(canvasSize, [x, y]),

View File

@ -95,6 +95,10 @@ class Renderer {
updateCanvasData() {} updateCanvasData() {}
getPointedColor() {
return null;
}
isChunkInView() { isChunkInView() {
return true; return true;
} }

View File

@ -166,6 +166,12 @@ class Renderer2D extends Renderer {
return this.chunkLoader.getColorIndexOfPixel(cx, cy); return this.chunkLoader.getColorIndexOfPixel(cx, cy);
} }
getPointedColor() {
return this.getColorIndexOfPixel(
...this.store.getState().canvas.hover,
);
}
updateView(view, origin) { updateView(view, origin) {
let [x, y, scale] = view; let [x, y, scale] = view;
const state = this.store.getState(); const state = this.store.getState();

View File

@ -35,7 +35,7 @@ import {
import { import {
setHover, setHover,
unsetHover, unsetHover,
selectColor, selectHoverColor,
} from '../store/actions'; } from '../store/actions';
import pixelTransferController from './PixelTransferController'; import pixelTransferController from './PixelTransferController';
@ -526,6 +526,40 @@ class Renderer3D extends Renderer {
this.updateRollOverMesh(0, 0); 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) { placeVoxel(x, y, z, color = null) {
const { const {
store, store,
@ -666,11 +700,8 @@ class Renderer3D extends Renderer {
innerHeight, innerHeight,
} = window; } = window;
const { const {
camera,
objects,
raycaster,
mouse,
store, store,
mouse,
} = this; } = this;
mouse.set( mouse.set(
@ -678,6 +709,18 @@ class Renderer3D extends Renderer {
-(clientY / innerHeight) * 2 + 1, -(clientY / innerHeight) * 2 + 1,
); );
if (button === 1) {
// middle mouse button
store.dispatch(selectHoverColor());
return;
}
const {
camera,
objects,
raycaster,
} = this;
raycaster.setFromCamera(mouse, camera); raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(objects); const intersects = raycaster.intersectObjects(objects);
@ -695,25 +738,6 @@ class Renderer3D extends Renderer {
const [x, y, z] = target.toArray(); const [x, y, z] = target.toArray();
this.placeVoxel(x, y, z); 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) { } else if (button === 2) {
// right mouse button // right mouse button
const target = intersect.point.clone() const target = intersect.point.clone()