@ngroznykh/papirus 0.3.5 → 0.3.7
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/LICENSE +14 -16
- package/LICENSE.ru.md +21 -0
- package/README.md +17 -2
- package/README.ru.md +17 -2
- package/dist/elements/TextLabel.d.ts +1 -1
- package/dist/elements/TextLabel.d.ts.map +1 -1
- package/dist/papirus.js +241 -7
- package/dist/papirus.js.map +1 -1
- package/dist/utils/SvgExporter.d.ts +12 -0
- package/dist/utils/SvgExporter.d.ts.map +1 -1
- package/package.json +6 -6
package/LICENSE
CHANGED
|
@@ -1,21 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
Papirus Dual License
|
|
2
2
|
|
|
3
3
|
Copyright (c) 2026 Papirus Contributors
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
5
|
+
This project is dual-licensed:
|
|
11
6
|
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
1) GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later)
|
|
8
|
+
You may use, modify, and distribute this software under AGPL terms.
|
|
9
|
+
If you modify and run this software (including over a network), you must
|
|
10
|
+
provide complete corresponding source code under AGPL.
|
|
14
11
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
12
|
+
2) Commercial License
|
|
13
|
+
For proprietary, closed-source, or commercial use cases that are not
|
|
14
|
+
compatible with AGPL obligations, you must obtain a commercial license.
|
|
15
|
+
See LICENSE_COMMERCIAL.md for terms and contact details.
|
|
16
|
+
|
|
17
|
+
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
18
|
+
|
|
19
|
+
Full AGPL text: https://www.gnu.org/licenses/agpl-3.0.txt
|
package/LICENSE.ru.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Лицензирование Papirus (перевод)
|
|
2
|
+
|
|
3
|
+
Этот документ является русскоязычным переводом и пояснением файла `LICENSE`.
|
|
4
|
+
|
|
5
|
+
В случае расхождений приоритет имеет оригинальный текст `LICENSE` (английская версия).
|
|
6
|
+
|
|
7
|
+
## Dual License
|
|
8
|
+
|
|
9
|
+
Проект распространяется по двойной лицензии:
|
|
10
|
+
|
|
11
|
+
1. **GNU Affero General Public License v3.0 или новее (AGPL-3.0-or-later)**
|
|
12
|
+
- Вы можете использовать, изменять и распространять ПО по условиям AGPL.
|
|
13
|
+
- Если вы модифицируете ПО и запускаете его (включая сетевой доступ), вы обязаны предоставить полный исходный код соответствующей версии по AGPL.
|
|
14
|
+
|
|
15
|
+
2. **Коммерческая лицензия**
|
|
16
|
+
- Для проприетарного/закрытого или иного коммерческого использования, несовместимого с обязанностями AGPL, требуется коммерческая лицензия.
|
|
17
|
+
- См. `LICENSE_COMMERCIAL.md`.
|
|
18
|
+
|
|
19
|
+
SPDX: `AGPL-3.0-or-later`
|
|
20
|
+
|
|
21
|
+
Полный текст AGPL: https://www.gnu.org/licenses/agpl-3.0.txt
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Papirus
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@ngroznykh/papirus)
|
|
4
|
-
[](LICENSE)
|
|
5
5
|
|
|
6
6
|
TypeScript library for building interactive 2D diagrams and flowcharts on HTML Canvas. Supports nodes, edges, groups, styling, serialization, export, and interactivity.
|
|
7
7
|
|
|
@@ -65,6 +65,7 @@ renderer.enableInteractions();
|
|
|
65
65
|
- [Utils](./docs/utils.md)
|
|
66
66
|
- [Changelog](./CHANGELOG.md)
|
|
67
67
|
- [Security Policy](./SECURITY.md)
|
|
68
|
+
- [Open Source Preparation](./docs/OPEN_SOURCE_PREPARATION.md)
|
|
68
69
|
|
|
69
70
|
## Features
|
|
70
71
|
|
|
@@ -193,6 +194,20 @@ npm run test:coverage # Tests with coverage
|
|
|
193
194
|
|
|
194
195
|
Please read [CONTRIBUTING.md](./CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests.
|
|
195
196
|
|
|
197
|
+
Key governance and contribution files:
|
|
198
|
+
|
|
199
|
+
- [CONTRIBUTING.md](./CONTRIBUTING.md) / [CONTRIBUTING.ru.md](./CONTRIBUTING.ru.md)
|
|
200
|
+
- [SECURITY.md](./SECURITY.md) / [SECURITY.ru.md](./SECURITY.ru.md)
|
|
201
|
+
- [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md) / [CODE_OF_CONDUCT.ru.md](./CODE_OF_CONDUCT.ru.md)
|
|
202
|
+
|
|
196
203
|
## License
|
|
197
204
|
|
|
198
|
-
This project
|
|
205
|
+
This project uses dual licensing:
|
|
206
|
+
|
|
207
|
+
- `AGPL-3.0-or-later` for open-source usage
|
|
208
|
+
- Commercial license for proprietary/closed-source commercial usage
|
|
209
|
+
|
|
210
|
+
See:
|
|
211
|
+
|
|
212
|
+
- [LICENSE](./LICENSE)
|
|
213
|
+
- [LICENSE_COMMERCIAL.md](./LICENSE_COMMERCIAL.md)
|
package/README.ru.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Papirus
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@ngroznykh/papirus)
|
|
4
|
-
[](LICENSE)
|
|
5
5
|
|
|
6
6
|
Papirus — библиотека на TypeScript для построения интерактивных 2D‑схем на HTML Canvas. Поддерживает узлы, связи, группы, стили, сериализацию, экспорт и интерактивность.
|
|
7
7
|
|
|
@@ -65,6 +65,7 @@ renderer.enableInteractions();
|
|
|
65
65
|
- [Утилиты](./docs/utils.md)
|
|
66
66
|
- [Changelog](./CHANGELOG.md)
|
|
67
67
|
- [Политика безопасности](./SECURITY.md)
|
|
68
|
+
- [Чеклист подготовки к Open Source](./docs/OPEN_SOURCE_PREPARATION.ru.md)
|
|
68
69
|
|
|
69
70
|
## Возможности
|
|
70
71
|
|
|
@@ -194,6 +195,20 @@ npm run test:coverage # Тесты с покрытием
|
|
|
194
195
|
|
|
195
196
|
См. [CONTRIBUTING.md](./CONTRIBUTING.md).
|
|
196
197
|
|
|
198
|
+
Ключевые файлы для open-source процесса:
|
|
199
|
+
|
|
200
|
+
- [CONTRIBUTING.md](./CONTRIBUTING.md) / [CONTRIBUTING.ru.md](./CONTRIBUTING.ru.md)
|
|
201
|
+
- [SECURITY.md](./SECURITY.md) / [SECURITY.ru.md](./SECURITY.ru.md)
|
|
202
|
+
- [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md) / [CODE_OF_CONDUCT.ru.md](./CODE_OF_CONDUCT.ru.md)
|
|
203
|
+
|
|
197
204
|
## Лицензия
|
|
198
205
|
|
|
199
|
-
|
|
206
|
+
Проект использует dual licensing:
|
|
207
|
+
|
|
208
|
+
- `AGPL-3.0-or-later` для open-source использования
|
|
209
|
+
- Коммерческая лицензия для проприетарного/закрытого коммерческого использования
|
|
210
|
+
|
|
211
|
+
См.:
|
|
212
|
+
|
|
213
|
+
- [LICENSE](./LICENSE) / [LICENSE.ru.md](./LICENSE.ru.md)
|
|
214
|
+
- [LICENSE_COMMERCIAL.md](./LICENSE_COMMERCIAL.md) / [LICENSE_COMMERCIAL.ru.md](./LICENSE_COMMERCIAL.ru.md)
|
|
@@ -89,7 +89,7 @@ export declare class TextLabel {
|
|
|
89
89
|
/**
|
|
90
90
|
* Render the label within bounds
|
|
91
91
|
*/
|
|
92
|
-
render(ctx: CanvasRenderingContext2D, bounds: Bounds): void;
|
|
92
|
+
render(ctx: CanvasRenderingContext2D, bounds: Bounds, alignOverride?: 'left' | 'center' | 'right'): void;
|
|
93
93
|
/**
|
|
94
94
|
* Render at a specific point (for edge labels)
|
|
95
95
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TextLabel.d.ts","sourceRoot":"","sources":["../../src/elements/TextLabel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAG1D,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;CACvB;AAaD;;GAEG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,aAAa,CAAC,CAAS;IAC/B,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,WAAW,CAAY;IAC/B,OAAO,CAAC,SAAS,CAAC,CAAS;IAC3B,OAAO,CAAC,aAAa,CAAC,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,WAAW,CAAC,CAAS;IAC7B,OAAO,CAAC,SAAS,CAAC,CAAa;IAC/B,OAAO,CAAC,aAAa,CAAQ;gBAEjB,OAAO,EAAE,gBAAgB;IAYrC;;OAEG;IACH,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,IAAI,IAAI,CAAC,KAAK,EAAE,MAAM,EAOrB;IAED;;;OAGG;IACH,IAAI,YAAY,IAAI,MAAM,GAAG,SAAS,CAErC;IAED,IAAI,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAKzC;IAED;;OAEG;IACH,IAAI,KAAK,IAAI,SAAS,CAErB;IAED,IAAI,KAAK,CAAC,KAAK,EAAE,SAAS,EAMzB;IAED;;OAEG;IACH,IAAI,UAAU,IAAI,MAAM,GAAG,SAAS,CAEnC;IAED,IAAI,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAOvC;IAED;;OAEG;IACH,IAAI,QAAQ,IAAI,MAAM,GAAG,SAAS,CAEjC;IAED,IAAI,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAQrC;IAED;;OAEG;IACH,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAShD;;OAEG;IACH,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,IAAI,OAAO,CAAC,KAAK,EAAE,MAAM,EAQxB;IAED;;OAEG;IACH,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,IAAI,MAAM,CAAC,KAAK,EAAE,MAAM,EAQvB;IAED,iBAAiB,CAAC,YAAY,EAAE,YAAY,GAAG,IAAI;IAWnD,WAAW,CAAC,OAAO,CAAC,EAAE,MAAM,IAAI,GAAG,IAAI;IAIvC;;OAEG;IACH,IAAI,aAAa,IAAI,MAAM,CAE1B;IAED;;OAEG;IACH,IAAI,cAAc,IAAI,MAAM,CAE3B;IAED;;OAEG;IACH,OAAO,CAAC,GAAG,EAAE,wBAAwB,GAAG;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE;IAsCzE;;OAEG;IACH,MAAM,CAAC,GAAG,EAAE,wBAAwB,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;
|
|
1
|
+
{"version":3,"file":"TextLabel.d.ts","sourceRoot":"","sources":["../../src/elements/TextLabel.ts"],"names":[],"mappings":"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"}
|
package/dist/papirus.js
CHANGED
|
@@ -2081,11 +2081,15 @@ class TextLabel {
|
|
|
2081
2081
|
/**
|
|
2082
2082
|
* Render the label within bounds
|
|
2083
2083
|
*/
|
|
2084
|
-
render(ctx, bounds) {
|
|
2084
|
+
render(ctx, bounds, alignOverride) {
|
|
2085
2085
|
if (this._lines.length === 0) {
|
|
2086
2086
|
this.measure(ctx);
|
|
2087
2087
|
}
|
|
2088
2088
|
this.applyStyle(ctx);
|
|
2089
|
+
const align = alignOverride ?? this._style.align ?? "center";
|
|
2090
|
+
if (alignOverride) {
|
|
2091
|
+
ctx.textAlign = align;
|
|
2092
|
+
}
|
|
2089
2093
|
const lineHeight = (this._style.fontSize ?? 14) * 1.2;
|
|
2090
2094
|
const totalHeight = this._lines.length * lineHeight;
|
|
2091
2095
|
const margin = this._margin;
|
|
@@ -2096,7 +2100,7 @@ class TextLabel {
|
|
|
2096
2100
|
height: Math.max(0, bounds.height - margin * 2)
|
|
2097
2101
|
};
|
|
2098
2102
|
let x;
|
|
2099
|
-
switch (
|
|
2103
|
+
switch (align) {
|
|
2100
2104
|
case "left":
|
|
2101
2105
|
x = innerBounds.x + this._padding;
|
|
2102
2106
|
break;
|
|
@@ -8673,11 +8677,15 @@ class SvgExporter {
|
|
|
8673
8677
|
const fill = style.fillColor ?? "#ffffff";
|
|
8674
8678
|
const stroke = style.strokeColor ?? "#333333";
|
|
8675
8679
|
const strokeWidth = style.strokeWidth ?? 2;
|
|
8676
|
-
const
|
|
8680
|
+
const baseOpacity = style.opacity ?? 1;
|
|
8681
|
+
const fillOpacity = (style.fillOpacity ?? 1) * baseOpacity;
|
|
8682
|
+
const strokeOpacity = (style.strokeOpacity ?? 1) * baseOpacity;
|
|
8683
|
+
const dash = style.lineDash?.length ? ` stroke-dasharray="${style.lineDash.join(" ")}"` : "";
|
|
8684
|
+
const dashOffset = style.lineDashOffset !== void 0 ? ` stroke-dashoffset="${style.lineDashOffset}"` : "";
|
|
8677
8685
|
let shape;
|
|
8678
8686
|
switch (node.typeName) {
|
|
8679
8687
|
case "rectangle": {
|
|
8680
|
-
const radius = (
|
|
8688
|
+
const radius = this.getNodeCornerRadius(node, bounds);
|
|
8681
8689
|
shape = `<rect x="${bounds.x}" y="${bounds.y}" width="${bounds.width}" height="${bounds.height}" rx="${radius}" ry="${radius}"`;
|
|
8682
8690
|
break;
|
|
8683
8691
|
}
|
|
@@ -8703,9 +8711,11 @@ class SvgExporter {
|
|
|
8703
8711
|
shape = `<rect x="${bounds.x}" y="${bounds.y}" width="${bounds.width}" height="${bounds.height}"`;
|
|
8704
8712
|
}
|
|
8705
8713
|
}
|
|
8706
|
-
const label = node.label ? this.renderTextLabel(node.label.text, node.
|
|
8714
|
+
const label = node.label ? this.renderTextLabel(node.label.text, node.getLabelPosition(), node.label.style) : "";
|
|
8715
|
+
const icon = this.renderNodeIcon(node, bounds);
|
|
8707
8716
|
return [
|
|
8708
|
-
`${shape} fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" opacity="${
|
|
8717
|
+
`${shape} fill="${fill}" fill-opacity="${fillOpacity}" stroke="${stroke}" stroke-width="${strokeWidth}" stroke-opacity="${strokeOpacity}"${dash}${dashOffset}/>`,
|
|
8718
|
+
icon,
|
|
8709
8719
|
label
|
|
8710
8720
|
].join("");
|
|
8711
8721
|
}
|
|
@@ -8892,12 +8902,236 @@ class SvgExporter {
|
|
|
8892
8902
|
const fontSize = style.fontSize ?? 14;
|
|
8893
8903
|
const fontFamily = style.fontFamily ?? "sans-serif";
|
|
8894
8904
|
const fontWeight = style.fontWeight ?? "normal";
|
|
8905
|
+
const opacity = style.opacity ?? 1;
|
|
8895
8906
|
const anchor = style.align === "left" ? "start" : style.align === "right" ? "end" : "middle";
|
|
8896
8907
|
const baseline = style.baseline === "top" ? "text-before-edge" : style.baseline === "bottom" ? "text-after-edge" : "middle";
|
|
8897
|
-
return `<text x="${point.x}" y="${point.y}" fill="${fill}" font-size="${fontSize}" font-family="${fontFamily}" font-weight="${fontWeight}" text-anchor="${anchor}" dominant-baseline="${baseline}">${this.escapeText(
|
|
8908
|
+
return `<text x="${point.x}" y="${point.y}" fill="${fill}" fill-opacity="${opacity}" font-size="${fontSize}" font-family="${fontFamily}" font-weight="${fontWeight}" text-anchor="${anchor}" dominant-baseline="${baseline}">${this.escapeText(
|
|
8898
8909
|
text
|
|
8899
8910
|
)}</text>`;
|
|
8900
8911
|
}
|
|
8912
|
+
getNodeCornerRadius(node, bounds) {
|
|
8913
|
+
const rectangleRadius = "cornerRadius" in node && typeof node.cornerRadius === "number" ? node.cornerRadius ?? 0 : node.style.cornerRadius ?? 0;
|
|
8914
|
+
return Math.max(0, Math.min(rectangleRadius, bounds.width / 2, bounds.height / 2));
|
|
8915
|
+
}
|
|
8916
|
+
renderNodeIcon(node, nodeBounds) {
|
|
8917
|
+
const icon = node.icon;
|
|
8918
|
+
if (!icon) {
|
|
8919
|
+
return "";
|
|
8920
|
+
}
|
|
8921
|
+
const opts = icon.options;
|
|
8922
|
+
const iconSize = icon.getSize();
|
|
8923
|
+
if (iconSize.width <= 0 || iconSize.height <= 0) {
|
|
8924
|
+
return "";
|
|
8925
|
+
}
|
|
8926
|
+
const iconBoxSize = this.getIconBoxSize(opts, iconSize);
|
|
8927
|
+
const iconBounds = this.getIconBounds(nodeBounds, iconBoxSize, opts.placement ?? "center");
|
|
8928
|
+
const drawRect = this.getIconDrawRect(iconBounds, opts, iconSize);
|
|
8929
|
+
if (drawRect.width <= 0 || drawRect.height <= 0) {
|
|
8930
|
+
return "";
|
|
8931
|
+
}
|
|
8932
|
+
const href = this.resolveIconHref(opts);
|
|
8933
|
+
if (!href) {
|
|
8934
|
+
return "";
|
|
8935
|
+
}
|
|
8936
|
+
const opacity = opts.opacity ?? 1;
|
|
8937
|
+
return `<image href="${this.escapeAttribute(href)}" x="${drawRect.x}" y="${drawRect.y}" width="${drawRect.width}" height="${drawRect.height}" opacity="${opacity}" preserveAspectRatio="none"/>`;
|
|
8938
|
+
}
|
|
8939
|
+
getIconBoxSize(opts, imageSize) {
|
|
8940
|
+
const padding = opts.padding ?? 8;
|
|
8941
|
+
const margin = Math.max(0, opts.margin ?? 0);
|
|
8942
|
+
return {
|
|
8943
|
+
width: imageSize.width + padding * 2 + margin * 2,
|
|
8944
|
+
height: imageSize.height + padding * 2 + margin * 2
|
|
8945
|
+
};
|
|
8946
|
+
}
|
|
8947
|
+
getIconBounds(bounds, iconBoxSize, placement) {
|
|
8948
|
+
switch (placement) {
|
|
8949
|
+
case "top":
|
|
8950
|
+
return { x: bounds.x, y: bounds.y, width: bounds.width, height: iconBoxSize.height };
|
|
8951
|
+
case "bottom":
|
|
8952
|
+
return {
|
|
8953
|
+
x: bounds.x,
|
|
8954
|
+
y: bounds.y + bounds.height - iconBoxSize.height,
|
|
8955
|
+
width: bounds.width,
|
|
8956
|
+
height: iconBoxSize.height
|
|
8957
|
+
};
|
|
8958
|
+
case "left":
|
|
8959
|
+
return { x: bounds.x, y: bounds.y, width: iconBoxSize.width, height: bounds.height };
|
|
8960
|
+
case "right":
|
|
8961
|
+
return {
|
|
8962
|
+
x: bounds.x + bounds.width - iconBoxSize.width,
|
|
8963
|
+
y: bounds.y,
|
|
8964
|
+
width: iconBoxSize.width,
|
|
8965
|
+
height: bounds.height
|
|
8966
|
+
};
|
|
8967
|
+
case "top-left":
|
|
8968
|
+
return { x: bounds.x, y: bounds.y, width: iconBoxSize.width, height: iconBoxSize.height };
|
|
8969
|
+
case "top-right":
|
|
8970
|
+
return {
|
|
8971
|
+
x: bounds.x + bounds.width - iconBoxSize.width,
|
|
8972
|
+
y: bounds.y,
|
|
8973
|
+
width: iconBoxSize.width,
|
|
8974
|
+
height: iconBoxSize.height
|
|
8975
|
+
};
|
|
8976
|
+
case "bottom-left":
|
|
8977
|
+
return {
|
|
8978
|
+
x: bounds.x,
|
|
8979
|
+
y: bounds.y + bounds.height - iconBoxSize.height,
|
|
8980
|
+
width: iconBoxSize.width,
|
|
8981
|
+
height: iconBoxSize.height
|
|
8982
|
+
};
|
|
8983
|
+
case "bottom-right":
|
|
8984
|
+
return {
|
|
8985
|
+
x: bounds.x + bounds.width - iconBoxSize.width,
|
|
8986
|
+
y: bounds.y + bounds.height - iconBoxSize.height,
|
|
8987
|
+
width: iconBoxSize.width,
|
|
8988
|
+
height: iconBoxSize.height
|
|
8989
|
+
};
|
|
8990
|
+
case "center":
|
|
8991
|
+
default:
|
|
8992
|
+
return bounds;
|
|
8993
|
+
}
|
|
8994
|
+
}
|
|
8995
|
+
getIconDrawRect(bounds, opts, imageSize) {
|
|
8996
|
+
const padding = opts.padding ?? 8;
|
|
8997
|
+
const margin = Math.max(0, opts.margin ?? 0);
|
|
8998
|
+
const fit = opts.fit ?? "none";
|
|
8999
|
+
const scaleWithBounds = opts.scaleWithBounds ?? false;
|
|
9000
|
+
const align = opts.align ?? "center";
|
|
9001
|
+
const verticalAlign = opts.verticalAlign ?? "center";
|
|
9002
|
+
const offsetX = opts.offsetX ?? 0;
|
|
9003
|
+
const offsetY = opts.offsetY ?? 0;
|
|
9004
|
+
const innerBounds = {
|
|
9005
|
+
x: bounds.x + margin,
|
|
9006
|
+
y: bounds.y + margin,
|
|
9007
|
+
width: Math.max(0, bounds.width - margin * 2),
|
|
9008
|
+
height: Math.max(0, bounds.height - margin * 2)
|
|
9009
|
+
};
|
|
9010
|
+
const availableWidth = Math.max(0, innerBounds.width - padding * 2);
|
|
9011
|
+
const availableHeight = Math.max(0, innerBounds.height - padding * 2);
|
|
9012
|
+
let drawWidth = opts.width ?? imageSize.width;
|
|
9013
|
+
let drawHeight = opts.height ?? imageSize.height;
|
|
9014
|
+
if (scaleWithBounds) {
|
|
9015
|
+
if ((fit === "contain" || fit === "cover") && imageSize.width > 0 && imageSize.height > 0) {
|
|
9016
|
+
const scaleX = availableWidth / imageSize.width;
|
|
9017
|
+
const scaleY = availableHeight / imageSize.height;
|
|
9018
|
+
const scale = fit === "contain" ? Math.min(scaleX, scaleY) : Math.max(scaleX, scaleY);
|
|
9019
|
+
drawWidth = imageSize.width * scale;
|
|
9020
|
+
drawHeight = imageSize.height * scale;
|
|
9021
|
+
} else if (fit === "stretch") {
|
|
9022
|
+
drawWidth = availableWidth;
|
|
9023
|
+
drawHeight = availableHeight;
|
|
9024
|
+
}
|
|
9025
|
+
}
|
|
9026
|
+
drawWidth = Math.min(Math.max(0, drawWidth), Math.max(0, availableWidth));
|
|
9027
|
+
drawHeight = Math.min(Math.max(0, drawHeight), Math.max(0, availableHeight));
|
|
9028
|
+
let x = innerBounds.x + padding;
|
|
9029
|
+
let y = innerBounds.y + padding;
|
|
9030
|
+
if (align === "center") {
|
|
9031
|
+
x = innerBounds.x + (innerBounds.width - drawWidth) / 2;
|
|
9032
|
+
} else if (align === "right") {
|
|
9033
|
+
x = innerBounds.x + innerBounds.width - drawWidth - padding;
|
|
9034
|
+
}
|
|
9035
|
+
if (verticalAlign === "center") {
|
|
9036
|
+
y = innerBounds.y + (innerBounds.height - drawHeight) / 2;
|
|
9037
|
+
} else if (verticalAlign === "bottom") {
|
|
9038
|
+
y = innerBounds.y + innerBounds.height - drawHeight - padding;
|
|
9039
|
+
}
|
|
9040
|
+
return {
|
|
9041
|
+
x: x + offsetX,
|
|
9042
|
+
y: y + offsetY,
|
|
9043
|
+
width: drawWidth,
|
|
9044
|
+
height: drawHeight
|
|
9045
|
+
};
|
|
9046
|
+
}
|
|
9047
|
+
resolveIconHref(opts) {
|
|
9048
|
+
const source = opts.source;
|
|
9049
|
+
if (source instanceof HTMLImageElement) {
|
|
9050
|
+
return source.src;
|
|
9051
|
+
}
|
|
9052
|
+
if (!source) {
|
|
9053
|
+
return "";
|
|
9054
|
+
}
|
|
9055
|
+
if (this.isSvgMarkup(source)) {
|
|
9056
|
+
return this.svgToDataUrl(this.tintSvg(source, opts.strokeColor, opts.fillColor));
|
|
9057
|
+
}
|
|
9058
|
+
const shouldInlineSvg = source.toLowerCase().endsWith(".svg");
|
|
9059
|
+
if (shouldInlineSvg) {
|
|
9060
|
+
const svgText = this.readSvgFromUrlSync(source);
|
|
9061
|
+
if (svgText) {
|
|
9062
|
+
return this.svgToDataUrl(this.tintSvg(svgText, opts.strokeColor, opts.fillColor));
|
|
9063
|
+
}
|
|
9064
|
+
}
|
|
9065
|
+
return source;
|
|
9066
|
+
}
|
|
9067
|
+
readSvgFromUrlSync(url) {
|
|
9068
|
+
if (typeof XMLHttpRequest === "undefined") {
|
|
9069
|
+
return null;
|
|
9070
|
+
}
|
|
9071
|
+
try {
|
|
9072
|
+
const xhr = new XMLHttpRequest();
|
|
9073
|
+
xhr.open("GET", url, false);
|
|
9074
|
+
xhr.send();
|
|
9075
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
9076
|
+
return xhr.responseText || null;
|
|
9077
|
+
}
|
|
9078
|
+
} catch {
|
|
9079
|
+
}
|
|
9080
|
+
return null;
|
|
9081
|
+
}
|
|
9082
|
+
isSvgMarkup(value) {
|
|
9083
|
+
const trimmed = value.trim().toLowerCase();
|
|
9084
|
+
return trimmed.startsWith("<svg") || trimmed.includes("<svg");
|
|
9085
|
+
}
|
|
9086
|
+
styleSetColor(style, key, color) {
|
|
9087
|
+
const hasKey = new RegExp(`${key}\\s*:`).test(style);
|
|
9088
|
+
if (hasKey) {
|
|
9089
|
+
return style.replace(new RegExp(`${key}\\s*:[^;]+`), `${key}:${color}`);
|
|
9090
|
+
}
|
|
9091
|
+
const suffix = style.trim().endsWith(";") || style.trim() === "" ? "" : ";";
|
|
9092
|
+
return `${style}${suffix}${key}:${color};`;
|
|
9093
|
+
}
|
|
9094
|
+
tintSvg(svgText, strokeColor, fillColor) {
|
|
9095
|
+
if (!strokeColor && !fillColor) return svgText;
|
|
9096
|
+
const parser = new DOMParser();
|
|
9097
|
+
const doc = parser.parseFromString(svgText, "image/svg+xml");
|
|
9098
|
+
const root = doc.documentElement;
|
|
9099
|
+
if (!root || root.nodeName.toLowerCase() === "parsererror") {
|
|
9100
|
+
return svgText;
|
|
9101
|
+
}
|
|
9102
|
+
const all = [root, ...Array.from(root.querySelectorAll("*"))];
|
|
9103
|
+
for (const el of all) {
|
|
9104
|
+
const stroke = el.getAttribute("stroke");
|
|
9105
|
+
if (strokeColor && stroke !== null && stroke.toLowerCase() !== "none") {
|
|
9106
|
+
el.setAttribute("stroke", strokeColor);
|
|
9107
|
+
}
|
|
9108
|
+
const fill = el.getAttribute("fill");
|
|
9109
|
+
if (fillColor && fill !== null && fill.toLowerCase() !== "none") {
|
|
9110
|
+
el.setAttribute("fill", fillColor);
|
|
9111
|
+
}
|
|
9112
|
+
const style = el.getAttribute("style");
|
|
9113
|
+
if (style) {
|
|
9114
|
+
let next = style;
|
|
9115
|
+
if (strokeColor && /stroke\s*:\s*(?!none)/.test(style)) {
|
|
9116
|
+
next = this.styleSetColor(next, "stroke", strokeColor);
|
|
9117
|
+
}
|
|
9118
|
+
if (fillColor && /fill\s*:\s*(?!none)/.test(style)) {
|
|
9119
|
+
next = this.styleSetColor(next, "fill", fillColor);
|
|
9120
|
+
}
|
|
9121
|
+
if (next !== style) {
|
|
9122
|
+
el.setAttribute("style", next);
|
|
9123
|
+
}
|
|
9124
|
+
}
|
|
9125
|
+
}
|
|
9126
|
+
return new XMLSerializer().serializeToString(root);
|
|
9127
|
+
}
|
|
9128
|
+
svgToDataUrl(svg) {
|
|
9129
|
+
const encoded = encodeURIComponent(svg).replace(/%0A/g, "").replace(/%0D/g, "").replace(/%09/g, " ").replace(/%20/g, " ");
|
|
9130
|
+
return `data:image/svg+xml;utf8,${encoded}`;
|
|
9131
|
+
}
|
|
9132
|
+
escapeAttribute(value) {
|
|
9133
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
9134
|
+
}
|
|
8901
9135
|
createEmptySvg(width, height, backgroundColor, includeBackground) {
|
|
8902
9136
|
return [
|
|
8903
9137
|
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`,
|