update packages

This commit is contained in:
HF 2022-01-01 01:39:01 +01:00
parent 44af557581
commit 8c17f56b79
15 changed files with 6164 additions and 20640 deletions

View File

@ -302,10 +302,6 @@ msgstr ""
msgid "Failed to establish session after register :("
msgstr ""
#: src/routes/api/auth/logout.js:13
msgid "You are not even logged in."
msgstr ""
#: src/routes/api/auth/verify.js:25
#: src/routes/api/auth/verify.js:32
msgid "Mail verification"
@ -321,6 +317,10 @@ msgid ""
"request a new one."
msgstr ""
#: src/routes/api/auth/logout.js:13
msgid "You are not even logged in."
msgstr ""
#: src/routes/api/auth/change_mail.js:41
#: src/routes/api/auth/change_passwd.js:37
#: src/routes/api/auth/delete_account.js:38

View File

@ -162,7 +162,7 @@ msgstr ""
msgid "Look at past Canvases"
msgstr ""
#: src/components/Converter.jsx:615
#: src/components/Converter.jsx:586
#: src/components/CoordinatesBox.jsx:32
msgid "Copy to Clipboard"
msgstr ""
@ -186,6 +186,11 @@ msgstr ""
msgid "Restore"
msgstr ""
#: src/components/buttons/CanvasSwitchButton.jsx:23
#: src/components/windows/index.js:22
msgid "Canvas Selection"
msgstr ""
#: src/components/buttons/ExpandMenuButton.jsx:23
msgid "Close Menu"
msgstr ""
@ -202,11 +207,6 @@ msgstr ""
msgid "Open Chat"
msgstr ""
#: src/components/buttons/CanvasSwitchButton.jsx:23
#: src/components/windows/index.js:22
msgid "Canvas Selection"
msgstr ""
#: src/actions/fetch.js:40
msgid "You made too many requests"
msgstr ""
@ -602,7 +602,7 @@ msgstr ""
msgid "For when you are playing on a potato."
msgstr ""
#: src/components/Converter.jsx:429
#: src/components/Converter.jsx:400
#: src/components/windows/Settings.jsx:195
msgid "Light Grid"
msgstr ""
@ -631,17 +631,6 @@ msgstr ""
msgid "Select Language"
msgstr ""
#: src/components/windows/CanvasSelect.jsx:32
msgid ""
"Select the canvas you want to use. Every canvas is unique and has "
"different palettes, cooldown and requirements. Archive of closed canvases "
"can be accessed here:"
msgstr ""
#: src/components/windows/CanvasSelect.jsx:40
msgid "Archive"
msgstr ""
#: src/components/windows/UserArea.jsx:27
msgid "Profile"
msgstr ""
@ -670,39 +659,6 @@ msgstr ""
msgid "Consider joining us on Guilded:"
msgstr ""
#: src/components/windows/Archive.jsx:20
msgid ""
"While we tend to not delete canvases, some canvases are started for fun or "
"as a request by users who currently like a meme. Those canvases can get "
"boring after a while and after weeks of no major change and if they really "
"aren't worth being kept active, we decide to remove them."
msgstr ""
#: src/components/windows/Archive.jsx:22
msgid ""
"Here we collect those canvases to archive them in a proper way (which is "
"currently just one)."
msgstr ""
#: src/components/windows/Archive.jsx:24
msgid "Political Compass Canvas"
msgstr ""
#: src/components/windows/Archive.jsx:31
msgid ""
"This canvas got requested during a time of political conflicts on the main "
"Earth canvas. It was a 1024x1024 representation of the political compass "
"with a 5s cooldown and 60s stacking. It got launched on May 11th and "
"remained active for months till it got shut down on November 30th."
msgstr ""
#: src/components/windows/Archive.jsx:32
msgid ""
"We decided to archive it as a timelapse with lossless encoded webm. Taking "
"a screenshot from the timelapse results in a perfect 1:1 representation of "
"how the canvas was at that time."
msgstr ""
#: src/components/windows/Register.jsx:81
msgid "Register new account here"
msgstr ""
@ -737,6 +693,50 @@ msgstr ""
msgid "Submit"
msgstr ""
#: src/components/windows/CanvasSelect.jsx:32
msgid ""
"Select the canvas you want to use. Every canvas is unique and has "
"different palettes, cooldown and requirements. Archive of closed canvases "
"can be accessed here:"
msgstr ""
#: src/components/windows/CanvasSelect.jsx:40
msgid "Archive"
msgstr ""
#: src/components/windows/Archive.jsx:20
msgid ""
"While we tend to not delete canvases, some canvases are started for fun or "
"as a request by users who currently like a meme. Those canvases can get "
"boring after a while and after weeks of no major change and if they really "
"aren't worth being kept active, we decide to remove them."
msgstr ""
#: src/components/windows/Archive.jsx:22
msgid ""
"Here we collect those canvases to archive them in a proper way (which is "
"currently just one)."
msgstr ""
#: src/components/windows/Archive.jsx:24
msgid "Political Compass Canvas"
msgstr ""
#: src/components/windows/Archive.jsx:31
msgid ""
"This canvas got requested during a time of political conflicts on the main "
"Earth canvas. It was a 1024x1024 representation of the political compass "
"with a 5s cooldown and 60s stacking. It got launched on May 11th and "
"remained active for months till it got shut down on November 30th."
msgstr ""
#: src/components/windows/Archive.jsx:32
msgid ""
"We decided to archive it as a timelapse with lossless encoded webm. Taking "
"a screenshot from the timelapse results in a perfect 1:1 representation of "
"how the canvas was at that time."
msgstr ""
#: src/components/windows/Chat.jsx:146
msgid "Channel settings"
msgstr ""
@ -809,30 +809,6 @@ msgstr ""
msgid "Password must be shorter than 60 characters."
msgstr ""
#: src/components/LogInArea.jsx:21
msgid "Login to access more features and stats."
msgstr ""
#: src/components/LogInArea.jsx:23
msgid "Login with Name or Mail:"
msgstr ""
#: src/components/LogInArea.jsx:30
msgid "I forgot my Password."
msgstr ""
#: src/components/LogInArea.jsx:31
msgid "or login with:"
msgstr ""
#: src/components/LogInArea.jsx:72
msgid "or register here:"
msgstr ""
#: src/components/LogInArea.jsx:79
msgid "Register"
msgstr ""
#: src/components/ChangeMail.jsx:91
#: src/components/ChangeName.jsx:68
#: src/components/ChangePassword.jsx:110
@ -840,35 +816,6 @@ msgstr ""
msgid "Save"
msgstr ""
#: src/components/CanvasItem.jsx:27
msgid "Cooldown"
msgstr ""
#: src/components/CanvasItem.jsx:33
msgid "Stacking till"
msgstr ""
#: src/components/CanvasItem.jsx:35
msgid "Ranked"
msgstr ""
#: src/components/CanvasItem.jsx:37
msgid "Requirements"
msgstr ""
#: src/components/CanvasItem.jsx:39
msgid "User Account"
msgstr ""
#: src/components/CanvasItem.jsx:41
#, javascript-format
msgid "and ${ canvas.req } Pixels set"
msgstr ""
#: src/components/CanvasItem.jsx:45
msgid "Dimensions"
msgstr ""
#: src/components/UserAreaContent.jsx:63
msgid "Todays Placed Pixels"
msgstr ""
@ -914,6 +861,30 @@ msgstr ""
msgid "Social Settings"
msgstr ""
#: src/components/LogInArea.jsx:21
msgid "Login to access more features and stats."
msgstr ""
#: src/components/LogInArea.jsx:23
msgid "Login with Name or Mail:"
msgstr ""
#: src/components/LogInArea.jsx:30
msgid "I forgot my Password."
msgstr ""
#: src/components/LogInArea.jsx:31
msgid "or login with:"
msgstr ""
#: src/components/LogInArea.jsx:72
msgid "or register here:"
msgstr ""
#: src/components/LogInArea.jsx:79
msgid "Register"
msgstr ""
#: src/components/Rankings.jsx:28
msgid "Total"
msgstr ""
@ -926,77 +897,6 @@ msgstr ""
msgid "Ranking updates every 5 min. Daily rankings get reset at midnight UTC."
msgstr ""
#: src/components/Converter.jsx:280
msgid "Choose Canvas"
msgstr ""
#: src/components/Converter.jsx:306
msgid "Palette Download"
msgstr ""
#: src/components/Converter.jsx:308
#, javascript-format
msgid "Palette for ${ gimpLink }"
msgstr ""
#: src/components/Converter.jsx:326
#, javascript-format
msgid "Credit for the Palette of the Moon goes to ${ starhouseLink }."
msgstr ""
#: src/components/Converter.jsx:329
msgid "Image Converter"
msgstr ""
#: src/components/Converter.jsx:330
msgid "Convert an image to canvas colors"
msgstr ""
#: src/components/Converter.jsx:341
msgid "Choose Strategy"
msgstr ""
#: src/components/Converter.jsx:368
msgid "Choose Color Mode"
msgstr ""
#: src/components/Converter.jsx:406
msgid "Add Grid (uncheck if you need a 1:1 template)"
msgstr ""
#: src/components/Converter.jsx:431
#: src/components/Converter.jsx:447
msgid "Offset"
msgstr ""
#: src/components/Converter.jsx:477
msgid "Scale Image"
msgstr ""
#: src/components/Converter.jsx:489
msgid "Width"
msgstr ""
#: src/components/Converter.jsx:519
msgid "Height"
msgstr ""
#: src/components/Converter.jsx:557
msgid "Keep Ratio"
msgstr ""
#: src/components/Converter.jsx:570
msgid "Anti Aliasing"
msgstr ""
#: src/components/Converter.jsx:584
msgid "Reset"
msgstr ""
#: src/components/Converter.jsx:603
msgid "Download Template"
msgstr ""
#: src/components/Admintools.jsx:184
msgid "Build image on canvas."
msgstr ""
@ -1075,12 +975,104 @@ msgstr ""
msgid "User Name"
msgstr ""
#: src/components/LogInForm.jsx:76
msgid "Name or Email"
#: src/components/Converter.jsx:260
msgid "Choose Canvas"
msgstr ""
#: src/components/LogInForm.jsx:87
msgid "LogIn"
#: src/components/Converter.jsx:286
msgid "Palette Download"
msgstr ""
#: src/components/Converter.jsx:288
#, javascript-format
msgid "Palette for ${ gimpLink }"
msgstr ""
#: src/components/Converter.jsx:306
#, javascript-format
msgid "Credit for the Palette of the Moon goes to ${ starhouseLink }."
msgstr ""
#: src/components/Converter.jsx:309
msgid "Image Converter"
msgstr ""
#: src/components/Converter.jsx:310
msgid "Convert an image to canvas colors"
msgstr ""
#: src/components/Converter.jsx:332
msgid "Choose Strategy"
msgstr ""
#: src/components/Converter.jsx:349
msgid "Choose Color Mode"
msgstr ""
#: src/components/Converter.jsx:377
msgid "Add Grid (uncheck if you need a 1:1 template)"
msgstr ""
#: src/components/Converter.jsx:402
#: src/components/Converter.jsx:418
msgid "Offset"
msgstr ""
#: src/components/Converter.jsx:448
msgid "Scale Image"
msgstr ""
#: src/components/Converter.jsx:460
msgid "Width"
msgstr ""
#: src/components/Converter.jsx:490
msgid "Height"
msgstr ""
#: src/components/Converter.jsx:528
msgid "Keep Ratio"
msgstr ""
#: src/components/Converter.jsx:541
msgid "Anti Aliasing"
msgstr ""
#: src/components/Converter.jsx:555
msgid "Reset"
msgstr ""
#: src/components/Converter.jsx:574
msgid "Download Template"
msgstr ""
#: src/components/CanvasItem.jsx:27
msgid "Cooldown"
msgstr ""
#: src/components/CanvasItem.jsx:33
msgid "Stacking till"
msgstr ""
#: src/components/CanvasItem.jsx:35
msgid "Ranked"
msgstr ""
#: src/components/CanvasItem.jsx:37
msgid "Requirements"
msgstr ""
#: src/components/CanvasItem.jsx:39
msgid "User Account"
msgstr ""
#: src/components/CanvasItem.jsx:41
#, javascript-format
msgid "and ${ canvas.req } Pixels set"
msgstr ""
#: src/components/CanvasItem.jsx:45
msgid "Dimensions"
msgstr ""
#: src/components/UserMessages.jsx:28
@ -1117,14 +1109,14 @@ msgstr ""
msgid "Confirm New Password"
msgstr ""
#: src/components/ChangeName.jsx:64
msgid "New Username"
msgstr ""
#: src/components/DeleteAccount.jsx:66
msgid "Yes, Delete My Account!"
msgstr ""
#: src/components/ChangeName.jsx:64
msgid "New Username"
msgstr ""
#: src/components/ChangeMail.jsx:59
msgid ""
"Changed Mail successfully. We sent you a verification mail, "
@ -1147,6 +1139,14 @@ msgstr ""
msgid "You have no users blocked"
msgstr ""
#: src/components/LogInForm.jsx:76
msgid "Name or Email"
msgstr ""
#: src/components/LogInForm.jsx:87
msgid "LogIn"
msgstr ""
#: src/components/windows/Help.jsx:15
#: src/components/windows/Settings.jsx:134
msgctxt "keybinds"

