diff --git a/src/client.js b/src/client.js index 8c5c8fb..9cd4d9c 100644 --- a/src/client.js +++ b/src/client.js @@ -41,7 +41,7 @@ import onKeyPress from './ui/keypress'; import App from './components/App'; -import Renderer from './ui/Renderer'; +import renderer from './ui/Renderer'; import ProtocolClient from './socket/ProtocolClient'; window.addEventListener('keydown', onKeyPress, false); @@ -190,8 +190,7 @@ document.addEventListener('DOMContentLoaded', () => { ); const viewport = initViewport(); - const renderer = new Renderer(); - renderer.setViewport(viewport); + renderer.setViewport(viewport, store); ProtocolClient.on('pixelUpdate', ({ i, j, offset, color, diff --git a/src/store/configureStore.js b/src/store/configureStore.js index c2025e1..a5f87a3 100644 --- a/src/store/configureStore.js +++ b/src/store/configureStore.js @@ -8,6 +8,7 @@ import { persistStore } from 'redux-persist'; import audio from './audio'; import swal from './sweetAlert'; import protocolClientHook from './protocolClientHook'; +import rendererHook from './rendererHook'; // import ads from './ads'; // import analytics from './analytics'; import array from './array'; @@ -38,6 +39,7 @@ const store = createStore( notifications, title, protocolClientHook, + rendererHook, // ads, // analytics, logger, diff --git a/src/store/rendererHook.js b/src/store/rendererHook.js new file mode 100644 index 0000000..5951284 --- /dev/null +++ b/src/store/rendererHook.js @@ -0,0 +1,53 @@ +/* + * Hooks for renderer + * + * @flow + */ + +import renderer from '../ui/Renderer'; + +export default (store) => (next) => (action) => { + // executed after reducers + const ret = next(action); + + const state = store.getState(); + + switch (action.type) { + case 'RELOAD_URL': + case 'SELECT_CANVAS': + case 'RECEIVE_ME': { + renderer.updateCanvasData(state); + break; + } + + case 'REQUEST_BIG_CHUNK': + case 'RECEIVE_BIG_CHUNK': + case 'RECEIVE_BIG_CHUNK_FAILURE': + case 'RECEIVE_IMAGE_TILE': { + renderer.forceNextRender = true; + break; + } + + case 'SET_SCALE': { + const { + viewscale, + canvasMaxTiledZoom, + view, + canvasSize, + } = state.canvas; + renderer.updateScale(viewscale, canvasMaxTiledZoom, view, canvasSize); + break; + } + + case 'SET_VIEW_COORDINATES': { + const { view, canvasSize } = state.canvas; + renderer.updateView(view, canvasSize); + break; + } + + default: + // nothing + } + + return ret; +}; diff --git a/src/ui/Renderer.js b/src/ui/Renderer.js index b9bd088..ac17a2c 100644 --- a/src/ui/Renderer.js +++ b/src/ui/Renderer.js @@ -11,7 +11,6 @@ import { getTileOfPixel, getPixelFromChunkOffset, } from '../core/utils'; -import store from './store'; import { fetchChunk, fetchTile } from '../actions'; import { @@ -35,31 +34,25 @@ const SCALE_THREASHOLD = Math.min( class Renderer { - lastFetchs: number; centerChunk: Cell; - view: Cell; tiledScale: number; tiledZoom: number; hover: boolean; - canvasId: number; //-- viewport: HTMLCanvasElement = null; + store; //-- - scale: number; forceNextRender: boolean; forceNextSubrender: boolean; canvas: HTMLCanvasElement; lastFetch: number; constructor() { - this.lastFetchs = 0; this.centerChunk = [null, null]; this.tiledScale = 0; this.tiledZoom = 4; this.hover = false; - this.canvasId = null; //-- - this.scale = 0; this.forceNextRender = true; this.forceNextSubrender = true; this.lastFetch = 0; @@ -76,33 +69,95 @@ class Renderer { } // HAS to be set before any rendering can happen - setViewport(viewport: HTMLCanvasElement) { + setViewport(viewport: HTMLCanvasElement, store) { this.viewport = viewport; + this.store = store; + const state = store.getState(); + const { + canvasMaxTiledZoom, + viewscale, + view, + canvasSize, + } = state.canvas; + this.updateCanvasData(state); + this.updateScale(viewscale, canvasMaxTiledZoom, view, canvasSize); + this.forceNextRender = true; } + updateCanvasData(state: State) { + const { + canvasMaxTiledZoom, + viewscale, + view, + canvasSize, + } = state.canvas; + this.tiledZoom = canvasMaxTiledZoom + Math.log2(this.tiledScale) / 2; + this.updateScale(viewscale, canvasMaxTiledZoom, view, canvasSize); + } + + updateScale( + viewscale: number, + canvasMaxTiledZoom: number, + view, + canvasSize, + ) { + pixelNotify.updateScale(viewscale); + let tiledScale = (viewscale > 0.5) ? 0 : Math.round(Math.log2(viewscale) / 2); + tiledScale = 4 ** tiledScale; + const tiledZoom = canvasMaxTiledZoom + Math.log2(tiledScale) / 2; + const relScale = viewscale / tiledScale; + + this.tiledScale = tiledScale; + this.tiledZoom = tiledZoom; + this.relScale = relScale; + this.updateView(view, canvasSize); + } + + updateView(view, canvasSize) { + const [x, y] = view; + let [cx, cy] = this.centerChunk; + const [curcx, curcy] = getTileOfPixel(this.tiledScale, [x, y], canvasSize); + if (cx !== curcx || cy !== curcy) { + cx = curcx; + cy = curcy; + this.centerChunk = [cx, cy]; + this.forceNextRender = true; + } else { + this.forceNextSubrender = true; + } + } + + renderPixel( i: number, j: number, offset: number, color: ColorIndex, ) { - if (this.scale < 0.8) return; - const scale = (this.scale > SCALE_THREASHOLD) ? 1 : this.scale; + const state: State = this.store.getState(); + const { + canvasSize, + palette, + scale, + } = state.canvas; + + if (scale < 0.8) return; + const scaleM = (scale > SCALE_THREASHOLD) ? 1 : scale; const context = this.canvas.getContext('2d'); if (!context) return; - const [x, y] = getPixelFromChunkOffset(i, j, offset, this.canvasSize); + const [x, y] = getPixelFromChunkOffset(i, j, offset, canvasSize); const [canX, canY] = this.centerChunk - .map((z) => (z + 0.5) * TILE_SIZE - this.canvasSize / 2); - const px = ((x - canX) * scale) + (CANVAS_WIDTH / 2); - const py = ((y - canY) * scale) + (CANVAS_HEIGHT / 2); + .map((z) => (z + 0.5) * TILE_SIZE - canvasSize / 2); + const px = ((x - canX) * scaleM) + (CANVAS_WIDTH / 2); + const py = ((y - canY) * scaleM) + (CANVAS_HEIGHT / 2); // if not part of our current canvas, do not render if (px < 0 || py >= CANVAS_WIDTH || py < 0 || py >= CANVAS_HEIGHT) return; - context.fillStyle = this.palette.colors[color]; - context.fillRect(px, py, scale, scale); + context.fillStyle = palette.colors[color]; + context.fillRect(px, py, scaleM, scaleM); pixelNotify.addPixel(x, y); this.forceNextSubrender = true; @@ -132,16 +187,25 @@ class Renderer { renderChunks( - chunks, - view: Cell, - canvasId: number, + state: State, ) { const context = this.canvas.getContext('2d'); if (!context) return; const { - centerChunk: chunkPosition, scale, tiledScale, tiledZoom, + centerChunk: chunkPosition, + tiledScale, + tiledZoom, + viewport, } = this; + const { + viewscale: scale, + canvasId, + canvasSize, + canvasMaxTiledZoom, + chunks, + } = state.canvas; + let { relScale } = this; // clear rect is just needed for Google Chrome, else it would flash regularly context.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); @@ -159,7 +223,7 @@ class Renderer { } // define how many chunks we will render // don't render chunks outside of viewport - const { width, height } = this.viewport; + const { width, height } = viewport; const CHUNK_RENDER_RADIUS_X = Math.ceil(width / TILE_SIZE / 2 / relScale); const CHUNK_RENDER_RADIUS_Y = Math.ceil(height / TILE_SIZE / 2 / relScale); // If scale is so large that neighbouring chunks wouldn't fit in canvas, @@ -194,7 +258,7 @@ class Renderer { const x = xOffset + dx * TILE_SIZE; const y = yOffset + dy * TILE_SIZE; - const chunkMaxXY = this.canvasSize / TILE_SIZE; + const chunkMaxXY = canvasSize / TILE_SIZE; if (cx < 0 || cx >= chunkMaxXY * tiledScale || cy < 0 || cy >= chunkMaxXY * tiledScale) { // if out of bounds context.fillRect(x, y, TILE_SIZE, TILE_SIZE); @@ -214,10 +278,10 @@ class Renderer { } else { // we don't have that chunk if (fetch) { - if (tiledZoom === this.canvasMaxTiledZoom) { - store.dispatch(fetchChunk(canvasId, [tiledZoom, cx, cy])); + if (tiledZoom === canvasMaxTiledZoom) { + this.store.dispatch(fetchChunk(canvasId, [tiledZoom, cx, cy])); } else { - store.dispatch(fetchTile(canvasId, [tiledZoom, cx, cy])); + this.store.dispatch(fetchTile(canvasId, [tiledZoom, cx, cy])); } } if (loadingTiles.hasTiles) { @@ -233,9 +297,14 @@ class Renderer { } + // keep in mind that everything we got here gets executed 60 times per second + // avoiding unneccessary stuff is important render() { - const { viewport } = this; - const state: State = store.getState(); + try{ + const { + viewport, + } = this; + const state: State = this.store.getState(); const { showGrid, showPixelNotify, @@ -247,57 +316,28 @@ class Renderer { placeAllowed, } = state.user; const { - chunks, view, - fetchs, viewscale, + canvasSize, canvasId, } = state.canvas; if (!view || canvasId === null) return; - if (this.canvasId !== canvasId) { - const { canvasSize, palette, canvasMaxTiledZoom } = state.canvas; - this.canvasSize = canvasSize; - this.palette = palette; - this.canvasMaxTiledZoom = canvasMaxTiledZoom; - this.tiledZoom = this.canvasMaxTiledZoom + Math.log2(this.tiledScale) / 2; - } - const [x, y] = view; - let [cx, cy] = this.centerChunk; + const [cx, cy] = this.centerChunk; // if we have to render pixelnotify const doRenderPixelnotify = (viewscale >= 0.5 && showPixelNotify && pixelNotify.doRender()); - // if we have to render offscreen canvas - let doRenderCanvas = (this.lastFetchs !== fetchs || this.forceNextRender); - if (viewscale !== this.scale) { - this.scale = viewscale; - pixelNotify.updateScale(viewscale); - this.tiledScale = (viewscale > 0.5) ? 0 : Math.round(Math.log2(viewscale) / 2); - this.tiledScale = 4 ** this.tiledScale; - this.tiledZoom = this.canvasMaxTiledZoom + Math.log2(this.tiledScale) / 2; - this.relScale = viewscale / this.tiledScale; - doRenderCanvas = true; - } // if we have to render placeholder const doRenderPlaceholder = (viewscale >= 3 && placeAllowed && (hover || this.hover) && !isPotato); - const doRenderPotatoPlaceholder = (viewscale >= 3 && placeAllowed && (hover !== this.hover || doRenderCanvas || this.forceNextSubrender || doRenderPixelnotify) && isPotato); + const doRenderPotatoPlaceholder = (viewscale >= 3 && placeAllowed && (hover !== this.hover || this.forceNextRender || this.forceNextSubrender || doRenderPixelnotify) && isPotato); //-- - if (view !== this.view) { - const [curcx, curcy] = getTileOfPixel(this.tiledScale, [x, y], this.canvasSize); - if (cx !== curcx || cy !== curcy) { - cx = curcx; - cy = curcy; - this.centerChunk = [cx, cy]; - doRenderCanvas = true; - } - this.view = view; // 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 - } else if ( + if ( // no full rerender - !doRenderCanvas + !this.forceNextRender // no render placeholder under cursor && !doRenderPlaceholder && !doRenderPotatoPlaceholder @@ -310,9 +350,8 @@ class Renderer { } this.hover = hover; - if (doRenderCanvas) { - this.renderChunks(chunks, view, canvasId); - this.lastFetchs = fetchs; + if (this.forceNextRender) { + this.renderChunks(state); } this.forceNextRender = false; this.forceNextSubrender = false; @@ -327,7 +366,8 @@ class Renderer { viewportCtx.imageSmoothingEnabled = false; // If scale is so large that neighbouring chunks wouldn't fit in offscreen canvas, // do scale = 1 in renderChunks and scale in render() - const canvasCenter = this.canvasSize / 2; + const canvasCenter = canvasSize / 2; + console.log("do render") if (viewscale > SCALE_THREASHOLD) { viewportCtx.save(); viewportCtx.scale(viewscale, viewscale); @@ -347,8 +387,12 @@ class Renderer { if (hover && doRenderPlaceholder) renderPlaceholder(state, viewport, viewscale); if (hover && doRenderPotatoPlaceholder) renderPotatoPlaceholder(state, viewport, viewscale); + } catch { + console.log("error"); + } } } -export default Renderer; +const renderer = new Renderer(); +export default renderer;