├── 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 | 
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 |
429 | - 支持图片拖拽和粘贴
430 | - 选择图片后可使用鼠标滚轮缩放
431 | - 每行文字可单独拖拽
432 |
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 |
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 | ,
680 |
685 | 每行文字独立控制
686 |
687 | ]
688 | })}
689 | {operationRow({
690 | icon: "file-ppt",
691 | label: "字体",
692 | component: (
693 |
704 | )
705 | })}
706 | {operationRow({
707 | icon: "pie-chart",
708 | label: "文字颜色",
709 | component: (
710 |
711 |
722 | {displayColorPicker ? (
723 |
733 | ) : (
734 | undefined
735 | )}
736 |
737 | )
738 | })}
739 | {operationRow({
740 | icon: "line-chart",
741 | label: "图片大小",
742 | component: (
743 |
744 |
745 |
752 |
753 |
754 |
761 |
762 |
763 | )
764 | })}
765 | {operationRow({
766 | icon: "file-word",
767 | label: "文字大小",
768 | component: (
769 | `${value}px`}
775 | onChange={this.fontSizeChange}
776 | marks={{
777 | [FONT_SIZE[0]]: `${FONT_SIZE[0]}px`,
778 | [FONT_SIZE[FONT_SIZE.length - 1]]: `${[
779 | FONT_SIZE[FONT_SIZE.length - 1]
780 | ]}px`
781 | }}
782 | />
783 | )
784 | })}
785 | {operationRow({
786 | icon: "picture",
787 | label: "图像旋转",
788 | component: (
789 |
794 | loadingImgReady ? `${value}°` : "请选择图片"
795 | }
796 | onChange={this.rotateImage}
797 | disabled={!loadingImgReady}
798 | marks={{
799 | 0: "0°(无旋转)",
800 | 90: "90°",
801 | 180: "180°",
802 | 360: "360°(无旋转)"
803 | }}
804 | />
805 | )
806 | })}
807 | {operationRow({
808 | icon: "share-alt",
809 | label: "图像翻转",
810 | component: (
811 |
812 |
813 |
819 | loadingImgReady ? `${value}°` : "请选择图片"
820 | }
821 | onChange={this.turnImage}
822 | disabled={!loadingImgReady}
823 | marks={{
824 | 0: "0°(无翻转)",
825 | 90: "90°",
826 | 180: "180°",
827 | 360: "360°(无翻转)"
828 | }}
829 | />
830 |
831 |
832 |
836 | X轴翻转
837 | Y轴翻转
838 |
839 |
840 |
841 | )
842 | })}
843 | {operationRow({
844 | icon: "skin",
845 | label: "图片压缩",
846 | component: (
847 | 压缩
848 | )
849 | })}
850 |
851 |
852 |
864 |
865 |
866 |
867 |
868 |
869 |
870 |
871 | GitHub
872 | {" "}
873 | (version:{APPVERSION}){" "}
874 |
875 | 使用说明
876 |
877 |
878 |
879 |
888 |
899 |
900 |
909 |
911 |
912 | );
913 | }
914 | componentWillUnmount() {
915 | const { drag, paste } = this.props;
916 | paste && this.unBindPasteListener(document.body);
917 | this.video = null;
918 | }
919 | componentDidMount() {
920 | const { drag, paste } = this.props;
921 | drag && this.bindDragListener(this.previewContent);
922 | paste && this.bindPasteListener(document.body);
923 | }
924 | }
925 |
926 | const _ReactMemeGenerator = Form.create()(ReactMemeGenerator);
927 |
928 | export default hot(module)(_ReactMemeGenerator);
929 |
--------------------------------------------------------------------------------