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",
"parserOptions": {
"babelOptions":{
"rootMode": "upward"
},
"ecmaFeatures": {
"jsx": true
}

View File

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

View File

@ -1,6 +1,5 @@
/**
*
* @flow
* Converts images to canvas palettes
*/
import React, { useState, useEffect } from 'react';
@ -11,8 +10,10 @@ import { jt, t } from 'ttag';
import {
ColorDistanceCalculators,
ImageQuantizerKernels,
readFileIntoCanvas,
scaleImage,
quantizeImage,
getImageDataOfFile,
addGrid,
} from '../utils/image';
import printGIMPPalette from '../core/exportGPL';
import { copyCanvasToClipboard } from '../utils/clipboard';
@ -23,119 +24,45 @@ function downloadOutput() {
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 rendering = false;
async function renderOutputImage(opts) {
if (!opts.file) {
if (!opts.imgCanvas) {
return;
}
renderOpts = opts;
if (rendering) {
console.log('skip rendering');
return;
}
console.log('render');
rendering = true;
while (renderOpts) {
const {
file, dither, grid, scaling,
colors, imgCanvas, ditherOpts, grid, scaling,
} = renderOpts;
renderOpts = null;
if (file) {
let image = file;
if (imgCanvas) {
let image = imgCanvas;
if (scaling.enabled) {
// scale
const { width, height, aa } = scaling;
image = scaleImage(
file,
imgCanvas,
width,
height,
aa,
);
}
// dither
const { colors, strategy, colorDist } = dither;
const progEl = document.getElementById('qprog');
progEl.innerText = 'Loading...';
// eslint-disable-next-line no-await-in-loop
image = await quantizeImage(colors, image, {
strategy,
colorDist,
...ditherOpts,
onProgress: (progress) => {
progEl.innerHTML = `Loading... ${Math.round(progress)} %`;
progEl.innerText = `Loading... ${Math.round(progress)} %`;
},
});
progEl.innerHTML = 'Done';
progEl.innerText = 'Done';
// grid
if (grid.enabled) {
const { light, offsetX, offsetY } = grid;
@ -151,7 +78,7 @@ async function renderOutputImage(opts) {
output.width = image.width;
output.height = image.height;
const ctx = output.getContext('2d');
ctx.putImageData(image, 0, 0);
ctx.drawImage(image, 0, 0);
}
}
rendering = false;
@ -170,10 +97,15 @@ function Converter() {
], shallowEqual);
const [selectedCanvas, selectCanvas] = useState(canvasId);
const [inputImageData, setInputImageData] = useState(null);
const [inputImageCanvas, setInputImageCanvas] = useState(null);
const [selectedStrategy, selectStrategy] = useState('Nearest');
const [selectedColorDist, selectColorDist] = useState('Euclidean');
const [selectedScaleKeepRatio, selectScaleKeepRatio] = useState(true);
const [extraOpts, setExtraOpts] = useState({
serpentine: true,
minColorDistance: 0,
GIMPerror: false,
});
const [scaleData, setScaleData] = useState({
enabled: false,
width: 10,
@ -181,36 +113,45 @@ function Converter() {
aa: true,
});
const [gridData, setGridData] = useState({
enabled: true,
enabled: false,
light: false,
offsetX: 0,
offsetY: 0,
});
const [extraRender, setExtraRender] = useState(false);
const [gridRender, setGridRender] = useState(false);
const [scalingRender, setScalingRender] = useState(false);
useEffect(() => {
if (inputImageData) {
if (inputImageCanvas) {
const canvas = canvases[selectedCanvas];
const dither = {
colors: canvas.colors.slice(canvas.cli),
strategy: selectedStrategy,
colorDist: selectedColorDist,
};
renderOutputImage({
file: inputImageData,
dither,
colors: canvas.colors.slice(canvas.cli),
imgCanvas: inputImageCanvas,
ditherOpts: {
strategy: selectedStrategy,
colorDist: selectedColorDist,
...extraOpts,
},
grid: gridData,
scaling: scaleData,
});
}
}, [
inputImageData,
selectedCanvas,
inputImageCanvas,
selectedStrategy,
selectedColorDist,
extraOpts,
scaleData,
selectedCanvas,
gridData,
]);
const {
serpentine,
minColorDistance,
GIMPerror,
} = extraOpts;
const {
enabled: gridEnabled,
light: gridLight,
@ -224,6 +165,24 @@ function Converter() {
aa: scalingAA,
} = 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 starhouseLink = (
<a href="https://twitter.com/starhousedev">
@ -291,15 +250,15 @@ function Converter() {
const fileSel = evt.target;
const file = (!fileSel.files || !fileSel.files[0])
? null : fileSel.files[0];
const imageData = await getImageDataOfFile(file);
setInputImageData(null);
const imageData = await readFileIntoCanvas(file);
setInputImageCanvas(null);
setScaleData({
enabled: false,
width: imageData.width,
height: imageData.height,
aa: true,
});
setInputImageData(imageData);
setInputImageCanvas(imageData);
}}
/>
<p className="modalcotext">{t`Choose Strategy`}:&nbsp;
@ -319,6 +278,59 @@ function Converter() {
}
</select>
</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;
<select
value={selectedColorDist}
@ -349,64 +361,60 @@ function Converter() {
/>
{t`Add Grid (uncheck if you need a 1:1 template)`}
</p>
{(gridEnabled)
? (
<div style={{
borderStyle: 'solid',
borderColor: '#D4D4D4',
borderWidth: 2,
padding: 5,
display: 'inline-block',
{(gridEnabled || gridRender) && (
<div
className={(gridEnabled && gridRender) ? 'convBox show' : 'convBox'}
onTransitionEnd={() => {
if (!gridEnabled) setGridRender(false);
}}
>
<p style={{ fontHeight: 16 }} className="modalcotext">
<input
type="checkbox"
checked={gridLight}
onChange={(e) => {
setGridData({
...gridData,
light: e.target.checked,
});
}}
/>
{t`Light Grid`}
</p>
<span className="modalcotext">{t`Offset`} X:&nbsp;
<input
type="number"
step="1"
min="0"
max="10"
style={{ width: '2em' }}
value={gridOffsetX}
onChange={(e) => {
setGridData({
...gridData,
offsetX: e.target.value,
});
}}
/>&nbsp;
</span>
<span className="modalcotext">{t`Offset`} Y:&nbsp;
<input
type="number"
step="1"
min="0"
max="10"
style={{ width: '2em' }}
value={gridOffsetY}
onChange={(e) => {
setGridData({
...gridData,
offsetY: e.target.value,
});
}}
/>
</span>
</div>
)
: null}
>
<p style={{ fontHeight: 16 }} className="modalcotext">
<input
type="checkbox"
checked={gridLight}
onChange={(e) => {
setGridData({
...gridData,
light: e.target.checked,
});
}}
/>
{t`Light Grid`}
</p>
<span className="modalcotext">{t`Offset`} X:&nbsp;
<input
type="number"
step="1"
min="0"
max="10"
style={{ width: '2em' }}
value={gridOffsetX}
onChange={(e) => {
setGridData({
...gridData,
offsetX: e.target.value,
});
}}
/>&nbsp;
</span>
<span className="modalcotext">{t`Offset`} Y:&nbsp;
<input
type="number"
step="1"
min="0"
max="10"
style={{ width: '2em' }}
value={gridOffsetY}
onChange={(e) => {
setGridData({
...gridData,
offsetY: e.target.value,
});
}}
/>
</span>
</div>
)}
<p style={{ fontHeight: 16 }} className="modalcotext">
<input
type="checkbox"
@ -420,117 +428,117 @@ function Converter() {
/>
{t`Scale Image`}
</p>
{(scalingEnabled)
? (
<div style={{
borderStyle: 'solid',
borderColor: '#D4D4D4',
borderWidth: 2,
padding: 5,
display: 'inline-block',
{(scalingEnabled || scalingRender) && (
<div
className={(scalingEnabled && scalingRender)
? 'convBox show'
: 'convBox'}
onTransitionEnd={() => {
if (!scalingEnabled) setScalingRender(false);
}}
>
<span className="modalcotext">{t`Width`}:&nbsp;
<input
type="number"
step="1"
min="1"
max="1024"
style={{ width: '3em' }}
value={scalingWidth}
onChange={(e) => {
const newWidth = (e.target.value > 1024)
? 1024 : e.target.value;
if (!newWidth) return;
if (selectedScaleKeepRatio && inputImageData) {
const ratio = inputImageData.width / inputImageData.height;
const newHeight = Math.round(newWidth / ratio);
if (newHeight <= 0) return;
setScaleData({
...scaleData,
width: newWidth,
height: newHeight,
});
return;
}
>
<span className="modalcotext">{t`Width`}:&nbsp;
<input
type="number"
step="1"
min="1"
max="1024"
style={{ width: '3em' }}
value={scalingWidth}
onChange={(e) => {
const newWidth = (e.target.value > 1024)
? 1024 : e.target.value;
if (!newWidth) return;
if (selectedScaleKeepRatio && inputImageCanvas) {
const ratio = inputImageCanvas.width
/ inputImageCanvas.height;
const newHeight = Math.round(newWidth / ratio);
if (newHeight <= 0) return;
setScaleData({
...scaleData,
width: newWidth,
height: newHeight,
});
}}
/>&nbsp;
</span>
<span className="modalcotext">{t`Height`}:&nbsp;
<input
type="number"
step="1"
min="1"
max="1024"
style={{ width: '3em' }}
value={scalingHeight}
onChange={(e) => {
const nuHeight = (e.target.value > 1024)
? 1024 : e.target.value;
if (!nuHeight) return;
if (selectedScaleKeepRatio && inputImageData) {
const ratio = inputImageData.width / inputImageData.height;
const nuWidth = Math.round(ratio * nuHeight);
if (nuWidth <= 0) return;
setScaleData({
...scaleData,
width: nuWidth,
height: nuHeight,
});
return;
}
return;
}
setScaleData({
...scaleData,
width: newWidth,
});
}}
/>&nbsp;
</span>
<span className="modalcotext">{t`Height`}:&nbsp;
<input
type="number"
step="1"
min="1"
max="1024"
style={{ width: '3em' }}
value={scalingHeight}
onChange={(e) => {
const nuHeight = (e.target.value > 1024)
? 1024 : e.target.value;
if (!nuHeight) return;
if (selectedScaleKeepRatio && inputImageCanvas) {
const ratio = inputImageCanvas.width
/ inputImageCanvas.height;
const nuWidth = Math.round(ratio * nuHeight);
if (nuWidth <= 0) return;
setScaleData({
...scaleData,
width: nuWidth,
height: nuHeight,
});
}}
/>
</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,
});
return;
}
setScaleData({
...scaleData,
height: nuHeight,
});
}}
>
{t`Reset`}
</button>
</div>
)
: null}
{(inputImageData)
/>
</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 (inputImageCanvas) {
setScaleData({
...scaleData,
width: inputImageCanvas.width,
height: inputImageCanvas.height,
});
}
}}
>
{t`Reset`}
</button>
</div>
)}
{(inputImageCanvas)
? (
<div>
<p id="qprog">...</p>

View File

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

View File

@ -26,7 +26,7 @@ const UserMessages = () => {
&& (
<p className="usermessages">
{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)
? (
<span

View File

@ -828,7 +828,7 @@ class VoxelPainterControls extends EventDispatcher {
// so camera.up is the orbit axis
const quat = new Quaternion()
.setFromUnitVectors(object.up, new Vector3(0, 1, 0));
const quatInverse = quat.clone().inverse();
const quatInverse = quat.clone().invert();
const lastPosition = new Vector3();
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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,22 +1,18 @@
/*
* Packet for sending and receiving pixels per chunk
* Multiple pixels can be sent at once
* Client side
*
* @flow
*/
type PixelUpdatePacket = {
x: number,
y: number,
pixels: Array,
};
const OP_CODE = 0xC1;
export default {
OP_CODE,
hydrate(data: DataView): PixelUpdatePacket {
/*
* @param data DataVies
*/
hydrate(data) {
/*
* 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 view = new DataView(buffer);
view.setUint8(0, OP_CODE);

View File

@ -1,17 +1,15 @@
/* @flow */
type PixelUpdatePacket = {
x: number,
y: number,
pixels: Array,
};
/*
* Packet for sending and receiving pixels per chunk
* Multiple pixels can be sent at once
* Server side.
*
* */
const OP_CODE = 0xC1;
export default {
OP_CODE,
hydrate(data: Buffer): PixelUpdatePacket {
hydrate(data) {
/*
* chunk coordinates
*/
@ -45,7 +43,7 @@ export default {
* @param chunkId id consisting of chunk coordinates
* @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]);
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;
export default {
OP_CODE,
hydrate(data: Buffer) {
// SERVER (Client)
hydrate(data) {
// SERVER (Receiver)
const canvasId = data[1];
return canvasId;
},
dehydrate(canvasId): ArrayBuffer {
dehydrate(canvasId) {
// CLIENT (Sender)
const buffer = new ArrayBuffer(1 + 1);
const view = new DataView(buffer);

View File

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

View File

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

View File

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

View File

@ -354,6 +354,22 @@ tr:nth-child(even) {
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 {
left: 16px;
top: 180px;

View File

@ -3,123 +3,93 @@
* https://github.com/Fyrestar/THREE.InfiniteGridHelper
* MIT License
*
* @flow
*/
/* eslint-disable max-len */
import * as THREE from 'three';
const InfiniteGridHelper = function InfiniteGridHelper(
size1,
size2,
color,
distance,
) {
color = color || new THREE.Color('white');
size1 = size1 || 10;
size2 = size2 || 100;
export default class InfiniteGridHelper extends THREE.Mesh {
constructor(size1, size2, color, distance, axes = 'xzy') {
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({
side: THREE.DoubleSide,
uniforms: {
uSize1: {
value: size1,
uniforms: {
uSize1: { value: size1 },
uSize2: {
value: size2,
},
uColor: {
value: color,
},
uDistance: {
value: distance,
},
},
uSize2: {
value: size2,
transparent: true,
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: `
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;
this.frustumCulled = false;
}
}

View File

@ -5,7 +5,7 @@
*/
import * as THREE from 'three';
import { Sky } from './Sky';
import Sky from './Sky';
import InfiniteGridHelper from './InfiniteGridHelper';
import VoxelPainterControls from '../controls/VoxelPainterControls';
@ -99,7 +99,6 @@ class Renderer {
rayleigh: 2,
mieCoefficient: 0.005,
mieDirectionalG: 0.8,
luminance: 1,
inclination: 0.49, // elevation / inclination
azimuth: 0.25, // Facing front,
sun: !true,
@ -107,7 +106,6 @@ class Renderer {
const { uniforms } = sky.material;
uniforms.turbidity.value = effectController.turbidity;
uniforms.rayleigh.value = effectController.rayleigh;
uniforms.luminance.value = effectController.luminance;
uniforms.mieCoefficient.value = effectController.mieCoefficient;
uniforms.mieDirectionalG.value = effectController.mieDirectionalG;
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"
* 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
* http://www.simonwallner.at/projects/atmospheric-scattering
* http://simonwallner.at/project/atmospheric-scattering/
*
* Improved by Martin Upitis
* http://blenderartists.org/forum/showthread.php?245954-preethams-sky-impementation-HDR
@ -14,224 +15,207 @@
* Three.js integration by zz85 http://twitter.com/blurspline
*/
/*
* taken from three.js examples
*/
/* eslint-disable */
/* eslint-disable max-len */
import {
BackSide,
BoxBufferGeometry,
Mesh,
ShaderMaterial,
UniformsUtils,
Vector3
} from 'three';
import * as THREE from 'three';
var Sky = function () {
export default class Sky extends THREE.Mesh {
static isSky = true;
var shader = Sky.SkyShader;
var material = new ShaderMaterial( {
fragmentShader: shader.fragmentShader,
vertexShader: shader.vertexShader,
uniforms: UniformsUtils.clone( shader.uniforms ),
side: BackSide
} );
Mesh.call( this, new BoxBufferGeometry( 1, 1, 1 ), material );
};
Sky.prototype = Object.create( Mesh.prototype );
constructor() {
const shader = Sky.SkyShader;
const material = new THREE.ShaderMaterial({
name: 'SkyShader',
fragmentShader: shader.fragmentShader,
vertexShader: shader.vertexShader,
uniforms: THREE.UniformsUtils.clone(shader.uniforms),
side: THREE.BackSide,
depthWrite: false,
});
super(new THREE.BoxGeometry(1, 1, 1), material);
}
}
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: {
"luminance": { value: 1 },
"turbidity": { value: 2 },
"rayleigh": { value: 1 },
"mieCoefficient": { value: 0.005 },
"mieDirectionalG": { value: 0.8 },
"sunPosition": { value: new Vector3() },
"up": { value: new Vector3( 0, 1, 0 ) }
},
varying vec3 vWorldPosition;
varying vec3 vSunDirection;
varying float vSunfade;
varying vec3 vBetaR;
varying vec3 vBetaM;
varying float vSunE;
vertexShader: [
'uniform vec3 sunPosition;',
'uniform float rayleigh;',
'uniform float turbidity;',
'uniform float mieCoefficient;',
'uniform vec3 up;',
// constants for atmospheric scattering
const float e = 2.71828182845904523536028747135266249775724709369995957;
const float pi = 3.141592653589793238462643383279502884197169;
'varying vec3 vWorldPosition;',
'varying vec3 vSunDirection;',
'varying float vSunfade;',
'varying vec3 vBetaR;',
'varying vec3 vBetaM;',
'varying float vSunE;',
// wavelength of used primaries, according to preetham
const vec3 lambda = vec3( 680E-9, 550E-9, 450E-9 );
// this pre-calcuation replaces older TotalRayleigh(vec3 lambda) function:
// (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 vec3 totalRayleigh = vec3( 5.804542996261093E-6, 1.3562911419845635E-5, 3.0265902468824876E-5 );
// constants for atmospheric scattering
'const float e = 2.71828182845904523536028747135266249775724709369995957;',
'const float pi = 3.141592653589793238462643383279502884197169;',
// mie stuff
// K coefficient for the primaries
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
'const vec3 lambda = vec3( 680E-9, 550E-9, 450E-9 );',
// this pre-calcuation replaces older TotalRayleigh(vec3 lambda) function:
// (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 vec3 totalRayleigh = vec3( 5.804542996261093E-6, 1.3562911419845635E-5, 3.0265902468824876E-5 );',
// earth shadow hack
// cutoffAngle = pi / 1.95;
const float cutoffAngle = 1.6110731556870734;
const float steepness = 1.5;
const float EE = 1000.0;
// mie stuff
// K coefficient for the primaries
'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 );',
float sunIntensity( float zenithAngleCos ) {
zenithAngleCos = clamp( zenithAngleCos, -1.0, 1.0 );
return EE * max( 0.0, 1.0 - pow( e, -( ( cutoffAngle - acos( zenithAngleCos ) ) / steepness ) ) );
}
// earth shadow hack
// cutoffAngle = pi / 1.95;
'const float cutoffAngle = 1.6110731556870734;',
'const float steepness = 1.5;',
'const float EE = 1000.0;',
vec3 totalMie( float T ) {
float c = ( 0.2 * T ) * 10E-18;
return 0.434 * c * MieConst;
}
'float sunIntensity( float zenithAngleCos ) {',
' zenithAngleCos = clamp( zenithAngleCos, -1.0, 1.0 );',
' return EE * max( 0.0, 1.0 - pow( e, -( ( cutoffAngle - acos( zenithAngleCos ) ) / steepness ) ) );',
'}',
void main() {
'vec3 totalMie( float T ) {',
' float c = ( 0.2 * T ) * 10E-18;',
' return 0.434 * c * MieConst;',
'}',
vec4 worldPosition = modelMatrix * vec4( position, 1.0 );
vWorldPosition = worldPosition.xyz;
'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 );',
' vWorldPosition = worldPosition.xyz;',
vSunDirection = normalize( sunPosition );
' gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );',
' gl_Position.z = gl_Position.w;', // set z to camera.far
vSunE = sunIntensity( dot( vSunDirection, up ) );
' 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
' vBetaR = totalRayleigh * rayleighCoefficient;',
}`,
fragmentShader:
/* glsl */
`
varying vec3 vWorldPosition;
varying vec3 vSunDirection;
varying float vSunfade;
varying vec3 vBetaR;
varying vec3 vBetaM;
varying float vSunE;
// mie coefficients
' vBetaM = totalMie( turbidity ) * mieCoefficient;',
uniform float mieDirectionalG;
uniform vec3 up;
'}'
].join( '\n' ),
const vec3 cameraPos = vec3( 0.0, 0.0, 0.0 );
fragmentShader: [
'varying vec3 vWorldPosition;',
'varying vec3 vSunDirection;',
'varying float vSunfade;',
'varying vec3 vBetaR;',
'varying vec3 vBetaM;',
'varying float vSunE;',
// constants for atmospheric scattering
const float pi = 3.141592653589793238462643383279502884197169;
'uniform float luminance;',
'uniform float mieDirectionalG;',
'uniform vec3 up;',
const float n = 1.0003; // refractive index of air
const float N = 2.545E25; // number of molecules per unit volume for air at 288.15K and 1013mb (sea level -45 celsius)
'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
'const float pi = 3.141592653589793238462643383279502884197169;',
// 3.0 / ( 16.0 * pi )
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
'const float N = 2.545E25;', // number of molecules per unit volume for air at 288.15K and 1013mb (sea level -45 celsius)
float rayleighPhase( float cosTheta ) {
return THREE_OVER_SIXTEENPI * ( 1.0 + pow( cosTheta, 2.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;',
float hgPhase( float cosTheta, float g ) {
float g2 = pow( g, 2.0 );
float inverse = 1.0 / pow( 1.0 - 2.0 * g * cosTheta + g2, 1.5 );
return ONE_OVER_FOURPI * ( ( 1.0 - g2 ) * inverse );
}
// 3.0 / ( 16.0 * pi )
'const float THREE_OVER_SIXTEENPI = 0.05968310365946075;',
// 1.0 / ( 4.0 * pi )
'const float ONE_OVER_FOURPI = 0.07957747154594767;',
void main() {
'float rayleighPhase( float cosTheta ) {',
' return THREE_OVER_SIXTEENPI * ( 1.0 + pow( cosTheta, 2.0 ) );',
'}',
vec3 direction = normalize( vWorldPosition - cameraPos );
'float hgPhase( float cosTheta, float g ) {',
' float g2 = pow( g, 2.0 );',
' float inverse = 1.0 / pow( 1.0 - 2.0 * g * cosTheta + g2, 1.5 );',
' return ONE_OVER_FOURPI * ( ( 1.0 - g2 ) * inverse );',
'}',
// optical length
// cutoff angle at 90 to avoid singularity in next formula.
float zenithAngle = acos( max( 0.0, dot( up, direction ) ) );
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
'const float A = 0.15;',
'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;',
// combined extinction factor
vec3 Fex = exp( -( vBetaR * sR + vBetaM * sM ) );
'const float whiteScale = 1.0748724675633854;', // 1.0 / Uncharted2Tonemap(1000.0)
// in scattering
float cosTheta = dot( direction, vSunDirection );
'vec3 Uncharted2Tonemap( vec3 x ) {',
' return ( ( x * ( A * x + C * B ) + D * E ) / ( x * ( A * x + B ) + D * F ) ) - E / F;',
'}',
float rPhase = rayleighPhase( cosTheta * 0.5 + 0.5 );
vec3 betaRTheta = vBetaR * rPhase;
float mPhase = hgPhase( cosTheta, mieDirectionalG );
vec3 betaMTheta = vBetaM * mPhase;
'void main() {',
// optical length
// 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;',
vec3 Lin = pow( vSunE * ( ( betaRTheta + betaMTheta ) / ( vBetaR + vBetaM ) ) * ( 1.0 - Fex ), vec3( 1.5 ) );
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 ) );
// combined extinction factor
' vec3 Fex = exp( -( vBetaR * sR + vBetaM * sM ) );',
// nightsky
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
' float cosTheta = dot( normalize( vWorldPosition - cameraPos ), vSunDirection );',
// composition + solar disc
float sundisk = smoothstep( sunAngularDiameterCos, sunAngularDiameterCos + 0.00002, cosTheta );
L0 += ( vSunE * 19000.0 * Fex ) * sundisk;
' float rPhase = rayleighPhase( cosTheta * 0.5 + 0.5 );',
' vec3 betaRTheta = vBetaR * rPhase;',
vec3 texColor = ( Lin + L0 ) * 0.04 + vec3( 0.0, 0.0003, 0.00075 );
' float mPhase = hgPhase( cosTheta, mieDirectionalG );',
' vec3 betaMTheta = vBetaM * mPhase;',
vec3 retColor = pow( texColor, vec3( 1.0 / ( 1.2 + ( 1.2 * vSunfade ) ) ) );
' vec3 Lin = pow( vSunE * ( ( betaRTheta + betaMTheta ) / ( vBetaR + vBetaM ) ) * ( 1.0 - Fex ), vec3( 1.5 ) );',
' 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 ) );',
gl_FragColor = vec4( retColor, 1.0 );
// nightsky
' vec3 direction = normalize( vWorldPosition - cameraPos );',
' 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' )
#include <tonemapping_fragment>
#include <encodings_fragment>
}`,
};
export { Sky };

View File

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

View File

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

View File

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

View File

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

View File

@ -1,15 +1,14 @@
/*
* password hashing
* @flow
*/
import bcrypt from 'bcrypt';
export function generateHash(password: string) {
export function generateHash(password) {
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;
return bcrypt.compareSync(password, hash);
}

View File

@ -7,6 +7,9 @@
import { utils, distance, image } from 'image-q';
/*
* available color distance calculators
*/
export const ColorDistanceCalculators = [
'Euclidean',
'Manhattan',
@ -21,6 +24,9 @@ export const ColorDistanceCalculators = [
'ManhattanNommyde',
];
/*
* available dithers
*/
export const ImageQuantizerKernels = [
'Nearest',
'Riemersma',
@ -35,7 +41,74 @@ export const ImageQuantizerKernels = [
'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) => {
const fr = new FileReader();
fr.onload = () => {
@ -46,8 +119,7 @@ export function getImageDataOfFile(file) {
cani.height = img.height;
const ctxi = cani.getContext('2d');
ctxi.drawImage(img, 0, 0);
const imdi = ctxi.getImageData(0, 0, img.width, img.height);
resolve(imdi);
resolve(cani);
};
img.onerror = (error) => reject(error);
img.src = fr.result;
@ -57,49 +129,12 @@ export function getImageDataOfFile(file) {
});
}
function* loadData(data, pointArray) {
let i = 0;
while (i < data.length) {
/*
if (!(i % 80)) {
yield Math.floor(i / data.length);
}
*/
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) {
/*
* converts pointContainer to HTMLCanvas
* @param pointContainer
* @return HTMLCanvas
*/
function createCanvasFromPointContainer(pointContainer) {
const width = pointContainer.getWidth();
const height = pointContainer.getHeight();
const data = pointContainer.toUint8Array();
@ -109,9 +144,24 @@ function createImageDataFromPointContainer(pointContainer) {
const ctx = can.getContext('2d');
const idata = ctx.createImageData(width, height);
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) {
const strategy = opts.strategy || 'Nearest';
const colorDist = opts.colorDist || 'Euclidean';
@ -164,7 +214,7 @@ function quantizePointContainer(colors, pointContainer, opts) {
default:
distCalc = new distance.Euclidean();
}
// idk why i need this :/
// could be needed for some reason sometimes
// eslint-disable-next-line no-underscore-dangle
if (distCalc._setDefaults) distCalc._setDefaults();
// construct image quantizer
@ -174,12 +224,23 @@ function quantizePointContainer(colors, pointContainer, opts) {
} else if (strategy === 'Riemersma') {
imageQuantizer = new image.ErrorDiffusionRiemersma(distCalc);
} 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(
distCalc,
image.ErrorDiffusionArrayKernel[strategy],
true,
0,
false,
// eslint-disable-next-line no-prototype-builtins
opts.hasOwnProperty('serpentine') ? opts.serpentine : true,
minColorDistance,
!!opts.GIMPerror,
);
}
// quantize
@ -188,7 +249,7 @@ function quantizePointContainer(colors, pointContainer, opts) {
try {
const result = iterator.next();
if (result.done) {
resolve(createImageDataFromPointContainer(pointContainer));
resolve(createCanvasFromPointContainer(pointContainer));
} else {
if (result.value.pointContainer) {
pointContainer = result.value.pointContainer;
@ -208,17 +269,17 @@ function quantizePointContainer(colors, pointContainer, opts) {
});
}
export function quantizeImage(colors, imageData, opts) {
return createPointContainerFromImageData(
imageData,
(value) => {
if (opts.onProgress) {
opts.onProgress(value);
}
},
).then((pointContainer) => quantizePointContainer(
/*
* quantize HTMLCanvas to palette
* see quantizePointContainer for parameter meanings
*/
export function quantizeImage(colors, imageCanvas, opts) {
const pointContainer = utils.PointContainer.fromHTMLCanvasElement(
imageCanvas,
);
return quantizePointContainer(
colors,
pointContainer,
opts,
));
);
}