refactor image converter and 3d stuff

This commit is contained in:
HF 2022-01-03 16:46:59 +01:00
parent e59df4fb62
commit 6170d35631
29 changed files with 721 additions and 715 deletions

View File

@ -4,6 +4,9 @@
], ],
"parser":"@babel/eslint-parser", "parser":"@babel/eslint-parser",
"parserOptions": { "parserOptions": {
"babelOptions":{
"rootMode": "upward"
},
"ecmaFeatures": { "ecmaFeatures": {
"jsx": true "jsx": true
} }

View File

@ -2,7 +2,6 @@
* Create canvases.json with localized translated * Create canvases.json with localized translated
* descriptions. * descriptions.
* *
* @flow
*/ */
import canvases from './canvases.json'; import canvases from './canvases.json';

View File

@ -1,6 +1,5 @@
/** /**
* * Converts images to canvas palettes
* @flow
*/ */
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
@ -11,8 +10,10 @@ import { jt, t } from 'ttag';
import { import {
ColorDistanceCalculators, ColorDistanceCalculators,
ImageQuantizerKernels, ImageQuantizerKernels,
readFileIntoCanvas,
scaleImage,
quantizeImage, quantizeImage,
getImageDataOfFile, addGrid,
} from '../utils/image'; } from '../utils/image';
import printGIMPPalette from '../core/exportGPL'; import printGIMPPalette from '../core/exportGPL';
import { copyCanvasToClipboard } from '../utils/clipboard'; import { copyCanvasToClipboard } from '../utils/clipboard';
@ -23,119 +24,45 @@ function downloadOutput() {
output.toBlob((blob) => fileDownload(blob, 'ppfunconvert.png')); output.toBlob((blob) => fileDownload(blob, 'ppfunconvert.png'));
} }
function createCanvasFromImageData(imgData) {
const { width, height } = imgData;
const inputCanvas = document.createElement('canvas');
inputCanvas.width = width;
inputCanvas.height = height;
const inputCtx = inputCanvas.getContext('2d');
inputCtx.putImageData(imgData, 0, 0);
return inputCanvas;
}
function addGrid(imgData, lightGrid, offsetX, offsetY) {
const image = createCanvasFromImageData(imgData);
const { width, height } = image;
const can = document.createElement('canvas');
const ctx = can.getContext('2d');
can.width = width * 5;
can.height = height * 5;
ctx.imageSmoothingEnabled = false;
ctx.mozImageSmoothingEnabled = false;
ctx.webkitImageSmoothingEnabled = false;
ctx.msImageSmoothingEnabled = false;
ctx.save();
ctx.scale(5.0, 5.0);
ctx.drawImage(image, 0, 0);
ctx.restore();
ctx.fillStyle = (lightGrid) ? '#DDDDDD' : '#222222';
for (let i = 0; i <= width; i += 1) {
const thick = ((i + (offsetX * 1)) % 10 === 0) ? 2 : 1;
ctx.fillRect(i * 5, 0, thick, can.height);
}
for (let j = 0; j <= height; j += 1) {
const thick = ((j + (offsetY * 1)) % 10 === 0) ? 2 : 1;
ctx.fillRect(0, j * 5, can.width, thick);
}
return ctx.getImageData(0, 0, can.width, can.height);
}
function scaleImage(imgData, width, height, doAA) {
const can = document.createElement('canvas');
const ctxo = can.getContext('2d');
can.width = width;
can.height = height;
const scaleX = width / imgData.width;
const scaleY = height / imgData.height;
if (doAA) {
// scale with canvas for antialiasing
const image = createCanvasFromImageData(imgData);
ctxo.save();
ctxo.scale(scaleX, scaleY);
ctxo.drawImage(image, 0, 0);
ctxo.restore();
return ctxo.getImageData(0, 0, width, height);
}
// scale manually
const imdo = ctxo.createImageData(width, height);
const { data: datao } = imdo;
const { data: datai } = imgData;
for (let y = 0; y < height; y += 1) {
for (let x = 0; x < width; x += 1) {
let posi = (Math.round(x / scaleX) + Math.round(y / scaleY)
* imgData.width) * 4;
let poso = (x + y * width) * 4;
datao[poso++] = datai[posi++];
datao[poso++] = datai[posi++];
datao[poso++] = datai[posi++];
datao[poso] = datai[posi];
}
}
return imdo;
}
let renderOpts = null; let renderOpts = null;
let rendering = false; let rendering = false;
async function renderOutputImage(opts) { async function renderOutputImage(opts) {
if (!opts.file) { if (!opts.imgCanvas) {
return; return;
} }
renderOpts = opts; renderOpts = opts;
if (rendering) { if (rendering) {
console.log('skip rendering');
return; return;
} }
console.log('render');
rendering = true; rendering = true;
while (renderOpts) { while (renderOpts) {
const { const {
file, dither, grid, scaling, colors, imgCanvas, ditherOpts, grid, scaling,
} = renderOpts; } = renderOpts;
renderOpts = null; renderOpts = null;
if (file) { if (imgCanvas) {
let image = file; let image = imgCanvas;
if (scaling.enabled) { if (scaling.enabled) {
// scale // scale
const { width, height, aa } = scaling; const { width, height, aa } = scaling;
image = scaleImage( image = scaleImage(
file, imgCanvas,
width, width,
height, height,
aa, aa,
); );
} }
// dither // dither
const { colors, strategy, colorDist } = dither;
const progEl = document.getElementById('qprog'); const progEl = document.getElementById('qprog');
progEl.innerText = 'Loading...';
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
image = await quantizeImage(colors, image, { image = await quantizeImage(colors, image, {
strategy, ...ditherOpts,
colorDist,
onProgress: (progress) => { onProgress: (progress) => {
progEl.innerHTML = `Loading... ${Math.round(progress)} %`; progEl.innerText = `Loading... ${Math.round(progress)} %`;
}, },
}); });
progEl.innerHTML = 'Done'; progEl.innerText = 'Done';
// grid // grid
if (grid.enabled) { if (grid.enabled) {
const { light, offsetX, offsetY } = grid; const { light, offsetX, offsetY } = grid;
@ -151,7 +78,7 @@ async function renderOutputImage(opts) {
output.width = image.width; output.width = image.width;
output.height = image.height; output.height = image.height;
const ctx = output.getContext('2d'); const ctx = output.getContext('2d');
ctx.putImageData(image, 0, 0); ctx.drawImage(image, 0, 0);
} }
} }
rendering = false; rendering = false;
@ -170,10 +97,15 @@ function Converter() {
], shallowEqual); ], shallowEqual);
const [selectedCanvas, selectCanvas] = useState(canvasId); const [selectedCanvas, selectCanvas] = useState(canvasId);
const [inputImageData, setInputImageData] = useState(null); const [inputImageCanvas, setInputImageCanvas] = useState(null);
const [selectedStrategy, selectStrategy] = useState('Nearest'); const [selectedStrategy, selectStrategy] = useState('Nearest');
const [selectedColorDist, selectColorDist] = useState('Euclidean'); const [selectedColorDist, selectColorDist] = useState('Euclidean');
const [selectedScaleKeepRatio, selectScaleKeepRatio] = useState(true); const [selectedScaleKeepRatio, selectScaleKeepRatio] = useState(true);
const [extraOpts, setExtraOpts] = useState({
serpentine: true,
minColorDistance: 0,
GIMPerror: false,
});
const [scaleData, setScaleData] = useState({ const [scaleData, setScaleData] = useState({
enabled: false, enabled: false,
width: 10, width: 10,
@ -181,36 +113,45 @@ function Converter() {
aa: true, aa: true,
}); });
const [gridData, setGridData] = useState({ const [gridData, setGridData] = useState({
enabled: true, enabled: false,
light: false, light: false,
offsetX: 0, offsetX: 0,
offsetY: 0, offsetY: 0,
}); });
const [extraRender, setExtraRender] = useState(false);
const [gridRender, setGridRender] = useState(false);
const [scalingRender, setScalingRender] = useState(false);
useEffect(() => { useEffect(() => {
if (inputImageData) { if (inputImageCanvas) {
const canvas = canvases[selectedCanvas]; const canvas = canvases[selectedCanvas];
const dither = {
colors: canvas.colors.slice(canvas.cli),
strategy: selectedStrategy,
colorDist: selectedColorDist,
};
renderOutputImage({ renderOutputImage({
file: inputImageData, colors: canvas.colors.slice(canvas.cli),
dither, imgCanvas: inputImageCanvas,
ditherOpts: {
strategy: selectedStrategy,
colorDist: selectedColorDist,
...extraOpts,
},
grid: gridData, grid: gridData,
scaling: scaleData, scaling: scaleData,
}); });
} }
}, [ }, [
inputImageData, selectedCanvas,
inputImageCanvas,
selectedStrategy, selectedStrategy,
selectedColorDist, selectedColorDist,
extraOpts,
scaleData, scaleData,
selectedCanvas,
gridData, gridData,
]); ]);
const {
serpentine,
minColorDistance,
GIMPerror,
} = extraOpts;
const { const {
enabled: gridEnabled, enabled: gridEnabled,
light: gridLight, light: gridLight,
@ -224,6 +165,24 @@ function Converter() {
aa: scalingAA, aa: scalingAA,
} = scaleData; } = scaleData;
const showExtraOptions = selectedStrategy !== 'Nearest'
&& selectedStrategy !== 'Riemersma';
useEffect(() => {
if (showExtraOptions) {
setTimeout(() => setExtraRender(true), 10);
}
}, [selectedStrategy]);
useEffect(() => {
if (gridEnabled) {
setTimeout(() => setGridRender(true), 10);
}
}, [gridData.enabled]);
useEffect(() => {
if (scalingEnabled) {
setTimeout(() => setScalingRender(true), 10);
}
}, [scaleData.enabled]);
const gimpLink = <a href="https://www.gimp.org">GIMP</a>; const gimpLink = <a href="https://www.gimp.org">GIMP</a>;
const starhouseLink = ( const starhouseLink = (
<a href="https://twitter.com/starhousedev"> <a href="https://twitter.com/starhousedev">
@ -291,15 +250,15 @@ function Converter() {
const fileSel = evt.target; const fileSel = evt.target;
const file = (!fileSel.files || !fileSel.files[0]) const file = (!fileSel.files || !fileSel.files[0])
? null : fileSel.files[0]; ? null : fileSel.files[0];
const imageData = await getImageDataOfFile(file); const imageData = await readFileIntoCanvas(file);
setInputImageData(null); setInputImageCanvas(null);
setScaleData({ setScaleData({
enabled: false, enabled: false,
width: imageData.width, width: imageData.width,
height: imageData.height, height: imageData.height,
aa: true, aa: true,
}); });
setInputImageData(imageData); setInputImageCanvas(imageData);
}} }}
/> />
<p className="modalcotext">{t`Choose Strategy`}:&nbsp; <p className="modalcotext">{t`Choose Strategy`}:&nbsp;
@ -319,6 +278,59 @@ function Converter() {
} }
</select> </select>
</p> </p>
{(showExtraOptions || extraRender) && (
<div
className={(showExtraOptions && extraRender)
? 'convBox show'
: 'convBox'}
onTransitionEnd={() => {
if (!showExtraOptions) setExtraRender(false);
}}
>
<p style={{ fontHeight: 16 }} className="modalcotext">
<input
type="checkbox"
checked={serpentine}
onChange={(e) => {
setExtraOpts({
...extraOpts,
serpentine: e.target.checked,
});
}}
/>
{t`Serpentine`}
</p>
<span className="modalcotext">{t`Minimum Color Distance`}:&nbsp;
<input
type="number"
step="1"
min="0"
max="100"
style={{ width: '4em' }}
value={minColorDistance}
onChange={(e) => {
setExtraOpts({
...extraOpts,
minColorDistance: e.target.value,
});
}}
/>&nbsp;
</span>
<p style={{ fontHeight: 16 }} className="modalcotext">
<input
type="checkbox"
checked={GIMPerror}
onChange={(e) => {
setExtraOpts({
...extraOpts,
GIMPerror: e.target.checked,
});
}}
/>
{t`Calculate like GIMP`}
</p>
</div>
)}
<p className="modalcotext">{t`Choose Color Mode`}:&nbsp; <p className="modalcotext">{t`Choose Color Mode`}:&nbsp;
<select <select
value={selectedColorDist} value={selectedColorDist}
@ -349,64 +361,60 @@ function Converter() {
/> />
{t`Add Grid (uncheck if you need a 1:1 template)`} {t`Add Grid (uncheck if you need a 1:1 template)`}
</p> </p>
{(gridEnabled) {(gridEnabled || gridRender) && (
? ( <div
<div style={{ className={(gridEnabled && gridRender) ? 'convBox show' : 'convBox'}
borderStyle: 'solid', onTransitionEnd={() => {
borderColor: '#D4D4D4', if (!gridEnabled) setGridRender(false);
borderWidth: 2,
padding: 5,
display: 'inline-block',
}} }}
> >
<p style={{ fontHeight: 16 }} className="modalcotext"> <p style={{ fontHeight: 16 }} className="modalcotext">
<input <input
type="checkbox" type="checkbox"
checked={gridLight} checked={gridLight}
onChange={(e) => { onChange={(e) => {
setGridData({ setGridData({
...gridData, ...gridData,
light: e.target.checked, light: e.target.checked,
}); });
}} }}
/> />
{t`Light Grid`} {t`Light Grid`}
</p> </p>
<span className="modalcotext">{t`Offset`} X:&nbsp; <span className="modalcotext">{t`Offset`} X:&nbsp;
<input <input
type="number" type="number"
step="1" step="1"
min="0" min="0"
max="10" max="10"
style={{ width: '2em' }} style={{ width: '2em' }}
value={gridOffsetX} value={gridOffsetX}
onChange={(e) => { onChange={(e) => {
setGridData({ setGridData({
...gridData, ...gridData,
offsetX: e.target.value, offsetX: e.target.value,
}); });
}} }}
/>&nbsp; />&nbsp;
</span> </span>
<span className="modalcotext">{t`Offset`} Y:&nbsp; <span className="modalcotext">{t`Offset`} Y:&nbsp;
<input <input
type="number" type="number"
step="1" step="1"
min="0" min="0"
max="10" max="10"
style={{ width: '2em' }} style={{ width: '2em' }}
value={gridOffsetY} value={gridOffsetY}
onChange={(e) => { onChange={(e) => {
setGridData({ setGridData({
...gridData, ...gridData,
offsetY: e.target.value, offsetY: e.target.value,
}); });
}} }}
/> />
</span> </span>
</div> </div>
) )}
: null}
<p style={{ fontHeight: 16 }} className="modalcotext"> <p style={{ fontHeight: 16 }} className="modalcotext">
<input <input
type="checkbox" type="checkbox"
@ -420,117 +428,117 @@ function Converter() {
/> />
{t`Scale Image`} {t`Scale Image`}
</p> </p>
{(scalingEnabled) {(scalingEnabled || scalingRender) && (
? ( <div
<div style={{ className={(scalingEnabled && scalingRender)
borderStyle: 'solid', ? 'convBox show'
borderColor: '#D4D4D4', : 'convBox'}
borderWidth: 2, onTransitionEnd={() => {
padding: 5, if (!scalingEnabled) setScalingRender(false);
display: 'inline-block',
}} }}
> >
<span className="modalcotext">{t`Width`}:&nbsp; <span className="modalcotext">{t`Width`}:&nbsp;
<input <input
type="number" type="number"
step="1" step="1"
min="1" min="1"
max="1024" max="1024"
style={{ width: '3em' }} style={{ width: '3em' }}
value={scalingWidth} value={scalingWidth}
onChange={(e) => { onChange={(e) => {
const newWidth = (e.target.value > 1024) const newWidth = (e.target.value > 1024)
? 1024 : e.target.value; ? 1024 : e.target.value;
if (!newWidth) return; if (!newWidth) return;
if (selectedScaleKeepRatio && inputImageData) { if (selectedScaleKeepRatio && inputImageCanvas) {
const ratio = inputImageData.width / inputImageData.height; const ratio = inputImageCanvas.width
const newHeight = Math.round(newWidth / ratio); / inputImageCanvas.height;
if (newHeight <= 0) return; const newHeight = Math.round(newWidth / ratio);
setScaleData({ if (newHeight <= 0) return;
...scaleData,
width: newWidth,
height: newHeight,
});
return;
}
setScaleData({ setScaleData({
...scaleData, ...scaleData,
width: newWidth, width: newWidth,
height: newHeight,
}); });
}} return;
/>&nbsp; }
</span> setScaleData({
<span className="modalcotext">{t`Height`}:&nbsp; ...scaleData,
<input width: newWidth,
type="number" });
step="1" }}
min="1" />&nbsp;
max="1024" </span>
style={{ width: '3em' }} <span className="modalcotext">{t`Height`}:&nbsp;
value={scalingHeight} <input
onChange={(e) => { type="number"
const nuHeight = (e.target.value > 1024) step="1"
? 1024 : e.target.value; min="1"
if (!nuHeight) return; max="1024"
if (selectedScaleKeepRatio && inputImageData) { style={{ width: '3em' }}
const ratio = inputImageData.width / inputImageData.height; value={scalingHeight}
const nuWidth = Math.round(ratio * nuHeight); onChange={(e) => {
if (nuWidth <= 0) return; const nuHeight = (e.target.value > 1024)
setScaleData({ ? 1024 : e.target.value;
...scaleData, if (!nuHeight) return;
width: nuWidth, if (selectedScaleKeepRatio && inputImageCanvas) {
height: nuHeight, const ratio = inputImageCanvas.width
}); / inputImageCanvas.height;
return; const nuWidth = Math.round(ratio * nuHeight);
} if (nuWidth <= 0) return;
setScaleData({ setScaleData({
...scaleData, ...scaleData,
width: nuWidth,
height: nuHeight, height: nuHeight,
}); });
}} return;
/>
</span>
<p style={{ fontHeight: 16 }} className="modalcotext">
<input
type="checkbox"
checked={selectedScaleKeepRatio}
onChange={(e) => {
selectScaleKeepRatio(e.target.checked);
}}
/>
{t`Keep Ratio`}
</p>
<p style={{ fontHeight: 16 }} className="modalcotext">
<input
type="checkbox"
checked={scalingAA}
onChange={(e) => {
setScaleData({
...scaleData,
aa: e.target.checked,
});
}}
/>
{t`Anti Aliasing`}
</p>
<button
type="button"
onClick={() => {
if (inputImageData) {
setScaleData({
...scaleData,
width: inputImageData.width,
height: inputImageData.height,
});
} }
setScaleData({
...scaleData,
height: nuHeight,
});
}} }}
> />
{t`Reset`} </span>
</button> <p style={{ fontHeight: 16 }} className="modalcotext">
</div> <input
) type="checkbox"
: null} checked={selectedScaleKeepRatio}
{(inputImageData) onChange={(e) => {
selectScaleKeepRatio(e.target.checked);
}}
/>
{t`Keep Ratio`}
</p>
<p style={{ fontHeight: 16 }} className="modalcotext">
<input
type="checkbox"
checked={scalingAA}
onChange={(e) => {
setScaleData({
...scaleData,
aa: e.target.checked,
});
}}
/>
{t`Anti Aliasing`}
</p>
<button
type="button"
onClick={() => {
if (inputImageCanvas) {
setScaleData({
...scaleData,
width: inputImageCanvas.width,
height: inputImageCanvas.height,
});
}
}}
>
{t`Reset`}
</button>
</div>
)}
{(inputImageCanvas)
? ( ? (
<div> <div>
<p id="qprog">...</p> <p id="qprog">...</p>

View File

@ -17,9 +17,9 @@ const Menu = () => {
const menuOpen = useSelector((state) => state.gui.menuOpen); const menuOpen = useSelector((state) => state.gui.menuOpen);
useEffect(() => { useEffect(() => {
window.setTimeout(() => { if (menuOpen) {
if (menuOpen) setRender(true); setTimeout(() => setRender(true), 10);
}, 10); }
}, [menuOpen]); }, [menuOpen]);
const onTransitionEnd = () => { const onTransitionEnd = () => {

View File

@ -26,7 +26,7 @@ const UserMessages = () => {
&& ( && (
<p className="usermessages"> <p className="usermessages">
{t`Please verify your mail address&nbsp; {t`Please verify your mail address&nbsp;
or your account could get deleted after a few days.`}&nbsp; or your account could get deleted after a few days.`}
{(verifyAnswer) {(verifyAnswer)
? ( ? (
<span <span

View File

@ -828,7 +828,7 @@ class VoxelPainterControls extends EventDispatcher {
// so camera.up is the orbit axis // so camera.up is the orbit axis
const quat = new Quaternion() const quat = new Quaternion()
.setFromUnitVectors(object.up, new Vector3(0, 1, 0)); .setFromUnitVectors(object.up, new Vector3(0, 1, 0));
const quatInverse = quat.clone().inverse(); const quatInverse = quat.clone().invert();
const lastPosition = new Vector3(); const lastPosition = new Vector3();
const lastQuaternion = new Quaternion(); const lastQuaternion = new Quaternion();
@ -1042,8 +1042,4 @@ class VoxelPainterControls extends EventDispatcher {
} }
} }
// VoxelPainterControls.prototype = Object.create(EventDispatcher.prototype);
// VoxelPainterControls.prototype.constructor = VoxelPainterControls;
export default VoxelPainterControls; export default VoxelPainterControls;

View File

@ -1,10 +1,10 @@
/* @flow */
const OP_CODE = 0xA6; const OP_CODE = 0xA6;
export default { export default {
OP_CODE, OP_CODE,
dehydrate(): ArrayBuffer {
dehydrate() {
// Server (sender)
const buffer = new ArrayBuffer(1); const buffer = new ArrayBuffer(1);
const view = new DataView(buffer); const view = new DataView(buffer);
view.setInt8(0, OP_CODE); view.setInt8(0, OP_CODE);

View File

@ -1,14 +1,13 @@
/* @flow */
const OP_CODE = 0xC2; const OP_CODE = 0xC2;
export default { export default {
OP_CODE, OP_CODE,
hydrate(data: DataView) { hydrate(data) {
// client (receiver)
return data.getUint32(1); return data.getUint32(1);
}, },
dehydrate(wait): Buffer { dehydrate(wait) {
// Server (sender)
const buffer = Buffer.allocUnsafe(1 + 4); const buffer = Buffer.allocUnsafe(1 + 4);
buffer.writeUInt8(OP_CODE, 0); buffer.writeUInt8(OP_CODE, 0);
buffer.writeUInt32BE(wait, 1); buffer.writeUInt32BE(wait, 1);

View File

@ -1,16 +1,13 @@
/* @flow */
const OP_CODE = 0xA2; const OP_CODE = 0xA2;
export default { export default {
OP_CODE, OP_CODE,
hydrate(data: Buffer) { hydrate(data) {
// SERVER (Client) // SERVER (Receiver)
const i = data[1] << 8 | data[2]; const i = data[1] << 8 | data[2];
return i; return i;
}, },
dehydrate(chunkid): Buffer { dehydrate(chunkid) {
// CLIENT (Sender) // CLIENT (Sender)
const buffer = new ArrayBuffer(1 + 2); const buffer = new ArrayBuffer(1 + 2);
const view = new DataView(buffer); const view = new DataView(buffer);

View File

@ -1,11 +1,11 @@
/* @flow */
const OP_CODE = 0xA4; const OP_CODE = 0xA4;
export default { export default {
OP_CODE, OP_CODE,
dehydrate(chunks: Array): ArrayBuffer { /*
* @param chunks Array of chunks
*/
dehydrate(chunks) {
// CLIENT (Sender) // CLIENT (Sender)
const buffer = new ArrayBuffer(1 + 1 + chunks.length * 2); const buffer = new ArrayBuffer(1 + 1 + chunks.length * 2);
const view = new Uint16Array(buffer); const view = new Uint16Array(buffer);

View File

@ -1,20 +1,14 @@
/* @flow */
type OnlineCounterPacket = {
online: number,
};
const OP_CODE = 0xA7; const OP_CODE = 0xA7;
export default { export default {
OP_CODE, OP_CODE,
hydrate(data: DataView): OnlineCounterPacket { hydrate(data) {
// CLIENT // CLIENT (receiver)
const online = data.getInt16(1); const online = data.getInt16(1);
return { online }; return { online };
}, },
dehydrate({ online }: OnlineCounterPacket): Buffer { dehydrate({ online }) {
// SERVER // SERVER (sender)
if (!process.env.BROWSER) { if (!process.env.BROWSER) {
const buffer = Buffer.allocUnsafe(1 + 2); const buffer = Buffer.allocUnsafe(1 + 2);
buffer.writeUInt8(OP_CODE, 0); buffer.writeUInt8(OP_CODE, 0);

View File

@ -1,11 +1,9 @@
/* @flow */
const OP_CODE = 0xC3; const OP_CODE = 0xC3;
export default { export default {
OP_CODE, OP_CODE,
hydrate(data: DataView) { hydrate(data) {
// Client (receiver)
const retCode = data.getUint8(1); const retCode = data.getUint8(1);
const wait = data.getUint32(2); const wait = data.getUint32(2);
const coolDownSeconds = data.getInt16(6); const coolDownSeconds = data.getInt16(6);
@ -17,7 +15,8 @@ export default {
pxlCnt, pxlCnt,
}; };
}, },
dehydrate(retCode, wait, coolDown, pxlCnt): Buffer { dehydrate(retCode, wait, coolDown, pxlCnt) {
// Server (sender)
const buffer = Buffer.allocUnsafe(1 + 1 + 4 + 2 + 1); const buffer = Buffer.allocUnsafe(1 + 1 + 4 + 2 + 1);
buffer.writeUInt8(OP_CODE, 0); buffer.writeUInt8(OP_CODE, 0);
buffer.writeUInt8(retCode, 1); buffer.writeUInt8(retCode, 1);

View File

@ -1,22 +1,18 @@
/* /*
* Packet for sending and receiving pixels per chunk * Packet for sending and receiving pixels per chunk
* Multiple pixels can be sent at once * Multiple pixels can be sent at once
* Client side
* *
* @flow
*/ */
type PixelUpdatePacket = {
x: number,
y: number,
pixels: Array,
};
const OP_CODE = 0xC1; const OP_CODE = 0xC1;
export default { export default {
OP_CODE, OP_CODE,
hydrate(data: DataView): PixelUpdatePacket { /*
* @param data DataVies
*/
hydrate(data) {
/* /*
* chunk coordinates * chunk coordinates
*/ */
@ -40,7 +36,7 @@ export default {
}; };
}, },
dehydrate(i, j, pixels): Buffer { dehydrate(i, j, pixels) {
const buffer = new ArrayBuffer(1 + 1 + 1 + pixels.length * 4); const buffer = new ArrayBuffer(1 + 1 + 1 + pixels.length * 4);
const view = new DataView(buffer); const view = new DataView(buffer);
view.setUint8(0, OP_CODE); view.setUint8(0, OP_CODE);

View File

@ -1,17 +1,15 @@
/* @flow */ /*
* Packet for sending and receiving pixels per chunk
* Multiple pixels can be sent at once
type PixelUpdatePacket = { * Server side.
x: number, *
y: number, * */
pixels: Array,
};
const OP_CODE = 0xC1; const OP_CODE = 0xC1;
export default { export default {
OP_CODE, OP_CODE,
hydrate(data: Buffer): PixelUpdatePacket { hydrate(data) {
/* /*
* chunk coordinates * chunk coordinates
*/ */
@ -45,7 +43,7 @@ export default {
* @param chunkId id consisting of chunk coordinates * @param chunkId id consisting of chunk coordinates
* @param pixels Buffer with offset and color of one or more pixels * @param pixels Buffer with offset and color of one or more pixels
*/ */
dehydrate(chunkId, pixels): Buffer { dehydrate(chunkId, pixels) {
const index = new Uint8Array([OP_CODE, chunkId >> 8, chunkId & 0xFF]); const index = new Uint8Array([OP_CODE, chunkId >> 8, chunkId & 0xFF]);
return Buffer.concat([index, pixels]); return Buffer.concat([index, pixels]);
}, },

View File

@ -0,0 +1,6 @@
# Binary Websocket Packages
Note that the node Server receives in [Buffer](https://nodejs.org/api/buffer.html), while the client receives [DataViews](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView) and sends ArrayBuffers.
Therefor the server can't share the same code with the client for hydrate / dehydrate.
Most packages are unidirectional so hydrate is for either client or server and dehydrate for the other one.
Bidrectional packages have two files, one for Client, another one for Server.

View File

@ -1,16 +1,13 @@
/* @flow */
const OP_CODE = 0xA0; const OP_CODE = 0xA0;
export default { export default {
OP_CODE, OP_CODE,
hydrate(data: Buffer) { hydrate(data) {
// SERVER (Client) // SERVER (Receiver)
const canvasId = data[1]; const canvasId = data[1];
return canvasId; return canvasId;
}, },
dehydrate(canvasId): ArrayBuffer { dehydrate(canvasId) {
// CLIENT (Sender) // CLIENT (Sender)
const buffer = new ArrayBuffer(1 + 1); const buffer = new ArrayBuffer(1 + 1);
const view = new DataView(buffer); const view = new DataView(buffer);

View File

@ -1,16 +1,13 @@
/* @flow */
const OP_CODE = 0xA1; const OP_CODE = 0xA1;
export default { export default {
OP_CODE, OP_CODE,
hydrate(data: Buffer) { hydrate(data) {
// SERVER (Client) // SERVER (Receiver)
const i = data[1] << 8 | data[2]; const i = data[1] << 8 | data[2];
return i; return i;
}, },
dehydrate(chunkid): ArrayBuffer { dehydrate(chunkid) {
// CLIENT (Sender) // CLIENT (Sender)
const buffer = new ArrayBuffer(1 + 2); const buffer = new ArrayBuffer(1 + 2);
const view = new DataView(buffer); const view = new DataView(buffer);

View File

@ -1,11 +1,11 @@
/* @flow */
const OP_CODE = 0xA3; const OP_CODE = 0xA3;
export default { export default {
OP_CODE, OP_CODE,
dehydrate(chunks: Array): ArrayBuffer { /*
* @param chunks Array of chunks
*/
dehydrate(chunks) {
// CLIENT (Sender) // CLIENT (Sender)
const buffer = new ArrayBuffer(1 + 1 + chunks.length * 2); const buffer = new ArrayBuffer(1 + 1 + chunks.length * 2);
const view = new Uint16Array(buffer); const view = new Uint16Array(buffer);

View File

@ -1,7 +1,6 @@
/* /*
* Hooks for renderer * Hooks for renderer
* *
* @flow
*/ */
import { import {

View File

@ -354,6 +354,22 @@ tr:nth-child(even) {
visibility: visible; visibility: visible;
} }
.convBox {
border-style: solid;
border-color: #D4D4D4;
border-width: 2px;
padding: 2px;
display: inline-block;
max-height: 0px;
visibility: hidden;
transition: 0.3s;
overflow: hidden;
}
.convBox.show {
visibility: visible;
max-height: 160px;
}
#helpbutton { #helpbutton {
left: 16px; left: 16px;
top: 180px; top: 180px;

View File

@ -3,123 +3,93 @@
* https://github.com/Fyrestar/THREE.InfiniteGridHelper * https://github.com/Fyrestar/THREE.InfiniteGridHelper
* MIT License * MIT License
* *
* @flow
*/ */
/* eslint-disable max-len */ /* eslint-disable max-len */
import * as THREE from 'three'; import * as THREE from 'three';
const InfiniteGridHelper = function InfiniteGridHelper( export default class InfiniteGridHelper extends THREE.Mesh {
size1, constructor(size1, size2, color, distance, axes = 'xzy') {
size2, color = color || new THREE.Color('white');
color, size1 = size1 || 10;
distance, size2 = size2 || 100;
) {
color = color || new THREE.Color('white');
size1 = size1 || 10;
size2 = size2 || 100;
distance = distance || 8000; distance = distance || 8000;
const planeAxes = axes.substr(0, 2);
const geometry = new THREE.PlaneBufferGeometry(2, 2, 1, 1);
const material = new THREE.ShaderMaterial({
const geometry = new THREE.PlaneBufferGeometry(2, 2, 1, 1); side: THREE.DoubleSide,
const material = new THREE.ShaderMaterial({ uniforms: {
uSize1: { value: size1 },
side: THREE.DoubleSide, uSize2: {
value: size2,
uniforms: { },
uSize1: { uColor: {
value: size1, value: color,
},
uDistance: {
value: distance,
},
}, },
uSize2: { transparent: true,
value: size2,
vertexShader: `
varying vec3 worldPosition;
uniform float uDistance;
void main() {
vec3 pos = position.${axes} * uDistance;
pos.${planeAxes} += cameraPosition.${planeAxes};
worldPosition = pos;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`,
fragmentShader: `
varying vec3 worldPosition;
uniform float uSize1;
uniform float uSize2;
uniform vec3 uColor;
uniform float uDistance;
float getGrid(float size) {
vec2 r = worldPosition.${planeAxes} / size;
vec2 grid = abs(fract(r - 0.5) - 0.5) / fwidth(r);
float line = min(grid.x, grid.y);
return 1.0 - min(line, 1.0);
}
void main() {
float d = 1.0 - min(distance(cameraPosition.${planeAxes}, worldPosition.${planeAxes}) / uDistance, 1.0);
float g1 = getGrid(uSize1);
float g2 = getGrid(uSize2);
gl_FragColor = vec4(uColor.rgb, mix(g2, g1, g1) * pow(d, 3.0));
gl_FragColor.a = mix(0.5 * gl_FragColor.a, gl_FragColor.a, g2);
if ( gl_FragColor.a <= 0.0 ) discard;
}
`,
extensions: {
derivatives: true,
}, },
uColor: { });
value: color,
},
uDistance: {
value: distance,
},
},
transparent: true,
vertexShader: `
varying vec3 worldPosition;
uniform float uDistance;
void main() {
vec3 pos = position.xzy * uDistance;
pos.xz += cameraPosition.xz;
worldPosition = pos;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`,
super(geometry, material);
fragmentShader: ` this.frustumCulled = false;
}
varying vec3 worldPosition; }
uniform float uSize1;
uniform float uSize2;
uniform vec3 uColor;
uniform float uDistance;
float getGrid(float size) {
vec2 r = worldPosition.xz / size;
vec2 grid = abs(fract(r - 0.5) - 0.5) / fwidth(r);
float line = min(grid.x, grid.y);
return 1.0 - min(line, 1.0);
}
void main() {
float d = 1.0 - min(distance(cameraPosition.xz, worldPosition.xz) / uDistance, 1.0);
float g1 = getGrid(uSize1);
float g2 = getGrid(uSize2);
gl_FragColor = vec4(uColor.rgb, mix(g2, g1, g1) * pow(d, 3.0));
gl_FragColor.a = mix(0.5 * gl_FragColor.a, gl_FragColor.a, g2);
if ( gl_FragColor.a <= 0.0 ) discard;
}
`,
extensions: {
derivatives: true,
},
});
THREE.Mesh.call(this, geometry, material);
this.frustumCulled = false;
};
InfiniteGridHelper.prototype = {
...THREE.Mesh.prototype,
...THREE.Object3D.prototype,
...THREE.EventDispatcher.prototype,
};
export default InfiniteGridHelper;

View File

@ -5,7 +5,7 @@
*/ */
import * as THREE from 'three'; import * as THREE from 'three';
import { Sky } from './Sky'; import Sky from './Sky';
import InfiniteGridHelper from './InfiniteGridHelper'; import InfiniteGridHelper from './InfiniteGridHelper';
import VoxelPainterControls from '../controls/VoxelPainterControls'; import VoxelPainterControls from '../controls/VoxelPainterControls';
@ -99,7 +99,6 @@ class Renderer {
rayleigh: 2, rayleigh: 2,
mieCoefficient: 0.005, mieCoefficient: 0.005,
mieDirectionalG: 0.8, mieDirectionalG: 0.8,
luminance: 1,
inclination: 0.49, // elevation / inclination inclination: 0.49, // elevation / inclination
azimuth: 0.25, // Facing front, azimuth: 0.25, // Facing front,
sun: !true, sun: !true,
@ -107,7 +106,6 @@ class Renderer {
const { uniforms } = sky.material; const { uniforms } = sky.material;
uniforms.turbidity.value = effectController.turbidity; uniforms.turbidity.value = effectController.turbidity;
uniforms.rayleigh.value = effectController.rayleigh; uniforms.rayleigh.value = effectController.rayleigh;
uniforms.luminance.value = effectController.luminance;
uniforms.mieCoefficient.value = effectController.mieCoefficient; uniforms.mieCoefficient.value = effectController.mieCoefficient;
uniforms.mieDirectionalG.value = effectController.mieDirectionalG; uniforms.mieDirectionalG.value = effectController.mieDirectionalG;
uniforms.sunPosition.value.set(400000, 400000, 400000); uniforms.sunPosition.value.set(400000, 400000, 400000);

View File

@ -1,12 +1,13 @@
/** /**
* @author zz85 / https://github.com/zz85 * From example code at:
* https://github.com/mrdoob/three.js/blob/dev/examples/js/objects/Sky.js
* *
* Based on "A Practical Analytic Model for Daylight" * Based on "A Practical Analytic Model for Daylight"
* aka The Preetham Model, the de facto standard analytic skydome model * aka The Preetham Model, the de facto standard analytic skydome model
* http://www.cs.utah.edu/~shirley/papers/sunsky/sunsky.pdf * https://www.researchgate.net/publication/220720443_A_Practical_Analytic_Model_for_Daylight
* *
* First implemented by Simon Wallner * First implemented by Simon Wallner
* http://www.simonwallner.at/projects/atmospheric-scattering * http://simonwallner.at/project/atmospheric-scattering/
* *
* Improved by Martin Upitis * Improved by Martin Upitis
* http://blenderartists.org/forum/showthread.php?245954-preethams-sky-impementation-HDR * http://blenderartists.org/forum/showthread.php?245954-preethams-sky-impementation-HDR
@ -14,224 +15,207 @@
* Three.js integration by zz85 http://twitter.com/blurspline * Three.js integration by zz85 http://twitter.com/blurspline
*/ */
/* /* eslint-disable max-len */
* taken from three.js examples
*/
/* eslint-disable */
import { import * as THREE from 'three';
BackSide,
BoxBufferGeometry,
Mesh,
ShaderMaterial,
UniformsUtils,
Vector3
} from 'three';
var Sky = function () { export default class Sky extends THREE.Mesh {
static isSky = true;
var shader = Sky.SkyShader; constructor() {
const shader = Sky.SkyShader;
var material = new ShaderMaterial( { const material = new THREE.ShaderMaterial({
fragmentShader: shader.fragmentShader, name: 'SkyShader',
vertexShader: shader.vertexShader, fragmentShader: shader.fragmentShader,
uniforms: UniformsUtils.clone( shader.uniforms ), vertexShader: shader.vertexShader,
side: BackSide uniforms: THREE.UniformsUtils.clone(shader.uniforms),
} ); side: THREE.BackSide,
depthWrite: false,
Mesh.call( this, new BoxBufferGeometry( 1, 1, 1 ), material ); });
super(new THREE.BoxGeometry(1, 1, 1), material);
}; }
}
Sky.prototype = Object.create( Mesh.prototype );
Sky.SkyShader = { Sky.SkyShader = {
uniforms: {
turbidity: {
value: 2,
},
rayleigh: {
value: 1,
},
mieCoefficient: {
value: 0.005,
},
mieDirectionalG: {
value: 0.8,
},
sunPosition: {
value: new THREE.Vector3(),
},
up: {
value: new THREE.Vector3(0, 1, 0),
},
},
vertexShader:
/* glsl */
`
uniform vec3 sunPosition;
uniform float rayleigh;
uniform float turbidity;
uniform float mieCoefficient;
uniform vec3 up;
uniforms: { varying vec3 vWorldPosition;
"luminance": { value: 1 }, varying vec3 vSunDirection;
"turbidity": { value: 2 }, varying float vSunfade;
"rayleigh": { value: 1 }, varying vec3 vBetaR;
"mieCoefficient": { value: 0.005 }, varying vec3 vBetaM;
"mieDirectionalG": { value: 0.8 }, varying float vSunE;
"sunPosition": { value: new Vector3() },
"up": { value: new Vector3( 0, 1, 0 ) }
},
vertexShader: [ // constants for atmospheric scattering
'uniform vec3 sunPosition;', const float e = 2.71828182845904523536028747135266249775724709369995957;
'uniform float rayleigh;', const float pi = 3.141592653589793238462643383279502884197169;
'uniform float turbidity;',
'uniform float mieCoefficient;',
'uniform vec3 up;',
'varying vec3 vWorldPosition;', // wavelength of used primaries, according to preetham
'varying vec3 vSunDirection;', const vec3 lambda = vec3( 680E-9, 550E-9, 450E-9 );
'varying float vSunfade;', // this pre-calcuation replaces older TotalRayleigh(vec3 lambda) function:
'varying vec3 vBetaR;', // (8.0 * pow(pi, 3.0) * pow(pow(n, 2.0) - 1.0, 2.0) * (6.0 + 3.0 * pn)) / (3.0 * N * pow(lambda, vec3(4.0)) * (6.0 - 7.0 * pn))
'varying vec3 vBetaM;', const vec3 totalRayleigh = vec3( 5.804542996261093E-6, 1.3562911419845635E-5, 3.0265902468824876E-5 );
'varying float vSunE;',
// constants for atmospheric scattering // mie stuff
'const float e = 2.71828182845904523536028747135266249775724709369995957;', // K coefficient for the primaries
'const float pi = 3.141592653589793238462643383279502884197169;', const float v = 4.0;
const vec3 K = vec3( 0.686, 0.678, 0.666 );
// MieConst = pi * pow( ( 2.0 * pi ) / lambda, vec3( v - 2.0 ) ) * K
const vec3 MieConst = vec3( 1.8399918514433978E14, 2.7798023919660528E14, 4.0790479543861094E14 );
// wavelength of used primaries, according to preetham // earth shadow hack
'const vec3 lambda = vec3( 680E-9, 550E-9, 450E-9 );', // cutoffAngle = pi / 1.95;
// this pre-calcuation replaces older TotalRayleigh(vec3 lambda) function: const float cutoffAngle = 1.6110731556870734;
// (8.0 * pow(pi, 3.0) * pow(pow(n, 2.0) - 1.0, 2.0) * (6.0 + 3.0 * pn)) / (3.0 * N * pow(lambda, vec3(4.0)) * (6.0 - 7.0 * pn)) const float steepness = 1.5;
'const vec3 totalRayleigh = vec3( 5.804542996261093E-6, 1.3562911419845635E-5, 3.0265902468824876E-5 );', const float EE = 1000.0;
// mie stuff float sunIntensity( float zenithAngleCos ) {
// K coefficient for the primaries zenithAngleCos = clamp( zenithAngleCos, -1.0, 1.0 );
'const float v = 4.0;', return EE * max( 0.0, 1.0 - pow( e, -( ( cutoffAngle - acos( zenithAngleCos ) ) / steepness ) ) );
'const vec3 K = vec3( 0.686, 0.678, 0.666 );', }
// MieConst = pi * pow( ( 2.0 * pi ) / lambda, vec3( v - 2.0 ) ) * K
'const vec3 MieConst = vec3( 1.8399918514433978E14, 2.7798023919660528E14, 4.0790479543861094E14 );',
// earth shadow hack vec3 totalMie( float T ) {
// cutoffAngle = pi / 1.95; float c = ( 0.2 * T ) * 10E-18;
'const float cutoffAngle = 1.6110731556870734;', return 0.434 * c * MieConst;
'const float steepness = 1.5;', }
'const float EE = 1000.0;',
'float sunIntensity( float zenithAngleCos ) {', void main() {
' zenithAngleCos = clamp( zenithAngleCos, -1.0, 1.0 );',
' return EE * max( 0.0, 1.0 - pow( e, -( ( cutoffAngle - acos( zenithAngleCos ) ) / steepness ) ) );',
'}',
'vec3 totalMie( float T ) {', vec4 worldPosition = modelMatrix * vec4( position, 1.0 );
' float c = ( 0.2 * T ) * 10E-18;', vWorldPosition = worldPosition.xyz;
' return 0.434 * c * MieConst;',
'}',
'void main() {', gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
gl_Position.z = gl_Position.w; // set z to camera.far
' vec4 worldPosition = modelMatrix * vec4( position, 1.0 );', vSunDirection = normalize( sunPosition );
' vWorldPosition = worldPosition.xyz;',
' gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );', vSunE = sunIntensity( dot( vSunDirection, up ) );
' gl_Position.z = gl_Position.w;', // set z to camera.far
' vSunDirection = normalize( sunPosition );', vSunfade = 1.0 - clamp( 1.0 - exp( ( sunPosition.y / 450000.0 ) ), 0.0, 1.0 );
' vSunE = sunIntensity( dot( vSunDirection, up ) );', float rayleighCoefficient = rayleigh - ( 1.0 * ( 1.0 - vSunfade ) );
' vSunfade = 1.0 - clamp( 1.0 - exp( ( sunPosition.y / 450000.0 ) ), 0.0, 1.0 );', // extinction (absorbtion + out scattering)
// rayleigh coefficients
vBetaR = totalRayleigh * rayleighCoefficient;
' float rayleighCoefficient = rayleigh - ( 1.0 * ( 1.0 - vSunfade ) );', // mie coefficients
vBetaM = totalMie( turbidity ) * mieCoefficient;
// extinction (absorbtion + out scattering) }`,
// rayleigh coefficients fragmentShader:
' vBetaR = totalRayleigh * rayleighCoefficient;', /* glsl */
`
varying vec3 vWorldPosition;
varying vec3 vSunDirection;
varying float vSunfade;
varying vec3 vBetaR;
varying vec3 vBetaM;
varying float vSunE;
// mie coefficients uniform float mieDirectionalG;
' vBetaM = totalMie( turbidity ) * mieCoefficient;', uniform vec3 up;
'}' const vec3 cameraPos = vec3( 0.0, 0.0, 0.0 );
].join( '\n' ),
fragmentShader: [ // constants for atmospheric scattering
'varying vec3 vWorldPosition;', const float pi = 3.141592653589793238462643383279502884197169;
'varying vec3 vSunDirection;',
'varying float vSunfade;',
'varying vec3 vBetaR;',
'varying vec3 vBetaM;',
'varying float vSunE;',
'uniform float luminance;', const float n = 1.0003; // refractive index of air
'uniform float mieDirectionalG;', const float N = 2.545E25; // number of molecules per unit volume for air at 288.15K and 1013mb (sea level -45 celsius)
'uniform vec3 up;',
'const vec3 cameraPos = vec3( 0.0, 0.0, 0.0 );', // optical length at zenith for molecules
const float rayleighZenithLength = 8.4E3;
const float mieZenithLength = 1.25E3;
// 66 arc seconds -> degrees, and the cosine of that
const float sunAngularDiameterCos = 0.999956676946448443553574619906976478926848692873900859324;
// constants for atmospheric scattering // 3.0 / ( 16.0 * pi )
'const float pi = 3.141592653589793238462643383279502884197169;', const float THREE_OVER_SIXTEENPI = 0.05968310365946075;
// 1.0 / ( 4.0 * pi )
const float ONE_OVER_FOURPI = 0.07957747154594767;
'const float n = 1.0003;', // refractive index of air float rayleighPhase( float cosTheta ) {
'const float N = 2.545E25;', // number of molecules per unit volume for air at 288.15K and 1013mb (sea level -45 celsius) return THREE_OVER_SIXTEENPI * ( 1.0 + pow( cosTheta, 2.0 ) );
}
// optical length at zenith for molecules float hgPhase( float cosTheta, float g ) {
'const float rayleighZenithLength = 8.4E3;', float g2 = pow( g, 2.0 );
'const float mieZenithLength = 1.25E3;', float inverse = 1.0 / pow( 1.0 - 2.0 * g * cosTheta + g2, 1.5 );
// 66 arc seconds -> degrees, and the cosine of that return ONE_OVER_FOURPI * ( ( 1.0 - g2 ) * inverse );
'const float sunAngularDiameterCos = 0.999956676946448443553574619906976478926848692873900859324;', }
// 3.0 / ( 16.0 * pi ) void main() {
'const float THREE_OVER_SIXTEENPI = 0.05968310365946075;',
// 1.0 / ( 4.0 * pi )
'const float ONE_OVER_FOURPI = 0.07957747154594767;',
'float rayleighPhase( float cosTheta ) {', vec3 direction = normalize( vWorldPosition - cameraPos );
' return THREE_OVER_SIXTEENPI * ( 1.0 + pow( cosTheta, 2.0 ) );',
'}',
'float hgPhase( float cosTheta, float g ) {', // optical length
' float g2 = pow( g, 2.0 );', // cutoff angle at 90 to avoid singularity in next formula.
' float inverse = 1.0 / pow( 1.0 - 2.0 * g * cosTheta + g2, 1.5 );', float zenithAngle = acos( max( 0.0, dot( up, direction ) ) );
' return ONE_OVER_FOURPI * ( ( 1.0 - g2 ) * inverse );', float inverse = 1.0 / ( cos( zenithAngle ) + 0.15 * pow( 93.885 - ( ( zenithAngle * 180.0 ) / pi ), -1.253 ) );
'}', float sR = rayleighZenithLength * inverse;
float sM = mieZenithLength * inverse;
// Filmic ToneMapping http://filmicgames.com/archives/75 // combined extinction factor
'const float A = 0.15;', vec3 Fex = exp( -( vBetaR * sR + vBetaM * sM ) );
'const float B = 0.50;',
'const float C = 0.10;',
'const float D = 0.20;',
'const float E = 0.02;',
'const float F = 0.30;',
'const float whiteScale = 1.0748724675633854;', // 1.0 / Uncharted2Tonemap(1000.0) // in scattering
float cosTheta = dot( direction, vSunDirection );
'vec3 Uncharted2Tonemap( vec3 x ) {', float rPhase = rayleighPhase( cosTheta * 0.5 + 0.5 );
' return ( ( x * ( A * x + C * B ) + D * E ) / ( x * ( A * x + B ) + D * F ) ) - E / F;', vec3 betaRTheta = vBetaR * rPhase;
'}',
float mPhase = hgPhase( cosTheta, mieDirectionalG );
vec3 betaMTheta = vBetaM * mPhase;
'void main() {', vec3 Lin = pow( vSunE * ( ( betaRTheta + betaMTheta ) / ( vBetaR + vBetaM ) ) * ( 1.0 - Fex ), vec3( 1.5 ) );
// optical length Lin *= mix( vec3( 1.0 ), pow( vSunE * ( ( betaRTheta + betaMTheta ) / ( vBetaR + vBetaM ) ) * Fex, vec3( 1.0 / 2.0 ) ), clamp( pow( 1.0 - dot( up, vSunDirection ), 5.0 ), 0.0, 1.0 ) );
// cutoff angle at 90 to avoid singularity in next formula.
' float zenithAngle = acos( max( 0.0, dot( up, normalize( vWorldPosition - cameraPos ) ) ) );',
' float inverse = 1.0 / ( cos( zenithAngle ) + 0.15 * pow( 93.885 - ( ( zenithAngle * 180.0 ) / pi ), -1.253 ) );',
' float sR = rayleighZenithLength * inverse;',
' float sM = mieZenithLength * inverse;',
// combined extinction factor // nightsky
' vec3 Fex = exp( -( vBetaR * sR + vBetaM * sM ) );', float theta = acos( direction.y ); // elevation --> y-axis, [-pi/2, pi/2]
float phi = atan( direction.z, direction.x ); // azimuth --> x-axis [-pi/2, pi/2]
vec2 uv = vec2( phi, theta ) / vec2( 2.0 * pi, pi ) + vec2( 0.5, 0.0 );
vec3 L0 = vec3( 0.1 ) * Fex;
// in scattering // composition + solar disc
' float cosTheta = dot( normalize( vWorldPosition - cameraPos ), vSunDirection );', float sundisk = smoothstep( sunAngularDiameterCos, sunAngularDiameterCos + 0.00002, cosTheta );
L0 += ( vSunE * 19000.0 * Fex ) * sundisk;
' float rPhase = rayleighPhase( cosTheta * 0.5 + 0.5 );', vec3 texColor = ( Lin + L0 ) * 0.04 + vec3( 0.0, 0.0003, 0.00075 );
' vec3 betaRTheta = vBetaR * rPhase;',
' float mPhase = hgPhase( cosTheta, mieDirectionalG );', vec3 retColor = pow( texColor, vec3( 1.0 / ( 1.2 + ( 1.2 * vSunfade ) ) ) );
' vec3 betaMTheta = vBetaM * mPhase;',
' vec3 Lin = pow( vSunE * ( ( betaRTheta + betaMTheta ) / ( vBetaR + vBetaM ) ) * ( 1.0 - Fex ), vec3( 1.5 ) );', gl_FragColor = vec4( retColor, 1.0 );
' Lin *= mix( vec3( 1.0 ), pow( vSunE * ( ( betaRTheta + betaMTheta ) / ( vBetaR + vBetaM ) ) * Fex, vec3( 1.0 / 2.0 ) ), clamp( pow( 1.0 - dot( up, vSunDirection ), 5.0 ), 0.0, 1.0 ) );',
// nightsky #include <tonemapping_fragment>
' vec3 direction = normalize( vWorldPosition - cameraPos );', #include <encodings_fragment>
' float theta = acos( direction.y ); // elevation --> y-axis, [-pi/2, pi/2]',
' float phi = atan( direction.z, direction.x ); // azimuth --> x-axis [-pi/2, pi/2]',
' vec2 uv = vec2( phi, theta ) / vec2( 2.0 * pi, pi ) + vec2( 0.5, 0.0 );',
' vec3 L0 = vec3( 0.1 ) * Fex;',
// composition + solar disc
' float sundisk = smoothstep( sunAngularDiameterCos, sunAngularDiameterCos + 0.00002, cosTheta );',
' L0 += ( vSunE * 19000.0 * Fex ) * sundisk;',
' vec3 texColor = ( Lin + L0 ) * 0.04 + vec3( 0.0, 0.0003, 0.00075 );',
' vec3 curr = Uncharted2Tonemap( ( log2( 2.0 / pow( luminance, 4.0 ) ) ) * texColor );',
' vec3 color = curr * whiteScale;',
' vec3 retColor = pow( color, vec3( 1.0 / ( 1.2 + ( 1.2 * vSunfade ) ) ) );',
' gl_FragColor = vec4( retColor, 1.0 );',
'}'
].join( '\n' )
}`,
}; };
export { Sky };

View File

@ -1,9 +1,3 @@
/**
*
* @flow
*/
export default class Counter<T> { export default class Counter<T> {
map: Map<T, number>; map: Map<T, number>;

View File

@ -1,6 +1,5 @@
/* /*
* rate limiter utils * rate limiter utils
* @flow
*/ */
@ -11,11 +10,11 @@
* @param onCooldown If we force to wait the whole burst time once the limit is reached * @param onCooldown If we force to wait the whole burst time once the limit is reached
*/ */
class RateLimiter { class RateLimiter {
msPerTick: number; msPerTick;
burstTime: number; burstTime;
cooldownCompletely: boolean; cooldownCompletely;
onCooldown: boolean; onCooldown;
wait: number; wait;
constructor(ticksPerMin = 20, burst = 20, cooldownCompletely = false) { constructor(ticksPerMin = 20, burst = 20, cooldownCompletely = false) {
this.wait = Date.now(); this.wait = Date.now();

View File

@ -1,6 +1,6 @@
/** /**
* *
* @flow * check for captcha requirement
*/ */
import logger from '../core/logger'; import logger from '../core/logger';
@ -51,7 +51,7 @@ function evaluateChar(charC, charU) {
* Compare captcha to result * Compare captcha to result
* @return true if same * @return true if same
*/ */
function evaluateResult(captchaText: string, userText: string) { function evaluateResult(captchaText, userText) {
if (captchaText.length !== userText.length) { if (captchaText.length !== userText.length) {
return false; return false;
} }
@ -74,8 +74,8 @@ function evaluateResult(captchaText: string, userText: string) {
* @param ttl time to be valid in seconds * @param ttl time to be valid in seconds
*/ */
export function setCaptchaSolution( export function setCaptchaSolution(
text: string, text,
ip: string, ip,
) { ) {
const key = `capt:${ip}`; const key = `capt:${ip}`;
return redis.setAsync(key, text, 'EX', CAPTCHA_TIMEOUT); return redis.setAsync(key, text, 'EX', CAPTCHA_TIMEOUT);
@ -91,8 +91,8 @@ export function setCaptchaSolution(
* 2 if wrong * 2 if wrong
*/ */
export async function checkCaptchaSolution( export async function checkCaptchaSolution(
text: string, text,
ip: string, ip,
) { ) {
const ipn = getIPv6Subnet(ip); const ipn = getIPv6Subnet(ip);
const key = `capt:${ip}`; const key = `capt:${ip}`;
@ -119,13 +119,13 @@ export async function checkCaptchaSolution(
* @param ip * @param ip
* @return boolean true if needed * @return boolean true if needed
*/ */
export async function needCaptcha(ip: string) { export async function needCaptcha(ip) {
if (!CAPTCHA_URL) { if (!CAPTCHA_URL) {
return false; return false;
} }
const key = `human:${getIPv6Subnet(ip)}`; const key = `human:${getIPv6Subnet(ip)}`;
const ttl: number = await redis.ttlAsync(key); const ttl = await redis.ttlAsync(key);
if (ttl > 0) { if (ttl > 0) {
return false; return false;
} }

View File

@ -1,6 +1,3 @@
/* @flow
*/
function fallbackCopyTextToClipboard(text) { function fallbackCopyTextToClipboard(text) {
const textArea = document.createElement('textarea'); const textArea = document.createElement('textarea');
textArea.value = text; textArea.value = text;

View File

@ -1,15 +1,14 @@
/* /*
* password hashing * password hashing
* @flow
*/ */
import bcrypt from 'bcrypt'; import bcrypt from 'bcrypt';
export function generateHash(password: string) { export function generateHash(password) {
return bcrypt.hashSync(password, bcrypt.genSaltSync(8), null); return bcrypt.hashSync(password, bcrypt.genSaltSync(8), null);
} }
export function compareToHash(password: string, hash: string) { export function compareToHash(password, hash) {
if (!password || !hash) return false; if (!password || !hash) return false;
return bcrypt.compareSync(password, hash); return bcrypt.compareSync(password, hash);
} }

View File

@ -7,6 +7,9 @@
import { utils, distance, image } from 'image-q'; import { utils, distance, image } from 'image-q';
/*
* available color distance calculators
*/
export const ColorDistanceCalculators = [ export const ColorDistanceCalculators = [
'Euclidean', 'Euclidean',
'Manhattan', 'Manhattan',
@ -21,6 +24,9 @@ export const ColorDistanceCalculators = [
'ManhattanNommyde', 'ManhattanNommyde',
]; ];
/*
* available dithers
*/
export const ImageQuantizerKernels = [ export const ImageQuantizerKernels = [
'Nearest', 'Nearest',
'Riemersma', 'Riemersma',
@ -35,7 +41,74 @@ export const ImageQuantizerKernels = [
'SierraLite', 'SierraLite',
]; ];
export function getImageDataOfFile(file) { export function addGrid(imgCanvas, lightGrid, offsetX, offsetY) {
const { width, height } = imgCanvas;
const can = document.createElement('canvas');
const ctx = can.getContext('2d');
can.width = width * 5;
can.height = height * 5;
ctx.imageSmoothingEnabled = false;
ctx.mozImageSmoothingEnabled = false;
ctx.webkitImageSmoothingEnabled = false;
ctx.msImageSmoothingEnabled = false;
ctx.save();
ctx.scale(5.0, 5.0);
ctx.drawImage(imgCanvas, 0, 0);
ctx.restore();
ctx.fillStyle = (lightGrid) ? '#DDDDDD' : '#222222';
for (let i = 0; i <= width; i += 1) {
const thick = ((i + (offsetX * 1)) % 10 === 0) ? 2 : 1;
ctx.fillRect(i * 5, 0, thick, can.height);
}
for (let j = 0; j <= height; j += 1) {
const thick = ((j + (offsetY * 1)) % 10 === 0) ? 2 : 1;
ctx.fillRect(0, j * 5, can.width, thick);
}
return can;
}
export function scaleImage(imgCanvas, width, height, doAA) {
const can = document.createElement('canvas');
can.width = width;
can.height = height;
const ctxo = can.getContext('2d');
const scaleX = width / imgCanvas.width;
const scaleY = height / imgCanvas.height;
if (doAA) {
// scale with canvas for antialiasing
ctxo.save();
ctxo.scale(scaleX, scaleY);
ctxo.drawImage(imgCanvas, 0, 0);
ctxo.restore();
} else {
// scale manually
const ctxi = imgCanvas.getContext('2d');
const imdi = ctxi.getImageData(0, 0, imgCanvas.width, imgCanvas.height);
const imdo = ctxo.createImageData(width, height);
const { data: datai } = imdi;
const { data: datao } = imdo;
for (let y = 0; y < height; y += 1) {
for (let x = 0; x < width; x += 1) {
let posi = (Math.round(x / scaleX) + Math.round(y / scaleY)
* imgCanvas.width) * 4;
let poso = (x + y * width) * 4;
datao[poso++] = datai[posi++];
datao[poso++] = datai[posi++];
datao[poso++] = datai[posi++];
datao[poso] = datai[posi];
}
}
ctxo.putImageData(imdo, 0, 0);
}
return can;
}
/*
* read File object into canvas
* @param file
* @return HTMLCanvas
*/
export function readFileIntoCanvas(file) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const fr = new FileReader(); const fr = new FileReader();
fr.onload = () => { fr.onload = () => {
@ -46,8 +119,7 @@ export function getImageDataOfFile(file) {
cani.height = img.height; cani.height = img.height;
const ctxi = cani.getContext('2d'); const ctxi = cani.getContext('2d');
ctxi.drawImage(img, 0, 0); ctxi.drawImage(img, 0, 0);
const imdi = ctxi.getImageData(0, 0, img.width, img.height); resolve(cani);
resolve(imdi);
}; };
img.onerror = (error) => reject(error); img.onerror = (error) => reject(error);
img.src = fr.result; img.src = fr.result;
@ -57,49 +129,12 @@ export function getImageDataOfFile(file) {
}); });
} }
function* loadData(data, pointArray) { /*
let i = 0; * converts pointContainer to HTMLCanvas
while (i < data.length) { * @param pointContainer
/* * @return HTMLCanvas
if (!(i % 80)) { */
yield Math.floor(i / data.length); function createCanvasFromPointContainer(pointContainer) {
}
*/
const point = utils.Point.createByRGBA(
data[i++],
data[i++],
data[i++],
data[i++],
);
pointArray.push(point);
}
}
async function createPointContainerFromImageData(imageData, onProgress) {
return new Promise((resolve) => {
const { width, height, data } = imageData;
const pointContainer = new utils.PointContainer();
pointContainer.setWidth(width);
pointContainer.setHeight(height);
const pointArray = pointContainer.getPointArray();
const iterator = loadData(data, pointArray);
const next = () => {
const result = iterator.next();
if (result.done) {
resolve(pointContainer);
} else {
if (onProgress) {
onProgress(result.value);
}
setTimeout(next, 0);
}
};
setTimeout(next, 0);
});
}
function createImageDataFromPointContainer(pointContainer) {
const width = pointContainer.getWidth(); const width = pointContainer.getWidth();
const height = pointContainer.getHeight(); const height = pointContainer.getHeight();
const data = pointContainer.toUint8Array(); const data = pointContainer.toUint8Array();
@ -109,9 +144,24 @@ function createImageDataFromPointContainer(pointContainer) {
const ctx = can.getContext('2d'); const ctx = can.getContext('2d');
const idata = ctx.createImageData(width, height); const idata = ctx.createImageData(width, height);
idata.data.set(data); idata.data.set(data);
return idata; ctx.putImageData(idata, 0, 0);
return can;
} }
/*
* quantizes point container
* @param colors Array of [r, g, b] color Arrays
* @param pointContainer pointContainer of input image
* @param opts Object with configuration:
* strategy: String of dithering strategy (ImageQuantizerKernels)
* colorDist: String of color distance calc (ColorDistanceCalculators)
* onProgress: function that gets called with integer of progress percentage
* and only available for not Nearest or Riemersma
* serpentine: type of dithering
* minColorDistance: minimal distance on which we start to dither
* GIMPerror: calculate error like GIMP
* @return Promise that resolves to HTMLCanvasElement of output image
*/
function quantizePointContainer(colors, pointContainer, opts) { function quantizePointContainer(colors, pointContainer, opts) {
const strategy = opts.strategy || 'Nearest'; const strategy = opts.strategy || 'Nearest';
const colorDist = opts.colorDist || 'Euclidean'; const colorDist = opts.colorDist || 'Euclidean';
@ -164,7 +214,7 @@ function quantizePointContainer(colors, pointContainer, opts) {
default: default:
distCalc = new distance.Euclidean(); distCalc = new distance.Euclidean();
} }
// idk why i need this :/ // could be needed for some reason sometimes
// eslint-disable-next-line no-underscore-dangle // eslint-disable-next-line no-underscore-dangle
if (distCalc._setDefaults) distCalc._setDefaults(); if (distCalc._setDefaults) distCalc._setDefaults();
// construct image quantizer // construct image quantizer
@ -174,12 +224,23 @@ function quantizePointContainer(colors, pointContainer, opts) {
} else if (strategy === 'Riemersma') { } else if (strategy === 'Riemersma') {
imageQuantizer = new image.ErrorDiffusionRiemersma(distCalc); imageQuantizer = new image.ErrorDiffusionRiemersma(distCalc);
} else { } else {
// minColorDistance is a percentage
let minColorDistance = 0;
// eslint-disable-next-line no-prototype-builtins
if (opts.hasOwnProperty('minColorDistance')) {
const mcdNumber = Number(opts.minColorDistance);
if (!Number.isNaN(mcdNumber)) {
minColorDistance = mcdNumber / 100 * 0.2;
}
}
imageQuantizer = new image.ErrorDiffusionArray( imageQuantizer = new image.ErrorDiffusionArray(
distCalc, distCalc,
image.ErrorDiffusionArrayKernel[strategy], image.ErrorDiffusionArrayKernel[strategy],
true, // eslint-disable-next-line no-prototype-builtins
0, opts.hasOwnProperty('serpentine') ? opts.serpentine : true,
false, minColorDistance,
!!opts.GIMPerror,
); );
} }
// quantize // quantize
@ -188,7 +249,7 @@ function quantizePointContainer(colors, pointContainer, opts) {
try { try {
const result = iterator.next(); const result = iterator.next();
if (result.done) { if (result.done) {
resolve(createImageDataFromPointContainer(pointContainer)); resolve(createCanvasFromPointContainer(pointContainer));
} else { } else {
if (result.value.pointContainer) { if (result.value.pointContainer) {
pointContainer = result.value.pointContainer; pointContainer = result.value.pointContainer;
@ -208,17 +269,17 @@ function quantizePointContainer(colors, pointContainer, opts) {
}); });
} }
export function quantizeImage(colors, imageData, opts) { /*
return createPointContainerFromImageData( * quantize HTMLCanvas to palette
imageData, * see quantizePointContainer for parameter meanings
(value) => { */
if (opts.onProgress) { export function quantizeImage(colors, imageCanvas, opts) {
opts.onProgress(value); const pointContainer = utils.PointContainer.fromHTMLCanvasElement(
} imageCanvas,
}, );
).then((pointContainer) => quantizePointContainer( return quantizePointContainer(
colors, colors,
pointContainer, pointContainer,
opts, opts,
)); );
} }