UNPKG

typography-canvas-renderer

Version:

A lightweight npm package for rendering typographic content (text and images) on HTML5 Canvas with full CSS styling support including borders, border-radius, multiple border styles, inline text rendering, auto height calculation, and image support

1,096 lines (1,043 loc) 70.8 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Typography Canvas Renderer - Standalone Example</title> <style> body { font-family: Arial, sans-serif; margin: 20px; background-color: #f5f5f5; } .container { max-width: 1200px; margin: 0 auto; } .example { background: white; padding: 20px; margin: 20px 0; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .canvas-container { text-align: center; margin: 20px 0; } canvas { border: 1px solid #ddd; max-width: 100%; height: auto; } button { background: #007bff; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; margin: 5px; } button:hover { background: #0056b3; } .download-link { display: inline-block; background: #28a745; color: white; text-decoration: none; padding: 10px 20px; border-radius: 4px; margin: 5px; } .download-link:hover { background: #1e7e34; } .input-section { background: white; padding: 20px; margin: 20px 0; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .preset-buttons { margin: 10px 0; } .preset-btn { background: #6c757d; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; margin: 5px; font-size: 14px; } .preset-btn:hover { background: #5a6268; } .preset-btn.active { background: #007bff; } textarea { width: 100%; height: 300px; font-family: 'Courier New', monospace; font-size: 12px; padding: 10px; border: 1px solid #ddd; border-radius: 4px; resize: vertical; } .form-group { margin: 15px 0; } label { display: block; margin-bottom: 5px; font-weight: bold; } .button-group { margin: 15px 0; } .error { color: #dc3545; background: #f8d7da; border: 1px solid #f5c6cb; padding: 10px; border-radius: 4px; margin: 10px 0; } .success { color: #155724; background: #d4edda; border: 1px solid #c3e6cb; padding: 10px; border-radius: 4px; margin: 10px 0; } </style> </head> <body> <div class="container"> <h1>Typography Canvas Renderer - Border Radius Test</h1> <p>This example demonstrates the fixed border radius functionality for both text and images.</p> <!-- Custom Input Section --> <div class="input-section"> <h2>Custom Canvas Configuration</h2> <p>Select a predefined example or create your own configuration:</p> <div class="form-group"> <label>Predefined Examples:</label> <div class="preset-buttons"> <button class="preset-btn" onclick="loadPreset('basic')">Basic Text</button> <button class="preset-btn" onclick="loadPreset('styled')">Styled Text</button> <button class="preset-btn" onclick="loadPreset('mixed')">Mixed Content</button> <button class="preset-btn" onclick="loadPreset('borders')">Border Styles</button> <button class="preset-btn" onclick="loadPreset('images')">Image Gallery</button> <button class="preset-btn" onclick="loadPreset('minimal')">Minimal</button> <button class="preset-btn" onclick="loadPreset('inline')">Inline Text</button> <button class="preset-btn" onclick="loadPreset('multiline')">Multi-line</button> <button class="preset-btn" onclick="loadPreset('textwrapping')">Text Wrapping</button> <button class="preset-btn" onclick="loadPreset('paddingalignment')">Padding & Alignment</button> <button class="preset-btn" onclick="loadPreset('opacity')">Opacity</button> </div> </div> <div class="form-group"> <label for="jsonInput">JSON Configuration:</label> <textarea id="jsonInput" placeholder="Enter your JSON configuration here..."></textarea> </div> <div class="button-group"> <button onclick="renderCustom()">Render Custom Canvas</button> <button onclick="validateJSON()">Validate JSON</button> <button onclick="clearInput()">Clear Input</button> </div> <div id="message"></div> <div class="canvas-container"> <canvas id="customCanvas" width="800" height="400"></canvas> </div> <div class="button-group"> <a id="customDownload" class="download-link" style="display: none;" download="custom-canvas.png">Download PNG</a> <button onclick="copyToClipboard()">Copy JSON to Clipboard</button> </div> </div> <div class="example"> <h2>Text with Border Radius</h2> <p>Text elements with rounded backgrounds and borders.</p> <div class="canvas-container"> <canvas id="textCanvas" width="800" height="300"></canvas> </div> <button onclick="renderTextExample()">Render Text Example</button> <a id="textDownload" class="download-link" style="display: none;" download="text-border-radius.png">Download PNG</a> </div> <div class="example"> <h2>Images with Border Radius</h2> <p>Image elements with rounded corners and borders.</p> <div class="canvas-container"> <canvas id="imageCanvas" width="800" height="300"></canvas> </div> <button onclick="renderImageExample()">Render Image Example</button> <a id="imageDownload" class="download-link" style="display: none;" download="image-border-radius.png">Download PNG</a> </div> <div class="example"> <h2>Mixed Content with Border Radius</h2> <p>Combination of text and images with various border radius values.</p> <div class="canvas-container"> <canvas id="mixedCanvas" width="800" height="400"></canvas> </div> <button onclick="renderMixedExample()">Render Mixed Example</button> <a id="mixedDownload" class="download-link" style="display: none;" download="mixed-border-radius.png">Download PNG</a> </div> </div> <script> // Predefined data sets const presets = { basic: { canvas: { width: 800, height: 400, background: '#f0f8ff' }, texts: [ { text: 'Hello World!', css: { 'font-size': '48px', 'font-family': 'Arial, sans-serif', 'color': '#2c3e50', 'left': '50px', 'top': '50px', 'z-index': '1' } } ], images: [] }, styled: { canvas: { width: 800, height: 400, background: '#ffffff' }, texts: [ { text: 'Styled Text Box', css: { 'font-size': '36px', 'color': '#ffffff', 'left': '100px', 'top': '100px', 'width': '600px', 'height': '80px', 'background-color': '#3498db', 'text-align': 'center', 'border': '3px solid #2980b9', 'border-radius': '15px', 'z-index': '1' } }, { text: 'With Rounded Corners', css: { 'font-size': '20px', 'color': '#34495e', 'left': '150px', 'top': '220px', 'width': '500px', 'height': '50px', 'background-color': '#ecf0f1', 'text-align': 'center', 'border': '2px dashed #bdc3c7', 'border-radius': '25px', 'z-index': '2' } } ], images: [] }, mixed: { canvas: { width: 800, height: 400, background: '#f8f9fa' }, texts: [ { text: 'Background Layer', css: { 'font-size': '32px', 'color': '#e9ecef', 'left': '50px', 'top': '50px', 'z-index': '1' } }, { text: 'Foreground Text', css: { 'font-size': '28px', 'color': '#212529', 'left': '100px', 'top': '100px', 'z-index': '3' } } ], images: [ { src: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iIzM0OTVkYiIvPgogIDx0ZXh0IHg9IjEwMCIgeT0iNTUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIyMCIgZmlsbD0id2hpdGUiIHRleHQtYW5jaG9yPSJtaWRkbGUiPkltYWdlPC90ZXh0Pgo8L3N2Zz4K', css: { 'width': '200px', 'height': '100px', 'left': '300px', 'top': '150px', 'border': '2px solid #17a2b8', 'border-radius': '20px', 'z-index': '2' } } ] }, borders: { canvas: { width: 800, height: 600, background: '#f8f9fa' }, texts: [ { text: 'Solid Border', css: { 'font-size': '24px', 'color': '#ffffff', 'left': '50px', 'top': '50px', 'width': '200px', 'height': '60px', 'background-color': '#e74c3c', 'text-align': 'center', 'border': '3px solid #c0392b', 'border-radius': '10px', 'z-index': '1' } }, { text: 'Dashed Border', css: { 'font-size': '20px', 'color': '#2c3e50', 'left': '300px', 'top': '50px', 'width': '200px', 'height': '60px', 'background-color': '#ecf0f1', 'text-align': 'center', 'border': '2px dashed #7f8c8d', 'border-radius': '15px', 'z-index': '2' } }, { text: 'Dotted Border', css: { 'font-size': '18px', 'color': '#ffffff', 'left': '550px', 'top': '50px', 'width': '200px', 'height': '60px', 'background-color': '#9b59b6', 'text-align': 'center', 'border': '2px dotted #8e44ad', 'border-radius': '20px', 'z-index': '3' } }, { text: 'Double Border', css: { 'font-size': '22px', 'color': '#2c3e50', 'left': '50px', 'top': '150px', 'width': '200px', 'height': '60px', 'background-color': '#f39c12', 'text-align': 'center', 'border': '4px double #e67e22', 'border-radius': '5px', 'z-index': '4' } }, { text: 'Rounded Corners', css: { 'font-size': '20px', 'color': '#ffffff', 'left': '300px', 'top': '150px', 'width': '200px', 'height': '60px', 'background-color': '#27ae60', 'text-align': 'center', 'border': '2px solid #229954', 'border-radius': '25px', 'z-index': '5' } }, { text: 'No Border', css: { 'font-size': '18px', 'color': '#2c3e50', 'left': '550px', 'top': '150px', 'width': '200px', 'height': '60px', 'background-color': '#bdc3c7', 'text-align': 'center', 'border-radius': '8px', 'z-index': '6' } } ], images: [] }, images: { canvas: { width: 800, height: 400, background: '#f8f9fa' }, texts: [], images: [ { src: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTUwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8cmVjdCB3aWR0aD0iMTUwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iIzM0OTVkYiIvPgogIDx0ZXh0IHg9Ijc1IiB5PSI1NSIgZm9udC1mYW1pbHk9IkFyaWFsIiBmb250LXNpemU9IjE2IiBmaWxsPSJ3aGl0ZSIgdGV4dC1hbmNob3I9Im1pZGRsZSI+SW1hZ2U8L3RleHQ+Cjwvc3ZnPgo=', css: { 'width': '150px', 'height': '100px', 'left': '50px', 'top': '50px', 'border': '3px solid #e74c3c', 'border-radius': '15px', 'z-index': '1' } }, { src: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTUwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8cmVjdCB3aWR0aD0iMTUwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iIzI3YWU2MCIvPgogIDx0ZXh0IHg9Ijc1IiB5PSI1NSIgZm9udC1mYW1pbHk9IkFyaWFsIiBmb250LXNpemU9IjE2IiBmaWxsPSJ3aGl0ZSIgdGV4dC1hbmNob3I9Im1pZGRsZSI+SW1hZ2U8L3RleHQ+Cjwvc3ZnPgo=', css: { 'width': '150px', 'height': '100px', 'left': '250px', 'top': '50px', 'border': '2px dashed #f39c12', 'border-radius': '20px', 'z-index': '2' } }, { src: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTUwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8cmVjdCB3aWR0aD0iMTUwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iIzliNTliNiIvPgogIDx0ZXh0IHg9Ijc1IiB5PSI1NSIgZm9udC1mYW1pbHk9IkFyaWFsIiBmb250LXNpemU9IjE2IiBmaWxsPSJ3aGl0ZSIgdGV4dC1hbmNob3I9Im1pZGRsZSI+SW1hZ2U8L3RleHQ+Cjwvc3ZnPgo=', css: { 'width': '150px', 'height': '100px', 'left': '450px', 'top': '50px', 'border': '2px dotted #8e44ad', 'border-radius': '25px', 'z-index': '3' } }, { src: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTUwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8cmVjdCB3aWR0aD0iMTUwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iI2VjZjBmMSIvPgogIDx0ZXh0IHg9Ijc1IiB5PSI1NSIgZm9udC1mYW1pbHk9IkFyaWFsIiBmb250LXNpemU9IjE2IiBmaWxsPSIjMmM0ZTUwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5JbWFnZTwvdGV4dD4KPC9zdmc+Cg==', css: { 'width': '150px', 'height': '100px', 'left': '650px', 'top': '50px', 'border': '4px double #34495e', 'border-radius': '10px', 'z-index': '4' } }, { src: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTUwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8cmVjdCB3aWR0aD0iMTUwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iI2YzOWMxMiIvPgogIDx0ZXh0IHg9Ijc1IiB5PSI1NSIgZm9udC1mYW1pbHk9IkFyaWFsIiBmb250LXNpemU9IjE2IiBmaWxsPSJ3aGl0ZSIgdGV4dC1hbmNob3I9Im1pZGRsZSI+SW1hZ2U8L3RleHQ+Cjwvc3ZnPgo=', css: { 'width': '150px', 'height': '100px', 'left': '50px', 'top': '200px', 'border': '2px solid #e67e22', 'border-radius': '30px', 'z-index': '5' } }, { src: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTUwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8cmVjdCB3aWR0aD0iMTUwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iI2JkYzNjNyIvPgogIDx0ZXh0IHg9Ijc1IiB5PSI1NSIgZm9udC1mYW1pbHk9IkFyaWFsIiBmb250LXNpemU9IjE2IiBmaWxsPSIjMmM0ZTUwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5JbWFnZTwvdGV4dD4KPC9zdmc+Cg==', css: { 'width': '150px', 'height': '100px', 'left': '250px', 'top': '200px', 'border-radius': '12px', 'z-index': '6' } } ] }, minimal: { canvas: { width: 400, height: 300, background: '#ffffff' }, texts: [ { text: 'Minimal', css: { 'font-size': '24px', 'color': '#333333', 'left': '50px', 'top': '50px', 'z-index': '1' } } ], images: [] }, inline: { canvas: { width: 600, height: 400, background: '#f8f9fa' }, texts: [ { text: 'Inline Text', css: { 'font-size': '24px', 'color': '#2c3e50', 'left': '50px', 'top': '50px', 'z-index': '1' } }, { text: 'With Background', css: { 'font-size': '20px', 'color': '#ffffff', 'left': '200px', 'top': '50px', 'background-color': '#3498db', 'border-radius': '5px', 'z-index': '2' } }, { text: 'And Border', css: { 'font-size': '18px', 'color': '#2c3e50', 'left': '350px', 'top': '50px', 'background-color': '#ecf0f1', 'border': '2px solid #bdc3c7', 'border-radius': '8px', 'z-index': '3' } }, { text: 'No Width Set - Renders Inline', css: { 'font-size': '16px', 'color': '#e74c3c', 'left': '50px', 'top': '100px', 'z-index': '4' } }, { text: 'Block Text with Width', css: { 'font-size': '16px', 'color': '#27ae60', 'left': '50px', 'top': '150px', 'width': '300px', 'height': '60px', 'background-color': '#d5f4e6', 'border': '2px solid #27ae60', 'border-radius': '10px', 'text-align': 'center', 'z-index': '5' } } ], images: [] }, multiline: { canvas: { width: 600, height: 400, background: '#f8f9fa' }, texts: [ { text: 'Multi-line Text\nSupport\nwith Borders', css: { 'font-size': '28px', 'font-family': 'Arial, sans-serif', 'color': '#2c3e50', 'left': '50px', 'top': '50px', 'width': '500px', 'height': '120px', 'background-color': '#e8f4f8', 'text-align': 'center', 'border': '3px solid #3498db', 'border-radius': '15px', 'line-height': '40px', 'z-index': '1' } }, { text: 'Left aligned\nmulti-line text\nwith dashed border', css: { 'font-size': '20px', 'color': '#34495e', 'left': '50px', 'top': '220px', 'width': '500px', 'height': '100px', 'background-color': '#ffffff', 'text-align': 'left', 'border': '2px dashed #7f8c8d', 'border-radius': '10px', 'line-height': '30px', 'z-index': '2' } } ], images: [] }, textwrapping: { canvas: { width: 700, height: 500, background: '#f8f9fa' }, texts: [ { text: 'This is a very long text that will automatically wrap to fit within the specified width of the border. The text wrapping feature ensures that long sentences are broken into multiple lines to maintain readability and proper formatting.', css: { 'font-size': '18px', 'font-family': 'Arial, sans-serif', 'color': '#2c3e50', 'left': '50px', 'top': '50px', 'width': '300px', 'height': '150px', 'background-color': '#e8f4f8', 'text-align': 'left', 'border': '2px solid #3498db', 'border-radius': '10px', 'line-height': '24px', 'z-index': '1' } }, { text: 'Center aligned text with automatic wrapping. This demonstrates how text wrapping works with different alignments and border styles.', css: { 'font-size': '16px', 'color': '#ffffff', 'left': '400px', 'top': '50px', 'width': '250px', 'height': '120px', 'background-color': '#e74c3c', 'text-align': 'center', 'border': '3px solid #c0392b', 'border-radius': '15px', 'line-height': '22px', 'z-index': '2' } }, { text: 'Right aligned wrapped text with dashed border. This shows how text wrapping works with right alignment and different border styles.', css: { 'font-size': '14px', 'color': '#34495e', 'left': '50px', 'top': '250px', 'width': '400px', 'height': '100px', 'background-color': '#ffffff', 'text-align': 'right', 'border': '2px dashed #7f8c8d', 'border-radius': '8px', 'line-height': '20px', 'z-index': '3' } } ], images: [] }, paddingalignment: { canvas: { width: 800, height: 600, background: '#f8f9fa' }, texts: [ { text: 'Top Aligned\nwith Custom Padding\n20px padding', css: { 'font-size': '20px', 'font-family': 'Arial, sans-serif', 'color': '#2c3e50', 'left': '50px', 'top': '50px', 'width': '200px', 'height': '120px', 'background-color': '#e8f4f8', 'text-align': 'center', 'vertical-align': 'top', 'padding': '20px', 'border': '3px solid #3498db', 'border-radius': '10px', 'line-height': '28px', 'z-index': '1' } }, { text: 'Middle Aligned\nwith Custom Padding\n15px padding', css: { 'font-size': '18px', 'color': '#ffffff', 'left': '300px', 'top': '50px', 'width': '200px', 'height': '120px', 'background-color': '#e74c3c', 'text-align': 'center', 'vertical-align': 'middle', 'padding': '15px', 'border': '2px solid #c0392b', 'border-radius': '12px', 'line-height': '26px', 'z-index': '2' } }, { text: 'Bottom Aligned\nwith Custom Padding\n10px padding', css: { 'font-size': '16px', 'color': '#34495e', 'left': '550px', 'top': '50px', 'width': '200px', 'height': '120px', 'background-color': '#ffffff', 'text-align': 'center', 'vertical-align': 'bottom', 'padding': '10px', 'border': '2px dashed #7f8c8d', 'border-radius': '8px', 'line-height': '24px', 'z-index': '3' } }, { text: 'Default Padding\nNo Custom Padding\nUses border width', css: { 'font-size': '16px', 'color': '#2c3e50', 'left': '50px', 'top': '250px', 'width': '200px', 'height': '100px', 'background-color': '#d5f4e6', 'text-align': 'center', 'vertical-align': 'middle', 'border': '5px solid #27ae60', 'border-radius': '15px', 'line-height': '22px', 'z-index': '4' } }, { text: 'Minimal Padding\n2px custom padding\nVery tight spacing', css: { 'font-size': '14px', 'color': '#ffffff', 'left': '300px', 'top': '250px', 'width': '200px', 'height': '100px', 'background-color': '#8e44ad', 'text-align': 'center', 'vertical-align': 'middle', 'padding': '2px', 'border': '2px solid #663399', 'border-radius': '6px', 'line-height': '20px', 'z-index': '5' } } ], images: [] }, opacity: { canvas: { width: 600, height: 400, background: '#f0f0f0' }, texts: [ { text: 'Full Opacity\n100% visible', css: { 'font-size': '24px', 'color': '#2c3e50', 'left': '50px', 'top': '50px', 'width': '200px', 'height': '80px', 'background-color': '#3498db', 'text-align': 'center', 'vertical-align': 'middle', 'border': '3px solid #2980b9', 'border-radius': '10px', 'opacity': '1.0', 'z-index': '1' } }, { text: 'Semi-transparent\n50% opacity', css: { 'font-size': '20px', 'color': '#ffffff', 'left': '300px', 'top': '50px', 'width': '200px', 'height': '80px', 'background-color': '#e74c3c', 'text-align': 'center', 'vertical-align': 'middle', 'border': '2px solid #c0392b', 'border-radius': '8px', 'opacity': '0.5', 'z-index': '2' } }, { text: 'Very transparent\n20% opacity', css: { 'font-size': '18px', 'color': '#2c3e50', 'left': '50px', 'top': '200px', 'width': '200px', 'height': '80px', 'background-color': '#f39c12', 'text-align': 'center', 'vertical-align': 'middle', 'border': '2px solid #e67e22', 'border-radius': '12px', 'opacity': '0.2', 'z-index': '3' } }, { text: 'Almost invisible\n10% opacity', css: { 'font-size': '16px', 'color': '#ffffff', 'left': '300px', 'top': '200px', 'width': '200px', 'height': '80px', 'background-color': '#9b59b6', 'text-align': 'center', 'vertical-align': 'middle', 'border': '2px solid #8e44ad', 'border-radius': '6px', 'opacity': '0.1', 'z-index': '4' } } ], images: [] } }; // Standalone implementation without ES6 modules // This replicates the core functionality of the canvas renderer // Text wrapping function const wrapText = (ctx, text, maxWidth) => { const words = text.split(' '); const lines = []; let currentLine = ''; for (let i = 0; i < words.length; i++) { const word = words[i]; const testLine = currentLine + (currentLine ? ' ' : '') + word; const testWidth = ctx.measureText(testLine).width; if (testWidth > maxWidth && currentLine) { // Current line is too long, start a new line lines.push(currentLine); currentLine = word; } else { // Add word to current line currentLine = testLine; } } // Add the last line if it's not empty if (currentLine) { lines.push(currentLine); } return lines; }; // Mock the canvas renderer functionality const renderCanvas = async (input, canvasId) => { const canvas = document.getElementById(canvasId); const ctx = canvas.getContext('2d'); // Set canvas dimensions canvas.width = input.canvas.width; canvas.height = input.canvas.height; // Clear canvas and set background ctx.clearRect(0, 0, canvas.width, canvas.height); const backgroundColor = input.canvas.background || 'white'; ctx.fillStyle = backgroundColor; ctx.fillRect(0, 0, canvas.width, canvas.height); // Process elements by z-index const elements = [...input.texts, ...input.images].sort((a, b) => { const aZ = parseInt(a.css['z-index'] || '0'); const bZ = parseInt(b.css['z-index'] || '0'); return aZ - bZ; }); // Render elements for (const element of elements) { const css = element.css; const left = parseInt(css.left || '0'); const top = parseInt(css.top || '0'); const width = parseInt(css.width || '100'); const height = parseInt(css.height || '100'); if (element.text) { // Render text ctx.font = `${css['font-size'] || '16px'} ${css['font-family'] || 'Arial'}`; ctx.fillStyle = css.color || 'black'; ctx.textAlign = css['text-align'] || 'left'; ctx.textBaseline = 'top'; // Save canvas state for opacity ctx.save(); // Set opacity if (css.opacity !== undefined) { ctx.globalAlpha = parseFloat(css.opacity); } // Handle multi-line text and text wrapping const fontSize = parseInt(css['font-size']) || 16; const lineHeight = parseInt(css['line-height']) || fontSize * 1.2; // Check if width is set to determine rendering mode const hasWidth = width !== undefined && width !== 100; // 100 is default fallback if (!hasWidth) { // Inline text rendering - no width constraint // Calculate padding for inline text (considering border width) const borderWidth = css.border ? parseInt(css.border.split(' ')[0]) || 0 : 0; const customPadding = css.padding ? parseInt(css.padding) : undefined; const padding = customPadding !== undefined ? customPadding : Math.max(borderWidth, 2); // Use custom padding or border width, minimum 2px // Measure text to get dimensions const textMetrics = ctx.measureText(element.text); const textWidth = textMetrics.width; const textHeight = fontSize; // Calculate total dimensions including padding const totalWidth = textWidth + (padding * 2); const totalHeight = textHeight + (padding * 2); // Position text inside the padded area const textX = left + padding; const textY = top + padding; // Draw background if specified (for inline text, use total dimensions) if (css['background-color']) { if (css['border-radius'] && parseInt(css['border-radius']) > 0) { // Draw rounded background drawRoundedRect(ctx, left, top, totalWidth, totalHeight, parseInt(css['border-radius']), true); } else { ctx.fillRect(left, top, totalWidth, totalHeight); } } // Draw text ctx.fillStyle = css.color || 'black'; ctx.fillText(element.text, textX, textY); // Draw border if specified (for inline text, use total dimensions) if (css.border) { const borderParts = css.border.split(' '); const borderWidth = parseInt(borderParts[0]) || 1; const borderStyle = borderParts[1] || 'solid'; const borderColor = borderParts[2] || 'black'; drawBorder(ctx, left, top, totalWidth, totalHeight, { width: borderWidth, style: borderStyle, color: borderColor }, css['border-radius'] ? parseInt(css['border-radius']) : undefined); } } else { // Block text rendering - width constraint applied // Calculate available width for text (considering padding) const borderWidth = css.border ? parseInt(css.border.split(' ')[0]) || 0 : 0; const customPadding = css.padding ? parseInt(css.padding) : undefined; const padding = customPadding !== undefined ? customPadding : Math.max(borderWidth, 4); // Use custom padding or minimum 4px const availableWidth = width - (padding * 2); // Process text: split by \n and wrap long lines let allLines = []; const manualLines = element.text.split('\n'); manualLines.forEach(line => { if (ctx.measureText(line).width > availableWidth) { // Wrap this line to fit within available width const wrappedLines = wrapText(ctx, line, availableWidth); allLines.push(...wrappedLines); } else { allLines.push(line); } }); // Calculate text position with padding and vertical alignment const textX = left + padding; // Handle vertical alignment let textY = top + padding; // Default to top alignment if (css['vertical-align']) { const totalTextHeight = allLines.length * lineHeight; const availableHeight = height - (padding * 2); if (css['vertical-align'] === 'middle' || css['vertical-align'] === 'center') { textY = top + padding + (availableHeight - totalTextHeight) / 2; } else if (css['vertical-align'] === 'bottom') { textY = top + height - padding - totalTextHeight; } } // Draw background if specified if (css['background-color']) { ctx.fillStyle = css['background-color']; if (css['border-radius'] && parseInt(css['border-radius']) > 0) { // Draw rounded background drawRoundedRect(ctx, left, top, width, height, parseInt(css['border-radius']), true); } else { ctx.fillRect(left, top, width, height); } } // Draw text with proper positioning ctx.fillStyle = css.color || 'black'; // Render each line allLines.forEach((line, index) => { const lineY = textY + (index * lineHeight); // Handle text alignment within the element if (css['text-align'] === 'center') { ctx.textAlign = 'center'; ctx.fillText(line, left + width / 2, lineY); } else if (css['text-align'] === 'right') { ctx.textAlign = 'right'; ctx.fillText(line, left + width - padding, lineY); } else { ctx.textAlign = 'left'; ctx.fillText(line, textX, lineY); } }); // Draw border if specified if (css.border) { const borderParts = css.border.split(' '); const borderWidth = parseInt(borderParts[0]) || 1; const borderStyle = borderParts[1] || 'solid'; const borderColor = borderParts[2] || 'black'; drawBorder(ctx, left, top, width, height, { width: borderWidth, style: borderStyle, color: borderColor }, css['border-radius'] ? parseInt(css['border-radius']) : undefined); } } // Restore canvas state (resets opacity) ctx.restore(); } else if (element.src) { // Render image const img = new Image(); await new Promise((resolve, reject) => { img.onload = resolve; img.onerror = reject; img.src = element.src; }); // Save canvas state for opacity ctx.save(); // Set opacity if (css.opacity !== undefined) { ctx.globalAlpha = parseFloat(css.opacity); } // Draw background if specified if (css['background-color']) { ctx.fillStyle = css['background-color']; if (css['border-radius'] && parseInt(css['border-radius']) > 0) { // Draw rounded background drawRoundedRect(ctx, left, top, width, height, parseInt(css['border-radius']), true); } else { ctx.fillRect(left, top, width, height); } } // Draw image if (css['border-radius'] && parseInt(css['border-radius']) > 0) { // Draw image with rounded corners drawRoundedImage(ctx, img, left, top, width, height, parseInt(css['border-radius'])); } else { ctx.drawImage(img, left, top, width, height); } // Draw border if specified if (css.border) {