<|start_filename|>examples/sine.js<|end_filename|>

function Sine(clumsy){
    clumsy.ctx.font = '24px VoronovFont';
    clumsy.padding(100);
    clumsy.range(0, 7, -2, 2);

    var sine = clumsy.tabulate(0, 2*Math.PI, 0.01, Math.sin);
    clumsy.draw(sine);

    clumsy.axis('x', 0, 7, 0.5);
    clumsy.axis('y', -2, 2, 0.5);

    clumsy.fillTextAtCenter('Синус', clumsy.canvas.width/2, 50);
}

if(typeof module != 'undefined' && module.exports){
    module.exports = Sine;
}


<|start_filename|>examples/readme-sine.js<|end_filename|>
var Canvas = require('canvas');
var Clumsy = require('../clumsy');
var helpers = require('../helpers');

var canvas = new Canvas(800, 600)
var clumsy = new Clumsy(canvas);

clumsy.padding(50);
clumsy.range(0, 7, -1.5, 1.5);


var sine = clumsy.tabulate(0, 2*Math.PI, 0.01, Math.sin);
clumsy.lineWidth(2);
clumsy.color('red');
clumsy.draw(sine);

helpers.saveAsPng(clumsy); // save as png of same name


<|start_filename|>examples/spiral-build.js<|end_filename|>

var Canvas = require('canvas');
var GIFEncoder = require('gifencoder');

var Clumsy = require('../clumsy');
var helpers = require('../helpers');

var Spiral = require('./spiral.js');

var canvas = new Canvas(600, 600);
var clumsy = new Clumsy(canvas);

var encoder = helpers.prepareEncoder(GIFEncoder, canvas);
var phase = 0;
var n = 10;

encoder.start();
for(var i = 0; i < n; i++){
    clumsy.seed(123);

    Spiral(clumsy, phase);

    phase += 2 * Math.PI / n;
    encoder.addFrame(clumsy.ctx);
};

encoder.finish();


<|start_filename|>examples/spiral.js<|end_filename|>

function Spiral(clumsy, phase){
    clumsy.font('24px VoronovFont');
    clumsy.clean('white');

    clumsy.padding(50);
    clumsy.range(-2, 2, -2, 2);
    clumsy.lineWidth(2);
    clumsy.radius(5);

    clumsy.color('black');
    clumsy.axis('x', -2, 2, 0.5);
    clumsy.axis('y', -2, 2, 0.5);

    var spiral = clumsy.tabulate(0, 3, 0.01, function(t){
        var r = 0.5 * t;
        return {
            x: r * Math.cos(2 * Math.PI * t + phase),
            y: r * Math.sin(2 * Math.PI * t + phase)
        };
    })

    clumsy.color('red');
    clumsy.draw(spiral);

    clumsy.fillTextAtCenter('Спираль', clumsy.canvas.width/2, 30);
}

if(typeof module != 'undefined' && module.exports){
    module.exports = Spiral;
}


<|start_filename|>test/clumsy_test.js<|end_filename|>

var assert = require('assert');

var Clumsy = require('../clumsy');

