├── tools ├── demo │ ├── app.wxss │ ├── package.json │ ├── app.js │ ├── pages │ │ └── index │ │ │ ├── index.json │ │ │ ├── index.wxss │ │ │ ├── index.wxml │ │ │ └── index.js │ ├── app.json │ └── project.config.json ├── config.js ├── checkwxss.js ├── checkcomponents.js ├── utils.js └── build.js ├── src ├── index.wxss ├── assets │ ├── copy.wxss │ └── icon.png ├── index.json ├── index.wxml ├── shapes │ ├── triangle.class.js │ ├── circle.class.js │ ├── ellipse.class.js │ ├── rect.class.js │ ├── polygon.class.js │ ├── image.class.js │ ├── object.class.js │ └── text.class.js ├── utils │ ├── object.js │ ├── string.js │ ├── index.js │ └── misc.js ├── index.js ├── mixins │ ├── shared_methods.mixin.js │ ├── observable.mixin.js │ ├── text_style.mixin.js │ ├── object_origin.mixin.js │ ├── object_interactivity.mixin.js │ ├── canvas_events.mixin.js │ └── object_geometry.mixin.js ├── sugarjs.js ├── pattern.class.js ├── intersection.class.js ├── gradient.class.js ├── point.class.js ├── color.class.js └── canvas.class.js ├── .gitignore ├── .npmignore ├── .babelrc ├── test ├── wx.test.js ├── index.test.js └── utils.js ├── tsconfig.json ├── gulpfile.js ├── LICENSE ├── publish.js ├── package.json ├── .eslintrc.js └── README.md /tools/demo/app.wxss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tools/demo/package.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /tools/demo/app.js: -------------------------------------------------------------------------------- 1 | App({}) 2 | -------------------------------------------------------------------------------- /src/index.wxss: -------------------------------------------------------------------------------- 1 | .index { 2 | color: green; 3 | } 4 | -------------------------------------------------------------------------------- /src/assets/copy.wxss: -------------------------------------------------------------------------------- 1 | page { 2 | width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /src/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": true, 3 | "usingComponents": {} 4 | } 5 | -------------------------------------------------------------------------------- /src/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suguoyao/miniprogram-canvas-sugarjs/HEAD/src/assets/icon.png -------------------------------------------------------------------------------- /tools/demo/pages/index/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": { 3 | "sugar": "../../components/index" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | package-lock.json 4 | 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | miniprogram_dist 12 | miniprogram_dev 13 | node_modules 14 | coverage -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | package-lock.json 4 | 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | test 12 | tools 13 | docs 14 | miniprogram_dev 15 | node_modules 16 | coverage -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | ["module-resolver", { 4 | "root": ["./src"], 5 | "alias": {} 6 | }] 7 | ], 8 | "presets": [ 9 | ["env", {"loose": true, "modules": "commonjs"}] 10 | ] 11 | } -------------------------------------------------------------------------------- /tools/demo/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages":[ 3 | "pages/index/index" 4 | ], 5 | "window":{ 6 | "backgroundTextStyle":"light", 7 | "navigationBarBackgroundColor": "#fff", 8 | "navigationBarTitleText": "Canvas-SugarJS", 9 | "navigationBarTextStyle":"black" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/index.wxml: -------------------------------------------------------------------------------- 1 | 12 | 13 | Canvas-SugarJS 14 | -------------------------------------------------------------------------------- /test/wx.test.js: -------------------------------------------------------------------------------- 1 | const _ = require('./utils') 2 | 3 | test('wx.getSystemInfo', async () => { 4 | wx.getSystemInfo({ 5 | success(res) { 6 | expect(res.errMsg).toBe('getSystemInfo:ok') 7 | }, 8 | complete(res) { 9 | expect(res.errMsg).toBe('getSystemInfo:ok') 10 | }, 11 | }) 12 | }) 13 | 14 | test('wx.getSystemInfoSync', async () => { 15 | const info = wx.getSystemInfoSync() 16 | expect(info.SDKVersion).toBe('2.4.1') 17 | expect(info.version).toBe('6.6.3') 18 | }) 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "target": "es2015", 5 | "lib": ["es2015", "es2017", "dom"], 6 | "noImplicitAny": false, 7 | "moduleResolution": "node", 8 | "sourceMap": true, 9 | "baseUrl": ".", 10 | "allowSyntheticDefaultImports": true, 11 | "experimentalDecorators": true, 12 | "emitDecoratorMetadata":true, 13 | "esModuleInterop": true, 14 | "resolveJsonModule": true 15 | }, 16 | "files": [ 17 | "node_modules/miniprogram-api-typings/index.d.ts" 18 | ], 19 | "include": [ 20 | "src/**/*.ts" 21 | ], 22 | "exclude": [ 23 | "node_modules" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | const _ = require('./utils') 2 | 3 | test('render', async () => { 4 | const componentId = _.load('index', 'comp') 5 | const component = _.render(componentId, {prop: 'index.test.properties'}) 6 | 7 | const parent = document.createElement('parent-wrapper') 8 | component.attach(parent) 9 | 10 | expect(_.match(component.dom, 'index.test.properties-falseother.properties-other')).toBe(true) 11 | 12 | await _.sleep(10) 13 | 14 | expect(_.match(component.dom, 'index.test.properties-trueother.properties-other')).toBe(true) 15 | }) 16 | -------------------------------------------------------------------------------- /src/shapes/triangle.class.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Sugar on 2020/6/6. 3 | */ 4 | const ObjectClass = require('./object.class') 5 | 6 | class TriangleClass extends ObjectClass { 7 | constructor(options) { 8 | super() 9 | 10 | this.type = 'triangle' 11 | this.width = 50 12 | this.height = 50 13 | 14 | this.initialize(options) 15 | } 16 | 17 | initialize(options) { 18 | super.initialize(options) 19 | } 20 | 21 | _render(ctx) { 22 | let widthBy2 = this.width / 2 23 | let heightBy2 = this.height / 2 24 | 25 | ctx.beginPath() 26 | ctx.moveTo(-widthBy2, heightBy2) 27 | ctx.lineTo(0, -heightBy2) 28 | ctx.lineTo(widthBy2, heightBy2) 29 | ctx.closePath() 30 | 31 | this._renderPaintInOrder(ctx) 32 | } 33 | } 34 | 35 | module.exports = TriangleClass 36 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp') 2 | const clean = require('gulp-clean') 3 | 4 | const config = require('./tools/config') 5 | const BuildTask = require('./tools/build') 6 | const id = require('./package.json').name || 'miniprogram-custom-component' 7 | 8 | // 构建任务实例 9 | // eslint-disable-next-line no-new 10 | new BuildTask(id, config.entry) 11 | 12 | // 清空生成目录和文件 13 | gulp.task('clean', gulp.series(() => gulp.src(config.distPath, {read: false, allowEmpty: true}).pipe(clean()), done => { 14 | if (config.isDev) { 15 | return gulp.src(config.demoDist, {read: false, allowEmpty: true}) 16 | .pipe(clean()) 17 | } 18 | 19 | return done() 20 | })) 21 | // 监听文件变化并进行开发模式构建 22 | gulp.task('watch', gulp.series(`${id}-watch`)) 23 | // 开发模式构建 24 | gulp.task('dev', gulp.series(`${id}-dev`)) 25 | // 生产模式构建 26 | gulp.task('default', gulp.series(`${id}-default`)) 27 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const simulate = require('miniprogram-simulate') 4 | const config = require('../tools/config') 5 | 6 | // const dir = config.srcPath // 使用源码进行测试,对于 debug 和代码覆盖率检测会比较友好 7 | const dir = config.distPath // 使用构建后代码进行测试,如果使用了 typescript 进行开发,必须选择此目录 8 | 9 | try { 10 | fs.accessSync(dir) 11 | } catch (err) { 12 | console.error('请先执行 npm run build 再进行单元测试!!!') 13 | } 14 | 15 | const oldLoad = simulate.load 16 | simulate.load = function (componentPath, ...args) { 17 | if (typeof componentPath === 'string') componentPath = path.join(dir, componentPath) 18 | return oldLoad(componentPath, ...args) 19 | } 20 | 21 | module.exports = simulate 22 | 23 | // adjust the simulated wx api 24 | const oldGetSystemInfoSync = global.wx.getSystemInfoSync 25 | global.wx.getSystemInfoSync = function() { 26 | const res = oldGetSystemInfoSync() 27 | res.SDKVersion = '2.4.1' 28 | 29 | return res 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/object.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Sugar on 2020/5/26. 3 | */ 4 | 5 | function extend(destination, source, deep) { 6 | if (deep) { 7 | if (source instanceof Array) { 8 | destination = [] 9 | for (var i = 0, len = source.length; i < len; i++) { 10 | destination[i] = extend({}, source[i], deep) 11 | } 12 | } else if (source && typeof source === 'object') { 13 | for (var property in source) { 14 | if (property === 'canvas') { 15 | destination[property] = extend({}, source[property]) 16 | } else if (source.hasOwnProperty(property)) { 17 | destination[property] = extend({}, source[property], deep) 18 | } 19 | } 20 | } else { 21 | destination = source 22 | } 23 | } else { 24 | for (var property in source) { 25 | destination[property] = source[property] 26 | } 27 | } 28 | return destination 29 | } 30 | 31 | function clone(object, deep) { 32 | return extend({}, object, deep) 33 | } 34 | 35 | module.exports = { 36 | extend, 37 | clone 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 sugar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tools/demo/project.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "项目配置文件。", 3 | "packOptions": { 4 | "ignore": [] 5 | }, 6 | "setting": { 7 | "urlCheck": false, 8 | "es6": true, 9 | "postcss": true, 10 | "preloadBackgroundData": false, 11 | "minified": true, 12 | "newFeature": true, 13 | "coverView": true, 14 | "nodeModules": true, 15 | "autoAudits": false, 16 | "showShadowRootInWxmlPanel": true, 17 | "scopeDataCheck": false, 18 | "checkInvalidKey": true, 19 | "checkSiteMap": true, 20 | "uploadWithSourceMap": true, 21 | "babelSetting": { 22 | "ignore": [], 23 | "disablePlugins": [], 24 | "outputPath": "" 25 | } 26 | }, 27 | "compileType": "miniprogram", 28 | "libVersion": "2.11.1", 29 | "appid": "wx29169e6dd665fd55", 30 | "projectname": "miniprogram-demo", 31 | "isGameTourist": false, 32 | "simulatorType": "wechat", 33 | "simulatorPluginLibVersion": {}, 34 | "condition": { 35 | "search": { 36 | "current": -1, 37 | "list": [] 38 | }, 39 | "conversation": { 40 | "current": -1, 41 | "list": [] 42 | }, 43 | "game": { 44 | "currentL": -1, 45 | "list": [] 46 | }, 47 | "miniprogram": { 48 | "current": -1, 49 | "list": [] 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tools/demo/pages/index/index.wxss: -------------------------------------------------------------------------------- 1 | page{ 2 | padding-bottom: 500px; 3 | } 4 | 5 | .btn-group{ 6 | display: flex; 7 | flex-wrap: wrap; 8 | padding: 5px; 9 | } 10 | 11 | .btn-group button{ 12 | margin: 5px; 13 | } 14 | 15 | button{ 16 | position: relative; 17 | border: 0rpx; 18 | display: inline-flex; 19 | align-items: center; 20 | justify-content: center; 21 | box-sizing: border-box; 22 | padding: 0 30rpx; 23 | font-size: 28rpx; 24 | height: 64rpx; 25 | line-height: 1; 26 | text-align: center; 27 | text-decoration: none; 28 | overflow: visible; 29 | margin-left: initial; 30 | transform: translate(0rpx, 0rpx); 31 | margin-right: initial; 32 | border-radius: 100px; 33 | color: #fff; 34 | box-shadow: 6rpx 6rpx 8rpx rgba(26, 26, 26, 0.2); 35 | } 36 | 37 | button.button-hover { 38 | transform: translate(1rpx, 1rpx); 39 | color: #fff; 40 | } 41 | 42 | button.orange{ 43 | background-color: #f37b1d; 44 | } 45 | 46 | button.black{ 47 | background-color: #666; 48 | } 49 | 50 | button.white{ 51 | background-color: #fff; 52 | } 53 | 54 | button.green{ 55 | background-color: #39b54a; 56 | } 57 | 58 | button.red{ 59 | background-color: #e54d42; 60 | } 61 | 62 | button:after{ 63 | display: none; 64 | border: 0; 65 | outline: 0; 66 | } 67 | -------------------------------------------------------------------------------- /src/shapes/circle.class.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Sugar on 2020/6/6. 3 | */ 4 | const ObjectClass = require('./object.class') 5 | 6 | const pi = Math.PI 7 | 8 | class CircleClass extends ObjectClass { 9 | constructor(options) { 10 | super() 11 | 12 | this.type = 'circle' 13 | this.radius = 0 14 | this.startAngle = 0 15 | this.endAngle = pi * 2 16 | 17 | this.initialize(options) 18 | } 19 | 20 | initialize(options) { 21 | super.initialize(options) 22 | } 23 | 24 | _set(key, value) { 25 | super._set(key, value) 26 | 27 | if (key === 'radius') { 28 | this.setRadius(value) 29 | } 30 | 31 | return this 32 | } 33 | 34 | _render(ctx) { 35 | ctx.beginPath() 36 | ctx.arc( 37 | 0, 38 | 0, 39 | this.radius, 40 | this.startAngle, 41 | this.endAngle, false) 42 | this._renderPaintInOrder(ctx) 43 | } 44 | 45 | getRadiusX() { 46 | return this.get('radius') * this.get('scaleX') 47 | } 48 | 49 | /** 50 | * 返回对象的垂直半径(根据对象的缩放比例) 51 | */ 52 | getRadiusY() { 53 | return this.get('radius') * this.get('scaleY') 54 | } 55 | 56 | /** 57 | * 设置对象的半径(并更新宽度) 58 | */ 59 | setRadius(value) { 60 | this.radius = value 61 | return this.set('width', value * 2).set('height', value * 2) 62 | } 63 | } 64 | 65 | module.exports = CircleClass 66 | -------------------------------------------------------------------------------- /src/shapes/ellipse.class.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Sugar on 2020/6/6. 3 | */ 4 | const ObjectClass = require('./object.class') 5 | 6 | const piBy2 = Math.PI * 2 7 | 8 | class EllipseClass extends ObjectClass { 9 | constructor(options) { 10 | super() 11 | 12 | this.type = 'ellipse' 13 | this.rx = 0 14 | this.ry = 0 15 | 16 | this.initialize(options) 17 | } 18 | 19 | initialize(options) { 20 | super.initialize(options) 21 | this.set('rx', options && options.rx || 0) 22 | this.set('ry', options && options.ry || 0) 23 | } 24 | 25 | _set(key, value) { 26 | super._set(key, value) 27 | switch (key) { 28 | case 'rx': 29 | this.rx = value 30 | this.set('width', value * 2) 31 | break; 32 | 33 | case 'ry': 34 | this.ry = value 35 | this.set('height', value * 2) 36 | break; 37 | 38 | } 39 | return this; 40 | } 41 | 42 | getRx() { 43 | return this.get('rx') * this.get('scaleX') 44 | } 45 | 46 | getRy() { 47 | return this.get('ry') * this.get('scaleY') 48 | } 49 | 50 | _render(ctx) { 51 | ctx.beginPath() 52 | ctx.save() 53 | ctx.transform(1, 0, 0, this.ry / this.rx, 0, 0) 54 | ctx.arc( 55 | 0, 56 | 0, 57 | this.rx, 58 | 0, 59 | piBy2, 60 | false); 61 | ctx.restore() 62 | this._renderPaintInOrder(ctx) 63 | } 64 | } 65 | 66 | module.exports = EllipseClass 67 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const {compareVersion} = require('./utils/index') 2 | 3 | const canvasId = 'canvas-sugarjs' 4 | 5 | Component({ 6 | properties: { 7 | width: { 8 | type: Number, 9 | value: 400 10 | }, 11 | height: { 12 | type: Number, 13 | value: 300 14 | }, 15 | }, 16 | data: { 17 | use2dCanvas: false, // 2.9.0 后可用canvas 2d 接口 18 | }, 19 | lifetimes: { 20 | attached() { 21 | // const {SDKVersion, pixelRatio: dpr} = wx.getSystemInfoSync() 22 | // const use2dCanvas = compareVersion(SDKVersion, '2.9.0') >= 0 23 | // this.dpr = dpr 24 | // this.setData({use2dCanvas}, () => { 25 | // if (use2dCanvas) { 26 | // const query = this.createSelectorQuery() 27 | // query.select(`#${canvasId}`) 28 | // .fields({node: true, size: true}) 29 | // .exec(res => { 30 | // const canvas = res[0].node 31 | // const ctx = canvas.getContext('2d') 32 | // canvas.width = res[0].width * dpr 33 | // canvas.height = res[0].height * dpr 34 | // // 在调用后,之后创建的路径其横纵坐标会被缩放。多次调用倍数会相乘。 35 | // ctx.scale(dpr, dpr) 36 | // this.ctx = ctx 37 | // this.canvas = canvas 38 | // }) 39 | // } else { 40 | // this.ctx = wx.createCanvasContext(canvasId, this) 41 | // } 42 | // }) 43 | } 44 | }, 45 | methods: {} 46 | }) 47 | -------------------------------------------------------------------------------- /src/mixins/shared_methods.mixin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Sugar on 2020/5/26. 3 | */ 4 | const Gradient = require('../gradient.class') 5 | const Pattern = require('../pattern.class') 6 | 7 | module.exports = { 8 | _setOptions: function (options) { 9 | for (var prop in options) { 10 | this.set(prop, options[prop]); 11 | } 12 | }, 13 | 14 | _initGradient: function (filler, property) { 15 | if (filler && filler.colorStops && !(filler instanceof Gradient)) { 16 | this.set(property, new Gradient(filler)); 17 | } 18 | }, 19 | 20 | _initPattern: function (filler, property, callback) { 21 | if (filler && filler.source && !(filler instanceof Pattern)) { 22 | this.set(property, new Pattern(filler, callback)); 23 | } else { 24 | callback && callback(); 25 | } 26 | }, 27 | 28 | _setObject: function (obj) { 29 | for (var prop in obj) { 30 | this._set(prop, obj[prop]); 31 | } 32 | }, 33 | 34 | set: function (key, value) { 35 | if (typeof key === 'object') { 36 | this._setObject(key); 37 | } else { 38 | this._set(key, value); 39 | } 40 | return this; 41 | }, 42 | 43 | _set: function (key, value) { 44 | this[key] = value; 45 | }, 46 | 47 | toggle: function (property) { 48 | var value = this.get(property); 49 | if (typeof value === 'boolean') { 50 | this.set(property, !value); 51 | } 52 | return this; 53 | }, 54 | 55 | get: function (property) { 56 | return this[property]; 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /tools/demo/pages/index/index.wxml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/shapes/rect.class.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Sugar on 2020/5/26. 3 | */ 4 | const ObjectClass = require('./object.class') 5 | 6 | class RectClass extends ObjectClass { 7 | constructor(options) { 8 | super(); 9 | this.type = 'rect' 10 | this.rx = 0 11 | this.ry = 0 12 | 13 | this.initialize(options) 14 | } 15 | 16 | initialize(options) { 17 | super.initialize(options) 18 | this._initRxRy() 19 | } 20 | 21 | _initRxRy() { 22 | if (this.rx && !this.ry) { 23 | this.ry = this.rx 24 | } else if (this.ry && !this.rx) { 25 | this.rx = this.ry 26 | } 27 | } 28 | 29 | _render(ctx) { 30 | console.log('绘制矩形', this) 31 | let rx = this.rx ? Math.min(this.rx, this.width / 2) : 0, 32 | ry = this.ry ? Math.min(this.ry, this.height / 2) : 0, 33 | w = this.width, 34 | h = this.height, 35 | x = -this.width / 2, 36 | y = -this.height / 2, 37 | isRounded = rx !== 0 || ry !== 0, 38 | k = 1 - 0.5522847498 39 | ctx.beginPath() 40 | 41 | ctx.moveTo(x + rx, y) 42 | 43 | ctx.lineTo(x + w - rx, y) 44 | isRounded && ctx.bezierCurveTo(x + w - k * rx, y, x + w, y + k * ry, x + w, y + ry) 45 | 46 | ctx.lineTo(x + w, y + h - ry) 47 | isRounded && ctx.bezierCurveTo(x + w, y + h - k * ry, x + w - k * rx, y + h, x + w - rx, y + h) 48 | 49 | ctx.lineTo(x + rx, y + h) 50 | isRounded && ctx.bezierCurveTo(x + k * rx, y + h, x, y + h - k * ry, x, y + h - ry) 51 | 52 | ctx.lineTo(x, y + ry) 53 | isRounded && ctx.bezierCurveTo(x, y + k * ry, x + k * rx, y, x + rx, y) 54 | 55 | ctx.closePath() 56 | 57 | this._renderPaintInOrder(ctx) 58 | } 59 | } 60 | 61 | module.exports = RectClass 62 | -------------------------------------------------------------------------------- /src/utils/string.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Sugar on 2020/5/27. 3 | */ 4 | 5 | function camelize(string) { 6 | return string.replace(/-+(.)?/g, function (match, character) { 7 | return character ? character.toUpperCase() : ''; 8 | }); 9 | } 10 | 11 | function capitalize(string, firstLetterOnly) { 12 | return string.charAt(0).toUpperCase() + 13 | (firstLetterOnly ? string.slice(1) : string.slice(1).toLowerCase()); 14 | } 15 | 16 | function escapeXml(string) { 17 | return string.replace(/&/g, '&') 18 | .replace(/"/g, '"') 19 | .replace(/'/g, ''') 20 | .replace(//g, '>'); 22 | } 23 | 24 | function graphemeSplit(textstring) { 25 | var i = 0, chr, graphemes = []; 26 | for (i = 0, chr; i < textstring.length; i++) { 27 | if ((chr = getWholeChar(textstring, i)) === false) { 28 | continue; 29 | } 30 | graphemes.push(chr); 31 | } 32 | return graphemes; 33 | } 34 | 35 | function getWholeChar(str, i) { 36 | var code = str.charCodeAt(i); 37 | 38 | if (isNaN(code)) { 39 | return ''; 40 | } 41 | if (code < 0xD800 || code > 0xDFFF) { 42 | return str.charAt(i); 43 | } 44 | 45 | if (0xD800 <= code && code <= 0xDBFF) { 46 | if (str.length <= (i + 1)) { 47 | throw 'High surrogate without following low surrogate'; 48 | } 49 | var next = str.charCodeAt(i + 1); 50 | if (0xDC00 > next || next > 0xDFFF) { 51 | throw 'High surrogate without following low surrogate'; 52 | } 53 | return str.charAt(i) + str.charAt(i + 1); 54 | } 55 | if (i === 0) { 56 | throw 'Low surrogate without preceding high surrogate'; 57 | } 58 | var prev = str.charCodeAt(i - 1); 59 | 60 | if (0xD800 > prev || prev > 0xDBFF) { 61 | throw 'Low surrogate without preceding high surrogate'; 62 | } 63 | return false; 64 | } 65 | 66 | module.exports = { 67 | camelize, 68 | capitalize, 69 | escapeXml, 70 | graphemeSplit 71 | } 72 | -------------------------------------------------------------------------------- /src/mixins/observable.mixin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Sugar on 2020/5/28. 3 | */ 4 | const {fill} = require('../utils/index'); 5 | 6 | const _removeEventListener = function (eventName, handler) { 7 | if (!this.__eventListeners[eventName]) { 8 | return; 9 | } 10 | var eventListener = this.__eventListeners[eventName]; 11 | if (handler) { 12 | eventListener[eventListener.indexOf(handler)] = false; 13 | } else { 14 | fill(eventListener, false); 15 | } 16 | } 17 | 18 | const on = function (eventName, handler) { 19 | if (!this.__eventListeners) { 20 | this.__eventListeners = {}; 21 | } 22 | if (arguments.length === 1) { 23 | for (var prop in eventName) { 24 | this.on(prop, eventName[prop]); 25 | } 26 | } else { 27 | if (!this.__eventListeners[eventName]) { 28 | this.__eventListeners[eventName] = []; 29 | } 30 | this.__eventListeners[eventName].push(handler); 31 | } 32 | return this; 33 | } 34 | 35 | const off = function (eventName, handler) { 36 | if (!this.__eventListeners) { 37 | return this; 38 | } 39 | 40 | if (arguments.length === 0) { 41 | for (eventName in this.__eventListeners) { 42 | _removeEventListener.call(this, eventName); 43 | } 44 | } else if (arguments.length === 1 && typeof arguments[0] === 'object') { 45 | for (var prop in eventName) { 46 | _removeEventListener.call(this, prop, eventName[prop]); 47 | } 48 | } else { 49 | _removeEventListener.call(this, eventName, handler); 50 | } 51 | return this; 52 | } 53 | 54 | const fire = function (eventName, options) { 55 | if (!this.__eventListeners) { 56 | return this; 57 | } 58 | 59 | var listenersForEvent = this.__eventListeners[eventName]; 60 | if (!listenersForEvent) { 61 | return this; 62 | } 63 | 64 | for (var i = 0, len = listenersForEvent.length; i < len; i++) { 65 | listenersForEvent[i] && listenersForEvent[i].call(this, options || {}); 66 | } 67 | this.__eventListeners[eventName] = listenersForEvent.filter((value) => { 68 | return value !== false; 69 | }); 70 | return this; 71 | } 72 | 73 | module.exports = { 74 | fire, 75 | on, 76 | off 77 | } 78 | -------------------------------------------------------------------------------- /src/sugarjs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Sugar on 2020/5/25. 3 | */ 4 | const CanvasClass = require('./canvas.class') 5 | const ObjectClass = require('./shapes/object.class') 6 | const ImageClass = require('./shapes/image.class') 7 | const TextClass = require('./shapes/text.class') 8 | const RectClass = require('./shapes/rect.class') 9 | const PolygonClass = require('./shapes/polygon.class') 10 | const TriangleClass = require('./shapes/triangle.class') 11 | const CircleClass = require('./shapes/circle.class') 12 | const EllipseClass = require('./shapes/ellipse.class') 13 | const GradientClass = require('./gradient.class') 14 | const PatternClass = require('./pattern.class') 15 | const PointClass = require('./point.class') 16 | const ColorClass = require('./color.class') 17 | 18 | const {mergeMethods} = require('./utils/index') 19 | const CommonMethods = require('./mixins/shared_methods.mixin') 20 | const Observable = require('./mixins/observable.mixin') 21 | const CanvasEvent = require('./mixins/canvas_events.mixin') 22 | const ObjectOrigin = require('./mixins/object_origin.mixin') 23 | const ObjectInteractivity = require('./mixins/object_interactivity.mixin') 24 | const ObjectGeometry = require('./mixins/object_geometry.mixin') 25 | const TextStyles = require('./mixins/text_style.mixin') 26 | 27 | const Sugar = {} 28 | 29 | mergeMethods(CanvasClass, CommonMethods) 30 | mergeMethods(CanvasClass, Observable) 31 | mergeMethods(CanvasClass, CanvasEvent) 32 | mergeMethods(ObjectClass, CommonMethods) 33 | mergeMethods(ObjectClass, Observable) 34 | mergeMethods(ObjectClass, ObjectOrigin) 35 | mergeMethods(ObjectClass, ObjectInteractivity) 36 | mergeMethods(ObjectClass, ObjectGeometry) 37 | mergeMethods(TextClass, TextStyles) 38 | 39 | Sugar.Canvas = CanvasClass 40 | Sugar.Object = ObjectClass 41 | Sugar.Image = ImageClass 42 | Sugar.Text = TextClass 43 | Sugar.Rect = RectClass 44 | Sugar.Polygon = PolygonClass 45 | Sugar.Triangle = TriangleClass 46 | Sugar.Circle = CircleClass 47 | Sugar.Ellipse = EllipseClass 48 | Sugar.Gradient = GradientClass 49 | Sugar.Pattern = PatternClass 50 | Sugar.Point = PointClass 51 | Sugar.Color = ColorClass 52 | 53 | 54 | module.exports = Sugar 55 | -------------------------------------------------------------------------------- /tools/config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const webpack = require('webpack') 4 | const nodeExternals = require('webpack-node-externals') 5 | 6 | const isDev = process.argv.indexOf('--develop') >= 0 7 | const isWatch = process.argv.indexOf('--watch') >= 0 8 | const demoSrc = path.resolve(__dirname, './demo') 9 | const demoDist = path.resolve(__dirname, '../miniprogram_dev') 10 | const src = path.resolve(__dirname, '../src') 11 | const dev = path.join(demoDist, 'components') 12 | const dist = path.resolve(__dirname, '../miniprogram_dist') 13 | 14 | module.exports = { 15 | entry: ['index', 'sugarjs'], 16 | 17 | isDev, 18 | isWatch, 19 | srcPath: src, // 源目录 20 | distPath: isDev ? dev : dist, // 目标目录 21 | 22 | demoSrc, // demo 源目录 23 | demoDist, // demo 目标目录 24 | 25 | wxss: { 26 | less: false, // 使用 less 来编写 wxss 27 | sourcemap: false, // 生成 less sourcemap 28 | }, 29 | 30 | js: { 31 | webpack: true, // 使用 webpack 来构建 js 32 | }, 33 | 34 | webpack: { 35 | mode: 'production', 36 | output: { 37 | filename: '[name].js', 38 | libraryTarget: 'commonjs2', 39 | }, 40 | target: 'node', 41 | externals: [nodeExternals()], // 忽略 node_modules 42 | module: { 43 | rules: [{ 44 | test: /\.js$/i, 45 | use: [{ 46 | loader: 'thread-loader', 47 | }, { 48 | loader: 'babel-loader', 49 | options: { 50 | cacheDirectory: true, 51 | }, 52 | }, 53 | // { 54 | // loader: 'eslint-loader', 55 | // } 56 | ], 57 | exclude: /node_modules/ 58 | }], 59 | }, 60 | resolve: { 61 | modules: [src, 'node_modules'], 62 | extensions: ['.js', '.json'], 63 | }, 64 | plugins: [ 65 | new webpack.DefinePlugin({}), 66 | new webpack.optimize.LimitChunkCountPlugin({maxChunks: 1}), 67 | ], 68 | optimization: { 69 | minimize: false, 70 | }, 71 | devtool: 'source-map', // 生成 js sourcemap 72 | performance: { 73 | hints: 'warning', 74 | assetFilter: assetFilename => assetFilename.endsWith('.js') 75 | } 76 | }, 77 | 78 | copy: ['./assets'], // 将会复制到目标目录 79 | } 80 | -------------------------------------------------------------------------------- /publish.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Sugar on 2020/6/1. 3 | */ 4 | 5 | const {execSync} = require('child_process') 6 | const semver = require('semver') 7 | const fs = require('fs') 8 | const path = require('path') 9 | const pkgPath = path.resolve(__dirname, './package.json') 10 | const pkgText = fs.readFileSync(pkgPath) 11 | const pkgObject = JSON.parse(pkgText) 12 | const yargs = require('yargs') 13 | const argv = yargs.alias('s', 'semver').argv 14 | const SEMVER_TYPE = argv.s ? argv.s : 3 // 默认为patch 15 | 16 | function getCurrentPublishedVersion() { 17 | return execSync(`npm view ${pkgObject.name} dist-tags.latest`).toString() 18 | } 19 | 20 | function getSemverType(type) { 21 | if (+type === 1) { 22 | return 'major' 23 | } 24 | if (+type === 2) { 25 | return 'minor' 26 | } 27 | if (+type === 3) { 28 | return 'patch' 29 | } 30 | } 31 | 32 | function updatePackageToGit(version) { 33 | execSync(`git checkout master`) 34 | execSync(`git add package.json`) 35 | execSync(`git commit -m "更新版本号到${version}"`) 36 | execSync(`git push -u origin master`) 37 | console.log('推送package.json更新到git 成功') 38 | } 39 | 40 | function writePackageJson(version) { 41 | // 更新package.json 42 | fs.writeFileSync( 43 | pkgPath, 44 | JSON.stringify(Object.assign(pkgObject, { 45 | version: version, 46 | }), null, 2) 47 | ) 48 | } 49 | 50 | function buildJS() { 51 | execSync(`yarn clean`) 52 | execSync(`yarn dist`) 53 | } 54 | 55 | function publish() { 56 | const currentPublishedVersion = getCurrentPublishedVersion() 57 | const toPublishVersion = semver.inc(currentPublishedVersion, getSemverType(SEMVER_TYPE)) 58 | console.log(`开始编译打包JS`) 59 | buildJS() 60 | console.log(`编译打包成功,已生成miniprogram_dist文件夹`) 61 | console.log(`当前线上${pkgObject.name}包的版本号为:${currentPublishedVersion}`) 62 | console.log(`开始发布npm包... 发布版本号为:${toPublishVersion}`) 63 | writePackageJson(toPublishVersion) 64 | execSync(`npm config set registry http://registry.npmjs.org/`) 65 | execSync(`npm publish`) 66 | console.log(`npm publish发布成功`) 67 | execSync(`npm config set registry https://registry.npm.taobao.org`) 68 | updatePackageToGit(toPublishVersion) 69 | } 70 | 71 | function run() { 72 | publish() 73 | } 74 | 75 | run() 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "miniprogram-canvas-sugarjs", 3 | "version": "1.0.4", 4 | "description": "打造一个致力于微信小程序的Canvas库,类似于H5原生JS Canvas库-FabricJS", 5 | "main": "miniprogram_dist/sugarjs.js", 6 | "scripts": { 7 | "dev": "gulp dev --develop", 8 | "watch": "gulp watch --develop --watch", 9 | "build": "gulp", 10 | "dist": "npm run build", 11 | "clean-dev": "gulp clean --develop", 12 | "clean": "gulp clean", 13 | "test": "jest --bail", 14 | "test-debug": "node --inspect-brk ./node_modules/jest/bin/jest.js --runInBand --bail", 15 | "coverage": "jest ./test/* --coverage --bail", 16 | "lint": "eslint \"src/**/*.js\" --fix", 17 | "lint-tools": "eslint \"tools/**/*.js\" --rule \"import/no-extraneous-dependencies: false\" --fix" 18 | }, 19 | "miniprogram": "miniprogram_dist", 20 | "jest": { 21 | "testEnvironment": "jsdom", 22 | "testURL": "https://jest.test", 23 | "collectCoverageFrom": [ 24 | "miniprogram_dist/**/*.js" 25 | ], 26 | "moduleDirectories": [ 27 | "node_modules", 28 | "miniprogram_dist" 29 | ] 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "" 34 | }, 35 | "author": "sugar ", 36 | "license": "MIT", 37 | "devDependencies": { 38 | "@typescript-eslint/eslint-plugin": "^2.28.0", 39 | "@typescript-eslint/parser": "^2.28.0", 40 | "babel-core": "^6.26.3", 41 | "babel-loader": "^7.1.5", 42 | "babel-plugin-module-resolver": "^3.2.0", 43 | "babel-preset-env": "^1.7.0", 44 | "colors": "^1.3.1", 45 | "eslint": "^5.14.1", 46 | "eslint-config-airbnb-base": "13.1.0", 47 | "eslint-loader": "^2.1.2", 48 | "eslint-plugin-import": "^2.16.0", 49 | "eslint-plugin-node": "^7.0.1", 50 | "eslint-plugin-promise": "^3.8.0", 51 | "gulp": "^4.0.0", 52 | "gulp-clean": "^0.4.0", 53 | "gulp-if": "^2.0.2", 54 | "gulp-install": "^1.1.0", 55 | "gulp-less": "^4.0.1", 56 | "gulp-rename": "^1.4.0", 57 | "gulp-sourcemaps": "^2.6.5", 58 | "jest": "^23.5.0", 59 | "miniprogram-api-typings": "^2.10.3-1", 60 | "miniprogram-simulate": "^1.2.0", 61 | "thread-loader": "^2.1.3", 62 | "through2": "^2.0.3", 63 | "ts-loader": "^7.0.0", 64 | "typescript": "^3.8.3", 65 | "vinyl": "^2.2.0", 66 | "webpack": "^4.29.5", 67 | "webpack-node-externals": "^1.7.2" 68 | }, 69 | "dependencies": {} 70 | } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'extends': [ 3 | 'airbnb-base', 4 | 'plugin:promise/recommended' 5 | ], 6 | 'parser': '@typescript-eslint/parser', 7 | 'plugins': ['@typescript-eslint'], 8 | 'parserOptions': { 9 | 'ecmaVersion': 9, 10 | 'ecmaFeatures': { 11 | 'jsx': false 12 | }, 13 | 'sourceType': 'module' 14 | }, 15 | 'env': { 16 | 'es6': true, 17 | 'node': true, 18 | 'jest': true 19 | }, 20 | 'plugins': [ 21 | 'import', 22 | 'node', 23 | 'promise' 24 | ], 25 | 'rules': { 26 | 'arrow-parens': 'off', 27 | 'comma-dangle': [ 28 | 'error', 29 | 'only-multiline' 30 | ], 31 | 'complexity': ['error', 10], 32 | 'func-names': 'off', 33 | 'global-require': 'off', 34 | 'handle-callback-err': [ 35 | 'error', 36 | '^(err|error)$' 37 | ], 38 | 'import/no-unresolved': [ 39 | 'error', 40 | { 41 | 'caseSensitive': true, 42 | 'commonjs': true, 43 | 'ignore': ['^[^.]'] 44 | } 45 | ], 46 | 'import/prefer-default-export': 'off', 47 | 'linebreak-style': 'off', 48 | 'no-catch-shadow': 'error', 49 | 'no-continue': 'off', 50 | 'no-div-regex': 'warn', 51 | 'no-else-return': 'off', 52 | 'no-param-reassign': 'off', 53 | 'no-plusplus': 'off', 54 | 'no-shadow': 'off', 55 | 'no-multi-assign': 'off', 56 | 'no-underscore-dangle': 'off', 57 | 'node/no-deprecated-api': 'error', 58 | 'node/process-exit-as-throw': 'error', 59 | 'object-curly-spacing': [ 60 | 'error', 61 | 'never' 62 | ], 63 | 'operator-linebreak': [ 64 | 'error', 65 | 'after', 66 | { 67 | 'overrides': { 68 | ':': 'before', 69 | '?': 'before' 70 | } 71 | } 72 | ], 73 | 'prefer-arrow-callback': 'off', 74 | 'prefer-destructuring': 'off', 75 | 'prefer-template': 'off', 76 | 'quote-props': [ 77 | 1, 78 | 'as-needed', 79 | { 80 | 'unnecessary': true 81 | } 82 | ], 83 | 'semi': [ 84 | 'error', 85 | 'never' 86 | ], 87 | 'no-await-in-loop': 'off', 88 | 'no-restricted-syntax': 'off', 89 | 'promise/always-return': 'off', 90 | }, 91 | 'globals': { 92 | 'window': true, 93 | 'document': true, 94 | 'App': true, 95 | 'Page': true, 96 | 'Component': true, 97 | 'Behavior': true, 98 | 'wx': true, 99 | 'getCurrentPages': true, 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # miniprogram-canvas-sugarjs 2 | 3 | [![](https://img.shields.io/npm/v/miniprogram-canvas-sugarjs)](https://www.npmjs.com/package/miniprogram-canvas-sugarjs) 4 | 5 | > 使用此组件需要依赖依赖开发者工具的 npm 构建。具体详情可查阅[微信小程序官方npm文档](https://developers.weixin.qq.com/miniprogram/dev/devtools/npm.html)。 6 | 7 | ## 介绍 8 | 9 | 打造一个致力于微信小程序的Canvas库,类似于H5原生JS Canvas库 - [FabricJS](http://fabricjs.com/) 10 | 11 |

