From d4ad7468f2bcd780a83133573ee3efb9d7e95818 Mon Sep 17 00:00:00 2001 From: candifloss Date: Sat, 8 Aug 2026 14:11:07 +0530 Subject: [PATCH] First commit --- .gitignore | 2 ++ README.md | 17 +++++++++-- app.js | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ index.html | 32 ++++++++++++++++++++ style.css | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 app.js create mode 100644 index.html create mode 100644 style.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..74fbad6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +dprint.json +test/ diff --git a/README.md b/README.md index 5fb8533..9da9c95 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,16 @@ -# image_overlay +# image_overlay -Tiny web app to combine a background image and a foreground image \ No newline at end of file +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 diff --git a/app.js b/app.js new file mode 100644 index 0000000..c4fd4fc --- /dev/null +++ b/app.js @@ -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(); +}; diff --git a/index.html b/index.html new file mode 100644 index 0000000..ff54d96 --- /dev/null +++ b/index.html @@ -0,0 +1,32 @@ + + + + + Wallpaper Composer + + + + + +
+ + + + + + + + + + + + + + + + +
+ + + + diff --git a/style.css b/style.css new file mode 100644 index 0000000..4313786 --- /dev/null +++ b/style.css @@ -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; +}