├── .compilerc ├── .gitignore ├── .stylelintrc ├── .vscode ├── launch.json └── settings.json ├── LICENSE.md ├── README.md ├── build ├── imports.css └── index.html ├── examples └── import-export │ └── json-cat.js ├── package.json ├── postcss.config.js ├── screenshots ├── animation-cat.gif └── screenshot-cat.png ├── src ├── assets │ ├── apple-touch-icon.png │ ├── dmg │ │ └── background.png │ ├── favicon.ico │ ├── pp-logo.png │ ├── pp-logo.png.icns │ ├── pp-logo.png.ico │ └── regular-icon.png ├── components │ ├── AddCloudPeerForm.jsx │ ├── Animation.jsx │ ├── App.jsx │ ├── AvatarContainer.jsx │ ├── Bucket.jsx │ ├── Button.jsx │ ├── Canvas.jsx │ ├── CellSize.jsx │ ├── CloudPeers.jsx │ ├── ColorPicker.jsx │ ├── CopyCSS.jsx │ ├── CssDisplay.jsx │ ├── DebugInfoContainer.jsx │ ├── Dimensions.jsx │ ├── DownloadDrawing.jsx │ ├── Duration.jsx │ ├── Eraser.jsx │ ├── Eyedropper.jsx │ ├── Field.jsx │ ├── Frame.jsx │ ├── FramesHandler.jsx │ ├── GridWrapper.jsx │ ├── HistoryContainer.jsx │ ├── ImportImageContainer.jsx │ ├── LoadDrawing.jsx │ ├── Modal.jsx │ ├── PaletteColor.jsx │ ├── PaletteGrid.jsx │ ├── Picker.jsx │ ├── PixelCanvas.jsx │ ├── PixelCell.jsx │ ├── PixelConflictContainer.jsx │ ├── PixelGrid.jsx │ ├── PresenceContainer.jsx │ ├── Preview.jsx │ ├── ProjectInfoContainer.jsx │ ├── RadioSelector.jsx │ ├── Reset.jsx │ ├── Root.jsx │ ├── ShareLinkContainer.jsx │ ├── SimpleNotification.jsx │ ├── SimpleSpinner.jsx │ ├── TitleContainer.jsx │ ├── Tooltip.jsx │ ├── TwitterForm.jsx │ ├── Version.jsx │ ├── VersionsContainer.jsx │ └── Window.jsx ├── css │ ├── _base.css │ ├── _normalize.css │ ├── _utils.css │ ├── _variables.css │ ├── components │ │ ├── _App.css │ │ ├── _Bucket.css │ │ ├── _Button.css │ │ ├── _CellSize.css │ │ ├── _CloudPeers.css │ │ ├── _ColorPicker.css │ │ ├── _CopyCss.css │ │ ├── _Credits.css │ │ ├── _CssDisplay.css │ │ ├── _Dimensions.css │ │ ├── _DownloadDrawing.css │ │ ├── _Duration.css │ │ ├── _Eraser.css │ │ ├── _EyeDropper.css │ │ ├── _Field.css │ │ ├── _Frame.css │ │ ├── _FramesHandler.css │ │ ├── _LoadDrawing.css │ │ ├── _Menu.css │ │ ├── _Modal.css │ │ ├── _NewProject.css │ │ ├── _PaletteColor.css │ │ ├── _PaletteGrid.css │ │ ├── _Peer.css │ │ ├── _Picker.css │ │ ├── _PixelConflict.css │ │ ├── _PixelGrid.css │ │ ├── _RadioSelector.css │ │ ├── _Reset.css │ │ ├── _SaveDrawing.css │ │ ├── _SimpleNotification.css │ │ ├── _SimpleSpinner.css │ │ ├── _Tooltip.css │ │ ├── _TwitterForm.css │ │ ├── _UndoRedo.css │ │ └── _Version.css │ ├── fonts │ │ ├── _fonts.css │ │ └── files │ │ │ ├── FontAwesome.otf │ │ │ ├── fontawesome-webfont.eot │ │ │ ├── fontawesome-webfont.svg │ │ │ ├── fontawesome-webfont.ttf │ │ │ ├── fontawesome-webfont.woff │ │ │ ├── fontawesome-webfont.woff2 │ │ │ ├── minecraftia-regular-webfont.eot │ │ │ ├── minecraftia-regular-webfont.svg │ │ │ ├── minecraftia-regular-webfont.ttf │ │ │ ├── minecraftia-regular-webfont.woff │ │ │ └── minecraftia-regular-webfont.woff2 │ ├── imports.css │ ├── input │ │ ├── _button.css │ │ └── _inputText.css │ └── layout │ │ ├── _banner.css │ │ ├── _grid.css │ │ ├── _header.css │ │ └── _queries.css ├── electron.js ├── index.html ├── index.jsx ├── lib │ └── hypermerge-redux.js ├── logic │ ├── Clock.js │ ├── Init.js │ ├── Keyboard.js │ ├── Mutation.js │ ├── Pixels.js │ └── Versions.js ├── records │ ├── CloudPeer.js │ ├── Identity.js │ ├── Peer.js │ ├── Pixel.js │ ├── Project.js │ ├── State.js │ └── Tree.js ├── store │ ├── actions │ │ └── actionCreators.js │ ├── autoSave.js │ ├── cloudPeers.js │ ├── configureStore.js │ ├── hypermergeHelpers.js │ ├── openUrlHandler.js │ ├── reducers │ │ ├── reducer.js │ │ └── reducerHelpers.js │ └── whenChanged.js └── utils │ ├── cssParse.js │ ├── serialization.js │ ├── share.js │ ├── shareLink.js │ └── storage.js └── yarn.lock /.compilerc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "development": { 4 | "application/javascript": { 5 | "presets": [ 6 | ["env", { "targets": { "electron": "1.6.0" } }], 7 | "react" 8 | ], 9 | "plugins": [ 10 | "transform-object-rest-spread", 11 | "transform-class-properties", 12 | "transform-es2015-classes", 13 | "react-hot-loader/babel" 14 | ], 15 | "sourceMaps": "inline" 16 | } 17 | }, 18 | "production": { 19 | "application/javascript": { 20 | "presets": [ 21 | ["env", { "targets": { "electron": "1.6.0" } }], 22 | "react" 23 | ], 24 | "plugins": [ 25 | "transform-object-rest-spread", 26 | "transform-class-properties", 27 | "transform-es2015-classes" 28 | ], 29 | "sourceMaps": "none" 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist/bundle.js 3 | npm-debug.log 4 | deploy/* 5 | Procfile 6 | config.json 7 | public 8 | routes 9 | npm-debug.log 10 | .directory 11 | /.data 12 | .env 13 | .eslintcache 14 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-standard", 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 | } 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Client 1", 11 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", 12 | "program": "${workspaceFolder}/src/electron.js", 13 | "protocol": "inspector", 14 | "env": { 15 | "CLIENT_ID": "1" 16 | } 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "standard.enable": true 3 | } 4 | -------------------------------------------------------------------------------- /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 | # pixelpusher 2 | 3 | ![](screenshots/animation-cat.gif) 4 | 5 | Read the announcement blog post here: 6 | 7 | [Pixelpusher: Real-time peer-to-peer collaboration with React](https://medium.com/@pvh/pixelpusher-real-time-peer-to-peer-collaboration-with-react-7c7bc8ecbf74) 8 | 9 | ## Downloadable releases 10 | 11 | https://github.com/automerge/pixelpusher/releases 12 | 13 | A Mac .dmg and Windows installer are available. These are hand built at the moment. 14 | 15 | ## Installation from source 16 | 17 | ```bash 18 | # Install latest node. I'm using 9.2.1 19 | brew install yarn 20 | brew install openssl 21 | yarn 22 | 23 | export CPPFLAGS=-I/usr/local/opt/openssl/include 24 | export LDFLAGS=-L/usr/local/opt/openssl/lib 25 | yarn start 26 | ``` 27 | 28 | You can start additional clients by setting `CLIENT_ID` (default: 0): 29 | 30 | ``` 31 | CLIENT_ID=1 yarn start 32 | ``` 33 | 34 | If you want to edit the CSS, for now, start the css watcher separately: 35 | 36 | ```bash 37 | yarn run css 38 | ``` 39 | 40 | ![pixelpusher](screenshots/screenshot-cat.png) 41 | 42 | ## Slack 43 | 44 | [Join the Automerge Slack community](https://communityinviter.com/apps/automerge/automerge) 45 | 46 | There is a #pixelpusher channel. Come share your artwork and ask us about the code! 47 | -------------------------------------------------------------------------------- /build/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |
11 |

PIXEL ART TO CSS

12 |
13 |
14 |

by JVALEN

15 | 20 |
21 |
22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/import-export/json-cat.js: -------------------------------------------------------------------------------- 1 | export const exampleCat = { 2 | "id": "ryCp2kGsx", 3 | "frames":[ 4 | { 5 | "pixels":[ 6 | "#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#303f46","#303f46","#303f46","#303f46","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffcdd2","#ffffff","#303f46","#303f46","#ffffff","#ffcdd2","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffcdd2","#ffcdd2","#ffffff","#ffffff","#ffcdd2","#ffcdd2","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffcdd2","#ffffff","#ffffff","#ffffff","#ffffff","#ffcdd2","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#000000","#ffffff","#ffffff","#000000","#ffffff","#ffffff","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#000000","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#303f46","#303f46","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#303f46","#303f46","#303f46","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#303f46","#303f46","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#303f46","#303f46","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#303f46","#ffffff","#ffffff","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#303f46","#303f46","#607d8b","#607d8b","#607d8b","#607d8b","#607d8b","#ffffff","#ffffff","#607d8b","#607d8b","#607d8b","#ffffff","#ffffff","#607d8b","#607d8b","#607d8b","#607d8b" 7 | ], 8 | "interval":25, 9 | "id":"ryHxk7Qsx" 10 | }, 11 | { 12 | "pixels":[ 13 | "#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#303f46","#303f46","#303f46","#303f46","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffcdd2","#ffffff","#303f46","#303f46","#ffffff","#ffcdd2","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffcdd2","#ffcdd2","#ffffff","#ffffff","#ffcdd2","#ffcdd2","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffcdd2","#ffffff","#ffffff","#ffffff","#ffffff","#ffcdd2","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#000000","#ffffff","#ffffff","#000000","#ffffff","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#000000","#ffffff","#ffffff","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#303f46","#303f46","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#303f46","#303f46","#303f46","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#303f46","#303f46","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#303f46","#303f46","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#303f46","#ffffff","#ffffff","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#303f46","#303f46","#607d8b","#607d8b","#607d8b","#607d8b","#607d8b","#ffffff","#ffffff","#607d8b","#607d8b","#607d8b","#ffffff","#ffffff","#607d8b","#607d8b","#607d8b","#607d8b" 14 | ], 15 | "interval":50, 16 | "id":"HkZXJXmie" 17 | }, 18 | { 19 | "pixels":[ 20 | "#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#303f46","#303f46","#303f46","#303f46","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffcdd2","#ffffff","#303f46","#303f46","#ffffff","#ffcdd2","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffcdd2","#ffcdd2","#ffffff","#ffffff","#ffcdd2","#ffcdd2","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffcdd2","#ffffff","#ffffff","#ffffff","#ffffff","#ffcdd2","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#000000","#ffffff","#ffffff","#000000","#ffffff","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#000000","#ffffff","#ffffff","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#303f46","#303f46","#303f46","#303f46","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#303f46","#303f46","#303f46","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#303f46","#303f46","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#303f46","#303f46","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#303f46","#ffffff","#ffffff","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#303f46","#303f46","#607d8b","#607d8b","#607d8b","#607d8b","#607d8b","#ffffff","#ffffff","#607d8b","#607d8b","#607d8b","#ffffff","#ffffff","#607d8b","#607d8b","#607d8b","#607d8b" 21 | ], 22 | "interval":75, 23 | "id":"BykH1Qmig" 24 | }, 25 | { 26 | "pixels":[ 27 | "#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#303f46","#303f46","#303f46","#303f46","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffcdd2","#ffffff","#303f46","#303f46","#ffffff","#ffcdd2","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffcdd2","#ffcdd2","#ffffff","#ffffff","#ffcdd2","#ffcdd2","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffcdd2","#ffffff","#ffffff","#ffffff","#ffffff","#ffcdd2","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#000000","#ffffff","#ffffff","#000000","#ffffff","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#000000","#ffffff","#ffffff","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#303f46","#303f46","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#303f46","#303f46","#303f46","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#303f46","#303f46","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#303f46","#303f46","#ffffff","#303f46","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#303f46","#ffffff","#ffffff","#303f46","#303f46","#303f46","#303f46","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#ffffff","#303f46","#303f46","#607d8b","#607d8b","#607d8b","#607d8b","#607d8b","#ffffff","#ffffff","#607d8b","#607d8b","#607d8b","#ffffff","#ffffff","#607d8b","#607d8b","#607d8b","#607d8b" 28 | ], 29 | "interval":100, 30 | "id":"S1F3nG7sg" 31 | } 32 | ], 33 | "palette":[ 34 | {"color":"#000000","id":0},{"color":"#ff0000","id":1},{"color":"#e91e63","id":2},{"color":"#9c27b0","id":3},{"color":"#673ab7","id":4},{"color":"#3f51b5","id":5},{"color":"#2196f3","id":6},{"color":"#03a9f4","id":7},{"color":"#00bcd4","id":8},{"color":"#009688","id":9},{"color":"#4caf50","id":10},{"color":"#8bc34a","id":11},{"color":"#cddc39","id":12},{"color":"#9ee07a","id":13},{"color":"#ffeb3b","id":14},{"color":"#ffc107","id":15},{"color":"#ff9800","id":16},{"color":"#ffcdd2","id":17},{"color":"#ff5722","id":18},{"color":"#795548","id":19},{"color":"#9e9e9e","id":20},{"color":"#607d8b","id":21},{"color":"#303f46","id":22},{"color":"#ffffff","id":23},{"color":"#383535","id":24},{"color":"#383534","id":25},{"color":"#383533","id":26},{"color":"#383532","id":27},{"color":"#383531","id":28},{"color":"#383530","id":29} 35 | ], 36 | "cellSize":5, 37 | "columns":16, 38 | "rows":16 39 | }; 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pixelpusher", 3 | "productName": "pixelpusher", 4 | "version": "0.0.4", 5 | "description": "Draw and animate Pixel Art, then export the results to CSS, share or download them, powered by Hypermerge", 6 | "main": "src/electron.js", 7 | "scripts": { 8 | "start": "electron-forge start", 9 | "package": "electron-forge package", 10 | "make": "electron-forge make", 11 | "publish": "electron-forge publish", 12 | "css": "postcss --watch --dir=build src/css/imports.css" 13 | }, 14 | "keywords": [], 15 | "author": "jeffpeterson", 16 | "config": { 17 | "forge": { 18 | "make_targets": { 19 | "win32": [ 20 | "squirrel" 21 | ], 22 | "darwin": [ 23 | "dmg" 24 | ], 25 | "linux": [ 26 | "deb", 27 | "rpm" 28 | ] 29 | }, 30 | "electronPackagerConfig": { 31 | "packageManager": "yarn", 32 | "icon": "src/assets/pp-logo.png.icns" 33 | }, 34 | "electronInstallerDMG": { 35 | "title": "pixelpusher", 36 | "icon": "./src/assets/pp-logo.png.icns", 37 | "iconsize": 100, 38 | "background": "./src/assets/dmg/background.png" 39 | }, 40 | "electronWinstallerConfig": { 41 | "name": "pixel_art_forge" 42 | }, 43 | "electronInstallerDebian": {}, 44 | "electronInstallerRedhat": {}, 45 | "github_repository": { 46 | "owner": "", 47 | "name": "" 48 | }, 49 | "windowsStoreConfig": { 50 | "packageName": "", 51 | "name": "pixelartforge" 52 | } 53 | } 54 | }, 55 | "dependencies": { 56 | "automerge": "^0.7.0", 57 | "bs58": "^4.0.1", 58 | "classnames": "^2.2.5", 59 | "cookie-parser": "^1.4.3", 60 | "electron-compile": "^6.4.2", 61 | "electron-devtools-installer": "^2.1.0", 62 | "electron-squirrel-startup": "^1.0.0", 63 | "get-image-pixels": "^1.0.1", 64 | "hypercore": "^6.12.1", 65 | "hyperdiscovery": "^7.0.0", 66 | "hypermerge": "^0.3.2", 67 | "immutable": "^3.8.2", 68 | "js-crc": "^0.2.0", 69 | "lodash": "^4.17.4", 70 | "lost": "^8.2.0", 71 | "mkdirp": "^0.5.1", 72 | "postcss-import": "^11.0.0", 73 | "postcss-reporter": "^5.0.0", 74 | "precss": "^2.0.0", 75 | "pretty-hash": "^1.0.1", 76 | "radium": "^0.19.6", 77 | "random-access-memory": "^2.4.0", 78 | "react": "^16.1.0", 79 | "react-color": "^2.13.8", 80 | "react-custom-scrollbars": "^4.2.1", 81 | "react-dom": "^16.1.0", 82 | "react-hot-loader": "^3.1.2", 83 | "react-modal": "^3.1.2", 84 | "react-redux": "^5.0.6", 85 | "react-transition-group": "^2.2.1", 86 | "redux": "^3.7.2", 87 | "shortid": "^2.2.8" 88 | }, 89 | "devDependencies": { 90 | "babel-plugin-transform-class-properties": "^6.24.1", 91 | "babel-plugin-transform-es2015-classes": "^6.24.1", 92 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 93 | "babel-preset-env": "^1.6.1", 94 | "babel-preset-react": "^6.24.1", 95 | "electron-forge": "^5.1.0", 96 | "electron-prebuilt-compile": "1.8.2", 97 | "postcss-cli": "^4.1.1", 98 | "postcss-url": "^7.3.0" 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = () => { 2 | return { 3 | plugins: [ 4 | require('postcss-import')(), 5 | require('postcss-url')({ 6 | url: 'inline', 7 | }), 8 | require('precss')(), 9 | require('autoprefixer')({ 10 | browsers: ['last 2 versions', 'IE > 8'] 11 | }), 12 | require('lost'), 13 | require('postcss-reporter')({ 14 | clearMessages: true 15 | }) 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /screenshots/animation-cat.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/automerge/pixelpusher/e03d9f0a2703f1f8e60b676c5bbf2381b9f4220e/screenshots/animation-cat.gif -------------------------------------------------------------------------------- /screenshots/screenshot-cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/automerge/pixelpusher/e03d9f0a2703f1f8e60b676c5bbf2381b9f4220e/screenshots/screenshot-cat.png -------------------------------------------------------------------------------- /src/assets/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/automerge/pixelpusher/e03d9f0a2703f1f8e60b676c5bbf2381b9f4220e/src/assets/apple-touch-icon.png -------------------------------------------------------------------------------- /src/assets/dmg/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/automerge/pixelpusher/e03d9f0a2703f1f8e60b676c5bbf2381b9f4220e/src/assets/dmg/background.png -------------------------------------------------------------------------------- /src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/automerge/pixelpusher/e03d9f0a2703f1f8e60b676c5bbf2381b9f4220e/src/assets/favicon.ico -------------------------------------------------------------------------------- /src/assets/pp-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/automerge/pixelpusher/e03d9f0a2703f1f8e60b676c5bbf2381b9f4220e/src/assets/pp-logo.png -------------------------------------------------------------------------------- /src/assets/pp-logo.png.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/automerge/pixelpusher/e03d9f0a2703f1f8e60b676c5bbf2381b9f4220e/src/assets/pp-logo.png.icns -------------------------------------------------------------------------------- /src/assets/pp-logo.png.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/automerge/pixelpusher/e03d9f0a2703f1f8e60b676c5bbf2381b9f4220e/src/assets/pp-logo.png.ico -------------------------------------------------------------------------------- /src/assets/regular-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/automerge/pixelpusher/e03d9f0a2703f1f8e60b676c5bbf2381b9f4220e/src/assets/regular-icon.png -------------------------------------------------------------------------------- /src/components/AddCloudPeerForm.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Field from './Field' 3 | 4 | export default class AddCloudPeerForm extends React.Component { 5 | constructor (props) { 6 | super(props) 7 | this.state = {key: ''} 8 | } 9 | 10 | click () { 11 | let key 12 | try { 13 | key = /^(dat:\/\/)?([0-9a-f]{64})$/i.exec(this.state.key)[2] 14 | if (!key) throw new Error('Missing key') 15 | this.props.onAdd(key) 16 | } catch (e) { 17 | this.setState({ 18 | validationError: 'Invalid key' 19 | }) 20 | } 21 | } 22 | 23 | render () { 24 | return ( 25 |
26 |

Add Archiver

27 | this.setState({key, validationError: null})} 31 | /> 32 | {this.state.validationError} 33 | 34 |
35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/components/Animation.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import radium from 'radium'; 3 | 4 | const Animation = radium((props) => { 5 | const pulseKeyframes = radium.keyframes(props.boxShadow, 'pulse'); 6 | const style = { 7 | position: 'absolute', 8 | animation: `x ${props.duration}s infinite`, 9 | animationName: pulseKeyframes 10 | }; 11 | return ( 12 |
13 | ); 14 | }); 15 | 16 | export default Animation; 17 | -------------------------------------------------------------------------------- /src/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PixelCanvasContainer from './PixelCanvas'; 3 | import CellSizeContainer from './CellSize'; 4 | import ColorPickerContainer from './ColorPicker'; 5 | import CloudPeersContainer from './CloudPeers'; 6 | import ModalContainer from './Modal'; 7 | import DimensionsContainer from './Dimensions'; 8 | import DurationContainer from './Duration'; 9 | import EraserContainer from './Eraser'; 10 | import BucketContainer from './Bucket'; 11 | import EyedropperContainer from './Eyedropper'; 12 | import FramesHandlerContainer from './FramesHandler'; 13 | import PaletteGridContainer from './PaletteGrid'; 14 | import ResetContainer from './Reset'; 15 | import SimpleNotificationContainer from './SimpleNotification'; 16 | import SimpleSpinnerContainer from './SimpleSpinner'; 17 | import PresenceContainer from './PresenceContainer'; 18 | import VersionsContainer from './VersionsContainer'; 19 | import DebugInfoContainer from './DebugInfoContainer'; 20 | import ShareLinkContainer from './ShareLinkContainer'; 21 | import ProjectInfoContainer from './ProjectInfoContainer'; 22 | import ImportImageContainer from './ImportImageContainer'; 23 | import TitleContainer from './TitleContainer'; 24 | 25 | export default class App extends React.Component { 26 | constructor() { 27 | super(); 28 | this.state = { 29 | modalType: null, 30 | modalOpen: false, 31 | helpOn: false, 32 | showCookiesBanner: false 33 | }; 34 | } 35 | 36 | changeModalType(type) { 37 | this.setState({ 38 | modalType: type, 39 | modalOpen: true 40 | }); 41 | } 42 | 43 | closeModal() { 44 | this.setState({ 45 | modalOpen: false 46 | }); 47 | } 48 | 49 | hideCookiesBanner() { 50 | this.setState({ 51 | showCookiesBanner: false 52 | }); 53 | } 54 | 55 | toggleHelp() { 56 | this.setState({ helpOn: !this.state.helpOn }); 57 | } 58 | 59 | tip(text) { 60 | return this.state.helpOn ? text : null 61 | } 62 | 63 | render() { 64 | return ( 65 |
66 | {this.renderHeader()} 67 |
68 | 69 | 74 |
75 | {this.renderLeftSide()} 76 |
77 | 78 | 79 |
86 | 87 |
88 | 89 |
90 | {this.renderRightSide()} 91 |
92 | 93 | { this.closeModal(); }} 97 | open={() => { this.changeModalType(this.state.modalType); }} 98 | /> 99 |
100 |
101 | ); 102 | } 103 | 104 | renderHeader() { 105 | return ( 106 |
107 |

PIXELPUSHER

108 |
109 | 110 |
111 |
112 | 118 |
119 |
120 | ); 121 | } 122 | 123 | renderLeftSide() { 124 | return ( 125 |
126 |
127 |
128 | 135 |
136 | 137 |
138 |
139 | 140 |
141 |
142 |
143 | 144 |
145 |
146 | 147 |
148 |
149 | 150 |
151 |
152 | 153 |
154 |
155 |
156 |
157 | 158 |
159 |
160 |
161 |
162 | 163 |
164 |
165 | 166 |
167 |
168 | 169 |
170 |
171 | 172 |
173 |
174 |

175 | Originally by JVALEN 176 |

177 | 178 |
179 |
180 |
181 | ); 182 | } 183 | 184 | renderRightSide() { 185 | return ( 186 |
187 |
188 | 189 | { this.changeModalType('addCloudPeer'); } 191 | } /> 192 |
193 |
194 | ); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/components/AvatarContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import * as Versions from '../logic/Versions' 4 | 5 | import Canvas from './Canvas' 6 | 7 | class Avatar extends React.Component { 8 | render () { 9 | const {identity, avatar} = this.props 10 | 11 | const color = identity && Versions.color(identity) 12 | 13 | if (!identity) return null 14 | 15 | return ( 16 |
17 | { avatar 18 | ? 19 | : null} 20 |
21 | ) 22 | } 23 | } 24 | 25 | const mapStateToProps = (state, {identityId}) => { 26 | const identity = state.identities.get(identityId, null) 27 | const avatarId = identity && identity.getIn(['doc', 'avatarId']) 28 | const avatar = avatarId && state.projects.get(avatarId) 29 | 30 | return { 31 | identity, 32 | avatar 33 | } 34 | } 35 | 36 | const mapDispatchToProps = dispatch => ({ 37 | dispatch 38 | }) 39 | 40 | const AvatarContainer = connect( 41 | mapStateToProps, 42 | mapDispatchToProps 43 | )(Avatar) 44 | 45 | export default AvatarContainer 46 | -------------------------------------------------------------------------------- /src/components/Bucket.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 Bucket = (props) => { 7 | const handleClick = () => { 8 | props.actions.setBucket(); 9 | }; 10 | 11 | return ( 12 | 46 | 69 |
70 | ) 71 | } 72 | } 73 | 74 | const mapStateToProps = state => ({ 75 | cloudPeers: state.cloudPeers 76 | }); 77 | 78 | const mapDispatchToProps = dispatch => ({ 79 | actions: bindActionCreators(actionCreators, dispatch), 80 | dispatch, 81 | }); 82 | 83 | const CloudPeersContainer = connect( 84 | mapStateToProps, 85 | mapDispatchToProps 86 | )(CloudPeers); 87 | export default CloudPeersContainer; -------------------------------------------------------------------------------- /src/components/ColorPicker.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import Picker from 'react-color'; 4 | import { getCurrentColor } from '../store/reducers/reducerHelpers'; 5 | 6 | class ColorPicker extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | displayColorPicker: false, 11 | }; 12 | } 13 | 14 | handleClick() { 15 | this.props.dispatch({type: 'SET_COLOR_PICKER'}); 16 | 17 | if (!this.state.displayColorPicker) { 18 | this.setState({ displayColorPicker: !this.state.displayColorPicker }); 19 | } 20 | } 21 | 22 | handleChange(color) { 23 | this.props.dispatch({type: 'SET_SWATCH_COLOR', color: color.hex}) 24 | } 25 | 26 | handleClose() { 27 | this.setState({ displayColorPicker: false }); 28 | } 29 | 30 | render() { 31 | /* Necessary inline styles for react-color component */ 32 | const styles = { 33 | picker: { 34 | position: 'relative', 35 | bottom: '9em' 36 | }, 37 | popover: { 38 | position: 'absolute', 39 | zIndex: '2', 40 | right: -250, 41 | top: 155 42 | }, 43 | cover: { 44 | position: 'fixed', 45 | top: 0, 46 | right: 0, 47 | bottom: 0, 48 | left: 0 49 | } 50 | }; 51 | 52 | const isSelected = this.props.colorPickerOn && this.state.displayColorPicker; 53 | 54 | return ( 55 |
56 | 26 | ); 27 | }; 28 | 29 | export default DownloadDrawing; 30 | -------------------------------------------------------------------------------- /src/components/Duration.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 Duration = (props) => { 7 | const handleChange = (event) => { 8 | props.actions.setDuration(event.target.value); 9 | }; 10 | return ( 11 |
12 | 15 | { handleChange(event); }} 19 | id="duration__input" 20 | /> 21 |
22 | ); 23 | }; 24 | 25 | const mapStateToProps = state => ({ 26 | duration: state.get('duration') 27 | }); 28 | 29 | const mapDispatchToProps = dispatch => ({ 30 | actions: bindActionCreators(actionCreators, dispatch) 31 | }); 32 | 33 | const DurationContainer = connect( 34 | mapStateToProps, 35 | mapDispatchToProps 36 | )(Duration); 37 | export default DurationContainer; 38 | -------------------------------------------------------------------------------- /src/components/Eraser.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 Eraser = (props) => { 7 | const handleClick = () => { 8 | props.actions.setEraser(); 9 | }; 10 | 11 | return ( 12 |
58 | ) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/components/FramesHandler.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { bindActionCreators } from 'redux'; 4 | import { Scrollbars } from 'react-custom-scrollbars'; 5 | import * as actionCreators from '../store/actions/actionCreators'; 6 | import Frame from './Frame'; 7 | import { getProjectPreview } from '../store/reducers/reducerHelpers'; 8 | 9 | class FramesHandler extends React.Component { 10 | state = { newFrame: false } 11 | 12 | onScrollbarUpdate() { 13 | if (this.state.newFrame) { 14 | this.setState({ newFrame: false }); 15 | this.scrollbars.scrollToRight(); 16 | } 17 | } 18 | 19 | renderFrames() { 20 | const {project} = this.props 21 | 22 | const {doc} = project 23 | 24 | if (!doc || !doc.get('frames')) return null 25 | 26 | return doc.get('frames').map((frame, index, frames) => 27 | 40 | ); 41 | } 42 | 43 | handleClick() { 44 | this.props.actions.createNewFrame(); 45 | this.setState({ newFrame: true }); 46 | } 47 | 48 | 49 | render() { 50 | const {project} = this.props 51 | 52 | if (!project) return null 53 | 54 | return ( 55 |
56 |
57 | { this.scrollbars = c; }} 62 | onUpdate={() => { this.onScrollbarUpdate(); }} 63 | > 64 |
65 | {this.renderFrames()} 66 |
67 |
68 |
69 | 75 |
76 | ); 77 | } 78 | } 79 | 80 | const mapStateToProps = state => ({ 81 | project: getProjectPreview(state), 82 | activeFrameIndex: state.get('activeFrameIndex') 83 | }); 84 | 85 | const mapDispatchToProps = dispatch => ({ 86 | actions: bindActionCreators(actionCreators, dispatch) 87 | }); 88 | 89 | const FramesHandlerContainer = connect( 90 | mapStateToProps, 91 | mapDispatchToProps 92 | )(FramesHandler); 93 | export default FramesHandlerContainer; 94 | -------------------------------------------------------------------------------- /src/components/GridWrapper.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PixelGrid from './PixelGrid'; 3 | 4 | export default class GridWrapper extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { 8 | dragging: false 9 | }; 10 | this.update = this.props.onCellEvent.bind(this); 11 | this.handleMouseUp = this.handleMouseUp.bind(this); 12 | this.handleMouseDown = this.handleMouseDown.bind(this); 13 | this.handleMouseOver = this.handleMouseOver.bind(this); 14 | this.handleTouchMove = this.handleTouchMove.bind(this); 15 | } 16 | 17 | shouldComponentUpdate(newProps) { 18 | return newProps.cells !== this.props.cells; 19 | } 20 | 21 | handleMouseUp(ev) { 22 | ev.preventDefault(); 23 | this.setState({ 24 | dragging: false 25 | }); 26 | } 27 | 28 | handleMouseDown(id, ev) { 29 | ev.preventDefault(); 30 | if (!this.state.dragging) this.update(id); 31 | this.setState({ 32 | dragging: true 33 | }); 34 | } 35 | 36 | handleMouseOver(id, ev) { 37 | ev.preventDefault(); 38 | if (this.state.dragging) this.update(id); 39 | } 40 | 41 | handleTouchMove(id, ev) { 42 | /* 43 | TODO: It should draw the every cell we are moving over 44 | like is done in handleMouseOver. But is not working due 45 | to the nature of the touch events. 46 | 47 | The target element in a touch event is always the one 48 | when the touch started, not the element under the cursor 49 | (like the mouse event behaviour) 50 | */ 51 | ev.preventDefault(); 52 | if (this.state.dragging) this.update(id); 53 | } 54 | 55 | render() { 56 | return ( 57 | 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/components/HistoryContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Automerge from 'automerge' 3 | import {fromJS} from 'immutable' 4 | import { connect } from 'react-redux'; 5 | import { shareLinkForProjectId } from '../utils/shareLink'; 6 | import { getProjectId } from '../store/reducers/reducerHelpers'; 7 | import Version from './Version'; 8 | import {related} from '../logic/Versions' 9 | 10 | class History extends React.Component { 11 | render() { 12 | const {projectId, projects, dispatch} = this.props 13 | if (!projectId) return null 14 | 15 | const currentProject = projects.get(projectId) 16 | 17 | if (!currentProject) return null 18 | 19 | const history = Automerge.getHistory(currentProject).reverse() 20 | 21 | return ( 22 |
23 |

History:

24 | {history.map(this.renderHistoryItem)} 25 |
26 | ) 27 | } 28 | 29 | renderHistoryItem = ({change}) => { 30 | const key = `${change.actor.slice(0, 4)}:${change.seq}` 31 | return ( 32 |
33 | {key} 34 |
35 | ) 36 | } 37 | } 38 | 39 | const mapStateToProps = state => ({ 40 | projects: state.projects, 41 | projectId: getProjectId(state), 42 | }); 43 | 44 | const mapDispatchToProps = dispatch => ({ 45 | dispatch, 46 | }); 47 | 48 | const HistoryContainer = connect( 49 | mapStateToProps, 50 | mapDispatchToProps 51 | )(History); 52 | export default HistoryContainer; 53 | -------------------------------------------------------------------------------- /src/components/ImportImageContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import Field from './Field' 4 | 5 | import * as Pixels from '../logic/Pixels' 6 | 7 | class ImportImage extends React.Component { 8 | render() { 9 | return ( 10 | 15 | ) 16 | } 17 | 18 | importImage = (_, e) => { 19 | const input = e.target 20 | if (!input.files || !input.files[0]) return 21 | 22 | const reader = new FileReader(); 23 | 24 | reader.onload = e => { 25 | const img = new Image() 26 | img.onload = () => this.readImage(img) 27 | img.src = e.target.result 28 | } 29 | 30 | reader.readAsDataURL(input.files[0]); 31 | } 32 | 33 | readImage = img => { 34 | const pixels = Pixels.getPixels(img) 35 | const {width, height} = img 36 | 37 | this.props.dispatch({type: 'PIXELS_IMPORTED', pixels, width, height}) 38 | } 39 | } 40 | 41 | const mapDispatchToProps = dispatch => ({dispatch}); 42 | 43 | const ImportImageContainer = connect( 44 | null, 45 | mapDispatchToProps 46 | )(ImportImage); 47 | 48 | export default ImportImageContainer; 49 | 50 | -------------------------------------------------------------------------------- /src/components/LoadDrawing.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Preview from './Preview'; 3 | import AvatarContainer from './AvatarContainer'; 4 | import Button from './Button'; 5 | import { 6 | generateExportString, exportedStringToProject 7 | } from '../utils/storage'; 8 | 9 | import Project from '../records/Project' 10 | import { keyFromShareLink } from '../utils/shareLink'; 11 | 12 | export default class LoadDrawing extends React.Component { 13 | getExportCode() { 14 | return generateExportString(this.props.project); 15 | } 16 | 17 | importProject() { 18 | const project = exportedStringToProject(this.importProjectData.value); 19 | 20 | if (project) { 21 | this.props.actions.setProject(project); 22 | this.props.close(); 23 | this.props.actions.sendNotification('Project successfully imported'); 24 | } else { 25 | this.props.actions.sendNotification("Sorry, the project couldn't be imported"); 26 | } 27 | } 28 | 29 | deleteProject(id, e) { 30 | e.stopPropagation(); 31 | 32 | this.props.dispatch({type: 'DELETE_DOCUMENT', id}); 33 | } 34 | 35 | newProjectClicked = e => { 36 | this.props.dispatch({type: 'NEW_PROJECT_CLICKED'}); 37 | this.props.close(); 38 | } 39 | 40 | projectClick(id) { 41 | this.props.actions.setProject(id); 42 | this.props.close(); 43 | } 44 | 45 | giveMeProjects() { 46 | const {identity} = this.props; 47 | const avatarId = identity.doc.get('avatarId') 48 | const projectGroups = this.props.projects.valueSeq() 49 | .filter(p => p.groupId) 50 | .groupBy(p => p.groupId); 51 | 52 | return projectGroups.map(group => { 53 | const project = group.find(p => p.doc && p.isWritable) || group.first() 54 | const {id, doc} = project 55 | 56 | if (!doc) return null 57 | 58 | return ( 59 |
{ this.projectClick(id); }} 62 | className="load-drawing__drawing" 63 | > 64 | 71 |
72 |
82 |

{doc.get('title')}

83 |
84 | {project.identityIds.map(id => 85 | 86 | )} 87 |
88 |
89 | ); 90 | }).valueSeq(); 91 | } 92 | 93 | giveMeOptions(type) { 94 | switch (type) { 95 | case 'import': { 96 | return ( 97 |
98 |

Paste a previously exported code

99 |