First commit
This commit is contained in:
parent
1e8725b15c
commit
d4ad7468f2
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
dprint.json
|
||||||
|
test/
|
||||||
17
README.md
17
README.md
@ -1,3 +1,16 @@
|
|||||||
# image_overlay
|
# image_overlay
|
||||||
|
|
||||||
Tiny web app to combine a background image and a foreground image
|
Tiny web app to combine a background image and a foreground image.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Open [index.html](index.html) in your browser.
|
||||||
|
2. Upload a background image and a foreground image.
|
||||||
|
3. Set the relative size, X offset, and Y offset of the foreground image.
|
||||||
|
4. Click **Download**.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
* A browser
|
||||||
|
* Two images
|
||||||
|
* Nothing else
|
||||||
|
|||||||
85
app.js
Normal file
85
app.js
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
// Minimal canvas compositor
|
||||||
|
|
||||||
|
const canvas = document.getElementById("canvas");
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
|
||||||
|
let bgImg = null;
|
||||||
|
let fgImg = null;
|
||||||
|
|
||||||
|
// --- image loader ---
|
||||||
|
function loadImage(file, callback) {
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => callback(img);
|
||||||
|
img.src = URL.createObjectURL(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- inputs ---
|
||||||
|
const bgInput = document.getElementById("bgInput");
|
||||||
|
const fgInput = document.getElementById("fgInput");
|
||||||
|
|
||||||
|
const scaleInput = document.getElementById("scale");
|
||||||
|
const offsetXInput = document.getElementById("offsetX");
|
||||||
|
const offsetYInput = document.getElementById("offsetY");
|
||||||
|
|
||||||
|
// --- event bindings ---
|
||||||
|
|
||||||
|
bgInput.onchange = (e) => {
|
||||||
|
loadImage(e.target.files[0], (img) => {
|
||||||
|
bgImg = img;
|
||||||
|
|
||||||
|
// canvas follows background resolution exactly
|
||||||
|
canvas.width = img.width;
|
||||||
|
canvas.height = img.height;
|
||||||
|
|
||||||
|
draw();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
fgInput.onchange = (e) => {
|
||||||
|
loadImage(e.target.files[0], (img) => {
|
||||||
|
fgImg = img;
|
||||||
|
draw();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
[scaleInput, offsetXInput, offsetYInput].forEach(el => {
|
||||||
|
el.oninput = draw;
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- draw pipeline ---
|
||||||
|
function draw() {
|
||||||
|
if (!bgImg) return;
|
||||||
|
|
||||||
|
// clear + draw background
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
ctx.drawImage(bgImg, 0, 0);
|
||||||
|
|
||||||
|
if (!fgImg) return;
|
||||||
|
|
||||||
|
const scale = parseFloat(scaleInput.value) / 100 || 0.2;
|
||||||
|
const offsetX = parseFloat(offsetXInput.value) || 0;
|
||||||
|
const offsetY = parseFloat(offsetYInput.value) || 0;
|
||||||
|
|
||||||
|
// scale based on background width
|
||||||
|
const targetWidth = bgImg.width * scale;
|
||||||
|
const aspect = fgImg.height / fgImg.width;
|
||||||
|
const targetHeight = targetWidth * aspect;
|
||||||
|
|
||||||
|
// centered + offsets
|
||||||
|
const x = (bgImg.width - targetWidth) / 2 + offsetX;
|
||||||
|
const y = (bgImg.height - targetHeight) / 2 + offsetY;
|
||||||
|
|
||||||
|
ctx.drawImage(fgImg, x, y, targetWidth, targetHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- download ---
|
||||||
|
document.getElementById("downloadBtn").onclick = () => {
|
||||||
|
if (!bgImg) return;
|
||||||
|
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.download = "wallpaper.png";
|
||||||
|
link.href = canvas.toDataURL("image/png");
|
||||||
|
link.click();
|
||||||
|
};
|
||||||
32
index.html
Normal file
32
index.html
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Wallpaper Composer</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<canvas id="canvas"></canvas>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<label>Background Image</label>
|
||||||
|
<input type="file" id="bgInput" accept="image/*">
|
||||||
|
|
||||||
|
<label>Foreground Image</label>
|
||||||
|
<input type="file" id="fgInput" accept="image/*">
|
||||||
|
|
||||||
|
<label>Scale (%)</label>
|
||||||
|
<input type="number" id="scale" value="20">
|
||||||
|
|
||||||
|
<label>Offset X (px)</label>
|
||||||
|
<input type="number" id="offsetX" value="0">
|
||||||
|
|
||||||
|
<label>Offset Y (px)</label>
|
||||||
|
<input type="number" id="offsetY" value="0">
|
||||||
|
|
||||||
|
<button id="downloadBtn">Download</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
81
style.css
Normal file
81
style.css
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
/* reset */
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: sans-serif;
|
||||||
|
overflow: hidden; /* no scroll */
|
||||||
|
background: #303030;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* canvas = full screen */
|
||||||
|
canvas {
|
||||||
|
position: fixed;
|
||||||
|
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
|
||||||
|
object-fit: contain; /* keep aspect ratio */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* bottom control bar */
|
||||||
|
.controls {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
padding: 10px;
|
||||||
|
align-items: center; /* vertical centering */
|
||||||
|
|
||||||
|
background: rgba(209, 209, 209, 0.07);
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls label {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.8;
|
||||||
|
color: #ccc;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center; /* vertical center inside its own box */
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls input,
|
||||||
|
.controls button {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px;
|
||||||
|
background: #70707023;
|
||||||
|
color: #eee;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls input::-webkit-outer-spin-button,
|
||||||
|
.controls input::-webkit-inner-spin-button {
|
||||||
|
/* display: none; <- Crashes Chrome on hover */
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls input[type="number"] {
|
||||||
|
-moz-appearance: textfield; /* Firefox */
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls input:hover,
|
||||||
|
.controls button:hover {
|
||||||
|
background: #99999934;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls > * {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user