├── .gitignore ├── example ├── xman.jpg ├── round.png ├── round2.png └── zhihu.png ├── src ├── style.css └── index.js ├── package.json ├── index.html └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | .vscode 3 | .DS_Store -------------------------------------------------------------------------------- /example/xman.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kitayoshi/string-knitting/HEAD/example/xman.jpg -------------------------------------------------------------------------------- /example/round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kitayoshi/string-knitting/HEAD/example/round.png -------------------------------------------------------------------------------- /example/round2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kitayoshi/string-knitting/HEAD/example/round2.png -------------------------------------------------------------------------------- /example/zhihu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kitayoshi/string-knitting/HEAD/example/zhihu.png -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'lato'; 3 | } 4 | 5 | svg { 6 | shape-rendering: optimizeSpeed; 7 | } 8 | 9 | #plate, 10 | #canvas { 11 | height: 600px; 12 | width: 600px; 13 | } 14 | 15 | #container { 16 | display: flex; 17 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "string-knitting", 3 | "version": "0.0.1", 4 | "description": "using string knitting image", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "npx http-server" 8 | }, 9 | "keywords": [ 10 | "string", 11 | "knitting", 12 | "art" 13 | ], 14 | "author": "midare", 15 | "license": "MIT", 16 | "dependencies": { 17 | "http-server": "^0.11.2", 18 | "svg.js": "^2.6.3", 19 | "zepto": "^1.2.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | String Knitting Art 8 | 9 | 10 | 11 | 12 |

String Knitting Art

