Add 3d voxel canvas
Add Canvas Selection Menu Improve Rendering Move ChunkRGB into seperate file
This commit is contained in:
parent
88990bf454
commit
caf08ee32d
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
|
@ -1,21 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>three.js webgl - interactive - voxel painter</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
|
||||
<link type="text/css" rel="stylesheet" href="main.css">
|
||||
<script src="assets/voxel.js"></script>
|
||||
<style>
|
||||
body {
|
||||
background-color: #f0f0f0;
|
||||
color: #444;
|
||||
}
|
||||
a {
|
||||
color: #08f;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
|
@ -8,12 +8,6 @@ import type {
|
|||
import type { Cell } from '../core/Cell';
|
||||
import type { ColorIndex } from '../core/Palette';
|
||||
|
||||
import { loadImage } from '../ui/loadImage';
|
||||
import {
|
||||
getColorIndexOfPixel,
|
||||
} from '../core/utils';
|
||||
|
||||
|
||||
export function sweetAlert(
|
||||
title: string,
|
||||
text: string,
|
||||
|
@ -228,7 +222,6 @@ export function requestPlacePixel(
|
|||
y,
|
||||
clr: color,
|
||||
token,
|
||||
a: x + y + 8,
|
||||
});
|
||||
|
||||
dispatch(setPlaceAllowed(false));
|
||||
|
@ -295,9 +288,7 @@ export function tryPlacePixel(
|
|||
? state.gui.selectedColor
|
||||
: color;
|
||||
|
||||
if (getColorIndexOfPixel(getState(), coordinates) !== selectedColor) {
|
||||
dispatch(requestPlacePixel(canvasId, coordinates, selectedColor));
|
||||
}
|
||||
dispatch(requestPlacePixel(canvasId, coordinates, selectedColor));
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -376,37 +367,23 @@ export function zoomOut(zoompoint): ThunkAction {
|
|||
};
|
||||
}
|
||||
|
||||
function requestBigChunk(center: Cell): Action {
|
||||
export function requestBigChunk(center: Cell): Action {
|
||||
return {
|
||||
type: 'REQUEST_BIG_CHUNK',
|
||||
center,
|
||||
};
|
||||
}
|
||||
|
||||
function receiveBigChunk(
|
||||
export function receiveBigChunk(
|
||||
center: Cell,
|
||||
arrayBuffer: ArrayBuffer,
|
||||
): Action {
|
||||
return {
|
||||
type: 'RECEIVE_BIG_CHUNK',
|
||||
center,
|
||||
arrayBuffer,
|
||||
};
|
||||
}
|
||||
|
||||
function receiveImageTile(
|
||||
center: Cell,
|
||||
tile: Image,
|
||||
): Action {
|
||||
return {
|
||||
type: 'RECEIVE_IMAGE_TILE',
|
||||
center,
|
||||
tile,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function receiveBigChunkFailure(center: Cell, error: Error): Action {
|
||||
export function receiveBigChunkFailure(center: Cell, error: Error): Action {
|
||||
return {
|
||||
type: 'RECEIVE_BIG_CHUNK_FAILURE',
|
||||
center,
|
||||
|
@ -414,74 +391,6 @@ function receiveBigChunkFailure(center: Cell, error: Error): Action {
|
|||
};
|
||||
}
|
||||
|
||||
export function fetchTile(canvasId, center: Cell): PromiseAction {
|
||||
const [cz, cx, cy] = center;
|
||||
|
||||
return async (dispatch) => {
|
||||
dispatch(requestBigChunk(center));
|
||||
try {
|
||||
const url = `/tiles/${canvasId}/${cz}/${cx}/${cy}.png`;
|
||||
const img = await loadImage(url);
|
||||
dispatch(receiveImageTile(center, img));
|
||||
} catch (error) {
|
||||
dispatch(receiveBigChunkFailure(center, error));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchHistoricalChunk(
|
||||
canvasId: number,
|
||||
center: Cell,
|
||||
historicalDate: string,
|
||||
historicalTime: string,
|
||||
): PromiseAction {
|
||||
const [cx, cy] = center;
|
||||
|
||||
return async (dispatch) => {
|
||||
let url = `${window.backupurl}/${historicalDate}/`;
|
||||
let zkey;
|
||||
if (historicalTime) {
|
||||
// incremential tiles
|
||||
zkey = `${historicalDate}${historicalTime}`;
|
||||
url += `${canvasId}/${historicalTime}/${cx}/${cy}.png`;
|
||||
} else {
|
||||
// full tiles
|
||||
zkey = historicalDate;
|
||||
url += `${canvasId}/tiles/${cx}/${cy}.png`;
|
||||
}
|
||||
const keyValues = [zkey, cx, cy];
|
||||
dispatch(requestBigChunk(keyValues));
|
||||
try {
|
||||
const img = await loadImage(url);
|
||||
dispatch(receiveImageTile(keyValues, img));
|
||||
} catch (error) {
|
||||
dispatch(receiveBigChunkFailure(keyValues, error));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchChunk(canvasId, center: Cell): PromiseAction {
|
||||
const [, cx, cy] = center;
|
||||
|
||||
return async (dispatch) => {
|
||||
dispatch(requestBigChunk(center));
|
||||
try {
|
||||
const url = `/chunks/${canvasId}/${cx}/${cy}.bmp`;
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
dispatch(receiveBigChunk(center, arrayBuffer));
|
||||
} else {
|
||||
const error = new Error('Network response was not ok.');
|
||||
dispatch(receiveBigChunkFailure(center, error));
|
||||
}
|
||||
} catch (error) {
|
||||
dispatch(receiveBigChunkFailure(center, error));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function receiveCoolDown(
|
||||
waitSeconds: number,
|
||||
): Action {
|
||||
|
@ -491,7 +400,6 @@ export function receiveCoolDown(
|
|||
};
|
||||
}
|
||||
|
||||
|
||||
export function receivePixelUpdate(
|
||||
i: number,
|
||||
j: number,
|
||||
|
@ -678,6 +586,10 @@ export function showHelpModal(): Action {
|
|||
return showModal('HELP');
|
||||
}
|
||||
|
||||
export function showCanvasSelectionModal(): Action {
|
||||
return showModal('CANVAS_SELECTION');
|
||||
}
|
||||
|
||||
export function showChatModal(): Action {
|
||||
if (window.innerWidth > 604) { return toggleChatBox(); }
|
||||
return showModal('CHAT');
|
||||
|
@ -714,10 +626,3 @@ export function urlChange(): PromiseAction {
|
|||
dispatch(reloadUrl());
|
||||
};
|
||||
}
|
||||
|
||||
export function switchCanvas(canvasId: number): PromiseAction {
|
||||
return async (dispatch) => {
|
||||
await dispatch(selectCanvas(canvasId));
|
||||
dispatch(onViewFinishChange());
|
||||
};
|
||||
}
|
||||
|
|
|
@ -41,8 +41,7 @@ export type Action =
|
|||
| { type: 'SET_VIEW_COORDINATES', view: Cell }
|
||||
| { type: 'SET_SCALE', scale: number, zoompoint: Cell }
|
||||
| { type: 'REQUEST_BIG_CHUNK', center: Cell }
|
||||
| { type: 'RECEIVE_BIG_CHUNK', center: Cell, arrayBuffer: ArrayBuffer }
|
||||
| { type: 'RECEIVE_IMAGE_TILE', center: Cell, tile: Image }
|
||||
| { type: 'RECEIVE_BIG_CHUNK', center: Cell }
|
||||
| { type: 'RECEIVE_BIG_CHUNK_FAILURE', center: Cell, error: Error }
|
||||
| { type: 'RECEIVE_COOLDOWN', waitSeconds: number }
|
||||
| { type: 'RECEIVE_PIXEL_UPDATE',
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"0": {
|
||||
"ident":"d",
|
||||
"title": "Earth",
|
||||
"colors": [
|
||||
[ 202, 227, 255 ],
|
||||
[ 255, 255, 255 ],
|
||||
|
@ -41,10 +42,12 @@
|
|||
"pcd" : 7000,
|
||||
"cds": 60000,
|
||||
"req": -1,
|
||||
"sd": "2020-01-08"
|
||||
"sd": "2020-01-08",
|
||||
"desc": "Our main canvas, a huge map of the world. Place everywhere you like"
|
||||
},
|
||||
"1": {
|
||||
"ident": "m",
|
||||
"title": "Moon",
|
||||
"colors" : [
|
||||
[ 49, 46, 47 ],
|
||||
[ 99, 92, 90 ],
|
||||
|
@ -85,6 +88,54 @@
|
|||
"pcd": 15000,
|
||||
"cds": 900000,
|
||||
"req": 8000,
|
||||
"sd": "2020-01-08"
|
||||
"sd": "2020-01-08",
|
||||
"desc": "Moon canvas with a pastel tone palette and black background"
|
||||
},
|
||||
"2": {
|
||||
"ident":"v",
|
||||
"title": "3D Canvas",
|
||||
"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 ]
|
||||
],
|
||||
"alpha": 0,
|
||||
"size": 1024,
|
||||
"v": true,
|
||||
"bcd": 2000,
|
||||
"pcd" : 2000,
|
||||
"cds": 60000,
|
||||
"req": 0,
|
||||
"sd": "2020-01-08",
|
||||
"desc": "Test 3D canvas. Changes are not saved."
|
||||
}
|
||||
}
|
||||
|
|
241
src/client.js
241
src/client.js
|
@ -4,208 +4,39 @@ import React from 'react';
|
|||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import fetch from 'isomorphic-fetch'; // TODO put in the beggining with webpack!
|
||||
import Hammer from 'hammerjs';
|
||||
|
||||
import './components/font.css';
|
||||
|
||||
// import initAds, { requestAds } from './ui/ads';
|
||||
import onKeyPress from './controls/keypress';
|
||||
import {
|
||||
screenToWorld,
|
||||
getColorIndexOfPixel,
|
||||
} from './core/utils';
|
||||
|
||||
import type { State } from './reducers';
|
||||
import initAds, { requestAds } from './ui/ads';
|
||||
import {
|
||||
tryPlacePixel,
|
||||
setHover,
|
||||
unsetHover,
|
||||
setViewCoordinates,
|
||||
setScale,
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
receivePixelUpdate,
|
||||
receiveCoolDown,
|
||||
fetchMe,
|
||||
fetchStats,
|
||||
initTimer,
|
||||
urlChange,
|
||||
onViewFinishChange,
|
||||
receiveOnline,
|
||||
receiveChatMessage,
|
||||
receiveChatHistory,
|
||||
selectColor,
|
||||
} from './actions';
|
||||
import store from './ui/store';
|
||||
|
||||
import onKeyPress from './ui/keypress';
|
||||
|
||||
import App from './components/App';
|
||||
|
||||
import renderer from './ui/Renderer';
|
||||
import { initRenderer, getRenderer } from './ui/renderer';
|
||||
import ProtocolClient from './socket/ProtocolClient';
|
||||
|
||||
window.addEventListener('keydown', onKeyPress, false);
|
||||
|
||||
|
||||
function initViewport() {
|
||||
const canvas = document.getElementById('gameWindow');
|
||||
|
||||
const viewport = canvas;
|
||||
viewport.width = window.innerWidth;
|
||||
viewport.height = window.innerHeight;
|
||||
|
||||
// track hover
|
||||
viewport.onmousemove = ({ clientX, clientY }: MouseEvent) => {
|
||||
store.dispatch(setHover([clientX, clientY]));
|
||||
};
|
||||
viewport.onmouseout = () => {
|
||||
store.dispatch(unsetHover());
|
||||
};
|
||||
viewport.onwheel = ({ deltaY }: WheelEvent) => {
|
||||
const state = store.getState();
|
||||
const { hover } = state.gui;
|
||||
let zoompoint = null;
|
||||
if (hover) {
|
||||
zoompoint = screenToWorld(state, viewport, hover);
|
||||
}
|
||||
if (deltaY < 0) {
|
||||
store.dispatch(zoomIn(zoompoint));
|
||||
}
|
||||
if (deltaY > 0) {
|
||||
store.dispatch(zoomOut(zoompoint));
|
||||
}
|
||||
store.dispatch(onViewFinishChange());
|
||||
};
|
||||
viewport.onauxclick = ({ which, clientX, clientY }: MouseEvent) => {
|
||||
// middle mouse button
|
||||
if (which !== 2) {
|
||||
return;
|
||||
}
|
||||
const state = store.getState();
|
||||
if (state.canvas.scale < 3) {
|
||||
return;
|
||||
}
|
||||
const coords = screenToWorld(state, viewport, [clientX, clientY]);
|
||||
const clrIndex = getColorIndexOfPixel(state, coords);
|
||||
if (clrIndex === null) {
|
||||
return;
|
||||
}
|
||||
store.dispatch(selectColor(clrIndex));
|
||||
};
|
||||
|
||||
// fingers controls on touch
|
||||
const hammertime = new Hammer(viewport);
|
||||
hammertime.get('pan').set({ direction: Hammer.DIRECTION_ALL });
|
||||
hammertime.get('swipe').set({ direction: Hammer.DIRECTION_ALL });
|
||||
// Zoom-in Zoom-out in touch devices
|
||||
hammertime.get('pinch').set({ enable: true });
|
||||
|
||||
hammertime.on('tap', ({ center }) => {
|
||||
const state = store.getState();
|
||||
const { autoZoomIn } = state.gui;
|
||||
const { placeAllowed } = state.user;
|
||||
|
||||
const {
|
||||
scale,
|
||||
isHistoricalView,
|
||||
} = state.canvas;
|
||||
if (isHistoricalView) return;
|
||||
|
||||
const { x, y } = center;
|
||||
const cell = screenToWorld(state, viewport, [x, y]);
|
||||
|
||||
if (autoZoomIn && scale < 8) {
|
||||
store.dispatch(setViewCoordinates(cell));
|
||||
store.dispatch(setScale(12));
|
||||
return;
|
||||
}
|
||||
|
||||
// don't allow placing of pixel just on low zoomlevels
|
||||
if (scale < 3) return;
|
||||
|
||||
if (!placeAllowed) return;
|
||||
|
||||
// dirty trick: to fetch only before multiple 3 AND on user action
|
||||
// if (pixelsPlaced % 3 === 0) requestAds();
|
||||
|
||||
// TODO assert only one finger
|
||||
store.dispatch(tryPlacePixel(cell));
|
||||
});
|
||||
|
||||
const initialState: State = store.getState();
|
||||
[window.lastPosX, window.lastPosY] = initialState.canvas.view;
|
||||
let lastScale = initialState.canvas.scale;
|
||||
hammertime.on(
|
||||
'panstart pinchstart pan pinch panend pinchend',
|
||||
({
|
||||
type, deltaX, deltaY, scale,
|
||||
}) => {
|
||||
viewport.style.cursor = 'move'; // like google maps
|
||||
const { scale: viewportScale } = store.getState().canvas;
|
||||
|
||||
// pinch start
|
||||
if (type === 'pinchstart') {
|
||||
store.dispatch(unsetHover());
|
||||
lastScale = viewportScale;
|
||||
}
|
||||
|
||||
// panstart
|
||||
if (type === 'panstart') {
|
||||
store.dispatch(unsetHover());
|
||||
const { view: initView } = store.getState().canvas;
|
||||
[window.lastPosX, window.lastPosY] = initView;
|
||||
}
|
||||
|
||||
// pinch
|
||||
if (type === 'pinch') {
|
||||
store.dispatch(setScale(lastScale * scale));
|
||||
}
|
||||
|
||||
// pan
|
||||
store.dispatch(setViewCoordinates([
|
||||
window.lastPosX - (deltaX / viewportScale),
|
||||
window.lastPosY - (deltaY / viewportScale),
|
||||
]));
|
||||
|
||||
// pinch end
|
||||
if (type === 'pinchend') {
|
||||
lastScale = viewportScale;
|
||||
}
|
||||
|
||||
// panend
|
||||
if (type === 'panend') {
|
||||
store.dispatch(onViewFinishChange());
|
||||
const { view } = store.getState().canvas;
|
||||
[window.lastPosX, window.lastPosY] = view;
|
||||
viewport.style.cursor = 'auto';
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return viewport;
|
||||
}
|
||||
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>,
|
||||
document.getElementById('app'),
|
||||
);
|
||||
|
||||
const viewport = initViewport();
|
||||
renderer.setViewport(viewport, store);
|
||||
function init() {
|
||||
initRenderer(store, false);
|
||||
|
||||
ProtocolClient.on('pixelUpdate', ({
|
||||
i, j, offset, color,
|
||||
}) => {
|
||||
store.dispatch(receivePixelUpdate(i, j, offset, color));
|
||||
// render updated pixel
|
||||
renderer.renderPixel(i, j, offset, color);
|
||||
});
|
||||
ProtocolClient.on('cooldownPacket', (waitSeconds) => {
|
||||
console.log(`Received CoolDown ${waitSeconds}`);
|
||||
store.dispatch(receiveCoolDown(waitSeconds));
|
||||
});
|
||||
ProtocolClient.on('onlineCounter', ({ online }) => {
|
||||
|
@ -221,55 +52,53 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
store.dispatch(fetchMe());
|
||||
});
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
viewport.width = window.innerWidth;
|
||||
viewport.height = window.innerHeight;
|
||||
renderer.forceNextRender = true;
|
||||
});
|
||||
window.addEventListener('hashchange', () => {
|
||||
store.dispatch(urlChange());
|
||||
});
|
||||
|
||||
store.subscribe(() => {
|
||||
// const state: State = store.getState();
|
||||
// this gets executed when store changes
|
||||
});
|
||||
|
||||
store.dispatch(initTimer());
|
||||
|
||||
window.animationLoop = function animationLoop() {
|
||||
renderer.render(viewport);
|
||||
window.requestAnimationFrame(window.animationLoop);
|
||||
}
|
||||
window.animationLoop();
|
||||
window.store = store;
|
||||
|
||||
store.dispatch(fetchMe());
|
||||
ProtocolClient.connect();
|
||||
|
||||
store.dispatch(fetchStats());
|
||||
setInterval(() => { store.dispatch(fetchStats()); }, 300000);
|
||||
}
|
||||
init();
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>,
|
||||
document.getElementById('app'),
|
||||
);
|
||||
|
||||
document.addEventListener('keydown', onKeyPress, false);
|
||||
|
||||
// garbage collection
|
||||
function runGC() {
|
||||
const state: State = store.getState();
|
||||
const { chunks } = state.canvas;
|
||||
const renderer = getRenderer();
|
||||
|
||||
const curTime = Date.now();
|
||||
let cnt = 0;
|
||||
chunks.forEach((value, key) => {
|
||||
if (curTime > value.timestamp + 300000) {
|
||||
cnt++;
|
||||
const [z, i, j] = value.cell;
|
||||
if (!renderer.isChunkInView(z, i, j)) {
|
||||
if (value.isBasechunk) {
|
||||
ProtocolClient.deRegisterChunk([i, j]);
|
||||
const chunks = renderer.getAllChunks();
|
||||
if (chunks) {
|
||||
const curTime = Date.now();
|
||||
let cnt = 0;
|
||||
chunks.forEach((value, key) => {
|
||||
if (curTime > value.timestamp + 300000) {
|
||||
cnt++;
|
||||
const [z, i, j] = value.cell;
|
||||
if (!renderer.isChunkInView(z, i, j)) {
|
||||
if (value.isBasechunk) {
|
||||
ProtocolClient.deRegisterChunk([i, j]);
|
||||
}
|
||||
chunks.delete(key);
|
||||
}
|
||||
chunks.delete(key);
|
||||
}
|
||||
}
|
||||
});
|
||||
console.log('Garbage collection cleaned', cnt, 'chunks');
|
||||
});
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Garbage collection cleaned', cnt, 'chunks');
|
||||
}
|
||||
}
|
||||
setInterval(runGC, 300000);
|
||||
});
|
||||
|
|
|
@ -4,32 +4,25 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { IconContext } from 'react-icons';
|
||||
|
||||
import type { State } from '../reducers';
|
||||
import CoolDownBox from './CoolDownBox';
|
||||
import NotifyBox from './NotifyBox';
|
||||
import CoordinatesBox from './CoordinatesBox';
|
||||
import GlobeButton from './GlobeButton';
|
||||
import CanvasSwitchButton from './CanvasSwitchButton';
|
||||
import OnlineBox from './OnlineBox';
|
||||
import PalselButton from './PalselButton';
|
||||
import ChatButton from './ChatButton';
|
||||
import Palette from './Palette';
|
||||
import ChatBox from './ChatBox';
|
||||
import Menu from './Menu';
|
||||
import UI from './UI';
|
||||
import ReCaptcha from './ReCaptcha';
|
||||
import ExpandMenuButton from './ExpandMenuButton';
|
||||
import ModalRoot from './ModalRoot';
|
||||
import HistorySelect from './HistorySelect';
|
||||
|
||||
import baseCss from './base.tcss';
|
||||
|
||||
const App = ({ isHistoricalView }) => (
|
||||
const App = () => (
|
||||
<div>
|
||||
{/* eslint-disable-next-line react/no-danger */}
|
||||
<style dangerouslySetInnerHTML={{ __html: baseCss }} />
|
||||
<canvas id="gameWindow" />
|
||||
<div id="outstreamContainer" />
|
||||
<ReCaptcha />
|
||||
<IconContext.Provider value={{ style: { verticalAlign: 'middle' } }}>
|
||||
|
@ -40,31 +33,10 @@ const App = ({ isHistoricalView }) => (
|
|||
<OnlineBox />
|
||||
<CoordinatesBox />
|
||||
<ExpandMenuButton />
|
||||
{
|
||||
(isHistoricalView)
|
||||
? <HistorySelect />
|
||||
: (
|
||||
<div>
|
||||
<PalselButton />
|
||||
<Palette />
|
||||
<GlobeButton />
|
||||
<CoolDownBox />
|
||||
<NotifyBox />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<UI />
|
||||
<ModalRoot />
|
||||
</IconContext.Provider>
|
||||
</div>
|
||||
);
|
||||
|
||||
function mapStateToProps(state: State) {
|
||||
const {
|
||||
isHistoricalView,
|
||||
} = state.canvas;
|
||||
return {
|
||||
isHistoricalView,
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(App);
|
||||
export default App;
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
/**
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { THREE_CANVAS_HEIGHT } from '../core/constants';
|
||||
import { selectCanvas } from '../actions';
|
||||
|
||||
|
||||
const textStyle = {
|
||||
color: 'hsla(218, 5%, 47%, .6)',
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
position: 'relative',
|
||||
textAlign: 'inherit',
|
||||
float: 'none',
|
||||
margin: 2,
|
||||
padding: 0,
|
||||
overflow: 'auto',
|
||||
};
|
||||
|
||||
const infoStyle = {
|
||||
color: '#4f545c',
|
||||
fontSize: 15,
|
||||
fontWeight: 500,
|
||||
position: 'relative',
|
||||
textAlign: 'inherit',
|
||||
float: 'none',
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
};
|
||||
|
||||
const titleStyle = {
|
||||
color: '#4f545c',
|
||||
marginLeft: 0,
|
||||
marginRight: 10,
|
||||
overflow: 'hidden',
|
||||
wordWrap: 'break-word',
|
||||
lineHeight: '24px',
|
||||
fontSize: 16,
|
||||
marginTop: 5,
|
||||
marginBottom: 0,
|
||||
fontWeight: 'bold',
|
||||
};
|
||||
|
||||
const buttonStyle = {
|
||||
marginTop: 10,
|
||||
marginBottom: 10,
|
||||
border: '#c5c5c5',
|
||||
borderStyle: 'solid',
|
||||
cursor: 'pointer',
|
||||
};
|
||||
|
||||
const CanvasItem = ({ canvasId, canvas, changeCanvas }) => (
|
||||
<div
|
||||
style={buttonStyle}
|
||||
onClick={() => { changeCanvas(canvasId); }}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<p style={textStyle}>
|
||||
<img
|
||||
style={{
|
||||
float: 'left', maxWidth: '20%', margin: 5, opacity: 0.5,
|
||||
}}
|
||||
alt="preview"
|
||||
src={`/preview${canvasId}.png`}
|
||||
/>
|
||||
<span style={titleStyle}>{canvas.title}</span><br />
|
||||
<span style={infoStyle}>{canvas.desc}</span><br />
|
||||
Cooldown:
|
||||
<span style={infoStyle}>
|
||||
{(canvas.bcd !== canvas.pcd)
|
||||
? <span> {canvas.bcd / 1000}s / {canvas.pcd / 1000}s</span>
|
||||
: <span> {canvas.bcd / 1000}s</span>}
|
||||
</span><br />
|
||||
Stacking till
|
||||
<span style={infoStyle}> {canvas.cds / 1000}s</span><br />
|
||||
{(canvas.req !== -1) ? <span>Requirements:<br /></span> : null}
|
||||
<span style={infoStyle}>
|
||||
{(canvas.req !== -1) ? <span>User Account </span> : null}
|
||||
{(canvas.req > 0) ? <span> and {canvas.req} Pixels set</span> : null}
|
||||
</span><br />
|
||||
Dimensions:
|
||||
<span style={infoStyle}> {canvas.size} x {canvas.size}
|
||||
{(canvas.v)
|
||||
? <span> x {THREE_CANVAS_HEIGHT} Voxels</span>
|
||||
: <span> Pixels</span>}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
changeCanvas(canvasId) {
|
||||
dispatch(selectCanvas(canvasId));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(CanvasItem);
|
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
// import FaFacebook from 'react-icons/lib/fa/facebook';
|
||||
// import FaTwitter from 'react-icons/lib/fa/twitter';
|
||||
// import FaRedditAlien from 'react-icons/lib/fa/reddit-alien';
|
||||
|
||||
import Modal from './Modal';
|
||||
import CanvasItem from './CanvasItem';
|
||||
|
||||
import type { State } from '../reducers';
|
||||
|
||||
|
||||
const textStyle = {
|
||||
color: 'hsla(218, 5%, 47%, .6)',
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
position: 'relative',
|
||||
textAlign: 'inherit',
|
||||
float: 'none',
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
lineHeight: 'normal',
|
||||
};
|
||||
|
||||
|
||||
const CanvasSelectModal = ({ canvases }) => (
|
||||
<Modal title="Canvas Selection">
|
||||
<p style={{ textAlign: 'center' }}>
|
||||
<p style={textStyle}>
|
||||
Select the canvas you want to use.
|
||||
Every canvas is unique and has different palettes,
|
||||
cooldown and requirements.
|
||||
</p>
|
||||
{
|
||||
Object.keys(canvases).map((canvasId) => (
|
||||
<CanvasItem canvasId={canvasId} canvas={canvases[canvasId]} />
|
||||
))
|
||||
}
|
||||
</p>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
function mapStateToProps(state: State) {
|
||||
const { canvases } = state.canvas;
|
||||
return { canvases };
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(CanvasSelectModal);
|
|
@ -5,16 +5,20 @@
|
|||
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { FaGlobe, FaGlobeAfrica } from 'react-icons/fa';
|
||||
import { FaGlobe } from 'react-icons/fa';
|
||||
|
||||
import { switchCanvas } from '../actions';
|
||||
import { showCanvasSelectionModal } from '../actions';
|
||||
|
||||
import type { State } from '../reducers';
|
||||
|
||||
|
||||
const CanvasSwitchButton = ({ canvasId, changeCanvas }) => (
|
||||
<div id="canvasbutton" className="actionbuttons" onClick={() => changeCanvas(canvasId)}>
|
||||
{(canvasId == 0) ? <FaGlobe /> : <FaGlobeAfrica />}
|
||||
const CanvasSwitchButton = ({ open }) => (
|
||||
<div
|
||||
id="canvasbutton"
|
||||
className="actionbuttons"
|
||||
onClick={open}
|
||||
>
|
||||
<FaGlobe />
|
||||
</div>
|
||||
);
|
||||
|
||||
|
@ -25,12 +29,11 @@ function mapStateToProps(state: State) {
|
|||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
changeCanvas(canvasId) {
|
||||
const newCanvasId = (canvasId == 0) ? 1 : 0;
|
||||
dispatch(switchCanvas(newCanvasId));
|
||||
open() {
|
||||
dispatch(showCanvasSelectionModal());
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps,
|
||||
export default connect(null,
|
||||
mapDispatchToProps)(CanvasSwitchButton);
|
||||
|
|
|
@ -6,26 +6,24 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { screenToWorld } from '../core/utils';
|
||||
import type { State } from '../reducers';
|
||||
|
||||
|
||||
function renderCoordinates([x, y]: Cell): string {
|
||||
return `(${x}, ${y})`;
|
||||
function renderCoordinates(cell): string {
|
||||
return `(${cell.join(', ')})`;
|
||||
}
|
||||
|
||||
// TODO vaya chapuza, arreglalo un poco...
|
||||
// TODO create viewport state
|
||||
const CoordinatesBox = ({ state, view, hover }) => (
|
||||
<div className="coorbox">{renderCoordinates(hover
|
||||
? screenToWorld(state, document.getElementById('gameWindow'), hover)
|
||||
: view.map(Math.round))}</div>
|
||||
const CoordinatesBox = ({ view, hover }) => (
|
||||
<div className="coorbox">{
|
||||
renderCoordinates(hover
|
||||
|| view.map(Math.round))
|
||||
}</div>
|
||||
);
|
||||
|
||||
function mapStateToProps(state: State) {
|
||||
const { view } = state.canvas;
|
||||
const { hover } = state.gui;
|
||||
return { view, state, hover };
|
||||
return { view, hover };
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(CoordinatesBox);
|
||||
|
|
|
@ -11,7 +11,6 @@ import LogInButton from './LogInButton';
|
|||
import DownloadButton from './DownloadButton';
|
||||
import MinecraftTPButton from './MinecraftTPButton';
|
||||
import MinecraftButton from './MinecraftButton';
|
||||
import VoxelButton from './VoxelButton';
|
||||
|
||||
const Menu = ({
|
||||
menuOpen, minecraftname, messages, canvasId,
|
||||
|
@ -22,7 +21,6 @@ const Menu = ({
|
|||
{(menuOpen) ? <DownloadButton /> : null}
|
||||
{(menuOpen) ? <MinecraftButton /> : null}
|
||||
{(menuOpen) ? <HelpButton /> : null}
|
||||
{(menuOpen) ? <VoxelButton /> : null}
|
||||
{(minecraftname && !messages.includes('not_mc_verified') && canvasId == 0) ? <MinecraftTPButton /> : null}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -12,6 +12,7 @@ import HelpModal from './HelpModal';
|
|||
import SettingsModal from './SettingsModal';
|
||||
import UserAreaModal from './UserAreaModal';
|
||||
import RegisterModal from './RegisterModal';
|
||||
import CanvasSelectModal from './CanvasSelectModal';
|
||||
import ChatModal from './ChatModal';
|
||||
import ForgotPasswordModal from './ForgotPasswordModal';
|
||||
import MinecraftModal from './MinecraftModal';
|
||||
|
@ -25,6 +26,7 @@ const MODAL_COMPONENTS = {
|
|||
FORGOT_PASSWORD: ForgotPasswordModal,
|
||||
CHAT: ChatModal,
|
||||
MINECRAFT: MinecraftModal,
|
||||
CANVAS_SELECTION: CanvasSelectModal,
|
||||
/* other modals */
|
||||
};
|
||||
|
||||
|
|
|
@ -10,12 +10,19 @@ import type { State } from '../reducers';
|
|||
|
||||
let style = {};
|
||||
function getStyle(notification) {
|
||||
if (notification) style = { backgroundColor: (notification >= 0) ? '#a9ffb0cc' : '#ffa9a9cc' };
|
||||
if (notification) {
|
||||
style = {
|
||||
backgroundColor: (notification >= 0) ? '#a9ffb0cc' : '#ffa9a9cc',
|
||||
};
|
||||
}
|
||||
return style;
|
||||
}
|
||||
|
||||
const NotifyBox = ({ notification }) => (
|
||||
<div className={(notification) ? 'notifyboxvis' : 'notifyboxhid'} style={getStyle(notification)}>
|
||||
<div
|
||||
className={(notification) ? 'notifyboxvis' : 'notifyboxhid'}
|
||||
style={getStyle(notification)}
|
||||
>
|
||||
{notification}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
/**
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import type { State } from '../reducers';
|
||||
import CoolDownBox from './CoolDownBox';
|
||||
import NotifyBox from './NotifyBox';
|
||||
import GlobeButton from './GlobeButton';
|
||||
import PalselButton from './PalselButton';
|
||||
import Palette from './Palette';
|
||||
import HistorySelect from './HistorySelect';
|
||||
|
||||
|
||||
const UI = ({ isHistoricalView }) => {
|
||||
if (isHistoricalView) {
|
||||
return <HistorySelect />;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<PalselButton />
|
||||
<Palette />
|
||||
<GlobeButton />
|
||||
<CoolDownBox />
|
||||
<NotifyBox />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function mapStateToProps(state: State) {
|
||||
const {
|
||||
isHistoricalView,
|
||||
} = state.canvas;
|
||||
return {
|
||||
isHistoricalView,
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(UI);
|
|
@ -1,24 +0,0 @@
|
|||
/**
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import type { State } from '../reducers';
|
||||
|
||||
async function switchVoxel() {
|
||||
await import(/* webpackChunkName: "voxel" */ '../voxel');
|
||||
console.log("Chunk voxel loaded");
|
||||
}
|
||||
|
||||
|
||||
const VoxelButton = ({
|
||||
canvasId, canvasIdent, canvasSize, view,
|
||||
}) => (
|
||||
<div id="voxelbutton" className="actionbuttons" onClick={switchVoxel}>
|
||||
♠
|
||||
</div>
|
||||
);
|
||||
|
||||
export default VoxelButton;
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,210 @@
|
|||
/*
|
||||
* Creates Viewport for 2D Canvas
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import Hammer from 'hammerjs';
|
||||
import keycode from 'keycode';
|
||||
|
||||
import {
|
||||
tryPlacePixel,
|
||||
setHover,
|
||||
unsetHover,
|
||||
setViewCoordinates,
|
||||
setScale,
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
selectColor,
|
||||
moveNorth,
|
||||
moveWest,
|
||||
moveSouth,
|
||||
moveEast,
|
||||
onViewFinishChange,
|
||||
} from '../actions';
|
||||
import {
|
||||
screenToWorld,
|
||||
} from '../core/utils';
|
||||
|
||||
let store = null;
|
||||
|
||||
function onKeyPress(event: KeyboardEvent) {
|
||||
// ignore key presses if modal is open or chat is used
|
||||
if (event.target.nodeName === 'INPUT'
|
||||
|| event.target.nodeName === 'TEXTAREA'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (keycode(event)) {
|
||||
case 'up':
|
||||
case 'w':
|
||||
store.dispatch(moveNorth());
|
||||
break;
|
||||
case 'left':
|
||||
case 'a':
|
||||
store.dispatch(moveWest());
|
||||
break;
|
||||
case 'down':
|
||||
case 's':
|
||||
store.dispatch(moveSouth());
|
||||
break;
|
||||
case 'right':
|
||||
case 'd':
|
||||
store.dispatch(moveEast());
|
||||
break;
|
||||
/*
|
||||
case 'space':
|
||||
if ($viewport) $viewport.click();
|
||||
return;
|
||||
*/
|
||||
case '+':
|
||||
case 'e':
|
||||
store.dispatch(zoomIn());
|
||||
break;
|
||||
case '-':
|
||||
case 'q':
|
||||
store.dispatch(zoomOut());
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
export function initControls(renderer, viewport: HTMLCanvasElement, curStore) {
|
||||
store = curStore;
|
||||
viewport.onmousemove = ({ clientX, clientY }: MouseEvent) => {
|
||||
const state = store.getState();
|
||||
const screenCoor = screenToWorld(state, viewport, [clientX, clientY]);
|
||||
store.dispatch(setHover(screenCoor));
|
||||
};
|
||||
viewport.onmouseout = () => {
|
||||
store.dispatch(unsetHover());
|
||||
};
|
||||
viewport.onwheel = ({ deltaY }: WheelEvent) => {
|
||||
const state = store.getState();
|
||||
const { hover } = state.gui;
|
||||
let zoompoint = null;
|
||||
if (hover) {
|
||||
zoompoint = hover;
|
||||
}
|
||||
if (deltaY < 0) {
|
||||
store.dispatch(zoomIn(zoompoint));
|
||||
}
|
||||
if (deltaY > 0) {
|
||||
store.dispatch(zoomOut(zoompoint));
|
||||
}
|
||||
store.dispatch(onViewFinishChange());
|
||||
};
|
||||
viewport.onauxclick = ({ which, clientX, clientY }: MouseEvent) => {
|
||||
// middle mouse button
|
||||
if (which !== 2) {
|
||||
return;
|
||||
}
|
||||
const state = store.getState();
|
||||
if (state.canvas.scale < 3) {
|
||||
return;
|
||||
}
|
||||
const coords = screenToWorld(state, viewport, [clientX, clientY]);
|
||||
const clrIndex = renderer.getColorIndexOfPixel(...coords);
|
||||
if (clrIndex === null) {
|
||||
return;
|
||||
}
|
||||
store.dispatch(selectColor(clrIndex));
|
||||
};
|
||||
|
||||
// fingers controls on touch
|
||||
const hammertime = new Hammer(viewport);
|
||||
hammertime.get('pan').set({ direction: Hammer.DIRECTION_ALL });
|
||||
hammertime.get('swipe').set({ direction: Hammer.DIRECTION_ALL });
|
||||
// Zoom-in Zoom-out in touch devices
|
||||
hammertime.get('pinch').set({ enable: true });
|
||||
|
||||
hammertime.on('tap', ({ center }) => {
|
||||
const state = store.getState();
|
||||
const { autoZoomIn, selectedColor } = state.gui;
|
||||
const { placeAllowed } = state.user;
|
||||
|
||||
const {
|
||||
scale,
|
||||
isHistoricalView,
|
||||
} = state.canvas;
|
||||
if (isHistoricalView) return;
|
||||
|
||||
const { x, y } = center;
|
||||
const cell = screenToWorld(state, viewport, [x, y]);
|
||||
|
||||
if (autoZoomIn && scale < 8) {
|
||||
store.dispatch(setViewCoordinates(cell));
|
||||
store.dispatch(setScale(12));
|
||||
return;
|
||||
}
|
||||
|
||||
// don't allow placing of pixel just on low zoomlevels
|
||||
if (scale < 3) return;
|
||||
|
||||
if (!placeAllowed) return;
|
||||
|
||||
// dirty trick: to fetch only before multiple 3 AND on user action
|
||||
// if (pixelsPlaced % 3 === 0) requestAds();
|
||||
|
||||
if (selectedColor !== renderer.getColorIndexOfPixel(...cell)) {
|
||||
store.dispatch(tryPlacePixel(cell));
|
||||
}
|
||||
});
|
||||
|
||||
const initialState: State = store.getState();
|
||||
[window.lastPosX, window.lastPosY] = initialState.canvas.view;
|
||||
let lastScale = initialState.canvas.scale;
|
||||
hammertime.on(
|
||||
'panstart pinchstart pan pinch panend pinchend',
|
||||
({
|
||||
type, deltaX, deltaY, scale,
|
||||
}) => {
|
||||
viewport.style.cursor = 'move'; // like google maps
|
||||
const { scale: viewportScale } = store.getState().canvas;
|
||||
|
||||
// pinch start
|
||||
if (type === 'pinchstart') {
|
||||
store.dispatch(unsetHover());
|
||||
lastScale = viewportScale;
|
||||
}
|
||||
|
||||
// panstart
|
||||
if (type === 'panstart') {
|
||||
store.dispatch(unsetHover());
|
||||
const { view: initView } = store.getState().canvas;
|
||||
[window.lastPosX, window.lastPosY] = initView;
|
||||
}
|
||||
|
||||
// pinch
|
||||
if (type === 'pinch') {
|
||||
store.dispatch(setScale(lastScale * scale));
|
||||
}
|
||||
|
||||
// pan
|
||||
store.dispatch(setViewCoordinates([
|
||||
window.lastPosX - (deltaX / viewportScale),
|
||||
window.lastPosY - (deltaY / viewportScale),
|
||||
]));
|
||||
|
||||
// pinch end
|
||||
if (type === 'pinchend') {
|
||||
lastScale = viewportScale;
|
||||
}
|
||||
|
||||
// panend
|
||||
if (type === 'panend') {
|
||||
store.dispatch(onViewFinishChange());
|
||||
const { view } = store.getState().canvas;
|
||||
[window.lastPosX, window.lastPosY] = view;
|
||||
viewport.style.cursor = 'auto';
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
document.addEventListener('keydown', onKeyPress, false);
|
||||
}
|
||||
|
||||
export function removeControls() {
|
||||
document.removeEventListener('keydown', onKeyPress, false);
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* keypress actions
|
||||
* @flow
|
||||
*/
|
||||
import keycode from 'keycode';
|
||||
|
||||
import store from '../ui/store';
|
||||
import {
|
||||
toggleGrid,
|
||||
togglePixelNotify,
|
||||
toggleMute,
|
||||
} from '../actions';
|
||||
|
||||
|
||||
function onKeyPress(event: KeyboardEvent) {
|
||||
// ignore key presses if modal is open or chat is used
|
||||
if (event.target.nodeName === 'INPUT'
|
||||
|| event.target.nodeName === 'TEXTAREA'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (keycode(event)) {
|
||||
case 'g':
|
||||
store.dispatch(toggleGrid());
|
||||
break;
|
||||
case 'c':
|
||||
store.dispatch(togglePixelNotify());
|
||||
break;
|
||||
case 'm':
|
||||
store.dispatch(toggleMute());
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
export default onKeyPress;
|
|
@ -40,15 +40,17 @@ export async function imageABGR2Canvas(
|
|||
const canvasMinXY = -(canvas.size / 2);
|
||||
const imageData = new Uint32Array(data.buffer);
|
||||
|
||||
const [ucx, ucy] = getChunkOfPixel([x, y], canvas.size);
|
||||
const [lcx, lcy] = getChunkOfPixel([(x + width), (y + height)], canvas.size);
|
||||
const [ucx, ucy] = getChunkOfPixel(canvas.size, x, y);
|
||||
const [lcx, lcy] = getChunkOfPixel(canvas.size, x + width, y + height);
|
||||
|
||||
logger.info(`Loading to chunks from ${ucx} / ${ucy} to ${lcx} / ${lcy} ...`);
|
||||
let chunk;
|
||||
for (let cx = ucx; cx <= lcx; cx += 1) {
|
||||
for (let cy = ucy; cy <= lcy; cy += 1) {
|
||||
chunk = await RedisCanvas.getChunk(cx, cy, canvasId);
|
||||
chunk = (chunk) ? new Uint8Array(chunk) : new Uint8Array(TILE_SIZE * TILE_SIZE);
|
||||
chunk = (chunk)
|
||||
? new Uint8Array(chunk)
|
||||
: new Uint8Array(TILE_SIZE * TILE_SIZE);
|
||||
// offset of chunk in image
|
||||
const cOffX = cx * TILE_SIZE + canvasMinXY - x;
|
||||
const cOffY = cy * TILE_SIZE + canvasMinXY - y;
|
||||
|
@ -60,7 +62,9 @@ export async function imageABGR2Canvas(
|
|||
const clrY = cOffY + py;
|
||||
if (clrX >= 0 && clrY >= 0 && clrX < width && clrY < height) {
|
||||
const clr = imageData[clrX + clrY * width];
|
||||
const clrIndex = (wipe) ? palette.abgr.indexOf(clr) : palette.abgr.indexOf(clr, 2);
|
||||
const clrIndex = (wipe)
|
||||
? palette.abgr.indexOf(clr)
|
||||
: palette.abgr.indexOf(clr, 2);
|
||||
if (~clrIndex) {
|
||||
const pixel = (protect) ? (clrIndex | 0x20) : clrIndex;
|
||||
chunk[cOff] = pixel;
|
||||
|
@ -113,15 +117,17 @@ export async function imagemask2Canvas(
|
|||
|
||||
const imageData = new Uint8Array(data.buffer);
|
||||
|
||||
const [ucx, ucy] = getChunkOfPixel([x, y], canvas.size);
|
||||
const [lcx, lcy] = getChunkOfPixel([(x + width), (y + height)], canvas.size);
|
||||
const [ucx, ucy] = getChunkOfPixel(canvas.size, x, y);
|
||||
const [lcx, lcy] = getChunkOfPixel(canvas.size, x + width, y + height);
|
||||
|
||||
logger.info(`Loading to chunks from ${ucx} / ${ucy} to ${lcx} / ${lcy} ...`);
|
||||
let chunk;
|
||||
for (let cx = ucx; cx <= lcx; cx += 1) {
|
||||
for (let cy = ucy; cy <= lcy; cy += 1) {
|
||||
chunk = await RedisCanvas.getChunk(cx, cy, canvasId);
|
||||
chunk = (chunk) ? new Uint8Array(chunk) : new Uint8Array(TILE_SIZE * TILE_SIZE);
|
||||
chunk = (chunk)
|
||||
? new Uint8Array(chunk)
|
||||
: new Uint8Array(TILE_SIZE * TILE_SIZE);
|
||||
// offset of chunk in image
|
||||
const cOffX = cx * TILE_SIZE + canvasMinXY - x;
|
||||
const cOffY = cy * TILE_SIZE + canvasMinXY - y;
|
||||
|
@ -133,7 +139,10 @@ export async function imagemask2Canvas(
|
|||
const clrY = cOffY + py;
|
||||
if (clrX >= 0 && clrY >= 0 && clrX < width && clrY < height) {
|
||||
let offset = (clrX + clrY * width) * 3;
|
||||
if (!imageData[offset++] && !imageData[offset++] && !imageData[offset]) {
|
||||
if (!imageData[offset++]
|
||||
&& !imageData[offset++]
|
||||
&& !imageData[offset]
|
||||
) {
|
||||
chunk[cOff] = filter(palette.abgr[chunk[cOff]]);
|
||||
pxlCnt += 1;
|
||||
}
|
||||
|
|
|
@ -62,6 +62,9 @@ export const DEFAULT_CANVASES = {
|
|||
|
||||
export const TILE_LOADING_IMAGE = './loading.png';
|
||||
|
||||
// constants for 3D voxel canvas
|
||||
export const THREE_CANVAS_HEIGHT = 128;
|
||||
export const THREE_TILE_SIZE = 64;
|
||||
// one bigchunk has 16x16 smallchunks, one smallchunk has 64x64 pixel, so one bigchunk is 1024x1024 pixels
|
||||
export const TILE_SIZE = 256;
|
||||
// how much to scale for a new tiled zoomlevel
|
||||
|
|
|
@ -8,9 +8,10 @@ import { getChunkOfPixel, getOffsetOfPixel } from './utils';
|
|||
import webSockets from '../socket/websockets';
|
||||
import logger from './logger';
|
||||
import RedisCanvas from '../data/models/RedisCanvas';
|
||||
import { registerPixelChange } from './tileserver';
|
||||
import canvases from '../canvases.json';
|
||||
|
||||
import { THREE_CANVAS_HEIGHT } from './constants';
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -21,13 +22,14 @@ import canvases from '../canvases.json';
|
|||
*/
|
||||
export function setPixel(
|
||||
canvasId: number,
|
||||
color: ColorIndex,
|
||||
x: number,
|
||||
y: number,
|
||||
color: ColorIndex,
|
||||
z: number = null,
|
||||
) {
|
||||
const canvasSize = canvases[canvasId].size;
|
||||
const [i, j] = getChunkOfPixel([x, y], canvasSize);
|
||||
const offset = getOffsetOfPixel(x, y, canvasSize);
|
||||
const [i, j] = getChunkOfPixel(canvasSize, x, y, z);
|
||||
const offset = getOffsetOfPixel(canvasSize, x, y, z);
|
||||
RedisCanvas.setPixelInChunk(i, j, offset, color, canvasId);
|
||||
webSockets.broadcastPixel(canvasId, i, j, offset, color);
|
||||
}
|
||||
|
@ -44,9 +46,10 @@ export function setPixel(
|
|||
async function draw(
|
||||
user: User,
|
||||
canvasId: number,
|
||||
color: ColorIndex,
|
||||
x: number,
|
||||
y: number,
|
||||
color: ColorIndex,
|
||||
z: number = null,
|
||||
): Promise<Object> {
|
||||
if (!({}.hasOwnProperty.call(canvases, canvasId))) {
|
||||
return {
|
||||
|
@ -65,6 +68,25 @@ async function draw(
|
|||
success: false,
|
||||
};
|
||||
}
|
||||
if (z !== null) {
|
||||
if (z >= THREE_CANVAS_HEIGHT) {
|
||||
return {
|
||||
error: 'You reached build limit. Can\'t place higher than 128 blocks.',
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
if (!canvas.v) {
|
||||
return {
|
||||
error: 'This is not a 3D canvas',
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
} else if (canvas.v) {
|
||||
return {
|
||||
error: 'This is a 3D canvas. z is required.',
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (canvas.req !== -1) {
|
||||
if (user.id === null) {
|
||||
|
@ -80,13 +102,14 @@ async function draw(
|
|||
if (totalPixels < canvas.req) {
|
||||
return {
|
||||
errorTitle: 'Not Yet :(',
|
||||
// eslint-disable-next-line max-len
|
||||
error: `You need to set ${canvas.req} pixels on another canvas first, before you can use this one.`,
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const setColor = await RedisCanvas.getPixel(x, y, canvasId);
|
||||
const setColor = await RedisCanvas.getPixel(canvasId, x, y, z);
|
||||
|
||||
let coolDown = !(setColor & 0x1E) ? canvas.bcd : canvas.pcd;
|
||||
if (user.isAdmin()) {
|
||||
|
@ -116,7 +139,7 @@ async function draw(
|
|||
};
|
||||
}
|
||||
|
||||
setPixel(canvasId, x, y, color);
|
||||
setPixel(canvasId, color, x, y, null);
|
||||
|
||||
user.setWait(waitLeft, canvasId);
|
||||
user.incrementPixelcount();
|
||||
|
@ -141,12 +164,13 @@ async function draw(
|
|||
function drawSafe(
|
||||
user: User,
|
||||
canvasId: number,
|
||||
color: ColorIndex,
|
||||
x: number,
|
||||
y: number,
|
||||
color: ColorIndex,
|
||||
z: number = null,
|
||||
): Promise<Cell> {
|
||||
if (user.isAdmin()) {
|
||||
return draw(user, canvasId, x, y, color);
|
||||
return draw(user, canvasId, color, x, y, z);
|
||||
}
|
||||
|
||||
// can just check for one unique occurence,
|
||||
|
@ -158,7 +182,7 @@ function drawSafe(
|
|||
using(
|
||||
redlock.disposer(`locks:${userId}`, 5000, logger.error),
|
||||
async () => {
|
||||
const ret = await draw(user, canvasId, x, y, color);
|
||||
const ret = await draw(user, canvasId, color, x, y, z);
|
||||
resolve(ret);
|
||||
},
|
||||
); // <-- unlock is automatically handled by bluebird
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* @flow
|
||||
*/
|
||||
|
||||
import Sequelize from 'sequelize';
|
||||
// import Sequelize from 'sequelize';
|
||||
import nodemailer from 'nodemailer';
|
||||
|
||||
import logger from './logger';
|
||||
|
|
|
@ -24,6 +24,10 @@ export async function updateBackupRedis(canvasRedis, backupRedis, canvases) {
|
|||
for (let i = 0; i < ids.length; i += 1) {
|
||||
const id = ids[i];
|
||||
const canvas = canvases[id];
|
||||
if (canvas.v) {
|
||||
// ignore 3D canvases
|
||||
continue;
|
||||
}
|
||||
const chunksXY = (canvas.size / TILE_SIZE);
|
||||
console.log('Copy Chunks to backup redis...');
|
||||
const startTime = Date.now();
|
||||
|
@ -67,6 +71,11 @@ export async function incrementialBackupRedis(
|
|||
for (let i = 0; i < ids.length; i += 1) {
|
||||
const id = ids[i];
|
||||
|
||||
const canvas = canvases[id];
|
||||
if (canvas.v) {
|
||||
// ignore 3D canvases
|
||||
continue;
|
||||
}
|
||||
|
||||
const canvasBackupDir = `${backupDir}/${id}`;
|
||||
if (!fs.existsSync(canvasBackupDir)) {
|
||||
|
@ -83,7 +92,6 @@ export async function incrementialBackupRedis(
|
|||
fs.mkdirSync(canvasTileBackupDir);
|
||||
}
|
||||
|
||||
const canvas = canvases[id];
|
||||
const palette = new Palette(canvas.colors, canvas.alpha);
|
||||
const chunksXY = (canvas.size / TILE_SIZE);
|
||||
console.log('Creating Incremential Backup...');
|
||||
|
|
|
@ -23,7 +23,7 @@ import {
|
|||
createTexture,
|
||||
initializeTiles,
|
||||
} from './Tile';
|
||||
import { mod, getChunkOfPixel, getMaxTiledZoom } from './utils';
|
||||
import { mod, getMaxTiledZoom } from './utils';
|
||||
|
||||
|
||||
// Array that holds cells of all changed base zoomlevel tiles
|
||||
|
@ -116,15 +116,6 @@ class CanvasUpdater {
|
|||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* register changed pixel, queue corespongind tile to reload
|
||||
* @param pixel Pixel that got changed
|
||||
*/
|
||||
registerPixelChange(pixel: Cell) {
|
||||
const chunk = getChunkOfPixel(pixel, this.canvas.size);
|
||||
return this.registerChunkChange(chunk);
|
||||
}
|
||||
|
||||
/*
|
||||
* initialize queues and start loops for updating tiles
|
||||
*/
|
||||
|
@ -163,14 +154,12 @@ class CanvasUpdater {
|
|||
}
|
||||
|
||||
export function registerChunkChange(canvasId: number, chunk: Cell) {
|
||||
return CanvasUpdaters[canvasId].registerChunkChange(chunk);
|
||||
if (CanvasUpdaters[canvasId]) {
|
||||
CanvasUpdaters[canvasId].registerChunkChange(chunk);
|
||||
}
|
||||
}
|
||||
RedisCanvas.setChunkChangeCallback(registerChunkChange);
|
||||
|
||||
export function registerPixelChange(canvasId: number, pixel: Cell) {
|
||||
return CanvasUpdaters[canvasId].registerPixelChange(pixel);
|
||||
}
|
||||
|
||||
/*
|
||||
* starting update loops for canvases
|
||||
*/
|
||||
|
@ -178,7 +167,12 @@ export function startAllCanvasLoops() {
|
|||
if (!fs.existsSync(`${TILE_FOLDER}`)) fs.mkdirSync(`${TILE_FOLDER}`);
|
||||
const ids = Object.keys(canvases);
|
||||
for (let i = 0; i < ids.length; i += 1) {
|
||||
const updater = new CanvasUpdater(parseInt(ids[i], 10));
|
||||
CanvasUpdaters[ids[i]] = updater;
|
||||
const id = parseInt(ids[i], 10);
|
||||
const canvas = canvases[id];
|
||||
if (!canvas.v) {
|
||||
// just 2D canvases
|
||||
const updater = new CanvasUpdater(id);
|
||||
CanvasUpdaters[ids[i]] = updater;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,11 @@
|
|||
import type { Cell } from './Cell';
|
||||
import type { State } from '../reducers';
|
||||
|
||||
import { TILE_SIZE, TILE_ZOOM_LEVEL } from './constants';
|
||||
import {
|
||||
TILE_SIZE,
|
||||
THREE_TILE_SIZE,
|
||||
TILE_ZOOM_LEVEL,
|
||||
} from './constants';
|
||||
|
||||
/**
|
||||
* http://stackoverflow.com/questions/4467539/javascript-modulo-not-behaving
|
||||
|
@ -15,13 +19,6 @@ export function mod(n: number, m: number): number {
|
|||
return ((n % m) + m) % m;
|
||||
}
|
||||
|
||||
export function sum(values: Array<number>): number {
|
||||
let total = 0;
|
||||
// TODO map reduce
|
||||
values.forEach((value) => total += value);
|
||||
return total;
|
||||
}
|
||||
|
||||
/*
|
||||
* returns random integer
|
||||
* @param min Minimum of random integer
|
||||
|
@ -41,13 +38,26 @@ export function clamp(n: number, min: number, max: number): number {
|
|||
return Math.max(min, Math.min(n, max));
|
||||
}
|
||||
|
||||
export function getChunkOfPixel(pixel: Cell, canvasSize: number = null): Cell {
|
||||
const target = pixel.map((x) => Math.floor((x + (canvasSize / 2)) / TILE_SIZE));
|
||||
return target;
|
||||
export function getChunkOfPixel(
|
||||
canvasSize: number = null,
|
||||
x: number,
|
||||
y: number,
|
||||
z: number = null,
|
||||
): Cell {
|
||||
const tileSize = (z === null) ? TILE_SIZE : THREE_TILE_SIZE;
|
||||
const cx = Math.floor((x + (canvasSize / 2)) / tileSize);
|
||||
const cy = Math.floor((y + (canvasSize / 2)) / tileSize);
|
||||
return [cx, cy];
|
||||
}
|
||||
|
||||
export function getTileOfPixel(tileScale: number, pixel: Cell, canvasSize: number = null): Cell {
|
||||
const target = pixel.map((x) => Math.floor((x + canvasSize / 2) / TILE_SIZE * tileScale));
|
||||
export function getTileOfPixel(
|
||||
tileScale: number,
|
||||
pixel: Cell,
|
||||
canvasSize: number = null,
|
||||
): Cell {
|
||||
const target = pixel.map(
|
||||
(x) => Math.floor((x + canvasSize / 2) / TILE_SIZE * tileScale),
|
||||
);
|
||||
return target;
|
||||
}
|
||||
|
||||
|
@ -62,11 +72,19 @@ export function getCanvasBoundaries(canvasSize: number): number {
|
|||
return [canvasMinXY, canvasMaxXY];
|
||||
}
|
||||
|
||||
export function getOffsetOfPixel(x: number, y: number, canvasSize: number = null): number {
|
||||
const modOffset = mod((canvasSize / 2), TILE_SIZE);
|
||||
const cx = mod(x + modOffset, TILE_SIZE);
|
||||
const cy = mod(y + modOffset, TILE_SIZE);
|
||||
return (cy * TILE_SIZE) + cx;
|
||||
export function getOffsetOfPixel(
|
||||
canvasSize: number = null,
|
||||
x: number,
|
||||
y: number,
|
||||
z: number = null,
|
||||
): number {
|
||||
const tileSize = (z === null) ? TILE_SIZE : THREE_TILE_SIZE;
|
||||
let offset = (z === null) ? 0 : (z * tileSize * tileSize);
|
||||
const modOffset = mod((canvasSize / 2), tileSize);
|
||||
const cx = mod(x + modOffset, tileSize);
|
||||
const cy = mod(y + modOffset, tileSize);
|
||||
offset += (cy * tileSize) + cx;
|
||||
return offset;
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -134,29 +152,6 @@ export function worldToScreen(
|
|||
];
|
||||
}
|
||||
|
||||
/*
|
||||
* Get Color Index of specific pixel
|
||||
* @param state State
|
||||
* @param viewport Viewport HTML canvas
|
||||
* @param coordinates Coords of pixel in World coordinates
|
||||
* @return number of color Index
|
||||
*/
|
||||
export function getColorIndexOfPixel(
|
||||
state: State,
|
||||
coordinates: Cell,
|
||||
): number {
|
||||
const { chunks, canvasSize, canvasMaxTiledZoom } = state.canvas;
|
||||
const [cx, cy] = getChunkOfPixel(coordinates, canvasSize);
|
||||
const key = `${canvasMaxTiledZoom}:${cx}:${cy}`;
|
||||
const chunk = chunks.get(key);
|
||||
if (!chunk) {
|
||||
return 0;
|
||||
}
|
||||
return chunk.getColorIndex(
|
||||
getCellInsideChunk(coordinates),
|
||||
);
|
||||
}
|
||||
|
||||
export function durationToString(
|
||||
ms: number,
|
||||
smallest: boolean = false,
|
||||
|
@ -166,6 +161,7 @@ export function durationToString(
|
|||
if (seconds < 60 && smallest) {
|
||||
timestring = seconds;
|
||||
} else {
|
||||
// eslint-disable-next-line max-len
|
||||
timestring = `${Math.floor(seconds / 60)}:${(`0${seconds % 60}`).slice(-2)}`;
|
||||
}
|
||||
return timestring;
|
||||
|
@ -182,8 +178,10 @@ export function numberToString(num: number): string {
|
|||
let postfixNum = 0;
|
||||
while (postfixNum < postfix.length) {
|
||||
if (num < 10000) {
|
||||
// eslint-disable-next-line max-len
|
||||
return `${Math.floor(num / 1000)}.${Math.floor((num % 1000) / 10)}${postfix[postfixNum]}`;
|
||||
} if (num < 100000) {
|
||||
// eslint-disable-next-line max-len
|
||||
return `${Math.floor(num / 1000)}.${Math.floor((num % 1000) / 100)}${postfix[postfixNum]}`;
|
||||
} if (num < 1000000) {
|
||||
return Math.floor(num / 1000) + postfix[postfixNum];
|
||||
|
@ -203,6 +201,7 @@ export function numberToStringFull(num: number): string {
|
|||
return `${Math.floor(num / 1000)}.${(`00${num % 1000}`).slice(-3)}`;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
return `${Math.floor(num / 1000000)}.${(`00${Math.floor(num / 1000)}`).slice(-3)}.${(`00${num % 1000}`).slice(-3)}`;
|
||||
}
|
||||
|
||||
|
|
|
@ -42,14 +42,15 @@ class RedisCanvas {
|
|||
}
|
||||
|
||||
static async setPixel(
|
||||
canvasId: number,
|
||||
color: number,
|
||||
x: number,
|
||||
y: number,
|
||||
color: number,
|
||||
canvasId: number,
|
||||
z: number = null,
|
||||
) {
|
||||
const canvasSize = canvases[canvasId].size;
|
||||
const [i, j] = getChunkOfPixel([x, y], canvasSize);
|
||||
const offset = getOffsetOfPixel(x, y, canvasSize);
|
||||
const [i, j] = getChunkOfPixel(canvasSize, x, y, z);
|
||||
const offset = getOffsetOfPixel(canvasSize, x, y, z);
|
||||
RedisCanvas.setPixelInChunk(i, j, offset, color, canvasId);
|
||||
}
|
||||
|
||||
|
@ -73,16 +74,17 @@ class RedisCanvas {
|
|||
}
|
||||
|
||||
static async getPixelIfExists(
|
||||
canvasId: number,
|
||||
x: number,
|
||||
y: number,
|
||||
canvasId: number,
|
||||
z: number = null,
|
||||
): Promise<number> {
|
||||
// 1st and 2nd bit -> not used yet
|
||||
// 3rd bit -> protected or not
|
||||
// rest (5 bits) -> index of color
|
||||
const canvasSize = canvases[canvasId].size;
|
||||
const [i, j] = getChunkOfPixel([x, y], canvasSize);
|
||||
const offset = getOffsetOfPixel(x, y, canvasSize);
|
||||
const [i, j] = getChunkOfPixel(canvasSize, x, y, z);
|
||||
const offset = getOffsetOfPixel(canvasSize, x, y, z);
|
||||
const args = [
|
||||
`ch:${canvasId}:${i}:${j}`,
|
||||
'GET',
|
||||
|
@ -96,12 +98,13 @@ class RedisCanvas {
|
|||
}
|
||||
|
||||
static async getPixel(
|
||||
canvasId: number,
|
||||
x: number,
|
||||
y: number,
|
||||
canvasId: number,
|
||||
z: number = null,
|
||||
): Promise<number> {
|
||||
const canvasAlpha = canvases[canvasId].alpha;
|
||||
const clr = RedisCanvas.getPixelIfExists(x, y, canvasId);
|
||||
const clr = RedisCanvas.getPixelIfExists(canvasId, x, y, z);
|
||||
return (clr == null) ? canvasAlpha : clr;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,8 +5,6 @@ import type { Cell } from '../core/Cell';
|
|||
import Palette from '../core/Palette';
|
||||
import {
|
||||
getMaxTiledZoom,
|
||||
getChunkOfPixel,
|
||||
getCellInsideChunk,
|
||||
clamp,
|
||||
getIdFromObject,
|
||||
} from '../core/utils';
|
||||
|
@ -19,20 +17,18 @@ import {
|
|||
DEFAULT_CANVASES,
|
||||
TILE_SIZE,
|
||||
} from '../core/constants';
|
||||
import ChunkRGB from '../ui/ChunkRGB';
|
||||
|
||||
export type CanvasState = {
|
||||
canvasId: number,
|
||||
canvasIdent: string,
|
||||
is3D: boolean,
|
||||
canvasSize: number,
|
||||
canvasMaxTiledZoom: number,
|
||||
canvasStartDate: string,
|
||||
palette: Palette,
|
||||
chunks: Map<string, ChunkRGB>,
|
||||
view: Cell,
|
||||
scale: number,
|
||||
viewscale: number,
|
||||
requested: Set<string>,
|
||||
fetchs: number,
|
||||
isHistoricalView: boolean,
|
||||
historicalDate: string,
|
||||
|
@ -67,27 +63,30 @@ function getViewFromURL(canvases: Object) {
|
|||
let colors;
|
||||
let canvasSize;
|
||||
let canvasStartDate;
|
||||
let is3D;
|
||||
if (canvasId == null) {
|
||||
// if canvas informations are not available yet
|
||||
// aka /api/me didn't load yet
|
||||
colors = canvases[DEFAULT_CANVAS_ID].colors;
|
||||
canvasSize = 1024;
|
||||
is3D = false;
|
||||
canvasStartDate = null;
|
||||
} else {
|
||||
const canvas = canvases[canvasId];
|
||||
colors = canvas.colors;
|
||||
canvasSize = canvas.size;
|
||||
is3D = !!canvas.v;
|
||||
canvasStartDate = canvas.sd;
|
||||
}
|
||||
|
||||
const x = parseInt(almost[1], 10);
|
||||
const y = parseInt(almost[2], 10);
|
||||
let urlscale = parseInt(almost[3], 10);
|
||||
if (isNaN(x) || isNaN(y)) {
|
||||
if (Number.isNaN(x) || Number.isNaN(y)) {
|
||||
const thrown = 'NaN';
|
||||
throw thrown;
|
||||
}
|
||||
if (!urlscale || isNaN(urlscale)) {
|
||||
if (!urlscale || Number.isNaN(urlscale)) {
|
||||
urlscale = DEFAULT_SCALE;
|
||||
} else {
|
||||
urlscale = 2 ** (urlscale / 10);
|
||||
|
@ -97,6 +96,7 @@ function getViewFromURL(canvases: Object) {
|
|||
canvasId,
|
||||
canvasIdent,
|
||||
canvasSize,
|
||||
is3D,
|
||||
canvasStartDate,
|
||||
canvasMaxTiledZoom: getMaxTiledZoom(canvasSize),
|
||||
palette: new Palette(colors, 0),
|
||||
|
@ -110,6 +110,7 @@ function getViewFromURL(canvases: Object) {
|
|||
canvasId: DEFAULT_CANVAS_ID,
|
||||
canvasIdent: canvases[DEFAULT_CANVAS_ID].ident,
|
||||
canvasSize: canvases[DEFAULT_CANVAS_ID].size,
|
||||
is3D: !!canvases[DEFAULT_CANVAS_ID].v,
|
||||
canvasStartDate: null,
|
||||
canvasMaxTiledZoom: getMaxTiledZoom(canvases[DEFAULT_CANVAS_ID].size),
|
||||
palette: new Palette(canvases[DEFAULT_CANVAS_ID].colors, 0),
|
||||
|
@ -121,9 +122,7 @@ function getViewFromURL(canvases: Object) {
|
|||
}
|
||||
|
||||
const initialState: CanvasState = {
|
||||
chunks: new Map(),
|
||||
...getViewFromURL(DEFAULT_CANVASES),
|
||||
requested: new Set(),
|
||||
fetchs: 0,
|
||||
isHistoricalView: false,
|
||||
historicalDate: null,
|
||||
|
@ -131,36 +130,11 @@ const initialState: CanvasState = {
|
|||
};
|
||||
|
||||
|
||||
export default function gui(
|
||||
export default function canvasReducer(
|
||||
state: CanvasState = initialState,
|
||||
action: Action,
|
||||
): CanvasState {
|
||||
switch (action.type) {
|
||||
case 'PLACE_PIXEL': {
|
||||
const {
|
||||
chunks, canvasMaxTiledZoom, palette, canvasSize,
|
||||
} = state;
|
||||
const { coordinates, color } = action;
|
||||
|
||||
const [cx, cy] = getChunkOfPixel(coordinates, canvasSize);
|
||||
const key = ChunkRGB.getKey(canvasMaxTiledZoom, cx, cy);
|
||||
let chunk = chunks.get(key);
|
||||
if (!chunk) {
|
||||
chunk = new ChunkRGB(palette, [canvasMaxTiledZoom, cx, cy]);
|
||||
chunks.set(chunk.key, chunk);
|
||||
}
|
||||
|
||||
// redis prediction
|
||||
chunk.setColor(
|
||||
getCellInsideChunk(coordinates),
|
||||
color,
|
||||
);
|
||||
return {
|
||||
...state,
|
||||
chunks,
|
||||
};
|
||||
}
|
||||
|
||||
case 'SET_SCALE': {
|
||||
let {
|
||||
view,
|
||||
|
@ -221,7 +195,7 @@ export default function gui(
|
|||
...state,
|
||||
scale: (scale < 1.0) ? 1.0 : scale,
|
||||
viewscale: (viewscale < 1.0) ? 1.0 : viewscale,
|
||||
isHistoricalView: !state.isHistoricalView,
|
||||
isHistoricalView: !state.is3D && !state.isHistoricalView,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -238,85 +212,36 @@ export default function gui(
|
|||
}
|
||||
|
||||
case 'RELOAD_URL': {
|
||||
const { canvasId, chunks, canvases } = state;
|
||||
const { canvases } = state;
|
||||
const nextstate = getViewFromURL(canvases);
|
||||
if (nextstate.canvasId !== canvasId) {
|
||||
chunks.clear();
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
...nextstate,
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* set url coordinates
|
||||
*/
|
||||
case 'ON_VIEW_FINISH_CHANGE': {
|
||||
const { view, viewscale, canvasIdent } = state;
|
||||
let [x, y] = view;
|
||||
x = Math.round(x);
|
||||
y = Math.round(y);
|
||||
const scale = Math.round(Math.log2(viewscale) * 10);
|
||||
const newhash = `#${canvasIdent},${x},${y},${scale}`;
|
||||
window.history.replaceState(undefined, undefined, newhash);
|
||||
return {
|
||||
...state,
|
||||
};
|
||||
}
|
||||
|
||||
case 'REQUEST_BIG_CHUNK': {
|
||||
const {
|
||||
palette, chunks, fetchs, requested,
|
||||
fetchs,
|
||||
} = state;
|
||||
const { center } = action;
|
||||
|
||||
const chunkRGB = new ChunkRGB(palette, center);
|
||||
// chunkRGB.preLoad(chunks);
|
||||
const { key } = chunkRGB;
|
||||
chunks.set(key, chunkRGB);
|
||||
|
||||
requested.add(key);
|
||||
return {
|
||||
...state,
|
||||
chunks,
|
||||
fetchs: fetchs + 1,
|
||||
requested,
|
||||
};
|
||||
}
|
||||
|
||||
case 'RECEIVE_BIG_CHUNK': {
|
||||
const { chunks, fetchs } = state;
|
||||
const { center, arrayBuffer } = action;
|
||||
|
||||
const key = ChunkRGB.getKey(...center);
|
||||
const chunk = chunks.get(key);
|
||||
if (!chunk) return state;
|
||||
|
||||
chunk.isBasechunk = true;
|
||||
if (arrayBuffer.byteLength) {
|
||||
const chunkArray = new Uint8Array(arrayBuffer);
|
||||
chunk.fromBuffer(chunkArray);
|
||||
} else {
|
||||
chunk.empty();
|
||||
}
|
||||
const { fetchs } = state;
|
||||
|
||||
return {
|
||||
...state,
|
||||
chunks,
|
||||
fetchs: fetchs + 1,
|
||||
};
|
||||
}
|
||||
|
||||
case 'RECEIVE_BIG_CHUNK_FAILURE': {
|
||||
const { chunks, fetchs } = state;
|
||||
const { center } = action;
|
||||
|
||||
const key = ChunkRGB.getKey(...center);
|
||||
const chunk = chunks.get(key);
|
||||
if (!chunk) return state;
|
||||
|
||||
chunk.empty();
|
||||
const { fetchs } = state;
|
||||
|
||||
return {
|
||||
...state,
|
||||
|
@ -324,52 +249,10 @@ export default function gui(
|
|||
};
|
||||
}
|
||||
|
||||
case 'RECEIVE_IMAGE_TILE': {
|
||||
const { chunks, fetchs } = state;
|
||||
const { center, tile } = action;
|
||||
|
||||
const key = ChunkRGB.getKey(...center);
|
||||
const chunk = chunks.get(key);
|
||||
if (!chunk) return state;
|
||||
|
||||
chunk.fromImage(tile);
|
||||
|
||||
return {
|
||||
...state,
|
||||
chunks,
|
||||
fetchs: fetchs + 1,
|
||||
};
|
||||
}
|
||||
|
||||
case 'RECEIVE_PIXEL_UPDATE': {
|
||||
const { chunks, canvasMaxTiledZoom } = state;
|
||||
// i, j: Coordinates of chunk
|
||||
// offset: Offset of pixel within said chunk
|
||||
const {
|
||||
i, j, offset, color,
|
||||
} = action;
|
||||
|
||||
const key = ChunkRGB.getKey(canvasMaxTiledZoom, i, j);
|
||||
const chunk = chunks.get(key);
|
||||
|
||||
// ignore because is not seen
|
||||
if (!chunk) return state;
|
||||
|
||||
const ix = offset % TILE_SIZE;
|
||||
const iy = Math.floor(offset / TILE_SIZE);
|
||||
chunk.setColor([ix, iy], color);
|
||||
|
||||
return {
|
||||
...state,
|
||||
chunks,
|
||||
};
|
||||
}
|
||||
|
||||
case 'SELECT_CANVAS': {
|
||||
let { canvasId } = action;
|
||||
const { canvases, chunks } = state;
|
||||
const { canvases, isHistoricalView } = state;
|
||||
|
||||
chunks.clear();
|
||||
let canvas = canvases[canvasId];
|
||||
if (!canvas) {
|
||||
canvasId = DEFAULT_CANVAS_ID;
|
||||
|
@ -379,23 +262,25 @@ export default function gui(
|
|||
size: canvasSize,
|
||||
sd: canvasStartDate,
|
||||
ident: canvasIdent,
|
||||
v: is3D,
|
||||
colors,
|
||||
} = canvas;
|
||||
const canvasMaxTiledZoom = getMaxTiledZoom(canvasSize);
|
||||
const palette = new Palette(colors, 0);
|
||||
const view = (canvasId === 0) ? getGivenCoords() : [0, 0];
|
||||
chunks.clear();
|
||||
return {
|
||||
...state,
|
||||
canvasId,
|
||||
canvasIdent,
|
||||
canvasSize,
|
||||
is3D,
|
||||
canvasStartDate,
|
||||
canvasMaxTiledZoom,
|
||||
palette,
|
||||
view,
|
||||
viewscale: DEFAULT_SCALE,
|
||||
scale: DEFAULT_SCALE,
|
||||
isHistoricalView: !is3D && isHistoricalView,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -411,6 +296,7 @@ export default function gui(
|
|||
const {
|
||||
size: canvasSize,
|
||||
sd: canvasStartDate,
|
||||
v: is3D,
|
||||
colors,
|
||||
} = canvases[canvasId];
|
||||
const canvasMaxTiledZoom = getMaxTiledZoom(canvasSize);
|
||||
|
@ -421,6 +307,7 @@ export default function gui(
|
|||
canvasId,
|
||||
canvasIdent,
|
||||
canvasSize,
|
||||
is3D,
|
||||
canvasStartDate,
|
||||
canvasMaxTiledZoom,
|
||||
palette,
|
||||
|
|
|
@ -37,6 +37,7 @@ export default function modal(
|
|||
};
|
||||
}
|
||||
|
||||
case 'SELECT_CANVAS':
|
||||
case 'HIDE_MODAL':
|
||||
return {
|
||||
...state,
|
||||
|
|
|
@ -157,6 +157,11 @@ router.post('/', upload.single('image'), async (req, res, next) => {
|
|||
|
||||
const canvas = canvases[canvasId];
|
||||
|
||||
if (canvas.v) {
|
||||
res.status(403).send('Can not upload Image to 3D canvas');
|
||||
return;
|
||||
}
|
||||
|
||||
const canvasMaxXY = canvas.size / 2;
|
||||
const canvasMinXY = -canvasMaxXY;
|
||||
if (x < canvasMinXY || y < canvasMinXY
|
||||
|
|
|
@ -6,14 +6,19 @@
|
|||
import type { Request, Response } from 'express';
|
||||
|
||||
import draw from '../../core/draw';
|
||||
import { blacklistDetector, cheapDetector, strongDetector } from '../../core/isProxy';
|
||||
import {
|
||||
blacklistDetector,
|
||||
cheapDetector,
|
||||
strongDetector,
|
||||
} from '../../core/isProxy';
|
||||
import verifyCaptcha from '../../utils/recaptcha';
|
||||
import logger from '../../core/logger';
|
||||
import redis from '../../data/redis';
|
||||
import { USE_PROXYCHECK, RECAPTCHA_SECRET, RECAPTCHA_TIME } from '../../core/config';
|
||||
import {
|
||||
User,
|
||||
} from '../../data/models';
|
||||
USE_PROXYCHECK,
|
||||
RECAPTCHA_SECRET,
|
||||
RECAPTCHA_TIME,
|
||||
} from '../../core/config';
|
||||
|
||||
|
||||
async function validate(req: Request, res: Response, next) {
|
||||
|
@ -21,6 +26,10 @@ async function validate(req: Request, res: Response, next) {
|
|||
const cn = parseInt(req.body.cn, 10);
|
||||
const x = parseInt(req.body.x, 10);
|
||||
const y = parseInt(req.body.y, 10);
|
||||
let z = null;
|
||||
if (req.body.z) {
|
||||
z = parseInt(req.body.z, 10);
|
||||
}
|
||||
const clr = parseInt(req.body.clr, 10);
|
||||
|
||||
if (Number.isNaN(cn)) {
|
||||
|
@ -33,6 +42,8 @@ async function validate(req: Request, res: Response, next) {
|
|||
error = 'No color selected';
|
||||
} else if (clr < 2 || clr > 31) {
|
||||
error = 'Invalid color selected';
|
||||
} else if (z !== null && Number.isNaN(z)) {
|
||||
error = 'z is not a valid number';
|
||||
}
|
||||
if (error !== null) {
|
||||
res.status(400).json({ errors: [error] });
|
||||
|
@ -42,6 +53,7 @@ async function validate(req: Request, res: Response, next) {
|
|||
req.body.cn = cn;
|
||||
req.body.x = x;
|
||||
req.body.y = y;
|
||||
req.body.z = z;
|
||||
req.body.clr = clr;
|
||||
|
||||
|
||||
|
@ -111,7 +123,7 @@ async function checkHuman(req: Request, res: Response, next) {
|
|||
// strongly check selective areas
|
||||
async function checkProxy(req: Request, res: Response, next) {
|
||||
const { trueIp: ip } = req;
|
||||
if (USE_PROXYCHECK && ip != '0.0.0.1') {
|
||||
if (USE_PROXYCHECK && ip !== '0.0.0.1') {
|
||||
/*
|
||||
//one area uses stronger detector
|
||||
const { x, y } = req.body;
|
||||
|
@ -146,6 +158,7 @@ async function checkProxy(req: Request, res: Response, next) {
|
|||
|
||||
// strongly check just specific areas for proxies
|
||||
// do not proxycheck the rest
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
async function checkProxySelective(req: Request, res: Response, next) {
|
||||
const { trueIp: ip } = req;
|
||||
if (USE_PROXYCHECK) {
|
||||
|
@ -177,18 +190,16 @@ async function place(req: Request, res: Response) {
|
|||
});
|
||||
|
||||
const {
|
||||
cn, x, y, clr,
|
||||
cn, x, y, z, clr,
|
||||
} = req.body;
|
||||
const { user, headers, trueIp } = req;
|
||||
const { ip } = user;
|
||||
const { user, trueIp } = req;
|
||||
|
||||
const isHashed = parseInt(req.body.a, 10) === (x + y + 8);
|
||||
|
||||
logger.info(`${trueIp} / ${user.id} wants to place ${clr} in (${x}, ${y})`);
|
||||
// eslint-disable-next-line max-len
|
||||
logger.info(`${trueIp} / ${user.id} wants to place ${clr} in (${x}, ${y}, ${z}) on canvas ${cn}`);
|
||||
|
||||
const {
|
||||
errorTitle, error, success, waitSeconds, coolDownSeconds,
|
||||
} = await draw(user, cn, x, y, clr);
|
||||
} = await draw(user, cn, clr, x, y, z);
|
||||
logger.log('debug', success);
|
||||
|
||||
if (success) {
|
||||
|
|
|
@ -7,10 +7,11 @@
|
|||
|
||||
import express from 'express';
|
||||
import type { Request, Response } from 'express';
|
||||
import sharp from 'sharp';
|
||||
import { TILE_SIZE, HOUR } from '../core/constants';
|
||||
import { TILE_FOLDER } from '../core/config';
|
||||
import RedisCanvas from '../data/models/RedisCanvas';
|
||||
import { HOUR } from '../core/constants';
|
||||
// import sharp from 'sharp';
|
||||
// import { TILE_SIZE, HOUR } from '../core/constants';
|
||||
// import RedisCanvas from '../data/models/RedisCanvas';
|
||||
|
||||
|
||||
const router = express.Router();
|
||||
|
@ -72,16 +73,17 @@ router.use('/', express.static(TILE_FOLDER, {
|
|||
/*
|
||||
* catch File Not Found: Send empty tile
|
||||
*/
|
||||
router.use('/:c([0-9]+)/:z([0-9]+)/:x([0-9]+)/:y([0-9]+).png', async (req: Request, res: Response) => {
|
||||
const { c: paramC } = req.params;
|
||||
const c = parseInt(paramC, 10);
|
||||
res.set({
|
||||
'Cache-Control': `public, s-maxage=${2 * 60 * 60}, max-age=${1 * 60 * 60}`, // seconds
|
||||
'Content-Type': 'image/png',
|
||||
router.use('/:c([0-9]+)/:z([0-9]+)/:x([0-9]+)/:y([0-9]+).png',
|
||||
async (req: Request, res: Response) => {
|
||||
const { c: paramC } = req.params;
|
||||
const c = parseInt(paramC, 10);
|
||||
res.set({
|
||||
'Cache-Control': `public, s-maxage=${2 * 3600}, max-age=${1 * 3600}`,
|
||||
'Content-Type': 'image/png',
|
||||
});
|
||||
res.status(200);
|
||||
res.sendFile(`${TILE_FOLDER}/${c}/emptytile.png`);
|
||||
});
|
||||
res.status(200);
|
||||
res.sendFile(`${TILE_FOLDER}/${c}/emptytile.png`);
|
||||
});
|
||||
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -28,7 +28,8 @@ async function verifyClient(info, done) {
|
|||
const { headers } = req;
|
||||
const ip = await getIPFromRequest(req);
|
||||
|
||||
if (!headers.authorization || headers.authorization != `Bearer ${APISOCKET_KEY}`) {
|
||||
if (!headers.authorization
|
||||
|| headers.authorization != `Bearer ${APISOCKET_KEY}`) {
|
||||
logger.warn(`API ws request from ${ip} authenticated`);
|
||||
return done(false);
|
||||
}
|
||||
|
@ -83,7 +84,9 @@ class APISocketServer extends WebSocketEvents {
|
|||
|
||||
const sendmsg = JSON.stringify(['msg', name, msg]);
|
||||
this.wss.clients.forEach((client) => {
|
||||
if (client !== ws && client.subChat && client.readyState === WebSocket.OPEN) {
|
||||
if (client !== ws
|
||||
&& client.subChat
|
||||
&& client.readyState === WebSocket.OPEN) {
|
||||
client.send(sendmsg);
|
||||
}
|
||||
});
|
||||
|
@ -117,9 +120,9 @@ class APISocketServer extends WebSocketEvents {
|
|||
});
|
||||
this.wss.clients.forEach((client) => {
|
||||
if (client.subOnline && client.readyState === WebSocket.OPEN) {
|
||||
frame.forEach((buffer) => {
|
||||
frame.forEach((data) => {
|
||||
try {
|
||||
client._socket.write(buffer);
|
||||
client._socket.write(data);
|
||||
} catch (error) {
|
||||
logger.error('(!) Catched error on write apisocket:', error);
|
||||
}
|
||||
|
@ -139,9 +142,9 @@ class APISocketServer extends WebSocketEvents {
|
|||
});
|
||||
this.wss.clients.forEach((client) => {
|
||||
if (client.subPxl && client.readyState === WebSocket.OPEN) {
|
||||
frame.forEach((buffer) => {
|
||||
frame.forEach((data) => {
|
||||
try {
|
||||
client._socket.write(buffer);
|
||||
client._socket.write(data);
|
||||
} catch (error) {
|
||||
logger.error('(!) Catched error on write apisocket:', error);
|
||||
}
|
||||
|
@ -177,7 +180,7 @@ class APISocketServer extends WebSocketEvents {
|
|||
if (clr < 0 || clr > 32) return;
|
||||
// be aware that user null has no cd
|
||||
if (!minecraftid && !ip) {
|
||||
setPixel(0, x, y, clr);
|
||||
setPixel(0, clr, x, y);
|
||||
ws.send(JSON.stringify(['retpxl', null, null, true, 0, 0]));
|
||||
return;
|
||||
}
|
||||
|
@ -185,7 +188,7 @@ class APISocketServer extends WebSocketEvents {
|
|||
user.ip = ip;
|
||||
const {
|
||||
error, success, waitSeconds, coolDownSeconds,
|
||||
} = await drawUnsafe(user, 0, x, y, clr);
|
||||
} = await drawUnsafe(user, 0, clr, x, y, null);
|
||||
ws.send(JSON.stringify([
|
||||
'retpxl',
|
||||
(minecraftid) || ip,
|
||||
|
@ -229,7 +232,9 @@ class APISocketServer extends WebSocketEvents {
|
|||
if (command == 'mcchat') {
|
||||
const [minecraftname, msg] = packet;
|
||||
const user = this.mc.minecraftname2User(minecraftname);
|
||||
const chatname = (user.id) ? `[MC] ${user.regUser.name}` : `[MC] ${minecraftname}`;
|
||||
const chatname = (user.id)
|
||||
? `[MC] ${user.regUser.name}`
|
||||
: `[MC] ${minecraftname}`;
|
||||
webSockets.broadcastChatMessage(chatname, msg, false);
|
||||
this.broadcastChatMessage(chatname, msg, true, ws);
|
||||
return;
|
||||
|
@ -265,6 +270,7 @@ class APISocketServer extends WebSocketEvents {
|
|||
|
||||
ws.isAlive = false;
|
||||
ws.ping(() => {});
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,8 @@ export default {
|
|||
// CLIENT
|
||||
const i = data.getInt16(1);
|
||||
const j = data.getInt16(3);
|
||||
// const offset = (data.getUint8(5) << 16) | data.getUint16(6);
|
||||
// const color = data.getUint8(8);
|
||||
const offset = data.getUint16(5);
|
||||
const color = data.getUint8(7);
|
||||
return {
|
||||
|
@ -26,11 +28,14 @@ export default {
|
|||
dehydrate(i, j, offset, color): Buffer {
|
||||
// SERVER
|
||||
if (!process.env.BROWSER) {
|
||||
const buffer = Buffer.allocUnsafe(1 + 2 + 2 + 2 + 1);
|
||||
const buffer = Buffer.allocUnsafe(1 + 2 + 2 + 1 + 2 + 1);
|
||||
buffer.writeUInt8(OP_CODE, 0);
|
||||
|
||||
buffer.writeInt16BE(i, 1);
|
||||
buffer.writeInt16BE(j, 3);
|
||||
// buffer.writeUInt8(offset >>> 16, 5);
|
||||
// buffer.writeUInt16BE(offset & 0x00FFFF, 6);
|
||||
// buffer.writeUInt8(color, 8);
|
||||
buffer.writeUInt16BE(offset, 5);
|
||||
buffer.writeUInt8(color, 7);
|
||||
|
||||
|
|
|
@ -27,7 +27,10 @@ export default (store) => (next) => (action) => {
|
|||
|
||||
|
||||
gainNode.gain.setValueAtTime(0.3, context.currentTime);
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.2, context.currentTime + 0.1);
|
||||
gainNode.gain.exponentialRampToValueAtTime(
|
||||
0.2,
|
||||
context.currentTime + 0.1,
|
||||
);
|
||||
|
||||
oscillatorNode.connect(gainNode);
|
||||
gainNode.connect(context.destination);
|
||||
|
@ -46,11 +49,17 @@ export default (store) => (next) => (action) => {
|
|||
// oscillatorNode.detune.value = -600
|
||||
|
||||
oscillatorNode.frequency.setValueAtTime(1479.98, context.currentTime);
|
||||
oscillatorNode.frequency.exponentialRampToValueAtTime(493.88, context.currentTime + 0.01);
|
||||
oscillatorNode.frequency.exponentialRampToValueAtTime(
|
||||
493.88,
|
||||
context.currentTime + 0.01,
|
||||
);
|
||||
|
||||
|
||||
gainNode.gain.setValueAtTime(0.5, context.currentTime);
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.2, context.currentTime + 0.1);
|
||||
gainNode.gain.exponentialRampToValueAtTime(
|
||||
0.2,
|
||||
context.currentTime + 0.1,
|
||||
);
|
||||
|
||||
oscillatorNode.connect(gainNode);
|
||||
gainNode.connect(context.destination);
|
||||
|
@ -68,9 +77,18 @@ export default (store) => (next) => (action) => {
|
|||
oscillatorNode.type = 'sine';
|
||||
oscillatorNode.detune.value = -900;
|
||||
oscillatorNode.frequency.setValueAtTime(600, context.currentTime);
|
||||
oscillatorNode.frequency.setValueAtTime(1400, context.currentTime + 0.025);
|
||||
oscillatorNode.frequency.setValueAtTime(1200, context.currentTime + 0.05);
|
||||
oscillatorNode.frequency.setValueAtTime(900, context.currentTime + 0.075);
|
||||
oscillatorNode.frequency.setValueAtTime(
|
||||
1400,
|
||||
context.currentTime + 0.025,
|
||||
);
|
||||
oscillatorNode.frequency.setValueAtTime(
|
||||
1200,
|
||||
context.currentTime + 0.05,
|
||||
);
|
||||
oscillatorNode.frequency.setValueAtTime(
|
||||
900,
|
||||
context.currentTime + 0.075,
|
||||
);
|
||||
|
||||
const lfo = context.createOscillator();
|
||||
lfo.type = 'sine';
|
||||
|
@ -94,10 +112,16 @@ export default (store) => (next) => (action) => {
|
|||
|
||||
oscillatorNode.type = 'sine';
|
||||
oscillatorNode.frequency.setValueAtTime(clrFreq, context.currentTime);
|
||||
oscillatorNode.frequency.exponentialRampToValueAtTime(1400, context.currentTime + 0.2);
|
||||
oscillatorNode.frequency.exponentialRampToValueAtTime(
|
||||
1400,
|
||||
context.currentTime + 0.2,
|
||||
);
|
||||
|
||||
gainNode.gain.setValueAtTime(0.5, context.currentTime);
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.2, context.currentTime + 0.1);
|
||||
gainNode.gain.exponentialRampToValueAtTime(
|
||||
0.2,
|
||||
context.currentTime + 0.1,
|
||||
);
|
||||
|
||||
oscillatorNode.connect(gainNode);
|
||||
gainNode.connect(context.destination);
|
||||
|
@ -114,11 +138,20 @@ export default (store) => (next) => (action) => {
|
|||
|
||||
oscillatorNode.type = 'sine';
|
||||
oscillatorNode.frequency.setValueAtTime(349.23, context.currentTime);
|
||||
oscillatorNode.frequency.setValueAtTime(523.25, context.currentTime + 0.1);
|
||||
oscillatorNode.frequency.setValueAtTime(698.46, context.currentTime + 0.2);
|
||||
oscillatorNode.frequency.setValueAtTime(
|
||||
523.25,
|
||||
context.currentTime + 0.1,
|
||||
);
|
||||
oscillatorNode.frequency.setValueAtTime(
|
||||
698.46,
|
||||
context.currentTime + 0.2,
|
||||
);
|
||||
|
||||
gainNode.gain.setValueAtTime(0.5, context.currentTime);
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.2, context.currentTime + 0.15);
|
||||
gainNode.gain.exponentialRampToValueAtTime(
|
||||
0.2,
|
||||
context.currentTime + 0.15,
|
||||
);
|
||||
|
||||
oscillatorNode.connect(gainNode);
|
||||
gainNode.connect(context.destination);
|
||||
|
@ -135,10 +168,16 @@ export default (store) => (next) => (action) => {
|
|||
|
||||
oscillatorNode.type = 'sine';
|
||||
oscillatorNode.frequency.setValueAtTime(310, context.currentTime);
|
||||
oscillatorNode.frequency.exponentialRampToValueAtTime(355, context.currentTime + 0.025);
|
||||
oscillatorNode.frequency.exponentialRampToValueAtTime(
|
||||
355,
|
||||
context.currentTime + 0.025,
|
||||
);
|
||||
|
||||
gainNode.gain.setValueAtTime(0.1, context.currentTime);
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.1, context.currentTime + 0.1);
|
||||
gainNode.gain.exponentialRampToValueAtTime(
|
||||
0.1,
|
||||
context.currentTime + 0.1,
|
||||
);
|
||||
|
||||
oscillatorNode.connect(gainNode);
|
||||
gainNode.connect(context.destination);
|
||||
|
|
|
@ -35,6 +35,7 @@ export default () => (next) => (action) => {
|
|||
// nothing
|
||||
}
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
}
|
||||
return next(action);
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
*/
|
||||
|
||||
function warn(error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(error.message || error);
|
||||
throw error; // To let the caller handle the rejection
|
||||
}
|
||||
|
|
|
@ -4,13 +4,17 @@
|
|||
* @flow
|
||||
*/
|
||||
|
||||
import renderer from '../ui/Renderer';
|
||||
import {
|
||||
getRenderer,
|
||||
initRenderer,
|
||||
} from '../ui/renderer';
|
||||
|
||||
export default (store) => (next) => (action) => {
|
||||
const { type } = action;
|
||||
|
||||
if (type == 'SET_HISTORICAL_TIME') {
|
||||
if (type === 'SET_HISTORICAL_TIME') {
|
||||
const state = store.getState();
|
||||
const renderer = getRenderer();
|
||||
renderer.updateOldHistoricalTime(state.canvas.historicalTime);
|
||||
}
|
||||
|
||||
|
@ -23,15 +27,21 @@ export default (store) => (next) => (action) => {
|
|||
case 'RELOAD_URL':
|
||||
case 'SELECT_CANVAS':
|
||||
case 'RECEIVE_ME': {
|
||||
renderer.updateCanvasData(state);
|
||||
const renderer = getRenderer();
|
||||
const { is3D } = state.canvas;
|
||||
if (is3D === renderer.is3D) {
|
||||
renderer.updateCanvasData(state);
|
||||
} else {
|
||||
initRenderer(store, is3D);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'SET_HISTORICAL_TIME':
|
||||
case 'REQUEST_BIG_CHUNK':
|
||||
case 'RECEIVE_BIG_CHUNK':
|
||||
case 'RECEIVE_BIG_CHUNK_FAILURE':
|
||||
case 'RECEIVE_IMAGE_TILE': {
|
||||
case 'RECEIVE_BIG_CHUNK_FAILURE': {
|
||||
const renderer = getRenderer();
|
||||
renderer.forceNextRender = true;
|
||||
break;
|
||||
}
|
||||
|
@ -44,12 +54,26 @@ export default (store) => (next) => (action) => {
|
|||
view,
|
||||
canvasSize,
|
||||
} = state.canvas;
|
||||
const renderer = getRenderer();
|
||||
renderer.updateScale(viewscale, canvasMaxTiledZoom, view, canvasSize);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'RECEIVE_PIXEL_UPDATE': {
|
||||
const {
|
||||
i,
|
||||
j,
|
||||
offset,
|
||||
color,
|
||||
} = action;
|
||||
const renderer = getRenderer();
|
||||
renderer.renderPixel(i, j, offset, color);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'SET_VIEW_COORDINATES': {
|
||||
const { view, canvasSize } = state.canvas;
|
||||
const renderer = getRenderer();
|
||||
renderer.updateView(view, canvasSize);
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -13,6 +13,8 @@ const TITLE = 'PixelPlanet.fun';
|
|||
let lastTitle = null;
|
||||
|
||||
export default (store) => (next) => (action) => {
|
||||
const ret = next(action);
|
||||
|
||||
switch (action.type) {
|
||||
case 'COOLDOWN_SET': {
|
||||
const { coolDown } = store.getState().user;
|
||||
|
@ -28,9 +30,26 @@ export default (store) => (next) => (action) => {
|
|||
break;
|
||||
}
|
||||
|
||||
|
||||
case 'SELECT_CANVAS':
|
||||
case 'ON_VIEW_FINISH_CHANGE': {
|
||||
const {
|
||||
view,
|
||||
viewscale,
|
||||
canvasIdent,
|
||||
} = store.getState().canvas;
|
||||
let [x, y] = view;
|
||||
x = Math.round(x);
|
||||
y = Math.round(y);
|
||||
const scale = Math.round(Math.log2(viewscale) * 10);
|
||||
const newhash = `#${canvasIdent},${x},${y},${scale}`;
|
||||
window.history.replaceState(undefined, undefined, newhash);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
// nothing
|
||||
}
|
||||
|
||||
return next(action);
|
||||
return ret;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,201 @@
|
|||
/*
|
||||
* Fetching and storing of 2D chunks
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import ChunkRGB from './ChunkRGB';
|
||||
import { TILE_SIZE } from '../core/constants';
|
||||
import {
|
||||
loadingTiles,
|
||||
loadImage,
|
||||
} from './loadImage';
|
||||
import {
|
||||
requestBigChunk,
|
||||
receiveBigChunk,
|
||||
receiveBigChunkFailure,
|
||||
} from '../actions';
|
||||
import {
|
||||
getCellInsideChunk,
|
||||
getChunkOfPixel,
|
||||
} from '../core/utils';
|
||||
|
||||
class ChunkLoader {
|
||||
store = null;
|
||||
canvasId: number;
|
||||
canvasMaxTiledZoom: number;
|
||||
palette;
|
||||
chunks: Map<string, ChunkRGB>;
|
||||
|
||||
constructor(store) {
|
||||
this.store = store;
|
||||
const state = store.getState();
|
||||
const {
|
||||
canvasId,
|
||||
canvasMaxTiledZoom,
|
||||
palette,
|
||||
} = state.canvas;
|
||||
this.canvasId = canvasId;
|
||||
this.canvasMaxTiledZoom = canvasMaxTiledZoom;
|
||||
this.palette = palette;
|
||||
this.chunks = new Map();
|
||||
}
|
||||
|
||||
getAllChunks() {
|
||||
return this.chunks;
|
||||
}
|
||||
|
||||
getPixelUpdate(
|
||||
cx: number,
|
||||
cy: number,
|
||||
offset: number,
|
||||
color: number,
|
||||
) {
|
||||
const chunk = this.chunks.get(`${this.canvasMaxTiledZoom}:${cx}:${cy}`);
|
||||
if (chunk) {
|
||||
const ix = offset % TILE_SIZE;
|
||||
const iy = Math.floor(offset / TILE_SIZE);
|
||||
chunk.setColor([ix, iy], color);
|
||||
}
|
||||
}
|
||||
|
||||
getColorIndexOfPixel(
|
||||
x: number,
|
||||
y: number,
|
||||
) {
|
||||
const state: State = this.store.getState();
|
||||
const { canvasSize } = state.canvas;
|
||||
const [cx, cy] = getChunkOfPixel(canvasSize, x, y);
|
||||
const key = `${this.canvasMaxTiledZoom}:${cx}:${cy}`;
|
||||
const chunk = this.chunks.get(key);
|
||||
if (!chunk) {
|
||||
return 0;
|
||||
}
|
||||
return chunk.getColorIndex(
|
||||
getCellInsideChunk([x, y]),
|
||||
);
|
||||
}
|
||||
|
||||
getChunk(zoom, cx: number, cy: number, fetch: boolean) {
|
||||
const chunkKey = `${zoom}:${cx}:${cy}`;
|
||||
const chunk = this.chunks.get(chunkKey);
|
||||
const { canvasId } = this;
|
||||
if (chunk) {
|
||||
if (chunk.ready) {
|
||||
return chunk.image;
|
||||
}
|
||||
return loadingTiles.getTile(canvasId);
|
||||
} if (fetch) {
|
||||
// fetch chunk
|
||||
const chunkRGB = new ChunkRGB(this.palette, chunkKey);
|
||||
this.chunks.set(chunkKey, chunkRGB);
|
||||
|
||||
if (this.canvasMaxTiledZoom === zoom) {
|
||||
this.fetchBaseChunk(zoom, cx, cy, chunkRGB);
|
||||
} else {
|
||||
this.fetchTile(zoom, cx, cy, chunkRGB);
|
||||
}
|
||||
}
|
||||
return loadingTiles.getTile(canvasId);
|
||||
}
|
||||
|
||||
getHistoricalChunk(cx, cy, fetch, historicalDate, historicalTime = null) {
|
||||
let chunkKey = (historicalTime)
|
||||
? `${historicalDate}${historicalTime}`
|
||||
: historicalDate;
|
||||
chunkKey += `:${cx}:${cy}`;
|
||||
const chunk = this.chunks.get(chunkKey);
|
||||
const { canvasId } = this;
|
||||
if (chunk) {
|
||||
if (chunk.ready) {
|
||||
return chunk.image;
|
||||
}
|
||||
return (historicalTime) ? null : loadingTiles.getTile(canvasId);
|
||||
} if (fetch) {
|
||||
// fetch tile
|
||||
const chunkRGB = new ChunkRGB(this.palette, chunkKey);
|
||||
this.chunks.set(chunkKey, chunkRGB);
|
||||
this.fetchHistoricalChunk(
|
||||
cx,
|
||||
cy,
|
||||
historicalDate,
|
||||
historicalTime,
|
||||
chunkRGB,
|
||||
);
|
||||
}
|
||||
return (historicalTime) ? null : loadingTiles.getTile(canvasId);
|
||||
}
|
||||
|
||||
async fetchHistoricalChunk(
|
||||
cx: number,
|
||||
cy: number,
|
||||
historicalDate: string,
|
||||
historicalTime: string,
|
||||
chunkRGB,
|
||||
) {
|
||||
const { canvasId } = this;
|
||||
const { key } = chunkRGB;
|
||||
let url = `${window.backupurl}/${historicalDate}/`;
|
||||
if (historicalTime) {
|
||||
// incremential tiles
|
||||
url += `${canvasId}/${historicalTime}/${cx}/${cy}.png`;
|
||||
} else {
|
||||
// full tiles
|
||||
url += `${canvasId}/tiles/${cx}/${cy}.png`;
|
||||
}
|
||||
this.store.dispatch(requestBigChunk(key));
|
||||
try {
|
||||
const img = await loadImage(url);
|
||||
chunkRGB.fromImage(img);
|
||||
this.store.dispatch(receiveBigChunk(key));
|
||||
} catch (error) {
|
||||
this.store.dispatch(receiveBigChunkFailure(key, error));
|
||||
if (historicalTime) {
|
||||
chunkRGB.empty(true);
|
||||
} else {
|
||||
chunkRGB.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fetchBaseChunk(zoom, cx: number, cy: number, chunkRGB) {
|
||||
const center = [zoom, cx, cy];
|
||||
this.store.dispatch(requestBigChunk(center));
|
||||
chunkRGB.isBasechunk = true;
|
||||
try {
|
||||
const url = `/chunks/${this.canvasId}/${cx}/${cy}.bmp`;
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
if (arrayBuffer.byteLength) {
|
||||
const chunkArray = new Uint8Array(arrayBuffer);
|
||||
chunkRGB.fromBuffer(chunkArray);
|
||||
} else {
|
||||
throw new Error('Chunk response was invalid');
|
||||
}
|
||||
this.store.dispatch(receiveBigChunk(center));
|
||||
} else {
|
||||
throw new Error('Network response was not ok.');
|
||||
}
|
||||
} catch (error) {
|
||||
chunkRGB.empty();
|
||||
this.store.dispatch(receiveBigChunkFailure(center, error));
|
||||
}
|
||||
}
|
||||
|
||||
async fetchTile(zoom, cx: number, cy: number, chunkRGB) {
|
||||
const center = [zoom, cx, cy];
|
||||
this.store.dispatch(requestBigChunk(center));
|
||||
try {
|
||||
const url = `/tiles/${this.canvasId}/${zoom}/${cx}/${cy}.png`;
|
||||
const img = await loadImage(url);
|
||||
chunkRGB.fromImage(img);
|
||||
this.store.dispatch(receiveBigChunk(center));
|
||||
} catch (error) {
|
||||
this.store.dispatch(receiveBigChunkFailure(center, error));
|
||||
chunkRGB.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ChunkLoader;
|
|
@ -7,7 +7,6 @@ import { TILE_SIZE } from '../core/constants';
|
|||
|
||||
|
||||
class ChunkRGB {
|
||||
cell: Cell;
|
||||
key: string;
|
||||
image: HTMLCanvasElement;
|
||||
ready: boolean;
|
||||
|
@ -15,7 +14,7 @@ class ChunkRGB {
|
|||
palette: Palette;
|
||||
isBasechunk: boolean;
|
||||
|
||||
constructor(palette: Palette, cell: Cell) {
|
||||
constructor(palette: Palette, key) {
|
||||
// isBasechunk gets set to true by RECEIVE_BIG_CHUNK
|
||||
// if true => chunk got requested from api/chunk and
|
||||
// receives websocket pixel updates
|
||||
|
@ -25,10 +24,8 @@ class ChunkRGB {
|
|||
this.image = document.createElement('canvas');
|
||||
this.image.width = TILE_SIZE;
|
||||
this.image.height = TILE_SIZE;
|
||||
this.cell = cell;
|
||||
this.key = ChunkRGB.getKey(...cell);
|
||||
this.key = key;
|
||||
this.ready = false;
|
||||
this.isEmpty = false;
|
||||
this.timestamp = Date.now();
|
||||
}
|
||||
|
||||
|
@ -89,21 +86,15 @@ class ChunkRGB {
|
|||
ctx.drawImage(img, 0, 0);
|
||||
}
|
||||
|
||||
empty() {
|
||||
empty(transparent: boolean = false) {
|
||||
this.ready = true;
|
||||
this.isEmpty = true;
|
||||
const { image, palette } = this;
|
||||
const ctx = image.getContext('2d');
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
ctx.fillStyle = palette.colors[0];
|
||||
ctx.fillRect(0, 0, TILE_SIZE, TILE_SIZE);
|
||||
}
|
||||
|
||||
static getKey(z: number, x: number, y: number) {
|
||||
// this is also hardcoded into core/utils.js at getColorIndexOfPixel
|
||||
// just to prevent whole ChunkRGB to get loaded into web.js
|
||||
// ...could test that at some point if really neccessary
|
||||
return `${z}:${x}:${y}`;
|
||||
if (!transparent) {
|
||||
const { image, palette } = this;
|
||||
const ctx = image.getContext('2d');
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
ctx.fillStyle = palette.colors[0];
|
||||
ctx.fillRect(0, 0, TILE_SIZE, TILE_SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
static getIndexFromCell([x, y]: Cell): number {
|
||||
|
|
|
@ -47,7 +47,7 @@ class PixelNotify {
|
|||
|
||||
|
||||
doRender() {
|
||||
return (this.pixelList.length != 0);
|
||||
return (this.pixelList.length !== 0);
|
||||
}
|
||||
|
||||
|
||||
|
@ -75,13 +75,18 @@ class PixelNotify {
|
|||
continue;
|
||||
}
|
||||
const [sx, sy] = worldToScreen(state, $viewport, [x, y])
|
||||
.map((x) => x + this.scale / 2);
|
||||
.map((z) => z + this.scale / 2);
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
const notRadius = timePasseded / PixelNotify.NOTIFICATION_TIME * this.notificationRadius;
|
||||
const circleScale = notRadius / 100;
|
||||
viewportCtx.save();
|
||||
viewportCtx.scale(circleScale, circleScale);
|
||||
viewportCtx.drawImage(this.notifcircle, sx / circleScale - 100, sy / circleScale - 100);
|
||||
viewportCtx.drawImage(
|
||||
this.notifcircle,
|
||||
sx / circleScale - 100,
|
||||
sy / circleScale - 100,
|
||||
);
|
||||
viewportCtx.restore();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
/*
|
||||
* Renders 2D canvases
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
@ -11,21 +12,19 @@ import {
|
|||
getTileOfPixel,
|
||||
getPixelFromChunkOffset,
|
||||
} from '../core/utils';
|
||||
import {
|
||||
fetchChunk,
|
||||
fetchTile,
|
||||
fetchHistoricalChunk,
|
||||
} from '../actions';
|
||||
|
||||
import {
|
||||
renderGrid,
|
||||
renderPlaceholder,
|
||||
renderPotatoPlaceholder,
|
||||
} from './renderelements';
|
||||
import ChunkRGB from './ChunkRGB';
|
||||
import { loadingTiles } from './loadImage';
|
||||
} from './render2Delements';
|
||||
import {
|
||||
initControls,
|
||||
removeControls,
|
||||
} from '../controls/PixelPainterControls';
|
||||
|
||||
|
||||
import ChunkLoader from './ChunkLoader2D';
|
||||
import pixelNotify from './PixelNotify';
|
||||
|
||||
// dimensions of offscreen canvas NOT whole canvas
|
||||
|
@ -38,6 +37,11 @@ const SCALE_THREASHOLD = Math.min(
|
|||
|
||||
|
||||
class Renderer {
|
||||
is3D: false;
|
||||
//
|
||||
canvasId: number = null;
|
||||
chunkLoader: Object = null;
|
||||
//--
|
||||
centerChunk: Cell;
|
||||
tiledScale: number;
|
||||
tiledZoom: number;
|
||||
|
@ -53,7 +57,7 @@ class Renderer {
|
|||
//--
|
||||
oldHistoricalTime: string;
|
||||
|
||||
constructor() {
|
||||
constructor(store) {
|
||||
this.centerChunk = [null, null];
|
||||
this.tiledScale = 0;
|
||||
this.tiledZoom = 4;
|
||||
|
@ -64,20 +68,44 @@ class Renderer {
|
|||
this.lastFetch = 0;
|
||||
this.oldHistoricalTime = null;
|
||||
//--
|
||||
const viewport = document.createElement('canvas');
|
||||
viewport.width = window.innerWidth;
|
||||
viewport.height = window.innerHeight;
|
||||
this.viewport = viewport;
|
||||
document.body.appendChild(this.viewport);
|
||||
//--
|
||||
this.resizeHandle = this.resizeHandle.bind(this);
|
||||
window.addEventListener('resize', this.resizeHandle);
|
||||
//--
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.canvas.width = CANVAS_WIDTH;
|
||||
this.canvas.height = CANVAS_HEIGHT;
|
||||
|
||||
const context = this.canvas.getContext('2d');
|
||||
if (!context) return;
|
||||
|
||||
context.fillStyle = '#000000';
|
||||
context.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
|
||||
//--
|
||||
this.setStore(store);
|
||||
}
|
||||
|
||||
destructor() {
|
||||
removeControls(this.viewport);
|
||||
window.removeEventListener('resize', this.resizeHandle);
|
||||
this.viewport.remove();
|
||||
}
|
||||
|
||||
getAllChunks() {
|
||||
return this.chunkLoader.getAllChunks();
|
||||
}
|
||||
|
||||
resizeHandle() {
|
||||
this.viewport.width = window.innerWidth;
|
||||
this.viewport.height = window.innerHeight;
|
||||
this.forceNextRender = true;
|
||||
}
|
||||
|
||||
// HAS to be set before any rendering can happen
|
||||
setViewport(viewport: HTMLCanvasElement, store) {
|
||||
this.viewport = viewport;
|
||||
setStore(store) {
|
||||
this.store = store;
|
||||
const state = store.getState();
|
||||
const {
|
||||
|
@ -87,8 +115,8 @@ class Renderer {
|
|||
canvasSize,
|
||||
} = state.canvas;
|
||||
this.updateCanvasData(state);
|
||||
initControls(this, this.viewport, store);
|
||||
this.updateScale(viewscale, canvasMaxTiledZoom, view, canvasSize);
|
||||
this.forceNextRender = true;
|
||||
}
|
||||
|
||||
updateCanvasData(state: State) {
|
||||
|
@ -97,7 +125,14 @@ class Renderer {
|
|||
viewscale,
|
||||
view,
|
||||
canvasSize,
|
||||
canvasId,
|
||||
} = state.canvas;
|
||||
if (canvasId !== this.canvasId) {
|
||||
this.canvasId = canvasId;
|
||||
if (canvasId !== null) {
|
||||
this.chunkLoader = new ChunkLoader(this.store);
|
||||
}
|
||||
}
|
||||
this.updateScale(viewscale, canvasMaxTiledZoom, view, canvasSize);
|
||||
}
|
||||
|
||||
|
@ -109,6 +144,10 @@ class Renderer {
|
|||
}
|
||||
}
|
||||
|
||||
getColorIndexOfPixel(cx, cy) {
|
||||
return this.chunkLoader.getColorIndexOfPixel(cx, cy);
|
||||
}
|
||||
|
||||
updateScale(
|
||||
viewscale: number,
|
||||
canvasMaxTiledZoom: number,
|
||||
|
@ -156,6 +195,7 @@ class Renderer {
|
|||
scale,
|
||||
isHistoricalView,
|
||||
} = state.canvas;
|
||||
this.chunkLoader.getPixelUpdate(i, j, offset, color);
|
||||
|
||||
if (scale < 0.8 || isHistoricalView) return;
|
||||
const scaleM = (scale > SCALE_THREASHOLD) ? 1 : scale;
|
||||
|
@ -216,10 +256,7 @@ class Renderer {
|
|||
} = this;
|
||||
const {
|
||||
viewscale: scale,
|
||||
canvasId,
|
||||
canvasSize,
|
||||
canvasMaxTiledZoom,
|
||||
chunks,
|
||||
} = state.canvas;
|
||||
|
||||
let { relScale } = this;
|
||||
|
@ -264,8 +301,7 @@ class Renderer {
|
|||
const [xc, yc] = chunkPosition; // center chunk
|
||||
// CLEAN margin
|
||||
// draw new chunks. If not existing, just clear.
|
||||
let chunk: ChunkRGB;
|
||||
let key: string;
|
||||
let chunk;
|
||||
for (let dx = -CHUNK_RENDER_RADIUS_X; dx <= CHUNK_RENDER_RADIUS_X; dx += 1) {
|
||||
for (let dy = -CHUNK_RENDER_RADIUS_Y; dy <= CHUNK_RENDER_RADIUS_Y; dy += 1) {
|
||||
const cx = xc + dx;
|
||||
|
@ -278,32 +314,11 @@ class Renderer {
|
|||
// if out of bounds
|
||||
context.fillRect(x, y, TILE_SIZE, TILE_SIZE);
|
||||
} else {
|
||||
key = ChunkRGB.getKey(tiledZoom, cx, cy);
|
||||
chunk = chunks.get(key);
|
||||
chunk = this.chunkLoader.getChunk(tiledZoom, cx, cy, fetch);
|
||||
if (chunk) {
|
||||
// render new chunk
|
||||
if (chunk.ready) {
|
||||
context.drawImage(chunk.image, x, y);
|
||||
if (fetch) chunk.timestamp = curTime;
|
||||
} else if (loadingTiles.hasTiles) {
|
||||
context.drawImage(loadingTiles.getTile(canvasId), x, y);
|
||||
} else {
|
||||
context.fillRect(x, y, TILE_SIZE, TILE_SIZE);
|
||||
}
|
||||
context.drawImage(chunk, x, y);
|
||||
} else {
|
||||
// we don't have that chunk
|
||||
if (fetch) {
|
||||
if (tiledZoom === canvasMaxTiledZoom) {
|
||||
this.store.dispatch(fetchChunk(canvasId, [tiledZoom, cx, cy]));
|
||||
} else {
|
||||
this.store.dispatch(fetchTile(canvasId, [tiledZoom, cx, cy]));
|
||||
}
|
||||
}
|
||||
if (loadingTiles.hasTiles) {
|
||||
context.drawImage(loadingTiles.getTile(canvasId), x, y);
|
||||
} else {
|
||||
context.fillRect(x, y, TILE_SIZE, TILE_SIZE);
|
||||
}
|
||||
context.fillRect(x, y, TILE_SIZE, TILE_SIZE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -313,10 +328,15 @@ class Renderer {
|
|||
|
||||
|
||||
render() {
|
||||
if (!this.chunkLoader) {
|
||||
return;
|
||||
}
|
||||
const state: State = this.store.getState();
|
||||
return (state.canvas.isHistoricalView)
|
||||
? this.renderHistorical(state)
|
||||
: this.renderMain(state);
|
||||
if (state.canvas.isHistoricalView) {
|
||||
this.renderHistorical(state);
|
||||
} else {
|
||||
this.renderMain(state);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -342,19 +362,29 @@ class Renderer {
|
|||
view,
|
||||
viewscale,
|
||||
canvasSize,
|
||||
canvasId,
|
||||
} = state.canvas;
|
||||
|
||||
if (!view || canvasId === null) return;
|
||||
|
||||
const [x, y] = view;
|
||||
const [cx, cy] = this.centerChunk;
|
||||
|
||||
// if we have to render pixelnotify
|
||||
const doRenderPixelnotify = (viewscale >= 0.5 && showPixelNotify && pixelNotify.doRender());
|
||||
// if we have to render placeholder
|
||||
const doRenderPlaceholder = (viewscale >= 3 && placeAllowed && (hover || this.hover) && !isPotato);
|
||||
const doRenderPotatoPlaceholder = (viewscale >= 3 && placeAllowed && (hover !== this.hover || this.forceNextRender || this.forceNextSubrender || doRenderPixelnotify) && isPotato);
|
||||
const doRenderPlaceholder = (
|
||||
viewscale >= 3
|
||||
&& placeAllowed
|
||||
&& (hover || this.hover)
|
||||
&& !isPotato
|
||||
);
|
||||
const doRenderPotatoPlaceholder = (
|
||||
viewscale >= 3
|
||||
&& placeAllowed
|
||||
&& (hover !== this.hover
|
||||
|| this.forceNextRender
|
||||
|| this.forceNextSubrender
|
||||
|| doRenderPixelnotify
|
||||
) && isPotato
|
||||
);
|
||||
//--
|
||||
// if we have nothing to render, return
|
||||
// note: this.hover is used to, to render without the placeholder one last time when cursor leaves window
|
||||
|
@ -425,9 +455,7 @@ class Renderer {
|
|||
} = this;
|
||||
const {
|
||||
viewscale,
|
||||
canvasId,
|
||||
canvasSize,
|
||||
chunks,
|
||||
historicalDate,
|
||||
historicalTime,
|
||||
} = state.canvas;
|
||||
|
@ -479,8 +507,7 @@ class Renderer {
|
|||
const [xc, yc] = chunkPosition; // center chunk
|
||||
// CLEAN margin
|
||||
// draw chunks. If not existing, just clear.
|
||||
let chunk: ChunkRGB;
|
||||
let key: string;
|
||||
let chunk;
|
||||
for (let dx = -CHUNK_RENDER_RADIUS_X; dx <= CHUNK_RENDER_RADIUS_X; dx += 1) {
|
||||
for (let dy = -CHUNK_RENDER_RADIUS_Y; dy <= CHUNK_RENDER_RADIUS_Y; dy += 1) {
|
||||
const cx = xc + dx;
|
||||
|
@ -494,55 +521,21 @@ class Renderer {
|
|||
context.fillRect(x, y, TILE_SIZE, TILE_SIZE);
|
||||
} else {
|
||||
// full chunks
|
||||
key = ChunkRGB.getKey(historicalDate, cx, cy);
|
||||
chunk = chunks.get(key);
|
||||
chunk = this.chunkLoader.getHistoricalChunk(cx, cy, fetch, historicalDate);
|
||||
if (chunk) {
|
||||
// render new chunk
|
||||
if (chunk.ready) {
|
||||
context.drawImage(chunk.image, x, y);
|
||||
if (fetch) chunk.timestamp = curTime;
|
||||
} else if (loadingTiles.hasTiles) {
|
||||
context.drawImage(loadingTiles.getTile(canvasId), x, y);
|
||||
} else {
|
||||
context.fillRect(x, y, TILE_SIZE, TILE_SIZE);
|
||||
}
|
||||
context.drawImage(chunk, x, y);
|
||||
} else {
|
||||
// we don't have that chunk
|
||||
if (fetch) {
|
||||
this.store.dispatch(fetchHistoricalChunk(canvasId, [cx, cy], historicalDate, null));
|
||||
}
|
||||
if (loadingTiles.hasTiles) {
|
||||
context.drawImage(loadingTiles.getTile(canvasId), x, y);
|
||||
} else {
|
||||
context.fillRect(x, y, TILE_SIZE, TILE_SIZE);
|
||||
}
|
||||
context.fillRect(x, y, TILE_SIZE, TILE_SIZE);
|
||||
}
|
||||
// incremential chunks
|
||||
if (historicalTime === '0000') continue;
|
||||
key = ChunkRGB.getKey(`${historicalDate}${historicalTime}`, cx, cy);
|
||||
chunk = chunks.get(key);
|
||||
chunk = this.chunkLoader.getHistoricalChunk(cx, cy, fetch, historicalDate, historicalTime);
|
||||
if (chunk) {
|
||||
// render new chunk
|
||||
if (!chunk.ready && oldHistoricalTime) {
|
||||
// redraw previous incremential chunk if new one is not there yet
|
||||
key = ChunkRGB.getKey(`${historicalDate}${oldHistoricalTime}`, cx, cy);
|
||||
chunk = chunks.get(key);
|
||||
}
|
||||
if (chunk && chunk.ready && !chunk.isEmpty) {
|
||||
context.drawImage(chunk.image, x, y);
|
||||
if (fetch) chunk.timestamp = curTime;
|
||||
}
|
||||
} else {
|
||||
if (fetch) {
|
||||
// we don't have that chunk
|
||||
this.store.dispatch(fetchHistoricalChunk(canvasId, [cx, cy], historicalDate, historicalTime));
|
||||
}
|
||||
if (oldHistoricalTime) {
|
||||
key = ChunkRGB.getKey(`${historicalDate}${oldHistoricalTime}`, cx, cy);
|
||||
chunk = chunks.get(key);
|
||||
if (chunk && chunk.ready && !chunk.isEmpty) {
|
||||
context.drawImage(chunk.image, x, y);
|
||||
}
|
||||
context.drawImage(chunk, x, y);
|
||||
} else if (oldHistoricalTime) {
|
||||
chunk = this.chunkLoader.getHistoricalChunk(cx, cy, false, historicalDate, oldHistoricalTime);
|
||||
if (chunk) {
|
||||
context.drawImage(chunk, x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -612,5 +605,4 @@ class Renderer {
|
|||
}
|
||||
|
||||
|
||||
const renderer = new Renderer();
|
||||
export default renderer;
|
||||
export default Renderer;
|
|
@ -0,0 +1,279 @@
|
|||
/*
|
||||
* 3D Renderer for VoxelCanvas
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import * as THREE from 'three';
|
||||
|
||||
import VoxelPainterControls from '../controls/VoxelPainterControls';
|
||||
import {
|
||||
setHover,
|
||||
} from '../actions';
|
||||
|
||||
|
||||
class Renderer {
|
||||
is3D = true;
|
||||
//--
|
||||
store;
|
||||
//--
|
||||
scene: Object;
|
||||
camera: Object;
|
||||
rollOverMesh: Object;
|
||||
voxel: Object;
|
||||
voxelMaterials: Array<Object>;
|
||||
objects: Array<Object>;
|
||||
plane: Object;
|
||||
//--
|
||||
controls: Object;
|
||||
threeRenderer: Object;
|
||||
//--
|
||||
mouse;
|
||||
raycaster;
|
||||
pressTime: number;
|
||||
|
||||
constructor(store) {
|
||||
this.store = store;
|
||||
const state = store.getState();
|
||||
this.objects = [];
|
||||
|
||||
// camera
|
||||
const camera = new THREE.PerspectiveCamera(
|
||||
45,
|
||||
window.innerWidth / window.innerHeight,
|
||||
1,
|
||||
2000,
|
||||
);
|
||||
camera.position.set(100, 160, 260);
|
||||
camera.lookAt(0, 0, 0);
|
||||
this.camera = camera;
|
||||
|
||||
// scene
|
||||
const scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color(0xf0f0f0);
|
||||
this.scene = scene;
|
||||
|
||||
// hover helper
|
||||
const rollOverGeo = new THREE.BoxBufferGeometry(10, 10, 10);
|
||||
const rollOverMaterial = new THREE.MeshBasicMaterial({
|
||||
color: 0xff0000,
|
||||
opacity: 0.5,
|
||||
transparent: true,
|
||||
});
|
||||
this.rollOverMesh = new THREE.Mesh(rollOverGeo, rollOverMaterial);
|
||||
scene.add(this.rollOverMesh);
|
||||
|
||||
// cubes
|
||||
this.voxel = new THREE.BoxBufferGeometry(10, 10, 10);
|
||||
this.initCubeMaterials(state);
|
||||
|
||||
// grid
|
||||
const gridHelper = new THREE.GridHelper(1000, 100, 0x555555, 0x555555);
|
||||
scene.add(gridHelper);
|
||||
|
||||
//
|
||||
this.raycaster = new THREE.Raycaster();
|
||||
this.mouse = new THREE.Vector2();
|
||||
|
||||
// Plane Floor
|
||||
const geometry = new THREE.PlaneBufferGeometry(5000, 5000);
|
||||
geometry.rotateX(-Math.PI / 2);
|
||||
const plane = new THREE.Mesh(
|
||||
geometry,
|
||||
new THREE.MeshLambertMaterial({ color: 0xcae3ff }),
|
||||
);
|
||||
scene.add(plane);
|
||||
this.plane = plane;
|
||||
this.objects.push(plane);
|
||||
|
||||
// lights
|
||||
const ambientLight = new THREE.AmbientLight(0x606060);
|
||||
scene.add(ambientLight);
|
||||
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff);
|
||||
directionalLight.position.set(1, 0.75, 0.5).normalize();
|
||||
scene.add(directionalLight);
|
||||
|
||||
// renderer
|
||||
const threeRenderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
threeRenderer.setPixelRatio(window.devicePixelRatio);
|
||||
threeRenderer.setSize(window.innerWidth, window.innerHeight);
|
||||
document.body.appendChild(threeRenderer.domElement);
|
||||
this.threeRenderer = threeRenderer;
|
||||
|
||||
// controls
|
||||
const controls = new VoxelPainterControls(camera, threeRenderer.domElement);
|
||||
controls.enableDamping = true;
|
||||
controls.dampingFactor = 0.75;
|
||||
controls.maxPolarAngle = Math.PI / 2;
|
||||
controls.minDistance = 100.00;
|
||||
controls.maxDistance = 1000.00;
|
||||
this.controls = controls;
|
||||
|
||||
const { domElement } = threeRenderer;
|
||||
|
||||
this.onDocumentMouseMove = this.onDocumentMouseMove.bind(this);
|
||||
this.onDocumentMouseDown = this.onDocumentMouseDown.bind(this);
|
||||
this.onDocumentMouseUp = this.onDocumentMouseUp.bind(this);
|
||||
this.onWindowResize = this.onWindowResize.bind(this);
|
||||
domElement.addEventListener('mousemove', this.onDocumentMouseMove, false);
|
||||
domElement.addEventListener('mousedown', this.onDocumentMouseDown, false);
|
||||
domElement.addEventListener('mouseup', this.onDocumentMouseUp, false);
|
||||
window.addEventListener('resize', this.onWindowResize, false);
|
||||
}
|
||||
|
||||
destructor() {
|
||||
window.addEventListener('resize', this.onWindowResize, false);
|
||||
this.threeRenderer.dispose();
|
||||
const { domElement } = this.threeRenderer;
|
||||
this.threeRenderer = null;
|
||||
domElement.remove();
|
||||
}
|
||||
|
||||
static getAllChunks() {
|
||||
return null;
|
||||
}
|
||||
|
||||
initCubeMaterials(state) {
|
||||
const { palette } = state.canvas;
|
||||
const { colors } = palette;
|
||||
const cubeMaterials = [];
|
||||
for (let index = 0; index < colors.length; index++) {
|
||||
const material = new THREE.MeshLambertMaterial({
|
||||
color: colors[index],
|
||||
});
|
||||
cubeMaterials.push(material);
|
||||
}
|
||||
this.voxelMaterials = cubeMaterials;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.threeRenderer) {
|
||||
return;
|
||||
}
|
||||
this.controls.update();
|
||||
this.threeRenderer.render(this.scene, this.camera);
|
||||
}
|
||||
|
||||
onWindowResize() {
|
||||
this.camera.aspect = window.innerWidth / window.innerHeight;
|
||||
this.camera.updateProjectionMatrix();
|
||||
this.threeRenderer.setSize(window.innerWidth, window.innerHeight);
|
||||
}
|
||||
|
||||
onDocumentMouseMove(event) {
|
||||
event.preventDefault();
|
||||
const {
|
||||
clientX,
|
||||
clientY,
|
||||
} = event;
|
||||
const {
|
||||
innerWidth,
|
||||
innerHeight,
|
||||
} = window;
|
||||
const {
|
||||
camera,
|
||||
objects,
|
||||
raycaster,
|
||||
mouse,
|
||||
rollOverMesh,
|
||||
} = this;
|
||||
|
||||
mouse.set(
|
||||
(clientX / innerWidth) * 2 - 1,
|
||||
-(clientY / innerHeight) * 2 + 1,
|
||||
);
|
||||
|
||||
raycaster.setFromCamera(mouse, camera);
|
||||
const intersects = raycaster.intersectObjects(objects);
|
||||
if (intersects.length > 0) {
|
||||
const intersect = intersects[0];
|
||||
rollOverMesh.position
|
||||
.copy(intersect.point)
|
||||
.add(intersect.face.normal);
|
||||
rollOverMesh.position
|
||||
.divideScalar(10)
|
||||
.floor()
|
||||
.multiplyScalar(10)
|
||||
.addScalar(5);
|
||||
}
|
||||
const hover = rollOverMesh.position
|
||||
.toArray()
|
||||
.map((u) => Math.floor(u / 10));
|
||||
this.store.dispatch(setHover(hover));
|
||||
}
|
||||
|
||||
onDocumentMouseDown() {
|
||||
this.pressTime = Date.now();
|
||||
}
|
||||
|
||||
onDocumentMouseUp(event) {
|
||||
if (Date.now() - this.pressTime > 600) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
const {
|
||||
clientX,
|
||||
clientY,
|
||||
} = event;
|
||||
const {
|
||||
innerWidth,
|
||||
innerHeight,
|
||||
} = window;
|
||||
const {
|
||||
camera,
|
||||
objects,
|
||||
raycaster,
|
||||
mouse,
|
||||
plane,
|
||||
voxel,
|
||||
voxelMaterials,
|
||||
store,
|
||||
scene,
|
||||
} = this;
|
||||
|
||||
mouse.set(
|
||||
(clientX / innerWidth) * 2 - 1,
|
||||
-(clientY / innerHeight) * 2 + 1,
|
||||
);
|
||||
|
||||
raycaster.setFromCamera(mouse, camera);
|
||||
|
||||
const intersects = raycaster.intersectObjects(objects);
|
||||
if (intersects.length > 0) {
|
||||
const intersect = intersects[0];
|
||||
|
||||
switch (event.button) {
|
||||
case 0: {
|
||||
// left mouse button
|
||||
const state = store.getState();
|
||||
const { selectedColor } = state.gui;
|
||||
const newVoxel = new THREE.Mesh(
|
||||
voxel,
|
||||
voxelMaterials[selectedColor],
|
||||
);
|
||||
newVoxel.position.copy(intersect.point)
|
||||
.add(intersect.face.normal);
|
||||
newVoxel.position.divideScalar(10)
|
||||
.floor()
|
||||
.multiplyScalar(10)
|
||||
.addScalar(5);
|
||||
scene.add(newVoxel);
|
||||
objects.push(newVoxel);
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
// right mouse button
|
||||
if (intersect.object !== plane) {
|
||||
scene.remove(intersect.object);
|
||||
objects.splice(objects.indexOf(intersect.object), 1);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Renderer;
|
|
@ -23,6 +23,7 @@ adTagParams.set('videoad_start_delay', 0);
|
|||
adTagParams.set('description_url', 'http://pixelplanet.fun/');
|
||||
adTagParams.set('max_ad_duration', 20000);
|
||||
if (__DEV__) adTagParams.set('adtest', 'on');
|
||||
// eslint-disable-next-line max-len
|
||||
const adTagUrl = `https://googleads.g.doubleclick.net/pagead/ads?${adTagParams.toString()}`;
|
||||
|
||||
/**
|
||||
|
@ -84,7 +85,7 @@ function init() {
|
|||
if (typeof google === 'undefined') return;
|
||||
outstreamContainer = document.getElementById('outstreamContainer');
|
||||
|
||||
adsController = new google.outstream.AdsController(
|
||||
adsController = new window.google.outstream.AdsController(
|
||||
outstreamContainer,
|
||||
onAdLoaded,
|
||||
onDone,
|
||||
|
|
|
@ -1,69 +0,0 @@
|
|||
/*
|
||||
* keypress actions
|
||||
* @flow
|
||||
*/
|
||||
import keycode from 'keycode';
|
||||
|
||||
import store from './store';
|
||||
import {
|
||||
toggleGrid,
|
||||
togglePixelNotify,
|
||||
toggleMute,
|
||||
moveNorth,
|
||||
moveWest,
|
||||
moveSouth,
|
||||
moveEast,
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
onViewFinishChange,
|
||||
} from '../actions';
|
||||
|
||||
|
||||
function onKeyPress(event: KeyboardEvent) {
|
||||
// ignore key presses if modal is open or chat is used
|
||||
if (event.target.nodeName == 'INPUT' || event.target.nodeName == 'TEXTAREA') return;
|
||||
|
||||
switch (keycode(event)) {
|
||||
case 'up':
|
||||
case 'w':
|
||||
store.dispatch(moveNorth());
|
||||
break;
|
||||
case 'left':
|
||||
case 'a':
|
||||
store.dispatch(moveWest());
|
||||
break;
|
||||
case 'down':
|
||||
case 's':
|
||||
store.dispatch(moveSouth());
|
||||
break;
|
||||
case 'right':
|
||||
case 'd':
|
||||
store.dispatch(moveEast());
|
||||
break;
|
||||
case 'g':
|
||||
store.dispatch(toggleGrid());
|
||||
return;
|
||||
case 'c':
|
||||
store.dispatch(togglePixelNotify());
|
||||
return;
|
||||
case 'space':
|
||||
if ($viewport) $viewport.click();
|
||||
return;
|
||||
case 'm':
|
||||
store.dispatch(toggleMute());
|
||||
return;
|
||||
case '+':
|
||||
case 'e':
|
||||
store.dispatch(zoomIn());
|
||||
return;
|
||||
case '-':
|
||||
case 'q':
|
||||
store.dispatch(zoomOut());
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
store.dispatch(onViewFinishChange());
|
||||
}
|
||||
|
||||
export default onKeyPress;
|
|
@ -21,10 +21,8 @@ export function loadImage(url) {
|
|||
*/
|
||||
class LoadingTiles {
|
||||
tiles: Object;
|
||||
hasTiles: boolean;
|
||||
|
||||
constructor() {
|
||||
this.hasTiles = false;
|
||||
this.tiles = {};
|
||||
this.loadLoadingTile(0);
|
||||
}
|
||||
|
@ -33,7 +31,7 @@ class LoadingTiles {
|
|||
if (typeof this.tiles[canvasId] === 'undefined') {
|
||||
this.loadLoadingTile(canvasId);
|
||||
}
|
||||
return this.tiles[canvasId] || this.tiles[0];
|
||||
return this.tiles[canvasId] || this.tiles[0] || null;
|
||||
}
|
||||
|
||||
async loadLoadingTile(canvasId: number) {
|
||||
|
@ -43,11 +41,7 @@ class LoadingTiles {
|
|||
this.tiles[canvasId] = null;
|
||||
const img = await loadImage(`./loading${canvasId}.png`);
|
||||
this.tiles[canvasId] = img;
|
||||
if (canvasId === 0) {
|
||||
this.hasTiles = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const loadingTiles = new LoadingTiles();
|
||||
|
|
|
@ -20,8 +20,7 @@ export function renderPlaceholder(
|
|||
const { selectedColor, hover } = state.gui;
|
||||
const { palette } = state.canvas;
|
||||
|
||||
const worldPos = screenToWorld(state, $viewport, hover);
|
||||
const [sx, sy] = worldToScreen(state, $viewport, worldPos);
|
||||
const [sx, sy] = worldToScreen(state, $viewport, hover);
|
||||
|
||||
viewportCtx.save();
|
||||
viewportCtx.translate(sx + (scale / 2), sy + (scale / 2));
|
||||
|
@ -55,8 +54,7 @@ export function renderPotatoPlaceholder(
|
|||
const { selectedColor, hover } = state.gui;
|
||||
const { palette } = state.canvas;
|
||||
|
||||
const worldPos = screenToWorld(state, $viewport, hover);
|
||||
const [sx, sy] = worldToScreen(state, $viewport, worldPos);
|
||||
const [sx, sy] = worldToScreen(state, $viewport, hover);
|
||||
|
||||
viewportCtx.save();
|
||||
viewportCtx.fillStyle = '#000';
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Manage renderers and switch between them
|
||||
* A renderer will create it's own viewport and append it
|
||||
* to document.body.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import Renderer2D from './Renderer2D';
|
||||
|
||||
let renderer = {
|
||||
render: () => null,
|
||||
destructor: () => null,
|
||||
renderPixel: () => null,
|
||||
updateCanvasData: () => null,
|
||||
};
|
||||
|
||||
function animationLoop() {
|
||||
renderer.render();
|
||||
window.requestAnimationFrame(animationLoop);
|
||||
}
|
||||
animationLoop();
|
||||
|
||||
export async function initRenderer(store, is3D: boolean) {
|
||||
renderer.destructor();
|
||||
if (is3D) {
|
||||
/* eslint-disable-next-line max-len */
|
||||
const module = await import(/* webpackChunkName: "voxel" */ '../ui/Renderer3D');
|
||||
const Renderer3D = module.default;
|
||||
renderer = new Renderer3D(store);
|
||||
} else {
|
||||
renderer = new Renderer2D(store);
|
||||
}
|
||||
return renderer;
|
||||
}
|
||||
|
||||
export function getRenderer() {
|
||||
return renderer;
|
||||
}
|
204
src/voxel.js
204
src/voxel.js
|
@ -1,204 +0,0 @@
|
|||
import * as THREE from 'three';
|
||||
import { VoxelPainterControls } from './controls/VoxelPainterControls';
|
||||
// import { OrbitControls } from './controls/OrbitControls';
|
||||
import store from './ui/store';
|
||||
|
||||
var camera, scene, renderer;
|
||||
var plane;
|
||||
var mouse, raycaster;
|
||||
|
||||
var rollOverMesh, rollOverMaterial;
|
||||
var cubeGeo, cubeMaterial;
|
||||
|
||||
var controls;
|
||||
|
||||
var objects = [];
|
||||
|
||||
function init() {
|
||||
// quit 2d rendering
|
||||
const canvas = document.getElementById('gameWindow').remove();
|
||||
|
||||
camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 2000 );
|
||||
camera.position.set( 100, 160, 260 );
|
||||
camera.lookAt( 0, 0, 0 );
|
||||
|
||||
scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color( 0xf0f0f0 );
|
||||
|
||||
// roll-over helpers
|
||||
|
||||
var rollOverGeo = new THREE.BoxBufferGeometry( 10, 10, 10 );
|
||||
rollOverMaterial = new THREE.MeshBasicMaterial( { color: 0xff0000, opacity: 0.5, transparent: true } );
|
||||
rollOverMesh = new THREE.Mesh( rollOverGeo, rollOverMaterial );
|
||||
scene.add( rollOverMesh );
|
||||
|
||||
// cubes
|
||||
cubeGeo = new THREE.BoxBufferGeometry( 10, 10, 10 );
|
||||
cubeMaterial = new THREE.MeshLambertMaterial( { color: 0xfeb74c } );
|
||||
|
||||
// grid
|
||||
var gridHelper = new THREE.GridHelper( 1000, 100, 0x555555, 0x555555 );
|
||||
scene.add( gridHelper );
|
||||
|
||||
//
|
||||
raycaster = new THREE.Raycaster();
|
||||
mouse = new THREE.Vector2();
|
||||
|
||||
|
||||
// Floor with random cold color triangles
|
||||
var floorGeometry = new THREE.PlaneBufferGeometry( 1000, 1000, 100, 100 );
|
||||
floorGeometry.rotateX( - Math.PI / 2 );
|
||||
// vertex displacement
|
||||
var vertex = new THREE.Vector3();
|
||||
var color = new THREE.Color();
|
||||
var position = floorGeometry.attributes.position;
|
||||
for ( var i = 0, l = position.count; i < l; i ++ ) {
|
||||
vertex.fromBufferAttribute( position, i );
|
||||
vertex.x += Math.random() * 20 - 10;
|
||||
vertex.y += Math.random() * 2;
|
||||
vertex.z += Math.random() * 20 - 10;
|
||||
position.setXYZ( i, vertex.x, vertex.y, vertex.z );
|
||||
}
|
||||
floorGeometry = floorGeometry.toNonIndexed(); // ensure each face has unique vertices
|
||||
position = floorGeometry.attributes.position;
|
||||
var colors = [];
|
||||
for ( var i = 0, l = position.count; i < l; i ++ ) {
|
||||
color.setHSL( Math.random() * 0.3 + 0.5, 0.75, Math.random() * 0.25 + 0.75 );
|
||||
colors.push( color.r, color.g, color.b );
|
||||
}
|
||||
floorGeometry.setAttribute( 'color', new THREE.Float32BufferAttribute( colors, 3 ) );
|
||||
var floorMaterial = new THREE.MeshBasicMaterial( { vertexColors: THREE.VertexColors } );
|
||||
plane = new THREE.Mesh( floorGeometry, floorMaterial );
|
||||
scene.add( plane );
|
||||
objects.push( plane );
|
||||
|
||||
/*
|
||||
// Plane Floor
|
||||
var geometry = new THREE.PlaneBufferGeometry( 5000, 5000 );
|
||||
geometry.rotateX( - Math.PI / 2 );
|
||||
plane = new THREE.Mesh( geometry, new THREE.MeshLambertMaterial({ color: 0x009900 }) );
|
||||
scene.add( plane );
|
||||
objects.push( plane );
|
||||
*/
|
||||
|
||||
|
||||
// lights
|
||||
|
||||
var ambientLight = new THREE.AmbientLight( 0x606060 );
|
||||
scene.add( ambientLight );
|
||||
|
||||
var directionalLight = new THREE.DirectionalLight( 0xffffff );
|
||||
directionalLight.position.set( 1, 0.75, 0.5 ).normalize();
|
||||
scene.add( directionalLight );
|
||||
|
||||
renderer = new THREE.WebGLRenderer( { antialias: true } );
|
||||
renderer.setPixelRatio( window.devicePixelRatio );
|
||||
renderer.setSize( window.innerWidth, window.innerHeight );
|
||||
document.body.appendChild( renderer.domElement );
|
||||
|
||||
controls = new VoxelPainterControls(camera, renderer.domElement);
|
||||
controls.enableDamping = true;
|
||||
controls.dampingFactor = 0.75;
|
||||
controls.maxPolarAngle = Math.PI / 2;
|
||||
controls.minDistance = 100.00;
|
||||
controls.maxDistance = 1000.00;
|
||||
/*controls.rotateSpeed = 1.5;
|
||||
controls.zoomSpeed = 10.0;
|
||||
controls.panSpeed = 0.3;
|
||||
controls.keys = [65, 83, 68]; // ASD
|
||||
controls.dynamicDampingFactor = 0.2;
|
||||
*/
|
||||
|
||||
renderer.domElement.addEventListener( 'mousemove', onDocumentMouseMove, false );
|
||||
renderer.domElement.addEventListener( 'mousedown', onDocumentMouseDown, false );
|
||||
renderer.domElement.addEventListener( 'mouseup', onDocumentMouseUp, false );
|
||||
//document.addEventListener( 'keydown', onDocumentKeyDown, false );
|
||||
//document.addEventListener( 'keyup', onDocumentKeyUp, false );
|
||||
//
|
||||
window.addEventListener( 'resize', onWindowResize, false );
|
||||
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
window.animationLoop = () => {
|
||||
controls.update();
|
||||
renderer.render( scene, camera );
|
||||
requestAnimationFrame(window.animationLoop);
|
||||
}
|
||||
|
||||
function onWindowResize() {
|
||||
|
||||
camera.aspect = window.innerWidth / window.innerHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize( window.innerWidth, window.innerHeight );
|
||||
|
||||
}
|
||||
|
||||
function onDocumentMouseMove( event ) {
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
mouse.set( ( event.clientX / window.innerWidth ) * 2 - 1, - ( event.clientY / window.innerHeight ) * 2 + 1 );
|
||||
|
||||
raycaster.setFromCamera( mouse, camera );
|
||||
|
||||
var intersects = raycaster.intersectObjects( objects );
|
||||
|
||||
if ( intersects.length > 0 ) {
|
||||
|
||||
var intersect = intersects[ 0 ];
|
||||
|
||||
rollOverMesh.position.copy( intersect.point ).add( intersect.face.normal );
|
||||
rollOverMesh.position.divideScalar( 10 ).floor().multiplyScalar( 10 ).addScalar( 5 );
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var pressTime = 0;
|
||||
function onDocumentMouseDown( event ) {
|
||||
pressTime = Date.now();
|
||||
}
|
||||
|
||||
function onDocumentMouseUp( event ) {
|
||||
if (Date.now() - pressTime > 600) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
mouse.set( ( event.clientX / window.innerWidth ) * 2 - 1, - ( event.clientY / window.innerHeight ) * 2 + 1 );
|
||||
|
||||
raycaster.setFromCamera( mouse, camera );
|
||||
|
||||
var intersects = raycaster.intersectObjects( objects );
|
||||
|
||||
if ( intersects.length > 0 ) {
|
||||
var intersect = intersects[ 0 ];
|
||||
|
||||
switch ( event.button ) {
|
||||
case 0:
|
||||
// left mouse button
|
||||
const state = store.getState();
|
||||
const clri = state.gui.selectedColor;
|
||||
const clr = state.canvas.palette.colors[clri];
|
||||
const material = new THREE.MeshLambertMaterial( { color: clr } );
|
||||
|
||||
console.log("set voxel");
|
||||
var voxel = new THREE.Mesh( cubeGeo, material );
|
||||
voxel.position.copy( intersect.point ).add( intersect.face.normal );
|
||||
voxel.position.divideScalar( 10 ).floor().multiplyScalar( 10 ).addScalar( 5 );
|
||||
scene.add( voxel );
|
||||
|
||||
objects.push( voxel );
|
||||
break;
|
||||
case 2:
|
||||
// right mouse button
|
||||
console.log("remove voxel");
|
||||
if ( intersect.object !== plane ) {
|
||||
scene.remove( intersect.object );
|
||||
objects.splice( objects.indexOf( intersect.object ), 1 );
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
199
src/voxelSA.js
199
src/voxelSA.js
|
@ -1,199 +0,0 @@
|
|||
import * as THREE from 'three';
|
||||
import { VoxelPainterControls } from './controls/VoxelPainterControls';
|
||||
// import { OrbitControls } from './controls/OrbitControls';
|
||||
|
||||
var camera, scene, renderer;
|
||||
var plane;
|
||||
var mouse, raycaster;
|
||||
|
||||
var rollOverMesh, rollOverMaterial;
|
||||
var cubeGeo, cubeMaterial;
|
||||
|
||||
var controls;
|
||||
|
||||
var objects = [];
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
init();
|
||||
render();
|
||||
});
|
||||
|
||||
function init() {
|
||||
|
||||
camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 2000 );
|
||||
camera.position.set( 100, 160, 260 );
|
||||
camera.lookAt( 0, 0, 0 );
|
||||
|
||||
scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color( 0xf0f0f0 );
|
||||
|
||||
// roll-over helpers
|
||||
|
||||
var rollOverGeo = new THREE.BoxBufferGeometry( 10, 10, 10 );
|
||||
rollOverMaterial = new THREE.MeshBasicMaterial( { color: 0xff0000, opacity: 0.5, transparent: true } );
|
||||
rollOverMesh = new THREE.Mesh( rollOverGeo, rollOverMaterial );
|
||||
scene.add( rollOverMesh );
|
||||
|
||||
// cubes
|
||||
cubeGeo = new THREE.BoxBufferGeometry( 10, 10, 10 );
|
||||
cubeMaterial = new THREE.MeshLambertMaterial( { color: 0xfeb74c } );
|
||||
|
||||
// grid
|
||||
var gridHelper = new THREE.GridHelper( 1000, 100, 0x555555, 0x555555 );
|
||||
scene.add( gridHelper );
|
||||
|
||||
//
|
||||
raycaster = new THREE.Raycaster();
|
||||
mouse = new THREE.Vector2();
|
||||
|
||||
|
||||
// Floor with random cold color triangles
|
||||
var floorGeometry = new THREE.PlaneBufferGeometry( 1000, 1000, 100, 100 );
|
||||
floorGeometry.rotateX( - Math.PI / 2 );
|
||||
// vertex displacement
|
||||
var vertex = new THREE.Vector3();
|
||||
var color = new THREE.Color();
|
||||
var position = floorGeometry.attributes.position;
|
||||
for ( var i = 0, l = position.count; i < l; i ++ ) {
|
||||
vertex.fromBufferAttribute( position, i );
|
||||
vertex.x += Math.random() * 20 - 10;
|
||||
vertex.y += Math.random() * 2;
|
||||
vertex.z += Math.random() * 20 - 10;
|
||||
position.setXYZ( i, vertex.x, vertex.y, vertex.z );
|
||||
}
|
||||
floorGeometry = floorGeometry.toNonIndexed(); // ensure each face has unique vertices
|
||||
position = floorGeometry.attributes.position;
|
||||
var colors = [];
|
||||
for ( var i = 0, l = position.count; i < l; i ++ ) {
|
||||
color.setHSL( Math.random() * 0.3 + 0.5, 0.75, Math.random() * 0.25 + 0.75 );
|
||||
colors.push( color.r, color.g, color.b );
|
||||
}
|
||||
floorGeometry.setAttribute( 'color', new THREE.Float32BufferAttribute( colors, 3 ) );
|
||||
var floorMaterial = new THREE.MeshBasicMaterial( { vertexColors: THREE.VertexColors } );
|
||||
plane = new THREE.Mesh( floorGeometry, floorMaterial );
|
||||
scene.add( plane );
|
||||
objects.push( plane );
|
||||
|
||||
/*
|
||||
// Plane Floor
|
||||
var geometry = new THREE.PlaneBufferGeometry( 5000, 5000 );
|
||||
geometry.rotateX( - Math.PI / 2 );
|
||||
plane = new THREE.Mesh( geometry, new THREE.MeshLambertMaterial({ color: 0x009900 }) );
|
||||
scene.add( plane );
|
||||
objects.push( plane );
|
||||
*/
|
||||
|
||||
|
||||
// lights
|
||||
|
||||
var ambientLight = new THREE.AmbientLight( 0x606060 );
|
||||
scene.add( ambientLight );
|
||||
|
||||
var directionalLight = new THREE.DirectionalLight( 0xffffff );
|
||||
directionalLight.position.set( 1, 0.75, 0.5 ).normalize();
|
||||
scene.add( directionalLight );
|
||||
|
||||
renderer = new THREE.WebGLRenderer( { antialias: true } );
|
||||
renderer.setPixelRatio( window.devicePixelRatio );
|
||||
renderer.setSize( window.innerWidth, window.innerHeight );
|
||||
document.body.appendChild( renderer.domElement );
|
||||
|
||||
controls = new VoxelPainterControls(camera, renderer.domElement);
|
||||
controls.enableDamping = true;
|
||||
controls.dampingFactor = 0.75;
|
||||
controls.maxPolarAngle = Math.PI / 2;
|
||||
controls.minDistance = 100.00;
|
||||
controls.maxDistance = 1000.00;
|
||||
/*controls.rotateSpeed = 1.5;
|
||||
controls.zoomSpeed = 10.0;
|
||||
controls.panSpeed = 0.3;
|
||||
controls.keys = [65, 83, 68]; // ASD
|
||||
controls.dynamicDampingFactor = 0.2;
|
||||
*/
|
||||
|
||||
renderer.domElement.addEventListener( 'mousemove', onDocumentMouseMove, false );
|
||||
renderer.domElement.addEventListener( 'mousedown', onDocumentMouseDown, false );
|
||||
renderer.domElement.addEventListener( 'mouseup', onDocumentMouseUp, false );
|
||||
//document.addEventListener( 'keydown', onDocumentKeyDown, false );
|
||||
//document.addEventListener( 'keyup', onDocumentKeyUp, false );
|
||||
//
|
||||
window.addEventListener( 'resize', onWindowResize, false );
|
||||
|
||||
}
|
||||
|
||||
function render() {
|
||||
controls.update();
|
||||
renderer.render( scene, camera );
|
||||
requestAnimationFrame(render);
|
||||
}
|
||||
|
||||
function onWindowResize() {
|
||||
|
||||
camera.aspect = window.innerWidth / window.innerHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize( window.innerWidth, window.innerHeight );
|
||||
|
||||
}
|
||||
|
||||
function onDocumentMouseMove( event ) {
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
mouse.set( ( event.clientX / window.innerWidth ) * 2 - 1, - ( event.clientY / window.innerHeight ) * 2 + 1 );
|
||||
|
||||
raycaster.setFromCamera( mouse, camera );
|
||||
|
||||
var intersects = raycaster.intersectObjects( objects );
|
||||
|
||||
if ( intersects.length > 0 ) {
|
||||
|
||||
var intersect = intersects[ 0 ];
|
||||
|
||||
rollOverMesh.position.copy( intersect.point ).add( intersect.face.normal );
|
||||
rollOverMesh.position.divideScalar( 10 ).floor().multiplyScalar( 10 ).addScalar( 5 );
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var pressTime = 0;
|
||||
function onDocumentMouseDown( event ) {
|
||||
pressTime = Date.now();
|
||||
}
|
||||
|
||||
function onDocumentMouseUp( event ) {
|
||||
if (Date.now() - pressTime > 600) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
mouse.set( ( event.clientX / window.innerWidth ) * 2 - 1, - ( event.clientY / window.innerHeight ) * 2 + 1 );
|
||||
|
||||
raycaster.setFromCamera( mouse, camera );
|
||||
|
||||
var intersects = raycaster.intersectObjects( objects );
|
||||
|
||||
if ( intersects.length > 0 ) {
|
||||
var intersect = intersects[ 0 ];
|
||||
|
||||
switch ( event.button ) {
|
||||
case 0:
|
||||
// left mouse button
|
||||
console.log("set voxel");
|
||||
var voxel = new THREE.Mesh( cubeGeo, cubeMaterial );
|
||||
voxel.position.copy( intersect.point ).add( intersect.face.normal );
|
||||
voxel.position.divideScalar( 10 ).floor().multiplyScalar( 10 ).addScalar( 5 );
|
||||
scene.add( voxel );
|
||||
|
||||
objects.push( voxel );
|
||||
break;
|
||||
case 2:
|
||||
// right mouse button
|
||||
console.log("remove voxel");
|
||||
if ( intersect.object !== plane ) {
|
||||
scene.remove( intersect.object );
|
||||
objects.splice( objects.indexOf( intersect.object ), 1 );
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue