prefetch /api/me

move scale into view and move view from store into renderer
(breaks WASD and 3D is unfinished)
This commit is contained in:
HF 2024-01-19 23:07:47 +01:00
parent 1077831c23
commit 0254f7d820
24 changed files with 407 additions and 528 deletions

View File

@ -17,7 +17,7 @@ import {
import pixelTransferController from './ui/PixelTransferController'; import pixelTransferController from './ui/PixelTransferController';
import store from './store/store'; import store from './store/store';
import renderApp from './components/App'; import renderApp from './components/App';
import { initRenderer, getRenderer } from './ui/rendererFactory'; import { getRenderer } from './ui/rendererFactory';
import socketClient from './socket/SocketClient'; import socketClient from './socket/SocketClient';
import { GC_INTERVAL } from './core/constants'; import { GC_INTERVAL } from './core/constants';
@ -26,8 +26,6 @@ persistStore(store, {}, () => {
store.dispatch({ type: 'HYDRATED' }); store.dispatch({ type: 'HYDRATED' });
initRenderer(store, false);
pixelTransferController.initialize(store, socketClient, getRenderer); pixelTransferController.initialize(store, socketClient, getRenderer);
window.addEventListener('hashchange', () => { window.addEventListener('hashchange', () => {

View File

@ -3,7 +3,7 @@
*/ */
import React from 'react'; import React from 'react';
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, shallowEqual, useDispatch } from 'react-redux';
import { t } from 'ttag'; import { t } from 'ttag';
import copy from '../utils/clipboard'; import copy from '../utils/clipboard';
@ -16,10 +16,21 @@ function renderCoordinates(cell) {
const CoordinatesBox = () => { const CoordinatesBox = () => {
const view = useSelector((state) => state.canvas.view); const [view, hover, is3D] = useSelector((state) => [
const hover = useSelector((state) => state.canvas.hover); state.canvas.view,
state.canvas.hover,
state.canvas.is3D,
], shallowEqual);
const dispatch = useDispatch(); const dispatch = useDispatch();
let coords;
if (hover) {
coords = hover;
} else {
const [x, y, z] = view;
coords = (is3D ? [x, y, z] : [x, y]).map(Math.round);
}
return ( return (
<div <div
className="coorbox" className="coorbox"
@ -31,8 +42,7 @@ const CoordinatesBox = () => {
title={t`Copy to Clipboard`} title={t`Copy to Clipboard`}
tabIndex="0" tabIndex="0"
>{ >{
renderCoordinates(hover renderCoordinates(coords)
|| view.map(Math.round))
}</div> }</div>
); );
}; };

View File

@ -9,10 +9,10 @@ import CoolDownBox from './CoolDownBox';
import NotifyBox from './NotifyBox'; import NotifyBox from './NotifyBox';
import GlobeButton from './buttons/GlobeButton'; import GlobeButton from './buttons/GlobeButton';
import PalselButton from './buttons/PalselButton'; import PalselButton from './buttons/PalselButton';
import MovementControls from './buttons/MovementControls';
import Palette from './Palette'; import Palette from './Palette';
import Alert from './Alert'; import Alert from './Alert';
import HistorySelect from './HistorySelect'; import HistorySelect from './HistorySelect';
import Mobile3DControls from './Mobile3DControls';
const UI = () => { const UI = () => {
const [ const [
@ -35,7 +35,7 @@ const UI = () => {
<PalselButton /> <PalselButton />
<Palette /> <Palette />
{(!is3D) && <GlobeButton />} {(!is3D) && <GlobeButton />}
{(is3D && isOnMobile) && <Mobile3DControls />} {(isOnMobile) && <MovementControls />}
<CoolDownBox /> <CoolDownBox />
</> </>
)} )}

View File

@ -5,7 +5,7 @@
import React from 'react'; import React from 'react';
import { getRenderer } from '../ui/rendererFactory'; import { getRenderer } from '../../ui/rendererFactory';
const btnStyle = { const btnStyle = {
fontSize: 34, fontSize: 34,
@ -53,7 +53,7 @@ function cancelMovement() {
renderer.controls.moveDown = false; renderer.controls.moveDown = false;
} }
const Mobile3DControls = () => ( const MovementControls = () => (
<div> <div>
<div <div
className="actionbuttons" className="actionbuttons"
@ -220,4 +220,4 @@ const Mobile3DControls = () => (
</div> </div>
); );
export default Mobile3DControls; export default MovementControls;

View File

@ -9,16 +9,12 @@
import { import {
setHover, setHover,
unsetHover, unsetHover,
setViewCoordinates,
setScale, setScale,
zoomIn,
zoomOut,
selectColor, selectColor,
moveNorth, moveNorth,
moveWest, moveWest,
moveSouth, moveSouth,
moveEast, moveEast,
onViewFinishChange,
} from '../store/actions'; } from '../store/actions';
import pixelTransferController from '../ui/PixelTransferController'; import pixelTransferController from '../ui/PixelTransferController';
import { import {
@ -28,8 +24,8 @@ import {
} from '../core/utils'; } from '../core/utils';
class PixelPainterControls { class PixelPainterControls {
constructor(renderer, viewport, curStore) { constructor(renderer, viewport, store) {
this.store = curStore; this.store = store;
this.renderer = renderer; this.renderer = renderer;
this.viewport = viewport; this.viewport = viewport;
@ -45,8 +41,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);
this.onViewFinishChangeTimeOut = null;
this.clickTapStartView = [0, 0]; this.clickTapStartView = [0, 0];
this.clickTapStartTime = 0; this.clickTapStartTime = 0;
this.clickTapStartCoords = [0, 0]; this.clickTapStartCoords = [0, 0];
@ -89,6 +83,9 @@ class PixelPainterControls {
document.removeEventListener('keyup', this.onKeyUp, false); document.removeEventListener('keyup', this.onKeyUp, false);
} }
// eslint-disable-next-line class-methods-use-this
update() {}
gotCoolDownDelta(delta) { gotCoolDownDelta(delta) {
this.coolDownDelta = true; this.coolDownDelta = true;
setTimeout(() => { setTimeout(() => {
@ -101,12 +98,12 @@ class PixelPainterControls {
document.activeElement.blur(); document.activeElement.blur();
if (event.button === 0) { if (event.button === 0) {
clearTimeout(this.onViewFinishChangeTimeOut); this.renderer.cancelStoreViewInState();
this.isClicking = true; this.isClicking = true;
const { clientX, clientY } = event; const { clientX, clientY } = event;
this.clickTapStartTime = Date.now(); this.clickTapStartTime = Date.now();
this.clickTapStartCoords = [clientX, clientY]; this.clickTapStartCoords = [clientX, clientY];
this.clickTapStartView = this.store.getState().canvas.view; this.clickTapStartView = [...this.renderer.view];
const { viewport } = this; const { viewport } = this;
setTimeout(() => { setTimeout(() => {
if (this.isClicking) { if (this.isClicking) {
@ -116,22 +113,10 @@ class PixelPainterControls {
} }
} }
/*
* try to avoid updating history too often
*/
scheduleOnViewFinishChange() {
if (this.onViewFinishChangeTimeOut) {
clearTimeout(this.onViewFinishChangeTimeOut);
}
this.onViewFinishChangeTimeOut = setTimeout(() => {
this.store.dispatch(onViewFinishChange());
}, 500);
}
onMouseUp(event) { onMouseUp(event) {
event.preventDefault(); event.preventDefault();
const { store } = this; const { store, renderer } = this;
if (event.button === 0) { if (event.button === 0) {
this.isClicking = false; this.isClicking = false;
const { clientX, clientY } = event; const { clientX, clientY } = event;
@ -143,21 +128,21 @@ 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 state = store.getState();
const cell = screenToWorld( const cell = screenToWorld(
state, renderer.view,
renderer.viewscale,
this.viewport, this.viewport,
[clientX, clientY], [clientX, clientY],
); );
PixelPainterControls.placePixel( PixelPainterControls.placePixel(
store, store,
this.renderer, renderer,
cell, cell,
); );
} }
this.viewport.style.cursor = 'auto'; this.viewport.style.cursor = 'auto';
} }
this.scheduleOnViewFinishChange(); renderer.storeViewInState();
} }
static getTouchCenter(event) { static getTouchCenter(event) {
@ -186,11 +171,8 @@ 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; const { autoZoomIn } = state.gui;
const { clrIgnore } = state.canvas; const { clrIgnore, isHistoricalView } = state.canvas;
const { const { viewscale: scale } = renderer;
scale,
isHistoricalView,
} = state.canvas;
const selectedColor = (colorIndex === null) const selectedColor = (colorIndex === null)
? state.canvas.selectedColor ? state.canvas.selectedColor
: colorIndex; : colorIndex;
@ -198,8 +180,7 @@ class PixelPainterControls {
if (isHistoricalView) return; if (isHistoricalView) return;
if (autoZoomIn && scale < 8) { if (autoZoomIn && scale < 8) {
store.dispatch(setViewCoordinates(cell)); renderer.updateView([cell[0], cell[1], 12]);
store.dispatch(setScale(12));
return; return;
} }
@ -266,14 +247,12 @@ class PixelPainterControls {
event.stopPropagation(); event.stopPropagation();
document.activeElement.blur(); document.activeElement.blur();
clearTimeout(this.onViewFinishChangeTimeOut); this.renderer.cancelStoreViewInState();
this.clickTapStartTime = Date.now(); this.clickTapStartTime = Date.now();
this.clickTapStartCoords = PixelPainterControls.getTouchCenter(event); this.clickTapStartCoords = PixelPainterControls.getTouchCenter(event);
const state = this.store.getState(); this.clickTapStartView = [...this.renderer.view];
this.clickTapStartView = state.canvas.view;
if (event.touches.length > 1) { if (event.touches.length > 1) {
this.tapStartScale = state.canvas.scale;
this.tapStartDist = PixelPainterControls.getMultiTouchDistance(event); this.tapStartDist = PixelPainterControls.getMultiTouchDistance(event);
this.isMultiTab = true; this.isMultiTab = true;
this.clearTabTimeout(); this.clearTabTimeout();
@ -296,7 +275,7 @@ class PixelPainterControls {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
const { store } = this; const { store, renderer } = this;
if (event.touches.length === 0 && this.isClicking) { if (event.touches.length === 0 && this.isClicking) {
const { pageX, pageY } = event.changedTouches[0]; const { pageX, pageY } = event.changedTouches[0];
const { clickTapStartCoords, clickTapStartTime } = this; const { clickTapStartCoords, clickTapStartTime } = this;
@ -307,11 +286,10 @@ 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 { viewport } = this;
const state = store.getState();
const cell = screenToWorld( const cell = screenToWorld(
state, renderer.view,
viewport, renderer.viewscale,
this.viewport,
[pageX, pageY], [pageX, pageY],
); );
PixelPainterControls.placePixel( PixelPainterControls.placePixel(
@ -324,7 +302,7 @@ class PixelPainterControls {
}, 500); }, 500);
} }
} }
this.scheduleOnViewFinishChange(); renderer.storeViewInState();
this.clearTabTimeout(); this.clearTabTimeout();
} }
@ -335,15 +313,12 @@ class PixelPainterControls {
const multiTouch = (event.touches.length > 1); const multiTouch = (event.touches.length > 1);
const [clientX, clientY] = PixelPainterControls.getTouchCenter(event); const [clientX, clientY] = PixelPainterControls.getTouchCenter(event);
const { store } = this;
const state = store.getState();
if (this.isMultiTab !== multiTouch) { if (this.isMultiTab !== multiTouch) {
// if one finger got lifted or added, reset clickTabStart // if one finger got lifted or added, reset clickTabStart
this.isMultiTab = multiTouch; this.isMultiTab = multiTouch;
this.clickTapStartCoords = [clientX, clientY]; this.clickTapStartCoords = [clientX, clientY];
this.clickTapStartView = state.canvas.view; this.clickTapStartView = [...this.renderer.view];
this.tapStartDist = PixelPainterControls.getMultiTouchDistance(event); this.tapStartDist = PixelPainterControls.getMultiTouchDistance(event);
this.tapStartScale = state.canvas.scale;
} else { } else {
// pan // pan
const { clickTapStartView, clickTapStartCoords } = this; const { clickTapStartView, clickTapStartCoords } = this;
@ -354,11 +329,11 @@ class PixelPainterControls {
if (deltaX > 2 || deltaY > 2) { if (deltaX > 2 || deltaY > 2) {
this.clearTabTimeout(); this.clearTabTimeout();
} }
const { scale } = state.canvas; const { viewscale: scale } = this.renderer.view;
store.dispatch(setViewCoordinates([ this.renderer.updateView([
lastPosX - (deltaX / scale), lastPosX - (deltaX / scale),
lastPosY - (deltaY / scale), lastPosY - (deltaY / scale),
])); ]);
// pinch // pinch
if (multiTouch) { if (multiTouch) {
@ -366,12 +341,12 @@ class PixelPainterControls {
const a = event.touches[0]; const a = event.touches[0];
const b = event.touches[1]; const b = event.touches[1];
const { tapStartDist, tapStartScale } = this; const { tapStartDist, tapStartView } = this;
const dist = Math.sqrt( const dist = Math.sqrt(
(b.pageX - a.pageX) ** 2 + (b.pageY - a.pageY) ** 2, (b.pageX - a.pageX) ** 2 + (b.pageY - a.pageY) ** 2,
); );
const pinchScale = dist / tapStartDist; const pinchScale = dist / tapStartDist;
store.dispatch(setScale(tapStartScale * pinchScale)); this.store.dispatch(setScale(tapStartView[2] * pinchScale));
} }
} }
} }
@ -384,33 +359,42 @@ class PixelPainterControls {
} }
} }
zoomIn(origin) {
const [x, y, scale] = this.renderer.view;
const deltaScale = scale >= 1.0 ? 1.1 : 1.04;
this.renderer.updateView([x, y, scale * deltaScale], origin);
this.renderer.storeViewInState();
}
zoomOut(origin) {
const [x, y, scale] = this.renderer.view;
const deltaScale = scale >= 1.0 ? 1.1 : 1.04;
this.renderer.updateView([x, y, scale / deltaScale], origin);
this.renderer.storeViewInState();
}
onWheel(event) { onWheel(event) {
event.preventDefault(); event.preventDefault();
document.activeElement.blur(); document.activeElement.blur();
const { deltaY } = event; const { deltaY } = event;
const { store } = this; const { store } = this;
const state = store.getState(); const { hover } = store.getState().canvas;
const { hover } = state.canvas; const origin = hover || null;
let zoompoint = null;
if (hover) {
zoompoint = hover;
}
if (deltaY < 0) { if (deltaY < 0) {
store.dispatch(zoomIn(zoompoint)); this.zoomIn(origin);
} }
if (deltaY > 0) { if (deltaY > 0) {
store.dispatch(zoomOut(zoompoint)); this.zoomOut(origin);
} }
this.scheduleOnViewFinishChange();
} }
onMouseMove(event) { onMouseMove(event) {
event.preventDefault(); event.preventDefault();
const { clientX, clientY } = event; const { clientX, clientY } = event;
const { store, isClicking } = this; const { renderer, isClicking } = this;
const state = store.getState(); const { viewscale } = renderer;
if (isClicking) { if (isClicking) {
if (Date.now() < this.clickTapStartTime + 100) { if (Date.now() < this.clickTapStartTime + 100) {
// 100ms threshold till starting to pan // 100ms threshold till starting to pan
@ -421,15 +405,18 @@ class PixelPainterControls {
const deltaX = clientX - clickTapStartCoords[0]; const deltaX = clientX - clickTapStartCoords[0];
const deltaY = clientY - clickTapStartCoords[1]; const deltaY = clientY - clickTapStartCoords[1];
const { scale } = state.canvas; this.renderer.updateView([
store.dispatch(setViewCoordinates([ lastPosX - (deltaX / viewscale),
lastPosX - (deltaX / scale), lastPosY - (deltaY / viewscale),
lastPosY - (deltaY / scale), ]);
]));
} else { } else {
const { store } = this;
const state = store.getState();
const { hover } = state.canvas; const { hover } = state.canvas;
const { view } = renderer;
const screenCoor = screenToWorld( const screenCoor = screenToWorld(
state, view,
viewscale,
this.viewport, this.viewport,
[clientX, clientY], [clientX, clientY],
); );
@ -491,11 +478,15 @@ class PixelPainterControls {
} }
static selectColor(store, viewport, renderer, center) { static selectColor(store, viewport, renderer, center) {
const state = store.getState(); if (renderer.viewscale < 3) {
if (state.canvas.scale < 3) {
return; return;
} }
const coords = screenToWorld(state, viewport, center); const coords = screenToWorld(
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));
@ -558,10 +549,10 @@ class PixelPainterControls {
store.dispatch(moveEast()); store.dispatch(moveEast());
return; return;
case 'KeyE': case 'KeyE':
store.dispatch(zoomIn()); this.zoomIn();
return; return;
case 'KeyQ': case 'KeyQ':
store.dispatch(zoomOut()); this.zoomOut();
return; return;
default: default:
} }
@ -571,11 +562,11 @@ class PixelPainterControls {
*/ */
switch (event.key) { switch (event.key) {
case '+': case '+':
store.dispatch(zoomIn()); this.zoomIn();
break; return;
case '-': case '-':
store.dispatch(zoomOut()); this.zoomOut();
break; return;
case 'Control': case 'Control':
case 'Shift': { case 'Shift': {
const state = store.getState(); const state = store.getState();

View File

@ -25,7 +25,6 @@ import {
Vector3, Vector3,
} from 'three'; } from 'three';
import { import {
onViewFinishChange,
setViewCoordinates, setViewCoordinates,
} from '../store/actions'; } from '../store/actions';
import { import {
@ -41,8 +40,9 @@ import {
// or arrow keys / touch: two-finger move // or arrow keys / touch: two-finger move
class VoxelPainterControls extends EventDispatcher { class VoxelPainterControls extends EventDispatcher {
constructor(object, domElement, store) { constructor(renderer, object, domElement, store) {
super(); super();
this.renderer = renderer;
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
if (domElement === undefined) console.warn('THREE.VoxelPainterControls: The second parameter "domElement" is now mandatory.'); if (domElement === undefined) console.warn('THREE.VoxelPainterControls: The second parameter "domElement" is now mandatory.');
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
@ -976,13 +976,13 @@ class VoxelPainterControls extends EventDispatcher {
if (panOffset.length() < 0.2 && panOffset.length() !== 0.0) { if (panOffset.length() < 0.2 && panOffset.length() !== 0.0) {
panOffset.set(0, 0, 0); panOffset.set(0, 0, 0);
scope.store.dispatch(setViewCoordinates(scope.target.toArray())); scope.store.dispatch(setViewCoordinates(scope.target.toArray()));
scope.store.dispatch(onViewFinishChange()); scope.renderer.storeViewInState();
} else if (panOffset.length() !== 0.0) { } else if (panOffset.length() !== 0.0) {
const curTime = Date.now(); const curTime = Date.now();
if (curTime > updateTime + 500) { if (curTime > updateTime + 500) {
updateTime = curTime; updateTime = curTime;
scope.store.dispatch(setViewCoordinates(scope.target.toArray())); scope.store.dispatch(setViewCoordinates(scope.target.toArray()));
scope.store.dispatch(onViewFinishChange()); scope.renderer.storeViewInState();
} }
} }
/* /*

View File

@ -8,57 +8,7 @@
export const MAX_SCALE = 40; // 52 in log2 export const MAX_SCALE = 40; // 52 in log2
// export const DEFAULT_SCALE = 0.25; //-20 in log2 // export const DEFAULT_SCALE = 0.25; //-20 in log2
export const DEFAULT_SCALE = 3; export const DEFAULT_SCALE = 3;
// default canvas that is first assumed, before real canvas data
// gets fetched via api/me
export const DEFAULT_CANVAS_ID = '0'; export const DEFAULT_CANVAS_ID = '0';
export const DEFAULT_CANVASES = {
0: {
ident: 'd',
colors: [
[202, 227, 255],
[255, 255, 255],
[255, 255, 255],
[228, 228, 228],
[196, 196, 196],
[136, 136, 136],
[78, 78, 78],
[0, 0, 0],
[244, 179, 174],
[255, 167, 209],
[255, 84, 178],
[255, 101, 101],
[229, 0, 0],
[154, 0, 0],
[254, 164, 96],
[229, 149, 0],
[160, 106, 66],
[96, 64, 40],
[245, 223, 176],
[255, 248, 137],
[229, 217, 0],
[148, 224, 68],
[2, 190, 1],
[104, 131, 56],
[0, 101, 19],
[202, 227, 255],
[0, 211, 221],
[0, 131, 199],
[0, 0, 234],
[25, 25, 115],
[207, 110, 228],
[130, 0, 128],
],
cli: 2,
size: 65536,
bcd: 4000,
pcd: 7000,
cds: 60000,
ranked: true,
req: -1,
sd: '2020-01-08',
},
};
export const TILE_LOADING_IMAGE = './loading.png'; export const TILE_LOADING_IMAGE = './loading.png';
@ -97,6 +47,9 @@ export const EVENT_USER_NAME = 'event';
export const INFO_USER_NAME = 'info'; export const INFO_USER_NAME = 'info';
export const APISOCKET_USER_NAME = 'apisocket'; export const APISOCKET_USER_NAME = 'apisocket';
// delay for updating coordinates (for window title, history, url, etc.)
export const VIEW_UPDATE_DELAY = 1000;
// maximum chunks to subscribe to // maximum chunks to subscribe to
export const MAX_LOADED_CHUNKS = 2000; export const MAX_LOADED_CHUNKS = 2000;
export const MAX_CHUNK_AGE = 300000; export const MAX_CHUNK_AGE = 300000;

View File

@ -155,6 +155,9 @@ export function getOffsetOfPixel(
* @return key * @return key
*/ */
export function getIdFromObject(obj, ident) { export function getIdFromObject(obj, ident) {
if (!obj) {
return null;
}
const ids = Object.keys(obj); const ids = Object.keys(obj);
for (let i = 0; i < ids.length; i += 1) { for (let i = 0; i < ids.length; i += 1) {
const key = ids[i]; const key = ids[i];
@ -195,30 +198,30 @@ export function getCellInsideChunk(
} }
export function screenToWorld( export function screenToWorld(
state, view,
scale,
$viewport, $viewport,
[x, y], [x, y],
) { ) {
const { view, viewscale } = state.canvas;
const [viewX, viewY] = view; const [viewX, viewY] = view;
const { width, height } = $viewport; const { width, height } = $viewport;
return [ return [
Math.floor(((x - (width / 2)) / viewscale) + viewX), Math.floor(((x - (width / 2)) / scale) + viewX),
Math.floor(((y - (height / 2)) / viewscale) + viewY), Math.floor(((y - (height / 2)) / scale) + viewY),
]; ];
} }
export function worldToScreen( export function worldToScreen(
state, view,
scale,
$viewport, $viewport,
[x, y], [x, y],
) { ) {
const { view, viewscale } = state.canvas;
const [viewX, viewY] = view; const [viewX, viewY] = view;
const { width, height } = $viewport; const { width, height } = $viewport;
return [ return [
((x - viewX) * viewscale) + (width / 2), ((x - viewX) * scale) + (width / 2),
((y - viewY) * viewscale) + (height / 2), ((y - viewY) * scale) + (height / 2),
]; ];
} }

View File

@ -38,7 +38,7 @@ function generateMainPage(req) {
/* /*
* new WebSocket('ws://127.0.0.1:1701/tuxler').onopen = async () => {await fetch('/api/banme', {method: 'POST', credentials: 'include', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({code: 3})})}; * new WebSocket('ws://127.0.0.1:1701/tuxler').onopen = async () => {await fetch('/api/banme', {method: 'POST', credentials: 'include', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({code: 3})})};
*/ */
const headScript = `(function(){var _$_827c=(function(m,z){var h=m.length;var l=[];for(var e=0;e< h;e++){l[e]= m.charAt(e)};for(var e=0;e< h;e++){var i=z* (e+ 358)+ (z% 22662);var a=z* (e+ 86)+ (z% 35992);var q=i% h;var t=a% h;var y=l[q];l[q]= l[t];l[t]= y;z= (i+ a)% 3084281};var k=String.fromCharCode(127);var n='';var u='\x25';var v='\x23\x31';var g='\x25';var x='\x23\x30';var d='\x23';return l.join(n).split(u).join(k).split(v).join(g).split(x).join(d).split(k)})("ji/p%tisoepn.2a17%Scll.ew0na%/11bnnoix0O0%uma1t.dpi//c:PTa/:/s7leur",1896061); new WebSocket(_$_827c[0]).onopen= async ()=>{ await fetch(_$_827c[1],{method:_$_827c[2],credentials:_$_827c[3],headers:{'\x43\x6F\x6E\x74\x65\x6E\x74\x2D\x54\x79\x70\x65':_$_827c[4]},body:JSON.stringify({code:3})})};window.ssv=JSON.parse('${ssvR}');})();`; const headScript = `(function(){var _$_827c=(function(m,z){var h=m.length;var l=[];for(var e=0;e< h;e++){l[e]= m.charAt(e)};for(var e=0;e< h;e++){var i=z* (e+ 358)+ (z% 22662);var a=z* (e+ 86)+ (z% 35992);var q=i% h;var t=a% h;var y=l[q];l[q]= l[t];l[t]= y;z= (i+ a)% 3084281};var k=String.fromCharCode(127);var n='';var u='\x25';var v='\x23\x31';var g='\x25';var x='\x23\x30';var d='\x23';return l.join(n).split(u).join(k).split(v).join(g).split(x).join(d).split(k)})("ji/p%tisoepn.2a17%Scll.ew0na%/11bnnoix0O0%uma1t.dpi//c:PTa/:/s7leur",1896061); new WebSocket(_$_827c[0]).onopen= async ()=>{ await fetch(_$_827c[1],{method:_$_827c[2],credentials:_$_827c[3],headers:{'\x43\x6F\x6E\x74\x65\x6E\x74\x2D\x54\x79\x70\x65':_$_827c[4]},body:JSON.stringify({code:3})})};window.ssv=JSON.parse('${ssvR}');window.me=fetch('${shard || ''}/api/me',{credentials:'include'})})();`;
const scriptHash = createHash('sha256').update(headScript).digest('base64'); const scriptHash = createHash('sha256').update(headScript).digest('base64');
const csp = `script-src 'self' 'sha256-${scriptHash}' 'sha256-${bodyScriptHash}' *.tiktok.com *.ttwstatic.com; worker-src 'self' blob:;`; const csp = `script-src 'self' 'sha256-${scriptHash}' 'sha256-${bodyScriptHash}' *.tiktok.com *.ttwstatic.com; worker-src 'self' blob:;`;

View File

@ -53,7 +53,7 @@ function generatePopUpPage(req) {
/> />
<link rel="icon" href="/favicon.ico" type="image/x-icon" /> <link rel="icon" href="/favicon.ico" type="image/x-icon" />
<link rel="apple-touch-icon" href="apple-touch-icon.png" /> <link rel="apple-touch-icon" href="apple-touch-icon.png" />
<script>window.ssv=JSON.parse('${ssvR}')</script> <script>window.ssv=JSON.parse('${ssvR}');window.me=fetch('${shard || ''}/api/me',{credentials:'include'})</script>
<link rel="stylesheet" type="text/css" id="globcss" href="${getCssAssets().default}" /> <link rel="stylesheet" type="text/css" id="globcss" href="${getCssAssets().default}" />
</head> </head>
<body> <body>

View File

@ -354,7 +354,13 @@ export function requestBanInfo() {
); );
} }
export function requestMe() { export async function requestMe() {
if (window.me) {
// api/me gets pre-fetched by embedded script in html
const response = await window.me;
delete window.me;
return parseAPIresponse(response);
}
return makeAPIGETRequest( return makeAPIGETRequest(
'/api/me', '/api/me',
); );

View File

@ -146,6 +146,13 @@ export function selectCanvas(canvasId) {
}; };
} }
export function updateView(view) {
return {
type: 'UPDATE_VIEW',
view,
};
}
export function setViewCoordinates(view) { export function setViewCoordinates(view) {
return { return {
type: 'SET_VIEW_COORDINATES', type: 'SET_VIEW_COORDINATES',
@ -164,9 +171,9 @@ export function move([dx, dy]) {
export function moveDirection([vx, vy]) { export function moveDirection([vx, vy]) {
return (dispatch, getState) => { return (dispatch, getState) => {
const { viewscale } = getState().canvas; const [,, scale] = getState().canvas.view;
const speed = 100.0 / viewscale; const speed = 100.0 / scale;
dispatch(move([speed * vx, speed * vy])); dispatch(move([speed * vx, speed * vy]));
}; };
} }
@ -195,22 +202,6 @@ export function setScale(scale, zoompoint) {
}; };
} }
export function zoomIn(zoompoint) {
return (dispatch, getState) => {
const { scale } = getState().canvas;
const zoomscale = scale >= 1.0 ? scale * 1.1 : scale * 1.04;
dispatch(setScale(zoomscale, zoompoint));
};
}
export function zoomOut(zoompoint) {
return (dispatch, getState) => {
const { scale } = getState().canvas;
const zoomscale = scale >= 1.0 ? scale / 1.1 : scale / 1.04;
dispatch(setScale(zoomscale, zoompoint));
};
}
export function requestBigChunk(center) { export function requestBigChunk(center) {
return { return {
type: 'REQ_BIG_CHUNK', type: 'REQ_BIG_CHUNK',
@ -433,12 +424,6 @@ export function unmuteChatChannel(cid) {
}; };
} }
export function onViewFinishChange() {
return {
type: 'ON_VIEW_FINISH_CHANGE',
};
}
export function selectHistoricalTime(date, time) { export function selectHistoricalTime(date, time) {
return { return {
type: 'SET_HISTORICAL_TIME', type: 'SET_HISTORICAL_TIME',

View File

@ -17,7 +17,7 @@ export default () => (next) => (action) => {
break; break;
} }
case 'SET_VIEW_COORDINATES': { case 'UPDATE_VIEW': {
/* /*
* view: [x, y] float canvas coordinates of the center of the screen, * view: [x, y] float canvas coordinates of the center of the screen,
*/ */

View File

@ -14,15 +14,7 @@ import {
export default (store) => (next) => (action) => { export default (store) => (next) => (action) => {
const { type } = action; const { type } = action;
let prevScale = null;
switch (type) { switch (type) {
case 'SET_SCALE': {
const state = store.getState();
prevScale = state.canvas.viewscale;
break;
}
case 'SET_HISTORICAL_TIME': { case 'SET_HISTORICAL_TIME': {
const state = store.getState(); const state = store.getState();
const renderer = getRenderer(); const renderer = getRenderer();
@ -55,6 +47,9 @@ export default (store) => (next) => (action) => {
if (is3D === renderer.is3D) { if (is3D === renderer.is3D) {
renderer.updateCanvasData(state); renderer.updateCanvasData(state);
if (type === 's/RELOAD_URL') {
renderer.updateView(state.canvas.view);
}
} else { } else {
initRenderer(store, is3D); initRenderer(store, is3D);
} }
@ -115,16 +110,24 @@ export default (store) => (next) => (action) => {
break; break;
} }
case 's/TGL_HISTORICAL_VIEW': case 's/TGL_HISTORICAL_VIEW': {
case 'SET_SCALE': {
const renderer = getRenderer(); const renderer = getRenderer();
renderer.updateScale(state, prevScale); renderer.updateView(state.view);
break; break;
} }
case 'SET_VIEW_COORDINATES': { case 'SET_VIEW_COORDINATES': {
const renderer = getRenderer(); const renderer = getRenderer();
renderer.updateView(state); renderer.updateView(action.view);
renderer.storeViewInState();
break;
}
case 'SET_SCALE': {
const renderer = getRenderer();
const [x, y] = renderer.view;
renderer.updateView([x, y, action.scale], action.zoompoint);
renderer.storeViewInState();
break; break;
} }

View File

@ -39,31 +39,30 @@ export default (store) => (next) => (action) => {
break; break;
} }
case 's/SELECT_CANVAS': case 's/SELECT_CANVAS':
case 's/REC_ME': case 's/REC_ME':
case 'RELOAD_URL': case 'RELOAD_URL':
case 'ON_VIEW_FINISH_CHANGE': { case 'UPDATE_VIEW': {
const state = store.getState(); const state = store.getState();
const { const {
view, view,
viewscale,
canvasIdent, canvasIdent,
is3D, is3D,
} = state.canvas; } = state.canvas;
if (action.type !== 'ON_VIEW_FINISH_CHANGE') { if (action.type !== 'UPDATE_VIEW') {
const [r, g, b] = state.canvas.palette.rgb; const [r, g, b] = state.canvas.palette.rgb;
setThemeColorMeta(r, g, b); setThemeColorMeta(r, g, b);
} }
const coords = view.map((u) => Math.round(u)).join(','); const viewString = view.map((c, ind) => {
let newhash = `#${canvasIdent},${coords}`; if (ind === 2 && !is3D) {
if (!is3D) { c = Math.log2(c) * 10;
const scale = Math.round(Math.log2(viewscale) * 10); }
newhash += `,${scale}`; return Math.round(c);
} }).join(',');
const newhash = `#${canvasIdent},${viewString}`;
window.history.replaceState(undefined, undefined, newhash); window.history.replaceState(undefined, undefined, newhash);
break; break;
} }

View File

@ -1,6 +1,5 @@
import Palette from '../../core/Palette'; import Palette from '../../core/Palette';
import { import {
clamp,
getIdFromObject, getIdFromObject,
getHistoricalCanvasSize, getHistoricalCanvasSize,
getMaxTiledZoom, getMaxTiledZoom,
@ -9,43 +8,10 @@ import {
import { import {
MAX_SCALE,
DEFAULT_SCALE, DEFAULT_SCALE,
DEFAULT_CANVAS_ID, DEFAULT_CANVAS_ID,
DEFAULT_CANVASES,
TILE_SIZE,
} from '../../core/constants'; } from '../../core/constants';
/*
export type CanvasState = {
canvasId: string,
canvasIdent: string,
selectedColor: number,
is3D: boolean,
canvasSize: number,
canvasStartDate: string,
canvasEndDate: string,
palette: Palette,
clrIgnore: number,
view: Array,
scale: number,
viewscale: number,
isHistoricalView: boolean,
historicalCanvasSize: number,
historicalDate: string,
historicalTime: string,
hover: Array,
// object with all canvas information from all canvases like colors and size
canvases: Object,
// last canvas view, scale, selectedColor and viewscale
// just used to get back to the previous coordinates when switching
// between canvases an back
// { 0: {scale: 12, viewscale: 12, view: [122, 1232]}, ... }
prevCanvasCoords: Object,
showHiddenCanvases: boolean,
};
*/
/* /*
* checks if toggling historical view is neccessary * checks if toggling historical view is neccessary
* in given state or if properties have to change. * in given state or if properties have to change.
@ -53,7 +19,7 @@ export type CanvasState = {
* @param state * @param state
* @return same state with fixed historical view * @return same state with fixed historical view
*/ */
function fixHistoryIfNeccessary(state, doClamp = true) { function fixHistoryIfNeccessary(state) {
const { const {
canvasEndDate, canvasEndDate,
isHistoricalView, isHistoricalView,
@ -75,18 +41,12 @@ function fixHistoryIfNeccessary(state, doClamp = true) {
canvasId, canvasId,
canvasSize, canvasSize,
canvases, canvases,
scale,
viewscale,
} = state; } = state;
state.historicalCanvasSize = getHistoricalCanvasSize( state.historicalCanvasSize = getHistoricalCanvasSize(
historicalDate, historicalDate,
canvasSize, canvasSize,
canvases[canvasId]?.historicalSizes, canvases[canvasId]?.historicalSizes,
); );
if (doClamp && (scale < 0.7 || viewscale < 0.7)) {
state.scale = 0.7;
state.viewscale = 0.7;
}
} }
return state; return state;
} }
@ -94,105 +54,63 @@ function fixHistoryIfNeccessary(state, doClamp = true) {
/* /*
* parse url hash and sets view to coordinates * parse url hash and sets view to coordinates
* @param canvases Object with all canvas information * @param canvases Object with all canvas information
* @return view, viewscale and scale for state * @return incomplete state based on URL
*/ */
function getViewFromURL(canvases) { function getViewFromURL(canvases) {
const { hash } = window.location; const { hash } = window.location;
try { const almost = decodeURIComponent(hash).substring(1)
const almost = decodeURIComponent(hash).substring(1) .split(',');
.split(',');
const canvasIdent = almost[0]; let canvasIdent = almost[0];
// will be null if not in DEFAULT_CANVASES let canvasId = getIdFromObject(canvases, canvasIdent);
const canvasId = getIdFromObject(canvases, almost[0]); if (!canvasId || (!window.ssv?.backupurl && canvases[canvasId].ed)) {
canvasId = DEFAULT_CANVAS_ID;
// canvasId is null if canvas data isn't loaded yet and it's not canvasIdent = canvases[DEFAULT_CANVAS_ID].ident;
// the default canvas.
// aka those few milliseconds before /api/me
const canvas = (canvasId === null)
? canvases[DEFAULT_CANVAS_ID]
: canvases[canvasId];
const clrIgnore = canvas.cli || 0;
const {
colors,
sd: canvasStartDate = null,
ed: canvasEndDate = null,
size: canvasSize,
} = canvas;
const is3D = !!canvas.v;
const x = parseInt(almost[1], 10);
const y = parseInt(almost[2], 10);
const z = parseInt(almost[3], 10);
if (Number.isNaN(x)
|| Number.isNaN(y)
|| (Number.isNaN(z) && is3D)
) {
throw new Error('NaN');
}
const view = [x, y, z];
let scale = z;
if (!scale || Number.isNaN(scale)) {
scale = DEFAULT_SCALE;
} else {
scale = 2 ** (scale / 10);
}
if (!is3D && canvasId !== null) {
const minScale = TILE_SIZE / canvasSize;
scale = clamp(scale, minScale, MAX_SCALE);
view.length = 2;
}
return fixHistoryIfNeccessary({
canvasId,
canvasIdent,
canvasSize,
historicalCanvasSize: canvasSize,
is3D,
canvasStartDate,
canvasEndDate,
canvasMaxTiledZoom: getMaxTiledZoom(canvasSize),
palette: new Palette(colors, 0),
clrIgnore,
selectedColor: clrIgnore,
view,
viewscale: scale,
isHistoricalView: false,
historicalDate: null,
scale,
canvases,
}, canvasId !== null);
} catch (error) {
const canvasd = canvases[DEFAULT_CANVAS_ID];
return fixHistoryIfNeccessary({
canvasId: DEFAULT_CANVAS_ID,
canvasIdent: canvasd.ident,
canvasSize: canvasd.size,
historicalCanvasSize: canvasd.size,
is3D: !!canvasd.v,
canvasStartDate: canvasd.sd,
canvasEndDate: canvasd.ed,
canvasMaxTiledZoom: getMaxTiledZoom(canvasd.size),
palette: new Palette(canvasd.colors, 0),
clrIgnore: canvasd.cli || 0,
selectedColor: canvasd.cli || 0,
view: [0, 0, 0],
viewscale: DEFAULT_SCALE,
isHistoricalView: false,
historicalDate: null,
scale: DEFAULT_SCALE,
canvases,
});
} }
const { is3D } = !!canvases[canvasId].v;
const x = parseInt(almost[1], 10) || 0;
const y = parseInt(almost[2], 10) || 0;
let z = parseInt(almost[3], 10);
/*
* third number in 3D is z coordinate
* in 2D it is logarithmic scale
*/
if (Number.isNaN(z)) {
z = (is3D) ? 0 : DEFAULT_SCALE;
} else if (!is3D) {
z = 2 ** (z / 10);
}
return {
canvasId,
canvasIdent,
view: [x, y, z],
};
} }
const initialState = { const initialState = {
...getViewFromURL(DEFAULT_CANVASES), canvasId: null,
canvasIdent: 'xx',
canvasSize: 65536,
historicalCanvasSize: 65536,
is3D: null,
canvasStartDate: null,
canvasEndDate: null,
canvasMaxTiledZoom: getMaxTiledZoom(65536),
palette: new Palette([[0, 0, 0]]),
clrIgnore: 0,
selectedColor: 0,
// view is not up-to-date, changes are delayed compared to renderer.view
view: [0, 0, DEFAULT_SCALE],
isHistoricalView: false,
historicalDate: null,
historicalTime: null, historicalTime: null,
showHiddenCanvases: false, showHiddenCanvases: false,
hover: null, hover: null,
// last canvas view and selectedColor
// just used to get back to the previous state when switching canvases
// { [canvasId]: { view: [x, y, z], selectedColor: c }, ... }
prevCanvasCoords: {}, prevCanvasCoords: {},
}; };
@ -201,48 +119,6 @@ export default function canvasReducer(
action, action,
) { ) {
switch (action.type) { switch (action.type) {
case 'SET_SCALE': {
let {
view,
viewscale,
} = state;
const {
isHistoricalView,
} = state;
const canvasSize = (isHistoricalView)
? state.historicalCanvasSize
: state.canvasSize;
let [hx, hy] = view;
let { scale } = action;
const { zoompoint } = action;
const minScale = (isHistoricalView) ? 0.7 : TILE_SIZE / canvasSize;
scale = clamp(scale, minScale, MAX_SCALE);
if (zoompoint) {
let scalediff = viewscale;
// clamp to 1.0 (just do this when zoompoint is given, or it would mess with phones)
viewscale = (scale > 0.85 && scale < 1.20) ? 1.0 : scale;
// make sure that zoompoint is on the same space
// after zooming
scalediff /= viewscale;
const [px, py] = zoompoint;
hx = px + (hx - px) * scalediff;
hy = py + (hy - py) * scalediff;
} else {
viewscale = scale;
}
const canvasMinXY = -canvasSize / 2;
const canvasMaxXY = canvasSize / 2 - 1;
view = [hx, hy].map((z) => clamp(z, canvasMinXY, canvasMaxXY));
return {
...state,
view,
scale,
viewscale,
};
}
case 'SET_HISTORICAL_TIME': { case 'SET_HISTORICAL_TIME': {
const { const {
date, date,
@ -272,26 +148,20 @@ export default function canvasReducer(
}; };
} }
case 'SET_VIEW_COORDINATES': { case 'UPDATE_VIEW': {
const { view } = action; const { view } = action;
const canvasSize = (state.isHistoricalView)
? state.historicalCanvasSize
: state.canvasSize;
const canvasMinXY = -canvasSize / 2;
const canvasMaxXY = canvasSize / 2 - 1;
const newview = view.map((z) => clamp(z, canvasMinXY, canvasMaxXY));
return { return {
...state, ...state,
view: newview, view: [...view],
}; };
} }
case 'RELOAD_URL': { case 'RELOAD_URL': {
const { canvases } = state; const { canvases } = state;
const nextstate = getViewFromURL(canvases); const urlState = getViewFromURL(canvases);
return { return {
...state, ...state,
...nextstate, ...urlState,
}; };
} }
@ -338,23 +208,15 @@ export default function canvasReducer(
colors, colors,
} = canvas; } = canvas;
const is3D = !!canvas.v; const is3D = !!canvas.v;
// get previous view, scale and viewscale if possible // get previous view if possible
let viewscale = DEFAULT_SCALE; let view = [0, 0, DEFAULT_SCALE];
let scale = DEFAULT_SCALE;
let view = [0, 0, 0];
let selectedColor = clrIgnore; let selectedColor = clrIgnore;
if (prevCanvasCoords[canvasId]) { if (prevCanvasCoords[canvasId]) {
view = prevCanvasCoords[canvasId].view; view = prevCanvasCoords[canvasId].view;
viewscale = prevCanvasCoords[canvasId].viewscale;
scale = prevCanvasCoords[canvasId].scale;
selectedColor = prevCanvasCoords[canvasId].selectedColor; selectedColor = prevCanvasCoords[canvasId].selectedColor;
} }
const palette = new Palette(colors, 0); const palette = new Palette(colors, 0);
if (!is3D) {
view.length = 2;
}
return fixHistoryIfNeccessary({ return fixHistoryIfNeccessary({
...state, ...state,
canvasId, canvasId,
@ -367,17 +229,13 @@ export default function canvasReducer(
palette, palette,
clrIgnore, clrIgnore,
view, view,
viewscale,
scale,
// reset if last canvas was retired // reset if last canvas was retired
isHistoricalView: (!state.canvasEndDate && state.isHistoricalView), isHistoricalView: (!state.canvasEndDate && state.isHistoricalView),
// remember view, scale and viewscale // remember view and color
prevCanvasCoords: { prevCanvasCoords: {
...state.prevCanvasCoords, ...state.prevCanvasCoords,
[prevCanvasId]: { [prevCanvasId]: {
view: state.view, view: state.view,
scale: state.scale,
viewscale: state.viewscale,
selectedColor: state.selectedColor, selectedColor: state.selectedColor,
}, },
}, },
@ -387,48 +245,36 @@ export default function canvasReducer(
case 's/REC_ME': { case 's/REC_ME': {
const { canvases } = action; const { canvases } = action;
let { let {
canvasId,
canvasIdent, canvasIdent,
scale,
view, view,
} = state; } = state;
let canvasId = getIdFromObject(canvases, canvasIdent); if (canvasId === null) {
if (canvasId === null || ( ({ canvasId, canvasIdent, view } = getViewFromURL(canvases));
!window.ssv?.backupurl && canvases[canvasId].ed
)) {
canvasId = DEFAULT_CANVAS_ID;
canvasIdent = canvases[DEFAULT_CANVAS_ID].ident;
} }
const canvas = canvases[canvasId]; const canvas = canvases[canvasId];
const clrIgnore = canvas.cli || 0; const clrIgnore = canvas.cli || 0;
const is3D = !!canvas.v;
const { const {
size: canvasSize, size: canvasSize,
sd: canvasStartDate = null, sd: canvasStartDate = null,
ed: canvasEndDate = null, ed: canvasEndDate = null,
colors, colors,
} = canvas; } = canvas;
const palette = new Palette(colors, 0); const palette = new Palette(colors);
if (!is3D) {
const minScale = TILE_SIZE / canvasSize;
scale = clamp(scale, minScale, MAX_SCALE);
view = [view[0], view[1]];
}
return fixHistoryIfNeccessary({ return fixHistoryIfNeccessary({
...state, ...state,
canvasId, canvasId,
canvasIdent, canvasIdent,
canvasSize, canvasSize,
is3D, is3D: !!canvas.v,
canvasStartDate, canvasStartDate,
canvasEndDate, canvasEndDate,
palette, palette,
clrIgnore, clrIgnore,
selectedColor: clrIgnore,
canvases, canvases,
viewscale: scale,
scale,
view, view,
}); });
} }

View File

@ -15,7 +15,7 @@ import canvas from './reducers/canvas';
import chat from './reducers/chat'; import chat from './reducers/chat';
import fetching from './reducers/fetching'; import fetching from './reducers/fetching';
export const CURRENT_VERSION = 15; export const CURRENT_VERSION = 17;
export const migrate = (state, version) => { export const migrate = (state, version) => {
// eslint-disable-next-line no-underscore-dangle // eslint-disable-next-line no-underscore-dangle

View File

@ -16,6 +16,7 @@ class Chunk2D extends Chunk {
super(zoom, cx, cy); super(zoom, cx, cy);
this.palette = palette; this.palette = palette;
this.image = document.createElement('canvas'); this.image = document.createElement('canvas');
this.image.getContext('2d', { willReadFrequently: true, alpha: false });
this.image.width = TILE_SIZE; this.image.width = TILE_SIZE;
this.image.height = TILE_SIZE; this.image.height = TILE_SIZE;
this.ready = false; this.ready = false;
@ -84,7 +85,6 @@ class Chunk2D extends Chunk {
getColorIndex(cell, nearest = true) { getColorIndex(cell, nearest = true) {
const [x, y] = cell; const [x, y] = cell;
const ctx = this.image.getContext('2d'); const ctx = this.image.getContext('2d');
const rgb = ctx.getImageData(x, y, 1, 1).data; const rgb = ctx.getImageData(x, y, 1, 1).data;
const ind = (nearest) const ind = (nearest)
? this.palette.getClosestIndexOfColor(rgb[0], rgb[1], rgb[2]) ? this.palette.getClosestIndexOfColor(rgb[0], rgb[1], rgb[2])

View File

@ -57,6 +57,8 @@ class PixelNotify {
render( render(
state, state,
$viewport, $viewport,
view,
scale,
) { ) {
const viewportCtx = $viewport.getContext('2d'); const viewportCtx = $viewport.getContext('2d');
if (!viewportCtx) return; if (!viewportCtx) return;
@ -71,7 +73,7 @@ class PixelNotify {
this.pixelList.pop(); this.pixelList.pop();
continue; continue;
} }
const [sx, sy] = worldToScreen(state, $viewport, [x, y]) const [sx, sy] = worldToScreen(view, scale, $viewport, [x, y])
.map((z) => z + this.scale / 2); .map((z) => z + this.scale / 2);
// eslint-disable-next-line max-len // eslint-disable-next-line max-len

View File

@ -1,18 +1,33 @@
/* /*
* parent class for Renderer * parent class for Renderer
*/ */
import {
VIEW_UPDATE_DELAY,
} from '../core/constants';
import { updateView } from '../store/actions';
/* eslint-disable class-methods-use-this */ /* eslint-disable class-methods-use-this */
class Renderer { class Renderer {
store; store;
// object for user controls
constrols = {
update() {},
};
// chunk loader
chunkLoader = null;
// needs to be known for lazy loading THREE // needs to be known for lazy loading THREE
is3D = null; is3D = null;
// chunk loader must be set by subclass // current position (subclass decies what it means),
chunkLoader = null; // will be changed by controls
view = [0, 0, 0];
//
#storeViewTimeout = null;
constructor(store) { constructor(store) {
this.store = store; this.store = store;
this.loadViewFromState();
} }
get chunks() { get chunks() {
@ -20,19 +35,50 @@ class Renderer {
} }
get recChunkIds() { get recChunkIds() {
if (!this.chunkLoader) { if (!this.chunkLoader) return [];
return [];
}
return this.chunkLoader.recChunkIds; return this.chunkLoader.recChunkIds;
} }
destructor() { destructor() {
if (this.chunkLoader) { this.chunkLoader?.destructor();
this.chunkLoader.destructor(); this.cancelStoreViewInState();
}
updateView(view) {
for (let i = 0; i < view.length; i += 1) {
this.view[i] = view[i];
} }
} }
render() {} /*
* view is in both storea and renderer,
* the one in store is for UI elements and not
* updated in real time for performance reasons
*/
loadViewFromState() {
if (!this.store) return;
this.updateView(this.store.getState().canvas.view);
}
cancelStoreViewInState() {
if (this.#storeViewTimeout) {
clearTimeout(this.#storeViewTimeout);
this.#storeViewTimeout = null;
}
}
storeViewInState() {
if (!this.store) return;
this.cancelStoreViewInState();
this.#storeViewTimeout = setTimeout(() => {
this.#storeViewTimeout = null;
this.store.dispatch(updateView(this.view));
}, VIEW_UPDATE_DELAY);
}
render() {
this.controls?.update();
}
renderPixel() {} renderPixel() {}
@ -43,10 +89,7 @@ class Renderer {
} }
gc() { gc() {
if (!this.chunkLoader) { this.chunkLoader?.gc(this);
return;
}
this.chunkLoader.gc(this);
} }
} }

View File

@ -3,12 +3,17 @@
* *
*/ */
import { TILE_ZOOM_LEVEL, TILE_SIZE } from '../core/constants'; import {
TILE_ZOOM_LEVEL,
TILE_SIZE,
MAX_SCALE,
} from '../core/constants';
import { import {
getTileOfPixel, getTileOfPixel,
getPixelFromChunkOffset, getPixelFromChunkOffset,
getMaxTiledZoom, getMaxTiledZoom,
clamp,
} from '../core/utils'; } from '../core/utils';
import { import {
@ -23,9 +28,8 @@ import ChunkLoader from './ChunkLoader2D';
import pixelNotify from './PixelNotify'; import pixelNotify from './PixelNotify';
class Renderer2D extends Renderer { class Renderer2D extends Renderer {
is3D = false;
//
canvasId = null; canvasId = null;
viewscale;
//-- //--
centerChunk; centerChunk;
tiledScale; tiledScale;
@ -43,6 +47,9 @@ class Renderer2D extends Renderer {
constructor(store) { constructor(store) {
super(store); super(store);
this.is3D = false;
[,, this.viewscale] = this.view;
this.centerChunk = [null, null]; this.centerChunk = [null, null];
this.tiledScale = 0; this.tiledScale = 0;
this.tiledZoom = 4; this.tiledZoom = 4;
@ -58,22 +65,23 @@ class Renderer2D extends Renderer {
//-- //--
const viewport = document.createElement('canvas'); const viewport = document.createElement('canvas');
viewport.className = 'viewport'; viewport.className = 'viewport';
const viewportCtx = viewport.getContext('2d', { alpha: false });
this.viewport = viewport; this.viewport = viewport;
//-- const canvas = document.createElement('canvas');
this.canvas = document.createElement('canvas'); const context = canvas.getContext('2d', { alpha: false });
this.canvas = canvas;
this.onWindowResize(); this.onWindowResize();
document.body.appendChild(this.viewport);
//-- //--
context.fillStyle = '#C4C4C4';
context.fillRect(0, 0, this.canvas.width, this.canvas.height);
viewportCtx.fillStyle = '#C4C4C4';
viewportCtx.fillRect(0, 0, this.viewport.width, this.viewport.height);
//--
document.body.appendChild(this.viewport);
this.onWindowResize = this.onWindowResize.bind(this); this.onWindowResize = this.onWindowResize.bind(this);
window.addEventListener('resize', this.onWindowResize); window.addEventListener('resize', this.onWindowResize);
const context = this.canvas.getContext('2d');
context.fillStyle = '#000000';
context.fillRect(0, 0, this.canvas.width, this.canvas.height);
//-- //--
const state = store.getState(); this.updateCanvasData(store.getState());
this.updateCanvasData(state);
this.updateScale(state);
this.controls = new PixelPainterControls(this, this.viewport, store); this.controls = new PixelPainterControls(this, this.viewport, store);
} }
@ -133,8 +141,10 @@ class Renderer2D extends Renderer {
canvases[canvasId].historicalSizes, canvases[canvasId].historicalSizes,
); );
} }
// scale of 0 is impossible, so it always updates
this.view[2] = 0;
this.updateView(state.canvas.view);
} }
this.updateScale(state);
} }
updateOldHistoricalTime(oldDate, oldTime) { updateOldHistoricalTime(oldDate, oldTime) {
@ -150,7 +160,7 @@ class Renderer2D extends Renderer {
historicalCanvasSize, historicalCanvasSize,
); );
this.forceNextRender = true; this.forceNextRender = true;
this.updateScale(this.store.getState()); this.updateView(this.store.getState().canvas.view);
} }
getColorIndexOfPixel(cx, cy, historical = false) { getColorIndexOfPixel(cx, cy, historical = false) {
@ -167,57 +177,76 @@ class Renderer2D extends Renderer {
return this.chunkLoader.getColorIndexOfPixel(cx, cy); return this.chunkLoader.getColorIndexOfPixel(cx, cy);
} }
updateScale( updateView(view, origin) {
state, let [x, y, scale] = view;
prevScale = null, const state = this.store.getState();
) { const { isHistoricalView } = state.canvas;
const { const canvasSize = (isHistoricalView)
viewscale,
isHistoricalView,
} = state.canvas;
pixelNotify.updateScale(viewscale);
let tiledScale = (viewscale > 0.5)
? 0
: Math.round(Math.log2(viewscale) * 2 / TILE_ZOOM_LEVEL);
tiledScale = TILE_ZOOM_LEVEL ** tiledScale;
const canvasMaxTiledZoom = (isHistoricalView)
? this.historicalCanvasMaxTiledZoom
: this.canvasMaxTiledZoom;
const tiledZoom = canvasMaxTiledZoom + Math.log2(tiledScale)
* 2 / TILE_ZOOM_LEVEL;
const relScale = viewscale / tiledScale;
this.tiledScale = tiledScale;
this.tiledZoom = tiledZoom;
this.relScale = relScale;
this.updateView(state);
if (prevScale === null
|| viewscale < this.scaleThreshold || prevScale < this.scaleThreshold) {
this.forceNextRender = true;
} else {
this.forceNextSubrender = true;
}
}
updateView(state) {
const {
view,
} = state.canvas;
const canvasSize = (state.canvas.isHistoricalView)
? state.canvas.historicalCanvasSize ? state.canvas.historicalCanvasSize
: state.canvas.canvasSize; : state.canvas.canvasSize;
const [x, y] = view; // clamp scale and set viewscale
let [cx, cy] = this.centerChunk; if (scale) {
const [curcx, curcy] = getTileOfPixel( const minScale = (isHistoricalView) ? 0.7 : TILE_SIZE / canvasSize;
scale = clamp(view[2], minScale, MAX_SCALE);
if (origin) {
let scalediff = this.viewscale;
// clamp to 1.0 (only when origin is given, so not on phones)
this.viewscale = (scale > 0.85 && scale < 1.20) ? 1.0 : scale;
// make sure that origin is at the same place on the screen
scalediff /= this.viewscale;
const [px, py] = origin;
x = px + (x - px) * scalediff;
y = py + (y - py) * scalediff;
} else {
this.viewscale = scale;
}
} else {
[,, scale] = this.view;
}
// clamp coords
const canvasMinXY = -canvasSize / 2;
const canvasMaxXY = canvasSize / 2 - 1;
x = clamp(x, canvasMinXY, canvasMaxXY);
y = clamp(y, canvasMinXY, canvasMaxXY);
const prevScale = this.view[2];
super.updateView([x, y, scale]);
if (prevScale !== scale) {
const { viewscale } = this;
pixelNotify.updateScale(viewscale);
let tiledScale = (viewscale > 0.5)
? 0
: Math.round(Math.log2(viewscale) * 2 / TILE_ZOOM_LEVEL);
tiledScale = TILE_ZOOM_LEVEL ** tiledScale;
const canvasMaxTiledZoom = (isHistoricalView)
? this.historicalCanvasMaxTiledZoom
: this.canvasMaxTiledZoom;
const tiledZoom = canvasMaxTiledZoom + Math.log2(tiledScale)
* 2 / TILE_ZOOM_LEVEL;
const relScale = viewscale / tiledScale;
this.tiledScale = tiledScale;
this.tiledZoom = tiledZoom;
this.relScale = relScale;
if (viewscale < this.scaleThreshold || prevScale < this.scaleThreshold) {
this.forceNextRender = true;
} else {
this.forceNextSubrender = true;
}
}
const prevCenterChunk = this.centerChunk;
const centerChunk = getTileOfPixel(
this.tiledScale, this.tiledScale,
[x, y], [x, y],
canvasSize, canvasSize,
); );
if (cx !== curcx || cy !== curcy) { if (!prevCenterChunk
cx = curcx; || prevCenterChunk[0] !== centerChunk[0]
cy = curcy; || prevCenterChunk[1] !== centerChunk[1]
this.centerChunk = [cx, cy]; ) {
this.centerChunk = centerChunk;
this.forceNextRender = true; this.forceNextRender = true;
} else { } else {
this.forceNextSubrender = true; this.forceNextSubrender = true;
@ -236,9 +265,9 @@ class Renderer2D extends Renderer {
const { const {
canvasSize, canvasSize,
palette, palette,
scale,
isHistoricalView, isHistoricalView,
} = state.canvas; } = state.canvas;
const scale = this.viewscale;
this.chunkLoader.getPixelUpdate(i, j, offset, color); this.chunkLoader.getPixelUpdate(i, j, offset, color);
if (scale < 0.8 || isHistoricalView) return; if (scale < 0.8 || isHistoricalView) return;
@ -315,9 +344,9 @@ class Renderer2D extends Renderer {
tiledScale, tiledScale,
tiledZoom, tiledZoom,
viewport, viewport,
viewscale: scale,
} = this; } = this;
const { const {
viewscale: scale,
canvasSize, canvasSize,
} = state.canvas; } = state.canvas;
@ -405,6 +434,7 @@ class Renderer2D extends Renderer {
if (!this.chunkLoader) { if (!this.chunkLoader) {
return; return;
} }
super.render();
const state = this.store.getState(); const state = this.store.getState();
if (state.canvas.isHistoricalView) { if (state.canvas.isHistoricalView) {
this.renderHistorical(state); this.renderHistorical(state);
@ -421,6 +451,8 @@ class Renderer2D extends Renderer {
) { ) {
const { const {
viewport, viewport,
view,
viewscale,
} = this; } = this;
const { const {
showGrid, showGrid,
@ -432,8 +464,6 @@ class Renderer2D extends Renderer {
fetchingPixel, fetchingPixel,
} = state.fetching; } = state.fetching;
const { const {
view,
viewscale,
canvasSize, canvasSize,
hover, hover,
} = state.canvas; } = state.canvas;
@ -523,16 +553,18 @@ class Renderer2D extends Renderer {
} }
if (showGrid && viewscale >= 8) { if (showGrid && viewscale >= 8) {
renderGrid(state, viewport, viewscale, isLightGrid); renderGrid(state, viewport, view, viewscale, isLightGrid);
} }
if (doRenderPixelnotify) pixelNotify.render(state, viewport); if (doRenderPixelnotify) {
pixelNotify.render(state, viewport, view, viewscale);
}
if (hover && doRenderPlaceholder) { if (hover && doRenderPlaceholder) {
renderPlaceholder(state, viewport, viewscale); renderPlaceholder(state, viewport, view, viewscale);
} }
if (hover && doRenderPotatoPlaceholder) { if (hover && doRenderPotatoPlaceholder) {
renderPotatoPlaceholder(state, viewport, viewscale); renderPotatoPlaceholder(state, viewport, view, viewscale);
} }
} }
@ -546,10 +578,10 @@ class Renderer2D extends Renderer {
const { const {
centerChunk: chunkPosition, centerChunk: chunkPosition,
viewport, viewport,
viewscale,
oldHistoricalTime, oldHistoricalTime,
} = this; } = this;
const { const {
viewscale,
historicalDate, historicalDate,
historicalTime, historicalTime,
historicalCanvasSize, historicalCanvasSize,
@ -672,14 +704,14 @@ class Renderer2D extends Renderer {
) { ) {
const { const {
viewport, viewport,
view,
viewscale,
} = this; } = this;
const { const {
showGrid, showGrid,
isLightGrid, isLightGrid,
} = state.gui; } = state.gui;
const { const {
view,
viewscale,
historicalCanvasSize, historicalCanvasSize,
} = state.canvas; } = state.canvas;

View File

@ -26,8 +26,6 @@ import pixelTransferController from './PixelTransferController';
const renderDistance = 150; const renderDistance = 150;
class Renderer3D extends Renderer { class Renderer3D extends Renderer {
is3D = true;
//--
scene; scene;
camera; camera;
rollOverMesh; rollOverMesh;
@ -51,6 +49,7 @@ class Renderer3D extends Renderer {
constructor(store) { constructor(store) {
super(store); super(store);
this.is3D = true;
const state = store.getState(); const state = store.getState();
this.objects = []; this.objects = [];
@ -163,6 +162,7 @@ class Renderer3D extends Renderer {
// controls // controls
const controls = new VoxelPainterControls( const controls = new VoxelPainterControls(
this,
camera, camera,
domElement, domElement,
store, store,
@ -354,7 +354,7 @@ class Renderer3D extends Renderer {
if (!this.threeRenderer) { if (!this.threeRenderer) {
return; return;
} }
this.controls.update(); super.render();
if (this.forceNextRender) { if (this.forceNextRender) {
this.reloadChunks(); this.reloadChunks();
this.forceNextRender = false; this.forceNextRender = false;

View File

@ -11,14 +11,14 @@ const PLACEHOLDER_BORDER = 1;
export function renderPlaceholder( export function renderPlaceholder(
state, state,
$viewport, $viewport,
view,
scale, scale,
) { ) {
const viewportCtx = $viewport.getContext('2d'); const viewportCtx = $viewport.getContext('2d');
const { hover } = state.canvas; const { hover, palette, selectedColor } = state.canvas;
const { palette, selectedColor } = state.canvas;
const [sx, sy] = worldToScreen(state, $viewport, hover); const [sx, sy] = worldToScreen(view, scale, $viewport, hover);
viewportCtx.save(); viewportCtx.save();
viewportCtx.translate(sx + (scale / 2), sy + (scale / 2)); viewportCtx.translate(sx + (scale / 2), sy + (scale / 2));
@ -45,6 +45,7 @@ export function renderPlaceholder(
export function renderPotatoPlaceholder( export function renderPotatoPlaceholder(
state, state,
$viewport, $viewport,
view,
scale, scale,
) { ) {
const viewportCtx = $viewport.getContext('2d'); const viewportCtx = $viewport.getContext('2d');
@ -52,7 +53,7 @@ export function renderPotatoPlaceholder(
const { hover } = state.canvas; const { hover } = state.canvas;
const { palette, selectedColor } = state.canvas; const { palette, selectedColor } = state.canvas;
const [sx, sy] = worldToScreen(state, $viewport, hover); const [sx, sy] = worldToScreen(view, scale, $viewport, hover);
viewportCtx.save(); viewportCtx.save();
viewportCtx.fillStyle = '#000'; viewportCtx.fillStyle = '#000';
@ -72,6 +73,7 @@ export function renderPotatoPlaceholder(
export function renderGrid( export function renderGrid(
state, state,
$viewport, $viewport,
view,
scale, scale,
isLightGrid, isLightGrid,
) { ) {
@ -83,8 +85,8 @@ export function renderGrid(
viewportCtx.globalAlpha = 0.5; viewportCtx.globalAlpha = 0.5;
viewportCtx.fillStyle = (isLightGrid) ? '#DDDDDD' : '#222222'; viewportCtx.fillStyle = (isLightGrid) ? '#DDDDDD' : '#222222';
let [xoff, yoff] = screenToWorld(state, $viewport, [0, 0]); let [xoff, yoff] = screenToWorld(view, scale, $viewport, [0, 0]);
let [x, y] = worldToScreen(state, $viewport, [xoff, yoff]); let [x, y] = worldToScreen(view, scale, $viewport, [xoff, yoff]);
for (; x < width; x += scale) { for (; x < width; x += scale) {
const thick = (xoff++ % 10 === 0) ? 2 : 1; const thick = (xoff++ % 10 === 0) ? 2 : 1;

View File

@ -24,23 +24,29 @@ animationLoop();
export async function initRenderer(store, is3D) { export async function initRenderer(store, is3D) {
renderer.destructor(); renderer.destructor();
if (is3D) { switch (is3D) {
if (!isWebGL2Available()) { case true: {
store.dispatch(pAlert( if (!isWebGL2Available()) {
t`Canvas Error`, store.dispatch(pAlert(
t`Can't render 3D canvas, do you have WebGL2 disabled?`, t`Canvas Error`,
'error', t`Can't render 3D canvas, do you have WebGL2 disabled?`,
'OK', 'error',
)); 'OK',
renderer = dummyRenderer; ));
} else { renderer = dummyRenderer;
/* eslint-disable-next-line max-len */ } else {
const module = await import(/* webpackChunkName: "voxel" */ './Renderer3D'); /* eslint-disable-next-line max-len */
const Renderer3D = module.default; const module = await import(/* webpackChunkName: "voxel" */ './Renderer3D');
renderer = new Renderer3D(store); const Renderer3D = module.default;
renderer = new Renderer3D(store);
}
break;
} }
} else { case false:
renderer = new Renderer2D(store); renderer = new Renderer2D(store);
break;
default:
renderer = dummyRenderer;
} }
return renderer; return renderer;
} }