mirror of
https://github.com/revarbat/BOSL2.git
synced 2025-08-21 07:01:26 +02:00
Added cropping UI to img2scad.html
This commit is contained in:
@@ -59,6 +59,62 @@ Versions 1-5: 22 April 2025 - by Alex Matulich
|
||||
|
||||
.tooltip:hover .tooltiptext {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* cropping control panel stuff */
|
||||
|
||||
.crop-container {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"top top top"
|
||||
"left center right"
|
||||
"bottom bottom bottom";
|
||||
grid-template-columns: auto 60px auto;
|
||||
grid-template-rows: auto 60px auto;
|
||||
gap: 4px;
|
||||
padding: 2px;
|
||||
box-sizing: border-box;
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.crop-center {
|
||||
grid-area: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border: 2px dashed #ccc;
|
||||
background-color: #eee;
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.crop-control {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.crop-control input[type="number"] {
|
||||
width: 6ch;
|
||||
padding: 2px;
|
||||
font-size: 1rem;
|
||||
text-align: right;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.crop-top { grid-area: top; }
|
||||
.crop-left { grid-area: left; }
|
||||
.crop-right { grid-area: right; }
|
||||
.crop-bottom {
|
||||
grid-area: bottom;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -82,15 +138,34 @@ Versions 1-5: 22 April 2025 - by Alex Matulich
|
||||
|
||||
<div class="uiContainer" id="outputArea">
|
||||
<div>
|
||||
|
||||
<h3>Transformations</h3>
|
||||
<label for="resizeWidth">Rescale original width (px):</label>
|
||||
<input type="number" id="resizeWidth" size="6" min="1" placeholder="e.g. 200" value="200"><br>
|
||||
<input type="number" id="resizeWidth" size="5" min="1" max="9000" value="100"><br>
|
||||
<button id="rotateLeft">⟲ Rotate left</button>
|
||||
<button id="rotateRight">⟳ Rotate right</button><br>
|
||||
<button id="flipHorizontal">⇋ Flip horizontal</button>
|
||||
<button id="flipVertical">⇵ Flip vertical</button>
|
||||
|
||||
<div class="crop-container">
|
||||
<div class="crop-control crop-top">
|
||||
<label for="crop-top">Top</label>
|
||||
<input type="number" id="cropTop" min="0" max="9999" value="0">
|
||||
</div>
|
||||
<div class="crop-control crop-left">
|
||||
<label for="crop-left">Left</label>
|
||||
<input type="number" id="cropLeft" min="0" max="9999" value="0">
|
||||
</div>
|
||||
<div class="crop-center">Crop</div>
|
||||
<div class="crop-control crop-right">
|
||||
<label for="crop-right">Right</label>
|
||||
<input type="number" id="cropRight" min="0" max="9999" value="0">
|
||||
</div>
|
||||
<div class="crop-control crop-bottom">
|
||||
<input type="number" id="cropBottom" min="0" max="9999" value="0">
|
||||
<label for="crop-bottom">Bottom</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Appearance</h3>
|
||||
|
||||
<input type="radio" name="grayModel" value="ntsc" checked><label for "grayModel" class="tooltip"> NTSC grayscale formula
|
||||
@@ -140,6 +215,10 @@ Versions 1-5: 22 April 2025 - by Alex Matulich
|
||||
const rotateRightBtn = document.getElementById('rotateRight');
|
||||
const flipHorizontalBtn = document.getElementById('flipHorizontal');
|
||||
const flipVerticalBtn = document.getElementById('flipVertical');
|
||||
const cropTop = document.getElementById('cropTop');
|
||||
const cropLeft = document.getElementById('cropLeft');
|
||||
const cropRight = document.getElementById('cropRight');
|
||||
const cropBottom = document.getElementById('cropBottom');
|
||||
const blurRadiusInput = document.getElementById('blurRadius');
|
||||
const arrayName = document.getElementById('arrayName');
|
||||
const inputArea = document.getElementById('inputArea');
|
||||
@@ -149,12 +228,24 @@ Versions 1-5: 22 April 2025 - by Alex Matulich
|
||||
const originalCtx = originalCanvas.getContext('2d');
|
||||
const grayscaleCtx = grayscaleCanvas.getContext('2d');
|
||||
|
||||
const cropID = [ cropRight, cropTop, cropLeft, cropBottom ];
|
||||
let edgeID = [ 0, 1, 2 ,3 ];
|
||||
const edgeconfig = [
|
||||
// noFlip flipH flipV flipV+H
|
||||
/* 0*/ [[0,1,2,3], [2,1,0,3], [0,3,2,1], [2,3,0,1]],
|
||||
/* 90*/ [[3,0,1,2], [1,0,3,2], [3,2,1,0], [1,2,3,0]],
|
||||
/*180*/ [[2,3,0,1], [0,3,2,1], [2,1,0,3], [0,1,2,3]],
|
||||
/*270*/ [[1,2,3,0], [3,2,1,0], [1,0,3,2], [3,0,1,2]]
|
||||
];
|
||||
let grayscaleMatrix = [];
|
||||
let currentImage = new Image();
|
||||
let rotation = 0;
|
||||
let flipH = false;
|
||||
let flipV = false;
|
||||
let fileSuffix = "";
|
||||
let origDim = { width:0, height:0 };
|
||||
let uncropDim = { width:0, height:0 };
|
||||
let cropDim = { width:0, height:0 };
|
||||
|
||||
function getGrayscaleModel() {
|
||||
return document.querySelector('input[name="grayModel"]:checked').value;
|
||||
@@ -216,44 +307,44 @@ Versions 1-5: 22 April 2025 - by Alex Matulich
|
||||
function processImage() {
|
||||
if (!currentImage.src) return;
|
||||
|
||||
const origWidth = currentImage.naturalWidth;
|
||||
const origHeight = currentImage.naturalHeight;
|
||||
origDim.width = currentImage.naturalWidth;
|
||||
origDim.height = currentImage.naturalHeight;
|
||||
|
||||
// display thumbnail original image
|
||||
const thumbWidth = 200;
|
||||
const thumbHeight = Math.round((origHeight / origWidth) * thumbWidth);
|
||||
const thumbHeight = Math.round((origDim.height / origDim.width) * thumbWidth);
|
||||
originalCanvas.width = thumbWidth;
|
||||
originalCanvas.height = thumbHeight;
|
||||
originalCtx.clearRect(0, 0, thumbWidth, thumbHeight);
|
||||
originalCtx.drawImage(currentImage, 0, 0, thumbWidth, thumbHeight);
|
||||
originalSizeText.textContent = `Original size: ${origWidth}×${origHeight}`;
|
||||
originalSizeText.textContent = `Original size: ${origDim.width}×${origDim.height}`;
|
||||
|
||||
let width = origWidth;
|
||||
let height = origHeight;
|
||||
// get output image dimensions
|
||||
uncropDim.width = origDim.width;
|
||||
uncropDim.height = origDim.height;
|
||||
const newWidth = parseInt(resizeWidthInput.value);
|
||||
if (!isNaN(newWidth) && newWidth > 0) {
|
||||
const aspectRatio = height / width;
|
||||
width = newWidth;
|
||||
height = Math.round(newWidth * aspectRatio);
|
||||
uncropDim.width = newWidth;
|
||||
uncropDim.height = Math.round(newWidth * origDim.height / origDim.width);
|
||||
}
|
||||
|
||||
// put original image in a temporary canvas with output dimensions and get image data
|
||||
const tempCanvas = document.createElement('canvas');
|
||||
tempCanvas.width = width;
|
||||
tempCanvas.height = height;
|
||||
tempCanvas.width = uncropDim.width;
|
||||
tempCanvas.height = uncropDim.height;
|
||||
const tempCtx = tempCanvas.getContext('2d');
|
||||
tempCtx.drawImage(currentImage, 0, 0, width, height);
|
||||
|
||||
const imgData = tempCtx.getImageData(0, 0, width, height);
|
||||
tempCtx.drawImage(currentImage, 0, 0, uncropDim.width, uncropDim.height);
|
||||
const imgData = tempCtx.getImageData(0, 0, uncropDim.width, uncropDim.height);
|
||||
const data = imgData.data;
|
||||
|
||||
// convert image data to grayscale
|
||||
const brightnessMatrix = [];
|
||||
|
||||
const model = getGrayscaleModel();
|
||||
const weights = model === 'linear' ? [0.2126, 0.7152, 0.0722] : [0.299, 0.587, 0.114];
|
||||
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let y = 0; y < uncropDim.height; y++) {
|
||||
const row = [];
|
||||
for (let x = 0; x < width; x++) {
|
||||
const i = (y * width + x) * 4;
|
||||
for (let x = 0; x < uncropDim.width; x++) {
|
||||
const i = (y * uncropDim.width + x) * 4;
|
||||
const r = data[i];
|
||||
const g = data[i + 1];
|
||||
const b = data[i + 2];
|
||||
@@ -263,49 +354,63 @@ Versions 1-5: 22 April 2025 - by Alex Matulich
|
||||
brightnessMatrix.push(row);
|
||||
}
|
||||
|
||||
// apply blurring to the grayscale image
|
||||
const blurRadius = parseInt(blurRadiusInput.value) || 0;
|
||||
const blurredMatrix = applyGaussianBlur(brightnessMatrix, blurRadius);
|
||||
|
||||
// crop the blurred matrix, gather min and max values in crop area
|
||||
const cropMatrix = [];
|
||||
let cropx1 = parseInt(cropID[edgeID[2]].value) || 0;
|
||||
let cropx2 = parseInt(cropID[edgeID[0]].value) || 0;
|
||||
let cropy1 = parseInt(cropID[edgeID[1]].value) || 0;
|
||||
let cropy2 = parseInt(cropID[edgeID[3]].value) || 0;
|
||||
let min = 255;
|
||||
let max = 0;
|
||||
for (let y=0; y<height; y++) {
|
||||
for(let x=0; x<width; x++) {
|
||||
for (let y=cropy1; y<uncropDim.height-cropy2; y++) {
|
||||
const row = [];
|
||||
for(let x=cropx1; x<uncropDim.width-cropx2; x++) {
|
||||
row.push(blurredMatrix[y][x]);
|
||||
min = Math.min(min, blurredMatrix[y][x]);
|
||||
max = Math.max(max, blurredMatrix[y][x]);
|
||||
}
|
||||
cropMatrix.push(row);
|
||||
}
|
||||
cropDim.width = uncropDim.width - cropx1 - cropx2;
|
||||
cropDim.height = uncropDim.height - cropy1 - cropy2;
|
||||
|
||||
// normalize cropped image brightness to 0-255 range, invert brightness if checkbox is selected
|
||||
const range = max - min || 1;
|
||||
grayscaleMatrix = [];
|
||||
const grayImgData = grayscaleCtx.createImageData(width, height);
|
||||
const grayImgData = grayscaleCtx.createImageData(cropDim.width, cropDim.height);
|
||||
const grayData = grayImgData.data;
|
||||
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let y = 0; y < cropDim.height; y++) {
|
||||
const row = [];
|
||||
for (let x = 0; x < width; x++) {
|
||||
let brightness = blurredMatrix[y][x];
|
||||
for (let x = 0; x < cropDim.width; x++) {
|
||||
let brightness = cropMatrix[y][x];
|
||||
brightness = ((brightness - min) / range) * 255;
|
||||
brightness = Math.round(brightness);
|
||||
brightness = Math.max(0, Math.min(255, brightness));
|
||||
if (invertBrightnessCheckbox.checked) {
|
||||
brightness = 255 - brightness;
|
||||
}
|
||||
const i = (y * width + x) * 4;
|
||||
grayData[i] = grayData[i + 1] = grayData[i + 2] = brightness;
|
||||
const i = (y * cropDim.width + x) * 4;
|
||||
grayData[i] = grayData[i+1] = grayData[i+2] = brightness;
|
||||
grayData[i + 3] = 255;
|
||||
row.push(brightness);
|
||||
}
|
||||
grayscaleMatrix.push(row);
|
||||
}
|
||||
|
||||
// rotate and flip image
|
||||
const rotated = (rotation % 180 !== 0);
|
||||
const finalWidth = rotated ? height : width;
|
||||
const finalHeight = rotated ? width : height;
|
||||
const finalWidth = rotated ? cropDim.height : cropDim.width;
|
||||
const finalHeight = rotated ? cropDim.width : cropDim.height;
|
||||
grayscaleCanvas.width = finalWidth;
|
||||
grayscaleCanvas.height = finalHeight;
|
||||
|
||||
const tempDrawCanvas = document.createElement('canvas');
|
||||
tempDrawCanvas.width = width;
|
||||
tempDrawCanvas.height = height;
|
||||
tempDrawCanvas.width = cropDim.width;
|
||||
tempDrawCanvas.height = cropDim.height;
|
||||
const tempDrawCtx = tempDrawCanvas.getContext('2d');
|
||||
tempDrawCtx.putImageData(grayImgData, 0, 0);
|
||||
|
||||
@@ -315,7 +420,7 @@ Versions 1-5: 22 April 2025 - by Alex Matulich
|
||||
grayscaleCtx.translate(finalWidth / 2, finalHeight / 2);
|
||||
grayscaleCtx.rotate(rotation * Math.PI / 180);
|
||||
grayscaleCtx.scale(flipH ? -1 : 1, flipV ? -1 : 1);
|
||||
grayscaleCtx.drawImage(tempDrawCanvas, -width / 2, -height / 2);
|
||||
grayscaleCtx.drawImage(tempDrawCanvas, -cropDim.width / 2, -cropDim.height / 2);
|
||||
grayscaleCtx.restore();
|
||||
|
||||
grayscaleSizeText.textContent = `Output size: ${finalWidth}×${finalHeight}`;
|
||||
@@ -328,6 +433,14 @@ Versions 1-5: 22 April 2025 - by Alex Matulich
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (e) {
|
||||
currentImage.onload = function () {
|
||||
cropLeft.value="0";
|
||||
cropRight.value="0";
|
||||
cropTop.value="0";
|
||||
cropBottom.value="0";
|
||||
rotate = 0;
|
||||
flipV = flipH = false;
|
||||
resizeWidthInput.value = "100";
|
||||
blurRadiusInput.value = "0";
|
||||
processImage();
|
||||
};
|
||||
currentImage.src = e.target.result;
|
||||
@@ -353,18 +466,91 @@ Versions 1-5: 22 April 2025 - by Alex Matulich
|
||||
}
|
||||
});
|
||||
|
||||
[resizeWidthInput, invertBrightnessCheckbox, normalizeToUnitCheckbox, blurRadiusInput, ...document.querySelectorAll('input[name="grayModel"]')].forEach(el => el.addEventListener('input', processImage));
|
||||
[resizeWidthInput, invertBrightnessCheckbox, normalizeToUnitCheckbox, blurRadiusInput,
|
||||
...document.querySelectorAll('input[name="grayModel"]')].forEach(el => el.addEventListener('input', processImage));
|
||||
|
||||
rotateLeftBtn.addEventListener('click', () => { rotation = (rotation - 90 + 360) % 360; processImage(); });
|
||||
rotateRightBtn.addEventListener('click', () => { rotation = (rotation + 90) % 360; processImage(); });
|
||||
flipHorizontalBtn.addEventListener('click', () => { flipH = !flipH; processImage(); });
|
||||
flipVerticalBtn.addEventListener('click', () => { flipV = !flipV; processImage(); });
|
||||
cropLeft.addEventListener('input', () => {
|
||||
if (!currentImage.src) { cropLeft.value="0"; return; }
|
||||
const cl = parseInt(cropLeft.value) || 0;
|
||||
const cr = parseInt(cropRight.value) || 0;
|
||||
if(uncropDim.width - cl - cr < 2) cropLeft.value = (uncropDim.width - cr - 2).toString();
|
||||
processImage();
|
||||
});
|
||||
cropTop.addEventListener('input', () => {
|
||||
if (!currentImage.src) { cropTop.value="0"; return; }
|
||||
const ct = parseInt(cropTop.value) || 0;
|
||||
const cb = parseInt(cropBottom.value) || 0;
|
||||
if(uncropDim.width - ct - cb < 2) cropTop.value = (uncropDim.height - cb - 2).toString();
|
||||
processImage();
|
||||
});
|
||||
cropRight.addEventListener('input', () => {
|
||||
if (!currentImage.src) { cropRight.value="0"; return; }
|
||||
const cl = parseInt(cropLeft.value) || 0;
|
||||
const cr = parseInt(cropRight.value) || 0;
|
||||
if(uncropDim.width - cl - cr < 2) cropRight.value = (uncropDim.width - cl - 2).toString();
|
||||
processImage();
|
||||
});
|
||||
cropBottom.addEventListener('input', () => {
|
||||
if (!currentImage.src) { cropBottom.value="0"; return; }
|
||||
const ct = parseInt(cropTop.value) || 0;
|
||||
const cb = parseInt(cropBottom.value) || 0;
|
||||
if(uncropDim.width - ct - cb < 2) cropBottom.value = (uncropDim.height - ct - 2).toString();
|
||||
processImage();
|
||||
});
|
||||
|
||||
function updateEdgeID(out="") {
|
||||
const fi = (flipH ? 1 : 0) + (flipV ? 2 : 0);
|
||||
const ri = Math.round(rotation/90);
|
||||
edgeID = edgeconfig[ri][fi];
|
||||
if (out.length>0) console.log(out, rotation, flipH, flipV, edgeID);
|
||||
}
|
||||
|
||||
rotateLeftBtn.addEventListener('click', () => {
|
||||
if (!currentImage.src) return;
|
||||
rotation = (rotation - 90 + 360) % 360;
|
||||
const tmp = cropTop.value;
|
||||
cropTop.value = cropRight.value;
|
||||
cropRight.value = cropBottom.value;
|
||||
cropBottom.value = cropLeft.value;
|
||||
cropLeft.value = tmp;
|
||||
updateEdgeID();
|
||||
processImage();
|
||||
});
|
||||
rotateRightBtn.addEventListener('click', () => {
|
||||
if (!currentImage.src) return;
|
||||
rotation = (rotation + 90) % 360;
|
||||
const tmp = cropTop.value;
|
||||
cropTop.value = cropLeft.value;
|
||||
cropLeft.value = cropBottom.value;
|
||||
cropBottom.value = cropRight.value;
|
||||
cropRight.value = tmp;
|
||||
updateEdgeID();
|
||||
processImage();
|
||||
});
|
||||
flipHorizontalBtn.addEventListener('click', () => {
|
||||
if (!currentImage.src) return;
|
||||
flipH = !flipH;
|
||||
let tmp = cropRight.value;
|
||||
cropRight.value = cropLeft.value;
|
||||
cropLeft.value = tmp;
|
||||
updateEdgeID();
|
||||
processImage();
|
||||
});
|
||||
flipVerticalBtn.addEventListener('click', () => {
|
||||
if (!currentImage.src) return;
|
||||
flipV = !flipV;
|
||||
let tmp = cropTop.value;
|
||||
cropTop.value = cropBottom.value;
|
||||
cropBottom.value = tmp;
|
||||
updateEdgeID();
|
||||
processImage();
|
||||
});
|
||||
|
||||
downloadButton.addEventListener('click', () => {
|
||||
if (grayscaleMatrix.length === 0) return alert("No grayscale data to save.");
|
||||
if (grayscaleMatrix.length === 0) return alert("No data to save.");
|
||||
const useUnit = normalizeToUnitCheckbox.checked;
|
||||
const arrayContent = grayscaleMatrix.map(row => {
|
||||
return " [" + row.map(val => useUnit ? (0.001 * Math.round((val / 255) * 1000)).toString().substring(0,5) : val).join(",") + "]";
|
||||
return " [" + row.map(val => useUnit ? parseFloat(val/255).toFixed(3) : val).join(",") + "]";
|
||||
}).join(",\n");
|
||||
const openscadArray = (arrayName.value.length>0 ? arrayName.value : 'image_array')+" = [\n" + arrayContent + "\n];";
|
||||
const blob = new Blob([openscadArray], { type: "text/plain" });
|
||||
|
Reference in New Issue
Block a user