├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── examples.html ├── index.html ├── instructions.html ├── package-lock.json ├── package.json ├── public ├── dist │ ├── 255db585f8a0eb28a1e4.svg │ ├── 2b3e1faf89f94a483539.png │ ├── 416d91365b44e4b4f477.png │ ├── 7a5643c730ac934189f5.svg │ ├── 7be8e3b179927ab7218d.svg │ ├── 8f2c4d11474275fbc161.png │ ├── css │ │ └── main.css │ └── js │ │ ├── app.js │ │ ├── main.js │ │ ├── main.js.LICENSE.txt │ │ ├── processor.js │ │ └── processor.js.LICENSE.txt ├── examples │ └── mt_fuji_8129.png ├── im │ ├── actual_error_grayscale.png │ ├── actual_error_terrarium.png │ ├── aizuwakamatsu_none.png │ ├── aizuwakamatsu_regular.png │ ├── aizuwakamatsu_smart.png │ ├── arrow.png │ ├── example_grand_canyon.png │ ├── grand_canyon_desserty.png │ ├── grand_canyon_desserty_2.png │ ├── grand_canyon_heightmap_sm.png │ ├── icon-down.png │ ├── icon-left.png │ ├── icon-marker.png │ ├── icon-right.png │ ├── icon-up.png │ ├── icons.ai │ ├── latlng_inputs.png │ ├── mapview.png │ ├── marker.png │ ├── mt_fuji_none.png │ ├── mt_fuji_regular.png │ └── mt_fuji_smart.png ├── test │ ├── 13-4092-2724.png │ ├── 13-4093-2724.png │ ├── 13-4094-2724.png │ ├── 13-4095-2724.png │ ├── 13-4096-2724.png │ ├── 13-4097-2724.png │ ├── bw16bit.png │ ├── terrarium.png │ ├── tiny16bitpng.png │ ├── tiny8bitpng.png │ └── tinyterrariumpng.png └── video │ └── grand_canyon.mp4 ├── rights.html ├── src ├── UPNG.d.ts ├── UPNG.js ├── app.ts ├── buffer.ts ├── helpers.ts ├── im │ └── Octicons-mark-github.svg ├── image.ts ├── main.ts ├── nextzen.ts ├── png.ts ├── processor.ts ├── sass │ ├── _overrides.scss │ ├── _variables.scss │ └── main.scss ├── templates │ ├── footer.html │ └── header.html └── types.d.ts ├── tsconfig.json ├── webpack.config.js ├── webpack.dev.config.js └── webpack.production.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015-ie"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Files 2 | 3 | .DS_Store 4 | .ruby-version 5 | npm-debug.log 6 | 7 | # Folders 8 | 9 | .idea/ 10 | .jekyll-cache/ 11 | .sass-cache 12 | _gh_pages 13 | _site 14 | node_modules 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Jeremy Thomas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unreal Engine 16 Bit Grayscale PNG Heightmap Generator 2 | 3 | This is the source code the for the website that allows you to generate 16 bit grayscale PNG heightmaps easily in browser. 4 | 5 | You can find the website here: 6 | 7 | [https://manticorp.github.io/unrealheightmap/](https://manticorp.github.io/unrealheightmap/) 8 | 9 | If you feel like supporting this project, please consider donating! 10 | 11 | [https://ko-fi.com/harrymustoeplayfair](https://ko-fi.com/harrymustoeplayfair) 12 | 13 | We have started hitting the free tier limits from some map services, so the site *will not work* until we get some funding to up these limits. 14 | 15 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/L4L212G6M7) 16 | -------------------------------------------------------------------------------- /examples.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 Bit Grayscale PNG Heightmap Generator for Unreal Engine 8 | 9 | 10 | 11 |
12 |
13 |

Examples

14 |

Below are a selection of examples made using this tools in Unreal Engine 5.1

15 |
16 |

Grand Canyon (64km)

17 |

Find it yourself here

18 |

Images

19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |

Video

28 |
29 |
30 |
31 |
32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 Bit Grayscale PNG Heightmap Generator for Unreal Engine 8 | 12 | 13 | 14 | 15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /instructions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 Bit Grayscale PNG Heightmap Generator for Unreal Engine 8 | 9 | 10 | 11 |
12 |
13 |

16 Bit PNG Heightmap Generator

14 |
15 |
16 |

Aim

17 |

The aim of this project is to generate 16 bit heightmap data for use in software such as Unreal Engine 5

18 |

High quality height data is available from Mapzen's global elevation service.

19 |
20 |
21 |

How to Export

22 |
23 |
24 |
25 | 26 |
27 | Enter your latitude/longitude values here 28 |
29 |
30 |
31 |
32 |

Using the map at the top of the screen, choose a location. You can also manually type in a latitude and longitude.

33 |

The orange rectangle shows the size of the exported image, and what will be included.

34 |

The size of the exported area depends on the output zoom and the output size in pixels.

35 |

After clicking 'Generate' a 16 bit PNG should await you.

36 |

Explanation of different options

37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
LatitudeThe Latitude at the centre of the image
LongitudeThe Longitude at the centre of the image
ZoomThe zoom level in the preview
Output ZoomThe zoom level to use for the output
Map Preview TypeThe tiles to use in the preview at the top of the page
Output Width (px)The output width of the entire image in pixels
Output Height (px)The output height of the entire image in pixels
Default UE5 SizesA preset list of useful sizes for generating Unreal Engine 5 terrain
Normalisation ModeThe normalisation mode to use. See below for more information
Norm FromOverrides the normlisation "from" parameter. Can be useful when low height data is funky, e.g. at the coast (just use 0)
Norm ToOverrides the normlisation "to" parameter
50 |
51 |
52 |
53 |

Top Tip: Copy & Paste

54 |

You can now copy and paste straight onto the page any lat/lng string. Acceptable formats include:

55 |
    56 |
  • 03°04′33″S 37°21′12″E
  • 57 |
  • 3.0674° S, 37.3556° E
  • 58 |
  • -3.0674, 37.3556
  • 59 |
60 |
61 |
62 |
63 |
64 | 65 |
66 | The orange box shows the area that will be exported. 67 |
68 |
69 |
70 |
71 |

Output

72 |

The output will be a 16 bit grayscale PNG file with the gray levels representing the height of the ground at that location.

73 |

16 bits gives 216 levels of detail between the lowest and highest point. That's 65536 levels - enough to detail from sea level to the top of mount everest (8849m) in roughly 13cm intervals.

74 |

For comparison, 8 bit detail only give 28 levels of detail, which is only 256 levels!

75 |

The mapzen data is actually much more detailed than 16 bit (it has 24 bits of data, with a fidelity of about 4mm in the data itself, although the measurements are probably only accurate to 1m maximum)

76 |
77 |
78 |

Normalisation Options

79 |

When using the data for import into Unreal Engine 5, it can be useful to normalise the data so that you're using the full 16 bit range of the file.

80 |
81 |
None
82 |
Do not perform any normalisation. The pixel value represents the height in m (note that negative values will be 0)
83 |
Regular
84 |
Scale the height values in the data to 0 to 65536, making full use of all 16 bits of the file's range
85 |
Smart
86 |
Unfortunately, there are some errors in the Mapzen data that throw off the normalisation - 0 values or high values that skew the normalisation completely. Using this method, we take a 99.9% window of the data to get rid of outliers. However, if the actual max/min are within 1 standard deviation of the windowed max/min then we use the actual values to retain the min/max data.
87 |
88 |

Examples

89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 |
Normalisation ModeNoneRegularSmart
Aizuwakamatsu
Without Source Error
Mt Fuji
With Source Error
113 |

Here is an example of an error in the source data:

114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |

FAQs

