jmgraph 3.2.22 → 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.
@@ -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 {
@@ -204,6 +205,112 @@ class WebglPath extends WebglBase {
204
205
  equalPoint(p1, p2) {
205
206
  return p1.x === p2.x && p1.y === p2.y;
206
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
+ }
207
314
  // 把path坐标集合转为线段集
208
315
  pathToLines(points) {
209
316
  let start = null;
@@ -476,9 +583,46 @@ class WebglPath extends WebglBase {
476
583
  }
477
584
  if(points && points.length) {
478
585
  const regular = lineWidth <= 1.2;
479
- points = regular? points : this.pathToPoints(points);
480
- const buffer = this.writePoints(points);
481
- 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
+ }
482
626
  // buffer 由 endDraw 统一清理
483
627
  }
484
628
  colorBuffer && this.disableVertexAttribArray(colorBuffer && colorBuffer.attr);
@@ -500,10 +644,9 @@ class WebglPath extends WebglBase {
500
644
 
501
645
  fillColor(color, points, bounds, type=1) {
502
646
 
503
- // 如果是渐变色,则需要计算偏移量的颜色
647
+ // 如果是渐变色,使用 GLSL 着色器直接计算
504
648
  if(this.isGradient(color)) {
505
- const imgData = color.toImageData(this, bounds, points);
506
- return this.fillImage(imgData.data, imgData.points, bounds);
649
+ return this.fillGradient(color, points, bounds);
507
650
  }
508
651
 
509
652
  // 标注为fill
@@ -516,6 +659,83 @@ class WebglPath extends WebglBase {
516
659
 
517
660
  }
518
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
+
519
739
  // 区域填充图片
520
740
  // points绘制的图形顶点
521
741
  // 图片整体绘制区域
@@ -581,6 +801,29 @@ class WebglPath extends WebglBase {
581
801
 
582
802
  // 规则图形(凸多边形,如圆):直接用 TRIANGLE_FAN 一次性绘制,无需 earcut
583
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
+ }
584
827
  const buffer = this.writePoints(points);
585
828
  const coordBuffer = isTexture? this.writePoints(points, this.program.attrs.a_text_coord): null;
586
829
  this.context.drawArrays(this.context.TRIANGLE_FAN, 0, points.length);
@@ -648,46 +891,73 @@ class WebglPath extends WebglBase {
648
891
  }
649
892
 
650
893
  drawText(text, x, y, bounds) {
651
- 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;
652
900
  if(!canvas) {
653
- return null;
901
+ canvas = document.createElement('canvas');
902
+ this.__textCanvas = canvas;
654
903
  }
655
904
  canvas.width = bounds.width;
656
905
  canvas.height = bounds.height;
657
906
 
658
- if(!canvas.width || !canvas.height) {
659
- return null;
660
- }
907
+ const ctx = canvas.getContext('2d', { willReadFrequently: true });
908
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
661
909
 
662
- this.textureContext.clearRect(0, 0, canvas.width, canvas.height);
663
910
  // 修改字体
664
- 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);
665
912
 
666
913
  x -= bounds.left;
667
914
  y -= bounds.top;
668
915
 
669
- this.setTextureStyle(this.style);
670
-
671
- 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
+ }
672
941
 
942
+ if(this.style.fillStyle && ctx.fillText) {
673
943
  if(this.style.maxWidth) {
674
- this.textureContext.fillText(text, x, y, this.style.maxWidth);
944
+ ctx.fillText(text, x, y, this.style.maxWidth);
675
945
  }
676
946
  else {
677
- this.textureContext.fillText(text, x, y);
947
+ ctx.fillText(text, x, y);
678
948
  }
679
949
  }
680
- if(this.textureContext.strokeText) {
681
-
950
+ if(this.style.strokeStyle && ctx.strokeText) {
682
951
  if(this.style.maxWidth) {
683
- this.textureContext.strokeText(text, x, y, this.style.maxWidth);
952
+ ctx.strokeText(text, x, y, this.style.maxWidth);
684
953
  }
685
954
  else {
686
- this.textureContext.strokeText(text, x, y);
955
+ ctx.strokeText(text, x, y);
687
956
  }
688
957
  }
958
+
689
959
  // 用纹理图片代替文字
690
- const data = this.textureContext.getImageData(0, 0, canvas.width, canvas.height);
960
+ const data = ctx.getImageData(0, 0, canvas.width, canvas.height);
691
961
  this.fillImage(data, this.points, bounds);
692
962
  }
693
963
  }
@@ -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
  }