@gjsify/canvas2d 0.1.0

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.
@@ -0,0 +1,36 @@
1
+ // CanvasGradient implementation backed by Cairo gradient patterns
2
+ // Reference: https://developer.mozilla.org/en-US/docs/Web/API/CanvasGradient
3
+
4
+ import Cairo from 'cairo';
5
+ import { parseColor } from './color.js';
6
+
7
+ /**
8
+ * CanvasGradient wrapping a Cairo LinearGradient or RadialGradient.
9
+ */
10
+ export class CanvasGradient {
11
+ private _pattern: Cairo.LinearGradient | Cairo.RadialGradient;
12
+
13
+ constructor(
14
+ type: 'linear' | 'radial',
15
+ x0: number, y0: number,
16
+ x1: number, y1: number,
17
+ r0?: number, r1?: number,
18
+ ) {
19
+ if (type === 'radial') {
20
+ this._pattern = new Cairo.RadialGradient(x0, y0, r0!, x1, y1, r1!);
21
+ } else {
22
+ this._pattern = new Cairo.LinearGradient(x0, y0, x1, y1);
23
+ }
24
+ }
25
+
26
+ addColorStop(offset: number, color: string): void {
27
+ const parsed = parseColor(color);
28
+ if (!parsed) return;
29
+ this._pattern.addColorStopRGBA(offset, parsed.r, parsed.g, parsed.b, parsed.a);
30
+ }
31
+
32
+ /** @internal Get the underlying Cairo pattern for rendering. */
33
+ _getCairoPattern(): Cairo.LinearGradient | Cairo.RadialGradient {
34
+ return this._pattern;
35
+ }
36
+ }
@@ -0,0 +1,131 @@
1
+ // Path2D implementation for Canvas 2D context
2
+ // Reference: https://developer.mozilla.org/en-US/docs/Web/API/Path2D
3
+ // Records path operations and replays them on a Cairo context.
4
+
5
+ import { quadraticToCubic, cairoRoundRect } from './cairo-utils.js';
6
+
7
+ /** A recorded path operation. */
8
+ type PathOp =
9
+ | { type: 'moveTo'; x: number; y: number }
10
+ | { type: 'lineTo'; x: number; y: number }
11
+ | { type: 'closePath' }
12
+ | { type: 'bezierCurveTo'; cp1x: number; cp1y: number; cp2x: number; cp2y: number; x: number; y: number }
13
+ | { type: 'quadraticCurveTo'; cpx: number; cpy: number; x: number; y: number }
14
+ | { type: 'arc'; x: number; y: number; radius: number; startAngle: number; endAngle: number; ccw: boolean }
15
+ | { type: 'ellipse'; x: number; y: number; rx: number; ry: number; rotation: number; startAngle: number; endAngle: number; ccw: boolean }
16
+ | { type: 'rect'; x: number; y: number; w: number; h: number }
17
+ | { type: 'roundRect'; x: number; y: number; w: number; h: number; radii: number | number[] };
18
+
19
+ /**
20
+ * Path2D records path operations for later replay on a CanvasRenderingContext2D.
21
+ */
22
+ export class Path2D {
23
+ /** @internal Recorded operations */
24
+ _ops: PathOp[] = [];
25
+
26
+ constructor(pathOrSvg?: Path2D | string) {
27
+ if (pathOrSvg instanceof Path2D) {
28
+ this._ops = [...pathOrSvg._ops];
29
+ }
30
+ // SVG path string parsing is not implemented (complex, rarely needed)
31
+ }
32
+
33
+ addPath(path: Path2D): void {
34
+ this._ops.push(...path._ops);
35
+ }
36
+
37
+ moveTo(x: number, y: number): void {
38
+ this._ops.push({ type: 'moveTo', x, y });
39
+ }
40
+
41
+ lineTo(x: number, y: number): void {
42
+ this._ops.push({ type: 'lineTo', x, y });
43
+ }
44
+
45
+ closePath(): void {
46
+ this._ops.push({ type: 'closePath' });
47
+ }
48
+
49
+ bezierCurveTo(cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number): void {
50
+ this._ops.push({ type: 'bezierCurveTo', cp1x, cp1y, cp2x, cp2y, x, y });
51
+ }
52
+
53
+ quadraticCurveTo(cpx: number, cpy: number, x: number, y: number): void {
54
+ this._ops.push({ type: 'quadraticCurveTo', cpx, cpy, x, y });
55
+ }
56
+
57
+ arc(x: number, y: number, radius: number, startAngle: number, endAngle: number, counterclockwise = false): void {
58
+ this._ops.push({ type: 'arc', x, y, radius, startAngle, endAngle, ccw: counterclockwise });
59
+ }
60
+
61
+ ellipse(x: number, y: number, radiusX: number, radiusY: number, rotation: number, startAngle: number, endAngle: number, counterclockwise = false): void {
62
+ if (radiusX < 0 || radiusY < 0) throw new RangeError('The radii provided are negative');
63
+ this._ops.push({ type: 'ellipse', x, y, rx: radiusX, ry: radiusY, rotation, startAngle, endAngle, ccw: counterclockwise });
64
+ }
65
+
66
+ rect(x: number, y: number, w: number, h: number): void {
67
+ this._ops.push({ type: 'rect', x, y, w, h });
68
+ }
69
+
70
+ roundRect(x: number, y: number, w: number, h: number, radii: number | number[] = 0): void {
71
+ this._ops.push({ type: 'roundRect', x, y, w, h, radii });
72
+ }
73
+
74
+ /**
75
+ * @internal Replay all recorded path operations onto a Cairo context.
76
+ */
77
+ _replayOnCairo(ctx: import('cairo').default.Context): void {
78
+ let lastX = 0, lastY = 0;
79
+
80
+ for (const op of this._ops) {
81
+ switch (op.type) {
82
+ case 'moveTo':
83
+ ctx.moveTo(op.x, op.y);
84
+ lastX = op.x; lastY = op.y;
85
+ break;
86
+ case 'lineTo':
87
+ ctx.lineTo(op.x, op.y);
88
+ lastX = op.x; lastY = op.y;
89
+ break;
90
+ case 'closePath':
91
+ ctx.closePath();
92
+ break;
93
+ case 'bezierCurveTo':
94
+ ctx.curveTo(op.cp1x, op.cp1y, op.cp2x, op.cp2y, op.x, op.y);
95
+ lastX = op.x; lastY = op.y;
96
+ break;
97
+ case 'quadraticCurveTo': {
98
+ const { cp1x, cp1y, cp2x, cp2y } = quadraticToCubic(lastX, lastY, op.cpx, op.cpy, op.x, op.y);
99
+ ctx.curveTo(cp1x, cp1y, cp2x, cp2y, op.x, op.y);
100
+ lastX = op.x; lastY = op.y;
101
+ break;
102
+ }
103
+ case 'arc':
104
+ if (op.ccw) {
105
+ ctx.arcNegative(op.x, op.y, op.radius, op.startAngle, op.endAngle);
106
+ } else {
107
+ ctx.arc(op.x, op.y, op.radius, op.startAngle, op.endAngle);
108
+ }
109
+ break;
110
+ case 'ellipse':
111
+ ctx.save();
112
+ ctx.translate(op.x, op.y);
113
+ ctx.rotate(op.rotation);
114
+ ctx.scale(op.rx, op.ry);
115
+ if (op.ccw) {
116
+ ctx.arcNegative(0, 0, 1, op.startAngle, op.endAngle);
117
+ } else {
118
+ ctx.arc(0, 0, 1, op.startAngle, op.endAngle);
119
+ }
120
+ ctx.restore();
121
+ break;
122
+ case 'rect':
123
+ ctx.rectangle(op.x, op.y, op.w, op.h);
124
+ break;
125
+ case 'roundRect':
126
+ cairoRoundRect(ctx, op.x, op.y, op.w, op.h, op.radii);
127
+ break;
128
+ }
129
+ }
130
+ }
131
+ }
@@ -0,0 +1,75 @@
1
+ // CanvasPattern implementation backed by Cairo SurfacePattern
2
+ // Reference: https://developer.mozilla.org/en-US/docs/Web/API/CanvasPattern
3
+
4
+ import Cairo from 'cairo';
5
+ import Gdk from 'gi://Gdk?version=4.0';
6
+
7
+ /**
8
+ * CanvasPattern wrapping a Cairo SurfacePattern.
9
+ */
10
+ export class CanvasPattern {
11
+ private _pattern: Cairo.SurfacePattern;
12
+
13
+ private constructor(surface: Cairo.ImageSurface, repetition: string | null) {
14
+ this._pattern = new Cairo.SurfacePattern(surface);
15
+
16
+ // Set extend mode based on repetition
17
+ // setExtend exists at runtime on SurfacePattern but is missing from GIR types
18
+ const pat = this._pattern as any;
19
+ switch (repetition) {
20
+ case 'repeat':
21
+ case '':
22
+ case null:
23
+ pat.setExtend(Cairo.Extend.REPEAT);
24
+ break;
25
+ case 'repeat-x':
26
+ case 'repeat-y':
27
+ // Cairo doesn't have separate x/y repeat — use REPEAT as approximation
28
+ pat.setExtend(Cairo.Extend.REPEAT);
29
+ break;
30
+ case 'no-repeat':
31
+ pat.setExtend(Cairo.Extend.NONE);
32
+ break;
33
+ }
34
+ }
35
+
36
+ /** Create a CanvasPattern from a supported image source. Returns null if unsupported. */
37
+ static create(image: any, repetition: string | null): CanvasPattern | null {
38
+ // HTMLImageElement (GdkPixbuf-backed)
39
+ if ('isPixbuf' in image && typeof (image as any).isPixbuf === 'function' && (image as any).isPixbuf()) {
40
+ const pixbuf = (image as any)._pixbuf as import('@girs/gdkpixbuf-2.0').default.Pixbuf;
41
+ // Create a Cairo surface from the pixbuf
42
+ const w = pixbuf.get_width();
43
+ const h = pixbuf.get_height();
44
+ const surface = new Cairo.ImageSurface(Cairo.Format.ARGB32, w, h);
45
+ const ctx = new Cairo.Context(surface);
46
+ Gdk.cairo_set_source_pixbuf(ctx as any, pixbuf, 0, 0);
47
+ ctx.paint();
48
+ ctx.$dispose();
49
+ return new CanvasPattern(surface, repetition);
50
+ }
51
+
52
+ // HTMLCanvasElement with a 2D context
53
+ if (typeof image?.getContext === 'function') {
54
+ const ctx2d = image.getContext('2d');
55
+ if (ctx2d && typeof ctx2d._getSurface === 'function') {
56
+ const sourceSurface = ctx2d._getSurface() as Cairo.ImageSurface;
57
+ const w = sourceSurface.getWidth();
58
+ const h = sourceSurface.getHeight();
59
+ const surface = new Cairo.ImageSurface(Cairo.Format.ARGB32, w, h);
60
+ const ctx = new Cairo.Context(surface);
61
+ ctx.setSourceSurface(sourceSurface, 0, 0);
62
+ ctx.paint();
63
+ ctx.$dispose();
64
+ return new CanvasPattern(surface, repetition);
65
+ }
66
+ }
67
+
68
+ return null;
69
+ }
70
+
71
+ /** @internal Get the underlying Cairo pattern for rendering. */
72
+ _getCairoPattern(): Cairo.SurfacePattern {
73
+ return this._pattern;
74
+ }
75
+ }