From 3a9d122456c9357618fcbe10a74dbc74d1c846bf Mon Sep 17 00:00:00 2001 From: Tj Holowaychuk Date: Wed, 29 Sep 2010 10:25:38 -0700 Subject: [PATCH] Initial commit --- .gitignore | 7 + History.md | 0 Makefile | 6 + Readme.md | 42 ++++++ lib/canvas.js | 127 +++++++++++++++++ lib/colors.js | 152 ++++++++++++++++++++ package.json | 11 ++ src/canvas.cc | 76 ++++++++++ src/canvas.h | 33 +++++ src/context2d.cc | 331 ++++++++++++++++++++++++++++++++++++++++++++ src/context2d.h | 41 ++++++ src/node-canvas.cc | 16 +++ test/canvas.test.js | 197 ++++++++++++++++++++++++++ wscript | 19 +++ 14 files changed, 1058 insertions(+) create mode 100644 .gitignore create mode 100644 History.md create mode 100644 Makefile create mode 100644 Readme.md create mode 100644 lib/canvas.js create mode 100644 lib/colors.js create mode 100644 package.json create mode 100644 src/canvas.cc create mode 100644 src/canvas.h create mode 100644 src/context2d.cc create mode 100644 src/context2d.h create mode 100644 src/node-canvas.cc create mode 100644 test/canvas.test.js create mode 100644 wscript diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a4b253 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +build +.DS_Store +test.js +.lock-wscript +test.png +test.html +test/*.png diff --git a/History.md b/History.md new file mode 100644 index 0000000..e69de29 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a2f2a41 --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ + +test: + @./support/expresso/bin/expresso \ + -I lib + +.PHONY: test \ No newline at end of file diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..2116a38 --- /dev/null +++ b/Readme.md @@ -0,0 +1,42 @@ + +# node-canvas + +## TODO + + [ ] drawing shapes + [ ] line styles + [ ] fix colors + [ ] gradients + [ ] Image + [ ] patterns + [ ] text + [ ] rectangles + [ ] transformations + [ ] compositing + [ ] PixelArray + [ ] async saving to disk (png, svg, etc) + +## License + +(The MIT License) + +Copyright (c) 2009-2010 TJ Holowaychuk <tj@vision-media.ca> + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/lib/canvas.js b/lib/canvas.js new file mode 100644 index 0000000..d892b73 --- /dev/null +++ b/lib/canvas.js @@ -0,0 +1,127 @@ + +/*! + * Canvas + * Copyright(c) 2010 TJ Holowaychuk + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var canvas = require('../build/default/canvas') + , colors = require('./colors') + , Canvas = canvas.Canvas + , Context2d = canvas.Context2d; + +/** + * Export `Canvas` as the module. + */ + +var Canvas = exports = module.exports = Canvas; + +/** + * Library version. + */ + +exports.version = '0.0.1'; + +/** + * Parse the given color `str`. + * + * Current supports: + * + * - #nnn + * - #nnnnnn + * - rgb(r,g,b) + * - rgba(r,g,b,a) + * - color + * + * Examples + * + * - #fff + * - #FFF + * - #FFFFFF + * - rgb(255,255,5) + * - rgba(255,255,5,.8) + * - rgba(255,255,5,0.8) + * - white + * - red + * + * @param {String} str + * @return {Array} + * @api public + */ + +exports.parseColor = function(str){ + str = colors[str] || String(str); + if (0 == str.indexOf('rgba')) { + var captures = /rgba\((\d{1,3}) *, *(\d{1,3}) *, *(\d{1,3}) *, *(\d+\.\d+|\.\d+|\d+) *\)/.exec(str); + if (!captures) return; + return [ + parseInt(captures[1], 10) + , parseInt(captures[2], 10) + , parseInt(captures[3], 10) + , parseFloat(captures[4], 10) + ]; + } else if (0 == str.indexOf('rgb')) { + var captures = /rgb\((\d{1,3}) *, *(\d{1,3}) *, *(\d{1,3}) *\)/.exec(str); + if (!captures) return; + return [ + parseInt(captures[1], 10) + , parseInt(captures[2], 10) + , parseInt(captures[3], 10) + , 1 + ]; + } else if ('#' == str.charAt(0) && str.length > 4) { + var captures = /#(\w{2})(\w{2})(\w{2})/.exec(str); + if (!captures) return; + return [ + parseInt(captures[1], 16) + , parseInt(captures[2], 16) + , parseInt(captures[3], 16) + , 1 + ]; + } else if ('#' == str.charAt(0)) { + var captures = /#(\w)(\w)(\w)/.exec(str); + if (!captures) return; + return [ + parseInt(captures[1] + captures[1], 16) + , parseInt(captures[2] + captures[2], 16) + , parseInt(captures[3] + captures[3], 16) + , 1 + ]; + } +}; + +/** + * Get a context object. + * + * @param {String} contextId + * @return {Context2d} + * @api public + */ + +Canvas.prototype.getContext = function(contextId){ + if ('2d' == contextId) { + var ctx = new Context2d(this); + this.context = ctx; + ctx.canvas = this; + return ctx; + } +}; + +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]); +}); + +Context2d.prototype.__defineGetter__('fillStyle', function(){ + var rgba = this.lastFillStyle; + return 'rgba(' + rgba[0] + ',' + rgba[1] + ',' + rgba[2] + ',' + rgba[3] + ')'; +}); \ No newline at end of file diff --git a/lib/colors.js b/lib/colors.js new file mode 100644 index 0000000..c03bc53 --- /dev/null +++ b/lib/colors.js @@ -0,0 +1,152 @@ + +/*! + * Canvas - colors + * Copyright(c) 2010 TJ Holowaychuk + * MIT Licensed + */ + +module.exports = { + aliceblue: '#f0f8ff' + , antiquewhite: '#faebd7' + , aqua: '#00ffff' + , aquamarine: '#7fffd4' + , azure: '#f0ffff' + , beige: '#f5f5dc' + , bisque: '#ffe4c4' + , black: '#000000' + , blanchedalmond: '#ffebcd' + , blue: '#0000ff' + , blueviolet: '#8a2be2' + , brown: '#a52a2a' + , burlywood: '#deb887' + , cadetblue: '#5f9ea0' + , chartreuse: '#7fff00' + , chocolate: '#d2691e' + , coral: '#ff7f50' + , cornflowerblue: '#6495ed' + , cornsilk: '#fff8dc' + , crimson: '#dc143c' + , cyan: '#00ffff' + , darkblue: '#00008b' + , darkcyan: '#008b8b' + , darkgoldenrod: '#b8860b' + , darkgray: '#a9a9a9' + , darkgreen: '#006400' + , darkkhaki: '#bdb76b' + , darkmagenta: '#8b008b' + , darkolivegreen: '#556b2f' + , darkorange: '#ff8c00' + , darkorchid: '#9932cc' + , darkred: '#8b0000' + , darksalmon: '#e9967a' + , darkseagreen: '#8fbc8f' + , darkslateblue: '#483d8b' + , darkslategray: '#2f4f4f' + , darkturquoise: '#00ced1' + , darkviolet: '#9400d3' + , deeppink: '#ff1493' + , deepskyblue: '#00bfff' + , dimgray: '#696969' + , dodgerblue: '#1e90ff' + , feldspar: '#d19275' + , firebrick: '#b22222' + , floralwhite: '#fffaf0' + , forestgreen: '#228b22' + , fuchsia: '#ff00ff' + , gainsboro: '#dcdcdc' + , ghostwhite: '#f8f8ff' + , gold: '#ffd700' + , goldenrod: '#daa520' + , gray: '#808080' + , green: '#008000' + , greenyellow: '#adff2f' + , honeydew: '#f0fff0' + , hotpink: '#ff69b4' + , indianred : '#cd5c5c' + , indigo : '#4b0082' + , ivory: '#fffff0' + , khaki: '#f0e68c' + , lavender: '#e6e6fa' + , lavenderblush: '#fff0f5' + , lawngreen: '#7cfc00' + , lemonchiffon: '#fffacd' + , lightblue: '#add8e6' + , lightcoral: '#f08080' + , lightcyan: '#e0ffff' + , lightgoldenrodyellow: '#fafad2' + , lightgrey: '#d3d3d3' + , lightgreen: '#90ee90' + , lightpink: '#ffb6c1' + , lightsalmon: '#ffa07a' + , lightseagreen: '#20b2aa' + , lightskyblue: '#87cefa' + , lightslateblue: '#8470ff' + , lightslategray: '#778899' + , lightsteelblue: '#b0c4de' + , lightyellow: '#ffffe0' + , lime: '#00ff00' + , limegreen: '#32cd32' + , linen: '#faf0e6' + , magenta: '#ff00ff' + , maroon: '#800000' + , mediumaquamarine: '#66cdaa' + , mediumblue: '#0000cd' + , mediumorchid: '#ba55d3' + , mediumpurple: '#9370d8' + , mediumseagreen: '#3cb371' + , mediumslateblue: '#7b68ee' + , mediumspringgreen: '#00fa9a' + , mediumturquoise: '#48d1cc' + , mediumvioletred: '#c71585' + , midnightblue: '#191970' + , mintcream: '#f5fffa' + , mistyrose: '#ffe4e1' + , moccasin: '#ffe4b5' + , navajowhite: '#ffdead' + , navy: '#000080' + , oldlace: '#fdf5e6' + , olive: '#808000' + , olivedrab: '#6b8e23' + , orange: '#ffa500' + , orangered: '#ff4500' + , orchid: '#da70d6' + , palegoldenrod: '#eee8aa' + , palegreen: '#98fb98' + , paleturquoise: '#afeeee' + , palevioletred: '#d87093' + , papayawhip: '#ffefd5' + , peachpuff: '#ffdab9' + , peru: '#cd853f' + , pink: '#ffc0cb' + , plum: '#dda0dd' + , powderblue: '#b0e0e6' + , purple: '#800080' + , red: '#ff0000' + , rosybrown: '#bc8f8f' + , royalblue: '#4169e1' + , saddlebrown: '#8b4513' + , salmon: '#fa8072' + , sandybrown: '#f4a460' + , seagreen: '#2e8b57' + , seashell: '#fff5ee' + , sienna: '#a0522d' + , silver: '#c0c0c0' + , skyblue: '#87ceeb' + , slateblue: '#6a5acd' + , slategray: '#708090' + , snow: '#fffafa' + , springgreen: '#00ff7f' + , steelblue: '#4682b4' + , tan: '#d2b48c' + , teal: '#008080' + , thistle: '#d8bfd8' + , tomato: '#ff6347' + , turquoise: '#40e0d0' + , violet: '#ee82ee' + , violetred: '#d02090' + , wheat: '#f5deb3' + , white: '#ffffff' + , whitesmoke: '#f5f5f5' + , yellow: '#ffff00' + , yellowgreen: '#9acd32' +}; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..d0e41a5 --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ "name": "canvas" + , "description": "node canvas implementation backed by Cairo" + , "version": "0.0.1" + , "author": "TJ Holowaychuk " + , "keywords": [] + , "main": "./lib/canvas.js" + , "scripts": { + "preinstall": "node-waf configure build" + } + , "engines": { "node": ">= 0.2.0" } +} \ No newline at end of file diff --git a/src/canvas.cc b/src/canvas.cc new file mode 100644 index 0000000..9068a8e --- /dev/null +++ b/src/canvas.cc @@ -0,0 +1,76 @@ + +// +// canvas.cc +// +// Copyright (c) 2010 LearnBoost +// + +#include "canvas.h" + +using namespace v8; + +/* + * Initialize Canvas. + */ + +void +Canvas::Initialize(Handle target) { + HandleScope scope; + Local t = FunctionTemplate::New(Canvas::New); + t->InstanceTemplate()->SetInternalFieldCount(1); + t->SetClassName(String::NewSymbol("Canvas")); + + NODE_SET_PROTOTYPE_METHOD(t, "savePNG", SavePNG); + target->Set(String::NewSymbol("Canvas"), t->GetFunction()); +} + +/* + * Initialize a Canvas with the given width and height. + */ + +Handle +Canvas::New(const Arguments &args) { + HandleScope scope; + + if (!args[0]->IsNumber()) + return ThrowException(Exception::TypeError(String::New("width required"))); + if (!args[1]->IsNumber()) + return ThrowException(Exception::TypeError(String::New("height required"))); + + Canvas *canvas = new Canvas(args[0]->Uint32Value(), args[1]->Uint32Value()); + canvas->Wrap(args.This()); + return args.This(); +} + +/* + * Initialize cairo surface. + */ + +Canvas::Canvas(int width, int height): ObjectWrap() { + _surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); +} + +/* + * Destroy cairo surface. + */ + +Canvas::~Canvas() { + cairo_surface_destroy(_surface); +} + +/* + * Save a PNG at the given path. + */ + +Handle +Canvas::SavePNG(const Arguments &args) { + HandleScope scope; + Canvas *canvas = ObjectWrap::Unwrap(args.This()); + + if (!args[0]->IsString()) + return ThrowException(Exception::TypeError(String::New("path required"))); + + String::Utf8Value path(args[0]->ToString()); + cairo_surface_write_to_png(canvas->getSurface(), *path); + return Undefined(); +} diff --git a/src/canvas.h b/src/canvas.h new file mode 100644 index 0000000..57138c1 --- /dev/null +++ b/src/canvas.h @@ -0,0 +1,33 @@ + +// +// canvas.h +// +// Copyright (c) 2010 LearnBoost +// + +#ifndef __NODE_CANVAS_H__ +#define __NODE_CANVAS_H__ + +#include +#include +#include +#include + +using namespace v8; + +class Canvas: public node::ObjectWrap { + public: + static void Initialize(Handle target); + 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: + ~Canvas(); + cairo_surface_t *_surface; +}; + +#endif \ No newline at end of file diff --git a/src/context2d.cc b/src/context2d.cc new file mode 100644 index 0000000..521db65 --- /dev/null +++ b/src/context2d.cc @@ -0,0 +1,331 @@ + +// +// context2d.cc +// +// Copyright (c) 2010 LearnBoost +// + +#include "canvas.h" +#include "context2d.h" +#include + +using namespace v8; +using namespace node; + +/* + * Rectangle arg assertions. + */ + +#define RECT_ARGS \ + if (!args[0]->IsNumber()) \ + return ThrowException(Exception::TypeError(String::New("x required"))); \ + if (!args[1]->IsNumber()) \ + return ThrowException(Exception::TypeError(String::New("y required"))); \ + if (!args[2]->IsNumber()) \ + return ThrowException(Exception::TypeError(String::New("width required"))); \ + if (!args[3]->IsNumber()) \ + return ThrowException(Exception::TypeError(String::New("height required"))); \ + int x = args[0]->Int32Value(); \ + int y = args[1]->Int32Value(); \ + int width = args[2]->Int32Value(); \ + int height = args[3]->Int32Value(); + +/* + * Initialize Context2d. + */ + +void +Context2d::Initialize(Handle target) { + HandleScope scope; + // Constructor + Local t = FunctionTemplate::New(Context2d::New); + t->InstanceTemplate()->SetInternalFieldCount(1); + t->SetClassName(String::NewSymbol("Context2d")); + + // Prototype + NODE_SET_PROTOTYPE_METHOD(t, "fill", Fill); + NODE_SET_PROTOTYPE_METHOD(t, "stroke", Stroke); + NODE_SET_PROTOTYPE_METHOD(t, "fillRect", FillRect); + NODE_SET_PROTOTYPE_METHOD(t, "strokeRect", StrokeRect); + NODE_SET_PROTOTYPE_METHOD(t, "clearRect", ClearRect); + NODE_SET_PROTOTYPE_METHOD(t, "moveTo", MoveTo); + NODE_SET_PROTOTYPE_METHOD(t, "lineTo", LineTo); + NODE_SET_PROTOTYPE_METHOD(t, "bezierCurveTo", BezierCurveTo); + NODE_SET_PROTOTYPE_METHOD(t, "beginPath", BeginPath); + NODE_SET_PROTOTYPE_METHOD(t, "closePath", ClosePath); + NODE_SET_PROTOTYPE_METHOD(t, "arc", Arc); + NODE_SET_PROTOTYPE_METHOD(t, "setFillRGBA", SetFillRGBA); + target->Set(String::NewSymbol("Context2d"), t->GetFunction()); +} + +/* + * Initialize a new Context2d with the given canvas. + */ + +Handle +Context2d::New(const Arguments &args) { + HandleScope scope; + Canvas *canvas = ObjectWrap::Unwrap(args[0]->ToObject()); + Context2d *context = new Context2d(canvas); + context->Wrap(args.This()); + return args.This(); +} + +/* + * Create a cairo context. + */ + +Context2d::Context2d(Canvas *canvas): ObjectWrap() { + _canvas = canvas; + _context = cairo_create(canvas->getSurface()); + cairo_set_source_rgba(_context, 0, 0, 0, 1); +} + +/* + * Destroy cairo context. + */ + +Context2d::~Context2d() { + cairo_destroy(_context); +} + +/* + * Set fill RGBA, use internally for fillStyle= + */ + +Handle +Context2d::SetFillRGBA(const Arguments &args) { + HandleScope scope; + + 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"))); + + Context2d *context = ObjectWrap::Unwrap(args.This()); + + cairo_set_source_rgba(context->getContext() + , args[0]->Int32Value() + , args[1]->Int32Value() + , args[2]->Int32Value() + , args[3]->NumberValue()); + + return Undefined(); +} + +/* + * Bezier curve. + */ + +Handle +Context2d::BezierCurveTo(const Arguments &args) { + HandleScope scope; + + if (!args[0]->IsNumber()) + return ThrowException(Exception::TypeError(String::New("cp1x required"))); + if (!args[1]->IsNumber()) + return ThrowException(Exception::TypeError(String::New("cp1y required"))); + if (!args[2]->IsNumber()) + return ThrowException(Exception::TypeError(String::New("cp2x required"))); + if (!args[3]->IsNumber()) + return ThrowException(Exception::TypeError(String::New("cp2y required"))); + if (!args[4]->IsNumber()) + return ThrowException(Exception::TypeError(String::New("x required"))); + if (!args[5]->IsNumber()) + return ThrowException(Exception::TypeError(String::New("y required"))); + + Context2d *context = ObjectWrap::Unwrap(args.This()); + cairo_curve_to(context->getContext() + , args[0]->NumberValue() + , args[1]->NumberValue() + , args[2]->NumberValue() + , args[3]->NumberValue() + , args[4]->NumberValue() + , args[5]->NumberValue()); + + return Undefined(); +} + +/* + * Creates a new subpath. + */ + +Handle +Context2d::BeginPath(const Arguments &args) { + HandleScope scope; + Context2d *context = ObjectWrap::Unwrap(args.This()); + cairo_new_path(context->getContext()); + return Undefined(); +} + +/* + * Marks the subpath as closed. + */ + +Handle +Context2d::ClosePath(const Arguments &args) { + HandleScope scope; + Context2d *context = ObjectWrap::Unwrap(args.This()); + cairo_close_path(context->getContext()); + return Undefined(); +} + +/* + * Fill the shape. + */ + +Handle +Context2d::Fill(const Arguments &args) { + HandleScope scope; + Context2d *context = ObjectWrap::Unwrap(args.This()); + cairo_fill(context->getContext()); + return Undefined(); +} + +/* + * Stroke the shape. + */ + +Handle +Context2d::Stroke(const Arguments &args) { + HandleScope scope; + Context2d *context = ObjectWrap::Unwrap(args.This()); + cairo_stroke(context->getContext()); + return Undefined(); +} + +/* + * Adds a point to the current subpath. + */ + +Handle +Context2d::LineTo(const Arguments &args) { + HandleScope scope; + + if (!args[0]->IsNumber()) + return ThrowException(Exception::TypeError(String::New("x required"))); + if (!args[1]->IsNumber()) + return ThrowException(Exception::TypeError(String::New("y required"))); + + Context2d *context = ObjectWrap::Unwrap(args.This()); + cairo_line_to(context->getContext() + , args[0]->Int32Value() + , args[1]->Int32Value()); + + return Undefined(); +} + +/* + * Creates a new subpath at the given point. + */ + +Handle +Context2d::MoveTo(const Arguments &args) { + HandleScope scope; + + if (!args[0]->IsNumber()) + return ThrowException(Exception::TypeError(String::New("x required"))); + if (!args[1]->IsNumber()) + return ThrowException(Exception::TypeError(String::New("y required"))); + + Context2d *context = ObjectWrap::Unwrap(args.This()); + cairo_move_to(context->getContext() + , args[0]->Int32Value() + , args[1]->Int32Value()); + + return Undefined(); +} + +/* + * Fill the rectangle defined by x, y, with and height. + */ + +Handle +Context2d::FillRect(const Arguments &args) { + HandleScope scope; + RECT_ARGS; + Context2d *context = ObjectWrap::Unwrap(args.This()); + cairo_t *ctx = context->getContext(); + cairo_rectangle(ctx, x, y, width, height); + cairo_fill(ctx); + return Undefined(); +} + +/* + * Stroke the rectangle defined by x, y, width and height. + */ + +Handle +Context2d::StrokeRect(const Arguments &args) { + HandleScope scope; + RECT_ARGS; + Context2d *context = ObjectWrap::Unwrap(args.This()); + cairo_t *ctx = context->getContext(); + cairo_rectangle(ctx, x, y, width, height); + cairo_stroke(ctx); + return Undefined(); +} + +/* + * Clears all pixels defined by x, y, width and height. + */ + +Handle +Context2d::ClearRect(const Arguments &args) { + HandleScope scope; + RECT_ARGS; + Context2d *context = ObjectWrap::Unwrap(args.This()); + cairo_t *ctx = context->getContext(); + cairo_set_operator(ctx, CAIRO_OPERATOR_CLEAR); + cairo_rectangle(ctx, x, y, width, height); + cairo_fill(ctx); + cairo_set_operator(ctx, CAIRO_OPERATOR_OVER); + return Undefined(); +} + +/* + * Adds an arc at x, y with the given radis and start/end angles. + */ + +Handle +Context2d::Arc(const Arguments &args) { + HandleScope scope; + + if (!args[0]->IsNumber()) + return ThrowException(Exception::TypeError(String::New("x required"))); + if (!args[1]->IsNumber()) + return ThrowException(Exception::TypeError(String::New("y required"))); + if (!args[2]->IsNumber()) + return ThrowException(Exception::TypeError(String::New("radius required"))); + if (!args[3]->IsNumber()) + return ThrowException(Exception::TypeError(String::New("startAngle required"))); + if (!args[4]->IsNumber()) + return ThrowException(Exception::TypeError(String::New("endAngle required"))); + + bool anticlockwise = args[5]->BooleanValue(); + + Context2d *context = ObjectWrap::Unwrap(args.This()); + cairo_t *ctx = context->getContext(); + + if (anticlockwise && M_PI * 2 != args[4]->NumberValue()) { + cairo_arc_negative(ctx + , args[0]->NumberValue() + , args[1]->NumberValue() + , args[2]->NumberValue() + , args[3]->NumberValue() + , args[4]->NumberValue()); + } else { + cairo_arc(ctx + , args[0]->NumberValue() + , args[1]->NumberValue() + , args[2]->NumberValue() + , args[3]->NumberValue() + , args[4]->NumberValue()); + } + + return Undefined(); +} diff --git a/src/context2d.h b/src/context2d.h new file mode 100644 index 0000000..1a70d6b --- /dev/null +++ b/src/context2d.h @@ -0,0 +1,41 @@ + +// +// context2d.h +// +// Copyright (c) 2010 LearnBoost +// + +#ifndef __NODE_CONTEXT2D_H__ +#define __NODE_CONTEXT2D_H__ + +#include "canvas.h" + +class Context2d: public node::ObjectWrap { + public: + static void Initialize(Handle target); + static Handle New(const Arguments &args); + static Handle BeginPath(const Arguments &args); + static Handle ClosePath(const Arguments &args); + static Handle Fill(const Arguments &args); + static Handle Stroke(const Arguments &args); + static Handle SetFillRGBA(const Arguments &args); + static Handle BezierCurveTo(const Arguments &args); + static Handle LineTo(const Arguments &args); + static Handle MoveTo(const Arguments &args); + static Handle FillRect(const Arguments &args); + static Handle StrokeRect(const Arguments &args); + static Handle ClearRect(const Arguments &args); + static Handle Arc(const Arguments &args); + inline cairo_t *getContext(){ return _context; } + inline Canvas *getCanvas(){ return _canvas; } + + protected: + Context2d(Canvas *canvas); + + private: + ~Context2d(); + Canvas *_canvas; + cairo_t *_context; +}; + +#endif \ No newline at end of file diff --git a/src/node-canvas.cc b/src/node-canvas.cc new file mode 100644 index 0000000..35911fc --- /dev/null +++ b/src/node-canvas.cc @@ -0,0 +1,16 @@ + +// +// node-canvas.cc +// +// Copyright (c) 2010 LearnBoost +// + +#include "canvas.h" +#include "context2d.h" + +extern "C" void +init (Handle target) { + HandleScope scope; + Canvas::Initialize(target); + Context2d::Initialize(target); +} \ No newline at end of file diff --git a/test/canvas.test.js b/test/canvas.test.js new file mode 100644 index 0000000..e412a3c --- /dev/null +++ b/test/canvas.test.js @@ -0,0 +1,197 @@ + +/** + * Module dependencies. + */ + +var Canvas = require('canvas') + , assert = require('assert') + , crypto = require('crypto') + , fs = require('fs'); + +function hash(val) { + return crypto.createHash('md5').update(val).digest('hex'); +} + +function assertChecksum(canvas, path, checksum, msg) { + canvas.savePNG(path); + fs.readFile(path, function(err, buf){ + assert.equal(hash(buf), checksum, msg); + }); +} + +module.exports = { + 'test .version': function(assert){ + assert.match(Canvas.version, /^\d+\.\d+\.\d+$/); + }, + + 'test .parseColor()': function(assert){ + assert.equal(null, Canvas.parseColor()); + assert.equal(null, Canvas.parseColor('')); + + // rgb() + assert.eql([255,165,0,1], Canvas.parseColor('rgb(255,165,0)')); + assert.eql([255,165,0,1], Canvas.parseColor('rgb(255, 165, 0)')); + assert.eql([255,165,0,1], Canvas.parseColor('rgb(255 , 165 , 0)')); + assert.equal(null, Canvas.parseColor('rgb()')); + + // rgba() + assert.eql([255,165,0,1], Canvas.parseColor('rgba(255,165,0,1)')); + assert.eql([255,165,0,1], Canvas.parseColor('rgba(255,165,0,1)')); + assert.eql([255,165,0,.6], Canvas.parseColor('rgba(255,165,0,0.6)')); + assert.eql([255,165,0,.6], Canvas.parseColor('rgba(255,165, 0, 0.6)')); + assert.eql([255,165,0,.6], Canvas.parseColor('rgba(255,165 , 0 ,.6)')); + assert.equal(null, Canvas.parseColor('rgba(2554,165 , 0 ,.6)')); + assert.equal(null, Canvas.parseColor('rgba()')); + + // hex + assert.eql([165,89,89,1], Canvas.parseColor('#A55959')); + assert.eql([255,255,255,1], Canvas.parseColor('#FFFFFF')); + assert.eql([255,255,255,1], Canvas.parseColor('#ffffff')); + assert.eql([255,255,255,1], Canvas.parseColor('#FFF')); + assert.eql([255,255,255,1], Canvas.parseColor('#fff')); + + // name + assert.eql([255,255,255,1], Canvas.parseColor('white')); + assert.eql([0,0,0,1], Canvas.parseColor('black')); + }, + + 'test Canvas#getContext("2d")': function(assert){ + var canvas = new Canvas(200, 300) + , ctx = canvas.getContext('2d'); + assert.ok('object' == typeof ctx); + assert.equal(canvas, ctx.canvas, 'context.canvas is not canvas'); + assert.equal(ctx, canvas.context, 'canvas.context is not context'); + }, + + 'test Canvas#getContext("invalid")': function(assert){ + assert.equal(null, new Canvas(200, 300).getContext('invalid')); + }, + + 'test Canvas#clearRect()': function(assert){ + var canvas = new Canvas(200, 200) + , ctx = canvas.getContext('2d') + , path = __dirname + '/clearRect.png'; + + ctx.fillRect(25,25,100,100); + ctx.clearRect(45,45,60,60); + ctx.fillRect(50,50,50,50); + + assertChecksum( + canvas + , path + , 'e21404e97142a76c0c8d14cf0fab400f' + , 'Canvas#clearRect() failed'); + }, + + 'test Canvas#strokeRect()': function(assert){ + var canvas = new Canvas(200, 200) + , ctx = canvas.getContext('2d') + , path = __dirname + '/strokeRect.png'; + + ctx.fillRect(25,25,100,100); + ctx.clearRect(45,45,60,60); + ctx.strokeRect(50,50,50,50); + + assertChecksum( + canvas + , path + , '1cd349f7d3d2ae5a2bce13ca35dcaa94' + , 'Canvas#strokeRect() failed'); + }, + + 'test Canvas#lineTo()': function(assert){ + var canvas = new Canvas(200, 200) + , ctx = canvas.getContext('2d') + , path = __dirname + '/lineTo.png'; + + // Filled triangle + ctx.beginPath(); + ctx.moveTo(25.5,25); + ctx.lineTo(105,25); + ctx.lineTo(25,105); + ctx.fill(); + + // Stroked triangle + ctx.beginPath(); + ctx.moveTo(125,125); + ctx.lineTo(125,45); + ctx.lineTo(45,125); + ctx.closePath(); + ctx.stroke(); + + assertChecksum( + canvas + , path + , '44cce447dcb15918c9baf9170f87911f' + , 'Canvas#lineTo() failed' + ); + }, + + 'test Canvas#arc()': function(assert){ + var canvas = new Canvas(200, 200) + , ctx = canvas.getContext('2d') + , path = __dirname + '/arc.png'; + + ctx.beginPath(); + ctx.arc(75,75,50,0,Math.PI*2,true); // Outer circle + ctx.moveTo(110,75); + ctx.arc(75,75,35,0,Math.PI,false); // Mouth + ctx.moveTo(65,65); + ctx.arc(60,65,5,0,Math.PI*2,true); // Left eye + ctx.moveTo(95,65); + ctx.arc(90,65,5,0,Math.PI*2,true); // Right eye + ctx.stroke(); + + assertChecksum( + canvas + , path + , '3c48a221b24c582f46e39c16678b12dd' + , 'Canvas#arc() failed'); + }, + + 'test Canvas#bezierCurveTo()': function(assert){ + var canvas = new Canvas(200, 200) + , ctx = canvas.getContext('2d') + , path = __dirname + '/bezierCurveTo.png'; + + ctx.beginPath(); + ctx.moveTo(75,40); + ctx.bezierCurveTo(75,37,70,25,50,25); + ctx.bezierCurveTo(20,25,20,62.5,20,62.5); + ctx.bezierCurveTo(20,80,40,102,75,120); + ctx.bezierCurveTo(110,102,130,80,130,62.5); + ctx.bezierCurveTo(130,62.5,130,25,100,25); + ctx.bezierCurveTo(85,25,75,37,75,40); + ctx.fill(); + + assertChecksum( + canvas + , path + , '5626a53780d77aecc490ec807ee0bc63' + , 'Canvas#bezierCurveTo() failed'); + }, + + 'test Canvas#fillStyle=': function(assert){ + var canvas = new Canvas(200, 200) + , ctx = canvas.getContext('2d') + , path = __dirname + '/fillStyle.png'; + + ctx.fillStyle = '#000'; + ctx.fillRect(110, 110, 50, 50); + assert.equal('rgba(0,0,0,1)', ctx.fillStyle); + + ctx.fillStyle = 'rgb(0,55,0)'; + ctx.fillRect(10, 10, 50, 50); + assert.equal('rgba(0,55,0,1)', ctx.fillStyle); + + ctx.fillStyle = 'rgba(0,0,0,0.5)'; + ctx.fillRect(60, 60, 50, 50); + assert.equal('rgba(0,0,0,0.5)', ctx.fillStyle); + + assertChecksum( + canvas + , path + , '01632d060ba4702a53862a955382d30d' + , 'Canvas#fillStyle= failed'); + } +} \ No newline at end of file diff --git a/wscript b/wscript new file mode 100644 index 0000000..30ed2c4 --- /dev/null +++ b/wscript @@ -0,0 +1,19 @@ +import glob + +srcdir = '.' +blddir = 'build' +VERSION = '0.0.1' + +def set_options(opt): + opt.tool_options('compiler_cxx') + +def configure(conf): + conf.check_tool('compiler_cxx') + conf.check_tool('node_addon') + conf.check_cfg(package='cairo', mandatory=1, args='--cflags --libs') + +def build(bld): + obj = bld.new_task_gen('cxx', 'shlib', 'node_addon') + obj.target = 'canvas' + obj.source = bld.glob('src/*.cc') + obj.uselib = ['CAIRO'] \ No newline at end of file