From 3e86ea25be394de787a4703fe3ff991e1b49e4c3 Mon Sep 17 00:00:00 2001 From: HF Date: Fri, 8 May 2020 15:41:03 +0200 Subject: [PATCH] Add converter in usermodal --- package-lock.json | 5 + package.json | 1 + src/client.js | 2 +- src/components/Converter.jsx | 329 ++++++++++++++++++++++++++++--- src/components/DailyRankings.jsx | 2 +- src/components/LogInForm.jsx | 28 ++- src/components/TotalRankings.jsx | 2 +- src/components/UserAreaModal.jsx | 99 +++++++--- src/components/UserMessages.jsx | 2 +- src/components/base.tcss | 4 +- src/core/exportGPL.js | 37 ++++ 11 files changed, 436 insertions(+), 75 deletions(-) create mode 100644 src/core/exportGPL.js diff --git a/package-lock.json b/package-lock.json index cfd23eb..4a537ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11285,6 +11285,11 @@ "any-promise": "^1.3.0" } }, + "rgbquant": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/rgbquant/-/rgbquant-1.1.2.tgz", + "integrity": "sha1-2V6Imo/LHmwaT6LMyL46QaL80YU=" + }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", diff --git a/package.json b/package.json index 9868902..cf7c00a 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "redux-logger": "^3.0.6", "redux-persist": "^6.0.0", "redux-thunk": "^2.2.0", + "rgbquant": "^1.1.2", "sequelize": "^5.21.7", "sharp": "^0.25.2", "startaudiocontext": "^1.2.1", diff --git a/src/client.js b/src/client.js index e7a2e8f..60cb922 100644 --- a/src/client.js +++ b/src/client.js @@ -84,7 +84,7 @@ function init() { } if (cnt > 1) { document.body.style.setProperty( - "-webkit-transform", "rotate(-180deg)", + '-webkit-transform', 'rotate(-180deg)', null, ); fetch('https://assets.pixelplanet.fun/iamabot'); diff --git a/src/components/Converter.jsx b/src/components/Converter.jsx index e01a93a..d05545c 100644 --- a/src/components/Converter.jsx +++ b/src/components/Converter.jsx @@ -3,10 +3,12 @@ * @flow */ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { connect } from 'react-redux'; import fileDownload from 'js-file-download'; +import RgbQuant from 'rgbquant'; +import printGIMPPalette from '../core/exportGPL'; import type { State } from '../reducers'; const titleStyle = { @@ -18,7 +20,7 @@ const titleStyle = { lineHeight: '24px', fontSize: 16, fontWeight: 500, - // marginTop: 0, + marginTop: 4, marginBottom: 0, }; @@ -32,35 +34,117 @@ const textStyle = { lineHeight: 'normal', }; -function appendNumberText(number) { - let appendStr = `${number} `; - if (number < 10) appendStr += ' '; - else if (number < 100) appendStr += ' '; - return appendStr; -} -function appendHexColorText(clr) { - let appendStr = ' #'; - clr.forEach((z) => { - if (z < 16) appendStr += '0'; - appendStr += z.toString(16); - }); - return appendStr; +function downloadOutput() { + const output = document.getElementById('imgoutput'); + output.toBlob((blob) => fileDownload(blob, 'ppfunconvert.png')); } +function readFile( + file, + selectFile, +) { + if (!file) { + return; + } + const fr = new FileReader(); + fr.onload = () => { + const img = new Image(); + img.onload = () => { + selectFile(img); + }; + img.src = fr.result; + }; + fr.readAsDataURL(file); +} -function printGIMPPalette(title, description, colors) { - let text = `GIMP Palette -#Palette Name: Pixelplanet${title} -#Description: ${description} -#Colors: ${colors.length}`; - colors.forEach((clr) => { - text += '\n'; - clr.forEach((z) => { - text += appendNumberText(z); - }); - text += appendHexColorText(clr); +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) { + const can = document.createElement('canvas'); + const ctx = can.getContext('2d'); + can.width = img.width * 5; + can.height = img.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.restore(); + ctx.fillStyle = (lightGrid) ? '#DDDDDD' : '#222222'; + + for (let i = 0; i <= img.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) { + const thick = ((j + (offsetY * 1)) % 10 === 0) ? 2 : 1; + ctx.fillRect(0, j * 5, can.width, thick); + } + + return can; +} + +function renderOutputImage( + colors, + selectedFile, + selectedStrategy, + selectedSerp, + selectedColorDist, + selectedDithDelta, + selectedAddGrid, + selectedLightGrid, + selectedGridOffsetX, + selectedGridOffsetY, +) { + if (!selectedFile) { + return; + } + const rgbQuant = new RgbQuant({ + colors: colors.length, + dithKern: selectedStrategy, + dithDelta: selectedDithDelta / 100, + dithSerp: selectedSerp, + palette: colors, + reIndex: false, + useCache: false, + colorDist: selectedColorDist, }); - fileDownload(text, `Pixelplanet${title}.gpl`); + rgbQuant.sample(selectedFile); + rgbQuant.palette(); + const pxls = rgbQuant.reduce(selectedFile); + let can = drawPixels(pxls, selectedFile.width, selectedFile.height); + const output = document.getElementById('imgoutput'); + if (selectedAddGrid) { + can = addGrid( + can, + selectedLightGrid, + selectedGridOffsetX, + selectedGridOffsetY, + ); + } + output.width = can.width; + output.height = can.height; + const ctx = output.getContext('2d'); + ctx.drawImage(can, 0, 0); } @@ -69,6 +153,41 @@ function Converter({ canvases, }) { const [selectedCanvas, selectCanvas] = useState(canvasId); + const [selectedFile, selectFile] = useState(null); + const [selectedStrategy, selectStrategy] = useState(''); + const [selectedSerp, selectSerp] = useState(false); + const [selectedColorDist, selectColorDist] = useState('euclidean'); + const [selectedDithDelta, selectDithDelta] = useState(0); + const [selectedAddGrid, selectAddGrid] = useState(true); + const [selectedLightGrid, selectLightGrid] = useState(false); + const [selectedGridOffsetX, selectGridOffsetX] = useState(0); + const [selectedGridOffsetY, selectGridOffsetY] = useState(0); + const input = document.createElement('canvas'); + + useEffect(() => { + if (selectedFile) { + const canvas = canvases[selectedCanvas]; + renderOutputImage( + canvas.colors.slice(canvas.cli), + selectedFile, + selectedStrategy, + selectedSerp, + selectedColorDist, + selectedDithDelta, + selectedAddGrid, + selectedLightGrid, + selectedGridOffsetX, + selectedGridOffsetY, + ); + } else { + const output = document.getElementById('imgoutput'); + const ctx = output.getContext('2d'); + output.width = 128; + output.height = 100; + ctx.fillStyle = '#C4C4C4'; + ctx.fillRect(0, 0, 128, 100); + } + }); return (

@@ -95,22 +214,172 @@ function Converter({ }

+

Palette Download

Palette for GIMP -

Credit for the Palette of the Moon goes to starhouse.

+

Credit for the Palette of the Moon goes to + starhouse.

+

Image Converter

+

Convert an image to canvas colors

+ { + const fileSel = evt.target; + const file = (!fileSel.files || !fileSel.files[0]) + ? null : fileSel.files[0]; + readFile(file, selectFile); + }} + /> +

Choose Strategy:  + +

+ + { + selectSerp(e.target.checked); + }} + /> + Serpentine Pattern Dithering +   + + { + const colorDist = (e.target.checked) + ? 'euclidean' : 'manhatten'; + selectColorDist(colorDist); + }} + /> + Manhatten Color Distance + +

Dithering Delta:  + { + selectDithDelta(e.target.value); + }} + />% +

