@solidtv/renderer 1.2.6 → 1.2.8

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.
Files changed (44) hide show
  1. package/dist/exports/canvas-shaders.d.ts +1 -0
  2. package/dist/exports/canvas-shaders.js +1 -0
  3. package/dist/exports/canvas-shaders.js.map +1 -1
  4. package/dist/exports/webgl-shaders.d.ts +1 -0
  5. package/dist/exports/webgl-shaders.js +1 -0
  6. package/dist/exports/webgl-shaders.js.map +1 -1
  7. package/dist/src/core/CoreTextureManager.d.ts +1 -0
  8. package/dist/src/core/CoreTextureManager.js +3 -0
  9. package/dist/src/core/CoreTextureManager.js.map +1 -1
  10. package/dist/src/core/lib/textureCompression.js +17 -5
  11. package/dist/src/core/lib/textureCompression.js.map +1 -1
  12. package/dist/src/core/lib/textureSvg.d.ts +14 -4
  13. package/dist/src/core/lib/textureSvg.js +61 -24
  14. package/dist/src/core/lib/textureSvg.js.map +1 -1
  15. package/dist/src/core/shaders/canvas/Blur.d.ts +11 -0
  16. package/dist/src/core/shaders/canvas/Blur.js +18 -0
  17. package/dist/src/core/shaders/canvas/Blur.js.map +1 -0
  18. package/dist/src/core/shaders/templates/BlurTemplate.d.ts +19 -0
  19. package/dist/src/core/shaders/templates/BlurTemplate.js +6 -0
  20. package/dist/src/core/shaders/templates/BlurTemplate.js.map +1 -0
  21. package/dist/src/core/shaders/webgl/Blur.d.ts +11 -0
  22. package/dist/src/core/shaders/webgl/Blur.js +46 -0
  23. package/dist/src/core/shaders/webgl/Blur.js.map +1 -0
  24. package/dist/src/core/text-rendering/SdfFontHandler.js +27 -6
  25. package/dist/src/core/text-rendering/SdfFontHandler.js.map +1 -1
  26. package/dist/src/core/textures/ImageTexture.js +2 -5
  27. package/dist/src/core/textures/ImageTexture.js.map +1 -1
  28. package/dist/src/main-api/Inspector.js +8 -1
  29. package/dist/src/main-api/Inspector.js.map +1 -1
  30. package/dist/tsconfig.dist.tsbuildinfo +1 -1
  31. package/exports/canvas-shaders.ts +1 -0
  32. package/exports/webgl-shaders.ts +1 -0
  33. package/package.json +1 -1
  34. package/src/core/CoreTextureManager.ts +4 -0
  35. package/src/core/lib/textureCompression.ts +24 -8
  36. package/src/core/lib/textureSvg.ts +67 -28
  37. package/src/core/shaders/canvas/Blur.ts +23 -0
  38. package/src/core/shaders/templates/BlurTemplate.test.ts +23 -0
  39. package/src/core/shaders/templates/BlurTemplate.ts +25 -0
  40. package/src/core/shaders/webgl/Blur.ts +47 -0
  41. package/src/core/text-rendering/SdfFontHandler.ts +29 -7
  42. package/src/core/textures/ImageTexture.ts +2 -13
  43. package/src/main-api/Inspector.ts +9 -2
  44. package/dist/tsconfig.tsbuildinfo +0 -1
@@ -1,5 +1,6 @@
1
1
  import { assertTruthy } from '../../utils.js';
2
2
  import { type TextureData } from '../textures/Texture.js';
3
+ import { isBase64Image } from './utils.js';
3
4
 
4
5
  /**
5
6
  * Tests if the given location is a SVG
@@ -14,11 +15,21 @@ export function isSvgImage(url: string): boolean {
14
15
  }
15
16
 
16
17
  /**
17
- * Loads a SVG image
18
- * @param url
19
- * @returns
18
+ * Loads a SVG image and rasterizes it for use as a texture.
19
+ *
20
+ * @remarks
21
+ * Rasterizes at `pixelRatio` to keep the texture sharp on HiDPI / 4K displays.
22
+ * `width`/`height` are interpreted as the logical (CSS-pixel) target size; the
23
+ * backing canvas is allocated at `width * pixelRatio` × `height * pixelRatio`.
24
+ *
25
+ * When `sw`/`sh` are provided they describe a source-region crop on the SVG
26
+ * (not a crop of the destination canvas) and are sampled via the 9-arg form of
27
+ * drawImage.
28
+ *
29
+ * Returns an `ImageBitmap` when available (zero CPU readback, transferable),
30
+ * falling back to `ImageData` on older browsers without `createImageBitmap`.
20
31
  */
