Dot絵エディター5



240714


DOM系

ボタン

二手いるやつ

                        <div id="layerControls">
                            <button onclick="handleClearGrid()">Clear Grid</button>
                            <button onclick="handleClearCurrentLayer()">Clear Current Layer</button>                    
                            <button onclick="grid.addLayer()">Add Layer</button>                    

                            <button id="addLayerBtn" class="layerControls-btn">Add Layer</button>
                            <button id="removeLayerBtn" class="layerControls-btn">Remove Layer</button>
                            <button id="showSelectedLayerBtn" class="layerControls-btn">Show Only Selected</button>
                            <button id="renameLayerBtn" class="layerControls-btn">Rename Layer</button>
                            <button id="copyLayerBtn" class="layerControls-btn">Copy Layer</button>
                            <button id="mergeLayerBtn" class="layerControls-btn">Merge Layer</button>
                            <ul id="layerList"></ul>
                        </div>


    document.getElementById('addLayerBtn').addEventListener('click', () => grid.addLayer());
    document.getElementById('removeLayerBtn').addEventListener('click', () => grid.removeLayer());
    document.getElementById('renameLayerBtn').addEventListener('click', () => handleRenameCurrentLayer());
    document.getElementById('copyLayerBtn').addEventListener('click', () => handleCopyCurrentLayer());
    document.getElementById('mergeLayerBtn').addEventListener('click', () => mergeCurrentLayer());
    document.getElementById('showSelectedLayerBtn').addEventListener('click', () => handleShowOnlySelectedLayer());

いらんやつ

                            <button onclick="handleClearGrid()">Clear Grid</button>
                            <button onclick="handleClearCurrentLayer()">Clear Current Layer</button>                    
                            <button onclick="grid.addLayer()">Add Layer</button>                    
                            <button onclick="grid.removeLayer()">Remove Layer</button>


画像入力->画素抽出のメソッド

                        <select id="extractMethod">
                            <option value="simple">Simple</option>
                            <option value="average">Average</option>
                            <option value="dithering">Dithering</option>
                            <option value="frequency">Frequency</option>         
                            <option value="skip">Skip</option>    
                            <option value="kmeans">KMeans</option>                       
                        </select>          

methodParams

                                    
                                        <div id="methodParams">
                                            <div id="simpleParams" style="display: none;">  
                                                <div>
                                                    <label for="preResize">Pre-Resize:</label>
                                                    <input type="checkbox" class="preResize" checked>
                                                </div>                                                
                                            </div>                                               
                                            <div id="averageParams" style="display: none;">
                                                <label for="paletteSize">Block Size:</label>
                                                <input type="number" id="blockWidth" min="1" max="256" value="4">
                                                <input type="number" id="blockHeight" min="1" max="256" value="4">    
                                                <div>
                                                    <label for="preResize">Pre-Resize:</label>
                                                    <input type="checkbox" class="preResize">
                                                </div>                                                
                                            </div>                                            
                                            <div id="ditheringParams" style="display: none;">
                                                <label for="paletteSize">Palette Size:</label>
                                                <input type="number" id="paletteSize" min="2" max="256" value="16">
                                                <div>
                                                    <label for="preResize">Pre-Resize:</label>
                                                    <input type="checkbox" class="preResize">
                                                </div>                                                     
                                            </div>
                                            <div id="frequencyParams" style="display: none;">
                                                <label for="colorCount">Color Count:</label>
                                                <input type="number" id="colorCount" min="1" max="256" value="16">
                                                <div>
                                                    <label for="preResize">Pre-Resize:</label>
                                                    <input type="checkbox" class="preResize">
                                                </div>                                                     
                                            </div>
                                            <div id="skipParams" style="display: none;">
                                                <label for="skipFactor">Skip Factor:</label>
                                                <input type="number" id="skipFactor" min="1" max="100" value="10">
                                                <div>
                                                    <label for="preResize">Pre-Resize:</label>
                                                    <input type="checkbox" class="preResize">
                                                </div>                                                     
                                            </div>
                                            <div id="kmeansParams" style="display: none;">
                                                <label for="kValue">K Value:</label>
                                                <input type="number" id="kValue" min="2" max="256" value="16">
                                                <div>
                                                    <label for="preResize">Pre-Resize:</label>
                                                    <input type="checkbox" class="preResize">
                                                </div>                                                     
                                            </div>
                                        </div>
                                    

