├── .gitignore
├── config.js
├── package.json
├── index.html
├── src
├── main.js
└── cocoscii.js
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | jspm_packages
2 |
--------------------------------------------------------------------------------
/config.js:
--------------------------------------------------------------------------------
1 | System.config({
2 | "baseURL": "/",
3 | "transpiler": "babel",
4 | "paths": {
5 | "*": "*.js"
6 | }
7 | });
8 |
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "jspm": {
3 | "directories": {
4 | "lib": ".",
5 | "packages": "jspm_packages"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Cocascii
6 |
7 |
8 |
9 |
10 |
11 |
15 |
16 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import cocoscii from "./cocoscii";
2 |
3 |
4 | const closeImg = cocoscii(`
5 | · · · 1 1 1 · · · ·
6 | · · 1 · · · · · 1 · ·
7 | · 1 · · · · · · · 1 ·
8 | 1 · · 2 · · · 3 · · 1
9 | 1 · · · # · # · · · 1
10 | 1 · · · · # · · · · 1
11 | 1 · · · # · # · · · 1
12 | 1 · · 3 · · · 2 · · 1
13 | · 1 · · · · · · · 1 ·
14 | · · 1 · · · · · · · ·
15 | · · · 1 1 1 1 1 · · ·
16 | `, (idx, style) => {
17 |
18 | if (idx === 0) {
19 | style.fill = "#000";
20 | } else {
21 | style.stroke = "#fff";
22 | }
23 |
24 | });
25 |
26 | const lockImg = cocoscii(`
27 | · · · · · · · · · · · · · · ·
28 | · · · · 1 · · · · · · 1 · · ·
29 | · · · · · · · · · · · · · · ·
30 | · · · · · · · · · · · · · · ·
31 | · · · · · · · · · · · · · · ·
32 | · · 3 · 1 · · · · · · 1 · 4 ·
33 | · · · · · · · · · · · · · · ·
34 | · · · · · · A · · A · · · · ·
35 | · · · · 1 · · · · · · 1 · · ·
36 | · · · · · · · C D · · · · · ·
37 | · · · · · · A · · A · · · · ·
38 | · · · · · · · · · · · · · · ·
39 | · · · · · · · B E · · · · · ·
40 | · · · · · · · · · · · · · · ·
41 | · · 6 · · · · · · · · · · 5 ·
42 | `, (idx, style) => {
43 |
44 | if (idx === 0) {
45 | style.fill = "";
46 | style.stroke = "#000";
47 | } else if (idx === 1){
48 | style.fill = "#000"
49 | style.stroke = "";
50 | } else {
51 | style.fill = "#fff";
52 | }
53 |
54 | });
55 |
56 | // Uh oh, we got a problem. Not pixel aligned.
57 | // args = [x + 3 / width, y + 3 / height, 1 / (width + 4), 1 / (height + 4)]
58 | // will fix it: buuut, not generalized.
59 | const gridImg = cocoscii(`
60 | 1 . 3 . 5 . 7
61 | . 9 . B . D .
62 | F . H . J . L
63 | . N . P . R .
64 | T . V . X . Z
65 | . b . d . f .
66 | h . j . l . n
67 | `, (idx, style) => { style.stroke="#000"; }, 8);
68 |
69 | document.body.appendChild(closeImg);
70 | document.body.appendChild(lockImg);
71 | document.body.appendChild(gridImg);
72 |
73 | export default {};
74 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Cocoscii
2 |
3 | This is a javascript (ES2015) version of the neat-o hack [ASCIImage](http://asciimage.org/) by Charles Parnot. ASCIImage lets you create icons by defining some control points for drawing lines, paths, and circles. You can also style each shape individually. [Read all about the original project](http://cocoamine.net/blog/2015/03/20/replacing-photoshop-with-nsstring/), then come back here.
4 |
5 | This is the JavaScript version by Mr Speaker, and creates standard DOM images (see [cocoscii.js](https://github.com/mrspeaker/cocoscii/blob/master/src/cocoscii.js) for the ES6). Usage looks like this:
6 |
7 | const closeImg = cocoscii(`
8 | · · · · 1 1 1 · · · ·
9 | · · 1 · · · · · 1 · ·
10 | · 1 · · · · · · · 1 ·
11 | 1 · · 2 · · · 3 · · 1
12 | 1 · · · # · # · · · 1
13 | 1 · · · · # · · · · 1
14 | 1 · · · # · # · · · 1
15 | 1 · · 3 · · · 2 · · 1
16 | · 1 · · · · · · · 1 ·
17 | · · 1 · · · · · 1 · ·
18 | · · · 1 1 1 1 1 · · ·
19 | `, (idx, style) => {
20 |
21 | if (idx === 0) {
22 | style.fill = "#000";
23 | } else {
24 | style.stroke = "#fff";
25 | }
26 |
27 | });
28 |
29 | This produces images like:
30 |
31 | 
32 | 
33 |
34 | The format is pretty funny: and a bit complicated to start with - so go read the original docs! The basics are, you fill a grid with the ordered markers `123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnpqrstuvwxyz`. Any other characters are ignored.
35 |
36 | If the numbers are sequential, they become a path. If you skip a number, then a new path starts. If a number is repeated twice, it becomes a line. If a number is repeated more than twice, it becomes a circle that fits inside the bounding box of all the points.
37 |
38 | The callback function is called once per defined shape. You can use that to alter the fillStyle, strokeStyle, and lineWidth for each shape.
39 |
40 | ## TODO:
41 |
42 | * stroke width too thin compared to ASCIImage
43 | * implement "ASCIIContextShouldClose"
44 |
45 | ## Building
46 |
47 | No dependencies, so run in a ES2015-y browser - otherwise, build with `jspm`:
48 |
49 | jspm install
50 |
51 | This project uses the `Babel` transpiler, but `Traceur` should work too. The only important file is `src/cocoscii.js`.
52 |
53 |
--------------------------------------------------------------------------------
/src/cocoscii.js:
--------------------------------------------------------------------------------
1 | /*
2 | rep: A "\n"-seperated ascii representation of the image.
3 | styles: A function to mutate the style dictionary. (shapeIndex: Number, dictionary: Object) => {}
4 | scale: Factor to scale the final image by.
5 | */
6 | function cocoscii (rep, styles = (idx, dict) => {}, scale = 4) {
7 |
8 | const order = "123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnpqrstuvwxyz";
9 |
10 | const rows = rep
11 | .split("\n")
12 | .filter(r => !r.match(/^\s*$/))
13 | .map(r => r.split("").filter(ch => ch != " "));
14 |
15 | const width = rows.reduce((max, row) => Math.max(max, row.length), 0);
16 | const height = rows.length;
17 |
18 | // Get the control points
19 | const points = rows
20 | .map((r, y) => r.map((ch, x) => { return {
21 | idx: order.indexOf(ch),
22 | ch, x, y
23 | }}))
24 | .reduce((flt, r) => [...flt, ...r], []) // Flatten
25 | .filter(p => p.idx != -1)
26 | .sort((p1, p2) => p1.idx - p2.idx);
27 |
28 | // Turn them into shapes
29 | const shapes = makeShapes(points);
30 |
31 | // ...and draw them
32 | const canvas = drawShapes(shapes, styles, width, height, scale);
33 |
34 | // Save to image
35 | const img = new Image();
36 | img.src = canvas.toDataURL("image/png");
37 | return img;
38 |
39 | }
40 |
41 | function makeShapes ([head, ...tail]) {
42 |
43 | function newShape (p) {
44 | return {
45 | type: "dot",
46 | points: [p]
47 | };
48 | }
49 |
50 | const shapes = tail.reduce(({cur, shapes}, p) => {
51 |
52 | const prev = cur.points.slice(-1)[0];
53 |
54 | // Another point in the path
55 | if (p.idx === prev.idx + 1 && ["dot", "path"].indexOf(cur.type) != -1) {
56 | cur.points.push(p);
57 | cur.type = "path";
58 | }
59 | // Line or circle
60 | else if (p.idx === prev.idx) {
61 | cur.points.push(p);
62 | cur.type = cur.points.length < 3 ? "line" : "circle";
63 | }
64 | // New shape
65 | else {
66 | shapes.push(cur);
67 | cur = newShape(p);
68 | }
69 |
70 | return {cur, shapes};
71 |
72 | }, {cur: newShape(head), shapes:[]});
73 |
74 | // Don't forget final shape!
75 | return [...shapes.shapes, shapes.cur];
76 |
77 | }
78 |
79 | function drawShapes (shapes, styles, width, height, scale) {
80 |
81 | let styleDict = {
82 | fill: "#000",
83 | stroke: "",
84 | lineWidth: "1"
85 | };
86 |
87 | const canvas = document.createElement("canvas");
88 | const ctx = canvas.getContext("2d");
89 |
90 | canvas.width = width * scale;
91 | canvas.height = height * scale;
92 |
93 | function getBBox ([{x, y}, ...tail]) {
94 | return tail.reduce(({min, max}, {x, y}) => {
95 | if (x < min.x) min.x = x;
96 | if (x > max.x) max.x = x;
97 | if (y < min.y) min.y = y;
98 | if (y > max.y) max.y = y;
99 | return {min, max};
100 | }, {min: {x, y}, max: {x, y}});
101 | }
102 |
103 | ctx.save();
104 | ctx.scale(scale, scale);
105 |
106 | shapes.forEach(({type, points}, i) => {
107 |
108 | styles(i, styleDict); // Apply function to mutate the shape's styles
109 |
110 | const [{x, y}, ...tail] = points;
111 | const {fill, stroke, lineWidth} = styleDict;
112 |
113 | ctx.fillStyle = fill;
114 | ctx.strokeStyle = stroke;
115 | ctx.lineWidth = lineWidth;
116 |
117 | switch (type) {
118 |
119 | case "dot":
120 | // TODO: positioning is not correct. See "gridImg" test case
121 | const args = [x, y, 1 / width, 1 / height];
122 | if (fill) ctx.fillRect(...args);
123 | if (stroke) ctx.strokeRect(...args);
124 | break;
125 |
126 | case "line":
127 | case "path":
128 | ctx.beginPath();
129 | ctx.moveTo(x, y);
130 | tail.forEach(({x, y}) => ctx.lineTo(x, y));
131 |
132 | if (fill) ctx.fill();
133 | if (stroke) ctx.stroke();
134 | break;
135 |
136 | case "circle":
137 | // Get "bounding box" of the circle
138 | const {min, max} = getBBox(points);
139 | const [w, h] = [max.x - min.x, max.y - min.y];
140 | const circ = Math.max(w, h);
141 |
142 | // Draw an elipse to fit
143 | ctx.save();
144 | ctx.translate(min.x + w / 2, min.y + h / 2);
145 | ctx.scale(w / circ, h / circ);
146 | ctx.beginPath();
147 | ctx.arc(0, 0, circ / 2, 0, Math.PI * 2, false);
148 | ctx.restore();
149 |
150 | if (fill) ctx.fill();
151 | if (stroke) ctx.stroke();
152 | break;
153 |
154 | }
155 |
156 | });
157 |
158 | ctx.restore();
159 |
160 | return canvas;
161 |
162 | }
163 |
164 | export default cocoscii;
165 |
--------------------------------------------------------------------------------