diff --git a/src/components/HarmonicaTabGenerator.jsx b/src/components/HarmonicaTabGenerator.jsx index b1d57f2..d482629 100644 --- a/src/components/HarmonicaTabGenerator.jsx +++ b/src/components/HarmonicaTabGenerator.jsx @@ -1,130 +1,177 @@ import React, { useState, useRef } from "react"; -import "../App.css"; +import "../styles.css"; + +// ============================================================================ +// CONSTANTS +// ============================================================================ const DEMO_TABS = ` 4 6 -5 5 -4 4 3 6 7 -6 6 -6' 6 5 4 4 4 5 5' 5 5 5 -5 5 4 - `; +`; -const HarmonicaTabGenerator = () => { - const [input, setInput] = useState(DEMO_TABS); - const [title, setTitle] = useState("My Harmonica Tab"); - const [maxLinesPerColumn, setMaxLinesPerColumn] = useState(10); - const svgRef = useRef(null); +const SVG_CONFIG = { + lineHeight: 40, + fontSize: 28, + annotationFontSize: 12, + titleFontSize: 36, + titleHeight: 80, + padding: 40, + columnGap: 80, + charWidth: 17.3, // For tab lines (monospace) + textCharWidth: 7, // For annotation lines + minColumnWidth: 200, + canvasScale: 2, // For PNG export quality + backgroundColor: "#fffef5", +}; - // Parse input and add + signs to positive numbers - const parseInput = (text) => { - const lines = text.split("\n"); - return lines.map((line) => { - // Check if line contains tab notation (numbers with optional minus signs, quotes/backticks for bends, and spaces) - const isTabLine = - /^[\s\d\-'"`'']+$/.test(line.trim()) && line.trim().length > 0; +// ============================================================================ +// UTILITY FUNCTIONS +// ============================================================================ - let processedContent = line; +/** + * Parse input text and identify tab lines vs annotation lines + * Adds + signs to positive numbers in tab lines + */ +const parseInput = (text) => { + const lines = text.split("\n"); - // If it's a tab line, add + signs to positive numbers (but preserve bend notation) - if (isTabLine) { - processedContent = line.replace( - /(\s|^)(\d+)(['"`'']?)/g, - (match, space, number, bend) => { - return space + "+" + number + bend; - }, - ); - } + return lines.map((line) => { + // Tab lines contain only numbers, spaces, minus signs, and bend markers + const isTabLine = + /^[\s\d\-'"`'']+$/.test(line.trim()) && line.trim().length > 0; - return { - content: processedContent, - isTab: isTabLine, - }; - }); - }; + let processedContent = line; - // Calculate layout (columns based on max lines per column) - const calculateLayout = (parsedLines) => { - const tabLines = parsedLines.filter((l) => l.isTab); - const numColumns = Math.ceil(tabLines.length / maxLinesPerColumn); - - const columns = []; - let currentIndex = 0; - - for (let col = 0; col < numColumns; col++) { - const columnLines = []; - let tabCount = 0; - - while ( - currentIndex < parsedLines.length && - tabCount < maxLinesPerColumn - ) { - const line = parsedLines[currentIndex]; - columnLines.push(line); - - if (line.isTab) { - tabCount++; - } - currentIndex++; - } - - columns.push(columnLines); + // Add + signs to positive numbers in tab lines + if (isTabLine) { + processedContent = line.replace( + /(\s|^)(\d+)(['"`'']?)/g, + (match, space, number, bend) => space + "+" + number + bend, + ); } - return columns; - }; + return { + content: processedContent, + isTab: isTabLine, + }; + }); +}; - const parsedLines = parseInput(input); - const columns = calculateLayout(parsedLines); +/** + * Calculate column layout based on max lines per column + */ +const calculateLayout = (parsedLines, maxLinesPerColumn) => { + const tabLines = parsedLines.filter((l) => l.isTab); + const numColumns = Math.ceil(tabLines.length / maxLinesPerColumn); - // Calculate the width of each column based on content - const calculateColumnWidth = (column) => { - let maxLength = 0; - column.forEach((line) => { - if (line.content.trim()) { - // Adjusted for letterSpacing of 0.5 instead of 2 - // Monospace at 28px is about 16.8px per char + 0.5px spacing - const charWidth = line.isTab ? 17.3 : 7; - const length = line.content.length * charWidth; - maxLength = Math.max(maxLength, length); + const columns = []; + let currentIndex = 0; + + for (let col = 0; col < numColumns; col++) { + const columnLines = []; + let tabCount = 0; + + while (currentIndex < parsedLines.length && tabCount < maxLinesPerColumn) { + const line = parsedLines[currentIndex]; + columnLines.push(line); + + if (line.isTab) { + tabCount++; } - }); - return Math.max(maxLength, 200); // Minimum 200px per column - }; + currentIndex++; + } - const columnWidths = columns.map(calculateColumnWidth); + columns.push(columnLines); + } + + return columns; +}; + +/** + * Calculate the width needed for a column based on its content + */ +const calculateColumnWidth = (column) => { + let maxLength = 0; + + column.forEach((line) => { + if (line.content.trim()) { + const charWidth = line.isTab + ? SVG_CONFIG.charWidth + : SVG_CONFIG.textCharWidth; + const length = line.content.length * charWidth; + maxLength = Math.max(maxLength, length); + } + }); + + return Math.max(maxLength, SVG_CONFIG.minColumnWidth); +}; + +/** + * Calculate SVG dimensions based on content + */ +const calculateSVGDimensions = (columns, columnWidths) => { const totalContentWidth = columnWidths.reduce((sum, width) => sum + width, 0); - // SVG dimensions and styling - const lineHeight = 40; - const fontSize = 28; - const annotationFontSize = 12; - const titleFontSize = 36; - const titleHeight = 80; - const padding = 40; - const columnGap = 80; - const maxLinesInAnyColumn = Math.max( ...columns.map((col) => { - return col.reduce((sum, line) => { - return sum + (line.isTab ? 1 : 0.5); - }, 0); + return col.reduce((sum, line) => sum + (line.isTab ? 1 : 0.5), 0); }), 1, ); const svgWidth = - totalContentWidth + (columns.length - 1) * columnGap + padding * 2; - const svgHeight = - maxLinesInAnyColumn * lineHeight + padding * 2 + titleHeight; + totalContentWidth + + (columns.length - 1) * SVG_CONFIG.columnGap + + SVG_CONFIG.padding * 2; - // Export as PNG + const svgHeight = + maxLinesInAnyColumn * SVG_CONFIG.lineHeight + + SVG_CONFIG.padding * 2 + + SVG_CONFIG.titleHeight; + + return { svgWidth, svgHeight }; +}; + +// ============================================================================ +// MAIN COMPONENT +// ============================================================================ + +const HarmonicaTabGenerator = () => { + // State + const [input, setInput] = useState(DEMO_TABS); + const [title, setTitle] = useState("My Harmonica Tab"); + const [maxLinesPerColumn, setMaxLinesPerColumn] = useState(10); + + // Ref for SVG element + const svgRef = useRef(null); + + // ============================================================================ + // COMPUTED VALUES + // ============================================================================ + + const parsedLines = parseInput(input); + const columns = calculateLayout(parsedLines, maxLinesPerColumn); + const columnWidths = columns.map(calculateColumnWidth); + const { svgWidth, svgHeight } = calculateSVGDimensions(columns, columnWidths); + + // ============================================================================ + // EVENT HANDLERS + // ============================================================================ + + /** + * Export the SVG as a PNG image + */ const exportAsPNG = () => { const svgElement = svgRef.current; const svgData = new XMLSerializer().serializeToString(svgElement); const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); - // Set canvas size (2x for better quality) - canvas.width = svgWidth * 2; - canvas.height = svgHeight * 2; + // Set canvas size with 2x scaling for better quality + canvas.width = svgWidth * SVG_CONFIG.canvasScale; + canvas.height = svgHeight * SVG_CONFIG.canvasScale; const img = new Image(); const svgBlob = new Blob([svgData], { @@ -133,7 +180,7 @@ const HarmonicaTabGenerator = () => { const url = URL.createObjectURL(svgBlob); img.onload = () => { - ctx.scale(2, 2); + ctx.scale(SVG_CONFIG.canvasScale, SVG_CONFIG.canvasScale); ctx.drawImage(img, 0, 0); URL.revokeObjectURL(url); @@ -148,7 +195,9 @@ const HarmonicaTabGenerator = () => { img.src = url; }; - // Export as SVG + /** + * Export the SVG as an SVG file + */ const exportAsSVG = () => { const svgElement = svgRef.current; const svgData = new XMLSerializer().serializeToString(svgElement); @@ -159,226 +208,283 @@ const HarmonicaTabGenerator = () => { link.click(); }; + // ============================================================================ + // RENDER + // ============================================================================ + return (
+ {/* Header */}

🎵 Harmonica Tabs Image Generator

Create beautiful, readable images of your harmonica tabs

- {/* Instructions Section - MOVED TO TOP */} -
-

📋 How to Use

-
    -
  • - 1 -
    - Enter your tabs - Type your harmonica tab - numbers with spaces -
    -
  • -
  • - 2 -
    - Auto-formatting - Positive numbers get a + sign - automatically. Bends: ' " ` ' (e.g., -3', 4") -
    -
  • -
  • - 3 -
    - Add annotations - Plain text appears smaller - and italicized -
    -
  • -
  • - 4 -
    - Customize layout - Set your title and rows per - column -
    -
  • -
  • - 5 -
    - Download - Save as PNG or SVG when ready -
    -
  • -
-
+ {/* Instructions */} + - {/* Settings Section */} -
-

⚙️ Settings

-
-
- - setTitle(e.target.value)} - className="input-field" - placeholder="Enter a title for your tabs..." - /> -
-
- -
- - setMaxLinesPerColumn(parseInt(e.target.value)) - } - className="slider" - /> - {maxLinesPerColumn} -
-
-
-
+ {/* Settings */} + - {/* Input Section */} -
-

✏️ Input Your Tabs

-