From 6170d356316d8b9f3ed63137bc77d41d47273f16 Mon Sep 17 00:00:00 2001 From: HF Date: Mon, 3 Jan 2022 16:46:59 +0100 Subject: [PATCH] refactor image converter and 3d stuff --- .eslintrc.json | 3 + src/canvasesDesc.js | 1 - src/components/Converter.jsx | 526 +++++++++--------- src/components/Menu.jsx | 6 +- src/components/UserMessages.jsx | 2 +- src/controls/VoxelPainterControls.js | 6 +- src/socket/packets/ChangedMe.js | 6 +- src/socket/packets/CoolDownPacket.js | 9 +- src/socket/packets/DeRegisterChunk.js | 9 +- .../packets/DeRegisterMultipleChunks.js | 8 +- src/socket/packets/OnlineCounter.js | 14 +- src/socket/packets/PixelReturn.js | 9 +- src/socket/packets/PixelUpdateClient.js | 16 +- src/socket/packets/PixelUpdateServer.js | 18 +- src/socket/packets/README.md | 6 + src/socket/packets/RegisterCanvas.js | 9 +- src/socket/packets/RegisterChunk.js | 9 +- src/socket/packets/RegisterMultipleChunks.js | 8 +- src/store/rendererHook.js | 1 - src/styles/default.css | 16 + src/ui/InfiniteGridHelper.js | 184 +++--- src/ui/Renderer3D.js | 4 +- src/ui/Sky.js | 340 ++++++----- src/utils/Counter.js | 6 - src/utils/RateLimiter.js | 11 +- src/utils/captcha.js | 16 +- src/utils/clipboard.js | 3 - src/utils/hash.js | 5 +- src/utils/image.js | 185 +++--- 29 files changed, 721 insertions(+), 715 deletions(-) create mode 100644 src/socket/packets/README.md diff --git a/.eslintrc.json b/.eslintrc.json index 0770620..697a1e8 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -4,6 +4,9 @@ ], "parser":"@babel/eslint-parser", "parserOptions": { + "babelOptions":{ + "rootMode": "upward" + }, "ecmaFeatures": { "jsx": true } diff --git a/src/canvasesDesc.js b/src/canvasesDesc.js index c647c39..829b8d1 100644 --- a/src/canvasesDesc.js +++ b/src/canvasesDesc.js @@ -2,7 +2,6 @@ * Create canvases.json with localized translated * descriptions. * - * @flow */ import canvases from './canvases.json'; diff --git a/src/components/Converter.jsx b/src/components/Converter.jsx index c4f207a..9932e6f 100644 --- a/src/components/Converter.jsx +++ b/src/components/Converter.jsx @@ -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 = GIMP; const starhouseLink = ( @@ -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); }} />

{t`Choose Strategy`}:  @@ -319,6 +278,59 @@ function Converter() { }

+ {(showExtraOptions || extraRender) && ( +
{ + if (!showExtraOptions) setExtraRender(false); + }} + > +

+ { + setExtraOpts({ + ...extraOpts, + serpentine: e.target.checked, + }); + }} + /> + {t`Serpentine`} +

+ {t`Minimum Color Distance`}:  + { + setExtraOpts({ + ...extraOpts, + minColorDistance: e.target.value, + }); + }} + />  + +

+ { + setExtraOpts({ + ...extraOpts, + GIMPerror: e.target.checked, + }); + }} + /> + {t`Calculate like GIMP`} +

+
+ )}

{t`Choose Color Mode`}:  { - setGridData({ - ...gridData, - light: e.target.checked, - }); - }} - /> - {t`Light Grid`} -

- {t`Offset`} X:  - { - setGridData({ - ...gridData, - offsetX: e.target.value, - }); - }} - />  - - {t`Offset`} Y:  - { - setGridData({ - ...gridData, - offsetY: e.target.value, - }); - }} - /> - - - ) - : null} + > +

+ { + setGridData({ + ...gridData, + light: e.target.checked, + }); + }} + /> + {t`Light Grid`} +

+ {t`Offset`} X:  + { + setGridData({ + ...gridData, + offsetX: e.target.value, + }); + }} + />  + + {t`Offset`} Y:  + { + setGridData({ + ...gridData, + offsetY: e.target.value, + }); + }} + /> + + + )}

{t`Scale Image`}

- {(scalingEnabled) - ? ( -
{ + if (!scalingEnabled) setScalingRender(false); }} - > - {t`Width`}:  - { - 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; - } + > + {t`Width`}:  + { + 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, }); - }} - />  - - {t`Height`}:  - { - 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, + }); + }} + />  + + {t`Height`}:  + { + 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, }); - }} - /> - -

