jmgraph 3.2.19 → 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.
Files changed (63) hide show
  1. package/README.md +311 -6
  2. package/dist/jmgraph.core.min.js +1 -1
  3. package/dist/jmgraph.core.min.js.map +1 -1
  4. package/dist/jmgraph.js +2022 -368
  5. package/dist/jmgraph.min.js +1 -1
  6. package/index.js +23 -25
  7. package/package.json +1 -1
  8. package/src/core/jmControl.js +199 -30
  9. package/src/core/jmFilter.js +150 -0
  10. package/src/core/jmGraph.js +207 -7
  11. package/src/core/jmLayer.js +142 -0
  12. package/src/core/jmPath.js +55 -0
  13. package/src/core/jmUtils.js +46 -37
  14. package/src/lib/webgl/base.js +10 -36
  15. package/src/lib/webgl/gradient.js +16 -3
  16. package/src/lib/webgl/index.js +5 -4
  17. package/src/lib/webgl/path.js +156 -33
  18. package/src/shapes/jmEllipse.js +91 -0
  19. package/src/shapes/jmLabel.js +126 -75
  20. package/src/shapes/jmPolygon.js +129 -0
  21. package/src/shapes/jmRect.js +107 -29
  22. package/src/shapes/jmStar.js +160 -0
  23. package/example/ball.html +0 -217
  24. package/example/base.html +0 -112
  25. package/example/canvas.html +0 -54
  26. package/example/cell.html +0 -284
  27. package/example/controls/arc.html +0 -129
  28. package/example/controls/arrowline.html +0 -78
  29. package/example/controls/bezier.html +0 -299
  30. package/example/controls/img.html +0 -97
  31. package/example/controls/label.html +0 -87
  32. package/example/controls/line.html +0 -173
  33. package/example/controls/prismatic.html +0 -63
  34. package/example/controls/rect.html +0 -64
  35. package/example/controls/resize.html +0 -112
  36. package/example/controls/test.html +0 -360
  37. package/example/es.html +0 -70
  38. package/example/es5module.html +0 -63
  39. package/example/heartarc.html +0 -116
  40. package/example/index.html +0 -47
  41. package/example/js/require.js +0 -5
  42. package/example/love/img/bling/bling.tps +0 -265
  43. package/example/love/img/bling.json +0 -87
  44. package/example/love/img/bling.tps +0 -295
  45. package/example/love/img/doc/bling.gif +0 -0
  46. package/example/love/img/love.json +0 -95
  47. package/example/love/img/love.tps +0 -315
  48. package/example/love/img/qq/qq.tps +0 -399
  49. package/example/love/img/qq.json +0 -242
  50. package/example/love/index.html +0 -40
  51. package/example/love/js/game.js +0 -558
  52. package/example/music.html +0 -211
  53. package/example/node/test.js +0 -138
  54. package/example/pdf.html +0 -187
  55. package/example/progress.html +0 -173
  56. package/example/pso.html +0 -148
  57. package/example/sort.html +0 -805
  58. package/example/tweenjs.html +0 -84
  59. package/example/webgl.html +0 -278
  60. package/example/xfj/img/dr_die.gif +0 -0
  61. package/example/xfj/index.html +0 -332
  62. package/example/xfj/shake.js +0 -49
  63. package/example/xfj/testori.html +0 -76
