├── .gitignore ├── _config.yml ├── .npmignore ├── .babelrc ├── example ├── index.html ├── index.scss └── index.jsx ├── dist ├── index.html ├── css │ └── index.8c74a543.css └── index.a1fb715b.js ├── src ├── index.less └── index.tsx ├── tsconfig.example.json ├── tsconfig.prod.json ├── tsconfig.dev.json ├── webpack.config.prod.js ├── package.json ├── lib ├── index.d.ts └── index.js ├── webpack.config.dev.js ├── webpack.config.example.js └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | example 4 | webpack.config.dev.js 5 | webpack.config.example.js 6 | tsconfig.dev.json 7 | tsconfig.example.json -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react", 4 | ["@babel/preset-env", { "targets": "> 2%, ie 11, safari > 9" }] 5 | 6 | ], 7 | "plugins": [ 8 | "@babel/plugin-proposal-class-properties" 9 | ] 10 | } -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 刮一刮 8 | 9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 刮一刮 8 | 9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /src/index.less: -------------------------------------------------------------------------------- 1 | .___scratch{ 2 | position: relative; 3 | display: inline-block; 4 | user-select: none; 5 | font-size: 0; 6 | &.frozen{ 7 | pointer-events: none; 8 | } 9 | .___content{ 10 | opacity: 0; 11 | &.visible{ 12 | opacity: 1; 13 | } 14 | } 15 | canvas{ 16 | position: absolute; 17 | top: 0; 18 | right: 0; 19 | bottom: 0; 20 | left: 0; 21 | height: 100%; 22 | width: 100%; 23 | } 24 | } -------------------------------------------------------------------------------- /tsconfig.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es6", "dom", "scripthost", "es2015.symbol"], 4 | "target": "es6", 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "importHelpers": true, 8 | "jsx": "preserve", 9 | "esModuleInterop": true, 10 | "allowUnreachableCode": true, 11 | "allowUnusedLabels": true, 12 | "baseUrl": ".", 13 | "removeComments": true, 14 | "paths": { 15 | "@/": ["./src/"] 16 | }, 17 | "allowSyntheticDefaultImports": true, 18 | "experimentalDecorators": true, 19 | "include": ["./example/*"], 20 | "exclude": ["node_modules", "*.d.ts"] 21 | }, 22 | "awesomeTypescriptLoaderOptions": { 23 | "errorsAsWarnings": true 24 | } 25 | } -------------------------------------------------------------------------------- /tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es6", "dom", "scripthost", "es2015.symbol"], 4 | "target": "es5", 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "importHelpers": true, 8 | "jsx": "react", 9 | "esModuleInterop": true, 10 | "allowUnreachableCode": true, 11 | "allowUnusedLabels": true, 12 | "baseUrl": ".", 13 | "declaration": true, 14 | "declarationDir": "lib", 15 | "paths": { 16 | "@/": ["./src/"] 17 | }, 18 | "allowSyntheticDefaultImports": true, 19 | "experimentalDecorators": true, 20 | "include": ["./src/*"], 21 | "exclude": ["node_modules", "*.d.ts"] 22 | }, 23 | "awesomeTypescriptLoaderOptions": { 24 | "errorsAsWarnings": true 25 | } 26 | } -------------------------------------------------------------------------------- /tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es6", "dom", "scripthost", "es2015.symbol"], 4 | "target": "es6", 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "importHelpers": true, 8 | "jsx": "preserve", 9 | "esModuleInterop": true, 10 | "sourceMap": true, 11 | "allowUnreachableCode": true, 12 | "allowUnusedLabels": true, 13 | "inlineSources": true, 14 | "baseUrl": ".", 15 | "removeComments": true, 16 | "paths": { 17 | "@/": ["./src/"] 18 | }, 19 | "allowSyntheticDefaultImports": true, 20 | "experimentalDecorators": true, 21 | "include": ["./example/*"], 22 | "exclude": ["node_modules", "*.d.ts"] 23 | }, 24 | "awesomeTypescriptLoaderOptions": { 25 | "errorsAsWarnings": true 26 | } 27 | } -------------------------------------------------------------------------------- /webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const nodeExternals = require('webpack-node-externals'); 3 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 4 | const autoprefixer = require('autoprefixer'); 5 | module.exports = { 6 | mode: 'production', 7 | 8 | entry: { 9 | index: './src/index.tsx' 10 | }, 11 | 12 | output: { 13 | filename: '[name].js', 14 | path: path.resolve(__dirname, "lib"), 15 | libraryTarget: 'commonjs2' 16 | }, 17 | 18 | module: { 19 | rules: [ 20 | { test: /\.css$/, use: ['style-loader', 'css-loader', {loader: 'postcss-loader', options: { plugins: [autoprefixer()]}}] }, 21 | { test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader', {loader: 'postcss-loader', options: { plugins: [autoprefixer()]}}] }, 22 | { test: /\.less$/, use: ['style-loader', 'css-loader', 'less-loader', {loader: 'postcss-loader', options: { plugins: [autoprefixer()]}}] }, 23 | { test: /\.(jpg|png|gif|bmp|jpeg|ttf|eot|svg|woff|woff2)$/, use: [{loader: 'url-loader', options: {limit:1024, name:'assets/[name]-[hash:8].[ext]'}}] }, 24 | { test: /\.(j|t)sx?$/, use: 'awesome-typescript-loader?configFileName=tsconfig.prod.json', exclude: /node_modules/ } 25 | ] 26 | }, 27 | 28 | externals: [nodeExternals()], 29 | 30 | resolve:{ 31 | extensions: [".js", ".jsx", ".ts", ".tsx", "json", "*"] 32 | }, 33 | 34 | plugins: [ 35 | new CleanWebpackPlugin() 36 | ] 37 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-scratch-perfect", 3 | "version": "2.0.1", 4 | "description": "刮一刮", 5 | "main": "lib/index.js", 6 | "directories": { 7 | "example": "example", 8 | "lib": "lib" 9 | }, 10 | "scripts": { 11 | "build": "webpack --config webpack.config.prod.js", 12 | "start": "cross-env NODE_ENV=development webpack-dev-server --config webpack.config.dev.js --hot --inline", 13 | "example": "cross-env NODE_ENV=production webpack --config webpack.config.example.js" 14 | }, 15 | "author": "dsmelon", 16 | "license": "ISC", 17 | "devDependencies": { 18 | "@babel/cli": "^7.6.4", 19 | "@babel/core": "^7.6.4", 20 | "@babel/plugin-proposal-class-properties": "^7.5.5", 21 | "@babel/preset-env": "^7.6.3", 22 | "@babel/preset-react": "^7.6.3", 23 | "@babel/runtime": "^7.6.3", 24 | "@types/react": "^16.9.11", 25 | "@types/react-dom": "^16.9.4", 26 | "autoprefixer": "^9.7.1", 27 | "awesome-typescript-loader": "^5.2.1", 28 | "babel-loader": "^8.0.0-beta.0", 29 | "clean-webpack-plugin": "^3.0.0", 30 | "cross-env": "^6.0.3", 31 | "css-loader": "^3.2.0", 32 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 33 | "file-loader": "^4.2.0", 34 | "html-webpack-plugin": "^3.2.0", 35 | "less": "^3.10.3", 36 | "less-loader": "^5.0.0", 37 | "node-sass": "^4.13.0", 38 | "npm": "^6.12.1", 39 | "postcss-loader": "^3.0.0", 40 | "react": "^16.11.0", 41 | "react-dom": "^16.11.0", 42 | "sass-loader": "^8.0.0", 43 | "style-loader": "^1.0.0", 44 | "typescript": "^3.7.2", 45 | "url-loader": "^2.2.0", 46 | "webpack": "^4.41.2", 47 | "webpack-cli": "^3.3.10", 48 | "webpack-dev-server": "^3.9.0", 49 | "webpack-node-externals": "^1.7.2" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import './index.less'; 3 | interface props { 4 | clear?: boolean; 5 | color?: string; 6 | className?: string; 7 | size?: number; 8 | onSuccess?: () => void; 9 | onChange?: (p: number) => void; 10 | imgRepeat?: "width" | "height" | "repeat"; 11 | round?: number[]; 12 | img?: string; 13 | mode?: "move" | "end"; 14 | percentage?: number; 15 | } 16 | interface state { 17 | ch: number; 18 | cw: number; 19 | isSuccess: boolean; 20 | visible: boolean; 21 | } 22 | export default class extends PureComponent { 23 | state: { 24 | ch: number; 25 | cw: number; 26 | isSuccess: boolean; 27 | visible: boolean; 28 | }; 29 | image: HTMLImageElement; 30 | wrap: HTMLDivElement; 31 | canvas: HTMLCanvasElement; 32 | ptg: number; 33 | size: number; 34 | round: number[]; 35 | cells: boolean[]; 36 | roundX: number[]; 37 | roundY: number[]; 38 | cellX: number; 39 | cellY: number; 40 | sum: number; 41 | roundLen: number; 42 | offsetLeft: number; 43 | offsetTop: number; 44 | preX: number; 45 | preY: number; 46 | ctx: CanvasRenderingContext2D; 47 | componentDidMount(): void; 48 | init: (bol: boolean) => void; 49 | down: (e: React.MouseEvent & React.TouchEvent) => void; 50 | move: (e: MouseEvent & TouchEvent) => void; 51 | up: (e: TouchEvent & MouseEvent, bol?: boolean) => void; 52 | addRound: (preX: number, preY: number, curX: number, curY: number) => void; 53 | handleRound: (curX: number, curY: number) => void; 54 | checkRound: (e: TouchEvent & MouseEvent) => void; 55 | render(): JSX.Element; 56 | } 57 | export {}; 58 | -------------------------------------------------------------------------------- /dist/css/index.8c74a543.css: -------------------------------------------------------------------------------- 1 | .___scratch { 2 | position: relative; 3 | display: inline-block; 4 | -webkit-user-select: none; 5 | -moz-user-select: none; 6 | -ms-user-select: none; 7 | user-select: none; 8 | font-size: 0; 9 | } 10 | .___scratch.frozen { 11 | pointer-events: none; 12 | } 13 | .___scratch .___content { 14 | opacity: 0; 15 | } 16 | .___scratch .___content.visible { 17 | opacity: 1; 18 | } 19 | .___scratch canvas { 20 | position: absolute; 21 | top: 0; 22 | right: 0; 23 | bottom: 0; 24 | left: 0; 25 | height: 100%; 26 | width: 100%; 27 | } 28 | *{padding:0;margin:0;box-sizing:content-box}body,html{background-color:#fdfdfd;height:100%;width:100%}.wrap{width:1024px;margin:0 auto;text-align:center}.wrap .left{vertical-align:top}.wrap .left .s1{font-size:100px;line-height:320px;height:320px;width:450px;color:deeppink;display:inline-block}.wrap .left .ppp{position:absolute;right:0;top:0;font-size:16px;z-index:10;padding:2px 5px;color:white;text-shadow:1px 1px black}.wrap .right{margin-left:20px;display:inline-block;vertical-align:top;text-align:left;line-height:2.5;font-size:16px;color:#333}.wrap .right p{display:inline-block;width:220px;text-align:right}.wrap .right input,.wrap .right select{height:22px;padding:0 10px;background-color:white;outline:none;margin-left:30px;vertical-align:middle;width:250px}.wrap .right input[type=checkbox]{height:22px;width:22px}.wrap .right span{color:deeppink}.wrap .doc{text-align:center;font-size:14px;text-shadow:0px 0px 1.5px;margin-top:20px}.wrap .doc table{width:100%;border:1px solid green;border-collapse:collapse}.wrap .doc table tr{color:darkviolet;line-height:2;text-align:left}.wrap .doc table tr td{border:1px solid green;padding:0 10px;box-shadow:0 0 6px 0 #888 inset}.wrap .doc table tr:first-child{color:deeppink;font-size:16px;text-align:center} 29 | -------------------------------------------------------------------------------- /webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const htmlWebpackPlugin = require('html-webpack-plugin'); 3 | const autoprefixer = require('autoprefixer'); 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 5 | module.exports = { 6 | mode: 'development', 7 | 8 | entry: { 9 | index: './example/index.jsx' 10 | }, 11 | 12 | output: { 13 | filename: 'bundle.js', 14 | path: path.resolve(__dirname, "dist") 15 | }, 16 | 17 | module: { 18 | rules: [ 19 | { test: /\.css$/, use: ExtractTextPlugin.extract({fallback: 'style-loader', use: ['css-loader', {loader: 'postcss-loader', options: { plugins: [autoprefixer()]}}]}) }, 20 | { test: /\.scss$/, use: ExtractTextPlugin.extract({fallback: 'style-loader', use: ['css-loader', 'sass-loader', {loader: 'postcss-loader', options: { plugins: [autoprefixer()]}}]}) }, 21 | { test: /\.less$/, use: ExtractTextPlugin.extract({fallback: 'style-loader', use: ['css-loader', 'less-loader', {loader: 'postcss-loader', options: { plugins: [autoprefixer()]}}]}) }, 22 | { test: /\.(jpg|png|gif|bmp|jpeg|ttf|eot|svg|woff|woff2)$/, use: [{loader: "url-loader", options: {limit:10240, name: 'assets/[name]-[hash:8].[ext]'}}] }, 23 | { test: /\.jsx?$/, use: 'babel-loader', exclude: /node_modules/ }, 24 | { test: /\.tsx?$/, use: ['babel-loader', 'awesome-typescript-loader?configFileName=tsconfig.dev.json'], exclude: /node_modules/ } 25 | ] 26 | }, 27 | 28 | resolve:{ 29 | extensions: [".js", ".jsx", ".ts", ".tsx", "json", "*"] 30 | }, 31 | 32 | devServer: { 33 | port: 8000, 34 | open: true, 35 | hot: true, 36 | host: "localhost", 37 | stats: "errors-only" 38 | }, 39 | 40 | devtool: 'inline-source-map', 41 | 42 | watchOptions: { 43 | ignored: /node_modules/ 44 | }, 45 | 46 | plugins: [ 47 | new ExtractTextPlugin({ 48 | filename: 'css/[name].[md5:contenthash:hex:8].css', 49 | allChunks: false 50 | }), 51 | new htmlWebpackPlugin({ 52 | template: 'example/index.html', 53 | hash: false 54 | }) 55 | ] 56 | } -------------------------------------------------------------------------------- /example/index.scss: -------------------------------------------------------------------------------- 1 | *{ 2 | padding: 0; 3 | margin: 0; 4 | box-sizing: content-box; 5 | } 6 | body,html{ 7 | background-color: #fdfdfd; 8 | height: 100%; 9 | width: 100%; 10 | } 11 | .wrap{ 12 | width: 1024px; 13 | margin: 0 auto; 14 | text-align: center; 15 | .left{ 16 | vertical-align: top; 17 | .s1{ 18 | font-size: 100px; 19 | line-height: 320px; 20 | height: 320px; 21 | width: 450px; 22 | color: deeppink; 23 | display: inline-block; 24 | } 25 | .ppp{ 26 | position: absolute; 27 | right: 0; 28 | top: 0; 29 | font-size: 16px; 30 | z-index: 10; 31 | padding: 2px 5px; 32 | color: white; 33 | text-shadow: 1px 1px black; 34 | } 35 | } 36 | .right{ 37 | margin-left: 20px; 38 | display: inline-block; 39 | vertical-align: top; 40 | text-align: left; 41 | line-height: 2.5; 42 | font-size: 16px; 43 | color: #333; 44 | p{ 45 | display: inline-block; 46 | width: 220px; 47 | text-align: right; 48 | } 49 | input, select{ 50 | height: 22px; 51 | padding: 0 10px; 52 | background-color: white; 53 | outline: none; 54 | margin-left: 30px; 55 | vertical-align: middle; 56 | width: 250px; 57 | } 58 | input[type=checkbox]{ 59 | height: 22px; 60 | width: 22px; 61 | } 62 | span{ 63 | color: deeppink; 64 | } 65 | } 66 | .doc{ 67 | text-align: center; 68 | font-size: 14px; 69 | text-shadow: 1px 1px 1.5px rgba(0,0,0,0.5); 70 | margin-top: 20px; 71 | table{ 72 | width: 100%; 73 | border: 1px solid green; 74 | border-collapse: collapse; 75 | tr{ 76 | color: darkviolet; 77 | line-height: 2; 78 | text-align: left; 79 | td{ 80 | border: 1px solid green; 81 | padding: 0 10px; 82 | box-shadow: 0 0 2px 0 #888 inset; 83 | } 84 | &:first-child{ 85 | color: deeppink; 86 | font-size: 16px; 87 | text-align: center; 88 | } 89 | } 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /webpack.config.example.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 3 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 4 | const htmlWebpackPlugin = require('html-webpack-plugin'); 5 | const autoprefixer = require('autoprefixer'); 6 | module.exports = { 7 | mode: 'production', 8 | 9 | entry: { 10 | index: './example/index.jsx' 11 | }, 12 | 13 | output: { 14 | filename: '[name].[chunkhash:8].js', 15 | path: path.resolve(__dirname, 'dist') 16 | }, 17 | 18 | module: { 19 | rules: [ 20 | { test: /\.css$/, use: ExtractTextPlugin.extract({fallback: 'style-loader', use: ['css-loader', {loader: 'postcss-loader', options: { plugins: [autoprefixer()]}}]}) }, 21 | { test: /\.scss$/, use: ExtractTextPlugin.extract({fallback: 'style-loader', use: ['css-loader', 'sass-loader', {loader: 'postcss-loader', options: { plugins: [autoprefixer()]}}]}) }, 22 | { test: /\.less$/, use: ExtractTextPlugin.extract({fallback: 'style-loader', use: ['css-loader', 'less-loader', {loader: 'postcss-loader', options: { plugins: [autoprefixer()]}}]}) }, 23 | { test: /\.(jpg|png|gif|bmp|jpeg|ttf|eot|svg|woff|woff2)$/, use: [{loader: 'url-loader', options: {limit:1024, name:'assets/[name]-[hash:8].[ext]'}}] }, 24 | { test: /\.jsx?$/, use: 'babel-loader', exclude: /node_modules/ }, 25 | { test: /\.tsx?$/, use: ['babel-loader', 'awesome-typescript-loader?configFileName=tsconfig.example.json'], exclude: /node_modules/ } 26 | ] 27 | }, 28 | devtool: false, 29 | 30 | optimization: { 31 | splitChunks: { 32 | chunks: 'async', 33 | minSize: 30000, 34 | maxSize: 0, 35 | minChunks: 1, 36 | maxAsyncRequests: 5, 37 | maxInitialRequests: 3, 38 | automaticNameDelimiter: '.', 39 | automaticNameMaxLength: 30, 40 | name: true, 41 | cacheGroups: { 42 | vendors: { 43 | test: /[\\/]node_modules[\\/]/, 44 | priority: -10 45 | }, 46 | default: { 47 | minChunks: 2, 48 | priority: -20, 49 | reuseExistingChunk: true 50 | } 51 | } 52 | } 53 | }, 54 | 55 | resolve:{ 56 | extensions: [".js", ".jsx", ".ts", ".tsx", "json", "*"] 57 | }, 58 | 59 | plugins: [ 60 | new CleanWebpackPlugin(), 61 | new ExtractTextPlugin({ 62 | filename: 'css/[name].[md5:contenthash:hex:8].css', 63 | allChunks: false 64 | }), 65 | new htmlWebpackPlugin({ 66 | template: 'example/index.html', 67 | hash: false 68 | }) 69 | ] 70 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # 欢迎使用react刮一刮组件 2 | 3 | 此组件使用图片时未使用getImageData方法,没有跨域问题,使用简单,适合多场景使用 4 | 5 | 安装 6 | ```cmd 7 | npm install react-scratch-perfect --save 8 | ``` 9 | 10 | 例子: [https://dsmelon.github.io/react-scratch-perfect/dist/index.html](https://dsmelon.github.io/react-scratch-perfect/dist/index.html) 11 | 12 | **api** 13 | 14 | | 参数名 | 类型 | 默认值 | 说明 | 值 | 15 | |:------------|:---------------------|:-------------|:--------------------------------------|:-------------------------------------------------| 16 | | className | string | 无 | 容器的类名 | | 17 | | clear | boolean | false | 完成后是否清除画布 | | 18 | | color | string | #808080 | 刮刮卡的颜色 | | 19 | | img | string | 无 | 刮刮卡的填充图片(如果图片加载失败,会使用颜色值)| | 20 | | imgRepeat | string | 无 | 图片填充方式 | width: 宽度撑满,高度自适应并居中
height: 高度撑满,宽度自适应并居中
repeat: 重复填充
无值会被拉伸铺满容器 | 21 | | size | number | 1/10容器宽度 | 画笔直径 | | 22 | | round | array\[number\] | \[0,0,0,0\] | 奖品限定区域,分别为上右下左的padding值 | | 23 | | percentage | number | 70 | 完成百分比(round之外的不参与计算) | | 24 | | mode |string | move | 在什么时刻触发onChange和onSuccess | move: 手指移动时触发onChange和onSuccess
end: 手指抬起时触发onChange和onSuccess | 25 | | onChange | function(percentage) | 无 | 改变时触发的函数,回传的是已经刮出的百分比 | | 26 | | onSuccess | function | 无 | 完成时的回调 | | 27 | 28 | 使用方法 29 | ```jsx 30 | 43 |
一等奖
44 |
45 | ``` 46 | ```css 47 | .s1{ 48 | font-size: 100px; 49 | line-height: 320px; 50 | height: 320px; 51 | width: 450px; 52 | color: deeppink; 53 | display: inline-block; 54 | } 55 | ``` 56 | github地址: [https://github.com/dsmelon/react-scratch-perfect](https://github.com/dsmelon/react-scratch-perfect) -------------------------------------------------------------------------------- /example/index.jsx: -------------------------------------------------------------------------------- 1 | import React, {PureComponent} from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import Scratch from '../src/index'; 4 | import './index.scss'; 5 | 6 | export default class App extends PureComponent{ 7 | state = { 8 | mode: "move", 9 | p: 0, 10 | color: "#808080", 11 | img: "", 12 | round: [114,77,114,77], 13 | size: 40, 14 | clear: false, 15 | imgRepeat: "", 16 | key: 0, 17 | percentage: 70 18 | } 19 | timer = -1; 20 | handleChange = (p) => { 21 | this.setState({p}) 22 | } 23 | onSuccess = () => { 24 | alert("success"); 25 | } 26 | change = e => { 27 | const {name, value, checked} = e.currentTarget; 28 | switch(name){ 29 | case "clear": 30 | this.setState({[name]: checked, key: ++this.state.key}); 31 | break; 32 | case "size": 33 | case "img": 34 | case "percentage": 35 | clearTimeout(this.timer); 36 | this.timer = setTimeout(()=>{ 37 | this.setState({[name]: value, key: ++this.state.key}); 38 | },400) 39 | break; 40 | case "round": 41 | clearTimeout(this.timer); 42 | this.timer = setTimeout(()=>{ 43 | this.setState({[name]: value ? value.split(",").map(_=>+_) : [], key: ++this.state.key}); 44 | },400) 45 | break; 46 | default: 47 | this.setState({[name]: value, key: ++this.state.key}); 48 | break; 49 | } 50 | this.setState({p: 0}); 51 | } 52 | render(){ 53 | return
54 | 68 |
一等奖
69 |

{this.state.p}

70 |
71 |
72 |

颜色( color )

:
73 |

图片( img )

:
74 |

图片填充方式( imgRepeat )

:
80 |

范围( round )

:
81 |

模式( mode )

:
85 |

画笔大小( size )

:
86 |

完成百分比( percentage )

:
87 |

完成后清除画布( clear )

: 88 |
89 |
90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 104 | 105 | 106 | 107 | 108 | 109 | 113 | 114 | 115 | 116 | 117 |
参数名类型默认值说明
classNamestring容器的类名{"\u3000"}
clearbooleanfalse完成后是否清除画布{"\u3000"}
colorstring#808080刮刮卡的颜色{"\u3000"}
imgstring刮刮卡的填充图片{"\u3000"}
imgRepeatstring图片填充方式 99 | width: 宽度撑满,高度自适应并居中
100 | height: 高度撑满,宽度自适应并居中
101 | repeat: 重复填充
102 | 无值或者其他值会被拉伸铺满容器 103 |
sizenumber1/10容器宽度画笔直径{"\u3000"}
roundarray[number][0,0,0,0]奖品限定区域,分别为上右下左的padding值{"\u3000"}
percentagenumber70完成百分比(round之外的不参与计算){"\u3000"}
modestringmove在什么时刻触发onChange和onSuccess 110 | move: 手指移动时触发onChange和onSuccess
111 | end: 手指抬起时触发onChange和onSuccess 112 |
onChangefunction(percentage)改变时触发的函数,回传的是已经刮出的百分比{"\u3000"}
onSuccessfunction完成时的回调{"\u3000"}
118 |
119 |
120 | } 121 | } 122 | 123 | ReactDom.render(, document.getElementById("root")); -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import './index.less'; 3 | const dpr = window.devicePixelRatio === 1 ? 2 : window.devicePixelRatio; 4 | const preventDefault = e => e && e.preventDefault(); 5 | //判断是否支持快速滚动 6 | let support = false; 7 | try { 8 | let options = Object.defineProperty({}, "passive", { 9 | get: () => support = true 10 | }); 11 | window.addEventListener("test", null, options); 12 | } catch(err) {} 13 | 14 | interface props{ 15 | clear?: boolean; 16 | color?: string; 17 | className?: string; 18 | size?: number; 19 | onSuccess?: () => void; 20 | onChange?: (p:number) => void; 21 | imgRepeat?: "width" | "height" | "repeat"; 22 | round?: number[]; 23 | img?: string; 24 | mode?: "move" | "end"; 25 | percentage?: number; 26 | } 27 | 28 | interface state{ 29 | ch: number; 30 | cw: number; 31 | isSuccess: boolean; 32 | visible: boolean; 33 | } 34 | 35 | export default class extends PureComponent{ 36 | state = { 37 | ch: 0, 38 | cw: 0, 39 | isSuccess: false, 40 | visible: false 41 | }; 42 | image: HTMLImageElement; 43 | wrap: HTMLDivElement; 44 | canvas: HTMLCanvasElement; 45 | ptg: number; 46 | size: number; 47 | round: number[]; 48 | cells: boolean[]; 49 | roundX: number[]; 50 | roundY: number[]; 51 | cellX: number; 52 | cellY: number; 53 | sum: number; 54 | roundLen: number; 55 | offsetLeft: number; 56 | offsetTop: number; 57 | preX: number; 58 | preY: number; 59 | ctx: CanvasRenderingContext2D; 60 | componentDidMount(){ 61 | const { img } = this.props; 62 | if(img){ 63 | this.image = document.createElement("img"); 64 | this.image.src = img; 65 | this.image.onload = () => this.init(true); 66 | this.image.onerror = () => this.init(false); 67 | }else{ 68 | this.init(false); 69 | } 70 | } 71 | init = (bol: boolean): void => { 72 | const { size, round = [0, 0, 0, 0], color = "#808080", imgRepeat } = this.props; 73 | const ch = this.wrap.clientHeight * dpr; 74 | const cw = this.wrap.clientWidth * dpr; 75 | this.setState({cw, ch}, ()=>{ 76 | this.canvas.addEventListener("touchmove", preventDefault, support ? {passive: false} : false); 77 | this.ctx = this.canvas.getContext('2d'); 78 | this.ctx.scale(dpr, dpr); 79 | if(bol){ 80 | let { width, height } = this.image; 81 | if(imgRepeat === "height"){ 82 | const w = width * (ch / dpr / height); 83 | this.ctx.drawImage(this.image, 0, 0, width, height, (cw / dpr - w) / 2, 0, w, ch / dpr); 84 | }else if(imgRepeat === "width"){ 85 | const h = height * (cw / dpr / width); 86 | this.ctx.drawImage(this.image, 0, 0, width, height, 0, (ch / dpr - h) / 2, cw / dpr, h); 87 | }else if(imgRepeat === "repeat"){ 88 | this.ctx.fillStyle = this.ctx.createPattern(this.image, "repeat"); 89 | this.ctx.fillRect(0, 0, cw, ch); 90 | }else{ 91 | this.ctx.drawImage(this.image, 0, 0, width, height, 0, 0, cw / dpr, ch / dpr); 92 | } 93 | }else{ 94 | this.ctx.fillStyle = color; 95 | this.ctx.fillRect(0, 0, cw, ch); 96 | } 97 | this.ptg = 0; 98 | this.ctx.globalCompositeOperation = 'destination-out'; 99 | this.ctx.strokeStyle = "#000000"; 100 | this.size = size || cw / dpr / 10; 101 | this.ctx.lineWidth = this.size; 102 | this.ctx.lineCap = 'round'; 103 | this.round = round; 104 | this.cells = []; 105 | this.roundX = [this.round[3], cw / dpr - this.round[1]]; 106 | this.roundY = [this.round[0], ch / dpr - this.round[2]]; 107 | this.cellX = (this.roundX[1] - this.roundX[0]) / 10; 108 | this.cellY = (this.roundY[1] - this.roundY[0]) / 10; 109 | this.sum = this.roundLen = 100; 110 | while(this.sum--) this.cells.push(false); 111 | setTimeout(() => { 112 | this.setState({visible: true}); 113 | }, 500); 114 | }); 115 | } 116 | down = (e: React.MouseEvent & React.TouchEvent): void => { 117 | const {left, top} = this.wrap.getClientRects()[0]; 118 | this.offsetLeft = left; 119 | this.offsetTop = top; 120 | let changedTouches = e.changedTouches; 121 | if(changedTouches){ 122 | const currentTouch = changedTouches[0]; 123 | this.preX = currentTouch.pageX - this.offsetLeft; 124 | this.preY = currentTouch.pageY - this.offsetTop; 125 | window.addEventListener("touchmove", this.move, false); 126 | window.addEventListener("touchend", this.up, false); 127 | }else{ 128 | this.preX = e.pageX - this.offsetLeft; 129 | this.preY = e.pageY - this.offsetTop; 130 | window.addEventListener("mousemove", this.move, false); 131 | window.addEventListener("mouseup", this.up, false); 132 | } 133 | 134 | this.handleRound(this.preX, this.preY); 135 | } 136 | move = (e: MouseEvent & TouchEvent): void => { 137 | const { mode = "move" } = this.props; 138 | let changedTouches = e.changedTouches; 139 | let preX = this.preX, preY = this.preY; 140 | this.ctx.beginPath(); 141 | this.ctx.moveTo(this.preX, this.preY); 142 | if(changedTouches){ 143 | const currentTouch = changedTouches[0]; 144 | this.preX = currentTouch.pageX - this.offsetLeft; 145 | this.preY = currentTouch.pageY - this.offsetTop; 146 | }else{ 147 | this.preX = e.pageX - this.offsetLeft; 148 | this.preY = e.pageY - this.offsetTop; 149 | } 150 | this.ctx.lineTo(this.preX, this.preY); 151 | this.ctx.stroke(); 152 | this.canvas.style.zIndex = ((+this.canvas.style.zIndex || 0) + 1) % 2 + 2 + ""; 153 | this.addRound(preX, preY, this.preX, this.preY); 154 | mode === "move" && this.checkRound(e); 155 | } 156 | up = (e: TouchEvent & MouseEvent, bol?: boolean): void => { 157 | const { mode } = this.props; 158 | let changedTouches = e.changedTouches; 159 | if(changedTouches){ 160 | window.removeEventListener("touchmove", this.move, false); 161 | window.removeEventListener("touchend", this.up, false); 162 | }else{ 163 | window.removeEventListener("mousemove", this.move, false); 164 | window.removeEventListener("mouseup", this.up, false); 165 | } 166 | mode === "end" && !bol && this.checkRound(e); 167 | } 168 | addRound = (preX: number, preY: number, curX: number, curY: number): void => { 169 | const dx = (curX - preX); 170 | const dy = (curY - preY); 171 | if(dx > this.cellX || dy > this.cellY){ 172 | let chunkX, chunkY; 173 | if(dy === 0){ 174 | chunkX = this.cellX; 175 | chunkY = 0; 176 | }else if(dx === 0){ 177 | chunkY = this.cellY; 178 | chunkX = 0; 179 | }else{ 180 | const ratio = Math.abs(dy / dx); 181 | if(ratio > this.cellY / this.cellX){ 182 | chunkX = this.cellY / 2 / ratio; 183 | chunkY = this.cellY / 2; 184 | }else{ 185 | chunkY = this.cellX / 2 * ratio; 186 | chunkX = this.cellX / 2; 187 | } 188 | } 189 | let [sx, ex] = dx > 0 ? [preX, curX] : [curX, preX]; 190 | let [sy, ey] = dy > 0 ? [preY, curY] : [curY, curY]; 191 | while(!(sx > ex || sy > ey)){ 192 | sx += chunkX; 193 | sy += chunkY; 194 | this.handleRound(sx, sy); 195 | } 196 | this.handleRound(curX, curY); 197 | }else{ 198 | this.handleRound(curX, curY); 199 | } 200 | } 201 | handleRound = (curX: number, curY: number): void => { 202 | const posX = (curX - this.round[3]) / this.cellX; 203 | const posY = (curY - this.round[0]) / this.cellY; 204 | const posXr = [Math.floor(posX - this.size / 2 / this.cellX), Math.floor(posX + this.size / 2 / this.cellX)]; 205 | const posYr = [Math.floor(posY - this.size / 2 / this.cellY), Math.floor(posY + this.size / 2 / this.cellY)]; 206 | for(let i = posXr[0]; i <= posXr[1]; i++){ 207 | if(i >= 0 && i < 10){ 208 | for(let j = posYr[0]; j <= posYr[1]; j++){ 209 | if(j >= 0 && j < 10){ 210 | const dx = curX - this.round[3] - (i + 0.5) * this.cellX; 211 | const dy = curY - this.round[0] - (j + 0.5) * this.cellY; 212 | const index = j * 10 + i; 213 | if(dx * dx + dy * dy < this.size * this.size / 4 && !this.cells[index]){ 214 | this.cells[index] = true; 215 | this.ptg++; 216 | } 217 | } 218 | } 219 | } 220 | } 221 | } 222 | checkRound = (e: TouchEvent & MouseEvent): void => { 223 | const { percentage = 70, onChange, clear, onSuccess } = this.props; 224 | onChange && onChange(this.ptg); 225 | if(this.ptg >= percentage){ 226 | clear && this.ctx.clearRect(0, 0, this.state.cw, this.state.ch); 227 | this.setState({isSuccess: true}); 228 | this.up(e, true); 229 | onSuccess && onSuccess(); 230 | } 231 | } 232 | render(){ 233 | const { children, className = "" } = this.props; 234 | return
this.wrap = dom}> 235 |
{children}
236 | this.canvas = dom} 238 | width={this.state.cw } 239 | height={this.state.ch } 240 | onMouseDown={this.down} 241 | onTouchStart={this.down} 242 | > 243 |
244 | } 245 | } -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | module.exports=function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=2)}([function(e,t){e.exports=require("react")},function(e,t){e.exports=require("tslib")},function(e,t,n){"use strict";n.r(t);var r=n(1),o=n(0),i=n.n(o),a=(n(3),1===window.devicePixelRatio?2:window.devicePixelRatio),s=function(e){return e&&e.preventDefault()},c=!1;try{var u=Object.defineProperty({},"passive",{get:function(){return c=!0}});window.addEventListener("test",null,u)}catch(e){}var l=function(e){function t(){var t=null!==e&&e.apply(this,arguments)||this;return t.state={ch:0,cw:0,isSuccess:!1,visible:!1},t.init=function(e){var n=t.props,r=n.size,o=n.round,i=void 0===o?[0,0,0,0]:o,u=n.color,l=void 0===u?"#808080":u,d=n.imgRepeat,f=t.wrap.clientHeight*a,p=t.wrap.clientWidth*a;t.setState({cw:p,ch:f},(function(){if(t.canvas.addEventListener("touchmove",s,!!c&&{passive:!1}),t.ctx=t.canvas.getContext("2d"),t.ctx.scale(a,a),e){var n=t.image,o=n.width,u=n.height;if("height"===d){var v=o*(f/a/u);t.ctx.drawImage(t.image,0,0,o,u,(p/a-v)/2,0,v,f/a)}else if("width"===d){var h=u*(p/a/o);t.ctx.drawImage(t.image,0,0,o,u,0,(f/a-h)/2,p/a,h)}else"repeat"===d?(t.ctx.fillStyle=t.ctx.createPattern(t.image,"repeat"),t.ctx.fillRect(0,0,p,f)):t.ctx.drawImage(t.image,0,0,o,u,0,0,p/a,f/a)}else t.ctx.fillStyle=l,t.ctx.fillRect(0,0,p,f);for(t.ptg=0,t.ctx.globalCompositeOperation="destination-out",t.ctx.strokeStyle="#000000",t.size=r||p/a/10,t.ctx.lineWidth=t.size,t.ctx.lineCap="round",t.round=i,t.cells=[],t.roundX=[t.round[3],p/a-t.round[1]],t.roundY=[t.round[0],f/a-t.round[2]],t.cellX=(t.roundX[1]-t.roundX[0])/10,t.cellY=(t.roundY[1]-t.roundY[0])/10,t.sum=t.roundLen=100;t.sum--;)t.cells.push(!1);setTimeout((function(){t.setState({visible:!0})}),500)}))},t.down=function(e){var n=t.wrap.getClientRects()[0],r=n.left,o=n.top;t.offsetLeft=r,t.offsetTop=o;var i=e.changedTouches;if(i){var a=i[0];t.preX=a.pageX-t.offsetLeft,t.preY=a.pageY-t.offsetTop,window.addEventListener("touchmove",t.move,!1),window.addEventListener("touchend",t.up,!1)}else t.preX=e.pageX-t.offsetLeft,t.preY=e.pageY-t.offsetTop,window.addEventListener("mousemove",t.move,!1),window.addEventListener("mouseup",t.up,!1);t.handleRound(t.preX,t.preY)},t.move=function(e){var n=t.props.mode,r=void 0===n?"move":n,o=e.changedTouches,i=t.preX,a=t.preY;if(t.ctx.beginPath(),t.ctx.moveTo(t.preX,t.preY),o){var s=o[0];t.preX=s.pageX-t.offsetLeft,t.preY=s.pageY-t.offsetTop}else t.preX=e.pageX-t.offsetLeft,t.preY=e.pageY-t.offsetTop;t.ctx.lineTo(t.preX,t.preY),t.ctx.stroke(),t.canvas.style.zIndex=((+t.canvas.style.zIndex||0)+1)%2+2+"",t.addRound(i,a,t.preX,t.preY),"move"===r&&t.checkRound(e)},t.up=function(e,n){var r=t.props.mode;e.changedTouches?(window.removeEventListener("touchmove",t.move,!1),window.removeEventListener("touchend",t.up,!1)):(window.removeEventListener("mousemove",t.move,!1),window.removeEventListener("mouseup",t.up,!1)),"end"===r&&!n&&t.checkRound(e)},t.addRound=function(e,n,r,o){var i=r-e,a=o-n;if(i>t.cellX||a>t.cellY){var s=void 0,c=void 0;if(0===a)s=t.cellX,c=0;else if(0===i)c=t.cellY,s=0;else{var u=Math.abs(a/i);u>t.cellY/t.cellX?(s=t.cellY/2/u,c=t.cellY/2):(c=t.cellX/2*u,s=t.cellX/2)}for(var l=i>0?[e,r]:[r,e],d=l[0],f=l[1],p=a>0?[n,o]:[o,o],v=p[0],h=p[1];!(d>f||v>h);)d+=s,v+=c,t.handleRound(d,v);t.handleRound(r,o)}else t.handleRound(r,o)},t.handleRound=function(e,n){for(var r=(e-t.round[3])/t.cellX,o=(n-t.round[0])/t.cellY,i=[Math.floor(r-t.size/2/t.cellX),Math.floor(r+t.size/2/t.cellX)],a=[Math.floor(o-t.size/2/t.cellY),Math.floor(o+t.size/2/t.cellY)],s=i[0];s<=i[1];s++)if(s>=0&&s<10)for(var c=a[0];c<=a[1];c++)if(c>=0&&c<10){var u=e-t.round[3]-(s+.5)*t.cellX,l=n-t.round[0]-(c+.5)*t.cellY,d=10*c+s;u*u+l*l=o&&(a&&t.ctx.clearRect(0,0,t.state.cw,t.state.ch),t.setState({isSuccess:!0}),t.up(e,!0),s&&s())},t}return Object(r.__extends)(t,e),t.prototype.componentDidMount=function(){var e=this,t=this.props.img;t?(this.image=document.createElement("img"),this.image.src=t,this.image.onload=function(){return e.init(!0)},this.image.onerror=function(){return e.init(!1)}):this.init(!1)},t.prototype.render=function(){var e=this,t=this.props,n=t.children,r=t.className,o=void 0===r?"":r;return i.a.createElement("div",{className:"___scratch "+o+" "+(this.state.isSuccess?"frozen":""),ref:function(t){return e.wrap=t}},i.a.createElement("div",{className:"___content "+(this.state.visible?"visible":"")},n),i.a.createElement("canvas",{ref:function(t){return e.canvas=t},width:this.state.cw,height:this.state.ch,onMouseDown:this.down,onTouchStart:this.down}))},t}(o.PureComponent);t.default=l},function(e,t,n){var r=n(4);"string"==typeof r&&(r=[[e.i,r,""]]);var o={insert:"head",singleton:!1};n(6)(r,o);r.locals&&(e.exports=r.locals)},function(e,t,n){(e.exports=n(5)(!1)).push([e.i,".___scratch {\n position: relative;\n display: inline-block;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n font-size: 0;\n}\n.___scratch.frozen {\n pointer-events: none;\n}\n.___scratch .___content {\n opacity: 0;\n}\n.___scratch .___content.visible {\n opacity: 1;\n}\n.___scratch canvas {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n height: 100%;\n width: 100%;\n}\n",""])},function(e,t,n){"use strict";e.exports=function(e){var t=[];return t.toString=function(){return this.map((function(t){var n=function(e,t){var n=e[1]||"",r=e[3];if(!r)return n;if(t&&"function"==typeof btoa){var o=(a=r,s=btoa(unescape(encodeURIComponent(JSON.stringify(a)))),c="sourceMappingURL=data:application/json;charset=utf-8;base64,".concat(s),"/*# ".concat(c," */")),i=r.sources.map((function(e){return"/*# sourceURL=".concat(r.sourceRoot).concat(e," */")}));return[n].concat(i).concat([o]).join("\n")}var a,s,c;return[n].join("\n")}(t,e);return t[2]?"@media ".concat(t[2],"{").concat(n,"}"):n})).join("")},t.i=function(e,n){"string"==typeof e&&(e=[[null,e,""]]);for(var r={},o=0;oz.length&&z.push(e)}function I(e,t,n){return null==e?0:function e(t,n,r,l){var o=typeof t;"undefined"!==o&&"boolean"!==o||(t=null);var u=!1;if(null===t)u=!0;else switch(o){case"string":case"number":u=!0;break;case"object":switch(t.$$typeof){case a:case i:u=!0}}if(u)return r(l,t,""===n?"."+F(t,0):n),1;if(u=0,n=""===n?".":n+":",Array.isArray(t))for(var c=0;c