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 = {
renderOutputImage({
colors: canvas.colors.slice(canvas.cli),
imgCanvas: inputImageCanvas,
ditherOpts: {
strategy: selectedStrategy,
colorDist: selectedColorDist,
};
renderOutputImage({
file: inputImageData,
dither,
...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,14 +361,11 @@ 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">
@ -405,8 +414,7 @@ function Converter() {
/>
</span>
</div>
)
: null}
)}
<p style={{ fontHeight: 16 }} className="modalcotext">
<input
type="checkbox"
@ -420,14 +428,13 @@ 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;
@ -442,8 +449,9 @@ function Converter() {
const newWidth = (e.target.value > 1024)
? 1024 : e.target.value;
if (!newWidth) return;
if (selectedScaleKeepRatio && inputImageData) {
const ratio = inputImageData.width / inputImageData.height;
if (selectedScaleKeepRatio && inputImageCanvas) {
const ratio = inputImageCanvas.width
/ inputImageCanvas.height;
const newHeight = Math.round(newWidth / ratio);
if (newHeight <= 0) return;
setScaleData({
@ -472,8 +480,9 @@ function Converter() {
const nuHeight = (e.target.value > 1024)
? 1024 : e.target.value;
if (!nuHeight) return;
if (selectedScaleKeepRatio && inputImageData) {
const ratio = inputImageData.width / inputImageData.height;
if (selectedScaleKeepRatio && inputImageCanvas) {
const ratio = inputImageCanvas.width
/ inputImageCanvas.height;
const nuWidth = Math.round(ratio * nuHeight);
if (nuWidth <= 0) return;
setScaleData({
@ -516,11 +525,11 @@ function Converter() {
<button
type="button"
onClick={() => {
if (inputImageData) {
if (inputImageCanvas) {
setScaleData({
...scaleData,
width: inputImageData.width,
height: inputImageData.height,
width: inputImageCanvas.width,
height: inputImageCanvas.height,
});
}
}}
@ -528,9 +537,8 @@ function Converter() {
{t`Reset`}
</button>
</div>
)
: null}
{(inputImageData)
)}
{(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,35 +3,27 @@
* 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,
) {
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;
const planeAxes = axes.substr(0, 2);
const geometry = new THREE.PlaneBufferGeometry(2, 2, 1, 1);
const material = new THREE.ShaderMaterial({
side: THREE.DoubleSide,
uniforms: {
uSize1: {
value: size1,
},
uSize1: { value: size1 },
uSize2: {
value: size2,
},
@ -43,27 +35,21 @@ const InfiniteGridHelper = function InfiniteGridHelper(
},
},
transparent: true,
vertexShader: `
varying vec3 worldPosition;
uniform float uDistance;
void main() {
vec3 pos = position.xzy * uDistance;
pos.xz += cameraPosition.xz;
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;
@ -71,24 +57,17 @@ const InfiniteGridHelper = function InfiniteGridHelper(
uniform vec3 uColor;
uniform float uDistance;
float getGrid(float size) {
vec2 r = worldPosition.xz / 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.xz, worldPosition.xz) / uDistance, 1.0);
float d = 1.0 - min(distance(cameraPosition.${planeAxes}, worldPosition.${planeAxes}) / uDistance, 1.0);
float g1 = getGrid(uSize1);
float g2 = getGrid(uSize2);
@ -107,19 +86,10 @@ const InfiniteGridHelper = function InfiniteGridHelper(
extensions: {
derivatives: true,
},
});
THREE.Mesh.call(this, geometry, material);
super(geometry, material);
this.frustumCulled = false;
};
InfiniteGridHelper.prototype = {
...THREE.Mesh.prototype,
...THREE.Object3D.prototype,
...THREE.EventDispatcher.prototype,
};
export default InfiniteGridHelper;
}
}

View File

@ -5,7 +5,7 @@
*/
import * as THREE from 'three';
import { 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( {
constructor() {
const shader = Sky.SkyShader;
const material = new THREE.ShaderMaterial({
name: 'SkyShader',
fragmentShader: shader.fragmentShader,
vertexShader: shader.vertexShader,
uniforms: UniformsUtils.clone( shader.uniforms ),
side: BackSide
uniforms: THREE.UniformsUtils.clone(shader.uniforms),
side: THREE.BackSide,
depthWrite: false,
});
Mesh.call( this, new BoxBufferGeometry( 1, 1, 1 ), material );
};
Sky.prototype = Object.create( Mesh.prototype );
super(new THREE.BoxGeometry(1, 1, 1), material);
}
}
Sky.SkyShader = {
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 ) }
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;
vertexShader: [
'uniform vec3 sunPosition;',
'uniform float rayleigh;',
'uniform float turbidity;',
'uniform float mieCoefficient;',
'uniform vec3 up;',
'varying vec3 vWorldPosition;',
'varying vec3 vSunDirection;',
'varying float vSunfade;',
'varying vec3 vBetaR;',
'varying vec3 vBetaM;',
'varying float vSunE;',
varying vec3 vWorldPosition;
varying vec3 vSunDirection;
varying float vSunfade;
varying vec3 vBetaR;
varying vec3 vBetaM;
varying float vSunE;
// constants for atmospheric scattering
'const float e = 2.71828182845904523536028747135266249775724709369995957;',
'const float pi = 3.141592653589793238462643383279502884197169;',
const float e = 2.71828182845904523536028747135266249775724709369995957;
const float pi = 3.141592653589793238462643383279502884197169;
// wavelength of used primaries, according to preetham
'const vec3 lambda = vec3( 680E-9, 550E-9, 450E-9 );',
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 );',
const vec3 totalRayleigh = vec3( 5.804542996261093E-6, 1.3562911419845635E-5, 3.0265902468824876E-5 );
// mie stuff
// K coefficient for the primaries
'const float v = 4.0;',
'const vec3 K = vec3( 0.686, 0.678, 0.666 );',
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 );',
const vec3 MieConst = vec3( 1.8399918514433978E14, 2.7798023919660528E14, 4.0790479543861094E14 );
// earth shadow hack
// cutoffAngle = pi / 1.95;
'const float cutoffAngle = 1.6110731556870734;',
'const float steepness = 1.5;',
'const float EE = 1000.0;',
const float cutoffAngle = 1.6110731556870734;
const float steepness = 1.5;
const float EE = 1000.0;
'float sunIntensity( float zenithAngleCos ) {',
' zenithAngleCos = clamp( zenithAngleCos, -1.0, 1.0 );',
' return EE * max( 0.0, 1.0 - pow( e, -( ( cutoffAngle - acos( zenithAngleCos ) ) / steepness ) ) );',
'}',
float sunIntensity( float zenithAngleCos ) {
zenithAngleCos = clamp( zenithAngleCos, -1.0, 1.0 );
return EE * max( 0.0, 1.0 - pow( e, -( ( cutoffAngle - acos( zenithAngleCos ) ) / steepness ) ) );
}
'vec3 totalMie( float T ) {',
' float c = ( 0.2 * T ) * 10E-18;',
' return 0.434 * c * MieConst;',
'}',
vec3 totalMie( float T ) {
float c = ( 0.2 * T ) * 10E-18;
return 0.434 * c * MieConst;
}
'void main() {',
void main() {
' vec4 worldPosition = modelMatrix * vec4( position, 1.0 );',
' vWorldPosition = worldPosition.xyz;',
vec4 worldPosition = modelMatrix * vec4( position, 1.0 );
vWorldPosition = worldPosition.xyz;
' gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );',
' gl_Position.z = gl_Position.w;', // set z to camera.far
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
gl_Position.z = gl_Position.w; // set z to camera.far
' vSunDirection = normalize( sunPosition );',
vSunDirection = normalize( sunPosition );
' vSunE = sunIntensity( dot( vSunDirection, up ) );',
vSunE = sunIntensity( dot( vSunDirection, up ) );
' vSunfade = 1.0 - clamp( 1.0 - exp( ( sunPosition.y / 450000.0 ) ), 0.0, 1.0 );',
vSunfade = 1.0 - clamp( 1.0 - exp( ( sunPosition.y / 450000.0 ) ), 0.0, 1.0 );
' float rayleighCoefficient = rayleigh - ( 1.0 * ( 1.0 - vSunfade ) );',
float rayleighCoefficient = rayleigh - ( 1.0 * ( 1.0 - vSunfade ) );
// extinction (absorbtion + out scattering)
// rayleigh coefficients
' vBetaR = totalRayleigh * rayleighCoefficient;',
vBetaR = totalRayleigh * rayleighCoefficient;
// mie coefficients
' vBetaM = totalMie( turbidity ) * mieCoefficient;',
vBetaM = totalMie( turbidity ) * mieCoefficient;
'}'
].join( '\n' ),
}`,
fragmentShader:
/* glsl */
`
varying vec3 vWorldPosition;
varying vec3 vSunDirection;
varying float vSunfade;
varying vec3 vBetaR;
varying vec3 vBetaM;
varying float vSunE;
fragmentShader: [
'varying vec3 vWorldPosition;',
'varying vec3 vSunDirection;',
'varying float vSunfade;',
'varying vec3 vBetaR;',
'varying vec3 vBetaM;',
'varying float vSunE;',
uniform float mieDirectionalG;
uniform vec3 up;
'uniform float luminance;',
'uniform float mieDirectionalG;',
'uniform vec3 up;',
'const vec3 cameraPos = vec3( 0.0, 0.0, 0.0 );',
const vec3 cameraPos = vec3( 0.0, 0.0, 0.0 );
// constants for atmospheric scattering
'const float pi = 3.141592653589793238462643383279502884197169;',
const float pi = 3.141592653589793238462643383279502884197169;
'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 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)
// optical length at zenith for molecules
'const float rayleighZenithLength = 8.4E3;',
'const float mieZenithLength = 1.25E3;',
const float rayleighZenithLength = 8.4E3;
const float mieZenithLength = 1.25E3;
// 66 arc seconds -> degrees, and the cosine of that
'const float sunAngularDiameterCos = 0.999956676946448443553574619906976478926848692873900859324;',
const float sunAngularDiameterCos = 0.999956676946448443553574619906976478926848692873900859324;
// 3.0 / ( 16.0 * pi )
'const float THREE_OVER_SIXTEENPI = 0.05968310365946075;',
const float THREE_OVER_SIXTEENPI = 0.05968310365946075;
// 1.0 / ( 4.0 * pi )
'const float ONE_OVER_FOURPI = 0.07957747154594767;',
const float ONE_OVER_FOURPI = 0.07957747154594767;
'float rayleighPhase( float cosTheta ) {',
' return THREE_OVER_SIXTEENPI * ( 1.0 + pow( cosTheta, 2.0 ) );',
'}',
float rayleighPhase( float cosTheta ) {
return THREE_OVER_SIXTEENPI * ( 1.0 + pow( cosTheta, 2.0 ) );
}
'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 );',
'}',
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 );
}
// 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;',
void main() {
'const float whiteScale = 1.0748724675633854;', // 1.0 / Uncharted2Tonemap(1000.0)
vec3 direction = normalize( vWorldPosition - cameraPos );
'vec3 Uncharted2Tonemap( vec3 x ) {',
' return ( ( x * ( A * x + C * B ) + D * E ) / ( x * ( A * x + B ) + D * F ) ) - E / F;',
'}',
'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;',
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;
// combined extinction factor
' vec3 Fex = exp( -( vBetaR * sR + vBetaM * sM ) );',
vec3 Fex = exp( -( vBetaR * sR + vBetaM * sM ) );
// in scattering
' float cosTheta = dot( normalize( vWorldPosition - cameraPos ), vSunDirection );',
float cosTheta = dot( direction, vSunDirection );
' float rPhase = rayleighPhase( cosTheta * 0.5 + 0.5 );',
' vec3 betaRTheta = vBetaR * rPhase;',
float rPhase = rayleighPhase( cosTheta * 0.5 + 0.5 );
vec3 betaRTheta = vBetaR * rPhase;
' float mPhase = hgPhase( cosTheta, mieDirectionalG );',
' vec3 betaMTheta = vBetaM * mPhase;',
float mPhase = hgPhase( cosTheta, mieDirectionalG );
vec3 betaMTheta = vBetaM * mPhase;
' 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 ) );',
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 ) );
// 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;',
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;',
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 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( texColor, vec3( 1.0 / ( 1.2 + ( 1.2 * vSunfade ) ) ) );
' vec3 retColor = pow( color, vec3( 1.0 / ( 1.2 + ( 1.2 * vSunfade ) ) ) );',
gl_FragColor = vec4( retColor, 1.0 );
' 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);
}
* converts pointContainer to HTMLCanvas
* @param pointContainer
* @return HTMLCanvas
*/
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) {
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,
));
);
}