- { - selectScaleKeepRatio(e.target.checked); - }} - /> - {t`Keep Ratio`} -

-

- { - setScaleData({ - ...scaleData, - aa: e.target.checked, - }); - }} - /> - {t`Anti Aliasing`} -

- -
- ) - : null} - {(inputImageData) + /> + +

+ { + selectScaleKeepRatio(e.target.checked); + }} + /> + {t`Keep Ratio`} +

+

+ { + setScaleData({ + ...scaleData, + aa: e.target.checked, + }); + }} + /> + {t`Anti Aliasing`} +

+ + + )} + {(inputImageCanvas) ? (

...

diff --git a/src/components/Menu.jsx b/src/components/Menu.jsx index df556b8..6a4e127 100644 --- a/src/components/Menu.jsx +++ b/src/components/Menu.jsx @@ -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 = () => { diff --git a/src/components/UserMessages.jsx b/src/components/UserMessages.jsx index 6bf6b14..3b08cbf 100644 --- a/src/components/UserMessages.jsx +++ b/src/components/UserMessages.jsx @@ -26,7 +26,7 @@ const 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) ? ( > 8, chunkId & 0xFF]); return Buffer.concat([index, pixels]); }, diff --git a/src/socket/packets/README.md b/src/socket/packets/README.md new file mode 100644 index 0000000..d758a2a --- /dev/null +++ b/src/socket/packets/README.md @@ -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. diff --git a/src/socket/packets/RegisterCanvas.js b/src/socket/packets/RegisterCanvas.js index d122dab..c7dcef5 100644 --- a/src/socket/packets/RegisterCanvas.js +++ b/src/socket/packets/RegisterCanvas.js @@ -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); diff --git a/src/socket/packets/RegisterChunk.js b/src/socket/packets/RegisterChunk.js index 6e68392..ed31a30 100644 --- a/src/socket/packets/RegisterChunk.js +++ b/src/socket/packets/RegisterChunk.js @@ -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); diff --git a/src/socket/packets/RegisterMultipleChunks.js b/src/socket/packets/RegisterMultipleChunks.js index e4c99c8..f2a0b44 100644 --- a/src/socket/packets/RegisterMultipleChunks.js +++ b/src/socket/packets/RegisterMultipleChunks.js @@ -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); diff --git a/src/store/rendererHook.js b/src/store/rendererHook.js index 3799771..ffca3aa 100644 --- a/src/store/rendererHook.js +++ b/src/store/rendererHook.js @@ -1,7 +1,6 @@ /* * Hooks for renderer * - * @flow */ import { diff --git a/src/styles/default.css b/src/styles/default.css index f219281..22ae3e3 100644 --- a/src/styles/default.css +++ b/src/styles/default.css @@ -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; diff --git a/src/ui/InfiniteGridHelper.js b/src/ui/InfiniteGridHelper.js index e9b548e..d7d9af7 100644 --- a/src/ui/InfiniteGridHelper.js +++ b/src/ui/InfiniteGridHelper.js @@ -3,123 +3,93 @@ * https://github.com/Fyrestar/THREE.InfiniteGridHelper * MIT License * - * @flow */ /* eslint-disable max-len */ import * as THREE from 'three'; -const InfiniteGridHelper = function InfiniteGridHelper( - size1, - size2, - color, - distance, -) { - color = color || new THREE.Color('white'); - size1 = size1 || 10; - size2 = size2 || 100; +export default class InfiniteGridHelper extends THREE.Mesh { + constructor(size1, size2, color, distance, axes = 'xzy') { + color = color || new THREE.Color('white'); + size1 = size1 || 10; + size2 = size2 || 100; - distance = distance || 8000; + distance = distance || 8000; + const planeAxes = axes.substr(0, 2); + const geometry = new THREE.PlaneBufferGeometry(2, 2, 1, 1); + const material = new THREE.ShaderMaterial({ - const geometry = new THREE.PlaneBufferGeometry(2, 2, 1, 1); + side: THREE.DoubleSide, - const material = new THREE.ShaderMaterial({ - - side: THREE.DoubleSide, - - uniforms: { - uSize1: { - value: size1, + uniforms: { + uSize1: { value: size1 }, + uSize2: { + value: size2, + }, + uColor: { + value: color, + }, + uDistance: { + value: distance, + }, }, - uSize2: { - value: size2, + transparent: true, + + vertexShader: ` + varying vec3 worldPosition; + uniform float uDistance; + void main() { + vec3 pos = position.${axes} * uDistance; + pos.${planeAxes} += cameraPosition.${planeAxes}; + + worldPosition = pos; + + gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0); + } + `, + + fragmentShader: ` + varying vec3 worldPosition; + + uniform float uSize1; + uniform float uSize2; + uniform vec3 uColor; + uniform float uDistance; + + float getGrid(float size) { + vec2 r = worldPosition.${planeAxes} / size; + + vec2 grid = abs(fract(r - 0.5) - 0.5) / fwidth(r); + float line = min(grid.x, grid.y); + + return 1.0 - min(line, 1.0); + } + + void main() { + float d = 1.0 - min(distance(cameraPosition.${planeAxes}, worldPosition.${planeAxes}) / uDistance, 1.0); + + float g1 = getGrid(uSize1); + float g2 = getGrid(uSize2); + + + gl_FragColor = vec4(uColor.rgb, mix(g2, g1, g1) * pow(d, 3.0)); + gl_FragColor.a = mix(0.5 * gl_FragColor.a, gl_FragColor.a, g2); + + if ( gl_FragColor.a <= 0.0 ) discard; + + + } + + `, + + extensions: { + derivatives: true, }, - uColor: { - value: color, - }, - uDistance: { - value: distance, - }, - }, - transparent: true, - vertexShader: ` - - varying vec3 worldPosition; - - uniform float uDistance; - - void main() { - - vec3 pos = position.xzy * uDistance; - pos.xz += cameraPosition.xz; - - worldPosition = pos; - - gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0); - - } - `, + }); + super(geometry, material); - fragmentShader: ` - - varying vec3 worldPosition; - - uniform float uSize1; - uniform float uSize2; - uniform vec3 uColor; - uniform float uDistance; - - - - float getGrid(float size) { - - vec2 r = worldPosition.xz / size; - - - vec2 grid = abs(fract(r - 0.5) - 0.5) / fwidth(r); - float line = min(grid.x, grid.y); - - - return 1.0 - min(line, 1.0); - } - - void main() { - - - float d = 1.0 - min(distance(cameraPosition.xz, worldPosition.xz) / uDistance, 1.0); - - float g1 = getGrid(uSize1); - float g2 = getGrid(uSize2); - - - gl_FragColor = vec4(uColor.rgb, mix(g2, g1, g1) * pow(d, 3.0)); - gl_FragColor.a = mix(0.5 * gl_FragColor.a, gl_FragColor.a, g2); - - if ( gl_FragColor.a <= 0.0 ) discard; - - - } - - `, - - extensions: { - derivatives: true, - }, - - }); - - - THREE.Mesh.call(this, geometry, material); - - this.frustumCulled = false; -}; - -InfiniteGridHelper.prototype = { - ...THREE.Mesh.prototype, - ...THREE.Object3D.prototype, - ...THREE.EventDispatcher.prototype, -}; - -export default InfiniteGridHelper; + this.frustumCulled = false; + } +} diff --git a/src/ui/Renderer3D.js b/src/ui/Renderer3D.js index e0d4763..e6b4707 100644 --- a/src/ui/Renderer3D.js +++ b/src/ui/Renderer3D.js @@ -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); diff --git a/src/ui/Sky.js b/src/ui/Sky.js index c3e4e87..6ecaa31 100644 --- a/src/ui/Sky.js +++ b/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 + #include + }`, }; - -export { Sky }; diff --git a/src/utils/Counter.js b/src/utils/Counter.js index 735a733..bc23685 100644 --- a/src/utils/Counter.js +++ b/src/utils/Counter.js @@ -1,9 +1,3 @@ -/** - * - * @flow - */ - - export default class Counter { map: Map; diff --git a/src/utils/RateLimiter.js b/src/utils/RateLimiter.js index 3604de0..d205575 100644 --- a/src/utils/RateLimiter.js +++ b/src/utils/RateLimiter.js @@ -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(); diff --git a/src/utils/captcha.js b/src/utils/captcha.js index 4552ebf..79b33e9 100644 --- a/src/utils/captcha.js +++ b/src/utils/captcha.js @@ -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; } diff --git a/src/utils/clipboard.js b/src/utils/clipboard.js index c54ac92..259447d 100644 --- a/src/utils/clipboard.js +++ b/src/utils/clipboard.js @@ -1,6 +1,3 @@ -/* @flow - */ - function fallbackCopyTextToClipboard(text) { const textArea = document.createElement('textarea'); textArea.value = text; diff --git a/src/utils/hash.js b/src/utils/hash.js index e59de4b..9271d98 100644 --- a/src/utils/hash.js +++ b/src/utils/hash.js @@ -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); } diff --git a/src/utils/image.js b/src/utils/image.js index c1986bd..cf0971b 100644 --- a/src/utils/image.js +++ b/src/utils/image.js @@ -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, - )); + ); }