@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.
- package/README.md +33 -0
- package/lib/esm/cairo-utils.js +172 -0
- package/lib/esm/canvas-drawing-area.js +112 -0
- package/lib/esm/canvas-gradient.js +23 -0
- package/lib/esm/canvas-path.js +104 -0
- package/lib/esm/canvas-pattern.js +58 -0
- package/lib/esm/canvas-rendering-context-2d.js +865 -0
- package/lib/esm/canvas-state.js +39 -0
- package/lib/esm/color.js +209 -0
- package/lib/esm/image-data.js +22 -0
- package/lib/esm/index.js +43 -0
- package/lib/types/cairo-utils.d.ts +54 -0
- package/lib/types/canvas-drawing-area.d.ts +482 -0
- package/lib/types/canvas-gradient.d.ts +11 -0
- package/lib/types/canvas-path.d.ts +80 -0
- package/lib/types/canvas-pattern.d.ts +12 -0
- package/lib/types/canvas-rendering-context-2d.d.ts +155 -0
- package/lib/types/canvas-state.d.ts +27 -0
- package/lib/types/color.d.ts +15 -0
- package/lib/types/image-data.d.ts +12 -0
- package/lib/types/index.d.ts +7 -0
- package/package.json +50 -0
- package/src/cairo-utils.ts +243 -0
- package/src/canvas-drawing-area.ts +164 -0
- package/src/canvas-gradient.ts +36 -0
- package/src/canvas-path.ts +131 -0
- package/src/canvas-pattern.ts +75 -0
- package/src/canvas-rendering-context-2d.ts +981 -0
- package/src/canvas-state.ts +77 -0
- package/src/color.ts +125 -0
- package/src/image-data.ts +34 -0
- package/src/index.spec.ts +828 -0
- package/src/index.ts +49 -0
- package/src/test.mts +6 -0
- package/tmp/.tsbuildinfo +1 -0
- package/tsconfig.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# @gjsify/canvas2d
|
|
2
|
+
|
|
3
|
+
GJS implementation of CanvasRenderingContext2D using Cairo and PangoCairo. Provides Canvas2DWidget extending Gtk.DrawingArea.
|
|
4
|
+
|
|
5
|
+
Part of the [gjsify](https://github.com/gjsify/gjsify) project — Node.js and Web APIs for GJS (GNOME JavaScript).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @gjsify/canvas2d
|
|
11
|
+
# or
|
|
12
|
+
yarn add @gjsify/canvas2d
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { Canvas2DWidget } from '@gjsify/canvas2d';
|
|
19
|
+
|
|
20
|
+
const widget = new Canvas2DWidget();
|
|
21
|
+
widget.installGlobals();
|
|
22
|
+
|
|
23
|
+
widget.onReady((canvas, ctx) => {
|
|
24
|
+
ctx.fillStyle = 'red';
|
|
25
|
+
ctx.fillRect(0, 0, 100, 100);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
window.set_child(widget);
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## License
|
|
32
|
+
|
|
33
|
+
MIT
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
function quadraticToCubic(cx, cy, cpx, cpy, x, y) {
|
|
2
|
+
return {
|
|
3
|
+
cp1x: cx + 2 / 3 * (cpx - cx),
|
|
4
|
+
cp1y: cy + 2 / 3 * (cpy - cy),
|
|
5
|
+
cp2x: x + 2 / 3 * (cpx - x),
|
|
6
|
+
cp2y: y + 2 / 3 * (cpy - y)
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
function computeArcTo(x0, y0, x1, y1, x2, y2, radius) {
|
|
10
|
+
const dx0 = x0 - x1;
|
|
11
|
+
const dy0 = y0 - y1;
|
|
12
|
+
const dx1 = x2 - x1;
|
|
13
|
+
const dy1 = y2 - y1;
|
|
14
|
+
const len0 = Math.sqrt(dx0 * dx0 + dy0 * dy0);
|
|
15
|
+
const len1 = Math.sqrt(dx1 * dx1 + dy1 * dy1);
|
|
16
|
+
if (len0 === 0 || len1 === 0) return null;
|
|
17
|
+
const ux0 = dx0 / len0;
|
|
18
|
+
const uy0 = dy0 / len0;
|
|
19
|
+
const ux1 = dx1 / len1;
|
|
20
|
+
const uy1 = dy1 / len1;
|
|
21
|
+
const cross = ux0 * uy1 - uy0 * ux1;
|
|
22
|
+
if (Math.abs(cross) < 1e-10) return null;
|
|
23
|
+
const dot = ux0 * ux1 + uy0 * uy1;
|
|
24
|
+
const halfAngle = Math.acos(Math.max(-1, Math.min(1, dot))) / 2;
|
|
25
|
+
const tanDist = radius / Math.tan(halfAngle);
|
|
26
|
+
const tx0 = x1 + ux0 * tanDist;
|
|
27
|
+
const ty0 = y1 + uy0 * tanDist;
|
|
28
|
+
const tx1 = x1 + ux1 * tanDist;
|
|
29
|
+
const ty1 = y1 + uy1 * tanDist;
|
|
30
|
+
const centerDist = radius / Math.sin(halfAngle);
|
|
31
|
+
const bisectX = (ux0 + ux1) / 2;
|
|
32
|
+
const bisectY = (uy0 + uy1) / 2;
|
|
33
|
+
const bisectLen = Math.sqrt(bisectX * bisectX + bisectY * bisectY);
|
|
34
|
+
const cx = x1 + bisectX / bisectLen * centerDist;
|
|
35
|
+
const cy = y1 + bisectY / bisectLen * centerDist;
|
|
36
|
+
const startAngle = Math.atan2(ty0 - cy, tx0 - cx);
|
|
37
|
+
const endAngle = Math.atan2(ty1 - cy, tx1 - cx);
|
|
38
|
+
const counterclockwise = cross > 0;
|
|
39
|
+
return { tx0, ty0, tx1, ty1, cx, cy, startAngle, endAngle, counterclockwise };
|
|
40
|
+
}
|
|
41
|
+
function cairoArcTo(ctx, x0, y0, x1, y1, x2, y2, radius) {
|
|
42
|
+
const result = computeArcTo(x0, y0, x1, y1, x2, y2, radius);
|
|
43
|
+
if (!result) {
|
|
44
|
+
ctx.lineTo(x1, y1);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const { tx0, ty0, cx, cy, startAngle, endAngle, counterclockwise } = result;
|
|
48
|
+
ctx.lineTo(tx0, ty0);
|
|
49
|
+
if (counterclockwise) {
|
|
50
|
+
ctx.arcNegative(cx, cy, radius, startAngle, endAngle);
|
|
51
|
+
} else {
|
|
52
|
+
ctx.arc(cx, cy, radius, startAngle, endAngle);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function cairoEllipse(ctx, x, y, radiusX, radiusY, rotation, startAngle, endAngle, counterclockwise) {
|
|
56
|
+
ctx.save();
|
|
57
|
+
ctx.translate(x, y);
|
|
58
|
+
ctx.rotate(rotation);
|
|
59
|
+
ctx.scale(radiusX, radiusY);
|
|
60
|
+
if (counterclockwise) {
|
|
61
|
+
ctx.arcNegative(0, 0, 1, startAngle, endAngle);
|
|
62
|
+
} else {
|
|
63
|
+
ctx.arc(0, 0, 1, startAngle, endAngle);
|
|
64
|
+
}
|
|
65
|
+
ctx.restore();
|
|
66
|
+
}
|
|
67
|
+
function cairoRoundRect(ctx, x, y, w, h, radii) {
|
|
68
|
+
let tl, tr, br, bl;
|
|
69
|
+
if (typeof radii === "number") {
|
|
70
|
+
tl = tr = br = bl = radii;
|
|
71
|
+
} else if (radii.length === 1) {
|
|
72
|
+
tl = tr = br = bl = radii[0];
|
|
73
|
+
} else if (radii.length === 2) {
|
|
74
|
+
tl = br = radii[0];
|
|
75
|
+
tr = bl = radii[1];
|
|
76
|
+
} else if (radii.length === 3) {
|
|
77
|
+
tl = radii[0];
|
|
78
|
+
tr = bl = radii[1];
|
|
79
|
+
br = radii[2];
|
|
80
|
+
} else {
|
|
81
|
+
tl = radii[0];
|
|
82
|
+
tr = radii[1];
|
|
83
|
+
br = radii[2];
|
|
84
|
+
bl = radii[3];
|
|
85
|
+
}
|
|
86
|
+
const maxR = Math.min(w / 2, h / 2);
|
|
87
|
+
tl = Math.min(tl, maxR);
|
|
88
|
+
tr = Math.min(tr, maxR);
|
|
89
|
+
br = Math.min(br, maxR);
|
|
90
|
+
bl = Math.min(bl, maxR);
|
|
91
|
+
const PI_2 = Math.PI / 2;
|
|
92
|
+
ctx.newSubPath();
|
|
93
|
+
ctx.arc(x + tl, y + tl, tl, Math.PI, Math.PI + PI_2);
|
|
94
|
+
ctx.arc(x + w - tr, y + tr, tr, -PI_2, 0);
|
|
95
|
+
ctx.arc(x + w - br, y + h - br, br, 0, PI_2);
|
|
96
|
+
ctx.arc(x + bl, y + h - bl, bl, PI_2, Math.PI);
|
|
97
|
+
ctx.closePath();
|
|
98
|
+
}
|
|
99
|
+
const COMPOSITE_OP_MAP = {
|
|
100
|
+
"source-over": 2,
|
|
101
|
+
// OVER
|
|
102
|
+
"source-in": 5,
|
|
103
|
+
// IN
|
|
104
|
+
"source-out": 6,
|
|
105
|
+
// OUT
|
|
106
|
+
"source-atop": 7,
|
|
107
|
+
// ATOP
|
|
108
|
+
"destination-over": 8,
|
|
109
|
+
// DEST_OVER
|
|
110
|
+
"destination-in": 9,
|
|
111
|
+
// DEST_IN
|
|
112
|
+
"destination-out": 10,
|
|
113
|
+
// DEST_OUT
|
|
114
|
+
"destination-atop": 11,
|
|
115
|
+
// DEST_ATOP
|
|
116
|
+
"lighter": 12,
|
|
117
|
+
// ADD
|
|
118
|
+
"copy": 1,
|
|
119
|
+
// SOURCE
|
|
120
|
+
"xor": 13,
|
|
121
|
+
// XOR
|
|
122
|
+
"multiply": 14,
|
|
123
|
+
// MULTIPLY
|
|
124
|
+
"screen": 15,
|
|
125
|
+
// SCREEN
|
|
126
|
+
"overlay": 16,
|
|
127
|
+
// OVERLAY
|
|
128
|
+
"darken": 17,
|
|
129
|
+
// DARKEN
|
|
130
|
+
"lighten": 18,
|
|
131
|
+
// LIGHTEN
|
|
132
|
+
"color-dodge": 19,
|
|
133
|
+
// COLOR_DODGE
|
|
134
|
+
"color-burn": 20,
|
|
135
|
+
// COLOR_BURN
|
|
136
|
+
"hard-light": 21,
|
|
137
|
+
// HARD_LIGHT
|
|
138
|
+
"soft-light": 22,
|
|
139
|
+
// SOFT_LIGHT
|
|
140
|
+
"difference": 23,
|
|
141
|
+
// DIFFERENCE
|
|
142
|
+
"exclusion": 24,
|
|
143
|
+
// EXCLUSION
|
|
144
|
+
"hue": 25,
|
|
145
|
+
// HSL_HUE
|
|
146
|
+
"saturation": 26,
|
|
147
|
+
// HSL_SATURATION
|
|
148
|
+
"color": 27,
|
|
149
|
+
// HSL_COLOR
|
|
150
|
+
"luminosity": 28
|
|
151
|
+
// HSL_LUMINOSITY
|
|
152
|
+
};
|
|
153
|
+
const LINE_CAP_MAP = {
|
|
154
|
+
"butt": 0,
|
|
155
|
+
"round": 1,
|
|
156
|
+
"square": 2
|
|
157
|
+
};
|
|
158
|
+
const LINE_JOIN_MAP = {
|
|
159
|
+
"miter": 0,
|
|
160
|
+
"round": 1,
|
|
161
|
+
"bevel": 2
|
|
162
|
+
};
|
|
163
|
+
export {
|
|
164
|
+
COMPOSITE_OP_MAP,
|
|
165
|
+
LINE_CAP_MAP,
|
|
166
|
+
LINE_JOIN_MAP,
|
|
167
|
+
cairoArcTo,
|
|
168
|
+
cairoEllipse,
|
|
169
|
+
cairoRoundRect,
|
|
170
|
+
computeArcTo,
|
|
171
|
+
quadraticToCubic
|
|
172
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import GObject from "gi://GObject";
|
|
2
|
+
import GLib from "gi://GLib?version=2.0";
|
|
3
|
+
import Gtk from "gi://Gtk?version=4.0";
|
|
4
|
+
import { HTMLCanvasElement as GjsifyHTMLCanvasElement } from "@gjsify/dom-elements";
|
|
5
|
+
import { attachEventControllers } from "@gjsify/event-bridge";
|
|
6
|
+
const Canvas2DWidget = GObject.registerClass(
|
|
7
|
+
{ GTypeName: "GjsifyCanvas2DWidget" },
|
|
8
|
+
class Canvas2DWidget2 extends Gtk.DrawingArea {
|
|
9
|
+
constructor(params) {
|
|
10
|
+
super(params);
|
|
11
|
+
this._canvas = null;
|
|
12
|
+
this._ctx = null;
|
|
13
|
+
this._readyCallbacks = [];
|
|
14
|
+
this._tickCallbackId = null;
|
|
15
|
+
this._frameCallback = null;
|
|
16
|
+
// Time origin in microseconds (GLib monotonic clock).
|
|
17
|
+
// Both requestAnimationFrame timestamps and performance.now() are
|
|
18
|
+
// relative to this origin, matching the browser DOMHighResTimeStamp spec.
|
|
19
|
+
this._timeOrigin = GLib.get_monotonic_time();
|
|
20
|
+
this.set_draw_func(this._onDraw.bind(this));
|
|
21
|
+
attachEventControllers(this, () => this._canvas);
|
|
22
|
+
this.connect("unrealize", () => {
|
|
23
|
+
if (this._tickCallbackId !== null) {
|
|
24
|
+
this.remove_tick_callback(this._tickCallbackId);
|
|
25
|
+
this._tickCallbackId = null;
|
|
26
|
+
}
|
|
27
|
+
if (this._ctx) {
|
|
28
|
+
this._ctx._dispose();
|
|
29
|
+
}
|
|
30
|
+
this._canvas = null;
|
|
31
|
+
this._ctx = null;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/** @internal Draw function called by GTK. Blits the Cairo surface to screen. */
|
|
35
|
+
_onDraw(_area, cr, width, height) {
|
|
36
|
+
if (!this._canvas) {
|
|
37
|
+
this._canvas = new GjsifyHTMLCanvasElement();
|
|
38
|
+
this._canvas.width = width;
|
|
39
|
+
this._canvas.height = height;
|
|
40
|
+
this._ctx = this._canvas.getContext("2d");
|
|
41
|
+
if (this._ctx) {
|
|
42
|
+
for (const cb of this._readyCallbacks) {
|
|
43
|
+
cb(this._canvas, this._ctx);
|
|
44
|
+
}
|
|
45
|
+
this._readyCallbacks = [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (this._canvas.width !== width || this._canvas.height !== height) {
|
|
49
|
+
this._canvas.width = width;
|
|
50
|
+
this._canvas.height = height;
|
|
51
|
+
}
|
|
52
|
+
if (this._ctx) {
|
|
53
|
+
const surface = this._ctx._getSurface();
|
|
54
|
+
cr.setSourceSurface(surface, 0, 0);
|
|
55
|
+
cr.paint();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** The HTMLCanvasElement backing this widget. Available after the first draw. */
|
|
59
|
+
get canvas() {
|
|
60
|
+
return this._canvas;
|
|
61
|
+
}
|
|
62
|
+
/** Get the 2D rendering context. Available after the first draw. */
|
|
63
|
+
getContext(_id) {
|
|
64
|
+
return this._ctx;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Register a callback to be invoked once the Canvas 2D context is ready.
|
|
68
|
+
* If the context is already available, the callback fires synchronously.
|
|
69
|
+
*/
|
|
70
|
+
onReady(cb) {
|
|
71
|
+
if (this._canvas && this._ctx) {
|
|
72
|
+
cb(this._canvas, this._ctx);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
this._readyCallbacks.push(cb);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Schedules a single animation frame callback, matching the browser `requestAnimationFrame` API.
|
|
79
|
+
* Backed by GTK frame clock (vsync-synced, typically ~60 FPS).
|
|
80
|
+
* Returns 0 (handle — cancel not yet implemented).
|
|
81
|
+
*/
|
|
82
|
+
requestAnimationFrame(cb) {
|
|
83
|
+
this._frameCallback = cb;
|
|
84
|
+
if (this._tickCallbackId === null) {
|
|
85
|
+
this._tickCallbackId = this.add_tick_callback((_widget, frameClock) => {
|
|
86
|
+
this._tickCallbackId = null;
|
|
87
|
+
const time = (frameClock.get_frame_time() - this._timeOrigin) / 1e3;
|
|
88
|
+
this._frameCallback?.(time);
|
|
89
|
+
this.queue_draw();
|
|
90
|
+
return GLib.SOURCE_REMOVE;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
this.queue_draw();
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Sets browser globals (`requestAnimationFrame`, `performance`) so that
|
|
98
|
+
* browser-targeted code works unchanged on GJS.
|
|
99
|
+
*/
|
|
100
|
+
installGlobals() {
|
|
101
|
+
globalThis.requestAnimationFrame = (cb) => this.requestAnimationFrame(cb);
|
|
102
|
+
const timeOrigin = this._timeOrigin;
|
|
103
|
+
globalThis.performance = {
|
|
104
|
+
now: () => (GLib.get_monotonic_time() - timeOrigin) / 1e3,
|
|
105
|
+
timeOrigin: Date.now()
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
);
|
|
110
|
+
export {
|
|
111
|
+
Canvas2DWidget
|
|
112
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import Cairo from "cairo";
|
|
2
|
+
import { parseColor } from "./color.js";
|
|
3
|
+
class CanvasGradient {
|
|
4
|
+
constructor(type, x0, y0, x1, y1, r0, r1) {
|
|
5
|
+
if (type === "radial") {
|
|
6
|
+
this._pattern = new Cairo.RadialGradient(x0, y0, r0, x1, y1, r1);
|
|
7
|
+
} else {
|
|
8
|
+
this._pattern = new Cairo.LinearGradient(x0, y0, x1, y1);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
addColorStop(offset, color) {
|
|
12
|
+
const parsed = parseColor(color);
|
|
13
|
+
if (!parsed) return;
|
|
14
|
+
this._pattern.addColorStopRGBA(offset, parsed.r, parsed.g, parsed.b, parsed.a);
|
|
15
|
+
}
|
|
16
|
+
/** @internal Get the underlying Cairo pattern for rendering. */
|
|
17
|
+
_getCairoPattern() {
|
|
18
|
+
return this._pattern;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export {
|
|
22
|
+
CanvasGradient
|
|
23
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { quadraticToCubic, cairoRoundRect } from "./cairo-utils.js";
|
|
2
|
+
class Path2D {
|
|
3
|
+
constructor(pathOrSvg) {
|
|
4
|
+
/** @internal Recorded operations */
|
|
5
|
+
this._ops = [];
|
|
6
|
+
if (pathOrSvg instanceof Path2D) {
|
|
7
|
+
this._ops = [...pathOrSvg._ops];
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
addPath(path) {
|
|
11
|
+
this._ops.push(...path._ops);
|
|
12
|
+
}
|
|
13
|
+
moveTo(x, y) {
|
|
14
|
+
this._ops.push({ type: "moveTo", x, y });
|
|
15
|
+
}
|
|
16
|
+
lineTo(x, y) {
|
|
17
|
+
this._ops.push({ type: "lineTo", x, y });
|
|
18
|
+
}
|
|
19
|
+
closePath() {
|
|
20
|
+
this._ops.push({ type: "closePath" });
|
|
21
|
+
}
|
|
22
|
+
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) {
|
|
23
|
+
this._ops.push({ type: "bezierCurveTo", cp1x, cp1y, cp2x, cp2y, x, y });
|
|
24
|
+
}
|
|
25
|
+
quadraticCurveTo(cpx, cpy, x, y) {
|
|
26
|
+
this._ops.push({ type: "quadraticCurveTo", cpx, cpy, x, y });
|
|
27
|
+
}
|
|
28
|
+
arc(x, y, radius, startAngle, endAngle, counterclockwise = false) {
|
|
29
|
+
this._ops.push({ type: "arc", x, y, radius, startAngle, endAngle, ccw: counterclockwise });
|
|
30
|
+
}
|
|
31
|
+
ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, counterclockwise = false) {
|
|
32
|
+
if (radiusX < 0 || radiusY < 0) throw new RangeError("The radii provided are negative");
|
|
33
|
+
this._ops.push({ type: "ellipse", x, y, rx: radiusX, ry: radiusY, rotation, startAngle, endAngle, ccw: counterclockwise });
|
|
34
|
+
}
|
|
35
|
+
rect(x, y, w, h) {
|
|
36
|
+
this._ops.push({ type: "rect", x, y, w, h });
|
|
37
|
+
}
|
|
38
|
+
roundRect(x, y, w, h, radii = 0) {
|
|
39
|
+
this._ops.push({ type: "roundRect", x, y, w, h, radii });
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* @internal Replay all recorded path operations onto a Cairo context.
|
|
43
|
+
*/
|
|
44
|
+
_replayOnCairo(ctx) {
|
|
45
|
+
let lastX = 0, lastY = 0;
|
|
46
|
+
for (const op of this._ops) {
|
|
47
|
+
switch (op.type) {
|
|
48
|
+
case "moveTo":
|
|
49
|
+
ctx.moveTo(op.x, op.y);
|
|
50
|
+
lastX = op.x;
|
|
51
|
+
lastY = op.y;
|
|
52
|
+
break;
|
|
53
|
+
case "lineTo":
|
|
54
|
+
ctx.lineTo(op.x, op.y);
|
|
55
|
+
lastX = op.x;
|
|
56
|
+
lastY = op.y;
|
|
57
|
+
break;
|
|
58
|
+
case "closePath":
|
|
59
|
+
ctx.closePath();
|
|
60
|
+
break;
|
|
61
|
+
case "bezierCurveTo":
|
|
62
|
+
ctx.curveTo(op.cp1x, op.cp1y, op.cp2x, op.cp2y, op.x, op.y);
|
|
63
|
+
lastX = op.x;
|
|
64
|
+
lastY = op.y;
|
|
65
|
+
break;
|
|
66
|
+
case "quadraticCurveTo": {
|
|
67
|
+
const { cp1x, cp1y, cp2x, cp2y } = quadraticToCubic(lastX, lastY, op.cpx, op.cpy, op.x, op.y);
|
|
68
|
+
ctx.curveTo(cp1x, cp1y, cp2x, cp2y, op.x, op.y);
|
|
69
|
+
lastX = op.x;
|
|
70
|
+
lastY = op.y;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
case "arc":
|
|
74
|
+
if (op.ccw) {
|
|
75
|
+
ctx.arcNegative(op.x, op.y, op.radius, op.startAngle, op.endAngle);
|
|
76
|
+
} else {
|
|
77
|
+
ctx.arc(op.x, op.y, op.radius, op.startAngle, op.endAngle);
|
|
78
|
+
}
|
|
79
|
+
break;
|
|
80
|
+
case "ellipse":
|
|
81
|
+
ctx.save();
|
|
82
|
+
ctx.translate(op.x, op.y);
|
|
83
|
+
ctx.rotate(op.rotation);
|
|
84
|
+
ctx.scale(op.rx, op.ry);
|
|
85
|
+
if (op.ccw) {
|
|
86
|
+
ctx.arcNegative(0, 0, 1, op.startAngle, op.endAngle);
|
|
87
|
+
} else {
|
|
88
|
+
ctx.arc(0, 0, 1, op.startAngle, op.endAngle);
|
|
89
|
+
}
|
|
90
|
+
ctx.restore();
|
|
91
|
+
break;
|
|
92
|
+
case "rect":
|
|
93
|
+
ctx.rectangle(op.x, op.y, op.w, op.h);
|
|
94
|
+
break;
|
|
95
|
+
case "roundRect":
|
|
96
|
+
cairoRoundRect(ctx, op.x, op.y, op.w, op.h, op.radii);
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export {
|
|
103
|
+
Path2D
|
|
104
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import Cairo from "cairo";
|
|
2
|
+
import Gdk from "gi://Gdk?version=4.0";
|
|
3
|
+
class CanvasPattern {
|
|
4
|
+
constructor(surface, repetition) {
|
|
5
|
+
this._pattern = new Cairo.SurfacePattern(surface);
|
|
6
|
+
const pat = this._pattern;
|
|
7
|
+
switch (repetition) {
|
|
8
|
+
case "repeat":
|
|
9
|
+
case "":
|
|
10
|
+
case null:
|
|
11
|
+
pat.setExtend(Cairo.Extend.REPEAT);
|
|
12
|
+
break;
|
|
13
|
+
case "repeat-x":
|
|
14
|
+
case "repeat-y":
|
|
15
|
+
pat.setExtend(Cairo.Extend.REPEAT);
|
|
16
|
+
break;
|
|
17
|
+
case "no-repeat":
|
|
18
|
+
pat.setExtend(Cairo.Extend.NONE);
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Create a CanvasPattern from a supported image source. Returns null if unsupported. */
|
|
23
|
+
static create(image, repetition) {
|
|
24
|
+
if ("isPixbuf" in image && typeof image.isPixbuf === "function" && image.isPixbuf()) {
|
|
25
|
+
const pixbuf = image._pixbuf;
|
|
26
|
+
const w = pixbuf.get_width();
|
|
27
|
+
const h = pixbuf.get_height();
|
|
28
|
+
const surface = new Cairo.ImageSurface(Cairo.Format.ARGB32, w, h);
|
|
29
|
+
const ctx = new Cairo.Context(surface);
|
|
30
|
+
Gdk.cairo_set_source_pixbuf(ctx, pixbuf, 0, 0);
|
|
31
|
+
ctx.paint();
|
|
32
|
+
ctx.$dispose();
|
|
33
|
+
return new CanvasPattern(surface, repetition);
|
|
34
|
+
}
|
|
35
|
+
if (typeof image?.getContext === "function") {
|
|
36
|
+
const ctx2d = image.getContext("2d");
|
|
37
|
+
if (ctx2d && typeof ctx2d._getSurface === "function") {
|
|
38
|
+
const sourceSurface = ctx2d._getSurface();
|
|
39
|
+
const w = sourceSurface.getWidth();
|
|
40
|
+
const h = sourceSurface.getHeight();
|
|
41
|
+
const surface = new Cairo.ImageSurface(Cairo.Format.ARGB32, w, h);
|
|
42
|
+
const ctx = new Cairo.Context(surface);
|
|
43
|
+
ctx.setSourceSurface(sourceSurface, 0, 0);
|
|
44
|
+
ctx.paint();
|
|
45
|
+
ctx.$dispose();
|
|
46
|
+
return new CanvasPattern(surface, repetition);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
/** @internal Get the underlying Cairo pattern for rendering. */
|
|
52
|
+
_getCairoPattern() {
|
|
53
|
+
return this._pattern;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export {
|
|
57
|
+
CanvasPattern
|
|
58
|
+
};
|