You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, carvingDepthStr = "15", stoneColorHex = "#8f979c", roughnessStr = "45", contrastStr = "1.5", blurStr = "0") {
const carvingDepth = parseFloat(carvingDepthStr);
const roughness = parseFloat(roughnessStr);
const contrast = parseFloat(contrastStr);
let blurAmt = parseFloat(blurStr);
const w = originalImg.width;
const h = originalImg.height;
// Helper to safely extract RGB from Hex snippet
function hexToRgb(hex) {
let r=143, g=151, b=156;
if(/^#([A-Fa-f0-9]{3}){1,2}$/.test(hex)){
hex = hex.substring(1);
if(hex.length === 3) hex = hex.split('').map(x => x + x).join('');
r = parseInt(hex.substring(0, 2), 16);
g = parseInt(hex.substring(2, 4), 16);
b = parseInt(hex.substring(4, 6), 16);
}
return [r, g, b];
}
const [bR, bG, bB] = hexToRgb(stoneColorHex);
// Auto-calculate appropriate blur based on image dimensions for a smooth carving slope if not set
if (blurAmt === 0) {
blurAmt = Math.max(2, Math.min(w, h) * 0.006);
}
// Step 1: Blur initial image to generate a bump map (Z map)
const blurCanvas = document.createElement('canvas');
blurCanvas.width = w;
blurCanvas.height = h;
const bCtx = blurCanvas.getContext('2d', { willReadFrequently: true });
// Draw with slight Gaussian blur to enable steep smooth slopes instead of jagged edges
bCtx.filter = `blur(${blurAmt}px)`;
bCtx.drawImage(originalImg, 0, 0, w, h);
let bData;
try {
bData = bCtx.getImageData(0, 0, w, h);
} catch(e) {
// Fallback for CORS tainted images in strict environments, removes the filter so they can potentially fail natively or pass if policy changes
bCtx.filter = 'none';
bCtx.drawImage(originalImg, 0, 0, w, h);
bData = bCtx.getImageData(0, 0, w, h);
}
// Step 2: Build the 3D surface map array (Z-coordinates)
const Z = new Float32Array(w * h);
const margin = Math.floor(Math.min(w, h) * 0.04); // the size of the outer tablet bevel
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const i = y * w + x;
const r = bData.data[i*4];
const g = bData.data[i*4+1];
const b = bData.data[i*4+2];
const a = bData.data[i*4+3] / 255;
// Calculate lightness and map it to a displacement elevation map (-100 to +100 roughly)
const lum = (r * 0.299 + g * 0.587 + b * 0.114);
const elevate = (lum - 128) * a * 0.7; // Bright carves outwards, Dark carves inwards
// Generate the outer 3D stone block bevel (dips down at canvas edges)
const distToEdge = Math.min(x, w - 1 - x, y, h - 1 - y);
let blockZ = 120; // flat rock surface baseline height
if (distToEdge < margin) {
const t = distToEdge / margin; // smoothstep beveling
const smoothT = t * t * (3 - 2 * t);
blockZ = smoothT * 120;
}
// Final Combined Height
Z[i] = blockZ + elevate;
}
}
// Step 3: Generate detailed multi-octave Stone Perlin-like Noise structure
function generateNoise(w, h, scale) {
const noise = new Float32Array(w * h);
const sw = Math.ceil(w / scale) + 1;
const sh = Math.ceil(h / scale) + 1;
const smallNoise = new Float32Array(sw * sh);
for(let i = 0; i < sw * sh; i++) smallNoise[i] = Math.random();
for(let y = 0; y < h; y++) {
const sy = y / scale;
const y0 = Math.floor(sy);
const ty = sy - y0;
const fy = ty * ty * (3 - 2 * ty);
const row0 = y0 * sw;
const row1 = (y0 + 1) * sw;
for(let x = 0; x < w; x++) {
const sx = x / scale;
const x0 = Math.floor(sx);
const tx = sx - x0;
const fx = tx * tx * (3 - 2 * tx);
const nx0 = smallNoise[row0 + x0] + (smallNoise[row0 + x0 + 1] - smallNoise[row0 + x0]) * fx;
const nx1 = smallNoise[row1 + x0] + (smallNoise[row1 + x0 + 1] - smallNoise[row1 + x0]) * fx;
noise[y*w + x] = nx0 + (nx1 - nx0) * fy;
}
}
return noise;
}
const dim = Math.min(w, h);
const noise32 = generateNoise(w, h, Math.max(16, dim * 0.1));
const noise16 = generateNoise(w, h, Math.max(8, dim * 0.05));
const noise8 = generateNoise(w, h, Math.max(4, dim * 0.025));
const noise4 = generateNoise(w, h, Math.max(2, dim * 0.0125));
const stoneTex = new Float32Array(w * h);
for(let i = 0; i < w * h; i++) {
stoneTex[i] = noise32[i] * 0.4 + noise16[i] * 0.3 + noise8[i] * 0.2 + noise4[i] * 0.1;
}
// Step 4: Shading, Bump Mapping & Final Composing Phase
const finalCanvas = document.createElement('canvas');
finalCanvas.width = w;
finalCanvas.height = h;
const fCtx = finalCanvas.getContext('2d');
const fData = fCtx.createImageData(w, h);
// Virtual Light pointing diagonally Top-Left
let lx = -1.0, ly = -1.5, lz = 1.2;
const llen = Math.sqrt(lx*lx + ly*ly + lz*lz);
lx /= llen; ly /= llen; lz /= llen;
const flatDiffuseBase = lz;
for(let y = 0; y < h; y++) {
for(let x = 0; x < w; x++) {
const i = y * w + x;
// To compute surface normals properly (Central Difference)
const xPrev = x > 0 ? x - 1 : x;
const xNext = x < w - 1 ? x + 1 : x;
const yPrev = y > 0 ? y - 1 : y;
const yNext = y < h - 1 ? y + 1 : y;
// Height-map differentials
let dx = (Z[y * w + xNext] - Z[y * w + xPrev]) / 2.0 * carvingDepth * 0.1;
let dy = (Z[yNext * w + x] - Z[yPrev * w + x]) / 2.0 * carvingDepth * 0.1;
// Micro-roughness differentials
const sdx = (stoneTex[y * w + xNext] - stoneTex[y * w + xPrev]) / 2.0;
const sdy = (stoneTex[yNext * w + x] - stoneTex[yPrev * w + x]) / 2.0;
// Blend image normals with natural stone bumping bumps
dx += sdx * roughness * 0.3;
dy += sdy * roughness * 0.3;
// Normalize vector direction
const nlen = Math.sqrt(dx*dx + dy*dy + 1.0);
const nx = -dx / nlen;
const ny = -dy / nlen;
const nz = 1.0 / nlen;
// Lambertian Illumination with baseline adjustments
const diffuse = Math.max(0, nx*lx + ny*ly + nz*lz);
let shadowIntensity = diffuse / flatDiffuseBase;
// Vignette calculation to fake 3D depth towards corners
const vDistX = Math.abs(x - w / 2) / (w / 2);
const vDistY = Math.abs(y - h / 2) / (h / 2);
const vignette = 1.0 - (vDistX * vDistX + vDistY * vDistY) * 0.12;
// Contrast modifier
shadowIntensity = Math.pow(shadowIntensity, contrast) * vignette;
// Compute Final Tint and Color
const texOffset = (stoneTex[i] - 0.5) * roughness;
fData.data[i*4] = Math.min(255, Math.max(0, (bR + texOffset) * shadowIntensity));
fData.data[i*4 + 1] = Math.min(255, Math.max(0, (bG + texOffset) * shadowIntensity));
fData.data[i*4 + 2] = Math.min(255, Math.max(0, (bB + texOffset) * shadowIntensity));
fData.data[i*4 + 3] = 255; // Rendered output is an entirely solid stone slab block
}
}
fCtx.putImageData(fData, 0, 0);
return finalCanvas;
}
Apply Changes