21
- export const loadSvg = (
32
+ export const loadSvg = async (
22
33
  url: string,
23
34
  width: number | null,
24
35
  height: number | null,
@@ -26,34 +37,62 @@ export const loadSvg = (
26
37
  sy: number | null,
27
38
  sw: number | null,
28
39
  sh: number | null,
40
+ pixelRatio: number,
29
41
  ): Promise<TextureData> => {
30
- return new Promise((resolve, reject) => {
31
- const canvas = document.createElement('canvas');
32
- const ctx = canvas.getContext('2d');
33
- assertTruthy(ctx);
34
-
35
- ctx.imageSmoothingEnabled = true;
36
- const img = new Image();
37
- img.onload = () => {
38
- const x = sx ?? 0;
39
- const y = sy ?? 0;
40
- const w = width || img.width;
41
- const h = height || img.height;
42
-
43
- canvas.width = w;
44
- canvas.height = h;
45
- ctx.drawImage(img, 0, 0, w, h);
46
-
47
- resolve({
48
- data: ctx.getImageData(x, y, sw ?? w, sh ?? h),
49
- premultiplyAlpha: false,
50
- });
51
- };
42
+ const img = new Image();
43
+ if (isBase64Image(url) === false) {
44
+ img.crossOrigin = 'anonymous';
45
+ }
52
46
 
47
+ await new Promise<void>((resolve, reject) => {
48
+ img.onload = () => resolve();
53
49
  img.onerror = (err) => {
54
- reject(err);
50
+ reject(
51
+ err instanceof Error ? err : new Error(`SVG loading failed: ${url}`),
52
+ );
55
53
  };
56
-
57
54
  img.src = url;
58
55
  });
56
+
57
+ // Target size precedence: explicit w/h on the texture, then the source
58
+ // crop dims (so a node that only sets srcWidth/srcHeight gets a texture
59
+ // sized to the crop, matching pre-fix behavior), then the SVG's intrinsic
60
+ // dimensions.
61
+ const targetW = width || sw || img.naturalWidth || img.width;
62
+ const targetH = height || sh || img.naturalHeight || img.height;
63
+ // Clamp the DPR multiplier so a sub-1 stage pixelRatio (e.g. an app
64
+ // rendering at 720p inside a 1080p design grid) doesn't downscale the
65
+ // raster below the requested size. HiDPI upscaling still applies.
66
+ const ratio = pixelRatio > 1 ? pixelRatio : 1;
67
+ const physW = Math.max(1, Math.ceil(targetW * ratio));
68
+ const physH = Math.max(1, Math.ceil(targetH * ratio));
69
+
70
+ const canvas = document.createElement('canvas');
71
+ canvas.width = physW;
72
+ canvas.height = physH;
73
+ const ctx = canvas.getContext('2d');
74
+ assertTruthy(ctx);
75
+
76
+ if (sw !== null && sh !== null) {
77
+ ctx.drawImage(img, sx ?? 0, sy ?? 0, sw, sh, 0, 0, physW, physH);
78
+ } else {
79
+ ctx.drawImage(img, 0, 0, physW, physH);
80
+ }
81
+
82
+ if (typeof createImageBitmap === 'function') {
83
+ try {
84
+ const bitmap = await createImageBitmap(canvas);
85
+ return {
86
+ data: bitmap,
87
+ premultiplyAlpha: false,
88
+ };
89
+ } catch {
90
+ // fall through to ImageData
91
+ }
92
+ }
93
+
94
+ return {
95
+ data: ctx.getImageData(0, 0, physW, physH),
96
+ premultiplyAlpha: false,
97
+ };
59
98
  };
@@ -0,0 +1,23 @@
1
+ import type { CanvasShaderType } from '../../renderers/canvas/CanvasShaderNode.js';
2
+ import { BlurTemplate, type BlurProps } from '../templates/BlurTemplate.js';
3
+
4
+ export interface ComputedBlurValues {
5
+ filter: string;
6
+ }
7
+
8
+ /**
9
+ * Canvas2D Blur backed by the native `ctx.filter = 'blur(Npx)'`. Browsers that
10
+ * lack filter support (Chrome 38-52) will silently no-op — accept the
11
+ * degradation rather than emulating a multi-pass blur in JS.
12
+ */
13
+ export const Blur: CanvasShaderType<BlurProps, ComputedBlurValues> = {
14
+ props: BlurTemplate.props,
15
+ saveAndRestore: true,
16
+ update() {
17
+ this.computed.filter = `blur(${this.props!.amount}px)`;
18
+ },
19
+ render(ctx, _node, renderContext) {
20
+ ctx.filter = this.computed.filter!;
21
+ renderContext();
22
+ },
23
+ };
@@ -0,0 +1,23 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { BlurTemplate, type BlurProps } from './BlurTemplate.js';
3
+ import { resolveShaderProps } from '../../renderers/CoreShaderNode.js';
4
+
5
+ function resolve(input: Partial<BlurProps>): BlurProps {
6
+ const props = { ...input } as Record<string, unknown>;
7
+ resolveShaderProps(props, BlurTemplate.props as never);
8
+ return props as unknown as BlurProps;
9
+ }
10
+
11
+ describe('BlurTemplate', () => {
12
+ it('applies default amount when omitted', () => {
13
+ expect(resolve({}).amount).toBe(4);
14
+ });
15
+
16
+ it('passes through user-provided amount', () => {
17
+ expect(resolve({ amount: 10 }).amount).toBe(10);
18
+ });
19
+
20
+ it('passes through zero (no blur)', () => {
21
+ expect(resolve({ amount: 0 }).amount).toBe(0);
22
+ });
23
+ });
@@ -0,0 +1,25 @@
1
+ import type { CoreShaderType } from '../../renderers/CoreShaderNode.js';
2
+
3
+ /**
4
+ * Properties of the {@link Blur} shader.
5
+ *
6
+ * Intended for nodes with an Image Texture. Applying it to color-only or text
7
+ * nodes will just blur the solid fill.
8
+ */
9
+ export interface BlurProps {
10
+ /**
11
+ * Blur amount in node-space pixels.
12
+ *
13
+ * Single-pass kernel — small values (1-8) look best. Larger values still
14
+ * work but lose smoothness because there are only 9 samples per pixel.
15
+ *
16
+ * @default 4
17
+ */
18
+ amount: number;
19
+ }
20
+
21
+ export const BlurTemplate: CoreShaderType<BlurProps> = {
22
+ props: {
23
+ amount: 4,
24
+ },
25
+ };
@@ -0,0 +1,47 @@
1
+ import type { WebGlShaderType } from '../../renderers/webgl/WebGlShaderNode.js';
2
+ import { BlurTemplate, type BlurProps } from '../templates/BlurTemplate.js';
3
+
4
+ /**
5
+ * Single-pass 3x3 Gaussian-approximation blur (1,2,1 / 2,4,2 / 1,2,1) / 16.
6
+ *
7
+ * 9 texture fetches, no second pass, no render target — designed for Image
8
+ * Textures on constrained devices. Larger blurs trade smoothness for speed;
9
+ * if a higher-quality blur is needed, stack multiple nodes or do a separable
10
+ * pass via a RenderTexture.
11
+ */
12
+ export const Blur: WebGlShaderType<BlurProps> = {
13
+ props: BlurTemplate.props,
14
+ update() {
15
+ this.uniform1f('u_amount', this.props!.amount);
16
+ },
17
+ fragment: `
18
+ # ifdef GL_FRAGMENT_PRECISION_HIGH
19
+ precision highp float;
20
+ # else
21
+ precision mediump float;
22
+ # endif
23
+
24
+ uniform vec2 u_dimensions;
25
+ uniform sampler2D u_texture;
26
+ uniform float u_amount;
27
+
28
+ varying vec4 v_color;
29
+ varying vec2 v_textureCoords;
30
+
31
+ void main() {
32
+ vec2 px = vec2(u_amount) / u_dimensions;
33
+
34
+ vec4 c = texture2D(u_texture, v_textureCoords) * 0.25;
35
+ c += texture2D(u_texture, v_textureCoords + vec2( px.x, 0.0)) * 0.125;
36
+ c += texture2D(u_texture, v_textureCoords + vec2(-px.x, 0.0)) * 0.125;
37
+ c += texture2D(u_texture, v_textureCoords + vec2(0.0, px.y)) * 0.125;
38
+ c += texture2D(u_texture, v_textureCoords + vec2(0.0, -px.y)) * 0.125;
39
+ c += texture2D(u_texture, v_textureCoords + vec2( px.x, px.y)) * 0.0625;
40
+ c += texture2D(u_texture, v_textureCoords + vec2(-px.x, -px.y)) * 0.0625;
41
+ c += texture2D(u_texture, v_textureCoords + vec2( px.x, -px.y)) * 0.0625;
42
+ c += texture2D(u_texture, v_textureCoords + vec2(-px.x, px.y)) * 0.0625;
43
+
44
+ gl_FragColor = c * v_color;
45
+ }
46
+ `,
47
+ };
@@ -322,13 +322,35 @@ export const loadFont = (
322
322
  const nwff: CoreTextNode[] = (nodesWaitingForFont[fontFamily] = []);
323
323
  // Create loading promise
324
324
  const loadPromise = (async (): Promise<void> => {
325
- // Load font JSON data
326
- const response = await fetch(atlasDataUrl);
327
- if (!response.ok) {
328
- throw new Error(`Failed to load font data: ${response.statusText}`);
329
- }
330
-
331
- const fontData = (await response.json()) as SdfFontData;
325
+ const fontData = await new Promise<SdfFontData>((resolve, reject) => {
326
+ const xhr = new XMLHttpRequest();
327
+ xhr.open('GET', atlasDataUrl, true);
328
+ xhr.responseType = 'json';
329
+ xhr.onload = () => {
330
+ if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 0) {
331
+ let data = xhr.response;
332
+ if (typeof data === 'string') {
333
+ try {
334
+ data = JSON.parse(data);
335
+ } catch (e) {
336
+ reject(new Error('Failed to parse font data JSON'));
337
+ return;
338
+ }
339
+ }
340
+ resolve(data as SdfFontData);
341
+ } else {
342
+ reject(new Error(`Failed to load font data: ${xhr.statusText}`));
343
+ }
344
+ };
345
+ xhr.onerror = () => {
346
+ reject(
347
+ new Error(
348
+ 'Network error occurred while trying to load the font data.',
349
+ ),
350
+ );
351
+ };
352
+ xhr.send(null);
353
+ });
332
354
  if (!fontData || !fontData.chars) {
333
355
  throw new Error('Invalid SDF font data format');
334
356
  }
@@ -314,19 +314,7 @@ export class ImageTexture extends Texture {
314
314
  return this.loadImage(absoluteSrc);
315
315
  }
316
316
 
317
- if (type === 'svg') {
318
- return loadSvg(
319
- absoluteSrc,
320
- this.props.w,
321
- this.props.h,
322
- this.props.sx,
323
- this.props.sy,
324
- this.props.sw,
325
- this.props.sh,
326
- );
327
- }
328
-
329
- if (isSvgImage(src) === true) {
317
+ if (type === 'svg' || isSvgImage(src) === true) {
330
318
  return loadSvg(
331
319
  absoluteSrc,
332
320
  this.props.w,
@@ -335,6 +323,7 @@ export class ImageTexture extends Texture {
335
323
  this.props.sy,
336
324
  this.props.sw,
337
325
  this.props.sh,
326
+ this.txManager.pixelRatio,
338
327
  );
339
328
  }
340
329
 
@@ -859,7 +859,6 @@ export class Inspector {
859
859
  // but we need it from the inspector to set the initial properties on the div element
860
860
  const mergedProps = {
861
861
  ...node.props,
862
-
863
862
  ...(node as unknown as { textProps: CoreTextNodeProps }).textProps,
864
863
  } as CoreTextNodeProps;
865
864
  const div = this.createDiv(node.id, mergedProps);
@@ -1230,6 +1229,11 @@ export class Inspector {
1230
1229
 
1231
1230
  // CSS mappable attribute
1232
1231
  if (stylePropertyMap[property]) {
1232
+ // Text nodes must always stay at opacity 0.001 — never let alpha updates override it.
1233
+ if (property === 'alpha' && div.style.pointerEvents === 'none') {
1234
+ return;
1235
+ }
1236
+
1233
1237
  const mappedStyleResponse = stylePropertyMap[property]?.(value);
1234
1238
 
1235
1239
  if (mappedStyleResponse === null) {
@@ -1354,7 +1358,10 @@ export class Inspector {
1354
1358
  div.style.left = `${x - w * mountX}px`;
1355
1359
  div.style.width = `${w}px`;
1356
1360
  div.style.height = `${h}px`;
1357
- div.style.opacity = `${alpha}`;
1361
+ // Text nodes must keep opacity at 0.001 — never override it from animation props.
1362
+ if (div.style.pointerEvents !== 'none') {
1363
+ div.style.opacity = `${alpha}`;
1364
+ }
1358
1365
  div.style.rotate = `${rotation}rad`;
1359
1366
  div.style.scale = `${scale}`;
1360
1367
  div.style.color = convertColorToRgba(color);
@@ -1 +0,0 @@
1
- {"fileNames":[],"fileInfos":[],"root":[],"options":{"allowJs":true,"allowSyntheticDefaultImports":true,"composite":true,"declaration":true,"experimentalDecorators":true,"module":100,"noImplicitAny":false,"noImplicitOverride":true,"noUncheckedIndexedAccess":true,"outDir":"./","rootDir":"..","sourceMap":true,"strict":true,"target":9,"verbatimModuleSyntax":true},"version":"5.8.3"}