メソッド選択時出現DOM

    document.getElementById('extractionMethod').addEventListener('change', function() {
        const method = this.value;
        document.querySelectorAll('#methodParams > div').forEach(div => div.style.display = 'none');
        document.getElementById(`${method}Params`).style.display = 'block';
        // if (method !== 'simple') {
        //     document.getElementById(`${method}Params`).style.display = 'block';
        // }
    });

#methodParamsはidセレクタ。
document.querySelectorAll('#methodParams > div')で、IDがmethodParamsの要素内にある全てのdiv要素を取得します。
forEach(div => div.style.display = 'none')で、全ての取得したdiv要素の表示スタイルを'none'に設定し、非表示にします。
パラメータが無いなら表示しないという選択肢もありもうすが、simpleAverageにもパラメータがついたので消えた。


処理ボタン

セレクタからメソッドを回収する
メソッドに応じたパラメータ入力DOMからパラメータを回収する
処理へ



    document.getElementById('processImageBtn').addEventListener('click', () => {
        if (heldImage) {
            const method = document.getElementById('extractionMethod').value;
            let params = {};

            // console.log(method);

            switch (method) {
                case 'simple':
                    params.preResize = document.querySelector('#simpleParams .preResize').checked; 
                    break;                
                case 'average':
                    params.blockWidth = parseInt(document.getElementById('blockWidth').value);
                    params.blockHeight = parseInt(document.getElementById('blockHeight').value);
                    params.preResize = document.querySelector('#averageParams .preResize').checked; 
                    break;                
                case 'dithering':
                    params.palette = getPaletteColors();
                    params.preResize = document.querySelector('#ditheringParams .preResize').checked; 
                    break;

                case 'frequency':
                    params.limit = parseInt(document.getElementById('colorCount').value);
                    params.preResize = document.querySelector('#frequencyParams .preResize').checked; 
                    break;
                case 'skip':
                    params.limit = parseInt(document.getElementById('skipFactor').value);
                    params.preResize = document.querySelector('#skipParams .preResize').checked; 
                    break;
                case 'kmeans':
                    params.k = parseInt(document.getElementById('kValue').value);
                    params.preResize = document.querySelector('#kmeansParams .preResize').checked; 
                    break;
            }
            processImageForGridAndPalette(heldImage, method, params);
        } else {
            alert('Please select or paste an image first.');
        }
    });

processImageForGridAndPalette

元の画像データから色を抽出し、
レイヤー(二次元色配列)もしくはパレット(一次元色配列)に適用する。事前にリサイズしてから色抽出するか、そのまま抽出するかの選択肢がある。


function processImageForGridAndPalette(img, method, params) {
    const targetOption = document.getElementById('inputTargetSelect').value;
    const [target, mode] = targetOption.split(' ');

    console.log(method);
    console.log(params);

    let imageData, colors;

    if (target === 'layer') {
        // imageData = resizeImage(img, grid.colCount, grid.rowCount);
        if (params.preResize) {
            console.log("resize");
            imageData = resizeImage(img, grid.colCount, grid.rowCount);
        } else {
            console.log("notResize");
            imageData = getImageData(img); // リサイズしない場合は元の画像データを使用
        }        
        colors = extractColors(imageData, method, params);

        if (mode === 'Current') {
            applyColorsToGrid(grid, colors, imageData.width, imageData.height);
        } else if (mode === 'new') {
            const newLayerIndex = grid.addLayer();
            applyColorsToGrid(grid, colors, imageData.width, imageData.height, newLayerIndex);
        }
        grid.draw();
    } else if (target === 'palette') {
        const paletteRowsValue = parseInt(document.getElementById("paletteRows").value);
        const paletteColsValue = parseInt(document.getElementById("paletteCols").value);
        
        // imageData = resizeImage(img, paletteColsValue, paletteRowsValue);
        if (params.preResize) {
            imageData = resizeImage(img, grid.colCount, grid.rowCount);
        } else {
            imageData = getImageData(img); // リサイズしない場合は元の画像データを使用
        }        
        colors = extractColors(imageData, method, params);

        if (mode === 'Current') {
            const currentPalette = paletteManager.getCurrentPalette();
            if (currentPalette) {
                applyColorsToPalette(currentPalette, colors, imageData.width, imageData.height);
            }
        } else if (mode === 'new') {
            const newPalette = new Palette(paletteRowsValue, paletteColsValue, 200, 200);
            applyColorsToPalette(newPalette, colors, imageData.width, imageData.height);
            paletteManager.addPalette(paletteRowsValue, paletteColsValue, 200, 200, "画像パレット", newPalette.colors);
        }
        paletteManager.updatePaletteDisplay();
    }
}


