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/README.md +137 -4
- package/dist/jmgraph.core.min.js +1 -1
- package/dist/jmgraph.core.min.js.map +1 -1
- package/dist/jmgraph.js +976 -827
- package/dist/jmgraph.min.js +1 -1
- package/index.js +13 -26
- package/package.json +1 -1
- package/src/core/jmControl.js +199 -30
- package/src/core/jmFilter.js +150 -0
- package/src/core/jmGraph.js +16 -265
- package/src/core/jmUtils.js +46 -37
- package/src/lib/webgl/base.js +10 -36
- package/src/lib/webgl/gradient.js +16 -3
- package/src/lib/webgl/index.js +5 -4
- package/src/lib/webgl/path.js +156 -33
- package/src/shapes/jmLabel.js +1 -62
- package/src/shapes/jmRect.js +107 -29
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
|
-
|
|
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
|
|
66
|
+
return new jmGraphImpl(...args);
|
|
82
67
|
}
|
|
83
68
|
|
|
84
|
-
export
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
107
|
-
jmGraph,
|
|
94
|
+
jmGraphImpl as jmGraph,
|
|
108
95
|
createJmGraph as create
|
|
109
96
|
};
|
|
110
97
|
|
package/package.json
CHANGED
package/src/core/jmControl.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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 =
|
|
504
|
-
margin.
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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 =
|
|
516
|
-
local.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
|
-
|
|
780
|
-
if(
|
|
781
|
-
|
|
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.
|
|
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
|
-
|
|
788
|
-
|
|
789
|
-
this.
|
|
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
|
-
|
|
857
|
-
|
|
858
|
-
|
|
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 };
|