Grid2
undo/redo/layer
%%html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Improved Interactive Canvas Grid with Palette</title>
<style>
body {
display: flex;
flex-direction: column;
align-items: center;
font-family: Arial, sans-serif;
}
#mainContainer {
display: flex;
justify-content: center;
width: 100%;
max-width: 1200px;
}
#gridContainer, #paletteContainer {
border: 1px solid #000;
margin: 20px;
}
#controls, #layerControls, #paletteControls {
text-align: center;
margin-bottom: 20px;
width: 100%;
}
.tool-btn, .layer-btn {
margin: 0 5px;
padding: 5px 10px;
background-color: #f0f0f0;
border: 1px solid #ccc;
border-radius: 3px;
cursor: pointer;
}
#layerList {
list-style-type: none;
padding: 0;
text-align: left;
}
#layerList li {
margin: 5px 0;
padding: 5px;
background-color: #f0f0f0;
border-radius: 3px;
}
#paletteInput, #gridOutput {
width: 100%;
height: 100px;
margin-bottom: 10px;
resize: vertical;
}
#gridContainer canvas, #paletteContainer canvas {
max-width: 100%;
height: auto;
}
</style>
</head>
<body>
<div id="controls">
<button id="resizeBtn" class="tool-btn">Resize Grid</button>
<input type="color" id="colorPicker" value="#000000">
<button class="tool-btn" data-tool="point">Point</button>
<button class="tool-btn" data-tool="pen">Pen</button>
<button class="tool-btn" data-tool="eraser">Eraser</button>
<button class="tool-btn" data-tool="line">Line</button>
<button id="undoBtn" class="tool-btn">Undo</button>
<button id="redoBtn" class="tool-btn">Redo</button>
<button id="exportBtn" class="tool-btn">Export (1:1)</button>
<button id="exportPixelBtn" class="tool-btn">Export (1 cell = 1 pixel)</button>
</div>
<div id="layerControls">
<button id="addLayerBtn" class="layer-btn">Add Layer</button>
<button id="removeLayerBtn" class="layer-btn">Remove Layer</button>
<ul id="layerList"></ul>
</div>
<div id="mainContainer">
<div id="gridContainer"></div>
<div id="paletteContainer"></div>
</div>
<div id="paletteControls">
<textarea id="paletteInput" placeholder="Enter hex colors, JSON array, or paste image URL"></textarea>
<button id="loadPaletteBtn" class="tool-btn">Load Palette</button>
<select id="extractMethod">
<option value="frequency">Frequency</option>
<option value="skip">Skip Frequency</option>
<option value="kmeans">K-means</option>
</select>
<input type="file" id="imageInput" accept="image/*">
</div>
<div id="gridOutputControls">
<textarea id="gridOutput" readonly></textarea>
<button id="copyToClipboardBtn" class="tool-btn">Copy to Clipboard</button>
<button id="exportTxtBtn" class="tool-btn">Export as TXT</button>
</div>
<script>
class Color {
constructor(r, g, b, a = 255) {
this.r = r;
this.g = g;
this.b = b;
this.a = a;
}
static fromHex(hex) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return new Color(r, g, b);
}
toHex() {
return `#${this.r.toString(16).padStart(2, '0')}${this.g.toString(16).padStart(2, '0')}${this.b.toString(16).padStart(2, '0')}`;
}
}
class Layer {
constructor(rowCount, colCount) {
this.cells = Array(rowCount).fill().map(() => Array(colCount).fill(new Color(255, 255, 255, 0)));
}
}
class Palette {
constructor(rowCount, colCount, width, height) {
this.canvas = document.createElement('canvas');
this.canvas.width = width;
this.canvas.height = height;
this.ctx = this.canvas.getContext("2d");
this.rowCount = rowCount;
this.colCount = colCount;
this.cellWidth = width / this.colCount;
this.cellHeight = height / this.rowCount;
this.colors = [];
this.canvas.addEventListener('click', this.handleClick.bind(this));
}
setColors(colors) {
this.colors = colors.slice(0, this.rowCount * this.colCount);
this.draw();
}
draw() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
for (let i = 0; i < this.colors.length; i++) {
const row = Math.floor(i / this.colCount);
const col = i % this.colCount;
const color = this.colors[i];
this.ctx.fillStyle = `rgb(${color.r},${color.g},${color.b})`;
this.ctx.fillRect(col * this.cellWidth, row * this.cellHeight, this.cellWidth, this.cellHeight);
}
// Draw grid lines
this.ctx.strokeStyle = '#000000';
this.ctx.lineWidth = 1;
for (let i = 1; i < this.colCount; i++) {
this.ctx.beginPath();
this.ctx.moveTo(i * this.cellWidth, 0);
this.ctx.lineTo(i * this.cellWidth, this.canvas.height);
this.ctx.stroke();
}
for (let i = 1; i < this.rowCount; i++) {
this.ctx.beginPath();
this.ctx.moveTo(0, i * this.cellHeight);
this.ctx.lineTo(this.canvas.width, i * this.cellHeight);
this.ctx.stroke();
}
}
handleClick(event) {
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const col = Math.floor(x / this.cellWidth);
const row = Math.floor(y / this.cellHeight);
const index = row * this.colCount + col;
if (index < this.colors.length) {
const selectedColor = this.colors[index];
document.getElementById('colorPicker').value = selectedColor.toHex();
grid.currentColor = selectedColor;
}
}
appendTo(container) {
container.appendChild(this.canvas);
}
}
class Grid {
constructor(rowCount, colCount, width, height) {
this.canvas = document.createElement('canvas');
this.canvas.width = width;
this.canvas.height = height;
this.ctx = this.canvas.getContext("2d");
this.currentColor = new Color(0, 0, 0);
this.rowCount = rowCount;
this.colCount = colCount;
this._cellWidth = width / this.colCount;
this._cellHeight = height / this.rowCount;
this.layers = [new Layer(rowCount, colCount)];
this.currentLayerIndex = 0;
this.cellInitColor = new Color(255, 255, 255, 0);
this.onGridLine = true;
this.lineColor = '#000000';
this.lineWidth = 1;
this.isSquareCell = true;
this.dirtyRegion = new Set();
this.tool = 'point';
this.isDrawing = false;
this.lastCell = null;
this.lineStartCell = null;
this.history = [];
this.historyIndex = -1;
this.canvas.addEventListener('mousedown', this.handleMouseDown.bind(this));
this.canvas.addEventListener('mousemove', this.handleMouseMove.bind(this));
this.canvas.addEventListener('mouseup', this.handleMouseUp.bind(this));
this.canvas.addEventListener('mouseleave', this.handleMouseUp.bind(this));
}
get currentLayer() {
return this.layers[this.currentLayerIndex];
}
get width() {
return this.canvas.width;
}
get height() {
return this.canvas.height;
}
get lowerEdge() {
const cw = this.width / this.colCount;
const ch = this.height / this.rowCount;
return Math.min(cw, ch);
}
get cellWidth() {
return this.isSquareCell ? this.lowerEdge : this._cellWidth;
}
get cellHeight() {
return this.isSquareCell ? this.lowerEdge : this._cellHeight;
}
get gridWidth() {
return this.cellWidth * this.colCount;
}
get gridHeight() {
return this.cellHeight * this.rowCount;
}
getGridIndex(x, y) {
const col = Math.floor(x / this.cellWidth);
const row = Math.floor(y / this.cellHeight);
return { row, col };
}
setColorAtIndex(row, col, color) {
if (!this.isValidIndex(row, col)) return false;
const action = {
type: 'setColor',
layerIndex: this.currentLayerIndex,
row, col,
oldColor: this.currentLayer.cells[row][col],
newColor: color
};
this.addToHistory(action);
this.applyAction(action);
this.updateGridOutput();
return true;
}
getColorAtIndex(row, col) {
if (!this.isValidIndex(row, col)) return null;
return this.currentLayer.cells[row][col];
}
isValidIndex(row, col) {
return row >= 0 && row < this.rowCount && col >= 0 && col < this.colCount;
}
applyAction(action) {
switch (action.type) {
case 'setColor':
this.layers[action.layerIndex].cells[action.row][action.col] = action.newColor;
this.dirtyRegion.add(`${action.row},${action.col}`);
break;
}
}
addToHistory(action) {
this.history = this.history.slice(0, this.historyIndex + 1);
this.history.push(action);
this.historyIndex++;
}
undo() {
if (this.historyIndex < 0) return;
const action = this.history[this.historyIndex];
this.revertAction(action);
this.historyIndex--;
this.draw();
this.updateGridOutput();
}
redo() {
if (this.historyIndex >= this.history.length - 1) return;
this.historyIndex++;
const action = this.history[this.historyIndex];
this.applyAction(action);
this.draw();
this.updateGridOutput();
}
revertAction(action) {
switch (action.type) {
case 'setColor':
this.layers[action.layerIndex].cells[action.row][action.col] = action.oldColor;
this.dirtyRegion.add(`${action.row},${action.col}`);
break;
}
}
drawCell(row, col, color) {
this.ctx.fillStyle = `rgba(${color.r},${color.g},${color.b},${color.a / 255})`;
this.ctx.fillRect(col * this.cellWidth, row * this.cellHeight, this.cellWidth, this.cellHeight);
}
drawCells() {
for (let layer of this.layers) {
for (let row = 0; row < this.rowCount; row++) {
for (let col = 0; col < this.colCount; col++) {
const color = layer.cells[row][col];
if (color.a > 0) {
this.drawCell(row, col, color);
}
}
}
}
}
drawGridLine(sx, sy, ex, ey) {
this.ctx.strokeStyle = this.lineColor;
this.ctx.lineWidth = this.lineWidth;
this.ctx.beginPath();
this.ctx.moveTo(sx, sy);
this.ctx.lineTo(ex, ey);
this.ctx.stroke();
}
drawGridLines() {
const we = this.colCount * this.cellWidth;
const he = this.rowCount * this.cellHeight;
for (let row = 0; row <= this.rowCount; row++) {
this.drawGridLine(0, row * this.cellHeight, we, row * this.cellHeight);
}
for (let col = 0; col <= this.colCount; col++) {
this.drawGridLine(col * this.cellWidth, 0, col * this.cellWidth, he);
}
}
draw() {
this.ctx.clearRect(0, 0, this.width, this.height);
for (let layer of this.layers) {
for (let row = 0; row < this.rowCount; row++) {
for (let col = 0; col < this.colCount; col++) {
const color = layer.cells[row][col];
if (color.a > 0) {
this.drawCell(row, col, color);
}
}
}
}
if (this.onGridLine) {
this.drawGridLines();
}
}
clearCells() {
this.ctx.fillStyle = `rgba(${this.cellInitColor.r},${this.cellInitColor.g},${this.cellInitColor.b},${this.cellInitColor.a / 255})`;
this.ctx.fillRect(0, 0, this.width, this.height);
}
resize(newRowCount, newColCount, newWidth, newHeight) {
const oldCells = this.layers.map(layer => layer.cells);
const oldRowCount = this.rowCount;
const oldColCount = this.colCount;
// Update dimensions
this.rowCount = newRowCount;
this.colCount = newColCount;
this.canvas.width = newWidth;
this.canvas.height = newHeight;
this._cellWidth = newWidth / newColCount;
this._cellHeight = newHeight / newRowCount;
// Resize all layers
this.layers = this.layers.map(layer => {
const newLayer = new Layer(newRowCount, newColCount);
const minRowCount = Math.min(oldRowCount, newRowCount);
const minColCount = Math.min(oldColCount, newColCount);
for (let r = 0; r < minRowCount; r++) {
for (let c = 0; c < minColCount; c++) {
newLayer.cells[r][c] = layer.cells[r][c];
}
}
return newLayer;
});
// Reset history as resize operation can't be undone
this.history = [];
this.historyIndex = -1;
// Redraw
this.draw();
this.updateGridOutput();
}
drawLine(startCell, endCell) {
const dx = Math.abs(endCell.col - startCell.col);
const dy = Math.abs(endCell.row - startCell.row);
const sx = startCell.col < endCell.col ? 1 : -1;
const sy = startCell.row < endCell.row ? 1 : -1;
let err = dx - dy;
let currentCell = {...startCell};
while (true) {
this.setColorAtIndex(currentCell.row, currentCell.col, this.currentColor);
if (currentCell.col === endCell.col && currentCell.row === endCell.row) break;
const e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
currentCell.col += sx;
}
if (e2 < dx) {
err += dx;
currentCell.row += sy;
}
}
}
handleMouseDown(event) {
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const cell = this.getGridIndex(x, y);
switch (this.tool) {
case 'point':
case 'pen':
case 'eraser':
this.isDrawing = true;
this.setColorAtIndex(cell.row, cell.col, this.tool === 'eraser' ? this.cellInitColor : this.currentColor);
this.lastCell = cell;
break;
case 'line':
if (!this.lineStartCell) {
this.lineStartCell = cell;
} else {
this.drawLine(this.lineStartCell, cell);
this.lineStartCell = null;
}
break;
}
this.draw();
}
handleMouseMove(event) {
if (!this.isDrawing || (this.tool !== 'pen' && this.tool !== 'eraser')) return;
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const cell = this.getGridIndex(x, y);
if (cell.row !== this.lastCell.row || cell.col !== this.lastCell.col) {
this.drawLine(this.lastCell, cell);
this.lastCell = cell;
this.draw();
}
}
handleMouseUp() {
this.isDrawing = false;
}
addLayer() {
this.layers.push(new Layer(this.rowCount, this.colCount));
this.currentLayerIndex = this.layers.length - 1;
this.updateLayerList();
}
removeLayer() {
if (this.layers.length > 1) {
this.layers.splice(this.currentLayerIndex, 1);
this.currentLayerIndex = Math.min(this.currentLayerIndex, this.layers.length - 1);
this.updateLayerList();
this.draw();
}
}
updateLayerList() {
const layerList = document.getElementById('layerList');
layerList.innerHTML = '';
this.layers.forEach((layer, index) => {
const li = document.createElement('li');
const radio = document.createElement('input');
radio.type = 'radio';
radio.name = 'layer';
radio.value = index;
radio.checked = index === this.currentLayerIndex;
radio.addEventListener('change', () => {
this.currentLayerIndex = index;
});
li.appendChild(radio);
li.appendChild(document.createTextNode(`Layer ${index + 1}`));
layerList.appendChild(li);
});
}
export(scale = 1) {
const canvas = document.createElement('canvas');
canvas.width = this.colCount * scale;
canvas.height = this.rowCount * scale;
const ctx = canvas.getContext('2d');
for (let layer of this.layers) {
for (let row = 0; row < this.rowCount; row++) {
for (let col = 0; col < this.colCount; col++) {
const color = layer.cells[row][col];
if (color.a > 0) {
ctx.fillStyle = `rgba(${color.r},${color.g},${color.b},${color.a / 255})`;
ctx.fillRect(col * scale, row * scale, scale, scale);
}
}
}
}
return canvas.toDataURL();
}
appendTo(container) {
container.appendChild(this.canvas);
}
exportAsJson() {
const colorArray = this.layers.flatMap(layer =>
layer.cells.flatMap(row =>
row.map(color => ({ r: color.r, g: color.g, b: color.b, a: color.a }))
)
);
return JSON.stringify(colorArray);
}
exportAsHexArray() {
const hexArray = this.layers.flatMap(layer =>
layer.cells.flatMap(row =>
row.map(color => color.toHex())
)
);
return JSON.stringify(hexArray);
}
updateGridOutput() {
const jsonOutput = this.exportAsJson();
const hexOutput = this.exportAsHexArray();
document.getElementById('gridOutput').value = `Color Objects:\n${jsonOutput}\n\nHex Array:\n${hexOutput}`;
}
}
// Helper functions for color extraction
function extractColorsFrequency(imageData, limit) {
const colorCounts = {};
for (let i = 0; i < imageData.data.length; i += 4) {
const color = `${imageData.data[i]},${imageData.data[i+1]},${imageData.data[i+2]}`;
colorCounts[color] = (colorCounts[color] || 0) + 1;
}
return Object.entries(colorCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, limit)
.map(([color]) => {
const [r, g, b] = color.split(',').map(Number);
return new Color(r, g, b);
});
}
function extractColorsSkip(imageData, limit) {
const colors = extractColorsFrequency(imageData, imageData.width * imageData.height);
const skip = Math.max(1, Math.floor(colors.length / limit));
return colors.filter((_, index) => index % skip === 0).slice(0, limit);
}
// KMeans関数を置き換え
function extractColorsKmeans(imageData, k) {
const points = [];
for (let i = 0; i < imageData.data.length; i += 4) {
points.push([imageData.data[i], imageData.data[i+1], imageData.data[i+2]]);
}
const centroids = simpleKMeans(points, k);
return centroids.map(centroid => new Color(Math.round(centroid[0]), Math.round(centroid[1]), Math.round(centroid[2])));
}
// Create and display the grid and palette
const gridContainer = document.getElementById('gridContainer');
const grid = new Grid(10, 10, 500, 500);
grid.appendTo(gridContainer);
grid.draw();
grid.updateLayerList();
const paletteContainer = document.getElementById('paletteContainer');
const palette = new Palette(4, 4, 200, 200);
palette.appendTo(paletteContainer);
// Event listeners for new functionality
document.getElementById('loadPaletteBtn').addEventListener('click', () => {
const input = document.getElementById('paletteInput').value;
let colors = [];
try {
if (input.startsWith('#')) {
// Hex string input
colors = input.split(/\s+/).map(hex => Color.fromHex(hex));
} else {
const parsed = JSON.parse(input);
if (Array.isArray(parsed)) {
if (typeof parsed[0] === 'string') {
// Array of hex strings
colors = parsed.map(hex => Color.fromHex(hex));
} else if (typeof parsed[0] === 'object') {
// Array of color objects
colors = parsed.map(obj => new Color(obj.r, obj.g, obj.b, obj.a));
}
} else if (typeof parsed === 'object') {
// Single color object
colors = [new Color(parsed.r, parsed.g, parsed.b, parsed.a)];
}
}
palette.setColors(colors);
} catch (e) {
console.error("Failed to parse input:", e);
alert("Failed to parse input. Please check the format and try again.");
}
});
document.getElementById('imageInput').addEventListener('change', (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const method = document.getElementById('extractMethod').value;
let colors;
try {
switch (method) {
case 'frequency':
colors = extractColorsFrequency(imageData, 16);
break;
case 'skip':
colors = extractColorsSkip(imageData, 16);
break;
case 'kmeans':
colors = extractColorsKmeans(imageData, 16);
break;
default:
throw new Error("Invalid extraction method");
}
palette.setColors(colors);
} catch (error) {
console.error("Error extracting colors:", error);
alert("Failed to extract colors from the image. Please try again or use a different method.");
}
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
});
document.getElementById('copyToClipboardBtn').addEventListener('click', () => {
const output = document.getElementById('gridOutput');
output.select();
document.execCommand('copy');
alert("Grid data copied to clipboard!");
});
document.getElementById('exportTxtBtn').addEventListener('click', () => {
const output = document.getElementById('gridOutput').value;
const blob = new Blob([output], {type: 'text/plain'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'grid_colors.txt';
a.click();
URL.revokeObjectURL(url);
});
document.getElementById('resizeBtn').addEventListener('click', () => {
const newSize = prompt("Enter new grid size (e.g., '20,20' for 20x20 grid):", `${grid.rowCount},${grid.colCount}`);
if (newSize) {
const [rows, cols] = newSize.split(',').map(Number);
if (rows > 0 && cols > 0) {
grid.resize(rows, cols, grid.width, grid.height);
} else {
alert("Invalid grid size. Please enter positive numbers.");
}
}
});
document.getElementById('colorPicker').addEventListener('input', (event) => {
grid.currentColor = Color.fromHex(event.target.value);
});
document.querySelectorAll('.tool-btn').forEach(btn => {
btn.addEventListener('click', (event) => {
grid.tool = event.target.dataset.tool;
grid.lineStartCell = null;
});
});
document.getElementById('undoBtn').addEventListener('click', () => grid.undo());
document.getElementById('redoBtn').addEventListener('click', () => grid.redo());
document.getElementById('addLayerBtn').addEventListener('click', () => grid.addLayer());
document.getElementById('removeLayerBtn').addEventListener('click', () => grid.removeLayer());
document.getElementById('exportBtn').addEventListener('click', () => {
const dataURL = grid.export(grid.cellWidth);
window.open(dataURL);
});
document.getElementById('exportPixelBtn').addEventListener('click', () => {
const dataURL = grid.export(1);
window.open(dataURL);
});
function simpleKMeans(points, k, maxIterations = 20) {
// ランダムに初期中心点を選択
let centroids = points.slice(0, k);
for (let iter = 0; iter < maxIterations; iter++) {
// 各点を最も近い中心点に割り当て
const clusters = Array(k).fill().map(() => []);
for (const point of points) {
let minDist = Infinity;
let closestCentroidIndex = 0;
for (let i = 0; i < k; i++) {
const dist = euclideanDistance(point, centroids[i]);
if (dist < minDist) {
minDist = dist;
closestCentroidIndex = i;
}
}
clusters[closestCentroidIndex].push(point);
}
// 新しい中心点を計算
const newCentroids = clusters.map(cluster =>
cluster.length > 0 ? averagePoint(cluster) : centroids[0]
);
// 収束チェック
if (centroids.every((centroid, i) =>
euclideanDistance(centroid, newCentroids[i]) < 0.001
)) {
break;
}
centroids = newCentroids;
}
return centroids;
}
function euclideanDistance(p1, p2) {
return Math.sqrt(p1.reduce((sum, v, i) => sum + Math.pow(v - p2[i], 2), 0));
}
function averagePoint(points) {
const sum = points.reduce((acc, p) => acc.map((v, i) => v + p[i]), Array(points[0].length).fill(0));
return sum.map(v => v / points.length);
}
// Initial grid output update
grid.updateGridOutput();
function resizeGridAndPalette() {
console.log('Resizing grid and palette');
try {
const mainContainer = document.getElementById('mainContainer');
const totalWidth = mainContainer.clientWidth;
const gridWidth = Math.floor(totalWidth * 0.7); // 70% of the container width
const paletteWidth = Math.floor(totalWidth * 0.25); // 25% of the container width
// Resize the existing grid
grid.resize(grid.rowCount, grid.colCount, gridWidth, gridWidth);
grid.draw(); // Redraw the grid after resizing
// Update the existing palette instead of creating a new one
palette.canvas.width = paletteWidth;
palette.canvas.height = paletteWidth;
palette.cellWidth = paletteWidth / palette.colCount;
palette.cellHeight = paletteWidth / palette.rowCount;
palette.draw(); // Redraw the palette after updating dimensions
console.log('Grid and palette resized');
} catch (error) {
console.error('Error in resizeGridAndPalette:', error);
}
}
window.addEventListener('resize', resizeGridAndPalette);
resizeGridAndPalette(); // Initial call to set sizes
</script>
</body>
</html>
JSON出力
# @title 最新改造中
%%html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Improved Interactive Canvas Grid with Palette</title>
<style>
body {
display: flex;
flex-direction: column;
align-items: center;
font-family: Arial, sans-serif;
}
#mainContainer {
display: flex;
justify-content: center;
width: 100%;
max-width: 1200px;
}
#gridContainer, #paletteContainer {
border: 1px solid #000;
margin: 20px;
}
#controls, #layerControls {
display: flex;
flex-direction: row;
text-align: center;
/* margin-bottom: 20px; */
width: 100%;
}
#gridControls {
text-align: center;
/* margin-bottom: 20px; */
width: 100%;
}
#paletteControls {
text-align: center;
/* margin-bottom: 20px; */
width: 100%;
}
.tool-btn, .layer-btn {
/* margin: 0 5px; */
/* padding: 5px 10px; */
background-color: #f0f0f0;
border: 1px solid #ccc;
border-radius: 3px;
cursor: pointer;
}
#layerList {
list-style-type: none;
padding: 0;
text-align: left;
}
#layerList li {
margin: 5px 0;
padding: 5px;
background-color: #f0f0f0;
border-radius: 3px;
}
#paletteInput, #gridOutput {
width: 100%;
height: 100px;
margin-bottom: 10px;
resize: vertical;
}
#gridContainer canvas, #paletteContainer canvas {
max-width: 100%;
height: auto;
}
.output-container {
display: flex;
justify-content: space-between;
margin-top: 20px;
}
.output-box {
width: 48%;
}
.output-textarea {
width: 100%;
height: 150px;
margin-bottom: 10px;
}
</style>
</head>
<body>
<div id="controls">
<button id="resizeBtn" class="tool-btn">Resize Grid</button>
<input type="color" id="colorPicker" value="#000000">
<button class="tool-btn" data-tool="point">Point</button>
<button class="tool-btn" data-tool="pen">Pen</button>
<button class="tool-btn" data-tool="eraser">Eraser</button>
<button class="tool-btn" data-tool="line">Line</button>
<button id="undoBtn" class="tool-btn">Undo</button>
<button id="redoBtn" class="tool-btn">Redo</button>
<button id="exportBtn" class="tool-btn">Export (1:1)</button>
<button id="exportPixelBtn" class="tool-btn">Export (1 cell = 1 pixel)</button>
</div>
<div id="layerControls">
<button id="addLayerBtn" class="layer-btn">Add Layer</button>
<button id="removeLayerBtn" class="layer-btn">Remove Layer</button>
<ul id="layerList"></ul>
</div>
<div id="mainContainer">
<div id="gridContainer"></div>
<div id="paletteContainer"></div>
</div>
<div id="paletteControls">
<h3>グリッドControl</h3>
</div>
<div id="paletteControls">
<h3>パレットControl</h3>
<textarea id="paletteInput" placeholder="Enter hex colors, JSON array, or paste image URL"></textarea>
<button id="loadPaletteBtn" class="tool-btn">Load Palette</button>
<select id="extractMethod">
<option value="frequency">Frequency</option>
<option value="skip">Skip Frequency</option>
<option value="kmeans">K-means</option>
</select>
<input type="file" id="imageInput" accept="image/*">
</div>
<div class="output-container">
<div class="output-box">
<h3>カラーオブジェクト出力</h3>
<textarea id="jsonOutput" class="output-textarea" readonly></textarea>
<button id="copyJsonBtn" class="tool-btn">クリップボードにコピー</button>
<button id="exportJsonBtn" class="tool-btn">JSONとしてエクスポート</button>
</div>
<div class="output-box">
<h3>16進数配列出力</h3>
<textarea id="hexOutput" class="output-textarea" readonly></textarea>
<button id="copyHexBtn" class="tool-btn">クリップボードにコピー</button>
<button id="exportHexBtn" class="tool-btn">TXTとしてエクスポート</button>
</div>
</div>
<script>
class Color {
constructor(r, g, b, a = 255) {
this.r = r;
this.g = g;
this.b = b;
this.a = a;
}
// static fromHex(hex) {
// const r = parseInt(hex.slice(1, 3), 16);
// const g = parseInt(hex.slice(3, 5), 16);
// const b = parseInt(hex.slice(5, 7), 16);
// return new Color(r, g, b);
// }
// toHex() {
// return `#${this.r.toString(16).padStart(2, '0')}${this.g.toString(16).padStart(2, '0')}${this.b.toString(16).padStart(2, '0')}`;
// }
}
// Helper function to convert hex to Color object
function hexToColor(hex) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return new Color(r, g, b);
}
// Helper function to convert Color object to hex
function colorToHex(color) {
return `#${color.r.toString(16).padStart(2, '0')}${color.g.toString(16).padStart(2, '0')}${color.b.toString(16).padStart(2, '0')}`;
}
class Layer {
constructor(rowCount, colCount) {
this.cells = Array(rowCount).fill().map(() => Array(colCount).fill(new Color(255, 255, 255, 0)));
}
}
class Palette {
constructor(rowCount, colCount, width, height) {
this.canvas = document.createElement('canvas');
this.canvas.width = width;
this.canvas.height = height;
this.ctx = this.canvas.getContext("2d");
this.rowCount = rowCount;
this.colCount = colCount;
this.cellWidth = width / this.colCount;
this.cellHeight = height / this.rowCount;
this.colors = [];
this.canvas.addEventListener('click', this.handleClick.bind(this));
}
//from hex
setColors(colors) {
this.colors = colors.slice(0, this.rowCount * this.colCount);
this.draw();
}
// draw() {
// this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// for (let i = 0; i < this.colors.length; i++) {
// const row = Math.floor(i / this.colCount);
// const col = i % this.colCount;
// const color = this.colors[i];
// this.ctx.fillStyle = `rgb(${color.r},${color.g},${color.b})`;
// this.ctx.fillRect(col * this.cellWidth, row * this.cellHeight, this.cellWidth, this.cellHeight);
// }
// // Draw grid lines
// this.ctx.strokeStyle = '#000000';
// this.ctx.lineWidth = 1;
// for (let i = 1; i < this.colCount; i++) {
// this.ctx.beginPath();
// this.ctx.moveTo(i * this.cellWidth, 0);
// this.ctx.lineTo(i * this.cellWidth, this.canvas.height);
// this.ctx.stroke();
// }
// for (let i = 1; i < this.rowCount; i++) {
// this.ctx.beginPath();
// this.ctx.moveTo(0, i * this.cellHeight);
// this.ctx.lineTo(this.canvas.width, i * this.cellHeight);
// this.ctx.stroke();
// }
// }
draw() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// Canas Draw
for (let i = 0; i < this.colors.length; i++) {
const row = Math.floor(i / this.colCount);
const col = i % this.colCount;
const color = this.colors[i];
this.ctx.fillStyle = `rgb(${color.r},${color.g},${color.b})`;
this.ctx.fillRect(col * this.cellWidth, row * this.cellHeight, this.cellWidth, this.cellHeight);
}
// Draw grid lines
this.ctx.strokeStyle = '#000000';
this.ctx.lineWidth = 1;
for (let i = 0; i <= this.colCount; i++) {
this.ctx.beginPath();
this.ctx.moveTo(i * this.cellWidth, 0);
this.ctx.lineTo(i * this.cellWidth, this.canvas.height);
this.ctx.stroke();
}
for (let i = 0; i <= this.rowCount; i++) {
this.ctx.beginPath();
this.ctx.moveTo(0, i * this.cellHeight);
this.ctx.lineTo(this.canvas.width, i * this.cellHeight);
this.ctx.stroke();
}
}
handleClick(event) {
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const col = Math.floor(x / this.cellWidth);
const row = Math.floor(y / this.cellHeight);
const index = row * this.colCount + col;
if (index < this.colors.length) {
const selectedColor = this.colors[index];
//document.getElementById('colorPicker').value = selectedColor.toHex();
//document.getElementById('colorPicker').value =rgbToHex(selectedColor.r, selectedColor.g, selectedColor.b);
document.getElementById('colorPicker').value = colorToHex(selectedColor);
grid.currentColor = selectedColor;
}
}
appendTo(container) {
container.appendChild(this.canvas);
}
}
class Grid {
constructor(rowCount, colCount, width, height) {
this.canvas = document.createElement('canvas');
this.canvas.width = width;
this.canvas.height = height;
this.ctx = this.canvas.getContext("2d");
this.currentColor = new Color(0, 0, 0);
this.rowCount = rowCount;
this.colCount = colCount;
this._cellWidth = width / this.colCount;
this._cellHeight = height / this.rowCount;
this.layers = [new Layer(rowCount, colCount)];
this.currentLayerIndex = 0;
this.cellInitColor = new Color(255, 255, 255, 0);
this.onGridLine = true;
this.lineColor = '#000000';
this.lineWidth = 1;
this.isSquareCell = true;
this.dirtyRegion = new Set();
this.tool = 'point';
this.isDrawing = false;
this.lastCell = null;
this.lineStartCell = null;
this.history = [];
this.historyIndex = -1;
this.canvas.addEventListener('mousedown', this.handleMouseDown.bind(this));
this.canvas.addEventListener('mousemove', this.handleMouseMove.bind(this));
this.canvas.addEventListener('mouseup', this.handleMouseUp.bind(this));
this.canvas.addEventListener('mouseleave', this.handleMouseUp.bind(this));
}
get currentLayer() {
return this.layers[this.currentLayerIndex];
}
get width() {
return this.canvas.width;
}
get height() {
return this.canvas.height;
}
get lowerEdge() {
const cw = this.width / this.colCount;
const ch = this.height / this.rowCount;
return Math.min(cw, ch);
}
get cellWidth() {
return this.isSquareCell ? this.lowerEdge : this._cellWidth;
}
get cellHeight() {
return this.isSquareCell ? this.lowerEdge : this._cellHeight;
}
get gridWidth() {
return this.cellWidth * this.colCount;
}
get gridHeight() {
return this.cellHeight * this.rowCount;
}
getGridIndex(x, y) {
const col = Math.floor(x / this.cellWidth);
const row = Math.floor(y / this.cellHeight);
return { row, col };
}
setColorAtIndex(row, col, color) {
if (!this.isValidIndex(row, col)) return false;
const action = {
type: 'setColor',
layerIndex: this.currentLayerIndex,
row, col,
oldColor: this.currentLayer.cells[row][col],
newColor: color
};
this.addToHistory(action);
this.applyAction(action);
this.updateGridOutput();
return true;
}
getColorAtIndex(row, col) {
if (!this.isValidIndex(row, col)) return null;
return this.currentLayer.cells[row][col];
}
isValidIndex(row, col) {
return row >= 0 && row < this.rowCount && col >= 0 && col < this.colCount;
}
applyAction(action) {
switch (action.type) {
case 'setColor':
this.layers[action.layerIndex].cells[action.row][action.col] = action.newColor;
this.dirtyRegion.add(`${action.row},${action.col}`);
break;
}
}
addToHistory(action) {
this.history = this.history.slice(0, this.historyIndex + 1);
this.history.push(action);
this.historyIndex++;
}
undo() {
if (this.historyIndex < 0) return;
const action = this.history[this.historyIndex];
this.revertAction(action);
this.historyIndex--;
this.draw();
this.updateGridOutput();
}
redo() {
if (this.historyIndex >= this.history.length - 1) return;
this.historyIndex++;
const action = this.history[this.historyIndex];
this.applyAction(action);
this.draw();
this.updateGridOutput();
}
revertAction(action) {
switch (action.type) {
case 'setColor':
this.layers[action.layerIndex].cells[action.row][action.col] = action.oldColor;
this.dirtyRegion.add(`${action.row},${action.col}`);
break;
}
}
drawCell(row, col, color) {
this.ctx.fillStyle = `rgba(${color.r},${color.g},${color.b},${color.a / 255})`;
this.ctx.fillRect(col * this.cellWidth, row * this.cellHeight, this.cellWidth, this.cellHeight);
}
drawCells() {
for (let layer of this.layers) {
for (let row = 0; row < this.rowCount; row++) {
for (let col = 0; col < this.colCount; col++) {
const color = layer.cells[row][col];
if (color.a > 0) {
this.drawCell(row, col, color);
}
}
}
}
}
drawGridLine(sx, sy, ex, ey) {
this.ctx.strokeStyle = this.lineColor;
this.ctx.lineWidth = this.lineWidth;
this.ctx.beginPath();
this.ctx.moveTo(sx, sy);
this.ctx.lineTo(ex, ey);
this.ctx.stroke();
}
drawGridLines() {
const we = this.colCount * this.cellWidth;
const he = this.rowCount * this.cellHeight;
for (let row = 0; row <= this.rowCount; row++) {
this.drawGridLine(0, row * this.cellHeight, we, row * this.cellHeight);
}
for (let col = 0; col <= this.colCount; col++) {
this.drawGridLine(col * this.cellWidth, 0, col * this.cellWidth, he);
}
}
draw() {
this.ctx.clearRect(0, 0, this.width, this.height);
for (let layer of this.layers) {
for (let row = 0; row < this.rowCount; row++) {
for (let col = 0; col < this.colCount; col++) {
const color = layer.cells[row][col];
if (color.a > 0) {
this.drawCell(row, col, color);
}
}
}
}
if (this.onGridLine) {
this.drawGridLines();
}
}
clearCells() {
this.ctx.fillStyle = `rgba(${this.cellInitColor.r},${this.cellInitColor.g},${this.cellInitColor.b},${this.cellInitColor.a / 255})`;
this.ctx.fillRect(0, 0, this.width, this.height);
}
resize(newRowCount, newColCount, newWidth, newHeight) {
const oldCells = this.layers.map(layer => layer.cells);
const oldRowCount = this.rowCount;
const oldColCount = this.colCount;
// Update dimensions
this.rowCount = newRowCount;
this.colCount = newColCount;
this.canvas.width = newWidth;
this.canvas.height = newHeight;
this._cellWidth = newWidth / newColCount;
this._cellHeight = newHeight / newRowCount;
// Resize all layers
this.layers = this.layers.map(layer => {
const newLayer = new Layer(newRowCount, newColCount);
const minRowCount = Math.min(oldRowCount, newRowCount);
const minColCount = Math.min(oldColCount, newColCount);
for (let r = 0; r < minRowCount; r++) {
for (let c = 0; c < minColCount; c++) {
newLayer.cells[r][c] = layer.cells[r][c];
}
}
return newLayer;
});
// Reset history as resize operation can't be undone
this.history = [];
this.historyIndex = -1;
// Redraw
this.draw();
this.updateGridOutput();
}
drawLine(startCell, endCell) {
const dx = Math.abs(endCell.col - startCell.col);
const dy = Math.abs(endCell.row - startCell.row);
const sx = startCell.col < endCell.col ? 1 : -1;
const sy = startCell.row < endCell.row ? 1 : -1;
let err = dx - dy;
let currentCell = {...startCell};
while (true) {
this.setColorAtIndex(currentCell.row, currentCell.col, this.currentColor);
if (currentCell.col === endCell.col && currentCell.row === endCell.row) break;
const e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
currentCell.col += sx;
}
if (e2 < dx) {
err += dx;
currentCell.row += sy;
}
}
}
handleMouseDown(event) {
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const cell = this.getGridIndex(x, y);
switch (this.tool) {
case 'point':
case 'pen':
case 'eraser':
this.isDrawing = true;
this.setColorAtIndex(cell.row, cell.col, this.tool === 'eraser' ? this.cellInitColor : this.currentColor);
this.lastCell = cell;
break;
case 'line':
if (!this.lineStartCell) {
this.lineStartCell = cell;
} else {
this.drawLine(this.lineStartCell, cell);
this.lineStartCell = null;
}
break;
}
this.draw();
}
handleMouseMove(event) {
if (!this.isDrawing || (this.tool !== 'pen' && this.tool !== 'eraser')) return;
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const cell = this.getGridIndex(x, y);
if (cell.row !== this.lastCell.row || cell.col !== this.lastCell.col) {
this.drawLine(this.lastCell, cell);
this.lastCell = cell;
this.draw();
}
}
handleMouseUp() {
this.isDrawing = false;
}
addLayer() {
this.layers.push(new Layer(this.rowCount, this.colCount));
this.currentLayerIndex = this.layers.length - 1;
this.updateLayerList();
}
removeLayer() {
if (this.layers.length > 1) {
this.layers.splice(this.currentLayerIndex, 1);
this.currentLayerIndex = Math.min(this.currentLayerIndex, this.layers.length - 1);
this.updateLayerList();
this.draw();
}
}
updateLayerList() {
const layerList = document.getElementById('layerList');
layerList.innerHTML = '';
this.layers.forEach((layer, index) => {
const li = document.createElement('li');
const radio = document.createElement('input');
radio.type = 'radio';
radio.name = 'layer';
radio.value = index;
radio.checked = index === this.currentLayerIndex;
radio.addEventListener('change', () => {
this.currentLayerIndex = index;
});
li.appendChild(radio);
li.appendChild(document.createTextNode(`Layer ${index + 1}`));
layerList.appendChild(li);
});
}
export(scale = 1) {
const canvas = document.createElement('canvas');
canvas.width = this.colCount * scale;
canvas.height = this.rowCount * scale;
const ctx = canvas.getContext('2d');
for (let layer of this.layers) {
for (let row = 0; row < this.rowCount; row++) {
for (let col = 0; col < this.colCount; col++) {
const color = layer.cells[row][col];
if (color.a > 0) {
ctx.fillStyle = `rgba(${color.r},${color.g},${color.b},${color.a / 255})`;
ctx.fillRect(col * scale, row * scale, scale, scale);
}
}
}
}
return canvas.toDataURL();
}
appendTo(container) {
container.appendChild(this.canvas);
}
exportAsJson() {
const colorArray = this.layers.flatMap(layer =>
layer.cells.flatMap(row =>
row.map(color => ({ r: color.r, g: color.g, b: color.b, a: color.a }))
)
);
return JSON.stringify(colorArray);
}
exportAsHexArray() {
const hexArray = this.layers.flatMap(layer =>
layer.cells.flatMap(row =>
//row.map(color => color.toHex())
//row.map(color =>rgbToHex(color.r, color.g, color.b))
row.map(color => colorToHex(color))
)
);
return JSON.stringify(hexArray);
}
// updateGridOutput() {
// const jsonOutput = this.exportAsJson();
// const hexOutput = this.exportAsHexArray();
// document.getElementById('gridOutput').value = `Color Objects:\n${jsonOutput}\n\nHex Array:\n${hexOutput}`;
// }
updateGridOutput() {
const jsonOutput = this.exportAsJson();
const hexOutput = this.exportAsHexArray();
document.getElementById('jsonOutput').value = jsonOutput;
document.getElementById('hexOutput').value = hexOutput;
}
}//Grid
// Helper functions for color extraction
function extractColorsFrequency(imageData, limit) {
const colorCounts = {};
for (let i = 0; i < imageData.data.length; i += 4) {
const color = `${imageData.data[i]},${imageData.data[i+1]},${imageData.data[i+2]}`;
colorCounts[color] = (colorCounts[color] || 0) + 1;
}
return Object.entries(colorCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, limit)
.map(([color]) => {
const [r, g, b] = color.split(',').map(Number);
return new Color(r, g, b);
});
}
function extractColorsSkip(imageData, limit) {
const colors = extractColorsFrequency(imageData, imageData.width * imageData.height);
const skip = Math.max(1, Math.floor(colors.length / limit));
return colors.filter((_, index) => index % skip === 0).slice(0, limit);
}
// function extractColorsKmeans(imageData, k) {
// const points = [];
// for (let i = 0; i < imageData.data.length; i += 4) {
// points.push([imageData.data[i], imageData.data[i+1], imageData.data[i+2]]);
// }
// const kmeans = new KMeans({k, maxIterations: 20});
// const clusters = kmeans.cluster(points);
// return clusters.centroids.map(centroid => new Color(Math.round(centroid[0]), Math.round(centroid[1]), Math.round(centroid[2])));
// }
// KMeans関数を置き換え
function extractColorsKmeans(imageData, k) {
const points = [];
for (let i = 0; i < imageData.data.length; i += 4) {
points.push([imageData.data[i], imageData.data[i+1], imageData.data[i+2]]);
}
const centroids = simpleKMeans(points, k);
return centroids.map(centroid => new Color(Math.round(centroid[0]), Math.round(centroid[1]), Math.round(centroid[2])));
}
// Event listeners for new functionality
document.getElementById('loadPaletteBtn').addEventListener('click', () => {
const input = document.getElementById('paletteInput').value;
let colors = [];
try {
if (input.startsWith('#')) {
// Hex string input
colors = input.split(/\s+/).map(hex => Color.fromHex(hex));
} else {
const parsed = JSON.parse(input);
console.log(parsed);
if (Array.isArray(parsed)) {
if (typeof parsed[0] === 'string') {
// Array of hex strings
//colors = parsed.map(hex => Color.fromHex(hex));
colors = input.split(/\s+/).map(hex => hexToColor(hex));
} else if (typeof parsed[0] === 'object') {
// Array of color objects
colors = parsed.map(obj => new Color(obj.r, obj.g, obj.b, obj.a));
}
} else if (typeof parsed === 'object') {
// Single color object
colors = [new Color(parsed.r, parsed.g, parsed.b, parsed.a)];
}
}
palette.setColors(colors);
} catch (e) {
console.error("Failed to parse input:", e);
alert("Failed to parse input. Please check the format and try again.");
}
});
document.getElementById('imageInput').addEventListener('change', (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const method = document.getElementById('extractMethod').value;
let colors;
try {
switch (method) {
case 'frequency':
colors = extractColorsFrequency(imageData, 16);
break;
case 'skip':
colors = extractColorsSkip(imageData, 16);
break;
case 'kmeans':
colors = extractColorsKmeans(imageData, 16);
break;
default:
throw new Error("Invalid extraction method");
}
palette.setColors(colors);
} catch (error) {
console.error("Error extracting colors:", error);
alert("Failed to extract colors from the image. Please try again or use a different method.");
}
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
});
// document.getElementById('copyToClipboardBtn').addEventListener('click', () => {
// const output = document.getElementById('gridOutput');
// output.select();
// document.execCommand('copy');
// alert("Grid data copied to clipboard!");
// });
// document.getElementById('exportTxtBtn').addEventListener('click', () => {
// const output = document.getElementById('gridOutput').value;
// const blob = new Blob([output], {type: 'text/plain'});
// const url = URL.createObjectURL(blob);
// const a = document.createElement('a');
// a.href = url;
// a.download = 'grid_colors.txt';
// a.click();
// URL.revokeObjectURL(url);
// });
document.getElementById('resizeBtn').addEventListener('click', () => {
const newSize = prompt("Enter new grid size (e.g., '20,20' for 20x20 grid):", `${grid.rowCount},${grid.colCount}`);
if (newSize) {
const [rows, cols] = newSize.split(',').map(Number);
if (rows > 0 && cols > 0) {
grid.resize(rows, cols, grid.width, grid.height);
} else {
alert("Invalid grid size. Please enter positive numbers.");
}
}
});
document.getElementById('colorPicker').addEventListener('input', (event) => {
//grid.currentColor = Color.fromHex(event.target.value);
grid.currentColor = hexToColor(event.target.value);
});
document.querySelectorAll('.tool-btn').forEach(btn => {
btn.addEventListener('click', (event) => {
grid.tool = event.target.dataset.tool;
grid.lineStartCell = null;
});
});
document.getElementById('undoBtn').addEventListener('click', () => grid.undo());
document.getElementById('redoBtn').addEventListener('click', () => grid.redo());
document.getElementById('addLayerBtn').addEventListener('click', () => grid.addLayer());
document.getElementById('removeLayerBtn').addEventListener('click', () => grid.removeLayer());
document.getElementById('exportBtn').addEventListener('click', () => {
const dataURL = grid.export(grid.cellWidth);
window.open(dataURL);
});
document.getElementById('exportPixelBtn').addEventListener('click', () => {
const dataURL = grid.export(1);
window.open(dataURL);
});
document.getElementById('copyJsonBtn').addEventListener('click', () => {
const output = document.getElementById('jsonOutput');
output.select();
document.execCommand('copy');
alert("カラーオブジェクトデータがクリップボードにコピーされました!");
});
document.getElementById('exportJsonBtn').addEventListener('click', () => {
const output = document.getElementById('jsonOutput').value;
const blob = new Blob([output], {type: 'application/json'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'grid_colors.json';
a.click();
URL.revokeObjectURL(url);
});
document.getElementById('copyHexBtn').addEventListener('click', () => {
const output = document.getElementById('hexOutput');
output.select();
document.execCommand('copy');
alert("16進数配列データがクリップボードにコピーされました!");
});
document.getElementById('exportHexBtn').addEventListener('click', () => {
const output = document.getElementById('hexOutput').value;
const blob = new Blob([output], {type: 'text/plain'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'grid_colors_hex.txt';
a.click();
URL.revokeObjectURL(url);
});
// 既存のイベントリスナーを削除
// document.getElementById('copyToClipboardBtn').remove();
// document.getElementById('exportTxtBtn').remove();
function simpleKMeans(points, k, maxIterations = 20) {
// ランダムに初期中心点を選択
let centroids = points.slice(0, k);
for (let iter = 0; iter < maxIterations; iter++) {
// 各点を最も近い中心点に割り当て
const clusters = Array(k).fill().map(() => []);
for (const point of points) {
let minDist = Infinity;
let closestCentroidIndex = 0;
for (let i = 0; i < k; i++) {
const dist = euclideanDistance(point, centroids[i]);
if (dist < minDist) {
minDist = dist;
closestCentroidIndex = i;
}
}
clusters[closestCentroidIndex].push(point);
}
// 新しい中心点を計算
const newCentroids = clusters.map(cluster =>
cluster.length > 0 ? averagePoint(cluster) : centroids[0]
);
// 収束チェック
if (centroids.every((centroid, i) =>
euclideanDistance(centroid, newCentroids[i]) < 0.001
)) {
break;
}
centroids = newCentroids;
}
return centroids;
}
function euclideanDistance(p1, p2) {
return Math.sqrt(p1.reduce((sum, v, i) => sum + Math.pow(v - p2[i], 2), 0));
}
function averagePoint(points) {
const sum = points.reduce((acc, p) => acc.map((v, i) => v + p[i]), Array(points[0].length).fill(0));
return sum.map(v => v / points.length);
}
// Responsive design
// function resizeGridAndPalette() {
// const mainContainer = document.getElementById('mainContainer');
// const totalWidth = mainContainer.clientWidth;
// const gridWidth = Math.floor(totalWidth * 0.7); // 70% of the container width
// const paletteWidth = Math.floor(totalWidth * 0.25); // 25% of the container width
// grid.resize(grid.rowCount, grid.colCount, gridWidth, gridWidth);
// palette = new Palette(4, 4, paletteWidth, paletteWidth);
// paletteContainer.innerHTML = '';
// palette.appendTo(paletteContainer);
// }
// function resizeGridAndPalette() {
// const mainContainer = document.getElementById('mainContainer');
// const totalWidth = mainContainer.clientWidth;
// const gridWidth = Math.floor(totalWidth * 0.7); // 70% of the container width
// const paletteWidth = Math.floor(totalWidth * 0.25); // 25% of the container width
// // Resize the existing grid
// grid.resize(grid.rowCount, grid.colCount, gridWidth, gridWidth);
// grid.draw(); // Redraw the grid after resizing
// // Recreate the palette
// paletteContainer.innerHTML = '';
// palette = new Palette(4, 4, paletteWidth, paletteWidth);
// palette.appendTo(paletteContainer);
// palette.setColors(palette.colors); // Restore the colors
// palette.draw(); // Draw the palette
// }
// function resizeGridAndPalette() {
// console.log('Resizing grid and palette');
// try {
// const mainContainer = document.getElementById('mainContainer');
// const totalWidth = mainContainer.clientWidth;
// const gridWidth = Math.floor(totalWidth * 0.7); // 70% of the container width
// const paletteWidth = Math.floor(totalWidth * 0.25); // 25% of the container width
// // Resize the existing grid
// grid.resize(grid.rowCount, grid.colCount, gridWidth, gridWidth);
// grid.draw(); // Redraw the grid after resizing
// // Update the existing palette instead of creating a new one
// palette.canvas.width = paletteWidth;
// palette.canvas.height = paletteWidth;
// palette.cellWidth = paletteWidth / palette.colCount;
// palette.cellHeight = paletteWidth / palette.rowCount;
// palette.draw(); // Redraw the palette after updating dimensions
// console.log('Grid and palette resized');
// } catch (error) {
// console.error('Error in resizeGridAndPalette:', error);
// }
// }
// カラーオブジェクトを作成する関数
function createColorObject(r, g, b) {
return { r, g, b, a: 255 };
}
// RGBカラーをヘックス形式に変換する関数
function rgbToHex(r, g, b) {
return '#' + [r, g, b].map(x => {
const hex = x.toString(16);
return hex.length === 1 ? '0' + hex : hex;
}).join('');
}
// RGカラー配列生成関数
function generateRGColorArray(rRowCount, gColCount) {
const colorArray = [];
for (let r = 0; r < rRowCount; r++) {
for (let g = 0; g < gColCount; g++) {
const rValue = Math.round((r / (rRowCount - 1)) * 255);
const gValue = Math.round((g / (gColCount - 1)) * 255);
colorArray.push(createColorObject(rValue, gValue, 0));
}
}
return colorArray;
}
// GBカラー配列生成関数
function generateGBColorArray(gRowCount, bColCount) {
const colorArray = [];
for (let g = 0; g < gRowCount; g++) {
for (let b = 0; b < bColCount; b++) {
const gValue = Math.round((g / (gRowCount - 1)) * 255);
const bValue = Math.round((b / (bColCount - 1)) * 255);
colorArray.push(createColorObject(0, gValue, bValue));
}
}
return colorArray;
}
// BRカラー配列生成関数
function generateBRColorArray(bRowCount, rColCount) {
const colorArray = [];
for (let b = 0; b < bRowCount; b++) {
for (let r = 0; r < rColCount; r++) {
const bValue = Math.round((b / (bRowCount - 1)) * 255);
const rValue = Math.round((r / (rColCount - 1)) * 255);
colorArray.push(createColorObject(rValue, 0, bValue));
}
}
return colorArray;
}
// RGヘックス配列生成関数
function generateRGHexArray(rRowCount, gColCount) {
const hexArray = [];
for (let r = 0; r < rRowCount; r++) {
for (let g = 0; g < gColCount; g++) {
const rValue = Math.round((r / (rRowCount - 1)) * 255);
const gValue = Math.round((g / (gColCount - 1)) * 255);
hexArray.push(rgbToHex(rValue, gValue, 0));
}
}
return hexArray;
}
// GBヘックス配列生成関数
function generateGBHexArray(gRowCount, bColCount) {
const hexArray = [];
for (let g = 0; g < gRowCount; g++) {
for (let b = 0; b < bColCount; b++) {
const gValue = Math.round((g / (gRowCount - 1)) * 255);
const bValue = Math.round((b / (bColCount - 1)) * 255);
hexArray.push(rgbToHex(0, gValue, bValue));
}
}
return hexArray;
}
// BRヘックス配列生成関数
function generateBRHexArray(bRowCount, rColCount) {
const hexArray = [];
for (let b = 0; b < bRowCount; b++) {
for (let r = 0; r < rColCount; r++) {
const bValue = Math.round((b / (bRowCount - 1)) * 255);
const rValue = Math.round((r / (rColCount - 1)) * 255);
hexArray.push(rgbToHex(rValue, 0, bValue));
}
}
return hexArray;
}
// Create and display the grid and palette
const gridContainer = document.getElementById('gridContainer');
const grid = new Grid(16, 16, 500, 500);
grid.appendTo(gridContainer);
grid.draw();
grid.updateLayerList();
const paletteContainer = document.getElementById('paletteContainer');
const palette = new Palette(16, 16, 200, 200);
palette.appendTo(paletteContainer);
palette.setColors(generateRGColorArray(16,16));
// const palette2 = new Palette(16, 16, 200, 200);
// palette2.appendTo(paletteContainer);
// palette2.setColors(generateGBColorArray(16,16));
// const palette3 = new Palette(16, 16, 200, 200);
// palette3.appendTo(paletteContainer);
// palette3.setColors(generateBRColorArray(16,16));
// Initial grid output update
grid.updateGridOutput();
//window.addEventListener('resize', resizeGridAndPalette);
//resizeGridAndPalette(); // Initial call to set sizes
</script>
</body>
</html>
画像入力
# @title 最新改造中2
%%html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Improved Interactive Canvas Grid with Palette</title>
<style>
body {
display: flex;
flex-direction: column;
align-items: center;
font-family: Arial, sans-serif;
}
#mainContainer {
display: flex;
justify-content: center;
width: 100%;
max-width: 1200px;
}
#gridContainer, #paletteContainer {
border: 1px solid #000;
margin: 20px;
}
#controls, #layerControls {
display: flex;
flex-direction: row;
text-align: center;
/* margin-bottom: 20px; */
width: 100%;
}
#gridControls {
/* display: flex;
flex-wrap: wrap; */
justify-content: space-between;
margin-bottom: 10px;
}
#gridControls input {
width: 50px;
}
#imagePasteArea {
width: 200px;
height: 200px;
border: 2px dashed #ccc;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
#paletteControls {
text-align: center;
/* margin-bottom: 20px; */
width: 100%;
}
.tool-btn, .layer-btn {
/* margin: 0 5px; */
/* padding: 5px 10px; */
background-color: #f0f0f0;
border: 1px solid #ccc;
border-radius: 3px;
cursor: pointer;
}
#layerList {
list-style-type: none;
padding: 0;
text-align: left;
}
#layerList li {
margin: 5px 0;
padding: 5px;
background-color: #f0f0f0;
border-radius: 3px;
}
#paletteInput, #gridOutput {
width: 100%;
height: 100px;
margin-bottom: 10px;
resize: vertical;
}
#gridContainer canvas, #paletteContainer canvas {
max-width: 100%;
height: auto;
}
.output-container {
display: flex;
justify-content: space-between;
margin-top: 20px;
}
.output-box {
width: 48%;
}
.output-textarea {
width: 100%;
height: 150px;
margin-bottom: 10px;
}
</style>
</head>
<body>
<div id="controls">
<button id="resizeBtn" class="tool-btn">Resize Grid</button>
<input type="color" id="colorPicker" value="#000000">
<button class="tool-btn" data-tool="point">Point</button>
<button class="tool-btn" data-tool="pen">Pen</button>
<button class="tool-btn" data-tool="eraser">Eraser</button>
<button class="tool-btn" data-tool="line">Line</button>
<button id="undoBtn" class="tool-btn">Undo</button>
<button id="redoBtn" class="tool-btn">Redo</button>
<button id="exportBtn" class="tool-btn">Export (1:1)</button>
<button id="exportPixelBtn" class="tool-btn">Export (1 cell = 1 pixel)</button>
</div>
<div id="layerControls">
<button id="addLayerBtn" class="layer-btn">Add Layer</button>
<button id="removeLayerBtn" class="layer-btn">Remove Layer</button>
<ul id="layerList"></ul>
</div>
<div id="mainContainer">
<div id="gridContainer"></div>
<div id="paletteContainer"></div>
</div>
<div id="gridControls">
<h3>グリッドControl</h3>
<div>
<label for="rowInput">Rows:</label>
<input type="number" id="rowInput" min="1" value="16">
</div>
<div>
<label for="colInput">Columns:</label>
<input type="number" id="colInput" min="1" value="16">
</div>
<button id="updateGridSizeBtn">Update Grid Size</button>
<div id="imagePasteArea">Click or paste image here</div>
</div>
<div id="paletteControls">
<h3>パレットControl</h3>
<textarea id="paletteInput" placeholder="Enter hex colors, JSON array, or paste image URL"></textarea>
<button id="loadPaletteBtn" class="tool-btn">Load Palette</button>
<select id="extractMethod">
<option value="frequency">Frequency</option>
<option value="skip">Skip Frequency</option>
<option value="kmeans">K-means</option>
</select>
<input type="file" id="imageInput" accept="image/*">
</div>
<div class="output-container">
<div class="output-box">
<h3>カラーオブジェクト出力</h3>
<textarea id="jsonOutput" class="output-textarea" readonly></textarea>
<button id="copyJsonBtn" class="tool-btn">クリップボードにコピー</button>
<button id="exportJsonBtn" class="tool-btn">JSONとしてエクスポート</button>
</div>
<div class="output-box">
<h3>16進数配列出力</h3>
<textarea id="hexOutput" class="output-textarea" readonly></textarea>
<button id="copyHexBtn" class="tool-btn">クリップボードにコピー</button>
<button id="exportHexBtn" class="tool-btn">TXTとしてエクスポート</button>
</div>
</div>
<script>
class Color {
constructor(r, g, b, a = 255) {
this.r = r;
this.g = g;
this.b = b;
this.a = a;
}
}
// Helper function to convert hex to Color object
function hexToColor(hex) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return new Color(r, g, b);
}
// Helper function to convert Color object to hex
function colorToHex(color) {
return `#${color.r.toString(16).padStart(2, '0')}${color.g.toString(16).padStart(2, '0')}${color.b.toString(16).padStart(2, '0')}`;
}
class Layer {
constructor(rowCount, colCount) {
this.cells = Array(rowCount).fill().map(() => Array(colCount).fill(new Color(255, 255, 255, 0)));
}
}
function updateGridSize() {
const newRows = parseInt(document.getElementById('rowInput').value);
const newCols = parseInt(document.getElementById('colInput').value);
if (newRows > 0 && newCols > 0) {
grid.resize(newRows, newCols, grid.width, grid.height);
} else {
alert("Invalid grid size. Please enter positive numbers.");
}
}
document.getElementById('updateGridSizeBtn').addEventListener('click', updateGridSize);
function handlePastedImage(e) {
const items = e.clipboardData.items;
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
const blob = items[i].getAsFile();
const reader = new FileReader();
reader.onload = function(event) {
const img = new Image();
img.onload = function() {
processImageForGrid(this);
};
img.src = event.target.result;
};
reader.readAsDataURL(blob);
}
}
}
function processImageForGrid(img) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const aspectRatio = img.width / img.height;
const gridAspectRatio = grid.colCount / grid.rowCount;
let srcWidth, srcHeight, destWidth, destHeight;
if (aspectRatio > gridAspectRatio) {
destWidth = grid.colCount;
destHeight = Math.min(grid.rowCount, Math.floor(destWidth / aspectRatio));
} else {
destHeight = grid.rowCount;
destWidth = Math.min(grid.colCount, Math.floor(destHeight * aspectRatio));
}
srcWidth = img.width;
srcHeight = (srcWidth / destWidth) * destHeight;
canvas.width = destWidth;
canvas.height = destHeight;
ctx.drawImage(img, 0, 0, srcWidth, srcHeight, 0, 0, destWidth, destHeight);
const imageData = ctx.getImageData(0, 0, destWidth, destHeight);
for (let y = 0; y < destHeight; y++) {
for (let x = 0; x < destWidth; x++) {
const i = (y * destWidth + x) * 4;
const r = imageData.data[i];
const g = imageData.data[i + 1];
const b = imageData.data[i + 2];
grid.setColorAtIndex(y, x, new Color(r, g, b));
}
}
grid.draw();
}
document.getElementById('imagePasteArea').addEventListener('paste', handlePastedImage);
class Palette {
constructor(rowCount, colCount, width, height) {
this.canvas = document.createElement('canvas');
this.canvas.width = width;
this.canvas.height = height;
this.ctx = this.canvas.getContext("2d");
this.rowCount = rowCount;
this.colCount = colCount;
this.cellWidth = width / this.colCount;
this.cellHeight = height / this.rowCount;
this.colors = [];
this.canvas.addEventListener('click', this.handleClick.bind(this));
}
//from hex
setColors(colors) {
this.colors = colors.slice(0, this.rowCount * this.colCount);
this.draw();
}
draw() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// Canas Draw
for (let i = 0; i < this.colors.length; i++) {
const row = Math.floor(i / this.colCount);
const col = i % this.colCount;
const color = this.colors[i];
this.ctx.fillStyle = `rgb(${color.r},${color.g},${color.b})`;
this.ctx.fillRect(col * this.cellWidth, row * this.cellHeight, this.cellWidth, this.cellHeight);
}
// Draw grid lines
this.ctx.strokeStyle = '#000000';
this.ctx.lineWidth = 1;
for (let i = 0; i <= this.colCount; i++) {
this.ctx.beginPath();
this.ctx.moveTo(i * this.cellWidth, 0);
this.ctx.lineTo(i * this.cellWidth, this.canvas.height);
this.ctx.stroke();
}
for (let i = 0; i <= this.rowCount; i++) {
this.ctx.beginPath();
this.ctx.moveTo(0, i * this.cellHeight);
this.ctx.lineTo(this.canvas.width, i * this.cellHeight);
this.ctx.stroke();
}
}
handleClick(event) {
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const col = Math.floor(x / this.cellWidth);
const row = Math.floor(y / this.cellHeight);
const index = row * this.colCount + col;
if (index < this.colors.length) {
const selectedColor = this.colors[index];
//document.getElementById('colorPicker').value = selectedColor.toHex();
//document.getElementById('colorPicker').value =rgbToHex(selectedColor.r, selectedColor.g, selectedColor.b);
document.getElementById('colorPicker').value = colorToHex(selectedColor);
grid.currentColor = selectedColor;
}
}
appendTo(container) {
container.appendChild(this.canvas);
}
}
class Grid {
constructor(rowCount, colCount, width, height) {
this.canvas = document.createElement('canvas');
this.canvas.width = width;
this.canvas.height = height;
this.ctx = this.canvas.getContext("2d");
this.currentColor = new Color(0, 0, 0);
this.rowCount = rowCount;
this.colCount = colCount;
this._cellWidth = width / this.colCount;
this._cellHeight = height / this.rowCount;
this.layers = [new Layer(rowCount, colCount)];
this.currentLayerIndex = 0;
this.cellInitColor = new Color(255, 255, 255, 0);
this.onGridLine = true;
this.lineColor = '#000000';
this.lineWidth = 1;
this.isSquareCell = true;
this.dirtyRegion = new Set();
this.tool = 'point';
this.isDrawing = false;
this.lastCell = null;
this.lineStartCell = null;
this.history = [];
this.historyIndex = -1;
this.canvas.addEventListener('mousedown', this.handleMouseDown.bind(this));
this.canvas.addEventListener('mousemove', this.handleMouseMove.bind(this));
this.canvas.addEventListener('mouseup', this.handleMouseUp.bind(this));
this.canvas.addEventListener('mouseleave', this.handleMouseUp.bind(this));
}
get currentLayer() {
return this.layers[this.currentLayerIndex];
}
get width() {
return this.canvas.width;
}
get height() {
return this.canvas.height;
}
get lowerEdge() {
const cw = this.width / this.colCount;
const ch = this.height / this.rowCount;
return Math.min(cw, ch);
}
get cellWidth() {
return this.isSquareCell ? this.lowerEdge : this._cellWidth;
}
get cellHeight() {
return this.isSquareCell ? this.lowerEdge : this._cellHeight;
}
get gridWidth() {
return this.cellWidth * this.colCount;
}
get gridHeight() {
return this.cellHeight * this.rowCount;
}
getGridIndex(x, y) {
const col = Math.floor(x / this.cellWidth);
const row = Math.floor(y / this.cellHeight);
return { row, col };
}
setColorAtIndex(row, col, color) {
if (!this.isValidIndex(row, col)) return false;
const action = {
type: 'setColor',
layerIndex: this.currentLayerIndex,
row, col,
oldColor: this.currentLayer.cells[row][col],
newColor: color
};
this.addToHistory(action);
this.applyAction(action);
this.updateGridOutput();
return true;
}
getColorAtIndex(row, col) {
if (!this.isValidIndex(row, col)) return null;
return this.currentLayer.cells[row][col];
}
isValidIndex(row, col) {
return row >= 0 && row < this.rowCount && col >= 0 && col < this.colCount;
}
applyAction(action) {
switch (action.type) {
case 'setColor':
this.layers[action.layerIndex].cells[action.row][action.col] = action.newColor;
this.dirtyRegion.add(`${action.row},${action.col}`);
break;
}
}
addToHistory(action) {
this.history = this.history.slice(0, this.historyIndex + 1);
this.history.push(action);
this.historyIndex++;
}
undo() {
if (this.historyIndex < 0) return;
const action = this.history[this.historyIndex];
this.revertAction(action);
this.historyIndex--;
this.draw();
this.updateGridOutput();
}
redo() {
if (this.historyIndex >= this.history.length - 1) return;
this.historyIndex++;
const action = this.history[this.historyIndex];
this.applyAction(action);
this.draw();
this.updateGridOutput();
}
revertAction(action) {
switch (action.type) {
case 'setColor':
this.layers[action.layerIndex].cells[action.row][action.col] = action.oldColor;
this.dirtyRegion.add(`${action.row},${action.col}`);
break;
}
}
drawCell(row, col, color) {
this.ctx.fillStyle = `rgba(${color.r},${color.g},${color.b},${color.a / 255})`;
this.ctx.fillRect(col * this.cellWidth, row * this.cellHeight, this.cellWidth, this.cellHeight);
}
drawCells() {
for (let layer of this.layers) {
for (let row = 0; row < this.rowCount; row++) {
for (let col = 0; col < this.colCount; col++) {
const color = layer.cells[row][col];
if (color.a > 0) {
this.drawCell(row, col, color);
}
}
}
}
}
drawGridLine(sx, sy, ex, ey) {
this.ctx.strokeStyle = this.lineColor;
this.ctx.lineWidth = this.lineWidth;
this.ctx.beginPath();
this.ctx.moveTo(sx, sy);
this.ctx.lineTo(ex, ey);
this.ctx.stroke();
}
drawGridLines() {
const we = this.colCount * this.cellWidth;
const he = this.rowCount * this.cellHeight;
for (let row = 0; row <= this.rowCount; row++) {
this.drawGridLine(0, row * this.cellHeight, we, row * this.cellHeight);
}
for (let col = 0; col <= this.colCount; col++) {
this.drawGridLine(col * this.cellWidth, 0, col * this.cellWidth, he);
}
}
draw() {
this.ctx.clearRect(0, 0, this.width, this.height);
for (let layer of this.layers) {
for (let row = 0; row < this.rowCount; row++) {
for (let col = 0; col < this.colCount; col++) {
const color = layer.cells[row][col];
if (color.a > 0) {
this.drawCell(row, col, color);
}
}
}
}
if (this.onGridLine) {
this.drawGridLines();
}
}
clearCells() {
this.ctx.fillStyle = `rgba(${this.cellInitColor.r},${this.cellInitColor.g},${this.cellInitColor.b},${this.cellInitColor.a / 255})`;
this.ctx.fillRect(0, 0, this.width, this.height);
}
resize(newRowCount, newColCount, newWidth, newHeight) {
const oldCells = this.layers.map(layer => layer.cells);
const oldRowCount = this.rowCount;
const oldColCount = this.colCount;
// Update dimensions
this.rowCount = newRowCount;
this.colCount = newColCount;
this.canvas.width = newWidth;
this.canvas.height = newHeight;
this._cellWidth = newWidth / newColCount;
this._cellHeight = newHeight / newRowCount;
// Resize all layers
this.layers = this.layers.map(layer => {
const newLayer = new Layer(newRowCount, newColCount);
const minRowCount = Math.min(oldRowCount, newRowCount);
const minColCount = Math.min(oldColCount, newColCount);
for (let r = 0; r < minRowCount; r++) {
for (let c = 0; c < minColCount; c++) {
newLayer.cells[r][c] = layer.cells[r][c];
}
}
return newLayer;
});
// Reset history as resize operation can't be undone
this.history = [];
this.historyIndex = -1;
// Redraw
this.draw();
this.updateGridOutput();
}
drawLine(startCell, endCell) {
const dx = Math.abs(endCell.col - startCell.col);
const dy = Math.abs(endCell.row - startCell.row);
const sx = startCell.col < endCell.col ? 1 : -1;
const sy = startCell.row < endCell.row ? 1 : -1;
let err = dx - dy;
let currentCell = {...startCell};
while (true) {
this.setColorAtIndex(currentCell.row, currentCell.col, this.currentColor);
if (currentCell.col === endCell.col && currentCell.row === endCell.row) break;
const e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
currentCell.col += sx;
}
if (e2 < dx) {
err += dx;
currentCell.row += sy;
}
}
}
handleMouseDown(event) {
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const cell = this.getGridIndex(x, y);
switch (this.tool) {
case 'point':
case 'pen':
case 'eraser':
this.isDrawing = true;
this.setColorAtIndex(cell.row, cell.col, this.tool === 'eraser' ? this.cellInitColor : this.currentColor);
this.lastCell = cell;
break;
case 'line':
if (!this.lineStartCell) {
this.lineStartCell = cell;
} else {
this.drawLine(this.lineStartCell, cell);
this.lineStartCell = null;
}
break;
}
this.draw();
}
handleMouseMove(event) {
if (!this.isDrawing || (this.tool !== 'pen' && this.tool !== 'eraser')) return;
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const cell = this.getGridIndex(x, y);
if (cell.row !== this.lastCell.row || cell.col !== this.lastCell.col) {
this.drawLine(this.lastCell, cell);
this.lastCell = cell;
this.draw();
}
}
handleMouseUp() {
this.isDrawing = false;
}
addLayer() {
this.layers.push(new Layer(this.rowCount, this.colCount));
this.currentLayerIndex = this.layers.length - 1;
this.updateLayerList();
}
removeLayer() {
if (this.layers.length > 1) {
this.layers.splice(this.currentLayerIndex, 1);
this.currentLayerIndex = Math.min(this.currentLayerIndex, this.layers.length - 1);
this.updateLayerList();
this.draw();
}
}
updateLayerList() {
const layerList = document.getElementById('layerList');
layerList.innerHTML = '';
this.layers.forEach((layer, index) => {
const li = document.createElement('li');
const radio = document.createElement('input');
radio.type = 'radio';
radio.name = 'layer';
radio.value = index;
radio.checked = index === this.currentLayerIndex;
radio.addEventListener('change', () => {
this.currentLayerIndex = index;
});
li.appendChild(radio);
li.appendChild(document.createTextNode(`Layer ${index + 1}`));
layerList.appendChild(li);
});
}
export(scale = 1) {
const canvas = document.createElement('canvas');
canvas.width = this.colCount * scale;
canvas.height = this.rowCount * scale;
const ctx = canvas.getContext('2d');
for (let layer of this.layers) {
for (let row = 0; row < this.rowCount; row++) {
for (let col = 0; col < this.colCount; col++) {
const color = layer.cells[row][col];
if (color.a > 0) {
ctx.fillStyle = `rgba(${color.r},${color.g},${color.b},${color.a / 255})`;
ctx.fillRect(col * scale, row * scale, scale, scale);
}
}
}
}
return canvas.toDataURL();
}
appendTo(container) {
container.appendChild(this.canvas);
}
exportAsJson() {
const colorArray = this.layers.flatMap(layer =>
layer.cells.flatMap(row =>
row.map(color => ({ r: color.r, g: color.g, b: color.b, a: color.a }))
)
);
return JSON.stringify(colorArray);
}
exportAsHexArray() {
const hexArray = this.layers.flatMap(layer =>
layer.cells.flatMap(row =>
//row.map(color => color.toHex())
//row.map(color =>rgbToHex(color.r, color.g, color.b))
row.map(color => colorToHex(color))
)
);
return JSON.stringify(hexArray);
}
// updateGridOutput() {
// const jsonOutput = this.exportAsJson();
// const hexOutput = this.exportAsHexArray();
// document.getElementById('gridOutput').value = `Color Objects:\n${jsonOutput}\n\nHex Array:\n${hexOutput}`;
// }
updateGridOutput() {
const jsonOutput = this.exportAsJson();
const hexOutput = this.exportAsHexArray();
document.getElementById('jsonOutput').value = jsonOutput;
document.getElementById('hexOutput').value = hexOutput;
}
}//Grid
// Helper functions for color extraction
function extractColorsFrequency(imageData, limit) {
const colorCounts = {};
for (let i = 0; i < imageData.data.length; i += 4) {
const color = `${imageData.data[i]},${imageData.data[i+1]},${imageData.data[i+2]}`;
colorCounts[color] = (colorCounts[color] || 0) + 1;
}
return Object.entries(colorCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, limit)
.map(([color]) => {
const [r, g, b] = color.split(',').map(Number);
return new Color(r, g, b);
});
}
function extractColorsSkip(imageData, limit) {
const colors = extractColorsFrequency(imageData, imageData.width * imageData.height);
const skip = Math.max(1, Math.floor(colors.length / limit));
return colors.filter((_, index) => index % skip === 0).slice(0, limit);
}
// function extractColorsKmeans(imageData, k) {
// const points = [];
// for (let i = 0; i < imageData.data.length; i += 4) {
// points.push([imageData.data[i], imageData.data[i+1], imageData.data[i+2]]);
// }
// const kmeans = new KMeans({k, maxIterations: 20});
// const clusters = kmeans.cluster(points);
// return clusters.centroids.map(centroid => new Color(Math.round(centroid[0]), Math.round(centroid[1]), Math.round(centroid[2])));
// }
// KMeans関数を置き換え
function extractColorsKmeans(imageData, k) {
const points = [];
for (let i = 0; i < imageData.data.length; i += 4) {
points.push([imageData.data[i], imageData.data[i+1], imageData.data[i+2]]);
}
const centroids = simpleKMeans(points, k);
return centroids.map(centroid => new Color(Math.round(centroid[0]), Math.round(centroid[1]), Math.round(centroid[2])));
}
// Event listeners for new functionality
document.getElementById('loadPaletteBtn').addEventListener('click', () => {
const input = document.getElementById('paletteInput').value;
let colors = [];
try {
if (input.startsWith('#')) {
// Hex string input
colors = input.split(/\s+/).map(hex => Color.fromHex(hex));
} else {
const parsed = JSON.parse(input);
console.log(parsed);
if (Array.isArray(parsed)) {
if (typeof parsed[0] === 'string') {
// Array of hex strings
//colors = parsed.map(hex => Color.fromHex(hex));
colors = input.split(/\s+/).map(hex => hexToColor(hex));
} else if (typeof parsed[0] === 'object') {
// Array of color objects
colors = parsed.map(obj => new Color(obj.r, obj.g, obj.b, obj.a));
}
} else if (typeof parsed === 'object') {
// Single color object
colors = [new Color(parsed.r, parsed.g, parsed.b, parsed.a)];
}
}
palette.setColors(colors);
} catch (e) {
console.error("Failed to parse input:", e);
alert("Failed to parse input. Please check the format and try again.");
}
});
document.getElementById('imageInput').addEventListener('change', (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const method = document.getElementById('extractMethod').value;
let colors;
try {
switch (method) {
case 'frequency':
colors = extractColorsFrequency(imageData, 16);
break;
case 'skip':
colors = extractColorsSkip(imageData, 16);
break;
case 'kmeans':
colors = extractColorsKmeans(imageData, 16);
break;
default:
throw new Error("Invalid extraction method");
}
palette.setColors(colors);
} catch (error) {
console.error("Error extracting colors:", error);
alert("Failed to extract colors from the image. Please try again or use a different method.");
}
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
});
// document.getElementById('copyToClipboardBtn').addEventListener('click', () => {
// const output = document.getElementById('gridOutput');
// output.select();
// document.execCommand('copy');
// alert("Grid data copied to clipboard!");
// });
// document.getElementById('exportTxtBtn').addEventListener('click', () => {
// const output = document.getElementById('gridOutput').value;
// const blob = new Blob([output], {type: 'text/plain'});
// const url = URL.createObjectURL(blob);
// const a = document.createElement('a');
// a.href = url;
// a.download = 'grid_colors.txt';
// a.click();
// URL.revokeObjectURL(url);
// });
document.getElementById('resizeBtn').addEventListener('click', () => {
const newSize = prompt("Enter new grid size (e.g., '20,20' for 20x20 grid):", `${grid.rowCount},${grid.colCount}`);
if (newSize) {
const [rows, cols] = newSize.split(',').map(Number);
if (rows > 0 && cols > 0) {
grid.resize(rows, cols, grid.width, grid.height);
} else {
alert("Invalid grid size. Please enter positive numbers.");
}
}
});
document.getElementById('colorPicker').addEventListener('input', (event) => {
//grid.currentColor = Color.fromHex(event.target.value);
grid.currentColor = hexToColor(event.target.value);
});
document.querySelectorAll('.tool-btn').forEach(btn => {
btn.addEventListener('click', (event) => {
grid.tool = event.target.dataset.tool;
grid.lineStartCell = null;
});
});
document.getElementById('undoBtn').addEventListener('click', () => grid.undo());
document.getElementById('redoBtn').addEventListener('click', () => grid.redo());
document.getElementById('addLayerBtn').addEventListener('click', () => grid.addLayer());
document.getElementById('removeLayerBtn').addEventListener('click', () => grid.removeLayer());
document.getElementById('exportBtn').addEventListener('click', () => {
const dataURL = grid.export(grid.cellWidth);
window.open(dataURL);
});
document.getElementById('exportPixelBtn').addEventListener('click', () => {
const dataURL = grid.export(1);
window.open(dataURL);
});
document.getElementById('copyJsonBtn').addEventListener('click', () => {
const output = document.getElementById('jsonOutput');
output.select();
document.execCommand('copy');
alert("カラーオブジェクトデータがクリップボードにコピーされました!");
});
document.getElementById('exportJsonBtn').addEventListener('click', () => {
const output = document.getElementById('jsonOutput').value;
const blob = new Blob([output], {type: 'application/json'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'grid_colors.json';
a.click();
URL.revokeObjectURL(url);
});
document.getElementById('copyHexBtn').addEventListener('click', () => {
const output = document.getElementById('hexOutput');
output.select();
document.execCommand('copy');
alert("16進数配列データがクリップボードにコピーされました!");
});
document.getElementById('exportHexBtn').addEventListener('click', () => {
const output = document.getElementById('hexOutput').value;
const blob = new Blob([output], {type: 'text/plain'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'grid_colors_hex.txt';
a.click();
URL.revokeObjectURL(url);
});
// 既存のイベントリスナーを削除
// document.getElementById('copyToClipboardBtn').remove();
// document.getElementById('exportTxtBtn').remove();
function simpleKMeans(points, k, maxIterations = 20) {
// ランダムに初期中心点を選択
let centroids = points.slice(0, k);
for (let iter = 0; iter < maxIterations; iter++) {
// 各点を最も近い中心点に割り当て
const clusters = Array(k).fill().map(() => []);
for (const point of points) {
let minDist = Infinity;
let closestCentroidIndex = 0;
for (let i = 0; i < k; i++) {
const dist = euclideanDistance(point, centroids[i]);
if (dist < minDist) {
minDist = dist;
closestCentroidIndex = i;
}
}
clusters[closestCentroidIndex].push(point);
}
// 新しい中心点を計算
const newCentroids = clusters.map(cluster =>
cluster.length > 0 ? averagePoint(cluster) : centroids[0]
);
// 収束チェック
if (centroids.every((centroid, i) =>
euclideanDistance(centroid, newCentroids[i]) < 0.001
)) {
break;
}
centroids = newCentroids;
}
return centroids;
}
function euclideanDistance(p1, p2) {
return Math.sqrt(p1.reduce((sum, v, i) => sum + Math.pow(v - p2[i], 2), 0));
}
function averagePoint(points) {
const sum = points.reduce((acc, p) => acc.map((v, i) => v + p[i]), Array(points[0].length).fill(0));
return sum.map(v => v / points.length);
}
// Responsive design
// function resizeGridAndPalette() {
// const mainContainer = document.getElementById('mainContainer');
// const totalWidth = mainContainer.clientWidth;
// const gridWidth = Math.floor(totalWidth * 0.7); // 70% of the container width
// const paletteWidth = Math.floor(totalWidth * 0.25); // 25% of the container width
// grid.resize(grid.rowCount, grid.colCount, gridWidth, gridWidth);
// palette = new Palette(4, 4, paletteWidth, paletteWidth);
// paletteContainer.innerHTML = '';
// palette.appendTo(paletteContainer);
// }
// function resizeGridAndPalette() {
// const mainContainer = document.getElementById('mainContainer');
// const totalWidth = mainContainer.clientWidth;
// const gridWidth = Math.floor(totalWidth * 0.7); // 70% of the container width
// const paletteWidth = Math.floor(totalWidth * 0.25); // 25% of the container width
// // Resize the existing grid
// grid.resize(grid.rowCount, grid.colCount, gridWidth, gridWidth);
// grid.draw(); // Redraw the grid after resizing
// // Recreate the palette
// paletteContainer.innerHTML = '';
// palette = new Palette(4, 4, paletteWidth, paletteWidth);
// palette.appendTo(paletteContainer);
// palette.setColors(palette.colors); // Restore the colors
// palette.draw(); // Draw the palette
// }
// function resizeGridAndPalette() {
// console.log('Resizing grid and palette');
// try {
// const mainContainer = document.getElementById('mainContainer');
// const totalWidth = mainContainer.clientWidth;
// const gridWidth = Math.floor(totalWidth * 0.7); // 70% of the container width
// const paletteWidth = Math.floor(totalWidth * 0.25); // 25% of the container width
// // Resize the existing grid
// grid.resize(grid.rowCount, grid.colCount, gridWidth, gridWidth);
// grid.draw(); // Redraw the grid after resizing
// // Update the existing palette instead of creating a new one
// palette.canvas.width = paletteWidth;
// palette.canvas.height = paletteWidth;
// palette.cellWidth = paletteWidth / palette.colCount;
// palette.cellHeight = paletteWidth / palette.rowCount;
// palette.draw(); // Redraw the palette after updating dimensions
// console.log('Grid and palette resized');
// } catch (error) {
// console.error('Error in resizeGridAndPalette:', error);
// }
// }
// カラーオブジェクトを作成する関数
function createColorObject(r, g, b) {
return { r, g, b, a: 255 };
}
// RGBカラーをヘックス形式に変換する関数
function rgbToHex(r, g, b) {
return '#' + [r, g, b].map(x => {
const hex = x.toString(16);
return hex.length === 1 ? '0' + hex : hex;
}).join('');
}
// RGカラー配列生成関数
function generateRGColorArray(rRowCount, gColCount) {
const colorArray = [];
for (let r = 0; r < rRowCount; r++) {
for (let g = 0; g < gColCount; g++) {
const rValue = Math.round((r / (rRowCount - 1)) * 255);
const gValue = Math.round((g / (gColCount - 1)) * 255);
colorArray.push(createColorObject(rValue, gValue, 0));
}
}
return colorArray;
}
// GBカラー配列生成関数
function generateGBColorArray(gRowCount, bColCount) {
const colorArray = [];
for (let g = 0; g < gRowCount; g++) {
for (let b = 0; b < bColCount; b++) {
const gValue = Math.round((g / (gRowCount - 1)) * 255);
const bValue = Math.round((b / (bColCount - 1)) * 255);
colorArray.push(createColorObject(0, gValue, bValue));
}
}
return colorArray;
}
// BRカラー配列生成関数
function generateBRColorArray(bRowCount, rColCount) {
const colorArray = [];
for (let b = 0; b < bRowCount; b++) {
for (let r = 0; r < rColCount; r++) {
const bValue = Math.round((b / (bRowCount - 1)) * 255);
const rValue = Math.round((r / (rColCount - 1)) * 255);
colorArray.push(createColorObject(rValue, 0, bValue));
}
}
return colorArray;
}
// RGヘックス配列生成関数
function generateRGHexArray(rRowCount, gColCount) {
const hexArray = [];
for (let r = 0; r < rRowCount; r++) {
for (let g = 0; g < gColCount; g++) {
const rValue = Math.round((r / (rRowCount - 1)) * 255);
const gValue = Math.round((g / (gColCount - 1)) * 255);
hexArray.push(rgbToHex(rValue, gValue, 0));
}
}
return hexArray;
}
// GBヘックス配列生成関数
function generateGBHexArray(gRowCount, bColCount) {
const hexArray = [];
for (let g = 0; g < gRowCount; g++) {
for (let b = 0; b < bColCount; b++) {
const gValue = Math.round((g / (gRowCount - 1)) * 255);
const bValue = Math.round((b / (bColCount - 1)) * 255);
hexArray.push(rgbToHex(0, gValue, bValue));
}
}
return hexArray;
}
// BRヘックス配列生成関数
function generateBRHexArray(bRowCount, rColCount) {
const hexArray = [];
for (let b = 0; b < bRowCount; b++) {
for (let r = 0; r < rColCount; r++) {
const bValue = Math.round((b / (bRowCount - 1)) * 255);
const rValue = Math.round((r / (rColCount - 1)) * 255);
hexArray.push(rgbToHex(rValue, 0, bValue));
}
}
return hexArray;
}
// Create and display the grid and palette
const gridContainer = document.getElementById('gridContainer');
const grid = new Grid(16, 16, 500, 500);
grid.appendTo(gridContainer);
grid.draw();
grid.updateLayerList();
const paletteContainer = document.getElementById('paletteContainer');
const palette = new Palette(16, 16, 200, 200);
palette.appendTo(paletteContainer);
palette.setColors(generateRGColorArray(16,16));
// const palette2 = new Palette(16, 16, 200, 200);
// palette2.appendTo(paletteContainer);
// palette2.setColors(generateGBColorArray(16,16));
// const palette3 = new Palette(16, 16, 200, 200);
// palette3.appendTo(paletteContainer);
// palette3.setColors(generateBRColorArray(16,16));
// Initial grid output update
grid.updateGridOutput();
//window.addEventListener('resize', resizeGridAndPalette);
//resizeGridAndPalette(); // Initial call to set sizes
</script>
</body>
</html>
パレット処理
既知の問題。
多分複数パレットの準備をしたせいでパレットのイベントが発火しなくなってる。
パレットは各々Canvasをもつ。
単純に考えるならCanvasが発火すれば良い。
しかしパレットのCanvasはどれくらいのサイズか分からないから新たに表示のためのDOMを生成する必要がある。これはパレットのキャンバスを新規のDOMにコピーすることで実現される。
しかしそのためにパレットのCanvasが発火する条件が消えた。
これを直すためには表示用のDOMが発火してデータをパレットに渡す必要がある。
ただし前提として、パレットをただの色オブジェクトないしヘックスの配列と考えるなら、パレットがCanvasを持つ必要すらないが。
例えば画像をそのままパレットに使用するような場合を考えるならこの構造の分離は必要。
現状Grid側はそのデータ構造を維持しており、パレットの方がGridよりリッチになっている。しかしGrid側も下書きを用意するなど考えると最終似たような構造になるであろう。
# @title 最新現行3
%%html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Advanced Interactive Canvas Grid with Palette Management</title>
<style>
body {
display: flex;
flex-direction: column;
align-items: center;
font-family: Arial, sans-serif;
}
#mainContainer {
display: flex;
justify-content: center;
width: 100%;
max-width: 1200px;
}
#gridContainer {
border: 1px solid #000;
margin: 20px;
}
#paletteContainer {
border: 1px solid #000;
margin: 20px;
}
#controls, #layerControls {
display: flex;
flex-direction: row;
text-align: center;
width: 100%;
}
#paletteControls {
/* display: flex;
flex-direction: row; */
text-align: center;
width: 100%;
}
.tool-btn, .layer-btn {
background-color: #f0f0f0;
border: 1px solid #ccc;
border-radius: 3px;
cursor: pointer;
}
#layerList {
list-style-type: none;
padding: 0;
text-align: left;
}
#layerList li {
margin: 5px 0;
padding: 5px;
background-color: #f0f0f0;
border-radius: 3px;
}
#paletteInput, #gridOutput {
width: 100%;
height: 100px;
margin-bottom: 10px;
resize: vertical;
}
.output-container {
display: flex;
justify-content: space-between;
margin-top: 20px;
}
.output-box {
width: 48%;
}
.output-textarea {
width: 100%;
height: 150px;
margin-bottom: 10px;
}
#gridControls {
/* display: flex;
flex-wrap: wrap; */
justify-content: space-between;
margin-bottom: 10px;
}
#gridControls input {
width: 50px;
}
#imagePasteArea {
width: 200px;
height: 200px;
border: 2px dashed #ccc;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
.palette-container {
/* display: flex;
flex-wrap: wrap; */
gap: 10px;
margin-bottom: 20px;
}
.palette-item {
border: 1px solid #ccc;
padding: 10px;
width: 220px;
}
.palette-canvas {
width: 200px;
height: 200px;
}
#processingControls {
margin-top: 20px;
}
#processingControls select, #processingControls input {
margin-right: 10px;
}
</style>
</head>
<body>
<div id="controls">
<button id="resizeBtn" class="tool-btn">Resize Grid</button>
<input type="color" id="colorPicker" value="#000000">
<button class="tool-btn" data-tool="point">Point</button>
<button class="tool-btn" data-tool="pen">Pen</button>
<button class="tool-btn" data-tool="eraser">Eraser</button>
<button class="tool-btn" data-tool="line">Line</button>
<button id="undoBtn" class="tool-btn">Undo</button>
<button id="redoBtn" class="tool-btn">Redo</button>
<button id="exportBtn" class="tool-btn">Export (1:1)</button>
<button id="exportPixelBtn" class="tool-btn">Export (1 cell = 1 pixel)</button>
</div>
<div id="layerControls">
<button id="addLayerBtn" class="layer-btn">Add Layer</button>
<button id="removeLayerBtn" class="layer-btn">Remove Layer</button>
<ul id="layerList"></ul>
</div>
<div id="mainContainer">
<div id="gridContainer"></div>
<div id="paletteContainer"></div>
</div>
<div id="gridControls">
<h3>Grid Controls</h3>
<div>
<label for="rowInput">Rows:</label>
<input type="number" id="rowInput" min="1" value="16">
</div>
<div>
<label for="colInput">Columns:</label>
<input type="number" id="colInput" min="1" value="16">
</div>
<button id="updateGridSizeBtn">Update Grid Size</button>
<div id="imagePasteArea">Click or paste image here</div>
</div>
<div id="paletteManagement">
<h3>Palette Management</h3>
<button id="addPaletteBtn">Add New Palette</button>
<div id="paletteControls">
<textarea id="paletteInput" placeholder="Enter hex colors, JSON array, or paste image URL"></textarea>
<button id="loadPaletteBtn" class="tool-btn">Load Palette</button>
<select id="extractMethod">
<option value="frequency">Frequency</option>
<option value="skip">Skip Frequency</option>
<option value="kmeans">K-means</option>
</select>
<input type="file" id="imageInput" accept="image/*">
</div>
</div>
<div id="processingControls">
<h3>Processing Controls</h3>
<select id="targetSelect">
<option value="">Select target</option>
</select>
<input type="text" id="processingParam" placeholder="Processing parameter">
<button id="scaleBtn">Scale</button>
<button id="invertBtn">Invert</button>
<button id="reduceColorsBtn">Reduce Colors</button>
</div>
<div class="output-container">
<div class="output-box">
<h3>カラーオブジェクト出力</h3>
<textarea id="jsonOutput" class="output-textarea" readonly></textarea>
<button id="copyJsonBtn" class="tool-btn">クリップボードにコピー</button>
<button id="exportJsonBtn" class="tool-btn">JSONとしてエクスポート</button>
</div>
<div class="output-box">
<h3>16進数配列出力</h3>
<textarea id="hexOutput" class="output-textarea" readonly></textarea>
<button id="copyHexBtn" class="tool-btn">クリップボードにコピー</button>
<button id="exportHexBtn" class="tool-btn">TXTとしてエクスポート</button>
</div>
</div>
<script>
class Color {
constructor(r, g, b, a = 255) {
this.r = r;
this.g = g;
this.b = b;
this.a = a;
}
}
function hexToColor(hex) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return new Color(r, g, b);
}
function colorToHex(color) {
return `#${color.r.toString(16).padStart(2, '0')}${color.g.toString(16).padStart(2, '0')}${color.b.toString(16).padStart(2, '0')}`;
}
class Layer {
constructor(rowCount, colCount) {
this.cells = Array(rowCount).fill().map(() => Array(colCount).fill(new Color(255, 255, 255, 0)));
}
}
class Palette {
constructor(rowCount, colCount, width, height) {
this.canvas = document.createElement('canvas');
this.canvas.width = width;
this.canvas.height = height;
this.ctx = this.canvas.getContext("2d");
this.rowCount = rowCount;
this.colCount = colCount;
this.cellWidth = width / this.colCount;
this.cellHeight = height / this.rowCount;
this.colors = [];
this.canvas.addEventListener('click', this.handleClick.bind(this));
}
setColors(colors) {
this.colors = colors.slice(0, this.rowCount * this.colCount);
this.draw();
}
draw() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
//canvas
for (let i = 0; i < this.colors.length; i++) {
const row = Math.floor(i / this.colCount);
const col = i % this.colCount;
const color = this.colors[i];
this.ctx.fillStyle = `rgb(${color.r},${color.g},${color.b})`;
this.ctx.fillRect(col * this.cellWidth, row * this.cellHeight, this.cellWidth, this.cellHeight);
}
//line
this.ctx.strokeStyle = '#000000';
this.ctx.lineWidth = 1;
for (let i = 0; i <= this.colCount; i++) {
this.ctx.beginPath();
this.ctx.moveTo(i * this.cellWidth, 0);
this.ctx.lineTo(i * this.cellWidth, this.canvas.height);
this.ctx.stroke();
}
for (let i = 0; i <= this.rowCount; i++) {
this.ctx.beginPath();
this.ctx.moveTo(0, i * this.cellHeight);
this.ctx.lineTo(this.canvas.width, i * this.cellHeight);
this.ctx.stroke();
}
}
handleClick(event) {
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const col = Math.floor(x / this.cellWidth);
const row = Math.floor(y / this.cellHeight);
const index = row * this.colCount + col;
if (index < this.colors.length) {
const selectedColor = this.colors[index];
document.getElementById('colorPicker').value = colorToHex(selectedColor);
grid.currentColor = selectedColor;
}
}
appendTo(container) {
container.appendChild(this.canvas);
}
}
class Grid {
constructor(rowCount, colCount, width, height) {
this.canvas = document.createElement('canvas');
this.canvas.width = width;
this.canvas.height = height;
this.ctx = this.canvas.getContext("2d");
this.currentColor = new Color(0, 0, 0);
this.rowCount = rowCount;
this.colCount = colCount;
this._cellWidth = width / this.colCount;
this._cellHeight = height / this.rowCount;
this.layers = [new Layer(rowCount, colCount)];
this.currentLayerIndex = 0;
this.cellInitColor = new Color(255, 255, 255, 0);
this.onGridLine = true;
this.lineColor = '#000000';
this.lineWidth = 1;
this.isSquareCell = true;
this.dirtyRegion = new Set();
this.tool = 'point';
this.isDrawing = false;
this.lastCell = null;
this.lineStartCell = null;
this.history = [];
this.historyIndex = -1;
this.canvas.addEventListener('mousedown', this.handleMouseDown.bind(this));
this.canvas.addEventListener('mousemove', this.handleMouseMove.bind(this));
this.canvas.addEventListener('mouseup', this.handleMouseUp.bind(this));
this.canvas.addEventListener('mouseleave', this.handleMouseUp.bind(this));
}
//#region props
get currentLayer() {
return this.layers[this.currentLayerIndex];
}
get width() {
return this.canvas.width;
}
get height() {
return this.canvas.height;
}
get lowerEdge() {
const cw = this.width / this.colCount;
const ch = this.height / this.rowCount;
return Math.min(cw, ch);
}
get cellWidth() {
return this.isSquareCell ? this.lowerEdge : this._cellWidth;
}
get cellHeight() {
return this.isSquareCell ? this.lowerEdge : this._cellHeight;
}
get gridWidth() {
return this.cellWidth * this.colCount;
}
get gridHeight() {
return this.cellHeight * this.rowCount;
}
//#endregion
//#region index
getGridIndex(x, y) {
const col = Math.floor(x / this.cellWidth);
const row = Math.floor(y / this.cellHeight);
return { row, col };
}
setColorAtIndex(row, col, color) {
if (!this.isValidIndex(row, col)) return false;
const action = {
type: 'setColor',
layerIndex: this.currentLayerIndex,
row, col,
oldColor: this.currentLayer.cells[row][col],
newColor: color
};
this.addToHistory(action);
this.applyAction(action);
this.updateGridOutput();
return true;
}
getColorAtIndex(row, col) {
if (!this.isValidIndex(row, col)) return null;
return this.currentLayer.cells[row][col];
}
isValidIndex(row, col) {
return row >= 0 && row < this.rowCount && col >= 0 && col < this.colCount;
}
//#endregion
//#region undo/redo
applyAction(action) {
switch (action.type) {
case 'setColor':
this.layers[action.layerIndex].cells[action.row][action.col] = action.newColor;
this.dirtyRegion.add(`${action.row},${action.col}`);
break;
}
}
addToHistory(action) {
this.history = this.history.slice(0, this.historyIndex + 1);
this.history.push(action);
this.historyIndex++;
}
undo() {
if (this.historyIndex < 0) return;
const action = this.history[this.historyIndex];
this.revertAction(action);
this.historyIndex--;
this.draw();
this.updateGridOutput();
}
redo() {
if (this.historyIndex >= this.history.length - 1) return;
this.historyIndex++;
const action = this.history[this.historyIndex];
this.applyAction(action);
this.draw();
this.updateGridOutput();
}
revertAction(action) {
switch (action.type) {
case 'setColor':
this.layers[action.layerIndex].cells[action.row][action.col] = action.oldColor;
this.dirtyRegion.add(`${action.row},${action.col}`);
break;
}
}
//#endregion
//#region draw
drawCell(row, col, color) {
this.ctx.fillStyle = `rgba(${color.r},${color.g},${color.b},${color.a / 255})`;
this.ctx.fillRect(col * this.cellWidth, row * this.cellHeight, this.cellWidth, this.cellHeight);
}
drawCells() {
for (let layer of this.layers) {
for (let row = 0; row < this.rowCount; row++) {
for (let col = 0; col < this.colCount; col++) {
const color = layer.cells[row][col];
if (color.a > 0) {
this.drawCell(row, col, color);
}
}
}
}
}
drawGridLine(sx, sy, ex, ey) {
this.ctx.strokeStyle = this.lineColor;
this.ctx.lineWidth = this.lineWidth;
this.ctx.beginPath();
this.ctx.moveTo(sx, sy);
this.ctx.lineTo(ex, ey);
this.ctx.stroke();
}
drawGridLines() {
const we = this.colCount * this.cellWidth;
const he = this.rowCount * this.cellHeight;
for (let row = 0; row <= this.rowCount; row++) {
this.drawGridLine(0, row * this.cellHeight, we, row * this.cellHeight);
}
for (let col = 0; col <= this.colCount; col++) {
this.drawGridLine(col * this.cellWidth, 0, col * this.cellWidth, he);
}
}
draw() {
this.ctx.clearRect(0, 0, this.width, this.height);
for (let layer of this.layers) {
for (let row = 0; row < this.rowCount; row++) {
for (let col = 0; col < this.colCount; col++) {
const color = layer.cells[row][col];
if (color.a > 0) {
this.drawCell(row, col, color);
}
}
}
}
if (this.onGridLine) {
this.drawGridLines();
}
}
//ブレセンハム
drawLine(startCell, endCell) {
const dx = Math.abs(endCell.col - startCell.col);
const dy = Math.abs(endCell.row - startCell.row);
const sx = startCell.col < endCell.col ? 1 : -1;
const sy = startCell.row < endCell.row ? 1 : -1;
let err = dx - dy;
let currentCell = {...startCell};
while (true) {
this.setColorAtIndex(currentCell.row, currentCell.col, this.currentColor);
if (currentCell.col === endCell.col && currentCell.row === endCell.row) break;
const e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
currentCell.col += sx;
}
if (e2 < dx) {
err += dx;
currentCell.row += sy;
}
}
}
clearCells() {
this.ctx.fillStyle = `rgba(${this.cellInitColor.r},${this.cellInitColor.g},${this.cellInitColor.b},${this.cellInitColor.a / 255})`;
this.ctx.fillRect(0, 0, this.width, this.height);
}
//#endregion
//#region handle
handleMouseDown(event) {
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const cell = this.getGridIndex(x, y);
switch (this.tool) {
case 'point':
case 'pen':
case 'eraser':
this.isDrawing = true;
this.setColorAtIndex(cell.row, cell.col, this.tool === 'eraser' ? this.cellInitColor : this.currentColor);
this.lastCell = cell;
break;
case 'line':
if (!this.lineStartCell) {
this.lineStartCell = cell;
} else {
this.drawLine(this.lineStartCell, cell);
this.lineStartCell = null;
}
break;
}
this.draw();
}
handleMouseMove(event) {
if (!this.isDrawing || (this.tool !== 'pen' && this.tool !== 'eraser')) return;
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const cell = this.getGridIndex(x, y);
if (cell.row !== this.lastCell.row || cell.col !== this.lastCell.col) {
this.drawLine(this.lastCell, cell);
this.lastCell = cell;
this.draw();
}
}
handleMouseUp() {
this.isDrawing = false;
}
//#endregion
//#region layer
addLayer() {
this.layers.push(new Layer(this.rowCount, this.colCount));
this.currentLayerIndex = this.layers.length - 1;
this.updateLayerList();
}
removeLayer() {
if (this.layers.length > 1) {
this.layers.splice(this.currentLayerIndex, 1);
this.currentLayerIndex = Math.min(this.currentLayerIndex, this.layers.length - 1);
this.updateLayerList();
this.draw();
}
}
//#endregion
resize(newRowCount, newColCount, newWidth, newHeight) {
const oldCells = this.layers.map(layer => layer.cells);
const oldRowCount = this.rowCount;
const oldColCount = this.colCount;
this.rowCount = newRowCount;
this.colCount = newColCount;
this.canvas.width = newWidth;
this.canvas.height = newHeight;
this._cellWidth = newWidth / newColCount;
this._cellHeight = newHeight / newRowCount;
this.layers = this.layers.map(layer => {
const newLayer = new Layer(newRowCount, newColCount);
const minRowCount = Math.min(oldRowCount, newRowCount);
const minColCount = Math.min(oldColCount, newColCount);
for (let r = 0; r < minRowCount; r++) {
for (let c = 0; c < minColCount; c++) {
newLayer.cells[r][c] = layer.cells[r][c];
}
}
return newLayer;
});
this.history = [];
this.historyIndex = -1;
this.draw();
this.updateGridOutput();
}
//#region update
updateLayerList() {
const layerList = document.getElementById('layerList');
layerList.innerHTML = '';
this.layers.forEach((layer, index) => {
const li = document.createElement('li');
const radio = document.createElement('input');
radio.type = 'radio';
radio.name = 'layer';
radio.value = index;
radio.checked = index === this.currentLayerIndex;
radio.addEventListener('change', () => {
this.currentLayerIndex = index;
});
li.appendChild(radio);
li.appendChild(document.createTextNode(`Layer ${index + 1}`));
layerList.appendChild(li);
});
}
updateGridOutput() {
const jsonOutput = this.exportAsJson();
const hexOutput = this.exportAsHexArray();
document.getElementById('jsonOutput').value = jsonOutput;
document.getElementById('hexOutput').value = hexOutput;
}
//#endregion
//#region export
export(scale = 1) {
const canvas = document.createElement('canvas');
canvas.width = this.colCount * scale;
canvas.height = this.rowCount * scale;
const ctx = canvas.getContext('2d');
for (let layer of this.layers) {
for (let row = 0; row < this.rowCount; row++) {
for (let col = 0; col < this.colCount; col++) {
const color = layer.cells[row][col];
if (color.a > 0) {
ctx.fillStyle = `rgba(${color.r},${color.g},${color.b},${color.a / 255})`;
ctx.fillRect(col * scale, row * scale, scale, scale);
}
}
}
}
return canvas.toDataURL();
}
exportAsJson() {
const colorArray = this.layers.flatMap(layer =>
layer.cells.flatMap(row =>
row.map(color => ({ r: color.r, g: color.g, b: color.b, a: color.a }))
)
);
return JSON.stringify(colorArray);
}
exportAsHexArray() {
const hexArray = this.layers.flatMap(layer =>
layer.cells.flatMap(row =>
row.map(color => colorToHex(color))
)
);
return JSON.stringify(hexArray);
}
//#endregion
appendTo(container) {
container.appendChild(this.canvas);
}
}//Grid
class ProcessingTarget {
constructor(id, name, type) {
this.id = id;
this.name = name;
this.type = type; // 'grid', 'layer', or 'palette'
}
}
class PaletteManager {
constructor() {
this.palettes = [];
this.nextId = 1;
}
addPalette(rowCount, colCount, w, h, name, colors) {
const id = `palette_${this.nextId++}`;
const newPalette = new Palette(rowCount, colCount, w, h);
//drawもやる
newPalette.setColors(colors);
this.palettes.push({ id, name, palette: newPalette });
return id;
}
getPalette(id) {
return this.palettes.find(p => p.id === id)?.palette;
}
removePalette(id) {
this.palettes = this.palettes.filter(p => p.id !== id);
}
}
const paletteManager = new PaletteManager();
//DOMとしてのPalette
function createDOMPalette(id, name, width = 200, height = 200) {
const domPalette = document.createElement('div');
domPalette.className = 'palette-item';
domPalette.style.display = 'flex';
domPalette.style.flexDirection = 'column';
domPalette.style.alignItems = 'center'; // 中央揃え
const h4 = document.createElement('h4');
h4.textContent = name;
domPalette.appendChild(h4);
const canvas = document.createElement('canvas');
canvas.className = 'palette-canvas';
canvas.width = width;
canvas.height = height;
canvas.dataset.id = id;
domPalette.appendChild(canvas);
const button = document.createElement('button');
button.className = 'remove-palette-btn';
button.dataset.id = id;
button.textContent = 'Remove';
domPalette.appendChild(button);
return domPalette;
}
//managerからパレットを取り出し、新規にCanvasを含むDOMを作成して
//そこにパレットからCanvasをコピー、DOMをコンテナに突っ込んでいく
function updatePaletteDisplay() {
const container = document.getElementById('paletteContainer');
container.innerHTML = '';
paletteManager.palettes.forEach(({ id, name, palette }) => {
const newDOMPalette = createDOMPalette(id, name, palette.canvas.width, palette.canvas.height);
container.appendChild(newDOMPalette);
//createDOMPaletteで生成したDOM毎のcanvas
const domCanvas = newDOMPalette.querySelector('canvas');
// palette.canvas.width = domCanvas.width;
// palette.canvas.height = domCanvas.height;
domCanvas.getContext('2d').drawImage(palette.canvas, 0, 0);
});
}
function updateTargetSelect() {
const select = document.getElementById('targetSelect');
select.innerHTML = '<option value="">Select target</option>';
const gridOption = document.createElement('option');
gridOption.value = 'grid_main';
gridOption.textContent = 'Main Grid';
select.appendChild(gridOption);
grid.layers.forEach((layer, index) => {
const layerOption = document.createElement('option');
layerOption.value = `layer_${index}`;
layerOption.textContent = `Layer ${index + 1}`;
select.appendChild(layerOption);
});
paletteManager.palettes.forEach(({ id, name }) => {
const paletteOption = document.createElement('option');
paletteOption.value = id;
paletteOption.textContent = name;
select.appendChild(paletteOption);
});
}
function getSelectedTarget() {
const select = document.getElementById('targetSelect');
const [type, id] = select.value.split('_');
if (type === 'grid') {
return new ProcessingTarget('grid_main', 'Main Grid', 'grid');
} else if (type === 'layer') {
return new ProcessingTarget(`layer_${id}`, `Layer ${parseInt(id) + 1}`, 'layer');
} else {
const palette = paletteManager.palettes.find(p => p.id === select.value);
return palette ? new ProcessingTarget(palette.id, palette.name, 'palette') : null;
}
}
function scaleColors(colors, factor) {
return colors.map(color => new Color(
Math.min(255, Math.max(0, Math.round(color.r * factor))),
Math.min(255, Math.max(0, Math.round(color.g * factor))),
Math.min(255, Math.max(0, Math.round(color.b * factor))),
color.a
));
}
function invertColors(colors) {
return colors.map(color => new Color(255 - color.r, 255 - color.g, 255 - color.b, color.a));
}
function reduceColors(colors, k) {
const points = colors.map(color => [color.r, color.g, color.b]);
const centroids = simpleKMeans(points, k);
return colors.map(color => {
const nearest = centroids.reduce((prev, curr) =>
(euclideanDistance([color.r, color.g, color.b], prev) < euclideanDistance([color.r, color.g, color.b], curr)) ? prev : curr
);
return new Color(Math.round(nearest[0]), Math.round(nearest[1]), Math.round(nearest[2]), color.a);
});
}
function extractColorsFrequency(imageData, limit) {
const colorCounts = {};
for (let i = 0; i < imageData.data.length; i += 4) {
const color = `${imageData.data[i]},${imageData.data[i+1]},${imageData.data[i+2]}`;
colorCounts[color] = (colorCounts[color] || 0) + 1;
}
return Object.entries(colorCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, limit)
.map(([color]) => {
const [r, g, b] = color.split(',').map(Number);
return new Color(r, g, b);
});
}
function extractColorsSkip(imageData, limit) {
const colors = extractColorsFrequency(imageData, imageData.width * imageData.height);
const skip = Math.max(1, Math.floor(colors.length / limit));
return colors.filter((_, index) => index % skip === 0).slice(0, limit);
}
function extractColorsKmeans(imageData, k) {
const points = [];
for (let i = 0; i < imageData.data.length; i += 4) {
points.push([imageData.data[i], imageData.data[i+1], imageData.data[i+2]]);
}
const centroids = simpleKMeans(points, k);
return centroids.map(centroid => new Color(Math.round(centroid[0]), Math.round(centroid[1]), Math.round(centroid[2])));
}
function simpleKMeans(points, k, maxIterations = 20) {
let centroids = points.slice(0, k);
for (let iter = 0; iter < maxIterations; iter++) {
const clusters = Array(k).fill().map(() => []);
for (const point of points) {
let minDist = Infinity;
let closestCentroidIndex = 0;
for (let i = 0; i < k; i++) {
const dist = euclideanDistance(point, centroids[i]);
if (dist < minDist) {
minDist = dist;
closestCentroidIndex = i;
}
}
clusters[closestCentroidIndex].push(point);
}
const newCentroids = clusters.map(cluster =>
cluster.length > 0 ? averagePoint(cluster) : centroids[0]
);
if (centroids.every((centroid, i) =>
euclideanDistance(centroid, newCentroids[i]) < 0.001
)) {
break;
}
centroids = newCentroids;
}
return centroids;
}
function euclideanDistance(p1, p2) {
return Math.sqrt(p1.reduce((sum, v, i) => sum + Math.pow(v - p2[i], 2), 0));
}
function averagePoint(points) {
const sum = points.reduce((acc, p) => acc.map((v, i) => v + p[i]), Array(points[0].length).fill(0));
return sum.map(v => v / points.length);
}
function createColorObject(r, g, b) {
return { r, g, b, a: 255 };
}
function rgbToHex(r, g, b) {
return '#' + [r, g, b].map(x => {
const hex = x.toString(16);
return hex.length === 1 ? '0' + hex : hex;
}).join('');
}
function generateRGColorArray(rRowCount, gColCount) {
const colorArray = [];
for (let r = 0; r < rRowCount; r++) {
for (let g = 0; g < gColCount; g++) {
const rValue = Math.round((r / (rRowCount - 1)) * 255);
const gValue = Math.round((g / (gColCount - 1)) * 255);
colorArray.push(createColorObject(rValue, gValue, 0));
}
}
return colorArray;
}
function generateGBColorArray(gRowCount, bColCount) {
const colorArray = [];
for (let g = 0; g < gRowCount; g++) {
for (let b = 0; b < bColCount; b++) {
const gValue = Math.round((g / (gRowCount - 1)) * 255);
const bValue = Math.round((b / (bColCount - 1)) * 255);
colorArray.push(createColorObject(0, gValue, bValue));
}
}
return colorArray;
}
function generateBRColorArray(bRowCount, rColCount) {
const colorArray = [];
for (let b = 0; b < bRowCount; b++) {
for (let r = 0; r < rColCount; r++) {
const bValue = Math.round((b / (bRowCount - 1)) * 255);
const rValue = Math.round((r / (rColCount - 1)) * 255);
colorArray.push(createColorObject(rValue, 0, bValue));
}
}
return colorArray;
}
function updateGridSize() {
const newRows = parseInt(document.getElementById('rowInput').value);
const newCols = parseInt(document.getElementById('colInput').value);
if (newRows > 0 && newCols > 0) {
grid.resize(newRows, newCols, grid.width, grid.height);
} else {
alert("Invalid grid size. Please enter positive numbers.");
}
}
function handlePastedImage(e) {
const items = e.clipboardData.items;
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
const blob = items[i].getAsFile();
const reader = new FileReader();
reader.onload = function(event) {
const img = new Image();
img.onload = function() {
processImageForGrid(this);
};
img.src = event.target.result;
};
reader.readAsDataURL(blob);
}
}
}
function processImageForGrid(img) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const aspectRatio = img.width / img.height;
const gridAspectRatio = grid.colCount / grid.rowCount;
let srcWidth, srcHeight, destWidth, destHeight;
if (aspectRatio > gridAspectRatio) {
destWidth = grid.colCount;
destHeight = Math.min(grid.rowCount, Math.floor(destWidth / aspectRatio));
} else {
destHeight = grid.rowCount;
destWidth = Math.min(grid.colCount, Math.floor(destHeight * aspectRatio));
}
srcWidth = img.width;
srcHeight = (srcWidth / destWidth) * destHeight;
canvas.width = destWidth;
canvas.height = destHeight;
ctx.drawImage(img, 0, 0, srcWidth, srcHeight, 0, 0, destWidth, destHeight);
const imageData = ctx.getImageData(0, 0, destWidth, destHeight);
for (let y = 0; y < destHeight; y++) {
for (let x = 0; x < destWidth; x++) {
const i = (y * destWidth + x) * 4;
const r = imageData.data[i];
const g = imageData.data[i + 1];
const b = imageData.data[i + 2];
grid.setColorAtIndex(y, x, new Color(r, g, b));
}
}
grid.draw();
}
//#region Event Listeners
document.getElementById('addPaletteBtn').addEventListener('click', () => {
const name = prompt('Enter palette name:');
if (name) {
const colors = generateRGColorArray(4, 4);
const id = paletteManager.addPalette(name, colors);
updatePaletteDisplay();
updateTargetSelect();
}
});
document.getElementById('paletteContainer').addEventListener('click', (e) => {
if (e.target.classList.contains('remove-palette-btn')) {
const id = e.target.dataset.id;
paletteManager.removePalette(id);
updatePaletteDisplay();
updateTargetSelect();
}
});
document.getElementById('scaleBtn').addEventListener('click', () => {
const target = getSelectedTarget();
if (!target) return;
const factor = parseFloat(document.getElementById('processingParam').value);
if (isNaN(factor)) {
alert('Please enter a valid scale factor');
return;
}
if (target.type === 'grid') {
grid.layers.forEach(layer => {
layer.cells = layer.cells.map(row => row.map(color => scaleColors([color], factor)[0]));
});
grid.draw();
} else if (target.type === 'layer') {
const layerIndex = parseInt(target.id.split('_')[1]);
grid.layers[layerIndex].cells = grid.layers[layerIndex].cells.map(row => row.map(color => scaleColors([color], factor)[0]));
grid.draw();
} else if (target.type === 'palette') {
const palette = paletteManager.getPalette(target.id);
if (palette) {
palette.setColors(scaleColors(palette.colors, factor));
updatePaletteDisplay();
}
}
});
document.getElementById('invertBtn').addEventListener('click', () => {
const target = getSelectedTarget();
if (!target) return;
if (target.type === 'grid') {
grid.layers.forEach(layer => {
layer.cells = layer.cells.map(row => row.map(color => invertColors([color])[0]));
});
grid.draw();
} else if (target.type === 'layer') {
const layerIndex = parseInt(target.id.split('_')[1]);
grid.layers[layerIndex].cells = grid.layers[layerIndex].cells.map(row => row.map(color => invertColors([color])[0]));
grid.draw();
} else if (target.type === 'palette') {
const palette = paletteManager.getPalette(target.id);
if (palette) {
palette.setColors(invertColors(palette.colors));
updatePaletteDisplay();
}
}
});
document.getElementById('reduceColorsBtn').addEventListener('click', () => {
const target = getSelectedTarget();
if (!target) return;
const k = parseInt(document.getElementById('processingParam').value);
if (isNaN(k) || k <= 0) {
alert('Please enter a valid number of colors');
return;
}
if (target.type === 'grid') {
const allColors = grid.layers.flatMap(layer => layer.cells.flat());
const reducedColors = reduceColors(allColors, k);
let colorIndex = 0;
grid.layers.forEach(layer => {
layer.cells = layer.cells.map(row => row.map(() => reducedColors[colorIndex++]));
});
grid.draw();
} else if (target.type === 'layer') {
const layerIndex = parseInt(target.id.split('_')[1]);
const layerColors = grid.layers[layerIndex].cells.flat();
const reducedColors = reduceColors(layerColors, k);
grid.layers[layerIndex].cells = grid.layers[layerIndex].cells.map(row =>
row.map((_, i) => reducedColors[i])
);
grid.draw();
} else if (target.type === 'palette') {
const palette = paletteManager.getPalette(target.id);
if (palette) {
palette.setColors(reduceColors(palette.colors, k));
updatePaletteDisplay();
}
}
});
document.getElementById('updateGridSizeBtn').addEventListener('click', updateGridSize);
document.getElementById('imagePasteArea').addEventListener('paste', handlePastedImage);
// const input = document.getElementById('paletteInput').value;
// let colors = [];
// try {
// if (input.startsWith('#')) {
// colors = input.split(/\s+/).map(hex => hexToColor(hex));
// } else {
// const parsed = JSON.parse(input);
// if (Array.isArray(parsed)) {
// if (typeof parsed[0] === 'string') {
// colors = parsed.map(hex => hexToColor(hex));
// } else if (typeof parsed[0] === 'object') {
// colors = parsed.map(obj => new Color(obj.r, obj.g, obj.b, obj.a));
// }
// } else if (typeof parsed === 'object') {
// colors = [new Color(parsed.r, parsed.g, parsed.b, parsed.a)];
// }
// }
// palette.setColors(colors);
// } catch (e) {
// console.error("Failed to parse input:", e);
// alert("Failed to parse input. Please check the format and try again.");
// }
// });
// 変更後:
document.getElementById('loadPaletteBtn').addEventListener('click', () => {
const input = document.getElementById('paletteInput').value;
let colors = [];
try {
if (input.startsWith('#')) {
colors = input.split(/\s+/).map(hex => hexToColor(hex));
} else {
const parsed = JSON.parse(input);
if (Array.isArray(parsed)) {
if (typeof parsed[0] === 'string') {
colors = parsed.map(hex => hexToColor(hex));
} else if (typeof parsed[0] === 'object') {
colors = parsed.map(obj => new Color(obj.r, obj.g, obj.b, obj.a));
}
} else if (typeof parsed === 'object') {
colors = [new Color(parsed.r, parsed.g, parsed.b, parsed.a)];
}
}
const newPaletteId = paletteManager.addPalette("Loaded Palette", colors);
updatePaletteDisplay();
updateTargetSelect();
} catch (e) {
console.error("Failed to parse input:", e);
alert("Failed to parse input. Please check the format and try again.");
}
});
document.getElementById('imageInput').addEventListener('change', (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const method = document.getElementById('extractMethod').value;
let colors;
try {
switch (method) {
case 'frequency':
colors = extractColorsFrequency(imageData, 16);
break;
case 'skip':
colors = extractColorsSkip(imageData, 16);
break;
case 'kmeans':
colors = extractColorsKmeans(imageData, 16);
break;
default:
throw new Error("Invalid extraction method");
}
const newPaletteId = paletteManager.addPalette("Image Palette", colors);
updatePaletteDisplay();
updateTargetSelect();
} catch (error) {
console.error("Error extracting colors:", error);
alert("Failed to extract colors from the image. Please try again or use a different method.");
}
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
});
document.getElementById('resizeBtn').addEventListener('click', () => {
const newSize = prompt("Enter new grid size (e.g., '20,20' for 20x20 grid):", `${grid.rowCount},${grid.colCount}`);
if (newSize) {
const [rows, cols] = newSize.split(',').map(Number);
if (rows > 0 && cols > 0) {
grid.resize(rows, cols, grid.width, grid.height);
} else {
alert("Invalid grid size. Please enter positive numbers.");
}
}
});
document.getElementById('colorPicker').addEventListener('input', (event) => {
grid.currentColor = hexToColor(event.target.value);
});
document.querySelectorAll('.tool-btn').forEach(btn => {
btn.addEventListener('click', (event) => {
grid.tool = event.target.dataset.tool;
grid.lineStartCell = null;
});
});
document.getElementById('undoBtn').addEventListener('click', () => grid.undo());
document.getElementById('redoBtn').addEventListener('click', () => grid.redo());
document.getElementById('addLayerBtn').addEventListener('click', () => grid.addLayer());
document.getElementById('removeLayerBtn').addEventListener('click', () => grid.removeLayer());
document.getElementById('exportBtn').addEventListener('click', () => {
const dataURL = grid.export(grid.cellWidth);
window.open(dataURL);
});
document.getElementById('exportPixelBtn').addEventListener('click', () => {
const dataURL = grid.export(1);
window.open(dataURL);
});
document.getElementById('copyJsonBtn').addEventListener('click', () => {
const output = document.getElementById('jsonOutput');
output.select();
document.execCommand('copy');
alert("カラーオブジェクトデータがクリップボードにコピーされました!");
});
document.getElementById('exportJsonBtn').addEventListener('click', () => {
const output = document.getElementById('jsonOutput').value;
const blob = new Blob([output], {type: 'application/json'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'grid_colors.json';
a.click();
URL.revokeObjectURL(url);
});
document.getElementById('copyHexBtn').addEventListener('click', () => {
const output = document.getElementById('hexOutput');
output.select();
document.execCommand('copy');
alert("16進数配列データがクリップボードにコピーされました!");
});
document.getElementById('exportHexBtn').addEventListener('click', () => {
const output = document.getElementById('hexOutput').value;
const blob = new Blob([output], {type: 'text/plain'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'grid_colors_hex.txt';
a.click();
URL.revokeObjectURL(url);
});
//#endregion
// Initialize grid and palette
const gridContainer = document.getElementById('gridContainer');
const grid = new Grid(16, 16, 500, 500);
grid.appendTo(gridContainer);
grid.draw();
grid.updateLayerList();
const initialPaletteId = paletteManager.addPalette(32,32,200,200, "Initial Palette", generateRGColorArray(32,32));
updatePaletteDisplay();
updateTargetSelect();
// Initial grid output update
grid.updateGridOutput();
</script>
</body>
</html>
この記事が気に入ったらサポートをしてみませんか?