├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── github-pages.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── LICENSE ├── _config.yml ├── babel.config.js ├── demo ├── index.js ├── main.js └── scss │ ├── iconfont.scss │ ├── index.scss │ ├── main.scss │ ├── mixin │ ├── clearfix.scss │ ├── common.scss │ └── iconfont.scss │ ├── normalize.scss │ ├── theme │ └── _default.scss │ └── variable.scss ├── html ├── 404.html └── index.html ├── index.js ├── jsdoc.conf.json ├── package-lock.json ├── package.json ├── postcss.config.js ├── public └── demo.jpeg ├── readme.md ├── rollup.config.js ├── src ├── command.js ├── commands │ ├── add-object.js │ ├── base.js │ ├── clear.js │ ├── load-image.js │ ├── remove.js │ ├── rotation-image.js │ └── zoom.js ├── consts.js ├── index.js ├── lib │ ├── canvas-to-blob.js │ ├── custom-event.js │ ├── event.js │ ├── shape-resize-helper.js │ └── util.js ├── module.js ├── modules │ ├── arrow.2.js │ ├── arrow.js │ ├── base.js │ ├── cropper.js │ ├── draw.js │ ├── image-loader.js │ ├── line.js │ ├── main.js │ ├── mosaic.1.js │ ├── mosaic.2.js │ ├── mosaic.js │ ├── pan.js │ ├── rotation.js │ ├── shape.js │ └── text.js └── shape │ ├── arrow.js │ ├── cropzone.js │ └── mosaic.js ├── webpack.config.js └── website ├── .fatherrc.ts ├── .gitignore ├── .npmrc ├── .umirc.ts ├── README.md ├── docs └── index.md ├── package.json ├── public └── images │ └── demo.jpeg ├── src ├── index.ts └── scss │ ├── iconfont.scss │ ├── index.scss │ ├── main.scss │ ├── mixin │ ├── clearfix.scss │ ├── common.scss │ └── iconfont.scss │ ├── normalize.scss │ ├── theme │ └── _default.scss │ └── variable.scss ├── tsconfig.json └── typings.d.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 4 12 | 13 | # Matches multiple files with brace expansion notation 14 | # Set default charset 15 | [*.{js,py}] 16 | charset = utf-8 17 | 18 | # 4 space indentation 19 | [*.py] 20 | indent_style = space 21 | indent_size = 4 22 | 23 | # Tab indentation (no size specified) 24 | [Makefile] 25 | indent_style = tab 26 | 27 | # Matches the exact files either package.json or .travis.yml 28 | [{package.json,.travis.yml}] 29 | indent_style = space 30 | indent_size = 4 31 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/**/*.js 2 | *.json 3 | dist/**/*.js 4 | babel/*.js 5 | postcss.config.js 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['airbnb', 'prettier'], 3 | parser: 'babel-eslint', 4 | plugins: ['jest'], 5 | env: { 6 | 'jest/globals': true 7 | }, 8 | globals: { 9 | window: true, 10 | Blob: true, 11 | atob: true, 12 | document: true 13 | }, 14 | settings: {}, 15 | rules: { 16 | 'max-lines': ['error', { max: 600, skipComments: true, skipBlankLines: true }], 17 | 'import/no-extraneous-dependencies': 'off', 18 | 'react/prop-types': 'off', 19 | 'jest/no-disabled-tests': 'warn', 20 | 'jest/no-focused-tests': 'error', 21 | 'jest/no-identical-title': 'error', 22 | 'jest/prefer-to-have-length': 'warn', 23 | 'jest/valid-expect': 'error', 24 | 'react/react-in-jsx-scope': 'off', 25 | 'react/jsx-filename-extension': 'off', 26 | 'react/jsx-no-undef': 'off', 27 | 'react/jsx-indent': 'off', 28 | 'react/no-access-state-in-setstate': 'off', 29 | 'no-shadow': 'off', 30 | calemcase: 'off', 31 | 'class-methods-use-this': 'off', 32 | 'react/destructuring-assignment': 'off', 33 | 'consistent-return': 'off', 34 | 'array-callback-return': 'off', 35 | 'import/named': 'off', 36 | 'import/prefer-default-export': 1, 37 | 'one-var': 'off', 38 | 'no-underscore-dangle': 'off', 39 | 'no-plusplus': 'off', 40 | camelcase: 1, 41 | 'no-console': 'off', 42 | 'no-empty': 1, 43 | 'no-unused-expressions': 1, 44 | 'no-multi-assign': 'off', 45 | 'import/first': 1, 46 | 'prefer-promise-reject-errors': 1, 47 | 'import/extensions': 'off', 48 | 'prefer-const': 1, 49 | 'no-bitwise': 'off', 50 | 'no-restricted-syntax': 1, 51 | 'no-param-reassign': 1, 52 | 'no-nested-ternary': 1, 53 | 'no-control-regex': 'off', 54 | 'react/no-unknown-property': 'off', 55 | 'react/jsx-one-expression-per-line': 'off', 56 | 'react/button-has-type': 'off', 57 | 'spaced-comment': 'off', 58 | 'react/sort-comp': 'off' 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /.github/workflows/github-pages.yml: -------------------------------------------------------------------------------- 1 | name: Example Build & Deploy to GitHub Pages 2 | on: 3 | push: 4 | branches: 5 | - master 6 | tags: 7 | - v1 8 | paths: 9 | - 'src/**' 10 | - 'website/**' 11 | - '.github/**' 12 | repository_dispatch: 13 | jobs: 14 | build: 15 | name: Build 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 🛎️ 19 | uses: actions/checkout@master 20 | with: 21 | persist-credentials: false 22 | - name: Cache node modules 23 | uses: actions/cache@v1 24 | with: 25 | path: ~/.npm 26 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 27 | restore-keys: | 28 | ${{ runner.os }}-build-${{ env.cache-name }}- 29 | ${{ runner.os }}-build- 30 | ${{ runner.os }}- 31 | - name: use Node.js 10 32 | uses: actions/setup-node@v1 33 | with: 34 | node-version: '10.x' 35 | - name: npm script 🔧 36 | run: | 37 | npm install --registry=https://registry.npmjs.com 38 | npm run install:website 39 | npm run build:website 40 | env: 41 | CI: true 42 | - name: Deploy 🚀 43 | uses: JamesIves/github-pages-deploy-action@releases/v3 44 | with: 45 | ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} 46 | BRANCH: gh-pages 47 | FOLDER: website/dist 48 | GIT_CONFIG_NAME: ${{ secrets.GIT_CONFIG_NAME}} 49 | GIT_CONFIG_EMAIL: ${{ secrets.GIT_CONFIG_EMAIL}} 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | out 4 | dist 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npm.taobao.org/ 2 | disturl=https://npm.taobao.org/dist 3 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 10 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | src/login 2 | src/lib/npm/**/*.js 3 | src/lib/wxapp-mobx/**/*.js 4 | src/lib/wxParse/**/*.js -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 4, 4 | "singleQuote": true, 5 | "semi": true, 6 | "arrowParens": "always", 7 | "bracketSpacing": true 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-present, Yuxi (Evan) You 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-architect -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(false); 3 | return { 4 | presets: [ 5 | [ 6 | '@babel/preset-env', 7 | { 8 | targets: { 9 | browsers: ['safari >= 9', 'android >= 4.0'] 10 | } 11 | } 12 | ], 13 | '@babel/preset-react' 14 | ], 15 | ignore: [], 16 | comments: false, 17 | plugins: [ 18 | [ 19 | '@babel/plugin-proposal-decorators', 20 | { 21 | legacy: true 22 | } 23 | ], 24 | '@babel/plugin-syntax-dynamic-import', 25 | '@babel/plugin-syntax-import-meta', 26 | '@babel/plugin-proposal-class-properties', 27 | '@babel/plugin-proposal-json-strings', 28 | '@babel/plugin-proposal-function-sent', 29 | '@babel/plugin-proposal-export-namespace-from', 30 | '@babel/plugin-proposal-numeric-separator', 31 | '@babel/plugin-proposal-throw-expressions', 32 | '@babel/plugin-proposal-export-default-from', 33 | '@babel/plugin-proposal-logical-assignment-operators', 34 | '@babel/plugin-proposal-optional-chaining', 35 | [ 36 | '@babel/plugin-proposal-pipeline-operator', 37 | { 38 | proposal: 'minimal' 39 | } 40 | ], 41 | '@babel/plugin-proposal-nullish-coalescing-operator', 42 | '@babel/plugin-proposal-do-expressions', 43 | '@babel/plugin-proposal-function-bind' 44 | ] 45 | }; 46 | }; 47 | -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by yeanzhi on 16/12/1. 3 | */ 4 | 'use strict'; 5 | import 'babel-polyfill'; 6 | import './scss/index.scss'; 7 | import React from 'react'; 8 | import ReactDOM from 'react-dom'; 9 | import Main from './main'; 10 | ReactDOM.render( 11 |
12 |
13 |
, 14 | document.getElementById('demo_container') 15 | ); -------------------------------------------------------------------------------- /demo/scss/iconfont.scss: -------------------------------------------------------------------------------- 1 | // font-face 2 | // @icon-url: 字体源文件的地址 3 | @font-face { 4 | 5 | 6 | font-family: 'dxicon'; 7 | 8 | src: url('#{$icon-url}.eot'); /* IE9*/ 9 | src: url('#{$icon-url}.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 10 | url('#{$icon-url}.woff') format('woff'), /* chrome、firefox */ 11 | url('#{$icon-url}.ttf') format('truetype'), /* chrome、firefox、opera、Safari, Android, iOS 4.2+*/ 12 | url('#{$icon-url}.svg#dxicon') format('svg'); /* iOS 4.1- */ 13 | 14 | } 15 | 16 | .#{$iconfont-css-prefix} { 17 | @include iconfont-mixin; 18 | -webkit-font-smoothing: antialiased; 19 | -moz-osx-font-smoothing: grayscale; 20 | } 21 | 22 | .#{$iconfont-css-prefix}-setting:before { 23 | content: "\e654"; 24 | } 25 | 26 | .#{$iconfont-css-prefix}-group-setting:before { 27 | content: "\e676"; 28 | } 29 | 30 | .#{$iconfont-css-prefix}-todo:before { 31 | content: "\e62d"; 32 | } 33 | 34 | .#{$iconfont-css-prefix}-right:before { 35 | content: "\e678"; 36 | } 37 | 38 | .#{$iconfont-css-prefix}-news:before { 39 | content: "\e657"; 40 | } 41 | 42 | .#{$iconfont-css-prefix}-contact:before { 43 | content: "\e651"; 44 | } 45 | 46 | .#{$iconfont-css-prefix}-group:before { 47 | content: "\e655"; 48 | } 49 | 50 | .#{$iconfont-css-prefix}-group1:before { 51 | content: "\e675"; 52 | } 53 | 54 | .#{$iconfont-css-prefix}-group2:before { 55 | content: "\e68c"; 56 | } 57 | 58 | .#{$iconfont-css-prefix}-addchat:before { 59 | content: "\e686"; 60 | } 61 | 62 | .#{$iconfont-css-prefix}-addapp:before { 63 | content: "\e602"; 64 | } 65 | 66 | .#{$iconfont-css-prefix}-xiangshang:before { 67 | content: "\e62c"; 68 | } 69 | 70 | .#{$iconfont-css-prefix}-arrow-down:before { 71 | content: "\e66b"; 72 | } 73 | 74 | //实心向下尖头 75 | 76 | .#{$iconfont-css-prefix}-arrow-right:before { 77 | content: "\e66a"; 78 | } 79 | 80 | .#{$iconfont-css-prefix}-mute:before { 81 | content: "\e670"; 82 | } 83 | 84 | .#{$iconfont-css-prefix}-round_close:before { 85 | content: "\e673"; 86 | } 87 | 88 | //缺失 89 | .#{$iconfont-css-prefix}-search:before { 90 | content: "\e66e"; 91 | } 92 | 93 | .#{$iconfont-css-prefix}-unrecognized:before { 94 | content: "\e66f"; 95 | } 96 | 97 | .#{$iconfont-css-prefix}-zhankai:before { 98 | content: "\e656"; 99 | } 100 | 101 | .#{$iconfont-css-prefix}-search-close:before { 102 | content: "\e679"; 103 | } 104 | 105 | .#{$iconfont-css-prefix}-pull_right:before { 106 | content: "\e674"; 107 | } 108 | 109 | .#{$iconfont-css-prefix}-message:before { 110 | content: "\e690"; 111 | } 112 | 113 | .#{$iconfont-css-prefix}-checkbox:before { 114 | content: "\e668"; 115 | } 116 | 117 | .#{$iconfont-css-prefix}-checkbox-checked:before { 118 | content: "\e669"; 119 | } 120 | 121 | .#{$iconfont-css-prefix}-radio_box:before { 122 | content: "\e604"; 123 | } 124 | 125 | .#{$iconfont-css-prefix}-radio_box_fill:before { 126 | content: "\e605"; 127 | } 128 | 129 | .#{$iconfont-css-prefix}-gender-woman:before { 130 | content: "\e66d"; 131 | } 132 | 133 | .#{$iconfont-css-prefix}-gender-man:before { 134 | content: "\e66c"; 135 | } 136 | 137 | .#{$iconfont-css-prefix}-star-full:before { 138 | content: "\e680"; 139 | } 140 | 141 | .#{$iconfont-css-prefix}-star:before { 142 | content: "\e67f"; 143 | } 144 | 145 | .#{$iconfont-css-prefix}-folder:before { 146 | content: "\e67e"; 147 | } 148 | 149 | .#{$iconfont-css-prefix}-sendfile:before { 150 | content: "\e61b"; 151 | } 152 | 153 | .#{$iconfont-css-prefix}-screenshot:before { 154 | content: "\e62b"; 155 | } 156 | 157 | .#{$iconfont-css-prefix}-unfold:before { 158 | content: "\e67b"; 159 | } 160 | 161 | .#{$iconfont-css-prefix}-emoji:before { 162 | content: "\e610"; 163 | } 164 | 165 | .#{$iconfont-css-prefix}-plus:before { 166 | content: "\e691"; 167 | } 168 | 169 | .#{$iconfont-css-prefix}-post:before { 170 | content: "\e600"; 171 | } 172 | 173 | // 174 | .#{$iconfont-css-prefix}-more:before { 175 | content: "\e652"; 176 | } 177 | 178 | .#{$iconfont-css-prefix}-file:before { 179 | content: "\e650"; 180 | } 181 | 182 | .#{$iconfont-css-prefix}-kefu:before { 183 | content: "\e653"; 184 | } 185 | 186 | 187 | //toastr 使用的 188 | .#{$iconfont-css-prefix}-error:before { 189 | content: "\e693"; 190 | } 191 | 192 | .#{$iconfont-css-prefix}-confirm:before { 193 | content: "\e684"; 194 | } 195 | 196 | //app 自定义配置 197 | .#{$iconfont-css-prefix}-confirm-fill:before { 198 | content: "\e603"; 199 | } 200 | 201 | .#{$iconfont-css-prefix}-remove-fill:before { 202 | content: "\e601"; 203 | } 204 | 205 | .#{$iconfont-css-prefix}-left_voice_0:before { 206 | content: "\e681"; 207 | } 208 | 209 | .#{$iconfont-css-prefix}-left_voice_1:before { 210 | content: "\e682"; 211 | } 212 | 213 | .#{$iconfont-css-prefix}-left_voice_2:before { 214 | content: "\e683"; 215 | } 216 | 217 | .#{$iconfont-css-prefix}-right_voice_0:before { 218 | content: "\e60b"; 219 | } 220 | 221 | .#{$iconfont-css-prefix}-right_voice_1:before { 222 | content: "\e60c"; 223 | } 224 | 225 | .#{$iconfont-css-prefix}-right_voice_2:before { 226 | content: "\e60d"; 227 | } 228 | 229 | .#{$iconfont-css-prefix}-remove:before { 230 | content: "\e673"; 231 | } 232 | 233 | .#{$iconfont-css-prefix}-warning:before { 234 | content: "\e672"; 235 | } 236 | 237 | .#{$iconfont-css-prefix}-remove-group:before { 238 | content: "\e694"; 239 | } 240 | 241 | .#{$iconfont-css-prefix}-round_down:before { 242 | content: "\e687"; 243 | } 244 | 245 | .#{$iconfont-css-prefix}-quit:before { 246 | content: "\e67a"; 247 | } 248 | 249 | .#{$iconfont-css-prefix}-crown:before { 250 | content: "\e667"; 251 | } 252 | 253 | .#{$iconfont-css-prefix}-info:before { 254 | content: "\e677"; 255 | } 256 | 257 | .#{$iconfont-css-prefix}-bubble-leader:before { 258 | content: "\e607"; 259 | } 260 | 261 | .#{$iconfont-css-prefix}-bubble-star:before { 262 | content: "\e608"; 263 | } 264 | 265 | .#{$iconfont-css-prefix}-yunpan:before { 266 | content: "\e613"; 267 | } 268 | 269 | .#{$iconfont-css-prefix}-yunpan-close:before { 270 | content: "\e60a"; 271 | } 272 | 273 | .#{$iconfont-css-prefix}-gongzongpingtai:before { 274 | content: "\e68b"; 275 | } 276 | 277 | .#{$iconfont-css-prefix}-dalaba:before { 278 | content: "\e689"; 279 | } 280 | 281 | .#{$iconfont-css-prefix}-message-fail:before { 282 | content: "\e606"; 283 | } 284 | 285 | .#{$iconfont-css-prefix}-quote:before { 286 | content: "\e627"; 287 | } 288 | 289 | .#{$iconfont-css-prefix}-chehui:before { 290 | content: "\e626"; 291 | } 292 | 293 | .#{$iconfont-css-prefix}-close_notice:before { 294 | content: "\e670"; 295 | } 296 | 297 | .#{$iconfont-css-prefix}-feedback:before { 298 | content: "\e614"; 299 | } 300 | 301 | .#{$iconfont-css-prefix}-open-qr:before { 302 | content: "\e61a"; 303 | } 304 | 305 | .#{$iconfont-css-prefix}-close-qr:before { 306 | content: "\e619"; 307 | } 308 | 309 | .#{$iconfont-css-prefix}-view-qr:before { 310 | content: "\e618"; 311 | } 312 | 313 | .#{$iconfont-css-prefix}-lfc:before { //left full corner 314 | content: "\e615"; 315 | } 316 | 317 | .#{$iconfont-css-prefix}-llc:before { 318 | content: "\e616"; 319 | } 320 | 321 | .#{$iconfont-css-prefix}-rfc:before { //left full corner 322 | content: "\e611"; 323 | } 324 | 325 | .#{$iconfont-css-prefix}-rlc:before { 326 | content: "\e617"; 327 | } 328 | .#{$iconfont-css-prefix}-i1000:before { 329 | content: "\e60e"; 330 | } 331 | 332 | .#{$iconfont-css-prefix}-survey:before { 333 | content: "\e60f"; 334 | } 335 | 336 | .#{$iconfont-css-prefix}-moremessage:before { 337 | content: "\e622"; 338 | } 339 | .#{$iconfont-css-prefix}-forward:before { 340 | content: "\e628"; 341 | } 342 | 343 | .#{$iconfont-css-prefix}-checkboxChecked:before { 344 | content: "\e620"; 345 | } 346 | .#{$iconfont-css-prefix}-checkboxUncheck:before { 347 | content: "\e61f"; 348 | } 349 | .#{$iconfont-css-prefix}-cancelChecked:before { 350 | content: "\e61e"; 351 | } 352 | 353 | .#{$iconfont-css-prefix}-tag-receipt:before { 354 | content: "\e61d"; 355 | } 356 | 357 | .#{$iconfont-css-prefix}-send-receipt:before { 358 | content: "\e61c"; 359 | } 360 | 361 | .#{$iconfont-css-prefix}-daiban:before { 362 | content: "\e625"; 363 | } 364 | .#{$iconfont-css-prefix}-backArrow:before { 365 | content: "\e623"; 366 | } 367 | .#{$iconfont-css-prefix}-update:before { 368 | content: "\e624"; 369 | } 370 | .#{$iconfont-css-prefix}-checked:before { 371 | content: "\e629"; 372 | } 373 | .#{$iconfont-css-prefix}-toastr-close:before { 374 | content: "\e62a"; 375 | } 376 | .#{$iconfont-css-prefix}-question-mark:before { 377 | content: "\e621"; 378 | } 379 | .#{$iconfont-css-prefix}-daily-qun:before { 380 | content: "\e62e"; 381 | } 382 | /*图片编辑器*/ 383 | .#{$iconfont-css-prefix}-image-fangda:before { 384 | content: "\e638"; 385 | } 386 | .#{$iconfont-css-prefix}-image-gou:before { 387 | content: "\e637"; 388 | } 389 | .#{$iconfont-css-prefix}-image-xuanzhuan:before { 390 | content: "\e636"; 391 | } 392 | .#{$iconfont-css-prefix}-image-masaike:before { 393 | content: "\e635"; 394 | } 395 | .#{$iconfont-css-prefix}-image-suoxiao:before { 396 | content: "\e634"; 397 | } 398 | .#{$iconfont-css-prefix}-image-text:before { 399 | content: "\e633"; 400 | } 401 | .#{$iconfont-css-prefix}-image-huabi:before { 402 | content: "\e632"; 403 | } 404 | .#{$iconfont-css-prefix}-image-jiantou:before { 405 | content: "\e631"; 406 | } 407 | .#{$iconfont-css-prefix}-image-guanbi:before { 408 | content: "\e630"; 409 | } 410 | .#{$iconfont-css-prefix}-image-jiancai:before { 411 | content: "\e62f"; 412 | } 413 | /* end */ 414 | -------------------------------------------------------------------------------- /demo/scss/index.scss: -------------------------------------------------------------------------------- 1 | @import "./variable"; 2 | @import "./mixin/iconfont"; 3 | @import "./mixin/clearfix"; 4 | @import "./theme/_default"; 5 | @import "./normalize"; 6 | @import "./iconfont"; 7 | @import "./main"; -------------------------------------------------------------------------------- /demo/scss/main.scss: -------------------------------------------------------------------------------- 1 | .wrap_inner { 2 | width: 700px; 3 | height: 500px; 4 | margin: 0 auto; 5 | display: block; 6 | position: relative; 7 | .main { 8 | height: 100%; 9 | width: 100%; 10 | margin-top:50px; 11 | .upload-file-image-preview { 12 | height: 400px; 13 | width: 700px; 14 | text-align: center; 15 | .xm-fabric-photo-editor-canvas-container { 16 | display: inline-block; 17 | } 18 | } 19 | .file-button { 20 | height: 100px; 21 | width: 100% 22 | } 23 | .file-info-progress { 24 | height: 4px; 25 | background-color: $chat-bg-color; 26 | border-radius: 2px; 27 | position: absolute; 28 | bottom: 0px; 29 | left: 0; 30 | right: 0; 31 | .file-info-progress-bar { 32 | width: 0; 33 | height: 4px; 34 | background-color: $success; 35 | border-radius: 2px; 36 | } 37 | } 38 | .file-button.upload-success { 39 | width: 100%; 40 | height: 64px; 41 | } 42 | .file-button { 43 | display: flex; 44 | flex-direction: row; 45 | justify-content: space-between; 46 | padding: 14px 20px; 47 | box-sizing: border-box; 48 | .ctn-tips { 49 | flex: 1; 50 | position: relative; 51 | padding: 18px 16px; 52 | -webkit-user-select: none; 53 | -moz-user-select: none; 54 | .text { 55 | color: $primary; 56 | cursor: pointer; 57 | } 58 | .moretips { 59 | position: absolute; 60 | top: -108px; 61 | background: rgba(0, 0, 0, 0.87); 62 | border-radius: 4px; 63 | color: white; 64 | width: 520px; 65 | padding: 13px 20px 10px 20px; 66 | ol { 67 | padding: 0 20px; 68 | li { 69 | list-style: initial; 70 | } 71 | } 72 | .close { 73 | color: white; 74 | top: 0; 75 | right: 0; 76 | .dxicon { 77 | font-style: 10px; 78 | color: rgba(255, 255, 255, 0.69); 79 | } 80 | } 81 | .arrow { 82 | position: absolute; 83 | bottom: -13px; 84 | left: 40px; 85 | .dxicon { 86 | color: rgba(0, 0, 0, 0.87); 87 | font-size: 17px; 88 | } 89 | } 90 | } 91 | } 92 | .image-thumb-btns { 93 | vertical-align: middle; 94 | font-size: 0; 95 | &:before { 96 | content: ''; 97 | display: inline-block; 98 | vertical-align: middle; 99 | font-size: 0; 100 | width: 0; 101 | height: 100%; 102 | } 103 | .thumb-divider { 104 | display: inline-block; 105 | height: 2px; 106 | border-bottom: 1px solid #eee; 107 | width: 60px; 108 | margin: 0 9px; 109 | } 110 | i { 111 | font-size: 18px; 112 | cursor: pointer; 113 | &:hover { 114 | color: $primary; 115 | } 116 | } 117 | } 118 | .image-tools-btns { 119 | vertical-align: middle; 120 | position: relative; 121 | .tools-divider { 122 | width: 1px; 123 | height: 24px; 124 | background: rgba(0,0,0,0.10); 125 | display: inline-block; 126 | vertical-align: middle; 127 | } 128 | i { 129 | font-size: 24px; 130 | margin-left: 14px; 131 | cursor: pointer; 132 | &:hover { 133 | color: $primary; 134 | } 135 | } 136 | .file-button-cancel { 137 | background: #FFFFFF; 138 | border: 1px solid rgba(0, 0, 0, 0.38); 139 | border-radius: 2px; 140 | height: 20px; 141 | width: 34px; 142 | font-size: 12px; 143 | color: rgba(0, 0, 0, 0.54); 144 | letter-spacing: 0; 145 | line-height: 18px; 146 | padding: 2px; 147 | cursor: pointer; 148 | } 149 | .tools-panel { 150 | position: absolute; 151 | top: 40px; 152 | width: 350px; 153 | height: 46px; 154 | box-sizing: border-box; 155 | background: #FFFFFF; 156 | box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.24); 157 | border-radius: 4px; 158 | padding: 8px 0; 159 | .tools-panel-brush { 160 | display: inline-block; 161 | div { 162 | height: 24px; 163 | width: 24px; 164 | display: inline-block; 165 | margin-left: 14px; 166 | text-align: center; 167 | vertical-align: middle; 168 | cursor: pointer; 169 | &:hover { 170 | span { 171 | background: rgba(0, 0, 0, 0.54); 172 | } 173 | } 174 | span { 175 | background: rgba(0, 0, 0, 0.24); 176 | &.active { 177 | background: rgba(0, 0, 0, 0.54); 178 | } 179 | } 180 | .small-brush { 181 | border-radius: 50%; 182 | width: 4px; 183 | height: 4px; 184 | display: inline-block; 185 | } 186 | .normal-brush { 187 | border-radius: 50%; 188 | width: 8px; 189 | height: 8px; 190 | display: inline-block; 191 | } 192 | .big-brush { 193 | border-radius: 50%; 194 | width: 12px; 195 | height: 12px; 196 | display: inline-block; 197 | } 198 | } 199 | } 200 | .tools-panel-color { 201 | display: inline-block; 202 | .color { 203 | border: 1px solid rgba(0, 0, 0, 0.10); 204 | border-radius: 2px; 205 | height: 16px; 206 | width: 16px; 207 | display: inline-block; 208 | margin-right: 8px; 209 | cursor: pointer; 210 | &.active { 211 | height: 22px; 212 | width: 22px; 213 | } 214 | &:hover { 215 | height: 22px; 216 | width: 22px; 217 | } 218 | &.red { 219 | background: #FF3440; 220 | } 221 | &.yellow { 222 | background: #FFCF50; 223 | } 224 | &.green { 225 | background: #00A344; 226 | } 227 | &.blue { 228 | background: #0DA9D6; 229 | } 230 | &.grey { 231 | background: #999999; 232 | } 233 | &.black { 234 | background: #000000; 235 | } 236 | &.white { 237 | background: #FFFFFF; 238 | } 239 | } 240 | } 241 | } 242 | } 243 | .ctn-btns { 244 | text-align: center; 245 | button { 246 | width: 88px; 247 | height: 36px; 248 | padding: 8px 0; 249 | } 250 | } 251 | &.upload-success { 252 | width: 100%; 253 | height: 56px; 254 | margin: 10px auto 0 auto; 255 | } 256 | } 257 | .file-button--pc { 258 | justify-content: flex-end; 259 | } 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /demo/scss/mixin/clearfix.scss: -------------------------------------------------------------------------------- 1 | // mixins for clearfix 2 | // ------------------------ 3 | @mixin clearfix() { 4 | zoom: 1; 5 | &:before, 6 | &:after { 7 | content: " "; 8 | display: table; 9 | } 10 | &:after { 11 | clear: both; 12 | visibility: hidden; 13 | font-size: 0; 14 | height: 0; 15 | } 16 | } -------------------------------------------------------------------------------- /demo/scss/mixin/common.scss: -------------------------------------------------------------------------------- 1 | @mixin cyclize-avatar($radius) { 2 | width: $radius * 2; 3 | border-radius: $radius; 4 | } 5 | -------------------------------------------------------------------------------- /demo/scss/mixin/iconfont.scss: -------------------------------------------------------------------------------- 1 | @mixin iconfont-mixin { 2 | display: inline-block; 3 | font-style: normal; 4 | vertical-align: middle; 5 | text-align: center; 6 | text-transform: none; 7 | text-rendering: auto; 8 | line-height: 1; 9 | font-size: 14px; 10 | 11 | &:before { 12 | display: block; 13 | font-family: "dxicon" !important; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /demo/scss/normalize.scss: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | // 4 | // 1. Set default font family to sans-serif. 5 | // 2. Prevent iOS and IE text size adjust after device orientation change, 6 | // without disabling user zoom. 7 | // 8 | 9 | html { 10 | font-family: sans-serif; // 1 11 | -ms-text-size-adjust: 100%; // 2 12 | -webkit-text-size-adjust: 100%; // 2 13 | } 14 | 15 | // 16 | // Remove default margin. 17 | // 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | // HTML5 display definitions 24 | // ========================================================================== 25 | 26 | // 27 | // Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | // Correct `block` display not defined for `details` or `summary` in IE 10/11 29 | // and Firefox. 30 | // Correct `block` display not defined for `main` in IE 11. 31 | // 32 | 33 | article, 34 | aside, 35 | details, 36 | figcaption, 37 | figure, 38 | footer, 39 | header, 40 | hgroup, 41 | main, 42 | menu, 43 | nav, 44 | section, 45 | summary { 46 | display: block; 47 | } 48 | 49 | // 50 | // 1. Correct `inline-block` display not defined in IE 8/9. 51 | // 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 52 | // 53 | 54 | audio, 55 | canvas, 56 | progress, 57 | video { 58 | display: inline-block; // 1 59 | vertical-align: baseline; // 2 60 | } 61 | 62 | // 63 | // Prevent modern browsers from displaying `audio` without controls. 64 | // Remove excess height in iOS 5 devices. 65 | // 66 | 67 | audio:not([controls]) { 68 | display: none; 69 | height: 0; 70 | } 71 | 72 | // 73 | // Address `[hidden]` styling not present in IE 8/9/10. 74 | // Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22. 75 | // 76 | 77 | [hidden], 78 | template { 79 | display: none; 80 | } 81 | 82 | // Links 83 | // ========================================================================== 84 | 85 | // 86 | // Remove the gray background color from active links in IE 10. 87 | // 88 | 89 | a { 90 | background-color: transparent; 91 | } 92 | 93 | // 94 | // Improve readability of focused elements when they are also in an 95 | // active/hover state. 96 | // 97 | 98 | a:active, 99 | a:hover { 100 | outline: 0; 101 | } 102 | 103 | // Text-level semantics 104 | // ========================================================================== 105 | 106 | // 107 | // Address styling not present in IE 8/9/10/11, Safari, and Chrome. 108 | // 109 | 110 | abbr[title] { 111 | border-bottom: 1px dotted; 112 | } 113 | 114 | // 115 | // Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 116 | // 117 | 118 | b, 119 | strong { 120 | font-weight: bold; 121 | } 122 | 123 | // 124 | // Address styling not present in Safari and Chrome. 125 | // 126 | 127 | dfn { 128 | font-style: italic; 129 | } 130 | 131 | // 132 | // Address variable `h1` font-size and margin within `section` and `article` 133 | // contexts in Firefox 4+, Safari, and Chrome. 134 | // 135 | 136 | h1 { 137 | font-size: 2em; 138 | margin: 0.67em 0; 139 | } 140 | 141 | // 142 | // Address styling not present in IE 8/9. 143 | // 144 | 145 | mark { 146 | background: #ff0; 147 | color: #000; 148 | } 149 | 150 | // 151 | // Address inconsistent and variable font size in all browsers. 152 | // 153 | 154 | small { 155 | font-size: 80%; 156 | } 157 | 158 | // 159 | // Prevent `sub` and `sup` affecting `line-height` in all browsers. 160 | // 161 | 162 | sub, 163 | sup { 164 | font-size: 75%; 165 | line-height: 0; 166 | position: relative; 167 | vertical-align: baseline; 168 | } 169 | 170 | sup { 171 | top: -0.5em; 172 | } 173 | 174 | sub { 175 | bottom: -0.25em; 176 | } 177 | 178 | // Embedded content 179 | // ========================================================================== 180 | 181 | // 182 | // Remove border when inside `a` element in IE 8/9/10. 183 | // 184 | 185 | img { 186 | border: 0; 187 | } 188 | 189 | // 190 | // Correct overflow not hidden in IE 9/10/11. 191 | // 192 | 193 | svg:not(:root) { 194 | overflow: hidden; 195 | } 196 | 197 | // Grouping content 198 | // ========================================================================== 199 | 200 | // 201 | // Address margin not present in IE 8/9 and Safari. 202 | // 203 | 204 | figure { 205 | margin: 1em 40px; 206 | } 207 | 208 | // 209 | // Address differences between Firefox and other browsers. 210 | // 211 | 212 | hr { 213 | box-sizing: content-box; 214 | height: 0; 215 | } 216 | 217 | // 218 | // Contain overflow in all browsers. 219 | // 220 | 221 | //pre { 222 | // overflow: auto; 223 | //} 224 | 225 | // 226 | // Address odd `em`-unit font size rendering in all browsers. 227 | // 228 | 229 | code, 230 | kbd, 231 | pre, 232 | samp { 233 | font-family: monospace, monospace; 234 | font-size: 1em; 235 | } 236 | 237 | // Forms 238 | // ========================================================================== 239 | 240 | // 241 | // Known limitation: by default, Chrome and Safari on OS X allow very limited 242 | // styling of `select`, unless a `border` property is set. 243 | // 244 | 245 | // 246 | // 1. Correct color not being inherited. 247 | // Known issue: affects color of disabled elements. 248 | // 2. Correct font properties not being inherited. 249 | // 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 250 | // 251 | 252 | button, 253 | input, 254 | optgroup, 255 | select, 256 | textarea { 257 | color: inherit; // 1 258 | font: inherit; // 2 259 | margin: 0; // 3 260 | } 261 | 262 | // 263 | // Address `overflow` set to `hidden` in IE 8/9/10/11. 264 | // 265 | 266 | button { 267 | overflow: visible; 268 | } 269 | 270 | // 271 | // Address inconsistent `text-transform` inheritance for `button` and `select`. 272 | // All other form control elements do not inherit `text-transform` values. 273 | // Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 274 | // Correct `select` style inheritance in Firefox. 275 | // 276 | 277 | button, 278 | select { 279 | text-transform: none; 280 | } 281 | 282 | // 283 | // 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 284 | // and `video` controls. 285 | // 2. Correct inability to style clickable `input` types in iOS. 286 | // 3. Improve usability and consistency of cursor style between image-type 287 | // `input` and others. 288 | // 289 | 290 | button, 291 | html input[type="button"], // 1 292 | input[type="reset"], 293 | input[type="submit"] { 294 | -webkit-appearance: button; // 2 295 | cursor: pointer; // 3 296 | } 297 | 298 | // 299 | // Re-set default cursor for disabled elements. 300 | // 301 | 302 | button[disabled], 303 | html input[disabled] { 304 | cursor: default; 305 | } 306 | 307 | // 308 | // Remove inner padding and border in Firefox 4+. 309 | // 310 | 311 | button::-moz-focus-inner, 312 | input::-moz-focus-inner { 313 | border: 0; 314 | padding: 0; 315 | } 316 | 317 | // 318 | // Address Firefox 4+ groupSetting `line-height` on `input` using `!important` in 319 | // the UA stylesheet. 320 | // 321 | 322 | input { 323 | line-height: normal; 324 | } 325 | 326 | // 327 | // It's recommended that you don't attempt to style these elements. 328 | // Firefox's implementation doesn't respect box-sizing, padding, or width. 329 | // 330 | // 1. Address box sizing set to `content-box` in IE 8/9/10. 331 | // 2. Remove excess padding in IE 8/9/10. 332 | // 333 | 334 | input[type="checkbox"], 335 | input[type="radio"] { 336 | box-sizing: border-box; // 1 337 | padding: 0; // 2 338 | } 339 | 340 | // 341 | // Fix the cursor style for Chrome's increment/decrement buttons. For certain 342 | // `font-size` values of the `input`, it causes the cursor style of the 343 | // decrement button to change from `default` to `text`. 344 | // 345 | 346 | input[type="number"]::-webkit-inner-spin-button, 347 | input[type="number"]::-webkit-outer-spin-button { 348 | height: auto; 349 | } 350 | 351 | // 352 | // 1. Address `appearance` set to `searchfield` in Safari and Chrome. 353 | // 2. Address `box-sizing` set to `border-box` in Safari and Chrome. 354 | // 355 | 356 | input[type="search"] { 357 | -webkit-appearance: textfield; // 1 358 | box-sizing: content-box; //2 359 | } 360 | 361 | // 362 | // Remove inner padding and search cancel button in Safari and Chrome on OS X. 363 | // Safari (but not Chrome) clips the cancel button when the search input has 364 | // padding (and `textfield` appearance). 365 | // 366 | 367 | input[type="search"]::-webkit-search-cancel-button, 368 | input[type="search"]::-webkit-search-decoration { 369 | -webkit-appearance: none; 370 | } 371 | 372 | // 373 | // Define consistent border, margin, and padding. 374 | // 375 | 376 | fieldset { 377 | border: 1px solid #c0c0c0; 378 | margin: 0 2px; 379 | padding: 0.35em 0.625em 0.75em; 380 | } 381 | 382 | // 383 | // 1. Correct `color` not being inherited in IE 8/9/10/11. 384 | // 2. Remove padding so people aren't caught out if they zero out fieldsets. 385 | // 386 | 387 | legend { 388 | border: 0; // 1 389 | padding: 0; // 2 390 | } 391 | 392 | // 393 | // Remove default vertical scrollbar in IE 8/9/10/11. 394 | // 395 | 396 | textarea { 397 | overflow: auto; 398 | } 399 | 400 | // 401 | // Don't inherit the `font-weight` (applied by a rule above). 402 | // NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 403 | // 404 | 405 | optgroup { 406 | font-weight: bold; 407 | } 408 | 409 | // Tables 410 | // ========================================================================== 411 | 412 | // 413 | // Remove most spacing between table cells. 414 | // 415 | 416 | table { 417 | border-collapse: collapse; 418 | border-spacing: 0; 419 | } 420 | 421 | td, 422 | th { 423 | padding: 0; 424 | } 425 | 426 | ul, li{ 427 | padding:0; 428 | margin:0; 429 | } 430 | li{ 431 | list-style:none; 432 | } 433 | -------------------------------------------------------------------------------- /demo/scss/theme/_default.scss: -------------------------------------------------------------------------------- 1 | //上面的六种颜色是要删掉的,不要再项目中引用了 2 | //灰白色,这里只是取一些更普通的名称 3 | //$color-darker : rgba(0,0,0,.87);/*#333333*/ 4 | //$color-dark : rgba(0,0,0,.54);/*#666666*/ 5 | //$color-base : rgba(0,0,0,.38);/*#666666*/ 6 | //$color-medium : #CCCCCC; 7 | //$color-light : #E5E5E5; 8 | //$color-lighter : #F8F8F8; 9 | //$color-lightest : #FFFFFF; 10 | 11 | $border-color : rgba(0,0,0,0.1); 12 | $chat-bg-color : #F3F5F7;//右侧聊天窗口的背景色 rgba(255,255,255, 0.75) 13 | $session-bg-color : #E8EBEF;//右侧聊天窗口的背景色 rgba(255,255,255, 0.65) 14 | $right-bg-color : #F3F5F7; 15 | $right-chat-bg-color : transparent; 16 | $search-ipt-bg-color : rgba(255,255,255, 0.5); 17 | 18 | //主色 19 | $primary : #118BFB; 20 | $navigation-bg : #2E3E51; 21 | $navigation-hover-bg : rgba($primary, 0.1); 22 | $navigation-active-bg : rgba(16,139,251,0.15); 23 | $navigation-active-border : $primary; 24 | $navigation-color : rgba($color-lightest, 0.5);//导航栏icon颜色 25 | 26 | $divider-color : rgba($color-lightest,0.1); 27 | 28 | 29 | $hover-color: rgba($primary, 0.05);//hover时候的颜色 30 | $active-color: rgba($primary, 0.15);//激活时候的颜色 31 | 32 | 33 | //辅助颜色 34 | $toast-common : #F3F9FF; 35 | $toast-abnormal : #FFFAF2; 36 | $toast-error : #FFF7F6; 37 | $toast-success : #F7FBF5; 38 | 39 | 40 | 41 | $color-grey : #F4F4F4;//搜索框 42 | $color-you : #CAE5F9; 43 | $color-me : #E5E5E5; 44 | 45 | //输入框背景色 46 | $input-background-color : #FBFBFB; 47 | 48 | 49 | 50 | $bg-image : $chat-bg-color;//背景图片 51 | 52 | $main-bg-style : url($chat-bg-color) left bottom/cover no-repeat at-2x; 53 | $group-file-bg-image : ''; 54 | $group-file-bg-group-path : rgba(0,0,0,0.02); 55 | $group-file-bg-group-path-border: 1px solid rgba(130, 130, 130, 0.1); 56 | $group-file-search-border: 1px solid rgba(0, 0, 0, 0.1); 57 | $group-file-header-tab-background-color: transparent; 58 | 59 | 60 | $btn-bg-base : $primary; 61 | $border-color-base : darken($primary, 5%); 62 | $btn-primary-color : #fff; 63 | $btn-primary-bg : $primary; 64 | 65 | $btn-ghost-color : #FFFFFF; 66 | $btn-ghost-bg : $supplement; 67 | $btn-ghost-border : darken($supplement, 5%); 68 | 69 | $btn-disable-color : #ccc; 70 | $btn-disable-bg : #f3f5f7; 71 | $btn-disable-border : $border-color-base; 72 | 73 | $btn-danger-color : #FFFFFF; 74 | $btn-danger-bg : $danger; 75 | $btn-danger-border : $danger; 76 | 77 | 78 | 79 | //new css 80 | $yunpan-bg : #FFFFFF 81 | -------------------------------------------------------------------------------- /demo/scss/variable.scss: -------------------------------------------------------------------------------- 1 | //这里定义了一些基础的变量 2 | 3 | //通用基础颜色 4 | $color-darker : rgba(0,0,0,.87);/*#333333*/ 5 | $color-dark : rgba(0,0,0,.54);/*#666666*/ 6 | $color-base : rgba(0,0,0,.38);/*#999999*/ 7 | $color-medium : rgba(0,0,0,.24);/*#CCCCCC*/ 8 | $color-light : rgba(0,0,0,.1);/*#E5E5E5*/ 9 | $color-light-1 : rgba(0,0,0,.05); 10 | $color-lighter : #F8F8F8; 11 | $color-lightest : #FFFFFF; 12 | 13 | //通用辅助颜色 14 | $supplement : #FF9801; 15 | $danger : #FF5D4A; 16 | $success : #5ABB3C; 17 | $name-other : #596E8F; 18 | $name-own : #CC841B; 19 | $color-pink : #F37D9A;//女性 20 | $color-blue : #46BEEF;//男性 21 | 22 | 23 | //存放一些宽度 24 | 25 | $main-title-height : 70px;//顶部70px 26 | $nav-width :60px; 27 | $left-width :280px;//内容区域左边列表宽度 28 | 29 | 30 | //z-index.scss 31 | $min-zindex : 1; 32 | $medium-zindex : 10; 33 | $large-zindex : 100; 34 | $max-zindex : 1000; 35 | 36 | //普通元素的padding 37 | $ctn-padding-xmin : 1px; 38 | $ctn-padding-min : 2px; 39 | $ctn-padding-normal : 5px; 40 | $ctn-padding-large : 10px; 41 | $ctn-padding-xlarge : 15px; 42 | $ctn-padding-xxlarge : 20px; 43 | 44 | //profile 宽高度 45 | 46 | //thumb 头像大小(头像的尺寸:80,60,40,36,28.) 47 | $thumb-base : 28px; 48 | $thumb-large : 36px; 49 | $thumb-xlarge : 40px; 50 | $thumb-xxlarge : 60px; 51 | $thumb-xxxlarge : 80px; 52 | 53 | 54 | //原有的头像尺寸(需要废弃) 55 | //$thumb 56 | //$thumb-min : 12px; 57 | //$thumb-xxxxlarge : $thumb-min * 6; 58 | //$thumb-big : 80px; 59 | 60 | 61 | 62 | //这里存字体相关的 63 | $font-family : "Helvetica Neue",Helvetica,"Apple Color Emoji",'Segoe UI Emoji', 'Segoe UI Symbol',Arial,"PingFang SC","Heiti SC", "Hiragino Sans GB","Microsoft YaHei","微软雅黑",sans-serif; 64 | $code-family : Consolas,Menlo,Courier,monospace; 65 | $font-size-small : 12px; //针对h6,说明文字 66 | $font-size-lSmall : 13px; //针对h6,说明文字 67 | $font-size-base : 14px; //正文,链接,h5 68 | $font-size-large : 16px; //h4 69 | $font-size-xlarge : 18px; //h3 70 | $font-size-xxlarge : 20px; //h2 71 | $font-size-xxxlarge : 24px; //h1 72 | $line-height-base : 1.5; 73 | $line-height-computed : floor(($font-size-base * $line-height-base)); 74 | 75 | $border-radius-base : 2px; 76 | $border-radius-normal : 4px; 77 | $border-radius-large : 5px; 78 | $border-radius-xlarge : 10px; 79 | $border-radius-xxlarge : 15px; 80 | 81 | 82 | 83 | // ICONFONT 84 | $iconfont-css-prefix : dxicon; 85 | 86 | $icon-url : "//at.alicdn.com/t/font_ovsogmhgit4ndn29"; //经常会改变,以后会换成本地的地址 87 | 88 | 89 | 90 | // Animation 91 | $ease-out : cubic-bezier(0.215, 0.61, 0.355, 1); 92 | $ease-in : cubic-bezier(0.55, 0.055, 0.675, 0.19); 93 | $ease-in-out : cubic-bezier(0.645, 0.045, 0.355, 1); 94 | $ease-out-back : cubic-bezier(0.12, 0.4, 0.29, 1.46); 95 | $ease-in-back : cubic-bezier(0.71, -0.46, 0.88, 0.6); 96 | $ease-in-out-back : cubic-bezier(0.71, -0.46, 0.29, 1.46); 97 | $ease-out-circ : cubic-bezier(0.08, 0.82, 0.17, 1); 98 | $ease-in-circ : cubic-bezier(0.6, 0.04, 0.98, 0.34); 99 | $ease-in-out-circ : cubic-bezier(0.78, 0.14, 0.15, 0.86); 100 | $ease-out-quint : cubic-bezier(0.23, 1, 0.32, 1); 101 | $ease-in-quint : cubic-bezier(0.755, 0.05, 0.855, 0.06); 102 | $ease-in-out-quint : cubic-bezier(0.86, 0, 0.07, 1); 103 | 104 | 105 | // 按钮边框颜色,理论上应该是背景色加深一点就可以了 106 | 107 | //$border-color-base : #d9d9d9; // base border outline a component 108 | //$box-shadow-base : 0 0 4px rgba(0, 0, 0, 0.17); 109 | //$border-color-split : #e9e9e9; // split border inside a component 110 | $cursor-disabled : not-allowed; 111 | $btn-font-weight : normal; 112 | 113 | 114 | 115 | 116 | 117 | $btn-default-color : $color-darker; 118 | $btn-default-bg : $color-lighter; 119 | $btn-default-border : $color-light; 120 | 121 | 122 | 123 | 124 | 125 | 126 | $btn-padding-base : 8px 31px; 127 | $btn-border-radius-base : 4px; 128 | 129 | $btn-font-size-lg : 14px; 130 | $btn-padding-lg : 4px 11px 5px 11px; 131 | $btn-border-radius-lg : $btn-border-radius-base; 132 | 133 | $btn-padding-sm : 1px 7px; 134 | $btn-border-radius-sm : $btn-border-radius-base; 135 | 136 | $btn-circle-size : 28px; 137 | $btn-circle-size-lg : 32px; 138 | $btn-circle-size-sm : 22px; 139 | 140 | $msg-title-height: 70px; 141 | $msg-border-color: $color-light; 142 | 143 | 144 | 145 | 146 | 147 | //气泡页部分宽度定义 148 | $msg-medium-min-width: 630px; 149 | $msg-medium-max-width: 1000px; 150 | $msg-medium-width : 860px; 151 | $msg-medium-left : 0px; 152 | $msg-medium-right : 0px; 153 | $msg-medium-top: 0px; 154 | $msg-medium-bottom: 0px; 155 | $slidepanel-width: 470px; 156 | 157 | -------------------------------------------------------------------------------- /html/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 | 404 10 | 11 | -------------------------------------------------------------------------------- /html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | kityphoto 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by yeanzhi on 17/2/7. 3 | */ 4 | 'use strict'; 5 | import FabricPhoto from './src/index.js'; 6 | export default FabricPhoto; 7 | -------------------------------------------------------------------------------- /jsdoc.conf.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ximing/fabric-photo/5e59eff9a37166f7ce14eed250c00329b444a091/jsdoc.conf.json -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fabric-photo", 3 | "version": "0.2.0", 4 | "description": "web 图片编辑器", 5 | "repository": "", 6 | "author": "ximing", 7 | "module": "dist/index.esm.js", 8 | "main": "dist/index.js", 9 | "scripts": { 10 | "i": "PUPPETEER_DOWNLOAD_HOST=https://npm.taobao.org/mirrors SASS_BINARY_SITE=https://npm.taobao.org/mirrors/node-sass npm install", 11 | "dev": "./node_modules/.bin/webpack-dev-server ", 12 | "install:website": "cd website && npm i --registry=https://registry.npmjs.com", 13 | "build:website": "cd website && rm -rf .umi dist && npm run docs:build", 14 | "build": "rollup -c" 15 | }, 16 | "publishConfig": { 17 | "access": "public", 18 | "registry": "https://registry.npmjs.org/" 19 | }, 20 | "dependencies": { 21 | "classnames": "^2.2.5", 22 | "fabric": "1.7.3" 23 | }, 24 | "devDependencies": { 25 | "@babel/cli": "^7.8.3", 26 | "@babel/core": "^7.8.3", 27 | "@babel/generator": "^7.8.3", 28 | "@babel/parser": "^7.8.3", 29 | "@babel/plugin-external-helpers": "^7.8.3", 30 | "@babel/plugin-proposal-class-properties": "^7.8.3", 31 | "@babel/plugin-proposal-decorators": "^7.8.3", 32 | "@babel/plugin-proposal-do-expressions": "^7.8.3", 33 | "@babel/plugin-proposal-export-default-from": "^7.8.3", 34 | "@babel/plugin-proposal-export-namespace-from": "^7.8.3", 35 | "@babel/plugin-proposal-function-bind": "^7.8.3", 36 | "@babel/plugin-proposal-function-sent": "^7.8.3", 37 | "@babel/plugin-proposal-json-strings": "^7.8.3", 38 | "@babel/plugin-proposal-logical-assignment-operators": "^7.8.3", 39 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.4.4", 40 | "@babel/plugin-proposal-numeric-separator": "^7.8.3", 41 | "@babel/plugin-proposal-object-rest-spread": "^7.8.3", 42 | "@babel/plugin-proposal-optional-chaining": "^7.8.3", 43 | "@babel/plugin-proposal-pipeline-operator": "^7.3.2", 44 | "@babel/plugin-proposal-throw-expressions": "^7.8.3", 45 | "@babel/plugin-syntax-decorators": "^7.8.3", 46 | "@babel/plugin-syntax-do-expressions": "^7.8.3", 47 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 48 | "@babel/plugin-syntax-import-meta": "^7.8.3", 49 | "@babel/plugin-syntax-object-rest-spread": "^7.8.3", 50 | "@babel/plugin-syntax-optional-chaining": "^7.8.3", 51 | "@babel/plugin-transform-react-jsx": "^7.8.3", 52 | "@babel/plugin-transform-runtime": "^7.8.3", 53 | "@babel/preset-env": "^7.8.3", 54 | "@babel/preset-react": "^7.8.3", 55 | "@commitlint/cli": "^8.3.5", 56 | "@commitlint/config-conventional": "^8.3.4", 57 | "@rollup/plugin-replace": "^2.2.0", 58 | "autoprefixer": "^9.7.4", 59 | "babel-eslint": "^10.0.1", 60 | "babel-loader": "^8.0.0-beta.0", 61 | "babel-plugin-import": "^1.8.0", 62 | "clean-webpack-plugin": "^3.0.0", 63 | "css-loader": "^3.4.2", 64 | "eslint": "^6.5.1", 65 | "eslint-config-airbnb": "^18.0.1", 66 | "eslint-config-prettier": "^6.4.0", 67 | "eslint-config-standard": "^14.1.0", 68 | "eslint-config-standard-jsx": "^8.1.0", 69 | "eslint-html-reporter": "^0.7.4", 70 | "eslint-loader": "^3.0.3", 71 | "eslint-plugin-flowtype": "^4.6.0", 72 | "eslint-plugin-import": "^2.18.2", 73 | "eslint-plugin-jest": "^23.6.0", 74 | "eslint-plugin-jsx-a11y": "^6.1.1", 75 | "eslint-plugin-node": "^11.0.0", 76 | "eslint-plugin-prettier": "^3.1.1", 77 | "eslint-plugin-promise": "^4.2.1", 78 | "eslint-plugin-react": "^7.18.0", 79 | "eslint-plugin-standard": "^4.0.1", 80 | "file-loader": "^5.0.2", 81 | "fs-extra": "^8.1.0", 82 | "html-loader": "^0.5.5", 83 | "html-webpack-plugin": "^3.0.6", 84 | "husky": "^4.2.2", 85 | "lerna": "^3.20.2", 86 | "less": "^3.11.1", 87 | "less-loader": "^5.0.0", 88 | "lint-staged": "^10.0.7", 89 | "memory-fs": "^0.5.0", 90 | "mini-css-extract-plugin": "^0.9.0", 91 | "mocha": "^7.0.0", 92 | "node-sass": "^4.13.1", 93 | "postcss": "^7.0.2", 94 | "postcss-clearfix": "^2.0.1", 95 | "postcss-flexbugs-fixes": "^4.2.0", 96 | "postcss-loader": "^3.0.0", 97 | "postcss-position": "^1.1.0", 98 | "postcss-preset-env": "^6.7.0", 99 | "postcss-size": "^3.0.0", 100 | "prettier": "^1.18.2", 101 | "react": "^16.10.2", 102 | "react-dom": "^16.10.2", 103 | "rollup": "^1.29.1", 104 | "rollup-plugin-alias": "^2.0.1", 105 | "rollup-plugin-babel": "^4.3.3", 106 | "rollup-plugin-commonjs": "^10.1.0", 107 | "rollup-plugin-json": "^4.0.0", 108 | "rollup-plugin-node-resolve": "^5.2.0", 109 | "rollup-plugin-url": "^3.0.1", 110 | "sass-loader": "^8.0.2", 111 | "style-loader": "^1.1.3", 112 | "stylelint": "^13.1.0", 113 | "url-loader": "^3.0.0", 114 | "webpack": "^4.41.0", 115 | "webpack-cli": "^3.3.9", 116 | "webpack-dev-server": "^3.8.2", 117 | "webpack-merge": "^4.1.4", 118 | "webpack-stream": "^5.1.1" 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by yeanzhi on 17/1/13. 3 | */ 4 | module.exports = { 5 | plugins: [ 6 | require('postcss-flexbugs-fixes'), 7 | require('postcss-preset-env')({ 8 | autoprefixer: { 9 | flexbox: 'no-2009' 10 | }, 11 | stage: 3 12 | }), 13 | require('postcss-clearfix')(), 14 | require('postcss-position')(), 15 | require('postcss-size')() 16 | ] 17 | }; 18 | -------------------------------------------------------------------------------- /public/demo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ximing/fabric-photo/5e59eff9a37166f7ce14eed250c00329b444a091/public/demo.jpeg -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # fabric photo 2 | 3 | 基于 canvas 的纯前端的图片编辑器,支持方形,圆形,箭头,缩放,拖拽,鹰眼,马赛克,涂鸦,线条,导出 png,剪切等 4 | ![image.png](https://s3.meituan.net/v1/mss_814dc1610cda4b2e8febd6ea2c809db5/apps-open/27dab3fc-22d4-465b-85f6-7cfa8e3f7c50_1500805977973?filename=image.png) 5 | 6 | ## online Demo 7 | 8 | 访问 [https://ximing.github.io/fabric-photo/](https://ximing.github.io/fabric-photo/) 体验 9 | 10 | ## 启动 demo 11 | 12 | ```bash 13 | # 安装依赖 14 | npm run i 15 | # 运行项目 16 | npm run dev 17 | ``` 18 | 19 | [结合 react 使用方法](https://github.com/ximing/fabric-photo/blob/master/demo/main.js) 20 | 21 | ## License 22 | 23 | [MIT](https://github.com/ximing/fabric-photo/blob/master/LICENSE) 24 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | const babel = require('rollup-plugin-babel'); 3 | const alias = require('rollup-plugin-alias'); 4 | 5 | const cwd = __dirname; 6 | 7 | const baseConfig = { 8 | input: join(cwd, 'src/index.js'), 9 | external: ['react', 'react-dom', 'jquery'], 10 | output: [ 11 | { 12 | file: join(cwd, 'dist/index.js'), 13 | format: 'cjs', 14 | sourcemap: true, 15 | exports: 'named' 16 | } 17 | ], 18 | plugins: [ 19 | alias({ 20 | entries: [ 21 | // { 22 | // find: 'fabric', 23 | // replacement: join(cwd, 'node_modules/fabric/dist/fabric.js') 24 | // } 25 | ] 26 | }), 27 | babel() 28 | ] 29 | }; 30 | const esmConfig = { 31 | ...baseConfig, 32 | output: { 33 | ...baseConfig.output, 34 | sourcemap: true, 35 | format: 'es', 36 | file: join(cwd, 'dist/index.esm.js') 37 | } 38 | }; 39 | 40 | function rollup() { 41 | const target = process.env.TARGET; 42 | if (target === 'umd') { 43 | return baseConfig; 44 | } 45 | if (target === 'esm') { 46 | return esmConfig; 47 | } 48 | return [baseConfig, esmConfig]; 49 | } 50 | module.exports = rollup(); 51 | -------------------------------------------------------------------------------- /src/command.js: -------------------------------------------------------------------------------- 1 | import consts from './consts'; 2 | 3 | import addObject from './commands/add-object'; 4 | import remove from './commands/remove'; 5 | import clear from './commands/clear'; 6 | import loadImage from './commands/load-image.js'; 7 | import zoom from './commands/zoom.js'; 8 | import rotationImage from './commands/rotation-image.js'; 9 | 10 | const { commandNames } = consts; 11 | const creators = {}; 12 | 13 | creators[commandNames.CLEAR_OBJECTS] = clear; 14 | creators[commandNames.ADD_OBJECT] = addObject; 15 | creators[commandNames.REMOVE_OBJECT] = remove; 16 | creators[commandNames.LOAD_IMAGE] = loadImage; 17 | creators[commandNames.ZOOM] = zoom; 18 | creators[commandNames.ROTATE_IMAGE] = rotationImage; 19 | 20 | function create(name, ...args) { 21 | return creators[name].apply(null, args); 22 | } 23 | 24 | export default { 25 | create 26 | }; 27 | -------------------------------------------------------------------------------- /src/commands/add-object.js: -------------------------------------------------------------------------------- 1 | import util from '../lib/util'; 2 | import Command from './base'; 3 | import consts from '../consts'; 4 | 5 | const { moduleNames } = consts; 6 | const { MAIN } = moduleNames; 7 | export default function(object) { 8 | util.stamp(object); 9 | 10 | return new Command({ 11 | /** 12 | * @param {object.} moduleMap - Modules injection 13 | * @returns {Promise} 14 | * @ignore 15 | */ 16 | execute(moduleMap) { 17 | return new Promise((resolve, reject) => { 18 | const canvas = moduleMap[MAIN].getCanvas(); 19 | 20 | if (!canvas.contains(object)) { 21 | canvas.add(object); 22 | resolve(object); 23 | } else { 24 | reject(); 25 | } 26 | }); 27 | }, 28 | /** 29 | * @param {object.} moduleMap - Modules injection 30 | * @returns {Promise} 31 | * @ignore 32 | */ 33 | undo(moduleMap) { 34 | return new Promise((resolve, reject) => { 35 | const canvas = moduleMap[MAIN].getCanvas(); 36 | 37 | if (canvas.contains(object)) { 38 | canvas.remove(object); 39 | resolve(object); 40 | } else { 41 | reject(); 42 | } 43 | }); 44 | } 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /src/commands/base.js: -------------------------------------------------------------------------------- 1 | export default class { 2 | constructor(actions) { 3 | this.execute = actions.execute; 4 | 5 | this.undo = actions.undo; 6 | 7 | this.executeCallback = null; 8 | 9 | this.undoCallback = null; 10 | } 11 | 12 | execute() { 13 | throw new Error('没有实现execute方法'); 14 | } 15 | 16 | undo() { 17 | throw new Error('没有实现undo方法'); 18 | } 19 | 20 | setExecuteCallback(callback) { 21 | this.executeCallback = callback; 22 | return this; 23 | } 24 | 25 | setUndoCallback(callback) { 26 | this.undoCallback = callback; 27 | return this; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/commands/clear.js: -------------------------------------------------------------------------------- 1 | import Command from './base'; 2 | import consts from '../consts'; 3 | 4 | const { moduleNames } = consts; 5 | const { MAIN } = moduleNames; 6 | export default function() { 7 | return new Command({ 8 | /** 9 | * @param {object.} moduleMap - Components injection 10 | * @returns {Promise} 11 | * @ignore 12 | */ 13 | execute(moduleMap) { 14 | return new Promise((resolve, reject) => { 15 | const canvas = moduleMap[MAIN].getCanvas(); 16 | const objs = canvas.getObjects(); 17 | 18 | // Slice: "canvas.clear()" clears the objects array, So shallow copy the array 19 | this.store = objs.slice(); 20 | objs.slice().forEach((obj) => { 21 | if (obj.get('type') === 'group') { 22 | canvas.remove(obj); 23 | } else { 24 | obj.remove(); 25 | } 26 | }); 27 | resolve(); 28 | }); 29 | }, 30 | /** 31 | * @param {object.} moduleMap - Components injection 32 | * @returns {Promise} 33 | * @ignore 34 | */ 35 | undo(moduleMap) { 36 | const canvas = moduleMap[MAIN].getCanvas(); 37 | const canvasContext = canvas; 38 | 39 | canvas.add.apply(canvasContext, this.store); 40 | 41 | return Promise.resolve(); 42 | } 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /src/commands/load-image.js: -------------------------------------------------------------------------------- 1 | import Command from './base'; 2 | import consts from '../consts'; 3 | 4 | const { moduleNames } = consts; 5 | const { IMAGE_LOADER } = moduleNames; 6 | export default function(imageName, img) { 7 | return new Command({ 8 | execute(moduleMap) { 9 | const loader = moduleMap[IMAGE_LOADER]; 10 | const canvas = loader.getCanvas(); 11 | 12 | this.store = { 13 | prevName: loader.getImageName(), 14 | prevImage: loader.getCanvasImage(), 15 | //"canvas.clear()" 会清除数据,所以用 slice进行一下 深拷贝 16 | objects: canvas.getObjects().slice() 17 | }; 18 | 19 | canvas.clear(); 20 | 21 | return loader.load(imageName, img); 22 | }, 23 | undo(moduleMap) { 24 | const loader = moduleMap[IMAGE_LOADER]; 25 | const canvas = loader.getCanvas(); 26 | const canvasContext = canvas; 27 | 28 | canvas.clear(); 29 | canvas.add.apply(canvasContext, this.store.objects); 30 | 31 | return loader.load(this.store.prevName, this.store.prevImage); 32 | } 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /src/commands/remove.js: -------------------------------------------------------------------------------- 1 | import Command from './base'; 2 | import consts from '../consts'; 3 | 4 | const { moduleNames } = consts; 5 | const { MAIN } = moduleNames; 6 | export default function(target) { 7 | return new Command({ 8 | /** 9 | * @param {object.} moduleMap - Modules injection 10 | * @returns {Promise} 11 | * @ignore 12 | */ 13 | execute(moduleMap) { 14 | return new Promise((resolve, reject) => { 15 | const canvas = moduleMap[MAIN].getCanvas(); 16 | const isValidGroup = target && target.isType('group') && !target.isEmpty(); 17 | 18 | if (isValidGroup) { 19 | canvas.discardActiveGroup(); // restore states for each objects 20 | this.store = target.getObjects(); 21 | target.forEachObject((obj) => { 22 | obj.remove(); 23 | }); 24 | resolve(); 25 | } else if (canvas.contains(target)) { 26 | this.store = [target]; 27 | target.remove(); 28 | resolve(); 29 | } else { 30 | reject(); 31 | } 32 | }); 33 | }, 34 | /** 35 | * @param {object.} moduleMap - Modules injection 36 | * @returns {Promise} 37 | * @ignore 38 | */ 39 | undo(moduleMap) { 40 | const canvas = moduleMap[MAIN].getCanvas(); 41 | const canvasContext = canvas; 42 | 43 | canvas.add.apply(canvasContext, this.store); 44 | 45 | return Promise.resolve(); 46 | } 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /src/commands/rotation-image.js: -------------------------------------------------------------------------------- 1 | import Command from './base'; 2 | import consts from '../consts'; 3 | 4 | const { moduleNames } = consts; 5 | export default function(type, angle) { 6 | return new Command({ 7 | execute(moduleMap) { 8 | const rotationComp = moduleMap[moduleNames.ROTATION]; 9 | this.store = rotationComp.getCurrentAngle(); 10 | return rotationComp[type](angle); 11 | }, 12 | undo(moduleMap) { 13 | const rotationComp = moduleMap[moduleNames.ROTATION]; 14 | return rotationComp.setAngle(this.store); 15 | } 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /src/commands/zoom.js: -------------------------------------------------------------------------------- 1 | import Command from './base'; 2 | import consts from '../consts'; 3 | 4 | const { moduleNames } = consts; 5 | const { MAIN } = moduleNames; 6 | export default function(zoom) { 7 | return new Command({ 8 | /** 9 | * @param {object.} moduleMap - Modules injection 10 | * @returns {Promise} 11 | * @ignore 12 | */ 13 | execute(moduleMap) { 14 | const mainModule = moduleMap[MAIN]; 15 | // const canvas = mainModule.getCanvas(); 16 | // this.zoom = (canvas.viewportTransform[0] || 1); 17 | // let zoom = rate * (canvas.viewportTransform[0] || 1); 18 | //直接这么设置是不行的,因为 这个本质上是在设置 transform 会导致坐标系乱套 19 | // this.zoom = canvas.getZoom(); 20 | // canvas.setZoom(zoom); 21 | //使用新的方法通过放大canvas本身的方式进行设置 22 | this.zoom = mainModule.getZoom(); //mainModule.getZoom(); 23 | mainModule.setZoom(zoom); 24 | return Promise.resolve(zoom); 25 | }, 26 | /** 27 | * @param {object.} moduleMap - Modules injection 28 | * @returns {Promise} 29 | * @ignore 30 | */ 31 | undo(moduleMap) { 32 | // const canvas = moduleMap[MAIN].getCanvas(); 33 | // const canvasContext = canvas; 34 | // canvas.setZoom.call(canvasContext, this.zoom); 35 | const mainModule = moduleMap[MAIN]; 36 | mainModule.setZoom(this.zoom); 37 | return Promise.resolve(this.zoom); 38 | } 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /src/consts.js: -------------------------------------------------------------------------------- 1 | import util from './lib/util'; 2 | 3 | export default { 4 | /** 5 | * Component names 6 | * @type {Object.} 7 | */ 8 | moduleNames: util.keyMirror( 9 | 'MAIN', 10 | 'IMAGE_LOADER', 11 | 'CROPPER', 12 | 'FLIP', 13 | 'ROTATION', 14 | 'FREE_DRAWING', 15 | 'LINE', 16 | 'ARROW', 17 | 'TEXT', 18 | 'ICON', 19 | 'FILTER', 20 | 'SHAPE', 21 | 'MOSAIC', 22 | 'PAN' 23 | ), 24 | 25 | /** 26 | * Command names 27 | * @type {Object.} 28 | */ 29 | commandNames: util.keyMirror( 30 | 'CLEAR', 31 | 'LOAD_IMAGE', 32 | 'FLIP_IMAGE', 33 | 'ROTATE_IMAGE', 34 | 'ADD_OBJECT', 35 | 'REMOVE_OBJECT', 36 | 'APPLY_FILTER', 37 | 'ZOOM' 38 | ), 39 | 40 | /** 41 | * Event names 42 | * @type {Object.} 43 | */ 44 | eventNames: { 45 | LOAD_IMAGE: 'loadImage', 46 | CLEAR_OBJECTS: 'clearObjects', 47 | CLEAR_IMAGE: 'clearImage', 48 | START_CROPPING: 'startCropping', 49 | END_CROPPING: 'endCropping', 50 | FLIP_IMAGE: 'flipImage', 51 | ROTATE_IMAGE: 'rotateImage', 52 | ADD_OBJECT: 'addObject', 53 | SELECT_OBJECT: 'selectObject', 54 | REMOVE_OBJECT: 'removeObject', 55 | ADJUST_OBJECT: 'adjustObject', 56 | START_FREE_DRAWING: 'startFreeDrawing', 57 | END_FREE_DRAWING: 'endFreeDrawing', 58 | START_LINE_DRAWING: 'startLineDrawing', 59 | END_LINE_DRAWING: 'endLineDrawing', 60 | START_PAN: 'startPan', 61 | END_PAN: 'endPan', 62 | START_ARROW_DRAWING: 'startArrowDrawing', 63 | END_ARROW_DRAWING: 'endArrowDrawing', 64 | START_MOSAIC_DRAWING: 'startMosaicDrawing', 65 | END_MOSAIC_DRAWING: 'endMosaicDrawing', 66 | EMPTY_REDO_STACK: 'emptyRedoStack', 67 | EMPTY_UNDO_STACK: 'emptyUndoStack', 68 | PUSH_UNDO_STACK: 'pushUndoStack', 69 | PUSH_REDO_STACK: 'pushRedoStack', 70 | ACTIVATE_TEXT: 'activateText', 71 | APPLY_FILTER: 'applyFilter', 72 | EDIT_TEXT: 'editText', 73 | MOUSE_DOWN: 'mousedown', 74 | CHANGE_ZOOM: 'changeZoom' 75 | }, 76 | 77 | /** 78 | * Editor states 79 | * @type {Object.} 80 | */ 81 | states: util.keyMirror( 82 | 'NORMAL', 83 | 'CROP', 84 | 'FREE_DRAWING', 85 | 'LINE', 86 | 'ARROW', 87 | 'MOSAIC', 88 | 'TEXT', 89 | 'SHAPE', 90 | 'PAN' 91 | ), 92 | 93 | /** 94 | * Shortcut key values 95 | * @type {Object.} 96 | */ 97 | keyCodes: { 98 | Z: 90, 99 | Y: 89, 100 | SHIFT: 16, 101 | BACKSPACE: 8, 102 | DEL: 46 103 | }, 104 | 105 | /** 106 | * Fabric object options 107 | * @type {Object.} 108 | */ 109 | fObjectOptions: { 110 | SELECTION_STYLE: { 111 | borderColor: '#118BFB', 112 | cornerColor: '#FFFFFF', 113 | cornerStrokeColor: '#118BFB', 114 | cornerSize: 12, 115 | padding: 1, 116 | originX: 'center', 117 | originY: 'center', 118 | transparentCorners: false, 119 | cornerStyle: 'circle' 120 | } 121 | }, 122 | 123 | rejectMessages: { 124 | flip: 'The flipX and flipY setting values are not changed.', 125 | rotation: 'The current angle is same the old angle.', 126 | loadImage: 'The background image is empty.', 127 | isLock: 'The executing command state is locked.', 128 | undo: 'The promise of undo command is reject.', 129 | redo: 'The promise of redo command is reject.' 130 | }, 131 | 132 | MOUSE_MOVE_THRESHOLD: 10 133 | }; 134 | -------------------------------------------------------------------------------- /src/lib/canvas-to-blob.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by yeanzhi on 16/12/12. 3 | */ 4 | let CanvasPrototype = window.HTMLCanvasElement && window.HTMLCanvasElement.prototype; 5 | let hasBlobConstructor = 6 | window.Blob && 7 | (function() { 8 | try { 9 | return Boolean(new Blob()); 10 | } catch (e) { 11 | return false; 12 | } 13 | })(); 14 | let hasArrayBufferViewSupport = 15 | hasBlobConstructor && 16 | window.Uint8Array && 17 | (function() { 18 | try { 19 | return new Blob([new Uint8Array(100)]).size === 100; 20 | } catch (e) { 21 | return false; 22 | } 23 | })(); 24 | let BlobBuilder = 25 | window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder; 26 | let dataURIPattern = /^data:((.*?)(;charset=.*?)?)(;base64)?,/; 27 | // eslint-disable-next-line import/no-mutable-exports 28 | let dataURLtoBlob = 29 | (hasBlobConstructor || BlobBuilder) && 30 | window.atob && 31 | window.ArrayBuffer && 32 | window.Uint8Array && 33 | function(dataURI) { 34 | let matches, mediaType, isBase64, dataString, byteString, arrayBuffer, intArray, i, bb; 35 | // Parse the dataURI components as per RFC 2397 36 | matches = dataURI.match(dataURIPattern); 37 | if (!matches) { 38 | throw new Error('invalid data URI'); 39 | } 40 | // Default to text/plain;charset=US-ASCII 41 | mediaType = matches[2] ? matches[1] : `text/plain${matches[3] || ';charset=US-ASCII'}`; 42 | isBase64 = !!matches[4]; 43 | dataString = dataURI.slice(matches[0].length); 44 | if (isBase64) { 45 | // Convert base64 to raw binary data held in a string: 46 | byteString = atob(dataString); 47 | } else { 48 | // Convert base64/URLEncoded data component to raw binary: 49 | byteString = decodeURIComponent(dataString); 50 | } 51 | // Write the bytes of the string to an ArrayBuffer: 52 | arrayBuffer = new ArrayBuffer(byteString.length); 53 | intArray = new Uint8Array(arrayBuffer); 54 | for (i = 0; i < byteString.length; i += 1) { 55 | intArray[i] = byteString.charCodeAt(i); 56 | } 57 | // Write the ArrayBuffer (or ArrayBufferView) to a blob: 58 | if (hasBlobConstructor) { 59 | return new Blob([hasArrayBufferViewSupport ? intArray : arrayBuffer], { 60 | type: mediaType 61 | }); 62 | } 63 | bb = new BlobBuilder(); 64 | bb.append(arrayBuffer); 65 | return bb.getBlob(mediaType); 66 | }; 67 | if (window.HTMLCanvasElement && !CanvasPrototype.toBlob) { 68 | if (CanvasPrototype.mozGetAsFile) { 69 | CanvasPrototype.toBlob = function(callback, type, quality) { 70 | if (quality && CanvasPrototype.toDataURL && dataURLtoBlob) { 71 | callback(dataURLtoBlob(this.toDataURL(type, quality))); 72 | } else { 73 | callback(this.mozGetAsFile('blob', type)); 74 | } 75 | }; 76 | } else if (CanvasPrototype.toDataURL && dataURLtoBlob) { 77 | CanvasPrototype.toBlob = function(callback, type, quality) { 78 | callback(dataURLtoBlob(this.toDataURL(type, quality))); 79 | }; 80 | } 81 | } 82 | export default dataURLtoBlob; 83 | window.dataURLtoBlob = dataURLtoBlob; 84 | -------------------------------------------------------------------------------- /src/lib/event.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by yeanzhi on 16/12/4. 3 | */ 4 | const events = require('events'); 5 | // 创建 eventEmitter 对象 6 | export default new events.EventEmitter(); 7 | -------------------------------------------------------------------------------- /src/lib/shape-resize-helper.js: -------------------------------------------------------------------------------- 1 | import { fabric } from 'fabric'; 2 | 3 | const DIVISOR = { 4 | rect: 1, 5 | circle: 2, 6 | triangle: 1 7 | }; 8 | const DIMENSION_KEYS = { 9 | rect: { 10 | w: 'width', 11 | h: 'height' 12 | }, 13 | circle: { 14 | w: 'rx', 15 | h: 'ry' 16 | }, 17 | triangle: { 18 | w: 'width', 19 | h: 'height' 20 | } 21 | }; 22 | 23 | /** 24 | * Set the start point value to the shape object 25 | * @param {fabric.Object} shape - Shape object 26 | * @ignore 27 | */ 28 | function setStartPoint(shape) { 29 | const originX = shape.getOriginX(); 30 | const originY = shape.getOriginY(); 31 | const originKey = originX.substring(0, 1) + originY.substring(0, 1); 32 | 33 | shape.startPoint = shape.origins[originKey]; 34 | } 35 | 36 | /** 37 | * Get the positions of ratated origin by the pointer value 38 | * @param {{x: number, y: number}} origin - Origin value 39 | * @param {{x: number, y: number}} pointer - Pointer value 40 | * @param {number} angle - Rotating angle 41 | * @returns {object} Postions of origin 42 | * @ignore 43 | */ 44 | function getPositionsOfRotatedOrigin(origin, pointer, angle) { 45 | const sx = origin.x; 46 | const sy = origin.y; 47 | const px = pointer.x; 48 | const py = pointer.y; 49 | const r = (angle * Math.PI) / 180; 50 | const rx = (px - sx) * Math.cos(r) - (py - sy) * Math.sin(r) + sx; 51 | const ry = (px - sx) * Math.sin(r) + (py - sy) * Math.cos(r) + sy; 52 | 53 | return { 54 | originX: sx > rx ? 'right' : 'left', 55 | originY: sy > ry ? 'bottom' : 'top' 56 | }; 57 | } 58 | 59 | /** 60 | * Whether the shape has the center origin or not 61 | * @param {fabric.Object} shape - Shape object 62 | * @returns {boolean} State 63 | * @ignore 64 | */ 65 | function hasCenterOrigin(shape) { 66 | return shape.getOriginX() === 'center' && shape.getOriginY() === 'center'; 67 | } 68 | 69 | /** 70 | * Adjust the origin of shape by the start point 71 | * @param {{x: number, y: number}} pointer - Pointer value 72 | * @param {fabric.Object} shape - Shape object 73 | * @ignore 74 | */ 75 | function adjustOriginByStartPoint(pointer, shape) { 76 | const centerPoint = shape.getPointByOrigin('center', 'center'); 77 | const angle = -shape.getAngle(); 78 | const originPositions = getPositionsOfRotatedOrigin(centerPoint, pointer, angle); 79 | const originX = originPositions.originX; 80 | const originY = originPositions.originY; 81 | const origin = shape.getPointByOrigin(originX, originY); 82 | const left = shape.getLeft() - (centerPoint.x - origin.x); 83 | const top = shape.getTop() - (centerPoint.x - origin.y); 84 | 85 | shape.set({ 86 | originX, 87 | originY, 88 | left, 89 | top 90 | }); 91 | 92 | shape.setCoords(); 93 | } 94 | 95 | /** 96 | * Adjust the origin of shape by the moving pointer value 97 | * @param {{x: number, y: number}} pointer - Pointer value 98 | * @param {fabric.Object} shape - Shape object 99 | * @ignore 100 | */ 101 | function adjustOriginByMovingPointer(pointer, shape) { 102 | const origin = shape.startPoint; 103 | const angle = -shape.getAngle(); 104 | const originPositions = getPositionsOfRotatedOrigin(origin, pointer, angle); 105 | const originX = originPositions.originX; 106 | const originY = originPositions.originY; 107 | 108 | shape.setPositionByOrigin(origin, originX, originY); 109 | } 110 | 111 | /** 112 | * Adjust the dimension of shape on firing scaling event 113 | * @param {fabric.Object} shape - Shape object 114 | * @ignore 115 | */ 116 | function adjustDimensionOnScaling(shape) { 117 | const type = shape.type; 118 | const dimensionKeys = DIMENSION_KEYS[type]; 119 | const scaleX = shape.scaleX; 120 | const scaleY = shape.scaleY; 121 | let width = shape[dimensionKeys.w] * scaleX; 122 | let height = shape[dimensionKeys.h] * scaleY; 123 | 124 | if (shape.isRegular) { 125 | const maxScale = Math.max(scaleX, scaleY); 126 | 127 | width = shape[dimensionKeys.w] * maxScale; 128 | height = shape[dimensionKeys.h] * maxScale; 129 | } 130 | 131 | const options = { 132 | hasControls: false, 133 | hasBorders: false, 134 | scaleX: 1, 135 | scaleY: 1 136 | }; 137 | 138 | options[dimensionKeys.w] = width; 139 | options[dimensionKeys.h] = height; 140 | 141 | shape.set(options); 142 | } 143 | 144 | /** 145 | * Adjust the dimension of shape on firing mouse move event 146 | * @param {{x: number, y: number}} pointer - Pointer value 147 | * @param {fabric.Object} shape - Shape object 148 | * @ignore 149 | */ 150 | function adjustDimensionOnMouseMove(pointer, shape) { 151 | const origin = shape.startPoint; 152 | const type = shape.type; 153 | const divisor = DIVISOR[type]; 154 | const dimensionKeys = DIMENSION_KEYS[type]; 155 | const strokeWidth = shape.strokeWidth; 156 | const isTriangle = !!(shape.type === 'triangle'); 157 | const options = {}; 158 | let width = Math.abs(origin.x - pointer.x) / divisor; 159 | let height = Math.abs(origin.y - pointer.y) / divisor; 160 | 161 | if (width > strokeWidth) { 162 | width -= strokeWidth / divisor; 163 | } 164 | 165 | if (height > strokeWidth) { 166 | height -= strokeWidth / divisor; 167 | } 168 | 169 | if (shape.isRegular) { 170 | width = height = Math.max(width, height); 171 | 172 | if (isTriangle) { 173 | height = (Math.sqrt(3) / 2) * width; 174 | } 175 | } 176 | 177 | options[dimensionKeys.w] = width; 178 | options[dimensionKeys.h] = height; 179 | 180 | shape.set(options); 181 | } 182 | 183 | export default { 184 | /** 185 | * Set each origin value to shape 186 | * @param {fabric.Object} shape - Shape object 187 | */ 188 | setOrigins(shape) { 189 | const leftTopPoint = shape.getPointByOrigin('left', 'top'); 190 | const rightTopPoint = shape.getPointByOrigin('right', 'top'); 191 | const rightBottomPoint = shape.getPointByOrigin('right', 'bottom'); 192 | const leftBottomPoint = shape.getPointByOrigin('left', 'bottom'); 193 | 194 | shape.origins = { 195 | lt: leftTopPoint, 196 | rt: rightTopPoint, 197 | rb: rightBottomPoint, 198 | lb: leftBottomPoint 199 | }; 200 | }, 201 | 202 | /** 203 | * Resize the shape 204 | * @param {fabric.Object} shape - Shape object 205 | * @param {{x: number, y: number}} pointer - Mouse pointer values on canvas 206 | * @param {boolean} isScaling - Whether the resizing action is scaling or not 207 | */ 208 | resize(shape, pointer, isScaling) { 209 | if (hasCenterOrigin(shape)) { 210 | adjustOriginByStartPoint(pointer, shape); 211 | setStartPoint(shape); 212 | } 213 | 214 | if (isScaling) { 215 | adjustDimensionOnScaling(shape, pointer); 216 | } else { 217 | adjustDimensionOnMouseMove(pointer, shape); 218 | } 219 | 220 | adjustOriginByMovingPointer(pointer, shape); 221 | }, 222 | 223 | /** 224 | * Adjust the origin position of shape to center 225 | * @param {fabric.Object} shape - Shape object 226 | */ 227 | adjustOriginToCenter(shape) { 228 | const centerPoint = shape.getPointByOrigin('center', 'center'); 229 | const originX = shape.getOriginX(); 230 | const originY = shape.getOriginY(); 231 | const origin = shape.getPointByOrigin(originX, originY); 232 | const left = shape.getLeft() + (centerPoint.x - origin.x); 233 | const top = shape.getTop() + (centerPoint.y - origin.y); 234 | 235 | shape.set({ 236 | hasControls: true, 237 | hasBorders: true, 238 | originX: 'center', 239 | originY: 'center', 240 | left, 241 | top 242 | }); 243 | 244 | shape.setCoords(); // For left, top properties 245 | } 246 | }; 247 | -------------------------------------------------------------------------------- /src/lib/util.js: -------------------------------------------------------------------------------- 1 | const { min, max } = Math; 2 | 3 | function isExisty(param) { 4 | return param != null; 5 | } 6 | 7 | function isUndefined(obj) { 8 | return obj === void 0; 9 | } 10 | 11 | function isNull(obj) { 12 | return obj === null; 13 | } 14 | 15 | function isTruthy(obj) { 16 | return isExisty(obj) && obj !== false; 17 | } 18 | 19 | function isFalsy(obj) { 20 | return !isTruthy(obj); 21 | } 22 | 23 | function isArguments(obj) { 24 | var result = isExisty(obj) && (toString.call(obj) === '[object Arguments]' || !!obj.callee); 25 | 26 | return result; 27 | } 28 | 29 | function isArray(obj) { 30 | return Array.isArray(obj); 31 | } 32 | 33 | function isFunction(obj) { 34 | return obj instanceof Function; 35 | } 36 | 37 | function createObject() { 38 | function F() {} 39 | 40 | return function(obj) { 41 | F.prototype = obj; 42 | return new F(); 43 | }; 44 | } 45 | 46 | function inherit(subType, superType) { 47 | var prototype = createObject(superType.prototype); 48 | prototype.constructor = subType; 49 | subType.prototype = prototype; 50 | } 51 | 52 | var lastId = 0; 53 | 54 | function stamp(obj) { 55 | obj.__xm_id = obj.__xm_id || ++lastId; 56 | return obj.__xm_id; 57 | } 58 | 59 | function pick(obj, paths) { 60 | var args = arguments, 61 | target = args[0], 62 | length = args.length, 63 | i; 64 | try { 65 | for (i = 1; i < length; i++) { 66 | target = target[args[i]]; 67 | } 68 | return target; 69 | } catch (e) { 70 | return; 71 | } 72 | } 73 | 74 | function hasStamp(obj) { 75 | return isExisty(pick(obj, '__xm_id')); 76 | } 77 | 78 | function resetLastId() { 79 | lastId = 0; 80 | } 81 | function inArray(val, arr, startIndex = 0) { 82 | arr = arr || []; 83 | let len = arr.length; 84 | for (let i = startIndex; i < len; i++) { 85 | if (arr[i] === val) { 86 | return true; 87 | } 88 | } 89 | return false; 90 | } 91 | function compareJSON(object) { 92 | var leftChain, 93 | rightChain, 94 | argsLen = arguments.length, 95 | i; 96 | 97 | function isSameObject(x, y) { 98 | var p; 99 | 100 | if (isNaN(x) && isNaN(y) && isNumber(x) && isNumber(y)) { 101 | return true; 102 | } 103 | 104 | if (x === y) { 105 | return true; 106 | } 107 | 108 | if ( 109 | (isFunction(x) && isFunction(y)) || 110 | (x instanceof Date && y instanceof Date) || 111 | (x instanceof RegExp && y instanceof RegExp) || 112 | (x instanceof String && y instanceof String) || 113 | (x instanceof Number && y instanceof Number) 114 | ) { 115 | return x.toString() === y.toString(); 116 | } 117 | 118 | if (!(x instanceof Object && y instanceof Object)) { 119 | return false; 120 | } 121 | 122 | if ( 123 | x.isPrototypeOf(y) || 124 | y.isPrototypeOf(x) || 125 | x.constructor !== y.constructor || 126 | x.prototype !== y.prototype 127 | ) { 128 | return false; 129 | } 130 | 131 | if (inArray(x, leftChain) > -1 || inArray(y, rightChain) > -1) { 132 | return false; 133 | } 134 | 135 | for (p in y) { 136 | if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) { 137 | return false; 138 | } else if (typeof y[p] !== typeof x[p]) { 139 | return false; 140 | } 141 | } 142 | 143 | for (p in x) { 144 | if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) { 145 | return false; 146 | } else if (typeof y[p] !== typeof x[p]) { 147 | return false; 148 | } 149 | 150 | if (typeof x[p] === 'object' || typeof x[p] === 'function') { 151 | leftChain.push(x); 152 | rightChain.push(y); 153 | 154 | if (!isSameObject(x[p], y[p])) { 155 | return false; 156 | } 157 | 158 | leftChain.pop(); 159 | rightChain.pop(); 160 | } else if (x[p] !== y[p]) { 161 | return false; 162 | } 163 | } 164 | 165 | return true; 166 | } 167 | 168 | if (argsLen < 1) { 169 | return true; 170 | } 171 | 172 | for (i = 1; i < argsLen; i++) { 173 | leftChain = []; 174 | rightChain = []; 175 | 176 | if (!isSameObject(arguments[0], arguments[i])) { 177 | return false; 178 | } 179 | } 180 | 181 | return true; 182 | } 183 | 184 | function clamp(value, minValue, maxValue) { 185 | let temp; 186 | if (minValue > maxValue) { 187 | temp = minValue; 188 | minValue = maxValue; 189 | maxValue = temp; 190 | } 191 | 192 | return max(minValue, min(value, maxValue)); 193 | } 194 | 195 | function keyMirror(...args) { 196 | const obj = {}; 197 | 198 | args.forEach((key) => { 199 | obj[key] = key; 200 | }); 201 | 202 | return obj; 203 | } 204 | 205 | function makeStyleText(styleObj) { 206 | let styleStr = ''; 207 | Object.keys(styleObj).forEach((key) => { 208 | styleStr += `${key}: ${styleObj[key]};`; 209 | }); 210 | 211 | return styleStr; 212 | } 213 | var browser = { 214 | chrome: false, 215 | firefox: false, 216 | safari: false, 217 | msie: false, 218 | edge: false, 219 | others: false, 220 | version: 0 221 | }; 222 | var nav = window.navigator, 223 | appName = nav.appName.replace(/\s/g, '_'), 224 | userAgent = nav.userAgent; 225 | 226 | var rIE = /MSIE\s([0-9]+[.0-9]*)/, 227 | rIE11 = /Trident.*rv:11\./, 228 | rEdge = /Edge\/(\d+)\./, 229 | versionRegex = { 230 | firefox: /Firefox\/(\d+)\./, 231 | chrome: /Chrome\/(\d+)\./, 232 | safari: /Version\/([\d\.]+)\sSafari\/(\d+)/ 233 | }; 234 | 235 | var key, tmp; 236 | 237 | var detector = { 238 | Microsoft_Internet_Explorer: function() { 239 | // ie8 ~ ie10 240 | browser.msie = true; 241 | browser.version = parseFloat(userAgent.match(rIE)[1]); 242 | }, 243 | Netscape: function() { 244 | var detected = false; 245 | 246 | if (rIE11.exec(userAgent)) { 247 | browser.msie = true; 248 | browser.version = 11; 249 | detected = true; 250 | } else if (rEdge.exec(userAgent)) { 251 | browser.edge = true; 252 | browser.version = userAgent.match(rEdge)[1]; 253 | detected = true; 254 | } else { 255 | for (key in versionRegex) { 256 | if (versionRegex.hasOwnProperty(key)) { 257 | tmp = userAgent.match(versionRegex[key]); 258 | if (tmp && tmp.length > 1) { 259 | browser[key] = detected = true; 260 | browser.version = parseFloat(tmp[1] || 0); 261 | break; 262 | } 263 | } 264 | } 265 | } 266 | if (!detected) { 267 | browser.others = true; 268 | } 269 | } 270 | }; 271 | 272 | var fn = detector[appName]; 273 | 274 | if (fn) { 275 | detector[appName](); 276 | } 277 | 278 | function forEachArray(arr, iteratee, context) { 279 | var index = 0, 280 | len = arr.length; 281 | 282 | context = context || null; 283 | 284 | for (; index < len; index++) { 285 | if (iteratee.call(context, arr[index], index, arr) === false) { 286 | break; 287 | } 288 | } 289 | } 290 | 291 | function forEachOwnProperties(obj, iteratee, context) { 292 | var key; 293 | 294 | context = context || null; 295 | 296 | for (key in obj) { 297 | if (obj.hasOwnProperty(key)) { 298 | if (iteratee.call(context, obj[key], key, obj) === false) { 299 | break; 300 | } 301 | } 302 | } 303 | } 304 | 305 | function forEach(obj, iteratee, context) { 306 | if (Array.isArray(obj)) { 307 | forEachArray(obj, iteratee, context); 308 | } else { 309 | forEachOwnProperties(obj, iteratee, context); 310 | } 311 | } 312 | function map(obj, iteratee, context) { 313 | var resultArray = []; 314 | 315 | context = context || null; 316 | 317 | forEach(obj, function() { 318 | resultArray.push(iteratee.apply(context, arguments)); 319 | }); 320 | 321 | return resultArray; 322 | } 323 | 324 | function isObject(obj) { 325 | return obj === Object(obj); 326 | } 327 | 328 | function isString(obj) { 329 | return typeof obj === 'string' || obj instanceof String; 330 | } 331 | 332 | function isNumber(obj) { 333 | return typeof obj === 'number' || obj instanceof Number; 334 | } 335 | function bind(fn, obj) { 336 | var slice = Array.prototype.slice; 337 | 338 | if (fn.bind) { 339 | return fn.bind.apply(fn, slice.call(arguments, 1)); 340 | } 341 | 342 | /* istanbul ignore next */ 343 | var args = slice.call(arguments, 2); 344 | 345 | /* istanbul ignore next */ 346 | return function() { 347 | /* istanbul ignore next */ 348 | return fn.apply(obj, args.length ? args.concat(slice.call(arguments)) : arguments); 349 | }; 350 | } 351 | function extend(target, objects) { 352 | var source, 353 | prop, 354 | hasOwnProp = Object.prototype.hasOwnProperty, 355 | i, 356 | len; 357 | 358 | for (i = 1, len = arguments.length; i < len; i++) { 359 | source = arguments[i]; 360 | for (prop in source) { 361 | if (hasOwnProp.call(source, prop)) { 362 | target[prop] = source[prop]; 363 | } 364 | } 365 | } 366 | return target; 367 | } 368 | function setStyle(obj, css) { 369 | for (let atr in css) { 370 | obj.style.setProperty(atr, css[atr]); 371 | } 372 | } 373 | export default { 374 | createObject: createObject(), 375 | inherit: inherit, 376 | isFunction: isFunction, 377 | isArray: isArray, 378 | isArguments: isArguments, 379 | isString: isString, 380 | isNumber: isNumber, 381 | isFalsy: isFalsy, 382 | isObject: isObject, 383 | isTruthy: isTruthy, 384 | isNull: isNull, 385 | isUndefined: isUndefined, 386 | compareJSON: compareJSON, 387 | hasStamp: hasStamp, 388 | resetLastId: resetLastId, 389 | stamp: stamp, 390 | pick: pick, 391 | clamp: clamp, 392 | keyMirror: keyMirror, 393 | browser: browser, 394 | makeStyleText: makeStyleText, 395 | forEach: forEach, 396 | map: map, 397 | isExisty: isExisty, 398 | bind: bind, 399 | extend: extend, 400 | setStyle: setStyle, 401 | inArray: inArray 402 | }; 403 | -------------------------------------------------------------------------------- /src/module.js: -------------------------------------------------------------------------------- 1 | import CustomEvents from './lib/custom-event'; 2 | 3 | import Main from './modules/main'; 4 | import Draw from './modules/draw'; 5 | import Text from './modules/text'; 6 | import ImageLoader from './modules/image-loader'; 7 | import Rotation from './modules/rotation'; 8 | import Shape from './modules/shape'; 9 | import Line from './modules/line'; 10 | import Arrow from './modules/arrow'; 11 | import Cropper from './modules/cropper'; 12 | import Mosaic from './modules/mosaic'; 13 | import Pan from './modules/pan'; 14 | 15 | import consts from './consts'; 16 | 17 | import util from './lib/util.js'; 18 | 19 | const { eventNames, rejectMessages } = consts; 20 | 21 | export default class { 22 | constructor() { 23 | this._customEvents = new CustomEvents(); 24 | 25 | this._undoStack = []; 26 | this._redoStack = []; 27 | 28 | this._moduleMap = {}; 29 | 30 | /* Lock-flag for executing command*/ 31 | this._isLocked = false; 32 | 33 | this._createModules(); 34 | } 35 | 36 | _createModules() { 37 | const main = new Main(); 38 | 39 | this._register(main); 40 | this._register(new Draw(main)); 41 | this._register(new Text(main)); 42 | this._register(new ImageLoader(main)); 43 | this._register(new Mosaic(main)); 44 | this._register(new Rotation(main)); 45 | this._register(new Shape(main)); 46 | this._register(new Line(main)); 47 | this._register(new Arrow(main)); 48 | this._register(new Cropper(main)); 49 | this._register(new Pan(main)); 50 | } 51 | 52 | _register(component) { 53 | this._moduleMap[component.getName()] = component; 54 | } 55 | 56 | _invokeExecution(command) { 57 | this.lock(); 58 | 59 | return command 60 | .execute(this._moduleMap) 61 | .then((value) => { 62 | this.pushUndoStack(command); 63 | this.unlock(); 64 | if (util.isFunction(command.executeCallback)) { 65 | command.executeCallback(value); 66 | } 67 | 68 | return value; 69 | }) 70 | .catch((err) => { 71 | this.unlock(); 72 | console.error(err); 73 | }) // do nothing with exception 74 | .then((value) => { 75 | this.unlock(); 76 | 77 | return value; 78 | }); 79 | } 80 | 81 | _invokeUndo(command) { 82 | this.lock(); 83 | 84 | return command 85 | .undo(this._moduleMap) 86 | .then((value) => { 87 | this.pushRedoStack(command); 88 | this.unlock(); 89 | if (util.isFunction(command.undoCallback)) { 90 | command.undoCallback(value); 91 | } 92 | 93 | return value; 94 | }) 95 | .catch(() => { 96 | this.unlock(); 97 | console.error(err); 98 | }) //TODO do nothing with exception 99 | .then((value) => { 100 | this.unlock(); 101 | 102 | return value; 103 | }); 104 | } 105 | 106 | _fire(...args) { 107 | const event = this._customEvents; 108 | const eventContext = event; 109 | event.emit.apply(eventContext, args); 110 | } 111 | 112 | on(...args) { 113 | const event = this._customEvents; 114 | const eventContext = event; 115 | event.on.apply(eventContext, args); 116 | } 117 | 118 | getModule(name) { 119 | return this._moduleMap[name]; 120 | } 121 | 122 | lock() { 123 | this._isLocked = true; 124 | } 125 | 126 | unlock() { 127 | this._isLocked = false; 128 | } 129 | 130 | /** 131 | * 执行命令 132 | * 存储命令到undo然后清除 redoStack 133 | * @param {Command} command - Command 134 | * @returns {Promise} 135 | */ 136 | invoke(command) { 137 | if (this._isLocked) { 138 | return Promise.reject(rejectMessages.isLock); 139 | } 140 | 141 | return this._invokeExecution(command).then((value) => { 142 | this.clearRedoStack(); 143 | 144 | return value; 145 | }); 146 | } 147 | 148 | //undo命令 149 | undo() { 150 | let command = this._undoStack.pop(); 151 | let promise; 152 | 153 | if (command && this._isLocked) { 154 | this.pushUndoStack(command, true); 155 | command = null; 156 | } 157 | if (command) { 158 | if (this.isEmptyUndoStack()) { 159 | this._fire(eventNames.EMPTY_UNDO_STACK); 160 | } 161 | promise = this._invokeUndo(command); 162 | } else { 163 | promise = Promise.reject(rejectMessages.undo); 164 | } 165 | 166 | return promise; 167 | } 168 | 169 | //redo命令 170 | redo() { 171 | let command = this._redoStack.pop(); 172 | 173 | let promise; 174 | 175 | if (command && this._isLocked) { 176 | this.pushRedoStack(command, true); 177 | command = null; 178 | } 179 | if (command) { 180 | if (this.isEmptyRedoStack()) { 181 | this._fire(eventNames.EMPTY_REDO_STACK); 182 | } 183 | promise = this._invokeExecution(command); 184 | } else { 185 | promise = Promise.reject(rejectMessages.redo); 186 | } 187 | 188 | return promise; 189 | } 190 | 191 | /** 192 | * Push undo stack 193 | * @param {Command} command - command 194 | * @param {boolean} [isSilent] - Fire event or not 195 | */ 196 | pushUndoStack(command, isSilent) { 197 | this._undoStack.push(command); 198 | if (!isSilent) { 199 | this._fire(eventNames.PUSH_UNDO_STACK); 200 | } 201 | } 202 | 203 | pushRedoStack(command, isSilent) { 204 | this._redoStack.push(command); 205 | if (!isSilent) { 206 | this._fire(eventNames.PUSH_REDO_STACK); 207 | } 208 | } 209 | 210 | isEmptyRedoStack() { 211 | return this._redoStack.length === 0; 212 | } 213 | 214 | isEmptyUndoStack() { 215 | return this._undoStack.length === 0; 216 | } 217 | 218 | clearUndoStack() { 219 | if (!this.isEmptyUndoStack()) { 220 | this._undoStack = []; 221 | this._fire(eventNames.EMPTY_UNDO_STACK); 222 | } 223 | } 224 | 225 | clearRedoStack() { 226 | if (!this.isEmptyRedoStack()) { 227 | this._redoStack = []; 228 | this._fire(eventNames.EMPTY_REDO_STACK); 229 | } 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/modules/arrow.2.js: -------------------------------------------------------------------------------- 1 | import { fabric } from 'fabric'; 2 | import Base from './base'; 3 | import consts from '../consts'; 4 | 5 | const abs = Math.abs; 6 | const arrowPath = 7 | 'M3.9603906,29.711582 C3.94156309,29.8708042 3.79272845,29.9999998 3.63155855,29.9999998 C3.482237,30.0001621 3.33535003,29.8737257 3.31603561,29.7117443 L2.24238114,5.11020599 C2.2384858,5.02109998 2.16642191,4.9706228 2.08072432,4.99789021 L0.0900407177,5.63039686 C0.00466773962,5.65750197 -0.0253588782,5.61871082 0.0233329345,5.5427516 L3.54894478,0.0568073759 C3.59747429,-0.0186649336 3.6757058,-0.0191518517 3.72455992,0.0566450699 L7.24725025,5.54047931 C7.29577976,5.61595162 7.26624006,5.65539199 7.18070478,5.62812457 L5.19034578,4.99691638 C5.10513511,4.96981127 5.03290892,5.01899 5.02901358,5.10923216 L3.9603906,29.711582 Z'; 8 | export default class Arrow extends Base { 9 | constructor(parent) { 10 | super(); 11 | this.setParent(parent); 12 | this.name = consts.moduleNames.ARROW; 13 | this._width = 5; 14 | this._radius = 3; 15 | this._dimension = { 16 | height: 20, 17 | width: 20 18 | }; 19 | this._oColor = new fabric.Color('rgba(0, 0, 0, 0.5)'); 20 | this._listeners = { 21 | mousedown: this._onFabricMouseDown.bind(this), 22 | mousemove: this._onFabricMouseMove.bind(this), 23 | mouseup: this._onFabricMouseUp.bind(this) 24 | }; 25 | } 26 | 27 | /** 28 | * Start drawing arrow mode 29 | * @param {{width: ?number, color: ?string,radius:?number,dimension:?object} [setting] - Brush width & color 30 | */ 31 | start(setting) { 32 | const canvas = this.getCanvas(); 33 | 34 | canvas.defaultCursor = 'crosshair'; 35 | canvas.selection = false; 36 | 37 | canvas.forEachObject((obj) => { 38 | obj.set({ 39 | evented: false 40 | }); 41 | }); 42 | this.setBrush(setting); 43 | canvas.on({ 44 | 'mouse:down': this._listeners.mousedown 45 | }); 46 | } 47 | 48 | /** 49 | * Set brush 50 | * @param {{width: ?number, color: ?string,radius:?number,dimension:?object} [setting] - Brush width & color 51 | */ 52 | setBrush(setting) { 53 | const brush = this.getCanvas().freeDrawingBrush; 54 | 55 | setting = setting || {}; 56 | this._width = setting.width || this._width; 57 | this._radius = setting.radius || this._radius; 58 | this._dimension = Object.assign(this._dimension, setting.dimension); 59 | 60 | if (setting.color) { 61 | this._oColor = new fabric.Color(setting.color); 62 | } 63 | brush.width = this._width; 64 | brush.color = this._oColor.toRgba(); 65 | } 66 | 67 | /** 68 | * Set obj style 69 | * @param {object} activeObj - Current selected text object 70 | * @param {object} styleObj - Initial styles 71 | */ 72 | setStyle(activeObj, styleObj) { 73 | activeObj.set(styleObj); 74 | this.getCanvas().renderAll(); 75 | } 76 | 77 | /** 78 | * End drawing line mode 79 | */ 80 | end() { 81 | const canvas = this.getCanvas(); 82 | 83 | canvas.defaultCursor = 'default'; 84 | canvas.selection = false; 85 | 86 | canvas.forEachObject((obj) => { 87 | obj.set({ 88 | evented: true 89 | }); 90 | }); 91 | 92 | canvas.off('mouse:down', this._listeners.mousedown); 93 | } 94 | 95 | /** 96 | * Mousedown event handler in fabric canvas 97 | * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event object 98 | * @private 99 | */ 100 | _onFabricMouseDown(fEvent) { 101 | const canvas = this.getCanvas(); 102 | if (fEvent.target && fEvent.target.customType === 'arrow') { 103 | canvas.trigger('object:selected', { target: fEvent.target }); 104 | return; 105 | } 106 | this.startPointer = canvas.getPointer(fEvent.e); 107 | let arrow = (this.arrow = new fabric.Path(arrowPath)); 108 | this.arrow.set(consts.fObjectOptions.SELECTION_STYLE); 109 | this.arrow.set({ 110 | left: this.startPointer.x, 111 | top: this.startPointer.y 112 | }); 113 | this.arrow.setOriginX('center'); 114 | this.arrow.setOriginY('bottom'); 115 | arrow.customType = 'arrow'; 116 | canvas.add(arrow); 117 | canvas.renderAll(); 118 | canvas.on({ 119 | 'mouse:move': this._listeners.mousemove, 120 | 'mouse:up': this._listeners.mouseup 121 | }); 122 | } 123 | 124 | getAngle(x1, y1, x2, y2) { 125 | let x = Math.abs(x1 - x2), 126 | y = Math.abs(y1 - y2), 127 | z = Math.sqrt(x * x + y * y), 128 | rotat = Math.round((Math.asin(y / z) / Math.PI) * 180); 129 | // 第一象限 130 | if (x2 >= x1 && y2 <= y1) { 131 | rotat = 90 - rotat; 132 | } 133 | // 第二象限 134 | else if (x2 <= x1 && y2 <= y1) { 135 | rotat = rotat - 90; 136 | } 137 | // 第三象限 138 | else if (x2 <= x1 && y2 >= y1) { 139 | rotat = 270 - rotat; 140 | } 141 | // 第四象限 142 | else if (x2 >= x1 && y2 >= y1) { 143 | rotat = 90 + rotat; 144 | } 145 | return rotat; 146 | } 147 | 148 | /** 149 | * Mousemove event handler in fabric canvas 150 | * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event object 151 | * @private 152 | */ 153 | _onFabricMouseMove(fEvent) { 154 | const canvas = this.getCanvas(); 155 | const pointer = canvas.getPointer(fEvent.e); 156 | const { x: sx, y: sy } = this.startPointer; 157 | const x = pointer.x; 158 | const y = pointer.y; 159 | if (abs(x - sx) + abs(y - sy) > 5) { 160 | if (x === sx && y > sy) { 161 | this.arrow.setOriginX('center'); 162 | this.arrow.setOriginY('bottom'); 163 | } else if (x < sx && y > sy) { 164 | this.arrow.setOriginX('right'); 165 | this.arrow.setOriginY('bottom'); 166 | } else if (x < sx && y === sy) { 167 | this.arrow.setOriginX('right'); 168 | this.arrow.setOriginY('bottom'); 169 | } else if (x < sx && y < sy) { 170 | this.arrow.setOriginX('right'); 171 | this.arrow.setOriginY('top'); 172 | } else if (x === sx && y === sy) { 173 | this.arrow.setOriginX('center'); 174 | this.arrow.setOriginY('center'); 175 | } else if (x > sx && y < sy) { 176 | this.arrow.setOriginX('left'); 177 | this.arrow.setOriginY('top'); 178 | } else if (x > sx && y === sy) { 179 | this.arrow.setOriginX('center'); 180 | this.arrow.setOriginY('left'); 181 | } else if (x > sx && y > sy) { 182 | this.arrow.setOriginX('left'); 183 | this.arrow.setOriginY('bottom'); 184 | } else if (x === sx && y < sy) { 185 | this.arrow.setOriginX('center'); 186 | this.arrow.setOriginY('top'); 187 | } 188 | let scale = Math.max( 189 | (abs(x - this.startPointer.x) / 8) * this.getRoot().getZoom(), 190 | (abs(y - this.startPointer.y) / 30) * this.getRoot().getZoom() 191 | ); 192 | this.arrow.scale(scale); 193 | let angle = this.getAngle(this.startPointer.x, this.startPointer.y, x, y); 194 | this.arrow.setAngle(angle); 195 | canvas.renderAll(); 196 | } 197 | } 198 | 199 | /** 200 | * Mouseup event handler in fabric canvas 201 | * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event object 202 | * @private 203 | */ 204 | _onFabricMouseUp() { 205 | const canvas = this.getCanvas(); 206 | 207 | this.arrow = null; 208 | 209 | canvas.off({ 210 | 'mouse:move': this._listeners.mousemove, 211 | 'mouse:up': this._listeners.mouseup 212 | }); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/modules/arrow.js: -------------------------------------------------------------------------------- 1 | import { fabric } from 'fabric'; 2 | import Base from './base'; 3 | import consts from '../consts'; 4 | // import util from '../lib/util'; 5 | 6 | // const resetStyles = { 7 | // fill: '#000000', 8 | // width: 5 9 | // }; 10 | export default class Arrow extends Base { 11 | constructor(parent) { 12 | super(); 13 | this.setParent(parent); 14 | this.name = consts.moduleNames.ARROW; 15 | this._width = 5; 16 | this._radius = 3; 17 | this._dimension = { 18 | height: 20, 19 | width: 20 20 | }; 21 | this._oColor = new fabric.Color('rgba(0, 0, 0, 0.5)'); 22 | this._listeners = { 23 | mousedown: this._onFabricMouseDown.bind(this), 24 | mousemove: this._onFabricMouseMove.bind(this), 25 | mouseup: this._onFabricMouseUp.bind(this) 26 | }; 27 | } 28 | 29 | /** 30 | * Start drawing arrow mode 31 | * @param {{width: ?number, color: ?string,radius:?number,dimension:?object} [setting] - Brush width & color 32 | */ 33 | start(setting) { 34 | const canvas = this.getCanvas(); 35 | 36 | canvas.defaultCursor = 'crosshair'; 37 | canvas.selection = false; 38 | 39 | canvas.forEachObject((obj) => { 40 | obj.set({ 41 | evented: false 42 | }); 43 | }); 44 | this.setBrush(setting); 45 | canvas.on({ 46 | 'mouse:down': this._listeners.mousedown 47 | }); 48 | } 49 | 50 | /** 51 | * Set brush 52 | * @param {{width: ?number, color: ?string,radius:?number,dimension:?object} [setting] - Brush width & color 53 | */ 54 | setBrush(setting) { 55 | const brush = this.getCanvas().freeDrawingBrush; 56 | 57 | setting = setting || {}; 58 | this._width = setting.width || this._width; 59 | this._radius = setting.radius || this._radius; 60 | this._dimension = Object.assign(this._dimension, setting.dimension); 61 | 62 | if (setting.color) { 63 | this._oColor = new fabric.Color(setting.color); 64 | } 65 | brush.width = this._width; 66 | brush.color = this._oColor.toRgba(); 67 | } 68 | 69 | /** 70 | * Set obj style 71 | * @param {object} activeObj - Current selected text object 72 | * @param {object} styleObj - Initial styles 73 | */ 74 | setStyle(activeObj, styleObj) { 75 | activeObj.set(styleObj); 76 | this.getCanvas().renderAll(); 77 | } 78 | 79 | /** 80 | * End drawing line mode 81 | */ 82 | end() { 83 | const canvas = this.getCanvas(); 84 | 85 | canvas.defaultCursor = 'default'; 86 | canvas.selection = false; 87 | 88 | canvas.forEachObject((obj) => { 89 | obj.set({ 90 | evented: true 91 | }); 92 | }); 93 | 94 | canvas.off('mouse:down', this._listeners.mousedown); 95 | } 96 | 97 | /** 98 | * Mousedown event handler in fabric canvas 99 | * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event object 100 | * @private 101 | */ 102 | _onFabricMouseDown(fEvent) { 103 | const canvas = this.getCanvas(); 104 | // if(fEvent.target && fEvent.target.customType === 'arrow') { 105 | // canvas.trigger('object:selected', {target: fEvent.target}); 106 | // return; 107 | // } 108 | const pointer = (this.startPointer = canvas.getPointer(fEvent.e)); 109 | //this.drawArrow(pointer,pointer); 110 | let group = (this.group = new fabric.Group( 111 | [ 112 | /*this.line, this.arrow, this.circle*/ 113 | ], 114 | { 115 | left: pointer.x, 116 | top: pointer.y 117 | // originX: 'center', 118 | // originY: 'center', 119 | // selection:true, 120 | // transparentCorners: true, 121 | // hasControls :true, 122 | // hasBorders :true 123 | } 124 | )); 125 | this.group.set(consts.fObjectOptions.SELECTION_STYLE); 126 | // this.group.set('selectable', true); 127 | group.customType = 'arrow'; 128 | canvas.add(group); 129 | canvas.renderAll(); 130 | canvas.on({ 131 | 'mouse:move': this._listeners.mousemove, 132 | 'mouse:up': this._listeners.mouseup 133 | }); 134 | } 135 | 136 | drawArrow(startPointer, endPointer) { 137 | const points = [startPointer.x, startPointer.y, endPointer.x, endPointer.y]; 138 | const line = (this.line = new fabric.Line(points, { 139 | stroke: this._oColor.toRgba(), 140 | strokeWidth: this._width, 141 | padding: 5, 142 | originX: 'center', 143 | originY: 'center' 144 | })); 145 | 146 | let centerX = (line.x1 + line.x2) / 2, 147 | centerY = (line.y1 + line.y2) / 2; 148 | let deltaX = line.left - centerX, 149 | deltaY = line.top - centerY; 150 | 151 | const arrow = (this.arrow = new fabric.Triangle({ 152 | left: line.get('x1') + deltaX, 153 | top: line.get('y1') + deltaY, 154 | originX: 'center', 155 | originY: 'center', 156 | pointType: 'arrow_start', 157 | angle: 158 | startPointer.x === endPointer.x && startPointer.y === endPointer.y 159 | ? -45 160 | : this.calcArrowAngle( 161 | startPointer.x, 162 | startPointer.y, 163 | endPointer.x, 164 | endPointer.y 165 | ) - 90, 166 | width: this._dimension.width, 167 | height: this._dimension.height, 168 | fill: this._oColor.toRgba() 169 | })); 170 | const circle = (this.circle = new fabric.Circle({ 171 | left: line.get('x2') + deltaX, 172 | top: line.get('y2') + deltaY, 173 | radius: this._radius, 174 | stroke: this._oColor.toRgba(), 175 | strokeWidth: this._width, 176 | originX: 'center', 177 | originY: 'center', 178 | pointType: 'arrow_end', 179 | fill: this._oColor.toRgba() 180 | })); 181 | line.customType = arrow.customType = circle.customType = 'arrow'; 182 | } 183 | 184 | /** 185 | * Mousemove event handler in fabric canvas 186 | * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event object 187 | * @private 188 | */ 189 | _onFabricMouseMove(fEvent) { 190 | const canvas = this.getCanvas(); 191 | const pointer = canvas.getPointer(fEvent.e); 192 | const { x, y } = pointer.x; 193 | if (Math.abs(x - this.startPointer.x) + Math.abs(y - this.startPointer.y) > 5) { 194 | this.group.remove(this.line, this.arrow, this.circle); 195 | this.drawArrow(pointer, this.startPointer); 196 | this.group.addWithUpdate(this.arrow); 197 | this.group.addWithUpdate(this.line); 198 | this.group.addWithUpdate(this.circle); 199 | canvas.renderAll(); 200 | } 201 | } 202 | 203 | /** 204 | * Mouseup event handler in fabric canvas 205 | * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event object 206 | * @private 207 | */ 208 | _onFabricMouseUp() { 209 | const canvas = this.getCanvas(); 210 | 211 | this.line = null; 212 | // canvas.setActiveObject(this.group); 213 | 214 | canvas.off({ 215 | 'mouse:move': this._listeners.mousemove, 216 | 'mouse:up': this._listeners.mouseup 217 | }); 218 | } 219 | 220 | calcArrowAngle(x1, y1, x2, y2) { 221 | var angle = 0, 222 | x, 223 | y; 224 | x = x2 - x1; 225 | y = y2 - y1; 226 | if (x === 0) { 227 | angle = y === 0 ? 0 : y > 0 ? Math.PI / 2 : (Math.PI * 3) / 2; 228 | } else if (y === 0) { 229 | angle = x > 0 ? 0 : Math.PI; 230 | } else { 231 | angle = 232 | x < 0 233 | ? Math.atan(y / x) + Math.PI 234 | : y < 0 235 | ? Math.atan(y / x) + 2 * Math.PI 236 | : Math.atan(y / x); 237 | } 238 | return (angle * 180) / Math.PI; 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/modules/base.js: -------------------------------------------------------------------------------- 1 | /* 2 | * module base class 3 | * all modules should inherite this base class 4 | */ 5 | export default class { 6 | /** 7 | * Save image(background) of canvas 8 | * @param {string} name - Name of image 9 | * @param {fabric.Image} oImage - Fabric image instance 10 | */ 11 | setCanvasImage(name, oImage) { 12 | this.getRoot().setCanvasImage(name, oImage); 13 | } 14 | 15 | /** 16 | * Returns canvas element of fabric.Canvas[[lower-canvas]] 17 | * @returns {HTMLCanvasElement} 18 | */ 19 | getCanvasElement() { 20 | return this.getRoot().getCanvasElement(); 21 | } 22 | 23 | /** 24 | * Get fabric.Canvas instance 25 | * @returns {fabric.Canvas} 26 | */ 27 | getCanvas() { 28 | return this.getRoot().getCanvas(); 29 | } 30 | 31 | /** 32 | * Get canvasImage (fabric.Image instance) 33 | * @returns {fabric.Image} 34 | */ 35 | getCanvasImage() { 36 | return this.getRoot().getCanvasImage(); 37 | } 38 | 39 | /** 40 | * Get image name 41 | * @returns {string} 42 | */ 43 | getImageName() { 44 | return this.getRoot().getImageName(); 45 | } 46 | 47 | /** 48 | * Get image editor 49 | * @returns {ImageEditor} 50 | */ 51 | getEditor() { 52 | return this.getRoot().getEditor(); 53 | } 54 | 55 | /** 56 | * Return component name 57 | * @returns {string} 58 | */ 59 | getName() { 60 | return this.name; 61 | } 62 | 63 | /** 64 | * Set image properties 65 | * @param {object} setting - Image properties 66 | * @param {boolean} [withRendering] - If true, The changed image will be reflected in the canvas 67 | */ 68 | setImageProperties(setting, withRendering) { 69 | this.getRoot().setImageProperties(setting, withRendering); 70 | } 71 | 72 | /** 73 | * Set canvas dimension - css only 74 | * @param {object} dimension - Canvas css dimension 75 | */ 76 | setCanvasCssDimension(dimension) { 77 | this.getRoot().setCanvasCssDimension(dimension); 78 | } 79 | 80 | /** 81 | * Set canvas dimension - css only 82 | * @param {object} dimension - Canvas backstore dimension 83 | */ 84 | setCanvasBackstoreDimension(dimension) { 85 | this.getRoot().setCanvasBackstoreDimension(dimension); 86 | } 87 | 88 | /** 89 | * Set parent 90 | * @param {Component|null} parent - Parent 91 | */ 92 | setParent(parent) { 93 | this._parent = parent || null; 94 | } 95 | 96 | /** 97 | * Adjust canvas dimension with scaling image 98 | */ 99 | adjustCanvasDimension() { 100 | this.getRoot().adjustCanvasDimension(); 101 | } 102 | 103 | /** 104 | * Return parent. 105 | * If the view is root, return null 106 | * @returns {Component|null} 107 | */ 108 | getParent() { 109 | return this._parent; 110 | } 111 | 112 | /** 113 | * Return root 114 | * @returns {Module} 115 | */ 116 | getRoot() { 117 | let next = this.getParent(); 118 | let current = this; // eslint-disable-line consistent-this 119 | 120 | while (next) { 121 | current = next; 122 | next = current.getParent(); 123 | } 124 | 125 | return current; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/modules/cropper.js: -------------------------------------------------------------------------------- 1 | import { fabric } from 'fabric'; 2 | 3 | import Base from './base.js'; 4 | import consts from '../consts'; 5 | import Cropzone from '../shape/cropzone'; 6 | import util from '../lib/util'; 7 | 8 | const { MOUSE_MOVE_THRESHOLD } = consts; 9 | const { clamp, bind } = util; 10 | const { keyCodes } = consts; 11 | 12 | export default class Cropper extends Base { 13 | constructor(parent) { 14 | super(); 15 | 16 | this.setParent(parent); 17 | 18 | this.name = consts.moduleNames.CROPPER; 19 | 20 | this._cropzone = null; 21 | 22 | this._startX = null; 23 | 24 | this._startY = null; 25 | 26 | this._withShiftKey = false; 27 | 28 | this._listeners = { 29 | keydown: bind(this._onKeyDown, this), 30 | keyup: bind(this._onKeyUp, this), 31 | mousedown: bind(this._onFabricMouseDown, this), 32 | mousemove: bind(this._onFabricMouseMove, this), 33 | mouseup: bind(this._onFabricMouseUp, this) 34 | }; 35 | } 36 | 37 | start() { 38 | if (this._cropzone) { 39 | return; 40 | } 41 | const canvas = this.getCanvas(); 42 | canvas.forEachObject((obj) => { 43 | // {@link http://fabricjs.com/docs/fabric.Object.html#evented} 44 | obj.evented = false; 45 | }); 46 | let canvasCssWidth = parseInt(canvas.wrapperEl.style['width'], 10), 47 | canvasCssHeight = parseInt(canvas.wrapperEl.style['height'], 10), 48 | canvasWidth = canvas.upperCanvasEl.width; 49 | let radio = canvasCssWidth / canvasWidth; 50 | let marginLeft = (canvasCssWidth * 0.1) / radio; 51 | let marginTop = (canvasCssHeight * 0.1) / radio; 52 | let width = (canvasCssWidth * 0.8) / radio; 53 | let height = (canvasCssHeight * 0.8) / radio; 54 | 55 | this._cropzone = new Cropzone({ 56 | left: marginLeft, 57 | top: marginTop, 58 | width: width, 59 | height: height, 60 | strokeWidth: 0, // {@link https://github.com/kangax/fabric.js/issues/2860} 61 | cornerStyle: 'circle', 62 | cornerColor: '#FFFFFF', 63 | cornerStrokeColor: '#118BFB', 64 | cornerSize: 15, 65 | fill: 'transparent', 66 | hasRotatingPoint: false, 67 | hasBorders: false, 68 | lockScalingFlip: true, 69 | lockRotation: true 70 | }); 71 | canvas.deactivateAll(); 72 | canvas.add(this._cropzone); 73 | canvas.on('mouse:down', this._listeners.mousedown); 74 | canvas.selection = false; 75 | canvas.defaultCursor = 'crosshair'; 76 | canvas.setActiveObject(this._cropzone); 77 | 78 | fabric.util.addListener(document, 'keydown', this._listeners.keydown); 79 | fabric.util.addListener(document, 'keyup', this._listeners.keyup); 80 | } 81 | 82 | /** 83 | * End cropping 84 | * @param {boolean} isApplying - Is applying or not 85 | * @returns {?{imageName: string, url: string}} cropped Image data 86 | */ 87 | end(isApplying) { 88 | const canvas = this.getCanvas(); 89 | const cropzone = this._cropzone; 90 | let data; 91 | canvas.off('mouse:down', this._listeners.mousedown); 92 | fabric.util.removeListener(document, 'keydown', this._listeners.keydown); 93 | fabric.util.removeListener(document, 'keyup', this._listeners.keyup); 94 | if (!cropzone) { 95 | return null; 96 | } 97 | cropzone.remove(); 98 | canvas.selection = false; 99 | canvas.defaultCursor = 'default'; 100 | canvas.forEachObject((obj) => { 101 | obj.evented = true; 102 | }); 103 | if (isApplying) { 104 | data = this._getCroppedImageData(); 105 | } 106 | this._cropzone = null; 107 | 108 | return data; 109 | } 110 | 111 | _onFabricMouseDown(fEvent) { 112 | const canvas = this.getCanvas(); 113 | 114 | if (fEvent.target) { 115 | return; 116 | } 117 | 118 | canvas.selection = false; 119 | const coord = canvas.getPointer(fEvent.e); 120 | 121 | this._startX = coord.x; 122 | this._startY = coord.y; 123 | 124 | canvas.on({ 125 | 'mouse:move': this._listeners.mousemove, 126 | 'mouse:up': this._listeners.mouseup 127 | }); 128 | } 129 | 130 | _onFabricMouseMove(fEvent) { 131 | const canvas = this.getCanvas(); 132 | const pointer = canvas.getPointer(fEvent.e); 133 | const x = pointer.x; 134 | const y = pointer.y; 135 | const cropzone = this._cropzone; 136 | 137 | if (Math.abs(x - this._startX) + Math.abs(y - this._startY) > MOUSE_MOVE_THRESHOLD) { 138 | cropzone.remove(); 139 | cropzone.set(this._calcRectDimensionFromPoint(x, y)); 140 | 141 | canvas.add(cropzone); 142 | } 143 | } 144 | 145 | /** 146 | * Get rect dimension setting from Canvas-Mouse-Position(x, y) 147 | * @param {number} x - Canvas-Mouse-Position x 148 | * @param {number} y - Canvas-Mouse-Position Y 149 | * @returns {{left: number, top: number, width: number, height: number}} 150 | * @private 151 | */ 152 | _calcRectDimensionFromPoint(x, y) { 153 | const canvas = this.getCanvas(); 154 | const canvasWidth = canvas.getWidth(); 155 | const canvasHeight = canvas.getHeight(); 156 | const startX = this._startX; 157 | const startY = this._startY; 158 | let left = clamp(x, 0, startX); 159 | let top = clamp(y, 0, startY); 160 | let width = clamp(x, startX, canvasWidth) - left; // (startX <= x(mouse) <= canvasWidth) - left 161 | let height = clamp(y, startY, canvasHeight) - top; // (startY <= y(mouse) <= canvasHeight) - top 162 | 163 | if (this._withShiftKey) { 164 | // make fixed ratio cropzone 165 | if (width > height) { 166 | height = width; 167 | } else if (height > width) { 168 | width = height; 169 | } 170 | 171 | if (startX >= x) { 172 | left = startX - width; 173 | } 174 | 175 | if (startY >= y) { 176 | top = startY - height; 177 | } 178 | } 179 | 180 | return { 181 | left, 182 | top, 183 | width, 184 | height 185 | }; 186 | } 187 | 188 | _onFabricMouseUp() { 189 | const cropzone = this._cropzone; 190 | const listeners = this._listeners; 191 | const canvas = this.getCanvas(); 192 | 193 | canvas.setActiveObject(cropzone); 194 | canvas.off({ 195 | 'mouse:move': listeners.mousemove, 196 | 'mouse:up': listeners.mouseup 197 | }); 198 | } 199 | 200 | _getCroppedImageData() { 201 | const cropzone = this._cropzone; 202 | 203 | if (!cropzone.isValid()) { 204 | return null; 205 | } 206 | 207 | const cropInfo = { 208 | left: cropzone.getLeft(), 209 | top: cropzone.getTop(), 210 | width: cropzone.getWidth(), 211 | height: cropzone.getHeight() 212 | }; 213 | 214 | return { 215 | imageName: this.getImageName(), 216 | url: this.getCanvas().toDataURL(cropInfo) 217 | }; 218 | } 219 | 220 | _onKeyDown(e) { 221 | if (e.keyCode === keyCodes.SHIFT) { 222 | this._withShiftKey = true; 223 | } 224 | } 225 | 226 | _onKeyUp(e) { 227 | if (e.keyCode === keyCodes.SHIFT) { 228 | this._withShiftKey = false; 229 | } 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/modules/draw.js: -------------------------------------------------------------------------------- 1 | import { fabric } from 'fabric'; 2 | import Base from './base.js'; 3 | import consts from '../consts'; 4 | 5 | export default class FreeDrawing extends Base { 6 | constructor(parent) { 7 | super(); 8 | this.setParent(parent); 9 | this.name = consts.moduleNames.FREE_DRAWING; 10 | this.width = 12; 11 | this.oColor = new fabric.Color('rgba(0, 0, 0, 0.5)'); 12 | } 13 | 14 | /** 15 | * Start free drawing mode 16 | * @param {{width: ?number, color: ?string}} [setting] - Brush width & color 17 | */ 18 | start(setting) { 19 | const canvas = this.getCanvas(); 20 | canvas.isDrawingMode = true; 21 | this.setBrush(setting); 22 | } 23 | 24 | /** 25 | * Set brush 26 | * @param {{width: ?number, color: ?string}} [setting] - Brush width & color 27 | */ 28 | setBrush(setting) { 29 | let brush = this.getCanvas().freeDrawingBrush; 30 | setting = setting || {}; 31 | this.width = setting.width || this.width; 32 | if (setting.color) { 33 | this.oColor = new fabric.Color(setting.color); 34 | } 35 | brush.width = this.width; 36 | brush.color = this.oColor.toRgba(); 37 | } 38 | 39 | /** 40 | * Set obj style 41 | * @param {object} activeObj - Current selected text object 42 | * @param {object} styleObj - Initial styles 43 | */ 44 | setStyle(activeObj, styleObj) { 45 | activeObj.set(styleObj); 46 | this.getCanvas().renderAll(); 47 | } 48 | 49 | /** 50 | * End free drawing mode 51 | */ 52 | end() { 53 | const canvas = this.getCanvas(); 54 | canvas.isDrawingMode = false; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/modules/image-loader.js: -------------------------------------------------------------------------------- 1 | import Base from './base.js'; 2 | import consts from '../consts'; 3 | 4 | const {moduleNames, rejectMessages} = consts; 5 | const imageOption = { 6 | padding: 0, 7 | crossOrigin: 'anonymous' 8 | }; 9 | 10 | export default class ImageLoader extends Base { 11 | constructor(parent) { 12 | super(); 13 | this.setParent(parent); 14 | this.name = moduleNames.IMAGE_LOADER; 15 | } 16 | 17 | /** 18 | * Load image from url 19 | * @param {?string} imageName - File name 20 | * @param {?(fabric.Image|string)} img - fabric.Image instance or URL of an image 21 | * @returns {jQuery.Deferred} deferred 22 | */ 23 | load(imageName, img) { 24 | let promise; 25 | 26 | if (!imageName && !img) { // Back to the initial state, not error. 27 | const canvas = this.getCanvas(); 28 | 29 | canvas.backgroundImage = null; 30 | canvas.renderAll(); 31 | 32 | promise = new Promise(resolve => { 33 | this.setCanvasImage('', null); 34 | resolve(); 35 | }); 36 | } else { 37 | promise = this._setBackgroundImage(img).then(oImage => { 38 | this.setCanvasImage(imageName, oImage); 39 | this.adjustCanvasDimension(); 40 | 41 | return oImage; 42 | }); 43 | } 44 | 45 | return promise; 46 | } 47 | 48 | /** 49 | * Set background image 50 | * @param {?(fabric.Image|String)} img fabric.Image instance or URL of an image to set background to 51 | * @returns {$.Deferred} deferred 52 | * @private 53 | */ 54 | _setBackgroundImage(img) { 55 | if (!img) { 56 | return Promise.reject(rejectMessages.loadImage); 57 | } 58 | 59 | return new Promise((resolve, reject) => { 60 | const canvas = this.getCanvas(); 61 | 62 | canvas.setBackgroundImage(img, () => { 63 | const oImage = canvas.backgroundImage; 64 | 65 | if (oImage.getElement()) { 66 | resolve(oImage); 67 | } else { 68 | reject(); 69 | } 70 | }, imageOption); 71 | }); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/modules/line.js: -------------------------------------------------------------------------------- 1 | import { fabric } from 'fabric'; 2 | import Base from './base'; 3 | import consts from '../consts'; 4 | 5 | export default class Line extends Base { 6 | constructor(parent) { 7 | super(); 8 | this.setParent(parent); 9 | this.name = consts.moduleNames.LINE; 10 | this._width = 12; 11 | this._oColor = new fabric.Color('rgba(0, 0, 0, 0.5)'); 12 | 13 | this._listeners = { 14 | mousedown: this._onFabricMouseDown.bind(this), 15 | mousemove: this._onFabricMouseMove.bind(this), 16 | mouseup: this._onFabricMouseUp.bind(this) 17 | }; 18 | } 19 | 20 | /** 21 | * Start drawing line mode 22 | * @param {{width: ?number, color: ?string}} [setting] - Brush width & color 23 | */ 24 | start(setting) { 25 | const canvas = this.getCanvas(); 26 | 27 | canvas.defaultCursor = 'crosshair'; 28 | canvas.selection = false; 29 | 30 | this.setBrush(setting); 31 | 32 | canvas.forEachObject((obj) => { 33 | obj.set({ 34 | evented: false 35 | }); 36 | }); 37 | 38 | canvas.on({ 39 | 'mouse:down': this._listeners.mousedown 40 | }); 41 | } 42 | 43 | /** 44 | * Set brush 45 | * @param {{width: ?number, color: ?string}} [setting] - Brush width & color 46 | */ 47 | setBrush(setting) { 48 | const brush = this.getCanvas().freeDrawingBrush; 49 | 50 | setting = setting || {}; 51 | this._width = setting.width || this._width; 52 | 53 | if (setting.color) { 54 | this._oColor = new fabric.Color(setting.color); 55 | } 56 | brush.width = this._width; 57 | brush.color = this._oColor.toRgba(); 58 | } 59 | 60 | /** 61 | * End drawing line mode 62 | */ 63 | end() { 64 | const canvas = this.getCanvas(); 65 | 66 | canvas.defaultCursor = 'default'; 67 | canvas.selection = false; 68 | 69 | canvas.forEachObject((obj) => { 70 | obj.set({ 71 | evented: true 72 | }); 73 | }); 74 | 75 | canvas.off('mouse:down', this._listeners.mousedown); 76 | } 77 | 78 | /** 79 | * Mousedown event handler in fabric canvas 80 | * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event object 81 | * @private 82 | */ 83 | _onFabricMouseDown(fEvent) { 84 | const canvas = this.getCanvas(); 85 | const pointer = canvas.getPointer(fEvent.e); 86 | const points = [pointer.x, pointer.y, pointer.x, pointer.y]; 87 | 88 | this._line = new fabric.Line(points, { 89 | stroke: this._oColor.toRgba(), 90 | strokeWidth: this._width, 91 | evented: false 92 | }); 93 | 94 | this._line.set(consts.fObjectOptions.SELECTION_STYLE); 95 | 96 | canvas.add(this._line); 97 | 98 | canvas.on({ 99 | 'mouse:move': this._listeners.mousemove, 100 | 'mouse:up': this._listeners.mouseup 101 | }); 102 | } 103 | 104 | /** 105 | * Mousemove event handler in fabric canvas 106 | * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event object 107 | * @private 108 | */ 109 | _onFabricMouseMove(fEvent) { 110 | const canvas = this.getCanvas(); 111 | const pointer = canvas.getPointer(fEvent.e); 112 | 113 | this._line.set({ 114 | x2: pointer.x, 115 | y2: pointer.y 116 | }); 117 | 118 | this._line.setCoords(); 119 | 120 | canvas.renderAll(); 121 | } 122 | 123 | /** 124 | * Mouseup event handler in fabric canvas 125 | * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event object 126 | * @private 127 | */ 128 | _onFabricMouseUp() { 129 | const canvas = this.getCanvas(); 130 | 131 | this._line = null; 132 | 133 | canvas.off({ 134 | 'mouse:move': this._listeners.mousemove, 135 | 'mouse:up': this._listeners.mouseup 136 | }); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/modules/mosaic.1.js: -------------------------------------------------------------------------------- 1 | import Base from './base.js'; 2 | import consts from '../consts'; 3 | 4 | export default class Mosaic extends Base { 5 | constructor(parent) { 6 | super(); 7 | this.setParent(parent); 8 | 9 | this.name = consts.moduleNames.MOSAIC; 10 | 11 | this._dimensions = 16; 12 | 13 | this._listeners = { 14 | mousedown: this._onFabricMouseDown.bind(this), 15 | mousemove: this._onFabricMouseMove.bind(this), 16 | mouseup: this._onFabricMouseUp.bind(this) 17 | }; 18 | } 19 | 20 | /** 21 | * @param {{dimensions: ?number}} [setting] - Mosaic width 22 | */ 23 | start(setting) { 24 | const canvas = this.getCanvas(); 25 | 26 | canvas.defaultCursor = 'crosshair'; 27 | canvas.selection = false; 28 | 29 | setting = setting || {}; 30 | this._dimensions = parseInt(setting.dimensions) || this._dimensions; 31 | 32 | canvas.forEachObject((obj) => { 33 | obj.set({ 34 | evented: false 35 | }); 36 | }); 37 | 38 | canvas.on({ 39 | 'mouse:down': this._listeners.mousedown 40 | }); 41 | } 42 | 43 | end() { 44 | const canvas = this.getCanvas(); 45 | 46 | canvas.defaultCursor = 'default'; 47 | canvas.selection = false; 48 | 49 | canvas.forEachObject((obj) => { 50 | obj.set({ 51 | evented: true 52 | }); 53 | }); 54 | 55 | canvas.off('mouse:down', this._listeners.mousedown); 56 | } 57 | 58 | _onFabricMouseDown(fEvent) { 59 | const canvas = this.getCanvas(); 60 | const pointer = (this.pointer = canvas.getPointer(fEvent.e)); 61 | this._mosaicGroup = new fabric.Group([], { 62 | left: pointer.x, 63 | top: pointer.y, 64 | originX: 'center', 65 | originY: 'center' 66 | }); 67 | canvas.add(this._mosaicGroup); 68 | this._mosaicGroup.set('selectable', false); 69 | canvas.renderAll(); 70 | canvas.on({ 71 | 'mouse:move': this._listeners.mousemove, 72 | 'mouse:up': this._listeners.mouseup 73 | }); 74 | } 75 | _onFabricMouseMove(fEvent) { 76 | let ratio = this.getCanvasRatio(); 77 | ratio = Math.ceil(ratio); 78 | let dimensions = this._dimensions * ratio; 79 | const canvas = this.getCanvas(); 80 | const pointer = canvas.getPointer(fEvent.e); 81 | let imageData = canvas.contextContainer.getImageData( 82 | parseInt(pointer.x), 83 | parseInt(pointer.y), 84 | dimensions, 85 | dimensions 86 | ); 87 | // let imageData = canvas.getContext().getImageData(parseInt(pointer.x), parseInt(pointer.y), this._dimensions, this._dimensions); 88 | let rgba = [0, 0, 0, 0]; 89 | let length = imageData.data.length / 4; 90 | for (let i = 0; i < length; i++) { 91 | rgba[0] += imageData.data[i * 4]; 92 | rgba[1] += imageData.data[i * 4 + 1]; 93 | rgba[2] += imageData.data[i * 4 + 2]; 94 | rgba[3] += imageData.data[i * 4 + 3]; 95 | } 96 | let mosaicRect = new fabric.Rect({ 97 | fill: `rgb(${parseInt(rgba[0] / length)},${parseInt(rgba[1] / length)},${parseInt( 98 | rgba[2] / length 99 | )})`, 100 | height: dimensions, 101 | width: dimensions, 102 | left: pointer.x, 103 | top: pointer.y 104 | }); 105 | //this._mosaicGroup.addWithUpdate(mosaicRect); 106 | canvas.add(mosaicRect); 107 | canvas.renderAll(); 108 | } 109 | 110 | _onFabricMouseUp() { 111 | const canvas = this.getCanvas(); 112 | this._mosaicGroup = null; 113 | this.pointer = null; 114 | canvas.off({ 115 | 'mouse:move': this._listeners.mousemove, 116 | 'mouse:up': this._listeners.mouseup 117 | }); 118 | } 119 | /** 120 | * Get ratio value of canvas 121 | * @returns {number} Ratio value 122 | */ 123 | getCanvasRatio() { 124 | const canvasElement = this.getCanvasElement(); 125 | const cssWidth = parseInt(canvasElement.style.width, 10); 126 | const originWidth = canvasElement.width; 127 | const ratio = originWidth / cssWidth; 128 | return ratio; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/modules/mosaic.2.js: -------------------------------------------------------------------------------- 1 | import Base from './base.js'; 2 | import consts from '../consts'; 3 | import MosaicShape from '../shape/mosaic.js'; 4 | 5 | export default class Mosaic extends Base { 6 | constructor(parent) { 7 | super(); 8 | this.setParent(parent); 9 | 10 | this.name = consts.moduleNames.MOSAIC; 11 | 12 | this._dimensions = 20; 13 | 14 | this._listeners = { 15 | mousedown: this._onFabricMouseDown.bind(this), 16 | mousemove: this._onFabricMouseMove.bind(this), 17 | mouseup: this._onFabricMouseUp.bind(this) 18 | }; 19 | } 20 | 21 | /** 22 | * @param {{dimensions: ?number}} [setting] - Mosaic width 23 | */ 24 | start(setting) { 25 | const canvas = this.getCanvas(); 26 | 27 | canvas.defaultCursor = 'crosshair'; 28 | canvas.selection = false; 29 | 30 | setting = setting || {}; 31 | this._dimensions = parseInt(setting.dimensions) || this._dimensions; 32 | 33 | canvas.forEachObject((obj) => { 34 | obj.set({ 35 | evented: false 36 | }); 37 | }); 38 | 39 | canvas.on({ 40 | 'mouse:down': this._listeners.mousedown 41 | }); 42 | } 43 | 44 | end() { 45 | const canvas = this.getCanvas(); 46 | 47 | canvas.defaultCursor = 'default'; 48 | canvas.selection = false; 49 | 50 | canvas.forEachObject((obj) => { 51 | obj.set({ 52 | evented: true 53 | }); 54 | }); 55 | 56 | canvas.off('mouse:down', this._listeners.mousedown); 57 | } 58 | 59 | _onFabricMouseDown(fEvent) { 60 | const canvas = this.getCanvas(); 61 | const pointer = (this.pointer = canvas.getPointer(fEvent.e)); 62 | this._mosaicShape = new MosaicShape({ 63 | mosaicRects: [], 64 | selectable: false, 65 | left: pointer.x, 66 | top: pointer.y, 67 | originX: 'center', 68 | originY: 'center' 69 | }); 70 | canvas.add(this._mosaicShape); 71 | canvas.renderAll(); 72 | canvas.on({ 73 | 'mouse:move': this._listeners.mousemove, 74 | 'mouse:up': this._listeners.mouseup 75 | }); 76 | } 77 | _onFabricMouseMove(fEvent) { 78 | let ratio = this.getCanvasRatio(); 79 | ratio = Math.ceil(ratio); 80 | let dimensions = this._dimensions * ratio; 81 | const canvas = this.getCanvas(); 82 | const pointer = canvas.getPointer(fEvent.e); 83 | let imageData = canvas.contextContainer.getImageData( 84 | parseInt(pointer.x), 85 | parseInt(pointer.y), 86 | dimensions, 87 | dimensions 88 | ); 89 | // let imageData = canvas.getContext().getImageData(parseInt(pointer.x), parseInt(pointer.y), this._dimensions, this._dimensions); 90 | let rgba = [0, 0, 0, 0]; 91 | let length = imageData.data.length / 4; 92 | for (let i = 0; i < length; i++) { 93 | rgba[0] += imageData.data[i * 4]; 94 | rgba[1] += imageData.data[i * 4 + 1]; 95 | rgba[2] += imageData.data[i * 4 + 2]; 96 | rgba[3] += imageData.data[i * 4 + 3]; 97 | } 98 | this._mosaicShape.addMosicRectWithUpdate({ 99 | left: pointer.x, 100 | top: pointer.y, 101 | fill: `rgb(${parseInt(rgba[0] / length)},${parseInt(rgba[1] / length)},${parseInt( 102 | rgba[2] / length 103 | )})`, 104 | dimensions: dimensions 105 | }); 106 | canvas.renderAll(); 107 | } 108 | 109 | _onFabricMouseUp() { 110 | const canvas = this.getCanvas(); 111 | this._mosaicShape = null; 112 | canvas.off({ 113 | 'mouse:move': this._listeners.mousemove, 114 | 'mouse:up': this._listeners.mouseup 115 | }); 116 | } 117 | 118 | /** 119 | * Get ratio value of canvas 120 | * @returns {number} Ratio value 121 | */ 122 | getCanvasRatio() { 123 | const canvasElement = this.getCanvasElement(); 124 | const cssWidth = parseInt(canvasElement.style.width, 10); 125 | const originWidth = canvasElement.width; 126 | const ratio = originWidth / cssWidth; 127 | return ratio; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/modules/mosaic.js: -------------------------------------------------------------------------------- 1 | import Base from './base.js'; 2 | import consts from '../consts'; 3 | import MosaicShape from '../shape/mosaic.js'; 4 | 5 | export default class Mosaic extends Base { 6 | constructor(parent) { 7 | super(); 8 | this.setParent(parent); 9 | 10 | this.name = consts.moduleNames.MOSAIC; 11 | 12 | this._dimensions = 8; 13 | 14 | this._listeners = { 15 | mousedown: this._onFabricMouseDown.bind(this), 16 | mousemove: this._onFabricMouseMove.bind(this), 17 | mouseup: this._onFabricMouseUp.bind(this) 18 | }; 19 | } 20 | 21 | /** 22 | * @param {{dimensions: ?number}} [setting] - Mosaic width 23 | */ 24 | start(setting) { 25 | const canvas = this.getCanvas(); 26 | 27 | canvas.defaultCursor = 'crosshair'; 28 | canvas.selection = false; 29 | 30 | setting = setting || {}; 31 | this._dimensions = parseInt(setting.dimensions, 10) || this._dimensions; 32 | 33 | canvas.forEachObject((obj) => { 34 | obj.set({ 35 | evented: false 36 | }); 37 | }); 38 | 39 | canvas.on({ 40 | 'mouse:down': this._listeners.mousedown 41 | }); 42 | } 43 | 44 | end() { 45 | const canvas = this.getCanvas(); 46 | 47 | canvas.defaultCursor = 'default'; 48 | canvas.selection = false; 49 | 50 | canvas.forEachObject((obj) => { 51 | obj.set({ 52 | evented: true 53 | }); 54 | }); 55 | canvas.off('mouse:down', this._listeners.mousedown); 56 | } 57 | 58 | _onFabricMouseDown() { 59 | const canvas = this.getCanvas(); 60 | let lowerCanvas = canvas.getElement(); 61 | let mosaicLayer = (this.mosaicLayer = lowerCanvas.cloneNode(true)); 62 | mosaicLayer.classList.remove('lower-canvas'); 63 | mosaicLayer.classList.add('mosaic-canvas'); 64 | this.mosaicArr = []; 65 | lowerCanvas.insertAdjacentElement('afterend', mosaicLayer); 66 | canvas.on({ 67 | 'mouse:move': this._listeners.mousemove, 68 | 'mouse:up': this._listeners.mouseup 69 | }); 70 | } 71 | 72 | _onFabricMouseMove(fEvent) { 73 | let ratio = this.getCanvasRatio(); 74 | ratio = Math.ceil(ratio); 75 | let dimensions = this._dimensions * ratio; 76 | const canvas = this.getCanvas(); 77 | const pointer = canvas.getPointer(fEvent.e); 78 | let imageData = canvas.contextContainer.getImageData( 79 | parseInt(pointer.x, 10), 80 | parseInt(pointer.y, 10), 81 | dimensions, 82 | dimensions 83 | ); 84 | // let imageData = canvas.getContext().getImageData(parseInt(pointer.x), parseInt(pointer.y), this._dimensions, this._dimensions); 85 | let rgba = [0, 0, 0, 0]; 86 | let length = imageData.data.length / 4; 87 | for (let i = 0; i < length; i++) { 88 | rgba[0] += imageData.data[i * 4]; 89 | rgba[1] += imageData.data[i * 4 + 1]; 90 | rgba[2] += imageData.data[i * 4 + 2]; 91 | rgba[3] += imageData.data[i * 4 + 3]; 92 | } 93 | let mosaicRect = { 94 | left: pointer.x, 95 | top: pointer.y, 96 | fill: `rgb(${Number.parseInt(rgba[0] / length, 10)},${Number.parseInt( 97 | rgba[1] / length, 98 | 10 99 | )},${Number.parseInt(rgba[2] / length, 10)})`, 100 | dimensions 101 | }; 102 | this.mosaicArr.push(mosaicRect); 103 | let ctx = this.mosaicLayer.getContext('2d'); 104 | ctx.fillStyle = mosaicRect.fill; 105 | ctx.fillRect(mosaicRect.left, mosaicRect.top, mosaicRect.dimensions, mosaicRect.dimensions); 106 | } 107 | 108 | _onFabricMouseUp() { 109 | const canvas = this.getCanvas(); 110 | if (this.mosaicArr && this.mosaicArr.length > 0) { 111 | let __mosaicShape = new MosaicShape({ 112 | mosaicRects: this.mosaicArr, 113 | selectable: false, 114 | left: 0, 115 | top: 0, 116 | originX: 'center', 117 | originY: 'center' 118 | }); 119 | canvas.add(__mosaicShape); 120 | canvas.renderAll(); 121 | } 122 | if (this.mosaicLayer) { 123 | this.mosaicLayer.parentNode.removeChild(this.mosaicLayer); 124 | } 125 | this.mosaicArr = []; 126 | canvas.off({ 127 | 'mouse:move': this._listeners.mousemove, 128 | 'mouse:up': this._listeners.mouseup 129 | }); 130 | } 131 | 132 | /** 133 | * Get ratio value of canvas 134 | * @returns {number} Ratio value 135 | */ 136 | getCanvasRatio() { 137 | const canvasElement = this.getCanvasElement(); 138 | const cssWidth = parseInt(canvasElement.style.width, 10); 139 | const originWidth = canvasElement.width; 140 | const ratio = originWidth / cssWidth; 141 | return ratio; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/modules/pan.js: -------------------------------------------------------------------------------- 1 | import Base from './base'; 2 | import consts from '../consts'; 3 | 4 | export default class Pan extends Base { 5 | constructor(parent) { 6 | super(); 7 | this.setParent(parent); 8 | this.name = consts.moduleNames.PAN; 9 | this._listeners = { 10 | mousedown: this._onFabricMouseDown.bind(this), 11 | mousemove: this._onFabricMouseMove.bind(this), 12 | mouseup: this._onFabricMouseUp.bind(this) 13 | }; 14 | } 15 | 16 | start() { 17 | const canvas = this.getCanvas(); 18 | 19 | canvas.defaultCursor = 'move'; 20 | canvas.selection = false; 21 | 22 | canvas.forEachObject((obj) => { 23 | obj.set({ 24 | evented: false 25 | }); 26 | }); 27 | 28 | canvas.on({ 29 | 'mouse:down': this._listeners.mousedown 30 | }); 31 | } 32 | 33 | end() { 34 | const canvas = this.getCanvas(); 35 | 36 | canvas.defaultCursor = 'default'; 37 | canvas.selection = false; 38 | 39 | canvas.forEachObject((obj) => { 40 | obj.set({ 41 | evented: true 42 | }); 43 | }); 44 | 45 | canvas.off('mouse:down', this._listeners.mousedown); 46 | } 47 | 48 | /** 49 | * Mousedown event handler in fabric canvas 50 | * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event object 51 | * @private 52 | */ 53 | _onFabricMouseDown(fEvent) { 54 | const canvas = this.getCanvas(); 55 | this.pointer = canvas.getPointer(fEvent.e); 56 | this.$lower = canvas.lowerCanvasEl; 57 | this.$upper = canvas.upperCanvasEl; 58 | this.$wrapper = canvas.wrapperEl; 59 | this.deltaX = parseInt(window.getComputedStyle(this.$lower).left, 10); 60 | this.deltaY = parseInt(window.getComputedStyle(this.$lower).top, 10); 61 | this.deltaWidth = 62 | this.$upper.getBoundingClientRect().width - this.$wrapper.getBoundingClientRect().width; 63 | this.deltaHeight = 64 | this.$upper.getBoundingClientRect().height - 65 | this.$wrapper.getBoundingClientRect().height; 66 | canvas.on({ 67 | 'mouse:move': this._listeners.mousemove, 68 | 'mouse:up': this._listeners.mouseup 69 | }); 70 | } 71 | 72 | /** 73 | * Mousemove event handler in fabric canvas 74 | * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event object 75 | * @private 76 | */ 77 | _onFabricMouseMove(fEvent) { 78 | // go out of use because of transform opver 79 | // var delta = new fabric.Point(fEvent.e.movementX, fEvent.e.movementY); 80 | // canvas.relativePan(delta); 81 | 82 | //safari9 not work for movement event 83 | // let deltaX = this.deltaX + fEvent.e.movementX; 84 | // let deltaY = this.deltaY + fEvent.e.movementY; 85 | const canvas = this.getCanvas(); 86 | const movePointer = canvas.getPointer(fEvent.e); 87 | 88 | let deltaX = this.deltaX + movePointer.x - this.pointer.x; 89 | let deltaY = this.deltaY + movePointer.y - this.pointer.y; 90 | 91 | if (this.deltaWidth > Math.abs(deltaX) && deltaX < 0) { 92 | this.$lower.style.left = deltaX; 93 | this.$upper.style.left = deltaX; 94 | this.deltaX = deltaX; 95 | } 96 | if (this.deltaHeight > Math.abs(deltaY) && deltaY < 0) { 97 | this.$lower.style.top = deltaY; 98 | this.$upper.style.top = deltaY; 99 | this.deltaY = deltaY; 100 | } 101 | } 102 | 103 | /** 104 | * Mouseup event handler in fabric canvas 105 | * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event object 106 | * @private 107 | */ 108 | _onFabricMouseUp() { 109 | const canvas = this.getCanvas(); 110 | this.pointer = null; 111 | this.$lower = null; 112 | this.$upper = null; 113 | canvas.off({ 114 | 'mouse:move': this._listeners.mousemove, 115 | 'mouse:up': this._listeners.mouseup 116 | }); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/modules/rotation.js: -------------------------------------------------------------------------------- 1 | import { fabric } from 'fabric'; 2 | import Base from './base'; 3 | import consts from '../consts'; 4 | 5 | const { rejectMessages } = consts; 6 | 7 | export default class Rotation extends Base { 8 | constructor(parent) { 9 | super(); 10 | this.setParent(parent); 11 | this.name = consts.moduleNames.ROTATION; 12 | } 13 | 14 | getCurrentAngle() { 15 | return this.getCanvasImage().angle; 16 | } 17 | 18 | /** 19 | * Set angle of the image 20 | * 21 | * Do not call "this.setImageProperties" for setting angle directly. 22 | * Before setting angle, The originX,Y of image should be set to center. 23 | * See "http://fabricjs.com/docs/fabric.Object.html#setAngle" 24 | * 25 | * @param {number} angle - Angle value 26 | * @returns {jQuery.Deferred} 27 | */ 28 | setAngle(angle) { 29 | const oldAngle = this.getCurrentAngle() % 360; // The angle is lower than 2*PI(===360 degrees) 30 | 31 | angle %= 360; 32 | if (angle === oldAngle) { 33 | return Promise.reject(rejectMessages.rotation); 34 | } 35 | const canvasImage = this.getCanvasImage(); 36 | const oldImageCenter = canvasImage.getCenterPoint(); 37 | canvasImage.setAngle(angle).setCoords(); 38 | this.adjustCanvasDimension(); 39 | const newImageCenter = canvasImage.getCenterPoint(); 40 | this._rotateForEachObject(oldImageCenter, newImageCenter, angle - oldAngle); 41 | 42 | return Promise.resolve(angle); 43 | } 44 | 45 | /** 46 | * Rotate for each object 47 | * @param {fabric.Point} oldImageCenter - Image center point before rotation 48 | * @param {fabric.Point} newImageCenter - Image center point after rotation 49 | * @param {number} angleDiff - Image angle difference after rotation 50 | * @private 51 | */ 52 | _rotateForEachObject(oldImageCenter, newImageCenter, angleDiff) { 53 | const canvas = this.getCanvas(); 54 | const centerDiff = { 55 | x: oldImageCenter.x - newImageCenter.x, 56 | y: oldImageCenter.y - newImageCenter.y 57 | }; 58 | 59 | canvas.forEachObject((obj) => { 60 | const objCenter = obj.getCenterPoint(); 61 | const radian = fabric.util.degreesToRadians(angleDiff); 62 | const newObjCenter = fabric.util.rotatePoint(objCenter, oldImageCenter, radian); 63 | 64 | obj.set({ 65 | left: newObjCenter.x - centerDiff.x, 66 | top: newObjCenter.y - centerDiff.y, 67 | angle: (obj.angle + angleDiff) % 360 68 | }); 69 | obj.setCoords(); 70 | }); 71 | canvas.renderAll(); 72 | } 73 | 74 | /** 75 | * Rotate the image 76 | * @param {number} additionalAngle - Additional angle 77 | * @returns {jQuery.Deferred} 78 | */ 79 | rotate(additionalAngle) { 80 | const current = this.getCurrentAngle(); 81 | 82 | return this.setAngle(current + additionalAngle); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/modules/shape.js: -------------------------------------------------------------------------------- 1 | import { fabric } from 'fabric'; 2 | 3 | import Base from './base'; 4 | import consts from '../consts'; 5 | import util from '../lib/util'; 6 | 7 | import resizeHelper from '../lib/shape-resize-helper.js'; 8 | 9 | const { inArray, extend } = util; 10 | 11 | const KEY_CODES = consts.keyCodes; 12 | const DEFAULT_TYPE = 'rect'; 13 | const DEFAULT_OPTIONS = { 14 | strokeWidth: 1, 15 | stroke: '#000000', 16 | fill: '#ffffff', 17 | width: 1, 18 | height: 1, 19 | rx: 0, 20 | ry: 0, 21 | lockSkewingX: true, 22 | lockSkewingY: true, 23 | lockUniScaling: false, 24 | bringForward: true, 25 | isRegular: false 26 | }; 27 | 28 | const shapeType = ['rect', 'circle', 'triangle']; 29 | 30 | export default class Shape extends Base { 31 | constructor(parent) { 32 | super(); 33 | this.setParent(parent); 34 | this.name = consts.moduleNames.SHAPE; 35 | this._shapeObj = null; 36 | 37 | this._type = DEFAULT_TYPE; 38 | 39 | /** 40 | * Options to draw the shape 41 | */ 42 | this._options = DEFAULT_OPTIONS; 43 | 44 | /** 45 | * Whether the shape object is selected or not 46 | */ 47 | this._isSelected = false; 48 | 49 | /** 50 | * Pointer for drawing shape (x, y) 51 | */ 52 | this._startPoint = {}; 53 | 54 | /** 55 | * Using shortcut on drawing shape 56 | */ 57 | this._withShiftKey = false; 58 | 59 | this._handlers = { 60 | mousedown: this._onFabricMouseDown.bind(this), 61 | mousemove: this._onFabricMouseMove.bind(this), 62 | mouseup: this._onFabricMouseUp.bind(this), 63 | keydown: this._onKeyDown.bind(this), 64 | keyup: this._onKeyUp.bind(this) 65 | }; 66 | } 67 | 68 | /** 69 | * Start to draw the shape on canvas 70 | */ 71 | startDrawingMode() { 72 | const canvas = this.getCanvas(); 73 | 74 | this._isSelected = false; 75 | 76 | canvas.defaultCursor = 'crosshair'; 77 | canvas.selection = false; 78 | canvas.uniScaleTransform = true; 79 | canvas.on({ 80 | 'mouse:down': this._handlers.mousedown 81 | }); 82 | 83 | fabric.util.addListener(document, 'keydown', this._handlers.keydown); 84 | fabric.util.addListener(document, 'keyup', this._handlers.keyup); 85 | } 86 | 87 | /** 88 | * End to draw the shape on canvas 89 | */ 90 | endDrawingMode() { 91 | const canvas = this.getCanvas(); 92 | 93 | this._isSelected = false; 94 | 95 | canvas.defaultCursor = 'default'; 96 | canvas.selection = false; 97 | canvas.uniScaleTransform = false; 98 | canvas.off({ 99 | 'mouse:down': this._handlers.mousedown 100 | }); 101 | 102 | fabric.util.removeListener(document, 'keydown', this._handlers.keydown); 103 | fabric.util.removeListener(document, 'keyup', this._handlers.keyup); 104 | } 105 | 106 | /** 107 | * Set states of the current drawing shape 108 | * @param {string} type - Shape type (ex: 'rect', 'circle') 109 | * @param {object} [options] - Shape options 110 | * @param {string} [options.fill] - Shape foreground color (ex: '#fff', 'transparent') 111 | * @param {string} [options.stoke] - Shape outline color 112 | * @param {number} [options.strokeWidth] - Shape outline width 113 | * @param {number} [options.width] - Width value (When type option is 'rect', this options can use) 114 | * @param {number} [options.height] - Height value (When type option is 'rect', this options can use) 115 | * @param {number} [options.rx] - Radius x value (When type option is 'circle', this options can use) 116 | * @param {number} [options.ry] - Radius y value (When type option is 'circle', this options can use) 117 | */ 118 | setStates(type, options) { 119 | this._type = type; 120 | 121 | if (options) { 122 | this._options = Object.assign(this._options, options); 123 | } 124 | } 125 | 126 | /** 127 | * Add the shape 128 | * @param {string} type - Shape type (ex: 'rect', 'circle') 129 | * @param {object} options - Shape options 130 | * @param {string} [options.fill] - Shape foreground color (ex: '#fff', 'transparent') 131 | * @param {string} [options.stroke] - Shape outline color 132 | * @param {number} [options.strokeWidth] - Shape outline width 133 | * @param {number} [options.width] - Width value (When type option is 'rect', this options can use) 134 | * @param {number} [options.height] - Height value (When type option is 'rect', this options can use) 135 | * @param {number} [options.rx] - Radius x value (When type option is 'circle', this options can use) 136 | * @param {number} [options.ry] - Radius y value (When type option is 'circle', this options can use) 137 | * @param {number} [options.isRegular] - Whether scaling shape has 1:1 ratio or not 138 | */ 139 | add(type, options) { 140 | const canvas = this.getCanvas(); 141 | options = this._createOptions(options); 142 | const shapeObj = this._createInstance(type, options); 143 | 144 | this._bindEventOnShape(shapeObj); 145 | 146 | canvas.add(shapeObj); 147 | } 148 | 149 | /** 150 | * Change the shape 151 | * @param {fabric.Object} shapeObj - Selected shape object on canvas 152 | * @param {object} options - Shape options 153 | * @param {string} [options.fill] - Shape foreground color (ex: '#fff', 'transparent') 154 | * @param {string} [options.stroke] - Shape outline color 155 | * @param {number} [options.strokeWidth] - Shape outline width 156 | * @param {number} [options.width] - Width value (When type option is 'rect', this options can use) 157 | * @param {number} [options.height] - Height value (When type option is 'rect', this options can use) 158 | * @param {number} [options.rx] - Radius x value (When type option is 'circle', this options can use) 159 | * @param {number} [options.ry] - Radius y value (When type option is 'circle', this options can use) 160 | * @param {number} [options.isRegular] - Whether scaling shape has 1:1 ratio or not 161 | */ 162 | change(shapeObj, options) { 163 | if (inArray(shapeObj.get('type'), shapeType) < 0) { 164 | return; 165 | } 166 | 167 | shapeObj.set(options); 168 | this.getCanvas().renderAll(); 169 | } 170 | 171 | /** 172 | * Create the instance of shape 173 | * @param {string} type - Shape type 174 | * @param {object} options - Options to creat the shape 175 | * @returns {fabric.Object} Shape instance 176 | */ 177 | _createInstance(type, options) { 178 | let instance; 179 | 180 | switch (type) { 181 | case 'rect': 182 | instance = new fabric.Rect(options); 183 | break; 184 | case 'circle': 185 | instance = new fabric.Ellipse( 186 | extend( 187 | { 188 | type: 'circle' 189 | }, 190 | options 191 | ) 192 | ); 193 | break; 194 | case 'triangle': 195 | instance = new fabric.Triangle(options); 196 | break; 197 | default: 198 | instance = {}; 199 | } 200 | 201 | return instance; 202 | } 203 | 204 | /** 205 | * Get the options to create the shape 206 | * @param {object} options - Options to creat the shape 207 | * @returns {object} Shape options 208 | */ 209 | _createOptions(options) { 210 | const selectionStyles = consts.fObjectOptions.SELECTION_STYLE; 211 | 212 | options = Object.assign({}, DEFAULT_OPTIONS, selectionStyles, options); 213 | 214 | if (options.isRegular) { 215 | options.lockUniScaling = true; 216 | } 217 | 218 | return options; 219 | } 220 | 221 | /** 222 | * Bind fabric events on the creating shape object 223 | * @param {fabric.Object} shapeObj - Shape object 224 | */ 225 | _bindEventOnShape(shapeObj) { 226 | const self = this; 227 | const canvas = this.getCanvas(); 228 | 229 | shapeObj.on({ 230 | added() { 231 | self._shapeObj = this; 232 | resizeHelper.setOrigins(self._shapeObj); 233 | }, 234 | selected() { 235 | self._isSelected = true; 236 | self._shapeObj = this; 237 | canvas.uniScaleTransform = true; 238 | canvas.defaultCursor = 'default'; 239 | resizeHelper.setOrigins(self._shapeObj); 240 | }, 241 | deselected() { 242 | self._isSelected = false; 243 | self._shapeObj = null; 244 | canvas.defaultCursor = 'crosshair'; 245 | canvas.uniScaleTransform = false; 246 | }, 247 | modified() { 248 | const currentObj = self._shapeObj; 249 | 250 | resizeHelper.adjustOriginToCenter(currentObj); 251 | resizeHelper.setOrigins(currentObj); 252 | }, 253 | scaling(fEvent) { 254 | const pointer = canvas.getPointer(fEvent.e); 255 | const currentObj = self._shapeObj; 256 | 257 | canvas.setCursor('crosshair'); 258 | resizeHelper.resize(currentObj, pointer, true); 259 | } 260 | }); 261 | } 262 | 263 | /** 264 | * MouseDown event handler on canvas 265 | * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event object 266 | */ 267 | _onFabricMouseDown(fEvent) { 268 | if (!this._isSelected && !this._shapeObj) { 269 | const canvas = this.getCanvas(); 270 | this._startPoint = canvas.getPointer(fEvent.e); 271 | 272 | canvas.on({ 273 | 'mouse:move': this._handlers.mousemove, 274 | 'mouse:up': this._handlers.mouseup 275 | }); 276 | } 277 | } 278 | 279 | /** 280 | * MouseDown event handler on canvas 281 | * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event object 282 | */ 283 | _onFabricMouseMove(fEvent) { 284 | const canvas = this.getCanvas(); 285 | const pointer = canvas.getPointer(fEvent.e); 286 | const startPointX = this._startPoint.x; 287 | const startPointY = this._startPoint.y; 288 | const width = startPointX - pointer.x; 289 | const height = startPointY - pointer.y; 290 | const shape = this._shapeObj; 291 | 292 | if (!shape) { 293 | this.add(this._type, { 294 | left: startPointX, 295 | top: startPointY, 296 | width, 297 | height 298 | }); 299 | } else { 300 | this._shapeObj.set({ 301 | isRegular: this._withShiftKey 302 | }); 303 | resizeHelper.resize(shape, pointer); 304 | canvas.renderAll(); 305 | } 306 | } 307 | 308 | /** 309 | * MouseUp event handler on canvas 310 | */ 311 | _onFabricMouseUp() { 312 | const canvas = this.getCanvas(); 313 | const shape = this._shapeObj; 314 | 315 | if (shape) { 316 | resizeHelper.adjustOriginToCenter(shape); 317 | } 318 | 319 | this._shapeObj = null; 320 | 321 | canvas.off({ 322 | 'mouse:move': this._handlers.mousemove, 323 | 'mouse:up': this._handlers.mouseup 324 | }); 325 | } 326 | 327 | /** 328 | * Keydown event handler on document 329 | * @param {KeyboardEvent} e - Event object 330 | */ 331 | _onKeyDown(e) { 332 | if (e.keyCode === KEY_CODES.SHIFT) { 333 | this._withShiftKey = true; 334 | 335 | if (this._shapeObj) { 336 | this._shapeObj.isRegular = true; 337 | } 338 | } 339 | } 340 | 341 | /** 342 | * Keyup event handler on document 343 | * @param {KeyboardEvent} e - Event object 344 | */ 345 | _onKeyUp(e) { 346 | if (e.keyCode === KEY_CODES.SHIFT) { 347 | this._withShiftKey = false; 348 | 349 | if (this._shapeObj) { 350 | this._shapeObj.isRegular = false; 351 | } 352 | } 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /src/shape/arrow.js: -------------------------------------------------------------------------------- 1 | import { fabric } from 'fabric'; 2 | 3 | const Arrow = fabric.util.createClass(fabric.Path, { 4 | /** 5 | * Constructor 6 | * @param {Object} options Options object 7 | * @override 8 | */ 9 | initialize(options) { 10 | options.type = 'arrow'; 11 | this.callSuper('initialize', options); 12 | this.on({ 13 | moving: this._onMoving, 14 | scaling: this._onScaling 15 | }); 16 | }, 17 | objectCaching: false, 18 | 19 | /** 20 | * Render Crop-zone 21 | * @param {CanvasRenderingContext2D} ctx - Context 22 | * @private 23 | * @override 24 | */ 25 | _render(ctx) { 26 | const cropzoneDashLineWidth = 7; 27 | const cropzoneDashLineOffset = 7; 28 | this.callSuper('_render', ctx); 29 | 30 | // Calc original scale 31 | const originalFlipX = this.flipX ? -1 : 1; 32 | const originalFlipY = this.flipY ? -1 : 1; 33 | const originalScaleX = originalFlipX / this.scaleX; 34 | const originalScaleY = originalFlipY / this.scaleY; 35 | 36 | // Set original scale 37 | ctx.scale(originalScaleX, originalScaleY); 38 | 39 | // Render outer rect 40 | this._fillOuterRect(ctx, 'rgba(0, 0, 0, 0.55)'); 41 | 42 | // Black dash line 43 | this._strokeBorder(ctx, 'rgb(0, 0, 0)', cropzoneDashLineWidth); 44 | 45 | // White dash line 46 | this._strokeBorder( 47 | ctx, 48 | 'rgb(255, 255, 255)', 49 | cropzoneDashLineWidth, 50 | cropzoneDashLineOffset 51 | ); 52 | 53 | // Reset scale 54 | ctx.scale(1 / originalScaleX, 1 / originalScaleY); 55 | }, 56 | 57 | drawControls(ctx) { 58 | if (!this.hasControls) { 59 | return this; 60 | } 61 | var wh = this._calculateCurrentDimensions(), 62 | width = wh.x, 63 | height = wh.y, 64 | scaleOffset = this.cornerSize, 65 | left = -(width + scaleOffset) / 2, 66 | top = -(height + scaleOffset) / 2, 67 | methodName = this.transparentCorners ? 'stroke' : 'fill'; 68 | ctx.save(); 69 | ctx.strokeStyle = ctx.fillStyle = this.cornerColor; 70 | if (!this.transparentCorners) { 71 | ctx.strokeStyle = this.cornerStrokeColor; 72 | } 73 | this._setLineDash(ctx, this.cornerDashArray, null); 74 | // top-left 75 | this._drawControl('tl', ctx, methodName, left, top); 76 | // top-right 77 | this._drawControl('tr', ctx, methodName, left + width, top); 78 | // bottom-left 79 | this._drawControl('bl', ctx, methodName, left, top + height); 80 | // bottom-right 81 | this._drawControl('br', ctx, methodName, left + width, top + height); 82 | if (!this.get('lockUniScaling')) { 83 | // middle-top 84 | this._drawControl('mt', ctx, methodName, left + width / 2, top); 85 | // middle-bottom 86 | this._drawControl('mb', ctx, methodName, left + width / 2, top + height); 87 | // middle-right 88 | this._drawControl('mr', ctx, methodName, left + width, top + height / 2); 89 | // middle-left 90 | this._drawControl('ml', ctx, methodName, left, top + height / 2); 91 | } 92 | // middle-top-rotate 93 | if (this.hasRotatingPoint) { 94 | this._drawControl( 95 | 'mtr', 96 | ctx, 97 | methodName, 98 | left + width / 2, 99 | top - this.rotatingPointOffset 100 | ); 101 | } 102 | ctx.restore(); 103 | } 104 | }); 105 | export default Arrow; 106 | -------------------------------------------------------------------------------- /src/shape/mosaic.js: -------------------------------------------------------------------------------- 1 | import { fabric } from 'fabric'; 2 | // var cacheProperties = fabric.Object.prototype.cacheProperties.concat(); 3 | // cacheProperties.push('_mosaicRects'); 4 | const Mosaic = fabric.util.createClass(fabric.Object, { 5 | type: 'mosaic', 6 | // statefullCache:true, 7 | // cacheProperties:cacheProperties, 8 | // objectCaching: true, 9 | objectCaching: false, 10 | 11 | initialize: function(options) { 12 | options || (options = {}); 13 | 14 | this._minPoint = { left: 0, top: 0 }; 15 | 16 | this._maxPoint = { left: 0, top: 0 }; 17 | 18 | this._mosaicRects = []; 19 | 20 | this.callSuper('initialize', options); 21 | 22 | this.addMosicRectWithUpdate(options.mosaicRects || []); 23 | }, 24 | 25 | toObject: function() { 26 | return fabric.util.object.extend(this.callSuper('toObject'), { 27 | _mosaicRects: this.get('_mosaicRects') 28 | }); 29 | }, 30 | 31 | _render: function(ctx) { 32 | this._mosaicRects.forEach((item, i) => { 33 | ctx.fillStyle = item.fill; 34 | ctx.fillRect(item.left, item.top, item.dimensions, item.dimensions); 35 | }); 36 | }, 37 | 38 | addMosaicRect: function(objects) { 39 | objects.forEach((object) => { 40 | if (object.left < this._minPoint.left || object.top < this._minPoint.top) { 41 | this._minPoint = { 42 | left: object.left, 43 | top: object.top 44 | }; 45 | } 46 | if (object.left > this._maxPoint.left || object.top > this._maxPoint.top) { 47 | this._maxPoint = { 48 | left: object.left, 49 | top: object.top 50 | }; 51 | } 52 | this._mosaicRects.push({ 53 | left: object.left, 54 | top: object.top, 55 | dimensions: object.dimensions, 56 | fill: object.fill 57 | }); 58 | }); 59 | }, 60 | 61 | addMosicRectWithUpdate: function(objects) { 62 | this.addMosaicRect(objects); 63 | this.set({ 64 | width: this._maxPoint.left - this._minPoint.left, 65 | height: this._maxPoint.top - this._minPoint.top, 66 | left: this._minPoint.left, 67 | top: this._minPoint.top, 68 | selectable: false 69 | }); 70 | } 71 | }); 72 | export default Mosaic; 73 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path'); 2 | const webpack = require('webpack'); 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | entry: { 7 | index: ['./demo/index.js'] 8 | }, 9 | output: { 10 | filename: '[name].js', 11 | sourceMapFilename: '[file].map', 12 | path: resolve(__dirname, 'public'), 13 | publicPath: '/public' 14 | }, 15 | devtool: 'cheap-module-eval-source-map', 16 | 17 | devServer: { 18 | contentBase: [path.join(__dirname, 'html'), path.join(__dirname, 'public')], 19 | compress: true, 20 | port: parseInt(process.env.PORT, 10) || 9876, 21 | host: '0.0.0.0', 22 | hot: true, 23 | inline: true, 24 | publicPath: '/dist/', 25 | historyApiFallback: { 26 | rewrites: [ 27 | { 28 | from: /^\/$/, 29 | to: '/html/index.html' 30 | } 31 | ] 32 | }, 33 | watchContentBase: true 34 | }, 35 | performance: { 36 | hints: false 37 | }, 38 | module: { 39 | rules: [ 40 | { 41 | test: /\.js$/, 42 | use: [ 43 | { 44 | loader: 'babel-loader' 45 | } 46 | ], 47 | exclude: [/node_modules/] 48 | }, 49 | { 50 | test: /\.css$/, 51 | use: ['style-loader', 'css-loader', 'postcss-loader'] 52 | }, 53 | { 54 | test: /\.scss$/, 55 | use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'] 56 | }, 57 | { 58 | test: /\.(png|jpg|jpeg|gif|woff|svg|eot|ttf|woff2)$/i, 59 | use: ['url-loader'] 60 | } 61 | ] 62 | }, 63 | externals: { 64 | jquery: 'jQuery', 65 | lodash: '_' 66 | }, 67 | plugins: [new webpack.HotModuleReplacementPlugin()] 68 | }; 69 | -------------------------------------------------------------------------------- /website/.fatherrc.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | esm: 'rollup', 3 | cjs: 'rollup', 4 | }; 5 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /npm-debug.log* 6 | /yarn-error.log 7 | /yarn.lock 8 | /package-lock.json 9 | 10 | # production 11 | /dist 12 | /docs-dist 13 | 14 | # misc 15 | .DS_Store 16 | 17 | # umi 18 | .umi 19 | .umi-production 20 | .umi-test 21 | .env.local 22 | 23 | # ide 24 | /.vscode 25 | /.idea 26 | -------------------------------------------------------------------------------- /website/.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npm.taobao.org/ 2 | disturl=https://npm.taobao.org/dist 3 | -------------------------------------------------------------------------------- /website/.umirc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'dumi'; 2 | 3 | export default defineConfig({ 4 | title: 'fabric-photo', 5 | favicon: 6 | 'https://user-images.githubusercontent.com/9554297/83762004-a0761b00-a6a9-11ea-83b4-9c8ff721d4b8.png', 7 | logo: 8 | 'https://user-images.githubusercontent.com/9554297/83762004-a0761b00-a6a9-11ea-83b4-9c8ff721d4b8.png', 9 | outputPath: 'dist', 10 | hash: process.env.NODE_ENV !== 'development', 11 | base: '/fabric-photo', 12 | publicPath: 13 | process.env.NODE_ENV !== 'development' ? 'https://ximing.github.io/fabric-photo/' : '/', 14 | exportStatic: {} 15 | 16 | // more config: https://d.umijs.org/config 17 | }); 18 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # website 2 | 3 | ## Getting Started 4 | 5 | Install dependencies, 6 | 7 | ```bash 8 | $ npm i 9 | ``` 10 | 11 | Start the dev server, 12 | 13 | ```bash 14 | $ npm start 15 | ``` 16 | 17 | Build documentation, 18 | 19 | ```bash 20 | $ npm run docs:build 21 | ``` 22 | 23 | Build library via `father-build`, 24 | 25 | ```bash 26 | $ npm run build 27 | ``` 28 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "website", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "start": "dumi dev", 7 | "docs:build": "dumi build", 8 | "docs:deploy": "gh-pages -d dist", 9 | "build": "father-build", 10 | "deploy": "npm run docs:build && npm run docs:deploy", 11 | "release": "npm run build && npm publish", 12 | "prettier": "prettier --write \"**/*.{js,jsx,tsx,ts,less,md,json}\"", 13 | "test": "umi-test", 14 | "test:coverage": "umi-test --coverage" 15 | }, 16 | "main": "dist/index.js", 17 | "module": "dist/index.esm.js", 18 | "typings": "dist/index.d.ts", 19 | "dependencies": { 20 | "react": "^16.12.0" 21 | }, 22 | "devDependencies": { 23 | "@umijs/plugin-sass": "^1.1.1", 24 | "@umijs/test": "^3.0.5", 25 | "dumi": "^1.0.13", 26 | "father-build": "^1.17.2", 27 | "gh-pages": "^3.0.0", 28 | "yorkie": "^2.0.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /website/public/images/demo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ximing/fabric-photo/5e59eff9a37166f7ce14eed250c00329b444a091/website/public/images/demo.jpeg -------------------------------------------------------------------------------- /website/src/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ximing/fabric-photo/5e59eff9a37166f7ce14eed250c00329b444a091/website/src/index.ts -------------------------------------------------------------------------------- /website/src/scss/iconfont.scss: -------------------------------------------------------------------------------- 1 | // font-face 2 | // @icon-url: 字体源文件的地址 3 | @font-face { 4 | 5 | 6 | font-family: 'dxicon'; 7 | 8 | src: url('#{$icon-url}.eot'); /* IE9*/ 9 | src: url('#{$icon-url}.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 10 | url('#{$icon-url}.woff') format('woff'), /* chrome、firefox */ 11 | url('#{$icon-url}.ttf') format('truetype'), /* chrome、firefox、opera、Safari, Android, iOS 4.2+*/ 12 | url('#{$icon-url}.svg#dxicon') format('svg'); /* iOS 4.1- */ 13 | 14 | } 15 | 16 | .#{$iconfont-css-prefix} { 17 | @include iconfont-mixin; 18 | -webkit-font-smoothing: antialiased; 19 | -moz-osx-font-smoothing: grayscale; 20 | } 21 | 22 | .#{$iconfont-css-prefix}-setting:before { 23 | content: "\e654"; 24 | } 25 | 26 | .#{$iconfont-css-prefix}-group-setting:before { 27 | content: "\e676"; 28 | } 29 | 30 | .#{$iconfont-css-prefix}-todo:before { 31 | content: "\e62d"; 32 | } 33 | 34 | .#{$iconfont-css-prefix}-right:before { 35 | content: "\e678"; 36 | } 37 | 38 | .#{$iconfont-css-prefix}-news:before { 39 | content: "\e657"; 40 | } 41 | 42 | .#{$iconfont-css-prefix}-contact:before { 43 | content: "\e651"; 44 | } 45 | 46 | .#{$iconfont-css-prefix}-group:before { 47 | content: "\e655"; 48 | } 49 | 50 | .#{$iconfont-css-prefix}-group1:before { 51 | content: "\e675"; 52 | } 53 | 54 | .#{$iconfont-css-prefix}-group2:before { 55 | content: "\e68c"; 56 | } 57 | 58 | .#{$iconfont-css-prefix}-addchat:before { 59 | content: "\e686"; 60 | } 61 | 62 | .#{$iconfont-css-prefix}-addapp:before { 63 | content: "\e602"; 64 | } 65 | 66 | .#{$iconfont-css-prefix}-xiangshang:before { 67 | content: "\e62c"; 68 | } 69 | 70 | .#{$iconfont-css-prefix}-arrow-down:before { 71 | content: "\e66b"; 72 | } 73 | 74 | //实心向下尖头 75 | 76 | .#{$iconfont-css-prefix}-arrow-right:before { 77 | content: "\e66a"; 78 | } 79 | 80 | .#{$iconfont-css-prefix}-mute:before { 81 | content: "\e670"; 82 | } 83 | 84 | .#{$iconfont-css-prefix}-round_close:before { 85 | content: "\e673"; 86 | } 87 | 88 | //缺失 89 | .#{$iconfont-css-prefix}-search:before { 90 | content: "\e66e"; 91 | } 92 | 93 | .#{$iconfont-css-prefix}-unrecognized:before { 94 | content: "\e66f"; 95 | } 96 | 97 | .#{$iconfont-css-prefix}-zhankai:before { 98 | content: "\e656"; 99 | } 100 | 101 | .#{$iconfont-css-prefix}-search-close:before { 102 | content: "\e679"; 103 | } 104 | 105 | .#{$iconfont-css-prefix}-pull_right:before { 106 | content: "\e674"; 107 | } 108 | 109 | .#{$iconfont-css-prefix}-message:before { 110 | content: "\e690"; 111 | } 112 | 113 | .#{$iconfont-css-prefix}-checkbox:before { 114 | content: "\e668"; 115 | } 116 | 117 | .#{$iconfont-css-prefix}-checkbox-checked:before { 118 | content: "\e669"; 119 | } 120 | 121 | .#{$iconfont-css-prefix}-radio_box:before { 122 | content: "\e604"; 123 | } 124 | 125 | .#{$iconfont-css-prefix}-radio_box_fill:before { 126 | content: "\e605"; 127 | } 128 | 129 | .#{$iconfont-css-prefix}-gender-woman:before { 130 | content: "\e66d"; 131 | } 132 | 133 | .#{$iconfont-css-prefix}-gender-man:before { 134 | content: "\e66c"; 135 | } 136 | 137 | .#{$iconfont-css-prefix}-star-full:before { 138 | content: "\e680"; 139 | } 140 | 141 | .#{$iconfont-css-prefix}-star:before { 142 | content: "\e67f"; 143 | } 144 | 145 | .#{$iconfont-css-prefix}-folder:before { 146 | content: "\e67e"; 147 | } 148 | 149 | .#{$iconfont-css-prefix}-sendfile:before { 150 | content: "\e61b"; 151 | } 152 | 153 | .#{$iconfont-css-prefix}-screenshot:before { 154 | content: "\e62b"; 155 | } 156 | 157 | .#{$iconfont-css-prefix}-unfold:before { 158 | content: "\e67b"; 159 | } 160 | 161 | .#{$iconfont-css-prefix}-emoji:before { 162 | content: "\e610"; 163 | } 164 | 165 | .#{$iconfont-css-prefix}-plus:before { 166 | content: "\e691"; 167 | } 168 | 169 | .#{$iconfont-css-prefix}-post:before { 170 | content: "\e600"; 171 | } 172 | 173 | // 174 | .#{$iconfont-css-prefix}-more:before { 175 | content: "\e652"; 176 | } 177 | 178 | .#{$iconfont-css-prefix}-file:before { 179 | content: "\e650"; 180 | } 181 | 182 | .#{$iconfont-css-prefix}-kefu:before { 183 | content: "\e653"; 184 | } 185 | 186 | 187 | //toastr 使用的 188 | .#{$iconfont-css-prefix}-error:before { 189 | content: "\e693"; 190 | } 191 | 192 | .#{$iconfont-css-prefix}-confirm:before { 193 | content: "\e684"; 194 | } 195 | 196 | //app 自定义配置 197 | .#{$iconfont-css-prefix}-confirm-fill:before { 198 | content: "\e603"; 199 | } 200 | 201 | .#{$iconfont-css-prefix}-remove-fill:before { 202 | content: "\e601"; 203 | } 204 | 205 | .#{$iconfont-css-prefix}-left_voice_0:before { 206 | content: "\e681"; 207 | } 208 | 209 | .#{$iconfont-css-prefix}-left_voice_1:before { 210 | content: "\e682"; 211 | } 212 | 213 | .#{$iconfont-css-prefix}-left_voice_2:before { 214 | content: "\e683"; 215 | } 216 | 217 | .#{$iconfont-css-prefix}-right_voice_0:before { 218 | content: "\e60b"; 219 | } 220 | 221 | .#{$iconfont-css-prefix}-right_voice_1:before { 222 | content: "\e60c"; 223 | } 224 | 225 | .#{$iconfont-css-prefix}-right_voice_2:before { 226 | content: "\e60d"; 227 | } 228 | 229 | .#{$iconfont-css-prefix}-remove:before { 230 | content: "\e673"; 231 | } 232 | 233 | .#{$iconfont-css-prefix}-warning:before { 234 | content: "\e672"; 235 | } 236 | 237 | .#{$iconfont-css-prefix}-remove-group:before { 238 | content: "\e694"; 239 | } 240 | 241 | .#{$iconfont-css-prefix}-round_down:before { 242 | content: "\e687"; 243 | } 244 | 245 | .#{$iconfont-css-prefix}-quit:before { 246 | content: "\e67a"; 247 | } 248 | 249 | .#{$iconfont-css-prefix}-crown:before { 250 | content: "\e667"; 251 | } 252 | 253 | .#{$iconfont-css-prefix}-info:before { 254 | content: "\e677"; 255 | } 256 | 257 | .#{$iconfont-css-prefix}-bubble-leader:before { 258 | content: "\e607"; 259 | } 260 | 261 | .#{$iconfont-css-prefix}-bubble-star:before { 262 | content: "\e608"; 263 | } 264 | 265 | .#{$iconfont-css-prefix}-yunpan:before { 266 | content: "\e613"; 267 | } 268 | 269 | .#{$iconfont-css-prefix}-yunpan-close:before { 270 | content: "\e60a"; 271 | } 272 | 273 | .#{$iconfont-css-prefix}-gongzongpingtai:before { 274 | content: "\e68b"; 275 | } 276 | 277 | .#{$iconfont-css-prefix}-dalaba:before { 278 | content: "\e689"; 279 | } 280 | 281 | .#{$iconfont-css-prefix}-message-fail:before { 282 | content: "\e606"; 283 | } 284 | 285 | .#{$iconfont-css-prefix}-quote:before { 286 | content: "\e627"; 287 | } 288 | 289 | .#{$iconfont-css-prefix}-chehui:before { 290 | content: "\e626"; 291 | } 292 | 293 | .#{$iconfont-css-prefix}-close_notice:before { 294 | content: "\e670"; 295 | } 296 | 297 | .#{$iconfont-css-prefix}-feedback:before { 298 | content: "\e614"; 299 | } 300 | 301 | .#{$iconfont-css-prefix}-open-qr:before { 302 | content: "\e61a"; 303 | } 304 | 305 | .#{$iconfont-css-prefix}-close-qr:before { 306 | content: "\e619"; 307 | } 308 | 309 | .#{$iconfont-css-prefix}-view-qr:before { 310 | content: "\e618"; 311 | } 312 | 313 | .#{$iconfont-css-prefix}-lfc:before { //left full corner 314 | content: "\e615"; 315 | } 316 | 317 | .#{$iconfont-css-prefix}-llc:before { 318 | content: "\e616"; 319 | } 320 | 321 | .#{$iconfont-css-prefix}-rfc:before { //left full corner 322 | content: "\e611"; 323 | } 324 | 325 | .#{$iconfont-css-prefix}-rlc:before { 326 | content: "\e617"; 327 | } 328 | .#{$iconfont-css-prefix}-i1000:before { 329 | content: "\e60e"; 330 | } 331 | 332 | .#{$iconfont-css-prefix}-survey:before { 333 | content: "\e60f"; 334 | } 335 | 336 | .#{$iconfont-css-prefix}-moremessage:before { 337 | content: "\e622"; 338 | } 339 | .#{$iconfont-css-prefix}-forward:before { 340 | content: "\e628"; 341 | } 342 | 343 | .#{$iconfont-css-prefix}-checkboxChecked:before { 344 | content: "\e620"; 345 | } 346 | .#{$iconfont-css-prefix}-checkboxUncheck:before { 347 | content: "\e61f"; 348 | } 349 | .#{$iconfont-css-prefix}-cancelChecked:before { 350 | content: "\e61e"; 351 | } 352 | 353 | .#{$iconfont-css-prefix}-tag-receipt:before { 354 | content: "\e61d"; 355 | } 356 | 357 | .#{$iconfont-css-prefix}-send-receipt:before { 358 | content: "\e61c"; 359 | } 360 | 361 | .#{$iconfont-css-prefix}-daiban:before { 362 | content: "\e625"; 363 | } 364 | .#{$iconfont-css-prefix}-backArrow:before { 365 | content: "\e623"; 366 | } 367 | .#{$iconfont-css-prefix}-update:before { 368 | content: "\e624"; 369 | } 370 | .#{$iconfont-css-prefix}-checked:before { 371 | content: "\e629"; 372 | } 373 | .#{$iconfont-css-prefix}-toastr-close:before { 374 | content: "\e62a"; 375 | } 376 | .#{$iconfont-css-prefix}-question-mark:before { 377 | content: "\e621"; 378 | } 379 | .#{$iconfont-css-prefix}-daily-qun:before { 380 | content: "\e62e"; 381 | } 382 | /*图片编辑器*/ 383 | .#{$iconfont-css-prefix}-image-fangda:before { 384 | content: "\e638"; 385 | } 386 | .#{$iconfont-css-prefix}-image-gou:before { 387 | content: "\e637"; 388 | } 389 | .#{$iconfont-css-prefix}-image-xuanzhuan:before { 390 | content: "\e636"; 391 | } 392 | .#{$iconfont-css-prefix}-image-masaike:before { 393 | content: "\e635"; 394 | } 395 | .#{$iconfont-css-prefix}-image-suoxiao:before { 396 | content: "\e634"; 397 | } 398 | .#{$iconfont-css-prefix}-image-text:before { 399 | content: "\e633"; 400 | } 401 | .#{$iconfont-css-prefix}-image-huabi:before { 402 | content: "\e632"; 403 | } 404 | .#{$iconfont-css-prefix}-image-jiantou:before { 405 | content: "\e631"; 406 | } 407 | .#{$iconfont-css-prefix}-image-guanbi:before { 408 | content: "\e630"; 409 | } 410 | .#{$iconfont-css-prefix}-image-jiancai:before { 411 | content: "\e62f"; 412 | } 413 | /* end */ 414 | -------------------------------------------------------------------------------- /website/src/scss/index.scss: -------------------------------------------------------------------------------- 1 | @import "./variable"; 2 | @import "./mixin/iconfont"; 3 | @import "./mixin/clearfix"; 4 | @import "./theme/_default"; 5 | @import "./normalize"; 6 | @import "./iconfont"; 7 | @import "./main"; 8 | -------------------------------------------------------------------------------- /website/src/scss/main.scss: -------------------------------------------------------------------------------- 1 | .wrap_inner { 2 | width: 700px; 3 | height: 500px; 4 | margin: 0 auto; 5 | display: block; 6 | position: relative; 7 | .main { 8 | height: 100%; 9 | width: 100%; 10 | margin-top:50px; 11 | .upload-file-image-preview { 12 | height: 400px; 13 | width: 700px; 14 | text-align: center; 15 | .xm-fabric-photo-editor-canvas-container { 16 | display: inline-block; 17 | } 18 | } 19 | .file-button { 20 | height: 100px; 21 | width: 100% 22 | } 23 | .file-info-progress { 24 | height: 4px; 25 | background-color: $chat-bg-color; 26 | border-radius: 2px; 27 | position: absolute; 28 | bottom: 0px; 29 | left: 0; 30 | right: 0; 31 | .file-info-progress-bar { 32 | width: 0; 33 | height: 4px; 34 | background-color: $success; 35 | border-radius: 2px; 36 | } 37 | } 38 | .file-button.upload-success { 39 | width: 100%; 40 | height: 64px; 41 | } 42 | .file-button { 43 | display: flex; 44 | flex-direction: row; 45 | justify-content: space-between; 46 | padding: 14px 20px; 47 | box-sizing: border-box; 48 | .ctn-tips { 49 | flex: 1; 50 | position: relative; 51 | padding: 18px 16px; 52 | -webkit-user-select: none; 53 | -moz-user-select: none; 54 | .text { 55 | color: $primary; 56 | cursor: pointer; 57 | } 58 | .moretips { 59 | position: absolute; 60 | top: -108px; 61 | background: rgba(0, 0, 0, 0.87); 62 | border-radius: 4px; 63 | color: white; 64 | width: 520px; 65 | padding: 13px 20px 10px 20px; 66 | ol { 67 | padding: 0 20px; 68 | li { 69 | list-style: initial; 70 | } 71 | } 72 | .close { 73 | color: white; 74 | top: 0; 75 | right: 0; 76 | .dxicon { 77 | font-style: 10px; 78 | color: rgba(255, 255, 255, 0.69); 79 | } 80 | } 81 | .arrow { 82 | position: absolute; 83 | bottom: -13px; 84 | left: 40px; 85 | .dxicon { 86 | color: rgba(0, 0, 0, 0.87); 87 | font-size: 17px; 88 | } 89 | } 90 | } 91 | } 92 | .image-thumb-btns { 93 | vertical-align: middle; 94 | font-size: 0; 95 | &:before { 96 | content: ''; 97 | display: inline-block; 98 | vertical-align: middle; 99 | font-size: 0; 100 | width: 0; 101 | height: 100%; 102 | } 103 | .thumb-divider { 104 | display: inline-block; 105 | height: 2px; 106 | border-bottom: 1px solid #eee; 107 | width: 60px; 108 | margin: 0 9px; 109 | } 110 | i { 111 | font-size: 18px; 112 | cursor: pointer; 113 | &:hover { 114 | color: $primary; 115 | } 116 | } 117 | } 118 | .image-tools-btns { 119 | vertical-align: middle; 120 | position: relative; 121 | .tools-divider { 122 | width: 1px; 123 | height: 24px; 124 | background: rgba(0,0,0,0.10); 125 | display: inline-block; 126 | vertical-align: middle; 127 | } 128 | i { 129 | font-size: 24px; 130 | margin-left: 14px; 131 | cursor: pointer; 132 | &:hover { 133 | color: $primary; 134 | } 135 | } 136 | .file-button-cancel { 137 | background: #FFFFFF; 138 | border: 1px solid rgba(0, 0, 0, 0.38); 139 | border-radius: 2px; 140 | height: 20px; 141 | width: 34px; 142 | font-size: 12px; 143 | color: rgba(0, 0, 0, 0.54); 144 | letter-spacing: 0; 145 | line-height: 18px; 146 | padding: 2px; 147 | cursor: pointer; 148 | } 149 | .tools-panel { 150 | position: absolute; 151 | top: 40px; 152 | width: 350px; 153 | height: 46px; 154 | box-sizing: border-box; 155 | background: #FFFFFF; 156 | box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.24); 157 | border-radius: 4px; 158 | padding: 8px 0; 159 | .tools-panel-brush { 160 | display: inline-block; 161 | div { 162 | height: 24px; 163 | width: 24px; 164 | display: inline-block; 165 | margin-left: 14px; 166 | text-align: center; 167 | vertical-align: middle; 168 | cursor: pointer; 169 | &:hover { 170 | span { 171 | background: rgba(0, 0, 0, 0.54); 172 | } 173 | } 174 | span { 175 | background: rgba(0, 0, 0, 0.24); 176 | &.active { 177 | background: rgba(0, 0, 0, 0.54); 178 | } 179 | } 180 | .small-brush { 181 | border-radius: 50%; 182 | width: 4px; 183 | height: 4px; 184 | display: inline-block; 185 | } 186 | .normal-brush { 187 | border-radius: 50%; 188 | width: 8px; 189 | height: 8px; 190 | display: inline-block; 191 | } 192 | .big-brush { 193 | border-radius: 50%; 194 | width: 12px; 195 | height: 12px; 196 | display: inline-block; 197 | } 198 | } 199 | } 200 | .tools-panel-color { 201 | display: inline-block; 202 | .color { 203 | border: 1px solid rgba(0, 0, 0, 0.10); 204 | border-radius: 2px; 205 | height: 16px; 206 | width: 16px; 207 | display: inline-block; 208 | margin-right: 8px; 209 | cursor: pointer; 210 | &.active { 211 | height: 22px; 212 | width: 22px; 213 | } 214 | &:hover { 215 | height: 22px; 216 | width: 22px; 217 | } 218 | &.red { 219 | background: #FF3440; 220 | } 221 | &.yellow { 222 | background: #FFCF50; 223 | } 224 | &.green { 225 | background: #00A344; 226 | } 227 | &.blue { 228 | background: #0DA9D6; 229 | } 230 | &.grey { 231 | background: #999999; 232 | } 233 | &.black { 234 | background: #000000; 235 | } 236 | &.white { 237 | background: #FFFFFF; 238 | } 239 | } 240 | } 241 | } 242 | } 243 | .ctn-btns { 244 | text-align: center; 245 | button { 246 | width: 88px; 247 | height: 36px; 248 | padding: 8px 0; 249 | } 250 | } 251 | &.upload-success { 252 | width: 100%; 253 | height: 56px; 254 | margin: 10px auto 0 auto; 255 | } 256 | } 257 | .file-button--pc { 258 | justify-content: flex-end; 259 | } 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /website/src/scss/mixin/clearfix.scss: -------------------------------------------------------------------------------- 1 | // mixins for clearfix 2 | // ------------------------ 3 | @mixin clearfix() { 4 | zoom: 1; 5 | &:before, 6 | &:after { 7 | content: " "; 8 | display: table; 9 | } 10 | &:after { 11 | clear: both; 12 | visibility: hidden; 13 | font-size: 0; 14 | height: 0; 15 | } 16 | } -------------------------------------------------------------------------------- /website/src/scss/mixin/common.scss: -------------------------------------------------------------------------------- 1 | @mixin cyclize-avatar($radius) { 2 | width: $radius * 2; 3 | border-radius: $radius; 4 | } 5 | -------------------------------------------------------------------------------- /website/src/scss/mixin/iconfont.scss: -------------------------------------------------------------------------------- 1 | @mixin iconfont-mixin { 2 | display: inline-block; 3 | font-style: normal; 4 | vertical-align: middle; 5 | text-align: center; 6 | text-transform: none; 7 | text-rendering: auto; 8 | line-height: 1; 9 | font-size: 14px; 10 | 11 | &:before { 12 | display: block; 13 | font-family: "dxicon" !important; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /website/src/scss/normalize.scss: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | // 4 | // 1. Set default font family to sans-serif. 5 | // 2. Prevent iOS and IE text size adjust after device orientation change, 6 | // without disabling user zoom. 7 | // 8 | 9 | html { 10 | font-family: sans-serif; // 1 11 | -ms-text-size-adjust: 100%; // 2 12 | -webkit-text-size-adjust: 100%; // 2 13 | } 14 | 15 | // 16 | // Remove default margin. 17 | // 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | // HTML5 display definitions 24 | // ========================================================================== 25 | 26 | // 27 | // Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | // Correct `block` display not defined for `details` or `summary` in IE 10/11 29 | // and Firefox. 30 | // Correct `block` display not defined for `main` in IE 11. 31 | // 32 | 33 | article, 34 | aside, 35 | details, 36 | figcaption, 37 | figure, 38 | footer, 39 | header, 40 | hgroup, 41 | main, 42 | menu, 43 | nav, 44 | section, 45 | summary { 46 | display: block; 47 | } 48 | 49 | // 50 | // 1. Correct `inline-block` display not defined in IE 8/9. 51 | // 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 52 | // 53 | 54 | audio, 55 | canvas, 56 | progress, 57 | video { 58 | display: inline-block; // 1 59 | vertical-align: baseline; // 2 60 | } 61 | 62 | // 63 | // Prevent modern browsers from displaying `audio` without controls. 64 | // Remove excess height in iOS 5 devices. 65 | // 66 | 67 | audio:not([controls]) { 68 | display: none; 69 | height: 0; 70 | } 71 | 72 | // 73 | // Address `[hidden]` styling not present in IE 8/9/10. 74 | // Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22. 75 | // 76 | 77 | [hidden], 78 | template { 79 | display: none; 80 | } 81 | 82 | // Links 83 | // ========================================================================== 84 | 85 | // 86 | // Remove the gray background color from active links in IE 10. 87 | // 88 | 89 | a { 90 | background-color: transparent; 91 | } 92 | 93 | // 94 | // Improve readability of focused elements when they are also in an 95 | // active/hover state. 96 | // 97 | 98 | a:active, 99 | a:hover { 100 | outline: 0; 101 | } 102 | 103 | // Text-level semantics 104 | // ========================================================================== 105 | 106 | // 107 | // Address styling not present in IE 8/9/10/11, Safari, and Chrome. 108 | // 109 | 110 | abbr[title] { 111 | border-bottom: 1px dotted; 112 | } 113 | 114 | // 115 | // Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 116 | // 117 | 118 | b, 119 | strong { 120 | font-weight: bold; 121 | } 122 | 123 | // 124 | // Address styling not present in Safari and Chrome. 125 | // 126 | 127 | dfn { 128 | font-style: italic; 129 | } 130 | 131 | // 132 | // Address variable `h1` font-size and margin within `section` and `article` 133 | // contexts in Firefox 4+, Safari, and Chrome. 134 | // 135 | 136 | h1 { 137 | font-size: 2em; 138 | margin: 0.67em 0; 139 | } 140 | 141 | // 142 | // Address styling not present in IE 8/9. 143 | // 144 | 145 | mark { 146 | background: #ff0; 147 | color: #000; 148 | } 149 | 150 | // 151 | // Address inconsistent and variable font size in all browsers. 152 | // 153 | 154 | small { 155 | font-size: 80%; 156 | } 157 | 158 | // 159 | // Prevent `sub` and `sup` affecting `line-height` in all browsers. 160 | // 161 | 162 | sub, 163 | sup { 164 | font-size: 75%; 165 | line-height: 0; 166 | position: relative; 167 | vertical-align: baseline; 168 | } 169 | 170 | sup { 171 | top: -0.5em; 172 | } 173 | 174 | sub { 175 | bottom: -0.25em; 176 | } 177 | 178 | // Embedded content 179 | // ========================================================================== 180 | 181 | // 182 | // Remove border when inside `a` element in IE 8/9/10. 183 | // 184 | 185 | img { 186 | border: 0; 187 | } 188 | 189 | // 190 | // Correct overflow not hidden in IE 9/10/11. 191 | // 192 | 193 | svg:not(:root) { 194 | overflow: hidden; 195 | } 196 | 197 | // Grouping content 198 | // ========================================================================== 199 | 200 | // 201 | // Address margin not present in IE 8/9 and Safari. 202 | // 203 | 204 | figure { 205 | margin: 1em 40px; 206 | } 207 | 208 | // 209 | // Address differences between Firefox and other browsers. 210 | // 211 | 212 | hr { 213 | box-sizing: content-box; 214 | height: 0; 215 | } 216 | 217 | // 218 | // Contain overflow in all browsers. 219 | // 220 | 221 | //pre { 222 | // overflow: auto; 223 | //} 224 | 225 | // 226 | // Address odd `em`-unit font size rendering in all browsers. 227 | // 228 | 229 | code, 230 | kbd, 231 | pre, 232 | samp { 233 | font-family: monospace, monospace; 234 | font-size: 1em; 235 | } 236 | 237 | // Forms 238 | // ========================================================================== 239 | 240 | // 241 | // Known limitation: by default, Chrome and Safari on OS X allow very limited 242 | // styling of `select`, unless a `border` property is set. 243 | // 244 | 245 | // 246 | // 1. Correct color not being inherited. 247 | // Known issue: affects color of disabled elements. 248 | // 2. Correct font properties not being inherited. 249 | // 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 250 | // 251 | 252 | button, 253 | input, 254 | optgroup, 255 | select, 256 | textarea { 257 | color: inherit; // 1 258 | font: inherit; // 2 259 | margin: 0; // 3 260 | } 261 | 262 | // 263 | // Address `overflow` set to `hidden` in IE 8/9/10/11. 264 | // 265 | 266 | button { 267 | overflow: visible; 268 | } 269 | 270 | // 271 | // Address inconsistent `text-transform` inheritance for `button` and `select`. 272 | // All other form control elements do not inherit `text-transform` values. 273 | // Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 274 | // Correct `select` style inheritance in Firefox. 275 | // 276 | 277 | button, 278 | select { 279 | text-transform: none; 280 | } 281 | 282 | // 283 | // 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 284 | // and `video` controls. 285 | // 2. Correct inability to style clickable `input` types in iOS. 286 | // 3. Improve usability and consistency of cursor style between image-type 287 | // `input` and others. 288 | // 289 | 290 | button, 291 | html input[type="button"], // 1 292 | input[type="reset"], 293 | input[type="submit"] { 294 | -webkit-appearance: button; // 2 295 | cursor: pointer; // 3 296 | } 297 | 298 | // 299 | // Re-set default cursor for disabled elements. 300 | // 301 | 302 | button[disabled], 303 | html input[disabled] { 304 | cursor: default; 305 | } 306 | 307 | // 308 | // Remove inner padding and border in Firefox 4+. 309 | // 310 | 311 | button::-moz-focus-inner, 312 | input::-moz-focus-inner { 313 | border: 0; 314 | padding: 0; 315 | } 316 | 317 | // 318 | // Address Firefox 4+ groupSetting `line-height` on `input` using `!important` in 319 | // the UA stylesheet. 320 | // 321 | 322 | input { 323 | line-height: normal; 324 | } 325 | 326 | // 327 | // It's recommended that you don't attempt to style these elements. 328 | // Firefox's implementation doesn't respect box-sizing, padding, or width. 329 | // 330 | // 1. Address box sizing set to `content-box` in IE 8/9/10. 331 | // 2. Remove excess padding in IE 8/9/10. 332 | // 333 | 334 | input[type="checkbox"], 335 | input[type="radio"] { 336 | box-sizing: border-box; // 1 337 | padding: 0; // 2 338 | } 339 | 340 | // 341 | // Fix the cursor style for Chrome's increment/decrement buttons. For certain 342 | // `font-size` values of the `input`, it causes the cursor style of the 343 | // decrement button to change from `default` to `text`. 344 | // 345 | 346 | input[type="number"]::-webkit-inner-spin-button, 347 | input[type="number"]::-webkit-outer-spin-button { 348 | height: auto; 349 | } 350 | 351 | // 352 | // 1. Address `appearance` set to `searchfield` in Safari and Chrome. 353 | // 2. Address `box-sizing` set to `border-box` in Safari and Chrome. 354 | // 355 | 356 | input[type="search"] { 357 | -webkit-appearance: textfield; // 1 358 | box-sizing: content-box; //2 359 | } 360 | 361 | // 362 | // Remove inner padding and search cancel button in Safari and Chrome on OS X. 363 | // Safari (but not Chrome) clips the cancel button when the search input has 364 | // padding (and `textfield` appearance). 365 | // 366 | 367 | input[type="search"]::-webkit-search-cancel-button, 368 | input[type="search"]::-webkit-search-decoration { 369 | -webkit-appearance: none; 370 | } 371 | 372 | // 373 | // Define consistent border, margin, and padding. 374 | // 375 | 376 | fieldset { 377 | border: 1px solid #c0c0c0; 378 | margin: 0 2px; 379 | padding: 0.35em 0.625em 0.75em; 380 | } 381 | 382 | // 383 | // 1. Correct `color` not being inherited in IE 8/9/10/11. 384 | // 2. Remove padding so people aren't caught out if they zero out fieldsets. 385 | // 386 | 387 | legend { 388 | border: 0; // 1 389 | padding: 0; // 2 390 | } 391 | 392 | // 393 | // Remove default vertical scrollbar in IE 8/9/10/11. 394 | // 395 | 396 | textarea { 397 | overflow: auto; 398 | } 399 | 400 | // 401 | // Don't inherit the `font-weight` (applied by a rule above). 402 | // NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 403 | // 404 | 405 | optgroup { 406 | font-weight: bold; 407 | } 408 | 409 | // Tables 410 | // ========================================================================== 411 | 412 | // 413 | // Remove most spacing between table cells. 414 | // 415 | 416 | table { 417 | border-collapse: collapse; 418 | border-spacing: 0; 419 | } 420 | 421 | td, 422 | th { 423 | padding: 0; 424 | } 425 | 426 | ul, li{ 427 | padding:0; 428 | margin:0; 429 | } 430 | li{ 431 | list-style:none; 432 | } 433 | -------------------------------------------------------------------------------- /website/src/scss/theme/_default.scss: -------------------------------------------------------------------------------- 1 | //上面的六种颜色是要删掉的,不要再项目中引用了 2 | //灰白色,这里只是取一些更普通的名称 3 | //$color-darker : rgba(0,0,0,.87);/*#333333*/ 4 | //$color-dark : rgba(0,0,0,.54);/*#666666*/ 5 | //$color-base : rgba(0,0,0,.38);/*#666666*/ 6 | //$color-medium : #CCCCCC; 7 | //$color-light : #E5E5E5; 8 | //$color-lighter : #F8F8F8; 9 | //$color-lightest : #FFFFFF; 10 | 11 | $border-color : rgba(0,0,0,0.1); 12 | $chat-bg-color : #F3F5F7;//右侧聊天窗口的背景色 rgba(255,255,255, 0.75) 13 | $session-bg-color : #E8EBEF;//右侧聊天窗口的背景色 rgba(255,255,255, 0.65) 14 | $right-bg-color : #F3F5F7; 15 | $right-chat-bg-color : transparent; 16 | $search-ipt-bg-color : rgba(255,255,255, 0.5); 17 | 18 | //主色 19 | $primary : #118BFB; 20 | $navigation-bg : #2E3E51; 21 | $navigation-hover-bg : rgba($primary, 0.1); 22 | $navigation-active-bg : rgba(16,139,251,0.15); 23 | $navigation-active-border : $primary; 24 | $navigation-color : rgba($color-lightest, 0.5);//导航栏icon颜色 25 | 26 | $divider-color : rgba($color-lightest,0.1); 27 | 28 | 29 | $hover-color: rgba($primary, 0.05);//hover时候的颜色 30 | $active-color: rgba($primary, 0.15);//激活时候的颜色 31 | 32 | 33 | //辅助颜色 34 | $toast-common : #F3F9FF; 35 | $toast-abnormal : #FFFAF2; 36 | $toast-error : #FFF7F6; 37 | $toast-success : #F7FBF5; 38 | 39 | 40 | 41 | $color-grey : #F4F4F4;//搜索框 42 | $color-you : #CAE5F9; 43 | $color-me : #E5E5E5; 44 | 45 | //输入框背景色 46 | $input-background-color : #FBFBFB; 47 | 48 | 49 | 50 | $bg-image : $chat-bg-color;//背景图片 51 | 52 | $main-bg-style : url($chat-bg-color) left bottom/cover no-repeat at-2x; 53 | $group-file-bg-image : ''; 54 | $group-file-bg-group-path : rgba(0,0,0,0.02); 55 | $group-file-bg-group-path-border: 1px solid rgba(130, 130, 130, 0.1); 56 | $group-file-search-border: 1px solid rgba(0, 0, 0, 0.1); 57 | $group-file-header-tab-background-color: transparent; 58 | 59 | 60 | $btn-bg-base : $primary; 61 | $border-color-base : darken($primary, 5%); 62 | $btn-primary-color : #fff; 63 | $btn-primary-bg : $primary; 64 | 65 | $btn-ghost-color : #FFFFFF; 66 | $btn-ghost-bg : $supplement; 67 | $btn-ghost-border : darken($supplement, 5%); 68 | 69 | $btn-disable-color : #ccc; 70 | $btn-disable-bg : #f3f5f7; 71 | $btn-disable-border : $border-color-base; 72 | 73 | $btn-danger-color : #FFFFFF; 74 | $btn-danger-bg : $danger; 75 | $btn-danger-border : $danger; 76 | 77 | 78 | 79 | //new css 80 | $yunpan-bg : #FFFFFF 81 | -------------------------------------------------------------------------------- /website/src/scss/variable.scss: -------------------------------------------------------------------------------- 1 | //这里定义了一些基础的变量 2 | 3 | //通用基础颜色 4 | $color-darker : rgba(0,0,0,.87);/*#333333*/ 5 | $color-dark : rgba(0,0,0,.54);/*#666666*/ 6 | $color-base : rgba(0,0,0,.38);/*#999999*/ 7 | $color-medium : rgba(0,0,0,.24);/*#CCCCCC*/ 8 | $color-light : rgba(0,0,0,.1);/*#E5E5E5*/ 9 | $color-light-1 : rgba(0,0,0,.05); 10 | $color-lighter : #F8F8F8; 11 | $color-lightest : #FFFFFF; 12 | 13 | //通用辅助颜色 14 | $supplement : #FF9801; 15 | $danger : #FF5D4A; 16 | $success : #5ABB3C; 17 | $name-other : #596E8F; 18 | $name-own : #CC841B; 19 | $color-pink : #F37D9A;//女性 20 | $color-blue : #46BEEF;//男性 21 | 22 | 23 | //存放一些宽度 24 | 25 | $main-title-height : 70px;//顶部70px 26 | $nav-width :60px; 27 | $left-width :280px;//内容区域左边列表宽度 28 | 29 | 30 | //z-index.scss 31 | $min-zindex : 1; 32 | $medium-zindex : 10; 33 | $large-zindex : 100; 34 | $max-zindex : 1000; 35 | 36 | //普通元素的padding 37 | $ctn-padding-xmin : 1px; 38 | $ctn-padding-min : 2px; 39 | $ctn-padding-normal : 5px; 40 | $ctn-padding-large : 10px; 41 | $ctn-padding-xlarge : 15px; 42 | $ctn-padding-xxlarge : 20px; 43 | 44 | //profile 宽高度 45 | 46 | //thumb 头像大小(头像的尺寸:80,60,40,36,28.) 47 | $thumb-base : 28px; 48 | $thumb-large : 36px; 49 | $thumb-xlarge : 40px; 50 | $thumb-xxlarge : 60px; 51 | $thumb-xxxlarge : 80px; 52 | 53 | 54 | //原有的头像尺寸(需要废弃) 55 | //$thumb 56 | //$thumb-min : 12px; 57 | //$thumb-xxxxlarge : $thumb-min * 6; 58 | //$thumb-big : 80px; 59 | 60 | 61 | 62 | //这里存字体相关的 63 | $font-family : "Helvetica Neue",Helvetica,"Apple Color Emoji",'Segoe UI Emoji', 'Segoe UI Symbol',Arial,"PingFang SC","Heiti SC", "Hiragino Sans GB","Microsoft YaHei","微软雅黑",sans-serif; 64 | $code-family : Consolas,Menlo,Courier,monospace; 65 | $font-size-small : 12px; //针对h6,说明文字 66 | $font-size-lSmall : 13px; //针对h6,说明文字 67 | $font-size-base : 14px; //正文,链接,h5 68 | $font-size-large : 16px; //h4 69 | $font-size-xlarge : 18px; //h3 70 | $font-size-xxlarge : 20px; //h2 71 | $font-size-xxxlarge : 24px; //h1 72 | $line-height-base : 1.5; 73 | $line-height-computed : floor(($font-size-base * $line-height-base)); 74 | 75 | $border-radius-base : 2px; 76 | $border-radius-normal : 4px; 77 | $border-radius-large : 5px; 78 | $border-radius-xlarge : 10px; 79 | $border-radius-xxlarge : 15px; 80 | 81 | 82 | 83 | // ICONFONT 84 | $iconfont-css-prefix : dxicon; 85 | 86 | $icon-url : "//at.alicdn.com/t/font_ovsogmhgit4ndn29"; //经常会改变,以后会换成本地的地址 87 | 88 | 89 | 90 | // Animation 91 | $ease-out : cubic-bezier(0.215, 0.61, 0.355, 1); 92 | $ease-in : cubic-bezier(0.55, 0.055, 0.675, 0.19); 93 | $ease-in-out : cubic-bezier(0.645, 0.045, 0.355, 1); 94 | $ease-out-back : cubic-bezier(0.12, 0.4, 0.29, 1.46); 95 | $ease-in-back : cubic-bezier(0.71, -0.46, 0.88, 0.6); 96 | $ease-in-out-back : cubic-bezier(0.71, -0.46, 0.29, 1.46); 97 | $ease-out-circ : cubic-bezier(0.08, 0.82, 0.17, 1); 98 | $ease-in-circ : cubic-bezier(0.6, 0.04, 0.98, 0.34); 99 | $ease-in-out-circ : cubic-bezier(0.78, 0.14, 0.15, 0.86); 100 | $ease-out-quint : cubic-bezier(0.23, 1, 0.32, 1); 101 | $ease-in-quint : cubic-bezier(0.755, 0.05, 0.855, 0.06); 102 | $ease-in-out-quint : cubic-bezier(0.86, 0, 0.07, 1); 103 | 104 | 105 | // 按钮边框颜色,理论上应该是背景色加深一点就可以了 106 | 107 | //$border-color-base : #d9d9d9; // base border outline a component 108 | //$box-shadow-base : 0 0 4px rgba(0, 0, 0, 0.17); 109 | //$border-color-split : #e9e9e9; // split border inside a component 110 | $cursor-disabled : not-allowed; 111 | $btn-font-weight : normal; 112 | 113 | 114 | 115 | 116 | 117 | $btn-default-color : $color-darker; 118 | $btn-default-bg : $color-lighter; 119 | $btn-default-border : $color-light; 120 | 121 | 122 | 123 | 124 | 125 | 126 | $btn-padding-base : 8px 31px; 127 | $btn-border-radius-base : 4px; 128 | 129 | $btn-font-size-lg : 14px; 130 | $btn-padding-lg : 4px 11px 5px 11px; 131 | $btn-border-radius-lg : $btn-border-radius-base; 132 | 133 | $btn-padding-sm : 1px 7px; 134 | $btn-border-radius-sm : $btn-border-radius-base; 135 | 136 | $btn-circle-size : 28px; 137 | $btn-circle-size-lg : 32px; 138 | $btn-circle-size-sm : 22px; 139 | 140 | $msg-title-height: 70px; 141 | $msg-border-color: $color-light; 142 | 143 | 144 | 145 | 146 | 147 | //气泡页部分宽度定义 148 | $msg-medium-min-width: 630px; 149 | $msg-medium-max-width: 1000px; 150 | $msg-medium-width : 860px; 151 | $msg-medium-left : 0px; 152 | $msg-medium-right : 0px; 153 | $msg-medium-top: 0px; 154 | $msg-medium-bottom: 0px; 155 | $slidepanel-width: 470px; 156 | 157 | -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "importHelpers": true, 7 | "jsx": "react", 8 | "esModuleInterop": true, 9 | "sourceMap": true, 10 | "baseUrl": "./", 11 | "strict": true, 12 | "paths": { 13 | "@/*": ["src/*"], 14 | "@@/*": ["src/.umi/*"] 15 | }, 16 | "allowSyntheticDefaultImports": true 17 | }, 18 | "exclude": [ 19 | "node_modules", 20 | "lib", 21 | "es", 22 | "dist", 23 | "typings", 24 | "**/__test__", 25 | "test", 26 | "docs", 27 | "tests" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /website/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css'; 2 | declare module '*.less'; 3 | --------------------------------------------------------------------------------