├── .babelrc ├── .browserslistrc ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── _demo ├── perf.html ├── perf2.html ├── style.css ├── test.html └── unpkg-test.html ├── bundle.js ├── coverage ├── clover.xml ├── coverage-final.json ├── lcov-report │ ├── base.css │ ├── block-navigation.js │ ├── createContextHistory.js.html │ ├── createObservableContext.js.html │ ├── defaults.js.html │ ├── domUtils.js.html │ ├── favicon.png │ ├── index.html │ ├── index.js.html │ ├── observeElDimensions.js.html │ ├── prettify.css │ ├── prettify.js │ ├── resizeCanvas.js.html │ ├── restoreFromHistory.js.html │ ├── setCanvasHTMLElementDimensions.js.html │ ├── sort-arrow-sprite.png │ ├── sorter.js │ ├── styles.js.html │ └── transformContextMatrix.js.html └── lcov.info ├── dist ├── cjs │ ├── index.js │ └── index.js.map ├── esm │ ├── index.js │ └── index.js.map ├── types │ ├── createContextHistory.d.ts │ ├── createObservableContext.d.ts │ ├── defaults.d.ts │ ├── domUtils.d.ts │ ├── index.d.ts │ ├── observeElDimensions.d.ts │ ├── resizeCanvas.d.ts │ ├── restoreFromHistory.d.ts │ ├── setCanvasHTMLElementDimensions.d.ts │ ├── styles.d.ts │ └── transformContextMatrix.d.ts ├── vb-canvas.min.js └── vb-canvas.min.js.map ├── jest.config.js ├── package-lock.json ├── package.json ├── readme.md ├── rollup.config.js ├── src ├── createContextHistory.js ├── createObservableContext.js ├── defaults.js ├── domUtils.js ├── index.js ├── observeElDimensions.js ├── resizeCanvas.js ├── restoreFromHistory.js ├── setCanvasHTMLElementDimensions.js ├── styles.js └── transformContextMatrix.js ├── tests └── VBCanvas.test.js ├── tsconfig.json └── vb-og.png /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 0.25% 2 | not dead -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "editor.tabSize": 2 5 | } 6 | -------------------------------------------------------------------------------- /_demo/perf.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | 20 | 21 | 22 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /_demo/perf2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 24 | 25 | 26 | 27 | 28 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /_demo/style.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georgedoescode/VBCanvas/8d1cac759cccf90b2a77ed45c70ed6ca771b0093/_demo/style.css -------------------------------------------------------------------------------- /_demo/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | VBCanvas Demo 7 | 8 | 40 | 41 | 42 |
43 | 44 | 45 | 46 | 47 |
48 | 49 | 260 | 261 | 262 | -------------------------------------------------------------------------------- /_demo/unpkg-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | VBCanvas Demo 7 | 8 | 20 | 21 | 22 |
23 | 24 | 25 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /bundle.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : 3 | typeof define === 'function' && define.amd ? define(['exports'], factory) : 4 | (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.vbcanvas = {})); 5 | }(this, (function (exports) { 'use strict'; 6 | 7 | function test() { 8 | 9 | } 10 | 11 | exports.test = test; 12 | 13 | Object.defineProperty(exports, '__esModule', { value: true }); 14 | 15 | }))); 16 | -------------------------------------------------------------------------------- /coverage/clover.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /coverage/lcov-report/base.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | margin:0; padding: 0; 3 | height: 100%; 4 | } 5 | body { 6 | font-family: Helvetica Neue, Helvetica, Arial; 7 | font-size: 14px; 8 | color:#333; 9 | } 10 | .small { font-size: 12px; } 11 | *, *:after, *:before { 12 | -webkit-box-sizing:border-box; 13 | -moz-box-sizing:border-box; 14 | box-sizing:border-box; 15 | } 16 | h1 { font-size: 20px; margin: 0;} 17 | h2 { font-size: 14px; } 18 | pre { 19 | font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; 20 | margin: 0; 21 | padding: 0; 22 | -moz-tab-size: 2; 23 | -o-tab-size: 2; 24 | tab-size: 2; 25 | } 26 | a { color:#0074D9; text-decoration:none; } 27 | a:hover { text-decoration:underline; } 28 | .strong { font-weight: bold; } 29 | .space-top1 { padding: 10px 0 0 0; } 30 | .pad2y { padding: 20px 0; } 31 | .pad1y { padding: 10px 0; } 32 | .pad2x { padding: 0 20px; } 33 | .pad2 { padding: 20px; } 34 | .pad1 { padding: 10px; } 35 | .space-left2 { padding-left:55px; } 36 | .space-right2 { padding-right:20px; } 37 | .center { text-align:center; } 38 | .clearfix { display:block; } 39 | .clearfix:after { 40 | content:''; 41 | display:block; 42 | height:0; 43 | clear:both; 44 | visibility:hidden; 45 | } 46 | .fl { float: left; } 47 | @media only screen and (max-width:640px) { 48 | .col3 { width:100%; max-width:100%; } 49 | .hide-mobile { display:none!important; } 50 | } 51 | 52 | .quiet { 53 | color: #7f7f7f; 54 | color: rgba(0,0,0,0.5); 55 | } 56 | .quiet a { opacity: 0.7; } 57 | 58 | .fraction { 59 | font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; 60 | font-size: 10px; 61 | color: #555; 62 | background: #E8E8E8; 63 | padding: 4px 5px; 64 | border-radius: 3px; 65 | vertical-align: middle; 66 | } 67 | 68 | div.path a:link, div.path a:visited { color: #333; } 69 | table.coverage { 70 | border-collapse: collapse; 71 | margin: 10px 0 0 0; 72 | padding: 0; 73 | } 74 | 75 | table.coverage td { 76 | margin: 0; 77 | padding: 0; 78 | vertical-align: top; 79 | } 80 | table.coverage td.line-count { 81 | text-align: right; 82 | padding: 0 5px 0 20px; 83 | } 84 | table.coverage td.line-coverage { 85 | text-align: right; 86 | padding-right: 10px; 87 | min-width:20px; 88 | } 89 | 90 | table.coverage td span.cline-any { 91 | display: inline-block; 92 | padding: 0 5px; 93 | width: 100%; 94 | } 95 | .missing-if-branch { 96 | display: inline-block; 97 | margin-right: 5px; 98 | border-radius: 3px; 99 | position: relative; 100 | padding: 0 4px; 101 | background: #333; 102 | color: yellow; 103 | } 104 | 105 | .skip-if-branch { 106 | display: none; 107 | margin-right: 10px; 108 | position: relative; 109 | padding: 0 4px; 110 | background: #ccc; 111 | color: white; 112 | } 113 | .missing-if-branch .typ, .skip-if-branch .typ { 114 | color: inherit !important; 115 | } 116 | .coverage-summary { 117 | border-collapse: collapse; 118 | width: 100%; 119 | } 120 | .coverage-summary tr { border-bottom: 1px solid #bbb; } 121 | .keyline-all { border: 1px solid #ddd; } 122 | .coverage-summary td, .coverage-summary th { padding: 10px; } 123 | .coverage-summary tbody { border: 1px solid #bbb; } 124 | .coverage-summary td { border-right: 1px solid #bbb; } 125 | .coverage-summary td:last-child { border-right: none; } 126 | .coverage-summary th { 127 | text-align: left; 128 | font-weight: normal; 129 | white-space: nowrap; 130 | } 131 | .coverage-summary th.file { border-right: none !important; } 132 | .coverage-summary th.pct { } 133 | .coverage-summary th.pic, 134 | .coverage-summary th.abs, 135 | .coverage-summary td.pct, 136 | .coverage-summary td.abs { text-align: right; } 137 | .coverage-summary td.file { white-space: nowrap; } 138 | .coverage-summary td.pic { min-width: 120px !important; } 139 | .coverage-summary tfoot td { } 140 | 141 | .coverage-summary .sorter { 142 | height: 10px; 143 | width: 7px; 144 | display: inline-block; 145 | margin-left: 0.5em; 146 | background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; 147 | } 148 | .coverage-summary .sorted .sorter { 149 | background-position: 0 -20px; 150 | } 151 | .coverage-summary .sorted-desc .sorter { 152 | background-position: 0 -10px; 153 | } 154 | .status-line { height: 10px; } 155 | /* yellow */ 156 | .cbranch-no { background: yellow !important; color: #111; } 157 | /* dark red */ 158 | .red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } 159 | .low .chart { border:1px solid #C21F39 } 160 | .highlighted, 161 | .highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ 162 | background: #C21F39 !important; 163 | } 164 | /* medium red */ 165 | .cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } 166 | /* light red */ 167 | .low, .cline-no { background:#FCE1E5 } 168 | /* light green */ 169 | .high, .cline-yes { background:rgb(230,245,208) } 170 | /* medium green */ 171 | .cstat-yes { background:rgb(161,215,106) } 172 | /* dark green */ 173 | .status-line.high, .high .cover-fill { background:rgb(77,146,33) } 174 | .high .chart { border:1px solid rgb(77,146,33) } 175 | /* dark yellow (gold) */ 176 | .status-line.medium, .medium .cover-fill { background: #f9cd0b; } 177 | .medium .chart { border:1px solid #f9cd0b; } 178 | /* light yellow */ 179 | .medium { background: #fff4c2; } 180 | 181 | .cstat-skip { background: #ddd; color: #111; } 182 | .fstat-skip { background: #ddd; color: #111 !important; } 183 | .cbranch-skip { background: #ddd !important; color: #111; } 184 | 185 | span.cline-neutral { background: #eaeaea; } 186 | 187 | .coverage-summary td.empty { 188 | opacity: .5; 189 | padding-top: 4px; 190 | padding-bottom: 4px; 191 | line-height: 1; 192 | color: #888; 193 | } 194 | 195 | .cover-fill, .cover-empty { 196 | display:inline-block; 197 | height: 12px; 198 | } 199 | .chart { 200 | line-height: 0; 201 | } 202 | .cover-empty { 203 | background: white; 204 | } 205 | .cover-full { 206 | border-right: none !important; 207 | } 208 | pre.prettyprint { 209 | border: none !important; 210 | padding: 0 !important; 211 | margin: 0 !important; 212 | } 213 | .com { color: #999 !important; } 214 | .ignore-none { color: #999; font-weight: normal; } 215 | 216 | .wrapper { 217 | min-height: 100%; 218 | height: auto !important; 219 | height: 100%; 220 | margin: 0 auto -48px; 221 | } 222 | .footer, .push { 223 | height: 48px; 224 | } 225 | -------------------------------------------------------------------------------- /coverage/lcov-report/block-navigation.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | var jumpToCode = (function init() { 3 | // Classes of code we would like to highlight in the file view 4 | var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; 5 | 6 | // Elements to highlight in the file listing view 7 | var fileListingElements = ['td.pct.low']; 8 | 9 | // We don't want to select elements that are direct descendants of another match 10 | var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` 11 | 12 | // Selecter that finds elements on the page to which we can jump 13 | var selector = 14 | fileListingElements.join(', ') + 15 | ', ' + 16 | notSelector + 17 | missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` 18 | 19 | // The NodeList of matching elements 20 | var missingCoverageElements = document.querySelectorAll(selector); 21 | 22 | var currentIndex; 23 | 24 | function toggleClass(index) { 25 | missingCoverageElements 26 | .item(currentIndex) 27 | .classList.remove('highlighted'); 28 | missingCoverageElements.item(index).classList.add('highlighted'); 29 | } 30 | 31 | function makeCurrent(index) { 32 | toggleClass(index); 33 | currentIndex = index; 34 | missingCoverageElements.item(index).scrollIntoView({ 35 | behavior: 'smooth', 36 | block: 'center', 37 | inline: 'center' 38 | }); 39 | } 40 | 41 | function goToPrevious() { 42 | var nextIndex = 0; 43 | if (typeof currentIndex !== 'number' || currentIndex === 0) { 44 | nextIndex = missingCoverageElements.length - 1; 45 | } else if (missingCoverageElements.length > 1) { 46 | nextIndex = currentIndex - 1; 47 | } 48 | 49 | makeCurrent(nextIndex); 50 | } 51 | 52 | function goToNext() { 53 | var nextIndex = 0; 54 | 55 | if ( 56 | typeof currentIndex === 'number' && 57 | currentIndex < missingCoverageElements.length - 1 58 | ) { 59 | nextIndex = currentIndex + 1; 60 | } 61 | 62 | makeCurrent(nextIndex); 63 | } 64 | 65 | return function jump(event) { 66 | switch (event.which) { 67 | case 78: // n 68 | case 74: // j 69 | goToNext(); 70 | break; 71 | case 66: // b 72 | case 75: // k 73 | case 80: // p 74 | goToPrevious(); 75 | break; 76 | } 77 | }; 78 | })(); 79 | window.addEventListener('keydown', jumpToCode); 80 | -------------------------------------------------------------------------------- /coverage/lcov-report/createContextHistory.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for createContextHistory.js 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files createContextHistory.js

23 |
24 | 25 |
26 | 37.5% 27 | Statements 28 | 3/8 29 |
30 | 31 | 32 |
33 | 100% 34 | Branches 35 | 0/0 36 |
37 | 38 | 39 |
40 | 20% 41 | Functions 42 | 1/5 43 |
44 | 45 | 46 |
47 | 37.5% 48 | Lines 49 | 3/8 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 |
58 |
59 |

 60 | 
1 61 | 2 62 | 3 63 | 4 64 | 5 65 | 6 66 | 7 67 | 8 68 | 9 69 | 10 70 | 11 71 | 12 72 | 13 73 | 14 74 | 15 75 | 16 76 | 17 77 | 18 78 | 19 79 | 20 80 | 21 81 | 22 82 | 23 83 | 24  84 | 4x 85 | 4x 86 |   87 | 4x 88 |   89 |   90 |   91 |   92 |   93 |   94 |   95 |   96 |   97 |   98 |   99 |   100 |   101 |   102 |   103 |   104 |   105 |   106 |  
function createContextHistory() {
107 |   const store = new Map();
108 |   let position = 0;
109 |  
110 |   return {
111 |     get entries() {
112 |       return store.values();
113 |     },
114 |     get size() {
115 |       return store.size;
116 |     },
117 |     push(type, name, args) {
118 |       store.set(position, { type, name, args });
119 |  
120 |       position++;
121 |     },
122 |     clear() {
123 |       store.clear();
124 |     },
125 |   };
126 | }
127 |  
128 | export { createContextHistory };
129 |  
130 | 131 |
132 |
133 | 138 | 139 | 140 | 145 | 146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /coverage/lcov-report/createObservableContext.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for createObservableContext.js 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files createObservableContext.js

23 |
24 | 25 |
26 | 11.11% 27 | Statements 28 | 1/9 29 |
30 | 31 | 32 |
33 | 0% 34 | Branches 35 | 0/2 36 |
37 | 38 | 39 |
40 | 25% 41 | Functions 42 | 1/4 43 |
44 | 45 | 46 |
47 | 11.11% 48 | Lines 49 | 1/9 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 |
58 |
59 |

 60 | 
1 61 | 2 62 | 3 63 | 4 64 | 5 65 | 6 66 | 7 67 | 8 68 | 9 69 | 10 70 | 11 71 | 12 72 | 13 73 | 14 74 | 15 75 | 16 76 | 17 77 | 18 78 | 19 79 | 20 80 | 21 81 | 22 82 | 23 83 | 24 84 | 25 85 | 26  86 | 4x 87 |   88 |   89 |   90 |   91 |   92 |   93 |   94 |   95 |   96 |   97 |   98 |   99 |   100 |   101 |   102 |   103 |   104 |   105 |   106 |   107 |   108 |   109 |   110 |  
function createObservableContext(baseContext, observe) {
111 |   return new Proxy(baseContext, {
112 |     get(target, name) {
113 |       if (typeof target[name] === 'function') {
114 |         return function () {
115 |           target[name].apply(target, arguments);
116 |  
117 |           observe('function', name, [...arguments]);
118 |         };
119 |       } else {
120 |         return target[name];
121 |       }
122 |     },
123 |     set(target, name, val) {
124 |       target[name] = val;
125 |  
126 |       observe('set', name, val);
127 |  
128 |       // succesful operation ✅
129 |       return true;
130 |     },
131 |   });
132 | }
133 |  
134 | export { createObservableContext };
135 |  
136 | 137 |
138 |
139 | 144 | 145 | 146 | 151 | 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /coverage/lcov-report/defaults.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for defaults.js 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files defaults.js

23 |
24 | 25 |
26 | 0% 27 | Statements 28 | 0/0 29 |
30 | 31 | 32 |
33 | 0% 34 | Branches 35 | 1/2 36 |
37 | 38 | 39 |
40 | 0% 41 | Functions 42 | 0/0 43 |
44 | 45 | 46 |
47 | 0% 48 | Lines 49 | 0/0 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 |
58 |
59 |

 60 | 
1 61 | 2 62 | 3 63 | 4 64 | 5 65 | 6 66 | 7 67 | 8 68 | 9 69 | 10  70 |   71 |   72 |   73 |   74 |   75 |   76 |   77 |   78 |  
export default {
 79 |   target: document.body,
 80 |   viewBox: [0, 0, 300, 150],
 81 |   autoAspectRatio: true,
 82 |   scaleMode: 'fit',
 83 |   resolution: window.devicePixelRatio || 1,
 84 |   static: false,
 85 |   id: Math.random(),
 86 | };
 87 |  
88 | 89 |
90 |
91 | 96 | 97 | 98 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /coverage/lcov-report/domUtils.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for domUtils.js 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files domUtils.js

23 |
24 | 25 |
26 | 100% 27 | Statements 28 | 10/10 29 |
30 | 31 | 32 |
33 | 100% 34 | Branches 35 | 2/2 36 |
37 | 38 | 39 |
40 | 100% 41 | Functions 42 | 5/5 43 |
44 | 45 | 46 |
47 | 100% 48 | Lines 49 | 10/10 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 |
58 |
59 |

 60 | 
1 61 | 2 62 | 3 63 | 4 64 | 5 65 | 6 66 | 7 67 | 8 68 | 9 69 | 10 70 | 11 71 | 12 72 | 13 73 | 14 74 | 15 75 | 16 76 | 17 77 | 18 78 | 19 79 | 20 80 | 21 81 | 22 82 | 23 83 | 24 84 | 25 85 | 26 86 | 27 87 | 28 88 | 29 89 | 30 90 | 31 91 | 32 92 | 33 93 | 34 94 | 35 95 | 36 96 | 37 97 | 38 98 | 39 99 | 40 100 | 41  101 | 4x 102 | 1x 103 |   104 | 3x 105 |   106 |   107 |   108 |   109 | 4x 110 |   111 | 4x 112 | 4x 113 |   114 | 4x 115 |   116 |   117 |   118 | 4x 119 |   120 |   121 |   122 | 4x 123 |   124 |   125 |   126 |   127 |   128 |   129 |   130 | 4x 131 |   132 |   133 |   134 |   135 |   136 |   137 |   138 |   139 |   140 |  
function resolveTarget(target) {
141 |   if (typeof target === 'string') {
142 |     return document.querySelector(target);
143 |   } else {
144 |     return target;
145 |   }
146 | }
147 |  
148 | function createCanvasHTMLElement(id) {
149 |   const el = document.createElement('canvas');
150 |  
151 |   el.classList.add('vb-canvas');
152 |   el.classList.add(id);
153 |  
154 |   return el;
155 | }
156 |  
157 | function getCanvasContext(canvasHTMLElement) {
158 |   return canvasHTMLElement.getContext('2d');
159 | }
160 |  
161 | function mountCanvasToDOM(target, el) {
162 |   target.appendChild(el);
163 | }
164 |  
165 | // https://gist.github.com/gordonbrander/2230317
166 | function randomID() {
167 |   // Math.random should be unique because of its seeding algorithm.
168 |   // Convert it to base 36 (numbers + letters), and grab the first 9 characters
169 |   // after the decimal.
170 |   return '_' + Math.random().toString(36).substr(2, 9);
171 | }
172 |  
173 | export {
174 |   resolveTarget,
175 |   createCanvasHTMLElement,
176 |   getCanvasContext,
177 |   mountCanvasToDOM,
178 |   randomID,
179 | };
180 |  
181 | 182 |
183 |
184 | 189 | 190 | 191 | 196 | 197 | 198 | 199 | 200 | -------------------------------------------------------------------------------- /coverage/lcov-report/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georgedoescode/VBCanvas/8d1cac759cccf90b2a77ed45c70ed6ca771b0093/coverage/lcov-report/favicon.png -------------------------------------------------------------------------------- /coverage/lcov-report/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for All files 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files

23 |
24 | 25 |
26 | 74% 27 | Statements 28 | 74/100 29 |
30 | 31 | 32 |
33 | 41.67% 34 | Branches 35 | 10/24 36 |
37 | 38 | 39 |
40 | 65.52% 41 | Functions 42 | 19/29 43 |
44 | 45 | 46 |
47 | 74.75% 48 | Lines 49 | 74/99 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 |
58 |
59 |
60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 |
FileStatementsBranchesFunctionsLines
createContextHistory.js 78 |
79 |
37.5%3/8100%0/020%1/537.5%3/8
createObservableContext.js 93 |
94 |
11.11%1/90%0/225%1/411.11%1/9
defaults.js 108 |
109 |
0%0/00%1/20%0/00%0/0
domUtils.js 123 |
124 |
100%10/10100%2/2100%5/5100%10/10
index.js 138 |
139 |
93.75%15/1650%1/266.67%2/393.75%15/16
observeElDimensions.js 153 |
154 |
37.5%3/80%0/450%1/237.5%3/8
resizeCanvas.js 168 |
169 |
75%3/450%1/2100%1/175%3/4
restoreFromHistory.js 183 |
184 |
0%0/40%0/20%0/10%0/4
setCanvasHTMLElementDimensions.js 198 |
199 |
90%9/1050%2/4100%2/2100%9/9
styles.js 213 |
214 |
100%11/11100%2/2100%3/3100%11/11
transformContextMatrix.js 228 |
229 |
95%19/2050%1/2100%3/395%19/20
242 |
243 |
244 |
245 | 250 | 251 | 252 | 257 | 258 | 259 | 260 | 261 | -------------------------------------------------------------------------------- /coverage/lcov-report/index.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for index.js 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files index.js

23 |
24 | 25 |
26 | 93.75% 27 | Statements 28 | 15/16 29 |
30 | 31 | 32 |
33 | 50% 34 | Branches 35 | 1/2 36 |
37 | 38 | 39 |
40 | 66.67% 41 | Functions 42 | 2/3 43 |
44 | 45 | 46 |
47 | 93.75% 48 | Lines 49 | 15/16 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 |
58 |
59 |

 60 | 
1 61 | 2 62 | 3 63 | 4 64 | 5 65 | 6 66 | 7 67 | 8 68 | 9 69 | 10 70 | 11 71 | 12 72 | 13 73 | 14 74 | 15 75 | 16 76 | 17 77 | 18 78 | 19 79 | 20 80 | 21 81 | 22 82 | 23 83 | 24 84 | 25 85 | 26 86 | 27 87 | 28 88 | 29 89 | 30 90 | 31 91 | 32 92 | 33 93 | 34 94 | 35 95 | 36 96 | 37 97 | 38 98 | 39 99 | 40 100 | 41 101 | 42 102 | 43 103 | 44 104 | 45 105 | 46 106 | 47 107 | 48 108 | 49 109 | 50 110 | 51 111 | 52 112 | 53 113 | 54 114 | 55 115 | 56 116 | 57 117 | 58 118 | 59 119 | 60  120 |   121 |   122 |   123 |   124 |   125 |   126 |   127 |   128 |   129 |   130 |   131 |   132 |   133 |   134 |   135 | 1x 136 |   137 |   138 | 4x 139 | 4x 140 |   141 | 4x 142 |   143 | 4x 144 |   145 | 4x 146 | 4x 147 |   148 | 4x 149 | 4x 150 |   151 |   152 |   153 |   154 |   155 |   156 | 4x 157 |   158 | 4x 159 | 4x 160 |   161 |   162 |   163 |   164 |   165 |   166 |   167 |   168 | 4x 169 | 4x 170 |   171 | 4x 172 |   173 |   174 |   175 |   176 |   177 |   178 |  
import DEFAULTS from './defaults';
179 |  
180 | import {
181 |   resolveTarget,
182 |   createCanvasHTMLElement,
183 |   getCanvasContext,
184 |   mountCanvasToDOM,
185 |   randomID,
186 | } from './domUtils';
187 |  
188 | import { createContextHistory } from './createContextHistory';
189 | import { createObservableContext } from './createObservableContext';
190 | import { observeElDimensions } from './observeElDimensions';
191 | import { resizeCanvas } from './resizeCanvas';
192 | import { createCanvasStyleSheet, createBaseCanvasStyles } from './styles';
193 |  
194 | createBaseCanvasStyles();
195 |  
196 | function createCanvas(opts) {
197 |   opts = Object.assign({}, DEFAULTS, opts);
198 |   opts.target = resolveTarget(opts.target);
199 |  
200 |   const canvasID = randomID();
201 |  
202 |   const history = createContextHistory();
203 |  
204 |   const canvasHTMLElement = createCanvasHTMLElement(canvasID);
205 |   const canvasStyleSheet = createCanvasStyleSheet(canvasID);
206 |  
207 |   const baseContext = getCanvasContext(canvasHTMLElement);
208 |   const observableContext = createObservableContext(
209 |     baseContext,
210 |     (type, name, args) => {
211 |       history.push(type, name, args);
212 |     }
213 |   );
214 |  
215 |   mountCanvasToDOM(opts.target, canvasHTMLElement);
216 |  
217 |   const resize = () =>
218 |     resizeCanvas({
219 |       opts,
220 |       canvasID,
221 |       canvasHTMLElement,
222 |       canvasStyleSheet,
223 |       baseContext,
224 |       history,
225 |     });
226 |  
227 |   resize();
228 |   observeElDimensions(canvasHTMLElement, resize);
229 |  
230 |   return {
231 |     el: canvasHTMLElement,
232 |     ctx: opts.static ? observableContext : baseContext,
233 |   };
234 | }
235 |  
236 | export { createCanvas };
237 |  
238 | 239 |
240 |
241 | 246 | 247 | 248 | 253 | 254 | 255 | 256 | 257 | -------------------------------------------------------------------------------- /coverage/lcov-report/observeElDimensions.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for observeElDimensions.js 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files observeElDimensions.js

23 |
24 | 25 |
26 | 37.5% 27 | Statements 28 | 3/8 29 |
30 | 31 | 32 |
33 | 0% 34 | Branches 35 | 0/4 36 |
37 | 38 | 39 |
40 | 50% 41 | Functions 42 | 1/2 43 |
44 | 45 | 46 |
47 | 37.5% 48 | Lines 49 | 3/8 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 |
58 |
59 |

 60 | 
1 61 | 2 62 | 3 63 | 4 64 | 5 65 | 6 66 | 7 67 | 8 68 | 9 69 | 10 70 | 11 71 | 12 72 | 13 73 | 14 74 | 15 75 | 16 76 | 17 77 | 18 78 | 19 79 | 20 80 | 21 81 | 22 82 | 23 83 | 24 84 | 25 85 | 26  86 |   87 |   88 |   89 | 4x 90 |   91 | 4x 92 |   93 |   94 |   95 |   96 |   97 |   98 |   99 |   100 |   101 |   102 |   103 |   104 |   105 |   106 | 4x 107 |   108 |   109 |   110 |  
import ResizeObserver from 'resize-observer-polyfill';
111 | import { debounce } from 'lodash-es';
112 |  
113 | function observeElDimensions(el, callback) {
114 |   let { width: prevWidth, height: prevHeight } = el.getBoundingClientRect();
115 |  
116 |   const resizeObserver = new ResizeObserver(
117 |     debounce(([entry]) => {
118 |       const { width, height } = entry.target.getBoundingClientRect();
119 |  
120 |       // prevent infinite resize loops if canvas CSS dimensions are not explicitely set
121 |  
122 |       if (width !== prevWidth || height !== prevHeight) {
123 |         callback(entry);
124 |  
125 |         prevWidth = width;
126 |         prevHeight = height;
127 |       }
128 |     }, 500)
129 |   );
130 |  
131 |   resizeObserver.observe(el);
132 | }
133 |  
134 | export { observeElDimensions };
135 |  
136 | 137 |
138 |
139 | 144 | 145 | 146 | 151 | 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /coverage/lcov-report/prettify.css: -------------------------------------------------------------------------------- 1 | .pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} 2 | -------------------------------------------------------------------------------- /coverage/lcov-report/prettify.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); 3 | -------------------------------------------------------------------------------- /coverage/lcov-report/resizeCanvas.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for resizeCanvas.js 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files resizeCanvas.js

