絵文字で絵を描く「Emoji Map」
・ダウンロードされる方はこちら。↓
・ソースコードはこちら。↓
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Emoji Pixel Studio</title>
<style>
:root {
--bg-app: #e0e0e0;
--bg-panel: #f5f5f5;
--border: #ccc;
--accent: #4a90e2;
--text: #333;
}
body {
margin: 0;
padding: 0;
font-family: sans-serif;
background: var(--bg-app);
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
user-select: none; /* UIの文字選択を防ぐ */
}
/* メニューバー */
#menubar {
background: #333;
color: white;
padding: 5px 10px;
display: flex;
gap: 15px;
font-size: 14px;
}
.menu-item { cursor: pointer; padding: 2px 8px; }
.menu-item:hover { background: #555; }
/* ツールバー */
#toolbar {
background: var(--bg-panel);
border-bottom: 1px solid var(--border);
padding: 8px;
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
}
.tool-btn {
width: 36px;
height: 36px;
border: 1px solid #999;
background: white;
border-radius: 4px;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
font-size: 20px;
position: relative;
}
.tool-btn:hover { background: #eee; }
.tool-btn.active {
background: #d0e8ff;
border-color: var(--accent);
box-shadow: inset 0 0 3px rgba(0,0,0,0.2);
}
.separator { width: 1px; height: 30px; background: #ccc; margin: 0 5px; }
/* メインエリア */
#main-area {
display: flex;
flex: 1;
overflow: hidden;
}
/* 左パネル(パレット) */
#left-panel {
width: 280px;
background: var(--bg-panel);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
z-index: 5;
}
#category-header {
padding: 5px;
display: flex;
gap: 5px;
background: #ddd;
border-bottom: 1px solid #ccc;
}
#category-tabs {
flex: 1;
overflow-x: auto;
display: flex;
gap: 2px;
scrollbar-width: thin;
}
.cat-tab {
padding: 4px 8px;
background: #eee;
border: 1px solid #ccc;
font-size: 12px;
cursor: pointer;
white-space: nowrap;
}
.cat-tab.active { background: white; border-bottom-color: white; font-weight: bold; }
#palette-grid {
flex: 1;
padding: 10px;
overflow-y: auto;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(32px, 1fr));
gap: 4px;
align-content: start;
}
.emoji-cell {
font-size: 24px;
text-align: center;
cursor: pointer;
border-radius: 4px;
padding: 2px;
}
.emoji-cell:hover { background: #ddd; }
.emoji-cell.selected { background: #badfff; outline: 2px solid var(--accent); }
/* キャンバスエリア */
#canvas-wrapper {
flex: 1;
position: relative;
overflow: hidden; /* スクロールはCanvas内で行うか、親のoverflowでやるか */
background: #888;
display: flex;
justify-content: center;
align-items: center;
}
/* スクロールバー付きコンテナ */
#scroll-container {
width: 100%;
height: 100%;
overflow: auto;
display: flex;
justify-content: center;
align-items: center;
background-image:
linear-gradient(45deg, #ccc 25%, transparent 25%),
linear-gradient(-45deg, #ccc 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #ccc 75%),
linear-gradient(-45deg, transparent 75%, #ccc 75%);
background-size: 20px 20px;
background-color: #fff;
}
canvas {
background: white;
box-shadow: 0 0 10px rgba(0,0,0,0.3);
cursor: crosshair;
}
/* ポップアップメニュー */
.popup-menu {
position: absolute;
background: white;
border: 1px solid #999;
box-shadow: 2px 2px 10px rgba(0,0,0,0.2);
min-width: 150px;
z-index: 1000;
display: none;
padding: 5px 0;
}
.popup-item {
padding: 5px 15px;
cursor: pointer;
font-size: 14px;
}
.popup-item:hover { background: #eee; }
.popup-sep { height: 1px; background: #ddd; margin: 3px 0; }
/* ダイアログオーバーレイ */
#overlay {
position: fixed; top:0; left:0; right:0; bottom:0;
background: rgba(0,0,0,0.4);
display: none;
z-index: 2000;
justify-content: center;
align-items: center;
}
.dialog {
background: white;
padding: 20px;
border-radius: 5px;
width: 400px;
display: flex;
flex-direction: column;
gap: 10px;
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
}
textarea { height: 150px; resize: vertical; font-family: monospace; }
</style>
</head>
<body>
<div id="menubar">
<div class="menu-item" onclick="fileMenu('new')">新規作成</div>
<div class="menu-item" onclick="fileMenu('open')">開く</div>
<div class="menu-item" onclick="fileMenu('save')">上書き保存</div>
<div class="menu-item" onclick="fileMenu('saveAs')">別名で保存(txt)</div>
<div class="menu-item" onclick="editMenu('copy')">コピー</div>
<div class="menu-item" onclick="editMenu('paste')">貼り付け</div>
<div class="menu-item" onclick="toggleGrid()">グリッド表示切替</div>
</div>
<div id="toolbar">
<button class="tool-btn active" id="btn-pen" title="ペン (Left Click)" onclick="setTool('pen')">✏️</button>
<button class="tool-btn" id="btn-eraser" title="消しゴム" onclick="setTool('eraser')">🧹</button>
<div class="separator"></div>
<button class="tool-btn" id="btn-select" title="範囲選択 (ドラッグで移動)" onclick="setTool('select')">⛶</button>
<button class="tool-btn" id="btn-fill" title="塗りつぶし" onclick="setTool('fill')">🪣</button>
<button class="tool-btn" id="btn-dropper" title="スポイト (Right Click)" onclick="setTool('dropper')">💉</button>
<div class="separator"></div>
<button class="tool-btn" id="btn-rect" title="四角形" onclick="setTool('rect')">⬜</button>
<button class="tool-btn" id="btn-rect-fill" title="四角形(塗り)" onclick="setTool('rect-fill')">⬛</button>
<button class="tool-btn" id="btn-circle" title="円" onclick="setTool('circle')">◯</button>
<button class="tool-btn" id="btn-circle-fill" title="円(塗り)" onclick="setTool('circle-fill')">●</button>
<div class="separator"></div>
<button class="tool-btn" title="拡大" onclick="changeZoom(0.2)">➕</button>
<button class="tool-btn" title="縮小" onclick="changeZoom(-0.2)">➖</button>
<span style="font-size:12px; margin-left:5px;">現在: <span id="current-emoji-display">⬛</span></span>
</div>
<div id="main-area">
<div id="left-panel">
<div id="category-header">
<div id="category-tabs"></div>
<button onclick="addCategory()" style="font-size:10px;">+</button>
<button onclick="deleteCategory()" style="font-size:10px;">🗑️</button>
</div>
<div id="palette-grid"></div>
</div>
<div id="canvas-wrapper">
<div id="scroll-container">
<canvas id="editor-canvas"></canvas>
</div>
</div>
</div>
<div id="ctx-menu" class="popup-menu">
<div class="popup-item" onclick="setTool('dropper'); hideCtx();">スポイト</div>
<div class="popup-sep"></div>
<div class="popup-item" onclick="editMenu('cut'); hideCtx();">カット</div>
<div class="popup-item" onclick="editMenu('copy'); hideCtx();">コピー</div>
<div class="popup-item" onclick="editMenu('paste'); hideCtx();">貼り付け</div>
<div class="popup-sep"></div>
<div class="popup-item" onclick="editMenu('selectAll'); hideCtx();">すべて選択</div>
<div class="popup-item" onclick="editMenu('deselect'); hideCtx();">選択解除</div>
</div>
<div id="overlay">
<div class="dialog">
<h3 id="dialog-title">Title</h3>
<textarea id="dialog-text"></textarea>
<div style="text-align:right;">
<button onclick="closeDialog()">閉じる</button>
</div>
</div>
</div>
<script>
/**
* 定数・設定
*/
const SPACE = ' '; // 全角スペース
const INIT_SIZE = 20;
let CELL_SIZE = 32;
// 状態管理
let grid = []; // 2D配列
let width = INIT_SIZE;
let height = INIT_SIZE;
let zoom = 1.0;
let currentTool = 'pen';
let currentEmoji = '⬛';
let showGrid = true;
// 選択範囲状態
let selection = null; // {x, y, w, h, floatingBuffer: null}
let isDraggingSelection = false;
let dragStartPos = null;
// 履歴(Undoは簡易実装のため今回は省略、構造のみ)
let history = [];
// カテゴリデータ
let categories = {
"基本": ["⬛","⬜","🟥","🟦","🟧","🟨","🟩","🟪","🟫"],
"顔": ["😀","😂","😊","🥰","😎","🤔","😭","😡"],
"自然": ["🌲","🌷","🌻","🔥","💧","⭐","🌙"],
"記号": ["❤","💔","💯","💢","💤","💨","💥"]
};
let currentCat = "基本";
/**
* 初期化
*/
const canvas = document.getElementById('editor-canvas');
const ctx = canvas.getContext('2d');
const scrollContainer = document.getElementById('scroll-container');
window.onload = () => {
initGridData(width, height);
renderCategoryTabs();
renderPalette();
updateCanvasSize();
draw();
// マウスイベント設定
canvas.addEventListener('mousedown', handleMouseDown);
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
// 右クリック
canvas.addEventListener('contextmenu', handleRightClick);
document.addEventListener('click', hideCtx); // 他をクリックしたら閉じる
};
/**
* データ操作系
*/
function initGridData(w, h) {
grid = [];
for(let y=0; y<h; y++) {
let row = [];
for(let x=0; x<w; x++) {
row.push(SPACE);
}
grid.push(row);
}
}
/**
* 描画システム (Canvas)
*/
function updateCanvasSize() {
canvas.width = width * CELL_SIZE * zoom;
canvas.height = height * CELL_SIZE * zoom;
draw();
}
function draw() {
// 背景クリア
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.scale(zoom, zoom);
ctx.font = `${Math.floor(CELL_SIZE * 0.8)}px sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// グリッドと文字の描画
for(let y=0; y<height; y++) {
for(let x=0; x<width; x++) {
// 選択範囲の移動中は、元の場所を空白として描画(移動中の絵は後で描く)
if (selection && selection.floatingBuffer && isInSelection(x, y) && isDraggingSelection) {
drawCell(x, y, SPACE, true);
} else {
drawCell(x, y, grid[y][x]);
}
}
}
// 選択範囲の枠とフローティングコンテンツ
if (selection) {
// 枠線
ctx.strokeStyle = '#007bff';
ctx.lineWidth = 2;
ctx.setLineDash([4, 2]);
ctx.strokeRect(selection.x * CELL_SIZE, selection.y * CELL_SIZE, selection.w * CELL_SIZE, selection.h * CELL_SIZE);
ctx.setLineDash([]);
// 移動中の絵を描画
if (selection.floatingBuffer) {
for(let ly=0; ly<selection.h; ly++) {
for(let lx=0; lx<selection.w; lx++) {
const char = selection.floatingBuffer[ly][lx];
if(char !== null) { // 透明部分は描かない
const targetX = selection.x + lx;
const targetY = selection.y + ly;
drawCell(targetX, targetY, char, false); // 上書き描画
}
}
}
}
}
// グリッド線
if (showGrid) {
ctx.strokeStyle = '#e0e0e0';
ctx.lineWidth = 1;
ctx.beginPath();
for(let x=0; x<=width; x++) {
ctx.moveTo(x*CELL_SIZE, 0);
ctx.lineTo(x*CELL_SIZE, height*CELL_SIZE);
}
for(let y=0; y<=height; y++) {
ctx.moveTo(0, y*CELL_SIZE);
ctx.lineTo(width*CELL_SIZE, y*CELL_SIZE);
}
ctx.stroke();
}
ctx.restore();
}
function drawCell(x, y, char, isBgOnly=false) {
const px = x * CELL_SIZE;
const py = y * CELL_SIZE;
// 背景(空白の場合はわかりやすくグレーにしてもいいが、今回は白)
/*
if(char === SPACE) {
ctx.fillStyle = '#fafafa';
ctx.fillRect(px, py, CELL_SIZE, CELL_SIZE);
}
*/
if (!isBgOnly && char !== SPACE) {
ctx.fillStyle = '#000';
ctx.fillText(char, px + CELL_SIZE/2, py + CELL_SIZE/2 + 2);
}
}
/**
* ツールロジック
*/
let isMouseDown = false;
let startX, startY; // ドラッグ開始用
let previewShape = null; // シェイプ描画中のプレビュー(未実装だが拡張用)
function handleMouseDown(e) {
if(e.button !== 0) return; // 左クリックのみ
const pos = getGridPos(e);
if(!pos) return;
isMouseDown = true;
startX = pos.x;
startY = pos.y;
// 選択範囲内のクリック -> 移動モード開始
if (currentTool === 'select' && selection && isInSelection(pos.x, pos.y)) {
isDraggingSelection = true;
dragStartPos = {x: pos.x, y: pos.y, selX: selection.x, selY: selection.y};
// まだフローティング化していなければ、グリッドからデータを切り取って浮かす
if (!selection.floatingBuffer) {
floatSelection();
}
return;
}
// 選択ツールだが範囲外 -> 新規選択開始
if (currentTool === 'select') {
commitSelection(); // 以前の選択を確定
selection = {x: pos.x, y: pos.y, w: 1, h: 1, floatingBuffer: null};
draw();
return;
}
// 通常描画ツール
commitSelection(); // 選択解除
useTool(pos.x, pos.y);
draw();
}
function handleMouseMove(e) {
if(!isMouseDown) return;
const pos = getGridPos(e);
if(!pos) return;
// 選択範囲のドラッグ移動
if (isDraggingSelection && selection) {
const dx = pos.x - dragStartPos.x;
const dy = pos.y - dragStartPos.y;
selection.x = dragStartPos.selX + dx;
selection.y = dragStartPos.selY + dy;
draw();
return;
}
// 範囲選択の拡大
if (currentTool === 'select' && !isDraggingSelection) {
const minX = Math.min(startX, pos.x);
const minY = Math.min(startY, pos.y);
const w = Math.abs(pos.x - startX) + 1;
const h = Math.abs(pos.y - startY) + 1;
selection = {x: minX, y: minY, w: w, h: h, floatingBuffer: null};
draw();
return;
}
// ペン・消しゴム(ドラッグ描画)
if (['pen', 'eraser'].includes(currentTool)) {
useTool(pos.x, pos.y);
draw();
}
}
function handleMouseUp(e) {
if (!isMouseDown) return;
isMouseDown = false;
isDraggingSelection = false;
const pos = getGridPos(e);
if(!pos) return; // 画面外リリース
// 図形ツールの確定
if (['rect', 'rect-fill', 'circle', 'circle-fill'].includes(currentTool)) {
drawShape(startX, startY, pos.x, pos.y, currentTool);
draw();
}
}
function handleRightClick(e) {
e.preventDefault();
// 右クリックでスポイト(即時実行)
const pos = getGridPos(e);
if (pos) {
const char = grid[pos.y][pos.x];
if (char !== SPACE) {
currentEmoji = char;
document.getElementById('current-emoji-display').innerText = currentEmoji;
setTool('pen'); // 自動でペンに戻る
}
}
// ポップアップメニュー表示
const menu = document.getElementById('ctx-menu');
menu.style.display = 'block';
menu.style.left = e.pageX + 'px';
menu.style.top = e.pageY + 'px';
}
function getGridPos(e) {
const rect = canvas.getBoundingClientRect();
const x = Math.floor((e.clientX - rect.left) / (CELL_SIZE * zoom));
const y = Math.floor((e.clientY - rect.top) / (CELL_SIZE * zoom));
// 範囲外チェック(選択移動中は外に出ても良いので緩めるか、clampするか)
// ここではシンプルにGrid内に収める
if(x < 0 || x >= width || y < 0 || y >= height) {
// ツールによっては外に出ると都合が悪い
if(isDraggingSelection) return {x, y}; // 移動中は外も許可(計算用)
return null;
}
return {x, y};
}
/**
* ツール機能実装
*/
function useTool(x, y) {
if (x < 0 || x >= width || y < 0 || y >= height) return;
if (currentTool === 'pen') {
grid[y][x] = currentEmoji;
} else if (currentTool === 'eraser') {
grid[y][x] = SPACE;
} else if (currentTool === 'fill') {
floodFill(x, y, grid[y][x], currentEmoji);
}
}
// 塗りつぶし (再帰またはスタック)
function floodFill(x, y, targetColor, replaceColor) {
if (targetColor === replaceColor) return;
if (grid[y][x] !== targetColor) return;
let stack = [[x, y]];
while (stack.length) {
let [cx, cy] = stack.pop();
if (cx < 0 || cx >= width || cy < 0 || cy >= height) continue;
if (grid[cy][cx] === targetColor) {
grid[cy][cx] = replaceColor;
stack.push([cx + 1, cy]);
stack.push([cx - 1, cy]);
stack.push([cx, cy + 1]);
stack.push([cx, cy - 1]);
}
}
}
// 図形描画
function drawShape(x1, y1, x2, y2, type) {
const minX = Math.min(x1, x2);
const maxX = Math.max(x1, x2);
const minY = Math.min(y1, y2);
const maxY = Math.max(y1, y2);
for (let y = minY; y <= maxY; y++) {
for (let x = minX; x <= maxX; x++) {
let draw = false;
if (type.includes('rect')) {
if (type === 'rect-fill') draw = true;
else draw = (x === minX || x === maxX || y === minY || y === maxY);
} else if (type.includes('circle')) {
// 簡易楕円判定
let a = (maxX - minX) / 2;
let b = (maxY - minY) / 2;
let cx = minX + a;
let cy = minY + b;
let val = Math.pow((x - cx)/a, 2) + Math.pow((y - cy)/b, 2);
if (type === 'circle-fill') draw = (val <= 1.2); // 少し緩めに
else draw = (val >= 0.8 && val <= 1.2);
}
if (draw) {
grid[y][x] = currentEmoji;
}
}
}
}
/**
* 選択範囲・移動ロジック
*/
function isInSelection(x, y) {
if (!selection) return false;
return x >= selection.x && x < selection.x + selection.w &&
y >= selection.y && y < selection.y + selection.h;
}
// 選択範囲の中身を「浮遊バッファ」に移す(Cut & Float)
function floatSelection() {
if (!selection) return;
const buffer = [];
for(let ly=0; ly<selection.h; ly++) {
let row = [];
for(let lx=0; lx<selection.w; lx++) {
const gx = selection.x + lx;
const gy = selection.y + ly;
if (gx >= 0 && gx < width && gy >= 0 && gy < height) {
row.push(grid[gy][gx]);
grid[gy][gx] = SPACE; // 元の場所は消す
} else {
row.push(SPACE);
}
}
buffer.push(row);
}
selection.floatingBuffer = buffer;
}
// 浮いている選択範囲をグリッドに焼き付ける(Paste / Drop)
function commitSelection() {
if (!selection || !selection.floatingBuffer) {
selection = null;
return;
}
// バッファの内容を現在の位置に書き込む
for(let ly=0; ly<selection.h; ly++) {
for(let lx=0; lx<selection.w; lx++) {
const gx = selection.x + lx;
const gy = selection.y + ly;
const char = selection.floatingBuffer[ly][lx];
// グリッド範囲内のみ書き込み
if (gx >= 0 && gx < width && gy >= 0 && gy < height && char !== null) {
// 全角スペース(透明扱い)の場合は書き込まないほうが自然なツールもあるが、
// ここでは「移動」なので上書きする
grid[gy][gx] = char;
}
}
}
selection = null;
draw();
}
/**
* UI・メニュー操作
*/
function setTool(name) {
// 選択ツール以外に切り替えたら確定する
if(name !== 'select') commitSelection();
currentTool = name;
document.querySelectorAll('.tool-btn').forEach(b => b.classList.remove('active'));
let btnId = 'btn-' + (['rect-fill','circle-fill'].includes(name) ? name : name.split('-')[0]);
if(name === 'pen' || name === 'eraser' || name === 'fill' || name === 'dropper' || name === 'select') {
document.getElementById('btn-' + name).classList.add('active');
} else {
// 図形系
document.getElementById(btnId).classList.add('active');
}
}
function changeZoom(delta) {
zoom = Math.max(0.2, Math.min(3.0, zoom + delta));
updateCanvasSize();
}
function toggleGrid() {
showGrid = !showGrid;
draw();
}
function hideCtx() {
document.getElementById('ctx-menu').style.display = 'none';
}
/**
* ファイル・編集メニュー
*/
function fileMenu(action) {
commitSelection();
if (action === 'new') {
if(confirm('現在の内容は消去されます。よろしいですか?')) {
initGridData(width, height);
draw();
}
} else if (action === 'save') {
// ローカルストレージに保存(簡易)
localStorage.setItem('emojiPixelData', JSON.stringify(grid));
alert('ブラウザに一時保存しました');
} else if (action === 'open') {
const data = localStorage.getItem('emojiPixelData');
if(data) {
grid = JSON.parse(data);
draw();
} else {
alert('保存されたデータがありません');
}
} else if (action === 'saveAs') {
// テキスト書き出し
let text = grid.map(row => row.join("")).join("\n");
showDialog("テキストとしてコピー", text);
}
}
function editMenu(action) {
if (action === 'selectAll') {
commitSelection();
selection = {x:0, y:0, w:width, h:height, floatingBuffer: null};
currentTool = 'select';
setTool('select');
draw();
} else if (action === 'deselect') {
commitSelection();
draw();
} else if (action === 'copy') {
// 簡易実装:選択範囲のテキストをクリップボードへ
if(!selection) return alert('範囲を選択してください');
// まだFloatしていない場合はグリッドから読む
let text = "";
const buf = selection.floatingBuffer;
for(let ly=0; ly<selection.h; ly++) {
for(let lx=0; lx<selection.w; lx++) {
if(buf) text += buf[ly][lx];
else text += grid[selection.y+ly][selection.x+lx];
}
text += "\n";
}
navigator.clipboard.writeText(text).then(()=>alert('コピーしました'));
}
}
/**
* パレット管理
*/
function renderCategoryTabs() {
const container = document.getElementById('category-tabs');
container.innerHTML = '';
for(let cat in categories) {
const div = document.createElement('div');
div.className = `cat-tab ${cat === currentCat ? 'active' : ''}`;
div.innerText = cat;
div.onclick = () => { currentCat = cat; renderCategoryTabs(); renderPalette(); };
container.appendChild(div);
}
}
function renderPalette() {
const container = document.getElementById('palette-grid');
container.innerHTML = '';
categories[currentCat].forEach(emoji => {
const div = document.createElement('div');
div.className = `emoji-cell ${emoji === currentEmoji ? 'selected' : ''}`;
div.innerText = emoji;
div.onclick = () => {
currentEmoji = emoji;
document.getElementById('current-emoji-display').innerText = emoji;
setTool('pen');
renderPalette();
};
container.appendChild(div);
});
}
function addCategory() {
const name = prompt("新しいカテゴリ名:");
if(name && !categories[name]) {
categories[name] = [];
currentCat = name;
renderCategoryTabs();
renderPalette();
}
}
function deleteCategory() {
if(confirm(`${currentCat} カテゴリを削除しますか?`)) {
delete categories[currentCat];
const keys = Object.keys(categories);
if(keys.length > 0) currentCat = keys[0];
renderCategoryTabs();
renderPalette();
}
}
// 外部ダイアログ表示
function showDialog(title, text) {
document.getElementById('dialog-title').innerText = title;
document.getElementById('dialog-text').value = text;
document.getElementById('overlay').style.display = 'flex';
}
function closeDialog() {
document.getElementById('overlay').style.display = 'none';
}
// パレットへの絵文字追加(左クリックで配置機能の拡張として、パレット上で右クリックなどで追加できると良いが、今回は簡易的に)
// ※実際の運用では「パレットに追加」ボタン等が必要
</script>
</body>
</html>

コメント