├── src ├── utils.js ├── styles │ ├── vars.less │ ├── mixins.less │ └── index.less ├── components │ ├── Container │ │ ├── index.less │ │ └── index.js │ └── ErrorBoundary │ │ └── index.js ├── config.js └── index.js ├── example ├── README.md ├── example.less ├── index.html ├── example.js └── webpack.config.js ├── assetsImg └── example.gif ├── .gitignore ├── postcss.config.json ├── .editorconfig ├── CN.md ├── index.html ├── README.md ├── .babelrc ├── LICENCE ├── main.js ├── .eslintrc.js └── package.json /src/utils.js: -------------------------------------------------------------------------------- 1 | export const isImage = (filename)=> /.*\/(jpg|jpeg|png)$/.test(filename) -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # react-meme-generator 2 | run example 3 | 4 | ``` 5 | $ npm run demo :) 6 | ``` 7 | -------------------------------------------------------------------------------- /assetsImg/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lijinke666/react-meme-generator/HEAD/assetsImg/example.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | lib 3 | assets 4 | npm-debug.log 5 | .DS_Store 6 | .history 7 | .npmignore 8 | yarn-error.log -------------------------------------------------------------------------------- /example/example.less: -------------------------------------------------------------------------------- 1 | #root,body,html{ 2 | height:100% 3 | } 4 | body,html{ 5 | margin:0; 6 | padding: 0; 7 | } 8 | h2.example-title{ 9 | padding: 40px; 10 | font-weight: 500 11 | } -------------------------------------------------------------------------------- /postcss.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "autoprefixer": { 3 | "browsers": [ 4 | "last 6 versions", 5 | "Android >= 4.0", 6 | "Firefox ESR", 7 | "not ie < 9" 8 | ], 9 | "sourceMap":false 10 | } 11 | } -------------------------------------------------------------------------------- /src/styles/vars.less: -------------------------------------------------------------------------------- 1 | @font-family:-apple-system,BlinkMacSystemFont,Helvetica Neue,PingFang SC,Microsoft YaHei,Source Han Sans SC,Noto Sans CJK SC,WenQuanYi Micro Hei,sans-serif; 2 | @main-width: 1200px; 3 | @font:'iconfont'; 4 | @border-color:#e8e8e8; 5 | @prefix:"react-mime"; 6 | @shadow:0 2px 8px rgba(0, 0, 0, 0.09); -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain 2 | # consistent coding styles between different editors and IDEs. 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /src/components/Container/index.less: -------------------------------------------------------------------------------- 1 | 2 | @import "../../styles/vars.less"; 3 | .container{ 4 | &::before,&::after{ 5 | content:""; 6 | clear: both; 7 | zoom: 1; 8 | } 9 | .wrap{ 10 | width:@main-width; 11 | @media screen and (max-width:768px){ 12 | width:100vw; 13 | } 14 | margin:0 auto; 15 | } 16 | } -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | react-meme-generator 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /CN.md: -------------------------------------------------------------------------------- 1 | # react-meme-generator 2 | :boy: 一个react 表情包制作器 (支持摄像头) 3 | 4 | ## 例子 5 | [https://lijinke666.github.io/react-meme-generator/](https://lijinke666.github.io/react-meme-generator/) 6 | 7 | ## 本地开发 8 | ``` 9 | git clone https://github.com/lijinke666/react-meme.git 10 | npm install | yarn 11 | npm start 12 | ``` 13 | 14 | ## 许可证 15 | [MIT](https://github.com/lijinke666/react-meme/blob/master/LICENCE) 16 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | react-meme-generator 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /example/example.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom" 3 | import ReactMemeGenerator from "../src" 4 | import {LocaleProvider} from "antd" 5 | import zhCN from 'antd/lib/locale-provider/zh_CN' 6 | 7 | import "../src/styles/index.less" 8 | import "./example.less" 9 | 10 | 11 | const Demo = () => ( 12 | 13 | 14 | 15 | ) 16 | ReactDOM.render( 17 | , 18 | document.getElementById('root') 19 | ) 20 | -------------------------------------------------------------------------------- /src/components/Container/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classNames from "classnames" 3 | import "./index.less" 4 | export default class Container extends React.PureComponent{ 5 | render(){ 6 | const {className,...attr} = this.props 7 | return( 8 |
9 |
10 | {this.props.children} 11 |
12 |
13 | ) 14 | } 15 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-meme-generator 2 | :joy: a meme generator for react, support camera ! 3 | 4 | Have Fun :) 5 | 6 | ## Screenshots 7 | ![example](https://github.com/lijinke666/react-meme-generator/blob/master/assetsImg/example.gif) 8 | 9 | ## Example 10 | [https://lijinke666.github.io/react-meme-generator/](https://lijinke666.github.io/react-meme-generator/) 11 | 12 | ## Feature List 13 | 14 | - [x] Batch date 15 | - [x] Picture size control 16 | - [x] Support drag and paste selection images 17 | - [x] Pricture compress 18 | - [x] Pricture rotate 19 | - [x] Camera capture 20 | 21 | ## Development 22 | ``` 23 | git clone https://github.com/lijinke666/react-meme-generator.git 24 | npm install | yarn 25 | npm start 26 | ``` 27 | 28 | ## License 29 | [MIT](https://github.com/lijinke666/react-meme-generator/blob/master/LICENCE) 30 | -------------------------------------------------------------------------------- /src/styles/mixins.less: -------------------------------------------------------------------------------- 1 | ul,li{ 2 | margin:0; 3 | padding: 0; 4 | list-style-type: none; 5 | } 6 | .clearfix(){ 7 | &::before,&::after{ 8 | clear:both; 9 | content:""; 10 | display: table; 11 | zoom: 1; 12 | } 13 | } 14 | *{ 15 | box-sizing: border-box; 16 | } 17 | .hidden(){ 18 | display: none !important; 19 | } 20 | .hidden{ 21 | .hidden(); 22 | } 23 | .text-center(){ 24 | text-align: center 25 | } 26 | .text-center{ 27 | text-align: center; 28 | } 29 | .transition(@time:.3s){ 30 | transition: all @time cubic-bezier(0.165, 0.84, 0.44, 1); 31 | } 32 | .center(){ 33 | display: flex; 34 | justify-content: center; 35 | align-items: center; 36 | } 37 | 38 | .ellipsis-n(@n:2){ 39 | -webkit-line-clamp:@n; 40 | display:-webkit-box; 41 | -webkit-box-orient: vertical; 42 | overflow: hidden; 43 | text-overflow: ellipsis; 44 | justify-content: center; 45 | } 46 | .ellipsis-1(){ 47 | white-space: nowrap; 48 | text-overflow: ellipsis; 49 | overflow: hidden; 50 | } 51 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "modules": false 7 | } 8 | ], 9 | "@babel/preset-react", 10 | [ 11 | "@babel/preset-stage-1", 12 | { 13 | "decoratorsLegacy": true 14 | } 15 | ] 16 | ], 17 | "env": { 18 | "test": { 19 | "presets": [ 20 | "@babel/preset-env", 21 | [ 22 | "@babel/preset-stage-1", 23 | { 24 | "decoratorsLegacy": true 25 | } 26 | ], 27 | "@babel/preset-react" 28 | ], 29 | "plugins": [ 30 | "@babel/plugin-transform-modules-commonjs", 31 | "dynamic-import-node" 32 | ] 33 | } 34 | }, 35 | "plugins": [ 36 | "@babel/plugin-transform-modules-commonjs", 37 | "@babel/plugin-proposal-object-rest-spread", 38 | "@babel/plugin-proposal-class-properties", 39 | "transform-decorators-legacy", 40 | "react-hot-loader/babel", 41 | [ 42 | "import", 43 | { 44 | "libraryName": "antd", 45 | "style": true 46 | } 47 | ] 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2018 jinke.Li 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. -------------------------------------------------------------------------------- /src/components/ErrorBoundary/index.js: -------------------------------------------------------------------------------- 1 | import React,{PureComponent} from "react" 2 | export default class ErrorBoundary extends PureComponent { 3 | constructor(props) { 4 | super(props); 5 | this.state = { error: null, errorInfo: null }; 6 | } 7 | 8 | componentDidCatch(error, errorInfo) { 9 | // Catch errors in any components below and re-render with error message 10 | this.setState({ 11 | error: error, 12 | errorInfo: errorInfo 13 | }) 14 | // You can also log error messages to an error reporting service here 15 | } 16 | 17 | render() { 18 | if (this.state.errorInfo) { 19 | // Error path 20 | return ( 21 |
22 |

