CoolFace
Apppublic

Xavstarks/pinsequence-string-art-generator

sourceHugging Faceupdated 1y agoView on Hugging Face
0likes
index.html361 linesDownload Raw Back to root
1<!DOCTYPE html>2<html lang="en" class="dark">3<head>4    <meta charset="UTF-8">5    <meta name="viewport" content="width=device-width, initial-scale=1.0">6    <title>PinSequence String Art Generator</title>7    <script src="https://cdn.tailwindcss.com"></script>8    <script src="https://unpkg.com/feather-icons"></script>9    <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>10    <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf-lib/1.17.1/pdf-lib.min.js"></script>11    <script src="https://unpkg.com/downloadjs@1.4.7"></script>12    <script>13        tailwind.config = {14            darkMode: 'class',15            theme: {16                extend: {17                    colors: {18                        primary: {19                            500: '#6366f1',20                        },21                        secondary: {22                            500: '#10b981',23                        }24                    }25                }26            }27        }28    </script>29    <style>30        .canvas-container {31            position: relative;32            margin: 0 auto;33        }34        canvas {35            border: 1px solid #e5e7eb;36            border-radius: 0.5rem;37            background-color: white;38        }39        .dark canvas {40            border-color: #374151;41            background-color: #1f2937;42        }43    </style>44</head>45<body class="bg-gray-100 dark:bg-gray-900 min-h-screen">46    <div class="container mx-auto px-4 py-8">47        <header class="flex justify-between items-center mb-8">48            <h1 class="text-3xl font-bold text-gray-800 dark:text-white">49                <span class="text-primary-500">Pin</span><span class="text-secondary-500">Sequence</span> Art50            </h1>51            <button id="themeToggle" class="p-2 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200">52                <i data-feather="moon"></i>53            </button>54        </header>55 56        <div class="grid grid-cols-1 lg:grid-cols-2 gap-8">57            <div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">58                <h2 class="text-xl font-semibold text-gray-800 dark:text-white mb-4">Input Parameters</h2>59                60                <div class="mb-4">61                    <label for="sequence" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">62                        Number Sequence (comma separated)63                    </label>64                    <textarea id="sequence" rows="3" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:text-white" placeholder="1,3,5,2,4,6,1,3,5..."></textarea>65                </div>66 67                <div class="grid grid-cols-2 gap-4 mb-4">68                    <div>69                        <label for="pinCount" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">70                            Number of Pins71                        </label>72                        <input type="number" id="pinCount" min="3" max="100" value="12" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:text-white">73                    </div>74                    <div>75                        <label for="circleRadius" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">76                            Circle Radius77                        </label>78                        <input type="number" id="circleRadius" min="50" max="300" value="150" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:text-white">79                    </div>80                </div>81                <div class="mb-4">82                    <label for="imageUpload" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">83                        Background Image (optional)84                    </label>85                    <input type="file" id="imageUpload" accept="image/*" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:text-white">86                </div>87 88                <div class="grid grid-cols-2 gap-4 mb-6">89<div>90                        <label for="lineColor" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">91                            Line Color92                        </label>93                        <input type="color" id="lineColor" value="#6366f1" class="w-full h-10 border border-gray-300 dark:border-gray-600 rounded-md cursor-pointer">94                    </div>95                    <div>96                        <label for="pinColor" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">97                            Pin Color98                        </label>99                        <input type="color" id="pinColor" value="#10b981" class="w-full h-10 border border-gray-300 dark:border-gray-600 rounded-md cursor-pointer">100                    </div>101                </div>102 103                <div class="flex flex-wrap gap-2">104                    <button id="generateBtn" class="px-4 py-2 bg-primary-500 text-white rounded-md hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800">105                        Generate Art106                    </button>107                    <button id="randomBtn" class="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-white rounded-md hover:bg-gray-300 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800">108                        Random Sequence109                    </button>110                    <button id="downloadBtn" class="px-4 py-2 bg-secondary-500 text-white rounded-md hover:bg-secondary-600 focus:outline-none focus:ring-2 focus:ring-secondary-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800">111                        Download PDF112                    </button>113                </div>114            </div>115 116            <div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">117                <h2 class="text-xl font-semibold text-gray-800 dark:text-white mb-4">String Art Preview</h2>118                <div class="canvas-container">119                    <canvas id="artCanvas" width="500" height="500"></canvas>120                </div>121                <div id="errorMsg" class="text-red-500 dark:text-red-400 mt-2 hidden"></div>122            </div>123        </div>124    </div>125 126    <script>127        document.addEventListener('DOMContentLoaded', function() {128            feather.replace();129            130            // Theme toggle131            const themeToggle = document.getElementById('themeToggle');132            themeToggle.addEventListener('click', function() {133                document.documentElement.classList.toggle('dark');134                const icon = themeToggle.querySelector('i');135                if (document.documentElement.classList.contains('dark')) {136                    feather.icons.sun.replace(icon);137                } else {138                    feather.icons.moon.replace(icon);139                }140            });141 142            // Canvas setup143            const canvas = document.getElementById('artCanvas');144            const ctx = canvas.getContext('2d');145            let pinPositions = [];146            let currentSequence = [];147            148            // Generate random sequence149            document.getElementById('randomBtn').addEventListener('click', function() {150                const pinCount = parseInt(document.getElementById('pinCount').value);151                const sequenceLength = Math.max(10, pinCount * 2);152                let randomSeq = [];153                for (let i = 0; i < sequenceLength; i++) {154                    randomSeq.push(Math.floor(Math.random() * pinCount) + 1);155                }156                document.getElementById('sequence').value = randomSeq.join(',');157                generateArt();158            });159 160            // Generate art161            document.getElementById('generateBtn').addEventListener('click', generateArt);162            163            // Download PDF164            document.getElementById('downloadBtn').addEventListener('click', async function() {165                try {166                    if (currentSequence.length === 0) {167                        throw new Error('Please generate art first');168                    }169                    170                    // Create PDF171                    const { PDFDocument, rgb } = PDFLib;172                    const pdfDoc = await PDFDocument.create();173                    const page = pdfDoc.addPage([600, 600]);174                    175                    // Draw circle and pins176                    const centerX = 300;177                    const centerY = 300;178                    const radius = parseInt(document.getElementById('circleRadius').value);179                    180                    page.drawCircle({181                        x: centerX,182                        y: centerY,183                        size: radius,184                        borderWidth: 1,185                        borderColor: rgb(0.5, 0.5, 0.5),186                    });187                    188                    const pinCount = pinPositions.length;189                    for (let i = 0; i < pinCount; i++) {190                        const pin = pinPositions[i];191                        page.drawCircle({192                            x: centerX + pin.x,193                            y: centerY + pin.y,194                            size: 3,195                            color: rgb(0, 0, 0),196                        });197                    }198                    199                    // Draw lines200                    const lineColorHex = document.getElementById('lineColor').value;201                    const lineColor = [202                        parseInt(lineColorHex.substr(1, 2), 16) / 255,203                        parseInt(lineColorHex.substr(3, 2), 16) / 255,204                        parseInt(lineColorHex.substr(5, 2), 16) / 255,205                    ];206                    207                    for (let i = 0; i < currentSequence.length - 1; i++) {208                        const fromPin = pinPositions[currentSequence[i] - 1];209                        const toPin = pinPositions[currentSequence[i + 1] - 1];210                        211                        page.drawLine({212                            start: { x: centerX + fromPin.x, y: centerY + fromPin.y },213                            end: { x: centerX + toPin.x, y: centerY + toPin.y },214                            thickness: 1,215                            color: rgb(lineColor[0], lineColor[1], lineColor[2]),216                        });217                    }218                    219                    // Save PDF220                    const pdfBytes = await pdfDoc.save();221                    download(pdfBytes, "string-art.pdf", "application/pdf");222                } catch (error) {223                    showError(error.message);224                }225            });226            let backgroundImage = null;227 228            // Handle image upload229            document.getElementById('imageUpload').addEventListener('change', function(e) {230                const file = e.target.files[0];231                if (file) {232                    const reader = new FileReader();233                    reader.onload = function(event) {234                        const img = new Image();235                        img.onload = function() {236                            backgroundImage = img;237                            generateArt();238                        };239                        img.src = event.target.result;240                    };241                    reader.readAsDataURL(file);242                }243            });244 245            function generateArt() {246try {247                    hideError();248                    const sequenceInput = document.getElementById('sequence').value.trim();249                    if (!sequenceInput) {250                        throw new Error('Please enter a number sequence');251                    }252                    253                    currentSequence = sequenceInput.split(',').map(Number);254                    const pinCount = parseInt(document.getElementById('pinCount').value);255                    const radius = parseInt(document.getElementById('circleRadius').value);256                    const lineColor = document.getElementById('lineColor').value;257                    const pinColor = document.getElementById('pinColor').value;258                    259                    // Validate sequence260                    for (let num of currentSequence) {261                        if (isNaN(num) || num < 1 || num > pinCount) {262                            throw new Error(`All numbers must be between 1 and ${pinCount}`);263                        }264                    }265                    266                    // Calculate pin positions267                    pinPositions = [];268                    const centerX = canvas.width / 2;269                    const centerY = canvas.height / 2;270                    271                    for (let i = 0; i < pinCount; i++) {272                        const angle = (i * (2 * Math.PI / pinCount)) - Math.PI/2;273                        pinPositions.push({274                            x: radius * Math.cos(angle),275                            y: radius * Math.sin(angle)276                        });277                    }278                    // Draw on canvas279                    ctx.clearRect(0, 0, canvas.width, canvas.height);280                    281                    // Draw background image if exists282                    if (backgroundImage) {283                        // Calculate dimensions to maintain aspect ratio284                        const hRatio = canvas.width / backgroundImage.width;285                        const vRatio = canvas.height / backgroundImage.height;286                        const ratio = Math.min(hRatio, vRatio);287                        const centerShiftX = (canvas.width - backgroundImage.width * ratio) / 2;288                        const centerShiftY = (canvas.height - backgroundImage.height * ratio) / 2;289                        290                        ctx.drawImage(291                            backgroundImage, 292                            0, 0, backgroundImage.width, backgroundImage.height,293                            centerShiftX, centerShiftY, 294                            backgroundImage.width * ratio, backgroundImage.height * ratio295                        );296                    }297                    298                    // Draw circle299ctx.beginPath();300                    ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);301                    ctx.strokeStyle = backgroundImage ? 'rgba(209, 213, 219, 0.7)' : '#d1d5db';302ctx.stroke();303                    304                    // Draw pins305                    for (let i = 0; i < pinPositions.length; i++) {306                        const pin = pinPositions[i];307                        ctx.beginPath();308                        ctx.arc(centerX + pin.x, centerY + pin.y, 3, 0, 2 * Math.PI);309                        ctx.fillStyle = pinColor;310                        ctx.fill();311                        312                        // Optional: add pin numbers313                        ctx.font = '12px Arial';314                        ctx.fillStyle = '#000';315                        ctx.textAlign = 'center';316                        ctx.textBaseline = 'middle';317                        ctx.fillText(i + 1, centerX + pin.x, centerY + pin.y);318                    }319                    320                    // Draw lines321                    ctx.strokeStyle = lineColor;322                    ctx.lineWidth = backgroundImage ? 2 : 1; // Thicker lines when there's a background323for (let i = 0; i < currentSequence.length - 1; i++) {324                        const fromPin = pinPositions[currentSequence[i] - 1];325                        const toPin = pinPositions[currentSequence[i + 1] - 1];326                        327                        ctx.beginPath();328                        ctx.moveTo(centerX + fromPin.x, centerY + fromPin.y);329                        ctx.lineTo(centerX + toPin.x, centerY + toPin.y);330                        ctx.stroke();331                    }332                    333                } catch (error) {334                    showError(error.message);335                }336            }337            338            function showError(message) {339                const errorMsg = document.getElementById('errorMsg');340                errorMsg.textContent = message;341                errorMsg.classList.remove('hidden');342            }343            344            function hideError() {345                document.getElementById('errorMsg').classList.add('hidden');346            }347                    // Initial generation with default values348                    document.getElementById('sequence').value = '1,3,5,2,4,6,1,3,5,2,4,6';349                    // Load default sample image350                    const sampleImg = new Image();351                    sampleImg.onload = function() {352                        backgroundImage = sampleImg;353                        generateArt();354                    };355                    sampleImg.src = 'http://static.photos/black/500x500/1';356generateArt();357        });358    </script>359</body>360</html>361