From 2a67ee5d9f5ba18b2280d01a3cff03d6260cbe77 Mon Sep 17 00:00:00 2001 From: Azgaar Date: Sat, 16 May 2020 00:52:45 +0300 Subject: [PATCH] v1.4.14 --- index.css | 11 +- index.html | 27 +- libs/publicstorage.js | 188 ------- libs/quantize.min.js | 1 - libs/rgbquant.js | 935 +++++++++++++++++++++++++++++++++ main.js | 1 + modules/river-generator.js | 8 +- modules/save-and-load.js | 8 - modules/ui/editors.js | 6 - modules/ui/general.js | 6 + modules/ui/heightmap-editor.js | 64 ++- modules/ui/markers-editor.js | 16 +- modules/ui/notes-editor.js | 4 +- modules/ui/provinces-editor.js | 2 +- 14 files changed, 1010 insertions(+), 267 deletions(-) delete mode 100644 libs/publicstorage.js delete mode 100644 libs/quantize.min.js create mode 100644 libs/rgbquant.js diff --git a/index.css b/index.css index f4e72beb..1cd18ea7 100644 --- a/index.css +++ b/index.css @@ -60,6 +60,13 @@ textarea { pointer-events: none; } +#preview { + position: absolute; + bottom: 1em; + left: 1em; + cursor: pointer; +} + #pickerContainer { position: absolute; z-index: 100; @@ -1021,8 +1028,8 @@ div#regimentSelectorBody > div > div { } .color-div { - width: 2.7em; - height: 1.1em; + width: 2.5em; + height: 1em; display: inline-block; margin: .1em .2em; border: 1px #c5c5c5 groove; diff --git a/index.html b/index.html index 896ff785..7c0e1c5f 100644 --- a/index.html +++ b/index.html @@ -2103,7 +2103,7 @@ - + @@ -2375,7 +2375,7 @@ - + @@ -2416,7 +2416,7 @@ - + @@ -2660,8 +2660,8 @@ - - + + @@ -2675,7 +2675,7 @@ Set height: (0)
-
+
@@ -3658,7 +3658,6 @@ - + - diff --git a/libs/publicstorage.js b/libs/publicstorage.js deleted file mode 100644 index 9d8ebe54..00000000 --- a/libs/publicstorage.js +++ /dev/null @@ -1,188 +0,0 @@ -// https://github.com/Highbrainer/jeevaneo-js-publicstorage. MIT -const IFRAME_ROOT_URL = "https://publicstorage.neocities.org/shared-iframe.html"; - -class PublicStorageAccess { - - constructor ({debug=false}={}) { - this.uid = this.uniqueId(); - this.debug=debug; - } - - uniqueId() { - function chr4(){ - return Math.random().toString(16).slice(-4); - } - return chr4() + chr4() + - '-' + chr4() + - '-' + chr4() + - '-' + chr4() + - '-' + chr4() + chr4() + chr4(); - } - - _debug(msg) { - if(this.debug) { - if(console && console.debug) { - console.debug(msg); - } - } - } - - prepareIFrame() { - const that = this; - const iframe = document.createElement("iframe"); - iframe.id=that.uid; - iframe.src=IFRAME_ROOT_URL + "?uid=init-"+that.uid; - iframe.style.cssText="display:none;"; - return new Promise(function(resolve, reject) { - window.addEventListener('message', function mafunc(tkn) { - - if (IFRAME_ROOT_URL.indexOf(tkn.origin)<0) { - return; - } - - try { - const packet = JSON.parse(tkn.data); - - if(!(packet.frameId === "init-" + that.uid)) { - // ignore - return; - } - - if(packet.ready) { - resolve(iframe); - } - } catch (e) { - reject(tkn.data); - } - window.removeEventListener('message', mafunc); - }); - onLoadThen().then(() => { - document.getElementsByTagName("body")[0].appendChild(iframe); - }); - - setTimeout(()=>reject(`Request ${that.uid} TIMEOUTED!`), 20000); - }); - } - - access(access, prop, value = null, level = "local") { - - if(!(access === "get" || access === "set" || access === "delete")) { - throw new Error("access can only be 'set', 'get' or 'delete' - not '" + access + "'"); - } - - if (!prop) { - throw new Error("Prop name is mandatory"); - } - - if(!(level === "local" || level === "session")) { - throw new Error("level can only be 'session' or 'local' - not '" + access + "'"); - } - - const that = this; - - const promise = new Promise(function(resolve, reject) { - that.prepareIFrame().then(iframe => { - window.addEventListener('message', function mafunc(tkn) { - if (IFRAME_ROOT_URL.indexOf(tkn.origin)<0) { - return; - } - try { - var packet = JSON.parse(tkn.data); - - if(!(packet.uid === that.uid)) { - // ignore - return; - } - resolve(packet.body); - } catch (e) { - reject(tkn.data); - } - iframe.parentNode.removeChild(iframe); - window.removeEventListener('message', mafunc); - }); - - const request = {uid:that.uid, access:access, prop:prop, value:value, level:level}; - iframe.contentWindow.postMessage(JSON.stringify(request), '*'); - setTimeout(()=>reject("TIMEOUTED!"), 20000); - }); - }); - return promise; - } - -} - -function __createDebugIFrame() { - onLoadThen().then(function(){ - const iframe = document.createElement("iframe"); - iframe.src=IFRAME_ROOT_URL + "?for-debug-only"; - iframe.style.cssText="display:none;"; - document.getElementsByTagName("body")[0].appendChild(iframe); - }); -} - -class PublicStorage { - - constructor({debug=false}={}) { - if(debug) { - __createDebugIFrame(); - } - } - - sessionGet(prop) { - return new PublicStorageAccess().access("get", prop, null, "session"); - } - sessionSet(prop, value) { - return new PublicStorageAccess().access("set", prop, value, "session"); - } - sessionUnset(prop) { - return new PublicStorageAccess().access("delete", prop, null, "session"); - } - localGet(prop) { - return new PublicStorageAccess().access("get", prop, null, "local"); - } - localSet(prop, value) { - return new PublicStorageAccess().access("set", prop, value, "local"); - } - localUnset(prop) { - return new PublicStorageAccess().access("delete", prop, null, "local"); - } - get(prop) { - return this.localGet(prop); - } - set(prop, value) { - return this.localSet(prop, value); - } - unset(prop) { - return this.localUnset(prop); - } -} - -const publicstorage = new PublicStorage(); - -function onLoadThen() { - return new Promise(function(resolve, reject) { - if (window) { - if(document.getElementsByTagName('BODY')[0]) { - resolve(); - } else { - registerOnLoad(function unregisterme() { - resolve(); - window.removeEventListener('load', unregisterme); - }); - } - } - setTimeout(function() {reject(new Error("Timeout waiting for onLoad!"));}, 10000); - }); -} - -function registerOnLoad(lambda) { - if (window.addEventListener) { - window.addEventListener('load', lambda); - } else if (window.attachEvent) { - window.attachEvent('onload', lambda); - } -} - -onLoadThen().then(() => window.publicstorage = publicstorage).catch(e=> console.error(e)); -//export {onLoadThen, PublicStorage, publicstorage as default} -// module.exports = onLoadThen(); diff --git a/libs/quantize.min.js b/libs/quantize.min.js deleted file mode 100644 index 4d8228eb..00000000 --- a/libs/quantize.min.js +++ /dev/null @@ -1 +0,0 @@ -if(!pv)var pv={map:function(r,n){var o={};return n?r.map(function(r,t){return o.index=t,n.call(o,r)}):r.slice()},naturalOrder:function(r,n){return rn?1:0},sum:function(r,n){var o={};return r.reduce(n?function(r,t,u){return o.index=u,r+n.call(o,t)}:function(r,n){return r+n},0)},max:function(r,n){return Math.max.apply(null,n?pv.map(r,n):r)}};var MMCQ=function(){var r=5,n=8-r,o=1e3,t=.75;function u(n,o,t){return(n<<2*r)+(o<s/2){for(u=n.copy(),e=n.copy(),i=(o=c-n[f])<=(t=n[a]-c)?Math.min(n[a]-1,~~(c+t/2)):Math.max(n[f],~~(c-1-o/2));!p[i];)i++;for(v=l[i];!v&&p[i-1];)v=l[--i];return u[a]=i,e[f]=u[a]+1,[u,e]}}}return i.prototype={volume:function(r){var n=this;return n._volume&&!r||(n._volume=(n.r2-n.r1+1)*(n.g2-n.g1+1)*(n.b2-n.b1+1)),n._volume},count:function(r){var n=this,o=n.histo;if(!n._count_set||r){var t,e,i,c=0;for(t=n.r1;t<=n.r2;t++)for(e=n.g1;e<=n.g2;e++)for(i=n.b1;i<=n.b2;i++)c+=o[u(t,e,i)]||0;n._count=c,n._count_set=!0}return n._count},copy:function(){var r=this;return new i(r.r1,r.r2,r.g1,r.g2,r.b1,r.b2,r.histo)},avg:function(n){var o=this,t=o.histo;if(!o._avg||n){var e,i,c,f,a=0,v=1<<8-r,s=0,p=0,l=0;for(i=o.r1;i<=o.r2;i++)for(c=o.g1;c<=o.g2;c++)for(f=o.b1;f<=o.b2;f++)a+=e=t[u(i,c,f)]||0,s+=e*(i+.5)*v,p+=e*(c+.5)*v,l+=e*(f+.5)*v;o._avg=a?[~~(s/a),~~(p/a),~~(l/a)]:[~~(v*(o.r1+o.r2+1)/2),~~(v*(o.g1+o.g2+1)/2),~~(v*(o.b1+o.b2+1)/2)]}return o._avg},contains:function(r){var o=this,t=r[0]>>n;return gval=r[1]>>n,bval=r[2]>>n,t>=o.r1&&t<=o.r2&&gval>=o.g1&&gval<=o.g2&&bval>=o.b1&&bval<=o.b2}},c.prototype={push:function(r){this.vboxes.push({vbox:r,color:r.avg()})},palette:function(){return this.vboxes.map(function(r){return r.color})},size:function(){return this.vboxes.size()},map:function(r){for(var n=this.vboxes,o=0;o251&&t[1]>251&&t[2]>251&&(r[o].color=[255,255,255])}},{quantize:function(a,v){if(v++,!a.length||v<2||v>256)return!1;var s,p,l,h,b,g,m=(s=a,g=new Array(1<<3*r),s.forEach(function(r){l=r[0]>>n,h=r[1]>>n,b=r[2]>>n,p=u(l,h,b),g[p]=(g[p]||0)+1}),g);m.forEach(function(){});var x,_,d,w,z,M,y,k,O,E,q=(x=m,z=1e6,M=0,y=1e6,k=0,O=1e6,E=0,a.forEach(function(r){_=r[0]>>n,d=r[1]>>n,w=r[2]>>n,_M&&(M=_),dk&&(k=d),wE&&(E=w)}),new i(z,M,y,k,O,E,x)),A=new e(function(r,n){return pv.naturalOrder(r.count(),n.count())});function C(r,n){for(var t,u=1,e=0;e=n)return;if(e++>o)return}else r.push(t),e++}A.push(q),C(A,t*v);for(var Q=new e(function(r,n){return pv.naturalOrder(r.count()*r.volume(),n.count()*n.volume())});A.size();)Q.push(A.pop());C(Q,v-Q.size());for(var j=new c;Q.size();)j.push(Q.pop());return j}}}(); \ No newline at end of file diff --git a/libs/rgbquant.js b/libs/rgbquant.js new file mode 100644 index 00000000..bc0bd1b9 --- /dev/null +++ b/libs/rgbquant.js @@ -0,0 +1,935 @@ +/* +* Copyright (c) 2015, Leon Sorokin +* All rights reserved. (MIT Licensed) +* +* RgbQuant.js - an image quantization lib +*/ + +(function(){ + function RgbQuant(opts) { + opts = opts || {}; + + // 1 = by global population, 2 = subregion population threshold + this.method = opts.method || 2; + // desired final palette size + this.colors = opts.colors || 256; + // # of highest-frequency colors to start with for palette reduction + this.initColors = opts.initColors || 4096; + // color-distance threshold for initial reduction pass + this.initDist = opts.initDist || 0.01; + // subsequent passes threshold + this.distIncr = opts.distIncr || 0.005; + // palette grouping + this.hueGroups = opts.hueGroups || 10; + this.satGroups = opts.satGroups || 10; + this.lumGroups = opts.lumGroups || 10; + // if > 0, enables hues stats and min-color retention per group + this.minHueCols = opts.minHueCols || 0; + // HueStats instance + this.hueStats = this.minHueCols ? new HueStats(this.hueGroups, this.minHueCols) : null; + + // subregion partitioning box size + this.boxSize = opts.boxSize || [64,64]; + // number of same pixels required within box for histogram inclusion + this.boxPxls = opts.boxPxls || 2; + // palette locked indicator + this.palLocked = false; + // palette sort order +// this.sortPal = ['hue-','lum-','sat-']; + + // dithering/error diffusion kernel name + this.dithKern = opts.dithKern || null; + // dither serpentine pattern + this.dithSerp = opts.dithSerp || false; + // minimum color difference (0-1) needed to dither + this.dithDelta = opts.dithDelta || 0; + + // accumulated histogram + this.histogram = {}; + // palette - rgb triplets + this.idxrgb = opts.palette ? opts.palette.slice(0) : []; + // palette - int32 vals + this.idxi32 = []; + // reverse lookup {i32:idx} + this.i32idx = {}; + // {i32:rgb} + this.i32rgb = {}; + // enable color caching (also incurs overhead of cache misses and cache building) + this.useCache = opts.useCache !== false; + // min color occurance count needed to qualify for caching + this.cacheFreq = opts.cacheFreq || 10; + // allows pre-defined palettes to be re-indexed (enabling palette compacting and sorting) + this.reIndex = opts.reIndex || this.idxrgb.length == 0; + // selection of color-distance equation + this.colorDist = opts.colorDist == "manhattan" ? distManhattan : distEuclidean; + + // if pre-defined palette, build lookups + if (this.idxrgb.length > 0) { + var self = this; + this.idxrgb.forEach(function(rgb, i) { + var i32 = ( + (255 << 24) | // alpha + (rgb[2] << 16) | // blue + (rgb[1] << 8) | // green + rgb[0] // red + ) >>> 0; + + self.idxi32[i] = i32; + self.i32idx[i32] = i; + self.i32rgb[i32] = rgb; + }); + } + } + + // gathers histogram info + RgbQuant.prototype.sample = function sample(img, width) { + if (this.palLocked) + throw "Cannot sample additional images, palette already assembled."; + + var data = getImageData(img, width); + + switch (this.method) { + case 1: this.colorStats1D(data.buf32); break; + case 2: this.colorStats2D(data.buf32, data.width); break; + } + }; + + // image quantizer + // todo: memoize colors here also + // @retType: 1 - Uint8Array (default), 2 - Indexed array, 3 - Match @img type (unimplemented, todo) + RgbQuant.prototype.reduce = function reduce(img, retType, dithKern, dithSerp) { + if (!this.palLocked) + this.buildPal(); + + dithKern = dithKern || this.dithKern; + dithSerp = typeof dithSerp != "undefined" ? dithSerp : this.dithSerp; + + retType = retType || 1; + + // reduce w/dither + if (dithKern) + var out32 = this.dither(img, dithKern, dithSerp); + else { + var data = getImageData(img), + buf32 = data.buf32, + len = buf32.length, + out32 = new Uint32Array(len); + + for (var i = 0; i < len; i++) { + var i32 = buf32[i]; + out32[i] = this.nearestColor(i32); + } + } + + if (retType == 1) + return new Uint8Array(out32.buffer); + + if (retType == 2) { + var out = [], + len = out32.length; + + for (var i = 0; i < len; i++) { + var i32 = out32[i]; + out[i] = this.i32idx[i32]; + } + + return out; + } + }; + + // adapted from http://jsbin.com/iXofIji/2/edit by PAEz + RgbQuant.prototype.dither = function(img, kernel, serpentine) { + // http://www.tannerhelland.com/4660/dithering-eleven-algorithms-source-code/ + var kernels = { + FloydSteinberg: [ + [7 / 16, 1, 0], + [3 / 16, -1, 1], + [5 / 16, 0, 1], + [1 / 16, 1, 1] + ], + FalseFloydSteinberg: [ + [3 / 8, 1, 0], + [3 / 8, 0, 1], + [2 / 8, 1, 1] + ], + Stucki: [ + [8 / 42, 1, 0], + [4 / 42, 2, 0], + [2 / 42, -2, 1], + [4 / 42, -1, 1], + [8 / 42, 0, 1], + [4 / 42, 1, 1], + [2 / 42, 2, 1], + [1 / 42, -2, 2], + [2 / 42, -1, 2], + [4 / 42, 0, 2], + [2 / 42, 1, 2], + [1 / 42, 2, 2] + ], + Atkinson: [ + [1 / 8, 1, 0], + [1 / 8, 2, 0], + [1 / 8, -1, 1], + [1 / 8, 0, 1], + [1 / 8, 1, 1], + [1 / 8, 0, 2] + ], + Jarvis: [ // Jarvis, Judice, and Ninke / JJN? + [7 / 48, 1, 0], + [5 / 48, 2, 0], + [3 / 48, -2, 1], + [5 / 48, -1, 1], + [7 / 48, 0, 1], + [5 / 48, 1, 1], + [3 / 48, 2, 1], + [1 / 48, -2, 2], + [3 / 48, -1, 2], + [5 / 48, 0, 2], + [3 / 48, 1, 2], + [1 / 48, 2, 2] + ], + Burkes: [ + [8 / 32, 1, 0], + [4 / 32, 2, 0], + [2 / 32, -2, 1], + [4 / 32, -1, 1], + [8 / 32, 0, 1], + [4 / 32, 1, 1], + [2 / 32, 2, 1], + ], + Sierra: [ + [5 / 32, 1, 0], + [3 / 32, 2, 0], + [2 / 32, -2, 1], + [4 / 32, -1, 1], + [5 / 32, 0, 1], + [4 / 32, 1, 1], + [2 / 32, 2, 1], + [2 / 32, -1, 2], + [3 / 32, 0, 2], + [2 / 32, 1, 2], + ], + TwoSierra: [ + [4 / 16, 1, 0], + [3 / 16, 2, 0], + [1 / 16, -2, 1], + [2 / 16, -1, 1], + [3 / 16, 0, 1], + [2 / 16, 1, 1], + [1 / 16, 2, 1], + ], + SierraLite: [ + [2 / 4, 1, 0], + [1 / 4, -1, 1], + [1 / 4, 0, 1], + ], + }; + + if (!kernel || !kernels[kernel]) { + throw 'Unknown dithering kernel: ' + kernel; + } + + var ds = kernels[kernel]; + + var data = getImageData(img), +// buf8 = data.buf8, + buf32 = data.buf32, + width = data.width, + height = data.height, + len = buf32.length; + + var dir = serpentine ? -1 : 1; + + for (var y = 0; y < height; y++) { + if (serpentine) + dir = dir * -1; + + var lni = y * width; + + for (var x = (dir == 1 ? 0 : width - 1), xend = (dir == 1 ? width : 0); x !== xend; x += dir) { + // Image pixel + var idx = lni + x, + i32 = buf32[idx], + r1 = (i32 & 0xff), + g1 = (i32 & 0xff00) >> 8, + b1 = (i32 & 0xff0000) >> 16; + + // Reduced pixel + var i32x = this.nearestColor(i32), + r2 = (i32x & 0xff), + g2 = (i32x & 0xff00) >> 8, + b2 = (i32x & 0xff0000) >> 16; + + buf32[idx] = + (255 << 24) | // alpha + (b2 << 16) | // blue + (g2 << 8) | // green + r2; + + // dithering strength + if (this.dithDelta) { + var dist = this.colorDist([r1, g1, b1], [r2, g2, b2]); + if (dist < this.dithDelta) + continue; + } + + // Component distance + var er = r1 - r2, + eg = g1 - g2, + eb = b1 - b2; + + for (var i = (dir == 1 ? 0 : ds.length - 1), end = (dir == 1 ? ds.length : 0); i !== end; i += dir) { + var x1 = ds[i][1] * dir, + y1 = ds[i][2]; + + var lni2 = y1 * width; + + if (x1 + x >= 0 && x1 + x < width && y1 + y >= 0 && y1 + y < height) { + var d = ds[i][0]; + var idx2 = idx + (lni2 + x1); + + var r3 = (buf32[idx2] & 0xff), + g3 = (buf32[idx2] & 0xff00) >> 8, + b3 = (buf32[idx2] & 0xff0000) >> 16; + + var r4 = Math.max(0, Math.min(255, r3 + er * d)), + g4 = Math.max(0, Math.min(255, g3 + eg * d)), + b4 = Math.max(0, Math.min(255, b3 + eb * d)); + + buf32[idx2] = + (255 << 24) | // alpha + (b4 << 16) | // blue + (g4 << 8) | // green + r4; // red + } + } + } + } + + return buf32; + }; + + // reduces histogram to palette, remaps & memoizes reduced colors + RgbQuant.prototype.buildPal = function buildPal(noSort) { + if (this.palLocked || this.idxrgb.length > 0 && this.idxrgb.length <= this.colors) return; + + var histG = this.histogram, + sorted = sortedHashKeys(histG, true); + + if (sorted.length == 0) + throw "Nothing has been sampled, palette cannot be built."; + + switch (this.method) { + case 1: + var cols = this.initColors, + last = sorted[cols - 1], + freq = histG[last]; + + var idxi32 = sorted.slice(0, cols); + + // add any cut off colors with same freq as last + var pos = cols, len = sorted.length; + while (pos < len && histG[sorted[pos]] == freq) + idxi32.push(sorted[pos++]); + + // inject min huegroup colors + if (this.hueStats) + this.hueStats.inject(idxi32); + + break; + case 2: + var idxi32 = sorted; + break; + } + + // int32-ify values + idxi32 = idxi32.map(function(v){return +v;}); + + this.reducePal(idxi32); + + if (!noSort && this.reIndex) + this.sortPal(); + + // build cache of top histogram colors + if (this.useCache) + this.cacheHistogram(idxi32); + + this.palLocked = true; + }; + + RgbQuant.prototype.palette = function palette(tuples, noSort) { + this.buildPal(noSort); + return tuples ? this.idxrgb : new Uint8Array((new Uint32Array(this.idxi32)).buffer); + }; + + RgbQuant.prototype.prunePal = function prunePal(keep) { + var i32; + + for (var j = 0; j < this.idxrgb.length; j++) { + if (!keep[j]) { + i32 = this.idxi32[j]; + this.idxrgb[j] = null; + this.idxi32[j] = null; + delete this.i32idx[i32]; + } + } + + // compact + if (this.reIndex) { + var idxrgb = [], + idxi32 = [], + i32idx = {}; + + for (var j = 0, i = 0; j < this.idxrgb.length; j++) { + if (this.idxrgb[j]) { + i32 = this.idxi32[j]; + idxrgb[i] = this.idxrgb[j]; + i32idx[i32] = i; + idxi32[i] = i32; + i++; + } + } + + this.idxrgb = idxrgb; + this.idxi32 = idxi32; + this.i32idx = i32idx; + } + }; + + // reduces similar colors from an importance-sorted Uint32 rgba array + RgbQuant.prototype.reducePal = function reducePal(idxi32) { + // if pre-defined palette's length exceeds target + if (this.idxrgb.length > this.colors) { + // quantize histogram to existing palette + var len = idxi32.length, keep = {}, uniques = 0, idx, pruned = false; + + for (var i = 0; i < len; i++) { + // palette length reached, unset all remaining colors (sparse palette) + if (uniques == this.colors && !pruned) { + this.prunePal(keep); + pruned = true; + } + + idx = this.nearestIndex(idxi32[i]); + + if (uniques < this.colors && !keep[idx]) { + keep[idx] = true; + uniques++; + } + } + + if (!pruned) { + this.prunePal(keep); + pruned = true; + } + } + // reduce histogram to create initial palette + else { + // build full rgb palette + var idxrgb = idxi32.map(function(i32) { + return [ + (i32 & 0xff), + (i32 & 0xff00) >> 8, + (i32 & 0xff0000) >> 16, + ]; + }); + + var len = idxrgb.length, + palLen = len, + thold = this.initDist; + + // palette already at or below desired length + if (palLen > this.colors) { + while (palLen > this.colors) { + var memDist = []; + + // iterate palette + for (var i = 0; i < len; i++) { + var pxi = idxrgb[i], i32i = idxi32[i]; + if (!pxi) continue; + + for (var j = i + 1; j < len; j++) { + var pxj = idxrgb[j], i32j = idxi32[j]; + if (!pxj) continue; + + var dist = this.colorDist(pxi, pxj); + + if (dist < thold) { + // store index,rgb,dist + memDist.push([j, pxj, i32j, dist]); + + // kill squashed value + delete(idxrgb[j]); + palLen--; + } + } + } + + // palette reduction pass + // console.log("palette length: " + palLen); + + // if palette is still much larger than target, increment by larger initDist + thold += (palLen > this.colors * 3) ? this.initDist : this.distIncr; + } + + // if palette is over-reduced, re-add removed colors with largest distances from last round + if (palLen < this.colors) { + // sort descending + sort.call(memDist, function(a,b) { + return b[3] - a[3]; + }); + + var k = 0; + while (palLen < this.colors) { + // re-inject rgb into final palette + idxrgb[memDist[k][0]] = memDist[k][1]; + + palLen++; + k++; + } + } + } + + var len = idxrgb.length; + for (var i = 0; i < len; i++) { + if (!idxrgb[i]) continue; + + this.idxrgb.push(idxrgb[i]); + this.idxi32.push(idxi32[i]); + + this.i32idx[idxi32[i]] = this.idxi32.length - 1; + this.i32rgb[idxi32[i]] = idxrgb[i]; + } + } + }; + + // global top-population + RgbQuant.prototype.colorStats1D = function colorStats1D(buf32) { + var histG = this.histogram, + num = 0, col, + len = buf32.length; + + for (var i = 0; i < len; i++) { + col = buf32[i]; + + // skip transparent + if ((col & 0xff000000) >> 24 == 0) continue; + + // collect hue stats + if (this.hueStats) + this.hueStats.check(col); + + if (col in histG) + histG[col]++; + else + histG[col] = 1; + } + }; + + // population threshold within subregions + // FIXME: this can over-reduce (few/no colors same?), need a way to keep + // important colors that dont ever reach local thresholds (gradients?) + RgbQuant.prototype.colorStats2D = function colorStats2D(buf32, width) { + var boxW = this.boxSize[0], + boxH = this.boxSize[1], + area = boxW * boxH, + boxes = makeBoxes(width, buf32.length / width, boxW, boxH), + histG = this.histogram, + self = this; + + boxes.forEach(function(box) { + var effc = Math.max(Math.round((box.w * box.h) / area) * self.boxPxls, 2), + histL = {}, col; + + iterBox(box, width, function(i) { + col = buf32[i]; + + // skip transparent + if ((col & 0xff000000) >> 24 == 0) return; + + // collect hue stats + if (self.hueStats) + self.hueStats.check(col); + + if (col in histG) + histG[col]++; + else if (col in histL) { + if (++histL[col] >= effc) + histG[col] = histL[col]; + } + else + histL[col] = 1; + }); + }); + + if (this.hueStats) + this.hueStats.inject(histG); + }; + + // TODO: group very low lum and very high lum colors + // TODO: pass custom sort order + RgbQuant.prototype.sortPal = function sortPal() { + var self = this; + + this.idxi32.sort(function(a,b) { + var idxA = self.i32idx[a], + idxB = self.i32idx[b], + rgbA = self.idxrgb[idxA], + rgbB = self.idxrgb[idxB]; + + var hslA = rgb2hsl(rgbA[0],rgbA[1],rgbA[2]), + hslB = rgb2hsl(rgbB[0],rgbB[1],rgbB[2]); + + // sort all grays + whites together + var hueA = (rgbA[0] == rgbA[1] && rgbA[1] == rgbA[2]) ? -1 : hueGroup(hslA.h, self.hueGroups); + var hueB = (rgbB[0] == rgbB[1] && rgbB[1] == rgbB[2]) ? -1 : hueGroup(hslB.h, self.hueGroups); + + var hueDiff = hueB - hueA; + if (hueDiff) return -hueDiff; + + var lumDiff = lumGroup(+hslB.l.toFixed(2)) - lumGroup(+hslA.l.toFixed(2)); + if (lumDiff) return -lumDiff; + + var satDiff = satGroup(+hslB.s.toFixed(2)) - satGroup(+hslA.s.toFixed(2)); + if (satDiff) return -satDiff; + }); + + // sync idxrgb & i32idx + this.idxi32.forEach(function(i32, i) { + self.idxrgb[i] = self.i32rgb[i32]; + self.i32idx[i32] = i; + }); + }; + + // TOTRY: use HUSL - http://boronine.com/husl/ + RgbQuant.prototype.nearestColor = function nearestColor(i32) { + var idx = this.nearestIndex(i32); + return idx === null ? 0 : this.idxi32[idx]; + }; + + // TOTRY: use HUSL - http://boronine.com/husl/ + RgbQuant.prototype.nearestIndex = function nearestIndex(i32) { + // alpha 0 returns null index + if ((i32 & 0xff000000) >> 24 == 0) + return null; + + if (this.useCache && (""+i32) in this.i32idx) + return this.i32idx[i32]; + + var min = 1000, + idx, + rgb = [ + (i32 & 0xff), + (i32 & 0xff00) >> 8, + (i32 & 0xff0000) >> 16, + ], + len = this.idxrgb.length; + + for (var i = 0; i < len; i++) { + if (!this.idxrgb[i]) continue; // sparse palettes + + var dist = this.colorDist(rgb, this.idxrgb[i]); + + if (dist < min) { + min = dist; + idx = i; + } + } + + return idx; + }; + + RgbQuant.prototype.cacheHistogram = function cacheHistogram(idxi32) { + for (var i = 0, i32 = idxi32[i]; i < idxi32.length && this.histogram[i32] >= this.cacheFreq; i32 = idxi32[i++]) + this.i32idx[i32] = this.nearestIndex(i32); + }; + + function HueStats(numGroups, minCols) { + this.numGroups = numGroups; + this.minCols = minCols; + this.stats = {}; + + for (var i = -1; i < numGroups; i++) + this.stats[i] = {num: 0, cols: []}; + + this.groupsFull = 0; + } + + HueStats.prototype.check = function checkHue(i32) { + if (this.groupsFull == this.numGroups + 1) + this.check = function() {return;}; + + var r = (i32 & 0xff), + g = (i32 & 0xff00) >> 8, + b = (i32 & 0xff0000) >> 16, + hg = (r == g && g == b) ? -1 : hueGroup(rgb2hsl(r,g,b).h, this.numGroups), + gr = this.stats[hg], + min = this.minCols; + + gr.num++; + + if (gr.num > min) + return; + if (gr.num == min) + this.groupsFull++; + + if (gr.num <= min) + this.stats[hg].cols.push(i32); + }; + + HueStats.prototype.inject = function injectHues(histG) { + for (var i = -1; i < this.numGroups; i++) { + if (this.stats[i].num <= this.minCols) { + switch (typeOf(histG)) { + case "Array": + this.stats[i].cols.forEach(function(col){ + if (histG.indexOf(col) == -1) + histG.push(col); + }); + break; + case "Object": + this.stats[i].cols.forEach(function(col){ + if (!histG[col]) + histG[col] = 1; + else + histG[col]++; + }); + break; + } + } + } + }; + + // Rec. 709 (sRGB) luma coef + var Pr = .2126, + Pg = .7152, + Pb = .0722; + + // http://alienryderflex.com/hsp.html + function rgb2lum(r,g,b) { + return Math.sqrt( + Pr * r*r + + Pg * g*g + + Pb * b*b + ); + } + + var rd = 255, + gd = 255, + bd = 255; + + var euclMax = Math.sqrt(Pr*rd*rd + Pg*gd*gd + Pb*bd*bd); + // perceptual Euclidean color distance + function distEuclidean(rgb0, rgb1) { + var rd = rgb1[0]-rgb0[0], + gd = rgb1[1]-rgb0[1], + bd = rgb1[2]-rgb0[2]; + + return Math.sqrt(Pr*rd*rd + Pg*gd*gd + Pb*bd*bd) / euclMax; + } + + var manhMax = Pr*rd + Pg*gd + Pb*bd; + // perceptual Manhattan color distance + function distManhattan(rgb0, rgb1) { + var rd = Math.abs(rgb1[0]-rgb0[0]), + gd = Math.abs(rgb1[1]-rgb0[1]), + bd = Math.abs(rgb1[2]-rgb0[2]); + + return (Pr*rd + Pg*gd + Pb*bd) / manhMax; + } + + // http://rgb2hsl.nichabi.com/javascript-function.php + function rgb2hsl(r, g, b) { + var max, min, h, s, l, d; + r /= 255; + g /= 255; + b /= 255; + max = Math.max(r, g, b); + min = Math.min(r, g, b); + l = (max + min) / 2; + if (max == min) { + h = s = 0; + } else { + d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + switch (max) { + case r: h = (g - b) / d + (g < b ? 6 : 0); break; + case g: h = (b - r) / d + 2; break; + case b: h = (r - g) / d + 4; break + } + h /= 6; + } +// h = Math.floor(h * 360) +// s = Math.floor(s * 100) +// l = Math.floor(l * 100) + return { + h: h, + s: s, + l: rgb2lum(r,g,b), + }; + } + + function hueGroup(hue, segs) { + var seg = 1/segs, + haf = seg/2; + + if (hue >= 1 - haf || hue <= haf) + return 0; + + for (var i = 1; i < segs; i++) { + var mid = i*seg; + if (hue >= mid - haf && hue <= mid + haf) + return i; + } + } + + function satGroup(sat) { + return sat; + } + + function lumGroup(lum) { + return lum; + } + + function typeOf(val) { + return Object.prototype.toString.call(val).slice(8,-1); + } + + var sort = isArrSortStable() ? Array.prototype.sort : stableSort; + + // must be used via stableSort.call(arr, fn) + function stableSort(fn) { + var type = typeOf(this[0]); + + if (type == "Number" || type == "String") { + var ord = {}, len = this.length, val; + + for (var i = 0; i < len; i++) { + val = this[i]; + if (ord[val] || ord[val] === 0) continue; + ord[val] = i; + } + + return this.sort(function(a,b) { + return fn(a,b) || ord[a] - ord[b]; + }); + } + else { + var ord = this.map(function(v){return v}); + + return this.sort(function(a,b) { + return fn(a,b) || ord.indexOf(a) - ord.indexOf(b); + }); + } + } + + // test if js engine's Array#sort implementation is stable + function isArrSortStable() { + var str = "abcdefghijklmnopqrstuvwxyz"; + + return "xyzvwtursopqmnklhijfgdeabc" == str.split("").sort(function(a,b) { + return ~~(str.indexOf(b)/2.3) - ~~(str.indexOf(a)/2.3); + }).join(""); + } + + // returns uniform pixel data from various img + // TODO?: if array is passed, createimagedata, createlement canvas? take a pxlen? + function getImageData(img, width) { + var can, ctx, imgd, buf8, buf32, height; + + switch (typeOf(img)) { + case "HTMLImageElement": + can = document.createElement("canvas"); + can.width = img.naturalWidth; + can.height = img.naturalHeight; + ctx = can.getContext("2d"); + ctx.drawImage(img,0,0); + case "Canvas": + case "HTMLCanvasElement": + can = can || img; + ctx = ctx || can.getContext("2d"); + case "CanvasRenderingContext2D": + ctx = ctx || img; + can = can || ctx.canvas; + imgd = ctx.getImageData(0, 0, can.width, can.height); + case "ImageData": + imgd = imgd || img; + width = imgd.width; + if (typeOf(imgd.data) == "CanvasPixelArray") + buf8 = new Uint8Array(imgd.data); + else + buf8 = imgd.data; + case "Array": + case "CanvasPixelArray": + buf8 = buf8 || new Uint8Array(img); + case "Uint8Array": + case "Uint8ClampedArray": + buf8 = buf8 || img; + buf32 = new Uint32Array(buf8.buffer); + case "Uint32Array": + buf32 = buf32 || img; + buf8 = buf8 || new Uint8Array(buf32.buffer); + width = width || buf32.length; + height = buf32.length / width; + } + + return { + can: can, + ctx: ctx, + imgd: imgd, + buf8: buf8, + buf32: buf32, + width: width, + height: height, + }; + } + + // partitions a rect of wid x hgt into + // array of bboxes of w0 x h0 (or less) + function makeBoxes(wid, hgt, w0, h0) { + var wnum = ~~(wid/w0), wrem = wid%w0, + hnum = ~~(hgt/h0), hrem = hgt%h0, + xend = wid-wrem, yend = hgt-hrem; + + var bxs = []; + for (var y = 0; y < hgt; y += h0) + for (var x = 0; x < wid; x += w0) + bxs.push({x:x, y:y, w:(x==xend?wrem:w0), h:(y==yend?hrem:h0)}); + + return bxs; + } + + // iterates @bbox within a parent rect of width @wid; calls @fn, passing index within parent + function iterBox(bbox, wid, fn) { + var b = bbox, + i0 = b.y * wid + b.x, + i1 = (b.y + b.h - 1) * wid + (b.x + b.w - 1), + cnt = 0, incr = wid - b.w + 1, i = i0; + + do { + fn.call(this, i); + i += (++cnt % b.w == 0) ? incr : 1; + } while (i <= i1); + } + + // returns array of hash keys sorted by their values + function sortedHashKeys(obj, desc) { + var keys = []; + + for (var key in obj) + keys.push(key); + + return sort.call(keys, function(a,b) { + return desc ? obj[b] - obj[a] : obj[a] - obj[b]; + }); + } + + // expose + this.RgbQuant = RgbQuant; + + // expose to commonJS + if (typeof module !== 'undefined' && module.exports) { + module.exports = RgbQuant; + } + +}).call(this); \ No newline at end of file diff --git a/main.js b/main.js index 9dd78c21..a2b40674 100644 --- a/main.js +++ b/main.js @@ -1174,6 +1174,7 @@ function rankCells() { const areaMean = d3.mean(cells.area); // to adjust population by cell area for (const i of cells.i) { + if (cells.h[i] < 20) continue; // no population in water let s = +biomesData.habitability[cells.biome[i]]; // base suitability derived from biome habitability if (!s) continue; // uninhabitable biomes has 0 suitability if (flMean) s += normalize(cells.fl[i] + cells.conf[i], flMean, flMax) * 250; // big rivers and confluences are valued diff --git a/modules/river-generator.js b/modules/river-generator.js index 1c89a031..3431aca7 100644 --- a/modules/river-generator.js +++ b/modules/river-generator.js @@ -271,10 +271,14 @@ // remove river and all its tributaries const remove = function(id) { + const cells = pack.cells; const riversToRemove = pack.rivers.filter(r => r.i === id || getBasin(r.i, r.parent, id) === id).map(r => r.i); riversToRemove.forEach(r => rivers.select("#river"+r).remove()); - pack.cells.r.forEach((r, i) => { - if (r && riversToRemove.includes(r)) pack.cells.r[i] = 0; + cells.r.forEach((r, i) => { + if (!r || !riversToRemove.includes(r)) return; + cells.r[i] = 0; + cells.fl[i] = grid.cells.prec[cells.g[i]]; + cells.conf[i] = 0; }); pack.rivers = pack.rivers.filter(r => !riversToRemove.includes(r.i)); } diff --git a/modules/save-and-load.js b/modules/save-and-load.js index 8311d5f0..607e1d7e 100644 --- a/modules/save-and-load.js +++ b/modules/save-and-load.js @@ -294,14 +294,6 @@ async function saveMap() { link.click(); tip(`${link.download} is saved. Open "Downloads" screen (CTRL + J) to check`, true, "success", 7000); window.URL.revokeObjectURL(URL); - - // send saved files count and size to server for usage analysis (for the future Cloud storage) - publicstorage.get("fmg").then(fmg => { - if (!fmg) return; - fmg.size = (fmg.size * fmg.maps + blob.size) / (fmg.maps + 1); - fmg.maps += 1; - publicstorage.set("fmg", fmg).then(fmg => console.log(fmg)); - }); } // Send .map file to server [test function] diff --git a/modules/ui/editors.js b/modules/ui/editors.js index 3b811f2c..e858beaf 100644 --- a/modules/ui/editors.js +++ b/modules/ui/editors.js @@ -111,12 +111,6 @@ function applySorting(headers) { }).forEach(line => list.appendChild(line)); } -// trigger trash button click on "Delete" keypress -function removeElementOnKey() { - $(".dialog:visible .icon-trash").click(); - $("button:visible:contains('Remove')").click(); -} - function addBurg(point) { const cells = pack.cells; const x = rn(point[0], 2), y = rn(point[1], 2); diff --git a/modules/ui/general.js b/modules/ui/general.js index 9833618b..3546f018 100644 --- a/modules/ui/general.js +++ b/modules/ui/general.js @@ -462,4 +462,10 @@ function pressControl() { if (zonesRemove.offsetParent) { zonesRemove.classList.contains("pressed") ? zonesRemove.classList.remove("pressed") : zonesRemove.classList.add("pressed"); } +} + +// trigger trash button click on "Delete" keypress +function removeElementOnKey() { + $(".dialog:visible .fastDelete").click(); + $("button:visible:contains('Remove')").click(); } \ No newline at end of file diff --git a/modules/ui/heightmap-editor.js b/modules/ui/heightmap-editor.js index f888d42c..87278555 100644 --- a/modules/ui/heightmap-editor.js +++ b/modules/ui/heightmap-editor.js @@ -21,7 +21,6 @@ function editHeightmap() { }); }() - let edits = []; restartHistory(); viewbox.insert("g", "#terrs").attr("id", "heights"); @@ -131,6 +130,10 @@ function editHeightmap() { return; } + delete window.edits; // remove global variable + redo.disabled = templateRedo.disabled = true; + undo.disabled = templateUndo.disabled = true; + customization = 0; customizationMenu.style.display = "none"; if (document.getElementById("options").querySelector(".tab > button.active").id === "toolsTab") toolsContent.style.display = "block"; @@ -141,7 +144,6 @@ function editHeightmap() { closeDialogs(); resetZoom(); - restartHistory(); if (document.getElementById("preview")) document.getElementById("preview").remove(); if (document.getElementById("canvas3d")) enterStandardView(); @@ -316,9 +318,7 @@ function editHeightmap() { const land = pack.cells.h[i] >= 20; // check biome - if (!biome[g]) pack.cells.biome[i] = getBiomeId(grid.cells.prec[g], grid.cells.temp[g]); - else if (!land && biome[g]) pack.cells.biome[i] = 0; - else pack.cells.biome[i] = biome[g]; + pack.cells.biome[i] = land && biome[g] ? biome[g] : getBiomeId(grid.cells.prec[g], pack.cells.h[i]); // rivers data if (!changeHeights.checked) { @@ -482,8 +482,8 @@ function editHeightmap() { // restart edits from 1st step function restartHistory() { - edits = []; - edits.n = 0; + window.edits = []; // declare temp global variable + window.edits.n = 0; redo.disabled = templateRedo.disabled = true; undo.disabled = templateUndo.disabled = true; updateHistory(); @@ -503,7 +503,7 @@ function editHeightmap() { document.getElementById("brushesButtons").addEventListener("click", e => toggleBrushMode(e)); document.getElementById("changeOnlyLand").addEventListener("click", e => changeOnlyLandClick(e)); document.getElementById("undo").addEventListener("click", () => restoreHistory(edits.n-1)); - document.getElementById("redo").addEventListener("click", () => restoreHistory(edits.n+1)); + document.getElementById("redo").addEventListener("click", () => restoreHistory(edits.n+1)); document.getElementById("rescaleShow").addEventListener("click", () => { document.getElementById("modifyButtons").style.display = "none"; document.getElementById("rescaleSection").style.display = "block"; @@ -986,7 +986,7 @@ function editHeightmap() { setOverlayOpacity(0); document.getElementById("convertImageLoad").classList.add("glow"); // add glow effect - tip('Image Converter is opened. Upload the image and assign the height for each of the colors', true, "warn"); // main tip + tip('Image Converter is opened. Upload the image and assign height value for each of the colors', true, "warn"); // main tip // remove all heights grid.cells.h = new Uint8Array(grid.cells.i.length); @@ -998,8 +998,8 @@ function editHeightmap() { // add color pallete void function createColorPallete() { - const container = d3.select("#colorScheme"); - container.selectAll("div").data(d3.range(101)).enter().append("div").attr("data-color", i => i) + d3.select("#imageConverterPalette").selectAll("div").data(d3.range(101)) + .enter().append("div").attr("data-color", i => i) .style("background-color", i => color(1-(i < 20 ? i-5 : i) / 100)) .style("width", i => i < 20 || i > 70 ? ".2em" : ".1em") .on("touchmove mousemove", showPalleteHeight).on("click", assignHeight); @@ -1020,7 +1020,7 @@ function editHeightmap() { const height = +this.getAttribute("data-color"); colorsSelectValue.innerHTML = height; colorsSelectFriendly.innerHTML = getHeight(height); - const former = colorScheme.querySelector(".hoveredColor") + const former = imageConverterPalette.querySelector(".hoveredColor") if (former) former.className = ""; this.className = "hoveredColor"; } @@ -1038,14 +1038,15 @@ function editHeightmap() { convertImageLoad.classList.remove("glow"); }; - reader.onloadend = function() {img.src = reader.result;}; + reader.onloadend = () => img.src = reader.result; reader.readAsDataURL(file); } function heightsFromImage(count) { const ctx = document.getElementById("canvas").getContext("2d"); - const imageData = ctx.getImageData(0, 0, graphWidth, graphHeight); - const data = imageData.data; + const q = new RgbQuant({colors:count}); + q.sample(ctx); + const data = q.reduce(ctx); viewbox.select("#heights").selectAll("*").remove(); d3.select("#imageConverter").selectAll("div.color-div").remove(); @@ -1053,27 +1054,22 @@ function editHeightmap() { colorsUnassigned.style.display = "block"; colorsAssigned.style.display = "none"; - const gridColors = grid.points.map(p => { + let usedColors = new Set(); + let gridColors = grid.points.map(p => { const x = Math.floor(p[0]-.01), y = Math.floor(p[1]-.01); const i = (x + y * graphWidth) * 4; const r = data[i], g = data[i+1], b = data[i+2]; + usedColors.add(`rgb(${r},${g},${b})`); return [r, g, b]; }); - const cmap = MMCQ.quantize(gridColors, count); - const usedColors = new Set(); - viewbox.select("#heights").selectAll("polygon").data(grid.cells.i).join("polygon") .attr("points", d => getGridPolygon(d)) - .attr("id", d => "cell"+d).attr("fill", d => { - const clr = `rgb(${cmap.nearest(gridColors[d])})`; - usedColors.add(clr); - return clr; - }).on("click", mapClicked); + .attr("id", d => "cell"+d).attr("fill", d => `rgb(${gridColors[d].join(",")})`) + .on("click", mapClicked); const unassigned = [...usedColors].sort((a, b) => d3.lab(a).l - d3.lab(b).l); - const unassignedContainer = d3.select("#colorsUnassigned"); - unassignedContainer.selectAll("div").data(unassigned).enter().append("div") + d3.select("#colorsUnassigned").selectAll("div").data(unassigned).enter().append("div") .attr("data-color", i => i).style("background-color", i => i) .attr("class", "color-div").on("click", colorClicked); @@ -1092,7 +1088,7 @@ function editHeightmap() { const selectedColor = imageConverter.querySelector("div.selectedColor"); if (selectedColor) selectedColor.classList.remove("selectedColor"); - const hoveredColor = colorScheme.querySelector("div.hoveredColor"); + const hoveredColor = imageConverterPalette.querySelector("div.hoveredColor"); if (hoveredColor) hoveredColor.classList.remove("hoveredColor"); colorsSelectValue.innerHTML = colorsSelectFriendly.innerHTML = 0; @@ -1101,7 +1097,7 @@ function editHeightmap() { if (this.dataset.height) { const height = +this.dataset.height; - colorScheme.querySelector(`div[data-color="${height}"]`).classList.add("hoveredColor"); + imageConverterPalette.querySelector(`div[data-color="${height}"]`).classList.add("hoveredColor"); colorsSelectValue.innerHTML = height; colorsSelectFriendly.innerHTML = getHeight(height); } @@ -1142,8 +1138,8 @@ function editHeightmap() { const lab = d3.lab(colorFrom); const normalized = type === "hue" ? rn(normalize(lab.b + lab.a / 2, -50, 200), 2) : rn(normalize(lab.l, -15, 100), 2); let heightTo = rn(normalized * 100); - if (assinged[heightTo] && heightTo < 100) heightTo += 1; // if height is already added, try increated one - if (assinged[heightTo] && heightTo < 100) heightTo += 1; // if height is already added, try increated one + if (assinged[heightTo] && heightTo < 100) heightTo += 1; // if height is already added, try increased one + if (assinged[heightTo] && heightTo < 100) heightTo += 1; // if height is already added, try increased one if (assinged[heightTo] && heightTo > 3) heightTo -= 3; // if increased one is also added, try decreased one if (assinged[heightTo] && heightTo > 1) heightTo -= 1; // if increased one is also added, try decreased one @@ -1158,16 +1154,16 @@ function editHeightmap() { }); // sort assigned colors by height - Array.from(colorsAssigned.children).sort((a, b) => { - return +a.dataset.height - +b.dataset.height; - }).forEach(line => colorsAssigned.appendChild(line)); + Array.from(colorsAssigned.children) + .sort((a, b) => +a.dataset.height - +b.dataset.height) + .forEach(line => colorsAssigned.appendChild(line)); colorsAssigned.style.display = "block"; colorsUnassigned.style.display = "none"; } function setConvertColorsNumber() { - prompt(`Please provide a desired number of colors.
An actual number depends on color scheme and may vary from desired`, + prompt(`Please set maximum number of colors.
An actual number is lower and depends on color scheme`, {default:+convertColors.value, step:1, min:3, max:255}, number => { convertColors.value = number; heightsFromImage(number); diff --git a/modules/ui/markers-editor.js b/modules/ui/markers-editor.js index f7cd33dd..a4686d89 100644 --- a/modules/ui/markers-editor.js +++ b/modules/ui/markers-editor.js @@ -135,14 +135,18 @@ function editMarker() { const id = elSelected.attr("data-id"); const used = document.querySelectorAll("use[data-id='"+id+"']"); const count = used.length === 1 ? "1 element" : used.length + " elements"; - alertMessage.innerHTML = "Are you sure you want to remove the marker (" + count + ")?"; + alertMessage.innerHTML = "Are you sure you want to remove all markers of that type (" + count + ")?"; - $("#alert").dialog({resizable: false, title: "Remove marker", + $("#alert").dialog({resizable: false, title: "Remove marker type", buttons: { Remove: function() { $(this).dialog("close"); if (id !== "#marker0") d3.select("#defs-markers").select(id).remove(); - used.forEach(e => e.remove()); + used.forEach(e => { + const index = notes.findIndex(n => n.id === e.id); + if (index != -1) notes.splice(index, 1); + e.remove(); + }); updateGroupOptions(); updateGroupOptions(); $("#markerEditor").dialog("close"); @@ -187,12 +191,6 @@ function editMarker() { d3.select("#defs-markers").select(id).select("text").attr("y", this.value + "%"); } - function applyCustomUnicodeIcon() { - if (!this.value) return; - const id = elSelected.attr("data-id"); - d3.select("#defs-markers").select(id).select("text").text(this.value); - } - function toggleStyleSection() { if (markerStyleSection.style.display === "inline-block") { markerEditor.querySelectorAll("button:not(#markerStyle)").forEach(b => b.style.display = "inline-block"); diff --git a/modules/ui/notes-editor.js b/modules/ui/notes-editor.js index 07376acc..cc49d536 100644 --- a/modules/ui/notes-editor.js +++ b/modules/ui/notes-editor.js @@ -41,7 +41,7 @@ function editNotes(id, name) { document.getElementById("notesDownload").addEventListener("click", downloadLegends); document.getElementById("notesUpload").addEventListener("click", () => legendsToLoad.click()); document.getElementById("legendsToLoad").addEventListener("change", function() {uploadFile(this, uploadLegends)}); - document.getElementById("notesRemove").addEventListener("click", triggernotesRemove); + document.getElementById("notesRemove").addEventListener("click", triggerNotesRemove); function changeObject() { const note = notes.find(note => note.id === this.value); @@ -96,7 +96,7 @@ function editNotes(id, name) { editNotes(notes[0].id, notes[0].name); } - function triggernotesRemove() { + function triggerNotesRemove() { alertMessage.innerHTML = "Are you sure you want to remove the selected note?"; $("#alert").dialog({resizable: false, title: "Remove note", buttons: { diff --git a/modules/ui/provinces-editor.js b/modules/ui/provinces-editor.js index 7d738529..3d64fafc 100644 --- a/modules/ui/provinces-editor.js +++ b/modules/ui/provinces-editor.js @@ -114,7 +114,7 @@ function editProvinces() { const capital = p.burg ? pack.burgs[p.burg].name : ''; const separable = p.burg && p.burg !== pack.states[p.state].capital; const focused = defs.select("#fog #focusProvince"+p.i).size(); - lines += `
+ lines += `