describe('Clumsy', function(){
    var clumsy = new Clumsy({
        width: 800, height: 600,
        getContext: function(){
            return {};
        }
    });

    it('should set paddings', function(){
        clumsy.padding(50);

        assert.equal(clumsy.padding_left, 50);
        assert.equal(clumsy.padding_right, 50);
        assert.equal(clumsy.padding_bottom, 50);
        assert.equal(clumsy.padding_top, 50);

        clumsy.padding(100, 150);

        assert.equal(clumsy.padding_left, 100);
        assert.equal(clumsy.padding_right, 100);
        assert.equal(clumsy.padding_bottom, 150);
        assert.equal(clumsy.padding_top, 150);

        clumsy.padding(200, 250, 300, 350);

        assert.equal(clumsy.padding_left, 200);
        assert.equal(clumsy.padding_right, 250);
        assert.equal(clumsy.padding_bottom, 300);
        assert.equal(clumsy.padding_top, 350);
    });

    it('should set ranges', function(){
        clumsy.range(10, 100);

        assert.equal(clumsy.range_left, 10);
        assert.equal(clumsy.range_right, 100);
        assert.equal(clumsy.range_bottom, 10);
        assert.equal(clumsy.range_top, 100);

        clumsy.range(20, 200, 30, 300);

        assert.equal(clumsy.range_left, 20);
        assert.equal(clumsy.range_right, 200);
        assert.equal(clumsy.range_bottom, 30);
        assert.equal(clumsy.range_top, 300);
    });

    it('should scale in horizontal', function(){
        clumsy.padding(50, 100, 150, 200);
        clumsy.range(10, 110, 20, 120);

        assert.equal(clumsy.scale_horizontal(), 6.5);
    });

    it('should scale in vertical', function(){
        clumsy.padding(50, 100, 150, 200);
        clumsy.range(10, 110, 20, 120);

        assert.equal(clumsy.scale_vertical(), 2.5);
    });

    it('should rescale a line', function(){
        clumsy.padding(50, 100, 150, 200);
        clumsy.range(10, 110, 20, 120);

        var line = [{x: 0,y: 0}, {x: 10,y: 20}, {x: 200,y: 300}];
        var rescaled = clumsy.rescale(line);

        assert.equal(
            JSON.stringify(rescaled),
            JSON.stringify([{ x: -15, y: 500}, { x: 50, y: 450}, { x: 1285, y: -250}]));
    });

    it('should replot a line in multiple chains', function(){
        clumsy.padding(100);
        clumsy.range(-10, 10);

        clumsy.radius(0);
        clumsy.step(20);

        var line = [{x: -10, y: -10}, {x: 10, y: 10}];
        var replotted = clumsy.replot(line);

        assert.equal(
            JSON.stringify(replotted),
            JSON.stringify([{"x":-10,"y":-10},{"x":0,"y":0},{"x":10,"y":10}])
        )

        clumsy.step(10);
        var line = [{x: -10, y: -10}, {x: 0, y: 0}, {x: 10, y: 10}];
        var replotted = clumsy.replot(line);

        assert.equal(
            JSON.stringify(replotted),
            JSON.stringify([{"x":-10,"y":-10},{"x":-5,"y":-5},{"x":0,"y":0},{"x":5,"y":5},{"x":10,"y":10}])
        )
    });

    it('should break of replotting an empty line', function(){
        assert.equal(
            JSON.stringify(clumsy.replot([])), JSON.stringify([]))
        assert.equal(
            JSON.stringify(clumsy.replot([{x: 0, y: 0}])), JSON.stringify([]))
    });

    it('should seed random numbers', function(){
        assert.notEqual(clumsy.random(), clumsy.random());

        clumsy.seed(1);
        var r0 = clumsy.random();
        clumsy.seed(1);
        var r1 = clumsy.random();
        assert.equal(r0, r1);
    });

    it('should adjust limits', function(){
        var result = clumsy.adjustLimits(-0.9, 1.7);
        assert.equal(JSON.stringify(result.limits), JSON.stringify([ -0.8, 1.6 ]));
        assert.equal(result.step, 0.1);

        var result2 = clumsy.adjustLimits(-8, 17);
        assert.equal(JSON.stringify(result2.limits), JSON.stringify([ -7, 16]));
        assert.equal(result2.step, 1);
    });

    it('should allow to tabulate functions', function(){
        var sine = clumsy.tabulate(0, 1, 1, Math.exp);
        assert.equal(JSON.stringify(sine), JSON.stringify([{x: 0, y: 1}, {x:1, y:Math.E}]));
    });

    it('should has getters and setters for all properties', function(){
        clumsy.step(11);
        assert(clumsy.step(), 11, 'a step sets incorrectly');

        clumsy.radius(12);
        assert(clumsy.radius(), 12, 'a radius sets incorrectly');

        clumsy.color('black');
        assert(clumsy.color(), 'black', 'a color sets incorrectly');

        clumsy.background('white');
        assert(clumsy.background(), 'white', 'a background sets incorrectly');

        clumsy.font('24px Arial');
        assert(clumsy.font(), '24px Arial', 'a font sets incorrectly');

        clumsy.step(11);
        assert(clumsy.step(), 11, 'a step sets incorrectly');
    });
});