125 |
126 | There is a weird artefact in my image - why is that? 127 | Unfortunately there are some errors in the source data - please check the 'Terrarium' preview type at the same zoom level and see if the artefacts are also present. 128 |
129 |
130 | How do I use these images? 131 | You can find information how to use the 16 bit png images for landscapes on the Unreal Engine website 132 |
133 |
134 | Why wouldn't I use the heightmapper website output? 135 | Unfortunately, because of the way it generates images, the output from heightmapper is 1) noisy, 2) only 8 bit, resulting in poor quality landscapes and jagged areas. In 8 bit, a 1000m tall mountain would have jagged edges every 4m in height. 136 |
137 |
138 |
139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "png-16-browser", 3 | "version": "0.0.5", 4 | "description": "16 Bit PNG", 5 | "private": true, 6 | "keywords": [], 7 | "author": "Harry Mustoe-Playfair ", 8 | "repository": { 9 | "type": "git" 10 | }, 11 | "license": "MIT", 12 | "devDependencies": { 13 | "autoprefixer": "^10.4.7", 14 | "babel-cli": "^6.26.0", 15 | "babel-preset-env": "^1.7.0", 16 | "babel-preset-es2015-ie": "^6.7.0", 17 | "bulma": "^0.9.4", 18 | "css-loader": "^6.7.3", 19 | "html-loader": "^4.2.0", 20 | "mini-css-extract-plugin": "^2.7.2", 21 | "node-sass": "^8.0.0", 22 | "npm-run-all": "^4.1.5", 23 | "postcss-cli": "^9.1.0", 24 | "sass": "^1.51.0", 25 | "sass-loader": "^13.2.0", 26 | "style-loader": "^3.3.1", 27 | "ts-loader": "^9.4.2", 28 | "typescript": "^4.9.5", 29 | "webpack": "^5.75.0", 30 | "webpack-cli": "^5.0.1", 31 | "webpack-merge": "^5.8.0" 32 | }, 33 | "scripts": { 34 | "watch": "webpack --config webpack.dev.config.js --watch", 35 | "build": "webpack --config webpack.dev.config.js", 36 | "author": "webpack --config webpack.production.config.js" 37 | }, 38 | "dependencies": { 39 | "@types/jquery": "^3.5.16", 40 | "@types/leaflet": "^1.9.1", 41 | "@types/leaflet-providers": "^1.2.1", 42 | "@types/throttle-debounce": "^5.0.0", 43 | "comlink": "^4.4.1", 44 | "jquery": "^3.6.3", 45 | "leaflet": "^1.9.4", 46 | "leaflet-providers": "^2.0.0", 47 | "pako": "^2.1.0", 48 | "throttle-debounce": "^5.0.0", 49 | "upng-js": "^2.1.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /public/dist/255db585f8a0eb28a1e4.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/dist/2b3e1faf89f94a483539.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticorp/unrealheightmap/b2e1a4bd30bfa2b8ee8859ba65fdfbf91634379e/public/dist/2b3e1faf89f94a483539.png -------------------------------------------------------------------------------- /public/dist/416d91365b44e4b4f477.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticorp/unrealheightmap/b2e1a4bd30bfa2b8ee8859ba65fdfbf91634379e/public/dist/416d91365b44e4b4f477.png -------------------------------------------------------------------------------- /public/dist/7a5643c730ac934189f5.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/dist/7be8e3b179927ab7218d.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/dist/8f2c4d11474275fbc161.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticorp/unrealheightmap/b2e1a4bd30bfa2b8ee8859ba65fdfbf91634379e/public/dist/8f2c4d11474275fbc161.png -------------------------------------------------------------------------------- /public/dist/js/main.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* @preserve 2 | * Leaflet 1.9.4, a JS library for interactive maps. https://leafletjs.com 3 | * (c) 2010-2023 Vladimir Agafonkin, (c) 2010-2011 CloudMade 4 | */ 5 | 6 | /*! 7 | * jQuery JavaScript Library v3.7.1 8 | * https://jquery.com/ 9 | * 10 | * Copyright OpenJS Foundation and other contributors 11 | * Released under the MIT license 12 | * https://jquery.org/license 13 | * 14 | * Date: 2023-08-28T13:37Z 15 | */ 16 | 17 | /** 18 | * @license 19 | * Copyright 2019 Google LLC 20 | * SPDX-License-Identifier: Apache-2.0 21 | */ 22 | -------------------------------------------------------------------------------- /public/dist/js/processor.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2019 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | -------------------------------------------------------------------------------- /public/examples/mt_fuji_8129.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticorp/unrealheightmap/b2e1a4bd30bfa2b8ee8859ba65fdfbf91634379e/public/examples/mt_fuji_8129.png -------------------------------------------------------------------------------- /public/im/actual_error_grayscale.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticorp/unrealheightmap/b2e1a4bd30bfa2b8ee8859ba65fdfbf91634379e/public/im/actual_error_grayscale.png -------------------------------------------------------------------------------- /public/im/actual_error_terrarium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticorp/unrealheightmap/b2e1a4bd30bfa2b8ee8859ba65fdfbf91634379e/public/im/actual_error_terrarium.png -------------------------------------------------------------------------------- /public/im/aizuwakamatsu_none.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticorp/unrealheightmap/b2e1a4bd30bfa2b8ee8859ba65fdfbf91634379e/public/im/aizuwakamatsu_none.png -------------------------------------------------------------------------------- /public/im/aizuwakamatsu_regular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticorp/unrealheightmap/b2e1a4bd30bfa2b8ee8859ba65fdfbf91634379e/public/im/aizuwakamatsu_regular.png -------------------------------------------------------------------------------- /public/im/aizuwakamatsu_smart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticorp/unrealheightmap/b2e1a4bd30bfa2b8ee8859ba65fdfbf91634379e/public/im/aizuwakamatsu_smart.png -------------------------------------------------------------------------------- /public/im/arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticorp/unrealheightmap/b2e1a4bd30bfa2b8ee8859ba65fdfbf91634379e/public/im/arrow.png -------------------------------------------------------------------------------- /public/im/example_grand_canyon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticorp/unrealheightmap/b2e1a4bd30bfa2b8ee8859ba65fdfbf91634379e/public/im/example_grand_canyon.png -------------------------------------------------------------------------------- /public/im/grand_canyon_desserty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticorp/unrealheightmap/b2e1a4bd30bfa2b8ee8859ba65fdfbf91634379e/public/im/grand_canyon_desserty.png -------------------------------------------------------------------------------- /public/im/grand_canyon_desserty_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticorp/unrealheightmap/b2e1a4bd30bfa2b8ee8859ba65fdfbf91634379e/public/im/grand_canyon_desserty_2.png -------------------------------------------------------------------------------- /public/im/grand_canyon_heightmap_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticorp/unrealheightmap/b2e1a4bd30bfa2b8ee8859ba65fdfbf91634379e/public/im/grand_canyon_heightmap_sm.png -------------------------------------------------------------------------------- /public/im/icon-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticorp/unrealheightmap/b2e1a4bd30bfa2b8ee8859ba65fdfbf91634379e/public/im/icon-down.png -------------------------------------------------------------------------------- /public/im/icon-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticorp/unrealheightmap/b2e1a4bd30bfa2b8ee8859ba65fdfbf91634379e/public/im/icon-left.png -------------------------------------------------------------------------------- /public/im/icon-marker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticorp/unrealheightmap/b2e1a4bd30bfa2b8ee8859ba65fdfbf91634379e/public/im/icon-marker.png -------------------------------------------------------------------------------- /public/im/icon-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticorp/unrealheightmap/b2e1a4bd30bfa2b8ee8859ba65fdfbf91634379e/public/im/icon-right.png -------------------------------------------------------------------------------- /public/im/icon-up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticorp/unrealheightmap/b2e1a4bd30bfa2b8ee8859ba65fdfbf91634379e/public/im/icon-up.png -------------------------------------------------------------------------------- /public/im/icons.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticorp/unrealheightmap/b2e1a4bd30bfa2b8ee8859ba65fdfbf91634379e/public/im/icons.ai -------------------------------------------------------------------------------- /public/im/latlng_inputs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticorp/unrealheightmap/b2e1a4bd30bfa2b8ee8859ba65fdfbf91634379e/public/im/latlng_inputs.png -------------------------------------------------------------------------------- /public/im/mapview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticorp/unrealheightmap/b2e1a4bd30bfa2b8ee8859ba65fdfbf91634379e/public/im/mapview.png -------------------------------------------------------------------------------- /public/im/marker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticorp/unrealheightmap/b2e1a4bd30bfa2b8ee8859ba65fdfbf91634379e/public/im/marker.png -------------------------------------------------------------------------------- /public/im/mt_fuji_none.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticorp/unrealheightmap/b2e1a4bd30bfa2b8ee8859ba65fdfbf91634379e/public/im/mt_fuji_none.png -------------------------------------------------------------------------------- /public/im/mt_fuji_regular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticorp/unrealheightmap/b2e1a4bd30bfa2b8ee8859ba65fdfbf91634379e/public/im/mt_fuji_regular.png -------------------------------------------------------------------------------- /public/im/mt_fuji_smart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticorp/unrealheightmap/b2e1a4bd30bfa2b8ee8859ba65fdfbf91634379e/public/im/mt_fuji_smart.png -------------------------------------------------------------------------------- /public/test/13-4092-2724.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticorp/unrealheightmap/b2e1a4bd30bfa2b8ee8859ba65fdfbf91634379e/public/test/13-4092-2724.png -------------------------------------------------------------------------------- /public/test/13-4093-2724.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticorp/unrealheightmap/b2e1a4bd30bfa2b8ee8859ba65fdfbf91634379e/public/test/13-4093-2724.png -------------------------------------------------------------------------------- /public/test/13-4094-2724.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticorp/unrealheightmap/b2e1a4bd30bfa2b8ee8859ba65fdfbf91634379e/public/test/13-4094-2724.png -------------------------------------------------------------------------------- /public/test/13-4095-2724.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticorp/unrealheightmap/b2e1a4bd30bfa2b8ee8859ba65fdfbf91634379e/public/test/13-4095-2724.png -------------------------------------------------------------------------------- /public/test/13-4096-2724.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticorp/unrealheightmap/b2e1a4bd30bfa2b8ee8859ba65fdfbf91634379e/public/test/13-4096-2724.png -------------------------------------------------------------------------------- /public/test/13-4097-2724.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticorp/unrealheightmap/b2e1a4bd30bfa2b8ee8859ba65fdfbf91634379e/public/test/13-4097-2724.png -------------------------------------------------------------------------------- /public/test/bw16bit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticorp/unrealheightmap/b2e1a4bd30bfa2b8ee8859ba65fdfbf91634379e/public/test/bw16bit.png -------------------------------------------------------------------------------- /public/test/terrarium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticorp/unrealheightmap/b2e1a4bd30bfa2b8ee8859ba65fdfbf91634379e/public/test/terrarium.png -------------------------------------------------------------------------------- /public/test/tiny16bitpng.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticorp/unrealheightmap/b2e1a4bd30bfa2b8ee8859ba65fdfbf91634379e/public/test/tiny16bitpng.png -------------------------------------------------------------------------------- /public/test/tiny8bitpng.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticorp/unrealheightmap/b2e1a4bd30bfa2b8ee8859ba65fdfbf91634379e/public/test/tiny8bitpng.png -------------------------------------------------------------------------------- /public/test/tinyterrariumpng.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticorp/unrealheightmap/b2e1a4bd30bfa2b8ee8859ba65fdfbf91634379e/public/test/tinyterrariumpng.png -------------------------------------------------------------------------------- /public/video/grand_canyon.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manticorp/unrealheightmap/b2e1a4bd30bfa2b8ee8859ba65fdfbf91634379e/public/video/grand_canyon.mp4 -------------------------------------------------------------------------------- /rights.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 Bit Grayscale PNG Heightmap Generator for Unreal Engine 8 | 9 | 10 | 11 |
12 |
13 |

Map License Information

14 |
15 |
16 |

Heightmap Data

17 |

The heightmap data comes from NextZen, which was set up when Mapzen shut down.

18 |

It is not 100% clear what the usage rights for the heightmaps are now. This was the "rights" page that appeared on the Mapzen site.

19 |

This was their attribution guidance, specifically.

20 |

If in doubt, the following snippet should be included on your project:

21 |

© Mapzen, OpenStreetMap, and others.

22 |
© <a href="https://www.mapzen.com/rights">Mapzen</a>,  <a href="https://openstreetmap.org/copyright">OpenStreetMap</a> , and <a href="https://www.mapzen.com/rights/#services-and-data-sources">others</a>.
23 |
24 |
25 |

Other Maps

26 |

All other maps should have a link to the copyright information on the bottom left of the map display.

27 |

This data should be used for the "Export Albedo" type map data.

28 |

Most map providers can be found on the leaflet providers page.

29 |
30 |
31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/UPNG.d.ts: -------------------------------------------------------------------------------- 1 | type TypedArray = 2 | | Int8Array 3 | | Uint8Array 4 | | Uint8ClampedArray 5 | | Int16Array 6 | | Uint16Array 7 | | Int32Array 8 | | Uint32Array 9 | | Float32Array 10 | | Float64Array; 11 | 12 | declare namespace UPNG { 13 | export interface ImageFrameRect { 14 | x: number; 15 | y: number; 16 | width: number; 17 | height: number; 18 | } 19 | 20 | export interface ImageFrame { 21 | rect: ImageFrameRect; 22 | delay: number; 23 | dispose: number; 24 | blend: number; 25 | } 26 | 27 | export interface ImageTabACTL { 28 | num_frames: number; 29 | num_plays: number; 30 | } 31 | 32 | export interface ImageTabText { 33 | [key: string]: string; 34 | } 35 | 36 | export interface ImageTabs { 37 | acTL?: ImageTabACTL | undefined; 38 | pHYs?: number[] | undefined; 39 | cHRM?: number[] | undefined; 40 | tEXt?: ImageTabText | undefined; 41 | iTXt?: ImageTabText | undefined; 42 | PLTE?: number[] | undefined; 43 | hIST?: number[] | undefined; 44 | tRNS?: (number | number[]) | undefined; // Depends on ctype 45 | gAMA?: number | undefined; 46 | sRGB?: number | undefined; 47 | bKGD?: (number | number[]) | undefined; // Depends on ctype 48 | } 49 | 50 | export interface Image { 51 | width: number; 52 | height: number; 53 | depth: number; 54 | ctype: number; 55 | frames: ImageFrame[]; 56 | tabs: ImageTabs; 57 | data: Uint8Array; 58 | } 59 | 60 | export interface QuantizeResult { 61 | abuf: ArrayBuffer; 62 | inds: Uint8Array; 63 | // Type is complicated and I am too lazy to work it out right now, sorry! 64 | plte: any[]; 65 | } 66 | 67 | export function encode( 68 | imgs: Uint8Array[], 69 | w: number, 70 | h: number, 71 | cnum: number, 72 | dels?: number[] 73 | ): ArrayBuffer; 74 | 75 | export function encodeLL( 76 | bufs: Uint8Array[], 77 | w: number, 78 | h: number, 79 | cc: number, 80 | ac: number, 81 | depth: number, 82 | dels?: number[], 83 | tabs?: ImageTabs 84 | ): ArrayBuffer; 85 | 86 | export function decode(buffer: ArrayBuffer | TypedArray): Image; 87 | export function decodeImage (data : Uint8Array, w : number, h : number, out : Image) : Uint8Array; 88 | export function toRGBA8(out: Image): Uint8Array[]; 89 | export function quantize(data: ArrayBuffer, psize: number): QuantizeResult; 90 | } 91 | export default UPNG; -------------------------------------------------------------------------------- /src/UPNG.js: -------------------------------------------------------------------------------- 1 | const pako = require('pako'); 2 | 3 | const UPNG = (function () { 4 | var _bin = { 5 | nextZero: function (data, p) { while (data[p] != 0) p++; return p; }, 6 | readUshort: function (buff, p) { return (buff[p] << 8) | buff[p + 1]; }, 7 | writeUshort: function (buff, p, n) { buff[p] = (n >> 8) & 255; buff[p + 1] = n & 255; }, 8 | readUint: function (buff, p) { return (buff[p] * (256 * 256 * 256)) + ((buff[p + 1] << 16) | (buff[p + 2] << 8) | buff[p + 3]); }, 9 | writeUint: function (buff, p, n) { buff[p] = (n >> 24) & 255; buff[p + 1] = (n >> 16) & 255; buff[p + 2] = (n >> 8) & 255; buff[p + 3] = n & 255; }, 10 | readASCII: function (buff, p, l) { let s = ''; for (let i = 0; i < l; i++) s += String.fromCharCode(buff[p + i]); return s; }, 11 | writeASCII: function (data, p, s) { for (let i = 0; i < s.length; i++) data[p + i] = s.charCodeAt(i); }, 12 | readBytes: function (buff, p, l) { const arr = []; for (let i = 0; i < l; i++) arr.push(buff[p + i]); return arr; }, 13 | pad: function (n) { return n.length < 2 ? '0' + n : n; }, 14 | readUTF8: function (buff, p, l) { 15 | let s = ''; let ns; 16 | for (let i = 0; i < l; i++) s += '%' + _bin.pad(buff[p + i].toString(16)); 17 | try { ns = decodeURIComponent(s); } catch (e) { return _bin.readASCII(buff, p, l); } 18 | return ns; 19 | } 20 | }; 21 | 22 | function toRGBA8 (out) { 23 | const w = out.width; const h = out.height; 24 | if (out.tabs.acTL == null) return [decodeImage(out.data, w, h, out).buffer]; 25 | 26 | const frms = []; 27 | if (out.frames[0].data == null) out.frames[0].data = out.data; 28 | 29 | const len = w * h * 4; const img = new Uint8Array(len); const empty = new Uint8Array(len); const prev = new Uint8Array(len); 30 | for (let i = 0; i < out.frames.length; i++) { 31 | const frm = out.frames[i]; 32 | const fx = frm.rect.x; const fy = frm.rect.y; const fw = frm.rect.width; const fh = frm.rect.height; 33 | const fdata = decodeImage(frm.data, fw, fh, out); 34 | 35 | if (i != 0) for (var j = 0; j < len; j++) prev[j] = img[j]; 36 | 37 | if (frm.blend == 0) _copyTile(fdata, fw, fh, img, w, h, fx, fy, 0); 38 | else if (frm.blend == 1) _copyTile(fdata, fw, fh, img, w, h, fx, fy, 1); 39 | 40 | frms.push(img.buffer.slice(0)); 41 | 42 | if (frm.dispose == 0) {} else if (frm.dispose == 1) _copyTile(empty, fw, fh, img, w, h, fx, fy, 0); 43 | else if (frm.dispose == 2) for (var j = 0; j < len; j++) img[j] = prev[j]; 44 | } 45 | return frms; 46 | } 47 | function decodeImage (data, w, h, out) { 48 | const area = w * h; const bpp = _getBPP(out); 49 | const bpl = Math.ceil(w * bpp / 8); // bytes per line 50 | 51 | const bf = new Uint8Array(area * 4); const bf32 = new Uint32Array(bf.buffer); 52 | const ctype = out.ctype; const depth = out.depth; 53 | const rs = _bin.readUshort; 54 | 55 | // console.log(ctype, depth); 56 | const time = Date.now(); 57 | 58 | if (ctype == 6) { // RGB + alpha 59 | const qarea = area << 2; 60 | if (depth == 8) for (var i = 0; i < qarea; i += 4) { bf[i] = data[i]; bf[i + 1] = data[i + 1]; bf[i + 2] = data[i + 2]; bf[i + 3] = data[i + 3]; } 61 | if (depth == 16) for (var i = 0; i < qarea; i++) { bf[i] = data[i << 1]; } 62 | } else if (ctype == 2) { // RGB 63 | const ts = out.tabs.tRNS; 64 | if (ts == null) { 65 | if (depth == 8) for (var i = 0; i < area; i++) { var ti = i * 3; bf32[i] = (255 << 24) | (data[ti + 2] << 16) | (data[ti + 1] << 8) | data[ti]; } 66 | if (depth == 16) for (var i = 0; i < area; i++) { var ti = i * 6; bf32[i] = (255 << 24) | (data[ti + 4] << 16) | (data[ti + 2] << 8) | data[ti]; } 67 | } else { 68 | var tr = ts[0]; const tg = ts[1]; const tb = ts[2]; 69 | if (depth == 8) { 70 | for (var i = 0; i < area; i++) { 71 | var qi = i << 2; var ti = i * 3; bf32[i] = (255 << 24) | (data[ti + 2] << 16) | (data[ti + 1] << 8) | data[ti]; 72 | if (data[ti] == tr && data[ti + 1] == tg && data[ti + 2] == tb) bf[qi + 3] = 0; 73 | } 74 | } 75 | if (depth == 16) { 76 | for (var i = 0; i < area; i++) { 77 | var qi = i << 2; var ti = i * 6; bf32[i] = (255 << 24) | (data[ti + 4] << 16) | (data[ti + 2] << 8) | data[ti]; 78 | if (rs(data, ti) == tr && rs(data, ti + 2) == tg && rs(data, ti + 4) == tb) bf[qi + 3] = 0; 79 | } 80 | } 81 | } 82 | } else if (ctype == 3) { // palette 83 | const p = out.tabs.PLTE; const ap = out.tabs.tRNS; const tl = ap ? ap.length : 0; 84 | // console.log(p, ap); 85 | if (depth == 1) { 86 | for (var y = 0; y < h; y++) { 87 | var s0 = y * bpl; var t0 = y * w; 88 | for (var i = 0; i < w; i++) { var qi = (t0 + i) << 2; var j = ((data[s0 + (i >> 3)] >> (7 - ((i & 7) << 0))) & 1); var cj = 3 * j; bf[qi] = p[cj]; bf[qi + 1] = p[cj + 1]; bf[qi + 2] = p[cj + 2]; bf[qi + 3] = (j < tl) ? ap[j] : 255; } 89 | } 90 | } 91 | if (depth == 2) { 92 | for (var y = 0; y < h; y++) { 93 | var s0 = y * bpl; var t0 = y * w; 94 | for (var i = 0; i < w; i++) { var qi = (t0 + i) << 2; var j = ((data[s0 + (i >> 2)] >> (6 - ((i & 3) << 1))) & 3); var cj = 3 * j; bf[qi] = p[cj]; bf[qi + 1] = p[cj + 1]; bf[qi + 2] = p[cj + 2]; bf[qi + 3] = (j < tl) ? ap[j] : 255; } 95 | } 96 | } 97 | if (depth == 4) { 98 | for (var y = 0; y < h; y++) { 99 | var s0 = y * bpl; var t0 = y * w; 100 | for (var i = 0; i < w; i++) { var qi = (t0 + i) << 2; var j = ((data[s0 + (i >> 1)] >> (4 - ((i & 1) << 2))) & 15); var cj = 3 * j; bf[qi] = p[cj]; bf[qi + 1] = p[cj + 1]; bf[qi + 2] = p[cj + 2]; bf[qi + 3] = (j < tl) ? ap[j] : 255; } 101 | } 102 | } 103 | if (depth == 8) for (var i = 0; i < area; i++) { var qi = i << 2; var j = data[i]; var cj = 3 * j; bf[qi] = p[cj]; bf[qi + 1] = p[cj + 1]; bf[qi + 2] = p[cj + 2]; bf[qi + 3] = (j < tl) ? ap[j] : 255; } 104 | } else if (ctype == 4) { // gray + alpha 105 | if (depth == 8) for (var i = 0; i < area; i++) { var qi = i << 2; var di = i << 1; var gr = data[di]; bf[qi] = gr; bf[qi + 1] = gr; bf[qi + 2] = gr; bf[qi + 3] = data[di + 1]; } 106 | if (depth == 16) for (var i = 0; i < area; i++) { var qi = i << 2; var di = i << 2; var gr = data[di]; bf[qi] = gr; bf[qi + 1] = gr; bf[qi + 2] = gr; bf[qi + 3] = data[di + 2]; } 107 | } else if (ctype == 0) { // gray 108 | var tr = out.tabs.tRNS ? out.tabs.tRNS : -1; 109 | for (var y = 0; y < h; y++) { 110 | const off = y * bpl; const to = y * w; 111 | if (depth == 1) for (var x = 0; x < w; x++) { var gr = 255 * ((data[off + (x >>> 3)] >>> (7 - ((x & 7)))) & 1); var al = (gr == tr * 255) ? 0 : 255; bf32[to + x] = (al << 24) | (gr << 16) | (gr << 8) | gr; } 112 | else if (depth == 2) for (var x = 0; x < w; x++) { var gr = 85 * ((data[off + (x >>> 2)] >>> (6 - ((x & 3) << 1))) & 3); var al = (gr == tr * 85) ? 0 : 255; bf32[to + x] = (al << 24) | (gr << 16) | (gr << 8) | gr; } 113 | else if (depth == 4) for (var x = 0; x < w; x++) { var gr = 17 * ((data[off + (x >>> 1)] >>> (4 - ((x & 1) << 2))) & 15); var al = (gr == tr * 17) ? 0 : 255; bf32[to + x] = (al << 24) | (gr << 16) | (gr << 8) | gr; } 114 | else if (depth == 8) for (var x = 0; x < w; x++) { var gr = data[off + x]; var al = (gr == tr) ? 0 : 255; bf32[to + x] = (al << 24) | (gr << 16) | (gr << 8) | gr; } 115 | else if (depth == 16) 116 | for (var x = 0; x < w; x++) { 117 | var gr = data[off + (x << 1)]; 118 | var al = (rs(data, off + (x << 1)) == tr) ? 0 : 255; 119 | bf32[to + x] = (al << 24) | (gr << 16) | (gr << 8) | gr; 120 | } 121 | } 122 | } 123 | // console.log(Date.now()-time); 124 | return bf; 125 | } 126 | 127 | function decode (buff) { 128 | const data = new Uint8Array(buff); let offset = 8; const bin = _bin; const rUs = bin.readUshort; const rUi = bin.readUint; 129 | const out = { tabs: {}, frames: [] }; 130 | const dd = new Uint8Array(data.length); let doff = 0; // put all IDAT data into it 131 | let fd; let foff = 0; // frames 132 | 133 | const mgck = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]; 134 | for (var i = 0; i < 8; i++) if (data[i] != mgck[i]) throw 'The input is not a PNG file!'; 135 | 136 | while (offset < data.length) { 137 | const len = bin.readUint(data, offset); offset += 4; 138 | const type = bin.readASCII(data, offset, 4); offset += 4; 139 | // console.log(type,len); 140 | 141 | if (type == 'IHDR') { _IHDR(data, offset, out); } else if (type == 'iCCP') { 142 | var off = offset; while (data[off] != 0) off++; 143 | const nam = bin.readASCII(data, offset, off - offset); 144 | const cpr = data[off + 1]; 145 | const fil = data.slice(off + 2, offset + len); 146 | let res = null; 147 | try { res = _inflate(fil); } catch (e) { res = inflateRaw(fil); } 148 | out.tabs[type] = res; 149 | } else if (type == 'CgBI') { out.tabs[type] = data.slice(offset, offset + 4); } else if (type == 'IDAT') { 150 | for (var i = 0; i < len; i++) dd[doff + i] = data[offset + i]; 151 | doff += len; 152 | } else if (type == 'acTL') { 153 | out.tabs[type] = { num_frames: rUi(data, offset), num_plays: rUi(data, offset + 4) }; 154 | fd = new Uint8Array(data.length); 155 | } else if (type == 'fcTL') { 156 | if (foff != 0) { 157 | var fr = out.frames[out.frames.length - 1]; 158 | fr.data = _decompress(out, fd.slice(0, foff), fr.rect.width, fr.rect.height); foff = 0; 159 | } 160 | const rct = { x: rUi(data, offset + 12), y: rUi(data, offset + 16), width: rUi(data, offset + 4), height: rUi(data, offset + 8) }; 161 | let del = rUs(data, offset + 22); del = rUs(data, offset + 20) / (del == 0 ? 100 : del); 162 | const frm = { rect: rct, delay: Math.round(del * 1000), dispose: data[offset + 24], blend: data[offset + 25] }; 163 | // console.log(frm); 164 | out.frames.push(frm); 165 | } else if (type == 'fdAT') { 166 | for (var i = 0; i < len - 4; i++) fd[foff + i] = data[offset + i + 4]; 167 | foff += len - 4; 168 | } else if (type == 'pHYs') { 169 | out.tabs[type] = [bin.readUint(data, offset), bin.readUint(data, offset + 4), data[offset + 8]]; 170 | } else if (type == 'cHRM') { 171 | out.tabs[type] = []; 172 | for (var i = 0; i < 8; i++) out.tabs[type].push(bin.readUint(data, offset + i * 4)); 173 | } else if (type == 'tEXt' || type == 'zTXt') { 174 | if (out.tabs[type] == null) out.tabs[type] = {}; 175 | var nz = bin.nextZero(data, offset); 176 | var keyw = bin.readASCII(data, offset, nz - offset); 177 | var text; var tl = offset + len - nz - 1; 178 | if (type == 'tEXt') text = bin.readASCII(data, nz + 1, tl); 179 | else { 180 | var bfr = _inflate(data.slice(nz + 2, nz + 2 + tl)); 181 | text = bin.readUTF8(bfr, 0, bfr.length); 182 | } 183 | out.tabs[type][keyw] = text; 184 | } else if (type == 'iTXt') { 185 | if (out.tabs[type] == null) out.tabs[type] = {}; 186 | var nz = 0; var off = offset; 187 | nz = bin.nextZero(data, off); 188 | var keyw = bin.readASCII(data, off, nz - off); off = nz + 1; 189 | const cflag = data[off]; const cmeth = data[off + 1]; off += 2; 190 | nz = bin.nextZero(data, off); 191 | const ltag = bin.readASCII(data, off, nz - off); off = nz + 1; 192 | nz = bin.nextZero(data, off); 193 | const tkeyw = bin.readUTF8(data, off, nz - off); off = nz + 1; 194 | var text; var tl = len - (off - offset); 195 | if (cflag == 0) text = bin.readUTF8(data, off, tl); 196 | else { 197 | var bfr = _inflate(data.slice(off, off + tl)); 198 | text = bin.readUTF8(bfr, 0, bfr.length); 199 | } 200 | out.tabs[type][keyw] = text; 201 | } else if (type == 'PLTE') { 202 | out.tabs[type] = bin.readBytes(data, offset, len); 203 | } else if (type == 'hIST') { 204 | const pl = out.tabs.PLTE.length / 3; 205 | out.tabs[type] = []; for (var i = 0; i < pl; i++) out.tabs[type].push(rUs(data, offset + i * 2)); 206 | } else if (type == 'tRNS') { 207 | if (out.ctype == 3) out.tabs[type] = bin.readBytes(data, offset, len); 208 | else if (out.ctype == 0) out.tabs[type] = rUs(data, offset); 209 | else if (out.ctype == 2) out.tabs[type] = [rUs(data, offset), rUs(data, offset + 2), rUs(data, offset + 4)]; 210 | // else console.log("tRNS for unsupported color type",out.ctype, len); 211 | } else if (type == 'gAMA') out.tabs[type] = bin.readUint(data, offset) / 100000; 212 | else if (type == 'sRGB') out.tabs[type] = data[offset]; 213 | else if (type == 'bKGD') { 214 | if (out.ctype == 0 || out.ctype == 4) out.tabs[type] = [rUs(data, offset)]; 215 | else if (out.ctype == 2 || out.ctype == 6) out.tabs[type] = [rUs(data, offset), rUs(data, offset + 2), rUs(data, offset + 4)]; 216 | else if (out.ctype == 3) out.tabs[type] = data[offset]; 217 | } else if (type == 'IEND') { 218 | break; 219 | } 220 | // else { console.log("unknown chunk type", type, len); out.tabs[type]=data.slice(offset,offset+len); } 221 | offset += len; 222 | const crc = bin.readUint(data, offset); offset += 4; 223 | } 224 | if (foff != 0) { 225 | var fr = out.frames[out.frames.length - 1]; 226 | fr.data = _decompress(out, fd.slice(0, foff), fr.rect.width, fr.rect.height); 227 | } 228 | out.data = _decompress(out, dd, out.width, out.height); 229 | 230 | delete out.compress; delete out.interlace; delete out.filter; 231 | return out; 232 | } 233 | 234 | function _decompress (out, dd, w, h) { 235 | var time = Date.now(); 236 | const bpp = _getBPP(out); const bpl = Math.ceil(w * bpp / 8); const buff = new Uint8Array((bpl + 1 + out.interlace) * h); 237 | if (out.tabs.CgBI) dd = inflateRaw(dd, buff); 238 | else dd = _inflate(dd, buff); 239 | // console.log(dd.length, buff.length); 240 | // console.log(Date.now()-time); 241 | 242 | var time = Date.now(); 243 | if (out.interlace == 0) dd = _filterZero(dd, out, 0, w, h); 244 | else if (out.interlace == 1) dd = _readInterlace(dd, out); 245 | // console.log(Date.now()-time); 246 | return dd; 247 | } 248 | 249 | function _inflate (data, buff) { const out = inflateRaw(new Uint8Array(data.buffer, 2, data.length - 6), buff); return out; } 250 | 251 | var inflateRaw = (function () { 252 | const H = {}; H.H = {}; H.H.N = function (N, W) { 253 | const R = Uint8Array; let i = 0; let m = 0; let J = 0; let h = 0; let Q = 0; let X = 0; let u = 0; let w = 0; let d = 0; let v; let C; 254 | if (N[0] == 3 && N[1] == 0) return W || new R(0); const V = H.H; const n = V.b; const A = V.e; const l = V.R; const M = V.n; const I = V.A; const e = V.Z; const b = V.m; const Z = W == null; 255 | if (Z)W = new R(N.length >>> 2 << 5); while (i == 0) { 256 | i = n(N, d, 1); m = n(N, d + 1, 2); d += 3; if (m == 0) { 257 | if ((d & 7) != 0)d += 8 - (d & 7); 258 | const D = (d >>> 3) + 4; const q = N[D - 4] | N[D - 3] << 8; if (Z)W = H.H.W(W, w + q); W.set(new R(N.buffer, N.byteOffset + D, q), w); d = D + q << 3; 259 | w += q; continue; 260 | } if (Z)W = H.H.W(W, w + (1 << 17)); if (m == 1) { v = b.J; C = b.h; X = (1 << 9) - 1; u = (1 << 5) - 1; } if (m == 2) { 261 | J = A(N, d, 5) + 257; 262 | h = A(N, d + 5, 5) + 1; Q = A(N, d + 10, 4) + 4; d += 14; const E = d; let j = 1; for (var c = 0; c < 38; c += 2) { b.Q[c] = 0; b.Q[c + 1] = 0; } for (var c = 0; 263 | c < Q; c++) { const K = A(N, d + c * 3, 3); b.Q[(b.X[c] << 1) + 1] = K; if (K > j)j = K; }d += 3 * Q; M(b.Q, j); I(b.Q, j, b.u); v = b.w; C = b.d; 264 | d = l(b.u, (1 << j) - 1, J + h, N, d, b.v); const r = V.V(b.v, 0, J, b.C); X = (1 << r) - 1; const S = V.V(b.v, J, h, b.D); u = (1 << S) - 1; M(b.C, r); 265 | I(b.C, r, v); M(b.D, S); I(b.D, S, C); 266 | } while (!0) { 267 | const T = v[e(N, d) & X]; d += T & 15; const p = T >>> 4; if (p >>> 8 == 0) { W[w++] = p; } else if (p == 256) { break; } else { 268 | let z = w + p - 254; 269 | if (p > 264) { const _ = b.q[p - 257]; z = w + (_ >>> 3) + A(N, d, _ & 7); d += _ & 7; } const $ = C[e(N, d) & u]; d += $ & 15; const s = $ >>> 4; const Y = b.c[s]; const a = (Y >>> 4) + n(N, d, Y & 15); 270 | d += Y & 15; while (w < z) { W[w] = W[w++ - a]; W[w] = W[w++ - a]; W[w] = W[w++ - a]; W[w] = W[w++ - a]; }w = z; 271 | } 272 | } 273 | } return W.length == w ? W : W.slice(0, w); 274 | }; 275 | H.H.W = function (N, W) { const R = N.length; if (W <= R) return N; const V = new Uint8Array(R << 1); V.set(N, 0); return V; }; 276 | H.H.R = function (N, W, R, V, n, A) { 277 | const l = H.H.e; const M = H.H.Z; let I = 0; while (I < R) { 278 | const e = N[M(V, n) & W]; n += e & 15; const b = e >>> 4; 279 | if (b <= 15) { A[I] = b; I++; } else { 280 | let Z = 0; let m = 0; if (b == 16) { m = 3 + l(V, n, 2); n += 2; Z = A[I - 1]; } else if (b == 17) { 281 | m = 3 + l(V, n, 3); 282 | n += 3; 283 | } else if (b == 18) { m = 11 + l(V, n, 7); n += 7; } const J = I + m; while (I < J) { A[I] = Z; I++; } 284 | } 285 | } return n; 286 | }; H.H.V = function (N, W, R, V) { 287 | let n = 0; let A = 0; const l = V.length >>> 1; 288 | while (A < R) { const M = N[A + W]; V[A << 1] = 0; V[(A << 1) + 1] = M; if (M > n)n = M; A++; } while (A < l) { V[A << 1] = 0; V[(A << 1) + 1] = 0; A++; } return n; 289 | }; 290 | H.H.n = function (N, W) { 291 | const R = H.H.m; const V = N.length; let n; let A; let l; var M; let I; const e = R.j; for (var M = 0; M <= W; M++)e[M] = 0; for (M = 1; M < V; M += 2)e[N[M]]++; 292 | const b = R.K; n = 0; e[0] = 0; for (A = 1; A <= W; A++) { n = n + e[A - 1] << 1; b[A] = n; } for (l = 0; l < V; l += 2) { 293 | I = N[l + 1]; if (I != 0) { 294 | N[l] = b[I]; 295 | b[I]++; 296 | } 297 | } 298 | }; H.H.A = function (N, W, R) { 299 | const V = N.length; const n = H.H.m; const A = n.r; for (let l = 0; l < V; l += 2) { 300 | if (N[l + 1] != 0) { 301 | const M = l >> 1; const I = N[l + 1]; const e = M << 4 | I; const b = W - I; let Z = N[l] << b; const m = Z + (1 << b); 302 | while (Z != m) { const J = A[Z] >>> 15 - W; R[J] = e; Z++; } 303 | } 304 | } 305 | }; H.H.l = function (N, W) { 306 | const R = H.H.m.r; const V = 15 - W; for (let n = 0; n < N.length; 307 | n += 2) { const A = N[n] << W - N[n + 1]; N[n] = R[A] >>> V; } 308 | }; H.H.M = function (N, W, R) { R = R << (W & 7); const V = W >>> 3; N[V] |= R; N[V + 1] |= R >>> 8; }; 309 | H.H.I = function (N, W, R) { R = R << (W & 7); const V = W >>> 3; N[V] |= R; N[V + 1] |= R >>> 8; N[V + 2] |= R >>> 16; }; H.H.e = function (N, W, R) { return (N[W >>> 3] | N[(W >>> 3) + 1] << 8) >>> (W & 7) & (1 << R) - 1; }; 310 | H.H.b = function (N, W, R) { return (N[W >>> 3] | N[(W >>> 3) + 1] << 8 | N[(W >>> 3) + 2] << 16) >>> (W & 7) & (1 << R) - 1; }; H.H.Z = function (N, W) { return (N[W >>> 3] | N[(W >>> 3) + 1] << 8 | N[(W >>> 3) + 2] << 16) >>> (W & 7); }; 311 | H.H.i = function (N, W) { return (N[W >>> 3] | N[(W >>> 3) + 1] << 8 | N[(W >>> 3) + 2] << 16 | N[(W >>> 3) + 3] << 24) >>> (W & 7); }; H.H.m = (function () { 312 | const N = Uint16Array; const W = Uint32Array; 313 | return { K: new N(16), j: new N(16), X: [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15], S: [3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258, 999, 999, 999], T: [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, 0, 0, 0], q: new N(32), p: [1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577, 65535, 65535], z: [0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 0, 0], c: new W(32), J: new N(512), _: [], h: new N(32), $: [], w: new N(32768), C: [], v: [], d: new N(32768), D: [], u: new N(512), Q: [], r: new N(1 << 15), s: new W(286), Y: new W(30), a: new W(19), t: new W(15e3), k: new N(1 << 16), g: new N(1 << 15) }; 314 | }()); 315 | (function () { 316 | const N = H.H.m; const W = 1 << 15; for (var R = 0; R < W; R++) { 317 | let V = R; V = (V & 2863311530) >>> 1 | (V & 1431655765) << 1; 318 | V = (V & 3435973836) >>> 2 | (V & 858993459) << 2; V = (V & 4042322160) >>> 4 | (V & 252645135) << 4; V = (V & 4278255360) >>> 8 | (V & 16711935) << 8; 319 | N.r[R] = (V >>> 16 | V << 16) >>> 17; 320 | } function n (A, l, M) { while (l-- != 0)A.push(0, M); } for (var R = 0; R < 32; R++) { 321 | N.q[R] = N.S[R] << 3 | N.T[R]; 322 | N.c[R] = N.p[R] << 4 | N.z[R]; 323 | }n(N._, 144, 8); n(N._, 255 - 143, 9); n(N._, 279 - 255, 7); n(N._, 287 - 279, 8); H.H.n(N._, 9); 324 | H.H.A(N._, 9, N.J); H.H.l(N._, 9); n(N.$, 32, 5); H.H.n(N.$, 5); H.H.A(N.$, 5, N.h); H.H.l(N.$, 5); n(N.Q, 19, 0); n(N.C, 286, 0); 325 | n(N.D, 30, 0); n(N.v, 320, 0); 326 | }()); return H.H.N; 327 | }()); 328 | 329 | function _readInterlace (data, out) { 330 | const w = out.width; const h = out.height; 331 | const bpp = _getBPP(out); const cbpp = bpp >> 3; const bpl = Math.ceil(w * bpp / 8); 332 | const img = new Uint8Array(h * bpl); 333 | let di = 0; 334 | 335 | const starting_row = [0, 0, 4, 0, 2, 0, 1]; 336 | const starting_col = [0, 4, 0, 2, 0, 1, 0]; 337 | const row_increment = [8, 8, 8, 4, 4, 2, 2]; 338 | const col_increment = [8, 8, 4, 4, 2, 2, 1]; 339 | 340 | let pass = 0; 341 | while (pass < 7) { 342 | const ri = row_increment[pass]; const ci = col_increment[pass]; 343 | let sw = 0; let sh = 0; 344 | let cr = starting_row[pass]; while (cr < h) { cr += ri; sh++; } 345 | let cc = starting_col[pass]; while (cc < w) { cc += ci; sw++; } 346 | const bpll = Math.ceil(sw * bpp / 8); 347 | _filterZero(data, out, di, sw, sh); 348 | 349 | let y = 0; let row = starting_row[pass]; 350 | while (row < h) { 351 | let col = starting_col[pass]; 352 | let cdi = (di + y * bpll) << 3; 353 | 354 | while (col < w) { 355 | if (bpp == 1) { 356 | var val = data[cdi >> 3]; val = (val >> (7 - (cdi & 7))) & 1; 357 | img[row * bpl + (col >> 3)] |= (val << (7 - ((col & 7) << 0))); 358 | } 359 | if (bpp == 2) { 360 | var val = data[cdi >> 3]; val = (val >> (6 - (cdi & 7))) & 3; 361 | img[row * bpl + (col >> 2)] |= (val << (6 - ((col & 3) << 1))); 362 | } 363 | if (bpp == 4) { 364 | var val = data[cdi >> 3]; val = (val >> (4 - (cdi & 7))) & 15; 365 | img[row * bpl + (col >> 1)] |= (val << (4 - ((col & 1) << 2))); 366 | } 367 | if (bpp >= 8) { 368 | const ii = row * bpl + col * cbpp; 369 | for (let j = 0; j < cbpp; j++) img[ii + j] = data[(cdi >> 3) + j]; 370 | } 371 | cdi += bpp; col += ci; 372 | } 373 | y++; row += ri; 374 | } 375 | if (sw * sh != 0) di += sh * (1 + bpll); 376 | pass = pass + 1; 377 | } 378 | return img; 379 | } 380 | 381 | function _getBPP (out) { 382 | const noc = [1, null, 3, 1, 2, null, 4][out.ctype]; 383 | return noc * out.depth; 384 | } 385 | 386 | function _filterZero (data, out, off, w, h) { 387 | let bpp = _getBPP(out); const bpl = Math.ceil(w * bpp / 8); 388 | bpp = Math.ceil(bpp / 8); 389 | 390 | let i; let di; let type = data[off]; let x = 0; 391 | 392 | if (type > 1) data[off] = [0, 0, 1][type - 2]; 393 | if (type == 3) for (x = bpp; x < bpl; x++) data[x + 1] = (data[x + 1] + (data[x + 1 - bpp] >>> 1)) & 255; 394 | 395 | for (let y = 0; y < h; y++) { 396 | i = off + y * bpl; di = i + y + 1; 397 | type = data[di - 1]; x = 0; 398 | 399 | if (type == 0) for (; x < bpl; x++) data[i + x] = data[di + x]; 400 | else if (type == 1) { 401 | for (; x < bpp; x++) data[i + x] = data[di + x]; 402 | for (; x < bpl; x++) data[i + x] = (data[di + x] + data[i + x - bpp]); 403 | } else if (type == 2) { for (; x < bpl; x++) data[i + x] = (data[di + x] + data[i + x - bpl]); } else if (type == 3) { 404 | for (; x < bpp; x++) data[i + x] = (data[di + x] + (data[i + x - bpl] >>> 1)); 405 | for (; x < bpl; x++) data[i + x] = (data[di + x] + ((data[i + x - bpl] + data[i + x - bpp]) >>> 1)); 406 | } else { 407 | for (; x < bpp; x++) data[i + x] = (data[di + x] + _paeth(0, data[i + x - bpl], 0)); 408 | for (; x < bpl; x++) data[i + x] = (data[di + x] + _paeth(data[i + x - bpp], data[i + x - bpl], data[i + x - bpp - bpl])); 409 | } 410 | } 411 | return data; 412 | } 413 | 414 | function _paeth (a, b, c) { 415 | const p = a + b - c; const pa = (p - a); const pb = (p - b); const pc = (p - c); 416 | if (pa * pa <= pb * pb && pa * pa <= pc * pc) return a; 417 | else if (pb * pb <= pc * pc) return b; 418 | return c; 419 | } 420 | 421 | function _IHDR (data, offset, out) { 422 | out.width = _bin.readUint(data, offset); offset += 4; 423 | out.height = _bin.readUint(data, offset); offset += 4; 424 | out.depth = data[offset]; offset++; 425 | out.ctype = data[offset]; offset++; 426 | out.compress = data[offset]; offset++; 427 | out.filter = data[offset]; offset++; 428 | out.interlace = data[offset]; offset++; 429 | } 430 | 431 | function _copyTile (sb, sw, sh, tb, tw, th, xoff, yoff, mode) { 432 | const w = Math.min(sw, tw); const h = Math.min(sh, th); 433 | let si = 0; let ti = 0; 434 | for (let y = 0; y < h; y++) { 435 | for (let x = 0; x < w; x++) { 436 | if (xoff >= 0 && yoff >= 0) { si = (y * sw + x) << 2; ti = ((yoff + y) * tw + xoff + x) << 2; } else { si = ((-yoff + y) * sw - xoff + x) << 2; ti = (y * tw + x) << 2; } 437 | 438 | if (mode == 0) { tb[ti] = sb[si]; tb[ti + 1] = sb[si + 1]; tb[ti + 2] = sb[si + 2]; tb[ti + 3] = sb[si + 3]; } else if (mode == 1) { 439 | var fa = sb[si + 3] * (1 / 255); var fr = sb[si] * fa; var fg = sb[si + 1] * fa; var fb = sb[si + 2] * fa; 440 | var ba = tb[ti + 3] * (1 / 255); var br = tb[ti] * ba; var bg = tb[ti + 1] * ba; var bb = tb[ti + 2] * ba; 441 | 442 | const ifa = 1 - fa; const oa = fa + ba * ifa; const ioa = (oa == 0 ? 0 : 1 / oa); 443 | tb[ti + 3] = 255 * oa; 444 | tb[ti + 0] = (fr + br * ifa) * ioa; 445 | tb[ti + 1] = (fg + bg * ifa) * ioa; 446 | tb[ti + 2] = (fb + bb * ifa) * ioa; 447 | } else if (mode == 2) { // copy only differences, otherwise zero 448 | var fa = sb[si + 3]; var fr = sb[si]; var fg = sb[si + 1]; var fb = sb[si + 2]; 449 | var ba = tb[ti + 3]; var br = tb[ti]; var bg = tb[ti + 1]; var bb = tb[ti + 2]; 450 | if (fa == ba && fr == br && fg == bg && fb == bb) { tb[ti] = 0; tb[ti + 1] = 0; tb[ti + 2] = 0; tb[ti + 3] = 0; } else { tb[ti] = fr; tb[ti + 1] = fg; tb[ti + 2] = fb; tb[ti + 3] = fa; } 451 | } else if (mode == 3) { // check if can be blended 452 | var fa = sb[si + 3]; var fr = sb[si]; var fg = sb[si + 1]; var fb = sb[si + 2]; 453 | var ba = tb[ti + 3]; var br = tb[ti]; var bg = tb[ti + 1]; var bb = tb[ti + 2]; 454 | if (fa == ba && fr == br && fg == bg && fb == bb) continue; 455 | // if(fa!=255 && ba!=0) return false; 456 | if (fa < 220 && ba > 20) return false; 457 | } 458 | } 459 | } 460 | return true; 461 | } 462 | 463 | return { 464 | decode: decode, 465 | decodeImage: decodeImage, 466 | toRGBA8: toRGBA8, 467 | _paeth: _paeth, 468 | _copyTile: _copyTile, 469 | _bin: _bin 470 | }; 471 | })(); 472 | 473 | (function () { 474 | const _copyTile = UPNG._copyTile; const _bin = UPNG._bin; const paeth = UPNG._paeth; 475 | var crcLib = { 476 | table: (function () { 477 | const tab = new Uint32Array(256); 478 | for (let n = 0; n < 256; n++) { 479 | let c = n; 480 | for (let k = 0; k < 8; k++) { 481 | if (c & 1) c = 0xedb88320 ^ (c >>> 1); 482 | else c = c >>> 1; 483 | } 484 | tab[n] = c; 485 | } 486 | return tab; 487 | })(), 488 | update: function (c, buf, off, len) { 489 | for (let i = 0; i < len; i++) c = crcLib.table[(c ^ buf[off + i]) & 0xff] ^ (c >>> 8); 490 | return c; 491 | }, 492 | crc: function (b, o, l) { return crcLib.update(0xffffffff, b, o, l) ^ 0xffffffff; } 493 | }; 494 | 495 | function addErr (er, tg, ti, f) { 496 | tg[ti] += (er[0] * f) >> 4; tg[ti + 1] += (er[1] * f) >> 4; tg[ti + 2] += (er[2] * f) >> 4; tg[ti + 3] += (er[3] * f) >> 4; 497 | } 498 | function N (x) { return Math.max(0, Math.min(255, x)); } 499 | function D (a, b) { const dr = a[0] - b[0]; const dg = a[1] - b[1]; const db = a[2] - b[2]; const da = a[3] - b[3]; return (dr * dr + dg * dg + db * db + da * da); } 500 | 501 | // MTD: 0: None, 1: floyd-steinberg, 2: Bayer 502 | function dither (sb, w, h, plte, tb, oind, MTD) { 503 | if (MTD == null) MTD = 1; 504 | 505 | const pc = plte.length; const nplt = []; const rads = []; 506 | for (var i = 0; i < pc; i++) { 507 | const c = plte[i]; 508 | nplt.push([((c >>> 0) & 255), ((c >>> 8) & 255), ((c >>> 16) & 255), ((c >>> 24) & 255)]); 509 | } 510 | for (var i = 0; i < pc; i++) { 511 | let ne = 0xffffffff; var ni = 0; 512 | for (var j = 0; j < pc; j++) { var ce = D(nplt[i], nplt[j]); if (j != i && ce < ne) { ne = ce; ni = j; } } 513 | const hd = Math.sqrt(ne) / 2; 514 | rads[i] = ~~(hd * hd); 515 | } 516 | 517 | const tb32 = new Uint32Array(tb.buffer); 518 | const err = new Int16Array(w * h * 4); 519 | 520 | /* 521 | var S=2, M = [ 522 | 0,2, 523 | 3,1]; // */ 524 | //* 525 | const S = 4; const M = [ 526 | 0, 8, 2, 10, 527 | 12, 4, 14, 6, 528 | 3, 11, 1, 9, 529 | 15, 7, 13, 5]; //* / 530 | for (var i = 0; i < M.length; i++) M[i] = 255 * (-0.5 + (M[i] + 0.5) / (S * S)); 531 | 532 | for (let y = 0; y < h; y++) { 533 | for (let x = 0; x < w; x++) { 534 | var i = (y * w + x) * 4; 535 | 536 | var cc; 537 | if (MTD != 2) cc = [N(sb[i] + err[i]), N(sb[i + 1] + err[i + 1]), N(sb[i + 2] + err[i + 2]), N(sb[i + 3] + err[i + 3])]; 538 | else { 539 | var ce = M[(y & (S - 1)) * S + (x & (S - 1))]; 540 | cc = [N(sb[i] + ce), N(sb[i + 1] + ce), N(sb[i + 2] + ce), N(sb[i + 3] + ce)]; 541 | } 542 | 543 | var ni = 0; let nd = 0xffffff; 544 | for (var j = 0; j < pc; j++) { 545 | const cd = D(cc, nplt[j]); 546 | if (cd < nd) { nd = cd; ni = j; } 547 | } 548 | 549 | const nc = nplt[ni]; 550 | const er = [cc[0] - nc[0], cc[1] - nc[1], cc[2] - nc[2], cc[3] - nc[3]]; 551 | 552 | if (MTD == 1) { 553 | // addErr(er, err, i+4, 16); 554 | if (x != w - 1) addErr(er, err, i + 4, 7); 555 | if (y != h - 1) { 556 | if (x != 0) addErr(er, err, i + 4 * w - 4, 3); 557 | addErr(er, err, i + 4 * w, 5); 558 | if (x != w - 1) addErr(er, err, i + 4 * w + 4, 1); 559 | }//* / 560 | } 561 | oind[i >> 2] = ni; tb32[i >> 2] = plte[ni]; 562 | } 563 | } 564 | } 565 | 566 | function encode (bufs, w, h, ps, dels, tabs, forbidPlte) { 567 | if (ps == null) ps = 0; 568 | if (forbidPlte == null) forbidPlte = false; 569 | 570 | const nimg = compress(bufs, w, h, ps, [false, false, false, 0, forbidPlte, false]); 571 | compressPNG(nimg, -1); 572 | 573 | return _main(nimg, w, h, dels, tabs); 574 | } 575 | 576 | function encodeLL (bufs, w, h, cc, ac, depth, dels, tabs) { 577 | const nimg = { ctype: 0 + (cc == 1 ? 0 : 2) + (ac == 0 ? 0 : 4), depth: depth, frames: [] }; 578 | 579 | const time = Date.now(); 580 | const bipp = (cc + ac) * depth; const bipl = bipp * w; 581 | for (let i = 0; i < bufs.length; i++) { nimg.frames.push({ rect: { x: 0, y: 0, width: w, height: h }, img: new Uint8Array(bufs[i]), blend: 0, dispose: 1, bpp: Math.ceil(bipp / 8), bpl: Math.ceil(bipl / 8) }); } 582 | 583 | compressPNG(nimg, 0, true); 584 | 585 | const out = _main(nimg, w, h, dels, tabs); 586 | return out; 587 | } 588 | 589 | function _main (nimg, w, h, dels, tabs) { 590 | if (tabs == null) tabs = {}; 591 | const crc = crcLib.crc; const wUi = _bin.writeUint; const wUs = _bin.writeUshort; const wAs = _bin.writeASCII; 592 | let offset = 8; const anim = nimg.frames.length > 1; let pltAlpha = false; 593 | 594 | let cicc; 595 | 596 | let leng = 8 + (16 + 5 + 4) /* + (9+4) */ + (anim ? 20 : 0); 597 | if (tabs.sRGB != null) leng += 8 + 1 + 4; 598 | if (tabs.pHYs != null) leng += 8 + 9 + 4; 599 | if (tabs.iCCP != null) { cicc = pako.deflate(tabs.iCCP); leng += 8 + 11 + 2 + cicc.length + 4; } 600 | if (nimg.ctype == 3) { 601 | var dl = nimg.plte.length; 602 | for (var i = 0; i < dl; i++) if ((nimg.plte[i] >>> 24) != 255) pltAlpha = true; 603 | leng += (8 + dl * 3 + 4) + (pltAlpha ? (8 + dl * 1 + 4) : 0); 604 | } 605 | for (var j = 0; j < nimg.frames.length; j++) { 606 | var fr = nimg.frames[j]; 607 | if (anim) leng += 38; 608 | leng += fr.cimg.length + 12; 609 | if (j != 0) leng += 4; 610 | } 611 | leng += 12; 612 | 613 | const data = new Uint8Array(leng); 614 | const wr = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]; 615 | for (var i = 0; i < 8; i++) data[i] = wr[i]; 616 | 617 | wUi(data, offset, 13); offset += 4; 618 | wAs(data, offset, 'IHDR'); offset += 4; 619 | wUi(data, offset, w); offset += 4; 620 | wUi(data, offset, h); offset += 4; 621 | data[offset] = nimg.depth; offset++; // depth 622 | data[offset] = nimg.ctype; offset++; // ctype 623 | data[offset] = 0; offset++; // compress 624 | data[offset] = 0; offset++; // filter 625 | data[offset] = 0; offset++; // interlace 626 | wUi(data, offset, crc(data, offset - 17, 17)); offset += 4; // crc 627 | 628 | // 13 bytes to say, that it is sRGB 629 | if (tabs.sRGB != null) { 630 | wUi(data, offset, 1); offset += 4; 631 | wAs(data, offset, 'sRGB'); offset += 4; 632 | data[offset] = tabs.sRGB; offset++; 633 | wUi(data, offset, crc(data, offset - 5, 5)); offset += 4; // crc 634 | } 635 | if (tabs.iCCP != null) { 636 | const sl = 11 + 2 + cicc.length; 637 | wUi(data, offset, sl); offset += 4; 638 | wAs(data, offset, 'iCCP'); offset += 4; 639 | wAs(data, offset, 'ICC profile'); offset += 11; offset += 2; 640 | data.set(cicc, offset); offset += cicc.length; 641 | wUi(data, offset, crc(data, offset - (sl + 4), sl + 4)); offset += 4; // crc 642 | } 643 | if (tabs.pHYs != null) { 644 | wUi(data, offset, 9); offset += 4; 645 | wAs(data, offset, 'pHYs'); offset += 4; 646 | wUi(data, offset, tabs.pHYs[0]); offset += 4; 647 | wUi(data, offset, tabs.pHYs[1]); offset += 4; 648 | data[offset] = tabs.pHYs[2]; offset++; 649 | wUi(data, offset, crc(data, offset - 13, 13)); offset += 4; // crc 650 | } 651 | 652 | if (anim) { 653 | wUi(data, offset, 8); offset += 4; 654 | wAs(data, offset, 'acTL'); offset += 4; 655 | wUi(data, offset, nimg.frames.length); offset += 4; 656 | wUi(data, offset, tabs.loop != null ? tabs.loop : 0); offset += 4; 657 | wUi(data, offset, crc(data, offset - 12, 12)); offset += 4; // crc 658 | } 659 | 660 | if (nimg.ctype == 3) { 661 | var dl = nimg.plte.length; 662 | wUi(data, offset, dl * 3); offset += 4; 663 | wAs(data, offset, 'PLTE'); offset += 4; 664 | for (var i = 0; i < dl; i++) { 665 | const ti = i * 3; const c = nimg.plte[i]; const r = (c) & 255; const g = (c >>> 8) & 255; const b = (c >>> 16) & 255; 666 | data[offset + ti + 0] = r; data[offset + ti + 1] = g; data[offset + ti + 2] = b; 667 | } 668 | offset += dl * 3; 669 | wUi(data, offset, crc(data, offset - dl * 3 - 4, dl * 3 + 4)); offset += 4; // crc 670 | 671 | if (pltAlpha) { 672 | wUi(data, offset, dl); offset += 4; 673 | wAs(data, offset, 'tRNS'); offset += 4; 674 | for (var i = 0; i < dl; i++) data[offset + i] = (nimg.plte[i] >>> 24) & 255; 675 | offset += dl; 676 | wUi(data, offset, crc(data, offset - dl - 4, dl + 4)); offset += 4; // crc 677 | } 678 | } 679 | 680 | let fi = 0; 681 | for (var j = 0; j < nimg.frames.length; j++) { 682 | var fr = nimg.frames[j]; 683 | if (anim) { 684 | wUi(data, offset, 26); offset += 4; 685 | wAs(data, offset, 'fcTL'); offset += 4; 686 | wUi(data, offset, fi++); offset += 4; 687 | wUi(data, offset, fr.rect.width); offset += 4; 688 | wUi(data, offset, fr.rect.height); offset += 4; 689 | wUi(data, offset, fr.rect.x); offset += 4; 690 | wUi(data, offset, fr.rect.y); offset += 4; 691 | wUs(data, offset, dels[j]); offset += 2; 692 | wUs(data, offset, 1000); offset += 2; 693 | data[offset] = fr.dispose; offset++; // dispose 694 | data[offset] = fr.blend; offset++; // blend 695 | wUi(data, offset, crc(data, offset - 30, 30)); offset += 4; // crc 696 | } 697 | 698 | const imgd = fr.cimg; var dl = imgd.length; 699 | wUi(data, offset, dl + (j == 0 ? 0 : 4)); offset += 4; 700 | const ioff = offset; 701 | wAs(data, offset, (j == 0) ? 'IDAT' : 'fdAT'); offset += 4; 702 | if (j != 0) { wUi(data, offset, fi++); offset += 4; } 703 | data.set(imgd, offset); 704 | offset += dl; 705 | wUi(data, offset, crc(data, ioff, offset - ioff)); offset += 4; // crc 706 | } 707 | 708 | wUi(data, offset, 0); offset += 4; 709 | wAs(data, offset, 'IEND'); offset += 4; 710 | wUi(data, offset, crc(data, offset - 4, 4)); offset += 4; // crc 711 | 712 | return data.buffer; 713 | } 714 | 715 | function compressPNG (out, filter, levelZero) { 716 | for (let i = 0; i < out.frames.length; i++) { 717 | const frm = out.frames[i]; const nw = frm.rect.width; const nh = frm.rect.height; 718 | const fdata = new Uint8Array(nh * frm.bpl + nh); 719 | frm.cimg = _filterZero(frm.img, nh, frm.bpp, frm.bpl, fdata, filter, levelZero); 720 | } 721 | } 722 | 723 | function compress (bufs, w, h, ps, prms) // prms: onlyBlend, minBits, forbidPlte 724 | { 725 | // var time = Date.now(); 726 | const onlyBlend = prms[0]; const evenCrd = prms[1]; const forbidPrev = prms[2]; const minBits = prms[3]; const forbidPlte = prms[4]; const dith = prms[5]; 727 | 728 | let ctype = 6; let depth = 8; let alphaAnd = 255; 729 | 730 | for (var j = 0; j < bufs.length; j++) { // when not quantized, other frames can contain colors, that are not in an initial frame 731 | const img = new Uint8Array(bufs[j]); var ilen = img.length; 732 | for (var i = 0; i < ilen; i += 4) alphaAnd &= img[i + 3]; 733 | } 734 | const gotAlpha = (alphaAnd != 255); 735 | 736 | // console.log("alpha check", Date.now()-time); time = Date.now(); 737 | 738 | // var brute = gotAlpha && forGIF; // brute : frames can only be copied, not "blended" 739 | const frms = framize(bufs, w, h, onlyBlend, evenCrd, forbidPrev); 740 | // console.log("framize", Date.now()-time); time = Date.now(); 741 | 742 | const cmap = {}; const plte = []; const inds = []; 743 | 744 | if (ps != 0) { 745 | const nbufs = []; for (var i = 0; i < frms.length; i++) nbufs.push(frms[i].img.buffer); 746 | 747 | const abuf = concatRGBA(nbufs); const qres = quantize(abuf, ps); 748 | 749 | for (var i = 0; i < qres.plte.length; i++) plte.push(qres.plte[i].est.rgba); 750 | 751 | let cof = 0; 752 | for (var i = 0; i < frms.length; i++) { 753 | var frm = frms[i]; const bln = frm.img.length; var ind = new Uint8Array(qres.inds.buffer, cof >> 2, bln >> 2); inds.push(ind); 754 | const bb = new Uint8Array(qres.abuf, cof, bln); 755 | 756 | // console.log(frm.img, frm.width, frm.height); 757 | // var time = Date.now(); 758 | if (dith) dither(frm.img, frm.rect.width, frm.rect.height, plte, bb, ind); 759 | // console.log(Date.now()-time); 760 | frm.img.set(bb); cof += bln; 761 | } 762 | 763 | // console.log("quantize", Date.now()-time); time = Date.now(); 764 | } else { 765 | // what if ps==0, but there are <=256 colors? we still need to detect, if the palette could be used 766 | for (var j = 0; j < frms.length; j++) { // when not quantized, other frames can contain colors, that are not in an initial frame 767 | var frm = frms[j]; const img32 = new Uint32Array(frm.img.buffer); var nw = frm.rect.width; var ilen = img32.length; 768 | var ind = new Uint8Array(ilen); inds.push(ind); 769 | for (var i = 0; i < ilen; i++) { 770 | const c = img32[i]; 771 | if (i != 0 && c == img32[i - 1]) ind[i] = ind[i - 1]; 772 | else if (i > nw && c == img32[i - nw]) ind[i] = ind[i - nw]; 773 | else { 774 | let cmc = cmap[c]; 775 | if (cmc == null) { cmap[c] = cmc = plte.length; plte.push(c); if (plte.length >= 300) break; } 776 | ind[i] = cmc; 777 | } 778 | } 779 | } 780 | // console.log("make palette", Date.now()-time); time = Date.now(); 781 | } 782 | 783 | const cc = plte.length; // console.log("colors:",cc); 784 | if (cc <= 256 && forbidPlte == false) { 785 | if (cc <= 2) depth = 1; else if (cc <= 4) depth = 2; else if (cc <= 16) depth = 4; else depth = 8; 786 | depth = Math.max(depth, minBits); 787 | } 788 | 789 | for (var j = 0; j < frms.length; j++) { 790 | var frm = frms[j]; const nx = frm.rect.x; const ny = frm.rect.y; var nw = frm.rect.width; const nh = frm.rect.height; 791 | let cimg = frm.img; const cimg32 = new Uint32Array(cimg.buffer); 792 | let bpl = 4 * nw; let bpp = 4; 793 | if (cc <= 256 && forbidPlte == false) { 794 | bpl = Math.ceil(depth * nw / 8); 795 | var nimg = new Uint8Array(bpl * nh); 796 | const inj = inds[j]; 797 | for (let y = 0; y < nh; y++) { 798 | var i = y * bpl; const ii = y * nw; 799 | if (depth == 8) for (var x = 0; x < nw; x++) nimg[i + (x)] = (inj[ii + x]); 800 | else if (depth == 4) for (var x = 0; x < nw; x++) nimg[i + (x >> 1)] |= (inj[ii + x] << (4 - (x & 1) * 4)); 801 | else if (depth == 2) for (var x = 0; x < nw; x++) nimg[i + (x >> 2)] |= (inj[ii + x] << (6 - (x & 3) * 2)); 802 | else if (depth == 1) for (var x = 0; x < nw; x++) nimg[i + (x >> 3)] |= (inj[ii + x] << (7 - (x & 7) * 1)); 803 | } 804 | cimg = nimg; ctype = 3; bpp = 1; 805 | } else if (gotAlpha == false && frms.length == 1) { // some next "reduced" frames may contain alpha for blending 806 | var nimg = new Uint8Array(nw * nh * 3); const area = nw * nh; 807 | for (var i = 0; i < area; i++) { const ti = i * 3; const qi = i * 4; nimg[ti] = cimg[qi]; nimg[ti + 1] = cimg[qi + 1]; nimg[ti + 2] = cimg[qi + 2]; } 808 | cimg = nimg; ctype = 2; bpp = 3; bpl = 3 * nw; 809 | } 810 | frm.img = cimg; frm.bpl = bpl; frm.bpp = bpp; 811 | } 812 | // console.log("colors => palette indices", Date.now()-time); time = Date.now(); 813 | 814 | return { ctype: ctype, depth: depth, plte: plte, frames: frms }; 815 | } 816 | function framize (bufs, w, h, alwaysBlend, evenCrd, forbidPrev) { 817 | /* DISPOSE 818 | - 0 : no change 819 | - 1 : clear to transparent 820 | - 2 : retstore to content before rendering (previous frame disposed) 821 | BLEND 822 | - 0 : replace 823 | - 1 : blend 824 | */ 825 | const frms = []; 826 | for (var j = 0; j < bufs.length; j++) { 827 | const cimg = new Uint8Array(bufs[j]); const cimg32 = new Uint32Array(cimg.buffer); 828 | var nimg; 829 | 830 | let nx = 0; let ny = 0; let nw = w; let nh = h; let blend = alwaysBlend ? 1 : 0; 831 | if (j != 0) { 832 | const tlim = (forbidPrev || alwaysBlend || j == 1 || frms[j - 2].dispose != 0) ? 1 : 2; let tstp = 0; let tarea = 1e9; 833 | for (let it = 0; it < tlim; it++) { 834 | var pimg = new Uint8Array(bufs[j - 1 - it]); const p32 = new Uint32Array(bufs[j - 1 - it]); 835 | let mix = w; let miy = h; let max = -1; let may = -1; 836 | for (let y = 0; y < h; y++) { 837 | for (let x = 0; x < w; x++) { 838 | var i = y * w + x; 839 | if (cimg32[i] != p32[i]) { 840 | if (x < mix) mix = x; if (x > max) max = x; 841 | if (y < miy) miy = y; if (y > may) may = y; 842 | } 843 | } 844 | } 845 | if (max == -1) mix = miy = max = may = 0; 846 | if (evenCrd) { if ((mix & 1) == 1)mix--; if ((miy & 1) == 1)miy--; } 847 | const sarea = (max - mix + 1) * (may - miy + 1); 848 | if (sarea < tarea) { 849 | tarea = sarea; tstp = it; 850 | nx = mix; ny = miy; nw = max - mix + 1; nh = may - miy + 1; 851 | } 852 | } 853 | 854 | // alwaysBlend: pokud zjistím, že blendit nelze, nastavím předchozímu snímku dispose=1. Zajistím, aby obsahoval můj obdélník. 855 | var pimg = new Uint8Array(bufs[j - 1 - tstp]); 856 | if (tstp == 1) frms[j - 1].dispose = 2; 857 | 858 | nimg = new Uint8Array(nw * nh * 4); 859 | _copyTile(pimg, w, h, nimg, nw, nh, -nx, -ny, 0); 860 | 861 | blend = _copyTile(cimg, w, h, nimg, nw, nh, -nx, -ny, 3) ? 1 : 0; 862 | if (blend == 1) _prepareDiff(cimg, w, h, nimg, { x: nx, y: ny, width: nw, height: nh }); 863 | else _copyTile(cimg, w, h, nimg, nw, nh, -nx, -ny, 0); 864 | } else nimg = cimg.slice(0); // img may be rewritten further ... don't rewrite input 865 | 866 | frms.push({ rect: { x: nx, y: ny, width: nw, height: nh }, img: nimg, blend: blend, dispose: 0 }); 867 | } 868 | 869 | if (alwaysBlend) { 870 | for (var j = 0; j < frms.length; j++) { 871 | var frm = frms[j]; if (frm.blend == 1) continue; 872 | const r0 = frm.rect; const r1 = frms[j - 1].rect; 873 | const miX = Math.min(r0.x, r1.x); const miY = Math.min(r0.y, r1.y); 874 | const maX = Math.max(r0.x + r0.width, r1.x + r1.width); const maY = Math.max(r0.y + r0.height, r1.y + r1.height); 875 | const r = { x: miX, y: miY, width: maX - miX, height: maY - miY }; 876 | 877 | frms[j - 1].dispose = 1; 878 | if (j - 1 != 0) { _updateFrame(bufs, w, h, frms, j - 1, r, evenCrd); } 879 | _updateFrame(bufs, w, h, frms, j, r, evenCrd); 880 | } 881 | } 882 | let area = 0; 883 | if (bufs.length != 1) { 884 | for (var i = 0; i < frms.length; i++) { 885 | var frm = frms[i]; 886 | area += frm.rect.width * frm.rect.height; 887 | // if(i==0 || frm.blend!=1) continue; 888 | // var ob = new Uint8Array( 889 | // console.log(frm.blend, frm.dispose, frm.rect); 890 | } 891 | } 892 | // if(area!=0) console.log(area); 893 | return frms; 894 | } 895 | function _updateFrame (bufs, w, h, frms, i, r, evenCrd) { 896 | const U8 = Uint8Array; const U32 = Uint32Array; 897 | const pimg = new U8(bufs[i - 1]); const pimg32 = new U32(bufs[i - 1]); const nimg = i + 1 < bufs.length ? new U8(bufs[i + 1]) : null; 898 | const cimg = new U8(bufs[i]); const cimg32 = new U32(cimg.buffer); 899 | 900 | let mix = w; let miy = h; let max = -1; let may = -1; 901 | for (let y = 0; y < r.height; y++) { 902 | for (let x = 0; x < r.width; x++) { 903 | const cx = r.x + x; const cy = r.y + y; 904 | const j = cy * w + cx; const cc = cimg32[j]; 905 | // no need to draw transparency, or to dispose it. Or, if writing the same color and the next one does not need transparency. 906 | if (cc == 0 || (frms[i - 1].dispose == 0 && pimg32[j] == cc && (nimg == null || nimg[j * 4 + 3] != 0))/**/) {} else { 907 | if (cx < mix) mix = cx; if (cx > max) max = cx; 908 | if (cy < miy) miy = cy; if (cy > may) may = cy; 909 | } 910 | } 911 | } 912 | if (max == -1) mix = miy = max = may = 0; 913 | if (evenCrd) { if ((mix & 1) == 1)mix--; if ((miy & 1) == 1)miy--; } 914 | r = { x: mix, y: miy, width: max - mix + 1, height: may - miy + 1 }; 915 | 916 | const fr = frms[i]; fr.rect = r; fr.blend = 1; fr.img = new Uint8Array(r.width * r.height * 4); 917 | if (frms[i - 1].dispose == 0) { 918 | _copyTile(pimg, w, h, fr.img, r.width, r.height, -r.x, -r.y, 0); 919 | _prepareDiff(cimg, w, h, fr.img, r); 920 | } else { _copyTile(cimg, w, h, fr.img, r.width, r.height, -r.x, -r.y, 0); } 921 | } 922 | function _prepareDiff (cimg, w, h, nimg, rec) { 923 | _copyTile(cimg, w, h, nimg, rec.width, rec.height, -rec.x, -rec.y, 2); 924 | } 925 | 926 | function _filterZero (img, h, bpp, bpl, data, filter, levelZero) { 927 | const fls = []; let ftry = [0, 1, 2, 3, 4]; 928 | if (filter != -1) ftry = [filter]; 929 | else if (h * bpl > 500000 || bpp == 1) ftry = [0]; 930 | let opts; if (levelZero) opts = { level: 0 }; 931 | 932 | const CMPR = (data.length > 10e6 && window.UZIP != null) ? window.UZIP : pako; 933 | 934 | const time = Date.now(); 935 | for (var i = 0; i < ftry.length; i++) { 936 | for (let y = 0; y < h; y++) _filterLine(data, img, y, bpl, bpp, ftry[i]); 937 | // var nimg = new Uint8Array(data.length); 938 | // var sz = UZIP.F.deflate(data, nimg); fls.push(nimg.slice(0,sz)); 939 | // var dfl = pako["deflate"](data), dl=dfl.length-4; 940 | // var crc = (dfl[dl+3]<<24)|(dfl[dl+2]<<16)|(dfl[dl+1]<<8)|(dfl[dl+0]<<0); 941 | // console.log(crc, UZIP.adler(data,2,data.length-6)); 942 | fls.push(CMPR.deflate(data, opts)); 943 | } 944 | 945 | let ti; let tsize = 1e9; 946 | for (var i = 0; i < fls.length; i++) if (fls[i].length < tsize) { ti = i; tsize = fls[i].length; } 947 | return fls[ti]; 948 | } 949 | function _filterLine (data, img, y, bpl, bpp, type) { 950 | const i = y * bpl; let di = i + y; 951 | data[di] = type; di++; 952 | 953 | if (type == 0) { 954 | if (bpl < 500) for (var x = 0; x < bpl; x++) data[di + x] = img[i + x]; 955 | else data.set(new Uint8Array(img.buffer, i, bpl), di); 956 | } else if (type == 1) { 957 | for (var x = 0; x < bpp; x++) data[di + x] = img[i + x]; 958 | for (var x = bpp; x < bpl; x++) data[di + x] = (img[i + x] - img[i + x - bpp] + 256) & 255; 959 | } else if (y == 0) { 960 | for (var x = 0; x < bpp; x++) data[di + x] = img[i + x]; 961 | 962 | if (type == 2) for (var x = bpp; x < bpl; x++) data[di + x] = img[i + x]; 963 | if (type == 3) for (var x = bpp; x < bpl; x++) data[di + x] = (img[i + x] - (img[i + x - bpp] >> 1) + 256) & 255; 964 | if (type == 4) for (var x = bpp; x < bpl; x++) data[di + x] = (img[i + x] - paeth(img[i + x - bpp], 0, 0) + 256) & 255; 965 | } else { 966 | if (type == 2) { for (var x = 0; x < bpl; x++) data[di + x] = (img[i + x] + 256 - img[i + x - bpl]) & 255; } 967 | if (type == 3) { 968 | for (var x = 0; x < bpp; x++) data[di + x] = (img[i + x] + 256 - (img[i + x - bpl] >> 1)) & 255; 969 | for (var x = bpp; x < bpl; x++) data[di + x] = (img[i + x] + 256 - ((img[i + x - bpl] + img[i + x - bpp]) >> 1)) & 255; 970 | } 971 | if (type == 4) { 972 | for (var x = 0; x < bpp; x++) data[di + x] = (img[i + x] + 256 - paeth(0, img[i + x - bpl], 0)) & 255; 973 | for (var x = bpp; x < bpl; x++) data[di + x] = (img[i + x] + 256 - paeth(img[i + x - bpp], img[i + x - bpl], img[i + x - bpp - bpl])) & 255; 974 | } 975 | } 976 | } 977 | 978 | function quantize (abuf, ps) { 979 | const sb = new Uint8Array(abuf); const tb = sb.slice(0); const tb32 = new Uint32Array(tb.buffer); 980 | 981 | const KD = getKDtree(tb, ps); 982 | const root = KD[0]; const leafs = KD[1]; 983 | 984 | const len = sb.length; 985 | 986 | const inds = new Uint8Array(len >> 2); let nd; 987 | if (sb.length < 20e6) // precise, but slow :( 988 | { 989 | for (var i = 0; i < len; i += 4) { 990 | var r = sb[i] * (1 / 255); var g = sb[i + 1] * (1 / 255); var b = sb[i + 2] * (1 / 255); var a = sb[i + 3] * (1 / 255); 991 | 992 | nd = getNearest(root, r, g, b, a); 993 | inds[i >> 2] = nd.ind; tb32[i >> 2] = nd.est.rgba; 994 | } 995 | } else { 996 | for (var i = 0; i < len; i += 4) { 997 | var r = sb[i] * (1 / 255); var g = sb[i + 1] * (1 / 255); var b = sb[i + 2] * (1 / 255); var a = sb[i + 3] * (1 / 255); 998 | 999 | nd = root; while (nd.left) nd = (planeDst(nd.est, r, g, b, a) <= 0) ? nd.left : nd.right; 1000 | inds[i >> 2] = nd.ind; tb32[i >> 2] = nd.est.rgba; 1001 | } 1002 | } 1003 | return { abuf: tb.buffer, inds: inds, plte: leafs }; 1004 | } 1005 | 1006 | function getKDtree (nimg, ps, err) { 1007 | if (err == null) err = 0.0001; 1008 | const nimg32 = new Uint32Array(nimg.buffer); 1009 | 1010 | const root = { i0: 0, i1: nimg.length, bst: null, est: null, tdst: 0, left: null, right: null }; // basic statistic, extra statistic 1011 | root.bst = stats(nimg, root.i0, root.i1); root.est = estats(root.bst); 1012 | const leafs = [root]; 1013 | 1014 | while (leafs.length < ps) { 1015 | let maxL = 0; let mi = 0; 1016 | for (var i = 0; i < leafs.length; i++) if (leafs[i].est.L > maxL) { maxL = leafs[i].est.L; mi = i; } 1017 | if (maxL < err) break; 1018 | const node = leafs[mi]; 1019 | 1020 | const s0 = splitPixels(nimg, nimg32, node.i0, node.i1, node.est.e, node.est.eMq255); 1021 | const s0wrong = (node.i0 >= s0 || node.i1 <= s0); 1022 | // console.log(maxL, leafs.length, mi); 1023 | if (s0wrong) { node.est.L = 0; continue; } 1024 | 1025 | const ln = { i0: node.i0, i1: s0, bst: null, est: null, tdst: 0, left: null, right: null }; ln.bst = stats(nimg, ln.i0, ln.i1); 1026 | ln.est = estats(ln.bst); 1027 | const rn = { i0: s0, i1: node.i1, bst: null, est: null, tdst: 0, left: null, right: null }; rn.bst = { R: [], m: [], N: node.bst.N - ln.bst.N }; 1028 | for (var i = 0; i < 16; i++) rn.bst.R[i] = node.bst.R[i] - ln.bst.R[i]; 1029 | for (var i = 0; i < 4; i++) rn.bst.m[i] = node.bst.m[i] - ln.bst.m[i]; 1030 | rn.est = estats(rn.bst); 1031 | 1032 | node.left = ln; node.right = rn; 1033 | leafs[mi] = ln; leafs.push(rn); 1034 | } 1035 | leafs.sort(function (a, b) { return b.bst.N - a.bst.N; }); 1036 | for (var i = 0; i < leafs.length; i++) leafs[i].ind = i; 1037 | return [root, leafs]; 1038 | } 1039 | 1040 | function getNearest (nd, r, g, b, a) { 1041 | if (nd.left == null) { nd.tdst = dist(nd.est.q, r, g, b, a); return nd; } 1042 | const pd = planeDst(nd.est, r, g, b, a); 1043 | 1044 | let node0 = nd.left; let node1 = nd.right; 1045 | if (pd > 0) { node0 = nd.right; node1 = nd.left; } 1046 | 1047 | const ln = getNearest(node0, r, g, b, a); 1048 | if (ln.tdst <= pd * pd) return ln; 1049 | const rn = getNearest(node1, r, g, b, a); 1050 | return rn.tdst < ln.tdst ? rn : ln; 1051 | } 1052 | function planeDst (est, r, g, b, a) { const e = est.e; return e[0] * r + e[1] * g + e[2] * b + e[3] * a - est.eMq; } 1053 | function dist (q, r, g, b, a) { const d0 = r - q[0]; const d1 = g - q[1]; const d2 = b - q[2]; const d3 = a - q[3]; return d0 * d0 + d1 * d1 + d2 * d2 + d3 * d3; } 1054 | 1055 | function splitPixels (nimg, nimg32, i0, i1, e, eMq) { 1056 | i1 -= 4; 1057 | const shfs = 0; 1058 | while (i0 < i1) { 1059 | while (vecDot(nimg, i0, e) <= eMq) i0 += 4; 1060 | while (vecDot(nimg, i1, e) > eMq) i1 -= 4; 1061 | if (i0 >= i1) break; 1062 | 1063 | const t = nimg32[i0 >> 2]; nimg32[i0 >> 2] = nimg32[i1 >> 2]; nimg32[i1 >> 2] = t; 1064 | 1065 | i0 += 4; i1 -= 4; 1066 | } 1067 | while (vecDot(nimg, i0, e) > eMq) i0 -= 4; 1068 | return i0 + 4; 1069 | } 1070 | function vecDot (nimg, i, e) { 1071 | return nimg[i] * e[0] + nimg[i + 1] * e[1] + nimg[i + 2] * e[2] + nimg[i + 3] * e[3]; 1072 | } 1073 | function stats (nimg, i0, i1) { 1074 | const R = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; 1075 | const m = [0, 0, 0, 0]; 1076 | const N = (i1 - i0) >> 2; 1077 | for (let i = i0; i < i1; i += 4) { 1078 | const r = nimg[i] * (1 / 255); const g = nimg[i + 1] * (1 / 255); const b = nimg[i + 2] * (1 / 255); const a = nimg[i + 3] * (1 / 255); 1079 | // var r = nimg[i], g = nimg[i+1], b = nimg[i+2], a = nimg[i+3]; 1080 | m[0] += r; m[1] += g; m[2] += b; m[3] += a; 1081 | 1082 | R[0] += r * r; R[1] += r * g; R[2] += r * b; R[3] += r * a; 1083 | R[5] += g * g; R[6] += g * b; R[7] += g * a; 1084 | R[10] += b * b; R[11] += b * a; 1085 | R[15] += a * a; 1086 | } 1087 | R[4] = R[1]; R[8] = R[2]; R[9] = R[6]; R[12] = R[3]; R[13] = R[7]; R[14] = R[11]; 1088 | 1089 | return { R: R, m: m, N: N }; 1090 | } 1091 | function estats (stats) { 1092 | const R = stats.R; const m = stats.m; const N = stats.N; 1093 | 1094 | // when all samples are equal, but N is large (millions), the Rj can be non-zero ( 0.0003.... - precission error) 1095 | const m0 = m[0]; const m1 = m[1]; const m2 = m[2]; const m3 = m[3]; const iN = (N == 0 ? 0 : 1 / N); 1096 | const Rj = [ 1097 | R[0] - m0 * m0 * iN, R[1] - m0 * m1 * iN, R[2] - m0 * m2 * iN, R[3] - m0 * m3 * iN, 1098 | R[4] - m1 * m0 * iN, R[5] - m1 * m1 * iN, R[6] - m1 * m2 * iN, R[7] - m1 * m3 * iN, 1099 | R[8] - m2 * m0 * iN, R[9] - m2 * m1 * iN, R[10] - m2 * m2 * iN, R[11] - m2 * m3 * iN, 1100 | R[12] - m3 * m0 * iN, R[13] - m3 * m1 * iN, R[14] - m3 * m2 * iN, R[15] - m3 * m3 * iN 1101 | ]; 1102 | 1103 | const A = Rj; const M = M4; 1104 | let b = [Math.random(), Math.random(), Math.random(), Math.random()]; let mi = 0; let tmi = 0; 1105 | 1106 | if (N != 0) { 1107 | for (let i = 0; i < 16; i++) { 1108 | b = M.multVec(A, b); tmi = Math.sqrt(M.dot(b, b)); b = M.sml(1 / tmi, b); 1109 | if (i != 0 && Math.abs(tmi - mi) < 1e-9) break; mi = tmi; 1110 | } 1111 | } 1112 | // b = [0,0,1,0]; mi=N; 1113 | const q = [m0 * iN, m1 * iN, m2 * iN, m3 * iN]; 1114 | const eMq255 = M.dot(M.sml(255, q), b); 1115 | 1116 | return { 1117 | Cov: Rj, 1118 | q: q, 1119 | e: b, 1120 | L: mi, 1121 | eMq255: eMq255, 1122 | eMq: M.dot(b, q), 1123 | rgba: (((Math.round(255 * q[3]) << 24) | (Math.round(255 * q[2]) << 16) | (Math.round(255 * q[1]) << 8) | (Math.round(255 * q[0]) << 0)) >>> 0) 1124 | }; 1125 | } 1126 | var M4 = { 1127 | multVec: function (m, v) { 1128 | return [ 1129 | m[0] * v[0] + m[1] * v[1] + m[2] * v[2] + m[3] * v[3], 1130 | m[4] * v[0] + m[5] * v[1] + m[6] * v[2] + m[7] * v[3], 1131 | m[8] * v[0] + m[9] * v[1] + m[10] * v[2] + m[11] * v[3], 1132 | m[12] * v[0] + m[13] * v[1] + m[14] * v[2] + m[15] * v[3] 1133 | ]; 1134 | }, 1135 | dot: function (x, y) { return x[0] * y[0] + x[1] * y[1] + x[2] * y[2] + x[3] * y[3]; }, 1136 | sml: function (a, y) { return [a * y[0], a * y[1], a * y[2], a * y[3]]; } 1137 | }; 1138 | 1139 | function concatRGBA (bufs) { 1140 | let tlen = 0; 1141 | for (var i = 0; i < bufs.length; i++) tlen += bufs[i].byteLength; 1142 | const nimg = new Uint8Array(tlen); let noff = 0; 1143 | for (var i = 0; i < bufs.length; i++) { 1144 | const img = new Uint8Array(bufs[i]); const il = img.length; 1145 | for (let j = 0; j < il; j += 4) { 1146 | let r = img[j]; let g = img[j + 1]; let b = img[j + 2]; const a = img[j + 3]; 1147 | if (a == 0) r = g = b = 0; 1148 | nimg[noff + j] = r; nimg[noff + j + 1] = g; nimg[noff + j + 2] = b; nimg[noff + j + 3] = a; 1149 | } 1150 | noff += il; 1151 | } 1152 | return nimg.buffer; 1153 | } 1154 | 1155 | UPNG.encode = encode; 1156 | UPNG.encodeLL = encodeLL; 1157 | UPNG.encode.compress = compress; 1158 | UPNG.encode.dither = dither; 1159 | 1160 | UPNG.quantize = quantize; 1161 | UPNG.quantize.getKDtree = getKDtree; 1162 | UPNG.quantize.getNearest = getNearest; 1163 | })(); 1164 | 1165 | export default UPNG; 1166 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import PNG from "./png"; 2 | import NextZen from "./nextzen"; 3 | import UPNG from "./UPNG"; 4 | import * as $ from "jquery"; 5 | import { throttle, debounce } from 'throttle-debounce'; 6 | import * as L from "leaflet"; 7 | import "leaflet-providers"; 8 | import {format, promiseAllInBatches} from "./helpers"; 9 | 10 | import * as Comlink from 'comlink'; 11 | import { 12 | NormaliseResult, 13 | TypedArrayToStlArgs, 14 | typedArrayToStlDefaults, 15 | NormRange, 16 | normMaxRange 17 | } from './processor'; 18 | 19 | const worker = new Worker("public/dist/js/processor.js"); 20 | const processor = Comlink.wrap(worker); 21 | 22 | import { 23 | TypedArray, 24 | TileCoords, 25 | LatLng, 26 | LatLngZoom, 27 | ConfigState, 28 | TileLoadState, 29 | NormaliseMode, 30 | roll, 31 | roundDigits, 32 | localFormatNumber, 33 | clamp 34 | } from "./helpers"; 35 | 36 | type AppArgs = { 37 | container: HTMLElement 38 | }; 39 | 40 | type Tab = any; 41 | type Del = any; 42 | 43 | interface Dictionary { 44 | [key: string]: T; 45 | } 46 | 47 | type AppEls = Dictionary>; 48 | 49 | type AppInputs = Dictionary|JQuery>; 50 | 51 | type StoredItemValue = { 52 | key: string, 53 | data: string, 54 | expires: number 55 | } 56 | 57 | type ImageLocation = TileCoords; 58 | 59 | let currentRequests: XMLHttpRequest[] = []; 60 | 61 | export default class App { 62 | static cache : Record = {}; 63 | container: HTMLElement; 64 | els: AppEls = {}; 65 | inputs: AppInputs = {}; 66 | imagesLoaded : ImageLocation[] = [] 67 | defaultSizes : string[] = [ 68 | '8129 x 8129', 69 | '4033 x 4033', 70 | '2017 x 2017', 71 | '1009 x 1009', 72 | '505 x 505', 73 | '253 x 253', 74 | '127 x 127', 75 | ]; 76 | meterFormatter : Intl.NumberFormat; 77 | map : L.Map; 78 | mapMarker : L.Marker; 79 | arrows : L.Marker[] = []; 80 | boundingRect : L.Rectangle; 81 | layers: Record = {}; 82 | layer: string = 'topo'; 83 | listenHashChange: boolean = false; 84 | doHeightsDebounced: () => void; 85 | savedKeys : string[] = [ 86 | 'latitude', 87 | 'longitude', 88 | 'zoom', 89 | 'outputzoom', 90 | 'width', 91 | 'height' 92 | ]; 93 | constructor({container} : AppArgs) { 94 | this.container = container; 95 | this.meterFormatter = new Intl.NumberFormat(undefined, {style:'unit', unit: 'meter'}); 96 | this.createMapLayers(); 97 | this.createAppElements(); 98 | this.insertSavedValues(); 99 | this.createMap(); 100 | this.hookControls(); 101 | this.listenHashChange = true; 102 | } 103 | createAppElements() { 104 | this.els.container = $(this.container); 105 | this.els.inputContainer = $('
'); 106 | this.els.container.append(this.els.inputContainer); 107 | 108 | this.createInputOptions(); 109 | this.createOutputOptions(); 110 | this.createSubmitButton(); 111 | this.resetOutput(); 112 | } 113 | showHideCurrentLayer() { 114 | this.layer = this.inputs.maptype.val().toString(); 115 | for (let [key, layer] of Object.entries(this.layers)) { 116 | if (key == this.layer) { 117 | layer.layer.addTo(this.map); 118 | } else { 119 | layer.layer.removeFrom(this.map); 120 | } 121 | } 122 | const mz = this.map.getMaxZoom(); 123 | this.inputs.zoom.prop('max', mz); 124 | this.inputs.outputzoom.prop('max', mz); 125 | this.inputs.zoom.val(Math.min(parseInt(this.inputs.zoom.val().toString()), mz)); 126 | this.inputs.outputzoom.val(Math.min(parseInt(this.inputs.outputzoom.val().toString()), mz)); 127 | } 128 | createMapLayers() { 129 | this.layers.topo = { 130 | layer: L.tileLayer.provider('Esri.WorldTopoMap'), 131 | label: 'Esri World Topo Map' 132 | }; 133 | this.layers.imagery = { 134 | layer: L.tileLayer.provider('Esri.WorldImagery'), 135 | label: 'Esri World Imagery' 136 | }; 137 | this.layers.relief = { 138 | layer: L.tileLayer.provider('Esri.WorldShadedRelief'), 139 | label: 'Esri World Shaded Relief' 140 | }; 141 | this.layers.natgeo = { 142 | layer: L.tileLayer.provider('Esri.NatGeoWorldMap'), 143 | label: 'Esri Nat Geo World Map' 144 | }; 145 | this.layers.osm = { 146 | layer: L.tileLayer.provider('OpenStreetMap.Mapnik'), 147 | label: 'Open Street Map' 148 | }; 149 | this.layers.osmhot = { 150 | layer: L.tileLayer.provider('OpenStreetMap.HOT'), 151 | label: 'Open Street Map HOT' 152 | }; 153 | this.layers.otm = { 154 | layer: L.tileLayer.provider('OpenTopoMap'), 155 | label: 'Open Topo Map' 156 | }; 157 | this.layers.opnvkarte = { 158 | layer: L.tileLayer.provider('OPNVKarte'), 159 | label: 'OPNVKarte' 160 | }; 161 | this.layers.usgsusimagery = { 162 | layer: L.tileLayer.provider('USGS.USImagery'), 163 | label: 'USGS US Imagery' 164 | }; 165 | this.layers.watercolor = { 166 | layer: L.tileLayer.provider('Stadia.StamenWatercolor'), 167 | label: 'Watercolor' 168 | }; 169 | this.layers.stadiastamentoner = { 170 | layer: L.tileLayer.provider('Stadia.StamenToner'), 171 | label: 'Toner Saver (with names)' 172 | }; 173 | this.layers.stadiastamentonerlite = { 174 | layer: L.tileLayer.provider('Stadia.StamenTonerLite'), 175 | label: 'Toner Saver Lite (with names)' 176 | }; 177 | this.layers.stadiastamentonerbg = { 178 | layer: L.tileLayer.provider('Stadia.StamenTonerBackground'), 179 | label: 'Toner Saver (without names)' 180 | }; 181 | this.layers.stadiaalidadesatellite = { 182 | layer: L.tileLayer('https://tiles.stadiamaps.com/tiles/alidade_satellite/{z}/{x}/{y}{r}.jpg', { 183 | minZoom: 0, 184 | maxZoom: 20, 185 | attribution: '© CNES, Distribution Airbus DS, © Airbus DS, © PlanetObserver (Contains Copernicus Data) | © Stadia Maps © OpenMapTiles © OpenStreetMap contributors', 186 | }), 187 | label: 'Stadia Satellite' 188 | }; 189 | this.layers.stadiasmooth = { 190 | layer: L.tileLayer.provider('Stadia.AlidadeSmooth'), 191 | label: 'Stadia Smooth' 192 | }; 193 | this.layers.stadiasmoothdark = { 194 | layer: L.tileLayer.provider('Stadia.AlidadeSmoothDark'), 195 | label: 'Stadia Smooth Dark' 196 | }; 197 | this.layers.nextzen = { 198 | layer: L.tileLayer( 199 | NextZen.getApiKeyedUrl(), 200 | { 201 | attribution: '© NextZen', 202 | maxZoom: 15 203 | } 204 | ), 205 | label: 'Terrarium (used for the heightmap export)' 206 | }; 207 | } 208 | createMap() { 209 | $('#map').height(512); 210 | 211 | const curLatLng : L.LatLngExpression = [parseFloat(this.inputs.latitude.val().toString()), parseFloat(this.inputs.longitude.val().toString())]; 212 | 213 | this.doHeightsDebounced = debounce(50, () => { 214 | this.getApproxHeightsForState(); 215 | }); 216 | 217 | this.map = L.map('map', { 218 | center: curLatLng, 219 | scrollWheelZoom: 'center', 220 | zoom: parseInt(this.inputs.zoom.val().toString()) 221 | }); 222 | 223 | this.layers.topo.layer.addTo(this.map); 224 | 225 | const icon = L.icon({ 226 | iconUrl: 'public/im/icon-marker.png', 227 | iconSize: [32, 32], 228 | iconAnchor: [16, 32], 229 | }); 230 | 231 | const arrowIcons = { 232 | up: L.icon({ 233 | iconUrl: 'public/im/icon-up.png', 234 | iconSize: [32, 32], 235 | iconAnchor: [16, 0], 236 | }), 237 | down: L.icon({ 238 | iconUrl: 'public/im/icon-down.png', 239 | iconSize: [32, 32], 240 | iconAnchor: [16, 32], 241 | }), 242 | right: L.icon({ 243 | iconUrl: 'public/im/icon-right.png', 244 | iconSize: [32, 32], 245 | iconAnchor: [32, 16], 246 | }), 247 | left: L.icon({ 248 | iconUrl: 'public/im/icon-left.png', 249 | iconSize: [32, 32], 250 | iconAnchor: [0, 16], 251 | }), 252 | }; 253 | 254 | this.mapMarker = L.marker(curLatLng, {icon}); 255 | this.mapMarker.addTo(this.map); 256 | 257 | this.arrows[0] = L.marker(curLatLng, {icon: arrowIcons.up}); 258 | this.arrows[1] = L.marker(curLatLng, {icon: arrowIcons.left}); 259 | this.arrows[2] = L.marker(curLatLng, {icon: arrowIcons.down}); 260 | this.arrows[3] = L.marker(curLatLng, {icon: arrowIcons.right}); 261 | this.arrows.map(arrow => arrow.addTo(this.map)); 262 | this.arrows.map((arrow, i) => arrow.on('click', () => { 263 | switch(i) { 264 | case 0: 265 | this.arrowClick(1, 0); 266 | break; 267 | case 1: 268 | this.arrowClick(0, -1); 269 | break; 270 | case 2: 271 | this.arrowClick(-1, 0); 272 | break; 273 | case 3: 274 | this.arrowClick(0, 1); 275 | break; 276 | } 277 | })); 278 | 279 | this.map.on('click', debounce(100, (e) => { 280 | this.setPositionTo(e.latlng); 281 | })); 282 | 283 | this.map.on('move', throttle(1000/60, (e) => { 284 | const center = this.map.getCenter(); 285 | const nlat = center.lat + 90; 286 | const lat = (((nlat % 180) + 180) % 180) - 90; 287 | const nlng = center.lng + 180; 288 | const lng = (((nlng % 360) + 360) % 360) - 180; 289 | this.inputs.latitude.val(lat); 290 | this.inputs.longitude.val(lng); 291 | this.inputs.zoom.val(this.map.getZoom()); 292 | this.mapMarker.setLatLng(center); 293 | this.saveLatLngZoomState(); 294 | this.updatePhysicalDimensions(); 295 | this.doHeightsDebounced(); 296 | })); 297 | 298 | this.map.on('moveend', this.doHeightsDebounced); 299 | 300 | window.addEventListener('resize', debounce(50, () => { 301 | this.resizeMap(); 302 | })) 303 | this.resizeMap(); 304 | 305 | this.updatePhysicalDimensions(); 306 | this.showHideCurrentLayer(); 307 | this.doHeightsDebounced(); 308 | } 309 | setPositionTo(latlng: L.LatLngLiteral) { 310 | this.map.setView(latlng); 311 | this.inputs.latitude.val(latlng.lat); 312 | this.inputs.longitude.val(latlng.lng); 313 | this.inputs.zoom.val(this.map.getZoom()); 314 | this.mapMarker.setLatLng(latlng); 315 | this.saveLatLngZoomState(); 316 | this.updatePhysicalDimensions(); 317 | this.doHeightsDebounced(); 318 | } 319 | arrowClick(up: 1|-1|0, right: 1|-1|0) { 320 | const state = this.getCurrentState(); 321 | const distance: [number, number] = [ 322 | Math.abs(state.bounds[0].latitude - state.bounds[1].latitude), 323 | Math.abs(state.bounds[0].longitude - state.bounds[1].longitude), 324 | ]; 325 | const newLatLng : L.LatLngLiteral = { 326 | lat: state.latitude + (up * distance[0]), 327 | lng: state.longitude + (right * distance[1]) 328 | } 329 | this.setPositionTo(newLatLng); 330 | } 331 | resizeMap() { 332 | const mapHeight = Math.min(768, Math.max(256, window.innerHeight)); 333 | $('#map').height(mapHeight); 334 | this.map.invalidateSize(); 335 | } 336 | updatePhysicalDimensions() { 337 | const state = this.getCurrentState(); 338 | const bounds : L.LatLngBoundsExpression = [ 339 | [ 340 | state.bounds[0].latitude, 341 | state.bounds[0].longitude 342 | ], [ 343 | state.bounds[1].latitude, 344 | state.bounds[1].longitude 345 | ] 346 | ]; 347 | if (!this.boundingRect) { 348 | this.boundingRect = L.rectangle(bounds, {color: "#ff7800", weight: 1}).addTo(this.map); 349 | } 350 | this.boundingRect.setBounds(bounds); 351 | this.arrows[0].setLatLng(this.latLngBetween(bounds[0], [bounds[0][0], bounds[1][1]])); 352 | this.arrows[1].setLatLng(this.latLngBetween(bounds[0], [bounds[1][0], bounds[0][1]])); 353 | this.arrows[2].setLatLng(this.latLngBetween([bounds[1][0], bounds[0][1]], bounds[1])); 354 | this.arrows[3].setLatLng(this.latLngBetween([bounds[0][0], bounds[1][1]], bounds[1])); 355 | 356 | const units = state.phys.width > 1000 ? 'km' : 'm'; 357 | const precision = state.phys.width > 100000 ? 0 : 1; 358 | const w = state.phys.width > 1000 ? state.phys.width/1000 : state.phys.width; 359 | const h = state.phys.height > 1000 ? state.phys.height/1000 : state.phys.height; 360 | 361 | const res = Math.max(state.phys.width/state.width, state.phys.height/state.height); 362 | const resunit = 'm'; 363 | const resprecision = res > 10 ? 1 : 2; 364 | 365 | this.els.generatorInfo.html(`${localFormatNumber(w, precision)} x ${localFormatNumber(h, precision)}${units} - ${localFormatNumber(res, resprecision)}${resunit}/px resolution`); 366 | } 367 | latLngBetween(a: L.LatLngTuple, b: L.LatLngTuple): L.LatLngTuple { 368 | if (a.length === 3 && b.length === 3) { 369 | return [(a[0]+b[0])/2, a[1] + (a[1]+b[1])/2, (a[2] + b[2])/2]; 370 | } 371 | return [(a[0]+b[0])/2, (a[1]+b[1])/2]; 372 | } 373 | createInputOptions() { 374 | // input options 375 | this.inputs.latitude = App.createInput({ 376 | name: 'latitude', 377 | placeholder: 'Latitude', 378 | type: 'number', 379 | min: '-90', 380 | max: '90', 381 | value: '27.994401411046148', 382 | step: '0.000000000000001' 383 | }); 384 | this.inputs.longitude = App.createInput({ 385 | name: 'longitude', 386 | placeholder: 'Longitude', 387 | type: 'number', 388 | min: '-180', 389 | max: '180', 390 | value: '86.92520141601562', 391 | step: '0.000000000000001' 392 | }); 393 | this.inputs.zoom = App.createInput({ 394 | name: 'zoom', 395 | placeholder: 'Zoom', 396 | type: 'number', 397 | min: '1', 398 | max: '18', 399 | step: '1', 400 | value: '13' 401 | }); 402 | this.inputs.outputzoom = App.createInput({ 403 | name: 'outputzoom', 404 | placeholder: 'Output Zoom Level', 405 | type: 'number', 406 | min: '1', 407 | max: '18', 408 | step: '1', 409 | value: '13' 410 | }); 411 | 412 | this.inputs.maptype = $('') as JQuery; 492 | 493 | this.inputs.defaultSize.append(``); 494 | for (let size of this.defaultSizes) { 495 | this.inputs.defaultSize.append(``); 496 | } 497 | 498 | this.els.columnsDefaultSizes = $('
'); 499 | this.els.columnSize = $('
'); 500 | 501 | this.els.defaultSizeControl = $('
').append($('
').append(this.inputs.defaultSize)); 502 | 503 | this.els.columnSize.append(App.createLabel('Default UE5 Sizes', {for:'width'})); 504 | this.els.columnSize.append(this.els.defaultSizeControl); 505 | 506 | this.els.columnsOutput.append(this.els.columnSize); 507 | 508 | this.els.inputContainer.append(this.els.columnsDefaultSizes); 509 | 510 | this.els.smartNormalisationControl = $(` 511 | 512 |
513 |
514 | 519 |
520 |
`); 521 | 522 | this.els.normFrom = $(` 523 | 524 |
525 | 526 |
`); 527 | 528 | this.els.normTo = $(` 529 | 530 |
531 | 532 |
`); 533 | 534 | this.els.columnsOutput.append( 535 | $('
').append(this.els.smartNormalisationControl) 536 | ); 537 | 538 | this.els.columnsOutput.append( 539 | $('
').append(this.els.normFrom) 540 | ); 541 | 542 | this.els.columnsOutput.append( 543 | $('
').append(this.els.normTo) 544 | ); 545 | 546 | this.inputs.smartNormalisationControl = this.els.smartNormalisationControl.find('select'); 547 | this.inputs.normFrom = this.els.normFrom.find('input'); 548 | this.inputs.normTo = this.els.normTo.find('input'); 549 | 550 | } 551 | createSubmitButton() { 552 | this.els.generatorInfo = $('
'); 553 | this.els.generate = $(''); 554 | this.els.generateAlbedo = $(''); 555 | this.els.inputContainer.append( 556 | $('
') 557 | .append( 558 | $('
').append( 559 | $('
').append( 560 | $('
') 561 | .append(this.els.generate) 562 | .append(this.els.generateAlbedo) 563 | ) 564 | ) 565 | ) 566 | .append(this.els.generatorInfo) 567 | ); 568 | } 569 | getInputState() { 570 | const state : Record = {}; 571 | for (let key of this.savedKeys) { 572 | const input = this.inputs[key]; 573 | if (input) { 574 | state[key] = input.val(); 575 | } 576 | } 577 | return state; 578 | } 579 | insertSavedValues() { 580 | for (let key of this.savedKeys) { 581 | let val = localStorage.getItem(key); 582 | if (val) { 583 | let value = JSON.parse(val) as StoredItemValue; 584 | const input = this.inputs[key]; 585 | if (value.data && input) { 586 | input.val(value.data); 587 | } 588 | } 589 | } 590 | this.insertValuesFromUrlHash(); 591 | } 592 | insertValuesFromUrlHash() { 593 | const state = this.parseHash(location.hash); 594 | for (let [key, value] of Object.entries(state)) { 595 | const input = this.inputs[key]; 596 | if (value && input) { 597 | input.val(value); 598 | } 599 | } 600 | } 601 | parseHash(hashstr : string) { 602 | const pairs = hashstr.replace(/^#/,'').replace(/^\//, '').split('/'); 603 | const out : Record = {}; 604 | for (let i = 0; i < pairs.length; i+=2) { 605 | out[pairs[i]] = pairs[i+1]; 606 | } 607 | return out; 608 | } 609 | objectToHash(hashobj : Record) { 610 | return '#' + Object.entries(hashobj).flat().join("/"); 611 | } 612 | storeValue(key : string, data : string, expires : number = null) { 613 | expires = expires || ((new Date()).getTime())+(1000*60*60*24*30); 614 | let storedItem : StoredItemValue = {key, data, expires}; 615 | localStorage.setItem(key, JSON.stringify(storedItem)); 616 | this.listenHashChange = false; 617 | location.hash = this.objectToHash(this.getInputState()); 618 | setTimeout(() => { 619 | this.listenHashChange = true; 620 | }, 1); 621 | } 622 | saveLatLngZoomState() { 623 | this.storeValue('latitude', this.inputs.latitude.val().toString()); 624 | this.storeValue('longitude', this.inputs.longitude.val().toString()); 625 | this.storeValue('zoom', this.inputs.zoom.val().toString()); 626 | } 627 | doDisEnableControls() { 628 | const outputZoomLevel = parseInt(this.inputs.outputzoom.val().toString()); 629 | if (outputZoomLevel > 15) { 630 | this.els.generate.prop('disabled', true); 631 | this.els.generate.prop('title', 'Cannot generate heightmap for output zoom > 15'); 632 | this.els.generate.text('Output Zoom too high (>15)'); 633 | } else { 634 | this.els.generate.prop('disabled', false); 635 | this.els.generate.prop('title', 'Generate heightmap'); 636 | this.els.generate.text('Generate Heightmap'); 637 | } 638 | } 639 | hookControls() { 640 | const doHeightsDebounced = debounce(50, () => { 641 | this.getApproxHeightsForState(); 642 | }); 643 | this.inputs.defaultSize.on('change input', debounce(100, () => { 644 | const val = this.inputs.defaultSize.val(); 645 | if (val && typeof val === 'string' && val.trim() !== '') { 646 | const parts = val.split(" x "); 647 | if (parts.length === 2) { 648 | this.inputs.width.val(parts[0]); 649 | this.inputs.height.val(parts[1]); 650 | } 651 | } 652 | this.storeValue('width', this.inputs.width.val().toString()); 653 | this.storeValue('height', this.inputs.height.val().toString()); 654 | this.updatePhysicalDimensions(); 655 | doHeightsDebounced(); 656 | })); 657 | 658 | this.els.generate.on('click touchend', debounce(100, () => { 659 | this.generate(); 660 | })); 661 | 662 | this.els.generateAlbedo.on('click touchend', debounce(100, () => { 663 | this.generateAlbedo(); 664 | })); 665 | 666 | this.inputs.maptype.on('change input', debounce(30, () => { 667 | this.storeValue('maptype', this.inputs.maptype.val().toString()); 668 | this.showHideCurrentLayer(); 669 | doHeightsDebounced(); 670 | })); 671 | 672 | this.inputs.latitude.on('change input', debounce(30, () => { 673 | this.storeValue('latitude', this.inputs.latitude.val().toString()); 674 | this.map.setView({ 675 | lat: parseFloat(this.inputs.latitude.val().toString()), 676 | lng: parseFloat(this.inputs.longitude.val().toString()) 677 | }); 678 | this.insertValuesFromUrlHash(); 679 | doHeightsDebounced(); 680 | })); 681 | 682 | this.inputs.longitude.on('change input', debounce(30, () => { 683 | this.storeValue('longitude', this.inputs.longitude.val().toString()); 684 | this.map.setView({ 685 | lat: parseFloat(this.inputs.latitude.val().toString()), 686 | lng: parseFloat(this.inputs.longitude.val().toString()) 687 | }); 688 | this.updatePhysicalDimensions(); 689 | doHeightsDebounced(); 690 | })); 691 | 692 | this.inputs.zoom.on('change input', debounce(30, () => { 693 | this.storeValue('zoom', this.inputs.zoom.val().toString()); 694 | this.map.setZoom(parseInt(this.inputs.zoom.val().toString())); 695 | this.updatePhysicalDimensions(); 696 | doHeightsDebounced(); 697 | })); 698 | 699 | this.inputs.outputzoom.on('change input', debounce(30, () => { 700 | this.storeValue('outputzoom', this.inputs.outputzoom.val().toString()); 701 | this.updatePhysicalDimensions(); 702 | this.doDisEnableControls(); 703 | doHeightsDebounced(); 704 | })); 705 | 706 | this.inputs.width.on('change input', debounce(30, () => { 707 | this.storeValue('width', this.inputs.width.val().toString()); 708 | this.updatePhysicalDimensions(); 709 | doHeightsDebounced(); 710 | })); 711 | 712 | this.inputs.height.on('change input', debounce(30, () => { 713 | this.storeValue('height', this.inputs.height.val().toString()); 714 | this.updatePhysicalDimensions(); 715 | doHeightsDebounced(); 716 | })); 717 | 718 | window.addEventListener('paste', (event : ClipboardEvent) => { 719 | // @ts-ignore 720 | let paste = (event.clipboardData || window.clipboardData).getData("text"); 721 | if (paste) { 722 | if (this.isPasteableText(paste)) { 723 | const latLng = this.getLatLngFromText(paste); 724 | console.log('Pasted latLng', latLng); 725 | 726 | this.storeValue('latitude', latLng.latitude.toString()); 727 | this.storeValue('longitude', latLng.longitude.toString()); 728 | this.inputs.latitude.val(latLng.latitude); 729 | this.inputs.longitude.val(latLng.longitude); 730 | this.map.setView({ 731 | lat: parseFloat(this.inputs.latitude.val().toString()), 732 | lng: parseFloat(this.inputs.longitude.val().toString()) 733 | }); 734 | this.updatePhysicalDimensions(); 735 | doHeightsDebounced(); 736 | } else { 737 | console.log('Text was not pasteable', paste); 738 | } 739 | } 740 | }); 741 | 742 | window.addEventListener('hashchange', debounce(50, (event : HashChangeEvent) => { 743 | if (this.listenHashChange) { 744 | this.insertValuesFromUrlHash(); 745 | this.listenHashChange = false; 746 | this.setMapViewFromInputs(); 747 | this.listenHashChange = true; 748 | } 749 | })); 750 | 751 | this.doDisEnableControls(); 752 | } 753 | setMapViewFromInputs() { 754 | this.map.setView({ 755 | lat: parseFloat(this.inputs.latitude.val().toString()), 756 | lng: parseFloat(this.inputs.longitude.val().toString()) 757 | }, parseInt(this.inputs.zoom.val().toString())); 758 | } 759 | getLatLngFromText(text : string) : LatLng|null { 760 | const google = /([0-9]{1,3}.?[0-9]*)°? ?(S|N), ?([0-9]{1,3}.?[0-9]*)°? ?(E|W)/; 761 | const gmatch = text.trim().match(google); 762 | if (gmatch) { 763 | let latitude = parseFloat(gmatch[1]); 764 | let longitude = parseFloat(gmatch[3]); 765 | if (gmatch[2].toUpperCase() === 'S') { 766 | latitude = -latitude; 767 | } 768 | if (gmatch[4].toUpperCase() === 'W') { 769 | longitude = -longitude; 770 | } 771 | return { 772 | latitude, 773 | longitude 774 | }; 775 | } 776 | // 32°53′S 64°13′W 777 | const generic = /([-0-9]{1,3}.?[0-9]*) *, *([-0-9]{1,3}.?[0-9]*)/; 778 | const genmatch = text.trim().match(generic); 779 | if (genmatch) { 780 | let latitude = parseFloat(genmatch[1]); 781 | let longitude = parseFloat(genmatch[2]) 782 | return { 783 | latitude, 784 | longitude 785 | }; 786 | } 787 | const degreesMinsSecs = /([0-9]{1,2})[*°] *([0-9]{1,2})[′'] *([0-9.]{1,6})[″"] *(S|N) ?([0-9]{1,3})[*°] *([0-9]{1,2})[′'] *([0-9.]{1,6})[″"] *(E|W)/; 788 | const dmsmatch = text.trim().match(degreesMinsSecs); 789 | if (dmsmatch) { 790 | let latitude = parseFloat(dmsmatch[1]) + parseFloat(dmsmatch[2])/60 + parseFloat(dmsmatch[3])/(60*60); 791 | let longitude = parseFloat(dmsmatch[5]) + parseFloat(dmsmatch[6])/60 + parseFloat(dmsmatch[7])/(60*60); 792 | if (dmsmatch[4].toUpperCase() === 'S') { 793 | latitude = -latitude; 794 | } 795 | if (dmsmatch[8].toUpperCase() === 'W') { 796 | longitude = -longitude; 797 | } 798 | return { 799 | latitude, 800 | longitude 801 | }; 802 | } 803 | const degreesMins = /([0-9]{1,2})[*°] *([0-9]{1,2})[′'] *(S|N) ?([0-9]{1,3})[*°] *([0-9]{1,2})[′'] *(E|W)/; 804 | const dmmatch = text.trim().match(degreesMins); 805 | if (dmmatch) { 806 | let latitude = parseFloat(dmmatch[1]) + parseFloat(dmmatch[2])/60; 807 | let longitude = parseFloat(dmmatch[4]) + parseFloat(dmmatch[5])/60; 808 | if (dmmatch[3].toUpperCase() === 'S') { 809 | latitude = -latitude; 810 | } 811 | if (dmmatch[6].toUpperCase() === 'W') { 812 | longitude = -longitude; 813 | } 814 | return { 815 | latitude, 816 | longitude 817 | }; 818 | } 819 | return null; 820 | } 821 | isPasteableText(text : string) : boolean { 822 | return this.getLatLngFromText(text) !== null; 823 | } 824 | newState() : ConfigState { 825 | return { 826 | x: 0, 827 | y: 0, 828 | z: 0, 829 | latitude: 0, 830 | longitude: 0, 831 | zoom: 0, 832 | width: 0, 833 | height: 0, 834 | exactPos: {x:0, y:0, z:0}, 835 | widthInTiles: 0, 836 | heightInTiles: 0, 837 | startx: 0, 838 | starty: 0, 839 | endx: 0, 840 | endy: 0, 841 | bounds: [{latitude:0, longitude:0},{latitude:0, longitude:0}], 842 | status: 'pending', 843 | phys: {width: 0, height: 0}, 844 | min: {x: 0, y: 0}, 845 | max: {x: 0, y: 0}, 846 | type: 'heightmap', 847 | url: '' 848 | }; 849 | } 850 | getCurrentState(scaleApprox : number = 1) : ConfigState { 851 | const state = this.newState(); 852 | 853 | const z = parseInt(this.inputs.outputzoom.val().toString()); 854 | const newZ = Math.floor(Math.min(18, Math.max(1, z + Math.log2(scaleApprox)))); 855 | const scale = Math.pow(2, newZ-z); 856 | 857 | state.latitude = parseFloat(this.inputs.latitude.val().toString()); 858 | state.longitude = parseFloat(this.inputs.longitude.val().toString()); 859 | state.z = newZ; 860 | state.zoom = newZ; 861 | state.width = parseInt(this.inputs.width.val().toString()) * scale; 862 | state.height = parseInt(this.inputs.height.val().toString()) * scale; 863 | 864 | state.exactPos = App.getTileCoordsFromLatLngExact(state.latitude, state.longitude, state.zoom); 865 | state.widthInTiles = state.width/NextZen.tileWidth; 866 | state.heightInTiles = state.height/NextZen.tileHeight; 867 | 868 | state.min = App.getTileCoordsFromLatLngExact( 85, -179.999, newZ); 869 | state.min.x = Math.floor(state.min.x); 870 | state.min.y = 0; 871 | state.max = App.getTileCoordsFromLatLngExact(-85, 179.999, newZ); 872 | state.max.x = Math.ceil(state.max.x); 873 | state.max.y = Math.floor(state.max.y); 874 | 875 | state.startx = Math.floor(state.exactPos.x - state.widthInTiles/2); 876 | state.endx = Math.floor(state.exactPos.x + state.widthInTiles/2); 877 | state.starty = clamp(Math.floor(state.exactPos.y - state.heightInTiles/2), state.min.y, state.max.y); 878 | state.endy = clamp(Math.floor(state.exactPos.y + state.heightInTiles/2), state.min.y, state.max.y); 879 | 880 | state.heightInTiles = clamp(state.exactPos.y + state.heightInTiles/2, state.min.y, state.max.y) - clamp(state.exactPos.y - state.heightInTiles/2, state.min.y, state.max.y); 881 | state.height = state.heightInTiles * NextZen.tileHeight; 882 | 883 | state.bounds = [ 884 | App.getLatLngFromTileCoords( 885 | state.exactPos.x - state.widthInTiles/2, 886 | state.exactPos.y - state.heightInTiles/2, 887 | state.zoom 888 | ), 889 | App.getLatLngFromTileCoords( 890 | state.exactPos.x + state.widthInTiles/2, 891 | state.exactPos.y + state.heightInTiles/2, 892 | state.zoom 893 | ) 894 | ]; 895 | 896 | 897 | state.phys = { 898 | height:0, 899 | width: 0, 900 | }; 901 | 902 | // Width = difference in longitudes 903 | state.phys.width = this.getDistanceBetweenLatLngs(state.bounds[0], { 904 | latitude: state.bounds[0].latitude, 905 | longitude: state.bounds[1].longitude, 906 | }); 907 | 908 | // Height = difference in latitudes 909 | state.phys.height = this.getDistanceBetweenLatLngs(state.bounds[0], { 910 | latitude: state.bounds[1].latitude, 911 | longitude: state.bounds[0].longitude, 912 | }); 913 | 914 | return state; 915 | } 916 | // From https://www.movable-type.co.uk/scripts/latlong.html 917 | getDistanceBetweenLatLngs(point1 : LatLng, point2 : LatLng) : number { 918 | const R = 6371e3; // metres 919 | const φ1 = point1.latitude * Math.PI/180; // φ, λ in radians 920 | const φ2 = point2.latitude * Math.PI/180; 921 | const Δφ = (point2.latitude-point1.latitude) * Math.PI/180; 922 | const Δλ = (point2.longitude-point1.longitude) * Math.PI/180; 923 | 924 | const a = Math.sin(Δφ/2) * Math.sin(Δφ/2) + 925 | Math.cos(φ1) * Math.cos(φ2) * 926 | Math.sin(Δλ/2) * Math.sin(Δλ/2); 927 | const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); 928 | 929 | const d = R * c; // in metres 930 | return d; 931 | } 932 | async fetchImage({x,y,z} : TileCoords, state : ConfigState) : Promise { 933 | return new Promise((resolve, reject) => { 934 | App.getImageAt({z, y, x}, state).then(buffer => { 935 | const png = PNG.fromBuffer(buffer); 936 | if (state.type === 'heightmap') { 937 | resolve({...state, x, y, buffer, heights: png.terrariumToGrayscale()}); 938 | } else { 939 | resolve({...state, x, y, buffer, heights: (png.getImageData() as Uint8Array)}); 940 | } 941 | }).catch(e => { 942 | for (let r of currentRequests) { 943 | r.abort(); 944 | } 945 | if (e.type === 'abort') { 946 | return; 947 | } 948 | console.error(e); 949 | reject({...state, x, y, z}); 950 | }); 951 | }); 952 | } 953 | getApproxHeightsForState() { 954 | const outputZoomLevel = parseInt(this.inputs.outputzoom.val().toString()); 955 | if (outputZoomLevel > 15) { 956 | this.els.generatorInfo.find('.heights').text(''); 957 | return; 958 | } 959 | let state = this.getCurrentState(1); 960 | if (state.width > 4000) { 961 | state = this.getCurrentState(1/8); 962 | } else if (state.width > 2000) { 963 | state = this.getCurrentState(1/4); 964 | } else if (state.width > 1000) { 965 | state = this.getCurrentState(1/2); 966 | } 967 | const imageFetches = []; 968 | const items : TileCoords[] = []; 969 | for (let x = state.startx; x <= state.endx; x++) { 970 | for (let y = state.starty; y <= state.endy; y++) { 971 | const nx = roll(x, state.min.x, state.max.x); 972 | items.push({z: state.z, x: nx, y: y}); 973 | } 974 | } 975 | return promiseAllInBatches((item) => this.fetchImage(item, state), items, 200, 0).then((result : TileLoadState[]) : Promise => { 976 | //@ts-ignore 977 | const resultToSend : TileLoadState[] = []; 978 | for (let s of result) { 979 | resultToSend.push({ 980 | z: s.z, 981 | x: s.x, 982 | y: s.y, 983 | heights: Comlink.transfer(s.heights, [s.heights.buffer]), 984 | buffer: s.buffer, 985 | width: s.width, 986 | height: s.height, 987 | exactPos: s.exactPos, 988 | widthInTiles: s.widthInTiles, 989 | heightInTiles: s.heightInTiles, 990 | startx: s.startx, 991 | starty: s.starty, 992 | endx: s.endx, 993 | endy: s.endy, 994 | status: s.status, 995 | bounds: s.bounds, 996 | phys: s.phys, 997 | min: s.min, 998 | max: s.max, 999 | type: s.type, 1000 | url: s.url, 1001 | latitude: s.latitude, 1002 | longitude: s.longitude, 1003 | zoom: s.zoom 1004 | }); 1005 | } 1006 | //@ts-ignore 1007 | return processor.combineImages(resultToSend, NormaliseMode.SmartWindow) 1008 | .then((output : NormaliseResult) => { 1009 | const fmt = this.meterFormatter; 1010 | const txt = `, Height range: ${fmt.format(output.minBefore)} to ${fmt.format(output.maxBefore)}`; 1011 | this.els.generatorInfo.find('.heights').text(txt); 1012 | }); 1013 | }).catch(e => { 1014 | console.error('Failed to load images', e); 1015 | }).finally(() => { 1016 | currentRequests = []; 1017 | }); 1018 | } 1019 | generate() { 1020 | this.els.generate.prop('disabled', true); 1021 | const state = this.getCurrentState(); 1022 | this.resetOutput(); 1023 | const imageFetches = []; 1024 | const items : TileCoords[] = []; 1025 | for (let x = state.startx; x <= state.endx; x++) { 1026 | for (let y = state.starty; y <= state.endy; y++) { 1027 | const nx = roll(x, state.min.x, state.max.x); 1028 | items.push({z: state.z, x: nx, y: y}); 1029 | } 1030 | } 1031 | return promiseAllInBatches((item) => { 1032 | return this.fetchImage(item, state) 1033 | .then((im) => { 1034 | this.imageLoaded(im); 1035 | return im; 1036 | }) 1037 | .catch((e) => { 1038 | this.displayError({text: `Failed to load image at tile x = ${state.x} y = ${state.y} - please try again`}); 1039 | }); 1040 | }, items, 200, 0).then((result : TileLoadState[]) : Promise => { 1041 | return new Promise((resolve, reject) => { 1042 | this.els.outputText.html('Generating images (should not take much longer)'); 1043 | setTimeout(() => { 1044 | resolve(this.generateOutput(result)); 1045 | },1); 1046 | }); 1047 | }).catch(e => { 1048 | console.error('Failed to load images', e); 1049 | }).finally(() => { 1050 | this.els.generate.prop('disabled', false); 1051 | currentRequests = []; 1052 | }); 1053 | } 1054 | generateAlbedo() { 1055 | this.els.generateAlbedo.prop('disabled', true); 1056 | this.els.generateAlbedo.text('Generating'); 1057 | const state = this.getCurrentState(); 1058 | this.resetOutput(); 1059 | const imageFetches = []; 1060 | const items : (ConfigState & TileCoords & {url: string})[] = []; 1061 | 1062 | const oldZoom = parseInt(this.inputs.zoom.val().toString()); 1063 | this.map.setZoom(parseInt(this.inputs.outputzoom.val().toString())); 1064 | 1065 | // This little timeout seems to help the map to 'Catch up' for some reason 1066 | setTimeout(() => { 1067 | const layer = this.layers[this.inputs.maptype.val().toString()].layer; 1068 | for (let x = (state.startx - 1); x <= (state.endx+1); x++) { 1069 | for (let y = (state.starty - 1); y <= (state.endy+1); y++) { 1070 | const nx = roll(x, state.min.x, state.max.x); 1071 | const coords = {z: state.z, x: nx, y: y}; 1072 | //@ts-ignore 1073 | const url = layer.getTileUrl(coords).replace('@2x', ''); 1074 | items.push({...state, ...coords, url: url}); 1075 | } 1076 | } 1077 | this.combineUrlsAndDownload(items) 1078 | .finally(() => { 1079 | this.inputs.zoom.val(oldZoom); 1080 | this.map.setZoom(oldZoom); 1081 | this.els.generateAlbedo.prop('disabled', false); 1082 | this.els.generateAlbedo.text('Generate Albedo from View'); 1083 | }); 1084 | }, 1000); 1085 | } 1086 | async combineImagesSimple(states : (ConfigState & TileCoords & {url: string})[]) : Promise { 1087 | const tileWidth = 256; 1088 | const increment = 1/tileWidth; 1089 | 1090 | const extent = { 1091 | x1: states[0].exactPos.x - states[0].widthInTiles/2, 1092 | x2: states[0].exactPos.x + states[0].widthInTiles/2, 1093 | y1: states[0].exactPos.y - states[0].heightInTiles/2, 1094 | y2: states[0].exactPos.y + states[0].heightInTiles/2 1095 | } 1096 | 1097 | const map : Record> = {}; 1098 | let total = 0; 1099 | for (let tile of states) { 1100 | if (!map[tile.x]) { 1101 | map[tile.x] = {}; 1102 | } 1103 | map[tile.x][tile.y] = tile; 1104 | total++; 1105 | } 1106 | 1107 | const canvas = document.createElement('canvas'); 1108 | const ctx = canvas.getContext("2d"); 1109 | canvas.width = states[0].width; 1110 | canvas.height = states[0].height; 1111 | 1112 | const promises = []; 1113 | 1114 | let i = 0; 1115 | for (let y = extent.y1; y < extent.y2+1; y ++) { 1116 | for (let x = extent.x1; x < extent.x2+1; x ++) { 1117 | const tile = { 1118 | x: Math.floor(x), 1119 | y: Math.floor(y) 1120 | }; 1121 | const px = { 1122 | x: Math.floor((x%1)*tileWidth), 1123 | y: Math.floor((y%1)*tileWidth) 1124 | }; 1125 | if (typeof map[tile.x] === 'undefined') { 1126 | console.error("Did not have map tile row", map[tile.x], tile.x, map); 1127 | } 1128 | const tileOb = map[tile.x][tile.y]; 1129 | promises.push(new Promise((resolve, reject) => { 1130 | let img = new Image(); 1131 | img.crossOrigin = "Anonymous"; 1132 | img.onload = () => { 1133 | this.els.generateAlbedo.text(`Downloaded ${i++}/${total}`); 1134 | const drawAt = { 1135 | x: Math.floor((tile.x - extent.x1) * tileWidth), 1136 | y: Math.floor((tile.y - extent.y1) * tileWidth) 1137 | }; 1138 | ctx.drawImage( 1139 | img, 1140 | drawAt.x, 1141 | drawAt.y 1142 | ); 1143 | resolve(); 1144 | }; 1145 | img.onerror = reject; 1146 | img.src = tileOb.url; 1147 | })); 1148 | } 1149 | } 1150 | return Promise.all(promises) 1151 | .then(r => { 1152 | this.els.generateAlbedo.text(`Getting Image Data`); 1153 | return ctx.getImageData(0, 0, canvas.width, canvas.height); 1154 | }).catch(e => { 1155 | console.error(e); 1156 | }); 1157 | } 1158 | async combineUrlsAndDownload(items : (ConfigState & TileCoords & {url: string})[]) { 1159 | //@ts-ignore 1160 | const output = await this.combineImagesSimple(items); 1161 | if (output) { 1162 | return this.saveOutputAlbedo(output, items); 1163 | } 1164 | } 1165 | async saveOutputAlbedo(output : ImageData, states : ConfigState[]) { 1166 | const s = states[0]; 1167 | const formatArgs = { 1168 | lat: s.latitude.toFixed(3).toString().replace(".",'_'), 1169 | lng: s.longitude.toFixed(3).toString().replace(".",'_'), 1170 | zoom: s.zoom, 1171 | w: s.width, 1172 | h: s.height, 1173 | layer: this.layer, 1174 | }; 1175 | const fn = format('{lat}_{lng}_{zoom}_{w}_{h}_albedo_{layer}.png', formatArgs); 1176 | 1177 | //@ts-ignore 1178 | const result = UPNG.encode([output.data.buffer], states[0].width, states[0].height, null); 1179 | 1180 | const blob = new Blob( [ result ] ); 1181 | const url = URL.createObjectURL( blob ); 1182 | const img : HTMLImageElement = new Image(); 1183 | img.src = url; 1184 | // So the Blob can be Garbage Collected 1185 | img.onload = e => URL.revokeObjectURL( url ); 1186 | 1187 | this.els.outputImage.append(img); 1188 | return this.download(blob, fn); 1189 | } 1190 | displayError(message : {text: string}) { 1191 | this.els.outputError.text(message.text); 1192 | 1193 | const errEl = $(`
1194 |
1195 |

Error

1196 | 1197 |
1198 |
1199 | ${message.text} 1200 |
1201 |
`).hide(); 1202 | this.els.messageStack?.append(errEl); 1203 | errEl.slideDown(); 1204 | setTimeout(() => errEl.slideUp(), 15000); 1205 | errEl.find('.delete').on('click touchend', () => errEl.slideUp()); 1206 | } 1207 | async generateOutput(states : TileLoadState[]) { 1208 | return this.generateOutputUsingWorker(states); 1209 | } 1210 | async generateOutputUsingWorker(states : TileLoadState[]) { 1211 | const norm : NormRange = {from : null, to : null} 1212 | if (this.inputs.normFrom.val() !== "") { 1213 | norm.from = parseFloat(this.inputs.normFrom.val().toString()); 1214 | } 1215 | if (this.inputs.normTo.val() !== "") { 1216 | norm.to = parseFloat(this.inputs.normTo.val().toString()); 1217 | } 1218 | //@ts-ignore 1219 | const output = await processor.combineImages(states, this.inputs.smartNormalisationControl.val(), norm) as NormaliseResult; 1220 | this.displayHeightData(output, states[0]); 1221 | return this.saveOutput(output.data, states); 1222 | } 1223 | displayHeightData(output : NormaliseResult, state: TileLoadState) { 1224 | const fmt = this.meterFormatter; 1225 | const range = output.maxBefore - output.minBefore; 1226 | const unrealZscaleFactor = 0.001953125; 1227 | const zScale = unrealZscaleFactor * range * 100; 1228 | const xyScale = ((state.phys.width * 100) / state.width); 1229 | // Start by multiplying 4207 by 100 to convert the height into centimeters. This equals 420,700 cm. 1230 | // Next, multiply this new value by the ratio: 420,700 multiplied by 0.001953125 equals 821.6796875. 1231 | // This gives you a Z scale value of 821.6796875 and results in a heightmap that will go from -210,350 cm to 210,350 cm. 1232 | const txt = `

Height range: ${fmt.format(output.minBefore)} to ${fmt.format(output.maxBefore)}

1233 |

In Unreal Engine, on import, a z scaling of ${localFormatNumber(zScale, 2)} should be used for 1:1 height scaling using a normalised image.

1234 |

x and y scales should be set to ${localFormatNumber(xyScale, 2)}

1235 |

For 3D printing, the height range is ${this.meterFormatter.format(range)} and height/width ratio is ${localFormatNumber(range/state.phys.width, 6)}

1236 |

i.e. if you printed this 100mm wide, it would have to be ${localFormatNumber(range/state.phys.width * 100, 3)}mm tall to be physically accurate

1237 | `; 1238 | this.els.outputText.html(txt); 1239 | } 1240 | async saveOutput(output : Float32Array, states : TileLoadState[]) { 1241 | const s = states[0]; 1242 | const formatArgs = { 1243 | lat: s.latitude.toFixed(3).toString().replace(".",'_'), 1244 | lng: s.longitude.toFixed(3).toString().replace(".",'_'), 1245 | zoom: s.zoom, 1246 | w: s.width, 1247 | h: s.height, 1248 | }; 1249 | const fn = format('{lat}_{lng}_{zoom}_{w}_{h}.png', formatArgs); 1250 | 1251 | return App.encodeToPng([PNG.Float32ArrayToPng16Bit(output)], states[0].width, states[0].height, 1, 0, 16).then(a => { 1252 | const blob = new Blob( [ a ] ); 1253 | const url = URL.createObjectURL( blob ); 1254 | const img : HTMLImageElement = new Image(); 1255 | img.src = url; 1256 | // So the Blob can be Garbage Collected 1257 | img.onload = e => URL.revokeObjectURL( url ); 1258 | 1259 | this.els.outputImage.append(img); 1260 | this.download(blob, fn); 1261 | }); 1262 | } 1263 | 1264 | download(contents : Blob, fn : string) { 1265 | const blobUrl = URL.createObjectURL(contents); 1266 | 1267 | // Create a link element 1268 | const link = document.createElement("a"); 1269 | 1270 | // Set link's href to point to the Blob URL 1271 | link.href = blobUrl; 1272 | link.download = fn; 1273 | 1274 | // Append link to the body 1275 | document.body.appendChild(link); 1276 | 1277 | // Dispatch click event on the link 1278 | // This is necessary as link.click() does not work on the latest firefox 1279 | link.dispatchEvent( 1280 | new MouseEvent('click', { 1281 | bubbles: true, 1282 | cancelable: true, 1283 | view: window 1284 | }) 1285 | ); 1286 | 1287 | // Remove link from body 1288 | document.body.removeChild(link); 1289 | } 1290 | 1291 | resetOutput() { 1292 | this.imagesLoaded = []; 1293 | if (!this.els.outputContainer) { 1294 | this.els.messageStack = $('
'); 1295 | 1296 | this.els.outputContainer = $('
'); 1297 | 1298 | this.els.outputTextContainer = $('
'); 1299 | this.els.outputText = $('
'); 1300 | 1301 | this.els.outputErrorContainer = $('
'); 1302 | this.els.outputError = $('
'); 1303 | 1304 | this.els.outputImageContainer = $('
'); 1305 | this.els.outputImage = $('
'); 1306 | 1307 | this.els.outputContainer.append(this.els.outputTextContainer.append(this.els.outputText)); 1308 | this.els.outputContainer.append(this.els.outputErrorContainer.append(this.els.outputError)); 1309 | this.els.outputContainer.append(this.els.outputImageContainer.append(this.els.outputImage)); 1310 | this.els.container.append(this.els.outputContainer); 1311 | this.els.container.append(this.els.messageStack); 1312 | } 1313 | this.els.outputText.empty(); 1314 | this.els.outputImage.empty(); 1315 | this.els.outputError.empty(); 1316 | } 1317 | imageLoaded(state : ConfigState) { 1318 | this.imagesLoaded.push(state); 1319 | this.doOutputText(state); 1320 | } 1321 | doOutputText(state : ConfigState) { 1322 | const widthInTiles = (state.endx - state.startx)+1; 1323 | const heightInTiles = (state.endy - state.starty)+1; 1324 | 1325 | const totalTiles = widthInTiles * heightInTiles; 1326 | this.els.outputText.text(`Loaded ${this.imagesLoaded.length} / ${totalTiles} tiles`); 1327 | } 1328 | 1329 | static createInput(attrs : Record = {type: 'text', class: 'input'}) : JQuery { 1330 | attrs = {type: 'text', class: 'input', ...attrs}; 1331 | return $('').attr(attrs) as JQuery; 1332 | } 1333 | static createLabel(text: string, attrs : Record = {class: 'label'}) : JQuery { 1334 | attrs = {class: 'label', ...attrs}; 1335 | return $(``).attr(attrs) as JQuery; 1336 | } 1337 | static toRad(n : number) { 1338 | return (n * Math.PI / 180); 1339 | } 1340 | static getTileCoordsFromLatLng(latitude : number, longitude : number, zoom : number) : TileCoords 1341 | { 1342 | const res = App.getTileCoordsFromLatLngExact(latitude, longitude, zoom); 1343 | return { 1344 | x: Math.floor(res.x), 1345 | y: Math.floor(res.y), 1346 | z: Math.floor(res.z), 1347 | } 1348 | } 1349 | static getTileCoordsFromLatLngExact(latitude : number, longitude : number, zoom : number) : TileCoords 1350 | { 1351 | if (zoom !== Math.floor(zoom)) { 1352 | throw new Error(`Zoom must be an integer, got ${zoom}`); 1353 | } 1354 | const x = ((longitude + 180)%360) / 360 * (1 << zoom); 1355 | const y = (1 - Math.log(Math.tan(App.toRad(latitude)) + 1 / Math.cos(App.toRad(latitude))) / Math.PI) / 2 * (1 << zoom); 1356 | return { 1357 | x, 1358 | y, 1359 | z: zoom 1360 | }; 1361 | } 1362 | static getLatLngFromTileCoords(x : number, y : number, z : number) : {latitude: number, longitude: number} 1363 | { 1364 | if (z !== Math.floor(z)) { 1365 | throw new Error(`Zoom must be an integer, got ${z}`); 1366 | } 1367 | let n = Math.PI - 2 * Math.PI * y / Math.pow(2, z); 1368 | return { 1369 | latitude: (180 / Math.PI * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n)))), 1370 | longitude: (x / Math.pow(2,z) * 360 - 180) 1371 | }; 1372 | } 1373 | static async getImageAt({x, y, z} : TileCoords, state : ConfigState) : Promise { 1374 | if (state.type === 'albedo') { 1375 | return App.getImageAsBuffer(format(state.url, {x,y,z})); 1376 | } 1377 | return App.getImageAsBuffer(NextZen.getUrl({x,y,z})); 1378 | } 1379 | static async getImageAsBuffer(im : string) : Promise { 1380 | return new Promise((resolve, reject) => { 1381 | const xhr = new XMLHttpRequest(); 1382 | currentRequests.push(xhr); 1383 | xhr.open("GET", im); 1384 | // xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); 1385 | // xhr.setRequestHeader('Access-Control-Allow-Origin', '*'); 1386 | xhr.responseType = "arraybuffer"; 1387 | 1388 | // xhr.addEventListener('loadstart', handleEvent); 1389 | xhr.addEventListener('load', () => resolve(xhr.response)); 1390 | // xhr.addEventListener('loadend', handleEvent); 1391 | xhr.addEventListener('progress', (e) => { 1392 | if (e.type === 'error') { 1393 | reject(e); 1394 | } 1395 | }); 1396 | xhr.addEventListener('error', reject); 1397 | xhr.addEventListener('abort', reject); 1398 | try { 1399 | xhr.send(); 1400 | } catch (e) { 1401 | reject(e); 1402 | } 1403 | }); 1404 | } 1405 | 1406 | // debug - might delete l8er 1407 | static async getPngFromUrl(im : string) : Promise { 1408 | return App.getImageAsBuffer(im).then(ab => PNG.fromBuffer(ab)); 1409 | } 1410 | static async encodeToPng( 1411 | bufs : Uint8Array[], 1412 | width : number, 1413 | height : number, 1414 | colourChannel : number, 1415 | alphaChannel : number, 1416 | depth : number, 1417 | dels: Del[] = null, 1418 | tabs: UPNG.ImageTabs = {} 1419 | ) : Promise { 1420 | return UPNG.encodeLL(bufs, width, height, colourChannel, alphaChannel, depth, dels, tabs) 1421 | } 1422 | async typedArrayToStl( 1423 | points: TypedArray, 1424 | widthpx : number, 1425 | heightpx : number, 1426 | {width, depth, height} : TypedArrayToStlArgs = typedArrayToStlDefaults 1427 | ) { 1428 | //@ts-ignore 1429 | return await processor.typedArrayToStl(points, widthpx, heightpx, {width, depth, height}); 1430 | } 1431 | } -------------------------------------------------------------------------------- /src/buffer.ts: -------------------------------------------------------------------------------- 1 | export type TypedArray = 2 | | Int8Array 3 | | Uint8Array 4 | | Uint8ClampedArray 5 | | Int16Array 6 | | Uint16Array 7 | | Int32Array 8 | | Uint32Array 9 | | Float32Array 10 | | Float64Array; 11 | 12 | const nextZero = (data : TypedArray, p: number) => { while (data[p] != 0) p++; return p; }; 13 | const readUshort = (buff : TypedArray, p: number) => { return (buff[p] << 8) | buff[p + 1]; }; 14 | const writeUshort = (buff : TypedArray, p: number, n: number) => { buff[p] = (n >> 8) & 255; buff[p + 1] = n & 255; }; 15 | const readUint = (buff : TypedArray, p: number) => { return (buff[p] * (256 * 256 * 256)) + ((buff[p + 1] << 16) | (buff[p + 2] << 8) | buff[p + 3]); }; 16 | const writeUint = (buff : TypedArray, p: number, n: number) => { buff[p] = (n >> 24) & 255; buff[p + 1] = (n >> 16) & 255; buff[p + 2] = (n >> 8) & 255; buff[p + 3] = n & 255; }; 17 | const readASCII = (buff : TypedArray, p: number, l: number) => { let s = ''; for (let i = 0; i < l; i++) s += String.fromCharCode(buff[p + i]); return s; }; 18 | const writeASCII = (data : TypedArray, p: number, s: string) => { for (let i = 0; i < s.length; i++) data[p + i] = s.charCodeAt(i); }; 19 | const readBytes = (buff : TypedArray, p: number, l: number) => { const arr = []; for (let i = 0; i < l; i++) arr.push(buff[p + i]); return arr; }; 20 | const pad = (n: string) => { return n.length < 2 ? '0' + n : n; }; 21 | const readUTF8 = (buff : TypedArray, p: number, l: number) => { 22 | let s = ''; let ns; 23 | for (let i = 0; i < l; i++) s += '%' + pad(buff[p + i].toString(16)); 24 | try { ns = decodeURIComponent(s); } catch (e) { return readASCII(buff, p, l); } 25 | return ns; 26 | }; 27 | 28 | const bufferReader = { 29 | nextZero, 30 | readUshort, 31 | writeUshort, 32 | readUint, 33 | writeUint, 34 | readASCII, 35 | writeASCII, 36 | readBytes, 37 | pad, 38 | readUTF8, 39 | }; 40 | 41 | export default bufferReader; -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | export const format = (str : string, obj : Record) : string => { 2 | for (let [key, value] of Object.entries(obj)) { 3 | str = str.replace(`{${key}}`, value.toString()); 4 | } 5 | return str; 6 | }; 7 | 8 | export type TypedArray = 9 | | Int8Array 10 | | Uint8Array 11 | | Uint8ClampedArray 12 | | Int16Array 13 | | Uint16Array 14 | | Int32Array 15 | | Uint32Array 16 | | Float32Array 17 | | Float64Array; 18 | 19 | export interface TileCoords { 20 | x: number, 21 | y: number, 22 | z: number 23 | } 24 | export interface LatLng { 25 | latitude: number, 26 | longitude: number 27 | } 28 | export interface LatLngZoom extends LatLng { 29 | zoom: number 30 | } 31 | 32 | export type ConfigState = TileCoords & LatLngZoom & { 33 | width : number, 34 | height : number, 35 | exactPos : TileCoords, 36 | widthInTiles : number, 37 | heightInTiles : number, 38 | startx: number, 39 | starty: number, 40 | endx: number, 41 | endy: number, 42 | status: string, 43 | bounds: [LatLng, LatLng], 44 | phys: {width: number, height: number}, 45 | min: {y: number, x: number}, 46 | max: {y: number, x: number}, 47 | type: 'albedo'|'heightmap', 48 | url: string, 49 | }; 50 | 51 | import PNG from "./png"; 52 | 53 | export type TileLoadState = ConfigState & {x: number, y: number, heights: Float32Array|Uint8Array, buffer: ArrayBuffer}; 54 | 55 | export enum NormaliseMode { 56 | Off = 0, 57 | Regular = 1, 58 | Smart = 2, 59 | SmartWindow = 3, 60 | Fixed = 4, 61 | } 62 | 63 | export const roll = (num: number, min: number = 0, max: number = 1) => modWithNeg(num - min, max - min) + min; 64 | export const modWithNeg = (x: number, mod: number) => ((x % mod) + mod) % mod; 65 | export const clamp = (num : number, min: number = 0, max: number = 1) => Math.max(min, Math.min(max, num)); 66 | 67 | /** 68 | * Same as Promise.all(items.map(item => task(item))), but it waits for 69 | * the first {batchSize} promises to finish before starting the next batch. 70 | * 71 | * @template A 72 | * @template B 73 | * @param {function(A): B} task The task to run for each item. 74 | * @param {A[]} items Arguments to pass to the task for each call. 75 | * @param {int} batchSize 76 | * @returns {Promise} 77 | */ 78 | export async function promiseAllInBatches(task : (item : T) => Promise|B, items : T[], batchSize : number, to : number = 0) : Promise { 79 | let position = 0; 80 | let results : B[] = []; 81 | while (position < items.length) { 82 | const itemsForBatch = items.slice(position, position + batchSize); 83 | results = [...results, ...await Promise.all(itemsForBatch.map(item => task(item)))]; 84 | position += batchSize; 85 | if (to > 0) { 86 | await new Promise((resolve) => setTimeout(resolve, to)); 87 | } 88 | } 89 | return results; 90 | } 91 | 92 | export function roundDigits(num : number, scale : number) : number { 93 | if(!("" + num).includes("e")) { 94 | return +(Math.round(parseFloat(num + "e+" + scale)) + "e-" + scale); 95 | } else { 96 | var arr = ("" + num).split("e"); 97 | var sig = "" 98 | if(+arr[1] + scale > 0) { 99 | sig = "+"; 100 | } 101 | return +(Math.round(parseFloat(+arr[0] + "e" + sig + (+arr[1] + scale))) + "e-" + scale); 102 | } 103 | } 104 | 105 | export function localFormatNumber(num : number, scale : number) : string { 106 | return roundDigits(num, scale).toLocaleString(); 107 | } -------------------------------------------------------------------------------- /src/im/Octicons-mark-github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/image.ts: -------------------------------------------------------------------------------- 1 | const loaded = (im : HTMLImageElement, timeout = 10000) : Promise => { 2 | if (im.complete || im.naturalWidth > 0) { 3 | return Promise.resolve(im); 4 | } else { 5 | return new Promise((resolve, reject) => { 6 | const to = setTimeout(() => reject('Image not loaded'), timeout); 7 | im.addEventListener('load', () => { 8 | clearTimeout(to); 9 | resolve(im); 10 | }); 11 | }); 12 | } 13 | } 14 | 15 | export default {loaded} -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import './sass/main.scss'; 2 | 3 | import header from "./templates/header.html"; 4 | import footer from "./templates/footer.html"; 5 | 6 | import * as $ from "jquery"; 7 | import App from "./app"; 8 | 9 | $('body').prepend(header); 10 | $('body').append(footer); 11 | 12 | document.addEventListener('DOMContentLoaded', () => { 13 | 14 | // Get all "navbar-burger" elements 15 | const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0); 16 | 17 | // Add a click event on each of them 18 | $navbarBurgers.forEach( (el : HTMLElement) => { 19 | el.addEventListener('click', () => { 20 | 21 | // Get the target from the "data-target" attribute 22 | const target = el.dataset.target; 23 | const $target = document.getElementById(target); 24 | 25 | // Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu" 26 | el.classList.toggle('is-active'); 27 | $target.classList.toggle('is-active'); 28 | 29 | }); 30 | }); 31 | 32 | const container : HTMLElement = document.getElementById('app'); 33 | if (container) { 34 | const app = new App({container}); 35 | //@ts-ignore 36 | window.app = app; 37 | 38 | // const url = 'public/test/tiny16bitpng.png'; 39 | // App.getPngFromUrl(url).then(png => { 40 | // return app.typedArrayToStl( 41 | // png.getImageDataTransformed(), 42 | // png.getWidth(), 43 | // png.getHeight() 44 | // ); 45 | // }).then((stlData) => { 46 | // return app.download(new Blob([stlData]), 'stl.stl'); 47 | // }); 48 | } 49 | }); -------------------------------------------------------------------------------- /src/nextzen.ts: -------------------------------------------------------------------------------- 1 | import {format} from "./helpers"; 2 | 3 | const KEY = 'FOkBTi_OQyaSFYGMo5x_-Q'; 4 | 5 | export default class NextZen { 6 | static baseUrl: string = "https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png?api_key={key}"; 7 | static tileWidth: number = 256; 8 | static tileHeight: number = 256; 9 | static getUrl(args : {x : number, y : number, z : number}) : string { 10 | return format(this.baseUrl, {...args, key: KEY}); 11 | } 12 | static getApiKeyedUrl() { 13 | return format(this.baseUrl, {key: KEY}); 14 | } 15 | } -------------------------------------------------------------------------------- /src/png.ts: -------------------------------------------------------------------------------- 1 | const pako = require('pako'); 2 | 3 | const pngHeader = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]); 4 | 5 | import {TypedArray} from "./helpers"; 6 | 7 | import {default as bufferTools} from "./buffer"; 8 | 9 | type byte1 = number; 10 | type byte1unsigned = number; 11 | type byte2 = number; 12 | type byte2unsigned = number; 13 | type byte4 = number; 14 | type byte4unsigned = number; 15 | 16 | enum ColorType { 17 | Grayscale = 0, 18 | Rgb = 2, 19 | Palette = 3, 20 | GrayscaleAlpha = 4, 21 | Rgba = 6 22 | } 23 | enum sRGBTypes { 24 | Perceptual = 0, 25 | RelativeColorimetric = 1, 26 | Saturation = 2, 27 | AbsoluteColorimetric = 3, 28 | }; 29 | enum ResolutionUnit { 30 | Unknown = 0, 31 | Meter = 1, 32 | }; 33 | enum PngLineFilter { 34 | None = 0, 35 | Sub = 1, 36 | Up = 2, 37 | Average = 3, 38 | Paeth = 4, 39 | }; 40 | 41 | type IHDR = { 42 | size: number, 43 | header: string, 44 | width: number, 45 | height: number, 46 | bitDepth: number, 47 | colorType: ColorType, 48 | compressionMethod: number, 49 | filterMethod: number, 50 | interlaceMethod: number 51 | } 52 | 53 | type PLTE = Uint8Array; 54 | type tRNS = TypedArray|number; 55 | type sRGB = sRGBTypes; 56 | type gAMA = number; 57 | type cHRM = { 58 | whitePointX: number, 59 | whitePointY: number, 60 | redX: number, 61 | redY: number, 62 | greenX: number, 63 | greenY: number, 64 | blueX: number, 65 | blueY: number, 66 | }; 67 | type iCCP = { 68 | profileName: string, 69 | compressionMethod: byte1unsigned, 70 | profile: TypedArray, 71 | compressedProfile: TypedArray, 72 | }; 73 | type tEXt = { 74 | keyword: string, 75 | text: string 76 | }; 77 | type zTXt = { 78 | keyword: string, 79 | compressionMethod: byte1unsigned, 80 | compressedText: TypedArray, 81 | text: string 82 | }; 83 | type iTXt = { 84 | keyword: string, 85 | compressionFlag: boolean, 86 | compressionMethod: byte1unsigned, 87 | languageTag: string, 88 | translatedKeyword: string, 89 | text: string 90 | }; 91 | type bKGD = TypedArray|number; 92 | type pHYs = { 93 | x: byte4unsigned, 94 | y: byte4unsigned, 95 | unit: ResolutionUnit, 96 | }; 97 | type tIME = { 98 | year: byte2, 99 | month: byte1unsigned, 100 | day: byte1unsigned, 101 | hour: byte1unsigned, 102 | minute: byte1unsigned, 103 | second: byte1unsigned, 104 | }; 105 | 106 | type ChunkDataContents = IHDR|PLTE|tRNS|sRGB|gAMA|cHRM|iCCP|tEXt|zTXt|iTXt|bKGD|pHYs|tIME; 107 | 108 | type ChunkData = { 109 | type: string, 110 | length: number, 111 | startOffset: number, 112 | dataOffset: number, 113 | data?: ChunkDataContents, 114 | } 115 | 116 | type DecodeImageArgs = { 117 | transparency: tRNS, 118 | palette: PLTE 119 | }; 120 | 121 | export default class PNG { 122 | buffer: ArrayBuffer; 123 | dataview: DataView; 124 | offset: number = 0; 125 | chunks: ChunkData[]; 126 | ihdr: IHDR; 127 | construct() {} 128 | static fromBuffer(buffer: ArrayBuffer) { 129 | return (new PNG).fromBuffer(buffer); 130 | } 131 | assertHasData() { 132 | this.assertHasDataview(); 133 | this.assertHasBuffer(); 134 | } 135 | assertHasBuffer() { 136 | if (!this.buffer) { 137 | throw new Error('Missing buffer'); 138 | } 139 | } 140 | assertHasDataview() { 141 | if (!this.dataview) { 142 | throw new Error('Missing dataview'); 143 | } 144 | } 145 | fromBuffer(buffer: ArrayBuffer) { 146 | if (!this.isPngBuffer(buffer)) { 147 | throw new Error('Buffer was not a valid PNG'); 148 | } 149 | this.buffer = buffer; 150 | this.dataview = new DataView(buffer); 151 | delete this.chunks; 152 | delete this.ihdr; 153 | return this; 154 | } 155 | checkTypedArrayEquality(view1: TypedArray, view2: TypedArray) { 156 | if (view1.byteLength != view1.byteLength) return false; 157 | if (view1.length != view1.length) return false; 158 | for (var i = 0 ; i != view1.length ; i++) 159 | { 160 | if (view1[i] != view1[i]) return false; 161 | } 162 | return true; 163 | } 164 | isPngBuffer(buffer: ArrayBuffer) : boolean { 165 | return this.checkTypedArrayEquality(pngHeader, new Uint8Array(buffer, 0, 8)); 166 | } 167 | readAscii(length: number = 1) : string { 168 | this.assertHasData(); 169 | let str = []; 170 | if (length < 0) throw new Error('Cannot have negative length'); 171 | for (let i = 0; i < length; i++) { 172 | str.push(String.fromCharCode(this.dataview.getUint8(this.offset+i))); 173 | } 174 | this.offset += length; 175 | return str.join(""); 176 | } 177 | readAsciiUntilNull(maxLength : number = 100000000) : string { 178 | this.assertHasData(); 179 | let str = []; 180 | if (length < 0) throw new Error('Cannot have negative length'); 181 | let i = 0; 182 | while (i++ < maxLength) { 183 | const byte = this.dataview.getUint8(this.offset); 184 | this.offset++; 185 | if (byte > 0) { 186 | str.push(String.fromCharCode(byte)); 187 | } else { 188 | break; 189 | } 190 | } 191 | return str.join(""); 192 | } 193 | asciiSafeByteStr(byte : number) : string { 194 | if (byte === 9) { 195 | return "\\t"; 196 | } 197 | if (byte === 12) { 198 | return "\\0x0C"; 199 | } 200 | if (byte === 13) { 201 | return "\\r"; 202 | } 203 | if (byte === 10) { 204 | return "\\n"; 205 | } 206 | if ( 207 | ( 208 | byte <= 31 && 209 | byte !== 10 210 | ) || 211 | ( 212 | byte >= 127 && 213 | byte <= 159 214 | ) 215 | ) { 216 | return '' 217 | } 218 | return String.fromCharCode(byte); 219 | } 220 | readSafeAsciiUntilNull(maxLength : number = 100000000) : string { 221 | this.assertHasData(); 222 | let str = []; 223 | if (length < 0) throw new Error('Cannot have negative length'); 224 | let i = 0; 225 | while (i++ < maxLength) { 226 | const byte = this.dataview.getUint8(this.offset); 227 | this.offset++; 228 | if (byte > 0) { 229 | str.push(this.asciiSafeByteStr(byte)); 230 | } else { 231 | break; 232 | } 233 | } 234 | return str.join(""); 235 | } 236 | readUTF8UntilNull(maxLength : number = 10000000) : string { 237 | this.assertHasData(); 238 | let len = 0; 239 | let byte = 0; 240 | do { 241 | byte = this.dataview.getUint8(this.offset + len); 242 | len++; 243 | } while (byte !== 0 && len < maxLength); 244 | const result = new TextDecoder().decode(new Uint8Array(this.buffer, this.offset, len)); 245 | this.offset += len; 246 | return result; 247 | } 248 | readUint8() : number { 249 | this.assertHasData(); 250 | this.offset += 1; 251 | return this.dataview.getUint8(this.offset-1); 252 | } 253 | readInt8() : number { 254 | this.assertHasData(); 255 | this.offset += 1; 256 | return this.dataview.getInt8(this.offset-1); 257 | } 258 | readInt16() : number { 259 | this.assertHasData(); 260 | this.offset += 2; 261 | return this.dataview.getInt16(this.offset-2, false); 262 | } 263 | readUint16() : number { 264 | this.assertHasData(); 265 | this.offset += 2; 266 | return this.dataview.getUint16(this.offset-2, false); 267 | } 268 | readInt32() : number { 269 | this.assertHasData(); 270 | this.offset += 4; 271 | return this.dataview.getInt32(this.offset-4, false); 272 | } 273 | readUint32() : number { 274 | this.assertHasData(); 275 | this.offset += 4; 276 | return this.dataview.getUint32(this.offset-4, false); 277 | } 278 | readBigInt64() : bigint { 279 | this.assertHasData(); 280 | this.offset += 8; 281 | return this.dataview.getBigInt64(this.offset-8, false); 282 | } 283 | readBigUint64() : bigint { 284 | this.assertHasData(); 285 | this.offset += 8; 286 | return this.dataview.getBigUint64(this.offset-8, false); 287 | } 288 | readFloat32() : number { 289 | this.assertHasData(); 290 | this.offset += 4; 291 | return this.dataview.getFloat32(this.offset-4, false); 292 | } 293 | readFloat64() : number { 294 | this.assertHasData(); 295 | this.offset += 8; 296 | return this.dataview.getFloat64(this.offset-8, false); 297 | } 298 | isTypedArray(data : any) : data is TypedArray { 299 | return ( 300 | (data instanceof Int8Array) || 301 | (data instanceof Uint8Array) || 302 | (data instanceof Uint8ClampedArray) || 303 | (data instanceof Int16Array) || 304 | (data instanceof Uint16Array) || 305 | (data instanceof Int32Array) || 306 | (data instanceof Uint32Array) || 307 | (data instanceof Float32Array) || 308 | (data instanceof Float64Array) 309 | ); 310 | } 311 | chunkIsIHDR(chunk : ChunkDataContents) : chunk is IHDR { 312 | if (typeof chunk !== 'undefined' && chunk !== null) { 313 | return true; 314 | } 315 | return false; 316 | } 317 | chunkIsPLTE(chunk : ChunkDataContents) : chunk is PLTE { 318 | return (chunk instanceof Uint8Array); 319 | } 320 | chunkIstRNS(chunk : ChunkDataContents) : chunk is tRNS { 321 | return this.isTypedArray(chunk); 322 | } 323 | chunkIssRGB(chunk : ChunkDataContents) : chunk is sRGB { 324 | return (typeof chunk === 'number' && chunk in sRGBTypes); 325 | } 326 | chunkIsgAMA(chunk : ChunkDataContents) : chunk is gAMA { 327 | return typeof chunk === 'number'; 328 | } 329 | chunkIscHRM(chunk : ChunkDataContents) : chunk is cHRM { 330 | if ( 331 | typeof chunk !== 'undefined' && 332 | chunk !== null && 333 | typeof chunk === 'object' 334 | ) { 335 | return ( 336 | typeof (chunk as cHRM).whitePointX === 'number' && 337 | typeof (chunk as cHRM).whitePointY === 'number' && 338 | typeof (chunk as cHRM).redX === 'number' && 339 | typeof (chunk as cHRM).redY === 'number' && 340 | typeof (chunk as cHRM).greenX === 'number' && 341 | typeof (chunk as cHRM).greenY === 'number' && 342 | typeof (chunk as cHRM).blueX === 'number' && 343 | typeof (chunk as cHRM).blueY === 'number' 344 | ); 345 | } 346 | return false; 347 | } 348 | chunkIsiCCP(chunk : ChunkDataContents) : chunk is iCCP { 349 | if ( 350 | typeof chunk !== 'undefined' && 351 | chunk !== null && 352 | typeof chunk === 'object' 353 | ) { 354 | return ( 355 | typeof (chunk as iCCP).profileName === 'string' && 356 | typeof (chunk as iCCP).compressionMethod === 'number' && 357 | this.isTypedArray((chunk as iCCP).profile) && 358 | this.isTypedArray((chunk as iCCP).compressedProfile) 359 | ); 360 | } 361 | return false; 362 | } 363 | chunkIstEXt(chunk : ChunkDataContents) : chunk is tEXt { 364 | if ( 365 | typeof chunk !== 'undefined' && 366 | chunk !== null && 367 | typeof chunk === 'object' 368 | ) { 369 | return ( 370 | typeof (chunk as tEXt).keyword === 'string' && 371 | typeof (chunk as tEXt).text === 'string' 372 | ); 373 | } 374 | return false; 375 | } 376 | chunkIszTXt(chunk : ChunkDataContents) : chunk is zTXt { 377 | if ( 378 | typeof chunk !== 'undefined' && 379 | chunk !== null && 380 | typeof chunk === 'object' 381 | ) { 382 | return ( 383 | typeof (chunk as zTXt).keyword === 'string' && 384 | typeof (chunk as zTXt).compressionMethod === 'number' && 385 | this.isTypedArray((chunk as zTXt).compressedText) && 386 | typeof (chunk as zTXt).text === 'string' 387 | ); 388 | } 389 | return false; 390 | } 391 | chunkIsiTXt(chunk : ChunkDataContents) : chunk is iTXt { 392 | if ( 393 | typeof chunk !== 'undefined' && 394 | chunk !== null && 395 | typeof chunk === 'object' 396 | ) { 397 | return ( 398 | typeof (chunk as iTXt).keyword === 'string' && 399 | typeof (chunk as iTXt).compressionMethod === 'number' && 400 | typeof (chunk as iTXt).compressionFlag === 'boolean' && 401 | typeof (chunk as iTXt).languageTag === 'string' && 402 | typeof (chunk as iTXt).translatedKeyword === 'string' && 403 | typeof (chunk as iTXt).text === 'string' 404 | ); 405 | } 406 | return false; 407 | } 408 | chunkIsbKGD(chunk : ChunkDataContents) : chunk is bKGD { 409 | return this.isTypedArray(chunk) || typeof chunk === 'number'; 410 | } 411 | chunkIspHYs(chunk : ChunkDataContents) : chunk is pHYs { 412 | if ( 413 | typeof chunk !== 'undefined' && 414 | chunk !== null && 415 | typeof chunk === 'object' 416 | ) { 417 | return ( 418 | typeof (chunk as pHYs).x === 'number' && 419 | typeof (chunk as pHYs).y === 'number' && 420 | (chunk as pHYs).unit in ResolutionUnit 421 | ); 422 | } 423 | return false; 424 | } 425 | chunkIstIME(chunk : ChunkDataContents) : chunk is tIME { 426 | if ( 427 | typeof chunk !== 'undefined' && 428 | chunk !== null && 429 | typeof chunk === 'object' 430 | ) { 431 | return ( 432 | typeof (chunk as tIME).year === 'number' && 433 | typeof (chunk as tIME).month === 'number' && 434 | typeof (chunk as tIME).day === 'number' && 435 | typeof (chunk as tIME).hour === 'number' && 436 | typeof (chunk as tIME).minute === 'number' && 437 | typeof (chunk as tIME).second === 'number' 438 | ); 439 | } 440 | return false; 441 | } 442 | getIHDR() : IHDR { 443 | this.assertHasData(); 444 | if (!this.ihdr) { 445 | this.offset = 8; 446 | const dataview = this.dataview; 447 | const size = this.readUint32(); 448 | const header = this.readAscii(4); 449 | return this.ihdr = { 450 | size, 451 | header, 452 | width: this.readUint32(), 453 | height: this.readUint32(), 454 | bitDepth: this.readUint8(), 455 | colorType: this.readUint8(), 456 | compressionMethod: this.readUint8(), 457 | filterMethod: this.readUint8(), 458 | interlaceMethod: this.readUint8() 459 | }; 460 | } 461 | return this.ihdr; 462 | } 463 | getWidth() : number { 464 | return this.getIHDR().width; 465 | } 466 | getHeight() : number { 467 | return this.getIHDR().height; 468 | } 469 | getArea() : number { 470 | return this.getWidth() * this.getHeight(); 471 | } 472 | getChunksByType(...types : string[][]|string[]) : ChunkData[] { 473 | const ntypes : string[] = types.flat(2); 474 | return this.getChunkData().filter((a) => ntypes.indexOf(a.type) !== -1); 475 | } 476 | getPLTE() : PLTE|null { 477 | const chunks = this.getChunksByType('PLTE'); 478 | if (chunks.length > 1) { 479 | throw new Error('Cannot have more than 1 PLTE chunk.'); 480 | } 481 | if (chunks.length === 1) { 482 | if (chunks[0].length % 3 !== 0) { 483 | throw new Error('Cannot have PLTE not a multiple of 3, length was ' + chunks[0].length); 484 | } 485 | let chunkData = chunks[0].data; 486 | if (!this.chunkIsPLTE(chunkData)) { 487 | chunkData = new Uint8Array(this.buffer, chunks[0].dataOffset, chunks[0].length); 488 | chunks[0].data = chunkData; 489 | } 490 | return chunkData; 491 | } 492 | return null; 493 | } 494 | gettRNS() : tRNS|null { 495 | const ihdr = this.getIHDR(); 496 | const chunks = this.getChunksByType('tRNS'); 497 | if (chunks.length > 1) { 498 | throw new Error('Cannot have more than 1 tRNS chunk.'); 499 | } 500 | if (chunks.length === 1) { 501 | let chunkData = chunks[0].data; 502 | if (this.chunkIstRNS(chunkData)) { 503 | return chunkData; 504 | } else { 505 | let trns = null; 506 | if (ihdr.colorType === ColorType.Palette) { 507 | trns = new Uint8Array(this.buffer, chunks[0].dataOffset, chunks[0].length); 508 | } else if (ihdr.colorType === ColorType.Grayscale) { 509 | if (ihdr.bitDepth === 8) { 510 | trns = this.dataview.getUint8(chunks[0].dataOffset+1); 511 | } else if (ihdr.bitDepth === 16) { 512 | trns = this.dataview.getUint16(chunks[0].dataOffset); 513 | } 514 | } else if (ihdr.colorType === ColorType.Rgb) { 515 | trns = new Uint16Array(this.buffer, chunks[0].dataOffset, chunks[0].length); 516 | } else { 517 | trns = null; 518 | throw new Error(`Color type ${ihdr.colorType} cannot have transparency`); 519 | } 520 | chunks[0].data = trns 521 | return trns; 522 | } 523 | } 524 | return null; 525 | } 526 | getgAMA() : gAMA|null { 527 | const ihdr = this.getIHDR(); 528 | const chunks = this.getChunksByType('gAMA'); 529 | if (chunks.length > 1) { 530 | throw new Error('Cannot have more than 1 gAMA chunk.'); 531 | } 532 | if (chunks.length === 1) { 533 | let gama = chunks[0].data; 534 | if (!this.chunkIsgAMA(gama)) { 535 | gama = (this.dataview.getUint32(chunks[0].dataOffset, false))/100000; 536 | chunks[0].data = gama; 537 | } 538 | return gama; 539 | } 540 | return null; 541 | } 542 | getcHRM() : cHRM|null { 543 | const ihdr = this.getIHDR(); 544 | const chunks = this.getChunksByType('cHRM'); 545 | if (chunks.length > 1) { 546 | throw new Error('Cannot have more than 1 cHRM chunk.'); 547 | } 548 | if (chunks.length === 1) { 549 | let chunkData = chunks[0].data; 550 | if (!this.chunkIscHRM(chunkData)) { 551 | const off = chunks[0].dataOffset; 552 | const chrm : cHRM = { 553 | whitePointX: (this.dataview.getUint32(off + ( 4 * 0 ), false) / 100000), 554 | whitePointY: (this.dataview.getUint32(off + ( 4 * 1 ), false) / 100000), 555 | redX: (this.dataview.getUint32(off + ( 4 * 2 ), false) / 100000), 556 | redY: (this.dataview.getUint32(off + ( 4 * 3 ), false) / 100000), 557 | greenX: (this.dataview.getUint32(off + ( 4 * 4 ), false) / 100000), 558 | greenY: (this.dataview.getUint32(off + ( 4 * 5 ), false) / 100000), 559 | blueX: (this.dataview.getUint32(off + ( 4 * 6 ), false) / 100000), 560 | blueY: (this.dataview.getUint32(off + ( 4 * 7 ), false) / 100000), 561 | } 562 | chunkData = chrm; 563 | chunks[0].data = chunkData; 564 | } 565 | return chunkData; 566 | } 567 | return null; 568 | } 569 | getsRGB() : sRGB|null { 570 | const chunks = this.getChunksByType('sRGB'); 571 | if (chunks.length > 1) { 572 | throw new Error('Cannot have more than 1 sRGB chunk.'); 573 | } 574 | if (chunks.length === 1) { 575 | let chunkData = chunks[0].data; 576 | if (!this.chunkIssRGB(chunkData)) { 577 | chunkData = this.dataview.getUint8(chunks[0].dataOffset); 578 | chunks[0].data = chunkData; 579 | } 580 | return chunkData; 581 | } 582 | return null; 583 | } 584 | getiCCP() : iCCP|null { 585 | const ihdr = this.getIHDR(); 586 | const chunks = this.getChunksByType('iCCP'); 587 | if (chunks.length > 1) { 588 | throw new Error('Cannot have more than 1 iCCP chunk.'); 589 | } 590 | if (chunks.length === 1) { 591 | let iccp = chunks[0].data 592 | if (!this.chunkIsiCCP(iccp)) { 593 | 594 | this.offset = chunks[0].dataOffset; 595 | 596 | const profileName = this.readSafeAsciiUntilNull(); 597 | const compressionMethod = this.readUint8(); 598 | const dataLength = chunks[0].length - (this.offset - chunks[0].dataOffset); 599 | const compressedProfile = new Uint8Array(this.buffer, this.offset, dataLength); 600 | const profile = pako.inflate(compressedProfile); 601 | 602 | iccp = { 603 | profileName, 604 | compressionMethod, 605 | compressedProfile, 606 | profile 607 | }; 608 | chunks[0].data = iccp; 609 | } 610 | return iccp; 611 | } 612 | return null; 613 | } 614 | gettEXt() : tEXt[]|null { 615 | const ihdr = this.getIHDR(); 616 | const chunks = this.getChunksByType('tEXt'); 617 | const textChunks : tEXt[] = []; 618 | chunks.forEach(chunk => { 619 | let textChunk = chunk.data; 620 | if (!this.chunkIstEXt(textChunk)) { 621 | this.offset = chunk.dataOffset; 622 | const keyword = this.readSafeAsciiUntilNull(chunk.length); 623 | const length = chunk.length - (this.offset - chunk.dataOffset); 624 | const text = this.readSafeAsciiUntilNull(length); 625 | textChunk = { 626 | keyword, 627 | text 628 | } 629 | chunk.data = textChunk; 630 | } 631 | textChunks.push(textChunk); 632 | }); 633 | return textChunks; 634 | } 635 | getzTXt() : zTXt[]|null { 636 | const ihdr = this.getIHDR(); 637 | const chunks = this.getChunksByType('zTXt'); 638 | const textChunks : zTXt[] = []; 639 | chunks.forEach(chunk => { 640 | let textChunk = chunk.data; 641 | if (!this.chunkIszTXt(textChunk)) { 642 | this.offset = chunk.dataOffset; 643 | 644 | const keyword = this.readSafeAsciiUntilNull(); 645 | const compressionMethod = this.readUint8(); 646 | const dataLength = chunk.length - (this.offset - chunk.dataOffset); 647 | const compressedText = new Uint8Array(this.buffer, this.offset, dataLength); 648 | const text = pako.inflate(compressedText, { to: 'string' }); 649 | 650 | textChunk = { 651 | keyword, 652 | compressionMethod, 653 | compressedText, 654 | text 655 | } 656 | chunk.data = textChunk; 657 | } 658 | textChunks.push(textChunk); 659 | }); 660 | return textChunks; 661 | } 662 | getiTXt() : iTXt[]|null { 663 | const ihdr = this.getIHDR(); 664 | const chunks = this.getChunksByType('iTXt'); 665 | const textChunks : iTXt[] = []; 666 | chunks.forEach(chunk => { 667 | let textChunk = chunk.data; 668 | if (!this.chunkIsiTXt(textChunk)) { 669 | this.offset = chunk.dataOffset; 670 | 671 | const keyword = this.readSafeAsciiUntilNull(); 672 | const compressionFlag = this.readUint8() > 0 ? true : false; 673 | const compressionMethod = this.readUint8(); 674 | const languageTag = this.readSafeAsciiUntilNull(); 675 | const translatedKeyword = this.readUTF8UntilNull(); 676 | 677 | const dataLength = chunk.length - (this.offset - chunk.dataOffset); 678 | let text = ''; 679 | if (compressionFlag) { 680 | const compressedText = new Uint8Array(this.buffer, this.offset, dataLength); 681 | text = pako.inflate(compressedText, { to: 'string' }); 682 | } else { 683 | text = this.readUTF8UntilNull(dataLength); 684 | } 685 | 686 | const itxt : iTXt = textChunk = { 687 | keyword, 688 | compressionFlag, 689 | compressionMethod, 690 | languageTag, 691 | translatedKeyword, 692 | text, 693 | }; 694 | chunk.data = textChunk; 695 | } 696 | textChunks.push(textChunk); 697 | }); 698 | return textChunks; 699 | } 700 | getbKGD() : bKGD|null { 701 | const ihdr = this.getIHDR(); 702 | const chunks = this.getChunksByType('bKGD'); 703 | if (chunks.length > 1) { 704 | throw new Error('Cannot have more than 1 tRNS chunk.'); 705 | } 706 | if (chunks.length === 1) { 707 | let chunkData = chunks[0].data; 708 | if (this.chunkIsbKGD(chunkData)) { 709 | return chunkData; 710 | } else { 711 | let bkgd = null; 712 | if (ihdr.colorType === ColorType.Palette) { 713 | bkgd = new Uint8Array(this.buffer, chunks[0].dataOffset, chunks[0].length); 714 | } else if (ihdr.colorType === ColorType.Grayscale || ihdr.colorType === ColorType.GrayscaleAlpha) { 715 | if (ihdr.bitDepth === 8) { 716 | bkgd = this.dataview.getUint8(chunks[0].dataOffset+1); 717 | } else if (ihdr.bitDepth === 16) { 718 | bkgd = this.dataview.getUint16(chunks[0].dataOffset); 719 | } 720 | } else if (ihdr.colorType === ColorType.Rgb || ihdr.colorType === ColorType.Rgba) { 721 | bkgd = new Uint16Array(this.buffer, chunks[0].dataOffset, chunks[0].length); 722 | } else { 723 | throw new Error(`Color type ${ihdr.colorType} cannot have bKGD`); 724 | } 725 | chunks[0].data = bkgd 726 | return bkgd; 727 | } 728 | } 729 | return null; 730 | } 731 | getpHYs() : pHYs|null { 732 | const chunks = this.getChunksByType('pHYs'); 733 | if (chunks.length > 1) { 734 | throw new Error('Cannot have more than 1 pHYs chunk.'); 735 | } 736 | if (chunks.length === 1) { 737 | let phys = chunks[0].data; 738 | if (!this.chunkIspHYs(phys)) { 739 | this.offset = chunks[0].dataOffset; 740 | const x = this.readUint32(); 741 | const y = this.readUint32(); 742 | const unit = this.readUint8(); 743 | phys = { 744 | x, 745 | y, 746 | unit 747 | }; 748 | chunks[0].data = phys 749 | } 750 | return phys; 751 | } 752 | return null; 753 | } 754 | gettIME() : tIME|null { 755 | const chunks = this.getChunksByType('tIME'); 756 | if (chunks.length > 1) { 757 | throw new Error('Cannot have more than 1 tIME chunk.'); 758 | } 759 | if (chunks.length === 1) { 760 | let time = chunks[0].data; 761 | if (!this.chunkIstIME(time)) { 762 | this.offset = chunks[0].dataOffset; 763 | 764 | const year = this.readUint16(); 765 | const month = this.readUint8(); 766 | const day = this.readUint8(); 767 | const hour = this.readUint8(); 768 | const minute = this.readUint8(); 769 | const second = this.readUint8(); 770 | 771 | time = { 772 | year, 773 | month, 774 | day, 775 | hour, 776 | minute, 777 | second, 778 | }; 779 | chunks[0].data = time 780 | } 781 | return time; 782 | } 783 | return null; 784 | } 785 | getChunkData() : ChunkData[] { 786 | this.assertHasData(); 787 | if (!this.chunks) { 788 | this.chunks = []; 789 | this.offset = 8; 790 | const MAX = 100000; 791 | let i = 0; 792 | while (this.offset < this.buffer.byteLength && i++ < MAX) { 793 | const startOff = this.offset; 794 | const chunk : ChunkData = { 795 | length : this.readUint32(), 796 | type : this.readAscii(4), 797 | startOffset : startOff, 798 | dataOffset : startOff + 8 799 | }; 800 | this.offset = startOff + 8 + 4 + chunk.length; 801 | this.chunks.push(chunk); 802 | } 803 | } 804 | return this.chunks; 805 | } 806 | populateChunks() : this { 807 | this.getChunkData(); 808 | this.getPLTE(); 809 | this.gettRNS(); 810 | this.getgAMA(); 811 | this.getcHRM(); 812 | this.getsRGB(); 813 | this.getiCCP(); 814 | this.gettEXt(); 815 | this.getzTXt(); 816 | this.getiTXt(); 817 | this.getbKGD(); 818 | this.getpHYs(); 819 | this.gettIME(); 820 | return this; 821 | } 822 | getImageDataRaw() { 823 | const idats = this.getChunksByType('IDAT'); 824 | const imdataLength = idats.reduce((a, b) => a + b.length, 0); 825 | const intermediateBuffer = new ArrayBuffer(imdataLength); 826 | const intermediateArray = new Uint8Array(intermediateBuffer); 827 | 828 | let curOff = 0; 829 | for (let idat of idats) { 830 | intermediateArray.set(new Uint8Array(this.buffer, idat.dataOffset, idat.length), curOff); 831 | curOff += idat.length; 832 | } 833 | 834 | const imageDataRaw = pako.inflate(intermediateArray); 835 | return imageDataRaw; 836 | } 837 | getImageDataTransformed() { 838 | const ihdr = this.getIHDR(); 839 | const imageDataRaw = this.getImageDataRaw(); 840 | 841 | if (ihdr.colorType === ColorType.Grayscale) { 842 | let res : TypedArray = new Uint8Array(imageDataRaw.buffer); 843 | res = this.filterZero(res, ihdr, 0); 844 | if (ihdr.bitDepth === 16) { 845 | const area = ihdr.width * ihdr.height; 846 | const res16 = new Uint16Array(area); 847 | const dv = new DataView(res.buffer); 848 | for (let i = 0; i < area; i++) { 849 | res16[i] = dv.getUint16(i*2, false); 850 | } 851 | return res16; 852 | } 853 | if (ihdr.bitDepth === 8) { 854 | return res; 855 | } 856 | } else if (ihdr.colorType === ColorType.Rgb) { 857 | let res : TypedArray = new Uint8Array(imageDataRaw.buffer); 858 | res = this.filterZero(res, ihdr, 0); 859 | if (ihdr.bitDepth === 16) { 860 | const area = ihdr.width * ihdr.height * 3; 861 | const res16 = new Uint16Array(area); 862 | const dv = new DataView(res.buffer); 863 | for (let i = 0; i < area; i++) { 864 | res16[i] = dv.getUint16(i*2, false); 865 | } 866 | return res16; 867 | } 868 | if (ihdr.bitDepth === 8) { 869 | return res; 870 | } 871 | } else if (ihdr.colorType === ColorType.Rgba) { 872 | let res : TypedArray = new Uint8Array(imageDataRaw.buffer); 873 | res = this.filterZero(res, ihdr, 0); 874 | if (ihdr.bitDepth === 16) { 875 | const area = ihdr.width * ihdr.height * 4; 876 | const res16 = new Uint16Array(area); 877 | const dv = new DataView(res.buffer); 878 | for (let i = 0; i < area; i++) { 879 | res16[i] = dv.getUint16(i*2, false); 880 | } 881 | return res16; 882 | } 883 | if (ihdr.bitDepth === 8) { 884 | return res; 885 | } 886 | } else if (ihdr.colorType === ColorType.GrayscaleAlpha) { 887 | let res : TypedArray = new Uint8Array(imageDataRaw.buffer); 888 | res = this.filterZero(res, ihdr, 0); 889 | if (ihdr.bitDepth === 16) { 890 | const area = ihdr.width * ihdr.height * 2; 891 | const res16 = new Uint16Array(area); 892 | const dv = new DataView(res.buffer); 893 | for (let i = 0; i < area; i++) { 894 | res16[i] = dv.getUint16(i*2, false); 895 | } 896 | return res16; 897 | } 898 | if (ihdr.bitDepth === 8) { 899 | return res; 900 | } 901 | } 902 | } 903 | getImageData() { 904 | const transparency = this.gettRNS(); 905 | const palette = this.getPLTE(); 906 | return this.decodeImage( 907 | this.getImageDataTransformed(), 908 | this.getIHDR(), 909 | { 910 | transparency, 911 | palette, 912 | } 913 | ); 914 | } 915 | bitsPerPixel(colorType : ColorType, depth : number) : number { 916 | const noc : Record = { 917 | 0: 1, // ColorType.Grayscale 918 | 2: 3, // ColorType.Rgb 919 | 3: 1, // ColorType.Palette 920 | 4: 2, // ColorType.GrayscaleAlpha 921 | 6: 4, // ColorType.Rgba 922 | }; 923 | return noc[colorType] * depth; 924 | } 925 | filterZero(data : TypedArray, {width, bitDepth, height, colorType} : IHDR, off : number = 0) { 926 | const w = width, h = height; 927 | let bpp = this.bitsPerPixel(colorType, bitDepth); 928 | const bpl = Math.ceil(w * bpp / 8); 929 | 930 | bpp = Math.ceil(bpp / 8); 931 | 932 | let i; 933 | let di; 934 | let type = data[off]; 935 | let x = 0; 936 | 937 | if (type > 1) { 938 | data[off] = [0, 0, 1][type - 2]; 939 | } 940 | if (type == 3) { 941 | for (x = bpp; x < bpl; x++) { 942 | data[x + 1] = (data[x + 1] + (data[x + 1 - bpp] >>> 1)) & 255; 943 | } 944 | } 945 | 946 | for (let y = 0; y < h; y++) { 947 | i = off + y * bpl; 948 | di = i + y + 1; 949 | type = data[di - 1]; x = 0; 950 | 951 | if (type == PngLineFilter.None) { 952 | for (; x < bpl; x++) { 953 | data[i + x] = data[di + x]; 954 | } 955 | } else if (type == PngLineFilter.Sub) { 956 | for (; x < bpp; x++) { 957 | data[i + x] = data[di + x]; 958 | } 959 | for (; x < bpl; x++) { 960 | data[i + x] = (data[di + x] + data[i + x - bpp]); 961 | } 962 | } else if (type == PngLineFilter.Up) { 963 | for (; x < bpl; x++) { 964 | data[i + x] = (data[di + x] + data[i + x - bpl]); 965 | } 966 | } else if (type == PngLineFilter.Average) { 967 | for (; x < bpp; x++) { 968 | data[i + x] = (data[di + x] + (data[i + x - bpl] >>> 1)); 969 | } 970 | for (; x < bpl; x++) { 971 | data[i + x] = (data[di + x] + ((data[i + x - bpl] + data[i + x - bpp]) >>> 1)); 972 | } 973 | } else if (type == PngLineFilter.Paeth) { 974 | for (; x < bpp; x++) { 975 | data[i + x] = (data[di + x] + this.unpaeth(0, data[i + x - bpl], 0)); 976 | } 977 | for (; x < bpl; x++) { 978 | data[i + x] = (data[di + x] + this.unpaeth(data[i + x - bpp], data[i + x - bpl], data[i + x - bpp - bpl])); 979 | } 980 | } 981 | } 982 | return data; 983 | } 984 | unpaeth (a : number, b : number, c : number) { 985 | const p = a + b - c; const pa = (p - a); const pb = (p - b); const pc = (p - c); 986 | if (pa * pa <= pb * pb && pa * pa <= pc * pc) return a; 987 | else if (pb * pb <= pc * pc) return b; 988 | return c; 989 | } 990 | decodeImage ( 991 | data : TypedArray, 992 | {width, bitDepth, height, colorType} : IHDR, 993 | { 994 | transparency = null, 995 | palette = null, 996 | } : DecodeImageArgs 997 | ) { 998 | const area = width * height; 999 | if (colorType == ColorType.Rgba) { 1000 | if (bitDepth == 8) { 1001 | return new Uint8Array(data).slice(0, area*4); 1002 | } 1003 | if (bitDepth == 16) { 1004 | return new Uint16Array(data).slice(0, area*4); 1005 | } 1006 | } else if (colorType == ColorType.Rgb) { 1007 | if (bitDepth == 8) { 1008 | return new Uint8Array(data).slice(0, area*3); 1009 | } 1010 | if (bitDepth == 16) { 1011 | return new Uint16Array(data).slice(0, area*3); 1012 | } 1013 | } else if (colorType == ColorType.Palette) { 1014 | const bpp = this.bitsPerPixel(colorType, bitDepth); 1015 | const bpl = Math.ceil(width * bpp / 8); // bytes per line 1016 | const bf = new Uint8Array(area * 4); 1017 | const p = palette; 1018 | const ap = transparency; 1019 | const tl = ap ? (typeof ap === 'number' ? 0 : ap.length) : 0; 1020 | // console.log(p, ap); 1021 | if (bitDepth == 1) { 1022 | for (var y = 0; y < height; y++) { 1023 | var s0 = y * bpl; 1024 | var t0 = y * width; 1025 | for (var i = 0; i < width; i++) { 1026 | var qi = (t0 + i) << 2; 1027 | var j = ((data[s0 + (i >> 3)] >> (7 - ((i & 7) << 0))) & 1); 1028 | var cj = 3 * j; 1029 | bf[qi] = p[cj]; 1030 | bf[qi + 1] = p[cj + 1]; 1031 | bf[qi + 2] = p[cj + 2]; 1032 | if (typeof ap === 'number') { 1033 | bf[qi + 3] = 255; 1034 | } else { 1035 | bf[qi + 3] = (j < tl) ? ap[j] : 255; 1036 | } 1037 | } 1038 | } 1039 | } 1040 | if (bitDepth == 2) { 1041 | for (var y = 0; y < height; y++) { 1042 | var s0 = y * bpl; 1043 | var t0 = y * width; 1044 | for (var i = 0; i < width; i++) { 1045 | var qi = (t0 + i) << 2; 1046 | var j = ((data[s0 + (i >> 2)] >> (6 - ((i & 3) << 1))) & 3); 1047 | var cj = 3 * j; 1048 | bf[qi] = p[cj]; 1049 | bf[qi + 1] = p[cj + 1]; 1050 | bf[qi + 2] = p[cj + 2]; 1051 | if (typeof ap === 'number') { 1052 | bf[qi + 3] = 255; 1053 | } else { 1054 | bf[qi + 3] = (j < tl) ? ap[j] : 255; 1055 | } 1056 | } 1057 | } 1058 | } 1059 | if (bitDepth == 4) { 1060 | for (var y = 0; y < height; y++) { 1061 | var s0 = y * bpl; 1062 | var t0 = y * width; 1063 | for (var i = 0; i < width; i++) { 1064 | var qi = (t0 + i) << 2; 1065 | var j = ((data[s0 + (i >> 1)] >> (4 - ((i & 1) << 2))) & 15); 1066 | var cj = 3 * j; 1067 | bf[qi] = p[cj]; 1068 | bf[qi + 1] = p[cj + 1]; 1069 | bf[qi + 2] = p[cj + 2]; 1070 | if (typeof ap === 'number') { 1071 | bf[qi + 3] = 255; 1072 | } else { 1073 | bf[qi + 3] = (j < tl) ? ap[j] : 255; 1074 | } 1075 | } 1076 | } 1077 | } 1078 | if (bitDepth == 8) { 1079 | for (var i = 0; i < area; i++) { 1080 | var qi = i << 2; 1081 | var j = data[i]; 1082 | var cj = 3 * j; 1083 | bf[qi] = p[cj]; 1084 | bf[qi + 1] = p[cj + 1]; 1085 | bf[qi + 2] = p[cj + 2]; 1086 | if (typeof ap === 'number') { 1087 | bf[qi + 3] = 255; 1088 | } else { 1089 | bf[qi + 3] = (j < tl) ? ap[j] : 255; 1090 | } 1091 | } 1092 | } 1093 | return bf; 1094 | } else if (colorType == ColorType.GrayscaleAlpha) { 1095 | if (bitDepth === 8) { 1096 | return new Uint8Array(data).slice(0, area * 2); 1097 | } 1098 | if (bitDepth === 16) { 1099 | return new Uint16Array(data).slice(0, area * 2); 1100 | } 1101 | throw new Error('Invalid bit depth'); 1102 | } else if (colorType == ColorType.Grayscale) { 1103 | if (bitDepth === 8 || bitDepth === 4 || bitDepth === 2 || bitDepth === 1) { 1104 | return new Uint8Array(data).slice(0, area); 1105 | } 1106 | if (bitDepth === 16) { 1107 | return new Uint16Array(data).slice(0, area); 1108 | } 1109 | throw new Error('Invalid bit depth'); 1110 | } 1111 | } 1112 | static Float32ArrayToPng16Bit(inp: Float32Array) : Uint8Array { 1113 | let u8 = new ArrayBuffer(inp.length*2); 1114 | let dv = new DataView(u8); 1115 | for (let i = 0; i < inp.length; i++) { 1116 | dv.setUint16(i * 2, inp[i]); 1117 | } 1118 | return new Uint8Array(u8); 1119 | } 1120 | static Uint16ArrayToPng16Bit(inp: Uint16Array) : Uint8Array { 1121 | let dv = new DataView(inp.buffer); 1122 | for (let i = 0; i < inp.length; i++) { 1123 | dv.setUint16(i * 2, inp[i]); 1124 | } 1125 | return new Uint8Array(inp.buffer); 1126 | } 1127 | static arrayToPng16Bit(inp: number[]|Uint16Array) : Uint8Array { 1128 | let u8 = new ArrayBuffer(inp.length*2); 1129 | let dv = new DataView(u8); 1130 | for (let i = 0; i < inp.length; i++) { 1131 | dv.setUint16(i * 2, inp[i]); 1132 | } 1133 | return new Uint8Array(u8); 1134 | } 1135 | terrariumToGrayscale() : Float32Array { 1136 | this.assertHasData(); 1137 | const pixels = this.getImageData(); 1138 | const area = this.getArea(); 1139 | const newPixels = new Float32Array(area); 1140 | // terrarium images are 24,8 decimal encodings with R representing 1141 | // the first 8 bits, G representing the next 8 and B representing the 1142 | // final 8 bits 1143 | // i.e. the height in metres is (R * 256) + (G) + (B / 256) 1144 | // so... 1145 | for (let i = 0; i < area; i++) { 1146 | newPixels[i] = ((pixels[(i*3)]*2**8) + (pixels[(i*3)+1])+ (pixels[(i*3)+2]/2**8)) - 2**15; 1147 | } 1148 | return newPixels; 1149 | } 1150 | } -------------------------------------------------------------------------------- /src/processor.ts: -------------------------------------------------------------------------------- 1 | import * as Comlink from "comlink"; 2 | 3 | import { 4 | TypedArray, 5 | TileCoords, 6 | LatLng, 7 | LatLngZoom, 8 | ConfigState, 9 | TileLoadState, 10 | NormaliseMode 11 | } from "./helpers"; 12 | 13 | import PNG from "./png"; 14 | 15 | export type NormaliseResult = { 16 | data: T, 17 | minBefore: number, 18 | maxBefore: number, 19 | minAfter: number, 20 | maxAfter: number 21 | }; 22 | 23 | export type TypedArrayToStlArgs = { 24 | width : number, 25 | depth : number, 26 | height: number 27 | }; 28 | export const typedArrayToStlDefaults : TypedArrayToStlArgs = { 29 | width : 100, 30 | depth : 100, 31 | height : 10, 32 | }; 33 | 34 | export type vec3 = [number, number, number]; 35 | export type trivec3 = [vec3, vec3, vec3, vec3]; 36 | 37 | export type NormRange = {from : number|null|undefined, to : number|null|undefined}; 38 | export const normMaxRange : NormRange = {from: -10929, to: 8848}; 39 | export const normDefaults : NormRange = {from: null, to: null}; 40 | 41 | const processor = { 42 | normaliseTypedArray(inp : T, norm: NormRange) : NormaliseResult { 43 | let bpe = 2; 44 | if (!Array.isArray(inp)) { 45 | if (inp instanceof Float32Array) { 46 | bpe = 2; 47 | } else { 48 | bpe = inp.BYTES_PER_ELEMENT; 49 | } 50 | } 51 | // For some reason, typescript does not think the reduce function as 52 | // used below is compatible with all typedarrays 53 | //@ts-ignore 54 | const max = (typeof norm.to === 'number') ? norm.to : inp.reduce((prev : number, cur : number) : number => Math.max(prev, cur), 0); 55 | //@ts-ignore 56 | const min = (typeof norm.from === 'number') ? norm.from : inp.reduce((prev : number, cur : number) : number => Math.min(prev, cur), max); 57 | const newMax = Math.pow(2, bpe * 8); 58 | const newMin = 0; 59 | const sub = max - min; 60 | const nsub = newMax - newMin; 61 | const factor = newMax/(max - sub); 62 | inp.forEach((a : number, index : number) => { 63 | if (a >= max) inp[index] = newMax; 64 | else if (a <= min) inp[index] = newMin; 65 | else inp[index] = (((a-min)/sub) * nsub + newMin); 66 | }); 67 | return { 68 | data: inp, 69 | minBefore: min, 70 | maxBefore: max, 71 | minAfter: newMin, 72 | maxAfter: newMax, 73 | }; 74 | }, 75 | normaliseTypedArraySmart(inp : T, norm: NormRange) : NormaliseResult { 76 | let bpe = 2; 77 | if (!Array.isArray(inp)) { 78 | if (inp instanceof Float32Array) { 79 | bpe = 2; 80 | } else { 81 | bpe = inp.BYTES_PER_ELEMENT; 82 | } 83 | } 84 | const n = inp.length 85 | 86 | const numStdDeviations = 10; 87 | 88 | // For some reason, typescript does not think the reduce function as 89 | // used below is compatible with all typedarrays 90 | //@ts-ignore 91 | const mean = inp.reduce((a : number, b : number) => a + b) / n 92 | //@ts-ignore 93 | const stddev = Math.sqrt(inp.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n) 94 | //@ts-ignore 95 | const actualMax = inp.reduce((prev : number, cur : number) : number => Math.max(prev, cur), 0); 96 | const max = (typeof norm.to === 'number') ? norm.to : Math.min(mean+stddev * numStdDeviations, actualMax); 97 | //@ts-ignore 98 | const actualMin = inp.reduce((prev : number, cur : number) : number => Math.min(prev, cur), max); 99 | const min = (typeof norm.from === 'number') ? norm.from : Math.max(mean-stddev * numStdDeviations, actualMin); 100 | 101 | const newMax = Math.pow(2, bpe * 8); 102 | const newMin = 0; 103 | const sub = max - min; 104 | const nsub = newMax - newMin; 105 | const factor = newMax/(max - sub); 106 | inp.forEach((a : number, index : number) => { 107 | if (a >= max) inp[index] = newMax; 108 | else if (a <= min) inp[index] = newMin; 109 | else inp[index] = (((a-min)/sub) * nsub + newMin); 110 | }); 111 | return { 112 | data: inp, 113 | minBefore: actualMin, 114 | maxBefore: actualMax, 115 | minAfter: newMin, 116 | maxAfter: newMax, 117 | }; 118 | }, 119 | normaliseTypedArraySmartWindow(inp : T, norm: NormRange) : NormaliseResult { 120 | let bpe = 2; 121 | if (!Array.isArray(inp)) { 122 | if (inp instanceof Float32Array) { 123 | bpe = 2; 124 | } else { 125 | bpe = inp.BYTES_PER_ELEMENT; 126 | } 127 | } 128 | const n = inp.length 129 | 130 | const numStdDeviations = 10; 131 | 132 | // For some reason, typescript does not think the reduce function as 133 | // used below is compatible with all typedarrays 134 | //@ts-ignore 135 | const mean = inp.reduce((a : number, b : number) => a + b) / n 136 | //@ts-ignore 137 | const stddev = Math.sqrt(inp.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n) 138 | 139 | const exclude = 0.0005; 140 | const copy = inp.slice(0); 141 | copy.sort(); 142 | const offset = Math.ceil(copy.length * exclude); 143 | const length = Math.floor(copy.length * (1-exclude*2)); 144 | const windowedCopy = copy.subarray(offset, length); 145 | 146 | //@ts-ignore 147 | const actualMax = copy[copy.length-1]; 148 | const windowedMax = windowedCopy[windowedCopy.length-1]; 149 | //@ts-ignore 150 | const actualMin = copy[0]; 151 | const windowedMin = windowedCopy[0]; 152 | 153 | const max = (typeof norm.to === 'number') ? norm.to : (windowedMax + stddev) > actualMax ? actualMax : windowedMax; 154 | const min = (typeof norm.from === 'number') ? norm.from : (windowedMin - stddev) < actualMin ? actualMin : windowedMin; 155 | 156 | const newMax = Math.pow(2, bpe * 8)-1; 157 | const newMin = 0; 158 | const sub = max - min; 159 | const nsub = newMax - newMin; 160 | const factor = newMax/(max - sub); 161 | inp.forEach((a : number, index : number) => { 162 | if (a >= max) inp[index] = newMax; 163 | else if (a <= min) inp[index] = newMin; 164 | else inp[index] = (((a-min)/sub) * nsub + newMin); 165 | }); 166 | return { 167 | data: inp, 168 | minBefore: min, 169 | maxBefore: max, 170 | minAfter: newMin, 171 | maxAfter: newMax, 172 | }; 173 | }, 174 | combineImages( 175 | states : TileLoadState[], 176 | normaliseMode : number = NormaliseMode.Regular, 177 | norm : NormRange = normDefaults 178 | ) : NormaliseResult { 179 | const area = states[0].width * states[0].height; 180 | let output = new Float32Array(area); 181 | const tileWidth = 256; 182 | const increment = 1/tileWidth; 183 | const map : Record> = {}; 184 | for (let tile of states) { 185 | if (!map[tile.x]) { 186 | map[tile.x] = {}; 187 | } 188 | map[tile.x][tile.y] = tile; 189 | } 190 | 191 | const extent = { 192 | x1: states[0].exactPos.x - states[0].widthInTiles/2, 193 | x2: states[0].exactPos.x + states[0].widthInTiles/2, 194 | y1: states[0].exactPos.y - states[0].heightInTiles/2, 195 | y2: states[0].exactPos.y + states[0].heightInTiles/2 196 | } 197 | 198 | let i = 0; 199 | for (let y = extent.y1; y < extent.y2; y += increment) { 200 | for (let x = extent.x1; x < extent.x2; x += increment) { 201 | const tile = { 202 | x: Math.floor(x), 203 | y: Math.floor(y) 204 | }; 205 | const px = { 206 | x: Math.floor((x%1)*tileWidth), 207 | y: Math.floor((y%1)*tileWidth) 208 | }; 209 | const idx = px.y*tileWidth + px.x; 210 | if (typeof map[tile.x] === 'undefined') { 211 | throw new Error(`x value ${tile.x} was undefined`); 212 | } else if (typeof map[tile.x][tile.y] === 'undefined') { 213 | throw new Error(`y value ${tile.y} was undefined`); 214 | } else { 215 | output[i++] = map[tile.x][tile.y].heights[idx]; 216 | } 217 | } 218 | } 219 | let result = { 220 | data: output, 221 | minBefore: Math.pow(2, 32), 222 | maxBefore: 0, 223 | minAfter: Math.pow(2, 32), 224 | maxAfter: 0, 225 | }; 226 | if ( 227 | normaliseMode == NormaliseMode.Regular || 228 | ( 229 | typeof norm.from == 'number' && 230 | typeof norm.to == 'number' 231 | ) 232 | ) { 233 | result = this.normaliseTypedArray(output, norm); 234 | } else if (normaliseMode == NormaliseMode.Smart) { 235 | result = this.normaliseTypedArraySmart(output, norm); 236 | } else if (normaliseMode == NormaliseMode.SmartWindow) { 237 | result = this.normaliseTypedArraySmartWindow(output, norm); 238 | } else { 239 | for (let i = 0; i < output.length; i++) { 240 | result.maxAfter = Math.max(output[i], result.maxAfter); 241 | result.minAfter = Math.min(output[i], result.minAfter); 242 | } 243 | result.maxBefore = result.maxAfter; 244 | result.minBefore = result.minAfter; 245 | } 246 | return result; 247 | }, 248 | typedArrayToStl( 249 | points: TypedArray, 250 | widthpx : number, 251 | heightpx : number, 252 | {width, depth, height} : TypedArrayToStlArgs = typedArrayToStlDefaults 253 | ) : ArrayBuffer { 254 | const dataLength = ((widthpx) * (heightpx)) * 50; 255 | console.log(points.length, dataLength); 256 | const size = 80 + 4 + dataLength; 257 | const result = new ArrayBuffer(dataLength); 258 | const dv = new DataView(result); 259 | dv.setUint32(80, (widthpx-1)*(heightpx-1), true); 260 | 261 | //@ts-ignore 262 | const max = points.reduce((acc, point) => Math.max(point, acc), 0); 263 | 264 | const o = (x : number, y : number) : number => (y * widthpx) + x; 265 | const n = (p1 : vec3, p2 : vec3, p3: vec3) : vec3 => { 266 | const A = [p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2]]; 267 | const B = [p3[0] - p1[0], p3[1] - p1[1], p3[2] - p1[2]]; 268 | return [ 269 | A[1] * B[2] - A[2] * B[1], 270 | A[2] * B[0] - A[0] * B[2], 271 | A[0] * B[1] - A[1] * B[0] 272 | ] 273 | } 274 | const pt = (tris : trivec3, off : number) => { 275 | tris.flat().forEach((flt : number, i : number) => { 276 | dv.setFloat32(off + (i * 4), flt, true); 277 | }); 278 | // dv.setUint16(off+48, 0, true); 279 | } 280 | 281 | let off = 84; 282 | for (let x = 0; x < (widthpx - 1); x += 2) { 283 | for (let y = 0; y < (heightpx - 1); y++) { 284 | const tri1 : trivec3 = [ 285 | [0,0,0], // normal 286 | [ x, y, points[o(x,y)]/max], // v1 287 | [x+1, y, points[o(x+1,y)]/max], // v2 288 | [ x, y+1, points[o(x,y+1)]/max], // v3 289 | ]; 290 | // tri1[0] = n(tri1[1], tri1[2], tri1[3]); 291 | pt(tri1, off); 292 | off += 50; 293 | 294 | const tri2 : trivec3 = [ 295 | [0,0,0], // normal 296 | [x+1, y, points[o(x+1,y)]/max], // v1 297 | [x+1, y+1, points[o(x+1,y+1)]/max], // v2 298 | [ x, y+1, points[o(x,y+1)]/max], // v3 299 | ]; 300 | // tri2[0] = n(tri2[1], tri2[2], tri2[3]); 301 | pt(tri2, off); 302 | off += 50; 303 | } 304 | } 305 | 306 | return result; 307 | } 308 | } 309 | 310 | Comlink.expose(processor); -------------------------------------------------------------------------------- /src/sass/_overrides.scss: -------------------------------------------------------------------------------- 1 | // Overrides 2 | @if $bulmaswatch-import-font { 3 | @import url("https://fonts.googleapis.com/css?family=Lato:300,400,700&display=swap"); 4 | } 5 | 6 | .section { 7 | background-color: $body-background-color; 8 | } 9 | 10 | .hero { 11 | background-color: $body-background-color; 12 | } 13 | 14 | .button { 15 | &.is-hovered, 16 | &:hover { 17 | background-color: darken($button-background-color, 3%); 18 | } 19 | @each $name, $pair in $colors { 20 | $color: nth($pair, 1); 21 | $color-invert: nth($pair, 2); 22 | 23 | &.is-#{$name} { 24 | &.is-hovered, 25 | &:hover { 26 | background-color: darken($color, 3%); 27 | } 28 | } 29 | } 30 | 31 | &.is-loading:after { 32 | border-color: transparent transparent $grey-light $grey-light; 33 | } 34 | } 35 | 36 | .label { 37 | color: $grey-lighter; 38 | } 39 | 40 | .notification { 41 | @each $name, $pair in $colors { 42 | $color: nth($pair, 1); 43 | $color-invert: nth($pair, 2); 44 | 45 | &.is-#{$name} { 46 | a:not(.button) { 47 | color: $color-invert; 48 | text-decoration: underline; 49 | } 50 | } 51 | } 52 | } 53 | 54 | .card { 55 | border: 1px solid $border; 56 | border-radius: $radius; 57 | 58 | .card-image { 59 | img { 60 | border-radius: $radius $radius 0 0; 61 | } 62 | } 63 | 64 | .card-header { 65 | border-radius: $radius $radius 0 0; 66 | } 67 | 68 | .card-footer, 69 | .card-footer-item { 70 | border-width: 1px; 71 | border-color: $border; 72 | } 73 | } 74 | 75 | .modal-card-body { 76 | background-color: $body-background-color; 77 | } 78 | 79 | .navbar { 80 | &.is-transparent { 81 | background-color: transparent; 82 | } 83 | 84 | @include until($navbar-breakpoint) { 85 | .navbar-menu { 86 | background-color: transparent; 87 | } 88 | } 89 | 90 | @each $name, $pair in $colors { 91 | $color: nth($pair, 1); 92 | $color-invert: nth($pair, 2); 93 | 94 | &.is-#{$name} { 95 | @include until($navbar-breakpoint) { 96 | .navbar-item, 97 | .navbar-link { 98 | color: $color-invert; 99 | 100 | &.is-active { 101 | color: rgba($color-invert, 0.7); 102 | } 103 | } 104 | 105 | .navbar-burger { 106 | span { 107 | background-color: $color-invert; 108 | } 109 | } 110 | } 111 | } 112 | } 113 | } 114 | 115 | .hero { 116 | .navbar { 117 | .navbar-dropdown { 118 | .navbar-item { 119 | color: $grey-lighter; 120 | } 121 | } 122 | } 123 | } -------------------------------------------------------------------------------- /src/sass/_variables.scss: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////// 2 | // SUPERHERO 3 | //////////////////////////////////////////////// 4 | $grey-darker: #1f2d3b; 5 | $grey-dark: #2b3e50; 6 | $grey: #4e5d6c; 7 | $grey-light: #8694a4; 8 | $grey-lighter: #dee5ed; 9 | 10 | $orange: #df691a; 11 | $yellow: #f0ad4e; 12 | $green: #5cb85c; 13 | $blue: #5bc0de; 14 | $red: #d9534f; 15 | 16 | $primary: $orange !default; 17 | 18 | $dark: darken($grey-darker, 3); 19 | 20 | $title-color: $grey-lighter; 21 | $title-weight: 400; 22 | $subtitle-strong-color: $grey-lighter; 23 | $subtitle-color: darken($grey-lighter, 10); 24 | $subtitle-strong-color: darken($grey-lighter, 10); 25 | 26 | $background: $grey-dark; 27 | $body-background-color: $grey-darker; 28 | $footer-background-color: $background; 29 | 30 | $border: $grey; 31 | 32 | $family-sans-serif: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif; 33 | 34 | $text: $grey-lighter; 35 | $text-strong: darken($grey-lighter, 10); 36 | $text-light: $grey-light; 37 | 38 | $box-background-color: $background; 39 | 40 | $card-shadow: none; 41 | $card-background-color: $background; 42 | $card-header-box-shadow: none; 43 | $card-header-background-color: darken($body-background-color, 3); 44 | $card-footer-background-color: darken($body-background-color, 3); 45 | 46 | $link: $grey-light; 47 | $link-hover: $grey-lighter; 48 | $link-focus: $grey-lighter; 49 | $link-active: $grey-lighter; 50 | 51 | $button-color: $grey-lighter; 52 | $button-background-color: $grey; 53 | $button-border-color: $grey; 54 | 55 | $button-hover-border: transparent; 56 | $button-active-border-color: transparent; 57 | 58 | $radius: 0; 59 | $radius-small: 0; 60 | 61 | $input-hover-color: $link-hover; 62 | $input-color: $grey-darker; 63 | $input-icon-color: $grey; 64 | $input-icon-active-color: $input-color; 65 | 66 | $table-color: $text; 67 | $table-head-color: $grey-lighter; 68 | $table-background-color: $background; 69 | $table-cell-border: 1px solid $grey; 70 | $table-row-hover-background-color: $grey-darker; 71 | $table-striped-row-even-background-color: $grey-darker; 72 | $table-striped-row-even-hover-background-color: lighten($grey-darker, 4); 73 | 74 | $navbar-background-color: $background; 75 | $navbar-item-color: $text; 76 | $navbar-item-hover-color: $grey-light; 77 | $navbar-item-active-color: $primary; 78 | $navbar-item-hover-background-color: rgba($grey-darker, 0.1); 79 | $navbar-item-active-background-color: rgba($grey-darker, 0.1); 80 | $navbar-dropdown-item-hover-color: $grey-light; 81 | $navbar-dropdown-item-active-color: $primary; 82 | $navbar-dropdown-background-color: $background; 83 | $navbar-dropdown-arrow: currentColor; 84 | 85 | $dropdown-content-background-color: $background; 86 | $dropdown-item-color: $text; 87 | $dropdown-item-hover-color: $text-light; 88 | 89 | $tabs-toggle-link-active-background-color: $background; 90 | $tabs-toggle-link-active-border-color: $border; 91 | $tabs-toggle-link-active-color: #fff; 92 | $tabs-boxed-link-active-background-color: $body-background-color; 93 | 94 | $pagination-color: $link; 95 | $pagination-border-color: $border; 96 | 97 | $bulmaswatch-import-font: true !default; 98 | 99 | $file-cta-background-color: $grey-darker; 100 | 101 | $progress-bar-background-color: $grey-dark; 102 | 103 | $panel-heading-background-color: $grey-dark; -------------------------------------------------------------------------------- /src/sass/main.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | @use 'sass:list'; 3 | @import '../../node_modules/leaflet/dist/leaflet.css'; 4 | 5 | @import 'bulma/sass/utilities/functions.sass'; 6 | @import 'bulma/sass/utilities/initial-variables.sass'; 7 | @import "variables"; 8 | 9 | $secondary: $red; 10 | $secondary-invert: findColorInvert($secondary); 11 | $secondary-light: findLightColor($secondary); 12 | $secondary-dark: findDarkColor($secondary); 13 | 14 | $tertiary: $purple; 15 | $tertiary-invert: findColorInvert($secondary); 16 | $tertiary-light: findLightColor($secondary); 17 | $tertiary-dark: findDarkColor($secondary); 18 | 19 | $cta: $red; 20 | $cta-invert: findColorInvert($secondary); 21 | $cta-light: findLightColor($secondary); 22 | $cta-dark: findDarkColor($secondary); 23 | 24 | $custom-colors: ( 25 | "secondary": ($secondary, $secondary-invert, $secondary-light, $secondary-dark), 26 | "tertiary": ($tertiary, $tertiary-invert, $tertiary-light, $tertiary-dark), 27 | "cta": ($cta, $cta-invert, $cta-light, $cta-dark) 28 | ); 29 | 30 | @import "~bulma/bulma"; 31 | @import "overrides"; 32 | 33 | .flag { 34 | font-family: "Twemoji Country Flags", Lato, "Helvetica Neue", Helvetica, Arial, sans-serif; 35 | margin-left: 0.35rem; 36 | } 37 | 38 | 39 | $tagColors: 40 | desaturate(lighten($orange, 30%), 20%) 41 | desaturate(lighten($yellow, 30%), 20%) 42 | desaturate(lighten($green, 30%), 20%) 43 | desaturate(lighten($blue, 30%), 20%) 44 | desaturate(lighten($red, 30%), 20%) 45 | ; 46 | .map-locations .tag { 47 | @for $idx from 1 through 5 { 48 | &:nth-child(5n+#{$idx}) { 49 | $c: list.nth($tagColors, $idx); 50 | background-color: $c; 51 | color: findColorInvert($c); 52 | } 53 | } 54 | } 55 | 56 | header { 57 | margin-bottom: 1.5rem; 58 | } 59 | 60 | .columns { 61 | &:last-child { 62 | margin-bottom: (-$column-gap) 63 | } 64 | &:not(:last-child) { 65 | margin-bottom: calc(0.5rem - #{$column-gap}) 66 | } 67 | } 68 | 69 | .content { 70 | dt { 71 | font-weight: bold; 72 | display: block; 73 | width: 100%; 74 | padding: calc($column-gap / 2) 0; 75 | } 76 | dd { 77 | margin-bottom: $column-gap; 78 | } 79 | } 80 | 81 | .message-stack { 82 | position: fixed; 83 | bottom: 0; 84 | right: 0; 85 | width: min(80vw, 400px); 86 | max-height: 100vh; 87 | overflow-y: auto; 88 | z-index: 999999; 89 | } 90 | .faq details { 91 | margin-bottom: 1rem; 92 | summary { 93 | font-style: italic; 94 | cursor: pointer; 95 | } 96 | } 97 | 98 | .docs main { 99 | padding-bottom: 2rem; 100 | } -------------------------------------------------------------------------------- /src/templates/footer.html: -------------------------------------------------------------------------------- 1 | 63 | -------------------------------------------------------------------------------- /src/templates/header.html: -------------------------------------------------------------------------------- 1 |
2 | 41 |
-------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.html' { 2 | const content: string; 3 | export default content; 4 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "sourceMap": true, 5 | "noImplicitAny": true, 6 | "module": "es6", 7 | "target": "es5", 8 | "jsx": "react", 9 | "allowJs": true, 10 | "moduleResolution": "node" 11 | } 12 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 3 | 4 | module.exports = { 5 | entry: { 6 | main: './src/main.ts', 7 | processor: './src/processor.ts' 8 | }, 9 | mode: 'development', 10 | output: { 11 | path: path.resolve(__dirname, 'public', 'dist'), 12 | filename: 'js/[name].js' 13 | }, 14 | resolve: { 15 | extensions: ['.tsx', '.ts', '.js'] 16 | }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.tsx?$/, 21 | use: 'ts-loader', 22 | exclude: /node_modules/ 23 | }, 24 | { 25 | test: /\.scss$/, 26 | use: [ 27 | MiniCssExtractPlugin.loader, 28 | { 29 | loader: 'css-loader' 30 | }, 31 | { 32 | loader: 'sass-loader', 33 | options: { 34 | sourceMap: true 35 | // options... 36 | } 37 | } 38 | ] 39 | }, 40 | { 41 | test: /\.html$/i, 42 | loader: 'html-loader' 43 | } 44 | ] 45 | }, 46 | plugins: [ 47 | new MiniCssExtractPlugin({ 48 | filename: 'css/main.css' 49 | }) 50 | ] 51 | }; 52 | -------------------------------------------------------------------------------- /webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { merge } = require('webpack-merge'); 3 | const common = require('./webpack.config.js'); 4 | module.exports = merge(common, { 5 | devtool: 'inline-source-map' 6 | }); 7 | -------------------------------------------------------------------------------- /webpack.production.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { merge } = require('webpack-merge'); 3 | const common = require('./webpack.config.js'); 4 | module.exports = merge(common, { 5 | mode: 'production' 6 | }); 7 | --------------------------------------------------------------------------------