You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, effectStyle = "spines", bookSizeStr = "") {
// Determine the effect mode: "spines" (bookshelf effect) or "emoji" (book emoji mosaic)
const effect = ["spines", "emoji"].includes(effectStyle.toLowerCase()) ? effectStyle.toLowerCase() : "spines";
// Parse base size for books or emoji pixels
let size = parseInt(bookSizeStr, 10);
if (isNaN(size) || size <= 0) {
size = effect === "spines" ? 40 : 12;
}
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const width = originalImg.width;
const height = originalImg.height;
if (effect === "emoji") {
// Emoji Mosaic Mode
let cols = Math.floor(width / size);
let rows = Math.floor(height / size);
if (cols === 0) cols = 1;
if (rows === 0) rows = 1;
// Downscale image to calculate pixel averages
let offCanvas = document.createElement('canvas');
offCanvas.width = cols;
offCanvas.height = rows;
let octx = offCanvas.getContext('2d', { willReadFrequently: true });
octx.drawImage(originalImg, 0, 0, cols, rows);
let imgData = octx.getImageData(0, 0, cols, rows).data;
canvas.width = cols * size;
canvas.height = rows * size;
ctx.clearRect(0, 0, canvas.width, canvas.height); // keep transparent background
ctx.font = `${size}px "Segoe UI Emoji", "Apple Color Emoji", "Noto Color Emoji", sans-serif`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
// Book-themed Emojis and their approximate RGB colors
const emojis = [
{ char: '📕', rgb: [212, 50, 48] }, // Red Book
{ char: '📗', rgb: [80, 160, 90] }, // Green Book
{ char: '📘', rgb: [50, 100, 200] }, // Blue Book
{ char: '📙', rgb: [240, 140, 50] }, // Orange Book
{ char: '📓', rgb: [30, 30, 30] }, // Notebook (mapped to very dark colors)
{ char: '📓', rgb: [80, 80, 80] }, // Notebook (grey)
{ char: '📔', rgb: [220, 200, 140] }, // Decorative Cover (Tan/Beige)
{ char: '📖', rgb: [240, 240, 240] }, // Open Book (White)
{ char: '📚', rgb: [140, 90, 80] }, // Books (Brown/Mixed)
{ char: '📒', rgb: [240, 200, 50] } // Ledger (Yellow)
];
function getClosestEmoji(r, g, b) {
let minDist = Infinity;
let best = '📖';
for (let e of emojis) {
let dr = r - e.rgb[0];
let dg = g - e.rgb[1];
let db = b - e.rgb[2];
let dist = dr*dr + dg*dg + db*db;
if (dist < minDist) {
minDist = dist;
best = e.char;
}
}
return best;
}
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
let idx = (y * cols + x) * 4;
let r = imgData[idx];
let g = imgData[idx+1];
let b = imgData[idx+2];
let a = imgData[idx+3];
// Only draw if pixel is fairly opaque
if (a > 20) {
let emoji = getClosestEmoji(r, g, b);
ctx.fillText(emoji, x * size + size / 2, y * size + size / 2 + size * 0.05);
}
}
}
return canvas;
}
// Default Mode: "Spines" (Bookshelf Generator)
canvas.width = width;
canvas.height = height;
// Draw bookshelf background space (dark wood / deep shadow)
ctx.fillStyle = "#110b07";
ctx.fillRect(0, 0, width, height);
let x = 0;
// Pseudo-random generator for consistent procedural rendering
let seed = 1234.5;
function random() {
let val = Math.sin(seed++) * 10000;
return val - Math.floor(val);
}
while (x < width) {
// Vary book width slightly based on input size
let bw = Math.floor(size + (random() * 20 - 10));
if (bw < 15) bw = 15; // enforce minimum size
if (x + bw > width) {
bw = width - x; // exact boundary cap on the last book
}
// Randomize differing book heights via top and bottom masking
let topDrop = Math.floor(random() > 0.5 ? random() * (height * 0.08) : random() * (height * 0.02));
let bottomDrop = Math.floor(random() > 0.5 ? random() * (height * 0.05) : random() * (height * 0.02));
let bookY = topDrop;
let bookH = height - topDrop - bottomDrop;
// Map the original image directly onto the spines
ctx.drawImage(originalImg, x, bookY, bw, bookH, x, bookY, bw, bookH);
// Draw 3D spine gradient (makes it look arched)
let grad = ctx.createLinearGradient(x, 0, x + bw, 0);
grad.addColorStop(0, "rgba(0,0,0,0.6)");
grad.addColorStop(0.1, "rgba(0,0,0,0.1)");
grad.addColorStop(0.35, "rgba(255,255,255,0.05)");
grad.addColorStop(0.65, "rgba(255,255,255,0.15)");
grad.addColorStop(0.9, "rgba(0,0,0,0.2)");
grad.addColorStop(1, "rgba(0,0,0,0.6)");
ctx.fillStyle = grad;
ctx.fillRect(x, bookY, bw, bookH);
// Inner shadows for top and bottom book curving
let vertGrad = ctx.createLinearGradient(0, bookY, 0, bookY + bookH);
vertGrad.addColorStop(0, "rgba(0,0,0,0.9)");
vertGrad.addColorStop(0.015, "rgba(0,0,0,0.0)");
vertGrad.addColorStop(0.985, "rgba(0,0,0,0.0)");
vertGrad.addColorStop(1, "rgba(0,0,0,0.9)");
ctx.fillStyle = vertGrad;
ctx.fillRect(x, bookY, bw, bookH);
// Top and bottom highlights (pages / book headbands)
ctx.fillStyle = "rgba(255,255,255,0.4)";
ctx.fillRect(x + 2, bookY + 1, bw - 4, 1);
ctx.fillRect(x + 2, bookY + bookH - 2, bw - 4, 1);
// Add horizontal horizontal spine bands (ribbing)
let numRibs = Math.floor(random() * 3) + 3; // 3 to 5 ribs per book
let ribSpacing = bookH / (numRibs + 1);
for (let r = 1; r <= numRibs; r++) {
let ry = Math.floor(bookY + ribSpacing * r);
ctx.fillStyle = "rgba(0,0,0,0.4)";
ctx.fillRect(x, ry - 1, bw, 2);
ctx.fillStyle = "rgba(255,255,255,0.15)";
ctx.fillRect(x, ry + 1, bw, 1);
// Random gold embellishments bordering the ribs
if (random() > 0.6) {
ctx.fillStyle = "rgba(255, 215, 0, 0.35)";
ctx.fillRect(x, ry - 3, bw, 1);
ctx.fillRect(x, ry + 3, bw, 1);
}
}
// Subtle gold-embossed faux title shapes
if (random() > 0.5 && ribSpacing > 40) {
let titleBaseY = bookY + ribSpacing * 1.5;
let titleParts = Math.floor(random() * 3) + 2;
ctx.fillStyle = "rgba(255, 215, 0, 0.3)";
for (let t = 0; t < titleParts; t++) {
let tw = bw * 0.4;
let th = 5 + random() * 15;
let tx = Math.floor(x + (bw - tw) / 2);
let ty = Math.floor(titleBaseY + t * 20);
if (ty + th < bookY + bookH - 20) {
ctx.fillRect(tx, ty, tw, th);
}
}
}
// Right-edge separator gap to distinguish individual volumes
ctx.fillStyle = "rgba(0,0,0,0.7)";
ctx.fillRect(x + bw - 1, bookY, 1, bookH);
// Advance to next spine
x += bw;
}
return canvas;
}
Apply Changes