├── .babelrc
├── .gitignore
├── README.md
├── dist
└── index.js
├── example
└── src
│ ├── card.jpg
│ ├── index.html
│ └── index.js
├── package.json
├── src
└── index.js
├── webpack.config.js
├── webpack.loaders.js
└── webpack.production.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "react",
4 | "es2015"
5 | ],
6 | "plugins": [
7 | "react-hot-loader/babel"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /example/dist
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-scratchcard
2 |
3 | A react component for displaying scratch card in your web app.
4 |
5 | ## Demo
6 |
7 | http://allu.io/react-scratchcard
8 |
9 | ## Installation
10 |
11 | ```
12 | $ npm install react-scratchcard
13 | ```
14 |
15 | ## Example
16 |
17 | ```javascript
18 | import React from 'react';
19 | import ScratchCard from 'react-scratchcard';
20 |
21 | const settings = {
22 | width: 640,
23 | height: 480,
24 | image: 'image.jpg',
25 | finishPercent: 50,
26 | onComplete: () => console.log('The card is now clear!')
27 | };
28 |
29 | const Example = () =>
30 |
31 | Congratulations! You WON!
32 | ;
33 | ```
34 |
35 | ## Credits
36 |
37 | Heavily inspired by [this pen by André Ruffert](https://codepen.io/andreruffert/pen/pvqly).
38 |
39 | Image used in the example by [webtreats](https://www.flickr.com/photos/webtreatsetc/).
40 |
--------------------------------------------------------------------------------
/dist/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 |
7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
8 |
9 | var _react = require('react');
10 |
11 | var _react2 = _interopRequireDefault(_react);
12 |
13 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
14 |
15 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
16 |
17 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
18 |
19 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
20 |
21 | var ScratchCard = function (_Component) {
22 | _inherits(ScratchCard, _Component);
23 |
24 | function ScratchCard(props) {
25 | _classCallCheck(this, ScratchCard);
26 |
27 | var _this = _possibleConstructorReturn(this, (ScratchCard.__proto__ || Object.getPrototypeOf(ScratchCard)).call(this, props));
28 |
29 | _this.state = { loaded: false };
30 | return _this;
31 | }
32 |
33 | _createClass(ScratchCard, [{
34 | key: 'componentDidMount',
35 | value: function componentDidMount() {
36 | var _this2 = this;
37 |
38 | this.isDrawing = false;
39 | this.lastPoint = null;
40 | this.ctx = this.canvas.getContext('2d');
41 |
42 | var image = new Image();
43 | image.crossOrigin = "Anonymous";
44 | image.onload = function () {
45 | _this2.ctx.drawImage(image, 0, 0);
46 | _this2.setState({ loaded: true });
47 | };
48 | image.src = this.props.image;
49 | }
50 | }, {
51 | key: 'getFilledInPixels',
52 | value: function getFilledInPixels(stride) {
53 | if (!stride || stride < 1) {
54 | stride = 1;
55 | }
56 |
57 | var pixels = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
58 | var total = pixels.data.length / stride;
59 | var count = 0;
60 |
61 | for (var i = 0; i < pixels.data.length; i += stride) {
62 | if (parseInt(pixels.data[i], 10) === 0) {
63 | count++;
64 | }
65 | }
66 |
67 | return Math.round(count / total * 100);
68 | }
69 | }, {
70 | key: 'getMouse',
71 | value: function getMouse(e, canvas) {
72 | var _canvas$getBoundingCl = canvas.getBoundingClientRect(),
73 | top = _canvas$getBoundingCl.top,
74 | left = _canvas$getBoundingCl.left;
75 |
76 | var scrollTop = window.pageYOffset || document.documentElement.scrollTop;
77 | var scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
78 |
79 | return {
80 | x: (e.pageX || e.touches[0].clientX) - left - scrollLeft,
81 | y: (e.pageY || e.touches[0].clientY) - top - scrollTop
82 | };
83 | }
84 | }, {
85 | key: 'distanceBetween',
86 | value: function distanceBetween(point1, point2) {
87 | return Math.sqrt(Math.pow(point2.x - point1.x, 2) + Math.pow(point2.y - point1.y, 2));
88 | }
89 | }, {
90 | key: 'angleBetween',
91 | value: function angleBetween(point1, point2) {
92 | return Math.atan2(point2.x - point1.x, point2.y - point1.y);
93 | }
94 | }, {
95 | key: 'handlePercentage',
96 | value: function handlePercentage() {
97 | var filledInPixels = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
98 |
99 | if (filledInPixels > this.props.finishPercent) {
100 | this.canvas.parentNode.removeChild(this.canvas);
101 | this.setState({ finished: true });
102 | if (this.props.onComplete) {
103 | this.props.onComplete();
104 | }
105 | }
106 | }
107 | }, {
108 | key: 'handleMouseDown',
109 | value: function handleMouseDown(e) {
110 | this.isDrawing = true;
111 | this.lastPoint = this.getMouse(e, this.canvas);
112 | }
113 | }, {
114 | key: 'handleMouseMove',
115 | value: function handleMouseMove(e) {
116 | if (!this.isDrawing) {
117 | return;
118 | }
119 |
120 | e.preventDefault();
121 |
122 | var currentPoint = this.getMouse(e, this.canvas);
123 | var distance = this.distanceBetween(this.lastPoint, currentPoint);
124 | var angle = this.angleBetween(this.lastPoint, currentPoint);
125 |
126 | var x = void 0,
127 | y = void 0;
128 |
129 | for (var i = 0; i < distance; i++) {
130 | x = this.lastPoint.x + Math.sin(angle) * i;
131 | y = this.lastPoint.y + Math.cos(angle) * i;
132 | this.ctx.globalCompositeOperation = 'destination-out';
133 | this.ctx.beginPath();
134 | this.ctx.arc(x, y, 25, 0, 2 * Math.PI, false);
135 | this.ctx.fill();
136 | }
137 |
138 | this.lastPoint = currentPoint;
139 | this.handlePercentage(this.getFilledInPixels(32));
140 | }
141 | }, {
142 | key: 'handleMouseUp',
143 | value: function handleMouseUp() {
144 | this.isDrawing = false;
145 | }
146 | }, {
147 | key: 'render',
148 | value: function render() {
149 | var _this3 = this;
150 |
151 | var containerStyle = {
152 | width: this.props.width + 'px',
153 | height: this.props.height + 'px',
154 | position: 'relative',
155 | WebkitUserSelect: 'none',
156 | MozUserSelect: 'none',
157 | msUserSelect: 'none',
158 | userSelect: 'none'
159 | };
160 |
161 | var canvasStyle = {
162 | position: 'absolute',
163 | top: 0,
164 | zIndex: 1
165 | };
166 |
167 | var resultStyle = {
168 | visibility: this.state.loaded ? 'visible' : 'hidden'
169 | };
170 |
171 | var canvasProps = {
172 | ref: function ref(_ref) {
173 | return _this3.canvas = _ref;
174 | },
175 | className: 'ScratchCard__Canvas',
176 | style: canvasStyle,
177 | width: this.props.width,
178 | height: this.props.height,
179 | onMouseDown: this.handleMouseDown.bind(this),
180 | onTouchStart: this.handleMouseDown.bind(this),
181 | onMouseMove: this.handleMouseMove.bind(this),
182 | onTouchMove: this.handleMouseMove.bind(this),
183 | onMouseUp: this.handleMouseUp.bind(this),
184 | onTouchEnd: this.handleMouseUp.bind(this)
185 | };
186 |
187 | return _react2.default.createElement(
188 | 'div',
189 | { className: 'ScratchCard__Container', style: containerStyle },
190 | _react2.default.createElement('canvas', canvasProps),
191 | _react2.default.createElement(
192 | 'div',
193 | { className: 'ScratchCard__Result', style: resultStyle },
194 | this.props.children
195 | )
196 | );
197 | }
198 | }]);
199 |
200 | return ScratchCard;
201 | }(_react.Component);
202 |
203 | ScratchCard.propTypes = {
204 | image: _react2.default.PropTypes.string.isRequired,
205 | width: _react2.default.PropTypes.number.isRequired,
206 | height: _react2.default.PropTypes.number.isRequired,
207 | finishPercent: _react2.default.PropTypes.number.isRequired,
208 | onComplete: _react2.default.PropTypes.func
209 | };
210 |
211 | var _default = ScratchCard;
212 | exports.default = _default;
213 | ;
214 |
215 | var _temp = function () {
216 | if (typeof __REACT_HOT_LOADER__ === 'undefined') {
217 | return;
218 | }
219 |
220 | __REACT_HOT_LOADER__.register(ScratchCard, 'ScratchCard', 'src/index.js');
221 |
222 | __REACT_HOT_LOADER__.register(_default, 'default', 'src/index.js');
223 | }();
224 |
225 | ;
--------------------------------------------------------------------------------
/example/src/card.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aleksik/react-scratchcard/a17d3dde75d2d85ac3551d4f49c9887b963eb693/example/src/card.jpg
--------------------------------------------------------------------------------
/example/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | <%= htmlWebpackPlugin.options.title %>
6 |
7 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/example/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 |
4 | import ScratchCard from '../../src';
5 | import cardImage from './card.jpg';
6 |
7 | const settings = {
8 | width: 640,
9 | height: 480,
10 | image: cardImage,
11 | finishPercent: 50,
12 | onComplete: () => console.log('The card is now clear!')
13 | };
14 |
15 | const Example = () =>
16 |
17 | Congratulations! You WON!
18 | ;
19 |
20 | render(, document.getElementById('root'));
21 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-scratchcard",
3 | "version": "1.1.2",
4 | "description": "React component for displaying scratch card in your web app.",
5 | "main": "dist/index.js",
6 | "scripts": {
7 | "build": "babel src -d dist",
8 | "start-example": "webpack-dev-server --progress --profile --colors",
9 | "build-example": "webpack --config webpack.production.config.js --progress --profile --colors"
10 | },
11 | "keywords": [
12 | "React",
13 | "Scratchcard"
14 | ],
15 | "author": "Aleksi Kaistinen (http://allu.io)",
16 | "license": "MIT",
17 | "repository": {
18 | "type": "git",
19 | "url": "https://github.com/aleksik/react-scratchcard.git"
20 | },
21 | "dependencies": {
22 | "react": "^15.0.0"
23 | },
24 | "devDependencies": {
25 | "babel-cli": "^6.16.0",
26 | "babel-core": "^6.17.0",
27 | "babel-loader": "^6.2.5",
28 | "babel-plugin-transform-class-properties": "^6.16.0",
29 | "babel-plugin-transform-decorators-legacy": "^1.3.4",
30 | "babel-plugin-transform-runtime": "^6.15.0",
31 | "babel-preset-es2015": "^6.16.0",
32 | "babel-preset-react": "^6.16.0",
33 | "css-loader": "^0.25.0",
34 | "extract-text-webpack-plugin": "^1.0.1",
35 | "file-loader": "^0.9.0",
36 | "html-webpack-plugin": "^2.22.0",
37 | "react-hot-loader": "^3.0.0-beta.5",
38 | "style-loader": "^0.13.1",
39 | "url-loader": "^0.5.7",
40 | "webpack": "^1.13.2",
41 | "webpack-cleanup-plugin": "^0.4.0",
42 | "webpack-dev-server": "^1.16.1",
43 | "react-dom": "^15.0.0"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | class ScratchCard extends Component {
4 |
5 | constructor(props) {
6 | super(props);
7 | this.state = { loaded: false }
8 | }
9 |
10 | componentDidMount() {
11 | this.isDrawing = false;
12 | this.lastPoint = null;
13 | this.ctx = this.canvas.getContext('2d');
14 |
15 | const image = new Image();
16 | image.crossOrigin = "Anonymous";
17 | image.onload = () => {
18 | this.ctx.drawImage(image, 0, 0);
19 | this.setState({ loaded: true });
20 | }
21 | image.src = this.props.image;
22 | }
23 |
24 | getFilledInPixels(stride) {
25 | if (!stride || stride < 1) {
26 | stride = 1;
27 | }
28 |
29 | const pixels = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
30 | const total = pixels.data.length / stride;
31 | let count = 0;
32 |
33 | for (let i = 0; i < pixels.data.length; i += stride) {
34 | if (parseInt(pixels.data[i], 10) === 0) {
35 | count++;
36 | }
37 | }
38 |
39 | return Math.round((count / total) * 100);
40 | }
41 |
42 | getMouse(e, canvas) {
43 | const {top, left} = canvas.getBoundingClientRect();
44 | const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
45 | const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
46 |
47 | return {
48 | x: (e.pageX || e.touches[0].clientX) - left - scrollLeft,
49 | y: (e.pageY || e.touches[0].clientY) - top - scrollTop
50 | }
51 | }
52 |
53 | distanceBetween(point1, point2) {
54 | return Math.sqrt(
55 | Math.pow(point2.x - point1.x, 2) + Math.pow(point2.y - point1.y, 2)
56 | );
57 | }
58 |
59 | angleBetween(point1, point2) {
60 | return Math.atan2(point2.x - point1.x, point2.y - point1.y);
61 | }
62 |
63 | handlePercentage(filledInPixels = 0) {
64 | if (filledInPixels > this.props.finishPercent) {
65 | this.canvas.parentNode.removeChild(this.canvas);
66 | this.setState({ finished: true });
67 | if (this.props.onComplete) {
68 | this.props.onComplete();
69 | }
70 | }
71 | }
72 |
73 | handleMouseDown(e) {
74 | this.isDrawing = true;
75 | this.lastPoint = this.getMouse(e, this.canvas);
76 | }
77 |
78 | handleMouseMove(e) {
79 | if (!this.isDrawing) {
80 | return;
81 | }
82 |
83 | e.preventDefault();
84 |
85 | const currentPoint = this.getMouse(e, this.canvas);
86 | const distance = this.distanceBetween(this.lastPoint, currentPoint);
87 | const angle = this.angleBetween(this.lastPoint, currentPoint);
88 |
89 | let x, y;
90 |
91 | for (let i = 0; i < distance; i++) {
92 | x = this.lastPoint.x + (Math.sin(angle) * i);
93 | y = this.lastPoint.y + (Math.cos(angle) * i);
94 | this.ctx.globalCompositeOperation = 'destination-out';
95 | this.ctx.beginPath();
96 | this.ctx.arc(x, y, 25, 0, 2 * Math.PI, false);
97 | this.ctx.fill();
98 | }
99 |
100 | this.lastPoint = currentPoint;
101 | this.handlePercentage(this.getFilledInPixels(32));
102 |
103 | }
104 |
105 | handleMouseUp() {
106 | this.isDrawing = false;
107 | }
108 |
109 | render() {
110 |
111 | const containerStyle = {
112 | width: this.props.width + 'px',
113 | height: this.props.height + 'px',
114 | position: 'relative',
115 | WebkitUserSelect: 'none',
116 | MozUserSelect: 'none',
117 | msUserSelect: 'none',
118 | userSelect: 'none'
119 | }
120 |
121 | const canvasStyle = {
122 | position: 'absolute',
123 | top: 0,
124 | zIndex: 1
125 | }
126 |
127 | const resultStyle = {
128 | visibility: this.state.loaded ? 'visible' : 'hidden'
129 | }
130 |
131 | const canvasProps = {
132 | ref: (ref) => this.canvas = ref,
133 | className: 'ScratchCard__Canvas',
134 | style: canvasStyle,
135 | width: this.props.width,
136 | height: this.props.height,
137 | onMouseDown: this.handleMouseDown.bind(this),
138 | onTouchStart: this.handleMouseDown.bind(this),
139 | onMouseMove: this.handleMouseMove.bind(this),
140 | onTouchMove: this.handleMouseMove.bind(this),
141 | onMouseUp: this.handleMouseUp.bind(this),
142 | onTouchEnd: this.handleMouseUp.bind(this)
143 | }
144 |
145 | return (
146 |
147 |
148 |
149 | {this.props.children}
150 |
151 |
152 | );
153 | }
154 |
155 | }
156 |
157 | ScratchCard.propTypes = {
158 | image: React.PropTypes.string.isRequired,
159 | width: React.PropTypes.number.isRequired,
160 | height: React.PropTypes.number.isRequired,
161 | finishPercent: React.PropTypes.number.isRequired,
162 | onComplete: React.PropTypes.func
163 | }
164 |
165 | export default ScratchCard;
166 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var webpack = require('webpack');
4 | var path = require('path');
5 | var loaders = require('./webpack.loaders');
6 | var HtmlWebpackPlugin = require('html-webpack-plugin');
7 |
8 | const HOST = process.env.HOST || '127.0.0.1';
9 | const PORT = process.env.PORT || '8888';
10 |
11 | loaders.push({
12 | test: /[\/\\]src[\/\\].*\.css/,
13 | exclude: /(node_modules|public)/,
14 | loaders: [
15 | 'style?sourceMap',
16 | 'css?modules&importLoaders=1&localIdentName=[path]___[name]__[local]___[hash:base64:5]'
17 | ]
18 | });
19 |
20 | module.exports = {
21 | entry: [
22 | `webpack-dev-server/client?http://${HOST}:${PORT}`,
23 | `webpack/hot/only-dev-server`,
24 | `./example/src/index.js`
25 | ],
26 | devtool: process.env.WEBPACK_DEVTOOL || 'cheap-module-source-map',
27 | output: {
28 | path: path.join(__dirname, 'example/dist'),
29 | filename: 'bundle.js'
30 | },
31 | resolve: {
32 | extensions: ['', '.js']
33 | },
34 | module: {
35 | loaders
36 | },
37 | devServer: {
38 | contentBase: './example/dist',
39 | noInfo: true,
40 | hot: true,
41 | inline: true,
42 | historyApiFallback: true,
43 | port: PORT,
44 | host: HOST
45 | },
46 | plugins: [
47 | new webpack.NoErrorsPlugin(),
48 | new webpack.HotModuleReplacementPlugin(),
49 | new HtmlWebpackPlugin({
50 | template: './example/src/index.html',
51 | title: 'react-scratchcard example'
52 | })
53 | ]
54 | }
55 |
--------------------------------------------------------------------------------
/webpack.loaders.js:
--------------------------------------------------------------------------------
1 | module.exports = [
2 | {
3 | test: /\.js?$/,
4 | exclude: /(node_modules|bower_components|public)/,
5 | loader: 'babel',
6 | query: {
7 | presets: ['es2015', 'react'],
8 | plugins: ['transform-runtime', 'transform-decorators-legacy', 'transform-class-properties'],
9 | }
10 | },
11 | {
12 | test: /\.jpg/,
13 | exclude: /(node_modules|bower_components)/,
14 | loader: "url-loader?limit=10000&mimetype=image/jpg"
15 | }
16 | ]
17 |
--------------------------------------------------------------------------------
/webpack.production.config.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 | var path = require('path');
3 | var loaders = require('./webpack.loaders');
4 | var ExtractTextPlugin = require('extract-text-webpack-plugin');
5 | var HtmlWebpackPlugin = require('html-webpack-plugin');
6 | var WebpackCleanupPlugin = require('webpack-cleanup-plugin');
7 |
8 | loaders.push({
9 | test: /[\/\\]src[\/\\].*\.css/,
10 | exclude: /(node_modules|bower_components|public)/,
11 | loader: ExtractTextPlugin.extract('style', 'css?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]')
12 | });
13 |
14 |
15 | module.exports = {
16 | entry: [
17 | './example/src/index.js'
18 | ],
19 | output: {
20 | path: path.join(__dirname, 'example/dist'),
21 | filename: '[chunkhash].js'
22 | },
23 | resolve: {
24 | extensions: ['', '.js']
25 | },
26 | module: {
27 | loaders
28 | },
29 | plugins: [
30 | new WebpackCleanupPlugin(),
31 | new webpack.DefinePlugin({
32 | 'process.env': {
33 | NODE_ENV: '"production"'
34 | }
35 | }),
36 | new webpack.optimize.UglifyJsPlugin({
37 | compress: {
38 | warnings: false,
39 | screw_ie8: true,
40 | drop_console: true,
41 | drop_debugger: true
42 | }
43 | }),
44 | new webpack.optimize.OccurenceOrderPlugin(),
45 | new ExtractTextPlugin('[contenthash].css', {
46 | allChunks: true
47 | }),
48 | new HtmlWebpackPlugin({
49 | template: './example/src/index.html',
50 | title: 'react-scratchcard example'
51 | }),
52 | new webpack.optimize.DedupePlugin()
53 | ]
54 | };
55 |
--------------------------------------------------------------------------------