You can edit the below JavaScript code to customize the image tool.
function processImage(
originalImg,
caseNumber = "CASE #001",
location = "UNKNOWN LOCATION",
customNotes = "PARANORMAL ACTIVITY SUSPECTED",
textColor = "rgba(0, 255, 0, 0.85)",
fontFamily = "Consolas, 'Courier New', monospace",
fontSize = 16,
showRecSymbol = 1,
recSymbolColor = "rgba(255, 0, 0, 0.9)",
applySpookyFilter = 1,
vignetteIntensity = 0.6
) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Use naturalWidth/Height if available, fallback to width/height.
// These might be 0 if the image isn't loaded yet or is invalid.
const imgWidth = originalImg.naturalWidth || originalImg.width;
const imgHeight = originalImg.naturalHeight || originalImg.height;
// Handle cases where the image might not be loaded or is invalid
if (imgWidth === 0 || imgHeight === 0) {
console.error("Image not loaded or has zero dimensions. Ensure originalImg is a fully loaded HTMLImageElement.");
// Return a small canvas with an error message
canvas.width = 250;
canvas.height = 50;
ctx.fillStyle = 'rgba(128, 0, 0, 0.8)'; // Dark red
ctx.fillRect(0,0,canvas.width, canvas.height);
ctx.fillStyle = 'white';
ctx.font = '12px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText("Error: Image data not available or invalid.", canvas.width/2, canvas.height/2);
return canvas;
}
canvas.width = imgWidth;
canvas.height = imgHeight;
// 1. Apply base image filter if requested
if (applySpookyFilter === 1) {
// This filter gives a "security camera" or "night vision footage" feel
ctx.filter = 'grayscale(60%) contrast(130%) brightness(85%)';
}
ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
ctx.filter = 'none'; // Reset filter for any subsequent drawings on the canvas
// 2. Apply vignette if requested and intensity is positive
// VignetteIntensity should be between 0 (no vignette) and 1 (max vignette)
const effectiveVignetteIntensity = Math.max(0, Math.min(1, vignetteIntensity));
if (applySpookyFilter === 1 && effectiveVignetteIntensity > 0) {
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
// Inner radius of the vignette's clear area
const innerRadius = Math.min(canvas.width, canvas.height) * 0.2; // Smaller clear center for more drama
// Outer radius where the vignette is most opaque
const outerRadius = Math.sqrt(Math.pow(centerX, 2) + Math.pow(centerY, 2)); // Covers up to the corners
const gradient = ctx.createRadialGradient(
centerX, centerY, innerRadius,
centerX, centerY, outerRadius
);
// Vignette fades from transparent to dark
gradient.addColorStop(0, 'rgba(0,0,0,0)'); // Transparent at innerRadius
// Control point for smoother or sharper fade
gradient.addColorStop(0.65, `rgba(0,0,0,${(effectiveVignetteIntensity * 0.4).toFixed(2)})`);
gradient.addColorStop(1, `rgba(0,0,0,${effectiveVignetteIntensity.toFixed(2)})`); // Full intensity at outerRadius
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
// Text and Symbol settings
const safeFontSize = Math.max(8, fontSize); // Ensure font size is reasonably usable
const padding = Math.max(8, Math.round(safeFontSize * 0.6));
ctx.font = `${safeFontSize}px ${fontFamily}`;
ctx.fillStyle = textColor;
// --- Top Text Elements ---
ctx.textBaseline = 'top'; // Align text from its top edge
// REC Symbol and Evidence Text (Top-left)
let currentXDrawPos = padding; // X-coordinate for drawing elements
if (showRecSymbol === 1) {
const recCircleRadius = Math.round(safeFontSize * 0.45);
const recCircleCenterY = padding + recCircleRadius;
const recCircleCenterX = currentXDrawPos + recCircleRadius;
ctx.beginPath();
ctx.arc(recCircleCenterX, recCircleCenterY, recCircleRadius, 0, Math.PI * 2);
ctx.fillStyle = recSymbolColor;
ctx.fill();
// "REC" text inside the circle, if space allows
const recTextSize = Math.round(recCircleRadius * 1.1); // Font size for "REC"
if (recTextSize >= 6) { // Only draw if potentially legible
ctx.font = `bold ${recTextSize}px ${fontFamily}`;
ctx.fillStyle = 'white';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText("REC", recCircleCenterX, recCircleCenterY);
}
currentXDrawPos += recCircleRadius * 2 + Math.round(padding / 1.5); // Advance X for evidence text
// Reset font settings for the main evidence text
ctx.font = `${safeFontSize}px ${fontFamily}`;
ctx.fillStyle = textColor;
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
}
const evidenceText = `EVIDENCE: ${caseNumber}`;
ctx.fillText(evidenceText, currentXDrawPos, padding);
// Timestamp (Top-right)
const now = new Date();
const timestamp = `${String(now.getFullYear())}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
ctx.textAlign = 'right';
ctx.fillText(timestamp, canvas.width - padding, padding);
// --- Bottom Text Elements ---
ctx.textBaseline = 'bottom'; // Align text from its bottom edge for easier placement at canvas bottom
ctx.font = `${safeFontSize}px ${fontFamily}`; // Ensure font is set
ctx.fillStyle = textColor;
const bottomTextY = canvas.height - padding; // Y-coordinate for the baseline of bottom-most text
// Measure text widths to decide on layout (side-by-side or stacked)
ctx.textAlign = 'left'; // Set for measuring location string
const locationWidth = ctx.measureText(location).width;
// For customNotes, measure as if it were drawn from the right
// (ctx.measureText assumes left-alignment, so this will correctly give its width)
const notesWidth = ctx.measureText(customNotes).width;
const spaceBetweenBottomTexts = padding * 2; // Required space between location and notes if side-by-side
// Check if location and customNotes can fit on one line without overlapping
if (padding + locationWidth + spaceBetweenBottomTexts + notesWidth + padding <= canvas.width) {
// Texts fit side-by-side
ctx.textAlign = 'left';
ctx.fillText(location, padding, bottomTextY);
ctx.textAlign = 'right';
ctx.fillText(customNotes, canvas.width - padding, bottomTextY);
} else {
// Texts are too long; stack them (Location above Notes, both left-aligned)
ctx.textAlign = 'left';
const textLineHeightApproximation = safeFontSize * 1.2; // Common estimate for line height
ctx.fillText(location, padding, bottomTextY - textLineHeightApproximation);
ctx.fillText(customNotes, padding, bottomTextY);
}
// Reset canvas context properties that were changed, to be a good citizen
ctx.textAlign = 'left';
ctx.textBaseline = 'alphabetic'; // Canvas default baseline
ctx.filter = 'none'; // Ensure filter is off
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 Evidence File Creator for Ghost Hunters allows users to create professionally annotated images tailored for paranormal investigations. By uploading an original image, users can add key details such as case number, location, and notes about the paranormal activity. This tool enhances images with visual effects, including a spooky filter and vignette, to give them a unique presentation. It integrates timestamps and custom annotations, making it ideal for ghost hunters, investigators, and enthusiasts looking to document their findings visually. It’s perfect for creating evidence files, reports, or sharing compelling images on social media.