Add converter in usermodal

This commit is contained in:
HF 2020-05-08 15:41:03 +02:00
parent 80fe10da7b
commit 3e86ea25be
11 changed files with 436 additions and 75 deletions

5
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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');

View File

@ -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 (
<p style={{ textAlign: 'center' }}>
@ -95,22 +214,172 @@ function Converter({
}
</select>
</p>
<h3 style={titleStyle}>Palette Download</h3>
<p style={textStyle}>
Palette for <a href="https://www.gimp.org">GIMP</a>:&nbsp;
<button
type="button"
style={{ display: 'inline' }}
onClick={() => {
const canvas = canvases[selectedCanvas];
const {
title, desc, colors, cli,
} = canvas;
printGIMPPalette(title, desc, colors.slice(cli));
fileDownload(
printGIMPPalette(title, desc, colors.slice(cli)),
`Pixelplanet${title}.gpl`,
);
}}
>
Download Palette
Download
</button>
<p>Credit for the Palette of the Moon goes to <a href="https://twitter.com/starhousedev">starhouse</a>.</p>
<p>Credit for the Palette of the Moon goes to
<a href="https://twitter.com/starhousedev">starhouse</a>.</p>
</p>
<h3 style={titleStyle}>Image Converter</h3>
<p style={textStyle}>Convert an image to canvas colors</p>
<input
type="file"
id="imgfile"
onChange={(evt) => {
const fileSel = evt.target;
const file = (!fileSel.files || !fileSel.files[0])
? null : fileSel.files[0];
readFile(file, selectFile);
}}
/>
<p style={textStyle}>Choose Strategy:&nbsp;
<select
onChange={(e) => {
const sel = e.target;
selectStrategy(sel.options[sel.selectedIndex].value);
}}
>
<option
value=""
selected={(selectedStrategy === '')}
>Default</option>
{
['FloydSteinberg',
'Stucki',
'Atkinson',
'Jarvis',
'Burkes',
'Sierra',
'TwoSierra',
'SierraLite',
'FalseFloydSteinberg'].map((strat) => (
<option
value={strat}
selected={(selectedStrategy === strat)}
>{strat}</option>
))
}
</select>
</p>
<span style={{ ...textStyle, fontHeight: 16 }}>
<input
type="checkbox"
onChange={(e) => {
selectSerp(e.target.checked);
}}
/>
Serpentine Pattern Dithering
</span>&nbsp;
<span style={{ ...textStyle, fontHeight: 16 }}>
<input
type="checkbox"
onClick={(e) => {
const colorDist = (e.target.checked)
? 'euclidean' : 'manhatten';
selectColorDist(colorDist);
}}
/>
Manhatten Color Distance
</span>
<p style={textStyle}>Dithering Delta:&nbsp;
<input
type="number"
step="1"
min="0"
max="100"
style={{ width: '3em' }}
value={selectedDithDelta}
onChange={(e) => {
selectDithDelta(e.target.value);
}}
/>%
</p>
<p style={{ ...textStyle, fontHeight: 16 }}>
<input
type="checkbox"
checked={selectedAddGrid}
onChange={(e) => {
selectAddGrid(e.target.checked);
}}
/>
Add Grid
</p>
{(selectedAddGrid)
? (
<div style={{
borderStyle: 'solid',
borderColor: '#D4D4D4',
borderWidth: 2,
padding: 5,
display: 'inline-block',
}}
>
<p style={{ ...textStyle, fontHeight: 16 }}>
<input
type="checkbox"
onChange={(e) => {
selectLightGrid(e.target.checked);
}}
/>
Light Grid
</p>
<span style={textStyle}>Offset X:&nbsp;
<input
type="number"
step="1"
min="0"
max="10"
style={{ width: '2em' }}
value={selectedGridOffsetX}
onChange={(e) => {
selectGridOffsetX(e.target.value);
}}
/>%
</span>
<span style={textStyle}>Offset Y:&nbsp;
<input
type="number"
step="1"
min="0"
max="10"
style={{ width: '2em' }}
value={selectedGridOffsetY}
onChange={(e) => {
selectGridOffsetY(e.target.value);
}}
/>%
</span>
</div>
)
: null}
<p>
<canvas
id="imgoutput"
style={{ width: '80%' }}
/>
</p>
<button
type="button"
onClick={downloadOutput}
>
Download Template
</button>
</p>
);
}

View File

