jmgraph 3.2.21 → 3.2.24

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.
@@ -105,23 +105,22 @@ class webgl {
105
105
  // 由具体的绘制方法处理
106
106
  }
107
107
 
108
- // 测量文本宽度(复用纹理 canvas 的 context)
108
+ // 测量文本宽度
109
109
  measureText(text) {
110
- const ctx = this.base.textureContext;
111
- if(ctx && ctx.measureText) return ctx.measureText(text);
112
- const canvas = document.createElement('canvas');
113
- const ctx2 = canvas.getContext('2d');
114
- return ctx2.measureText(text);
110
+ if(this.base && this.base._measureCtx) {
111
+ return this.base._measureCtx.measureText(text);
112
+ }
113
+ return { width: 15 };
115
114
  }
116
115
 
117
116
  // 创建线性渐变
118
- createLinearGradient(x1, y1, x2, y2) {
119
- return this.base.createLinearGradient(x1, y1, x2, y2);
117
+ createLinearGradient(x1, y1, x2, y2, bounds) {
118
+ return this.base.createLinearGradient(x1, y1, x2, y2, bounds);
120
119
  }
121
120
 
122
121
  // 创建径向渐变
123
- createRadialGradient(x1, y1, r1, x2, y2, r2) {
124
- return this.base.createRadialGradient(x1, y1, r1, x2, y2, r2);
122
+ createRadialGradient(x1, y1, r1, x2, y2, r2, bounds) {
123
+ return this.base.createRadialGradient(x1, y1, r1, x2, y2, r2, bounds);
125
124
  }
126
125
 
127
126
  // 绘制图像
@@ -1,4 +1,5 @@
1
- import WebglBase from './base.js';
1
+ import WebglBase, { MAX_STOPS } from './base.js';
2
+ import earcut from '../earcut.js';
2
3
 
3
4
  // path 绘制类
4
5
  class WebglPath extends WebglBase {
@@ -55,8 +56,14 @@ class WebglPath extends WebglBase {
55
56
  //this.useProgram();
56
57
 
57
58
  if(parentBounds) this.parentAbsoluteBounds = parentBounds;
58
- // 写入当前canvas大小
59
- this.context.uniform2f(this.program.uniforms.a_center_point.location, this.graph.width / 2, this.graph.height / 2);
59
+ // 缓存中心点值,只在变化时才更新 uniform
60
+ const cx = this.graph.width / 2;
61
+ const cy = this.graph.height / 2;
62
+ if(this.__lastCenterX !== cx || this.__lastCenterY !== cy) {
63
+ this.context.uniform2f(this.program.uniforms.a_center_point.location, cx, cy);
64
+ this.__lastCenterX = cx;
65
+ this.__lastCenterY = cy;
66
+ }
60
67
  }
61
68
 
62
69
  setFragColor(color) {
@@ -198,6 +205,112 @@ class WebglPath extends WebglBase {
198
205
  equalPoint(p1, p2) {
199
206
  return p1.x === p2.x && p1.y === p2.y;
200
207
  }
208
+
209
+ // 将带 moveTo 标记的点集拆分为外轮廓和多个洞
210
+ splitSubPaths(points) {
211
+ const subPaths = [];
212
+ let current = [];
213
+ for(let i = 0; i < points.length; i++) {
214
+ const p = points[i];
215
+ if(p.m && current.length > 0) {
216
+ subPaths.push(current);
217
+ current = [];
218
+ }
219
+ current.push(p);
220
+ }
221
+ if(current.length > 0) subPaths.push(current);
222
+
223
+ // 面积最大的作为外轮廓,其余作为洞
224
+ let maxArea = -1;
225
+ let outerIdx = 0;
226
+ for(let i = 0; i < subPaths.length; i++) {
227
+ const area = Math.abs(this.polygonArea(subPaths[i]));
228
+ if(area > maxArea) {
229
+ maxArea = area;
230
+ outerIdx = i;
231
+ }
232
+ }
233
+
234
+ const outerPoints = subPaths[outerIdx];
235
+ const holes = [];
236
+ for(let i = 0; i < subPaths.length; i++) {
237
+ if(i !== outerIdx) holes.push(subPaths[i]);
238
+ }
239
+ return { outerPoints, holes };
240
+ }
241
+
242
+ // 计算多边形面积(Shoelace 公式)
243
+ polygonArea(points) {
244
+ let area = 0;
245
+ const n = points.length;
246
+ for(let i = 0; i < n; i++) {
247
+ const j = (i + 1) % n;
248
+ area += points[i].x * points[j].y;
249
+ area -= points[j].x * points[i].y;
250
+ }
251
+ return area / 2;
252
+ }
253
+
254
+ // 使用 earcut 带 holes 填充多边形
255
+ fillWithHoles(outerPoints, holes, isTexture = false) {
256
+ // 将所有点合并:外轮廓 + 各个洞,并记录洞的起始索引
257
+ const allPoints = [...outerPoints];
258
+ const holeIndices = [];
259
+ for(const hole of holes) {
260
+ holeIndices.push(allPoints.length);
261
+ allPoints.push(...hole);
262
+ }
263
+
264
+ const dim = 2;
265
+ const vertexData = [];
266
+ for(const p of allPoints) {
267
+ vertexData.push(p.x, p.y);
268
+ }
269
+
270
+ // 用 earcut 进行带洞三角化
271
+ const indices = earcut(vertexData, holeIndices, dim);
272
+
273
+ if(!indices || indices.length < 3) return;
274
+
275
+ // 构建 GPU 顶点数据
276
+ const allVertices = [];
277
+ const allTexCoords = [];
278
+ for(let i = 0; i < indices.length; i++) {
279
+ const p = allPoints[indices[i]];
280
+ allVertices.push(p.x, p.y);
281
+ if(isTexture) allTexCoords.push(p.x, p.y);
282
+ }
283
+
284
+ const gl = this.context;
285
+ const vertexArr = new Float32Array(allVertices);
286
+
287
+ let posBuffer = this.__cachedBuffers.find(b => b.attr === this.program.attrs.a_position);
288
+ if(!posBuffer) {
289
+ posBuffer = this.createFloat32Buffer(vertexArr, gl.ARRAY_BUFFER, gl.DYNAMIC_DRAW);
290
+ posBuffer.attr = this.program.attrs.a_position;
291
+ this.__cachedBuffers.push(posBuffer);
292
+ } else {
293
+ gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer.buffer);
294
+ gl.bufferData(gl.ARRAY_BUFFER, vertexArr, gl.DYNAMIC_DRAW);
295
+ }
296
+ this.writeVertexAttrib(posBuffer, this.program.attrs.a_position, 2, 0, 0);
297
+
298
+ if(isTexture && allTexCoords.length) {
299
+ const texData = new Float32Array(allTexCoords);
300
+ let texBuffer = this.__cachedBuffers.find(b => b.attr === this.program.attrs.a_text_coord);
301
+ if(!texBuffer) {
302
+ texBuffer = this.createFloat32Buffer(texData, gl.ARRAY_BUFFER, gl.DYNAMIC_DRAW);
303
+ texBuffer.attr = this.program.attrs.a_text_coord;
304
+ this.__cachedBuffers.push(texBuffer);
305
+ } else {
306
+ gl.bindBuffer(gl.ARRAY_BUFFER, texBuffer.buffer);
307
+ gl.bufferData(gl.ARRAY_BUFFER, texData, gl.DYNAMIC_DRAW);
308
+ }
309
+ this.writeVertexAttrib(texBuffer, this.program.attrs.a_text_coord, 2, 0, 0);
310
+ }
311
+
312
+ gl.drawArrays(gl.TRIANGLES, 0, allVertices.length / 2);
313
+ }
201
314
  // 把path坐标集合转为线段集
202
315
  pathToLines(points) {
203
316
  let start = null;
@@ -470,9 +583,46 @@ class WebglPath extends WebglBase {
470
583
  }
471
584
  if(points && points.length) {
472
585
  const regular = lineWidth <= 1.2;
473
- points = regular? points : this.pathToPoints(points);
474
- const buffer = this.writePoints(points);
475
- this.context.drawArrays(regular? this.context.LINE_LOOP: this.context.POINTS, 0, points.length);
586
+ const hasMoveTo = points.some && points.some(p => p.m);
587
+ const isRing = !hasMoveTo && this.needCut; // 空心形状(jmHArc close=true 时无 m 标记)
588
+ if(regular && (hasMoveTo || isRing)) {
589
+ // 有 moveTo 标记或空心形状时,分段绘制每个子路径的 LINE_LOOP
590
+ // 避免 LINE_LOOP 把不同子路径的点连起来产生拉扯线
591
+ if(hasMoveTo) {
592
+ let subPath = [];
593
+ for(let i = 0; i < points.length; i++) {
594
+ if(points[i].m && subPath.length > 0) {
595
+ const buffer = this.writePoints(subPath);
596
+ this.context.drawArrays(this.context.LINE_LOOP, 0, subPath.length);
597
+ subPath = [];
598
+ }
599
+ subPath.push(points[i]);
600
+ }
601
+ if(subPath.length > 1) {
602
+ const buffer = this.writePoints(subPath);
603
+ this.context.drawArrays(this.context.LINE_LOOP, 0, subPath.length);
604
+ }
605
+ }
606
+ else if(isRing) {
607
+ // 空心形状:前半段为内弧,后半段为外弧(反向),各自 LINE_LOOP
608
+ const mid = Math.floor(points.length / 2);
609
+ const inner = points.slice(0, mid);
610
+ const outer = points.slice(mid);
611
+ if(inner.length > 1) {
612
+ this.writePoints(inner);
613
+ this.context.drawArrays(this.context.LINE_LOOP, 0, inner.length);
614
+ }
615
+ if(outer.length > 1) {
616
+ this.writePoints(outer);
617
+ this.context.drawArrays(this.context.LINE_LOOP, 0, outer.length);
618
+ }
619
+ }
620
+ }
621
+ else {
622
+ points = regular? points : this.pathToPoints(points);
623
+ const buffer = this.writePoints(points);
624
+ this.context.drawArrays(regular? this.context.LINE_LOOP: this.context.POINTS, 0, points.length);
625
+ }
476
626
  // buffer 由 endDraw 统一清理
477
627
  }
478
628
  colorBuffer && this.disableVertexAttribArray(colorBuffer && colorBuffer.attr);
@@ -494,10 +644,9 @@ class WebglPath extends WebglBase {
494
644
 
495
645
  fillColor(color, points, bounds, type=1) {
496
646
 
497
- // 如果是渐变色,则需要计算偏移量的颜色
647
+ // 如果是渐变色,使用 GLSL 着色器直接计算
498
648
  if(this.isGradient(color)) {
499
- const imgData = color.toImageData(this, bounds, points);
500
- return this.fillImage(imgData.data, imgData.points, bounds);
649
+ return this.fillGradient(color, points, bounds);
501
650
  }
502
651
 
503
652
  // 标注为fill
@@ -510,6 +659,83 @@ class WebglPath extends WebglBase {
510
659
 
511
660
  }
512
661
 
662
+ /**
663
+ * 使用 GLSL 着色器渲染渐变填充
664
+ * 无需 textureCanvas,直接通过 uniform 传递渐变参数给 GPU
665
+ */
666
+ fillGradient(gradient, points, bounds) {
667
+ const params = gradient.toUniformParams();
668
+ if(!params) return;
669
+
670
+ // 标注为 GLSL 渐变 (type=5)
671
+ this.context.uniform1i(this.program.uniforms.a_type.location, 5);
672
+
673
+ // 设置 globalAlpha(通过 v_single_color.a 传递给着色器)
674
+ this.context.uniform4f(this.program.uniforms.v_single_color.location, 1.0, 1.0, 1.0, this.style.globalAlpha);
675
+
676
+ // 设置渐变类型
677
+ if(this.program.uniforms.u_gradient_type) {
678
+ this.context.uniform1i(this.program.uniforms.u_gradient_type.location, params.gradientType);
679
+ }
680
+
681
+ // 设置渐变起点/终点
682
+ if(this.program.uniforms.u_gradient_start) {
683
+ this.context.uniform4fv(this.program.uniforms.u_gradient_start.location, params.gradientStart);
684
+ }
685
+ if(this.program.uniforms.u_gradient_end) {
686
+ this.context.uniform4fv(this.program.uniforms.u_gradient_end.location, params.gradientEnd);
687
+ }
688
+
689
+ // 设置颜色断点数量
690
+ if(this.program.uniforms.u_gradient_stop_count) {
691
+ this.context.uniform1i(this.program.uniforms.u_gradient_stop_count.location, params.stopCount);
692
+ }
693
+
694
+ // 设置每个 stop 的 offset
695
+ // 关键:必须填充完整的 MAX_STOPS 长度数组,否则未初始化元素默认为 0
696
+ // 会导致着色器循环中 t >= 0 始终为 true,返回黑色
697
+ if(this.program.uniforms.u_gradient_offsets) {
698
+ const offsets = new Float32Array(MAX_STOPS);
699
+ for(let i = 0; i < params.stopCount; i++) {
700
+ offsets[i] = params.stops[i * 5];
701
+ }
702
+ // 用 2.0 填充剩余项,使 t(0~1) >= 2.0 为 false,不会被匹配
703
+ for(let i = params.stopCount; i < MAX_STOPS; i++) {
704
+ offsets[i] = 2.0;
705
+ }
706
+ this.context.uniform1fv(this.program.uniforms.u_gradient_offsets.location, offsets);
707
+ }
708
+
709
+ // 设置每个 stop 的颜色 (rgba)
710
+ if(this.program.uniforms.u_gradient_colors) {
711
+ const colors = new Float32Array(MAX_STOPS * 4);
712
+ for(let i = 0; i < params.stopCount; i++) {
713
+ colors[i * 4 + 0] = params.stops[i * 5 + 1]; // r
714
+ colors[i * 4 + 1] = params.stops[i * 5 + 2]; // g
715
+ colors[i * 4 + 2] = params.stops[i * 5 + 3]; // b
716
+ colors[i * 4 + 3] = params.stops[i * 5 + 4]; // a
717
+ }
718
+ // 用最后一个 stop 的颜色填充剩余项,确保不会返回黑色
719
+ if(params.stopCount > 0) {
720
+ const lastR = params.stops[(params.stopCount - 1) * 5 + 1];
721
+ const lastG = params.stops[(params.stopCount - 1) * 5 + 2];
722
+ const lastB = params.stops[(params.stopCount - 1) * 5 + 3];
723
+ const lastA = params.stops[(params.stopCount - 1) * 5 + 4];
724
+ for(let i = params.stopCount; i < MAX_STOPS; i++) {
725
+ colors[i * 4 + 0] = lastR;
726
+ colors[i * 4 + 1] = lastG;
727
+ colors[i * 4 + 2] = lastB;
728
+ colors[i * 4 + 3] = lastA;
729
+ }
730
+ }
731
+ this.context.uniform4fv(this.program.uniforms.u_gradient_colors.location, colors);
732
+ }
733
+
734
+ // 填充多边形(需要纹理坐标来计算渐变位置)
735
+ this.fillPolygons(points, true);
736
+ this.disableVertexAttribArray(this.program.attrs.a_text_coord);
737
+ }
738
+
513
739
  // 区域填充图片
514
740
  // points绘制的图形顶点
515
741
  // 图片整体绘制区域
@@ -575,6 +801,29 @@ class WebglPath extends WebglBase {
575
801
 
576
802
  // 规则图形(凸多边形,如圆):直接用 TRIANGLE_FAN 一次性绘制,无需 earcut
577
803
  if(this.isRegular) {
804
+ // 检查是否有 moveTo 标记,如果有说明路径包含多个子路径(如空心圆弧 jmHArc)
805
+ const hasMoveTo = points.some && points.some(p => p.m);
806
+ if(hasMoveTo) {
807
+ // 有 m 标记:按 m 标记拆分子路径
808
+ const { outerPoints, holes } = this.splitSubPaths(points);
809
+ this.fillWithHoles(outerPoints, holes, isTexture);
810
+ return;
811
+ }
812
+ // 无 m 标记但 needCut=true 表示空心形状(如 jmHArc close=true)
813
+ // 前半段为内弧,后半段为外弧(反向),按中点拆分
814
+ if(this.needCut && points.length >= 6) {
815
+ const mid = Math.floor(points.length / 2);
816
+ const inner = points.slice(0, mid);
817
+ const outer = points.slice(mid);
818
+ const innerArea = Math.abs(this.polygonArea(inner));
819
+ const outerArea = Math.abs(this.polygonArea(outer));
820
+ if(outerArea >= innerArea) {
821
+ this.fillWithHoles(outer, [inner], isTexture);
822
+ } else {
823
+ this.fillWithHoles(inner, [outer], isTexture);
824
+ }
825
+ return;
826
+ }
578
827
  const buffer = this.writePoints(points);
579
828
  const coordBuffer = isTexture? this.writePoints(points, this.program.attrs.a_text_coord): null;
580
829
  this.context.drawArrays(this.context.TRIANGLE_FAN, 0, points.length);
@@ -642,46 +891,73 @@ class WebglPath extends WebglBase {
642
891
  }
643
892
 
644
893
  drawText(text, x, y, bounds) {
645
- let canvas = this.textureCanvas;
894
+ // 文本渲染仍需要 2D canvas 绘制字形,然后作为纹理上传
895
+ // 使用临时 canvas,不依赖共享的 textureCanvas
896
+ if(!bounds.width || !bounds.height) return null;
897
+ if(typeof document === 'undefined') return null;
898
+
899
+ let canvas = this.__textCanvas;
646
900
  if(!canvas) {
647
- return null;
901
+ canvas = document.createElement('canvas');
902
+ this.__textCanvas = canvas;
648
903
  }
649
904
  canvas.width = bounds.width;
650
905
  canvas.height = bounds.height;
651
906
 
652
- if(!canvas.width || !canvas.height) {
653
- return null;
654
- }
907
+ const ctx = canvas.getContext('2d', { willReadFrequently: true });
908
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
655
909
 
656
- this.textureContext.clearRect(0, 0, canvas.width, canvas.height);
657
910
  // 修改字体
658
- this.textureContext.font = this.style.font || (this.style.fontSize + 'px ' + this.style.fontFamily);
911
+ ctx.font = this.style.font || (this.style.fontSize + 'px ' + this.style.fontFamily);
659
912
 
660
913
  x -= bounds.left;
661
914
  y -= bounds.top;
662
915
 
663
- this.setTextureStyle(this.style);
664
-
665
- if(this.style.fillStyle && this.textureContext.fillText) {
916
+ // 设置文本样式
917
+ if(this.style.fillStyle) {
918
+ ctx.fillStyle = this.graph.utils.toColor(this.style.fillStyle);
919
+ }
920
+ if(this.style.strokeStyle) {
921
+ ctx.strokeStyle = this.graph.utils.toColor(this.style.strokeStyle);
922
+ }
923
+ if(this.style.shadowColor) {
924
+ ctx.shadowColor = this.graph.utils.toColor(this.style.shadowColor);
925
+ }
926
+ if(this.style.shadowBlur) {
927
+ ctx.shadowBlur = this.style.shadowBlur;
928
+ }
929
+ if(this.style.shadowOffsetX !== undefined) {
930
+ ctx.shadowOffsetX = this.style.shadowOffsetX;
931
+ }
932
+ if(this.style.shadowOffsetY !== undefined) {
933
+ ctx.shadowOffsetY = this.style.shadowOffsetY;
934
+ }
935
+ if(this.style.textAlign) {
936
+ ctx.textAlign = this.style.textAlign;
937
+ }
938
+ if(this.style.textBaseline) {
939
+ ctx.textBaseline = this.style.textBaseline;
940
+ }
666
941
 
942
+ if(this.style.fillStyle && ctx.fillText) {
667
943
  if(this.style.maxWidth) {
668
- this.textureContext.fillText(text, x, y, this.style.maxWidth);
944
+ ctx.fillText(text, x, y, this.style.maxWidth);
669
945
  }
670
946
  else {
671
- this.textureContext.fillText(text, x, y);
947
+ ctx.fillText(text, x, y);
672
948
  }
673
949
  }
674
- if(this.textureContext.strokeText) {
675
-
950
+ if(this.style.strokeStyle && ctx.strokeText) {
676
951
  if(this.style.maxWidth) {
677
- this.textureContext.strokeText(text, x, y, this.style.maxWidth);
952
+ ctx.strokeText(text, x, y, this.style.maxWidth);
678
953
  }
679
954
  else {
680
- this.textureContext.strokeText(text, x, y);
955
+ ctx.strokeText(text, x, y);
681
956
  }
682
957
  }
958
+
683
959
  // 用纹理图片代替文字
684
- const data = this.textureContext.getImageData(0, 0, canvas.width, canvas.height);
960
+ const data = ctx.getImageData(0, 0, canvas.width, canvas.height);
685
961
  this.fillImage(data, this.points, bounds);
686
962
  }
687
963
  }
@@ -123,7 +123,6 @@ export default class jmArc extends jmPath {
123
123
  if((mw == 0 && mh == 0) || start == end) return;
124
124
 
125
125
  let anticlockwise = this.anticlockwise;
126
- this.points = [];
127
126
  let step = 1 / Math.max(mw, mh);
128
127
 
129
128
  //如果是逆时针绘制,则角度为负数,并且结束角为2Math.PI-end
@@ -134,18 +133,33 @@ export default class jmArc extends jmPath {
134
133
  }
135
134
  if(start > end) step = -step;
136
135
 
137
- if(this.isFan) this.points.push(location.center);// 如果是扇形,则从中心开始画
136
+ // 预计算需要的点数量
137
+ let pointCount = Math.ceil(Math.abs(end - start) / Math.abs(step)) + 1;
138
+ if(this.isFan) pointCount++;
139
+
140
+ // 复用已有数组,避免每帧分配;大小变化时才重建
141
+ if(!this.points || this.points.length !== pointCount) {
142
+ this.points = new Array(pointCount);
143
+ for(let i = 0; i < pointCount; i++) {
144
+ this.points[i] = { x: 0, y: 0 };
145
+ }
146
+ }
147
+
148
+ let idx = 0;
149
+ if(this.isFan) {
150
+ this.points[idx].x = location.center.x;
151
+ this.points[idx].y = location.center.y;
152
+ idx++;
153
+ }
138
154
 
139
155
  //椭圆方程x=a*cos(r) ,y=b*sin(r)
140
156
  for(let r=start;;r += step) {
141
157
  if(step > 0 && r > end) r = end;
142
158
  else if(step < 0 && r < end) r = end;
143
159
 
144
- const p = {
145
- x : Math.cos(r) * mw + cx,
146
- y : Math.sin(r) * mh + cy
147
- };
148
- this.points.push(p);
160
+ this.points[idx].x = Math.cos(r) * mw + cx;
161
+ this.points[idx].y = Math.sin(r) * mh + cy;
162
+ idx++;
149
163
 
150
164
  if(r == end) break;
151
165
  }
@@ -103,8 +103,8 @@ export default class jmHArc extends jmArc {
103
103
 
104
104
  maxps.reverse();//大圆逆序
105
105
  if(!this.style || !this.style.close) {
106
- maxps[0].m = true;//开始画大圆时表示为移动
107
- }
106
+ maxps[0].m = true;//非闭合时标记 moveTo,分隔内外两个子路径
107
+ }
108
108
  this.points = minps.concat(maxps);
109
109
  }
110
110
  }