Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/bridge.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand Down
93 changes: 88 additions & 5 deletions src/surface.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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.
Expand All @@ -649,12 +705,12 @@ Napi::Value CtxPath(const Napi::CallbackInfo& info) {
Napi::Float64Array stream = info[1].As<Napi::Float64Array>();
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<float>(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<int>(code);
if (i + 1 + kArgs[op] > n) break;
const size_t a = i + 1;
Expand Down Expand Up @@ -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];
}
Expand All @@ -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 = {};
};

Expand All @@ -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);
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions test/path-stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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');
Expand Down
24 changes: 24 additions & 0 deletions test/primitives.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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');
Loading