@mirage-engine/painter 0.4.0 → 1.0.0
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/CHANGELOG.md +6 -0
- package/dist/mirage-painter.js +478 -192
- package/dist/mirage-painter.umd.js +127 -67
- package/dist/src/Box/BoxGenerator.d.ts +18 -0
- package/dist/src/Painter.d.ts +5 -3
- package/dist/src/Text/FontMetricsManager.d.ts +10 -0
- package/dist/src/Text/TextGenerator.d.ts +11 -0
- package/dist/src/index.d.ts +3 -2
- package/dist/src/shaders/index.d.ts +9 -0
- package/dist/src/tools/parser.d.ts +16 -0
- package/dist/src/types/common.d.ts +4 -0
- package/package.json +1 -1
- package/src/Box/BoxGenerator.ts +286 -0
- package/src/Painter.ts +30 -27
- package/src/Text/FontMetricsManager.ts +62 -0
- package/src/Text/TextGenerator.ts +123 -0
- package/src/env.d.ts +4 -0
- package/src/index.ts +4 -3
- package/src/shaders/base/box-fragment.glsl +129 -0
- package/src/shaders/base/box-vertex.glsl +7 -0
- package/src/shaders/chunk/base-color-chunk.glsl +2 -0
- package/src/shaders/chunk/decl-chunk.glsl +4 -0
- package/src/shaders/chunk/uv-chunk.glsl +3 -0
- package/src/shaders/index.ts +16 -0
- package/src/tools/parser.ts +148 -0
- package/src/types/common.ts +4 -1
- package/dist/src/BoxGenerator.d.ts +0 -4
- package/dist/src/TextGenerator.d.ts +0 -3
- package/dist/src/dev/devShader.d.ts +0 -5
- package/src/BoxGenerator.ts +0 -236
- package/src/TextGenerator.ts +0 -85
- package/src/dev/devShader.ts +0 -114
package/src/Painter.ts
CHANGED
|
@@ -1,36 +1,40 @@
|
|
|
1
1
|
import * as THREE from "three";
|
|
2
|
-
import { BoxStyles, TextStyles, ShaderHooks} from "./types";
|
|
3
|
-
import {
|
|
4
|
-
|
|
2
|
+
import { BoxStyles, TextStyles, ShaderHooks } from "./types";
|
|
3
|
+
import {
|
|
4
|
+
createBoxMaterial,
|
|
5
|
+
updateBoxMaterial,
|
|
6
|
+
setBoxUniforms,
|
|
7
|
+
BoxUniformValues,
|
|
8
|
+
} from "./Box/BoxGenerator";
|
|
9
|
+
import { TextGenerator } from "./Text/TextGenerator";
|
|
5
10
|
|
|
6
11
|
export const Painter = {
|
|
7
12
|
create(
|
|
8
13
|
type: "BOX" | "TEXT",
|
|
9
|
-
styles:
|
|
14
|
+
styles: BoxStyles | TextStyles,
|
|
10
15
|
content: string,
|
|
11
16
|
width: number,
|
|
12
17
|
height: number,
|
|
13
18
|
quality: number = 2,
|
|
14
19
|
texture: THREE.Texture | null = null,
|
|
15
|
-
shaderHooks?: ShaderHooks
|
|
20
|
+
shaderHooks?: ShaderHooks,
|
|
16
21
|
): THREE.Material {
|
|
17
22
|
if (type === "BOX") {
|
|
18
|
-
return createBoxMaterial(
|
|
23
|
+
return createBoxMaterial(
|
|
24
|
+
styles as BoxStyles,
|
|
25
|
+
width,
|
|
26
|
+
height,
|
|
27
|
+
texture,
|
|
28
|
+
shaderHooks,
|
|
29
|
+
);
|
|
19
30
|
} else if (type === "TEXT") {
|
|
20
|
-
|
|
31
|
+
return new TextGenerator(
|
|
21
32
|
content || "",
|
|
22
33
|
styles as TextStyles,
|
|
23
34
|
width,
|
|
24
35
|
height,
|
|
25
36
|
quality,
|
|
26
37
|
);
|
|
27
|
-
|
|
28
|
-
return new THREE.MeshBasicMaterial({
|
|
29
|
-
map: texture,
|
|
30
|
-
transparent: true,
|
|
31
|
-
side: THREE.FrontSide,
|
|
32
|
-
color: 0xffffff,
|
|
33
|
-
});
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
return new THREE.MeshBasicMaterial({ visible: false });
|
|
@@ -39,12 +43,12 @@ export const Painter = {
|
|
|
39
43
|
update(
|
|
40
44
|
material: THREE.Material,
|
|
41
45
|
type: "BOX" | "TEXT",
|
|
42
|
-
styles:
|
|
46
|
+
styles: BoxStyles | TextStyles,
|
|
43
47
|
content: string,
|
|
44
48
|
width: number,
|
|
45
49
|
height: number,
|
|
46
50
|
quality: number = 2,
|
|
47
|
-
texture?: THREE.Texture | null
|
|
51
|
+
texture?: THREE.Texture | null,
|
|
48
52
|
) {
|
|
49
53
|
if (type === "BOX") {
|
|
50
54
|
updateBoxMaterial(
|
|
@@ -52,25 +56,24 @@ export const Painter = {
|
|
|
52
56
|
styles as BoxStyles,
|
|
53
57
|
width,
|
|
54
58
|
height,
|
|
55
|
-
texture
|
|
59
|
+
texture,
|
|
56
60
|
);
|
|
57
61
|
} else if (type === "TEXT") {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
if (basicMat.map) {
|
|
61
|
-
basicMat.map.dispose();
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const newTexture = createTextTexture(
|
|
62
|
+
const textMat = material as TextGenerator;
|
|
63
|
+
textMat.updateText(
|
|
65
64
|
content || "",
|
|
66
65
|
styles as TextStyles,
|
|
67
66
|
width,
|
|
68
67
|
height,
|
|
69
68
|
quality,
|
|
70
69
|
);
|
|
71
|
-
|
|
72
|
-
basicMat.map = newTexture;
|
|
73
|
-
basicMat.needsUpdate = true;
|
|
74
70
|
}
|
|
75
71
|
},
|
|
72
|
+
|
|
73
|
+
forceUpdateUniforms(
|
|
74
|
+
material: THREE.ShaderMaterial,
|
|
75
|
+
values: BoxUniformValues,
|
|
76
|
+
) {
|
|
77
|
+
setBoxUniforms(material, values);
|
|
78
|
+
},
|
|
76
79
|
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export class FontMetricsManager {
|
|
2
|
+
private static cache: Map<string, number> = new Map();
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Calculates or retrieves the exact alphabetic baseline (in pixels) for the given font.
|
|
6
|
+
* @param font CSS font shorthand string (e.g., "400 16px Inter")
|
|
7
|
+
* @returns Baseline Y offset in pixels
|
|
8
|
+
*/
|
|
9
|
+
static getBaseline(font: string): number {
|
|
10
|
+
if (this.cache.has(font)) {
|
|
11
|
+
return this.cache.get(font)!;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const baseline = this.calculateBaselineFromDOM(font);
|
|
15
|
+
this.cache.set(font, baseline);
|
|
16
|
+
return baseline;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private static calculateBaselineFromDOM(font: string): number {
|
|
20
|
+
// Return 0 when running in a non‑DOM environment (e.g., server‑side rendering)
|
|
21
|
+
if (typeof document === "undefined") return 0;
|
|
22
|
+
|
|
23
|
+
const container = document.createElement("div");
|
|
24
|
+
const span = document.createElement("span");
|
|
25
|
+
const img = document.createElement("img"); // 1×1 transparent pixel
|
|
26
|
+
|
|
27
|
+
// Hide the helper elements and apply the target font
|
|
28
|
+
container.style.visibility = "hidden";
|
|
29
|
+
container.style.position = "absolute";
|
|
30
|
+
container.style.top = "0px";
|
|
31
|
+
container.style.left = "0px";
|
|
32
|
+
container.style.font = font;
|
|
33
|
+
|
|
34
|
+
// Reset padding/margin/border to isolate pure font metrics
|
|
35
|
+
container.style.margin = "0";
|
|
36
|
+
container.style.padding = "0";
|
|
37
|
+
container.style.border = "none";
|
|
38
|
+
span.style.margin = "0";
|
|
39
|
+
span.style.padding = "0";
|
|
40
|
+
span.style.border = "none";
|
|
41
|
+
span.style.lineHeight = "normal"; // Prevent line‑height interference
|
|
42
|
+
|
|
43
|
+
// Align the 1×1 image to the baseline of the span
|
|
44
|
+
img.width = 1;
|
|
45
|
+
img.height = 1;
|
|
46
|
+
img.style.verticalAlign = "baseline";
|
|
47
|
+
|
|
48
|
+
// Assemble the DOM structure
|
|
49
|
+
span.appendChild(document.createTextNode("Hidden Text"));
|
|
50
|
+
container.appendChild(span);
|
|
51
|
+
container.appendChild(img);
|
|
52
|
+
|
|
53
|
+
document.body.appendChild(container);
|
|
54
|
+
|
|
55
|
+
// Measure the distance from the top of the span to the top of the baseline‑aligned image
|
|
56
|
+
const baseline = img.offsetTop - span.offsetTop;
|
|
57
|
+
|
|
58
|
+
document.body.removeChild(container);
|
|
59
|
+
|
|
60
|
+
return baseline;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { TextStyles } from "../types";
|
|
3
|
+
import { FontMetricsManager } from "./FontMetricsManager";
|
|
4
|
+
|
|
5
|
+
export class TextGenerator extends THREE.MeshBasicMaterial {
|
|
6
|
+
private canvas: HTMLCanvasElement;
|
|
7
|
+
private ctx: CanvasRenderingContext2D;
|
|
8
|
+
private qualityFactor: number;
|
|
9
|
+
|
|
10
|
+
constructor(text: string, styles: TextStyles, rectWidth: number, rectHeight: number, qualityFactor: number = 2) {
|
|
11
|
+
super({
|
|
12
|
+
transparent: true,
|
|
13
|
+
side: THREE.FrontSide,
|
|
14
|
+
color: 0xffffff,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
this.canvas = document.createElement("canvas");
|
|
18
|
+
this.ctx = this.canvas.getContext("2d")!;
|
|
19
|
+
|
|
20
|
+
if (!this.ctx) {
|
|
21
|
+
throw new Error("[Mirage] Failed to create canvas context");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
this.qualityFactor = qualityFactor;
|
|
25
|
+
|
|
26
|
+
this.map = new THREE.CanvasTexture(this.canvas);
|
|
27
|
+
this.map.colorSpace = THREE.LinearSRGBColorSpace;
|
|
28
|
+
this.map.minFilter = THREE.LinearFilter;
|
|
29
|
+
this.map.magFilter = THREE.LinearFilter;
|
|
30
|
+
|
|
31
|
+
this.updateText(text, styles, rectWidth, rectHeight);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private wrapText(text: string, maxWidth: number): string[] {
|
|
35
|
+
const forcedLines = text.split("\n");
|
|
36
|
+
const resultLines: string[] = [];
|
|
37
|
+
|
|
38
|
+
forcedLines.forEach((line) => {
|
|
39
|
+
const words = line.match(/[^\s\-]+\-?|\-|\s+/g) || [];
|
|
40
|
+
|
|
41
|
+
if (words.length === 0) {
|
|
42
|
+
resultLines.push("");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let currentLine = words[0];
|
|
47
|
+
|
|
48
|
+
for (let i = 1; i < words.length; i++) {
|
|
49
|
+
const word = words[i];
|
|
50
|
+
const width = this.ctx.measureText(currentLine + word).width;
|
|
51
|
+
|
|
52
|
+
if (width <= maxWidth + 2) {
|
|
53
|
+
currentLine += word;
|
|
54
|
+
} else {
|
|
55
|
+
if (currentLine) resultLines.push(currentLine);
|
|
56
|
+
currentLine = word.trimStart();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (currentLine) {
|
|
61
|
+
resultLines.push(currentLine);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return resultLines;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public updateText(text: string, styles: TextStyles, rectWidth: number, rectHeight: number, qualityFactor?: number) {
|
|
69
|
+
if (qualityFactor !== undefined) {
|
|
70
|
+
this.qualityFactor = qualityFactor;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const pixelRatio = window.devicePixelRatio || 1;
|
|
74
|
+
const scale = pixelRatio * this.qualityFactor;
|
|
75
|
+
|
|
76
|
+
const newCanvasWidth = rectWidth * scale;
|
|
77
|
+
const newCanvasHeight = rectHeight * scale;
|
|
78
|
+
|
|
79
|
+
if (this.canvas.width !== newCanvasWidth || this.canvas.height !== newCanvasHeight) {
|
|
80
|
+
this.canvas.width = newCanvasWidth;
|
|
81
|
+
this.canvas.height = newCanvasHeight;
|
|
82
|
+
} else {
|
|
83
|
+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Reset transform and apply scale
|
|
87
|
+
this.ctx.setTransform(scale, 0, 0, scale, 0, 0);
|
|
88
|
+
|
|
89
|
+
this.ctx.font = styles.font;
|
|
90
|
+
this.ctx.fillStyle = styles.color;
|
|
91
|
+
this.ctx.textBaseline = "alphabetic";
|
|
92
|
+
this.ctx.globalAlpha = 1;
|
|
93
|
+
|
|
94
|
+
const lines = this.wrapText(text, rectWidth);
|
|
95
|
+
const lineHeight = styles.lineHeight;
|
|
96
|
+
const baseline = FontMetricsManager.getBaseline(styles.font);
|
|
97
|
+
|
|
98
|
+
lines.forEach((line, index) => {
|
|
99
|
+
const y = index * lineHeight + baseline;
|
|
100
|
+
|
|
101
|
+
let x = 0;
|
|
102
|
+
if (styles.textAlign === "center") {
|
|
103
|
+
x = rectWidth / 2;
|
|
104
|
+
} else if (styles.textAlign === "right") {
|
|
105
|
+
x = rectWidth;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
this.ctx.textAlign = styles.textAlign as CanvasTextAlign;
|
|
109
|
+
this.ctx.fillText(line, x, y);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (this.map) {
|
|
113
|
+
this.map.needsUpdate = true;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public dispose() {
|
|
118
|
+
if (this.map) {
|
|
119
|
+
this.map.dispose();
|
|
120
|
+
}
|
|
121
|
+
super.dispose();
|
|
122
|
+
}
|
|
123
|
+
}
|
package/src/env.d.ts
ADDED
package/src/index.ts
CHANGED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
varying vec2 vUv;
|
|
2
|
+
uniform vec2 uSize;
|
|
3
|
+
uniform vec4 uBorderRadius;
|
|
4
|
+
uniform float uBorderWidth;
|
|
5
|
+
uniform vec4 uBgColor;
|
|
6
|
+
uniform vec4 uBorderColor;
|
|
7
|
+
uniform float uOpacity;
|
|
8
|
+
|
|
9
|
+
uniform int uGradientCount;
|
|
10
|
+
uniform float uGradientAngle;
|
|
11
|
+
uniform vec4 uGradientColors[8];
|
|
12
|
+
uniform float uGradientStops[8];
|
|
13
|
+
|
|
14
|
+
#INJECT_DECLARATIONS
|
|
15
|
+
|
|
16
|
+
vec3 linearToSrgb(vec3 linearColor) {
|
|
17
|
+
vec3 linearPart = 12.92 * linearColor;
|
|
18
|
+
vec3 curvePart = 1.055 * pow(linearColor, vec3(1.0 / 2.4)) - 0.055;
|
|
19
|
+
vec3 switchCondition = step(vec3(0.0031308), linearColor);
|
|
20
|
+
return mix(linearPart, curvePart, switchCondition);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
vec3 srgbToLinear(vec3 srgbColor) {
|
|
24
|
+
vec3 linearPart = srgbColor / 12.92;
|
|
25
|
+
vec3 curvePart = pow((srgbColor + vec3(0.055)) / 1.055, vec3(2.4));
|
|
26
|
+
vec3 switchCondition = step(vec3(0.04045), srgbColor);
|
|
27
|
+
return mix(linearPart, curvePart, switchCondition);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
vec4 blendSrcOverInLinear(vec4 front, vec4 back) {
|
|
31
|
+
front.rgb = srgbToLinear(front.rgb);
|
|
32
|
+
back.rgb = srgbToLinear(back.rgb);
|
|
33
|
+
float aOut = front.a + back.a * (1.0 - front.a);
|
|
34
|
+
float safeAlpha = max(aOut, 0.0001);
|
|
35
|
+
vec3 cOut = (front.rgb * front.a + back.rgb * back.a * (1.0 - front.a)) / safeAlpha;
|
|
36
|
+
return vec4(linearToSrgb(cOut), aOut);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
vec4 blendSrcOver(vec4 front, vec4 back) {
|
|
40
|
+
float aOut = front.a + back.a * (1.0 - front.a);
|
|
41
|
+
float safeAlpha = max(aOut, 0.0001);
|
|
42
|
+
vec3 cOut = (front.rgb * front.a + back.rgb * back.a * (1.0 - front.a)) / safeAlpha;
|
|
43
|
+
return vec4(cOut, aOut);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
float sdRoundedBox(vec2 p, vec2 b, float r) {
|
|
47
|
+
vec2 q = abs(p) - b + r;
|
|
48
|
+
return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
float sdVisualBox(vec2 p, vec2 b, float r) {
|
|
52
|
+
vec2 q = abs(p) - b + r;
|
|
53
|
+
float n = 2.01;
|
|
54
|
+
float d = pow(pow(max(q.x, 0.0), n) + pow(max(q.y, 0.0), n), 1.0/n) - r;
|
|
55
|
+
return min(max(q.x, q.y), 0.0) + d;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
vec4 calculateGradientLayer(vec2 uv) {
|
|
59
|
+
if (uGradientCount < 2) return uGradientColors[0];
|
|
60
|
+
|
|
61
|
+
vec2 dir = vec2(sin(uGradientAngle), cos(uGradientAngle));
|
|
62
|
+
vec2 p = (uv - 0.5) * uSize;
|
|
63
|
+
float proj = dot(p, dir);
|
|
64
|
+
float L = abs(uSize.x * dir.x) + abs(uSize.y * dir.y);
|
|
65
|
+
float t = clamp((proj / L) + 0.5, 0.0, 1.0);
|
|
66
|
+
|
|
67
|
+
vec4 color = uGradientColors[0];
|
|
68
|
+
for (int i = 1; i < 8; i++) {
|
|
69
|
+
if (i >= uGradientCount) break;
|
|
70
|
+
float prevStop = uGradientStops[i-1];
|
|
71
|
+
float currStop = uGradientStops[i];
|
|
72
|
+
|
|
73
|
+
float factor = clamp((t - prevStop) / (currStop - prevStop + 0.00001), 0.0, 1.0);
|
|
74
|
+
color = mix(color, uGradientColors[i], factor);
|
|
75
|
+
}
|
|
76
|
+
// return vec4(linearToSrgb(color.rgb), color.a);
|
|
77
|
+
return vec4(color);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
void main() {
|
|
81
|
+
vec2 p = (vUv - 0.5) * uSize;
|
|
82
|
+
vec2 halfSize = uSize * 0.5;
|
|
83
|
+
|
|
84
|
+
#INJECT_UV_MODIFIER
|
|
85
|
+
|
|
86
|
+
// color decision pipeline
|
|
87
|
+
vec4 baseColor = vec4(uBgColor.rgb, uBgColor.a);
|
|
88
|
+
|
|
89
|
+
if (uGradientCount > 0) {
|
|
90
|
+
vec4 gradColor = calculateGradientLayer(vUv);
|
|
91
|
+
baseColor = blendSrcOver(gradColor, baseColor);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#INJECT_BASE_COLOR
|
|
95
|
+
|
|
96
|
+
// Hybrid SDF
|
|
97
|
+
vec2 xRadii = mix(uBorderRadius.xw, uBorderRadius.yz, step(0.0, p.x));
|
|
98
|
+
float r = mix(xRadii.y, xRadii.x, step(0.0, p.y));
|
|
99
|
+
r = min(r, min(halfSize.x, halfSize.y));
|
|
100
|
+
|
|
101
|
+
float d = sdRoundedBox(p, halfSize, r);
|
|
102
|
+
|
|
103
|
+
float d_smooth = sdVisualBox(p, halfSize, r);
|
|
104
|
+
float d_visual = d_smooth / fwidth(d_smooth);
|
|
105
|
+
|
|
106
|
+
// rendering pipeline
|
|
107
|
+
float bgMask = 1.0 - smoothstep(-0.5, 0.5, d_visual);
|
|
108
|
+
|
|
109
|
+
float halfBorder = uBorderWidth * 0.5;
|
|
110
|
+
float borderD = abs(d + halfBorder) - halfBorder;
|
|
111
|
+
float borderAlpha = (1.0 - smoothstep(0.0, 1.0, borderD)) * uBorderColor.a;
|
|
112
|
+
if (uBorderWidth <= 0.01) {
|
|
113
|
+
borderAlpha = 0.0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// final blending (border + background) using blendSrcOver
|
|
117
|
+
vec4 borderLayer = vec4(uBorderColor.rgb, borderAlpha);
|
|
118
|
+
vec4 finalColor = blendSrcOver(borderLayer, baseColor);
|
|
119
|
+
// final color control (Tint, Noise)
|
|
120
|
+
#INJECT_COLOR_MODIFIER
|
|
121
|
+
|
|
122
|
+
float finalOpacity = finalColor.a * bgMask * uOpacity;
|
|
123
|
+
if (finalOpacity < 0.001) discard;
|
|
124
|
+
|
|
125
|
+
gl_FragColor = vec4(finalColor.rgb, finalOpacity);
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
// #include <colorspace_fragment>
|
|
129
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import vertexShader from "./base/box-vertex.glsl?raw";
|
|
2
|
+
import fragmentShader from "./base/box-fragment.glsl?raw";
|
|
3
|
+
import declChunk from "./chunk/decl-chunk.glsl?raw";
|
|
4
|
+
import uvChunk from "./chunk/uv-chunk.glsl?raw";
|
|
5
|
+
import baseColorChunk from "./chunk/base-color-chunk.glsl?raw";
|
|
6
|
+
|
|
7
|
+
export const BoxShader = {
|
|
8
|
+
vertexShader,
|
|
9
|
+
fragmentShader,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const BoxChunk = {
|
|
13
|
+
declChunk,
|
|
14
|
+
uvChunk,
|
|
15
|
+
baseColorChunk,
|
|
16
|
+
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
|
|
3
|
+
export function parsePixelValue(value: string | number): number {
|
|
4
|
+
if (typeof value === "number") return value;
|
|
5
|
+
return parseFloat(value) || 0;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function parseColor(colorStr: string) {
|
|
9
|
+
if (!colorStr || colorStr === "transparent") {
|
|
10
|
+
return { color: new THREE.Color(0xffffff), alpha: 0.0 };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const rgbaMatch = colorStr.match(
|
|
14
|
+
/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/,
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
if (rgbaMatch) {
|
|
18
|
+
const r = parseInt(rgbaMatch[1], 10);
|
|
19
|
+
const g = parseInt(rgbaMatch[2], 10);
|
|
20
|
+
const b = parseInt(rgbaMatch[3], 10);
|
|
21
|
+
const a = rgbaMatch[4] !== undefined ? parseFloat(rgbaMatch[4]) : 1.0;
|
|
22
|
+
return { color: new THREE.Color(`rgb(${r}, ${g}, ${b})`), alpha: a };
|
|
23
|
+
}
|
|
24
|
+
return { color: new THREE.Color(colorStr), alpha: 1.0 };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function splitByComma(str: string) {
|
|
28
|
+
const result = [];
|
|
29
|
+
let current = "";
|
|
30
|
+
let depth = 0;
|
|
31
|
+
for (let i = 0; i < str.length; i++) {
|
|
32
|
+
const char = str[i];
|
|
33
|
+
if (char === "(") depth++;
|
|
34
|
+
else if (char === ")") depth--;
|
|
35
|
+
else if (char === "," && depth === 0) {
|
|
36
|
+
result.push(current.trim());
|
|
37
|
+
current = "";
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
current += char;
|
|
41
|
+
}
|
|
42
|
+
if (current) result.push(current.trim());
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function parseLinearGradient(value: string | null | undefined) {
|
|
47
|
+
if (!value || typeof value !== "string" || !value.includes("linear-gradient")) return null;
|
|
48
|
+
|
|
49
|
+
const match = value.match(/linear-gradient\((.*)\)/);
|
|
50
|
+
if (!match) return null;
|
|
51
|
+
|
|
52
|
+
const content = match[1];
|
|
53
|
+
const parts = splitByComma(content);
|
|
54
|
+
|
|
55
|
+
let angle = Math.PI; // default: to bottom
|
|
56
|
+
let firstStopIndex = 0;
|
|
57
|
+
|
|
58
|
+
const firstPart = parts[0].trim();
|
|
59
|
+
if (firstPart.startsWith("to ")) {
|
|
60
|
+
if (firstPart === "to top") angle = 0;
|
|
61
|
+
else if (firstPart === "to right") angle = Math.PI / 2;
|
|
62
|
+
else if (firstPart === "to bottom") angle = Math.PI;
|
|
63
|
+
else if (firstPart === "to left") angle = Math.PI * 1.5;
|
|
64
|
+
else if (firstPart === "to top right" || firstPart === "to right top")
|
|
65
|
+
angle = Math.PI / 4;
|
|
66
|
+
else if (
|
|
67
|
+
firstPart === "to bottom right" ||
|
|
68
|
+
firstPart === "to right bottom"
|
|
69
|
+
)
|
|
70
|
+
angle = Math.PI * 0.75;
|
|
71
|
+
else if (firstPart === "to bottom left" || firstPart === "to left bottom")
|
|
72
|
+
angle = Math.PI * 1.25;
|
|
73
|
+
else if (firstPart === "to top left" || firstPart === "to left top")
|
|
74
|
+
angle = Math.PI * 1.75;
|
|
75
|
+
firstStopIndex = 1;
|
|
76
|
+
} else if (
|
|
77
|
+
firstPart.endsWith("deg") ||
|
|
78
|
+
firstPart.endsWith("rad") ||
|
|
79
|
+
firstPart.endsWith("turn")
|
|
80
|
+
) {
|
|
81
|
+
const num = parseFloat(firstPart);
|
|
82
|
+
if (firstPart.endsWith("deg")) angle = num * (Math.PI / 180);
|
|
83
|
+
else if (firstPart.endsWith("rad")) angle = num;
|
|
84
|
+
else if (firstPart.endsWith("turn")) angle = num * Math.PI * 2;
|
|
85
|
+
firstStopIndex = 1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const stops = [];
|
|
89
|
+
for (let i = firstStopIndex; i < parts.length; i++) {
|
|
90
|
+
if (stops.length >= 8) break;
|
|
91
|
+
const part = parts[i].trim();
|
|
92
|
+
const lastSpace = part.lastIndexOf(" ");
|
|
93
|
+
let colorStr = part;
|
|
94
|
+
let stopPos = null;
|
|
95
|
+
|
|
96
|
+
if (lastSpace !== -1 && !part.endsWith(")")) {
|
|
97
|
+
const possiblePos = part.substring(lastSpace + 1);
|
|
98
|
+
if (
|
|
99
|
+
possiblePos.endsWith("%") ||
|
|
100
|
+
possiblePos.endsWith("px") ||
|
|
101
|
+
!isNaN(parseFloat(possiblePos))
|
|
102
|
+
) {
|
|
103
|
+
colorStr = part.substring(0, lastSpace).trim();
|
|
104
|
+
stopPos = possiblePos;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const parsedColor = parseColor(colorStr);
|
|
109
|
+
stops.push({
|
|
110
|
+
color: parsedColor.color,
|
|
111
|
+
alpha: parsedColor.alpha,
|
|
112
|
+
rawStop: stopPos,
|
|
113
|
+
stop: 0,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (stops.length > 0) {
|
|
118
|
+
for (let i = 0; i < stops.length; i++) {
|
|
119
|
+
if (stops[i].rawStop !== null) {
|
|
120
|
+
stops[i].stop = parseFloat(stops[i].rawStop!) / 100;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (stops[0].rawStop === null) stops[0].stop = 0.0;
|
|
125
|
+
|
|
126
|
+
if (stops.length > 1) {
|
|
127
|
+
if (stops[stops.length - 1].rawStop === null) stops[stops.length - 1].stop = 1.0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let lastKnownIndex = 0;
|
|
131
|
+
for (let i = 1; i < stops.length; i++) {
|
|
132
|
+
if (stops[i].rawStop !== null || i === stops.length - 1) {
|
|
133
|
+
const diff = i - lastKnownIndex;
|
|
134
|
+
if (diff > 1) {
|
|
135
|
+
const startVal = stops[lastKnownIndex].stop;
|
|
136
|
+
const endVal = stops[i].stop;
|
|
137
|
+
const step = (endVal - startVal) / diff;
|
|
138
|
+
for (let j = 1; j < diff; j++) {
|
|
139
|
+
stops[lastKnownIndex + j].stop = startVal + step * j;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
lastKnownIndex = i;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { angle, stops };
|
|
148
|
+
}
|
package/src/types/common.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// For CanvasRenderingContext2D
|
|
2
2
|
export interface TextStyles {
|
|
3
3
|
font: string;
|
|
4
|
+
fontSize :string;
|
|
4
5
|
color: string;
|
|
5
6
|
textAlign: CanvasTextAlign;
|
|
6
7
|
textBaseline: CanvasTextBaseline;
|
|
@@ -11,9 +12,11 @@ export interface TextStyles {
|
|
|
11
12
|
|
|
12
13
|
export interface BoxStyles {
|
|
13
14
|
backgroundColor: string;
|
|
15
|
+
backgroundImage: string;
|
|
16
|
+
imageSrc?: string;
|
|
17
|
+
isTraveler?: boolean;
|
|
14
18
|
opacity: number;
|
|
15
19
|
zIndex: number;
|
|
16
|
-
|
|
17
20
|
borderRadius: string;
|
|
18
21
|
borderColor: string;
|
|
19
22
|
borderWidth: string;
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import { BoxStyles, ShaderHooks } from './types';
|
|
2
|
-
import * as THREE from "three";
|
|
3
|
-
export declare function createBoxMaterial(styles: BoxStyles, width: number, height: number, texture?: THREE.Texture | null, hooks?: ShaderHooks): THREE.ShaderMaterial;
|
|
4
|
-
export declare function updateBoxMaterial(material: THREE.ShaderMaterial, styles: BoxStyles, width: number, height: number, texture?: THREE.Texture | null): void;
|