├── .gitignore ├── LICENSE ├── README.md ├── default_lightmap.png ├── default_lightmap_large.png ├── demo-src ├── index.js ├── lzw.js └── model-compression.js ├── demo-worker-src ├── gifenc.d.ts ├── gifenc.js ├── index.js └── jsconfig.json ├── docs ├── example.txt ├── favicon.png ├── index.html ├── index.js ├── models.html └── worker.js ├── font.gif ├── jsconfig.json ├── package-lock.json ├── package.json ├── rollup.config-demo.mjs ├── rollup.config.mjs └── src ├── color.js ├── environment.js ├── image.js ├── index.js ├── lighting.js ├── model-data-parser.js ├── model-gl-loader.js ├── model-parser.js ├── model.js ├── parser-utils.js ├── pass.js ├── pico.js ├── shader-program.js └── text.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # Mac 107 | .DS_Store 108 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Luca Harris 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebGL viewer for picoCAD files 2 | 3 | ## [lucatronica.github.io/picocad-web-viewer/](https://lucatronica.github.io/picocad-web-viewer/) 4 | ## [luca.games/picocad/](https://luca.games/picocad/) 5 | 6 | ## Light-maps 7 | 8 | Color palettes and shading can be customized using light-maps. 9 | 10 | ![Enlarged default picoCAD light-map](default_lightmap_large.png) 11 | 12 | A light-map must be 32 pixels wide, but can be any height. 13 | 14 | Each 2-pixel-column specifies the shading for one color. 15 | 16 | * Each row is a shading level, going from light-to-dark from top-to-bottom. 17 | * The pair of pixels in each row are used to control dithering. 18 | * To have no dithering make both colors the same. 19 | * The order of the columns correspond to the PICO-8 color indices. 20 | 21 | The default picoCAD light-map is a good starting point for customization: ![Default picoCAD light-map](default_lightmap.png) 22 | 23 | [See here for examples of custom light-maps](https://luca.games/picocad/light-maps/). 24 | 25 | ## Usage 26 | 27 | The model viewer can be freely used in other contexts. 28 | 29 | ```js 30 | // Example usage // 31 | 32 | import PicoCADViewer from "./pico-cad-viewer.esm.js"; 33 | 34 | const myCanvas = document.getElementByID("my-canvas"); 35 | 36 | const viewer = new PicoCadViewer({ 37 | canvas: myCanvas, 38 | }); 39 | 40 | // Load models from file, string or URL. 41 | viewer.load("./my-model.txt"); 42 | 43 | // Draw the model manually or start a draw loop. 44 | if (oneShot) { 45 | viewer.draw(); 46 | } else { 47 | let spin = 0; 48 | 49 | viewer.startDrawLoop((dt) => { 50 | // This callback is called before every frame is drawn. 51 | spin += dt; 52 | viewer.setTurntableCamera(8, spin, 0.1); 53 | viewer.setLightDirectionFromCamera(); 54 | }); 55 | } 56 | ``` 57 | -------------------------------------------------------------------------------- /default_lightmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucatronica/picocad-web-viewer/fcbd8a2fa94ccfcdf2dc5b273c836c1afc03fe3b/default_lightmap.png -------------------------------------------------------------------------------- /default_lightmap_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucatronica/picocad-web-viewer/fcbd8a2fa94ccfcdf2dc5b273c836c1afc03fe3b/default_lightmap_large.png -------------------------------------------------------------------------------- /demo-src/index.js: -------------------------------------------------------------------------------- 1 | import PicoCADViewer from "../src/index"; 2 | import { urlCompressModel, urlDecompressModel } from "./model-compression"; 3 | import { PICO_COLORS } from "../src/pico"; 4 | 5 | // Get elements 6 | const texCanvas = /** @type {HTMLCanvasElement} */(document.getElementById("texture")); 7 | const texHDImage = /** @type {HTMLImageElement} */(document.getElementById("texture-hd")); 8 | const viewportCanvas = /** @type {HTMLCanvasElement} */(document.getElementById("viewport")); 9 | const inputResolution = /** @type {HTMLSelectElement} */(document.getElementById("input-resolution")); 10 | const inputAutoTurn = /** @type {HTMLInputElement} */(document.getElementById("input-auto-turn")); 11 | const inputWireframe = /** @type {HTMLInputElement} */(document.getElementById("input-wireframe")); 12 | const inputWireframeColor = /** @type {HTMLInputElement} */(document.getElementById("input-wireframe-color")); 13 | const inputRenderMode = /** @type {HTMLSelectElement} */(document.getElementById("input-render-mode")); 14 | const inputBackgroundColor = /** @type {HTMLInputElement} */(document.getElementById("input-background-color")); 15 | const inputBackgroundColorEnabled = /** @type {HTMLInputElement} */(document.getElementById("input-background-color-enabled")); 16 | const inputBackgroundTransparent = /** @type {HTMLInputElement} */(document.getElementById("input-background-transparent")); 17 | const inputShading = /** @type {HTMLInputElement} */(document.getElementById("input-shading")); 18 | const inputFOV = /** @type {HTMLInputElement} */(document.getElementById("input-fov")); 19 | const inputHDContainer = /** @type {HTMLElement} */(document.getElementById("hd-controls")); 20 | const inputHDSteps = /** @type {HTMLInputElement} */(document.getElementById("input-hd-steps")); 21 | const inputHDAmbient = /** @type {HTMLInputElement} */(document.getElementById("input-hd-ambient")); 22 | const btnShowControls = /** @type {HTMLButtonElement} */(document.getElementById("btn-show-controls")); 23 | const inputGifFps = /** @type {HTMLInputElement} */(document.getElementById("input-gif-fps")); 24 | const inputOutlineSize = /** @type {HTMLInputElement} */(document.getElementById("input-outline-size")); 25 | const inputOutlineColor = /** @type {HTMLInputElement} */(document.getElementById("input-outline-color")); 26 | const inputWatermark = /** @type {HTMLInputElement} */(document.getElementById("input-watermark")); 27 | const btnRecordGIF = /** @type {HTMLButtonElement} */(document.getElementById("btn-record-gif")); 28 | const gifStatusEl = /** @type {HTMLButtonElement} */(document.getElementById("gif-status")); 29 | const popupControls = document.getElementById("popup-controls"); 30 | const popupImageOptions = document.getElementById("popup-image-options"); 31 | const statsTable = document.getElementById("stats"); 32 | 33 | // Load worker. 34 | let worker = new Worker("worker.js"); 35 | let workerLoaded = false; 36 | 37 | worker.onmessage = (event) => { 38 | let data = event.data; 39 | 40 | // Handle response to message. 41 | let type = data.type; 42 | 43 | if (type === "gif") { 44 | gifStatusEl.hidden = true; 45 | 46 | downloadGif(data.data); 47 | } 48 | 49 | // Once loaded, enabled recording etc. 50 | if (!workerLoaded) { 51 | workerLoaded = true; 52 | btnRecordGIF.disabled = false; 53 | } 54 | }; 55 | 56 | // Create viewer 57 | const pcv = new PicoCADViewer({ 58 | canvas: viewportCanvas, 59 | }); 60 | window["viewer"] = pcv; 61 | 62 | // App/renderer state 63 | let cameraSpin = -Math.PI / 2; 64 | let cameraRoll = 0.2; 65 | let cameraRadius = 12; 66 | let cameraTurntableSpeed = 0.75; 67 | let cameraTurntableCenter = {x: 0, y: 1.5, z: 0}; 68 | let cameraTurntableAuto = true; 69 | let cameraMode = 0; 70 | 71 | 72 | // Model load wrapper. 73 | 74 | /** 75 | * @param {import("../src/index").PicoCADSource} source 76 | */ 77 | async function loadModel(source, saveToURL=true) { 78 | // Load the model. 79 | const model = await pcv.load(source); 80 | window["model"] = model; 81 | 82 | console.log(`=== load "${model.name}" ===`); 83 | 84 | // Enable UI hints. 85 | viewportCanvas.classList.add("loaded"); 86 | 87 | // Set turntable radius from zoom level. 88 | cameraRadius = model.zoomLevel; 89 | 90 | // Draw texture. 91 | texCanvas.hidden = false; 92 | texHDImage.hidden = true; 93 | texCanvasCtx.putImageData(pcv.getModelTexture(), 0, 0); 94 | 95 | // Reset custom state. 96 | pcv.removeHDTexture(); 97 | 98 | // Show stats 99 | const faceCount = model.objects.reduce((acc, obj) => acc + obj.faces.length, 0); 100 | 101 | while (statsTable.lastChild != null) statsTable.lastChild.remove(); 102 | 103 | statsTable.append(h("li", { class: "filename" }, pcv.model.name), h("br")); 104 | 105 | const stats = { 106 | // "Colors": pcv.getTextureColorCount(), 107 | "Objects": model.objects.length, 108 | "Faces": faceCount, 109 | }; 110 | 111 | console.log(`${pcv.getTriangleCount()} triangles, ${pcv.getDrawCallCount()} draw calls`); 112 | 113 | for (const [key, value] of Object.entries(stats)) { 114 | statsTable.append(h("li", {}, ` ${key}: ${value}`)); 115 | } 116 | 117 | // Add compressed model text to URL. 118 | if (saveToURL) { 119 | const compressed = urlCompressModel(model); 120 | history.pushState(null, "", "#" + compressed); 121 | console.log(`lzw base64: ${compressed.length} bytes`); 122 | } 123 | } 124 | 125 | 126 | // Example model. 127 | 128 | function loadExample(saveToURL=true) { 129 | loadModel("./example.txt", saveToURL); 130 | } 131 | 132 | 133 | // Palette loading 134 | 135 | let hdImageObjectURL = null; 136 | 137 | /** 138 | * @param {File} file 139 | */ 140 | function loadImage(file) { 141 | if (hdImageObjectURL != null) { 142 | URL.revokeObjectURL(hdImageObjectURL); 143 | } 144 | 145 | hdImageObjectURL = URL.createObjectURL(file); 146 | 147 | const img = new Image(); 148 | img.onload = () => { 149 | // Get ImageData. 150 | const canvas = document.createElement("canvas"); 151 | canvas.id = "image-drop-preview"; 152 | canvas.width = img.naturalWidth; 153 | canvas.height = img.naturalHeight; 154 | const ctx = canvas.getContext("2d"); 155 | ctx.drawImage(img, 0, 0); 156 | const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); 157 | 158 | // Check texture info. 159 | const isPico8 = isPico8Texture(imageData); 160 | const isLightMap = imageData.width === 32; 161 | 162 | // Get actions we can make. 163 | /** @type {[string, number, () => void][]} */ 164 | const actions = []; 165 | 166 | // Light map action 167 | if (isLightMap) { 168 | actions.push(["Light-map", 0, () => { 169 | pcv.setLightMap(imageData); 170 | texCanvasCtx.putImageData(pcv.getModelTexture(), 0, 0); 171 | },]); 172 | 173 | // These have been hidden for simplicity. 174 | // Maybe show in expandable list 175 | // actions.push(["Texture light-map", 1, () => { 176 | // pcv.setTextureLightMap(imageData); 177 | // texCanvasCtx.putImageData(pcv.getModelTexture(), 0, 0); 178 | // },]); 179 | } 180 | 181 | // Basic texture action 182 | actions.push(["Texture", 0, () => { 183 | texCanvas.hidden = true; 184 | texHDImage.hidden = false; 185 | texHDImage.src = hdImageObjectURL; 186 | 187 | if (isPico8) { 188 | // PICO-8 style texture 189 | inputHDContainer.hidden = true; 190 | pcv.removeHDTexture(); 191 | pcv.setIndexTexture(imageData); 192 | } else { 193 | // HD Texture 194 | inputHDContainer.hidden = false; 195 | pcv.setHDTexture(imageData); 196 | } 197 | }]); 198 | 199 | // If just one action, execute it right away. 200 | if (actions.length === 1) { 201 | actions[0][2](); 202 | return; 203 | } 204 | 205 | // Show actions in popup. 206 | popupImageOptions.hidden = false; 207 | const el = popupImageOptions.firstElementChild; 208 | el.innerHTML = ""; 209 | 210 | el.append( 211 | canvas, 212 | h("p", {}, "How should this image be used?"), 213 | ); 214 | 215 | for (const [label, ident, callback] of actions) { 216 | const btn = h("div", {class: "lil-btn"}, label); 217 | btn.style.marginLeft = `${ident * 30}px`; 218 | btn.onclick = () => { 219 | callback(); 220 | popupImageOptions.hidden = true; 221 | }; 222 | el.append(btn); 223 | } 224 | }; 225 | 226 | img.src = hdImageObjectURL; 227 | } 228 | 229 | 230 | /** 231 | * @param {number} r 232 | * @param {number} g 233 | * @param {number} b 234 | */ 235 | function rgbToInt(r, g, b) { 236 | return 0xff000000 | (b << 16) | (g << 8) | r; 237 | } 238 | 239 | /** 240 | * @param {ImageData} imageData 241 | */ 242 | function isPico8Texture(imageData) { 243 | const colors = new Set(PICO_COLORS.map(([r, g, b]) => rgbToInt(r, g, b))); 244 | 245 | const ints = new Int32Array(imageData.data.buffer); 246 | 247 | for (let i = 0, n = ints.length; i < n; i++) { 248 | const int = ints[i]; 249 | if (!colors.has(int)) { 250 | return false; 251 | } 252 | } 253 | 254 | return true; 255 | } 256 | 257 | 258 | // Extra model loading steps + stats 259 | 260 | const texCanvasCtx = texCanvas.getContext("2d"); 261 | 262 | 263 | // Popups 264 | 265 | document.querySelectorAll(".popup").forEach(/** @type {(el: HTMLElement) => void} */(popup) => { 266 | popup.addEventListener("click", (event) => { 267 | if (event.target === popup) { 268 | popup.hidden = !popup.hidden; 269 | } 270 | }); 271 | }); 272 | 273 | btnShowControls.onclick = () => { 274 | popupControls.hidden = !popupControls.hidden; 275 | }; 276 | 277 | popupControls.querySelectorAll("kbd").forEach(kbd => { 278 | kbd.onclick = () => keyPressed(kbd.textContent.toLowerCase()); 279 | }); 280 | 281 | 282 | // GIF recording 283 | 284 | const GIF_MAX_LEN = 30; // seconds 285 | 286 | /** Delay between frames in seconds. */ 287 | let gifDelay = 0.02; 288 | let gifRecording = false; 289 | let gifTime = 0; 290 | let gifInitialSpin = 0; 291 | 292 | function toggleGifRecording() { 293 | if (gifRecording) { 294 | stopRecordingGif(); 295 | } else { 296 | startRecordingGif(); 297 | } 298 | } 299 | 300 | function startRecordingGif() { 301 | gifRecording = true; 302 | gifTime = 0; 303 | gifInitialSpin = cameraSpin; 304 | 305 | btnRecordGIF.textContent = "Recording GIF..."; 306 | btnRecordGIF.classList.add("recording"); 307 | viewportCanvas.classList.add("recording"); 308 | inputGifFps.disabled = true; 309 | } 310 | 311 | function stopRecordingGif() { 312 | gifRecording = false; 313 | 314 | btnRecordGIF.textContent = "Record GIF"; 315 | btnRecordGIF.classList.remove("recording"); 316 | viewportCanvas.classList.remove("recording"); 317 | inputGifFps.disabled = false; 318 | gifStatusEl.hidden = false; 319 | 320 | // Render GIF in worker. 321 | let resolution = pcv.getResolution(); 322 | let background = pcv.getRenderedBackgroundColor(); 323 | let palette = null; 324 | let transparentIndex = -1; 325 | 326 | // If possible, determine the global color palette. 327 | if (!pcv.hasHDTexture()) { 328 | palette = pcv.getPalette(); 329 | 330 | if (palette.length > 256) { 331 | palette = null; 332 | } else { 333 | // Have a palette, handle transparent background index. 334 | // A bit hacky, we know that the background index is at the end of the `pcv.getPalette()` array. 335 | if (pcv.backgroundColor != null && pcv.backgroundColor[3] < 1) { 336 | transparentIndex = palette.length - 1; 337 | } 338 | } 339 | } 340 | 341 | worker.postMessage({ 342 | type: "generate", 343 | width: resolution.width, 344 | height: resolution.height, 345 | scale: resolution.scale, 346 | delay: Math.round(gifDelay * 1000), 347 | background: background, 348 | palette: palette, 349 | transparentIndex: transparentIndex, 350 | }); 351 | } 352 | 353 | /** 354 | * @param {Uint8Array} bytes 355 | */ 356 | function downloadGif(bytes) { 357 | const fileName = `${pcv.model.name}.gif`; 358 | 359 | const file = new File([ bytes ], fileName, { 360 | type: "image/gif", 361 | }); 362 | 363 | const url = URL.createObjectURL(file); 364 | 365 | const a = document.createElement("a"); 366 | a.href = url; 367 | a.download = fileName; 368 | document.body.append(a); 369 | a.click(); 370 | 371 | a.remove(); 372 | URL.revokeObjectURL(url); 373 | 374 | console.log(`downloaded ${fileName} ${file.size / 1024}kb`); 375 | } 376 | 377 | // /** @type {{ scale: number, indices?: Uint8Array, pixels?: Uint8Array }[]} */ 378 | // let gifFrames = []; 379 | 380 | function putGifFrame() { 381 | let data = pcv.getPixels(); 382 | 383 | worker.postMessage({ 384 | type: "frame", 385 | data: data, 386 | }, [ data.buffer ]); 387 | } 388 | 389 | 390 | // Input 391 | 392 | /** 393 | * @param {string} key 394 | */ 395 | function keyPressed(key) { 396 | if (key === "r") { 397 | inputWireFrameHandler(!pcv.drawWireframe); 398 | } else if (key === "t") { 399 | inputAutoTurnHandler(!cameraTurntableAuto) 400 | } else if (key === "m") { 401 | inputRenderModeHandler(inputRenderMode.value === "texture" ? "color" : "texture"); 402 | } else if (key === "l") { 403 | inputShadingHandler(!inputShading.checked); 404 | } else if (key === "/" || key === "?") { 405 | loadExample(); 406 | } else if (key === "g") { 407 | toggleGifRecording(); 408 | } else if (key === "pause") { 409 | pcv.stopDrawLoop(); 410 | viewportCanvas.style.opacity = "0.5"; 411 | } 412 | } 413 | 414 | const keys = Object.create(null); 415 | 416 | window.onkeydown = event => { 417 | if (event.target === document.body && !event.ctrlKey && !event.metaKey) { 418 | event.preventDefault(); 419 | const key = event.key.toLowerCase(); 420 | keys[key] = true; 421 | 422 | keyPressed(key); 423 | } 424 | }; 425 | window.onkeyup = event => { 426 | if (!event.ctrlKey && !event.metaKey) { 427 | event.preventDefault(); 428 | keys[event.key.toLowerCase()] = false; 429 | } 430 | }; 431 | 432 | viewportCanvas.ondblclick = () => { 433 | if (pcv.loaded) { 434 | /** @type {HTMLElement} */( document.activeElement )?.blur(); 435 | 436 | viewportCanvas.requestPointerLock(); 437 | } 438 | }; 439 | 440 | viewportCanvas.oncontextmenu = (event) => { 441 | event.preventDefault(); 442 | } 443 | 444 | document.onpointerlockchange = (event) => { 445 | if (document.pointerLockElement === viewportCanvas) { 446 | cameraMode = 1; 447 | } else { 448 | cameraMode = 0; 449 | } 450 | }; 451 | 452 | 453 | // Viewport mouse controls. 454 | 455 | let mouseDown = /** @type {boolean[]} */(Array(5)).fill(false); 456 | let mouseDownViewport = /** @type {boolean[]} */(Array(5)).fill(false); 457 | let mouse = [0, 0]; 458 | 459 | window.onmousedown = (event) => { 460 | const button = event.button; 461 | const isViewport = event.target == viewportCanvas; 462 | 463 | mouseDown[button] = true; 464 | mouseDownViewport[button] = isViewport; 465 | 466 | if (isViewport) { 467 | event.preventDefault(); 468 | 469 | viewportCanvas.classList.add("grabbing"); 470 | 471 | if (cameraMode === 0 && button === 0) { 472 | inputAutoTurnHandler(false); 473 | } 474 | } 475 | }; 476 | 477 | window.onmouseup = (event) => { 478 | const button = event.button; 479 | 480 | mouseDown[button] = false; 481 | mouseDownViewport[button] = false; 482 | 483 | viewportCanvas.classList.remove("grabbing"); 484 | }; 485 | 486 | window.onmousemove = (event) => { 487 | const mouseNow = [event.clientX, event.clientY]; 488 | const mouseDelta = [mouseNow[0] - mouse[0], mouseNow[1] - mouse[1]]; 489 | 490 | if (cameraMode === 1 && document.pointerLockElement === viewportCanvas) { 491 | const sensitivity = 0.003; 492 | 493 | cameraSpin += event.movementX * sensitivity; 494 | cameraRoll += event.movementY * sensitivity; 495 | } else if (cameraMode == 0) { 496 | if (mouseDownViewport[0]) { 497 | const sensitivity = 0.005; 498 | 499 | cameraSpin += mouseDelta[0] * sensitivity; 500 | cameraRoll += mouseDelta[1] * sensitivity; 501 | } else if (mouseDownViewport[1] || mouseDownViewport[2]) { 502 | const sensitivity = 0.005; 503 | 504 | const up = pcv.getCameraUp(); 505 | const right = pcv.getCameraRight(); 506 | const rightDelta = mouseDelta[0] * sensitivity; 507 | const upDelta = -mouseDelta[1] * sensitivity; 508 | 509 | cameraTurntableCenter.x += right.x * rightDelta + up.x * upDelta; 510 | cameraTurntableCenter.y += right.y * rightDelta + up.y * upDelta; 511 | cameraTurntableCenter.z += right.z * rightDelta + up.z * upDelta; 512 | } 513 | } 514 | 515 | mouse = mouseNow; 516 | }; 517 | 518 | viewportCanvas.onwheel = (event) => { 519 | event.preventDefault(); 520 | 521 | const dy = clamp(-6, 6, event.deltaY) 522 | 523 | if (cameraMode === 1 || (cameraMode === 0 && event.altKey)) { 524 | inputFOVUpdate(pcv.cameraFOV + dy); 525 | } else if (cameraMode === 0) { 526 | cameraRadius = clamp(0, 200, cameraRadius + dy * 0.5); 527 | } 528 | }; 529 | 530 | 531 | // Viewport touch controls. 532 | 533 | let currTouch = [0, 0, -1]; 534 | let touchViewport = false; 535 | 536 | document.addEventListener("touchstart", (event) => { 537 | touchViewport = event.target == viewportCanvas; 538 | 539 | const touch = event.changedTouches[0]; 540 | 541 | currTouch = [touch.clientX, touch.clientY, touch.identifier]; 542 | 543 | if (touchViewport) { 544 | event.preventDefault(); 545 | 546 | inputAutoTurnHandler(false); 547 | } 548 | }, { passive: false }); 549 | 550 | document.addEventListener("touchmove", (event) => { 551 | const touch = Array.from(event.changedTouches).find(touch => touch.identifier === currTouch[2]); 552 | 553 | if (touch != null) { 554 | const newTouch = [touch.clientX, touch.clientY, touch.identifier] 555 | 556 | if (touchViewport) { 557 | const delta = [newTouch[0] - currTouch[0], newTouch[1] - currTouch[1]]; 558 | const sensitivity = 0.01; 559 | 560 | cameraSpin += delta[0] * sensitivity; 561 | cameraRoll += delta[1] * sensitivity; 562 | } 563 | 564 | currTouch = newTouch; 565 | } 566 | }); 567 | 568 | document.addEventListener("touchend", (event) => { 569 | const touch = Array.from(event.changedTouches).find(touch => touch.identifier === currTouch[2]); 570 | 571 | if (touch != null) { 572 | touchViewport = false; 573 | } 574 | }); 575 | 576 | 577 | // Controls. 578 | 579 | if (window.innerWidth < 700) { 580 | inputResolution.value = "128,128,2"; 581 | } 582 | 583 | inputHandler(inputResolution, value => { 584 | const [w, h, scale] = value.split(",").map(s => Number(s)); 585 | 586 | pcv.setResolution(w, h, scale); 587 | }); 588 | 589 | const inputFOVUpdate = inputHandler(inputFOV, () => { 590 | pcv.cameraFOV = inputFOV.valueAsNumber; 591 | inputFOV.nextElementSibling.textContent = inputFOV.value; 592 | }); 593 | 594 | const inputWireFrameHandler = inputHandler(inputWireframe, () => { 595 | pcv.drawWireframe = inputWireframe.checked; 596 | }); 597 | 598 | const inputAutoTurnHandler = inputHandler(inputAutoTurn, () => { 599 | cameraTurntableAuto = inputAutoTurn.checked; 600 | }); 601 | 602 | inputHandler(inputWireframeColor, (value) => { 603 | pcv.wireframeColor = hexToRGB(value); 604 | }); 605 | 606 | inputHandler(inputBackgroundColorEnabled, updateCustomBackground); 607 | inputHandler(inputBackgroundColor, updateCustomBackground); 608 | inputHandler(inputBackgroundTransparent, updateCustomBackground); 609 | 610 | function updateCustomBackground() { 611 | if (inputBackgroundTransparent.checked) { 612 | pcv.backgroundColor = [0, 0, 0, 0]; 613 | } else { 614 | pcv.backgroundColor = inputBackgroundColorEnabled.checked ? hexToRGB(inputBackgroundColor.value) : null; 615 | } 616 | } 617 | 618 | const inputRenderModeHandler = inputHandler(inputRenderMode, value => { 619 | pcv.renderMode = /** @type {import("../src/index").PicoCADRenderMode} */(value); 620 | }); 621 | 622 | const inputShadingHandler = inputHandler(inputShading, () => { 623 | pcv.shading = inputShading.checked; 624 | }); 625 | 626 | const inputHDStepsHandler = inputHandler(inputHDSteps, () => { 627 | pcv.hdOptions.shadingSteps = inputHDSteps.valueAsNumber; 628 | inputHDSteps.nextElementSibling.textContent = inputHDSteps.value; 629 | }); 630 | 631 | const inputHDAmbientHandler = inputHandler(inputHDAmbient, (value) => { 632 | pcv.hdOptions.shadingColor = hexToRGB(value); 633 | }); 634 | 635 | const inputGifFpsHandler = inputHandler(inputGifFps, value => { 636 | gifDelay = Number(value) / 100; 637 | }); 638 | 639 | const inputOutlineSizeHandler = inputHandler(inputOutlineSize, () => { 640 | pcv.outlineSize = inputOutlineSize.valueAsNumber; 641 | }); 642 | 643 | const inputOutlineColorHandler = inputHandler(inputOutlineColor, (value) => { 644 | pcv.outlineColor = hexToRGB(value); 645 | }); 646 | 647 | const inputWatermarkHandler = inputHandler(inputWatermark, (value) => { 648 | pcv.setWatermark(value); 649 | }); 650 | 651 | btnRecordGIF.onclick = () => { 652 | toggleGifRecording(); 653 | }; 654 | 655 | 656 | // Render loop 657 | 658 | pcv.startDrawLoop((dt) => { 659 | // Camera controls 660 | const lookSpeed = 1.2 * dt; 661 | 662 | let inputLR = 0; 663 | let inputFB = 0; 664 | let inputUD = 0; 665 | let inputCameraLR = 0; 666 | let inputCameraUD = 0; 667 | if (keys["w"]) inputFB += 1; 668 | if (keys["s"]) inputFB -= 1; 669 | if (keys["a"]) inputLR -= 1; 670 | if (keys["d"]) inputLR += 1; 671 | if (keys["q"] || keys["shift"] || keys["control"]) inputUD -= 1; 672 | if (keys["e"] || keys[" "]) inputUD += 1; 673 | if (keys["arrowleft"]) inputCameraLR -= 1; 674 | if (keys["arrowright"]) inputCameraLR += 1; 675 | if (keys["arrowup"]) inputCameraUD -= 1; 676 | if (keys["arrowdown"]) inputCameraUD += 1; 677 | 678 | if (cameraMode === 0) { 679 | // turntable 680 | cameraRoll += (inputFB + inputCameraUD) * lookSpeed; 681 | cameraTurntableCenter.y += inputUD * 3 * dt; 682 | 683 | if (cameraTurntableAuto) { 684 | cameraTurntableSpeed -= (inputLR + inputCameraLR) * 2 * dt; 685 | cameraTurntableSpeed = clamp(-2, 2, cameraTurntableSpeed); 686 | 687 | cameraSpin += cameraTurntableSpeed * dt; 688 | } else { 689 | cameraSpin += (inputLR + inputCameraLR) * lookSpeed; 690 | } 691 | 692 | pcv.setTurntableCamera(cameraRadius, cameraSpin, cameraRoll, cameraTurntableCenter); 693 | } else if (cameraMode === 1) { 694 | // fps 695 | cameraSpin += inputCameraLR * lookSpeed; 696 | cameraRoll += inputCameraUD * lookSpeed; 697 | cameraRoll = clamp(-Math.PI / 2, Math.PI / 2, cameraRoll); 698 | 699 | pcv.cameraRotation.x = cameraRoll; 700 | pcv.cameraRotation.y = cameraSpin; 701 | 702 | if (inputLR !== 0 || inputUD !== 0 || inputFB !== 0) { 703 | const speed = 6 * dt; 704 | 705 | const right = pcv.getCameraRight(); 706 | const up = pcv.getCameraUp(); 707 | const forward = pcv.getCameraForward(); 708 | 709 | const pos = pcv.cameraPosition; 710 | pos.x += (right.x * inputLR + forward.x * inputFB + up.x * inputUD) * speed; 711 | pos.y += (right.y * inputLR + forward.y * inputFB + up.y * inputUD) * speed; 712 | pos.z += (right.z * inputLR + forward.z * inputFB + up.z * inputUD) * speed; 713 | } 714 | } 715 | 716 | pcv.setLightDirectionFromCamera(); 717 | }, (dt) => { 718 | if (gifRecording) { 719 | const prev = gifTime; 720 | gifTime += dt; 721 | 722 | if (gifTime >= GIF_MAX_LEN || (inputAutoTurn && Math.abs(gifInitialSpin - cameraSpin) >= Math.PI * 2)) { 723 | stopRecordingGif(); 724 | } else if (prev === 0 || Math.floor(prev / gifDelay) !== Math.floor(gifTime / gifDelay)) { 725 | putGifFrame(); 726 | } 727 | } 728 | }); 729 | 730 | 731 | // Handle file dropping. 732 | 733 | window.addEventListener("dragover", (event) => { 734 | event.preventDefault(); 735 | }); 736 | 737 | let dragDepthCount = 0; 738 | 739 | window.addEventListener("dragenter", (event) => { 740 | if (dragDepthCount === 0) { 741 | if (event.dataTransfer.types.includes("Files")) { 742 | document.body.classList.add("drag"); 743 | } 744 | } 745 | 746 | dragDepthCount++; 747 | }); 748 | 749 | 750 | window.addEventListener("dragleave", (event) => { 751 | dragDepthCount--; 752 | 753 | if (dragDepthCount === 0) { 754 | dragEnd(); 755 | } 756 | }); 757 | 758 | 759 | function dragEnd() { 760 | document.body.classList.remove("drag"); 761 | } 762 | 763 | window.addEventListener("drop", (event) => { 764 | event.preventDefault(); 765 | 766 | dragDepthCount = 0; 767 | 768 | dragEnd(); 769 | 770 | const file = event.dataTransfer.files[0]; 771 | if (file != null) { 772 | handleFile(file); 773 | } 774 | }); 775 | 776 | // Handling pasting. 777 | document.body.addEventListener("paste", (event) => { 778 | const s = event.clipboardData.getData("text/plain"); 779 | if (s != null && s.length > 0) { 780 | loadModel(s); 781 | } else { 782 | const file = event.clipboardData.files[0]; 783 | if (file != null) { 784 | handleFile(file); 785 | } 786 | } 787 | }); 788 | 789 | // File handler 790 | /** 791 | * @param {File} file 792 | */ 793 | function handleFile(file) { 794 | const i = file.name.lastIndexOf("."); 795 | const ext = i < 0 ? "" : file.name.slice(i + 1).toLowerCase(); 796 | if (ext === "png" || ext === "jpg" || ext === "jpeg" || ext === "bmp" || ext === "gif") { 797 | loadImage(file); 798 | } else { 799 | loadModel(file); 800 | } 801 | } 802 | 803 | /** 804 | * @param {number} a 805 | * @param {number} b 806 | * @param {number} x 807 | */ 808 | function clamp(a, b, x) { 809 | return x < a ? a : (x > b ? b : x); 810 | } 811 | 812 | /** 813 | * @param {string|HTMLElement} tag 814 | * @param {*} attributes 815 | * @param {...any} nodes 816 | */ 817 | function h(tag, attributes, ...nodes) { 818 | tag = tag instanceof HTMLElement ? tag : document.createElement(tag); 819 | if (attributes != null) { 820 | for (const k in attributes) { 821 | tag.setAttribute(k, attributes[k]); 822 | } 823 | } 824 | tag.append(...nodes); 825 | return tag; 826 | } 827 | 828 | /** 829 | * @param {HTMLSelectElement|HTMLInputElement} input 830 | * @param {(value: string, init: boolean) => void} onchange 831 | * @returns {(value: any) => void} Call to change value 832 | */ 833 | function inputHandler(input, onchange) { 834 | function listener() { 835 | onchange(input.value, false); 836 | } 837 | 838 | input[input instanceof HTMLSelectElement ? "onchange" : "oninput"] = listener; 839 | 840 | onchange(input.value, true); 841 | 842 | if (input instanceof HTMLInputElement) { 843 | return (value) => { 844 | if (input.disabled) return; 845 | if (typeof value === "boolean") { 846 | input.checked = value; 847 | } else { 848 | input.value = value; 849 | } 850 | listener(); 851 | }; 852 | } else { 853 | return (value) => { 854 | if (input.disabled) return; 855 | input.value = value; 856 | listener(); 857 | }; 858 | } 859 | } 860 | 861 | /** 862 | * @param {string} s 863 | * @returns {number[]} 864 | */ 865 | function hexToRGB(s) { 866 | return [ 867 | s.slice(1, 3), 868 | s.slice(3, 5), 869 | s.slice(5, 7), 870 | ].map(s => parseInt(s, 16) / 255);; 871 | } 872 | 873 | // Load starting model. 874 | 875 | if (!loadModelFromURL()) { 876 | loadExample(false); 877 | } 878 | 879 | function loadModelFromURL() { 880 | if (location.hash.length > 1) { 881 | loadModel(urlDecompressModel(location.hash.slice(1)), false); 882 | return true; 883 | } 884 | } 885 | 886 | onhashchange = (event) => { 887 | loadModelFromURL(); 888 | }; 889 | 890 | window["loadModel"] = loadModel; 891 | -------------------------------------------------------------------------------- /demo-src/lzw.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @license 3 | * Adapted from lzwCompress.js 4 | * 5 | * Copyright (c) 2012-2021 floydpink 6 | * Licensed under the MIT license. 7 | */ 8 | 9 | let _lzwLoggingEnabled = false; 10 | const _self = {}, 11 | _lzwLog = function (message) { 12 | try { 13 | console.log('lzwCompress: ' + 14 | (new Date()).toISOString() + ' : ' + (typeof (message) === 'object' ? JSON.stringify(message) : message)); 15 | } catch (e) { 16 | } 17 | }; 18 | 19 | // KeyOptimize 20 | // http://stackoverflow.com/questions/4433402/replace-keys-json-in-javascript 21 | (function (self) { 22 | let _keys = []; 23 | const comparer = function (key) { 24 | return function (e) { 25 | return e === key; 26 | }; 27 | }, 28 | inArray = function (array, comparer) { 29 | for (let i = 0; i < array.length; i++) { 30 | if (comparer(array[i])) { 31 | return true; 32 | } 33 | } 34 | return false; 35 | }, 36 | pushNew = function (array, element, comparer) { 37 | if (!inArray(array, comparer)) { 38 | array.push(element); 39 | } 40 | }, 41 | _extractKeys = function (obj) { 42 | if (typeof obj === 'object') { 43 | for (let key in obj) { 44 | if (!Array.isArray(obj)) { 45 | pushNew(_keys, key, comparer(key)); 46 | } 47 | _extractKeys(obj[key]); 48 | } 49 | } 50 | }, 51 | _encode = function (obj) { 52 | if (typeof obj !== 'object') { 53 | return obj; 54 | } 55 | for (let prop in obj) { 56 | if (!Array.isArray(obj)) { 57 | if (obj.hasOwnProperty(prop)) { 58 | obj[_keys.indexOf(prop)] = _encode(obj[prop]); 59 | delete obj[prop]; 60 | } 61 | } else { 62 | obj[prop] = _encode(obj[prop]); 63 | } 64 | } 65 | return obj; 66 | }, 67 | _decode = function (obj) { 68 | if (typeof obj !== 'object') { 69 | return obj; 70 | } 71 | for (let prop in obj) { 72 | if (!Array.isArray(obj)) { 73 | if (obj.hasOwnProperty(prop) && _keys[prop]) { 74 | obj[_keys[prop]] = _decode(obj[prop]); 75 | delete obj[prop]; 76 | } 77 | } else { 78 | obj[prop] = _decode(obj[prop]); 79 | } 80 | } 81 | return obj; 82 | }, 83 | compress = function (json) { 84 | _keys = []; 85 | const jsonObj = JSON.parse(json); 86 | _extractKeys(jsonObj); 87 | _lzwLoggingEnabled && _lzwLog('keys length : ' + _keys.length); 88 | _lzwLoggingEnabled && _lzwLog('keys : ' + _keys); 89 | return JSON.stringify({__k : _keys, __v : _encode(jsonObj)}); 90 | }, 91 | decompress = function (minifiedJson) { 92 | const obj = minifiedJson; 93 | if (typeof (obj) !== 'object') { 94 | return minifiedJson; 95 | } 96 | if (!obj.hasOwnProperty('__k')) { 97 | return JSON.stringify(obj); 98 | } 99 | _keys = obj.__k; 100 | return _decode(obj.__v); 101 | }; 102 | 103 | self.KeyOptimize = { 104 | pack : compress, 105 | unpack : decompress 106 | }; 107 | }(_self)); 108 | 109 | // LZWCompress 110 | // http://stackoverflow.com/a/2252533/218882 111 | // http://rosettacode.org/wiki/LZW_compression#JavaScript 112 | (function (self) { 113 | const compress = function (uncompressed) { 114 | if (typeof (uncompressed) !== 'string') { 115 | return uncompressed; 116 | } 117 | let i; 118 | const dictionary = Object.create(null); 119 | let c, 120 | wc, 121 | w = ''; 122 | const result = []; 123 | let dictSize = 256; 124 | for (i = 0; i < 256; i += 1) { 125 | dictionary[String.fromCharCode(i)] = i; 126 | } 127 | for (i = 0; i < uncompressed.length; i += 1) { 128 | c = uncompressed.charAt(i); 129 | wc = w + c; 130 | if (dictionary[wc]) { 131 | w = wc; 132 | } else { 133 | if (dictionary[w] === undefined) { 134 | return uncompressed; 135 | } 136 | result.push(dictionary[w]); 137 | dictionary[wc] = dictSize++; 138 | w = String(c); 139 | } 140 | } 141 | if (w !== '') { 142 | result.push(dictionary[w]); 143 | } 144 | return result; 145 | }, 146 | decompress = function (compressed) { 147 | if (!Array.isArray(compressed)) { 148 | return compressed; 149 | } 150 | let i; 151 | const dictionary = []; 152 | let w, 153 | result, 154 | k, 155 | entry = '', 156 | dictSize = 256; 157 | for (i = 0; i < 256; i += 1) { 158 | dictionary[i] = String.fromCharCode(i); 159 | } 160 | w = String.fromCharCode(compressed[0]); 161 | result = w; 162 | for (i = 1; i < compressed.length; i += 1) { 163 | k = compressed[i]; 164 | if (dictionary[k]) { 165 | entry = dictionary[k]; 166 | } else { 167 | if (k === dictSize) { 168 | entry = w + w.charAt(0); 169 | } else { 170 | return null; 171 | } 172 | } 173 | result += entry; 174 | dictionary[dictSize++] = w + entry.charAt(0); 175 | w = entry; 176 | } 177 | return result; 178 | }; 179 | 180 | self.LZWCompress = { 181 | pack : compress, 182 | unpack : decompress 183 | }; 184 | }(_self)); 185 | 186 | /** 187 | * @param {string} obj 188 | * @returns {number[]} 189 | */ 190 | export function compress(obj) { 191 | _lzwLoggingEnabled && _lzwLog('original (uncompressed) : ' + obj); 192 | if (!obj || obj === true || obj instanceof Date) { 193 | return obj; 194 | } 195 | let result = obj; 196 | if (typeof obj === 'object') { 197 | result = _self.KeyOptimize.pack(JSON.stringify(obj)); 198 | _lzwLoggingEnabled && _lzwLog('key optimized: ' + result); 199 | } 200 | const packedObj = _self.LZWCompress.pack(result); 201 | _lzwLoggingEnabled && _lzwLog('packed (compressed) : ' + packedObj); 202 | return packedObj; 203 | } 204 | 205 | /** 206 | * @param {number[]} compressedObj 207 | * @returns {string} 208 | */ 209 | export function decompress(compressedObj) { 210 | _lzwLoggingEnabled && _lzwLog('original (compressed) : ' + compressedObj); 211 | if (!compressedObj || compressedObj === true || compressedObj instanceof Date) { 212 | return compressedObj; 213 | } 214 | let probableJSON, result = _self.LZWCompress.unpack(compressedObj); 215 | try { 216 | probableJSON = JSON.parse(result); 217 | } catch (e) { 218 | _lzwLoggingEnabled && _lzwLog('unpacked (uncompressed) : ' + result); 219 | return result; 220 | } 221 | if (typeof probableJSON === 'object') { 222 | result = _self.KeyOptimize.unpack(probableJSON); 223 | } 224 | _lzwLoggingEnabled && _lzwLog('unpacked (uncompressed) : ' + result); 225 | return result; 226 | } 227 | 228 | /** 229 | * @param {boolean} enable 230 | */ 231 | export function enableLogging(enable) { 232 | _lzwLoggingEnabled = enable; 233 | }; 234 | -------------------------------------------------------------------------------- /demo-src/model-compression.js: -------------------------------------------------------------------------------- 1 | import { PicoCADModel, PicoCADModelFace, PicoCADModelObject } from "../src/model"; 2 | import * as LZW from "./lzw"; 3 | 4 | 5 | /** 6 | * @param {PicoCADModel} model 7 | * @returns {string} 8 | */ 9 | export function urlCompressModel(model) { 10 | const bytes = modelToBytes(model); 11 | console.log("binary: " + bytes.length + " bytes"); 12 | 13 | const lzw = LZW.compress(toByteString(bytes)); 14 | 15 | const lzwBitstream = lzwNumbersToBitStream(lzw); 16 | 17 | let s = btoa(toByteString(lzwBitstream)); 18 | 19 | // trim "=" from end 20 | let lastEq = s.indexOf("=", s.length - 4); 21 | if (lastEq >= 0) s = s.slice(0, lastEq); 22 | 23 | return s; 24 | } 25 | 26 | /** 27 | * @param {string} s 28 | * @returns {PicoCADModel} 29 | */ 30 | export function urlDecompressModel(s) { 31 | const lzwBitstream = fromByteString(atob(s)); 32 | 33 | const lzw = bitStreamToLZWNumbers(lzwBitstream); 34 | 35 | const bytes = fromByteString(LZW.decompress(lzw)); 36 | 37 | return bytesToModel(bytes); 38 | } 39 | 40 | /** 41 | * @param {number[]} bytes 42 | * @returns {string} 43 | */ 44 | function toByteString(bytes) { 45 | let s = ""; 46 | for (const byte of bytes) { 47 | s += String.fromCharCode(byte); 48 | } 49 | return s; 50 | } 51 | 52 | /** 53 | * @param {string} s 54 | * @returns {number[]} 55 | */ 56 | function fromByteString(s) { 57 | const bytes = Array(s.length); 58 | for (let i = 0; i < s.length; i++) { 59 | bytes[i] = s.charCodeAt(i); 60 | } 61 | return bytes; 62 | } 63 | 64 | 65 | // Encoding/Decoding 66 | 67 | const ENCODING_VERSION_1_0 = 1; 68 | 69 | const ALPHABET = "\0abcdefghijklmnopqrstuvwxyz0123456789_ "; 70 | 71 | const TEXTURE_ENCODING_RLE = 0; 72 | const TEXTURE_ENCODING_PACKED = 1; 73 | 74 | const uint16 = new Uint16Array(1); 75 | const uint8 = new Uint8Array(uint16.buffer); 76 | 77 | /** 78 | * @param {PicoCADModel} model 79 | */ 80 | function modelToBytes(model) { 81 | // Utils 82 | /** 83 | * @param {string} s 84 | */ 85 | function putPackedString(s) { 86 | s = s.toLowerCase(); 87 | 88 | for (let i = 0; i < s.length; i++) { 89 | const n = ALPHABET.indexOf(s.charAt(i)); 90 | if (n > 0) putPacked(n, 6); 91 | } 92 | 93 | putPacked(0, 6); 94 | } 95 | 96 | /** 97 | * @param {number[]} xs 98 | */ 99 | function putFloats(xs) { 100 | for (const x of xs) { 101 | putFloat(x); 102 | } 103 | } 104 | 105 | /** 106 | * @param {number} x 107 | */ 108 | function putFloat(x) { 109 | const r = Math.round(x * 64) / 64; 110 | 111 | if (r >= -16 && r <= 15.75 && Number.isInteger(r * 4)) { 112 | bytes.push(192 + r * 4); 113 | } else { 114 | const n = 8192 + r * 64; 115 | if (n >= 32768) throw Error(`can't encode float "${x}"`); 116 | 117 | bytes.push((n & 65280) >> 8); 118 | bytes.push((n & 255)); 119 | } 120 | } 121 | 122 | let packByte = 0; 123 | let packIndex = 0; 124 | 125 | function packEnd() { 126 | if (packIndex > 0) { 127 | bytes.push(packByte); 128 | } 129 | packByte = 0; 130 | packIndex = 0; 131 | } 132 | 133 | /** 134 | * @param {number} x 135 | * @param {number} bits Bits per integer 136 | */ 137 | function putPacked(x, bits) { 138 | for (let bit_i = 0; bit_i < bits; bit_i++) { 139 | const bit = 1 << bit_i; 140 | const b = (x & bit) >> bit_i; 141 | 142 | putPackedBit(b); 143 | } 144 | } 145 | 146 | /** 147 | * @param {number} b 148 | */ 149 | function putPackedBit(b) { 150 | packByte += b << packIndex; 151 | packIndex++; 152 | 153 | if (packIndex >= 8) { 154 | bytes.push(packByte); 155 | packIndex = 0; 156 | packByte = 0; 157 | } 158 | } 159 | 160 | 161 | // Start encoding. 162 | const bytes = /** @type {number[]} */([]); 163 | 164 | // Put encoding version (future proofing). 165 | bytes.push(ENCODING_VERSION_1_0); 166 | 167 | //Put model meta. 168 | putPackedString(model.name); 169 | 170 | putPacked(model.zoomLevel, 7); 171 | putPacked(model.backgroundIndex, 4); 172 | putPacked(model.alphaIndex, 4); 173 | 174 | packEnd(); 175 | 176 | // Put object data. 177 | if (model.objects.length >= 256) throw Error("Too many objects"); 178 | bytes.push(model.objects.length); 179 | 180 | for (const object of model.objects) { 181 | putPackedString(object.name); 182 | packEnd(); 183 | putFloats(object.position); 184 | 185 | // Put vertices 186 | if (object.vertices.length >= 256) throw Error("Too many vertices on object"); 187 | bytes.push(object.vertices.length); 188 | 189 | for (const vertex of object.vertices) { 190 | putFloats(vertex); 191 | } 192 | 193 | // Put faces 194 | const bpi = bitsToStore(object.vertices.length - 1); 195 | 196 | if (object.faces.length >= 256) throw Error("Too many faces on object"); 197 | bytes.push(object.faces.length); 198 | 199 | for (const face of object.faces) { 200 | // Put face meta 201 | putPacked(face.colorIndex, 4); 202 | putPackedBit(face.texture ? 1 : 0); 203 | putPackedBit(face.shading ? 1 : 0); 204 | putPackedBit(face.doubleSided ? 1 : 0); 205 | putPackedBit(face.renderFirst ? 1 : 0); 206 | 207 | // We know the max index value based on the number of vertices. 208 | // So we can pack them more for smaller number of vertices. 209 | for (const index of face.indices) { 210 | putPacked(index, bpi); 211 | } 212 | putPacked(face.indices[0], bpi); 213 | 214 | // UVs are usually between -1 and 17, at a 0.25 resolution. 215 | // => ~72 possible values => 7bits per U/V value 216 | if (face.texture) { 217 | for (const uv of face.uvs) { 218 | putPacked(32 + uv[0] * 4, 7); 219 | putPacked(32 + uv[1] * 4, 7); 220 | } 221 | } 222 | } 223 | 224 | packEnd(); 225 | } 226 | 227 | // Put texture data. 228 | let final_i = model.texture.length - 1; 229 | const finalIndex = model.texture[final_i]; 230 | while (final_i >= 1) { 231 | if (model.texture[final_i - 1] === finalIndex) { 232 | final_i--; 233 | } else { 234 | break; 235 | } 236 | } 237 | 238 | /** @type {number[]} */ 239 | const rleBytes = []; 240 | 241 | // generate 1byte per pixel w/ run length encoding 242 | for (let i = 0; i <= final_i; ) { 243 | const index = model.texture[i]; 244 | let repeats = 0; 245 | 246 | i++; 247 | while (i <= final_i) { 248 | if (model.texture[i] === index) { 249 | i++; 250 | repeats++; 251 | if (repeats == 15) break; 252 | } else { 253 | break; 254 | } 255 | } 256 | 257 | rleBytes.push((index << 4) + repeats); 258 | } 259 | 260 | // Use run length encoding version if it's shorter! 261 | if (rleBytes.length < final_i / 2) { 262 | bytes.push(TEXTURE_ENCODING_RLE); 263 | 264 | for (const byte of rleBytes) { 265 | bytes.push(byte); 266 | } 267 | } else { 268 | // 4bits per pixel 269 | bytes.push(TEXTURE_ENCODING_PACKED); 270 | 271 | if (final_i % 2 == 0) final_i++; 272 | 273 | for (let i = 1; i <= final_i; i += 2) { 274 | bytes.push((model.texture[i - 1] << 4) + model.texture[i]); 275 | } 276 | } 277 | 278 | return bytes; 279 | } 280 | 281 | 282 | /** 283 | * @param {number[]} bytes 284 | * @returns {PicoCADModel} 285 | */ 286 | function bytesToModel(bytes) { 287 | // Utils 288 | function getPackedString() { 289 | let s = ""; 290 | 291 | while (true) { 292 | const n = getPacked(6); 293 | if (n === 0) break; 294 | if (n >= ALPHABET.length) throw Error("invalid encoded string"); 295 | s += ALPHABET.charAt(n); 296 | } 297 | 298 | return s; 299 | } 300 | 301 | /** 302 | * @returns {number} 303 | */ 304 | function getByte() { 305 | if (byte_i < bytes.length) { 306 | return bytes[byte_i++]; 307 | } else { 308 | throw Error("unexpected of input"); 309 | } 310 | } 311 | 312 | /** 313 | * @returns {number} 314 | */ 315 | function getOptionalByte() { 316 | if (byte_i < bytes.length) { 317 | return bytes[byte_i++]; 318 | } else { 319 | return -1; 320 | } 321 | } 322 | 323 | /** 324 | * @returns {number} 325 | */ 326 | function getFloat() { 327 | const b0 = getByte(); 328 | 329 | if (b0 >= 128) { 330 | return ((b0 & 127) - 64) / 4; 331 | } 332 | 333 | const n = (b0 << 8) + getByte(); 334 | 335 | return (n - 8192) / 64; 336 | } 337 | 338 | /** 339 | * @param {number} n 340 | * @returns {number[]} 341 | */ 342 | function getFloats(n) { 343 | const out = Array(n); 344 | for (let i = 0; i < n; i++) { 345 | out[i] = getFloat(); 346 | } 347 | return out; 348 | } 349 | 350 | let packIndex = 0; 351 | 352 | function packEnd() { 353 | if (packIndex > 0) { 354 | byte_i++; 355 | packIndex = 0; 356 | } 357 | } 358 | 359 | /** 360 | * @param {number} bits 361 | * @returns {number} 362 | */ 363 | function getPacked(bits) { 364 | let byte = bytes[byte_i]; 365 | let packValue = 0; 366 | 367 | for (let bit_i = 0; bit_i < bits; bit_i++) { 368 | const bit = 1 << packIndex; 369 | const b = (byte & bit) >> packIndex; 370 | packValue += b << bit_i; 371 | packIndex++; 372 | 373 | if (packIndex >= 8) { 374 | packIndex = 0; 375 | byte_i++; 376 | if (byte_i >= bytes.length) { 377 | throw Error("Unexpected end of input"); 378 | } 379 | byte = bytes[byte_i]; 380 | } 381 | } 382 | 383 | return packValue; 384 | } 385 | 386 | 387 | // Start decoding. 388 | let byte_i = 0; 389 | 390 | // Get encoding version (unused at the moment). 391 | const encodingVersion = getByte(); 392 | if (encodingVersion !== ENCODING_VERSION_1_0) throw Error(`invalid encoding version ${encodingVersion}`); 393 | 394 | // Get model meta. 395 | const modelName = getPackedString(); 396 | 397 | const zoomLevel = getPacked(7); 398 | const backgroundIndex = getPacked(4); 399 | const alphaIndex = getPacked(4); 400 | 401 | packEnd(); 402 | 403 | // Get object data. 404 | const objectCount = getByte(); 405 | const objects = /** @type {PicoCADModelObject[]} */(Array(objectCount)); 406 | 407 | for (let object_i = 0; object_i < objectCount; object_i++) { 408 | const objectName = getPackedString(); 409 | packEnd(); 410 | const objectPos = getFloats(3); 411 | 412 | // Get vertices 413 | const vertexCount = getByte(); 414 | const vertices = /** @type {number[][]} */(Array(vertexCount)); 415 | 416 | for (let vertex_i = 0; vertex_i < vertexCount; vertex_i++) { 417 | vertices[vertex_i] = getFloats(3); 418 | } 419 | 420 | // Get faces 421 | const bpi = bitsToStore(vertexCount - 1); 422 | 423 | const faceCount = getByte(); 424 | 425 | const faces = /** @type {PicoCADModelFace[]} */(Array(faceCount)); 426 | 427 | for (let face_i = 0; face_i < faceCount; face_i++) { 428 | // Get face meta. 429 | const colorIndex = getPacked(4); 430 | const texture = getPacked(1) === 1; 431 | const shading = getPacked(1) === 1; 432 | const doubleSided = getPacked(1) === 1; 433 | const renderFirst = getPacked(1) === 1; 434 | 435 | // Get Indices. 436 | const index0 = getPacked(bpi); 437 | const indices = [index0]; 438 | while (true) { 439 | const index = getPacked(bpi); 440 | 441 | if (index === index0) break; 442 | 443 | indices.push(index); 444 | } 445 | const index_count = indices.length; 446 | 447 | // Get UVs 448 | let uvs = /** @type {number[][]} */(Array(index_count)); 449 | 450 | if (texture) { 451 | for (let uv_i = 0; uv_i < index_count; uv_i++) { 452 | const p0 = getPacked(7); 453 | const p1 = getPacked(7); 454 | 455 | uvs[uv_i] = [ 456 | (p0 - 32) / 4, 457 | (p1 - 32) / 4, 458 | ]; 459 | } 460 | } else { 461 | for (let uv_i = 0; uv_i < index_count; uv_i++) { 462 | uvs[uv_i] = [0, 0]; 463 | } 464 | } 465 | 466 | faces[face_i] = new PicoCADModelFace( 467 | indices, 468 | colorIndex, 469 | uvs, 470 | { 471 | texture: texture, 472 | shading: shading, 473 | doubleSided: doubleSided, 474 | renderFirst: renderFirst, 475 | } 476 | ); 477 | } 478 | 479 | packEnd(); 480 | 481 | // Got object! 482 | objects[object_i] = new PicoCADModelObject( 483 | objectName, 484 | objectPos, 485 | [0, 0, 0], 486 | vertices, 487 | faces, 488 | ); 489 | } 490 | 491 | // Get texture data. 492 | const textureEncoding = getByte(); 493 | 494 | /** @type {number[]} */ 495 | let textureIndices = []; 496 | 497 | if (textureEncoding === TEXTURE_ENCODING_RLE) { 498 | for (let i = 0; i <= 15360; ) { 499 | const byte = getOptionalByte(); 500 | if (byte < 0) break; 501 | 502 | const index = (byte & 0b11110000) >> 4; 503 | const count = (byte & 0b00001111) + 1; 504 | 505 | for (let j = 0; j < count; j++) { 506 | textureIndices.push(index); 507 | } 508 | } 509 | } else if (textureEncoding === TEXTURE_ENCODING_PACKED) { 510 | for (let i = 0; i < 7680; i++) { 511 | const byte = getOptionalByte(); 512 | if (byte < 0) break; 513 | 514 | textureIndices.push( 515 | (byte & 0b11110000) >> 4, 516 | byte & 0b1111, 517 | ); 518 | } 519 | } else { 520 | throw Error(`Invalid texture encoding code: ${textureEncoding}`); 521 | } 522 | 523 | // Add repeated trailing index 524 | if (textureIndices.length < 15360) { 525 | const index = textureIndices[textureIndices.length - 1]; 526 | 527 | while (textureIndices.length < 15360) { 528 | textureIndices.push(index); 529 | } 530 | } 531 | 532 | // Done! 533 | return new PicoCADModel(objects, { 534 | name: modelName, 535 | alphaIndex: alphaIndex, 536 | backgroundIndex: backgroundIndex, 537 | zoomLevel: zoomLevel, 538 | texture: textureIndices, 539 | }); 540 | } 541 | 542 | /** 543 | * @param {number[]} xs 544 | */ 545 | function lzwNumbersToBitStream(xs) { 546 | const bytes = []; 547 | let byte = 0; 548 | let byte_i = 0; 549 | 550 | let maxSize = 512; 551 | let bits = 9; 552 | 553 | for (let i = 0; i < xs.length; i++) { 554 | if (256 + i >= maxSize) { 555 | bits++; 556 | maxSize *= 2; 557 | } 558 | 559 | const x = xs[i]; 560 | 561 | for (let bit_i = 0; bit_i < bits; bit_i++) { 562 | let bit = 1 << bit_i; 563 | let b = (x & bit) >> bit_i; 564 | byte += b << byte_i; 565 | byte_i++; 566 | 567 | if (byte_i >= 8) { 568 | bytes.push(byte); 569 | byte_i = 0; 570 | byte = 0; 571 | } 572 | } 573 | } 574 | 575 | if (byte_i > 0) { 576 | bytes.push(byte); 577 | } 578 | 579 | return bytes; 580 | } 581 | 582 | /** 583 | * @param {number[]} bytes 584 | */ 585 | function bitStreamToLZWNumbers(bytes) { 586 | const xs = []; 587 | let x = 0; 588 | let x_i = 0; 589 | 590 | let maxSize = 512; 591 | let bits = 9; 592 | 593 | for (let i = 0; i < bytes.length; i++) { 594 | const byte = bytes[i]; 595 | 596 | for (let bit_i = 0; bit_i < 8; bit_i++) { 597 | let bit = 1 << bit_i; 598 | let b = (byte & bit) >> bit_i; 599 | x += b << x_i; 600 | x_i++; 601 | 602 | if (x_i >= bits) { 603 | xs.push(x); 604 | x_i = 0; 605 | x = 0; 606 | 607 | if (256 + xs.length >= maxSize) { 608 | bits++; 609 | maxSize *= 2; 610 | } 611 | } 612 | } 613 | } 614 | 615 | return xs; 616 | } 617 | 618 | /** 619 | * Get the min number of bits required to store `x`. 620 | * @param {number} x 621 | */ 622 | function bitsToStore(x) { 623 | return Math.floor(Math.log2(x) + 1); 624 | } 625 | -------------------------------------------------------------------------------- /demo-worker-src/gifenc.d.ts: -------------------------------------------------------------------------------- 1 | 2 | export class GIFEncoder { 3 | /** 4 | * The currently backed ArrayBuffer, note this reference may change as the buffer grows in size. 5 | */ 6 | buffer: ArrayBuffer; 7 | /** 8 | * An internal API that holds an expandable buffer and allows writing single or multiple bytes. 9 | */ 10 | stream: { 11 | writeByte(byte: number): void; 12 | writeBytes(array: Uint8Array, offset: number, byteLength: number): void; 13 | }; 14 | 15 | constructor(options?: { 16 | /** 17 | * In "auto" mode, the header and first-frame metadata (global palette) will be written upon writing the first frame. 18 | * 19 | * If set to false, you will be responsible for first writing a GIF header, then writing frames with { first } boolean specified. 20 | * 21 | * Defaults to false. 22 | */ 23 | auto?: boolean, 24 | /** 25 | * The number of bytes to initially set the internal buffer to, it will grow as bytes are written to the stream 26 | * 27 | * Defaults to 1024. 28 | */ 29 | initialCapacity?: number, 30 | }) 31 | 32 | /** 33 | * Writes a single frame into the GIF stream. 34 | * @param index The indexed image (set of palette indices, one byte per index). 35 | */ 36 | writeFrame(index: Uint8Array, width: number, height: number, options?: { 37 | /** 38 | * The color table for this frame, which is required for the first frame (i.e. global color table) but optional for subsequent frames. 39 | * 40 | * If not specified, the frame will use the first (global) color table in the stream. 41 | */ 42 | palette?: GIFPalette, 43 | /** 44 | * In non-auto mode, set this to true when encoding the first frame in an image or sequence, and it will encode the Logical Screen Descriptor and a Global Color Table. 45 | * 46 | * This option is ignored in auto mode. 47 | */ 48 | first?: boolean, 49 | /** 50 | * Enable 1-bit transparency for this frame. 51 | * 52 | * Defaults to false. 53 | */ 54 | transparent?: boolean, 55 | /** 56 | * If `transparent` is enabled, the color at the specified palette index will be treated as fully transparent for this frame. 57 | * 58 | * Defaults to 0 59 | */ 60 | transparentIndex?: number, 61 | /** 62 | * The frame delay in milliseconds. 63 | * 64 | * Defaults to 0. 65 | */ 66 | delay?: number, 67 | /** 68 | * Repeat count, set to `-1` for 'once', `0` for 'forever', and any other positive integer for the number of repetitions. 69 | * 70 | * Defaults to 0. 71 | */ 72 | repeat?: number, 73 | /** 74 | * Advanced GIF dispose flag override, -1 is 'use default'. 75 | * 76 | * Defaults to -1. 77 | */ 78 | dispose?: number, 79 | }): void; 80 | 81 | /** 82 | * Writes the GIF end-of-stream character, required after writing all frames for the image to encode correctly. 83 | */ 84 | finish(): void; 85 | 86 | /** 87 | * Gets a slice of the Uint8Array bytes that is underlying this GIF stream. (Note: this incurs a copy). 88 | */ 89 | bytes(): Uint8Array; 90 | 91 | /** 92 | * Gets a direct typed array buffer view into the Uint8Array bytes underlying this GIF stream. (Note: no copy involved, but best to use this carefully). 93 | */ 94 | bytesView(): Uint8Array; 95 | 96 | /** 97 | * Writes a GIF header into the stream, only necessary if you have specified `{ auto: false }` in the GIFEncoder options. 98 | */ 99 | writeHeader(): void; 100 | 101 | /** 102 | * Resets this GIF stream by simply setting its internal stream cursor (index) to zero, so that subsequent writes will replace the previous data in the underlying buffer. 103 | */ 104 | reset(): void; 105 | 106 | /** 107 | * For the given pixel as [r,g,b] or [r,g,b,a] (depending on your pixel format), determines the index (0...N) of the nearest color in your palette array of colors in the same RGB(A) format. 108 | */ 109 | nearestColorIndex(palette: GIFPalette, pixel: number[]): number; 110 | 111 | /** 112 | * Same as `nearestColorIndex`, but returns a tuple of index and distance (euclidean distance squared). 113 | */ 114 | nearestColorIndexWithDistance(palette: GIFPalette, pixel: number[]): [number, number]; 115 | } 116 | export default GIFEncoder; 117 | 118 | /** 119 | * Given the image contained by `rgba`, this method will quantize the total number of colors down to a reduced palette no greater than `maxColors`. 120 | * @param rgba A flat Uint8Array or Uint8ClampedArray of per-pixel RGBA data. 121 | * @param format Defaults to "rgb565". 122 | * @returns An index array (each one byte) length equal to rgba.length / 4. 123 | */ 124 | export function quantize(rgba: Uint8Array | Uint8ClampedArray, maxColors: number, options?: { 125 | /** Defaults to "rgb565". */ 126 | format?: GIFFormat | null; 127 | /** Defaults to false. if alpha format is selected, this will go through all quantized RGBA colors and set their alpha to either 0x00 if the alpha is less than or equal to 127, otherwise it will be set to 0xFF. You can specify a number here instead of a boolean to use a specific 1-bit alpha threshold. */ 128 | oneBitAlpha?: boolean | number | null; 129 | /** Defaults to true. if alpha format is selected and the quantized color is below `clearAlphaThreshold`, it will be replaced with `clearAlphaColor` (i.e. RGB colors with 0 opacity will be replaced with pure black). */ 130 | clearAlpha?: boolean | null; 131 | /** Defaults to 0. If alpha and `clearAlpha` is enabled, and a quantized pixel has an alpha below or equal to this value, its RGB values will be set to `clearAlphaColor` */ 132 | clearAlphaThreshold?: number; 133 | /** Defaults to `0x00`. if alpha and `clearAlpha` is enabled and a quantized pixel is being cleared, this is the color its RGB channels will be cleared to (typically you will choose `0x00` or `0xff`). */ 134 | clearAlphaColor?: number; 135 | }): GIFPalette; 136 | 137 | /** 138 | * This will determine the color index for each pixel in the rgba image. 139 | * @param rgbaBytes A flat Uint8Array or Uint8ClampedArray of per-pixel RGBA data. 140 | * @param palette 141 | * @param format Defaults to "rgb565". 142 | * @returns An index array (each one byte) length equal to rgba.length / 4. 143 | */ 144 | export function applyPalette(rgba: Uint8Array | Uint8ClampedArray, palette: GIFPalette, format?: GIFFormat): Uint8Array; 145 | 146 | /** 147 | * `rgb565`: 5 bits red, 6 bits green, 5 bits blue (better quality, slower). 148 | * 149 | * `rgb444`: 4 bits per channel (lower quality, faster). 150 | * 151 | * `rgba4444`: 4 bits per channel with alpha. 152 | */ 153 | export type GIFFormat = "rgb565" | "rgb444" | "rgba4444"; 154 | 155 | /** 156 | * A set of RGB or RGBA colors, in byte per channel format. 157 | * @example 158 | * const palette = [ 159 | * // black 160 | * [0, 0, 0], 161 | * // white 162 | * [255, 255, 255], 163 | * ]; 164 | */ 165 | export type GIFPalette = number[][]; 166 | -------------------------------------------------------------------------------- /demo-worker-src/gifenc.js: -------------------------------------------------------------------------------- 1 | /* 2 | @licence https://github.com/mattdesl/gifenc 3 | The MIT License (MIT) 4 | Copyright (c) 2017 Matt DesLauriers 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 20 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 21 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 22 | OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | var X={signature:"GIF",version:"89a",trailer:59,extensionIntroducer:33,applicationExtensionLabel:255,graphicControlExtensionLabel:249,imageSeparator:44,signatureSize:3,versionSize:3,globalColorTableFlagMask:128,colorResolutionMask:112,sortFlagMask:8,globalColorTableSizeMask:7,applicationIdentifierSize:8,applicationAuthCodeSize:3,disposalMethodMask:28,userInputFlagMask:2,transparentColorFlagMask:1,localColorTableFlagMask:128,interlaceFlagMask:64,idSortFlagMask:32,localColorTableSizeMask:7};function F(t=256){let e=0,s=new Uint8Array(t);return{get buffer(){return s.buffer},reset(){e=0},bytesView(){return s.subarray(0,e)},bytes(){return s.slice(0,e)},writeByte(r){n(e+1),s[e]=r,e++},writeBytes(r,o=0,i=r.length){n(e+i);for(let c=0;c=r)return;var i=1024*1024;r=Math.max(r,o*(o>>0),o!=0&&(r=Math.max(r,256));let c=s;s=new Uint8Array(r),e>0&&s.set(c.subarray(0,e),0)}}var O=12,J=5003,lt=[0,1,3,7,15,31,63,127,255,511,1023,2047,4095,8191,16383,32767,65535];function at(t,e,s,n,r=F(512),o=new Uint8Array(256),i=new Int32Array(J),c=new Int32Array(J)){let x=i.length,a=Math.max(2,n);o.fill(0),c.fill(0),i.fill(-1);let l=0,f=0,g=a+1,h=g,b=!1,w=h,_=(1<=0;)if(M-=V,M<0&&(M+=x),i[M]===v){A=c[M];break t}I(A),A=m,B<1<0?l|=y<=8;)o[p++]=l&255,p>=254&&(r.writeByte(p),r.writeBytesView(o,0,p),p=0),l>>=8,f-=8;if((B>_||b)&&(b?(w=h,_=(1<0;)o[p++]=l&255,p>=254&&(r.writeByte(p),r.writeBytesView(o,0,p),p=0),l>>=8,f-=8;p>0&&(r.writeByte(p),r.writeBytesView(o,0,p),p=0)}}}var $=at;function D(t,e,s){return t<<8&63488|e<<2&992|s>>3}function G(t,e,s,n){return t>>4|e&240|(s&240)<<4|(n&240)<<8}function j(t,e,s){return t>>4<<8|e&240|s>>4}function R(t,e,s){return ts?s:t}function T(t){return t*t}function tt(t,e,s){var n=0,r=1e100;let o=t[e],i=o.cnt,c=o.ac,x=o.rc,a=o.gc,l=o.bc;for(var f=o.fw;f!=0;f=t[f].fw){let h=t[f],b=h.cnt,w=i*b/(i+b);if(!(w>=r)){var g=0;s&&(g+=w*T(h.ac-c),g>=r)||(g+=w*T(h.rc-x),!(g>=r)&&(g+=w*T(h.gc-a),!(g>=r)&&(g+=w*T(h.bc-l),!(g>=r)&&(r=g,n=f))))}}o.err=r,o.nn=n}function Q(){return{ac:0,rc:0,gc:0,bc:0,cnt:0,nn:0,fw:0,bk:0,tm:0,mtm:0,err:0}}function ut(t,e){let s=e==="rgb444"?4096:65536,n=new Array(s),r=t.length;if(e==="rgba4444")for(let o=0;o>24&255,x=i>>16&255,a=i>>8&255,l=i&255,f=G(l,a,x,c),g=f in n?n[f]:n[f]=Q();g.rc+=l,g.gc+=a,g.bc+=x,g.ac+=c,g.cnt++}else if(e==="rgb444")for(let o=0;o>16&255,x=i>>8&255,a=i&255,l=j(a,x,c),f=l in n?n[l]:n[l]=Q();f.rc+=a,f.gc+=x,f.bc+=c,f.cnt++}else for(let o=0;o>16&255,x=i>>8&255,a=i&255,l=D(a,x,c),f=l in n?n[l]:n[l]=Q();f.rc+=a,f.gc+=x,f.bc+=c,f.cnt++}return n}function H(t,e,s={}){let{format:n="rgb565",clearAlpha:r=!0,clearAlphaColor:o=0,clearAlphaThreshold:i=0,oneBitAlpha:c=!1}=s;if(!t||!t.buffer)throw new Error("quantize() expected RGBA Uint8Array data");if(!(t instanceof Uint8Array)&&!(t instanceof Uint8ClampedArray))throw new Error("quantize() expected RGBA Uint8Array data");let x=new Uint32Array(t.buffer),a=s.useSqrt!==!1,l=n==="rgba4444",f=ut(x,n),g=f.length,h=g-1,b=new Uint32Array(g+1);for(var w=0,u=0;u1&&(p=B>>1,!(f[k=b[p]].err<=A));B=p)b[B]=k;b[B]=u}var z=w-e;for(u=0;u=d.mtm&&f[d.nn].mtm<=d.tm)break;d.mtm==h?I=b[1]=b[b[0]--]:(tt(f,I,!1),d.tm=u);var A=f[I].err;for(B=1;(p=B+B)<=b[0]&&(pf[b[p+1]].err&&p++,!(A<=f[k=b[p]].err));B=p)b[B]=k;b[B]=I}var y=f[d.nn],m=d.cnt,v=y.cnt,_=1/(m+v);l&&(d.ac=_*(m*d.ac+v*y.ac)),d.rc=_*(m*d.rc+v*y.rc),d.gc=_*(m*d.gc+v*y.gc),d.bc=_*(m*d.bc+v*y.bc),d.cnt+=y.cnt,d.mtm=++u,f[y.bk].fw=y.fw,f[y.fw].bk=y.bk,y.mtm=h}let M=[];var V=0;for(u=0;;++V){let L=R(Math.round(f[u].rc),0,255),C=R(Math.round(f[u].gc),0,255),Y=R(Math.round(f[u].bc),0,255),E=255;if(l){if(E=R(Math.round(f[u].ac),0,255),c){let st=typeof c=="number"?c:127;E=E<=st?0:255}r&&E<=i&&(L=C=Y=o,E=0)}let K=l?[L,C,Y,E]:[L,C,Y];if(xt(M,K)||M.push(K),(u=f[u].fw)==0)break}return M}function xt(t,e){for(let s=0;s=4&&e.length>=4?n[3]===e[3]:!0;if(r&&o)return!0}return!1}function U(t,e){var s=0,n;for(n=0;n1?Math.round(t/e)*e:t}function et(t,{roundRGB:e=5,roundAlpha:s=10,oneBitAlpha:n=null}={}){let r=new Uint32Array(t.buffer);for(let o=0;o>24&255,x=i>>16&255,a=i>>8&255,l=i&255;if(c=P(c,s),n){let f=typeof n=="number"?n:127;c=c<=f?0:255}l=P(l,e),a=P(a,e),x=P(x,e),r[o]=c<<24|x<<16|a<<8|l<<0}}function nt(t,e,s="rgb565"){if(!t||!t.buffer)throw new Error("quantize() expected RGBA Uint8Array data");if(!(t instanceof Uint8Array)&&!(t instanceof Uint8ClampedArray))throw new Error("quantize() expected RGBA Uint8Array data");if(e.length>256)throw new Error("applyPalette() only works with 256 colors or less");let n=new Uint32Array(t.buffer),r=n.length,o=s==="rgb444"?4096:65536,i=new Uint8Array(r),c=new Array(o),x=s==="rgba4444";if(s==="rgba4444")for(let a=0;a>24&255,g=l>>16&255,h=l>>8&255,b=l&255,w=G(b,h,g,f),_=w in c?c[w]:c[w]=gt(b,h,g,f,e);i[a]=_}else{let a=s==="rgb444"?j:D;for(let l=0;l>16&255,h=f>>8&255,b=f&255,w=a(b,h,g),_=w in c?c[w]:c[w]=bt(b,h,g,e);i[l]=_}}return i}function gt(t,e,s,n,r){let o=0,i=1e100;for(let c=0;ci)continue;let f=x[0];if(l+=q(f-t),l>i)continue;let g=x[1];if(l+=q(g-e),l>i)continue;let h=x[2];l+=q(h-s),!(l>i)&&(i=l,o=c)}return o}function bt(t,e,s,n){let r=0,o=1e100;for(let i=0;io)continue;let l=c[1];if(a+=q(l-e),a>o)continue;let f=c[2];a+=q(f-s),!(a>o)&&(o=a,r=i)}return r}function rt(t,e,s=5){if(!t.length||!e.length)return;let n=t.map(i=>i.slice(0,3)),r=s*s,o=t[0].length;for(let i=0;io?c=c.slice(0,3):c=c.slice();let x=N(n,c.slice(0,3),U),a=x[0],l=x[1];l>0&&l<=r&&(t[a]=c)}}function q(t){return t*t}function W(t,e,s=U){let n=Infinity,r=-1;for(let o=0;o=0&&dt(n,k)}let z=Math.round(_/10);wt(n,p,z,b,w);let d=Boolean(u)&&!A;ht(n,f,g,d?u:null),d&&it(n,u),yt(n,l,f,g,B,o,i,c)}};function a(){ft(n,"GIF89a")}}function wt(t,e,s,n,r){t.writeByte(33),t.writeByte(249),t.writeByte(4),r<0&&(r=0,n=!1);var o,i;n?(o=1,i=2):(o=0,i=0),e>=0&&(i=e&7),i<<=2;let c=0;t.writeByte(0|i|c|o),S(t,s),t.writeByte(r||0),t.writeByte(0)}function pt(t,e,s,n,r=8){let o=1,i=0,c=Z(n.length)-1,x=o<<7|r-1<<4|i<<3|c,a=0,l=0;S(t,e),S(t,s),t.writeBytes([x,a,l])}function dt(t,e){t.writeByte(33),t.writeByte(255),t.writeByte(11),ft(t,"NETSCAPE2.0"),t.writeByte(3),t.writeByte(1),S(t,e),t.writeByte(0)}function it(t,e){let s=1<>8&255)}function ft(t,e){for(var s=0;s [ rgbToInt(rgb), i ])); 30 | 31 | for (let i = 0; i < frames.length; i++) { 32 | let frame = frames[i]; 33 | 34 | // Replace transparent pixels with background color. 35 | for (let i = 0; i < frame.length; i += 4) { 36 | if (frame[i + 3] < 255) { 37 | frame[i ] = background[0]; 38 | frame[i + 1] = background[1]; 39 | frame[i + 2] = background[2]; 40 | frame[i + 3] = background[3]; 41 | } 42 | } 43 | 44 | // Quantize and apply palette. 45 | let palette = null; 46 | let indices; 47 | let isTransparent = false; 48 | 49 | if (globalPalette == null) { 50 | // Have no palette, need to quantize and paletize each frame. 51 | palette = quantize(frame, 256, { 52 | format: "rgba4444", 53 | }); 54 | 55 | // Check for transparent pixels. 56 | for (let ci = 0; ci < palette.length; ci++) { 57 | let color = palette[ci]; 58 | 59 | if (color.length >= 4 && color[3] < 255) { 60 | isTransparent = true; 61 | transparentIndex = ci; 62 | } 63 | } 64 | 65 | indices = applyPalette(frame, palette, "rgba4444"); 66 | } else { 67 | // Only need to write the global palette once. 68 | if (i === 0) { 69 | palette = globalPalette; 70 | } 71 | 72 | // Use our simplified method of mapping colors to indices. 73 | // Should be more efficient since we know what colors are going to appear. 74 | let ints = new Uint32Array(frame.buffer); 75 | 76 | if (recycledIndices == null) { 77 | recycledIndices = new Uint8Array(pixelCount); 78 | } 79 | 80 | for (let i = 0; i < ints.length; i++) { 81 | recycledIndices[i] = paletteIntToIndex.get(ints[i]) ?? 0; 82 | } 83 | 84 | indices = recycledIndices; 85 | 86 | isTransparent = transparentIndex >= 0; 87 | } 88 | 89 | // Transform indices: 90 | // Flip vertically (WebGL buffer is top to bottom). 91 | let srcRowIndex = pixelCount - width; 92 | let dstRowIndex = 0; 93 | let outWidth1 = width * scale; 94 | let outWidth2 = outWidth1 * scale; 95 | 96 | for (let y = 0; y < height; y++) { 97 | let srcIndex = srcRowIndex; 98 | let dstIndex = dstRowIndex; 99 | 100 | for (let x = 0; x < width; x++) { 101 | let index = indices[srcIndex]; 102 | 103 | if (scale === 1) { 104 | scaledIndices[dstIndex] = index; 105 | dstIndex++; 106 | } else { 107 | // Repeat index in square region. 108 | let outIndexLoc = dstIndex; 109 | 110 | for (let sy = 0; sy < scale; sy++) { 111 | for (let sx = 0; sx < scale; sx++) { 112 | scaledIndices[outIndexLoc + sx] = index; 113 | } 114 | 115 | outIndexLoc += outWidth1; 116 | } 117 | 118 | dstIndex += scale; 119 | } 120 | 121 | srcIndex++; 122 | } 123 | 124 | srcRowIndex -= width; 125 | dstRowIndex += outWidth2; 126 | } 127 | 128 | // Write indices to GIF buffer. 129 | gifRecorder.writeFrame(scaledIndices, width * scale, height * scale, { 130 | palette: palette, 131 | delay: delay, 132 | transparent: isTransparent, 133 | transparentIndex: transparentIndex, 134 | }); 135 | } 136 | 137 | // Finalize GIF and send to main script. 138 | gifRecorder.finish(); 139 | 140 | let buffer = gifRecorder.bytesView(); 141 | 142 | postMessage({ type: "gif", data: buffer }, [ buffer.buffer ]); 143 | 144 | // Cleanup. 145 | gifRecorder.reset(); 146 | 147 | frames.length = 0; 148 | } 149 | 150 | addEventListener("message", (event) => { 151 | let data = event.data; 152 | 153 | let eventType = data.type; 154 | 155 | if (eventType === "generate") { 156 | generateAndReset(data.width, data.height, data.scale, data.delay, data.background, data.palette, data.transparentIndex); 157 | } else if (eventType === "frame") { 158 | addFrame(data.data); 159 | } 160 | }); 161 | 162 | // Tell main script we're ready. 163 | postMessage({ type: "load" }); 164 | -------------------------------------------------------------------------------- /demo-worker-src/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true, 4 | "target": "es2019", 5 | "lib": [ "ES2019", "WebWorker" ] 6 | }, 7 | "include": [ "./**/*" ] 8 | } -------------------------------------------------------------------------------- /docs/example.txt: -------------------------------------------------------------------------------- 1 | picocad;example;12;1;0 2 | { 3 | { 4 | name='cube', pos={0,-0.5,0}, rot={0,0,0}, 5 | v={ 6 | {-1,-1.5,-1}, 7 | {1,-1.5,-1}, 8 | {1,0.5,-1}, 9 | {-1,0.5,-1}, 10 | {-1,-1.5,1}, 11 | {1,-1.5,1}, 12 | {1,0.5,1}, 13 | {-1,0.5,1} 14 | }, 15 | f={ 16 | {1,2,3,4, c=12, uv={7.5,0.5,8.5,0.5,8.5,1.5,7.5,1.5} }, 17 | {6,5,8,7, c=10, uv={3.5,0.5,4.5,0.5,4.5,1.5,3.5,1.5} }, 18 | {5,6,2,1, c=15, uv={9.5,0.5,10.5,0.5,10.5,1.5,9.5,1.5} }, 19 | {5,1,4,8, c=11, uv={5.5,0.5,6.5,0.5,6.5,1.5,5.5,1.5} }, 20 | {2,6,7,3, c=8, uv={1.5,0.5,2.5,0.5,2.5,1.5,1.5,1.5} }, 21 | {4,3,7,8, c=6, uv={13.5,0.5,14.5,0.5,14.5,1.5,13.5,1.5} } 22 | } 23 | } 24 | }% 25 | 00000000eeee8888eeee8888aaaa9999aaaa9999bbbb3333bbbb3333ccccddddccccddddffffeeeeffffeeee7777666677776666555566665555666600000000 26 | 00000000eeee8888eeee8888aaaa9999aaaa9999bbbb3333bbbb3333ccccddddccccddddffffeeeeffffeeee7777666677776666555566665555666600000000 27 | 00000000eeee8888eeee8888aaaa9999aaaa9999bbbb3333bbbb3333ccccddddccccddddffffeeeeffffeeee7777666677776666555566665555666600000000 28 | 00000000eeee8888eeee8888aaaa9999aaaa9999bbbb3333bbbb3333ccccddddccccddddffffeeeeffffeeee7777666677776666555566665555666600000000 29 | 000000008888eeee8888eeee9999aaaa9999aaaa3333bbbb3333bbbbddddccccddddcccceeeeffffeeeeffff6666777766667777666655556666555500000000 30 | 000000008888eeee8888eeee9999aaaa9999aaaa3333bbbb3333bbbbddddccccddddcccceeeeffffeeeeffff6666777766667777666655556666555500000000 31 | 000000008888eeee8888eeee9999aaaa9999aaaa3333bbbb3333bbbbddddccccddddcccceeeeffffeeeeffff6666777766667777666655556666555500000000 32 | 000000008888eeee8888eeee9999aaaa9999aaaa3333bbbb3333bbbbddddccccddddcccceeeeffffeeeeffff6666777766667777666655556666555500000000 33 | 00000000eeee8888eeee8888aaaa9999aaaa9999bbbb3333bbbb3333ccccddddccccddddffffeeeeffffeeee7777666677776666555566665555666600000000 34 | 00000000eeee8888eeee8888aaaa9999aaaa9999bbbb3333bbbb3333ccccddddccccddddffffeeeeffffeeee7777666677776666555566665555666600000000 35 | 00000000eeee8888eeee8888aaaa9999aaaa9999bbbb3333bbbb3333ccccddddccccddddffffeeeeffffeeee7777666677776666555566665555666600000000 36 | 00000000eeee8888eeee8888aaaa9999aaaa9999bbbb3333bbbb3333ccccddddccccddddffffeeeeffffeeee7777666677776666555566665555666600000000 37 | 000000008888eeee8888eeee9999aaaa9999aaaa3333bbbb3333bbbbddddccccddddcccceeeeffffeeeeffff6666777766667777666655556666555500000000 38 | 000000008888eeee8888eeee9999aaaa9999aaaa3333bbbb3333bbbbddddccccddddcccceeeeffffeeeeffff6666777766667777666655556666555500000000 39 | 000000008888eeee8888eeee9999aaaa9999aaaa3333bbbb3333bbbbddddccccddddcccceeeeffffeeeeffff6666777766667777666655556666555500000000 40 | 000000008888eeee8888eeee9999aaaa9999aaaa3333bbbb3333bbbbddddccccddddcccceeeeffffeeeeffff6666777766667777666655556666555500000000 41 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 42 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 43 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 44 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 45 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 46 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 47 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 48 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 49 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 50 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 51 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 52 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 53 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 54 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 55 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 56 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 57 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 58 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 59 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 60 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 61 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 62 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 63 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 64 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 65 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 66 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 67 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 68 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 69 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 70 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 71 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 72 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 73 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 74 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 75 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 76 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 77 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 78 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 79 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 80 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 81 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 82 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 83 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 84 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 85 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 86 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 87 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 88 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 89 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 90 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 91 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 92 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 93 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 94 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 95 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 96 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 97 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 98 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 99 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 100 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 101 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 102 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 103 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 104 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 105 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 106 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 107 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 108 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 109 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 110 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 111 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 112 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 113 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 114 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 115 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 116 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 117 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 118 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 119 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 120 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 121 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 122 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 123 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 124 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 125 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 126 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 127 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 128 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 129 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 130 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 131 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 132 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 133 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 134 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 135 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 136 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 137 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 138 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 139 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 140 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 141 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 142 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 143 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 144 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 145 | -------------------------------------------------------------------------------- /docs/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucatronica/picocad-web-viewer/fcbd8a2fa94ccfcdf2dc5b273c836c1afc03fe3b/docs/favicon.png -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 275 | picoCAD Viewer 276 | 277 | 278 | 279 |
280 |
281 |
282 |
283 | 309 | 313 |
314 |
315 | 323 | 327 |
328 |
329 | 334 |
335 |
336 | 341 |
342 |
343 | 348 | 352 |
353 |
354 | 359 |
360 | 375 |
376 | 385 | 386 | 387 |
388 |
389 | 393 |
394 |
395 |
396 |
397 | 398 | 399 |
    400 |
    401 |
    402 | 409 | 434 | 438 | 439 | 440 | 441 | -------------------------------------------------------------------------------- /docs/index.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";var t="undefined"!=typeof Float32Array?Float32Array:Array;Math.hypot||(Math.hypot=function(){for(var t=0,e=arguments.length;e--;)t+=arguments[e]*arguments[e];return Math.sqrt(t)});var e=function(t,e,r,n,i){var o,a=1/Math.tan(e/2);return t[0]=a/r,t[1]=0,t[2]=0,t[3]=0,t[4]=0,t[5]=a,t[6]=0,t[7]=0,t[8]=0,t[9]=0,t[11]=-1,t[12]=0,t[13]=0,t[15]=0,null!=i&&i!==1/0?(o=1/(n-i),t[10]=(i+n)*o,t[14]=2*i*n*o):(t[10]=-1,t[14]=-2*n),t};function r(){var e=new t(3);return t!=Float32Array&&(e[0]=0,e[1]=0,e[2]=0),e}r();const n=[[0,0,0],[29,43,83],[126,37,83],[0,135,81],[171,82,54],[95,87,79],[194,195,199],[255,241,232],[255,0,77],[255,163,0],[255,236,39],[0,228,54],[41,173,255],[131,118,156],[255,119,168],[255,204,170]];class i{constructor(t,e={}){this.objects=t,this.name=e.name,this.backgroundIndex=e.backgroundIndex??0,this.alphaIndex=e.alphaIndex??0,this.zoomLevel=e.zoomLevel,this.texture=e.texture}backgroundColor(){return n[this.backgroundIndex]}alphaColor(){return n[this.alphaIndex]}textureAsImage(t){null==t&&(t=n);const e=new ImageData(128,128),r=e.data,i=this.texture,o=this.alphaIndex;let a=0,s=0;for(let e=0;e<120;e++)for(let e=0;e<128;e++){const e=i[a];if(e!==o){const n=t[e];r[s]=n[0],r[s+1]=n[1],r[s+2]=n[2],r[s+3]=255}a++,s+=4}return e}}class o{constructor(t,e,r,n,i){this.name=t,this.position=e,this.rotation=r,this.vertices=n,this.faces=i}}class a{constructor(t,e,r,n={}){this.indices=t,this.colorIndex=e,this.uvs=r,this.shading=n.shading??!0,this.texture=n.texture??!0,this.doubleSided=n.doubleSided??!1,this.renderFirst=n.renderFirst??!1}color(){return n[this.colorIndex]}}class s{constructor(t,e={}){this.gl=t,this.cull=e.cull??!0,this.shading=e.shading??!0,this.texture=e.texture??!0,this.clearDepth=e.clearDepth??!1,this.vertices=[],this.normals=[],this.uvs=[],this.colorUVs=[],this.triangles=[]}save(){const t=this.gl;this.vertexCount=this.triangles.length,this.isEmpty()||(this.vertexBuffer=t.createBuffer(),this.uvBuffer=t.createBuffer(),this.colorUVBuffer=t.createBuffer(),this.triangleBuffer=t.createBuffer(),this.normalBuffer=t.createBuffer(),t.bindBuffer(t.ARRAY_BUFFER,this.vertexBuffer),t.bufferData(t.ARRAY_BUFFER,new Float32Array(this.vertices),t.STATIC_DRAW),t.bindBuffer(t.ARRAY_BUFFER,this.uvBuffer),t.bufferData(t.ARRAY_BUFFER,new Float32Array(this.uvs),t.STATIC_DRAW),t.bindBuffer(t.ARRAY_BUFFER,this.colorUVBuffer),t.bufferData(t.ARRAY_BUFFER,new Float32Array(this.colorUVs),t.STATIC_DRAW),t.bindBuffer(t.ELEMENT_ARRAY_BUFFER,this.triangleBuffer),t.bufferData(t.ELEMENT_ARRAY_BUFFER,new Uint16Array(this.triangles),t.STATIC_DRAW),t.bindBuffer(t.ARRAY_BUFFER,this.normalBuffer),t.bufferData(t.ARRAY_BUFFER,new Float32Array(this.normals),t.STATIC_DRAW)),this.uvs=null,this.colorUVs=null,this.normals=null,this.vertices=null,this.triangles=null}isEmpty(){return 0===this.vertexCount}free(){const t=this.gl;t.deleteBuffer(this.vertexBuffer),t.deleteBuffer(this.uvBuffer),t.deleteBuffer(this.colorUVBuffer),t.deleteBuffer(this.triangleBuffer),t.deleteBuffer(this.normalBuffer)}}class l{constructor(t){this.gl=t,this.vertices=[]}save(){const t=this.gl;this.vertexCount=Math.floor(this.vertices.length/3),this.vertexBuffer=t.createBuffer(),t.bindBuffer(t.ARRAY_BUFFER,this.vertexBuffer),t.bufferData(t.ARRAY_BUFFER,new Float32Array(this.vertices),t.STATIC_DRAW),this.vertices=null}free(){this.gl.deleteBuffer(this.vertexBuffer)}}function u(t,e,r){const{passes:n,wireframe:i}=function(t,e,r){const n=[];for(let e=0;e<16;e++){const r=e%2<1,i=e%4<2,o=e%8<4;n.push(new s(t,{cull:!r,shading:i,texture:o,clearDepth:8===e}))}const i=new l(t),o=i.vertices;for(const t of e.objects){const e=t.position,i=t.vertices.map((t=>[-t[0]-e[0],-t[1]-e[1],t[2]+e[2]]));for(const e of t.faces){const t=e.indices,a=e.uvs,s=n[(e.doubleSided?0:1)+(e.shading?0:2)+(e.texture?0:4)+(e.renderFirst?0:8)],l=s.vertices,u=s.triangles,f=s.normals,d=s.uvs,m=s.colorUVs,g=.03125+e.colorIndex/16,p=0,x=Math.floor(l.length/3),v=[],_=[];for(let e=0;e1){const t=v[0],e=v[1],n=v[2],i=v[3],o=_[0],a=_[1],s=_[2],c=_[3];for(let u=0;u<=r;u++){const x=u/r,v=[h(t[0],e[0],x),h(t[1],e[1],x),h(t[2],e[2],x),h(o[0],a[0],x),h(o[1],a[1],x)],_=[h(i[0],n[0],x),h(i[1],n[1],x),h(i[2],n[2],x),h(c[0],s[0],x),h(c[1],s[1],x)];for(let t=0;t<=r;t++){const e=t/r;l.push(h(v[0],_[0],e),h(v[1],_[1],e),h(v[2],_[2],e)),d.push(h(v[3],_[3],e),h(v[4],_[4],e)),m.push(g,p),f.push(T[0],T[1],T[2])}}for(let t=0;t0)return[u[0]/h,u[1]/h,u[2]/h]}var e,r;return[1,0,0]}function f(t){return Math.hypot(t[0],t[1],t[2])}class d{constructor(t,e,r){this.gl=t;const n=this.createShader(t.VERTEX_SHADER,e),i=this.createShader(t.FRAGMENT_SHADER,r);if(this.program=t.createProgram(),t.attachShader(this.program,n),t.attachShader(this.program,i),t.linkProgram(this.program),t.deleteShader(n),t.deleteShader(i),!t.getProgramParameter(this.program,t.LINK_STATUS)){const e=t.getProgramInfoLog(this.program);throw t.deleteProgram(this.program),Error("program compilation failed: "+e)}this.vertexLocation=this.getAttribLocation("vertex")}createShader(t,e){const r=this.gl,n=r.createShader(t);if(r.shaderSource(n,e),r.compileShader(n),!r.getShaderParameter(n,r.COMPILE_STATUS)){const e=r.getShaderInfoLog(n);throw r.deleteShader(n),Error(`${t===r.FRAGMENT_SHADER?"fragment":"vertex"} shader compilation failed: ${e}`)}return n}getAttribLocation(t){return this.gl.getAttribLocation(this.program,t)}getUniformLocation(t){return this.gl.getUniformLocation(this.program,t)}use(){this.gl.useProgram(this.program)}free(){this.gl.deleteProgram(this.program)}}function m(){return function(t,e,r){const i=new ImageData(t,e),o=i.data,a=t*e;let s=0;for(let t=0;t="0"&&r<="9")return o();throw Error("Unkown value ("+e+'): "'+r+'" = '+r.charCodeAt(0))}function n(){e++;const n={array:[],dict:Object.create(null)};for(a();;){const i=t.charAt(e);if("}"===i){e++;break}let o;if(i>="a"&&i<="z"){let r=e;for(e++;;){if("="===t.charAt(e))break;e++}o=t.slice(r,e),e++}const s=r();null==o?n.array.push(s):n.dict[o]=s,a();","===t.charAt(e)&&(e++,a())}return n}function i(){const r=e,n=t.indexOf("'",e+1);if(n<0)throw Error("No end!!!");if(e=n+1,e===r)throw Error("!!!!");return t.slice(r+1,n)}function o(){const r=e;for(;;){const r=t.charAt(e);if(!("-"===r||"."===r||r>="0"&&r<="9"))break;e++}if(e===r)throw Error("!!!!");return Number(t.slice(r,e))}function a(){for(;;){const r=t.charAt(e);if(" "!==r&&"\n"!==r&&"\r"!==r&&"\t"!==r)break;e++}}}(t)}function p(t){let e=0,r=t.length;for(;eNumber(t))),[c,f]=function(t,e){const r=t.indexOf(e);return r<0?[t,""]:[t.slice(0,r),t.slice(r+e.length)]}(r,"%"),d=g(c),m=d.array.map((t=>{const e=t.dict.name,r=t.dict.pos.array,n=t.dict.rot.array,i=t.dict.v.array.map((t=>t.array)),s=t.dict.f.array.map((t=>{const e=t.array.map((t=>t-1)),r=t.dict.c,n=t.dict.uv.array,i=[];for(let t=1;tMath.floor(255.999*t)))}function T(t,e,r){return.2126*t+.7152*e+.0722*r}const b="R0lGODdhgACAAIAAAAAAAP///yH5BAkKAAAALAAAAACAAIAAAAL/hI+py+0Po5y02ouz3rz7D4biSJbmiabqyrbuC5vBHBtBcitz/ty859PpaIgdjngIFgE/pvMIxTWaRpAy+UwibVtnM9sDM7tVLOP7FQOHVDJZfLWp3cLzMV2PYsow6oQvJ4GGV1NoeIiYqLgYJoUGBhg3tpBWKUQogrnkeIfUZaaVt0kpCmr1h9pWyhVB2Pba+mkXsuP5dtk6tarGS6E5CiH5CJu7KjkruMu4zNzs/BwTOUiMRR14h10k61ucXa1sueSnDFf+uDsNzinlHaptPRauBc9dr70OfI1OKs7/bewvXzxy7Qa+K6jP4LyACgUydKgOmsSJFCtaRHXx2TE6/0P0LDzo0UvBSOXYhGTVL6U7h8IG7TvH7l5MkP9qJuTiZxy+m7xawhQojFTOnShtNiyKdCVPkvA+bRyqMmnGqVSrWr3KzOmZnz24TupIUw5Uo0GbIpspMpg5oniGsZ1jslTbsxWkgQXI1o3XhPLQmqq7Ne/Dr0DfqBo59ySlbVgbO34M+aE0lzLDHiUsFvHYpRwVb077NzPLwRsllxy5Nljn0KClao07+qSlz8eEvsUV++1et6Np98pL+aNwzrRXix7eOjnjyMybO6cIaI5Ts76rI3xd+ULZ2wCtR01+sy9ruKgPW4brvbBXWZWo+8V8HChu5EwZJn6/l/zd+PA5G/8zrNl+4f3yXIEGHghNLUYEwSA7Cl6jU1g0bDFZOw/GUwtNF5qiIBENYrggD56AkiFOWWw44Q98XFjFhAOpWJktInYoUoMLEuYiXxSWCKKJLZbYoQ8zgkjhPULi5CGNNYrlYogimsFjj8OlKGFaP5a04pAmInnjhygCyRo96UQl5Iw/sqjXLQiuyWab0UQJB48b+gIkjTZuCaFn2eTw5BR8yuiNk8qRyKArS/I1SpRwvtLkkrYQCeNKHormZYplZngnhhza10mfmiT5Z5Za9hepF4XqcmeOSvTF53Gt/jbPiBDKCacgdT6Y6aQ8NXSkO08S6Gawwg7rQK2QEIrdrlj/dhLaorSStMmIigp4qShyLnZmdGYShOieyZrZZ0zAliqXPkr+idxM65mbh3l5LuWPihHKpwunEGmqnKFQMqsUns1O56m8weXjbLTLEYtwwm6+Wg2j+xYlr1polsFeng6TySsboFocSJmEdmWwjJFue+KKGg95BcWrvvtqy4mixXC4fymJ3qrQenqiI0du7LKp4f76xMj4Dv3VqOi+e+xiscqk66Mdw0dx0bbd+KJc2ppltcJab621TuCSy2XD5wHLwZxJMSoziR5RGaOuHTMs8T71zrxop75GayTeSPM6ZthICtgatKfJHXSSe/eaWqPqchxxRPaO3Z7BesD999H0/Yptmb5rG8t15543N++/Rdr9MKnTihurwP2orl62X+vtd6Jm95Q67GkP9Utb48z7865Co8QV5U/nKKW4igKoduxh0hUetXNn18u6OSetZm7K6xiFNdOhs9vn3n8PeiyC40XcjvYdX30ddZPur+zRUb90vOYam7sdYBev7Hz9sjt9vWDzpjzhCWx373kepmBWusEMb3AGFEfa4MW+SfCueiTb39SsJaoraexe4OugByWSwXeMDkb3K96XVNYM8cyChLnpndw+NB5sbWCAIvxdfFxIu/j5hYbQQwrNiqVAVlmJf/rZYXccV5dnDWxTOEpT4zbnLtHR44NUrGJGCgAAOw==";let A=!1,E=null;function y(){return A||(A=!0,async function(){return new Promise(((t,e)=>{let r=new Image;r.onload=()=>t(r),r.onerror=()=>e("could not load PICO-8 font"),r.src="data:image/gif;base64,"+b}))}().then((t=>E=t))),E} 2 | /* 3 | * @license 4 | * Adapted from lzwCompress.js 5 | * 6 | * Copyright (c) 2012-2021 floydpink 7 | * Licensed under the MIT license. 8 | */ 9 | const w={};function R(t){const e=function(t){function e(t){t=t.toLowerCase();for(let e=0;e0&&s(r,6)}s(0,6)}function r(t){for(const e of t)n(e)}function n(t){const e=Math.round(64*t)/64;if(e>=-16&&e<=15.75&&Number.isInteger(4*e))u.push(192+4*e);else{const r=8192+64*e;if(r>=32768)throw Error(`can't encode float "${t}"`);u.push((65280&r)>>8),u.push(255&r)}}let i=0,o=0;function a(){o>0&&u.push(i),i=0,o=0}function s(t,e){for(let r=0;r>r)}}function l(t){i+=t<=8&&(u.push(i),o=0,i=0)}const u=[];if(u.push(U),e(t.name),s(t.zoomLevel,7),s(t.backgroundIndex,4),s(t.alphaIndex,4),a(),t.objects.length>=256)throw Error("Too many objects");u.push(t.objects.length);for(const n of t.objects){if(e(n.name),a(),r(n.position),n.vertices.length>=256)throw Error("Too many vertices on object");u.push(n.vertices.length);for(const t of n.vertices)r(t);const t=k(n.vertices.length-1);if(n.faces.length>=256)throw Error("Too many faces on object");u.push(n.faces.length);for(const e of n.faces){s(e.colorIndex,4),l(e.texture?1:0),l(e.shading?1:0),l(e.doubleSided?1:0),l(e.renderFirst?1:0);for(const r of e.indices)s(r,t);if(s(e.indices[0],t),e.texture)for(const t of e.uvs)s(32+4*t[0],7),s(32+4*t[1],7)}a()}let h=t.texture.length-1;const c=t.texture[h];for(;h>=1&&t.texture[h-1]===c;)h--;const f=[];for(let e=0;e<=h;){const r=t.texture[e];let n=0;for(e++;e<=h&&t.texture[e]===r&&(e++,n++,15!=n););f.push((r<<4)+n)}if(f.length=i&&(o++,i*=2);const s=t[a];for(let t=0;t>t<=8&&(e.push(r),n=0,r=0)}}n>0&&e.push(r);return e}(function(t){if(!t||!0===t||t instanceof Date)return t;let e=t;return"object"==typeof t&&(e=w.KeyOptimize.pack(JSON.stringify(t))),w.LZWCompress.pack(e)}(F(e)));let n=btoa(F(r)),i=n.indexOf("=",n.length-4);return i>=0&&(n=n.slice(0,i)),n}function B(t){const e=function(t){const e=[];let r=0,n=0,i=512,o=9;for(let a=0;a>t<=o&&(e.push(r),n=0,r=0,256+e.length>=i&&(o++,i*=2))}}return e}(C(atob(t)));return function(t){function e(){let t="";for(;;){const e=c(6);if(0===e)break;if(e>=M.length)throw Error("invalid encoded string");t+=M.charAt(e)}return t}function r(){if(f=128)return((127&t)-64)/4;return((t<<8)+r()-8192)/64}function l(t){const e=Array(t);for(let r=0;r0&&(f++,u=0)}function c(e){let r=t[f],n=0;for(let i=0;i>u<=8){if(u=0,f++,f>=t.length)throw Error("Unexpected end of input");r=t[f]}}return n}let f=0;const d=r();if(d!==U)throw Error(`invalid encoding version ${d}`);const m=e(),g=c(7),p=c(4),x=c(4);h();const v=r(),_=Array(v);for(let t=0;t>4,r=1+(15&t);for(let t=0;t>4,15&t)}}if(b.length<15360){const t=b[b.length-1];for(;b.length<15360;)b.push(t)}return new i(_,{name:m,alphaIndex:x,backgroundIndex:p,zoomLevel:g,texture:b})}(C(function(t){if(!t||!0===t||t instanceof Date)return t;let e,r=w.LZWCompress.unpack(t);try{e=JSON.parse(r)}catch(t){return r}return"object"==typeof e&&(r=w.KeyOptimize.unpack(e)),r}(e)))}function F(t){let e="";for(const r of t)e+=String.fromCharCode(r);return e}function C(t){const e=Array(t.length);for(let r=0;r{let e=t.data;"gif"===e.type&&(it.hidden=!0,function(t){const e=`${ht.model.name}.gif`,r=new File([t],e,{type:"image/gif"}),n=URL.createObjectURL(r),i=document.createElement("a");i.href=n,i.download=e,document.body.append(i),i.click(),i.remove(),URL.revokeObjectURL(n),console.log(`downloaded ${e} ${r.size/1024}kb`)}(e.data)),ut||(ut=!0,nt.disabled=!1)};const ht=new class{constructor(t={}){this.canvas=t.canvas,null==this.canvas&&(this.canvas=document.createElement("canvas"));const e=this.gl=this.canvas.getContext("webgl",{antialias:!1,preserveDrawingBuffer:t.preserveDrawingBuffer??!1});e.enable(e.BLEND),e.blendFunc(e.SRC_ALPHA,e.ONE_MINUS_SRC_ALPHA),this.cameraPosition={x:0,y:0,z:0},this.cameraRotation={x:0,y:0,z:0},this.cameraFOV=t.fov??90,this.loaded=!1,this.model=null,this.shading=t.shading??!0,this.renderMode=t.renderMode??"texture",this.backgroundColor=null,this.outlineColor=[0,0,0,1],this.outlineSize=0,this._watermarkText=null,this._watermarkSize=null,this._watermarkBuffer=null,this._watermarkTriangleCount=0,this.drawWireframe=t.drawWireframe??!1,this.wireframeXray=t.wireframeXray??!0,this.wireframeColor=t.wireframeColor??[1,1,1],this.tesselationCount=t.tesselationCount??3,this.lightDirection=t.lightDirection??{x:1,y:-1,z:0},this.hdOptions={shadingSteps:4,shadingColor:[.1,.1,.1]},this._passes=[],this._colorIndexTex=this._createTexture(null,this.gl.LUMINANCE,this.gl.LUMINANCE,this.gl.UNSIGNED_BYTE,new Uint8Array([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]),16,1),this._indexTex=null,this._fontTex=null,this.resetLightMap(),this._programTexture=function(t){const e=new d(t,"\n\t\tattribute vec4 vertex;\n\t\tattribute vec3 normal;\n\t\tattribute vec2 uv;\n\n\t\tvarying highp vec2 v_uv;\n\t\tvarying highp vec3 v_normal;\n\n\t\tuniform mat4 mvp;\n\n\t\tvoid main() {\n\t\t\tv_normal = normal;\n\t\t\tv_uv = uv;\n\t\t\tgl_Position = mvp * vertex;\n\t\t}\n\t","\n\t\tvarying highp vec2 v_uv;\n\t\tvarying highp vec3 v_normal;\n\t\t\n\t\tuniform sampler2D indexTex;\n\t\tuniform sampler2D lightMap;\n\t\tuniform highp vec3 lightDir;\n\t\tuniform highp float lightMapOffset;\n\t\tuniform highp float lightMapGradient;\n\n\t\tvoid main() {\n\t\t\thighp float index = texture2D(indexTex, v_uv).r;\n\t\t\tif (index == 1.0) discard;\n\t\t\thighp float intensity = clamp(lightMapGradient * abs(dot(v_normal, lightDir)) + lightMapOffset, 0.0, 1.0);\n\t\t\tgl_FragColor = texture2D(lightMap, vec2(0.015625 + index * 15.9375 + mod(gl_FragCoord.x + gl_FragCoord.y, 2.0) * 0.03125, 1.0 - intensity));\n\t\t}\n\t");return{program:e,locations:{uv:e.getAttribLocation("uv"),normal:e.getAttribLocation("normal"),indexTex:e.getUniformLocation("indexTex"),lightMap:e.getUniformLocation("lightMap"),lightDir:e.getUniformLocation("lightDir"),mvp:e.getUniformLocation("mvp"),lightMapOffset:e.getUniformLocation("lightMapOffset"),lightMapGradient:e.getUniformLocation("lightMapGradient")}}}(e),this._programUnlitTexture=function(t){const e=new d(t,"\n\t\tattribute vec4 vertex;\n\t\tattribute vec2 uv;\n\n\t\tvarying highp vec2 v_uv;\n\n\t\tuniform mat4 mvp;\n\n\t\tvoid main() {\n\t\t\tv_uv = uv;\n\t\t\tgl_Position = mvp * vertex;\n\t\t}\n\t","\n\t\tvarying highp vec2 v_uv;\n\t\t\n\t\tuniform sampler2D indexTex;\n\t\tuniform sampler2D lightMap;\n\n\t\tvoid main() {\n\t\t\thighp float index = texture2D(indexTex, v_uv).r;\n\t\t\tif (index == 1.0) discard;\n\t\t\tgl_FragColor = texture2D(lightMap, vec2(0.015625 + index * 15.9375, 0.0));\n\t\t}\n\t");return{program:e,locations:{uv:e.getAttribLocation("uv"),mvp:e.getUniformLocation("mvp"),indexTex:e.getUniformLocation("indexTex"),lightMap:e.getUniformLocation("lightMap")}}}(e),this._programHDTexture=function(t){const e=new d(t,"\n\t\tattribute vec4 vertex;\n\t\tattribute vec3 normal;\n\t\tattribute vec2 uv;\n\n\t\tvarying highp vec2 v_uv;\n\t\tvarying highp vec3 v_normal;\n\n\t\tuniform mat4 mvp;\n\n\t\tvoid main() {\n\t\t\tv_uv = uv;\n\t\t\tv_normal = normal;\n\t\t\tgl_Position = mvp * vertex;\n\t\t}\n\t","\n\t\tvarying highp vec2 v_uv;\n\t\tvarying highp vec3 v_normal;\n\t\t\n\t\tuniform highp float lightSteps;\n\t\tuniform sampler2D mainTex;\n\t\tuniform highp vec3 lightDir;\n\t\tuniform highp vec3 lightAmbient;\n\n\t\tvoid main() {\n\t\t\thighp vec4 col = texture2D(mainTex, v_uv);\n\t\t\tif (col.a != 1.0) discard;\n\t\t\thighp float pixel = mod(gl_FragCoord.x + gl_FragCoord.y, 2.0);\n\t\t\thighp float intensity = abs(dot(v_normal, lightDir)) * 2.2 - 0.2;\n\t\t\tintensity = floor(intensity * (lightSteps + 0.5) + pixel/2.0) / lightSteps;\n\t\t\tintensity = clamp(intensity, 0.0, 1.0);\n\t\t\tgl_FragColor = vec4(mix(col.rgb * lightAmbient, col.rgb, intensity), 1.0);\n\t\t}\n\t");return{program:e,locations:{uv:e.getAttribLocation("uv"),normal:e.getAttribLocation("normal"),mvp:e.getUniformLocation("mvp"),lightSteps:e.getUniformLocation("lightSteps"),lightAmbient:e.getUniformLocation("lightAmbient"),mainTex:e.getUniformLocation("mainTex"),lightDir:e.getUniformLocation("lightDir")}}}(e),this._wireframe=null,this._programWireframe=function(t){const e=new d(t,"\n\t\tattribute vec4 vertex;\n\n\t\tuniform mat4 mvp;\n\n\t\tvoid main() {\n\t\t\tgl_Position = mvp * vertex;\n\t\t}\n\t","\n\t\tuniform lowp vec4 color;\n\n\t\tvoid main() {\n\t\t\tgl_FragColor = color;\n\t\t}\n\t");return{program:e,locations:{mvp:e.getUniformLocation("mvp"),color:e.getUniformLocation("color")}}}(e),this._programText=function(t){const e=new d(t,"\n\t\tattribute vec2 vertex;\n\t\tattribute vec2 uv;\n\n\t\tvarying highp vec2 v_uv;\n\n\t\tuniform highp vec4 data;\n\n\t\tvoid main() {\n\t\t\tv_uv = uv;\n\t\t\tgl_Position = vec4((data.xy + vertex) * data.zw, 0.0, 1.0);\n\t\t}\n\t","\n\t\tvarying highp vec2 v_uv;\n\t\t\n\t\tuniform sampler2D mainTex;\n\t\tuniform lowp vec4 color;\n\n\t\tvoid main() {\n\t\t\tgl_FragColor = texture2D(mainTex, v_uv) * color;\n\t\t}\n\t");return{program:e,locations:{uv:e.getAttribLocation("uv"),data:e.getUniformLocation("data"),mainTex:e.getUniformLocation("mainTex"),color:e.getUniformLocation("color")}}}(e),this._programFramebuffer=function(t){const e=new d(t,"\n\t\tattribute vec4 vertex;\n\n\t\tvarying highp vec2 v_uv;\n\n\t\tvoid main() {\n\t\t\tv_uv = 0.5 + vertex.xy * 0.5;\n\t\t\tgl_Position = vertex;\n\t\t}\n\t","\n\t\tvarying highp vec2 v_uv;\n\t\t\n\t\tuniform sampler2D mainTex;\n\n\t\tvoid main() {\n\t\t\tgl_FragColor = texture2D(mainTex, v_uv);\n\t\t}\n\t");return{program:e,locations:{mainTex:e.getUniformLocation("mainTex")}}}(e),this._programOutline=function(t){const e=new d(t,"\n\t\tattribute vec4 vertex;\n\n\t\tvarying highp vec2 v_uv;\n\n\t\tvoid main() {\n\t\t\tv_uv = 0.5 + vertex.xy * 0.5;\n\t\t\tgl_Position = vertex;\n\t\t}\n\t","\n\t\tvarying highp vec2 v_uv;\n\t\t\n\t\tuniform sampler2D mainTex;\n\t\tuniform highp vec2 pixel;\n\t\tuniform lowp vec4 outlineColor;\n\n\t\tvoid main() {\n\t\t\tlowp float a = 1.0 - texture2D(mainTex, v_uv).a;\n\t\t\tlowp float b = texture2D(mainTex, v_uv + vec2(pixel.x, 0.0)).a + texture2D(mainTex, v_uv - vec2(pixel.x, 0.0)).a + texture2D(mainTex, v_uv + vec2(0.0, pixel.y)).a + texture2D(mainTex, v_uv - vec2(0.0, pixel.y)).a;\n\t\t\tgl_FragColor = mix(\n\t\t\t\tmix(\n\t\t\t\t\ttexture2D(mainTex, v_uv),\n\t\t\t\t\tvec4(0.0, 0.0, 0.0, 0.0),\n\t\t\t\t\ta\n\t\t\t\t),\n\t\t\t\toutlineColor,\n\t\t\t\tmin(1.0, a * b)\n\t\t\t);\n\t\t}\n\t");return{program:e,locations:{mainTex:e.getUniformLocation("mainTex"),pixel:e.getUniformLocation("pixel"),outlineColor:e.getUniformLocation("outlineColor")}}}(e),this._depthBuffer=e.createRenderbuffer(),this._frameBuffer=e.createFramebuffer(),this._frameBuffer2=e.createFramebuffer(),this._framebufferCurrent=null,this._screenQuads=e.createBuffer(),e.bindBuffer(e.ARRAY_BUFFER,this._screenQuads),e.bufferData(e.ARRAY_BUFFER,new Float32Array([-1,-1,1,-1,-1,1,-1,1,1,-1,1,1]),e.STATIC_DRAW);const r=t.resolution;null==r?this.setResolution(128,128,1):this.setResolution(r.width,r.height,r.scale??1),e.enable(e.DEPTH_TEST),e.depthFunc(e.LEQUAL),e.pixelStorei(e.UNPACK_ALIGNMENT,1)}setResolution(t,e,r=1){if(null!=this._resolution&&this._resolution[0]===t&&this._resolution[1]===e&&this._resolution[2]===r)return;this._resolution=[t,e,r,0,0];const n=t*r,i=e*r,o=this.gl,a=this.canvas;a.width=n,a.height=i,this._frameBufferTex=this._createTexture(this._frameBufferTex,o.RGBA,o.RGBA,o.UNSIGNED_BYTE,null,t,e),this._frameBufferTex2=this._createTexture(this._frameBufferTex2,o.RGBA,o.RGBA,o.UNSIGNED_BYTE,null,t,e),o.bindRenderbuffer(o.RENDERBUFFER,this._depthBuffer),o.renderbufferStorage(o.RENDERBUFFER,o.DEPTH_COMPONENT16,t,e),o.bindFramebuffer(o.FRAMEBUFFER,this._frameBuffer),o.framebufferTexture2D(o.FRAMEBUFFER,o.COLOR_ATTACHMENT0,o.TEXTURE_2D,this._frameBufferTex,0),o.framebufferRenderbuffer(o.FRAMEBUFFER,o.DEPTH_ATTACHMENT,o.RENDERBUFFER,this._depthBuffer),o.bindFramebuffer(o.FRAMEBUFFER,this._frameBuffer2),o.framebufferTexture2D(o.FRAMEBUFFER,o.COLOR_ATTACHMENT0,o.TEXTURE_2D,this._frameBufferTex2,0),a.style.width=`${n}px`,a.style.height=`${i}px`}getResolution(){return{width:this._resolution[0],height:this._resolution[1],scale:this._resolution[2]}}get modelTexture(){return this.getModelTexture()}getModelTexture(){return this.model.textureAsImage(this._lightMapColors)}resetLightMap(){null!=this._lightMapTex&&this.gl.deleteTexture(this._lightMapTex),this._lightMapTex=this._createTexture(null,this.gl.RGB,this.gl.RGB,this.gl.UNSIGNED_BYTE,m()),this._lightMapColors=n.slice()}getPalette(){let t=this._lightMapColors.map((t=>[t[0],t[1],t[2],255]));if(this.drawWireframe){const e=_(this.wireframeColor);4!=e.length&&e.push(255),t.push(e)}if(this.outlineSize>=1){const e=_(this.outlineColor);4!=e.length&&e.push(255),t.push(e)}if(null!=this.backgroundColor){const e=_(this.backgroundColor);4!=e.length&&e.push(255),t.push(e)}return t}getRenderedBackgroundColor(){if(null==this.backgroundColor){const t=this._lightMapColors[this.model.backgroundIndex];return[t[0],t[1],t[2],255]}return[Math.floor(255.999*this.backgroundColor[0]),Math.floor(255.999*this.backgroundColor[1]),Math.floor(255.999*this.backgroundColor[2]),this.backgroundColor.length<=3?255:Math.floor(255.999*this.backgroundColor[3])]}_getLightMapColors(t){const e=t.data,r=Array(16),n=new Set;for(let t=0;t<16;t++){let i=8*t,o=e[i],a=e[i+1],s=e[i+2],l=[o,a,s];r[t]=l,n.add(o+256*a+65536*s)}for(let t=0;t[4278190080|r<<16|e<<8|t,this.model.alphaIndex===n?255:n]))),a=new Int32Array(t.data.buffer);for(let t=0;t=65&&r<=90?t=r-65+97:r>=97&&r<=122?t=r-97+65:r<128?t=r:r>=65280&&r<=65374?t=r-65280+32:r>=12352&&r<=12543&&(t=154,r>=12448&&(r-=96,t=204),r<=12362?t+=Math.floor((r-12353)/2):12387===r?t+=46:r<=12393?(r>12387&&r--,r-=12363,t+=5+Math.floor(r/2),r%2==1&&(e.push(t),t=30)):r<=12398?t+=r-12394+20:r<=12413?(r-=12399,t+=25+Math.floor(r/3),r%3==1&&(e.push(t),t=30),r%3==2&&(e.push(t),t=31)):r<=12418?t+=r-12414+30:r<=12424?(r-=12419,t+=35+Math.floor(r/2),r%2==0&&(t+=12)):r<=12429?t+=r-12425+38:r<=12431?t+=43:12434===r?t+=44:12435===r&&(t+=45))}e.push(t)}let n=new Float32Array(24*e.length),i=0,o=0,a=0,s=0,l=(t,e,r,o)=>{n[i++]=t,n[i++]=e,n[i++]=r,n[i++]=o};for(let t=0;t{if(!t.type.startsWith("text"))throw Error("picoCAD file must be a text file");const n=new FileReader;n.onload=()=>{e(this._loadString(n.result))},n.onerror=()=>{r(n.error)},n.readAsText(t)}))}_loadString(t){this._loadModel(x(t))}_loadModel(t){const e=this.gl;if(this.loaded=!1,this.model=null,this.loaded){for(const t of this._passes)t.free();this._passes=[],this._wireframe.free(),e.deleteTexture(this._indexTex),this._indexTex=null}const r=u(e,t,this.tesselationCount);this._passes=r.passes,this._wireframe=r.wireframe,this._indexTex=this._createTexture(this._indexTex,e.LUMINANCE,e.LUMINANCE,e.UNSIGNED_BYTE,new Uint8Array(r.textureIndices),128,128),this.loaded=!0,this.model=t}draw(){const r="none"!==this.renderMode,n="color"===this.renderMode;if(!this.loaded||!r&&!this.drawWireframe)return;const i=this.gl,o=this.canvas;i.bindFramebuffer(i.FRAMEBUFFER,this._frameBuffer),i.viewport(0,0,this._resolution[0],this._resolution[1]),i.clearColor(0,0,0,0),i.clearDepth(1),i.clear(i.COLOR_BUFFER_BIT|i.DEPTH_BUFFER_BIT);const a=(s=new t(16),t!=Float32Array&&(s[1]=0,s[2]=0,s[3]=0,s[4]=0,s[6]=0,s[7]=0,s[8]=0,s[9]=0,s[11]=0,s[12]=0,s[13]=0,s[14]=0),s[0]=1,s[5]=1,s[10]=1,s[15]=1,s);var s;e(a,this.cameraFOV*Math.PI/180,this._resolution[0]/this._resolution[1],.1,400),function(t,e,r){var n=Math.sin(r),i=Math.cos(r),o=e[4],a=e[5],s=e[6],l=e[7],u=e[8],h=e[9],c=e[10],f=e[11];e!==t&&(t[0]=e[0],t[1]=e[1],t[2]=e[2],t[3]=e[3],t[12]=e[12],t[13]=e[13],t[14]=e[14],t[15]=e[15]),t[4]=o*i+u*n,t[5]=a*i+h*n,t[6]=s*i+c*n,t[7]=l*i+f*n,t[8]=u*i-o*n,t[9]=h*i-a*n,t[10]=c*i-s*n,t[11]=f*i-l*n}(a,a,this.cameraRotation.x),function(t,e,r){var n=Math.sin(r),i=Math.cos(r),o=e[0],a=e[1],s=e[2],l=e[3],u=e[8],h=e[9],c=e[10],f=e[11];e!==t&&(t[4]=e[4],t[5]=e[5],t[6]=e[6],t[7]=e[7],t[12]=e[12],t[13]=e[13],t[14]=e[14],t[15]=e[15]),t[0]=o*i-u*n,t[1]=a*i-h*n,t[2]=s*i-c*n,t[3]=l*i-f*n,t[8]=o*n+u*i,t[9]=a*n+h*i,t[10]=s*n+c*i,t[11]=l*n+f*i}(a,a,this.cameraRotation.y),function(t,e,r){var n=Math.sin(r),i=Math.cos(r),o=e[0],a=e[1],s=e[2],l=e[3],u=e[4],h=e[5],c=e[6],f=e[7];e!==t&&(t[8]=e[8],t[9]=e[9],t[10]=e[10],t[11]=e[11],t[12]=e[12],t[13]=e[13],t[14]=e[14],t[15]=e[15]),t[0]=o*i+u*n,t[1]=a*i+h*n,t[2]=s*i+c*n,t[3]=l*i+f*n,t[4]=u*i-o*n,t[5]=h*i-a*n,t[6]=c*i-s*n,t[7]=f*i-l*n}(a,a,this.cameraRotation.z),function(t,e,r){var n,i,o,a,s,l,u,h,c,f,d,m,g=r[0],p=r[1],x=r[2];e===t?(t[12]=e[0]*g+e[4]*p+e[8]*x+e[12],t[13]=e[1]*g+e[5]*p+e[9]*x+e[13],t[14]=e[2]*g+e[6]*p+e[10]*x+e[14],t[15]=e[3]*g+e[7]*p+e[11]*x+e[15]):(n=e[0],i=e[1],o=e[2],a=e[3],s=e[4],l=e[5],u=e[6],h=e[7],c=e[8],f=e[9],d=e[10],m=e[11],t[0]=n,t[1]=i,t[2]=o,t[3]=a,t[4]=s,t[5]=l,t[6]=u,t[7]=h,t[8]=c,t[9]=f,t[10]=d,t[11]=m,t[12]=n*g+s*p+c*x+e[12],t[13]=i*g+l*p+f*x+e[13],t[14]=o*g+u*p+d*x+e[14],t[15]=a*g+h*p+m*x+e[15])}(a,a,[this.cameraPosition.x,this.cameraPosition.y,this.cameraPosition.z]);const l=function(t){let e=Math.hypot(t.x,t.y,t.z);0===e&&(e=1);return{x:t.x/e,y:t.y/e,z:t.z/e}}(this.lightDirection);if(r)for(const t of this._passes){if(t.clearDepth&&i.clear(i.DEPTH_BUFFER_BIT),t.isEmpty())continue;const e=n||!t.texture,r=null==this._hdTex||e?this.shading&&t.shading?this._programTexture:this._programUnlitTexture:this._programHDTexture;r.program.use(),i.uniformMatrix4fv(r.locations.mvp,!1,a),i.bindBuffer(i.ARRAY_BUFFER,t.vertexBuffer),i.vertexAttribPointer(r.program.vertexLocation,3,i.FLOAT,!1,0,0),i.enableVertexAttribArray(r.program.vertexLocation),i.bindBuffer(i.ARRAY_BUFFER,e?t.colorUVBuffer:t.uvBuffer),i.vertexAttribPointer(r.locations.uv,2,i.FLOAT,!1,0,0),i.enableVertexAttribArray(r.locations.uv),r===this._programUnlitTexture?(i.activeTexture(i.TEXTURE0),i.bindTexture(i.TEXTURE_2D,e?this._colorIndexTex:this._indexTex),i.uniform1i(r.locations.indexTex,0),i.activeTexture(i.TEXTURE1),i.bindTexture(i.TEXTURE_2D,this._lightMapTex),i.uniform1i(r.locations.lightMap,1)):r===this._programTexture?(i.activeTexture(i.TEXTURE0),i.bindTexture(i.TEXTURE_2D,e?this._colorIndexTex:this._indexTex),i.uniform1i(r.locations.indexTex,0),i.activeTexture(i.TEXTURE1),i.bindTexture(i.TEXTURE_2D,this._lightMapTex),i.uniform1i(r.locations.lightMap,1),i.uniform1f(r.locations.lightMapOffset,-.3571428571428572),i.uniform1f(r.locations.lightMapGradient,2.857142857142857),i.uniform3f(r.locations.lightDir,l.x,l.y,l.z),i.bindBuffer(i.ARRAY_BUFFER,t.normalBuffer),i.vertexAttribPointer(r.locations.normal,3,i.FLOAT,!1,0,0),i.enableVertexAttribArray(r.locations.normal)):r===this._programHDTexture&&(i.activeTexture(i.TEXTURE0),i.bindTexture(i.TEXTURE_2D,this._hdTex),i.uniform1i(r.locations.mainTex,0),i.uniform3f(r.locations.lightDir,l.x,l.y,l.z),i.uniform1f(r.locations.lightSteps,this.hdOptions.shadingSteps),i.uniform3f(r.locations.lightAmbient,this.hdOptions.shadingColor[0],this.hdOptions.shadingColor[1],this.hdOptions.shadingColor[2]),i.bindBuffer(i.ARRAY_BUFFER,t.normalBuffer),i.vertexAttribPointer(r.locations.normal,3,i.FLOAT,!1,0,0),i.enableVertexAttribArray(r.locations.normal)),t.cull?i.enable(i.CULL_FACE):i.disable(i.CULL_FACE),i.bindBuffer(i.ELEMENT_ARRAY_BUFFER,t.triangleBuffer),i.drawElements(i.TRIANGLES,t.vertexCount,i.UNSIGNED_SHORT,0),i.disableVertexAttribArray(r.program.vertexLocation),i.disableVertexAttribArray(r.locations.uv),r===this._programTexture&&i.disableVertexAttribArray(r.locations.normal)}this.drawWireframe&&(r&&this.wireframeXray&&i.clear(i.DEPTH_BUFFER_BIT),this._programWireframe.program.use(),i.uniformMatrix4fv(this._programWireframe.locations.mvp,!1,a),i.uniform4fv(this._programWireframe.locations.color,[this.wireframeColor[0],this.wireframeColor[1],this.wireframeColor[2],1]),i.bindBuffer(i.ARRAY_BUFFER,this._wireframe.vertexBuffer),i.vertexAttribPointer(this._programWireframe.program.vertexLocation,3,i.FLOAT,!1,0,0),i.enableVertexAttribArray(this._programWireframe.program.vertexLocation),i.drawArrays(i.LINES,0,this._wireframe.vertexCount));let u=this._frameBufferTex,h=this.outlineSize;if(h>0&&(i.bindFramebuffer(i.FRAMEBUFFER,this._frameBuffer2),i.clear(i.COLOR_BUFFER_BIT)),h>0){let t=this._programOutline;t.program.use(),i.uniform2f(t.locations.pixel,1/this._resolution[0],1/this._resolution[1]),i.uniform4f(t.locations.outlineColor,this.outlineColor[0],this.outlineColor[1],this.outlineColor[2],this.outlineColor[3]??1),i.bindBuffer(i.ARRAY_BUFFER,this._screenQuads),i.vertexAttribPointer(t.program.vertexLocation,2,i.FLOAT,!1,0,0),i.enableVertexAttribArray(t.program.vertexLocation);for(let e=0;e.5,r=null;for(let n of this._lightMapColors){let i=T(n[0]/255,n[1]/255,n[2]/255);t&&(i=1-i),(!e||i>r)&&(e=n,r=i)}}else e=this._lightMapColors[(this.model.backgroundIndex+8)%16];i.uniform4f(t.locations.color,e[0]/255,e[1]/255,e[2]/255,1),i.activeTexture(i.TEXTURE0),i.bindTexture(i.TEXTURE_2D,this._fontTex),i.uniform1i(t.locations.mainTex,0),i.drawArrays(i.TRIANGLES,0,this._watermarkTriangleCount)}}if(i.bindFramebuffer(i.FRAMEBUFFER,null),i.viewport(0,0,o.width,o.height),null==this.backgroundColor){const t=this._lightMapColors[this.model.backgroundIndex];i.clearColor(t[0]/255,t[1]/255,t[2]/255,1)}else i.clearColor(this.backgroundColor[0],this.backgroundColor[1],this.backgroundColor[2],this.backgroundColor[3]??1);i.clear(i.COLOR_BUFFER_BIT);let c=this._programFramebuffer;c.program.use(),i.activeTexture(i.TEXTURE0),i.bindTexture(i.TEXTURE_2D,u),i.uniform1i(c.locations.mainTex,0),i.bindBuffer(i.ARRAY_BUFFER,this._screenQuads),i.vertexAttribPointer(c.program.vertexLocation,2,i.FLOAT,!1,0,0),i.enableVertexAttribArray(c.program.vertexLocation),i.drawArrays(i.TRIANGLES,0,6)}startDrawLoop(t,e){let r=performance.now(),n=r;const i=()=>{if(null!=t){const e=performance.now();t((e-r)/1e3),r=e}if(this.draw(),null!=e){const t=performance.now();e((t-n)/1e3),n=t}this._rafID=requestAnimationFrame(i)};this._rafID=requestAnimationFrame(i)}stopDrawLoop(){null!=this._rafID&&cancelAnimationFrame(this._rafID)}getCameraRight(){return this._transformDirection(1,0,0)}getCameraUp(){return this._transformDirection(0,-1,0)}getCameraForward(){return this._transformDirection(0,0,-1)}setLightDirectionFromCamera(){const t=this.getCameraUp(),e=this.getCameraForward();this.lightDirection={x:e.x-.4*t.x,y:e.y-.4*t.y,z:e.z-.4*t.z}}getTextureColorCount(t=!1){const e=Array(16).fill(!1);let r=0;for(const t of this.model.texture)e[t]||(e[t]=!0,r++);return r}getTriangleCount(){let t=0;for(const e of this._passes)t+=Math.floor(e.vertexCount/3);return t}getDrawCallCount(){let t=1;for(const e of this._passes)e.isEmpty()||t++;return this.drawWireframe&&t++,t+=this.outlineSize,t}_transformDirection(t,e,n){const i=r();!function(t,e,r,n){t[0]=e,t[1]=r,t[2]=n}(i,t,e,n);const o=((a=r())[0]=0,a[1]=0,a[2]=0,a);var a;return function(t,e,r,n){var i=[],o=[];i[0]=e[0]-r[0],i[1]=e[1]-r[1],i[2]=e[2]-r[2],o[0]=i[0],o[1]=i[1]*Math.cos(n)-i[2]*Math.sin(n),o[2]=i[1]*Math.sin(n)+i[2]*Math.cos(n),t[0]=o[0]+r[0],t[1]=o[1]+r[1],t[2]=o[2]+r[2]}(i,i,o,Math.PI+this.cameraRotation.x),function(t,e,r,n){var i=[],o=[];i[0]=e[0]-r[0],i[1]=e[1]-r[1],i[2]=e[2]-r[2],o[0]=i[2]*Math.sin(n)+i[0]*Math.cos(n),o[1]=i[1],o[2]=i[2]*Math.cos(n)-i[0]*Math.sin(n),t[0]=o[0]+r[0],t[1]=o[1]+r[1],t[2]=o[2]+r[2]}(i,i,o,this.cameraRotation.y),function(t,e,r,n){var i=[],o=[];i[0]=e[0]-r[0],i[1]=e[1]-r[1],i[2]=e[2]-r[2],o[0]=i[0]*Math.cos(n)-i[1]*Math.sin(n),o[1]=i[0]*Math.sin(n)+i[1]*Math.cos(n),o[2]=i[2],t[0]=o[0]+r[0],t[1]=o[1]+r[1],t[2]=o[2]+r[2]}(i,i,o,Math.PI+this.cameraRotation.z),{x:i[0],y:i[1],z:i[2]}}setTurntableCamera(t,e,r,n={x:0,y:1.5,z:0}){const i=Math.PI-e;r=-r,this.cameraPosition={x:t*Math.cos(r)*Math.sin(i)-n.x,y:t*Math.sin(r)-n.y,z:t*Math.cos(r)*Math.cos(i)-n.z},this.cameraRotation={y:e,x:-r,z:0}}getPixels(){const t=this.gl,[e,r]=this._resolution,n=new Uint8Array(4*(e*r));return t.bindFramebuffer(t.FRAMEBUFFER,this._framebufferCurrent),t.readPixels(0,0,e,r,t.RGBA,t.UNSIGNED_BYTE,n),t.bindFramebuffer(t.FRAMEBUFFER,null),n}getPixelIndices(t=1){const e=this.gl,[r,n]=this._resolution,i=r*n,o=r*t,a=o*t,s=t*t;t=Math.max(1,Math.floor(t));const l=new Uint8Array(4*i);e.bindFramebuffer(e.FRAMEBUFFER,this._framebufferCurrent),e.readPixels(0,0,r,n,e.RGBA,e.UNSIGNED_BYTE,l),e.bindFramebuffer(e.FRAMEBUFFER,null);const u=this.getPalette(),h=new Map(u.map(((t,e)=>[v(t),e])));h.set(0,this.backgroundColor?u.length-1:this.model.backgroundIndex);const c=new Uint32Array(l.buffer),f=new Uint8Array(i*s);let d=i-r,m=0;for(let e=0;et+e.faces.length),0);for(;null!=st.lastChild;)st.lastChild.remove();st.append(Ht("li",{class:"filename"},ht.model.name),Ht("br"));const i={Objects:r.objects.length,Faces:n};console.log(`${ht.getTriangleCount()} triangles, ${ht.getDrawCallCount()} draw calls`);for(const[t,e]of Object.entries(i))st.append(Ht("li",{},` ${t}: ${e}`));if(e){const t=R(r);history.pushState(null,"","#"+t),console.log(`lzw base64: ${t.length} bytes`)}}function _t(t=!0){vt("./example.txt",t)}let Tt=null;function bt(t){null!=Tt&&URL.revokeObjectURL(Tt),Tt=URL.createObjectURL(t);const e=new Image;e.onload=()=>{const t=document.createElement("canvas");t.id="image-drop-preview",t.width=e.naturalWidth,t.height=e.naturalHeight;const r=t.getContext("2d");r.drawImage(e,0,0);const i=r.getImageData(0,0,t.width,t.height),o=function(t){const e=new Set(n.map((([t,e,r])=>function(t,e,r){return 4278190080|r<<16|e<<8|t}(t,e,r)))),r=new Int32Array(t.data.buffer);for(let t=0,n=r.length;t{ht.setLightMap(i),At.putImageData(ht.getModelTexture(),0,0)}]),a.push(["Texture",0,()=>{S.hidden=!0,N.hidden=!1,N.src=Tt,o?(q.hidden=!0,ht.removeHDTexture(),ht.setIndexTexture(i)):(q.hidden=!1,ht.setHDTexture(i))}]),1===a.length)return void a[0][2]();at.hidden=!1;const s=at.firstElementChild;s.innerHTML="",s.append(t,Ht("p",{},"How should this image be used?"));for(const[t,e,r]of a){const n=Ht("div",{class:"lil-btn"},t);n.style.marginLeft=30*e+"px",n.onclick=()=>{r(),at.hidden=!0},s.append(n)}},e.src=Tt}const At=S.getContext("2d");document.querySelectorAll(".popup").forEach((t=>{t.addEventListener("click",(e=>{e.target===t&&(t.hidden=!t.hidden)}))})),J.onclick=()=>{ot.hidden=!ot.hidden},ot.querySelectorAll("kbd").forEach((t=>{t.onclick=()=>Ct(t.textContent.toLowerCase())}));let Et=.02,yt=!1,wt=0,Rt=0;function Bt(){yt?Ft():(yt=!0,wt=0,Rt=ct,nt.textContent="Recording GIF...",nt.classList.add("recording"),O.classList.add("recording"),$.disabled=!0)}function Ft(){yt=!1,nt.textContent="Record GIF",nt.classList.remove("recording"),O.classList.remove("recording"),$.disabled=!1,it.hidden=!1;let t=ht.getResolution(),e=ht.getRenderedBackgroundColor(),r=null,n=-1;ht.hasHDTexture()||(r=ht.getPalette(),r.length>256?r=null:null!=ht.backgroundColor&&ht.backgroundColor[3]<1&&(n=r.length-1)),lt.postMessage({type:"generate",width:t.width,height:t.height,scale:t.scale,delay:Math.round(1e3*Et),background:e,palette:r,transparentIndex:n})}function Ct(t){"r"===t?Nt(!ht.drawWireframe):"t"===t?Ot(!pt):"m"===t?zt("texture"===W.value?"color":"texture"):"l"===t?Gt(!V.checked):"/"===t||"?"===t?_t():"g"===t?Bt():"pause"===t&&(ht.stopDrawLoop(),O.style.opacity="0.5")}const Ut=Object.create(null);window.onkeydown=t=>{if(t.target===document.body&&!t.ctrlKey&&!t.metaKey){t.preventDefault();const e=t.key.toLowerCase();Ut[e]=!0,Ct(e)}},window.onkeyup=t=>{t.ctrlKey||t.metaKey||(t.preventDefault(),Ut[t.key.toLowerCase()]=!1)},O.ondblclick=()=>{ht.loaded&&(document.activeElement?.blur(),O.requestPointerLock())},O.oncontextmenu=t=>{t.preventDefault()},document.onpointerlockchange=t=>{xt=document.pointerLockElement===O?1:0};let Mt=Array(5).fill(!1),Lt=Array(5).fill(!1),Dt=[0,0];window.onmousedown=t=>{const e=t.button,r=t.target==O;Mt[e]=!0,Lt[e]=r,r&&(t.preventDefault(),O.classList.add("grabbing"),0===xt&&0===e&&Ot(!1))},window.onmouseup=t=>{const e=t.button;Mt[e]=!1,Lt[e]=!1,O.classList.remove("grabbing")},window.onmousemove=t=>{const e=[t.clientX,t.clientY],r=[e[0]-Dt[0],e[1]-Dt[1]];if(1===xt&&document.pointerLockElement===O){const e=.003;ct+=t.movementX*e,ft+=t.movementY*e}else if(0==xt)if(Lt[0]){const t=.005;ct+=r[0]*t,ft+=r[1]*t}else if(Lt[1]||Lt[2]){const t=.005,e=ht.getCameraUp(),n=ht.getCameraRight(),i=r[0]*t,o=-r[1]*t;gt.x+=n.x*i+e.x*o,gt.y+=n.y*i+e.y*o,gt.z+=n.z*i+e.z*o}Dt=e},O.onwheel=t=>{t.preventDefault();const e=jt(-6,6,t.deltaY);1===xt||0===xt&&t.altKey?St(ht.cameraFOV+e):0===xt&&(dt=jt(0,200,dt+.5*e))};let It=[0,0,-1],kt=!1;document.addEventListener("touchstart",(t=>{kt=t.target==O;const e=t.changedTouches[0];It=[e.clientX,e.clientY,e.identifier],kt&&(t.preventDefault(),Ot(!1))}),{passive:!1}),document.addEventListener("touchmove",(t=>{const e=Array.from(t.changedTouches).find((t=>t.identifier===It[2]));if(null!=e){const t=[e.clientX,e.clientY,e.identifier];if(kt){const e=[t[0]-It[0],t[1]-It[1]],r=.01;ct+=e[0]*r,ft+=e[1]*r}It=t}})),document.addEventListener("touchend",(t=>{null!=Array.from(t.changedTouches).find((t=>t.identifier===It[2]))&&(kt=!1)})),window.innerWidth<700&&(P.value="128,128,2"),Vt(P,(t=>{const[e,r,n]=t.split(",").map((t=>Number(t)));ht.setResolution(e,r,n)}));const St=Vt(K,(()=>{ht.cameraFOV=K.valueAsNumber,K.nextElementSibling.textContent=K.value})),Nt=Vt(G,(()=>{ht.drawWireframe=G.checked})),Ot=Vt(z,(()=>{pt=z.checked}));function Pt(){H.checked?ht.backgroundColor=[0,0,0,0]:ht.backgroundColor=j.checked?Kt(X.value):null}Vt(Y,(t=>{ht.wireframeColor=Kt(t)})),Vt(j,Pt),Vt(X,Pt),Vt(H,Pt);const zt=Vt(W,(t=>{ht.renderMode=t})),Gt=Vt(V,(()=>{ht.shading=V.checked}));Vt(Q,(()=>{ht.hdOptions.shadingSteps=Q.valueAsNumber,Q.nextElementSibling.textContent=Q.value})),Vt(Z,(t=>{ht.hdOptions.shadingColor=Kt(t)})),Vt($,(t=>{Et=Number(t)/100})),Vt(tt,(()=>{ht.outlineSize=tt.valueAsNumber})),Vt(et,(t=>{ht.outlineColor=Kt(t)})),Vt(rt,(t=>{ht.setWatermark(t)})),nt.onclick=()=>{Bt()},ht.startDrawLoop((t=>{const e=1.2*t;let r=0,n=0,i=0,o=0,a=0;if(Ut.w&&(n+=1),Ut.s&&(n-=1),Ut.a&&(r-=1),Ut.d&&(r+=1),(Ut.q||Ut.shift||Ut.control)&&(i-=1),(Ut.e||Ut[" "])&&(i+=1),Ut.arrowleft&&(o-=1),Ut.arrowright&&(o+=1),Ut.arrowup&&(a-=1),Ut.arrowdown&&(a+=1),0===xt)ft+=(n+a)*e,gt.y+=3*i*t,pt?(mt-=2*(r+o)*t,mt=jt(-2,2,mt),ct+=mt*t):ct+=(r+o)*e,ht.setTurntableCamera(dt,ct,ft,gt);else if(1===xt&&(ct+=o*e,ft+=a*e,ft=jt(-Math.PI/2,Math.PI/2,ft),ht.cameraRotation.x=ft,ht.cameraRotation.y=ct,0!==r||0!==i||0!==n)){const e=6*t,o=ht.getCameraRight(),a=ht.getCameraUp(),s=ht.getCameraForward(),l=ht.cameraPosition;l.x+=(o.x*r+s.x*n+a.x*i)*e,l.y+=(o.y*r+s.y*n+a.y*i)*e,l.z+=(o.z*r+s.z*n+a.z*i)*e}ht.setLightDirectionFromCamera()}),(t=>{if(yt){const e=wt;wt+=t,wt>=30||z&&Math.abs(Rt-ct)>=2*Math.PI?Ft():0!==e&&Math.floor(e/Et)===Math.floor(wt/Et)||function(){let t=ht.getPixels();lt.postMessage({type:"frame",data:t},[t.buffer])}()}})),window.addEventListener("dragover",(t=>{t.preventDefault()}));let Yt=0;function Wt(){document.body.classList.remove("drag")}function Xt(t){const e=t.name.lastIndexOf("."),r=e<0?"":t.name.slice(e+1).toLowerCase();"png"===r||"jpg"===r||"jpeg"===r||"bmp"===r||"gif"===r?bt(t):vt(t)}function jt(t,e,r){return re?e:r}function Ht(t,e,...r){if(t=t instanceof HTMLElement?t:document.createElement(t),null!=e)for(const r in e)t.setAttribute(r,e[r]);return t.append(...r),t}function Vt(t,e){function r(){e(t.value,!1)}return t[t instanceof HTMLSelectElement?"onchange":"oninput"]=r,e(t.value,!0),t instanceof HTMLInputElement?e=>{t.disabled||("boolean"==typeof e?t.checked=e:t.value=e,r())}:e=>{t.disabled||(t.value=e,r())}}function Kt(t){return[t.slice(1,3),t.slice(3,5),t.slice(5,7)].map((t=>parseInt(t,16)/255))}function qt(){if(location.hash.length>1)return vt(B(location.hash.slice(1)),!1),!0}window.addEventListener("dragenter",(t=>{0===Yt&&t.dataTransfer.types.includes("Files")&&document.body.classList.add("drag"),Yt++})),window.addEventListener("dragleave",(t=>{Yt--,0===Yt&&Wt()})),window.addEventListener("drop",(t=>{t.preventDefault(),Yt=0,Wt();const e=t.dataTransfer.files[0];null!=e&&Xt(e)})),document.body.addEventListener("paste",(t=>{const e=t.clipboardData.getData("text/plain");if(null!=e&&e.length>0)vt(e);else{const e=t.clipboardData.files[0];null!=e&&Xt(e)}})),qt()||_t(!1),onhashchange=t=>{qt()},window.loadModel=vt}(); 10 | -------------------------------------------------------------------------------- /docs/models.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | picoCAD Viewer Model 8 | 9 | 10 | 16 | 17 | -------------------------------------------------------------------------------- /docs/worker.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict"; 2 | /* 3 | @licence https://github.com/mattdesl/gifenc 4 | The MIT License (MIT) 5 | Copyright (c) 2017 Matt DesLauriers 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 21 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 22 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 23 | OR OTHER DEALINGS IN THE SOFTWARE. 24 | */var t=59;function e(t=256){let e=0,r=new Uint8Array(t);return{get buffer(){return r.buffer},reset(){e=0},bytesView:()=>r.subarray(0,e),bytes:()=>r.slice(0,e),writeByte(t){n(e+1),r[e]=t,e++},writeBytes(t,a=0,l=t.length){n(e+l);for(let n=0;n=t)return;t=Math.max(t,n*(n<1048576?2:1.125)>>>0),0!=n&&(t=Math.max(t,256));let a=r;r=new Uint8Array(t),e>0&&r.set(a.subarray(0,e),0)}}var r=[0,1,3,7,15,31,63,127,255,511,1023,2047,4095,8191,16383,32767,65535];var n=function(t,n,a,l,i=e(512),f=new Uint8Array(256),c=new Int32Array(5003),o=new Int32Array(5003)){let u=c.length,w=Math.max(2,l);f.fill(0),o.fill(0),c.fill(-1);let y=0,s=0,h=w+1,g=h,b=!1,m=g,B=(1<=0;)if(n-=l,n<0&&(n+=u),c[n]===r){M=o[n];break t}x(M),M=e,d<4096?(o[n]=d++,c[n]=r):(c.fill(-1),d=p+2,b=!0,x(p))}return x(M),x(A),i.writeByte(0),i.bytesView();function x(t){for(y&=r[s],s>0?y|=t<=8;)f[v++]=255&y,v>=254&&(i.writeByte(v),i.writeBytesView(f,0,v),v=0),y>>=8,s-=8;if((d>B||b)&&(b?(m=g,B=(1<0;)f[v++]=255&y,v>=254&&(i.writeByte(v),i.writeBytesView(f,0,v),v=0),y>>=8,s-=8;v>0&&(i.writeByte(v),i.writeBytesView(f,0,v),v=0)}}};function a(t,e,r){return t<<8&63488|e<<2&992|r>>3}function l(t,e,r,n){return t>>4|240&e|(240&r)<<4|(240&n)<<8}function i(t,e,r){return t>>4<<8|240&e|r>>4}function f(t,e,r){return tr?r:t}function c(t){return t*t}function o(t,e,r){var n=0,a=1e100;let l=t[e],i=l.cnt,f=l.ac,o=l.rc,u=l.gc,w=l.bc;for(var y=l.fw;0!=y;y=t[y].fw){let e=t[y],l=e.cnt,h=i*l/(i+l);if(!(h>=a)){var s=0;r&&(s+=h*c(e.ac-f))>=a||!((s+=h*c(e.rc-o))>=a)&&(!((s+=h*c(e.gc-u))>=a)&&(!((s+=h*c(e.bc-w))>=a)&&(a=s,n=y)))}}l.err=a,l.nn=n}function u(t,e,r={}){let{format:n="rgb565",clearAlpha:u=!0,clearAlphaColor:y=0,clearAlphaThreshold:s=0,oneBitAlpha:h=!1}=r;if(!t||!t.buffer)throw new Error("quantize() expected RGBA Uint8Array data");if(!(t instanceof Uint8Array||t instanceof Uint8ClampedArray))throw new Error("quantize() expected RGBA Uint8Array data");let g=new Uint32Array(t.buffer),b=!1!==r.useSqrt,m="rgba4444"===n,B=function(t,e){let r=new Array("rgb444"===e?4096:65536),n=t.length;if("rgba4444"===e)for(let e=0;e>24&255,i=n>>16&255,f=n>>8&255,c=255&n,o=l(c,f,i,a),u=o in r?r[o]:r[o]={ac:0,rc:0,gc:0,bc:0,cnt:0,nn:0,fw:0,bk:0,tm:0,mtm:0,err:0};u.rc+=c,u.gc+=f,u.bc+=i,u.ac+=a,u.cnt++}else if("rgb444"===e)for(let e=0;e>16&255,l=n>>8&255,f=255&n,c=i(f,l,a),o=c in r?r[c]:r[c]={ac:0,rc:0,gc:0,bc:0,cnt:0,nn:0,fw:0,bk:0,tm:0,mtm:0,err:0};o.rc+=f,o.gc+=l,o.bc+=a,o.cnt++}else for(let e=0;e>16&255,i=n>>8&255,f=255&n,c=a(f,i,l),o=c in r?r[c]:r[c]={ac:0,rc:0,gc:0,bc:0,cnt:0,nn:0,fw:0,bk:0,tm:0,mtm:0,err:0};o.rc+=f,o.gc+=i,o.bc+=l,o.cnt++}return r}(g,n),p=B.length,A=p-1,d=new Uint32Array(p+1);for(var v=0,M=0;M1&&!(B[k=d[E=x>>1]].err<=V);x=E)d[x]=k;d[x]=M}var I=v-e;for(M=0;M=q.mtm&&B[q.nn].mtm<=q.tm)break;q.mtm==A?C=d[1]=d[d[0]--]:(o(B,C,!1),q.tm=M);V=B[C].err;for(x=1;(E=x+x)<=d[0]&&(EB[d[E+1]].err&&E++,!(V<=B[k=d[E]].err));x=E)d[x]=k;d[x]=C}var G=B[q.nn],z=q.cnt,F=G.cnt;U=1/(z+F);m&&(q.ac=U*(z*q.ac+F*G.ac)),q.rc=U*(z*q.rc+F*G.rc),q.gc=U*(z*q.gc+F*G.gc),q.bc=U*(z*q.bc+F*G.bc),q.cnt+=G.cnt,q.mtm=++M,B[G.bk].fw=G.fw,B[G.fw].bk=G.bk,G.mtm=A}let R=[];var L=0;for(M=0;;++L){let t=f(Math.round(B[M].rc),0,255),e=f(Math.round(B[M].gc),0,255),r=f(Math.round(B[M].bc),0,255),n=255;if(m){if(n=f(Math.round(B[M].ac),0,255),h){n=n<=("number"==typeof h?h:127)?0:255}u&&n<=s&&(t=e=r=y,n=0)}let a=m?[t,e,r,n]:[t,e,r];if(w(R,a)||R.push(a),0==(M=B[M].fw))break}return R}function w(t,e){for(let r=0;r=4&&e.length>=4)||n[3]===e[3];if(a&&l)return!0}return!1}function y(t,e,r="rgb565"){if(!t||!t.buffer)throw new Error("quantize() expected RGBA Uint8Array data");if(!(t instanceof Uint8Array||t instanceof Uint8ClampedArray))throw new Error("quantize() expected RGBA Uint8Array data");if(e.length>256)throw new Error("applyPalette() only works with 256 colors or less");let n=new Uint32Array(t.buffer),f=n.length,c="rgb444"===r?4096:65536,o=new Uint8Array(f),u=new Array(c);if("rgba4444"===r)for(let t=0;t>24&255,i=r>>16&255,f=r>>8&255,c=255&r,w=l(c,f,i,a),y=w in u?u[w]:u[w]=s(c,f,i,a,e);o[t]=y}else{let t="rgb444"===r?i:a;for(let r=0;r>16&255,i=a>>8&255,f=255&a,c=t(f,i,l),w=c in u?u[c]:u[c]=h(f,i,l,e);o[r]=w}}return o}function s(t,e,r,n,a){let l=0,i=1e100;for(let f=0;fi||(o+=g(c[0]-t),o>i||(o+=g(c[1]-e),o>i||(o+=g(c[2]-r),!(o>i)&&(i=o,l=f))))}return l}function h(t,e,r,n){let a=0,l=1e100;for(let i=0;il||(c+=g(f[1]-e),c>l||(c+=g(f[2]-r),!(c>l)&&(l=c,a=i)))}return a}function g(t){return t*t}function b(t,e){let r=1<>8&255)}function B(t,e){for(var r=0;ri.bytes(),bytesView:()=>i.bytesView(),get buffer(){return i.buffer},get stream(){return i},writeHeader:w,writeFrame(t,e,r,a={}){let{transparent:y=!1,transparentIndex:s=0,delay:h=0,palette:g=null,repeat:A=0,colorDepth:d=8,dispose:v=-1}=a,M=!1;if(l?u||(M=!0,w(),u=!0):M=Boolean(a.first),e=Math.max(0,Math.floor(e)),r=Math.max(0,Math.floor(r)),M){if(!g)throw new Error("First frame must include a { palette } option");(function(t,e,r,n,a=8){let l=1,i=0,f=p(n.length)-1,c=l<<7|a-1<<4|i<<3|f,o=0,u=0;m(t,e),m(t,r),t.writeBytes([c,o,u])})(i,e,r,g,d),b(i,g),A>=0&&function(t,e){t.writeByte(33),t.writeByte(255),t.writeByte(11),B(t,"NETSCAPE2.0"),t.writeByte(3),t.writeByte(1),m(t,e),t.writeByte(0)}(i,A)}let U=Math.round(h/10);!function(t,e,r,n,a){var l,i;t.writeByte(33),t.writeByte(249),t.writeByte(4),a<0&&(a=0,n=!1),n?(l=1,i=2):(l=0,i=0),e>=0&&(i=7&e),i<<=2;let f=0;t.writeByte(0|i|f|l),m(t,r),t.writeByte(a||0),t.writeByte(0)}(i,v,U,y,s);let k=Boolean(g)&&!M;(function(t,e,r,n){if(t.writeByte(44),m(t,0),m(t,0),m(t,e),m(t,r),n){let e=0,r=0,a=p(n.length)-1;t.writeByte(128|e|r|0|a)}else t.writeByte(0)})(i,e,r,k?g:null),k&&b(i,g),function(t,e,r,a,l=8,i,f,c){n(r,a,e,l,t,i,f,c)}(i,t,e,r,d,f,c,o)}};function w(){B(i,"GIF89a")}},v=[];addEventListener("message",(t=>{let e=t.data,r=e.type;var n;"generate"===r?function(t,e,r,n,a,l,i){let f=t*e,c=null,o=new Uint8Array(f*r*r),w=null==l?null:new Map(l.map(((t,e)=>[A(t),e])));for(let s=0;s=4&&e[3]<255&&(m=!0,i=t)}g=y(h,b,"rgba4444")}else{0===s&&(b=l);let t=new Uint32Array(h.buffer);null==c&&(c=new Uint8Array(f));for(let e=0;e=0}let B=f-t,p=0,A=t*r,M=A*r;for(let n=0;n=6.0.0" 31 | } 32 | }, 33 | "node_modules/@jridgewell/resolve-uri": { 34 | "version": "3.1.0", 35 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", 36 | "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", 37 | "dev": true, 38 | "engines": { 39 | "node": ">=6.0.0" 40 | } 41 | }, 42 | "node_modules/@jridgewell/set-array": { 43 | "version": "1.1.2", 44 | "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", 45 | "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", 46 | "dev": true, 47 | "engines": { 48 | "node": ">=6.0.0" 49 | } 50 | }, 51 | "node_modules/@jridgewell/source-map": { 52 | "version": "0.3.3", 53 | "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz", 54 | "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", 55 | "dev": true, 56 | "dependencies": { 57 | "@jridgewell/gen-mapping": "^0.3.0", 58 | "@jridgewell/trace-mapping": "^0.3.9" 59 | } 60 | }, 61 | "node_modules/@jridgewell/sourcemap-codec": { 62 | "version": "1.4.15", 63 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", 64 | "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", 65 | "dev": true 66 | }, 67 | "node_modules/@jridgewell/trace-mapping": { 68 | "version": "0.3.18", 69 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", 70 | "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", 71 | "dev": true, 72 | "dependencies": { 73 | "@jridgewell/resolve-uri": "3.1.0", 74 | "@jridgewell/sourcemap-codec": "1.4.14" 75 | } 76 | }, 77 | "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { 78 | "version": "1.4.14", 79 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", 80 | "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", 81 | "dev": true 82 | }, 83 | "node_modules/@rollup/plugin-terser": { 84 | "version": "0.4.1", 85 | "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.1.tgz", 86 | "integrity": "sha512-aKS32sw5a7hy+fEXVy+5T95aDIwjpGHCTv833HXVtyKMDoVS7pBr5K3L9hEQoNqbJFjfANPrNpIXlTQ7is00eA==", 87 | "dev": true, 88 | "dependencies": { 89 | "serialize-javascript": "^6.0.0", 90 | "smob": "^0.0.6", 91 | "terser": "^5.15.1" 92 | }, 93 | "engines": { 94 | "node": ">=14.0.0" 95 | }, 96 | "peerDependencies": { 97 | "rollup": "^2.x || ^3.x" 98 | }, 99 | "peerDependenciesMeta": { 100 | "rollup": { 101 | "optional": true 102 | } 103 | } 104 | }, 105 | "node_modules/acorn": { 106 | "version": "8.8.2", 107 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", 108 | "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", 109 | "dev": true, 110 | "bin": { 111 | "acorn": "bin/acorn" 112 | }, 113 | "engines": { 114 | "node": ">=0.4.0" 115 | } 116 | }, 117 | "node_modules/buffer-from": { 118 | "version": "1.1.2", 119 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", 120 | "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", 121 | "dev": true 122 | }, 123 | "node_modules/commander": { 124 | "version": "2.20.3", 125 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", 126 | "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", 127 | "dev": true 128 | }, 129 | "node_modules/fsevents": { 130 | "version": "2.3.2", 131 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 132 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 133 | "dev": true, 134 | "hasInstallScript": true, 135 | "optional": true, 136 | "os": [ 137 | "darwin" 138 | ], 139 | "engines": { 140 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 141 | } 142 | }, 143 | "node_modules/gl-matrix": { 144 | "version": "3.4.3", 145 | "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz", 146 | "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==" 147 | }, 148 | "node_modules/randombytes": { 149 | "version": "2.1.0", 150 | "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", 151 | "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", 152 | "dev": true, 153 | "dependencies": { 154 | "safe-buffer": "^5.1.0" 155 | } 156 | }, 157 | "node_modules/rollup": { 158 | "version": "3.21.0", 159 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.21.0.tgz", 160 | "integrity": "sha512-ANPhVcyeHvYdQMUyCbczy33nbLzI7RzrBje4uvNiTDJGIMtlKoOStmympwr9OtS1LZxiDmE2wvxHyVhoLtf1KQ==", 161 | "dev": true, 162 | "bin": { 163 | "rollup": "dist/bin/rollup" 164 | }, 165 | "engines": { 166 | "node": ">=14.18.0", 167 | "npm": ">=8.0.0" 168 | }, 169 | "optionalDependencies": { 170 | "fsevents": "~2.3.2" 171 | } 172 | }, 173 | "node_modules/safe-buffer": { 174 | "version": "5.2.1", 175 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 176 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 177 | "dev": true, 178 | "funding": [ 179 | { 180 | "type": "github", 181 | "url": "https://github.com/sponsors/feross" 182 | }, 183 | { 184 | "type": "patreon", 185 | "url": "https://www.patreon.com/feross" 186 | }, 187 | { 188 | "type": "consulting", 189 | "url": "https://feross.org/support" 190 | } 191 | ] 192 | }, 193 | "node_modules/serialize-javascript": { 194 | "version": "6.0.1", 195 | "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", 196 | "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", 197 | "dev": true, 198 | "dependencies": { 199 | "randombytes": "^2.1.0" 200 | } 201 | }, 202 | "node_modules/smob": { 203 | "version": "0.0.6", 204 | "resolved": "https://registry.npmjs.org/smob/-/smob-0.0.6.tgz", 205 | "integrity": "sha512-V21+XeNni+tTyiST1MHsa84AQhT1aFZipzPpOFAVB8DkHzwJyjjAmt9bgwnuZiZWnIbMo2duE29wybxv/7HWUw==", 206 | "dev": true 207 | }, 208 | "node_modules/source-map": { 209 | "version": "0.6.1", 210 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 211 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 212 | "dev": true, 213 | "engines": { 214 | "node": ">=0.10.0" 215 | } 216 | }, 217 | "node_modules/source-map-support": { 218 | "version": "0.5.21", 219 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", 220 | "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", 221 | "dev": true, 222 | "dependencies": { 223 | "buffer-from": "^1.0.0", 224 | "source-map": "^0.6.0" 225 | } 226 | }, 227 | "node_modules/terser": { 228 | "version": "5.17.1", 229 | "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.1.tgz", 230 | "integrity": "sha512-hVl35zClmpisy6oaoKALOpS0rDYLxRFLHhRuDlEGTKey9qHjS1w9GMORjuwIMt70Wan4lwsLYyWDVnWgF+KUEw==", 231 | "dev": true, 232 | "dependencies": { 233 | "@jridgewell/source-map": "^0.3.2", 234 | "acorn": "^8.5.0", 235 | "commander": "^2.20.0", 236 | "source-map-support": "~0.5.20" 237 | }, 238 | "bin": { 239 | "terser": "bin/terser" 240 | }, 241 | "engines": { 242 | "node": ">=10" 243 | } 244 | } 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "picocad-web-viewer", 3 | "version": "1.0.0", 4 | "description": "WebGL viewer for picoCAD scenes.", 5 | "private": true, 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "build": "rollup -c", 10 | "build-demo": "rollup --config rollup.config-demo.mjs", 11 | "watch-demo": "rollup --config rollup.config-demo.mjs --watch --environment BUILD:development" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/lucatronica/picocad-web-viewer.git" 16 | }, 17 | "keywords": [ 18 | "picoCAD", 19 | "pico8", 20 | "pico-8" 21 | ], 22 | "author": "Luca Harris", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/lucatronica/picocad-web-viewer/issues" 26 | }, 27 | "homepage": "https://github.com/lucatronica/picocad-web-viewer#readme", 28 | "dependencies": { 29 | "gl-matrix": "^3.3.0" 30 | }, 31 | "devDependencies": { 32 | "rollup": "^3.21.0", 33 | "@rollup/plugin-terser": "^0.4.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /rollup.config-demo.mjs: -------------------------------------------------------------------------------- 1 | import terser from "@rollup/plugin-terser"; 2 | 3 | const development = process.env.BUILD === "development"; 4 | 5 | export default [ 6 | // Main script. 7 | { 8 | input: "demo-src/index.js", 9 | output: [ 10 | { 11 | file: "docs/index.js", 12 | format: "iife", 13 | exports: "none", 14 | plugins: development ? [] : [ terser() ], 15 | }, 16 | ], 17 | }, 18 | // Worker script. 19 | { 20 | input: "demo-worker-src/index.js", 21 | output: [ 22 | { 23 | file: "docs/worker.js", 24 | format: "iife", 25 | exports: "none", 26 | plugins: development ? [] : [ terser() ], 27 | }, 28 | ], 29 | }, 30 | ]; 31 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import terser from "@rollup/plugin-terser"; 2 | 3 | export default { 4 | input: "src/index.js", 5 | output: [ 6 | { 7 | file: "dist/pico-cad-viewer.esm.js", 8 | format: "esm", 9 | }, 10 | { 11 | file: "dist/pico-cad-viewer.min.js", 12 | format: "iife", 13 | name: "PicoCadViewer", 14 | plugins: [ terser() ], 15 | }, 16 | ], 17 | }; 18 | -------------------------------------------------------------------------------- /src/color.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @param {number} x 4 | * @param {number} n 5 | * @returns {number} 6 | */ 7 | function leftShift(x, n) { 8 | for (let i = 0; i < n; i++) { 9 | x *= 2; 10 | } 11 | return x; 12 | } 13 | 14 | /** 15 | * RGB255 to packed int. 16 | * @param {number[]} rgb 17 | * @returns {number} 18 | */ 19 | export function rgbToInt(rgb) { 20 | // Need to do alpha left shift manually to prevent overflow 21 | return (rgb.length < 4 ? 0xff000000 : leftShift(rgb[3], 24)) + (rgb[2] << 16) + (rgb[1] << 8) + rgb[0]; 22 | } 23 | 24 | /** 25 | * @param {number[]} rgb RGB or RGBA 26 | * @returns {number[]} 27 | */ 28 | export function rgb255To01(rgb) { 29 | return rgb.map(a => a / 255); 30 | } 31 | 32 | /** 33 | * @param {number[]} rgb RGB or RGBA 34 | * @returns {number[]} 35 | */ 36 | export function rgb01to255(rgb) { 37 | return rgb.map(a => Math.floor(a * 255.999)); 38 | } 39 | 40 | /** 41 | * @param {number} r 42 | * @param {number} g 43 | * @param {number} b 44 | */ 45 | export function luma(r, g, b) { 46 | return r * 0.2126 + g * 0.7152 + b * 0.0722; 47 | } 48 | -------------------------------------------------------------------------------- /src/environment.js: -------------------------------------------------------------------------------- 1 | 2 | const userAgent = navigator.userAgent; 3 | 4 | export const isSafari = userAgent.includes("Safari/") && !userAgent.includes("Edg/"); 5 | -------------------------------------------------------------------------------- /src/image.js: -------------------------------------------------------------------------------- 1 | import { PICO_COLORS } from "./pico"; 2 | 3 | /** 4 | * Creates an image from a string of hex indices. 5 | * @param {number} w 6 | * @param {number} h 7 | * @param {string} hexData 8 | * @returns {ImageData} 9 | * @example 10 | * createP8Image(4, 4, "0123456789abcdef") // Image that shows the 4x4 grid of PICO-8 colors. 11 | */ 12 | export function createP8Image(w, h, hexData) { 13 | const img = new ImageData(w, h); 14 | 15 | // init data 16 | const data = img.data; 17 | 18 | const n = w * h; 19 | let index = 0; 20 | for (let i = 0; i < n; i++) { 21 | const color = PICO_COLORS[parseInt(hexData.charAt(i), 16)]; 22 | 23 | data[index ] = color[0]; 24 | data[index + 1] = color[1]; 25 | data[index + 2] = color[2]; 26 | data[index + 3] = 255; 27 | index += 4; 28 | } 29 | 30 | return img; 31 | } 32 | -------------------------------------------------------------------------------- /src/lighting.js: -------------------------------------------------------------------------------- 1 | import { createP8Image } from "./image.js"; 2 | 3 | // Each group of 2x4 pixels represents the gradient for a given color. 4 | // Top -> bottom = light -> dark. 5 | // The two columns are for dithering: there may be two different colors for a given light level. 6 | // (This is based directly off picoCAD's lighting). 7 | 8 | export function createLightMap() { 9 | return createP8Image(32, 4, "00112233445566778899aabbccddeeff0010213542516d768294a9b3cdd5e8fe000011552211dd6622449933dd55889900000011110055dd1122445555112244"); 10 | } 11 | -------------------------------------------------------------------------------- /src/model-data-parser.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @param {string} s 4 | * @returns {LuaPicoCADModel} 5 | */ 6 | export function parsePicoCADData(s) { 7 | return /** @type {any} */(parseLua(s)); 8 | } 9 | 10 | /** 11 | * @param {string} s 12 | * @returns {LuaTable} 13 | */ 14 | function parseLua(s) { 15 | let i = 0; 16 | 17 | return readObject(); 18 | 19 | function readValue() { 20 | const c = s.charAt(i); 21 | if (c === "{") { 22 | return readObject(); 23 | } else if (c === "'") { 24 | return readString(); 25 | } else if (c === "-" || c === "." || (c >= "0" && c <= "9")) { 26 | return readNumber(); 27 | } else { 28 | throw Error("Unkown value (" + i + "): " + "\"" + c + "\" = " + c.charCodeAt(0)); 29 | } 30 | } 31 | 32 | function readObject() { 33 | i++; // { 34 | 35 | const obj = { 36 | array: [], 37 | dict: Object.create(null), 38 | }; 39 | 40 | skipWhitespace(); 41 | 42 | while (true) { 43 | const c = s.charAt(i); 44 | 45 | if (c === "}") { 46 | i++; 47 | break; 48 | } 49 | 50 | let key; 51 | 52 | if (c >= "a" && c <= "z") { 53 | // key-value pair 54 | let start = i; 55 | i++; 56 | 57 | while (true) { 58 | const c = s.charAt(i); 59 | if (c === "=") { 60 | break; 61 | } else { 62 | i++; 63 | } 64 | } 65 | 66 | key = s.slice(start, i); 67 | i++; // = 68 | } 69 | 70 | const value = readValue(); 71 | 72 | if (key == null) { 73 | obj.array.push(value); 74 | } else { 75 | obj.dict[key] = value; 76 | } 77 | 78 | skipWhitespace(); 79 | 80 | const cc = s.charAt(i); 81 | 82 | if (cc === ",") { 83 | i++; 84 | skipWhitespace(); 85 | } 86 | } 87 | 88 | return obj; 89 | } 90 | 91 | function readString() { 92 | // assuming no escapes 93 | const start = i; 94 | const j = s.indexOf("'", i + 1); 95 | if (j < 0) { 96 | throw Error("No end!!!"); 97 | } 98 | i = j + 1; 99 | if (i === start) { 100 | throw Error("!!!!"); 101 | } 102 | return s.slice(start + 1, j); 103 | } 104 | 105 | function readNumber() { 106 | const start = i; 107 | 108 | while (true) { 109 | const c = s.charAt(i); 110 | 111 | if (c === "-" || c === "." || (c >= "0" && c <= "9")) { 112 | i++; 113 | } else { 114 | break; 115 | } 116 | } 117 | 118 | if (i === start) { 119 | throw Error("!!!!"); 120 | } 121 | 122 | return Number(s.slice(start, i)); 123 | } 124 | 125 | function skipWhitespace() { 126 | while (true) { 127 | const c = s.charAt(i); 128 | 129 | if (c === " " || c === "\n" || c === "\r" || c === "\t") { 130 | i++; 131 | } else { 132 | break; 133 | } 134 | } 135 | } 136 | } 137 | 138 | /** @typedef {string | number | LuaTable} LuaValue */ 139 | /** @typedef {{array: LuaValue[], dict: Record}} LuaTable */ 140 | /** @typedef {{array: T[], dict: Record}} LuaArray @template T */ 141 | /** @typedef {{array: LuaValue[], dict: T}} LuaDict @template T */ 142 | /** @typedef {{array: T[], dict: U}} LuaArrayDict @template T @template U */ 143 | /** @typedef {LuaArray, rot: LuaArray, v: LuaArray>, f: LuaArray}>>}>>} LuaPicoCADModel */ 144 | -------------------------------------------------------------------------------- /src/model-gl-loader.js: -------------------------------------------------------------------------------- 1 | import { PicoCADModel } from "./model"; 2 | import { Pass, WirePass } from "./pass"; 3 | import { PICO_COLORS } from "./pico"; 4 | 5 | /** 6 | * @param {WebGLRenderingContext} gl 7 | * @param {PicoCADModel} model 8 | * @param {number} tesselationCount Pass 0 to do no tesselation 9 | */ 10 | export function prepareModelForRendering(gl, model, tesselationCount) { 11 | const { passes, wireframe } = loadModel(gl, model, tesselationCount + 1); 12 | 13 | // Read texture. 14 | const texture = convertTexture(model.texture, model.alphaIndex); 15 | 16 | return { 17 | passes: passes, 18 | wireframe: wireframe, 19 | textureIndices: texture, 20 | }; 21 | } 22 | 23 | /** 24 | * @param {WebGLRenderingContext} gl 25 | * @param {PicoCADModel} model 26 | * @param {number} tn Number of tessellations 27 | */ 28 | function loadModel(gl, model, tn) { 29 | const passes = []; 30 | 31 | for (let i = 0; i < 16; i++) { 32 | const doubleSided = i % 2 < 1; 33 | const shading = i % 4 < 2; 34 | const texture = i % 8 < 4; 35 | // const priority = i < 8; // This flag is achieved using the ordering of the passes. That's why it's the last one :) 36 | 37 | passes.push(new Pass(gl, { 38 | cull: !doubleSided, 39 | shading: shading, 40 | texture: texture, 41 | clearDepth: i === 8, 42 | })); 43 | } 44 | 45 | const wireframePass = new WirePass(gl); 46 | const wireframeVertices = wireframePass.vertices; 47 | 48 | for (const object of model.objects) { 49 | const pos = object.position; 50 | // const rot = object.rotation; // unused? 51 | 52 | const rawVertices = object.vertices.map(xs => { 53 | return [ 54 | -xs[0] - pos[0], 55 | -xs[1] - pos[1], 56 | xs[2] + pos[2], 57 | ]; 58 | }); 59 | 60 | // pioCAD stores each vertex once. 61 | // But we'll have to duplicate vertices across faces! 62 | 63 | for (const face of object.faces) { 64 | const faceIndices = face.indices; 65 | const rawUVs = face.uvs; 66 | 67 | // Configure pass based on face props 68 | const pass = passes[ 69 | (face.doubleSided ? 0 : 1) + 70 | (face.shading ? 0 : 2) + 71 | (face.texture ? 0 : 4) + 72 | (face.renderFirst ? 0 : 8) 73 | ]; 74 | 75 | const vertices = pass.vertices; 76 | const triangles = pass.triangles; 77 | const normals = pass.normals; 78 | const uvs = pass.uvs; 79 | const colorUVs = pass.colorUVs; 80 | 81 | // Color UVs 82 | const colorU = 0.03125 + face.colorIndex / 16; 83 | const colorV = 0; 84 | 85 | // Get current vertex index. 86 | const vertexIndex0 = Math.floor(vertices.length / 3); 87 | 88 | // Get faces vertices and uvs. 89 | // Save face edges to wireframe buffer. 90 | const faceVertices = []; 91 | const faceUVs = []; 92 | 93 | for (let i = 0; i < faceIndices.length; i++) { 94 | const vertex = rawVertices[faceIndices[i]]; 95 | const vertex2 = rawVertices[faceIndices[i === 0 ? faceIndices.length - 1 : i - 1]]; 96 | const rawUV = rawUVs[i]; 97 | 98 | faceVertices.push(vertex); 99 | 100 | wireframeVertices.push( 101 | vertex[0], vertex[1], vertex[2], 102 | vertex2[0], vertex2[1], vertex2[2], 103 | ); 104 | 105 | faceUVs.push([ 106 | rawUV[0] / 16, 107 | rawUV[1] / 16, 108 | ]); 109 | } 110 | 111 | // Calculate face normal (should be same for all triangles) 112 | const faceNormal = calculateFaceNormal(faceVertices); 113 | 114 | // Get triangles 115 | if (faceIndices.length === 4 && face.texture && tn > 1) { 116 | // Tesselate quad. 117 | const c0 = faceVertices[0]; 118 | const c1 = faceVertices[1]; 119 | const c2 = faceVertices[2]; 120 | const c3 = faceVertices[3]; 121 | 122 | const uv0 = faceUVs[0]; 123 | const uv1 = faceUVs[1]; 124 | const uv2 = faceUVs[2]; 125 | const uv3 = faceUVs[3]; 126 | 127 | for (let xi = 0; xi <= tn; xi++) { 128 | const xt = xi / tn; 129 | 130 | const p0 = [ 131 | lerp(c0[0], c1[0], xt), 132 | lerp(c0[1], c1[1], xt), 133 | lerp(c0[2], c1[2], xt), 134 | lerp(uv0[0], uv1[0], xt), 135 | lerp(uv0[1], uv1[1], xt), 136 | ]; 137 | const p1 = [ 138 | lerp(c3[0], c2[0], xt), 139 | lerp(c3[1], c2[1], xt), 140 | lerp(c3[2], c2[2], xt), 141 | lerp(uv3[0], uv2[0], xt), 142 | lerp(uv3[1], uv2[1], xt), 143 | ]; 144 | 145 | for (let yi = 0; yi <= tn; yi++) { 146 | const yt = yi / tn; 147 | 148 | vertices.push( 149 | lerp(p0[0], p1[0], yt), 150 | lerp(p0[1], p1[1], yt), 151 | lerp(p0[2], p1[2], yt), 152 | ); 153 | uvs.push( 154 | lerp(p0[3], p1[3], yt), 155 | lerp(p0[4], p1[4], yt), 156 | ); 157 | colorUVs.push(colorU, colorV); 158 | normals.push(faceNormal[0], faceNormal[1], faceNormal[2]); 159 | } 160 | } 161 | 162 | for (let xi = 0; xi < tn; xi++) { 163 | for (let yi = 0; yi < tn; yi++) { 164 | const dy = yi * (tn + 1); 165 | 166 | // add two triangles for each subdivided quad 167 | const n1 = vertexIndex0 + dy + xi + 1; 168 | const n2 = vertexIndex0 + dy + xi + tn + 1; 169 | triangles.push( 170 | // 1 171 | vertexIndex0 + dy + xi, 172 | n1, 173 | n2, 174 | // 2 175 | n2, 176 | n1, 177 | vertexIndex0 + dy + xi + tn + 2, 178 | ); 179 | } 180 | } 181 | } else { 182 | // Save vertices used by this face. 183 | for (const vertex of faceVertices) { 184 | vertices.push(vertex[0], vertex[1], vertex[2]); 185 | 186 | normals.push(faceNormal[0], faceNormal[1], faceNormal[2]); 187 | } 188 | 189 | // Save UVs used by this face. 190 | // We always save both texture and color info, since models can be rendered in color mode. 191 | 192 | for (let i = 0; i < faceUVs.length; i++) { 193 | // Save color. 194 | colorUVs.push(colorU, colorV); 195 | 196 | // Save texture UVs. 197 | if (face.texture) { 198 | const uv = faceUVs[i]; 199 | uvs.push(uv[0], uv[1]); 200 | } else { 201 | // Re-use color UV. 202 | uvs.push(colorU, colorV); 203 | } 204 | } 205 | 206 | // Triangulate polygon. 207 | // This just uses fan triangulation :) 208 | for (let i = 0, n = faceIndices.length - 2; i < n; i++) { 209 | triangles.push( 210 | vertexIndex0 + 1 + i, 211 | vertexIndex0, 212 | vertexIndex0 + 2 + i, 213 | ); 214 | } 215 | } 216 | } 217 | } 218 | 219 | // Init and return passes. 220 | for (const pass of passes) { 221 | pass.save(); 222 | } 223 | 224 | wireframePass.save(); 225 | 226 | return { 227 | passes: passes, 228 | wireframe: wireframePass, 229 | }; 230 | } 231 | 232 | /** 233 | * @param {number} a 234 | * @param {number} b 235 | * @param {number} t 236 | */ 237 | function lerp(a, b, t) { 238 | return a + (b - a) * t; 239 | } 240 | 241 | /** 242 | * @param {number[][]} vertices 243 | */ 244 | function calculateFaceNormal(vertices) { 245 | for (let i = 0; i < vertices.length; i++) { 246 | const v0 = vertices[i]; 247 | const v1 = vertices[(i + 1) % vertices.length]; 248 | const v2 = vertices[(i + 2) % vertices.length]; 249 | 250 | const d0 = [ 251 | v0[0] - v1[0], 252 | v0[1] - v1[1], 253 | v0[2] - v1[2], 254 | ]; 255 | const d1 = [ 256 | v1[0] - v2[0], 257 | v1[1] - v2[1], 258 | v1[2] - v2[2], 259 | ]; 260 | 261 | const c = cross(d1, d0); 262 | const len = length(c); 263 | if (len > 0) { 264 | return [ 265 | c[0] / len, 266 | c[1] / len, 267 | c[2] / len, 268 | ]; 269 | } 270 | } 271 | 272 | // All edges are parallel (a line)... Just return any vector :) 273 | return [1, 0, 0]; 274 | } 275 | 276 | /** 277 | * @param {number[]} a 278 | * @param {number[]} b 279 | */ 280 | function cross(a, b) { 281 | return [ 282 | a[1] * b[2] - a[2] * b[1], 283 | a[2] * b[0] - a[0] * b[2], 284 | a[0] * b[1] - a[1] * b[0], 285 | ]; 286 | } 287 | 288 | /** 289 | * @param {number[]} a 290 | */ 291 | function length(a) { 292 | return Math.hypot(a[0], a[1], a[2]); 293 | } 294 | 295 | 296 | /** 297 | * @param {number[]} sourceIndices 298 | * @param {number} alphaIndex 299 | */ 300 | export function convertTexture(sourceIndices, alphaIndex) { 301 | const result = /** @type {number[]} */(Array(16384)).fill(255); 302 | 303 | for (let i = 0; i < sourceIndices.length; i++) { 304 | const index = sourceIndices[i]; 305 | if (index === alphaIndex) { 306 | // this is transparent 307 | result[i] = 255; 308 | } else { 309 | result[i] = index; 310 | } 311 | } 312 | 313 | // Add hidden indices on bottom row for single color faces. 314 | for (let i = 0; i < 16; i++) { 315 | result[16256 + i] = i; 316 | } 317 | 318 | return result; 319 | } 320 | -------------------------------------------------------------------------------- /src/model-parser.js: -------------------------------------------------------------------------------- 1 | import { PicoCADModel, PicoCADModelFace, PicoCADModelObject } from "./model"; 2 | import { parsePicoCADData } from "./model-data-parser"; 3 | import { readLine, splitString } from "./parser-utils"; 4 | 5 | /** 6 | * @param {string} source 7 | * @returns {PicoCADModel} 8 | */ 9 | export function parsePicoCADModel(source) { 10 | if (!source.startsWith("picocad;")) { 11 | throw Error("Not a picoCAD file."); 12 | } 13 | 14 | // Read header. 15 | const [header, body] = readLine(source); 16 | 17 | const headerValues = header.split(";"); 18 | const fileName = headerValues[1]; 19 | const [bestZoom, bgIndex, alphaIndex] = headerValues.slice(2).map(s => Number(s)); 20 | 21 | const [dataStr, texStr] = splitString(body, "%"); 22 | 23 | // Read data. 24 | const luaData = parsePicoCADData(dataStr); 25 | const objects = parseLuaData(luaData); 26 | 27 | // Read texture. 28 | const texIndices = parseTexture(readLine(texStr)[1]); 29 | 30 | // Done! 31 | return new PicoCADModel(objects, { 32 | name: fileName, 33 | alphaIndex: alphaIndex, 34 | backgroundIndex: bgIndex, 35 | zoomLevel: bestZoom, 36 | texture: texIndices, 37 | }); 38 | } 39 | 40 | /** 41 | * @param {import("./model-data-parser").LuaPicoCADModel} lua 42 | * @returns {PicoCADModelObject[]} 43 | */ 44 | function parseLuaData(lua) { 45 | return lua.array.map(luaObject => { 46 | const name = luaObject.dict.name; 47 | const pos = luaObject.dict.pos.array; 48 | const rot = luaObject.dict.rot.array; 49 | 50 | const vertices = luaObject.dict.v.array.map(la => la.array); 51 | 52 | const faces = luaObject.dict.f.array.map(luaFace => { 53 | const indices = luaFace.array.map(x => x - 1); 54 | const color = luaFace.dict.c; 55 | 56 | const flatUVs = luaFace.dict.uv.array; 57 | const uvs = []; 58 | for (let i = 1; i < flatUVs.length; i += 2) { 59 | uvs.push([ 60 | flatUVs[i - 1], 61 | flatUVs[i], 62 | ]); 63 | } 64 | 65 | return new PicoCADModelFace(indices, color, uvs, { 66 | shading: luaFace.dict.noshade !== 1, 67 | texture: luaFace.dict.notex !== 1, 68 | doubleSided: luaFace.dict.dbl === 1, 69 | renderFirst: luaFace.dict.prio === 1, 70 | }); 71 | }); 72 | 73 | return new PicoCADModelObject(name, pos, rot, vertices, faces); 74 | }); 75 | } 76 | 77 | /** 78 | * @param {string} s 79 | * @returns {number[]} 80 | */ 81 | function parseTexture(s) { 82 | const indices = /** @type {number[]} */(Array(15360)); 83 | 84 | let i = 0; 85 | let line; 86 | for (let y = 0; y < 120; y++) { 87 | [line, s] = readLine(s); 88 | 89 | for (let x = 0; x < 128; x++) { 90 | indices[i] = Number.parseInt(line.charAt(x), 16); 91 | i++; 92 | } 93 | } 94 | 95 | return indices; 96 | } 97 | -------------------------------------------------------------------------------- /src/model.js: -------------------------------------------------------------------------------- 1 | import { PICO_COLORS } from "./pico"; 2 | 3 | export class PicoCADModel { 4 | /** 5 | * @param {PicoCADModelObject[]} objects 6 | * @param {object} [options] 7 | * @param {string} [options.name] The model name. 8 | * @param {number} [options.backgroundIndex] The PICO-8 color index used for the background. Defaults to 0. 9 | * @param {number} [options.alphaIndex] The PICO-8 color index used for the texture alpha. Defaults to 0. 10 | * @param {number} [options.zoomLevel] The preferred initial zoom level. 11 | * @param {number[]} [options.texture] The 128x120 texture as an array of PICO-8 color indices. 12 | */ 13 | constructor(objects, options={}) { 14 | this.objects = objects; 15 | /** The model name. */ 16 | this.name = options.name; 17 | /** The PICO-8 color index used for the background. */ 18 | this.backgroundIndex = options.backgroundIndex ?? 0; 19 | /** The PICO-8 color index used for the texture alpha. */ 20 | this.alphaIndex = options.alphaIndex ?? 0; 21 | /** The PICO-8 color index used for the texture alpha. */ 22 | this.zoomLevel = options.zoomLevel; 23 | /** The 128x120 texture as an array of PICO-8 color indices. */ 24 | this.texture = options.texture; 25 | } 26 | 27 | backgroundColor() { 28 | return PICO_COLORS[this.backgroundIndex]; 29 | } 30 | 31 | alphaColor() { 32 | return PICO_COLORS[this.alphaIndex]; 33 | } 34 | 35 | /** 36 | * Converts the texture to pixels. 37 | * @param {number[][]} [colors] Maps indices to RGB colors. Defaults to the PICO-8 colors. 38 | * @returns {ImageData} 39 | * @example 40 | * let colors = Array(16); 41 | * colors.fill([10, 25, 120]) 42 | * model.textureAsImage(colors) 43 | */ 44 | textureAsImage(colors) { 45 | if (colors == null) { 46 | colors = PICO_COLORS; 47 | } 48 | 49 | const imgData = new ImageData(128, 128); 50 | const data = imgData.data; 51 | const tex = this.texture; 52 | const alphaIndex = this.alphaIndex; 53 | 54 | let i = 0; 55 | let ti = 0; 56 | for (let y = 0; y < 120; y++) { 57 | for (let x = 0; x < 128; x++) { 58 | const index = tex[i]; 59 | 60 | if (index !== alphaIndex) { 61 | const rgb = colors[index]; 62 | 63 | data[ti ] = rgb[0]; 64 | data[ti + 1] = rgb[1]; 65 | data[ti + 2] = rgb[2]; 66 | data[ti + 3] = 255; 67 | } 68 | 69 | i++; 70 | ti += 4; 71 | } 72 | } 73 | 74 | return imgData; 75 | } 76 | } 77 | 78 | export class PicoCADModelObject { 79 | /** 80 | * @param {string} name 81 | * @param {number[]} position 82 | * @param {number[]} rotation 83 | * @param {number[][]} vertices Array of triplets of 3D vertices. 84 | * @param {PicoCADModelFace[]} faces 85 | */ 86 | constructor(name, position, rotation, vertices, faces) { 87 | this.name = name; 88 | this.position = position; 89 | this.rotation = rotation; 90 | /** Array of triplets of 3D vertices. */ 91 | this.vertices = vertices; 92 | this.faces = faces; 93 | } 94 | } 95 | 96 | /** 97 | * The face of an object. 98 | */ 99 | export class PicoCADModelFace { 100 | /** 101 | * @param {number[]} indices Indices that point to a vertex in the object vertices. 0 base index. 102 | * @param {number} colorIndex PICO-8 color index. 103 | * @param {number[][]} uvs Array of pairs of UVs. Range is [0, 16]. 104 | * @param {object} [options] 105 | * @param {boolean} [options.shading] Defaults to true. 106 | * @param {boolean} [options.texture] Defaults to true. 107 | * @param {boolean} [options.doubleSided] Defaults to false. 108 | * @param {boolean} [options.renderFirst] Defaults to false. 109 | */ 110 | constructor(indices, colorIndex, uvs, options={}) { 111 | /** Indices that point to a vertex in the object vertices. 0 base index. */ 112 | this.indices = indices; 113 | /** PICO-8 color index. */ 114 | this.colorIndex = colorIndex; 115 | /** Array of pairs of UVs. Range is [0, 16]. */ 116 | this.uvs = uvs; 117 | /** If this face should be rendered with shading. */ 118 | this.shading = options.shading ?? true; 119 | /** If this face should be rendered with it's texture, or just using it's face color. */ 120 | this.texture = options.texture ?? true; 121 | this.doubleSided = options.doubleSided ?? false; 122 | this.renderFirst = options.renderFirst ?? false; 123 | } 124 | 125 | color() { 126 | return PICO_COLORS[this.colorIndex]; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/parser-utils.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @param {string} s 4 | * @param {string} sep 5 | * @returns {[string, string]} 6 | */ 7 | export function splitString(s, sep) { 8 | const i = s.indexOf(sep); 9 | return i < 0 ? [s, ""] : [s.slice(0, i), s.slice(i + sep.length)]; 10 | } 11 | 12 | /** 13 | * @param {string} s 14 | * @returns {[string, string]} 15 | */ 16 | export function readLine(s) { 17 | let i = 0; 18 | let end = s.length; 19 | while (i < s.length) { 20 | const c = s.charAt(i); 21 | i++; 22 | if (c === "\n") { 23 | end = i - 1; 24 | break; 25 | } else if (c === "\r") { 26 | end = i - 1; 27 | if (s.charAt(i) === "\n") { 28 | i++; 29 | } 30 | break; 31 | } 32 | } 33 | 34 | return [s.slice(0, end), s.slice(i)]; 35 | } 36 | -------------------------------------------------------------------------------- /src/pass.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * A rendering pass. 4 | */ 5 | export class Pass { 6 | /** 7 | * @param {WebGLRenderingContext} gl 8 | * @param {{cull?: boolean, shading?: boolean, texture?: boolean, clearDepth?: boolean}} [options] 9 | */ 10 | constructor(gl, options={}) { 11 | this.gl = gl; 12 | this.cull = options.cull ?? true; 13 | this.shading = options.shading ?? true; 14 | this.texture = options.texture ?? true; 15 | this.clearDepth = options.clearDepth ?? false; 16 | 17 | /** @type {number[]} */ 18 | this.vertices = []; 19 | /** @type {number[]} */ 20 | this.normals = []; 21 | /** @type {number[]} */ 22 | this.uvs = []; 23 | /** @type {number[]} */ 24 | this.colorUVs = []; 25 | /** @type {number[]} */ 26 | this.triangles = []; 27 | } 28 | 29 | /** 30 | * Upload changes to the GL context. 31 | */ 32 | save() { 33 | const gl = this.gl; 34 | 35 | this.vertexCount = this.triangles.length; 36 | 37 | if (!this.isEmpty()) { 38 | this.vertexBuffer = gl.createBuffer(); 39 | this.uvBuffer = gl.createBuffer(); 40 | this.colorUVBuffer = gl.createBuffer(); 41 | this.triangleBuffer = gl.createBuffer(); 42 | this.normalBuffer = gl.createBuffer(); 43 | 44 | gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); 45 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.vertices), gl.STATIC_DRAW); 46 | 47 | gl.bindBuffer(gl.ARRAY_BUFFER, this.uvBuffer); 48 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.uvs), gl.STATIC_DRAW); 49 | 50 | gl.bindBuffer(gl.ARRAY_BUFFER, this.colorUVBuffer); 51 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.colorUVs), gl.STATIC_DRAW); 52 | 53 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.triangleBuffer); 54 | gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(this.triangles), gl.STATIC_DRAW); 55 | 56 | gl.bindBuffer(gl.ARRAY_BUFFER, this.normalBuffer); 57 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.normals), gl.STATIC_DRAW); 58 | } 59 | 60 | this.uvs = null; 61 | this.colorUVs = null; 62 | this.normals = null; 63 | this.vertices = null; 64 | this.triangles = null; 65 | } 66 | 67 | /** 68 | * If there is nothing to render. 69 | */ 70 | isEmpty() { 71 | return this.vertexCount === 0; 72 | } 73 | 74 | free() { 75 | const gl = this.gl; 76 | 77 | gl.deleteBuffer(this.vertexBuffer); 78 | gl.deleteBuffer(this.uvBuffer); 79 | gl.deleteBuffer(this.colorUVBuffer); 80 | gl.deleteBuffer(this.triangleBuffer); 81 | gl.deleteBuffer(this.normalBuffer); 82 | } 83 | } 84 | 85 | export class WirePass { 86 | /** 87 | * @param {WebGLRenderingContext} gl 88 | */ 89 | constructor(gl) { 90 | this.gl = gl; 91 | 92 | /** @type {number[]} */ 93 | this.vertices = []; 94 | } 95 | 96 | save() { 97 | const gl = this.gl; 98 | 99 | this.vertexCount = Math.floor(this.vertices.length / 3); 100 | 101 | this.vertexBuffer = gl.createBuffer(); 102 | gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); 103 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.vertices), gl.STATIC_DRAW); 104 | 105 | this.vertices = null; 106 | } 107 | 108 | free() { 109 | this.gl.deleteBuffer(this.vertexBuffer); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/pico.js: -------------------------------------------------------------------------------- 1 | 2 | export const PICO_COLORS = [ 3 | [0, 0, 0], 4 | [29, 43, 83], 5 | [126, 37, 83], 6 | [0, 135, 81], 7 | [171, 82, 54], 8 | [95, 87, 79], 9 | [194, 195, 199], 10 | [255, 241, 232], 11 | [255, 0, 77], 12 | [255, 163, 0], 13 | [255, 236, 39], 14 | [0, 228, 54], 15 | [41, 173, 255], 16 | [131, 118, 156], 17 | [255, 119, 168], 18 | [255, 204, 170], 19 | ]; 20 | -------------------------------------------------------------------------------- /src/shader-program.js: -------------------------------------------------------------------------------- 1 | 2 | export class ShaderProgram { 3 | /** 4 | * @param {WebGLRenderingContext} gl 5 | * @param {string} vertexShader 6 | * @param {string} fragmentShader 7 | */ 8 | constructor(gl, vertexShader, fragmentShader) { 9 | this.gl = gl; 10 | 11 | // create shaders 12 | const vs = this.createShader(gl.VERTEX_SHADER, vertexShader); 13 | const fs = this.createShader(gl.FRAGMENT_SHADER, fragmentShader); 14 | 15 | this.program = gl.createProgram(); 16 | gl.attachShader(this.program, vs); 17 | gl.attachShader(this.program, fs); 18 | gl.linkProgram(this.program); 19 | 20 | gl.deleteShader(vs); 21 | gl.deleteShader(fs); 22 | 23 | if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) { 24 | const msg = gl.getProgramInfoLog(this.program); 25 | gl.deleteProgram(this.program); 26 | throw Error("program compilation failed: " + msg); 27 | } 28 | 29 | this.vertexLocation = this.getAttribLocation("vertex"); 30 | } 31 | 32 | /** 33 | * @param {number} type 34 | * @param {string} source 35 | */ 36 | createShader(type, source) { 37 | const gl = this.gl; 38 | 39 | const shader = gl.createShader(type); 40 | 41 | gl.shaderSource(shader, source); 42 | gl.compileShader(shader); 43 | 44 | if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { 45 | const msg = gl.getShaderInfoLog(shader); 46 | gl.deleteShader(shader); 47 | throw Error(`${type === gl.FRAGMENT_SHADER ? "fragment" : "vertex"} shader compilation failed: ${msg}`); 48 | } 49 | 50 | return shader; 51 | } 52 | 53 | /** 54 | * @param {string} name 55 | */ 56 | getAttribLocation(name) { 57 | return this.gl.getAttribLocation(this.program, name); 58 | } 59 | 60 | /** 61 | * @param {string} name 62 | */ 63 | getUniformLocation(name) { 64 | return this.gl.getUniformLocation(this.program, name); 65 | } 66 | 67 | use() { 68 | this.gl.useProgram(this.program); 69 | } 70 | 71 | free() { 72 | this.gl.deleteProgram(this.program); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/text.js: -------------------------------------------------------------------------------- 1 | 2 | // Direct Base64 encoding of font.gif. 3 | // Adapted from the CC-0 PICO-8 font: https://www.lexaloffle.com/gfx/pico-8_font_022.png 4 | const FONT_GIF_BASE64 = "R0lGODdhgACAAIAAAAAAAP///yH5BAkKAAAALAAAAACAAIAAAAL/hI+py+0Po5y02ouz3rz7D4biSJbmiabqyrbuC5vBHBtBcitz/ty859PpaIgdjngIFgE/pvMIxTWaRpAy+UwibVtnM9sDM7tVLOP7FQOHVDJZfLWp3cLzMV2PYsow6oQvJ4GGV1NoeIiYqLgYJoUGBhg3tpBWKUQogrnkeIfUZaaVt0kpCmr1h9pWyhVB2Pba+mkXsuP5dtk6tarGS6E5CiH5CJu7KjkruMu4zNzs/BwTOUiMRR14h10k61ucXa1sueSnDFf+uDsNzinlHaptPRauBc9dr70OfI1OKs7/bewvXzxy7Qa+K6jP4LyACgUydKgOmsSJFCtaRHXx2TE6/0P0LDzo0UvBSOXYhGTVL6U7h8IG7TvH7l5MkP9qJuTiZxy+m7xawhQojFTOnShtNiyKdCVPkvA+bRyqMmnGqVSrWr3KzOmZnz24TupIUw5Uo0GbIpspMpg5oniGsZ1jslTbsxWkgQXI1o3XhPLQmqq7Ne/Dr0DfqBo59ySlbVgbO34M+aE0lzLDHiUsFvHYpRwVb077NzPLwRsllxy5Nljn0KClao07+qSlz8eEvsUV++1et6Np98pL+aNwzrRXix7eOjnjyMybO6cIaI5Ts76rI3xd+ULZ2wCtR01+sy9ruKgPW4brvbBXWZWo+8V8HChu5EwZJn6/l/zd+PA5G/8zrNl+4f3yXIEGHghNLUYEwSA7Cl6jU1g0bDFZOw/GUwtNF5qiIBENYrggD56AkiFOWWw44Q98XFjFhAOpWJktInYoUoMLEuYiXxSWCKKJLZbYoQ8zgkjhPULi5CGNNYrlYogimsFjj8OlKGFaP5a04pAmInnjhygCyRo96UQl5Iw/sqjXLQiuyWab0UQJB48b+gIkjTZuCaFn2eTw5BR8yuiNk8qRyKArS/I1SpRwvtLkkrYQCeNKHormZYplZngnhhza10mfmiT5Z5Za9hepF4XqcmeOSvTF53Gt/jbPiBDKCacgdT6Y6aQ8NXSkO08S6Gawwg7rQK2QEIrdrlj/dhLaorSStMmIigp4qShyLnZmdGYShOieyZrZZ0zAliqXPkr+idxM65mbh3l5LuWPihHKpwunEGmqnKFQMqsUns1O56m8weXjbLTLEYtwwm6+Wg2j+xYlr1polsFeng6TySsboFocSJmEdmWwjJFue+KKGg95BcWrvvtqy4mixXC4fymJ3qrQenqiI0du7LKp4f76xMj4Dv3VqOi+e+xiscqk66Mdw0dx0bbd+KJc2ppltcJab621TuCSy2XD5wHLwZxJMSoziR5RGaOuHTMs8T71zrxop75GayTeSPM6ZthICtgatKfJHXSSe/eaWqPqchxxRPaO3Z7BesD999H0/Yptmb5rG8t15543N++/Rdr9MKnTihurwP2orl62X+vtd6Jm95Q67GkP9Utb48z7865Co8QV5U/nKKW4igKoduxh0hUetXNn18u6OSetZm7K6xiFNdOhs9vn3n8PeiyC40XcjvYdX30ddZPur+zRUb90vOYam7sdYBev7Hz9sjt9vWDzpjzhCWx373kepmBWusEMb3AGFEfa4MW+SfCueiTb39SsJaoraexe4OugByWSwXeMDkb3K96XVNYM8cyChLnpndw+NB5sbWCAIvxdfFxIu/j5hYbQQwrNiqVAVlmJf/rZYXccV5dnDWxTOEpT4zbnLtHR44NUrGJGCgAAOw=="; 5 | 6 | /** 7 | * @returns {Promise} 8 | */ 9 | async function loadFontImage() { 10 | return new Promise((resolve, reject) => { 11 | let img = new Image(); 12 | img.onload = () => resolve(img); 13 | img.onerror = () => reject("could not load PICO-8 font"); 14 | img.src = "data:image/gif;base64," + FONT_GIF_BASE64; 15 | }); 16 | } 17 | 18 | let lazyLoading = false; 19 | let lazyLoadedFont = null; 20 | 21 | /** 22 | * @returns {HTMLImageElement|null} 23 | */ 24 | export function lazyLoadedFontImage() { 25 | if (!lazyLoading) { 26 | lazyLoading = true; 27 | loadFontImage().then(font => lazyLoadedFont = font); 28 | } 29 | return lazyLoadedFont; 30 | } 31 | --------------------------------------------------------------------------------