extract

extractColorsSimple

左上、というか色データの戦闘から順番に取得
これだけだとほとんど意味がないが、事前にリサイズしてから取得するとうまいこと全体がとれる。

// 画像データから色を抽出する関数(シンプルな方法)
function extractColorsSimple(imageData) {
    const colors = [];
    for (let i = 0; i < imageData.data.length; i += 4) {
        colors.push(new Color(
            imageData.data[i],
            imageData.data[i + 1],
            imageData.data[i + 2],
            imageData.data[i + 3]
        ));
    }
    return colors;
}

extractColorsAverage

ブロックごとに色を平均して取得

// 平均色を使用する方法
function extractColorsAverage(imageData, blockWidth, blockHeight) {
    const colors = [];
    for (let y = 0; y < imageData.height; y += blockHeight) {
        for (let x = 0; x < imageData.width; x += blockWidth) {
            let r = 0, g = 0, b = 0, a = 0, count = 0;
            for (let dy = 0; dy < blockHeight && y + dy < imageData.height; dy++) {
                for (let dx = 0; dx < blockWidth && x + dx < imageData.width; dx++) {
                    const i = ((y + dy) * imageData.width + (x + dx)) * 4;
                    r += imageData.data[i];
                    g += imageData.data[i + 1];
                    b += imageData.data[i + 2];
                    a += imageData.data[i + 3];
                    count++;
                }
            }
            colors.push(new Color(
                Math.round(r / count),
                Math.round(g / count),
                Math.round(b / count),
                Math.round(a / count)
            ));
        }
    }
    return colors;
}

extractColorsWithDithering

ディザリング
パレットを参照できるように改造する必要がある。

// ディザリングを適用する方法
function extractColorsWithDithering(imageData, palette) {
    const colors = [];
    const error = new Array(imageData.width * imageData.height * 3).fill(0);

    for (let y = 0; y < imageData.height; y++) {
        for (let x = 0; x < imageData.width; x++) {
            const i = (y * imageData.width + x) * 4;
            const r = imageData.data[i] + error[(y * imageData.width + x) * 3];
            const g = imageData.data[i + 1] + error[(y * imageData.width + x) * 3 + 1];
            const b = imageData.data[i + 2] + error[(y * imageData.width + x) * 3 + 2];

            const closestColor = findClosestColor(r, g, b, palette);
            colors.push(closestColor);

            const er = r - closestColor.r;
            const eg = g - closestColor.g;
            const eb = b - closestColor.b;

            if (x + 1 < imageData.width) {
                error[(y * imageData.width + x + 1) * 3] += er * 7 / 16;
                error[(y * imageData.width + x + 1) * 3 + 1] += eg * 7 / 16;
                error[(y * imageData.width + x + 1) * 3 + 2] += eb * 7 / 16;
            }
            if (y + 1 < imageData.height) {
                if (x > 0) {
                    error[((y + 1) * imageData.width + x - 1) * 3] += er * 3 / 16;
                    error[((y + 1) * imageData.width + x - 1) * 3 + 1] += eg * 3 / 16;
                    error[((y + 1) * imageData.width + x - 1) * 3 + 2] += eb * 3 / 16;
                }
                error[((y + 1) * imageData.width + x) * 3] += er * 5 / 16;
                error[((y + 1) * imageData.width + x) * 3 + 1] += eg * 5 / 16;
                error[((y + 1) * imageData.width + x) * 3 + 2] += eb * 5 / 16;
                if (x + 1 < imageData.width) {
                    error[((y + 1) * imageData.width + x + 1) * 3] += er * 1 / 16;
                    error[((y + 1) * imageData.width + x + 1) * 3 + 1] += eg * 1 / 16;
                    error[((y + 1) * imageData.width + x + 1) * 3 + 2] += eb * 1 / 16;
                }
            }
        }
    }
    return colors;
}

