You can edit the below JavaScript code to customize the image tool.
Apply Changes
/**
* Creates a 3D perspective art effect of an image on a sidewalk,
* themed like a Walmart parking lot.
*
* @param {Image} originalImg The original javascript Image object.
* @param {number} [perspective=0.7] Controls the depth of the perspective. A value between 0 (no perspective) and 1 (maximum perspective).
* @param {number} [angle=0] Skews the image horizontally. Positive values skew the top to the left, negative to the right. Value is in pixels of horizontal offset.
* @param {string} [sidewalkColor='909090'] The hex color of the sidewalk (e.g., '909090' for gray), without the '#'.
* @param {string} [lineColor='f0e54d'] The hex color of the parking lot lines (e.g., 'f0e54d' for yellow), without the '#'.
* @returns {HTMLCanvasElement} A canvas element with the final 3D art.
*/
function processImage(originalImg, perspective = 0.7, angle = 0, sidewalkColor = '909090', lineColor = 'f0e54d') {
const CANVAS_WIDTH = 800;
const CANVAS_HEIGHT = 600;
/**
* Helper function to draw the sidewalk background with texture, lines, and cracks.
* @param {CanvasRenderingContext2D} ctx The canvas context to draw on.
* @param {number} width The width of the canvas.
* @param {number} height The height of the canvas.
* @param {string} sColor The hex string for the sidewalk color.
* @param {string} lColor The hex string for the line color.
*/
const drawSidewalk = (ctx, width, height, sColor, lColor) => {
// 1. Create a base concrete texture with noise
const imageData = ctx.createImageData(width, height);
const data = imageData.data;
const r_base = parseInt(sColor.substring(0, 2), 16);
const g_base = parseInt(sColor.substring(2, 4), 16);
const b_base = parseInt(sColor.substring(4, 6), 16);
for (let i = 0; i < data.length; i += 4) {
const noise = (Math.random() - 0.5) * 40;
data[i] = r_base + noise;
data[i + 1] = g_base + noise;
data[i + 2] = b_base + noise;
data[i + 3] = 255;
}
ctx.putImageData(imageData, 0, 0);
// 2. Draw perspectived parking lines
ctx.save();
ctx.strokeStyle = `#${lColor}`;
ctx.lineWidth = 15;
ctx.globalAlpha = 0.7;
ctx.beginPath();
ctx.moveTo(width * 0.1, height);
ctx.lineTo(width * 0.35, height * 0.4);
ctx.moveTo(width * 0.9, height);
ctx.lineTo(width * 0.65, height * 0.4);
ctx.stroke();
ctx.restore();
// 3. Draw a few cracks for realism
ctx.save();
ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)';
ctx.lineWidth = 1.5;
for (let i = 0; i < 3; i++) {
ctx.beginPath();
let currentX = Math.random() * width;
let currentY = height;
ctx.moveTo(currentX, currentY);
for (let j = 0; j < 5; j++) {
currentX += (Math.random() - 0.5) * 150;
currentY -= (Math.random() * 100 + 20);
if (currentY < height * 0.4) break;
ctx.lineTo(currentX, currentY);
}
ctx.stroke();
}
ctx.restore();
};
// --- Main canvas setup ---
const canvas = document.createElement('canvas');
canvas.width = CANVAS_WIDTH;
canvas.height = CANVAS_HEIGHT;
const ctx = canvas.getContext('2d');
// 1. Draw the sidewalk background first
drawSidewalk(ctx, CANVAS_WIDTH, CANVAS_HEIGHT, sidewalkColor, lineColor);
// --- 2. Pre-process the source image to add a chalk/paint texture ---
const texturedImgCanvas = document.createElement('canvas');
const texCtx = texturedImgCanvas.getContext('2d', { willReadFrequently: true });
const imgWidth = originalImg.width;
const imgHeight = originalImg.height;
texturedImgCanvas.width = imgWidth;
texturedImgCanvas.height = imgHeight;
// Draw the image onto the temporary canvas
texCtx.drawImage(originalImg, 0, 0);
// Apply a subtle noise overlay to make it look less perfect like worn paint
const noiseData = texCtx.getImageData(0, 0, imgWidth, imgHeight);
const noisePixels = noiseData.data;
for (let i = 0; i < noisePixels.length; i += 4) {
// Only apply noise to non-transparent pixels
if (noisePixels[i + 3] > 0) {
const noise = (Math.random() - 0.5) * 50;
noisePixels[i] = Math.max(0, Math.min(255, noisePixels[i] + noise));
noisePixels[i + 1] = Math.max(0, Math.min(255, noisePixels[i + 1] + noise));
noisePixels[i + 2] = Math.max(0, Math.min(255, noisePixels[i + 2] + noise));
}
}
texCtx.putImageData(noiseData, 0, 0);
// --- 3. Draw the textured image with perspective onto the main canvas ---
// Define the trapezoid for the perspective transformation
const topY = CANVAS_HEIGHT * 0.4;
const bottomY = CANVAS_HEIGHT * 0.95;
const bottomWidth = imgWidth * 1.5; // Make the base appear wider and closer
const safePerspective = Math.max(0.01, Math.min(0.9, perspective));
const topWidth = bottomWidth * (1 - safePerspective);
// Set alpha for a semi-transparent "painted on" look
ctx.globalAlpha = 0.9;
// Iterate through each horizontal scanline of the source image
for (let y = 0; y < imgHeight; y++) {
// Calculate the ratio of the current line's position (0 at top, 1 at bottom)
const ratio = y / (imgHeight - 1);
// Interpolate the width and y-position on the destination canvas
const destY = topY + (bottomY - topY) * ratio;
const destWidth = topWidth + (bottomWidth - topWidth) * ratio;
let destX = (CANVAS_WIDTH - destWidth) / 2;
// Apply horizontal skew based on the 'angle' parameter
const skewAmount = (ratio - 0.5) * -angle;
destX += skewAmount;
// Overlap strips slightly to avoid anti-aliasing gaps
const destHeight = ((bottomY - topY) / imgHeight) * 1.5;
ctx.drawImage(
texturedImgCanvas,
0, y, imgWidth, 1, // Source: 1-pixel high slice
destX, destY, destWidth, destHeight // Destination: stretched and positioned slice
);
}
// Reset global alpha
ctx.globalAlpha = 1.0;
return canvas;
}
Apply Changes