├── README.md ├── console.snapshot.js └── index.html /README.md: -------------------------------------------------------------------------------- 1 | # console.snapshot( _canvas_ ) 2 | ### A simple canvas profiler 3 | 4 | `console.snapshot` takes and inputted _<canvas>_ element and outputs a snapshot of it into the console. It makes debugging the canvas a little less dramatic. See [this demo](http://adriancooney.github.io/console.snapshot). `console.snapshot` is a fork of the [`console.image`](http://github.com/adriancooney/console.image) and actually does something useful. 5 | 6 | ![The demo](http://i.imgur.com/fA0UGXT.png) 7 | 8 | ## Usage 9 | ### console.snapshot( _<canvas>_ ) 10 | `console.snapshot` profiles a canvas for 1 iteration in the browser render loop or for one `requestAnimationFrame` tick and outputs the canvas context call stack and state changes to the console. 11 | 12 | ```js 13 | var canvas = document.getElementById("canvas"); 14 | 15 | console.snapshot(canvas); 16 | ``` 17 | 18 | ![Sample output](http://i.imgur.com/CCO85C5.png); 19 | 20 | ### console.screenshot( _<canvas>_ [, _<scale>_ ] ) 21 | `console.screenshot` takes in a `HTMLCanvasElement`, base64 encodes it using `toDataURL` and then outputs it to the canvas using `console.image`. You can also pass along an optional scale factor to scale down the outputted image for better viewing within the Dev tools. 22 | 23 | ```js 24 | var canvas = document.createElement("canvas"), 25 | ctx = canvas.getContext("2d"); 26 | 27 | // ... 28 | //draw 29 | // ... 30 | 31 | console.screenshot(canvas); 32 | console.screenshot(canvas, 0.8); //Snapshot it and scale the output to 80% of the original size 33 | ``` 34 | 35 | ![Screenshot demo](http://i.imgur.com/e1EUuhM.png) 36 | 37 | ### console.image( _<url>_ ) 38 | `console.image` outputs an image from a url into the console. See [console.image](http://github.com/adriancooney/console.image). 39 | 40 | ```js 41 | console.image("http://i.imgur.com/wWPQK.gif"); 42 | ``` 43 | 44 | ## Troubleshooting 45 | ##### I'm getting "Uncaught Error: SecurityError: DOM Exception 18" when I try to snapshot the canvas. 46 | This is caused by printing a non CORS-enabled image on the canvas. The browser blocks the `toDataURL` function which is what `console.image` depends on to print the canvas. To fix this, see [this tutorial on HTML5Rocks](http://www.html5rocks.com/en/tutorials/cors/) or consider passing your image through a CORS proxy such as [corsproxy.com](http://corsproxy.com). 47 | 48 | License: MIT 49 | -------------------------------------------------------------------------------- /console.snapshot.js: -------------------------------------------------------------------------------- 1 | /** 2 | * An actual useful fork of dunxrion/console.image 3 | * Created by Adrian Cooney 4 | * http://dunxrion.github.io 5 | */ 6 | 7 | (function(console) { 8 | "use strict"; 9 | 10 | /** 11 | * Since the console.log doesn't respond to the `display` style, 12 | * setting a width and height has no effect. In fact, the only styles 13 | * I've found it responds to is font-size, background-image and color. 14 | * To combat the image repeating, we have to get a create a font bounding 15 | * box so to speak with the unicode box characters. EDIT: See Readme.md 16 | * 17 | * @param {int} width The height of the box 18 | * @param {int} height The width of the box 19 | * @return {object} {string, css} 20 | */ 21 | function getBox(width, height) { 22 | return { 23 | string: "+", 24 | style: "font-size: 1px; padding: " + Math.floor(height/2) + "px " + Math.floor(width/2) + "px; line-height: " + height + "px;" 25 | } 26 | } 27 | 28 | /** 29 | * Profile class for a canvas 30 | * @param {CanvasRenderingContext2D} ctx The context to profile 31 | * @param {HTMLCanvasElement} canvas The canvas element 32 | */ 33 | var Profile = function(ctx, canvas) { 34 | this.ctx = ctx; 35 | this.canvas = canvas; 36 | this.stack = []; 37 | this.startTime = (new Date()).getTime(); 38 | this.collectFPS = true; 39 | this.frames = 0; 40 | 41 | //Add the initial state 42 | this.addState(this.returnState(ctx)); 43 | 44 | //Add a sort of man in the middle/proxy for all the 45 | //context render functions and tell the profiler 46 | //when a function call happens. We send along the 47 | //function name, the arguments, and the context state 48 | var that = this; 49 | for(var fn in ctx) { //Move the native data to a namespace 50 | if(typeof ctx[fn] == "function") { 51 | ctx["_CF_" + fn] = ctx[fn]; 52 | (function(fn) { //Create the closure 53 | ctx[fn] = function() { 54 | that.addState(that.returnState(ctx)); 55 | that.addFunctionCall(fn, Array.prototype.slice.call(arguments)); 56 | ctx["_CF_" + fn].apply(ctx, arguments); 57 | } 58 | })(fn); 59 | } 60 | } 61 | 62 | //Start collecting the frames 63 | (function tick() { 64 | if(that.collectFPS) that.frames++, requestAnimationFrame(tick); 65 | })(); 66 | }; 67 | 68 | /** 69 | * Stop profiling 70 | * @return {null} 71 | */ 72 | Profile.prototype.end = function() { 73 | this.collectFPS = false; 74 | this.duration = ((new Date()).getTime()) - this.startTime; 75 | this.FPS = (this.frames * 1000)/this.duration; //TODO: fix this formula 76 | 77 | //Remove the man in the middle 78 | var ctx = this.ctx; 79 | for(var fn in ctx) { 80 | if(fn.match(/_CF_/)) { 81 | fn = fn.replace("_CF_", ""); 82 | ctx[fn] = ctx["_CF_" + fn]; 83 | delete ctx["_CF_" + fn]; //And remove the cache 84 | } 85 | } 86 | } 87 | 88 | /** 89 | * Return the useful data from the context 90 | * to represent the state of the context 91 | * Only adds the properties and not functions 92 | * @param {CanvasRenderContext2D} ctx The context to make a state from 93 | * @return {Object} State object 94 | */ 95 | Profile.prototype.returnState = function(ctx) { 96 | var obj = {}; 97 | if(!this._stateKeys) { 98 | this._stateKeys = []; 99 | for(var key in ctx) { 100 | if(typeof ctx[key] == "string" || typeof ctx[key] == "number") this._stateKeys.push(key), obj[key] = ctx[key]; 101 | } 102 | } else { 103 | for (var i = this._stateKeys.length - 1; i >= 0; i--) { 104 | var key = this._stateKeys[i]; 105 | obj[key] = ctx[key]; 106 | }; 107 | } 108 | 109 | return obj; 110 | }; 111 | 112 | /** 113 | * Add a state object to the current profile 114 | * @param {StateObject} state See Profile#returnState 115 | */ 116 | Profile.prototype.addState = function(state) { 117 | this.stack.push(["state", state]); 118 | }; 119 | 120 | /** 121 | * Add a function call to the current profile 122 | * @param {string} fn The function name 123 | * @param {array} args The array of arguments passed to the function 124 | */ 125 | Profile.prototype.addFunctionCall = function(fn, args) { 126 | this.stack.push(["functionCall", fn, args]); 127 | }; 128 | 129 | /** 130 | * Output the profile to the console in a nice, readable way. 131 | * @return {null} 132 | */ 133 | Profile.prototype.outputToConsole = function() { 134 | var prevState = [], group = 1, scope = 1; 135 | var callCount = 0, stateChanges = 0; 136 | 137 | //console.group is a synchronous so this led to some 138 | //messy state saving and changing but it works in the end 139 | console.group("Canvas snapshot"); 140 | //console.log("%cFPS: %c" + this.FPS, "color: green", "color: #000"); Fix the formula! 141 | 142 | console.group("Rendering"); 143 | this.stack.forEach(function(item, i) { 144 | switch(item[0]) { 145 | case "functionCall": 146 | callCount++; 147 | 148 | if(item[1] == "save") console.groupCollapsed("Saved Draw State #" + group), group++, scope++; 149 | console.log("%c" + item[1] + "%c(" + item[2].join(", ") + ")", "color: blue; font-style: italic", "color: #000"); 150 | if(item[1] == "restore") console.groupEnd(), scope--; 151 | break; 152 | 153 | case "state": 154 | var state = item[1]; 155 | 156 | if(!prevState.length) { 157 | console.groupCollapsed("Initial state"); 158 | for(var key in state) console.log(key + " = " + state[key]); 159 | console.groupEnd(); 160 | } else { 161 | for(var key in state) 162 | if(prevState[scope] && prevState[scope][key] !== state[key]) 163 | console.log("%c" + key + " %c= " + state[key], "color: purple; font-style: italic", "color: #000"), stateChanges++; 164 | } 165 | 166 | prevState[scope] = state; 167 | break; 168 | } 169 | }); 170 | console.groupEnd(); 171 | 172 | //Add the screenshot 173 | console.groupCollapsed("Screenshot"); 174 | console.screenshot(this.canvas, function() { 175 | console.groupEnd(); //End the screenshot group 176 | 177 | console.group("Statistics"); //Stats group 178 | console.log("Function call count: ", callCount); 179 | console.log("State change count: ", stateChanges); 180 | console.groupEnd(); //End stats group 181 | 182 | console.groupEnd(); //End the major group 183 | }); 184 | }; 185 | 186 | /** 187 | * Display an image in the console. 188 | * @param {string} url The url of the image. 189 | * @param {int} scale Scale factor on the image 190 | * @return {null} 191 | */ 192 | console.image = function(url, scale, callback) { 193 | if(typeof scale == "function") callback = scale, scale = 1; 194 | if(!scale) scale = 1; 195 | 196 | var img = new Image(); 197 | 198 | img.onload = function() { 199 | var dim = getBox(this.width * scale, this.height * scale); 200 | console.log("%c" + dim.string, dim.style + "background-image: url(" + url + "); background-size: " + (this.width * scale) + "px " + (this.height * scale) + "px; color: transparent;"); 201 | if(callback) callback(); 202 | }; 203 | 204 | img.src = url; 205 | img.style.background = "url(" + url + ")"; //Preload it again.. 206 | }; 207 | 208 | /** 209 | * Snapshot a canvas context and output it to the console. 210 | * @param {HTMLCanvasElement} canvas The canvas element 211 | * @return {null} 212 | */ 213 | console.screenshot = function(canvas, scale) { 214 | var url = canvas.toDataURL(), 215 | width = canvas.width, 216 | height = canvas.height, 217 | scale = scale || 1, 218 | dim = getBox(width * scale, height * scale); 219 | 220 | console.log("%c" + dim.string, dim.style + "background-image: url(" + url + "); background-size: " + (width * scale) + "px " + (height * scale) + "px; color: transparent;"); 221 | }; 222 | 223 | /** 224 | * Snapshot/Profile a canvas element 225 | * @param {HTMLCanvasElement} canvas The canvas element to profile 226 | * @return {null} 227 | */ 228 | console.snapshot = function(canvas) { 229 | //Start the profiling. 230 | var profile = new Profile(canvas.getContext("2d"), canvas); 231 | requestAnimationFrame(function() { 232 | profile.end(); 233 | profile.outputToConsole(); 234 | }); 235 | } 236 | })(console); 237 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | console.snapshot 5 | 78 | 79 | 80 |
81 |
82 | Github 83 |

console.snapshot( canvas )

84 |
85 |
86 |

Snapshot a canvas context and output it to the console.

87 |
88 |
89 | 90 |
91 |

Pop open the Chrome Dev tools (Ctrl/CMD + Shift + J) and click the button below to snapshot the canvas to the left and see the output within the console.

92 |
93 | 94 | 95 |
96 |
97 |
98 |

This is the brainchild of a bit of idiocy and some insightful comments on a Hacker News thread. Created by @dunxrion.

99 |
100 |
101 | 102 | 103 | 196 | --------------------------------------------------------------------------------