refactor image converter and 3d stuff
This commit is contained in:
parent
e59df4fb62
commit
6170d35631
|
@ -4,6 +4,9 @@
|
|||
],
|
||||
"parser":"@babel/eslint-parser",
|
||||
"parserOptions": {
|
||||
"babelOptions":{
|
||||
"rootMode": "upward"
|
||||
},
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
* Create canvases.json with localized translated
|
||||
* descriptions.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import canvases from './canvases.json';
|
||||
|
|
|
@ -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`}:
|
||||
|
@ -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`}:
|
||||
<input
|
||||
type="number"
|
||||
step="1"
|
||||
min="0"
|
||||
max="100"
|
||||
style={{ width: '4em' }}
|
||||
value={minColorDistance}
|
||||
onChange={(e) => {
|
||||
setExtraOpts({
|
||||
...extraOpts,
|
||||
minColorDistance: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</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`}:
|
||||
<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:
|
||||
<input
|
||||
type="number"
|
||||
step="1"
|
||||
min="0"
|
||||
max="10"
|
||||
style={{ width: '2em' }}
|
||||
value={gridOffsetX}
|
||||
onChange={(e) => {
|
||||
setGridData({
|
||||
...gridData,
|
||||
offsetX: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<span className="modalcotext">{t`Offset`} Y:
|
||||
<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:
|
||||
<input
|
||||
type="number"
|
||||
step="1"
|
||||
min="0"
|
||||
max="10"
|
||||
style={{ width: '2em' }}
|
||||
value={gridOffsetX}
|
||||
onChange={(e) => {
|
||||
setGridData({
|
||||
...gridData,
|
||||
offsetX: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<span className="modalcotext">{t`Offset`} Y:
|
||||
<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`}:
|
||||
<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`}:
|
||||
<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,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<span className="modalcotext">{t`Height`}:
|
||||
<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,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<span className="modalcotext">{t`Height`}:
|
||||
<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>
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -26,7 +26,7 @@ const UserMessages = () => {
|
|||
&& (
|
||||
<p className="usermessages">
|
||||
{t`Please verify your mail address
|
||||
or your account could get deleted after a few days.`}
|
||||
or your account could get deleted after a few days.`}
|
||||
{(verifyAnswer)
|
||||
? (
|
||||
<span
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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]);
|
||||
},
|
||||
|
|
6
src/socket/packets/README.md
Normal file
6
src/socket/packets/README.md
Normal 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.
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
/*
|
||||
* Hooks for renderer
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
super(geometry, material);
|
||||
|
||||
uniform float uDistance;
|
||||
|
||||
void main() {
|
||||
|
||||
vec3 pos = position.xzy * uDistance;
|
||||
pos.xz += cameraPosition.xz;
|
||||
|
||||
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.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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
340
src/ui/Sky.js
340
src/ui/Sky.js
|
@ -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 };
|
||||
|
|
|
@ -1,9 +1,3 @@
|
|||
/**
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
|
||||
export default class Counter<T> {
|
||||
map: Map<T, number>;
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
/* @flow
|
||||
*/
|
||||
|
||||
function fallbackCopyTextToClipboard(text) {
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
));
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user