25704
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,9 +11,9 @@
"scripts": {
"build": "npm run webpack && npm run minify-css",
"build-en": "npm run extract && npm run minify-css",
"webpack": "webpack --config ./webpack.config.web.babel.js && parallel-webpack --config ./webpack.config.client.babel.js",
"webpack": "webpack --config ./webpack.config.web.babel.js && webpack --config ./webpack.config.client.babel.js",
"minify-css": "babel-node scripts/minifyCss.js",
"extract": "webpack --env extract --config ./webpack.config.web.babel.js && webpack --env extract --config ./webpack.config.client.babel.js",
"extract": "webpack --env extract --config ./webpack.config.web.babel.js && webpack --env extract --env development --config ./webpack.config.client.babel.js",
"babel-node": "cd $INIT_CWD && babel-node",
"lint": "cd $INIT_CWD && eslint --ext .jsx --ext .js",
"lint:src": "eslint --ext .jsx --ext .js src",
@ -30,53 +30,52 @@
"dependencies": {
"bcrypt": "^5.0.1",
"bluebird": "^3.5.0",
"body-parser": "^1.17.2",
"bufferutil": "^4.0.3",
"body-parser": "^1.19.1",
"bufferutil": "^4.0.6",
"compression": "^1.7.3",
"connect-redis": "^6.0.0",
"cookie": "^0.4.1",
"core-js": "^3.16.2",
"core-js": "^3.20.2",
"cors": "^2.8.4",
"etag": "^1.8.1",
"express": "^4.15.3",
"express": "^4.17.2",
"express-limiter": "^1.6.0",
"express-session": "^1.17.2",
"global": "^4.3.2",
"http-proxy-agent": "^4.0.1",
"image-q": "^2.1.2",
"ip-address": "^7.1.0",
"image-q": "^3.0.5",
"isomorphic-fetch": "^3.0.0",
"js-file-download": "^0.4.12",
"localforage": "^1.10.0",
"morgan": "^1.10.0",
"multer": "^1.4.3",
"mysql2": "^2.3.0",
"nodemailer": "^6.6.3",
"passport": "^0.4.0",
"multer": "^1.4.4",
"mysql2": "^2.3.3",
"nodemailer": "^6.7.2",
"passport": "^0.5.2",
"passport-discord": "^0.1.4",
"passport-facebook": "^3.0.0",
"passport-google-oauth": "^2.0.0",
"passport-json": "^1.2.0",
"passport-reddit": "^0.2.4",
"passport-vkontakte": "^0.5.0",
"ppfun-captcha": "^1.6.5",
"ppfun-captcha": "^1.6.6",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-icons": "^4.2.0",
"react-redux": "^7.2.4",
"react-icons": "^4.3.1",
"react-redux": "^7.2.6",
"react-responsive": "^8.2.0",
"react-stay-scrolled": "^7.3.1",
"react-stay-scrolled": "^7.4.0",
"react-toggle-button": "^2.1.0",
"redis": "^3.1.2",
"redlock": "^4.0.0",
"redux": "^4.1.1",
"redux": "^4.1.2",
"redux-logger": "^3.0.6",
"redux-persist": "^6.0.0",
"redux-thunk": "^2.2.0",
"sequelize": "^6.6.5",
"sharp": "^0.27.2",
"redux-thunk": "^2.4.1",
"sequelize": "^6.12.4",
"sharp": "^0.29.3",
"startaudiocontext": "^1.2.1",
"three": "^0.125.2",
"three": "^0.136.0",
"three-trackballcontrols": "^0.9.0",
"ttag": "^1.7.24",
"ttag-po-loader": "0.0.2",
@ -86,68 +85,65 @@
"ws": "^7.5.3"
},
"devDependencies": {
"@babel/cli": "^7.14.8",
"@babel/core": "^7.15.0",
"@babel/node": "^7.14.9",
"@babel/plugin-proposal-class-properties": "^7.14.5",
"@babel/plugin-proposal-decorators": "^7.14.5",
"@babel/plugin-proposal-do-expressions": "^7.14.5",
"@babel/plugin-proposal-export-default-from": "^7.14.5",
"@babel/plugin-proposal-export-namespace-from": "^7.14.5",
"@babel/plugin-proposal-function-bind": "^7.14.5",
"@babel/plugin-proposal-function-sent": "^7.14.5",
"@babel/plugin-proposal-json-strings": "^7.14.5",
"@babel/plugin-proposal-logical-assignment-operators": "^7.14.5",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5",
"@babel/plugin-proposal-numeric-separator": "^7.14.5",
"@babel/plugin-proposal-object-rest-spread": "^7.14.7",
"@babel/plugin-proposal-optional-chaining": "^7.14.5",
"@babel/plugin-proposal-pipeline-operator": "^7.15.0",
"@babel/plugin-proposal-throw-expressions": "^7.14.5",
"@babel/cli": "^7.16.7",
"@babel/core": "^7.16.7",
"@babel/node": "^7.16.7",
"@babel/plugin-proposal-class-properties": "^7.16.7",
"@babel/plugin-proposal-decorators": "^7.16.7",
"@babel/plugin-proposal-do-expressions": "^7.16.7",
"@babel/plugin-proposal-export-default-from": "^7.16.7",
"@babel/plugin-proposal-export-namespace-from": "^7.16.7",
"@babel/plugin-proposal-function-bind": "^7.16.7",
"@babel/plugin-proposal-function-sent": "^7.16.7",
"@babel/plugin-proposal-json-strings": "^7.16.7",
"@babel/plugin-proposal-logical-assignment-operators": "^7.16.7",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.7",
"@babel/plugin-proposal-numeric-separator": "^7.16.7",
"@babel/plugin-proposal-object-rest-spread": "^7.16.7",
"@babel/plugin-proposal-optional-chaining": "^7.16.7",
"@babel/plugin-proposal-pipeline-operator": "^7.16.7",
"@babel/plugin-proposal-throw-expressions": "^7.16.7",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-syntax-import-meta": "^7.10.4",
"@babel/plugin-transform-flow-strip-types": "^7.14.5",
"@babel/plugin-transform-react-constant-elements": "^7.14.5",
"@babel/plugin-transform-react-inline-elements": "^7.14.5",
"@babel/polyfill": "^7.11.5",
"@babel/preset-env": "^7.15.0",
"@babel/preset-flow": "^7.14.5",
"@babel/preset-react": "^7.14.5",
"@babel/preset-typescript": "^7.15.0",
"@babel/plugin-transform-flow-strip-types": "^7.16.7",
"@babel/plugin-transform-react-constant-elements": "^7.16.7",
"@babel/plugin-transform-react-inline-elements": "^7.16.7",
"@babel/preset-env": "^7.16.7",
"@babel/preset-flow": "^7.16.7",
"@babel/preset-react": "^7.16.7",
"@babel/preset-typescript": "^7.16.7",
"assets-webpack-plugin": "^7.1.1",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.2.2",
"babel-loader": "^8.2.3",
"babel-plugin-transform-react-pure-class-to-function": "^1.0.1",
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
"babel-plugin-ttag": "^1.7.30",
"clean-css": "^5.1.5",
"copy-webpack-plugin": "^8.1.1",
"css-loader": "^5.2.7",
"eslint": "^7.32.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-config-airbnb-base": "^14.2.1",
"eslint-plugin-flowtype": "^5.9.0",
"eslint-plugin-import": "^2.24.1",
"eslint-plugin-jsx-a11y": "^6.3.1",
"eslint-plugin-react": "^7.24.0",
"flow-bin": "^0.143.1",
"generate-package-json-webpack-plugin": "^2.1.3",
"glob": "^7.1.7",
"clean-css": "^5.2.2",
"copy-webpack-plugin": "^10.2.0",
"css-loader": "^6.5.1",
"eslint": "^8.6.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-flowtype": "^8.0.3",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-react": "^7.28.0",
"flow-bin": "^0.168.0",
"generate-package-json-webpack-plugin": "^2.5.1",
"glob": "^7.2.0",
"json-loader": "^0.5.4",
"mkdirp": "^1.0.4",
"npm-check": "^5.9.2",
"parallel-webpack": "^2.6.0",
"react-hot-loader": "^4.13.0",
"react-svg-loader": "^3.0.3",
"rimraf": "^3.0.2",
"style-loader": "^2.0.0",
"style-loader": "^3.3.1",
"ttag-cli": "^1.9.3",
"webpack": "^5.51.1",
"webpack-bundle-analyzer": "^4.4.2",
"webpack-cli": "^4.8.0",
"webpack-dev-middleware": "^4.3.0",
"webpack-hot-middleware": "^2.18.0",
"webpack-node-externals": "^2.5.2",
"webpack": "^5.65.0",
"webpack-bundle-analyzer": "^4.5.0",
"webpack-cli": "^4.9.1",
"webpack-dev-middleware": "^5.3.0",
"webpack-hot-middleware": "^2.25.1",
"webpack-node-externals": "^3.0.0",
"write-file-webpack-plugin": "^4.0.2"
}
}

