Angedust Stuff
  • HOME
  • MODS ▼
    Texture Packs Resource Packs
  • SCRIPTS
  • GUIDES

Obi's tool for creating pixel art in Angeldust

ai script tool
(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
  };
})();
← Older Posts

Tags

ai guide mod resource_pack script some_ai tool

Archive

▼ 2026 (4)
▼ November (2)
► May (1)
► January (1)
► 2025 (2)
► September (1)
► May (1)