@ -9,7 +9,7 @@ import { connect } from 'react-redux';
import type { State } from '../reducers';
const DailyRankings = ({ totalDailyRanking }) => (
<div style={{ overflowY: 'auto' }}>
<div style={{ overflowY: 'auto', display: 'inline-block' }}>
<table>
<tr>
<th>#</th>

View File

@ -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 (
<form onSubmit={this.handleSubmit}>
{errors.map((error) => (
<p key={error}>Error: {error}</p>
))}
<input
value={nameoremail}
style={inputStyles}
value={this.state.nameoremail}
onChange={(evt) => this.setState({ nameoremail: evt.target.value })}
type="text"
placeholder="Name or Email"
/>
<input
value={password}
style={inputStyles}
value={this.state.password}
onChange={(evt) => this.setState({ password: evt.target.value })}
type="password"
placeholder="Password"
/>
<button type="submit">{(this.state.submitting) ? '...' : 'LogIn'}</button>
<p>
<button type="submit">
{(submitting) ? '...' : 'LogIn'}
</button>
</p>
</form>
);
}

View File

@ -9,7 +9,7 @@ import { connect } from 'react-redux';
import type { State } from '../reducers';
const TotalRankings = ({ totalRanking }) => (
<div style={{ overflowY: 'auto' }}>
<div style={{ overflowY: 'auto', display: 'inline-block' }}>
<table>
<tr>
<th>#</th>

View File

@ -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 }) => (
<p style={textStyle}>Login to access more features and stats.</p><br />
<h2>Login with Mail:</h2>
<LogInForm me={me} />
<p className="modallink" onClick={forgot_password}>I forgot my Password.</p>
<p
className="modallink"
onClick={forgot_password}
>
I forgot my Password.</p>
<h2>or login with:</h2>
<a href="./api/auth/discord"><img style={logoStyle} width={32} src={`${window.assetserver}/discordlogo.svg`} alt="Discord" /></a>
<a href="./api/auth/google"><img style={logoStyle} width={32} src={`${window.assetserver}/googlelogo.svg`} alt="Google" /></a>
<a href="./api/auth/facebook"><img style={logoStyle} width={32} src={`${window.assetserver}/facebooklogo.svg`} alt="Facebook" /></a>
<a href="./api/auth/vk"><img style={logoStyle} width={32} src={`${window.assetserver}/vklogo.svg`} alt="vk" /></a>
<a href="./api/auth/reddit"><img style={logoStyle} width={32} src={`${window.assetserver}/redditlogo.svg`} alt="vk" /></a>
<a href="./api/auth/discord">
<img
style={logoStyle}
width={32}
src={`${window.assetserver}/discordlogo.svg`}
alt="Discord"
/>
</a>
<a href="./api/auth/google">
<img
style={logoStyle}
width={32}
src={`${window.assetserver}/googlelogo.svg`}
alt="Google"
/>
</a>
<a href="./api/auth/facebook">
<img
style={logoStyle}
width={32}
src={`${window.assetserver}/facebooklogo.svg`}
alt="Facebook"
/>
</a>
<a href="./api/auth/vk">
<img
style={logoStyle}
width={32}
src={`${window.assetserver}/vklogo.svg`}
alt="vk"
/>
</a>
<a href="./api/auth/reddit">
<img
style={logoStyle}
width={32}
src={`${window.assetserver}/redditlogo.svg`}
alt="vk"
/>
</a>
<h2>or register here:</h2>
<button type="button" onClick={register}>Register</button>
</p>
);
const UserAreaModal = ({
name, register, forgot_password, doMe, logout, setName, setMailreg, canvases,
name, register, forgot_password, doMe, logout, setUserName, setUserMailreg,
}) => (
<Modal title="User Area">
<p style={{ textAlign: 'center' }}>
{(name === null)
? <LogInArea register={register} forgot_password={forgot_password} me={doMe} />
? (
<LogInArea
register={register}
forgot_password={forgot_password}
me={doMe}
/>
)
: (
<Tabs>
<div label="Profile">
<UserArea
logout={logout}
set_name={setName}
set_mailreg={setMailreg}
set_name={setUserName}
set_mailreg={setUserMailreg}
/>
</div>
<div label="Ranking">
@ -93,7 +126,9 @@ const UserAreaModal = ({
</div>
</Tabs>
)}
<p>Also join our Discord: <a href="./discord" target="_blank">pixelplanet.fun/discord</a></p>
<p>Also join our Discord:&nbsp;
<a href="./discord" target="_blank">pixelplanet.fun/discord</a>
</p>
</p>
</Modal>
);
@ -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);

View File

@ -79,7 +79,7 @@ class UserMessages extends React.Component {
{(messages.includes('not_verified') && messages.splice(messages.indexOf('not_verified'), 1))
? (
<p className="usermessages">
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.&nbsp;
{(this.state.verify_answer)
? <span className="modallink">{this.state.verify_answer}</span>
: <span className="modallink" onClick={this.submit_resend_verify}>Click here to request a new verification mail.</span>}

View File

@ -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;

37
src/core/exportGPL.js Normal file
View File

@ -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;