├── .gitignore ├── LICENSE ├── README.md ├── icons └── ShaderBalls.png ├── manifest.json ├── orb.png ├── package.json ├── privacy-policy └── README.md ├── readme-assets ├── ShaderBalls.png └── ShaderBalls.webp └── shaderballs.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ShaderBalls 2 | 3 | ![](readme-assets/ShaderBalls.webp) 4 | 5 | **🎛️ - Your PRs are always welcome! Accepting anything that extends vanilla shadertoy.com functionality.** 6 | 7 | ## What it does 8 | 9 | This extension just includes: 10 | 11 | * A firefox/chromium extension to easily preview your shadertoy.com shaders on 3D objects. 12 | * Free, public-domain/CC0-1.0, opensource. 13 | * New button to trigger 3D preview 14 | 15 | ![](readme-assets/ShaderBalls.png) 16 | 17 | ## How does it function 18 | 19 | * This add-on injects JavaScript into https://shadertoy.com pages so it can add new UX and features. 20 | * Powered by popular open-source libs [THREE.js](https://github.com/mrdoob/three.js) and [Camera-Controls](https://github.com/yomotsu/camera-controls). 21 | * Under the hood it uses the the THREE.CanvasTexture to easily map the live-rendered shader onto 3D objects. 22 | 23 | ## Dev Instructions FireFox 24 | 25 | ```bash 26 | git clone git@github.com:SoundSafari/ShaderBalls.git 27 | cd Shaderballs 28 | npm install 29 | ``` 30 | 31 | To run the shaderballs extension: 32 | 33 | * Install the 34 | [web-ext](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Getting_started_with_web-ext) 35 | tool. At the command line, open the ShaderBall's folder and type 36 | `web-ext run`. This launches Firefox and installs the extension automatically. 37 | This tool provides some additional development features, such as 38 | [automatic reloading](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Getting_started_with_web-ext#Automatic_extension_reloading). 39 | 40 | Or.. 41 | 42 | * Open Firefox and load the `about:debugging` page. Click 43 | [Load Temporary Add-on](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Temporary_Installation_in_Firefox) 44 | and select the `manifest.json` file within the ShaderBalls folder. 45 | Here is a [video](https://www.youtube.com/watch?v=cer9EUKegG4) 46 | that demonstrates how to do this. 47 | 48 | ## Dev Instructions Chromium 49 | 50 | ```bash 51 | git clone git@github.com:SoundSafari/ShaderBalls.git 52 | cd Shaderballs 53 | npm install 54 | ``` 55 | 56 | 1. In your browser navigate to `chrome://extensions`. 57 | 2. Enable developer-mode (top-right). 58 | 3. Load unpacked extension button > select the ShaderBalls folder. 59 | 60 | For chrome, each time you make a code update, you'll need to manually refresh the extension from the `chrome://extensions` page and refresh any active tab(s) that were running ShaderBalls. 61 | -------------------------------------------------------------------------------- /icons/ShaderBalls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoundSafari/ShaderBalls/f4c03c61c572b6c3b6d68df470c00d31ea66f767/icons/ShaderBalls.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "description": "View your shaders textured onto spheres. Adds new UX to shadertoy.com so you can draft equirectangular/spherical shaders more easily", 4 | "manifest_version": 3, 5 | "name": "Shaderballs", 6 | "version": "4.20.0", 7 | "icons": { 8 | "256": "icons/ShaderBalls.png" 9 | }, 10 | 11 | "content_scripts": [ 12 | { 13 | "matches": ["*://www.shadertoy.com/*"], 14 | "js": ["shaderballs.js"], 15 | "run_at": "document_idle" 16 | } 17 | ], 18 | 19 | "web_accessible_resources": [{ 20 | "resources":[ 21 | "node_modules/three/build/three.module.min.js", 22 | "node_modules/camera-controls/dist/camera-controls.module.min.js", 23 | "orb.png" 24 | ], 25 | "matches": ["*://www.shadertoy.com/*"] 26 | }] 27 | } 28 | -------------------------------------------------------------------------------- /orb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoundSafari/ShaderBalls/f4c03c61c572b6c3b6d68df470c00d31ea66f767/orb.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ShaderBalls", 3 | "version": "4.20.0", 4 | "description": "A firefox/chromium extension to easily preview your shadertoy.com shaders on 3D objects.\n\n(Free, public-domain/CC0-1.0, opensource)\n**This add-on injects JavaScript into https://shadertoy.com pages.** ", 5 | "main": "shaderballs.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "SoundSafari - Linux Nation LLC", 10 | "license": "CC0-1.0", 11 | "dependencies": { 12 | "camera-controls": "^2.8.5", 13 | "three": "^0.165.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /privacy-policy/README.md: -------------------------------------------------------------------------------- 1 | # ShaderBalls Privacy Policy 2 | 3 | This web-extension does not collect, send, report or save any user related data. 4 | It therefore promises to continue no-data collection practices and as such is 5 | never in a position to sell, trade, or transmit (in any way) information about 6 | users and web-extension usage. 7 | 8 | Furthermore ShaderBalls promises to never share, sell, or trade any data 9 | Google collects about installs/uninstalls, impressions, users or ratings. 10 | 11 | It is the intent of ShaderBalls to add functionality to the [shadertoy.com](https://www.shadertoy.com) shader editor and it is able to do that without any user or 12 | usage data. As such no data of any kind is actively collected and any data collected 13 | as byproduct of publishing this extension on the Chrome Web Store shall be strictly 14 | kept private and never exchanged with any other parties. 15 | -------------------------------------------------------------------------------- /readme-assets/ShaderBalls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoundSafari/ShaderBalls/f4c03c61c572b6c3b6d68df470c00d31ea66f767/readme-assets/ShaderBalls.png -------------------------------------------------------------------------------- /readme-assets/ShaderBalls.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoundSafari/ShaderBalls/f4c03c61c572b6c3b6d68df470c00d31ea66f767/readme-assets/ShaderBalls.webp -------------------------------------------------------------------------------- /shaderballs.js: -------------------------------------------------------------------------------- 1 | /* // Ascii art credit https://textart.sh/topic/ball 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 | const logBalls = ["🪩","⚽️","⚾️","🏀","🏈","🎾","🎱","🏉","🏐","🔮","🥎","🧶"]; 37 | const randBall = () => {return logBalls[Math.floor(Math.random() * logBalls.length)]}; 38 | 39 | console.log(`${randBall()} : Shaderballs is ballin!`); 40 | 41 | const injectionPt = document.getElementById("myFullScreen"); 42 | const ourButton = document.createElement("img"); 43 | ourButton.src = chrome.runtime.getURL("orb.png"); 44 | // Doesn't work lmfao => ourButton.style.backgroundImage = `url('${chrome.runtime.getURL("orb.png")}')`; 45 | ourButton.style.width = "20px"; 46 | ourButton.style.height = "20px"; 47 | ourButton.style.cursor = "pointer"; 48 | ourButton.className="uiButton"; 49 | injectionPt.parentElement.insertBefore(ourButton, injectionPt); 50 | const shadertoyCanvas = document.getElementById("demogl"); 51 | if(injectionPt && shadertoyCanvas) { 52 | (async () => { 53 | const srcTHREE = chrome.runtime.getURL("node_modules/three/build/three.module.min.js"); 54 | const srcCameraCtrls = chrome.runtime.getURL("node_modules/camera-controls/dist/camera-controls.module.min.js"); 55 | const THREE = await import(srcTHREE); 56 | let CameraControls = await import(srcCameraCtrls); 57 | CameraControls = CameraControls.default; 58 | // Dev Note : Cannot use vanilla THREE.js camera-controls b/c module-maps are not possible 59 | // Seee GH issue tradgedy https://github.com/WICG/import-maps/issues/92 60 | CameraControls.install({THREE: THREE}); 61 | 62 | ourButton.addEventListener("mousedown", () => { 63 | console.log(`${randBall()} : OrbToy Created.`); 64 | const babyWindow = document.createElement("div"); 65 | babyWindow.className="shaderballs"; 66 | babyWindow.style.position = "absolute"; 67 | babyWindow.style.top = "40px"; 68 | babyWindow.style.left = "50%"; 69 | babyWindow.style.borderRadius = "16px"; 70 | babyWindow.style.backdropFilter = "blur(5px)"; 71 | babyWindow.style.border = "1px solid ivory"; 72 | babyWindow.style.display = "flex"; 73 | babyWindow.style.flexDirection = "column"; 74 | babyWindow.style.overflow = "hidden"; 75 | babyWindow.style.zIndex="69";//lol 76 | babyWindow.style.boxShadow = "0 4px 8px rgba(0, 0, 0, 0.1), 0 6px 20px rgba(0, 0, 0, 0.1)"; 77 | babyWindow.style.width = "50vh";//`${shadertoyCanvas.clientHeight}px`; 78 | babyWindow.style.height = "50vh";//`${shadertoyCanvas.clientHeight}px`; 79 | babyWindow.tabIndex = 1; 80 | const header = document.createElement("div"); 81 | header.style.display = "flex"; 82 | header.style.flexDirection = "row"; 83 | header.style.backgroundColor = "#f0f0f0"; 84 | header.style.width = "100%"; 85 | const title = document.createElement("div") 86 | title.innerText = "Shaderballs"; 87 | title.width = "100%"; 88 | title.style.fontFamily = "Lobster,Tahoma,Arial" 89 | title.style.fontWeight = "bold"; 90 | title.style.textAlign = "center"; 91 | title.style.padding = "5px"; 92 | title.style.fontSize="16px" 93 | const close = document.createElement("div"); 94 | close.innerText = "X"; 95 | close.style.fontSize="18px" 96 | close.style.fontWeight = "bold"; 97 | close.style.cursor = "pointer"; 98 | close.style.padding = "5px"; 99 | close.style.marginLeft = "auto"; 100 | close.style.marginRight = "5px"; 101 | close.fontWeight = "bold"; 102 | 103 | const canvas = document.createElement("canvas"); 104 | canvas.style.setProperty('height', '100%', 'important'); 105 | canvas.style.setProperty('width', '100%', 'important');; 106 | 107 | header.appendChild(title); 108 | header.appendChild(close); 109 | babyWindow.appendChild(header); 110 | babyWindow.appendChild(canvas); 111 | 112 | document.body.appendChild(babyWindow); 113 | const gl = new THREE.WebGLRenderer({ 114 | canvas, 115 | antialias: true, 116 | alpha: true, 117 | preserveDrawingBuffer: true 118 | }); 119 | gl.setClearColor(0xFFFFFF, 0); 120 | gl.setPixelRatio(window.devicePixelRatio); 121 | const height = canvas.clientHeight; 122 | const width = canvas.clientWidth; 123 | gl.setSize(width, height); 124 | // THREE.js assigns absolute PX values that we don't want so we re-assign them. 125 | canvas.style.setProperty('height', '100%', 'important'); 126 | canvas.style.setProperty('width', '100%', 'important'); 127 | gl.autoClear = false; 128 | const camera = new THREE.PerspectiveCamera( 129 | 47, 130 | width / height, 131 | 0.1, 132 | 200000 133 | ); 134 | camera.position.set(0, 0, 1); 135 | const ctrls = new CameraControls(camera, canvas) 136 | const scene = new THREE.Scene(); 137 | const orb = new THREE.Mesh( 138 | new THREE.SphereGeometry(0.3, 32, 32), 139 | new THREE.MeshBasicMaterial({ color: 0xFFFFFF, map: new THREE.CanvasTexture(shadertoyCanvas), side: THREE.DoubleSide}) 140 | ); 141 | orb.material.map.colorSpace = THREE.SRGBColorSpace; 142 | scene.add(orb); 143 | 144 | let frame = null; 145 | const clock = new THREE.Clock(); 146 | function render(){ 147 | ctrls.update(clock.getDelta()); 148 | orb.material.map.needsUpdate = true; 149 | gl.clear(); 150 | gl.render(scene, camera); 151 | frame = requestAnimationFrame(render); 152 | } 153 | frame = requestAnimationFrame(render); 154 | close.addEventListener("mousedown", (e) => { 155 | e.preventDefault(); 156 | e.stopPropagation(); 157 | 158 | // THIS NEEDS TO HAPPEN FIRST (Weird Chrome Bug) 159 | babyWindow.remove(); 160 | 161 | cancelAnimationFrame(frame); 162 | orb.material.map.dispose(); 163 | orb.material.dispose(); 164 | orb.geometry.dispose(); 165 | console.log(`${randBall()} : Removed Orb Toy`) 166 | }) 167 | 168 | let isDragging = false; 169 | let offsetX, offsetY; 170 | 171 | header.addEventListener('mousedown', (e) => { 172 | e.preventDefault(); 173 | e.stopPropagation(); 174 | isDragging = true; 175 | // Calculate the offset (difference) between the mouse position and the element's top-left corner 176 | offsetX = e.clientX - babyWindow.getBoundingClientRect().left; 177 | offsetY = e.clientY - babyWindow.getBoundingClientRect().top; 178 | header.style.cursor = 'grabbing'; 179 | focusBabyWindow(); 180 | }); 181 | 182 | document.addEventListener('mousemove', (e) => { 183 | if (isDragging) { 184 | // Calculate the new position of the element 185 | const x = e.clientX - offsetX; 186 | const y = Math.max(e.clientY - offsetY,0); 187 | // Set the new position of the element 188 | babyWindow.style.left = `${x}px`; 189 | babyWindow.style.top = `${y}px`; 190 | } 191 | }); 192 | 193 | window.addEventListener('mouseup', () => { 194 | isDragging = false; 195 | header.style.cursor = 'pointer'; 196 | }); 197 | 198 | const resizableWindow = document.getElementById('resizableWindow'); 199 | let isResizing = false; 200 | let originalWidth, originalHeight, originalX, originalY, originalMouseX, originalMouseY; 201 | let edge; 202 | canvas.addEventListener('resize', (e) => {}) 203 | babyWindow.addEventListener('mousedown', (e) => { 204 | const rect = babyWindow.getBoundingClientRect(); 205 | const offset = 10; // Distance from the edge to start resizing 206 | 207 | if (e.clientX >= rect.right - offset && e.clientY >= rect.bottom - offset) { 208 | edge = 'bottom-right'; 209 | } else if(e.clientX <= rect.left + offset && e.clientY >= rect.bottom - offset) { 210 | edge = 'bottom-left'; 211 | } else if (e.clientX >= rect.right - offset) { 212 | edge = 'right'; 213 | } else if (e.clientX <= rect.left + offset) { 214 | edge = 'left'; 215 | } else if (e.clientY >= rect.bottom - offset) { 216 | edge = 'bottom'; 217 | console.log("bottoms") 218 | } else { 219 | edge = null; 220 | } 221 | 222 | if (edge) { 223 | e.preventDefault(); 224 | e.stopPropagation(); 225 | isResizing = true; 226 | originalWidth = parseFloat(getComputedStyle(babyWindow, null).getPropertyValue('width').replace('px', '')); 227 | originalHeight = parseFloat(getComputedStyle(babyWindow, null).getPropertyValue('height').replace('px', '')); 228 | originalX = babyWindow.getBoundingClientRect().left; 229 | originalY = babyWindow.getBoundingClientRect().top; 230 | originalMouseX = e.pageX; 231 | originalMouseY = e.pageY; 232 | window.addEventListener('mousemove', resize); 233 | window.addEventListener('mouseup', stopResize); 234 | } 235 | }); 236 | 237 | function resize(e) { 238 | if (edge === 'right') { 239 | const width = originalWidth + (e.pageX - originalMouseX); 240 | if (width > 100) { // Minimum width 241 | babyWindow.style.width = width + 'px'; 242 | } 243 | } else if (edge === 'left') { 244 | const width = originalWidth - (e.pageX - originalMouseX); 245 | if (width > 100) { // Minimum width 246 | babyWindow.style.width = width + 'px'; 247 | babyWindow.style.left = originalX + (e.pageX - originalMouseX) + 'px'; 248 | } 249 | } else if (edge === 'bottom') { 250 | const height = originalHeight + (e.pageY - originalMouseY); 251 | if (height > 100) { // Minimum height 252 | babyWindow.style.height = height + 'px'; 253 | } 254 | } else if (edge === 'bottom-right') { 255 | const width = originalWidth + (e.pageX - originalMouseX); 256 | const height = originalHeight + (e.pageY - originalMouseY); 257 | if (width > 100) { // Minimum width 258 | babyWindow.style.width = width + 'px'; 259 | } 260 | if (height > 100) { // Minimum height 261 | babyWindow.style.height = height + 'px'; 262 | } 263 | } else if (edge === 'bottom-left') { 264 | const width = originalWidth - (e.pageX - originalMouseX); 265 | const height = originalHeight + (e.pageY - originalMouseY); 266 | if (width > 100) { // Minimum width 267 | babyWindow.style.width = width + 'px'; 268 | babyWindow.style.left = originalX + (e.pageX - originalMouseX) + 'px'; 269 | } 270 | if (height > 100) { // Minimum height 271 | babyWindow.style.height = height + 'px'; 272 | } 273 | } 274 | } 275 | function stopResize() { 276 | isResizing = false; 277 | window.removeEventListener('mousemove', resize); 278 | window.removeEventListener('mouseup', stopResize); 279 | camera.aspect = canvas.clientWidth / canvas.clientHeight; 280 | camera.updateProjectionMatrix() 281 | gl.setSize(canvas.clientWidth, canvas.clientHeight); 282 | canvas.style.setProperty('height', '100%', 'important'); 283 | canvas.style.setProperty('width', '100%', 'important'); 284 | } 285 | babyWindow.addEventListener('mousemove', (e) => { 286 | const rect = babyWindow.getBoundingClientRect(); 287 | const offset = 10; // Distance from the edge to change cursor 288 | 289 | if (e.clientX >= rect.right - offset && e.clientY >= rect.bottom - offset) { 290 | babyWindow.style.cursor = 'nwse-resize'; 291 | } else if (e.clientX <= rect.left + offset && e.clientY >= rect.bottom - offset) { 292 | babyWindow.style.cursor = 'nesw-resize'; 293 | } else if (e.clientX >= rect.right - offset) { 294 | babyWindow.style.cursor = 'ew-resize'; 295 | } else if (e.clientX <= rect.left + offset) { 296 | babyWindow.style.cursor = 'ew-resize'; 297 | } else if (e.clientY >= rect.bottom - offset) { 298 | babyWindow.style.cursor = 'ns-resize'; 299 | } else { 300 | babyWindow.style.cursor = 'default'; 301 | } 302 | }); 303 | babyWindow.addEventListener('focus', (e) => { 304 | focusBabyWindow(); 305 | }) 306 | function focusBabyWindow(){ 307 | //const balls = document.getElementsByClassName("shaderballs"); 308 | document.body.appendChild(babyWindow); 309 | } 310 | }) 311 | })(); 312 | 313 | } 314 | --------------------------------------------------------------------------------