(function() {
var RGB = {
"0": [181, 181, 181],
"1": [206, 206, 206],
"2": [197, 197, 197],
"3": [181, 181, 181],
"4": [173, 173, 173],
"5": [164, 164, 164],
"6": [156, 156, 156],
"7": [140, 140, 140],
"8": [132, 132, 132],
"9": [123, 123, 123],
"10": [115, 115, 115],
"11": [99, 99, 99],
"12": [90, 90, 90],
"13": [82, 82, 82],
"14": [66, 66, 66],
"15": [58, 58, 58],
"16": [49, 49, 49],
"17": [41, 41, 41],
"18": [33, 33, 33],
"19": [25, 25, 25],
"20": [82, 58, 24],
"21": [107, 74, 33],
"22": [132, 91, 40],
"23": [163, 107, 58],
"24": [182, 123, 67],
"25": [68, 123, 182],
"26": [58, 107, 165],
"27": [40, 90, 131],
"28": [34, 75, 106],
"29": [26, 58, 83],
"30": [82, 66, 41],
"31": [107, 83, 49],
"32": [132, 99, 66],
"33": [165, 123, 83],
"34": [182, 140, 90],
"35": [91, 140, 181],
"36": [81, 123, 163],
"37": [66, 99, 132],
"38": [48, 82, 107],
"39": [40, 66, 82],
"40": [82, 66, 50],
"41": [106, 90, 65],
"42": [133, 107, 82],
"43": [164, 132, 107],
"44": [181, 148, 115],
"45": [115, 148, 181],
"46": [107, 132, 163],
"47": [81, 107, 132],
"48": [67, 90, 108],
"49": [49, 66, 82],
"50": [82, 73, 66],
"51": [106, 99, 81],
"52": [132, 115, 107],
"53": [164, 148, 132],
"54": [181, 164, 148],
"55": [148, 164, 180],
"56": [132, 148, 164],
"57": [108, 115, 133],
"58": [82, 99, 107],
"59": [66, 75, 82],
"60": [81, 82, 66],
"61": [106, 99, 81],
"62": [132, 123, 106],
"63": [164, 156, 133],
"64": [181, 174, 148],
"65": [164, 174, 140],
"66": [149, 156, 123],
"67": [115, 123, 99],
"68": [90, 99, 82],
"69": [74, 83, 66],
"70": [82, 75, 49],
"71": [108, 99, 66],
"72": [132, 123, 82],
"73": [163, 148, 107],
"74": [180, 164, 115],
"75": [155, 173, 115],
"76": [141, 156, 99],
"77": [107, 124, 82],
"78": [90, 100, 66],
"79": [66, 81, 48],
"80": [83, 74, 41],
"81": [107, 89, 49],
"82": [131, 115, 66],
"83": [165, 140, 83],
"84": [182, 164, 90],
"85": [148, 173, 82],
"86": [132, 155, 73],
"87": [107, 123, 58],
"88": [82, 98, 49],
"89": [66, 83, 41],
"90": [81, 66, 25],
"91": [107, 90, 34],
"92": [132, 107, 41],
"93": [165, 140, 58],
"94": [181, 156, 66],
"95": [140, 173, 58],
"96": [123, 156, 49],
"97": [99, 122, 40],
"98": [82, 98, 33],
"99": [58, 81, 25],
"100": [83, 25, 49],
"101": [107, 33, 66],
"102": [132, 41, 74],
"103": [164, 57, 99],
"104": [181, 66, 107],
"105": [123, 66, 181],
"106": [107, 58, 165],
"107": [90, 41, 132],
"108": [74, 34, 107],
"109": [58, 25, 82],
"110": [82, 41, 59],
"111": [107, 49, 74],
"112": [131, 66, 90],
"113": [163, 82, 115],
"114": [181, 90, 123],
"115": [140, 89, 181],
"116": [124, 82, 164],
"117": [98, 66, 131],
"118": [82, 49, 106],
"119": [66, 41, 82],
"120": [82, 49, 66],
"121": [107, 66, 82],
"122": [133, 82, 99],
"123": [164, 107, 123],
"124": [181, 116, 140],
"125": [147, 115, 180],
"126": [133, 107, 164],
"127": [106, 82, 132],
"128": [91, 66, 106],
"129": [66, 49, 83],
"130": [82, 67, 74],
"131": [107, 81, 90],
"132": [132, 106, 115],
"133": [163, 132, 140],
"134": [181, 149, 164],
"135": [165, 148, 182],
"136": [147, 132, 163],
"137": [116, 106, 131],
"138": [98, 82, 108],
"139": [74, 65, 82],
"140": [82, 66, 66],
"141": [107, 81, 82],
"142": [132, 106, 107],
"143": [164, 132, 133],
"144": [182, 148, 147],
"145": [141, 172, 174],
"146": [124, 155, 157],
"147": [99, 123, 123],
"148": [83, 99, 99],
"149": [66, 82, 82],
"150": [83, 49, 48],
"151": [107, 65, 66],
"152": [132, 82, 83],
"153": [165, 107, 106],
"154": [181, 115, 116],
"155": [115, 173, 174],
"156": [100, 156, 157],
"157": [83, 123, 123],
"158": [68, 98, 100],
"159": [50, 81, 83],
"160": [82, 40, 41],
"161": [107, 49, 48],
"162": [132, 66, 67],
"163": [166, 82, 82],
"164": [181, 89, 89],
"165": [83, 173, 173],
"166": [73, 156, 156],
"167": [59, 123, 123],
"168": [49, 99, 98],
"169": [42, 82, 82],
"170": [83, 25, 24],
"171": [107, 33, 34],
"172": [132, 40, 41],
"173": [164, 58, 58],
"174": [181, 65, 66],
"175": [59, 173, 173],
"176": [50, 156, 156],
"177": [41, 122, 123],
"178": [33, 99, 98],
"179": [25, 82, 83]
},
BLOCKS = [
["Coal", 225, 143],
["Dirt", 224, 185],
["Cracked_dirt", 224, 186],
["Frozen_dirt", 224, 187],
["Granite", 224, 188],
["Grass", 224, 189],
["Tall_dry_grass", 224, 192],
["Mossy_grass", 224, 193],
["Thick_grass", 224, 196],
["Thin_grass", 224, 199],
["Tundra_grass", 224, 201],
["Dark_paradise_grass", 224, 191],
["Paradise_grass", 224, 190],
["Coral_grass", 224, 197],
["Blue_grass", 224, 200],
["Wool_grass", 224, 198],
["Chaos_grass", 224, 194],
["Shadow_grass", 224, 202],
["Vivid_grass", 224, 203],
["Gravel", 224, 204],
["Crystal_gravel", 224, 205],
["Glacial_ice", 224, 206],
["Gray_marble", 224, 208],
["Blue_marble", 224, 207],
["Moss", 224, 209],
["Razor_moss", 224, 210],
["Zen_moss", 224, 211],
["Mossy_branches", 224, 212],
["Doom_branches", 224, 213],
["Mud", 224, 214],
["Gel_mud", 224, 215],
["Radiant_mud", 224, 216],
["Zen_mud", 224, 217],
["Rock", 224, 218],
["Mossy_Rock", 224, 219],
["Psychadelic_rock", 224, 220],
["Orange_rock", 224, 221],
["Volcanic_rock", 225, 144],
["Roots", 224, 222],
["Sand", 224, 223],
["Ruby_sand", 224, 224],
["Savannah_sand", 224, 225],
["Oasis_sand", 224, 226],
["Sleep_sand", 224, 227],
["Dream_sand", 224, 228],
["Shifting_sand", 224, 229],
["Snow", 224, 230],
["Woodchips", 224, 235],
["Cheese", 225, 182],
["Water", 65, 142],
["Bamboo", 224, 9],
["Bricks", 224, 10],
["Sandy_bricks", 224, 11],
["Cinder_blocks", 224, 12],
["Large_cinder_blocks", 224, 13],
["Overgrown_cinder_blocks", 224, 14],
["Cobble", 224, 15],
["Dark_cobble", 224, 16],
["Green_cobble", 224, 17],
["Concrete", 224, 18],
["Crate", 224, 19],
["Facing_bricks_cross", 224, 20],
["Facing_bricks_horizontal", 224, 21],
["Facing_bricks_certical", 224, 22],
["Glass", 224, 23],
["Hedge", 224, 25],
["Blue_hedge", 225, 145],
["Pink_hedge", 225, 146],
["Hexagonal_bricks", 224, 26],
["Yellow_hexagonal_bricks", 224, 27],
["Bear_icon", 224, 28],
["Bear_icon_ruin", 224, 29],
["Horse_icon", 224, 30],
["Horse_icon_ruin", 224, 31],
["Raven_icon", 224, 32],
["Raven_icon_ruin", 224, 33],
["Black_marble", 224, 34],
["Green_marble", 224, 35],
["Orange_marble", 224, 36],
["Red_marble", 225, 91],
["White_marble", 224, 37],
["Copper", 224, 49],
["Copper_circles", 224, 46],
["Copper_gears", 224, 54],
["Copper_grate", 224, 56],
["Copper_beams", 224, 42],
["Gold", 224, 50],
["Gold_circles", 224, 47],
["Gold_grate", 224, 57],
["Gold_beams", 224, 43],
["Steel", 224, 52],
["Steel_circles", 224, 48],
["Steel_gears", 224, 55],
["Steel_grate", 224, 58],
["Steel_beams", 224, 44],
["Corroded_steel", 224, 53],
["Corroded_steel_grate", 224, 59],
["Corroded_steel_beams", 224, 45],
["Purple_anodized_steel", 225, 147],
["Paving_tiles", 224, 60],
["Red_paving_tiles", 225, 92],
["Ruin_stone", 224, 62],
["Concrete_shelves", 224, 63],
["White_marble_shelves", 224, 64],
["Steel_shelves", 224, 65],
["Corroded_steel_shelves", 224, 66],
["Wooden_shelves", 224, 67],
["Dark_wooden_shelves", 224, 68],
["Tiles_left", 224, 69],
["Tiles_right", 224, 70],
["Tiles_single", 224, 71],
["White_timber_framing", 224, 72],
["Blue_timber_framing", 224, 73],
["Gray_timber_framing", 224, 74],
["Orange_timber_framing", 224, 75],
["Pink_timber_framing", 225, 93],
["Red_timber_framing", 225, 94],
["Purple_timber_framing", 225, 95],
["Yellow_timber_framing", 224, 76],
["Wooden_planks", 224, 77],
["Blue_wooden_planks", 225, 148],
["Dark_wooden_planks", 224, 78],
["Black_roofing_tiles", 224, 130],
["Blue_roofing_tiles", 224, 131],
["Orange_roofing_tiles", 224, 132],
["Red_roofing_tiles", 225, 126],
["Purple_roofing_tiles", 225, 127],
["Tapered_stones", 224, 133],
["Blue_cloth", 224, 173],
["Brown_cloth", 224, 174],
["Creme_cloth", 224, 175],
["Green_cloth", 225, 159],
["Pink_cloth", 225, 129],
["Red_cloth", 225, 130],
["Purple_cloth", 225, 131],
["Saphire_glass", 225, 160],
["Emerald_glass", 225, 161],
["Glass_shaped", 225, 132],
["Citrine_glass", 225, 162],
["Dry_hedge", 225, 163],
["Shaped_copper", 224, 176],
["Shaped_gold", 225, 175],
["Shaped_patina", 224, 177],
["Shaped_red_anodized_steel", 225, 133],
["Shaped_steel", 224, 178],
["Shaped_corroded_steel", 224, 179],
["Shaped_purple_anodized_steel", 225, 134],
["Golden_beams_shaped", 225, 135],
["Steel_beams_shaped", 225, 136],
["White_plaster", 224, 180],
["Blue_plaster", 224, 181],
["Grey_plaster", 224, 182],
["Orange_plaster", 224, 183],
["Pink_plaster", 225, 137],
["Red_plaster", 225, 138],
["Purple_plaster", 225, 139],
["Yellow_plaster", 224, 184],
["Brown_rope_net", 225, 164],
["Yellow_rope_net", 225, 165],
["Lava", 65, 141],
["Cyanergy", 225, 168],
["Lantern", 224, 168],
["Chaos_crystal", 224, 233],
["Symbionts", 224, 234],
["Crimson_lantern_block", 224, 170],
["Violet_lantern_block", 224, 171],
["Treasure", 224, 172]
];
var root = document.getElementById('ap-app');
var nums = Object.keys(RGB).map(Number).filter(function(n) {
return n != 0
});
root.innerHTML = '<style>#ap-app{font:14px/1.45 system-ui,-apple-system,Segoe UI,Arial,sans-serif;color:#2c3e50;max-width:980px;margin:auto}#ap-app h2{font-size:24px;margin:0 0 2px}#ap-app h2 small{font-size:14px;font-weight:600;color:#e74c3c}#ap-app .r{margin:6px 0;display:flex;gap:8px;align-items:center;flex-wrap:wrap}#ap-app button{padding:7px 14px;cursor:pointer;border:0;border-radius:8px;background:#34495e;color:#fff;font:600 13px system-ui,Arial,sans-serif;transition:background .15s}#ap-app button:hover{background:#2c3e50}#ap-app button.on,#ap-app button.on:hover{background:#e74c3c}#ap-app #ap-put{background:#e74c3c;font-size:14px}#ap-app #ap-put:hover{background:#c0392b}#ap-app input[type=text],#ap-app input[type=number],#ap-app input:not([type]){border:1px solid #cfd8e0;border-radius:6px;padding:4px 6px;font:inherit;color:inherit;background:#fff}#ap-app input[list]{border:1px solid #cfd8e0;border-radius:6px;padding:5px 7px;font:inherit;box-sizing:border-box}#ap-app input[type=range],#ap-app input[type=checkbox]{accent-color:#e74c3c}#ap-app input[type=color]{border:1px solid #cfd8e0;border-radius:6px;padding:0;width:34px;height:24px;background:#fff;cursor:pointer}#ap-app select{border:1px solid #cfd8e0;border-radius:6px;padding:5px 7px;font:inherit;background:#fff;color:inherit}#ap-app canvas{border:1px solid #d5dbe1;border-radius:10px;background:#1f2933;max-width:100%;box-shadow:0 2px 8px rgba(44,62,80,.12)}#ap-app h4{margin:16px 0 5px;padding-bottom:3px;font-size:11px;text-transform:uppercase;letter-spacing:.07em;color:#7f8c8d;border-bottom:1px solid #e1e6eb}#ap-app small{color:#8795a1;font-size:12px}#ap-controls{width:250px;flex:none;box-sizing:border-box;background:#f6f8fa;border:1px solid #e1e6eb;border-radius:12px;padding:6px 14px 14px}#ap-controls h4:first-child{margin-top:8px}#ap-info{margin-top:8px;padding:6px 10px;background:#f6f8fa;border-radius:8px;color:#5d6d7e}#ap-pal{line-height:0;max-height:230px;overflow:auto;border-radius:6px}#ap-pal div{width:15px;height:15px;display:inline-block;border:1px solid #0003;box-sizing:border-box;cursor:pointer}#ap-pal div.sel{outline:2px solid #e74c3c;z-index:2;position:relative}#ap-app code{background:#eef1f4;padding:1px 5px;border-radius:4px;font-size:12px}#ap-app .ap-help{font-size:13px;color:#5d6d7e;background:#fdf2f0;border-left:4px solid #e74c3c;border-radius:6px;padding:8px 12px;margin-top:14px}#ap-app .ap-credit{font-size:12px;color:#8795a1;text-align:right;margin:8px 0 0}#ap-visbox{background:#fff;border:1px solid #cfd8e0;border-radius:8px;padding:8px;margin-top:6px}#ap-visbox select{width:100%}</style>' +
'<h2 style="margin:0">Angeldust Painter <small>by obi</small></h2><p style="margin:4px 0">Turns a picture into painted blocks. Runs in your browser, nothing is uploaded. Originally made in python by Obi (@˙ıqo -).</p>' +
'<div style="display:flex;gap:14px;flex-wrap:wrap"><div id="ap-controls">' +
'<div id="ap-s-edit"><input type="file" id="ap-file" accept="image/*" style="width:100%"><div id="ap-fname"><small>no image</small></div>' +
'<h4>Block</h4><input list="ap-blocks" id="ap-block" value="White_marble" style="width:100%"><datalist id="ap-blocks"></datalist><small>type to search</small>' +
'<h4>Size</h4><div class="r">Width in blocks <input type="number" id="ap-width" value="64" min="1" max="1024"></div><input type="range" id="ap-wr" min="1" max="256" value="64" style="width:100%">' +
'<label><input type="checkbox" id="ap-grid" checked> Follow detected pixel grid</label><br><small id="ap-gi"></small><div id="ap-size"></div>' +
'<h4>Background</h4><div class="r"><label><input type="checkbox" id="ap-bg"> Make transparent</label><input type="color" id="ap-bgc" value="#000000"></div><small>click the original to pick the colour</small>' +
'<div class="r">Tolerance <input type="range" id="ap-tol" min="0" max="120" value="20"></div>' +
'<h4>Colours</h4><div class="r">Brightness importance <input type="range" id="ap-light" min="0.1" max="2" step="0.05" value="1" style="width:90px"></div><small>lower keeps hues, higher keeps light/dark</small>' +
'<div class="r" style="margin-top:6px">Max colours <input type="number" id="ap-maxc" min="1" max="179" value="179" style="width:52px"> <input type="range" id="ap-maxcr" min="1" max="179" value="179" style="width:85px"></div><small id="ap-cused">Colours used: -</small>' +
'<h4>Dithering</h4><div class="r"><select id="ap-dither"><option value="none">None</option><option value="fs">Floyd-Steinberg</option><option value="atkinson">Atkinson</option><option value="jarvis">Jarvis-Judice-Ninke</option><option value="sierralite">Sierra Lite</option><option value="bayer4">Ordered 4x4</option><option value="bayer8">Ordered 8x8</option></select></div><div class="r">Strength <input type="range" id="ap-dstr" min="0" max="100" value="100"></div><small>mixes palette colours to fake shades, best on photos</small>' +
'<h4>Placement (bottom left claim)</h4><div class="r">X <input type="number" id="ap-ox" value="32770"> Y <input type="number" id="ap-oy" value="32769"></div></div>' +
'<div id="ap-s-man" style="display:none"><h4>Manual pixel art mode</h4><p>Click a block to tick it off, or hold and drag to tick many at once. Start on a ticked block to untick while dragging. Right-drag to move, mouse wheel to zoom.</p>' +
'<h4>Select colour to build</h4><div id="ap-visbox"><div class="r"><select id="ap-visc"><option value="0">All colours (normal)</option></select></div><div class="r" style="margin-top:4px"><label><input type="checkbox" id="ap-dist" checked> Show in-between blocks</label></div><div id="ap-visinfo" style="font-size:12px;color:#5d6d7e;margin-top:4px;line-height:1.35"></div></div>' +
'<div id="ap-prog" style="margin-top:8px;font-weight:600"></div><div class="r" style="margin-top:6px"><button id="ap-clearm">Clear all marks</button></div></div>' +
'<div id="ap-s-paint" style="display:none"><h4>Paint mode</h4><div class="r" id="ap-tools"></div><div class="r">Brush size <input type="range" id="ap-bsize" min="1" max="9" value="1" style="width:90px"><span id="ap-bs">1</span></div><label><input type="checkbox" id="ap-fillshape"> Filled rectangle / ellipse</label><br><label><input type="checkbox" id="ap-global"> Fill replaces every block of that colour</label><br><small>Keys: B L R O F I, Ctrl+Z undo, Ctrl+Y redo</small><div class="r"><button id="ap-eraser">Eraser (no block)</button><span id="ap-selc"></span></div><div id="ap-pal"></div>' +
'<div class="r"><button id="ap-undo">Undo</button><button id="ap-redo">Redo</button><button id="ap-clearc">Clear canvas</button></div>' +
'<h4>Add / remove blocks (negative removes)</h4><div class="r">Left <input type="number" id="ap-pl" value="0"> Right <input type="number" id="ap-pr" value="0"> Top <input type="number" id="ap-pt" value="0"> Bottom <input type="number" id="ap-pb" value="0"></div><button id="ap-resize">Apply size change</button>' +
'<h4>New blank picture</h4><div class="r"><input type="number" id="ap-nw" value="32"> x <input type="number" id="ap-nh" value="32"> <button id="ap-new">Create</button></div></div>' +
'<div class="r" style="margin-top:12px"><button id="ap-put" style="width:100%;padding:8px">Put into game (download .zip)</button></div></div>' +
'<div style="flex:1;min-width:300px"><div class="r" style="flex-wrap:nowrap;align-items:flex-start"><div id="ap-ow"><b>Original</b><br><canvas id="ap-oc" width="430" height="520"></canvas></div><div><b>In game</b><br><canvas id="ap-vc" width="430" height="520"></canvas></div></div>' +
'<div id="ap-info" style="min-height:20px;font-size:13px"></div><div class="r" style="justify-content:flex-end"><button id="ap-bpaint">Paint mode</button><button id="ap-bman">Manual pixel art mode</button><button id="ap-bedit" style="display:none">Back to editor</button></div></div></div>' +
'<div class="ap-help"><b>Put into game</b> downloads a .zip of claim files. Unzip it into your Angeldust folder, replace existing files, then restart Angeldust. It replaces whatever is at that X / Y spot.<br><br><b>Windows</b><br>Folder: <code>C:\\Users\\YOUR_NAME\\AppData\\Roaming\\Metagaming B.V\\Angeldust</code><br>Open it: press Win+R, paste <code>%appdata%\\Metagaming B.V\\Angeldust</code> and press Enter.<br><br><b>macOS (Mac App Store version)</b><br>Folder: <code>~/Library/Containers/st.angeldu.AngeldustOSX/Data/Library/Application Support/Metagaming B.V./Angeldust</code><br>Open it: in Finder press Shift+Cmd+G, paste the folder path and press Enter.<br><br><b>macOS (other versions)</b><br>Folder: <code>~/Library/Application Support/Metagaming B.V./Angeldust</code><br>Open it: in Finder press Shift+Cmd+G, paste the folder path and press Enter.<br><br><b>Linux</b><br>Folder: <code>~/.local/share/Metagaming B.V./Angeldust</code><br>Open it: in your file manager press Ctrl+L, paste the folder path and press Enter.</div><p class="ap-credit">Angeldust Painter by obi</p>';
function $(i) {
return document.getElementById('ap-' + i)
}
var dl = $('blocks');
BLOCKS.forEach(function(b) {
var o = document.createElement('option');
o.value = b[0];
dl.appendChild(o)
});
var vc = $('vc'),
vx = vc.getContext('2d'),
oc = $('oc'),
ox_ = oc.getContext('2d');
var src = null,
srcCv = null,
grid = null,
art = null,
artCv = null,
dirty = true,
mode = 'edit',
bgc = [0, 0, 0],
sel = 1,
undoS = [],
redoS = [],
marks = {},
z = 8,
ox = 0,
oy = 0,
pan = null,
painting = false,
raf = 0,
visColor = 0;
var clamp = function(v, a, b) {
return Math.max(a, Math.min(b, v))
};
function median(a) {
a.sort(function(x, y) {
return x - y
});
return a[a.length >> 1]
}
function lab(c) {
var r = c.map(function(v) {
v /= 255;
return v > 0.04045 ? Math.pow((v + 0.055) / 1.055, 2.4) : v / 12.92
});
var x = (0.4124 * r[0] + 0.3576 * r[1] + 0.1805 * r[2]) / 0.95047,
y = 0.2126 * r[0] + 0.7152 * r[1] + 0.0722 * r[2],
q = (0.0193 * r[0] + 0.1192 * r[1] + 0.9505 * r[2]) / 1.08883;
var f = function(t) {
return t > 0.008856 ? Math.cbrt(t) : 7.787 * t + 16 / 116
};
return [116 * f(y) - 16, 500 * (f(x) - f(y)), 200 * (f(y) - f(q))]
}
var palLab = nums.map(function(n) {
return lab(RGB[n])
});
function closest(rgb, lw, pool) {
pool = pool || nums;
var best = 1e18,
bi = pool[0],
p = lab(rgb);
for (var i = 0; i < pool.length; i++) {
var n = pool[i],
q = palLab[nums.indexOf(n)],
a = (p[0] - q[0]) * lw,
b = p[1] - q[1],
c = p[2] - q[2],
d = a * a + b * b + c * c;
if (d < best) {
best = d;
bi = n
}
}
return bi
}
function detectGrid(img) {
var w = img.w,
h = img.h,
d = img.data,
g = new Float64Array(w * h),
res = [];
for (var i = 0; i < w * h; i++) g[i] = (d[i * 4] + d[i * 4 + 1] + d[i * 4 + 2]) * d[i * 4 + 3] / 255;
for (var ax = 1; ax >= 0; ax--) {
var n = (ax ? w : h) - 1,
p = new Float64Array(Math.max(n, 0));
if (n < 8) return null;
if (ax) {
for (var y = 0; y < h; y++)
for (var x = 0; x < n; x++) p[x] += Math.abs(g[y * w + x + 1] - g[y * w + x])
} else {
for (var y = 0; y < n; y++)
for (var x = 0; x < w; x++) p[y] += Math.abs(g[(y + 1) * w + x] - g[y * w + x])
}
var lo = Math.max(2, Math.ceil(n / 64)),
hi = Math.floor(n / 2);
if (hi <= lo + 4) return null;
var m = 0;
for (var j = 0; j < n; j++) m += p[j];
m /= n;
var pw = [],
re = [],
im = [];
for (var k = lo; k < hi; k++) {
var a = 0,
b = 0,
t = -2 * Math.PI * k / n;
for (var j = 0; j < n; j++) {
var v = p[j] - m;
a += v * Math.cos(t * j);
b += v * Math.sin(t * j)
}
re.push(a);
im.push(b);
pw.push(Math.hypot(a, b))
}
var bi = 0;
for (var i = 0; i < pw.length; i++)
if (pw[i] > pw[bi]) bi = i;
var s = pw.slice().sort(function(x, y) {
return x - y
});
if (pw[bi] < 5 * s[s.length >> 1]) return null;
var cell = n / (lo + bi),
off = ((-Math.atan2(im[bi], re[bi]) / (2 * Math.PI) * cell + 1) % cell + cell) % cell;
if (off > cell / 2) off -= cell;
res.push(cell, off)
}
if (Math.abs(res[0] - res[2]) > 0.1 * Math.max(res[0], res[2])) return null;
return res
}
function gridWidth() {
return Math.max(1, Math.round((src.w - grid[1]) / grid[0]))
}
function downscale(img, width, gr) {
var w = img.w,
h = img.h,
height, xe = [],
ye = [];
if (gr) {
height = Math.max(1, Math.round((h - gr[3]) / gr[2]));
for (var i = 0; i <= width; i++) xe.push(gr[1] + gr[0] * i);
for (var i = 0; i <= height; i++) ye.push(gr[3] + gr[2] * i)
} else {
height = Math.max(1, Math.round(h * width / w));
for (var i = 0; i <= width; i++) xe.push(w * i / width);
for (var i = 0; i <= height; i++) ye.push(h * i / height)
}
var n = clamp(Math.floor(Math.min(w / width, h / height)), 1, 5),
f = [];
for (var i = 0; i < n; i++) f.push(n > 1 ? 0.2 + 0.6 * i / (n - 1) : 0.5);
var out = new Uint8ClampedArray(width * height * 4);
for (var y = 0; y < height; y++)
for (var x = 0; x < width; x++) {
var ch = [
[],
[],
[],
[]
];
for (var j = 0; j < n; j++)
for (var k = 0; k < n; k++) {
var px = clamp(Math.floor(xe[x] + f[k] * (xe[x + 1] - xe[x])), 0, w - 1),
py = clamp(Math.floor(ye[y] + f[j] * (ye[y + 1] - ye[y])), 0, h - 1),
o = (py * w + px) * 4;
for (var c = 0; c < 4; c++) ch[c].push(img.data[o + c])
}
for (var c = 0; c < 4; c++) out[(y * width + x) * 4 + c] = median(ch[c])
}
return {
w: width,
h: height,
data: out
}
}
function borderColour(a) {
var cnt = {},
best = 0,
bk = '0,0,0',
w = a.w,
h = a.h,
d = a.data;
function add(i) {
var k = d[i * 4] + ',' + d[i * 4 + 1] + ',' + d[i * 4 + 2];
cnt[k] = (cnt[k] || 0) + 1
}
for (var x = 0; x < w; x++) {
add(x);
add((h - 1) * w + x)
}
for (var y = 0; y < h; y++) {
add(y * w);
add(y * w + w - 1)
}
for (var k in cnt)
if (cnt[k] > best) {
best = cnt[k];
bk = k
} return bk.split(',').map(Number)
}
function removeBg(a, bc, tol) {
var w = a.w,
h = a.h,
d = a.data,
seen = new Uint8Array(w * h),
st = [];
function near(i) {
return Math.max(Math.abs(d[i * 4] - bc[0]), Math.abs(d[i * 4 + 1] - bc[1]), Math.abs(d[i * 4 + 2] - bc[2])) <= tol
}
function add(i) {
if (!seen[i] && near(i)) {
seen[i] = 1;
st.push(i)
}
}
for (var x = 0; x < w; x++) {
add(x);
add((h - 1) * w + x)
}
for (var y = 0; y < h; y++) {
add(y * w);
add(y * w + w - 1)
}
while (st.length) {
var i = st.pop(),
x = i % w,
y = (i - x) / w;
if (x > 0) add(i - 1);
if (x < w - 1) add(i + 1);
if (y > 0) add(i - w);
if (y < h - 1) add(i + w)
}
for (var i = 0; i < w * h; i++)
if (seen[i]) d[i * 4 + 3] = 0
}
function bayer(n) {
var m = [
[0]
];
while (m.length < n) {
var k = m.length,
o = [];
for (var y = 0; y < 2 * k; y++) {
o.push([]);
for (var x = 0; x < 2 * k; x++) {
var v = 4 * m[y % k][x % k];
o[y].push(v + (y < k ? (x < k ? 0 : 2) : (x < k ? 3 : 1)))
}
}
m = o
}
return m
}
var KERN = {
fs: [
[1, 0, 7 / 16],
[-1, 1, 3 / 16],
[0, 1, 5 / 16],
[1, 1, 1 / 16]
],
atkinson: [
[1, 0, 1 / 8],
[2, 0, 1 / 8],
[-1, 1, 1 / 8],
[0, 1, 1 / 8],
[1, 1, 1 / 8],
[0, 2, 1 / 8]
],
jarvis: [
[1, 0, 7 / 48],
[2, 0, 5 / 48],
[-2, 1, 3 / 48],
[-1, 1, 5 / 48],
[0, 1, 7 / 48],
[1, 1, 5 / 48],
[2, 1, 3 / 48],
[-2, 2, 1 / 48],
[-1, 2, 3 / 48],
[0, 2, 5 / 48],
[1, 2, 3 / 48],
[2, 2, 1 / 48]
],
sierralite: [
[1, 0, 0.5],
[-1, 1, 0.25],
[0, 1, 0.25]
]
};
function quant(a, lw, algo, st, maxC) {
algo = algo || 'none';
st = st == null ? 1 : st;
maxC = clamp(maxC || 179, 1, 179);
var w = a.w,
h = a.h,
d = a.data,
c = new Int32Array(w * h),
cache = {};
var allowed = nums;
if (maxC < nums.length) {
var counts = {}, cacheTmp = {};
for (var i = 0; i < w * h; i++) {
if (d[i * 4 + 3] < 128) continue;
var r = d[i * 4], g = d[i * 4 + 1], b = d[i * 4 + 2];
var k = (r << 16) | (g << 8) | b;
if (!cacheTmp[k]) cacheTmp[k] = closest([r, g, b], lw, nums);
var col = cacheTmp[k];
counts[col] = (counts[col] || 0) + 1;
}
var usedCols = Object.keys(counts).map(Number).sort(function(x, y) {
return counts[y] - counts[x];
});
if (usedCols.length <= maxC) {
allowed = usedCols.length ? usedCols : [1];
} else {
var picked = [usedCols[0]], pool = usedCols.slice(1);
while (picked.length < maxC && pool.length) {
var bestIdx = 0, bestScore = -1;
for (var p = 0; p < pool.length; p++) {
var cand = pool[p],
candLab = palLab[nums.indexOf(cand)],
minD = 1e18;
for (var q = 0; q < picked.length; q++) {
var qLab = palLab[nums.indexOf(picked[q])],
da = (candLab[0] - qLab[0]) * lw,
db = candLab[1] - qLab[1],
dc = candLab[2] - qLab[2],
distSq = da * da + db * db + dc * dc;
if (distSq < minD) minD = distSq;
}
var sc = minD * Math.sqrt(counts[cand] || 1);
if (sc > bestScore) {
bestScore = sc;
bestIdx = p;
}
}
picked.push(pool.splice(bestIdx, 1)[0]);
}
allowed = picked;
}
}
function near(r, g, b) {
r = clamp(Math.round(r), 0, 255);
g = clamp(Math.round(g), 0, 255);
b = clamp(Math.round(b), 0, 255);
var k = r << 16 | g << 8 | b;
if (!(k in cache)) cache[k] = closest([r, g, b], lw, allowed);
return cache[k]
}
if (algo == 'none' || !st) {
for (var i = 0; i < c.length; i++) {
if (d[i * 4 + 3] < 128) continue;
c[i] = near(d[i * 4], d[i * 4 + 1], d[i * 4 + 2])
}
} else if (algo.indexOf('bayer') == 0) {
var n = +algo.slice(5),
B = bayer(n);
for (var y = 0; y < h; y++)
for (var x = 0; x < w; x++) {
var i = y * w + x;
if (d[i * 4 + 3] < 128) continue;
var t = ((B[y % n][x % n] + 0.5) / (n * n) - 0.5) * st * 64;
c[i] = near(d[i * 4] + t, d[i * 4 + 1] + t, d[i * 4 + 2] + t)
}
} else {
var K = KERN[algo] || KERN.fs,
buf = new Float32Array(w * h * 3);
for (var i = 0; i < w * h; i++) {
buf[i * 3] = d[i * 4];
buf[i * 3 + 1] = d[i * 4 + 1];
buf[i * 3 + 2] = d[i * 4 + 2]
}
for (var y = 0; y < h; y++)
for (var x = 0; x < w; x++) {
var i = y * w + x;
if (d[i * 4 + 3] < 128) continue;
var v = [clamp(buf[i * 3], 0, 255), clamp(buf[i * 3 + 1], 0, 255), clamp(buf[i * 3 + 2], 0, 255)],
q = near(v[0], v[1], v[2]),
p = RGB[q];
c[i] = q;
for (var k = 0; k < K.length; k++) {
var xx = x + K[k][0],
yy = y + K[k][1];
if (xx < 0 || xx >= w || yy >= h) continue;
var j = yy * w + xx;
if (d[j * 4 + 3] < 128) continue;
for (var ch = 0; ch < 3; ch++) buf[j * 3 + ch] += (v[ch] - p[ch]) * K[k][2] * st
}
}
}
return {
w: w,
h: h,
c: c
}
}
function getMST(points) {
var n = points.length;
if (n <= 1) return [];
if (n > 1200) {
var edges = [];
for (var i = 0; i < n; i++) {
var bestD = 1e9, bi = -1;
for (var j = 0; j < n; j++) {
if (i == j) continue;
var dx = points[i].x - points[j].x, dy = points[i].y - points[j].y, d = Math.hypot(dx, dy);
if (d < bestD) { bestD = d; bi = j; }
}
if (bi > i) edges.push([i, bi, bestD]);
}
return edges;
}
var inTree = new Uint8Array(n), minDist = new Float64Array(n), parent = new Int32Array(n);
for (var i = 0; i < n; i++) { minDist[i] = 1e9; parent[i] = -1; }
minDist[0] = 0;
var edges = [];
for (var step = 0; step < n; step++) {
var u = -1, best = 1e9;
for (var i = 0; i < n; i++) {
if (!inTree[i] && minDist[i] < best) { best = minDist[i]; u = i; }
}
if (u === -1) break;
inTree[u] = 1;
if (parent[u] !== -1) edges.push([parent[u], u, best]);
var pu = points[u];
for (var v = 0; v < n; v++) {
if (!inTree[v]) {
var dx = pu.x - points[v].x, dy = pu.y - points[v].y, d = Math.hypot(dx, dy);
if (d < minDist[v]) {
minDist[v] = d;
parent[v] = u;
}
}
}
}
return edges;
}
function getBetweenInfo(pts) {
var edges = getMST(pts);
var betweenMap = {};
edges.forEach(function(e) {
var p1 = pts[e[0]], p2 = pts[e[1]];
var sx = p1.x < p2.x ? 1 : (p1.x > p2.x ? -1 : 0);
var sy = p1.y < p2.y ? 1 : (p1.y > p2.y ? -1 : 0);
var step = 1;
if (sx !== 0) {
for (var x = p1.x + sx; x !== p2.x; x += sx) {
var key = p1.y * art.w + x;
if (!betweenMap[key]) betweenMap[key] = { x: x, y: p1.y, num: step };
step++;
}
}
if (sx !== 0 && sy !== 0) {
var cornerKey = p1.y * art.w + p2.x;
if (!betweenMap[cornerKey]) betweenMap[cornerKey] = { x: p2.x, y: p1.y, num: step };
step++;
}
if (sy !== 0) {
for (var y = p1.y + sy; y !== p2.y; y += sy) {
var key = y * art.w + p2.x;
if (!betweenMap[key]) betweenMap[key] = { x: p2.x, y: y, num: step };
step++;
}
}
});
return { edges: edges, between: betweenMap };
}
function claims(w, h) {
return Math.ceil(w / 32) * Math.ceil(h / 32)
}
function updateVisSelector() {
var selEl = $('visc');
if (!selEl) return;
var prev = visColor;
var counts = {};
if (art) {
for (var i = 0; i < art.c.length; i++) {
var n = art.c[i];
if (n) counts[n] = (counts[n] || 0) + 1;
}
}
var list = Object.keys(counts).map(Number).sort(function(a, b) {
return counts[b] - counts[a];
});
selEl.innerHTML = '';
var opt0 = document.createElement('option');
opt0.value = '0';
opt0.textContent = 'All colours (normal)';
selEl.appendChild(opt0);
list.forEach(function(n) {
var o = document.createElement('option');
o.value = String(n);
o.textContent = 'Colour ' + n + ' (' + counts[n] + ' blocks)';
selEl.appendChild(o);
});
if (counts[prev]) {
selEl.value = String(prev);
visColor = prev;
} else {
selEl.value = '0';
visColor = 0;
}
updateVisInfo();
}
function updateVisInfo() {
var info = $('visinfo');
if (!info) return;
if (!art) {
info.textContent = 'No image loaded.';
return;
}
if (visColor === 0) {
info.textContent = 'Select a colour above to isolate it and show in-between block numbers.';
return;
}
var pts = [];
for (var y = 0; y < art.h; y++)
for (var x = 0; x < art.w; x++)
if (art.c[y * art.w + x] === visColor) pts.push({x: x, y: y});
if (!pts.length) {
info.textContent = 'No blocks of colour ' + visColor + '.';
return;
}
if (pts.length === 1) {
info.textContent = 'Colour ' + visColor + ': exactly 1 block at (' + pts[0].x + ', ' + pts[0].y + ').';
return;
}
var r = getBetweenInfo(pts);
var minD = 1e9, sumD = 0;
r.edges.forEach(function(e) {
if (e[2] < minD) minD = e[2];
sumD += e[2];
});
var avgD = (sumD / r.edges.length).toFixed(1);
var bCount = Object.keys(r.between).length;
info.textContent = 'Colour ' + visColor + ': ' + pts.length + ' blocks. Closest neighbour: ' + minD.toFixed(1) + ' blk. In-between blocks: ' + bCount + '.';
}
function recompute() {
if (!src) return;
var ug = grid && $('grid').checked,
width = ug ? gridWidth() : clamp(+$('width').value || 1, 1, 1024),
maxC = clamp(+$('maxc').value || 179, 1, 179);
var a = downscale(src, width, ug ? grid : null);
if ($('bg').checked) removeBg(a, bgc, +$('tol').value);
art = quant(a, +$('light').value, $('dither').value, +$('dstr').value / 100, maxC);
marks = {};
undoS = [];
redoS = [];
dirty = true;
$('width').value = width;
$('wr').value = Math.min(width, 256);
$('width').disabled = $('wr').disabled = ug;
var s = 0, usedMap = {};
for (var i = 0; i < art.c.length; i++) {
if (art.c[i]) {
s++;
usedMap[art.c[i]] = 1;
}
}
var usedCount = Object.keys(usedMap).length;
$('cused').textContent = 'Colours used: ' + usedCount + ' (limit: ' + maxC + ')';
$('size').textContent = art.w + ' x ' + art.h + ' blocks, ' + s + ' placed, ' + claims(art.w, art.h) + ' claims (' + Math.ceil(art.w / 32) + ' x ' + Math.ceil(art.h / 32) + ')';
updateVisSelector();
fit();
draw()
}
function setSrc(img) {
src = img;
srcCv = document.createElement('canvas');
srcCv.width = img.w;
srcCv.height = img.h;
srcCv.getContext('2d').putImageData(new ImageData(img.data, img.w, img.h), 0, 0);
grid = detectGrid(img);
$('gi').textContent = grid ? 'pixel grid found: ' + grid[0].toFixed(2) + ' px cells, ' + gridWidth() + ' blocks wide' : 'no pixel grid found';
$('grid').disabled = !grid;
$('grid').checked = !!grid;
if (!grid && img.w > 0) $('width').value = Math.min(64, img.w);
var bb = borderColour(downscale(img, Math.min(img.w, 64), null));
bgc = bb;
$('bgc').value = hex(bb);
recompute();
drawOrig()
}
function hex(c) {
return '#' + c.map(function(v) {
return (v < 16 ? '0' : '') + v.toString(16)
}).join('')
}
function ensureArt() {
if (!art) {
art = {
w: 32,
h: 32,
c: new Int32Array(1024)
};
dirty = true;
fit();
updateVisSelector()
}
}
function buildArtCv() {
if (!artCv) artCv = document.createElement('canvas');
artCv.width = art.w;
artCv.height = art.h;
var id = new ImageData(art.w, art.h);
for (var i = 0; i < art.c.length; i++) {
var n = art.c[i];
if (n) {
var c = RGB[n];
id.data[i * 4] = c[0];
id.data[i * 4 + 1] = c[1];
id.data[i * 4 + 2] = c[2];
id.data[i * 4 + 3] = 255
}
}
artCv.getContext('2d').putImageData(id, 0, 0);
dirty = false
}
function fit() {
if (!art) return;
z = Math.max(1, Math.min(vc.width / art.w, vc.height / art.h));
ox = (vc.width - art.w * z) / 2;
oy = (vc.height - art.h * z) / 2
}
function later() {
if (!raf) raf = requestAnimationFrame(function() {
raf = 0;
draw()
})
}
function draw() {
vx.clearRect(0, 0, vc.width, vc.height);
if (!art) return;
if (dirty) buildArtCv();
vx.imageSmoothingEnabled = false;
var visPts = [];
if (mode == 'man' && visColor > 0) {
for (var y = 0; y < art.h; y++)
for (var x = 0; x < art.w; x++) {
var idx = y * art.w + x;
if (art.c[idx] === visColor) visPts.push({ x: x, y: y, i: idx });
}
}
if (mode != 'man') {
vx.drawImage(artCv, ox, oy, art.w * z, art.h * z);
} else {
if (visColor === 0) {
var x0 = Math.max(0, Math.floor(-ox / z)),
y0 = Math.max(0, Math.floor(-oy / z)),
x1 = Math.min(art.w, Math.ceil((vc.width - ox) / z)),
y1 = Math.min(art.h, Math.ceil((vc.height - oy) / z));
vx.textAlign = 'center';
vx.textBaseline = 'middle';
for (var y = y0; y < y1; y++) {
for (var x = x0; x < x1; x++) {
var i = y * art.w + x,
n = art.c[i];
if (!n) continue;
var c = RGB[n],
dn = marks[i],
px = ox + x * z,
py = oy + y * z;
vx.fillStyle = 'rgb(' + c[0] + ',' + c[1] + ',' + c[2] + ')';
vx.fillRect(px, py, z, z);
if (dn) {
vx.fillStyle = 'rgba(0,0,0,0.6)';
vx.fillRect(px, py, z, z);
}
if (z >= 9) {
var light = c[0] + c[1] + c[2] > 380 && !dn;
if (dn) {
vx.strokeStyle = '#fff';
vx.lineWidth = Math.max(1.5, z / 9);
vx.beginPath();
vx.moveTo(px + z * 0.28, py + z * 0.52);
vx.lineTo(px + z * 0.45, py + z * 0.7);
vx.lineTo(px + z * 0.74, py + z * 0.32);
vx.stroke();
} else {
vx.font = 'bold ' + Math.floor(z * (n > 99 ? 0.36 : 0.46)) + 'px Arial';
vx.fillStyle = light ? '#000' : '#fff';
vx.fillText(String(n), px + z / 2, py + z / 2 + 1);
}
}
if (z >= 5) {
vx.strokeStyle = 'rgba(0,0,0,0.3)';
vx.lineWidth = 1;
vx.strokeRect(px + 0.5, py + 0.5, z - 1, z - 1);
}
}
}
}
}
// In Manual Mode when a colour is chosen: render ONLY that colour & in-between blocks
if (mode == 'man' && visColor > 0) {
var col = RGB[visColor];
var isWarm = col[0] > 120 && col[1] < 140;
var gColor = isWarm ? '#00d2ff' : '#ff9f1c';
var gFill = isWarm ? 'rgba(0, 210, 255, 0.45)' : 'rgba(255, 159, 28, 0.45)';
var rInfo = getBetweenInfo(visPts);
// Render the numbered in-between blocks
if ($('dist').checked) {
vx.textAlign = 'center';
vx.textBaseline = 'middle';
for (var k in rInfo.between) {
var bp = rInfo.between[k];
if (art.c[k] === visColor) continue;
var bpx = ox + bp.x * z, bpy = oy + bp.y * z;
vx.fillStyle = gFill;
vx.fillRect(bpx, bpy, z, z);
vx.strokeStyle = gColor;
vx.lineWidth = 1;
vx.strokeRect(bpx + 0.5, bpy + 0.5, z - 1, z - 1);
if (z >= 9) {
vx.font = 'bold ' + Math.max(9, Math.floor(z * 0.45)) + 'px Arial';
vx.fillStyle = '#ffffff';
vx.fillText(String(bp.num), bpx + z / 2, bpy + z / 2 + 1);
} else if (z >= 3) {
vx.fillStyle = gColor;
vx.fillRect(bpx + z / 2 - 1, bpy + z / 2 - 1, 2, 2);
}
}
}
// Render ONLY the selected colour blocks
vx.textAlign = 'center';
vx.textBaseline = 'middle';
visPts.forEach(function(p) {
var px = ox + p.x * z, py = oy + p.y * z;
var dn = marks[p.i];
vx.fillStyle = 'rgb(' + col[0] + ',' + col[1] + ',' + col[2] + ')';
vx.fillRect(px, py, z, z);
if (dn) {
vx.fillStyle = 'rgba(0,0,0,0.6)';
vx.fillRect(px, py, z, z);
vx.strokeStyle = '#fff';
vx.lineWidth = Math.max(1.5, z / 9);
vx.beginPath();
vx.moveTo(px + z * 0.28, py + z * 0.52);
vx.lineTo(px + z * 0.45, py + z * 0.7);
vx.lineTo(px + z * 0.74, py + z * 0.32);
vx.stroke();
} else if (z >= 9) {
var light = col[0] + col[1] + col[2] > 380;
vx.font = 'bold ' + Math.floor(z * (visColor > 99 ? 0.36 : 0.46)) + 'px Arial';
vx.fillStyle = light ? '#000' : '#fff';
vx.fillText(String(visColor), px + z / 2, py + z / 2 + 1);
}
vx.strokeStyle = '#ffffff';
vx.lineWidth = Math.max(1, z > 8 ? 2 : 1);
vx.strokeRect(px + 0.5, py + 0.5, z - 1, z - 1);
});
}
if (shape && mode == 'paint') {
vx.fillStyle = sel ? ('rgba(' + RGB[sel] + ',0.85)') : 'rgba(255,255,255,0.45)';
shapeCells(shape.t, shape.x0, shape.y0, shape.x1, shape.y1).forEach(function(i) {
var x = i % art.w,
y = (i - x) / art.w;
vx.fillRect(ox + x * z, oy + y * z, z, z)
})
}
var H = Math.ceil(art.h / 32) * 32,
W = Math.ceil(art.w / 32) * 32;
vx.strokeStyle = 'rgba(255,255,255,0.7)';
vx.lineWidth = 1;
vx.beginPath();
for (var x = 32; x < art.w; x += 32) {
vx.moveTo(Math.round(ox + x * z) + 0.5, oy);
vx.lineTo(Math.round(ox + x * z) + 0.5, oy + art.h * z)
}
for (var y = H - 32; y > 0; y -= 32) {
if (y >= art.h) continue;
vx.moveTo(ox, Math.round(oy + y * z) + 0.5);
vx.lineTo(ox + art.w * z, Math.round(oy + y * z) + 0.5)
}
vx.stroke();
if (mode == 'man') progress()
}
function progress() {
var t = 0,
d = 0;
for (var i = 0; i < art.c.length; i++)
if (art.c[i]) {
if (visColor > 0 && art.c[i] !== visColor) continue;
t++;
if (marks[i]) d++
}
$('prog').textContent = (visColor > 0 ? ('Colour ' + visColor + ': ') : '') + d + ' of ' + t + ' blocks done';
}
function drawOrig() {
ox_.clearRect(0, 0, oc.width, oc.height);
if (!srcCv) return;
var s = Math.min(oc.width / src.w, oc.height / src.h);
ox_.imageSmoothingEnabled = false;
ox_.drawImage(srcCv, (oc.width - src.w * s) / 2, (oc.height - src.h * s) / 2, src.w * s, src.h * s)
}
oc.addEventListener('click', function(e) {
if (!src) return;
var r = oc.getBoundingClientRect(),
k = oc.width / r.width,
s = Math.min(oc.width / src.w, oc.height / src.h),
x = Math.floor(((e.clientX - r.left) * k - (oc.width - src.w * s) / 2) / s),
y = Math.floor(((e.clientY - r.top) * k - (oc.height - src.h * s) / 2) / s);
if (x < 0 || y < 0 || x >= src.w || y >= src.h) return;
var o = (y * src.w + x) * 4;
bgc = [src.data[o], src.data[o + 1], src.data[o + 2]];
$('bgc').value = hex(bgc);
if ($('bg').checked) recompute()
});
$('bgc').addEventListener('input', function() {
var v = this.value;
bgc = [parseInt(v.substr(1, 2), 16), parseInt(v.substr(3, 2), 16), parseInt(v.substr(5, 2), 16)];
if ($('bg').checked) recompute()
});
function cellAt(e) {
var r = vc.getBoundingClientRect(),
k = vc.width / r.width,
x = Math.floor(((e.clientX - r.left) * k - ox) / z),
y = Math.floor(((e.clientY - r.top) * k - oy) / z);
return (art && x >= 0 && y >= 0 && x < art.w && y < art.h) ? {
x: x,
y: y,
i: y * art.w + x
} : null
}
function snap() {
return {
w: art.w,
h: art.h,
c: art.c.slice()
}
}
function pushUndo() {
undoS.push(snap());
if (undoS.length > 100) undoS.shift();
redoS = []
}
function restore(s) {
art = {
w: s.w,
h: s.h,
c: s.c.slice()
};
dirty = true;
updateVisSelector();
draw()
}
function doUndo() {
if (!undoS.length) return;
redoS.push(snap());
restore(undoS.pop())
}
function doRedo() {
if (!redoS.length) return;
undoS.push(snap());
restore(redoS.pop())
}
vc.addEventListener('contextmenu', function(e) {
e.preventDefault()
});
var tool = 'pencil',
shape = null,
marking = false,
markOn = true,
lastC = null;
function cellClamp(e) {
var r = vc.getBoundingClientRect(),
k = vc.width / r.width,
x = Math.floor(((e.clientX - r.left) * k - ox) / z),
y = Math.floor(((e.clientY - r.top) * k - oy) / z);
return {
x: clamp(x, 0, art.w - 1),
y: clamp(y, 0, art.h - 1)
}
}
function lineCells(x0, y0, x1, y1) {
var out = [],
dx = Math.abs(x1 - x0),
dy = -Math.abs(y1 - y0),
sx = x0 < x1 ? 1 : -1,
sy = y0 < y1 ? 1 : -1,
err = dx + dy;
for (;;) {
out.push([x0, y0]);
if (x0 == x1 && y0 == y1) break;
var e2 = 2 * err;
if (e2 >= dy) {
err += dy;
x0 += sx
}
if (e2 <= dx) {
err += dx;
y0 += sy
}
}
return out
}
function stampInto(x, y, out) {
var s = +$('bsize').value,
half = Math.floor((s - 1) / 2),
lim = (s - 1) * (s - 1) / 4 + 0.5;
for (var i = 0; i < s; i++)
for (var j = 0; j < s; j++) {
var dx = i - (s - 1) / 2,
dy = j - (s - 1) / 2;
if (dx * dx + dy * dy > lim) continue;
var X = x + i - half,
Y = y + j - half;
if (X >= 0 && Y >= 0 && X < art.w && Y < art.h) out.push(Y * art.w + X)
}
}
function shapeCells(t, x0, y0, x1, y1) {
var out = [],
fillIt = $('fillshape').checked;
if (t == 'line') {
lineCells(x0, y0, x1, y1).forEach(function(p) {
stampInto(p[0], p[1], out)
});
return out
}
var ax = Math.min(x0, x1),
bx = Math.max(x0, x1),
ay = Math.min(y0, y1),
by = Math.max(y0, y1);
if (t == 'rect') {
for (var y = ay; y <= by; y++)
for (var x = ax; x <= bx; x++)
if (fillIt || x == ax || x == bx || y == ay || y == by) out.push(y * art.w + x);
return out
}
var cx = (ax + bx) / 2,
cy = (ay + by) / 2,
rx = (bx - ax) / 2 + 0.5,
ry = (by - ay) / 2 + 0.5;
function inside(x, y, a, b) {
var u = (x - cx) / a,
v = (y - cy) / b;
return u * u + v * v <= 1
}
for (var y = ay; y <= by; y++)
for (var x = ax; x <= bx; x++) {
if (!inside(x, y, rx, ry)) continue;
if (!fillIt && rx > 1.5 && ry > 1.5 && inside(x, y, rx - 1, ry - 1)) continue;
out.push(y * art.w + x)
}
return out
}
function floodFill(x, y) {
var w = art.w,
h = art.h,
c = art.c,
t = c[y * w + x];
if (t == sel) return;
if ($('global').checked) {
for (var i = 0; i < c.length; i++)
if (c[i] == t) c[i] = sel
} else {
var st = [y * w + x];
c[st[0]] = sel;
while (st.length) {
var i = st.pop(),
px = i % w,
py = (i - px) / w;
if (px > 0 && c[i - 1] == t) {
c[i - 1] = sel;
st.push(i - 1)
}
if (px < w - 1 && c[i + 1] == t) {
c[i + 1] = sel;
st.push(i + 1)
}
if (py > 0 && c[i - w] == t) {
c[i - w] = sel;
st.push(i - w)
}
if (py < h - 1 && c[i + w] == t) {
c[i + w] = sel;
st.push(i + w)
}
}
}
dirty = true;
updateVisSelector();
draw()
}
function paintPoints(pts) {
var cells = [];
pts.forEach(function(p) {
stampInto(p[0], p[1], cells)
});
cells.forEach(function(i) {
art.c[i] = sel
});
dirty = true;
later()
}
function setMark(i) {
if (markOn) marks[i] = 1;
else delete marks[i]
}
vc.addEventListener('mousedown', function(e) {
if (!art) return;
if (e.button == 2) {
pan = {
x: e.clientX,
y: e.clientY
};
return
}
if (e.button != 0) return;
var c = cellAt(e);
if (!c) return;
if (mode == 'man') {
if (!art.c[c.i]) return;
if (visColor > 0 && art.c[c.i] !== visColor) return;
markOn = !marks[c.i];
marking = true;
lastC = c;
setMark(c.i);
draw()
} else if (mode == 'paint') {
if (tool == 'pick') {
selectC(art.c[c.i]);
return
}
pushUndo();
if (tool == 'fill') floodFill(c.x, c.y);
else if (tool == 'pencil') {
painting = true;
lastC = c;
paintPoints([
[c.x, c.y]
])
} else {
shape = {
t: tool,
x0: c.x,
y0: c.y,
x1: c.x,
y1: c.y
};
draw()
}
}
});
window.addEventListener('mouseup', function() {
pan = null;
marking = false;
if (painting) {
painting = false;
updateVisSelector();
}
if (shape) {
var sh = shape;
shape = null;
shapeCells(sh.t, sh.x0, sh.y0, sh.x1, sh.y1).forEach(function(i) {
art.c[i] = sel
});
dirty = true;
updateVisSelector();
draw()
}
});
window.addEventListener('mousemove', function(e) {
if (pan) {
var k = vc.width / vc.getBoundingClientRect().width;
ox += (e.clientX - pan.x) * k;
oy += (e.clientY - pan.y) * k;
pan = {
x: e.clientX,
y: e.clientY
};
draw()
} else if (art && (painting || marking || shape)) {
var c = cellClamp(e);
if (painting) {
paintPoints(lineCells(lastC.x, lastC.y, c.x, c.y));
lastC = c
} else if (marking) {
lineCells(lastC.x, lastC.y, c.x, c.y).forEach(function(p) {
var i = p[1] * art.w + p[0];
if (art.c[i] && (visColor === 0 || art.c[i] === visColor)) setMark(i)
});
lastC = c;
later()
} else if (shape) {
shape.x1 = c.x;
shape.y1 = c.y;
later()
}
}
});
vc.addEventListener('mousemove', function(e) {
var c = cellAt(e);
if (c) {
var n = art.c[c.i];
$('info').textContent = 'block ' + c.x + ', ' + c.y + (n ? ' - colour ' + n : ' - empty')
}
});
vc.addEventListener('wheel', function(e) {
e.preventDefault();
if (!art) return;
var r = vc.getBoundingClientRect(),
k = vc.width / r.width,
px = (e.clientX - r.left) * k,
py = (e.clientY - r.top) * k,
nz = clamp(z * (e.deltaY < 0 ? 1.2 : 1 / 1.2), 1, 80);
ox = px - (px - ox) * nz / z;
oy = py - (py - oy) * nz / z;
z = nz;
draw()
}, {
passive: false
});
window.addEventListener('keydown', function(e) {
if (mode != 'paint') return;
var tg = e.target.tagName;
if (tg == 'INPUT' || tg == 'SELECT' || tg == 'TEXTAREA') return;
if (e.ctrlKey || e.metaKey) {
var k = e.key.toLowerCase();
if (k == 'z') {
e.preventDefault();
e.shiftKey ? doRedo() : doUndo()
} else if (k == 'y') {
e.preventDefault();
doRedo()
}
return
}
var map = {
b: 'pencil',
l: 'line',
r: 'rect',
o: 'ellipse',
f: 'fill',
i: 'pick'
},
t = map[e.key.toLowerCase()];
if (t) setTool(t);
else if (e.key.toLowerCase() == 'e') selectC(0)
});
function setMode(m) {
mode = m;
if (m != 'man') visColor = 0;
$('s-edit').style.display = m == 'edit' ? '' : 'none';
$('s-man').style.display = m == 'man' ? '' : 'none';
$('s-paint').style.display = m == 'paint' ? '' : 'none';
$('ow').style.display = m == 'edit' ? '' : 'none';
$('bpaint').style.display = $('bman').style.display = m == 'edit' ? '' : 'none';
$('bedit').style.display = m == 'edit' ? 'none' : '';
vc.width = m == 'edit' ? 430 : 880;
vc.height = m == 'edit' ? 520 : 600;
if (m == 'man') updateVisSelector();
fit();
draw();
if (m == 'edit') drawOrig()
}
$('bpaint').onclick = function() {
ensureArt();
setMode('paint')
};
$('bman').onclick = function() {
if (!art) {
alert('Open an image first');
return
}
setMode('man')
};
$('bedit').onclick = function() {
setMode('edit')
};
$('clearm').onclick = function() {
marks = {};
draw()
};
var pal = $('pal');
nums.forEach(function(n) {
var d = document.createElement('div'),
c = RGB[n];
d.style.background = 'rgb(' + c + ')';
d.title = String(n);
d.onclick = function() {
selectC(n)
};
d.id = 'ap-p' + n;
pal.appendChild(d)
});
function selectC(n) {
var o = document.getElementById('ap-p' + sel);
if (o) o.className = '';
sel = n;
var d = document.getElementById('ap-p' + n);
if (d) d.className = 'sel';
$('selc').textContent = n ? 'colour ' + n : 'eraser'
}
var TOOLS = [
['pencil', 'Pencil', 'B'],
['line', 'Line', 'L'],
['rect', 'Rectangle', 'R'],
['ellipse', 'Ellipse', 'O'],
['fill', 'Fill', 'F'],
['pick', 'Picker', 'I']
];
function setTool(t) {
tool = t;
TOOLS.forEach(function(x) {
$('t-' + x[0]).className = x[0] == t ? 'on' : ''
})
}
TOOLS.forEach(function(x) {
var b = document.createElement('button');
b.id = 'ap-t-' + x[0];
b.textContent = x[1];
b.title = x[1] + ' (' + x[2] + ')';
b.onclick = function() {
setTool(x[0])
};
$('tools').appendChild(b)
});
setTool('pencil');
$('bsize').addEventListener('input', function() {
$('bs').textContent = this.value
});
selectC(1);
$('eraser').onclick = function() {
selectC(0)
};
$('undo').onclick = doUndo;
$('redo').onclick = doRedo;
$('clearc').onclick = function() {
ensureArt();
pushUndo();
art.c.fill(0);
dirty = true;
updateVisSelector();
draw()
};
$('resize').onclick = function() {
ensureArt();
var l = +$('pl').value || 0,
r = +$('pr').value || 0,
t = +$('pt').value || 0,
b = +$('pb').value || 0,
nw = art.w + l + r,
nh = art.h + t + b;
if (nw < 1 || nh < 1 || nw > 1024 || nh > 1024) {
alert('Size must be between 1 and 1024');
return
}
pushUndo();
var c = new Int32Array(nw * nh);
for (var y = 0; y < art.h; y++)
for (var x = 0; x < art.w; x++) {
var X = x + l,
Y = y + t;
if (X >= 0 && Y >= 0 && X < nw && Y < nh) c[Y * nw + X] = art.c[y * art.w + x]
}
art = {
w: nw,
h: nh,
c: c
};
['pl', 'pr', 'pt', 'pb'].forEach(function(k) {
$(k).value = 0
});
dirty = true;
updateVisSelector();
fit();
draw()
};
$('new').onclick = function() {
var w = clamp(+$('nw').value || 1, 1, 1024),
h = clamp(+$('nh').value || 1, 1, 1024);
if (art) pushUndo();
art = {
w: w,
h: h,
c: new Int32Array(w * h)
};
dirty = true;
updateVisSelector();
fit();
draw()
};
['width', 'bg', 'tol', 'light', 'grid', 'dither', 'dstr'].forEach(function(k) {
$(k).addEventListener('change', recompute)
});
$('wr').addEventListener('input', function() {
$('width').value = this.value
});
$('wr').addEventListener('change', recompute);
$('maxc').addEventListener('input', function() {
$('maxcr').value = this.value
});
$('maxcr').addEventListener('input', function() {
$('maxc').value = this.value
});
['maxc', 'maxcr'].forEach(function(k) {
$(k).addEventListener('change', recompute)
});
$('visc').addEventListener('change', function() {
visColor = +this.value;
updateVisInfo();
draw()
});
$('dist').addEventListener('change', function() {
draw()
});
$('file').addEventListener('change', function() {
var f = this.files[0];
if (!f) return;
var im = new Image();
im.onload = function() {
var c = document.createElement('canvas');
c.width = im.naturalWidth;
c.height = im.naturalHeight;
var x = c.getContext('2d');
x.drawImage(im, 0, 0);
var d = x.getImageData(0, 0, c.width, c.height);
$('fname').textContent = f.name + ' (' + c.width + 'x' + c.height + ')';
setSrc({
w: c.width,
h: c.height,
data: d.data
});
if (mode != 'edit') setMode('edit')
};
im.src = URL.createObjectURL(f)
});
var RUN = [
[512, 253],
[256, 252],
[128, 251],
[64, 250],
[32, 249],
[16, 248],
[8, 247],
[4, 246],
[3, 245]
];
function encLayer(cells) {
var d = [],
i = 0;
while (i < cells.length) {
var v = cells[i],
j = i;
while (j < cells.length && cells[j] == v) j++;
var n = j - i;
RUN.forEach(function(r) {
while (n >= r[0]) {
d.push(r[1], v);
n -= r[0]
}
});
while (n-- > 0) d.push(v);
i = j
}
return d
}
function push(a, b) {
for (var i = 0; i < b.length; i++) a.push(b[i])
}
function paintSection(cx, cy, layers) {
var s = [0x53, 0, 0],
v = (2 * cx + 1) * 1048576 + cy,
b = [];
for (var i = 0; i < 5; i++) {
b.unshift(v % 256);
v = Math.floor(v / 256)
}
s = s.concat(b, [0, 0]);
var enc = [];
for (var z = 0; z < 32; z++) enc.push(layers[z] && layers[z].some(function(x) {
return x
}) ? encLayer(layers[z]) : []);
enc.forEach(function(e) {
s.push(e.length >> 8, e.length & 255)
});
enc.forEach(function(e) {
push(s, e)
});
s.push(0);
return s
}
function fullLayer(id) {
var r = [0, 4];
for (var i = 0; i < 4; i++) r.push(0, 255, id[0], id[1]);
return r
}
function buildLayer(ids) {
var seg = [],
prev = null;
ids.forEach(function(id) {
var k = id[0] + ',' + id[1];
if (k != prev || seg[seg.length - 3] == 255) {
seg.push(0, 0, id[0], id[1]);
prev = k
} else seg[seg.length - 3]++
});
return [seg.length >> 10, (seg.length / 4) % 256].concat(seg)
}
function hex5(n) {
var s = n.toString(16);
while (s.length < 5) s = '0' + s;
return s
}
function writeClaims(a, block, orig) {
var w = a.w,
h = a.h,
cw = Math.ceil(w / 32),
ch = Math.ceil(h / 32),
W = cw * 32,
H = ch * 32,
E = [0, 1],
files = [],
pc = new Int32Array(W * H),
name = new Array(64).fill(0);
for (var y = 0; y < h; y++)
for (var x = 0; x < w; x++) pc[(H - 1 - y) * W + x] = a.c[y * w + x];
for (var cy = 0; cy < ch; cy++)
for (var cx = 0; cx < cw; cx++) {
var cells = [],
solid = [];
for (var y = 0; y < 32; y++)
for (var x = 0; x < 32; x++) {
var v = pc[(cy * 32 + y) * W + cx * 32 + x];
cells.push(v);
solid.push(v ? 1 : 0)
}
var bl = solid.every(function(s) {
return s
}) ? fullLayer(block) : buildLayer(solid.map(function(s) {
return s ? block : E
}));
var lo = [],
up = [];
for (var z = 0; z < 32; z++) {
push(lo, z == 2 ? bl : fullLayer(E));
push(up, fullLayer(E))
}
push(lo, [0, 3, 0, 0, 0, 0]);
push(lo, name);
push(lo, paintSection(orig[0] + cx, orig[1] + cy, {
2: cells
}));
push(up, [0, 2, 0, 0, 0, 0]);
push(up, name);
var hx = hex5(orig[0] + cx) + hex5(orig[1] + cy);
files.push([hx + '0.chunk', lo]);
files.push([hx + '1.chunk', up])
}
return files
}
var T = [];
for (var n = 0; n < 256; n++) {
var c = n;
for (var k = 0; k < 8; k++) c = c & 1 ? 0xEDB88320 ^ (c >>> 1) : c >>> 1;
T[n] = c >>> 0
}
function crc(d) {
var c = 0xFFFFFFFF;
for (var i = 0; i < d.length; i++) c = T[(c ^ d[i]) & 255] ^ (c >>> 8);
return (c ^ 0xFFFFFFFF) >>> 0
}
function u16(v) {
return [v & 255, (v >> 8) & 255]
}
function u32(v) {
return [v & 255, (v >> 8) & 255, (v >> 16) & 255, (v >>> 24) & 255]
}
function zip(files) {
var out = [],
cen = [],
off = 0;
files.forEach(function(f) {
var nm = [];
for (var i = 0; i < f[0].length; i++) nm.push(f[0].charCodeAt(i));
var cr = crc(f[1]),
L = f[1].length,
h = [].concat(u32(0x04034b50), u16(20), u16(0), u16(0), u16(0), u16(0x21), u32(cr), u32(L), u32(L), u16(nm.length), u16(0), nm);
out.push(new Uint8Array(h), Uint8Array.from(f[1]));
cen.push(new Uint8Array([].concat(u32(0x02014b50), u16(20), u16(20), u16(0), u16(0), u16(0), u16(0x21), u32(cr), u32(L), u32(L), u16(nm.length), u16(0), u16(0), u16(0), u16(0), u32(0), u32(off), nm)));
off += h.length + L
});
var cs = 0;
cen.forEach(function(c) {
out.push(c);
cs += c.length
});
out.push(new Uint8Array([].concat(u32(0x06054b50), u16(0), u16(0), u16(files.length), u16(files.length), u32(cs), u32(off), u16(0))));
return new Blob(out, {
type: 'application/zip'
})
}
$('put').onclick = function() {
if (!art) {
alert('Open an image first');
return
}
var nm = $('block').value.toLowerCase(),
b = BLOCKS.filter(function(x) {
return x[0].toLowerCase() == nm
})[0];
if (!b) {
alert('Unknown block, pick one from the list');
return
}
if (Math.ceil(art.w / 32) > 32 || Math.ceil(art.h / 32) > 32) {
alert('Too big (limit 32 x 32 claims), use a smaller width');
return
}
var files = writeClaims(art, [b[1], b[2]], [+$('ox').value, +$('oy').value]),
a = document.createElement('a');
a.href = URL.createObjectURL(zip(files));
a.download = 'angeldust_claims.zip';
document.body.appendChild(a);
a.click();
a.remove();
$('info').textContent = 'Downloaded ' + files.length + ' files. Unzip them into the game folder and restart Angeldust.'
};
if (window.__ADTEST) window.__ADTEST = {
getArt: function() {
return art
},
getMarks: function() {
return marks
},
writeClaims: writeClaims,
detectGrid: detectGrid,
quant: quant,
downscale: downscale
};
})();