function findClosestColor(r, g, b, palette) {
    let minDistance = Infinity;
    let closestColor = null;
    for (const color of palette) {
        const distance = Math.sqrt(
            Math.pow(r - color.r, 2) +
            Math.pow(g - color.g, 2) +
            Math.pow(b - color.b, 2)
        );
        if (distance < minDistance) {
            minDistance = distance;
            closestColor = color;
        }
    }
    return closestColor;
}

extractColorsFrequency

色の出現頻度


// 画像から最も頻繁に使用されている色を抽出する関数
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;
    }
    // 頻度順にソートし、上位limit個の色を返す
    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);
        });
}

extractColorsSkip

ステップごとに抽出する。

// 画像から等間隔で色を抽出する関数
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);
}

extractColorsKmeans

kmeanでクラスタリングしてから抽出する。

// k-meansアルゴリズムを使用して画像から代表的な色を抽出する関数
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])));
}


LayerManagerV2

index管理からid管理へ

レイヤーが配列構造ならindexで管理できる。
Group化を考える場合、考慮する項目が増える。

Groupが各々配列を持つ場合、
配列の中に配列を持ち、そこからさらに配列を……となる。
この場合、

深さ優先で1つの配列とみなすやり方
id管理
indexのリストで管理
Groupをidで管理し、そっからindex管理

などが考えられる。

id管理を考える場合、
一意のidを生成するコスト、衝突問題がある。
indexなら足したり引いたりするだけだったレイヤーの上下移動がめんどくさくなる。

基本的にindex管理の時に可能だった操作に対応するid管理の操作を作成すれば、その実行に掛かる時間を考えなければ問題はない。

ID

