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.
- package/README.md +1 -0
- package/dist/jmgraph.core.min.js +1 -1
- package/dist/jmgraph.core.min.js.map +1 -1
- package/dist/jmgraph.js +631 -290
- package/dist/jmgraph.min.js +1 -1
- package/index.js +5 -1
- package/package.json +1 -1
- package/src/core/jmGradient.js +4 -2
- package/src/core/jmGraph.js +0 -2
- package/src/lib/webgl/base.js +109 -90
- package/src/lib/webgl/gradient.js +99 -142
- package/src/lib/webgl/index.js +9 -10
- package/src/lib/webgl/path.js +294 -24
- package/src/shapes/jmHArc.js +2 -2
package/src/lib/webgl/path.js
CHANGED
|
@@ -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
|
-
|
|
480
|
-
const
|
|
481
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
659
|
-
|
|
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
|
-
|
|
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
|
-
|
|
670
|
-
|
|
671
|
-
|
|
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
|
-
|
|
944
|
+
ctx.fillText(text, x, y, this.style.maxWidth);
|
|
675
945
|
}
|
|
676
946
|
else {
|
|
677
|
-
|
|
947
|
+
ctx.fillText(text, x, y);
|
|
678
948
|
}
|
|
679
949
|
}
|
|
680
|
-
if(this.
|
|
681
|
-
|
|
950
|
+
if(this.style.strokeStyle && ctx.strokeText) {
|
|
682
951
|
if(this.style.maxWidth) {
|
|
683
|
-
|
|
952
|
+
ctx.strokeText(text, x, y, this.style.maxWidth);
|
|
684
953
|
}
|
|
685
954
|
else {
|
|
686
|
-
|
|
955
|
+
ctx.strokeText(text, x, y);
|
|
687
956
|
}
|
|
688
957
|
}
|
|
958
|
+
|
|
689
959
|
// 用纹理图片代替文字
|
|
690
|
-
const data =
|
|
960
|
+
const data = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
691
961
|
this.fillImage(data, this.points, bounds);
|
|
692
962
|
}
|
|
693
963
|
}
|
package/src/shapes/jmHArc.js
CHANGED
|
@@ -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
|
}
|