├── .babelrc
├── .browserslistrc
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .huskyrc
├── .lintstagedrc
├── .npmrc
├── .prettierignore
├── .prettierrc
├── .stylelintrc
├── .travis.yml
├── LICENSE.md
├── README.md
├── examples
└── import-export
│ ├── cat.txt
│ └── json-cat.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── _redirects
├── index.dev.html
└── index.html
├── screenshots
├── animation-cat.gif
├── screenshot-cat.png
├── screenshot-potion.png
└── tree-pixelartcss.png
├── src
├── assets
│ ├── apple-touch-icon.png
│ ├── bmac-icon.png
│ ├── coindrop-img.png
│ ├── favicon.ico
│ └── regular-icon.png
├── components
│ ├── Animation.jsx
│ ├── App.jsx
│ ├── Bucket.jsx
│ ├── CellSize.jsx
│ ├── CellsInfo.jsx
│ ├── Checkbox.jsx
│ ├── ColorPicker.jsx
│ ├── Cookies.jsx
│ ├── CopyCSS.jsx
│ ├── CssDisplay.jsx
│ ├── Dimensions.jsx
│ ├── DownloadDrawing.jsx
│ ├── Duration.jsx
│ ├── Eraser.jsx
│ ├── Eyedropper.jsx
│ ├── Frame.jsx
│ ├── FramesHandler.jsx
│ ├── GridWrapper.jsx
│ ├── KeyBindings.jsx
│ ├── KeyBindingsLegend.jsx
│ ├── LoadDrawing.jsx
│ ├── Modal.jsx
│ ├── Move.jsx
│ ├── NewProject.jsx
│ ├── NotFound.jsx
│ ├── Output.jsx
│ ├── PaletteColor.jsx
│ ├── PaletteGrid.jsx
│ ├── Picker.jsx
│ ├── PixelCanvas.jsx
│ ├── PixelCell.jsx
│ ├── PixelGrid.jsx
│ ├── Preview.jsx
│ ├── PreviewBox.jsx
│ ├── RadioSelector.jsx
│ ├── Reset.jsx
│ ├── Root.jsx
│ ├── SaveDrawing.jsx
│ ├── SimpleNotification.jsx
│ ├── SimpleSpinner.jsx
│ ├── UndoRedo.jsx
│ ├── UsefulData.jsx
│ ├── common
│ │ └── Button.jsx
│ └── loadFromFile
│ │ ├── ImageDimensions.jsx
│ │ ├── ImageSetup.jsx
│ │ ├── ImageSizeDisplay.jsx
│ │ ├── ValidationMessage.jsx
│ │ └── index.jsx
├── css
│ ├── _base.css
│ ├── _normalize.css
│ ├── _utils.css
│ ├── _variables.css
│ ├── components
│ │ ├── _App.css
│ │ ├── _Bucket.css
│ │ ├── _CellInfo.css
│ │ ├── _CellSize.css
│ │ ├── _Checkbox.css
│ │ ├── _ColorPicker.css
│ │ ├── _CopyCss.css
│ │ ├── _CssDisplay.css
│ │ ├── _Dimensions.css
│ │ ├── _DownloadDrawing.css
│ │ ├── _Duration.css
│ │ ├── _Eraser.css
│ │ ├── _EyeDropper.css
│ │ ├── _Frame.css
│ │ ├── _FramesHandler.css
│ │ ├── _LoadDrawing.css
│ │ ├── _Modal.css
│ │ ├── _Move.css
│ │ ├── _NewProject.css
│ │ ├── _Output.css
│ │ ├── _PaletteColor.css
│ │ ├── _PaletteGrid.css
│ │ ├── _Picker.css
│ │ ├── _PixelGrid.css
│ │ ├── _PreviewBox.css
│ │ ├── _RadioSelector.css
│ │ ├── _Reset.css
│ │ ├── _SaveDrawing.css
│ │ ├── _SimpleNotification.css
│ │ ├── _SimpleSpinner.css
│ │ ├── _UndoRedo.css
│ │ └── _UsefulData.css
│ ├── fonts
│ │ ├── _fonts.css
│ │ └── files
│ │ │ ├── minecraftia-regular-webfont.eot
│ │ │ ├── minecraftia-regular-webfont.svg
│ │ │ ├── minecraftia-regular-webfont.ttf
│ │ │ ├── minecraftia-regular-webfont.woff
│ │ │ ├── minecraftia-regular-webfont.woff2
│ │ │ ├── webfont-icons.eot
│ │ │ ├── webfont-icons.svg
│ │ │ ├── webfont-icons.ttf
│ │ │ └── webfont-icons.woff
│ ├── imports.css
│ ├── input
│ │ ├── _button.css
│ │ └── _inputText.css
│ ├── layout
│ │ ├── _flex.css
│ │ ├── _grid.css
│ │ ├── _header.css
│ │ └── _queries.css
│ └── views
│ │ ├── _cookies.css
│ │ └── _notFound.css
├── index.jsx
├── store
│ ├── actions
│ │ ├── actionCreators.js
│ │ └── actionTypes.js
│ ├── configureStore.js
│ └── reducers
│ │ ├── activeFrameReducer.js
│ │ ├── drawingToolReducer.js
│ │ ├── drawingToolStates.js
│ │ ├── framesReducer.js
│ │ ├── paletteReducer.js
│ │ └── reducer.js
└── utils
│ ├── ImageToCanvas.js
│ ├── breakpoints.js
│ ├── canvasGIF.js
│ ├── color.js
│ ├── cssParse.js
│ ├── drawHandlersProvider.js
│ ├── intervals.js
│ ├── loadFromCanvas.js
│ ├── outputParse.js
│ ├── polyfills.js
│ ├── random.js
│ ├── startup.js
│ ├── storage.js
│ └── throttle.js
├── test
├── actions.test.js
├── activeFrameReducer.test.js
├── drawingToolReducer.test.js
├── framesReducer.test.js
├── paletteReducer.test.js
├── reducer.test.js
└── utils
│ ├── color.test.js
│ ├── intervals.test.js
│ ├── loadFromCanvas.test.js
│ └── outputParse.test.js
├── webpack.config.js
└── webpack.production.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env",
4 | "@babel/preset-react"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/.browserslistrc:
--------------------------------------------------------------------------------
1 | last 2 versions
2 | IE > 8
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
3 | dist
4 | deploy
5 | *config.js
6 | images
7 | screenshots
8 | examples
9 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["airbnb", "eslint-config-prettier"],
3 | "env": {
4 | "browser": true,
5 | "node": true,
6 | "jest": true
7 | },
8 | "parser": "babel-eslint",
9 | "parserOptions": {
10 | "ecmaFeatures": {
11 | "forOf": true,
12 | "jsx": true,
13 | "es6": true
14 | }
15 | },
16 | "plugins": ["react-hooks"],
17 | "rules": {
18 | "comma-dangle": 0,
19 | "indent": [2, 2, { "SwitchCase": 1 }],
20 | "react/prop-types": 0,
21 | "func-names": 0,
22 | "arrow-body-style": [2, "as-needed"],
23 | "no-underscore-dangle": ["error", { "allow": ["__INITIAL_STATE__"] }],
24 | "new-cap": ["error", { "capIsNewExceptions": ["Map", "List"] }],
25 | "react/prefer-es6-class": 1,
26 | "no-restricted-syntax": 0,
27 | "no-plusplus": ["error", { "allowForLoopAfterthoughts": true }],
28 | "import/no-extraneous-dependencies": ["error", { "devDependencies": true }],
29 | "quote-props": ["error", "consistent"],
30 | "no-console": 0,
31 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
32 | "react/jsx-props-no-spreading": [0, { "custom": "ignore" }],
33 | "jsx-a11y/no-static-element-interactions": 0,
34 | "jsx-a11y/label-has-for": [
35 | 2,
36 | { "required": { "some": ["nesting", "id"] } }
37 | ],
38 | "react-hooks/rules-of-hooks": "error",
39 | "react-hooks/exhaustive-deps": "warn"
40 | },
41 | "globals": {
42 | "$": true
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.huskyrc:
--------------------------------------------------------------------------------
1 | {
2 | "hooks": {
3 | "pre-commit": "lint-staged"
4 | }
5 | }
--------------------------------------------------------------------------------
/.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 | }
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | engine-strict=true
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
3 | dist
4 | deploy
5 | *config.js
6 | images
7 | screenshots
8 | examples
--------------------------------------------------------------------------------
/.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 | }
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
onMouseDown(id, ev)}
28 | onMouseOver={ev => onMouseOver(id, ev)}
29 | onFocus={ev => onMouseOver(id, ev)}
30 | onTouchStart={ev => onMouseDown(id, ev)}
31 | style={styles}
32 | />
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/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/components/Preview.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | generatePixelDrawCss,
4 | generateAnimationCSSData
5 | } from '../utils/cssParse';
6 | import Animation from './Animation';
7 |
8 | const Preview = props => {
9 | const generatePreview = () => {
10 | const { activeFrameIndex, duration, storedData, animationName } = props;
11 | const { frames, columns, cellSize, animate } = storedData || props;
12 | const animation = frames.size > 1 && animate;
13 | let animationData;
14 | let cssString;
15 |
16 | const styles = {
17 | previewWrapper: {
18 | height: cellSize,
19 | width: cellSize,
20 | position: 'absolute',
21 | top: '-5px',
22 | left: '-5px'
23 | }
24 | };
25 |
26 | if (animation) {
27 | animationData = generateAnimationCSSData(frames, columns, cellSize);
28 | } else {
29 | cssString = generatePixelDrawCss(
30 | frames.get(activeFrameIndex),
31 | columns,
32 | cellSize,
33 | 'string'
34 | );
35 |
36 | styles.previewWrapper.boxShadow = cssString;
37 | styles.previewWrapper.MozBoxShadow = cssString;
38 | styles.previewWrapper.WebkitBoxShadow = cssString;
39 | }
40 |
41 | return (
42 |
43 | {animation ? (
44 |
49 | ) : null}
50 |
51 | );
52 | };
53 |
54 | const { storedData } = props;
55 | const { columns, rows, cellSize } = storedData || props;
56 | const style = {
57 | width: columns * cellSize,
58 | height: rows * cellSize,
59 | position: 'relative'
60 | };
61 |
62 | return (
63 |
64 | {generatePreview()}
65 |
66 | );
67 | };
68 | export default Preview;
69 |
--------------------------------------------------------------------------------
/src/components/PreviewBox.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { useSelector } from 'react-redux';
3 | import Preview from './Preview';
4 |
5 | const PreviewBox = props => {
6 | const [animate, setAnimate] = useState(false);
7 | const [isNormalSize, setNormalSize] = useState(true);
8 | const frames = useSelector(state => state.present.get('frames'));
9 | const duration = useSelector(state => state.present.get('duration'));
10 | const frameList = frames.get('list');
11 | const activeFrameIndex = frames.get('activeIndex');
12 | const columns = frames.get('columns');
13 | const rows = frames.get('rows');
14 | const { helpOn, callback } = props;
15 | const animMessage = `${animate ? 'Pause' : 'Play'} the animation`;
16 | const zoomMessage = `Zoom ${isNormalSize ? '0.5' : '1.5'}`;
17 | const animTooltip = helpOn ? animMessage : null;
18 | const zoomTooltip = helpOn ? zoomMessage : null;
19 | const smPixelSize = 3;
20 | const bgPixelSize = 6;
21 |
22 | return (
23 |
24 |
25 |
26 | setAnimate(!animate)}
30 | aria-label="Animation control"
31 | />
32 |
33 |
34 | {
39 | setNormalSize(!isNormalSize);
40 | }}
41 | />
42 |
43 |
44 |
50 |
51 |
52 |
64 |
65 | );
66 | };
67 |
68 | export default PreviewBox;
69 |
--------------------------------------------------------------------------------
/src/components/RadioSelector.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const RadioSelector = ({ name, selected, legend, options, change }) => {
4 | const availableOptions = ops =>
5 | ops.map(item => (
6 |
7 | {
13 | change(item.value, name);
14 | }}
15 | checked={selected === item.value}
16 | />
17 | {item.description}
18 |
19 | ));
20 |
21 | return (
22 |
23 | {legend ? {legend} : null}
24 | {availableOptions(options)}
25 |
26 | );
27 | };
28 |
29 | export default RadioSelector;
30 |
--------------------------------------------------------------------------------
/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 |
7 | RESET
8 |
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/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/SaveDrawing.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { bindActionCreators } from 'redux';
4 | import shortid from 'shortid';
5 | import * as actionCreators from '../store/actions/actionCreators';
6 | import { saveProjectToStorage } from '../utils/storage';
7 |
8 | const SaveDrawing = props => {
9 | const save = () => {
10 | const drawingToSave = {
11 | frames: props.frames,
12 | paletteGridData: props.paletteGridData,
13 | cellSize: props.cellSize,
14 | columns: props.columns,
15 | rows: props.rows,
16 | animate: props.frames.size > 1,
17 | id: shortid.generate()
18 | };
19 |
20 | if (saveProjectToStorage(localStorage, drawingToSave)) {
21 | props.actions.sendNotification('Drawing saved');
22 | }
23 | };
24 |
25 | return (
26 |
27 | {
30 | save();
31 | }}
32 | >
33 | SAVE
34 |
35 |
36 | );
37 | };
38 |
39 | const mapStateToProps = state => {
40 | const frames = state.present.get('frames');
41 | return {
42 | frames: frames.get('list'),
43 | columns: frames.get('columns'),
44 | rows: frames.get('rows'),
45 | cellSize: state.present.get('cellSize'),
46 | paletteGridData: state.present.getIn(['palette', 'grid'])
47 | };
48 | };
49 |
50 | const mapDispatchToProps = dispatch => ({
51 | actions: bindActionCreators(actionCreators, dispatch)
52 | });
53 |
54 | const SaveDrawingContainer = connect(
55 | mapStateToProps,
56 | mapDispatchToProps
57 | )(SaveDrawing);
58 | export default SaveDrawingContainer;
59 |
--------------------------------------------------------------------------------
/src/components/SimpleNotification.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { bindActionCreators } from 'redux';
4 | import { TransitionGroup, CSSTransition } from 'react-transition-group';
5 | import * as actionCreators from '../store/actions/actionCreators';
6 |
7 | const SimpleNotification = ({
8 | duration,
9 | fadeInTime,
10 | fadeOutTime,
11 | notifications,
12 | actions
13 | }) => {
14 | const removeNotifications = () => {
15 | setTimeout(() => {
16 | actions.sendNotification('');
17 | }, duration);
18 | };
19 | const timeout = { enter: fadeInTime, exit: fadeOutTime };
20 | const notificationList = notifications.map(item => (
21 |
26 |
27 | {item.message}
28 |
29 |
30 | ));
31 |
32 | if (notificationList.size > 0) {
33 | removeNotifications();
34 | }
35 |
36 | return
{notificationList} ;
37 | };
38 |
39 | const mapStateToProps = state => ({
40 | notifications: state.present.get('notifications')
41 | });
42 |
43 | const mapDispatchToProps = dispatch => ({
44 | actions: bindActionCreators(actionCreators, dispatch)
45 | });
46 |
47 | const SimpleNotificationContainer = connect(
48 | mapStateToProps,
49 | mapDispatchToProps
50 | )(SimpleNotification);
51 | export default SimpleNotificationContainer;
52 |
--------------------------------------------------------------------------------
/src/components/SimpleSpinner.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | const SimpleSpinner = ({ loading }) => (
5 |
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/components/UndoRedo.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 UndoRedo = props => {
7 | const undo = () => {
8 | props.actions.undo();
9 | };
10 |
11 | const redo = () => {
12 | props.actions.redo();
13 | };
14 |
15 | return (
16 |
17 | {
20 | undo();
21 | }}
22 | >
23 |
24 |
25 | {
28 | redo();
29 | }}
30 | >
31 |
32 |
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/components/UsefulData.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import RadioSelector from './RadioSelector';
3 | import Checkbox from './Checkbox';
4 | import Output from './Output';
5 | import generateFramesOutput from '../utils/outputParse';
6 |
7 | const UsefulData = props => {
8 | const [colorFormatId, setColorFormat] = useState(0);
9 | const [checkboxOddState, setCheckboxOdd] = useState(false);
10 | const [checkboxEvenState, setCheckboxEven] = useState(false);
11 | const { frames, columns } = props;
12 | const colorFormatOptions = [
13 | {
14 | value: 0,
15 | description: '#000000',
16 | labelFor: 'c-format-hcss',
17 | id: 0
18 | },
19 | { value: 1, description: '0x000000', labelFor: 'c-format-hx', id: 1 },
20 | {
21 | value: 2,
22 | description: 'rgba(0,0,0,1)',
23 | labelFor: 'c-format-rgba',
24 | id: 2
25 | }
26 | ];
27 | const changeColorFormat = value => {
28 | setColorFormat(value);
29 | };
30 | const generateUsefulDataOutput = (
31 | formatId,
32 | reverseOddRows,
33 | reverseEvenRows
34 | ) =>
35 | generateFramesOutput({
36 | frames,
37 | columns,
38 | options: {
39 | colorFormat: formatId,
40 | reverseOdd: reverseOddRows,
41 | reverseEven: reverseEvenRows
42 | }
43 | });
44 | return (
45 |
46 |
Get additional data from your project
47 |
48 | Here you will find every pixel color values grouped by frame. You can
49 | modify the output with the following options:
50 |
51 |
52 |
53 |
54 |
Pixel color format
55 |
61 |
62 |
63 |
Change the order of the rows
64 | {
70 | setCheckboxOdd(!checkboxOddState);
71 | }}
72 | />
73 | {
79 | setCheckboxEven(!checkboxEvenState);
80 | }}
81 | />
82 |
83 |
84 |
85 |
98 |
99 |
100 |
101 | );
102 | };
103 | export default UsefulData;
104 |
--------------------------------------------------------------------------------
/src/components/common/Button.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled, { ThemeProvider, css } from 'styled-components';
4 | import theme from 'styled-theming';
5 |
6 | const colors = {
7 | silver: '#bbb',
8 | mineShaft: '#313131',
9 | doveGray: '#707070',
10 | tundora: '#4b4949',
11 | lotus: '#803c3c',
12 | buccaneer: '#733939',
13 | cowboy: '#552a2a',
14 | steelblue: '#5786c1',
15 | sanMarino: '#4171ae',
16 | eastBay: '#3a587f',
17 | chathamsBlue: '#164075',
18 | chambray: '#2f5382',
19 | cloudBurst: '#253c5a',
20 | alto: '#e0e0e0',
21 | silveChalice: '#a0a0a0',
22 | nobel: '#b7b7b7',
23 | shrub: '#0e8044'
24 | };
25 |
26 | const textColor = theme.variants('mode', 'variant', {
27 | default: { default: colors.silver },
28 | action: { default: colors.silver },
29 | close: { default: colors.silver },
30 | info: { default: colors.silver },
31 | white: { default: 'black' },
32 | proceed: { default: colors.silver }
33 | });
34 |
35 | const bgColor = theme.variants('mode', 'variant', {
36 | default: { default: colors.mineShaft },
37 | action: { default: colors.lotus },
38 | close: { default: colors.steelblue },
39 | info: { default: colors.chathamsBlue },
40 | white: { default: colors.alto },
41 | proceed: { default: colors.shrub }
42 | });
43 |
44 | const boxShadowColor = theme.variants('mode', 'variant', {
45 | default: { default: colors.doveGray },
46 | action: { default: colors.buccaneer },
47 | close: { default: colors.sanMarino },
48 | info: { default: colors.chambray },
49 | white: { default: colors.silveChalice },
50 | proceed: { default: colors.nobel }
51 | });
52 |
53 | const bgActiveColor = theme.variants('mode', 'variant', {
54 | default: { default: colors.tundora },
55 | action: { default: colors.cowboy },
56 | close: { default: colors.eastBay },
57 | info: { default: colors.cloudBurst },
58 | white: { default: colors.nobel },
59 | proceed: { default: colors.shrub }
60 | });
61 |
62 | const ButtonCSS = css`
63 | background: none;
64 | border: none;
65 | outline: none;
66 | border-radius: 2px;
67 | padding: 10px;
68 | font-size: 1em;
69 | text-decoration: none;
70 | transition-duration: 0.1s;
71 | color: ${textColor};
72 | background-color: ${bgColor};
73 | box-shadow: 0 5px 0 0 ${boxShadowColor};
74 | margin: 0 auto;
75 | &:hover,
76 | &.selected {
77 | background-color: ${bgActiveColor};
78 | }
79 | &:hover {
80 | cursor: pointer;
81 | }
82 | &:active {
83 | transform: translate(0, 5px);
84 | box-shadow: 0 1px 0 ${bgActiveColor};
85 | background-color: ${bgActiveColor};
86 | }
87 |
88 | ${props => {
89 | const widthOptions = { full: '100%', half: '50%', normal: 'auto' };
90 | const size = widthOptions[props.size];
91 | return (
92 | props.size &&
93 | css`
94 | width: ${size};
95 | `
96 | );
97 | }};
98 | `;
99 |
100 | const ButtonStyled = styled.button.attrs(props => ({
101 | disabled: props.disabled
102 | }))`
103 | ${ButtonCSS}
104 | ${props =>
105 | props.size &&
106 | css`
107 | opacity: ${props.disabled ? '0.5' : '1'};
108 | `};
109 | `;
110 |
111 | const InputFileLabelStyled = styled.label.attrs({
112 | htmlFor: 'load-image-input'
113 | })`
114 | ${ButtonCSS}
115 | cursor: pointer;
116 | `;
117 |
118 | const InputFileStyled = styled.input.attrs({
119 | type: 'file',
120 | id: 'load-image-input',
121 | role: 'button'
122 | })`
123 | opacity: 0;
124 | position: absolute;
125 | z-index: -1;
126 | `;
127 |
128 | const Button = ({
129 | children,
130 | variant,
131 | onClick,
132 | onChange,
133 | type,
134 | size,
135 | ariaLabel,
136 | disabled = false
137 | }) => (
138 |
139 | {type === 'file' ? (
140 | <>
141 |
142 | {children}
143 |
144 |
145 | >
146 | ) : (
147 |
154 | {children}
155 |
156 | )}
157 |
158 | );
159 | Button.propTypes = {
160 | variant: PropTypes.oneOf([
161 | 'default',
162 | 'info',
163 | 'close',
164 | 'action',
165 | 'white',
166 | 'proceed'
167 | ]),
168 | size: PropTypes.oneOf(['normal', 'half', 'full']),
169 | ariaLabel: PropTypes.string.isRequired,
170 | onClick(props, ...rest) {
171 | if (!props.type) {
172 | return PropTypes.func.isRequired(props, ...rest);
173 | }
174 | return PropTypes.func(props, ...rest);
175 | }
176 | };
177 |
178 | Button.defaultProps = {
179 | variant: 'default',
180 | size: 'normal',
181 | onClick: () => {}
182 | };
183 |
184 | export default Button;
185 |
--------------------------------------------------------------------------------
/src/components/loadFromFile/ImageDimensions.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import ImageSizeDisplay from './ImageSizeDisplay';
4 |
5 | const ImageSizeSection = styled.div`
6 | background-color: whitesmoke;
7 | padding: 1rem 1rem;
8 | `;
9 |
10 | const ImageDimensions = ({
11 | imageDimensions,
12 | resultDimensions,
13 | validationError
14 | }) => (
15 |
16 |
21 |
32 |
33 | );
34 |
35 | export default ImageDimensions;
36 |
--------------------------------------------------------------------------------
/src/components/loadFromFile/ImageSetup.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import breakpoints from '../../utils/breakpoints';
4 | import Picker from '../Picker';
5 | import { getCanvasDimensions } from '../../utils/loadFromCanvas';
6 |
7 | const LoadSetup = styled.div`
8 | display: flex;
9 | flex-wrap: wrap;
10 | align-items: start;
11 | margin: 0 0 1.5rem;
12 | background-color: whitesmoke;
13 | padding: 1rem 0;
14 | `;
15 |
16 | const PickerWrapper = styled.div`
17 | padding: 1rem;
18 | margin: 0 auto 1rem auto;
19 | width: 100%;
20 | @media only screen and (${breakpoints.device.lg}) {
21 | width: 50%;
22 | }
23 | `;
24 |
25 | const PickerTitle = styled.h2`
26 | display: block;
27 | text-align: center;
28 | margin: 0;
29 | font-size: 1rem;
30 | top: 0;
31 | padding: 0 0 0.5rem 0;
32 | `;
33 |
34 | const PickerInfoIcon = styled.i`
35 | position: relative;
36 | background-color: #2f5382;
37 | color: white;
38 | border-radius: 9999px;
39 | top: -9px;
40 | padding: 0.2rem;
41 | margin-left: 0.4rem;
42 | `;
43 |
44 | const getImageDimensions = (canvasDimensions, pSize, frameAmount) => {
45 | const pixelsWidth = Math.round((canvasDimensions.w / pSize) * 100) / 100;
46 | const pixelsHeight =
47 | Math.round((canvasDimensions.h / pSize / frameAmount) * 100) / 100;
48 | return {
49 | original: { w: canvasDimensions.w, h: canvasDimensions.h },
50 | result: { w: pixelsWidth, h: pixelsHeight }
51 | };
52 | };
53 |
54 | const ImageSetupSection = ({
55 | canvasRef,
56 | frameCount,
57 | setFrameCount,
58 | pixelSize,
59 | setPixelSize,
60 | setResultDimensions,
61 | imgSetupValidation
62 | }) => {
63 | const getPickerAction = (property, setProperty) => (type, behaviour) => {
64 | const newPickerCount = property.value + behaviour;
65 | const pixelValue = property.id === 'frame' ? pixelSize : newPickerCount;
66 | const frameValue = property.id === 'frame' ? newPickerCount : frameCount;
67 | setProperty(newPickerCount);
68 | setResultDimensions(
69 | getImageDimensions(getCanvasDimensions(canvasRef), pixelValue, frameValue)
70 | .result
71 | );
72 | imgSetupValidation(getCanvasDimensions(canvasRef), pixelValue, frameValue);
73 | };
74 |
75 | const framePickerAction = getPickerAction(
76 | { value: frameCount, id: 'frame' },
77 | setFrameCount
78 | );
79 | const pixelSizePickerAction = getPickerAction(
80 | { value: pixelSize, id: 'pixel' },
81 | setPixelSize
82 | );
83 |
84 | return (
85 |
86 |
87 |
88 | Number of Frames
89 |
90 |
91 |
92 |
93 |
98 |
99 |
100 |
101 | Pixel Size
102 |
103 |
104 |
105 |
106 |
111 |
112 |
113 | );
114 | };
115 |
116 | export default ImageSetupSection;
117 |
--------------------------------------------------------------------------------
/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/components/loadFromFile/ValidationMessage.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | const ValidationContainer = styled.div`
5 | width: 100%;
6 | border: 1px solid red;
7 | `;
8 |
9 | const TitleContainer = styled.h3`
10 | display: block;
11 | text-align: center;
12 | font-size: 1.2rem;
13 | top: 0;
14 | color: white;
15 | background-color: red;
16 | margin: 0;
17 | padding: 0.5rem;
18 | `;
19 |
20 | const MessageContainer = styled.p`
21 | font-size: 1rem;
22 | color: red;
23 | font-weight: bold;
24 | padding: 0.5rem 0;
25 | `;
26 |
27 | const ValidationMessage = ({ value }) => (
28 |
29 | {value.title}
30 | {value.message}
31 |
32 | );
33 | export default ValidationMessage;
34 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/css/_variables.css:
--------------------------------------------------------------------------------
1 | $font-pixel: 'minecraftiaregular', 'Helvetica Neue Light', 'Helvetica Neue',
2 | 'Helvetica', 'Arial', sans-serif;
3 | $font-icons: 'WebFontIcons';
4 | $font-monospace: 'monospace';
5 |
6 | $max-width-container: 12em;
7 |
8 | $color-scorpion: #585858;
9 | $color-boulder: #757575;
10 | $color-dustyGray: #969696;
11 | $color-silver: #bbb;
12 | $color-brandyRose: #b17e7e;
13 |
14 | /* Button colors */
15 | $color-tamarillo: #961818;
16 | $color-darkTan: #6b1010;
17 | $color-moccaccino: #741515;
18 |
19 | $color-mineShaft: #313131;
20 | $color-doveGray: #707070;
21 | $color-tundora: #4b4949;
22 |
23 | $color-lotus: #803c3c;
24 | $color-buccaneer: #733939;
25 | $color-cowboy: #552a2a;
26 |
27 | $color-steelblue: #5786c1;
28 | $color-sanMarino: #4171ae;
29 | $color-eastBay: #3a587f;
30 |
31 | $color-chathamsBlue: #164075;
32 | $color-chambray: #2f5382;
33 | $color-cloudBurst: #253c5a;
34 |
35 | $color-shrub: #0e8044;
36 |
37 | $color-alto: #e0e0e0;
38 | $color-silveChalice: #a0a0a0;
39 | $color-nobel: #b7b7b7;
40 |
41 | $color-back-frames: rgba(51, 50, 50, 0.2);
42 |
--------------------------------------------------------------------------------
/src/css/components/_App.css:
--------------------------------------------------------------------------------
1 | #app {
2 | .app__main-container,
3 | .app__central-container {
4 | lost-utility: clearfix;
5 | }
6 |
7 | .app__main,
8 | .app__central-container {
9 | margin-bottom: 2em;
10 | }
11 |
12 | .app__frames-container {
13 | margin-bottom: 1em;
14 | }
15 |
16 | .app__frames-container[data-tooltip]::after {
17 | width: 80%;
18 | margin-left: -40%;
19 | }
20 |
21 | .app__load-save-container {
22 | margin-bottom: 2em;
23 |
24 | .app__load-button {
25 | @mixin button gray;
26 | }
27 |
28 | lost-utility: clearfix;
29 |
30 | > div,
31 | > button {
32 | lost-column: 1/2 0 0.5em;
33 | }
34 | }
35 |
36 | .app__social-container {
37 | .app__download-button {
38 | @mixin button brown;
39 | @mixin icon download;
40 |
41 | width: 100%;
42 | margin-top: 1em;
43 | }
44 | }
45 |
46 | .app__help-container {
47 | lost-utility: clearfix;
48 | margin: 1em 0;
49 |
50 | > div {
51 | lost-column: 1/2 0 0.5em;
52 | }
53 |
54 | .app__toggle-help-button {
55 | @mixin button darkblue;
56 | @mixin icon help;
57 |
58 | &.selected {
59 | box-shadow: 0 0 0 2px $color-silver;
60 | }
61 |
62 | width: 100%;
63 | }
64 |
65 | .app__shortcuts-button {
66 | @mixin button darkblue;
67 | @mixin icon keybindings;
68 |
69 | width: 100%;
70 | }
71 | }
72 |
73 | .app__copycss-button {
74 | @mixin button white;
75 |
76 | width: 100%;
77 | margin-top: 1em;
78 | font-weight: bold !important;
79 | color: $color-mineShaft;
80 | }
81 |
82 | .app__preview-button {
83 | @mixin button gray;
84 |
85 | width: 100%;
86 | margin: 0 0 0.6em;
87 | display: table;
88 | }
89 |
90 | .app__tools-wrapper {
91 | text-align: center;
92 | margin: 1em 0 0.6em;
93 |
94 | > div {
95 | button {
96 | font-size: 1.8em;
97 | text-align: center;
98 | color: $color-mineShaft;
99 | padding: 0.4em 0;
100 | width: 100%;
101 | border: none;
102 | background-color: transparent;
103 |
104 | &:focus {
105 | outline: 0;
106 | }
107 |
108 | &.selected {
109 | color: $color-silver;
110 | background-color: $color-mineShaft;
111 |
112 | & > div {
113 | background-color: $color-mineShaft;
114 | }
115 | }
116 | }
117 | }
118 | }
119 |
120 | .app__left-side,
121 | .app__right-side {
122 | width: 60%;
123 | }
124 |
125 | .app__right-side {
126 | margin-left: auto;
127 | margin-right: 0;
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/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/_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/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/_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/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/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/_Dimensions.css:
--------------------------------------------------------------------------------
1 | .dimensions {
2 | margin: 1em 0 0;
3 | padding: 0.5em 0;
4 | }
5 |
--------------------------------------------------------------------------------
/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/_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/css/components/_Eraser.css:
--------------------------------------------------------------------------------
1 | .eraser {
2 | @mixin icon eraser;
3 |
4 | &:hover {
5 | cursor: pointer;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/css/components/_EyeDropper.css:
--------------------------------------------------------------------------------
1 | .eyedropper {
2 | @mixin icon eyedropper;
3 |
4 | &:hover {
5 | cursor: pointer;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/css/components/_Frame.css:
--------------------------------------------------------------------------------
1 | .frame {
2 | border: 1px solid $color-mineShaft;
3 | background-color: $color-silver;
4 | color: white;
5 | width: 70px;
6 | height: 84px;
7 | margin: 0 0.3em;
8 | flex: 0 0 auto;
9 | position: relative;
10 | overflow: hidden;
11 | opacity: 0.4;
12 |
13 | .delete,
14 | .duplicate {
15 | position: absolute;
16 | color: white;
17 | right: 0;
18 | background-color: $color-mineShaft;
19 | border: 1px solid $color-tundora;
20 | padding: 0.1em;
21 | }
22 |
23 | .delete {
24 | @mixin icon trash;
25 |
26 | font-size: 1.2em;
27 | top: 0;
28 | border-width: 0 0 2px 2px;
29 | }
30 |
31 | .duplicate {
32 | @mixin icon duplicate;
33 |
34 | bottom: 23px;
35 | border-width: 2px 0 0 2px;
36 | }
37 |
38 | .frame__percentage {
39 | position: absolute;
40 | bottom: 0;
41 | left: 0;
42 | height: 23px;
43 | border-top: 2px solid $color-tundora;
44 | }
45 |
46 | &.active {
47 | border: 2px solid $color-tamarillo;
48 | opacity: 1;
49 |
50 | .delete {
51 | cursor: no-drop;
52 | }
53 |
54 | .duplicate {
55 | cursor: copy;
56 | }
57 |
58 | .delete,
59 | .duplicate {
60 | border-color: $color-tamarillo;
61 | }
62 |
63 | .frame__percentage {
64 | border-color: $color-tamarillo;
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/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/css/components/_LoadDrawing.css:
--------------------------------------------------------------------------------
1 | .load-drawing {
2 | text-align: center;
3 | h2 {
4 | padding: 2em 0 0;
5 | margin-bottom: 0;
6 | font-size: 1.2em;
7 | text-align: center;
8 |
9 | display: flex;
10 | flex-direction: column;
11 | overflow: auto;
12 | min-width: auto;
13 | }
14 |
15 | .load-drawing__container {
16 | overflow: auto;
17 | display: flex;
18 | flex-direction: row;
19 | flex-wrap: wrap;
20 | text-align: center;
21 |
22 | &.empty {
23 | overflow-y: hidden;
24 | display: block;
25 | }
26 |
27 | .load-drawing__drawing {
28 | position: relative;
29 | border: 3px solid black;
30 | cursor: pointer;
31 | flex: 0 1 auto;
32 | margin: 0.5em;
33 | padding-right: 2em;
34 |
35 | .drawing__delete {
36 | @mixin icon trash;
37 |
38 | position: absolute;
39 | font-size: 1.7em;
40 | color: white;
41 | top: 0;
42 | right: 0;
43 | cursor: no-drop;
44 | padding: 0.1em;
45 | background-color: $color-mineShaft;
46 | border: 2px solid black;
47 | border-width: 0 0 2px 2px;
48 | }
49 |
50 | .preview {
51 | margin: 0 auto;
52 | }
53 | }
54 | }
55 |
56 | .load-drawing__export {
57 | overflow-x: scroll;
58 | width: 100%;
59 | margin: 0 auto;
60 | background-color: $color-mineShaft;
61 | color: $color-silver;
62 | padding: 0.5em;
63 | text-align: left;
64 | }
65 |
66 | .load-drawing__import {
67 | font-family: $font-monospace;
68 | display: block;
69 | width: 90%;
70 | resize: none;
71 | height: 6em;
72 | margin: 0 auto;
73 | padding: 0.5em;
74 | background-color: $color-mineShaft;
75 | color: $color-silver;
76 | }
77 |
78 | .import__button {
79 | @mixin button red;
80 |
81 | margin: 1.5em auto;
82 | display: table;
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/css/components/_Modal.css:
--------------------------------------------------------------------------------
1 | .modal {
2 | display: flex;
3 | flex: 1;
4 | flex-direction: column;
5 | overflow-x: hidden;
6 | .modal__header {
7 | text-align: right;
8 | padding-bottom: 1em;
9 | button.close {
10 | @mixin button blue;
11 | padding: 0.4em 0.7em 0.3em 0.8em;
12 | }
13 | }
14 | .preview {
15 | margin: 0 auto;
16 | }
17 | .modal__body {
18 | overflow: auto;
19 | }
20 | .modal__preview,
21 | .modal__load {
22 | .modal__preview--wrapper {
23 | margin: 1em auto;
24 | display: table;
25 | }
26 | }
27 |
28 | .modal__load,
29 | .modal__preview,
30 | .modal__body {
31 | fieldset {
32 | padding: 1em 0;
33 |
34 | label {
35 | margin: 1em 0.5em;
36 | display: inline-block;
37 | }
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/css/components/_Move.css:
--------------------------------------------------------------------------------
1 | .move {
2 | @mixin icon move;
3 |
4 | &:hover {
5 | cursor: pointer;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/css/components/_Output.css:
--------------------------------------------------------------------------------
1 | .output {
2 | position: relative;
3 | text-align: center;
4 | .copy-to-clipboard__container {
5 | position: absolute;
6 | left: 0;
7 | top: 0;
8 | button.copy-to-clipboard {
9 | @mixin button brown;
10 | padding: 0.1em 0.4em 0em 0.5em;
11 | margin: 0.5em 0.5em 0;
12 | }
13 | span {
14 | background-color: $color-shrub;
15 | color: white;
16 | padding: 0.1em 1.6em;
17 | display: none;
18 | &.show {
19 | display: inline;
20 | }
21 | }
22 | }
23 | .output__text {
24 | font-family: $font-monospace;
25 | overflow-x: auto;
26 | background-color: $color-mineShaft;
27 | color: $color-silver;
28 | padding: 3em 0.5em 0;
29 | text-align: left;
30 | display: block;
31 | width: 100%;
32 | resize: none;
33 | height: 20em;
34 | }
35 | .output__pre {
36 | white-space: pre;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/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/_PaletteGrid.css:
--------------------------------------------------------------------------------
1 | .palette-grid {
2 | lost-utility: clearfix;
3 | text-align: center;
4 | margin: 0.5em 0;
5 | }
6 |
--------------------------------------------------------------------------------
/src/css/components/_Picker.css:
--------------------------------------------------------------------------------
1 | .picker {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | text-align: center;
6 |
7 | label.picker__columns {
8 | @mixin icon columns;
9 | }
10 |
11 | label.picker__rows {
12 | @mixin icon rows;
13 | }
14 |
15 | label.picker__columns,
16 | label.picker__rows {
17 | display: flex;
18 | flex: 0 0 100%;
19 | }
20 |
21 | label {
22 | flex: 0.5;
23 | font-size: 2em !important;
24 | color: $color-mineShaft;
25 | position: relative;
26 | margin: 0 auto;
27 | text-align: center;
28 |
29 | &::before {
30 | display: inline-block;
31 | width: 1.5em;
32 | text-align: left;
33 | }
34 | }
35 |
36 | .picker__container,
37 | label {
38 | display: flex;
39 | align-items: center;
40 | justify-content: center;
41 | }
42 |
43 | .picker__value,
44 | .picker__buttons {
45 | flex: 1;
46 | }
47 |
48 | .picker__container {
49 | font-family: $font-pixel;
50 | font-size: 16px;
51 | color: $color-silver;
52 | background-color: $color-tundora;
53 | border: 3px solid $color-boulder;
54 | flex: 1;
55 |
56 | button {
57 | color: $color-silver;
58 | background-color: $color-mineShaft;
59 | border: none;
60 | outline: none;
61 | display: block;
62 | width: 100%;
63 | height: 2em;
64 | border-left: 3px solid $color-boulder;
65 | }
66 |
67 | button:hover {
68 | background-color: $color-tundora;
69 | }
70 |
71 | .button-add {
72 | border-bottom: 1px solid $color-boulder;
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/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/_PreviewBox.css:
--------------------------------------------------------------------------------
1 | .preview-box {
2 | & .buttons {
3 | @mixin flex-row-center;
4 | margin-bottom: 0.5rem;
5 | div {
6 | padding-left: 0.2em;
7 | }
8 | }
9 |
10 | & .preview-box__container {
11 | @mixin flex-row-center;
12 | background-color: $color-back-frames;
13 | padding: 0.8em 0 0.8em 0;
14 | margin: 0.8em 0;
15 | align-items: center;
16 | }
17 |
18 | .play {
19 | @mixin button gray;
20 | @mixin icon play;
21 | }
22 |
23 | .pause {
24 | @mixin button brown;
25 | @mixin icon pause;
26 | }
27 |
28 | .screen-full {
29 | @mixin button gray;
30 | @mixin icon screen-full;
31 | }
32 |
33 | .screen-normal {
34 | @mixin button gray;
35 | @mixin icon screen-normal;
36 | }
37 |
38 | .frames {
39 | @mixin button gray;
40 | @mixin icon frames;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/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/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/components/_SaveDrawing.css:
--------------------------------------------------------------------------------
1 | .save-drawing {
2 | button {
3 | @mixin button gray;
4 |
5 | width: 100%;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/css/components/_SimpleNotification.css:
--------------------------------------------------------------------------------
1 | .simple-notification {
2 | background-color: #313131;
3 | color: $color-silver;
4 | width: 250px;
5 | text-align: center;
6 | padding: 1em;
7 | position: fixed;
8 | z-index: 1;
9 | left: 50%;
10 | margin-left: -125px;
11 | top: 1em;
12 | border: 1px solid orange;
13 | }
14 |
15 | /* ReactCSSTransitionGroup related rules */
16 | .simple-notification-enter {
17 | opacity: 0.01;
18 | }
19 |
20 | .simple-notification-enter.simple-notification-enter-active {
21 | opacity: 1;
22 | transition: opacity 1000ms ease-in;
23 | }
24 |
25 | .simple-notification-exit {
26 | opacity: 1;
27 | }
28 |
29 | .simple-notification-exit.simple-notification-exit-active {
30 | opacity: 0.01;
31 | transition: opacity 1000ms ease-out;
32 | }
33 |
--------------------------------------------------------------------------------
/src/css/components/_SimpleSpinner.css:
--------------------------------------------------------------------------------
1 | .simple-spinner {
2 | position: absolute;
3 | z-index: 1;
4 | left: 0;
5 | right: 0;
6 | margin-left: auto;
7 | margin-right: auto;
8 | width: 100%;
9 | height: 100%;
10 | background-color: black;
11 | opacity: 0.4;
12 | top: 0;
13 | display: none;
14 |
15 | &.display {
16 | display: block;
17 | }
18 |
19 | .circle {
20 | height: 60px;
21 | width: 60px;
22 | margin: 94px auto 0;
23 | position: fixed;
24 | animation: spin-rotation 0.6s infinite linear;
25 | border-left: 6px solid rgba(239, 149, 50, 0.35);
26 | border-right: 6px solid rgba(239, 149, 50, 0.35);
27 | border-bottom: 6px solid rgba(239, 149, 50, 0.35);
28 | border-top: 6px solid rgba(219, 109, 26, 1);
29 | border-radius: 100%;
30 | top: 50%;
31 | left: 50%;
32 | margin-top: -30px;
33 | margin-left: -30px;
34 | }
35 | }
36 |
37 | @keyframes spin-rotation {
38 | from {
39 | transform: rotate(0deg);
40 | }
41 |
42 | to {
43 | transform: rotate(359deg);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/css/components/_UsefulData.css:
--------------------------------------------------------------------------------
1 | .useful-data {
2 | text-align: center;
3 | .useful-data__rows {
4 | border: none;
5 | background-color: $color-alto;
6 | padding: 1em 0;
7 | .checkbox {
8 | padding: 0.4em 0;
9 | }
10 | }
11 | .useful-data__export {
12 | font-family: $font-monospace;
13 | overflow-x: hidden;
14 | margin: 1em auto;
15 | background-color: $color-mineShaft;
16 | color: $color-silver;
17 | padding: 0.5em;
18 | text-align: left;
19 | display: block;
20 | width: 100%;
21 | resize: none;
22 | height: 20em;
23 | }
24 | .useful-data__options {
25 | lost-utility: clearfix;
26 | fieldset.useful-data__rows,
27 | .useful-data__output {
28 | lost-column: 1/2 0 0;
29 | }
30 | fieldset.useful-data__rows {
31 | lost-utility: clearfix;
32 | margin: 0;
33 | height: 20em;
34 | .useful-data__pixel-format,
35 | .useful-data__reverse-rows {
36 | lost-column: 1/2 0 0;
37 | }
38 | }
39 | }
40 | }
41 |
42 | @media only screen and (max-width: 730px) {
43 | .useful-data {
44 | .useful-data__options {
45 | fieldset.useful-data__rows,
46 | .useful-data__output {
47 | lost-column: 1/1;
48 | }
49 | fieldset.useful-data__rows {
50 | height: auto;
51 | .useful-data__pixel-format,
52 | .useful-data__reverse-rows {
53 | lost-column: 1/1;
54 | }
55 | }
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/css/fonts/_fonts.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'minecraftiaregular';
3 | src: url('fonts/files/minecraftia-regular-webfont.eot');
4 | src: url('fonts/files/minecraftia-regular-webfont.eot?#iefix')
5 | format('embedded-opentype'),
6 | url('fonts/files/minecraftia-regular-webfont.woff2') format('woff2'),
7 | url('fonts/files/minecraftia-regular-webfont.woff') format('woff'),
8 | url('fonts/files/minecraftia-regular-webfont.ttf') format('truetype'),
9 | url('fonts/files/minecraftia-regular-webfont.svg#minecraftiaregular')
10 | format('svg');
11 | font-weight: normal;
12 | font-style: normal;
13 | }
14 |
15 | @font-face {
16 | font-family: 'WebFontIcons';
17 | src: url('fonts/files/webfont-icons.eot?v=4.5.0');
18 | src: url('fonts/files/webfont-icons.eot?#iefix&v=4.5.0')
19 | format('embedded-opentype'),
20 | url('fonts/files/webfont-icons.woff?v=4.5.0') format('woff'),
21 | url('fonts/files/webfont-icons.ttf?v=4.5.0') format('truetype'),
22 | url('fonts/files/webfont-icons.svg?v=4.5.0#webfonticons') format('svg');
23 | font-weight: normal;
24 | font-style: normal;
25 | }
26 |
27 | @define-mixin icon $type {
28 | display: inline-block;
29 | font: normal normal normal 14px/1 WebFontIcons;
30 | font-size: inherit;
31 | text-rendering: auto;
32 | -webkit-font-smoothing: antialiased;
33 | -moz-osx-font-smoothing: grayscale;
34 |
35 | &::before {
36 | @extend $type;
37 | }
38 | }
39 |
40 | @define-extend twitter {
41 | content: '\61';
42 | }
43 | @define-extend keybindings {
44 | content: '\62';
45 | }
46 | @define-extend play {
47 | content: '\63';
48 | }
49 | @define-extend duplicate {
50 | content: '\64';
51 | }
52 | @define-extend eraser {
53 | content: '\65';
54 | }
55 | @define-extend pause {
56 | content: '\66';
57 | }
58 | @define-extend screen-full {
59 | content: '\67';
60 | }
61 | @define-extend paint-brush {
62 | content: '\68';
63 | }
64 | @define-extend move {
65 | content: '\69';
66 | }
67 | @define-extend screen-normal {
68 | content: '\6a';
69 | }
70 | @define-extend frames {
71 | content: '\6b';
72 | }
73 | @define-extend columns {
74 | content: '\6c';
75 | }
76 | @define-extend rows {
77 | content: '\6d';
78 | }
79 | @define-extend bucket {
80 | content: '\6e';
81 | }
82 | @define-extend eyedropper {
83 | content: '\6f';
84 | }
85 | @define-extend undo {
86 | content: '\70';
87 | }
88 | @define-extend redo {
89 | content: '\71';
90 | }
91 | @define-extend download {
92 | content: '\72';
93 | }
94 | @define-extend help {
95 | content: '\73';
96 | }
97 | @define-extend trash {
98 | content: '\74';
99 | }
100 |
101 | .icon-help {
102 | @mixin icon help;
103 | }
104 |
--------------------------------------------------------------------------------
/src/css/fonts/files/minecraftia-regular-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sealdeck/pixel-art-react/6d617d1268aa9e2098164748581f7c4d9497fba7/src/css/fonts/files/minecraftia-regular-webfont.eot
--------------------------------------------------------------------------------
/src/css/fonts/files/minecraftia-regular-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sealdeck/pixel-art-react/6d617d1268aa9e2098164748581f7c4d9497fba7/src/css/fonts/files/minecraftia-regular-webfont.ttf
--------------------------------------------------------------------------------
/src/css/fonts/files/minecraftia-regular-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sealdeck/pixel-art-react/6d617d1268aa9e2098164748581f7c4d9497fba7/src/css/fonts/files/minecraftia-regular-webfont.woff
--------------------------------------------------------------------------------
/src/css/fonts/files/minecraftia-regular-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sealdeck/pixel-art-react/6d617d1268aa9e2098164748581f7c4d9497fba7/src/css/fonts/files/minecraftia-regular-webfont.woff2
--------------------------------------------------------------------------------
/src/css/fonts/files/webfont-icons.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sealdeck/pixel-art-react/6d617d1268aa9e2098164748581f7c4d9497fba7/src/css/fonts/files/webfont-icons.eot
--------------------------------------------------------------------------------
/src/css/fonts/files/webfont-icons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sealdeck/pixel-art-react/6d617d1268aa9e2098164748581f7c4d9497fba7/src/css/fonts/files/webfont-icons.ttf
--------------------------------------------------------------------------------
/src/css/fonts/files/webfont-icons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sealdeck/pixel-art-react/6d617d1268aa9e2098164748581f7c4d9497fba7/src/css/fonts/files/webfont-icons.woff
--------------------------------------------------------------------------------
/src/css/imports.css:
--------------------------------------------------------------------------------
1 | @charset 'UTF-8';
2 |
3 | @import '_normalize.css';
4 | @import 'fonts/_fonts.css';
5 | @import '_variables.css';
6 | @import '_utils.css';
7 | @import '_base.css';
8 | @import 'layout/_grid.css';
9 | @import 'layout/_header.css';
10 | @import 'layout/_queries.css';
11 | @import 'layout/_flex.css';
12 | @import 'input/_inputText.css';
13 | @import 'input/_button.css';
14 | @import 'views/_cookies.css';
15 | @import 'views/_notFound.css';
16 | @import 'components/_PixelGrid.css';
17 | @import 'components/_SimpleNotification.css';
18 | @import 'components/_SimpleSpinner.css';
19 | @import 'components/_LoadDrawing.css';
20 | @import 'components/_SaveDrawing.css';
21 | @import 'components/_NewProject.css';
22 | @import 'components/_UndoRedo.css';
23 | @import 'components/_App.css';
24 | @import 'components/_Reset.css';
25 | @import 'components/_RadioSelector.css';
26 | @import 'components/_PaletteGrid.css';
27 | @import 'components/_PaletteColor.css';
28 | @import 'components/_Modal.css';
29 | @import 'components/_FramesHandler.css';
30 | @import 'components/_Frame.css';
31 | @import 'components/_EyeDropper.css';
32 | @import 'components/_Eraser.css';
33 | @import 'components/_Bucket.css';
34 | @import 'components/_UsefulData.css';
35 | @import 'components/_Output.css';
36 | @import 'components/_Checkbox.css';
37 | @import 'components/_Move.css';
38 | @import 'components/_DownloadDrawing.css';
39 | @import 'components/_Dimensions.css';
40 | @import 'components/_Picker.css';
41 | @import 'components/_CellSize.css';
42 | @import 'components/_CssDisplay.css';
43 | @import 'components/_CopyCss.css';
44 | @import 'components/_ColorPicker.css';
45 | @import 'components/_Duration.css';
46 | @import 'components/_CellInfo.css';
47 | @import 'components/_PreviewBox.css';
48 |
--------------------------------------------------------------------------------
/src/css/input/_button.css:
--------------------------------------------------------------------------------
1 | @define-mixin generate-button $color-text, $color-bg, $color-boxshadow,
2 | $color-bg-active {
3 | background: none;
4 | border: none;
5 | outline: none;
6 | border-radius: 2px;
7 | padding: 10px;
8 | font-size: 1em;
9 | text-decoration: none;
10 | transition-duration: 0.1s;
11 | color: $color-text;
12 | background-color: $color-bg;
13 | box-shadow: 0 5px 0 0 $color-boxshadow;
14 |
15 | &:hover,
16 | &.selected {
17 | background-color: $color-bg-active;
18 | }
19 |
20 | &:hover {
21 | cursor: pointer;
22 | }
23 |
24 | &:active {
25 | transform: translate(0, 5px);
26 | box-shadow: 0 1px 0 $color-bg-active;
27 | background-color: $color-bg-active;
28 | }
29 | }
30 |
31 | @define-mixin button $color, $font: $font-pixel {
32 | @if $color == red {
33 | @mixin generate-button $color-silver, $color-tamarillo, $color-darkTan,
34 | $color-moccaccino;
35 | }
36 |
37 | @if $color == gray {
38 | @mixin generate-button $color-silver, $color-mineShaft, $color-doveGray,
39 | $color-tundora;
40 | }
41 |
42 | @if $color == brown {
43 | @mixin generate-button $color-silver, $color-lotus, $color-buccaneer,
44 | $color-cowboy;
45 | }
46 |
47 | @if $color == blue {
48 | @mixin generate-button $color-silver, $color-steelblue, $color-sanMarino,
49 | $color-eastBay;
50 | }
51 |
52 | @if $color == darkblue {
53 | @mixin generate-button $color-silver, $color-chathamsBlue, $color-chambray,
54 | $color-cloudBurst;
55 | }
56 |
57 | @if $color == white {
58 | @mixin generate-button black, $color-alto, $color-silveChalice, $color-nobel;
59 | }
60 |
61 | font-family: $font;
62 | }
63 |
--------------------------------------------------------------------------------
/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/css/layout/_flex.css:
--------------------------------------------------------------------------------
1 | @define-mixin flex-row-center {
2 | display: flex;
3 | flex-flow: row nowrap;
4 | justify-content: center;
5 | }
6 |
--------------------------------------------------------------------------------
/src/css/layout/_grid.css:
--------------------------------------------------------------------------------
1 | .clearfix {
2 | lost-utility: clearfix;
3 | }
4 |
5 | .col-1-3 {
6 | lost-column: 1/3;
7 | }
8 |
9 | .col-2-3 {
10 | lost-column: 2/3;
11 | }
12 |
13 | .col-1-2 {
14 | lost-column: 1/2;
15 | }
16 |
17 | .col-1-4 {
18 | lost-column: 1/4;
19 | }
20 |
21 | .col-2-4 {
22 | lost-column: 2/4;
23 | }
24 |
25 | .col-3-4,
26 | .col-6-8 {
27 | lost-column: 3/4;
28 | }
29 |
30 | .col-1-8 {
31 | lost-column: 1/8;
32 | }
33 |
34 | .col-7-8 {
35 | lost-column: 7/8;
36 | }
37 |
38 | .grid-3 {
39 | lost-utility: clearfix;
40 |
41 | & > div {
42 | lost-column: 1/3 3 0;
43 | }
44 | }
45 |
46 | .grid-4 {
47 | lost-utility: clearfix;
48 |
49 | & > div {
50 | lost-column: 1/4 4 0;
51 | }
52 | }
53 |
54 | .grid-2 {
55 | lost-utility: clearfix;
56 |
57 | & > div {
58 | lost-column: 1/2 2 0;
59 | }
60 | }
61 |
62 | .max-width-container {
63 | max-width: $max-width-container;
64 | }
65 |
66 | .max-width-container-centered {
67 | max-width: $max-width-container;
68 | margin: 0 auto;
69 | }
70 |
--------------------------------------------------------------------------------
/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/layout/_queries.css:
--------------------------------------------------------------------------------
1 | @media only screen and (max-width: 1250px) {
2 | .app__left-side,
3 | .app__right-side {
4 | width: 100% !important;
5 | }
6 | }
7 |
8 | @media only screen and (max-width: 780px) {
9 | .dimensions {
10 | label {
11 | font-size: 1.5em !important;
12 | top: -0.2em;
13 | }
14 | }
15 | }
16 |
17 | @media only screen and (min-width: 730px) {
18 | /*
19 | Fix small vertical scrollbar glitch for some resolutions
20 | inside frames-handler
21 | Detected in Chrome
22 | */
23 | .app__frames-container::-webkit-scrollbar,
24 | .app__frames-container div::-webkit-scrollbar {
25 | width: 1em !important;
26 | }
27 | }
28 |
29 | @media only screen and (max-width: 730px) {
30 | .app__copycss-button {
31 | margin-top: 1.4em !important;
32 | }
33 |
34 | .app__central-container {
35 | .col-1-4 {
36 | &.left {
37 | lost-column: none;
38 |
39 | .app__copycss-button {
40 | padding: 4px !important;
41 | }
42 |
43 | .palette-grid {
44 | margin: 0;
45 | }
46 | }
47 |
48 | &.right {
49 | lost-column: none;
50 | }
51 | }
52 |
53 | .col-2-4 {
54 | &.center {
55 | lost-column: none;
56 |
57 | .grid-container {
58 | margin: 1em auto !important;
59 | }
60 | }
61 | }
62 | }
63 |
64 | .app__mobile--container {
65 | lost-utility: clearfix;
66 |
67 | .app__mobile--group {
68 | lost-column: 1/2;
69 | }
70 |
71 | max-width: none;
72 | }
73 |
74 | .app__preview-button {
75 | margin-top: 1em !important;
76 | }
77 |
78 | body {
79 | width: 100% !important;
80 | }
81 |
82 | /* Avoid button hover effect in mobile devices */
83 | .undo-redo,
84 | .app__load-save-container,
85 | .new-project,
86 | .frames-handler,
87 | .app__right-side {
88 | button:hover,
89 | button.selected {
90 | background-color: $color-mineShaft !important;
91 | }
92 | }
93 |
94 | button.app__copycss-button {
95 | &:hover,
96 | &.selected {
97 | background-color: $color-alto !important;
98 | }
99 | }
100 |
101 | .app__toggle-help-button {
102 | &:hover,
103 | &.selected {
104 | background-color: $color-chathamsBlue !important;
105 | }
106 | }
107 |
108 | .load-drawing__import {
109 | height: 10em !important;
110 | }
111 | }
112 |
113 | @media only screen and (max-width: 360px) {
114 | button,
115 | .dimensions {
116 | font-size: 0.8em !important;
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/css/views/_cookies.css:
--------------------------------------------------------------------------------
1 | .cookies-disclaimer {
2 | padding: 2em;
3 | text-align: left;
4 | color: $color-silver;
5 | width: 50%;
6 | margin: 0 auto;
7 | h2 {
8 | font-size: 2.5em;
9 | top: 0;
10 | margin: 1em 0 1.5em 0;
11 | display: block;
12 | text-align: center;
13 | padding: 0;
14 | }
15 | h3 {
16 | font-size: 1.5em;
17 | }
18 | a {
19 | text-decoration: none;
20 | color: #78b4ff;
21 | }
22 | li {
23 | list-style-type: none;
24 | }
25 | ul li:before {
26 | content: '•';
27 | font-size: 100%;
28 | padding-right: 10px;
29 | }
30 | .art-wrapper {
31 | height: 100px;
32 | width: 100px;
33 | margin: 1em auto;
34 | }
35 | .pixelart-cookie {
36 | box-shadow: 40px 10px 0 #ac7d40, 50px 10px 0 #ac7d40, 60px 10px 0 #332413,
37 | 70px 10px 0 #eea64b, 30px 20px 0 #ac7d40, 40px 20px 0 #eea64b,
38 | 50px 20px 0 #eea64b, 60px 20px 0 #eea64b, 70px 20px 0 #eea64b,
39 | 80px 20px 0 #ac7d40, 20px 30px 0 #ac7d40, 30px 30px 0 #332413,
40 | 40px 30px 0 #eea64b, 50px 30px 0 #eea64b, 60px 30px 0 #eea64b,
41 | 70px 30px 0 #332413, 80px 30px 0 #332413, 90px 30px 0 #eea64b,
42 | 20px 40px 0 #eea64b, 30px 40px 0 #eea64b, 40px 40px 0 #eea64b,
43 | 50px 40px 0 #332413, 60px 40px 0 #eea64b, 70px 40px 0 #332413,
44 | 80px 40px 0 #332413, 90px 40px 0 #eea64b, 10px 50px 0 #ac7d40,
45 | 20px 50px 0 #332413, 30px 50px 0 #eea64b, 40px 50px 0 #eea64b,
46 | 50px 50px 0 #eea64b, 60px 50px 0 #eea64b, 70px 50px 0 #eea64b,
47 | 80px 50px 0 #eea64b, 90px 50px 0 #eea64b, 100px 50px 0 #ac7d40,
48 | 10px 60px 0 #ac7d40, 20px 60px 0 #eea64b, 30px 60px 0 #eea64b,
49 | 40px 60px 0 #332413, 50px 60px 0 #eea64b, 60px 60px 0 #eea64b,
50 | 70px 60px 0 #eea64b, 80px 60px 0 #eea64b, 90px 60px 0 #332413,
51 | 100px 60px 0 #ac7d40, 10px 70px 0 #9a4719, 20px 70px 0 #332413,
52 | 30px 70px 0 #eea64b, 40px 70px 0 #eea64b, 50px 70px 0 #eea64b,
53 | 60px 70px 0 #332413, 70px 70px 0 #eea64b, 80px 70px 0 #dc9736,
54 | 90px 70px 0 #dc9736, 100px 70px 0 #9a4719, 20px 80px 0 #9a4719,
55 | 30px 80px 0 #dc9736, 40px 80px 0 #dc9736, 50px 80px 0 #dc9736,
56 | 60px 80px 0 #dc9736, 70px 80px 0 #dc9736, 80px 80px 0 #dc9736,
57 | 90px 80px 0 #9a4719, 30px 90px 0 #9a4719, 40px 90px 0 #dc9736,
58 | 50px 90px 0 #dc9736, 60px 90px 0 #dc9736, 70px 90px 0 #332413,
59 | 80px 90px 0 #9a4719, 40px 100px 0 #9a4719, 50px 100px 0 #9a4719,
60 | 60px 100px 0 #9a4719, 70px 100px 0 #9a4719;
61 | -webkit-box-shadow: 40px 10px 0 #ac7d40, 50px 10px 0 #ac7d40,
62 | 60px 10px 0 #332413, 70px 10px 0 #eea64b, 30px 20px 0 #ac7d40,
63 | 40px 20px 0 #eea64b, 50px 20px 0 #eea64b, 60px 20px 0 #eea64b,
64 | 70px 20px 0 #eea64b, 80px 20px 0 #ac7d40, 20px 30px 0 #ac7d40,
65 | 30px 30px 0 #332413, 40px 30px 0 #eea64b, 50px 30px 0 #eea64b,
66 | 60px 30px 0 #eea64b, 70px 30px 0 #332413, 80px 30px 0 #332413,
67 | 90px 30px 0 #eea64b, 20px 40px 0 #eea64b, 30px 40px 0 #eea64b,
68 | 40px 40px 0 #eea64b, 50px 40px 0 #332413, 60px 40px 0 #eea64b,
69 | 70px 40px 0 #332413, 80px 40px 0 #332413, 90px 40px 0 #eea64b,
70 | 10px 50px 0 #ac7d40, 20px 50px 0 #332413, 30px 50px 0 #eea64b,
71 | 40px 50px 0 #eea64b, 50px 50px 0 #eea64b, 60px 50px 0 #eea64b,
72 | 70px 50px 0 #eea64b, 80px 50px 0 #eea64b, 90px 50px 0 #eea64b,
73 | 100px 50px 0 #ac7d40, 10px 60px 0 #ac7d40, 20px 60px 0 #eea64b,
74 | 30px 60px 0 #eea64b, 40px 60px 0 #332413, 50px 60px 0 #eea64b,
75 | 60px 60px 0 #eea64b, 70px 60px 0 #eea64b, 80px 60px 0 #eea64b,
76 | 90px 60px 0 #332413, 100px 60px 0 #ac7d40, 10px 70px 0 #9a4719,
77 | 20px 70px 0 #332413, 30px 70px 0 #eea64b, 40px 70px 0 #eea64b,
78 | 50px 70px 0 #eea64b, 60px 70px 0 #332413, 70px 70px 0 #eea64b,
79 | 80px 70px 0 #dc9736, 90px 70px 0 #dc9736, 100px 70px 0 #9a4719,
80 | 20px 80px 0 #9a4719, 30px 80px 0 #dc9736, 40px 80px 0 #dc9736,
81 | 50px 80px 0 #dc9736, 60px 80px 0 #dc9736, 70px 80px 0 #dc9736,
82 | 80px 80px 0 #dc9736, 90px 80px 0 #9a4719, 30px 90px 0 #9a4719,
83 | 40px 90px 0 #dc9736, 50px 90px 0 #dc9736, 60px 90px 0 #dc9736,
84 | 70px 90px 0 #332413, 80px 90px 0 #9a4719, 40px 100px 0 #9a4719,
85 | 50px 100px 0 #9a4719, 60px 100px 0 #9a4719, 70px 100px 0 #9a4719;
86 | -moz-box-shadow: 40px 10px 0 #ac7d40, 50px 10px 0 #ac7d40,
87 | 60px 10px 0 #332413, 70px 10px 0 #eea64b, 30px 20px 0 #ac7d40,
88 | 40px 20px 0 #eea64b, 50px 20px 0 #eea64b, 60px 20px 0 #eea64b,
89 | 70px 20px 0 #eea64b, 80px 20px 0 #ac7d40, 20px 30px 0 #ac7d40,
90 | 30px 30px 0 #332413, 40px 30px 0 #eea64b, 50px 30px 0 #eea64b,
91 | 60px 30px 0 #eea64b, 70px 30px 0 #332413, 80px 30px 0 #332413,
92 | 90px 30px 0 #eea64b, 20px 40px 0 #eea64b, 30px 40px 0 #eea64b,
93 | 40px 40px 0 #eea64b, 50px 40px 0 #332413, 60px 40px 0 #eea64b,
94 | 70px 40px 0 #332413, 80px 40px 0 #332413, 90px 40px 0 #eea64b,
95 | 10px 50px 0 #ac7d40, 20px 50px 0 #332413, 30px 50px 0 #eea64b,
96 | 40px 50px 0 #eea64b, 50px 50px 0 #eea64b, 60px 50px 0 #eea64b,
97 | 70px 50px 0 #eea64b, 80px 50px 0 #eea64b, 90px 50px 0 #eea64b,
98 | 100px 50px 0 #ac7d40, 10px 60px 0 #ac7d40, 20px 60px 0 #eea64b,
99 | 30px 60px 0 #eea64b, 40px 60px 0 #332413, 50px 60px 0 #eea64b,
100 | 60px 60px 0 #eea64b, 70px 60px 0 #eea64b, 80px 60px 0 #eea64b,
101 | 90px 60px 0 #332413, 100px 60px 0 #ac7d40, 10px 70px 0 #9a4719,
102 | 20px 70px 0 #332413, 30px 70px 0 #eea64b, 40px 70px 0 #eea64b,
103 | 50px 70px 0 #eea64b, 60px 70px 0 #332413, 70px 70px 0 #eea64b,
104 | 80px 70px 0 #dc9736, 90px 70px 0 #dc9736, 100px 70px 0 #9a4719,
105 | 20px 80px 0 #9a4719, 30px 80px 0 #dc9736, 40px 80px 0 #dc9736,
106 | 50px 80px 0 #dc9736, 60px 80px 0 #dc9736, 70px 80px 0 #dc9736,
107 | 80px 80px 0 #dc9736, 90px 80px 0 #9a4719, 30px 90px 0 #9a4719,
108 | 40px 90px 0 #dc9736, 50px 90px 0 #dc9736, 60px 90px 0 #dc9736,
109 | 70px 90px 0 #332413, 80px 90px 0 #9a4719, 40px 100px 0 #9a4719,
110 | 50px 100px 0 #9a4719, 60px 100px 0 #9a4719, 70px 100px 0 #9a4719;
111 | height: 10px;
112 | width: 10px;
113 | }
114 | span.highlight {
115 | color: $color-brandyRose;
116 | }
117 | }
118 |
119 | @media only screen and (max-width: 1200px) {
120 | .cookies-disclaimer {
121 | width: 80%;
122 | }
123 | }
124 |
125 | @media only screen and (max-width: 700px) {
126 | .cookies-disclaimer {
127 | width: 90%;
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/src/css/views/_notFound.css:
--------------------------------------------------------------------------------
1 | .not-found {
2 | text-align: center;
3 | h2 {
4 | font-size: 2.5em;
5 | top: 0;
6 | margin: 1em 0 1em 0;
7 | display: block;
8 | text-align: center;
9 | padding: 0;
10 | color: $color-brandyRose;
11 | }
12 | a {
13 | color: #78b4ff;
14 | }
15 | .art-wrapper {
16 | height: 100px;
17 | width: 320px;
18 | margin: 1em auto 9em;
19 | }
20 |
21 | .pixelart-notfound {
22 | position: absolute;
23 | animation: notfound 1s infinite;
24 | -webkit-animation: notfound 1s infinite;
25 | -moz-animation: notfound 1s infinite;
26 | -o-animation: notfound 1s infinite;
27 | }
28 | }
29 |
30 | @keyframes notfound {
31 | 0%,
32 | 25% {
33 | box-shadow: 20px 40px 0 0 rgba(187, 187, 187, 1),
34 | 80px 40px 0 0 rgba(187, 187, 187, 1),
35 | 140px 40px 0 0 rgba(187, 187, 187, 1),
36 | 160px 40px 0 0 rgba(187, 187, 187, 1),
37 | 220px 40px 0 0 rgba(187, 187, 187, 1),
38 | 280px 40px 0 0 rgba(187, 187, 187, 1),
39 | 20px 60px 0 0 rgba(187, 187, 187, 1), 80px 60px 0 0 rgba(187, 187, 187, 1),
40 | 120px 60px 0 0 rgba(187, 187, 187, 1),
41 | 180px 60px 0 0 rgba(187, 187, 187, 1),
42 | 220px 60px 0 0 rgba(187, 187, 187, 1),
43 | 280px 60px 0 0 rgba(187, 187, 187, 1),
44 | 20px 80px 0 0 rgba(187, 187, 187, 1), 40px 80px 0 0 rgba(187, 187, 187, 1),
45 | 60px 80px 0 0 rgba(187, 187, 187, 1), 80px 80px 0 0 rgba(187, 187, 187, 1),
46 | 120px 80px 0 0 rgba(187, 187, 187, 1),
47 | 180px 80px 0 0 rgba(187, 187, 187, 1),
48 | 220px 80px 0 0 rgba(187, 187, 187, 1),
49 | 240px 80px 0 0 rgba(187, 187, 187, 1),
50 | 260px 80px 0 0 rgba(187, 187, 187, 1),
51 | 280px 80px 0 0 rgba(187, 187, 187, 1),
52 | 80px 100px 0 0 rgba(187, 187, 187, 1),
53 | 120px 100px 0 0 rgba(187, 187, 187, 1),
54 | 180px 100px 0 0 rgba(187, 187, 187, 1),
55 | 280px 100px 0 0 rgba(187, 187, 187, 1),
56 | 80px 120px 0 0 rgba(187, 187, 187, 1),
57 | 140px 120px 0 0 rgba(187, 187, 187, 1),
58 | 160px 120px 0 0 rgba(187, 187, 187, 1),
59 | 280px 120px 0 0 rgba(187, 187, 187, 1);
60 | height: 20px;
61 | width: 20px;
62 | }
63 | 25.01%,
64 | 50% {
65 | box-shadow: 20px 20px 0 0 rgba(187, 187, 187, 1),
66 | 80px 20px 0 0 rgba(187, 187, 187, 1), 20px 40px 0 0 rgba(187, 187, 187, 1),
67 | 80px 40px 0 0 rgba(187, 187, 187, 1),
68 | 140px 40px 0 0 rgba(187, 187, 187, 1),
69 | 160px 40px 0 0 rgba(187, 187, 187, 1),
70 | 220px 40px 0 0 rgba(187, 187, 187, 1),
71 | 280px 40px 0 0 rgba(187, 187, 187, 1),
72 | 20px 60px 0 0 rgba(187, 187, 187, 1), 40px 60px 0 0 rgba(187, 187, 187, 1),
73 | 60px 60px 0 0 rgba(187, 187, 187, 1), 80px 60px 0 0 rgba(187, 187, 187, 1),
74 | 120px 60px 0 0 rgba(187, 187, 187, 1),
75 | 180px 60px 0 0 rgba(187, 187, 187, 1),
76 | 220px 60px 0 0 rgba(187, 187, 187, 1),
77 | 280px 60px 0 0 rgba(187, 187, 187, 1),
78 | 80px 80px 0 0 rgba(187, 187, 187, 1),
79 | 120px 80px 0 0 rgba(187, 187, 187, 1),
80 | 180px 80px 0 0 rgba(187, 187, 187, 1),
81 | 220px 80px 0 0 rgba(187, 187, 187, 1),
82 | 240px 80px 0 0 rgba(187, 187, 187, 1),
83 | 260px 80px 0 0 rgba(187, 187, 187, 1),
84 | 280px 80px 0 0 rgba(187, 187, 187, 1),
85 | 80px 100px 0 0 rgba(187, 187, 187, 1),
86 | 120px 100px 0 0 rgba(187, 187, 187, 1),
87 | 180px 100px 0 0 rgba(187, 187, 187, 1),
88 | 280px 100px 0 0 rgba(187, 187, 187, 1),
89 | 140px 120px 0 0 rgba(187, 187, 187, 1),
90 | 160px 120px 0 0 rgba(187, 187, 187, 1),
91 | 280px 120px 0 0 rgba(187, 187, 187, 1);
92 | height: 20px;
93 | width: 20px;
94 | }
95 | 50.01%,
96 | 75% {
97 | box-shadow: 140px 20px 0 0 rgba(187, 187, 187, 1),
98 | 160px 20px 0 0 rgba(187, 187, 187, 1),
99 | 20px 40px 0 0 rgba(187, 187, 187, 1), 80px 40px 0 0 rgba(187, 187, 187, 1),
100 | 120px 40px 0 0 rgba(187, 187, 187, 1),
101 | 180px 40px 0 0 rgba(187, 187, 187, 1),
102 | 220px 40px 0 0 rgba(187, 187, 187, 1),
103 | 280px 40px 0 0 rgba(187, 187, 187, 1),
104 | 20px 60px 0 0 rgba(187, 187, 187, 1), 80px 60px 0 0 rgba(187, 187, 187, 1),
105 | 120px 60px 0 0 rgba(187, 187, 187, 1),
106 | 180px 60px 0 0 rgba(187, 187, 187, 1),
107 | 220px 60px 0 0 rgba(187, 187, 187, 1),
108 | 280px 60px 0 0 rgba(187, 187, 187, 1),
109 | 20px 80px 0 0 rgba(187, 187, 187, 1), 40px 80px 0 0 rgba(187, 187, 187, 1),
110 | 60px 80px 0 0 rgba(187, 187, 187, 1), 80px 80px 0 0 rgba(187, 187, 187, 1),
111 | 120px 80px 0 0 rgba(187, 187, 187, 1),
112 | 180px 80px 0 0 rgba(187, 187, 187, 1),
113 | 220px 80px 0 0 rgba(187, 187, 187, 1),
114 | 240px 80px 0 0 rgba(187, 187, 187, 1),
115 | 260px 80px 0 0 rgba(187, 187, 187, 1),
116 | 280px 80px 0 0 rgba(187, 187, 187, 1),
117 | 80px 100px 0 0 rgba(187, 187, 187, 1),
118 | 140px 100px 0 0 rgba(187, 187, 187, 1),
119 | 160px 100px 0 0 rgba(187, 187, 187, 1),
120 | 280px 100px 0 0 rgba(187, 187, 187, 1),
121 | 80px 120px 0 0 rgba(187, 187, 187, 1),
122 | 280px 120px 0 0 rgba(187, 187, 187, 1);
123 | height: 20px;
124 | width: 20px;
125 | }
126 | 75.01%,
127 | 100% {
128 | box-shadow: 220px 20px 0 0 rgba(187, 187, 187, 1),
129 | 280px 20px 0 0 rgba(187, 187, 187, 1),
130 | 20px 40px 0 0 rgba(187, 187, 187, 1), 80px 40px 0 0 rgba(187, 187, 187, 1),
131 | 140px 40px 0 0 rgba(187, 187, 187, 1),
132 | 160px 40px 0 0 rgba(187, 187, 187, 1),
133 | 220px 40px 0 0 rgba(187, 187, 187, 1),
134 | 280px 40px 0 0 rgba(187, 187, 187, 1),
135 | 20px 60px 0 0 rgba(187, 187, 187, 1), 80px 60px 0 0 rgba(187, 187, 187, 1),
136 | 120px 60px 0 0 rgba(187, 187, 187, 1),
137 | 180px 60px 0 0 rgba(187, 187, 187, 1),
138 | 220px 60px 0 0 rgba(187, 187, 187, 1),
139 | 240px 60px 0 0 rgba(187, 187, 187, 1),
140 | 260px 60px 0 0 rgba(187, 187, 187, 1),
141 | 280px 60px 0 0 rgba(187, 187, 187, 1),
142 | 20px 80px 0 0 rgba(187, 187, 187, 1), 40px 80px 0 0 rgba(187, 187, 187, 1),
143 | 60px 80px 0 0 rgba(187, 187, 187, 1), 80px 80px 0 0 rgba(187, 187, 187, 1),
144 | 120px 80px 0 0 rgba(187, 187, 187, 1),
145 | 180px 80px 0 0 rgba(187, 187, 187, 1),
146 | 280px 80px 0 0 rgba(187, 187, 187, 1),
147 | 80px 100px 0 0 rgba(187, 187, 187, 1),
148 | 120px 100px 0 0 rgba(187, 187, 187, 1),
149 | 180px 100px 0 0 rgba(187, 187, 187, 1),
150 | 280px 100px 0 0 rgba(187, 187, 187, 1),
151 | 80px 120px 0 0 rgba(187, 187, 187, 1),
152 | 140px 120px 0 0 rgba(187, 187, 187, 1),
153 | 160px 120px 0 0 rgba(187, 187, 187, 1);
154 | height: 20px;
155 | width: 20px;
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/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/store/actions/actionCreators.js:
--------------------------------------------------------------------------------
1 | import { ActionCreators } from 'redux-undo';
2 | import * as types from './actionTypes';
3 |
4 | export function setInitialState(options) {
5 | return {
6 | type: types.SET_INITIAL_STATE,
7 | options
8 | };
9 | }
10 |
11 | export function changeDimensions(gridProperty, increment) {
12 | return {
13 | type: types.CHANGE_DIMENSIONS,
14 | gridProperty,
15 | increment
16 | };
17 | }
18 |
19 | export function updateGridBoundaries(gridElement) {
20 | return {
21 | type: types.UPDATE_GRID_BOUNDARIES,
22 | gridElement
23 | };
24 | }
25 |
26 | export function selectPaletteColor(position) {
27 | return {
28 | type: types.SELECT_PALETTE_COLOR,
29 | position
30 | };
31 | }
32 |
33 | export function setCustomColor(customColor) {
34 | return {
35 | type: types.SET_CUSTOM_COLOR,
36 | customColor
37 | };
38 | }
39 |
40 | export function cellAction({
41 | id,
42 | drawingTool,
43 | color,
44 | paletteColor,
45 | columns,
46 | rows
47 | }) {
48 | return {
49 | type: `APPLY_${drawingTool}`,
50 | id,
51 | color,
52 | paletteColor,
53 | columns,
54 | rows
55 | };
56 | }
57 |
58 | export function moveDrawing({ xDiff, yDiff, cellWidth }) {
59 | return {
60 | type: 'MOVE_DRAWING',
61 | moveDiff: { xDiff, yDiff, cellWidth }
62 | };
63 | }
64 |
65 | export function setDrawing(
66 | frames,
67 | paletteGridData,
68 | cellSize,
69 | columns,
70 | rows,
71 | hoveredIndex
72 | ) {
73 | return {
74 | type: types.SET_DRAWING,
75 | frames,
76 | paletteGridData,
77 | cellSize,
78 | columns,
79 | rows,
80 | hoveredIndex
81 | };
82 | }
83 |
84 | export function endDrag() {
85 | return {
86 | type: types.END_DRAG
87 | };
88 | }
89 |
90 | export function switchTool(tool) {
91 | return {
92 | type: types.SWITCH_TOOL,
93 | tool
94 | };
95 | }
96 |
97 | export function setCellSize(cellSize) {
98 | return {
99 | type: types.SET_CELL_SIZE,
100 | cellSize
101 | };
102 | }
103 |
104 | export function resetGrid() {
105 | return {
106 | type: types.SET_RESET_GRID
107 | };
108 | }
109 |
110 | export function showSpinner() {
111 | return {
112 | type: types.SHOW_SPINNER
113 | };
114 | }
115 |
116 | export function hideSpinner() {
117 | return {
118 | type: types.HIDE_SPINNER
119 | };
120 | }
121 |
122 | export function sendNotification(message) {
123 | return {
124 | type: types.SEND_NOTIFICATION,
125 | message
126 | };
127 | }
128 |
129 | export function changeActiveFrame(frameIndex) {
130 | return {
131 | type: types.CHANGE_ACTIVE_FRAME,
132 | frameIndex
133 | };
134 | }
135 |
136 | export function reorderFrame(selectedIndex, destinationIndex) {
137 | return {
138 | type: types.REORDER_FRAME,
139 | selectedIndex,
140 | destinationIndex
141 | };
142 | }
143 |
144 | export function createNewFrame() {
145 | return {
146 | type: types.CREATE_NEW_FRAME
147 | };
148 | }
149 |
150 | export function deleteFrame(frameId) {
151 | return {
152 | type: types.DELETE_FRAME,
153 | frameId
154 | };
155 | }
156 |
157 | export function duplicateFrame(frameId) {
158 | return {
159 | type: types.DUPLICATE_FRAME,
160 | frameId
161 | };
162 | }
163 |
164 | export function setDuration(duration) {
165 | return {
166 | type: types.SET_DURATION,
167 | duration
168 | };
169 | }
170 |
171 | export function changeFrameInterval(frameIndex, interval) {
172 | return {
173 | type: types.CHANGE_FRAME_INTERVAL,
174 | frameIndex,
175 | interval
176 | };
177 | }
178 |
179 | export function newProject() {
180 | return {
181 | type: types.NEW_PROJECT
182 | };
183 | }
184 |
185 | export function changeHoveredCell(cell) {
186 | return {
187 | type: types.CHANGE_HOVERED_CELL,
188 | cell
189 | };
190 | }
191 |
192 | export function undo() {
193 | return ActionCreators.undo();
194 | }
195 |
196 | export function redo() {
197 | return ActionCreators.redo();
198 | }
199 |
--------------------------------------------------------------------------------
/src/store/actions/actionTypes.js:
--------------------------------------------------------------------------------
1 | import {
2 | PENCIL,
3 | ERASER,
4 | BUCKET,
5 | EYEDROPPER
6 | } from '../reducers/drawingToolStates';
7 |
8 | export const SET_INITIAL_STATE = 'SET_INITIAL_STATE';
9 | export const CHANGE_DIMENSIONS = 'CHANGE_DIMENSIONS';
10 | export const SET_GRID_DIMENSION = 'SET_GRID_DIMENSION';
11 | export const SELECT_PALETTE_COLOR = 'SELECT_PALETTE_COLOR';
12 | export const SET_CUSTOM_COLOR = 'SET_CUSTOM_COLOR';
13 | export const APPLY_PENCIL = `APPLY_${PENCIL}`;
14 | export const APPLY_ERASER = `APPLY_${ERASER}`;
15 | export const APPLY_BUCKET = `APPLY_${BUCKET}`;
16 | export const APPLY_EYEDROPPER = `APPLY_${EYEDROPPER}`;
17 | export const MOVE_DRAWING = 'MOVE_DRAWING';
18 | export const SET_DRAWING = 'SET_DRAWING';
19 | export const END_DRAG = 'END_DRAG';
20 | export const SWITCH_TOOL = 'SWITCH_TOOL';
21 | export const SET_CELL_SIZE = 'SET_CELL_SIZE';
22 | export const SET_RESET_GRID = 'SET_RESET_GRID';
23 | export const SHOW_SPINNER = 'SHOW_SPINNER';
24 | export const HIDE_SPINNER = 'HIDE_SPINNER';
25 | export const SEND_NOTIFICATION = 'SEND_NOTIFICATION';
26 | export const CHANGE_ACTIVE_FRAME = 'CHANGE_ACTIVE_FRAME';
27 | export const REORDER_FRAME = 'REORDER_FRAME';
28 | export const CREATE_NEW_FRAME = 'CREATE_NEW_FRAME';
29 | export const DELETE_FRAME = 'DELETE_FRAME';
30 | export const DUPLICATE_FRAME = 'DUPLICATE_FRAME';
31 | export const SET_DURATION = 'SET_DURATION';
32 | export const CHANGE_FRAME_INTERVAL = 'CHANGE_FRAME_INTERVAL';
33 | export const NEW_PROJECT = 'NEW_PROJECT';
34 | export const UPDATE_GRID_BOUNDARIES = 'UPDATE_GRID_BOUNDARIES';
35 | export const CHANGE_HOVERED_CELL = 'CHANGE_HOVERED_CELL';
36 |
--------------------------------------------------------------------------------
/src/store/configureStore.js:
--------------------------------------------------------------------------------
1 | import { createStore } from 'redux';
2 | import undoable, { includeAction } from 'redux-undo';
3 | import reducer from './reducers/reducer';
4 | import {
5 | CHANGE_DIMENSIONS,
6 | APPLY_PENCIL,
7 | APPLY_ERASER,
8 | APPLY_BUCKET,
9 | APPLY_EYEDROPPER,
10 | MOVE_DRAWING,
11 | SHOW_SPINNER,
12 | NEW_PROJECT,
13 | SET_DRAWING,
14 | SET_CELL_SIZE,
15 | SET_RESET_GRID
16 | } from './actions/actionTypes';
17 |
18 | const createIncludedActions = () =>
19 | includeAction([
20 | CHANGE_DIMENSIONS,
21 | APPLY_PENCIL,
22 | APPLY_ERASER,
23 | APPLY_BUCKET,
24 | APPLY_EYEDROPPER,
25 | MOVE_DRAWING,
26 | SET_DRAWING,
27 | SET_CELL_SIZE,
28 | SET_RESET_GRID,
29 | NEW_PROJECT
30 | ]);
31 |
32 | const configureStore = devMode => {
33 | const store = createStore(
34 | undoable(reducer, {
35 | filter: createIncludedActions(),
36 | debug: devMode,
37 | ignoreInitialState: true
38 | })
39 | );
40 |
41 | store.dispatch({
42 | type: SHOW_SPINNER
43 | });
44 |
45 | return store;
46 | };
47 |
48 | export default configureStore;
49 |
--------------------------------------------------------------------------------
/src/store/reducers/activeFrameReducer.js:
--------------------------------------------------------------------------------
1 | import * as types from '../actions/actionTypes';
2 |
3 | export const GRID_INITIAL_COLOR = 'rgba(49, 49, 49, 1)';
4 |
5 | const updateFrameProp = prop => propReducer => (frames, action) => {
6 | const activeIndex = frames.get('activeIndex');
7 | return frames.updateIn(['list', activeIndex, prop], stateProp =>
8 | propReducer(stateProp, action)
9 | );
10 | };
11 |
12 | const updateGrid = updateFrameProp('grid');
13 | const updateInterval = updateFrameProp('interval');
14 |
15 | const isSameColor = (colorA, colorB) =>
16 | (colorA || GRID_INITIAL_COLOR) === (colorB || GRID_INITIAL_COLOR);
17 |
18 | const getSameColorAdjacentCells = (frameGrid, columns, rows, id, color) => {
19 | const adjacentCollection = [];
20 | let auxId;
21 |
22 | if ((id + 1) % columns !== 0) {
23 | // Not at the very right
24 | auxId = id + 1;
25 | if (isSameColor(frameGrid.get(auxId), color)) {
26 | adjacentCollection.push(auxId);
27 | }
28 | }
29 | if (id % columns !== 0) {
30 | // Not at the very left
31 | auxId = id - 1;
32 | if (isSameColor(frameGrid.get(auxId), color)) {
33 | adjacentCollection.push(auxId);
34 | }
35 | }
36 | if (id >= columns) {
37 | // Not at the very top
38 | auxId = id - columns;
39 | if (isSameColor(frameGrid.get(auxId), color)) {
40 | adjacentCollection.push(auxId);
41 | }
42 | }
43 | if (id < columns * rows - columns) {
44 | // Not at the very bottom
45 | auxId = id + columns;
46 | if (isSameColor(frameGrid.get(auxId), color)) {
47 | adjacentCollection.push(auxId);
48 | }
49 | }
50 |
51 | return adjacentCollection;
52 | };
53 |
54 | const drawPixel = (pixelGrid, color, id) => pixelGrid.set(id, color);
55 |
56 | const applyBucketToGrid = (grid, { id, paletteColor, columns, rows }) => {
57 | const queue = [id];
58 | const cellColor = grid.get(id);
59 | let currentId;
60 | let newGrid = grid;
61 | let adjacents;
62 | let auxAdjacentId;
63 | let auxAdjacentColor;
64 |
65 | while (queue.length > 0) {
66 | currentId = queue.shift();
67 | newGrid = drawPixel(newGrid, paletteColor, currentId);
68 | adjacents = getSameColorAdjacentCells(
69 | newGrid,
70 | columns,
71 | rows,
72 | currentId,
73 | cellColor
74 | );
75 |
76 | for (let i = 0; i < adjacents.length; i++) {
77 | auxAdjacentId = adjacents[i];
78 | auxAdjacentColor = newGrid.get(auxAdjacentId);
79 | // Avoid introduce repeated or painted already cell into the queue
80 | if (
81 | queue.indexOf(auxAdjacentId) === -1 &&
82 | auxAdjacentColor !== paletteColor
83 | ) {
84 | queue.push(auxAdjacentId);
85 | }
86 | }
87 | }
88 |
89 | return newGrid;
90 | };
91 |
92 | const applyPencilToGrid = (pixelGrid, { paletteColor, id }) =>
93 | drawPixel(pixelGrid, paletteColor, id);
94 |
95 | const applyBucket = updateGrid(applyBucketToGrid);
96 |
97 | const shiftPixelsDown = (grid, columnCount) =>
98 | grid.withMutations(mutableGrid => {
99 | for (let i = 0; i < columnCount; i++) {
100 | const lastValue = mutableGrid.last();
101 | mutableGrid.pop().unshift(lastValue);
102 | }
103 | });
104 |
105 | const shiftPixelsUp = (grid, columnCount) =>
106 | grid.withMutations(mutableGrid => {
107 | for (let i = 0; i < columnCount; i++) {
108 | const firstValue = mutableGrid.first();
109 | mutableGrid.shift().push(firstValue);
110 | }
111 | });
112 |
113 | const getGridColumnIndexes = (columnId, columnCount, cellCount) => {
114 | let i = 0;
115 | const indexes = [];
116 | while (i < cellCount) {
117 | if (i % columnCount === columnId) {
118 | indexes.push(i);
119 | i += columnCount;
120 | } else {
121 | i += 1;
122 | }
123 | }
124 | return indexes;
125 | };
126 |
127 | const shiftPixelsLeft = (grid, columnCount) => {
128 | const indexArray = getGridColumnIndexes(0, columnCount, grid.size);
129 | let tempGrid = grid;
130 | for (const cellIndex of indexArray) {
131 | const valueToMove = tempGrid.get(cellIndex);
132 | const target = cellIndex + columnCount;
133 | tempGrid = tempGrid.insert(target, valueToMove);
134 | tempGrid = tempGrid.delete(cellIndex);
135 | }
136 | return tempGrid;
137 | };
138 |
139 | const shiftPixelsRight = (grid, columnCount) => {
140 | const indexArray = getGridColumnIndexes(
141 | columnCount - 1,
142 | columnCount,
143 | grid.size
144 | );
145 | let tempGrid = grid;
146 | for (const cellIndex of indexArray) {
147 | const valueToMove = tempGrid.get(cellIndex);
148 | const target = cellIndex - columnCount + 1;
149 | tempGrid = tempGrid.insert(target < 0 ? 1 : target, valueToMove);
150 | tempGrid = tempGrid.delete(cellIndex + 1);
151 | }
152 | return tempGrid;
153 | };
154 |
155 | const applyMove = (frames, action) => {
156 | const { xDiff, yDiff, cellWidth } = action.moveDiff;
157 | const x = xDiff / cellWidth;
158 | const y = yDiff / cellWidth;
159 | const xDirection = x < 0 ? 'LEFT' : 'RIGHT';
160 | const yDirection = y < 0 ? 'UP' : 'DOWN';
161 | const horizontal = Math.abs(x) > 1 ? xDirection : '';
162 | const vertical = Math.abs(y) > 1 ? yDirection : '';
163 | const activeIndex = frames.get('activeIndex');
164 | const currentFrame = frames
165 | .get('list')
166 | .get(activeIndex)
167 | .get('grid');
168 |
169 | const columnCount = frames.get('columns');
170 | let frameShifted = currentFrame;
171 |
172 | switch (horizontal) {
173 | case 'LEFT':
174 | frameShifted = shiftPixelsLeft(currentFrame, columnCount);
175 | break;
176 | case 'RIGHT':
177 | frameShifted = shiftPixelsRight(currentFrame, columnCount);
178 | break;
179 | default:
180 | }
181 |
182 | switch (vertical) {
183 | case 'UP':
184 | frameShifted = shiftPixelsUp(frameShifted, columnCount);
185 | break;
186 | case 'DOWN':
187 | frameShifted = shiftPixelsDown(frameShifted, columnCount);
188 | break;
189 | default:
190 | }
191 | return frames.setIn(['list', activeIndex, 'grid'], frameShifted);
192 | };
193 |
194 | const applyPencil = updateGrid(applyPencilToGrid);
195 |
196 | const applyEraser = updateGrid((pixelGrid, { id }) =>
197 | drawPixel(pixelGrid, '', id)
198 | );
199 |
200 | const resetGrid = updateGrid(pixelGrid => pixelGrid.map(() => ''));
201 |
202 | const changeFrameInterval = updateInterval(
203 | (previousInterval, { interval }) => interval
204 | );
205 |
206 | export default function(frames, action) {
207 | switch (action.type) {
208 | case types.APPLY_PENCIL:
209 | return applyPencil(frames, action);
210 | case types.APPLY_ERASER:
211 | return applyEraser(frames, action);
212 | case types.APPLY_BUCKET:
213 | return applyBucket(frames, action);
214 | case types.MOVE_DRAWING:
215 | return applyMove(frames, action);
216 | case types.SET_RESET_GRID:
217 | return resetGrid(frames);
218 | case types.CHANGE_FRAME_INTERVAL:
219 | return changeFrameInterval(frames, action);
220 | default:
221 | return frames;
222 | }
223 | }
224 |
--------------------------------------------------------------------------------
/src/store/reducers/drawingToolReducer.js:
--------------------------------------------------------------------------------
1 | import * as types from '../actions/actionTypes';
2 | import { PENCIL, ERASER, EYEDROPPER, MOVE } from './drawingToolStates';
3 |
4 | const switchTool = (drawingTool = PENCIL, tool) => {
5 | if (drawingTool === tool) {
6 | return PENCIL;
7 | }
8 | return tool;
9 | };
10 |
11 | const disableTool = (drawingTool, tool) => {
12 | if (drawingTool === tool) {
13 | return PENCIL;
14 | }
15 | return drawingTool;
16 | };
17 |
18 | export default function drawingToolReducer(drawingTool = PENCIL, action) {
19 | switch (action.type) {
20 | case types.SET_INITIAL_STATE:
21 | case types.NEW_PROJECT:
22 | case types.APPLY_EYEDROPPER:
23 | return PENCIL;
24 | case types.SELECT_PALETTE_COLOR:
25 | return [EYEDROPPER, ERASER, MOVE].reduce(disableTool, drawingTool);
26 | case types.SWITCH_TOOL:
27 | return switchTool(drawingTool, action.tool);
28 | default:
29 | return drawingTool;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/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/store/reducers/framesReducer.js:
--------------------------------------------------------------------------------
1 | import { List, Map, fromJS } from 'immutable';
2 | import shortid from 'shortid';
3 | import * as types from '../actions/actionTypes';
4 | import getTimeInterval from '../../utils/intervals';
5 |
6 | const createGrid = numCells => {
7 | let newGrid = List();
8 | // Set every cell with the initial color
9 | for (let i = 0; i < numCells; i++) {
10 | newGrid = newGrid.push('');
11 | }
12 | return newGrid;
13 | };
14 |
15 | const resizeGrid = (grid, gridProperty, increment, dimensions) => {
16 | const totalCells = dimensions.rows * dimensions.columns;
17 | let newGrid = grid;
18 |
19 | if (gridProperty === 'columns') {
20 | // Resize by columns
21 | if (increment > 0) {
22 | // Add a row at the end
23 | for (let i = totalCells; i > 0; i -= dimensions.columns) {
24 | newGrid = newGrid.insert(i, '');
25 | }
26 | } else {
27 | for (let i = totalCells; i > 0; i -= dimensions.columns) {
28 | newGrid = newGrid.splice(i - 1, 1);
29 | }
30 | }
31 | } else if (gridProperty === 'rows') {
32 | // Resize by rows
33 | if (increment > 0) {
34 | // Add a row at the end
35 | for (let i = 0; i < dimensions.columns; i++) {
36 | newGrid = newGrid.push('');
37 | }
38 | } else {
39 | // Remove the last row
40 | for (let i = 0; i < dimensions.columns; i++) {
41 | newGrid = newGrid.splice(-1, 1);
42 | }
43 | }
44 | }
45 |
46 | return newGrid;
47 | };
48 |
49 | const create = (cellsCount, intervalPercentage) =>
50 | Map({
51 | grid: createGrid(cellsCount),
52 | interval: intervalPercentage,
53 | key: shortid.generate()
54 | });
55 |
56 | const resetIntervals = frameList =>
57 | frameList.map((frame, index) =>
58 | Map({
59 | grid: frame.get('grid'),
60 | interval: getTimeInterval(index, frameList.size),
61 | key: frame.get('key')
62 | })
63 | );
64 | const getFrame = (frames, frameId) => {
65 | const frameList = frames.get('list');
66 | const frame = frameList.get(frameId);
67 | return Map({
68 | grid: frame.get('grid'),
69 | interval: frame.get('interval'),
70 | key: shortid.generate()
71 | });
72 | };
73 |
74 | const initFrames = (action = {}) => {
75 | const options = action.options || {};
76 | const columns = parseInt(options.columns, 10) || 20;
77 | const rows = parseInt(options.rows, 10) || 20;
78 | const list = resetIntervals(List([create(columns * rows)]));
79 | const hoveredIndex = undefined;
80 | return Map({
81 | list,
82 | columns,
83 | rows,
84 | activeIndex: 0,
85 | hoveredIndex
86 | });
87 | };
88 |
89 | const changeActiveFrame = (frames, action) => {
90 | const activeIndex = action.frameIndex;
91 | return frames.merge({ activeIndex });
92 | };
93 |
94 | const reorderFrame = (frames, action) => {
95 | const frameList = frames.get('list');
96 | const { selectedIndex, destinationIndex } = action;
97 | const targetIsBefore = selectedIndex < destinationIndex;
98 | const insertPosition = destinationIndex + (targetIsBefore ? 1 : 0);
99 | const deletePosition = selectedIndex + (targetIsBefore ? 0 : 1);
100 | const list = resetIntervals(
101 | frameList
102 | .splice(insertPosition, 0, getFrame(frames, selectedIndex))
103 | .splice(deletePosition, 1)
104 | );
105 |
106 | return frames.merge({
107 | list,
108 | activeIndex: destinationIndex
109 | });
110 | };
111 |
112 | const createNewFrame = frames => {
113 | const frameList = frames.get('list');
114 | const list = resetIntervals(
115 | frameList.push(create(frameList.getIn([0, 'grid']).size, 100))
116 | );
117 | return frames.merge({
118 | list,
119 | activeIndex: frameList.size
120 | });
121 | };
122 |
123 | const deleteFrame = (frames, action) => {
124 | const { frameId } = action;
125 | const frameList = frames.get('list');
126 | if (frameList.size <= 1) {
127 | return frames;
128 | }
129 | const activeIndex = frames.get('activeIndex');
130 | const reduceFrameIndex = activeIndex >= frameId && activeIndex > 0;
131 | return frames.merge(
132 | {
133 | list: resetIntervals(frameList.splice(frameId, 1))
134 | },
135 | reduceFrameIndex ? { activeIndex: frameList.size - 2 } : {}
136 | );
137 | };
138 |
139 | const duplicateFrame = (frames, action) => {
140 | const { frameId } = action;
141 | const frameList = frames.get('list');
142 | const list = resetIntervals(
143 | frameList.splice(frameId, 0, getFrame(frames, frameId))
144 | );
145 | return frames.merge({
146 | list,
147 | activeIndex: frameId + 1
148 | });
149 | };
150 |
151 | const changeDimensions = (frames, { gridProperty, increment }) => {
152 | const dimensions = {
153 | columns: frames.get('columns'),
154 | rows: frames.get('rows')
155 | };
156 | const list = frames.get('list').map(frame =>
157 | Map({
158 | grid: resizeGrid(frame.get('grid'), gridProperty, increment, dimensions),
159 | interval: frame.get('interval'),
160 | key: frame.get('key')
161 | })
162 | );
163 | return frames.merge({
164 | list,
165 | [gridProperty]: frames.get(gridProperty) + increment
166 | });
167 | };
168 |
169 | const setFrames = (frames, action) => {
170 | const { columns, rows, hoveredIndex } = action;
171 | const frameList = action.frames;
172 | return fromJS({
173 | list: frameList,
174 | columns,
175 | rows,
176 | activeIndex: 0,
177 | hoveredIndex
178 | });
179 | };
180 |
181 | const changeHoveredCell = (frames, cell) =>
182 | frames.merge({ hoveredIndex: cell });
183 |
184 | export default function(frames = initFrames(), action) {
185 | switch (action.type) {
186 | case types.SET_INITIAL_STATE:
187 | case types.NEW_PROJECT:
188 | return initFrames(action);
189 | case types.SET_DRAWING:
190 | return setFrames(frames, action);
191 | case types.CHANGE_ACTIVE_FRAME:
192 | return changeActiveFrame(frames, action);
193 | case types.REORDER_FRAME:
194 | return reorderFrame(frames, action);
195 | case types.CREATE_NEW_FRAME:
196 | return createNewFrame(frames);
197 | case types.DELETE_FRAME:
198 | return deleteFrame(frames, action);
199 | case types.DUPLICATE_FRAME:
200 | return duplicateFrame(frames, action);
201 | case types.CHANGE_DIMENSIONS:
202 | return changeDimensions(frames, action);
203 | case types.CHANGE_HOVERED_CELL:
204 | return changeHoveredCell(frames, action.cell);
205 | default:
206 | return frames;
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/src/store/reducers/paletteReducer.js:
--------------------------------------------------------------------------------
1 | import { List, Map, fromJS } from 'immutable';
2 | import shortid from 'shortid';
3 | import * as types from '../actions/actionTypes';
4 | import { GRID_INITIAL_COLOR } from './activeFrameReducer';
5 |
6 | const getPositionFirstMatchInPalette = (grid, color) =>
7 | grid.findIndex(gridColor => gridColor.get('color') === color);
8 |
9 | const isColorInPalette = (grid, color) =>
10 | getPositionFirstMatchInPalette(grid, color) !== -1;
11 |
12 | const parseColorToString = colorData =>
13 | typeof colorData === 'string'
14 | ? colorData
15 | : `rgba(${colorData.r},${colorData.g},${colorData.b},${colorData.a})`;
16 |
17 | const disableColor = (palette, action) => {
18 | if (action.tool === 'ERASER' || action.tool === 'MOVE') {
19 | return palette.set('position', -1);
20 | }
21 | return palette;
22 | };
23 |
24 | const addColorToLastGridCell = (palette, newColor) => {
25 | const grid = palette.get('grid');
26 | const lastPosition = grid.size - 1;
27 | return palette.merge({
28 | grid: grid.setIn([lastPosition, 'color'], parseColorToString(newColor)),
29 | position: lastPosition
30 | });
31 | };
32 |
33 | const createPaletteGrid = () =>
34 | List([
35 | 'rgba(0, 0, 0, 1)',
36 | 'rgba(255, 0, 0, 1)',
37 | 'rgba(233, 30, 99, 1)',
38 | 'rgba(156, 39, 176, 1)',
39 | 'rgba(103, 58, 183, 1)',
40 | 'rgba(63, 81, 181, 1)',
41 | 'rgba(33, 150, 243, 1)',
42 | 'rgba(3, 169, 244, 1)',
43 | 'rgba(0, 188, 212, 1)',
44 | 'rgba(0, 150, 136, 1)',
45 | 'rgba(76, 175, 80, 1)',
46 | 'rgba(139, 195, 74, 1)',
47 | 'rgba(205, 220, 57, 1)',
48 | 'rgba(158, 224, 122, 1)',
49 | 'rgba(255, 235, 59, 1)',
50 | 'rgba(255, 193, 7, 1)',
51 | 'rgba(255, 152, 0, 1)',
52 | 'rgba(255, 205, 210, 1)',
53 | 'rgba(255, 87, 34, 1)',
54 | 'rgba(121, 85, 72, 1)',
55 | 'rgba(158, 158, 158, 1)',
56 | 'rgba(96, 125, 139, 1)',
57 | 'rgba(48, 63, 70, 1)',
58 | 'rgba(255, 255, 255, 1)',
59 | 'rgba(56, 53, 53, 1)',
60 | 'rgba(56, 53, 53, 1)',
61 | 'rgba(56, 53, 53, 1)',
62 | 'rgba(56, 53, 53, 1)',
63 | 'rgba(56, 53, 53, 1)',
64 | 'rgba(56, 53, 53, 1)'
65 | ]).map(color => Map({ color, id: shortid.generate() }));
66 |
67 | const isColorSelected = palette => palette.get('position') !== -1;
68 |
69 | const resetSelectedColorState = palette => palette.set('position', 0);
70 |
71 | const createPalette = () =>
72 | Map({
73 | grid: createPaletteGrid(),
74 | position: 0
75 | });
76 |
77 | const getCellColor = ({ color }) => color || GRID_INITIAL_COLOR;
78 |
79 | const eyedropColor = (palette, action) => {
80 | const cellColor = getCellColor(action);
81 | const grid = palette.get('grid');
82 |
83 | if (!isColorInPalette(grid, cellColor)) {
84 | return addColorToLastGridCell(palette, cellColor);
85 | }
86 | return palette.set(
87 | 'position',
88 | getPositionFirstMatchInPalette(grid, cellColor)
89 | );
90 | };
91 |
92 | const preparePalette = palette => {
93 | if (!isColorSelected(palette)) {
94 | return resetSelectedColorState(palette);
95 | }
96 | return palette;
97 | };
98 |
99 | const selectPaletteColor = (palette, action) =>
100 | palette.set('position', action.position);
101 |
102 | const setCustomColor = (palette, { customColor }) => {
103 | if (!isColorSelected(palette)) {
104 | return addColorToLastGridCell(palette, customColor);
105 | }
106 | const customColorRgba = parseColorToString(customColor);
107 | return palette.setIn(
108 | ['grid', palette.get('position'), 'color'],
109 | customColorRgba
110 | );
111 | };
112 |
113 | const setPalette = (palette, action) => {
114 | const defaultPalette = action.paletteGridData.length === 0;
115 | return palette.set(
116 | 'grid',
117 | fromJS(defaultPalette ? createPaletteGrid() : action.paletteGridData)
118 | );
119 | };
120 |
121 | export default function paletteReducer(palette = createPalette(), action) {
122 | switch (action.type) {
123 | case types.SET_INITIAL_STATE:
124 | case types.NEW_PROJECT:
125 | return createPalette();
126 | case types.APPLY_EYEDROPPER:
127 | return eyedropColor(palette, action);
128 | case types.APPLY_PENCIL:
129 | case types.APPLY_BUCKET:
130 | return preparePalette(palette);
131 | case types.SELECT_PALETTE_COLOR:
132 | return selectPaletteColor(palette, action);
133 | case types.SET_CUSTOM_COLOR:
134 | return setCustomColor(palette, action);
135 | case types.SWITCH_TOOL:
136 | return disableColor(palette, action);
137 | case types.SET_DRAWING:
138 | return setPalette(palette, action);
139 | default:
140 | return palette;
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/src/store/reducers/reducer.js:
--------------------------------------------------------------------------------
1 | import { List, Map } from 'immutable';
2 | import paletteReducer from './paletteReducer';
3 | import framesReducer from './framesReducer';
4 | import activeFrameReducer from './activeFrameReducer';
5 | import drawingToolReducer from './drawingToolReducer';
6 | import * as types from '../actions/actionTypes';
7 |
8 | function setInitialState(state) {
9 | const cellSize = 10;
10 |
11 | const initialState = {
12 | cellSize,
13 | loading: false,
14 | notifications: List(),
15 | duration: 1
16 | };
17 |
18 | return state.merge(initialState);
19 | }
20 |
21 | function setDrawing(state, action) {
22 | return state.set('cellSize', action.cellSize);
23 | }
24 |
25 | function setCellSize(state, cellSize) {
26 | return state.merge({ cellSize });
27 | }
28 |
29 | function showSpinner(state) {
30 | return state.merge({ loading: true });
31 | }
32 |
33 | function hideSpinner(state) {
34 | return state.merge({ loading: false });
35 | }
36 |
37 | function sendNotification(state, message) {
38 | return state.merge({
39 | notifications: message === '' ? List() : List([{ message, id: 0 }])
40 | });
41 | }
42 |
43 | function setDuration(state, duration) {
44 | return state.merge({ duration });
45 | }
46 |
47 | function updateGridBoundaries(state, action) {
48 | const { x, y, width, height } = action.gridElement.getBoundingClientRect();
49 | return state.set('gridBoundaries', {
50 | x,
51 | y,
52 | width,
53 | height
54 | });
55 | }
56 |
57 | function generateDefaultState() {
58 | return setInitialState(Map(), { type: types.SET_INITIAL_STATE, state: {} });
59 | }
60 |
61 | const pipeReducers = reducers => (initialState, action) =>
62 | reducers.reduce((state, reducer) => reducer(state, action), initialState);
63 |
64 | function partialReducer(state, action) {
65 | switch (action.type) {
66 | case types.SET_INITIAL_STATE:
67 | return setInitialState(state);
68 | case types.SET_DRAWING:
69 | return setDrawing(state, action);
70 | case types.SET_CELL_SIZE:
71 | return setCellSize(state, action.cellSize);
72 | case types.SHOW_SPINNER:
73 | return showSpinner(state);
74 | case types.HIDE_SPINNER:
75 | return hideSpinner(state);
76 | case types.SEND_NOTIFICATION:
77 | return sendNotification(state, action.message);
78 | case types.SET_DURATION:
79 | return setDuration(state, action.duration);
80 | case types.NEW_PROJECT:
81 | return setInitialState(state);
82 | case types.UPDATE_GRID_BOUNDARIES:
83 | return updateGridBoundaries(state, action);
84 | default:
85 | }
86 | return state;
87 | }
88 |
89 | export default function(state = generateDefaultState(), action) {
90 | return partialReducer(state, action).merge({
91 | drawingTool: drawingToolReducer(state.get('drawingTool'), action),
92 | palette: paletteReducer(state.get('palette'), action),
93 | frames: pipeReducers([framesReducer, activeFrameReducer])(
94 | state.get('frames'),
95 | action
96 | )
97 | });
98 | }
99 |
--------------------------------------------------------------------------------
/src/utils/ImageToCanvas.js:
--------------------------------------------------------------------------------
1 | const drawFileImageToCanvas = (file, canvas, imageLoadedCallback) => {
2 | if (file && file.type.match('image.*')) {
3 | const reader = new FileReader();
4 | const context = canvas.getContext('2d');
5 | const img = new Image();
6 | img.crossOrigin = 'anonymous';
7 | img.style.display = 'none';
8 | img.onload = function() {
9 | context.canvas.width = img.width;
10 | context.canvas.height = img.height;
11 | context.drawImage(img, 0, 0);
12 | imageLoadedCallback({ w: img.width, h: img.height });
13 | };
14 | reader.readAsDataURL(file);
15 | reader.onload = function(evt) {
16 | if (evt.target.readyState === FileReader.DONE) {
17 | img.src = evt.target.result;
18 | }
19 | };
20 | return {};
21 | }
22 | return { errorType: 'notImage' };
23 | };
24 |
25 | export default drawFileImageToCanvas;
26 |
--------------------------------------------------------------------------------
/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/utils/canvasGIF.js:
--------------------------------------------------------------------------------
1 | import GIFEncoder from 'gif-encoder';
2 | import blobStream from 'blob-stream';
3 | import { saveAs } from 'file-saver';
4 | import randomString from './random';
5 |
6 | function fillCanvasWithFrame(canvas, frameInfo) {
7 | const { frame, cols, cellSize, frameHeight, frameIdx } = frameInfo;
8 | const ctx = canvas;
9 | frame.get('grid').forEach((fillStyle, pixelIdx) => {
10 | if (!fillStyle) {
11 | return;
12 | }
13 | ctx.fillStyle = fillStyle;
14 |
15 | const col = pixelIdx % cols;
16 | const row = Math.floor(pixelIdx / cols);
17 | ctx.fillRect(
18 | col * cellSize,
19 | row * cellSize + frameHeight * frameIdx,
20 | cellSize,
21 | cellSize
22 | );
23 | });
24 | return ctx;
25 | }
26 |
27 | function renderImageToCanvas(type, canvasInfo, currentFrameInfo, frames) {
28 | const { canvas, canvasHeight, canvasWidth } = canvasInfo;
29 | const { frame, frameHeight, frameWidth, cellSize } = currentFrameInfo;
30 | const cols = Math.floor(frameWidth / cellSize);
31 | let ctx = canvas.getContext('2d');
32 | ctx.canvas.width = canvasWidth;
33 | ctx.canvas.height = canvasHeight;
34 | switch (type) {
35 | case 'spritesheet':
36 | frames.forEach((currentFrame, frameIdx) => {
37 | ctx = fillCanvasWithFrame(ctx, {
38 | frame: currentFrame,
39 | cols,
40 | cellSize,
41 | frameHeight,
42 | frameIdx
43 | });
44 | });
45 | break;
46 | default:
47 | ctx = fillCanvasWithFrame(ctx, {
48 | frame,
49 | cols,
50 | cellSize,
51 | frameHeight,
52 | frameIdx: 0
53 | });
54 | break;
55 | }
56 | return ctx.getImageData(0, 0, canvasWidth, canvasHeight).data;
57 | }
58 |
59 | const saveCanvasToDisk = (blob, fileExtension) => {
60 | saveAs(blob, `${randomString()}.${fileExtension}`);
61 | };
62 |
63 | function renderFrames(settings) {
64 | const {
65 | type,
66 | frames,
67 | duration,
68 | activeFrame,
69 | rows,
70 | columns,
71 | cellSize
72 | } = settings;
73 |
74 | const durationInMillisecond = duration * 1000;
75 | const frameWidth = columns * cellSize;
76 | const frameHeight = rows * cellSize;
77 | const canvasWidth = frameWidth;
78 | const canvasHeight =
79 | type === 'spritesheet' ? frameHeight * frames.size : frameHeight;
80 |
81 | const canvas = document.createElement('canvas');
82 | const gif = new GIFEncoder(canvasWidth, canvasHeight);
83 | gif.pipe(blobStream()).on('finish', function() {
84 | saveCanvasToDisk(this.toBlob(), 'gif');
85 | });
86 |
87 | gif.setRepeat(0); // loop indefinitely
88 | gif.setDispose(3); // restore to previous
89 | gif.writeHeader();
90 |
91 | switch (type) {
92 | case 'single':
93 | case 'spritesheet':
94 | renderImageToCanvas(
95 | type,
96 | {
97 | canvas,
98 | canvasHeight,
99 | canvasWidth
100 | },
101 | {
102 | frame: activeFrame,
103 | frameHeight,
104 | frameWidth,
105 | cellSize
106 | },
107 | frames
108 | );
109 | canvas.toBlob(function(blob) {
110 | saveCanvasToDisk(blob, 'png');
111 | });
112 | break;
113 | default: {
114 | let previousInterval = 0;
115 | frames.forEach((frame, idx, framesArray) => {
116 | const isLastFrame = idx === framesArray.length - 1;
117 | const currentInterval = isLastFrame
118 | ? 100
119 | : frames.get(idx).get('interval');
120 | const diff = currentInterval - previousInterval;
121 | const delay = diff * 0.01 * durationInMillisecond;
122 |
123 | gif.setDelay(delay);
124 | previousInterval = currentInterval;
125 |
126 | gif.addFrame(
127 | renderImageToCanvas(
128 | type,
129 | {
130 | canvas,
131 | canvasHeight,
132 | canvasWidth
133 | },
134 | {
135 | frame,
136 | frameHeight,
137 | frameWidth,
138 | cellSize
139 | }
140 | )
141 | );
142 | });
143 | gif.finish();
144 | }
145 | }
146 | }
147 |
148 | export default renderFrames;
149 |
--------------------------------------------------------------------------------
/src/utils/color.js:
--------------------------------------------------------------------------------
1 | /*
2 | * getRgbaValues
3 | * @param {string} The pixel color in the following format: rgba(0,0,0,1)
4 | * @return {object} An object with r, g, b, a properties with its int correspondent value
5 | */
6 | const getRgbaValues = color => {
7 | const match = color.match(
8 | /rgba?\((\d{1,3}), ?(\d{1,3}), ?(\d{1,3})\)?(?:, ?(\d(?:\.\d*)?)\))?/
9 | );
10 | return match ? { r: match[1], g: match[2], b: match[3], a: match[4] } : {};
11 | };
12 |
13 | /*
14 | * getRgbHexValues
15 | * @param {string} The pixel color in the following format: #000000
16 | * @return {object} An object with r, g, b properties with its hex correspondent value
17 | */
18 | const getRgbHexValues = color => {
19 | const match = color.match(/.{1,2}/g);
20 | return match ? { r: match[0], g: match[1], b: match[2] } : {};
21 | };
22 |
23 | /*
24 | * isRgba
25 | * @param {string}
26 | * @return {boolean} True if the string contains 'rgba'
27 | */
28 | const isRgba = color => color.includes('rgba');
29 |
30 | /*
31 | * padHexValue
32 | * @param {number}
33 | * @return {string} Add a 0 before the number if this only had a digit
34 | */
35 | const padHexValue = value => (value.length === 1 ? `0${value}` : value);
36 |
37 | /*
38 | * normalizeHexValue
39 | * @param {number} Integer number
40 | * @return {string} Return the hex value with 2 digits
41 | */
42 | const normalizeHexValue = value =>
43 | padHexValue(parseInt(value, 10).toString(16));
44 |
45 | /*
46 | * parseRgbaToHex
47 | * @param {string} The color value in rgba: rgba(0,0,0,1)
48 | * @return {string} Returns the hex value with 6 digits, dropping the opacity
49 | */
50 | const parseRgbaToHex = colorCode => {
51 | const rgbaValues = getRgbaValues(colorCode);
52 | return `${normalizeHexValue(rgbaValues.r)}${normalizeHexValue(
53 | rgbaValues.g
54 | )}${normalizeHexValue(rgbaValues.b)}`;
55 | };
56 |
57 | /*
58 | * parseHexToRgba
59 | * @param {string} The color value in hex: 000000
60 | * @param {number} The color's opacity: int value from 0 to 1
61 | * @return {string} Returns the rbga value like rgba(0,0,0,1) always with the opacity set to 1
62 | */
63 | const parseHexToRgba = (colorCode, opacity) => {
64 | const hexValues = getRgbHexValues(colorCode);
65 | return `rgba(${parseInt(hexValues.r, 16)},${parseInt(
66 | hexValues.g,
67 | 16
68 | )},${parseInt(hexValues.b, 16)},${opacity})`;
69 | };
70 |
71 | /*
72 | * normalizeColor
73 | * @param {string} The color value, it could be in the following formats: '', rgba(0,0,0,1) or #000000
74 | * @return {string} Returns just the hex value with 6 digits
75 | */
76 | const normalizeColor = colorCode => {
77 | const defaultValue = '000000';
78 | const normalized = {
79 | color:
80 | typeof colorCode === 'string' && colorCode ? colorCode : defaultValue,
81 | opacity: 1
82 | };
83 | if (isRgba(normalized.color)) {
84 | const rgbaValues = getRgbaValues(normalized.color);
85 | normalized.color = parseRgbaToHex(normalized.color);
86 | normalized.opacity = rgbaValues.a;
87 | }
88 | if (normalized.color !== defaultValue) {
89 | normalized.color = normalized.color.replace('#', '');
90 | }
91 | return normalized;
92 | };
93 |
94 | /*
95 | * formatPixelColorOutput
96 | * @param {string} The pixel color
97 | * @param {number} formatId There are 3 format types
98 | * 0: #000000
99 | * 1: 0x000000
100 | * 2: rgba(0,0,0,1)
101 | * @return {string} The pixel color formatted
102 | */
103 | const formatPixelColorOutput = (color, formatId) => {
104 | const colorFormatted = normalizeColor(color);
105 |
106 | switch (formatId) {
107 | case 0:
108 | case 1:
109 | return `${formatId === 0 ? '#' : '0x'}${colorFormatted.color}`;
110 | default:
111 | return parseHexToRgba(colorFormatted.color, colorFormatted.opacity);
112 | }
113 | };
114 |
115 | export default formatPixelColorOutput;
116 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/utils/drawHandlersProvider.js:
--------------------------------------------------------------------------------
1 | const fromPositionToId = (posX, posY, grid, columns) => {
2 | const id = posX + columns * posY;
3 | return id < grid.size && posX >= 0 && posX < columns && posY >= 0 ? id : null;
4 | };
5 |
6 | const fromEventToId = (ev, props) => {
7 | const [{ radiusX, radiusY, clientX, clientY }] = ev.targetTouches;
8 | const {
9 | columns,
10 | grid,
11 | gridBoundaries: { x, y, width, height }
12 | } = props;
13 | const posX = Math.round(((clientX - x - radiusX) * columns) / width);
14 | const posY = Math.round(((clientY - y - radiusY) * columns) / height);
15 | return fromPositionToId(posX, posY, grid, columns);
16 | };
17 |
18 | const getCellActionProps = (props, id) => ({
19 | color: props.grid.get(id),
20 | id,
21 | ...props
22 | });
23 |
24 | const getCellCoordinates = (id, columnsCount) => {
25 | const y = Math.trunc(Math.abs(id / columnsCount));
26 | const x = id - columnsCount * y;
27 | return { x: x + 1, y: y + 1 };
28 | };
29 |
30 | const drawHandlersProvider = rootComponent => ({
31 | onMouseUp() {
32 | rootComponent.setState({
33 | dragging: false
34 | });
35 | },
36 | drawHandlersFactory(gridComponent) {
37 | return {
38 | onMouseDown(id, ev) {
39 | ev.preventDefault();
40 | const { props } = gridComponent;
41 | if (props.drawingTool !== 'MOVE') {
42 | const actionProps = getCellActionProps(props, id);
43 | if (!rootComponent.state.dragging) props.cellAction(actionProps);
44 | rootComponent.setState({
45 | dragging: true
46 | });
47 | }
48 | },
49 | onMouseOver(id, ev) {
50 | ev.preventDefault();
51 | const { props } = gridComponent;
52 | props.hoveredCell(getCellCoordinates(id, props.columns));
53 | if (props.drawingTool !== 'MOVE') {
54 | const actionProps = getCellActionProps(props, id);
55 | if (rootComponent.state.dragging) props.cellAction(actionProps);
56 | }
57 | },
58 | onTouchMove(ev) {
59 | ev.preventDefault();
60 | const { props } = gridComponent;
61 | if (props.drawingTool !== 'MOVE') {
62 | const id = fromEventToId(ev, props);
63 | const actionProps = getCellActionProps(props, id);
64 | if (id !== null && rootComponent.state.dragging) {
65 | props.cellAction(actionProps);
66 | }
67 | }
68 | },
69 | onMoveTouchMove(ev) {
70 | ev.preventDefault();
71 | const { props } = gridComponent;
72 | if (props.drawingTool === 'MOVE') {
73 | const { draggingCoord } = rootComponent.state;
74 | const { dragging } = rootComponent.state;
75 | const touch = ev.touches[0];
76 | const { pageX, pageY } = touch;
77 | const xDiff = draggingCoord ? pageX - draggingCoord.clientX : 0;
78 | const yDiff = draggingCoord ? pageY - draggingCoord.clientY : 0;
79 | const cellWidth = ev.target.clientWidth;
80 | if (
81 | dragging &&
82 | (Math.abs(xDiff) > cellWidth || Math.abs(yDiff) > cellWidth)
83 | ) {
84 | rootComponent.setState({
85 | draggingCoord: { clientX: pageX, clientY: pageY }
86 | });
87 | props.applyMove({ xDiff, yDiff, cellWidth });
88 | }
89 | }
90 | },
91 | onMoveMouseOver(ev) {
92 | ev.preventDefault();
93 | const { props } = gridComponent;
94 | if (props.drawingTool === 'MOVE') {
95 | const { draggingCoord } = rootComponent.state;
96 | const { dragging } = rootComponent.state;
97 | const { clientX, clientY } = ev;
98 | const xDiff = draggingCoord ? clientX - draggingCoord.clientX : 0;
99 | const yDiff = draggingCoord ? clientY - draggingCoord.clientY : 0;
100 | const cellWidth = ev.target.clientWidth;
101 | if (
102 | dragging &&
103 | (Math.abs(xDiff) > cellWidth || Math.abs(yDiff) > cellWidth)
104 | ) {
105 | rootComponent.setState({
106 | draggingCoord: { clientX, clientY }
107 | });
108 | props.applyMove({ xDiff, yDiff, cellWidth });
109 | }
110 | }
111 | },
112 | onMoveMouseDown(ev) {
113 | ev.preventDefault();
114 | const { props } = gridComponent;
115 | if (props.drawingTool === 'MOVE') {
116 | const { clientX, clientY } = ev;
117 | rootComponent.setState({
118 | dragging: true,
119 | draggingCoord: { clientX, clientY }
120 | });
121 | }
122 | },
123 | onMoveTouchStart(ev) {
124 | ev.preventDefault();
125 | const { props } = gridComponent;
126 | if (props.drawingTool === 'MOVE') {
127 | const touch = ev.touches[0];
128 | const { pageX, pageY } = touch;
129 | rootComponent.setState({
130 | dragging: true,
131 | draggingCoord: { clientX: pageX, clientY: pageY }
132 | });
133 | }
134 | }
135 | };
136 | }
137 | });
138 |
139 | export default drawHandlersProvider;
140 |
--------------------------------------------------------------------------------
/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/utils/loadFromCanvas.js:
--------------------------------------------------------------------------------
1 | import { fromJS } from 'immutable';
2 | import shortid from 'shortid';
3 | import getTimeInterval from './intervals';
4 |
5 | export const getDimensionIntervals = (dimension, numberOfFrames) => {
6 | const dimensionPerFrame = Math.floor(dimension / numberOfFrames);
7 | const intervals = [];
8 | let start = 0;
9 | let end = dimensionPerFrame;
10 | for (let i = 0; i < numberOfFrames; i++) {
11 | intervals.push({
12 | start,
13 | end,
14 | timePercentage: getTimeInterval(i, numberOfFrames)
15 | });
16 | start += dimensionPerFrame;
17 | end += dimensionPerFrame;
18 | }
19 | return intervals;
20 | };
21 |
22 | const generateFrames = (imageContext, numberOfFrames, pixSize = 1) => {
23 | const { width, height } = imageContext.canvas;
24 | const heightIntervals = getDimensionIntervals(height, numberOfFrames);
25 | const frameCollection = [];
26 |
27 | heightIntervals.forEach(heightInterval => {
28 | const pixelWidth = pixSize;
29 | const pixelHeight = pixSize;
30 |
31 | const grid = [];
32 | for (
33 | let y = heightInterval.start;
34 | y + pixelHeight <= heightInterval.end;
35 | y += pixelWidth
36 | ) {
37 | for (let x = 0; x + pixelWidth <= width; x += pixelWidth) {
38 | const currentPixel = imageContext.getImageData(
39 | x,
40 | y,
41 | pixelWidth,
42 | pixelHeight
43 | ).data;
44 | grid.push(
45 | `rgba(${currentPixel[0]},${currentPixel[1]},${currentPixel[2]},${currentPixel[3]})`
46 | );
47 | }
48 | }
49 |
50 | frameCollection.push({
51 | grid,
52 | interval: heightInterval.timePercentage,
53 | key: shortid.generate()
54 | });
55 | });
56 |
57 | console.log(frameCollection);
58 |
59 | return fromJS(frameCollection);
60 | };
61 |
62 | export const getCanvasDimensions = canvasRef => {
63 | if (canvasRef && canvasRef.current) {
64 | const canvas = canvasRef.current;
65 | const context = canvas.getContext('2d');
66 | return { w: context.canvas.width, h: context.canvas.height };
67 | }
68 | return { w: 0, h: 0 };
69 | };
70 |
71 | export default generateFrames;
72 |
--------------------------------------------------------------------------------
/src/utils/outputParse.js:
--------------------------------------------------------------------------------
1 | import formatPixelColorOutput from './color';
2 |
3 | /*
4 | * arrayChunks
5 | * @param {array} An array
6 | * @param {number} The number of elements we want in our chunk
7 | * @return {array} An array of arrays chunks
8 | */
9 | const arrayChunks = (array, chunkSize) =>
10 | Array(Math.ceil(array.length / chunkSize))
11 | .fill()
12 | .map((_, index) => index * chunkSize)
13 | .map(begin => array.slice(begin, begin + chunkSize));
14 |
15 | /*
16 | * formatFrameOutput
17 | * @param {array} The frame, an array of color values
18 | * @param {number} The columns count
19 | * @param {object} It contains different options to format the output
20 | * @return {string} The formatted output of the passed frame
21 | */
22 | const formatFrameOutput = (frame, columns, options) => {
23 | const isEven = number => number % 2 === 0;
24 | const flattened = arr => [].concat(...arr);
25 | let frameRows = arrayChunks(frame, columns);
26 | frameRows = frameRows.map((row, index) => {
27 | if (
28 | (isEven(index + 1) && options.reverseEven) ||
29 | (!isEven(index + 1) && options.reverseOdd)
30 | ) {
31 | return row.reverse();
32 | }
33 | return row;
34 | });
35 | const frameFormatted = flattened(frameRows);
36 |
37 | const lastPixelPos = frameFormatted.length;
38 | return frameFormatted.reduce((acc, pixel, index) => {
39 | const pixelFormatted = formatPixelColorOutput(pixel, options.colorFormat);
40 | return `${acc} ${pixelFormatted}${index + 1 === lastPixelPos ? '' : ','}${
41 | (index + 1) % columns ? '' : '\n'
42 | }`;
43 | }, '');
44 | };
45 |
46 | /*
47 | * generateFramesOutput
48 | * @param {object} It contains all frames data, the columns count and different options to format the output
49 | * @return {string} The formatted output of all the frames
50 | */
51 | const generateFramesOutput = ({ frames, columns, options }) =>
52 | frames
53 | .toJS()
54 | .reduce(
55 | (acc, frame, index) =>
56 | `${acc}${index ? '\n' : ''}frame${index} = {\n${formatFrameOutput(
57 | frame.grid,
58 | columns,
59 | options
60 | )}};`,
61 | ''
62 | );
63 |
64 | export default generateFramesOutput;
65 |
--------------------------------------------------------------------------------
/src/utils/polyfills.js:
--------------------------------------------------------------------------------
1 | import 'canvas-toBlob';
2 | import 'whatwg-fetch';
3 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/storage.js:
--------------------------------------------------------------------------------
1 | import { exampleCat } from '../../examples/import-export/json-cat';
2 |
3 | const STORAGE_KEY = 'pixelart-react-v3-0-0';
4 |
5 | /*
6 | * Storage data structure
7 | *
8 | * {
9 | * stored: [
10 | * { frames: [],paletteGridData, cellSize, columns, rows, animate},
11 | * { frames: [],paletteGridData, cellSize, columns, rows, animate},
12 | * ...
13 | * ]
14 | * current: position
15 | * }
16 | *
17 | */
18 |
19 | function saveDataToStorage(storage, data) {
20 | try {
21 | storage.setItem(STORAGE_KEY, JSON.stringify(data));
22 | return true;
23 | } catch (e) {
24 | return false; // There was an error
25 | }
26 | }
27 |
28 | /*
29 | Storage initialization
30 | */
31 | export function initStorage(storage) {
32 | storage.setItem(
33 | STORAGE_KEY,
34 | JSON.stringify({
35 | stored: [exampleCat], // Load an example project data by default
36 | current: 0
37 | })
38 | );
39 | }
40 |
41 | /*
42 | Get stored data from the storage
43 | */
44 | export function getDataFromStorage(storage) {
45 | try {
46 | const data = storage.getItem(STORAGE_KEY);
47 | return data ? JSON.parse(data) : false;
48 | } catch (e) {
49 | return false; // There was an error
50 | }
51 | }
52 |
53 | /*
54 | Save a project into the stored data collection
55 | */
56 | export function saveProjectToStorage(storage, projectData) {
57 | try {
58 | let dataStored = getDataFromStorage(storage);
59 | if (dataStored) {
60 | dataStored.stored.push(projectData);
61 | dataStored.current = dataStored.stored.length - 1;
62 | } else {
63 | dataStored = {
64 | stored: [projectData],
65 | current: 0
66 | };
67 | }
68 | storage.setItem(STORAGE_KEY, JSON.stringify(dataStored));
69 | return true;
70 | } catch (e) {
71 | return false; // There was an error
72 | }
73 | }
74 |
75 | /*
76 | Remove a project from the stored data collection
77 | */
78 | export function removeProjectFromStorage(storage, indexToRemove) {
79 | const dataStored = getDataFromStorage(storage);
80 | if (dataStored) {
81 | let newCurrent = 0;
82 | dataStored.stored.splice(indexToRemove, 1);
83 | if (dataStored.stored.length === 0) {
84 | newCurrent = -1; // Empty collection
85 | } else if (dataStored.current > indexToRemove) {
86 | newCurrent = dataStored.current - 1; // Current is greater than the one to remove
87 | }
88 | dataStored.current = newCurrent;
89 | return saveDataToStorage(storage, dataStored);
90 | }
91 | return false; // There was an error if it reaches this code
92 | }
93 |
94 | /*
95 | Returns the export code
96 | */
97 | export function generateExportString(projectData) {
98 | try {
99 | return JSON.stringify(projectData);
100 | } catch (e) {
101 | return 'Sorry, there was an error';
102 | }
103 | }
104 |
105 | /*
106 | Returns project data ready from a exported data string
107 | */
108 | export function exportedStringToProjectData(projectData) {
109 | if (projectData === '') {
110 | return false;
111 | }
112 | try {
113 | return JSON.parse(projectData);
114 | } catch (e) {
115 | return false;
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/test/drawingToolReducer.test.js:
--------------------------------------------------------------------------------
1 | import {
2 | PENCIL,
3 | BUCKET,
4 | MOVE,
5 | ERASER,
6 | EYEDROPPER,
7 | COLOR_PICKER
8 | } from '../src/store/reducers/drawingToolStates';
9 | import reducer from '../src/store/reducers/drawingToolReducer';
10 | import { APPLY_EYEDROPPER } from '../src/store/actions/actionTypes';
11 | import * as actions from '../src/store/actions/actionCreators';
12 |
13 | const otherAction = () => ({});
14 |
15 | describe('drawing tool reducer: SET_INITIAL_STATE', () => {
16 | it('should set PENCIL state', () => {
17 | const state = EYEDROPPER;
18 | const nextState = reducer(state, actions.setInitialState({}));
19 |
20 | expect(nextState).toEqual(PENCIL);
21 | });
22 | });
23 |
24 | describe('drawing tool reducer: NEW_PROJECT', () => {
25 | it('should set PENCIL state', () => {
26 | const state = ERASER;
27 | const nextState = reducer(state, actions.newProject());
28 |
29 | expect(nextState).toEqual(PENCIL);
30 | });
31 | });
32 |
33 | describe('drawing tool reducer: APPLY_EYEDROPPER', () => {
34 | it('should set PENCIL state when eyedropper action is performed', () => {
35 | const nextState = reducer('', { type: APPLY_EYEDROPPER });
36 |
37 | expect(nextState).toEqual(PENCIL);
38 | });
39 | });
40 |
41 | describe('drawing tool reducer: SELECT_PALETTE_COLOR', () => {
42 | it('should set PENCIL state when state is EYEDROPPER', () => {
43 | const state = EYEDROPPER;
44 | const nextState = reducer(state, actions.selectPaletteColor());
45 |
46 | expect(nextState).toEqual(PENCIL);
47 | });
48 |
49 | it('should set PENCIL state when state is ERASER', () => {
50 | const state = ERASER;
51 | const nextState = reducer(state, actions.selectPaletteColor());
52 |
53 | expect(nextState).toEqual(PENCIL);
54 | });
55 |
56 | it('should keep the same state when state is BUCKET', () => {
57 | const state = BUCKET;
58 | const nextState = reducer(state, actions.selectPaletteColor());
59 |
60 | expect(nextState).toEqual(state);
61 | });
62 |
63 | it('should keep the same state when state is MOVE', () => {
64 | const state = MOVE;
65 | const nextState = reducer(state, actions.selectPaletteColor());
66 |
67 | expect(nextState).toEqual(PENCIL);
68 | });
69 |
70 | it('should keep the same state when state is PENCIL', () => {
71 | const state = PENCIL;
72 | const nextState = reducer(state, actions.selectPaletteColor());
73 |
74 | expect(nextState).toEqual(state);
75 | });
76 | });
77 |
78 | describe('drawing tool reducer: SWITCH_TOOL', () => {
79 | it('should set the action tool when state is PENCIL', () => {
80 | const tool = ERASER;
81 | const state = PENCIL;
82 | const nextState = reducer(state, actions.switchTool(tool));
83 |
84 | expect(nextState).toEqual(tool);
85 | });
86 |
87 | it('should set the action PENCIL when state is the same as action tool', () => {
88 | const tool = EYEDROPPER;
89 | const state = EYEDROPPER;
90 | const nextState = reducer(state, actions.switchTool(tool));
91 |
92 | expect(nextState).toEqual(PENCIL);
93 | });
94 |
95 | it('should set the action tool when state is different to action tool', () => {
96 | const tool = COLOR_PICKER;
97 | const state = BUCKET;
98 | const nextState = reducer(state, actions.switchTool(tool));
99 |
100 | expect(nextState).toEqual(tool);
101 | });
102 | });
103 |
104 | describe('drawing tool reducer: <
>', () => {
105 | it('should keep the same state', () => {
106 | const state = BUCKET;
107 | const nextState = reducer(state, otherAction());
108 |
109 | expect(nextState).toEqual(state);
110 | });
111 | });
112 |
--------------------------------------------------------------------------------
/test/reducer.test.js:
--------------------------------------------------------------------------------
1 | import { Map } from 'immutable';
2 | import reducer from '../src/store/reducers/reducer';
3 | import * as actions from '../src/store/actions/actionCreators';
4 |
5 | describe('reducer: UPDATE_GRID_BOUNDARIES', () => {
6 | it('should update the grid boundaries', () => {
7 | const newBoundaries = {
8 | x: 100,
9 | y: 200,
10 | width: 400,
11 | height: 350
12 | };
13 | const gridElement = {
14 | getBoundingClientRect: () => newBoundaries
15 | };
16 | const dummyState = reducer(Map(), actions.setInitialState({}));
17 | const nextState = reducer(
18 | dummyState,
19 | actions.updateGridBoundaries(gridElement)
20 | );
21 |
22 | expect(nextState.get('gridBoundaries')).toEqual(newBoundaries);
23 | });
24 |
25 | it('grid boundaries is not updated with properties distinct to x, y, width or height', () => {
26 | const newBoundaries = {
27 | x: 100,
28 | y: 200,
29 | width: 400,
30 | height: 350,
31 | extraProp: 'hello'
32 | };
33 | const gridElement = {
34 | getBoundingClientRect: () => newBoundaries
35 | };
36 | const dummyState = reducer(Map(), actions.setInitialState({}));
37 | const nextState = reducer(
38 | dummyState,
39 | actions.updateGridBoundaries(gridElement)
40 | );
41 |
42 | expect(nextState.get('gridBoundaries').extraProp).toEqual(undefined);
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/test/utils/color.test.js:
--------------------------------------------------------------------------------
1 | import formatPixelColorOutput from '../../src/utils/color';
2 |
3 | describe('formatPixelColorOutput', () => {
4 | describe('When the params are empty or not valid', () => {
5 | it('should return the rgba format of black color', () => {
6 | const blackRgba = 'rgba(0,0,0,1)';
7 | expect(formatPixelColorOutput(null, null)).toEqual(blackRgba);
8 | expect(formatPixelColorOutput(undefined, undefined)).toEqual(blackRgba);
9 | expect(formatPixelColorOutput(20, null)).toEqual(blackRgba);
10 | });
11 | });
12 | describe('When the chosen formatId is 0', () => {
13 | it('should return color formatted as: #000000', () => {
14 | expect(formatPixelColorOutput('rgba(255,255,255,1)', 0)).toEqual(
15 | '#ffffff'
16 | );
17 | expect(formatPixelColorOutput('#ffffff', 0)).toEqual('#ffffff');
18 | expect(formatPixelColorOutput('', 0)).toEqual('#000000');
19 | });
20 | });
21 | describe('When the chosen formatId is 1', () => {
22 | it('should return color formatted as: 0x000000', () => {
23 | expect(formatPixelColorOutput('rgba(255,255,255,1)', 1)).toEqual(
24 | '0xffffff'
25 | );
26 | expect(formatPixelColorOutput('#ffffff', 1)).toEqual('0xffffff');
27 | expect(formatPixelColorOutput('', 1)).toEqual('0x000000');
28 | });
29 | });
30 | describe('When the chosen formatId is 2', () => {
31 | it('should return color formatted as: rgba(0,0,0,1)', () => {
32 | expect(formatPixelColorOutput('rgba(255,255,255,1)', 2)).toEqual(
33 | 'rgba(255,255,255,1)'
34 | );
35 | expect(formatPixelColorOutput('#ffffff', 2)).toEqual(
36 | 'rgba(255,255,255,1)'
37 | );
38 | expect(formatPixelColorOutput('', 2)).toEqual('rgba(0,0,0,1)');
39 | });
40 | });
41 | describe('When the input color is rgba', () => {
42 | it('should keep the opacity if the format is rgba', () => {
43 | expect(formatPixelColorOutput('rgba(255,255,255,0.6)', 2)).toEqual(
44 | 'rgba(255,255,255,0.6)'
45 | );
46 | });
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/test/utils/loadFromCanvas.test.js:
--------------------------------------------------------------------------------
1 | import 'jest-canvas-mock';
2 | import {
3 | getDimensionIntervals,
4 | getCanvasDimensions
5 | } from '../../src/utils/loadFromCanvas';
6 |
7 | describe('loadFromCanvas tests', () => {
8 | describe('getCanvasDimensions', () => {
9 | describe('When the canvas ref received is not valid', () => {
10 | it('should return width and height 0', () => {
11 | const expectedOutput = { w: 0, h: 0 };
12 | expect(getCanvasDimensions(null)).toEqual(expectedOutput);
13 | expect(getCanvasDimensions({})).toEqual(expectedOutput);
14 | });
15 | });
16 | describe('When canvasRef contains a reference to a canvas', () => {
17 | it('should return its width and height dimensions', () => {
18 | const CANVAS_WIDTH = 100;
19 | const CANVAS_HEIGHT = 200;
20 | const canvas = document.createElement('canvas');
21 | canvas.width = CANVAS_WIDTH;
22 | canvas.height = CANVAS_HEIGHT;
23 | const canvasRef = { current: canvas };
24 | const expectedOutput = { w: CANVAS_WIDTH, h: CANVAS_HEIGHT };
25 |
26 | expect(getCanvasDimensions(canvasRef)).toEqual(expectedOutput);
27 | });
28 | });
29 | });
30 | describe('getDimensionIntervals', () => {
31 | describe('If only one frame is received', () => {
32 | describe('and dimension is greater than 0', () => {
33 | it('should return only an interval 0-100, 100%', () => {
34 | const expectedOutput = [
35 | {
36 | start: 0,
37 | end: 100,
38 | timePercentage: 100
39 | }
40 | ];
41 | expect(getDimensionIntervals(100, 1)).toEqual(expectedOutput);
42 | });
43 | });
44 | describe('and dimension is 0 or null', () => {
45 | it('should return only an interval 0-0, 100%', () => {
46 | const expectedOutput = [
47 | {
48 | start: 0,
49 | end: 0,
50 | timePercentage: 100
51 | }
52 | ];
53 | expect(getDimensionIntervals(0, 1)).toEqual(expectedOutput);
54 | expect(getDimensionIntervals(null, 1)).toEqual(expectedOutput);
55 | });
56 | });
57 | });
58 |
59 | describe('For a given dimension size and more than one frame', () => {
60 | it('should return each equal interval with start, end and time percentage', () => {
61 | const expectedOutput = [
62 | {
63 | start: 0,
64 | end: 25,
65 | timePercentage: 25
66 | },
67 | {
68 | start: 25,
69 | end: 50,
70 | timePercentage: 50
71 | },
72 | {
73 | start: 50,
74 | end: 75,
75 | timePercentage: 75
76 | },
77 | {
78 | start: 75,
79 | end: 100,
80 | timePercentage: 100
81 | }
82 | ];
83 | expect(getDimensionIntervals(100, 4)).toEqual(expectedOutput);
84 | });
85 | });
86 | describe('If the frame value is 0 or null', () => {
87 | it('should return an empty array', () => {
88 | expect(getDimensionIntervals(100, 0)).toEqual([]);
89 | });
90 | });
91 | });
92 | });
93 |
--------------------------------------------------------------------------------
/test/utils/outputParse.test.js:
--------------------------------------------------------------------------------
1 | import { List, Map } from 'immutable';
2 | import generateFramesOutput from '../../src/utils/outputParse';
3 |
4 | const gridMock = ['#111111', '#222222', '#222222', '#111111'];
5 | const generateFrames = grid =>
6 | List([
7 | Map({
8 | grid: List(grid)
9 | })
10 | ]);
11 |
12 | describe('generateFramesOutput', () => {
13 | const frames = generateFrames(gridMock);
14 | const columns = 2;
15 |
16 | describe('Color format', () => {
17 | describe('When the colorFormat is 0', () => {
18 | it('should return the output with the color formatted like: #000000', () => {
19 | const options = {
20 | colorFormat: 0,
21 | reverseOdd: false,
22 | reverseEven: false
23 | };
24 | expect(
25 | generateFramesOutput({
26 | frames,
27 | columns,
28 | options
29 | })
30 | ).toEqual(`frame0 = {\n #111111, #222222,\n #222222, #111111\n};`);
31 | });
32 | });
33 | describe('When the colorFormat is 1', () => {
34 | it('should return the output with the color formatted like: 0x000000', () => {
35 | const options = {
36 | colorFormat: 1,
37 | reverseOdd: false,
38 | reverseEven: false
39 | };
40 | expect(
41 | generateFramesOutput({
42 | frames,
43 | columns,
44 | options
45 | })
46 | ).toEqual(`frame0 = {\n 0x111111, 0x222222,\n 0x222222, 0x111111\n};`);
47 | });
48 | });
49 | describe('When the colorFormat is 2', () => {
50 | it('should return the output with the color formatted like: rgba(0,0,0,1)', () => {
51 | const options = {
52 | colorFormat: 2,
53 | reverseOdd: false,
54 | reverseEven: false
55 | };
56 | expect(
57 | generateFramesOutput({
58 | frames,
59 | columns,
60 | options
61 | })
62 | ).toEqual(
63 | `frame0 = {\n rgba(17,17,17,1), rgba(34,34,34,1),\n rgba(34,34,34,1), rgba(17,17,17,1)\n};`
64 | );
65 | });
66 | });
67 | });
68 | describe('Reverse rows', () => {
69 | describe('When the reverseOdd is true', () => {
70 | it('should return the odd rows reversed', () => {
71 | const options = {
72 | colorFormat: 0,
73 | reverseOdd: true,
74 | reverseEven: false
75 | };
76 | expect(
77 | generateFramesOutput({
78 | frames,
79 | columns,
80 | options
81 | })
82 | ).toEqual(`frame0 = {\n #222222, #111111,\n #222222, #111111\n};`);
83 | });
84 | });
85 | describe('When the reverseEven is true', () => {
86 | it('should return the odd rows reversed', () => {
87 | const options = {
88 | colorFormat: 0,
89 | reverseOdd: false,
90 | reverseEven: true
91 | };
92 | expect(
93 | generateFramesOutput({
94 | frames,
95 | columns,
96 | options
97 | })
98 | ).toEqual(`frame0 = {\n #111111, #222222,\n #111111, #222222\n};`);
99 | });
100 | });
101 | });
102 | });
103 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const CopyWebpackPlugin = require('copy-webpack-plugin');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
5 | const Dotenv = require('dotenv-webpack');
6 | const path = require('path');
7 |
8 | module.exports = {
9 | mode: "development",
10 | devtool: 'cheap-module-source-map',
11 | entry: [
12 | './src/utils/polyfills.js',
13 | './src/index.jsx',
14 | ],
15 | output: {
16 | filename: 'bundle.js',
17 | path: path.join(__dirname, '/public'),
18 | },
19 | module: {
20 | rules: [
21 | {
22 | test: /\.jsx?$/,
23 | exclude: /node_modules/,
24 | use: [
25 | 'babel-loader'
26 | ]
27 | },
28 | {
29 | test: /\.css$/i,
30 | use: [
31 | MiniCssExtractPlugin.loader,
32 | 'css-loader',
33 | 'postcss-loader',
34 | ],
35 | },
36 | {
37 | test: /\.(ttf|eot|svg|woff(2)?)(\?v=[\d.]+)?(\?[a-z0-9#-]+)?$/,
38 | loader: 'url-loader',
39 | options: {
40 | limit: 100000,
41 | name: './css/[hash].[ext]',
42 | },
43 | }
44 | ]
45 | },
46 | resolve: {
47 | extensions: ['.js', '.jsx'],
48 | fallback: {
49 | stream: require.resolve("stream-browserify"),
50 | util: require.resolve("util"),
51 | buffer: require.resolve("buffer/"),
52 | assert: require.resolve("assert/"),
53 | },
54 | },
55 | plugins: [
56 | new Dotenv(),
57 | new CopyWebpackPlugin({
58 | patterns: [
59 | { from: 'src/assets/favicon.ico', to: 'favicon.ico' },
60 | { from: 'src/assets/coindrop-img.png', to: 'coindrop-img.png' }
61 | ],
62 | }),
63 | new MiniCssExtractPlugin({
64 | filename: 'css/main.css',
65 | }),
66 | new HtmlWebpackPlugin({
67 | template: './public/index.dev.html',
68 | inject: true,
69 | }),
70 | new webpack.DefinePlugin({
71 | 'process.env.NODE_ENV': '"development"'
72 | }),
73 | new webpack.ProvidePlugin({
74 | process: 'process/browser',
75 | Buffer: ['buffer', 'Buffer'],
76 | }),
77 | ],
78 | devServer: {
79 | static: './public',
80 | historyApiFallback: true,
81 | },
82 | target: "web",
83 | stats: false
84 | };
85 |
--------------------------------------------------------------------------------
/webpack.production.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const CopyWebpackPlugin = require('copy-webpack-plugin');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
5 | const Dotenv = require('dotenv-webpack');
6 | const path = require('path');
7 |
8 | const config = {
9 | mode: "production",
10 | entry: [
11 | './src/utils/polyfills.js',
12 | './src/index.jsx',
13 | ],
14 | output: {
15 | filename: 'bundle.js',
16 | path: path.join(__dirname, '/deploy'),
17 | },
18 | module: {
19 | rules: [
20 | {
21 | test: /\.jsx?$/,
22 | exclude: /node_modules/,
23 | loader: 'babel-loader'
24 | },
25 | {
26 | test: /\.css$/i,
27 | use: [
28 | MiniCssExtractPlugin.loader,
29 | 'css-loader',
30 | 'postcss-loader',
31 | ],
32 | },
33 | {
34 | test: /\.(ttf|eot|svg|woff(2)?)(\?v=[\d.]+)?(\?[a-z0-9#-]+)?$/,
35 | loader: 'url-loader',
36 | options: {
37 | limit: 100000,
38 | name: './css/[hash].[ext]',
39 | },
40 | }
41 | ]
42 | },
43 | resolve: {
44 | extensions: ['.js', '.jsx'],
45 | fallback: {
46 | stream: require.resolve("stream-browserify"),
47 | util: require.resolve("util"),
48 | buffer: require.resolve("buffer/"),
49 | assert: require.resolve("assert/"),
50 | },
51 | },
52 | plugins: [
53 | new Dotenv(),
54 | new CopyWebpackPlugin({
55 | patterns: [
56 | { from: 'src/assets/favicon.ico', to: 'favicon.ico' },
57 | { from: 'src/assets/apple-touch-icon.png', to: 'apple-touch-icon.png' },
58 | { from: 'src/assets/regular-icon.png', to: 'regular-icon.png' },
59 | { from: 'src/assets/coindrop-img.png', to: 'coindrop-img.png' },
60 | { from: './public/_redirects', to: './' }
61 | ],
62 | }),
63 | new MiniCssExtractPlugin({
64 | filename: 'css/main.css',
65 | }),
66 | new HtmlWebpackPlugin({
67 | template: './public/index.html',
68 | inject: true,
69 | }),
70 | new webpack.DefinePlugin({
71 | 'process.env.NODE_ENV': '"production"'
72 | }),
73 | new webpack.ProvidePlugin({
74 | process: 'process/browser',
75 | Buffer: ['buffer', 'Buffer'],
76 | }),
77 | ],
78 | target: "web",
79 | stats: false,
80 | };
81 |
82 | module.exports = config;
83 |
--------------------------------------------------------------------------------