├── .babelrc
├── .editorconfig
├── .eslintrc
├── .gitignore
├── .idea
├── dictionaries
│ └── svrcek.xml
├── jsLibraryMappings.xml
└── vcs.xml
├── .npmignore
├── LICENSE
├── README.md
├── circle.yml
├── docs
├── frog.gif
└── react3.gif
├── example
├── .babelrc
├── SketchExample.jsx
├── build
│ └── bundle.js
├── index.html
├── index.js
├── package.json
├── server.js
└── webpack.config.js
├── package.json
└── src
├── SketchPad.jsx
├── index.js
└── tools
├── Ellipse.js
├── Line.js
├── Pencil.js
├── Rectangle.js
└── index.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets":["es2015","react"],
3 | "plugins":[
4 | "transform-class-properties",
5 | "transform-object-rest-spread"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs.
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 |
9 | # We recommend you to keep these unchanged.
10 | charset = utf-8
11 | end_of_line = lf
12 | indent_size = 2
13 | indent_style = space
14 | insert_final_newline = true
15 | trim_trailing_whitespace = true
16 |
17 | [package.json]
18 | indent_style = space
19 | indent_size = 2
20 |
21 | [*.md]
22 | trim_trailing_whitespace = false
23 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint-config-airbnb",
3 | "env": {
4 | "browser": true,
5 | "node": true,
6 | "mocha": true
7 | },
8 | "ecmaFeatures": {
9 | "destructing": true,
10 | "classes": true
11 | },
12 | "rules": {
13 | "no-param-reassign": 0,
14 | "consistent-return": 0,
15 | "import/default": 0,
16 | "import/no-duplicates": 0,
17 | "import/named": 0,
18 | "import/namespace": 0,
19 | "import/no-unresolved": 0,
20 | "import/no-named-as-default": 2,
21 | "comma-dangle": 0,
22 | // not sure why airbnb turned this on. gross!
23 | "indent": [2, 2, {"SwitchCase": 1}],
24 | "no-console": 0,
25 | "no-alert": 0,
26 | "max-len":[2,140]
27 | },
28 | "plugins": [
29 | "import"
30 | ],
31 | "parser": "babel-eslint"
32 | }
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | lib/
3 | npm-debug.log
4 | .idea
5 |
--------------------------------------------------------------------------------
/.idea/dictionaries/svrcek.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/.idea/jsLibraryMappings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | src
2 | example
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Michal Svrček
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, 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,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-sketchpad
2 | Sketch pad created with canvas
3 |
4 | ## Why I built this?
5 |
6 | 1. to learn
7 | 2. to learn
8 | 3. to learn
9 | 4. just have fun? :D
10 |
11 | ## Example:
12 |
13 | 
14 |
15 | 
16 |
17 | There was websocket used in this gifs, which is not part of example. To make it work with syncing just run this little websocket server
18 |
19 | ```js
20 | import Server from 'socket.io';
21 | const io = new Server().attach(12346);
22 |
23 | io.on('connection', (socket) => {
24 | socket.on('addItem', (data) => {
25 | console.log(data);
26 | socket.broadcast.emit('addItem', data);
27 | });
28 | });
29 | ```
30 |
31 | ## API:
32 |
33 | |Attribute |Type |Default Value |Description |
34 | |--- |--- |--- |--- |
35 | | width | number | 500 | width of canvas in pixels |
36 | | height | number | 500 | height of the canvas in pixels |
37 | | items | array | - | array of items to draw in canvas |
38 | | animate | bool | true | few tools, for example pencil, can be animated when drawn |
39 | | canvasClassName | string | .canvas | css class of canvas |
40 | | color | string | #000 | primary drawing color |
41 | | fillColor | string | `""` | color used for filling items like circle or rectangle, empty string is no filling |
42 | | size | number | 5 | size of the item |
43 | | tool | string | TOOL_PENCIL | currently used tool from the map |
44 | | toolsMap | object | object map | keys are tool names, values are tool functions, by default Pencil, Line, Circle and Rectangle tools are available |
45 | | onItemStart | func | - | function to be executed on item start, most of the time first argument is item |
46 | | onEveryItemChange | func | - | function to be executed on item change, most of the time first argument is item, other arguments describe changes |
47 | | onDebouncedItemChange | func | - | function to be executed in interval on item change, most of the time first argument is item, other arguments describe batched changes |
48 | | onCompleteItem | func | - | function to be executed on item end, most of the time first argument is item |
49 | | debounceTime | number | 1000 | how often onDebouncedItemChange will be called |
50 |
--------------------------------------------------------------------------------
/circle.yml:
--------------------------------------------------------------------------------
1 | machine:
2 | node:
3 | version: 4.0
4 | environment:
5 | CONTINUOUS_INTEGRATION: true
6 |
7 | dependencies:
8 | cache_directories:
9 | - node_modules
10 | override:
11 | - npm prune && npm install
12 |
--------------------------------------------------------------------------------
/docs/frog.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/svrcekmichal/react-sketchpad/b4006c6fdddbdc52cecd6db4c9e4b511cca9ff80/docs/frog.gif
--------------------------------------------------------------------------------
/docs/react3.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/svrcekmichal/react-sketchpad/b4006c6fdddbdc52cecd6db4c9e4b511cca9ff80/docs/react3.gif
--------------------------------------------------------------------------------
/example/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets":["es2015","react"],
3 | "plugins":[
4 | "transform-class-properties",
5 | "transform-object-rest-spread"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/example/SketchExample.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { SketchPad, TOOL_PENCIL, TOOL_LINE, TOOL_RECTANGLE, TOOL_ELLIPSE } from './../src';
3 | import IO from 'socket.io-client'
4 |
5 | const wsClient = IO(`ws://127.0.0.1:12346`);
6 |
7 | export default class SketchExample extends Component
8 | {
9 | socket = null;
10 |
11 | constructor(props) {
12 | super(props);
13 |
14 | this.state = {
15 | tool:TOOL_PENCIL,
16 | size: 2,
17 | color: '#000000',
18 | fill: false,
19 | fillColor: '#444444',
20 | items: []
21 | }
22 | }
23 |
24 | componentDidMount() {
25 | wsClient.on('addItem', item => this.setState({items: this.state.items.concat([item])}));
26 | }
27 |
28 | render() {
29 | const { tool, size, color, fill, fillColor, items } = this.state;
30 | return (
31 |
32 |
React SketchPad
33 |
34 | wsClient.emit('addItem', i)}
44 | />
45 |
46 |
88 |
89 | );
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/example/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import SketchExample from './SketchExample';
4 |
5 | render((
6 |
7 | ), document.getElementById('app'));
8 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-sketchpad-example",
3 | "version": "0.0.1",
4 | "description": "Sketch pad created with canvas",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "node ./server.js",
8 | "build": "webpack"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/svrcekmichal/react-sketchpad.git"
13 | },
14 | "keywords": [],
15 | "author": "",
16 | "license": "MIT",
17 | "bugs": {
18 | "url": "https://github.com/svrcekmichal/react-sketchpad/issues"
19 | },
20 | "homepage": "https://github.com/svrcekmichal/react-sketchpad#readme",
21 | "dependencies": {
22 | "babel-cli": "^6.7.5",
23 | "babel-loader": "^6.2.4",
24 | "babel-plugin-transform-class-properties": "^6.6.0",
25 | "babel-plugin-transform-object-rest-spread": "^6.6.5",
26 | "babel-preset-es2015": "^6.6.0",
27 | "babel-preset-react": "^6.5.0",
28 | "react": "^0.14.0 || ^15.0.0",
29 | "react-dom": "^0.14.0 || ^15.0.0",
30 | "react-hot-loader": "^1.3.0",
31 | "socket.io-client": "^1.4.5",
32 | "uuid": "^2.0.0",
33 | "webpack": "^1.12.15",
34 | "webpack-dev-server": "^1.14.1"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/example/server.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 | var WebpackDevServer = require('webpack-dev-server');
3 | var config = require('./webpack.config');
4 |
5 | new WebpackDevServer(webpack(config), {
6 | publicPath: config.output.publicPath,
7 | hot: true,
8 | historyApiFallback: true,
9 | colors:true,
10 | contentBase: __dirname
11 | }).listen(12345, '127.0.0.1', function (err, result) {
12 | if (err) {
13 | return console.log(err);
14 | }
15 |
16 | console.log('Listening at http://127.0.0.1:12345/');
17 | });
18 |
--------------------------------------------------------------------------------
/example/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var webpack = require( "webpack" );
3 |
4 | module.exports = {
5 | devtool: 'eval',
6 | entry: {
7 | app: [
8 | 'webpack-dev-server/client?http://127.0.0.1:12345',
9 | 'webpack/hot/only-dev-server',
10 | path.join(__dirname, 'index.js')
11 | ]
12 | },
13 | output: {
14 | path: path.join(__dirname, 'build'),
15 | publicPath: '/build',
16 | filename: 'bundle.js'
17 | },
18 | plugins: [
19 | new webpack.HotModuleReplacementPlugin(),
20 | ],
21 | progress: true,
22 | module: {
23 | loaders: [{
24 | test: /\.jsx?$/,
25 | loaders: ['react-hot','babel'],
26 | exclude: path.join(__dirname, 'node_modules')
27 | }]
28 | },
29 | resolve: {
30 | root: path.join(__dirname, 'node_modules'),
31 | extensions: ['', '.js', '.jsx']
32 | },
33 | resolveLoader: {
34 | root: path.join(__dirname, 'node_modules')
35 | }
36 | };
37 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-sketchpad",
3 | "version": "0.0.3",
4 | "description": "Sketch pad created with canvas",
5 | "main": "index.js",
6 | "scripts": {
7 | "prepublish": "npm run lint && npm run build",
8 | "build": "babel ./src -d ./lib --ignore '__tests__'",
9 | "lint": "eslint -c .eslintrc src",
10 | "lint:fix": "eslint -c .eslintrc src --fix"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/svrcekmichal/react-sketchpad.git"
15 | },
16 | "keywords": [],
17 | "author": "",
18 | "license": "MIT",
19 | "bugs": {
20 | "url": "https://github.com/svrcekmichal/react-sketchpad/issues"
21 | },
22 | "homepage": "https://github.com/svrcekmichal/react-sketchpad#readme",
23 | "dependencies": {
24 | "react": "^0.14.0 || ^15.0.0",
25 | "react-dom": "^0.14.0 || ^15.0.0",
26 | "uuid": "^2.0.0"
27 | },
28 | "devDependencies": {
29 | "babel-cli": "^6.7.5",
30 | "babel-eslint": "^6.0.2",
31 | "babel-plugin-transform-class-properties": "^6.6.0",
32 | "babel-plugin-transform-object-rest-spread": "^6.6.5",
33 | "babel-preset-es2015": "^6.6.0",
34 | "babel-preset-react": "^6.5.0",
35 | "eslint": "^2.7.0",
36 | "eslint-config-airbnb": "^7.0.0",
37 | "eslint-plugin-import": "^1.4.0",
38 | "eslint-plugin-jsx-a11y": "^0.6.2",
39 | "eslint-plugin-react": "^4.3.0"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/SketchPad.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react';
2 | import { findDOMNode } from 'react-dom'
3 | import { Pencil, TOOL_PENCIL, Line, TOOL_LINE, Ellipse, TOOL_ELLIPSE, Rectangle, TOOL_RECTANGLE } from './tools'
4 |
5 | export const toolsMap = {
6 | [TOOL_PENCIL]: Pencil,
7 | [TOOL_LINE]: Line,
8 | [TOOL_RECTANGLE]: Rectangle,
9 | [TOOL_ELLIPSE]: Ellipse
10 | };
11 |
12 | export default class SketchPad extends Component {
13 |
14 | tool = null;
15 | interval = null;
16 |
17 | static propTypes = {
18 | width: PropTypes.number,
19 | height: PropTypes.number,
20 | items: PropTypes.array.isRequired,
21 | animate: PropTypes.bool,
22 | canvasClassName: PropTypes.string,
23 | color: PropTypes.string,
24 | fillColor: PropTypes.string,
25 | size: PropTypes.number,
26 | tool: PropTypes.string,
27 | toolsMap: PropTypes.object,
28 | onItemStart: PropTypes.func, // function(stroke:Stroke) { ... }
29 | onEveryItemChange: PropTypes.func, // function(idStroke:string, x:number, y:number) { ... }
30 | onDebouncedItemChange: PropTypes.func, // function(idStroke, points:Point[]) { ... }
31 | onCompleteItem: PropTypes.func, // function(stroke:Stroke) { ... }
32 | debounceTime: PropTypes.number,
33 | };
34 |
35 | static defaultProps = {
36 | width: 500,
37 | height: 500,
38 | color: '#000',
39 | size: 5,
40 | fillColor: '',
41 | canvasClassName: 'canvas',
42 | debounceTime: 1000,
43 | animate: true,
44 | tool: TOOL_PENCIL,
45 | toolsMap
46 | };
47 |
48 | constructor(props) {
49 | super(props);
50 | this.initTool = this.initTool.bind(this);
51 | this.onMouseDown = this.onMouseDown.bind(this);
52 | this.onMouseMove = this.onMouseMove.bind(this);
53 | this.onDebouncedMove = this.onDebouncedMove.bind(this);
54 | this.onMouseUp = this.onMouseUp.bind(this);
55 | }
56 |
57 | componentDidMount() {
58 | this.canvas = findDOMNode(this.canvasRef);
59 | this.ctx = this.canvas.getContext('2d');
60 | this.initTool(this.props.tool);
61 | }
62 |
63 | componentWillReceiveProps({tool, items}) {
64 | items
65 | .filter(item => this.props.items.indexOf(item) === -1)
66 | .forEach(item => {
67 | this.initTool(item.tool);
68 | this.tool.draw(item, this.props.animate);
69 | });
70 | this.initTool(tool);
71 | }
72 |
73 | initTool(tool) {
74 | this.tool = this.props.toolsMap[tool](this.ctx);
75 | }
76 |
77 | onMouseDown(e) {
78 | const data = this.tool.onMouseDown(...this.getCursorPosition(e), this.props.color, this.props.size, this.props.fillColor);
79 | data && data[0] && this.props.onItemStart && this.props.onItemStart.apply(null, data);
80 | if (this.props.onDebouncedItemChange) {
81 | this.interval = setInterval(this.onDebouncedMove, this.props.debounceTime);
82 | }
83 | }
84 |
85 | onDebouncedMove() {
86 | if (typeof this.tool.onDebouncedMouseMove == 'function' && this.props.onDebouncedItemChange) {
87 | this.props.onDebouncedItemChange.apply(null, this.tool.onDebouncedMouseMove());
88 | }
89 | }
90 |
91 | onMouseMove(e) {
92 | const data = this.tool.onMouseMove(...this.getCursorPosition(e));
93 | data && data[0] && this.props.onEveryItemChange && this.props.onEveryItemChange.apply(null, data);
94 | }
95 |
96 | onMouseUp(e) {
97 | const data = this.tool.onMouseUp(...this.getCursorPosition(e));
98 | data && data[0] && this.props.onCompleteItem && this.props.onCompleteItem.apply(null, data);
99 | if (this.props.onDebouncedItemChange) {
100 | clearInterval(this.interval);
101 | this.interval = null;
102 | }
103 | }
104 |
105 | getCursorPosition(e) {
106 | const {top, left} = this.canvas.getBoundingClientRect();
107 | return [
108 | e.clientX - left,
109 | e.clientY - top
110 | ];
111 | }
112 |
113 | render() {
114 | const {width, height, canvasClassName} = this.props;
115 | return (
116 |