Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Reddit Label Generator</title>
- <script src="https://unpkg.com/[email protected]/dist/gif.js"></script>
- <script src="https://cdn.jsdelivr.net/npm/[email protected]/omggif.min.js"></script>
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@600&family=Roboto+Mono:wght@600&family=Playfair+Display:wght@600&family=Montserrat:wght@600&family=Oswald:wght@600&display=swap" rel="stylesheet">
- <style>
- * {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
- }
- body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- min-height: 100vh;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 20px;
- }
- .container {
- background: white;
- border-radius: 16px;
- padding: 40px;
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
- max-width: 600px;
- width: 100%;
- }
- h1 {
- font-size: 28px;
- font-weight: 700;
- margin-bottom: 8px;
- color: #1a1a1a;
- }
- .subtitle {
- color: #666;
- margin-bottom: 30px;
- font-size: 14px;
- }
- .preview-section {
- background: #f8f9fa;
- border-radius: 12px;
- padding: 40px;
- margin-bottom: 30px;
- display: flex;
- justify-content: center;
- align-items: center;
- min-height: 200px;
- overflow: hidden;
- }
- .reddit-label {
- display: inline-flex;
- align-items: stretch;
- background-color: #FF4500;
- border-radius: 6px;
- overflow: hidden;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
- transition: transform 0.3s ease;
- transform-origin: center;
- }
- .image-slot {
- width: 100px;
- height: 100px;
- background: #ffffff;
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- position: relative;
- flex-shrink: 0;
- overflow: hidden;
- margin: 3px 0 3px 3px;
- }
- .image-slot img {
- width: 100%;
- height: 100%;
- object-fit: cover;
- display: block;
- }
- .placeholder-icon {
- font-size: 32px;
- color: #ccc;
- font-weight: 300;
- user-select: none;
- }
- .text-bubble {
- background: white;
- padding: 12px 20px;
- display: flex;
- align-items: center;
- justify-content: center;
- min-width: 100px;
- max-width: 500px;
- border-radius: 0 4px 4px 0;
- margin: 3px 3px 3px 0;
- box-sizing: border-box;
- min-height: 48px;
- }
- .text-bubble span {
- font-size: 16px; /* Default */
- font-weight: 600;
- color: #1a1a1a;
- line-height: 1.2;
- text-align: center;
- max-width: 100%;
- display: inline-block;
- word-wrap: break-word;
- overflow-wrap: break-word;
- }
- .controls {
- display: flex;
- flex-direction: column;
- gap: 20px;
- }
- .control-group {
- display: flex;
- flex-direction: column;
- gap: 8px;
- }
- .control-row {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 15px;
- }
- label {
- font-size: 14px;
- font-weight: 600;
- color: #333;
- }
- input[type="text"], select {
- padding: 12px 16px;
- border: 2px solid #e0e0e0;
- border-radius: 8px;
- font-size: 15px;
- transition: border-color 0.2s;
- font-family: inherit;
- background: white;
- }
- input[type="number"] {
- padding: 12px 16px;
- border: 2px solid #e0e0e0;
- border-radius: 8px;
- font-size: 15px;
- transition: border-color 0.2s;
- font-family: inherit;
- width: 100%;
- }
- input[type="number"]:focus {
- outline: none;
- border-color: #667eea;
- }
- .color-input-wrapper {
- display: flex;
- gap: 12px;
- align-items: center;
- }
- input[type="color"] {
- width: 60px;
- height: 44px;
- border: 2px solid #e0e0e0;
- border-radius: 8px;
- cursor: pointer;
- }
- .button {
- padding: 14px 24px;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- border: none;
- border-radius: 8px;
- font-size: 15px;
- font-weight: 600;
- cursor: pointer;
- box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
- }
- .button:hover {
- transform: translateY(-2px);
- }
- .tip {
- background: #fff3cd;
- border-left: 4px solid #ffc107;
- padding: 12px 16px;
- border-radius: 4px;
- font-size: 13px;
- color: #856404;
- margin-top: 20px;
- }
- </style>
- </head>
- <body>
- <div class="container">
- <h1>Reddit Label Generator</h1>
- <p class="subtitle">Create custom labels with custom fonts and sizing</p>
- <div class="preview-section">
- <div class="reddit-label" id="redditLabel">
- <div class="image-slot" id="imageSlot">
- <span class="placeholder-icon" id="placeholderIcon">+</span>
- <img id="uploadedImage" style="display: none;" alt="Uploaded">
- </div>
- <div class="text-bubble">
- <span id="labelText">Your Text Here</span>
- </div>
- </div>
- </div>
- <div class="controls">
- <div class="control-group">
- <label for="textInput">Label Text</label>
- <input type="text" id="textInput" value="Your Text Here" maxlength="100">
- </div>
- <div class="control-row">
- <div class="control-group">
- <label for="fontPicker">Font Family</label>
- <select id="fontPicker">
- <option value="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif">System Sans</option>
- <option value="'Inter', sans-serif">Inter</option>
- <option value="'Montserrat', sans-serif">Montserrat</option>
- <option value="'Oswald', sans-serif">Oswald</option>
- <option value="'Playfair Display', serif">Playfair</option>
- <option value="'Roboto Mono', monospace">Roboto Mono</option>
- </select>
- </div>
- <div class="control-group">
- <label for="fontSizeInput">Font Size (px)</label>
- <input type="number" id="fontSizeInput" min="10" max="40" value="16">
- </div>
- </div>
- <div class="control-row">
- <div class="control-group">
- <label for="colorInput">Frame Color</label>
- <div class="color-input-wrapper">
- <input type="color" id="colorInput" value="#FF4500">
- </div>
- </div>
- <div class="control-group">
- <label>Image</label>
- <button class="button" onclick="document.getElementById('fileInput').click()">Upload</button>
- <input type="file" id="fileInput" accept="image/*,image/gif,.gif" style="display:none">
- </div>
- </div>
- <button class="button" id="downloadBtn">Download PNG</button>
- <button class="button" id="downloadGifBtn" style="margin-top: 10px;">Download GIF</button>
- </div>
- <div class="tip">
- <strong>Tip:</strong> Adjust the font size to control how much text fits. Larger text will automatically scale the label down to fit the preview.
- </div>
- </div>
- <script>
- const textInput = document.getElementById('textInput');
- const colorInput = document.getElementById('colorInput');
- const fontPicker = document.getElementById('fontPicker');
- const fontSizeInput = document.getElementById('fontSizeInput');
- const labelText = document.getElementById('labelText');
- const redditLabel = document.getElementById('redditLabel');
- const imageSlot = document.getElementById('imageSlot');
- const uploadedImage = document.getElementById('uploadedImage');
- const placeholderIcon = document.getElementById('placeholderIcon');
- const fileInput = document.getElementById('fileInput');
- const downloadBtn = document.getElementById('downloadBtn');
- let currentImageData = null;
- function updatePreview() {
- labelText.textContent = textInput.value || 'Your Text Here';
- labelText.style.fontFamily = fontPicker.value;
- labelText.style.fontSize = fontSizeInput.value + 'px';
- updateScale();
- }
- function updateScale() {
- // Adjust scale of the whole label if it gets too wide
- const containerWidth = 500;
- const currentWidth = redditLabel.offsetWidth;
- if (currentWidth > containerWidth) {
- const scale = containerWidth / currentWidth;
- redditLabel.style.transform = `scale(${scale})`;
- } else {
- redditLabel.style.transform = `scale(1)`;
- }
- }
- // Listeners
- [textInput, fontPicker, fontSizeInput].forEach(el => {
- el.addEventListener('input', updatePreview);
- });
- colorInput.addEventListener('input', (e) => {
- redditLabel.style.backgroundColor = e.target.value;
- });
- imageSlot.addEventListener('click', () => fileInput.click());
- let isGif = false;
- let gifFramesData = []; // Store extracted frames with timing
- fileInput.addEventListener('change', (e) => {
- const file = e.target.files[0];
- if (!file) return;
- console.log('File selected:', file.name, file.type);
- isGif = file.type === 'image/gif';
- // Always load the image for display
- const reader = new FileReader();
- reader.onload = (event) => {
- currentImageData = event.target.result;
- uploadedImage.src = currentImageData;
- uploadedImage.style.display = 'block';
- placeholderIcon.style.display = 'none';
- };
- reader.readAsDataURL(file);
- // If it's a GIF, also extract frames
- if (isGif) {
- extractGifFrames(file);
- }
- });
- async function extractGifFrames(file) {
- try {
- console.log('Starting GIF extraction...');
- console.log('GifReader available?', typeof GifReader !== 'undefined');
- if (typeof GifReader === 'undefined') {
- console.error('omggif library not loaded!');
- return;
- }
- const arrayBuffer = await file.arrayBuffer();
- console.log('ArrayBuffer size:', arrayBuffer.byteLength);
- const intArray = new Uint8Array(arrayBuffer);
- const reader = new GifReader(intArray);
- const numFrames = reader.numFrames();
- console.log('Number of frames:', numFrames);
- gifFramesData = [];
- for (let i = 0; i < numFrames; i++) {
- const frameInfo = reader.frameInfo(i);
- const pixels = new Uint8Array(reader.width * reader.height * 4);
- reader.decodeAndBlitFrameRGBA(i, pixels);
- gifFramesData.push({
- pixels: pixels,
- delay: frameInfo.delay * 10 || 100, // delay is in hundredths of a second, convert to ms
- width: reader.width,
- height: reader.height
- });
- }
- console.log(`Successfully extracted ${gifFramesData.length} frames from GIF`);
- console.log('Sample frame - delay:', gifFramesData[0].delay + 'ms');
- } catch (error) {
- console.error('Error extracting GIF frames:', error);
- console.error('Error stack:', error.stack);
- gifFramesData = [];
- }
- }
- function wrapText(ctx, text, maxWidth) {
- const words = text.split(' ');
- const lines = [];
- let currentLine = '';
- for (let word of words) {
- const testLine = currentLine + (currentLine ? ' ' : '') + word;
- if (ctx.measureText(testLine).width > maxWidth && currentLine) {
- lines.push(currentLine);
- currentLine = word;
- } else {
- currentLine = testLine;
- }
- }
- if (currentLine) lines.push(currentLine);
- return lines;
- }
- downloadBtn.addEventListener('click', () => {
- const frameColor = colorInput.value;
- const text = labelText.textContent;
- const selectedFont = fontPicker.value;
- const fontSize = parseInt(fontSizeInput.value);
- const frameWidth = 3;
- const imageMargin = 3;
- const imageSize = 100;
- const textPaddingX = 20;
- const textPaddingY = 12;
- const lineHeight = fontSize * 1.2;
- const maxTextWidth = 460;
- const tempCanvas = document.createElement('canvas');
- const tempCtx = tempCanvas.getContext('2d');
- tempCtx.font = `600 ${fontSize}px ${selectedFont}`;
- const lines = wrapText(tempCtx, text, maxTextWidth);
- const maxLineWidth = Math.max(...lines.map(line => tempCtx.measureText(line).width));
- const textWidth = Math.min(maxLineWidth, maxTextWidth);
- const textHeight = lines.length * lineHeight;
- const bubbleWidth = textWidth + (textPaddingX * 2);
- const bubbleHeight = Math.max(imageSize, textHeight + (textPaddingY * 2));
- const totalWidth = frameWidth + imageMargin + imageSize + imageMargin + bubbleWidth + imageMargin + frameWidth;
- const totalHeight = bubbleHeight + (frameWidth * 2) + (imageMargin * 2);
- const canvas = document.createElement('canvas');
- canvas.width = totalWidth;
- canvas.height = totalHeight;
- const ctx = canvas.getContext('2d');
- // Draw Frame
- ctx.fillStyle = frameColor;
- ctx.beginPath();
- if (ctx.roundRect) ctx.roundRect(0, 0, totalWidth, totalHeight, 6);
- else ctx.rect(0, 0, totalWidth, totalHeight);
- ctx.fill();
- // Draw Image Area
- const imageX = frameWidth + imageMargin;
- const imageY = frameWidth + imageMargin;
- ctx.fillStyle = 'white';
- ctx.fillRect(imageX, imageY, imageSize, imageSize);
- const renderFinal = () => {
- const bubbleX = imageX + imageSize + imageMargin;
- const bubbleY = frameWidth + imageMargin;
- ctx.fillStyle = 'white';
- ctx.beginPath();
- if (ctx.roundRect) ctx.roundRect(bubbleX, bubbleY, bubbleWidth, bubbleHeight, 4);
- else ctx.rect(bubbleX, bubbleY, bubbleWidth, bubbleHeight);
- ctx.fill();
- ctx.fillStyle = '#1a1a1a';
- ctx.font = `600 ${fontSize}px ${selectedFont}`;
- ctx.textAlign = 'center';
- ctx.textBaseline = 'top';
- const textX = bubbleX + (bubbleWidth / 2);
- const startY = bubbleY + (bubbleHeight / 2) - (textHeight / 2);
- lines.forEach((line, index) => {
- ctx.fillText(line, textX, startY + (index * lineHeight));
- });
- const link = document.createElement('a');
- link.download = 'reddit-label.png';
- link.href = canvas.toDataURL();
- link.click();
- };
- if (currentImageData) {
- const img = new Image();
- img.onload = () => {
- ctx.drawImage(img, imageX, imageY, imageSize, imageSize);
- renderFinal();
- };
- img.src = currentImageData;
- } else {
- renderFinal();
- }
- });
- // Download as GIF
- const downloadGifBtn = document.getElementById('downloadGifBtn');
- downloadGifBtn.addEventListener('click', async () => {
- console.log('Download GIF clicked');
- console.log('isGif:', isGif);
- console.log('gifFramesData.length:', gifFramesData.length);
- if (!isGif) {
- alert('Please upload an animated GIF to export as GIF. The current image is not detected as a GIF file.');
- return;
- }
- if (gifFramesData.length === 0) {
- alert('GIF was detected but frame extraction failed. Please check the browser console for errors and try a different GIF file.');
- return;
- }
- // Get label dimensions
- const labelClone = redditLabel.cloneNode(true);
- const container = document.createElement('div');
- container.style.position = 'absolute';
- container.style.left = '-9999px';
- document.body.appendChild(container);
- container.appendChild(labelClone);
- await new Promise(resolve => setTimeout(resolve, 100));
- const rect = labelClone.getBoundingClientRect();
- // Get image position
- const imageSlotEl = labelClone.querySelector('.image-slot');
- const imageRect = imageSlotEl.getBoundingClientRect();
- const imageX = imageRect.left - rect.left;
- const imageY = imageRect.top - rect.top;
- const imageSize = imageRect.width;
- // Get text bubble info
- const textBubble = labelClone.querySelector('.text-bubble');
- const bubbleRect = textBubble.getBoundingClientRect();
- const bubbleX = bubbleRect.left - rect.left;
- const bubbleY = bubbleRect.top - rect.top;
- // Create GIF encoder
- const gif = new GIF({
- workers: 2,
- quality: 10,
- width: Math.ceil(rect.width),
- height: Math.ceil(rect.height)
- });
- // Process each frame from the original GIF
- for (let i = 0; i < gifFramesData.length; i++) {
- const frameData = gifFramesData[i];
- const canvas = document.createElement('canvas');
- canvas.width = Math.ceil(rect.width);
- canvas.height = Math.ceil(rect.height);
- const ctx = canvas.getContext('2d');
- // Draw background (frame color)
- ctx.fillStyle = colorInput.value;
- ctx.beginPath();
- ctx.roundRect(0, 0, rect.width, rect.height, 8);
- ctx.fill();
- // Draw image slot background
- ctx.fillStyle = 'white';
- ctx.fillRect(imageX, imageY, imageSize, imageSize);
- // Draw the actual GIF frame
- const frameCanvas = document.createElement('canvas');
- frameCanvas.width = frameData.width;
- frameCanvas.height = frameData.height;
- const frameCtx = frameCanvas.getContext('2d');
- // Create ImageData from RGBA pixel array
- const imageData = frameCtx.createImageData(frameData.width, frameData.height);
- imageData.data.set(frameData.pixels);
- frameCtx.putImageData(imageData, 0, 0);
- // Draw frame scaled to fit the image slot
- ctx.drawImage(frameCanvas, imageX, imageY, imageSize, imageSize);
- // Draw text bubble
- ctx.fillStyle = 'white';
- ctx.beginPath();
- ctx.roundRect(bubbleX, bubbleY, bubbleRect.width, bubbleRect.height, 6);
- ctx.fill();
- // Draw text
- const textSpan = labelClone.querySelector('.text-bubble span');
- const textStyle = window.getComputedStyle(textSpan);
- ctx.fillStyle = textStyle.color;
- ctx.font = `${textStyle.fontWeight} ${textStyle.fontSize} ${textStyle.fontFamily}`;
- ctx.textAlign = 'center';
- const textX = bubbleX + (bubbleRect.width / 2);
- const lineHeight = parseFloat(textStyle.lineHeight);
- const words = textSpan.textContent.split(' ');
- const lines = [];
- let currentLine = '';
- for (let j = 0; j < words.length; j++) {
- const testLine = currentLine + (currentLine ? ' ' : '') + words[j];
- const metrics = ctx.measureText(testLine);
- if (metrics.width > bubbleRect.width - 40 && currentLine) {
- lines.push(currentLine);
- currentLine = words[j];
- } else {
- currentLine = testLine;
- }
- }
- if (currentLine) lines.push(currentLine);
- const totalTextHeight = lines.length * lineHeight;
- const startY = bubbleY + (bubbleRect.height / 2) - (totalTextHeight / 2) + (lineHeight * 0.3);
- lines.forEach((line, index) => {
- ctx.fillText(line, textX, startY + (index * lineHeight));
- });
- // Add frame to GIF with original timing
- gif.addFrame(canvas, { delay: frameData.delay });
- }
- document.body.removeChild(container);
- // Render and download
- gif.on('finished', function(blob) {
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = 'reddit-label.gif';
- a.click();
- URL.revokeObjectURL(url);
- });
- gif.render();
- });
- // Initialize
- updatePreview();
- </script>
- </body>
- </html>
Advertisement
Add Comment
Please, Sign In to add comment