View File

@ -1,44 +0,0 @@
/*
* @flow
*/
import path from 'path';
import fs from 'fs';
/* eslint-disable no-console */
function patchImageQ() {
try {
/* fix image-q imports here
* Pretty dirty, but we did write an issue and they might
* update one day
*/
console.log('Patching image-q set-immediate import');
const regex = /core-js\/fn\/set-immediate/g;
const files = [
path.resolve(
__dirname, '..', 'node_modules',
'image-q', 'dist', 'esm', 'basicAPI.js',
),
path.resolve(
__dirname, '..', 'node_modules',
'image-q', 'dist', 'esm', 'helper.js',
),
];
files.forEach((file) => {
let fileContent = fs.readFileSync(file, 'utf8');
fileContent = fileContent.replace(
regex,
'core-js/features/set-immediate',
);
fs.writeFileSync(file, fileContent);
});
console.log('Patching image-q done');
} catch {
console.log('Error while patching image-q');
}
}
export default function patch() {
patchImageQ();
}

View File

@ -6,9 +6,14 @@
import React, { useState, useEffect } from 'react';
import { useSelector, shallowEqual } from 'react-redux';
import fileDownload from 'js-file-download';
import iq from 'image-q';
import { jt, t } from 'ttag';
import {
ColorDistanceCalculators,
ImageQuantizerKernels,
quantizeImage,
getImageDataOfFile,
} from '../utils/image';
import printGIMPPalette from '../core/exportGPL';
import { copyCanvasToClipboard } from '../utils/clipboard';
@ -43,181 +48,63 @@ function readFile(
fr.readAsDataURL(file);
}
const ColorDistanceCalculators = [
'Euclidean',
'Manhattan',
'CIEDE2000',
'CIE94Textiles',
'CIE94GraphicArts',
'EuclideanBT709NoAlpha',
'EuclideanBT709',
'ManhattanBT709',
'CMetric',
'PNGQuant',
'ManhattanNommyde',
];
function quantize(
pointContainer,
colors,
colorDist,
strategy,
onProgress,
) {
return new Promise((resolve, reject) => {
// read colors into palette
const palette = new iq.utils.Palette();
palette.add(iq.utils.Point.createByRGBA(0, 0, 0, 0));
colors.forEach((clr) => {
const [r, g, b] = clr;
const point = iq.utils.Point.createByRGBA(r, g, b, 255);
palette.add(point);
});
// construct color distance calculator
let distance;
switch (colorDist) {
case 'Euclidean':
distance = new iq.distance.Euclidean();
break;
case 'Manhattan':
distance = new iq.distance.Manhattan();
break;
case 'CIEDE2000':
distance = new iq.distance.CIEDE2000();
break;
case 'CIE94Textiles':
distance = new iq.distance.CIE94Textiles();
break;
case 'CIE94GraphicArts':
distance = new iq.distance.CIE94GraphicArts();
break;
case 'EuclideanBT709NoAlpha':
distance = new iq.distance.EuclideanBT709NoAlpha();
break;
case 'EuclideanBT709':
distance = new iq.distance.EuclideanBT709();
break;
case 'ManhattanBT709':
distance = new iq.distance.ManhattanBT709();
break;
case 'CMetric':
distance = new iq.distance.CMetric();
break;
case 'PNGQuant':
distance = new iq.distance.PNGQuant();
break;
case 'ManhattanNommyde':
distance = new iq.distance.ManhattanNommyde();
break;
default:
distance = new iq.distance.Euclidean();
}
// construct image quantizer
let imageQuantizer;
if (strategy === 'Nearest') {
imageQuantizer = new iq.image.NearestColor(distance);
} else if (strategy === 'Riemersma') {
imageQuantizer = new iq.image.ErrorDiffusionRiemersma(distance);
} else {
imageQuantizer = new iq.image.ErrorDiffusionArray(
distance,
iq.image.ErrorDiffusionArrayKernel[strategy],
true,
0,
false,
);
}
// quantize
let outPointContainer;
const iterator = imageQuantizer.quantize(pointContainer, palette);
const next = () => {
try {
const result = iterator.next();
if (result.done) {
resolve(outPointContainer);
} else {
if (result.value.pointContainer) {
outPointContainer = result.value.pointContainer;
}
if (onProgress) onProgress(result.value.progress);
setTimeout(next, 10);
}
} catch (error) {
reject(error);
}
};
setTimeout(next, 10);
});
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 drawPixels(idxi8, width, height) {
const can = document.createElement('canvas');
can.width = width;
can.height = height;
const ctx = can.getContext('2d');
ctx.imageSmoothingEnabled = false;
ctx.mozImageSmoothingEnabled = false;
ctx.webkitImageSmoothingEnabled = false;
ctx.msImageSmoothingEnabled = false;
const imgd = ctx.createImageData(can.width, can.height);
const { data } = imgd;
for (let i = 0, len = data.length; i < len; ++i) data[i] = idxi8[i];
ctx.putImageData(imgd, 0, 0);
return can;
}
function addGrid(img, lightGrid, offsetX, offsetY) {
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 = img.width * 5;
can.height = img.height * 5;
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(img, 0, 0);
ctx.drawImage(image, 0, 0);
ctx.restore();
ctx.fillStyle = (lightGrid) ? '#DDDDDD' : '#222222';
for (let i = 0; i <= img.width; i += 1) {
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 <= img.height; j += 1) {
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;
return ctx.getImageData(0, 0, can.width, can.height);
}
function scaleImage(img, width, height, doAA) {
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 / img.width;
const scaleY = height / img.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(img, 0, 0);
ctxo.drawImage(image, 0, 0);
ctxo.restore();
return can;
return ctxo.getImageData(0, 0, width, height);
}
// scale manually
const imdo = ctxo.createImageData(width, height);
const { data: datao } = imdo;
const cani = document.createElement('canvas');
cani.width = img.width;
cani.height = img.height;
const ctxi = cani.getContext('2d');
ctxi.drawImage(img, 0, 0);
const imdi = ctxi.getImageData(0, 0, img.width, img.height);
const { data: datai } = imdi;
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)
@ -229,30 +116,29 @@ function scaleImage(img, width, height, doAA) {
datao[poso] = datai[posi];
}
}
ctxo.putImageData(imdo, 0, 0);
return can;
return imdo;
}
let newOpts = null;
let renderOpts = null;
let rendering = false;
async function renderOutputImage(opts) {
if (!opts.file) {
return;
}
renderOpts = opts;
if (rendering) {
newOpts = opts;
console.log('skip rendering');
return;
}
console.log('render');
rendering = true;
let renderOpts = opts;
while (renderOpts) {
newOpts = null;
const {
file, dither, grid, scaling,
} = renderOpts;
renderOpts = null;
if (file) {
let image = file;
let pointContainer = null;
if (scaling.enabled) {
// scale
const { width, height, aa } = scaling;
@ -262,29 +148,18 @@ async function renderOutputImage(opts) {
height,
aa,
);
pointContainer = iq.utils.PointContainer.fromHTMLCanvasElement(image);
} else {
pointContainer = iq.utils.PointContainer.fromHTMLImageElement(image);
}
// dither
const { colors, strategy, colorDist } = dither;
const progEl = document.getElementById('qprog');
// eslint-disable-next-line no-await-in-loop
pointContainer = await quantize(
pointContainer,
colors,
colorDist,
image = await quantizeImage(colors, image, {
strategy,
(progress) => {
colorDist,
onProgress: (progress) => {
progEl.innerHTML = `Loading... ${Math.round(progress)} %`;
},
);
});
progEl.innerHTML = 'Done';
image = drawPixels(
pointContainer.toUint8Array(),
image.width,
image.height,
);
// grid
if (grid.enabled) {
const { light, offsetX, offsetY } = grid;
@ -300,10 +175,8 @@ async function renderOutputImage(opts) {
output.width = image.width;
output.height = image.height;
const ctx = output.getContext('2d');
ctx.drawImage(image, 0, 0);
ctx.putImageData(image, 0, 0);
}
// render again if requested in the meantime
renderOpts = newOpts;
}
rendering = false;
}
@ -321,7 +194,7 @@ function Converter() {
], shallowEqual);
const [selectedCanvas, selectCanvas] = useState(canvasId);
const [selectedFile, selectFile] = useState(null);
const [inputImageData, setInputImageData] = useState(null);
const [selectedStrategy, selectStrategy] = useState('Nearest');
const [selectedColorDist, selectColorDist] = useState('Euclidean');
const [selectedScaleKeepRatio, selectScaleKeepRatio] = useState(true);
@ -339,7 +212,7 @@ function Converter() {
});
useEffect(() => {
if (selectedFile) {
if (inputImageData) {
const canvas = canvases[selectedCanvas];
const dither = {
colors: canvas.colors.slice(canvas.cli),
@ -347,13 +220,20 @@ function Converter() {
colorDist: selectedColorDist,
};
renderOutputImage({
file: selectedFile,
file: inputImageData,
dither,
grid: gridData,
scaling: scaleData,
});
}
});
}, [
inputImageData,
selectedStrategy,
selectedColorDist,
scaleData,
selectedCanvas,
gridData,
]);
const {
enabled: gridEnabled,
@ -379,6 +259,7 @@ function Converter() {
<div style={{ textAlign: 'center' }}>
<div className="modalcotext">{t`Choose Canvas`}:&nbsp;
<select
value={selectedCanvas}
onChange={(e) => {
const sel = e.target;
selectCanvas(sel.options[sel.selectedIndex].value);
@ -391,7 +272,6 @@ function Converter() {
? null
: (
<option
selected={canvas === selectedCanvas}
value={canvas}
>
{
@ -431,28 +311,36 @@ function Converter() {
<input
type="file"
id="imgfile"
onChange={(evt) => {
onChange={async (evt) => {
const fileSel = evt.target;
const file = (!fileSel.files || !fileSel.files[0])
? null : fileSel.files[0];
readFile(file, selectFile, setScaleData);
if (!file) {
setInputImageData(null);
return;
}
const imageData = await getImageDataOfFile(file);
setScaleData({
enabled: false,
width: imageData.width,
height: imageData.height,
aa: true,
});
setInputImageData(imageData);
}}
/>
<p className="modalcotext">{t`Choose Strategy`}:&nbsp;
<select
value={selectedStrategy}
onChange={(e) => {
const sel = e.target;
selectStrategy(sel.options[sel.selectedIndex].value);
}}
>
{
['Nearest',
'Riemersma',
...Object.keys(iq.image.ErrorDiffusionArrayKernel),
].map((strat) => (
ImageQuantizerKernels.map((strat) => (
<option
value={strat}
selected={(selectedStrategy === strat)}
>{strat}</option>
))
}
@ -460,6 +348,7 @@ function Converter() {
</p>
<p className="modalcotext">{t`Choose Color Mode`}:&nbsp;
<select
value={selectedColorDist}
onChange={(e) => {
const sel = e.target;
selectColorDist(sel.options[sel.selectedIndex].value);
@ -469,7 +358,6 @@ function Converter() {
ColorDistanceCalculators.map((strat) => (
<option
value={strat}
selected={(selectedColorDist === strat)}
>{strat}</option>
))
}
@ -581,8 +469,8 @@ function Converter() {
const newWidth = (e.target.value > 1024)
? 1024 : e.target.value;
if (!newWidth) return;
if (selectedScaleKeepRatio && selectedFile) {
const ratio = selectedFile.width / selectedFile.height;
if (selectedScaleKeepRatio && inputImageData) {
const ratio = inputImageData.width / inputImageData.height;
const newHeight = Math.round(newWidth / ratio);
if (newHeight <= 0) return;
setScaleData({
@ -611,8 +499,8 @@ function Converter() {
const nuHeight = (e.target.value > 1024)
? 1024 : e.target.value;
if (!nuHeight) return;
if (selectedScaleKeepRatio && selectedFile) {
const ratio = selectedFile.width / selectedFile.height;
if (selectedScaleKeepRatio && inputImageData) {
const ratio = inputImageData.width / inputImageData.height;
const nuWidth = Math.round(ratio * nuHeight);
if (nuWidth <= 0) return;
setScaleData({
@ -655,11 +543,11 @@ function Converter() {
<button
type="button"
onClick={() => {
if (selectedFile) {
if (inputImageData) {
setScaleData({
...scaleData,
width: selectedFile.width,
height: selectedFile.height,
width: inputImageData.width,
height: inputImageData.height,
});
}
}}
@ -669,7 +557,7 @@ function Converter() {
</div>
)
: null}
{(selectedFile)
{(inputImageData)
? (
<div>
<p id="qprog">...</p>
@ -704,4 +592,4 @@ function Converter() {
);
}
export default React.memo(Converter);
export default Converter;

View File

@ -14,13 +14,16 @@ const DailyRankings = () => {
return (
<div style={{ overflowY: 'auto', display: 'inline-block' }}>
<table>
<tr>
<th>#</th>
<th>user</th>
<th>Pixels</th>
<th># Total</th>
<th>Total Pixels</th>
</tr>
<thead>
<tr>
<th>#</th>
<th>user</th>
<th>Pixels</th>
<th># Total</th>
<th>Total Pixels</th>
</tr>
</thead>
<tbody>
{
totalDailyRanking.map((rank) => (
<tr>
@ -32,6 +35,7 @@ const DailyRankings = () => {
</tr>
))
}
</tbody>
</table>
</div>
);

View File

@ -35,6 +35,7 @@ function LanguageSelect() {
<div style={{ textAlign: 'right' }}>
<span>
<select
value={langSel}
onChange={(e) => {
const sel = e.target;
setLangSel(sel.options[sel.selectedIndex].value);
@ -43,7 +44,6 @@ function LanguageSelect() {
{
langs.map(([l]) => (
<option
selected={l === langSel}
value={l}
>
{l.toUpperCase()}

View File

@ -12,13 +12,16 @@ const TotalRankings = () => {
return (
<div style={{ overflowY: 'auto', display: 'inline-block' }}>
<table>
<tr>
<th>#</th>
<th>user</th>
<th>Pixels</th>
<th># Today</th>
<th>Pixels Today</th>
</tr>
<thead>
<tr>
<th>#</th>
<th>user</th>
<th>Pixels</th>
<th># Today</th>
<th>Pixels Today</th>
</tr>
</thead>
<tbody>
{
totalRanking.map((rank) => (
<tr>
@ -30,6 +33,7 @@ const TotalRankings = () => {
</tr>
))
}
</tbody>
</table>
</div>
);

View File

@ -19,7 +19,7 @@ const WindowManager = () => {
return (
<div id="wm">
{
{
windowIds.map((id) => (<Window key={id} id={id} />))
}
</div>

View File

@ -36,7 +36,7 @@ const Help = () => {
const mailLink = <a href="mailto:pixelplanetdev@gmail.com">pixelplanetdev@gmail.com</a>;
return (
<p style={{ textAlign: 'center', paddingLeft: '5%', paddingRight: '5%' }}>
<div style={{ textAlign: 'center', paddingLeft: '5%', paddingRight: '5%' }}>
<p className="modaltext">
{t`Place color pixels on a large canvas with other players online!`}<br />
{t`Our main canvas is a huge worldmap, you can place wherever you like, but you will have to wait a specific \
@ -85,7 +85,7 @@ can be downloaded from mega.nz here: `}<a href="https://mega.nz/#!JpkBwAbJ!EnSLl
{jt`Click ${mouseSymbol} middle mouse button or ${touchSymbol} long-tap to select current hovering color`}<br />
</div>
<p>{t`Partners:`} <a href="https://www.crazygames.com/c/io" target="_blank" rel="noopener noreferrer">crazygames.com</a></p>
</p>
</div>
);
};

View File

@ -55,6 +55,7 @@ const SettingsItemSelect = ({
<h3 style={titleStyles} className="modaltitle">{title}</h3>
{(icon) && <img alt="" src={icon} />}
<select
value={selected}
onChange={(e) => {
const sel = e.target;
onSelect(sel.options[sel.selectedIndex].value);
@ -63,7 +64,6 @@ const SettingsItemSelect = ({
{
values.map((value) => (
<option
selected={value === selected}
value={value}
>
{value}

186
src/utils/image.js Normal file
View File

@ -0,0 +1,186 @@
/**
* Basic image manipulation and quantization
*
* @flow
*/
import { utils, distance, image } from 'image-q';
export const ColorDistanceCalculators = [
'Euclidean',
'Manhattan',
'CIEDE2000',
'CIE94Textiles',
'CIE94GraphicArts',
'EuclideanBT709NoAlpha',
'EuclideanBT709',
'ManhattanBT709',
'CMetric',
'PNGQuant',
'ManhattanNommyde',
];
export const ImageQuantizerKernels = [
'Nearest',
'Riemersma',
'FloydSteinberg',
'FalseFloydSteinberg',
'Stucki',
'Atkinson',
'Jarvis',
'Burkes',
'Sierra',
'TwoSierra',
'SierraLite',
];
export function getImageDataOfFile(file) {
return new Promise((resolve, reject) => {
const fr = new FileReader();
fr.onload = () => {
const img = new Image();
img.onload = () => {
const cani = document.createElement('canvas');
cani.width = img.width;
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);
};
img.onerror = (error) => reject(error);
img.src = fr.result;
};
fr.onerror = (error) => reject(error);
fr.readAsDataURL(file);
});
}
function createPointContainerFromImageData(imageData) {
const { width, height, data } = imageData;
console.log('create container from image data', data);
const pointContainer = new utils.PointContainer();
pointContainer.setWidth(width);
pointContainer.setHeight(height);
const pointArray = pointContainer.getPointArray();
let i = 0;
while (i < data.length) {
const point = utils.Point.createByRGBA(
data[i++],
data[i++],
data[i++],
data[i++],
);
pointArray.push(point);
}
return pointContainer;
}
function createImageDataFromPointContainer(pointContainer) {
const width = pointContainer.getWidth();
const height = pointContainer.getHeight();
const data = pointContainer.toUint8Array();
const can = document.createElement('canvas');
can.width = width;
can.height = height;
const ctx = can.getContext('2d');
const idata = ctx.createImageData(width, height);
idata.data.set(data);
return idata;
}
export function quantizeImage(colors, imageData, opts) {
console.log('quantize image');
return new Promise((resolve, reject) => {
const pointContainer = createPointContainerFromImageData(imageData);
const strategy = opts.strategy || 'Nearest';
const colorDist = opts.colorDist || 'Euclidean';
// create palette
const palette = new utils.Palette();
palette.add(utils.Point.createByRGBA(0, 0, 0, 0));
for (let i = 0; i < colors.length; i += 1) {
const [r, g, b] = colors[i];
const point = utils.Point.createByRGBA(r, g, b, 255);
palette.add(point);
}
console.log('palette', palette);
// construct color distance calculator
let distCalc;
switch (colorDist) {
case 'Euclidean':
distCalc = new distance.Euclidean();
break;
case 'Manhattan':
distCalc = new distance.Manhattan();
break;
case 'CIEDE2000':
distCalc = new distance.CIEDE2000();
break;
case 'CIE94Textiles':
distCalc = new distance.CIE94Textiles();
break;
case 'CIE94GraphicArts':
distCalc = new distance.CIE94GraphicArts();
break;
case 'EuclideanBT709NoAlpha':
distCalc = new distance.EuclideanBT709NoAlpha();
break;
case 'EuclideanBT709':
distCalc = new distance.EuclideanBT709();
break;
case 'ManhattanBT709':
distCalc = new distance.ManhattanBT709();
break;
case 'CMetric':
distCalc = new distance.CMetric();
break;
case 'PNGQuant':
distCalc = new distance.PNGQuant();
break;
case 'ManhattanNommyde':
distCalc = new distance.ManhattanNommyde();
break;
default:
distCalc = new distance.Euclidean();
}
// idk why i need this :/
if (distCalc._setDefaults) distCalc._setDefaults();
// construct image quantizer
let imageQuantizer;
if (strategy === 'Nearest') {
imageQuantizer = new image.NearestColor(distCalc);
} else if (strategy === 'Riemersma') {
imageQuantizer = new image.ErrorDiffusionRiemersma(distCalc);
} else {
imageQuantizer = new image.ErrorDiffusionArray(
distCalc,
image.ErrorDiffusionArrayKernel[strategy],
true,
0,
false,
);
}
console.log('quantizer', imageQuantizer);
// quantize
let outPointContainer;
const iterator = imageQuantizer.quantize(pointContainer, palette);
const next = () => {
try {
const result = iterator.next();
if (result.done) {
resolve(createImageDataFromPointContainer(outPointContainer));
} else {
if (result.value.pointContainer) {
outPointContainer = result.value.pointContainer;
}
if (opts.onProgress) opts.onProgress(result.value.progress);
setTimeout(next, 0);
}
} catch (error) {
reject(error);
}
};
setTimeout(next, 0);
});
}

View File

@ -43,18 +43,7 @@ export function buildWebpackClientConfig(
const babelPlugins = [
'@babel/plugin-transform-flow-strip-types',
['@babel/plugin-proposal-decorators', { legacy: true }],
'@babel/plugin-proposal-function-sent',
'@babel/plugin-proposal-export-namespace-from',
'@babel/plugin-proposal-numeric-separator',
'@babel/plugin-proposal-throw-expressions',
['@babel/plugin-proposal-class-properties', { loose: true }],
['@babel/plugin-proposal-private-methods', { loose: true }],
[
'@babel/plugin-proposal-private-property-in-object',
{ loose: true },
],
'@babel/proposal-object-rest-spread',
// react-optimize
'@babel/transform-react-constant-elements',
'@babel/transform-react-inline-elements',
@ -69,7 +58,7 @@ export function buildWebpackClientConfig(
context: __dirname,
mode: (development) ? 'development' : 'production',
devtool: 'source-map',
devtool: (development) ? 'eval' : false,
entry: {
[(locale !== 'default') ? `client-${locale}` : 'client']:
@ -121,7 +110,7 @@ export function buildWebpackClientConfig(
],
},
{
test: /\.(js|jsx|ts|tsx)$/,
test: /\.(js|jsx)$/,
loader: 'babel-loader',
include: [
path.resolve(__dirname, 'src'),
@ -136,14 +125,13 @@ export function buildWebpackClientConfig(
targets: {
browsers: pkg.browserslist,
},
modules: false,
useBuiltIns: 'usage',
corejs: {
version: 3,
},
debug: false,
}],
'@babel/typescript',
//'@babel/typescript',
'@babel/react',
],
plugins: babelPlugins,
@ -214,7 +202,6 @@ export function buildWebpackClientConfig(
* maybe some day in the future it might be
* better than babel-loader cacheDirectory,
* but right now it isn't
*
cache: {
type: 'filesystem',
cacheDirectory: path.resolve(
@ -225,6 +212,7 @@ export function buildWebpackClientConfig(
),
},
*/
cache: false,
};
}
@ -250,12 +238,12 @@ function buildWebpackClientConfigAllLangs(development, analyze) {
}
export default ({
debug, analyze, extract, locale,
development, analyze, extract, locale,
}) => {
if (extract || locale) {
return buildWebpackClientConfig(
debug, analyze, locale || 'default', extract,
development, analyze, locale || 'default', extract,
);
}
return buildWebpackClientConfigAllLangs(debug, analyze);
return buildWebpackClientConfigAllLangs(development, analyze);
};

View File

@ -7,11 +7,8 @@ import nodeExternals from 'webpack-node-externals';
import GeneratePackageJsonPlugin from 'generate-package-json-webpack-plugin';
import CopyPlugin from 'copy-webpack-plugin';
import patch from './scripts/patch';
import pkg from './package.json';
patch();
const basePackageValues = {
name: pkg.name,
version: pkg.version,
@ -29,18 +26,7 @@ const ttag = {};
const babelPlugins = [
'@babel/plugin-transform-flow-strip-types',
['@babel/plugin-proposal-decorators', { legacy: true }],
'@babel/plugin-proposal-function-sent',
'@babel/plugin-proposal-export-namespace-from',
'@babel/plugin-proposal-numeric-separator',
'@babel/plugin-proposal-throw-expressions',
['@babel/plugin-proposal-class-properties', { loose: true }],
['@babel/plugin-proposal-private-methods', { loose: true }],
[
'@babel/plugin-proposal-private-property-in-object',
{ loose: true },
],
'@babel/proposal-object-rest-spread',
// react-optimize
'@babel/transform-react-constant-elements',
'@babel/transform-react-inline-elements',
@ -51,7 +37,7 @@ const babelPlugins = [
export default ({
debug, extract,
development, extract,
}) => {
if (extract) {
ttag.extract = {
@ -65,7 +51,7 @@ export default ({
target: 'node',
context: __dirname,
mode: (debug) ? 'development' : 'production',
mode: (development) ? 'development' : 'production',
entry: {
web: [path.resolve(__dirname, 'src', 'web.js')],
@ -140,7 +126,7 @@ export default ({
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': debug ? '"development"' : '"production"',
'process.env.NODE_ENV': development ? '"development"' : '"production"',
'process.env.BROWSER': false,
}),
// create package.json for deployment