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 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(() => {

View File

@ -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))}
>
</div>
@ -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))}
>
</div>
@ -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))}
>
</div>
@ -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))}
>
</div>
@ -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))}
>
</div>
@ -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))}
>
</div>

View File

@ -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 (
<div
id="pencilbutton"
className={`actionbuttons${pencilEnabled ? ' pressed' : ''}`}
className={
`actionbuttons${(holdPaint === HOLD_PAINT.PENCIL) ? ' pressed' : ''}`
}
role="button"
title={(pencilEnabled) ? t`Disable Pencil` : t`Enable Pencil`}
title={(holdPaint === HOLD_PAINT.PENCIL)
? t`Disable Pencil`
: t`Enable Pencil`}
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>
);
};

View File

@ -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;

View File

@ -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:
}
}

View File

@ -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;

View File

@ -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,
};

View File

@ -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',

View File

@ -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;
}

View File

@ -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:

View File

@ -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]),

View File

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

View File

@ -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();

View File

@ -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()