| Server IP : 127.0.0.1 / Your IP : 216.73.216.48 Web Server : Apache/2.4.58 (Win64) OpenSSL/3.1.3 PHP/8.2.12 System : Windows NT DESKTOP-3H4FHQJ 10.0 build 19045 (Windows 10) AMD64 User : win 10 ( 0) PHP Version : 8.2.12 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : OFF | Perl : OFF | Python : OFF | Sudo : OFF | Pkexec : OFF Directory : D:/xampp/htdocs-coblaa/CB_quote_editor/ |
Upload File : |
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CB Quote Editor</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&family=Roboto:wght@400;700&family=Playfair+Display:wght@400;700&family=Open+Sans:wght@400;700&family=Lora:wght@440;700&family=Times+New+Roman&family=Didot&family=Georgia&family=Palatino&family=Bookman&family=New+Century+Schoolbook&family=American+Typewriter&family=Andale+Mono&family=Courier+New&family=Arial&family=Helvetica&family=Verdana&family=Trebuchet+MS&family=Gill+Sans&family=Noto+Sans&family=Optima&family=Comic+Sans+MS&family=Apple+Chancery&family=Bradley+Hand&family=Brush+Script+MT&family=Snell+Roundhand&family=Impact&family=Luminari&family=Chalkduster&family=Jazz+LET&family=Blippo&family=Stencil+Std&family=Marker+Felt&family=Trattatello&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Inter', sans-serif;
margin: 0;
overflow-y: auto; /* Allow scrolling for more controls and overall page */
display: flex;
flex-direction: column;
align-items: center;
justify-content: center; /* Center content vertically */
min-height: 100vh;
background-color: #f0f2f5; /* Light background */
color: #333;
}
canvas {
border-radius: 1rem; /* Rounded corners for canvas */
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
background-color: #ffffff; /* White canvas background */
display: block;
touch-action: none; /* Disable default touch actions to prevent scrolling/zooming during drag */
cursor: grab; /* Indicate draggable */
/* Canvas dimensions will be set by JS for responsiveness */
}
canvas.dragging {
cursor: grabbing; /* Indicate dragging in progress */
}
.section-container {
width: 90%;
max-width: 600px;
margin-top: 1.5rem;
padding: 1.25rem;
background-color: #ffffff;
border-radius: 1rem;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08);
}
.section-title {
font-size: 1.25rem;
font-weight: 700;
color: #333;
margin-bottom: 1rem;
text-align: center;
}
.button-group {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
justify-content: center;
margin-bottom: 1rem;
}
.button {
padding: 0.75rem 1.5rem;
border-radius: 0.75rem;
font-weight: 700;
cursor: pointer;
transition: all 0.2s ease-in-out;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
font-size: 1rem;
}
.button-primary {
background-color: #4CAF50; /* Green */
color: white;
border: none;
}
.button-primary:hover {
background-color: #45a049;
transform: translateY(-2px);
}
.button-secondary {
background-color: #007BFF; /* Blue */
color: white;
border: none;
}
.button-secondary:hover {
background-color: #0056b3;
transform: translateY(-2px);
}
.button-tertiary {
background-color: #FFC107; /* Amber */
color: #333;
border: none;
}
.button-tertiary:hover {
background-color: #e0a800;
transform: translateY(-2px);
}
.button-danger {
background-color: #dc3545; /* Red */
color: white;
border: none;
}
.button-danger:hover {
background-color: #c82333;
transform: translateY(-2px);
}
.control-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1rem;
}
.control-group {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.control-group label {
font-size: 0.9rem;
font-weight: 600;
color: #555;
}
.control-group input[type="color"],
.control-group input[type="number"],
.control-group input[type="text"],
.control-group textarea,
.control-group select {
padding: 0.5rem;
border-radius: 0.5rem;
border: 1px solid #ccc;
font-family: 'Inter', sans-serif;
font-size: 0.9rem;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
width: 100%; /* Full width within its grid column */
}
.control-group textarea {
height: 70px; /* Adjust height for textareas */
resize: vertical;
}
/* Specific font family styles for the select option previews */
.font-inter { font-family: 'Inter', sans-serif; }
.font-roboto { font-family: 'Roboto', sans-serif; }
.font-playfair-display { font-family: 'Playfair Display', serif; }
.font-open-sans { font-family: 'Open Sans', sans-serif; }
.font-lora { font-family: 'Lora', serif; }
.font-times { font-family: 'Times', 'Times New Roman', serif; }
.font-didot { font-family: 'Didot', serif; }
.font-georgia { font-family: 'Georgia', serif; }
.font-palatino { font-family: 'Palatino', 'URW Palladio L', serif; }
.font-bookman { font-family: 'Bookman', 'URW Bookman L', serif; }
.font-new-century-schoolbook { font-family: 'New Century Schoolbook', 'TeX Gyre Schola', serif; }
.font-american-typewriter { font-family: 'American Typewriter', serif; }
.font-serif { font-family: 'serif'; }
.font-monospace { font-family: 'monospace'; }
.font-andale-mono { font-family: 'Andale Mono', monospace; }
.font-courier-new { font-family: 'Courier New', monospace; }
.font-courier { font-family: 'Courier', monospace; }
.font-arial { font-family: 'Arial', sans-serif; }
.font-helvetica { font-family: 'Helvetica', sans-serif; }
.font-verdana { font-family: 'Verdana', sans-serif; }
.font-trebuchet-ms { font-family: 'Trebuchet MS', sans-serif; }
.font-gill-sans { font-family: 'Gill Sans', sans-serif; }
.font-noto-sans { font-family: 'Noto Sans', sans-serif; }
.font-avantgarde { font-family: 'Avantgarde', 'TeX Gyre Adventor', 'URW Gothic L', sans-serif; }
.font-optima { font-family: 'Optima', sans-serif; }
.font-arial-narrow { font-family: 'Arial Narrow', sans-serif; }
.font-sans-serif { font-family: 'sans-serif'; }
.font-freemono { font-family: 'FreeMono', monospace; }
.font-ocr-a-std { font-family: 'OCR A Std', monospace; }
.font-dejavu-sans-mono { font-family: 'DejaVu Sans Mono', monospace; }
.font-cursive { font-family: 'cursive'; }
.font-comic-sans-ms { font-family: 'Comic Sans MS', 'Comic Sans', cursive; }
.font-apple-chancery { font-family: 'Apple Chancery', cursive; }
.font-bradley-hand { font-family: 'Bradley Hand', cursive; }
.font-brush-script-mt { font-family: 'Brush Script MT', 'Brush Script Std', cursive; }
.font-snell-roundhand { font-family: 'Snell Roundhand', cursive; }
.font-urw-chancery-l { font-family: 'URW Chancery L', cursive; }
.font-impact { font-family: 'Impact', fantasy; }
.font-luminari { font-family: 'Luminari', fantasy; }
.font-chalkduster { font-family: 'Chalkduster', fantasy; }
.font-jazz-let { font-family: 'Jazz LET', fantasy; }
.font-blippo { font-family: 'Blippo', fantasy; }
.font-stencil-std { font-family: 'Stencil Std', fantasy; }
.font-marker-felt { font-family: 'Marker Felt', fantasy; }
.font-trattatello { font-family: 'Trattatello', fantasy; }
.font-fantasy { font-family: 'fantasy'; }
/* Responsive adjustments */
@media (max-width: 768px) {
body {
padding: 1rem;
}
.section-container {
width: 95%;
padding: 1rem;
}
.button-group {
flex-direction: column;
gap: 0.5rem;
}
.button {
width: 100%;
}
.control-grid {
grid-template-columns: 1fr; /* Stack controls vertically on small screens */
}
/* Removed canvas-wrapper height adjustment as it's no longer needed */
}
</style>
</head>
<body class="bg-gray-100 flex flex-col items-center min-h-screen">
<div class="section-title" style="height:50px;line-height:50px;">CB Quote Editor</div>
<!-- Canvas is now directly in the body, and its size will be set by JS -->
<canvas id="quoteCanvas"></canvas>
<div class="section-container">
<div class="section-title">Quote Text</div>
<div class="control-grid">
<div class="control-group">
<label for="quoteLine1Textarea">Title:</label>
<textarea id="quoteLine1Textarea" placeholder="Enter title..."></textarea>
</div>
<div class="control-group">
<label for="quoteLine2Textarea">Body Text (Optional):</label>
<textarea id="quoteLine2Textarea" placeholder="Enter body text..."></textarea>
</div>
<div class="control-group">
<label for="authorInput">Author:</label>
<input type="text" id="authorInput" placeholder="Enter author..." value="Coblaa">
</div>
</div>
<!-- Removed Update Quote and New Random Quote buttons -->
</div>
<div class="section-container">
<div class="section-title">Text Settings</div>
<div class="control-grid">
<div class="control-group">
<label for="quoteLine1Color">Title Color:</label>
<input type="color" id="quoteLine1Color" value="#333333">
</div>
<div class="control-group">
<label for="quoteLine1FontSize">Title Font Size:</label>
<input type="number" id="quoteLine1FontSize" value="18" min="10" max="100">
</div>
<div class="control-group">
<label for="titleYAdjust">Title Vertical Adjust (px):</label>
<input type="number" id="titleYAdjust" value="0" step="5">
</div>
<div class="control-group">
<label for="titleXAdjust">Title Horizontal Adjust (px):</label>
<input type="number" id="titleXAdjust" value="0" step="5">
</div>
<div class="control-group">
<label for="quoteLine2Color">Body Text Color:</label>
<input type="color" id="quoteLine2Color" value="#333333">
</div>
<div class="control-group">
<label for="quoteLine2FontSize">Body Text Font Size:</label>
<input type="number" id="quoteLine2FontSize" value="16" min="10" max="100">
</div>
<div class="control-group">
<label for="bodyYAdjust">Body Vertical Adjust (px):</label>
<input type="number" id="bodyYAdjust" value="0" step="5">
</div>
<div class="control-group">
<label for="bodyXAdjust">Body Horizontal Adjust (px):</label>
<input type="number" id="bodyXAdjust" value="0" step="5">
</div>
<div class="control-group">
<label for="authorTextColor">Author Color:</label>
<input type="color" id="authorTextColor" value="#555555">
</div>
<div class="control-group">
<label for="authorFontSize">Author Font Size:</label>
<input type="number" id="authorFontSize" value="12" min="10" max="100">
</div>
<div class="control-group">
<label for="authorYAdjust">Author Vertical Adjust (px):</label>
<input type="number" id="authorYAdjust" value="0" step="5">
</div>
<div class="control-group">
<label for="authorXAdjust">Author Horizontal Adjust (px):</label>
<input type="number" id="authorXAdjust" value="0" step="5">
</div>
<div class="control-group">
<label for="fontFamilySelector">Font Family:</label>
<select id="fontFamilySelector">
<option value="Inter, sans-serif" class="font-inter">Inter</option>
<option value="Roboto, sans-serif" class="font-roboto">Roboto</option>
<option value="Playfair Display, serif" class="font-playfair-display">Playfair Display</option>
<option value="Open Sans, sans-serif" class="font-open-sans">Open Sans</option>
<option value="Lora, serif" class="font-lora">Lora</option>
<option value="Times, Times New Roman, serif" class="font-times">Times, Times New Roman</option>
<option value="Didot, serif" class="font-didot">Didot</option>
<option value="Georgia, serif" class="font-georgia">Georgia</option>
<option value="Palatino, URW Palladio L, serif" class="font-palatino">Palatino</option>
<option value="Bookman, URW Bookman L, serif" class="font-bookman">Bookman</option>
<option value="New Century Schoolbook, TeX Gyre Schola, serif" class="font-new-century-schoolbook">New Century Schoolbook</option>
<option value="American Typewriter, serif" class="font-american-typewriter">American Typewriter</option>
<option value="serif" class="font-serif">serif (Generic)</option>
<option value="Andale Mono, monospace" class="font-andale-mono">Andale Mono</option>
<option value="Courier New, monospace" class="font-courier-new">Courier New</option>
<option value="Courier, monospace" class="font-courier">Courier</option>
<option value="Arial, sans-serif" class="font-arial">Arial</option>
<option value="Helvetica, sans-serif" class="font-helvetica">Helvetica</option>
<option value="Verdana, sans-serif" class="font-verdana">Verdana</option>
<option value="Trebuchet MS, sans-serif" class="font-trebuchet-ms">Trebuchet MS</option>
<option value="Gill Sans, sans-serif" class="font-gill-sans">Gill Sans</option>
<option value="Noto Sans, sans-serif" class="font-noto-sans">Noto Sans</option>
<option value="Avantgarde, TeX Gyre Adventor, URW Gothic L, sans-serif" class="font-avantgarde">Avantgarde</option>
<option value="Optima, sans-serif" class="font-optima">Optima</option>
<option value="Arial Narrow, sans-serif" class="font-arial-narrow">Arial Narrow</option>
<option value="sans-serif" class="font-sans-serif">sans-serif (Generic)</option>
<option value="FreeMono, monospace" class="font-freemono">FreeMono</option>
<option value="OCR A Std, monospace" class="font-ocr-a-std">OCR A Std</option>
<option value="DejaVu Sans Mono, monospace" class="font-dejavu-sans-mono">DejaVu Sans Mono</option>
<option value="monospace" class="font-monospace">monospace (Generic)</option>
<option value="Comic Sans MS, Comic Sans, cursive" class="font-comic-sans-ms">Comic Sans MS</option>
<option value="Apple Chancery, cursive" class="font-apple-chancery">Apple Chancery</option>
<option value="Bradley Hand, cursive" class="font-bradley-hand">Bradley Hand</option>
<option value="Brush Script MT, Brush Script Std, cursive" class="font-brush-script-mt">Brush Script MT</option>
<option value="Snell Roundhand, cursive" class="font-snell-roundhand">Snell Roundhand</option>
<option value="URW Chancery L, cursive" class="font-urw-chancery-l">URW Chancery L</option>
<option value="cursive" class="font-cursive">cursive (Generic)</option>
<option value="Impact, fantasy" class="font-impact">Impact</option>
<option value="Luminari, fantasy" class="font-luminari">Luminari</option>
<option value="Chalkduster, fantasy" class="font-chalkduster">Chalkduster</option>
<option value="Jazz LET, fantasy" class="font-jazz-let">Jazz LET</option>
<option value="Blippo, fantasy" class="font-blippo">Blippo</option>
<option value="Stencil Std, fantasy" class="font-stencil-std">Stencil Std</option>
<option value="Marker Felt, fantasy" class="font-marker-felt">Marker Felt</option>
<option value="Trattatello, fantasy" class="font-trattatello">Trattatello</option>
<option value="fantasy" class="font-fantasy">fantasy (Generic)</option>
</select>
</div>
</div>
</div>
<div class="section-container">
<div class="section-title">Background Settings</div>
<div class="control-grid">
<div class="control-group">
<label for="bgColorPicker">Background Color:</label>
<input type="color" id="bgColorPicker" value="#ffffff">
</div>
<div class="control-group">
<label>Background Image:</label>
<button id="bgImageBtn" class="button button-tertiary">Add Image</button>
<input type="file" id="imageUpload" accept="image/*" style="display: none;">
</div>
<div class="control-group">
<label> </label> <!-- Placeholder for alignment -->
<button id="clearBgImageBtn" class="button button-danger">Clear Image</button>
</div>
</div>
</div>
<div class="section-container">
<div class="section-title">Export</div>
<div class="button-group">
<button id="downloadBtn" class="button button-primary">Download Quote Image</button>
<button id="applyDownloadStyleBtn" class="button button-secondary">Apply Download Style</button>
<button id="resetDownloadStyleBtn" class="button button-secondary" style="display: none;">Reset Download Style</button>
</div>
</div>
<script>
// Get the canvas element and its 2D rendering context
const canvas = document.getElementById('quoteCanvas');
const ctx = canvas.getContext('2d');
// Removed canvasWrapper as it's no longer needed for scrolling the canvas itself
// Get buttons
const downloadBtn = document.getElementById('downloadBtn');
const bgImageBtn = document.getElementById('bgImageBtn');
const clearBgImageBtn = document.getElementById('clearBgImageBtn');
const imageUpload = document.getElementById('imageUpload');
const applyDownloadStyleBtn = document.getElementById('applyDownloadStyleBtn');
const resetDownloadStyleBtn = document.getElementById('resetDownloadStyleBtn');
// Get text input fields
const quoteLine1Textarea = document.getElementById('quoteLine1Textarea');
const quoteLine2Textarea = document.getElementById('quoteLine2Textarea');
const authorInput = document.getElementById('authorInput');
// Get text setting controls
const quoteLine1Color = document.getElementById('quoteLine1Color');
const quoteLine1FontSize = document.getElementById('quoteLine1FontSize'); // Get actual element
const quoteLine2Color = document.getElementById('quoteLine2Color');
const quoteLine2FontSizeInput = document.getElementById('quoteLine2FontSize');
const authorTextColor = document.getElementById('authorTextColor');
const authorFontSizeInput = document.getElementById('authorFontSize');
const fontFamilySelector = document.getElementById('fontFamilySelector');
// Position adjustment controls
const titleYAdjust = document.getElementById('titleYAdjust');
const bodyYAdjust = document.getElementById('bodyYAdjust');
const authorYAdjust = document.getElementById('authorYAdjust');
const titleXAdjust = document.getElementById('titleXAdjust');
const bodyXAdjust = document.getElementById('bodyXAdjust');
const authorXAdjust = document.getElementById('authorXAdjust');
// Get background setting controls
const bgColorPicker = document.getElementById('bgColorPicker');
// State variables for drag and drop
let isDragging = false;
let draggedElement = null; // 'title', 'body', or 'author'
let dragStartX = 0; // X position where drag started (client coordinates)
let dragStartY = 0; // Y position where drag started (client coordinates)
let initialElementXAdjust = 0; // Initial X adjustment value of the dragged element
let initialElementYAdjust = 0; // Initial Y adjustment value of the dragged element
// Bounding boxes for hit testing (updated in drawQuote)
let titleBBox = null;
let bodyBBox = null;
let authorBBox = null;
let backgroundImage = null; // To store the background image
let customBackgroundColor = '#ffffff'; // Variable for custom background color, now defaulting to white
let applyDownloadStyle = true; // State variable for controlling both margin and border-radius
/**
* Converts client (mouse/touch) coordinates to canvas-relative coordinates.
* @param {HTMLCanvasElement} canvas The canvas element.
* @param {MouseEvent|TouchEvent} evt The event object.
* @returns {{x: number, y: number}} The coordinates relative to the canvas's top-left corner.
*/
function getCanvasMousePos(canvas, evt) {
const rect = canvas.getBoundingClientRect();
let clientX, clientY;
if (evt.touches) { // Touch event
clientX = evt.touches[0].clientX;
clientY = evt.touches[0].clientY;
} else { // Mouse event
clientX = evt.clientX;
clientY = evt.clientY;
}
// Calculate position relative to the canvas element
const x = clientX - rect.left;
const y = clientY - rect.top;
// Scale coordinates if canvas is scaled by CSS
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
return { x: x * scaleX, y: y * scaleY };
}
/**
* Adjusts the canvas size to fit the screen and redraws the quote.
*/
function resizeCanvas() {
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// Target aspect ratio for the canvas (e.g., 9:16 for portrait mobile)
const targetAspectRatio = 9 / 16;
// Canvas will take up 90% of viewport width, and height will adjust to maintain aspect ratio
let canvasWidth = viewportWidth * 0.9;
let canvasHeight = canvasWidth / targetAspectRatio;
// If calculated height exceeds viewport height (minus header/footer space), adjust based on height
// Approx 150px for header/footer/controls
const availableHeight = viewportHeight - 150;
if (canvasHeight > availableHeight) {
canvasHeight = availableHeight;
canvasWidth = canvasHeight * targetAspectRatio;
}
// Ensure canvas dimensions are at least a minimum size
const minCanvasWidth = 250;
const minCanvasHeight = 400;
if (canvasWidth < minCanvasWidth) {
canvasWidth = minCanvasWidth;
canvasHeight = minCanvasWidth / targetAspectRatio;
}
if (canvasHeight < minCanvasHeight) {
canvasHeight = minCanvasHeight;
canvasWidth = minCanvasHeight * targetAspectRatio;
}
// Set canvas drawing buffer size (internal resolution)
canvas.width = canvasWidth;
canvas.height = canvasHeight;
// Apply CSS to make it fit the container if needed (though direct width/height usually suffice)
canvas.style.width = `${canvasWidth}px`;
canvas.style.height = `${canvasHeight}px`;
drawQuote(); // Redraw the quote after resizing
}
/**
* Calculates the height and max width of a text block without drawing it.
* @param {CanvasRenderingContext2D} context The canvas rendering context.
* @param {string} text The text to measure.
* @param {number} maxWidth The maximum width for text wrapping.
* @param {number} fontSize The font size.
* @param {number} lineHeight The line height.
* @param {string} fontFamily The font family.
* @param {string} fontStyle The font style (e.g., 'normal', 'italic').
* @param {string} fontWeight The font weight (e.g., 'normal', 'bold').
* @returns {{height: number, actualMaxWidth: number}} The total height and the maximum line width of the text block.
*/
function calculateTextBlockMetrics(context, text, maxWidth, fontSize, lineHeight, fontFamily, fontStyle = 'normal', fontWeight = 'bold') {
if (!text) return { height: 0, actualMaxWidth: 0 };
context.font = `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`;
const paragraphs = text.split('\n');
let totalHeight = 0;
let actualMaxWidth = 0;
for (let p = 0; p < paragraphs.length; p++) {
const words = paragraphs[p].split(' ');
let line = '';
let currentLineMetrics = [];
for (let n = 0; n < words.length; n++) {
const testLine = line + words[n] + ' ';
const metrics = context.measureText(testLine);
if (metrics.width > maxWidth && n > 0) {
currentLineMetrics.push({ text: line, width: context.measureText(line).width });
line = words[n] + ' ';
} else {
line = testLine;
}
}
currentLineMetrics.push({ text: line, width: context.measureText(line).width });
for (const lineMet of currentLineMetrics) {
if (lineMet.width > actualMaxWidth) {
actualMaxWidth = lineMet.width;
}
}
totalHeight += currentLineMetrics.length * lineHeight;
}
return { height: totalHeight, actualMaxWidth: actualMaxWidth };
}
/**
* Draws the current quote on the canvas.
*/
function drawQuote() {
// Clear the canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw background image if available
if (backgroundImage) {
const imgAspectRatio = backgroundImage.width / backgroundImage.height;
const canvasAspectRatio = canvas.width / canvas.height;
let sx, sy, sWidth, sHeight;
let dx, dy, dWidth, dHeight;
if (imgAspectRatio > canvasAspectRatio) {
sHeight = backgroundImage.height;
sWidth = sHeight * canvasAspectRatio;
sx = (backgroundImage.width - sWidth) / 2;
sy = 0;
} else {
sWidth = backgroundImage.width;
sHeight = sWidth / canvasAspectRatio;
sx = 0;
sy = (backgroundImage.height - sHeight) / 2;
}
dx = 0;
dy = 0;
dWidth = canvas.width;
dHeight = canvas.height;
ctx.drawImage(backgroundImage, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
} else {
ctx.fillStyle = customBackgroundColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
// Get text from the input fields
const quoteLine1Text = quoteLine1Textarea.value;
const quoteLine2Text = quoteLine2Textarea.value;
const authorText = authorInput.value;
// Get selected text properties from input elements
const currentLine1FontSize = parseFloat(quoteLine1FontSize.value); // Get value directly
const currentLine2FontSize = parseFloat(quoteLine2FontSizeInput.value);
const currentAuthorFontSize = parseFloat(authorFontSizeInput.value);
const line1Color = quoteLine1Color.value;
const line2Color = quoteLine2Color.value;
const authorColor = authorTextColor.value;
const fontFamily = fontFamilySelector.value;
// Get vertical adjustments
const titleY = parseFloat(titleYAdjust.value);
const bodyY = parseFloat(bodyYAdjust.value);
const authorY = parseFloat(authorYAdjust.value);
// Get horizontal adjustments
const titleX = parseFloat(titleXAdjust.value);
const bodyX = parseFloat(bodyXAdjust.value);
const authorX = parseFloat(authorXAdjust.value);
const maxWidth = canvas.width * 0.8; // 80% of canvas width
const centerX = canvas.width / 2;
/**
* Wraps and draws text on the canvas, handling explicit newlines.
* Returns the final Y position after drawing and also the height and max width of the drawn block.
* @param {CanvasRenderingContext2D} context The canvas rendering context.
* @param {string} text The text to draw.
* @param {number} xCenter The x-coordinate for drawing (center aligned).
* @param {number} yStart The initial y-coordinate for drawing.
* @param {number} maxWidth The maximum width for text wrapping.
* @param {number} fontSize The font size.
* @param {number} lineHeight The line height.
* @param {string} color The text color.
* @param {string} fontFamily The font family.
* @param {string} fontStyle The font style (e.g., 'normal', 'italic').
* @param {string} fontWeight The font weight (e.g., 'normal', 'bold').
* @returns {{finalY: number, height: number, actualMaxWidth: number}} Metrics of the drawn text block.
*/
function wrapAndDrawText(context, text, xCenter, yStart, maxWidth, fontSize, lineHeight, color, fontFamily, fontStyle = 'normal', fontWeight = 'bold') {
if (!text) return { finalY: yStart, height: 0, actualMaxWidth: 0 };
const paragraphs = text.split('\n');
let currentY = yStart;
let totalDrawnHeight = 0;
let maxLineWidth = 0;
context.font = `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`;
context.fillStyle = color;
context.textAlign = 'center'; // Keep text aligned to the center of its effective X position
for (let p = 0; p < paragraphs.length; p++) {
const words = paragraphs[p].split(' ');
let line = '';
let linesToDraw = [];
let currentParagraphMaxWidth = 0;
for (let n = 0; n < words.length; n++) {
const testLine = line + words[n] + ' ';
const metrics = context.measureText(testLine);
if (metrics.width > maxWidth && n > 0) {
linesToDraw.push(line);
currentParagraphMaxWidth = Math.max(currentParagraphMaxWidth, context.measureText(line).width);
line = words[n] + ' ';
} else {
line = testLine;
}
}
linesToDraw.push(line);
currentParagraphMaxWidth = Math.max(currentParagraphMaxWidth, context.measureText(line).width);
for (let i = 0; i < linesToDraw.length; i++) {
context.fillText(linesToDraw[i], xCenter, currentY + (i * lineHeight));
}
totalDrawnHeight += (linesToDraw.length * lineHeight);
currentY += (linesToDraw.length * lineHeight);
maxLineWidth = Math.max(maxLineWidth, currentParagraphMaxWidth); // Update overall max width
}
return { finalY: currentY, height: totalDrawnHeight, actualMaxWidth: maxLineWidth };
}
let currentDrawingY = 0;
let totalContentHeight = 0;
// Calculate heights and max widths for initial centering
const titleMetrics = calculateTextBlockMetrics(ctx, quoteLine1Text, maxWidth, currentLine1FontSize, currentLine1FontSize * 1.5, fontFamily, 'normal', 'bold');
const bodyMetrics = calculateTextBlockMetrics(ctx, quoteLine2Text, maxWidth, currentLine2FontSize, currentLine2FontSize * 1.5, fontFamily, 'normal', 'normal');
const authorMetrics = calculateTextBlockMetrics(ctx, `- ${authorText}`, maxWidth, currentAuthorFontSize, currentAuthorFontSize * 1.5, fontFamily, 'italic', 'normal');
totalContentHeight = titleMetrics.height;
if (quoteLine2Text) totalContentHeight += (currentLine1FontSize * 0.5) + bodyMetrics.height;
if (authorText) totalContentHeight += (currentLine1FontSize * 1.5) + authorMetrics.height;
const initialLayoutY = (canvas.height / 2) - (totalContentHeight / 2);
// Draw Title
if (quoteLine1Text) {
const startY = initialLayoutY + titleY;
const drawX = centerX + titleX;
const { height, actualMaxWidth } = wrapAndDrawText(ctx, quoteLine1Text, drawX, startY, maxWidth, currentLine1FontSize, currentLine1FontSize * 1.5, line1Color, fontFamily, 'normal', 'bold');
titleBBox = {
x: drawX - (actualMaxWidth / 2),
y: startY - (currentLine1FontSize * 0.75), // Adjust y to be closer to the top of the text block
width: actualMaxWidth,
height: height + (currentLine1FontSize * 0.75) // Adjust height to cover the text block
};
} else {
titleBBox = null;
}
// Draw Body Text
if (quoteLine2Text) {
// If title is present, start body text after title's bounding box, plus a gap
const prevElementBottom = titleBBox ? titleBBox.y + titleBBox.height : initialLayoutY;
const startY = prevElementBottom + (currentLine1FontSize * 0.5) + bodyY;
const drawX = centerX + bodyX;
const { height, actualMaxWidth } = wrapAndDrawText(ctx, quoteLine2Text, drawX, startY, maxWidth, currentLine2FontSize, currentLine2FontSize * 1.5, line2Color, fontFamily, 'normal', 'normal');
bodyBBox = {
x: drawX - (actualMaxWidth / 2),
y: startY - (currentLine2FontSize * 0.75),
width: actualMaxWidth,
height: height + (currentLine2FontSize * 0.75)
};
} else {
bodyBBox = null;
}
// Draw Author
if (authorText) {
// Start author after body text's bounding box (if present), or title's, plus a gap
const prevElementBottom = bodyBBox ? bodyBBox.y + bodyBBox.height : (titleBBox ? titleBBox.y + titleBBox.height : initialLayoutY);
const startY = prevElementBottom + (currentLine1FontSize * 1.5) + authorY;
const drawX = centerX + authorX;
const { height, actualMaxWidth } = wrapAndDrawText(ctx, `- ${authorText}`, drawX, startY, maxWidth, currentAuthorFontSize, currentAuthorFontSize * 1.5, authorColor, fontFamily, 'italic', 'normal');
authorBBox = {
x: drawX - (actualMaxWidth / 2),
y: startY - (currentAuthorFontSize * 0.75),
width: actualMaxWidth,
height: height + (currentAuthorFontSize * 0.75)
};
} else {
authorBBox = null;
}
}
/**
* Checks if a point (x, y) is within any text element's bounding box.
* @param {number} x The x-coordinate to test (canvas coordinates).
* @param {number} y The y-coordinate to test (canvas coordinates).
* @returns {string|null} The name of the element ('title', 'body', 'author') if hit, otherwise null.
*/
function hitTest(x, y) {
if (titleBBox && x >= titleBBox.x && x <= titleBBox.x + titleBBox.width && y >= titleBBox.y && y <= titleBBox.y + titleBBox.height) {
return 'title';
}
if (bodyBBox && x >= bodyBBox.x && x <= bodyBBox.x + bodyBBox.width && y >= bodyBBox.y && y <= bodyBBox.y + bodyBBox.height) {
return 'body';
}
if (authorBBox && x >= authorBBox.x && x <= authorBBox.x + authorBBox.width && y >= authorBBox.y && y <= authorBBox.y + authorBBox.height) {
return 'author';
}
return null;
}
/**
* Handles mouse down and touch start events for dragging.
* @param {MouseEvent|TouchEvent} e The event object.
*/
function handleDragStart(e) {
e.preventDefault(); // Prevent default touch actions (scrolling, zooming) and text selection
const pos = getCanvasMousePos(canvas, e);
const hitElement = hitTest(pos.x, pos.y);
if (hitElement) {
isDragging = true;
draggedElement = hitElement;
dragStartX = e.touches ? e.touches[0].clientX : e.clientX;
dragStartY = e.touches ? e.touches[0].clientY : e.clientY;
switch (draggedElement) {
case 'title':
initialElementXAdjust = parseFloat(titleXAdjust.value);
initialElementYAdjust = parseFloat(titleYAdjust.value);
break;
case 'body':
initialElementXAdjust = parseFloat(bodyXAdjust.value);
initialElementYAdjust = parseFloat(bodyYAdjust.value);
break;
case 'author':
initialElementXAdjust = parseFloat(authorXAdjust.value);
initialElementYAdjust = parseFloat(authorYAdjust.value);
break;
}
canvas.classList.add('dragging'); // Add dragging cursor
}
}
/**
* Handles mouse move and touch move events for dragging.
* @param {MouseEvent|TouchEvent} e The event object.
*/
function handleDragMove(e) {
if (!isDragging) return;
e.preventDefault(); // Prevent default touch actions (scrolling, zooming)
const currentX = e.touches ? e.touches[0].clientX : e.clientX;
const currentY = e.touches ? e.touches[0].clientY : e.clientY;
const dx = currentX - dragStartX;
const dy = currentY - dragStartY;
switch (draggedElement) {
case 'title':
titleXAdjust.value = initialElementXAdjust + dx;
titleYAdjust.value = initialElementYAdjust + dy;
break;
case 'body':
bodyXAdjust.value = initialElementXAdjust + dx;
bodyYAdjust.value = initialElementYAdjust + dy;
break;
case 'author':
authorXAdjust.value = initialElementXAdjust + dx;
authorYAdjust.value = initialElementYAdjust + dy;
break;
}
drawQuote(); // Redraw with new position
}
/**
* Handles mouse up and touch end events to stop dragging.
* @param {MouseEvent|TouchEvent} e The event object.
*/
function handleDragEnd(e) {
isDragging = false;
draggedElement = null;
canvas.classList.remove('dragging'); // Remove dragging cursor
}
/**
* Downloads the canvas content as a PNG image with border-radius and margin.
*/
function downloadQuote() {
// Create a temporary canvas that matches the full resolution of the drawing canvas
const tempCanvas = document.createElement('canvas');
tempCanvas.width = canvas.width;
tempCanvas.height = canvas.height;
const tempCtx = tempCanvas.getContext('2d');
// Draw the current state of the main canvas onto the temporary canvas
tempCtx.drawImage(canvas, 0, 0);
// Now, apply the download style (margin and border-radius) if requested
const finalCanvas = document.createElement('canvas');
const originalWidth = tempCanvas.width;
const originalHeight = tempCanvas.height;
const marginPx = applyDownloadStyle ? Math.round(originalWidth * 0.02) : 0; // 2% of original width
const borderRadiusPx = applyDownloadStyle ? 20 : 0;
finalCanvas.width = originalWidth + (2 * marginPx);
finalCanvas.height = originalHeight + (2 * marginPx);
const finalCtx = finalCanvas.getContext('2d');
// Fill the margin area with the body background color
finalCtx.fillStyle = '#f0f2f5'; // Using the body background color
finalCtx.fillRect(0, 0, finalCanvas.width, finalCanvas.height);
if (applyDownloadStyle) {
// Draw the temporary canvas content with border-radius
finalCtx.save(); // Save the context state
// Create a rounded rectangle path for clipping
finalCtx.beginPath();
finalCtx.moveTo(marginPx + borderRadiusPx, marginPx);
finalCtx.lineTo(originalWidth + marginPx - borderRadiusPx, marginPx);
finalCtx.arcTo(originalWidth + marginPx, marginPx, originalWidth + marginPx, marginPx + borderRadiusPx, borderRadiusPx);
finalCtx.lineTo(originalWidth + marginPx, originalHeight + marginPx - borderRadiusPx);
finalCtx.arcTo(originalWidth + marginPx, originalHeight + marginPx, originalWidth + marginPx - borderRadiusPx, originalHeight + marginPx, borderRadiusPx);
finalCtx.lineTo(marginPx + borderRadiusPx, originalHeight + marginPx);
finalCtx.arcTo(marginPx, originalHeight + marginPx, marginPx, originalHeight + marginPx - borderRadiusPx, borderRadiusPx);
finalCtx.lineTo(marginPx, marginPx + borderRadiusPx);
finalCtx.arcTo(marginPx, marginPx, marginPx + borderRadiusPx, marginPx, borderRadiusPx);
finalCtx.closePath();
finalCtx.clip(); // Clip subsequent drawing to this path
}
// Draw the content from the temporary canvas onto the final canvas, offset by the margin
finalCtx.drawImage(tempCanvas, marginPx, marginPx, originalWidth, originalHeight);
if (applyDownloadStyle) {
finalCtx.restore(); // Restore the context state (remove clipping path)
}
// Convert the final canvas to an image and trigger download
const image = finalCanvas.toDataURL('image/png');
const link = document.createElement('a');
link.download = 'quote_canvas_with_style.png';
document.body.appendChild(link); // Append to body to make click work reliably
link.href = image;
link.click();
document.body.removeChild(link);
}
/**
* Handles the selection of a background image.
*/
function handleImageUpload(event) {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(e) {
const img = new Image();
img.onload = function() {
backgroundImage = img;
customBackgroundColor = '#ffffff'; // Clear custom background color
bgColorPicker.value = '#ffffff'; // Reset picker
drawQuote();
};
img.onerror = function() {
console.error("Error loading image.");
backgroundImage = null;
drawQuote();
};
img.src = e.target.result;
};
reader.onerror = function() {
console.error("Error reading file.");
backgroundImage = null;
drawQuote();
};
reader.readAsDataURL(file);
}
}
/**
* Clears the background image and resets background color to default.
*/
function clearBackgroundImage() {
backgroundImage = null;
customBackgroundColor = '#ffffff';
bgColorPicker.value = '#ffffff';
drawQuote();
}
// Event Listeners
window.addEventListener('resize', resizeCanvas); // Re-added resize listener
downloadBtn.addEventListener('click', downloadQuote);
bgImageBtn.addEventListener('click', () => imageUpload.click());
imageUpload.addEventListener('change', handleImageUpload);
clearBgImageBtn.addEventListener('click', clearBackgroundImage);
// Text input field listeners (real-time update)
quoteLine1Textarea.addEventListener('input', drawQuote);
quoteLine2Textarea.addEventListener('input', drawQuote);
authorInput.addEventListener('input', drawQuote);
// Text settings control listeners
quoteLine1Color.addEventListener('input', drawQuote);
quoteLine1FontSize.addEventListener('input', drawQuote); // Corrected to use the element directly
quoteLine2Color.addEventListener('input', drawQuote);
quoteLine2FontSizeInput.addEventListener('input', drawQuote);
authorTextColor.addEventListener('input', drawQuote);
authorFontSizeInput.addEventListener('input', drawQuote);
fontFamilySelector.addEventListener('change', drawQuote); // Use 'change' for select
// Position adjustment control listeners (now also triggered by drag)
titleYAdjust.addEventListener('input', drawQuote);
bodyYAdjust.addEventListener('input', drawQuote);
authorYAdjust.addEventListener('input', drawQuote);
titleXAdjust.addEventListener('input', drawQuote);
bodyXAdjust.addEventListener('input', drawQuote);
authorXAdjust.addEventListener('input', drawQuote);
// Background color picker listener
bgColorPicker.addEventListener('input', () => {
customBackgroundColor = bgColorPicker.value;
backgroundImage = null; // Clear background image if a color is selected
drawQuote();
});
// New button event listeners for download style
applyDownloadStyleBtn.addEventListener('click', () => {
applyDownloadStyle = true;
applyDownloadStyleBtn.style.display = 'none';
resetDownloadStyleBtn.style.display = 'inline-block';
});
resetDownloadStyleBtn.addEventListener('click', () => {
applyDownloadStyle = false;
resetDownloadStyleBtn.style.display = 'none';
applyDownloadStyleBtn.style.display = 'inline-block';
});
// Canvas drag and drop event listeners
canvas.addEventListener('mousedown', handleDragStart);
canvas.addEventListener('mousemove', handleDragMove);
canvas.addEventListener('mouseup', handleDragEnd);
canvas.addEventListener('mouseleave', handleDragEnd); // End drag if mouse leaves canvas
// Touch events for mobile
canvas.addEventListener('touchstart', handleDragStart);
canvas.addEventListener('touchmove', handleDragMove);
canvas.addEventListener('touchend', handleDragEnd);
canvas.addEventListener('touchcancel', handleDragEnd);
// Initial setup
window.onload = function() {
resizeCanvas(); // Set initial canvas size and draw quote
};
</script>
</body>
</html>