├── .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 |
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 | 
22 |
23 | 
24 |
25 | 
26 |
27 | 
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 |
--------------------------------------------------------------------------------