23 |
24 | 25 |
26 | 75% 27 | Statements 28 | 3/4 29 |
30 | 31 | 32 |
33 | 50% 34 | Branches 35 | 1/2 36 |
37 | 38 | 39 |
40 | 100% 41 | Functions 42 | 1/1 43 |
44 | 45 | 46 |
47 | 75% 48 | Lines 49 | 3/4 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 |
58 |
59 |

 60 | 
1 61 | 2 62 | 3 63 | 4 64 | 5 65 | 6 66 | 7 67 | 8 68 | 9 69 | 10 70 | 11 71 | 12 72 | 13 73 | 14 74 | 15 75 | 16 76 | 17 77 | 18 78 | 19 79 | 20 80 | 21 81 | 22 82 | 23 83 | 24 84 | 25 85 | 26 86 | 27 87 | 28 88 | 29 89 | 30 90 | 31 91 | 32 92 | 33 93 | 34 94 | 35  95 |   96 |   97 |   98 |   99 |   100 |   101 |   102 |   103 |   104 |   105 |   106 | 4x 107 |   108 |   109 |   110 |   111 |   112 |   113 |   114 |   115 | 4x 116 |   117 |   118 |   119 |   120 |   121 |   122 | 4x 123 |   124 |   125 |   126 |   127 |   128 |  
import { setCanvasHTMLElementDimensions } from './setCanvasHTMLElementDimensions';
129 | import { transformContextMatrix } from './transformContextMatrix';
130 | import { restoreFromHistory } from './restoreFromHistory';
131 |  
132 | function resizeCanvas({
133 |   opts,
134 |   canvasID,
135 |   canvasHTMLElement,
136 |   canvasStyleSheet,
137 |   baseContext,
138 |   history,
139 | }) {
140 |   setCanvasHTMLElementDimensions({
141 |     id: canvasID,
142 |     el: canvasHTMLElement,
143 |     autoAspectRatio: opts.autoAspectRatio,
144 |     viewBox: opts.viewBox,
145 |     resolution: opts.resolution,
146 |     styleSheet: canvasStyleSheet,
147 |   });
148 |  
149 |   transformContextMatrix({
150 |     ctx: baseContext,
151 |     viewBox: opts.viewBox,
152 |     resolution: opts.resolution,
153 |     scaleMode: opts.scaleMode,
154 |   });
155 |  
156 |   Iif (opts.static) {
157 |     restoreFromHistory(baseContext, history);
158 |   }
159 | }
160 |  
161 | export { resizeCanvas };
162 |  
163 | 164 |
165 |
166 | 171 | 172 | 173 | 178 | 179 | 180 | 181 | 182 | -------------------------------------------------------------------------------- /coverage/lcov-report/restoreFromHistory.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for restoreFromHistory.js 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files restoreFromHistory.js

