Cube Builder
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!
- Wybierz rozmiar sześcianu w polu poziom LUT.
- Kliknij na dowolny kolor po lewej stronie na warstwę sześcianu wejściowego. Pojawi się okno wyboru koloru.
- Wybierz kolor co spowoduje zmianę koloru i będzie to widoczne na warstwie sześcianu po prawej stronie.
- Powtórz tą czynność klikając na inny kolor po lewej stronie.
- Wybierz kolejną warstwę w polu stronicowania poniżej obu sześcianów.
- Powtórz czynności 1, 2, 3, 4.
- Wybierz kolejną warstwę lub wróć do poprzedniej, jeśli to konieczne.
- W każdej chwili możesz zmienić wybrany kolor klikając ponownie na warstwę sześcianu po lewej.
- Zaznacz wygenerowany Adobe Cube lub Portable Pixmap Hald CLUT i skopiuj do schowka.
- Wklej skopiowany tekst do pliku tekstowego lub zapisz tekst używając przycisku pobierz.
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">«</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">»</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.