13 | 14 |
15 |
16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # String Knitting 2 | 3 | Inspired by [A new way to knit (2016)](http://artof01.com/vrellis/works/knit.html), petros vrellis. 4 | 5 | Check for the online demonstration. 6 | 7 | ## Run 8 | 9 | `npm i` and `npm start`, check . 10 | 11 | Due to the CORS policy, you must host the index.html and other static files via http protocal. You may use other static-file-host-server other than `http-server`. 12 | 13 | ## Change image 14 | 15 | change image url in function.js at last line. 16 | 17 | ## Example 18 | 19 | four images in exmaple folder 20 | 21 | ![xman](https://user-images.githubusercontent.com/1732164/31550179-197fb1da-aff6-11e7-8466-f6f9f0392137.jpg) 22 | 23 | ![zhihu](https://user-images.githubusercontent.com/1732164/31550199-2bed1baa-aff6-11e7-88ca-d5930b5a1460.jpg) 24 | 25 | ![round](https://user-images.githubusercontent.com/1732164/31550224-4195f0a8-aff6-11e7-9708-db17a51c98c1.jpg) 26 | 27 | ![round2](https://user-images.githubusercontent.com/1732164/31550249-4f17af1e-aff6-11e7-8efc-28c3b689cc44.jpg) 28 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | function generatePinList(length, width, height) { 2 | const center = [width / 2, height / 2]; 3 | const radius = width / 2; 4 | const angleUnit = (Math.PI * 2) / length; 5 | 6 | const pinList = Array(length) 7 | .fill() 8 | .map((_, index) => { 9 | const angle = angleUnit * index - Math.PI / 2; // make pinList[0] at 12 o'clock 10 | 11 | const x = Math.round(center[0] + radius * Math.cos(angle)); 12 | const y = Math.round(center[1] + radius * Math.sin(angle)); 13 | 14 | if (x === width) { 15 | return [x - 1, y]; 16 | } 17 | 18 | if (y === height) { 19 | return [x, y - 1]; 20 | } 21 | 22 | return [x, y]; 23 | }); 24 | 25 | return pinList; 26 | } 27 | 28 | function isDotOnLine(dot, start, end) { 29 | // straight line 30 | if (end[0] - start[0] === 0) { 31 | if (dot[0] === end[0]) { 32 | return true; 33 | } else { 34 | return false; 35 | } 36 | } 37 | 38 | const slope = (end[1] - start[1]) / (end[0] - start[0]); 39 | const intercept = start[1] - slope * start[0]; 40 | 41 | const blockTopY = dot[1] + 0.5; 42 | const blockBottomY = dot[1] - 0.5; 43 | const blockLeftY = slope * (dot[0] - 0.5) + intercept; 44 | const blockRightY = slope * (dot[0] + 0.5) + intercept; 45 | 46 | if (Math.abs(slope) <= 1) { 47 | if ( 48 | (blockLeftY >= blockBottomY && blockLeftY <= blockTopY) || 49 | (blockRightY >= blockBottomY && blockRightY <= blockTopY) 50 | ) { 51 | return true; 52 | } else { 53 | return false; 54 | } 55 | } else { 56 | if (slope > 0) { 57 | if (blockLeftY > blockTopY || blockRightY < blockBottomY) { 58 | return false; 59 | } else { 60 | return true; 61 | } 62 | } else { 63 | if (blockRightY > blockTopY || blockLeftY < blockBottomY) { 64 | return false; 65 | } else { 66 | return true; 67 | } 68 | } 69 | } 70 | } 71 | 72 | function getPointListOnLine(start, end) { 73 | const pointList = []; 74 | const movementX = end[0] > start[0] ? 1 : -1; 75 | const movementY = end[1] > start[1] ? 1 : -1; 76 | 77 | let currentX = start[0]; 78 | let currentY = start[1]; 79 | 80 | let loopcount = 0; 81 | while ((currentX !== end[0] || currentY !== end[1]) && loopcount <= 1000) { 82 | pointList.push([currentX, currentY]); 83 | if (isDotOnLine([currentX + movementX, currentY], start, end)) { 84 | currentX += movementX; 85 | } else { 86 | currentY += movementY; 87 | } 88 | 89 | loopcount++; 90 | } 91 | pointList.push(end); 92 | 93 | return pointList; 94 | } 95 | 96 | // function getPointListOnLine(start, end) { 97 | // const pointList = []; 98 | // const dx = Math.abs(end[0] - start[0]); 99 | // const dy = -Math.abs(end[1] - start[1]); 100 | // const sx = start[0] < end[0] ? 1 : -1; 101 | // const sy = start[1] < end[1] ? 1 : -1; 102 | // let e = dx + dy; 103 | // let e2 = 0; 104 | 105 | // const a = [start[0], start[1]]; 106 | // while (true) { 107 | // pointList.push([a[0], a[1]]); 108 | // if (a[0] === end[0] && a[1] === end[1]) break; 109 | // e2 = 2 * e; 110 | // if (e2 > dy) { 111 | // e += dy; 112 | // a[0] += sx; 113 | // } 114 | // if (e2 < dx) { 115 | // e += dx; 116 | // a[1] += sy; 117 | // } 118 | // } 119 | 120 | // return pointList; 121 | // } 122 | 123 | function getImageData(imageData, dot) { 124 | const startIndex = (dot[1] * imageData.width + dot[0]) * 4; // rgba 125 | return [ 126 | imageData.data[startIndex], 127 | imageData.data[startIndex + 1], 128 | imageData.data[startIndex + 2], 129 | imageData.data[startIndex + 3] 130 | ]; 131 | } 132 | 133 | function reduceImageData(imageData, start, end) { 134 | const dotList = getPointListOnLine(start, end); 135 | 136 | dotList.forEach(dot => { 137 | const startIndex = (dot[1] * imageData.width + dot[0]) * 4; // rgba 138 | imageData.data[startIndex] += 50; 139 | 140 | if (imageData.data[startIndex] > 255) { 141 | imageData.data[startIndex] = 255; 142 | } 143 | }); 144 | } 145 | 146 | function getLineScore(imageData, start, end) { 147 | const dotList = getPointListOnLine(start, end); 148 | 149 | dotScoreList = dotList.map(dot => { 150 | const colorR = getImageData(imageData, dot)[0]; // r channel 151 | 152 | const dotScore = 1 - colorR / 255; // darker is higher 153 | 154 | return dotScore; 155 | }); 156 | 157 | const score = dotScoreList.reduce((a, b) => a + b, 0) / dotScoreList.length; 158 | 159 | return score; 160 | } 161 | 162 | function isLineDrawn(lineList, startPinIndex, endPinIndex) { 163 | const lineFound = lineList.find(line => { 164 | if ( 165 | (startPinIndex === line[0] && endPinIndex === line[1]) || 166 | (startPinIndex === line[1] && endPinIndex === line[0]) 167 | ) { 168 | return true; 169 | } 170 | 171 | return false; 172 | }); 173 | 174 | return Boolean(lineFound); 175 | } 176 | 177 | function isPinTooClose(pinList, startPinIndex, endPinIndex) { 178 | let pinDistance = Math.abs(endPinIndex - startPinIndex); 179 | pinDistance = 180 | pinDistance > pinList.length / 2 181 | ? pinList.length - pinDistance 182 | : pinDistance; 183 | 184 | if (pinDistance < 25) { 185 | return true; 186 | } 187 | 188 | return false; 189 | } 190 | 191 | function drawLine(plate, start, end) { 192 | const line = plate.line().stroke({ width: 0.5, opacity: 0.6 }); 193 | line.plot([start, end]); 194 | } 195 | 196 | const lineLimit = 1500; 197 | let lineCount = 0; 198 | 199 | let plate; 200 | let image; 201 | let imageData; 202 | let pinList; 203 | let lineList = []; 204 | 205 | function draw(plate, imageData, pinList, startPinIndex) { 206 | let endPinIndex; 207 | let highestScore = 0; 208 | 209 | pinList.forEach((pin, index) => { 210 | if ( 211 | startPinIndex === index || 212 | isLineDrawn(lineList, startPinIndex, index) || 213 | isPinTooClose(pinList, startPinIndex, index) 214 | ) { 215 | return; 216 | } 217 | 218 | const score = getLineScore(imageData, pinList[startPinIndex], pin); 219 | // console.log(startPinIndex, score); 220 | 221 | if (score > highestScore) { 222 | endPinIndex = index; 223 | highestScore = score; 224 | } 225 | }); 226 | 227 | lineCount++; 228 | if (lineCount <= lineLimit) { 229 | lineList.push([startPinIndex, endPinIndex]); 230 | drawLine(plate, pinList[startPinIndex], pinList[endPinIndex]); 231 | reduceImageData(imageData, pinList[startPinIndex], pinList[endPinIndex]); 232 | 233 | setTimeout(() => { 234 | draw(plate, imageData, pinList, endPinIndex); 235 | }, 3); 236 | } 237 | } 238 | 239 | function init() { 240 | const canvas = $("canvas")[0]; 241 | const ctx = canvas.getContext("2d"); 242 | ctx.drawImage(image, 0, 0); 243 | imageData = ctx.getImageData(0, 0, 600, 600); 244 | 245 | pinList = generatePinList(200, 600, 600); 246 | 247 | draw(plate, imageData, pinList, 0); 248 | 249 | // Array(600) 250 | // .fill() 251 | // .forEach((_, x) => { 252 | // Array(100) 253 | // .fill() 254 | // .forEach((_, y) => { 255 | // const color = getImageData(imageData, [x, y])[0].toString(16); 256 | 257 | // plate 258 | // .rect(1, 1) 259 | // .fill({ color: `#${color}${color}${color}` }) 260 | // .move(x, y); 261 | // }); 262 | // }); 263 | 264 | // pinList.forEach(startPin => { 265 | // pinList.forEach(endPin => { 266 | // if (lineCount <= lineLimit) { 267 | // drawLine(plate, startPin, endPin); 268 | // const score = getLineScore(imageData, startPin, endPin); 269 | 270 | // console.log(score); 271 | // } 272 | // lineCount++; 273 | // }); 274 | // }); 275 | } 276 | 277 | $(() => { 278 | plate = SVG("plate").size(600, 600); 279 | 280 | image = new Image(); 281 | image.src = "./example/xman.jpg"; 282 | image.onload = init; 283 | }); 284 | --------------------------------------------------------------------------------