diff --git a/Readme.md b/Readme.md index bb852b7..d1294a6 100644 --- a/Readme.md +++ b/Readme.md @@ -14,9 +14,9 @@ [x] list of valid css colors [x] match canvas defaults (mostly, test suite from webkit would be nice) [x] transformations + [x] gradients [ ] make sure constructor names match [ ] anti aliasing - [ ] gradients [ ] Image [ ] patterns [ ] text diff --git a/examples/gradients.js b/examples/gradients.js new file mode 100644 index 0000000..3bbb880 --- /dev/null +++ b/examples/gradients.js @@ -0,0 +1,29 @@ + +/** + * Module dependencies. + */ + +var Canvas = require('../lib/canvas') + , canvas = new Canvas(320, 320) + , ctx = canvas.getContext('2d'); + +// Create gradients +var lingrad = ctx.createLinearGradient(0,0,0,150); +lingrad.addColorStop(0, '#00ABEB'); +lingrad.addColorStop(0.5, '#fff'); +lingrad.addColorStop(0.5, '#26C000'); +lingrad.addColorStop(1, '#fff'); + +var lingrad2 = ctx.createLinearGradient(0,50,0,95); +lingrad2.addColorStop(0.5, '#000'); +lingrad2.addColorStop(1, 'rgba(0,0,0,0)'); + +// assign gradients to fill and stroke styles +ctx.fillStyle = lingrad; +ctx.strokeStyle = lingrad2; + +// draw shapes +ctx.fillRect(10,10,130,130); +ctx.strokeRect(50,50,50,50); + +canvas.savePNG(__dirname + '/gradients.png'); \ No newline at end of file diff --git a/lib/canvas.js b/lib/canvas.js index fa4243d..ccdfd4a 100644 --- a/lib/canvas.js +++ b/lib/canvas.js @@ -13,6 +13,7 @@ var canvas = require('../build/default/canvas') , colors = require('./colors') , Canvas = canvas.Canvas , Context2d = canvas.Context2d + , CanvasGradient = canvas.CanvasGradient , cairoVersion = canvas.cairoVersion; /** @@ -118,6 +119,24 @@ Canvas.prototype.getContext = function(contextId){ } }; +CanvasGradient.prototype.addColorStop = function(offset, color){ + var rgba = exports.parseColor(color) || [0,0,0,1]; + this.addColorStopRGBA( + offset + , rgba[0] + , rgba[1] + , rgba[2] + , rgba[3]); +}; + +Context2d.prototype.createLinearGradient = function(x0, y0, x1, y1){ + return new CanvasGradient(x0, y0, x1, y1); +}; + +Context2d.prototype.createRadialGradient = function(x0, y0, r0, x1, y1, r1){ + return new CanvasGradient(x0, y0, r0, x1, y1, r1); +}; + /** * Set the fill style with the given css color string. * @@ -126,13 +145,17 @@ Canvas.prototype.getContext = function(contextId){ */ Context2d.prototype.__defineSetter__('fillStyle', function(val){ - var rgba = exports.parseColor(val) || [0,0,0,1]; - this.lastFillStyle = rgba; - this.setFillRGBA( - rgba[0] - , rgba[1] - , rgba[2] - , rgba[3]); + if (val instanceof CanvasGradient) { + this.setFillPattern(val); + } else if ('string' == typeof val) { + var rgba = exports.parseColor(val) || [0,0,0,1]; + this.lastFillStyle = rgba; + this.setFillRGBA( + rgba[0] + , rgba[1] + , rgba[2] + , rgba[3]); + } }); /** @@ -154,13 +177,17 @@ Context2d.prototype.__defineGetter__('fillStyle', function(){ */ Context2d.prototype.__defineSetter__('strokeStyle', function(val){ - var rgba = exports.parseColor(val) || [0,0,0,1]; - this.lastStrokeStyle = rgba; - this.setStrokeRGBA( - rgba[0] - , rgba[1] - , rgba[2] - , rgba[3]); + if (val instanceof CanvasGradient) { + this.setStrokePattern(val); + } else if ('string' == typeof val) { + var rgba = exports.parseColor(val) || [0,0,0,1]; + this.lastStrokeStyle = rgba; + this.setStrokeRGBA( + rgba[0] + , rgba[1] + , rgba[2] + , rgba[3]); + } }); /** diff --git a/src/canvas.cc b/src/canvas.cc index 9068a8e..ea7bd48 100644 --- a/src/canvas.cc +++ b/src/canvas.cc @@ -37,7 +37,7 @@ Canvas::New(const Arguments &args) { if (!args[1]->IsNumber()) return ThrowException(Exception::TypeError(String::New("height required"))); - Canvas *canvas = new Canvas(args[0]->Uint32Value(), args[1]->Uint32Value()); + Canvas *canvas = new Canvas(args[0]->Int32Value(), args[1]->Int32Value()); canvas->Wrap(args.This()); return args.This(); } diff --git a/src/canvas.h b/src/canvas.h index 57138c1..f359be5 100644 --- a/src/canvas.h +++ b/src/canvas.h @@ -13,6 +13,24 @@ #include #include +/* + * RGBA arg assertions. + */ + +#define RGBA_ARGS(N) \ + if (!args[N]->IsNumber()) \ + return ThrowException(Exception::TypeError(String::New("r required"))); \ + if (!args[N+1]->IsNumber()) \ + return ThrowException(Exception::TypeError(String::New("g required"))); \ + if (!args[N+2]->IsNumber()) \ + return ThrowException(Exception::TypeError(String::New("b required"))); \ + if (!args[N+3]->IsNumber()) \ + return ThrowException(Exception::TypeError(String::New("alpha required"))); \ + float r = args[N]->Int32Value(); \ + float g = args[N+1]->Int32Value(); \ + float b = args[N+2]->Int32Value(); \ + float a = args[N+3]->NumberValue(); + using namespace v8; class Canvas: public node::ObjectWrap { @@ -21,8 +39,6 @@ class Canvas: public node::ObjectWrap { static Handle New(const Arguments &args); static Handle SavePNG(const Arguments &args); inline cairo_surface_t *getSurface(){ return _surface; } - - protected: Canvas(int width, int height); private: diff --git a/src/context2d.cc b/src/context2d.cc index c318918..ee76c5e 100644 --- a/src/context2d.cc +++ b/src/context2d.cc @@ -5,10 +5,11 @@ // Copyright (c) 2010 LearnBoost // -#include "canvas.h" -#include "context2d.h" #include #include +#include "canvas.h" +#include "context2d.h" +#include "gradient.h" using namespace v8; using namespace node; @@ -23,6 +24,12 @@ using namespace node; _.b = B / 255 * 1; \ _.a = A; \ +#define SET_SOURCE(C) \ + if (C##Pattern) \ + cairo_set_source(ctx, C##Pattern); \ + else \ + SET_SOURCE_RGBA(C) + /* * Set source RGBA. */ @@ -48,24 +55,6 @@ using namespace node; int width = args[2]->Int32Value(); \ int height = args[3]->Int32Value(); -/* - * RGBA arg assertions. - */ - -#define RGBA_ARGS \ - if (!args[0]->IsNumber()) \ - return ThrowException(Exception::TypeError(String::New("r required"))); \ - if (!args[1]->IsNumber()) \ - return ThrowException(Exception::TypeError(String::New("g required"))); \ - if (!args[2]->IsNumber()) \ - return ThrowException(Exception::TypeError(String::New("b required"))); \ - if (!args[3]->IsNumber()) \ - return ThrowException(Exception::TypeError(String::New("alpha required"))); \ - float r = args[0]->Int32Value(); \ - float g = args[1]->Int32Value(); \ - float b = args[2]->Int32Value(); \ - float a = args[3]->NumberValue(); - /* * Initialize Context2d. */ @@ -100,6 +89,8 @@ Context2d::Initialize(Handle target) { NODE_SET_PROTOTYPE_METHOD(t, "arc", Arc); NODE_SET_PROTOTYPE_METHOD(t, "setFillRGBA", SetFillRGBA); NODE_SET_PROTOTYPE_METHOD(t, "setStrokeRGBA", SetStrokeRGBA); + NODE_SET_PROTOTYPE_METHOD(t, "setFillPattern", SetFillPattern); + NODE_SET_PROTOTYPE_METHOD(t, "setStrokePattern", SetStrokePattern); proto->SetAccessor(String::NewSymbol("globalCompositeOperation"), GetGlobalCompositeOperation, SetGlobalCompositeOperation); proto->SetAccessor(String::NewSymbol("globalAlpha"), GetGlobalAlpha, SetGlobalAlpha); proto->SetAccessor(String::NewSymbol("miterLimit"), GetMiterLimit, SetMiterLimit); @@ -130,6 +121,7 @@ Context2d::Context2d(Canvas *canvas): ObjectWrap() { _canvas = canvas; _context = cairo_create(canvas->getSurface()); cairo_set_line_width(_context, 1); + fillPattern = strokePattern = NULL; globalAlpha = -1; RGBA(fill,0,0,0,1); RGBA(stroke,0,0,0,1); @@ -337,6 +329,34 @@ Context2d::SetLineCap(Local prop, Local val, const AccessorInfo & } } +/* + * Set fill pattern, used internally for fillStyle= + */ + +Handle +Context2d::SetFillPattern(const Arguments &args) { + HandleScope scope; + // TODO: HasInstance / error handling + Context2d *context = ObjectWrap::Unwrap(args.This()); + Gradient *grad = ObjectWrap::Unwrap(args[0]->ToObject()); + context->fillPattern = grad->getPattern(); + return Undefined(); +} + +/* + * Set stroke pattern, used internally for strokeStyle= + */ + +Handle +Context2d::SetStrokePattern(const Arguments &args) { + HandleScope scope; + // TODO: HasInstance / error handling + Context2d *context = ObjectWrap::Unwrap(args.This()); + Gradient *grad = ObjectWrap::Unwrap(args[0]->ToObject()); + context->strokePattern = grad->getPattern(); + return Undefined(); +} + /* * Set fill RGBA, used internally for fillStyle= */ @@ -344,7 +364,7 @@ Context2d::SetLineCap(Local prop, Local val, const AccessorInfo & Handle Context2d::SetFillRGBA(const Arguments &args) { HandleScope scope; - RGBA_ARGS; + RGBA_ARGS(0); Context2d *context = ObjectWrap::Unwrap(args.This()); RGBA(context->fill,r,g,b,a); return Undefined(); @@ -357,7 +377,7 @@ Context2d::SetFillRGBA(const Arguments &args) { Handle Context2d::SetStrokeRGBA(const Arguments &args) { HandleScope scope; - RGBA_ARGS; + RGBA_ARGS(0); Context2d *context = ObjectWrap::Unwrap(args.This()); RGBA(context->stroke,r,g,b,a); return Undefined(); @@ -511,7 +531,7 @@ Context2d::Fill(const Arguments &args) { HandleScope scope; Context2d *context = ObjectWrap::Unwrap(args.This()); cairo_t *ctx = context->getContext(); - SET_SOURCE_RGBA(context->fill); + SET_SOURCE(context->fill); cairo_fill_preserve(ctx); return Undefined(); } @@ -525,7 +545,7 @@ Context2d::Stroke(const Arguments &args) { HandleScope scope; Context2d *context = ObjectWrap::Unwrap(args.This()); cairo_t *ctx = context->getContext(); - SET_SOURCE_RGBA(context->stroke); + SET_SOURCE(context->stroke); cairo_stroke_preserve(ctx); return Undefined(); } @@ -583,7 +603,7 @@ Context2d::FillRect(const Arguments &args) { Context2d *context = ObjectWrap::Unwrap(args.This()); cairo_t *ctx = context->getContext(); cairo_rectangle(ctx, x, y, width, height); - SET_SOURCE_RGBA(context->fill); + SET_SOURCE(context->fill); cairo_fill(ctx); return Undefined(); } @@ -599,7 +619,7 @@ Context2d::StrokeRect(const Arguments &args) { Context2d *context = ObjectWrap::Unwrap(args.This()); cairo_t *ctx = context->getContext(); cairo_rectangle(ctx, x, y, width, height); - SET_SOURCE_RGBA(context->stroke); + SET_SOURCE(context->stroke); cairo_stroke(ctx); return Undefined(); } @@ -616,7 +636,6 @@ Context2d::ClearRect(const Arguments &args) { cairo_t *ctx = context->getContext(); cairo_set_operator(ctx, CAIRO_OPERATOR_CLEAR); cairo_rectangle(ctx, x, y, width, height); - SET_SOURCE_RGBA(context->fill); cairo_fill(ctx); cairo_set_operator(ctx, CAIRO_OPERATOR_OVER); return Undefined(); diff --git a/src/context2d.h b/src/context2d.h index e5fc5f9..b348c9f 100644 --- a/src/context2d.h +++ b/src/context2d.h @@ -9,6 +9,7 @@ #define __NODE_CONTEXT2D_H__ #include "canvas.h" +#include "gradient.h" typedef struct { float r, g, b, a; @@ -18,6 +19,8 @@ class Context2d: public node::ObjectWrap { public: rgba_t fill; rgba_t stroke; + cairo_pattern_t *fillPattern; + cairo_pattern_t *strokePattern; float globalAlpha; static void Initialize(Handle target); static Handle New(const Arguments &args); @@ -31,8 +34,11 @@ class Context2d: public node::ObjectWrap { static Handle ClosePath(const Arguments &args); static Handle Fill(const Arguments &args); static Handle Stroke(const Arguments &args); + static Handle SetSource(const Arguments &args); static Handle SetFillRGBA(const Arguments &args); static Handle SetStrokeRGBA(const Arguments &args); + static Handle SetFillPattern(const Arguments &args); + static Handle SetStrokePattern(const Arguments &args); static Handle BezierCurveTo(const Arguments &args); static Handle LineTo(const Arguments &args); static Handle MoveTo(const Arguments &args); diff --git a/src/gradient.cc b/src/gradient.cc new file mode 100644 index 0000000..8423694 --- /dev/null +++ b/src/gradient.cc @@ -0,0 +1,78 @@ + +// +// gradient.cc +// +// Copyright (c) 2010 LearnBoost +// + +#include "canvas.h" +#include "gradient.h" + +void +Gradient::Initialize(Handle target) { + HandleScope scope; + Local t = FunctionTemplate::New(Gradient::New); + t->InstanceTemplate()->SetInternalFieldCount(1); + t->SetClassName(String::NewSymbol("CanvasGradient")); + + NODE_SET_PROTOTYPE_METHOD(t, "addColorStopRGBA", AddColorStopRGBA); + target->Set(String::NewSymbol("CanvasGradient"), t->GetFunction()); +} + +Handle +Gradient::New(const Arguments &args) { + HandleScope scope; + + // Linear + if (4 == args.Length()) { + Gradient *grad = new Gradient( + args[0]->NumberValue() + , args[1]->NumberValue() + , args[2]->NumberValue() + , args[3]->NumberValue()); + grad->Wrap(args.This()); + return args.This(); + } + + // Radial + if (6 == args.Length()) { + Gradient *grad = new Gradient( + args[0]->NumberValue() + , args[1]->NumberValue() + , args[2]->NumberValue() + , args[3]->NumberValue() + , args[4]->NumberValue() + , args[5]->NumberValue()); + grad->Wrap(args.This()); + return args.This(); + } + + return ThrowException(Exception::TypeError(String::New("invalid arguments"))); +} + +Handle +Gradient::AddColorStopRGBA(const Arguments &args) { + HandleScope scope; + if (!args[0]->IsNumber()) \ + return ThrowException(Exception::TypeError(String::New("offset required"))); \ + RGBA_ARGS(1); + Gradient *grad = ObjectWrap::Unwrap(args.This()); + cairo_pattern_add_color_stop_rgba( + grad->getPattern() + , args[0]->NumberValue() + , r / 255 * 1 + , g / 255 * 1 + , b / 255 * 1 + , a); + return Undefined(); +} + +Gradient::Gradient(double x0, double y0, double x1, double y1): + _x0(x0), _y0(y0), _x1(x1), _y1(y1) { + _pattern = cairo_pattern_create_linear(x0, y0, x1, y1); +} + +Gradient::Gradient(double x0, double y0, double r0, double x1, double y1, double r1): + _x0(x0), _y0(y0), _x1(x1), _y1(y1), _r0(r0), _r1(r1) { + _pattern = cairo_pattern_create_radial(x0, y0, r0, x1, y1, r1); +} diff --git a/src/gradient.h b/src/gradient.h new file mode 100644 index 0000000..097d779 --- /dev/null +++ b/src/gradient.h @@ -0,0 +1,29 @@ + +// +// gradient.h +// +// Copyright (c) 2010 LearnBoost +// + +#ifndef __NODE_GRADIENT_H__ +#define __NODE_GRADIENT_H__ + +#include "canvas.h" + +using namespace v8; + +class Gradient: public node::ObjectWrap { + public: + static void Initialize(Handle target); + static Handle New(const Arguments &args); + static Handle AddColorStopRGBA(const Arguments &args); + Gradient(double x0, double y0, double x1, double y1); + Gradient(double x0, double y0, double r0, double x1, double y1, double r1); + inline cairo_pattern_t *getPattern(){ return _pattern; } + + private: + double _x0, _y0, _x1, _y1, _r0, _r1; + cairo_pattern_t *_pattern; +}; + +#endif \ No newline at end of file diff --git a/src/node-canvas.cc b/src/node-canvas.cc index 6e72460..1b0ee68 100644 --- a/src/node-canvas.cc +++ b/src/node-canvas.cc @@ -7,11 +7,13 @@ #include "canvas.h" #include "context2d.h" +#include "gradient.h" extern "C" void init (Handle target) { HandleScope scope; Canvas::Initialize(target); Context2d::Initialize(target); + Gradient::Initialize(target); target->Set(String::New("cairoVersion"), String::New(cairo_version_string())); } \ No newline at end of file