@solidtv/renderer 1.1.5 → 1.2.1
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/dist/exports/canvas-shaders.d.ts +1 -0
- package/dist/exports/canvas-shaders.js +1 -0
- package/dist/exports/canvas-shaders.js.map +1 -1
- package/dist/exports/webgl-shaders.d.ts +1 -0
- package/dist/exports/webgl-shaders.js +1 -0
- package/dist/exports/webgl-shaders.js.map +1 -1
- package/dist/src/common/CommonTypes.d.ts +5 -0
- package/dist/src/core/CoreNode.js +4 -2
- package/dist/src/core/CoreNode.js.map +1 -1
- package/dist/src/core/CoreTextNode.d.ts +17 -0
- package/dist/src/core/CoreTextNode.js +33 -7
- package/dist/src/core/CoreTextNode.js.map +1 -1
- package/dist/src/core/Stage.js +6 -0
- package/dist/src/core/Stage.js.map +1 -1
- package/dist/src/core/renderers/webgl/WebGlRenderer.js +11 -8
- package/dist/src/core/renderers/webgl/WebGlRenderer.js.map +1 -1
- package/dist/src/core/renderers/webgl/WebGlShaderProgram.js +3 -2
- package/dist/src/core/renderers/webgl/WebGlShaderProgram.js.map +1 -1
- package/dist/src/core/shaders/canvas/RadialProgress.d.ts +10 -0
- package/dist/src/core/shaders/canvas/RadialProgress.js +87 -0
- package/dist/src/core/shaders/canvas/RadialProgress.js.map +1 -0
- package/dist/src/core/shaders/templates/BorderTemplate.d.ts +1 -1
- package/dist/src/core/shaders/templates/BorderTemplate.js +1 -1
- package/dist/src/core/shaders/templates/BorderTemplate.js.map +1 -1
- package/dist/src/core/shaders/templates/RadialProgressTemplate.d.ts +61 -0
- package/dist/src/core/shaders/templates/RadialProgressTemplate.js +54 -0
- package/dist/src/core/shaders/templates/RadialProgressTemplate.js.map +1 -0
- package/dist/src/core/shaders/webgl/RadialProgress.d.ts +3 -0
- package/dist/src/core/shaders/webgl/RadialProgress.js +145 -0
- package/dist/src/core/shaders/webgl/RadialProgress.js.map +1 -0
- package/dist/src/core/text-rendering/CanvasTextRenderer.js +12 -2
- package/dist/src/core/text-rendering/CanvasTextRenderer.js.map +1 -1
- package/dist/src/core/text-rendering/SdfFontHandler.js +35 -0
- package/dist/src/core/text-rendering/SdfFontHandler.js.map +1 -1
- package/dist/src/core/text-rendering/SdfTextRenderer.js +32 -20
- package/dist/src/core/text-rendering/SdfTextRenderer.js.map +1 -1
- package/dist/src/core/text-rendering/TextLayoutEngine.d.ts +7 -1
- package/dist/src/core/text-rendering/TextLayoutEngine.js +94 -10
- package/dist/src/core/text-rendering/TextLayoutEngine.js.map +1 -1
- package/dist/src/core/text-rendering/TextRenderer.d.ts +84 -10
- package/dist/src/main-api/Renderer.d.ts +23 -1
- package/dist/src/main-api/Renderer.js +6 -1
- package/dist/src/main-api/Renderer.js.map +1 -1
- package/dist/tsconfig.dist.tsbuildinfo +1 -1
- package/exports/canvas-shaders.ts +1 -0
- package/exports/webgl-shaders.ts +1 -0
- package/package.json +1 -1
- package/src/common/CommonTypes.ts +5 -0
- package/src/core/CoreNode.ts +6 -2
- package/src/core/CoreTextNode.ts +34 -6
- package/src/core/Stage.ts +7 -0
- package/src/core/renderers/webgl/WebGlCtxTexture.test.ts +94 -0
- package/src/core/renderers/webgl/WebGlRenderer.ts +14 -10
- package/src/core/renderers/webgl/WebGlShaderProgram.ts +3 -2
- package/src/core/shaders/canvas/RadialProgress.ts +113 -0
- package/src/core/shaders/templates/BorderTemplate.ts +2 -2
- package/src/core/shaders/templates/RadialProgressTemplate.test.ts +125 -0
- package/src/core/shaders/templates/RadialProgressTemplate.ts +112 -0
- package/src/core/shaders/webgl/RadialProgress.ts +163 -0
- package/src/core/text-rendering/CanvasTextRenderer.ts +14 -2
- package/src/core/text-rendering/SdfFontHandler.ts +37 -0
- package/src/core/text-rendering/SdfTextRenderer.ts +41 -22
- package/src/core/text-rendering/TextLayoutEngine.ts +99 -12
- package/src/core/text-rendering/TextRenderer.ts +85 -10
- package/src/main-api/Renderer.ts +33 -2
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { CanvasShaderType } from '../../renderers/canvas/CanvasShaderNode.js';
|
|
2
|
+
import {
|
|
3
|
+
RadialProgressTemplate,
|
|
4
|
+
type RadialProgressProps,
|
|
5
|
+
} from '../templates/RadialProgressTemplate.js';
|
|
6
|
+
|
|
7
|
+
export interface ComputedRadialProgressValues {
|
|
8
|
+
cx: number;
|
|
9
|
+
cy: number;
|
|
10
|
+
radius: number;
|
|
11
|
+
colorChannels: number[][]; // [r,g,b,a] per stop color, 0..255 (a in 0..1)
|
|
12
|
+
trackColor: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const SEGMENTS = 64;
|
|
16
|
+
|
|
17
|
+
function lerpColor(a: number[], b: number[], t: number): string {
|
|
18
|
+
const r = Math.round(a[0]! + (b[0]! - a[0]!) * t);
|
|
19
|
+
const g = Math.round(a[1]! + (b[1]! - a[1]!) * t);
|
|
20
|
+
const bl = Math.round(a[2]! + (b[2]! - a[2]!) * t);
|
|
21
|
+
const al = a[3]! + (b[3]! - a[3]!) * t;
|
|
22
|
+
return `rgba(${r},${g},${bl},${al})`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function colorAt(channels: number[][], stops: number[], t: number): string {
|
|
26
|
+
const last = channels.length - 1;
|
|
27
|
+
if (t <= stops[0]!) return lerpColor(channels[0]!, channels[0]!, 0);
|
|
28
|
+
if (t >= stops[last]!) return lerpColor(channels[last]!, channels[last]!, 0);
|
|
29
|
+
for (let i = 0; i < last; i++) {
|
|
30
|
+
const left = stops[i]!;
|
|
31
|
+
const right = stops[i + 1]!;
|
|
32
|
+
if (t >= left && t <= right) {
|
|
33
|
+
const lt = (t - left) / (right - left);
|
|
34
|
+
return lerpColor(channels[i]!, channels[i + 1]!, lt);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return lerpColor(channels[last]!, channels[last]!, 0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function toChannels(rgba: number): number[] {
|
|
41
|
+
return [
|
|
42
|
+
(rgba >>> 24) & 0xff,
|
|
43
|
+
(rgba >>> 16) & 0xff,
|
|
44
|
+
(rgba >>> 8) & 0xff,
|
|
45
|
+
(rgba & 0xff) / 255,
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const RadialProgress: CanvasShaderType<
|
|
50
|
+
RadialProgressProps,
|
|
51
|
+
ComputedRadialProgressValues
|
|
52
|
+
> = {
|
|
53
|
+
props: RadialProgressTemplate.props,
|
|
54
|
+
update(node) {
|
|
55
|
+
const props = this.props!;
|
|
56
|
+
const autoRadius = Math.min(node.w, node.h) * 0.5 - props.width * 0.5;
|
|
57
|
+
const radius = props.radius > 0 ? props.radius : autoRadius;
|
|
58
|
+
|
|
59
|
+
const colorChannels: number[][] = [];
|
|
60
|
+
for (let i = 0; i < props.colors.length; i++) {
|
|
61
|
+
colorChannels.push(toChannels(props.colors[i]!));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.computed = {
|
|
65
|
+
cx: node.w * 0.5,
|
|
66
|
+
cy: node.h * 0.5,
|
|
67
|
+
radius,
|
|
68
|
+
colorChannels,
|
|
69
|
+
trackColor:
|
|
70
|
+
props.trackColor !== 0 ? this.toColorString(props.trackColor) : null,
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
render(ctx, node, renderContext) {
|
|
74
|
+
renderContext();
|
|
75
|
+
const { cx, cy, radius, colorChannels, trackColor } = this
|
|
76
|
+
.computed as ComputedRadialProgressValues;
|
|
77
|
+
const { tx, ty } = node.globalTransform!;
|
|
78
|
+
const props = this.props!;
|
|
79
|
+
const { width, progress, startAngle, direction, cap } = props;
|
|
80
|
+
const stops = props.stops;
|
|
81
|
+
|
|
82
|
+
const ax = tx + cx;
|
|
83
|
+
const ay = ty + cy;
|
|
84
|
+
|
|
85
|
+
ctx.lineWidth = width;
|
|
86
|
+
ctx.lineCap = cap === 1 ? 'round' : 'butt';
|
|
87
|
+
|
|
88
|
+
if (trackColor !== null) {
|
|
89
|
+
ctx.strokeStyle = trackColor;
|
|
90
|
+
ctx.beginPath();
|
|
91
|
+
ctx.arc(ax, ay, radius, 0, Math.PI * 2);
|
|
92
|
+
ctx.stroke();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (progress <= 0) return;
|
|
96
|
+
|
|
97
|
+
const sweep = Math.PI * 2 * progress * direction;
|
|
98
|
+
const step = sweep / SEGMENTS;
|
|
99
|
+
// Overlap segments by a tiny amount so the seams don't show on canvas AA
|
|
100
|
+
const overlap = Math.abs(step) * 0.02;
|
|
101
|
+
|
|
102
|
+
for (let i = 0; i < SEGMENTS; i++) {
|
|
103
|
+
const t = i / (SEGMENTS - 1);
|
|
104
|
+
ctx.strokeStyle = colorAt(colorChannels, stops, t);
|
|
105
|
+
ctx.beginPath();
|
|
106
|
+
const a0 = startAngle + step * i;
|
|
107
|
+
const a1 =
|
|
108
|
+
startAngle + step * (i + 1) + (direction === 1 ? overlap : -overlap);
|
|
109
|
+
ctx.arc(ax, ay, radius, a0, a1, direction === -1);
|
|
110
|
+
ctx.stroke();
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
};
|
|
@@ -21,7 +21,7 @@ export interface BorderProps {
|
|
|
21
21
|
/**
|
|
22
22
|
* Alignment of the border
|
|
23
23
|
*
|
|
24
|
-
* @default '
|
|
24
|
+
* @default 'outside'
|
|
25
25
|
*/
|
|
26
26
|
align: number | 'inside' | 'center' | 'outside';
|
|
27
27
|
/**
|
|
@@ -63,7 +63,7 @@ export function getBorderProps<P extends string>(
|
|
|
63
63
|
},
|
|
64
64
|
[pf + 'color']: 0xffffffff,
|
|
65
65
|
[pf + 'align']: {
|
|
66
|
-
default:
|
|
66
|
+
default: 1, //outside,
|
|
67
67
|
resolve(value) {
|
|
68
68
|
if (!isNaN(value)) {
|
|
69
69
|
return value as number;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
RadialProgressTemplate,
|
|
4
|
+
type RadialProgressProps,
|
|
5
|
+
} from './RadialProgressTemplate.js';
|
|
6
|
+
import {
|
|
7
|
+
isAdvancedShaderProp,
|
|
8
|
+
resolveShaderProps,
|
|
9
|
+
} from '../../renderers/CoreShaderNode.js';
|
|
10
|
+
|
|
11
|
+
function resolve(input: Partial<RadialProgressProps>): RadialProgressProps {
|
|
12
|
+
const props = { ...input } as Record<string, unknown>;
|
|
13
|
+
resolveShaderProps(props, RadialProgressTemplate.props as never);
|
|
14
|
+
return props as unknown as RadialProgressProps;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('RadialProgressTemplate', () => {
|
|
18
|
+
describe('progress', () => {
|
|
19
|
+
const cfg = RadialProgressTemplate.props!.progress;
|
|
20
|
+
if (!isAdvancedShaderProp(cfg))
|
|
21
|
+
throw new Error('progress should be advanced');
|
|
22
|
+
|
|
23
|
+
it('clamps values below 0 to 0', () => {
|
|
24
|
+
expect(cfg.resolve!(-0.5, {} as never)).toBe(0);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('clamps values above 1 to 1', () => {
|
|
28
|
+
expect(cfg.resolve!(2, {} as never)).toBe(1);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('passes through in-range values', () => {
|
|
32
|
+
expect(cfg.resolve!(0.42, {} as never)).toBe(0.42);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('returns default when undefined', () => {
|
|
36
|
+
expect(cfg.resolve!(undefined as never, {} as never)).toBe(1);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('colors', () => {
|
|
41
|
+
const cfg = RadialProgressTemplate.props!.colors;
|
|
42
|
+
if (!isAdvancedShaderProp(cfg))
|
|
43
|
+
throw new Error('colors should be advanced');
|
|
44
|
+
|
|
45
|
+
it('falls back to default on undefined', () => {
|
|
46
|
+
expect(cfg.resolve!(undefined as never, {} as never)).toEqual([
|
|
47
|
+
0xffffffff,
|
|
48
|
+
]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('falls back to default on empty array', () => {
|
|
52
|
+
expect(cfg.resolve!([] as never, {} as never)).toEqual([0xffffffff]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('passes through user-provided colors', () => {
|
|
56
|
+
const input = [0xff0000ff, 0x00ff00ff];
|
|
57
|
+
expect(cfg.resolve!(input, {} as never)).toEqual(input);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('stops', () => {
|
|
62
|
+
const cfg = RadialProgressTemplate.props!.stops;
|
|
63
|
+
if (!isAdvancedShaderProp(cfg)) throw new Error('stops should be advanced');
|
|
64
|
+
|
|
65
|
+
it('auto-distributes when omitted (n=3)', () => {
|
|
66
|
+
const out = cfg.resolve!(
|
|
67
|
+
undefined as never,
|
|
68
|
+
{
|
|
69
|
+
colors: [1, 2, 3],
|
|
70
|
+
} as never,
|
|
71
|
+
);
|
|
72
|
+
expect(out).toEqual([0, 0.5, 1]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('auto-distributes when length mismatches', () => {
|
|
76
|
+
const out = cfg.resolve!(
|
|
77
|
+
[0, 1] as never,
|
|
78
|
+
{
|
|
79
|
+
colors: [1, 2, 3],
|
|
80
|
+
} as never,
|
|
81
|
+
);
|
|
82
|
+
expect(out).toEqual([0, 0.5, 1]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('handles single color (n=1) without NaN', () => {
|
|
86
|
+
const out = cfg.resolve!(undefined as never, { colors: [1] } as never);
|
|
87
|
+
expect(out).toEqual([0]);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('passes through valid stops', () => {
|
|
91
|
+
const out = cfg.resolve!(
|
|
92
|
+
[0, 0.3, 1] as never,
|
|
93
|
+
{
|
|
94
|
+
colors: [1, 2, 3],
|
|
95
|
+
} as never,
|
|
96
|
+
);
|
|
97
|
+
expect(out).toEqual([0, 0.3, 1]);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('defaults via resolveShaderProps', () => {
|
|
102
|
+
it('applies all defaults when no props given', () => {
|
|
103
|
+
const r = resolve({});
|
|
104
|
+
expect(r.width).toBe(8);
|
|
105
|
+
expect(r.radius).toBe(0);
|
|
106
|
+
expect(r.progress).toBe(1);
|
|
107
|
+
expect(r.startAngle).toBeCloseTo(-Math.PI / 2);
|
|
108
|
+
expect(r.direction).toBe(1);
|
|
109
|
+
expect(r.colors).toEqual([0xffffffff]);
|
|
110
|
+
expect(r.stops).toEqual([0]);
|
|
111
|
+
expect(r.trackColor).toBe(0x00000000);
|
|
112
|
+
expect(r.cap).toBe(1);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('clamps progress through full resolution path', () => {
|
|
116
|
+
const r = resolve({ progress: 1.5 });
|
|
117
|
+
expect(r.progress).toBe(1);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('auto-distributes stops through full resolution path', () => {
|
|
121
|
+
const r = resolve({ colors: [0xff0000ff, 0x00ff00ff, 0x0000ffff] });
|
|
122
|
+
expect(r.stops).toEqual([0, 0.5, 1]);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { CoreShaderType } from '../../renderers/CoreShaderNode.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Properties of the {@link RadialProgress} shader
|
|
5
|
+
*/
|
|
6
|
+
export interface RadialProgressProps {
|
|
7
|
+
/**
|
|
8
|
+
* Stroke width of the ring in pixels
|
|
9
|
+
*
|
|
10
|
+
* @default 8
|
|
11
|
+
*/
|
|
12
|
+
width: number;
|
|
13
|
+
/**
|
|
14
|
+
* Outer radius of the ring in pixels. When 0, auto-fits the node:
|
|
15
|
+
* `min(node.w, node.h) / 2 - width / 2`
|
|
16
|
+
*
|
|
17
|
+
* @default 0
|
|
18
|
+
*/
|
|
19
|
+
radius: number;
|
|
20
|
+
/**
|
|
21
|
+
* Portion of the ring that is filled, in `[0, 1]`
|
|
22
|
+
*
|
|
23
|
+
* @default 1
|
|
24
|
+
*/
|
|
25
|
+
progress: number;
|
|
26
|
+
/**
|
|
27
|
+
* Angle (in radians) where the filled arc starts. `-PI/2` is 12 o'clock.
|
|
28
|
+
*
|
|
29
|
+
* @default -Math.PI / 2
|
|
30
|
+
*/
|
|
31
|
+
startAngle: number;
|
|
32
|
+
/**
|
|
33
|
+
* Sweep direction. `1` = clockwise, `-1` = counter-clockwise.
|
|
34
|
+
*
|
|
35
|
+
* @default 1
|
|
36
|
+
*/
|
|
37
|
+
direction: 1 | -1;
|
|
38
|
+
/**
|
|
39
|
+
* Colors swept along the filled arc.
|
|
40
|
+
*
|
|
41
|
+
* @default [0xffffffff]
|
|
42
|
+
*/
|
|
43
|
+
colors: number[];
|
|
44
|
+
/**
|
|
45
|
+
* Color stops along the filled arc, in `[0, 1]`. Auto-distributed when omitted
|
|
46
|
+
* or when length doesn't match `colors`.
|
|
47
|
+
*/
|
|
48
|
+
stops: number[];
|
|
49
|
+
/**
|
|
50
|
+
* Background ring color (drawn under the full circle). `0x00000000` disables.
|
|
51
|
+
*
|
|
52
|
+
* @default 0x00000000
|
|
53
|
+
*/
|
|
54
|
+
trackColor: number;
|
|
55
|
+
/**
|
|
56
|
+
* Arc end-cap style. `0` = butt, `1` = round.
|
|
57
|
+
*
|
|
58
|
+
* @default 1
|
|
59
|
+
*/
|
|
60
|
+
cap: 0 | 1;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const RadialProgressTemplate: CoreShaderType<RadialProgressProps> = {
|
|
64
|
+
props: {
|
|
65
|
+
width: 8,
|
|
66
|
+
radius: 0,
|
|
67
|
+
progress: {
|
|
68
|
+
default: 1,
|
|
69
|
+
resolve(value) {
|
|
70
|
+
if (value === undefined) return this.default;
|
|
71
|
+
if (value < 0) return 0;
|
|
72
|
+
if (value > 1) return 1;
|
|
73
|
+
return value;
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
startAngle: -Math.PI / 2,
|
|
77
|
+
direction: 1,
|
|
78
|
+
colors: {
|
|
79
|
+
default: [0xffffffff],
|
|
80
|
+
resolve(value) {
|
|
81
|
+
if (value !== undefined && value.length > 0) {
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
return ([] as number[]).concat(this.default);
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
stops: {
|
|
88
|
+
default: [0],
|
|
89
|
+
resolve(value, props) {
|
|
90
|
+
if (value !== undefined && value.length === props.colors.length) {
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
if (value === undefined) {
|
|
94
|
+
value = [];
|
|
95
|
+
}
|
|
96
|
+
const len = props.colors.length;
|
|
97
|
+
if (len === 1) {
|
|
98
|
+
value[0] = 0;
|
|
99
|
+
value.length = 1;
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
for (let i = 0; i < len; i++) {
|
|
103
|
+
value[i] = i * (1 / (len - 1));
|
|
104
|
+
}
|
|
105
|
+
value.length = len;
|
|
106
|
+
return value;
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
trackColor: 0x00000000,
|
|
110
|
+
cap: 1,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { CoreNode } from '../../CoreNode.js';
|
|
2
|
+
import { getNormalizedRgbaComponents } from '../../lib/utils.js';
|
|
3
|
+
import {
|
|
4
|
+
RadialProgressTemplate,
|
|
5
|
+
type RadialProgressProps,
|
|
6
|
+
} from '../templates/RadialProgressTemplate.js';
|
|
7
|
+
import type { WebGlShaderType } from '../../renderers/webgl/WebGlShaderNode.js';
|
|
8
|
+
import type { WebGlRenderer } from '../../renderers/webgl/WebGlRenderer.js';
|
|
9
|
+
|
|
10
|
+
export const RadialProgress: WebGlShaderType<RadialProgressProps> = {
|
|
11
|
+
props: RadialProgressTemplate.props,
|
|
12
|
+
update(node: CoreNode) {
|
|
13
|
+
const props = this.props!;
|
|
14
|
+
|
|
15
|
+
const autoRadius = Math.min(node.w, node.h) * 0.5 - props.width * 0.5;
|
|
16
|
+
const radius = props.radius > 0 ? props.radius : autoRadius;
|
|
17
|
+
|
|
18
|
+
this.uniform2f('u_center', node.w * 0.5, node.h * 0.5);
|
|
19
|
+
this.uniform1f('u_radius', radius);
|
|
20
|
+
this.uniform1f('u_width', props.width);
|
|
21
|
+
this.uniform1f('u_progress', props.progress);
|
|
22
|
+
this.uniform1f('u_startAngle', props.startAngle);
|
|
23
|
+
this.uniform1f('u_direction', props.direction);
|
|
24
|
+
this.uniform1fv('u_stops', new Float32Array(props.stops));
|
|
25
|
+
|
|
26
|
+
const colors: number[] = [];
|
|
27
|
+
for (let i = 0; i < props.colors.length; i++) {
|
|
28
|
+
const norm = getNormalizedRgbaComponents(props.colors[i]!);
|
|
29
|
+
colors.push(norm[0]!, norm[1]!, norm[2]!, norm[3]!);
|
|
30
|
+
}
|
|
31
|
+
this.uniform4fv('u_colors', new Float32Array(colors));
|
|
32
|
+
|
|
33
|
+
const trackNorm = getNormalizedRgbaComponents(props.trackColor);
|
|
34
|
+
this.uniform4f(
|
|
35
|
+
'u_trackColor',
|
|
36
|
+
trackNorm[0]!,
|
|
37
|
+
trackNorm[1]!,
|
|
38
|
+
trackNorm[2]!,
|
|
39
|
+
trackNorm[3]!,
|
|
40
|
+
);
|
|
41
|
+
},
|
|
42
|
+
getCacheMarkers(props: RadialProgressProps) {
|
|
43
|
+
return `colors:${props.colors.length}|cap:${props.cap}|track:${
|
|
44
|
+
props.trackColor !== 0 ? 1 : 0
|
|
45
|
+
}`;
|
|
46
|
+
},
|
|
47
|
+
fragment(renderer: WebGlRenderer, props: RadialProgressProps) {
|
|
48
|
+
const maxStops = Math.max(props.colors.length, 1);
|
|
49
|
+
return `
|
|
50
|
+
# ifdef GL_FRAGMENT_PRECISION_HIGH
|
|
51
|
+
precision highp float;
|
|
52
|
+
# else
|
|
53
|
+
precision mediump float;
|
|
54
|
+
# endif
|
|
55
|
+
|
|
56
|
+
#define MAX_STOPS ${maxStops}
|
|
57
|
+
#define LAST_STOP ${maxStops - 1}
|
|
58
|
+
#define CAP_ROUND ${props.cap}
|
|
59
|
+
#define HAS_TRACK ${props.trackColor !== 0 ? 1 : 0}
|
|
60
|
+
|
|
61
|
+
#define TWO_PI 6.28318530717958647692
|
|
62
|
+
|
|
63
|
+
uniform float u_alpha;
|
|
64
|
+
uniform vec2 u_dimensions;
|
|
65
|
+
uniform sampler2D u_texture;
|
|
66
|
+
|
|
67
|
+
uniform vec2 u_center;
|
|
68
|
+
uniform float u_radius;
|
|
69
|
+
uniform float u_width;
|
|
70
|
+
uniform float u_progress;
|
|
71
|
+
uniform float u_startAngle;
|
|
72
|
+
uniform float u_direction;
|
|
73
|
+
|
|
74
|
+
uniform float u_stops[MAX_STOPS];
|
|
75
|
+
uniform vec4 u_colors[MAX_STOPS];
|
|
76
|
+
uniform vec4 u_trackColor;
|
|
77
|
+
|
|
78
|
+
varying vec4 v_color;
|
|
79
|
+
varying vec2 v_textureCoords;
|
|
80
|
+
varying vec2 v_nodeCoords;
|
|
81
|
+
|
|
82
|
+
vec4 getGradientColor(float dist) {
|
|
83
|
+
dist = clamp(dist, 0.0, 1.0);
|
|
84
|
+
|
|
85
|
+
if (dist <= u_stops[0]) {
|
|
86
|
+
return u_colors[0];
|
|
87
|
+
}
|
|
88
|
+
if (dist >= u_stops[LAST_STOP]) {
|
|
89
|
+
return u_colors[LAST_STOP];
|
|
90
|
+
}
|
|
91
|
+
for (int i = 0; i < LAST_STOP; i++) {
|
|
92
|
+
float left = u_stops[i];
|
|
93
|
+
float right = u_stops[i + 1];
|
|
94
|
+
if (dist >= left && dist <= right) {
|
|
95
|
+
float lDist = smoothstep(left, right, dist);
|
|
96
|
+
return mix(u_colors[i], u_colors[i + 1], lDist);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return u_colors[LAST_STOP];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Coverage of a disc centered at \`c\` with radius \`r\` at pixel \`p\` (with 1px AA)
|
|
103
|
+
float discCoverage(vec2 p, vec2 c, float r) {
|
|
104
|
+
return 1.0 - smoothstep(r - 1.0, r + 1.0, length(p - c));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
void main() {
|
|
108
|
+
vec4 base = texture2D(u_texture, v_textureCoords) * v_color;
|
|
109
|
+
|
|
110
|
+
vec2 p = v_nodeCoords.xy * u_dimensions - u_center;
|
|
111
|
+
float dist = length(p);
|
|
112
|
+
float halfW = u_width * 0.5;
|
|
113
|
+
|
|
114
|
+
// Ring coverage: 1 inside the stroke band, 0 outside (with 1px AA on both edges)
|
|
115
|
+
float ringCoverage =
|
|
116
|
+
smoothstep(u_radius - halfW - 1.0, u_radius - halfW + 1.0, dist) *
|
|
117
|
+
(1.0 - smoothstep(u_radius + halfW - 1.0, u_radius + halfW + 1.0, dist));
|
|
118
|
+
|
|
119
|
+
// Angle along the arc, normalized to [0, 1) starting at u_startAngle
|
|
120
|
+
float ang = atan(p.y, p.x);
|
|
121
|
+
float t = mod((ang - u_startAngle) * u_direction, TWO_PI) / TWO_PI;
|
|
122
|
+
|
|
123
|
+
// Filled arc coverage (1 if in filled arc, else 0). When progress >= 1 the
|
|
124
|
+
// whole ring is filled regardless of \`t\` -- guards against the mod() seam.
|
|
125
|
+
float arcCoverage = u_progress >= 1.0 ? 1.0 : step(t, u_progress);
|
|
126
|
+
float fillCoverage = ringCoverage * arcCoverage;
|
|
127
|
+
|
|
128
|
+
#if CAP_ROUND
|
|
129
|
+
// Round caps: discs of radius halfW at the start and head of the arc
|
|
130
|
+
float a0 = u_startAngle;
|
|
131
|
+
float a1 = u_startAngle + u_direction * u_progress * TWO_PI;
|
|
132
|
+
vec2 cap0 = vec2(cos(a0), sin(a0)) * u_radius;
|
|
133
|
+
vec2 cap1 = vec2(cos(a1), sin(a1)) * u_radius;
|
|
134
|
+
float capMask = max(discCoverage(p, cap0, halfW), discCoverage(p, cap1, halfW));
|
|
135
|
+
// Caps only visible when there's something to cap (progress > 0 and < 1).
|
|
136
|
+
float capGate = step(0.0001, u_progress) * step(u_progress, 0.9999);
|
|
137
|
+
fillCoverage = max(fillCoverage, capMask * capGate);
|
|
138
|
+
#endif
|
|
139
|
+
|
|
140
|
+
// Sample gradient. Normalize \`t\` to the *filled* portion so the gradient
|
|
141
|
+
// spans the visible arc end-to-end regardless of progress.
|
|
142
|
+
float gradT = u_progress > 0.0 ? clamp(t / u_progress, 0.0, 1.0) : 0.0;
|
|
143
|
+
vec4 fillCol = getGradientColor(gradT);
|
|
144
|
+
|
|
145
|
+
// Composite: track under fill (if track enabled), both gated by ringCoverage
|
|
146
|
+
vec4 layer = vec4(0.0);
|
|
147
|
+
#if HAS_TRACK
|
|
148
|
+
float trackCoverage = ringCoverage * (1.0 - fillCoverage);
|
|
149
|
+
layer = u_trackColor * trackCoverage + fillCol * fillCoverage;
|
|
150
|
+
#else
|
|
151
|
+
layer = fillCol * fillCoverage;
|
|
152
|
+
#endif
|
|
153
|
+
|
|
154
|
+
// Composite layer over base. Output alpha = base.a + layer.a*(1-base.a)
|
|
155
|
+
// so the ring is visible even when the node's base color is fully transparent.
|
|
156
|
+
float la = clamp(layer.a, 0.0, 1.0);
|
|
157
|
+
vec3 blended = mix(base.rgb, layer.rgb, la);
|
|
158
|
+
float outA = base.a + la * (1.0 - base.a);
|
|
159
|
+
gl_FragColor = vec4(blended, outA);
|
|
160
|
+
}
|
|
161
|
+
`;
|
|
162
|
+
},
|
|
163
|
+
};
|
|
@@ -87,6 +87,7 @@ const renderText = (props: CoreTextNodeProps): TextRenderInfo => {
|
|
|
87
87
|
return {
|
|
88
88
|
width: 0,
|
|
89
89
|
height: 0,
|
|
90
|
+
trimmedHeight: 0,
|
|
90
91
|
};
|
|
91
92
|
}
|
|
92
93
|
|
|
@@ -109,7 +110,10 @@ const renderText = (props: CoreTextNodeProps): TextRenderInfo => {
|
|
|
109
110
|
const font = `${fontStyle} ${fontSize}px Unknown, ${fontFamily}`;
|
|
110
111
|
// Get font metrics and calculate line height
|
|
111
112
|
measureContext.font = font;
|
|
112
|
-
|
|
113
|
+
// The layout engine emits line[4] as the alphabetic baseline Y, matching
|
|
114
|
+
// CSS line box layout. Both contexts must use 'alphabetic' so fillText draws
|
|
115
|
+
// the baseline exactly at line[4].
|
|
116
|
+
measureContext.textBaseline = 'alphabetic';
|
|
113
117
|
|
|
114
118
|
const metrics = CanvasFontHandler.getFontMetrics(fontFamily, fontSize);
|
|
115
119
|
|
|
@@ -150,7 +154,7 @@ const renderText = (props: CoreTextNodeProps): TextRenderInfo => {
|
|
|
150
154
|
const a = color & 0xff;
|
|
151
155
|
context.fillStyle = `rgba(${r},${g},${b},${a / 255})`;
|
|
152
156
|
context.font = font;
|
|
153
|
-
context.textBaseline = '
|
|
157
|
+
context.textBaseline = 'alphabetic';
|
|
154
158
|
|
|
155
159
|
// Performance optimization for large fonts
|
|
156
160
|
if (fontSize >= 128) {
|
|
@@ -188,10 +192,18 @@ const renderText = (props: CoreTextNodeProps): TextRenderInfo => {
|
|
|
188
192
|
if (canvas.width > 0 && canvas.height > 0) {
|
|
189
193
|
imageData = context.getImageData(0, 0, canvasW, canvasH);
|
|
190
194
|
}
|
|
195
|
+
// Cap-top of first line to descender bottom of last line.
|
|
196
|
+
// descender is negative in NormalizedFontMetrics.
|
|
197
|
+
const trimmedHeight =
|
|
198
|
+
lineAmount > 0
|
|
199
|
+
? metrics.capHeight - metrics.descender + (lineAmount - 1) * lineHeightPx
|
|
200
|
+
: 0;
|
|
201
|
+
|
|
191
202
|
return {
|
|
192
203
|
imageData,
|
|
193
204
|
width: effectiveWidth,
|
|
194
205
|
height: effectiveHeight,
|
|
206
|
+
trimmedHeight,
|
|
195
207
|
remainingLines,
|
|
196
208
|
hasRemainingText,
|
|
197
209
|
};
|
|
@@ -229,6 +229,43 @@ const processFontData = (
|
|
|
229
229
|
unitsPerEm: 1000,
|
|
230
230
|
};
|
|
231
231
|
|
|
232
|
+
// Derive cap-height from the atlas when the metrics block doesn't already
|
|
233
|
+
// supply it. The layout engine uses this value to vertically center
|
|
234
|
+
// capital letters on each line. BMFont stores per-glyph `yoffset` as the
|
|
235
|
+
// distance from the line-box top to the glyph's top, and `common.base` as
|
|
236
|
+
// the distance from the line-box top to the alphabetic baseline — so the
|
|
237
|
+
// distance from the baseline up to the top of 'H' (atlas design px) is
|
|
238
|
+
// `common.base - H.yoffset`. Converted into font units it slots into
|
|
239
|
+
// `FontMetrics.capHeight` alongside the existing ascender / descender
|
|
240
|
+
// values and flows through `normalizeFontMetrics`.
|
|
241
|
+
if (metrics.capHeight === undefined) {
|
|
242
|
+
const capGlyph = glyphMap.get(72); // 'H'
|
|
243
|
+
if (capGlyph !== undefined) {
|
|
244
|
+
const capHeightAtlasPx = fontData.common.base - capGlyph.yoffset;
|
|
245
|
+
metrics = {
|
|
246
|
+
...metrics,
|
|
247
|
+
capHeight: (capHeightAtlasPx / fontData.info.size) * metrics.unitsPerEm,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
// If 'H' isn't in the atlas (icon-only fonts, etc.) we leave capHeight
|
|
251
|
+
// undefined and rely on the 0.7 × ascender fallback inside
|
|
252
|
+
// normalizeFontMetrics.
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Same derivation for x-height using glyph 'x' (id 120). Only consumed
|
|
256
|
+
// when `RendererMainSettings.textBaselineMode === 'x'`; otherwise the
|
|
257
|
+
// 0.5 × ascender fallback inside `normalizeFontMetrics` is sufficient.
|
|
258
|
+
if (metrics.xHeight === undefined) {
|
|
259
|
+
const xGlyph = glyphMap.get(120); // 'x'
|
|
260
|
+
if (xGlyph !== undefined) {
|
|
261
|
+
const xHeightAtlasPx = fontData.common.base - xGlyph.yoffset;
|
|
262
|
+
metrics = {
|
|
263
|
+
...metrics,
|
|
264
|
+
xHeight: (xHeightAtlasPx / fontData.info.size) * metrics.unitsPerEm,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
232
269
|
// Cache processed data
|
|
233
270
|
fontCache.set(fontFamily, {
|
|
234
271
|
data: fontData,
|