Przejdź do głównej treści

Cube Builder

Fotografia

Twórz profesjonalne profile kolorystyczne z łatwością!

Moja aplikacja umożliwia intuicyjne tworzenie i edycję siatek kolorów, które możesz eksportować jako:

  • LUT 3D w formacie Adobe Cube — idealne do zaawansowanej korekcji barw w popularnych programach do edycji wideo.
  • HaldCLUT — wszechstronne rozwiązanie dla przetwarzania obrazu w wielu narzędziach graficznych.

Klikaj, zmieniaj kolory i uzyskuj precyzyjne efekty bez zbędnych komplikacji. Odkryj nowe możliwości twórcze już teraz!

Jak obsługiwać aplikację?
  1. Wybierz rozmiar sześcianu w polu poziom LUT.
  2. Kliknij na dowolny kolor po lewej stronie na warstwę sześcianu wejściowego. Pojawi się okno wyboru koloru.
  3. Wybierz kolor co spowoduje zmianę koloru i będzie to widoczne na warstwie sześcianu po prawej stronie.
  4. Powtórz tą czynność klikając na inny kolor po lewej stronie.
  5. Wybierz kolejną warstwę w polu stronicowania poniżej obu sześcianów.
  6. Powtórz czynności 1, 2, 3, 4.
  7. Wybierz kolejną warstwę lub wróć do poprzedniej, jeśli to konieczne.
  8. W każdej chwili możesz zmienić wybrany kolor klikając ponownie na warstwę sześcianu po lewej.
  9. Zaznacz wygenerowany Adobe Cube lub Portable Pixmap Hald CLUT i skopiuj do schowka.
  10. Wklej skopiowany tekst do pliku tekstowego lub zapisz tekst używając przycisku pobierz.
Sześcian wejściowy
Sześcian wyjściowy

Adobe CUBE

Hald CLUT PPM

Kod po stronie przeglądarki

  <style>
    canvas {
      border: 1px solid black;
      display: inline-block;      
    }
      .pagination a {cursor: pointer;}  
  </style>
<div id="app">
    <div class="row mb-3">
        <div class="col-sm-3">
            <label class="form-label" for="level">Poziom LUT (2 - 6)</label>
            <input class="form-control" id="level" type=number min="2" max="6" step="1" v-model="level" v-debounce="{ handler: levelChange, delay: 500, event: 'change' }" />                               
        </div>
    </div>
    <div class="row mb-3">
        <div class="col-sm-6 text-center">
            <div>Sześcian wejściowy</div>
            <div class="mt-3">
                <canvas id="colorGridCanvas1" width="320" height="320" v-on:click=canvas1Click($event)></canvas>                
            </div>            
        </div>
        <div class="col-sm-6 text-center">
            <div>Sześcian wyjściowy</div>
            <div class="mt-3">
                <canvas id="colorGridCanvas2" width="320" height="320"></canvas>                      
            </div>            
        </div>
    </div>       
    <hr />
    <div class="d-flex justify-content-center text-center mb-3">
        <nav aria-label="Page navigation example">
            <ul class="pagination">
                <li class="page-item">
                    <a class="page-link" aria-label="Previous" v-on:click="setCurrentPage(currentPage-1)">
                        <span aria-hidden="true">&laquo;</span>
                    </a>
                </li>
                <li class="page-item" v-for="page in pages" v-bind:class="page == currentPage ? 'active' : ''"><a class="page-link" v-on:click="setCurrentPage(page)">{{page}}</a></li>
                <li class="page-item">
                    <a class="page-link" aria-label="Next" v-on:click="setCurrentPage(currentPage+1)">
                        <span aria-hidden="true">&raquo;</span>
                    </a>
                </li>
            </ul>
        </nav>
    </div>
    
    <div class="row mb-3">
        <div class="col-md-6">
            <h3>Adobe CUBE</h3>
            <pre class="vh-50 overflow-auto"><code class="language-cube" id="outputCube" v-text="outputCube"></code></pre>
        </div>
        <div class="col-md-6">
            <h3>Hald CLUT PPM</h3>
            <pre class="vh-50 overflow-auto"><code class="language-ppm" id="outputPPM" v-text="outputPPM"></code></pre>
        </div>
    </div>
    
    <div class="row mb-3">
        <div class="col-12">
            <button class="btn btn-primary m-1" v-on:click="downloadAdobeCube()">
                Pobierz Adobe Cube
            </button>
            <button class="btn btn-secondary m-1" v-on:click="downloadHaldClut()">
                Pobierz Hald CLUT
            </button>
        </div>
    </div>
    
    <!-- Begin Color Select Modal -->
    <div class="modal fade" id="colorModal" tabindex="-1" aria-labelledby="colorTitle" aria-hidden="true">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <div class="h5 modal-title" id="colorTitle">Wybierz kolor</div>
                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                </div>
                <div class="modal-body">
                    <div class="row my-3">
                        <div class="col-12">
                            <label class="form-label" for="newColor">Aktualnie wybrany kolor po prawej stronie</label>
                            <input type="color" id="newColor" v-model="newColor" class="form-control form-control-color w-100" />
                        </div>
                    </div>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Anuluj</button>
                    <button type="button" class="btn btn-primary" v-on:click="applyColorModal">OK</button>
                </div>
            </div>
        </div>
    </div>
    <!-- End Modal -->
       
