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>
var 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 () {
                var 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 ()
        {
            var 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()
            {
                var 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()
            {
                var 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) 
            {
                var 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()
            {
                var 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)
            {
                var 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()
            {                
                var 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()
            {
                var self = this;
                self.renderPPM('ppmCanvas', self.outputPPM.trim());    
            },
            canvas1Click: function(event)
            {
                var 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) 
            {
                var 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()
            {
                var 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()
            {
                var 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) 
            {
                var 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

Informacja

Aplikacja nie korzysta z kodu po stronie serwera.

Tagi

Fotografia

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.