报错了,崽儿 :(

23 |
24 | {this.state.error && this.state.error.toString()} 25 |
26 | {this.state.errorInfo.componentStack} 27 |
28 |
29 | ); 30 | } 31 | // Normally, just render children 32 | return this.props.children; 33 | } 34 | } -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const {app,BrowserWindow} = require('electron') 2 | 3 | const path = require('path') 4 | const url = require('url') 5 | 6 | // Keep a global reference of the window object, if you don't, the window will 7 | // be closed automatically when the JavaScript object is garbage collected. 8 | let mainWindow 9 | 10 | function createWindow () { 11 | // Create the browser window. 12 | mainWindow = new BrowserWindow({width: 1200, height: 600}) 13 | 14 | // and load the index.html of the app. 15 | mainWindow.loadURL(url.format({ 16 | pathname: path.join(__dirname, 'index.html'), 17 | protocol: 'file:', 18 | slashes: true 19 | })) 20 | 21 | // Open the DevTools. 22 | // mainWindow.webContents.openDevTools() 23 | 24 | // Emitted when the window is closed. 25 | mainWindow.on('closed', function () { 26 | // Dereference the window object, usually you would store windows 27 | // in an array if your app supports multi windows, this is the time 28 | // when you should delete the corresponding element. 29 | mainWindow = null 30 | }) 31 | } 32 | 33 | // This method will be called when Electron has finished 34 | // initialization and is ready to create browser windows. 35 | // Some APIs can only be used after this event occurs. 36 | app.on('ready', createWindow) 37 | 38 | // Quit when all windows are closed. 39 | app.on('window-all-closed', function () { 40 | // On OS X it is common for applications and their menu bar 41 | // to stay active until the user quits explicitly with Cmd + Q 42 | if (process.platform !== 'darwin') { 43 | app.quit() 44 | } 45 | }) 46 | 47 | app.on('activate', function () { 48 | // On OS X it's common to re-create a window in the app when the 49 | // dock icon is clicked and there are no other windows open. 50 | if (mainWindow === null) { 51 | createWindow() 52 | } 53 | }) -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "parserOptions": { 3 | "ecmaVersion": 8, 4 | "sourceType": "module", 5 | "ecmaFeatures": { 6 | "jsx": true 7 | } 8 | }, 9 | "parser": "babel-eslint", 10 | "plugins": [ 11 | "babel", 12 | "react" 13 | ], 14 | "extends": "eslint:recommended", 15 | "env": { 16 | "es6": true, 17 | "browser": true, 18 | "commonjs": true, 19 | }, 20 | "globals": { 21 | }, 22 | "rules": { 23 | "object-shorthand": "error", 24 | "generator-star-spacing": ["error", "after"], 25 | "camelcase": ["error", {"properties": "never"}], 26 | "eqeqeq": ["error", "smart"], 27 | "linebreak-style": ["error", "unix"], 28 | "new-cap": "error", 29 | "no-array-constructor": "error", 30 | "no-lonely-if": "error", 31 | "no-loop-func": "error", 32 | "no-param-reassign": "error", 33 | "no-sequences": "error", 34 | "no-shadow-restricted-names": "error", 35 | "no-unneeded-ternary": "error", 36 | "no-unused-expressions": "error", 37 | "no-unused-vars": ["error", {"args": "none"}], 38 | "no-use-before-define": ["error", "nofunc"], 39 | "no-var": "error", 40 | "prefer-arrow-callback": "error", 41 | "prefer-spread": "error", 42 | "prefer-template": "error", 43 | "wrap-iife": ["error", "inside"], 44 | "yoda": ["error", "never"], 45 | "react/jsx-uses-react": "error", 46 | "react/jsx-uses-vars": "error", 47 | "react/jsx-no-undef": ["error", {"allowGlobals": true}], 48 | "react/jsx-no-bind": ["error", {"allowArrowFunctions": true}], 49 | "react/jsx-key": "error", 50 | "react/no-unknown-property": "error", 51 | "react/no-string-refs": "error", 52 | "react/no-direct-mutation-state": "error", 53 | } 54 | } -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | export const prefix = "react-meme" 2 | 3 | //可选字体配置 4 | export const fontFamily = [ 5 | { 6 | label: "微软雅黑", 7 | value: "Microsoft YaHei" 8 | }, 9 | { 10 | label: "海维提卡", 11 | value: "Helvetica" 12 | }, { 13 | label: "草书", 14 | value: "cursive" 15 | }, { 16 | label: "宋体", 17 | value: "SimSub" 18 | }, { 19 | label: "黑体", 20 | value: "SimHei" 21 | }, { 22 | label: "楷体", 23 | value: "KaiTi" 24 | }, { 25 | label: "华文黑体", 26 | value: "STKaiti" 27 | }, { 28 | label: "隶书", 29 | value: "LiSu" 30 | }, { 31 | label: "幼圆", 32 | value: "YouYuan" 33 | } 34 | ] 35 | 36 | //图片处理类型 TODO 37 | export const imageProcess = [ 38 | { 39 | label: "系统默认", 40 | value: "default" 41 | }, 42 | { 43 | label: "翻转", 44 | value: "reversal" 45 | }, 46 | { 47 | label: "压缩", 48 | value: "compress" 49 | } 50 | ] 51 | 52 | export const fontSize = Array.from({ length: 100 }).map((_, i) => i + 1).filter(v => v >= 12) 53 | 54 | //默认 20 px 55 | export const defaultFontSize = fontSize[4] 56 | 57 | export const defaultFontColor = "#444" 58 | 59 | export const defaultFontText = "示例文字" 60 | 61 | //图片最大限制 62 | export const img_max_size = 1024 63 | 64 | //图片每次缩放的值 65 | export const range = 0.05 66 | 67 | //文字每次缩放的值 68 | export const textRange = 1 69 | 70 | export const whellScaleRange = [0.4, 3.0] 71 | 72 | //文本缩放 最大最小值限制 73 | export const textWhellScaleRange = [fontSize[0],fontSize[fontSize.length-1]] 74 | 75 | //图片默认缩放比例 76 | export const defaultScale = 1.0 77 | export const defaultRotate = 0 78 | 79 | export const defaultQuality = 0.50 80 | 81 | //图片预览区域宽高 82 | export const previewContentStyle = { 83 | width: 300, 84 | height: 300 85 | } -------------------------------------------------------------------------------- /src/styles/index.less: -------------------------------------------------------------------------------- 1 | @import "./vars.less"; 2 | @import "./mixins.less"; 3 | .react-meme { 4 | &-item { 5 | padding-bottom: 3%; 6 | } 7 | &-main{ 8 | padding: 10px 0 80px 0; 9 | } 10 | .color-section { 11 | padding: 5px; 12 | background: #fff; 13 | border-radius: 1px; 14 | box-shadow: 0 0 0 1px rgba(0, 0, 0, .1); 15 | display: inline-block; 16 | cursor: pointer; 17 | .color { 18 | width: 90px; 19 | height: 22px; 20 | border-radius: 2px; 21 | } 22 | } 23 | .popover { 24 | position: absolute; 25 | left: 0; 26 | top: 41px; 27 | z-index: 999; 28 | text-align: right; 29 | } 30 | .cover { 31 | position: fixed; 32 | top: 0; 33 | left: 0; 34 | right: 0; 35 | bottom: 0; 36 | } 37 | .preview-content { 38 | width: 300px; 39 | height: 300px; 40 | border: 1px solid @border-color; 41 | overflow: hidden; 42 | margin-bottom: 20px; 43 | position: relative; 44 | z-index: 99; 45 | transform: translate3d(0, 0, 0); 46 | transition: .3s cubic-bezier(0.075, 0.82, 0.165, 1); 47 | &:hover { 48 | box-shadow: @shadow; 49 | border-color: rgba(0, 0, 0, 0.09); 50 | } 51 | &.drag-active { 52 | border: 2px dashed @border-color; 53 | background-color: rgba(0, 0, 0, .1) 54 | } 55 | .preview-image { 56 | user-select: none; 57 | cursor: -webkit-grab; 58 | -webkit-user-drag: none; 59 | img{ 60 | transition: transform 100ms cubic-bezier(0.25, 0.46, 0.33, 0.98) 61 | } 62 | } 63 | } 64 | &-text { 65 | position: absolute; 66 | left: 0; 67 | top: 0; 68 | cursor: move; 69 | z-index: 99; 70 | margin: 0; 71 | padding: 0 72 | } 73 | } 74 | 75 | .react-meme-resize-btn { 76 | margin-left: 10px; 77 | } -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const path = require("path"); 3 | const OpenBrowserPlugin = require("open-browser-webpack-plugin"); 4 | 5 | const HOST = "localhost"; 6 | const PORT = 8081; 7 | 8 | module.exports = env => { 9 | const mode = (env && env.mode) || "DEV"; 10 | const options = { 11 | entry: 12 | mode === "DEV" 13 | ? [ 14 | "react-hot-loader/patch", //热更新 15 | `webpack-dev-server/client?http://${HOST}:${PORT}`, 16 | "webpack/hot/only-dev-server", 17 | path.join(__dirname, "../example/example.js") 18 | ] 19 | : path.join(__dirname, "../example/example.js"), 20 | 21 | output: { 22 | path: path.join(__dirname, "../example/dist"), 23 | filename: "build.js" 24 | }, 25 | //模块加载器 26 | module: { 27 | rules: [ 28 | { 29 | test: /\.js[x]?$/, 30 | use: [ 31 | { 32 | loader: "babel-loader" 33 | } 34 | ], 35 | exclude: "/node_modules/" 36 | }, 37 | { 38 | test: /\.less$/, 39 | use: [ 40 | { loader: "style-loader" }, 41 | { 42 | loader: "css-loader", 43 | options: { minimize: false, sourceMap: true } 44 | }, 45 | { 46 | loader: "less-loader", 47 | options: { 48 | sourceMap: true 49 | } 50 | } 51 | ] 52 | }, 53 | { 54 | test: /\.css$/, 55 | use: [ 56 | { loader: "style-loader" }, //loader 倒序执行 先执行 less-laoder 57 | { 58 | loader: "css-loader", 59 | options: { minimize: false, sourceMap: true } 60 | } 61 | ] 62 | }, 63 | { 64 | test: /\.(eot|ttf|svg|woff|woff2)$/, 65 | use: [ 66 | { 67 | loader: "file-loader", 68 | options: { 69 | name: "fonts/[name][hash:8].[ext]" 70 | } 71 | } 72 | ] 73 | } 74 | ] 75 | }, 76 | devtool: "source-map", 77 | //自动补全后缀 78 | resolve: { 79 | enforceExtension: false, 80 | extensions: [".js", ".jsx", ".json"], 81 | modules: [path.resolve("src"), path.resolve("."), "node_modules"] 82 | }, 83 | externals: { 84 | async: "commonjs async" 85 | }, 86 | devServer: { 87 | contentBase: path.join(__dirname, "../example/"), 88 | inline: true, 89 | port: PORT, 90 | publicPath: "/dist/", 91 | historyApiFallback: true, 92 | stats: { 93 | color: true, 94 | errors: true, 95 | version: true, 96 | warnings: true, 97 | progress: true 98 | } 99 | }, 100 | plugins: [ 101 | new OpenBrowserPlugin({ 102 | url: `http:${HOST}:${PORT}/` 103 | }) 104 | ] 105 | }; 106 | if (mode === "PROD") { 107 | options.plugins = options.plugins.concat([ 108 | new webpack.optimize.UglifyJsPlugin({ 109 | output: { 110 | comments: false 111 | }, 112 | compress: { 113 | warnings: false 114 | } 115 | }) 116 | ]); 117 | } 118 | return options; 119 | }; 120 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-meme-generator", 3 | "version": "0.0.7", 4 | "description": "a meme generator for react", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "start": "npm run demo", 8 | "test": "jest", 9 | "clean": "rimraf lib && rimraf assets", 10 | "build-demo": "webpack --env.mode=PROD --progress --config ./example/webpack.config.js", 11 | "demo": "webpack-dev-server --progress --inline --hot --config ./example/webpack.config.js", 12 | "pc": "electron ./index.html", 13 | "build:pc": "electron-packager ./main.js memeGenerator --darwin --out memeGenerator --arch=x64 --overwrite --ignore=node_modules .history src assetsImg assets lib --electron-version=2.0.0", 14 | "@babel": "babel-upgrade --write" 15 | }, 16 | "author": "Jinke.Li", 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/lijinke666/react-meme-generator" 20 | }, 21 | "homepage": "https://lijinke666.github.io/react-meme-generator", 22 | "bugs": { 23 | "url": "https://github.com/lijinke666/react-meme-generator/issues" 24 | }, 25 | "license": "MIT", 26 | "keywords": [ 27 | "react", 28 | "reactjs", 29 | "meme", 30 | "emoji", 31 | "component", 32 | "generator", 33 | "react-meme-generator" 34 | ], 35 | "dependencies": { 36 | "antd": "^3.5.1", 37 | "classnames": "^2.2.5", 38 | "dom-to-image": "^2.6.0", 39 | "prop-types": "^15.6.0", 40 | "react": "^16.2.0", 41 | "react-color": "^2.13.8", 42 | "react-draggable": "^3.0.4" 43 | }, 44 | "devDependencies": { 45 | "@babel/cli": "7.0.0-beta.44", 46 | "@babel/core": "7.0.0-beta.44", 47 | "@babel/plugin-proposal-class-properties": "7.0.0-beta.44", 48 | "@babel/plugin-proposal-object-rest-spread": "7.0.0-beta.44", 49 | "@babel/plugin-syntax-dynamic-import": "7.0.0-beta.44", 50 | "@babel/plugin-syntax-object-rest-spread": "7.0.0-beta.44", 51 | "@babel/plugin-transform-object-assign": "7.0.0-beta.44", 52 | "@babel/plugin-transform-runtime": "7.0.0-beta.44", 53 | "@babel/preset-env": "7.0.0-beta.44", 54 | "@babel/preset-react": "7.0.0-beta.44", 55 | "@babel/preset-stage-0": "7.0.0-beta.44", 56 | "@babel/preset-stage-1": "7.0.0-beta.44", 57 | "@babel/runtime": "7.0.0-beta.44", 58 | "autoprefixer": "^6.7.2", 59 | "babel-core": "^7.0.0-bridge.0", 60 | "babel-jest": "^19.0.0", 61 | "babel-loader": "^8.0.0-beta.0", 62 | "babel-plugin-add-module-exports": "^0.2.1", 63 | "babel-plugin-dynamic-import-node": "^1.0.2", 64 | "babel-plugin-import": "^1.6.3", 65 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 66 | "chai": "^4.1.2", 67 | "copy-webpack-plugin": "latest", 68 | "css-loader": "~0.23.0", 69 | "enzyme": "^3.2.0", 70 | "enzyme-adapter-react-16": "^1.1.0", 71 | "enzyme-to-json": "^3.2.2", 72 | "extract-text-webpack-plugin": "^2.0.0-beta.4", 73 | "file-loader": "^0.9.0", 74 | "html-loader": "^0.4.4", 75 | "html-webpack-plugin": "^2.28.0", 76 | "imagemin-webpack-plugin": "^1.4.4", 77 | "jest": "^19.0.2", 78 | "less": "^2.7.2", 79 | "less-loader": "^2.2.3", 80 | "open-browser-webpack-plugin": "0.0.2", 81 | "optimize-css-assets-webpack-plugin": "^1.3.0", 82 | "postcss": "^6.0.12", 83 | "postcss-cli": "^4.1.1", 84 | "postcss-loader": "^1.2.2", 85 | "rc-button": "^0.1.5", 86 | "rc-message": "^1.3.2", 87 | "react-dom": "^16.4.2", 88 | "react-hot-loader": "^4.0.1", 89 | "react-jinke-music-player": "latest", 90 | "react-loader": "^2.4.0", 91 | "react-test-renderer": "^15.6.1", 92 | "regenerator-runtime": "^0.11.0", 93 | "rimraf": "^2.6.0", 94 | "style-loader": "~0.13.0", 95 | "url-loader": "^0.5.8", 96 | "webpack": "^2.7.0", 97 | "webpack-dev-server": "^3.1.11" 98 | }, 99 | "jest": { 100 | "collectCoverageFrom": [ 101 | "src/*" 102 | ], 103 | "moduleFileExtensions": [ 104 | "js", 105 | "jsx" 106 | ], 107 | "setupFiles": [ 108 | "./__tests__/setup.js" 109 | ], 110 | "snapshotSerializers": [ 111 | "enzyme-to-json/serializer" 112 | ] 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name react 表情包 制作器 3 | * @author jinke.li 4 | */ 5 | import React, { PureComponent, Fragment } from "react"; 6 | import Container from "./components/Container"; 7 | import cls from "classnames"; 8 | import { 9 | Button, 10 | Divider, 11 | Col, 12 | Row, 13 | Form, 14 | Input, 15 | Checkbox, 16 | Modal, 17 | message, 18 | Select, 19 | Slider, 20 | Tooltip, 21 | Radio 22 | } from "antd"; 23 | import { SketchPicker } from "react-color"; 24 | import Draggable from "react-draggable"; 25 | import domToImage from "dom-to-image"; 26 | import { hot } from "react-hot-loader"; 27 | import { 28 | prefix, 29 | fontFamily, 30 | defaultFontText, 31 | defaultFontColor, 32 | imageProcess, 33 | defaultFontSize, 34 | fontSize as FONT_SIZE, 35 | maxFileSize as IMG_MAX_SIZE, 36 | previewContentStyle, 37 | range, 38 | textRange, 39 | whellScaleRange, 40 | textWhellScaleRange, 41 | defaultScale, 42 | defaultRotate, 43 | defaultQuality 44 | } from "./config"; 45 | 46 | import { isImage } from "./utils"; 47 | import { 48 | name as APPNAME, 49 | version as APPVERSION, 50 | repository 51 | } from "../package.json"; 52 | 53 | const { FormItem } = Form; 54 | const { Option } = Select; 55 | const { TextArea } = Input; 56 | const RadioGroup = Radio.Group; 57 | 58 | class ReactMemeGenerator extends PureComponent { 59 | state = { 60 | cameraVisible: false, 61 | displayColorPicker: false, 62 | fontColor: defaultFontColor, 63 | fontSize: defaultFontSize, 64 | text: defaultFontText, 65 | font: fontFamily[0].value, 66 | loadingImgReady: false, 67 | dragAreaClass: false, //拖拽区域active class 68 | textDragX: 0, 69 | textDragY: 0, 70 | imageDragX: 0, 71 | imageDragY: 0, 72 | isRotateText: false, 73 | rotate: defaultRotate, //旋转角度 74 | scale: defaultScale, //缩放比例 75 | toggleText: false, //为false 文字换行时属于整体 反之为独立的一行 不受其他控制 76 | width: previewContentStyle.width, 77 | height: previewContentStyle.height, 78 | drawLoading: false, 79 | rotateX: 0, //翻转 80 | rotateY: 0, 81 | isRotateX: false, //x轴翻转 82 | isCompress:false 83 | }; 84 | activeDragAreaClass = "drag-active"; 85 | constructor(props) { 86 | super(props); 87 | } 88 | static defaultProps = { 89 | defaultFont: fontFamily[0].value, 90 | defaultImageProcess: imageProcess[0].value, 91 | defaultText: defaultFontText, 92 | defaultFontSize, 93 | drag: true, 94 | paste: true 95 | }; 96 | imageWidthChange = e => { 97 | this.setState({ width: e.target.value }); 98 | }; 99 | imageHeightChange = e => { 100 | this.setState({ height: e.target.value }); 101 | }; 102 | toggleColorPicker = () => { 103 | this.setState({ displayColorPicker: !this.state.displayColorPicker }); 104 | }; 105 | closeColorPicker = () => { 106 | this.setState({ displayColorPicker: false }); 107 | }; 108 | colorChange = ({ hex }) => { 109 | this.setState({ fontColor: hex }); 110 | }; 111 | drawMeme = () => { 112 | const { width, height, loadingImgReady,isCompress } = this.state; 113 | if (!loadingImgReady) return message.error("请选择图片!"); 114 | 115 | this.setState({ drawLoading: true }); 116 | 117 | const imageArea = document.querySelector(".preview-content"); 118 | const options = { 119 | width, 120 | height, 121 | } 122 | if(isCompress){ 123 | options.quality = defaultQuality 124 | } 125 | domToImage 126 | .toPng(imageArea, options) 127 | .then(dataUrl => { 128 | this.setState({ drawLoading: false }); 129 | Modal.confirm({ 130 | title: "生成成功", 131 | content: , 132 | onOk: () => { 133 | message.success("下载成功!"); 134 | const filename = Date.now() 135 | const ext = isCompress ? 'jpeg' : 'png' 136 | var link = document.createElement("a"); 137 | link.download = `${filename}.${ext}`; 138 | link.href = dataUrl; 139 | link.click(); 140 | }, 141 | okText: "立即下载", 142 | cancelText: "再改一改" 143 | }); 144 | }) 145 | .catch(err => { 146 | message.error(err); 147 | this.setState({ drawLoading: false }); 148 | }); 149 | }; 150 | closeImageWhellTip = () => { 151 | setImmediate(() => { 152 | this.setState({ imageWhellTipVisible: false }); 153 | }); 154 | }; 155 | resizeImageScale = () => { 156 | const { scale } = this.state; 157 | if (scale != defaultScale) { 158 | this.setState({ scale: defaultScale }); 159 | } 160 | }; 161 | resetImageRotate = () => { 162 | const { rotate } = this.state; 163 | if (rotate != defaultRotate) { 164 | this.setState({ scale: defaultRotate }); 165 | } 166 | }; 167 | //文字鼠标滚轮缩放 168 | bindTextWheel = e => { 169 | e.stopPropagation(); 170 | const y = e.deltaY ? e.deltaY : e.wheelDeltaY; //火狐有特殊 171 | const [min, max] = textWhellScaleRange; 172 | this.setState(({ fontSize }) => { 173 | let _fontSize = fontSize; 174 | if (y > 0) { 175 | _fontSize -= textRange; 176 | _fontSize = Math.max(min, _fontSize); 177 | return { 178 | fontSize: _fontSize 179 | }; 180 | } else { 181 | _fontSize += textRange; 182 | _fontSize = Math.min(max, _fontSize); 183 | return { 184 | fontSize: _fontSize 185 | }; 186 | } 187 | }); 188 | return false; 189 | }; 190 | //图片鼠标滚轮缩放 191 | bindImageMouseWheel = e => { 192 | const y = e.deltaY ? e.deltaY : e.wheelDeltaY; //火狐有特殊 193 | const [min, max] = whellScaleRange; 194 | this.setState(({ scale }) => { 195 | let _scale = scale; 196 | if (y > 0) { 197 | _scale -= range; 198 | _scale = Math.max(min, _scale); 199 | return { 200 | scale: _scale, 201 | imageWhellTipVisible: true 202 | }; 203 | } else { 204 | _scale += range; 205 | _scale = Math.min(max, _scale); 206 | return { 207 | scale: _scale, 208 | imageWhellTipVisible: true 209 | }; 210 | } 211 | }); 212 | return false; 213 | }; 214 | openCamera = () => { 215 | if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { 216 | navigator.mediaDevices 217 | .getUserMedia({ 218 | video: true, 219 | audio: true 220 | }) 221 | .then(stream => { 222 | const hide = message.loading('盛世美颜即将出现...') 223 | this.setState( 224 | { 225 | cameraVisible: true 226 | }, 227 | () => { 228 | setTimeout(()=>{ 229 | try { 230 | this.video.srcObject = stream 231 | this.video.play(); 232 | } catch (err) { 233 | console.log(err); 234 | Modal.error({ 235 | title: "摄像头失败", 236 | content: err.message 237 | }); 238 | } finally{ 239 | hide() 240 | } 241 | },1000) 242 | 243 | } 244 | ); 245 | }) 246 | .catch(err => { 247 | console.log(err) 248 | Modal.error({ 249 | title: "调用摄像头失败", 250 | content: err.toString() 251 | }); 252 | this.setState({ cameraVisible: false }); 253 | }); 254 | } else { 255 | Modal.error({ title: "抱歉,你的电脑暂不支持摄像头!" }); 256 | this.setState({ cameraVisible: false }); 257 | } 258 | // this.setState({ cameraVisible: true }) 259 | }; 260 | closeCamera = () => { 261 | this.setState({ cameraVisible: false, cameraUrl:"" }); 262 | }; 263 | fontSizeChange = value => { 264 | this.setState({ fontSize: value }); 265 | }; 266 | screenShotCamera = () => { 267 | const canvas = document.createElement("canvas"); 268 | const ctx = canvas.getContext("2d"); 269 | const { width, height } = previewContentStyle; 270 | canvas.width = width; 271 | canvas.height = height; 272 | ctx.drawImage(this.video, 0, 0, width, height); 273 | const data = canvas.toDataURL("image/png"); 274 | message.success('截取摄像头画面成功!') 275 | this.setState({ 276 | currentImg: { 277 | src: data 278 | }, 279 | cameraVisible:false, 280 | scale: defaultScale, 281 | loading: false, 282 | loadingImgReady: true 283 | }); 284 | }; 285 | onSelectFile = () => { 286 | this.file.click(); 287 | }; 288 | imageChange = () => { 289 | const files = Array.from(this.file.files); 290 | this.renderImage(files[0]); 291 | }; 292 | renderImage = file => { 293 | if (file && Object.is(typeof file, "object")) { 294 | let { type, name, size } = file; 295 | if (!isImage(type)) { 296 | return message.error("无效的图片格式"); 297 | } 298 | this.setState({ loading: true }); 299 | const url = window.URL.createObjectURL(file); 300 | this.setState({ 301 | currentImg: { 302 | src: url, 303 | size: `${~~(size / 1024)}KB`, 304 | type 305 | }, 306 | scale: defaultScale, 307 | loading: false, 308 | loadingImgReady: true 309 | }); 310 | } 311 | }; 312 | stopAll = target => { 313 | target.stopPropagation(); 314 | target.preventDefault(); 315 | }; 316 | //绑定拖拽事件 317 | bindDragListener = (dragArea, dragAreaClass = true) => { 318 | document.addEventListener( 319 | "dragenter", 320 | e => { 321 | this.addDragAreaStyle(); 322 | }, 323 | false 324 | ); 325 | document.addEventListener( 326 | "dragleave", 327 | e => { 328 | this.removeDragAreaStyle(); 329 | }, 330 | false 331 | ); 332 | //进入 333 | dragArea.addEventListener( 334 | "dragenter", 335 | e => { 336 | this.stopAll(e); 337 | this.addDragAreaStyle(); 338 | }, 339 | false 340 | ); 341 | //离开 342 | dragArea.addEventListener( 343 | "dragleave", 344 | e => { 345 | this.stopAll(e); 346 | this.removeDragAreaStyle(); 347 | }, 348 | false 349 | ); 350 | //移动 351 | dragArea.addEventListener( 352 | "dragover", 353 | e => { 354 | this.stopAll(e); 355 | this.addDragAreaStyle(); 356 | }, 357 | false 358 | ); 359 | dragArea.addEventListener( 360 | "drop", 361 | e => { 362 | this.stopAll(e); 363 | this.removeDragAreaStyle(); 364 | const files = e.dataTransfer.files; 365 | this.renderImage(Array.from(files)[0]); 366 | }, 367 | false 368 | ); 369 | }; 370 | addDragAreaStyle = () => { 371 | this.setState({ dragAreaClass: true }); 372 | }; 373 | removeDragAreaStyle = () => { 374 | this.setState({ dragAreaClass: false }); 375 | }; 376 | onTextChange = e => { 377 | this.setState({ text: e.target.value }); 378 | }; 379 | fontFamilyChange = value => { 380 | this.setState({ font: value }); 381 | }; 382 | stopDragText = (e, { x, y }) => { 383 | this.setState({ 384 | textDragX: x, 385 | textDragY: y 386 | }); 387 | }; 388 | stopDragImage = (e, { x, y }) => { 389 | this.setState({ 390 | imageDragX: x, 391 | imageDragY: y 392 | }); 393 | }; 394 | rotateImage = value => { 395 | this.setState({ rotate: value }); 396 | }; 397 | toggleRotateStatus = e => { 398 | this.setState({ 399 | isRotateText: e.target.checked 400 | }); 401 | }; 402 | toggleText = e => { 403 | this.setState({ toggleText: e.target.checked }); 404 | }; 405 | pasteHandler = e => { 406 | const { items, types } = e.clipboardData; 407 | if (!items) return; 408 | 409 | const item = items[0]; //只要一张图片 410 | const { kind, type } = item; //kind 种类 ,type 类型 411 | if (kind.toLocaleLowerCase() != "file") { 412 | return message.error("错误的文件类型!"); 413 | } 414 | const file = item.getAsFile(); 415 | this.renderImage(file); 416 | }; 417 | //粘贴图片 418 | bindPasteListener = area => { 419 | area.addEventListener("paste", this.pasteHandler); 420 | }; 421 | unBindPasteListener = area => { 422 | area.removeEventListener("paste", this.pasteHandler); 423 | }; 424 | howToUse = () => { 425 | Modal.info({ 426 | title: "使用说明", 427 | content: ( 428 | 433 | ) 434 | }); 435 | }; 436 | //翻转图片 437 | turnImage = value => { 438 | this.setState(({ isRotateX }) => ({ 439 | [isRotateX ? "rotateX" : "rotateY"]: value 440 | })); 441 | }; 442 | turnRotateChange = e => { 443 | this.setState({ isRotateX: e.target.value,rotateX:0,rotateY:0 }); 444 | }; 445 | onCompress = (e)=>{ 446 | this.setState({isCompress:e.target.checked}) 447 | } 448 | compressChange = (value)=>{ 449 | this.setState({quality:value}) 450 | } 451 | render() { 452 | const { getFieldDecorator } = this.props.form; 453 | const formItemLayout = { 454 | labelCol: { span: 4 }, 455 | wrapperCol: { span: 14 } 456 | }; 457 | const buttonItemLayout = { 458 | wrapperCol: { span: 14, offset: 4 } 459 | }; 460 | 461 | const { 462 | cameraVisible, 463 | cameraUrl, 464 | fontColor, 465 | fontSize, 466 | font, 467 | text, 468 | displayColorPicker, 469 | loading, 470 | loadingImgReady, 471 | currentImg, 472 | dragAreaClass, 473 | textDragX, 474 | textDragY, 475 | imageDragX, 476 | imageDragY, 477 | isRotateText, 478 | rotate, 479 | scale, 480 | imageWhellTipVisible, 481 | toggleText, 482 | memeModalVisible, 483 | drawLoading, 484 | rotateX, 485 | rotateY, 486 | width, 487 | height, 488 | isCompress 489 | } = this.state; 490 | 491 | const _scale = scale.toFixed(2); 492 | 493 | const { 494 | defaultFont, 495 | defaultFontSize, 496 | defaultImageProcess, 497 | defaultText 498 | } = this.props; 499 | 500 | const labelSpan = 4; 501 | const valueSpan = 19; 502 | const offsetSpan = 1; 503 | 504 | const operationRow = ({ icon = "edit", label, component }) => ( 505 | 506 | 507 | 510 | 511 | 516 | {component} 517 | 518 | 519 | ); 520 | 521 | const imageTransFormConfig = { 522 | transform: `rotate(${rotate}deg) rotateX(${rotateX}deg) rotateY(${rotateY}deg) scale(${_scale})` 523 | }; 524 | 525 | const previewImageEvents = loadingImgReady 526 | ? { 527 | onWheel: this.bindImageMouseWheel, 528 | onMouseLeave: this.closeImageWhellTip 529 | } 530 | : {}; 531 | 532 | const previewImageSize = { 533 | width:`${width}px`, 534 | height:`${height}px` 535 | } 536 | 537 | return ( 538 | 539 | 540 |