+

+ { + selectAddGrid(e.target.checked); + }} + /> + Add Grid +

+ {(selectedAddGrid) + ? ( +
+

+ { + selectLightGrid(e.target.checked); + }} + /> + Light Grid +

+ Offset X:  + { + selectGridOffsetX(e.target.value); + }} + />% + + Offset Y:  + { + selectGridOffsetY(e.target.value); + }} + />% + +
+ ) + : null} +

+ +

+

); } diff --git a/src/components/DailyRankings.jsx b/src/components/DailyRankings.jsx index f83cff7..672519c 100644 --- a/src/components/DailyRankings.jsx +++ b/src/components/DailyRankings.jsx @@ -9,7 +9,7 @@ import { connect } from 'react-redux'; import type { State } from '../reducers'; const DailyRankings = ({ totalDailyRanking }) => ( -
+
diff --git a/src/components/LogInForm.jsx b/src/components/LogInForm.jsx index f25ca57..d3a71a9 100644 --- a/src/components/LogInForm.jsx +++ b/src/components/LogInForm.jsx @@ -20,7 +20,7 @@ function validate(nameoremail, password) { return errors; } -async function submit_login(nameoremail, password, component) { +async function submitLogin(nameoremail, password) { const body = JSON.stringify({ nameoremail, password, @@ -37,8 +37,9 @@ async function submit_login(nameoremail, password, component) { } const inputStyles = { - display: 'block', + display: 'inline-block', width: '100%', + maxWidth: '35em', }; class LogInForm extends React.Component { @@ -59,6 +60,7 @@ class LogInForm extends React.Component { e.preventDefault(); const { nameoremail, password, submitting } = this.state; + const { me: setMe } = this.props; if (submitting) return; const errors = validate(nameoremail, password); @@ -67,7 +69,10 @@ class LogInForm extends React.Component { if (errors.length > 0) return; this.setState({ submitting: true }); - const { errors: resperrors, me } = await submit_login(nameoremail, password); + const { errors: resperrors, me } = await submitLogin( + nameoremail, + password, + ); if (resperrors) { this.setState({ errors: resperrors, @@ -75,32 +80,37 @@ class LogInForm extends React.Component { }); return; } - this.props.me(me); + setMe(me); } render() { - const { errors } = this.state; + const { + errors, nameoremail, password, submitting, + } = this.state; return ( {errors.map((error) => (

Error: {error}

))} this.setState({ nameoremail: evt.target.value })} type="text" placeholder="Name or Email" /> this.setState({ password: evt.target.value })} type="password" placeholder="Password" /> - - +

+ +

); } diff --git a/src/components/TotalRankings.jsx b/src/components/TotalRankings.jsx index 95c9bab..a227f90 100644 --- a/src/components/TotalRankings.jsx +++ b/src/components/TotalRankings.jsx @@ -9,7 +9,7 @@ import { connect } from 'react-redux'; import type { State } from '../reducers'; const TotalRankings = ({ totalRanking }) => ( -
+
#
diff --git a/src/components/UserAreaModal.jsx b/src/components/UserAreaModal.jsx index 82a5479..47ab544 100644 --- a/src/components/UserAreaModal.jsx +++ b/src/components/UserAreaModal.jsx @@ -10,8 +10,6 @@ import Modal from './Modal'; import type { State } from '../reducers'; -const Converter = React.lazy(() => import(/* webpackChunkName: "converter" */ './Converter')); - import { showRegisterModal, showForgotPasswordModal, setName, setMailreg, receiveMe, @@ -21,23 +19,13 @@ import Tabs from './Tabs'; import UserArea from './UserArea'; import Rankings from './Rankings'; +// eslint-disable-next-line max-len +const Converter = React.lazy(() => import(/* webpackChunkName: "converter" */ './Converter')); + const logoStyle = { marginRight: 5, }; -const titleStyle = { - color: '#4f545c', - marginLeft: 0, - marginRight: 10, - overflow: 'hidden', - wordWrap: 'break-word', - lineHeight: '24px', - fontSize: 16, - fontWeight: 500, - // marginTop: 0, - marginBottom: 0, -}; - const textStyle = { color: 'hsla(218, 5%, 47%, .6)', fontSize: 14, @@ -55,32 +43,77 @@ const LogInArea = ({ register, forgot_password, me }) => (

Login to access more features and stats.


Login with Mail:

-

I forgot my Password.

+

+ I forgot my Password.

or login with:

- Discord - Google - Facebook - vk - vk + + Discord + + + Google + + + Facebook + + + vk + + + vk +

or register here:

); const UserAreaModal = ({ - name, register, forgot_password, doMe, logout, setName, setMailreg, canvases, + name, register, forgot_password, doMe, logout, setUserName, setUserMailreg, }) => (

{(name === null) - ? + ? ( + + ) : (

@@ -93,7 +126,9 @@ const UserAreaModal = ({
)} -

Also join our Discord: pixelplanet.fun/discord

+

Also join our Discord:  + pixelplanet.fun/discord +

); @@ -109,14 +144,17 @@ function mapDispatchToProps(dispatch) { doMe(me) { dispatch(receiveMe(me)); }, - setName(name) { + setUserName(name) { dispatch(setName(name)); }, - setMailreg(mailreg) { + setUserMailreg(mailreg) { dispatch(setMailreg(mailreg)); }, async logout() { - const response = await fetch('./api/auth/logout', { credentials: 'include' }); + const response = await fetch( + './api/auth/logout', + { credentials: 'include' }, + ); if (response.ok) { const resp = await response.json(); dispatch(receiveMe(resp.me)); @@ -127,8 +165,7 @@ function mapDispatchToProps(dispatch) { function mapStateToProps(state: State) { const { name } = state.user; - const { canvases } = state.canvas; - return { name, canvases }; + return { name }; } export default connect(mapStateToProps, mapDispatchToProps)(UserAreaModal); diff --git a/src/components/UserMessages.jsx b/src/components/UserMessages.jsx index a4a5bbb..3c5c35a 100644 --- a/src/components/UserMessages.jsx +++ b/src/components/UserMessages.jsx @@ -79,7 +79,7 @@ class UserMessages extends React.Component { {(messages.includes('not_verified') && messages.splice(messages.indexOf('not_verified'), 1)) ? (

- Please verify your mail address or your account could get deleted after a few days. + Please verify your mail address or your account could get deleted after a few days.  {(this.state.verify_answer) ? {this.state.verify_answer} : Click here to request a new verification mail.} diff --git a/src/components/base.tcss b/src/components/base.tcss index 5193967..4b36116 100644 --- a/src/components/base.tcss +++ b/src/components/base.tcss @@ -217,7 +217,8 @@ kbd { padding: 20px 40px; transform: translate(-50%, -50%); height: 80%; - max-height: 700px; + max-height: 900px; + width: 70%; transition: all 0.5s ease 0s; } @media (max-width: 604px) { @@ -228,6 +229,7 @@ kbd { right: 0px; bottom: 0px; height: 100%; + width: 100%; transform: none; max-width: none; max-height: none; diff --git a/src/core/exportGPL.js b/src/core/exportGPL.js new file mode 100644 index 0000000..f975e2e --- /dev/null +++ b/src/core/exportGPL.js @@ -0,0 +1,37 @@ +/* + * + * @flow + */ + +function appendNumberText(number) { + let appendStr = `${number} `; + if (number < 10) appendStr += ' '; + else if (number < 100) appendStr += ' '; + return appendStr; +} +function appendHexColorText(clr) { + let appendStr = ' #'; + clr.forEach((z) => { + if (z < 16) appendStr += '0'; + appendStr += z.toString(16); + }); + return appendStr; +} + + +function printGIMPPalette(title, description, colors) { + let text = `GIMP Palette +#Palette Name: Pixelplanet${title} +#Description: ${description} +#Colors: ${colors.length}`; + colors.forEach((clr) => { + text += '\n'; + clr.forEach((z) => { + text += appendNumberText(z); + }); + text += appendHexColorText(clr); + }); + return text; +} + +export default printGIMPPalette;

#