Xavstarks/pinsequence-string-art-generator
0
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 