Please bookmark this page to avoid losing your image tool!

Image Cryptid Sighting Report Creator

(Free & Supports Bulk Upload)

Drag & drop your images here or

The result will appear here...
You can edit the below JavaScript code to customize the image tool.
async function processImage(
    originalImg,
    reportTitle = "FIELD REPORT: UNIDENTIFIED SUBJECT",
    sightingDate = new Date().toISOString().split('T')[0],
    sightingTime = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false }),
    sightingLocation = "SECTOR 7 // COORDINATES REDACTED",
    caseFileNumber = `CFN-${Math.random().toString(36).substring(2, 8).toUpperCase()}`,
    agentName = "AGENT [REDACTED]",
    reportNotes = "Subject observed exhibiting anomalous characteristics. Photographic evidence attached. Conditions: Low light, significant distance. Quality of media is suboptimal but warrants further analysis.",
    classificationStamp = "CONFIDENTIAL // EYES ONLY",
    imageEffect = "grainy" // "none", "grainy", "blurry"
) {

    // --- Helper: Load Web Font ---
    async function loadWebFont(fontFamily, fontUrl) {
        try {
            // Check if font is already loaded or available using the CSS Font Loading API
            if (document.fonts.check(`12px "${fontFamily}"`)) {
                return true;
            }
            const fontFace = new FontFace(fontFamily, `url(${fontUrl}) format('woff2')`);
            await fontFace.load();
            document.fonts.add(fontFace);
            return true;
        } catch (e) {
            console.error(`Font ${fontFamily} failed to load from ${fontUrl}:`, e);
            return false; 
        }
    }

    // --- Helper: Wrap Text for Height Calculation ---
    function getWrappedTextHeight(context, text, maxWidth, lineHeight, font) {
        context.font = font;
        const words = text.split(' ');
        if (text.trim() === "") return 0;
        
        let line = '';
        let linesCount = 1;

        for (let n = 0; n < words.length; n++) {
            const testLine = line + words[n] + ' ';
            const metrics = context.measureText(testLine);
            if (metrics.width > maxWidth && n > 0) {
                linesCount++;
                line = words[n] + ' ';
            } else {
                line = testLine;
            }
        }
        return linesCount * lineHeight;
    }

    // --- Helper: Wrap Text and Draw ---
    function wrapTextActualDraw(context, text, x, y, maxWidth, lineHeight, font, color) {
        context.font = font;
        context.fillStyle = color;
        context.textBaseline = 'top';

        const words = text.split(' ');
        if (text.trim() === "") return y;

        let line = '';
        let currentY = y;

        for (let n = 0; n < words.length; n++) {
            const word = words[n];
            const testLineTry = line + word + ' ';
            const metrics = context.measureText(testLineTry);
            
            if (metrics.width > maxWidth && line.length > 0) { // line.length > 0 to ensure not breaking an empty line
                context.fillText(line.trim(), x, currentY);
                line = word + ' ';
                currentY += lineHeight;
            } else {
                line = testLineTry;
            }
        }
        context.fillText(line.trim(), x, currentY); // Draw the last line
        return currentY + lineHeight; 
    }


    // --- Constants and Configuration ---
    const CANVAS_WIDTH = 800;
    const PADDING = 40;
    const CONTENT_WIDTH = CANVAS_WIDTH - 2 * PADDING;
    
    const BG_COLOR = "#F0EAD6"; // Creamy beige
    const TEXT_COLOR = "#3D3D3D"; // Dark desaturated gray
    const TEXT_COLOR_MUTED = "#666666";
    const STAMP_TEXT_COLOR = "rgba(170, 0, 0, 0.75)"; // Semi-transparent red
    const STAMP_BORDER_COLOR = "rgba(100, 0, 0, 0.8)"; // Darker red for outline

    const FONT_URL_SPECIAL_ELITE = 'https://fonts.gstatic.com/s/specialelite/v13/XLYgIZbkc4JPVQnNI7zQMYCSTlNPVSNONA.woff2';
    const fontLoaded = await loadWebFont('Special Elite', FONT_URL_SPECIAL_ELITE);
    const fontFamilyCSS = fontLoaded ? "'Special Elite', monospace" : "monospace";

    const FONT_TITLE_SIZE = 26;
    const FONT_TITLE = `bold ${FONT_TITLE_SIZE}px ${fontFamilyCSS}`;
    const FONT_HEADER_LABEL_SIZE = 13;
    const FONT_HEADER_LABEL = `bold ${FONT_HEADER_LABEL_SIZE}px ${fontFamilyCSS}`;
    const FONT_HEADER_VALUE_SIZE = 13;
    const FONT_HEADER_VALUE = `${FONT_HEADER_VALUE_SIZE}px ${fontFamilyCSS}`;
    const FONT_NOTES_TITLE_SIZE = 14;
    const FONT_NOTES_TITLE = `bold ${FONT_NOTES_TITLE_SIZE}px ${fontFamilyCSS}`;
    const FONT_NOTES_CONTENT_SIZE = 12;
    const FONT_NOTES_CONTENT = `${FONT_NOTES_CONTENT_SIZE}px ${fontFamilyCSS}`;
    const FONT_STAMP_SIZE = 28;
    const FONT_STAMP = `bold ${FONT_STAMP_SIZE}px 'Arial Black', Gadget, sans-serif`;

    const TITLE_SPACING = 30;
    const HEADER_BLOCK_SPACING = 15;
    const DIVIDER_SPACING_AFTER = 20;
    const IMAGE_SPACING_AFTER = 25;
    const NOTES_SPACING_AFTER = 20; // Space after notes content, before stamp consideration
    const STAMP_AREA_RESERVE_HEIGHT = FONT_STAMP_SIZE * 1.5;


    const LINE_HEIGHT_HEADER = FONT_HEADER_LABEL_SIZE * 1.5;
    const LINE_HEIGHT_NOTES_TITLE_ACTUAL = FONT_NOTES_TITLE_SIZE * 1.2; // Actual drawing height for title
    const LINE_HEIGHT_NOTES_CONTENT = FONT_NOTES_CONTENT_SIZE * 1.4;


    // --- 1. Calculate Content Heights and Total Canvas Height ---
    let layoutY = PADDING;

    layoutY += FONT_TITLE_SIZE; 
    layoutY += TITLE_SPACING;

    layoutY += LINE_HEIGHT_HEADER * 4; // 4 lines in header block
    layoutY += HEADER_BLOCK_SPACING;

    layoutY += 1; // Divider line height
    layoutY += DIVIDER_SPACING_AFTER;

    const MAX_IMG_DISPLAY_HEIGHT = 350; 
    const MAX_IMG_DISPLAY_WIDTH = CONTENT_WIDTH;
    let imgDisplayWidth = originalImg.naturalWidth || originalImg.width;
    let imgDisplayHeight = originalImg.naturalHeight || originalImg.height;
    const aspectRatio = imgDisplayWidth / imgDisplayHeight;

    if (imgDisplayWidth > MAX_IMG_DISPLAY_WIDTH) {
        imgDisplayWidth = MAX_IMG_DISPLAY_WIDTH;
        imgDisplayHeight = imgDisplayWidth / aspectRatio;
    }
    if (imgDisplayHeight > MAX_IMG_DISPLAY_HEIGHT) {
        imgDisplayHeight = MAX_IMG_DISPLAY_HEIGHT;
        imgDisplayWidth = imgDisplayHeight * aspectRatio;
    }
    layoutY += imgDisplayHeight;
    layoutY += IMAGE_SPACING_AFTER;

    layoutY += LINE_HEIGHT_NOTES_TITLE_ACTUAL;
    
    const tempCtxMeasure = document.createElement('canvas').getContext('2d');
    const notesActualHeight = getWrappedTextHeight(tempCtxMeasure, reportNotes, CONTENT_WIDTH, LINE_HEIGHT_NOTES_CONTENT, FONT_NOTES_CONTENT);
    layoutY += notesActualHeight;
    layoutY += NOTES_SPACING_AFTER;

    layoutY += STAMP_AREA_RESERVE_HEIGHT; 

    const totalCalculatedHeight = layoutY + PADDING;


    // --- 2. Create Main Canvas and Draw ---
    const canvas = document.createElement('canvas');
    canvas.width = CANVAS_WIDTH;
    canvas.height = totalCalculatedHeight;
    const ctx = canvas.getContext('2d');

    ctx.fillStyle = BG_COLOR;
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.textBaseline = 'top'; 

    let currentY = PADDING; // Reset Y for drawing

    ctx.font = FONT_TITLE;
    ctx.fillStyle = TEXT_COLOR;
    ctx.textAlign = 'center';
    ctx.fillText(reportTitle, CANVAS_WIDTH / 2, currentY);
    currentY += FONT_TITLE_SIZE + TITLE_SPACING;

    ctx.textAlign = 'left';
    const headerValueXOffset = PADDING + 95; 

    ctx.font = FONT_HEADER_LABEL;
    ctx.fillStyle = TEXT_COLOR;
    ctx.fillText("CASE FILE:", PADDING, currentY);
    ctx.font = FONT_HEADER_VALUE;
    ctx.fillText(caseFileNumber, headerValueXOffset, currentY);
    currentY += LINE_HEIGHT_HEADER;

    ctx.font = FONT_HEADER_LABEL;
    ctx.fillText("DATE:", PADDING, currentY);
    ctx.font = FONT_HEADER_VALUE;
    ctx.fillText(sightingDate, headerValueXOffset, currentY);
    
    const timeLabelText = "TIME:";
    ctx.font = FONT_HEADER_LABEL;
    const timeLabelWidth = ctx.measureText(timeLabelText).width;
    ctx.font = FONT_HEADER_VALUE; // For measuring value
    const timeValueWidth = ctx.measureText(sightingTime).width;
    const timeValueX = CANVAS_WIDTH - PADDING - timeValueWidth;
    const timeLabelX = timeValueX - timeLabelWidth - 5; 
    ctx.font = FONT_HEADER_LABEL;
    ctx.fillText(timeLabelText, timeLabelX, currentY);
    ctx.font = FONT_HEADER_VALUE;
    ctx.fillText(sightingTime, timeValueX, currentY);
    currentY += LINE_HEIGHT_HEADER;

    ctx.font = FONT_HEADER_LABEL;
    ctx.fillText("LOCATION:", PADDING, currentY);
    ctx.font = FONT_HEADER_VALUE;
    ctx.fillText(sightingLocation, headerValueXOffset, currentY, CONTENT_WIDTH - (headerValueXOffset - PADDING)); 
    currentY += LINE_HEIGHT_HEADER;

    ctx.font = FONT_HEADER_LABEL;
    ctx.fillText("AGENT:", PADDING, currentY);
    ctx.font = FONT_HEADER_VALUE;
    ctx.fillText(agentName, headerValueXOffset, currentY);
    currentY += LINE_HEIGHT_HEADER + HEADER_BLOCK_SPACING;
    
    ctx.beginPath();
    ctx.moveTo(PADDING, currentY);
    ctx.lineTo(CANVAS_WIDTH - PADDING, currentY);
    ctx.strokeStyle = TEXT_COLOR_MUTED; 
    ctx.lineWidth = 0.75; 
    ctx.stroke();
    currentY += 1 + DIVIDER_SPACING_AFTER;

    const processedImageCanvas = document.createElement('canvas');
    processedImageCanvas.width = originalImg.naturalWidth || originalImg.width;
    processedImageCanvas.height = originalImg.naturalHeight || originalImg.height;
    const pCtx = processedImageCanvas.getContext('2d');
    pCtx.drawImage(originalImg, 0, 0, processedImageCanvas.width, processedImageCanvas.height);

    if (imageEffect === "grainy") {
        const imageData = pCtx.getImageData(0, 0, processedImageCanvas.width, processedImageCanvas.height);
        const data = imageData.data;
        const grainAmount = 30; 
        for (let i = 0; i < data.length; i += 4) {
            if (data[i+3] === 0) continue; 
            const noise = (Math.random() - 0.5) * grainAmount;
            data[i]     = Math.max(0, Math.min(255, data[i] + noise));
            data[i + 1] = Math.max(0, Math.min(255, data[i + 1] + noise));
            data[i + 2] = Math.max(0, Math.min(255, data[i + 2] + noise));
        }
        pCtx.putImageData(imageData, 0, 0);
    } else if (imageEffect === "blurry") {
        pCtx.filter = 'blur(1.2px)'; 
        pCtx.drawImage(processedImageCanvas, 0, 0); 
        pCtx.filter = 'none'; 
    }

    const imgX = (CANVAS_WIDTH - imgDisplayWidth) / 2;
    ctx.drawImage(processedImageCanvas, imgX, currentY, imgDisplayWidth, imgDisplayHeight);
    
    ctx.strokeStyle = "#999999"; 
    ctx.lineWidth = 1;
    ctx.strokeRect(imgX -1, currentY -1, imgDisplayWidth + 2, imgDisplayHeight + 2);

    currentY += imgDisplayHeight + IMAGE_SPACING_AFTER;

    ctx.textAlign = 'left';
    ctx.font = FONT_NOTES_TITLE;
    ctx.fillStyle = TEXT_COLOR;
    ctx.fillText("OBSERVATION NOTES:", PADDING, currentY);
    currentY += LINE_HEIGHT_NOTES_TITLE_ACTUAL;
    
    currentY = wrapTextActualDraw(ctx, reportNotes, PADDING, currentY, CONTENT_WIDTH, LINE_HEIGHT_NOTES_CONTENT, FONT_NOTES_CONTENT, TEXT_COLOR);
    currentY += NOTES_SPACING_AFTER;


    const stampAngle = -Math.PI / 12; // Approx 15 degrees
    ctx.font = FONT_STAMP; // Set font for measuring stamp text
    const stampTextMetrics = ctx.measureText(classificationStamp);
    const stampTextWidth = stampTextMetrics.width;
    
    // Position stamp near bottom right
    const stampBoxWidth = stampTextWidth * Math.cos(stampAngle) + FONT_STAMP_SIZE * Math.sin(Math.abs(stampAngle)); // Approximated bounding box width
    const stampX = CANVAS_WIDTH - PADDING - (stampBoxWidth / 2) - 10; // Adjust positioning
    const stampY = totalCalculatedHeight - PADDING - (FONT_STAMP_SIZE / 2) - 10;

    ctx.save();
    ctx.textAlign = 'center'; // Center the text for rotation
    ctx.translate(stampX, stampY); 
    ctx.rotate(stampAngle); 
    
    ctx.fillStyle = STAMP_TEXT_COLOR;
    ctx.fillText(classificationStamp, 0, 0);
    
    ctx.strokeStyle = STAMP_BORDER_COLOR;
    ctx.lineWidth = 0.7; 
    ctx.strokeText(classificationStamp, 0, 0);
    ctx.restore();

    return canvas;
}

Free Image Tool Creator

Can't find the image tool you're looking for?
Create one based on your own needs now!

Description

The Image Cryptid Sighting Report Creator is a web-based tool that enables users to create detailed reports of unidentified cryptid sightings. Users can upload an image related to their sighting, and the tool will generate a visually formatted report that includes specific metadata such as the report title, sighting date and time, location, case file number, agent name, and observation notes. The report can be customized with different image effects, such as grainy or blurry, to enhance the photographic evidence. This tool is ideal for cryptid enthusiasts, researchers, or anyone documenting peculiar sightings, facilitating both personal records and sharing with the community.

Leave a Reply

Your email address will not be published. Required fields are marked *