23 |
24 | 25 |
26 | 0% 27 | Statements 28 | 0/4 29 |
30 | 31 | 32 |
33 | 0% 34 | Branches 35 | 0/2 36 |
37 | 38 | 39 |
40 | 0% 41 | Functions 42 | 0/1 43 |
44 | 45 | 46 |
47 | 0% 48 | Lines 49 | 0/4 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 |
58 |
59 |

 60 | 
1 61 | 2 62 | 3 63 | 4 64 | 5 65 | 6 66 | 7 67 | 8 68 | 9 69 | 10 70 | 11 71 | 12  72 |   73 |   74 |   75 |   76 |   77 |   78 |   79 |   80 |   81 |   82 |  
function restoreFromHistory(ctx, history) {
 83 |   for (const entry of history.entries) {
 84 |     if (entry.type === 'function') {
 85 |       ctx[entry.name].apply(ctx, entry.args);
 86 |     } else {
 87 |       ctx[entry.name] = entry.args;
 88 |     }
 89 |   }
 90 | }
 91 |  
 92 | export { restoreFromHistory };
 93 |  
94 | 95 |
96 |
97 | 102 | 103 | 104 | 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /coverage/lcov-report/setCanvasHTMLElementDimensions.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for setCanvasHTMLElementDimensions.js 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files setCanvasHTMLElementDimensions.js

23 |
24 | 25 |
26 | 90% 27 | Statements 28 | 9/10 29 |
30 | 31 | 32 |
33 | 50% 34 | Branches 35 | 2/4 36 |
37 | 38 | 39 |
40 | 100% 41 | Functions 42 | 2/2 43 |
44 | 45 | 46 |
47 | 100% 48 | Lines 49 | 9/9 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 |
58 |
59 |

 60 | 
