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.
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);
})();