diff --git a/src/bridge.h b/src/bridge.h index 64c0f2d..d99981b 100644 --- a/src/bridge.h +++ b/src/bridge.h @@ -39,11 +39,14 @@ extern IDWriteFactory3* g_dwrite; // all, and models a clip as a push/pop — so the stack is ours to keep, and a // restore has to unwind exactly the clips its save left open. -enum class PathOp { Move, Line, Curve, Quad, Close, Rect, RoundRect, Arc, Ellipse }; +enum class PathOp { Move, Line, Curve, Quad, Close, Rect, RoundRect, Arc, Ellipse, RoundRectXY }; struct PathCmd { PathOp op; float a = 0, b = 0, c = 0, d = 0, e = 0, f = 0, g = 0, h = 0; + // the four more a RoundRectXY carries: its corners' radii are eight + // numbers, e to l, a pair to a corner from the top left + float i = 0, j = 0, k = 0, l = 0; bool flag = false; }; diff --git a/src/surface.cc b/src/surface.cc index 2dbfcec..1baf3bf 100644 --- a/src/surface.cc +++ b/src/surface.cc @@ -104,6 +104,36 @@ void AppendRoundRect(ID2D1GeometrySink* sink, const PathCmd& cmd) { sink->EndFigure(D2D1_FIGURE_END_CLOSED); } +// A rounded rect whose corners are elliptical, each its own pair of radii +// (e, f top left; g, h top right; i, j bottom right; k, l bottom left): +// canvas's roundRect with `{ x, y }` radii. The caller has scaled them to +// fit, as canvas does. A corner with no extent on one of its axes is no +// curve at all, and is square — two lines would cut it off diagonally. +void AppendRoundRectXY(ID2D1GeometrySink* sink, const PathCmd& cmd) { + const float x = cmd.a, y = cmd.b, r = cmd.a + cmd.c, b = cmd.b + cmd.d; + float radii[8] = {cmd.e, cmd.f, cmd.g, cmd.h, cmd.i, cmd.j, cmd.k, cmd.l}; + for (int n = 0; n < 4; n++) { + if (!(radii[n * 2] > 0 && radii[n * 2 + 1] > 0)) radii[n * 2] = radii[n * 2 + 1] = 0; + } + const float tlx = radii[0], tly = radii[1], trx = radii[2], trY = radii[3]; + const float brx = radii[4], brY = radii[5], blx = radii[6], blY = radii[7]; + const auto corner = [&](D2D1_POINT_2F to, float rx, float ry) { + D2D1_ARC_SEGMENT s = {to, {rx, ry}, 0, D2D1_SWEEP_DIRECTION_CLOCKWISE, + D2D1_ARC_SIZE_SMALL}; + sink->AddArc(s); + }; + sink->BeginFigure({x + tlx, y}, D2D1_FIGURE_BEGIN_FILLED); + sink->AddLine({r - trx, y}); + if (trx > 0) corner({r, y + trY}, trx, trY); + sink->AddLine({r, b - brY}); + if (brx > 0) corner({r - brx, b}, brx, brY); + sink->AddLine({x + blx, b}); + if (blx > 0) corner({x, b - blY}, blx, blY); + sink->AddLine({x, y + tly}); + if (tlx > 0) corner({x + tlx, y}, tlx, tly); + sink->EndFigure(D2D1_FIGURE_END_CLOSED); +} + } // namespace // --------------------------------------------------------------------------- @@ -315,6 +345,13 @@ ID2D1PathGeometry* BuildPath(Surface* surface, bool evenOdd) { } AppendRoundRect(sink, cmd); break; + case PathOp::RoundRectXY: + if (figureOpen) { + sink->EndFigure(D2D1_FIGURE_END_OPEN); + figureOpen = false; + } + AppendRoundRectXY(sink, cmd); + break; case PathOp::Arc: AppendArc(sink, cmd.a, cmd.b, cmd.c, cmd.c, cmd.d, cmd.e, cmd.flag, figureOpen); @@ -584,6 +621,22 @@ Napi::Value CtxRoundRect(const Napi::CallbackInfo& info) { return info.Env().Undefined(); } +// roundRectXY(surface, x, y, w, h, tlx, tly, trx, try, brx, bry, blx, bly) +// — appkit's verb: elliptical corners, each its own pair of radii. +Napi::Value CtxRoundRectXY(const Napi::CallbackInfo& info) { + Surface* s = Arg(info); + if (s) { + PathCmd cmd = {PathOp::RoundRectXY, F(info, 1), F(info, 2), F(info, 3), + F(info, 4), F(info, 5), F(info, 6), F(info, 7), F(info, 8)}; + cmd.i = F(info, 9); + cmd.j = F(info, 10); + cmd.k = F(info, 11); + cmd.l = F(info, 12); + s->path.push_back(cmd); + } + return info.Env().Undefined(); +} + Napi::Value CtxArc(const Napi::CallbackInfo& info) { Surface* s = Arg(info); if (s) { @@ -638,8 +691,11 @@ Napi::Value CtxClosePath(const Napi::CallbackInfo& info) { // 1 line x y 6 round rect x y w h r0 r1 r2 r3 // 2 curve c1 c2 x y (6) 7 arc cx cy r start end anticlockwise // 3 quad c x y (4) 8 ellipse cx cy rx ry -// 4 close +// 4 close 9 round rect x y w h, then each corner's +// x and y radius from the top left (12) // +// A bridge that answers ctxRoundRectXY reads op 9 too: that is how +// react-x11 knows it may send one, where an older bridge is sent curves. // A stream that ends inside an op, or names one this does not know, stops // there: what came before it stands, which is what the per-point verbs do // with a path a painter abandons half way. @@ -649,12 +705,12 @@ Napi::Value CtxPath(const Napi::CallbackInfo& info) { Napi::Float64Array stream = info[1].As(); const double* c = stream.Data(); const size_t n = stream.ElementLength(); - static const int kArgs[] = {2, 2, 6, 4, 0, 4, 8, 6, 4}; + static const int kArgs[] = {2, 2, 6, 4, 0, 4, 8, 6, 4, 12}; const auto f = [&](size_t at) { return static_cast(c[at]); }; size_t i = 0; while (i < n) { const double code = c[i]; - if (!(code >= 0 && code <= 8)) break; + if (!(code >= 0 && code <= 9)) break; const int op = static_cast(code); if (i + 1 + kArgs[op] > n) break; const size_t a = i + 1; @@ -685,6 +741,16 @@ Napi::Value CtxPath(const Napi::CallbackInfo& info) { case 8: s->path.push_back({PathOp::Ellipse, f(a), f(a + 1), f(a + 2), f(a + 3)}); break; + case 9: { + PathCmd cmd = {PathOp::RoundRectXY, f(a), f(a + 1), f(a + 2), f(a + 3), + f(a + 4), f(a + 5), f(a + 6), f(a + 7)}; + cmd.i = f(a + 8); + cmd.j = f(a + 9); + cmd.k = f(a + 10); + cmd.l = f(a + 11); + s->path.push_back(cmd); + break; + } } i = a + kArgs[op]; } @@ -704,6 +770,9 @@ struct Primitive { Shape shape = Shape::None; D2D1_RECT_F rect = {}; float radius = 0; + // a round rect's vertical radius, where its corners are elliptical and + // all alike — Direct2D's rounded rect has the two + float radiusY = 0; D2D1_ELLIPSE ellipse = {}; }; @@ -729,9 +798,22 @@ Primitive PrimitiveOf(const Surface* s) { const float limit = (std::min)(out.rect.right - out.rect.left, out.rect.bottom - out.rect.top) / 2; out.radius = (std::min)(cmd.e, limit); + out.radiusY = out.radius; out.shape = out.radius > 0 ? Shape::RoundRect : Shape::Rect; return out; } + case PathOp::RoundRectXY: { + // four corners alike, which one Direct2D rounded rect draws + if (cmd.e != cmd.g || cmd.e != cmd.i || cmd.e != cmd.k) return out; + if (cmd.f != cmd.h || cmd.f != cmd.j || cmd.f != cmd.l) return out; + if (!(cmd.c >= 0 && cmd.d >= 0)) return out; + out.rect = box(cmd.a, cmd.b, cmd.c, cmd.d); + const bool curved = cmd.e > 0 && cmd.f > 0; + out.radius = curved ? (std::min)(cmd.e, (out.rect.right - out.rect.left) / 2) : 0; + out.radiusY = curved ? (std::min)(cmd.f, (out.rect.bottom - out.rect.top) / 2) : 0; + out.shape = curved ? Shape::RoundRect : Shape::Rect; + return out; + } case PathOp::Ellipse: out.shape = Shape::Ellipse; out.ellipse = D2D1::Ellipse(D2D1::Point2F(cmd.a, cmd.b), cmd.c, cmd.d); @@ -766,7 +848,7 @@ Napi::Value CtxFill(const Napi::CallbackInfo& info) { s->dc->FillRectangle(primitive.rect, brush); } else if (primitive.shape == Shape::RoundRect) { s->dc->FillRoundedRectangle( - D2D1::RoundedRect(primitive.rect, primitive.radius, primitive.radius), brush); + D2D1::RoundedRect(primitive.rect, primitive.radius, primitive.radiusY), brush); } else { s->dc->FillEllipse(primitive.ellipse, brush); } @@ -797,7 +879,7 @@ Napi::Value CtxStroke(const Napi::CallbackInfo& info) { s->dc->DrawRectangle(primitive.rect, brush, width, style); } else if (primitive.shape == Shape::RoundRect) { s->dc->DrawRoundedRectangle( - D2D1::RoundedRect(primitive.rect, primitive.radius, primitive.radius), brush, + D2D1::RoundedRect(primitive.rect, primitive.radius, primitive.radiusY), brush, width, style); } else { s->dc->DrawEllipse(primitive.ellipse, brush, width, style); @@ -1108,6 +1190,7 @@ void InitSurfaceExports(Napi::Env env, Napi::Object exports) { set("ctxLineTo", CtxLineTo); set("ctxRect", CtxRect); set("ctxRoundRect", CtxRoundRect); + set("ctxRoundRectXY", CtxRoundRectXY); set("ctxArc", CtxArc); set("ctxEllipse", CtxEllipse); set("ctxCurveTo", CtxCurveTo); diff --git a/test/path-stream.js b/test/path-stream.js index 1814e0d..d254be0 100644 --- a/test/path-stream.js +++ b/test/path-stream.js @@ -46,6 +46,7 @@ const stream = [ 6, 75, 50, 35, 30, 6, 6, 6, 6, 7, 30, 70, 10, 0, Math.PI * 1.5, 0, 8, 100, 70, 12, 8, + 9, 45, 20, 30, 25, 10, 5, 0, 0, 6, 12, 3, 3, ]; const perPoint = render((s) => { @@ -58,6 +59,7 @@ const perPoint = render((s) => { win32.ctxRoundRect(s, 75, 50, 35, 30, 6, 6, 6, 6); win32.ctxArc(s, 30, 70, 10, 0, Math.PI * 1.5, false); win32.ctxEllipse(s, 100, 70, 12, 8); + win32.ctxRoundRectXY(s, 45, 20, 30, 25, 10, 5, 0, 0, 6, 12, 3, 3); }); const whole = render((s) => win32.ctxPath(s, new Float64Array(stream))); assert.ok(same(perPoint, whole), 'the stream draws what the verbs draw'); diff --git a/test/primitives.js b/test/primitives.js index 1c6fe90..a8d51f8 100644 --- a/test/primitives.js +++ b/test/primitives.js @@ -72,6 +72,13 @@ const cases = { if (path) win32.ctxMoveTo(s, 100, 45); win32.ctxEllipse(s, 60, 45, 40, 25); }, + // corners elliptical and all alike: one Direct2D rounded rect, which has + // an x and a y radius of its own + 'elliptical round rect': (s, path) => { + win32.ctxBeginPath(s); + if (path) win32.ctxMoveTo(s, 20, 15); + win32.ctxRoundRectXY(s, 20, 15, 80, 60, 24, 10, 24, 10, 24, 10, 24, 10); + }, }; for (const [name, shape] of Object.entries(cases)) { @@ -107,5 +114,22 @@ for (const [name, shape] of Object.entries(cases)) { assert.ok(pixels[j] < 120, 'and the top-right one is not'); } +// Elliptical corners of their own: a path, the corners cut as ellipses — +// shallow where wider than tall — and one with no extent on an axis square. +{ + const draw = (s) => { + colour(s); + win32.ctxBeginPath(s); + win32.ctxRoundRectXY(s, 20, 15, 80, 60, 40, 10, 0, 20, 20, 20, 10, 0); + win32.ctxFill(s, false); + }; + const pixels = render(draw); + const at = (x, y) => pixels[(y * W + x) * 4]; + assert.ok(at(21, 16) > 200, 'the top-left corner is cut away'); + assert.ok(at(50, 16) < 120, 'but not past most of its shallow ellipse'); + assert.ok(at(98, 16) < 120, 'a corner 0 across is square'); + assert.ok(at(21, 73) < 120, 'and so is one 0 down'); +} + win32.stop(); console.log('primitives: ok');