class Layer {
    constructor(rowCount, colCount, name = "") {
        this.id = `layer_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
        this.cells = Array(rowCount).fill().map(() => Array(colCount).fill(new Color(255, 255, 255, 0)));
        this.name = name;
        this.blendMode = 'normal';
        this.visible = true;
        this.opacity = 1;
        this.locked = false;
    }
}

この部分。

this.id = `layer_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;

ミリ秒単位のタイムスタンプと乱数を組み合わせればだいたい一意だろうとClaude3.5くんからの納得のいく回答。ただ、使ってる疑似乱数ががタイムスタンプを取っ掛かりにしてたらなんとも言えんとは思う。

ID管理の場合、だいたいの操作で探索が発生する。
以下は多分深さ優先。

    findLayer(id, layers = this.layers) {
        for (const layer of layers) {
            if (layer.id === id) return layer;
            if (layer instanceof LayerGroup) {
                const found = this.findLayer(id, layer.layers);
                if (found) return found;
            }
        }
        return null;
    }


add/create

配列の末尾に追加し、
追加されたレイヤーをCurrentLayerとするタイプ

createLayer(rowCount, colCount, name = "") {
    this.layerCount++;
    const layer = new Layer(rowCount, colCount, name || `レイヤー ${this.layerCount}`);
    this.layers.push(layer);
    this.currentLayerIndex = this.layers.length - 1;
    return {newLayer:layer, newLayerIndex:this.currentLayerIndex};
}

ID型

    createLayer(rowCount, colCount, name = "", parentId = null) {
        this.layerCount++;
        const layer = new Layer(rowCount, colCount, name || `レイヤー ${this.layerCount}`);
        if (parentId) {
            const parent = this.findLayer(parentId);
            if (parent instanceof LayerGroup) {
                parent.layers.push(layer);
            } else {
                throw new Error("Parent is not a LayerGroup");
            }
        } else {
            this.layers.push(layer);
        }
        this.currentLayerId = layer.id;
        return { newLayer: layer, newLayerId: layer.id };
    }

at/insert

配列の指定indexに追加。
追加されたレイヤーをCurrentLayerとする。

//指定位置に作成
createLayerAt(index, rowCount, colCount, name = "") {
     this.layerCount++;
     const layer = new Layer(rowCount, colCount, name || `レイヤー ${this.layerCount}`);
     this.layers.splice(index, 0, layer);
     this.currentLayerIndex = index;
     return {newLayer:layer, newLayerIndex:this.currentLayerIndex};
}

ID, index型

親、すなわちlayer配列の保持者(ここではGroup)を探索。見つからなければthis(ここではLayerManager)が親となる。

    insertLayer(parentId, index, layer) {
        this.layerCount++;
        const parent = parentId ? this.findLayer(parentId) : this;
        if (parent instanceof LayerGroup || parent === this) {
            parent.layers.splice(index, 0, layer);
            this.currentLayerId = layer.id;
        }
    }

ID, index型
やってること多分同じ。

    createLayerAt(index, rowCount, colCount, name = "", parentId = null) {
        this.layerCount++;
        const layer = new Layer(rowCount, colCount, name || `レイヤー ${this.layerCount}`);
        if (parentId) {
            const parent = this.findLayer(parentId);
            if (parent instanceof LayerGroup) {
                parent.layers.splice(index, 0, layer);
            } else {
                throw new Error("Parent is not a LayerGroup");
            }
        } else {
            this.layers.splice(index, 0, layer);
        }
        this.currentLayerId = layer.id;
        return { newLayer: layer, newLayerId: layer.id };
    }

remove

削除するレイヤーがCurrentLayerかどうかで処理が異なるが
index管理なら配列の範囲を外れなければ適当でも良い。

removeLayer(index) {
     if (this.layers.length > 1 && index >= 0 && index < this.layers.length) {
         this.layers.splice(index, 1);
         this.currentLayerIndex = Math.min(this.currentLayerIndex, this.layers.length - 1);
     }
}

ID型

再帰的に探索してヒットしたら削除。
動作的にはfind(id)するのと変わらないと思われる。
ただし削除成功およびそれがCurrentLayerだった場合、
追加処理が発生する。

    removeLayer(id) {
        const removeLayerRecursive = (layers) => {
            for (let i = 0; i < layers.length; i++) {
                if (layers[i].id === id) {
                    layers.splice(i, 1);
                    return true;
                }
                if (layers[i] instanceof LayerGroup) {
                    if (removeLayerRecursive(layers[i].layers)) {
                        return true;
                    }
                }
            }
            return false;
        };

        if (removeLayerRecursive(this.layers)) {
            // レイヤーが削除された場合、現在のレイヤーを更新
            if (this.currentLayerId === id) {
                const flatLayers = this.getFlattenedLayers();
                this.currentLayerId = flatLayers.length > 0 ? flatLayers[0].id : null;
            }
        }
    }

ここでは結局1つの配列に統合して順序を明確にしている。

    getFlattenedLayers() {
        const flattenLayers = (layers) => {
            return layers.reduce((acc, layer) => {
                if (layer instanceof LayerGroup) {
                    return [...acc, ...flattenLayers(layer.layers)];
                }
                return [...acc, layer];
            }, []);
        };
        return flattenLayers(this.layers);
    }


find

再掲

    findLayer(id, layers = this.layers) {
        for (const layer of layers) {
            if (layer.id === id) return layer;
            if (layer instanceof LayerGroup) {
                const found = this.findLayer(id, layer.layers);
                if (found) return found;
            }
        }
        return null;
    }

layerの親(ここではLayerManagerかLayerGroup)を返す

    findParent(layerId, parent = this) {
        for (let i = 0; i < parent.layers.length; i++) {
            if (parent.layers[i].id === layerId) {
                return parent;
            }
            if (parent.layers[i] instanceof LayerGroup) {
                const result = this.findParent(layerId, parent.layers[i]);
                if (result) return result;
            }
        }
        return null;
    }    


path

indexのリストに基づく。

    // レイヤーの階層構造を考慮したインデックスを取得
    getLayerPath(id) {
        const path = [];
        const findPath = (layers, targetId, currentPath) => {
            for (let i = 0; i < layers.length; i++) {
                const layer = layers[i];
                const newPath = [...currentPath, i];
                if (layer.id === targetId) {
                    path.push(...newPath);
                    return true;
                }
                if (layer instanceof LayerGroup) {
                    if (findPath(layer.layers, targetId, newPath)) {
                        return true;
                    }
                }
            }
            return false;
        };
        findPath(this.layers, id, []);
        return path;
    }

    // パス(indexのリスト)に基づいてレイヤーを取得
    getLayerByPath(path) {
        let current = this.layers;
        for (const index of path) {
            if (current[index] instanceof LayerGroup) {
                current = current[index].layers;
            } else {
                return current[index];
            }
        }
        return null;
    }

この記事が気に入ったらサポートをしてみませんか?