jmgraph 3.2.20 → 3.2.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -16,16 +16,16 @@ import {jmEllipse} from "./src/shapes/jmEllipse.js";
16
16
  import {jmPolygon} from "./src/shapes/jmPolygon.js";
17
17
  import {jmStar} from "./src/shapes/jmStar.js";
18
18
 
19
- import { jmGraph as jmGraphCore,
19
+ import { jmGraph as jmGraphCore,
20
20
  jmUtils,
21
21
  jmList,
22
22
  jmProperty,
23
23
  jmShadow,
24
24
  jmGradient,
25
+ jmFilter,
25
26
  jmEvents,
26
27
  jmControl,
27
- jmPath,
28
- jmLayer } from "./src/core/jmGraph.js";
28
+ jmPath } from "./src/core/jmGraph.js";
29
29
 
30
30
  const shapes = {
31
31
  "arc": jmArc,
@@ -46,22 +46,11 @@ const shapes = {
46
46
  "star": jmStar
47
47
  }
48
48
 
49
- export default class jmGraph extends jmGraphCore {
49
+ class jmGraphImpl extends jmGraphCore {
50
50
  constructor(canvas, option, callback) {
51
-
52
- const targetType = new.target;
53
-
54
51
  // 合并shapes
55
52
  option = Object.assign({}, option);
56
53
  option.shapes = Object.assign(shapes, option.shapes||{});
57
-
58
- //不是用new实例化的话,返回一个promise
59
- if(!targetType || !(targetType.prototype instanceof jmGraphCore)) {
60
- return new Promise(function(resolve, reject){
61
- var g = new jmGraph(canvas, option, callback);
62
- if(resolve) resolve(g);
63
- });
64
- }
65
54
 
66
55
  if(typeof option == 'function') {
67
56
  callback = option;
@@ -70,24 +59,23 @@ export default class jmGraph extends jmGraphCore {
70
59
 
71
60
  super(canvas, option, callback);
72
61
  }
73
-
74
- static create(...args) {
75
- return createJmGraph(...args);
76
- }
77
62
  }
78
63
 
79
- //创建实例
64
+ //创建实例,支持不加 new 直接调用
80
65
  const createJmGraph = (...args) => {
81
- return new jmGraph(...args);
66
+ return new jmGraphImpl(...args);
82
67
  }
83
68
 
84
- export {
85
- jmUtils,
86
- jmList,
69
+ export default jmGraphImpl;
70
+
71
+ export {
72
+ jmUtils,
73
+ jmList,
87
74
  jmControl,
88
75
  jmPath,
89
76
  jmShadow,
90
77
  jmGradient,
78
+ jmFilter,
91
79
  jmArc,
92
80
  jmArrow,
93
81
  jmBezier,
@@ -103,8 +91,7 @@ export {
103
91
  jmEllipse,
104
92
  jmPolygon,
105
93
  jmStar,
106
- jmLayer,
107
- jmGraph,
94
+ jmGraphImpl as jmGraph,
108
95
  createJmGraph as create
109
96
  };
110
97
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "family": "jmgraph",
3
3
  "name": "jmgraph",
4
- "version": "3.2.20",
4
+ "version": "3.2.21",
5
5
  "description": "一个简单的canvas画图库",
6
6
  "homepage": "https://surl.fit/tools/tools/jmgraph",
7
7
  "keywords": [
@@ -3,6 +3,7 @@ import {jmUtils} from "./jmUtils.js";
3
3
  import {jmList} from "./jmList.js";
4
4
  import {jmGradient} from "./jmGradient.js";
5
5
  import {jmShadow} from "./jmShadow.js";
6
+ import {jmFilter} from "./jmFilter.js";
6
7
  import {jmProperty} from "./jmProperty.js";
7
8
  import WebglPath from "../lib/webgl/path.js";
8
9
 
@@ -27,7 +28,9 @@ const jmStyleMap = {
27
28
  'shadowOffsetY' : 'shadowOffsetY',
28
29
  'shadowColor' : 'shadowColor',
29
30
  'lineJoin': 'lineJoin',
30
- 'lineCap':'lineCap'
31
+ 'lineCap':'lineCap',
32
+ 'lineDashOffset': 'lineDashOffset',
33
+ 'globalCompositeOperation': 'globalCompositeOperation'
31
34
  };
32
35
 
33
36
  export default class jmControl extends jmProperty {
@@ -238,7 +241,9 @@ export default class jmControl extends jmProperty {
238
241
  }
239
242
 
240
243
  setStyle(style) {
241
- style = style || jmUtils.clone(this.style, true);
244
+ if(!style) {
245
+ style = this.style;
246
+ }
242
247
  if(!style) return;
243
248
 
244
249
  const __setStyle = (style, name, mpkey) => {
@@ -322,6 +327,120 @@ export default class jmControl extends jmProperty {
322
327
  this.cursor = styleValue;
323
328
  break;
324
329
  }
330
+ // ===== 新增样式特性 =====
331
+
332
+ // 虚线样式:支持自定义lineDash模式 (如 [5, 3, 2] 或 "5,3,2")
333
+ case 'lineDash' : {
334
+ if(!this.context.setLineDash) break;
335
+ let dash;
336
+ if(typeof styleValue === 'string') {
337
+ dash = styleValue.split(',').map(v => parseFloat(v.trim())).filter(v => !isNaN(v));
338
+ }
339
+ else if(Array.isArray(styleValue)) {
340
+ dash = styleValue.map(v => parseFloat(v)).filter(v => !isNaN(v));
341
+ }
342
+ if(dash && dash.length) {
343
+ this.context.setLineDash(dash);
344
+ }
345
+ else {
346
+ this.context.setLineDash([]);
347
+ }
348
+ break;
349
+ }
350
+ // 虚线偏移量
351
+ case 'lineDashOffset' : {
352
+ if(!this.context.setLineDash) break;
353
+ this.context.lineDashOffset = Number(styleValue) || 0;
354
+ break;
355
+ }
356
+ // CSS滤镜效果 (blur, grayscale, sepia, brightness, contrast, saturate, hue-rotate, invert, opacity)
357
+ case 'filter' : {
358
+ if(this.context.filter === undefined) break;
359
+ if(styleValue instanceof jmFilter) {
360
+ this.context.filter = styleValue.toCanvasFilter();
361
+ }
362
+ else if(typeof styleValue === 'string') {
363
+ this.context.filter = styleValue || 'none';
364
+ }
365
+ else if(typeof styleValue === 'object') {
366
+ this.context.filter = (new jmFilter(styleValue)).toCanvasFilter();
367
+ }
368
+ break;
369
+ }
370
+ // 混合模式 (source-over, multiply, screen, overlay, darken, lighten, etc.)
371
+ case 'globalCompositeOperation' : {
372
+ if(!this.context.globalCompositeOperation) break;
373
+ this.context.globalCompositeOperation = styleValue;
374
+ break;
375
+ }
376
+ // 裁剪路径:通过canvas clip实现
377
+ case 'clipPath' : {
378
+ if(!this.context.clip) break;
379
+ // clipPath可以是一个图形控件实例
380
+ if(styleValue && styleValue.points && styleValue.points.length > 0) {
381
+ const bounds = this.parent && this.parent.absoluteBounds ? this.parent.absoluteBounds : this.absoluteBounds;
382
+ this.context.beginPath();
383
+ this.context.moveTo(styleValue.points[0].x + (bounds ? bounds.left : 0), styleValue.points[0].y + (bounds ? bounds.top : 0));
384
+ for(let i = 1; i < styleValue.points.length; i++) {
385
+ if(styleValue.points[i].m) {
386
+ this.context.moveTo(styleValue.points[i].x + (bounds ? bounds.left : 0), styleValue.points[i].y + (bounds ? bounds.top : 0));
387
+ }
388
+ else {
389
+ this.context.lineTo(styleValue.points[i].x + (bounds ? bounds.left : 0), styleValue.points[i].y + (bounds ? bounds.top : 0));
390
+ }
391
+ }
392
+ if(styleValue.style && styleValue.style.close) {
393
+ this.context.closePath();
394
+ }
395
+ this.context.clip();
396
+ }
397
+ break;
398
+ }
399
+ // 遮罩效果:通过globalCompositeOperation + destination-in实现
400
+ case 'mask' : {
401
+ if(!this.context.globalCompositeOperation) break;
402
+ // mask是一个图形控件实例,在绘制前需要先应用mask
403
+ // 这里只是标记,实际绘制在paint流程中处理
404
+ this.__mask = styleValue;
405
+ break;
406
+ }
407
+ // 图片阴影描边阴影(WebGL纹理canvas用)
408
+ case 'shadowColor' : {
409
+ if(this.webglControl) {
410
+ this.webglControl.setStyle('shadowColor', styleValue);
411
+ }
412
+ else {
413
+ this.context.shadowColor = jmUtils.toColor(styleValue);
414
+ }
415
+ break;
416
+ }
417
+ case 'shadowBlur' : {
418
+ if(this.webglControl) {
419
+ this.webglControl.setStyle('shadowBlur', styleValue);
420
+ }
421
+ else {
422
+ this.context.shadowBlur = Number(styleValue) || 0;
423
+ }
424
+ break;
425
+ }
426
+ case 'shadowOffsetX' : {
427
+ if(this.webglControl) {
428
+ this.webglControl.setStyle('shadowOffsetX', styleValue);
429
+ }
430
+ else {
431
+ this.context.shadowOffsetX = Number(styleValue) || 0;
432
+ }
433
+ break;
434
+ }
435
+ case 'shadowOffsetY' : {
436
+ if(this.webglControl) {
437
+ this.webglControl.setStyle('shadowOffsetY', styleValue);
438
+ }
439
+ else {
440
+ this.context.shadowOffsetY = Number(styleValue) || 0;
441
+ }
442
+ break;
443
+ }
325
444
  }
326
445
  }
327
446
  }
@@ -342,6 +461,9 @@ export default class jmControl extends jmProperty {
342
461
  else if(t == 'string' && k == 'shadow') {
343
462
  style[k] = new jmShadow(style[k]);
344
463
  }
464
+ else if(t == 'string' && k == 'filter') {
465
+ style[k] = new jmFilter(style[k]);
466
+ }
345
467
  __setStyle(style[k], k);
346
468
  }
347
469
  }
@@ -486,25 +608,30 @@ export default class jmControl extends jmProperty {
486
608
  * @method getLocation
487
609
  * @return {object} 当前控件位置参数,包括中心点坐标,右上角坐标,宽高
488
610
  */
489
- getLocation(clone=true) {
611
+ getLocation() {
490
612
  //如果已经计算过则直接返回
491
613
  //在开画之前会清空此对象
492
614
  //if(reset !== true && this.location) return this.location;
493
615
 
494
616
  let local = this.location = {left: 0,top: 0,width: 0,height: 0};
495
- local.position = typeof this.position == 'function'? this.position(): jmUtils.clone(this.position);
496
- local.center = this.center && typeof this.center === 'function'?this.center(): jmUtils.clone(this.center);//中心
497
- local.start = this.start && typeof this.start === 'function'?this.start(): jmUtils.clone(this.start);//起点
498
- local.end = this.end && typeof this.end === 'function'?this.end(): jmUtils.clone(this.end);//起点
617
+
618
+ // 检查是否有百分比参数需要解析,没有则直接引用避免克隆开销
619
+ const needResolve = this.parent && (jmUtils.checkPercent(this.width) || jmUtils.checkPercent(this.height) ||
620
+ (this.position && jmUtils.checkPercent(this.position.x)) || (this.position && jmUtils.checkPercent(this.position.y)));
621
+ local.position = typeof this.position == 'function'? this.position(): (needResolve? jmUtils.clone(this.position) : this.position);
622
+ local.center = this.center && typeof this.center === 'function'?this.center(): (needResolve? jmUtils.clone(this.center) : this.center);//中心
623
+ local.start = this.start && typeof this.start === 'function'?this.start(): (needResolve? jmUtils.clone(this.start) : this.start);//起点
624
+ local.end = this.end && typeof this.end === 'function'?this.end(): (needResolve? jmUtils.clone(this.end) : this.end);//起点
499
625
  local.radius = this.radius;//半径
500
626
  local.width = this.width;
501
627
  local.height = this.height;
502
628
 
503
- const margin = jmUtils.clone(this.style.margin, {});
504
- margin.left = (margin.left || 0);
505
- margin.top = (margin.top || 0);
506
- margin.right = (margin.right || 0);
507
- margin.bottom = (margin.bottom || 0);
629
+ const margin = this.style.margin;
630
+ const marginObj = needResolve && margin ? jmUtils.clone(margin, {}) : (margin || {});
631
+ marginObj.left = (marginObj.left || 0);
632
+ marginObj.top = (marginObj.top || 0);
633
+ marginObj.right = (marginObj.right || 0);
634
+ marginObj.bottom = (marginObj.bottom || 0);
508
635
 
509
636
  //如果没有指定位置,但指定了margin。则位置取margin偏移量
510
637
  if(local.position) {
@@ -512,8 +639,8 @@ export default class jmControl extends jmProperty {
512
639
  local.top = local.position.y;
513
640
  }
514
641
  else {
515
- local.left = margin.left;
516
- local.top = margin.top;
642
+ local.left = marginObj.left;
643
+ local.top = marginObj.top;
517
644
  }
518
645
 
519
646
  if(this.parent) {
@@ -775,22 +902,31 @@ export default class jmControl extends jmProperty {
775
902
  if(this.webglControl) this.webglControl.closePath();
776
903
  this.context.closePath && this.context.closePath();
777
904
  }
778
-
779
- const fill = this.style['fill'] || this.style['fillStyle'];
780
- if(fill) {
781
- if(this.webglControl) {
905
+
906
+ // 根据渲染模式选择不同的绘制路径
907
+ if(this.webglControl) {
908
+ // WebGL 模式:使用 WebGL 绘制
909
+ const fill = this.style['fill'] || this.style['fillStyle'];
910
+ if(fill) {
782
911
  const bounds = this.getBounds();
783
912
  this.webglControl.fill(bounds);
784
913
  }
785
- this.context.fill && this.context.fill();
914
+ if(this.style['stroke'] || (!fill && !this.is('jmGraph'))) {
915
+ this.webglControl.stroke();
916
+ }
917
+ if(this.webglControl.endDraw) this.webglControl.endDraw();
786
918
  }
787
- if(this.style['stroke'] || (!fill && !this.is('jmGraph'))) {
788
- if(this.webglControl) this.webglControl.stroke();
789
- this.context.stroke && this.context.stroke();
919
+ else {
920
+ // 2D 模式:使用 Canvas 2D API 绘制
921
+ const fill = this.style['fill'] || this.style['fillStyle'];
922
+ if(fill) {
923
+ this.context.fill && this.context.fill();
924
+ }
925
+ if(this.style['stroke'] || (!fill && !this.is('jmGraph'))) {
926
+ this.context.stroke && this.context.stroke();
927
+ }
790
928
  }
791
929
 
792
- if(this.webglControl && this.webglControl.endDraw) this.webglControl.endDraw();
793
-
794
930
  this.needUpdate = false;
795
931
  }
796
932
 
@@ -806,9 +942,7 @@ export default class jmControl extends jmProperty {
806
942
  const bounds = this.parent && this.parent.absoluteBounds?this.parent.absoluteBounds:this.absoluteBounds;
807
943
  if(this.webglControl) {
808
944
  this.webglControl.setParentBounds(bounds);
809
- this.webglControl.draw([
810
- ...this.points
811
- ]);
945
+ this.webglControl.draw(this.points);
812
946
  }
813
947
  else if(this.context && this.context.moveTo) {
814
948
  this.context.moveTo(this.points[0].x + bounds.left,this.points[0].y + bounds.top);
@@ -853,9 +987,44 @@ export default class jmControl extends jmProperty {
853
987
 
854
988
  this.setStyle();//设定样式
855
989
 
856
- if(needDraw && this.beginDraw) this.beginDraw();
857
- if(needDraw && this.draw) this.draw();
858
- if(needDraw && this.endDraw) this.endDraw();
990
+ // 应用mask遮罩效果:在mask区域内绘制当前控件
991
+ // 使用 destination-in 合成模式,只保留mask区域内的内容
992
+ const maskStyle = this.style.mask || this.__mask;
993
+ if(maskStyle && maskStyle.points && this.context.globalCompositeOperation) {
994
+ // 先绘制当前控件
995
+ if(needDraw && this.beginDraw) this.beginDraw();
996
+ if(needDraw && this.draw) this.draw();
997
+ if(needDraw && this.endDraw) this.endDraw();
998
+
999
+ // 再应用mask裁剪
1000
+ this.context.globalCompositeOperation = 'destination-in';
1001
+ if(maskStyle.initPoints) maskStyle.initPoints();
1002
+ const mBounds = maskStyle.parent && maskStyle.parent.absoluteBounds ? maskStyle.parent.absoluteBounds : this.absoluteBounds;
1003
+ this.context.beginPath();
1004
+ if(maskStyle.points && maskStyle.points.length > 0) {
1005
+ this.context.moveTo(maskStyle.points[0].x + (mBounds ? mBounds.left : 0), maskStyle.points[0].y + (mBounds ? mBounds.top : 0));
1006
+ for(let i = 1; i < maskStyle.points.length; i++) {
1007
+ if(maskStyle.points[i].m) {
1008
+ this.context.moveTo(maskStyle.points[i].x + (mBounds ? mBounds.left : 0), maskStyle.points[i].y + (mBounds ? mBounds.top : 0));
1009
+ }
1010
+ else {
1011
+ this.context.lineTo(maskStyle.points[i].x + (mBounds ? mBounds.left : 0), maskStyle.points[i].y + (mBounds ? mBounds.top : 0));
1012
+ }
1013
+ }
1014
+ if(maskStyle.style && maskStyle.style.close) {
1015
+ this.context.closePath();
1016
+ }
1017
+ }
1018
+ this.context.fillStyle = '#ffffff';
1019
+ this.context.fill();
1020
+ // 恢复合成模式
1021
+ this.context.globalCompositeOperation = 'source-over';
1022
+ }
1023
+ else {
1024
+ if(needDraw && this.beginDraw) this.beginDraw();
1025
+ if(needDraw && this.draw) this.draw();
1026
+ if(needDraw && this.endDraw) this.endDraw();
1027
+ }
859
1028
 
860
1029
  if(this.children) {
861
1030
  this.children.each(function(i,item) {
@@ -0,0 +1,150 @@
1
+ import {jmUtils} from "./jmUtils.js";
2
+
3
+ /**
4
+ * CSS滤镜效果类
5
+ * 支持的滤镜: blur, grayscale, sepia, brightness, contrast, saturate, hue-rotate, invert, opacity
6
+ *
7
+ * @class jmFilter
8
+ * @param {string|object} opt 滤镜参数
9
+ * 字符串格式: "blur(2px) grayscale(50%) brightness(1.2)"
10
+ * 对象格式: { blur: 2, grayscale: 0.5, brightness: 1.2 }
11
+ */
12
+ export default class jmFilter {
13
+ constructor(opt) {
14
+ this.filters = [];
15
+
16
+ if(typeof opt === 'string') {
17
+ this.fromString(opt);
18
+ }
19
+ else if(opt && typeof opt === 'object') {
20
+ for(let k in opt) {
21
+ if(k === 'constructor' || k === 'filters') continue;
22
+ this.addFilter(k, opt[k]);
23
+ }
24
+ }
25
+ }
26
+
27
+ /**
28
+ * 添加单个滤镜
29
+ * @param {string} name 滤镜名称 (blur, grayscale, sepia, brightness, contrast, saturate, hue-rotate, invert, opacity)
30
+ * @param {number|string} value 滤镜值
31
+ */
32
+ addFilter(name, value) {
33
+ name = name.toLowerCase().trim();
34
+ if(typeof value === 'string') {
35
+ value = parseFloat(value);
36
+ }
37
+ if(isNaN(value)) return;
38
+
39
+ // 规范化滤镜名称
40
+ const normalized = {
41
+ 'blur': 'blur',
42
+ 'grayscale': 'grayscale',
43
+ 'greyscale': 'grayscale',
44
+ 'sepia': 'sepia',
45
+ 'brightness': 'brightness',
46
+ 'contrast': 'contrast',
47
+ 'saturate': 'saturate',
48
+ 'hue-rotate': 'hueRotate',
49
+ 'hueRotate': 'hueRotate',
50
+ 'invert': 'invert',
51
+ 'opacity': 'opacity'
52
+ }[name];
53
+
54
+ if(!normalized) return;
55
+
56
+ // 检查是否已有同名滤镜,有则更新
57
+ const existing = this.filters.find(f => f.name === normalized);
58
+ if(existing) {
59
+ existing.value = value;
60
+ }
61
+ else {
62
+ this.filters.push({ name: normalized, value: value });
63
+ }
64
+ }
65
+
66
+ /**
67
+ * 从字符串格式解析滤镜
68
+ * 格式: "blur(2px) grayscale(50%) brightness(1.2)"
69
+ * @param {string} s 滤镜字符串
70
+ */
71
+ fromString(s) {
72
+ if(!s || typeof s !== 'string') return;
73
+ // 匹配 filterName(value) 模式
74
+ const regex = /([a-zA-Z-]+)\s*\(\s*([^)]+)\s*\)/g;
75
+ let match;
76
+ while((match = regex.exec(s)) !== null) {
77
+ const name = match[1];
78
+ const valueStr = match[2].replace(/[a-z%]+$/i, '').trim();
79
+ const value = parseFloat(valueStr);
80
+ if(!isNaN(value)) {
81
+ this.addFilter(name, value);
82
+ }
83
+ }
84
+ }
85
+
86
+ /**
87
+ * 转换为CSS filter字符串格式
88
+ * @returns {string}
89
+ */
90
+ toString() {
91
+ return this.filters.map(f => {
92
+ switch(f.name) {
93
+ case 'blur':
94
+ return `blur(${f.value}px)`;
95
+ case 'hueRotate':
96
+ return `hue-rotate(${f.value}deg)`;
97
+ default:
98
+ return `${f.name}(${f.value})`;
99
+ }
100
+ }).join(' ');
101
+ }
102
+
103
+ /**
104
+ * 转换为Canvas context.filter可用的字符串
105
+ * @returns {string}
106
+ */
107
+ toCanvasFilter() {
108
+ if(this.filters.length === 0) return 'none';
109
+ return this.toString();
110
+ }
111
+
112
+ /**
113
+ * 检查是否有指定名称的滤镜
114
+ * @param {string} name 滤镜名称
115
+ * @returns {boolean}
116
+ */
117
+ has(name) {
118
+ return this.filters.some(f => f.name === name);
119
+ }
120
+
121
+ /**
122
+ * 获取指定滤镜的值
123
+ * @param {string} name 滤镜名称
124
+ * @returns {number|undefined}
125
+ */
126
+ get(name) {
127
+ const f = this.filters.find(f => f.name === name);
128
+ return f ? f.value : undefined;
129
+ }
130
+
131
+ /**
132
+ * 移除指定滤镜
133
+ * @param {string} name 滤镜名称
134
+ */
135
+ remove(name) {
136
+ const index = this.filters.findIndex(f => f.name === name);
137
+ if(index > -1) {
138
+ this.filters.splice(index, 1);
139
+ }
140
+ }
141
+
142
+ /**
143
+ * 清空所有滤镜
144
+ */
145
+ clear() {
146
+ this.filters = [];
147
+ }
148
+ }
149
+
150
+ export { jmFilter };