jmgraph 3.2.25 → 3.2.27
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 +188 -0
- package/dist/jmgraph.core.min.js +1 -1
- package/dist/jmgraph.core.min.js.map +1 -1
- package/dist/jmgraph.js +769 -63
- package/dist/jmgraph.min.js +1 -1
- package/index.d.ts +142 -1
- package/package.json +1 -1
- package/src/core/jmGradient.js +595 -63
- package/src/core/jmUtils.js +38 -2
package/src/core/jmGradient.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {jmUtils} from "./jmUtils.js";
|
|
1
|
+
import {jmUtils, colorKeywords} from "./jmUtils.js";
|
|
2
2
|
import {jmList} from "./jmList.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -66,28 +66,65 @@ export default class jmGradient {
|
|
|
66
66
|
d = location.radius * 2;
|
|
67
67
|
}
|
|
68
68
|
if(!d) {
|
|
69
|
-
d = Math.min(location.width,location.height);
|
|
69
|
+
d = Math.min(location.width || 0, location.height || 0);
|
|
70
70
|
}
|
|
71
|
+
if(d <= 0) {
|
|
72
|
+
d = Math.max(bounds.width || 0, bounds.height || 0, 100);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const width = bounds.width || d;
|
|
76
|
+
const height = bounds.height || d;
|
|
71
77
|
|
|
72
|
-
//let offsetLine = 1;//渐变长度或半径
|
|
73
|
-
//处理百分比参数
|
|
74
78
|
if(jmUtils.checkPercent(x1)) {
|
|
75
|
-
x1 = jmUtils.percentToNumber(x1) *
|
|
79
|
+
x1 = jmUtils.percentToNumber(x1) * width;
|
|
80
|
+
}
|
|
81
|
+
else if(typeof x1 === 'number' && x1 >= 0 && x1 <= 1) {
|
|
82
|
+
x1 = x1 * width;
|
|
83
|
+
}
|
|
84
|
+
else if(typeof x1 === 'string') {
|
|
85
|
+
const num = parseFloat(x1);
|
|
86
|
+
if(!isNaN(num)) x1 = num;
|
|
76
87
|
}
|
|
77
88
|
if(jmUtils.checkPercent(x2)) {
|
|
78
|
-
x2 = jmUtils.percentToNumber(x2) *
|
|
89
|
+
x2 = jmUtils.percentToNumber(x2) * width;
|
|
90
|
+
}
|
|
91
|
+
else if(typeof x2 === 'number' && x2 >= 0 && x2 <= 1) {
|
|
92
|
+
x2 = x2 * width;
|
|
93
|
+
}
|
|
94
|
+
else if(typeof x2 === 'string') {
|
|
95
|
+
const num = parseFloat(x2);
|
|
96
|
+
if(!isNaN(num)) x2 = num;
|
|
79
97
|
}
|
|
80
98
|
if(jmUtils.checkPercent(y1)) {
|
|
81
|
-
y1 = jmUtils.percentToNumber(y1) *
|
|
99
|
+
y1 = jmUtils.percentToNumber(y1) * height;
|
|
100
|
+
}
|
|
101
|
+
else if(typeof y1 === 'number' && y1 >= 0 && y1 <= 1) {
|
|
102
|
+
y1 = y1 * height;
|
|
103
|
+
}
|
|
104
|
+
else if(typeof y1 === 'string') {
|
|
105
|
+
const num = parseFloat(y1);
|
|
106
|
+
if(!isNaN(num)) y1 = num;
|
|
82
107
|
}
|
|
83
108
|
if(jmUtils.checkPercent(y2)) {
|
|
84
|
-
y2 = jmUtils.percentToNumber(y2) *
|
|
85
|
-
}
|
|
109
|
+
y2 = jmUtils.percentToNumber(y2) * height;
|
|
110
|
+
}
|
|
111
|
+
else if(typeof y2 === 'number' && y2 >= 0 && y2 <= 1) {
|
|
112
|
+
y2 = y2 * height;
|
|
113
|
+
}
|
|
114
|
+
else if(typeof y2 === 'string') {
|
|
115
|
+
const num = parseFloat(y2);
|
|
116
|
+
if(!isNaN(num)) y2 = num;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
x1 = Number(x1) || 0;
|
|
120
|
+
y1 = Number(y1) || 0;
|
|
121
|
+
x2 = Number(x2) || 0;
|
|
122
|
+
y2 = Number(y2) || 0;
|
|
86
123
|
|
|
87
|
-
let sx1 =
|
|
88
|
-
let sy1 =
|
|
89
|
-
let sx2 =
|
|
90
|
-
let sy2 =
|
|
124
|
+
let sx1 = x1 + (bounds.left || 0);
|
|
125
|
+
let sy1 = y1 + (bounds.top || 0);
|
|
126
|
+
let sx2 = x2 + (bounds.left || 0);
|
|
127
|
+
let sy2 = y2 + (bounds.top || 0);
|
|
91
128
|
if(this.type === 'linear') {
|
|
92
129
|
if(control.mode === 'webgl' && control.webglControl) {
|
|
93
130
|
// WebGL 着色器中 v_text_coord 是绝对坐标,需要传递绝对坐标
|
|
@@ -101,24 +138,47 @@ export default class jmGradient {
|
|
|
101
138
|
else if(this.type === 'radial') {
|
|
102
139
|
let r1 = this.r1||0;
|
|
103
140
|
let r2 = this.r2;
|
|
141
|
+
|
|
142
|
+
if(d <= 0) {
|
|
143
|
+
d = Math.max(bounds.width || 0, bounds.height || 0, 1);
|
|
144
|
+
}
|
|
145
|
+
|
|
104
146
|
if(jmUtils.checkPercent(r1)) {
|
|
105
147
|
r1 = jmUtils.percentToNumber(r1);
|
|
106
148
|
r1 = d * r1;
|
|
107
149
|
}
|
|
150
|
+
else if(typeof r1 === 'number' && r1 >= 0 && r1 <= 1) {
|
|
151
|
+
r1 = r1 * d;
|
|
152
|
+
}
|
|
153
|
+
else if(typeof r1 === 'string') {
|
|
154
|
+
r1 = parseFloat(r1) || 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
108
157
|
if(jmUtils.checkPercent(r2)) {
|
|
109
158
|
r2 = jmUtils.percentToNumber(r2);
|
|
110
159
|
r2 = d * r2;
|
|
111
160
|
}
|
|
161
|
+
else if(typeof r2 === 'number' && r2 >= 0 && r2 <= 1) {
|
|
162
|
+
r2 = r2 * d;
|
|
163
|
+
}
|
|
164
|
+
else if(typeof r2 === 'string') {
|
|
165
|
+
r2 = parseFloat(r2);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if(r2 === undefined || r2 === null || isNaN(Number(r2)) || Number(r2) <= 0) {
|
|
169
|
+
r2 = d / 2;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
r1 = Number(r1) || 0;
|
|
173
|
+
r2 = Number(r2) || d / 2;
|
|
174
|
+
|
|
112
175
|
if(control.mode === 'webgl' && control.webglControl) {
|
|
113
|
-
// WebGL 着色器中 v_text_coord 是绝对坐标,需要传递绝对坐标
|
|
114
176
|
gradient = control.webglControl.createRadialGradient(sx1, sy1, r1, sx2, sy2, r2, bounds);
|
|
115
177
|
gradient.key = this.toString();
|
|
116
178
|
}
|
|
117
|
-
//offsetLine = Math.abs(r2 - r1);//二圆半径差
|
|
118
179
|
else if(context.createRadialGradient) {
|
|
119
180
|
gradient = context.createRadialGradient(sx1, sy1, r1, sx2, sy2, r2);
|
|
120
181
|
}
|
|
121
|
-
//小程序的接口特殊
|
|
122
182
|
else if(context.createCircularGradient) {
|
|
123
183
|
gradient = context.createCircularGradient(sx1, sy1, r2);
|
|
124
184
|
}
|
|
@@ -141,67 +201,539 @@ export default class jmGradient {
|
|
|
141
201
|
}
|
|
142
202
|
|
|
143
203
|
/**
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* linear-gradient
|
|
147
|
-
*
|
|
204
|
+
* 解析渐变字符串
|
|
205
|
+
* 支持的格式:
|
|
206
|
+
* - linear-gradient(180deg, #8b5cf6 0%, #6366f1 50%, #4f46e5 100%)
|
|
207
|
+
* - linear-gradient(to top, red, blue)
|
|
208
|
+
* - linear-gradient(to right bottom, red, blue)
|
|
209
|
+
* - linear-gradient(45deg, red, blue)
|
|
210
|
+
* - linear-gradient(0.5turn, red, blue)
|
|
211
|
+
* - radial-gradient(circle, red, blue)
|
|
212
|
+
* - radial-gradient(ellipse at top, red, blue)
|
|
148
213
|
*
|
|
149
214
|
* @method fromString
|
|
150
215
|
* @for jmGradient
|
|
151
|
-
* @
|
|
216
|
+
* @param {string} s 渐变字符串
|
|
152
217
|
*/
|
|
153
218
|
fromString(s) {
|
|
154
|
-
if(!s)
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
219
|
+
if(!s) {
|
|
220
|
+
console.warn('jmGradient: 渐变字符串为空');
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
// 使用 [\s\S] 匹配任意字符(包括换行符),支持多行渐变字符串
|
|
224
|
+
const gradientMatch = s.match(/(linear|radial)-gradient\s*\(\s*([\s\S]+)\)/i);
|
|
225
|
+
if(!gradientMatch || gradientMatch.length < 3) {
|
|
226
|
+
console.warn('jmGradient: 无效的渐变字符串格式: "' + s + '"');
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const type = gradientMatch[1].toLowerCase();
|
|
231
|
+
if(type !== 'linear' && type !== 'radial') {
|
|
232
|
+
console.warn('jmGradient: 不支持的渐变类型 "' + type + '",仅支持 linear 和 radial');
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
this.type = type;
|
|
237
|
+
const content = jmUtils.trim(gradientMatch[2]);
|
|
238
|
+
|
|
239
|
+
const splitIndex = this._findSplitIndex(content);
|
|
240
|
+
if(splitIndex < 0) {
|
|
241
|
+
console.warn('jmGradient: 无法解析渐变内容: "' + content + '"');
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const params = content.substring(0, splitIndex).trim();
|
|
246
|
+
const colorPart = content.substring(splitIndex + 1).trim();
|
|
247
|
+
|
|
248
|
+
if(!colorPart) {
|
|
249
|
+
console.warn('jmGradient: 未找到颜色停止点');
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if(this.type === 'linear') {
|
|
254
|
+
this._parseLinearParams(params);
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
this._parseRadialParams(params);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const colorCount = this._parseColorStops(colorPart);
|
|
261
|
+
if(colorCount === 0) {
|
|
262
|
+
console.warn('jmGradient: 未找到有效的颜色停止点: "' + colorPart + '"');
|
|
263
|
+
}
|
|
264
|
+
else if(colorCount < 2) {
|
|
265
|
+
console.warn('jmGradient: 颜色停止点至少需要2个,当前只有 ' + colorCount + ' 个');
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* 找到参数和颜色的分割位置(第一个不在括号内的逗号)
|
|
271
|
+
* @param {string} content 内容字符串
|
|
272
|
+
* @returns {number} 分割位置索引
|
|
273
|
+
*/
|
|
274
|
+
_findSplitIndex(content) {
|
|
275
|
+
let depth = 0;
|
|
276
|
+
|
|
277
|
+
for(let i = 0; i < content.length; i++) {
|
|
278
|
+
const char = content[i];
|
|
279
|
+
if(char === '(') {
|
|
280
|
+
depth++;
|
|
165
281
|
}
|
|
166
|
-
else {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
282
|
+
else if(char === ')') {
|
|
283
|
+
depth--;
|
|
284
|
+
}
|
|
285
|
+
else if(char === ',' && depth === 0) {
|
|
286
|
+
return i;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return -1;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* 验证渐变配置是否有效
|
|
295
|
+
* @returns {boolean} 是否有效
|
|
296
|
+
*/
|
|
297
|
+
isValid() {
|
|
298
|
+
if(!this.type) return false;
|
|
299
|
+
if(this.type === 'linear') {
|
|
300
|
+
return typeof this.x1 !== 'undefined' ||
|
|
301
|
+
typeof this.x2 !== 'undefined' ||
|
|
302
|
+
typeof this._angle !== 'undefined';
|
|
303
|
+
}
|
|
304
|
+
if(this.type === 'radial') {
|
|
305
|
+
return this.stops && this.stops.length >= 2;
|
|
306
|
+
}
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* 解析线性渐变参数
|
|
312
|
+
* @param {string} params 参数字符串
|
|
313
|
+
*/
|
|
314
|
+
_parseLinearParams(params) {
|
|
315
|
+
const trimmed = jmUtils.trim(params);
|
|
316
|
+
|
|
317
|
+
if(trimmed.startsWith('to ')) {
|
|
318
|
+
const direction = trimmed.substring(3).toLowerCase().trim();
|
|
319
|
+
const dir = this._directionToAngle(direction);
|
|
320
|
+
this._angle = dir.angle;
|
|
321
|
+
this.x1 = dir.x1;
|
|
322
|
+
this.y1 = dir.y1;
|
|
323
|
+
this.x2 = dir.x2;
|
|
324
|
+
this.y2 = dir.y2;
|
|
325
|
+
}
|
|
326
|
+
else if(this._hasAngleUnit(trimmed)) {
|
|
327
|
+
const angle = this._parseAngle(trimmed);
|
|
328
|
+
this._angle = angle;
|
|
329
|
+
const coords = this._angleToCoords(angle);
|
|
330
|
+
this.x1 = coords.x1;
|
|
331
|
+
this.y1 = coords.y1;
|
|
332
|
+
this.x2 = coords.x2;
|
|
333
|
+
this.y2 = coords.y2;
|
|
334
|
+
}
|
|
335
|
+
else if(trimmed.startsWith('at ')) {
|
|
336
|
+
const radialMatch = trimmed.match(/at\s+(.+)/i);
|
|
337
|
+
if(radialMatch) {
|
|
338
|
+
this._parseRadialParams(trimmed);
|
|
171
339
|
}
|
|
172
340
|
}
|
|
173
|
-
//径向渐变
|
|
174
341
|
else {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
this.
|
|
178
|
-
this.
|
|
342
|
+
const parts = trimmed.split(/\s+/);
|
|
343
|
+
if(parts.length >= 4) {
|
|
344
|
+
this.x1 = parts[0];
|
|
345
|
+
this.y1 = parts[1];
|
|
346
|
+
this.x2 = parts[2];
|
|
347
|
+
this.y2 = parts[3];
|
|
179
348
|
}
|
|
180
|
-
else {
|
|
181
|
-
this.x1 =
|
|
182
|
-
this.y1 =
|
|
183
|
-
this.
|
|
184
|
-
this.
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
349
|
+
else if(parts.length === 2) {
|
|
350
|
+
this.x1 = 0;
|
|
351
|
+
this.y1 = 0;
|
|
352
|
+
this.x2 = parts[0];
|
|
353
|
+
this.y2 = parts[1];
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* 解析径向渐变参数
|
|
360
|
+
* @param {string} params 参数字符串
|
|
361
|
+
*/
|
|
362
|
+
_parseRadialParams(params) {
|
|
363
|
+
const trimmed = jmUtils.trim(params);
|
|
364
|
+
|
|
365
|
+
this.shape = 'ellipse';
|
|
366
|
+
this.position = { x: '50%', y: '50%' };
|
|
367
|
+
|
|
368
|
+
const atMatch = trimmed.match(/^(.+?)\s+at\s+(.+)$/i);
|
|
369
|
+
if(atMatch) {
|
|
370
|
+
const shapePart = jmUtils.trim(atMatch[1]);
|
|
371
|
+
const posPart = jmUtils.trim(atMatch[2]);
|
|
372
|
+
this._parseRadialShape(shapePart);
|
|
373
|
+
this._parseRadialPosition(posPart);
|
|
374
|
+
}
|
|
375
|
+
else if(trimmed.startsWith('circle') || trimmed.startsWith('ellipse')) {
|
|
376
|
+
this._parseRadialShape(trimmed);
|
|
377
|
+
this.x1 = '50%';
|
|
378
|
+
this.y1 = '50%';
|
|
379
|
+
this.x2 = '50%';
|
|
380
|
+
this.y2 = '50%';
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
const parts = trimmed.split(/\s+/);
|
|
384
|
+
if(parts.length >= 3) {
|
|
385
|
+
this.x1 = parts[0];
|
|
386
|
+
this.y1 = parts[1];
|
|
387
|
+
this.r1 = parts[2];
|
|
388
|
+
}
|
|
389
|
+
if(parts.length >= 6) {
|
|
390
|
+
this.x2 = parts[3];
|
|
391
|
+
this.y2 = parts[4];
|
|
392
|
+
this.r2 = parts[5];
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if(this.x1 === undefined && this.y1 === undefined) {
|
|
397
|
+
this.x1 = '50%';
|
|
398
|
+
this.y1 = '50%';
|
|
399
|
+
}
|
|
400
|
+
if(this.x2 === undefined && this.y2 === undefined) {
|
|
401
|
+
this.x2 = '50%';
|
|
402
|
+
this.y2 = '50%';
|
|
403
|
+
}
|
|
404
|
+
if(this.r2 === undefined) {
|
|
405
|
+
this.r2 = '50%';
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* 解析径向渐变形状
|
|
411
|
+
* @param {string} shapePart 形状描述
|
|
412
|
+
*/
|
|
413
|
+
_parseRadialShape(shapePart) {
|
|
414
|
+
if(shapePart.startsWith('circle')) {
|
|
415
|
+
this.shape = 'circle';
|
|
416
|
+
const sizeMatch = shapePart.match(/circle\s*\(\s*([^)]+)\s*\)/i);
|
|
417
|
+
if(sizeMatch) {
|
|
418
|
+
this.r2 = jmUtils.trim(sizeMatch[1]);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
else if(shapePart.startsWith('ellipse')) {
|
|
422
|
+
this.shape = 'ellipse';
|
|
423
|
+
const sizeMatch = shapePart.match(/ellipse\s*\(\s*([^)]+)\s*\)/i);
|
|
424
|
+
if(sizeMatch) {
|
|
425
|
+
const sizes = jmUtils.trim(sizeMatch[1]).split(/\s+/);
|
|
426
|
+
if(sizes.length >= 2) {
|
|
427
|
+
this.rx = sizes[0];
|
|
428
|
+
this.ry = sizes[1];
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* 解析径向渐变位置
|
|
436
|
+
* @param {string} posPart 位置描述
|
|
437
|
+
*/
|
|
438
|
+
_parseRadialPosition(posPart) {
|
|
439
|
+
const parts = posPart.split(/\s+/);
|
|
440
|
+
if(parts.length >= 2) {
|
|
441
|
+
this.x1 = parts[0];
|
|
442
|
+
this.y1 = parts[1];
|
|
443
|
+
this.x2 = parts[0];
|
|
444
|
+
this.y2 = parts[1];
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* 解析颜色停止点
|
|
450
|
+
* @param {string} colorPart 颜色部分字符串
|
|
451
|
+
* @returns {number} 成功解析的颜色数量
|
|
452
|
+
*/
|
|
453
|
+
_parseColorStops(colorPart) {
|
|
454
|
+
if(!colorPart) {
|
|
455
|
+
return 0;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const stops = this._splitColorStops(colorPart);
|
|
459
|
+
let lastOffset = -1;
|
|
460
|
+
let colorCount = 0;
|
|
461
|
+
|
|
462
|
+
for(let i = 0; i < stops.length; i++) {
|
|
463
|
+
const stop = jmUtils.trim(stops[i]);
|
|
464
|
+
if(!stop) continue;
|
|
465
|
+
|
|
466
|
+
const parsed = this._parseSingleColorStop(stop);
|
|
467
|
+
if(!parsed) {
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
let { color, offset } = parsed;
|
|
472
|
+
|
|
473
|
+
if(color === 'transparent') {
|
|
474
|
+
color = 'rgba(0,0,0,0)';
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if(!this._isValidColor(color)) {
|
|
478
|
+
console.warn('jmGradient: 无效的颜色格式 "' + color + '"');
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const normalizedOffset = this._normalizeOffset(offset);
|
|
483
|
+
let finalOffset = normalizedOffset;
|
|
484
|
+
|
|
485
|
+
if(finalOffset === null) {
|
|
486
|
+
if(i === 0) {
|
|
487
|
+
finalOffset = 0;
|
|
488
|
+
}
|
|
489
|
+
else if(i === stops.length - 1) {
|
|
490
|
+
finalOffset = 1;
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
const nextOffset = this._findNextOffset(stops, i);
|
|
494
|
+
if(nextOffset !== null) {
|
|
495
|
+
finalOffset = (lastOffset + nextOffset) / 2;
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
finalOffset = Math.min(1, lastOffset + (1 - lastOffset) / (stops.length - i));
|
|
201
499
|
}
|
|
202
500
|
}
|
|
203
501
|
}
|
|
502
|
+
|
|
503
|
+
if(finalOffset !== null) {
|
|
504
|
+
if(finalOffset < 0 || finalOffset > 1) {
|
|
505
|
+
console.warn('jmGradient: 颜色偏移量 ' + finalOffset + ' 超出有效范围 [0, 1],将调整为有效范围');
|
|
506
|
+
finalOffset = Math.max(0, Math.min(1, finalOffset));
|
|
507
|
+
}
|
|
508
|
+
lastOffset = finalOffset;
|
|
509
|
+
this.addStop(finalOffset, color);
|
|
510
|
+
colorCount++;
|
|
511
|
+
}
|
|
204
512
|
}
|
|
513
|
+
|
|
514
|
+
return colorCount;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* 分割颜色停止点字符串
|
|
519
|
+
* @param {string} colorPart 颜色部分字符串
|
|
520
|
+
* @returns {string[]} 颜色停止点数组
|
|
521
|
+
*/
|
|
522
|
+
_splitColorStops(colorPart) {
|
|
523
|
+
const stops = [];
|
|
524
|
+
let depth = 0;
|
|
525
|
+
let current = '';
|
|
526
|
+
|
|
527
|
+
for(let i = 0; i < colorPart.length; i++) {
|
|
528
|
+
const char = colorPart[i];
|
|
529
|
+
if(char === '(') {
|
|
530
|
+
depth++;
|
|
531
|
+
current += char;
|
|
532
|
+
}
|
|
533
|
+
else if(char === ')') {
|
|
534
|
+
depth--;
|
|
535
|
+
current += char;
|
|
536
|
+
}
|
|
537
|
+
else if(char === ',' && depth === 0) {
|
|
538
|
+
stops.push(current.trim());
|
|
539
|
+
current = '';
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
current += char;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if(current.trim()) {
|
|
547
|
+
stops.push(current.trim());
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return stops;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* 解析单个颜色停止点
|
|
555
|
+
* @param {string} stop 单个颜色停止点字符串
|
|
556
|
+
* @returns {object|null} {color, offset} 或 null
|
|
557
|
+
*/
|
|
558
|
+
_parseSingleColorStop(stop) {
|
|
559
|
+
const hexMatch = stop.match(/^(#[a-fA-F0-9]{3,8})\s*(\d+(?:\.\d+)?%?)?$/i);
|
|
560
|
+
if(hexMatch) {
|
|
561
|
+
return { color: hexMatch[1], offset: hexMatch[2] || null };
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const rgbaMatch = stop.match(/^(rgba?\s*\([^)]+\))\s*(\d+(?:\.\d+)?%?)?$/i);
|
|
565
|
+
if(rgbaMatch) {
|
|
566
|
+
return { color: rgbaMatch[1], offset: rgbaMatch[2] || null };
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const hslaMatch = stop.match(/^(hsla?\s*\([^)]+\))\s*(\d+(?:\.\d+)?%?)?$/i);
|
|
570
|
+
if(hslaMatch) {
|
|
571
|
+
return { color: hslaMatch[1], offset: hslaMatch[2] || null };
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const namedMatch = stop.match(/^([a-zA-Z]+)\s*(\d+(?:\.\d+)?%?)?$/i);
|
|
575
|
+
if(namedMatch && this._isValidColor(namedMatch[1])) {
|
|
576
|
+
return { color: namedMatch[1], offset: namedMatch[2] || null };
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* 查找下一个有偏移量的颜色停止点
|
|
584
|
+
* @param {string[]} stops 颜色停止点数组
|
|
585
|
+
* @param {number} currentIndex 当前索引
|
|
586
|
+
* @returns {number|null} 下一个偏移量或null
|
|
587
|
+
*/
|
|
588
|
+
_findNextOffset(stops, currentIndex) {
|
|
589
|
+
for(let i = currentIndex + 1; i < stops.length; i++) {
|
|
590
|
+
const parsed = this._parseSingleColorStop(jmUtils.trim(stops[i]));
|
|
591
|
+
if(parsed && parsed.offset) {
|
|
592
|
+
return this._normalizeOffset(parsed.offset);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* 验证颜色格式是否有效
|
|
600
|
+
* @param {string} color 颜色字符串
|
|
601
|
+
* @returns {boolean} 是否有效
|
|
602
|
+
*/
|
|
603
|
+
_isValidColor(color) {
|
|
604
|
+
if(!color) return false;
|
|
605
|
+
|
|
606
|
+
const hexPattern = /^#([a-fA-F0-9]{3,8})$/;
|
|
607
|
+
if(hexPattern.test(color)) return true;
|
|
608
|
+
|
|
609
|
+
// 支持 rgba(r,g,b,a) 和 rgba(r, g, b, a) 等各种空格格式
|
|
610
|
+
const rgbPattern = /^rgba?\s*\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*(,\s*[\d.]+\s*)?\)$/i;
|
|
611
|
+
if(rgbPattern.test(color)) return true;
|
|
612
|
+
|
|
613
|
+
const hslPattern = /^hsla?\s*\(\s*\d{1,3}\s*,\s*\d{1,3}%?\s*,\s*\d{1,3}%?\s*(,\s*[\d.]+\s*)?\)$/i;
|
|
614
|
+
if(hslPattern.test(color)) return true;
|
|
615
|
+
|
|
616
|
+
// 使用 jmUtils 中的完整 CSS 颜色关键字表
|
|
617
|
+
if(colorKeywords && colorKeywords[color.toLowerCase()]) return true;
|
|
618
|
+
|
|
619
|
+
// 宽松处理:符合 CSS 关键字命名规则的字符串也视为有效颜色
|
|
620
|
+
// (纯字母,可能在运行时被浏览器或其他环境解析)
|
|
621
|
+
if(/^[a-zA-Z]+$/.test(color)) return true;
|
|
622
|
+
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* 标准化偏移值
|
|
628
|
+
* @param {string} offset 偏移字符串
|
|
629
|
+
* @returns {number|null} 0-1之间的数值或null
|
|
630
|
+
*/
|
|
631
|
+
_normalizeOffset(offset) {
|
|
632
|
+
if(!offset) return null;
|
|
633
|
+
offset = jmUtils.trim(offset);
|
|
634
|
+
if(offset.endsWith('%')) {
|
|
635
|
+
return parseFloat(offset) / 100;
|
|
636
|
+
}
|
|
637
|
+
const num = parseFloat(offset);
|
|
638
|
+
if(isNaN(num)) return null;
|
|
639
|
+
if(num > 1) {
|
|
640
|
+
return num / 100;
|
|
641
|
+
}
|
|
642
|
+
return num;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* 检查字符串是否包含角度单位
|
|
647
|
+
* @param {string} str 待检查字符串
|
|
648
|
+
* @returns {boolean}
|
|
649
|
+
*/
|
|
650
|
+
_hasAngleUnit(str) {
|
|
651
|
+
return /^-?\d+(\.\d+)?\s*(deg|rad|grad|turn)$/i.test(str);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* 解析角度值
|
|
656
|
+
* @param {string} angleStr 角度字符串
|
|
657
|
+
* @returns {number} 弧度值
|
|
658
|
+
*/
|
|
659
|
+
_parseAngle(angleStr) {
|
|
660
|
+
angleStr = jmUtils.trim(angleStr);
|
|
661
|
+
const match = angleStr.match(/^(-?\d+(\.\d+)?)\s*(deg|rad|grad|turn)?$/i);
|
|
662
|
+
if(!match) return 0;
|
|
663
|
+
|
|
664
|
+
let value = parseFloat(match[1]);
|
|
665
|
+
const unit = (match[3] || 'deg').toLowerCase();
|
|
666
|
+
|
|
667
|
+
switch(unit) {
|
|
668
|
+
case 'deg':
|
|
669
|
+
return value * Math.PI / 180;
|
|
670
|
+
case 'rad':
|
|
671
|
+
return value;
|
|
672
|
+
case 'grad':
|
|
673
|
+
return value * Math.PI / 200;
|
|
674
|
+
case 'turn':
|
|
675
|
+
return value * 2 * Math.PI;
|
|
676
|
+
default:
|
|
677
|
+
return value * Math.PI / 180;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* 将角度转换为起点和终点坐标
|
|
683
|
+
* @param {number} angle 弧度值
|
|
684
|
+
* @returns {object} 坐标对象
|
|
685
|
+
*/
|
|
686
|
+
_angleToCoords(angle) {
|
|
687
|
+
const x = Math.cos(angle);
|
|
688
|
+
const y = -Math.sin(angle);
|
|
689
|
+
|
|
690
|
+
return {
|
|
691
|
+
x1: Math.round((0.5 - x * 0.5) * 1000) / 1000,
|
|
692
|
+
y1: Math.round((0.5 + y * 0.5) * 1000) / 1000,
|
|
693
|
+
x2: Math.round((0.5 + x * 0.5) * 1000) / 1000,
|
|
694
|
+
y2: Math.round((0.5 - y * 0.5) * 1000) / 1000
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* 将方向关键词转换为角度和坐标
|
|
700
|
+
* @param {string} direction 方向描述
|
|
701
|
+
* @returns {object} 包含angle和坐标的对象
|
|
702
|
+
*/
|
|
703
|
+
_directionToAngle(direction) {
|
|
704
|
+
const directions = {
|
|
705
|
+
'to top': { angle: 0, x1: '50%', y1: '100%', x2: '50%', y2: '0%' },
|
|
706
|
+
'to bottom': { angle: Math.PI, x1: '50%', y1: '0%', x2: '50%', y2: '100%' },
|
|
707
|
+
'to left': { angle: -Math.PI / 2, x1: '100%', y1: '50%', x2: '0%', y2: '50%' },
|
|
708
|
+
'to right': { angle: Math.PI / 2, x1: '0%', y1: '50%', x2: '100%', y2: '50%' },
|
|
709
|
+
'to top left': { angle: -Math.PI * 3 / 4, x1: '100%', y1: '100%', x2: '0%', y2: '0%' },
|
|
710
|
+
'to top right': { angle: -Math.PI / 4, x1: '0%', y1: '100%', x2: '100%', y2: '0%' },
|
|
711
|
+
'to bottom left': { angle: Math.PI * 3 / 4, x1: '100%', y1: '0%', x2: '0%', y2: '100%' },
|
|
712
|
+
'to bottom right': { angle: Math.PI / 4, x1: '0%', y1: '0%', x2: '100%', y2: '100%' },
|
|
713
|
+
'top': { angle: 0, x1: '50%', y1: '100%', x2: '50%', y2: '0%' },
|
|
714
|
+
'bottom': { angle: Math.PI, x1: '50%', y1: '0%', x2: '50%', y2: '100%' },
|
|
715
|
+
'left': { angle: -Math.PI / 2, x1: '100%', y1: '50%', x2: '0%', y2: '50%' },
|
|
716
|
+
'right': { angle: Math.PI / 2, x1: '0%', y1: '50%', x2: '100%', y2: '50%' }
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
const dir = directions[direction];
|
|
720
|
+
if(dir) {
|
|
721
|
+
return dir;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const keywordMatch = direction.match(/to\s+(top|bottom|left|right)/i);
|
|
725
|
+
if(keywordMatch) {
|
|
726
|
+
const mainDir = keywordMatch[1].toLowerCase();
|
|
727
|
+
const secDir = direction.replace(keywordMatch[0], '').trim();
|
|
728
|
+
if(secDir) {
|
|
729
|
+
const combined = `to ${mainDir} ${secDir}`;
|
|
730
|
+
if(directions[combined]) {
|
|
731
|
+
return directions[combined];
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
return { angle: 0, x1: '50%', y1: '100%', x2: '50%', y2: '0%' };
|
|
205
737
|
}
|
|
206
738
|
|
|
207
739
|
/**
|