Last active
February 11, 2026 01:22
-
-
Save mapkepp/30f6823c12f5131505cfe37e013e6fe5 to your computer and use it in GitHub Desktop.
Генератор карточек лото
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="ru"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Генератор карточек для Русского Лотто</title> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf-lib/1.17.1/pdf-lib.min.js"></script> | |
| <style> | |
| body { | |
| font-family: Arial, sans-serif; | |
| text-align: center; | |
| padding: 20px; | |
| margin: 0; | |
| } | |
| #controls { | |
| margin-bottom: 20px; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 15px; | |
| } | |
| .control-group { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| width: 100%; | |
| max-width: 600px; | |
| justify-content: flex-start; | |
| } | |
| label { | |
| font-weight: bold; | |
| white-space: nowrap; | |
| min-width: 200px; | |
| text-align: left; | |
| } | |
| input[type="number"] { | |
| padding: 5px; | |
| width: 80px; | |
| } | |
| button { | |
| margin-top: 15px; | |
| padding: 10px 20px; | |
| font-size: 16px; | |
| cursor: pointer; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>Генератор карточек для Русского Лотто</h1> | |
| <div id="controls"> | |
| <div class="control-group"> | |
| <label for="pageCount">Количество страниц кратно 3м (на странице по 4 карточки):</label> | |
| <input type="number" id="pageCount" min="1" value="6"> | |
| </div> | |
| <div class="control-group"> | |
| <label for="fontFamily">Шрифт чисел:</label> | |
| <select id="fontFamily"> | |
| <option value="Helvetica" selected>Helvetica (обычный)</option> | |
| <option value="HelveticaBold">Helvetica Bold (жирный)</option> | |
| <option value="Helvetica-Oblique">Helvetica Oblique (курсив)</option> | |
| <option value="Helvetica-BoldOblique">Helvetica Bold Oblique</option> | |
| </select> | |
| </div> | |
| <div class="control-group"> | |
| <label for="fontSize">Размер шрифта чисел (pt):</label> | |
| <input type="number" id="fontSize" min="16" max="36" value="30"> | |
| </div> | |
| <div class="control-group"> | |
| <label for="outerBorder">Толщина внешней рамки (px):</label> | |
| <input type="number" id="outerBorder" min="1" max="10" value="4"> | |
| </div> | |
| <div class="control-group"> | |
| <label for="innerBorder">Толщина внутренних линий (px):</label> | |
| <input type="number" id="innerBorder" min="1" max="5" value="2"> | |
| </div> | |
| <!-- Ввод ширины в десятых мм --> | |
| <div class="control-group"> | |
| <label for="cardWidthTenths">Ширина карточки (десятые мм) стандарт 2200:</label> | |
| <input type="number" id="cardWidthTenths" min="1060" max="2120" value="1965"> | |
| </div> | |
| <!-- Ввод высоты в десятых мм --> | |
| <div class="control-group"> | |
| <label for="cardHeightTenths">Высота карточки (десятые мм) стандарт 800:</label> | |
| <input type="number" id="cardHeightTenths" min="350" max="1060" value="657"> | |
| </div> | |
| <div class="control-group"> | |
| <label for="verticalSpacing">Расстояние между карточками (pt):</label> | |
| <input type="number" id="verticalSpacing" min="7" max="150" value="20"> | |
| </div> | |
| <!-- Размер шрифта метки --> | |
| <div class="control-group"> | |
| <label for="dateTimeFontSize">Размер шрифта даты‑времени (pt):</label> | |
| <input type="number" id="dateTimeFontSize" min="1" max="20" value="3"> | |
| </div> | |
| <div class="control-group"> | |
| <label for="numberFontSize">Размер шрифта номера (pt):</label> | |
| <input type="number" id="numberFontSize" min="1" max="20" value="4"> | |
| </div> | |
| <!-- Расстояние метки от рамки --> | |
| <div class="control-group"> | |
| <label for="footerMargin">Расстояние метки от рамки (pt):</label> | |
| <input type="number" id="footerMargin" min="-50" max="50" value="5"> | |
| </div> | |
| <button onclick="generatePDF()">Сгенерировать PDF</button> | |
| </div> | |
| <div id="cardContainer"> | |
| <!-- Карточки будут отображаться здесь --> | |
| </div> | |
| <script> | |
| // Генерация карточки лото (9×3, 15 чисел) | |
| const generateLotoCard = () => { | |
| const card = [Array(9).fill(0), Array(9).fill(0), Array(9).fill(0)]; | |
| const columnRanges = [ | |
| [1, 9], [10, 19], [20, 29], [30, 39], [40, 49], | |
| [50, 59], [60, 69], [70, 79], [80, 90] | |
| ]; | |
| const generateValidCard = () => { | |
| const tempCard = [Array(9).fill(0), Array(9).fill(0), Array(9).fill(0)]; | |
| for (let col = 0; col < 9; col++) { | |
| const [min, max] = columnRanges[col]; | |
| const numbersInColumn = Array.from( | |
| { length: max - min + 1 }, | |
| (_, i) => i + min | |
| ); | |
| const numCount = Math.floor(Math.random() * 2) + 1; | |
| const selectedNumbers = []; | |
| for (let i = 0; i < numCount; i++) { | |
| const randomIndex = Math.floor(Math.random() * numbersInColumn.length); | |
| const num = numbersInColumn[randomIndex]; | |
| selectedNumbers.push(num); | |
| numbersInColumn.splice(randomIndex, 1); | |
| } | |
| for (const num of selectedNumbers) { | |
| let row; | |
| do { | |
| row = Math.floor(Math.random() * 3); | |
| } while (tempCard[row][col] !== 0); | |
| tempCard[row][col] = num; | |
| } | |
| } | |
| const isValid = tempCard.every(row => row.filter(n => n !== 0).length === 5); | |
| return isValid ? tempCard : generateValidCard(); | |
| }; | |
| return generateValidCard(); | |
| }; | |
| // Генерация PDF | |
| const generatePDF = async () => { | |
| try { | |
| const pageCount = parseInt(document.getElementById('pageCount').value); | |
| const fontSize = parseInt(document.getElementById('fontSize').value); | |
| const outerBorder = parseInt(document.getElementById('outerBorder').value); | |
| const innerBorder = parseInt(document.getElementById('innerBorder').value); | |
| // Конвертация: десятые мм → pt (1 десятая мм = 0.283464567 pt) | |
| const tenthsToPt = 0.283464567; | |
| // Расчёт высоты карточки в pt | |
| const cardHeightInput = parseInt(document.getElementById('cardHeightTenths').value); | |
| const cardHeight = Math.round(cardHeightInput * tenthsToPt); | |
| // Расчёт ширины карточки в pt | |
| const cardWidthInput = parseInt(document.getElementById('cardWidthTenths').value); | |
| const cardWidth = Math.round(cardWidthInput * tenthsToPt); | |
| const verticalSpacing = parseInt(document.getElementById('verticalSpacing').value); | |
| // Размеры шрифтов для метки | |
| const dateTimeFontSize = parseInt(document.getElementById('dateTimeFontSize').value); | |
| const numberFontSize = parseInt(document.getElementById('numberFontSize').value); | |
| // Расстояние от нижней рамки карточки до метки (в pt) | |
| const footerMargin = parseInt(document.getElementById('footerMargin').value); | |
| const { PDFDocument } = PDFLib; | |
| const pdfDoc = await PDFDocument.create(); | |
| const pageWidth = 595; // A4 ширина в pt | |
| const pageHeight = 842; // A4 высота в pt | |
| // Считываем выбранный шрифт (как раньше) | |
| const fontFamily = document.getElementById('fontFamily').value; | |
| let font; | |
| switch (fontFamily) { | |
| case 'Helvetica': | |
| font = await pdfDoc.embedFont(PDFLib.StandardFonts.Helvetica); | |
| break; | |
| case 'HelveticaBold': | |
| font = await pdfDoc.embedFont(PDFLib.StandardFonts.HelveticaBold); | |
| break; | |
| case 'Helvetica-Oblique': | |
| font = await pdfDoc.embedFont(PDFLib.StandardFonts['Helvetica-Oblique']); | |
| break; | |
| case 'Helvetica-BoldOblique': | |
| font = await pdfDoc.embedFont(PDFLib.StandardFonts['Helvetica-BoldOblique']); | |
| break; | |
| default: | |
| font = await pdfDoc.embedFont(PDFLib.StandardFonts.Helvetica); | |
| } | |
| const blackColor = PDFLib.rgb(0, 0, 0); | |
| // !!! ВАЖНО: счётчик теперь ВНЕ цикла по страницам | |
| let globalCardCounter = 1; | |
| for (let i = 0; i < pageCount; i++) { | |
| const page = pdfDoc.addPage([pageWidth, pageHeight]); | |
| // Расчёт количества карточек на странице по вертикали | |
| const cardsPerColumn = Math.floor((pageHeight - 0) / (cardHeight + verticalSpacing)); | |
| for (let row = 0; row < cardsPerColumn; row++) { | |
| const card = generateLotoCard(); | |
| const cardY = pageHeight - (row + 1) * (cardHeight + verticalSpacing) - 0; | |
| const cardX = (pageWidth - cardWidth) / 2; | |
| // Внешняя рамка карточки (как раньше) | |
| page.drawRectangle({ | |
| x: cardX, | |
| y: cardY, | |
| width: cardWidth, | |
| height: cardHeight, | |
| borderColor: blackColor, | |
| borderWidth: outerBorder, | |
| }); | |
| const cellWidth = cardWidth / 9; | |
| const cellHeight = cardHeight / 3; | |
| // Рисуем ячейки и числа (как раньше) | |
| for (let rowIndex = 0; rowIndex < 3; rowIndex++) { | |
| for (let colIndex = 0; colIndex < 9; colIndex++) { | |
| const num = card[rowIndex][colIndex]; | |
| const xCenter = cardX + colIndex * cellWidth + cellWidth / 2; | |
| const yCenter = cardY + (2 - rowIndex) * cellHeight + cellHeight / 2; | |
| page.drawRectangle({ | |
| x: cardX + colIndex * cellWidth, | |
| y: cardY + (2 - rowIndex) * cellHeight, | |
| width: cellWidth, | |
| height: cellHeight, | |
| borderColor: blackColor, | |
| borderWidth: innerBorder, | |
| }); | |
| if (num !== 0) { | |
| const text = num.toString(); | |
| const textWidth = font.widthOfTextAtSize(text, fontSize); | |
| const x = xCenter - textWidth / 2; | |
| let k; | |
| if (fontSize <= 22) { | |
| k = 0.35; | |
| } else if (fontSize <= 28) { | |
| k = 0.35 - (fontSize - 22) * (0.05 / 6); | |
| } else { | |
| k = 0.30 - (fontSize - 28) * (0.05 / 8); | |
| } | |
| const y = yCenter - fontSize * k; | |
| page.drawText(text, { | |
| x: x, | |
| y: y, | |
| size: fontSize, | |
| font: font, | |
| color: blackColor, | |
| }); | |
| } | |
| } | |
| } | |
| // --- МЕТКА ВНИЗУ КАРТОЧКИ --- | |
| const now = new Date(); | |
| const year = now.getFullYear(); | |
| const month = String(now.getMonth() + 1).padStart(2, '0'); | |
| const day = String(now.getDate()).padStart(2, '0'); | |
| const hours = String(now.getHours()).padStart(2, '0'); | |
| const minutes = String(now.getMinutes()).padStart(2, '0'); | |
| const seconds = String(now.getSeconds()).padStart(2, '0'); | |
| const milliseconds = String(now.getMilliseconds()).padStart(3, '0'); | |
| const dateTimeStr = `${year}${month}${day}${hours}${minutes}${seconds}${milliseconds}`; | |
| const numberStr = `${globalCardCounter}`; // Используем общий счётчик | |
| const footerY = cardY + footerMargin; | |
| // Отрисовка даты-времени | |
| const dateTimeTextWidth = font.widthOfTextAtSize(dateTimeStr, dateTimeFontSize); | |
| const dateTimeX = cardX + (cardWidth - dateTimeTextWidth - 5) / 2; | |
| page.drawText(dateTimeStr, { | |
| x: dateTimeX, | |
| y: footerY, | |
| size: dateTimeFontSize, | |
| font: font, | |
| color: blackColor, | |
| }); | |
| // Отрисовка номера | |
| const numberTextWidth = font.widthOfTextAtSize(numberStr, numberFontSize); | |
| const numberX = dateTimeX + dateTimeTextWidth + 3; | |
| page.drawText(numberStr, { | |
| x: numberX, | |
| y: footerY, | |
| size: numberFontSize, | |
| font: font, | |
| color: blackColor, | |
| }); | |
| globalCardCounter++; // Увеличиваем счётчик ПОСЛЕ отрисовки карточки | |
| } | |
| } | |
| // Сохранение и скачивание PDF | |
| const pdfBytes = await pdfDoc.save(); | |
| const blob = new Blob([pdfBytes], { type: 'application/pdf' }); | |
| const url = URL.createObjectURL(blob); | |
| const link = document.createElement('a'); | |
| link.href = url; | |
| link.download = 'loto_cards.pdf'; | |
| link.style.display = 'none'; | |
| document.body.appendChild(link); | |
| link.click(); | |
| // Очистка: удаление временного URL и ссылки | |
| URL.revokeObjectURL(url); | |
| document.body.removeChild(link); | |
| } catch (error) { | |
| console.error('Ошибка при генерации PDF:', error); | |
| alert('Произошла ошибка при создании PDF. Проверьте консоль для деталей.'); | |
| } | |
| }; | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment