Skip to content

Commit c92893c

Browse files
authored
Merge pull request #160 from dt10812/patch-1
Create image-enhancer.html
2 parents e0c6d49 + ddff661 commit c92893c

File tree

1 file changed

+397
-0
lines changed

1 file changed

+397
-0
lines changed

entries/image-enhancer.html

Lines changed: 397 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,397 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Image Enhancer</title>
7+
<style>
8+
body {
9+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
10+
display: flex;
11+
flex-direction: column;
12+
align-items: center;
13+
background-color: #1a1a2e; /* Dark background */
14+
color: #e0e0e0;
15+
margin: 0;
16+
padding: 20px;
17+
box-sizing: border-box;
18+
min-height: 100vh;
19+
}
20+
21+
h1 {
22+
color: #8be9fd;
23+
margin-bottom: 20px;
24+
text-shadow: 0 0 10px rgba(139, 233, 253, 0.3);
25+
}
26+
27+
.container {
28+
background-color: #2a2a4a;
29+
padding: 30px;
30+
border-radius: 12px;
31+
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
32+
display: flex;
33+
flex-direction: column;
34+
align-items: center;
35+
max-width: 900px;
36+
width: 100%;
37+
margin-bottom: 20px;
38+
}
39+
40+
.controls {
41+
display: grid;
42+
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
43+
gap: 15px;
44+
margin-bottom: 25px;
45+
width: 100%;
46+
max-width: 700px;
47+
}
48+
49+
.control-group {
50+
display: flex;
51+
flex-direction: column;
52+
align-items: center;
53+
}
54+
55+
label {
56+
margin-bottom: 5px;
57+
color: #a0a0a0;
58+
font-size: 0.9em;
59+
}
60+
61+
input[type="range"] {
62+
width: 100%;
63+
-webkit-appearance: none;
64+
height: 8px;
65+
background: #444;
66+
border-radius: 5px;
67+
outline: none;
68+
transition: opacity .2s;
69+
}
70+
71+
input[type="range"]::-webkit-slider-thumb {
72+
-webkit-appearance: none;
73+
appearance: none;
74+
width: 20px;
75+
height: 20px;
76+
border-radius: 50%;
77+
background: #8be9fd;
78+
cursor: pointer;
79+
box-shadow: 0 0 5px rgba(139, 233, 253, 0.5);
80+
}
81+
82+
input[type="range"]::-moz-range-thumb {
83+
width: 20px;
84+
height: 20px;
85+
border-radius: 50%;
86+
background: #8be9fd;
87+
cursor: pointer;
88+
box-shadow: 0 0 5px rgba(139, 233, 253, 0.5);
89+
}
90+
91+
button {
92+
background-color: #6272a4;
93+
color: white;
94+
border: none;
95+
padding: 10px 20px;
96+
border-radius: 5px;
97+
cursor: pointer;
98+
font-size: 1em;
99+
transition: background-color 0.3s ease, transform 0.2s ease;
100+
margin: 5px;
101+
}
102+
103+
button:hover {
104+
background-color: #44475a;
105+
transform: translateY(-2px);
106+
}
107+
108+
button:active {
109+
transform: translateY(0);
110+
}
111+
112+
input[type="file"] {
113+
display: none; /* Hide default file input */
114+
}
115+
116+
.upload-button {
117+
background-color: #50fa7b;
118+
color: #282a36;
119+
padding: 10px 20px;
120+
border-radius: 5px;
121+
cursor: pointer;
122+
font-size: 1em;
123+
transition: background-color 0.3s ease, transform 0.2s ease;
124+
margin-bottom: 20px;
125+
}
126+
127+
.upload-button:hover {
128+
background-color: #3bfa65;
129+
transform: translateY(-2px);
130+
}
131+
132+
canvas {
133+
border: 2px dashed #44475a;
134+
max-width: 100%; /* Ensure canvas scales down if too big */
135+
height: auto; /* Maintain aspect ratio */
136+
display: block;
137+
margin-top: 20px;
138+
box-shadow: 0 0 15px rgba(0, 0, 0, 0.3);
139+
background-color: #333; /* Placeholder background for canvas */
140+
image-rendering: pixelated; /* For sharper pixel scaling in browser preview */
141+
image-rendering: -moz-crisp-edges;
142+
image-rendering: crisp-edges;
143+
}
144+
145+
.download-button {
146+
background-color: #ff79c6;
147+
margin-top: 25px;
148+
padding: 12px 25px;
149+
}
150+
151+
.download-button:hover {
152+
background-color: #ff52b2;
153+
}
154+
155+
.message {
156+
margin-top: 15px;
157+
color: #ffb86c;
158+
font-size: 0.9em;
159+
text-align: center;
160+
}
161+
162+
@media (max-width: 600px) {
163+
.controls {
164+
grid-template-columns: 1fr;
165+
}
166+
.container {
167+
padding: 20px;
168+
}
169+
}
170+
</style>
171+
</head>
172+
<body>
173+
174+
<h1>✨ Pure JS Image Enhancer with 2x Scaling ✨</h1>
175+
176+
<div class="container">
177+
<input type="file" id="imageUpload" accept="image/*">
178+
<label for="imageUpload" class="upload-button">Upload Image</label>
179+
180+
<div class="controls">
181+
<div class="control-group">
182+
<label for="grayscale">Grayscale</label>
183+
<input type="range" id="grayscale" min="0" max="100" value="0">
184+
</div>
185+
<div class="control-group">
186+
<label for="sepia">Sepia</label>
187+
<input type="range" id="sepia" min="0" max="100" value="0">
188+
</div>
189+
<div class="control-group">
190+
<label for="brightness">Brightness</label>
191+
<input type="range" id="brightness" min="0" max="200" value="100">
192+
</div>
193+
<div class="control-group">
194+
<label for="contrast">Contrast</label>
195+
<input type="range" id="contrast" min="0" max="200" value="100">
196+
</div>
197+
</div>
198+
199+
<div>
200+
<button id="applyFilters">Apply Filters & Scale 2x</button>
201+
<button id="resetFilters">Reset</button>
202+
</div>
203+
204+
<canvas id="imageCanvas"></canvas>
205+
<button id="downloadImage" class="download-button" style="display:none;">Download Enhanced Image</button>
206+
<p class="message" id="statusMessage"></p>
207+
</div>
208+
209+
<script>
210+
const imageUpload = document.getElementById('imageUpload');
211+
const imageCanvas = document.getElementById('imageCanvas');
212+
const ctx = imageCanvas.getContext('2d');
213+
const grayscaleSlider = document.getElementById('grayscale');
214+
const sepiaSlider = document.getElementById('sepia');
215+
const brightnessSlider = document.getElementById('brightness');
216+
const contrastSlider = document.getElementById('contrast');
217+
const applyFiltersBtn = document.getElementById('applyFilters');
218+
const resetFiltersBtn = document.getElementById('resetFilters');
219+
const downloadImageBtn = document.getElementById('downloadImage');
220+
const statusMessage = document.getElementById('statusMessage');
221+
222+
let originalImage = new Image();
223+
let originalImageData = null; // Store the original image data (1x resolution)
224+
225+
// Set initial canvas dimensions (will be resized to scaled image)
226+
imageCanvas.width = 600;
227+
imageCanvas.height = 400;
228+
229+
// --- Event Listeners ---
230+
231+
imageUpload.addEventListener('change', (e) => {
232+
const file = e.target.files[0];
233+
if (file) {
234+
const reader = new FileReader();
235+
reader.onload = (event) => {
236+
originalImage.onload = () => {
237+
// Temporarily draw original image to get its pixel data
238+
const tempCanvas = document.createElement('canvas');
239+
const tempCtx = tempCanvas.getContext('2d');
240+
tempCanvas.width = originalImage.width;
241+
tempCanvas.height = originalImage.height;
242+
tempCtx.drawImage(originalImage, 0, 0);
243+
244+
// Store the original pixel data at 1x resolution
245+
originalImageData = tempCtx.getImageData(0, 0, originalImage.width, originalImage.height);
246+
247+
// Reset sliders and apply immediately with no filters (just scaling)
248+
resetSliders();
249+
applyAllFiltersAndScale();
250+
downloadImageBtn.style.display = 'block';
251+
statusMessage.textContent = 'Image loaded and scaled 2x. Adjust sliders and click "Apply Filters & Scale 2x".';
252+
};
253+
originalImage.src = event.target.result;
254+
};
255+
reader.readAsDataURL(file);
256+
} else {
257+
statusMessage.textContent = 'Please select an image file.';
258+
}
259+
});
260+
261+
applyFiltersBtn.addEventListener('click', () => {
262+
if (originalImageData) {
263+
applyAllFiltersAndScale();
264+
statusMessage.textContent = 'Filters applied and image scaled 2x!';
265+
} else {
266+
statusMessage.textContent = 'Please upload an image first.';
267+
}
268+
});
269+
270+
resetFiltersBtn.addEventListener('click', () => {
271+
if (originalImageData) {
272+
resetSliders();
273+
applyAllFiltersAndScale(); // Re-apply without filters, just 2x scaling
274+
statusMessage.textContent = 'Filters reset to original, image remains 2x scaled.';
275+
} else {
276+
statusMessage.textContent = 'No image or filters to reset.';
277+
}
278+
});
279+
280+
downloadImageBtn.addEventListener('click', () => {
281+
if (originalImageData) {
282+
const dataURL = imageCanvas.toDataURL('image/png'); // Can be 'image/jpeg'
283+
const a = document.createElement('a');
284+
a.href = dataURL;
285+
a.download = `enhanced_${originalImageData.width * 2}x${originalImageData.height * 2}.png`;
286+
document.body.appendChild(a);
287+
a.click();
288+
document.body.removeChild(a);
289+
statusMessage.textContent = 'Image downloaded!';
290+
} else {
291+
statusMessage.textContent = 'Nothing to download. Please upload and enhance an image.';
292+
}
293+
});
294+
295+
// --- Filter and Scaling Logic ---
296+
297+
function applyAllFiltersAndScale() {
298+
if (!originalImageData) return;
299+
300+
const originalWidth = originalImageData.width;
301+
const originalHeight = originalImageData.height;
302+
const scaledWidth = originalWidth * 2;
303+
const scaledHeight = originalHeight * 2;
304+
305+
// Prepare a new ImageData object for the scaled and filtered image
306+
const scaledImageData = ctx.createImageData(scaledWidth, scaledHeight);
307+
const originalPixels = originalImageData.data;
308+
const scaledPixels = scaledImageData.data;
309+
310+
const grayscaleVal = parseInt(grayscaleSlider.value);
311+
const sepiaVal = parseInt(sepiaSlider.value);
312+
const brightnessVal = parseInt(brightnessSlider.value);
313+
const contrastVal = parseInt(contrastSlider.value);
314+
315+
// Loop through the NEW, SCALED image dimensions
316+
for (let y = 0; y < scaledHeight; y++) {
317+
for (let x = 0; x < scaledWidth; x++) {
318+
// Calculate corresponding pixel in the ORIGINAL image (Nearest Neighbor)
319+
// (x / 2) and (y / 2) because we are taking each pixel from the original and
320+
// applying it to a 2x2 block in the scaled image.
321+
const originalX = Math.floor(x / 2);
322+
const originalY = Math.floor(y / 2);
323+
324+
// Get the index of the original pixel
325+
const originalIndex = (originalY * originalWidth + originalX) * 4;
326+
327+
let r = originalPixels[originalIndex];
328+
let g = originalPixels[originalIndex + 1];
329+
let b = originalPixels[originalIndex + 2];
330+
let a = originalPixels[originalIndex + 3]; // Alpha channel
331+
332+
// --- Apply Filters to the pixel from original image ---
333+
334+
// Grayscale
335+
if (grayscaleVal > 0) {
336+
let avg = (r + g + b) / 3;
337+
r = r + (avg - r) * (grayscaleVal / 100);
338+
g = g + (avg - g) * (grayscaleVal / 100);
339+
b = b + (avg - b) * (grayscaleVal / 100);
340+
}
341+
342+
// Sepia
343+
if (sepiaVal > 0) {
344+
const tr = (r * 0.393 + g * 0.769 + b * 0.189);
345+
const tg = (r * 0.349 + g * 0.686 + b * 0.168);
346+
const tb = (r * 0.272 + g * 0.534 + b * 0.131);
347+
348+
r = r + (tr - r) * (sepiaVal / 100);
349+
g = g + (tg - g) * (sepiaVal / 100);
350+
b = b + (tb - b) * (sepiaVal / 100);
351+
}
352+
353+
// Brightness
354+
let brightnessAdjust = (brightnessVal - 100);
355+
r += brightnessAdjust;
356+
g += brightnessAdjust;
357+
b += brightnessAdjust;
358+
359+
// Contrast
360+
let contrastAdjust = (contrastVal / 100);
361+
r = ((r - 128) * contrastAdjust) + 128;
362+
g = ((g - 128) * contrastAdjust) + 128;
363+
b = ((b - 128) * contrastAdjust) + 128;
364+
365+
// Clamp values to 0-255
366+
r = Math.max(0, Math.min(255, r));
367+
g = Math.max(0, Math.min(255, g));
368+
b = Math.max(0, Math.min(255, b));
369+
370+
371+
// --- Set the pixel in the SCALED image data ---
372+
const scaledIndex = (y * scaledWidth + x) * 4;
373+
scaledPixels[scaledIndex] = r;
374+
scaledPixels[scaledIndex + 1] = g;
375+
scaledPixels[scaledIndex + 2] = b;
376+
scaledPixels[scaledIndex + 3] = a; // Keep original alpha
377+
}
378+
}
379+
380+
// Update canvas dimensions and draw the new scaled and filtered image
381+
imageCanvas.width = scaledWidth;
382+
imageCanvas.height = scaledHeight;
383+
ctx.putImageData(scaledImageData, 0, 0);
384+
}
385+
386+
function resetSliders() {
387+
grayscaleSlider.value = 0;
388+
sepiaSlider.value = 0;
389+
brightnessSlider.value = 100;
390+
contrastSlider.value = 100;
391+
}
392+
393+
// Initial message
394+
statusMessage.textContent = 'Upload an image to get started!';
395+
</script>
396+
</body>
397+
</html>

0 commit comments

Comments
 (0)