0
0

画像の減色・誤差拡散・タイリング処理

Posted at

昔の貧弱な表示装置を思い出しつつ、24ビットカラーを様々な形式に変換するものを作ってみました。(画像ビュワー作成も目的の1つです)

スクリーン ショットは元画像を基本8色(黒、青、赤、緑、紫、水色、黄色、白)の誤差拡散法で処理したものです。

Sample.jpg

See the Pen 低質画像生成 by Ikiuo (@ikiuo) on CodePen.

ソースコード

HTML+JavaScript (長いので折りたたみ)
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>低質画像生成</title>
    <style>
     html {
         background-color: #3f5f7f;
     }

     .bggray {
         background-color: lightgray;
     }
     .bgwhite {
         background-color: white;
     }

     .absolute {
         position: absolute;
     }
     .relative {
         position: relative;
         left: 0; top: 0;
     }
     .center {
         text-align: center;
     }
     .right {
         text-align: right;
     }
     .vtop {
         vertical-align: top;
     }
     .small {
         font-size: small;
     }
     .xsmall {
         font-size: x-small;
     }
     .xxsmall {
         font-size: xx-small;
     }

     .noframe {
         padding: 0;
         border-width: 0;
         border-collaspe: collaspe;
         border-spacing: 0;
         margin: 0;
     }
     .menuframe {
         background-color: white;
         left: 0; top: 0;
         z-index: 999;
     }
     .menuheader {
         cursor: default;
         font-wight: bold;
         font-size: small;
     }
     .menuitem {
         border-width: 0;
         cursor: default;
         font-size: small;
     }
     .menuitem:hover {
         background-color: #dfefff;
     }

     .altcolor {
         display: default;
     }

     .screen {
         padding: 0px;
         border: solid 1px black;
         /* border-width: 0px; */
         margin: 0px;
     }
    </style>
  </head>
  <body class="noframe">

    <table class="absolute noframe menuframe">
      <tr>
        <td class="noframe">
          <table id="tagMenu" class="noframe menuframe" width="100%" hidden>
            <tr><td id="menuRendering" class="menuitem">描画形式</td></tr>
            <tr><td id="menuRenderingAuto" class="menuitem">&nbsp;&nbsp;auto</td></tr>
            <tr><td id="menuRenderingSmooth" class="menuitem" hidden>&nbsp;&nbsp;smooth</td></tr>
            <tr><td id="menuRenderingHighQuality" class="menuitem" hidden>&nbsp;&nbsp;high-quality</td></tr>
            <tr><td id="menuRenderingCrispEdges" class="menuitem" hidden>&nbsp;&nbsp;crisp-edges</td></tr>
            <tr><td id="menuRenderingPixelated" class="menuitem">&nbsp;&nbsp;pixelated</td></tr>

            <tr><td><hr></td></tr>

            <tr><td id="menuOriginal" class="menuitem">元画像</td></tr>

            <tr><td><table>
              <tr>
                <td class="menuheader right">色成分</td>
                <td id="menuColorRed" class="menuitem"></td>
                <td id="menuColorGreen" class="menuitem"></td>
                <td id="menuColorBlue" class="menuitem"></td>
              </tr>
              <tr>
                <td class="xsmall center" colspan="5">- 固定色変換 -</td>
              </tr>
              <tr>
                <td class="menuheader right">65536色</td>
                <td id="menuReduceColor65536" class="menuitem"></td>
                <td id="menuErrDiffColor65536F" class="menuitem"></td>
                <td id="menuTileColor65536F" class="menuitem"></td>
              </tr>
              <tr>
                <td class="menuheader right">32768色</td>
                <td id="menuReduceColor32768" class="menuitem"></td>
                <td id="menuErrDiffColor32768F" class="menuitem"></td>
                <td id="menuTileColor32768F" class="menuitem"></td>
              </tr>
              <tr>
                <td class="menuheader right">4096色</td>
                <td id="menuReduceColor4096" class="menuitem"></td>
                <td id="menuErrDiffColor4096F" class="menuitem"></td>
                <td id="menuTileColor4096F" class="menuitem"></td>
              </tr>
              <tr>
                <td class="menuheader right">512色</td>
                <td id="menuReduceColor512" class="menuitem"></td>
                <td id="menuErrDiffColor512F" class="menuitem"></td>
                <td id="menuTileColor512F" class="menuitem"></td>
              </tr>
              <tr>
                <td class="menuheader right">256色</td>
                <td id="menuReduceColor256" class="menuitem"></td>
                <td id="menuErrDiffColor256F" class="menuitem"></td>
                <td id="menuTileColor256F" class="menuitem"></td>
              </tr>
              <tr class="altcolor">
                <td class="menuheader right">216色</td>
                <td id="menuReduceColor216" class="menuitem"></td>
                <td id="menuErrDiffColor216F" class="menuitem"></td>
                <td id="menuTileColor216F" class="menuitem"></td>
              </tr>
              <tr class="altcolor">
                <td class="menuheader right">125色</td>
                <td id="menuReduceColor125" class="menuitem"></td>
                <td id="menuErrDiffColor125F" class="menuitem"></td>
                <td id="menuTileColor125F" class="menuitem"></td>
              </tr>
              <tr>
                <td class="menuheader right">64色</td>
                <td id="menuReduceColor64" class="menuitem"></td>
                <td id="menuErrDiffColor64F" class="menuitem"></td>
                <td id="menuTileColor64F" class="menuitem"></td>
              </tr>
              <tr class="altcolor">
                <td class="menuheader right">27色</td>
                <td id="menuReduceColor27" class="menuitem"></td>
                <td id="menuErrDiffColor27F" class="menuitem"></td>
                <td id="menuTileColor27F" class="menuitem"></td>
              </tr>
              <tr>
                <td class="menuheader right">16色</td>
                <td id="menuReduceColor16" class="menuitem"></td>
                <td id="menuErrDiffColor16F" class="menuitem"></td>
                <td id="menuTileColor16F" class="menuitem"></td>
              </tr>
              <tr>
                <td class="menuheader right">8色</td>
                <td id="menuReduceColor8" class="menuitem"></td>
                <td id="menuErrDiffColor8" class="menuitem"></td>
                <td id="menuTileColor8F" class="menuitem"></td>
              </tr>

              <tr>
                <td class="menuheader right">256階調</td>
                <td id="menuGrayScale256" class="menuitem"></td>
                <!-- <td id="menuErrDiffGrayScale256" class="menuitem">散</td> -->
              </tr>
              <tr>
                <td class="menuheader right">64階調</td>
                <td id="menuGrayScale64" class="menuitem"></td>
                <td id="menuErrDiffGrayScale64" class="menuitem"></td>
                <td id="menuTileGrayScale64" class="menuitem"></td>
              </tr>
              <tr>
                <td class="menuheader right">16階調</td>
                <td id="menuGrayScale16" class="menuitem"></td>
                <td id="menuErrDiffGrayScale16" class="menuitem"></td>
                <td id="menuTileGrayScale16" class="menuitem"></td>
              </tr>
              <tr>
                <td class="menuheader right">8階調</td>
                <td id="menuGrayScale8" class="menuitem"></td>
                <td id="menuErrDiffGrayScale8" class="menuitem"></td>
                <td id="menuTileGrayScale8" class="menuitem"></td>
              </tr>
              <tr>
                <td class="menuheader right">4階調</td>
                <td id="menuGrayScale4" class="menuitem"></td>
                <td id="menuErrDiffGrayScale4" class="menuitem"></td>
                <td id="menuTileGrayScale4" class="menuitem"></td>
              </tr>
              <tr>
                <td class="menuheader right">白黒2色</td>
                <td id="menuColorWB" class="menuitem"></td>
                <td id="menuErrDiffColorWB" class="menuitem"></td>
                <td id="menuTileColorWB" class="menuitem"></td>
              </tr>

            </table></td></tr>

            <tr id="setGIF1" hidden><td><hr></td></tr>

            <tr id="setGIF2" hidden><td>
              <input id="menuCreateGIF" name="menuCreateGIF" type="checkbox">
              <label for="menuCreateGIF" class="small">GIF生成を抑制</label>
            </td></tr>

            <tr><td><hr></td></tr>

            <tr>
              <td class="small">
                <details><summary>キー操作</summary>
                  <div>
                    &nbsp;&nbsp;「9」: ウィンドウ合わせ<br>
                    &nbsp;&nbsp;「0」: 等倍<br>
                    &nbsp;&nbsp;「-」: 縮小(半分)<br>
                    &nbsp;&nbsp;「+」: 拡大(2倍)<br>
                    スクロール<br>
                    &nbsp;&nbsp;CTRL+マウス<br>
                    ズーム<br>
                    &nbsp;&nbsp;CTRL+ホイール<br>
                  </div>
                </details>
                <details>
                  <summary>説明</summary>
                  <div>
                    変換種類<br>
                    &nbsp;&nbsp;単:単純減色<br>
                    &nbsp;&nbsp;散:誤差拡散<br>
                    &nbsp;&nbsp;柄:市松模様<br>
                  </div>
                </details>
              </td>
            </tr>
          </table>
        </td>
      </tr>
    </table>

    <table class="absolute bgwhite noframe">
      <tr><td id="tagScreen" class="noframe bggray"></td></tr>
    </table>

    <script>

     /* ********************************** */

     /*
      * GIF 形式データを生成するクラス
      */

     class MyGIF {
         static signature = [0x47, 0x49, 0x46];
         static version87a = [0x38, 0x37, 0x61];
         static version89a = [0x38, 0x39, 0x61];

         // コンストラクタ.
         constructor(width, height, resolution, background, global_color, aspect) {
             const S = MyGIF;
             this.S = S;
             this.signature = S.signature;
             this.version = S.version89a;
             this.logicalScreen = S.createLogicalScreenDescriptor(
                 width, height, resolution, background, global_color, aspect);
             this.descriptor = [];
         }

         // GIF形式のバイナリを取得する.
         get_binary() {
             return [
                 this.signature,
                 this.version,
                 this.logicalScreen.get_binary(),
                 this.descriptor.map((d, _) => d.get_binary()).flat(),
             ].flat();
         }

         // 任意の Descriptor を追加する.
         appendDescriptor(descriptor) {
             this.descriptor.push(descriptor);
         }

         // Image Descriptor を追加する.
         appendImageDescriptor(x, y, w, h, data, color, interlace) {
             this.descriptor.push(this.S.createImageDescriptor(x, y, w, h, data, color, interlace));
         }

         // Graphic Control Extension を追加する.
         appendGraphicControlExtension(delay, method, transparent, input) {
             this.descriptor.push(this.S.createGraphicControlExtension(delay, method, transparent, input));
         }

         // Comment Extension を追加する.
         appendCommentExtension(text) {
             this.descriptor.push(this.S.createCommentExtension(text));
         }

         // Plain Text Extension を追加する.
         appendPlainTextExtension(x, y, w, h, cw, ch, fc, bc, text) {
             this.descriptor.push(this.S.createPlainTextExtension(x, y, w, h, cw, ch, fc, bc, text));
         }

         // Application Extension を追加する.
         appendApplicationExtension(id, code, data) {
             this.descriptor.push(this.S.createApplicationExtension(id, code, data));
         }

         // アニメーションのループ回数を設定する.
         appendApplicationExtensionForLoop(count) {
             this.appendApplicationExtension('NETSCAPE', '2.0', [1, (count & 0xff), ((count >> 8) & 0xff)]);
         }

         // Trailer を追加する.
         appendTrailer() {
             this.descriptor.push(this.S.createTrailer());
         }

         // データ URL を返す.
         toDataURL() {
             const b64s = this.S.encodeBase64(this.get_binary())
             return 'data:image/gif;charset=utf-8;base64,' + b64s;
         }

         // 8 ビット形式の取得.
         static uint8el(x) {
             return ((!x ? 0 : x) & 0xff);
         }

         // 16 ビット・リトル・エンディアン形式の取得.
         static uint16el(x) {
             const y = (!x ? 0 : x);
             return [(y & 0xff), ((y >> 8) & 0xff)];
         }

         // 色情報の過不足を調整.
         static fixColorTable(depth, color) {
             const colen = (1 << (depth + 1)) * 3;
             let table = color.flat();
             return ((table.length > colen) ? table.slice(0, colen) :
                     table.concat(Array(colen - table.length).fill(0)));
         }

         // 文字の UTF-8 バイナリ化.
         static charToUTF8(c) {
             if (c < 0x0080) return c;
             if (c < 0x0800) return [
                 (0xc0 | (c >> 6)),
                 (0x80 | (c & 0x3f)),
             ];
             return [
                 (0xe0 | (c >> 12)),
                 (0x80 | ((c >> 6) & 0x3f)),
                 (0x80 | (c & 0x3f)),
             ];
         }
         static stringToUTF8(s) {
             return [...Array(s.length)].map((_, i) => MyGIF.charToUTF8(s.charCodeAt(i))).flat();
         }

         // Data Sub-blocks の生成.
         static createDataSubBlocks(data) {
             let block = [];
             let pos = 0;
             let length = data.length;
             while (length) {
                 let slen = (length < 256) ? length : 255;
                 block.push(slen);
                 block.push(data.slice(pos, pos + slen));
                 pos += slen;
                 length -= slen;
             }
             block.push(0);
             return block.flat();
         }

         // Logical Screen Descriptor の生成.
         static createLogicalScreenDescriptor(width, height, resolution, background, color, sort, aspect) {
             const S = MyGIF;
             const D = {
                 logical_screen_width: width,
                 logical_screen_height: height,

                 size_of_global_color_table: 0,
                 sort_flag: sort,
                 color_resolution: resolution,
                 global_color_table_flag: false,

                 background_color_index: background,
                 pixel_aspect_ratio: aspect ?? 49,

                 global_color_table: null,

                 set_color: (function(table) {
                     const flag = (table != null);
                     D.size_of_global_color_table =
                         (!flag ? 0 : (31 - Math.clz32(table.length-1)));
                     D.global_color_table_flag = flag;
                     D.global_color_table = table;
                 }),

                 get_binary: (function() {
                     const sgct = (!D.size_of_global_color_table ? 0 : (7 & D.size_of_global_color_table));
                     const sort = (!D.sort_flag ? 0 : 1);
                     const res = (!D.color_resolution ? 0 : (7 & D.color_resolution));
                     const gcf = (!D.global_color_table_flag ? 0 : 1);
                     const bgc = (!D.background_color_index ? 0 : D.background_color_index);
                     const aspect = ((D.pixel_aspect_ratio == null) ? 49 : D.pixel_aspect_ratio);
                     const color = (!gcf ? [] : S.fixColorTable(sgct, D.global_color_table));
                     return [
                         S.uint16el(D.logical_screen_width),
                         S.uint16el(D.logical_screen_height),
                         (sgct | (sort << 3) | (res << 4) | (gcf << 7)),
                         S.uint8el(bgc),
                         S.uint8el(aspect),
                         color.map((v, _) => S.uint8el(v)),
                     ].flat();
                 }),
             }
             D.set_color(color);
             return D;
         }

         // Image Descriptor の生成.
         static createImageDescriptor(x, y, w, h, data, color, interlace, sort) {
             const S = MyGIF;
             const D = {
                 descriptor: 0x2c,

                 image_left_position: x,
                 image_top_position: y,
                 image_width: w,
                 image_height: h,

                 size_of_local_color_table: 0,
                 sort_flag: sort,
                 interlace_flag: interlace,
                 local_color_table_flag: false,

                 local_color_table: null,
                 table_based_image_data: S.createTableBasedImageData(data),

                 set_color_table: (function(table) {
                     const flag = (table != null);
                     D.size_of_local_color_table =
                         (!flag ? 0 : (31 - Math.clz32(table.length-1)));
                     D.local_color_table_flag = flag;
                     D.local_color_table = table;
                 }),

                 get_binary: (function() {
                     const slct = (!D.size_of_local_color_table ? 0 : (7 & D.size_of_local_color_table));
                     const sort = (!D.sort_flag ? 0 : 1);
                     const interlace = (!D.interlace_flag ? 0 : 1);
                     const lcf = (!D.local_color_table_flag ? 0 : 1);
                     const color = (!lcf ? [] : S.fixColorTable(slct, D.local_color_table));
                     return [
                         D.descriptor,
                         S.uint16el(D.image_left_position),
                         S.uint16el(D.image_top_position),
                         S.uint16el(D.image_width),
                         S.uint16el(D.image_height),
                         (slct | (sort << 5) | (interlace << 6) | (lcf << 7)),
                         color.map((v, _) => S.uint8el(v)),
                         D.table_based_image_data.lzw_minimum_code_size,
                         D.table_based_image_data.data,
                     ].flat();
                 }),
             }
             D.set_color_table(color);
             return D;
         }

         // Table Based Image の生成 (LZW圧縮)
         static createTableBasedImageData(source) {
             const S = MyGIF;
             const source_size = source.length;
             // const src_max = Math.max.apply(null, source);  // 'Maximum call stack size exceeded' がでる.
             const src_max = function() { let m = 0; for (const d of source) if (m < d) m = d; return m; }();
             const src_max_clz = (32 - Math.clz32(src_max));

             const bits_init = ((src_max_clz < 2) ? 2 : src_max_clz);
             let bits_curr = (bits_init + 1);

             const code_base = (1 << bits_init);
             const code_clear = code_base;
             const code_end = code_base + 1;
             const code_max = ((1 << 12) - 1);

             let code_curr = code_end;
             let code_step = (1 << bits_curr);

             const tree = [...Array(code_max + 2)].map((_, i) => ({ code: i, next: -1, down: -1, data: 0 }));

             const buffer = [];
             let bs_pos = 0;

             const write = function(data) {
                 const bits = bits_curr;
                 let idxs = (bs_pos >> 3);
                 const idxe = ((bs_pos + bits - 1) >> 3);
                 data <<= (bs_pos & 7);
                 if (idxs < buffer.length) {
                     buffer[idxs] |= (data & 0xff);
                     data >>= 8;
                     idxs++;
                 }
                 while (idxs <= idxe) {
                     buffer.push(data & 0xff);
                     data >>= 8;
                     idxs++;
                 }
                 bs_pos += bits;
             }

             write(code_clear);
             if (!source_size) {
                 write(code_end);
                 return {
                     lzw_minimum_code_size: bits_init,
                     data: S.createDataSubBlocks(buffer),
                 }
             }

             let siter = source.values();
             let sdat = siter.next();
             lzw: for (;;) {
                 let data, next;
                 let node = tree[(data = sdat.value)];
                 scan: for (;;) {
                     if ((sdat = siter.next()).done) {
                         write(node.code);
                         break lzw;
                     }
                     data = sdat.value;
                     if ((next = tree[node.down]) == null)
                         break;
                     while (data != next.data)
                         if ((next = tree[next.next]) == null)
                             break scan;
                     node = next;
                 }

                 write(node.code);
                 {
                     const next = tree[++code_curr];
                     // next.code = code_curr;
                     next.next = node.down;
                     next.down = -1;
                     next.data = data;
                     node.down = code_curr;
                 }
                 if (code_curr < code_step)
                     continue;
                 if (code_curr < code_max) {
                     bits_curr++;
                     if ((code_step <<= 1) < code_max)
                         continue;
                     code_step = code_max;
                     continue;
                 }
                 write(code_clear);

                 bits_curr = bits_init + 1;
                 code_step = (1 << bits_curr);
                 code_curr = code_end;
                 for (let i = 0; i < code_base; i++) {
                     const node = tree[i];
                     // node.code = i;
                     node.next = -1;
                     node.down = -1;
                     // node.data = 0;
                 }
             }
             write(code_end);

             return {
                 lzw_minimum_code_size: bits_init,
                 data: S.createDataSubBlocks(buffer),
             }
         }

         // Graphic Control Extension の生成.
         static createGraphicControlExtension(delay, method, transparent, input) {
             const S = MyGIF;
             const D = {
                 descriptor: 0x21,
                 label: 0xf9,

                 disposal_method: method,
                 user_input_flag: input,
                 transparent_color_flag: false,

                 delay_time: delay,
                 transparent_color_index: 0,

                 set_transparent: (function(index) {
                     D.transparent_color_flag = (index != null);
                     D.transparent_color_index = (!index ? 0 : index);
                 }),

                 get_binary: (function() {
                     const method = (!D.disposal_method ? 0 : (7 & D.disposal_method));
                     const input = (!D.user_input_flag ? 0 : 1);
                     const ftrans = (!D.transparent_color_flag ? 0 : 1);
                     return [
                         D.descriptor,
                         D.label,
                         4, /* Data Sub-blocks */
                         ((method << 3) | (input << 6) | (ftrans << 7)),
                         S.uint16el(D.delay_time),
                         S.uint8el(D.transparent_color_index),
                         0, /* Data Sub-blocks */
                     ].flat();
                 }),
             }
             D.set_transparent(transparent);
             return D;
         }

         // Comment Extension の生成.
         static createCommentExtension(text) {
             const S = MyGIF;
             const D = {
                 descriptor: 0x21,
                 label: 0xfe,

                 text: text,

                 get_binary: (() => [
                     D.descriptor,
                     D.label,
                     S.createDataSubBlocks(S.stringToUTF8(D.tex)),
                 ].flat()),
             }
             return D;
         }

         // Plain Text Extension の生成.
         static createPlainTextExtension(x, y, w, h, cw, ch, fc, bc, text) {
             const S = MyGIF;
             const D = {
                 descriptor: 0x21,
                 label: 0x01,

                 text_grid_left_position: x,
                 text_grid_top_position: y,
                 text_grid_width: w,
                 text_grid_height: h,
                 character_cell_width: cw,
                 character_cell_height: ch,
                 text_foreground_color_index: fc,
                 text_background_color_index: bc,
                 plain_text_data: text,

                 get_binary: (() => [
                     D.descriptor,
                     D.label,
                     12, /* Data Sub-blocks */
                     S.uint16el(D.text_grid_left_position),
                     S.uint16el(D.text_grid_top_position),
                     S.uint16el(D.text_grid_width),
                     S.uint16el(D.text_grid_height),
                     S.uint8el(D.character_cell_width),
                     S.uint8el(D.character_cell_height),
                     S.uint8el(D.text_foreground_color_index),
                     S.uint8el(D.text_background_color_index),
                     S.createDataSubBlocks(S.stringToUTF8(D.plain_text_data)),
                 ].flat()),
             }
             return D;
         }

         // Application Extension の生成.
         static createApplicationExtension(id, code, data) {
             const S = MyGIF;
             const D = {
                 descriptor: 0x21,
                 label: 0xff,

                 application_identifier: id,
                 application_authentication_code: code,
                 application_data: data,

                 get_binary: (() => [
                     D.descriptor,
                     D.label,
                     11, /* Data Sub-blocks */
                     S.stringToUTF8(D.application_identifier + '        ').slice(0, 8),
                     S.stringToUTF8(D.application_authentication_code + '   ').slice(0, 3),
                     S.createDataSubBlocks(D.application_data.map((v, _) => S.uint8el(v))),
                 ].flat()),
             }
             return D;
         }

         // Trailer の生成.
         static createTrailer() {
             return {
                 descriptor: 0x3b,
                 get_binary: (() => 0x3b),
             }
         }

         // バイナリの Base 64 符号化.
         static encodeBase64(b) {
             const table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
             const blen = b.length;
             const brem = blen % 3;
             const bcnt = blen - brem;
             let s = '';
             let i = 0;
             while (i < bcnt) {
                 const d0 = b[i++];
                 const d1 = b[i++];
                 const d2 = b[i++];
                 const d = (d0 << 16) | (d1 << 8) | d2;
                 s += table[(d >> 18) & 0x3f];
                 s += table[(d >> 12) & 0x3f];
                 s += table[(d >>  6) & 0x3f];
                 s += table[d & 0x3f];
             }
             if (brem) {
                 const b2 = (brem == 2);
                 const d0 = b[i++];
                 const d1 = (b2 ? b[i++] : 0);
                 const d = (d0 << 16) | (d1 << 8);
                 s += table[(d >> 18) & 0x3f];
                 s += table[(d >> 12) & 0x3f];
                 s += (b2 ? table[(d >> 6) & 0x3f] : '=');
                 s += '=';
             }
             return s;
         }
     }

     /* ********************************** */

     class Vector {
         static add(a, b) { return a.map((v, i) => v + b[i]); }
         static sub(a, b) { return a.map((v, i) => v - b[i]); }
         static dot(a, b) { return a.reduce((a, v, i) => a + v * b[i], 0); }
         static mul(v, m) { return v.map((v) => v * m); }
         static div(v, m) { return v.map((v) => v / m); }

         static length(v) { return Math.sqrt(Vector.dot(v, v)); }
         static length2(s, e) { return Vector.length(Vector.sub(e, s)); }
         static unit(v) { return Vector.div(v, Vector.length(v)); }
         static unit2(s, e) { return Vector.unit(Vector.sub(e, s)); }
         static zoom(p, c, s) { return Vector.add(Vector.mul(Vector.sub(p, c), s), c); }

         static op_as = [Vector.add, Vector.sub];
         static op_asm = [Vector.add, Vector.sub, Vector.mul];
         static op_asd = [Vector.add, Vector.sub, Vector.div];
         static op_amd = [Vector.add, Vector.mul, Vector.div];
         static op_smd = [Vector.sub, Vector.mul, Vector.div];
         static op_asmd = [Vector.add, Vector.sub, Vector.mul, Vector.div];
     }

     function clamp(min, val, max) {
         return Math.max(min, Math.min(val, max));
     }

     /* ********************************** */

     /*
      * 成分色化
      */

     function createColorPlane(rgba, index, width, height) {
         const len = rgba.length;
         const out = new Uint8Array(len >> 2);

         for (let ip = index, op = 0; ip < len; ip +=4, op++)
             out[op] = rgba[ip];

         const clut = [...Array(256)].map((_, i) => [
             index == 0 ? i : 0,
             index == 1 ? i : 0,
             index == 2 ? i : 0,
         ]);

         return [clut, out, width, height];
     }
     function createColorRed(rgba, width, heigh) {
         return createColorPlane(rgba, 0, width, heigh);
     }
     function createColorGreen(rgba, width, heigh) {
         return createColorPlane(rgba, 1, width, heigh);
     }
     function createColorBlue(rgba, width, heigh) {
         return createColorPlane(rgba, 2, width, heigh);
     }

     /*
      * インデックス・カラー値変換表生成
      */

     function makeColorToIndex(level) {
         const l = level - 1;
         return new Uint8Array([...Array(256)]
             .map((_, v) => Math.trunc(v * l / 255)));
     }

     function makeColorToIndexR(level) {
         const l = level - 1;
         return new Uint8Array([...Array(256)]
             .map((_, v) => clamp(0, Math.round(v * l / 255), 255)));
     }

     function makeIndexToColor(level) {
         const l = level - 1;
         return new Uint8Array([...Array(level)]
             .map((_, v) => Math.trunc(v * 255 / l)));
     }

     function makeColorIndex(level) {
         return [
             makeIndexToColor(level),
             makeColorToIndex(level),
         ];
     }

     function makeColorIndexR(level) {
         return [
             makeIndexToColor(level),
             makeColorToIndexR(level),
         ];
     }

     function makeColorToColor(level) {
         const [ctab, btab] = makeColorIndex(level);
         return btab.map((v) => ctab[v]);
     }

     function makeColorToColorR(level) {
         const [ctab, btab] = makeColorIndexR(level);
         return btab.map((v) => ctab[v]);
     }

     function makeGrayScaleClut(ctab) {
         return [...ctab].map((v) => [v, v, v]);
     }

     function makeClut(table) {
         const [rt, gt, bt] = table;
         const [rn, gn, bn] = table.map((v) => v.length);
         const [cm, rm, gm, bm] = [rn * gn * bn, gn * bn, bn, 1];
         return [...Array(cm)].map((_, i) => [
             rt[Math.trunc(i / rm) % rn],
             gt[Math.trunc(i / gm) % gn],
             bt[Math.trunc(i / bm) % bn],
         ]);
     }

     /*
      * 色選択
      */

     const SQRT3 = Math.sqrt(3);
     const ISQRT3 = 1 / SQRT3;

     function rgbToKey(r, g, b) {
         return (r << 16) | (g << 8) | b;
     }

     function colorInfo(r, g, b) {
         const rf = r / 255;
         const gf = g / 255;
         const bf = b / 255;
         const lf = Math.sqrt(rf * rf + gf * gf + bf * bf);
         return {
             key: rgbToKey(r, g, b),
             rgb: [r, g, b],
             vec: [rf, gf, bf],
             unit: (lf == 0
                  ? [ISQRT3, ISQRT3, ISQRT3]
                  : [rf / lf, gf / lf, bf / lf]),
             count: 0,
             index: null,
         }
     }

     function colorDistance(p, q) {
         const [pr, pg, pb] = p.vec;
         const [qr, qg, qb] = q.vec;
         const dr = pr - qr;
         const dg = pg - qg;
         const db = pb - qb;
         const cl = dr * dr + dg * dg + db * db;
         const [px, py, pz] = p.unit;
         const [qx, qy, qz] = q.unit;
         const cc = px * qx + py * qy + pz * qz;
         return cl * (2.0 - Math.pow(cc, 0.25));
     }

     function allColorTable(rgba) {
         const len = rgba.length;
         const color = new Map();
         const ckey = new Array(len >> 2);

         for (let p = 0, k = 0; p < len; p += 4, k++) {
             const r = rgba[p + 0];
             const g = rgba[p + 1];
             const b = rgba[p + 2];
             const d = rgbToKey(r, g, b);
             const i = color.get(d);
             if (i == null) {
                 const n = colorInfo(r, g, b);
                 color.set(d, n);
                 ckey[k] = n;
             } else {
                 i.count += 1;
                 ckey[k] = i;
             }
         }
         return [color, ckey];
     }

     function filterColor(color, reserved) {
         for (const rgb of reserved)
             color.delete(rgbToKey(...rgb));
         return color;
     }

     function sortColorTable(color) {
         const table = Array();
         for (const item of color.entries())
             table.push(item);
         table.sort((a, b) => clamp(-1, (b.count - a.count), +1));
         return table;
     }

     function selectColor(rgba, width, height, color) {
         const tcol = color.map((c) => colorInfo(...c));
         const [pcol, ckey] = allColorTable(rgba);

         const tcnt = tcol.length;
         for (const [k, p] of pcol) {
             let dp = 0;
             let dmin = colorDistance(p, tcol[0]);
             for (let tp = 1; tp < tcnt; tp++) {
                 const dlen = colorDistance(p, tcol[tp]);
                 if (dmin > dlen) {
                     dp = tp;
                     dmin = dlen;
                 }
             }
             p.index = dp;
         }

         const clen = ckey.length;
         const cindex = Array(clen);
         for (let cp = 0; cp < clen; cp++)
             cindex[cp] = ckey[cp].index;
         return cindex;
     }

     /*
      * 減色
      */

     function reduceColorTableI(rgba, width, height, table) {
         const len = rgba.length;
         const out = new Uint8ClampedArray(len >> 2);

         const [[rct, rbt], [gct, gbt], [bct, bbt]] = table;
         const [rn, gn, bn] = [rct.length, gct.length, bct.length];
         const [cm, rm, gm, bm] = [rn * gn * bn, gn * bn, bn, 1];

         let op = 0;
         for (let cp = 0; cp < len; cp += 4) {
             const r = rgba[cp + 0];
             const g = rgba[cp + 1];
             const b = rgba[cp + 2];
             const a = rgba[cp + 3];

             const ri = rbt[r];
             const gi = gbt[g];
             const bi = bbt[b];

             out[op++] = ri * rm + gm * gi + bi;
         }

         const clut = [...Array(cm)].map((_, i) => [
             rct[Math.trunc(i / rm) % rn],
             gct[Math.trunc(i / gm) % gn],
             bct[Math.trunc(i / bm) % bn],
         ]);
         return [clut, out, width, height];
     }

     function reduceColorLevelRI(rgba, width, height, level) {
         const tab = makeColorIndexR(level);
         return reduceColorTableI(rgba, width, height, [tab, tab, tab]);
     }

     function reduceColorLevelR(rgba, width, height, level) {
         const len = rgba.length;
         const out = new Uint8ClampedArray(len);
         const rtab = makeColorToColorR(level);
         for (let cp = 0; cp < len; cp += 4) {
             const r = rgba[cp + 0];
             const g = rgba[cp + 1];
             const b = rgba[cp + 2];
             const a = rgba[cp + 3];

             out[cp + 0] = rtab[r];
             out[cp + 1] = rtab[g];
             out[cp + 2] = rtab[b];
             out[cp + 3] = a;
         }
         return [null, out, width, height];
     }

     function reduceColorClutN(rgba, width, height, clut) {
         const out = selectColor(rgba, width, height, clut);
         return [clut, out, width, height];
     }

     function reduceColorTableNI(rgba, width, height, table) {
         return reduceColorClutN(rgba, width, height, makeClut(table));
     }

     function reduceColorLevelNI(rgba, width, height, level) {
         const ctab = makeIndexToColor(level);
         const table = [ctab, ctab, ctab];
         return reduceColorTableNI(rgba, width, height, table);
     }

     /*
      * 簡易減色
      */

     const Color16Clut = [
         [0x00, 0x00, 0x00],
         [0x7f, 0x00, 0x00],
         [0x00, 0x7f, 0x00],
         [0x7f, 0x7f, 0x00],
         [0x00, 0x00, 0x7f],
         [0x7f, 0x00, 0x7f],
         [0x00, 0x7f, 0x7f],
         [0x7f, 0x7f, 0x7f],

         [0x00, 0x00, 0x00],
         [0xff, 0x00, 0x00],
         [0x00, 0xff, 0x00],
         [0xff, 0xff, 0x00],
         [0x00, 0x00, 0xff],
         [0xff, 0x00, 0xff],
         [0x00, 0xff, 0xff],
         [0xff, 0xff, 0xff],
     ];

     const Color16ClutIndex = [
         0+0, // 0:0:0
         0+1, // 1:0:0
         8+1, // 2:0:0
         0,

         0+2, // 0:1:0
         0+3, // 1:1:0
         8+1, // 2:1:0
         0,

         8+2, // 0:2:0
         8+2, // 1:2:0
         8+3, // 2:2:0
         0,

         0, 0, 0, 0,

         0+4, // 0:0:1
         0+5, // 1:0:1
         8+1, // 2:0:1
         0,

         0+6, // 0:1:1
         0+7, // 1:1:1
         8+1, // 2:1:1
         0,

         8+2, // 0:2:1
         8+2, // 1:2:1
         8+3, // 2:2:1
         0,

         0, 0, 0, 0,

         8+4, // 0:0:2
         8+4, // 1:0:2
         8+5, // 2:0:2
         0,

         8+4, // 0:1:2
         8+4, // 1:1:2
         8+5, // 2:1:2
         0,

         8+6, // 0:2:2
         8+6, // 1:2:2
         8+7, // 2:2:2
         0,

         0, 0, 0, 0,
     ];

     function reduceColor8(rgba, width, height) {
         return reduceColorLevelRI(rgba, width, height, 2);
     }

     function reduceColor16(rgba, width, height) {
         const len = rgba.length;
         const out = new Uint8Array(len >> 2);
         const index = Color16ClutIndex;

         for (let ip = 0, op = 0; ip < len; ip +=4, op++) {
             const r = rgba[ip + 0];
             const g = rgba[ip + 1];
             const b = rgba[ip + 2];

             const ri = (r < 64) ? 0 : (r < 192) ? 0x01 : 0x02;
             const gi = (g < 64) ? 0 : (g < 192) ? 0x04 : 0x08;
             const bi = (b < 64) ? 0 : (b < 192) ? 0x10 : 0x20;

             out[op] = index[ri | gi | bi];
         }
         return [Color16Clut, out, width, height];
     }

     function reduceColor27(rgba, width, height) {
         return reduceColorLevelRI(rgba, width, height, 3);
     }

     function reduceColor64(rgba, width, height) {
         return reduceColorLevelRI(rgba, width, height, 4);
     }

     function reduceColor125(rgba, width, height) {
         return reduceColorLevelRI(rgba, width, height, 5);
     }

     function reduceColor216(rgba, width, height) {
         return reduceColorLevelRI(rgba, width, height, 6);
     }

     function reduceColor256(rgba, width, height) {
         const ctab8 = makeColorIndexR(8);
         const ctab4 = makeColorIndexR(4);
         const table = [ctab8, ctab8, ctab4];
         return reduceColorTableI(rgba, width, height, table);
     }

     function reduceColor512(rgba, width, height) {
         return reduceColorLevelR(rgba, width, height, 8);
     }

     function reduceColor4096(rgba, width, height) {
         return reduceColorLevelR(rgba, width, height, 16);
     }

     function reduceColor32768(rgba, width, height) {
         return reduceColorLevelR(rgba, width, height, 32);
     }

     function reduceColor65536(rgba, width, height, bits) {
         const len = rgba.length;
         const out = new Uint8ClampedArray(len);

         const [ctab, btab] = makeColorIndex(1 << 6);

         let op = 0;
         for (let cp = 0; cp < len; cp += 4) {
             const r = rgba[cp + 0];
             const g = rgba[cp + 1];
             const b = rgba[cp + 2];
             const a = rgba[cp + 3];

             const ri = btab[r];
             const gi = btab[g];
             const bi = btab[b];
             const rm = Math.max(ri, gi, bi) < 32 ? 0x1f : 0x3e;

             const rc = ctab[ri & rm];
             const gc = ctab[gi & rm];
             const bc = ctab[bi & rm];

             out[cp + 0] = rc;
             out[cp + 1] = gc;
             out[cp + 2] = bc;
             out[cp + 3] = a;
         }

         return [null, out, width, height];
     }

     /*
      * 近傍色
      */

     function nearestColorLevel(rgba, width, height, level) {
         const tab = makeIndexToColor(level);
         const color = makeClut([tab, tab, tab]);
         const index = selectColor(rgba, width, height, color);
         return [color, index, width, height];
     }

     function toNearestColor8Fixed(rgba, width, height) {
         return nearestColorLevel(rgba, width, height, 2);
     }

     /*
      * 白黒(グレースケール)
      */

     function toGrayScale(r, g, b) {
         return (g >>> 1) + (r >>> 2) + (r >>> 3) + (b >>> 3);
     }

     /*
      * 白黒
      */

     function toGrayScaleLevel(rgba, width, height, level) {
         const len = rgba.length;
         const out = new Uint8Array(len >> 2);
         const [ctab, btab] = makeColorIndexR(level);

         for (let ip = 0, op = 0; ip < len; ip +=4, op++) {
             const r = rgba[ip + 0];
             const g = rgba[ip + 1];
             const b = rgba[ip + 2];
             out[op] = btab[toGrayScale(r, g, b)];
         }

         const clut = [...Array(level)].map(
             (_, i) => [ctab[i], ctab[i], ctab[i]]);
         return [clut, out, width, height];
     }

     function toColorWB(rgba, width, height) {
         return toGrayScaleLevel(rgba, width, height, 2);
     }

     function toGrayScale4(rgba, width, height) {
         return toGrayScaleLevel(rgba, width, height, 4);
     }

     function toGrayScale8(rgba, width, height) {
         return toGrayScaleLevel(rgba, width, height, 8);
     }

     function toGrayScale16(rgba, width, height) {
         return toGrayScaleLevel(rgba, width, height, 16);
     }

     function toGrayScale64(rgba, width, height) {
         return toGrayScaleLevel(rgba, width, height, 64);
     }

     function toGrayScale256(rgba, width, height) {
         return toGrayScaleLevel(rgba, width, height, 256);
     }

     /*
      *
      */

     function setGrayScaleError(l, lc, d1, d2, d3, xp) {
         const ld = l - lc;
         const l2 = ld >>> 2;
         const l3 = ld >>> 3;
         const l4 = ld >>> 4;
         const lx = ld - (l2 << 1)  - (l3 << 1) - (l4 << 2);

         //

         d1[xp + 4 * 1 + 0] += l2;
         d1[xp + 4 * 2 + 0] += l4;

         //

         d2[xp - 4 * 2 + 0] += l4;
         d2[xp - 4 * 1 + 0] += l3;
         d2[xp + 4 * 0 + 0] += l2;
         d2[xp + 4 * 1 + 0] += l3;
         d2[xp + 4 * 2 + 0] += l4;

         //

         d3[xp - 4 * 1 + 0] += lx;
         d3[xp + 4 * 0 + 0] += l4;
     }

     /*
      * 誤差拡散白黒(N階調)
      */

     function toErrDiffGrayScaleLevel(rgba, width, height, level) {
         const len = rgba.length;
         const out = new Uint8Array(len >> 2);
         const lbsz = (width + 16) << 2;

         const [ctab, btab] = makeColorIndex(level);

         let cp = 0, op = 0;
         let dl1 = new Uint16Array(lbsz);
         let dl2 = new Uint16Array(lbsz);
         let dl3 = new Uint16Array(lbsz);

         for (let y = 0; y < 3; y++) {
             const d1 = dl1;
             const d2 = dl2;
             const d3 = dl3;
             d3.fill(0);

             cp = 0;
             for (let x = 0, xp = 4 * 4; x < width; x++, cp += 4, xp += 4) {
                 const r = rgba[cp + 0];
                 const g = rgba[cp + 1];
                 const b = rgba[cp + 2];

                 const w = toGrayScale(r, g, b);
                 const l = w + d1[xp];
                 const li = btab[Math.min(255, l)];

                 const lc = ctab[li];
                 setGrayScaleError(l, lc, d1, d2, d3, xp);
             }

             dl1 = d2;
             dl2 = d3;
             dl3 = d1;
         }

         cp = 0;
         for (let y = 0; y < height; y++) {
             const d1 = dl1;
             const d2 = dl2;
             const d3 = dl3;
             d3.fill(0);

             for (let x = 0, xp = 1 * 4; x < 3; x++, xp += 4) {
                 const r = rgba[cp + 0];
                 const g = rgba[cp + 1];
                 const b = rgba[cp + 2];

                 const w = toGrayScale(r, g, b);
                 const l = w + d1[xp];
                 const li = btab[Math.min(255, l)];

                 const lc = ctab[li];
                 setGrayScaleError(l, lc, d1, d2, d3, xp);
             }

             for (let x = 0, xp = 4 * 4; x < width; x++, cp += 4, xp += 4) {
                 const r = rgba[cp + 0];
                 const g = rgba[cp + 1];
                 const b = rgba[cp + 2];

                 const w = toGrayScale(r, g, b);
                 const l = w + d1[xp];
                 const li = btab[Math.min(255, l)];

                 out[op++] = li;

                 const lc = ctab[li];
                 setGrayScaleError(l, lc, d1, d2, d3, xp);
             }

             for (let x = 0, xp = (width + 4) * 4; x < 3; x++, xp += 4) {
                 const r = rgba[cp + 0 - 4];
                 const g = rgba[cp + 1 - 4];
                 const b = rgba[cp + 2 - 4];

                 const w = toGrayScale(r, g, b);
                 const l = w + d1[xp];
                 const li = btab[Math.min(255, l)];

                 const lc = ctab[li];
                 setGrayScaleError(l, lc, d1, d2, d3, xp);
             }

             dl1 = d2;
             dl2 = d3;
             dl3 = d1;
         }

         const clut = makeGrayScaleClut(ctab);
         return [clut, out, width, height];
     }

     function toErrDiffColorWB(rgba, width, height) {
         return toErrDiffGrayScaleLevel(rgba, width, height, 2);
     }

     function toErrDiffGrayScale4(rgba, width, height) {
         return toErrDiffGrayScaleLevel(rgba, width, height, 4);
     }

     function toErrDiffGrayScale8(rgba, width, height) {
         return toErrDiffGrayScaleLevel(rgba, width, height, 8);
     }

     function toErrDiffGrayScale16(rgba, width, height) {
         return toErrDiffGrayScaleLevel(rgba, width, height, 16);
     }

     function toErrDiffGrayScale64(rgba, width, height) {
         return toErrDiffGrayScaleLevel(rgba, width, height, 64);
     }

     function toErrDiffGrayScale256(rgba, width, height) {
         return toErrDiffGrayScaleLevel(rgba, width, height, 256);
     }

     /*
      * 誤差拡散
      */

     function setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp) {
         const rd = r - rc;
         const gd = g - gc;
         const bd = b - bc;

         const r2 = rd >>> 2;
         const g2 = gd >>> 2;
         const b2 = bd >>> 2;

         const r3 = rd >>> 3;
         const g3 = gd >>> 3;
         const b3 = bd >>> 3;

         const r4 = rd >>> 4;
         const g4 = gd >>> 4;
         const b4 = bd >>> 4;

         const rx = rd - (r2 << 1)  - (r3 << 1) - (r4 << 2);
         const gx = gd - (g2 << 1)  - (g3 << 1) - (g4 << 2);
         const bx = bd - (b2 << 1)  - (b3 << 1) - (b4 << 2);

         //

         d1[xp + 4 * 1 + 0] += r2;
         d1[xp + 4 * 1 + 1] += g2;
         d1[xp + 4 * 1 + 2] += b2;

         d1[xp + 4 * 2 + 0] += r4;
         d1[xp + 4 * 2 + 1] += g4;
         d1[xp + 4 * 2 + 2] += b4;

         //

         d2[xp - 4 * 2 + 0] += r4;
         d2[xp - 4 * 2 + 1] += g4;
         d2[xp - 4 * 2 + 2] += b4;

         d2[xp - 4 * 1 + 0] += r3;
         d2[xp - 4 * 1 + 1] += g3;
         d2[xp - 4 * 1 + 2] += b3;

         d2[xp + 4 * 0 + 0] += r2;
         d2[xp + 4 * 0 + 1] += g2;
         d2[xp + 4 * 0 + 2] += b2;

         d2[xp + 4 * 1 + 0] += r3;
         d2[xp + 4 * 1 + 1] += g3;
         d2[xp + 4 * 1 + 2] += b3;

         d2[xp + 4 * 2 + 0] += r4;
         d2[xp + 4 * 2 + 1] += g4;
         d2[xp + 4 * 2 + 2] += b4;

         //

         d3[xp - 4 * 1 + 0] += rx;
         d3[xp - 4 * 1 + 1] += gx;
         d3[xp - 4 * 1 + 2] += bx;

         d3[xp + 4 * 0 + 0] += r4;
         d3[xp + 4 * 0 + 1] += g4;
         d3[xp + 4 * 0 + 2] += b4;
     }

     function toErrDiffColorTable(rgba, width, height, table) {
         const len = rgba.length;
         const out = new Uint8Array(len >> 2);
         const lbsz = (width + 16) << 2;

         const [[rct, rbt], [gct, gbt], [bct, bbt]] = table;
         const [rn, gn, bn] = [rct.length, gct.length, bct.length];
         const [cm, rm, gm, bm] = [rn * gn * bn, gn * bn, bn, 1];

         let cp = 0, op = 0;
         let dl1 = new Uint16Array(lbsz);
         let dl2 = new Uint16Array(lbsz);
         let dl3 = new Uint16Array(lbsz);

         for (let y = 0; y < 3; y++) {
             const d1 = dl1;
             const d2 = dl2;
             const d3 = dl3;
             d3.fill(0);

             cp = 0;
             for (let x = 0, xp = 4 * 4; x < width; x++, cp += 4, xp += 4) {
                 const r = rgba[cp + 0] + d1[xp + 0];
                 const g = rgba[cp + 1] + d1[xp + 1];
                 const b = rgba[cp + 2] + d1[xp + 2];

                 const ri = rbt[Math.min(255, r)];
                 const gi = rbt[Math.min(255, g)];
                 const bi = rbt[Math.min(255, b)];

                 const rc = rct[ri];
                 const gc = gct[gi];
                 const bc = bct[bi];

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             dl1 = d2;
             dl2 = d3;
             dl3 = d1;
         }

         cp = 0;
         for (let y = 0; y < height; y++) {
             const d1 = dl1;
             const d2 = dl2;
             const d3 = dl3;
             d3.fill(0);

             for (let x = 0, xp = 1 * 4; x < 3; x++, xp += 4) {
                 const r = rgba[cp + 0] + d1[xp + 0];
                 const g = rgba[cp + 1] + d1[xp + 1];
                 const b = rgba[cp + 2] + d1[xp + 2];

                 const ri = rbt[Math.min(255, r)];
                 const gi = rbt[Math.min(255, g)];
                 const bi = rbt[Math.min(255, b)];

                 const rc = rct[ri];
                 const gc = gct[gi];
                 const bc = bct[bi];

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             for (let x = 0, xp = 4 * 4; x < width; x++, cp += 4, xp += 4) {
                 const r = rgba[cp + 0] + d1[xp + 0];
                 const g = rgba[cp + 1] + d1[xp + 1];
                 const b = rgba[cp + 2] + d1[xp + 2];

                 const ri = rbt[Math.min(255, r)];
                 const gi = rbt[Math.min(255, g)];
                 const bi = rbt[Math.min(255, b)];

                 out[op++] = ri * rm + gm * gi + bi;

                 const rc = rct[ri];
                 const gc = gct[gi];
                 const bc = bct[bi];

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             for (let x = 0, xp = (width + 4) * 4; x < 3; x++, xp += 4) {
                 const r = rgba[cp + 0 - 4] + d1[xp + 0];
                 const g = rgba[cp + 1 - 4] + d1[xp + 1];
                 const b = rgba[cp + 2 - 4] + d1[xp + 2];

                 const ri = rbt[Math.min(255, r)];
                 const gi = rbt[Math.min(255, g)];
                 const bi = rbt[Math.min(255, b)];

                 const rc = rct[ri];
                 const gc = gct[gi];
                 const bc = bct[bi];

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             dl1 = d2;
             dl2 = d3;
             dl3 = d1;
         }

         const clut = [...Array(cm)].map((_, i) => [
             rct[Math.trunc(i / rm) % rn],
             gct[Math.trunc(i / gm) % gn],
             bct[Math.trunc(i / bm) % bn],
         ]);
         return [clut, out, width, height];
     }

     function toErrDiffColor8(rgba, width, height) {
         const ctab = [0x00, 0xff];
         const btab = [...Array(256)].map((_, i) => (i >= 255) ? 1 : 0);
         const tab = [ctab, btab];
         return toErrDiffColorTable(rgba, width, height, [tab, tab, tab]);
     }

     function toErrDiffColor16Fixed(rgba, width, height) {
         const len = rgba.length;
         const out = new Uint8Array(len >> 2);
         const lbsz = (width + 16) << 2;

         const clut = Color16Clut;
         const index = Color16ClutIndex;

         let cp = 0, op = 0;
         let dl1 = new Uint16Array(lbsz);
         let dl2 = new Uint16Array(lbsz);
         let dl3 = new Uint16Array(lbsz);

         for (let y = 0; y < 3; y++) {
             const d1 = dl1;
             const d2 = dl2;
             const d3 = dl3;
             d3.fill(0);

             cp = 0;
             for (let x = 0, xp = 4 * 4; x < width; x++, cp += 4, xp += 4) {
                 const r = rgba[cp + 0] + d1[xp + 0];
                 const g = rgba[cp + 1] + d1[xp + 1];
                 const b = rgba[cp + 2] + d1[xp + 2];

                 const ri = (r < 223) ? 0 : (r < 255) ? 0x01 : 0x02;
                 const gi = (g < 223) ? 0 : (g < 255) ? 0x04 : 0x08;
                 const bi = (b < 223) ? 0 : (b < 255) ? 0x10 : 0x20;
                 const ci = index[ri | gi | bi];

                 const color = clut[ci];
                 const rc = color[0];
                 const gc = color[1];
                 const bc = color[2];

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             dl1 = d2;
             dl2 = d3;
             dl3 = d1;
         }

         cp = 0;
         for (let y = 0; y < height; y++) {
             const d1 = dl1;
             const d2 = dl2;
             const d3 = dl3;
             d3.fill(0);

             for (let x = 0, xp = 1 * 4; x < 3; x++, xp += 4) {
                 const r = rgba[cp + 0] + d1[xp + 0];
                 const g = rgba[cp + 1] + d1[xp + 1];
                 const b = rgba[cp + 2] + d1[xp + 2];

                 const ri = (r < 223) ? 0 : (r < 255) ? 0x01 : 0x02;
                 const gi = (g < 223) ? 0 : (g < 255) ? 0x04 : 0x08;
                 const bi = (b < 223) ? 0 : (b < 255) ? 0x10 : 0x20;
                 const ci = index[ri | gi | bi];

                 const color = clut[ci];
                 const rc = color[0];
                 const gc = color[1];
                 const bc = color[2];

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             for (let x = 0, xp = 4 * 4; x < width; x++, cp += 4, xp += 4) {
                 const r = rgba[cp + 0] + d1[xp + 0];
                 const g = rgba[cp + 1] + d1[xp + 1];
                 const b = rgba[cp + 2] + d1[xp + 2];

                 const ri = (r < 223) ? 0 : (r < 255) ? 0x01 : 0x02;
                 const gi = (g < 223) ? 0 : (g < 255) ? 0x04 : 0x08;
                 const bi = (b < 223) ? 0 : (b < 255) ? 0x10 : 0x20;
                 const ci = index[ri | gi | bi];

                 out[op++] = ci;

                 const color = clut[ci];
                 const rc = color[0];
                 const gc = color[1];
                 const bc = color[2];

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             for (let x = 0, xp = (width + 4) * 4; x < 3; x++, xp += 4) {
                 const r = rgba[cp + 0 - 4] + d1[xp + 0];
                 const g = rgba[cp + 1 - 4] + d1[xp + 1];
                 const b = rgba[cp + 2 - 4] + d1[xp + 2];

                 const ri = (r < 223) ? 0 : (r < 255) ? 0x01 : 0x02;
                 const gi = (g < 223) ? 0 : (g < 255) ? 0x04 : 0x08;
                 const bi = (b < 223) ? 0 : (b < 255) ? 0x10 : 0x20;
                 const ci = index[ri | gi | bi];

                 const color = clut[ci];
                 const rc = color[0];
                 const gc = color[1];
                 const bc = color[2];

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             dl1 = d2;
             dl2 = d3;
             dl3 = d1;
         }

         return [clut, out, width, height];
     }

     function toErrDiffColor27Fixed(rgba, width, height) {
         const tab = makeColorIndex(3);
         return toErrDiffColorTable(rgba, width, height, [tab, tab, tab]);
     }

     function toErrDiffColor64Fixed(rgba, width, height) {
         const tab = makeColorIndex(4);
         return toErrDiffColorTable(rgba, width, height, [tab, tab, tab]);
     }

     function toErrDiffColor125Fixed(rgba, width, height) {
         const tab = makeColorIndex(5);
         return toErrDiffColorTable(rgba, width, height, [tab, tab, tab]);
     }

     function toErrDiffColor216Fixed(rgba, width, height) {
         const tab = makeColorIndex(6);
         return toErrDiffColorTable(rgba, width, height, [tab, tab, tab]);
     }

     function toErrDiffColor256Fixed(rgba, width, height) {
         const len = rgba.length;
         const out = new Uint8Array(len >> 2);
         const lbsz = (width + 16) << 2;

         const [ctab1, btab1] = makeColorIndex(8);
         const [ctab2, btab2] = makeColorIndex(4);

         let cp = 0, op = 0;
         let dl1 = new Uint16Array(lbsz);
         let dl2 = new Uint16Array(lbsz);
         let dl3 = new Uint16Array(lbsz);

         for (let y = 0; y < 3; y++) {
             const d1 = dl1;
             const d2 = dl2;
             const d3 = dl3;
             d3.fill(0);

             cp = 0;
             for (let x = 0, xp = 4 * 4; x < width; x++, cp += 4, xp += 4) {
                 const r = rgba[cp + 0] + d1[xp + 0];
                 const g = rgba[cp + 1] + d1[xp + 1];
                 const b = rgba[cp + 2] + d1[xp + 2];

                 const ri = btab1[Math.min(255, r)];
                 const gi = btab1[Math.min(255, g)];
                 const bi = btab2[Math.min(255, b)];

                 const rc = ctab1[ri];
                 const gc = ctab1[gi];
                 const bc = ctab2[bi];

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             dl1 = d2;
             dl2 = d3;
             dl3 = d1;
         }

         cp = 0;
         for (let y = 0; y < height; y++) {
             const d1 = dl1;
             const d2 = dl2;
             const d3 = dl3;
             d3.fill(0);

             for (let x = 0, xp = 1 * 4; x < 3; x++, xp += 4) {
                 const r = rgba[cp + 0] + d1[xp + 0];
                 const g = rgba[cp + 1] + d1[xp + 1];
                 const b = rgba[cp + 2] + d1[xp + 2];

                 const ri = btab1[Math.min(255, r)];
                 const gi = btab1[Math.min(255, g)];
                 const bi = btab2[Math.min(255, b)];

                 const rc = ctab1[ri];
                 const gc = ctab1[gi];
                 const bc = ctab2[bi];

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             for (let x = 0, xp = 4 * 4; x < width; x++, cp += 4, xp += 4) {
                 const r = rgba[cp + 0] + d1[xp + 0];
                 const g = rgba[cp + 1] + d1[xp + 1];
                 const b = rgba[cp + 2] + d1[xp + 2];

                 const ri = btab1[Math.min(255, r)];
                 const gi = btab1[Math.min(255, g)];
                 const bi = btab2[Math.min(255, b)];

                 out[op++] = (ri << 5) | (gi << 2) | bi;

                 const rc = ctab1[ri];
                 const gc = ctab1[gi];
                 const bc = ctab2[bi];

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             for (let x = 0, xp = (width + 4) * 4; x < 3; x++, xp += 4) {
                 const r = rgba[cp + 0 - 4] + d1[xp + 0];
                 const g = rgba[cp + 1 - 4] + d1[xp + 1];
                 const b = rgba[cp + 2 - 4] + d1[xp + 2];

                 const ri = btab1[Math.min(255, r)];
                 const gi = btab1[Math.min(255, g)];
                 const bi = btab2[Math.min(255, b)];

                 const rc = ctab1[ri];
                 const gc = ctab1[gi];
                 const bc = ctab2[bi];

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             dl1 = d2;
             dl2 = d3;
             dl3 = d1;
         }

         const clut = [...Array(256)].map((_, i) => [
             ctab1[(i >> 5) & 7],
             ctab1[(i >> 2) & 7],
             ctab2[(i >> 0) & 3],
         ]);
         return [clut, out, width, height];
     }

     function toErrDiffColorFixed(rgba, width, height, level) {
         const len = rgba.length;
         const out = new Uint8ClampedArray(len);
         const lbsz = (width + 16) << 2;

         const ctab = makeColorToColor(level);

         let cp = 0;
         let dl1 = new Uint16Array(lbsz);
         let dl2 = new Uint16Array(lbsz);
         let dl3 = new Uint16Array(lbsz);

         for (let y = 0; y < 3; y++) {
             const d1 = dl1;
             const d2 = dl2;
             const d3 = dl3;
             d3.fill(0);

             cp = 0;
             for (let x = 0, xp = 4 * 4; x < width; x++, cp += 4, xp += 4) {
                 const r = rgba[cp + 0] + d1[xp + 0];
                 const g = rgba[cp + 1] + d1[xp + 1];
                 const b = rgba[cp + 2] + d1[xp + 2];

                 const rc = ctab[Math.min(255, r)];
                 const gc = ctab[Math.min(255, g)];
                 const bc = ctab[Math.min(255, b)];

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             dl1 = d2;
             dl2 = d3;
             dl3 = d1;
         }

         cp = 0;
         for (let y = 0; y < height; y++) {
             const d1 = dl1;
             const d2 = dl2;
             const d3 = dl3;
             d3.fill(0);

             for (let x = 0, xp = 1 * 4; x < 3; x++, xp += 4) {
                 const r = rgba[cp + 0] + d1[xp + 0];
                 const g = rgba[cp + 1] + d1[xp + 1];
                 const b = rgba[cp + 2] + d1[xp + 2];

                 const rc = ctab[Math.min(255, r)];
                 const gc = ctab[Math.min(255, g)];
                 const bc = ctab[Math.min(255, b)];

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             for (let x = 0, xp = 4 * 4; x < width; x++, cp += 4, xp += 4) {
                 const r = rgba[cp + 0] + d1[xp + 0];
                 const g = rgba[cp + 1] + d1[xp + 1];
                 const b = rgba[cp + 2] + d1[xp + 2];
                 const a = rgba[cp + 3];

                 const rc = ctab[Math.min(255, r)];
                 const gc = ctab[Math.min(255, g)];
                 const bc = ctab[Math.min(255, b)];

                 out[cp + 0] = rc;
                 out[cp + 1] = gc;
                 out[cp + 2] = bc;
                 out[cp + 3] = a;

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             for (let x = 0, xp = (width + 4) * 4; x < 3; x++, xp += 4) {
                 const r = rgba[cp + 0 - 4] + d1[xp + 0];
                 const g = rgba[cp + 1 - 4] + d1[xp + 1];
                 const b = rgba[cp + 2 - 4] + d1[xp + 2];

                 const rc = ctab[Math.min(255, r)];
                 const gc = ctab[Math.min(255, g)];
                 const bc = ctab[Math.min(255, b)];

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             dl1 = d2;
             dl2 = d3;
             dl3 = d1;
         }

         return [null, out, width, height];
     }

     function toErrDiffColor512Fixed(rgba, width, height) {
         return toErrDiffColorFixed(rgba, width, height, 8);
     }
     function toErrDiffColor4096Fixed(rgba, width, height) {
         return toErrDiffColorFixed(rgba, width, height, 16);
     }
     function toErrDiffColor32768Fixed(rgba, width, height) {
         return toErrDiffColorFixed(rgba, width, height, 32);
     }

     function toErrDiffColor65536Fixed(rgba, width, height, bits) {
         const len = rgba.length;
         const out = new Uint8ClampedArray(len);
         const lbsz = (width + 16) << 2;

         const [ctab, btab] = makeColorIndex(1 << 6);

         let cp = 0;
         let dl1 = new Uint16Array(lbsz);
         let dl2 = new Uint16Array(lbsz);
         let dl3 = new Uint16Array(lbsz);

         for (let y = 0; y < 3; y++) {
             const d1 = dl1;
             const d2 = dl2;
             const d3 = dl3;
             d3.fill(0);

             cp = 0;
             for (let x = 0, xp = 4 * 4; x < width; x++, cp += 4, xp += 4) {
                 const r = rgba[cp + 0] + d1[xp + 0];
                 const g = rgba[cp + 1] + d1[xp + 1];
                 const b = rgba[cp + 2] + d1[xp + 2];

                 const ri = btab[Math.min(255, r)];
                 const gi = btab[Math.min(255, g)];
                 const bi = btab[Math.min(255, b)];
                 const rm = Math.max(ri, gi, bi) < 32 ? 0x1f : 0x3e;

                 const rc = ctab[ri & rm];
                 const gc = ctab[gi & rm];
                 const bc = ctab[bi & rm];

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             dl1 = d2;
             dl2 = d3;
             dl3 = d1;
         }

         cp = 0;
         for (let y = 0; y < height; y++) {
             const d1 = dl1;
             const d2 = dl2;
             const d3 = dl3;
             d3.fill(0);

             for (let x = 0, xp = 1 * 4; x < 3; x++, xp += 4) {
                 const r = rgba[cp + 0] + d1[xp + 0];
                 const g = rgba[cp + 1] + d1[xp + 1];
                 const b = rgba[cp + 2] + d1[xp + 2];

                 const ri = btab[Math.min(255, r)];
                 const gi = btab[Math.min(255, g)];
                 const bi = btab[Math.min(255, b)];
                 const rm = Math.max(ri, gi, bi) < 32 ? 0x1f : 0x3e;

                 const rc = ctab[ri & rm];
                 const gc = ctab[gi & rm];
                 const bc = ctab[bi & rm];

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             for (let x = 0, xp = 4 * 4; x < width; x++, cp += 4, xp += 4) {
                 const r = rgba[cp + 0] + d1[xp + 0];
                 const g = rgba[cp + 1] + d1[xp + 1];
                 const b = rgba[cp + 2] + d1[xp + 2];
                 const a = rgba[cp + 3];

                 const ri = btab[Math.min(255, r)];
                 const gi = btab[Math.min(255, g)];
                 const bi = btab[Math.min(255, b)];
                 const rm = Math.max(ri, gi, bi) < 32 ? 0x1f : 0x3e;

                 const rc = ctab[ri & rm];
                 const gc = ctab[gi & rm];
                 const bc = ctab[bi & rm];

                 out[cp + 0] = rc;
                 out[cp + 1] = gc;
                 out[cp + 2] = bc;
                 out[cp + 3] = a;

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             for (let x = 0, xp = (width + 4) * 4; x < 3; x++, xp += 4) {
                 const r = rgba[cp + 0 - 4] + d1[xp + 0];
                 const g = rgba[cp + 1 - 4] + d1[xp + 1];
                 const b = rgba[cp + 2 - 4] + d1[xp + 2];

                 const ri = btab[Math.min(255, r)];
                 const gi = btab[Math.min(255, g)];
                 const bi = btab[Math.min(255, b)];
                 const rm = Math.max(ri, gi, bi) < 32 ? 0x1f : 0x3e;

                 const rc = ctab[ri & rm];
                 const gc = ctab[gi & rm];
                 const bc = ctab[bi & rm];

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             dl1 = d2;
             dl2 = d3;
             dl3 = d1;
         }

         return [null, out, width, height];
     }

     /*
      * タイリング
      */

     class FillPattern4x4 {
         static table = [
             [4, 4],
             [0, 0], [2, 2], [2, 0], [0, 2],
             [1, 1], [3, 3], [3, 1], [1, 3],
             [1, 0], [3, 2], [3, 0], [1, 2],
             [0, 1], [2, 3], [2, 1], [0, 3],
         ].map((v) => [(v[1] << 2) | v[0]]);

         static flag = [...Array(16)].map(
             (_, i) => [...Array(16)].map(
                 (_, j) => FillPattern4x4.table.slice(0, j + 1).findIndex(
                     (v) => (v == i)) < 0 ? 0 : 1));

         static matrix = [0, 4, 8, 12].map(
             (v) => FillPattern4x4.flag.slice(v, v + 4));

         static colorToIndex(level) {
             const l = ((level - 1) << 4);
             return new Uint16Array([...Array(256)]
                 .map((_, v) => Math.trunc(v * l / 255)));
         }
     }

     function tileColorTableI(rgba, width, height, table) {
         const P = FillPattern4x4;
         const matrix = P.matrix;
         const len = rgba.length;
         const out = new Uint8Array(len >> 2);

         const [rct, gct, bct] = table;
         const [rn, gn, bn] = [rct.length, gct.length, bct.length];
         const [cm, rm, gm, bm] = [rn * gn * bn, gn * bn, bn, 1];

         const rpt = P.colorToIndex(rn);
         const gpt = P.colorToIndex(gn);
         const bpt = P.colorToIndex(bn);

         let cp = 0, op = 0;
         for (let y = 0; y < height; y++) {
             const pat = matrix[y & 3];

             for (let x = 0; x < width; x++, cp += 4) {
                 const q = pat[x & 3];

                 const r = rgba[cp + 0];
                 const g = rgba[cp + 1];
                 const b = rgba[cp + 2];

                 const rp = rpt[r];
                 const gp = gpt[g];
                 const bp = bpt[b];

                 const ro = q[rp & 15];
                 const go = q[gp & 15];
                 const bo = q[bp & 15];

                 const ri = (rp >> 4) + ro;
                 const gi = (gp >> 4) + go;
                 const bi = (bp >> 4) + bo;

                 out[op++] = ri * rm + gm * gi + bi;
             }
         }

         const clut = [...Array(cm)].map((_, i) => [
             rct[Math.trunc(i / rm) % rn],
             gct[Math.trunc(i / gm) % gn],
             bct[Math.trunc(i / bm) % bn],
         ]);
         return [clut, out, width, height];
     }

     function tileColorLevelI(rgba, width, height, level) {
         const tab = makeIndexToColor(level);
         return tileColorTableI(rgba, width, height, [tab, tab, tab]);
     }

     function tileColor8Fixed(rgba, width, height) {
         return tileColorLevelI(rgba, width, height, 2);
     }

     function tileColor16Fixed(rgba, width, height) {
         const P = FillPattern4x4;
         const matrix = P.matrix;
         const len = rgba.length;
         const out = new Uint8Array(len >> 2);

         const clut = Color16Clut;
         const index = Color16ClutIndex;

         const ptab = P.colorToIndex(3);

         let cp = 0, op = 0;
         for (let y = 0; y < height; y++) {
             const pat = matrix[y & 3];

             for (let x = 0; x < width; x++, cp += 4) {
                 const q = pat[x & 3];

                 const r = rgba[cp + 0];
                 const g = rgba[cp + 1];
                 const b = rgba[cp + 2];

                 const rp = ptab[r];
                 const gp = ptab[g];
                 const bp = ptab[b];

                 const ro = q[rp & 15];
                 const go = q[gp & 15];
                 const bo = q[bp & 15];

                 const ri = (rp >> 4) + ro;
                 const gi = (gp >> 4) + go;
                 const bi = (bp >> 4) + bo;

                 const ci = index[(ri << 0) | (gi << 2) | (bi << 4)];

                 out[op++] = ci;
             }
         }

         return [clut, out, width, height];
     }

     function tileColor27Fixed(rgba, width, height) {
         return tileColorLevelI(rgba, width, height, 3);
     }

     function tileColor64Fixed(rgba, width, height) {
         return tileColorLevelI(rgba, width, height, 4);
     }

     function tileColor125Fixed(rgba, width, height) {
         return tileColorLevelI(rgba, width, height, 5);
     }

     function tileColor216Fixed(rgba, width, height) {
         return tileColorLevelI(rgba, width, height, 6);
     }

     function tileColor256Fixed(rgba, width, height) {
         const tab1 = makeIndexToColor(8);
         const tab2 = makeIndexToColor(4);
         return tileColorTableI(rgba, width, height, [tab1, tab1, tab2]);
     }

     function tileColorTable(rgba, width, height, table) {
         const P = FillPattern4x4;
         const matrix = P.matrix;
         const len = rgba.length;
         const out = new Uint8ClampedArray(len);

         const [rct, gct, bct] = table;
         const rpt = P.colorToIndex(rct.length);
         const gpt = P.colorToIndex(gct.length);
         const bpt = P.colorToIndex(bct.length);

         let cp = 0, op = 0;
         for (let y = 0; y < height; y++) {
             const pat = matrix[y & 3];

             for (let x = 0; x < width; x++, cp += 4) {
                 const q = pat[x & 3];

                 const r = rgba[cp + 0];
                 const g = rgba[cp + 1];
                 const b = rgba[cp + 2];
                 const a = rgba[cp + 3];

                 const rp = rpt[r];
                 const gp = gpt[g];
                 const bp = bpt[b];

                 const ro = q[rp & 15];
                 const go = q[gp & 15];
                 const bo = q[bp & 15];

                 const ri = (rp >> 4) + ro;
                 const gi = (gp >> 4) + go;
                 const bi = (bp >> 4) + bo;

                 out[cp + 0] = rct[ri];
                 out[cp + 1] = gct[gi];
                 out[cp + 2] = bct[bi];
                 out[cp + 3] = a;
             }
         }

         return [null, out, width, height];
     }

     function tileColorLevel(rgba, width, height, level) {
         const tab = makeIndexToColor(level);
         return tileColorTable(rgba, width, height, [tab, tab, tab]);
     }

     function tileColor512Fixed(rgba, width, height) {
         return tileColorLevel(rgba, width, height, 8);
     }

     function tileColor4096Fixed(rgba, width, height) {
         return tileColorLevel(rgba, width, height, 16);
     }

     function tileColor32768Fixed(rgba, width, height) {
         return tileColorLevel(rgba, width, height, 32);
     }

     function tileColor65536Fixed(rgba, width, height) {
         const P = FillPattern4x4;
         const matrix = P.matrix;
         const len = rgba.length;
         const out = new Uint8ClampedArray(len);

         const ctab = makeIndexToColor(64);
         const ptab = P.colorToIndex(64);

         let cp = 0, op = 0;
         for (let y = 0; y < height; y++) {
             const pat = matrix[y & 3];

             for (let x = 0; x < width; x++, cp += 4) {
                 const q = pat[x & 3];

                 const r = rgba[cp + 0];
                 const g = rgba[cp + 1];
                 const b = rgba[cp + 2];
                 const a = rgba[cp + 3];

                 const rp = ptab[r];
                 const gp = ptab[g];
                 const bp = ptab[b];

                 const ro = q[rp & 15];
                 const go = q[gp & 15];
                 const bo = q[bp & 15];

                 const ri = (rp >> 4) + ro;
                 const gi = (gp >> 4) + go;
                 const bi = (bp >> 4) + bo;
                 const rm = Math.max(ri, gi, bi) < 32 ? 0x1f : 0x3e;

                 out[cp + 0] = ctab[ri & rm];
                 out[cp + 1] = ctab[gi & rm];
                 out[cp + 2] = ctab[bi & rm];
                 out[cp + 3] = a;
             }
         }

         return [null, out, width, height];
     }

     function tileGrayScaleLevel(rgba, width, height, level) {
         const P = FillPattern4x4;
         const matrix = P.matrix;
         const inp = toGrayScale256(rgba, width, height)[1];
         const out = new Uint8Array(inp.length);

         const ctab = makeIndexToColor(level);
         const ptab = P.colorToIndex(level);

         let cp = 0;
         for (let y = 0; y < height; y++) {
             const pat = matrix[y & 3];

             for (let x = 0; x < width; x++) {
                 const q = pat[x & 3];
                 const w = inp[cp];
                 const pi = ptab[w];
                 const po = q[pi & 15];
                 out[cp++] = (pi >> 4) + po;
             }
         }

         const clut = makeGrayScaleClut(ctab);
         return [clut, out, width, height];
     }

     function tileColorWB(rgba, width, height) {
         return tileGrayScaleLevel(rgba, width, height, 2);
     }

     function tileGrayScale4(rgba, width, height) {
         return tileGrayScaleLevel(rgba, width, height, 4);
     }

     function tileGrayScale8(rgba, width, height) {
         return tileGrayScaleLevel(rgba, width, height, 8);
     }

     function tileGrayScale16(rgba, width, height) {
         return tileGrayScaleLevel(rgba, width, height, 16);
     }

     function tileGrayScale64(rgba, width, height) {
         return tileGrayScaleLevel(rgba, width, height, 64);
     }

     /*
      *
      */

     const createTag = ((name) => document.createElement(name));

     function getImageData(image) {
         const [iw, ih] = [image.naturalWidth, image.naturalHeight];
         const osc = new OffscreenCanvas(iw, ih);
         const ctx = osc.getContext('2d');
         ctx.drawImage(image, 0, 0);
         return [[iw, ih], ctx.getImageData(0, 0, iw, ih)];
     }

     function blobToURL(blob) {
         return blob.arrayBuffer().then((buffer) => (
             `data:${blob.type};charset=utf-8;base64,` +
             MyGIF.encodeBase64(new Uint8Array(buffer))));
     }

     function createBitmap(parameter) {
         const [clut, pixel, width, height] = parameter;
         let binary = pixel;
         if (clut) {
             const color = clut.map((d) => [d[0], d[1], d[2], 255]);
             const data = [...pixel].map((p) => color[p]);
             binary = new Uint8ClampedArray(data.flat());
         }
         const image = new ImageData(binary, width, height);
         const osc = new OffscreenCanvas(width, height);
         const ctx = osc.getContext('2d');
         ctx.putImageData(image, 0, 0);
         return osc.convertToBlob().then(blobToURL);
     }

     function createGIF(parameter) {
         const [clut, pixel, width, height] = parameter;
         let bits = Math.max(1, 31 - Math.clz32(clut.length));
         const gif = new MyGIF(width, height, bits, 0, clut);
         gif.appendImageDescriptor(0, 0, width, height, pixel);
         gif.appendTrailer();
         return gif;
     }

     function createGIFURL(parameter) {
         return new Promise((resolve) =>
             resolve(createGIF(parameter).toDataURL()));
     }

     function createImageObject(parameter, nogif) {
         const bmp = nogif || !parameter[0] || (parameter[0].length > 256);
         return (bmp ? createBitmap : createGIFURL)(parameter);
     }

     /*
      *
      */

     class DropImage {
         static fromEvent(event) {
             return [...event.dataTransfer.files]
                 .filter((f) => f.type.match('^image/'));
         }

         static load(blobs) {
             const files = [...blobs].map((b) => ({blob: b}));
             return new Promise((resolve, reject) => {
                 function next(files, index) {
                     const drop = files[index];
                     drop.image = new Image();
                     drop.image.onload = (() => (++index < files.length)
                                              ? next(files, index) : resolve(files));
                     drop.image.onerror = reject;
                     drop.image.src = drop.src = URL.createObjectURL(drop.blob);
                 }
                 (files.length > 0) ? next(files, 0) : resolve(files);
             });
         }
     }

     class PageScroll {
         #stopFlag = false;

         constructor() {
             const keyboard = ((ev) => this.keyboardEvent(ev));
             const pointer = ((ev) => this.pointerEvent(ev));

             window.addEventListener('DOMMouseScroll', pointer);
             window.addEventListener('keydown', keyboard);
             window.addEventListener('touchmove', pointer, { passive: false });
             window.addEventListener('wheel', pointer, { passive: false });
         }

         pointerEvent(event) {
             if (this.#stopFlag)
                 event.preventDefault();
         }

         keyboardEvent(event) {
             if (this.#stopFlag) {
                 if ([37, 38, 39, 40].includes(event.code))
                     event.preventDefault();
             }
         }

         disable() { this.#stopFlag = true; }
         enable() { this.#stopFlag = false; }
     }

     class ImageViewer {
         constructor() {
             const S = ImageViewer;

             const html = document.documentElement;
             const screen = tagScreen;

             this.S = S;
             this.window = window;
             this.html = html;
             this.scroll = new PageScroll();
             this.screen = screen;
             this.screen_wmin = 448;
             this.screen_hmin = 336;

             this.original = null;
             this.image = null;
             this.image_scale = 1.0;
             this.image_offset = [0.0, 0.0];

             this.mouse_last = null;

             this.ondrop = null;

             const keyboard = ((ev) => this.keyboardEvent(ev));
             const resize = ((ev) => this.resizeEvent(ev));
             window.addEventListener('keydown', keyboard);
             window.addEventListener('keyup', keyboard);
             window.addEventListener('resize', resize);

             const pointer = ((ev) => this.pointerEvent(ev));
             screen.addEventListener('mouseenter', pointer);
             screen.addEventListener('mousemove', pointer);
             screen.addEventListener('mouseleave', pointer);
             screen.addEventListener('wheel', pointer, { passive: false });

             const dragover = ((ev) => this.dragOverEvent(ev));
             const drop = ((ev) => this.dropEvent(ev));
             screen.addEventListener('dragover', dragover);
             screen.addEventListener('drop', drop);

             this.setOriginal();
             this.updateScreenSize();
         }

         /*
          *
          */

         getScreenSize() {
             const screen = this.screen;
             return [screen.width, screen.height];
         }

         getScreenCenter() {
             const [w, h] = this.getScreenSize()
             return [w / 2.0, h / 2.0];
         }

         getImageSize() {
             const image = this.image;
             return [image.naturalWidth, image.naturalHeight];
         }

         getImageCenter() {
             const [w, h] = this.getImageSize()
             return [w / 2.0, h / 2.0];
         }

         imageToScreen(pos) {
             const [vadd, vsub, vmul] = Vector.op_asm;
             const spos = vmul(vsub(pos, this.getImageCenter()), this.image_scale);
             return vadd(vadd(spos, this.getScreenCenter()), this.image_offset);
         }

         screenToImage(pos) {
             const [vadd, vsub, vdiv] = Vector.op_asd;
             const spos = vsub(vsub(pos, this.getScreenCenter()), this.image_offset);
             return vadd(vdiv(spos, this.image_scale), this.getImageCenter());
         }

         setImagePosition(pos) {
             const image = this.image;
             if (!image)
                 return;
             if (!pos)
                 pos = this.imageToScreen([0.0, 0.0]);

             const screen = this.screen;
             screen.style.textAlign = 'left';
             screen.style.verticalAlign = 'top';

             const [ix, iy] = pos;
             const [dw, dh] = Vector.mul(this.getImageSize(), this.image_scale);
             image.style.left = `${Math.trunc(ix)}px`;
             image.style.top = `${Math.trunc(iy)}px`;
             image.width = dw;
             image.height = dh;
         }

         adjustScale(scale) {
             const image = this.image;
             if (!image)
                 return scale;

             const [sw, sh] = this.getScreenSize();
             const [iw, ih] = this.getImageSize();
             const fscale = Math.min(sw / iw, sh / ih);
             const smin = Math.min(fscale, 1.0) / 8.0;
             const smax = Math.max(fscale, 2.0) * 16.0;
             return clamp(smin, scale, smax);
         }

         setScroll(offset) {
             const image = this.image;
             if (!image)
                 return;
             this.image_offset = Vector.add(this.image_offset, offset);
             this.updateImagePosition();
         }

         setZoom(scale, pos, direct) {
             const image = this.image;
             if (!image)
                 return;
             if (!pos)
                 pos = this.getScreenCenter();

             const pscale = this.image_scale;
             const nscale = this.adjustScale(direct ? scale : pscale * scale);
             this.image_scale = nscale;
             scale = nscale / pscale;

             pos = Vector.sub(pos, this.getScreenCenter());
             this.image_offset = Vector.zoom(this.image_offset, pos, scale);

             this.updateImagePosition();
         }

         setFitScreen() {
             const image = this.image;
             if (!image) return;

             const [sw, sh] = this.getScreenSize();
             const [iw, ih] = this.getImageSize();
             const scale = Math.min(sw / iw, sh / ih);
             this.image_scale = scale;
             this.image_offset = [0.0, 0.0];

             const [vadd, vsub, vmul, vdiv] = Vector.op_asmd;
             this.setImagePosition(vdiv(vsub([sw, sh], vmul([iw, ih], scale)), 2.0));
         }

         updateImagePosition() {
             const image = this.image;
             if (!image)
                 return;

             const ip = this.image_offset;
             const [lx, ly] = Vector.mul(this.getImageCenter(), this.image_scale);
             this.image_offset = [clamp(-lx, ip[0], lx), clamp(-ly, ip[1], ly)];
             this.setImagePosition();
         }

         updateScreenSize() {
             const html = this.html;
             const screen = this.screen;

             const width = Math.max(this.screen_wmin, html.clientWidth);
             const height = Math.max(this.screen_hmin, html.clientHeight);

             screen.width = width;
             screen.height = height;

             this.image_offset = [0.0, 0.0];
             this.setZoom(1.0);
         }

         resizeEvent(event) {
             this.updateScreenSize();
         }

         /*
          *
          */

         clearScreen() {
             const screen = this.screen;
             while (screen.firstChild)
                 screen.removeChild(screen.firstChild);
         }

         setMessage(message) {
             this.clearScreen();

             const screen = this.screen;
             screen.style.textAlign = 'center';
             screen.style.verticalAlign = 'middle';

             const tag = createTag('div');
             tag.style.fontSize = 'xx-large';
             tag.style.color = 'white';
             tag.innerHTML = message;

             screen.appendChild(tag);
         }

         setImage(image) {
             this.clearScreen();
             image.className = 'relative';
             this.screen.appendChild(image);
             this.image = image;
         }

         setOriginal(image) {
             if (this.original) {
                 const url = this.original.src;
                 this.original = null;
                 URL.revokeObjectURL(url);
             }
             this.image = image;
             if (image) {
                 this.original = image;
                 this.setImage(image);
                 this.setFitScreen();

                 if (this.ondrop)
                     this.ondrop(this);
             } else {
                 this.setMessage('ここに画像ファイルをドロップしてください');
             }
         }

         dragOverEvent(event) {
             event.preventDefault();
             event.stopPropagation();
         }

         dropEvent(event) {
             event.preventDefault();
             event.stopPropagation();
             const images = DropImage.fromEvent(event);
             if (images.length == 1) {
                 this.setMessage('ただいま読み込み中...');
                 DropImage.load(images).then(
                     (files) => this.setOriginal(files[0].image),
                     console.log
                 );
             }
         }

         /*
          *
          */

         #focus = false;
         #scrollLock = false;

         setScrollLock(mode) {
             this.#scrollLock = mode;
             if (mode)
                 this.disableScroll();
         }

         disableScroll() {
             if (this.#focus || this.#scrollLock)
                 this.scroll.disable();
         }

         enableScroll() {
             if (!this.#scrollLock)
                 this.scroll.enable();
         }

         checkScrollKey(event) {
             return false;
         }

         keyboardEvent(event) {
             switch (event.type) {
                 case 'keydown':
                     if (this.checkScrollKey(event)) {
                         this.disableScroll();
                         break;
                     }
                     let scroll = null;
                     switch (event.code) {
                         case 'ArrowUp':
                             scroll = [0.0, -16.0];
                             break;
                         case 'ArrowDown':
                             scroll = [0.0, +16.0];
                             break;
                         case 'ArrowLeft':
                             scroll = [-16.0, 0.0];
                             break;
                         case 'ArrowRight':
                             scroll = [+16.0, 0.0];
                             break;
                     }
                     if (scroll) {
                         if (event.shiftKey)
                             scroll = Vector.div(scroll, 8.0);
                         this.setScroll(scroll);
                         break;
                     }

                     let scale = null;
                     let image_scale = null;
                     switch (event.key) {
                         case '0':
                             this.image_offset = [0.0, 0.0];
                         case '1':
                             image_scale = 1.0;
                             break;
                         case '2':
                             image_scale = 2.0;
                             break;
                         case '3':
                             image_scale = 3.0;
                             break;
                         case '4':
                             image_scale = 4.0;
                             break;

                         case '9':
                             this.setFitScreen();
                             break;
                         case '+':
                         case ';':
                             scale = 2.0;
                             break;
                         case '=':
                         case '-':
                             scale = 0.5;
                             break;
                     }
                     if (image_scale) {
                         this.setZoom(image_scale, null, true);
                     } else if (scale) {
                         if (event.ctrlKey)
                             scale = Math.pow(scale, 0.125);
                         this.setZoom(scale);
                         break;
                     }

                     break;

                 case 'keyup':
                     if (!this.checkScrollKey(event))
                         this.enableScroll();
                     break;
             }
         }

         getEventOffset(event) {
             if (!event.offsetX || !event.offsetY)
                 return null;
             const offs = [event.offsetX, event.offsetY];
             const target = event.target;
             if (target.id == this.screen.id)
                 return offs;
             return Vector.add(offs, [target.offsetLeft, target.offsetTop]);
         }

         pointerEvent(event) {
             const compose = ((ev) => (ev.altKey ||  ev.ctrlKey ||
                                       ev.metaKey || ev.shiftKey));

             const ppos = this.mouse_last;
             const cpos = this.getEventOffset(event);
             switch (event.type) {
                 case 'mouseenter':
                     this.#focus = true;
                     if (this.checkScrollKey(event))
                         this.disableScroll();
                     break;

                 case 'mouseleave':
                     this.#focus = false;
                     this.enableScroll();
                     break;

                 case 'mousemove':
                     if (compose(event) && ppos)
                         this.setScroll(Vector.sub(cpos, ppos));
                     break;

                 case 'wheel':
                     if (!compose(event))
                         this.setScroll([-event.deltaX, -event.deltaY]);
                     else
                         this.setZoom(Math.pow(2, - event.deltaY / 128), cpos);
                     break;
             }
             this.mouse_last = cpos;
         }
     }

     /*
      *
      */

     window.onload = function() {
         document.body.className = 'noframe';

         const viewer = new ImageViewer();
         viewer.setScrollLock(true);

         const image_rendering = [
             ['auto', menuRenderingAuto],
             // ['smooth', menuRenderingSmooth],
             // ['high-quality', menuRenderingHighQuality],
             // ['crisp-edges', menuRenderingCrispEdges],
             ['pixelated', menuRenderingPixelated],
         ];

         const generated = {
             pixel: null,
             rendering: 'pixelated',
             image: {
                 "menuColorRed": { func: createColorRed, image: null },
                 "menuColorGreen": { func: createColorGreen, image: null },
                 "menuColorBlue": { func: createColorBlue, image: null },

                 "menuReduceColor65536": { func: reduceColor65536, image: null, png: true },
                 "menuReduceColor32768": { func: reduceColor32768, image: null, png: true },
                 "menuReduceColor4096": { func: reduceColor4096, image: null, png: true },
                 "menuReduceColor512": { func: reduceColor512, image: null, png: true },
                 "menuReduceColor256": { func: reduceColor256, image: null },
                 "menuReduceColor216": { func: reduceColor216, image: null },
                 "menuReduceColor125": { func: reduceColor125, image: null },
                 "menuReduceColor64": { func: reduceColor64, image: null },
                 "menuReduceColor27": { func: reduceColor27, image: null },
                 "menuReduceColor16": { func: reduceColor16, image: null },
                 "menuReduceColor8": { func: reduceColor8, image: null },

                 "menuErrDiffColor65536F": { func: toErrDiffColor65536Fixed, image: null, png: true },
                 "menuErrDiffColor32768F": { func: toErrDiffColor32768Fixed, image: null, png: true },
                 "menuErrDiffColor4096F": { func: toErrDiffColor4096Fixed, image: null, png: true },
                 "menuErrDiffColor512F": { func: toErrDiffColor512Fixed, image: null, png: true },
                 "menuErrDiffColor256F": { func: toErrDiffColor256Fixed, image: null },
                 "menuErrDiffColor216F": { func: toErrDiffColor216Fixed, image: null },
                 "menuErrDiffColor125F": { func: toErrDiffColor125Fixed, image: null },
                 "menuErrDiffColor64F": { func: toErrDiffColor64Fixed, image: null },
                 "menuErrDiffColor27F": { func: toErrDiffColor27Fixed, image: null },
                 "menuErrDiffColor16F": { func: toErrDiffColor16Fixed, image: null },
                 "menuErrDiffColor8": { func: toErrDiffColor8, image: null },

                 "menuErrDiffGrayScale256": { func: toErrDiffGrayScale256, image: null },
                 "menuErrDiffGrayScale64": { func: toErrDiffGrayScale64, image: null },
                 "menuErrDiffGrayScale16": { func: toErrDiffGrayScale16, image: null },
                 "menuErrDiffGrayScale8": { func: toErrDiffGrayScale8, image: null },
                 "menuErrDiffGrayScale4": { func: toErrDiffGrayScale4, image: null },
                 "menuErrDiffColorWB": { func: toErrDiffColorWB, image: null },

                 "menuTileColor65536F": { func: tileColor65536Fixed, image: null },
                 "menuTileColor32768F": { func: tileColor32768Fixed, image: null },
                 "menuTileColor4096F": { func: tileColor4096Fixed, image: null },
                 "menuTileColor512F": { func: tileColor512Fixed, image: null },
                 "menuTileColor256F": { func: tileColor256Fixed, image: null },
                 "menuTileColor216F": { func: tileColor216Fixed, image: null },
                 "menuTileColor125F": { func: tileColor125Fixed, image: null },
                 "menuTileColor64F": { func: tileColor64Fixed, image: null },
                 "menuTileColor27F": { func: tileColor27Fixed, image: null },
                 "menuTileColor16F": { func: tileColor16Fixed, image: null },
                 "menuTileColor8F": { func: tileColor8Fixed, image: null },

                 "menuGrayScale256": { func: toGrayScale256, image: null },
                 "menuGrayScale64": { func: toGrayScale64, image: null },
                 "menuGrayScale16": { func: toGrayScale16, image: null },
                 "menuGrayScale8": { func: toGrayScale8, image: null },
                 "menuGrayScale4": { func: toGrayScale4, image: null },
                 "menuColorWB": { func: toColorWB, image: null },

                 "menuTileGrayScale64": { func: tileGrayScale64, image: null },
                 "menuTileGrayScale16": { func: tileGrayScale16, image: null },
                 "menuTileGrayScale8": { func: tileGrayScale8, image: null },
                 "menuTileGrayScale4": { func: tileGrayScale4, image: null },
                 "menuTileColorWB": { func: tileColorWB, image: null },
             },
         }

         const image_table = Object.keys(generated.image).map(
             (id) => document.getElementById(id)).filter(
                 (item) => item != null);

         function getPixelData() {
             if (!generated.pixel)
                 generated.pixel = getImageData(viewer.original);
             return generated.pixel;
         }
         function clearImage(image) {
             URL.revokeObjectURL(image.src);
             return null;
         }

         function getImageRendering(mode) {
             return image_rendering.findIndex((v) => v[0] == mode);
         }
         function setImageRendering() {
             const rendering = generated.rendering;
             viewer.image.style.setProperty('image-rendering', rendering, 'important');
             image_rendering.forEach((d, i) => {
                 const tag = d[1];
                 tag.style.color = (d[0] == rendering) ? 'green': 'black';
             });
         }
         function setImage(image) {
             viewer.setImage(image);
             viewer.setImagePosition();
             setImageRendering(image);
         }
         function setSelectImage(id) {
             image_table.forEach((tag) => {
                 tag.style.color = (id == tag.id) ? 'darkred' : 'black';
             });
         }

         function setFilteredImage(mid, mgen) {
             const pixel = getPixelData();
             const [iw, ih] = pixel[0];
             const idata = mgen.func(pixel[1].data, iw, ih);
             const nogif = menuCreateGIF.checked;
             createImageObject(idata, nogif).then((url) => {
                 const tag = createTag('img');
                 tag.width = iw;
                 tag.height = ih;
                 tag.onload = ((e) => setImage(e.target));
                 tag.src = url;
                 mgen.image = tag;
             });
             setSelectImage(mid);
         }

         function onclick(event) {
             const mid = event.target.id;
             if (mid == 'menuOriginal') {
                 setImage(viewer.original);
                 setSelectImage(mid);
                 return;
             }
             const mgen = generated.image[mid];
             if (mgen) {
                 if (mgen.image != null) {
                     setImage(mgen.image);
                     setSelectImage(mid);
                 } else {
                     viewer.setMessage(
                         'ただいま生成中、しばらくお待ち下さい。<br>' +
                         '<small>数分かかることがあります。<br>' +
                         'その間はブラウザが応答しません。</small>');
                     setTimeout(() => setFilteredImage(mid, mgen), 1);
                 }
                 return;
             }

             let rendering = null;
             if (mid == 'menuRendering') {
                 const ridx = getImageRendering(generated.rendering);
                 const nidx = (ridx + 1) % image_rendering.length;
                 rendering = image_rendering[nidx][0];
             } else {
                 const ridx = image_rendering.findIndex((v) => v[1].id == mid);
                 const nidx = (ridx >= 0) ? ridx : image_rendering.length - 1;
                 rendering = image_rendering[nidx][0];
             }
             if (rendering) {
                 generated.rendering = rendering;
                 setImageRendering();
             }
         }

         viewer.ondrop = ((viewer) => {
             generated.pixel = null;
             Object.values(generated.image).forEach((v) => v.image = null);

             const [w, h] = viewer.getImageSize();
             menuOriginal.innerText = `元画像(${w}x${h})`;
             setImageRendering();
             setSelectImage('menuOriginal');
             tagMenu.hidden = false;
         });

         document.querySelectorAll('.menuitem').forEach(
             (item) => item.addEventListener('click', onclick));

         menuCreateGIF.onchange = ((event) => {
             Object.values(generated.image).forEach(
                 (v) => { if (!v.png) v.image = null; });
         });
     }

    </script>
  </body>
</html>
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0