12 | 13 |

14 | 15 | ## 开发进度 16 | 17 | 开发中... 18 | 19 | ## 使用方法 20 | 21 | 1. 安装组件 22 | 23 | ``` 24 | npm install --save miniprogram-canvas-sugarjs 25 | ``` 26 | 27 | 2. .wxml 28 | 29 | ```html 30 | 37 | 38 | ``` 39 | 40 | 3. 在.js文件中引用 41 | 42 | ```js 43 | const sugar = require('miniprogram-canvas-sugarjs') 44 | 45 | Page({ 46 | data: { 47 | width: windowWidth, 48 | height: 500, 49 | }, 50 | onReady() { 51 | const query = wx.createSelectorQuery() 52 | query.select(`#sugarjs`).fields({node: true, size: true}).exec(res => { 53 | const canvas = res[0].node 54 | this.sugar = new sugar.Canvas({ 55 | canvas: canvas, 56 | width: this.data.width, 57 | height: this.data.height, 58 | backgroundColor: 'skyblue' 59 | }) 60 | }) 61 | } 62 | }) 63 | ``` 64 | 65 | 66 | 67 | ### 功能清单 68 | 69 | 1. canvas主体 70 | - [x] 初始化(宽高、背景) 71 | - [ ] 其他... 72 | 73 | 2. 图层类 74 | - [x] 基类ObjectClass 75 | - [x] 图片ImageClass 76 | - [x] 文本TextClass 77 | - [x] 矩形RectClass 78 | - [x] 三角形TriangleClass 79 | - [x] 多边形PolygonClass 80 | - [x] 圆CircleClass 81 | - [x] 椭圆EllipseClass 82 | - [ ] 直线LineClass 83 | - [ ] 群组GroupClass 84 | - [ ] 其他... 85 | 86 | 87 | 3. 操作 88 | - [x] 增删 89 | - [x] 点击图层进入选中状态(显示图层边框控件) 90 | - [x] 拖动 91 | - [x] 缩放 92 | - [x] 旋转 93 | - [x] 翻转 94 | - [ ] 图层层级管理(上移、下移、置顶、置底) 95 | - [ ] 文本内容编辑 96 | - [ ] 其他... 97 | 98 | 4. 事件监听 99 | - [x] canvas初始化周期事件 100 | - [x] 手指触摸事件 101 | - [ ] 清单3中的操作事件的监听 102 | - [ ] 其他... 103 | 104 | 5. 拓展、增强功能 105 | - [ ] 状态存储(撤销undo、恢复redo) 106 | - [ ] 导入、导出canvas数据 107 | - [x] toDataURL生成图片 108 | - [ ] 手势缩放、旋转 109 | - [ ] 动画 110 | - [ ] 其他... 111 | 112 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Sugar on 2020/5/25. 3 | */ 4 | 5 | /** 6 | * 小程序版本库版本号比较 7 | * @param v1 8 | * @param v2 9 | * @returns {number} 10 | */ 11 | const compareVersion = (v1, v2) => { 12 | v1 = v1.split('.') 13 | v2 = v2.split('.') 14 | const len = Math.max(v1.length, v2.length) 15 | while (v1.length < len) { 16 | v1.push('0') 17 | } 18 | while (v2.length < len) { 19 | v2.push('0') 20 | } 21 | for (let i = 0; i < len; i++) { 22 | const num1 = parseInt(v1[i], 10) 23 | const num2 = parseInt(v2[i], 10) 24 | 25 | if (num1 > num2) { 26 | return 1 27 | } else if (num1 < num2) { 28 | return -1 29 | } 30 | } 31 | 32 | return 0 33 | } 34 | 35 | 36 | let slice = Array.prototype.slice; 37 | 38 | function invoke(array, method) { 39 | let args = slice.call(arguments, 2), result = []; 40 | for (let i = 0, len = array.length; i < len; i++) { 41 | result[i] = args.length ? array[i][method].apply(array[i], args) : array[i][method].call(array[i]); 42 | } 43 | return result; 44 | } 45 | 46 | function max(array, byProperty) { 47 | return find(array, byProperty, function (value1, value2) { 48 | return value1 >= value2; 49 | }); 50 | } 51 | 52 | function min(array, byProperty) { 53 | return find(array, byProperty, function (value1, value2) { 54 | return value1 < value2; 55 | }); 56 | } 57 | 58 | function fill(array, value) { 59 | let k = array.length; 60 | while (k--) { 61 | array[k] = value; 62 | } 63 | return array; 64 | } 65 | 66 | function find(array, byProperty, condition) { 67 | if (!array || array.length === 0) { 68 | return; 69 | } 70 | 71 | let i = array.length - 1, 72 | result = byProperty ? array[i][byProperty] : array[i]; 73 | if (byProperty) { 74 | while (i--) { 75 | if (condition(array[i][byProperty], result)) { 76 | result = array[i][byProperty]; 77 | } 78 | } 79 | } else { 80 | while (i--) { 81 | if (condition(array[i], result)) { 82 | result = array[i]; 83 | } 84 | } 85 | } 86 | return result; 87 | } 88 | 89 | function toFixed(number, fractionDigits) { 90 | return parseFloat(Number(number).toFixed(fractionDigits)); 91 | } 92 | 93 | function mergeMethods(a, b) { 94 | for (const prop in b) { 95 | if (b.hasOwnProperty(prop)) { 96 | a.prototype[prop] = b[prop] 97 | } 98 | } 99 | return a 100 | } 101 | 102 | module.exports = { 103 | compareVersion, 104 | fill, 105 | invoke, 106 | min, 107 | max, 108 | toFixed, 109 | mergeMethods 110 | } 111 | -------------------------------------------------------------------------------- /src/shapes/polygon.class.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Sugar on 2020/6/7. 3 | */ 4 | const ObjectClass = require('./object.class') 5 | const { 6 | min, 7 | max, 8 | } = require('../utils/index') 9 | 10 | class PolygonClass extends ObjectClass { 11 | constructor(points, options) { 12 | super() 13 | 14 | this.type = 'polygon' 15 | this.points = null 16 | 17 | this.initialize(points, options) 18 | } 19 | 20 | initialize(points, options) { 21 | options = options || {} 22 | this.points = points || [] 23 | super.initialize(options) 24 | this._setPositionDimensions(options) 25 | } 26 | 27 | _setPositionDimensions(options) { 28 | let calcDim = this._calcDimensions(options), correctLeftTop 29 | this.width = calcDim.width 30 | this.height = calcDim.height 31 | if (!options.fromSVG) { 32 | correctLeftTop = this.translateToGivenOrigin( 33 | {x: calcDim.left - this.strokeWidth / 2, y: calcDim.top - this.strokeWidth / 2}, 34 | 'left', 35 | 'top', 36 | this.originX, 37 | this.originY 38 | ) 39 | } 40 | if (typeof options.left === 'undefined') { 41 | // this.left = options.fromSVG ? calcDim.left : correctLeftTop.x 42 | this.left = correctLeftTop.x 43 | } 44 | if (typeof options.top === 'undefined') { 45 | // this.top = options.fromSVG ? calcDim.top : correctLeftTop.y 46 | this.top = correctLeftTop.y 47 | } 48 | this.pathOffset = { 49 | x: calcDim.left + this.width / 2, 50 | y: calcDim.top + this.height / 2 51 | } 52 | } 53 | 54 | _calcDimensions() { 55 | let points = this.points, 56 | minX = min(points, 'x') || 0, 57 | minY = min(points, 'y') || 0, 58 | maxX = max(points, 'x') || 0, 59 | maxY = max(points, 'y') || 0, 60 | width = (maxX - minX), 61 | height = (maxY - minY) 62 | 63 | return { 64 | left: minX, 65 | top: minY, 66 | width: width, 67 | height: height 68 | } 69 | } 70 | 71 | commonRender(ctx) { 72 | let point, len = this.points.length, 73 | x = this.pathOffset.x, 74 | y = this.pathOffset.y 75 | 76 | if (!len || isNaN(this.points[len - 1].y)) { 77 | return false 78 | } 79 | ctx.beginPath() 80 | ctx.moveTo(this.points[0].x - x, this.points[0].y - y) 81 | for (let i = 0; i < len; i++) { 82 | point = this.points[i] 83 | ctx.lineTo(point.x - x, point.y - y) 84 | } 85 | return true 86 | } 87 | 88 | _render(ctx) { 89 | if (!this.commonRender(ctx)) { 90 | return; 91 | } 92 | ctx.closePath() 93 | this._renderPaintInOrder(ctx) 94 | } 95 | 96 | complexity() { 97 | return this.get('points').length 98 | } 99 | } 100 | 101 | module.exports = PolygonClass 102 | -------------------------------------------------------------------------------- /src/pattern.class.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Sugar on 2020/5/26. 3 | */ 4 | const ObjectClass = require('./shapes/object.class') 5 | const {toFixed} = require('./utils/index') 6 | const {populateWithProperties} = require('./utils/misc') 7 | 8 | class PatternClass { 9 | constructor(options) { 10 | this.repeat = 'repeat' 11 | this.offsetX = 0 12 | this.offsetY = 0 13 | this.initialize(options) 14 | } 15 | 16 | 17 | initialize(options, callback) { 18 | options || (options = {}); 19 | 20 | this.id = ObjectClass.__uid++; 21 | this.setOptions(options); 22 | if (!options.source || (options.source && typeof options.source !== 'string')) { 23 | callback && callback(this); 24 | return; 25 | } else { 26 | // img src string 27 | let _this = this; 28 | // this.source = createImage(); 29 | // loadImage(options.source, function (img, isError) { 30 | // _this.source = img; 31 | // callback && callback(_this, isError); 32 | // }, null, this.crossOrigin); 33 | } 34 | } 35 | 36 | toObject(propertiesToInclude) { 37 | let NUM_FRACTION_DIGITS = 2, 38 | source, object; 39 | 40 | // element 41 | if (typeof this.source.src === 'string') { 42 | source = this.source.src; 43 | } 44 | // element 45 | else if (typeof this.source === 'object' && this.source.toDataURL) { 46 | source = this.source.toDataURL(); 47 | } 48 | 49 | object = { 50 | type: 'pattern', 51 | source: source, 52 | repeat: this.repeat, 53 | crossOrigin: this.crossOrigin, 54 | offsetX: toFixed(this.offsetX, NUM_FRACTION_DIGITS), 55 | offsetY: toFixed(this.offsetY, NUM_FRACTION_DIGITS), 56 | patternTransform: this.patternTransform ? this.patternTransform.concat() : null 57 | }; 58 | populateWithProperties(this, object, propertiesToInclude); 59 | 60 | return object; 61 | } 62 | 63 | setOptions(options) { 64 | for (let prop in options) { 65 | this[prop] = options[prop]; 66 | } 67 | } 68 | 69 | /** 70 | * 返回CanvasPattern的实例 71 | * @param {CanvasContext} ctx 72 | * @return {CanvasPattern} 73 | */ 74 | toLive(ctx) { 75 | let source = this.source; 76 | if (!source) { 77 | return ''; 78 | } 79 | 80 | // 重复的图像源,支持代码包路径和本地临时路径 (本地路径) 81 | if (typeof source.src !== 'undefined') { 82 | if (!source.complete) { 83 | return ''; 84 | } 85 | if (source.naturalWidth === 0 || source.naturalHeight === 0) { 86 | return ''; 87 | } 88 | } 89 | 90 | /** 91 | * createPattern第二个参数 string repetition 92 | repeat 水平竖直方向都重复 93 | repeat-x 水平方向重复 94 | repeat-y 竖直方向重复 95 | no-repeat 不重复 96 | */ 97 | return ctx.createPattern(source, this.repeat); 98 | } 99 | } 100 | 101 | module.exports = PatternClass 102 | -------------------------------------------------------------------------------- /tools/checkwxss.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const through = require('through2') 3 | const Vinyl = require('vinyl') 4 | 5 | const _ = require('./utils') 6 | 7 | /** 8 | * 获取 import 列表 9 | */ 10 | function getImportList(wxss, filePath) { 11 | const reg = /@import\s+(?:(?:"([^"]+)")|(?:'([^']+)'));/ig 12 | const importList = [] 13 | let execRes = reg.exec(wxss) 14 | 15 | while (execRes && (execRes[1] || execRes[2])) { 16 | importList.push({ 17 | code: execRes[0], 18 | path: path.join(path.dirname(filePath), execRes[1] || execRes[2]), 19 | }) 20 | execRes = reg.exec(wxss) 21 | } 22 | 23 | return importList 24 | } 25 | 26 | /** 27 | * 获取 wxss 内容 28 | */ 29 | async function getContent(wxss, filePath, cwd) { 30 | let importList = [] 31 | 32 | if (wxss) { 33 | const currentImportList = getImportList(wxss, filePath) 34 | 35 | for (const item of currentImportList) { 36 | // 替换掉 import 语句,不让 less 编译 37 | wxss = wxss.replace(item.code, `/* *updated for miniprogram-custom-component* ${item.code} */`) 38 | 39 | // 处理依赖的 wxss 40 | const importWxss = await _.readFile(item.path) 41 | const importInfo = await getContent(importWxss, item.path, cwd) 42 | 43 | // 获取依赖列表 44 | importList.push(new Vinyl({ 45 | cwd, 46 | path: item.path, 47 | contents: Buffer.from(importInfo.wxss, 'utf8'), 48 | })) 49 | importList = importList.concat(importInfo.importList) 50 | } 51 | } 52 | 53 | return { 54 | wxss, 55 | importList, 56 | } 57 | } 58 | 59 | module.exports = { 60 | start() { 61 | return through.obj(function (file, enc, cb) { 62 | if (file.isBuffer()) { 63 | getContent(file.contents.toString('utf8'), file.path, file.cwd).then(res => { 64 | const {wxss, importList} = res 65 | 66 | importList.forEach(importFile => this.push(importFile)) 67 | 68 | file.contents = Buffer.from(wxss, 'utf8') 69 | this.push(file) 70 | // eslint-disable-next-line promise/no-callback-in-promise 71 | cb() 72 | }).catch(err => { 73 | // eslint-disable-next-line no-console 74 | console.warn(`deal with ${file.path} failed: ${err.stack}`) 75 | this.push(file) 76 | // eslint-disable-next-line promise/no-callback-in-promise 77 | cb() 78 | }) 79 | } else { 80 | this.push(file) 81 | cb() 82 | } 83 | }) 84 | }, 85 | 86 | end() { 87 | return through.obj(function (file, enc, cb) { 88 | if (file.isBuffer) { 89 | const reg = /\/\*\s\*updated for miniprogram-custom-component\*\s(@import\s+(?:(?:"([^"]+)")|(?:'([^"]+)'));)\s\*\//ig 90 | const wxss = file.contents.toString('utf8').replace(reg, (all, $1) => $1) 91 | 92 | file.contents = Buffer.from(wxss, 'utf8') 93 | this.push(file) 94 | cb() 95 | } else { 96 | this.push(file) 97 | cb() 98 | } 99 | }) 100 | }, 101 | } 102 | -------------------------------------------------------------------------------- /tools/checkcomponents.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const _ = require('./utils') 4 | const config = require('./config') 5 | 6 | const srcPath = config.srcPath 7 | 8 | /** 9 | * 获取 json 路径相关信息 10 | */ 11 | function getJsonPathInfo(jsonPath) { 12 | const dirPath = path.dirname(jsonPath) 13 | const fileName = path.basename(jsonPath, '.json') 14 | const relative = path.relative(srcPath, dirPath) 15 | const fileBase = path.join(relative, fileName) 16 | 17 | return { 18 | dirPath, fileName, relative, fileBase 19 | } 20 | } 21 | 22 | /** 23 | * 检测是否包含其他自定义组件 24 | */ 25 | const checkProps = ['usingComponents', 'componentGenerics'] 26 | const hasCheckMap = {} 27 | async function checkIncludedComponents(jsonPath, componentListMap) { 28 | const json = _.readJson(jsonPath) 29 | if (!json) throw new Error(`json is not valid: "${jsonPath}"`) 30 | 31 | const {dirPath, fileName, fileBase} = getJsonPathInfo(jsonPath) 32 | if (hasCheckMap[fileBase]) return 33 | hasCheckMap[fileBase] = true 34 | 35 | for (let i = 0, len = checkProps.length; i < len; i++) { 36 | const checkProp = checkProps[i] 37 | const checkPropValue = json[checkProp] || {} 38 | const keys = Object.keys(checkPropValue) 39 | 40 | for (let j = 0, jlen = keys.length; j < jlen; j++) { 41 | const key = keys[j] 42 | let value = typeof checkPropValue[key] === 'object' ? checkPropValue[key].default : checkPropValue[key] 43 | if (!value || typeof value === 'boolean') continue 44 | 45 | value = _.transformPath(value, path.sep) 46 | 47 | // 检查相对路径 48 | const componentPath = `${path.join(dirPath, value)}.json` 49 | const isExists = await _.checkFileExists(componentPath) 50 | if (isExists) { 51 | await checkIncludedComponents(componentPath, componentListMap) 52 | } 53 | } 54 | } 55 | 56 | const wholeFileBase = path.join(dirPath, fileName) 57 | let jsExt = '.js' 58 | const isJsFileExists = await _.checkFileExists(wholeFileBase + '.ts') 59 | if (isJsFileExists) { 60 | jsExt = '.ts' 61 | } 62 | 63 | // 进入存储 64 | componentListMap.wxmlFileList.push(`${fileBase}.wxml`) 65 | componentListMap.wxssFileList.push(`${fileBase}.wxss`) 66 | componentListMap.jsonFileList.push(`${fileBase}.json`) 67 | componentListMap.jsFileList.push(`${fileBase}${jsExt}`) 68 | 69 | componentListMap.jsFileMap[fileBase] = `${wholeFileBase}${jsExt}` 70 | } 71 | 72 | module.exports = async function (entry) { 73 | const componentListMap = { 74 | wxmlFileList: [], 75 | wxssFileList: [], 76 | jsonFileList: [], 77 | jsFileList: [], 78 | 79 | jsFileMap: {}, // 为 webpack entry 所用 80 | } 81 | 82 | const isExists = await _.checkFileExists(entry) 83 | if (!isExists) { 84 | const {dirPath, fileName, fileBase} = getJsonPathInfo(entry) 85 | 86 | const wholeFileBase = path.join(dirPath, fileName) 87 | let jsExt = '.js' 88 | const isJsFileExists = await _.checkFileExists(wholeFileBase + '.ts') 89 | if (isJsFileExists) { 90 | jsExt = '.ts' 91 | } 92 | componentListMap.jsFileList.push(`${fileBase}${jsExt}`) 93 | componentListMap.jsFileMap[fileBase] = `${wholeFileBase}${jsExt}` 94 | 95 | return componentListMap 96 | } 97 | 98 | await checkIncludedComponents(entry, componentListMap) 99 | 100 | return componentListMap 101 | } 102 | -------------------------------------------------------------------------------- /src/intersection.class.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Sugar on 2020/5/30. 3 | */ 4 | const PointClass = require('./point.class') 5 | 6 | class IntersectionClass { 7 | constructor(status) { 8 | this.status = status; 9 | this.points = []; 10 | } 11 | 12 | appendPoint(point) { 13 | this.points.push(point); 14 | return this; 15 | } 16 | 17 | appendPoints(points) { 18 | this.points = this.points.concat(points); 19 | return this; 20 | } 21 | } 22 | 23 | IntersectionClass.intersectLineLine = function (a1, a2, b1, b2) { 24 | let result, 25 | uaT = (b2.x - b1.x) * (a1.y - b1.y) - (b2.y - b1.y) * (a1.x - b1.x), 26 | ubT = (a2.x - a1.x) * (a1.y - b1.y) - (a2.y - a1.y) * (a1.x - b1.x), 27 | uB = (b2.y - b1.y) * (a2.x - a1.x) - (b2.x - b1.x) * (a2.y - a1.y); 28 | if (uB !== 0) { 29 | let ua = uaT / uB, 30 | ub = ubT / uB; 31 | if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) { 32 | result = new IntersectionClass('Intersection'); 33 | result.appendPoint(new PointClass(a1.x + ua * (a2.x - a1.x), a1.y + ua * (a2.y - a1.y))); 34 | } else { 35 | result = new Intersection(); 36 | } 37 | } else { 38 | if (uaT === 0 || ubT === 0) { 39 | result = new IntersectionClass('Coincident'); 40 | } else { 41 | result = new IntersectionClass('Parallel'); 42 | } 43 | } 44 | return result; 45 | } 46 | 47 | IntersectionClass.intersectLinePolygon = function (a1, a2, points) { 48 | let result = new IntersectionClass(), 49 | length = points.length, 50 | b1, b2, inter, i; 51 | 52 | for (i = 0; i < length; i++) { 53 | b1 = points[i]; 54 | b2 = points[(i + 1) % length]; 55 | inter = IntersectionClass.intersectLineLine(a1, a2, b1, b2); 56 | 57 | result.appendPoints(inter.points); 58 | } 59 | if (result.points.length > 0) { 60 | result.status = 'Intersection'; 61 | } 62 | return result; 63 | } 64 | 65 | IntersectionClass.intersectPolygonPolygon = function (points1, points2) { 66 | let result = new IntersectionClass(), 67 | length = points1.length, i; 68 | 69 | for (i = 0; i < length; i++) { 70 | let a1 = points1[i], 71 | a2 = points1[(i + 1) % length], 72 | inter = IntersectionClass.intersectLinePolygon(a1, a2, points2); 73 | 74 | result.appendPoints(inter.points); 75 | } 76 | if (result.points.length > 0) { 77 | result.status = 'Intersection'; 78 | } 79 | return result; 80 | }; 81 | 82 | IntersectionClass.intersectPolygonRectangle = function (points, r1, r2) { 83 | let min = r1.min(r2), 84 | max = r1.max(r2), 85 | topRight = new PointClass(max.x, min.y), 86 | bottomLeft = new PointClass(min.x, max.y), 87 | inter1 = IntersectionClass.intersectLinePolygon(min, topRight, points), 88 | inter2 = IntersectionClass.intersectLinePolygon(topRight, max, points), 89 | inter3 = IntersectionClass.intersectLinePolygon(max, bottomLeft, points), 90 | inter4 = IntersectionClass.intersectLinePolygon(bottomLeft, min, points), 91 | result = new IntersectionClass(); 92 | 93 | result.appendPoints(inter1.points); 94 | result.appendPoints(inter2.points); 95 | result.appendPoints(inter3.points); 96 | result.appendPoints(inter4.points); 97 | 98 | if (result.points.length > 0) { 99 | result.status = 'Intersection'; 100 | } 101 | return result; 102 | }; 103 | 104 | module.exports = IntersectionClass 105 | -------------------------------------------------------------------------------- /src/shapes/image.class.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Sugar on 2020/5/26. 3 | */ 4 | const ObjectClass = require('./object.class') 5 | const {clone} = require('../utils/object') 6 | const {loadImage} = require('../utils/misc') 7 | 8 | class ImageClass extends ObjectClass { 9 | constructor(image, options) { 10 | super(options) 11 | 12 | this.type = 'image' 13 | this.cacheKey = '' // 用于检索图像的标识key 14 | this._filterScalingX = 1 15 | this._filterScalingY = 1 16 | this.cropX = 0 17 | this.cropY = 0 18 | 19 | this.initialize(image, options) 20 | } 21 | 22 | initialize(element, options) { 23 | options || (options = {}); 24 | // this.filters = []; 25 | this.cacheKey = 'texture' + ObjectClass.__uid++; 26 | this.setElement(element, options); 27 | } 28 | 29 | getElement() { 30 | return this._element || {}; 31 | } 32 | 33 | setElement(element, options) { 34 | // this.removeTexture(this.cacheKey); 35 | // this.removeTexture(this.cacheKey + '_filtered'); 36 | this._element = element; 37 | this._originalElement = element; 38 | this._initConfig(options); 39 | // if (this.filters.length !== 0) { 40 | // this.applyFilters(); 41 | // } 42 | 43 | return this 44 | } 45 | 46 | _initConfig(options) { 47 | options || (options = {}); 48 | this.setOptions(options); 49 | this._setWidthHeight(options); 50 | } 51 | 52 | _setWidthHeight(options) { 53 | options || (options = {}); 54 | let el = this.getElement(); 55 | // el.width el.height为图像原始宽高 56 | let width = options.width || el.width || 0; 57 | let height = options.height || el.height || 0; 58 | this.width = width 59 | this.height = height 60 | } 61 | 62 | _render(ctx) { 63 | this._stroke(ctx) 64 | this._renderPaintInOrder(ctx) 65 | } 66 | 67 | _renderFill(ctx) { 68 | // console.log('绘制图片', this) 69 | let elementToDraw = this._element 70 | if (!elementToDraw) { 71 | return; 72 | } 73 | let dW = this.width, dH = this.height, 74 | // elementToDraw的width和height为图像原始宽高 75 | sW = Math.min(elementToDraw.width, dW * this._filterScalingX), 76 | sH = Math.min(elementToDraw.height, dH * this._filterScalingY), 77 | dx = -dW / 2, dy = -dH / 2, 78 | sX = Math.max(0, this.cropX * this._filterScalingX), 79 | sY = Math.max(0, this.cropY * this._filterScalingY) 80 | // console.log(sX, sY, sW, sH, dx, dy, dW, dH); 81 | elementToDraw && ctx.drawImage(elementToDraw, sX, sY, sW, sH, dx, dy, dW, dH) 82 | // elementToDraw && ctx.drawImage(elementToDraw, sX, sY, sW, sH, 0, 0, dW, dH) 83 | // elementToDraw && ctx.drawImage(elementToDraw, this.left, this.top, sW, sH) 84 | } 85 | 86 | _stroke(ctx) { 87 | if (!this.stroke || this.strokeWidth === 0) { 88 | return; 89 | } 90 | let w = this.width / 2, h = this.height / 2 91 | ctx.beginPath() 92 | ctx.moveTo(-w, -h) 93 | ctx.lineTo(w, -h) 94 | ctx.lineTo(w, h) 95 | ctx.lineTo(-w, h) 96 | ctx.lineTo(-w, -h) 97 | ctx.closePath() 98 | } 99 | 100 | /** 101 | * 图像是否应用了裁剪 102 | * @return {Boolean} 103 | */ 104 | hasCrop() { 105 | return this.cropX || this.cropY || this.width < this._element.width || this.height < this._element.height 106 | } 107 | } 108 | 109 | /** 110 | * 通过对象创建sugar.Image实例 111 | * @static 112 | * @param {Object} object 113 | * @param {Function} callback 图像创建成功后的回调 114 | */ 115 | ImageClass.fromObject = (_object, callback) => { 116 | let object = clone(_object) 117 | loadImage(object.src, (img, isError) => { 118 | if (isError) { 119 | callback && callback(null, true); 120 | return; 121 | } 122 | const image = new ImageClass(img, object) 123 | callback(image, false) 124 | }, null) 125 | } 126 | 127 | /** 128 | * 通过URL创建sugar.Image实例 129 | * @static 130 | * @param {String} url 图像URL 131 | * @param {Function} [callback] 132 | * @param {Object} [imgOptions] 133 | */ 134 | ImageClass.fromURL = (url, callback, imgOptions) => { 135 | loadImage(url, (img, isError) => { 136 | callback && callback(new ImageClass(img, imgOptions), isError); 137 | }, null) 138 | } 139 | 140 | module.exports = ImageClass 141 | -------------------------------------------------------------------------------- /src/gradient.class.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Sugar on 2020/5/26. 3 | */ 4 | const ObjectClass = require('./shapes/object.class') 5 | const ColorClass = require('color.class') 6 | const {clone} = require('./utils/object') 7 | const {populateWithProperties} = require('./utils/misc') 8 | 9 | class GradientClass { 10 | constructor(options) { 11 | this.type = 'linear' // linear 或 radial 12 | this.offsetX = 0 13 | this.offsetY = 0 14 | this.gradientTransform = null 15 | this.gradientUnits = 'pixels' 16 | this.initialize(options) 17 | } 18 | 19 | initialize(options) { 20 | options || (options = {}); 21 | options.coords || (options.coords = {}); 22 | 23 | let coords, _this = this; 24 | 25 | // sets everything, then coords and colorstops get sets again 26 | Object.keys(options).forEach(function (option) { 27 | _this[option] = options[option]; 28 | }); 29 | 30 | if (this.id) { 31 | this.id += '_' + ObjectClass.__uid++; 32 | } else { 33 | this.id = ObjectClass.__uid++; 34 | } 35 | 36 | coords = { 37 | x1: options.coords.x1 || 0, 38 | y1: options.coords.y1 || 0, 39 | x2: options.coords.x2 || 0, 40 | y2: options.coords.y2 || 0 41 | }; 42 | 43 | if (this.type === 'radial') { 44 | coords.r1 = options.coords.r1 || 0; 45 | coords.r2 = options.coords.r2 || 0; 46 | } 47 | 48 | this.coords = coords; 49 | this.colorStops = options.colorStops.slice(); 50 | } 51 | 52 | addColorStop(colorStops) { 53 | for (const position in colorStops) { 54 | var color = new ColorClass(colorStops[position]) 55 | this.colorStops.push({ 56 | offset: parseFloat(position), 57 | color: color.toRgb(), 58 | opacity: color.getAlpha() 59 | }) 60 | } 61 | return this 62 | } 63 | 64 | toObject(propertiesToInclude) { 65 | let object = { 66 | type: this.type, 67 | coords: this.coords, 68 | colorStops: this.colorStops, 69 | offsetX: this.offsetX, 70 | offsetY: this.offsetY, 71 | gradientUnits: this.gradientUnits, 72 | gradientTransform: this.gradientTransform ? this.gradientTransform.concat() : this.gradientTransform 73 | }; 74 | populateWithProperties(this, object, propertiesToInclude); 75 | 76 | return object; 77 | } 78 | 79 | /** 80 | * 返回CanvasGradient的实例 81 | * @param {CanvasContext} ctx 82 | * @return {CanvasGradient} 83 | */ 84 | toLive(ctx) { 85 | let gradient, coords = clone(this.coords), i, len; 86 | 87 | if (!this.type) { 88 | return; 89 | } 90 | 91 | if (this.type === 'linear') { 92 | gradient = ctx.createLinearGradient( 93 | coords.x1, coords.y1, coords.x2, coords.y2); 94 | } else if (this.type === 'radial') { 95 | gradient = ctx.createRadialGradient( 96 | coords.x1, coords.y1, coords.r1, coords.x2, coords.y2, coords.r2); 97 | } 98 | 99 | for (i = 0, len = this.colorStops.length; i < len; i++) { 100 | var color = this.colorStops[i].color, 101 | opacity = this.colorStops[i].opacity, 102 | offset = this.colorStops[i].offset; 103 | 104 | if (typeof opacity !== 'undefined') { 105 | color = new ColorClass(color).setAlpha(opacity).toRgba(); 106 | } 107 | gradient.addColorStop(offset, color); 108 | } 109 | 110 | return gradient; 111 | } 112 | } 113 | 114 | /** 115 | * @private 116 | */ 117 | function __convertPercentUnitsToValues(instance, options, svgOptions, gradientUnits) { 118 | var propValue, finalValue; 119 | Object.keys(options).forEach(function (prop) { 120 | propValue = options[prop]; 121 | if (propValue === 'Infinity') { 122 | finalValue = 1; 123 | } else if (propValue === '-Infinity') { 124 | finalValue = 0; 125 | } else { 126 | finalValue = parseFloat(options[prop], 10); 127 | if (typeof propValue === 'string' && /^(\d+\.\d+)%|(\d+)%$/.test(propValue)) { 128 | finalValue *= 0.01; 129 | if (gradientUnits === 'pixels') { 130 | // then we need to fix those percentages here in svg parsing 131 | if (prop === 'x1' || prop === 'x2' || prop === 'r2') { 132 | finalValue *= svgOptions.viewBoxWidth || svgOptions.width; 133 | } 134 | if (prop === 'y1' || prop === 'y2') { 135 | finalValue *= svgOptions.viewBoxHeight || svgOptions.height; 136 | } 137 | } 138 | } 139 | } 140 | options[prop] = finalValue; 141 | }); 142 | } 143 | 144 | module.exports = GradientClass 145 | -------------------------------------------------------------------------------- /tools/utils.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | // eslint-disable-next-line no-unused-vars 5 | const colors = require('colors') 6 | const through = require('through2') 7 | 8 | /** 9 | * 异步函数封装 10 | */ 11 | function wrap(func, scope) { 12 | return function (...args) { 13 | if (args.length) { 14 | const temp = args.pop() 15 | if (typeof temp !== 'function') { 16 | args.push(temp) 17 | } 18 | } 19 | 20 | return new Promise(function (resolve, reject) { 21 | args.push(function (err, data) { 22 | if (err) reject(err) 23 | else resolve(data) 24 | }) 25 | 26 | func.apply((scope || null), args) 27 | }) 28 | } 29 | } 30 | 31 | const accessSync = wrap(fs.access) 32 | const statSync = wrap(fs.stat) 33 | const renameSync = wrap(fs.rename) 34 | const mkdirSync = wrap(fs.mkdir) 35 | const readFileSync = wrap(fs.readFile) 36 | const writeFileSync = wrap(fs.writeFile) 37 | 38 | /** 39 | * 调整路径分隔符 40 | */ 41 | function transformPath(filePath, sep = '/') { 42 | return filePath.replace(/[\\/]/g, sep) 43 | } 44 | 45 | /** 46 | * 检查文件是否存在 47 | */ 48 | async function checkFileExists(filePath) { 49 | try { 50 | await accessSync(filePath) 51 | return true 52 | } catch (err) { 53 | return false 54 | } 55 | } 56 | 57 | /** 58 | * 递归创建目录 59 | */ 60 | async function recursiveMkdir(dirPath) { 61 | const prevDirPath = path.dirname(dirPath) 62 | try { 63 | await accessSync(prevDirPath) 64 | } catch (err) { 65 | // 上一级目录不存在 66 | await recursiveMkdir(prevDirPath) 67 | } 68 | 69 | try { 70 | await accessSync(dirPath) 71 | 72 | const stat = await statSync(dirPath) 73 | if (stat && !stat.isDirectory()) { 74 | // 目标路径存在,但不是目录 75 | await renameSync(dirPath, `${dirPath}.bak`) // 将此文件重命名为 .bak 后缀 76 | await mkdirSync(dirPath) 77 | } 78 | } catch (err) { 79 | // 目标路径不存在 80 | await mkdirSync(dirPath) 81 | } 82 | } 83 | 84 | /** 85 | * 读取 json 86 | */ 87 | function readJson(filePath) { 88 | try { 89 | // eslint-disable-next-line import/no-dynamic-require 90 | const content = require(filePath) 91 | delete require.cache[require.resolve(filePath)] 92 | return content 93 | } catch (err) { 94 | return null 95 | } 96 | } 97 | 98 | /** 99 | * 读取文件 100 | */ 101 | async function readFile(filePath) { 102 | try { 103 | return await readFileSync(filePath, 'utf8') 104 | } catch (err) { 105 | // eslint-disable-next-line no-console 106 | return console.error(err) 107 | } 108 | } 109 | 110 | /** 111 | * 写文件 112 | */ 113 | async function writeFile(filePath, data) { 114 | try { 115 | await recursiveMkdir(path.dirname(filePath)) 116 | return await writeFileSync(filePath, data, 'utf8') 117 | } catch (err) { 118 | // eslint-disable-next-line no-console 119 | return console.error(err) 120 | } 121 | } 122 | 123 | /** 124 | * 时间格式化 125 | */ 126 | function format(time, reg) { 127 | const date = typeof time === 'string' ? new Date(time) : time 128 | const map = {} 129 | map.yyyy = date.getFullYear() 130 | map.yy = ('' + map.yyyy).substr(2) 131 | map.M = date.getMonth() + 1 132 | map.MM = (map.M < 10 ? '0' : '') + map.M 133 | map.d = date.getDate() 134 | map.dd = (map.d < 10 ? '0' : '') + map.d 135 | map.H = date.getHours() 136 | map.HH = (map.H < 10 ? '0' : '') + map.H 137 | map.m = date.getMinutes() 138 | map.mm = (map.m < 10 ? '0' : '') + map.m 139 | map.s = date.getSeconds() 140 | map.ss = (map.s < 10 ? '0' : '') + map.s 141 | 142 | return reg.replace(/\byyyy|yy|MM|M|dd|d|HH|H|mm|m|ss|s\b/g, $1 => map[$1]) 143 | } 144 | 145 | /** 146 | * 日志插件 147 | */ 148 | function logger(action = 'copy') { 149 | return through.obj(function (file, enc, cb) { 150 | const type = path.extname(file.path).slice(1).toLowerCase() 151 | 152 | // eslint-disable-next-line no-console 153 | console.log(`[${format(new Date(), 'yyyy-MM-dd HH:mm:ss').grey}] [${action.green} ${type.green}] ${'=>'.cyan} ${file.path}`) 154 | 155 | this.push(file) 156 | cb() 157 | }) 158 | } 159 | 160 | /** 161 | * 比较数组是否相等 162 | */ 163 | function compareArray(arr1, arr2) { 164 | if (!Array.isArray(arr1) || !Array.isArray(arr2)) return false 165 | if (arr1.length !== arr2.length) return false 166 | 167 | for (let i = 0, len = arr1.length; i < len; i++) { 168 | if (arr1[i] !== arr2[i]) return false 169 | } 170 | 171 | return true 172 | } 173 | 174 | /** 175 | * 合并两个对象 176 | */ 177 | function merge(obj1, obj2) { 178 | Object.keys(obj2).forEach(key => { 179 | if (Array.isArray(obj1[key]) && Array.isArray(obj2[key])) { 180 | obj1[key] = obj1[key].concat(obj2[key]) 181 | } else if (typeof obj1[key] === 'object' && typeof obj2[key] === 'object') { 182 | obj1[key] = Object.assign(obj1[key], obj2[key]) 183 | } else { 184 | obj1[key] = obj2[key] 185 | } 186 | }) 187 | 188 | return obj1 189 | } 190 | 191 | /** 192 | * 获取 id 193 | */ 194 | let seed = +new Date() 195 | function getId() { 196 | return ++seed 197 | } 198 | 199 | module.exports = { 200 | wrap, 201 | transformPath, 202 | 203 | checkFileExists, 204 | readJson, 205 | readFile, 206 | writeFile, 207 | 208 | logger, 209 | format, 210 | compareArray, 211 | merge, 212 | getId, 213 | } 214 | -------------------------------------------------------------------------------- /src/point.class.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Sugar on 2020/5/26. 3 | */ 4 | 5 | class PointClass { 6 | constructor(x, y) { 7 | this.type = 'point' 8 | this.x = x 9 | this.y = y 10 | } 11 | 12 | /** 13 | * 向当前点添加另一点的值并返回新点 14 | * @param {sugar.Point} that 15 | * @return {sugar.Point} new PointClass 16 | */ 17 | add(that) { 18 | return new PointClass(this.x + that.x, this.y + that.y) 19 | } 20 | 21 | /** 22 | * 在当前的点添加另一点 23 | * @param {sugar.Point} that 24 | * @return {sugar.Point} thisArg 25 | * @chainable 26 | */ 27 | addEquals(that) { 28 | this.x += that.x 29 | this.y += that.y 30 | return this 31 | } 32 | 33 | /** 34 | * 在当前点加值并返回一个新的点 35 | * @param {Number} scalar 36 | * @return {sugar.Point} new PointClass 37 | */ 38 | scalarAdd(scalar) { 39 | return new PointClass(this.x + scalar, this.y + scalar) 40 | } 41 | 42 | /** 43 | * 在当前的点加值 44 | * @param {Number} scalar 45 | * @return {sugar.Point} thisArg 46 | */ 47 | scalarAddEquals(scalar) { 48 | this.x += scalar 49 | this.y += scalar 50 | return this 51 | } 52 | 53 | /** 54 | * 向该点减另一点的值并返回新点 55 | * @param {sugar.Point} that 56 | * @return {sugar.Point} new PointClass 57 | */ 58 | subtract(that) { 59 | return new PointClass(this.x - that.x, this.y - that.y) 60 | } 61 | 62 | /** 63 | * 在当前的点减值 64 | * @param {sugar.Point} that 65 | * @return {sugar.Point} thisArg 66 | * @chainable 67 | */ 68 | subtractEquals(that) { 69 | this.x -= that.x 70 | this.y -= that.y 71 | return this 72 | } 73 | 74 | /** 75 | * 向当前点减值并返回新的点 76 | * @param {Number} scalar 77 | * @return {sugar.Point} 78 | */ 79 | scalarSubtract(scalar) { 80 | return new PointClass(this.x - scalar, this.y - scalar) 81 | } 82 | 83 | /** 84 | * 当前点减值 85 | * @param {Number} scalar 86 | * @return {sugar.Point} thisArg 87 | */ 88 | scalarSubtractEquals(scalar) { 89 | this.x -= scalar 90 | this.y -= scalar 91 | return this 92 | } 93 | 94 | 95 | multiply(scalar) { 96 | return new PointClass(this.x * scalar, this.y * scalar) 97 | } 98 | 99 | multiplyEquals(scalar) { 100 | this.x *= scalar 101 | this.y *= scalar 102 | return this 103 | } 104 | 105 | divide(scalar) { 106 | return new PointClass(this.x / scalar, this.y / scalar) 107 | } 108 | 109 | divideEquals(scalar) { 110 | this.x /= scalar 111 | this.y /= scalar 112 | return this 113 | } 114 | 115 | /** 116 | * 如果此点等于另一点,则返回true 117 | * @param {sugar.Point} that 118 | * @return {Boolean} 119 | */ 120 | eq(that) { 121 | return (this.x === that.x && this.y === that.y) 122 | } 123 | 124 | /** 125 | * 如果此点小于另一点,则返回true 126 | * @param {sugar.Point} that 127 | * @return {Boolean} 128 | */ 129 | lt(that) { 130 | return (this.x < that.x && this.y < that.y) 131 | } 132 | 133 | /** 134 | * 如果此点小于或等于另一点,则返回true 135 | * @param {sugar.Point} that 136 | * @return {Boolean} 137 | */ 138 | lte(that) { 139 | return (this.x <= that.x && this.y <= that.y) 140 | } 141 | 142 | /** 143 | 144 | * 如果此点大于另一点,则返回true 145 | * @param {sugar.Point} that 146 | * @return {Boolean} 147 | */ 148 | gt(that) { 149 | return (this.x > that.x && this.y > that.y) 150 | } 151 | 152 | /** 153 | * 如果此点大于或等于另一点,则返回true 154 | * @param {sugar.Point} that 155 | * @return {Boolean} 156 | */ 157 | gte(that) { 158 | return (this.x >= that.x && this.y >= that.y) 159 | } 160 | 161 | 162 | lerp(that, t) { 163 | if (typeof t === 'undefined') { 164 | t = 0.5 165 | } 166 | t = Math.max(Math.min(1, t), 0) 167 | return new PointClass(this.x + (that.x - this.x) * t, this.y + (that.y - this.y) * t) 168 | } 169 | 170 | /** 171 | * 返回此点与另一点的距离 172 | * @param {sugar.Point} that 173 | * @return {Number} 174 | */ 175 | distanceFrom(that) { 176 | var dx = this.x - that.x, 177 | dy = this.y - that.y 178 | return Math.sqrt(dx * dx + dy * dy) 179 | } 180 | 181 | /** 182 | * 返回此点与另一点之间的点 183 | * @param {sugar.Point} that 184 | * @return {sugar.Point} 185 | */ 186 | midPointFrom(that) { 187 | return this.lerp(that) 188 | } 189 | 190 | /** 191 | * 返回一个新点,该点是该点和另一个点的最小值 192 | * @param {sugar.Point} that 193 | * @return {sugar.Point} 194 | */ 195 | min(that) { 196 | return new PointClass(Math.min(this.x, that.x), Math.min(this.y, that.y)) 197 | } 198 | 199 | /** 200 | * 返回一个新点,该点是该点和另一个点的最大值 201 | * @param {sugar.Point} that 202 | * @return {sugar.Point} 203 | */ 204 | max(that) { 205 | return new PointClass(Math.max(this.x, that.x), Math.max(this.y, that.y)) 206 | } 207 | 208 | /** 209 | * 返回此点的字符串表示形式 210 | * @return {String} 211 | */ 212 | toString() { 213 | return this.x + ',' + this.y 214 | } 215 | 216 | /** 217 | * 设置此点的x/y 218 | * @param {Number} x 219 | * @param {Number} y 220 | * @chainable 221 | */ 222 | setXY(x, y) { 223 | this.x = x 224 | this.y = y 225 | return this 226 | } 227 | 228 | /** 229 | * 设置此点的x 230 | * @param {Number} x 231 | * @chainable 232 | */ 233 | setX(x) { 234 | this.x = x 235 | return this 236 | } 237 | 238 | /** 239 | * 设置此点的y 240 | * @param {Number} y 241 | * @chainable 242 | */ 243 | setY(y) { 244 | this.y = y 245 | return this 246 | } 247 | 248 | /** 249 | * 从另一个点设置该点的x/y 250 | * @param {sugar.Point} that 251 | * @chainable 252 | */ 253 | setFromPoint(that) { 254 | this.x = that.x 255 | this.y = that.y 256 | return this 257 | } 258 | 259 | /** 260 | * 和另一个点交换x和y 261 | * @param {sugar.Point} that 262 | */ 263 | swap(that) { 264 | var x = this.x, 265 | y = this.y 266 | this.x = that.x 267 | this.y = that.y 268 | that.x = x 269 | that.y = y 270 | } 271 | 272 | /** 273 | * 返回该点的克隆实例 274 | * @return {sugar.Point} 275 | */ 276 | clone() { 277 | return new PointClass(this.x, this.y) 278 | } 279 | } 280 | 281 | module.exports = PointClass 282 | -------------------------------------------------------------------------------- /src/mixins/text_style.mixin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Sugar on 2020/5/27. 3 | */ 4 | const {extend} = require('../utils/object') 5 | 6 | module.exports = { 7 | isEmptyStyles(lineIndex) { 8 | if (!this.styles) { 9 | return true; 10 | } 11 | if (typeof lineIndex !== 'undefined' && !this.styles[lineIndex]) { 12 | return true; 13 | } 14 | let obj = typeof lineIndex === 'undefined' ? this.styles : {line: this.styles[lineIndex]}; 15 | for (let p1 in obj) { 16 | for (let p2 in obj[p1]) { 17 | for (let p3 in obj[p1][p2]) { 18 | return false; 19 | } 20 | } 21 | } 22 | return true; 23 | }, 24 | 25 | styleHas(property, lineIndex) { 26 | if (!this.styles || !property || property === '') { 27 | return false; 28 | } 29 | if (typeof lineIndex !== 'undefined' && !this.styles[lineIndex]) { 30 | return false; 31 | } 32 | let obj = typeof lineIndex === 'undefined' ? this.styles : {0: this.styles[lineIndex]}; 33 | for (let p1 in obj) { 34 | for (let p2 in obj[p1]) { 35 | if (typeof obj[p1][p2][property] !== 'undefined') { 36 | return true; 37 | } 38 | } 39 | } 40 | return false; 41 | }, 42 | 43 | cleanStyle(property) { 44 | if (!this.styles || !property || property === '') { 45 | return false; 46 | } 47 | let obj = this.styles, stylesCount = 0, letterCount, stylePropertyValue, 48 | allStyleObjectPropertiesMatch = true, graphemeCount = 0, styleObject; 49 | // eslint-disable-next-line 50 | for (let p1 in obj) { 51 | letterCount = 0; 52 | // eslint-disable-next-line 53 | for (let p2 in obj[p1]) { 54 | let styleObject = obj[p1][p2], 55 | stylePropertyHasBeenSet = styleObject.hasOwnProperty(property); 56 | 57 | stylesCount++; 58 | 59 | if (stylePropertyHasBeenSet) { 60 | if (!stylePropertyValue) { 61 | stylePropertyValue = styleObject[property]; 62 | } else if (styleObject[property] !== stylePropertyValue) { 63 | allStyleObjectPropertiesMatch = false; 64 | } 65 | 66 | if (styleObject[property] === this[property]) { 67 | delete styleObject[property]; 68 | } 69 | } else { 70 | allStyleObjectPropertiesMatch = false; 71 | } 72 | 73 | if (Object.keys(styleObject).length !== 0) { 74 | letterCount++; 75 | } else { 76 | delete obj[p1][p2]; 77 | } 78 | } 79 | 80 | if (letterCount === 0) { 81 | delete obj[p1]; 82 | } 83 | } 84 | for (let i = 0; i < this._textLines.length; i++) { 85 | graphemeCount += this._textLines[i].length; 86 | } 87 | if (allStyleObjectPropertiesMatch && stylesCount === graphemeCount) { 88 | this[property] = stylePropertyValue; 89 | this.removeStyle(property); 90 | } 91 | }, 92 | 93 | removeStyle(property) { 94 | if (!this.styles || !property || property === '') { 95 | return; 96 | } 97 | let obj = this.styles, line, lineNum, charNum; 98 | for (lineNum in obj) { 99 | line = obj[lineNum]; 100 | for (charNum in line) { 101 | delete line[charNum][property]; 102 | if (Object.keys(line[charNum]).length === 0) { 103 | delete line[charNum]; 104 | } 105 | } 106 | if (Object.keys(line).length === 0) { 107 | delete obj[lineNum]; 108 | } 109 | } 110 | }, 111 | 112 | _extendStyles(index, styles) { 113 | let loc = this.get2DCursorLocation(index); 114 | 115 | if (!this._getLineStyle(loc.lineIndex)) { 116 | this._setLineStyle(loc.lineIndex); 117 | } 118 | 119 | if (!this._getStyleDeclaration(loc.lineIndex, loc.charIndex)) { 120 | this._setStyleDeclaration(loc.lineIndex, loc.charIndex, {}); 121 | } 122 | 123 | extend(this._getStyleDeclaration(loc.lineIndex, loc.charIndex), styles); 124 | }, 125 | 126 | get2DCursorLocation(selectionStart, skipWrapping) { 127 | if (typeof selectionStart === 'undefined') { 128 | selectionStart = this.selectionStart; 129 | } 130 | let lines = skipWrapping ? this._unwrappedTextLines : this._textLines, 131 | len = lines.length; 132 | for (let i = 0; i < len; i++) { 133 | if (selectionStart <= lines[i].length) { 134 | return { 135 | lineIndex: i, 136 | charIndex: selectionStart 137 | }; 138 | } 139 | selectionStart -= lines[i].length + this.missingNewlineOffset(i); 140 | } 141 | return { 142 | lineIndex: i - 1, 143 | charIndex: lines[i - 1].length < selectionStart ? lines[i - 1].length : selectionStart 144 | }; 145 | }, 146 | 147 | getSelectionStyles(startIndex, endIndex, complete) { 148 | if (typeof startIndex === 'undefined') { 149 | startIndex = this.selectionStart || 0; 150 | } 151 | if (typeof endIndex === 'undefined') { 152 | endIndex = this.selectionEnd || startIndex; 153 | } 154 | let styles = []; 155 | for (let i = startIndex; i < endIndex; i++) { 156 | styles.push(this.getStyleAtPosition(i, complete)); 157 | } 158 | return styles; 159 | }, 160 | 161 | getStyleAtPosition(position, complete) { 162 | let loc = this.get2DCursorLocation(position), 163 | style = complete ? this.getCompleteStyleDeclaration(loc.lineIndex, loc.charIndex) : 164 | this._getStyleDeclaration(loc.lineIndex, loc.charIndex); 165 | return style || {}; 166 | }, 167 | 168 | setSelectionStyles(styles, startIndex, endIndex) { 169 | if (typeof startIndex === 'undefined') { 170 | startIndex = this.selectionStart || 0; 171 | } 172 | if (typeof endIndex === 'undefined') { 173 | endIndex = this.selectionEnd || startIndex; 174 | } 175 | for (let i = startIndex; i < endIndex; i++) { 176 | this._extendStyles(i, styles); 177 | } 178 | this._forceClearCache = true; 179 | return this; 180 | }, 181 | 182 | _getStyleDeclaration(lineIndex, charIndex) { 183 | let lineStyle = this.styles && this.styles[lineIndex]; 184 | if (!lineStyle) { 185 | return null; 186 | } 187 | return lineStyle[charIndex]; 188 | }, 189 | 190 | getCompleteStyleDeclaration(lineIndex, charIndex) { 191 | let style = this._getStyleDeclaration(lineIndex, charIndex) || {}, 192 | styleObject = {}, prop; 193 | for (let i = 0; i < this._styleProperties.length; i++) { 194 | prop = this._styleProperties[i]; 195 | styleObject[prop] = typeof style[prop] === 'undefined' ? this[prop] : style[prop]; 196 | } 197 | return styleObject; 198 | }, 199 | 200 | _setStyleDeclaration(lineIndex, charIndex, style) { 201 | this.styles[lineIndex][charIndex] = style; 202 | }, 203 | 204 | _deleteStyleDeclaration(lineIndex, charIndex) { 205 | delete this.styles[lineIndex][charIndex]; 206 | }, 207 | 208 | _getLineStyle(lineIndex) { 209 | return !!this.styles[lineIndex]; 210 | }, 211 | 212 | _setLineStyle(lineIndex) { 213 | this.styles[lineIndex] = {}; 214 | }, 215 | 216 | _deleteLineStyle(lineIndex) { 217 | delete this.styles[lineIndex]; 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/mixins/object_origin.mixin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Sugar on 2020/5/30. 3 | */ 4 | const PointClass = require('../point.class') 5 | const {degreesToRadians, cos, sin, rotatePoint} = require('../utils/misc') 6 | const originXOffset = { 7 | left: -0.5, 8 | center: 0, 9 | right: 0.5 10 | } 11 | const originYOffset = { 12 | top: -0.5, 13 | center: 0, 14 | bottom: 0.5 15 | } 16 | 17 | module.exports = { 18 | translateToGivenOrigin: function (point, fromOriginX, fromOriginY, toOriginX, toOriginY) { 19 | let x = point.x, 20 | y = point.y, 21 | offsetX, offsetY, dim; 22 | 23 | if (typeof fromOriginX === 'string') { 24 | fromOriginX = originXOffset[fromOriginX]; 25 | } else { 26 | fromOriginX -= 0.5; 27 | } 28 | 29 | if (typeof toOriginX === 'string') { 30 | toOriginX = originXOffset[toOriginX]; 31 | } else { 32 | toOriginX -= 0.5; 33 | } 34 | 35 | offsetX = toOriginX - fromOriginX; 36 | 37 | if (typeof fromOriginY === 'string') { 38 | fromOriginY = originYOffset[fromOriginY]; 39 | } else { 40 | fromOriginY -= 0.5; 41 | } 42 | 43 | if (typeof toOriginY === 'string') { 44 | toOriginY = originYOffset[toOriginY]; 45 | } else { 46 | toOriginY -= 0.5; 47 | } 48 | 49 | offsetY = toOriginY - fromOriginY; 50 | 51 | if (offsetX || offsetY) { 52 | dim = this._getTransformedDimensions(); 53 | x = point.x + offsetX * dim.x; 54 | y = point.y + offsetY * dim.y; 55 | } 56 | 57 | return new PointClass(x, y); 58 | }, 59 | 60 | 61 | translateToCenterPoint: function (point, originX, originY) { 62 | let p = this.translateToGivenOrigin(point, originX, originY, 'center', 'center'); 63 | if (this.angle) { 64 | return rotatePoint(p, point, degreesToRadians(this.angle)); 65 | } 66 | return p; 67 | }, 68 | 69 | /** 70 | * Translates the coordinates from center to origin coordinates (based on the object's dimensions) 71 | * @param {PointClass} center The point which corresponds to center of the object 72 | * @param {String} originX Horizontal origin: 'left', 'center' or 'right' 73 | * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' 74 | * @return {PointClass} 75 | */ 76 | translateToOriginPoint: function (center, originX, originY) { 77 | let p = this.translateToGivenOrigin(center, 'center', 'center', originX, originY); 78 | if (this.angle) { 79 | return rotatePoint(p, center, degreesToRadians(this.angle)); 80 | } 81 | return p; 82 | }, 83 | 84 | /** 85 | * Returns the real center coordinates of the object 86 | * @return {PointClass} 87 | */ 88 | getCenterPoint: function () { 89 | let leftTop = new PointClass(this.left, this.top); 90 | return this.translateToCenterPoint(leftTop, this.originX, this.originY); 91 | }, 92 | 93 | /** 94 | * Returns the coordinates of the object based on center coordinates 95 | * @param {PointClass} point The point which corresponds to the originX and originY params 96 | * @return {PointClass} 97 | */ 98 | // getOriginPoint: function(center) { 99 | // return this.translateToOriginPoint(center, this.originX, this.originY); 100 | // }, 101 | 102 | /** 103 | * Returns the coordinates of the object as if it has a different origin 104 | * @param {String} originX Horizontal origin: 'left', 'center' or 'right' 105 | * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' 106 | * @return {PointClass} 107 | */ 108 | getPointByOrigin: function (originX, originY) { 109 | let center = this.getCenterPoint(); 110 | return this.translateToOriginPoint(center, originX, originY); 111 | }, 112 | 113 | /** 114 | * Returns the point in local coordinates 115 | * @param {PointClass} point The point relative to the global coordinate system 116 | * @param {String} originX Horizontal origin: 'left', 'center' or 'right' 117 | * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' 118 | * @return {PointClass} 119 | */ 120 | toLocalPoint: function (point, originX, originY) { 121 | let center = this.getCenterPoint(), 122 | p, p2; 123 | 124 | if (typeof originX !== 'undefined' && typeof originY !== 'undefined') { 125 | p = this.translateToGivenOrigin(center, 'center', 'center', originX, originY); 126 | } else { 127 | p = new PointClass(this.left, this.top); 128 | } 129 | 130 | p2 = new PointClass(point.x, point.y); 131 | if (this.angle) { 132 | p2 = rotatePoint(p2, center, -degreesToRadians(this.angle)); 133 | } 134 | return p2.subtractEquals(p); 135 | }, 136 | 137 | /** 138 | * Returns the point in global coordinates 139 | * @param {PointClass} The point relative to the local coordinate system 140 | * @return {PointClass} 141 | */ 142 | // toGlobalPoint: function(point) { 143 | // return rotatePoint(point, this.getCenterPoint(), degreesToRadians(this.angle)).addEquals(new PointClass(this.left, this.top)); 144 | // }, 145 | 146 | /** 147 | * Sets the position of the object taking into consideration the object's origin 148 | * @param {PointClass} pos The new position of the object 149 | * @param {String} originX Horizontal origin: 'left', 'center' or 'right' 150 | * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' 151 | * @return {void} 152 | */ 153 | setPositionByOrigin: function (pos, originX, originY) { 154 | let center = this.translateToCenterPoint(pos, originX, originY), 155 | position = this.translateToOriginPoint(center, this.originX, this.originY); 156 | this.set('left', position.x); 157 | this.set('top', position.y); 158 | }, 159 | 160 | /** 161 | * @param {String} to One of 'left', 'center', 'right' 162 | */ 163 | adjustPosition: function (to) { 164 | let angle = degreesToRadians(this.angle), 165 | hypotFull = this.getScaledWidth(), 166 | xFull = cos(angle) * hypotFull, 167 | yFull = sin(angle) * hypotFull, 168 | offsetFrom, offsetTo; 169 | 170 | //TODO: this function does not consider mixed situation like top, center. 171 | if (typeof this.originX === 'string') { 172 | offsetFrom = originXOffset[this.originX]; 173 | } else { 174 | offsetFrom = this.originX - 0.5; 175 | } 176 | if (typeof to === 'string') { 177 | offsetTo = originXOffset[to]; 178 | } else { 179 | offsetTo = to - 0.5; 180 | } 181 | this.left += xFull * (offsetTo - offsetFrom); 182 | this.top += yFull * (offsetTo - offsetFrom); 183 | this.setCoords(); 184 | this.originX = to; 185 | }, 186 | 187 | /** 188 | * Sets the origin/position of the object to it's center point 189 | * @private 190 | * @return {void} 191 | */ 192 | _setOriginToCenter: function () { 193 | this._originalOriginX = this.originX; 194 | this._originalOriginY = this.originY; 195 | 196 | let center = this.getCenterPoint(); 197 | 198 | this.originX = 'center'; 199 | this.originY = 'center'; 200 | 201 | this.left = center.x; 202 | this.top = center.y; 203 | }, 204 | 205 | /** 206 | * Resets the origin/position of the object to it's original origin 207 | * @private 208 | * @return {void} 209 | */ 210 | _resetOrigin: function () { 211 | let originPoint = this.translateToOriginPoint( 212 | this.getCenterPoint(), 213 | this._originalOriginX, 214 | this._originalOriginY); 215 | 216 | this.originX = this._originalOriginX; 217 | this.originY = this._originalOriginY; 218 | 219 | this.left = originPoint.x; 220 | this.top = originPoint.y; 221 | 222 | this._originalOriginX = null; 223 | this._originalOriginY = null; 224 | }, 225 | 226 | /** 227 | * @private 228 | */ 229 | _getLeftTopCoords: function () { 230 | return this.translateToOriginPoint(this.getCenterPoint(), 'left', 'top'); 231 | }, 232 | } 233 | -------------------------------------------------------------------------------- /tools/demo/pages/index/index.js: -------------------------------------------------------------------------------- 1 | const sugar = require('../../components/sugarjs') 2 | const {windowWidth} = wx.getSystemInfoSync() 3 | 4 | const randomNumInRange = (min, max) => { 5 | let range = max - min 6 | return Math.round(Math.random() * range) + min 7 | } 8 | 9 | Page({ 10 | data: { 11 | width: windowWidth, 12 | height: 500, 13 | selectObj: null, 14 | dataUrl: null, 15 | }, 16 | onReady() { 17 | // this.sugar = createCanvasSugarJS({ 18 | // width: this.data.width, 19 | // height: this.data.height, 20 | // context: wx.createCanvasContext('sugarjs') 21 | // }) 22 | 23 | // const ctx = wx.createCanvasContext('sugarjs') 24 | // this.canvas = new sugar.Canvas(ctx) 25 | 26 | const query = wx.createSelectorQuery() 27 | query.select(`#sugarjs`) 28 | .fields({node: true, size: true}) 29 | .exec(res => { 30 | const canvas = res[0].node 31 | this.sugar = new sugar.Canvas({ 32 | canvas: canvas, 33 | width: this.data.width, 34 | height: this.data.height, 35 | backgroundColor: 'skyblue' 36 | }) 37 | // this.sugar.preserveObjectStacking = true // 禁止选中图层时自动置于顶部 38 | 39 | console.log('sugar.Canvas初始化', this.sugar) 40 | // this.sugar.setBackgroundImage('http://g.hiphotos.baidu.com/zhidao/pic/item/6a600c338744ebf844eebc72d9f9d72a6159a7e4.jpg', this.sugar.renderAll.bind(this.sugar), { 41 | // width: this.data.width, 42 | // height: this.data.height 43 | // }) 44 | 45 | sugar.Image.fromURL('https://desk-fd.zol-img.com.cn/t_s1024x768c5/g5/M00/02/09/ChMkJlbKzvWIBEvXABxtIglgbHoAALJQAOMI70AHG06059.jpg', (img) => { 46 | img.set({ 47 | scaleX: this.data.width / img.width, 48 | scaleY: this.data.height / img.height 49 | }) 50 | this.sugar.setBackgroundImage(img, this.sugar.renderAll.bind(this.sugar)) 51 | }) 52 | 53 | this.sugar.on('selection:created', (e) => { 54 | console.log('触发canvas事件selection:created', e.target) 55 | // this.setData({selectObj: e.target}) 56 | }) 57 | this.sugar.on('selection:updated', (e) => { 58 | console.log('触发canvas事件selection:updated', e.target) 59 | // this.setData({selectObj: e.target}) 60 | }) 61 | this.sugar.on('selection:cleared', (e) => { 62 | console.log('触发canvas事件selection:cleared') 63 | // this.setData({selectObj: null}) 64 | }) 65 | this.sugar.on('object:added', (e) => { 66 | console.log('触发canvas事件object:added', e) 67 | }) 68 | this.sugar.on('object:removed', (e) => { 69 | console.log('触发canvas事件object:removed', e) 70 | }) 71 | }) 72 | }, 73 | addText() { 74 | const text = new sugar.Text('Sugar苏\n换行', { 75 | left: randomNumInRange(0, 200), 76 | top: randomNumInRange(0, 400), 77 | fontSize: 50, 78 | fill: 'yellow' 79 | }) 80 | this.sugar.add(text).setActiveObject(text) 81 | }, 82 | addImage1() { 83 | sugar.Image.fromURL('https://sugars.oss-cn-shenzhen.aliyuncs.com/diy/decorate/decorate1.png', (img) => { 84 | img.set({ 85 | left: randomNumInRange(0, this.data.width - 80), 86 | top: randomNumInRange(0, this.data.height - 80), 87 | }) 88 | this.sugar.add(img).setActiveObject(img) 89 | }) 90 | }, 91 | addImage2() { 92 | sugar.Image.fromURL('https://sugars.oss-cn-shenzhen.aliyuncs.com/diy/decorate/decorate9.png', (img) => { 93 | img.set({ 94 | left: randomNumInRange(0, this.data.width - 202), 95 | top: randomNumInRange(0, this.data.height - 170), 96 | }) 97 | this.sugar.add(img).setActiveObject(img) 98 | }) 99 | }, 100 | selectImage() { 101 | wx.chooseImage({ 102 | count: 1, 103 | sizeType: ['original', 'compressed'], 104 | sourceType: ['album', 'camera'], 105 | success: (res) => { 106 | const tempFilePath = res.tempFilePaths[0] 107 | sugar.Image.fromURL(tempFilePath, (img) => { 108 | img.set({ 109 | scaleX: 0.5, 110 | scaleY: 0.5, 111 | left: 0, 112 | top: 0 113 | }) 114 | this.sugar.add(img).setActiveObject(img) 115 | }) 116 | } 117 | }) 118 | }, 119 | getCanvasObject() { 120 | console.log(this.sugar) 121 | }, 122 | rotate() { 123 | const activeObject = this.sugar.getActiveObject() 124 | if (!activeObject) return 125 | activeObject.rotate(activeObject.angle === 360 ? 90 : activeObject.angle + 90) 126 | this.sugar.renderAll() 127 | }, 128 | flip() { 129 | const activeObject = this.sugar.getActiveObject() 130 | if (!activeObject) return 131 | activeObject.set({ 132 | scaleX: -activeObject.scaleX 133 | }) 134 | this.sugar.renderAll() 135 | }, 136 | zoomUp() { 137 | const activeObject = this.sugar.getActiveObject() 138 | if (!activeObject) return 139 | activeObject.set({ 140 | scaleX: activeObject.scaleX * 1.1, 141 | scaleY: activeObject.scaleY * 1.1 142 | }) 143 | this.sugar.renderAll() 144 | }, 145 | zoomOut() { 146 | const activeObject = this.sugar.getActiveObject() 147 | if (!activeObject) return 148 | activeObject.set({ 149 | scaleX: activeObject.scaleX * 0.9, 150 | scaleY: activeObject.scaleY * 0.9 151 | }) 152 | this.sugar.renderAll() 153 | }, 154 | addRect() { 155 | const rect = new sugar.Rect({ 156 | left: 100, 157 | top: 100, 158 | fill: 'green', 159 | width: 100, 160 | height: 100 161 | }) 162 | 163 | this.sugar.add(rect) 164 | }, 165 | addPolygon() { 166 | const points = [{ 167 | x: 3, y: 4 168 | }, { 169 | x: 16, y: 3 170 | }, { 171 | x: 30, y: 5 172 | }, { 173 | x: 25, y: 55 174 | }, { 175 | x: 19, y: 44 176 | }, { 177 | x: 15, y: 30 178 | }, { 179 | x: 15, y: 55 180 | }, { 181 | x: 9, y: 55 182 | }, { 183 | x: 6, y: 53 184 | }, { 185 | x: -2, y: 55 186 | }, { 187 | x: -4, y: 40 188 | }, { 189 | x: 0, y: 20 190 | }] 191 | const polygon = new sugar.Polygon(points, { 192 | left: 100, 193 | top: 50, 194 | // scaleX: 4, 195 | // scaleY: 4, 196 | fill: '#D81B60' 197 | }) 198 | 199 | this.sugar.add(polygon) 200 | }, 201 | addTriangle() { 202 | const triangle = new sugar.Triangle({ 203 | left: 55, 204 | top: 100, 205 | fill: 'pink', 206 | width: 80, 207 | height: 120 208 | }) 209 | 210 | this.sugar.add(triangle) 211 | }, 212 | addCircle() { 213 | const circle = new sugar.Circle({ 214 | fill: '#0c32a0', 215 | radius: 50, 216 | top: 200, 217 | left: 200 218 | }) 219 | 220 | this.sugar.add(circle) 221 | }, 222 | addEllipse() { 223 | const ellipse = new sugar.Ellipse({ 224 | fill: '#440250', 225 | rx: 50, 226 | ry: 100, 227 | top: 166, 228 | left: 166 229 | }) 230 | 231 | this.sugar.add(ellipse) 232 | }, 233 | deleteObject() { 234 | const activeObject = this.sugar.getActiveObject() 235 | if (!activeObject) return 236 | this.sugar.remove(activeObject) 237 | this.sugar.renderAll() 238 | }, 239 | toDataURL() { 240 | this.setData({ 241 | dataUrl: this.sugar.toDataURL() 242 | }) 243 | }, 244 | preview() { 245 | wx.previewImage({ 246 | urls: [this.data.dataUrl] 247 | }) 248 | }, 249 | touchstart(e) { 250 | this.sugar.touchstart(e) 251 | // console.log('小程序touchstart', e) 252 | }, 253 | touchmove(e) { 254 | this.sugar.touchmove(e) 255 | // console.log('小程序touchmove', e) 256 | }, 257 | touchend(e) { 258 | this.sugar.touchend(e) 259 | // console.log('小程序touchend', e) 260 | }, 261 | touchcancel(e) { 262 | // console.log('小程序touchcancel', e) 263 | }, 264 | longtap(e) { 265 | this.sugar.longtap(e) 266 | // console.log('小程序longtap', e) 267 | }, 268 | }) 269 | -------------------------------------------------------------------------------- /src/mixins/object_interactivity.mixin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Sugar on 2020/5/30. 3 | */ 4 | 5 | const {degreesToRadians, cos, sin, sizeAfterTransform} = require('../utils/misc') 6 | 7 | module.exports = { 8 | /** 9 | * 确定点击哪个角 10 | * @private 11 | * @param {Object} pointer 触摸指针 12 | * @return {String|Boolean} 13 | */ 14 | _findTargetCorner: function (pointer, forTouch) { 15 | if (!this.hasControls || this.group || (!this.canvas || this.canvas._activeObject !== this)) { 16 | return false; 17 | } 18 | 19 | let ex = pointer.x, 20 | ey = pointer.y, 21 | xPoints, 22 | lines, keys = Object.keys(this.oCoords), 23 | j = keys.length - 1, i; 24 | this.__corner = 0; 25 | 26 | for (; j >= 0; j--) { 27 | i = keys[j]; 28 | if (!this.isControlVisible(i)) { 29 | continue; 30 | } 31 | 32 | lines = this._getImageLines(forTouch ? this.oCoords[i].touchCorner : this.oCoords[i].corner); 33 | 34 | xPoints = this._findCrossPoints({x: ex, y: ey}, lines); 35 | if (xPoints !== 0 && xPoints % 2 === 1) { 36 | this.__corner = i; 37 | return i; 38 | } 39 | } 40 | return false; 41 | }, 42 | 43 | forEachControl: function (fn) { 44 | for (let i in this.controls) { 45 | fn(this.controls[i], i, this); 46 | } 47 | }, 48 | 49 | /** 50 | * @private 51 | */ 52 | _setCornerCoords: function () { 53 | let coords = this.oCoords, 54 | newTheta = degreesToRadians(45 - this.angle), 55 | cosTheta = cos(newTheta), 56 | sinTheta = sin(newTheta), 57 | /* Math.sqrt(2 * Math.pow(this.cornerSize, 2)) / 2, */ 58 | /* 0.707106 stands for sqrt(2)/2 */ 59 | cornerHypotenuse = this.cornerSize * 0.707106, 60 | touchHypotenuse = this.touchCornerSize * 0.707106, 61 | cosHalfOffset = cornerHypotenuse * cosTheta, 62 | sinHalfOffset = cornerHypotenuse * sinTheta, 63 | touchCosHalfOffset = touchHypotenuse * cosTheta, 64 | touchSinHalfOffset = touchHypotenuse * sinTheta, 65 | x, y; 66 | 67 | for (let control in coords) { 68 | x = coords[control].x; 69 | y = coords[control].y; 70 | coords[control].corner = { 71 | tl: { 72 | x: x - sinHalfOffset, 73 | y: y - cosHalfOffset 74 | }, 75 | tr: { 76 | x: x + cosHalfOffset, 77 | y: y - sinHalfOffset 78 | }, 79 | bl: { 80 | x: x - cosHalfOffset, 81 | y: y + sinHalfOffset 82 | }, 83 | br: { 84 | x: x + sinHalfOffset, 85 | y: y + cosHalfOffset 86 | } 87 | }; 88 | coords[control].touchCorner = { 89 | tl: { 90 | x: x - touchSinHalfOffset, 91 | y: y - touchCosHalfOffset 92 | }, 93 | tr: { 94 | x: x + touchCosHalfOffset, 95 | y: y - touchSinHalfOffset 96 | }, 97 | bl: { 98 | x: x - touchCosHalfOffset, 99 | y: y + touchSinHalfOffset 100 | }, 101 | br: { 102 | x: x + touchSinHalfOffset, 103 | y: y + touchCosHalfOffset 104 | } 105 | }; 106 | } 107 | }, 108 | 109 | drawSelectionBackground: function (ctx) { 110 | if (!this.selectionBackgroundColor || 111 | (this.canvas && !this.canvas.interactive) || 112 | (this.canvas && this.canvas._activeObject !== this) 113 | ) { 114 | return this; 115 | } 116 | ctx.save(); 117 | let center = this.getCenterPoint(), wh = this._calculateCurrentDimensions(), 118 | vpt = this.canvas.viewportTransform; 119 | ctx.translate(center.x, center.y); 120 | ctx.scale(1 / vpt[0], 1 / vpt[3]); 121 | ctx.rotate(degreesToRadians(this.angle)); 122 | ctx.fillStyle = this.selectionBackgroundColor; 123 | ctx.fillRect(-wh.x / 2, -wh.y / 2, wh.x, wh.y); 124 | ctx.restore(); 125 | return this; 126 | }, 127 | 128 | drawBorders: function (ctx, styleOverride) { 129 | styleOverride = styleOverride || {}; 130 | let wh = this._calculateCurrentDimensions(), 131 | strokeWidth = this.borderScaleFactor, 132 | width = wh.x + strokeWidth, 133 | height = wh.y + strokeWidth, 134 | hasControls = typeof styleOverride.hasControls !== 'undefined' ? 135 | styleOverride.hasControls : this.hasControls, 136 | shouldStroke = false; 137 | 138 | ctx.save(); 139 | ctx.strokeStyle = styleOverride.borderColor || this.borderColor; 140 | this._setLineDash(ctx, styleOverride.borderDashArray || this.borderDashArray, null); 141 | // console.log('---------drawBorders', this.left, this.top) 142 | ctx.strokeRect( 143 | -width / 2, 144 | -height / 2, 145 | // this.left, 146 | // this.top, 147 | width, 148 | height 149 | ); 150 | 151 | if (hasControls) { 152 | ctx.beginPath(); 153 | this.forEachControl(function (control, key, object) { 154 | if (control.withConnection && control.getVisibility(object, key)) { 155 | // reset movement for each control 156 | shouldStroke = true; 157 | ctx.moveTo(control.x * width, control.y * height); 158 | ctx.lineTo( 159 | control.x * width + control.offsetX, 160 | control.y * height + control.offsetY 161 | ); 162 | } 163 | }); 164 | if (shouldStroke) { 165 | ctx.stroke(); 166 | } 167 | } 168 | ctx.restore(); 169 | return this; 170 | }, 171 | 172 | drawBordersInGroup: function (ctx, options, styleOverride) { 173 | styleOverride = styleOverride || {}; 174 | var bbox = sizeAfterTransform(this.width, this.height, options), 175 | strokeWidth = this.strokeWidth, 176 | strokeUniform = this.strokeUniform, 177 | borderScaleFactor = this.borderScaleFactor, 178 | width = 179 | bbox.x + strokeWidth * (strokeUniform ? this.canvas.getZoom() : options.scaleX) + borderScaleFactor, 180 | height = 181 | bbox.y + strokeWidth * (strokeUniform ? this.canvas.getZoom() : options.scaleY) + borderScaleFactor; 182 | ctx.save(); 183 | this._setLineDash(ctx, styleOverride.borderDashArray || this.borderDashArray, null); 184 | ctx.strokeStyle = styleOverride.borderColor || this.borderColor; 185 | ctx.strokeRect( 186 | -width / 2, 187 | -height / 2, 188 | // this.left, 189 | // this.top, 190 | width, 191 | height 192 | ); 193 | 194 | ctx.restore(); 195 | return this; 196 | }, 197 | 198 | drawControls: function (ctx, styleOverride) { 199 | styleOverride = styleOverride || {}; 200 | ctx.save(); 201 | ctx.setTransform(this.canvas.getRetinaScaling(), 0, 0, this.canvas.getRetinaScaling(), 0, 0); 202 | ctx.strokeStyle = ctx.fillStyle = styleOverride.cornerColor || this.cornerColor; 203 | if (!this.transparentCorners) { 204 | ctx.strokeStyle = styleOverride.cornerStrokeColor || this.cornerStrokeColor; 205 | } 206 | this._setLineDash(ctx, styleOverride.cornerDashArray || this.cornerDashArray, null); 207 | this.setCoords(); 208 | this.forEachControl(function (control, key, object) { 209 | if (control.getVisibility(object, key)) { 210 | control.render(ctx, 211 | object.oCoords[key].x, 212 | object.oCoords[key].y, styleOverride, object); 213 | } 214 | }); 215 | ctx.restore(); 216 | 217 | return this; 218 | }, 219 | 220 | isControlVisible: function (controlKey) { 221 | return this.controls[controlKey] && this.controls[controlKey].getVisibility(this, controlKey); 222 | }, 223 | 224 | setControlVisible: function (controlKey, visible) { 225 | if (!this._controlsVisibility) { 226 | this._controlsVisibility = {}; 227 | } 228 | this._controlsVisibility[controlKey] = visible; 229 | return this; 230 | }, 231 | 232 | setControlsVisibility: function (options) { 233 | options || (options = {}); 234 | 235 | for (let p in options) { 236 | this.setControlVisible(p, options[p]); 237 | } 238 | return this; 239 | }, 240 | 241 | 242 | onDeselect: function () { 243 | }, 244 | 245 | onSelect: function () { 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/utils/misc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Sugar on 2020/5/26. 3 | */ 4 | const PointClass = require('../point.class') 5 | const {min, max} = require('./index') 6 | 7 | let sqrt = Math.sqrt, 8 | atan2 = Math.atan2, 9 | pow = Math.pow, 10 | PiBy180 = Math.PI / 180, 11 | PiBy2 = Math.PI / 2 12 | 13 | /** 14 | * 逆向变换t 15 | * @param {Array} t 要变换的数据 16 | * @return {Array} 变换后的数据 17 | */ 18 | const invertTransform = (t) => { 19 | let a = 1 / (t[0] * t[3] - t[1] * t[2]), 20 | r = [a * t[3], -a * t[1], -a * t[2], a * t[0]], 21 | o = transformPoint({x: t[4], y: t[5]}, r, true) 22 | r[4] = -o.x 23 | r[5] = -o.y 24 | return r 25 | } 26 | 27 | /** 28 | * 将变换t应用于点p 29 | * @static 30 | * @param {sugar.Point} p 转变点 31 | * @param {Array} t 矩阵 32 | * @param {Boolean} [ignoreOffset] 指定是否不偏移 33 | * @return {sugar.Point} 转换后的点 34 | */ 35 | const transformPoint = (p, t, ignoreOffset) => { 36 | if (ignoreOffset) { 37 | return new PointClass( 38 | t[0] * p.x + t[2] * p.y, 39 | t[1] * p.x + t[3] * p.y 40 | ) 41 | } 42 | return new PointClass( 43 | t[0] * p.x + t[2] * p.y + t[4], 44 | t[1] * p.x + t[3] * p.y + t[5] 45 | ) 46 | } 47 | 48 | /** 49 | * 用另一个对象的属性填充一个对象 50 | * @static 51 | * @param {Object} source 源对象 52 | * @param {Object} destination 目标对象 53 | * @return {Array} properties 要包括的属性名称 54 | */ 55 | const populateWithProperties = (source, destination, properties) => { 56 | if (properties && Object.prototype.toString.call(properties) === '[object Array]') { 57 | for (let i = 0, len = properties.length; i < len; i++) { 58 | if (properties[i] in source) { 59 | destination[properties[i]] = source[properties[i]]; 60 | } 61 | } 62 | } 63 | } 64 | 65 | 66 | /** 67 | * 获取图片。网络图片需先配置download域名才能生效。 68 | * @param {String} url 69 | * @param {Function} callback 70 | * @param {*} [context] 调用回调的上下文 71 | */ 72 | const loadImage = (url, callback, context) => { 73 | if (!url) { 74 | callback && callback.call(context, url); 75 | return; 76 | } 77 | 78 | const query = wx.createSelectorQuery() 79 | query.select(`#sugarjs`) 80 | .fields({node: true, size: true}) 81 | .exec(res => { 82 | const canvas = res[0].node 83 | wx.getImageInfo({ 84 | src: url, 85 | success(res) { 86 | /** 87 | * res数据结构 88 | width number 图片原始宽度,单位px。不考虑旋转。 89 | height number 图片原始高度,单位px。不考虑旋转。 90 | path string 图片的本地路径 91 | orientation string 拍照时设备方向 92 | type string 图片格式 93 | */ 94 | let img = canvas.createImage() 95 | img.src = res.path 96 | img.onload = (res) => { 97 | callback && callback.call(context, img, false) 98 | img = img.onload = img.onerror = null 99 | } 100 | 101 | img.onerror = () => { 102 | callback && callback.call(context, null, true) 103 | img = img.onload = img.onerror = null 104 | } 105 | 106 | // callback && callback.call(context, res, false) 107 | }, 108 | fail(err) { 109 | callback && callback.call(context, null, true) 110 | } 111 | }) 112 | }) 113 | } 114 | 115 | const degreesToRadians = (degrees) => { 116 | return degrees * PiBy180 117 | } 118 | 119 | const cos = (angle) => { 120 | if (angle === 0) { 121 | return 1; 122 | } 123 | if (angle < 0) { 124 | // cos(a) = cos(-a) 125 | angle = -angle; 126 | } 127 | let angleSlice = angle / PiBy2; 128 | switch (angleSlice) { 129 | case 1: 130 | case 3: 131 | return 0; 132 | case 2: 133 | return -1; 134 | } 135 | return Math.cos(angle); 136 | } 137 | 138 | const sin = (angle) => { 139 | if (angle === 0) { 140 | return 0; 141 | } 142 | let angleSlice = angle / PiBy2, sign = 1; 143 | if (angle < 0) { 144 | // sin(-a) = -sin(a) 145 | sign = -1; 146 | } 147 | switch (angleSlice) { 148 | case 1: 149 | return sign; 150 | case 2: 151 | return 0; 152 | case 3: 153 | return -sign; 154 | } 155 | return Math.sin(angle); 156 | } 157 | 158 | const sizeAfterTransform = (width, height, options) => { 159 | let dimX = width / 2, dimY = height / 2, 160 | points = [ 161 | { 162 | x: -dimX, 163 | y: -dimY 164 | }, 165 | { 166 | x: dimX, 167 | y: -dimY 168 | }, 169 | { 170 | x: -dimX, 171 | y: dimY 172 | }, 173 | { 174 | x: dimX, 175 | y: dimY 176 | }], 177 | transformMatrix = calcDimensionsMatrix(options), 178 | bbox = makeBoundingBoxFromPoints(points, transformMatrix); 179 | return { 180 | x: bbox.width, 181 | y: bbox.height, 182 | }; 183 | } 184 | 185 | const calcDimensionsMatrix = (options) => { 186 | let scaleX = typeof options.scaleX === 'undefined' ? 1 : options.scaleX, 187 | scaleY = typeof options.scaleY === 'undefined' ? 1 : options.scaleY, 188 | scaleMatrix = [ 189 | options.flipX ? -scaleX : scaleX, 190 | 0, 191 | 0, 192 | options.flipY ? -scaleY : scaleY, 193 | 0, 194 | 0], 195 | multiply = multiplyTransformMatrices; 196 | if (options.skewX) { 197 | scaleMatrix = multiply( 198 | scaleMatrix, 199 | [1, 0, Math.tan(degreesToRadians(options.skewX)), 1], 200 | true); 201 | } 202 | if (options.skewY) { 203 | scaleMatrix = multiply( 204 | scaleMatrix, 205 | [1, Math.tan(degreesToRadians(options.skewY)), 0, 1], 206 | true); 207 | } 208 | return scaleMatrix; 209 | } 210 | 211 | const multiplyTransformMatrices = (a, b, is2x2) => { 212 | return [ 213 | a[0] * b[0] + a[2] * b[1], 214 | a[1] * b[0] + a[3] * b[1], 215 | a[0] * b[2] + a[2] * b[3], 216 | a[1] * b[2] + a[3] * b[3], 217 | is2x2 ? 0 : a[0] * b[4] + a[2] * b[5] + a[4], 218 | is2x2 ? 0 : a[1] * b[4] + a[3] * b[5] + a[5] 219 | ]; 220 | } 221 | 222 | const makeBoundingBoxFromPoints = (points, transform) => { 223 | if (transform) { 224 | for (let i = 0; i < points.length; i++) { 225 | points[i] = transformPoint(points[i], transform); 226 | } 227 | } 228 | let xPoints = [points[0].x, points[1].x, points[2].x, points[3].x], 229 | minX = min(xPoints), 230 | maxX = max(xPoints), 231 | width = maxX - minX, 232 | yPoints = [points[0].y, points[1].y, points[2].y, points[3].y], 233 | minY = min(yPoints), 234 | maxY = max(yPoints), 235 | height = maxY - minY; 236 | 237 | return { 238 | left: minX, 239 | top: minY, 240 | width: width, 241 | height: height 242 | }; 243 | } 244 | 245 | const qrDecompose = (a) => { 246 | let angle = atan2(a[1], a[0]), 247 | denom = pow(a[0], 2) + pow(a[1], 2), 248 | scaleX = sqrt(denom), 249 | scaleY = (a[0] * a[3] - a[2] * a[1]) / scaleX, 250 | skewX = atan2(a[0] * a[2] + a[1] * a [3], denom); 251 | return { 252 | angle: angle / PiBy180, 253 | scaleX: scaleX, 254 | scaleY: scaleY, 255 | skewX: skewX / PiBy180, 256 | skewY: 0, 257 | translateX: a[4], 258 | translateY: a[5] 259 | }; 260 | } 261 | 262 | const rotateVector = (vector, radians) => { 263 | let s = sin(radians), 264 | c = cos(radians), 265 | rx = vector.x * c - vector.y * s, 266 | ry = vector.x * s + vector.y * c; 267 | return { 268 | x: rx, 269 | y: ry 270 | }; 271 | } 272 | 273 | const rotatePoint = (point, origin, radians) => { 274 | point.subtractEquals(origin); 275 | let v = rotateVector(point, radians); 276 | return new PointClass(v.x, v.y).addEquals(origin); 277 | } 278 | 279 | const calcRotateMatrix = (options) => { 280 | if (!options.angle) { 281 | return [1, 0, 0, 1, 0, 0].concat(); 282 | } 283 | let theta = degreesToRadians(options.angle), 284 | c = cos(theta), 285 | s = sin(theta); 286 | return [c, s, -s, c, 0, 0]; 287 | } 288 | 289 | const composeMatrix = (options) => { 290 | var matrix = [1, 0, 0, 1, options.translateX || 0, options.translateY || 0], 291 | multiply = multiplyTransformMatrices; 292 | if (options.angle) { 293 | matrix = multiply(matrix, calcRotateMatrix(options)); 294 | } 295 | if (options.scaleX !== 1 || options.scaleY !== 1 || 296 | options.skewX || options.skewY || options.flipX || options.flipY) { 297 | matrix = multiply(matrix, calcDimensionsMatrix(options)); 298 | } 299 | return matrix; 300 | } 301 | 302 | module.exports = { 303 | invertTransform, 304 | transformPoint, 305 | populateWithProperties, 306 | makeBoundingBoxFromPoints, 307 | loadImage, 308 | degreesToRadians, 309 | cos, 310 | sin, 311 | sizeAfterTransform, 312 | calcDimensionsMatrix, 313 | multiplyTransformMatrices, 314 | qrDecompose, 315 | rotateVector, 316 | rotatePoint, 317 | calcRotateMatrix, 318 | composeMatrix, 319 | } 320 | -------------------------------------------------------------------------------- /src/mixins/canvas_events.mixin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Sugar on 2020/5/30. 3 | */ 4 | module.exports = { 5 | /** 6 | * @param e 7 | * changedTouches: [ 8 | {identifier: 0 9 | x: 188.0001220703125 10 | y: 218.99996948242188 11 | } 12 | ], 13 | currentTarget:{ 14 | dataset:{} 15 | id: "sugarjs" 16 | offsetLeft: 0 17 | offsetTop: 0 18 | } 19 | target:{ 20 | dataset: {} 21 | id: "sugarjs" 22 | offsetLeft: 0 23 | offsetTop: 0 24 | } 25 | timeStamp: 5821.974999998929 26 | touches: [ 27 | { 28 | identifier: 0 29 | x: 188.0001220703125 30 | y: 218.99996948242188 31 | } 32 | ], 33 | type: "touchstart" 34 | */ 35 | touchstart: function (e) { 36 | this._target = null 37 | this._handleEvent(e, 'start:before'); 38 | this._target = this._currentTransform ? this._currentTransform.target : this.findTarget(e) || null 39 | 40 | var target = this._target 41 | var pointer = this._pointer; 42 | var shouldRender = this._shouldRender(target) 43 | 44 | if (this._shouldClearSelection(e, target)) { 45 | this.discardActiveObject(e) 46 | } 47 | 48 | if (target) { 49 | var alreadySelected = target === this._activeObject; 50 | if (target.selectable) { 51 | console.log('点击目标', target) 52 | this.setActiveObject(target, e); 53 | } 54 | // var corner = target._findTargetCorner( 55 | // this.getPointer(e, true), 56 | // true 57 | // ); 58 | // target.__corner = corner; 59 | if (target === this._activeObject) { 60 | this._setupCurrentTransform(e, target, alreadySelected); 61 | } 62 | } 63 | this._handleEvent(e, 'start'); 64 | (shouldRender) && this.requestRenderAll(); 65 | }, 66 | touchmove: function (e) { 67 | if (!this.allowTouchScrolling) return 68 | // console.log('move', e) 69 | 70 | let target = this.findTarget(e) 71 | if (e.touches && e.touches.length === 2) { 72 | // 双指手势 73 | if (target) { 74 | // TODO 缩放、旋转 75 | // this.__gesturesRenderer() 76 | // this._handleEvent(e, 'gesture'); 77 | } 78 | return 79 | } 80 | this._handleEvent(e, 'move:before'); 81 | 82 | if (!this._currentTransform) { 83 | // target = this.findTarget(e) || null; 84 | // this._setCursorFromEvent(e, target); 85 | // this._fireOverOutEvents(target, e); 86 | } else { 87 | this._transformObject(e); 88 | } 89 | // console.log('移动目标', this._currentTransform) 90 | this._handleEvent(e, 'move'); 91 | this._resetTransformEventData(); 92 | }, 93 | touchend: function (e) { 94 | // console.log(e) 95 | if (e.touches.length > 0) { 96 | return 97 | } 98 | let target 99 | // let transform = this._currentTransform 100 | // let groupSelector = this._groupSelector 101 | // let shouldRender = false 102 | this._resetTransformEventData() 103 | this._target = this._currentTransform ? this._currentTransform.target : this.findTarget(e) || null 104 | target = this._target; 105 | this._handleEvent(e, 'end:before'); 106 | // if (transform) { 107 | // this._finalizeCurrentTransform(e); 108 | // shouldRender = transform.actionPerformed; 109 | // } 110 | if (target) { 111 | target.isMoving = false; 112 | } 113 | this._handleEvent(e, 'end'); 114 | // this._groupSelector = null; 115 | this._currentTransform = null 116 | // if (shouldRender) { 117 | // this.requestRenderAll(); 118 | // } else if (!isClick) { 119 | // this.renderTop(); 120 | // } 121 | this._resetTransformEventData() 122 | }, 123 | longtap: function (e) { 124 | 125 | }, 126 | _handleEvent: function (e, eventType, button, isClick) { 127 | var target = this._target, 128 | targets = this.targets || [], 129 | options = { 130 | e: e, 131 | target: target, 132 | subTargets: targets, 133 | // button: button || LEFT_CLICK, 134 | // isClick: isClick || false, 135 | // pointer: this._pointer, 136 | // absolutePointer: this._absolutePointer, 137 | transform: this._currentTransform 138 | }; 139 | this.fire('touch:' + eventType, options); 140 | target && target.fire('touch' + eventType, options); 141 | for (var i = 0; i < targets.length; i++) { 142 | targets[i].fire('touch' + eventType, options); 143 | } 144 | }, 145 | _shouldRender: function (target) { 146 | var activeObject = this._activeObject; 147 | 148 | if ( 149 | !!activeObject !== !!target || 150 | (activeObject && target && (activeObject !== target)) 151 | ) { 152 | return true; 153 | } 154 | return false; 155 | }, 156 | 157 | findTarget: function (e, skipGroup) { 158 | if (this.skipTargetFind) { 159 | return; 160 | } 161 | let ignoreZoom = true, 162 | pointer = this.getPointer(e, ignoreZoom), 163 | activeObject = this._activeObject, 164 | aObjects = this.getActiveObjects(), 165 | activeTarget, activeTargetSubs 166 | 167 | this.targets = []; 168 | 169 | if (aObjects.length > 1 && !skipGroup && activeObject === this._searchPossibleTargets([activeObject], pointer)) { 170 | return activeObject; 171 | } 172 | if (aObjects.length === 1 && activeObject._findTargetCorner(pointer)) { 173 | return activeObject; 174 | } 175 | if (aObjects.length === 1 && 176 | activeObject === this._searchPossibleTargets([activeObject], pointer)) { 177 | if (!this.preserveObjectStacking) { 178 | return activeObject; 179 | } else { 180 | activeTarget = activeObject; 181 | activeTargetSubs = this.targets; 182 | this.targets = []; 183 | } 184 | } 185 | var target = this._searchPossibleTargets(this._objects, pointer); 186 | if (e[this.altSelectionKey] && target && activeTarget && target !== activeTarget) { 187 | target = activeTarget; 188 | this.targets = activeTargetSubs; 189 | } 190 | return target; 191 | }, 192 | 193 | _searchPossibleTargets: function (objects, pointer) { 194 | var target, i = objects.length, subTarget; 195 | while (i--) { 196 | var objToCheck = objects[i]; 197 | // var pointerToUse = objToCheck.group && objToCheck.group.type !== 'activeSelection' ? 198 | // this._normalizePointer(objToCheck.group, pointer) : pointer; 199 | if (this._checkTarget(pointer, objToCheck, pointer)) { 200 | target = objects[i]; 201 | // if (target.subTargetCheck && target instanceof Group) { 202 | // subTarget = this._searchPossibleTargets(target._objects, pointer); 203 | // subTarget && this.targets.push(subTarget); 204 | // } 205 | break; 206 | } 207 | } 208 | return target; 209 | }, 210 | 211 | getPointer: function (e, ignoreZoom) { 212 | if (this._pointer && ignoreZoom) { 213 | return this._pointer; 214 | } 215 | 216 | let pointer = { 217 | x: e.touches.length > 0 ? e.touches[0].x : e.changedTouches[0].x, 218 | y: e.touches.length > 0 ? e.touches[0].y : e.changedTouches[0].y 219 | } 220 | 221 | return pointer 222 | }, 223 | 224 | _checkTarget: function (pointer, obj, globalPointer) { 225 | if (obj && 226 | obj.visible && 227 | obj.evented && 228 | this.containsPoint(null, obj, pointer)) { 229 | // if ((this.perPixelTargetFind || obj.perPixelTargetFind) && !obj.isEditing) { 230 | // var isTransparent = this.isTargetTransparent(obj, globalPointer.x, globalPointer.y); 231 | // if (!isTransparent) { 232 | // return true; 233 | // } 234 | // } else { 235 | return true; 236 | // } 237 | } 238 | }, 239 | 240 | containsPoint: function (e, target, point) { 241 | var ignoreZoom = true, 242 | pointer = point || this.getPointer(e, ignoreZoom), 243 | xy; 244 | 245 | // if (target.group && target.group === this._activeObject && target.group.type === 'activeSelection') { 246 | // xy = this._normalizePointer(target.group, pointer); 247 | // } else { 248 | xy = {x: pointer.x, y: pointer.y}; 249 | // } 250 | // return (target.containsPoint(xy) || !!target._findTargetCorner(pointer, true)); 251 | return target.left <= xy.x && xy.x <= (target.left + target.width) && target.top <= xy.y && xy.y <= (target.top + target.height) 252 | }, 253 | 254 | _transformObject: function (e) { 255 | let pointer = this.getPointer(e), 256 | transform = this._currentTransform; 257 | 258 | transform.reset = false; 259 | transform.target.isMoving = true; 260 | 261 | this._performTransformAction(e, transform, pointer); 262 | transform.actionPerformed && this.requestRenderAll(); 263 | }, 264 | 265 | _performTransformAction: function (e, transform, pointer) { 266 | let x = pointer.x, 267 | y = pointer.y, 268 | action = transform.action, 269 | actionPerformed = false, 270 | options = { 271 | target: transform.target, 272 | e: e, 273 | transform: transform, 274 | pointer: pointer 275 | }; 276 | 277 | if (action === 'drag') { 278 | actionPerformed = this._translateObject(x, y); 279 | if (actionPerformed) { 280 | // this._fire('moving', options); 281 | // this.setCursor(options.target.moveCursor || this.moveCursor); 282 | } 283 | } 284 | transform.actionPerformed = transform.actionPerformed || actionPerformed; 285 | }, 286 | 287 | _resetTransformEventData: function () { 288 | this._target = null; 289 | this._pointer = null; 290 | this._absolutePointer = null; 291 | }, 292 | 293 | _beforeTransform: function (e) { 294 | let t = this._currentTransform; 295 | // this.stateful && t.target.saveState(); 296 | this.fire('before:transform', { 297 | e: e, 298 | transform: t, 299 | }); 300 | }, 301 | __gesturesParams: null, 302 | __gesturesRenderer: function () { 303 | 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /src/shapes/object.class.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Sugar on 2020/5/26. 3 | */ 4 | 5 | const { 6 | multiplyTransformMatrices, 7 | qrDecompose, 8 | degreesToRadians 9 | } = require('../utils/misc') 10 | 11 | class ObjectClass { 12 | constructor(options) { 13 | this.type = 'object' 14 | this.originX = 'left' 15 | this.originY = 'top' 16 | this.top = 0 17 | this.left = 0 18 | this.width = 0 19 | this.height = 0 20 | this.originY = 'top' 21 | this.scaleX = 1 22 | this.scaleY = 1 23 | this.flipX = false 24 | this.flipY = false 25 | this.opacity = 1 26 | this.angle = 0 27 | this.skewX = 0 28 | this.skewY = 0 29 | this.stroke = null 30 | this.strokeWidth = 1 31 | this.strokeDashArray = null 32 | this.strokeDashOffset = 0 33 | this.padding = 0 34 | this.cornerSize = 13 35 | this.touchCornerSize = 24 36 | this.transparentCorners = true 37 | this.fill = 'rgb(0,0,0)' 38 | this.strokeWidth = 1 39 | this.backgroundColor = '' 40 | this.borderColor = 'orange' 41 | this.borderDashArray = null 42 | this.cornerColor = 'blue' 43 | this.cornerStrokeColor = null 44 | this.cornerStyle = 'rect' 45 | this.cornerDashArray = null 46 | this.centeredScaling = false 47 | this.centeredRotation = true // 如果为true,则将以物体中心为原点 48 | this.selectable = true 49 | this.evented = true 50 | this.visible = true 51 | this.hasControls = true 52 | this.hasBorders = true 53 | this.lockMovementX = false 54 | this.lockMovementY = false 55 | this.lockRotation = false 56 | this.lockScalingX = false 57 | this.lockScalingY = false 58 | this.lockSkewingX = false 59 | this.lockSkewingY = false 60 | this.selectionBackgroundColor = '' 61 | this.paintFirst = 'stroke' 62 | this.borderScaleFactor = 1 63 | this.borderOpacityWhenMoving = 0.4 64 | this.minScaleLimit = 0 65 | this.__corner = 0 66 | } 67 | 68 | initialize(options) { 69 | if (options) { 70 | this.setOptions(options); 71 | } 72 | } 73 | 74 | setOptions(options) { 75 | this._setOptions(options); 76 | this._initGradient(options.fill, 'fill'); 77 | this._initGradient(options.stroke, 'stroke'); 78 | this._initPattern(options.fill, 'fill'); 79 | this._initPattern(options.stroke, 'stroke'); 80 | } 81 | 82 | /* 83 | * @private 84 | * 判断是否可渲染 85 | * @memberOf sugar.Object.prototype 86 | * @return {Boolean} 87 | */ 88 | isNotVisible() { 89 | return this.opacity === 0 || (!this.width && !this.height) || !this.visible 90 | } 91 | 92 | /** 93 | * 在指定的上下文中渲染对象 94 | * @param {CanvasContext} ctx 微信组件的绘图上下文 95 | */ 96 | render(ctx) { 97 | if (this.isNotVisible()) { 98 | return 99 | } 100 | ctx.save() 101 | // this._setupCompositeOperation(ctx) 102 | this.drawSelectionBackground(ctx) 103 | this.transform(ctx) 104 | // this._setOpacity(ctx) 105 | // this._setShadow(ctx, this) 106 | // if (this.shouldCache()) { 107 | // this.renderCache() 108 | // this.drawCacheOnCanvas(ctx); 109 | // } else { 110 | // this._removeCacheCanvas(); 111 | this.drawObject(ctx) 112 | // if (this.objectCaching && this.statefullCache) { 113 | // this.saveState({propertySet: 'cacheProperties'}); 114 | // } 115 | // } 116 | ctx.restore() 117 | } 118 | 119 | transform(ctx) { 120 | let m = this.calcOwnMatrix() 121 | // if (this.group && !this.group._transformDone) { 122 | // m = this.calcTransformMatrix(); 123 | // } 124 | // console.log(m) 125 | ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); 126 | } 127 | 128 | drawObject(ctx, forClipping) { 129 | let originalFill = this.fill 130 | let originalStroke = this.stroke 131 | if (forClipping) { 132 | this.fill = 'black' 133 | this.stroke = '' 134 | // this._setClippingProperties(ctx) // TODO 135 | } else { 136 | this._renderBackground(ctx) 137 | this._setStrokeStyles(ctx, this) 138 | this._setFillStyles(ctx, this) 139 | } 140 | // 调用子类的_render方法,绘制到canvas 141 | this._render(ctx) 142 | // this._drawClipPath(ctx) 143 | this.fill = originalFill 144 | this.stroke = originalStroke 145 | } 146 | 147 | _removeShadow(ctx) { 148 | if (!this.shadow) { 149 | return; 150 | } 151 | 152 | ctx.shadowColor = ''; 153 | ctx.shadowBlur = ctx.shadowOffsetX = ctx.shadowOffsetY = 0; 154 | } 155 | 156 | _applyPatternGradientTransform(ctx, filler) { 157 | if (!filler || !filler.toLive) { 158 | return {offsetX: 0, offsetY: 0}; 159 | } 160 | var t = filler.gradientTransform || filler.patternTransform; 161 | var offsetX = -this.width / 2 + filler.offsetX || 0, 162 | offsetY = -this.height / 2 + filler.offsetY || 0; 163 | 164 | if (filler.gradientUnits === 'percentage') { 165 | ctx.transform(this.width, 0, 0, this.height, offsetX, offsetY); 166 | } else { 167 | ctx.transform(1, 0, 0, 1, offsetX, offsetY); 168 | } 169 | if (t) { 170 | ctx.transform(t[0], t[1], t[2], t[3], t[4], t[5]); 171 | } 172 | return {offsetX: offsetX, offsetY: offsetY}; 173 | } 174 | 175 | _renderPaintInOrder(ctx) { 176 | if (this.paintFirst === 'stroke') { 177 | this._renderStroke(ctx); 178 | this._renderFill(ctx); 179 | } else { 180 | this._renderFill(ctx); 181 | this._renderStroke(ctx); 182 | } 183 | } 184 | 185 | _render() { 186 | 187 | } 188 | 189 | _renderFill(ctx) { 190 | if (!this.fill) { 191 | return; 192 | } 193 | 194 | ctx.save(); 195 | // this._applyPatternGradientTransform(ctx, this.fill); 196 | if (this.fillRule === 'evenodd') { 197 | ctx.fill('evenodd'); 198 | } else { 199 | ctx.fill(); 200 | } 201 | ctx.restore(); 202 | } 203 | 204 | _renderStroke(ctx) { 205 | 206 | } 207 | 208 | getViewportTransform() { 209 | if (this.canvas && this.canvas.viewportTransform) { 210 | return this.canvas.viewportTransform; 211 | } 212 | return [1, 0, 0, 1, 0, 0].concat(); 213 | } 214 | 215 | /** 216 | * 为对象绘制背景,尺寸不变 217 | * @private 218 | * @param {CanvasRenderingContext2D} ctx 219 | */ 220 | _renderBackground(ctx) { 221 | if (!this.backgroundColor) { 222 | return; 223 | } 224 | let dim = this._getNonTransformedDimensions(); 225 | ctx.fillStyle = this.backgroundColor; 226 | 227 | ctx.fillRect( 228 | -dim.x / 2, 229 | -dim.y / 2, 230 | dim.x, 231 | dim.y 232 | ); 233 | // if there is background color no other shadows 234 | // should be casted 235 | // this._removeShadow(ctx); 236 | } 237 | 238 | getObjectOpacity() { 239 | let opacity = this.opacity; 240 | // if (this.group) { 241 | // opacity *= this.group.getObjectOpacity(); 242 | // } 243 | return opacity; 244 | } 245 | 246 | _set(key, value) { 247 | console.log('set scaleX') 248 | let shouldConstrainValue = (key === 'scaleX' || key === 'scaleY') 249 | 250 | if (shouldConstrainValue) { 251 | value = this._constrainScale(value); 252 | } 253 | if (key === 'scaleX' && value < 0) { 254 | this.flipX = !this.flipX 255 | value *= -1; 256 | } else if (key === 'scaleY' && value < 0) { 257 | this.flipY = !this.flipY 258 | value *= -1; 259 | } 260 | 261 | this[key] = value; 262 | 263 | return this 264 | } 265 | 266 | _setOpacity(ctx) { 267 | if (this.group && !this.group._transformDone) { 268 | ctx.globalAlpha = this.getObjectOpacity(); 269 | } else { 270 | ctx.globalAlpha *= this.opacity; 271 | } 272 | } 273 | 274 | _setStrokeStyles(ctx, decl) { 275 | if (decl.stroke) { 276 | ctx.lineWidth = decl.strokeWidth; 277 | ctx.lineCap = decl.strokeLineCap; 278 | ctx.lineDashOffset = decl.strokeDashOffset; 279 | ctx.lineJoin = decl.strokeLineJoin; 280 | ctx.miterLimit = decl.strokeMiterLimit; 281 | ctx.strokeStyle = decl.stroke.toLive 282 | ? decl.stroke.toLive(ctx, this) 283 | : decl.stroke; 284 | } 285 | } 286 | 287 | _setFillStyles(ctx, decl) { 288 | if (decl.fill) { 289 | ctx.fillStyle = decl.fill.toLive 290 | ? decl.fill.toLive(ctx, this) 291 | : decl.fill; 292 | } 293 | } 294 | 295 | _setClippingProperties(ctx) { 296 | ctx.globalAlpha = 1; 297 | ctx.strokeStyle = 'transparent'; 298 | ctx.fillStyle = '#000000'; 299 | } 300 | 301 | _setLineDash(ctx, dashArray, alternative) { 302 | if (!dashArray || dashArray.length === 0) { 303 | return; 304 | } 305 | if (1 & dashArray.length) { 306 | dashArray.push.apply(dashArray, dashArray); 307 | } 308 | if (supportsLineDash) { 309 | ctx.setLineDash(dashArray); 310 | } else { 311 | alternative && alternative(ctx); 312 | } 313 | } 314 | 315 | /** 316 | * 渲染对象的控件和边框 317 | * @param {CanvasRenderingContext2D} ctx 318 | * @param {Object} [styleOverride] 覆盖对象样式的属性 319 | */ 320 | _renderControls(ctx, styleOverride) { 321 | let vpt = this.getViewportTransform(), 322 | matrix = this.calcTransformMatrix(), 323 | options, drawBorders, drawControls; 324 | styleOverride = styleOverride || {}; 325 | drawBorders = typeof styleOverride.hasBorders !== 'undefined' ? styleOverride.hasBorders : this.hasBorders; 326 | drawControls = typeof styleOverride.hasControls !== 'undefined' ? styleOverride.hasControls : this.hasControls; 327 | matrix = multiplyTransformMatrices(vpt, matrix); 328 | options = qrDecompose(matrix); 329 | ctx.save(); 330 | ctx.translate(options.translateX, options.translateY); 331 | ctx.lineWidth = 1 * this.borderScaleFactor; 332 | if (!this.group) { 333 | ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; 334 | } 335 | if (styleOverride.forActiveSelection) { 336 | ctx.rotate(degreesToRadians(options.angle)); 337 | drawBorders && this.drawBordersInGroup(ctx, options, styleOverride); 338 | } else { 339 | ctx.rotate(degreesToRadians(this.angle)); 340 | drawBorders && this.drawBorders(ctx, styleOverride); 341 | } 342 | drawControls && this.drawControls(ctx, styleOverride); 343 | ctx.restore(); 344 | } 345 | 346 | rotate(angle) { 347 | let shouldCenterOrigin = (this.originX !== 'center' || this.originY !== 'center') && this.centeredRotation; 348 | 349 | if (shouldCenterOrigin) { 350 | this._setOriginToCenter(); 351 | } 352 | 353 | this.set('angle', angle); 354 | 355 | if (shouldCenterOrigin) { 356 | this._resetOrigin(); 357 | } 358 | 359 | return this 360 | } 361 | } 362 | 363 | ObjectClass.__uid = 0 364 | 365 | module.exports = ObjectClass 366 | -------------------------------------------------------------------------------- /tools/build.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs') 3 | 4 | const gulp = require('gulp') 5 | const clean = require('gulp-clean') 6 | const less = require('gulp-less') 7 | const rename = require('gulp-rename') 8 | const gulpif = require('gulp-if') 9 | const sourcemaps = require('gulp-sourcemaps') 10 | const webpack = require('webpack') 11 | const gulpInstall = require('gulp-install') 12 | 13 | const config = require('./config') 14 | const checkComponents = require('./checkcomponents') 15 | const checkWxss = require('./checkwxss') 16 | const _ = require('./utils') 17 | 18 | const jsConfig = config.js || {} 19 | const wxssConfig = config.wxss || {} 20 | const srcPath = config.srcPath 21 | const distPath = config.distPath 22 | 23 | /** 24 | * 获取 wxss 流 25 | */ 26 | function wxss(wxssFileList) { 27 | if (!wxssFileList.length) return false 28 | 29 | return gulp.src(wxssFileList, {cwd: srcPath, base: srcPath}) 30 | .pipe(checkWxss.start()) // 开始处理 import 31 | .pipe(gulpif(wxssConfig.less && wxssConfig.sourcemap, sourcemaps.init())) 32 | .pipe(gulpif(wxssConfig.less, less({paths: [srcPath]}))) 33 | .pipe(checkWxss.end()) // 结束处理 import 34 | .pipe(rename({extname: '.wxss'})) 35 | .pipe(gulpif(wxssConfig.less && wxssConfig.sourcemap, sourcemaps.write('./'))) 36 | .pipe(_.logger(wxssConfig.less ? 'generate' : undefined)) 37 | .pipe(gulp.dest(distPath)) 38 | } 39 | 40 | /** 41 | * 获取 js 流 42 | */ 43 | function js(jsFileMap, scope) { 44 | const webpackConfig = config.webpack 45 | const webpackCallback = (err, stats) => { 46 | if (!err) { 47 | // eslint-disable-next-line no-console 48 | console.log(stats.toString({ 49 | assets: true, 50 | cached: false, 51 | colors: true, 52 | children: false, 53 | errors: true, 54 | warnings: true, 55 | version: true, 56 | modules: false, 57 | publicPath: true, 58 | })) 59 | } else { 60 | // eslint-disable-next-line no-console 61 | console.log(err) 62 | } 63 | } 64 | 65 | webpackConfig.entry = jsFileMap 66 | webpackConfig.output.path = distPath 67 | 68 | if (scope.webpackWatcher) { 69 | scope.webpackWatcher.close() 70 | scope.webpackWatcher = null 71 | } 72 | 73 | if (config.isWatch) { 74 | scope.webpackWatcher = webpack(webpackConfig).watch({ 75 | ignored: /node_modules/, 76 | }, webpackCallback) 77 | } else { 78 | webpack(webpackConfig).run(webpackCallback) 79 | } 80 | } 81 | 82 | /** 83 | * 拷贝文件 84 | */ 85 | function copy(copyFileList) { 86 | if (!copyFileList.length) return false 87 | 88 | return gulp.src(copyFileList, {cwd: srcPath, base: srcPath}) 89 | .pipe(_.logger()) 90 | .pipe(gulp.dest(distPath)) 91 | } 92 | 93 | /** 94 | * 安装依赖包 95 | */ 96 | function install() { 97 | return gulp.series(async () => { 98 | const demoDist = config.demoDist 99 | const demoPackageJsonPath = path.join(demoDist, 'package.json') 100 | const packageJson = _.readJson(path.resolve(__dirname, '../package.json')) 101 | const dependencies = packageJson.dependencies || {} 102 | 103 | await _.writeFile(demoPackageJsonPath, JSON.stringify({dependencies}, null, '\t')) // write dev demo's package.json 104 | }, () => { 105 | const demoDist = config.demoDist 106 | const demoPackageJsonPath = path.join(demoDist, 'package.json') 107 | 108 | return gulp.src(demoPackageJsonPath) 109 | .pipe(gulpInstall({production: true})) 110 | }) 111 | } 112 | 113 | class BuildTask { 114 | constructor(id, entry) { 115 | if (!entry) return 116 | 117 | this.id = id 118 | this.entries = Array.isArray(config.entry) ? config.entry : [config.entry] 119 | this.copyList = Array.isArray(config.copy) ? config.copy : [] 120 | this.componentListMap = {} 121 | this.cachedComponentListMap = {} 122 | 123 | this.init() 124 | } 125 | 126 | init() { 127 | const id = this.id 128 | 129 | /** 130 | * 清空目标目录 131 | */ 132 | gulp.task(`${id}-clean-dist`, () => gulp.src(distPath, {read: false, allowEmpty: true}).pipe(clean())) 133 | 134 | /** 135 | * 拷贝 demo 到目标目录 136 | */ 137 | let isDemoExists = false 138 | gulp.task(`${id}-demo`, gulp.series(async () => { 139 | const demoDist = config.demoDist 140 | 141 | isDemoExists = await _.checkFileExists(path.join(demoDist, 'project.config.json')) 142 | }, done => { 143 | if (!isDemoExists) { 144 | const demoSrc = config.demoSrc 145 | const demoDist = config.demoDist 146 | 147 | return gulp.src('**/*', {cwd: demoSrc, base: demoSrc}) 148 | .pipe(gulp.dest(demoDist)) 149 | } 150 | 151 | return done() 152 | })) 153 | 154 | /** 155 | * 安装依赖包 156 | */ 157 | gulp.task(`${id}-install`, install()) 158 | 159 | /** 160 | * 检查自定义组件 161 | */ 162 | gulp.task(`${id}-component-check`, async () => { 163 | const entries = this.entries 164 | const mergeComponentListMap = {} 165 | for (let i = 0, len = entries.length; i < len; i++) { 166 | let entry = entries[i] 167 | entry = path.join(srcPath, `${entry}.json`) 168 | const newComponentListMap = await checkComponents(entry) 169 | 170 | _.merge(mergeComponentListMap, newComponentListMap) 171 | } 172 | 173 | this.cachedComponentListMap = this.componentListMap 174 | this.componentListMap = mergeComponentListMap 175 | }) 176 | 177 | /** 178 | * 写 json 文件到目标目录 179 | */ 180 | gulp.task(`${id}-component-json`, done => { 181 | const jsonFileList = this.componentListMap.jsonFileList 182 | 183 | if (jsonFileList && jsonFileList.length) { 184 | return copy(this.componentListMap.jsonFileList) 185 | } 186 | 187 | return done() 188 | }) 189 | 190 | /** 191 | * 拷贝 wxml 文件到目标目录 192 | */ 193 | gulp.task(`${id}-component-wxml`, done => { 194 | const wxmlFileList = this.componentListMap.wxmlFileList 195 | 196 | if (wxmlFileList && 197 | wxmlFileList.length && 198 | !_.compareArray(this.cachedComponentListMap.wxmlFileList, wxmlFileList)) { 199 | return copy(wxmlFileList) 200 | } 201 | 202 | return done() 203 | }) 204 | 205 | /** 206 | * 生成 wxss 文件到目标目录 207 | */ 208 | gulp.task(`${id}-component-wxss`, done => { 209 | const wxssFileList = this.componentListMap.wxssFileList 210 | 211 | if (wxssFileList && 212 | wxssFileList.length && 213 | !_.compareArray(this.cachedComponentListMap.wxssFileList, wxssFileList)) { 214 | return wxss(wxssFileList, srcPath, distPath) 215 | } 216 | 217 | return done() 218 | }) 219 | 220 | /** 221 | * 生成 js 文件到目标目录 222 | */ 223 | gulp.task(`${id}-component-js`, done => { 224 | const jsFileList = this.componentListMap.jsFileList 225 | 226 | if (jsFileList && 227 | jsFileList.length && 228 | !_.compareArray(this.cachedComponentListMap.jsFileList, jsFileList)) { 229 | if (jsConfig.webpack) { 230 | js(this.componentListMap.jsFileMap, this) 231 | } else { 232 | return copy(jsFileList) 233 | } 234 | } 235 | 236 | return done() 237 | }) 238 | 239 | /** 240 | * 拷贝相关资源到目标目录 241 | */ 242 | gulp.task(`${id}-copy`, gulp.parallel(done => { 243 | const copyList = this.copyList 244 | const copyFileList = copyList.map(copyFilePath => { 245 | try { 246 | if (fs.statSync(path.join(srcPath, copyFilePath)).isDirectory()) { 247 | return path.join(copyFilePath, '**/*.!(wxss)') 248 | } else { 249 | return copyFilePath 250 | } 251 | } catch (err) { 252 | // eslint-disable-next-line no-console 253 | console.error(err) 254 | return null 255 | } 256 | }).filter(copyFilePath => !!copyFilePath) 257 | 258 | if (copyFileList.length) return copy(copyFileList) 259 | 260 | return done() 261 | }, done => { 262 | const copyList = this.copyList 263 | const copyFileList = copyList.map(copyFilePath => { 264 | try { 265 | if (fs.statSync(path.join(srcPath, copyFilePath)).isDirectory()) { 266 | return path.join(copyFilePath, '**/*.wxss') 267 | } else if (copyFilePath.slice(-5) === '.wxss') { 268 | return copyFilePath 269 | } else { 270 | return null 271 | } 272 | } catch (err) { 273 | // eslint-disable-next-line no-console 274 | console.error(err) 275 | return null 276 | } 277 | }).filter(copyFilePath => !!copyFilePath) 278 | 279 | if (copyFileList.length) return wxss(copyFileList, srcPath, distPath) 280 | 281 | return done() 282 | })) 283 | 284 | /** 285 | * 监听 js 变化 286 | */ 287 | gulp.task(`${id}-watch-js`, done => { 288 | if (!jsConfig.webpack) { 289 | return gulp.watch(this.componentListMap.jsFileList, {cwd: srcPath, base: srcPath}, gulp.series(`${id}-component-js`)) 290 | } 291 | 292 | return done() 293 | }) 294 | 295 | /** 296 | * 监听 json 变化 297 | */ 298 | gulp.task(`${id}-watch-json`, () => gulp.watch(this.componentListMap.jsonFileList, {cwd: srcPath, base: srcPath}, gulp.series(`${id}-component-check`, gulp.parallel(`${id}-component-wxml`, `${id}-component-wxss`, `${id}-component-js`, `${id}-component-json`)))) 299 | 300 | /** 301 | * 监听 wxml 变化 302 | */ 303 | gulp.task(`${id}-watch-wxml`, () => { 304 | this.cachedComponentListMap.wxmlFileList = null 305 | return gulp.watch(this.componentListMap.wxmlFileList, {cwd: srcPath, base: srcPath}, gulp.series(`${id}-component-wxml`)) 306 | }) 307 | 308 | /** 309 | * 监听 wxss 变化 310 | */ 311 | gulp.task(`${id}-watch-wxss`, () => { 312 | this.cachedComponentListMap.wxssFileList = null 313 | return gulp.watch('**/*.wxss', {cwd: srcPath, base: srcPath}, gulp.series(`${id}-component-wxss`)) 314 | }) 315 | 316 | /** 317 | * 监听相关资源变化 318 | */ 319 | gulp.task(`${id}-watch-copy`, () => { 320 | const copyList = this.copyList 321 | const copyFileList = copyList.map(copyFilePath => { 322 | try { 323 | if (fs.statSync(path.join(srcPath, copyFilePath)).isDirectory()) { 324 | return path.join(copyFilePath, '**/*') 325 | } else { 326 | return copyFilePath 327 | } 328 | } catch (err) { 329 | // eslint-disable-next-line no-console 330 | console.error(err) 331 | return null 332 | } 333 | }).filter(copyFilePath => !!copyFilePath) 334 | const watchCallback = filePath => copy([filePath]) 335 | 336 | return gulp.watch(copyFileList, {cwd: srcPath, base: srcPath}) 337 | .on('change', watchCallback) 338 | .on('add', watchCallback) 339 | .on('unlink', watchCallback) 340 | }) 341 | 342 | /** 343 | * 监听 demo 变化 344 | */ 345 | gulp.task(`${id}-watch-demo`, () => { 346 | const demoSrc = config.demoSrc 347 | const demoDist = config.demoDist 348 | const watchCallback = filePath => gulp.src(filePath, {cwd: demoSrc, base: demoSrc}) 349 | .pipe(gulp.dest(demoDist)) 350 | 351 | return gulp.watch('**/*', {cwd: demoSrc, base: demoSrc}) 352 | .on('change', watchCallback) 353 | .on('add', watchCallback) 354 | .on('unlink', watchCallback) 355 | }) 356 | 357 | /** 358 | * 监听安装包列表变化 359 | */ 360 | gulp.task(`${id}-watch-install`, () => gulp.watch(path.resolve(__dirname, '../package.json'), install())) 361 | 362 | /** 363 | * 构建相关任务 364 | */ 365 | gulp.task(`${id}-build`, gulp.series(`${id}-clean-dist`, `${id}-component-check`, gulp.parallel(`${id}-component-wxml`, `${id}-component-wxss`, `${id}-component-js`, `${id}-component-json`, `${id}-copy`))) 366 | 367 | gulp.task(`${id}-watch`, gulp.series(`${id}-build`, `${id}-demo`, `${id}-install`, gulp.parallel(`${id}-watch-wxml`, `${id}-watch-wxss`, `${id}-watch-js`, `${id}-watch-json`, `${id}-watch-copy`, `${id}-watch-install`, `${id}-watch-demo`))) 368 | 369 | gulp.task(`${id}-dev`, gulp.series(`${id}-build`, `${id}-demo`, `${id}-install`)) 370 | 371 | gulp.task(`${id}-default`, gulp.series(`${id}-build`)) 372 | } 373 | } 374 | 375 | module.exports = BuildTask 376 | -------------------------------------------------------------------------------- /src/color.class.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Sugar on 2020/5/26. 3 | */ 4 | const {max, min} = require('./utils/index') 5 | 6 | class ColorClass { 7 | constructor() { 8 | 9 | } 10 | 11 | _tryParsingColor(color) { 12 | let source; 13 | 14 | if (color in ColorClass.colorNameMap) { 15 | color = ColorClass.colorNameMap[color]; 16 | } 17 | 18 | if (color === 'transparent') { 19 | source = [255, 255, 255, 0]; 20 | } 21 | 22 | if (!source) { 23 | source = ColorClass.sourceFromHex(color); 24 | } 25 | if (!source) { 26 | source = ColorClass.sourceFromRgb(color); 27 | } 28 | if (!source) { 29 | source = ColorClass.sourceFromHsl(color); 30 | } 31 | if (!source) { 32 | source = [0, 0, 0, 1]; 33 | } 34 | if (source) { 35 | this.setSource(source); 36 | } 37 | } 38 | 39 | _rgbToHsl(r, g, b) { 40 | r /= 255; 41 | g /= 255; 42 | b /= 255; 43 | 44 | let h, s, l, 45 | max = max([r, g, b]), 46 | min = min([r, g, b]); 47 | 48 | l = (max + min) / 2; 49 | 50 | if (max === min) { 51 | h = s = 0; // achromatic 52 | } else { 53 | let d = max - min; 54 | s = l > 0.5 ? d / (2 - max - min) : d / (max + min); 55 | switch (max) { 56 | case r: 57 | h = (g - b) / d + (g < b ? 6 : 0); 58 | break; 59 | case g: 60 | h = (b - r) / d + 2; 61 | break; 62 | case b: 63 | h = (r - g) / d + 4; 64 | break; 65 | } 66 | h /= 6; 67 | } 68 | 69 | return [ 70 | Math.round(h * 360), 71 | Math.round(s * 100), 72 | Math.round(l * 100) 73 | ]; 74 | } 75 | 76 | getSource() { 77 | return this._source; 78 | } 79 | 80 | setSource(source) { 81 | this._source = source; 82 | } 83 | 84 | toRgb() { 85 | let source = this.getSource(); 86 | return 'rgb(' + source[0] + ',' + source[1] + ',' + source[2] + ')'; 87 | } 88 | 89 | toRgba() { 90 | let source = this.getSource(); 91 | return 'rgba(' + source[0] + ',' + source[1] + ',' + source[2] + ',' + source[3] + ')'; 92 | } 93 | 94 | toHsl() { 95 | let source = this.getSource(), 96 | hsl = this._rgbToHsl(source[0], source[1], source[2]); 97 | 98 | return 'hsl(' + hsl[0] + ',' + hsl[1] + '%,' + hsl[2] + '%)'; 99 | } 100 | 101 | toHsla() { 102 | let source = this.getSource(), 103 | hsl = this._rgbToHsl(source[0], source[1], source[2]); 104 | 105 | return 'hsla(' + hsl[0] + ',' + hsl[1] + '%,' + hsl[2] + '%,' + source[3] + ')'; 106 | } 107 | 108 | toHex() { 109 | let source = this.getSource(), r, g, b; 110 | 111 | r = source[0].toString(16); 112 | r = (r.length === 1) ? ('0' + r) : r; 113 | 114 | g = source[1].toString(16); 115 | g = (g.length === 1) ? ('0' + g) : g; 116 | 117 | b = source[2].toString(16); 118 | b = (b.length === 1) ? ('0' + b) : b; 119 | 120 | return r.toUpperCase() + g.toUpperCase() + b.toUpperCase(); 121 | } 122 | 123 | toHexa() { 124 | let source = this.getSource(), a; 125 | 126 | a = Math.round(source[3] * 255); 127 | a = a.toString(16); 128 | a = (a.length === 1) ? ('0' + a) : a; 129 | 130 | return this.toHex() + a.toUpperCase(); 131 | } 132 | 133 | getAlpha() { 134 | return this.getSource()[3]; 135 | } 136 | 137 | setAlpha(alpha) { 138 | let source = this.getSource(); 139 | source[3] = alpha; 140 | this.setSource(source); 141 | return this; 142 | } 143 | 144 | toGrayscale() { 145 | let source = this.getSource(), 146 | average = parseInt((source[0] * 0.3 + source[1] * 0.59 + source[2] * 0.11).toFixed(0), 10), 147 | currentAlpha = source[3]; 148 | this.setSource([average, average, average, currentAlpha]); 149 | return this; 150 | } 151 | 152 | toBlackWhite(threshold) { 153 | let source = this.getSource(), 154 | average = (source[0] * 0.3 + source[1] * 0.59 + source[2] * 0.11).toFixed(0), 155 | currentAlpha = source[3]; 156 | 157 | threshold = threshold || 127; 158 | 159 | average = (Number(average) < Number(threshold)) ? 0 : 255; 160 | this.setSource([average, average, average, currentAlpha]); 161 | return this; 162 | } 163 | 164 | overlayWith(otherColor) { 165 | if (!(otherColor instanceof Color)) { 166 | otherColor = new ColorClass(otherColor); 167 | } 168 | 169 | let result = [], 170 | alpha = this.getAlpha(), 171 | otherAlpha = 0.5, 172 | source = this.getSource(), 173 | otherSource = otherColorClass.getSource(), i; 174 | 175 | for (i = 0; i < 3; i++) { 176 | result.push(Math.round((source[i] * (1 - otherAlpha)) + (otherSource[i] * otherAlpha))); 177 | } 178 | 179 | result[3] = alpha; 180 | this.setSource(result); 181 | return this; 182 | } 183 | } 184 | 185 | ColorClass.reRGBa = /^rgba?\(\s*(\d{1,3}(?:\.\d+)?\%?)\s*,\s*(\d{1,3}(?:\.\d+)?\%?)\s*,\s*(\d{1,3}(?:\.\d+)?\%?)\s*(?:\s*,\s*((?:\d*\.?\d+)?)\s*)?\)$/i; 186 | 187 | ColorClass.reHSLa = /^hsla?\(\s*(\d{1,3})\s*,\s*(\d{1,3}\%)\s*,\s*(\d{1,3}\%)\s*(?:\s*,\s*(\d+(?:\.\d+)?)\s*)?\)$/i; 188 | 189 | ColorClass.reHex = /^#?([0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{4}|[0-9a-f]{3})$/i; 190 | 191 | ColorClass.colorNameMap = { 192 | aliceblue: '#F0F8FF', 193 | antiquewhite: '#FAEBD7', 194 | aqua: '#00FFFF', 195 | aquamarine: '#7FFFD4', 196 | azure: '#F0FFFF', 197 | beige: '#F5F5DC', 198 | bisque: '#FFE4C4', 199 | black: '#000000', 200 | blanchedalmond: '#FFEBCD', 201 | blue: '#0000FF', 202 | blueviolet: '#8A2BE2', 203 | brown: '#A52A2A', 204 | burlywood: '#DEB887', 205 | cadetblue: '#5F9EA0', 206 | chartreuse: '#7FFF00', 207 | chocolate: '#D2691E', 208 | coral: '#FF7F50', 209 | cornflowerblue: '#6495ED', 210 | cornsilk: '#FFF8DC', 211 | crimson: '#DC143C', 212 | cyan: '#00FFFF', 213 | darkblue: '#00008B', 214 | darkcyan: '#008B8B', 215 | darkgoldenrod: '#B8860B', 216 | darkgray: '#A9A9A9', 217 | darkgrey: '#A9A9A9', 218 | darkgreen: '#006400', 219 | darkkhaki: '#BDB76B', 220 | darkmagenta: '#8B008B', 221 | darkolivegreen: '#556B2F', 222 | darkorange: '#FF8C00', 223 | darkorchid: '#9932CC', 224 | darkred: '#8B0000', 225 | darksalmon: '#E9967A', 226 | darkseagreen: '#8FBC8F', 227 | darkslateblue: '#483D8B', 228 | darkslategray: '#2F4F4F', 229 | darkslategrey: '#2F4F4F', 230 | darkturquoise: '#00CED1', 231 | darkviolet: '#9400D3', 232 | deeppink: '#FF1493', 233 | deepskyblue: '#00BFFF', 234 | dimgray: '#696969', 235 | dimgrey: '#696969', 236 | dodgerblue: '#1E90FF', 237 | firebrick: '#B22222', 238 | floralwhite: '#FFFAF0', 239 | forestgreen: '#228B22', 240 | fuchsia: '#FF00FF', 241 | gainsboro: '#DCDCDC', 242 | ghostwhite: '#F8F8FF', 243 | gold: '#FFD700', 244 | goldenrod: '#DAA520', 245 | gray: '#808080', 246 | grey: '#808080', 247 | green: '#008000', 248 | greenyellow: '#ADFF2F', 249 | honeydew: '#F0FFF0', 250 | hotpink: '#FF69B4', 251 | indianred: '#CD5C5C', 252 | indigo: '#4B0082', 253 | ivory: '#FFFFF0', 254 | khaki: '#F0E68C', 255 | lavender: '#E6E6FA', 256 | lavenderblush: '#FFF0F5', 257 | lawngreen: '#7CFC00', 258 | lemonchiffon: '#FFFACD', 259 | lightblue: '#ADD8E6', 260 | lightcoral: '#F08080', 261 | lightcyan: '#E0FFFF', 262 | lightgoldenrodyellow: '#FAFAD2', 263 | lightgray: '#D3D3D3', 264 | lightgrey: '#D3D3D3', 265 | lightgreen: '#90EE90', 266 | lightpink: '#FFB6C1', 267 | lightsalmon: '#FFA07A', 268 | lightseagreen: '#20B2AA', 269 | lightskyblue: '#87CEFA', 270 | lightslategray: '#778899', 271 | lightslategrey: '#778899', 272 | lightsteelblue: '#B0C4DE', 273 | lightyellow: '#FFFFE0', 274 | lime: '#00FF00', 275 | limegreen: '#32CD32', 276 | linen: '#FAF0E6', 277 | magenta: '#FF00FF', 278 | maroon: '#800000', 279 | mediumaquamarine: '#66CDAA', 280 | mediumblue: '#0000CD', 281 | mediumorchid: '#BA55D3', 282 | mediumpurple: '#9370DB', 283 | mediumseagreen: '#3CB371', 284 | mediumslateblue: '#7B68EE', 285 | mediumspringgreen: '#00FA9A', 286 | mediumturquoise: '#48D1CC', 287 | mediumvioletred: '#C71585', 288 | midnightblue: '#191970', 289 | mintcream: '#F5FFFA', 290 | mistyrose: '#FFE4E1', 291 | moccasin: '#FFE4B5', 292 | navajowhite: '#FFDEAD', 293 | navy: '#000080', 294 | oldlace: '#FDF5E6', 295 | olive: '#808000', 296 | olivedrab: '#6B8E23', 297 | orange: '#FFA500', 298 | orangered: '#FF4500', 299 | orchid: '#DA70D6', 300 | palegoldenrod: '#EEE8AA', 301 | palegreen: '#98FB98', 302 | paleturquoise: '#AFEEEE', 303 | palevioletred: '#DB7093', 304 | papayawhip: '#FFEFD5', 305 | peachpuff: '#FFDAB9', 306 | peru: '#CD853F', 307 | pink: '#FFC0CB', 308 | plum: '#DDA0DD', 309 | powderblue: '#B0E0E6', 310 | purple: '#800080', 311 | rebeccapurple: '#663399', 312 | red: '#FF0000', 313 | rosybrown: '#BC8F8F', 314 | royalblue: '#4169E1', 315 | saddlebrown: '#8B4513', 316 | salmon: '#FA8072', 317 | sandybrown: '#F4A460', 318 | seagreen: '#2E8B57', 319 | seashell: '#FFF5EE', 320 | sienna: '#A0522D', 321 | silver: '#C0C0C0', 322 | skyblue: '#87CEEB', 323 | slateblue: '#6A5ACD', 324 | slategray: '#708090', 325 | slategrey: '#708090', 326 | snow: '#FFFAFA', 327 | springgreen: '#00FF7F', 328 | steelblue: '#4682B4', 329 | tan: '#D2B48C', 330 | teal: '#008080', 331 | thistle: '#D8BFD8', 332 | tomato: '#FF6347', 333 | turquoise: '#40E0D0', 334 | violet: '#EE82EE', 335 | wheat: '#F5DEB3', 336 | white: '#FFFFFF', 337 | whitesmoke: '#F5F5F5', 338 | yellow: '#FFFF00', 339 | yellowgreen: '#9ACD32' 340 | }; 341 | 342 | 343 | function hue2rgb(p, q, t) { 344 | if (t < 0) { 345 | t += 1; 346 | } 347 | if (t > 1) { 348 | t -= 1; 349 | } 350 | if (t < 1 / 6) { 351 | return p + (q - p) * 6 * t; 352 | } 353 | if (t < 1 / 2) { 354 | return q; 355 | } 356 | if (t < 2 / 3) { 357 | return p + (q - p) * (2 / 3 - t) * 6; 358 | } 359 | return p; 360 | } 361 | 362 | ColorClass.fromRgb = function (color) { 363 | return ColorClass.fromSource(ColorClass.sourceFromRgb(color)); 364 | }; 365 | 366 | ColorClass.sourceFromRgb = function (color) { 367 | let match = color.match(ColorClass.reRGBa); 368 | if (match) { 369 | let r = parseInt(match[1], 10) / (/%$/.test(match[1]) ? 100 : 1) * (/%$/.test(match[1]) ? 255 : 1), 370 | g = parseInt(match[2], 10) / (/%$/.test(match[2]) ? 100 : 1) * (/%$/.test(match[2]) ? 255 : 1), 371 | b = parseInt(match[3], 10) / (/%$/.test(match[3]) ? 100 : 1) * (/%$/.test(match[3]) ? 255 : 1); 372 | 373 | return [ 374 | parseInt(r, 10), 375 | parseInt(g, 10), 376 | parseInt(b, 10), 377 | match[4] ? parseFloat(match[4]) : 1 378 | ]; 379 | } 380 | }; 381 | 382 | ColorClass.fromRgba = ColorClass.fromRgb; 383 | 384 | ColorClass.fromHsl = function (color) { 385 | return ColorClass.fromSource(ColorClass.sourceFromHsl(color)); 386 | }; 387 | 388 | ColorClass.sourceFromHsl = function (color) { 389 | let match = color.match(ColorClass.reHSLa); 390 | if (!match) { 391 | return; 392 | } 393 | 394 | let h = (((parseFloat(match[1]) % 360) + 360) % 360) / 360, 395 | s = parseFloat(match[2]) / (/%$/.test(match[2]) ? 100 : 1), 396 | l = parseFloat(match[3]) / (/%$/.test(match[3]) ? 100 : 1), 397 | r, g, b; 398 | 399 | if (s === 0) { 400 | r = g = b = l; 401 | } else { 402 | let q = l <= 0.5 ? l * (s + 1) : l + s - l * s, 403 | p = l * 2 - q; 404 | 405 | r = hue2rgb(p, q, h + 1 / 3); 406 | g = hue2rgb(p, q, h); 407 | b = hue2rgb(p, q, h - 1 / 3); 408 | } 409 | 410 | return [ 411 | Math.round(r * 255), 412 | Math.round(g * 255), 413 | Math.round(b * 255), 414 | match[4] ? parseFloat(match[4]) : 1 415 | ]; 416 | }; 417 | 418 | ColorClass.fromHsla = ColorClass.fromHsl; 419 | 420 | ColorClass.fromHex = function (color) { 421 | return ColorClass.fromSource(ColorClass.sourceFromHex(color)); 422 | }; 423 | 424 | ColorClass.sourceFromHex = function (color) { 425 | if (color.match(ColorClass.reHex)) { 426 | let value = color.slice(color.indexOf('#') + 1), 427 | isShortNotation = (value.length === 3 || value.length === 4), 428 | isRGBa = (value.length === 8 || value.length === 4), 429 | r = isShortNotation ? (value.charAt(0) + value.charAt(0)) : value.substring(0, 2), 430 | g = isShortNotation ? (value.charAt(1) + value.charAt(1)) : value.substring(2, 4), 431 | b = isShortNotation ? (value.charAt(2) + value.charAt(2)) : value.substring(4, 6), 432 | a = isRGBa ? (isShortNotation ? (value.charAt(3) + value.charAt(3)) : value.substring(6, 8)) : 'FF'; 433 | 434 | return [ 435 | parseInt(r, 16), 436 | parseInt(g, 16), 437 | parseInt(b, 16), 438 | parseFloat((parseInt(a, 16) / 255).toFixed(2)) 439 | ]; 440 | } 441 | }; 442 | 443 | ColorClass.fromSource = function (source) { 444 | let oColor = new Color(); 445 | oColorClass.setSource(source); 446 | return oColor; 447 | }; 448 | 449 | module.exports = ColorClass 450 | -------------------------------------------------------------------------------- /src/mixins/object_geometry.mixin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Sugar on 2020/5/30. 3 | */ 4 | 5 | const PointClass = require('../point.class') 6 | const IntersectionClass = require('../intersection.class') 7 | const { 8 | degreesToRadians, 9 | multiplyTransformMatrices, 10 | transformPoint, 11 | calcDimensionsMatrix, 12 | sizeAfterTransform, 13 | makeBoundingBoxFromPoints, 14 | cos, sin, 15 | calcRotateMatrix, 16 | composeMatrix, 17 | } = require('../utils/misc') 18 | 19 | function arrayFromCoords(coords) { 20 | return [ 21 | new PointClass(coords.tl.x, coords.tl.y), 22 | new PointClass(coords.tr.x, coords.tr.y), 23 | new PointClass(coords.br.x, coords.br.y), 24 | new PointClass(coords.bl.x, coords.bl.y) 25 | ]; 26 | } 27 | 28 | 29 | module.exports = { 30 | oCoords: null, 31 | aCoords: null, 32 | lineCoords: null, 33 | ownMatrixCache: null, 34 | matrixCache: null, 35 | controls: {}, 36 | _getCoords: function (absolute, calculate) { 37 | if (calculate) { 38 | return (absolute ? this.calcACoords() : this.calcLineCoords()); 39 | } 40 | if (!this.aCoords || !this.lineCoords) { 41 | this.setCoords(true); 42 | } 43 | return (absolute ? this.aCoords : this.lineCoords); 44 | }, 45 | 46 | getCoords: function (absolute, calculate) { 47 | return arrayFromCoords(this._getCoords(absolute, calculate)); 48 | }, 49 | 50 | intersectsWithRect: function (pointTL, pointBR, absolute, calculate) { 51 | let coords = this.getCoords(absolute, calculate), 52 | intersection = IntersectionClass.intersectPolygonRectangle( 53 | coords, 54 | pointTL, 55 | pointBR 56 | ); 57 | return intersection.status === 'Intersection'; 58 | }, 59 | 60 | intersectsWithObject: function (other, absolute, calculate) { 61 | let intersection = IntersectionClass.intersectPolygonPolygon( 62 | this.getCoords(absolute, calculate), 63 | other.getCoords(absolute, calculate) 64 | ); 65 | 66 | return intersection.status === 'Intersection' 67 | || other.isContainedWithinObject(this, absolute, calculate) 68 | || this.isContainedWithinObject(other, absolute, calculate); 69 | }, 70 | 71 | isContainedWithinObject: function (other, absolute, calculate) { 72 | let points = this.getCoords(absolute, calculate), 73 | otherCoords = absolute ? other.aCoords : other.lineCoords, 74 | i = 0, lines = other._getImageLines(otherCoords); 75 | for (; i < 4; i++) { 76 | if (!other.containsPoint(points[i], lines)) { 77 | return false; 78 | } 79 | } 80 | return true; 81 | }, 82 | 83 | isContainedWithinRect: function (pointTL, pointBR, absolute, calculate) { 84 | let boundingRect = this.getBoundingRect(absolute, calculate); 85 | 86 | return ( 87 | boundingRect.left >= pointTL.x && 88 | boundingRect.left + boundingRect.width <= pointBR.x && 89 | boundingRect.top >= pointTL.y && 90 | boundingRect.top + boundingRect.height <= pointBR.y 91 | ); 92 | }, 93 | 94 | containsPoint: function (point, lines, absolute, calculate) { 95 | let coords = this._getCoords(absolute, calculate); 96 | let l = lines || this._getImageLines(coords); 97 | let xPoints = this._findCrossPoints(point, l); 98 | return (xPoints !== 0 && xPoints % 2 === 1); 99 | }, 100 | 101 | isOnScreen: function (calculate) { 102 | if (!this.canvas) { 103 | return false; 104 | } 105 | let pointTL = this.canvas.vptCoords.tl, pointBR = this.canvas.vptCoords.br; 106 | let points = this.getCoords(true, calculate), point; 107 | for (let i = 0; i < 4; i++) { 108 | point = points[i]; 109 | if (point.x <= pointBR.x && point.x >= pointTL.x && point.y <= pointBR.y && point.y >= pointTL.y) { 110 | return true; 111 | } 112 | } 113 | // no points on screen, check intersection with absolute coordinates 114 | if (this.intersectsWithRect(pointTL, pointBR, true, calculate)) { 115 | return true; 116 | } 117 | return this._containsCenterOfCanvas(pointTL, pointBR, calculate); 118 | }, 119 | 120 | _containsCenterOfCanvas: function (pointTL, pointBR, calculate) { 121 | // worst case scenario the object is so big that contains the screen 122 | let centerPoint = {x: (pointTL.x + pointBR.x) / 2, y: (pointTL.y + pointBR.y) / 2}; 123 | if (this.containsPoint(centerPoint, null, true, calculate)) { 124 | return true; 125 | } 126 | return false; 127 | }, 128 | 129 | isPartiallyOnScreen: function (calculate) { 130 | if (!this.canvas) { 131 | return false; 132 | } 133 | let pointTL = this.canvas.vptCoords.tl, pointBR = this.canvas.vptCoords.br; 134 | if (this.intersectsWithRect(pointTL, pointBR, true, calculate)) { 135 | return true; 136 | } 137 | return this._containsCenterOfCanvas(pointTL, pointBR, calculate); 138 | }, 139 | 140 | _getImageLines: function (oCoords) { 141 | 142 | let lines = { 143 | topline: { 144 | o: oCoords.tl, 145 | d: oCoords.tr 146 | }, 147 | rightline: { 148 | o: oCoords.tr, 149 | d: oCoords.br 150 | }, 151 | bottomline: { 152 | o: oCoords.br, 153 | d: oCoords.bl 154 | }, 155 | leftline: { 156 | o: oCoords.bl, 157 | d: oCoords.tl 158 | } 159 | }; 160 | 161 | // // debugging 162 | // if (this.canvas.contextTop) { 163 | // this.canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2); 164 | // this.canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2); 165 | // 166 | // this.canvas.contextTop.fillRect(lines.leftline.d.x, lines.leftline.d.y, 2, 2); 167 | // this.canvas.contextTop.fillRect(lines.leftline.o.x, lines.leftline.o.y, 2, 2); 168 | // 169 | // this.canvas.contextTop.fillRect(lines.topline.d.x, lines.topline.d.y, 2, 2); 170 | // this.canvas.contextTop.fillRect(lines.topline.o.x, lines.topline.o.y, 2, 2); 171 | // 172 | // this.canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2); 173 | // this.canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2); 174 | // } 175 | 176 | return lines; 177 | }, 178 | 179 | _findCrossPoints: function (point, lines) { 180 | let b1, b2, a1, a2, xi, // yi, 181 | xcount = 0, 182 | iLine; 183 | 184 | for (let lineKey in lines) { 185 | iLine = lines[lineKey]; 186 | // optimisation 1: line below point. no cross 187 | if ((iLine.o.y < point.y) && (iLine.d.y < point.y)) { 188 | continue; 189 | } 190 | // optimisation 2: line above point. no cross 191 | if ((iLine.o.y >= point.y) && (iLine.d.y >= point.y)) { 192 | continue; 193 | } 194 | // optimisation 3: vertical line case 195 | if ((iLine.o.x === iLine.d.x) && (iLine.o.x >= point.x)) { 196 | xi = iLine.o.x; 197 | // yi = point.y; 198 | } 199 | // calculate the intersection point 200 | else { 201 | b1 = 0; 202 | b2 = (iLine.d.y - iLine.o.y) / (iLine.d.x - iLine.o.x); 203 | a1 = point.y - b1 * point.x; 204 | a2 = iLine.o.y - b2 * iLine.o.x; 205 | 206 | xi = -(a1 - a2) / (b1 - b2); 207 | // yi = a1 + b1 * xi; 208 | } 209 | // dont count xi < point.x cases 210 | if (xi >= point.x) { 211 | xcount += 1; 212 | } 213 | // optimisation 4: specific for square images 214 | if (xcount === 2) { 215 | break; 216 | } 217 | } 218 | return xcount; 219 | }, 220 | 221 | getBoundingRect: function (absolute, calculate) { 222 | let coords = this.getCoords(absolute, calculate); 223 | return makeBoundingBoxFromPoints(coords); 224 | }, 225 | 226 | getScaledWidth: function () { 227 | return this._getTransformedDimensions().x; 228 | }, 229 | 230 | getScaledHeight: function () { 231 | return this._getTransformedDimensions().y; 232 | }, 233 | 234 | _constrainScale: function (value) { 235 | if (Math.abs(value) < this.minScaleLimit) { 236 | if (value < 0) { 237 | return -this.minScaleLimit; 238 | } else { 239 | return this.minScaleLimit; 240 | } 241 | } else if (value === 0) { 242 | return 0.0001; 243 | } 244 | return value; 245 | }, 246 | 247 | scale: function (value) { 248 | this._set('scaleX', value); 249 | this._set('scaleY', value); 250 | return this.setCoords(); 251 | }, 252 | 253 | scaleToWidth: function (value, absolute) { 254 | // adjust to bounding rect factor so that rotated shapes would fit as well 255 | let boundingRectFactor = this.getBoundingRect(absolute).width / this.getScaledWidth(); 256 | return this.scale(value / this.width / boundingRectFactor); 257 | }, 258 | 259 | scaleToHeight: function (value, absolute) { 260 | // adjust to bounding rect factor so that rotated shapes would fit as well 261 | let boundingRectFactor = this.getBoundingRect(absolute).height / this.getScaledHeight(); 262 | return this.scale(value / this.height / boundingRectFactor); 263 | }, 264 | 265 | calcCoords: function (absolute) { 266 | // this is a compatibility function to avoid removing calcCoords now. 267 | if (absolute) { 268 | return this.calcACoords(); 269 | } 270 | return this.calcOCoords(); 271 | }, 272 | 273 | calcLineCoords: function () { 274 | var vpt = this.getViewportTransform(), 275 | padding = this.padding, angle = degreesToRadians(this.angle), 276 | c = cos(angle), s = sin(angle), 277 | cosP = c * padding, sinP = s * padding, cosPSinP = cosP + sinP, 278 | cosPMinusSinP = cosP - sinP, aCoords = this.calcACoords(); 279 | 280 | let lineCoords = { 281 | tl: transformPoint(aCoords.tl, vpt), 282 | tr: transformPoint(aCoords.tr, vpt), 283 | bl: transformPoint(aCoords.bl, vpt), 284 | br: transformPoint(aCoords.br, vpt), 285 | }; 286 | 287 | if (padding) { 288 | lineCoords.tl.x -= cosPMinusSinP; 289 | lineCoords.tl.y -= cosPSinP; 290 | lineCoords.tr.x += cosPSinP; 291 | lineCoords.tr.y -= cosPMinusSinP; 292 | lineCoords.bl.x -= cosPSinP; 293 | lineCoords.bl.y += cosPMinusSinP; 294 | lineCoords.br.x += cosPMinusSinP; 295 | lineCoords.br.y += cosPSinP; 296 | } 297 | 298 | return lineCoords; 299 | }, 300 | 301 | calcOCoords: function () { 302 | var rotateMatrix = this._calcRotateMatrix(), 303 | translateMatrix = this._calcTranslateMatrix(), 304 | vpt = this.getViewportTransform(), 305 | startMatrix = multiplyTransformMatrices(vpt, translateMatrix), 306 | finalMatrix = multiplyTransformMatrices(startMatrix, rotateMatrix), 307 | finalMatrix = multiplyTransformMatrices(finalMatrix, [1 / vpt[0], 0, 0, 1 / vpt[3], 0, 0]), 308 | dim = this._calculateCurrentDimensions(), 309 | coords = {}; 310 | this.forEachControl(function (control, key, object) { 311 | coords[key] = control.positionHandler(dim, finalMatrix, object); 312 | }); 313 | 314 | // debug code 315 | // let canvas = this.canvas; 316 | // setTimeout(function() { 317 | // canvas.contextTop.clearRect(0, 0, 700, 700); 318 | // canvas.contextTop.fillStyle = 'green'; 319 | // Object.keys(coords).forEach(function(key) { 320 | // let control = coords[key]; 321 | // canvas.contextTop.fillRect(control.x, control.y, 3, 3); 322 | // }); 323 | // }, 50); 324 | return coords; 325 | }, 326 | 327 | calcACoords: function () { 328 | let rotateMatrix = this._calcRotateMatrix(), 329 | translateMatrix = this._calcTranslateMatrix(), 330 | finalMatrix = multiplyTransformMatrices(translateMatrix, rotateMatrix), 331 | dim = this._getTransformedDimensions(), 332 | w = dim.x / 2, h = dim.y / 2; 333 | return { 334 | // corners 335 | tl: transformPoint({x: -w, y: -h}, finalMatrix), 336 | tr: transformPoint({x: w, y: -h}, finalMatrix), 337 | bl: transformPoint({x: -w, y: h}, finalMatrix), 338 | br: transformPoint({x: w, y: h}, finalMatrix) 339 | }; 340 | }, 341 | 342 | setCoords: function (skipCorners) { 343 | this.aCoords = this.calcACoords(); 344 | this.lineCoords = this.calcLineCoords(); 345 | if (skipCorners) { 346 | return this; 347 | } 348 | // set coordinates of the draggable boxes in the corners used to scale/rotate the image 349 | this.oCoords = this.calcOCoords(); 350 | this._setCornerCoords && this._setCornerCoords(); 351 | return this; 352 | }, 353 | 354 | _calcRotateMatrix: function () { 355 | return calcRotateMatrix(this); 356 | }, 357 | 358 | _calcTranslateMatrix: function () { 359 | let center = this.getCenterPoint(); 360 | return [1, 0, 0, 1, center.x, center.y]; 361 | }, 362 | 363 | transformMatrixKey: function (skipGroup) { 364 | let sep = '_', prefix = ''; 365 | if (!skipGroup && this.group) { 366 | prefix = this.group.transformMatrixKey(skipGroup) + sep; 367 | } 368 | ; 369 | return prefix + this.top + sep + this.left + sep + this.scaleX + sep + this.scaleY + 370 | sep + this.skewX + sep + this.skewY + sep + this.angle + sep + this.originX + sep + this.originY + 371 | sep + this.width + sep + this.height + sep + this.strokeWidth + this.flipX + this.flipY; 372 | }, 373 | 374 | calcTransformMatrix: function (skipGroup) { 375 | let matrix = this.calcOwnMatrix(); 376 | if (skipGroup || !this.group) { 377 | return matrix; 378 | } 379 | let key = this.transformMatrixKey(skipGroup), cache = this.matrixCache || (this.matrixCache = {}); 380 | if (cache.key === key) { 381 | return cache.value; 382 | } 383 | if (this.group) { 384 | matrix = multiplyTransformMatrices(this.group.calcTransformMatrix(false), matrix); 385 | } 386 | cache.key = key; 387 | cache.value = matrix; 388 | return matrix; 389 | }, 390 | 391 | calcOwnMatrix: function () { 392 | let key = this.transformMatrixKey(true), cache = this.ownMatrixCache || (this.ownMatrixCache = {}); 393 | if (cache.key === key) { 394 | return cache.value; 395 | } 396 | let tMatrix = this._calcTranslateMatrix(), 397 | options = { 398 | angle: this.angle, 399 | translateX: tMatrix[4], 400 | translateY: tMatrix[5], 401 | scaleX: this.scaleX, 402 | scaleY: this.scaleY, 403 | skewX: this.skewX, 404 | skewY: this.skewY, 405 | flipX: this.flipX, 406 | flipY: this.flipY, 407 | }; 408 | cache.key = key; 409 | cache.value = composeMatrix(options); 410 | return cache.value; 411 | }, 412 | 413 | _calcDimensionsTransformMatrix: function (skewX, skewY, flipping) { 414 | return calcDimensionsMatrix({ 415 | skewX: skewX, 416 | skewY: skewY, 417 | scaleX: this.scaleX * (flipping && this.flipX ? -1 : 1), 418 | scaleY: this.scaleY * (flipping && this.flipY ? -1 : 1) 419 | }); 420 | }, 421 | 422 | _getNonTransformedDimensions: function () { 423 | let strokeWidth = this.strokeWidth, 424 | w = this.width + strokeWidth, 425 | h = this.height + strokeWidth; 426 | return {x: w, y: h}; 427 | }, 428 | 429 | _getTransformedDimensions: function (skewX, skewY) { 430 | if (typeof skewX === 'undefined') { 431 | skewX = this.skewX; 432 | } 433 | if (typeof skewY === 'undefined') { 434 | skewY = this.skewY; 435 | } 436 | let dimensions = this._getNonTransformedDimensions(), dimX, dimY, 437 | noSkew = skewX === 0 && skewY === 0; 438 | 439 | if (this.strokeUniform) { 440 | dimX = this.width; 441 | dimY = this.height; 442 | } else { 443 | dimX = dimensions.x; 444 | dimY = dimensions.y; 445 | } 446 | if (noSkew) { 447 | return this._finalizeDimensions(dimX * this.scaleX, dimY * this.scaleY); 448 | } 449 | let bbox = sizeAfterTransform(dimX, dimY, { 450 | scaleX: this.scaleX, 451 | scaleY: this.scaleY, 452 | skewX: skewX, 453 | skewY: skewY, 454 | }); 455 | return this._finalizeDimensions(bbox.x, bbox.y); 456 | }, 457 | 458 | _finalizeDimensions: function (width, height) { 459 | return this.strokeUniform ? 460 | {x: width + this.strokeWidth, y: height + this.strokeWidth} 461 | : 462 | {x: width, y: height}; 463 | }, 464 | 465 | _calculateCurrentDimensions: function () { 466 | let vpt = this.getViewportTransform(), 467 | dim = this._getTransformedDimensions(), 468 | p = transformPoint(dim, vpt, true); 469 | return p.scalarAdd(2 * this.padding); 470 | }, 471 | } 472 | -------------------------------------------------------------------------------- /src/canvas.class.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Sugar on 2020/5/26. 3 | */ 4 | const {invertTransform, transformPoint, loadImage} = require('./utils/misc') 5 | const PointClass = require('point.class') 6 | const ImageClass = require('./shapes/image.class') 7 | 8 | class CanvasClass { 9 | constructor(options) { 10 | let canvas = options.canvas 11 | let ctx = canvas ? canvas.getContext('2d') : null 12 | if (!canvas || !ctx) { 13 | throw new Error(`请传入组件节点`) 14 | } 15 | const dpr = wx.getSystemInfoSync().pixelRatio 16 | canvas.width = options.width * dpr 17 | canvas.height = options.height * dpr 18 | ctx.scale(dpr, dpr) 19 | this.ctx = ctx 20 | this.canvas = canvas 21 | this.dpr = dpr 22 | // this.width = width 23 | // this.height = height 24 | // this._objects = [] 25 | 26 | this.backgroundImage = null // 画布实例的背景图像 27 | this.backgroundColor = '' // 画布实例的背景颜色 28 | this.overlayImage = null 29 | this.overlayColor = '' 30 | 31 | this.backgroundVpt = true 32 | this.overlayVpt = true 33 | this.controlsAboveOverlay = false // 是否在覆盖图像上方渲染对象 34 | 35 | this.viewportTransform = [1, 0, 0, 1, 0, 0] 36 | this.vptCoords = {} // 画布的四个角左边,属性为tl,tr,bl,br 37 | 38 | this.preserveObjectStacking = false 39 | this.allowTouchScrolling = true 40 | 41 | this.initialize(options) 42 | } 43 | 44 | initialize(options) { 45 | // this.renderAndResetBound = this.renderAndReset.bind(this); 46 | this.requestRenderAllBound = this.requestRenderAll.bind(this) 47 | this._initStatic(options) 48 | this._initInteractive() 49 | } 50 | 51 | _initInteractive() { 52 | this._currentTransform = null 53 | this._groupSelector = null 54 | // this._initEventListeners() 55 | 56 | // this._initRetinaScaling() 57 | 58 | // this.calcOffset() 59 | } 60 | 61 | 62 | /** 63 | * @private 64 | * @param {Object} [options] object 65 | */ 66 | _initStatic(options) { 67 | let cb = this.requestRenderAllBound 68 | this._objects = [] 69 | this._initOptions(options); 70 | if (options.backgroundImage) { 71 | this.setBackgroundImage(options.backgroundImage, cb); 72 | } 73 | if (options.backgroundColor) { 74 | this.setBackgroundColor(options.backgroundColor, cb) 75 | } 76 | // if (options.overlayImage) { 77 | // this.setOverlayImage(options.overlayImage, cb); 78 | // } 79 | // if (options.overlayColor) { 80 | // this.setOverlayColor(options.overlayColor, cb); 81 | // } 82 | } 83 | 84 | _initOptions(options) { 85 | this._setOptions(options); 86 | 87 | this.width = this.width || 0; 88 | this.height = this.height || 0; 89 | 90 | this.viewportTransform = this.viewportTransform.slice(); 91 | } 92 | 93 | _isRetinaScaling() { 94 | return (this.dpr !== 1 && this.enableRetinaScaling); 95 | } 96 | 97 | getRetinaScaling() { 98 | return this._isRetinaScaling() ? this.dpr : 1; 99 | } 100 | 101 | setBackgroundImage(image, callback, options) { 102 | return this.__setBgOverlayImage('backgroundImage', image, callback, options) 103 | } 104 | 105 | setBackgroundColor(backgroundColor, callback) { 106 | return this.__setBgOverlayColor('backgroundColor', backgroundColor, callback) 107 | } 108 | 109 | __setBgOverlayImage(property, image, callback, options) { 110 | if (typeof image === 'string') { 111 | loadImage(image, (img, isError) => { 112 | console.log('设置背景图', img); 113 | if (img) { 114 | let instance = new ImageClass(img, options) 115 | this[property] = instance 116 | instance.canvas = this 117 | } 118 | callback && callback(img, isError) 119 | }, this) 120 | } else { 121 | options && image.setOptions(options) 122 | this[property] = image 123 | image && (image.canvas = this) 124 | callback && callback(image, false) 125 | } 126 | 127 | return this 128 | } 129 | 130 | __setBgOverlayColor(property, color, callback) { 131 | this[property] = color 132 | this._initGradient(color, property) 133 | this._initPattern(color, property, callback) 134 | return this 135 | } 136 | 137 | add() { 138 | this._objects.push.apply(this._objects, arguments) 139 | if (this._onObjectAdded) { 140 | for (let i = 0; i < arguments.length; i++) { 141 | this._onObjectAdded(arguments[i]) 142 | } 143 | } 144 | this.requestRenderAll() 145 | return this 146 | } 147 | 148 | remove() { 149 | let objects = this._objects 150 | let index 151 | let somethingRemoved = false 152 | 153 | for (let i = 0; i < arguments.length; i++) { 154 | index = objects.indexOf(arguments[i]) 155 | 156 | if (index !== -1) { 157 | somethingRemoved = true 158 | objects.splice(index, 1) 159 | this._onObjectRemoved && this._onObjectRemoved(arguments[i]) 160 | } 161 | } 162 | 163 | somethingRemoved && this.requestRenderAll() 164 | return this 165 | } 166 | 167 | _onObjectAdded(obj) { 168 | // this.stateful && obj.setupState() 169 | obj._set('canvas', this); 170 | obj.setCoords(); 171 | this.fire('object:added', {target: obj}) 172 | obj.fire('added'); 173 | } 174 | 175 | _onObjectRemoved(obj) { 176 | if (obj === this._activeObject) { 177 | this.fire('before:selection:cleared', {target: obj}) 178 | this._discardActiveObject() 179 | this.fire('selection:cleared', {target: obj}) 180 | obj.fire('deselected'); 181 | } 182 | this.fire('object:removed', {target: obj}) 183 | obj.fire('removed') 184 | delete obj.canvas 185 | } 186 | 187 | /** 188 | * 清除画布元素的指定上下文 189 | * @param {CanvasContext} ctx 要清除的上下文 190 | * @return {sugar.Canvas} thisArg 191 | */ 192 | clearContext(ctx) { 193 | ctx.clearRect(0, 0, this.width, this.height) 194 | return this 195 | } 196 | 197 | /** 198 | * 返回绘制对象的画布的上下文 199 | * @return {CanvasContext} 200 | */ 201 | getContext() { 202 | return this.ctx 203 | } 204 | 205 | discardActiveObject(e) { 206 | let currentActives = this.getActiveObjects(), activeObject = this.getActiveObject() 207 | if (currentActives.length) { 208 | this.fire('before:selection:cleared', {target: activeObject, e: e}) 209 | } 210 | this._discardActiveObject(e) 211 | this._fireSelectionEvents(currentActives, e) 212 | return this 213 | } 214 | 215 | /** 216 | * 清除实例的所有上下文(背景,主要内容等) 217 | * @return {sugar.Canvas} thisArg 218 | */ 219 | clear() { 220 | this.discardActiveObject() 221 | this._objects.length = 0 222 | this.backgroundImage = null 223 | this.backgroundColor = '' 224 | this.clearContext(this.ctx) 225 | // this.fire('canvas:cleared') 226 | this.requestRenderAll() 227 | return this 228 | } 229 | 230 | _shouldClearSelection(e, target) { 231 | let activeObjects = this.getActiveObjects(), 232 | activeObject = this._activeObject 233 | 234 | return ( 235 | !target 236 | || 237 | (target && 238 | activeObject && 239 | activeObjects.length > 1 && 240 | activeObjects.indexOf(target) === -1 && 241 | activeObject !== target) 242 | || 243 | (target && !target.evented) 244 | || 245 | (target && 246 | !target.selectable && 247 | activeObject && 248 | activeObject !== target) 249 | ) 250 | } 251 | 252 | _setupCurrentTransform(e, target, alreadySelected) { 253 | if (!target) { 254 | return; 255 | } 256 | 257 | let pointer = this.getPointer(e), corner = target.__corner, 258 | // actionHandler = !!corner && target.controls[corner].getActionHandler(), 259 | // action = this._getActionFromCorner(alreadySelected, corner, e, target), 260 | // origin = this._getOriginFromCorner(target, corner), 261 | transform = { 262 | target: target, 263 | // action: action, 264 | action: 'drag', 265 | // actionHandler: actionHandler, 266 | corner: corner, 267 | scaleX: target.scaleX, 268 | scaleY: target.scaleY, 269 | skewX: target.skewX, 270 | skewY: target.skewY, 271 | offsetX: pointer.x - target.left, 272 | offsetY: pointer.y - target.top, 273 | // originX: origin.x, 274 | // originY: origin.y, 275 | originX: target.originX, 276 | originY: target.originY, 277 | ex: pointer.x, 278 | ey: pointer.y, 279 | lastX: pointer.x, 280 | lastY: pointer.y, 281 | // theta: degreesToRadians(target.angle), 282 | width: target.width * target.scaleX, 283 | }; 284 | 285 | // if (this._shouldCenterTransform(target, action, altKey)) { 286 | // transform.originX = 'center'; 287 | // transform.originY = 'center'; 288 | // } 289 | // transform.original.originX = origin.x; 290 | // transform.original.originY = origin.y; 291 | this._currentTransform = transform; 292 | this._beforeTransform(e); 293 | } 294 | 295 | _translateObject(x, y) { 296 | let transform = this._currentTransform, 297 | target = transform.target, 298 | newLeft = x - transform.offsetX, 299 | newTop = y - transform.offsetY, 300 | moveX = !target.get('lockMovementX') && target.left !== newLeft, 301 | moveY = !target.get('lockMovementY') && target.top !== newTop 302 | 303 | moveX && target.set('left', newLeft) 304 | moveY && target.set('top', newTop) 305 | return moveX || moveY 306 | } 307 | 308 | /** 309 | * 绘制对象的控件(边框/控件) 310 | * @param {CanvasContext} ctx 311 | */ 312 | drawControls(ctx) { 313 | let activeObject = this._activeObject 314 | 315 | if (activeObject) { 316 | // console.log('drawControls, activeObject:', activeObject) 317 | activeObject._renderControls(ctx) 318 | } 319 | } 320 | 321 | getZoom() { 322 | return this.viewportTransform[0]; 323 | } 324 | 325 | /** 326 | * 将renderAll请求追加到下一个动画帧 327 | * 除非已经在进行中,否则就什么也不做 328 | * @return {sugar.Canvas} instance 329 | */ 330 | requestRenderAll() { 331 | if (!this.isRendering) { 332 | this.isRendering = this.canvas.requestAnimationFrame(() => { 333 | this.isRendering = 0 334 | this.renderAll() 335 | }) 336 | } 337 | return this 338 | } 339 | 340 | /** 341 | * 使用当前视口计算画布4个角的位置 342 | * @return {Object} points 343 | */ 344 | calcViewportBoundaries() { 345 | let points = {}, width = this.width, height = this.height, 346 | iVpt = invertTransform(this.viewportTransform) 347 | points.tl = transformPoint({x: 0, y: 0}, iVpt) 348 | points.br = transformPoint({x: width, y: height}, iVpt) 349 | points.tr = new PointClass(points.br.x, points.tl.y) 350 | points.bl = new PointClass(points.tl.x, points.br.y) 351 | this.vptCoords = points 352 | return points 353 | } 354 | 355 | cancelRequestedRender() { 356 | if (this.isRendering) { 357 | this.canvas.cancelAnimationFrame(this.isRendering) 358 | this.isRendering = 0 359 | } 360 | } 361 | 362 | /** 363 | * 渲染画布 364 | * @return {sugar.Canvas} instance 365 | */ 366 | renderAll() { 367 | this.renderCanvas(this.ctx, this._chooseObjectsToRender()) 368 | return this 369 | } 370 | 371 | _chooseObjectsToRender() { 372 | let activeObjects = this.getActiveObjects(), 373 | object, objsToRender, activeGroupObjects 374 | 375 | if (activeObjects.length > 0 && !this.preserveObjectStacking) { 376 | objsToRender = [] 377 | activeGroupObjects = [] 378 | for (let i = 0; i < this._objects.length; i++) { 379 | object = this._objects[i] 380 | if (activeObjects.indexOf(object) === -1) { 381 | objsToRender.push(object) 382 | } else { 383 | activeGroupObjects.push(object) 384 | } 385 | } 386 | if (activeObjects.length > 1) { 387 | this._activeObject._objects = activeGroupObjects 388 | } 389 | objsToRender.push.apply(objsToRender, activeGroupObjects) 390 | } else { 391 | objsToRender = this._objects 392 | } 393 | return objsToRender 394 | } 395 | 396 | /** 397 | * 渲染背景,对象,叠加层和控件 398 | * @param {CanvasContext} ctx 399 | * @param {Array} objects 待渲染的图层对象 400 | * @return {sugar.Canvas} instance 401 | */ 402 | renderCanvas(ctx, objects) { 403 | let v = this.viewportTransform 404 | this.cancelRequestedRender() 405 | this.calcViewportBoundaries() 406 | this.clearContext(ctx) 407 | // setImageSmoothing(ctx, this.imageSmoothingEnabled); 408 | this.fire('before:render', {ctx: ctx,}); 409 | // console.log('before:render'); 410 | this._renderBackground(ctx); 411 | 412 | ctx.save(); 413 | // 对所有渲染过程应用一次视口变换 414 | ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]) 415 | this._renderObjects(ctx, objects) 416 | ctx.restore() 417 | if (!this.controlsAboveOverlay) { 418 | this.drawControls(ctx) 419 | } 420 | // this._renderOverlay(ctx) 421 | if (this.controlsAboveOverlay) { 422 | this.drawControls(ctx) 423 | } 424 | // console.log('renderCanvas after:render'); 425 | this.fire('after:render', {ctx: ctx,}) 426 | } 427 | 428 | /** 429 | * @private 430 | * @param {CanvasContext} ctx 431 | * @param {Array} objects 432 | */ 433 | _renderObjects(ctx, objects) { 434 | for (let i = 0; i < objects.length; i++) { 435 | objects[i] && objects[i].render(ctx) 436 | } 437 | } 438 | 439 | /** 440 | * @private 441 | * @param {CanvasContext} ctx 442 | * @param {string} property 'background' 或 'overlay' 443 | */ 444 | _renderBackgroundOrOverlay(ctx, property) { 445 | let fill = this[property + 'Color'] 446 | let object = this[property + 'Image'] 447 | let v = this.viewportTransform 448 | let needsVpt = this[property + 'Vpt'] 449 | if (!fill && !object) { 450 | return 451 | } 452 | if (fill) { 453 | ctx.save() 454 | ctx.beginPath(); 455 | ctx.moveTo(0, 0) 456 | ctx.lineTo(this.width, 0) 457 | ctx.lineTo(this.width, this.height) 458 | ctx.lineTo(0, this.height) 459 | ctx.closePath() 460 | ctx.fillStyle = fill.toLive ? fill.toLive(ctx, this) : fill 461 | if (needsVpt) { 462 | ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]) 463 | } 464 | ctx.transform(1, 0, 0, 1, fill.offsetX || 0, fill.offsetY || 0) 465 | // let m = fill.gradientTransform || fill.patternTransform 466 | // m && ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]) 467 | ctx.fill() 468 | ctx.restore() 469 | } 470 | if (object) { 471 | ctx.save() 472 | if (needsVpt) { 473 | ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]) 474 | } 475 | object.render(ctx) 476 | ctx.restore() 477 | } 478 | } 479 | 480 | /** 481 | * @private 482 | * @param {CanvasContext} ctx 483 | */ 484 | _renderBackground(ctx) { 485 | this._renderBackgroundOrOverlay(ctx, 'background') 486 | } 487 | 488 | /** 489 | * @private 490 | * @param {CanvasContext} ctx 491 | */ 492 | _renderOverlay(ctx) { 493 | this._renderBackgroundOrOverlay(ctx, 'overlay') 494 | } 495 | 496 | /** 497 | * 返回画布中心的坐标 498 | * @return {Object} object 返回值是具有top和left属性的对象 499 | */ 500 | getCenter() { 501 | return { 502 | top: this.height / 2, 503 | left: this.width / 2 504 | } 505 | } 506 | 507 | /** 508 | * 返回当前选中操作的对象 509 | * @return {sugar.Object} 510 | */ 511 | getActiveObject() { 512 | return this._activeObject 513 | } 514 | 515 | /** 516 | * 返回具有当前所选操作的对象数组 517 | * @return {sugar.Object} active object 518 | */ 519 | getActiveObjects() { 520 | let active = this._activeObject 521 | if (active) { 522 | if (active.type === 'activeSelection' && active._objects) { 523 | return active._objects.slice(0) 524 | } else { 525 | return [active] 526 | } 527 | } 528 | return [] 529 | } 530 | 531 | /** 532 | * 将一个对象在画布中设置为选中激活状态 533 | * @param {sugar.Object} object 设为激活的对象 534 | * @param {Event} [e] 事件(触发"object:selected"时传递) 535 | * @return {sugar.Canvas} 536 | */ 537 | setActiveObject(object, e) { 538 | let currentActives = this.getActiveObjects() 539 | this._setActiveObject(object, e) 540 | this._fireSelectionEvents(currentActives, e) 541 | return this; 542 | } 543 | 544 | /** 545 | * @private 546 | * @param {Object} object 设为激活的对象 547 | * @param {Event} [e] 事件(触发"object:selected"时传递) 548 | * @return {Boolean} 如果对象为选中激活状态,返回true 549 | */ 550 | _setActiveObject(object, e) { 551 | if (this._activeObject === object) { 552 | // 当前对象已选中 553 | return false 554 | } 555 | if (!this._discardActiveObject(e, object)) { 556 | return false 557 | } 558 | if (object.onSelect({e: e})) { 559 | return false 560 | } 561 | this._activeObject = object 562 | return true 563 | } 564 | 565 | /** 566 | * @private 567 | */ 568 | _discardActiveObject(e, object) { 569 | let obj = this._activeObject; 570 | if (obj) { 571 | if (obj.onDeselect({e: e, object: object})) { 572 | return false 573 | } 574 | this._activeObject = null 575 | } 576 | return true 577 | } 578 | 579 | _fireSelectionEvents(oldObjects, e) { 580 | let somethingChanged = false, 581 | objects = this.getActiveObjects(), 582 | added = [], 583 | removed = [], 584 | opt = {e: e} 585 | 586 | oldObjects.forEach((oldObject) => { 587 | if (objects.indexOf(oldObject) === -1) { 588 | somethingChanged = true 589 | oldObject.fire('deselected', opt) 590 | removed.push(oldObject) 591 | } 592 | }); 593 | objects.forEach((object) => { 594 | if (oldObjects.indexOf(object) === -1) { 595 | somethingChanged = true 596 | object.fire('selected', opt) 597 | added.push(object) 598 | } 599 | }); 600 | if (oldObjects.length > 0 && objects.length > 0) { 601 | opt.selected = added 602 | opt.deselected = removed 603 | opt.updated = added[0] || removed[0] 604 | opt.target = this._activeObject 605 | somethingChanged && this.fire('selection:updated', opt) 606 | } else if (objects.length > 0) { 607 | opt.selected = added 608 | opt.target = this._activeObject 609 | this.fire('selection:created', opt) 610 | } else if (oldObjects.length > 0) { 611 | opt.deselected = removed 612 | this.fire('selection:cleared', opt) 613 | } 614 | } 615 | 616 | toDataURL(options) { 617 | try { 618 | options || (options = {}) 619 | 620 | let format = options.format || 'jpeg' 621 | let quality = options.quality || 0.5 622 | 623 | /** 624 | * toDataURL 微信基础库 2.11.0 开始支持 625 | * 626 | * string type 627 | * 图片格式,默认为 image/png 628 | * 629 | * number encoderOptions 630 | * 在指定图片格式为 image/jpeg 或 image/webp的情况下,可以从 0 到 1 的区间内选择图片的质量。如果超出取值范围,将会使用默认值 0.92。其他参数会被忽略。 631 | */ 632 | return this.canvas.toDataURL(`image/${format}`, quality) 633 | } catch (e) { 634 | throw new Error('当前微信基础库不支持toDataURL,2.11.0开始支持') 635 | } 636 | } 637 | } 638 | 639 | module.exports = CanvasClass 640 | -------------------------------------------------------------------------------- /src/shapes/text.class.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Sugar on 2020/5/26. 3 | */ 4 | const ObjectClass = require('./object.class') 5 | const {graphemeSplit} = require('../utils/string') 6 | 7 | class TextClass extends ObjectClass { 8 | constructor(text, options) { 9 | super(options) 10 | 11 | this.type = 'text' 12 | this.stroke = null 13 | this.fontSize = 16 14 | this.fontWeight = 'normal' 15 | this.fontFamily = 'sans-serif' 16 | this.underline = false 17 | this.overline = false 18 | this.linethrough = false 19 | this.textAlign = 'left' 20 | this.fontStyle = 'normal' 21 | this.lineHeight = 1.16 22 | this.charSpacing = 0 23 | this.styles = null 24 | this._fontSizeMult = 1.13 25 | this._fontSizeFraction = 0.222 26 | 27 | this._reNewline = /\r?\n/ 28 | this._reSpaceAndTab = /[ \t\r]/ 29 | this._reSpacesAndTabs = /[ \t\r]/g 30 | 31 | this.__charBounds = [] 32 | this.MIN_TEXT_WIDTH = 2 33 | this.CACHE_FONT_SIZE = 400 34 | 35 | this._styleProperties = [ 36 | 'stroke', 37 | 'strokeWidth', 38 | 'fill', 39 | 'fontFamily', 40 | 'fontSize', 41 | 'fontWeight', 42 | 'fontStyle', 43 | 'underline', 44 | 'overline', 45 | 'linethrough', 46 | // 'deltaY', 47 | // 'textBackgroundColor', 48 | ] 49 | this._dimensionAffectingProps = [ 50 | 'fontSize', 51 | 'fontWeight', 52 | 'fontFamily', 53 | 'fontStyle', 54 | 'lineHeight', 55 | 'text', 56 | 'charSpacing', 57 | 'textAlign', 58 | 'styles', 59 | ] 60 | 61 | this.initialize(text, options) 62 | } 63 | 64 | initialize(text, options) { 65 | this.styles = options ? (options.styles || {}) : {} 66 | this.text = text 67 | super.initialize(options) 68 | this.initDimensions(); 69 | this.setCoords(); 70 | // this.setupState({propertySet: '_dimensionAffectingProps'}); 71 | } 72 | 73 | initDimensions() { 74 | this._splitText(); 75 | this._clearCache(); 76 | 77 | if (this.canvas && this.canvas.ctx) { 78 | // this.width = this.canvas.ctx.measureText(this.text).width || this.MIN_TEXT_WIDTH 79 | this.width = this.calcTextWidth() || this.MIN_TEXT_WIDTH 80 | } 81 | this.height = this.calcTextHeight() 82 | if (this.textAlign.indexOf('justify') !== -1) { 83 | this.enlargeSpaces(); 84 | } 85 | this.height = this.calcTextHeight(); 86 | // this.saveState({propertySet: '_dimensionAffectingProps'}) 87 | } 88 | 89 | enlargeSpaces() { 90 | let diffSpace, currentLineWidth, numberOfSpaces, accumulatedSpace, line, charBound, spaces; 91 | for (let i = 0, len = this._textLines.length; i < len; i++) { 92 | if (this.textAlign !== 'justify' && (i === len - 1 || this.isEndOfWrapping(i))) { 93 | continue; 94 | } 95 | accumulatedSpace = 0; 96 | line = this._textLines[i]; 97 | currentLineWidth = this.getLineWidth(i); 98 | if (currentLineWidth < this.width && (spaces = this.textLines[i].match(this._reSpacesAndTabs))) { 99 | numberOfSpaces = spaces.length; 100 | diffSpace = (this.width - currentLineWidth) / numberOfSpaces; 101 | for (let j = 0, jlen = line.length; j <= jlen; j++) { 102 | charBound = this.__charBounds[i][j]; 103 | if (this._reSpaceAndTab.test(line[j])) { 104 | charBound.width += diffSpace; 105 | charBound.kernedWidth += diffSpace; 106 | charBound.left += accumulatedSpace; 107 | accumulatedSpace += diffSpace; 108 | } else { 109 | charBound.left += accumulatedSpace; 110 | } 111 | } 112 | } 113 | } 114 | } 115 | 116 | getFontCache(decl) { 117 | let fontFamily = decl.fontFamily.toLowerCase(); 118 | if (!TextClass.charWidthsCache[fontFamily]) { 119 | TextClass.charWidthsCache[fontFamily] = {}; 120 | } 121 | let cache = TextClass.charWidthsCache[fontFamily], 122 | cacheProp = decl.fontStyle.toLowerCase() + '_' + (decl.fontWeight + '').toLowerCase(); 123 | if (!cache[cacheProp]) { 124 | cache[cacheProp] = {}; 125 | } 126 | return cache[cacheProp]; 127 | } 128 | 129 | calcTextHeight() { 130 | let lineHeight, height = 0; 131 | for (let i = 0, len = this._textLines.length; i < len; i++) { 132 | lineHeight = this.getHeightOfLine(i); 133 | height += (i === len - 1 ? lineHeight / this.lineHeight : lineHeight); 134 | } 135 | return height; 136 | } 137 | 138 | getHeightOfLine(lineIndex) { 139 | if (this.__lineHeights[lineIndex]) { 140 | return this.__lineHeights[lineIndex]; 141 | } 142 | 143 | let line = this._textLines[lineIndex], 144 | maxHeight = this.getHeightOfChar(lineIndex, 0); 145 | for (let i = 1; i < line.length; i++) { 146 | maxHeight = Math.max(this.getHeightOfChar(lineIndex, i), maxHeight); 147 | } 148 | 149 | return this.__lineHeights[lineIndex] = maxHeight * this.lineHeight * this._fontSizeMult; 150 | } 151 | 152 | getHeightOfChar(line, _char) { 153 | return this.getValueOfPropertyAt(line, _char, 'fontSize'); 154 | } 155 | 156 | getValueOfPropertyAt(lineIndex, charIndex, property) { 157 | var charStyle = this._getStyleDeclaration(lineIndex, charIndex); 158 | if (charStyle && typeof charStyle[property] !== 'undefined') { 159 | return charStyle[property]; 160 | } 161 | return this[property]; 162 | } 163 | 164 | _clearCache() { 165 | this.__lineWidths = [] 166 | this.__lineHeights = [] 167 | this.__charBounds = [] 168 | } 169 | 170 | isEndOfWrapping(lineIndex) { 171 | return lineIndex === this._textLines.length - 1; 172 | } 173 | 174 | missingNewlineOffset() { 175 | return 1; 176 | } 177 | 178 | getMeasuringContext() { 179 | let _measuringContext = this.canvas && this.canvas.ctx 180 | return _measuringContext; 181 | } 182 | 183 | _splitText() { 184 | let newLines = this._splitTextIntoLines(this.text) 185 | this.textLines = newLines.lines 186 | this._textLines = newLines.graphemeLines 187 | this._unwrappedTextLines = newLines._unwrappedLines 188 | this._text = newLines.graphemeText 189 | return newLines 190 | } 191 | 192 | /** 193 | * 返回分行后的文本数组. 194 | * @param {String} text 195 | * @returns {Array} 196 | */ 197 | _splitTextIntoLines(text) { 198 | let lines = text.split(this._reNewline), 199 | newLines = new Array(lines.length), 200 | newLine = ['\n'], 201 | newText = [] 202 | for (let i = 0; i < lines.length; i++) { 203 | newLines[i] = graphemeSplit(lines[i]) 204 | newText = newText.concat(newLines[i], newLine) 205 | } 206 | newText.pop() 207 | return {_unwrappedLines: newLines, lines: lines, graphemeText: newText, graphemeLines: newLines} 208 | } 209 | 210 | calcTextWidth() { 211 | let maxWidth = this.getLineWidth(0); 212 | 213 | for (let i = 1, len = this._textLines.length; i < len; i++) { 214 | let currentLineWidth = this.getLineWidth(i); 215 | if (currentLineWidth > maxWidth) { 216 | maxWidth = currentLineWidth; 217 | } 218 | } 219 | console.log('文本宽度', maxWidth) 220 | return maxWidth; 221 | } 222 | 223 | getLineWidth(lineIndex) { 224 | if (this.__lineWidths[lineIndex]) { 225 | return this.__lineWidths[lineIndex]; 226 | } 227 | 228 | let width, line = this._textLines[lineIndex], lineInfo; 229 | 230 | if (line === '') { 231 | width = 0; 232 | } else { 233 | lineInfo = this.measureLine(lineIndex); 234 | width = lineInfo.width; 235 | } 236 | this.__lineWidths[lineIndex] = width; 237 | return width; 238 | } 239 | 240 | measureLine(lineIndex) { 241 | let lineInfo = this._measureLine(lineIndex); 242 | if (this.charSpacing !== 0) { 243 | lineInfo.width -= this._getWidthOfCharSpacing(); 244 | } 245 | if (lineInfo.width < 0) { 246 | lineInfo.width = 0; 247 | } 248 | return lineInfo; 249 | } 250 | 251 | _measureLine(lineIndex) { 252 | let width = 0, i, grapheme, line = this._textLines[lineIndex], prevGrapheme, 253 | graphemeInfo, numOfSpaces = 0, lineBounds = new Array(line.length); 254 | 255 | this.__charBounds[lineIndex] = lineBounds; 256 | for (i = 0; i < line.length; i++) { 257 | grapheme = line[i]; 258 | graphemeInfo = this._getGraphemeBox(grapheme, lineIndex, i, prevGrapheme); 259 | lineBounds[i] = graphemeInfo; 260 | width += graphemeInfo.kernedWidth; 261 | prevGrapheme = grapheme; 262 | } 263 | // this latest bound box represent the last character of the line 264 | // to simplify cursor handling in interactive mode. 265 | lineBounds[i] = { 266 | left: graphemeInfo ? graphemeInfo.left + graphemeInfo.width : 0, 267 | width: 0, 268 | kernedWidth: 0, 269 | height: this.fontSize 270 | }; 271 | return {width: width, numOfSpaces: numOfSpaces}; 272 | } 273 | 274 | _renderTextCommon(ctx, method) { 275 | ctx.save(); 276 | let lineHeights = 0, left = this._getLeftOffset(ctx), top = this._getTopOffset(ctx), 277 | offsets = this._applyPatternGradientTransform(ctx, method === 'fillText' ? this.fill : this.stroke); 278 | for (let i = 0, len = this._textLines.length; i < len; i++) { 279 | let heightOfLine = this.getHeightOfLine(i), 280 | maxHeight = heightOfLine / this.lineHeight, 281 | leftOffset = this._getLineLeftOffset(i); 282 | this._renderTextLine( 283 | method, 284 | ctx, 285 | this._textLines[i], 286 | left + leftOffset - offsets.offsetX, // TODO 优化position 287 | top + lineHeights + maxHeight - offsets.offsetY, 288 | i 289 | ); 290 | lineHeights += heightOfLine; 291 | } 292 | ctx.restore(); 293 | } 294 | 295 | _getLeftOffset(ctx) { 296 | // this.width = ctx.measureText(this.text).width || this.MIN_TEXT_WIDTH 297 | return -this.width / 2; 298 | } 299 | 300 | _getTopOffset(ctx) { 301 | return -this.height / 2; 302 | } 303 | 304 | _getLineLeftOffset(lineIndex) { 305 | let lineWidth = this.getLineWidth(lineIndex); 306 | if (this.textAlign === 'center') { 307 | return (this.width - lineWidth) / 2; 308 | } 309 | if (this.textAlign === 'right') { 310 | return this.width - lineWidth; 311 | } 312 | if (this.textAlign === 'justify-center' && this.isEndOfWrapping(lineIndex)) { 313 | return (this.width - lineWidth) / 2; 314 | } 315 | if (this.textAlign === 'justify-right' && this.isEndOfWrapping(lineIndex)) { 316 | return this.width - lineWidth; 317 | } 318 | return 0; 319 | } 320 | 321 | _getGraphemeBox(grapheme, lineIndex, charIndex, prevGrapheme, skipLeft) { 322 | let style = this.getCompleteStyleDeclaration(lineIndex, charIndex), 323 | prevStyle = prevGrapheme ? this.getCompleteStyleDeclaration(lineIndex, charIndex - 1) : {}, 324 | info = this._measureChar(grapheme, style, prevGrapheme, prevStyle), 325 | kernedWidth = info.kernedWidth, 326 | width = info.width, charSpacing; 327 | 328 | if (this.charSpacing !== 0) { 329 | charSpacing = this._getWidthOfCharSpacing(); 330 | width += charSpacing; 331 | kernedWidth += charSpacing; 332 | } 333 | 334 | let box = { 335 | width: width, 336 | left: 0, 337 | height: style.fontSize, 338 | kernedWidth: kernedWidth, 339 | deltaY: style.deltaY, 340 | }; 341 | if (charIndex > 0 && !skipLeft) { 342 | let previousBox = this.__charBounds[lineIndex][charIndex - 1]; 343 | box.left = previousBox.left + previousBox.width + info.kernedWidth - info.width; 344 | } 345 | return box; 346 | } 347 | 348 | _measureChar(_char, charStyle, previousChar, prevCharStyle) { 349 | let fontCache = this.getFontCache(charStyle), 350 | fontDeclaration = this._getFontDeclaration(charStyle), 351 | previousFontDeclaration = this._getFontDeclaration(prevCharStyle), couple = previousChar + _char, 352 | stylesAreEqual = fontDeclaration === previousFontDeclaration, width, coupleWidth, previousWidth, 353 | fontMultiplier = charStyle.fontSize / this.CACHE_FONT_SIZE, kernedWidth; 354 | 355 | if (previousChar && fontCache[previousChar] !== undefined) { 356 | previousWidth = fontCache[previousChar]; 357 | } 358 | if (fontCache[_char] !== undefined) { 359 | kernedWidth = width = fontCache[_char]; 360 | } 361 | if (stylesAreEqual && fontCache[couple] !== undefined) { 362 | coupleWidth = fontCache[couple]; 363 | kernedWidth = coupleWidth - previousWidth; 364 | } 365 | let ctx 366 | if (width === undefined || previousWidth === undefined || coupleWidth === undefined) { 367 | ctx = this.getMeasuringContext(); 368 | this._setTextStyles(ctx, charStyle, true); 369 | } 370 | if (width === undefined) { 371 | kernedWidth = width = ctx.measureText(_char).width; 372 | fontCache[_char] = width; 373 | } 374 | if (previousWidth === undefined && stylesAreEqual && previousChar) { 375 | previousWidth = ctx.measureText(previousChar).width; 376 | fontCache[previousChar] = previousWidth; 377 | } 378 | if (stylesAreEqual && coupleWidth === undefined) { 379 | coupleWidth = ctx.measureText(couple).width; 380 | fontCache[couple] = coupleWidth; 381 | kernedWidth = coupleWidth - previousWidth; 382 | } 383 | return {width: width * fontMultiplier, kernedWidth: kernedWidth * fontMultiplier}; 384 | } 385 | 386 | _getFontDeclaration(styleObject, forMeasuring) { 387 | // let style = styleObject || this, family = this.fontFamily, 388 | // fontIsGeneric = TextClass.genericFonts.indexOf(family.toLowerCase()) > -1; 389 | // let fontFamily = family === undefined || 390 | // family.indexOf('\'') > -1 || family.indexOf(',') > -1 || 391 | // family.indexOf('"') > -1 || fontIsGeneric 392 | // ? style.fontFamily : '"' + style.fontFamily + '"'; 393 | // return [ 394 | // style.fontStyle, 395 | // style.fontWeight, 396 | // forMeasuring ? this.CACHE_FONT_SIZE + 'px' : style.fontSize + 'px', 397 | // fontFamily 398 | // ].join(' '); 399 | 400 | return `${this.fontStyle} ${this.fontWeight} ${this.fontSize}px ${this.fontFamily}` 401 | } 402 | 403 | _getWidthOfCharSpacing() { 404 | if (this.charSpacing !== 0) { 405 | return this.fontSize * this.charSpacing / 1000; 406 | } 407 | return 0; 408 | } 409 | 410 | _setTextStyles(ctx, charStyle, forMeasuring) { 411 | ctx.font = this._getFontDeclaration(charStyle, forMeasuring); 412 | this.width = ctx.measureText(this.text).width || this.MIN_TEXT_WIDTH 413 | } 414 | 415 | _renderTextLinesBackground(ctx) { 416 | if (!this.textBackgroundColor && !this.styleHas('textBackgroundColor')) { 417 | return; 418 | } 419 | var lineTopOffset = 0, heightOfLine, 420 | lineLeftOffset, originalFill = ctx.fillStyle, 421 | line, lastColor, 422 | leftOffset = this._getLeftOffset(ctx), 423 | topOffset = this._getTopOffset(ctx), 424 | boxStart = 0, boxWidth = 0, charBox, currentColor; 425 | 426 | for (var i = 0, len = this._textLines.length; i < len; i++) { 427 | heightOfLine = this.getHeightOfLine(i); 428 | if (!this.textBackgroundColor && !this.styleHas('textBackgroundColor', i)) { 429 | lineTopOffset += heightOfLine; 430 | continue; 431 | } 432 | line = this._textLines[i]; 433 | lineLeftOffset = this._getLineLeftOffset(i); 434 | boxWidth = 0; 435 | boxStart = 0; 436 | lastColor = this.getValueOfPropertyAt(i, 0, 'textBackgroundColor'); 437 | for (var j = 0, jlen = line.length; j < jlen; j++) { 438 | charBox = this.__charBounds[i][j]; 439 | currentColor = this.getValueOfPropertyAt(i, j, 'textBackgroundColor'); 440 | if (currentColor !== lastColor) { 441 | ctx.fillStyle = lastColor; 442 | lastColor && ctx.fillRect( 443 | leftOffset + lineLeftOffset + boxStart, 444 | topOffset + lineTopOffset, 445 | boxWidth, 446 | heightOfLine / this.lineHeight 447 | ); 448 | boxStart = charBox.left; 449 | boxWidth = charBox.width; 450 | lastColor = currentColor; 451 | } else { 452 | boxWidth += charBox.kernedWidth; 453 | } 454 | } 455 | if (currentColor) { 456 | ctx.fillStyle = currentColor; 457 | ctx.fillRect( 458 | leftOffset + lineLeftOffset + boxStart, 459 | topOffset + lineTopOffset, 460 | boxWidth, 461 | heightOfLine / this.lineHeight 462 | ); 463 | } 464 | lineTopOffset += heightOfLine; 465 | } 466 | ctx.fillStyle = originalFill; 467 | 468 | this._removeShadow(ctx); 469 | } 470 | 471 | _renderTextDecoration(ctx, type) { 472 | if (!this[type] && !this.styleHas(type)) { 473 | return; 474 | } 475 | var heightOfLine, size, _size, 476 | lineLeftOffset, dy, _dy, 477 | line, lastDecoration, 478 | leftOffset = this._getLeftOffset(ctx), 479 | topOffset = this._getTopOffset(ctx), top, 480 | boxStart, boxWidth, charBox, currentDecoration, 481 | maxHeight, currentFill, lastFill, 482 | charSpacing = this._getWidthOfCharSpacing(); 483 | 484 | for (var i = 0, len = this._textLines.length; i < len; i++) { 485 | heightOfLine = this.getHeightOfLine(i); 486 | if (!this[type] && !this.styleHas(type, i)) { 487 | topOffset += heightOfLine; 488 | continue; 489 | } 490 | line = this._textLines[i]; 491 | maxHeight = heightOfLine / this.lineHeight; 492 | lineLeftOffset = this._getLineLeftOffset(i); 493 | boxStart = 0; 494 | boxWidth = 0; 495 | lastDecoration = this.getValueOfPropertyAt(i, 0, type); 496 | lastFill = this.getValueOfPropertyAt(i, 0, 'fill'); 497 | top = topOffset + maxHeight * (1 - this._fontSizeFraction); 498 | size = this.getHeightOfChar(i, 0); 499 | dy = this.getValueOfPropertyAt(i, 0, 'deltaY'); 500 | for (var j = 0, jlen = line.length; j < jlen; j++) { 501 | charBox = this.__charBounds[i][j]; 502 | currentDecoration = this.getValueOfPropertyAt(i, j, type); 503 | currentFill = this.getValueOfPropertyAt(i, j, 'fill'); 504 | _size = this.getHeightOfChar(i, j); 505 | _dy = this.getValueOfPropertyAt(i, j, 'deltaY'); 506 | if ((currentDecoration !== lastDecoration || currentFill !== lastFill || _size !== size || _dy !== dy) && 507 | boxWidth > 0) { 508 | ctx.fillStyle = lastFill; 509 | lastDecoration && lastFill && ctx.fillRect( 510 | leftOffset + lineLeftOffset + boxStart, 511 | top + this.offsets[type] * size + dy, 512 | boxWidth, 513 | this.fontSize / 15 514 | ); 515 | boxStart = charBox.left; 516 | boxWidth = charBox.width; 517 | lastDecoration = currentDecoration; 518 | lastFill = currentFill; 519 | size = _size; 520 | dy = _dy; 521 | } else { 522 | boxWidth += charBox.kernedWidth; 523 | } 524 | } 525 | ctx.fillStyle = currentFill; 526 | currentDecoration && currentFill && ctx.fillRect( 527 | leftOffset + lineLeftOffset + boxStart, 528 | top + this.offsets[type] * size + dy, 529 | boxWidth - charSpacing, 530 | this.fontSize / 15 531 | ); 532 | topOffset += heightOfLine; 533 | } 534 | this._removeShadow(ctx); 535 | } 536 | 537 | _renderTextStroke(ctx) { 538 | if ((!this.stroke || this.strokeWidth === 0) && this.isEmptyStyles()) { 539 | return; 540 | } 541 | 542 | if (this.shadow && !this.shadow.affectStroke) { 543 | this._removeShadow(ctx); 544 | } 545 | 546 | ctx.save(); 547 | this._setLineDash(ctx, this.strokeDashArray); 548 | ctx.beginPath(); 549 | this._renderTextCommon(ctx, 'strokeText'); 550 | ctx.closePath(); 551 | ctx.restore(); 552 | } 553 | 554 | _renderText(ctx) { 555 | if (this.paintFirst === 'stroke') { 556 | this._renderTextStroke(ctx); 557 | this._renderTextFill(ctx); 558 | } else { 559 | this._renderTextFill(ctx); 560 | this._renderTextStroke(ctx); 561 | } 562 | } 563 | 564 | _renderTextFill(ctx) { 565 | if (!this.fill && !this.styleHas('fill')) { 566 | return; 567 | } 568 | 569 | this._renderTextCommon(ctx, 'fillText'); 570 | } 571 | 572 | _renderTextLine(method, ctx, line, left, top, lineIndex) { 573 | this._renderChars(method, ctx, line, left, top, lineIndex); 574 | } 575 | 576 | _renderChars(method, ctx, line, left, top, lineIndex) { 577 | // set proper line offset 578 | var lineHeight = this.getHeightOfLine(lineIndex), 579 | isJustify = this.textAlign.indexOf('justify') !== -1, 580 | actualStyle, 581 | nextStyle, 582 | charsToRender = '', 583 | charBox, 584 | boxWidth = 0, 585 | timeToRender, 586 | shortCut = !isJustify && this.charSpacing === 0 && this.isEmptyStyles(lineIndex); 587 | 588 | ctx.save(); 589 | top -= lineHeight * this._fontSizeFraction / this.lineHeight; 590 | if (shortCut) { 591 | // render all the line in one pass without checking 592 | this._renderChar(method, ctx, lineIndex, 0, this.textLines[lineIndex], left, top, lineHeight); 593 | ctx.restore(); 594 | return; 595 | } 596 | for (var i = 0, len = line.length - 1; i <= len; i++) { 597 | timeToRender = i === len || this.charSpacing; 598 | charsToRender += line[i]; 599 | charBox = this.__charBounds[lineIndex][i]; 600 | if (boxWidth === 0) { 601 | left += charBox.kernedWidth - charBox.width; 602 | boxWidth += charBox.width; 603 | } else { 604 | boxWidth += charBox.kernedWidth; 605 | } 606 | if (isJustify && !timeToRender) { 607 | if (this._reSpaceAndTab.test(line[i])) { 608 | timeToRender = true; 609 | } 610 | } 611 | if (!timeToRender) { 612 | // if we have charSpacing, we render char by char 613 | actualStyle = actualStyle || this.getCompleteStyleDeclaration(lineIndex, i); 614 | nextStyle = this.getCompleteStyleDeclaration(lineIndex, i + 1); 615 | timeToRender = this._hasStyleChanged(actualStyle, nextStyle); 616 | } 617 | if (timeToRender) { 618 | this._renderChar(method, ctx, lineIndex, i, charsToRender, left, top, lineHeight); 619 | charsToRender = ''; 620 | actualStyle = nextStyle; 621 | left += boxWidth; 622 | boxWidth = 0; 623 | } 624 | } 625 | ctx.restore(); 626 | } 627 | 628 | _renderChar(method, ctx, lineIndex, charIndex, _char, left, top) { 629 | var decl = this._getStyleDeclaration(lineIndex, charIndex), 630 | fullDecl = this.getCompleteStyleDeclaration(lineIndex, charIndex), 631 | shouldFill = method === 'fillText' && fullDecl.fill, 632 | shouldStroke = method === 'strokeText' && fullDecl.stroke && fullDecl.strokeWidth; 633 | 634 | if (!shouldStroke && !shouldFill) { 635 | return; 636 | } 637 | decl && ctx.save(); 638 | 639 | this._applyCharStyles(method, ctx, lineIndex, charIndex, fullDecl); 640 | 641 | if (decl && decl.textBackgroundColor) { 642 | this._removeShadow(ctx); 643 | } 644 | if (decl && decl.deltaY) { 645 | top += decl.deltaY; 646 | } 647 | // console.log('ctx.fillText', _char, left, top) 648 | shouldFill && ctx.fillText(_char, left, top); 649 | shouldStroke && ctx.strokeText(_char, left, top); 650 | decl && ctx.restore(); 651 | } 652 | 653 | _applyCharStyles(method, ctx, lineIndex, charIndex, styleDeclaration) { 654 | this._setFillStyles(ctx, styleDeclaration); 655 | this._setStrokeStyles(ctx, styleDeclaration); 656 | ctx.font = this._getFontDeclaration(styleDeclaration); 657 | } 658 | 659 | _hasStyleChanged(prevStyle, thisStyle) { 660 | return prevStyle.fill !== thisStyle.fill || 661 | prevStyle.stroke !== thisStyle.stroke || 662 | prevStyle.strokeWidth !== thisStyle.strokeWidth || 663 | prevStyle.fontSize !== thisStyle.fontSize || 664 | prevStyle.fontFamily !== thisStyle.fontFamily || 665 | prevStyle.fontWeight !== thisStyle.fontWeight || 666 | prevStyle.fontStyle !== thisStyle.fontStyle || 667 | prevStyle.deltaY !== thisStyle.deltaY; 668 | } 669 | 670 | set(key, value) { 671 | super.set(key, value) 672 | let needsDims = false; 673 | if (typeof key === 'object') { 674 | for (let _key in key) { 675 | needsDims = needsDims || this._dimensionAffectingProps.indexOf(_key) !== -1; 676 | } 677 | } else { 678 | needsDims = this._dimensionAffectingProps.indexOf(key) !== -1; 679 | } 680 | if (needsDims) { 681 | this.initDimensions(); 682 | this.setCoords(); 683 | } 684 | return this; 685 | } 686 | 687 | _render(ctx) { 688 | // console.log('_render绘制文字', ctx, this); 689 | // ctx.save() 690 | // // italic bold 20px cursive 691 | // ctx.font = `${this.fontStyle} ${this.fontWeight} ${this.fontSize}px ${this.fontFamily}` 692 | // this.width = ctx.measureText(this.text).width || this.MIN_TEXT_WIDTH 693 | // ctx.fillStyle = this.fill 694 | // // ctx.fillText(this.text, this.left, this.top + this.fontSize) 695 | // ctx.fillText(this.text, this.left, this.top) 696 | // // ctx.strokeText(this.text, this.left, this.top) 697 | // ctx.restore(); 698 | this._setTextStyles(ctx); 699 | this._renderTextLinesBackground(ctx); 700 | this._renderTextDecoration(ctx, 'underline'); 701 | this._renderText(ctx); 702 | this._renderTextDecoration(ctx, 'overline'); 703 | this._renderTextDecoration(ctx, 'linethrough'); 704 | } 705 | 706 | render(ctx) { 707 | if (!this.visible) { 708 | return; 709 | } 710 | // if (this.canvas && this.canvas.skipOffscreen && !this.group && !this.isOnScreen()) { 711 | // return; 712 | // } 713 | // if (this._shouldClearDimensionCache()) { 714 | // this.initDimensions(); 715 | // } 716 | super.render(ctx) 717 | } 718 | } 719 | 720 | TextClass.charWidthsCache = {} 721 | TextClass.genericFonts = ['sans-serif', 'serif', 'cursive', 'fantasy', 'monospace'] 722 | 723 | module.exports = TextClass 724 | --------------------------------------------------------------------------------