nekoli

gif exporter

Dec 19th, 2025
96
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 24.32 KB | None | 0 0
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>Reddit Label Generator</title>
  7. <script src="https://unpkg.com/[email protected]/dist/gif.js"></script>
  8. <script src="https://cdn.jsdelivr.net/npm/[email protected]/omggif.min.js"></script>
  9. <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">
  10. <style>
  11. * {
  12. margin: 0;
  13. padding: 0;
  14. box-sizing: border-box;
  15. }
  16.  
  17. body {
  18. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
  19. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  20. min-height: 100vh;
  21. display: flex;
  22. flex-direction: column;
  23. align-items: center;
  24. justify-content: center;
  25. padding: 20px;
  26. }
  27.  
  28. .container {
  29. background: white;
  30. border-radius: 16px;
  31. padding: 40px;
  32. box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
  33. max-width: 600px;
  34. width: 100%;
  35. }
  36.  
  37. h1 {
  38. font-size: 28px;
  39. font-weight: 700;
  40. margin-bottom: 8px;
  41. color: #1a1a1a;
  42. }
  43.  
  44. .subtitle {
  45. color: #666;
  46. margin-bottom: 30px;
  47. font-size: 14px;
  48. }
  49.  
  50. .preview-section {
  51. background: #f8f9fa;
  52. border-radius: 12px;
  53. padding: 40px;
  54. margin-bottom: 30px;
  55. display: flex;
  56. justify-content: center;
  57. align-items: center;
  58. min-height: 200px;
  59. overflow: hidden;
  60. }
  61.  
  62. .reddit-label {
  63. display: inline-flex;
  64. align-items: stretch;
  65. background-color: #FF4500;
  66. border-radius: 6px;
  67. overflow: hidden;
  68. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  69. transition: transform 0.3s ease;
  70. transform-origin: center;
  71. }
  72.  
  73. .image-slot {
  74. width: 100px;
  75. height: 100px;
  76. background: #ffffff;
  77. display: flex;
  78. align-items: center;
  79. justify-content: center;
  80. cursor: pointer;
  81. position: relative;
  82. flex-shrink: 0;
  83. overflow: hidden;
  84. margin: 3px 0 3px 3px;
  85. }
  86.  
  87. .image-slot img {
  88. width: 100%;
  89. height: 100%;
  90. object-fit: cover;
  91. display: block;
  92. }
  93.  
  94. .placeholder-icon {
  95. font-size: 32px;
  96. color: #ccc;
  97. font-weight: 300;
  98. user-select: none;
  99. }
  100.  
  101. .text-bubble {
  102. background: white;
  103. padding: 12px 20px;
  104. display: flex;
  105. align-items: center;
  106. justify-content: center;
  107. min-width: 100px;
  108. max-width: 500px;
  109. border-radius: 0 4px 4px 0;
  110. margin: 3px 3px 3px 0;
  111. box-sizing: border-box;
  112. min-height: 48px;
  113. }
  114.  
  115. .text-bubble span {
  116. font-size: 16px; /* Default */
  117. font-weight: 600;
  118. color: #1a1a1a;
  119. line-height: 1.2;
  120. text-align: center;
  121. max-width: 100%;
  122. display: inline-block;
  123. word-wrap: break-word;
  124. overflow-wrap: break-word;
  125. }
  126.  
  127. .controls {
  128. display: flex;
  129. flex-direction: column;
  130. gap: 20px;
  131. }
  132.  
  133. .control-group {
  134. display: flex;
  135. flex-direction: column;
  136. gap: 8px;
  137. }
  138.  
  139. .control-row {
  140. display: grid;
  141. grid-template-columns: 1fr 1fr;
  142. gap: 15px;
  143. }
  144.  
  145. label {
  146. font-size: 14px;
  147. font-weight: 600;
  148. color: #333;
  149. }
  150.  
  151. input[type="text"], select {
  152. padding: 12px 16px;
  153. border: 2px solid #e0e0e0;
  154. border-radius: 8px;
  155. font-size: 15px;
  156. transition: border-color 0.2s;
  157. font-family: inherit;
  158. background: white;
  159. }
  160.  
  161. input[type="number"] {
  162. padding: 12px 16px;
  163. border: 2px solid #e0e0e0;
  164. border-radius: 8px;
  165. font-size: 15px;
  166. transition: border-color 0.2s;
  167. font-family: inherit;
  168. width: 100%;
  169. }
  170.  
  171. input[type="number"]:focus {
  172. outline: none;
  173. border-color: #667eea;
  174. }
  175.  
  176. .color-input-wrapper {
  177. display: flex;
  178. gap: 12px;
  179. align-items: center;
  180. }
  181.  
  182. input[type="color"] {
  183. width: 60px;
  184. height: 44px;
  185. border: 2px solid #e0e0e0;
  186. border-radius: 8px;
  187. cursor: pointer;
  188. }
  189.  
  190. .button {
  191. padding: 14px 24px;
  192. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  193. color: white;
  194. border: none;
  195. border-radius: 8px;
  196. font-size: 15px;
  197. font-weight: 600;
  198. cursor: pointer;
  199. box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
  200. }
  201.  
  202. .button:hover {
  203. transform: translateY(-2px);
  204. }
  205.  
  206. .tip {
  207. background: #fff3cd;
  208. border-left: 4px solid #ffc107;
  209. padding: 12px 16px;
  210. border-radius: 4px;
  211. font-size: 13px;
  212. color: #856404;
  213. margin-top: 20px;
  214. }
  215. </style>
  216. </head>
  217. <body>
  218. <div class="container">
  219. <h1>Reddit Label Generator</h1>
  220. <p class="subtitle">Create custom labels with custom fonts and sizing</p>
  221.  
  222. <div class="preview-section">
  223. <div class="reddit-label" id="redditLabel">
  224. <div class="image-slot" id="imageSlot">
  225. <span class="placeholder-icon" id="placeholderIcon">+</span>
  226. <img id="uploadedImage" style="display: none;" alt="Uploaded">
  227. </div>
  228. <div class="text-bubble">
  229. <span id="labelText">Your Text Here</span>
  230. </div>
  231. </div>
  232. </div>
  233.  
  234. <div class="controls">
  235. <div class="control-group">
  236. <label for="textInput">Label Text</label>
  237. <input type="text" id="textInput" value="Your Text Here" maxlength="100">
  238. </div>
  239.  
  240. <div class="control-row">
  241. <div class="control-group">
  242. <label for="fontPicker">Font Family</label>
  243. <select id="fontPicker">
  244. <option value="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif">System Sans</option>
  245. <option value="'Inter', sans-serif">Inter</option>
  246. <option value="'Montserrat', sans-serif">Montserrat</option>
  247. <option value="'Oswald', sans-serif">Oswald</option>
  248. <option value="'Playfair Display', serif">Playfair</option>
  249. <option value="'Roboto Mono', monospace">Roboto Mono</option>
  250. </select>
  251. </div>
  252. <div class="control-group">
  253. <label for="fontSizeInput">Font Size (px)</label>
  254. <input type="number" id="fontSizeInput" min="10" max="40" value="16">
  255. </div>
  256. </div>
  257.  
  258. <div class="control-row">
  259. <div class="control-group">
  260. <label for="colorInput">Frame Color</label>
  261. <div class="color-input-wrapper">
  262. <input type="color" id="colorInput" value="#FF4500">
  263. </div>
  264. </div>
  265. <div class="control-group">
  266. <label>Image</label>
  267. <button class="button" onclick="document.getElementById('fileInput').click()">Upload</button>
  268. <input type="file" id="fileInput" accept="image/*,image/gif,.gif" style="display:none">
  269. </div>
  270. </div>
  271.  
  272. <button class="button" id="downloadBtn">Download PNG</button>
  273. <button class="button" id="downloadGifBtn" style="margin-top: 10px;">Download GIF</button>
  274. </div>
  275.  
  276. <div class="tip">
  277. <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.
  278. </div>
  279. </div>
  280.  
  281. <script>
  282. const textInput = document.getElementById('textInput');
  283. const colorInput = document.getElementById('colorInput');
  284. const fontPicker = document.getElementById('fontPicker');
  285. const fontSizeInput = document.getElementById('fontSizeInput');
  286. const labelText = document.getElementById('labelText');
  287. const redditLabel = document.getElementById('redditLabel');
  288. const imageSlot = document.getElementById('imageSlot');
  289. const uploadedImage = document.getElementById('uploadedImage');
  290. const placeholderIcon = document.getElementById('placeholderIcon');
  291. const fileInput = document.getElementById('fileInput');
  292. const downloadBtn = document.getElementById('downloadBtn');
  293.  
  294. let currentImageData = null;
  295.  
  296. function updatePreview() {
  297. labelText.textContent = textInput.value || 'Your Text Here';
  298. labelText.style.fontFamily = fontPicker.value;
  299. labelText.style.fontSize = fontSizeInput.value + 'px';
  300. updateScale();
  301. }
  302.  
  303. function updateScale() {
  304. // Adjust scale of the whole label if it gets too wide
  305. const containerWidth = 500;
  306. const currentWidth = redditLabel.offsetWidth;
  307. if (currentWidth > containerWidth) {
  308. const scale = containerWidth / currentWidth;
  309. redditLabel.style.transform = `scale(${scale})`;
  310. } else {
  311. redditLabel.style.transform = `scale(1)`;
  312. }
  313. }
  314.  
  315. // Listeners
  316. [textInput, fontPicker, fontSizeInput].forEach(el => {
  317. el.addEventListener('input', updatePreview);
  318. });
  319.  
  320. colorInput.addEventListener('input', (e) => {
  321. redditLabel.style.backgroundColor = e.target.value;
  322. });
  323.  
  324. imageSlot.addEventListener('click', () => fileInput.click());
  325.  
  326. let isGif = false;
  327. let gifFramesData = []; // Store extracted frames with timing
  328.  
  329. fileInput.addEventListener('change', (e) => {
  330. const file = e.target.files[0];
  331. if (!file) return;
  332.  
  333. console.log('File selected:', file.name, file.type);
  334. isGif = file.type === 'image/gif';
  335.  
  336. // Always load the image for display
  337. const reader = new FileReader();
  338. reader.onload = (event) => {
  339. currentImageData = event.target.result;
  340. uploadedImage.src = currentImageData;
  341. uploadedImage.style.display = 'block';
  342. placeholderIcon.style.display = 'none';
  343. };
  344. reader.readAsDataURL(file);
  345.  
  346. // If it's a GIF, also extract frames
  347. if (isGif) {
  348. extractGifFrames(file);
  349. }
  350. });
  351.  
  352. async function extractGifFrames(file) {
  353. try {
  354. console.log('Starting GIF extraction...');
  355. console.log('GifReader available?', typeof GifReader !== 'undefined');
  356.  
  357. if (typeof GifReader === 'undefined') {
  358. console.error('omggif library not loaded!');
  359. return;
  360. }
  361.  
  362. const arrayBuffer = await file.arrayBuffer();
  363. console.log('ArrayBuffer size:', arrayBuffer.byteLength);
  364.  
  365. const intArray = new Uint8Array(arrayBuffer);
  366. const reader = new GifReader(intArray);
  367.  
  368. const numFrames = reader.numFrames();
  369. console.log('Number of frames:', numFrames);
  370.  
  371. gifFramesData = [];
  372.  
  373. for (let i = 0; i < numFrames; i++) {
  374. const frameInfo = reader.frameInfo(i);
  375. const pixels = new Uint8Array(reader.width * reader.height * 4);
  376. reader.decodeAndBlitFrameRGBA(i, pixels);
  377.  
  378. gifFramesData.push({
  379. pixels: pixels,
  380. delay: frameInfo.delay * 10 || 100, // delay is in hundredths of a second, convert to ms
  381. width: reader.width,
  382. height: reader.height
  383. });
  384. }
  385.  
  386. console.log(`Successfully extracted ${gifFramesData.length} frames from GIF`);
  387. console.log('Sample frame - delay:', gifFramesData[0].delay + 'ms');
  388. } catch (error) {
  389. console.error('Error extracting GIF frames:', error);
  390. console.error('Error stack:', error.stack);
  391. gifFramesData = [];
  392. }
  393. }
  394.  
  395. function wrapText(ctx, text, maxWidth) {
  396. const words = text.split(' ');
  397. const lines = [];
  398. let currentLine = '';
  399.  
  400. for (let word of words) {
  401. const testLine = currentLine + (currentLine ? ' ' : '') + word;
  402. if (ctx.measureText(testLine).width > maxWidth && currentLine) {
  403. lines.push(currentLine);
  404. currentLine = word;
  405. } else {
  406. currentLine = testLine;
  407. }
  408. }
  409. if (currentLine) lines.push(currentLine);
  410. return lines;
  411. }
  412.  
  413. downloadBtn.addEventListener('click', () => {
  414. const frameColor = colorInput.value;
  415. const text = labelText.textContent;
  416. const selectedFont = fontPicker.value;
  417. const fontSize = parseInt(fontSizeInput.value);
  418.  
  419. const frameWidth = 3;
  420. const imageMargin = 3;
  421. const imageSize = 100;
  422. const textPaddingX = 20;
  423. const textPaddingY = 12;
  424. const lineHeight = fontSize * 1.2;
  425. const maxTextWidth = 460;
  426.  
  427. const tempCanvas = document.createElement('canvas');
  428. const tempCtx = tempCanvas.getContext('2d');
  429. tempCtx.font = `600 ${fontSize}px ${selectedFont}`;
  430.  
  431. const lines = wrapText(tempCtx, text, maxTextWidth);
  432. const maxLineWidth = Math.max(...lines.map(line => tempCtx.measureText(line).width));
  433. const textWidth = Math.min(maxLineWidth, maxTextWidth);
  434. const textHeight = lines.length * lineHeight;
  435.  
  436. const bubbleWidth = textWidth + (textPaddingX * 2);
  437. const bubbleHeight = Math.max(imageSize, textHeight + (textPaddingY * 2));
  438. const totalWidth = frameWidth + imageMargin + imageSize + imageMargin + bubbleWidth + imageMargin + frameWidth;
  439. const totalHeight = bubbleHeight + (frameWidth * 2) + (imageMargin * 2);
  440.  
  441. const canvas = document.createElement('canvas');
  442. canvas.width = totalWidth;
  443. canvas.height = totalHeight;
  444. const ctx = canvas.getContext('2d');
  445.  
  446. // Draw Frame
  447. ctx.fillStyle = frameColor;
  448. ctx.beginPath();
  449. if (ctx.roundRect) ctx.roundRect(0, 0, totalWidth, totalHeight, 6);
  450. else ctx.rect(0, 0, totalWidth, totalHeight);
  451. ctx.fill();
  452.  
  453. // Draw Image Area
  454. const imageX = frameWidth + imageMargin;
  455. const imageY = frameWidth + imageMargin;
  456. ctx.fillStyle = 'white';
  457. ctx.fillRect(imageX, imageY, imageSize, imageSize);
  458.  
  459. const renderFinal = () => {
  460. const bubbleX = imageX + imageSize + imageMargin;
  461. const bubbleY = frameWidth + imageMargin;
  462.  
  463. ctx.fillStyle = 'white';
  464. ctx.beginPath();
  465. if (ctx.roundRect) ctx.roundRect(bubbleX, bubbleY, bubbleWidth, bubbleHeight, 4);
  466. else ctx.rect(bubbleX, bubbleY, bubbleWidth, bubbleHeight);
  467. ctx.fill();
  468.  
  469. ctx.fillStyle = '#1a1a1a';
  470. ctx.font = `600 ${fontSize}px ${selectedFont}`;
  471. ctx.textAlign = 'center';
  472. ctx.textBaseline = 'top';
  473.  
  474. const textX = bubbleX + (bubbleWidth / 2);
  475. const startY = bubbleY + (bubbleHeight / 2) - (textHeight / 2);
  476.  
  477. lines.forEach((line, index) => {
  478. ctx.fillText(line, textX, startY + (index * lineHeight));
  479. });
  480.  
  481. const link = document.createElement('a');
  482. link.download = 'reddit-label.png';
  483. link.href = canvas.toDataURL();
  484. link.click();
  485. };
  486.  
  487. if (currentImageData) {
  488. const img = new Image();
  489. img.onload = () => {
  490. ctx.drawImage(img, imageX, imageY, imageSize, imageSize);
  491. renderFinal();
  492. };
  493. img.src = currentImageData;
  494. } else {
  495. renderFinal();
  496. }
  497. });
  498.  
  499. // Download as GIF
  500. const downloadGifBtn = document.getElementById('downloadGifBtn');
  501.  
  502. downloadGifBtn.addEventListener('click', async () => {
  503. console.log('Download GIF clicked');
  504. console.log('isGif:', isGif);
  505. console.log('gifFramesData.length:', gifFramesData.length);
  506.  
  507. if (!isGif) {
  508. alert('Please upload an animated GIF to export as GIF. The current image is not detected as a GIF file.');
  509. return;
  510. }
  511.  
  512. if (gifFramesData.length === 0) {
  513. alert('GIF was detected but frame extraction failed. Please check the browser console for errors and try a different GIF file.');
  514. return;
  515. }
  516.  
  517. // Get label dimensions
  518. const labelClone = redditLabel.cloneNode(true);
  519. const container = document.createElement('div');
  520. container.style.position = 'absolute';
  521. container.style.left = '-9999px';
  522. document.body.appendChild(container);
  523. container.appendChild(labelClone);
  524.  
  525. await new Promise(resolve => setTimeout(resolve, 100));
  526. const rect = labelClone.getBoundingClientRect();
  527.  
  528. // Get image position
  529. const imageSlotEl = labelClone.querySelector('.image-slot');
  530. const imageRect = imageSlotEl.getBoundingClientRect();
  531. const imageX = imageRect.left - rect.left;
  532. const imageY = imageRect.top - rect.top;
  533. const imageSize = imageRect.width;
  534.  
  535. // Get text bubble info
  536. const textBubble = labelClone.querySelector('.text-bubble');
  537. const bubbleRect = textBubble.getBoundingClientRect();
  538. const bubbleX = bubbleRect.left - rect.left;
  539. const bubbleY = bubbleRect.top - rect.top;
  540.  
  541. // Create GIF encoder
  542. const gif = new GIF({
  543. workers: 2,
  544. quality: 10,
  545. width: Math.ceil(rect.width),
  546. height: Math.ceil(rect.height)
  547. });
  548.  
  549. // Process each frame from the original GIF
  550. for (let i = 0; i < gifFramesData.length; i++) {
  551. const frameData = gifFramesData[i];
  552. const canvas = document.createElement('canvas');
  553. canvas.width = Math.ceil(rect.width);
  554. canvas.height = Math.ceil(rect.height);
  555. const ctx = canvas.getContext('2d');
  556.  
  557. // Draw background (frame color)
  558. ctx.fillStyle = colorInput.value;
  559. ctx.beginPath();
  560. ctx.roundRect(0, 0, rect.width, rect.height, 8);
  561. ctx.fill();
  562.  
  563. // Draw image slot background
  564. ctx.fillStyle = 'white';
  565. ctx.fillRect(imageX, imageY, imageSize, imageSize);
  566.  
  567. // Draw the actual GIF frame
  568. const frameCanvas = document.createElement('canvas');
  569. frameCanvas.width = frameData.width;
  570. frameCanvas.height = frameData.height;
  571. const frameCtx = frameCanvas.getContext('2d');
  572.  
  573. // Create ImageData from RGBA pixel array
  574. const imageData = frameCtx.createImageData(frameData.width, frameData.height);
  575. imageData.data.set(frameData.pixels);
  576. frameCtx.putImageData(imageData, 0, 0);
  577.  
  578. // Draw frame scaled to fit the image slot
  579. ctx.drawImage(frameCanvas, imageX, imageY, imageSize, imageSize);
  580.  
  581. // Draw text bubble
  582. ctx.fillStyle = 'white';
  583. ctx.beginPath();
  584. ctx.roundRect(bubbleX, bubbleY, bubbleRect.width, bubbleRect.height, 6);
  585. ctx.fill();
  586.  
  587. // Draw text
  588. const textSpan = labelClone.querySelector('.text-bubble span');
  589. const textStyle = window.getComputedStyle(textSpan);
  590. ctx.fillStyle = textStyle.color;
  591. ctx.font = `${textStyle.fontWeight} ${textStyle.fontSize} ${textStyle.fontFamily}`;
  592. ctx.textAlign = 'center';
  593.  
  594. const textX = bubbleX + (bubbleRect.width / 2);
  595. const lineHeight = parseFloat(textStyle.lineHeight);
  596. const words = textSpan.textContent.split(' ');
  597. const lines = [];
  598. let currentLine = '';
  599.  
  600. for (let j = 0; j < words.length; j++) {
  601. const testLine = currentLine + (currentLine ? ' ' : '') + words[j];
  602. const metrics = ctx.measureText(testLine);
  603. if (metrics.width > bubbleRect.width - 40 && currentLine) {
  604. lines.push(currentLine);
  605. currentLine = words[j];
  606. } else {
  607. currentLine = testLine;
  608. }
  609. }
  610. if (currentLine) lines.push(currentLine);
  611.  
  612. const totalTextHeight = lines.length * lineHeight;
  613. const startY = bubbleY + (bubbleRect.height / 2) - (totalTextHeight / 2) + (lineHeight * 0.3);
  614.  
  615. lines.forEach((line, index) => {
  616. ctx.fillText(line, textX, startY + (index * lineHeight));
  617. });
  618.  
  619. // Add frame to GIF with original timing
  620. gif.addFrame(canvas, { delay: frameData.delay });
  621. }
  622.  
  623. document.body.removeChild(container);
  624.  
  625. // Render and download
  626. gif.on('finished', function(blob) {
  627. const url = URL.createObjectURL(blob);
  628. const a = document.createElement('a');
  629. a.href = url;
  630. a.download = 'reddit-label.gif';
  631. a.click();
  632. URL.revokeObjectURL(url);
  633. });
  634.  
  635. gif.render();
  636. });
  637.  
  638. // Initialize
  639. updatePreview();
  640. </script>
  641. </body>
  642. </html>
Advertisement
Add Comment
Please, Sign In to add comment