@hypen-space/web 0.4.956 → 0.5.2
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/README.md +20 -13
- package/dist/canvas/events.js +46 -7
- package/dist/canvas/events.js.map +1 -1
- package/dist/canvas/index.d.ts +0 -1
- package/dist/canvas/index.js +0 -1
- package/dist/canvas/index.js.map +1 -1
- package/dist/canvas/layout.js +57 -44
- package/dist/canvas/layout.js.map +1 -1
- package/dist/canvas/paint.js +159 -60
- package/dist/canvas/paint.js.map +1 -1
- package/dist/canvas/renderer.d.ts +8 -3
- package/dist/canvas/renderer.js +32 -43
- package/dist/canvas/renderer.js.map +1 -1
- package/dist/canvas/types.d.ts +3 -0
- package/dist/canvas/utils.d.ts +0 -8
- package/dist/canvas/utils.js +3 -34
- package/dist/canvas/utils.js.map +1 -1
- package/dist/canvas/variants.d.ts +49 -0
- package/dist/canvas/variants.js +137 -0
- package/dist/canvas/variants.js.map +1 -0
- package/dist/dom/applicators/events.js +1 -0
- package/dist/dom/applicators/events.js.map +1 -1
- package/dist/dom/applicators/font.d.ts +11 -6
- package/dist/dom/applicators/font.js +16 -27
- package/dist/dom/applicators/font.js.map +1 -1
- package/dist/dom/applicators/index.js +47 -45
- package/dist/dom/applicators/index.js.map +1 -1
- package/dist/dom/canvas/index.js +2 -2
- package/dist/dom/canvas/index.js.map +1 -1
- package/dist/dom/components/app.js +2 -2
- package/dist/dom/components/app.js.map +1 -1
- package/dist/dom/components/audio.js +2 -2
- package/dist/dom/components/audio.js.map +1 -1
- package/dist/dom/components/avatar.js +3 -4
- package/dist/dom/components/avatar.js.map +1 -1
- package/dist/dom/components/badge.js +2 -2
- package/dist/dom/components/badge.js.map +1 -1
- package/dist/dom/components/button.js +2 -2
- package/dist/dom/components/button.js.map +1 -1
- package/dist/dom/components/card.js +2 -2
- package/dist/dom/components/card.js.map +1 -1
- package/dist/dom/components/center.js +2 -2
- package/dist/dom/components/center.js.map +1 -1
- package/dist/dom/components/checkbox.js +4 -5
- package/dist/dom/components/checkbox.js.map +1 -1
- package/dist/dom/components/column.js +2 -2
- package/dist/dom/components/column.js.map +1 -1
- package/dist/dom/components/container.js +2 -2
- package/dist/dom/components/container.js.map +1 -1
- package/dist/dom/components/divider.js +2 -2
- package/dist/dom/components/divider.js.map +1 -1
- package/dist/dom/components/grid.js +10 -10
- package/dist/dom/components/grid.js.map +1 -1
- package/dist/dom/components/heading.js +3 -4
- package/dist/dom/components/heading.js.map +1 -1
- package/dist/dom/components/hypenapp.js +2 -2
- package/dist/dom/components/hypenapp.js.map +1 -1
- package/dist/dom/components/icon.js +4 -5
- package/dist/dom/components/icon.js.map +1 -1
- package/dist/dom/components/image.js +2 -2
- package/dist/dom/components/image.js.map +1 -1
- package/dist/dom/components/index.d.ts +2 -2
- package/dist/dom/components/index.js +2 -2
- package/dist/dom/components/index.js.map +1 -1
- package/dist/dom/components/input.js +2 -2
- package/dist/dom/components/input.js.map +1 -1
- package/dist/dom/components/link.js +2 -2
- package/dist/dom/components/link.js.map +1 -1
- package/dist/dom/components/list.js +2 -2
- package/dist/dom/components/list.js.map +1 -1
- package/dist/dom/components/paragraph.js +2 -2
- package/dist/dom/components/paragraph.js.map +1 -1
- package/dist/dom/components/progressbar.js +3 -3
- package/dist/dom/components/progressbar.js.map +1 -1
- package/dist/dom/components/route.js +2 -2
- package/dist/dom/components/route.js.map +1 -1
- package/dist/dom/components/router.js +2 -2
- package/dist/dom/components/router.js.map +1 -1
- package/dist/dom/components/row.js +10 -10
- package/dist/dom/components/row.js.map +1 -1
- package/dist/dom/components/select.js +3 -4
- package/dist/dom/components/select.js.map +1 -1
- package/dist/dom/components/slider.js +2 -2
- package/dist/dom/components/slider.js.map +1 -1
- package/dist/dom/components/spacer.js +2 -2
- package/dist/dom/components/spacer.js.map +1 -1
- package/dist/dom/components/spinner.js +4 -4
- package/dist/dom/components/spinner.js.map +1 -1
- package/dist/dom/components/stack.js +10 -10
- package/dist/dom/components/stack.js.map +1 -1
- package/dist/dom/components/switch.js +5 -6
- package/dist/dom/components/switch.js.map +1 -1
- package/dist/dom/components/text.js +2 -2
- package/dist/dom/components/text.js.map +1 -1
- package/dist/dom/components/textarea.js +2 -2
- package/dist/dom/components/textarea.js.map +1 -1
- package/dist/dom/components/video.js +2 -2
- package/dist/dom/components/video.js.map +1 -1
- package/dist/dom/debug.js +6 -8
- package/dist/dom/debug.js.map +1 -1
- package/dist/dom/index.d.ts +0 -1
- package/dist/dom/index.js +0 -1
- package/dist/dom/index.js.map +1 -1
- package/dist/dom/renderer.js +2 -3
- package/dist/dom/renderer.js.map +1 -1
- package/dist/variants.d.ts +90 -0
- package/dist/variants.js +206 -0
- package/dist/variants.js.map +1 -0
- package/package.json +2 -2
- package/src/canvas/QUICKSTART.md +12 -7
- package/src/canvas/README.md +8 -5
- package/src/canvas/events.ts +49 -7
- package/src/canvas/index.ts +0 -8
- package/src/canvas/layout.ts +57 -49
- package/src/canvas/paint.ts +163 -58
- package/src/canvas/renderer.ts +38 -46
- package/src/canvas/types.ts +14 -0
- package/src/canvas/utils.ts +3 -37
- package/src/canvas/variants.ts +153 -0
- package/src/dom/README.md +8 -7
- package/src/dom/applicators/events.ts +1 -0
- package/src/dom/applicators/font.ts +16 -29
- package/src/dom/applicators/index.ts +56 -46
- package/src/dom/canvas/index.ts +2 -2
- package/src/dom/components/app.ts +2 -2
- package/src/dom/components/audio.ts +2 -2
- package/src/dom/components/avatar.ts +3 -4
- package/src/dom/components/badge.ts +2 -2
- package/src/dom/components/button.ts +2 -2
- package/src/dom/components/card.ts +2 -2
- package/src/dom/components/center.ts +2 -2
- package/src/dom/components/checkbox.ts +4 -5
- package/src/dom/components/column.ts +2 -2
- package/src/dom/components/container.ts +2 -2
- package/src/dom/components/divider.ts +2 -2
- package/src/dom/components/grid.ts +10 -10
- package/src/dom/components/heading.ts +3 -4
- package/src/dom/components/hypenapp.ts +2 -2
- package/src/dom/components/icon.ts +4 -5
- package/src/dom/components/image.ts +2 -2
- package/src/dom/components/index.ts +3 -3
- package/src/dom/components/input.ts +2 -2
- package/src/dom/components/link.ts +2 -2
- package/src/dom/components/list.ts +2 -2
- package/src/dom/components/paragraph.ts +2 -2
- package/src/dom/components/progressbar.ts +3 -3
- package/src/dom/components/route.ts +2 -2
- package/src/dom/components/router.ts +2 -2
- package/src/dom/components/row.ts +10 -10
- package/src/dom/components/select.ts +3 -4
- package/src/dom/components/slider.ts +2 -2
- package/src/dom/components/spacer.ts +2 -2
- package/src/dom/components/spinner.ts +4 -4
- package/src/dom/components/stack.ts +10 -10
- package/src/dom/components/switch.ts +5 -6
- package/src/dom/components/text.ts +2 -2
- package/src/dom/components/textarea.ts +2 -2
- package/src/dom/components/video.ts +2 -2
- package/src/dom/debug.ts +6 -8
- package/src/dom/index.ts +0 -8
- package/src/dom/renderer.ts +2 -3
- package/src/variants.ts +249 -0
- package/dist/client.d.ts +0 -36
- package/dist/client.js +0 -47
- package/dist/client.js.map +0 -1
- package/src/client.ts +0 -88
package/src/canvas/paint.ts
CHANGED
|
@@ -205,14 +205,9 @@ function paintContainer(ctx: CanvasRenderingContext2D, node: VirtualNode): void
|
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
// Draw background
|
|
208
|
-
const
|
|
209
|
-
if (
|
|
210
|
-
|
|
211
|
-
if (typeof backgroundColor === "string" && backgroundColor.includes("gradient")) {
|
|
212
|
-
ctx.fillStyle = parseGradient(ctx, backgroundColor, x, y, width, height);
|
|
213
|
-
} else {
|
|
214
|
-
ctx.fillStyle = backgroundColor;
|
|
215
|
-
}
|
|
208
|
+
const background = resolveBackgroundPaint(props);
|
|
209
|
+
if (background) {
|
|
210
|
+
ctx.fillStyle = resolveCanvasPaint(ctx, background, x, y, width, height);
|
|
216
211
|
|
|
217
212
|
if (radius > 0) {
|
|
218
213
|
drawRoundedRect(ctx, x, y, width, height, radius);
|
|
@@ -383,7 +378,7 @@ function paintButton(ctx: CanvasRenderingContext2D, node: VirtualNode): void {
|
|
|
383
378
|
const y = layout.y;
|
|
384
379
|
const width = layout.width;
|
|
385
380
|
const height = layout.height;
|
|
386
|
-
const radius = layout.border.radius;
|
|
381
|
+
const radius = layout.border.radius || 4;
|
|
387
382
|
|
|
388
383
|
// Apply shadow if specified
|
|
389
384
|
const shadow = props.shadow || props.boxShadow;
|
|
@@ -402,16 +397,12 @@ function paintButton(ctx: CanvasRenderingContext2D, node: VirtualNode): void {
|
|
|
402
397
|
backgroundColor = props.hoverColor;
|
|
403
398
|
} else if (node.focused && props.focusColor !== undefined) {
|
|
404
399
|
backgroundColor = props.focusColor;
|
|
405
|
-
} else
|
|
406
|
-
backgroundColor = props
|
|
400
|
+
} else {
|
|
401
|
+
backgroundColor = resolveBackgroundPaint(props);
|
|
407
402
|
}
|
|
408
403
|
|
|
409
404
|
if (backgroundColor !== undefined) {
|
|
410
|
-
|
|
411
|
-
ctx.fillStyle = parseGradient(ctx, backgroundColor, x, y, width, height);
|
|
412
|
-
} else {
|
|
413
|
-
ctx.fillStyle = backgroundColor;
|
|
414
|
-
}
|
|
405
|
+
ctx.fillStyle = resolveCanvasPaint(ctx, backgroundColor, x, y, width, height);
|
|
415
406
|
drawRoundedRect(ctx, x, y, width, height, radius);
|
|
416
407
|
ctx.fill();
|
|
417
408
|
}
|
|
@@ -449,7 +440,7 @@ function paintInput(ctx: CanvasRenderingContext2D, node: VirtualNode): void {
|
|
|
449
440
|
const y = layout.y;
|
|
450
441
|
const width = layout.width;
|
|
451
442
|
const height = layout.height;
|
|
452
|
-
const radius = layout.border.radius;
|
|
443
|
+
const radius = layout.border.radius || 4;
|
|
453
444
|
|
|
454
445
|
// Background
|
|
455
446
|
ctx.fillStyle = props.backgroundColor || "#ffffff";
|
|
@@ -724,14 +715,6 @@ function drawRoundedRect(
|
|
|
724
715
|
radius: number
|
|
725
716
|
): void {
|
|
726
717
|
if (radius <= 0 || width <= 0 || height <= 0) {
|
|
727
|
-
// beginPath is critical here — without it, this rect appends to the
|
|
728
|
-
// existing path and the next `ctx.fill()` re-fills every previously
|
|
729
|
-
// queued rect with the current fillStyle. That manifested as a
|
|
730
|
-
// calculator grid where every button painted in the last button's
|
|
731
|
-
// colour and only the last button's child text survived (each
|
|
732
|
-
// subsequent fill repainted over earlier children). Mirrors the
|
|
733
|
-
// beginPath() the rounded branch already does below.
|
|
734
|
-
ctx.beginPath();
|
|
735
718
|
ctx.rect(x, y, width, height);
|
|
736
719
|
return;
|
|
737
720
|
}
|
|
@@ -782,6 +765,45 @@ function applyShadow(ctx: CanvasRenderingContext2D, shadow: any): void {
|
|
|
782
765
|
}
|
|
783
766
|
}
|
|
784
767
|
|
|
768
|
+
/**
|
|
769
|
+
* Resolve DOM background applicator props into the CSS-like paint strings
|
|
770
|
+
* Canvas understands. DOM applicators map `.linearGradient("...")` to
|
|
771
|
+
* `background-image: linear-gradient(...)`; Canvas receives the original prop
|
|
772
|
+
* and must do the same translation before painting.
|
|
773
|
+
*/
|
|
774
|
+
function resolveBackgroundPaint(props: Record<string, any>): string | undefined {
|
|
775
|
+
if (props.linearGradient !== undefined) {
|
|
776
|
+
return `linear-gradient(${props.linearGradient})`;
|
|
777
|
+
}
|
|
778
|
+
if (props.radialGradient !== undefined) {
|
|
779
|
+
return `radial-gradient(${props.radialGradient})`;
|
|
780
|
+
}
|
|
781
|
+
if (props.conicGradient !== undefined) {
|
|
782
|
+
return `conic-gradient(${props.conicGradient})`;
|
|
783
|
+
}
|
|
784
|
+
if (props.backgroundImage !== undefined) {
|
|
785
|
+
return String(props.backgroundImage);
|
|
786
|
+
}
|
|
787
|
+
if (props.backgroundColor !== undefined) {
|
|
788
|
+
return String(props.backgroundColor);
|
|
789
|
+
}
|
|
790
|
+
if (props.background !== undefined) {
|
|
791
|
+
return String(props.background);
|
|
792
|
+
}
|
|
793
|
+
return undefined;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function resolveCanvasPaint(
|
|
797
|
+
ctx: CanvasRenderingContext2D,
|
|
798
|
+
paint: string,
|
|
799
|
+
x: number,
|
|
800
|
+
y: number,
|
|
801
|
+
width: number,
|
|
802
|
+
height: number,
|
|
803
|
+
): CanvasGradient | string {
|
|
804
|
+
return paint.includes("gradient(") ? parseGradient(ctx, paint, x, y, width, height) : paint;
|
|
805
|
+
}
|
|
806
|
+
|
|
785
807
|
/**
|
|
786
808
|
* Parse gradient string and create canvas gradient
|
|
787
809
|
* Supports:
|
|
@@ -802,7 +824,7 @@ function parseGradient(
|
|
|
802
824
|
const match = gradientStr.match(/linear-gradient\((.*)\)/);
|
|
803
825
|
if (!match) return gradientStr;
|
|
804
826
|
|
|
805
|
-
const parts = match[1]
|
|
827
|
+
const parts = splitCssArgs(match[1]);
|
|
806
828
|
|
|
807
829
|
// Determine direction (default to bottom)
|
|
808
830
|
let x0 = x, y0 = y, x1 = x, y1 = y + height;
|
|
@@ -812,29 +834,20 @@ function parseGradient(
|
|
|
812
834
|
colorStart = 1;
|
|
813
835
|
const direction = parts[0];
|
|
814
836
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
x1 =
|
|
821
|
-
|
|
822
|
-
y1 = y;
|
|
823
|
-
} else if (direction.includes("to top") || direction === "0deg") {
|
|
824
|
-
y0 = y + height;
|
|
825
|
-
y1 = y;
|
|
837
|
+
const angle = parseCssGradientAngle(direction);
|
|
838
|
+
if (angle !== null) {
|
|
839
|
+
const line = gradientLineForAngle(angle, x, y, width, height);
|
|
840
|
+
x0 = line.x0;
|
|
841
|
+
y0 = line.y0;
|
|
842
|
+
x1 = line.x1;
|
|
843
|
+
y1 = line.y1;
|
|
826
844
|
}
|
|
827
|
-
// Default is "to bottom" which we already set
|
|
828
845
|
}
|
|
829
846
|
|
|
830
847
|
const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
|
|
831
848
|
|
|
832
849
|
// Add color stops
|
|
833
|
-
|
|
834
|
-
colors.forEach((color, i) => {
|
|
835
|
-
const stop = i / (colors.length - 1);
|
|
836
|
-
gradient.addColorStop(stop, color.trim());
|
|
837
|
-
});
|
|
850
|
+
addColorStops(gradient, parts.slice(colorStart));
|
|
838
851
|
|
|
839
852
|
return gradient;
|
|
840
853
|
} else if (gradientStr.startsWith("radial-gradient")) {
|
|
@@ -842,7 +855,7 @@ function parseGradient(
|
|
|
842
855
|
const match = gradientStr.match(/radial-gradient\((.*)\)/);
|
|
843
856
|
if (!match) return gradientStr;
|
|
844
857
|
|
|
845
|
-
const parts = match[1]
|
|
858
|
+
const parts = splitCssArgs(match[1]);
|
|
846
859
|
|
|
847
860
|
// Create radial gradient from center
|
|
848
861
|
const centerX = x + width / 2;
|
|
@@ -852,17 +865,116 @@ function parseGradient(
|
|
|
852
865
|
const gradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, radius);
|
|
853
866
|
|
|
854
867
|
// Add color stops
|
|
855
|
-
|
|
856
|
-
const stop = i / (parts.length - 1);
|
|
857
|
-
gradient.addColorStop(stop, color.trim());
|
|
858
|
-
});
|
|
868
|
+
addColorStops(gradient, parts);
|
|
859
869
|
|
|
870
|
+
return gradient;
|
|
871
|
+
} else if (gradientStr.startsWith("conic-gradient")) {
|
|
872
|
+
const match = gradientStr.match(/conic-gradient\((.*)\)/);
|
|
873
|
+
if (!match) return gradientStr;
|
|
874
|
+
const createConicGradient = (ctx as CanvasRenderingContext2D & {
|
|
875
|
+
createConicGradient?: (startAngle: number, x: number, y: number) => CanvasGradient;
|
|
876
|
+
}).createConicGradient;
|
|
877
|
+
if (!createConicGradient) return firstGradientColor(match[1]) ?? gradientStr;
|
|
878
|
+
const centerX = x + width / 2;
|
|
879
|
+
const centerY = y + height / 2;
|
|
880
|
+
const gradient = createConicGradient.call(ctx, 0, centerX, centerY);
|
|
881
|
+
addColorStops(gradient, splitCssArgs(match[1]));
|
|
860
882
|
return gradient;
|
|
861
883
|
}
|
|
862
884
|
|
|
863
885
|
return gradientStr;
|
|
864
886
|
}
|
|
865
887
|
|
|
888
|
+
function splitCssArgs(input: string): string[] {
|
|
889
|
+
const parts: string[] = [];
|
|
890
|
+
let depth = 0;
|
|
891
|
+
let start = 0;
|
|
892
|
+
for (let i = 0; i < input.length; i++) {
|
|
893
|
+
const ch = input[i];
|
|
894
|
+
if (ch === "(") depth++;
|
|
895
|
+
else if (ch === ")") depth = Math.max(0, depth - 1);
|
|
896
|
+
else if (ch === "," && depth === 0) {
|
|
897
|
+
const part = input.slice(start, i).trim();
|
|
898
|
+
if (part) parts.push(part);
|
|
899
|
+
start = i + 1;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
const tail = input.slice(start).trim();
|
|
903
|
+
if (tail) parts.push(tail);
|
|
904
|
+
return parts;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
function parseCssGradientAngle(direction: string): number | null {
|
|
908
|
+
const trimmed = direction.trim().toLowerCase();
|
|
909
|
+
const deg = trimmed.match(/^(-?(?:\d+|\d*\.\d+))deg$/);
|
|
910
|
+
if (deg) return Number(deg[1]);
|
|
911
|
+
if (!trimmed.startsWith("to ")) return null;
|
|
912
|
+
|
|
913
|
+
const words = new Set(trimmed.slice(3).split(/\s+/).filter(Boolean));
|
|
914
|
+
if (words.has("top") && words.has("right")) return 45;
|
|
915
|
+
if (words.has("right") && words.has("bottom")) return 135;
|
|
916
|
+
if (words.has("bottom") && words.has("left")) return 225;
|
|
917
|
+
if (words.has("left") && words.has("top")) return 315;
|
|
918
|
+
if (words.has("top")) return 0;
|
|
919
|
+
if (words.has("right")) return 90;
|
|
920
|
+
if (words.has("bottom")) return 180;
|
|
921
|
+
if (words.has("left")) return 270;
|
|
922
|
+
return null;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function gradientLineForAngle(
|
|
926
|
+
cssDegrees: number,
|
|
927
|
+
x: number,
|
|
928
|
+
y: number,
|
|
929
|
+
width: number,
|
|
930
|
+
height: number,
|
|
931
|
+
): { x0: number; y0: number; x1: number; y1: number } {
|
|
932
|
+
const radians = (cssDegrees * Math.PI) / 180;
|
|
933
|
+
const cx = x + width / 2;
|
|
934
|
+
const cy = y + height / 2;
|
|
935
|
+
const half = Math.sqrt(width * width + height * height) / 2;
|
|
936
|
+
const dx = Math.sin(radians) * half;
|
|
937
|
+
const dy = -Math.cos(radians) * half;
|
|
938
|
+
return {
|
|
939
|
+
x0: cx - dx,
|
|
940
|
+
y0: cy - dy,
|
|
941
|
+
x1: cx + dx,
|
|
942
|
+
y1: cy + dy,
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function parseColorStop(raw: string, fallbackStop: number): { color: string; stop: number } {
|
|
947
|
+
const trimmed = raw.trim();
|
|
948
|
+
const match = trimmed.match(/^(.*\S)\s+(-?(?:\d+|\d*\.\d+)%)$/);
|
|
949
|
+
if (!match) return { color: trimmed, stop: fallbackStop };
|
|
950
|
+
return {
|
|
951
|
+
color: match[1].trim(),
|
|
952
|
+
stop: Math.max(0, Math.min(1, parseFloat(match[2]) / 100)),
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function addColorStops(gradient: CanvasGradient, rawStops: string[]): void {
|
|
957
|
+
const stops = rawStops.filter(Boolean);
|
|
958
|
+
if (stops.length === 0) return;
|
|
959
|
+
if (stops.length === 1) {
|
|
960
|
+
const parsed = parseColorStop(stops[0], 0);
|
|
961
|
+
gradient.addColorStop(0, parsed.color);
|
|
962
|
+
gradient.addColorStop(1, parsed.color);
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
stops.forEach((raw, i) => {
|
|
966
|
+
const fallback = i / (stops.length - 1);
|
|
967
|
+
const { color, stop } = parseColorStop(raw, fallback);
|
|
968
|
+
gradient.addColorStop(stop, color);
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function firstGradientColor(input: string): string | undefined {
|
|
973
|
+
const first = splitCssArgs(input)[0];
|
|
974
|
+
if (!first) return undefined;
|
|
975
|
+
return parseColorStop(first, 0).color;
|
|
976
|
+
}
|
|
977
|
+
|
|
866
978
|
/**
|
|
867
979
|
* Apply CSS-like transforms to canvas context
|
|
868
980
|
* Supports:
|
|
@@ -1276,19 +1388,15 @@ function paintCard(ctx: CanvasRenderingContext2D, node: VirtualNode): void {
|
|
|
1276
1388
|
const y = layout.y;
|
|
1277
1389
|
const width = layout.width;
|
|
1278
1390
|
const height = layout.height;
|
|
1279
|
-
const radius = layout.border.radius;
|
|
1391
|
+
const radius = layout.border.radius || 8;
|
|
1280
1392
|
|
|
1281
1393
|
// Default card shadow
|
|
1282
1394
|
const shadow = props.shadow || props.boxShadow || "0 2 8 rgba(0,0,0,0.1)";
|
|
1283
1395
|
applyShadow(ctx, shadow);
|
|
1284
1396
|
|
|
1285
1397
|
// Background
|
|
1286
|
-
const backgroundColor = props
|
|
1287
|
-
|
|
1288
|
-
ctx.fillStyle = parseGradient(ctx, backgroundColor, x, y, width, height);
|
|
1289
|
-
} else {
|
|
1290
|
-
ctx.fillStyle = backgroundColor;
|
|
1291
|
-
}
|
|
1398
|
+
const backgroundColor = resolveBackgroundPaint(props) || "#ffffff";
|
|
1399
|
+
ctx.fillStyle = resolveCanvasPaint(ctx, backgroundColor, x, y, width, height);
|
|
1292
1400
|
|
|
1293
1401
|
drawRoundedRect(ctx, x, y, width, height, radius);
|
|
1294
1402
|
ctx.fill();
|
|
@@ -1639,6 +1747,3 @@ function drawStar(
|
|
|
1639
1747
|
|
|
1640
1748
|
|
|
1641
1749
|
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
package/src/canvas/renderer.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
refreshApplicator,
|
|
36
36
|
parseApplicatorBase,
|
|
37
37
|
} from "./props.js";
|
|
38
|
+
import { applyVariants, invalidateVariantCache, deriveNodeComputed } from "./variants.js";
|
|
38
39
|
|
|
39
40
|
const DEFAULT_OPTIONS: CanvasRendererOptions = {
|
|
40
41
|
devicePixelRatio: typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1,
|
|
@@ -73,10 +74,6 @@ export class CanvasRenderer implements Renderer {
|
|
|
73
74
|
private rafId: number | null = null;
|
|
74
75
|
private needsRedraw = false;
|
|
75
76
|
|
|
76
|
-
private resizeObserver: ResizeObserver | null = null;
|
|
77
|
-
private dprMediaQuery: MediaQueryList | null = null;
|
|
78
|
-
private dprChangeHandler: (() => void) | null = null;
|
|
79
|
-
|
|
80
77
|
private frameCount = 0;
|
|
81
78
|
private lastFrameTime = 0;
|
|
82
79
|
|
|
@@ -142,30 +139,6 @@ export class CanvasRenderer implements Renderer {
|
|
|
142
139
|
// Eagerly initialise Taffy WASM for layout (non-blocking — fallback used until ready)
|
|
143
140
|
initTaffyLayout();
|
|
144
141
|
|
|
145
|
-
// Observe size changes — without this, the canvas backing store stays
|
|
146
|
-
// pinned at construction-time dimensions while CSS stretches the
|
|
147
|
-
// display box, and content gets bitmap-stretched / hit-testing drifts.
|
|
148
|
-
if (typeof ResizeObserver !== "undefined") {
|
|
149
|
-
this.resizeObserver = new ResizeObserver(() => {
|
|
150
|
-
this.setupHiDPI();
|
|
151
|
-
this.scheduleRedraw();
|
|
152
|
-
});
|
|
153
|
-
this.resizeObserver.observe(this.canvas);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Track DPR changes (e.g. dragging window across monitors with
|
|
157
|
-
// different scaling). matchMedia fires once per crossing.
|
|
158
|
-
if (typeof window !== "undefined" && typeof window.matchMedia === "function") {
|
|
159
|
-
const dpr = window.devicePixelRatio || 1;
|
|
160
|
-
this.dprMediaQuery = window.matchMedia(`(resolution: ${dpr}dppx)`);
|
|
161
|
-
this.dprChangeHandler = () => {
|
|
162
|
-
this.options.devicePixelRatio = window.devicePixelRatio || 1;
|
|
163
|
-
this.setupHiDPI();
|
|
164
|
-
this.scheduleRedraw();
|
|
165
|
-
};
|
|
166
|
-
this.dprMediaQuery.addEventListener("change", this.dprChangeHandler);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
142
|
// Don't schedule initial render - wait for patches
|
|
170
143
|
}
|
|
171
144
|
|
|
@@ -176,18 +149,15 @@ export class CanvasRenderer implements Renderer {
|
|
|
176
149
|
const dpr = this.options.devicePixelRatio || 1;
|
|
177
150
|
const rect = this.canvas.getBoundingClientRect();
|
|
178
151
|
|
|
179
|
-
// Skip when the host is collapsed — a 0-pixel backing store throws
|
|
180
|
-
// in some 2D contexts and the next non-zero resize will reinitialise.
|
|
181
|
-
if (rect.width === 0 || rect.height === 0) return;
|
|
182
|
-
|
|
183
152
|
this.canvas.width = rect.width * dpr;
|
|
184
153
|
this.canvas.height = rect.height * dpr;
|
|
185
154
|
|
|
186
|
-
// Reset transform first — `scale()` is cumulative across calls, so on
|
|
187
|
-
// a resize the second call would compound (dpr²) and shrink content.
|
|
188
|
-
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
189
155
|
this.ctx.scale(dpr, dpr);
|
|
190
156
|
|
|
157
|
+
// Update canvas display size
|
|
158
|
+
this.canvas.style.width = `${rect.width}px`;
|
|
159
|
+
this.canvas.style.height = `${rect.height}px`;
|
|
160
|
+
|
|
191
161
|
// Sync dirty tracker with new canvas size
|
|
192
162
|
if (this.dirtyTracker) {
|
|
193
163
|
this.dirtyTracker.setCanvasSize(rect.width, rect.height);
|
|
@@ -325,6 +295,10 @@ export class CanvasRenderer implements Renderer {
|
|
|
325
295
|
|
|
326
296
|
node.props[name] = value;
|
|
327
297
|
|
|
298
|
+
// A new key may introduce a variant marker (e.g. `padding@md.0`) — drop the
|
|
299
|
+
// cached variant-base set so the next frame rescans.
|
|
300
|
+
invalidateVariantCache(node);
|
|
301
|
+
|
|
328
302
|
// If this is an applicator-namespaced key (e.g. `flex.0`, `onClick.to`),
|
|
329
303
|
// rebuild the derived flat/aggregate entry under the base name so layout,
|
|
330
304
|
// paint, and event dispatch see the updated value.
|
|
@@ -373,6 +347,8 @@ export class CanvasRenderer implements Renderer {
|
|
|
373
347
|
|
|
374
348
|
delete node.props[name];
|
|
375
349
|
|
|
350
|
+
invalidateVariantCache(node);
|
|
351
|
+
|
|
376
352
|
const base = parseApplicatorBase(name);
|
|
377
353
|
if (base !== null) {
|
|
378
354
|
refreshApplicator(node.props, base);
|
|
@@ -611,6 +587,20 @@ export class CanvasRenderer implements Renderer {
|
|
|
611
587
|
}
|
|
612
588
|
}
|
|
613
589
|
|
|
590
|
+
/**
|
|
591
|
+
* Re-derive cached computed fields that may have been overridden by variant
|
|
592
|
+
* resolution. `node.opacity` is read straight off `node.props.opacity` in
|
|
593
|
+
* paint, but it is cached on the node at create/setProp time — after a
|
|
594
|
+
* variant pass changes `props.opacity` (e.g. `opacity:disabled`), the cache
|
|
595
|
+
* must be refreshed or the paint would use the stale value.
|
|
596
|
+
*/
|
|
597
|
+
private refreshComputedProps(node: VirtualNode): void {
|
|
598
|
+
deriveNodeComputed(node);
|
|
599
|
+
for (const child of node.children) {
|
|
600
|
+
this.refreshComputedProps(child);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
614
604
|
/**
|
|
615
605
|
* Full canvas repaint (default behavior when dirty rects disabled)
|
|
616
606
|
*/
|
|
@@ -626,10 +616,17 @@ export class CanvasRenderer implements Renderer {
|
|
|
626
616
|
|
|
627
617
|
// Layout and paint
|
|
628
618
|
if (this.rootNode) {
|
|
619
|
+
// Resolve responsive + state variants against the current content width
|
|
620
|
+
// BEFORE layout so spacing/size winners feed the layout engine and
|
|
621
|
+
// colour/opacity winners feed paint.
|
|
622
|
+
const contentWidth = this.canvas.width / dpr;
|
|
623
|
+
applyVariants(this.rootNode, contentWidth);
|
|
624
|
+
this.refreshComputedProps(this.rootNode);
|
|
625
|
+
|
|
629
626
|
computeLayout(
|
|
630
627
|
this.ctx,
|
|
631
628
|
this.rootNode,
|
|
632
|
-
|
|
629
|
+
contentWidth,
|
|
633
630
|
this.canvas.height / dpr
|
|
634
631
|
);
|
|
635
632
|
|
|
@@ -649,10 +646,14 @@ export class CanvasRenderer implements Renderer {
|
|
|
649
646
|
private renderWithDirtyRects(dpr: number): void {
|
|
650
647
|
// Always run layout so nodes have up-to-date bounds
|
|
651
648
|
if (this.rootNode) {
|
|
649
|
+
const contentWidth = this.canvas.width / dpr;
|
|
650
|
+
applyVariants(this.rootNode, contentWidth);
|
|
651
|
+
this.refreshComputedProps(this.rootNode);
|
|
652
|
+
|
|
652
653
|
computeLayout(
|
|
653
654
|
this.ctx,
|
|
654
655
|
this.rootNode,
|
|
655
|
-
|
|
656
|
+
contentWidth,
|
|
656
657
|
this.canvas.height / dpr
|
|
657
658
|
);
|
|
658
659
|
ScrollManager.updateScrollBounds(this.rootNode);
|
|
@@ -764,15 +765,6 @@ export class CanvasRenderer implements Renderer {
|
|
|
764
765
|
if (this.rafId !== null) {
|
|
765
766
|
cancelAnimationFrame(this.rafId);
|
|
766
767
|
}
|
|
767
|
-
if (this.resizeObserver) {
|
|
768
|
-
this.resizeObserver.disconnect();
|
|
769
|
-
this.resizeObserver = null;
|
|
770
|
-
}
|
|
771
|
-
if (this.dprMediaQuery && this.dprChangeHandler) {
|
|
772
|
-
this.dprMediaQuery.removeEventListener("change", this.dprChangeHandler);
|
|
773
|
-
this.dprMediaQuery = null;
|
|
774
|
-
this.dprChangeHandler = null;
|
|
775
|
-
}
|
|
776
768
|
this.eventManager.destroy();
|
|
777
769
|
this.scrollManager.destroy();
|
|
778
770
|
this.selectionManager.destroy();
|
package/src/canvas/types.ts
CHANGED
|
@@ -27,6 +27,20 @@ export interface VirtualNode {
|
|
|
27
27
|
|
|
28
28
|
// Scroll state (managed by ScrollManager, not serialised)
|
|
29
29
|
scrollState?: ScrollState;
|
|
30
|
+
|
|
31
|
+
// Pointer-pressed (active) state, tracked by the event manager from
|
|
32
|
+
// mousedown/mouseup so `:active` paint variants can resolve.
|
|
33
|
+
pressed?: boolean;
|
|
34
|
+
|
|
35
|
+
// --- Variant resolution bookkeeping (managed by applyVariants) ---
|
|
36
|
+
// Set of applicator base names that have at least one `@bp`/`:state` variant
|
|
37
|
+
// key on this node. Computed lazily; null means "not yet scanned", an empty
|
|
38
|
+
// set means "scanned, no variants".
|
|
39
|
+
variantBases?: Set<string> | null;
|
|
40
|
+
// Snapshot of the node's original (variant-free) base values, captured the
|
|
41
|
+
// first time a variant override is applied so each frame resolves from the
|
|
42
|
+
// un-overridden base instead of compounding overrides.
|
|
43
|
+
variantOriginals?: Record<string, unknown>;
|
|
30
44
|
}
|
|
31
45
|
|
|
32
46
|
export interface Layout {
|
package/src/canvas/utils.ts
CHANGED
|
@@ -12,42 +12,6 @@ import type { BoxSpacing, Rectangle, Point, VirtualNode } from "./types.js";
|
|
|
12
12
|
// and treating `3.5rem` as 3.5px, collapsing whole layouts to 1/16 size.
|
|
13
13
|
const ROOT_FONT_PX = 16;
|
|
14
14
|
|
|
15
|
-
/**
|
|
16
|
-
* Active layout viewport — used to resolve `vw` / `vh` correctly during a
|
|
17
|
-
* layout pass. Set by `computeLayout` (layout.ts) at entry and cleared on
|
|
18
|
-
* exit. Layout traversal is synchronous, so a single module-level slot is
|
|
19
|
-
* sufficient and avoids threading a `viewport` parameter through every
|
|
20
|
-
* `cssLengthToPx` call site (there are dozens).
|
|
21
|
-
*
|
|
22
|
-
* Falls back to `window.innerWidth` / `window.innerHeight` outside a layout
|
|
23
|
-
* pass (e.g. for direct callers that resolve sizes ahead of layout). When
|
|
24
|
-
* neither is available — e.g. headless tests — `vw`/`vh` degrade to the
|
|
25
|
-
* pre-fix unitless-pixel behavior.
|
|
26
|
-
*/
|
|
27
|
-
let currentViewport: { width: number; height: number } | null = null;
|
|
28
|
-
|
|
29
|
-
export function setCurrentViewport(viewport: { width: number; height: number } | null): void {
|
|
30
|
-
currentViewport = viewport;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function getCurrentViewport(): { width: number; height: number } | null {
|
|
34
|
-
return currentViewport;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function resolveViewportPx(unit: "vw" | "vh", n: number): number {
|
|
38
|
-
if (currentViewport) {
|
|
39
|
-
const dim = unit === "vw" ? currentViewport.width : currentViewport.height;
|
|
40
|
-
return (n / 100) * dim;
|
|
41
|
-
}
|
|
42
|
-
if (typeof window !== "undefined") {
|
|
43
|
-
const dim = unit === "vw" ? window.innerWidth : window.innerHeight;
|
|
44
|
-
if (Number.isFinite(dim) && dim > 0) return (n / 100) * dim;
|
|
45
|
-
}
|
|
46
|
-
// Last resort: treat as unitless px so the layout still gets a numeric
|
|
47
|
-
// value (matches pre-fix behavior in the no-viewport case).
|
|
48
|
-
return n;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
15
|
/**
|
|
52
16
|
* Parse a CSS length string (or number) into pixels.
|
|
53
17
|
* Returns `null` for `"auto"` or unparseable input; returns `null` for `%`
|
|
@@ -69,8 +33,10 @@ export function cssLengthToPx(value: any): number | null {
|
|
|
69
33
|
case "px": return n;
|
|
70
34
|
case "rem":
|
|
71
35
|
case "em": return n * ROOT_FONT_PX;
|
|
36
|
+
// We don't have a viewport reference here; treat vw/vh as unitless px
|
|
37
|
+
// fallback so the layout gets _some_ numeric value rather than zero.
|
|
72
38
|
case "vw":
|
|
73
|
-
case "vh": return
|
|
39
|
+
case "vh": return n;
|
|
74
40
|
// Typographic point — 1/72 inch at CSS reference density (96dpi).
|
|
75
41
|
case "pt": return n * (96 / 72);
|
|
76
42
|
// Density-independent (dp) and scale-independent (sp) logical pixels
|