├── .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 | ![closeicon](https://cloud.githubusercontent.com/assets/129330/6765643/432159c6-cfc0-11e4-80e5-5c7e2071b0a1.png) 32 | ![lockicon](https://cloud.githubusercontent.com/assets/129330/6767176/cd748e3e-cff9-11e4-8ed7-d8b5467a8bfb.png) 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 | --------------------------------------------------------------------------------