├── .eslintrc ├── README.md ├── example.js ├── example2.js ├── index.js └── package.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "indent": [ 4 | 2, 5 | 2 6 | ], 7 | "quotes": [ 8 | 2, 9 | "double" 10 | ], 11 | "linebreak-style": [ 12 | 2, 13 | "unix" 14 | ], 15 | "semi": [ 16 | 2, 17 | "always" 18 | ] 19 | }, 20 | "env": { 21 | "node": true, 22 | "browser": true 23 | }, 24 | "extends": "eslint:recommended" 25 | } 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | json2d 2 | ======= 3 | 4 | Express vectorial content in JSON using canvas2d directives 5 | 6 | ## Goal 7 | 8 | **json2d** is a thin wrapper to expose canvas2d API in a JSON DSL. 9 | The goal is to statically describe scalable content. 10 | 11 | ## The API 12 | 13 | ``` 14 | var json2d = require("json2d"); 15 | var canvas = ...; 16 | var ctx = canvas.getContext("2d"); 17 | var json2d = json2d(ctx); 18 | 19 | // Now call the render method: 20 | json2d.render(data); 21 | ``` 22 | 23 | ## The `data` format 24 | 25 | ```js 26 | { 27 | "size": [ Number, Number ], 28 | "background": String, 29 | "draws": Directive[] 30 | } 31 | ``` 32 | 33 | > where `Directive` is either: 34 | - an `Object` defining all styles to set 35 | - an `Array` defining the draw atomic operation: the method to call followed by parameters 36 | - a sub `Directive[]` 37 | 38 | ### `size` 39 | 40 | You have to **define your viewport `size`** and the rendering will scale to the canvas size and will also keep your ratio (it will fit the biggest centered rectangle). 41 | 42 | ### `background` 43 | 44 | The full canvas will be filled with a **`background` color**. 45 | 46 | ### `draws` 47 | 48 | Your **`draws` directives** will be used to fill and draw content. These directives are **direct mapping from [Canvas 2D context API](http://www.w3.org/TR/2dcontext/)**. Almost every possible shapes that canvas2d provides are supported with following DSL: 49 | 50 | **`draws` directives is an array where each element `E` is one of following:** 51 | - If `E` is an *object*, this object defines all styles to set: all values or this object will be set to the canvas context. 52 | - If `E` is an *array*, this defines an atomic drawing operation: the first element is the canvas2d context method name and the following elements are arguments to that method. 53 | - If `E` is an *array* and `E[0]` is also an array, E is a sub array of `draws` directives. It defines a group of operations. Such group is scoped: `ctx.save()` will be called at the start and `ctx.restore()` will be called at the end. 54 | 55 | All draws that occurs will be scaled relatively to the `size` you have defined. That way, we can define scalable (vectorial) content. 56 | 57 | ## Extension to the Canvas 2D API 58 | 59 | There are some exceptions where the `draws` get extended. 60 | 61 | ### drawImage 62 | 63 | `json2d` supports images drawing with `drawImage` but to define the image you can pass the URL (or a data64 URL) in the first argument. 64 | 65 | ### Multi-line texts support 66 | 67 | `json2d` supports multi-line texts using the `\n` character. 68 | To do so, every texts will be split on `\n` and result of multiple texts draws. 69 | Note that you still have to define where the new lines are. 70 | 71 | In that context, you 72 | **MUST provide a 4th parameter** if you want that multi-line feature: **the lineHeight in pixels**. 73 | 74 | > Note: the `maxWidth` that allows the [canvas2d specification](http://www.w3.org/TR/2dcontext/#drawing-text-to-the-canvas) is not supported by json2d. 75 | 76 | ## Full Example: 77 | 78 | ```js 79 | var json = { 80 | "background": "#efd", 81 | "size": [ 800, 600 ], 82 | "draws": [ 83 | [ "drawImage", "http://i.imgur.com/N8a9CkZ.jpg", 530, 700, 800, 200, 0, 400, 800, 200 ], 84 | { "font": "italic bold 20px monospace", "fillStyle": "#09f", "textBaseline": "top", "textAlign": "right" }, 85 | [ "fillText", "some texts...\nLorem ipsum dolor sit amet,\n consectetur adipiscing elit,\n sed do eiusmod tempor incididunt ut labore\net dolore magna aliqua.", 780, 10, 20 ], 86 | { "font": "italic 40px sans-serif", "fillStyle": "#099", "textBaseline": "middle", "textAlign": "center" }, 87 | [ "fillText", "does support\nmulti-line texts!\n\nusing the '\\n' character.\n\nand also supports images :)", 400, 200, 48], 88 | [ "beginPath" ], 89 | { "fillStyle": "#f00" }, 90 | [ "arc", 40, 40, 20, 0, 7 ], 91 | [ "fill" ], 92 | [ 93 | [ "beginPath" ], 94 | { "fillStyle": "#f0f", "strokeStyle": "#909", "lineWidth": 4 }, 95 | [ "moveTo", 100, 60 ], 96 | [ "lineTo", 80, 20 ], 97 | [ "lineTo", 120, 20 ], 98 | [ "fill" ], 99 | [ "closePath" ], 100 | [ "stroke" ] 101 | ], 102 | [ 103 | [ "rotate", -1.5708 ], 104 | { "font": "normal 60px sans-serif", "fillStyle": "#f00", "textBaseline": "top", "textAlign": "right" }, 105 | [ "fillText", "Some shapes", -80, 0 ] 106 | ], 107 | [ 108 | [ "beginPath" ], 109 | { "strokeStyle": "rgba(0,0,0,0.2)", "lineWidth": 4 }, 110 | [ "rect", 2, 2, 796, 596 ], 111 | [ "stroke" ] 112 | ] 113 | ] 114 | }; 115 | 116 | function Canvas (w, h, r) { 117 | var canvas = document.createElement("canvas"); 118 | canvas.width = r * w; 119 | canvas.height = r * h; 120 | canvas.style.width = w + "px"; 121 | canvas.style.height = h + "px"; 122 | return canvas; 123 | } 124 | 125 | var json2d = require("json2d"); 126 | var canvas = Canvas(600, 300, window.devicePixelRatio || 1); 127 | var ctx = canvas.getContext("2d"); 128 | var json2d = json2d(ctx); 129 | json2d.render(json); 130 | document.body.appendChild(canvas); 131 | ``` 132 | 133 | ![](http://i.imgur.com/hAPUzTb.png) 134 | 135 | Note in the example how the content is trying to take the biggest possible rectangle in the canvas viewport. For the sake of this example, we have drawn the biggest possible rectangle with a red stroke, but usually you just fill text and shapes in the middle to have a seamless rendering. 136 | 137 | ## `json2d(context2d, **resolveImage**)` 138 | 139 | The json2d constructor allows a second parameter: 140 | A function to resolve an `Image` by URL. 141 | 142 | This allows to cache images yourself for instance 143 | to ensure that all images are loaded before calling `render`. 144 | 145 | By default, `render()` will postpone other `render()` every time an image is loaded 146 | (like a web page will render images one by one after they loads). 147 | 148 | ## `render(data, **visitor**)` 149 | 150 | The render method allows a second parameter: 151 | a visitor function called after each rendering steps. 152 | 153 | It is called in **post order** for each draw *Directive*. 154 | 155 | That function is called with 2 parameters: 156 | - the `path`: an array of indexes of the tree path of the current draw. 157 | - the `draw` object. 158 | 159 | This function is used by `json2d-editor` implementation. 160 | 161 | ## Used by... 162 | 163 | - [diaporama](https://github.com/gre/diaporama/) 164 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | var json = { 2 | "background": "#efd", 3 | "size": [ 800, 600 ], 4 | "draws": [ 5 | [ "drawImage", "http://i.imgur.com/N8a9CkZ.jpg", 530, 700, 800, 200, 0, 400, 800, 200 ], 6 | { "font": "italic bold 20px monospace", "fillStyle": "#09f", "textBaseline": "top", "textAlign": "right" }, 7 | [ "fillText", "some texts...\nLorem ipsum dolor sit amet,\n consectetur adipiscing elit,\n sed do eiusmod tempor incididunt ut labore\net dolore magna aliqua.", 780, 10, 20 ], 8 | { "font": "italic 40px sans-serif", "fillStyle": "#099", "textBaseline": "middle", "textAlign": "center" }, 9 | [ "fillText", "does support\nmulti-line texts!\n\nusing the '\\n' character.\n\nand also supports images :)", 400, 200, 48], 10 | [ "beginPath" ], 11 | { "fillStyle": "#f00" }, 12 | [ "arc", 40, 40, 20, 0, 7 ], 13 | [ "fill" ], 14 | [ 15 | { "fillStyle": "#f0f", "strokeStyle": "#909", "lineWidth": 4 }, 16 | [ "beginPath" ], 17 | [ "moveTo", 100, 60 ], 18 | [ "lineTo", 80, 20 ], 19 | [ "lineTo", 120, 20 ], 20 | [ "fill" ], 21 | [ "closePath" ], 22 | [ "stroke" ] 23 | ], 24 | [ 25 | [ "rotate", -1.5708 ], 26 | { "font": "normal 60px sans-serif", "fillStyle": "#f00", "textBaseline": "top", "textAlign": "right" }, 27 | [ "fillText", "Some shapes", -80, 0 ] 28 | ], 29 | [ 30 | [ "beginPath" ], 31 | { "strokeStyle": "rgba(0,0,0,0.2)", "lineWidth": 4 }, 32 | [ "rect", 2, 2, 796, 596 ], 33 | [ "stroke" ] 34 | ] 35 | ] 36 | }; 37 | 38 | 39 | function Canvas (w, h, r) { 40 | var canvas = document.createElement("canvas"); 41 | canvas.width = r * w; 42 | canvas.height = r * h; 43 | canvas.style.width = w + "px"; 44 | canvas.style.height = h + "px"; 45 | return canvas; 46 | } 47 | 48 | var json2d = require("."); 49 | var canvas = Canvas(600, 300, window.devicePixelRatio || 1); 50 | var ctx = canvas.getContext("2d"); 51 | var json2d = json2d(ctx); 52 | json2d.render(json, console.log.bind(console)); 53 | document.body.appendChild(canvas); 54 | 55 | 56 | window.json2d = json2d; // for debug purpose 57 | -------------------------------------------------------------------------------- /example2.js: -------------------------------------------------------------------------------- 1 | var json = { 2 | "background": "#000", 3 | "size": [ 800, 600 ], 4 | "draws": [ 5 | ["drawImage", "http://i.imgur.com/K0iotM4.jpg", 0, 0, 400, 300], 6 | ["drawImage", "http://i.imgur.com/rGQ1GBo.jpg", 400, 0, 400, 300], 7 | ["drawImage", "http://i.imgur.com/3Xkv00y.jpg", 0, 300, 400, 300], 8 | ["drawImage", "http://i.imgur.com/gKLcnIT.jpg", 400, 300, 400, 300] 9 | ] 10 | }; 11 | 12 | create().render(json); 13 | 14 | document.body.appendChild(document.createElement("br")); 15 | 16 | var asyncjson2d = create(waitAllImages(json, function () { 17 | asyncjson2d.render(json); 18 | })); 19 | 20 | /////////////////////////////////////////////// 21 | 22 | function Canvas (w, h, r) { 23 | var canvas = document.createElement("canvas"); 24 | canvas.width = r * w; 25 | canvas.height = r * h; 26 | canvas.style.width = w + "px"; 27 | canvas.style.height = h + "px"; 28 | return canvas; 29 | } 30 | 31 | function forEachImage (draws, cb) { 32 | draws.forEach(function (op) { 33 | if (op instanceof Array) { 34 | if (typeof op[0] === "object") { 35 | forEachImage(op, cb); 36 | } 37 | else if (op[0]==="drawImage") { 38 | cb(op[1]); 39 | } 40 | } 41 | }); 42 | } 43 | 44 | function waitAllImages (json, done) { 45 | var imgsCache = {}; 46 | var loaded = 0; 47 | var count = 0; 48 | forEachImage(json.draws, function (src) { 49 | ++count; 50 | var img = new window.Image(); 51 | img.onload = function () { 52 | if(++loaded === count) 53 | done(); 54 | }; 55 | img.src = src; 56 | imgsCache[src] = img; 57 | }); 58 | if (count === 0) setTimeout(done, 0); 59 | return function (url) { 60 | return imgsCache[url]; 61 | }; 62 | } 63 | 64 | function create (resolveImage) { 65 | var json2d = require("."); 66 | var canvas = Canvas(400, 300, window.devicePixelRatio || 1); 67 | var ctx = canvas.getContext("2d"); 68 | var json2d = json2d(ctx, resolveImage); 69 | document.body.appendChild(canvas); 70 | return json2d; 71 | } 72 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var drawImage = require("draw-image-normalized"); 2 | var rectCrop = require("rect-crop"); 3 | 4 | var defaults = { 5 | background: "#000", 6 | size: [1000, 1000], 7 | draws: [] 8 | }; 9 | 10 | function UnsupportedDrawOperation (reason, path) { 11 | var temp = Error.call(this, reason); 12 | this.name = temp.name = "UnsupportedDrawOperation"; 13 | this.stack = temp.stack; 14 | this.message = temp.message; 15 | this.path = path; 16 | } 17 | UnsupportedDrawOperation.prototype = Object.create(Error.prototype, { 18 | constructor: { 19 | value: UnsupportedDrawOperation, 20 | writable: true, 21 | configurable: true 22 | } 23 | }); 24 | 25 | function defaultResolveImage (url) { 26 | if (url in this._imgs) { 27 | return this._imgs[url]; 28 | } 29 | var img = new window.Image(); 30 | img.crossOrigin = true; 31 | img.src = url; 32 | this._imgs[url] = img; 33 | img.onload = this.flush.bind(this); 34 | return img; 35 | } 36 | 37 | function json2d (context2d, resolveImage) { 38 | if (!(this instanceof json2d)) 39 | return new json2d(context2d, resolveImage); 40 | this.ctx = context2d; 41 | this._imgs = {}; 42 | this.resolveImage = resolveImage ? function (src) { 43 | var res = resolveImage(src); 44 | if (typeof res === "string") 45 | return defaultResolveImage.call(this, res); 46 | return res; 47 | } : defaultResolveImage; 48 | } 49 | 50 | json2d.defaultResolveImage = defaultResolveImage; 51 | json2d.defaults = defaults; 52 | 53 | json2d.prototype = { 54 | destroy: function () { 55 | this._item = null; 56 | this._imgs = null; 57 | this.ctx = null; 58 | }, 59 | 60 | getSize: function (item) { 61 | return item.size || defaults.size; 62 | }, 63 | 64 | getRectangle: function (item) { 65 | var size = this.getSize(item); 66 | var w = size[0]; 67 | var h = size[1]; 68 | return rectCrop.largest({ width: w, height: h }, this.ctx.canvas); 69 | }, 70 | 71 | flush: function () { 72 | if (this._item) this.render(this._item); 73 | }, 74 | 75 | render: function (item, visitor) { 76 | this._item = item; 77 | 78 | var ctx = this.ctx; 79 | var canvas = ctx.canvas; 80 | var W = canvas.width; 81 | var H = canvas.height; 82 | var bg = item.background || defaults.size; 83 | var size = this.getSize(item); 84 | var rect = this.getRectangle(item); 85 | var w = size[0]; 86 | var h = size[1]; 87 | 88 | ctx.save(); 89 | ctx.fillStyle = bg; 90 | ctx.fillRect(0, 0, W, H); 91 | ctx.translate(Math.round(rect[0]), Math.round(rect[1])); 92 | ctx.scale(rect[2] / w, rect[3] / h); 93 | this._renderRec(item.draws || defaults.draws, [], visitor || function(){}); 94 | ctx.restore(); 95 | }, 96 | 97 | _renderRec: function (draws, path, visitor) { 98 | var ctx = this.ctx; 99 | var drawslength = draws.length; 100 | for (var i=0; i