<|start_filename|>helpers/index.js<|end_filename|>

var path = require('path');
var fs = require('fs');

var takeGifName = function(){
    return path.basename(process.argv[1], '.js').replace('-build', '') + '.gif';
}

var takePngName = function(){
    return path.basename(process.argv[1], '.js').replace('-build', '') + '.png';
}

module.exports = {
    takeGifName: takeGifName,
    takePngName: takePngName,
    prepareEncoder: function(ctor, canvas, name, repeat, delay, quality){
        if(!ctor || !canvas){
            throw('First and second arguments are required. Pass constructor of GIFEncoder as first, and instance of Canvas as second');
        };

        var encoder = new ctor(canvas.width, canvas.height);
        encoder.createReadStream().pipe(
            fs.createWriteStream(
                name || takeGifName()));

        encoder.setRepeat(repeat || 0);   // 0 for repeat, -1 for no-repeat
        encoder.setDelay(delay || 50);  // frame delay in ms
        encoder.setQuality(quality || 10); // image quality. 10 is default.
        return encoder;
    },
    saveAsPng: function(clumsy){
        if(!clumsy){
            throw('First argument are required. It must be instance of Clumsy');
        };

        var name = takePngName();
        var out = fs.createWriteStream(path.join('./', name));
        clumsy.canvas.pngStream().pipe(out);
    }
}


<|start_filename|>examples/readme-axis.js<|end_filename|>
var Canvas = require('canvas');
var Clumsy = require('../clumsy');
var helpers = require('../helpers');

var canvas = new Canvas(800, 600)
var clumsy = new Clumsy(canvas);

clumsy.font('24px VoronovFont');
clumsy.padding(50);
clumsy.range(0, 7, -2, 2);
clumsy.lineWidth(2);

clumsy.axis('x', 0, 7, 0.5);
clumsy.axis('y', -2, 2, 0.5);

clumsy.color('red');
var sine = clumsy.tabulate(0, 2*Math.PI, 0.01, Math.sin);
clumsy.draw(sine);


clumsy.fillTextAtCenter('Синус', clumsy.canvas.width/2, 50);

helpers.saveAsPng(clumsy);


<|start_filename|>examples/arrow-build.js<|end_filename|>
var Canvas = require('canvas');
var Clumsy = require('../clumsy');

var canvas = new Canvas(800, 600)
var clumsy = new Clumsy(canvas);
var helpers = require('../helpers');

clumsy.font('24px VoronovFont');

clumsy.padding(100);
clumsy.range(-3, 3, -1.5, 1.5);

clumsy.axis('x', -2.5, 2.5, 0.5);


clumsy.fillTextAtCenter('Стрелочка', canvas.width/2, 50);

helpers.saveAsPng(clumsy);


<|start_filename|>examples/sine-build.js<|end_filename|>
var Canvas = require('canvas');
var Clumsy = require('../clumsy');
var drawSine = require('./sine');
var path = require('path');
var fs = require('fs');


var canvas = new Canvas(800, 600)
var clumsy = new Clumsy(canvas);

drawSine(clumsy);

var name = path.basename(process.argv[1], '.js').replace('-build', '') + '.png';
var out = fs.createWriteStream(path.join('./', name));
canvas.pngStream().pipe(out);


<|start_filename|>package.json<|end_filename|>
{
  "name": "clumsy",
  "version": "1.1.0",
  "description": "draws graphics and figures in xkcd style",
  "main": "clumsy.js",
  "scripts": {
    "test": "mocha test"
  },
  "repository": {
    "type": "git",
    "url": "git+ssh://git@github.com/kreshikhin/clumsy.git"
  },
  "keywords": [
    "figures",
    "drawing",
    "plotting",
    "plot",
    "canvas",
    "animation",
    "visualization",
    "png",
    "gif",
    "xkcd"
  ],
  "author": "<NAME>",
  "public": true,
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/kreshikhin/clumsy/issues"
  },
  "homepage": "https://github.com/kreshikhin/clumsy#readme",
  "dependencies": {
    "lodash": "*"
  },
  "devDependencies": {
    "mocha": "*"
  }
}


