├── .github └── workflows │ └── static.yml ├── .gitignore ├── .nvmrc ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── screenshots └── desktop.png └── src ├── Editor.jsx ├── EditorCanvas.jsx ├── EditorImporter.jsx ├── EditorMetadata.jsx ├── EditorMetadataInput.jsx ├── EditorPalette.jsx ├── EditorQrGenerator.jsx ├── EditorSwatch.jsx ├── EditorTools.js ├── acnl.js ├── index.css ├── index.js ├── jsqrcode.js ├── logo.svg └── qrcode.js /.github/workflows/static.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["master"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: false 23 | 24 | jobs: 25 | # Single deploy job since we're just deploying 26 | deploy: 27 | environment: 28 | name: github-pages 29 | url: ${{ steps.deployment.outputs.page_url }} 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v3 34 | - name: Setup Pages 35 | uses: actions/configure-pages@v3 36 | - name: Install Node 37 | uses: actions/setup-node@v3.8.1 38 | with: 39 | node-version-file: '.nvmrc' 40 | - name: Build 41 | run: | 42 | npm ci 43 | npm run build --if-present 44 | env: 45 | PUBLIC_URL: '/react-acnl-pattern-tool' 46 | CI: 'false' 47 | - name: Upload artifact 48 | uses: actions/upload-pages-artifact@v2 49 | with: 50 | # Upload entire repository 51 | path: './build' 52 | - name: Deploy to GitHub Pages 53 | id: deployment 54 | uses: actions/deploy-pages@v2 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v15.14.0 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Animal Crossing New Leaf Tool 2 | 3 |
4 | 5 | 6 | 7 |
8 |
9 |

10 | Official Website 11 |

12 | A React.js port of the initial 13 | Animal Crossing New Leaf Pattern Tool by Thulinma 14 |

15 |

This project was bootstrapped with Create React App

16 |
17 | 18 | 19 | ## Table of Contents 20 | * [Stack](#stack) 21 | * [Notes](#notes) 22 | * [Available Scripts](#available-scripts) 23 | * [Old Architecture](#old-architecture) 24 | * [New Architecture](#new-architecture) 25 | * [Optimizations](#optimizations) 26 | * [Performance Evaluation](#performance-evaluation) 27 | 28 | ## Stack 29 | 30 | * [file-saver](https://github.com/eligrey/FileSaver.js/) (writing out a binary file) 31 | * [jsqrcode](https://github.com/LazarSoft/jsqrcode) (reading in qr codes) 32 | * [qrcode-generator](https://github.com/kazuhikoarase/qrcode-generator) (generating qr codes) 33 | * (other original dependencies have been removed for optimization) 34 | 35 | ## Notes 36 | 37 | * the [qrcode-generator](https://github.com/kazuhikoarase/qrcode-generator) library doesn't support multipart QR codes (at all), we will be using the patched version of it by Thulinma instead available [here](https://github.com/Thulinma/ACNLPatternTool/blob/master/qrcode.js). 38 | 39 | * the [jsqrcode](https://github.com/LazarSoft/jsqrcode) library still has trouble recognizing the QR codes and there are still some errors to be fixed in the original library. We will be using Thulinma's patched version at [his fork's branch](https://github.com/Thulinma/jsqrcode/tree/finder_fix_mini). 40 | 41 | ## Available Scripts 42 | 43 | In the project directory, you can run: 44 | 45 | ### `npm start` 46 | 47 | Runs the app in the development mode.
48 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 49 | 50 | The page will reload if you make edits.
51 | You will also see any lint errors in the console. 52 | 53 | 54 | ## Old Architecture 55 | 56 | The architecture for original application (not this one) was unorganized. Both the controller and model directly rendered parts of the view on their own. It wasn't very clear how it should be handled. 57 | 58 | `acnl.js` 59 | ```javascript 60 | 61 | function setColor() { 62 | ... 63 | for (var i in canvasses){ 64 | drawPixel(canvasses[i], x, y, c, getZoom(canvasses[i].canvas)); 65 | } 66 | } 67 | 68 | ``` 69 | 70 | `page.js` 71 | ```javascript 72 | .mousemove(function(event) { 73 | ... 74 | ACNL.setColor(x, y, chosen_color) 75 | }) 76 | ``` 77 | 78 | ## New Architecture 79 | 80 | The new architecture uses an MVC model. The controller interacts with the model and is responsible for rendering the visual representation of the model. The controller now consists of several parts. 81 | 82 | * Editor 83 | * Canvas 84 | * Swatch 85 | * Palette 86 | * QR Code Generator 87 | 88 | Note: the term model will refer to the `ACNL` class from `acnl.js` which represents the ACNL file format used to save/render QR codes in Animal Crossing New Leaf (ACNL). 89 | 90 | Note: the term pixel in this write-up refers to a pattern pixel drawn onto the canvas (as the pattern size is 32x32 pixels and up to 4 patterns can exist on a canvas), not a physical or css pixel. 91 | 92 | The editor is the parent of the canvas, palette, swatch, and qr code generator. It acts as the main center of control. Components cannot update each other directly, but must now communicate with the editor component in order to update other components and the model respectively. Figuratively, the 'editor' is a user that can manipulate both the model and the view. The 'editor' holds onto user information (e.g. current drawing color). 93 | 94 | The components themselves are now modular, easily allowing for additional modifications to be added. For example, pixel tools can be added in the form of a module. All they have to do is return a list of pixels that need to be colored in for the editor to handle via `updatePixelBuffer([pixels...[x, y]])` (read into optimizations on the pixel buffer). It is now possible to introduce pen sizes and bucket tools by simplying extending the EditorTools module. 95 | 96 | ## Optimizations 97 | 98 | ### 1. The Pixel Buffer 99 | When the user is drawing on the canvas, there are two operations that need to be executed: 100 | 101 | 1. the canvas needs to update itself to color in the pixel 102 | 2. the ACNL file needs to have update itself to have the color of the pixel in the data changed 103 | 104 | Since React will force a re-render (redrawing the entire pattern from scratch) when we modify the file and the render is expensive, this application now uses a `pixelBuffer` to store the necessary file modifications. The drawing is performed live while the changed pixels are cached to be applied to the file at a later time. `updatePixelBuffer(x, y)` will handle both the drawing and caching of the pixel that needs to be modified. It will also schedule the application of the file modifications in the `pixelBuffer` to a time when the user is free. 105 | 106 | The `pixelBuffer` is specific to the chosen drawing color and will force the file to update when the chosen drawing color has changed. 107 | 108 | The `pixelBuffer` also prevents the additions of pixels that match the last added pixel in the buffer. This is useful when the user is slowly drawing and the `mousemove` will generate draws on the same pixel more than once. Although this helps reduce duplicates, it will not prevent duplicate pixels from existing in the buffer. 109 | 110 | ### 2. More Caching 111 | 112 | Expensive operations such as `getBoundingClientRect()` and `getContext("2d")` have all been cached into the canvas components and can now conditionally update when necessary (via resize/scrolling or re-rendering respectively). 113 | 114 | ### 3. Controlling Renders 115 | 116 | Re-renders have also been further reduced by manually controlling component update conditions via `shouldComponentUpdate()` on all components. 117 | 118 | The qr code generator no longer probes for the `typeNumber` and uses hardcoded `typeNumbers` to reduce runtime. The qr code generator now only updates the qr codes that have been affected by data changes and not all qr codes. 119 | 120 | ## Performance Evaluation 121 | 122 | ### Overall 123 | 124 | Even though the application has been restructured to fit into the React.js framework with optimizations added and dependencies (e.g. jquery) removed, the overall performance (for drawing) of the application turned out worst than the original (in some respects). Let's get down to the specifics. We'll discuss just drawing and event handling since these are two most important. The specific use case we'll be discussing is drawing on pattern continuously without lifting the mouse. The statistical test was done via Chrome's performance profiles at varying performance throttling levels (1x, 4x, 6x). 125 | 126 | ### Pattern Rendering 127 | 128 | [Reference on Painting and Compositing.](https://medium.com/outsystems-experts/how-to-achieve-60-fps-animations-with-css3-db7b98610108) 129 | 130 | Both tools take the same approach to drawing. They only overwrite the color of pixel that's being drawn on instead of fully redrawing the pattern with the changed pixel. However, the React.js port was able to both match and beat out the original tool in terms of painting and compositing at the time it was forked (the first commit on github). The original tool redraws the entire pattern unecessarily several times: 131 | 132 | 1. when the user starts to click 133 | 2. when the user releases click 134 | 135 | Both cases have been entirely removed in this port. Full refreshes are deferred to a scheduler that will sync the pattern with its data equivalent when the user is not trying to draw with the chosen swatch color. 136 | 137 | This port also caches expensive operations such as `DOMelement.getBoundingClientRect()` and `canvas.getContext()`. While jquery uses `offset()`, in the background it is making calls to `getBoudingClientRect()` which causes [layout thrashing](http://wilsonpage.co.uk/preventing-layout-thrashing/). This problem is removed entirely in this port since each canvas's `boundingClientRect` is cached alongside `canvas.getContext()` into the component and updated only when necessary. Both functions are used quite frequently in the drawing process, so caching them slightly improved performance. 138 | 139 | ### Event Handling 140 | 141 | Our port loses out in this race, even with the optimizations. If we were to compare raw event handler functions (`draw` in `Editor.jsx` vs `setColor` in `acnl.js` from the original) performance used for drawing against each other, this version does much, much better than the original (by almost `300%` in fact). How is it possible that we still lose in overall event handling? 142 | 143 | React uses a [synthetic event handler](https://reactjs.org/docs/events.html) instead of native event handlers to account for browser compatibility. The synthetic event handler comes with a lot of overhead, both from being passed around and from deep encapsulation. 144 | 145 | When drawing for `10s` straight, the port's total raw handling time went from `31.0ms` to a total synthetic event handling time of `567.1ms`. The original tool, using jquery to handle events, went from a raw `96.0ms` to `215.1ms`. While we beat the original tool by a wide margin in raw event handling (due to the `pixelBuffer` caching the file operations), we were not able to beat the original tool in overall event handling due to React's synthetic wrapper. 146 | 147 | ### Conclusion 148 | 149 | So what did we learn with all this effort? Don't use React.js when you have to constantly handle events like in a drawing application. It's a bad idea due to the overhead costs associated with the synethetic event wrapper created by React. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-pattern-tool", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "file-saver": "^2.0.0", 7 | "react": "^16.6.3", 8 | "react-dom": "^16.6.3", 9 | "react-scripts": "2.1.1" 10 | }, 11 | "scripts": { 12 | "start": "react-scripts start", 13 | "build": "react-scripts build", 14 | "test": "react-scripts test", 15 | "eject": "react-scripts eject" 16 | }, 17 | "eslintConfig": { 18 | "extends": "react-app" 19 | }, 20 | "browserslist": [ 21 | ">0.2%", 22 | "not dead", 23 | "not ie <= 11", 24 | "not op_mini all" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DamSenViet/react-acnl-pattern-tool/8353cc7efe6c6c6d5e4d8565c290eaf8352bf800/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | React Animal Crossing Pattern Tool 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /screenshots/desktop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DamSenViet/react-acnl-pattern-tool/8353cc7efe6c6c6d5e4d8565c290eaf8352bf800/screenshots/desktop.png -------------------------------------------------------------------------------- /src/Editor.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import EditorCanvas from './EditorCanvas.jsx'; 3 | import EditorPalette from './EditorPalette.jsx'; 4 | import EditorSwatch from './EditorSwatch.jsx'; 5 | import EditorMetadata from './EditorMetadata.jsx'; 6 | import EditorImporter from './EditorImporter.jsx'; 7 | import EditorQrGenerator from './EditorQrGenerator.jsx'; 8 | import * as EditorTools from './EditorTools.js'; 9 | 10 | // regular js imports 11 | import ACNL from './acnl.js'; 12 | 13 | // control center for all editor things 14 | // maintains control for drawing and data 15 | // using async callback patterns (b/c setState is async) 16 | class Editor extends React.Component { 17 | constructor(props) { 18 | super(props); 19 | this.state = { 20 | acnl: new ACNL(), 21 | chosenColor: 0, 22 | chosenTool: new EditorTools.Pen(), 23 | isDrawing: false, 24 | // buffers pixel operations for a SINGLE chosen color 25 | pixelBuffer: [], 26 | pixelRefreshTimer: null, 27 | // keep track of canvases so I can make later calls to their draws 28 | canvases: [ 29 | React.createRef(), 30 | React.createRef(), 31 | React.createRef() 32 | ], 33 | shouldQrCodeUpdate: false, 34 | qrRefreshTimer: null, 35 | }; 36 | } 37 | 38 | // canvas will need this to draw and drag across and between multiple canvases 39 | setIsDrawing(isDrawing) { 40 | if (isDrawing !== this.state.isDrawing) { 41 | this.setState({ 42 | isDrawing: isDrawing, 43 | }); 44 | } 45 | } 46 | 47 | selectSwatchColor(newChosenColor) { 48 | // before switching colors, need to empty pixelBuffer 49 | // each pixelBuffer is specific to the chosen color 50 | // gotta make sure pixels in buffer are colored with old color, not new one 51 | this.refreshPixels(() => { 52 | let chosenColor = this.state.chosenColor; 53 | if (chosenColor !== newChosenColor) { 54 | // not setting Qr timer here, because just changing colors 55 | this.setState({ 56 | chosenColor: newChosenColor, 57 | shouldQrCodeUpdate: false, 58 | }); 59 | } 60 | }); 61 | } 62 | 63 | // any time we make modifications to acnl data, need to reset qr timer 64 | selectPaletteColor(newBinColor) { 65 | this.refreshPixels(() => { 66 | let acnl = this.state.acnl.clone(); 67 | let chosenColor = this.state.chosenColor; 68 | let chosenBinColor = acnl.swatch[chosenColor]; 69 | if (chosenBinColor !== newBinColor) { 70 | acnl.setSwatchColor(chosenColor, newBinColor); 71 | this.setState( 72 | { 73 | acnl: acnl, 74 | shouldQrCodeUpdate: false, 75 | }, 76 | () => this.setQrCodeTimer() 77 | ); 78 | } 79 | }); 80 | } 81 | 82 | // store changes 83 | // need to guarantee a pixel refresh (complete update to ACNL file) sometime 84 | // support for multi-pixel drawing tools e.g. bucket, bigger pen sizes 85 | // by adding specific pixels 86 | updatePixelBuffer(pixelsToAdd) { 87 | this.clearQrCodeTimer(); 88 | 89 | // mousemove might be called "too quickly" and add the last pixel twice 90 | // do not handle duplicate pixels in the last pos of the buffer 91 | let pixelBuffer = this.state.pixelBuffer.slice(); 92 | let chosenTool = this.state.chosenTool; 93 | if (chosenTool.willUpdatePixelBuffer(pixelsToAdd, pixelBuffer)) { 94 | this.clearPixelRefreshTimer(); 95 | let chosenColor = this.state.chosenColor; 96 | 97 | // update context before performing operations 98 | for (let i = 0; i < this.state.canvases.length; ++i) { 99 | this.state.canvases.forEach(ref => { 100 | // KEEP CONTEXT CACHED for full re-render speed 101 | // losing context here, update context right before drawing 102 | // not much time spent updating context anyway 103 | ref.current.updateContext(); 104 | }); 105 | } 106 | 107 | // add each pixel to the buffer and color it in 108 | for (let i = 0; i < pixelsToAdd.length; ++i) { 109 | let pixel = pixelsToAdd[i]; 110 | let x = pixel[0]; 111 | let y = pixel[1]; 112 | pixelBuffer.push([x, y]); 113 | for (let i = 0; i < this.state.canvases.length; ++i) { 114 | this.state.canvases[i].current.drawPixel(x, y, chosenColor); 115 | } 116 | } 117 | 118 | this.setState( 119 | { 120 | pixelBuffer: pixelBuffer, 121 | shouldQrCodeUpdate: false, 122 | }, 123 | () => this.setPixelRefreshTimer() 124 | ); 125 | } 126 | } 127 | 128 | // batch apply changes in pixel buffer 129 | refreshPixels(callback) { 130 | // e.g. selectPaletteColor & setSwatchColor, some don't need to set qr timer 131 | // callback needs to manually set qr timer (to prevent duplicate setTimers) 132 | this.clearQrCodeTimer(); 133 | let pixelBuffer = this.state.pixelBuffer.slice(); 134 | // if there's nothing in the buffer, no need to update 135 | if (pixelBuffer.length === 0) { 136 | if (callback) callback(); 137 | return; 138 | } 139 | 140 | let acnl = this.state.acnl.clone(); 141 | let chosenColor = this.state.chosenColor; 142 | for (let i = 0; i < pixelBuffer.length; ++i) { 143 | let x = pixelBuffer[i][0]; 144 | let y = pixelBuffer[i][1]; 145 | acnl.colorPixel(x, y, chosenColor); 146 | } 147 | 148 | // empty pixel buffer and update acnl 149 | this.setState( 150 | { 151 | acnl: acnl, 152 | pixelBuffer: [], 153 | shouldQrCodeUpdate: false, 154 | }, 155 | () => { 156 | if (callback) callback(); 157 | } 158 | ); 159 | } 160 | 161 | clearPixelRefreshTimer() { 162 | // no need to check existence, since this will be called too many times 163 | window.clearTimeout(this.state.pixelRefreshTimer); 164 | } 165 | 166 | setPixelRefreshTimer() { 167 | let pixelRefreshTimer = window.setTimeout(() => { 168 | this.refreshPixels(() => { 169 | this.setQrCodeTimer() 170 | }); 171 | }, 500); 172 | this.setState({ 173 | pixelRefreshTimer: pixelRefreshTimer 174 | }); 175 | } 176 | 177 | refreshQrCode() { 178 | this.setState({ 179 | shouldQrCodeUpdate: true, 180 | pixelRefreshTimer: null 181 | }); 182 | // console.log("trigger Qr refresh"); 183 | } 184 | 185 | clearQrCodeTimer() { 186 | if (this.state.qrRefreshTimer) { 187 | window.clearTimeout(this.state.qrRefreshTimer); 188 | this.setState({ 189 | qrRefreshTimer: null, 190 | }); 191 | } 192 | } 193 | 194 | setQrCodeTimer() { 195 | let qrRefreshTimer = window.setTimeout(() => { 196 | this.refreshQrCode(); 197 | }, 2500); 198 | this.setState({ 199 | qrRefreshTimer: qrRefreshTimer, 200 | }); 201 | } 202 | 203 | 204 | // deal with metadata 205 | 206 | updatePatternTitle(title) { 207 | this.clearQrCodeTimer(); 208 | 209 | let acnl = this.state.acnl.clone(); 210 | if (acnl.patternTitle !== title) { 211 | acnl.patternTitle = title; 212 | this.setState( 213 | { 214 | acnl: acnl, 215 | shouldQrCodeUpdate: false, 216 | }, 217 | () => this.setQrCodeTimer() 218 | ); 219 | } 220 | } 221 | 222 | updateUserName(name) { 223 | this.clearQrCodeTimer(); 224 | 225 | let acnl = this.state.acnl.clone(); 226 | if (acnl.userName !== name) { 227 | acnl.userName = name; 228 | this.setState( 229 | { 230 | acnl: acnl, 231 | shouldQrCodeUpdate: false, 232 | }, 233 | () => this.setQrCodeTimer() 234 | ); 235 | } 236 | } 237 | 238 | updateUserID(id) { 239 | this.clearQrCodeTimer(); 240 | 241 | let acnl = this.state.acnl.clone(); 242 | if (acnl.userID !== id) { 243 | acnl.userID = id; 244 | this.setState( 245 | { 246 | acnl: acnl, 247 | shouldQrCodeUpdate: false, 248 | }, 249 | () => this.setQrCodeTimer() 250 | ); 251 | } 252 | } 253 | 254 | updateTownName(name) { 255 | this.clearQrCodeTimer(); 256 | 257 | let acnl = this.state.acnl.clone(); 258 | if (acnl.townName !== name) { 259 | acnl.townName = name; 260 | this.setState( 261 | { 262 | acnl: acnl, 263 | shouldQrCodeUpdate: false, 264 | }, 265 | () => this.setQrCodeTimer() 266 | ); 267 | } 268 | } 269 | 270 | updateTownID(id) { 271 | this.clearQrCodeTimer(); 272 | 273 | let acnl = this.state.acnl.clone(); 274 | if (acnl.townID !== id) { 275 | acnl.townID = id; 276 | this.setState( 277 | { 278 | acnl: acnl, 279 | pixelRefreshTimer: null, 280 | shouldQrCodeUpdate: false, 281 | }, 282 | () => this.setQrCodeTimer() 283 | ); 284 | } 285 | } 286 | 287 | // replace entire acnl and state 288 | import(acnlData) { 289 | this.clearPixelRefreshTimer(); 290 | this.clearQrCodeTimer(); 291 | this.setState( 292 | { 293 | acnl: new ACNL(acnlData), 294 | chosenColor: 0, 295 | isDrawing: false, 296 | pixelBuffer: [], 297 | pixelRefreshTimer: null, 298 | shouldQrCodeUpdate: false, 299 | qrRefreshTimer: null, 300 | }, 301 | () => this.setQrCodeTimer() 302 | ); 303 | } 304 | 305 | 306 | // perform import and convert according to setting 307 | // we are just replacing the swatch, pattern data 308 | convert(imgData, convSet) { 309 | this.clearPixelRefreshTimer(); 310 | this.clearQrCodeTimer(); 311 | 312 | let acnl = this.state.acnl.clone(); 313 | // turn into a standard pattern 314 | acnl.toStandardPattern(); 315 | 316 | // select the palette 317 | if (convSet === "top") this.usePaletteTop(acnl, imgData); 318 | else if (convSet === "lowest") this.usePaletteLowest(acnl, imgData); 319 | else if (convSet === "grey") this.usePaletteGrey(acnl); 320 | else if (convSet === "sepia") this.usePaletteSepia(acnl); 321 | 322 | this.drawImage(acnl, imgData); 323 | 324 | this.setState( 325 | { 326 | acnl: acnl, 327 | chosenColor: 0, 328 | isDrawing: false, 329 | pixelBuffer: [], 330 | pixelRefreshTimer: null, 331 | shouldQrCodeUpdate: false, 332 | qrRefreshTimer: null, 333 | }, 334 | () => this.setQrCodeTimer() 335 | ); 336 | } 337 | 338 | /* CONVERT HELPER START */ 339 | // pick palette from 15 most used, inaccurate 340 | usePaletteTop(acnl, imgData) { 341 | let palette = []; 342 | for (let i = 0; i < 256; ++i) { 343 | palette.push({ 344 | binColor: i, 345 | score: 0, 346 | }); 347 | } 348 | 349 | let scorePaletteColor = (r, g, b) => { 350 | let best = 120; 351 | let bestPaletteColor = 0; 352 | for (let i = 0; i < 256; i++) { 353 | let toMatch = ACNL.paletteBinToHex[i]; 354 | if (toMatch === undefined) continue; 355 | let x = parseInt(toMatch.substr(1, 2), 16); 356 | let y = parseInt(toMatch.substr(3, 2), 16); 357 | let z = parseInt(toMatch.substr(5, 2), 16); 358 | let matchDegree = Math.abs(x - r) + Math.abs(y - g) + Math.abs(z - b); 359 | if (matchDegree < best) { 360 | best = matchDegree; 361 | bestPaletteColor = i; 362 | } 363 | } 364 | // increment score for its occurence 365 | palette[bestPaletteColor].score++; 366 | } 367 | 368 | // accumulate scores 369 | for (let i = 0; i < 4096; i += 4) { 370 | scorePaletteColor( 371 | imgData.data[i], 372 | imgData.data[i + 1], 373 | imgData.data[i + 2] 374 | ); 375 | } 376 | 377 | // sort by palette occurences, decreasing order 378 | palette.sort((a, b) => { 379 | if (a.score > b.score) return -1; 380 | if (a.score < b.score) return 1; 381 | return 0; 382 | }); 383 | 384 | // rebinding, cut the palette, leaving only binColors 385 | let swatchBinColors = palette.slice(0, 15) 386 | .map((palObj) => palObj.binColor); 387 | 388 | for (let i = 0; i < swatchBinColors.length; ++i) { 389 | acnl.setSwatchColor(i, swatchBinColors[i]); 390 | } 391 | } 392 | 393 | // choosing palette from top 40 colors, accurate 394 | usePaletteLowest(acnl, imgData) { 395 | let palette = []; 396 | let prepixels = []; 397 | for (let i = 0; i < 256; ++i) { 398 | palette.push({ 399 | binColor: i, 400 | score: 0, 401 | }); 402 | } 403 | 404 | let scorePalette = (pixel, r, g, b) => { 405 | let matches = {}; 406 | let best = 120; 407 | let bestPaletteColor = 0; 408 | for (let i = 0; i < 256; ++i) { 409 | let toMatch = ACNL.paletteBinToHex[i]; 410 | if (toMatch === undefined) continue; 411 | let x = parseInt(toMatch.substr(1, 2), 16); 412 | let y = parseInt(toMatch.substr(3, 2), 16); 413 | let z = parseInt(toMatch.substr(5, 2), 16); 414 | let matchDegree = Math.abs(x - r) + Math.abs(y - g) + Math.abs(z - b); 415 | if (matchDegree < best) { 416 | best = matchDegree; 417 | bestPaletteColor = i; 418 | } 419 | if (matchDegree < 120) { 420 | matches[i.toString()] = matchDegree; 421 | } 422 | } 423 | palette[bestPaletteColor].score++; 424 | prepixels[pixel] = matches; 425 | } 426 | 427 | for (let i = 0; i < 4096; i += 4) { 428 | scorePalette( 429 | i / 4, 430 | imgData.data[i], 431 | imgData.data[i + 1], 432 | imgData.data[i + 2] 433 | ); 434 | } 435 | 436 | // sort by to decreasing score 437 | palette.sort((a, b) => { 438 | if (a.score > b.score) return -1; 439 | if (a.score < b.score) return 1; 440 | return 0; 441 | }); 442 | 443 | palette = palette.slice(0, 40); 444 | let bestBinColors = []; 445 | let bestScore = 0x200000; // can always do better than this 446 | 447 | // not using alert here, prevent alert from blocking thread 448 | console.log("optimizing color palette..."); 449 | for (let i = 0; i < 4000 && palette.length > 16; ++i) { 450 | let chosenBinColors = []; 451 | 452 | // pick random colors out of top 40 453 | while (chosenBinColors.length < 15 && chosenBinColors < palette.length) { 454 | let next = palette[Math.floor(Math.random() * palette.length)].binColor; 455 | if (chosenBinColors.includes(next)) continue; 456 | chosenBinColors.push(next); 457 | } 458 | 459 | // score random selection 460 | let currentScore = 0; 461 | 462 | // stop at first score that meets the criteria 463 | for (let pixel in prepixels) { 464 | let lowPixel = 750; 465 | for (let m in prepixels[pixel]) { 466 | if (!chosenBinColors.includes(parseInt(m))) continue; 467 | if (prepixels[pixel][m] < lowPixel) { 468 | lowPixel = prepixels[pixel][m]; 469 | } 470 | } 471 | currentScore += lowPixel; 472 | if (currentScore >= bestScore) break; 473 | } 474 | 475 | if (currentScore < bestScore) { 476 | bestScore = currentScore; 477 | bestBinColors = chosenBinColors; 478 | } 479 | } 480 | 481 | for (let i = 0; i < 15 && i < bestBinColors.length; ++i) { 482 | acnl.setSwatchColor(i, bestBinColors[i]); 483 | } 484 | } 485 | 486 | usePaletteGrey(acnl) { 487 | for (let i = 0; i < 15; i++) { 488 | acnl.setSwatchColor(i, 0x10 * i + 0xF); 489 | } 490 | } 491 | 492 | usePaletteSepia(acnl) { 493 | for (let i = 0; i < 9; i++) { 494 | acnl.setSwatchColor(i, 0x30 + i); 495 | } 496 | for (let i = 9; i < 15; i++) { 497 | acnl.setSwatchColor(i, 0x60 + i - 6); 498 | } 499 | } 500 | 501 | 502 | // AKA previously "recolorize" 503 | // draw image onto pattern based on palette 504 | drawImage(acnl, imgData) { 505 | // for a given rgb color, find the closest matching color in the swatch 506 | let matchedSwatchColor = (r, g, b) => { 507 | let best = 255 * 3; 508 | let bestSwatchColor = 0; 509 | for (let i = 0; i < 15; ++i) { 510 | let swatchColor = acnl.getSwatchColor(i); 511 | let toMatch = ACNL.paletteBinToHex[swatchColor]; 512 | let x = parseInt(toMatch.substr(1, 2), 16); 513 | let y = parseInt(toMatch.substr(3, 2), 16); 514 | let z = parseInt(toMatch.substr(5, 2), 16); 515 | let matchDegree = Math.abs(x - r) + Math.abs(y - g) + Math.abs(z - b); 516 | if (matchDegree < best) { 517 | best = matchDegree; 518 | bestSwatchColor = i; 519 | } 520 | } 521 | return bestSwatchColor; 522 | } 523 | 524 | for (let i = 0; i < 4096; i += 4) { 525 | let x = Math.floor(i / 4) % 32; 526 | let y = Math.floor(Math.floor(i / 4) / 32); 527 | acnl.colorPixel(x, y, matchedSwatchColor( 528 | imgData.data[i], 529 | imgData.data[i + 1], 530 | imgData.data[i + 2] 531 | )); 532 | } 533 | } 534 | /* CONVERT HELPER END */ 535 | 536 | shouldComponentUpdate(nextProps, nextState) { 537 | // only render after refreshing pixels 538 | if (nextState.pixelBuffer.length === 0) return true; 539 | else return false; 540 | } 541 | 542 | render() { 543 | let acnl = this.state.acnl; 544 | let chosenColor = this.state.chosenColor; 545 | let chosenTool = this.state.chosenTool; 546 | let isDrawing = this.state.isDrawing; 547 | let canvases = this.state.canvases; 548 | let canvasSizes = [64, 128, 512]; 549 | // perform actualZoom calculations 550 | let actualZooms = canvasSizes.map((size) => { 551 | if (acnl.isProPattern()) return size / 64; 552 | else return size / 32; 553 | }); 554 | let shouldQrCodeUpdate = this.state.shouldQrCodeUpdate; 555 | 556 | 557 | return ( 558 |
559 |
560 | 574 | 575 | 589 | 590 |
591 | 592 | 606 | 607 |
608 | 612 | 613 | 618 |
619 | 620 | 621 | 634 | 635 | 639 | 640 | 645 |
646 | ); 647 | } 648 | } 649 | 650 | export default Editor; 651 | -------------------------------------------------------------------------------- /src/EditorCanvas.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ACNL from './acnl.js'; 3 | 4 | // this component will attempt to supress updates to minimize full re-renders 5 | class EditorCanvas extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.canvas = React.createRef(); 9 | 10 | // not using state for these since these are technically static 11 | // to be updated as the DOM model updates/moves 12 | // cannot afford to be asynchronous, since these need to always be current 13 | // e.g. boundingClientRect or context 14 | // caching rect to prevent reflows and save cpu 15 | // also cache context for speed on full-redraws 16 | this.boundingClientRect = null; 17 | this.context = null; 18 | } 19 | 20 | updateContext() { 21 | let context = this.canvas.current.getContext("2d"); 22 | this.context = context; 23 | } 24 | 25 | updateBoundingClientRect(){ 26 | let boundingClientRect = this.canvas.current.getBoundingClientRect(); 27 | this.boundingClientRect = boundingClientRect; 28 | } 29 | 30 | draw(event) { 31 | let actualZoom = this.props.actualZoom; 32 | let boundingClientRect = this.boundingClientRect; 33 | let x = event.pageX - boundingClientRect.left - window.scrollX; 34 | let y = event.pageY - boundingClientRect.top - window.scrollY; 35 | 36 | x = Math.floor(x / actualZoom); 37 | y = Math.floor(y / actualZoom); 38 | 39 | // browser will attempt to dump mousemove event before it completes 40 | // if handler is not fast enough, need to ensure speed, using buffers 41 | // updatePixelBuffer will command all canvases to draw the pixels too 42 | // console.log(x, y); 43 | let pattern = this.props.pattern; 44 | let chosenTool = this.props.chosenTool; 45 | this.props.updatePixelBuffer(chosenTool.transform(x, y, pattern)); 46 | } 47 | 48 | 49 | // occurs as the last event in a click-n-drag, if it completes 50 | // refresh, and kill timers to force refresh 51 | onClick(event) { 52 | // console.log("mouse click"); 53 | this.draw(event); 54 | } 55 | 56 | onMouseDown(event) { 57 | this.props.setIsDrawing(true); 58 | // console.log("started drawing"); 59 | } 60 | 61 | onMouseMove(event) { 62 | let isDrawing = this.props.isDrawing; 63 | if (isDrawing && event.buttons === 1) { 64 | this.draw(event); 65 | } 66 | } 67 | 68 | // WILL NOT TRIGGER IF MOUSEUP OUTSIDE OF CANVAS ELEMENTS 69 | // timer exists in the editor to force refresh 70 | // mouseup triggers before mouseclick 71 | onMouseUp(event) { 72 | // console.log("mouse up"); 73 | this.props.setIsDrawing(false); 74 | } 75 | 76 | drawPatterns() { 77 | // adjust zoom factor for pattern size 78 | let patterns = this.props.patterns; 79 | for (let i = 0; i < patterns.length; ++i) { 80 | let pixelPair = patterns.charCodeAt(i); 81 | // get pixel binColors 82 | let firstColor = pixelPair & 0x0F; 83 | let secondColor = pixelPair >> 4; 84 | this.drawOffset(i * 2, firstColor); 85 | this.drawOffset(i * 2 + 1, secondColor); 86 | } 87 | } 88 | 89 | drawOffset(offset, chosenColor) { 90 | let x = (offset % 32); 91 | let y = Math.floor(offset / 32); 92 | this.drawPixel(x, y, chosenColor); 93 | } 94 | 95 | drawPixel(x, y, chosenColor) { 96 | let context = this.context; 97 | let zoom = this.props.actualZoom; 98 | 99 | if (y > 63) { 100 | y-= 64; x+= 32; 101 | } 102 | 103 | context.fillStyle = ACNL.paletteBinToHex[this.props.swatch[chosenColor]]; 104 | context.fillRect(x * zoom, y * zoom, zoom, zoom); 105 | // draw the grid lines if zoom is large enough 106 | if (zoom > 5) { 107 | context.fillStyle = "#AAAAAA"; 108 | context.fillRect(x * zoom + zoom - 1, y * zoom, 1, zoom); 109 | context.fillRect(x * zoom, y * zoom + zoom - 1, zoom, 1); 110 | } 111 | } 112 | 113 | // can't call draw inside render b/c on the first render 114 | // there's no reference to the actual node 115 | // create reference 116 | componentDidMount() { 117 | this.updateContext(); 118 | this.updateBoundingClientRect(); 119 | this.drawPatterns(); 120 | // attaching event handlers 121 | window.addEventListener("scroll", this.updateBoundingClientRect.bind(this)); 122 | window.addEventListener("resize", this.updateBoundingClientRect.bind(this)); 123 | } 124 | 125 | // only fully re-render when pattern is updated or swatch colors change 126 | // only occurs after editor applies refresh changes 127 | shouldComponentUpdate(nextProps, nextState) { 128 | if (this.props.patterns !== nextProps.patterns) return true; 129 | 130 | // manually check swatch b/c of object instance comparison 131 | // if swatch has changed, colors have changed 132 | // doesn't work for some reason, throwing error for context 133 | for (let i = 0; i < 15; ++i) { 134 | if (this.props.swatch[i] !== nextProps.swatch[i]) return true; 135 | } 136 | 137 | return false; 138 | } 139 | 140 | componentDidUpdate(prevProps, prevState, snapshot) { 141 | // upon re-rendering, update the context and bounds since technically new 142 | // canvas, redraw patterns too 143 | this.updateContext(); 144 | this.updateBoundingClientRect(); 145 | this.drawPatterns(); 146 | } 147 | 148 | componentWillUnmount() { 149 | window.removeEventListener("scroll", this.updateBoundingClientRect.bind(this)); 150 | window.removeEventListener("resize", this.updateBoundingClientRect.bind(this)); 151 | } 152 | 153 | render() { 154 | // console.log("rendered canvas"); 155 | let size = this.props.size; 156 | let canvasNumber = this.props.canvasNumber; 157 | 158 | let className = "canvas"; 159 | if (canvasNumber === 0) void(0); 160 | else if (canvasNumber === 1) className += "-zoom"; 161 | else if (canvasNumber === 2) className += "-zoomier"; 162 | 163 | return ( 164 | 174 | 175 | ); 176 | } 177 | } 178 | 179 | export default EditorCanvas; 180 | -------------------------------------------------------------------------------- /src/EditorImporter.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import qrcode from "./jsqrcode.js"; 3 | 4 | // handles all imports for the tool 5 | // will do both qr detection and image conversion 6 | class EditorImporter extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | // reads qr code from image or loads .acnl file 10 | this.loader = React.createRef(); 11 | // converts image to qr code 12 | this.converter = React.createRef(); 13 | this.converterSetting = React.createRef(); 14 | 15 | this.state = { 16 | loaderPart: 1, 17 | loaderData : "", 18 | } 19 | } 20 | 21 | onLoad() { 22 | let fileReader = new FileReader(); 23 | // image of some kind (find the qr code in the image) 24 | if (/image./.test(this.loader.current.files[0].type)) { 25 | fileReader.onload = (event) => { 26 | qrcode.callback = (qrData) => { 27 | if (qrData.length < 0x21C) { 28 | window.alert(`Could not recognize QR code.\nQR Code too short: ${qrData.length}`); 29 | } 30 | 31 | // part of a pro pattern 32 | else if (qrData.length === 0x21C) { 33 | let loaderPart = this.state.loaderPart; 34 | let loaderData = this.state.loaderData.slice(); 35 | loaderData += qrData; 36 | 37 | // last one, load it 38 | if (loaderPart === 4) { 39 | this.props.import(loaderData); 40 | 41 | // technically don't even need this since import will cause 42 | // entire Editor to rerender 43 | this.setState({ 44 | loaderPart: 1, 45 | loaderData: "", 46 | }); 47 | } 48 | 49 | else { 50 | loaderPart += 1; 51 | this.setState({ 52 | loaderPart: loaderPart, 53 | loaderData: loaderData, 54 | }); 55 | 56 | // can't trigger click for next upload 57 | // chrome is blocking the qr code 58 | // tell user about next qr code 59 | window.alert(`Please add in the next QR code ${loaderPart}/4`); 60 | } 61 | } 62 | 63 | // regular pattern 64 | else if (qrData.length === 0x26C) { 65 | this.props.import(qrData); 66 | } 67 | 68 | }; 69 | qrcode.decode(event.target.result); 70 | } 71 | fileReader.readAsDataURL(this.loader.current.files[0]); 72 | } 73 | 74 | // acnl file type (already checks for valid extension and file name) 75 | // doesn't interfere with qr loading if you're interleaving import methods 76 | else if (/.\.acnl$/.test(this.loader.current.files[0].name)) { 77 | fileReader.onload = (event) => { 78 | this.props.import(event.target.result); 79 | }; 80 | fileReader.readAsBinaryString(this.loader.current.files[0]); 81 | } 82 | 83 | // invalid file type 84 | else { 85 | window.alert("Chosen file was not valid."); 86 | // do nothing 87 | } 88 | 89 | // reset input so filename isn't logged into the input forever 90 | this.loader.current.value = ""; 91 | } 92 | 93 | onConvert() { 94 | // only if import is an image 95 | if (/image./.test(this.converter.current.files[0].type)) { 96 | let fileReader = new FileReader(); 97 | fileReader.onload = (event) => { 98 | let img = new Image(); 99 | img.onload = () => { 100 | // using canvas to convert 101 | let canvasEle = document.createElement('canvas'); 102 | canvasEle.width = 32; 103 | canvasEle.height = 32; 104 | 105 | let context = canvasEle.getContext("2d"); 106 | 107 | // canvas will automatically scale image down for us to desired pixel grid 108 | context.drawImage(img, 0, 0, 32, 32); 109 | 110 | let imgData = context.getImageData(0, 0, 32, 32); 111 | 112 | let convSet = this.converterSetting.current.value; 113 | 114 | // determine conversion method, pass it up 115 | this.props.convert(imgData, convSet); 116 | }; 117 | img.src = event.target.result; 118 | }; 119 | fileReader.readAsDataURL(this.converter.current.files[0]); 120 | } 121 | // reset input 122 | this.converter.current.value = ""; 123 | } 124 | 125 | shouldComponentUpdate() { 126 | // no need to update 127 | return false; 128 | } 129 | 130 | render() { 131 | return ( 132 |
133 |
134 | Load ACNL file or QR-image: 135 | 140 |
141 | 142 |
143 | Convert Image 144 | 150 | 156 |
157 |
158 | ); 159 | } 160 | } 161 | 162 | export default EditorImporter; 163 | -------------------------------------------------------------------------------- /src/EditorMetadata.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import EditorMetadataInput from './EditorMetadataInput.jsx'; 3 | 4 | class EditorMetadata extends React.Component { 5 | 6 | shouldComponentUpdate(nextProps, nextState) { 7 | if ( 8 | this.props.patternTitle !== nextProps.patternTitle || 9 | this.props.userName !== nextProps.userName || 10 | this.props.userID !== nextProps.userID || 11 | this.props.townName !== nextProps.townName || 12 | this.props.townID !== nextProps.townID 13 | ) return true; 14 | return false; 15 | } 16 | 17 | render() { 18 | return ( 19 |
20 |
21 | Title: 22 | 28 |
29 | 30 |
31 | Creator: 32 | 38 | 44 |
45 | 46 |
47 | Town: 48 | 54 | 60 |
61 |
62 | ); 63 | } 64 | } 65 | 66 | export default EditorMetadata; 67 | -------------------------------------------------------------------------------- /src/EditorMetadataInput.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class EditorMetadataInput extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.input = React.createRef(); 7 | } 8 | 9 | shouldComponentUpdate(nextProps, nextState) { 10 | if (this.props.data !== nextProps.data) return true; 11 | return false; 12 | } 13 | 14 | limitCharacters () { 15 | let limit; 16 | 17 | // hex field 18 | if (this.props.fieldType === "hex") { 19 | limit = 4; 20 | } 21 | // name field 22 | else { 23 | if (this.props.field === "pattern") limit = 20; 24 | else limit = 10; 25 | } 26 | 27 | if (this.input.current.value.length > limit) { 28 | this.input.current.value = this.input.current.value.substr(0, limit); 29 | } 30 | } 31 | 32 | onChange(event) { 33 | this.limitCharacters(); 34 | this.props.onUpdate(this.input.current.value); 35 | } 36 | 37 | render() { 38 | let data = this.props.data; 39 | let className = "metadata-input"; 40 | if (this.props.fieldType === "hex") className += " id"; 41 | else { 42 | if (this.props.field === "pattern") className += " title"; 43 | else className += " name"; 44 | } 45 | 46 | return ( 47 | // onChange triggers onInput in React, 48 | // React uses synthetic wrapper events 49 | 56 | ); 57 | } 58 | } 59 | 60 | export default EditorMetadataInput; 61 | -------------------------------------------------------------------------------- /src/EditorPalette.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ACNL from './acnl.js'; 3 | 4 | class EditorPaletteColor extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | // for binding events to underlying html elements 8 | this.editorPaletteColor = React.createRef(); 9 | } 10 | 11 | // event handlers do not have this context matched to the component instance 12 | // when using es6 classes 13 | onMouseMove(event) { 14 | // if holding down click and dragging, change color 15 | if (event.buttons === 1) { 16 | this.editorPaletteColor.current.click(); 17 | } 18 | } 19 | 20 | render() { 21 | let isPicked = this.props.isPicked; 22 | let className = "palette-color"; 23 | if (isPicked) className += " picked"; 24 | let backgroundColor = { backgroundColor : this.props.color }; 25 | let onClick = this.props.onClick; 26 | 27 | return ( 28 |
35 |
36 | ); 37 | } 38 | } 39 | 40 | 41 | class EditorPalette extends React.Component { 42 | // input (color) is a hex code used for mapping to ACNL file 43 | renderColor(binColor) { 44 | let isPicked = this.props.chosenBinColor === binColor; 45 | let color = ACNL.paletteBinToHex[binColor]; 46 | 47 | // automatically perforrm click with number 48 | return ( 49 | this.props.onClick(binColor)} 54 | /> 55 | ); 56 | } 57 | 58 | shouldComponentUpdate(nextProps, nextState) { 59 | if (this.props.chosenBinColor !== nextProps.chosenBinColor) return true; 60 | return false; 61 | } 62 | 63 | render() { 64 | // PROCEDURALLY GENERATING PALETTE 65 | let colorBlocks = []; 66 | for (let i = 0x00; i < 0xFF; i+=0x10) { 67 | let colors = []; 68 | for (let j = 0x00; j < 0x09; j+=0x01) { 69 | colors.push(this.renderColor(i + j)); 70 | } 71 | 72 | let colorBlock = ( 73 |
74 | {colors} 75 |
76 | ); 77 | 78 | colorBlocks.push(colorBlock); 79 | } 80 | 81 | // grey row 82 | let greyColors = []; 83 | for (let i = 0x0F; i < 0xFF; i += 0x10) { 84 | greyColors.push(this.renderColor(i)); 85 | } 86 | let greyColorBlock = ( 87 |
88 | {greyColors} 89 |
90 | ); 91 | colorBlocks.push(greyColorBlock); 92 | 93 | 94 | return ( 95 |
{colorBlocks}
96 | ); 97 | } 98 | } 99 | 100 | export default EditorPalette; 101 | -------------------------------------------------------------------------------- /src/EditorQrGenerator.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // qrencode library 3 | import qrencode from './qrcode.js'; 4 | 5 | class EditorQrCode extends React.Component { 6 | 7 | createQrCode(data, typeNumber, multipartNum, multipartTotal, 8 | multipartParity) { 9 | 10 | let qr = qrencode(typeNumber, "L", multipartNum, multipartTotal, multipartParity); 11 | qr.addData(data); 12 | qr.make(); 13 | return qr; 14 | } 15 | 16 | // only re-render if user has attempted to update that section of the qr code 17 | shouldComponentUpdate(nextProps, nextState) { 18 | if (this.props.data !== nextProps.data) return true; 19 | return false; 20 | } 21 | 22 | render() { 23 | // create QR code 24 | let data = this.props.data; 25 | let typeNumber = this.props.typeNumber; 26 | let multipartNum = this.props.multipartNum; 27 | let multipartTotal = this.props.multipartTotal; 28 | let multipartParity = this.props.multipartParity; 29 | 30 | let qr = this.createQrCode(data, typeNumber, multipartNum, multipartTotal, 31 | multipartParity); 32 | 33 | // console.log("rendered QR", multipartNum); 34 | return ( 35 |
38 |
39 | ); 40 | } 41 | } 42 | 43 | class EditorQrGenerator extends React.Component { 44 | // control update, only update qr code when told 45 | shouldComponentUpdate(nextProps, nextState) { 46 | return nextProps.shouldQrCodeUpdate; 47 | } 48 | 49 | render() { 50 | // console.log("rendered"); 51 | let data = this.props.data; 52 | let isProPattern = this.props.isProPattern; 53 | let qrCodes = []; 54 | 55 | if (isProPattern) { 56 | // split data into parts 57 | for (let i = 0; i < 4; ++i) { 58 | qrCodes.push( 59 | 67 | ); 68 | } 69 | } 70 | else { 71 | // use all of data 72 | qrCodes.push( 73 | 78 | ); 79 | } 80 | 81 | return ( 82 |
{qrCodes}
83 | ); 84 | } 85 | } 86 | 87 | export default EditorQrGenerator; 88 | -------------------------------------------------------------------------------- /src/EditorSwatch.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ACNL from './acnl.js' 3 | 4 | class EditorSwatchColor extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | 8 | // for binding events to underlying html elements 9 | this.editorSwatchColor = React.createRef(); 10 | } 11 | 12 | // event handlers do not have this context matched to the component instance 13 | // when using es6 classes 14 | onMouseMove(event) { 15 | // if holding down click and dragging, change color 16 | if (event.buttons === 1) { 17 | this.editorSwatchColor.current.click(); 18 | } 19 | } 20 | 21 | render() { 22 | let isPicked = this.props.isPicked; 23 | let className = "swatch-color"; 24 | if (isPicked) className += " picked"; 25 | let backgroundColor = { backgroundColor : this.props.color }; 26 | let onClick = this.props.onClick; 27 | 28 | return ( 29 |
36 |
37 | ); 38 | } 39 | } 40 | 41 | class EditorSwatch extends React.Component { 42 | renderColor(i) { 43 | let isPicked = (this.props.chosenColor === i) 44 | let binColor = this.props.swatch[i]; 45 | let color = ACNL.paletteBinToHex[binColor]; 46 | 47 | // automatically perform onclick with preset number 48 | return ( 49 | this.props.onClick(i)} 53 | /> 54 | ); 55 | } 56 | 57 | shouldComponentUpdate(nextProps, nextState) { 58 | if (this.props.chosenColor !== nextProps.chosenColor) return true; 59 | // check swatch 60 | for (let i = 0; i < 15; ++i) { 61 | if (this.props.swatch[i] !== nextProps.swatch[i]) return true; 62 | } 63 | 64 | return false; 65 | } 66 | 67 | render() { 68 | // console.log("rendered swatch"); 69 | return ( 70 |
71 |
72 | {this.renderColor(0)} 73 | {this.renderColor(1)} 74 | {this.renderColor(2)} 75 |
76 |
77 | {this.renderColor(3)} 78 | {this.renderColor(4)} 79 | {this.renderColor(5)} 80 |
81 |
82 | {this.renderColor(6)} 83 | {this.renderColor(7)} 84 | {this.renderColor(8)} 85 |
86 |
87 | {this.renderColor(9)} 88 | {this.renderColor(10)} 89 | {this.renderColor(11)} 90 |
91 |
92 | {this.renderColor(12)} 93 | {this.renderColor(13)} 94 | {this.renderColor(14)} 95 |
96 |
97 | ); 98 | } 99 | } 100 | 101 | 102 | export default EditorSwatch; 103 | -------------------------------------------------------------------------------- /src/EditorTools.js: -------------------------------------------------------------------------------- 1 | // this class represents the template for drawing tools 2 | class EditorDrawingTool { 3 | /** 4 | * takes in the clicked pixel coordinate and returns a list of other 5 | * pixel coordinates to update pixelBuffer with 6 | * @param {Number} x the x-coordinate of clicked pixel 7 | * @param {Number} y the y-coordinate of clicked pixel 8 | * @param {Number} pattern the pattern data 9 | * @return {Array(pixels...[x, y])} the list of pixels to color in with the chosen color 10 | */ 11 | transform(x, y, pattern) { 12 | // pseudo-virtual function 13 | window.alert( 14 | this.constructor.name 15 | + ".transform(x, y, pattern)" 16 | + " has not been implemented." 17 | ); 18 | } 19 | 20 | /** 21 | * checks pixelBuffer and determines whether or not pixelBuffer should be 22 | * updated with list of pixels passed by transform(x, y, pattern), used for 23 | * optimization to reduce (not prevent) addition of duplicate pixels; 24 | * can modify pixelsToAdd, but do not modify pixelBuffer 25 | * @param {Array} pixelsToAdd pixel coordinates to add to pixelBuffer 26 | * @param {Array} pixelBuffer contains pixel coordinates [x,y] to update file 27 | * @return {Boolean} true if you plan on adding pixels to transform 28 | */ 29 | willUpdatePixelBuffer(pixelsToAdd, pixelBuffer) { 30 | // by default it returns true if you choose not to override 31 | return true; 32 | } 33 | } 34 | 35 | // prepackaged with tool, example of implementation 36 | export class Pen extends EditorDrawingTool { 37 | transform(x, y, pattern) { 38 | return ([[x, y]]); 39 | } 40 | 41 | willUpdatePixelBuffer(pixelsToAdd, pixelBuffer) { 42 | let x = pixelsToAdd[0][0]; 43 | let y = pixelsToAdd[0][1]; 44 | let pixelBufferLastIndex = pixelBuffer.length - 1; 45 | // if last pixel in buffer doesn't match pixel we're adding 46 | // we're updating, prevent duplicate of last 47 | if ( 48 | pixelBufferLastIndex < 0 || 49 | x !== pixelBuffer[pixelBufferLastIndex][0] || 50 | y !== pixelBuffer[pixelBufferLastIndex][1] 51 | ) return true; 52 | // found a match in the last index 53 | return false; 54 | } 55 | } 56 | 57 | // add your other tools below, remember to EXPORT the class like the pen -------------------------------------------------------------------------------- /src/acnl.js: -------------------------------------------------------------------------------- 1 | //ACNL data layout (identical to binary QR code contents): 2 | //.ACNL file type for importing 3 | // 4 | //0x 00 - 0x 29 ( 42) = Pattern Title 5 | // 0x28 0x29 are null terminators 6 | 7 | //0x 2A - 0x 2B ( 2) = User ID // huhhh??? 8 | //0x 2C - 0x 3F ( 20) = User Name 9 | //0x 40 - 0x 41 ( 2) = Town ID // what is this even for lmao 10 | //0x 42 - 0x 55 ( 20) = Town Name 11 | //0x 56 - 0x 57 ( 2) = Unknown (values are usually random - changing seems to have no effect) 12 | //0x 58 - 0x 66 ( 15) = Color code indexes (COLOR SWATCH) 13 | //0x 67 ( 1) = Unknown (value is usually random - changing seems to have no effect) 14 | //0x 68 ( 1) = Ten? (seems to always be 0x0A) 15 | //0x 69 ( 1) = Pattern type (normal patterns: 0x09, dresses: 0x00, photo boards: 0x08) 16 | //0x 6A - 0x 6B ( 2) = Zero? (seems to always be 0x0000) 17 | //0x 6C - 0x26B (512) = Pattern Data 1 (mandatory) 18 | //0x26C - 0x46B (512) = Pattern Data 2 (optional) 19 | //0x46C - 0x66B (512) = Pattern Data 3 (optional) 20 | //0x66C - 0x86B (512) = Pattern Data 4 (optional) 21 | //0x86C - 0x86F ( 4) = Zero padding (optional) 22 | 23 | import * as FileSaver from 'file-saver'; 24 | 25 | class ACNL { 26 | constructor(data) { 27 | if ( 28 | arguments.length === 1 && 29 | (data.length === 0x26C || 30 | data.length === 0x870) 31 | ) { 32 | this.data = data; 33 | } else { 34 | // set default data 35 | // initial data is only 620 in length 36 | // only contains 1 pattern 37 | this.data = window.atob("RQBtAHAAdAB5AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABVAG4AawBuAG8AdwBuAAAAAAAAAAAAVQBuAGsAbgBvAHcAbgAAAAAAAABeCw8fLz9PX29/j5+vv8/f73YKCQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="); 38 | 39 | // alternate default for testing 40 | // this.data = window.atob("QgBvAGwAZAAgAGEAbgBkACAAQgByAGEAcwBoAAAAAAAAAAAAAAAAAAAAYPVKAGEAawBlAAAAAAAAAAAAAAAAAC2xTABlAHcAZQBzAAAAAAAAAAAAAQAxCvBxdCEPI9KDJKA/EqOh78wKCQAAVVVVUiISVRFVVRERERUREVVVUkVVFRUR7u7uHlVVFRFVVVVUJVIi7t3dye5VJVURVVJVVVIh4t7Q3d3sXlUiEVVVVVJUId4AoN3dzV4iVUFVVRRVFeEOoN3d3d3sIhFBVVRRRVHuAODunt3d7CUVQVVVREThDgruG56d3ewlEURVURQU7tDdvhve2c3uJRFEVUQR7trd3b7h3p3MXiIRRFVB4dDd7t7t7t3d7lUlEURVEQ7Q7VXl3d3d3V5VJRFEheHarR5VVd7d3d3tVSIVRFXh3e0REVXu3d3N7FUiEUQR4d3tERHl7t7d7cxeUhFEVeHdHhFU5d3e3ezNXhIRRFVB7hVBVOXd3t3s3l4VEURVEUQREVXl3d7N3c5eFRFBVRURVVVV5d3e3czOXlVRQVVVFVVV5e7d3t3Nzu5eVUFVVVEVVe5n3d7d3M7c7oVBVVWBUoVu1u3e3d3OzOyIQVWCWFiC5e7u3d3d7czsWEFViCWFUlXV7t3d3e3ujhURVVVRhVJV5d7t7t3tVYUVESVSVVVV5e7t7lXu7R4RERFVFVVVEe7d5hUR5c3uEVIRVBVVVeHefV4VUeXe7F5YUYRVVVXh3XYeERVV3szuVVW1iFiF4W3nFRFVVe7N7FVVu4uIVeVt7hJVWFXo3exVVbu7u0tV7i4iVVVVVe6OVVU="); 41 | 42 | // default is pro (for testing) 43 | // this.data = window.atob("UwBhAHQAdQByAG4AIABIAG8AbwBkAGkAZQAAAAAAAAAAAAAAAAAAAAAA4PpNAG8AbwAtAE0AbwBvAAAAAAABAEbjTQBvAG8AIABMAGUAYQBmAAAAAQAxEd/f1Z+vjzIzz3+fD+9ff8sKAwAA3d3d3d2tZmd2Ztrd3d3d3d3d3d3d3WR2Z0bd3d3d3d2qqqqZmd1NdmfU3Z2ZmaqampmqqqqZEbu7EZmZmaqqqtqdmZmpmrG7uxupqqqqmZna3d3dnZq5mZmbqZmZmdnd2t3d3d3dvdmd293d3d3drdrd3d3d3b3ZnduIiNjd3a3a3d3d3d292Z2L3Yjd3d2t2t3d3d3dvdmd242I2N3drdrd3d3d3b3ZnduI3djd3a3a3d3d3d292Z2L2I3Y3d2t2t3d3d3dvdmdi4iI3d3drdrd3d3d3b3Zndvd3d3d3a3a3d3d3d292Z3b3d3d3d2t2t3d3d3d3d3d3d3d3d3drdrd3d3d3d3d3d3d3d3d3a3a3d3d3d3d3d3d3d3d3d2t2t3d3d3d3d3d3d3d3d3draqqqqqqqqqqqqqqqqqqqqqZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmaqqqqqqqqqqqqqqqqqqqqp3d3d3d3d3d3d3d3d3d3d3ZmZmZmZmZmZmZmZmZmZmZjMzMzMzMzMzMzMzMzMzMzNTMjNSUlJSM1IzUlJSM1MyJSUjJSUlJTU1IyUlJTUlJVMyM1JSUlIzMjNSUlIzUzIzMzMzMzMzMyUzMzMzMzMzUlJSUlJSUjJSU1JSUlJSUiUlJSUlJSUlMyMlJSUlJSXdqqqqqqqqqqqqqqqqqqrd3d2ZmZmqqqqqqpqZmZnd3ZnZ3d3dmamqqqqa3d3dTUSqqtrd3Z3d3d3d3d3d3aqq2p2q2t3d3d3d3d3dqqqaqdrdnand3d3d3d3d3ZqZmZna3d2dqqqq3d3dqqqZ3d2t2t3d3d2ZqaqqqpSZ3d3drdrd3d3d3ZmZmZnZ3d3d3a3a3d3d3d2NiIiI3d3d3d2t2t3d3d3d2N3d3djd3d3drdrd3d3djd3d3d2N3d3d3a3a3d3d3Yjd3d3djdjd3d2t2t3d3YjY3d3d3d2I2N3drdrd3Y3d2N3d3d3d2I3d3a3a3d3diIiIiIiIiIjY3d2t2t3d3d2N3d3d3Y3d3d3drdrd3d3d3djd3d3Y3d3d3a3a3d3d3d2NiIiI3d3d3d2tqqqqqqqqqqqqqqqqqqqqqpSZmZmZmZmZmZmZmZmZmZmUmZmZmZmZmZmZmZmZmZmZqqqqqqqqqqqqqqqqqqqqqnd3d3d3d3d3d3d3d3d3d3dmZmZmZmZmZmZmZmZmZmZmMzMzMzMzMzMzMzMzMzMzM1MyM1JSUlIyM1JSUlIzUzIlJSMlJSUlJSMlJSUlNSUlUzIzUlJSUjIzUlJSjYjdiKjZ3ama3d2p2d3d3Y2I3YiomZ2pzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMwAAAAAjYjdiKjZ3ama3d2p2d3d3Y2I3YiomZ2pzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMwAAAAA"); 44 | } 45 | } 46 | 47 | // need this for immutability in React 48 | clone() { 49 | return new ACNL(this.data); 50 | } 51 | 52 | checkDataAccess(offset) { 53 | if ( 54 | offset < 0 || 55 | offset >= this.data.length 56 | ) throw new Error("attempted to access invalid address of data"); 57 | } 58 | 59 | getByte(offset) { 60 | this.checkDataAccess(offset); 61 | return this.data.charCodeAt(offset); 62 | } 63 | 64 | setByte(offset, val) { 65 | this.checkDataAccess(offset); 66 | this.data = 67 | this.data.substr(0, offset) + 68 | String.fromCharCode(val) + 69 | this.data.substr(offset + 1); 70 | } 71 | 72 | // allows only for reading of utf-16 portions of data 73 | from_utf16(offset) { 74 | this.checkDataAccess(offset); 75 | 76 | let len; 77 | switch(offset) { 78 | // pattern title 79 | case 0x00: len = 40; break; 80 | // creator name 81 | case 0x2c: len = 20; break; 82 | // town name 83 | case 0x42: len = 20; break; 84 | default: throw new Error("not valid utf-16 data"); 85 | } 86 | 87 | let tmp = ""; 88 | for (let i = offset; i < offset + len; i += 2){ 89 | // reconstruct the utf16 byte 90 | // note utf-16 format is big endian (most significant byte stored first) 91 | // but, in storage we use little endian (most significant byte last) 92 | // therefore we need to grab the right byte first 93 | let char = (this.data.charCodeAt(i+1) << 8) + this.data.charCodeAt(i); 94 | if (char === 0){return tmp;} 95 | tmp += String.fromCharCode(char); 96 | } 97 | return tmp; 98 | } 99 | 100 | // allows only for writing to utf-16 portions of data 101 | to_utf16(offset, str) { 102 | let len; 103 | switch(offset) { 104 | // pattern title 105 | case 0x00: len = 20; break; 106 | // creator name 107 | case 0x2C: len = 10; break; 108 | // town name 109 | case 0x42: len = 10; break; 110 | default: throw new Error("not valid utf-16 data location"); 111 | } 112 | 113 | for (let i = 0; i < len; ++i){ 114 | if (i >= str.length){ 115 | this.setByte(offset + i*2, 0); 116 | this.setByte(offset + i*2+1, 0); 117 | } else{ 118 | this.setByte(offset + i*2, str.charCodeAt(i) & 0xFF); 119 | this.setByte(offset + i*2+1, (str.charCodeAt(i) >> 8) & 0xFF); 120 | } 121 | } 122 | } 123 | 124 | download() { 125 | try { 126 | let ab = new ArrayBuffer(this.data.length); 127 | let ia = new Uint8Array(ab); 128 | for (let i = 0; i < this.data.length; i++) { 129 | ia[i] = this.data.charCodeAt(i); 130 | } 131 | let blob = new Blob([ia], {"type": "application/octet-stream"}); 132 | 133 | FileSaver.saveAs(blob, this.from_utf16(0x00)+".acnl"); 134 | } 135 | catch { 136 | alert("Failed to save file. Try using a different browser."); 137 | } 138 | } 139 | 140 | getID(offset) { 141 | switch(offset) { 142 | // user id 143 | case 0x2A: break; 144 | // town id 145 | case 0x40: break; 146 | default: throw new Error("data address specified is not an id"); 147 | } 148 | 149 | let left = this.getByte(offset); 150 | let right = this.getByte(offset + 1); 151 | let val = (left << 8) + right; 152 | val = val.toString(16); 153 | return val; 154 | } 155 | 156 | setID(offset, str) { 157 | // verify input 158 | // str might be empty, check that too 159 | if (str === "" || str === '') str = "0x0000"; 160 | 161 | // verify offset 162 | switch(offset) { 163 | // user id 164 | case 0x2A: break; 165 | // town id 166 | case 0x40: break; 167 | default: throw new Error("data address specified is not an id"); 168 | } 169 | 170 | let num = parseInt(str, 16); 171 | // check if num is larger than 16 bits and valid 172 | if (isNaN(num) || num >= 65536) { 173 | // get the smallest substr possible 174 | for (let i = str.length; i >= 1; --i) { 175 | num = parseInt(str.substr(0, i), 16); 176 | if (!isNaN(num) && num < 65536) { 177 | break; 178 | } 179 | // didn't pass check, last character 180 | else if (i === 1) { 181 | throw new Error("input is not a valid id", str); 182 | } 183 | } 184 | } 185 | 186 | // store first byte's, then second 187 | // & with 2 bytes of 1's in binary 188 | // bit shifting to remove extra bits and pad with 0's in binary 189 | // AND operator 0xFF leaves only the last 8 bits 190 | // console.log("setting", num.toString(16)); 191 | this.setByte(offset, (num >> 8) & 0xFF); 192 | this.setByte(offset + 1, num & 0xFF); 193 | } 194 | 195 | 196 | // turns a pro pattern into a regular pattern 197 | toStandardPattern() { 198 | if (this.data.length > 0x26C) { 199 | this.data = this.data.substr(0, 0x26C); 200 | } 201 | } 202 | 203 | // turns regular pattern into a pro pattern 204 | toProPattern() { 205 | if (this.data.length < 0x870) { 206 | // note repeat is the only way to add trash values 207 | this.data = this.data 208 | + String.fromCharCode(0).repeat(0x870 - this.data.length); 209 | } 210 | } 211 | 212 | isProPattern() { 213 | return this.data.length === 0x870; 214 | } 215 | 216 | colorPixel(x, y, chosenColor) { 217 | if (chosenColor < 0 || chosenColor > 15) { 218 | throw new Error("invalid chosen color"); 219 | } 220 | 221 | // check if x, y is available coordinate 222 | // check for 64 vs 32 bit patterns 223 | if ( 224 | isNaN(x) || 225 | isNaN(y) || 226 | x < 0 || 227 | y < 0 || 228 | x > 63 || 229 | y > 63 230 | ) return false; 231 | 232 | if ( 233 | this.data.length !== 0x870 && 234 | (x > 31 || y > 31) 235 | ) return false; 236 | 237 | // each "pixel" in the pattern is only half a byte (colors are 0-14) 238 | // since each pattern is 32 x 32, we need only 16 bytes in width 239 | // to represent a row of the pattern, y * 16 allows us to skip rows 240 | // x assumes that you can get pixels from 0 -> 64 in this situation 241 | // to get column, we need to x/2 242 | 243 | // reminder that this is a port, will have to refactor this to only color 244 | // pixels in specific patterns in the future 245 | 246 | // determine pattern quadrant 247 | 248 | let patternNum; 249 | // top left -> pattern 1 250 | if (x <= 31 && y <= 31) patternNum = 0; 251 | // bottom left -> pattern 2 252 | else if (x <= 31 && y <= 63) patternNum = 1; 253 | // top right -> pattern 3 254 | else if (x <= 63 && y <= 31) patternNum = 2; 255 | // bottom right -> pattern 4 256 | else if (x <= 63 && y <= 63) patternNum = 3; 257 | 258 | let offset = 0x6C + Math.floor(x % 32 / 2) + (y % 32) * 16; 259 | // correct offset based on quadrant 260 | offset += (patternNum * 512); 261 | // console.log(offset.toString(16)); 262 | 263 | // need to make sure we don't override other pixels 264 | let val = this.data.charCodeAt(offset) & 0xFF; 265 | let oldval = val; 266 | if ((x % 2) === 1) { 267 | // keep last half, replace first half with chosen color 268 | val = (val & 0x0F) + (chosenColor << 4); 269 | } else { 270 | // keep first half, replace second half with chosen color 271 | val = (val & 0xF0) + chosenColor; 272 | } 273 | 274 | if (val === oldval) { 275 | return false; 276 | } 277 | 278 | this.setByte(offset, val); 279 | return true; 280 | } 281 | 282 | get patterns() { 283 | return this.data.substr(0x6C, 2048); 284 | } 285 | 286 | get swatch() { 287 | let binColorsStr = this.data.substr(0x58, 15).split(""); 288 | return binColorsStr.map((char) => { 289 | return char.charCodeAt(0); 290 | }); 291 | } 292 | 293 | setSwatchColor(chosenColor, newBinColor) { 294 | if (chosenColor < 0 || chosenColor > 15) throw new Error("invalid chosen color"); 295 | if (ACNL.paletteBinToHex[newBinColor] === undefined) { 296 | throw new Error("new color is invalid"); 297 | } 298 | this.setByte(0x58 + chosenColor, newBinColor); 299 | } 300 | 301 | // this one returns the binary color 302 | getSwatchColor(chosenColor) { 303 | if (chosenColor < 0 || chosenColor > 15) throw new Error("invalid chosen color"); 304 | return this.getByte(0x58 + chosenColor); 305 | } 306 | 307 | get patternTitle() { 308 | return this.from_utf16(0x00); 309 | } 310 | 311 | set patternTitle(str) { 312 | this.to_utf16(0x00, str); 313 | } 314 | 315 | get userName() { 316 | return this.from_utf16(0x2C); 317 | } 318 | 319 | set userName(str) { 320 | this.to_utf16(0x2C ,str); 321 | } 322 | 323 | get userID() { 324 | return this.getID(0x2A); 325 | } 326 | 327 | set userID(str) { 328 | this.setID(0x2A, str); 329 | } 330 | 331 | get townName() { 332 | return this.from_utf16(0x42); 333 | } 334 | 335 | set townName(str) { 336 | this.to_utf16(0x42, str); 337 | } 338 | 339 | get townID() { 340 | return this.getID(0x40); 341 | } 342 | 343 | set townID(str) { 344 | return this.setID(0x40, str); 345 | } 346 | 347 | 348 | static get paletteBinToHex() { 349 | return { 350 | //pinks 351 | 0x00: "#FFEFFF", 352 | 0x01: "#FF9AAD", 353 | 0x02: "#EF559C", 354 | 0x03: "#FF65AD", 355 | 0x04: "#FF0063", 356 | 0x05: "#BD4573", 357 | 0x06: "#CE0052", 358 | 0x07: "#9C0031", 359 | 0x08: "#522031", 360 | 361 | //reds 362 | 0x10: "#FFBACE", 363 | 0x11: "#FF7573", 364 | 0x12: "#DE3010", 365 | 0x13: "#FF5542", 366 | 0x14: "#FF0000", 367 | 0x15: "#CE6563", 368 | 0x16: "#BD4542", 369 | 0x17: "#BD0000", 370 | 0x18: "#8C2021", 371 | 372 | //oranges 373 | 0x20: "#DECFBD", 374 | 0x21: "#FFCF63", 375 | 0x22: "#DE6521", 376 | 0x23: "#FFAA21", 377 | 0x24: "#FF6500", 378 | 0x25: "#BD8A52", 379 | 0x26: "#DE4500", 380 | 0x27: "#BD4500", 381 | 0x28: "#633010", 382 | 383 | //pastels or something, I guess? 384 | 0x30: "#FFEFDE", 385 | 0x31: "#FFDFCE", 386 | 0x32: "#FFCFAD", 387 | 0x33: "#FFBA8C", 388 | 0x34: "#FFAA8C", 389 | 0x35: "#DE8A63", 390 | 0x36: "#BD6542", 391 | 0x37: "#9C5531", 392 | 0x38: "#8C4521", 393 | 394 | //purple 395 | 0x40: "#FFCFFF", 396 | 0x41: "#EF8AFF", 397 | 0x42: "#CE65DE", 398 | 0x43: "#BD8ACE", 399 | 0x44: "#CE00FF", 400 | 0x45: "#9C659C", 401 | 0x46: "#8C00AD", 402 | 0x47: "#520073", 403 | 0x48: "#310042", 404 | 405 | // more pink 406 | 0x50: "#FFBAFF", 407 | 0x51: "#FF9AFF", 408 | 0x52: "#DE20BD", 409 | 0x53: "#FF55EF", 410 | 0x54: "#FF00CE", 411 | 0x55: "#8C5573", 412 | 0x56: "#BD009C", 413 | 0x57: "#8C0063", 414 | 0x58: "#520042", 415 | 416 | // brown 417 | 0x60: "#DEBA9C", 418 | 0x61: "#CEAA73", 419 | 0x62: "#734531", 420 | 0x63: "#AD7542", 421 | 0x64: "#9C3000", 422 | 0x65: "#733021", 423 | 0x66: "#522000", 424 | 0x67: "#311000", 425 | 0x68: "#211000", 426 | 427 | // yellow 428 | 0x70: "#FFFFCE", 429 | 0x71: "#FFFF73", 430 | 0x72: "#DEDF21", 431 | 0x73: "#FFFF00", 432 | 0x74: "#FFDF00", 433 | 0x75: "#CEAA00", 434 | 0x76: "#9C9A00", 435 | 0x77: "#8C7500", 436 | 0x78: "#525500", 437 | 438 | 439 | // blue 440 | 0x80: "#DEBAFF", 441 | 0x81: "#BD9AEF", 442 | 0x82: "#6330CE", 443 | 0x83: "#9C55FF", 444 | 0x84: "#6300FF", 445 | 0x85: "#52458C", 446 | 0x86: "#42009C", 447 | 0x87: "#210063", 448 | 0x88: "#211031", 449 | 450 | // ehm... also blue? 451 | 0x90: "#BDBAFF", 452 | 0x91: "#8C9AFF", 453 | 0x92: "#3130AD", 454 | 0x93: "#3155EF", 455 | 0x94: "#0000FF", 456 | 0x95: "#31308C", 457 | 0x96: "#0000AD", 458 | 0x97: "#101063", 459 | 0x98: "#000021", 460 | 461 | 462 | // green 463 | 0xA0: "#9CEFBD", 464 | 0xA1: "#63CF73", 465 | 0xA2: "#216510", 466 | 0xA3: "#42AA31", 467 | 0xA4: "#008A31", 468 | 0xA5: "#527552", 469 | 0xA6: "#215500", 470 | 0xA7: "#103021", 471 | 0xA8: "#002010", 472 | 473 | // icky greenish yellow 474 | 0xB0: "#DEFFBD", 475 | 0xB1: "#CEFF8C", 476 | 0xB2: "#8CAA52", 477 | 0xB3: "#ADDF8C", 478 | 0xB4: "#8CFF00", 479 | 0xB5: "#ADBA9C", 480 | 0xB6: "#63BA00", 481 | 0xB7: "#529A00", 482 | 0xB8: "#316500", 483 | 484 | // Wtf? More blue? 485 | 0xC0: "#BDDFFF", 486 | 0xC1: "#73CFFF", 487 | 0xC2: "#31559C", 488 | 0xC3: "#639AFF", 489 | 0xC4: "#1075FF", 490 | 0xC5: "#4275AD", 491 | 0xC6: "#214573", 492 | 0xC7: "#002073", 493 | 0xC8: "#001042", 494 | 495 | // gonna call this cyan 496 | 0xD0: "#ADFFFF", 497 | 0xD1: "#52FFFF", 498 | 0xD2: "#008ABD", 499 | 0xD3: "#52BACE", 500 | 0xD4: "#00CFFF", 501 | 0xD5: "#429AAD", 502 | 0xD6: "#00658C", 503 | 0xD7: "#004552", 504 | 0xD8: "#002031", 505 | 506 | // more cyan, because we didn't have enough blue-like colors yet 507 | 0xE0: "#CEFFEF", 508 | 0xE1: "#ADEFDE", 509 | 0xE2: "#31CFAD", 510 | 0xE3: "#52EFBD", 511 | 0xE4: "#00FFCE", 512 | 0xE5: "#73AAAD", 513 | 0xE6: "#00AA9C", 514 | 0xE7: "#008A73", 515 | 0xE8: "#004531", 516 | 517 | // also green. Fuck it, whatever. 518 | 0xF0: "#ADFFAD", 519 | 0xF1: "#73FF73", 520 | 0xF2: "#63DF42", 521 | 0xF3: "#00FF00", 522 | 0xF4: "#21DF21", 523 | 0xF5: "#52BA52", 524 | 0xF6: "#00BA00", 525 | 0xF7: "#008A00", 526 | 0xF8: "#214521", 527 | 528 | //greys 529 | 0x0F: "#FFFFFF", 530 | 0x1F: "#ECECEC", 531 | 0x2F: "#DADADA", 532 | 0x3F: "#C8C8C8", 533 | 0x4F: "#B6B6B6", 534 | 0x5F: "#A3A3A3", 535 | 0x6F: "#919191", 536 | 0x7F: "#7F7F7F", 537 | 0x8F: "#6D6D6D", 538 | 0x9F: "#5B5B5B", 539 | 0xAF: "#484848", 540 | 0xBF: "#363636", 541 | 0xCF: "#242424", 542 | 0xDF: "#121212", 543 | 0xEF: "#000000", 544 | }; 545 | } 546 | 547 | static get paletteHexToBin() { 548 | return { 549 | "#FFEFFF" : 0x00, 550 | "#FF9AAD" : 0x01, 551 | "#EF559C" : 0x02, 552 | "#FF65AD" : 0x03, 553 | "#FF0063" : 0x04, 554 | "#BD4573" : 0x05, 555 | "#CE0052" : 0x06, 556 | "#9C0031" : 0x07, 557 | "#522031" : 0x08, 558 | "#FFBACE" : 0x10, 559 | "#FF7573" : 0x11, 560 | "#DE3010" : 0x12, 561 | "#FF5542" : 0x13, 562 | "#FF0000" : 0x14, 563 | "#CE6563" : 0x15, 564 | "#BD4542" : 0x16, 565 | "#BD0000" : 0x17, 566 | "#8C2021" : 0x18, 567 | "#DECFBD" : 0x20, 568 | "#FFCF63" : 0x21, 569 | "#DE6521" : 0x22, 570 | "#FFAA21" : 0x23, 571 | "#FF6500" : 0x24, 572 | "#BD8A52" : 0x25, 573 | "#DE4500" : 0x26, 574 | "#BD4500" : 0x27, 575 | "#633010" : 0x28, 576 | "#FFEFDE" : 0x30, 577 | "#FFDFCE" : 0x31, 578 | "#FFCFAD" : 0x32, 579 | "#FFBA8C" : 0x33, 580 | "#FFAA8C" : 0x34, 581 | "#DE8A63" : 0x35, 582 | "#BD6542" : 0x36, 583 | "#9C5531" : 0x37, 584 | "#8C4521" : 0x38, 585 | "#FFCFFF" : 0x40, 586 | "#EF8AFF" : 0x41, 587 | "#CE65DE" : 0x42, 588 | "#BD8ACE" : 0x43, 589 | "#CE00FF" : 0x44, 590 | "#9C659C" : 0x45, 591 | "#8C00AD" : 0x46, 592 | "#520073" : 0x47, 593 | "#310042" : 0x48, 594 | "#FFBAFF" : 0x50, 595 | "#FF9AFF" : 0x51, 596 | "#DE20BD" : 0x52, 597 | "#FF55EF" : 0x53, 598 | "#FF00CE" : 0x54, 599 | "#8C5573" : 0x55, 600 | "#BD009C" : 0x56, 601 | "#8C0063" : 0x57, 602 | "#520042" : 0x58, 603 | "#DEBA9C" : 0x60, 604 | "#CEAA73" : 0x61, 605 | "#734531" : 0x62, 606 | "#AD7542" : 0x63, 607 | "#9C3000" : 0x64, 608 | "#733021" : 0x65, 609 | "#522000" : 0x66, 610 | "#311000" : 0x67, 611 | "#211000" : 0x68, 612 | "#FFFFCE" : 0x70, 613 | "#FFFF73" : 0x71, 614 | "#DEDF21" : 0x72, 615 | "#FFFF00" : 0x73, 616 | "#FFDF00" : 0x74, 617 | "#CEAA00" : 0x75, 618 | "#9C9A00" : 0x76, 619 | "#8C7500" : 0x77, 620 | "#525500" : 0x78, 621 | "#DEBAFF" : 0x80, 622 | "#BD9AEF" : 0x81, 623 | "#6330CE" : 0x82, 624 | "#9C55FF" : 0x83, 625 | "#6300FF" : 0x84, 626 | "#52458C" : 0x85, 627 | "#42009C" : 0x86, 628 | "#210063" : 0x87, 629 | "#211031" : 0x88, 630 | "#BDBAFF" : 0x90, 631 | "#8C9AFF" : 0x91, 632 | "#3130AD" : 0x92, 633 | "#3155EF" : 0x93, 634 | "#0000FF" : 0x94, 635 | "#31308C" : 0x95, 636 | "#0000AD" : 0x96, 637 | "#101063" : 0x97, 638 | "#000021" : 0x98, 639 | "#9CEFBD" : 0xA0, 640 | "#63CF73" : 0xA1, 641 | "#216510" : 0xA2, 642 | "#42AA31" : 0xA3, 643 | "#008A31" : 0xA4, 644 | "#527552" : 0xA5, 645 | "#215500" : 0xA6, 646 | "#103021" : 0xA7, 647 | "#002010" : 0xA8, 648 | "#DEFFBD" : 0xB0, 649 | "#CEFF8C" : 0xB1, 650 | "#8CAA52" : 0xB2, 651 | "#ADDF8C" : 0xB3, 652 | "#8CFF00" : 0xB4, 653 | "#ADBA9C" : 0xB5, 654 | "#63BA00" : 0xB6, 655 | "#529A00" : 0xB7, 656 | "#316500" : 0xB8, 657 | "#BDDFFF" : 0xC0, 658 | "#73CFFF" : 0xC1, 659 | "#31559C" : 0xC2, 660 | "#639AFF" : 0xC3, 661 | "#1075FF" : 0xC4, 662 | "#4275AD" : 0xC5, 663 | "#214573" : 0xC6, 664 | "#002073" : 0xC7, 665 | "#001042" : 0xC8, 666 | "#ADFFFF" : 0xD0, 667 | "#52FFFF" : 0xD1, 668 | "#008ABD" : 0xD2, 669 | "#52BACE" : 0xD3, 670 | "#00CFFF" : 0xD4, 671 | "#429AAD" : 0xD5, 672 | "#00658C" : 0xD6, 673 | "#004552" : 0xD7, 674 | "#002031" : 0xD8, 675 | "#CEFFEF" : 0xE0, 676 | "#ADEFDE" : 0xE1, 677 | "#31CFAD" : 0xE2, 678 | "#52EFBD" : 0xE3, 679 | "#00FFCE" : 0xE4, 680 | "#73AAAD" : 0xE5, 681 | "#00AA9C" : 0xE6, 682 | "#008A73" : 0xE7, 683 | "#004531" : 0xE8, 684 | "#ADFFAD" : 0xF0, 685 | "#73FF73" : 0xF1, 686 | "#63DF42" : 0xF2, 687 | "#00FF00" : 0xF3, 688 | "#21DF21" : 0xF4, 689 | "#52BA52" : 0xF5, 690 | "#00BA00" : 0xF6, 691 | "#008A00" : 0xF7, 692 | "#214521" : 0xF8, 693 | "#FFFFFF" : 0x0F, 694 | "#ECECEC" : 0x1F, 695 | "#DADADA" : 0x2F, 696 | "#C8C8C8" : 0x3F, 697 | "#B6B6B6" : 0x4F, 698 | "#A3A3A3" : 0x5F, 699 | "#919191" : 0x6F, 700 | "#7F7F7F" : 0x7F, 701 | "#6D6D6D" : 0x8F, 702 | "#5B5B5B" : 0x9F, 703 | "#484848" : 0xAF, 704 | "#363636" : 0xBF, 705 | "#242424" : 0xCF, 706 | "#121212" : 0xDF, 707 | "#000000" : 0xEF, 708 | }; 709 | } 710 | } 711 | 712 | export default ACNL; 713 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | color: white; 3 | background-color: black; 4 | } 5 | 6 | .editor { 7 | padding-top: 40px; 8 | padding-left: 40px; 9 | } 10 | 11 | .canvas-container { 12 | display: inline-block; 13 | vertical-align: top; 14 | margin-right: 20px; 15 | } 16 | 17 | .canvas { 18 | display:inline-block; 19 | margin-left: 32px; 20 | margin-top: 32px; 21 | border: 1px solid white; 22 | background-color: white; 23 | } 24 | 25 | .canvas-zoom { 26 | display: block; 27 | } 28 | 29 | .canvas-zoomier { 30 | margin-left: 5px; 31 | margin-right: 5px; 32 | vertical-align: bottom; 33 | } 34 | 35 | .color-tools { 36 | display: inline-block; 37 | width: 164px; 38 | vertical-align: top; 39 | margin-left: 20px; 40 | } 41 | 42 | .swatch, .palette { 43 | user-select: none; 44 | border: 2px solid white; 45 | } 46 | 47 | .swatch { 48 | margin-top: 32px; 49 | display: inline-block; 50 | margin-right: 5px; 51 | vertical-align: top; 52 | } 53 | 54 | .swatch-color-row { 55 | height: 32px; 56 | background-color: black; 57 | } 58 | 59 | 60 | .swatch-color { 61 | width: 32px; 62 | height: 32px; 63 | display: inline-block; 64 | margin: 0; 65 | padding: 0; 66 | font-size: 0; 67 | } 68 | 69 | .swatch-color.picked { 70 | width: 24px; 71 | height: 24px; 72 | margin: 4px; 73 | } 74 | 75 | 76 | .palette { 77 | display: inline-block; 78 | width: 160px; 79 | height: 180px; 80 | vertical-align: bottom; 81 | } 82 | 83 | .palette-color-block { 84 | height: 30px; 85 | width: 30px; 86 | float: left; 87 | overflow: hidden; 88 | margin: 5px; 89 | } 90 | 91 | .palette-color-row { 92 | height: 10px; 93 | width: 150px; 94 | float: left; 95 | overflow: hidden; 96 | margin: 5px; 97 | } 98 | 99 | .palette-color { 100 | width: 10px; 101 | height: 10px; 102 | border: 0; 103 | float: left; 104 | margin: 0; 105 | padding: 0; 106 | } 107 | 108 | .palette-color.picked { 109 | width: 5px; 110 | height: 5px; 111 | margin: 2.5px; 112 | } 113 | 114 | .metadata-settings { 115 | margin-top: 30px; 116 | } 117 | 118 | .metadata-label { 119 | width: 7em; 120 | display: inline-block; 121 | } 122 | 123 | .metadata-input { 124 | border: 0; 125 | color: white; 126 | background-color: #444444; 127 | padding-left: 1em; 128 | padding-right: 1em; 129 | } 130 | 131 | .metadata-input.title { 132 | width: 15em; 133 | } 134 | 135 | .metadata-input.name { 136 | width: 9em; 137 | } 138 | 139 | .metadata-input.id { 140 | margin-left: 1em; 141 | width: 3em; 142 | } 143 | 144 | .qr-code-container { 145 | margin-top: 100px; 146 | } 147 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // crucial 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import './index.css'; 5 | 6 | // component imports 7 | import Editor from './Editor.jsx'; 8 | 9 | 10 | ReactDOM.render(, document.getElementById('root')); 11 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/qrcode.js: -------------------------------------------------------------------------------- 1 | //--------------------------------------------------------------------- 2 | // 3 | // QR Code Generator for JavaScript 4 | // 5 | // Copyright (c) 2009 Kazuhiko Arase 6 | // 7 | // URL: http://www.d-project.com/ 8 | // 9 | // Licensed under the MIT license: 10 | // http://www.opensource.org/licenses/mit-license.php 11 | // 12 | // The word 'QR Code' is registered trademark of 13 | // DENSO WAVE INCORPORATED 14 | // http://www.denso-wave.com/qrcode/faqpatent-e.html 15 | // 16 | //--------------------------------------------------------------------- 17 | 18 | // NOTE: This was patched by Thulinma for the multipart spec 19 | 20 | let qrencode = function () { 21 | 22 | //--------------------------------------------------------------------- 23 | // qrcode 24 | //--------------------------------------------------------------------- 25 | 26 | /** 27 | * qrcode 28 | * @param typeNumber 1 to 10 29 | * @param errorCorrectLevel 'L','M','Q','H' 30 | */ 31 | let qrcode = function (typeNumber, errorCorrectLevel, multipart_num, multipart_total, multipart_parity) { 32 | 33 | let PAD0 = 0xEC; 34 | let PAD1 = 0x11; 35 | 36 | let _typeNumber = typeNumber; 37 | let _errorCorrectLevel = QRErrorCorrectLevel[errorCorrectLevel]; 38 | let _modules = null; 39 | let _moduleCount = 0; 40 | let _dataCache = null; 41 | let _dataList = []; 42 | 43 | let _this = {}; 44 | 45 | let makeImpl = function (test, maskPattern) { 46 | 47 | _moduleCount = _typeNumber * 4 + 17; 48 | _modules = function (moduleCount) { 49 | let modules = new Array(moduleCount); 50 | for (let row = 0; row < moduleCount; row += 1) { 51 | modules[row] = new Array(moduleCount); 52 | for (let col = 0; col < moduleCount; col += 1) { 53 | modules[row][col] = null; 54 | } 55 | } 56 | return modules; 57 | }(_moduleCount); 58 | 59 | setupPositionProbePattern(0, 0); 60 | setupPositionProbePattern(_moduleCount - 7, 0); 61 | setupPositionProbePattern(0, _moduleCount - 7); 62 | setupPositionAdjustPattern(); 63 | setupTimingPattern(); 64 | setupTypeInfo(test, maskPattern); 65 | 66 | if (_typeNumber >= 7) { 67 | setupTypeNumber(test); 68 | } 69 | 70 | if (_dataCache == null) { 71 | _dataCache = createData(_typeNumber, _errorCorrectLevel, _dataList, multipart_num, multipart_total, multipart_parity); 72 | } 73 | 74 | mapData(_dataCache, maskPattern); 75 | }; 76 | 77 | let setupPositionProbePattern = function (row, col) { 78 | 79 | for (let r = -1; r <= 7; r += 1) { 80 | 81 | if (row + r <= -1 || _moduleCount <= row + r) continue; 82 | 83 | for (let c = -1; c <= 7; c += 1) { 84 | 85 | if (col + c <= -1 || _moduleCount <= col + c) continue; 86 | 87 | if ((0 <= r && r <= 6 && (c === 0 || c === 6)) 88 | || (0 <= c && c <= 6 && (r === 0 || r === 6)) 89 | || (2 <= r && r <= 4 && 2 <= c && c <= 4)) { 90 | _modules[row + r][col + c] = true; 91 | } else { 92 | _modules[row + r][col + c] = false; 93 | } 94 | } 95 | } 96 | }; 97 | 98 | let getBestMaskPattern = function () { 99 | 100 | let minLostPoint = 0; 101 | let pattern = 0; 102 | 103 | for (let i = 0; i < 8; i += 1) { 104 | 105 | makeImpl(true, i); 106 | 107 | let lostPoint = QRUtil.getLostPoint(_this); 108 | 109 | if (i === 0 || minLostPoint > lostPoint) { 110 | minLostPoint = lostPoint; 111 | pattern = i; 112 | } 113 | } 114 | 115 | return pattern; 116 | }; 117 | 118 | let setupTimingPattern = function () { 119 | 120 | for (let r = 8; r < _moduleCount - 8; r += 1) { 121 | if (_modules[r][6] != null) { 122 | continue; 123 | } 124 | _modules[r][6] = (r % 2 === 0); 125 | } 126 | 127 | for (let c = 8; c < _moduleCount - 8; c += 1) { 128 | if (_modules[6][c] != null) { 129 | continue; 130 | } 131 | _modules[6][c] = (c % 2 === 0); 132 | } 133 | }; 134 | 135 | let setupPositionAdjustPattern = function () { 136 | 137 | let pos = QRUtil.getPatternPosition(_typeNumber); 138 | 139 | for (let i = 0; i < pos.length; i += 1) { 140 | 141 | for (let j = 0; j < pos.length; j += 1) { 142 | 143 | let row = pos[i]; 144 | let col = pos[j]; 145 | 146 | if (_modules[row][col] != null) { 147 | continue; 148 | } 149 | 150 | for (let r = -2; r <= 2; r += 1) { 151 | 152 | for (let c = -2; c <= 2; c += 1) { 153 | 154 | if (r === -2 || r === 2 || c === -2 || c === 2 155 | || (r === 0 && c === 0)) { 156 | _modules[row + r][col + c] = true; 157 | } else { 158 | _modules[row + r][col + c] = false; 159 | } 160 | } 161 | } 162 | } 163 | } 164 | }; 165 | 166 | let setupTypeNumber = function (test) { 167 | 168 | let bits = QRUtil.getBCHTypeNumber(_typeNumber); 169 | 170 | for (let i = 0; i < 18; i += 1) { 171 | let mod = (!test && ((bits >> i) & 1) === 1); 172 | _modules[Math.floor(i / 3)][i % 3 + _moduleCount - 8 - 3] = mod; 173 | } 174 | 175 | for (let i = 0; i < 18; i += 1) { 176 | let mod = (!test && ((bits >> i) & 1) === 1); 177 | _modules[i % 3 + _moduleCount - 8 - 3][Math.floor(i / 3)] = mod; 178 | } 179 | }; 180 | 181 | let setupTypeInfo = function (test, maskPattern) { 182 | 183 | let data = (_errorCorrectLevel << 3) | maskPattern; 184 | let bits = QRUtil.getBCHTypeInfo(data); 185 | 186 | // vertical 187 | for (let i = 0; i < 15; i += 1) { 188 | 189 | let mod = (!test && ((bits >> i) & 1) === 1); 190 | 191 | if (i < 6) { 192 | _modules[i][8] = mod; 193 | } else if (i < 8) { 194 | _modules[i + 1][8] = mod; 195 | } else { 196 | _modules[_moduleCount - 15 + i][8] = mod; 197 | } 198 | } 199 | 200 | // horizontal 201 | for (let i = 0; i < 15; i += 1) { 202 | 203 | let mod = (!test && ((bits >> i) & 1) === 1); 204 | 205 | if (i < 8) { 206 | _modules[8][_moduleCount - i - 1] = mod; 207 | } else if (i < 9) { 208 | _modules[8][15 - i - 1 + 1] = mod; 209 | } else { 210 | _modules[8][15 - i - 1] = mod; 211 | } 212 | } 213 | 214 | // fixed module 215 | _modules[_moduleCount - 8][8] = (!test); 216 | }; 217 | 218 | let mapData = function (data, maskPattern) { 219 | 220 | let inc = -1; 221 | let row = _moduleCount - 1; 222 | let bitIndex = 7; 223 | let byteIndex = 0; 224 | let maskFunc = QRUtil.getMaskFunction(maskPattern); 225 | 226 | for (let col = _moduleCount - 1; col > 0; col -= 2) { 227 | 228 | if (col === 6) col -= 1; 229 | 230 | while (true) { 231 | 232 | for (let c = 0; c < 2; c += 1) { 233 | 234 | if (_modules[row][col - c] == null) { 235 | 236 | let dark = false; 237 | 238 | if (byteIndex < data.length) { 239 | dark = (((data[byteIndex] >>> bitIndex) & 1) === 1); 240 | } 241 | 242 | let mask = maskFunc(row, col - c); 243 | 244 | if (mask) { 245 | dark = !dark; 246 | } 247 | 248 | _modules[row][col - c] = dark; 249 | bitIndex -= 1; 250 | 251 | if (bitIndex === -1) { 252 | byteIndex += 1; 253 | bitIndex = 7; 254 | } 255 | } 256 | } 257 | 258 | row += inc; 259 | 260 | if (row < 0 || _moduleCount <= row) { 261 | row -= inc; 262 | inc = -inc; 263 | break; 264 | } 265 | } 266 | } 267 | }; 268 | 269 | let createBytes = function (buffer, rsBlocks) { 270 | 271 | let offset = 0; 272 | 273 | let maxDcCount = 0; 274 | let maxEcCount = 0; 275 | 276 | let dcdata = new Array(rsBlocks.length); 277 | let ecdata = new Array(rsBlocks.length); 278 | 279 | for (let r = 0; r < rsBlocks.length; r += 1) { 280 | 281 | let dcCount = rsBlocks[r].dataCount; 282 | let ecCount = rsBlocks[r].totalCount - dcCount; 283 | 284 | maxDcCount = Math.max(maxDcCount, dcCount); 285 | maxEcCount = Math.max(maxEcCount, ecCount); 286 | 287 | dcdata[r] = new Array(dcCount); 288 | 289 | for (let i = 0; i < dcdata[r].length; i += 1) { 290 | dcdata[r][i] = 0xff & buffer.getBuffer()[i + offset]; 291 | } 292 | offset += dcCount; 293 | 294 | let rsPoly = QRUtil.getErrorCorrectPolynomial(ecCount); 295 | let rawPoly = qrPolynomial(dcdata[r], rsPoly.getLength() - 1); 296 | 297 | let modPoly = rawPoly.mod(rsPoly); 298 | ecdata[r] = new Array(rsPoly.getLength() - 1); 299 | for (let i = 0; i < ecdata[r].length; i += 1) { 300 | let modIndex = i + modPoly.getLength() - ecdata[r].length; 301 | ecdata[r][i] = (modIndex >= 0) ? modPoly.get(modIndex) : 0; 302 | } 303 | } 304 | 305 | let totalCodeCount = 0; 306 | for (let i = 0; i < rsBlocks.length; i += 1) { 307 | totalCodeCount += rsBlocks[i].totalCount; 308 | } 309 | 310 | let data = new Array(totalCodeCount); 311 | let index = 0; 312 | 313 | for (let i = 0; i < maxDcCount; i += 1) { 314 | for (let r = 0; r < rsBlocks.length; r += 1) { 315 | if (i < dcdata[r].length) { 316 | data[index] = dcdata[r][i]; 317 | index += 1; 318 | } 319 | } 320 | } 321 | 322 | for (let i = 0; i < maxEcCount; i += 1) { 323 | for (let r = 0; r < rsBlocks.length; r += 1) { 324 | if (i < ecdata[r].length) { 325 | data[index] = ecdata[r][i]; 326 | index += 1; 327 | } 328 | } 329 | } 330 | 331 | return data; 332 | }; 333 | 334 | let createData = function (typeNumber, errorCorrectLevel, dataList, multipart_num, multipart_total, multipart_parity) { 335 | 336 | let rsBlocks = QRRSBlock.getRSBlocks(typeNumber, errorCorrectLevel); 337 | 338 | let buffer = qrBitBuffer(); 339 | 340 | if (multipart_total > 0) { 341 | buffer.put(3, 4);//structured extend 342 | buffer.put(multipart_num, 4);//current frame number 343 | buffer.put(multipart_total, 4);//total frame count 344 | buffer.put(multipart_parity, 8);//parity byte 345 | } 346 | 347 | for (let i = 0; i < dataList.length; i += 1) { 348 | let data = dataList[i]; 349 | buffer.put(data.getMode(), 4); 350 | buffer.put(data.getLength(), QRUtil.getLengthInBits(data.getMode(), typeNumber)); 351 | data.write(buffer); 352 | } 353 | 354 | // calc num max data. 355 | let totalDataCount = 0; 356 | for (let i = 0; i < rsBlocks.length; i += 1) { 357 | totalDataCount += rsBlocks[i].dataCount; 358 | } 359 | 360 | if (buffer.getLengthInBits() > totalDataCount * 8) { 361 | throw new Error('code length overflow. (' 362 | + buffer.getLengthInBits() 363 | + '>' 364 | + totalDataCount * 8 365 | + ')'); 366 | } 367 | 368 | // end code 369 | if (buffer.getLengthInBits() + 4 <= totalDataCount * 8) { 370 | buffer.put(0, 4); 371 | } 372 | 373 | // padding 374 | while (buffer.getLengthInBits() % 8 !== 0) { 375 | buffer.putBit(false); 376 | } 377 | 378 | // padding 379 | while (true) { 380 | 381 | if (buffer.getLengthInBits() >= totalDataCount * 8) { 382 | break; 383 | } 384 | buffer.put(PAD0, 8); 385 | 386 | if (buffer.getLengthInBits() >= totalDataCount * 8) { 387 | break; 388 | } 389 | buffer.put(PAD1, 8); 390 | } 391 | 392 | return createBytes(buffer, rsBlocks); 393 | }; 394 | 395 | _this.addData = function (data) { 396 | let newData = qr8BitByte(data); 397 | _dataList.push(newData); 398 | _dataCache = null; 399 | }; 400 | 401 | _this.isDark = function (row, col) { 402 | if (row < 0 || _moduleCount <= row || col < 0 || _moduleCount <= col) { 403 | throw new Error(row + ',' + col); 404 | } 405 | return _modules[row][col]; 406 | }; 407 | 408 | _this.getModuleCount = function () { 409 | return _moduleCount; 410 | }; 411 | 412 | _this.make = function () { 413 | makeImpl(false, getBestMaskPattern()); 414 | }; 415 | 416 | _this.createTableTag = function (cellSize, margin) { 417 | 418 | cellSize = cellSize || 2; 419 | margin = (typeof margin == 'undefined') ? cellSize * 4 : margin; 420 | 421 | let qrHtml = ''; 422 | 423 | qrHtml += ''; 428 | qrHtml += ''; 429 | 430 | for (let r = 0; r < _this.getModuleCount(); r += 1) { 431 | 432 | qrHtml += ''; 433 | 434 | for (let c = 0; c < _this.getModuleCount(); c += 1) { 435 | qrHtml += ''; 448 | } 449 | 450 | qrHtml += ''; 451 | qrHtml += '
'; 445 | } 446 | 447 | qrHtml += '
'; 452 | 453 | return qrHtml; 454 | }; 455 | 456 | _this.createImgTag = function (cellSize, margin) { 457 | 458 | cellSize = cellSize || 2; 459 | margin = (typeof margin == 'undefined') ? cellSize * 4 : margin; 460 | 461 | let size = _this.getModuleCount() * cellSize + margin * 2; 462 | let min = margin; 463 | let max = size - margin; 464 | 465 | return createImgTag(size, size, function (x, y) { 466 | if (min <= x && x < max && min <= y && y < max) { 467 | let c = Math.floor((x - min) / cellSize); 468 | let r = Math.floor((y - min) / cellSize); 469 | return _this.isDark(r, c) ? 0 : 1; 470 | } else { 471 | return 1; 472 | } 473 | }); 474 | }; 475 | 476 | return _this; 477 | }; 478 | 479 | //--------------------------------------------------------------------- 480 | // qrcode.stringToBytes 481 | //--------------------------------------------------------------------- 482 | 483 | qrcode.stringToBytes = function (s) { 484 | let bytes = []; 485 | for (let i = 0; i < s.length; i += 1) { 486 | let c = s.charCodeAt(i); 487 | bytes.push(c & 0xff); 488 | } 489 | return bytes; 490 | }; 491 | 492 | //--------------------------------------------------------------------- 493 | // qrcode.createStringToBytes 494 | //--------------------------------------------------------------------- 495 | 496 | /** 497 | * @param unicodeData base64 string of byte array. 498 | * [16bit Unicode],[16bit Bytes], ... 499 | * @param numChars 500 | */ 501 | qrcode.createStringToBytes = function (unicodeData, numChars) { 502 | 503 | // create conversion map. 504 | 505 | let unicodeMap = function () { 506 | 507 | let bin = base64DecodeInputStream(unicodeData); 508 | let read = function () { 509 | let b = bin.read(); 510 | if (b === -1) throw new Error(); 511 | return b; 512 | }; 513 | 514 | let count = 0; 515 | let unicodeMap = {}; 516 | while (true) { 517 | let b0 = bin.read(); 518 | if (b0 === -1) break; 519 | let b1 = read(); 520 | let b2 = read(); 521 | let b3 = read(); 522 | let k = String.fromCharCode((b0 << 8) | b1); 523 | let v = (b2 << 8) | b3; 524 | unicodeMap[k] = v; 525 | count += 1; 526 | } 527 | if (count !== numChars) { 528 | throw new Error(count + ' != ' + numChars); 529 | } 530 | 531 | return unicodeMap; 532 | }(); 533 | 534 | let unknownChar = '?'.charCodeAt(0); 535 | 536 | return function (s) { 537 | let bytes = []; 538 | for (let i = 0; i < s.length; i += 1) { 539 | let c = s.charCodeAt(i); 540 | if (c < 128) { 541 | bytes.push(c); 542 | } else { 543 | let b = unicodeMap[s.charAt(i)]; 544 | if (typeof b == 'number') { 545 | if ((b & 0xff) === b) { 546 | // 1byte 547 | bytes.push(b); 548 | } else { 549 | // 2bytes 550 | bytes.push(b >>> 8); 551 | bytes.push(b & 0xff); 552 | } 553 | } else { 554 | bytes.push(unknownChar); 555 | } 556 | } 557 | } 558 | return bytes; 559 | }; 560 | }; 561 | 562 | //--------------------------------------------------------------------- 563 | // QRMode 564 | //--------------------------------------------------------------------- 565 | 566 | let QRMode = { 567 | MODE_NUMBER: 1 << 0, 568 | MODE_ALPHA_NUM: 1 << 1, 569 | MODE_8BIT_BYTE: 1 << 2, 570 | MODE_KANJI: 1 << 3 571 | }; 572 | 573 | //--------------------------------------------------------------------- 574 | // QRErrorCorrectLevel 575 | //--------------------------------------------------------------------- 576 | 577 | let QRErrorCorrectLevel = { 578 | L: 1, 579 | M: 0, 580 | Q: 3, 581 | H: 2 582 | }; 583 | 584 | //--------------------------------------------------------------------- 585 | // QRMaskPattern 586 | //--------------------------------------------------------------------- 587 | 588 | let QRMaskPattern = { 589 | PATTERN000: 0, 590 | PATTERN001: 1, 591 | PATTERN010: 2, 592 | PATTERN011: 3, 593 | PATTERN100: 4, 594 | PATTERN101: 5, 595 | PATTERN110: 6, 596 | PATTERN111: 7 597 | }; 598 | 599 | //--------------------------------------------------------------------- 600 | // QRUtil 601 | //--------------------------------------------------------------------- 602 | 603 | let QRUtil = function () { 604 | 605 | let PATTERN_POSITION_TABLE = [ 606 | [], 607 | [6, 18], 608 | [6, 22], 609 | [6, 26], 610 | [6, 30], 611 | [6, 34], 612 | [6, 22, 38], 613 | [6, 24, 42], 614 | [6, 26, 46], 615 | [6, 28, 50], 616 | [6, 30, 54], 617 | [6, 32, 58], 618 | [6, 34, 62], 619 | [6, 26, 46, 66], 620 | [6, 26, 48, 70], 621 | [6, 26, 50, 74], 622 | [6, 30, 54, 78], 623 | [6, 30, 56, 82], 624 | [6, 30, 58, 86], 625 | [6, 34, 62, 90], 626 | [6, 28, 50, 72, 94], 627 | [6, 26, 50, 74, 98], 628 | [6, 30, 54, 78, 102], 629 | [6, 28, 54, 80, 106], 630 | [6, 32, 58, 84, 110], 631 | [6, 30, 58, 86, 114], 632 | [6, 34, 62, 90, 118], 633 | [6, 26, 50, 74, 98, 122], 634 | [6, 30, 54, 78, 102, 126], 635 | [6, 26, 52, 78, 104, 130], 636 | [6, 30, 56, 82, 108, 134], 637 | [6, 34, 60, 86, 112, 138], 638 | [6, 30, 58, 86, 114, 142], 639 | [6, 34, 62, 90, 118, 146], 640 | [6, 30, 54, 78, 102, 126, 150], 641 | [6, 24, 50, 76, 102, 128, 154], 642 | [6, 28, 54, 80, 106, 132, 158], 643 | [6, 32, 58, 84, 110, 136, 162], 644 | [6, 26, 54, 82, 110, 138, 166], 645 | [6, 30, 58, 86, 114, 142, 170] 646 | ]; 647 | let G15 = (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0); 648 | let G18 = (1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8) | (1 << 5) | (1 << 2) | (1 << 0); 649 | let G15_MASK = (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1); 650 | 651 | let _this = {}; 652 | 653 | let getBCHDigit = function (data) { 654 | let digit = 0; 655 | while (data !== 0) { 656 | digit += 1; 657 | data >>>= 1; 658 | } 659 | return digit; 660 | }; 661 | 662 | _this.getBCHTypeInfo = function (data) { 663 | let d = data << 10; 664 | while (getBCHDigit(d) - getBCHDigit(G15) >= 0) { 665 | d ^= (G15 << (getBCHDigit(d) - getBCHDigit(G15))); 666 | } 667 | return ((data << 10) | d) ^ G15_MASK; 668 | }; 669 | 670 | _this.getBCHTypeNumber = function (data) { 671 | let d = data << 12; 672 | while (getBCHDigit(d) - getBCHDigit(G18) >= 0) { 673 | d ^= (G18 << (getBCHDigit(d) - getBCHDigit(G18))); 674 | } 675 | return (data << 12) | d; 676 | }; 677 | 678 | _this.getPatternPosition = function (typeNumber) { 679 | return PATTERN_POSITION_TABLE[typeNumber - 1]; 680 | }; 681 | 682 | _this.getMaskFunction = function (maskPattern) { 683 | 684 | switch (maskPattern) { 685 | 686 | case QRMaskPattern.PATTERN000: 687 | return function (i, j) { return (i + j) % 2 === 0; }; 688 | case QRMaskPattern.PATTERN001: 689 | return function (i, j) { return i % 2 === 0; }; 690 | case QRMaskPattern.PATTERN010: 691 | return function (i, j) { return j % 3 === 0; }; 692 | case QRMaskPattern.PATTERN011: 693 | return function (i, j) { return (i + j) % 3 === 0; }; 694 | case QRMaskPattern.PATTERN100: 695 | return function (i, j) { return (Math.floor(i / 2) + Math.floor(j / 3)) % 2 === 0; }; 696 | case QRMaskPattern.PATTERN101: 697 | return function (i, j) { return (i * j) % 2 + (i * j) % 3 === 0; }; 698 | case QRMaskPattern.PATTERN110: 699 | return function (i, j) { return ((i * j) % 2 + (i * j) % 3) % 2 === 0; }; 700 | case QRMaskPattern.PATTERN111: 701 | return function (i, j) { return ((i * j) % 3 + (i + j) % 2) % 2 === 0; }; 702 | 703 | default: 704 | throw new Error('bad maskPattern:' + maskPattern); 705 | } 706 | }; 707 | 708 | _this.getErrorCorrectPolynomial = function (errorCorrectLength) { 709 | let a = qrPolynomial([1], 0); 710 | for (let i = 0; i < errorCorrectLength; i += 1) { 711 | a = a.multiply(qrPolynomial([1, QRMath.gexp(i)], 0)); 712 | } 713 | return a; 714 | }; 715 | 716 | _this.getLengthInBits = function (mode, type) { 717 | 718 | if (1 <= type && type < 10) { 719 | 720 | // 1 - 9 721 | 722 | switch (mode) { 723 | case QRMode.MODE_NUMBER: return 10; 724 | case QRMode.MODE_ALPHA_NUM: return 9; 725 | case QRMode.MODE_8BIT_BYTE: return 8; 726 | case QRMode.MODE_KANJI: return 8; 727 | default: 728 | throw new Error('mode:' + mode); 729 | } 730 | 731 | } else if (type < 27) { 732 | 733 | // 10 - 26 734 | 735 | switch (mode) { 736 | case QRMode.MODE_NUMBER: return 12; 737 | case QRMode.MODE_ALPHA_NUM: return 11; 738 | case QRMode.MODE_8BIT_BYTE: return 16; 739 | case QRMode.MODE_KANJI: return 10; 740 | default: 741 | throw new Error('mode:' + mode); 742 | } 743 | 744 | } else if (type < 41) { 745 | 746 | // 27 - 40 747 | 748 | switch (mode) { 749 | case QRMode.MODE_NUMBER: return 14; 750 | case QRMode.MODE_ALPHA_NUM: return 13; 751 | case QRMode.MODE_8BIT_BYTE: return 16; 752 | case QRMode.MODE_KANJI: return 12; 753 | default: 754 | throw new Error('mode:' + mode); 755 | } 756 | 757 | } else { 758 | throw new Error('type:' + type); 759 | } 760 | }; 761 | 762 | _this.getLostPoint = function (qrcode) { 763 | 764 | let moduleCount = qrcode.getModuleCount(); 765 | 766 | let lostPoint = 0; 767 | 768 | // LEVEL1 769 | 770 | for (let row = 0; row < moduleCount; row += 1) { 771 | for (let col = 0; col < moduleCount; col += 1) { 772 | 773 | let sameCount = 0; 774 | let dark = qrcode.isDark(row, col); 775 | 776 | for (let r = -1; r <= 1; r += 1) { 777 | 778 | if (row + r < 0 || moduleCount <= row + r) { 779 | continue; 780 | } 781 | 782 | for (let c = -1; c <= 1; c += 1) { 783 | 784 | if (col + c < 0 || moduleCount <= col + c) { 785 | continue; 786 | } 787 | 788 | if (r === 0 && c === 0) { 789 | continue; 790 | } 791 | 792 | if (dark === qrcode.isDark(row + r, col + c)) { 793 | sameCount += 1; 794 | } 795 | } 796 | } 797 | 798 | if (sameCount > 5) { 799 | lostPoint += (3 + sameCount - 5); 800 | } 801 | } 802 | }; 803 | 804 | // LEVEL2 805 | 806 | for (let row = 0; row < moduleCount - 1; row += 1) { 807 | for (let col = 0; col < moduleCount - 1; col += 1) { 808 | let count = 0; 809 | if (qrcode.isDark(row, col)) count += 1; 810 | if (qrcode.isDark(row + 1, col)) count += 1; 811 | if (qrcode.isDark(row, col + 1)) count += 1; 812 | if (qrcode.isDark(row + 1, col + 1)) count += 1; 813 | if (count === 0 || count === 4) { 814 | lostPoint += 3; 815 | } 816 | } 817 | } 818 | 819 | // LEVEL3 820 | 821 | for (let row = 0; row < moduleCount; row += 1) { 822 | for (let col = 0; col < moduleCount - 6; col += 1) { 823 | if (qrcode.isDark(row, col) 824 | && !qrcode.isDark(row, col + 1) 825 | && qrcode.isDark(row, col + 2) 826 | && qrcode.isDark(row, col + 3) 827 | && qrcode.isDark(row, col + 4) 828 | && !qrcode.isDark(row, col + 5) 829 | && qrcode.isDark(row, col + 6)) { 830 | lostPoint += 40; 831 | } 832 | } 833 | } 834 | 835 | for (let col = 0; col < moduleCount; col += 1) { 836 | for (let row = 0; row < moduleCount - 6; row += 1) { 837 | if (qrcode.isDark(row, col) 838 | && !qrcode.isDark(row + 1, col) 839 | && qrcode.isDark(row + 2, col) 840 | && qrcode.isDark(row + 3, col) 841 | && qrcode.isDark(row + 4, col) 842 | && !qrcode.isDark(row + 5, col) 843 | && qrcode.isDark(row + 6, col)) { 844 | lostPoint += 40; 845 | } 846 | } 847 | } 848 | 849 | // LEVEL4 850 | 851 | let darkCount = 0; 852 | 853 | for (let col = 0; col < moduleCount; col += 1) { 854 | for (let row = 0; row < moduleCount; row += 1) { 855 | if (qrcode.isDark(row, col)) { 856 | darkCount += 1; 857 | } 858 | } 859 | } 860 | 861 | let ratio = Math.abs(100 * darkCount / moduleCount / moduleCount - 50) / 5; 862 | lostPoint += ratio * 10; 863 | 864 | return lostPoint; 865 | }; 866 | 867 | return _this; 868 | }(); 869 | 870 | //--------------------------------------------------------------------- 871 | // QRMath 872 | //--------------------------------------------------------------------- 873 | 874 | let QRMath = function () { 875 | 876 | let EXP_TABLE = new Array(256); 877 | let LOG_TABLE = new Array(256); 878 | 879 | // initialize tables 880 | for (let i = 0; i < 8; i += 1) { 881 | EXP_TABLE[i] = 1 << i; 882 | } 883 | for (let i = 8; i < 256; i += 1) { 884 | EXP_TABLE[i] = EXP_TABLE[i - 4] 885 | ^ EXP_TABLE[i - 5] 886 | ^ EXP_TABLE[i - 6] 887 | ^ EXP_TABLE[i - 8]; 888 | } 889 | for (let i = 0; i < 255; i += 1) { 890 | LOG_TABLE[EXP_TABLE[i]] = i; 891 | } 892 | 893 | let _this = {}; 894 | 895 | _this.glog = function (n) { 896 | 897 | if (n < 1) { 898 | throw new Error('glog(' + n + ')'); 899 | } 900 | 901 | return LOG_TABLE[n]; 902 | }; 903 | 904 | _this.gexp = function (n) { 905 | 906 | while (n < 0) { 907 | n += 255; 908 | } 909 | 910 | while (n >= 256) { 911 | n -= 255; 912 | } 913 | 914 | return EXP_TABLE[n]; 915 | }; 916 | 917 | return _this; 918 | }(); 919 | 920 | //--------------------------------------------------------------------- 921 | // qrPolynomial 922 | //--------------------------------------------------------------------- 923 | 924 | function qrPolynomial(num, shift) { 925 | 926 | if (typeof num.length == 'undefined') { 927 | throw new Error(num.length + '/' + shift); 928 | } 929 | 930 | let _num = function () { 931 | let offset = 0; 932 | while (offset < num.length && num[offset] === 0) { 933 | offset += 1; 934 | } 935 | let _num = new Array(num.length - offset + shift); 936 | for (let i = 0; i < num.length - offset; i += 1) { 937 | _num[i] = num[i + offset]; 938 | } 939 | return _num; 940 | }(); 941 | 942 | let _this = {}; 943 | 944 | _this.get = function (index) { 945 | return _num[index]; 946 | }; 947 | 948 | _this.getLength = function () { 949 | return _num.length; 950 | }; 951 | 952 | _this.multiply = function (e) { 953 | 954 | let num = new Array(_this.getLength() + e.getLength() - 1); 955 | 956 | for (let i = 0; i < _this.getLength(); i += 1) { 957 | for (let j = 0; j < e.getLength(); j += 1) { 958 | num[i + j] ^= QRMath.gexp(QRMath.glog(_this.get(i)) + QRMath.glog(e.get(j))); 959 | } 960 | } 961 | 962 | return qrPolynomial(num, 0); 963 | }; 964 | 965 | _this.mod = function (e) { 966 | 967 | if (_this.getLength() - e.getLength() < 0) { 968 | return _this; 969 | } 970 | 971 | let ratio = QRMath.glog(_this.get(0)) - QRMath.glog(e.get(0)); 972 | 973 | let num = new Array(_this.getLength()); 974 | for (let i = 0; i < _this.getLength(); i += 1) { 975 | num[i] = _this.get(i); 976 | } 977 | 978 | for (let i = 0; i < e.getLength(); i += 1) { 979 | num[i] ^= QRMath.gexp(QRMath.glog(e.get(i)) + ratio); 980 | } 981 | 982 | // recursive call 983 | return qrPolynomial(num, 0).mod(e); 984 | }; 985 | 986 | return _this; 987 | }; 988 | 989 | //--------------------------------------------------------------------- 990 | // QRRSBlock 991 | //--------------------------------------------------------------------- 992 | 993 | let QRRSBlock = function () { 994 | 995 | let RS_BLOCK_TABLE = [ 996 | 997 | // L 998 | // M 999 | // Q 1000 | // H 1001 | 1002 | // 1 1003 | [1, 26, 19], 1004 | [1, 26, 16], 1005 | [1, 26, 13], 1006 | [1, 26, 9], 1007 | 1008 | // 2 1009 | [1, 44, 34], 1010 | [1, 44, 28], 1011 | [1, 44, 22], 1012 | [1, 44, 16], 1013 | 1014 | // 3 1015 | [1, 70, 55], 1016 | [1, 70, 44], 1017 | [2, 35, 17], 1018 | [2, 35, 13], 1019 | 1020 | // 4 1021 | [1, 100, 80], 1022 | [2, 50, 32], 1023 | [2, 50, 24], 1024 | [4, 25, 9], 1025 | 1026 | // 5 1027 | [1, 134, 108], 1028 | [2, 67, 43], 1029 | [2, 33, 15, 2, 34, 16], 1030 | [2, 33, 11, 2, 34, 12], 1031 | 1032 | // 6 1033 | [2, 86, 68], 1034 | [4, 43, 27], 1035 | [4, 43, 19], 1036 | [4, 43, 15], 1037 | 1038 | // 7 1039 | [2, 98, 78], 1040 | [4, 49, 31], 1041 | [2, 32, 14, 4, 33, 15], 1042 | [4, 39, 13, 1, 40, 14], 1043 | 1044 | // 8 1045 | [2, 121, 97], 1046 | [2, 60, 38, 2, 61, 39], 1047 | [4, 40, 18, 2, 41, 19], 1048 | [4, 40, 14, 2, 41, 15], 1049 | 1050 | // 9 1051 | [2, 146, 116], 1052 | [3, 58, 36, 2, 59, 37], 1053 | [4, 36, 16, 4, 37, 17], 1054 | [4, 36, 12, 4, 37, 13], 1055 | 1056 | // 10 1057 | [2, 86, 68, 2, 87, 69], 1058 | [4, 69, 43, 1, 70, 44], 1059 | [6, 43, 19, 2, 44, 20], 1060 | [6, 43, 15, 2, 44, 16], 1061 | 1062 | // 11 1063 | [4, 101, 81], 1064 | [1, 80, 50, 4, 81, 51], 1065 | [4, 50, 22, 4, 51, 23], 1066 | [3, 36, 12, 8, 37, 13], 1067 | 1068 | // 12 1069 | [2, 116, 92, 2, 117, 93], 1070 | [6, 58, 36, 2, 59, 37], 1071 | [4, 46, 20, 6, 47, 21], 1072 | [7, 42, 14, 4, 43, 15], 1073 | 1074 | // 13 1075 | [4, 133, 107], 1076 | [8, 59, 37, 1, 60, 38], 1077 | [8, 44, 20, 4, 45, 21], 1078 | [12, 33, 11, 4, 34, 12], 1079 | 1080 | // 14 1081 | [3, 145, 115, 1, 146, 116], 1082 | [4, 64, 40, 5, 65, 41], 1083 | [11, 36, 16, 5, 37, 17], 1084 | [11, 36, 12, 5, 37, 13], 1085 | 1086 | // 15 1087 | [5, 109, 87, 1, 110, 88], 1088 | [5, 65, 41, 5, 66, 42], 1089 | [5, 54, 24, 7, 55, 25], 1090 | [11, 36, 12], 1091 | 1092 | // 16 1093 | [5, 122, 98, 1, 123, 99], 1094 | [7, 73, 45, 3, 74, 46], 1095 | [15, 43, 19, 2, 44, 20], 1096 | [3, 45, 15, 13, 46, 16], 1097 | 1098 | // 17 1099 | [1, 135, 107, 5, 136, 108], 1100 | [10, 74, 46, 1, 75, 47], 1101 | [1, 50, 22, 15, 51, 23], 1102 | [2, 42, 14, 17, 43, 15], 1103 | 1104 | // 18 1105 | [5, 150, 120, 1, 151, 121], 1106 | [9, 69, 43, 4, 70, 44], 1107 | [17, 50, 22, 1, 51, 23], 1108 | [2, 42, 14, 19, 43, 15], 1109 | 1110 | // 19 1111 | [3, 141, 113, 4, 142, 114], 1112 | [3, 70, 44, 11, 71, 45], 1113 | [17, 47, 21, 4, 48, 22], 1114 | [9, 39, 13, 16, 40, 14], 1115 | 1116 | // 20 1117 | [3, 135, 107, 5, 136, 108], 1118 | [3, 67, 41, 13, 68, 42], 1119 | [15, 54, 24, 5, 55, 25], 1120 | [15, 43, 15, 10, 44, 16], 1121 | 1122 | // 21 1123 | [4, 144, 116, 4, 145, 117], 1124 | [17, 68, 42], 1125 | [17, 50, 22, 6, 51, 23], 1126 | [19, 46, 16, 6, 47, 17], 1127 | 1128 | // 22 1129 | [2, 139, 111, 7, 140, 112], 1130 | [17, 74, 46], 1131 | [7, 54, 24, 16, 55, 25], 1132 | [34, 37, 13], 1133 | 1134 | // 23 1135 | [4, 151, 121, 5, 152, 122], 1136 | [4, 75, 47, 14, 76, 48], 1137 | [11, 54, 24, 14, 55, 25], 1138 | [16, 45, 15, 14, 46, 16], 1139 | 1140 | // 24 1141 | [6, 147, 117, 4, 148, 118], 1142 | [6, 73, 45, 14, 74, 46], 1143 | [11, 54, 24, 16, 55, 25], 1144 | [30, 46, 16, 2, 47, 17], 1145 | 1146 | // 25 1147 | [8, 132, 106, 4, 133, 107], 1148 | [8, 75, 47, 13, 76, 48], 1149 | [7, 54, 24, 22, 55, 25], 1150 | [22, 45, 15, 13, 46, 16], 1151 | 1152 | // 26 1153 | [10, 142, 114, 2, 143, 115], 1154 | [19, 74, 46, 4, 75, 47], 1155 | [28, 50, 22, 6, 51, 23], 1156 | [33, 46, 16, 4, 47, 17], 1157 | 1158 | // 27 1159 | [8, 152, 122, 4, 153, 123], 1160 | [22, 73, 45, 3, 74, 46], 1161 | [8, 53, 23, 26, 54, 24], 1162 | [12, 45, 15, 28, 46, 16], 1163 | 1164 | // 28 1165 | [3, 147, 117, 10, 148, 118], 1166 | [3, 73, 45, 23, 74, 46], 1167 | [4, 54, 24, 31, 55, 25], 1168 | [11, 45, 15, 31, 46, 16], 1169 | 1170 | // 29 1171 | [7, 146, 116, 7, 147, 117], 1172 | [21, 73, 45, 7, 74, 46], 1173 | [1, 53, 23, 37, 54, 24], 1174 | [19, 45, 15, 26, 46, 16], 1175 | 1176 | // 30 1177 | [5, 145, 115, 10, 146, 116], 1178 | [19, 75, 47, 10, 76, 48], 1179 | [15, 54, 24, 25, 55, 25], 1180 | [23, 45, 15, 25, 46, 16], 1181 | 1182 | // 31 1183 | [13, 145, 115, 3, 146, 116], 1184 | [2, 74, 46, 29, 75, 47], 1185 | [42, 54, 24, 1, 55, 25], 1186 | [23, 45, 15, 28, 46, 16], 1187 | 1188 | // 32 1189 | [17, 145, 115], 1190 | [10, 74, 46, 23, 75, 47], 1191 | [10, 54, 24, 35, 55, 25], 1192 | [19, 45, 15, 35, 46, 16], 1193 | 1194 | // 33 1195 | [17, 145, 115, 1, 146, 116], 1196 | [14, 74, 46, 21, 75, 47], 1197 | [29, 54, 24, 19, 55, 25], 1198 | [11, 45, 15, 46, 46, 16], 1199 | 1200 | // 34 1201 | [13, 145, 115, 6, 146, 116], 1202 | [14, 74, 46, 23, 75, 47], 1203 | [44, 54, 24, 7, 55, 25], 1204 | [59, 46, 16, 1, 47, 17], 1205 | 1206 | // 35 1207 | [12, 151, 121, 7, 152, 122], 1208 | [12, 75, 47, 26, 76, 48], 1209 | [39, 54, 24, 14, 55, 25], 1210 | [22, 45, 15, 41, 46, 16], 1211 | 1212 | // 36 1213 | [6, 151, 121, 14, 152, 122], 1214 | [6, 75, 47, 34, 76, 48], 1215 | [46, 54, 24, 10, 55, 25], 1216 | [2, 45, 15, 64, 46, 16], 1217 | 1218 | // 37 1219 | [17, 152, 122, 4, 153, 123], 1220 | [29, 74, 46, 14, 75, 47], 1221 | [49, 54, 24, 10, 55, 25], 1222 | [24, 45, 15, 46, 46, 16], 1223 | 1224 | // 38 1225 | [4, 152, 122, 18, 153, 123], 1226 | [13, 74, 46, 32, 75, 47], 1227 | [48, 54, 24, 14, 55, 25], 1228 | [42, 45, 15, 32, 46, 16], 1229 | 1230 | // 39 1231 | [20, 147, 117, 4, 148, 118], 1232 | [40, 75, 47, 7, 76, 48], 1233 | [43, 54, 24, 22, 55, 25], 1234 | [10, 45, 15, 67, 46, 16], 1235 | 1236 | // 40 1237 | [19, 148, 118, 6, 149, 119], 1238 | [18, 75, 47, 31, 76, 48], 1239 | [34, 54, 24, 34, 55, 25], 1240 | [20, 45, 15, 61, 46, 16] 1241 | ]; 1242 | 1243 | let qrRSBlock = function (totalCount, dataCount) { 1244 | let _this = {}; 1245 | _this.totalCount = totalCount; 1246 | _this.dataCount = dataCount; 1247 | return _this; 1248 | }; 1249 | 1250 | let _this = {}; 1251 | 1252 | let getRsBlockTable = function (typeNumber, errorCorrectLevel) { 1253 | 1254 | switch (errorCorrectLevel) { 1255 | case QRErrorCorrectLevel.L: 1256 | return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 0]; 1257 | case QRErrorCorrectLevel.M: 1258 | return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 1]; 1259 | case QRErrorCorrectLevel.Q: 1260 | return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 2]; 1261 | case QRErrorCorrectLevel.H: 1262 | return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 3]; 1263 | default: 1264 | return undefined; 1265 | } 1266 | }; 1267 | 1268 | _this.getRSBlocks = function (typeNumber, errorCorrectLevel) { 1269 | 1270 | let rsBlock = getRsBlockTable(typeNumber, errorCorrectLevel); 1271 | 1272 | if (typeof rsBlock == 'undefined') { 1273 | throw new Error('bad rs block @ typeNumber:' + typeNumber + 1274 | '/errorCorrectLevel:' + errorCorrectLevel); 1275 | } 1276 | 1277 | let length = rsBlock.length / 3; 1278 | 1279 | let list = []; 1280 | 1281 | for (let i = 0; i < length; i += 1) { 1282 | 1283 | let count = rsBlock[i * 3 + 0]; 1284 | let totalCount = rsBlock[i * 3 + 1]; 1285 | let dataCount = rsBlock[i * 3 + 2]; 1286 | 1287 | for (let j = 0; j < count; j += 1) { 1288 | list.push(qrRSBlock(totalCount, dataCount)); 1289 | } 1290 | } 1291 | 1292 | return list; 1293 | }; 1294 | 1295 | return _this; 1296 | }(); 1297 | 1298 | //--------------------------------------------------------------------- 1299 | // qrBitBuffer 1300 | //--------------------------------------------------------------------- 1301 | 1302 | let qrBitBuffer = function () { 1303 | 1304 | let _buffer = []; 1305 | let _length = 0; 1306 | 1307 | let _this = {}; 1308 | 1309 | _this.getBuffer = function () { 1310 | return _buffer; 1311 | }; 1312 | 1313 | _this.get = function (index) { 1314 | let bufIndex = Math.floor(index / 8); 1315 | return ((_buffer[bufIndex] >>> (7 - index % 8)) & 1) === 1; 1316 | }; 1317 | 1318 | _this.put = function (num, length) { 1319 | for (let i = 0; i < length; i += 1) { 1320 | _this.putBit(((num >>> (length - i - 1)) & 1) === 1); 1321 | } 1322 | }; 1323 | 1324 | _this.getLengthInBits = function () { 1325 | return _length; 1326 | }; 1327 | 1328 | _this.putBit = function (bit) { 1329 | 1330 | let bufIndex = Math.floor(_length / 8); 1331 | if (_buffer.length <= bufIndex) { 1332 | _buffer.push(0); 1333 | } 1334 | 1335 | if (bit) { 1336 | _buffer[bufIndex] |= (0x80 >>> (_length % 8)); 1337 | } 1338 | 1339 | _length += 1; 1340 | }; 1341 | 1342 | return _this; 1343 | }; 1344 | 1345 | //--------------------------------------------------------------------- 1346 | // qr8BitByte 1347 | //--------------------------------------------------------------------- 1348 | 1349 | let qr8BitByte = function (data) { 1350 | 1351 | let _mode = QRMode.MODE_8BIT_BYTE; 1352 | // let _data = data; 1353 | let _bytes = qrcode.stringToBytes(data); 1354 | 1355 | let _this = {}; 1356 | 1357 | _this.getMode = function () { 1358 | return _mode; 1359 | }; 1360 | 1361 | _this.getLength = function (buffer) { 1362 | return _bytes.length; 1363 | }; 1364 | 1365 | _this.write = function (buffer) { 1366 | for (let i = 0; i < _bytes.length; i += 1) { 1367 | buffer.put(_bytes[i], 8); 1368 | } 1369 | }; 1370 | 1371 | return _this; 1372 | }; 1373 | 1374 | //===================================================================== 1375 | // GIF Support etc. 1376 | // 1377 | 1378 | //--------------------------------------------------------------------- 1379 | // byteArrayOutputStream 1380 | //--------------------------------------------------------------------- 1381 | 1382 | let byteArrayOutputStream = function () { 1383 | 1384 | let _bytes = []; 1385 | 1386 | let _this = {}; 1387 | 1388 | _this.writeByte = function (b) { 1389 | _bytes.push(b & 0xff); 1390 | }; 1391 | 1392 | _this.writeShort = function (i) { 1393 | _this.writeByte(i); 1394 | _this.writeByte(i >>> 8); 1395 | }; 1396 | 1397 | _this.writeBytes = function (b, off, len) { 1398 | off = off || 0; 1399 | len = len || b.length; 1400 | for (let i = 0; i < len; i += 1) { 1401 | _this.writeByte(b[i + off]); 1402 | } 1403 | }; 1404 | 1405 | _this.writeString = function (s) { 1406 | for (let i = 0; i < s.length; i += 1) { 1407 | _this.writeByte(s.charCodeAt(i)); 1408 | } 1409 | }; 1410 | 1411 | _this.toByteArray = function () { 1412 | return _bytes; 1413 | }; 1414 | 1415 | _this.toString = function () { 1416 | let s = ''; 1417 | s += '['; 1418 | for (let i = 0; i < _bytes.length; i += 1) { 1419 | if (i > 0) { 1420 | s += ','; 1421 | } 1422 | s += _bytes[i]; 1423 | } 1424 | s += ']'; 1425 | return s; 1426 | }; 1427 | 1428 | return _this; 1429 | }; 1430 | 1431 | //--------------------------------------------------------------------- 1432 | // base64EncodeOutputStream 1433 | //--------------------------------------------------------------------- 1434 | 1435 | let base64EncodeOutputStream = function () { 1436 | 1437 | let _buffer = 0; 1438 | let _buflen = 0; 1439 | let _length = 0; 1440 | let _base64 = ''; 1441 | 1442 | let _this = {}; 1443 | 1444 | let writeEncoded = function (b) { 1445 | _base64 += String.fromCharCode(encode(b & 0x3f)); 1446 | }; 1447 | 1448 | let encode = function (n) { 1449 | if (n < 0) { 1450 | // error. 1451 | } else if (n < 26) { 1452 | return 0x41 + n; 1453 | } else if (n < 52) { 1454 | return 0x61 + (n - 26); 1455 | } else if (n < 62) { 1456 | return 0x30 + (n - 52); 1457 | } else if (n === 62) { 1458 | return 0x2b; 1459 | } else if (n === 63) { 1460 | return 0x2f; 1461 | } 1462 | throw new Error('n:' + n); 1463 | }; 1464 | 1465 | _this.writeByte = function (n) { 1466 | 1467 | _buffer = (_buffer << 8) | (n & 0xff); 1468 | _buflen += 8; 1469 | _length += 1; 1470 | 1471 | while (_buflen >= 6) { 1472 | writeEncoded(_buffer >>> (_buflen - 6)); 1473 | _buflen -= 6; 1474 | } 1475 | }; 1476 | 1477 | _this.flush = function () { 1478 | 1479 | if (_buflen > 0) { 1480 | writeEncoded(_buffer << (6 - _buflen)); 1481 | _buffer = 0; 1482 | _buflen = 0; 1483 | } 1484 | 1485 | if (_length % 3 !== 0) { 1486 | // padding 1487 | let padlen = 3 - _length % 3; 1488 | for (let i = 0; i < padlen; i += 1) { 1489 | _base64 += '='; 1490 | } 1491 | } 1492 | }; 1493 | 1494 | _this.toString = function () { 1495 | return _base64; 1496 | }; 1497 | 1498 | return _this; 1499 | }; 1500 | 1501 | //--------------------------------------------------------------------- 1502 | // base64DecodeInputStream 1503 | //--------------------------------------------------------------------- 1504 | 1505 | let base64DecodeInputStream = function (str) { 1506 | 1507 | let _str = str; 1508 | let _pos = 0; 1509 | let _buffer = 0; 1510 | let _buflen = 0; 1511 | 1512 | let _this = {}; 1513 | 1514 | _this.read = function () { 1515 | 1516 | while (_buflen < 8) { 1517 | 1518 | if (_pos >= _str.length) { 1519 | if (_buflen === 0) { 1520 | return -1; 1521 | } 1522 | throw new Error('unexpected end of file./' + _buflen); 1523 | } 1524 | 1525 | let c = _str.charAt(_pos); 1526 | _pos += 1; 1527 | 1528 | if (c === '=') { 1529 | _buflen = 0; 1530 | return -1; 1531 | } else if (c.match(/^\s$/)) { 1532 | // ignore if whitespace. 1533 | continue; 1534 | } 1535 | 1536 | _buffer = (_buffer << 6) | decode(c.charCodeAt(0)); 1537 | _buflen += 6; 1538 | } 1539 | 1540 | let n = (_buffer >>> (_buflen - 8)) & 0xff; 1541 | _buflen -= 8; 1542 | return n; 1543 | }; 1544 | 1545 | let decode = function (c) { 1546 | if (0x41 <= c && c <= 0x5a) { 1547 | return c - 0x41; 1548 | } else if (0x61 <= c && c <= 0x7a) { 1549 | return c - 0x61 + 26; 1550 | } else if (0x30 <= c && c <= 0x39) { 1551 | return c - 0x30 + 52; 1552 | } else if (c === 0x2b) { 1553 | return 62; 1554 | } else if (c === 0x2f) { 1555 | return 63; 1556 | } else { 1557 | throw new Error('c:' + c); 1558 | } 1559 | }; 1560 | 1561 | return _this; 1562 | }; 1563 | 1564 | //--------------------------------------------------------------------- 1565 | // gifImage (B/W) 1566 | //--------------------------------------------------------------------- 1567 | 1568 | let gifImage = function (width, height) { 1569 | 1570 | let _width = width; 1571 | let _height = height; 1572 | let _data = new Array(width * height); 1573 | 1574 | let _this = {}; 1575 | 1576 | _this.setPixel = function (x, y, pixel) { 1577 | _data[y * _width + x] = pixel; 1578 | }; 1579 | 1580 | _this.write = function (out) { 1581 | 1582 | //--------------------------------- 1583 | // GIF Signature 1584 | 1585 | out.writeString('GIF87a'); 1586 | 1587 | //--------------------------------- 1588 | // Screen Descriptor 1589 | 1590 | out.writeShort(_width); 1591 | out.writeShort(_height); 1592 | 1593 | out.writeByte(0x80); // 2bit 1594 | out.writeByte(0); 1595 | out.writeByte(0); 1596 | 1597 | //--------------------------------- 1598 | // Global Color Map 1599 | 1600 | // black 1601 | out.writeByte(0x00); 1602 | out.writeByte(0x00); 1603 | out.writeByte(0x00); 1604 | 1605 | // white 1606 | out.writeByte(0xff); 1607 | out.writeByte(0xff); 1608 | out.writeByte(0xff); 1609 | 1610 | //--------------------------------- 1611 | // Image Descriptor 1612 | 1613 | out.writeString(','); 1614 | out.writeShort(0); 1615 | out.writeShort(0); 1616 | out.writeShort(_width); 1617 | out.writeShort(_height); 1618 | out.writeByte(0); 1619 | 1620 | //--------------------------------- 1621 | // Local Color Map 1622 | 1623 | //--------------------------------- 1624 | // Raster Data 1625 | 1626 | let lzwMinCodeSize = 2; 1627 | let raster = getLZWRaster(lzwMinCodeSize); 1628 | 1629 | out.writeByte(lzwMinCodeSize); 1630 | 1631 | let offset = 0; 1632 | 1633 | while (raster.length - offset > 255) { 1634 | out.writeByte(255); 1635 | out.writeBytes(raster, offset, 255); 1636 | offset += 255; 1637 | } 1638 | 1639 | out.writeByte(raster.length - offset); 1640 | out.writeBytes(raster, offset, raster.length - offset); 1641 | out.writeByte(0x00); 1642 | 1643 | //--------------------------------- 1644 | // GIF Terminator 1645 | out.writeString(';'); 1646 | }; 1647 | 1648 | let bitOutputStream = function (out) { 1649 | 1650 | let _out = out; 1651 | let _bitLength = 0; 1652 | let _bitBuffer = 0; 1653 | 1654 | let _this = {}; 1655 | 1656 | _this.write = function (data, length) { 1657 | 1658 | if ((data >>> length) !== 0) { 1659 | throw new Error('length over'); 1660 | } 1661 | 1662 | while (_bitLength + length >= 8) { 1663 | _out.writeByte(0xff & ((data << _bitLength) | _bitBuffer)); 1664 | length -= (8 - _bitLength); 1665 | data >>>= (8 - _bitLength); 1666 | _bitBuffer = 0; 1667 | _bitLength = 0; 1668 | } 1669 | 1670 | _bitBuffer = (data << _bitLength) | _bitBuffer; 1671 | _bitLength = _bitLength + length; 1672 | }; 1673 | 1674 | _this.flush = function () { 1675 | if (_bitLength > 0) { 1676 | _out.writeByte(_bitBuffer); 1677 | } 1678 | }; 1679 | 1680 | return _this; 1681 | }; 1682 | 1683 | let getLZWRaster = function (lzwMinCodeSize) { 1684 | 1685 | let clearCode = 1 << lzwMinCodeSize; 1686 | let endCode = (1 << lzwMinCodeSize) + 1; 1687 | let bitLength = lzwMinCodeSize + 1; 1688 | 1689 | // Setup LZWTable 1690 | let table = lzwTable(); 1691 | 1692 | for (let i = 0; i < clearCode; i += 1) { 1693 | table.add(String.fromCharCode(i)); 1694 | } 1695 | table.add(String.fromCharCode(clearCode)); 1696 | table.add(String.fromCharCode(endCode)); 1697 | 1698 | let byteOut = byteArrayOutputStream(); 1699 | let bitOut = bitOutputStream(byteOut); 1700 | 1701 | // clear code 1702 | bitOut.write(clearCode, bitLength); 1703 | 1704 | let dataIndex = 0; 1705 | 1706 | let s = String.fromCharCode(_data[dataIndex]); 1707 | dataIndex += 1; 1708 | 1709 | while (dataIndex < _data.length) { 1710 | 1711 | let c = String.fromCharCode(_data[dataIndex]); 1712 | dataIndex += 1; 1713 | 1714 | if (table.contains(s + c)) { 1715 | 1716 | s = s + c; 1717 | 1718 | } else { 1719 | 1720 | bitOut.write(table.indexOf(s), bitLength); 1721 | 1722 | if (table.size() < 0xfff) { 1723 | 1724 | if (table.size() === (1 << bitLength)) { 1725 | bitLength += 1; 1726 | } 1727 | 1728 | table.add(s + c); 1729 | } 1730 | 1731 | s = c; 1732 | } 1733 | } 1734 | 1735 | bitOut.write(table.indexOf(s), bitLength); 1736 | 1737 | // end code 1738 | bitOut.write(endCode, bitLength); 1739 | 1740 | bitOut.flush(); 1741 | 1742 | return byteOut.toByteArray(); 1743 | }; 1744 | 1745 | let lzwTable = function () { 1746 | 1747 | let _map = {}; 1748 | let _size = 0; 1749 | 1750 | let _this = {}; 1751 | 1752 | _this.add = function (key) { 1753 | if (_this.contains(key)) { 1754 | throw new Error('dup key:' + key); 1755 | } 1756 | _map[key] = _size; 1757 | _size += 1; 1758 | }; 1759 | 1760 | _this.size = function () { 1761 | return _size; 1762 | }; 1763 | 1764 | _this.indexOf = function (key) { 1765 | return _map[key]; 1766 | }; 1767 | 1768 | _this.contains = function (key) { 1769 | return typeof _map[key] != 'undefined'; 1770 | }; 1771 | 1772 | return _this; 1773 | }; 1774 | 1775 | return _this; 1776 | }; 1777 | 1778 | let createImgTag = function (width, height, getPixel, alt) { 1779 | 1780 | let gif = gifImage(width, height); 1781 | for (let y = 0; y < height; y += 1) { 1782 | for (let x = 0; x < width; x += 1) { 1783 | gif.setPixel(x, y, getPixel(x, y)); 1784 | } 1785 | } 1786 | 1787 | let b = byteArrayOutputStream(); 1788 | gif.write(b); 1789 | 1790 | let base64 = base64EncodeOutputStream(); 1791 | let bytes = b.toByteArray(); 1792 | for (let i = 0; i < bytes.length; i += 1) { 1793 | base64.writeByte(bytes[i]); 1794 | } 1795 | base64.flush(); 1796 | 1797 | let img = ''; 1798 | img += '