├── .npmrc ├── public ├── _redirects ├── index.dev.html └── index.html ├── .browserslistrc ├── .huskyrc ├── src ├── utils │ ├── polyfills.js │ ├── random.js │ ├── intervals.js │ ├── breakpoints.js │ ├── throttle.js │ ├── ImageToCanvas.js │ ├── startup.js │ ├── cssParse.js │ ├── loadFromCanvas.js │ ├── outputParse.js │ ├── storage.js │ ├── color.js │ ├── canvasGIF.js │ └── drawHandlersProvider.js ├── assets │ ├── favicon.ico │ ├── bmac-icon.png │ ├── coindrop-img.png │ ├── regular-icon.png │ └── apple-touch-icon.png ├── css │ ├── components │ │ ├── _Dimensions.css │ │ ├── _Move.css │ │ ├── _Bucket.css │ │ ├── _CellInfo.css │ │ ├── _Eraser.css │ │ ├── _SaveDrawing.css │ │ ├── _EyeDropper.css │ │ ├── _PaletteGrid.css │ │ ├── _DownloadDrawing.css │ │ ├── _Reset.css │ │ ├── _NewProject.css │ │ ├── _PaletteColor.css │ │ ├── _ColorPicker.css │ │ ├── _CssDisplay.css │ │ ├── _CellSize.css │ │ ├── _Duration.css │ │ ├── _UndoRedo.css │ │ ├── _CopyCss.css │ │ ├── _FramesHandler.css │ │ ├── _PixelGrid.css │ │ ├── _Checkbox.css │ │ ├── _RadioSelector.css │ │ ├── _SimpleNotification.css │ │ ├── _Modal.css │ │ ├── _PreviewBox.css │ │ ├── _Output.css │ │ ├── _SimpleSpinner.css │ │ ├── _Frame.css │ │ ├── _UsefulData.css │ │ ├── _Picker.css │ │ ├── _LoadDrawing.css │ │ └── _App.css │ ├── fonts │ │ ├── files │ │ │ ├── webfont-icons.eot │ │ │ ├── webfont-icons.ttf │ │ │ ├── webfont-icons.woff │ │ │ ├── minecraftia-regular-webfont.eot │ │ │ ├── minecraftia-regular-webfont.ttf │ │ │ ├── minecraftia-regular-webfont.woff │ │ │ └── minecraftia-regular-webfont.woff2 │ │ └── _fonts.css │ ├── layout │ │ ├── _flex.css │ │ ├── _grid.css │ │ ├── _header.css │ │ └── _queries.css │ ├── input │ │ ├── _inputText.css │ │ └── _button.css │ ├── _variables.css │ ├── _base.css │ ├── _utils.css │ ├── imports.css │ └── views │ │ ├── _cookies.css │ │ └── _notFound.css ├── store │ ├── reducers │ │ ├── drawingToolStates.js │ │ ├── drawingToolReducer.js │ │ ├── reducer.js │ │ ├── paletteReducer.js │ │ ├── framesReducer.js │ │ └── activeFrameReducer.js │ ├── configureStore.js │ └── actions │ │ ├── actionTypes.js │ │ └── actionCreators.js ├── components │ ├── NotFound.jsx │ ├── SimpleSpinner.jsx │ ├── Checkbox.jsx │ ├── Reset.jsx │ ├── CellsInfo.jsx │ ├── Root.jsx │ ├── PixelGrid.jsx │ ├── PaletteColor.jsx │ ├── Move.jsx │ ├── Animation.jsx │ ├── DownloadDrawing.jsx │ ├── NewProject.jsx │ ├── Eraser.jsx │ ├── Bucket.jsx │ ├── loadFromFile │ │ ├── ValidationMessage.jsx │ │ ├── ImageDimensions.jsx │ │ ├── ImageSizeDisplay.jsx │ │ └── ImageSetup.jsx │ ├── RadioSelector.jsx │ ├── Eyedropper.jsx │ ├── CssDisplay.jsx │ ├── PixelCell.jsx │ ├── Duration.jsx │ ├── UndoRedo.jsx │ ├── CellSize.jsx │ ├── PaletteGrid.jsx │ ├── Picker.jsx │ ├── Output.jsx │ ├── Dimensions.jsx │ ├── CopyCSS.jsx │ ├── SimpleNotification.jsx │ ├── Cookies.jsx │ ├── GridWrapper.jsx │ ├── SaveDrawing.jsx │ ├── Preview.jsx │ ├── KeyBindings.jsx │ ├── PreviewBox.jsx │ ├── ColorPicker.jsx │ ├── Frame.jsx │ ├── KeyBindingsLegend.jsx │ ├── UsefulData.jsx │ ├── PixelCanvas.jsx │ ├── FramesHandler.jsx │ └── common │ │ └── Button.jsx └── index.jsx ├── .babelrc ├── .prettierignore ├── .eslintignore ├── screenshots ├── animation-cat.gif ├── screenshot-cat.png ├── tree-pixelartcss.png └── screenshot-potion.png ├── .gitignore ├── .lintstagedrc ├── .travis.yml ├── postcss.config.js ├── .stylelintrc ├── .prettierrc ├── LICENSE.md ├── test ├── utils │ ├── intervals.test.js │ ├── color.test.js │ ├── loadFromCanvas.test.js │ └── outputParse.test.js ├── reducer.test.js └── drawingToolReducer.test.js ├── .eslintrc ├── webpack.config.js ├── webpack.production.config.js ├── package.json └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | last 2 versions 2 | IE > 8 -------------------------------------------------------------------------------- /.huskyrc: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": "lint-staged" 4 | } 5 | } -------------------------------------------------------------------------------- /src/utils/polyfills.js: -------------------------------------------------------------------------------- 1 | import 'canvas-toBlob'; 2 | import 'whatwg-fetch'; 3 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HowittColet/pixel-art-react/HEAD/src/assets/favicon.ico -------------------------------------------------------------------------------- /src/css/components/_Dimensions.css: -------------------------------------------------------------------------------- 1 | .dimensions { 2 | margin: 1em 0 0; 3 | padding: 0.5em 0; 4 | } 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | dist 4 | deploy 5 | *config.js 6 | images 7 | screenshots 8 | examples -------------------------------------------------------------------------------- /src/assets/bmac-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HowittColet/pixel-art-react/HEAD/src/assets/bmac-icon.png -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | dist 4 | deploy 5 | *config.js 6 | images 7 | screenshots 8 | examples 9 | -------------------------------------------------------------------------------- /src/assets/coindrop-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HowittColet/pixel-art-react/HEAD/src/assets/coindrop-img.png -------------------------------------------------------------------------------- /src/assets/regular-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HowittColet/pixel-art-react/HEAD/src/assets/regular-icon.png -------------------------------------------------------------------------------- /screenshots/animation-cat.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HowittColet/pixel-art-react/HEAD/screenshots/animation-cat.gif -------------------------------------------------------------------------------- /screenshots/screenshot-cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HowittColet/pixel-art-react/HEAD/screenshots/screenshot-cat.png -------------------------------------------------------------------------------- /screenshots/tree-pixelartcss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HowittColet/pixel-art-react/HEAD/screenshots/tree-pixelartcss.png -------------------------------------------------------------------------------- /src/assets/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HowittColet/pixel-art-react/HEAD/src/assets/apple-touch-icon.png -------------------------------------------------------------------------------- /src/css/components/_Move.css: -------------------------------------------------------------------------------- 1 | .move { 2 | @mixin icon move; 3 | 4 | &:hover { 5 | cursor: pointer; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /screenshots/screenshot-potion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HowittColet/pixel-art-react/HEAD/screenshots/screenshot-potion.png -------------------------------------------------------------------------------- /src/css/components/_Bucket.css: -------------------------------------------------------------------------------- 1 | .bucket { 2 | @mixin icon bucket; 3 | 4 | &:hover { 5 | cursor: pointer; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/css/components/_CellInfo.css: -------------------------------------------------------------------------------- 1 | .cellinfo { 2 | color: $color-silver; 3 | text-align: center; 4 | margin-top: 1em; 5 | } 6 | -------------------------------------------------------------------------------- /src/css/components/_Eraser.css: -------------------------------------------------------------------------------- 1 | .eraser { 2 | @mixin icon eraser; 3 | 4 | &:hover { 5 | cursor: pointer; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/css/components/_SaveDrawing.css: -------------------------------------------------------------------------------- 1 | .save-drawing { 2 | button { 3 | @mixin button gray; 4 | 5 | width: 100%; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/css/fonts/files/webfont-icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HowittColet/pixel-art-react/HEAD/src/css/fonts/files/webfont-icons.eot -------------------------------------------------------------------------------- /src/css/fonts/files/webfont-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HowittColet/pixel-art-react/HEAD/src/css/fonts/files/webfont-icons.ttf -------------------------------------------------------------------------------- /src/css/components/_EyeDropper.css: -------------------------------------------------------------------------------- 1 | .eyedropper { 2 | @mixin icon eyedropper; 3 | 4 | &:hover { 5 | cursor: pointer; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/css/components/_PaletteGrid.css: -------------------------------------------------------------------------------- 1 | .palette-grid { 2 | lost-utility: clearfix; 3 | text-align: center; 4 | margin: 0.5em 0; 5 | } 6 | -------------------------------------------------------------------------------- /src/css/fonts/files/webfont-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HowittColet/pixel-art-react/HEAD/src/css/fonts/files/webfont-icons.woff -------------------------------------------------------------------------------- /src/css/components/_DownloadDrawing.css: -------------------------------------------------------------------------------- 1 | .download-btn { 2 | @mixin button brown; 3 | 4 | margin: 1.5em auto; 5 | display: table; 6 | } 7 | -------------------------------------------------------------------------------- /src/css/components/_Reset.css: -------------------------------------------------------------------------------- 1 | .reset { 2 | width: 100%; 3 | margin: 0.5em auto; 4 | display: table; 5 | 6 | @mixin button gray; 7 | } 8 | -------------------------------------------------------------------------------- /src/css/layout/_flex.css: -------------------------------------------------------------------------------- 1 | @define-mixin flex-row-center { 2 | display: flex; 3 | flex-flow: row nowrap; 4 | justify-content: center; 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist/bundle.js 3 | npm-debug.log 4 | deploy 5 | config.json 6 | routes 7 | .env 8 | npm-debug.log 9 | .directory 10 | -------------------------------------------------------------------------------- /src/css/fonts/files/minecraftia-regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HowittColet/pixel-art-react/HEAD/src/css/fonts/files/minecraftia-regular-webfont.eot -------------------------------------------------------------------------------- /src/css/fonts/files/minecraftia-regular-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HowittColet/pixel-art-react/HEAD/src/css/fonts/files/minecraftia-regular-webfont.ttf -------------------------------------------------------------------------------- /src/css/fonts/files/minecraftia-regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HowittColet/pixel-art-react/HEAD/src/css/fonts/files/minecraftia-regular-webfont.woff -------------------------------------------------------------------------------- /src/css/fonts/files/minecraftia-regular-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HowittColet/pixel-art-react/HEAD/src/css/fonts/files/minecraftia-regular-webfont.woff2 -------------------------------------------------------------------------------- /src/utils/random.js: -------------------------------------------------------------------------------- 1 | export default function randomString() { 2 | return Math.random() 3 | .toString(36) 4 | .replace(/[^a-z]+/g, '') 5 | .substr(0, 8); 6 | } 7 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "**/*.{js,jsx}": "eslint", 3 | "src/**/*.css": "stylelint", 4 | "**/*.{js,jsx,json,yml,yaml,css,md}": [ 5 | "prettier --write", 6 | "git add" 7 | ] 8 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '16' 4 | before_install: 5 | - npm i -g npm@7.24.1 6 | script: 7 | - npm run lint 8 | - npm run csslint 9 | - npm test 10 | -------------------------------------------------------------------------------- /src/css/components/_NewProject.css: -------------------------------------------------------------------------------- 1 | .new-project { 2 | button { 3 | @mixin button gray; 4 | 5 | width: 100%; 6 | padding: 0.5em; 7 | margin-bottom: 0.6em; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('autoprefixer'), 4 | require('postcss-import'), 5 | require('precss'), 6 | require('lost'), 7 | require('postcss-reporter'), 8 | ] 9 | } -------------------------------------------------------------------------------- /src/css/components/_PaletteColor.css: -------------------------------------------------------------------------------- 1 | .palette-color { 2 | float: left; 3 | border: 2px solid $color-scorpion; 4 | border-width: 2px; 5 | 6 | &.selected { 7 | border-color: white; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/css/components/_ColorPicker.css: -------------------------------------------------------------------------------- 1 | .color-picker { 2 | .color-picker__button { 3 | @mixin icon paint-brush; 4 | 5 | display: block; 6 | } 7 | 8 | &:hover { 9 | cursor: pointer; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/store/reducers/drawingToolStates.js: -------------------------------------------------------------------------------- 1 | export const PENCIL = 'PENCIL'; 2 | export const ERASER = 'ERASER'; 3 | export const BUCKET = 'BUCKET'; 4 | export const MOVE = 'MOVE'; 5 | export const EYEDROPPER = 'EYEDROPPER'; 6 | export const COLOR_PICKER = 'COLOR_PICKER'; 7 | -------------------------------------------------------------------------------- /src/css/components/_CssDisplay.css: -------------------------------------------------------------------------------- 1 | .css-display { 2 | position: absolute; 3 | top: -1.6em; 4 | left: 0; 5 | opacity: 0.1; 6 | z-index: -1; 7 | padding: 1em; 8 | margin-top: 1em; 9 | color: black; 10 | user-select: none; 11 | font-size: 0.8em; 12 | } 13 | -------------------------------------------------------------------------------- /src/css/components/_CellSize.css: -------------------------------------------------------------------------------- 1 | .cell-size { 2 | border: 3px solid $color-boulder; 3 | background-color: $color-mineShaft; 4 | color: $color-silver; 5 | text-align: center; 6 | 7 | label, 8 | input { 9 | padding-top: 0.2em; 10 | } 11 | 12 | label { 13 | display: block; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/intervals.js: -------------------------------------------------------------------------------- 1 | export default function getTimeInterval( 2 | currentFrameIndex = 0, 3 | totalFrames = 1 4 | ) { 5 | const equalPercentage = 100 / (totalFrames || 1); 6 | return totalFrames === 1 || totalFrames === 0 7 | ? 100 8 | : Math.round((currentFrameIndex + 1) * equalPercentage * 10) / 10; 9 | } 10 | -------------------------------------------------------------------------------- /src/css/components/_Duration.css: -------------------------------------------------------------------------------- 1 | .duration { 2 | margin-top: 1em; 3 | border: 3px solid $color-boulder; 4 | background-color: $color-mineShaft; 5 | color: $color-silver; 6 | text-align: center; 7 | 8 | label, 9 | input { 10 | padding-top: 0.2em; 11 | } 12 | 13 | label { 14 | display: block; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/breakpoints.js: -------------------------------------------------------------------------------- 1 | const size = { 2 | xs: '360px', 3 | sm: '460px', 4 | md: '600px', 5 | lg: '1000px' 6 | }; 7 | const device = { 8 | xs: `min-width: ${size.xs}`, 9 | sm: `min-width: ${size.sm}`, 10 | md: `min-width: ${size.md}`, 11 | lg: `min-width: ${size.lg}` 12 | }; 13 | 14 | export default { size, device }; 15 | -------------------------------------------------------------------------------- /src/components/NotFound.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const NotFound = () => ( 4 |
5 |

Oops! No pixels to draw here

6 |
7 |
8 |
9 | Go back to the editor 10 |
11 | ); 12 | 13 | export default NotFound; 14 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-recommended", 4 | "stylelint-config-lost" 5 | ], 6 | "rules": { 7 | "at-rule-no-unknown": [ 8 | true, 9 | { 10 | "ignoreAtRules": ["mixin", "if", "extend", "/^define[a-z]*/"] 11 | } 12 | ], 13 | "no-extra-semicolons": null, 14 | "font-family-no-missing-generic-family-keyword": null 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/css/components/_UndoRedo.css: -------------------------------------------------------------------------------- 1 | .undo-redo { 2 | lost-utility: clearfix; 3 | 4 | button { 5 | @mixin button gray; 6 | 7 | lost-column: 1/2 0 0.5em; 8 | font-size: 1.2em; 9 | } 10 | 11 | .undo-redo__icon--undo { 12 | @mixin icon undo; 13 | 14 | display: block; 15 | } 16 | 17 | .undo-redo__icon--redo { 18 | @mixin icon redo; 19 | 20 | display: block; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "htmlWhitespaceSensitivity": "css", 5 | "insertPragma": false, 6 | "jsxBracketSameLine": false, 7 | "jsxSingleQuote": false, 8 | "printWidth": 80, 9 | "proseWrap": "preserve", 10 | "requirePragma": false, 11 | "semi": true, 12 | "singleQuote": true, 13 | "tabWidth": 2, 14 | "trailingComma": "none", 15 | "useTabs": false 16 | } -------------------------------------------------------------------------------- /src/css/components/_CopyCss.css: -------------------------------------------------------------------------------- 1 | .copy-css { 2 | h2 { 3 | padding: 2em 0 1em; 4 | margin-bottom: 0; 5 | font-size: 1em; 6 | display: block; 7 | text-align: center; 8 | 9 | span { 10 | color: $color-tamarillo; 11 | } 12 | } 13 | 14 | .copy-css__string { 15 | overflow-x: scroll; 16 | background-color: $color-mineShaft; 17 | color: $color-silver; 18 | padding: 0.5em; 19 | text-align: left; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/components/SimpleSpinner.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | const SimpleSpinner = ({ loading }) => ( 5 |
6 |
7 |
8 | ); 9 | 10 | const mapStateToProps = state => ({ 11 | loading: state.present.get('loading') 12 | }); 13 | 14 | const SimpleSpinnerContainer = connect(mapStateToProps)(SimpleSpinner); 15 | export default SimpleSpinnerContainer; 16 | -------------------------------------------------------------------------------- /src/utils/throttle.js: -------------------------------------------------------------------------------- 1 | const throttle = (fn, limit) => { 2 | let id; 3 | let now; 4 | let limitTime; 5 | const execFn = () => { 6 | limitTime = now + limit; 7 | fn(); 8 | }; 9 | return () => { 10 | now = Date.now(); 11 | if (!limitTime || limitTime <= now) { 12 | execFn(); 13 | } else { 14 | if (id) { 15 | clearTimeout(id); 16 | } 17 | id = setTimeout(execFn, limitTime - now); 18 | } 19 | }; 20 | }; 21 | 22 | export default throttle; 23 | -------------------------------------------------------------------------------- /src/css/components/_FramesHandler.css: -------------------------------------------------------------------------------- 1 | .frames-handler { 2 | lost-utility: clearfix; 3 | 4 | .frames-handler__add { 5 | @mixin button gray; 6 | 7 | height: 86px; 8 | float: left; 9 | } 10 | 11 | .frame-handler__list { 12 | display: flex; 13 | background-color: $color-back-frames; 14 | padding: 0.2em; 15 | 16 | .list__container { 17 | height: 85px; 18 | display: flex; 19 | flex-wrap: nowrap; 20 | padding-bottom: 0.5em; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import ReactGA from 'react-ga'; 4 | import './css/imports.css'; 5 | import configureStore from './store/configureStore'; 6 | import Root from './components/Root'; 7 | 8 | const devMode = process.env.NODE_ENV === 'development'; 9 | const store = configureStore(devMode); 10 | 11 | if (process.env.GOOGLE_ANALYTICS_ID) { 12 | ReactGA.initialize(process.env.GOOGLE_ANALYTICS_ID); 13 | } 14 | ReactDOM.render(, document.getElementById('app')); 15 | -------------------------------------------------------------------------------- /src/components/Checkbox.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Checkbox = ({ name, labelFor, checked, description, onChange }) => ( 4 |
5 | 15 |
16 | ); 17 | 18 | export default Checkbox; 19 | -------------------------------------------------------------------------------- /src/components/Reset.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { resetGrid } from '../store/actions/actionCreators'; 4 | 5 | const Reset = ({ resetGridDispatch }) => ( 6 | 9 | ); 10 | 11 | const mapDispatchToProps = dispatch => ({ 12 | resetGridDispatch: () => dispatch(resetGrid()) 13 | }); 14 | 15 | const ResetContainer = connect( 16 | null, 17 | mapDispatchToProps 18 | )(Reset); 19 | export default ResetContainer; 20 | -------------------------------------------------------------------------------- /src/css/components/_PixelGrid.css: -------------------------------------------------------------------------------- 1 | .grid-container { 2 | lost-utility: clearfix; 3 | line-height: 0; 4 | min-height: 1px; 5 | margin: 0 auto; 6 | width: 90%; 7 | touch-action: none; 8 | 9 | div { 10 | float: left; 11 | border: 1px solid $color-scorpion; 12 | border-width: 0 1px 1px 0; 13 | } 14 | 15 | &.cell { 16 | cursor: cell; 17 | } 18 | 19 | &.context-menu { 20 | cursor: context-menu; 21 | } 22 | 23 | &.copy { 24 | cursor: copy; 25 | } 26 | 27 | &.all-scroll { 28 | cursor: all-scroll; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/css/components/_Checkbox.css: -------------------------------------------------------------------------------- 1 | .checkbox { 2 | border: none; 3 | text-align: center; 4 | margin: 0 auto; 5 | padding: 1em 0; 6 | 7 | label { 8 | span { 9 | padding: 0.6em 1em; 10 | transition: all 0.3s; 11 | } 12 | 13 | input { 14 | display: none; 15 | 16 | &:checked + span { 17 | color: white; 18 | background-color: black; 19 | } 20 | 21 | &:not(:checked) + span { 22 | &:hover { 23 | box-shadow: 0 0 0 2px $color-scorpion inset; 24 | } 25 | } 26 | } 27 | 28 | margin: 0 0.5em; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/css/components/_RadioSelector.css: -------------------------------------------------------------------------------- 1 | .radio-selector { 2 | border: none; 3 | text-align: center; 4 | margin: 0 auto; 5 | 6 | label { 7 | span { 8 | padding: 0.6em 1em; 9 | border: 1px solid $color-scorpion; 10 | transition: all 0.3s; 11 | } 12 | 13 | input { 14 | display: none; 15 | 16 | &:checked + span { 17 | color: white; 18 | background-color: black; 19 | } 20 | 21 | &:not(:checked) + span { 22 | &:hover { 23 | box-shadow: 0 0 0 2px $color-scorpion inset; 24 | } 25 | } 26 | } 27 | 28 | margin: 0 0.5em; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/CellsInfo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | const CellsInfo = props => { 5 | const { hoveredIndex } = props; 6 | const xPos = hoveredIndex ? hoveredIndex.get('x') : '0'; 7 | const yPos = hoveredIndex ? hoveredIndex.get('y') : '0'; 8 | return ( 9 | <>{hoveredIndex &&
{`${xPos}, ${yPos}`}
} 10 | ); 11 | }; 12 | 13 | const mapStateToProps = state => ({ 14 | hoveredIndex: state.present.getIn(['frames', 'hoveredIndex']) 15 | }); 16 | 17 | const CellsInfoContainer = connect(mapStateToProps)(CellsInfo); 18 | export default CellsInfoContainer; 19 | -------------------------------------------------------------------------------- /src/components/Root.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; 4 | import App from './App'; 5 | import Cookies from './Cookies'; 6 | import NotFound from './NotFound'; 7 | 8 | const Root = ({ store }) => ( 9 | 10 | 11 | 12 | } /> 13 | } /> 14 | } /> 15 | 16 | 17 | 18 | ); 19 | 20 | export default Root; 21 | -------------------------------------------------------------------------------- /src/components/PixelGrid.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PixelCell from './PixelCell'; 3 | 4 | const PixelGrid = ({ 5 | cells, 6 | drawHandlers, 7 | classes, 8 | nbrColumns, 9 | hoveredCell 10 | }) => ( 11 |
12 | {cells.map(cell => ( 13 | drawHandlers.onMouseOver(id, ev)} 19 | nbrColumns={nbrColumns} 20 | hoveredCell={hoveredCell} 21 | /> 22 | ))} 23 |
24 | ); 25 | export default PixelGrid; 26 | -------------------------------------------------------------------------------- /src/css/input/_inputText.css: -------------------------------------------------------------------------------- 1 | @define-mixin inputText { 2 | appearance: none; 3 | box-shadow: none; 4 | border-radius: none; 5 | text-align: center; 6 | font-size: 1em; 7 | color: $color-silver; 8 | border: none; 9 | width: 100%; 10 | background-color: $color-tundora; 11 | transition: background-color 0.3s; 12 | 13 | &:focus { 14 | color: $color-mineShaft; 15 | background-color: $color-dustyGray; 16 | outline: none; 17 | } 18 | } 19 | 20 | input[type='text'], 21 | input[type='number'] { 22 | @mixin inputText; 23 | } 24 | 25 | input[type='number'] { 26 | -moz-appearance: textfield; 27 | &::-webkit-outer-spin-button, 28 | &::-webkit-inner-spin-button { 29 | -webkit-appearance: none; 30 | margin: 0; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/components/PaletteColor.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const PaletteColor = props => { 4 | const { 5 | positionInPalette, 6 | width, 7 | color, 8 | selected, 9 | selectPaletteColor 10 | } = props; 11 | 12 | const handleClick = () => selectPaletteColor(positionInPalette); 13 | 14 | const cellColor = color; 15 | const styles = { 16 | width: `${width}%`, 17 | paddingBottom: `${width}%`, 18 | backgroundColor: cellColor 19 | }; 20 | 21 | return ( 22 | 30 | ); 31 | }; 32 | 33 | export default DownloadDrawing; 34 | -------------------------------------------------------------------------------- /src/components/NewProject.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { bindActionCreators } from 'redux'; 4 | import * as actionCreators from '../store/actions/actionCreators'; 5 | 6 | const NewProject = props => { 7 | const newProject = () => { 8 | props.actions.newProject(); 9 | }; 10 | 11 | return ( 12 |
13 | 21 |
22 | ); 23 | }; 24 | 25 | const mapDispatchToProps = dispatch => ({ 26 | actions: bindActionCreators(actionCreators, dispatch) 27 | }); 28 | 29 | const NewProjectContainer = connect( 30 | null, 31 | mapDispatchToProps 32 | )(NewProject); 33 | export default NewProjectContainer; 34 | -------------------------------------------------------------------------------- /src/components/Eraser.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { switchTool } from '../store/actions/actionCreators'; 4 | import { ERASER } from '../store/reducers/drawingToolStates'; 5 | 6 | const Eraser = ({ eraserOn, switchEraser }) => ( 7 | 25 | 33 |
34 | ); 35 | }; 36 | 37 | const mapDispatchToProps = dispatch => ({ 38 | actions: bindActionCreators(actionCreators, dispatch) 39 | }); 40 | 41 | const UndoRedoContainer = connect( 42 | null, 43 | mapDispatchToProps 44 | )(UndoRedo); 45 | export default UndoRedoContainer; 46 | -------------------------------------------------------------------------------- /src/utils/startup.js: -------------------------------------------------------------------------------- 1 | import * as actionCreators from '../store/actions/actionCreators'; 2 | import { initStorage, getDataFromStorage } from './storage'; 3 | 4 | /* 5 | Initial actions to dispatch: 6 | 1. Hide spinner 7 | 2. Load a project if there is a current one 8 | */ 9 | const initialSetup = (dispatch, storage) => { 10 | dispatch(actionCreators.hideSpinner()); 11 | 12 | const dataStored = getDataFromStorage(storage); 13 | if (dataStored) { 14 | // Load current project from the storage 15 | const currentProjectIndex = dataStored.current; 16 | if (currentProjectIndex >= 0) { 17 | const { 18 | frames, 19 | paletteGridData, 20 | columns, 21 | rows, 22 | cellSize 23 | } = dataStored.stored[currentProjectIndex]; 24 | 25 | dispatch( 26 | actionCreators.setDrawing( 27 | frames, 28 | paletteGridData, 29 | cellSize, 30 | columns, 31 | rows 32 | ) 33 | ); 34 | } 35 | } else { 36 | // If no data initialize storage 37 | initStorage(storage); 38 | } 39 | }; 40 | 41 | export default initialSetup; 42 | -------------------------------------------------------------------------------- /src/utils/cssParse.js: -------------------------------------------------------------------------------- 1 | import { 2 | getImageData, 3 | getImageCssClassOutput, 4 | getAnimationKeyframes, 5 | getAnimationCssClassOutput 6 | } from 'box-shadow-pixels'; 7 | 8 | const PIXELART_CSS_CLASS_NAME = 'pixelart-to-css'; 9 | 10 | export function generatePixelDrawCss(frame, columns, cellSize, type) { 11 | return getImageData(frame.get('grid'), { 12 | format: type, 13 | pSize: cellSize, 14 | c: columns 15 | }); 16 | } 17 | 18 | export function getCssImageClassOutput(frame, columns, cellSize) { 19 | return getImageCssClassOutput(frame.get('grid'), { 20 | format: 'string', 21 | pSize: cellSize, 22 | c: columns, 23 | cssClassName: PIXELART_CSS_CLASS_NAME 24 | }); 25 | } 26 | 27 | export function exportAnimationData(frames, columns, cellSize, duration) { 28 | return getAnimationCssClassOutput(frames, { 29 | pSize: cellSize, 30 | c: columns, 31 | duration, 32 | cssClassName: PIXELART_CSS_CLASS_NAME 33 | }); 34 | } 35 | 36 | export function generateAnimationCSSData(frames, columns, cellSize) { 37 | return getAnimationKeyframes(frames, { 38 | pSize: cellSize, 39 | c: columns 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Javier Valencia Romero 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /src/components/CellSize.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { bindActionCreators } from 'redux'; 4 | import * as actionCreators from '../store/actions/actionCreators'; 5 | 6 | const CellSize = props => { 7 | const handleCellSizeChange = event => { 8 | props.actions.setCellSize(+event.target.value || 0); 9 | }; 10 | 11 | const { cellSize } = props; 12 | 13 | return ( 14 |
15 | 26 |
27 | ); 28 | }; 29 | 30 | const mapStateToProps = state => ({ 31 | cellSize: state.present.get('cellSize') 32 | }); 33 | 34 | const mapDispatchToProps = dispatch => ({ 35 | actions: bindActionCreators(actionCreators, dispatch) 36 | }); 37 | 38 | const CellSizeContainer = connect( 39 | mapStateToProps, 40 | mapDispatchToProps 41 | )(CellSize); 42 | export default CellSizeContainer; 43 | -------------------------------------------------------------------------------- /src/components/loadFromFile/ImageSizeDisplay.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled, { css } from 'styled-components'; 3 | 4 | const ImageDimensionDisplayContainer = styled.div` 5 | padding: 0.5rem 0; 6 | text-align: center; 7 | font-size: 1.2rem; 8 | `; 9 | 10 | const ImageDimensionDisplay = styled.span` 11 | ${props => 12 | props.error && 13 | css` 14 | color: red; 15 | `} 16 | `; 17 | 18 | const WarningSign = styled.span` 19 | margin-right: 0.5rem; 20 | width: 2rem; 21 | height: 2rem; 22 | padding: 3px 14px; 23 | display: inline-block; 24 | background-color: red; 25 | color: white; 26 | `; 27 | 28 | const ImageSizeDisplay = ({ description, width, height }) => ( 29 | 30 | {(width.error || height.error) && !} 31 | {description} 32 |   33 | 34 | {width.value} 35 | 36 |  x  37 | 38 | {height.value} 39 | 40 | 41 | ); 42 | 43 | export default ImageSizeDisplay; 44 | -------------------------------------------------------------------------------- /src/css/_base.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | background-color: $color-scorpion; 4 | } 5 | 6 | *, 7 | *::before, 8 | *::after { 9 | box-sizing: inherit; 10 | } 11 | 12 | html, 13 | body { 14 | position: relative; 15 | font-family: $font-pixel; 16 | } 17 | 18 | body { 19 | overflow: auto; 20 | } 21 | 22 | .app-container { 23 | height: 100%; 24 | width: 90%; 25 | margin: 0 auto; 26 | padding: 0; 27 | } 28 | 29 | h1 { 30 | font-size: 2em; 31 | } 32 | 33 | h2 { 34 | font-size: 0.8em; 35 | padding-right: 1em; 36 | display: inline; 37 | position: relative; 38 | top: -0.9em; 39 | } 40 | 41 | h3 { 42 | font-size: 1em; 43 | } 44 | 45 | .block { 46 | display: block; 47 | } 48 | 49 | .hidden { 50 | display: none; 51 | } 52 | 53 | .text-center { 54 | text-align: center; 55 | } 56 | 57 | .text-2xl { 58 | font-size: 1.5rem; 59 | line-height: 2rem; 60 | } 61 | 62 | .mx-auto { 63 | margin-left: auto; 64 | margin-right: auto; 65 | } 66 | 67 | .flex { 68 | display: flex; 69 | } 70 | 71 | .flex-wrap { 72 | flex-wrap: wrap; 73 | } 74 | 75 | .flex-col { 76 | flex-direction: column; 77 | } 78 | 79 | .flex-row { 80 | flex-direction: row; 81 | } 82 | -------------------------------------------------------------------------------- /test/utils/intervals.test.js: -------------------------------------------------------------------------------- 1 | import getTimeInterval from '../../src/utils/intervals'; 2 | 3 | describe('intervals tests', () => { 4 | describe('getTimeInterval', () => { 5 | describe('When the total of frames value is 0 or 1', () => { 6 | it('should return always 100', () => { 7 | expect(getTimeInterval(0, 0)).toEqual(100); 8 | expect(getTimeInterval(1, 0)).toEqual(100); 9 | expect(getTimeInterval(0, 1)).toEqual(100); 10 | expect(getTimeInterval(1, 1)).toEqual(100); 11 | }); 12 | }); 13 | describe('When the total of frames value is greater than 1', () => { 14 | it('should return the proper interval', () => { 15 | expect(getTimeInterval(0, 2)).toEqual(50); 16 | expect(getTimeInterval(1, 2)).toEqual(100); 17 | expect(getTimeInterval(0, 3)).toEqual(33.3); 18 | expect(getTimeInterval(1, 3)).toEqual(66.7); 19 | expect(getTimeInterval(2, 3)).toEqual(100); 20 | expect(getTimeInterval(0, 4)).toEqual(25); 21 | expect(getTimeInterval(1, 4)).toEqual(50); 22 | expect(getTimeInterval(2, 4)).toEqual(75); 23 | expect(getTimeInterval(3, 4)).toEqual(100); 24 | }); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/components/PaletteGrid.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { bindActionCreators } from 'redux'; 4 | import { selectPaletteColor } from '../store/actions/actionCreators'; 5 | import PaletteColor from './PaletteColor'; 6 | 7 | const PaletteGrid = props => { 8 | const getColors = () => { 9 | const { grid, position } = props; 10 | const width = 100 / 6; 11 | 12 | return grid.map((color, index) => ( 13 | 21 | )); 22 | }; 23 | 24 | return
{getColors()}
; 25 | }; 26 | 27 | const mapStateToProps = state => state.present.get('palette').toObject(); 28 | 29 | const mapDispatchToProps = dispatch => 30 | bindActionCreators( 31 | { 32 | selectPaletteColor 33 | }, 34 | dispatch 35 | ); 36 | 37 | const PaletteGridContainer = connect( 38 | mapStateToProps, 39 | mapDispatchToProps 40 | )(PaletteGrid); 41 | export default PaletteGridContainer; 42 | -------------------------------------------------------------------------------- /src/css/layout/_header.css: -------------------------------------------------------------------------------- 1 | header { 2 | color: $color-silver; 3 | lost-utility: clearfix; 4 | margin: 1em 0; 5 | a { 6 | text-decoration: none; 7 | color: inherit; 8 | &:visited, 9 | &:hover, 10 | &:active { 11 | color: inherit; 12 | } 13 | } 14 | h1 { 15 | margin: 0; 16 | } 17 | 18 | .header__social { 19 | text-align: right; 20 | 21 | .header__credits { 22 | font-size: 12px; 23 | margin-bottom: 0.2em; 24 | 25 | a { 26 | color: $color-silver; 27 | } 28 | 29 | span { 30 | color: $color-brandyRose; 31 | } 32 | 33 | img { 34 | margin: 0 0 0 0.3em; 35 | transition: transform 0.2s ease-in-out; 36 | } 37 | 38 | h2 { 39 | display: block; 40 | font-size: 0.8em; 41 | padding-right: 1.3em; 42 | position: relative; 43 | margin: 0; 44 | top: 0; 45 | } 46 | 47 | &:hover { 48 | img { 49 | transform: scale(1.2); 50 | } 51 | } 52 | } 53 | } 54 | } 55 | 56 | @media only screen and (max-width: 460px) { 57 | header { 58 | .col-2-3, 59 | .col-1-3 { 60 | lost-column: 1/2; 61 | } 62 | } 63 | h1 { 64 | font-size: 1.5em; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/css/_utils.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Tooltip 3 | */ 4 | 5 | [data-tooltip] { 6 | position: relative; 7 | cursor: pointer; 8 | text-align: center; 9 | } 10 | 11 | [data-tooltip]::before, 12 | [data-tooltip]::after { 13 | position: absolute; 14 | opacity: 0; 15 | transition: opacity 0.2s ease-in-out, visibility 0.2s ease-in-out, 16 | transform 0.2s cubic-bezier(0.71, 1.7, 0.77, 1.24); 17 | transform: translate3d(0, 0, 0); 18 | pointer-events: none; 19 | bottom: 100%; 20 | left: 50%; 21 | } 22 | 23 | [data-tooltip]::before { 24 | z-index: 1001; 25 | border: 6px solid transparent; 26 | background: transparent; 27 | content: ''; 28 | margin-left: -6px; 29 | margin-bottom: -12px; 30 | border-top-color: black; 31 | border-top-color: #4caf50; 32 | } 33 | 34 | [data-tooltip]::after { 35 | z-index: 1000; 36 | padding: 8px; 37 | width: 160px; 38 | background-color: black; 39 | background-color: #4caf50; 40 | color: white; 41 | content: attr(data-tooltip); 42 | font-size: 14px; 43 | line-height: 1.2; 44 | font-weight: normal; 45 | margin-left: -80px; 46 | } 47 | 48 | [data-tooltip]:hover::before, 49 | [data-tooltip]:hover::after, 50 | [data-tooltip]:focus::before, 51 | [data-tooltip]:focus::after { 52 | visibility: visible; 53 | opacity: 1; 54 | transform: translateY(-12px); 55 | } 56 | -------------------------------------------------------------------------------- /src/components/Picker.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Picker = ({ type, value, action, min = 1, max = 0 }) => { 4 | const pickerType = `picker__${type}`; 5 | return ( 6 |
7 | 38 |
39 | ); 40 | }; 41 | 42 | export default Picker; 43 | -------------------------------------------------------------------------------- /src/components/Output.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react'; 2 | 3 | const Output = ({ 4 | copyClipboardData = {}, 5 | readOnly = true, 6 | outputText, 7 | preFormatted = false 8 | }) => { 9 | const { showButton, textButton, successMessage } = copyClipboardData; 10 | const [copySuccess, setCopySuccess] = useState(''); 11 | const textAreaRef = useRef(null); 12 | const copyToClipboard = e => { 13 | textAreaRef.current.select(); 14 | document.execCommand('copy'); 15 | e.target.focus(); 16 | setCopySuccess(successMessage || 'Copied!'); 17 | }; 18 | return ( 19 |
20 | {showButton && document.queryCommandSupported('copy') && ( 21 |
22 | 29 | {copySuccess} 30 |
31 | )} 32 |