Add 3d voxel canvas

Add Canvas Selection Menu
Improve Rendering
Move ChunkRGB into seperate file
This commit is contained in:
HF 2020-01-22 15:34:46 +01:00
parent 88990bf454
commit caf08ee32d
54 changed files with 2499 additions and 3686 deletions

BIN
public/preview0.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
public/preview1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
public/preview2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

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

View File

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

View File

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

View File

@ -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."
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

42
src/components/UI.jsx Normal file
View File

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

View File

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

View File

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

37
src/controls/keypress.js Normal file
View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@
* @flow
*/
import Sequelize from 'sequelize';
// import Sequelize from 'sequelize';
import nodemailer from 'nodemailer';
import logger from './logger';

View File

@ -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...');

View File

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

View File

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

View File

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

View File

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

View File

@ -37,6 +37,7 @@ export default function modal(
};
}
case 'SELECT_CANVAS':
case 'HIDE_MODAL':
return {
...state,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -35,6 +35,7 @@ export default () => (next) => (action) => {
// nothing
}
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
}
return next(action);

View File

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

View File

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

View File

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

201
src/ui/ChunkLoader2D.js Normal file
View File

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

View File

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

View File

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

View File

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

279
src/ui/Renderer3D.js Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

39
src/ui/renderer.js Normal file
View File

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

View File

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

View File

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