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 for GIMP: -
Credit for the Palette of the Moon goes to starhouse.
+Credit for the Palette of the Moon goes to + starhouse.
+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); + }} + />% + ++ +
+ ); } 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 ( ); } 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 }) => (
---|