├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── demo ├── demo.js ├── index.html ├── ripple-worklet.js └── style.css ├── package.json └── src ├── index.js ├── realm.js └── util.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | /package-lock.json 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution, 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google.com/conduct/). -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2018 Google Inc. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | CSS Paint Polyfill demo 3 | 4 |

5 | CSS Custom Paint / Paint Worklets polyfill 6 | npm 7 |

8 |

9 | 10 | A polyfill that brings Houdini's [CSS Custom Paint API] and Paint Worklets to all modern browsers (Edge, Firefox, Safari and Chrome). 11 | 12 | Performance is particularly good in Firefox and Safari, where this polyfill leverages `-webkit-canvas()` and `-moz-element()` for optimized rendering. For other browsers, framerate is governed by Canvas `toDataURL()` / `toBlob()` speed. 13 | 14 | As of version 3, this polyfill also includes basic implementations of `CSS.supports()`, `CSS.registerProperty()` and CSS unit functions (`CSS.px()` etc), which are injected in browsers without native support. 15 | 16 | ## What are Paint Worklets? 17 | 18 | Paint Worklets are JavaScript modules in which you can program custom graphics code. Once registered, they can be applied to elements using CSS: 19 | 20 |
21 | 22 | An example `box.js` worklet: 23 | 24 | ```js 25 | registerPaint('box', class { 26 | paint(ctx, geom, properties) { 27 | ctx.fillRect(0, 0, geom.width, geom.height) 28 | } 29 | }) 30 | ``` 31 | 32 | 33 | 34 | ... registered and applied on a page: 35 | 36 | ```js 37 | CSS.paintWorklet.addModule('./box.js') 38 | 39 | var el = document.querySelector('h1') 40 | el.style.background = 'paint(box)' 41 | ``` 42 | 43 |
44 | 45 | For a more complete example, see the [demo](https://github.com/GoogleChromeLabs/css-paint-polyfill/tree/master/demo). 46 | 47 | --- 48 | 49 | ## Installation & Usage 50 | 51 | ```html 52 | 53 | 54 | 55 | ``` 56 | 57 | Or with a bundler: 58 | 59 | ```js 60 | import 'css-paint-polyfill'; 61 | ``` 62 | 63 | ... or with ES Modules on the web: 64 | 65 | ```js 66 | import 'https://unpkg.com/css-paint-polyfill'; 67 | ``` 68 | 69 | --- 70 | 71 | ## Contributing 72 | 73 | See [CONTRIBUTING.md](https://github.com/GoogleChromeLabs/css-paint-polyfill/blob/master/CONTRIBUTING.md). 74 | 75 | To hack on the polyfill locally: 76 | 77 | ```sh 78 | git clone git@github.com:GoogleChromeLabs/css-paint-polyfill.git 79 | cd css-paint-polyfill 80 | npm i 81 | npm start 82 | # open http://localhost:5000 83 | ``` 84 | 85 | --- 86 | 87 |

88 | css-paint-polyfill 89 |

90 | 91 | [CSS Custom Paint API]: https://developers.google.com/web/updates/2018/01/paintapi 92 | -------------------------------------------------------------------------------- /demo/demo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * Unless required by applicable law or agreed to in writing, software 8 | * distributed under the License is distributed on an "AS IS" BASIS, 9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | * See the License for the specific language governing permissions and 11 | * limitations under the License. 12 | */ 13 | 14 | /* eslint-disable */ 15 | 16 | // Copied verbatim from houdini-samples: 17 | // https://github.com/GoogleChromeLabs/houdini-samples/blob/master/paint-worklet/ripple 18 | 19 | // Inject a tiny tranpiler to make IE11+ behave nicely: 20 | try { 21 | eval('class F {}'); 22 | } 23 | catch(e) { 24 | CSS.paintWorklet.transpile = window.transpilerLite; 25 | } 26 | 27 | var p = CSS.paintWorklet.addModule('./ripple-worklet.js'); 28 | if (p) { 29 | p.then(() => console.info('./ripple-worlet.js registered')); 30 | } 31 | 32 | if (!window.performance) window.performance = { now: Date.now.bind(Date) }; 33 | 34 | if (!window.requestAnimationFrame) window.requestAnimationFrame = function(cb) { return setTimeout(doAnim, cb); }; 35 | function doAnim(cb) { cb(performance.now()); } 36 | 37 | function ripple(evt) { 38 | var button = this, 39 | rect = button.getBoundingClientRect(), 40 | x = evt.clientX - rect.left, 41 | y = evt.clientY - rect.top, 42 | start = performance.now(); 43 | button.classList.add('animating'); 44 | requestAnimationFrame(function raf(now) { 45 | var count = Math.floor(now - start); 46 | button.style.cssText = '--ripple-x: ' + x + '; --ripple-y: ' + y + '; --animation-tick: ' + count + ';'; 47 | if (count > 1000) { 48 | button.classList.remove('animating'); 49 | button.style.cssText = '--animation-tick: 0;'; 50 | return; 51 | } 52 | requestAnimationFrame(raf); 53 | }) 54 | } 55 | [].forEach.call(document.querySelectorAll('.ripple'), function (el) { 56 | el.addEventListener('click', ripple); 57 | }); -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

9 | This is a demonstration of the CSS Paint Polyfill. 10 |

11 | 12 |

13 | Note: Currently, both Chrome and this polyfill run Paint Worklets in an isolated context on the main thread. 14 |

15 | 16 |
17 | Source: 18 |
demo.js
19 |
ripple-worklet.js
20 |
style.css
21 |
22 | 23 | 24 | 25 | 26 |
27 | 28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /demo/ripple-worklet.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * Unless required by applicable law or agreed to in writing, software 8 | * distributed under the License is distributed on an "AS IS" BASIS, 9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | * See the License for the specific language governing permissions and 11 | * limitations under the License. 12 | */ 13 | 14 | /* global registerPaint */ 15 | 16 | registerPaint('ripple', class { 17 | static get inputProperties() { 18 | return [ 19 | 'background-color', 20 | '--ripple-color', 21 | '--animation-tick', 22 | '--ripple-x', 23 | '--ripple-y', 24 | '--ripple-speed' 25 | ]; 26 | } 27 | static get contextOptions() { 28 | return { 29 | 30 | /** Proposed option to force using 1:1 pixel mapping instead of CSS Pixels. */ 31 | // nativePixels: true, 32 | 33 | /** Proposed option to disable the default upscaling via high resolution backing canvas. 34 | * 35 | * Note: if animating and crisp lines are less important, disabling scaling 36 | * improves polyfill performance, since it reduces the canvas size by 75%. 37 | */ 38 | scaling: false 39 | }; 40 | } 41 | paint(ctx, geom, properties) { 42 | const bgColor = properties.get('background-color').toString(); 43 | const rippleColor = properties.get('--ripple-color').toString(); 44 | const x = parseFloat(properties.get('--ripple-x').toString()); 45 | const y = parseFloat(properties.get('--ripple-y').toString()); 46 | const speed = parseFloat((properties.get('--ripple-speed') || '').toString()) || 1; 47 | let tick = parseFloat(properties.get('--animation-tick').toString()); 48 | tick *= speed; 49 | if (tick < 0) tick = 0; 50 | if (tick > 1000) tick = 1000; 51 | 52 | ctx.fillStyle = bgColor; 53 | ctx.fillRect(0, 0, geom.width, geom.height); 54 | 55 | ctx.fillStyle = rippleColor; 56 | ctx.globalAlpha = 1 - tick / 1000; 57 | ctx.arc( 58 | x, y, // center 59 | geom.width * tick / 1000, // radius 60 | 0, // startAngle 61 | 2 * Math.PI //endAngle 62 | ); 63 | ctx.fill(); 64 | } 65 | }); -------------------------------------------------------------------------------- /demo/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font: 16px/1.3 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 3 | } 4 | pre { 5 | margin: 0; 6 | } 7 | .ripple { 8 | position: relative; 9 | width: 300px; 10 | height: 300px; 11 | border-radius: 50%; 12 | margin: 10px; 13 | font-size: 5em; 14 | line-height: 1; 15 | background-color: rgb(255,64,129); 16 | border: 0; 17 | box-shadow: 0 1px 1.5px 0 rgba(0,0,0,.12),0 1px 1px 0 rgba(0,0,0,.24); 18 | color: white; 19 | --ripple-x: 0; 20 | --ripple-y: 0; 21 | --ripple-color: rgba(255,255,255,0.54); 22 | --animation-tick: 0; 23 | } 24 | .spin::before { 25 | content: ''; 26 | display: block; 27 | position: absolute; 28 | box-sizing: border-box; 29 | left: 0; 30 | top: 0; 31 | width: 100%; 32 | height: 100%; 33 | border-radius: 50%; 34 | border: 2px dashed rgba(255,255,255,0.5); 35 | pointer-events: none; 36 | animation: spin 60s linear forwards infinite; 37 | } 38 | .ripple:nth-of-type(2) { 39 | --ripple-color: rgba(0,0,0,0.8); 40 | --ripple-speed: 2; 41 | } 42 | .ripple:nth-of-type(3) { 43 | font-size: 3em; 44 | width: 200px; 45 | height: 200px; 46 | background-color: #55815b; 47 | --ripple-color: #5315cf; 48 | } 49 | @keyframes spin { 50 | 0% { transform: rotate(0deg); } 51 | 100% { transform: rotate(360deg); } 52 | } 53 | @media (min-width: 600px) { 54 | .ripple { 55 | background-color: rgb(129,64,255); 56 | } 57 | } 58 | .ripple:focus { 59 | outline: none; 60 | } 61 | .ripple.animating { 62 | background-image: paint(ripple); 63 | } 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "css-paint-polyfill", 3 | "version": "3.4.0", 4 | "description": "A polyfill for the CSS Paint API, with special browser optimizations.", 5 | "source": "src/index.js", 6 | "main": "dist/css-paint-polyfill.js", 7 | "scripts": { 8 | "build": "microbundle -f iife && cp -r demo dist/", 9 | "start": "concurrently serve \"microbundle watch -f iife\"", 10 | "test": "eslint src", 11 | "release": "npm run -s build && npm t && git commit -am $npm_package_version && git tag $npm_package_version && git push && git push --tags && npm publish", 12 | "deploy": "cp -rf demo build && cp -r dist build/ && sed -i '' 's/\\.\\.\\/dist/dist/' build/index.html && gh-pages -d build && rm -rf build" 13 | }, 14 | "eslintConfig": { 15 | "extends": "eslint-config-developit", 16 | "rules": { 17 | "prefer-spread": 0 18 | } 19 | }, 20 | "files": [ 21 | "src", 22 | "dist" 23 | ], 24 | "keywords": [ 25 | "paint", 26 | "worklet", 27 | "polyfill", 28 | "houdini", 29 | "custom paint", 30 | "css paint", 31 | "paint worklet", 32 | "worklet" 33 | ], 34 | "author": "Google Chrome Developers ", 35 | "license": "Apache-2.0", 36 | "devDependencies": { 37 | "concurrently": "^3.5.1", 38 | "eslint": "^7.29.0", 39 | "eslint-config-developit": "^1.2.0", 40 | "gh-pages": "^1.1.0", 41 | "microbundle": "^0.5.1", 42 | "serve": "^11.3.0", 43 | "transpiler-lite": "gist:781ef9620da8a30228b9f0c6e21fa7f6" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * Unless required by applicable law or agreed to in writing, software 8 | * distributed under the License is distributed on an "AS IS" BASIS, 9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | * See the License for the specific language governing permissions and 11 | * limitations under the License. 12 | */ 13 | 14 | import { Realm } from './realm'; 15 | import { defineProperty, fetchText } from './util'; 16 | 17 | let paintWorklet; 18 | 19 | let CSS = window.CSS; 20 | if (!CSS) window.CSS = CSS = {}; 21 | 22 | if (!CSS.supports) CSS.supports = function s(property, value) { 23 | if (property == 'paint') return true; 24 | if (value) { 25 | const el = styleIsolationFrame.contentDocument.body; 26 | el.style.cssText = property + ':' + value; 27 | return el.style.cssText.length > 0; 28 | } 29 | let tokenizer = /(^|not|(or)|(and))\s*\(\s*(.+?)\s*:(.+?)\)\s*|(.)/gi, 30 | comparison, v, t, n; 31 | // [, not, or, and, key, value, unknown] 32 | while ((t = tokenizer.exec(property))) { 33 | if (t[6]) return false; 34 | n = s(t[4], t[5]); 35 | v = t[2] ? (v || n) : t[3] ? (v && n) : (comparison = !t[1], n); 36 | } 37 | return v == comparison; 38 | }; 39 | 40 | if (!CSS.escape) CSS.escape = s => s.replace(/([^\w-])/g,'\\$1'); 41 | 42 | /** @type {{ [name: string]: { name: string, syntax: string, inherits: boolean, initialValue: string }} } */ 43 | const CSS_PROPERTIES = {}; 44 | if (!CSS.registerProperty) CSS.registerProperty = function (def) { 45 | CSS_PROPERTIES[def.name] = def; 46 | }; 47 | 48 | // Minimal poorlyfill for CSS properties+values 49 | function CSSUnitValue(value, unit) { 50 | const num = parseFloat(value); 51 | this.value = isNaN(num) ? value : num; 52 | this.unit = unit; 53 | } 54 | CSSUnitValue.prototype.toString = function() { 55 | return this.value + (this.unit == 'number' ? '' : this.unit); 56 | }; 57 | CSSUnitValue.prototype.valueOf = function() { 58 | return this.value; 59 | }; 60 | 61 | 'Hz Q ch cm deg dpcm dpi ddpx em ex fr grad in kHz mm ms number pc percent pt px rad rem s turn vh vmax vmin vw'.split(' ').forEach(unit => { 62 | if (!CSS[unit]) { 63 | CSS[unit] = v => new CSSUnitValue(v, unit); 64 | } 65 | }); 66 | 67 | // Matches CSS properties that can accept a paint() value: 68 | const IMAGE_CSS_PROPERTIES = /(background|mask|cursor|-image|-source)/; 69 | 70 | const supportsPaintWorklet = !!CSS.paintWorklet; 71 | if (!supportsPaintWorklet) { 72 | paintWorklet = new PaintWorklet(); 73 | defineProperty(CSS, 'paintWorklet', { 74 | enumerable: true, 75 | configurable: true, 76 | get: () => paintWorklet 77 | }); 78 | } 79 | 80 | const GLOBAL_ID = 'css-paint-polyfill'; 81 | 82 | let root = document.createElement(GLOBAL_ID); 83 | if (!supportsPaintWorklet) { 84 | document.documentElement.appendChild(root); 85 | } 86 | 87 | let styleIsolationFrame = document.createElement('iframe'); 88 | styleIsolationFrame.style.cssText = 'position:absolute; left:0; top:-999px; width:1px; height:1px;'; 89 | root.appendChild(styleIsolationFrame); 90 | 91 | let overridesStylesheet = document.createElement('style'); 92 | overridesStylesheet.id = GLOBAL_ID; 93 | overridesStylesheet.$$isPaint = true; 94 | root.appendChild(overridesStylesheet); 95 | let overrideStyles = overridesStylesheet.sheet; 96 | let testStyles = root.style; 97 | 98 | // when `true`, interception of styles is disabled 99 | let bypassStyleHooks = false; 100 | 101 | const EMPTY_ARRAY = []; 102 | const HAS_PAINT = /(paint\(|-moz-element\(#paint-|-webkit-canvas\(paint-|[('"]blob:[^'"#]+#paint=|[('"]data:image\/paint-)/; 103 | const USE_CSS_CANVAS_CONTEXT = 'getCSSCanvasContext' in document; 104 | const USE_CSS_ELEMENT = (testStyles.backgroundImage = `-moz-element(#${GLOBAL_ID})`) === testStyles.backgroundImage; 105 | const HAS_PROMISE = (typeof Promise === 'function'); 106 | testStyles.cssText = 'display:none !important;'; 107 | 108 | let defer = window.requestAnimationFrame || setTimeout; 109 | let getDevicePixelRatio = () => window.devicePixelRatio || 1; 110 | 111 | let painters = {}; 112 | let trackedRules = {}; 113 | let styleSheetCounter = 0; 114 | 115 | function registerPaint(name, Painter, worklet) { 116 | // if (painters[name]!=null) throw Error(`registerPaint(${name}): name already registered`); 117 | painters[name] = { 118 | worklet, 119 | Painter, 120 | properties: Painter.inputProperties ? [].slice.call(Painter.inputProperties) : [], 121 | bit: 0, 122 | instances: [] 123 | }; 124 | let query = ''; 125 | for (let i=overrideStyles.cssRules.length; i--; ) { 126 | const rule = overrideStyles.cssRules[i]; 127 | if (rule.style.cssText.indexOf('-' + name) !== -1) { 128 | query += rule.selectorText; 129 | } 130 | } 131 | if (query) processItem(query, true); 132 | } 133 | 134 | function getPainter(name) { 135 | let painter = painters[name]; 136 | // if (painter == null) throw Error(`No paint defined for "${name}"`); 137 | return painter; 138 | } 139 | 140 | function getPainterInstance(painter) { 141 | // Alternate between two instances. 142 | // @TODO should alternate between two *worklets*. Class instances are meaningless for perf. 143 | let inst = painter.bit ^= 1; 144 | return painter.instances[inst] || (painter.instances[inst] = new painter.Painter()); 145 | } 146 | 147 | function paintRuleWalker(rule, context) { 148 | let css = rule.cssText; 149 | const hasPaint = HAS_PAINT.test(css); 150 | 151 | if (context.isNew === true && hasPaint) { 152 | if (css !== (css = escapePaintRules(css))) { 153 | rule = replaceRule(rule, css); 154 | } 155 | } 156 | 157 | // Hello future self! 158 | // This eager exit avoids tracking unpainted rules. 159 | // That seems reasonable, but it wasn't in place in 3.0... 160 | // Perhaps I'm missing something, if so, I apologize. 161 | if (!hasPaint) return; 162 | 163 | let selector = rule.selectorText, 164 | cssText = getCssText(rule.style), 165 | index, key, cached; 166 | 167 | if (context.counters[selector] == null) { 168 | index = context.counters[selector] = 1; 169 | } 170 | else { 171 | index = ++context.counters[selector]; 172 | } 173 | key = 'sheet' + context.sheetId + '\n' + selector + '\n' + index; 174 | if (trackedRules[key] != null) { 175 | cached = trackedRules[key]; 176 | if (cached.selector === selector) { 177 | cached.rule = rule; 178 | if (cached.cssText !== cssText) { 179 | context.toProcess.push(cached); 180 | } 181 | return; 182 | } 183 | context.toRemove.push(cached); 184 | } 185 | else { 186 | cached = trackedRules[key] = { key, selector, cssText, properties: {}, rule }; 187 | context.toProcess.push(cached.selector); 188 | } 189 | } 190 | 191 | function walk(node, iterator) { 192 | if ('ownerSVGElement' in node) return; 193 | iterator(node); 194 | let child = node.firstElementChild; 195 | while (child) { 196 | walk(child, iterator); 197 | child = child.nextElementSibling; 198 | } 199 | } 200 | 201 | function update() { 202 | let sheets = [].slice.call(document.styleSheets), 203 | context = { 204 | toProcess: [], 205 | toRemove: [], 206 | counters: {}, 207 | isNew: false, 208 | sheetId: null, 209 | // this property is unused - it's assigned to in order to prevent Terser from removing the try/catch on L220 210 | rules: null 211 | }, 212 | invalidateAll; 213 | 214 | for (let i=0; i0) { 242 | processItem(context.toProcess.join(', ')); 243 | } 244 | 245 | // If a new stylesheet is injected, invalidate all geometry and paint output. 246 | if (invalidateAll) { 247 | processItem('[data-css-paint]', true); 248 | } 249 | } 250 | 251 | function walkStyles(sheet, iterator, context) { 252 | let stack = [[0, sheet.cssRules]], 253 | current = stack[0], 254 | rules = current[1]; 255 | if (rules) { 256 | for (let j=0; stack.length>0; j++) { 257 | if (j>=rules.length) { 258 | stack.pop(); 259 | let len = stack.length; 260 | if (len > 0) { 261 | current = stack[len - 1]; 262 | rules = current[1]; 263 | j = current[0]; 264 | } 265 | continue; 266 | } 267 | current[0] = j; 268 | let rule = rules[j]; 269 | // process @import rules (requires re-fetching) 270 | if (rule.type === 3) { 271 | if (rule.$$isPaint) continue; 272 | const mq = rule.media && rule.media.mediaText; 273 | if (mq && !self.matchMedia(mq).matches) continue; 274 | // don't refetch google font stylesheets 275 | if (/ts\.g.{7}is\.com\/css/.test(rule.href)) continue; 276 | rule.$$isPaint = true; 277 | fetchText(rule.href, processRemoteSheet); 278 | continue; 279 | } 280 | if (rule.type !== 1) { 281 | if (rule.cssRules && rule.cssRules.length>0) { 282 | stack.push([0, rule.cssRules]); 283 | } 284 | continue; 285 | } 286 | let r = iterator(rule, context); 287 | if (r!==undefined) context = r; 288 | } 289 | } 290 | return context; 291 | } 292 | 293 | function parseCss(css) { 294 | let parent = styleIsolationFrame.contentDocument.body; 295 | let style = document.createElement('style'); 296 | style.media = 'print'; 297 | style.$$paintid = ++styleSheetCounter; 298 | style.appendChild(document.createTextNode(css)); 299 | parent.appendChild(style); 300 | style.sheet.remove = () => parent.removeChild(style); 301 | return style.sheet; 302 | } 303 | 304 | function replaceRule(rule, newRule) { 305 | let sheet = rule.parentStyleSheet, 306 | parent = rule.parentRule, 307 | rules = (parent || sheet).cssRules, 308 | index = rules.length - 1; 309 | for (let i=0; i<=index; i++) { 310 | if (rules[i] === rule) { 311 | (parent || sheet).deleteRule(i); 312 | index = i; 313 | break; 314 | } 315 | } 316 | if (newRule!=null) { 317 | if (parent) { 318 | let index = parent.appendRule(newRule); 319 | return parent.cssRules[index]; 320 | } 321 | sheet.insertRule(newRule, index); 322 | return sheet.cssRules[index]; 323 | } 324 | } 325 | 326 | // Replace paint(id) with url(data:image/paint-id) for a newly detected stylesheet 327 | function processNewSheet(node) { 328 | if (node.$$isPaint) return; 329 | 330 | if (node.href) { 331 | fetchText(node.href, processRemoteSheet); 332 | return false; 333 | } 334 | 335 | for (let i=node.childNodes.length; i--; ) { 336 | let css = node.childNodes[i].nodeValue; 337 | let escaped = escapePaintRules(css); 338 | if (escaped !== css) { 339 | node.childNodes[i].nodeValue = escaped; 340 | } 341 | } 342 | } 343 | 344 | function processRemoteSheet(css) { 345 | let sheet = parseCss(escapePaintRules(css)); 346 | 347 | // In Firefox, accessing .cssRules in a stylesheet with pending @import rules fails. 348 | // Try to wait for them to resolve, otherwise try again after a long delay. 349 | try { 350 | sheet._ = sheet.cssRules.length; 351 | } 352 | catch (e) { 353 | let next = () => { 354 | if (sheet) processRemoteSheetRules(sheet); 355 | sheet = null; 356 | clearTimeout(timer); 357 | }; 358 | sheet.ownerNode.onload = sheet.ownerNode.onerror = next; 359 | let timer = setTimeout(next, 5000); 360 | return; 361 | } 362 | 363 | processRemoteSheetRules(sheet); 364 | } 365 | 366 | function processRemoteSheetRules(sheet) { 367 | let newSheet = ''; 368 | walkStyles(sheet, (rule) => { 369 | if (rule.type !== 1) return; 370 | let css = ''; 371 | for (let i=0; i el && el.$$needsOverrides === true); 433 | if (disable) disableOverrides(); 434 | while (updateQueue.length) { 435 | let el = updateQueue.pop(); 436 | if (el) maybeUpdateElement(el); 437 | } 438 | if (disable) enableOverrides(); 439 | } 440 | 441 | function processItem(selector, forceInvalidate) { 442 | try { 443 | let sel = document.querySelectorAll(selector); 444 | for (let i=0; i { 452 | if (--count) return; 453 | callback.apply(null, args || EMPTY_ARRAY); 454 | }; 455 | for (let i=0; i\s]/g, ''); 544 | if (typeof CSS[s] === 'function') v = CSS[s](v); 545 | } 546 | // Safari returns whitespace around values: 547 | if (typeof v === 'string') v = v.trim(); 548 | return v; 549 | }, 550 | getRaw(name) { 551 | if (name in propertyContainerCache) return propertyContainerCache[name]; 552 | let v = currentProperties.getPropertyValue(name); 553 | // Safari returns whitespace around values: 554 | if (typeof v === 'string') v = v.trim(); 555 | return propertyContainerCache[name] = v; 556 | } 557 | }; 558 | 559 | // Get element geometry, relying on cached values if possible. 560 | function getElementGeometry(element) { 561 | return element.$$paintGeometry || (element.$$paintGeometry = { 562 | width: element.clientWidth, 563 | height: element.clientHeight, 564 | live: false 565 | }); 566 | } 567 | 568 | const resizeObserver = window.ResizeObserver && new window.ResizeObserver((entries) => { 569 | for (let i=0; i resolve = r); 901 | } 902 | 903 | fetchText(url, code => { 904 | let context = { 905 | registerPaint(name, Painter) { 906 | registerPaint(name, Painter, { 907 | context, 908 | realm 909 | }); 910 | } 911 | }; 912 | defineProperty(context, 'devicePixelRatio', { 913 | get: getDevicePixelRatio 914 | }); 915 | context.self = context; 916 | let parent = styleIsolationFrame.contentDocument && styleIsolationFrame.contentDocument.body || root; 917 | let realm = new Realm(context, parent); 918 | 919 | code = (this.transpile || String)(code); 920 | 921 | realm.exec(code); 922 | if (resolve) resolve(); 923 | }); 924 | 925 | return p; 926 | }; 927 | 928 | function init() { 929 | let lock = false; 930 | new MutationObserver(records => { 931 | if (lock===true || overrideLocks) return; 932 | lock = true; 933 | for (let i = 0; i < records.length; i++) { 934 | let record = records[i], target = record.target, added, removed; 935 | // Ignore all inline SVG mutations: 936 | if (target && 'ownerSVGElement' in target) { 937 | continue; 938 | } 939 | if (record.type === 'childList') { 940 | if ((added = record.addedNodes)) { 941 | for (let j = 0; j < added.length; j++) { 942 | if (added[j].nodeType === 1) { 943 | // Newly inserted elements can contain entire subtrees 944 | // if constructed before the root is attached. Only the root 945 | // emits a mutation, so we have to visit all children: 946 | walk(added[j], queueUpdate); 947 | } 948 | } 949 | } 950 | if ((removed = record.removedNodes)) { 951 | for (let j = 0; j < removed.length; j++) { 952 | if (resizeObserver && removed[j].$$paintGeometry) { 953 | removed[j].$$paintGeometry.live = false; 954 | if (resizeObserver) resizeObserver.unobserve(removed[j]); 955 | } 956 | } 957 | } 958 | } 959 | else if (record.type==='attributes' && target.nodeType === 1) { 960 | // prevent removal of data-css-paint attribute 961 | if (record.attributeName === 'data-css-paint' && record.oldValue && target.$$paintId != null && !target.getAttribute('data-css-paint')) { 962 | ensurePaintId(target); 963 | continue; 964 | } 965 | walk(target, invalidateElementGeometry); 966 | } 967 | } 968 | lock = false; 969 | }).observe(document.body, { 970 | childList: true, 971 | attributes: true, 972 | attributeOldValue: true, 973 | subtree: true 974 | }); 975 | 976 | const setAttributeDesc = Object.getOwnPropertyDescriptor(Element.prototype, 'setAttribute'); 977 | const oldSetAttribute = setAttributeDesc.value; 978 | setAttributeDesc.value = function(name, value) { 979 | if (name === 'style' && HAS_PAINT.test(value)) { 980 | value = escapePaintRules(value); 981 | ensurePaintId(this); 982 | this.$$needsOverrides = true; 983 | invalidateElementGeometry(this); 984 | } 985 | return oldSetAttribute.call(this, name, value); 986 | }; 987 | defineProperty(Element.prototype, 'setAttribute', setAttributeDesc); 988 | 989 | // avoid frameworks removing the data-css-paint attribute: 990 | const removeAttributeDesc = Object.getOwnPropertyDescriptor(Element.prototype, 'removeAttribute'); 991 | const oldRemoveAttribute = removeAttributeDesc.value; 992 | removeAttributeDesc.value = function(name) { 993 | if (name === 'data-css-paint') return; 994 | return oldRemoveAttribute.call(this, name); 995 | }; 996 | defineProperty(Element.prototype, 'removeAttribute', removeAttributeDesc); 997 | 998 | let styleDesc = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'style'); 999 | const oldStyleGetter = styleDesc.get; 1000 | styleDesc.set = function(value) { 1001 | const style = styleDesc.get.call(this); 1002 | return style.cssText = value; 1003 | }; 1004 | styleDesc.get = function() { 1005 | const style = oldStyleGetter.call(this); 1006 | if (style.ownerElement !== this) { 1007 | defineProperty(style, 'ownerElement', { value: this }); 1008 | } 1009 | return style; 1010 | }; 1011 | defineProperty(HTMLElement.prototype, 'style', styleDesc); 1012 | 1013 | /** @type {PropertyDescriptorMap} */ 1014 | const propDescs = {}; 1015 | 1016 | let cssTextDesc = Object.getOwnPropertyDescriptor(CSSStyleDeclaration.prototype, 'cssText'); 1017 | let oldSet = cssTextDesc.set; 1018 | cssTextDesc.set = function (value) { 1019 | if (!overrideLocks && HAS_PAINT.test(value)) { 1020 | value = value && escapePaintRules(value); 1021 | const owner = this.ownerElement; 1022 | if (owner) { 1023 | ensurePaintId(owner); 1024 | owner.$$needsOverrides = true; 1025 | invalidateElementGeometry(owner); 1026 | } 1027 | } 1028 | return oldSet.call(this, value); 1029 | }; 1030 | propDescs.cssText = cssTextDesc; 1031 | 1032 | const properties = Object.keys((window.CSS2Properties || CSSStyleDeclaration).prototype).filter(m => IMAGE_CSS_PROPERTIES.test(m)); 1033 | properties.forEach((prop) => { 1034 | const n = prop.replace(/([A-Z])/g, '-$1').toLowerCase(); 1035 | propDescs[prop] = { 1036 | configurable: true, 1037 | enumerable: true, 1038 | get() { 1039 | let pri = this.getPropertyPriority(n); 1040 | return this.getPropertyValue(n) + (pri ? ' !'+pri : ''); 1041 | }, 1042 | set(value) { 1043 | const v = String(value).match(/^(.*?)\s*(?:!\s*(important)\s*)?$/); 1044 | this.setProperty(n, v[1], v[2]); 1045 | return this[prop]; 1046 | } 1047 | }; 1048 | }); 1049 | 1050 | let setPropertyDesc = Object.getOwnPropertyDescriptor(CSSStyleDeclaration.prototype, 'setProperty'); 1051 | let oldSetProperty = setPropertyDesc.value; 1052 | setPropertyDesc.value = function (name, value, priority) { 1053 | if (!bypassStyleHooks && !overrideLocks && HAS_PAINT.test(value)) { 1054 | value = value && escapePaintRules(value); 1055 | const owner = this.ownerElement; 1056 | if (owner) { 1057 | ensurePaintId(owner); 1058 | owner.$$needsOverrides = true; 1059 | invalidateElementGeometry(owner); 1060 | } 1061 | } 1062 | oldSetProperty.call(this, name, value, priority); 1063 | }; 1064 | propDescs.setProperty = setPropertyDesc; 1065 | 1066 | Object.defineProperties(CSSStyleDeclaration.prototype, propDescs); 1067 | if (window.CSS2Properties) { 1068 | Object.defineProperties(window.CSS2Properties.prototype, propDescs); 1069 | } 1070 | 1071 | addEventListener('resize', () => { 1072 | processItem('[data-css-paint]'); 1073 | }); 1074 | 1075 | const OPTS = { passive: true }; 1076 | 1077 | [ 1078 | 'animationiteration', 1079 | 'animationend', 1080 | 'animationstart', 1081 | 'transitionstart', 1082 | 'transitionend', 1083 | 'transitionrun', 1084 | 'transitioncancel', 1085 | 'mouseover', 1086 | 'mouseout', 1087 | 'mousedown', 1088 | 'mouseup', 1089 | 'focus', 1090 | 'blur' 1091 | ].forEach(event => { 1092 | addEventListener(event, updateFromEvent, OPTS); 1093 | }); 1094 | 1095 | function updateFromEvent(e) { 1096 | let t = e.target; 1097 | while (t) { 1098 | if (t.nodeType === 1) queueUpdate(t); 1099 | t = t.parentNode; 1100 | } 1101 | } 1102 | 1103 | update(); 1104 | processItem('[style*="paint"]'); 1105 | } 1106 | 1107 | if (!supportsPaintWorklet) { 1108 | try { 1109 | init(); 1110 | } 1111 | catch (e) { 1112 | } 1113 | } 1114 | -------------------------------------------------------------------------------- /src/realm.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * Unless required by applicable law or agreed to in writing, software 8 | * distributed under the License is distributed on an "AS IS" BASIS, 9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | * See the License for the specific language governing permissions and 11 | * limitations under the License. 12 | */ 13 | 14 | export function Realm(scope, parentElement) { 15 | let frame = document.createElement('iframe'); 16 | frame.style.cssText = 'position:absolute; left:0; top:-999px; width:1px; height:1px;'; 17 | parentElement.appendChild(frame); 18 | let win = frame.contentWindow, 19 | doc = win.document, 20 | vars = 'var window,$hook'; 21 | for (let i in win) { 22 | if (!(i in scope) && i!=='eval') { 23 | vars += ','; 24 | vars += i; 25 | } 26 | } 27 | for (let i in scope) { 28 | vars += ','; 29 | vars += i; 30 | vars += '=self.'; 31 | vars += i; 32 | } 33 | let script = doc.createElement('script'); 34 | script.appendChild(doc.createTextNode( 35 | `function $hook(self,console) {"use strict"; 36 | ${vars};return function() {return eval(arguments[0])}}` 37 | )); 38 | doc.body.appendChild(script); 39 | this.exec = win.$hook(scope, console); 40 | // this.destroy = () => { parentElement.removeChild(frame); }; 41 | } 42 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * Unless required by applicable law or agreed to in writing, software 8 | * distributed under the License is distributed on an "AS IS" BASIS, 9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | * See the License for the specific language governing permissions and 11 | * limitations under the License. 12 | */ 13 | 14 | /** Canvas#toBlob() ponyfill */ 15 | export function canvasToBlob(canvas, callback, type, quality) { 16 | if (canvas.toBlob) return canvas.toBlob(callback, type, quality); 17 | 18 | let bin = atob(canvas.toDataURL(type, quality).split(',')[1]), 19 | arr = new Uint8Array(bin.length); 20 | for (let i=0; i r.text() ) */ 25 | export function fetchText(url, callback) { 26 | let xhr = new XMLHttpRequest(); 27 | xhr.onreadystatechange = () => { 28 | if (xhr.readyState===4) { 29 | callback(xhr.responseText); 30 | } 31 | }; 32 | xhr.open('GET', url, true); 33 | xhr.send(); 34 | } 35 | 36 | /** Object.defineProperty() ponyfill */ 37 | export function defineProperty(obj, name, def) { 38 | if (Object.defineProperty) { 39 | Object.defineProperty(obj, name, def); 40 | } 41 | else { 42 | obj[name] = def.get(); 43 | } 44 | } 45 | --------------------------------------------------------------------------------