I coded a ASCII art generator in Node JS
Tags: node, javascript, ascii, art
Published: 2021-04-17 12:00:27

Hi 👋
In this article you'll see how to make a ASCII art generator from an image

## The result:

![The img used](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f2qum6rrzj69wjycg63h.png)

![result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4k2g7jt8q80vm5wvll5c.png)

but first

## what is this ASCII art ?
> ASCII art is a graphic design technique that uses computers for presentation and consists of pictures pieced together from the 95 printable characters defined by the ASCII Standard from 1963 and ASCII compliant character sets with proprietary extended characters

## Prerequisites
I'll use those packages:
For this project I wanted to use my JS knowledge, so I'll use: 
* [Node js](https://nodejs.org/en/)
* [Sharp](https://sharp.pixelplumbing.com/)
* [readline-sync](https://www.npmjs.com/package/readline-sync)

```npm
npm i sharp readline-sync
```

## Steps for the program:
When I was thinking of ASCII art, I imagined that it was made with some sort of edge detection algorythm, Oh boy I was wrong, for making an ASCII art you from a picture, you'll need to:
* turn the image into a black and white image
* resize the image 
* replace all the black and white pixel by character defines for brightness and darkness / shadow

Alright let's get into it, I'll first create a package.json file by doing a:
```
npm init
```

Once I have my package, I'll create a index.js file, this is where my code will be.

Alright once that done, I'll import all the dependencies necessary for this project like this: 
```javascript
const sharp = require("sharp");
const readlineSync = require("readline-sync");
const fs = require("fs");
```
then let's first ask the user the image that it want to convert
### Get the user input
For this I'll create function called **loadFileFromPath** and into it I will get the user input like this: 
```javascript
var filePath = readlineSync.question("What's the file path ");
```

**Why do we need readlineSync?**
You probably wondering what is tha readlineSync package. This allow us to enter a input in the console Synchronously since node JS is Asynchronous, the code continues it's execution, so we use this **to wait for the user input**

then I'll test if the path is correct or not with a try/catch like this:
```javascript
try {
    const file = await sharp(filePath);
    return file;
  } catch (error) {
    console.error(error);
  }
```

and the all function looks like this:
```javascript
const loadFileFromPath = async () => {
  var filePath = readlineSync.question("What's the file path ");
  try {
    const file = await sharp(filePath);
    return file;
  } catch (error) {
    console.error(error);
  }
};
```

### Convert to black and white
For this I'll first create a function named **convertToGrayscale** with a **path parameter** like this:
```javascript
const convertToGrayscale = async (path) => {
  // code
};
```

in this function I'll load the img and change it's color values to B&W and finally I return the b&w result
```javascript 
const convertToGrayscale = async (path) => {
 const img = await path; 
 const bw = await img.gamma().greyscale();
 return bw;
};
```
### Resizing the image
For this I'll first create a function named **resizeImg** with **bw and newWidth = 100 parameters** like this:
```javascript 
const resizeImg = async (bw, newWidth = 100) => {
  //code
};
```
t
I'll then await for the bw image and await the blackAndWhite wariable result then get it's metadatas for getting access to the sizes properties
```javascript 
const resizeImg = async (bw, newWidth = 100) => {
  const blackAndWhite = await bw;
  const size = await blackAndWhite.metadata();
};
```   
we then calculate the ratio of the image, for that we just divide the width by the height and we get the ratio. Then we calculate our new height with: 
```javascript 
const ratio = size.width / size.height;
newHeight = parseInt(newWidth * ratio);
```  

Then we finally resize the image and return it like this:
```javascript 
const resized = await blackAndWhite.resize(newWidth, newHeight, {
    fit: "outside",
  });
return resized;
```
The whole function should look like this: 
```javascript 
const resizeImg = async (bw, newWidth = 100) => {
  const blackAndWhite = await bw;
  const size = await blackAndWhite.metadata();
  const ratio = size.width / size.height;
  newHeight = parseInt(newWidth * ratio);
  const resized = await blackAndWhite.resize(newWidth, newHeight, {
    fit: "outside",
  });

  return resized;
};
```  
### Convert pixels to ASCII characters
For this I'll first create a function named **pixelToAscii** with a **img parameter** like this:
```javascript
const pixelToAscii = async (img) => {
 //code
};
```

then I'll create variable to hold the img with an await keyword. I'll then get the pixels Array of the image and store it in a variable named pixels.

```javascript
var newImg = await img;
const pixels = await newImg.raw().toBuffer();
};
```

Then I'll create a variable named characters which gonna contains an empty String. I then go through each pixel from the pixels array and the ASCII character to the string I created earlier:
```javascript
characters = "";
pixels.forEach((pixel) => {
    characters = characters + ASCII_CHARS[Math.floor(pixel * interval)];
  });
```
You might notice two global variable that I did not mentioned yet:
* interval
* ASCII_CHARS

I'll explain you what both those variables are:
* ASCII_CHARS is the variable that hold all the ASCII characters:
```javascript
ASCII_CHARS = "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,\"^`'. ".split(
  ""
);
```
* interval is the ascii that should be assign to the color (intensity)
```javascript
charLength = ASCII_CHARS.length;
interval = charLength / 256;
```

Okay now we know what are those variable let's get back to the function, it should now look like this:
```javascript
const pixelToAscii = async (img) => {
  var newImg = await img;
  const pixels = await newImg.raw().toBuffer();
  characters = "";
  pixels.forEach((pixel) => {
    characters = characters + ASCII_CHARS[Math.floor(pixel * interval)];
  });
  return characters;
};
```

Now we have all our steps, let's create the core of the app:

### The main function
For this I'll first create a function named **main** with **newWidth = 100 parameters** like this:
```javascript
const main = async (newWidth = 100) => {
  //code
};
```
in this function I'll create a function named: **newImgData* which gonna be equal to all those function we created earlier nested like so:
```javascript
const main = async (newWidth = 100) => {
  const newImgData = await pixelToAscii(
    resizeImg(convertToGrayscale(loadFileFromPath()))
  );
};
```
then I'll get the length of my characters and create an empty variable named **ASCII** like this:
```javascript
const pixels = newImgData.length;
let ASCII = "";
```
then I'll loop through the pixels list like so:
```javascript
for (i = 0; i < pixels; i += newWidth) {
    let line = newImgData.split("").slice(i, i + newWidth);
    ASCII = ASCII + "\n" + line;
  }
```

so basicaly I'm setting the line splitting. I'm getting the size of newWidth and then slice the array as a line of this newWidth
and then add the "\n" character to go to the next line.

### Export to a text file
And finally in the same function I had this to save the text in a text file

```javascript
 setTimeout(() => {
    fs.writeFile("output.txt", ASCII, () => {
      console.log("done");
    });
  }, 5000);
```

and VOILA we got a ASCII art generator from image, oh and of course don't forget the **main()** to first call the function

the complete code should look like this: 
```javascript
const sharp = require("sharp");
const readlineSync = require("readline-sync");
const fs = require("fs");

ASCII_CHARS = "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,\"^`'. ".split(
  ""
);
charLength = ASCII_CHARS.length;
interval = charLength / 256;
var newHeight = null;
const main = async (newWidth = 100) => {
  const newImgData = await pixelToAscii(
    resizeImg(convertToGrayscale(loadFileFromPath()))
  );
  const pixels = newImgData.length;
  let ASCII = "";
  for (i = 0; i < pixels; i += newWidth) {
    let line = newImgData.split("").slice(i, i + newWidth);
    ASCII = ASCII + "\n" + line;
  }

  setTimeout(() => {
    fs.writeFile("output.txt", ASCII, () => {
      console.log("done");
    });
  }, 5000);
};

const convertToGrayscale = async (path) => {
  const img = await path;
  const bw = await img.gamma().greyscale();
  return bw;
};

const resizeImg = async (bw, newWidth = 100) => {
  const blackAndWhite = await bw;
  const size = await blackAndWhite.metadata();
  const ratio = size.width / size.height;
  newHeight = parseInt(newWidth * ratio);
  const resized = await blackAndWhite.resize(newWidth, newHeight, {
    fit: "outside",
  });

  return resized;
};

const pixelToAscii = async (img) => {
  var newImg = await img;
  const pixels = await newImg.raw().toBuffer();
  characters = "";
  pixels.forEach((pixel) => {
    characters = characters + ASCII_CHARS[Math.floor(pixel * interval)];
  });
  return characters;
};

const loadFileFromPath = async () => {
  var filePath = readlineSync.question("What's the file path ");
  try {
    const file = await sharp(filePath);
    return file;
  } catch (error) {
    console.error(error);
  }
};
main();
```



## What I learned throughout this project ?
This project was really interesting to make I first discovered that you can nest functions, I also discovered, how does ASCII art was working, I learned about node js Asynchronous problem for user input and how to solve this problem, and finally how to do some basic image manipulation.

## Conclusion
Thank you for reading this, I hope this helped you in any way
You can follow me on:
[instagram](https://www.instagram.com/atndesign/)
[youtube](https://www.youtube.com/channel/UCrzBRMk7vilOTzONeLrmJEA)

Hope you'll have an awesome day / Having an awesome day
and don't forget keep learning