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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"install": "node scripts/install.js",
"build": "node-gyp rebuild",
"demo": "node demo/demo.js",
"test": "node test/app-lifecycle.js && node test/animation.js && node test/layer-transform.js && node test/contents-gravity.js && node test/contents-center.js && node test/accessibility-display.js && node test/ignores-mouse-events.js && node test/popup-menu.js && node test/pasteboard-write.js && node test/surface-blit.js && node test/surface-faded.js && node test/image-smoothing.js && node test/surface-colorspace.js && node test/video-surface.js && node test/player.js && node test/round-rect-xy.js && node test/clip-rule.js && node test/control-bezels.js && node test/text-features.js && node test/font-from-data.js && node test/text-coverage.js && node test/text-typesetter.js && node test/text-line-height.js && node test/text-line-width.js && node test/text-justify.js && node test/text-scale.js && node test/symbols.js && node test/eventkit-permissions.js && node test/calendars.js && node test/calendar-writes.js && node test/screencolor.js && node test/notifications.js && node test/threaded.js && node test/threaded-verbs.js && node test/threaded-frames.js && node test/threaded-resize.js && node test/ending-worker.js && node test/live-resize.js && node test/activation-policy.js"
"test": "node test/app-lifecycle.js && node test/animation.js && node test/layer-transform.js && node test/contents-gravity.js && node test/contents-center.js && node test/accessibility-display.js && node test/ignores-mouse-events.js && node test/popup-menu.js && node test/pasteboard-write.js && node test/surface-blit.js && node test/surface-faded.js && node test/image-smoothing.js && node test/surface-colorspace.js && node test/video-surface.js && node test/player.js && node test/round-rect-xy.js && node test/clip-rule.js && node test/radial-gradient.js && node test/control-bezels.js && node test/text-features.js && node test/font-from-data.js && node test/text-coverage.js && node test/text-typesetter.js && node test/text-line-height.js && node test/text-line-width.js && node test/text-justify.js && node test/text-scale.js && node test/symbols.js && node test/eventkit-permissions.js && node test/calendars.js && node test/calendar-writes.js && node test/screencolor.js && node test/notifications.js && node test/threaded.js && node test/threaded-verbs.js && node test/threaded-frames.js && node test/threaded-resize.js && node test/ending-worker.js && node test/live-resize.js && node test/activation-policy.js"
},
"dependencies": {
"node-addon-api": "^8.3.0"
Expand Down
78 changes: 60 additions & 18 deletions src/backend.mm
Original file line number Diff line number Diff line change
Expand Up @@ -4371,16 +4371,9 @@ static void KeepPathAround(CGContextRef ctx, void (^op)(void)) {
return info.Env().Undefined();
}

// fillLinearGradient(surface, x0, y0, x1, y1, stops [offset,r,g,b,a,...],
// mode: 0 = fill current path, 1 = fill rect args follow)
static Napi::Value CtxFillLinearGradient(const Napi::CallbackInfo& info) {
CALSurface* s = SurfaceFrom(info[0]);
if (!s) return info.Env().Undefined();
double x0 = info[1].As<Napi::Number>().DoubleValue();
double y0 = info[2].As<Napi::Number>().DoubleValue();
double x1 = info[3].As<Napi::Number>().DoubleValue();
double y1 = info[4].As<Napi::Number>().DoubleValue();
Napi::Array stopsArr = info[5].As<Napi::Array>();
// A gradient's stops, flat as [offset, r, g, b, a, ...] with each offset in
// [0, 1], in sRGB: what fillLinearGradient and fillRadialGradient draw.
static CGGradientRef GradientOfStops(Napi::Array stopsArr) {
std::vector<CGFloat> locs;
std::vector<CGFloat> comps;
for (uint32_t i = 0; i + 4 < stopsArr.Length(); i += 5) {
Expand All @@ -4394,17 +4387,37 @@ static void KeepPathAround(CGContextRef ctx, void (^op)(void)) {
CGGradientRef grad = CGGradientCreateWithColorComponents(
cs, comps.data(), locs.data(), locs.size());
CGColorSpaceRelease(cs);
CGContextSaveGState(s->ctx);
if (info.Length() > 6 && info[6].IsNumber()) {
// clip to the given rect (fillRect with a gradient fillStyle)
CGContextClipToRect(s->ctx,
CGRectMake(info[6].As<Napi::Number>().DoubleValue(),
info[7].As<Napi::Number>().DoubleValue(),
info[8].As<Napi::Number>().DoubleValue(),
info[9].As<Napi::Number>().DoubleValue()));
return grad;
}

// What a gradient fill is cut to, in a saved state: the rect whose x, y, w
// and h start at argument `at` where one follows (fillRect with a gradient
// fillStyle), else the current path, which is kept for after.
static void ClipForGradient(CALSurface* s, const Napi::CallbackInfo& info,
size_t at) {
if (info.Length() > at + 3 && info[at].IsNumber()) {
CGContextClipToRect(
s->ctx, CGRectMake(info[at].As<Napi::Number>().DoubleValue(),
info[at + 1].As<Napi::Number>().DoubleValue(),
info[at + 2].As<Napi::Number>().DoubleValue(),
info[at + 3].As<Napi::Number>().DoubleValue()));
} else {
KeepPathAround(s->ctx, ^{ CGContextClip(s->ctx); });
}
}

// fillLinearGradient(surface, x0, y0, x1, y1, stops [offset,r,g,b,a,...],
// mode: 0 = fill current path, 1 = fill rect args follow)
static Napi::Value CtxFillLinearGradient(const Napi::CallbackInfo& info) {
CALSurface* s = SurfaceFrom(info[0]);
if (!s) return info.Env().Undefined();
double x0 = info[1].As<Napi::Number>().DoubleValue();
double y0 = info[2].As<Napi::Number>().DoubleValue();
double x1 = info[3].As<Napi::Number>().DoubleValue();
double y1 = info[4].As<Napi::Number>().DoubleValue();
CGGradientRef grad = GradientOfStops(info[5].As<Napi::Array>());
CGContextSaveGState(s->ctx);
ClipForGradient(s, info, 6);
CGContextDrawLinearGradient(
s->ctx, grad, CGPointMake(x0, y0), CGPointMake(x1, y1),
kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation);
Expand All @@ -4413,6 +4426,34 @@ static void KeepPathAround(CGContextRef ctx, void (^op)(void)) {
return info.Env().Undefined();
}

// fillRadialGradient(surface, x0, y0, r0, x1, y1, r1,
// stops [offset,r,g,b,a,...], x?, y?, w?, h?)
//
// canvas's createRadialGradient: the colours spread from the start circle to
// the end one, and the end colours carried on past both, as canvas extends
// them — the current path filled with it, or the rect that follows the
// stops. CoreGraphics' two-circle gradient is canvas's: the same cone where
// the circles do not nest, painted from the end circle in.
static Napi::Value CtxFillRadialGradient(const Napi::CallbackInfo& info) {
CALSurface* s = SurfaceFrom(info[0]);
if (!s) return info.Env().Undefined();
double x0 = info[1].As<Napi::Number>().DoubleValue();
double y0 = info[2].As<Napi::Number>().DoubleValue();
double r0 = info[3].As<Napi::Number>().DoubleValue();
double x1 = info[4].As<Napi::Number>().DoubleValue();
double y1 = info[5].As<Napi::Number>().DoubleValue();
double r1 = info[6].As<Napi::Number>().DoubleValue();
CGGradientRef grad = GradientOfStops(info[7].As<Napi::Array>());
CGContextSaveGState(s->ctx);
ClipForGradient(s, info, 8);
CGContextDrawRadialGradient(
s->ctx, grad, CGPointMake(x0, y0), r0, CGPointMake(x1, y1), r1,
kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation);
CGContextRestoreGState(s->ctx);
CGGradientRelease(grad);
return info.Env().Undefined();
}

// drawSurface(dst, src, sx, sy, sw, sh, dx, dy, dw, dh)
static Napi::Value CtxDrawSurface(const Napi::CallbackInfo& info) {
CALSurface* dst = SurfaceFrom(info[0]);
Expand Down Expand Up @@ -7981,6 +8022,7 @@ void InitBackend(Napi::Env env, Napi::Object exports) {
BFN("ctxClearRect", CtxClearRect);
BFN("ctxFillRects", CtxFillRects);
BFN("ctxFillLinearGradient", CtxFillLinearGradient);
BFN("ctxFillRadialGradient", CtxFillRadialGradient);
BFN("ctxDrawSurface", CtxDrawSurface);
BFN("ctxDrawSurfaceFaded", CtxDrawSurfaceFaded);
BFN("ctxPutImageData", CtxPutImageData);
Expand Down
83 changes: 83 additions & 0 deletions test/radial-gradient.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
'use strict';
// ctxFillRadialGradient(surface, x0, y0, r0, x1, y1, r1, stops, x?, y?, w?, h?):
// canvas's createRadialGradient, which react-x11's context had nothing to
// hand to — it answered with a linear gradient along no line, so every
// radial gradient a page drew, a CSS one included, came out flat in one
// colour.
//
// Checked: the start colour at the centre, the end colour at the end
// circle and carried on past it, a colour between the two halfway out; the
// rect form fills that rect and nothing outside it; the path form fills the
// path, and the path is still there after; and two circles that do not
// share a centre move the colours with them. Exits 0 when every expectation
// held.

const { native: n } = require('..');

let failed = 0;
const fail = (msg, ...rest) => {
console.error('radial-gradient:', msg, ...rest);
failed++;
};
const ok = (cond, msg, ...rest) => {
if (!cond) fail(msg, ...rest);
};

const W = 100;
const px = (s, x, y) => Array.from(n.ctxGetImageData(s, x, y, 1, 1));
const near = (a, b, tol = 6) => a.every((v, i) => Math.abs(v - b[i]) <= tol);
// red at the centre to blue at 40px
const STOPS = [0, 1, 0, 0, 1, 1, 0, 0, 1, 1];

{
const s = n.createSurface(W, W, 1);
n.ctxFillRadialGradient(s, 50, 50, 0, 50, 50, 40, STOPS, 0, 0, W, W);
ok(near(px(s, 50, 50), [255, 0, 0, 255]), 'red at the centre', px(s, 50, 50));
const mid = px(s, 70, 50);
ok(mid[0] > 90 && mid[0] < 165 && mid[2] > 90 && mid[2] < 165,
'between the two halfway out', mid);
ok(near(px(s, 95, 50), [0, 0, 255, 255]), 'blue past the end circle',
px(s, 95, 50));
ok(near(px(s, 2, 2), [0, 0, 255, 255]), 'blue in the corner', px(s, 2, 2));
n.releaseSurface?.(s);
}

{
// the rect form fills that rect and nothing else
const s = n.createSurface(W, W, 1);
n.ctxFillRadialGradient(s, 50, 50, 0, 50, 50, 40, STOPS, 30, 30, 40, 40);
ok(px(s, 50, 50)[3] === 255, 'the rect is filled');
ok(px(s, 10, 10)[3] === 0, 'outside the rect is not', px(s, 10, 10));
n.releaseSurface?.(s);
}

{
// the path form: the path filled, and kept
const s = n.createSurface(W, W, 1);
n.ctxBeginPath(s);
n.ctxRect(s, 0, 0, 50, W);
n.ctxFillRadialGradient(s, 50, 50, 0, 50, 50, 40, STOPS);
ok(px(s, 25, 50)[3] === 255, 'the path is filled');
ok(px(s, 75, 50)[3] === 0, 'outside the path is not', px(s, 75, 50));
n.ctxSetFillColor(s, 0, 1, 0, 1);
n.ctxFill(s, false);
ok(near(px(s, 25, 50), [0, 255, 0, 255]), 'the path is still there after',
px(s, 25, 50));
n.releaseSurface?.(s);
}

{
// the circles move the colours: centred at 20,50 the red is there
const s = n.createSurface(W, W, 1);
n.ctxFillRadialGradient(s, 20, 50, 0, 20, 50, 40, STOPS, 0, 0, W, W);
ok(near(px(s, 20, 50), [255, 0, 0, 255]), 'red where the circle is',
px(s, 20, 50));
ok(near(px(s, 80, 50), [0, 0, 255, 255]), 'blue far from it', px(s, 80, 50));
n.releaseSurface?.(s);
}

if (failed) {
console.error(`radial-gradient: ${failed} expectation(s) failed`);
process.exit(1);
}
console.log('radial-gradient: ok');
Loading