Free Crochet Graph Pattern Maker

Free Crochet Graph Pattern Maker – KnotToYarn

🧶 Free Crochet Graph Pattern Maker

Design tapestry crochet / graphghan patterns — paint cells, upload a photo to auto-convert, then download your chart. Each square = 1 SC stitch.

Size ×
Tool
Color
Stats
Width40 sc
Height40 rows
Total stitches1,600
Colored cells0
Colors used0
Color Legend
Paint cells to see colors used.
Tips
✎ Click + drag to paint
▤ Fill fills a region
⚬ Pick color from grid
Right-click = erase
P/F/E/I = keyboard shortcuts
How to read this pattern: Each square = 1 single crochet (SC). Read the chart bottom to top, right-to-left on odd rows, left-to-right on even rows (standard tapestry crochet direction). Download your chart and keep it open while you crochet!
'); } // ── IMAGE UPLOAD → PATTERN ──────────────────────────────────────── function ktyOpenUpload() { const p = document.getElementById('ktyUploadPanel'); p.style.display = p.style.display==='none' ? 'block' : 'none'; } function ktyPickFile() { document.getElementById('ktyFileInput').click(); } function nearestColor(r,g,b, subset) { let best=subset[0], bestD=Infinity; subset.forEach(hex => { const pr=parseInt(hex.slice(1,3),16), pg=parseInt(hex.slice(3,5),16), pb=parseInt(hex.slice(5,7),16); // perceptual weighting const d = 2*(r-pr)**2 + 4*(g-pg)**2 + 3*(b-pb)**2; if(d p.c); const reader = new FileReader(); reader.onload = ev => { const img = new Image(); img.onload = () => { // Sample at 8x resolution so each cell averages 64 pixels const SCALE = 8; const tmp = document.createElement('canvas'); tmp.width = gW * SCALE; tmp.height = gH * SCALE; const tc = tmp.getContext('2d'); // Fill white first so transparent pixels -> white, not black tc.fillStyle = '#FFFFFF'; tc.fillRect(0, 0, tmp.width, tmp.height); tc.drawImage(img, 0, 0, tmp.width, tmp.height); // Read ALL pixels at once const data = tc.getImageData(0, 0, tmp.width, tmp.height).data; const W = tmp.width; // For each grid cell, average all SCALExSCALE pixels const pixelColors = []; for (let row = 0; row < gH; row++) { for (let col = 0; col < gW; col++) { let rSum = 0, gSum = 0, bSum = 0, aSum = 0; for (let sy = 0; sy < SCALE; sy++) { for (let sx = 0; sx < SCALE; sx++) { const i = ((row * SCALE + sy) * W + (col * SCALE + sx)) * 4; rSum += data[i]; gSum += data[i+1]; bSum += data[i+2]; aSum += data[i+3]; } } const n = SCALE * SCALE; const avgA = aSum / n; const matched = (avgA < 64) ? '#FFFFFF' : nearestColor(rSum / n, gSum / n, bSum / n, palColors); pixelColors.push({r: row, c: col, matched}); } } // Detect background color by sampling the 4 corner cells let bgColor = '#FFFFFF'; if (skipBg) { const cornerCells = [[0,0],[0,gW-1],[gH-1,0],[gH-1,gW-1]]; const cCount = {}; cornerCells.forEach(([crow, ccol]) => { let rS=0,gS=0,bS=0,aS=0; for (let sy=0;sy b[1]-a[1])[0][0]; } // Count usage of non-background colors, pick top N const usage = {}; pixelColors.forEach(({matched}) => { if (skipBg && matched === bgColor) return; usage[matched] = (usage[matched]||0) + 1; }); const topColors = Object.entries(usage).sort((a,b) => b[1]-a[1]).slice(0, maxColors).map(e => e[0]); // Paint every cell: background -> white (empty), others -> nearest top color pixelColors.forEach(({r,c,matched}) => { if (skipBg && matched === bgColor) { cells[r][c] = '#FFFFFF'; } else if (topColors.includes(matched)) { cells[r][c] = matched; } else { // Re-map to nearest of the chosen top colors const pr = parseInt(matched.slice(1,3),16); const pg = parseInt(matched.slice(3,5),16); const pb = parseInt(matched.slice(5,7),16); cells[r][c] = nearestColor(pr, pg, pb, topColors); } }); render(); updateStats(); const used = new Set(cells.flat().filter(c => c !== '#FFFFFF')).size; status.textContent = 'Done! Pattern created using ' + used + ' colors. You can paint over any cells to adjust.'; input.value = ''; }; img.src = ev.target.result; }; reader.readAsDataURL(file); } // ── KEYBOARD SHORTCUTS ──────────────────────────────────────────── document.addEventListener('keydown', e => { if (e.target.tagName==='INPUT'||e.target.tagName==='SELECT') return; if (e.key==='p'||e.key==='P') ktyTool('paint'); if (e.key==='f'||e.key==='F') ktyTool('fill'); if (e.key==='e'||e.key==='E') ktyTool('erase'); if (e.key==='i'||e.key==='I') ktyTool('pick'); }); // ── EXPOSE FOR BUTTONS ──────────────────────────────────────────── function ktyPreset(w, h) { gW = w; gH = h; document.getElementById('ktyW').value = w; document.getElementById('ktyH').value = h; initGrid(false); } window.ktyTool=ktyTool; window.ktySetColor=ktySetColor; window.ktyApplySize=ktyApplySize; window.ktyClear=ktyClear; window.ktyDownload=ktyDownload; window.ktyPrint=ktyPrint; window.ktyOpenUpload=ktyOpenUpload; window.ktyPickFile=ktyPickFile; window.ktyProcessImage=ktyProcessImage; window.ktyPreset=ktyPreset; // ── START ───────────────────────────────────────────────────────── initGrid(false); })();