@gjsify/canvas2d-core 0.1.8 → 0.1.10
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/lib/esm/cairo-utils.js +8 -8
- package/lib/esm/canvas-rendering-context-2d.js +190 -80
- package/lib/esm/color.js +38 -0
- package/lib/types/cairo-utils.d.ts +12 -1
- package/lib/types/canvas-clearing.spec.d.ts +2 -0
- package/lib/types/canvas-color.spec.d.ts +2 -0
- package/lib/types/canvas-composite.spec.d.ts +2 -0
- package/lib/types/canvas-drawimage.spec.d.ts +2 -0
- package/lib/types/canvas-imagedata.spec.d.ts +2 -0
- package/lib/types/canvas-rendering-context-2d.d.ts +30 -3
- package/lib/types/canvas-state.spec.d.ts +2 -0
- package/lib/types/canvas-transform.spec.d.ts +2 -0
- package/lib/types/color.d.ts +5 -1
- package/package.json +11 -11
- package/src/cairo-utils.ts +20 -9
- package/src/canvas-clearing.spec.ts +126 -0
- package/src/canvas-color.spec.ts +113 -0
- package/src/canvas-composite.spec.ts +114 -0
- package/src/canvas-drawimage.spec.ts +287 -0
- package/src/canvas-imagedata.spec.ts +150 -0
- package/src/canvas-rendering-context-2d.ts +253 -94
- package/src/canvas-state.spec.ts +245 -0
- package/src/canvas-transform.spec.ts +211 -0
- package/src/color.ts +53 -1
- package/src/test.mts +17 -1
- package/tmp/.tsbuildinfo +1 -1
package/lib/esm/cairo-utils.js
CHANGED
|
@@ -99,25 +99,25 @@ function cairoRoundRect(ctx, x, y, w, h, radii) {
|
|
|
99
99
|
const COMPOSITE_OP_MAP = {
|
|
100
100
|
"source-over": 2,
|
|
101
101
|
// OVER
|
|
102
|
-
"source-in":
|
|
102
|
+
"source-in": 3,
|
|
103
103
|
// IN
|
|
104
|
-
"source-out":
|
|
104
|
+
"source-out": 4,
|
|
105
105
|
// OUT
|
|
106
|
-
"source-atop":
|
|
106
|
+
"source-atop": 5,
|
|
107
107
|
// ATOP
|
|
108
|
-
"destination-over":
|
|
108
|
+
"destination-over": 7,
|
|
109
109
|
// DEST_OVER
|
|
110
|
-
"destination-in":
|
|
110
|
+
"destination-in": 8,
|
|
111
111
|
// DEST_IN
|
|
112
|
-
"destination-out":
|
|
112
|
+
"destination-out": 9,
|
|
113
113
|
// DEST_OUT
|
|
114
|
-
"destination-atop":
|
|
114
|
+
"destination-atop": 10,
|
|
115
115
|
// DEST_ATOP
|
|
116
116
|
"lighter": 12,
|
|
117
117
|
// ADD
|
|
118
118
|
"copy": 1,
|
|
119
119
|
// SOURCE
|
|
120
|
-
"xor":
|
|
120
|
+
"xor": 11,
|
|
121
121
|
// XOR
|
|
122
122
|
"multiply": 14,
|
|
123
123
|
// MULTIPLY
|
|
@@ -59,6 +59,7 @@ class CanvasRenderingContext2D {
|
|
|
59
59
|
this._ctx.setSource(style._getCairoPattern());
|
|
60
60
|
} else if (style instanceof OurCanvasPattern) {
|
|
61
61
|
this._ctx.setSource(style._getCairoPattern());
|
|
62
|
+
this._applyPatternFilter();
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
65
|
/** Apply the current stroke style to the Cairo context. */
|
|
@@ -72,6 +73,27 @@ class CanvasRenderingContext2D {
|
|
|
72
73
|
this._ctx.setSource(style._getCairoPattern());
|
|
73
74
|
} else if (style instanceof OurCanvasPattern) {
|
|
74
75
|
this._ctx.setSource(style._getCairoPattern());
|
|
76
|
+
this._applyPatternFilter();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Apply the current imageSmoothingEnabled + imageSmoothingQuality state
|
|
81
|
+
* to the currently installed Cairo source pattern. Per Canvas 2D spec,
|
|
82
|
+
* the filter is read from the context at *draw* time, not at pattern
|
|
83
|
+
* creation — so we re-apply it on every fill/stroke.
|
|
84
|
+
*/
|
|
85
|
+
_applyPatternFilter() {
|
|
86
|
+
const pat = this._ctx.getSource?.();
|
|
87
|
+
if (pat && typeof pat.setFilter === "function") {
|
|
88
|
+
let filter;
|
|
89
|
+
if (!this._state.imageSmoothingEnabled) {
|
|
90
|
+
filter = Cairo.Filter.NEAREST;
|
|
91
|
+
} else if (this._state.imageSmoothingQuality === "high") {
|
|
92
|
+
filter = Cairo.Filter.BEST;
|
|
93
|
+
} else {
|
|
94
|
+
filter = Cairo.Filter.BILINEAR;
|
|
95
|
+
}
|
|
96
|
+
pat.setFilter(filter);
|
|
75
97
|
}
|
|
76
98
|
}
|
|
77
99
|
/** Apply line properties to the Cairo context. */
|
|
@@ -102,32 +124,45 @@ class CanvasRenderingContext2D {
|
|
|
102
124
|
return c !== null && c.a > 0;
|
|
103
125
|
}
|
|
104
126
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
127
|
+
* Convert a distance from device pixels to Cairo user space by inverting
|
|
128
|
+
* the linear part of the current CTM (translation doesn't affect distances).
|
|
129
|
+
*
|
|
130
|
+
* Canvas 2D spec: shadowOffsetX/Y are in CSS pixels and are NOT scaled by
|
|
131
|
+
* the current transform. This helper converts them to user-space offsets so
|
|
132
|
+
* that `ctx.moveTo(x + sdx, y + sdy)` produces the correct pixel offset
|
|
133
|
+
* regardless of any ctx.scale() or ctx.rotate() in effect.
|
|
108
134
|
*/
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
135
|
+
_deviceToUserDistance(dx, dy) {
|
|
136
|
+
const origin = this._ctx.userToDevice(0, 0);
|
|
137
|
+
const xAxis = this._ctx.userToDevice(1, 0);
|
|
138
|
+
const yAxis = this._ctx.userToDevice(0, 1);
|
|
139
|
+
const a = (xAxis[0] ?? 0) - (origin[0] ?? 0);
|
|
140
|
+
const b = (xAxis[1] ?? 0) - (origin[1] ?? 0);
|
|
141
|
+
const c = (yAxis[0] ?? 0) - (origin[0] ?? 0);
|
|
142
|
+
const d = (yAxis[1] ?? 0) - (origin[1] ?? 0);
|
|
143
|
+
const det = a * d - b * c;
|
|
144
|
+
if (Math.abs(det) < 1e-10) return [dx, dy];
|
|
145
|
+
return [
|
|
146
|
+
(d * dx - c * dy) / det,
|
|
147
|
+
(-b * dx + a * dy) / det
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Shadow rendering is intentionally a no-op.
|
|
152
|
+
*
|
|
153
|
+
* Proper Canvas 2D shadows require a Gaussian blur pass on an isolated
|
|
154
|
+
* temporary surface, which cannot be emulated reliably without a full
|
|
155
|
+
* Path2D replay or pixel-level manipulation. The previous implementation
|
|
156
|
+
* attempted to use a temp surface but never replayed the path onto it
|
|
157
|
+
* (because `drawOp` closes over the main context), leaving the shadow
|
|
158
|
+
* surface empty while still leaking memory.
|
|
159
|
+
*
|
|
160
|
+
* Excalibur and most 2D game engines bake glow/outline effects into
|
|
161
|
+
* sprites rather than relying on canvas shadows, so this no-op does not
|
|
162
|
+
* affect the showcase. A correct implementation is tracked as a
|
|
163
|
+
* separate Canvas 2D Phase-5 enhancement.
|
|
164
|
+
*/
|
|
165
|
+
_renderShadow(_drawOp) {
|
|
131
166
|
}
|
|
132
167
|
// ---- State ----
|
|
133
168
|
save() {
|
|
@@ -164,9 +199,17 @@ class CanvasRenderingContext2D {
|
|
|
164
199
|
*/
|
|
165
200
|
transform(a, b, c, d, e, f) {
|
|
166
201
|
this._ensureSurface();
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
202
|
+
if (!Number.isFinite(a) || !Number.isFinite(b) || !Number.isFinite(c) || !Number.isFinite(d) || !Number.isFinite(e) || !Number.isFinite(f)) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const tx = e;
|
|
206
|
+
const ty = f;
|
|
207
|
+
const sx = Math.hypot(a, b);
|
|
208
|
+
const sy = Math.hypot(c, d);
|
|
209
|
+
const rotation = Math.atan2(b, a);
|
|
210
|
+
this._ctx.translate(tx, ty);
|
|
211
|
+
if (rotation !== 0) this._ctx.rotate(rotation);
|
|
212
|
+
if (sx !== 1 || sy !== 1) this._ctx.scale(sx, sy);
|
|
170
213
|
}
|
|
171
214
|
setTransform(a, b, c, d, e, f) {
|
|
172
215
|
this._ensureSurface();
|
|
@@ -192,36 +235,45 @@ class CanvasRenderingContext2D {
|
|
|
192
235
|
* Return the current transformation matrix as a DOMMatrix-like object.
|
|
193
236
|
*/
|
|
194
237
|
getTransform() {
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
238
|
+
const origin = this._ctx.userToDevice(0, 0);
|
|
239
|
+
const xAxis = this._ctx.userToDevice(1, 0);
|
|
240
|
+
const yAxis = this._ctx.userToDevice(0, 1);
|
|
241
|
+
const e = origin[0] ?? 0;
|
|
242
|
+
const f = origin[1] ?? 0;
|
|
243
|
+
const a = (xAxis[0] ?? 0) - e;
|
|
244
|
+
const b = (xAxis[1] ?? 0) - f;
|
|
245
|
+
const c = (yAxis[0] ?? 0) - e;
|
|
246
|
+
const d = (yAxis[1] ?? 0) - f;
|
|
247
|
+
const DOMMatrixCtor = globalThis.DOMMatrix;
|
|
248
|
+
if (typeof DOMMatrixCtor === "function") {
|
|
249
|
+
return new DOMMatrixCtor([a, b, c, d, e, f]);
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
a,
|
|
253
|
+
b,
|
|
254
|
+
c,
|
|
255
|
+
d,
|
|
256
|
+
e,
|
|
257
|
+
f,
|
|
258
|
+
m11: a,
|
|
259
|
+
m12: b,
|
|
260
|
+
m13: 0,
|
|
261
|
+
m14: 0,
|
|
262
|
+
m21: c,
|
|
263
|
+
m22: d,
|
|
264
|
+
m23: 0,
|
|
265
|
+
m24: 0,
|
|
266
|
+
m31: 0,
|
|
267
|
+
m32: 0,
|
|
268
|
+
m33: 1,
|
|
269
|
+
m34: 0,
|
|
270
|
+
m41: e,
|
|
271
|
+
m42: f,
|
|
272
|
+
m43: 0,
|
|
273
|
+
m44: 1,
|
|
274
|
+
is2D: true,
|
|
275
|
+
isIdentity: a === 1 && b === 0 && c === 0 && d === 1 && e === 0 && f === 0
|
|
276
|
+
};
|
|
225
277
|
}
|
|
226
278
|
resetTransform() {
|
|
227
279
|
this._ensureSurface();
|
|
@@ -677,13 +729,33 @@ class CanvasRenderingContext2D {
|
|
|
677
729
|
dw = a7;
|
|
678
730
|
dh = a8;
|
|
679
731
|
}
|
|
732
|
+
if (sw === 0 || sh === 0 || dw === 0 || dh === 0) {
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
680
735
|
this._ctx.save();
|
|
736
|
+
this._ctx.rectangle(dx, dy, dw, dh);
|
|
737
|
+
this._ctx.clip();
|
|
681
738
|
this._ctx.translate(dx, dy);
|
|
682
739
|
this._ctx.scale(dw / sw, dh / sh);
|
|
683
740
|
this._ctx.translate(-sx, -sy);
|
|
684
741
|
Gdk.cairo_set_source_pixbuf(this._ctx, pixbuf, 0, 0);
|
|
685
|
-
this._ctx.
|
|
686
|
-
|
|
742
|
+
const pat = this._ctx.getSource?.();
|
|
743
|
+
if (pat && typeof pat.setFilter === "function") {
|
|
744
|
+
let filter;
|
|
745
|
+
if (!this._state.imageSmoothingEnabled) {
|
|
746
|
+
filter = Cairo.Filter.NEAREST;
|
|
747
|
+
} else if (this._state.imageSmoothingQuality === "high") {
|
|
748
|
+
filter = Cairo.Filter.BEST;
|
|
749
|
+
} else {
|
|
750
|
+
filter = Cairo.Filter.BILINEAR;
|
|
751
|
+
}
|
|
752
|
+
pat.setFilter(filter);
|
|
753
|
+
}
|
|
754
|
+
if (this._state.globalAlpha < 1) {
|
|
755
|
+
this._ctx.paintWithAlpha(this._state.globalAlpha);
|
|
756
|
+
} else {
|
|
757
|
+
this._ctx.paint();
|
|
758
|
+
}
|
|
687
759
|
this._ctx.restore();
|
|
688
760
|
}
|
|
689
761
|
_getDrawImageSource(image) {
|
|
@@ -727,18 +799,19 @@ class CanvasRenderingContext2D {
|
|
|
727
799
|
const style = match[1] || "";
|
|
728
800
|
const weight = match[3] || "";
|
|
729
801
|
let size = parseFloat(match[4]) || 10;
|
|
730
|
-
const unit = match[5] || "px";
|
|
802
|
+
const unit = (match[5] || "px").toLowerCase();
|
|
731
803
|
const family = (match[6] || "sans-serif").replace(/['"]/g, "").trim();
|
|
732
|
-
if (unit === "
|
|
733
|
-
else if (unit === "em" || unit === "rem") size = size *
|
|
734
|
-
else if (unit === "%") size = size / 100 *
|
|
804
|
+
if (unit === "pt") size = size * 96 / 72;
|
|
805
|
+
else if (unit === "em" || unit === "rem") size = size * 16;
|
|
806
|
+
else if (unit === "%") size = size / 100 * 16;
|
|
735
807
|
let pangoStr = family;
|
|
736
808
|
if (style === "italic") pangoStr += " Italic";
|
|
737
809
|
else if (style === "oblique") pangoStr += " Oblique";
|
|
738
810
|
if (weight === "bold" || weight === "bolder" || parseInt(weight) >= 600) pangoStr += " Bold";
|
|
739
811
|
else if (weight === "lighter" || parseInt(weight) > 0 && parseInt(weight) <= 300) pangoStr += " Light";
|
|
740
|
-
|
|
741
|
-
|
|
812
|
+
const desc = Pango.font_description_from_string(pangoStr);
|
|
813
|
+
desc.set_absolute_size(size * Pango.SCALE);
|
|
814
|
+
return desc;
|
|
742
815
|
}
|
|
743
816
|
/**
|
|
744
817
|
* Compute the x-offset for text alignment relative to the given x coordinate.
|
|
@@ -801,11 +874,45 @@ class CanvasRenderingContext2D {
|
|
|
801
874
|
fillText(text, x, y, _maxWidth) {
|
|
802
875
|
this._ensureSurface();
|
|
803
876
|
this._applyCompositing();
|
|
804
|
-
this._applyFillStyle();
|
|
805
877
|
const layout = this._createTextLayout(text);
|
|
806
878
|
const xOff = this._getTextAlignOffset(layout);
|
|
807
879
|
const yOff = this._getTextBaselineOffset(layout);
|
|
880
|
+
if (this._hasShadow()) {
|
|
881
|
+
const sc = parseColor(this._state.shadowColor);
|
|
882
|
+
if (sc) {
|
|
883
|
+
const [sdx, sdy] = this._deviceToUserDistance(
|
|
884
|
+
this._state.shadowOffsetX,
|
|
885
|
+
this._state.shadowOffsetY
|
|
886
|
+
);
|
|
887
|
+
const blur = this._state.shadowBlur;
|
|
888
|
+
let taps;
|
|
889
|
+
if (blur > 0) {
|
|
890
|
+
const [bu] = this._deviceToUserDistance(blur, 0);
|
|
891
|
+
const [, bv] = this._deviceToUserDistance(0, blur);
|
|
892
|
+
taps = [
|
|
893
|
+
[sdx, sdy, sc.a],
|
|
894
|
+
[sdx + bu, sdy, sc.a * 0.5],
|
|
895
|
+
[sdx - bu, sdy, sc.a * 0.5],
|
|
896
|
+
[sdx, sdy + bv, sc.a * 0.5],
|
|
897
|
+
[sdx, sdy - bv, sc.a * 0.5]
|
|
898
|
+
];
|
|
899
|
+
} else {
|
|
900
|
+
taps = [[sdx, sdy, sc.a]];
|
|
901
|
+
}
|
|
902
|
+
const aa = this._state.imageSmoothingEnabled ? Cairo.Antialias.DEFAULT : Cairo.Antialias.NONE;
|
|
903
|
+
for (const [tx, ty, ta] of taps) {
|
|
904
|
+
this._ctx.save();
|
|
905
|
+
this._ctx.setAntialias(aa);
|
|
906
|
+
this._ctx.setSourceRGBA(sc.r, sc.g, sc.b, ta);
|
|
907
|
+
this._ctx.moveTo(x + xOff + tx, y + yOff + ty);
|
|
908
|
+
PangoCairo.show_layout(this._ctx, layout);
|
|
909
|
+
this._ctx.restore();
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
this._applyFillStyle();
|
|
808
914
|
this._ctx.save();
|
|
915
|
+
this._ctx.setAntialias(this._state.imageSmoothingEnabled ? Cairo.Antialias.DEFAULT : Cairo.Antialias.NONE);
|
|
809
916
|
this._ctx.moveTo(x + xOff, y + yOff);
|
|
810
917
|
PangoCairo.show_layout(this._ctx, layout);
|
|
811
918
|
this._ctx.restore();
|
|
@@ -819,6 +926,7 @@ class CanvasRenderingContext2D {
|
|
|
819
926
|
const xOff = this._getTextAlignOffset(layout);
|
|
820
927
|
const yOff = this._getTextBaselineOffset(layout);
|
|
821
928
|
this._ctx.save();
|
|
929
|
+
this._ctx.setAntialias(this._state.imageSmoothingEnabled ? Cairo.Antialias.DEFAULT : Cairo.Antialias.NONE);
|
|
822
930
|
this._ctx.moveTo(x + xOff, y + yOff);
|
|
823
931
|
PangoCairo.layout_path(this._ctx, layout);
|
|
824
932
|
this._ctx.stroke();
|
|
@@ -828,24 +936,26 @@ class CanvasRenderingContext2D {
|
|
|
828
936
|
this._ensureSurface();
|
|
829
937
|
const layout = this._createTextLayout(text);
|
|
830
938
|
const [inkRect, logicalRect] = layout.get_pixel_extents();
|
|
939
|
+
const baselinePx = layout.get_baseline() / Pango.SCALE;
|
|
940
|
+
const actualAscent = Math.max(0, baselinePx - inkRect.y);
|
|
941
|
+
const actualDescent = Math.max(0, inkRect.y + inkRect.height - baselinePx);
|
|
831
942
|
const fontDesc = layout.get_font_description() || this._parseFontToDescription(this._state.font);
|
|
832
|
-
const
|
|
833
|
-
const
|
|
834
|
-
const
|
|
835
|
-
const descent = metrics.get_descent() / Pango.SCALE;
|
|
943
|
+
const metrics = layout.get_context().get_metrics(fontDesc, null);
|
|
944
|
+
const fontAscent = metrics.get_ascent() / Pango.SCALE;
|
|
945
|
+
const fontDescent = metrics.get_descent() / Pango.SCALE;
|
|
836
946
|
return {
|
|
837
947
|
width: logicalRect.width,
|
|
838
|
-
actualBoundingBoxAscent:
|
|
839
|
-
actualBoundingBoxDescent:
|
|
840
|
-
actualBoundingBoxLeft: -inkRect.x,
|
|
948
|
+
actualBoundingBoxAscent: actualAscent,
|
|
949
|
+
actualBoundingBoxDescent: actualDescent,
|
|
950
|
+
actualBoundingBoxLeft: Math.max(0, -inkRect.x),
|
|
841
951
|
actualBoundingBoxRight: inkRect.x + inkRect.width,
|
|
842
|
-
fontBoundingBoxAscent:
|
|
843
|
-
fontBoundingBoxDescent:
|
|
952
|
+
fontBoundingBoxAscent: fontAscent,
|
|
953
|
+
fontBoundingBoxDescent: fontDescent,
|
|
844
954
|
alphabeticBaseline: 0,
|
|
845
|
-
emHeightAscent:
|
|
846
|
-
emHeightDescent:
|
|
847
|
-
hangingBaseline:
|
|
848
|
-
ideographicBaseline: -
|
|
955
|
+
emHeightAscent: fontAscent,
|
|
956
|
+
emHeightDescent: fontDescent,
|
|
957
|
+
hangingBaseline: fontAscent * 0.8,
|
|
958
|
+
ideographicBaseline: -fontDescent
|
|
849
959
|
};
|
|
850
960
|
}
|
|
851
961
|
// ---- toDataURL/toBlob support ----
|
package/lib/esm/color.js
CHANGED
|
@@ -166,6 +166,23 @@ function parseColor(color) {
|
|
|
166
166
|
a: rgbMatch[4] !== void 0 ? parseComponent(rgbMatch[4], 1) : 1
|
|
167
167
|
};
|
|
168
168
|
}
|
|
169
|
+
const hslMatch = trimmed.match(
|
|
170
|
+
/^hsla?\(\s*(\d+(?:\.\d+)?)\s*[,\s]\s*(\d+(?:\.\d+)?)(%)?\s*[,\s]\s*(\d+(?:\.\d+)?)(%)?\s*(?:[,/]\s*(\d+(?:\.\d+)?%?))?\s*\)$/
|
|
171
|
+
);
|
|
172
|
+
if (hslMatch) {
|
|
173
|
+
let h = parseFloat(hslMatch[1]);
|
|
174
|
+
let s = parseFloat(hslMatch[2]);
|
|
175
|
+
const sPct = hslMatch[3] === "%";
|
|
176
|
+
let l = parseFloat(hslMatch[4]);
|
|
177
|
+
const lPct = hslMatch[5] === "%";
|
|
178
|
+
const a = hslMatch[6] !== void 0 ? parseComponent(hslMatch[6], 1) : 1;
|
|
179
|
+
if (h > 1) h /= 360;
|
|
180
|
+
if (sPct) s /= 100;
|
|
181
|
+
else if (s > 1) s /= 100;
|
|
182
|
+
if (lPct) l /= 100;
|
|
183
|
+
else if (l > 1) l /= 100;
|
|
184
|
+
return hslToRGBA(h, s, l, Math.max(0, Math.min(1, a)));
|
|
185
|
+
}
|
|
169
186
|
return null;
|
|
170
187
|
}
|
|
171
188
|
function parseHex(hex) {
|
|
@@ -200,6 +217,27 @@ function parseComponent(value, max) {
|
|
|
200
217
|
}
|
|
201
218
|
return parseFloat(value);
|
|
202
219
|
}
|
|
220
|
+
function hue2rgb(p, q, t) {
|
|
221
|
+
if (t < 0) t += 1;
|
|
222
|
+
if (t > 1) t -= 1;
|
|
223
|
+
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
224
|
+
if (t < 1 / 2) return q;
|
|
225
|
+
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
226
|
+
return p;
|
|
227
|
+
}
|
|
228
|
+
function hslToRGBA(h, s, l, a) {
|
|
229
|
+
let r, g, b;
|
|
230
|
+
if (s === 0) {
|
|
231
|
+
r = g = b = l;
|
|
232
|
+
} else {
|
|
233
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
234
|
+
const p = 2 * l - q;
|
|
235
|
+
r = hue2rgb(p, q, h + 1 / 3);
|
|
236
|
+
g = hue2rgb(p, q, h);
|
|
237
|
+
b = hue2rgb(p, q, h - 1 / 3);
|
|
238
|
+
}
|
|
239
|
+
return { r, g, b, a };
|
|
240
|
+
}
|
|
203
241
|
const BLACK = { r: 0, g: 0, b: 0, a: 1 };
|
|
204
242
|
const TRANSPARENT = { r: 0, g: 0, b: 0, a: 0 };
|
|
205
243
|
export {
|
|
@@ -46,7 +46,18 @@ export declare function cairoEllipse(ctx: Cairo.Context, x: number, y: number, r
|
|
|
46
46
|
* Implements the Canvas roundRect(x, y, w, h, radii) method.
|
|
47
47
|
*/
|
|
48
48
|
export declare function cairoRoundRect(ctx: Cairo.Context, x: number, y: number, w: number, h: number, radii: number | number[]): void;
|
|
49
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Map Canvas globalCompositeOperation to Cairo.Operator values.
|
|
51
|
+
*
|
|
52
|
+
* Cairo.Operator enum (verified runtime in GJS 1.86):
|
|
53
|
+
* CLEAR=0, SOURCE=1, OVER=2, IN=3, OUT=4, ATOP=5,
|
|
54
|
+
* DEST=6, DEST_OVER=7, DEST_IN=8, DEST_OUT=9, DEST_ATOP=10,
|
|
55
|
+
* XOR=11, ADD=12, SATURATE=13,
|
|
56
|
+
* MULTIPLY=14, SCREEN=15, OVERLAY=16, DARKEN=17, LIGHTEN=18,
|
|
57
|
+
* COLOR_DODGE=19, COLOR_BURN=20, HARD_LIGHT=21, SOFT_LIGHT=22,
|
|
58
|
+
* DIFFERENCE=23, EXCLUSION=24, HSL_HUE=25, HSL_SATURATION=26,
|
|
59
|
+
* HSL_COLOR=27, HSL_LUMINOSITY=28
|
|
60
|
+
*/
|
|
50
61
|
export declare const COMPOSITE_OP_MAP: Record<string, number>;
|
|
51
62
|
/** Map Canvas lineCap to Cairo.LineCap values */
|
|
52
63
|
export declare const LINE_CAP_MAP: Record<string, number>;
|
|
@@ -21,6 +21,13 @@ export declare class CanvasRenderingContext2D {
|
|
|
21
21
|
private _applyFillStyle;
|
|
22
22
|
/** Apply the current stroke style to the Cairo context. */
|
|
23
23
|
private _applyStrokeStyle;
|
|
24
|
+
/**
|
|
25
|
+
* Apply the current imageSmoothingEnabled + imageSmoothingQuality state
|
|
26
|
+
* to the currently installed Cairo source pattern. Per Canvas 2D spec,
|
|
27
|
+
* the filter is read from the context at *draw* time, not at pattern
|
|
28
|
+
* creation — so we re-apply it on every fill/stroke.
|
|
29
|
+
*/
|
|
30
|
+
private _applyPatternFilter;
|
|
24
31
|
/** Apply line properties to the Cairo context. */
|
|
25
32
|
private _applyLineStyle;
|
|
26
33
|
/** Apply compositing operator. */
|
|
@@ -30,9 +37,29 @@ export declare class CanvasRenderingContext2D {
|
|
|
30
37
|
/** Check if shadow rendering is needed. */
|
|
31
38
|
private _hasShadow;
|
|
32
39
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
40
|
+
* Convert a distance from device pixels to Cairo user space by inverting
|
|
41
|
+
* the linear part of the current CTM (translation doesn't affect distances).
|
|
42
|
+
*
|
|
43
|
+
* Canvas 2D spec: shadowOffsetX/Y are in CSS pixels and are NOT scaled by
|
|
44
|
+
* the current transform. This helper converts them to user-space offsets so
|
|
45
|
+
* that `ctx.moveTo(x + sdx, y + sdy)` produces the correct pixel offset
|
|
46
|
+
* regardless of any ctx.scale() or ctx.rotate() in effect.
|
|
47
|
+
*/
|
|
48
|
+
private _deviceToUserDistance;
|
|
49
|
+
/**
|
|
50
|
+
* Shadow rendering is intentionally a no-op.
|
|
51
|
+
*
|
|
52
|
+
* Proper Canvas 2D shadows require a Gaussian blur pass on an isolated
|
|
53
|
+
* temporary surface, which cannot be emulated reliably without a full
|
|
54
|
+
* Path2D replay or pixel-level manipulation. The previous implementation
|
|
55
|
+
* attempted to use a temp surface but never replayed the path onto it
|
|
56
|
+
* (because `drawOp` closes over the main context), leaving the shadow
|
|
57
|
+
* surface empty while still leaking memory.
|
|
58
|
+
*
|
|
59
|
+
* Excalibur and most 2D game engines bake glow/outline effects into
|
|
60
|
+
* sprites rather than relying on canvas shadows, so this no-op does not
|
|
61
|
+
* affect the showcase. A correct implementation is tracked as a
|
|
62
|
+
* separate Canvas 2D Phase-5 enhancement.
|
|
36
63
|
*/
|
|
37
64
|
private _renderShadow;
|
|
38
65
|
save(): void;
|
package/lib/types/color.d.ts
CHANGED
|
@@ -6,7 +6,11 @@ export interface RGBA {
|
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
8
|
* Parse a CSS color string into RGBA components (0-1 range).
|
|
9
|
-
* Supports: #rgb, #rrggbb, #rgba, #rrggbbaa, rgb(), rgba(), named colors, 'transparent'.
|
|
9
|
+
* Supports: #rgb, #rrggbb, #rgba, #rrggbbaa, rgb(), rgba(), hsl(), hsla(), named colors, 'transparent'.
|
|
10
|
+
*
|
|
11
|
+
* Also handles Excalibur's non-standard HSL format where h/s/l are all in 0-1 range (not degrees/%).
|
|
12
|
+
* Excalibur's Color.toString() returns `hsla(h, s, l, a)` with values in 0-1 normalized form
|
|
13
|
+
* (e.g. Color.White → "hsla(0, 0, 1, 1)", Color.Black → "hsla(0, 0, 0, 1)").
|
|
10
14
|
*/
|
|
11
15
|
export declare function parseColor(color: string): RGBA | null;
|
|
12
16
|
/** Default color: opaque black */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/canvas2d-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Cairo-backed Canvas 2D core (CanvasRenderingContext2D, Path2D, ImageData) — no GTK dependency",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "lib/esm/index.js",
|
|
@@ -30,18 +30,18 @@
|
|
|
30
30
|
"offscreen"
|
|
31
31
|
],
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@girs/gdk-4.0": "^4.0.0-4.0.0-rc.
|
|
34
|
-
"@girs/gdkpixbuf-2.0": "^2.0.0-4.0.0-rc.
|
|
35
|
-
"@girs/gjs": "^4.0.0-rc.
|
|
36
|
-
"@girs/glib-2.0": "^2.88.0-4.0.0-rc.
|
|
37
|
-
"@girs/gobject-2.0": "^2.88.0-4.0.0-rc.
|
|
38
|
-
"@girs/pango-1.0": "^1.57.1-4.0.0-rc.
|
|
39
|
-
"@girs/pangocairo-1.0": "^1.0.0-4.0.0-rc.
|
|
33
|
+
"@girs/gdk-4.0": "^4.0.0-4.0.0-rc.2",
|
|
34
|
+
"@girs/gdkpixbuf-2.0": "^2.0.0-4.0.0-rc.2",
|
|
35
|
+
"@girs/gjs": "^4.0.0-rc.2",
|
|
36
|
+
"@girs/glib-2.0": "^2.88.0-4.0.0-rc.2",
|
|
37
|
+
"@girs/gobject-2.0": "^2.88.0-4.0.0-rc.2",
|
|
38
|
+
"@girs/pango-1.0": "^1.57.1-4.0.0-rc.2",
|
|
39
|
+
"@girs/pangocairo-1.0": "^1.0.0-4.0.0-rc.2"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@gjsify/cli": "^0.1.
|
|
43
|
-
"@gjsify/unit": "^0.1.
|
|
44
|
-
"@types/node": "^25.
|
|
42
|
+
"@gjsify/cli": "^0.1.10",
|
|
43
|
+
"@gjsify/unit": "^0.1.10",
|
|
44
|
+
"@types/node": "^25.6.0",
|
|
45
45
|
"typescript": "^6.0.2"
|
|
46
46
|
}
|
|
47
47
|
}
|
package/src/cairo-utils.ts
CHANGED
|
@@ -198,19 +198,30 @@ export function cairoRoundRect(
|
|
|
198
198
|
ctx.closePath();
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
/**
|
|
201
|
+
/**
|
|
202
|
+
* Map Canvas globalCompositeOperation to Cairo.Operator values.
|
|
203
|
+
*
|
|
204
|
+
* Cairo.Operator enum (verified runtime in GJS 1.86):
|
|
205
|
+
* CLEAR=0, SOURCE=1, OVER=2, IN=3, OUT=4, ATOP=5,
|
|
206
|
+
* DEST=6, DEST_OVER=7, DEST_IN=8, DEST_OUT=9, DEST_ATOP=10,
|
|
207
|
+
* XOR=11, ADD=12, SATURATE=13,
|
|
208
|
+
* MULTIPLY=14, SCREEN=15, OVERLAY=16, DARKEN=17, LIGHTEN=18,
|
|
209
|
+
* COLOR_DODGE=19, COLOR_BURN=20, HARD_LIGHT=21, SOFT_LIGHT=22,
|
|
210
|
+
* DIFFERENCE=23, EXCLUSION=24, HSL_HUE=25, HSL_SATURATION=26,
|
|
211
|
+
* HSL_COLOR=27, HSL_LUMINOSITY=28
|
|
212
|
+
*/
|
|
202
213
|
export const COMPOSITE_OP_MAP: Record<string, number> = {
|
|
203
214
|
'source-over': 2, // OVER
|
|
204
|
-
'source-in':
|
|
205
|
-
'source-out':
|
|
206
|
-
'source-atop':
|
|
207
|
-
'destination-over':
|
|
208
|
-
'destination-in':
|
|
209
|
-
'destination-out':
|
|
210
|
-
'destination-atop':
|
|
215
|
+
'source-in': 3, // IN
|
|
216
|
+
'source-out': 4, // OUT
|
|
217
|
+
'source-atop': 5, // ATOP
|
|
218
|
+
'destination-over': 7, // DEST_OVER
|
|
219
|
+
'destination-in': 8, // DEST_IN
|
|
220
|
+
'destination-out': 9, // DEST_OUT
|
|
221
|
+
'destination-atop': 10,// DEST_ATOP
|
|
211
222
|
'lighter': 12, // ADD
|
|
212
223
|
'copy': 1, // SOURCE
|
|
213
|
-
'xor':
|
|
224
|
+
'xor': 11, // XOR
|
|
214
225
|
'multiply': 14, // MULTIPLY
|
|
215
226
|
'screen': 15, // SCREEN
|
|
216
227
|
'overlay': 16, // OVERLAY
|