</div>  
<script src="http://www.dariuszrorat.ugu.pl/assets/js/highlightjs/languages/cube.js" defer></script>
<script src="http://www.dariuszrorat.ugu.pl/assets/js/highlightjs/languages/ppm.js" defer></script>
<script>
let vm = null;
    
document.addEventListener('DOMContentLoaded', function() {    
Vue.directive('debounce', {
  bind(el, binding) {
    // binding.value = { handler, delay, event? }
    const { handler, delay = 300, event = 'input' } = binding.value;
    let timeoutId;

    const listener = (...args) => {
      clearTimeout(timeoutId);
      timeoutId = setTimeout(() => {
        handler.apply(null, args);
      }, delay);
    };

    el.__debounce_listener__ = listener;
    el.addEventListener(event, listener);
  },
  unbind(el, binding) {
    const { event = 'input' } = binding.value || {};
    el.removeEventListener(event, el.__debounce_listener__);
    delete el.__debounce_listener__;
  }
});
    
    vm = new Vue({
        el: '#app',
        data: {
            level: 2,
            currentPage: 1,                                    
            gridSize: 2,
            cellSize: 0,
            newColor: '',
            colorIndex: 0,
            inputData: [],
            outputData: [],
            outputCube: '',
            outputPPM: '',
            highlightNeeded: false,
            //canvas
            colorModal: null,
            canvas1: null,
            canvas2: null,
            ctx1: null,
            ctx2: null,
            colors1: [],
            colors2: []                                                    
        },
    updated() {      
            this.$nextTick(function () {
                const self = this;
                if (self.highlightNeeded)
                {                
                    const elCube = document.getElementById('outputCube');                
                    if (elCube)
                    {
                        if (elCube.hasAttribute('data-highlighted'))
                            elCube.removeAttribute('data-highlighted');
                        if (elCube.hasAttribute('data-highlighter'))
                            elCube.removeAttribute('data-highlighter');
                        if (elCube.classList.contains('hljs'))
                            elCube.classList.remove(...Array.from(elCube.classList).filter(cls => cls.startsWith('hljs')));
                        elCube.innerHTML = elCube.textContent;
                        hljs.highlightElement(elCube);                    
                    }   
                
                    const elPPM = document.getElementById('outputPPM');                
                    if (elPPM)
                    {
                        if (elPPM.hasAttribute('data-highlighted'))
                            elPPM.removeAttribute('data-highlighted');
                        if (elPPM.hasAttribute('data-highlighter'))
                            elPPM.removeAttribute('data-highlighter');
                        if (elPPM.classList.contains('hljs'))
                            elPPM.classList.remove(...Array.from(elPPM.classList).filter(cls => cls.startsWith('hljs')));
                        elPPM.innerHTML = elPPM.textContent;
                        hljs.highlightElement(elPPM);                    
                    }   
             
                    self.highlightNeeded = false;
                }
            });    
        },
        mounted: function ()
        {
            const self = this;
            this.$nextTick(function () {
                self.colorModal = new bootstrap.Modal(document.getElementById('colorModal'), {keyboard: true});
                
                self.canvas1 = document.getElementById('colorGridCanvas1');
                self.canvas2 = document.getElementById('colorGridCanvas2');
                self.ctx1 = self.canvas1.getContext('2d');
                self.ctx2 = self.canvas2.getContext('2d');
                self.cellSize = self.canvas1.width / self.gridSize;
                
                self.fillData();
                self.updateColors();
                self.printCube();
                self.printPPM();
                                
                self.drawGrid(self.ctx1, self.colors1);
                self.drawGrid(self.ctx2, self.colors2);
            });
        },
        methods: {
            rgbToHex: function(r, g, b) 
            {
                const toHex = color => color.toString(16).padStart(2, '0');
                return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
            },            
            hexToRgb: function(hex) 
            {
                if (!/^#([a-fA-F0-9]{3}|[a-fA-F0-9]{6})$/.test(hex)) 
                {
                    throw new Error("Nieprawidłowy format koloru hex");
                }
            
                if (hex.length === 4) 
                {
                    hex = `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}`;
                }

                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 { r, g, b };
            },
            flattenCube: function(level, b, g, r)
            {
                return b * level * level + g * level + r;
            },
            clampByte: function(x)
            {
                const n = Math.round(x);
                if (n > 255)
                {
                    return 255;        
                }
                else if (n < 0)
                {
                    return 0;
                }    
                return n;
            },
            fillData: function()
            {
                const self = this;
                self.inputData = [];
                self.outputData = [];
                
                for (let b = 0; b < self.level; b++)
                {
                    for (let g = 0; g < self.level; g++)
                    {
                        for (let r = 0; r < self.level; r++)
                        {
                            self.inputData.push({
                                R: (r / (self.level - 1)),
                                G: (g / (self.level - 1)),
                                B: (b / (self.level - 1))
                            });

                            self.outputData.push({
                                R: (r / (self.level - 1)),
                                G: (g / (self.level - 1)),
                                B: (b / (self.level - 1))
                            });
                        }    
                    }    
                }
            },
            updateColors: function()
            {
                const self = this;
                self.colors1 = [];
                self.colors2 = [];
                
                let i = 0;                
                for (let g = 0; g < self.level; g++)
                {
                    for (let r = 0; r < self.level; r++)
                    {
                        let j = i + (self.currentPage-1)*self.level*self.level;
                        self.colors1.push(self.rgbToHex(Math.round(self.inputData[j].R*255), 
                                                        Math.round(self.inputData[j].G*255),
                                                        Math.round(self.inputData[j].B*255)));

                        self.colors2.push(self.rgbToHex(Math.round(self.outputData[j].R*255), 
                                                        Math.round(self.outputData[j].G*255),
                                                        Math.round(self.outputData[j].B*255)));
                        i++;
                    }    
                }
            },
            drawGrid: function (ctx, colors) 
            {
                const self = this;
                for (let row = 0; row < self.gridSize; row++) 
                {
                    for (let col = 0; col < self.gridSize; col++) 
                    {
                        let colorIndex = row * self.gridSize + col;
                        ctx.fillStyle = colors[colorIndex];
                        ctx.fillRect(col * self.cellSize, row * self.cellSize, self.cellSize, self.cellSize);
                    }
                }
            },
            showColorModal: function(colorIndex, existingColor)
            {
                this.colorIndex = colorIndex;
                this.newColor = existingColor;
                this.colorModal.show();
            },
            applyColorModal: function()
            {
                const self = this;
                self.colors2[self.colorIndex] = self.newColor;
                self.drawGrid(self.ctx2, self.colors2);
                let j = (self.currentPage-1) * self.level * self.level + self.colorIndex;
                let rgb = self.hexToRgb(self.newColor);
                self.outputData[j] = {
                        R: rgb.r / 255,
                        G: rgb.g / 255,
                        B: rgb.b / 255
                };           
                self.printCube();
                self.printPPM();                
                self.colorModal.hide();
                self.highlightNeeded = true;
            },
            setCurrentPage: function(page)
            {
                const self = this;
                if ((page >= 1) && (page <= this.level))
                {
                    self.currentPage = page;
                    
                    self.updateColors();                                
                    self.drawGrid(self.ctx1, self.colors1);
                    self.drawGrid(self.ctx2, self.colors2);
                    
                }                
            },
            levelChange: function()
            {                
                const self = this;                
                self.gridSize = self.level;
                self.cellSize = self.canvas1.width / self.gridSize;
                
                self.fillData();
                self.updateColors();
                self.printCube();
                self.printPPM();
                                
                self.drawGrid(self.ctx1, self.colors1);
                self.drawGrid(self.ctx2, self.colors2);                    
                
                self.highlightNeeded = true;
            },
            //not used
            showPPMPreview: function()
            {
                const self = this;
                self.renderPPM('ppmCanvas', self.outputPPM.trim());    
            },
            canvas1Click: function(event)
            {
                const self = this;
                const rect = self.canvas1.getBoundingClientRect();
                const x = event.clientX - rect.left;
                const y = event.clientY - rect.top;

                const col = Math.floor(x / self.cellSize);
                const row = Math.floor(y / self.cellSize);
                const colorIndex = row * self.gridSize + col;
                
                self.showColorModal(colorIndex, self.colors2[colorIndex]);
            },
            download: function(filename, text) 
            {
                let element = document.createElement('a');
                element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
                element.setAttribute('download', filename);
                element.style.display = 'none';
                document.body.appendChild(element);
                element.click();
                document.body.removeChild(element);
            },
            downloadAdobeCube: function()
            {
                this.download('output.cube', this.outputCube);    
            },
            downloadHaldClut: function()
            {
                this.download('output.ppm', this.outputPPM);
            },
            printCube: function()
            {
                const self = this;
                let text = 'TITLE "MYLUT"\n'
                text += 'DOMAIN_MIN 0 0 0\n';
                text += 'DOMAIN_MAX 1 1 1\n';
                text += 'LUT_3D_SIZE ' + self.level + '\n';
                
                for (let i = 0; i < self.outputData.length; i++)
                {
                    let rgb = self.outputData[i];
                    text += rgb.R.toFixed(9) + ' ' + rgb.G.toFixed(9) + ' ' + rgb.B.toFixed(9) + '\n';
                }
                
                self.outputCube = text;                
            },
            printPPM: function()
            {
                const self = this;
                let text = 'P3\n';
                text += '# Created by Cube Builder\n';                
                text += self.level*self.level*self.level + ' ' + self.level*self.level*self.level + '\n';
                text += '255\n';
                
                let PN = 0;
                for (let b = 0; b < self.level*self.level; b++)
                {
                    for (let g = 0; g < self.level*self.level; g++)
                    {
                        for (let r = 0; r < self.level*self.level; r++)
                        {
                            let offsetR = (1.0 / (self.level*self.level - 1.0)) * r * (self.level - 1);
                            let offsetG = (1.0 / (self.level*self.level - 1.0)) * g * (self.level - 1);
                            let offsetB = (1.0 / (self.level*self.level - 1.0)) * b * (self.level - 1);
                            let indexR = Math.floor(offsetR);
                            let indexG = Math.floor(offsetG);
                            let indexB = Math.floor(offsetB);
                            let scaleR = offsetR - indexR;
                            let scaleG = offsetG - indexG;
                            let scaleB = offsetB - indexB;
                            let nextR = indexR + 1;
                            let nextG = indexG + 1;
                            let nextB = indexB + 1;
                            
                            if (indexR == (self.level-1))
                            {
                                nextR = indexR;    
                            }
                            if (indexG == (self.level-1))
                            {
                                nextG = indexG;    
                            }
                            if (indexB == (self.level-1))
                            {
                                nextB = indexB;    
                            }
                            
                            let PR = self.clampByte(255.0 * (self.outputData[self.flattenCube(self.level, indexB, indexG, indexR)].R
                                                   + scaleR * (self.outputData[self.flattenCube(self.level, indexB, indexG, nextR)].R
                                                              - self.outputData[self.flattenCube(self.level, indexB, indexG, indexR)].R)));
                            let PG = self.clampByte(255.0 * (self.outputData[self.flattenCube(self.level, indexB, indexG, indexR)].G
                                                   + scaleG * (self.outputData[self.flattenCube(self.level, indexB, nextG, indexR)].G
                                                              - self.outputData[self.flattenCube(self.level, indexB, indexG, indexR)].G)));
                            let PB = self.clampByte(255.0 * (self.outputData[self.flattenCube(self.level, indexB, indexG, indexR)].B
                                                   + scaleB * (self.outputData[self.flattenCube(self.level, nextB, indexG, indexR)].B
                                                              - self.outputData[self.flattenCube(self.level, indexB, indexG, indexR)].B)));
                            text += PR + ' ' + PG + ' ' + PB + ' ';
                            
                            PN++;                            
                            if (PN == 5)
                            {
                                text += "\n";
                                PN = 0;
                            }
                        }    
                    }    
                }    
                
                self.outputPPM = text;                
            },
            parsePPM: function(ppm)
            {
                const lines = ppm.split('\n').filter(line => !line.startsWith('#') && line.trim() !== '');
                if (lines[0] !== 'P3') throw new Error("Unsupported format: " + lines[0]);

                const [width, height] = lines[1].split(' ').map(Number);
                const maxVal = parseInt(lines[2]);

                const pixelValues = lines.slice(3).join(' ').trim().split(/\s+/).map(Number);
                const pixels = [];

                for (let i = 0; i < pixelValues.length; i += 3) {
                    pixels.push({
                        r: pixelValues[i],
                        g: pixelValues[i + 1],
                        b: pixelValues[i + 2],
                    });
                }

                return { width, height, pixels };     
            },
            renderPPM: function(canvasId, ppmString) 
            {
                const self = this;
                const canvas = document.getElementById(canvasId);
                const ctx = canvas.getContext('2d');

                const { width, height, pixels } = self.parsePPM(ppmString);

                canvas.width = width;
                canvas.height = height;

                const imageData = ctx.createImageData(width, height);

                for (let i = 0; i < pixels.length; i++) {
                    const { r, g, b } = pixels[i];
                    const idx = i * 4;
                    imageData.data[idx] = r;
                    imageData.data[idx + 1] = g;
                    imageData.data[idx + 2] = b;
                    imageData.data[idx + 3] = 255; // alpha
                }

                ctx.putImageData(imageData, 0, 0);
            } 
        },
        computed: {
            pages: function()
            {
                let data = [];
                for (let i = 0; i < this.level; i++)                                
                {
                    data.push(i+1);                                
                }                  
                return data;                                
            }
        }
    });
});                                                
</script>

Kod po stronie serwera

Brak kodu serwera

Ta aplikacja działa wyłącznie w przeglądarce i nie korzysta z kodu po stronie serwera.

Licencja

## BSD-3-Clause License Agreement
BSD-3-Clause
Сopyright (c) 2026 Dariusz Rorat

All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice,
   this list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its
   contributors may be used to endorse or promote products derived from
   this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
9 lutego 2025 4

Kategorie

Technologie

Dziękujemy!
()

Informacja o cookies

Moja strona internetowa wykorzystuje wyłącznie niezbędne pliki cookies, które są wymagane do jej prawidłowego działania. Nie używam ciasteczek w celach marketingowych ani analitycznych. Korzystając z mojej strony, wyrażasz zgodę na stosowanie tych plików. Możesz dowiedzieć się więcej w mojej polityce prywatności.