<|start_filename|>examples/scituner-standing-phase-build.js<|end_filename|>

var Canvas = require('canvas');
var GIFEncoder = require('gifencoder');
var path = require('path');
var fs = require('fs');

var Clumsy = require('../clumsy');
var helpers = require('../helpers');

var Draw = require('./scituner-standing-phase.js');

var canvas = new Canvas(800, 600);
var clumsy = new Clumsy(canvas);

var encoder = helpers.prepareEncoder(GIFEncoder, canvas);

var fps = 12;
var duration = 3000; // duration of GID, in ms
var n = duration * fps / 1000;
encoder.setDelay(duration / n)

encoder.start();
for(var i = 0; i < n; i++){
    Draw(clumsy, 2 * i / n - 1);
    encoder.addFrame(clumsy.ctx);
};

encoder.finish();


<|start_filename|>examples/scituner-standing-group.js<|end_filename|>

function StandingGroup(clumsy, shift){
    var canvas = clumsy.canvas;

    clumsy.clean('white');
    clumsy.font('24px VoronovFont');
    clumsy.padding(50);
    clumsy.range(0, 1.1, -1, 1);
    clumsy.radius(3);
    clumsy.step(10);
    clumsy.lineWidth(2);

    clumsy.color('black');
    clumsy.axis('x', 0, 1.1);
    clumsy.axis('y', -1, 1);

    var f0 = 5;

    var wave = clumsy.tabulate(0, 1.01, 0.01, function(t0){
        var dt = shift / f0;
        var t = t0 + dt;
        return 0.5 * Math.sin(2*Math.PI*f0*t) * Math.exp(-15*(t0-0.5)*(t0-0.5));
    });

    clumsy.color('red');
    clumsy.draw(wave);

    clumsy.fillTextAtCenter("Стоячая волна, Vгр = 0", canvas.width/2, 50);
    clumsy.fillText("x(t)", 60, 60);
    clumsy.fillText("t", 740, 330);
}

if(typeof module != 'undefined' && module.exports){
    module.exports = StandingGroup;
}


<|start_filename|>examples/overdraw.js<|end_filename|>

var Canvas = require('canvas');
var Clumsy = require('../clumsy');
var helpers = require('../helpers');
var fs = require('fs');
var path = require('path');

var canvas = new Canvas(800, 600);
var clumsy = new Clumsy(canvas);

clumsy.padding(50);
clumsy.range(0, 7, -1.5, 1.5);
clumsy.font('24px VoronovFont');

clumsy.clean('white');
clumsy.lineWidth(2);

clumsy.axis('x', 0, 7, 0.5);

clumsy.color('red');
var sine = clumsy.tabulate(0, 2*Math.PI+0.1, 0.1, Math.sin);
clumsy.overdraw(sine);

clumsy.color('black');
clumsy.axis('y', -1.5, 1.5, 0.5);

clumsy.fillTextAtCenter('График со сглаживанием', clumsy.canvas.width/2, 50);

var name = helpers.takePngName();
var out = fs.createWriteStream(path.join('./', name));
canvas.pngStream().pipe(out);


<|start_filename|>examples/bad-sine2-build.js<|end_filename|>

var Canvas = require('canvas');
var Clumsy = require('../clumsy');
var helpers = require('../helpers');
var fs = require('fs');
var path = require('path');

var canvas = new Canvas(800, 600);
var clumsy = new Clumsy(canvas);

clumsy.padding(50);
clumsy.range(0, 2*Math.PI, -1.5, 1.5);
clumsy.font('24px VoronovFont');

clumsy.radius(20);
clumsy.step(30);
clumsy.lineWidth(2);

var sine = clumsy.tabulate(0, 2*Math.PI, 0.25, function(t){
    return {x: t, y: Math.sin(t), mark: '', normal: {x:0, y:0}};
});

sine = clumsy.rescale(sine);

clumsy.ctx.beginPath();
clumsy.ctx.moveTo(sine[0].x, sine[0].y);

