├── .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 |55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |
57 |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 | |
55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |
57 |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 | |
55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |
57 |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 | |
55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |
57 |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 | |
55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |
57 |File | 64 |65 | | Statements | 66 |67 | | Branches | 68 |69 | | Functions | 70 |71 | | Lines | 72 |73 | |
---|---|---|---|---|---|---|---|---|---|
createContextHistory.js | 77 |
78 |
79 | |
80 | 37.5% | 81 |3/8 | 82 |100% | 83 |0/0 | 84 |20% | 85 |1/5 | 86 |37.5% | 87 |3/8 | 88 |
createObservableContext.js | 92 |
93 |
94 | |
95 | 11.11% | 96 |1/9 | 97 |0% | 98 |0/2 | 99 |25% | 100 |1/4 | 101 |11.11% | 102 |1/9 | 103 |
defaults.js | 107 |
108 |
109 | |
110 | 0% | 111 |0/0 | 112 |0% | 113 |1/2 | 114 |0% | 115 |0/0 | 116 |0% | 117 |0/0 | 118 |
domUtils.js | 122 |
123 |
124 | |
125 | 100% | 126 |10/10 | 127 |100% | 128 |2/2 | 129 |100% | 130 |5/5 | 131 |100% | 132 |10/10 | 133 |
index.js | 137 |
138 |
139 | |
140 | 93.75% | 141 |15/16 | 142 |50% | 143 |1/2 | 144 |66.67% | 145 |2/3 | 146 |93.75% | 147 |15/16 | 148 |
observeElDimensions.js | 152 |
153 |
154 | |
155 | 37.5% | 156 |3/8 | 157 |0% | 158 |0/4 | 159 |50% | 160 |1/2 | 161 |37.5% | 162 |3/8 | 163 |
resizeCanvas.js | 167 |
168 |
169 | |
170 | 75% | 171 |3/4 | 172 |50% | 173 |1/2 | 174 |100% | 175 |1/1 | 176 |75% | 177 |3/4 | 178 |
restoreFromHistory.js | 182 |
183 |
184 | |
185 | 0% | 186 |0/4 | 187 |0% | 188 |0/2 | 189 |0% | 190 |0/1 | 191 |0% | 192 |0/4 | 193 |
setCanvasHTMLElementDimensions.js | 197 |
198 |
199 | |
200 | 90% | 201 |9/10 | 202 |50% | 203 |2/4 | 204 |100% | 205 |2/2 | 206 |100% | 207 |9/9 | 208 |
styles.js | 212 |
213 |
214 | |
215 | 100% | 216 |11/11 | 217 |100% | 218 |2/2 | 219 |100% | 220 |3/3 | 221 |100% | 222 |11/11 | 223 |
transformContextMatrix.js | 227 |
228 |
229 | |
230 | 95% | 231 |19/20 | 232 |50% | 233 |1/2 | 234 |100% | 235 |3/3 | 236 |95% | 237 |19/20 | 238 |
55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |
57 |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 | |
55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |
57 |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 | |