1 61 | 2 62 | 3 63 | 4 64 | 5 65 | 6 66 | 7 67 | 8 68 | 9 69 | 10 70 | 11 71 | 12 72 | 13 73 | 14 74 | 15 75 | 16 76 | 17 77 | 18 78 | 19 79 | 20 80 | 21 81 | 22 82 | 23 83 | 24 84 | 25 85 | 26 86 | 27 87 | 28 88 | 29 89 | 30 90 | 31 91 | 32  92 | 4x 93 | 4x 94 |   95 | 4x 96 |   97 |   98 |   99 |   100 |   101 |   102 |   103 |   104 |   105 |   106 | 4x 107 | 4x 108 |   109 | 4x 110 |   111 |   112 |   113 |   114 |   115 | 4x 116 |   117 | 4x 118 | 4x 119 |   120 |   121 |   122 |  
function calculateHeightFromAspectRatio(el, viewBox) {
123 |   const width = el.getBoundingClientRect().width;
124 |   const aspectRatioPercentage = viewBox[3] / viewBox[2];
125 |  
126 |   return width * aspectRatioPercentage + 'px';
127 | }
128 |  
129 | function setCanvasHTMLElementDimensions({
130 |   id,
131 |   el,
132 |   autoAspectRatio,
133 |   viewBox,
134 |   resolution,
135 |   styleSheet,
136 | }) {
137 |   Eif (autoAspectRatio) {
138 |     Iif (styleSheet.rules?.length) styleSheet.deleteRule(0);
139 |  
140 |     styleSheet.insertRule(
141 |       `.${id} { height: ${calculateHeightFromAspectRatio(el, viewBox)} }`,
142 |       0
143 |     );
144 |   }
145 |  
146 |   const { width, height } = el.getBoundingClientRect();
147 |  
148 |   el.width = width * resolution;
149 |   el.height = height * resolution;
150 | }
151 |  
152 | export { setCanvasHTMLElementDimensions };
153 |  
154 | 155 |
156 |
157 | 162 | 163 | 164 | 169 | 170 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /coverage/lcov-report/sort-arrow-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georgedoescode/VBCanvas/8d1cac759cccf90b2a77ed45c70ed6ca771b0093/coverage/lcov-report/sort-arrow-sprite.png -------------------------------------------------------------------------------- /coverage/lcov-report/sorter.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | var addSorting = (function() { 3 | 'use strict'; 4 | var cols, 5 | currentSort = { 6 | index: 0, 7 | desc: false 8 | }; 9 | 10 | // returns the summary table element 11 | function getTable() { 12 | return document.querySelector('.coverage-summary'); 13 | } 14 | // returns the thead element of the summary table 15 | function getTableHeader() { 16 | return getTable().querySelector('thead tr'); 17 | } 18 | // returns the tbody element of the summary table 19 | function getTableBody() { 20 | return getTable().querySelector('tbody'); 21 | } 22 | // returns the th element for nth column 23 | function getNthColumn(n) { 24 | return getTableHeader().querySelectorAll('th')[n]; 25 | } 26 | 27 | // loads all columns 28 | function loadColumns() { 29 | var colNodes = getTableHeader().querySelectorAll('th'), 30 | colNode, 31 | cols = [], 32 | col, 33 | i; 34 | 35 | for (i = 0; i < colNodes.length; i += 1) { 36 | colNode = colNodes[i]; 37 | col = { 38 | key: colNode.getAttribute('data-col'), 39 | sortable: !colNode.getAttribute('data-nosort'), 40 | type: colNode.getAttribute('data-type') || 'string' 41 | }; 42 | cols.push(col); 43 | if (col.sortable) { 44 | col.defaultDescSort = col.type === 'number'; 45 | colNode.innerHTML = 46 | colNode.innerHTML + ''; 47 | } 48 | } 49 | return cols; 50 | } 51 | // attaches a data attribute to every tr element with an object 52 | // of data values keyed by column name 53 | function loadRowData(tableRow) { 54 | var tableCols = tableRow.querySelectorAll('td'), 55 | colNode, 56 | col, 57 | data = {}, 58 | i, 59 | val; 60 | for (i = 0; i < tableCols.length; i += 1) { 61 | colNode = tableCols[i]; 62 | col = cols[i]; 63 | val = colNode.getAttribute('data-value'); 64 | if (col.type === 'number') { 65 | val = Number(val); 66 | } 67 | data[col.key] = val; 68 | } 69 | return data; 70 | } 71 | // loads all row data 72 | function loadData() { 73 | var rows = getTableBody().querySelectorAll('tr'), 74 | i; 75 | 76 | for (i = 0; i < rows.length; i += 1) { 77 | rows[i].data = loadRowData(rows[i]); 78 | } 79 | } 80 | // sorts the table using the data for the ith column 81 | function sortByIndex(index, desc) { 82 | var key = cols[index].key, 83 | sorter = function(a, b) { 84 | a = a.data[key]; 85 | b = b.data[key]; 86 | return a < b ? -1 : a > b ? 1 : 0; 87 | }, 88 | finalSorter = sorter, 89 | tableBody = document.querySelector('.coverage-summary tbody'), 90 | rowNodes = tableBody.querySelectorAll('tr'), 91 | rows = [], 92 | i; 93 | 94 | if (desc) { 95 | finalSorter = function(a, b) { 96 | return -1 * sorter(a, b); 97 | }; 98 | } 99 | 100 | for (i = 0; i < rowNodes.length; i += 1) { 101 | rows.push(rowNodes[i]); 102 | tableBody.removeChild(rowNodes[i]); 103 | } 104 | 105 | rows.sort(finalSorter); 106 | 107 | for (i = 0; i < rows.length; i += 1) { 108 | tableBody.appendChild(rows[i]); 109 | } 110 | } 111 | // removes sort indicators for current column being sorted 112 | function removeSortIndicators() { 113 | var col = getNthColumn(currentSort.index), 114 | cls = col.className; 115 | 116 | cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); 117 | col.className = cls; 118 | } 119 | // adds sort indicators for current column being sorted 120 | function addSortIndicators() { 121 | getNthColumn(currentSort.index).className += currentSort.desc 122 | ? ' sorted-desc' 123 | : ' sorted'; 124 | } 125 | // adds event listeners for all sorter widgets 126 | function enableUI() { 127 | var i, 128 | el, 129 | ithSorter = function ithSorter(i) { 130 | var col = cols[i]; 131 | 132 | return function() { 133 | var desc = col.defaultDescSort; 134 | 135 | if (currentSort.index === i) { 136 | desc = !currentSort.desc; 137 | } 138 | sortByIndex(i, desc); 139 | removeSortIndicators(); 140 | currentSort.index = i; 141 | currentSort.desc = desc; 142 | addSortIndicators(); 143 | }; 144 | }; 145 | for (i = 0; i < cols.length; i += 1) { 146 | if (cols[i].sortable) { 147 | // add the click event handler on the th so users 148 | // dont have to click on those tiny arrows 149 | el = getNthColumn(i).querySelector('.sorter').parentElement; 150 | if (el.addEventListener) { 151 | el.addEventListener('click', ithSorter(i)); 152 | } else { 153 | el.attachEvent('onclick', ithSorter(i)); 154 | } 155 | } 156 | } 157 | } 158 | // adds sorting functionality to the UI 159 | return function() { 160 | if (!getTable()) { 161 | return; 162 | } 163 | cols = loadColumns(); 164 | loadData(); 165 | addSortIndicators(); 166 | enableUI(); 167 | }; 168 | })(); 169 | 170 | window.addEventListener('load', addSorting); 171 | -------------------------------------------------------------------------------- /coverage/lcov-report/styles.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for styles.js 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files styles.js

23 |
24 | 25 |
26 | 100% 27 | Statements 28 | 11/11 29 |
30 | 31 | 32 |
33 | 100% 34 | Branches 35 | 2/2 36 |
37 | 38 | 39 |
40 | 100% 41 | Functions 42 | 3/3 43 |
44 | 45 | 46 |
47 | 100% 48 | Lines 49 | 11/11 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 |
58 |
59 |

 60 | 
1 61 | 2 62 | 3 63 | 4 64 | 5 65 | 6 66 | 7 67 | 8 68 | 9 69 | 10 70 | 11 71 | 12 72 | 13 73 | 14 74 | 15 75 | 16 76 | 17 77 | 18 78 | 19 79 | 20 80 | 21 81 | 22 82 | 23 83 | 24 84 | 25 85 | 26 86 | 27 87 | 28 88 | 29 89 | 30 90 | 31  91 | 4x 92 |   93 |   94 |   95 | 1x 96 | 1x 97 |   98 | 1x 99 |   100 | 1x 101 |   102 | 1x 103 |   104 |   105 |   106 |   107 |   108 |   109 | 4x 110 | 4x 111 |   112 | 4x 113 |   114 | 4x 115 |   116 | 4x 117 |   118 |   119 |   120 |  
function insertAfter(newNode, referenceNode) {
121 |   referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
122 | }
123 |  
124 | function createBaseCanvasStyles() {
125 |   const baseStyleSheet = document.createElement('style');
126 |   const target = document.styleSheets[0]?.ownerNode || document.head.firstChild;
127 |  
128 |   baseStyleSheet.id = 'vb-canvas-base-styles';
129 |  
130 |   document.head.insertBefore(baseStyleSheet, target);
131 |  
132 |   baseStyleSheet.sheet.insertRule(
133 |     '.vb-canvas { width: 100%; max-width: 100%; }',
134 |     0
135 |   );
136 | }
137 |  
138 | function createCanvasStyleSheet(id) {
139 |   const canvasStyleSheet = document.createElement('style');
140 |   const target = document.getElementById('vb-canvas-base-styles');
141 |  
142 |   canvasStyleSheet.setAttribute('data-canvas-id', id);
143 |  
144 |   insertAfter(canvasStyleSheet, target);
145 |  
146 |   return canvasStyleSheet.sheet;
147 | }
148 |  
149 | export { createCanvasStyleSheet, createBaseCanvasStyles };
150 |  
151 | 152 |
153 |
154 | 159 | 160 | 161 | 166 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /coverage/lcov-report/transformContextMatrix.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for transformContextMatrix.js 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files transformContextMatrix.js

23 |
24 | 25 |
26 | 95% 27 | Statements 28 | 19/20 29 |
30 | 31 | 32 |
33 | 50% 34 | Branches 35 | 1/2 36 |
37 | 38 | 39 |
40 | 100% 41 | Functions 42 | 3/3 43 |
44 | 45 | 46 |
47 | 95% 48 | Lines 49 | 19/20 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 |
58 |
59 |

 60 | 
1 61 | 2 62 | 3 63 | 4 64 | 5 65 | 6 66 | 7 67 | 8 68 | 9 69 | 10 70 | 11 71 | 12 72 | 13 73 | 14 74 | 15 75 | 16 76 | 17 77 | 18 78 | 19 79 | 20 80 | 21 81 | 22 82 | 23 83 | 24 84 | 25 85 | 26 86 | 27 87 | 28 88 | 29 89 | 30 90 | 31 91 | 32 92 | 33 93 | 34 94 | 35 95 | 36 96 | 37 97 | 38 98 | 39 99 | 40 100 | 41 101 | 42 102 | 43 103 | 44 104 | 45 105 | 46 106 | 47 107 | 48 108 | 49 109 | 50 110 | 51 111 | 52 112 | 53 113 | 54 114 | 55 115 | 56 116 | 57 117 | 58 118 | 59 119 | 60 120 | 61 121 | 62  122 |   123 |   124 |   125 |   126 |   127 |   128 |   129 |   130 | 4x 131 | 4x 132 |   133 |   134 |   135 |   136 | 4x 137 |   138 |   139 |   140 |   141 |   142 |   143 |   144 | 4x 145 | 4x 146 | 4x 147 | 4x 148 |   149 |   150 |   151 | 4x 152 | 4x 153 |   154 |   155 |   156 |   157 | 4x 158 |   159 | 4x 160 | 4x 161 |   162 | 4x 163 |   164 |   165 |   166 |   167 |   168 |   169 |   170 | 4x 171 | 4x 172 |   173 | 4x 174 | 4x 175 |   176 | 4x 177 |   178 | 4x 179 |   180 |   181 |   182 |  
function calculateAspectRatio(
183 |   srcWidth,
184 |   srcHeight,
185 |   maxWidth,
186 |   maxHeight,
187 |   scaleMode
188 | ) {
189 |   let ratio;
190 |  
191 |   Eif (scaleMode === 'fit') {
192 |     ratio = Math.min(maxWidth / srcWidth, maxHeight / srcHeight);
193 |   } else {
194 |     ratio = Math.max(maxWidth / srcWidth, maxHeight / srcHeight);
195 |   }
196 |  
197 |   return {
198 |     fitWidth: srcWidth * ratio,
199 |     fitHeight: srcHeight * ratio,
200 |     ratio: ratio,
201 |   };
202 | }
203 |  
204 | function clipCtx(ctx, viewBoxWidth, viewBoxHeight) {
205 |   ctx.beginPath(viewBoxWidth, viewBoxHeight);
206 |   ctx.rect(0, 0, viewBoxWidth, viewBoxHeight);
207 |   ctx.clip();
208 |   ctx.closePath();
209 | }
210 |  
211 | function transformContextMatrix({ ctx, viewBox, resolution, scaleMode }) {
212 |   const viewBoxWidth = viewBox[2];
213 |   const viewBoxHeight = viewBox[3];
214 |  
215 |   let {
216 |     width: canvasWidth,
217 |     height: canvasHeight,
218 |   } = ctx.canvas.getBoundingClientRect();
219 |  
220 |   canvasWidth *= resolution;
221 |   canvasHeight *= resolution;
222 |  
223 |   const { fitWidth, fitHeight, ratio } = calculateAspectRatio(
224 |     viewBoxWidth,
225 |     viewBoxHeight,
226 |     canvasWidth,
227 |     canvasHeight,
228 |     scaleMode
229 |   );
230 |  
231 |   const scaleX = fitWidth / viewBoxWidth;
232 |   const scaleY = fitHeight / viewBoxHeight;
233 |  
234 |   const translateX = -viewBox[0] * ratio + (canvasWidth - fitWidth) / 2;
235 |   const translateY = -viewBox[1] * ratio + (canvasHeight - fitHeight) / 2;
236 |  
237 |   ctx.setTransform(scaleX, 0, 0, scaleY, translateX, translateY);
238 |  
239 |   clipCtx(ctx, viewBoxWidth, viewBoxHeight);
240 | }
241 |  
242 | export { transformContextMatrix };
243 |  
244 | 245 |
246 |
247 | 252 | 253 | 254 | 259 | 260 | 261 | 262 | 263 | -------------------------------------------------------------------------------- /coverage/lcov.info: -------------------------------------------------------------------------------- 1 | TN: 2 | SF:src/createContextHistory.js 3 | FN:1,createContextHistory 4 | FN:6,(anonymous_1) 5 | FN:9,(anonymous_2) 6 | FN:12,(anonymous_3) 7 | FN:17,(anonymous_4) 8 | FNF:5 9 | FNH:1 10 | FNDA:4,createContextHistory 11 | FNDA:0,(anonymous_1) 12 | FNDA:0,(anonymous_2) 13 | FNDA:0,(anonymous_3) 14 | FNDA:0,(anonymous_4) 15 | DA:2,4 16 | DA:3,4 17 | DA:5,4 18 | DA:7,0 19 | DA:10,0 20 | DA:13,0 21 | DA:15,0 22 | DA:18,0 23 | LF:8 24 | LH:3 25 | BRF:0 26 | BRH:0 27 | end_of_record 28 | TN: 29 | SF:src/createObservableContext.js 30 | FN:1,createObservableContext 31 | FN:3,(anonymous_1) 32 | FN:5,(anonymous_2) 33 | FN:14,(anonymous_3) 34 | FNF:4 35 | FNH:1 36 | FNDA:4,createObservableContext 37 | FNDA:0,(anonymous_1) 38 | FNDA:0,(anonymous_2) 39 | FNDA:0,(anonymous_3) 40 | DA:2,4 41 | DA:4,0 42 | DA:5,0 43 | DA:6,0 44 | DA:8,0 45 | DA:11,0 46 | DA:15,0 47 | DA:17,0 48 | DA:20,0 49 | LF:9 50 | LH:1 51 | BRDA:4,0,0,0 52 | BRDA:4,0,1,0 53 | BRF:2 54 | BRH:0 55 | end_of_record 56 | TN: 57 | SF:src/defaults.js 58 | FNF:0 59 | FNH:0 60 | LF:0 61 | LH:0 62 | BRDA:6,0,0,1 63 | BRDA:6,0,1,0 64 | BRF:2 65 | BRH:1 66 | end_of_record 67 | TN: 68 | SF:src/domUtils.js 69 | FN:1,resolveTarget 70 | FN:9,createCanvasHTMLElement 71 | FN:18,getCanvasContext 72 | FN:22,mountCanvasToDOM 73 | FN:27,randomID 74 | FNF:5 75 | FNH:5 76 | FNDA:4,resolveTarget 77 | FNDA:4,createCanvasHTMLElement 78 | FNDA:4,getCanvasContext 79 | FNDA:4,mountCanvasToDOM 80 | FNDA:4,randomID 81 | DA:2,4 82 | DA:3,1 83 | DA:5,3 84 | DA:10,4 85 | DA:12,4 86 | DA:13,4 87 | DA:15,4 88 | DA:19,4 89 | DA:23,4 90 | DA:31,4 91 | LF:10 92 | LH:10 93 | BRDA:2,0,0,1 94 | BRDA:2,0,1,3 95 | BRF:2 96 | BRH:2 97 | end_of_record 98 | TN: 99 | SF:src/index.js 100 | FN:19,createCanvas 101 | FN:33,(anonymous_1) 102 | FN:40,(anonymous_2) 103 | FNF:3 104 | FNH:2 105 | FNDA:4,createCanvas 106 | FNDA:0,(anonymous_1) 107 | FNDA:4,(anonymous_2) 108 | DA:17,1 109 | DA:20,4 110 | DA:21,4 111 | DA:23,4 112 | DA:25,4 113 | DA:27,4 114 | DA:28,4 115 | DA:30,4 116 | DA:31,4 117 | DA:34,0 118 | DA:38,4 119 | DA:40,4 120 | DA:41,4 121 | DA:50,4 122 | DA:51,4 123 | DA:53,4 124 | LF:16 125 | LH:15 126 | BRDA:55,0,0,0 127 | BRDA:55,0,1,4 128 | BRF:2 129 | BRH:1 130 | end_of_record 131 | TN: 132 | SF:src/observeElDimensions.js 133 | FN:4,observeElDimensions 134 | FN:8,(anonymous_1) 135 | FNF:2 136 | FNH:1 137 | FNDA:4,observeElDimensions 138 | FNDA:0,(anonymous_1) 139 | DA:5,4 140 | DA:7,4 141 | DA:9,0 142 | DA:13,0 143 | DA:14,0 144 | DA:16,0 145 | DA:17,0 146 | DA:22,4 147 | LF:8 148 | LH:3 149 | BRDA:13,0,0,0 150 | BRDA:13,0,1,0 151 | BRDA:13,1,0,0 152 | BRDA:13,1,1,0 153 | BRF:4 154 | BRH:0 155 | end_of_record 156 | TN: 157 | SF:src/resizeCanvas.js 158 | FN:5,resizeCanvas 159 | FNF:1 160 | FNH:1 161 | FNDA:4,resizeCanvas 162 | DA:13,4 163 | DA:22,4 164 | DA:29,4 165 | DA:30,0 166 | LF:4 167 | LH:3 168 | BRDA:29,0,0,0 169 | BRDA:29,0,1,4 170 | BRF:2 171 | BRH:1 172 | end_of_record 173 | TN: 174 | SF:src/restoreFromHistory.js 175 | FN:1,restoreFromHistory 176 | FNF:1 177 | FNH:0 178 | FNDA:0,restoreFromHistory 179 | DA:2,0 180 | DA:3,0 181 | DA:4,0 182 | DA:6,0 183 | LF:4 184 | LH:0 185 | BRDA:3,0,0,0 186 | BRDA:3,0,1,0 187 | BRF:2 188 | BRH:0 189 | end_of_record 190 | TN: 191 | SF:src/setCanvasHTMLElementDimensions.js 192 | FN:1,calculateHeightFromAspectRatio 193 | FN:8,setCanvasHTMLElementDimensions 194 | FNF:2 195 | FNH:2 196 | FNDA:4,calculateHeightFromAspectRatio 197 | FNDA:4,setCanvasHTMLElementDimensions 198 | DA:2,4 199 | DA:3,4 200 | DA:5,4 201 | DA:16,4 202 | DA:17,4 203 | DA:19,4 204 | DA:25,4 205 | DA:27,4 206 | DA:28,4 207 | LF:9 208 | LH:9 209 | BRDA:16,0,0,4 210 | BRDA:16,0,1,0 211 | BRDA:17,1,0,0 212 | BRDA:17,1,1,4 213 | BRF:4 214 | BRH:2 215 | end_of_record 216 | TN: 217 | SF:src/styles.js 218 | FN:1,insertAfter 219 | FN:5,createBaseCanvasStyles 220 | FN:19,createCanvasStyleSheet 221 | FNF:3 222 | FNH:3 223 | FNDA:4,insertAfter 224 | FNDA:1,createBaseCanvasStyles 225 | FNDA:4,createCanvasStyleSheet 226 | DA:2,4 227 | DA:6,1 228 | DA:7,1 229 | DA:9,1 230 | DA:11,1 231 | DA:13,1 232 | DA:20,4 233 | DA:21,4 234 | DA:23,4 235 | DA:25,4 236 | DA:27,4 237 | LF:11 238 | LH:11 239 | BRDA:7,0,0,1 240 | BRDA:7,0,1,1 241 | BRF:2 242 | BRH:2 243 | end_of_record 244 | TN: 245 | SF:src/transformContextMatrix.js 246 | FN:1,calculateAspectRatio 247 | FN:23,clipCtx 248 | FN:30,transformContextMatrix 249 | FNF:3 250 | FNH:3 251 | FNDA:4,calculateAspectRatio 252 | FNDA:4,clipCtx 253 | FNDA:4,transformContextMatrix 254 | DA:10,4 255 | DA:11,4 256 | DA:13,0 257 | DA:16,4 258 | DA:24,4 259 | DA:25,4 260 | DA:26,4 261 | DA:27,4 262 | DA:31,4 263 | DA:32,4 264 | DA:37,4 265 | DA:39,4 266 | DA:40,4 267 | DA:42,4 268 | DA:50,4 269 | DA:51,4 270 | DA:53,4 271 | DA:54,4 272 | DA:56,4 273 | DA:58,4 274 | LF:20 275 | LH:19 276 | BRDA:10,0,0,4 277 | BRDA:10,0,1,0 278 | BRF:2 279 | BRH:1 280 | end_of_record 281 | -------------------------------------------------------------------------------- /dist/cjs/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { value: true }); 4 | 5 | var ResizeObserver = require('resize-observer-polyfill'); 6 | var lodashEs = require('lodash-es'); 7 | 8 | function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } 9 | 10 | var ResizeObserver__default = /*#__PURE__*/_interopDefaultLegacy(ResizeObserver); 11 | 12 | var DEFAULTS = { 13 | target: document.body, 14 | viewBox: [0, 0, 300, 150], 15 | autoAspectRatio: true, 16 | scaleMode: 'fit', 17 | resolution: window.devicePixelRatio || 1, 18 | static: false, 19 | id: Math.random(), 20 | }; 21 | 22 | function resolveTarget(target) { 23 | if (typeof target === 'string') { 24 | return document.querySelector(target); 25 | } else { 26 | return target; 27 | } 28 | } 29 | 30 | function createCanvasHTMLElement(id) { 31 | const el = document.createElement('canvas'); 32 | 33 | el.classList.add('vb-canvas'); 34 | el.classList.add(id); 35 | 36 | return el; 37 | } 38 | 39 | /** @param {HTMLCanvasElement} canvasHTMLElement */ 40 | function getCanvasContext(canvasHTMLElement) { 41 | return canvasHTMLElement.getContext('2d'); 42 | } 43 | 44 | /** 45 | * @param {HTMLElement} target 46 | * @param {HTMLCanvasElement} el 47 | */ 48 | function mountCanvasToDOM(target, el) { 49 | target.appendChild(el); 50 | } 51 | 52 | // https://gist.github.com/gordonbrander/2230317 53 | function randomID() { 54 | // Math.random should be unique because of its seeding algorithm. 55 | // Convert it to base 36 (numbers + letters), and grab the first 9 characters 56 | // after the decimal. 57 | return '_' + Math.random().toString(36).substr(2, 9); 58 | } 59 | 60 | function createContextHistory() { 61 | const store = new Map(); 62 | let position = 0; 63 | 64 | return { 65 | get entries() { 66 | return store.values(); 67 | }, 68 | get size() { 69 | return store.size; 70 | }, 71 | /** 72 | * @param {"function" | "set"} type 73 | * @param {string | number | symbol} name 74 | * @param {unknown} args 75 | */ 76 | push(type, name, args) { 77 | store.set(position, { type, name, args }); 78 | 79 | position++; 80 | }, 81 | clear() { 82 | store.clear(); 83 | }, 84 | }; 85 | } 86 | 87 | /** 88 | * @param {CanvasRenderingContext2D} baseContext 89 | * @param {ReturnType['push']} observe 90 | */ 91 | function createObservableContext(baseContext, observe) { 92 | return new Proxy(baseContext, { 93 | get(target, name) { 94 | if (typeof target[name] === 'function') { 95 | return function () { 96 | observe('function', name, [...arguments]); 97 | 98 | return target[name].apply(target, arguments); 99 | }; 100 | } else { 101 | return target[name]; 102 | } 103 | }, 104 | set(target, name, val) { 105 | target[name] = val; 106 | 107 | observe('set', name, val); 108 | 109 | // succesful operation ✅ 110 | return true; 111 | }, 112 | }); 113 | } 114 | 115 | function observeElDimensions(el, callback) { 116 | let { width: prevWidth, height: prevHeight } = el.getBoundingClientRect(); 117 | 118 | const resizeObserver = new ResizeObserver__default['default']( 119 | lodashEs.debounce(([entry]) => { 120 | const { width, height } = entry.target.getBoundingClientRect(); 121 | 122 | // prevent infinite resize loops if canvas CSS dimensions are not explicitely set 123 | if (~~width !== ~~prevWidth || ~~height !== ~~prevHeight) { 124 | callback(entry); 125 | 126 | prevWidth = width; 127 | prevHeight = height; 128 | } 129 | }, 500) 130 | ); 131 | 132 | resizeObserver.observe(el); 133 | } 134 | 135 | function calculateHeightFromAspectRatio(el, viewBox) { 136 | const width = el.getBoundingClientRect().width; 137 | const aspectRatioPercentage = viewBox[3] / viewBox[2]; 138 | 139 | return width * aspectRatioPercentage + 'px'; 140 | } 141 | 142 | function setCanvasHTMLElementDimensions({ 143 | id, 144 | el, 145 | autoAspectRatio, 146 | viewBox, 147 | resolution, 148 | styleSheet, 149 | }) { 150 | if (autoAspectRatio) { 151 | if (styleSheet.rules?.length) styleSheet.deleteRule(0); 152 | 153 | styleSheet.insertRule( 154 | `.${id} { height: ${calculateHeightFromAspectRatio(el, viewBox)} }`, 155 | 0 156 | ); 157 | } 158 | 159 | const { width, height } = el.getBoundingClientRect(); 160 | 161 | el.width = width * resolution; 162 | el.height = height * resolution; 163 | } 164 | 165 | function calculateAspectRatio( 166 | srcWidth, 167 | srcHeight, 168 | maxWidth, 169 | maxHeight, 170 | scaleMode 171 | ) { 172 | let ratio; 173 | 174 | if (scaleMode === 'fit') { 175 | ratio = Math.min(maxWidth / srcWidth, maxHeight / srcHeight); 176 | } else { 177 | ratio = Math.max(maxWidth / srcWidth, maxHeight / srcHeight); 178 | } 179 | 180 | return { 181 | fitWidth: srcWidth * ratio, 182 | fitHeight: srcHeight * ratio, 183 | ratio: ratio, 184 | }; 185 | } 186 | 187 | function clipCtx(ctx, viewBoxWidth, viewBoxHeight) { 188 | ctx.beginPath(viewBoxWidth, viewBoxHeight); 189 | ctx.rect(0, 0, viewBoxWidth, viewBoxHeight); 190 | ctx.clip(); 191 | ctx.closePath(); 192 | } 193 | 194 | function transformContextMatrix({ ctx, viewBox, resolution, scaleMode }) { 195 | const viewBoxWidth = viewBox[2]; 196 | const viewBoxHeight = viewBox[3]; 197 | 198 | let { 199 | width: canvasWidth, 200 | height: canvasHeight, 201 | } = ctx.canvas.getBoundingClientRect(); 202 | 203 | canvasWidth *= resolution; 204 | canvasHeight *= resolution; 205 | 206 | const { fitWidth, fitHeight, ratio } = calculateAspectRatio( 207 | viewBoxWidth, 208 | viewBoxHeight, 209 | canvasWidth, 210 | canvasHeight, 211 | scaleMode 212 | ); 213 | 214 | const scaleX = fitWidth / viewBoxWidth; 215 | const scaleY = fitHeight / viewBoxHeight; 216 | 217 | const translateX = -viewBox[0] * ratio + (canvasWidth - fitWidth) / 2; 218 | const translateY = -viewBox[1] * ratio + (canvasHeight - fitHeight) / 2; 219 | 220 | ctx.setTransform(scaleX, 0, 0, scaleY, translateX, translateY); 221 | 222 | clipCtx(ctx, viewBoxWidth, viewBoxHeight); 223 | } 224 | 225 | function restoreFromHistory(ctx, history) { 226 | for (const entry of history.entries) { 227 | if (entry.type === 'function') { 228 | ctx[entry.name].apply(ctx, entry.args); 229 | } else { 230 | ctx[entry.name] = entry.args; 231 | } 232 | } 233 | } 234 | 235 | /** 236 | * @typedef ResizeCanvasOptions 237 | * @property {import(".").CreateCanvasOptions} opts 238 | * @property {string} canvasID 239 | * @property {HTMLCanvasElement} canvasHTMLElement 240 | * @property {CSSStyleSheet} canvasStyleSheet 241 | * @property {CanvasRenderingContext2D} baseContext 242 | * @property {ReturnType} history 243 | */ 244 | 245 | /** 246 | * @param {ResizeCanvasOptions} options 247 | */ 248 | function resizeCanvas({ 249 | opts, 250 | canvasID, 251 | canvasHTMLElement, 252 | canvasStyleSheet, 253 | baseContext, 254 | history, 255 | }) { 256 | setCanvasHTMLElementDimensions({ 257 | id: canvasID, 258 | el: canvasHTMLElement, 259 | autoAspectRatio: opts.autoAspectRatio, 260 | viewBox: opts.viewBox, 261 | resolution: opts.resolution, 262 | styleSheet: canvasStyleSheet, 263 | }); 264 | 265 | transformContextMatrix({ 266 | ctx: baseContext, 267 | viewBox: opts.viewBox, 268 | resolution: opts.resolution, 269 | scaleMode: opts.scaleMode, 270 | }); 271 | 272 | if (opts.static) { 273 | restoreFromHistory(baseContext, history); 274 | } 275 | } 276 | 277 | function insertAfter(newNode, referenceNode) { 278 | referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); 279 | } 280 | 281 | function createBaseCanvasStyles() { 282 | const baseStyleSheet = document.createElement('style'); 283 | const target = document.styleSheets[0]?.ownerNode || document.head.firstChild; 284 | 285 | baseStyleSheet.id = 'vb-canvas-base-styles'; 286 | 287 | document.head.insertBefore(baseStyleSheet, target); 288 | 289 | baseStyleSheet.sheet.insertRule( 290 | '.vb-canvas { width: 100%; max-width: 100%; }', 291 | 0 292 | ); 293 | } 294 | 295 | function createCanvasStyleSheet(id) { 296 | const canvasStyleSheet = document.createElement('style'); 297 | const target = document.getElementById('vb-canvas-base-styles'); 298 | 299 | canvasStyleSheet.setAttribute('data-canvas-id', id); 300 | 301 | insertAfter(canvasStyleSheet, target); 302 | 303 | return canvasStyleSheet.sheet; 304 | } 305 | 306 | createBaseCanvasStyles(); 307 | 308 | /** 309 | * @typedef CreateCanvasOptions 310 | * @property {HTMLElement} [target=document.body] Where to add the `` element in the DOM. 311 | * @property {[x: number, y: number, w: number, h: number]} [viewBox=[0, 0, 200, 200]] Canvas viewbox (x, y, w, h). Mirrors SVG Behaviour. 312 | * @property {boolean} [autoAspectRatio=true] Match DOM dimensions to `viewBox`. When true, `` elements behave in a similar way to ``. 313 | * @property {"fit" | "fill"} [scaleMode='fit'] VBCanvas's version of `preserveAspectRatio`. Accepts `fit` or `fill`. `fit` is equal to SVG'sxMidYMid meet. `fill` is equal to SVG'sxMidYMid slice. 314 | * @property {number} [resolution=window.devicePixelRatio] Pixel density of the ``. 315 | * @property {boolean} [static=false] Retains canvas drawing on resize, without the need of an animation loop. 316 | */ 317 | 318 | /** 319 | * @param {CreateCanvasOptions} opts 320 | */ 321 | function createCanvas(opts) { 322 | opts = Object.assign({}, DEFAULTS, opts); 323 | opts.target = resolveTarget(opts.target); 324 | 325 | const canvasID = randomID(); 326 | 327 | const history = createContextHistory(); 328 | 329 | const canvasHTMLElement = createCanvasHTMLElement(canvasID); 330 | const canvasStyleSheet = createCanvasStyleSheet(canvasID); 331 | 332 | const baseContext = getCanvasContext(canvasHTMLElement); 333 | const observableContext = createObservableContext( 334 | baseContext, 335 | (type, name, args) => { 336 | history.push(type, name, args); 337 | } 338 | ); 339 | 340 | mountCanvasToDOM(opts.target, canvasHTMLElement); 341 | 342 | const resize = () => 343 | resizeCanvas({ 344 | opts, 345 | canvasID, 346 | canvasHTMLElement, 347 | canvasStyleSheet, 348 | baseContext, 349 | history, 350 | }); 351 | 352 | resize(); 353 | observeElDimensions(canvasHTMLElement, resize); 354 | 355 | return { 356 | el: canvasHTMLElement, 357 | ctx: opts.static ? observableContext : baseContext, 358 | }; 359 | } 360 | 361 | exports.createCanvas = createCanvas; 362 | //# sourceMappingURL=index.js.map 363 | -------------------------------------------------------------------------------- /dist/esm/index.js: -------------------------------------------------------------------------------- 1 | import ResizeObserver from 'resize-observer-polyfill'; 2 | import { debounce } from 'lodash-es'; 3 | 4 | var DEFAULTS = { 5 | target: document.body, 6 | viewBox: [0, 0, 300, 150], 7 | autoAspectRatio: true, 8 | scaleMode: 'fit', 9 | resolution: window.devicePixelRatio || 1, 10 | static: false, 11 | id: Math.random(), 12 | }; 13 | 14 | function resolveTarget(target) { 15 | if (typeof target === 'string') { 16 | return document.querySelector(target); 17 | } else { 18 | return target; 19 | } 20 | } 21 | 22 | function createCanvasHTMLElement(id) { 23 | const el = document.createElement('canvas'); 24 | 25 | el.classList.add('vb-canvas'); 26 | el.classList.add(id); 27 | 28 | return el; 29 | } 30 | 31 | /** @param {HTMLCanvasElement} canvasHTMLElement */ 32 | function getCanvasContext(canvasHTMLElement) { 33 | return canvasHTMLElement.getContext('2d'); 34 | } 35 | 36 | /** 37 | * @param {HTMLElement} target 38 | * @param {HTMLCanvasElement} el 39 | */ 40 | function mountCanvasToDOM(target, el) { 41 | target.appendChild(el); 42 | } 43 | 44 | // https://gist.github.com/gordonbrander/2230317 45 | function randomID() { 46 | // Math.random should be unique because of its seeding algorithm. 47 | // Convert it to base 36 (numbers + letters), and grab the first 9 characters 48 | // after the decimal. 49 | return '_' + Math.random().toString(36).substr(2, 9); 50 | } 51 | 52 | function createContextHistory() { 53 | const store = new Map(); 54 | let position = 0; 55 | 56 | return { 57 | get entries() { 58 | return store.values(); 59 | }, 60 | get size() { 61 | return store.size; 62 | }, 63 | /** 64 | * @param {"function" | "set"} type 65 | * @param {string | number | symbol} name 66 | * @param {unknown} args 67 | */ 68 | push(type, name, args) { 69 | store.set(position, { type, name, args }); 70 | 71 | position++; 72 | }, 73 | clear() { 74 | store.clear(); 75 | }, 76 | }; 77 | } 78 | 79 | /** 80 | * @param {CanvasRenderingContext2D} baseContext 81 | * @param {ReturnType['push']} observe 82 | */ 83 | function createObservableContext(baseContext, observe) { 84 | return new Proxy(baseContext, { 85 | get(target, name) { 86 | if (typeof target[name] === 'function') { 87 | return function () { 88 | observe('function', name, [...arguments]); 89 | 90 | return target[name].apply(target, arguments); 91 | }; 92 | } else { 93 | return target[name]; 94 | } 95 | }, 96 | set(target, name, val) { 97 | target[name] = val; 98 | 99 | observe('set', name, val); 100 | 101 | // succesful operation ✅ 102 | return true; 103 | }, 104 | }); 105 | } 106 | 107 | function observeElDimensions(el, callback) { 108 | let { width: prevWidth, height: prevHeight } = el.getBoundingClientRect(); 109 | 110 | const resizeObserver = new ResizeObserver( 111 | debounce(([entry]) => { 112 | const { width, height } = entry.target.getBoundingClientRect(); 113 | 114 | // prevent infinite resize loops if canvas CSS dimensions are not explicitely set 115 | if (~~width !== ~~prevWidth || ~~height !== ~~prevHeight) { 116 | callback(entry); 117 | 118 | prevWidth = width; 119 | prevHeight = height; 120 | } 121 | }, 500) 122 | ); 123 | 124 | resizeObserver.observe(el); 125 | } 126 | 127 | function calculateHeightFromAspectRatio(el, viewBox) { 128 | const width = el.getBoundingClientRect().width; 129 | const aspectRatioPercentage = viewBox[3] / viewBox[2]; 130 | 131 | return width * aspectRatioPercentage + 'px'; 132 | } 133 | 134 | function setCanvasHTMLElementDimensions({ 135 | id, 136 | el, 137 | autoAspectRatio, 138 | viewBox, 139 | resolution, 140 | styleSheet, 141 | }) { 142 | if (autoAspectRatio) { 143 | if (styleSheet.rules?.length) styleSheet.deleteRule(0); 144 | 145 | styleSheet.insertRule( 146 | `.${id} { height: ${calculateHeightFromAspectRatio(el, viewBox)} }`, 147 | 0 148 | ); 149 | } 150 | 151 | const { width, height } = el.getBoundingClientRect(); 152 | 153 | el.width = width * resolution; 154 | el.height = height * resolution; 155 | } 156 | 157 | function calculateAspectRatio( 158 | srcWidth, 159 | srcHeight, 160 | maxWidth, 161 | maxHeight, 162 | scaleMode 163 | ) { 164 | let ratio; 165 | 166 | if (scaleMode === 'fit') { 167 | ratio = Math.min(maxWidth / srcWidth, maxHeight / srcHeight); 168 | } else { 169 | ratio = Math.max(maxWidth / srcWidth, maxHeight / srcHeight); 170 | } 171 | 172 | return { 173 | fitWidth: srcWidth * ratio, 174 | fitHeight: srcHeight * ratio, 175 | ratio: ratio, 176 | }; 177 | } 178 | 179 | function clipCtx(ctx, viewBoxWidth, viewBoxHeight) { 180 | ctx.beginPath(viewBoxWidth, viewBoxHeight); 181 | ctx.rect(0, 0, viewBoxWidth, viewBoxHeight); 182 | ctx.clip(); 183 | ctx.closePath(); 184 | } 185 | 186 | function transformContextMatrix({ ctx, viewBox, resolution, scaleMode }) { 187 | const viewBoxWidth = viewBox[2]; 188 | const viewBoxHeight = viewBox[3]; 189 | 190 | let { 191 | width: canvasWidth, 192 | height: canvasHeight, 193 | } = ctx.canvas.getBoundingClientRect(); 194 | 195 | canvasWidth *= resolution; 196 | canvasHeight *= resolution; 197 | 198 | const { fitWidth, fitHeight, ratio } = calculateAspectRatio( 199 | viewBoxWidth, 200 | viewBoxHeight, 201 | canvasWidth, 202 | canvasHeight, 203 | scaleMode 204 | ); 205 | 206 | const scaleX = fitWidth / viewBoxWidth; 207 | const scaleY = fitHeight / viewBoxHeight; 208 | 209 | const translateX = -viewBox[0] * ratio + (canvasWidth - fitWidth) / 2; 210 | const translateY = -viewBox[1] * ratio + (canvasHeight - fitHeight) / 2; 211 | 212 | ctx.setTransform(scaleX, 0, 0, scaleY, translateX, translateY); 213 | 214 | clipCtx(ctx, viewBoxWidth, viewBoxHeight); 215 | } 216 | 217 | function restoreFromHistory(ctx, history) { 218 | for (const entry of history.entries) { 219 | if (entry.type === 'function') { 220 | ctx[entry.name].apply(ctx, entry.args); 221 | } else { 222 | ctx[entry.name] = entry.args; 223 | } 224 | } 225 | } 226 | 227 | /** 228 | * @typedef ResizeCanvasOptions 229 | * @property {import(".").CreateCanvasOptions} opts 230 | * @property {string} canvasID 231 | * @property {HTMLCanvasElement} canvasHTMLElement 232 | * @property {CSSStyleSheet} canvasStyleSheet 233 | * @property {CanvasRenderingContext2D} baseContext 234 | * @property {ReturnType} history 235 | */ 236 | 237 | /** 238 | * @param {ResizeCanvasOptions} options 239 | */ 240 | function resizeCanvas({ 241 | opts, 242 | canvasID, 243 | canvasHTMLElement, 244 | canvasStyleSheet, 245 | baseContext, 246 | history, 247 | }) { 248 | setCanvasHTMLElementDimensions({ 249 | id: canvasID, 250 | el: canvasHTMLElement, 251 | autoAspectRatio: opts.autoAspectRatio, 252 | viewBox: opts.viewBox, 253 | resolution: opts.resolution, 254 | styleSheet: canvasStyleSheet, 255 | }); 256 | 257 | transformContextMatrix({ 258 | ctx: baseContext, 259 | viewBox: opts.viewBox, 260 | resolution: opts.resolution, 261 | scaleMode: opts.scaleMode, 262 | }); 263 | 264 | if (opts.static) { 265 | restoreFromHistory(baseContext, history); 266 | } 267 | } 268 | 269 | function insertAfter(newNode, referenceNode) { 270 | referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); 271 | } 272 | 273 | function createBaseCanvasStyles() { 274 | const baseStyleSheet = document.createElement('style'); 275 | const target = document.styleSheets[0]?.ownerNode || document.head.firstChild; 276 | 277 | baseStyleSheet.id = 'vb-canvas-base-styles'; 278 | 279 | document.head.insertBefore(baseStyleSheet, target); 280 | 281 | baseStyleSheet.sheet.insertRule( 282 | '.vb-canvas { width: 100%; max-width: 100%; }', 283 | 0 284 | ); 285 | } 286 | 287 | function createCanvasStyleSheet(id) { 288 | const canvasStyleSheet = document.createElement('style'); 289 | const target = document.getElementById('vb-canvas-base-styles'); 290 | 291 | canvasStyleSheet.setAttribute('data-canvas-id', id); 292 | 293 | insertAfter(canvasStyleSheet, target); 294 | 295 | return canvasStyleSheet.sheet; 296 | } 297 | 298 | createBaseCanvasStyles(); 299 | 300 | /** 301 | * @typedef CreateCanvasOptions 302 | * @property {HTMLElement} [target=document.body] Where to add the `` element in the DOM. 303 | * @property {[x: number, y: number, w: number, h: number]} [viewBox=[0, 0, 200, 200]] Canvas viewbox (x, y, w, h). Mirrors SVG Behaviour. 304 | * @property {boolean} [autoAspectRatio=true] Match DOM dimensions to `viewBox`. When true, `` elements behave in a similar way to ``. 305 | * @property {"fit" | "fill"} [scaleMode='fit'] VBCanvas's version of `preserveAspectRatio`. Accepts `fit` or `fill`. `fit` is equal to SVG'sxMidYMid meet. `fill` is equal to SVG'sxMidYMid slice. 306 | * @property {number} [resolution=window.devicePixelRatio] Pixel density of the ``. 307 | * @property {boolean} [static=false] Retains canvas drawing on resize, without the need of an animation loop. 308 | */ 309 | 310 | /** 311 | * @param {CreateCanvasOptions} opts 312 | */ 313 | function createCanvas(opts) { 314 | opts = Object.assign({}, DEFAULTS, opts); 315 | opts.target = resolveTarget(opts.target); 316 | 317 | const canvasID = randomID(); 318 | 319 | const history = createContextHistory(); 320 | 321 | const canvasHTMLElement = createCanvasHTMLElement(canvasID); 322 | const canvasStyleSheet = createCanvasStyleSheet(canvasID); 323 | 324 | const baseContext = getCanvasContext(canvasHTMLElement); 325 | const observableContext = createObservableContext( 326 | baseContext, 327 | (type, name, args) => { 328 | history.push(type, name, args); 329 | } 330 | ); 331 | 332 | mountCanvasToDOM(opts.target, canvasHTMLElement); 333 | 334 | const resize = () => 335 | resizeCanvas({ 336 | opts, 337 | canvasID, 338 | canvasHTMLElement, 339 | canvasStyleSheet, 340 | baseContext, 341 | history, 342 | }); 343 | 344 | resize(); 345 | observeElDimensions(canvasHTMLElement, resize); 346 | 347 | return { 348 | el: canvasHTMLElement, 349 | ctx: opts.static ? observableContext : baseContext, 350 | }; 351 | } 352 | 353 | export { createCanvas }; 354 | //# sourceMappingURL=index.js.map 355 | -------------------------------------------------------------------------------- /dist/types/createContextHistory.d.ts: -------------------------------------------------------------------------------- 1 | export function createContextHistory(): { 2 | readonly entries: IterableIterator; 3 | readonly size: number; 4 | /** 5 | * @param {"function" | "set"} type 6 | * @param {string | number | symbol} name 7 | * @param {unknown} args 8 | */ 9 | push(type: "function" | "set", name: string | number | symbol, args: unknown): void; 10 | clear(): void; 11 | }; 12 | -------------------------------------------------------------------------------- /dist/types/createObservableContext.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {CanvasRenderingContext2D} baseContext 3 | * @param {ReturnType['push']} observe 4 | */ 5 | export function createObservableContext(baseContext: CanvasRenderingContext2D, observe: ReturnType['push']): CanvasRenderingContext2D; 6 | -------------------------------------------------------------------------------- /dist/types/defaults.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace _default { 2 | export const target: HTMLElement; 3 | export const viewBox: number[]; 4 | export const autoAspectRatio: boolean; 5 | export const scaleMode: string; 6 | export const resolution: number; 7 | const _static: boolean; 8 | export { _static as static }; 9 | export const id: number; 10 | } 11 | export default _default; 12 | -------------------------------------------------------------------------------- /dist/types/domUtils.d.ts: -------------------------------------------------------------------------------- 1 | export function resolveTarget(target: any): any; 2 | export function createCanvasHTMLElement(id: any): HTMLCanvasElement; 3 | /** @param {HTMLCanvasElement} canvasHTMLElement */ 4 | export function getCanvasContext(canvasHTMLElement: HTMLCanvasElement): CanvasRenderingContext2D; 5 | /** 6 | * @param {HTMLElement} target 7 | * @param {HTMLCanvasElement} el 8 | */ 9 | export function mountCanvasToDOM(target: HTMLElement, el: HTMLCanvasElement): void; 10 | export function randomID(): string; 11 | -------------------------------------------------------------------------------- /dist/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export type CreateCanvasOptions = { 2 | /** 3 | * Where to add the `` element in the DOM. 4 | */ 5 | target?: HTMLElement; 6 | /** 7 | * Canvas viewbox (x, y, w, h). Mirrors SVG Behaviour. 8 | */ 9 | viewBox?: [x: number, y: number, w: number, h: number]; 10 | /** 11 | * Match DOM dimensions to `viewBox`. When true, `` elements behave in a similar way to ``. 12 | */ 13 | autoAspectRatio?: boolean; 14 | /** 15 | * VBCanvas's version of `preserveAspectRatio`. Accepts `fit` or `fill`. `fit` is equal to SVG'sxMidYMid meet. `fill` is equal to SVG'sxMidYMid slice. 16 | */ 17 | scaleMode?: "fit" | "fill"; 18 | /** 19 | * Pixel density of the ``. 20 | */ 21 | resolution?: number; 22 | /** 23 | * Retains canvas drawing on resize, without the need of an animation loop. 24 | */ 25 | static?: boolean; 26 | }; 27 | /** 28 | * @typedef CreateCanvasOptions 29 | * @property {HTMLElement} [target=document.body] Where to add the `` element in the DOM. 30 | * @property {[x: number, y: number, w: number, h: number]} [viewBox=[0, 0, 200, 200]] Canvas viewbox (x, y, w, h). Mirrors SVG Behaviour. 31 | * @property {boolean} [autoAspectRatio=true] Match DOM dimensions to `viewBox`. When true, `` elements behave in a similar way to ``. 32 | * @property {"fit" | "fill"} [scaleMode='fit'] VBCanvas's version of `preserveAspectRatio`. Accepts `fit` or `fill`. `fit` is equal to SVG'sxMidYMid meet. `fill` is equal to SVG'sxMidYMid slice. 33 | * @property {number} [resolution=window.devicePixelRatio] Pixel density of the ``. 34 | * @property {boolean} [static=false] Retains canvas drawing on resize, without the need of an animation loop. 35 | */ 36 | /** 37 | * @param {CreateCanvasOptions} opts 38 | */ 39 | export function createCanvas(opts: CreateCanvasOptions): { 40 | el: HTMLCanvasElement; 41 | ctx: CanvasRenderingContext2D; 42 | }; 43 | -------------------------------------------------------------------------------- /dist/types/observeElDimensions.d.ts: -------------------------------------------------------------------------------- 1 | export function observeElDimensions(el: any, callback: any): void; 2 | -------------------------------------------------------------------------------- /dist/types/resizeCanvas.d.ts: -------------------------------------------------------------------------------- 1 | export type ResizeCanvasOptions = { 2 | opts: import(".").CreateCanvasOptions; 3 | canvasID: string; 4 | canvasHTMLElement: HTMLCanvasElement; 5 | canvasStyleSheet: CSSStyleSheet; 6 | baseContext: CanvasRenderingContext2D; 7 | history: ReturnType; 8 | }; 9 | /** 10 | * @typedef ResizeCanvasOptions 11 | * @property {import(".").CreateCanvasOptions} opts 12 | * @property {string} canvasID 13 | * @property {HTMLCanvasElement} canvasHTMLElement 14 | * @property {CSSStyleSheet} canvasStyleSheet 15 | * @property {CanvasRenderingContext2D} baseContext 16 | * @property {ReturnType} history 17 | */ 18 | /** 19 | * @param {ResizeCanvasOptions} options 20 | */ 21 | export function resizeCanvas({ opts, canvasID, canvasHTMLElement, canvasStyleSheet, baseContext, history, }: ResizeCanvasOptions): void; 22 | -------------------------------------------------------------------------------- /dist/types/restoreFromHistory.d.ts: -------------------------------------------------------------------------------- 1 | export function restoreFromHistory(ctx: any, history: any): void; 2 | -------------------------------------------------------------------------------- /dist/types/setCanvasHTMLElementDimensions.d.ts: -------------------------------------------------------------------------------- 1 | export function setCanvasHTMLElementDimensions({ id, el, autoAspectRatio, viewBox, resolution, styleSheet, }: { 2 | id: any; 3 | el: any; 4 | autoAspectRatio: any; 5 | viewBox: any; 6 | resolution: any; 7 | styleSheet: any; 8 | }): void; 9 | -------------------------------------------------------------------------------- /dist/types/styles.d.ts: -------------------------------------------------------------------------------- 1 | export function createCanvasStyleSheet(id: any): CSSStyleSheet; 2 | export function createBaseCanvasStyles(): void; 3 | -------------------------------------------------------------------------------- /dist/types/transformContextMatrix.d.ts: -------------------------------------------------------------------------------- 1 | export function transformContextMatrix({ ctx, viewBox, resolution, scaleMode }: { 2 | ctx: any; 3 | viewBox: any; 4 | resolution: any; 5 | scaleMode: any; 6 | }): void; 7 | -------------------------------------------------------------------------------- /dist/vb-canvas.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).VBCanvas={})}(this,(function(t){"use strict";var e={target:document.body,viewBox:[0,0,300,150],autoAspectRatio:!0,scaleMode:"fit",resolution:window.devicePixelRatio||1,static:!1,id:Math.random()};function n(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){if("undefined"==typeof Symbol||!(Symbol.iterator in Object(t)))return;var n=[],r=!0,i=!1,o=void 0;try{for(var a,s=t[Symbol.iterator]();!(r=(a=s.next()).done)&&(n.push(a.value),!e||n.length!==e);r=!0);}catch(t){i=!0,o=t}finally{try{r||null==s.return||s.return()}finally{if(i)throw o}}return n}(t,e)||r(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function r(t,e){if(t){if("string"==typeof t)return i(t,e);var n=Object.prototype.toString.call(t).slice(8,-1);return"Object"===n&&t.constructor&&(n=t.constructor.name),"Map"===n||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?i(t,e):void 0}}function i(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,r=new Array(e);n0},t.prototype.connect_=function(){a&&!this.connected_&&(document.addEventListener("transitionend",this.onTransitionEnd_),window.addEventListener("resize",this.refresh),f?(this.mutationsObserver_=new MutationObserver(this.refresh),this.mutationsObserver_.observe(document,{attributes:!0,childList:!0,characterData:!0,subtree:!0})):(document.addEventListener("DOMSubtreeModified",this.refresh),this.mutationEventsAdded_=!0),this.connected_=!0)},t.prototype.disconnect_=function(){a&&this.connected_&&(document.removeEventListener("transitionend",this.onTransitionEnd_),window.removeEventListener("resize",this.refresh),this.mutationsObserver_&&this.mutationsObserver_.disconnect(),this.mutationEventsAdded_&&document.removeEventListener("DOMSubtreeModified",this.refresh),this.mutationsObserver_=null,this.mutationEventsAdded_=!1,this.connected_=!1)},t.prototype.onTransitionEnd_=function(t){var e=t.propertyName,n=void 0===e?"":e;u.some((function(t){return!!~n.indexOf(t)}))&&this.refresh()},t.getInstance=function(){return this.instance_||(this.instance_=new t),this.instance_},t.instance_=null,t}(),l=function(t,e){for(var n=0,r=Object.keys(e);n0},t}(),x="undefined"!=typeof WeakMap?new WeakMap:new o,M=function t(e){if(!(this instanceof t))throw new TypeError("Cannot call a class as a function.");if(!arguments.length)throw new TypeError("1 argument required, but only 0 present.");var n=h.getInstance(),r=new E(e,n,this);x.set(this,r)};["observe","unobserve","disconnect"].forEach((function(t){M.prototype[t]=function(){var e;return(e=x.get(this))[t].apply(e,arguments)}}));var A=void 0!==s.ResizeObserver?s.ResizeObserver:M,S="object"==typeof global&&global&&global.Object===Object&&global,T="object"==typeof self&&self&&self.Object===Object&&self,j=S||T||Function("return this")(),R=j.Symbol,B=Object.prototype,C=B.hasOwnProperty,D=B.toString,I=R?R.toStringTag:void 0;var L=Object.prototype.toString;var z=R?R.toStringTag:void 0;function W(t){return null==t?void 0===t?"[object Undefined]":"[object Null]":z&&z in Object(t)?function(t){var e=C.call(t,I),n=t[I];try{t[I]=void 0;var r=!0}catch(t){}var i=D.call(t);return r&&(e?t[I]=n:delete t[I]),i}(t):function(t){return L.call(t)}(t)}function k(t){var e=typeof t;return null!=t&&("object"==e||"function"==e)}var H=/^\s+|\s+$/g,N=/^[-+]0x[0-9a-f]+$/i,P=/^0b[01]+$/i,q=/^0o[0-7]+$/i,F=parseInt;function G(t){if("number"==typeof t)return t;if(function(t){return"symbol"==typeof t||function(t){return null!=t&&"object"==typeof t}(t)&&"[object Symbol]"==W(t)}(t))return NaN;if(k(t)){var e="function"==typeof t.valueOf?t.valueOf():t;t=k(e)?e+"":e}if("string"!=typeof t)return 0===t?t:+t;t=t.replace(H,"");var n=P.test(t);return n||q.test(t)?F(t.slice(2),n?2:8):N.test(t)?NaN:+t}var V,$,U,J=function(){return j.Date.now()},K=Math.max,Q=Math.min;function X(t,e,n){var r,i,o,a,s,c,u=0,f=!1,h=!1,l=!0;if("function"!=typeof t)throw new TypeError("Expected a function");function d(e){var n=r,o=i;return r=i=void 0,u=e,a=t.apply(o,n)}function v(t){return u=t,s=setTimeout(b,e),f?d(t):a}function p(t){var n=t-c;return void 0===c||n>=e||n<0||h&&t-u>=o}function b(){var t=J();if(p(t))return y(t);s=setTimeout(b,function(t){var n=e-(t-c);return h?Q(n,o-(t-u)):n}(t))}function y(t){return s=void 0,l&&r?d(t):(r=i=void 0,a)}function m(){var t=J(),n=p(t);if(r=arguments,i=this,c=t,n){if(void 0===s)return v(c);if(h)return clearTimeout(s),s=setTimeout(b,e),d(c)}return void 0===s&&(s=setTimeout(b,e)),a}return e=G(e)||0,k(n)&&(f=!!n.leading,o=(h="maxWait"in n)?K(G(n.maxWait)||0,e):o,l="trailing"in n?!!n.trailing:l),m.cancel=function(){void 0!==s&&clearTimeout(s),u=0,r=c=i=s=void 0},m.flush=function(){return void 0===s?a:y(J())},m}function Y(t){var e=t.ctx,n=t.viewBox,r=t.resolution,i=t.scaleMode,o=n[2],a=n[3],s=e.canvas.getBoundingClientRect(),c=s.width,u=s.height,f=function(t,e,n,r,i){var o;return{fitWidth:t*(o="fit"===i?Math.min(n/t,r/e):Math.max(n/t,r/e)),fitHeight:e*o,ratio:o}}(o,a,c*=r,u*=r,i),h=f.fitWidth,l=f.fitHeight,d=f.ratio,v=h/o,p=l/a,b=-n[0]*d+(c-h)/2,y=-n[1]*d+(u-l)/2;e.setTransform(v,0,0,p,b,y),function(t,e,n){t.beginPath(e,n),t.rect(0,0,e,n),t.clip(),t.closePath()}(e,o,a)}function Z(t,e){var n,i=function(t,e){var n;if("undefined"==typeof Symbol||null==t[Symbol.iterator]){if(Array.isArray(t)||(n=r(t))||e&&t&&"number"==typeof t.length){n&&(t=n);var i=0,o=function(){};return{s:o,n:function(){return i>=t.length?{done:!0}:{done:!1,value:t[i++]}},e:function(t){throw t},f:o}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,s=!0,c=!1;return{s:function(){n=t[Symbol.iterator]()},n:function(){var t=n.next();return s=t.done,t},e:function(t){c=!0,a=t},f:function(){try{s||null==n.return||n.return()}finally{if(c)throw a}}}}(e.entries);try{for(i.s();!(n=i.n()).done;){var o=n.value;"function"===o.type?t[o.name].apply(t,o.args):t[o.name]=o.args}}catch(t){i.e(t)}finally{i.f()}}$=document.createElement("style"),U=(null===(V=document.styleSheets[0])||void 0===V?void 0:V.ownerNode)||document.head.firstChild,$.id="vb-canvas-base-styles",document.head.insertBefore($,U),$.sheet.insertRule(".vb-canvas { width: 100%; max-width: 100%; }",0),t.createCanvas=function(t){(t=Object.assign({},e,t)).target=function(t){return"string"==typeof t?document.querySelector(t):t}(t.target);var r,i,o,a,s="_"+Math.random().toString(36).substr(2,9),c=(r=new Map,i=0,{get entries(){return r.values()},get size(){return r.size},push:function(t,e,n){r.set(i,{type:t,name:e,args:n}),i++},clear:function(){r.clear()}}),u=(o=s,(a=document.createElement("canvas")).classList.add("vb-canvas"),a.classList.add(o),a),f=function(t){var e,n,r=document.createElement("style"),i=document.getElementById("vb-canvas-base-styles");return r.setAttribute("data-canvas-id",t),e=r,(n=i).parentNode.insertBefore(e,n.nextSibling),r.sheet}(s),h=function(t){return t.getContext("2d")}(u),l=function(t,e){return new Proxy(t,{get:function(t,n){return"function"==typeof t[n]?function(){return e("function",n,Array.prototype.slice.call(arguments)),t[n].apply(t,arguments)}:t[n]},set:function(t,n,r){return t[n]=r,e("set",n,r),!0}})}(h,(function(t,e,n){c.push(t,e,n)}));!function(t,e){t.appendChild(e)}(t.target,u);var d=function(){return function(t){var e=t.opts,n=t.canvasID,r=t.canvasHTMLElement,i=t.canvasStyleSheet,o=t.baseContext,a=t.history;!function(t){var e,n=t.id,r=t.el,i=t.autoAspectRatio,o=t.viewBox,a=t.resolution,s=t.styleSheet;i&&((null===(e=s.rules)||void 0===e?void 0:e.length)&&s.deleteRule(0),s.insertRule(".".concat(n," { height: ").concat(function(t,e){return t.getBoundingClientRect().width*(e[3]/e[2])+"px"}(r,o)," }"),0));var c=r.getBoundingClientRect(),u=c.width,f=c.height;r.width=u*a,r.height=f*a}({id:n,el:r,autoAspectRatio:e.autoAspectRatio,viewBox:e.viewBox,resolution:e.resolution,styleSheet:i}),Y({ctx:o,viewBox:e.viewBox,resolution:e.resolution,scaleMode:e.scaleMode}),e.static&&Z(o,a)}({opts:t,canvasID:s,canvasHTMLElement:u,canvasStyleSheet:f,baseContext:h,history:c})};return d(),function(t,e){var r=t.getBoundingClientRect(),i=r.width,o=r.height;new A(X((function(t){var r=n(t,1)[0],a=r.target.getBoundingClientRect(),s=a.width,c=a.height;~~s==~~i&&~~c==~~o||(e(r),i=s,o=c)}),500)).observe(t)}(u,d),{el:u,ctx:t.static?l:h}}})); 2 | //# sourceMappingURL=vb-canvas.min.js.map 3 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleNameMapper: { 3 | '^lodash-es$': 'lodash', 4 | }, 5 | setupFiles: ['jest-canvas-mock'], 6 | }; 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vb-canvas", 3 | "source": "src/index.js", 4 | "main": "dist/cjs/index.js", 5 | "module": "dist/esm/index.js", 6 | "version": "0.1.8", 7 | "description": "", 8 | "dependencies": { 9 | "lodash-es": "^4.17.15", 10 | "resize-observer-polyfill": "^1.5.1" 11 | }, 12 | "devDependencies": { 13 | "@babel/core": "^7.12.3", 14 | "@babel/preset-env": "^7.12.1", 15 | "@rollup/plugin-babel": "^5.2.1", 16 | "@rollup/plugin-node-resolve": "^10.0.0", 17 | "jest": "^26.6.3", 18 | "jest-canvas-mock": "^2.3.0", 19 | "jest-puppeteer": "^4.4.0", 20 | "puppeteer": "^5.5.0", 21 | "rollup": "^2.33.2", 22 | "rollup-plugin-terser": "^7.0.2", 23 | "typescript": "^4.1.2" 24 | }, 25 | "scripts": { 26 | "build": "rollup -c", 27 | "dev": "rollup -c -w", 28 | "test": "jest --watchAll --coverage", 29 | "types": "tsc" 30 | }, 31 | "keywords": [], 32 | "author": "", 33 | "license": "ISC" 34 | } 35 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # HTML5 Canvas, responsive. 2 | 3 | VBCanvas allows you to define a canvas viewBox attribute [just like SVG](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/viewBox). Once set, the canvas will render it's contents relative to the viewBox and scale the canvas automatically! 4 | 5 | ## Demo 6 | 7 | - [Simple animation (fit / xMidYMid meet)](https://codepen.io/georgedoescode/pen/ZEOgLwZ?editors=0010) 8 | - [Simple animation (fill / xMidYMid slice)](https://codepen.io/georgedoescode/pen/GRjJQgm) 9 | 10 | ## Installation 11 | 12 | ### 1. Package Manager 13 | 14 | ```bash 15 | # npm 16 | npm i vb-canvas 17 | 18 | # Yarn 19 | yarn add vb-canvas 20 | ``` 21 | 22 | ### 2. CDN 23 | 24 | ```html 25 | 26 | ``` 27 | 28 | ## Usage 29 | 30 | The quickest way to take VBCanvas for a spin is to create a `.html` file and grab the library from `unpkg`. Once you are all set up, you can create canvasses using `VBCanvas.createCanvas`. 31 | 32 | Check out the code below for a complete example. 33 | 34 | ```html 35 | 36 | 37 | 38 | 39 | 40 | VBCanvas Demo 41 | 42 | 54 | 55 | 56 |
57 | 58 | 59 | 82 | 83 | 84 | ``` 85 | 86 | **For more examples, check out the demo.** 87 | 88 | ### Module bundlers 89 | 90 | To use VBCanvas in a module based environment, import the `createCanvas` function from `VBCanvas` like so: 91 | 92 | ```javascript 93 | import { createCanvas } from 'VBCanvas'; 94 | 95 | const canvasWidth = 100; 96 | const canvasHeight = 100; 97 | 98 | const { ctx } = createCanvas({ 99 | viewBox: [0, 0, canvasWidth, canvasHeight], 100 | ... 101 | }) 102 | ``` 103 | 104 | ### Options 105 | 106 | | Name | Type | Default | Description | 107 | | ----------------- | -------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | 108 | | `target` | `String Or DOM Node` | `document.body` | Where to add the `` element in the DOM | 109 | | `viewBox` | `Array` | `[0, 0, 200, 200]` | Canvas viewbox (x, y, w, h). Mirrors SVG Behaviour. | 110 | | `autoAspectRatio` | `Boolean` | `true` | Match DOM dimensions to `viewBox`. When true, `` elements behave in a similar way to ``. | 111 | | `scaleMode` | `String` | `fit` | VBCanvas's version of `preserveAspectRatio`. Accepts `fit` or `fill`. `fit` is equal to SVG's`xMidYMid meet`. `fill` is equal to SVG's`xMidYMid slice`. | 112 | | `resolution` | `Number` | `window.devicePixelRatio` | Pixel density of the `` | 113 | | `static` | `Boolean` | `false` | Retains canvas drawing on resize, without the need of an animation loop. | 114 | 115 | ## PRs always welcome! 116 | 117 | This library is still in development, so you may find some small bugs. If you do, please open an issue or PR! 118 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 2 | import { terser } from 'rollup-plugin-terser'; 3 | import babel from '@rollup/plugin-babel'; 4 | 5 | import pkg from './package.json'; 6 | 7 | const OUTPUT_DIR = 'dist'; 8 | const INPUT = './src/index.js'; 9 | 10 | export default [ 11 | // UMD 12 | { 13 | input: INPUT, 14 | plugins: [ 15 | nodeResolve(), 16 | babel({ 17 | babelHelpers: 'bundled', 18 | }), 19 | terser(), 20 | ], 21 | output: { 22 | file: `${OUTPUT_DIR}/${pkg.name}.min.js`, 23 | format: 'umd', 24 | name: 'VBCanvas', 25 | esModule: false, 26 | exports: 'named', 27 | sourcemap: true, 28 | }, 29 | }, 30 | // ESM + CJS 31 | { 32 | input: INPUT, 33 | plugins: [nodeResolve()], 34 | external: ['lodash-es', 'resize-observer-polyfill'], 35 | output: [ 36 | // ESM 37 | { 38 | dir: `${OUTPUT_DIR}/esm`, 39 | format: 'esm', 40 | exports: 'named', 41 | sourcemap: true, 42 | }, 43 | // CJS 44 | { 45 | dir: `${OUTPUT_DIR}/cjs`, 46 | format: 'cjs', 47 | exports: 'named', 48 | sourcemap: true, 49 | }, 50 | ], 51 | }, 52 | ]; 53 | -------------------------------------------------------------------------------- /src/createContextHistory.js: -------------------------------------------------------------------------------- 1 | function createContextHistory() { 2 | const store = new Map(); 3 | let position = 0; 4 | 5 | return { 6 | get entries() { 7 | return store.values(); 8 | }, 9 | get size() { 10 | return store.size; 11 | }, 12 | /** 13 | * @param {"function" | "set"} type 14 | * @param {string | number | symbol} name 15 | * @param {unknown} args 16 | */ 17 | push(type, name, args) { 18 | store.set(position, { type, name, args }); 19 | 20 | position++; 21 | }, 22 | clear() { 23 | store.clear(); 24 | }, 25 | }; 26 | } 27 | 28 | export { createContextHistory }; 29 | -------------------------------------------------------------------------------- /src/createObservableContext.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {CanvasRenderingContext2D} baseContext 3 | * @param {ReturnType['push']} observe 4 | */ 5 | function createObservableContext(baseContext, observe) { 6 | return new Proxy(baseContext, { 7 | get(target, name) { 8 | if (typeof target[name] === 'function') { 9 | return function () { 10 | observe('function', name, [...arguments]); 11 | 12 | return target[name].apply(target, arguments); 13 | }; 14 | } else { 15 | return target[name]; 16 | } 17 | }, 18 | set(target, name, val) { 19 | target[name] = val; 20 | 21 | observe('set', name, val); 22 | 23 | // succesful operation ✅ 24 | return true; 25 | }, 26 | }); 27 | } 28 | 29 | export { createObservableContext }; 30 | -------------------------------------------------------------------------------- /src/defaults.js: -------------------------------------------------------------------------------- 1 | export default { 2 | target: document.body, 3 | viewBox: [0, 0, 300, 150], 4 | autoAspectRatio: true, 5 | scaleMode: 'fit', 6 | resolution: window.devicePixelRatio || 1, 7 | static: false, 8 | id: Math.random(), 9 | }; 10 | -------------------------------------------------------------------------------- /src/domUtils.js: -------------------------------------------------------------------------------- 1 | function resolveTarget(target) { 2 | if (typeof target === 'string') { 3 | return document.querySelector(target); 4 | } else { 5 | return target; 6 | } 7 | } 8 | 9 | function createCanvasHTMLElement(id) { 10 | const el = document.createElement('canvas'); 11 | 12 | el.classList.add('vb-canvas'); 13 | el.classList.add(id); 14 | 15 | return el; 16 | } 17 | 18 | /** @param {HTMLCanvasElement} canvasHTMLElement */ 19 | function getCanvasContext(canvasHTMLElement) { 20 | return canvasHTMLElement.getContext('2d'); 21 | } 22 | 23 | /** 24 | * @param {HTMLElement} target 25 | * @param {HTMLCanvasElement} el 26 | */ 27 | function mountCanvasToDOM(target, el) { 28 | target.appendChild(el); 29 | } 30 | 31 | // https://gist.github.com/gordonbrander/2230317 32 | function randomID() { 33 | // Math.random should be unique because of its seeding algorithm. 34 | // Convert it to base 36 (numbers + letters), and grab the first 9 characters 35 | // after the decimal. 36 | return '_' + Math.random().toString(36).substr(2, 9); 37 | } 38 | 39 | export { 40 | resolveTarget, 41 | createCanvasHTMLElement, 42 | getCanvasContext, 43 | mountCanvasToDOM, 44 | randomID, 45 | }; 46 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import DEFAULTS from './defaults'; 2 | 3 | import { 4 | resolveTarget, 5 | createCanvasHTMLElement, 6 | getCanvasContext, 7 | mountCanvasToDOM, 8 | randomID, 9 | } from './domUtils'; 10 | 11 | import { createContextHistory } from './createContextHistory'; 12 | import { createObservableContext } from './createObservableContext'; 13 | import { observeElDimensions } from './observeElDimensions'; 14 | import { resizeCanvas } from './resizeCanvas'; 15 | import { createCanvasStyleSheet, createBaseCanvasStyles } from './styles'; 16 | 17 | createBaseCanvasStyles(); 18 | 19 | /** 20 | * @typedef CreateCanvasOptions 21 | * @property {HTMLElement} [target=document.body] Where to add the `` element in the DOM. 22 | * @property {[x: number, y: number, w: number, h: number]} [viewBox=[0, 0, 200, 200]] Canvas viewbox (x, y, w, h). Mirrors SVG Behaviour. 23 | * @property {boolean} [autoAspectRatio=true] Match DOM dimensions to `viewBox`. When true, `` elements behave in a similar way to ``. 24 | * @property {"fit" | "fill"} [scaleMode='fit'] VBCanvas's version of `preserveAspectRatio`. Accepts `fit` or `fill`. `fit` is equal to SVG'sxMidYMid meet. `fill` is equal to SVG'sxMidYMid slice. 25 | * @property {number} [resolution=window.devicePixelRatio] Pixel density of the ``. 26 | * @property {boolean} [static=false] Retains canvas drawing on resize, without the need of an animation loop. 27 | */ 28 | 29 | /** 30 | * @param {CreateCanvasOptions} opts 31 | */ 32 | function createCanvas(opts) { 33 | opts = Object.assign({}, DEFAULTS, opts); 34 | opts.target = resolveTarget(opts.target); 35 | 36 | const canvasID = randomID(); 37 | 38 | const history = createContextHistory(); 39 | 40 | const canvasHTMLElement = createCanvasHTMLElement(canvasID); 41 | const canvasStyleSheet = createCanvasStyleSheet(canvasID); 42 | 43 | const baseContext = getCanvasContext(canvasHTMLElement); 44 | const observableContext = createObservableContext( 45 | baseContext, 46 | (type, name, args) => { 47 | history.push(type, name, args); 48 | } 49 | ); 50 | 51 | mountCanvasToDOM(opts.target, canvasHTMLElement); 52 | 53 | const resize = () => 54 | resizeCanvas({ 55 | opts, 56 | canvasID, 57 | canvasHTMLElement, 58 | canvasStyleSheet, 59 | baseContext, 60 | history, 61 | }); 62 | 63 | resize(); 64 | observeElDimensions(canvasHTMLElement, resize); 65 | 66 | return { 67 | el: canvasHTMLElement, 68 | ctx: opts.static ? observableContext : baseContext, 69 | }; 70 | } 71 | 72 | export { createCanvas }; 73 | -------------------------------------------------------------------------------- /src/observeElDimensions.js: -------------------------------------------------------------------------------- 1 | import ResizeObserver from 'resize-observer-polyfill'; 2 | import { debounce } from 'lodash-es'; 3 | 4 | function observeElDimensions(el, callback) { 5 | let { width: prevWidth, height: prevHeight } = el.getBoundingClientRect(); 6 | 7 | const resizeObserver = new ResizeObserver( 8 | debounce(([entry]) => { 9 | const { width, height } = entry.target.getBoundingClientRect(); 10 | 11 | // prevent infinite resize loops if canvas CSS dimensions are not explicitely set 12 | if (~~width !== ~~prevWidth || ~~height !== ~~prevHeight) { 13 | callback(entry); 14 | 15 | prevWidth = width; 16 | prevHeight = height; 17 | } 18 | }, 500) 19 | ); 20 | 21 | resizeObserver.observe(el); 22 | } 23 | 24 | export { observeElDimensions }; 25 | -------------------------------------------------------------------------------- /src/resizeCanvas.js: -------------------------------------------------------------------------------- 1 | import { setCanvasHTMLElementDimensions } from './setCanvasHTMLElementDimensions'; 2 | import { transformContextMatrix } from './transformContextMatrix'; 3 | import { restoreFromHistory } from './restoreFromHistory'; 4 | 5 | /** 6 | * @typedef ResizeCanvasOptions 7 | * @property {import(".").CreateCanvasOptions} opts 8 | * @property {string} canvasID 9 | * @property {HTMLCanvasElement} canvasHTMLElement 10 | * @property {CSSStyleSheet} canvasStyleSheet 11 | * @property {CanvasRenderingContext2D} baseContext 12 | * @property {ReturnType} history 13 | */ 14 | 15 | /** 16 | * @param {ResizeCanvasOptions} options 17 | */ 18 | function resizeCanvas({ 19 | opts, 20 | canvasID, 21 | canvasHTMLElement, 22 | canvasStyleSheet, 23 | baseContext, 24 | history, 25 | }) { 26 | setCanvasHTMLElementDimensions({ 27 | id: canvasID, 28 | el: canvasHTMLElement, 29 | autoAspectRatio: opts.autoAspectRatio, 30 | viewBox: opts.viewBox, 31 | resolution: opts.resolution, 32 | styleSheet: canvasStyleSheet, 33 | }); 34 | 35 | transformContextMatrix({ 36 | ctx: baseContext, 37 | viewBox: opts.viewBox, 38 | resolution: opts.resolution, 39 | scaleMode: opts.scaleMode, 40 | }); 41 | 42 | if (opts.static) { 43 | restoreFromHistory(baseContext, history); 44 | } 45 | } 46 | 47 | export { resizeCanvas }; 48 | -------------------------------------------------------------------------------- /src/restoreFromHistory.js: -------------------------------------------------------------------------------- 1 | function restoreFromHistory(ctx, history) { 2 | for (const entry of history.entries) { 3 | if (entry.type === 'function') { 4 | ctx[entry.name].apply(ctx, entry.args); 5 | } else { 6 | ctx[entry.name] = entry.args; 7 | } 8 | } 9 | } 10 | 11 | export { restoreFromHistory }; 12 | -------------------------------------------------------------------------------- /src/setCanvasHTMLElementDimensions.js: -------------------------------------------------------------------------------- 1 | function calculateHeightFromAspectRatio(el, viewBox) { 2 | const width = el.getBoundingClientRect().width; 3 | const aspectRatioPercentage = viewBox[3] / viewBox[2]; 4 | 5 | return width * aspectRatioPercentage + 'px'; 6 | } 7 | 8 | function setCanvasHTMLElementDimensions({ 9 | id, 10 | el, 11 | autoAspectRatio, 12 | viewBox, 13 | resolution, 14 | styleSheet, 15 | }) { 16 | if (autoAspectRatio) { 17 | if (styleSheet.rules?.length) styleSheet.deleteRule(0); 18 | 19 | styleSheet.insertRule( 20 | `.${id} { height: ${calculateHeightFromAspectRatio(el, viewBox)} }`, 21 | 0 22 | ); 23 | } 24 | 25 | const { width, height } = el.getBoundingClientRect(); 26 | 27 | el.width = width * resolution; 28 | el.height = height * resolution; 29 | } 30 | 31 | export { setCanvasHTMLElementDimensions }; 32 | -------------------------------------------------------------------------------- /src/styles.js: -------------------------------------------------------------------------------- 1 | function insertAfter(newNode, referenceNode) { 2 | referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); 3 | } 4 | 5 | function createBaseCanvasStyles() { 6 | const baseStyleSheet = document.createElement('style'); 7 | const target = document.styleSheets[0]?.ownerNode || document.head.firstChild; 8 | 9 | baseStyleSheet.id = 'vb-canvas-base-styles'; 10 | 11 | document.head.insertBefore(baseStyleSheet, target); 12 | 13 | baseStyleSheet.sheet.insertRule( 14 | '.vb-canvas { width: 100%; max-width: 100%; }', 15 | 0 16 | ); 17 | } 18 | 19 | function createCanvasStyleSheet(id) { 20 | const canvasStyleSheet = document.createElement('style'); 21 | const target = document.getElementById('vb-canvas-base-styles'); 22 | 23 | canvasStyleSheet.setAttribute('data-canvas-id', id); 24 | 25 | insertAfter(canvasStyleSheet, target); 26 | 27 | return canvasStyleSheet.sheet; 28 | } 29 | 30 | export { createCanvasStyleSheet, createBaseCanvasStyles }; 31 | -------------------------------------------------------------------------------- /src/transformContextMatrix.js: -------------------------------------------------------------------------------- 1 | function calculateAspectRatio( 2 | srcWidth, 3 | srcHeight, 4 | maxWidth, 5 | maxHeight, 6 | scaleMode 7 | ) { 8 | let ratio; 9 | 10 | if (scaleMode === 'fit') { 11 | ratio = Math.min(maxWidth / srcWidth, maxHeight / srcHeight); 12 | } else { 13 | ratio = Math.max(maxWidth / srcWidth, maxHeight / srcHeight); 14 | } 15 | 16 | return { 17 | fitWidth: srcWidth * ratio, 18 | fitHeight: srcHeight * ratio, 19 | ratio: ratio, 20 | }; 21 | } 22 | 23 | function clipCtx(ctx, viewBoxWidth, viewBoxHeight) { 24 | ctx.beginPath(viewBoxWidth, viewBoxHeight); 25 | ctx.rect(0, 0, viewBoxWidth, viewBoxHeight); 26 | ctx.clip(); 27 | ctx.closePath(); 28 | } 29 | 30 | function transformContextMatrix({ ctx, viewBox, resolution, scaleMode }) { 31 | const viewBoxWidth = viewBox[2]; 32 | const viewBoxHeight = viewBox[3]; 33 | 34 | let { 35 | width: canvasWidth, 36 | height: canvasHeight, 37 | } = ctx.canvas.getBoundingClientRect(); 38 | 39 | canvasWidth *= resolution; 40 | canvasHeight *= resolution; 41 | 42 | const { fitWidth, fitHeight, ratio } = calculateAspectRatio( 43 | viewBoxWidth, 44 | viewBoxHeight, 45 | canvasWidth, 46 | canvasHeight, 47 | scaleMode 48 | ); 49 | 50 | const scaleX = fitWidth / viewBoxWidth; 51 | const scaleY = fitHeight / viewBoxHeight; 52 | 53 | const translateX = -viewBox[0] * ratio + (canvasWidth - fitWidth) / 2; 54 | const translateY = -viewBox[1] * ratio + (canvasHeight - fitHeight) / 2; 55 | 56 | ctx.setTransform(scaleX, 0, 0, scaleY, translateX, translateY); 57 | 58 | clipCtx(ctx, viewBoxWidth, viewBoxHeight); 59 | } 60 | 61 | export { transformContextMatrix }; 62 | -------------------------------------------------------------------------------- /tests/VBCanvas.test.js: -------------------------------------------------------------------------------- 1 | import { createCanvas } from '../src/index'; 2 | import defaultCanvasOpts from '../src/defaults'; 3 | 4 | const originalDocHead = document.head.innerHTML; 5 | const originalDocBody = document.body.innerHTML; 6 | 7 | describe('VBCanvas', () => { 8 | afterEach(() => { 9 | document.body.innerHTML = originalDocBody; 10 | document.head.innerHTML = originalDocHead; 11 | }); 12 | 13 | describe('VBCanvas -> Initialise', () => { 14 | it('When no options are passed, then the library initialises as expected', () => { 15 | expect(createCanvas()).toEqual( 16 | expect.objectContaining({ 17 | ctx: expect.any(CanvasRenderingContext2D), 18 | el: expect.any(HTMLCanvasElement), 19 | }) 20 | ); 21 | }); 22 | 23 | it('When options are passed, then the library initialises as expected', () => { 24 | expect( 25 | createCanvas({ 26 | ...defaultCanvasOpts, 27 | }) 28 | ).toEqual( 29 | expect.objectContaining({ 30 | ctx: expect.any(CanvasRenderingContext2D), 31 | el: expect.any(HTMLCanvasElement), 32 | }) 33 | ); 34 | }); 35 | 36 | it('When a String target is passed, then the canvas element is mounted correctly', () => { 37 | const target = document.createElement('div'); 38 | target.id = 'target'; 39 | 40 | const otherDiv = document.createElement('div'); 41 | 42 | document.body.appendChild(target); 43 | document.body.appendChild(otherDiv); 44 | 45 | const { el } = createCanvas({ 46 | target: '#target', 47 | }); 48 | 49 | expect(target.contains(el)).toBe(true); 50 | expect(otherDiv.contains(el)).toBe(false); 51 | }); 52 | 53 | it('When a DOM node target is passed, then the canvas element is mounted correctly', () => { 54 | const target = document.createElement('div'); 55 | target.id = 'target'; 56 | 57 | const otherDiv = document.createElement('div'); 58 | 59 | document.body.appendChild(target); 60 | document.body.appendChild(otherDiv); 61 | 62 | const { el } = createCanvas({ 63 | target: document.getElementById('target'), 64 | }); 65 | 66 | expect(target.contains(el)).toBe(true); 67 | expect(otherDiv.contains(el)).toBe(false); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "declaration": true, 5 | "emitDeclarationOnly": true, 6 | "outDir": "./dist/types" 7 | }, 8 | "include": ["./src/*.js", "./src/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /vb-og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georgedoescode/VBCanvas/8d1cac759cccf90b2a77ed45c70ed6ca771b0093/vb-og.png --------------------------------------------------------------------------------