@@ -53,8 +53,14 @@ class WebglGradient {
53
53
 
54
54
  // 转为渐变为纹理
55
55
  toImageData(control, bounds, points=null) {
56
- //const key = this.key || this.toString();
57
- //if(WebglGradientTextureCache[key]) return WebglGradientTextureCache[key];
56
+ // 缓存基于渐变参数(不含 bounds,因为同一个渐变只是位置不同时纹理相同)
57
+ const gradientKey = this.toString();
58
+ if(this.__cachedData && this.__cacheKey === gradientKey &&
59
+ this.__cachedData.data && this.__cachedData.data.width === Math.ceil(bounds.width) &&
60
+ this.__cachedData.data.data && this.__cachedData.data.data.height === Math.ceil(bounds.height)) {
61
+ return this.__cachedData;
62
+ }
63
+
58
64
  if(!control.textureContext) {
59
65
  return null;
60
66
  }
@@ -72,11 +78,18 @@ class WebglGradient {
72
78
 
73
79
  const data = control.toFillTexture(gradient, bounds, points);
74
80
 
75
- //WebglGradientTextureCache[key] = data;
81
+ this.__cachedData = data;
82
+ this.__cacheKey = gradientKey;
76
83
 
77
84
  return data;
78
85
  }
79
86
 
87
+ // 当渐变参数变化时使缓存失效
88
+ invalidateCache() {
89
+ this.__cachedData = null;
90
+ this.__cacheKey = null;
91
+ }
92
+
80
93
  // 根据绘制图形的坐标计算出对应点的颜色
81
94
  /*
82
95
  toPointColors(points) {
@@ -105,12 +105,13 @@ class webgl {
105
105
  // 由具体的绘制方法处理
106
106
  }
107
107
 
108
- // 测量文本宽度
108
+ // 测量文本宽度(复用纹理 canvas 的 context)
109
109
  measureText(text) {
110
- // 使用 2D canvas 测量
110
+ const ctx = this.base.textureContext;
111
+ if(ctx && ctx.measureText) return ctx.measureText(text);
111
112
  const canvas = document.createElement('canvas');
112
- const ctx = canvas.getContext('2d');
113
- return ctx.measureText(text);
113
+ const ctx2 = canvas.getContext('2d');
114
+ return ctx2.measureText(text);
114
115
  }
115
116
 
116
117
  // 创建线性渐变
@@ -9,6 +9,40 @@ class WebglPath extends WebglBase {
9
9
  this.needCut = option.needCut || false;
10
10
  this.control = option.control;
11
11
  this.points = [];
12
+ // 缓存 buffer 和纹理,避免每帧创建/销毁
13
+ this.__cachedBuffers = [];
14
+ this.__cachedTexture = null;
15
+ this.__cachedTextureKey = null;
16
+ }
17
+
18
+ // 释放缓存的 WebGL 资源
19
+ dispose() {
20
+ for(const buf of this.__cachedBuffers) {
21
+ this.deleteBuffer(buf);
22
+ }
23
+ this.__cachedBuffers = [];
24
+ if(this.__cachedTexture) {
25
+ this.deleteTexture(this.__cachedTexture);
26
+ this.__cachedTexture = null;
27
+ this.__cachedTextureKey = null;
28
+ }
29
+ }
30
+
31
+ // 获取或创建 buffer,优先复用缓存
32
+ getOrCreateBuffer(data, attr) {
33
+ let buffer = this.__cachedBuffers.find(b => b.attr === attr);
34
+ if(buffer) {
35
+ const gl = this.context;
36
+ const float32 = new Float32Array(data);
37
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer.buffer);
38
+ gl.bufferData(gl.ARRAY_BUFFER, float32, gl.DYNAMIC_DRAW);
39
+ buffer.data = data;
40
+ return buffer;
41
+ }
42
+ buffer = this.createFloat32Buffer(data);
43
+ buffer.attr = attr;
44
+ this.__cachedBuffers.push(buffer);
45
+ return buffer;
12
46
  }
13
47
 
14
48
  // 应用变换到点
@@ -63,6 +97,7 @@ class WebglPath extends WebglBase {
63
97
  endDraw() {
64
98
  if(this.points) delete this.points;
65
99
  if(this.pathPoints) delete this.pathPoints;
100
+ // 缓存的纹理保留到下次绘制(渐变可能不变)
66
101
  }
67
102
 
68
103
  // 图形封闭
@@ -74,21 +109,47 @@ class WebglPath extends WebglBase {
74
109
  }
75
110
  }
76
111
 
77
- // 绘制点数组
112
+ // 绘制点数组(使用 DYNAMIC_DRAW 复用 buffer,避免每帧 create/delete)
78
113
  writePoints(points, attr = this.program.attrs.a_position) {
79
-
80
114
  const fixedPoints = [];
81
- for(const p of points) {
82
- // 应用变换矩阵
83
- const transformedPoint = this.applyTransform(p);
84
- fixedPoints.push(
85
- transformedPoint.x + this.parentAbsoluteBounds.left,
86
- transformedPoint.y + this.parentAbsoluteBounds.top
87
- );
88
- }
89
- const vertexBuffer = this.createFloat32Buffer(fixedPoints);
115
+ const [a, b, c, d, tx, ty] = this.transformMatrix;
116
+ const isIdentity = (a === 1 && b === 0 && c === 0 && d === 1 && tx === 0 && ty === 0);
117
+ const offsetLeft = this.parentAbsoluteBounds.left;
118
+ const offsetTop = this.parentAbsoluteBounds.top;
119
+
120
+ if(isIdentity) {
121
+ // 单位矩阵时直接加偏移,避免逐点调用 applyTransform
122
+ for(let i = 0; i < points.length; i++) {
123
+ fixedPoints.push(points[i].x + offsetLeft, points[i].y + offsetTop);
124
+ }
125
+ } else {
126
+ for(const p of points) {
127
+ const transformedPoint = this.applyTransform(p);
128
+ fixedPoints.push(
129
+ transformedPoint.x + offsetLeft,
130
+ transformedPoint.y + offsetTop
131
+ );
132
+ }
133
+ }
134
+ const float32 = new Float32Array(fixedPoints);
135
+ const gl = this.context;
136
+
137
+ // 复用已有 buffer 或创建新的
138
+ if(this.__cachedBuffers.length > 0) {
139
+ // 找一个同 attr 的 buffer 复用
140
+ let buffer = this.__cachedBuffers.find(b => b.attr === attr);
141
+ if(buffer) {
142
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer.buffer);
143
+ gl.bufferData(gl.ARRAY_BUFFER, float32, gl.DYNAMIC_DRAW);
144
+ buffer.data = fixedPoints;
145
+ this.writeVertexAttrib(buffer, attr, 2, 0, 0);
146
+ return buffer;
147
+ }
148
+ }
149
+ const vertexBuffer = this.createFloat32Buffer(float32, gl.ARRAY_BUFFER, gl.DYNAMIC_DRAW);
90
150
  this.writeVertexAttrib(vertexBuffer, attr, 2, 0, 0);
91
151
  vertexBuffer.attr = attr;
152
+ this.__cachedBuffers.push(vertexBuffer);
92
153
  return vertexBuffer;
93
154
  }
94
155
 
@@ -370,9 +431,10 @@ class WebglPath extends WebglBase {
370
431
 
371
432
  // 分割成一个个规则的三角形,不规则的多边形不全割的话纹理就会没法正确覆盖
372
433
  getTriangles(points) {
373
-
374
434
  this.trianglesCache = this.trianglesCache||(this.trianglesCache={});
375
- const key = JSON.stringify(points);
435
+ // 快速缓存 key:用长度和首尾点坐标(比 JSON.stringify 快几个数量级)
436
+ const len = points.length;
437
+ const key = len + '_' + points[0].x + '_' + points[0].y + '_' + points[len-1].x + '_' + points[len-1].y;
376
438
  if(this.trianglesCache[key]) return this.trianglesCache[key];
377
439
 
378
440
  const res = [];
@@ -411,10 +473,9 @@ class WebglPath extends WebglBase {
411
473
  points = regular? points : this.pathToPoints(points);
412
474
  const buffer = this.writePoints(points);
413
475
  this.context.drawArrays(regular? this.context.LINE_LOOP: this.context.POINTS, 0, points.length);
414
- this.deleteBuffer(buffer);
476
+ // buffer 由 endDraw 统一清理
415
477
  }
416
- colorBuffer && this.deleteBuffer(colorBuffer);
417
- colorBuffer && this.disableVertexAttribArray(colorBuffer.attr);
478
+ colorBuffer && this.disableVertexAttribArray(colorBuffer && colorBuffer.attr);
418
479
  }
419
480
 
420
481
  // 填充图形
@@ -445,8 +506,7 @@ class WebglPath extends WebglBase {
445
506
 
446
507
  this.fillPolygons(points);
447
508
 
448
- colorBuffer && this.deleteBuffer(colorBuffer);
449
- colorBuffer && this.disableVertexAttribArray(colorBuffer.attr);
509
+ colorBuffer && this.disableVertexAttribArray(colorBuffer && colorBuffer.attr);
450
510
 
451
511
  }
452
512
 
@@ -456,8 +516,24 @@ class WebglPath extends WebglBase {
456
516
  fillImage(img, points, bounds) {
457
517
  if(!img) return;
458
518
 
459
- // 设置纹理
460
- const texture = img instanceof ImageData? this.createDataTexture(img) : this.createImgTexture(img);
519
+ // 对于 ImageData,生成缓存 key(基于渐变参数或 bounds),复用纹理
520
+ let texture = null;
521
+ if(img instanceof ImageData) {
522
+ const key = `${img.width}_${img.height}_${bounds.width}_${bounds.height}_${bounds.left}_${bounds.top}`;
523
+ if(this.__cachedTexture && this.__cachedTextureKey === key) {
524
+ texture = this.__cachedTexture;
525
+ } else {
526
+ texture = this.createDataTexture(img);
527
+ // 释放旧纹理
528
+ if(this.__cachedTexture) {
529
+ this.deleteTexture(this.__cachedTexture);
530
+ }
531
+ this.__cachedTexture = texture;
532
+ this.__cachedTextureKey = key;
533
+ }
534
+ } else {
535
+ texture = this.createImgTexture(img);
536
+ }
461
537
  this.context.uniform1i(this.program.uniforms.u_sample.location, 0); // 纹理单元传递给着色器
462
538
 
463
539
  // 指定纹理区域尺寸
@@ -470,7 +546,10 @@ class WebglPath extends WebglBase {
470
546
 
471
547
  this.fillTexture(points);
472
548
 
473
- this.deleteTexture(texture);
549
+ // 仅对非缓存纹理(非 ImageData)立即删除
550
+ if(!(img instanceof ImageData)) {
551
+ this.deleteTexture(texture);
552
+ }
474
553
  }
475
554
 
476
555
  fillTexture(points) {
@@ -486,23 +565,67 @@ class WebglPath extends WebglBase {
486
565
 
487
566
  // 进行多边形填充
488
567
  fillPolygons(points, isTexture = false) {
489
- if(points.length > 3) {
490
- const triangles = this.needCut? this.earCutPointsToTriangles(points): this.getTriangles(points);
491
- if(triangles.length) {
492
- for(const triangle of triangles) {
493
- this.fillPolygons(triangle, isTexture);// 这里就变成了规则的图形了
494
- }
495
- }
496
- }
497
- else {
568
+ if(points.length <= 3) {
569
+ // 3个点以下的三角形直接画
498
570
  const buffer = this.writePoints(points);
499
- // 纹理坐标
500
571
  const coordBuffer = isTexture? this.writePoints(points, this.program.attrs.a_text_coord): null;
572
+ this.context.drawArrays(this.context.TRIANGLE_FAN, 0, points.length);
573
+ return;
574
+ }
501
575
 
576
+ // 规则图形(凸多边形,如圆):直接用 TRIANGLE_FAN 一次性绘制,无需 earcut
577
+ if(this.isRegular) {
578
+ const buffer = this.writePoints(points);
579
+ const coordBuffer = isTexture? this.writePoints(points, this.program.attrs.a_text_coord): null;
502
580
  this.context.drawArrays(this.context.TRIANGLE_FAN, 0, points.length);
503
- this.deleteBuffer(buffer);
504
- coordBuffer && this.deleteBuffer(coordBuffer);
581
+ return;
505
582
  }
583
+
584
+ // 不规则图形:需要 earcut 三角化后,合并为一个大的顶点缓冲区,单次 drawArrays
585
+ const triangles = this.needCut? this.earCutPointsToTriangles(points): this.getTriangles(points);
586
+ if(!triangles.length) return;
587
+
588
+ // 合并所有三角形的顶点到一个数组
589
+ const allVertices = [];
590
+ const allTexCoords = [];
591
+ for(const triangle of triangles) {
592
+ for(const p of triangle) {
593
+ allVertices.push(p.x, p.y);
594
+ if(isTexture) allTexCoords.push(p.x, p.y);
595
+ }
596
+ }
597
+
598
+ // 一次性上传所有数据并绘制
599
+ const vertexData = new Float32Array(allVertices);
600
+ const gl = this.context;
601
+
602
+ // 复用或创建 position buffer
603
+ let posBuffer = this.__cachedBuffers.find(b => b.attr === this.program.attrs.a_position);
604
+ if(!posBuffer) {
605
+ posBuffer = this.createFloat32Buffer(vertexData, gl.ARRAY_BUFFER, gl.DYNAMIC_DRAW);
606
+ posBuffer.attr = this.program.attrs.a_position;
607
+ this.__cachedBuffers.push(posBuffer);
608
+ } else {
609
+ gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer.buffer);
610
+ gl.bufferData(gl.ARRAY_BUFFER, vertexData, gl.DYNAMIC_DRAW);
611
+ }
612
+ this.writeVertexAttrib(posBuffer, this.program.attrs.a_position, 2, 0, 0);
613
+
614
+ if(isTexture && allTexCoords.length) {
615
+ const texData = new Float32Array(allTexCoords);
616
+ let texBuffer = this.__cachedBuffers.find(b => b.attr === this.program.attrs.a_text_coord);
617
+ if(!texBuffer) {
618
+ texBuffer = this.createFloat32Buffer(texData, gl.ARRAY_BUFFER, gl.DYNAMIC_DRAW);
619
+ texBuffer.attr = this.program.attrs.a_text_coord;
620
+ this.__cachedBuffers.push(texBuffer);
621
+ } else {
622
+ gl.bindBuffer(gl.ARRAY_BUFFER, texBuffer.buffer);
623
+ gl.bufferData(gl.ARRAY_BUFFER, texData, gl.DYNAMIC_DRAW);
624
+ }
625
+ this.writeVertexAttrib(texBuffer, this.program.attrs.a_text_coord, 2, 0, 0);
626
+ }
627
+
628
+ gl.drawArrays(gl.TRIANGLES, 0, allVertices.length / 2);
506
629
  }
507
630
 
508
631
  // 填充图形
@@ -0,0 +1,91 @@
1
+ import {jmArc} from "./jmArc.js";
2
+
3
+ /**
4
+ * 画椭圆
5
+ * 椭圆是通过缩放圆形来实现的,支持完整的椭圆和椭圆弧
6
+ * 可以指定起始角度和结束角度来绘制椭圆弧
7
+ *
8
+ * @class jmEllipse
9
+ * @extends jmArc
10
+ * @param {object} params 椭圆的参数
11
+ * @param {object} [params.center={x:0,y:0}] 椭圆中心点坐标
12
+ * @param {number} [params.width=100] 椭圆宽度(长轴直径)
13
+ * @param {number} [params.height=60] 椭圆高度(短轴直径)
14
+ * @param {number} [params.startAngle=0] 起始角度(弧度)
15
+ * @param {number} [params.endAngle=Math.PI*2] 结束角度(弧度)
16
+ * @param {boolean} [params.anticlockwise=false] 是否逆时针绘制
17
+ */
18
+ export default class jmEllipse extends jmArc {
19
+
20
+ constructor(params, t='jmEllipse') {
21
+ params = params || {};
22
+ params.isRegular = true; // 标记为规则图形
23
+ super(params, t);
24
+ }
25
+
26
+ /**
27
+ * 初始化图形点
28
+ * 为WebGL模式生成控制点,2D模式使用draw方法直接绘制
29
+ *
30
+ * @method initPoints
31
+ * @private
32
+ * @for jmEllipse
33
+ */
34
+ initPoints() {
35
+ // WebGL模式使用父类的点生成方法
36
+ if(this.graph.mode === 'webgl') {
37
+ return super.initPoints();
38
+ }
39
+
40
+ // 2D模式:生成4个控制点用于边界计算
41
+ // 这些点不是实际的绘制点,而是用于碰撞检测和边界计算
42
+ let location = this.getLocation();
43
+
44
+ this.points = [];
45
+ this.points.push({x:location.center.x - location.width/2, y:location.center.y}); // 左
46
+ this.points.push({x:location.center.x, y:location.center.y - location.height/2}); // 上
47
+ this.points.push({x:location.center.x + location.width/2, y:location.center.y}); // 右
48
+ this.points.push({x:location.center.x, y:location.center.y + location.height/2}); // 下
49
+ }
50
+
51
+ /**
52
+ * 重写基类画图,此处为画一个椭圆
53
+ * 使用Canvas的变换功能(平移和缩放)来绘制椭圆
54
+ *
55
+ * @method draw
56
+ */
57
+ draw() {
58
+ // WebGL模式使用父类的绘制方法
59
+ if(this.graph.mode === 'webgl') {
60
+ return super.draw();
61
+ }
62
+
63
+ // 获取边界和位置信息
64
+ let bounds = this.parent && this.parent.absoluteBounds ? this.parent.absoluteBounds : this.absoluteBounds;
65
+ let location = this.getLocation();
66
+
67
+ // 获取椭圆弧参数
68
+ let start = this.startAngle || 0;
69
+ let end = this.endAngle || Math.PI * 2;
70
+ let anticlockwise = this.anticlockwise || false;
71
+
72
+ // 椭圆绘制:通过变换圆形来实现
73
+ // 1. 保存当前绘图状态
74
+ this.context.save();
75
+
76
+ // 2. 平移到椭圆中心
77
+ this.context.translate(location.center.x + bounds.left, location.center.y + bounds.top);
78
+
79
+ // 3. 缩放坐标系,使圆形变为椭圆
80
+ // 将X轴缩放width/2,Y轴缩放height/2,这样单位圆就变成了椭圆
81
+ this.context.scale(location.width/2, location.height/2);
82
+
83
+ // 4. 绘制单位圆(会被缩放成椭圆)
84
+ this.context.arc(0, 0, 1, start, end, anticlockwise);
85
+
86
+ // 5. 恢复绘图状态
87
+ this.context.restore();
88
+ }
89
+ }
90
+
91
+ export { jmEllipse };
@@ -22,7 +22,7 @@ export default class jmLabel extends jmControl {
22
22
  this.style.textAlign = this.style.textAlign || 'left';
23
23
  //文字垂直对齐
24
24
  this.style.textBaseline = this.style.textBaseline || 'middle';
25
- this.text = params.text || '';
25
+ this.text = params.text || params.value || '';
26
26
 
27
27
  this.center = params.center || null;
28
28
  }
@@ -108,43 +108,137 @@ export default class jmLabel extends jmControl {
108
108
 
109
109
  /**
110
110
  * 测试获取文本所占大小
111
- *
111
+ * 计算文本渲染所需的宽度和高度,支持自动换行
112
+ *
112
113
  * @method testSize
113
- * @return {object} 含文本大小的对象
114
+ * @return {object} 含文本大小的对象 {width, height}
114
115
  */
115
116
  testSize() {
117
+ // 使用缓存提高性能,避免重复计算
116
118
  if(this.__size) return this.__size;
117
119
 
118
- if(this.webglControl) this.__size = this.webglControl.testSize(this.text, this.style);
120
+ if(this.webglControl) {
121
+ this.__size = this.webglControl.testSize(this.text, this.style);
122
+ }
119
123
  else {
120
124
  this.context.save && this.context.save();
121
- // 修改字体,用来计算
125
+
126
+ // 设置字体样式用于测量
122
127
  this.setStyle({
123
128
  font: this.style.font || (this.style.fontSize + 'px ' + this.style.fontFamily)
124
129
  });
125
- //计算宽度
126
- this.__size = this.context.measureText?
127
- this.context.measureText(this.text):
128
- {width:15};
130
+
131
+ // 计算文本尺寸
132
+ if(this.style.maxWidth && this.text) {
133
+ // 文本换行处理
134
+ const lines = this.wrapText(this.text, this.style.maxWidth);
135
+ let maxWidth = 0;
136
+
137
+ // 找出最宽的一行
138
+ for(let line of lines) {
139
+ const width = this.context.measureText(line).width;
140
+ if(width > maxWidth) maxWidth = width;
141
+ }
142
+
143
+ // 计算总高度(行数 × 行高)
144
+ const lineHeight = this.style.lineHeight || this.style.fontSize * 1.2;
145
+ this.__size = {
146
+ width: maxWidth,
147
+ height: lineHeight * lines.length
148
+ };
149
+ }
150
+ else {
151
+ // 单行文本
152
+ this.__size = this.context.measureText ?
153
+ this.context.measureText(this.text) :
154
+ {width: 15};
155
+ this.__size.height = this.style.fontSize ? this.style.fontSize : 15;
156
+ }
157
+
129
158
  this.context.restore && this.context.restore();
130
- this.__size.height = this.style.fontSize?this.style.fontSize:15;
131
159
  }
132
160
 
161
+ // 设置默认宽高
133
162
  if(!this.width) this.width = this.__size.width;
134
163
  if(!this.height) this.height = this.__size.height;
135
164
 
136
165
  return this.__size;
137
166
  }
138
167
 
168
+ /**
169
+ * 文本换行处理
170
+ * 根据最大宽度将文本分割成多行
171
+ * 支持中英文混合文本,优先在空格处换行
172
+ *
173
+ * @method wrapText
174
+ * @param {string} text 文本内容
175
+ * @param {number} maxWidth 最大宽度(像素)
176
+ * @return {array} 换行后的文本数组
177
+ */
178
+ wrapText(text, maxWidth) {
179
+ // 参数验证
180
+ if(!text || !maxWidth) return [text || ''];
181
+
182
+ // 检查缓存,避免重复计算
183
+ const cacheKey = `${text}_${maxWidth}`;
184
+ if(this.__wrapTextCache && this.__wrapTextCache.key === cacheKey) {
185
+ return this.__wrapTextCache.lines;
186
+ }
187
+
188
+ const lines = [];
189
+
190
+ // 先按换行符分割
191
+ const paragraphs = text.split('\n');
192
+
193
+ for(let paragraph of paragraphs) {
194
+ // 如果段落为空,添加空行
195
+ if(!paragraph) {
196
+ lines.push('');
197
+ continue;
198
+ }
199
+
200
+ // 按空格分割单词
201
+ const words = paragraph.split(' ');
202
+ let currentLine = words[0];
203
+
204
+ for(let i = 1; i < words.length; i++) {
205
+ const word = words[i];
206
+ const testLine = currentLine + ' ' + word;
207
+ const metrics = this.context.measureText(testLine);
208
+ const testWidth = metrics.width;
209
+
210
+ if(testWidth <= maxWidth) {
211
+ // 当前行还能容纳这个单词
212
+ currentLine = testLine;
213
+ } else {
214
+ // 当前行已满,保存当前行并开始新行
215
+ if(currentLine) lines.push(currentLine);
216
+ currentLine = word;
217
+ }
218
+ }
219
+
220
+ // 添加最后一行
221
+ if(currentLine) lines.push(currentLine);
222
+ }
223
+
224
+ // 缓存结果
225
+ this.__wrapTextCache = {
226
+ key: cacheKey,
227
+ lines: lines
228
+ };
229
+
230
+ return lines;
231
+ }
232
+
139
233
  /**
140
234
  * 根据位置偏移画字符串
141
235
  *
142
236
  * @method draw
143
237
  */
144
- draw() {
238
+ draw() {
145
239
 
146
240
  //获取当前控件的绝对位置
147
- let bounds = this.parent && this.parent.absoluteBounds?this.parent.absoluteBounds:this.absoluteBounds;
241
+ let bounds = this.parent && this.parent.absoluteBounds?this.parent.absoluteBounds:this.absoluteBounds;
148
242
  const size = this.testSize();
149
243
  let location = this.location;
150
244
  let x = location.left + bounds.left;
@@ -184,7 +278,16 @@ export default class jmLabel extends jmControl {
184
278
  }
185
279
  else if(this.style.fill && this.context.fillText) {
186
280
  if(this.style.maxWidth) {
187
- this.context.fillText(txt,x,y, this.style.maxWidth);
281
+ // 绘制换行文本
282
+ const lines = this.wrapText(txt, this.style.maxWidth);
283
+ const lineHeight = this.style.fontSize;
284
+ // 调整起始Y位置以支持垂直对齐
285
+ const startY = y - (lines.length - 1) * lineHeight / 2;
286
+
287
+ for(let i = 0; i < lines.length; i++) {
288
+ const lineY = startY + i * lineHeight;
289
+ this.context.fillText(lines[i], x, lineY);
290
+ }
188
291
  }
189
292
  else {
190
293
  this.context.fillText(txt,x,y);
@@ -192,74 +295,22 @@ export default class jmLabel extends jmControl {
192
295
  }
193
296
  else if(this.context.strokeText) {
194
297
  if(this.style.maxWidth) {
195
- this.context.strokeText(txt,x,y, this.style.maxWidth);
298
+ // 绘制换行文本
299
+ const lines = this.wrapText(txt, this.style.maxWidth);
300
+ const lineHeight = this.style.fontSize;
301
+ // 调整起始Y位置以支持垂直对齐
302
+ const startY = y - (lines.length - 1) * lineHeight / 2;
303
+
304
+ for(let i = 0; i < lines.length; i++) {
305
+ const lineY = startY + i * lineHeight;
306
+ this.context.strokeText(lines[i], x, lineY);
307
+ }
196
308
  }
197
309
  else {
198
310
  this.context.strokeText(txt,x,y);
199
311
  }
200
312
  }
201
313
  }
202
- //如果有指定边框,则画出边框
203
- if(this.style.border) {
204
- //如果指定了边框样式
205
- if(this.style.border.style) {
206
- this.context.save && this.context.save();
207
- this.setStyle(this.style.border.style);
208
- }
209
- if(this.mode === '2d') {
210
- this.context.moveTo(this.points[0].x + bounds.left,this.points[0].y + bounds.top);
211
- if(this.style.border.top) {
212
- this.context.lineTo(this.points[1].x + bounds.left,this.points[1].y + bounds.top);
213
- }
214
-
215
- if(this.style.border.right) {
216
- this.context.moveTo(this.points[1].x + bounds.left,this.points[1].y + bounds.top);
217
- this.context.lineTo(this.points[2].x + bounds.left,this.points[2].y + bounds.top);
218
- }
219
-
220
- if(this.style.border.bottom) {
221
- this.context.moveTo(this.points[2].x + bounds.left,this.points[2].y + bounds.top);
222
- this.context.lineTo(this.points[3].x + bounds.left,this.points[3].y + bounds.top);
223
- }
224
-
225
- if(this.style.border.left) {
226
- this.context.moveTo(this.points[3].x + bounds.left,this.points[3].y + bounds.top);
227
- this.context.lineTo(this.points[0].x + bounds.left,this.points[0].y + bounds.top);
228
- }
229
- }
230
- else {
231
- const points = [];
232
- if(this.style.border.top) {
233
- points.push(this.points[0]);
234
- points.push(this.points[1]);
235
- }
236
-
237
- if(this.style.border.right) {
238
- points.push({
239
- ...this.points[1],
240
- m: true
241
- });
242
- points.push(this.points[2]);
243
- }
244
-
245
- if(this.style.border.bottom) {
246
- points.push({
247
- ...this.points[2],
248
- m: true
249
- });
250
- points.push(this.points[3]);
251
- }
252
-
253
- if(this.style.border.left) {
254
- points.push({
255
- ...this.points[3],
256
- m: true
257
- });
258
- points.push(this.points[0]);
259
- }
260
- points.length && this.webglControl && this.webglControl.stroke(points);
261
- }
262
- }
263
314
  }
264
315
 
265
316
  endDraw() {