diff --git a/.changeset/six-years-tickle.md b/.changeset/six-years-tickle.md new file mode 100644 index 00000000..6a70e992 --- /dev/null +++ b/.changeset/six-years-tickle.md @@ -0,0 +1,5 @@ +--- +'nxjs-runtime': patch +--- + +Add Canvas `isPointInPath()` diff --git a/apps/tests/src/canvas.ts b/apps/tests/src/canvas.ts index 8b078fea..1c17c93f 100644 --- a/apps/tests/src/canvas.ts +++ b/apps/tests/src/canvas.ts @@ -19,4 +19,11 @@ test('`CanvasRenderingContext2D#getImageData()`', () => { assert.equal(data.data[3], 255); }); +test('`CanvasRenderingContext2D#isPointInPath()`', () => { + ctx.beginPath(); + ctx.rect(10, 10, 100, 100); + assert.equal(ctx.isPointInPath(30, 70), true); + assert.equal(ctx.isPointInPath(8, 8), false); +}); + test.run(); diff --git a/packages/runtime/src/canvas/canvas-rendering-context-2d.ts b/packages/runtime/src/canvas/canvas-rendering-context-2d.ts index aae16f9f..77998c82 100644 --- a/packages/runtime/src/canvas/canvas-rendering-context-2d.ts +++ b/packages/runtime/src/canvas/canvas-rendering-context-2d.ts @@ -54,28 +54,6 @@ export class CanvasRenderingContext2D { // TODO: implement declare direction: string; declare fontKerning: string; - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/clip) */ - clip(fillRule?: CanvasFillRule): void; - clip(path: Path2D, fillRule?: CanvasFillRule): void; - clip(path: unknown, fillRule?: unknown) { - stub(); - } - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/isPointInPath) */ - isPointInPath(x: number, y: number, fillRule?: CanvasFillRule): boolean; - isPointInPath( - path: Path2D, - x: number, - y: number, - fillRule?: CanvasFillRule - ): boolean; - isPointInPath( - path: unknown, - x: unknown, - y: unknown, - fillRule?: unknown - ): boolean { - stub(); - } /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/isPointInStroke) */ isPointInStroke(x: number, y: number): boolean; isPointInStroke(path: Path2D, x: number, y: number): boolean; @@ -322,6 +300,64 @@ export class CanvasRenderingContext2D { stub(); } + /** + * Turns the current or given path into the current clipping region. The + * previous clipping region, if any, is intersected with the current or + * given path to create the new clipping region. + * + * @example + * + * ```typescript + * // Create circular clipping region + * ctx.beginPath(); + * ctx.arc(100, 75, 50, 0, Math.PI * 2); + * ctx.clip(); + * + * // Draw stuff that gets clipped + * ctx.fillStyle = "blue"; + * ctx.fillRect(0, 0, canvas.width, canvas.height); + * ctx.fillStyle = "orange"; + * ctx.fillRect(0, 0, 100, 100); + * ``` + * + * @see https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/clip + */ + clip(fillRule?: CanvasFillRule): void; + clip(path: Path2D, fillRule?: CanvasFillRule): void; + clip(path: unknown, fillRule?: unknown) { + stub(); + } + + /** + * Reports whether or not the specified point is contained in the current path. + * + * @example + * + * ```typescript + * ctx.beginPath(); + * ctx.rect(10, 10, 100, 100); + * console.log(ctx.isPointInPath(30, 70)); + * // true + * ``` + * + * @see https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/isPointInPath + */ + isPointInPath(x: number, y: number, fillRule?: CanvasFillRule): boolean; + isPointInPath( + path: Path2D, + x: number, + y: number, + fillRule?: CanvasFillRule + ): boolean; + isPointInPath( + path: unknown, + x: unknown, + y: unknown, + fillRule?: unknown + ): boolean { + stub(); + } + /** * Attempts to add a straight line from the current point to the start of * the current sub-path. If the shape has already been closed or has only diff --git a/packages/runtime/src/canvas/offscreen-canvas-rendering-context-2d.ts b/packages/runtime/src/canvas/offscreen-canvas-rendering-context-2d.ts index ec3e2719..11fdb020 100644 --- a/packages/runtime/src/canvas/offscreen-canvas-rendering-context-2d.ts +++ b/packages/runtime/src/canvas/offscreen-canvas-rendering-context-2d.ts @@ -54,28 +54,6 @@ export class OffscreenCanvasRenderingContext2D { // TODO: implement declare direction: string; declare fontKerning: string; - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/clip) */ - clip(fillRule?: CanvasFillRule): void; - clip(path: Path2D, fillRule?: CanvasFillRule): void; - clip(path: unknown, fillRule?: unknown) { - stub(); - } - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/isPointInPath) */ - isPointInPath(x: number, y: number, fillRule?: CanvasFillRule): boolean; - isPointInPath( - path: Path2D, - x: number, - y: number, - fillRule?: CanvasFillRule - ): boolean; - isPointInPath( - path: unknown, - x: unknown, - y: unknown, - fillRule?: unknown - ): boolean { - stub(); - } /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/isPointInStroke) */ isPointInStroke(x: number, y: number): boolean; isPointInStroke(path: Path2D, x: number, y: number): boolean; @@ -320,6 +298,64 @@ export class OffscreenCanvasRenderingContext2D { stub(); } + /** + * Turns the current or given path into the current clipping region. The + * previous clipping region, if any, is intersected with the current or + * given path to create the new clipping region. + * + * @example + * + * ```typescript + * // Create circular clipping region + * ctx.beginPath(); + * ctx.arc(100, 75, 50, 0, Math.PI * 2); + * ctx.clip(); + * + * // Draw stuff that gets clipped + * ctx.fillStyle = "blue"; + * ctx.fillRect(0, 0, canvas.width, canvas.height); + * ctx.fillStyle = "orange"; + * ctx.fillRect(0, 0, 100, 100); + * ``` + * + * @see https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/clip + */ + clip(fillRule?: CanvasFillRule): void; + clip(path: Path2D, fillRule?: CanvasFillRule): void; + clip(path: unknown, fillRule?: unknown) { + stub(); + } + + /** + * Reports whether or not the specified point is contained in the current path. + * + * @example + * + * ```typescript + * ctx.beginPath(); + * ctx.rect(10, 10, 100, 100); + * console.log(ctx.isPointInPath(30, 70)); + * // true + * ``` + * + * @see https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/isPointInPath + */ + isPointInPath(x: number, y: number, fillRule?: CanvasFillRule): boolean; + isPointInPath( + path: Path2D, + x: number, + y: number, + fillRule?: CanvasFillRule + ): boolean; + isPointInPath( + path: unknown, + x: unknown, + y: unknown, + fillRule?: unknown + ): boolean { + stub(); + } + /** * Attempts to add a straight line from the current point to the start of * the current sub-path. If the shape has already been closed or has only diff --git a/source/canvas.c b/source/canvas.c index eed6c809..2edfe06f 100644 --- a/source/canvas.c +++ b/source/canvas.c @@ -98,6 +98,14 @@ static void set_fill_rule(JSContext *ctx, JSValueConst fill_rule, cairo_t *cr) cairo_set_fill_rule(cr, rule); } +static void apply_path(JSContext *ctx, JSValue this_val, JSValue path) { + nx_context_t *nx_ctx = JS_GetContextOpaque(ctx); + JSValue apply_path_func = JS_GetPropertyStr(ctx, nx_ctx->init_obj, "applyPath"); + JSValue apply_path_argv[] = {this_val, path}; + JS_Call(ctx, apply_path_func, JS_NULL, 2, apply_path_argv); + JS_FreeValue(ctx, apply_path_func); +} + static void save_path(nx_canvas_context_2d_t *context) { context->path = cairo_copy_path_flat(context->ctx); @@ -166,6 +174,37 @@ static JSValue nx_canvas_context_2d_move_to(JSContext *ctx, JSValueConst this_va return JS_UNDEFINED; } +static JSValue nx_canvas_context_2d_is_point_in_path(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) +{ + CANVAS_CONTEXT_THIS; + JSValue path = JS_NULL; + if (argc > 0 && JS_IsObject(argv[0])) { + path = argv[0]; + argc--; + argv++; + } + bool is_in = false; + if (argc >= 2) { + double args[2]; + if (js_validate_doubles_args(ctx, argv, args, 2, 0)) + return JS_EXCEPTION; + if (argc == 3 && JS_IsString(argv[2])) { + set_fill_rule(ctx, argv[2], cr); + } + bool needs_restore = false; + if (!JS_IsNull(path)) { + needs_restore = true; + save_path(context); + apply_path(ctx, this_val, path); + } + is_in = cairo_in_fill(cr, args[0], args[1]); + if (needs_restore) { + restore_path(context); + } + } + return JS_NewBool(ctx, is_in); +} + static JSValue nx_canvas_context_2d_line_to(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { CANVAS_CONTEXT_THIS; @@ -1674,7 +1713,7 @@ static JSValue nx_canvas_context_2d_fill(JSContext *ctx, JSValueConst this_val, { fill_rule = argv[0]; } - else + else if (!JS_IsUndefined(argv[0])) { return JS_ThrowTypeError(ctx, "Expected Path2D or string at index 0"); } @@ -1693,7 +1732,7 @@ static JSValue nx_canvas_context_2d_fill(JSContext *ctx, JSValueConst this_val, { fill_rule = argv[1]; } - else + else if (!JS_IsUndefined(argv[1])) { return JS_ThrowTypeError(ctx, "Expected string at index 1"); } @@ -1709,11 +1748,7 @@ static JSValue nx_canvas_context_2d_fill(JSContext *ctx, JSValueConst this_val, else { save_path(context); - nx_context_t *nx_ctx = JS_GetContextOpaque(ctx); - JSValue apply_path_func = JS_GetPropertyStr(ctx, nx_ctx->init_obj, "applyPath"); - JSValue apply_path_argv[] = {this_val, argv[0]}; - JS_Call(ctx, apply_path_func, JS_NULL, 2, apply_path_argv); - JS_FreeValue(ctx, apply_path_func); + apply_path(ctx, this_val, path); fill(context, false); restore_path(context); } @@ -1742,11 +1777,7 @@ static JSValue nx_canvas_context_2d_stroke(JSContext *ctx, JSValueConst this_val else { save_path(context); - nx_context_t *nx_ctx = JS_GetContextOpaque(ctx); - JSValue apply_path_func = JS_GetPropertyStr(ctx, nx_ctx->init_obj, "applyPath"); - JSValue apply_path_argv[] = {this_val, argv[0]}; - JS_Call(ctx, apply_path_func, JS_NULL, 2, apply_path_argv); - JS_FreeValue(ctx, apply_path_func); + apply_path(ctx, this_val, path); stroke(context, false); restore_path(context); } @@ -2673,6 +2704,7 @@ static JSValue nx_canvas_context_2d_init_class(JSContext *ctx, JSValueConst this NX_DEF_FUNC(proto, "fillRect", nx_canvas_context_2d_fill_rect, 4); NX_DEF_FUNC(proto, "fillText", nx_canvas_context_2d_fill_text, 3); NX_DEF_FUNC(proto, "getLineDash", nx_canvas_context_2d_get_line_dash, 0); + NX_DEF_FUNC(proto, "isPointInPath", nx_canvas_context_2d_is_point_in_path, 2); NX_DEF_FUNC(proto, "lineTo", nx_canvas_context_2d_line_to, 2); NX_DEF_FUNC(proto, "measureText", nx_canvas_context_2d_measure_text, 1); NX_DEF_FUNC(proto, "moveTo", nx_canvas_context_2d_move_to, 2);