You can edit the below JavaScript code to customize the image tool.
async function processImage(originalImg, frameColor = '#503020', frameThickness = 20, numVerticalPanes = 3, archHeightRatio = 0.35) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const w = originalImg.naturalWidth || originalImg.width;
const h = originalImg.naturalHeight || originalImg.height;
canvas.width = w;
canvas.height = h;
// Parameter validation and clamping
let clampedFrameThickness = Math.max(1, frameThickness);
// Ensure frameThickness doesn't make the image area too small or negative
clampedFrameThickness = Math.min(clampedFrameThickness, w / 2.5, h / 2.5);
let clampedNumVerticalPanes = Math.max(1, Math.floor(numVerticalPanes));
let clampedArchHeightRatio = Math.max(0.05, Math.min(archHeightRatio, 0.95)); // Arch height ratio between 5% and 95% of total height
// archStartY is the y-coordinate (from top, y=0) where the vertical sides of the outer window frame end and the arch begins.
// This value also represents the height of the arched portion of the window from its base up to the window's highest point (y=0).
let archStartY = h * clampedArchHeightRatio;
// Ensure archStartY is valid with respect to frameThickness
// Arch's visible peak (y=0) should be meaningfully above the inner frame's top edge (y=clampedFrameThickness)
archStartY = Math.max(clampedFrameThickness + 1, archStartY);
// Arch base must be above the bottom frame edge.
archStartY = Math.min(archStartY, h - clampedFrameThickness - 1);
// 1. Draw the original image, clipped to the outer shape of the window.
ctx.save();
ctx.beginPath();
ctx.moveTo(0, h); // Bottom-left
ctx.lineTo(0, archStartY); // Up to where arch starts on left (y-coordinate from top)
ctx.lineTo(w / 2, 0); // To the peak of the arch (y=0 for the tip of the window)
ctx.lineTo(w, archStartY); // Down to where arch starts on right
ctx.lineTo(w, h); // Bottom-right
ctx.closePath();
ctx.clip(); // Apply clipping path
ctx.drawImage(originalImg, 0, 0, w, h);
ctx.restore(); // Remove clipping path
// 2. Calculate inner frame geometry
// y_inner_arch_base: Y-coordinate where the inner arch's curved part meets its vertical side.
// The peak of the inner arch (top of glass area) is at y = clampedFrameThickness.
// This formula is derived by ensuring the slope of the inner arch segment is parallel to the outer arch segment.
let y_inner_arch_base = archStartY + clampedFrameThickness * (1 - (2 * archStartY) / w);
// Clamp y_inner_arch_base to be between the inner arch peak and the bottom of the glass area.
y_inner_arch_base = Math.max(clampedFrameThickness, y_inner_arch_base); // Cannot be above inner peak
y_inner_arch_base = Math.min(y_inner_arch_base, h - clampedFrameThickness); // Cannot be below bottom of glass
// 3. Draw the main stone frame (outer border)
ctx.fillStyle = frameColor;
ctx.beginPath();
// Define outer path (clockwise) - this is identical to the clipping path
ctx.moveTo(0, h); ctx.lineTo(0, archStartY); ctx.lineTo(w / 2, 0); ctx.lineTo(w, archStartY); ctx.lineTo(w, h); ctx.closePath();
// Define inner path (counter-clockwise for creating a hole)
// This path traces the boundary of the glass area.
// Start from bottom-right of glass area, go counter-clockwise.
ctx.moveTo(w - clampedFrameThickness, h - clampedFrameThickness); // Bottom-right of glass
ctx.lineTo(clampedFrameThickness, h - clampedFrameThickness); // Bottom-left of glass
ctx.lineTo(clampedFrameThickness, y_inner_arch_base); // Up to inner arch base left
ctx.lineTo(w / 2, clampedFrameThickness); // To inner peak (y=clampedFrameThickness)
ctx.lineTo(w - clampedFrameThickness, y_inner_arch_base); // To inner arch base right
ctx.lineTo(w - clampedFrameThickness, h - clampedFrameThickness); // Back to bottom-right of glass
ctx.closePath(); // Close the inner path
// Use 'evenodd' fill rule: fills area between outer and inner paths.
ctx.fill('evenodd');
// 4. Helper function to determine the Y-coordinate on the inner arch boundary for a given X
function getInnerArchY(x_val) {
const inner_peak_y = clampedFrameThickness; // Y-coord of the glass arch peak
const glass_area_left_x = clampedFrameThickness; // X-coord of the left vertical side of glass area
const glass_area_right_x = w - clampedFrameThickness; // X-coord of the right vertical side of glass area
const window_center_x = w / 2;
// Clamp x_val to be within the glass area to prevent errors if called with outside values
const current_x = Math.max(glass_area_left_x, Math.min(x_val, glass_area_right_x));
if (Math.abs(current_x - window_center_x) < 1e-6) return inner_peak_y; // At the center peak
if (current_x < window_center_x) { // Left half of inner arch
// Points defining the left slope of the inner arch:
// P1: (glass_area_left_x, y_inner_arch_base)
// P2: (window_center_x, inner_peak_y)
const P1_x = glass_area_left_x;
const P1_y = y_inner_arch_base;
const P2_x = window_center_x;
const P2_y = inner_peak_y;
// Denominator (P2_x - P1_x) is w/2 - clampedFrameThickness.
// Clamping of clampedFrameThickness ensures this is positive and non-zero.
const slope = (P2_y - P1_y) / (P2_x - P1_x);
return slope * (current_x - P1_x) + P1_y;
} else { // Right half of inner arch
// Points defining the right slope:
// P1: (window_center_x, inner_peak_y)
// P2: (glass_area_right_x, y_inner_arch_base)
const P1_x = window_center_x;
const P1_y = inner_peak_y;
const P2_x = glass_area_right_x;
const P2_y = y_inner_arch_base;
const slope = (P2_y - P1_y) / (P2_x - P1_x);
return slope * (current_x - P1_x) + P1_y;
}
}
// 5. Draw Mullions (vertical dividers)
// Total width available within the outer frame for glass panes and internal mullions
const W_inside_frame = w - 2 * clampedFrameThickness;
// Total width taken by all internal mullions
const total_internal_mullion_width = (clampedNumVerticalPanes - 1) * clampedFrameThickness;
// Total width purely for glass across all panes
const pure_glass_width_all_panes = W_inside_frame - total_internal_mullion_width;
if (clampedNumVerticalPanes > 1 && pure_glass_width_all_panes > 0) { // Only draw mullions if there are multiple panes and space for glass
const single_pane_glass_width = pure_glass_width_all_panes / clampedNumVerticalPanes;
// Start drawing from the left edge of the glass area
let current_glass_pane_x_start = clampedFrameThickness;
for (let i = 0; i < clampedNumVerticalPanes - 1; i++) { // Need to draw (numVerticalPanes - 1) mullions
const mullion_x_start = current_glass_pane_x_start + single_pane_glass_width;
const mullion_x_end = mullion_x_start + clampedFrameThickness;
// Safety check: ensure mullion doesn't extend beyond the frame
if (mullion_x_end > w - clampedFrameThickness) {
break;
}
ctx.beginPath();
// Bottom edge of mullion
ctx.moveTo(mullion_x_start, h - clampedFrameThickness);
ctx.lineTo(mullion_x_end, h - clampedFrameThickness);
// Top edge of mullion - follows the inner arch curve
const y_top_right_mullion = getInnerArchY(mullion_x_end);
const y_top_left_mullion = getInnerArchY(mullion_x_start);
ctx.lineTo(mullion_x_end, y_top_right_mullion);
ctx.lineTo(mullion_x_start, y_top_left_mullion);
ctx.closePath();
ctx.fill(); // Mullions are the same color as the frame
// Next pane starts after this mullion
current_glass_pane_x_start = mullion_x_end;
}
}
return canvas;
}
Free Image Tool Creator
Can't find the image tool you're looking for? Create one based on your own needs now!
The Image Cathedral Window Frame Creator is a web tool designed to transform standard images into stylized representations resembling cathedral windows. Users can customize various aspects of the window frame, including color, thickness, and the number of vertical panes. This tool is suitable for artistic and decorative purposes, allowing users to create visually appealing images for use in projects such as digital artwork, social media posts, or personalized gifts. It gives users a unique way to present their images with a gothic architectural design aesthetic.