for(var i=0; i < sine.length; i++){
    clumsy.ctx.lineTo(
        sine[i].x + clumsy.radius() * (Math.random() - 0.5),
        sine[i].y + clumsy.radius() * (Math.random() - 0.5));
};
clumsy.ctx.stroke();

clumsy.fillTextAtCenter('Без сглаживания', clumsy.canvas.width/2, 50);

var name = helpers.takePngName();
var out = fs.createWriteStream(path.join('./', name));
canvas.pngStream().pipe(out);


<|start_filename|>clumsy.js<|end_filename|>

function Clumsy(canvas){
    if(!canvas){
        throw('Canvas is required as first argument.')
    }

    var self = this;

    self.canvas = canvas;
    self.ctx = self.canvas.getContext('2d');

    self.ctx.lineWidth = 1;
    self.ctx.lineCap = 'round';

    self.padding_left = 0;
    self.padding_right = 0;
    self.padding_bottom = 0;
    self.padding_top = 0;

    self.step_ = 30;
    self.radius_ = 10;

    self.defaultBoxAscent_ = 16;

    self.background_ = 'white';

    function isNumber(n){
        return !isNaN(parseFloat(n)) && isFinite(n);
    }

    function isSet(object){
        return (typeof object !== 'undefined');
    }

    self.font = function(font){
        if(font === undefined) return self.ctx.font;
        self.ctx.font = font;
    }

    self.step = function(step){
        if(step === undefined) return self.step_;
        self.step_ = step;
    }

    self.radius = function(radius){
        if(radius === undefined) return self.radius_;
        self.radius_ = radius;
    }

    self.background = function(background){
        if(background === undefined) return self.background_;
        self.background_ = background;
    }

    self.defaultBoxAscent = function(defaultBoxAscent){
        if(defaultBoxAscent === undefined) return self.defaultBoxAscent_;
        self.defaultBoxAscent_ = defaultBoxAscent;
    }

    self.color = function(color){
        if(color === undefined) return self.ctx.strokeStyle;
        self.ctx.strokeStyle = color;
    }

    self.lineWidth = function(width){
        if(width === undefined){
            return self.ctx.lineWidth;
        }

        self.ctx.lineWidth = width;
    }

    self.clean = function(color){
        self.ctx.save();
        self.background_ = color || self.background_ || 'white';
        self.ctx.fillStyle = self.background_;
        self.ctx.fillRect(0, 0, self.canvas.width, self.canvas.height);
        self.ctx.restore();
    }

    self.padding = function(p0, p1, p2, p3){
        if(p1 === undefined){
            self.padding_left = p0;
            self.padding_right = p0;
            self.padding_bottom = p0;
            self.padding_top = p0;
            return
        }

        if(p2 === undefined){
            self.padding_left = p0;
            self.padding_right = p0;
            self.padding_bottom = p1;
            self.padding_top = p1;
            return;
        }

        self.padding_left = p0;
        self.padding_right = p1;
        self.padding_bottom = p2;
        self.padding_top = p3;
    };

    self.range_left = 0;
    self.range_right = self.canvas.width;
    self.range_bottom = 0;
    self.range_top = self.canvas.height;

    self.range_horizontal = function(){
        return self.range_right - self.range_left;
    };

    self.range_vertical = function(){
        return self.range_top - self.range_bottom;
    };

    self.range = function(r0, r1, r2, r3){
        if(r3 === undefined){
            self.range_left = r0;
            self.range_right = r1;
            self.range_bottom = r0;
            self.range_top = r1;
            return;
        }

        self.range_left = r0;
        self.range_right = r1;
        self.range_bottom = r2;
        self.range_top = r3;
    };


    self.scale_horizontal = function(){
        return (self.canvas.width - self.padding_left - self.padding_right) / self.range_horizontal();
    };

    self.scale_vertical = function(){
        return (self.canvas.height - self.padding_bottom - self.padding_top) / self.range_vertical();
    }

    self.overdraw = function(line){
        self.ctx.save();
        var seed = self.seed();
        self.ctx.strokeStyle = self.background_;
        self.ctx.lineWidth *= 3;
        self.seed(seed);
        self.draw(line);
        self.ctx.restore();
        self.seed(seed);
        self.draw(line);
    }

    self.tabulate = function(start, end, step, cb){
        var result = [];
        for(var t = start; t <= end; t+= step){
            var f = cb(t);

            if(isNumber(f)){
                result.push({x: t, y: f});
                continue;
            }

            if(isSet(f) && isSet(f.x) && isSet(f.y)){
                result.push(f);
            }
        }
        return result;
    }

    self.draw = function(line){
        var ctx = self.ctx;

        if(line.length < 2) return [];

        var rescaledLine = self.rescale(line);
        var replottedLine = self.replot(rescaledLine);

        ctx.beginPath();
        ctx.moveTo(replottedLine[0].x, replottedLine[0].y);
        var last = replottedLine[0];

        if(replottedLine.length > 2){
            for(var i = 1; i < replottedLine.length - 2; i ++){
                var point = replottedLine[i];
                var nextPoint = replottedLine[i+1];

                var xc = (point.x + nextPoint.x) / 2;
                var yc = (point.y + nextPoint.y) / 2;
                ctx.quadraticCurveTo(point.x, point.y, xc, yc);
            }

            ctx.quadraticCurveTo(replottedLine[i].x, replottedLine[i].y, replottedLine[i+1].x,replottedLine[i+1].y);
                    var last = {x: replottedLine[i+1].x, y:replottedLine[i+1].y};
            last = replottedLine[i+1];
        }else{
            ctx.lineTo(replottedLine[1].x, replottedLine[1].y);
            last = replottedLine[1];
        }

        ctx.stroke();

        for(var i = 0; i < replottedLine.length; i++){
            var point = replottedLine[i];

            if(point.mark === undefined) continue;
            var tick_size = point.tick_size || 10;
            ctx.beginPath();
            ctx.moveTo(
                point.x - 0.5 * tick_size * point.normal.x,
                point.y - 0.5 * tick_size * point.normal.y);
            ctx.lineTo(
                point.x + 0.5 * tick_size * point.normal.x,
                point.y + 0.5 * tick_size * point.normal.y);
            ctx.stroke();

            self.drawText(point.mark,
                point.x + tick_size * point.normal.x,
                point.y + tick_size * point.normal.y,
                point.normal);
        }

        if(replottedLine.length >= 2){
            var l = replottedLine.length - 1;
            var p0 = replottedLine[l-1];
            var p1 = replottedLine[l];

            var dir = { x: p1.x - p0.x, y: p1.y - p0.y};
            dir_length = Math.sqrt(dir.x*dir.x+dir.y*dir.y);
            last.direction = {x: dir.x / dir_length, y: dir.y / dir_length};
        };


        return last;
    }

    self.rescale = function(line){
        var result = [];
        var scale_horizontal = self.scale_horizontal();
        var scale_vertical = self.scale_vertical();

        for(var i = 0; i < line.length; i++){
            var point = line[i];

            result.push({
                x: (point.x - self.range_left) * scale_horizontal  + self.padding_left,
                y: self.canvas.height - ((point.y - self.range_bottom) * scale_vertical  + self.padding_bottom),
                mark: point.mark,
                normal: point.normal
            });
        };
        return result;
    };

    self.replot = function(line){
        var step = self.step_;
        var radius = self.radius_;

        var accuracy = 0.25;

        if(line.length < 2) return [];
        var replottedLine = [];

        var beginning = line[0];
        replottedLine.push(beginning);

        for(var i = 1; i < line.length; i++){
            var point = line[i];
            var dx = point.x - beginning.x;
            var dy = point.y - beginning.y;
            var d = Math.sqrt(dx*dx+dy*dy);

            if(point.mark !== undefined){
                replottedLine.push(point);
                beginning = point;
                continue;
            }

            if(d < step * (1 - accuracy) && (i + 1 < line.length)){
                // too short
                continue;
            }

            if(d > step * (1 + accuracy)){
                // too long
                var n = Math.ceil(d / step);
                for(var j = 1; j < n; j++){
                    replottedLine.push({
                        x: beginning.x + dx * j / n,
                        y: beginning.y + dy * j / n
                    });
                }
            }

            replottedLine.push(point);
            beginning = point;
        };

        for(var i = 1; i < replottedLine.length; i++){
            var point = replottedLine[i];
            replottedLine[i].x = point.x + radius * (self.random() - 0.5);
            replottedLine[i].y = point.y + radius * (self.random() - 0.5);
        };

        return replottedLine;
    };

    self.rightArrow = function(x, y, dx, dy){
        var ctx = self.ctx;
        ctx.beginPath();
        ctx.moveTo(x-dx, y+dy);
        ctx.lineTo(x, y);
        ctx.lineTo(x-dx, y-dy);
        ctx.stroke();
    };

    self.topArrow = function(x, y, dx, dy){
        var ctx = self.ctx;
        ctx.beginPath();
        ctx.moveTo(x-dx, y-dy);
        ctx.lineTo(x, y);
        ctx.lineTo(x+dx, y-dy);
        ctx.stroke();
    };

    self.fillTextAtCenter = function(text, x, y){
        var size = self.ctx.measureText(text);
        self.ctx.fillText(text, x - size.width/2, y);
    };

    self.fillText = function(text, x, y){
        self.ctx.fillText(text, x, y);
    };

    self.axis = function(axis, t0, t1, options){
        if(isNumber(options)){
            options = {step: options}
        };

        options = options || {};

        var dir = axis;
        if(axis === 'x' || axis === '+x') dir = {x: 1, y: 0};
        if(axis === 'y' || axis === '+y') dir = {x: 0, y: 1};
        if(axis === '-x') dir = {x: -1, y: 0};
        if(axis === '-y') dir = {x: -0, y: 1};

        dir = {
            x: dir.x / Math.sqrt(dir.x*dir.x+dir.y*dir.y),
            y: dir.y / Math.sqrt(dir.x*dir.x+dir.y*dir.y),
        };

        var adjusted = self.adjustLimits(t0, t1);

        var opts = options || {};
        var line = [];
        var step = options.step || adjusted.step;
        var zero = opts.zero || {x: 0, y: 0};
        var limits = opts.limits || [t0 + step, t1 - step];
        var hide_zero = opts.hide_zero !== undefined ? opts.hide_zero : true;

        var mark = opts.mark || function(t){
            return parseInt(t) + '.' + parseInt(Math.abs(t*10) % 10)
        };

        if(isSet(opts.step)){
            for(var t=limits[0]; t <= limits[1]; t += opts.step){
                if(hide_zero && t === 0){
                    continue;
                };

                line.push({
                    x: zero.x + dir.x*t,
                    y: zero.y + dir.y*t,
                    normal: {x: -dir.y, y: dir.x},
                    mark: mark(t),
                    style: opts.style,
                    tick_size: opts.tick_size || 5
                });
            };
        }

        if(t0 != limits[0]) line.unshift({
            x: zero.x + dir.x*t0,
            y: zero.y + dir.y*t0
        });

        if(t1 != limits[0]) line.push({
            x: zero.x + dir.x*t1,
            y: zero.y + dir.y*t1
        });

        var point = self.draw(line);
        self.drawArrow(point, point.direction);
    };

    self.drawAxis = self.axis;

    self.drawArrow = function(point, direction){
        if(!direction) return;

        var ctx = self.ctx;
        var arrow_length = 10;
        var arrow_width = 5

        var normal = {x: direction.y, y: -direction.x};

        var line = [
            {x: point.x + arrow_width*normal.x - arrow_length*direction.x,
            y: point.y + arrow_width*normal.y - arrow_length*direction.y},
            {x: point.x, y: point.y},
            {x: point.x - arrow_width*normal.x - arrow_length*direction.x,
            y: point.y - arrow_width*normal.y - arrow_length*direction.y}
        ];

        ctx.beginPath();
        ctx.moveTo(line[0].x, line[0].y);
        ctx.lineTo(line[1].x, line[1].y);
        ctx.lineTo(line[2].x, line[2].y);
        ctx.stroke();
    }

    self.drawText = function(text, x, y, align){
        var ctx = self.ctx;
        var m = ctx.measureText(text);
        m.actualBoundingBoxAscent = m.actualBoundingBoxAscent || self.defaultBoxAscent_;

        if(align === 'center'){
            ctx.fillText(text, x - m.width / 2, y);
            return;
        }
        if(isSet(align) && isNumber(align.x) && isNumber(align.y)){
            ctx.fillText(text,
                x + (-1 + align.x) * m.width / 2,
                y + (1 + align.y) * m.actualBoundingBoxAscent / 2);
            return;
        }

        ctx.fillText(text, x, y);
    };

    // random numbers
    var m_w = 123456789;
    var m_z = 987654321;
    var mask = 0xffffffff;

    // Takes any integer
    self.seed = function(i){
        if(i === undefined){
            return m_w;
        }
        m_w = i;
        m_z = 987654321;;
    }

    // Returns number between 0 (inclusive) and 1.0 (exclusive),
    // just like Math.random().
    self.random = function(){
        m_z = (36969 * (m_z & 65535) + (m_z >> 16)) & mask;
        m_w = (18000 * (m_w & 65535) + (m_w >> 16)) & mask;
        var result = ((m_z << 16) + m_w) & mask;
        result /= 4294967296;
        return result + 0.5;
    }

    self.adjustLimits = function(start, end){
        if(start === end) { return [0, 0]; };
        var d = Math.abs(end - start) / 10;
        var p = Math.floor(Math.log(d) / Math.log(10));
        var step = Math.pow(10, p);

        var limit_start = start > 0 ?
            Math.floor(start / step - 0.5) * step:
            Math.ceil(start / step + 0.5) * step;
        var limit_end = end > 0 ?
            Math.floor(end / step - 0.5) * step:
            Math.ceil(end / step + 0.5) * step;

        return {limits: [limit_start, limit_end], step: step};
    }
};

