昔の貧弱な表示装置を思い出しつつ、24ビットカラーを様々な形式に変換するものを作ってみました。(画像ビュワー作成も目的の1つです)
スクリーン ショットは元画像を基本8色(黒、青、赤、緑、紫、水色、黄色、白)の誤差拡散法で処理したものです。
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"> auto</td></tr>
<tr><td id="menuRenderingSmooth" class="menuitem" hidden> smooth</td></tr>
<tr><td id="menuRenderingHighQuality" class="menuitem" hidden> high-quality</td></tr>
<tr><td id="menuRenderingCrispEdges" class="menuitem" hidden> crisp-edges</td></tr>
<tr><td id="menuRenderingPixelated" class="menuitem"> 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>
「9」: ウィンドウ合わせ<br>
「0」: 等倍<br>
「-」: 縮小(半分)<br>
「+」: 拡大(2倍)<br>
スクロール<br>
CTRL+マウス<br>
ズーム<br>
CTRL+ホイール<br>
</div>
</details>
<details>
<summary>説明</summary>
<div>
変換種類<br>
単:単純減色<br>
散:誤差拡散<br>
柄:市松模様<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>