<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>漢字アスキーアート変換ツール（前処理・最適化強化版）</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      background: #fafafa;
      padding: 5px;
    }
    .container {
      margin: 0 auto;
      background: #fff;
      padding: 40px;
      width: 95%;
      box-shadow: 0 0 10px rgba(0,0,0,0.1);
    }
    h1 {
      text-align: center;
      margin-bottom: 20px;
    }
    label {
      display: block;
      margin-top: 15px;
    }
    input, select, button {
      margin-top: 5px;
      padding: 5px;
      font-size: 14px;
    }
    /* アスキーアート出力エリアを大きく表示 */
    #result {
      white-space: inline-block;
      font-family: monospace;
      font-size: 16px;
      background: #f0f0f0;
      margin-top: 20px;
      overflow: auto;
      min-height: 600px;
    }
    /* キャンバスは画面上に表示しない */
    canvas { display: none; }
  </style>
</head>
<body>
  <div class="container">
    <h1>漢字アスキーアート変換ツール</h1>
    <div>
      <label for="upload">画像をアップロード:</label>
      <input type="file" id="upload" accept="image/*">
    </div>
    <div>
      <label for="mode">モードを選択:</label>
      <select id="mode">
        <option value="density">濃度モード</option>
        <option value="shape">形モード</option>
      </select>
    </div>
    <div>
      <label for="scale">1ブロックのサイズ（ピクセル数）:</label>
      <input type="number" id="scale" value="10" min="1">
    </div>
    <div>
      <label for="charset">使用漢字セット（任意、空欄の場合は常用漢字全体を使用）:</label>
      <input type="text" id="charset" size="40" value="">
    </div>
    <div>
      <label for="transparencyThreshold">透過判定アルファ値閾値 (0～255):</label>
      <input type="number" id="transparencyThreshold" value="128" min="0" max="255">
    </div>
    <div>
      <label for="densityFactor">濃度調整係数 (デフォルトは1.0):</label>
      <input type="number" id="densityFactor" value="1.0" step="0.1" min="0.1">
    </div>
    <div>
      <label for="showBackground">元の画像を背景に表示:</label>
      <input type="checkbox" id="showBackground">
    </div>
    <div style="margin-top:15px;">
      <button id="convertBtn">変換開始</button>
      <button id="downloadBtn">テキストファイルとして保存</button>
    </div>
    <h2>出力結果:</h2>
    <pre id="result"></pre>
  </div>
  
  <!-- 画像処理用キャンバス -->
  <canvas id="canvas"></canvas>
  
  <script>
    /**********************
     * 画像前処理・最適化用関数群
     **********************/
    
    // 画像が大きすぎる場合、最大サイズ (maxWidth×maxHeight) に縮小します。
    function optimizeCanvasSize(canvas, maxWidth, maxHeight) {
      const tempCanvas = document.createElement('canvas');
      const tempCtx = tempCanvas.getContext('2d');
      const originalWidth = canvas.width;
      const originalHeight = canvas.height;
      let newWidth = originalWidth;
      let newHeight = originalHeight;
      if (originalWidth > maxWidth) {
        newWidth = maxWidth;
        newHeight = Math.round(originalHeight * (maxWidth / originalWidth));
      }
      if (newHeight > maxHeight) {
        newHeight = maxHeight;
        newWidth = Math.round(originalWidth * (maxHeight / originalHeight));
      }
      if (newWidth !== originalWidth || newHeight !== originalHeight) {
        tempCanvas.width = newWidth;
        tempCanvas.height = newHeight;
        tempCtx.drawImage(canvas, 0, 0, originalWidth, originalHeight, 0, 0, newWidth, newHeight);
        canvas.width = newWidth;
        canvas.height = newHeight;
        const mainCtx = canvas.getContext('2d');
        mainCtx.drawImage(tempCanvas, 0, 0);
      }
    }
    
    // 画像データからグレースケールとアルファ値のマップを作成
    function buildGrayscaleAlphaMaps(canvas) {
      const ctx = canvas.getContext('2d');
      const width = canvas.width, height = canvas.height;
      const imgData = ctx.getImageData(0, 0, width, height).data;
      const grayscale = new Float32Array(width * height);
      const alpha = new Float32Array(width * height);
      for (let i = 0; i < width * height; i++){
        const idx = i * 4;
        const r = imgData[idx], g = imgData[idx + 1], b = imgData[idx + 2], a = imgData[idx + 3];
        // 標準的な輝度計算
        grayscale[i] = 0.299 * r + 0.587 * g + 0.114 * b;
        alpha[i] = a;
      }
      return { grayscale, alpha, width, height };
    }
    
    // シンプルなボックスブラー（半径radius）による平滑化
    function boxBlur(map, width, height, radius) {
      const output = new Float32Array(width * height);
      for (let y = 0; y < height; y++){
        for (let x = 0; x < width; x++){
          let sum = 0, count = 0;
          for (let dy = -radius; dy <= radius; dy++){
            for (let dx = -radius; dx <= radius; dx++){
              const nx = x + dx, ny = y + dy;
              if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
                sum += map[ny * width + nx];
                count++;
              }
            }
          }
          output[y * width + x] = sum / count;
        }
      }
      return output;
    }
    
    // 前処理として、グレースケール・アルファマップを生成し、ボックスブラーでノイズ除去を実施
    function preprocessCanvas(canvas) {
      const maps = buildGrayscaleAlphaMaps(canvas);
      maps.grayscale = boxBlur(maps.grayscale, maps.width, maps.height, 1);
      maps.alpha = boxBlur(maps.alpha, maps.width, maps.height, 1);
      return maps;
    }
    
    // グローバル変数で前処理済みデータを保持
    let preprocessedMap = null;
    
    /**********************
     * 画像読み込み＆最適化
     **********************/
    document.getElementById('upload').addEventListener('change', function(e) {
      if (e.target.files && e.target.files[0]) {
        const reader = new FileReader();
        reader.onload = function(event) {
          const img = new Image();
          img.onload = function() {
            const canvas = document.getElementById('canvas');
            // 元画像サイズでキャンバスを設定
            canvas.width = img.width;
            canvas.height = img.height;
            const ctx = canvas.getContext('2d');
            ctx.drawImage(img, 0, 0);
            // サイズ最適化（例：最大1024×1024に縮小）
            optimizeCanvasSize(canvas, 2048, 2048);
            // 前処理済みマップを生成
            preprocessedMap = preprocessCanvas(canvas);
          }
          img.src = event.target.result;
        }
        reader.readAsDataURL(e.target.files[0]);
      }
    });
    
    /**********************
     * 変換処理（各モード）
     **********************/
    document.getElementById('convertBtn').addEventListener('click', function() {
      const mode = document.getElementById('mode').value;
      const scale = parseInt(document.getElementById('scale').value, 10);
      let charset = document.getElementById('charset').value;
      if (!charset.trim()) { 
        charset = DEFAULT_CANDIDATE_SET; 
      }
      const alphaThreshold = parseInt(document.getElementById('transparencyThreshold').value, 10);
      const canvas = document.getElementById('canvas');
      if (canvas.width === 0 || canvas.height === 0) {
        alert("まず画像をアップロードしてください。");
        return;
      }
      // 前処理済みマップの再生成（すでにあれば再利用）
      if (!preprocessedMap) {
        preprocessedMap = preprocessCanvas(canvas);
      }
      if (mode === "density") {
        // 濃度モード用に、候補漢字の自動濃度計算・ソート（薄い→濃い）
        charset = sortCandidatesByDensity(charset, scale);
        convertDensityMode(canvas, scale, charset, alphaThreshold, preprocessedMap);
      } else if (mode === "shape") {
        convertShapeMode(canvas, scale, charset, alphaThreshold, preprocessedMap);
      }
      
      // チェックボックスの状態に応じて背景画像を設定
      if (document.getElementById('showBackground').checked) {
        const dataURL = canvas.toDataURL();
        const resultEl = document.getElementById('result');
        resultEl.style.backgroundImage = 'url(' + dataURL + ')';
        resultEl.style.backgroundRepeat = 'no-repeat';
        resultEl.style.backgroundPosition = 'left';
        resultEl.style.backgroundSize = 'contain';
      } else {
        document.getElementById('result').style.backgroundImage = 'none';
      }
    });
    
    // テキストファイルとして保存
    document.getElementById('downloadBtn').addEventListener('click', function() {
      const text = document.getElementById('result').textContent;
      if (!text) {
        alert("まず変換を実行してください。");
        return;
      }
      const blob = new Blob([text], { type: "text/plain;charset=utf-8" });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = "ascii_art.txt";
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
    });
    
    /**********************
     * 候補漢字の自動濃度判定とソート
     **********************/
    const DEFAULT_CANDIDATE_SET = "一乙九七十人二入八力刀丁又了下口三山子女小上夕千川大土丸弓工才万士久干己寸亡及丈凡与刃巾乞円王火月犬五手水中天日木文六引牛今元戸午公止少心切太内父分方毛友区化反予欠氏不夫支比仏尺収仁介刈凶互井丹匹斤幻孔冗双斗乏升弔屯厄牙勾爪匂片玉左四出正生石田白本目右立外兄古広矢市台冬半母北用央去号皿仕写主申世他打代皮氷平由礼以加功札史司失必付辺包末未民令圧永可刊旧句示犯布弁穴冊処庁幼甘丘巨玄込召占奴払矛甲巧斥凹且囚汁仙凸尼丙瓦尻旦丼氾𠮟休気糸字耳先早竹虫年百名羽会回交光考行合寺自色西多池地当同肉米毎安曲血向死次式守州全有羊両列衣印各共好成争仲兆伝灯老因仮件再在舌団任宇灰危机吸后至存宅扱芋汚汗朽叫仰旨芝朱舟巡旬尽吐弐忙劣企吉刑如匠伐帆伏吏缶江充迅壮肌妃朴妄伎臼汎花貝見車赤足村男町何角汽近形言谷作社図声走体弟売麦来里医究局君決住助身対投豆坂返役位囲改完希求芸告材児初臣折束低努兵別利良冷労応快技均災志似序状条判防余我系孝困私否批忘卵乱壱戒含却狂迎更抗攻伺秀床吹即沢沖沈抜尾坊妙肝忌岐坑克寿伸辛択尿伴芳邦妨没抑励亜吟呉佐肖抄杉妥但呈廷忍妊把伯扶戻串沙芯汰那阪肘冶妖沃呂弄雨学空金青林画岩京国姉知長直店東歩妹明門夜委育泳岸苦具幸始使事実者昔取受所注定波板表服物放味命油和英果芽官季泣協径固刷参治周松卒底的典毒念府法牧例易往価河居券効妻枝舎述承招性制版肥非武沿延拡供呼刻若宗垂担宙忠届乳拝並宝枚依押奇祈拠況屈肩刺沼征姓拓抵到突杯泊拍迫彼怖抱肪茂炎欧殴佳怪岳拘祉侍邪昇炊阻卓抽泌苗奉房免炉拐劾宜拒享茎弦肯昆肢叔尚枢斉析拙坪邸泥迭披附侮沸併泡奔抹岬盲枠宛旺岡苛玩股虎采刹呪狙妬奈阜枕弥拉音草科海活計後室首秋春食星前茶昼点南風屋界客急級係県研指持拾重昭乗神相送待炭柱追度畑発美秒品負面洋胃栄紀軍型建律昨祝省信浅単飛変便約勇要逆限故厚査政祖則退独保迷映革看巻皇紅砂姿城専宣染泉洗奏段派肺背威為皆狭枯荒香恒咲狩柔盾侵是俗耐珍怒逃峠柄冒盆郎哀卸架悔冠軌虐峡契弧孤郊恨削施牲促胎怠胆訂帝卑赴封胞某幽厘姻疫垣括糾挟洪侯拷砕臭俊叙浄津甚帥窃荘衷挑勅亭貞洞柳畏茨咽怨柿柵拶拭栃虹眉訃勃昧侶思校家夏記帰原高紙時弱書通馬院員荷起宮庫根酒消真息速庭島配倍病勉旅流案害挙訓郡候航差殺残借笑席倉孫帯徒特梅粉脈浴料連益桜恩格個耕財師修素造能破俵容留株胸降骨座蚕射従純除将針値展党討納俳班秘陛鬼恐恵兼軒剣剤脂称浸振陣扇恥致途透唐桃倒胴悩般被疲浜敏浮捕峰砲眠娘紋涙烈恋朗悦宴華既脅倹悟娯疾殊徐辱粋衰隻桑託畜哲凍匿畔姫紛倣埋浪翁蚊核陥飢恭貢剛唆栽宰索桟酌珠准殉祥症宵娠唇畝逝栓租捜挿泰逐秩朕逓浦俸剖紡耗竜倫挨唄俺釜桁拳挫恣袖凄脊捉酎捗哺冥脇剝魚教強黄黒細週雪船組鳥野理悪球祭習終宿章商進深族第帳笛転都動部問貨械救健康菜産唱清巣側停堂得敗票副望陸移液眼規基寄許経険現混採授術常情責設接断張貧婦務率略異域郷済視捨推盛窓探著頂脳閉訪密訳郵翌欲陰菓乾脚掘婚彩惨執斜釈寂紹脱淡添盗販描猛粒郭掛勘貫菊虚偶啓掲控紺赦酔惜措粗掃逮袋窒彫陳陶豚粘婆排陪符崩隆猟陵尉逸涯殻喝渇患偽菌蛍渓斎崎蛇渋粛淑庶渉訟剰紳崇据旋曹眺釣偵悼軟培舶猫瓶偏堀麻唯悠庸涼累萎淫崖亀惧舷梗頃痕埼斬鹿羞戚曽爽唾堆貪梨捻森雲絵間場晴朝答道買番飲運温開階寒期軽湖港歯集暑勝植短着等登湯童悲筆遊葉陽落街覚喜給極景結最散順焼象隊達貯然博飯費無満量営過賀検減証税絶測属貸程提統備評富復報割揮貴筋勤敬裁策詞衆就善装創尊痛晩補棒握偉越援奥幾距御堅圏項紫煮畳殖尋訴替弾遅堤渡塔鈍普幅傍帽雄絡惑腕詠敢換喚棋欺喫遇雇硬慌絞軸湿晶掌焦遂随婿葬超痘蛮募揚揺裂廊湾渦棺堪款閑暁琴隅詐酢傘滋循硝粧詔診疎喪堕惰棚塚搭筒棟廃媒扉雰塀遍愉裕猶痢硫塁嵐椅媛葛喉須痩貼椎斑喩湧貿遠園楽新数電話暗意感漢業詩想鉄農福路愛塩試辞照節戦続置腸働解幹義禁群鉱罪資飼準勢損墓豊夢預絹源署傷蒸誠聖暖賃腹幕盟裏違煙鉛暇雅較勧詰傾遣誇鼓継歳載詳飾触慎寝跡僧嘆蓄跳殿搬微誉腰溶雷嫁塊慨該隔滑棄愚携催債搾慈摂賊滞滝稚塗飽滅裸零廉楼猿虞禍靴褐寛頑傑嫌献碁溝嗣酬愁奨睡践禅塑痴艇督漠鉢煩頒酪虜鈴賄彙楷蓋毀嗅僅窟詣隙傲塞嫉腫腎裾煎羨腺詮溺頓蜂睦慄賂塡歌語算読聞鳴駅銀鼻様緑練関管旗察種静説漁歴演慣境構際雑酸製精銭総増像態適銅徳複綿領閣疑誤穀誌磁障層認暮模維隠箇駆豪雌需端徴摘滴稲髪罰腐漫慢網踊暦概綱酵獄魂遭憎奪碑漂慕墨膜誘漏寡酷漆遮銃塾緒彰誓漸駄嫡漬寧閥僕銘僚熊箋遡遜綻蔑貌蜜瘍辣璃瑠線横談調箱億課器賞選熱標養輪確潔賛質敵導編暴遺劇権熟諸蔵誕潮論鋭影縁監歓輝儀戯撃稿趣震澄踏輩範盤膚敷賦舞噴舗黙慮慰閲餓緩緊撮暫潤遵衝嘱審穂請潜諾駐鋳墜締墳魅憂霊謁稼潟窮勲慶賜縄遷槽徹撤賠罷賓憤幣弊褒撲摩窯履寮潰畿憬稽駒摯餌憧踪誰嘲罵箸膝蔽餅親頭館橋整薬機積録衛興築燃輸激憲鋼樹縦操糖奮緯憶壊獲獣薪濁曇濃薄繁避壁謡頼隣隷穏凝憩賢墾錯諮錠嬢壇篤縛縫謀膨擁錬懐還憾薫衡儒壌薦磨諭融諧骸錦錮醒膳緻諦賭麺頰講謝績厳縮優覧環鮮燥翼療齢犠擦鍛聴嚇轄擬矯謹謙購懇爵醜償礁繊霜濯謄頻曖臆鍵戴瞳謎鍋闇瞭顔曜題観験類額職織簡難臨鎖瞬騒贈闘離穫騎繕礎鎮藩覆癖翻濫糧襟顕繭懲癒顎鎌韓藤璧藍願鏡識警臓繰爆霧麗鶏鯨髄瀬簿韻璽藻覇譜羅艶躍露顧魔艦鶴驚襲籠鑑鬱";
    function calculateCharDensity(char, size) {
      const offCanvas = document.createElement('canvas');
      offCanvas.width = size;
      offCanvas.height = size;
      const ctx = offCanvas.getContext('2d');
      ctx.fillStyle = "white";
      ctx.fillRect(0, 0, size, size);
      ctx.font = size + "px sans-serif";
      ctx.textAlign = "center";
      ctx.textBaseline = "middle";
      ctx.fillStyle = "black";
      ctx.fillText(char, size / 2, size / 2);
      const imageData = ctx.getImageData(0, 0, size, size).data;
      let darkCount = 0;
      for (let i = 0; i < imageData.length; i += 4) {
        const brightness = 0.299 * imageData[i] + 0.587 * imageData[i + 1] + 0.114 * imageData[i + 2];
        if (brightness < 128) { darkCount++; }
      }
      return darkCount / (size * size);
    }
    
    function sortCandidatesByDensity(candidateString, size) {
      const chars = Array.from(candidateString);
      const densityArray = chars.map(char => ({
        char: char,
        density: calculateCharDensity(char, size)
      }));
      densityArray.sort((a, b) => a.density - b.density);
      return densityArray.map(item => item.char).join("");
    }
    
    /**********************
     * 濃度モード変換
     **********************/
    function convertDensityMode(canvas, blockSize, charset, alphaThreshold, preMap) {
      const width = preMap.width;
      const height = preMap.height;
      const grayscale = preMap.grayscale;
      const alpha = preMap.alpha;
      let result = "";
      // 全体的な濃さ調整の係数（デフォルトは1.0）
      const densityFactor = parseFloat(document.getElementById('densityFactor').value || "1.0");
    
      for (let y = 0; y < height; y += blockSize) {
        let line = "";
        for (let x = 0; x < width; x += blockSize) {
          let sumBrightness = 0, sumAlpha = 0, count = 0;
          for (let j = 0; j < blockSize; j++) {
            for (let i = 0; i < blockSize; i++) {
              const posX = x + i, posY = y + j;
              if (posX < width && posY < height) {
                const idx = posY * width + posX;
                sumBrightness += grayscale[idx];
                sumAlpha += alpha[idx];
                count++;
              }
            }
          }
          const avgAlpha = sumAlpha / count;
          if (avgAlpha < alphaThreshold) {
            line += "　";
          } else {
            // densityFactor で調整：倍率 < 1 なら暗く、大きいなら明るくなる
            const modifiedBrightness = Math.min(avgBrightnessAdjustment(sumBrightness / count, densityFactor), 255);
            let charIndex = Math.floor(((255 - modifiedBrightness) / 255) * charset.length);
            if (charIndex >= charset.length) { charIndex = charset.length - 1; }
            line += charset.charAt(charIndex);
          }
        }
        result += line + "\n";
      }
      document.getElementById('result').textContent = result;
    }
    
    // 補助関数： densityFactor を掛けた平均輝度を返す
    function avgBrightnessAdjustment(avgBrightness, factor) {
      return avgBrightness * factor;
    }
    
    /**********************
     * 形状モード変換
     **********************/
    function convertShapeMode(canvas, blockSize, charset, alphaThreshold, preMap) {
      const width = preMap.width;
      const height = preMap.height;
      const grayscale = preMap.grayscale;
      const alpha = preMap.alpha;
      let result = "";
      
      // ブロックごとに二値化パターンを取得
      function getBlockPattern(x, y, blockSize) {
        let pattern = [];
        let sumAlpha = 0, count = 0;
        for (let j = 0; j < blockSize; j++) {
          let row = [];
          for (let i = 0; i < blockSize; i++) {
            const posX = x + i, posY = y + j;
            let value = 255;
            if (posX < width && posY < height) {
              const idx = posY * width + posX;
              value = grayscale[idx];
              sumAlpha += alpha[idx];
              count++;
            }
            row.push(value < 128 ? 1 : 0);
          }
          pattern.push(row);
        }
        return { pattern: pattern, avgAlpha: sumAlpha / count };
      }
      
      // 候補漢字のビットマップを取得（キャッシュ利用）
      function getCharBitmap(char, size) {
        const key = char + "_" + size;
        if (charBitmapCache[key]) { return charBitmapCache[key]; }
        const offCanvas = document.createElement('canvas');
        offCanvas.width = size;
        offCanvas.height = size;
        const offCtx = offCanvas.getContext('2d');
        offCtx.fillStyle = "white";
        offCtx.fillRect(0, 0, size, size);
        offCtx.font = size + "px sans-serif";
        offCtx.textAlign = "center";
        offCtx.textBaseline = "middle";
        offCtx.fillStyle = "black";
        offCtx.fillText(char, size / 2, size / 2);
        const imageData = offCtx.getImageData(0, 0, size, size).data;
        let bitmap = [];
        for (let j = 0; j < size; j++) {
          let row = [];
          for (let i = 0; i < size; i++) {
            const idx = (j * size + i) * 4;
            const brightness = 0.299 * imageData[idx] + 0.587 * imageData[idx + 1] + 0.114 * imageData[idx + 2];
            row.push(brightness < 128 ? 1 : 0);
          }
          bitmap.push(row);
        }
        charBitmapCache[key] = bitmap;
        return bitmap;
      }
      
      function calcPatternDifference(pattern1, pattern2) {
        let diff = 0;
        for (let j = 0; j < pattern1.length; j++) {
          for (let i = 0; i < pattern1[j].length; i++) {
            diff += Math.abs(pattern1[j][i] - pattern2[j][i]);
          }
        }
        return diff;
      }
      
      if (!charset.trim()) {
        charset = DEFAULT_CANDIDATE_SET;
      }
      const candidates = Array.from(charset);
      
      for (let y = 0; y < height; y += blockSize) {
        let line = "";
        for (let x = 0; x < width; x += blockSize) {
          const { pattern: blockPattern, avgAlpha } = getBlockPattern(x, y, blockSize);
          if (avgAlpha < alphaThreshold) {
            line += "　";
          } else {
            let bestChar = "";
            let minDiff = Infinity;
            for (let char of candidates) {
              const charBitmap = getCharBitmap(char, blockSize);
              const diff = calcPatternDifference(blockPattern, charBitmap);
              if (diff < minDiff) {
                minDiff = diff;
                bestChar = char;
              }
            }
            line += bestChar;
          }
        }
        result += line + "\n";
      }
      document.getElementById('result').textContent = result;
    }
    
    // 形モード用の候補文字ビットマップキャッシュ
    const charBitmapCache = {};
  </script>
</body>
</html>