541 | {APPNAME} 542 |

543 |
544 |
(this.previewArea = previewArea)} 547 | > 548 | 549 | 550 | 554 | 缩放比例: {_scale} 555 | , 556 | 564 | ]} 565 | visible={imageWhellTipVisible} 566 | > 567 |
(this.previewContent = node)} 569 | className={cls("preview-content", { 570 | [this.activeDragAreaClass]: dragAreaClass 571 | })} 572 | {...previewImageEvents} 573 | style={ 574 | isRotateText 575 | ? { ...imageTransFormConfig, ...previewImageSize } 576 | : previewImageSize 577 | } 578 | > 579 | {loadingImgReady ? ( 580 | 584 |
585 | (this.previewImage = node)} 588 | src={currentImg.src} 589 | style={loadingImgReady ? imageTransFormConfig : {}} 590 | /> 591 |
592 |
593 | ) : ( 594 | undefined 595 | )} 596 | 597 | {toggleText ? ( 598 | text.split(/\n/).map((value, i) => { 599 | return ( 600 | 606 |
615 | {value} 616 |
617 |
618 | ); 619 | }) 620 | ) : ( 621 | 622 |
630 |                         {text}
631 |                       
632 |
633 | )} 634 |
635 |
636 | 637 | 638 | (this.file = node)} 643 | onChange={this.imageChange} 644 | /> 645 | 646 | 655 | 656 | 657 | 665 | 666 | 667 | 668 | 669 | {operationRow({ 670 | label: "文字", 671 | component: [ 672 |