@solidtv/renderer 1.5.4 → 1.5.5
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/index.d.ts +1 -0
- package/dist/exports/index.js +1 -0
- package/dist/exports/index.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/core/CoreShaderManager.js +16 -2
- package/dist/src/core/CoreShaderManager.js.map +1 -1
- package/dist/src/core/lib/WebGlContextWrapper.d.ts +13 -0
- package/dist/src/core/lib/WebGlContextWrapper.js +15 -0
- package/dist/src/core/lib/WebGlContextWrapper.js.map +1 -1
- package/dist/src/core/renderers/canvas/CanvasRenderer.d.ts +10 -0
- package/dist/src/core/renderers/canvas/CanvasRenderer.js +19 -0
- package/dist/src/core/renderers/canvas/CanvasRenderer.js.map +1 -1
- package/dist/src/core/renderers/webgl/WebGlRenderer.js +14 -1
- package/dist/src/core/renderers/webgl/WebGlRenderer.js.map +1 -1
- package/dist/src/core/shaders/canvas/EdgeFade.d.ts +10 -0
- package/dist/src/core/shaders/canvas/EdgeFade.js +92 -0
- package/dist/src/core/shaders/canvas/EdgeFade.js.map +1 -0
- package/dist/src/core/shaders/canvas/LinearGradient.js +1 -18
- package/dist/src/core/shaders/canvas/LinearGradient.js.map +1 -1
- package/dist/src/core/shaders/templates/EdgeFadeTemplate.d.ts +32 -0
- package/dist/src/core/shaders/templates/EdgeFadeTemplate.js +9 -0
- package/dist/src/core/shaders/templates/EdgeFadeTemplate.js.map +1 -0
- package/dist/src/core/shaders/webgl/EdgeFade.d.ts +9 -0
- package/dist/src/core/shaders/webgl/EdgeFade.js +74 -0
- package/dist/src/core/shaders/webgl/EdgeFade.js.map +1 -0
- package/dist/src/core/shaders/webgl/LinearGradient.js +31 -51
- package/dist/src/core/shaders/webgl/LinearGradient.js.map +1 -1
- package/dist/src/core/shaders/webgl/RadialGradient.js +2 -2
- package/dist/src/core/shaders/webgl/RadialProgress.js +7 -0
- package/dist/src/core/shaders/webgl/RadialProgress.js.map +1 -1
- package/dist/src/core/text-rendering/SdfFontHandler.d.ts +1 -0
- package/dist/src/core/text-rendering/SdfFontHandler.js +88 -51
- package/dist/src/core/text-rendering/SdfFontHandler.js.map +1 -1
- package/dist/src/core/textures/ImageTexture.js +2 -1
- package/dist/src/core/textures/ImageTexture.js.map +1 -1
- package/dist/tsconfig.dist.tsbuildinfo +1 -1
- package/exports/canvas-shaders.ts +1 -0
- package/exports/index.ts +1 -0
- package/exports/webgl-shaders.ts +1 -0
- package/package.json +1 -1
- package/src/core/CoreShaderManager.contextLoss.test.ts +72 -0
- package/src/core/CoreShaderManager.ts +15 -2
- package/src/core/lib/WebGlContextWrapper.ts +16 -0
- package/src/core/renderers/canvas/CanvasRenderer.ts +20 -0
- package/src/core/renderers/webgl/WebGlRenderer.contextLoss.test.ts +49 -0
- package/src/core/renderers/webgl/WebGlRenderer.ts +13 -1
- package/src/core/shaders/canvas/EdgeFade.ts +105 -0
- package/src/core/shaders/canvas/LinearGradient.test.ts +45 -0
- package/src/core/shaders/canvas/LinearGradient.ts +1 -18
- package/src/core/shaders/templates/EdgeFadeTemplate.test.ts +35 -0
- package/src/core/shaders/templates/EdgeFadeTemplate.ts +41 -0
- package/src/core/shaders/webgl/EdgeFade.ts +84 -0
- package/src/core/shaders/webgl/LinearGradient.test.ts +70 -0
- package/src/core/shaders/webgl/LinearGradient.ts +36 -51
- package/src/core/shaders/webgl/RadialGradient.ts +2 -2
- package/src/core/shaders/webgl/RadialProgress.ts +7 -0
- package/src/core/text-rendering/SdfFontHandler.ts +77 -30
- package/src/core/text-rendering/tests/SdfFontHandler.test.ts +181 -35
- package/src/core/textures/ImageTexture.test.ts +91 -0
- package/src/core/textures/ImageTexture.ts +4 -1
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { CanvasShaderType } from '../../renderers/canvas/CanvasShaderNode.js';
|
|
2
|
+
import type { CanvasRenderer } from '../../renderers/canvas/CanvasRenderer.js';
|
|
3
|
+
import {
|
|
4
|
+
EdgeFadeTemplate,
|
|
5
|
+
type EdgeFadeProps,
|
|
6
|
+
} from '../templates/EdgeFadeTemplate.js';
|
|
7
|
+
|
|
8
|
+
// Shared scratch canvas, grown on demand and reused across all EdgeFade nodes.
|
|
9
|
+
// Rendering is single-threaded and non-reentrant so one scratch is enough.
|
|
10
|
+
let scratchCanvas: HTMLCanvasElement | null = null;
|
|
11
|
+
let scratchCtx: CanvasRenderingContext2D | null = null;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Canvas2D implementation of {@link EdgeFade}: the node's content is drawn
|
|
15
|
+
* into an offscreen canvas, the edge ramps are erased with destination-out
|
|
16
|
+
* gradients, and the result is composited onto the main canvas. This keeps
|
|
17
|
+
* the background behind the node intact, which a destination-out pass on the
|
|
18
|
+
* main canvas could not.
|
|
19
|
+
*/
|
|
20
|
+
export const EdgeFade: CanvasShaderType<EdgeFadeProps> = {
|
|
21
|
+
props: EdgeFadeTemplate.props,
|
|
22
|
+
render(ctx, node, renderContext) {
|
|
23
|
+
const props = this.props!;
|
|
24
|
+
const left = props.left;
|
|
25
|
+
const top = props.top;
|
|
26
|
+
const right = props.right;
|
|
27
|
+
const bottom = props.bottom;
|
|
28
|
+
|
|
29
|
+
if (left <= 0 && top <= 0 && right <= 0 && bottom <= 0) {
|
|
30
|
+
renderContext();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const w = node.props.w;
|
|
35
|
+
const h = node.props.h;
|
|
36
|
+
if (w <= 0 || h <= 0) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const pr = this.stage.pixelRatio;
|
|
41
|
+
const sw = w * pr;
|
|
42
|
+
const sh = h * pr;
|
|
43
|
+
|
|
44
|
+
if (scratchCanvas === null) {
|
|
45
|
+
scratchCanvas = document.createElement('canvas');
|
|
46
|
+
scratchCtx = scratchCanvas.getContext('2d') as CanvasRenderingContext2D;
|
|
47
|
+
}
|
|
48
|
+
const sctx = scratchCtx!;
|
|
49
|
+
|
|
50
|
+
if (scratchCanvas.width < sw || scratchCanvas.height < sh) {
|
|
51
|
+
// Growing the canvas implicitly clears it
|
|
52
|
+
scratchCanvas.width = Math.ceil(sw);
|
|
53
|
+
scratchCanvas.height = Math.ceil(sh);
|
|
54
|
+
} else {
|
|
55
|
+
sctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
56
|
+
sctx.clearRect(0, 0, sw, sh);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Draw the node's content at the scratch origin
|
|
60
|
+
const tx = node.globalTransform!.tx;
|
|
61
|
+
const ty = node.globalTransform!.ty;
|
|
62
|
+
sctx.setTransform(pr, 0, 0, pr, -tx * pr, -ty * pr);
|
|
63
|
+
(this.stage.renderer as CanvasRenderer).renderNodeContent(node, sctx);
|
|
64
|
+
sctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
65
|
+
|
|
66
|
+
// Erase each edge with a linear ramp. Sequential destination-out passes
|
|
67
|
+
// multiply: dst *= (1 - g), matching the WebGL ramp product.
|
|
68
|
+
sctx.globalCompositeOperation = 'destination-out';
|
|
69
|
+
if (left > 0) {
|
|
70
|
+
const d = left * pr;
|
|
71
|
+
const g = sctx.createLinearGradient(0, 0, d, 0);
|
|
72
|
+
g.addColorStop(0, 'rgba(0,0,0,1)');
|
|
73
|
+
g.addColorStop(1, 'rgba(0,0,0,0)');
|
|
74
|
+
sctx.fillStyle = g;
|
|
75
|
+
sctx.fillRect(0, 0, d, sh);
|
|
76
|
+
}
|
|
77
|
+
if (top > 0) {
|
|
78
|
+
const d = top * pr;
|
|
79
|
+
const g = sctx.createLinearGradient(0, 0, 0, d);
|
|
80
|
+
g.addColorStop(0, 'rgba(0,0,0,1)');
|
|
81
|
+
g.addColorStop(1, 'rgba(0,0,0,0)');
|
|
82
|
+
sctx.fillStyle = g;
|
|
83
|
+
sctx.fillRect(0, 0, sw, d);
|
|
84
|
+
}
|
|
85
|
+
if (right > 0) {
|
|
86
|
+
const d = right * pr;
|
|
87
|
+
const g = sctx.createLinearGradient(sw - d, 0, sw, 0);
|
|
88
|
+
g.addColorStop(0, 'rgba(0,0,0,0)');
|
|
89
|
+
g.addColorStop(1, 'rgba(0,0,0,1)');
|
|
90
|
+
sctx.fillStyle = g;
|
|
91
|
+
sctx.fillRect(sw - d, 0, d, sh);
|
|
92
|
+
}
|
|
93
|
+
if (bottom > 0) {
|
|
94
|
+
const d = bottom * pr;
|
|
95
|
+
const g = sctx.createLinearGradient(0, sh - d, 0, sh);
|
|
96
|
+
g.addColorStop(0, 'rgba(0,0,0,0)');
|
|
97
|
+
g.addColorStop(1, 'rgba(0,0,0,1)');
|
|
98
|
+
sctx.fillStyle = g;
|
|
99
|
+
sctx.fillRect(0, sh - d, sw, d);
|
|
100
|
+
}
|
|
101
|
+
sctx.globalCompositeOperation = 'source-over';
|
|
102
|
+
|
|
103
|
+
ctx.drawImage(scratchCanvas, 0, 0, sw, sh, tx, ty, w, h);
|
|
104
|
+
},
|
|
105
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { LinearGradient } from './LinearGradient.js';
|
|
3
|
+
import { normalizeCanvasColor } from '../../lib/colorCache.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Invoke the canvas LinearGradient `update()` with a minimal fake shader node
|
|
7
|
+
* and return the computed CSS color strings.
|
|
8
|
+
*/
|
|
9
|
+
function computeColors(colors: number[]): string[] {
|
|
10
|
+
const ctx = {
|
|
11
|
+
props: { colors, stops: [0, 1], angle: 0 },
|
|
12
|
+
computed: undefined as unknown,
|
|
13
|
+
toColorString: (value: number) => normalizeCanvasColor(value, true),
|
|
14
|
+
};
|
|
15
|
+
// Colors are RGBA encoded (alpha in the low byte) — see parseToRgbaString.
|
|
16
|
+
LinearGradient.update!.call(ctx as never, { w: 100, h: 100 } as never);
|
|
17
|
+
return (ctx.computed as { colors: string[] }).colors;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('canvas LinearGradient color mapping', () => {
|
|
21
|
+
it('preserves per-stop alpha (no opaque fallback)', () => {
|
|
22
|
+
// 0x00ff0080 is a half-transparent green stop. The previous
|
|
23
|
+
// "nearest opaque RGB" workaround mis-read the high byte as alpha
|
|
24
|
+
// (it is actually red in RGBA) and overwrote the low byte, turning
|
|
25
|
+
// transparent stops solid. The faithful mapping must keep alpha ~0.5.
|
|
26
|
+
const [opaqueRed, halfGreen] = computeColors([0xff0000ff, 0x00ff0080]);
|
|
27
|
+
|
|
28
|
+
expect(opaqueRed).toBe('rgba(255,0,0,1)');
|
|
29
|
+
expect(halfGreen).toBe(`rgba(0,255,0,${0x80 / 255})`);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('keeps fully transparent stops transparent', () => {
|
|
33
|
+
const [, transparent] = computeColors([0x000000ff, 0x00000000]);
|
|
34
|
+
expect(transparent).toBe('rgba(0,0,0,0)');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('maps every stop straight through toColorString', () => {
|
|
38
|
+
const colors = [0xff0000ff, 0x00ff00ff, 0x0000ffff];
|
|
39
|
+
expect(computeColors(colors)).toEqual([
|
|
40
|
+
'rgba(255,0,0,1)',
|
|
41
|
+
'rgba(0,255,0,1)',
|
|
42
|
+
'rgba(0,0,255,1)',
|
|
43
|
+
]);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -31,24 +31,7 @@ export const LinearGradient: CanvasShaderType<
|
|
|
31
31
|
y0: line * Math.sin(angle) + nHeight * 0.5,
|
|
32
32
|
x1: line * Math.cos(angle + Math.PI) + nWidth * 0.5,
|
|
33
33
|
y1: line * Math.sin(angle + Math.PI) + nHeight * 0.5,
|
|
34
|
-
colors: this.props!.colors.map((value
|
|
35
|
-
const alpha = (value >>> 24) & 0xff;
|
|
36
|
-
if (alpha === 0) {
|
|
37
|
-
let nearestRGB = value & 0x00ffffff;
|
|
38
|
-
for (let step = 1; step < arr.length; step++) {
|
|
39
|
-
if (i - step >= 0 && ((arr[i - step]! >>> 24) & 0xff) > 0) {
|
|
40
|
-
nearestRGB = arr[i - step]! & 0x00ffffff;
|
|
41
|
-
break;
|
|
42
|
-
}
|
|
43
|
-
if (i + step < arr.length && ((arr[i + step]! >>> 24) & 0xff) > 0) {
|
|
44
|
-
nearestRGB = arr[i + step]! & 0x00ffffff;
|
|
45
|
-
break;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
value = (value & 0xff000000) | nearestRGB;
|
|
49
|
-
}
|
|
50
|
-
return this.toColorString(value);
|
|
51
|
-
}),
|
|
34
|
+
colors: this.props!.colors.map((value) => this.toColorString(value)),
|
|
52
35
|
};
|
|
53
36
|
},
|
|
54
37
|
render(ctx, node, renderContext) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { EdgeFadeTemplate, type EdgeFadeProps } from './EdgeFadeTemplate.js';
|
|
3
|
+
import { resolveShaderProps } from '../../renderers/CoreShaderNode.js';
|
|
4
|
+
|
|
5
|
+
function resolve(input: Partial<EdgeFadeProps>): EdgeFadeProps {
|
|
6
|
+
const props = { ...input } as Record<string, unknown>;
|
|
7
|
+
resolveShaderProps(props, EdgeFadeTemplate.props as never);
|
|
8
|
+
return props as unknown as EdgeFadeProps;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe('EdgeFadeTemplate', () => {
|
|
12
|
+
it('defaults all edges to 0 (no fade)', () => {
|
|
13
|
+
const props = resolve({});
|
|
14
|
+
expect(props.left).toBe(0);
|
|
15
|
+
expect(props.top).toBe(0);
|
|
16
|
+
expect(props.right).toBe(0);
|
|
17
|
+
expect(props.bottom).toBe(0);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('passes through a single edge, leaving others at 0', () => {
|
|
21
|
+
const props = resolve({ right: 420 });
|
|
22
|
+
expect(props.right).toBe(420);
|
|
23
|
+
expect(props.left).toBe(0);
|
|
24
|
+
expect(props.top).toBe(0);
|
|
25
|
+
expect(props.bottom).toBe(0);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('passes through all edges', () => {
|
|
29
|
+
const props = resolve({ left: 10, top: 20, right: 30, bottom: 40 });
|
|
30
|
+
expect(props.left).toBe(10);
|
|
31
|
+
expect(props.top).toBe(20);
|
|
32
|
+
expect(props.right).toBe(30);
|
|
33
|
+
expect(props.bottom).toBe(40);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { CoreShaderType } from '../../renderers/CoreShaderNode.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Properties of the {@link EdgeFade} shader
|
|
5
|
+
*/
|
|
6
|
+
export interface EdgeFadeProps {
|
|
7
|
+
/**
|
|
8
|
+
* Fade distance in pixels from the left edge. Alpha ramps 0 → 1 over this
|
|
9
|
+
* distance. 0 disables the fade for this edge.
|
|
10
|
+
*
|
|
11
|
+
* @default 0
|
|
12
|
+
*/
|
|
13
|
+
left: number;
|
|
14
|
+
/**
|
|
15
|
+
* Fade distance in pixels from the top edge.
|
|
16
|
+
*
|
|
17
|
+
* @default 0
|
|
18
|
+
*/
|
|
19
|
+
top: number;
|
|
20
|
+
/**
|
|
21
|
+
* Fade distance in pixels from the right edge.
|
|
22
|
+
*
|
|
23
|
+
* @default 0
|
|
24
|
+
*/
|
|
25
|
+
right: number;
|
|
26
|
+
/**
|
|
27
|
+
* Fade distance in pixels from the bottom edge.
|
|
28
|
+
*
|
|
29
|
+
* @default 0
|
|
30
|
+
*/
|
|
31
|
+
bottom: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const EdgeFadeTemplate: CoreShaderType<EdgeFadeProps> = {
|
|
35
|
+
props: {
|
|
36
|
+
left: 0,
|
|
37
|
+
top: 0,
|
|
38
|
+
right: 0,
|
|
39
|
+
bottom: 0,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { WebGlShaderType } from '../../renderers/webgl/WebGlShaderNode.js';
|
|
2
|
+
import {
|
|
3
|
+
EdgeFadeTemplate,
|
|
4
|
+
type EdgeFadeProps,
|
|
5
|
+
} from '../templates/EdgeFadeTemplate.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Multiplies the node's alpha by a linear ramp inward from each edge with a
|
|
9
|
+
* non-zero fade distance, revealing whatever is rendered behind the node.
|
|
10
|
+
* Unlike {@link LinearGradient} this masks the texture's own alpha instead of
|
|
11
|
+
* blending a gradient color over its RGB.
|
|
12
|
+
*/
|
|
13
|
+
export const EdgeFade: WebGlShaderType<EdgeFadeProps> = {
|
|
14
|
+
props: EdgeFadeTemplate.props,
|
|
15
|
+
update() {
|
|
16
|
+
const props = this.props!;
|
|
17
|
+
// Reciprocals are uploaded so the fragment shader needs no division and
|
|
18
|
+
// no zero-guard branch: 1e6 saturates clamp(px * recip) to 1.0 within a
|
|
19
|
+
// fraction of a pixel, which is exactly "no fade on this edge".
|
|
20
|
+
this.uniform4f(
|
|
21
|
+
'u_fadeRecip',
|
|
22
|
+
props.left > 0 ? 1 / props.left : 1e6,
|
|
23
|
+
props.top > 0 ? 1 / props.top : 1e6,
|
|
24
|
+
props.right > 0 ? 1 / props.right : 1e6,
|
|
25
|
+
props.bottom > 0 ? 1 / props.bottom : 1e6,
|
|
26
|
+
);
|
|
27
|
+
},
|
|
28
|
+
vertex: `
|
|
29
|
+
# ifdef GL_FRAGMENT_PRECISION_HIGH
|
|
30
|
+
precision highp float;
|
|
31
|
+
# else
|
|
32
|
+
precision mediump float;
|
|
33
|
+
# endif
|
|
34
|
+
|
|
35
|
+
attribute vec2 a_position;
|
|
36
|
+
attribute vec2 a_textureCoords;
|
|
37
|
+
attribute vec4 a_color;
|
|
38
|
+
attribute vec2 a_nodeCoords;
|
|
39
|
+
|
|
40
|
+
uniform vec2 u_resolution;
|
|
41
|
+
uniform float u_pixelRatio;
|
|
42
|
+
|
|
43
|
+
varying vec4 v_color;
|
|
44
|
+
varying vec2 v_textureCoords;
|
|
45
|
+
varying vec2 v_nodeCoords;
|
|
46
|
+
|
|
47
|
+
void main() {
|
|
48
|
+
vec2 normalized = a_position * u_pixelRatio / u_resolution;
|
|
49
|
+
vec2 zero_two = normalized * 2.0;
|
|
50
|
+
vec2 clip_space = zero_two - 1.0;
|
|
51
|
+
|
|
52
|
+
v_color = a_color;
|
|
53
|
+
v_textureCoords = a_textureCoords;
|
|
54
|
+
v_nodeCoords = a_nodeCoords;
|
|
55
|
+
|
|
56
|
+
gl_Position = vec4(clip_space * vec2(1.0, -1.0), 0, 1);
|
|
57
|
+
}
|
|
58
|
+
`,
|
|
59
|
+
fragment: `
|
|
60
|
+
# ifdef GL_FRAGMENT_PRECISION_HIGH
|
|
61
|
+
precision highp float;
|
|
62
|
+
# else
|
|
63
|
+
precision mediump float;
|
|
64
|
+
# endif
|
|
65
|
+
|
|
66
|
+
uniform vec2 u_dimensions;
|
|
67
|
+
uniform sampler2D u_texture;
|
|
68
|
+
uniform vec4 u_fadeRecip;
|
|
69
|
+
|
|
70
|
+
varying vec4 v_color;
|
|
71
|
+
varying vec2 v_textureCoords;
|
|
72
|
+
varying vec2 v_nodeCoords;
|
|
73
|
+
|
|
74
|
+
void main() {
|
|
75
|
+
vec4 color = v_color * texture2D(u_texture, v_textureCoords);
|
|
76
|
+
vec2 px = v_nodeCoords * u_dimensions;
|
|
77
|
+
float fade = clamp(px.x * u_fadeRecip.x, 0.0, 1.0)
|
|
78
|
+
* clamp(px.y * u_fadeRecip.y, 0.0, 1.0)
|
|
79
|
+
* clamp((u_dimensions.x - px.x) * u_fadeRecip.z, 0.0, 1.0)
|
|
80
|
+
* clamp((u_dimensions.y - px.y) * u_fadeRecip.w, 0.0, 1.0);
|
|
81
|
+
gl_FragColor = color * fade;
|
|
82
|
+
}
|
|
83
|
+
`,
|
|
84
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { LinearGradient } from './LinearGradient.js';
|
|
3
|
+
|
|
4
|
+
interface GradUniforms {
|
|
5
|
+
a: [number, number];
|
|
6
|
+
b: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Invoke the WebGL LinearGradient `update()` with a fake shader node that
|
|
11
|
+
* captures the gradient uniforms (`u_grad_a` / `u_grad_b`). The fragment
|
|
12
|
+
* shader computes `dist = dot(v_textureCoords, u_grad_a) + u_grad_b`, so these
|
|
13
|
+
* uniforms fully define the gradient ramp.
|
|
14
|
+
*/
|
|
15
|
+
function computeGrad(angle: number, w: number, h: number): GradUniforms {
|
|
16
|
+
const out: GradUniforms = { a: [0, 0], b: 0 };
|
|
17
|
+
const ctx = {
|
|
18
|
+
props: { colors: [0x000000ff, 0xffffffff], stops: [0, 1], angle },
|
|
19
|
+
uniform2f: (name: string, v0: number, v1: number) => {
|
|
20
|
+
if (name === 'u_grad_a') out.a = [v0, v1];
|
|
21
|
+
},
|
|
22
|
+
uniform1f: (name: string, v: number) => {
|
|
23
|
+
if (name === 'u_grad_b') out.b = v;
|
|
24
|
+
},
|
|
25
|
+
uniform1fv: () => undefined,
|
|
26
|
+
uniform4fv: () => undefined,
|
|
27
|
+
};
|
|
28
|
+
LinearGradient.update!.call(ctx as never, { w, h } as never);
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const distAt = (g: GradUniforms, tx: number, ty: number) =>
|
|
33
|
+
tx * g.a[0] + ty * g.a[1] + g.b;
|
|
34
|
+
|
|
35
|
+
describe('webgl LinearGradient gradient uniforms', () => {
|
|
36
|
+
it('angle 0 ramps top -> bottom across the node-local box', () => {
|
|
37
|
+
const g = computeGrad(0, 200, 100);
|
|
38
|
+
expect(g.a[0]).toBeCloseTo(0, 5);
|
|
39
|
+
expect(g.a[1]).toBeCloseTo(1, 5);
|
|
40
|
+
expect(g.b).toBeCloseTo(0, 5);
|
|
41
|
+
// dist runs 0 (top) -> 1 (bottom)
|
|
42
|
+
expect(distAt(g, 0.5, 0)).toBeCloseTo(0, 5);
|
|
43
|
+
expect(distAt(g, 0.5, 1)).toBeCloseTo(1, 5);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('angle 90deg ramps horizontally', () => {
|
|
47
|
+
const g = computeGrad(Math.PI / 2, 200, 100);
|
|
48
|
+
expect(g.a[0]).toBeCloseTo(-1, 5);
|
|
49
|
+
expect(g.a[1]).toBeCloseTo(0, 5);
|
|
50
|
+
expect(distAt(g, 0, 0.5)).toBeCloseTo(1, 5);
|
|
51
|
+
expect(distAt(g, 1, 0.5)).toBeCloseTo(0, 5);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('endpoints stay within [0,1] for an arbitrary angle', () => {
|
|
55
|
+
const g = computeGrad(Math.PI / 4, 640, 360);
|
|
56
|
+
// Box corners must map into the clampable [0,1] ramp range.
|
|
57
|
+
const corners = [
|
|
58
|
+
distAt(g, 0, 0),
|
|
59
|
+
distAt(g, 1, 0),
|
|
60
|
+
distAt(g, 0, 1),
|
|
61
|
+
distAt(g, 1, 1),
|
|
62
|
+
];
|
|
63
|
+
for (let i = 0; i < corners.length; i++) {
|
|
64
|
+
expect(corners[i]).toBeGreaterThanOrEqual(-1e-6);
|
|
65
|
+
expect(corners[i]).toBeLessThanOrEqual(1 + 1e-6);
|
|
66
|
+
}
|
|
67
|
+
// The gradient axis is symmetric about the box center -> dist = 0.5 there.
|
|
68
|
+
expect(distAt(g, 0.5, 0.5)).toBeCloseTo(0.5, 5);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CoreNode } from '../../CoreNode.js';
|
|
1
2
|
import { getNormalizedRgbaComponents } from '../../lib/utils.js';
|
|
2
3
|
import {
|
|
3
4
|
LinearGradientTemplate,
|
|
@@ -8,9 +9,33 @@ import type { WebGlShaderType } from '../../renderers/webgl/WebGlShaderNode.js';
|
|
|
8
9
|
|
|
9
10
|
export const LinearGradient: WebGlShaderType<LinearGradientProps> = {
|
|
10
11
|
props: LinearGradientTemplate.props,
|
|
11
|
-
update() {
|
|
12
|
+
update(node: CoreNode) {
|
|
12
13
|
const props = this.props!;
|
|
13
|
-
|
|
14
|
+
|
|
15
|
+
// The gradient distance is an affine function of the node-local texture
|
|
16
|
+
// coordinates, so it reduces to `dist = dot(v_textureCoords, a) + b`.
|
|
17
|
+
// `a`/`b` depend only on the angle and node dimensions (both are part of
|
|
18
|
+
// the value-key), so we compute them once on the CPU here instead of
|
|
19
|
+
// recomputing the trig per fragment on the GPU.
|
|
20
|
+
const angle = props.angle - (Math.PI / 180) * 90;
|
|
21
|
+
const c = Math.cos(angle);
|
|
22
|
+
const s = Math.sin(angle);
|
|
23
|
+
const w = node.w;
|
|
24
|
+
const h = node.h;
|
|
25
|
+
|
|
26
|
+
const lineDist = Math.abs(w * c) + Math.abs(h * s);
|
|
27
|
+
// Gradient axis (from -> to), gradVec = -lineDist * (c, s)
|
|
28
|
+
const gx = -lineDist * c;
|
|
29
|
+
const gy = -lineDist * s;
|
|
30
|
+
const gg = gx * gx + gy * gy;
|
|
31
|
+
const invGG = gg > 0 ? 1 / gg : 0;
|
|
32
|
+
// Gradient origin: f = lineDist * 0.5 * (c, s) + dimensions * 0.5
|
|
33
|
+
const fx = lineDist * 0.5 * c + w * 0.5;
|
|
34
|
+
const fy = lineDist * 0.5 * s + h * 0.5;
|
|
35
|
+
|
|
36
|
+
this.uniform2f('u_grad_a', w * gx * invGG, h * gy * invGG);
|
|
37
|
+
this.uniform1f('u_grad_b', -(fx * gx + fy * gy) * invGG);
|
|
38
|
+
|
|
14
39
|
this.uniform1fv('u_stops', new Float32Array(props.stops));
|
|
15
40
|
const colors: number[] = [];
|
|
16
41
|
for (let i = 0; i < props.colors.length; i++) {
|
|
@@ -22,51 +47,6 @@ export const LinearGradient: WebGlShaderType<LinearGradientProps> = {
|
|
|
22
47
|
getCacheMarkers(props: LinearGradientProps) {
|
|
23
48
|
return `colors:${props.colors.length}`;
|
|
24
49
|
},
|
|
25
|
-
vertex: `
|
|
26
|
-
# ifdef GL_FRAGMENT_PRECISION_HIGH
|
|
27
|
-
precision highp float;
|
|
28
|
-
# else
|
|
29
|
-
precision mediump float;
|
|
30
|
-
# endif
|
|
31
|
-
|
|
32
|
-
attribute vec2 a_position;
|
|
33
|
-
attribute vec2 a_textureCoords;
|
|
34
|
-
attribute vec4 a_color;
|
|
35
|
-
|
|
36
|
-
uniform vec2 u_resolution;
|
|
37
|
-
uniform float u_pixelRatio;
|
|
38
|
-
uniform vec2 u_dimensions;
|
|
39
|
-
uniform float u_angle;
|
|
40
|
-
|
|
41
|
-
varying vec4 v_color;
|
|
42
|
-
varying vec2 v_textureCoords;
|
|
43
|
-
varying float v_dist;
|
|
44
|
-
|
|
45
|
-
const float PI = 3.14159265359;
|
|
46
|
-
|
|
47
|
-
vec2 calcPoint(float d, float angle) {
|
|
48
|
-
return d * vec2(cos(angle), sin(angle)) + (u_dimensions * 0.5);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
void main() {
|
|
52
|
-
vec2 normalized = a_position * u_pixelRatio / u_resolution;
|
|
53
|
-
vec2 zero_two = normalized * 2.0;
|
|
54
|
-
vec2 clip_space = zero_two - 1.0;
|
|
55
|
-
|
|
56
|
-
gl_Position = vec4(clip_space * vec2(1.0, -1.0), 0, 1);
|
|
57
|
-
|
|
58
|
-
v_color = a_color;
|
|
59
|
-
v_textureCoords = a_textureCoords;
|
|
60
|
-
|
|
61
|
-
float a = u_angle;
|
|
62
|
-
float lineDist = abs(u_dimensions.x * cos(a)) + abs(u_dimensions.y * sin(a));
|
|
63
|
-
vec2 f = calcPoint(lineDist * 0.5, a);
|
|
64
|
-
vec2 t = calcPoint(lineDist * 0.5, a + PI);
|
|
65
|
-
vec2 gradVec = t - f;
|
|
66
|
-
float dist = dot(a_textureCoords * u_dimensions - f, gradVec) / dot(gradVec, gradVec);
|
|
67
|
-
v_dist = dist;
|
|
68
|
-
}
|
|
69
|
-
`,
|
|
70
50
|
fragment(renderer: WebGlRenderer, props: LinearGradientProps) {
|
|
71
51
|
return `
|
|
72
52
|
# ifdef GL_FRAGMENT_PRECISION_HIGH
|
|
@@ -78,13 +58,17 @@ export const LinearGradient: WebGlShaderType<LinearGradientProps> = {
|
|
|
78
58
|
#define MAX_STOPS ${props.colors.length}
|
|
79
59
|
#define LAST_STOP ${props.colors.length - 1}
|
|
80
60
|
|
|
61
|
+
uniform float u_alpha;
|
|
62
|
+
|
|
81
63
|
uniform sampler2D u_texture;
|
|
64
|
+
|
|
65
|
+
uniform vec2 u_grad_a;
|
|
66
|
+
uniform float u_grad_b;
|
|
82
67
|
uniform float u_stops[MAX_STOPS];
|
|
83
68
|
uniform vec4 u_colors[MAX_STOPS];
|
|
84
69
|
|
|
85
70
|
varying vec4 v_color;
|
|
86
71
|
varying vec2 v_textureCoords;
|
|
87
|
-
varying float v_dist;
|
|
88
72
|
|
|
89
73
|
vec4 getGradientColor(float dist) {
|
|
90
74
|
dist = clamp(dist, 0.0, 1.0);
|
|
@@ -110,9 +94,10 @@ export const LinearGradient: WebGlShaderType<LinearGradientProps> = {
|
|
|
110
94
|
|
|
111
95
|
void main() {
|
|
112
96
|
vec4 color = texture2D(u_texture, v_textureCoords) * v_color;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
97
|
+
float dist = dot(v_textureCoords, u_grad_a) + u_grad_b;
|
|
98
|
+
vec4 colorOut = getGradientColor(dist);
|
|
99
|
+
color = mix(color, colorOut, clamp(colorOut.a, 0.0, 1.0));
|
|
100
|
+
gl_FragColor = color * u_alpha;
|
|
116
101
|
}
|
|
117
102
|
`;
|
|
118
103
|
},
|
|
@@ -83,8 +83,8 @@ export const RadialGradient: WebGlShaderType<RadialGradientProps> = {
|
|
|
83
83
|
float dist = length((point - u_projection) / u_size);
|
|
84
84
|
|
|
85
85
|
vec4 colorOut = getGradientColor(dist);
|
|
86
|
-
|
|
87
|
-
gl_FragColor =
|
|
86
|
+
color = mix(color, colorOut, clamp(colorOut.a, 0.0, 1.0));
|
|
87
|
+
gl_FragColor = color * u_alpha;
|
|
88
88
|
}
|
|
89
89
|
`;
|
|
90
90
|
},
|
|
@@ -171,6 +171,13 @@ export const RadialProgress: WebGlShaderType<RadialProgressProps> = {
|
|
|
171
171
|
layer = fillPM * fillCoverage;
|
|
172
172
|
#endif
|
|
173
173
|
|
|
174
|
+
// Apply node opacity to the introduced ring/track colors. They come
|
|
175
|
+
// from u_colors / u_trackColor and do not carry worldAlpha, so without
|
|
176
|
+
// this a fading RadialProgress node would keep its ring fully opaque.
|
|
177
|
+
// \`base\` already includes worldAlpha (via v_color), so it is left as-is
|
|
178
|
+
// to avoid double-applying. Scaling a premultiplied layer is valid.
|
|
179
|
+
layer *= u_alpha;
|
|
180
|
+
|
|
174
181
|
// Premultiplied "over": out = src + dst*(1 - src.a). The output stays
|
|
175
182
|
// visible on a fully-transparent \`base\` because layer brings its own alpha.
|
|
176
183
|
float la = clamp(layer.a, 0.0, 1.0);
|