if (typeof module !== 'undefined' && module.exports) {
    module.exports = Clumsy;
}


<|start_filename|>examples/readme-intro.js<|end_filename|>
var Canvas = require('canvas');
var Clumsy = require('../clumsy');

var canvas = new Canvas(800, 600)
var clumsy = new Clumsy(canvas);
var helpers = require('../helpers');

clumsy.font('24px VoronovFont');

clumsy.padding(100);
clumsy.range(0, 7, -1.5, 1.5);

clumsy.clean();

clumsy.color('black');
clumsy.lineWidth(2);
clumsy.axis('x', 0, 7, 1);
clumsy.axis('y', -1.5, 1.5, 0.5);

var sine = clumsy.tabulate(0, 2*Math.PI, 0.05, Math.sin);
clumsy.color('green');
clumsy.lineWidth(3);
clumsy.overdraw(sine);

var cosine = clumsy.tabulate(0, 2*Math.PI, 0.05, Math.cos);
clumsy.color('blue');
clumsy.lineWidth(3);
clumsy.overdraw(cosine);




clumsy.fillTextAtCenter('Sine vs Cosine', canvas.width/2, 50);
clumsy.fillText("x(t)", 110, 110);
clumsy.fillText("t", 690, 330);

helpers.saveAsPng(clumsy);


<|start_filename|>examples/bad-sine1-build.js<|end_filename|>

var Canvas = require('canvas');
var Clumsy = require('../clumsy');
var helpers = require('../helpers');
var fs = require('fs');
var path = require('path');

var canvas = new Canvas(800, 600);
var clumsy = new Clumsy(canvas);

clumsy.padding(50);
clumsy.range(0, 2*Math.PI, -1.5, 1.5);
clumsy.font('24px VoronovFont');

clumsy.radius(30);
clumsy.step(1);
clumsy.lineWidth(2);

var sine = clumsy.tabulate(0, 2*Math.PI, 0.1, Math.sin);

clumsy.color('black');
clumsy.draw(sine);
clumsy.fillTextAtCenter('Неправильный шаг', clumsy.canvas.width/2, 50);

var name = helpers.takePngName();
var out = fs.createWriteStream(path.join('./', name));
canvas.pngStream().pipe(out);
