Hvis du er like gammel som meg, så husker du kanskje lyden av et modem og de fete ASCII-tegningene som ønsket deg velkommen når du logget på en BBS.

  _  __           _                          _               
 | |/ /          | |                        | |              
 | ' /  ___    __| |  ___  _ __ ___    __ _ | | __ ___  _ __ 
 |  <  / _ \  / _` | / _ \| '_ ` _ \  / _` || |/ // _ \| '__|
 | . \| (_) || (_| ||  __/| | | | | || (_| ||   <|  __/| |   
 |_|\_\\___/  \__,_| \___||_| |_| |_| \__,_||_|\_\\___||_|

Som regel var dette håndlaget kunst, som sikkert tok timesvis å lage. Det har vi hverken tid eller evner til, så la oss heller spørre datamaskinen om litt hjelp. Hva skal til for å generere ASCII fra et bilde i nettleseren?

Først, et bilde

Det første vi må gjøre, er å kunne lese ut fargeinformasjon fra et bilde. Der kan <canvas>-elementet hjelpe oss. Canvas-APIet gir oss tilgang til hver enkelt piksel og hvilken farge de har. Vi laster inn et bilde på et eller annet vis (det er flere måter å gjøre det på), og tegner det til canvas.

Det ser slik ut:

// Last inn et bilde som er embeddet i siden.
// Alternativt så kan man be brukeren last opp et bilde
const rawImageElement = document.getElementById("sourceImage");
const canvas = document.getElementById('theCanvas');
const context = canvas.getContext('2d');
context.drawImage(rawImageElement, 0, 0, canvas.width, canvas.height)

Nå er vi klare til å hente ut fargene til hver piksel. Det gjør vi via et ImageData- objekt. Man skulle kanskje forvente at APIet ga deg tilgang til hver piksel i form av en to-dimensjonal matrise. Det vi konseptuelt sett ønsker oss er en getPixelAt(x, y)-funksjon som returnerer en representasjon av pikselen på det punktet.

Det kunne f.eks sett slik ut:

{ red: 1, green: 2, blue: 3, alpha: 0.5 }

Så heldige er vi ikke, en slik funksjon må vi i så fall lage selv. Det kommer vi tilbake til.

ImageData har en data-property som er en én-dimensjonal Uint8ClampedArray.

const imageData = context.getImageData(0, 0, canvas.width, canvas.height)

Hva i alle dager betyr det?

  • Uint8 betyr at hver verdi er en unsigned integer av 8 bit, dvs 2^8 mulige verdier. Det betyr at du kan representere heltallene mellom 0 og 255. Det er det samme range som RGB-fargene defineres i på web.
  • Clamped hinter til at verdiene ikke kan under eller overflow’e. Det passer fint med bildebehandling, f.eks hvis du vil øke lysheten i et bilde. Maks lyshet til en farge er 255, så hvis vi legger på 1 så får vi 255 og ikke 0.

Fra en dimensjon til en annen

Denne array’en er i én dimensjon, men et bilde har to dimensjoner. Hvordan kan vi hente ut fargen til en piksel på posisjon [x, y]?

Hver piksel er representert med fire elementer i arrayen. Det ser slik ut:

[
    x0y0Red, x0y0Green, x0y0Blue, x0y0Alpha, 
    x1y0Red, x1y0Green, x1y0Blue, x1y0Alpha,
    x2y0Red, x2y0Green, x2y0Blue, x2y0Alpha,
    ...
]

For å hente ut en gitt piksel så kan vi f.eks gjøre slik:

const getPixelAt = (imageData, x, y) => {
    const redIdx = y * (imageData.width * 4) + x * 4;
    return {
        red: imageData.data[redIdx],
        green: imageData.data[redIdx + 1],
        blue: imageData.data[redIdx + 2],
        alpha: imageData.data[redIdx + 3]
    }
}

Konverter til gråtoner

Da har vi fargene til hver piksel lett tilgjengelig, og kan starte prosessen med å konvertere de til ASCII-karakterer. Siden vi skal vise ASCII-karakterene på denne nettsiden, så kan vi velge å beholde fargeinformasjonen i bildet ved å legge på farge på hver ASCII-karakter. Tradisjonelt sett så har ikke ASCII-kunst noe farge, så vi nøyer oss med å bruke gråtoner.

Det er flere måter å konvertere farger til gråtoner, men den enkleste er å ta gjennomsnittet av RGB-verdiene.

Så hvis fargen er {red: 100, green: 10, blue: 10} så blir gråtonen:

(100 + 10 + 10) / 3 = 40

Eksemplet under viser hvordan man kan konvertere en gråtone-verdi til en ASCII-karakter.

Gråtoner

$@B%8&WM#*ohkbdqwmO0QCJYXzvunrjf/|()1}[]?-+~<>i!lI;:,"^`'.

Her kan vi se at ASCII-karakteren $ representerer den mørkeste gråtonen (0), mens den et punktum representerer den lyseste tonen (255). Tanken er å lage en liste med karakterer hvor de gradvis fyller “firkanten” sin med mindre og mindre piksler.

Koden for å konvertere en gråtone-verdi til en ASCII-karakter blir da:

const brightnessToChar = (darkToBrightArray, brightness) => {
    const charIdx = Math.floor(((darkToBrightArray.length-1) / 255) * brightness)
    const character = darkToBrightArray[charIdx];
    // Force the web page to actually render a space character
    return character === " " ? "&nbsp;": character;
}

Vi kan kalle denne funksjonen slik:

const DARK_TO_BRIGHT_ASCII = "@#$&%*o+i;:,.'` "
brightnessToChar(DARK_TO_BRIGHT_ASCII, 0) // returns @
brightnessToChar(DARK_TO_BRIGHT_ASCII, 255) // returns &nbsp;

Her kan vi leke oss med ulike varianter for å se hvilke lister med ASCII-karakterer som passer best. I det følgende så bruker vi en kortere variant som gir et mindre detaljert uttrykk.

@#$&%*o+i;:,.'`

Konvertere et bilde

Her har jeg tatt et bilde av en hoppende glad hund, la oss prøve å konvertere det til ASCII. På forhånd har jeg fjernet bakgrunnen for å få best mulig resultat. Man kan selvsagt fjerne bakgrunnen automatisk med kode, men det får være en annen bloggpost.

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
*
#
+
%
&
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
o
%
o
+
&
o
 
 
 
 
 
 
 
 
 
 
 
 
 
 
*
%
 
 
 
i
$
+
*
o
+
%
 
 
 
 
 
 
 
 
 
 
 
 
 
 
#
%
$
 
 
 
 
@
i
i
+
o
 
 
 
 
 
 
 
 
 
 
 
 
#
#
&
$
@
#
&
&
&
$
$
&
,
;
o
 
 
 
 
 
 
 
 
 
 
 
 
 
&
%
%
$
#
%
&
&
*
:
.
$
%
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
#
&
@
o
.
.
%
o
'
,
;
:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
@
#
$
&
,
:
$
&
*
i
%
 
 
 
 
 
 
 
 
 
%
 
 
 
 
 
 
 
#
@
o
i
&
&
&
%
&
@
o
 
 
 
 
 
 
 
 
+
&
 
 
 
 
 
 
 
@
#
$
%
&
#
*
+
o
 
 
 
 
 
 
 
 
 
 
 
&
 
 
 
 
 
 
$
$
&
&
&
#
+
;
:
:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
%
+
o
&
*
o
o
+
;
,
:
i
 
 
 
 
 
 
 
 
 
 
%
 
 
*
%
#
;
o
+
+
o
$
$
.
,
:
:
 
 
 
 
 
 
 
 
 
 
&
*
%
%
$
o
i
*
*
o
:
*
*
,
;
i
 
 
 
 
 
 
 
 
 
 
 
&
%
%
%
&
o
+
o
o
&
o
+
o
%
%
o
 
 
 
 
 
 
 
 
 
 
&
%
&
&
$
#
%
o
&
&
&
%
+
i
$
&
%
%
 
 
 
 
 
 
 
 
:
&
%
&
$
$
$
@
$
#
&
#
&
;
,
*
%
$
*
+
 
 
 
 
 
 
 
$
&
%
&
@
#
$
#
@
#
#
$
%
*
o
*
*
&
;
:
,
 
 
 
 
 
 
$
&
%
$
@
@
$
$
@
@
$
$
$
$
&
%
&
.
.
:
*
*
 
 
 
 
 
$
$
*
#
@
@
@
$
+
o
&
$
@
&
%
:
;
,
,
,
*
%
 
 
 
 
 
$
#
%
&
@
@
%
*
:
;
+
%
i
i
i
i
+
i
o
%
+
o
 
 
 
 
i
$
@
&
&
#
#
#
$
*
:
;
i
o
+
*
+
*
$
*
+
o
*
 
 
 
 
 
$
@
&
&
@
$
$
&
%
*
+
+
*
*
%
&
$
i
o
*
o
o
 
 
 
 
 
$
#
&
&
$
#
#
%
%
%
%
*
+
:
i
%
;
:
+
i
*
 
 
 
 
 
 
$
@
@
@
#
&
#
%
$
*
*
o
+
:
.
,
:
:
o
%
+
 
 
 
 
 
 
$
@
@
@
@
#
&
&
$
*
o
;
:
,
:
:
.
,
:
i
+
o
 
 
 
 
 
&
#
#
#
@
@
@
$
#
$
+
o
:
:
i
o
o
;
:
i
+
 
 
 
 
 
o
$
#
&
&
$
$
$
&
#
%
o
+
+
i
o
*
%
o
:
;
+
 
 
 
 
 
%
#
#
&
%
%
&
$
#
%
&
&
+
i
o
%
o
*
*
i
,
;
 
 
 
 
 
%
@
@
#
&
%
$
&
#
&
 
 
 
 
+
$
#
o
:
:
.
:
 
 
 
 
 
 
$
@
&
*
%
&
$
*
 
 
 
 
 
 
 
 
+
:
:
:
i
 
 
 
 
 
 
#
@
$
%
$
%
o
 
 
 
 
 
 
 
 
 
i
:
i
i
 
 
 
 
 
 
 
+
#
&
+
&
*
+
 
 
 
 
 
 
 
 
 
'
;
*
,
 
 
 
 
 
 
 
 
#
#
*
o
+
 
 
 
 
 
 
 
 
 
 
 
$
&
+
 
 
 
 
 
 
 
 
#
#
+
i
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
#
%
i
;
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
%
i
i
i
*
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
;
 
i
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

For å rendre hver celle med ASCII-karakterer, så er det bare å konvertere hver piksel til en <div>, og så putte det i en CSS-grid.

const addToDom = (imageData, parentDomElement) => {
    const container = document.createElement("div");
    container.style = "font-family: monospace; display:grid; grid-template-columns: repeat(" + imageData.width  +", 1rem)"

    for (let i=0; i < imageData.data.length; i += 4) {
        const x = (i / 4) % imageData.width;
        const y = Math.floor((i / 4) / imageData.width);

        const cell = document.createElement("div")
        cell.innerHTML = brightnessToChar(brightnessAt(x,y, imageData));
        container.appendChild(cell)
    }
    parentDomElement.appendChild(container);
}

Jeg ser din ASCII, og høyner med Emoji

Dette var vel og bra, men vi kan gjøre enda bedre. Hva om vi bytter ut ASCII med emoji?

Først trenger vi å vite hvilke emojier som er støttet på web’en, det kan vi finne her.

En mulig mapping fra mørk til lys kan da f.eks være dette:

🖤 🥷 🦍 🦓 👣 👻 💀 👀 🦴 🤍 💬 🗯

Everything is a remix

Resultatet blir som følger

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
👻
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
👣
🥷
👻
🦓
🦓
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
👻
🦓
👻
👻
🦓
👻
 
 
 
 
 
 
 
 
 
 
 
 
 
 
👣
🦓
 
 
 
💀
🦍
💀
👣
👣
💀
🦓
 
 
 
 
 
 
 
 
 
 
 
 
 
 
🥷
🦓
🥷
 
 
 
 
🖤
👀
👀
👻
👣
 
 
 
 
 
 
 
 
 
 
 
 
🥷
🥷
🦍
🥷
🖤
🥷
🦍
🦍
🦍
🦍
🥷
🦍
🤍
👀
👻
 
 
 
 
 
 
 
 
 
 
 
 
 
🦍
🦓
🦓
🦍
🥷
🦓
🦍
🦍
👣
🦴
🤍
🥷
🦓
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
🥷
🦍
🖤
👻
🤍
🤍
🦓
👻
🗯
🤍
👀
🦴
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
🖤
🖤
🥷
🦓
🤍
🦴
🦍
🦍
👣
💀
🦓
 
 
 
 
 
 
 
 
 
🦓
 
 
 
 
 
 
 
🖤
🖤
👻
💀
🦍
🦍
🦍
🦓
🦍
🖤
👣
 
 
 
 
 
 
 
 
👻
🦍
 
 
 
 
 
 
 
🖤
🖤
🥷
🦓
🦍
🥷
👣
👻
👣
 
 
 
 
 
 
 
 
 
 
 
🦍
 
 
 
 
 
 
🥷
🥷
🦓
🦓
🦍
🥷
👻
👀
🦴
🦴
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
🦓
💀
👻
🦍
👣
👻
👻
💀
👀
🦴
🦴
💀
 
 
 
 
 
 
 
 
 
 
🦓
 
 
👣
🦓
🥷
👀
👻
👻
👻
👣
🥷
🦍
💬
🤍
🦴
🦴
 
 
 
 
 
 
 
 
 
 
🦍
👣
🦓
🦓
🦍
👻
💀
👣
👣
👻
🦴
👣
👣
🤍
👀
💀
 
 
 
 
 
 
 
 
 
 
 
🦓
🦓
🦓
🦓
🦍
👻
👻
👻
👻
🦍
👻
💀
👻
🦓
🦓
👻
 
 
 
 
 
 
 
 
 
 
🦍
🦓
🦓
🦍
🦍
🥷
🦓
👻
🦓
🦍
🦍
🦓
👻
💀
🥷
🦍
🦓
🦓
 
 
 
 
 
 
 
 
🦴
🦍
🦓
🦓
🥷
🥷
🥷
🖤
🥷
🥷
🦍
🥷
🦍
👀
🦴
👣
🦓
🦍
👣
👻
 
 
 
 
 
 
 
🦍
🦍
🦓
🦍
🖤
🖤
🥷
🥷
🖤
🖤
🥷
🥷
🦓
👣
👻
👣
👣
🦍
👀
🦴
🤍
 
 
 
 
 
 
🦍
🦍
🦓
🦍
🖤
🖤
🥷
🥷
🖤
🖤
🦍
🦍
🦍
🦍
🦓
🦓
🦍
🤍
🤍
🦴
👣
👣
 
 
 
 
 
🦍
🦍
👣
🥷
🖤
🖤
🖤
🦍
💀
👻
🦍
🥷
🖤
🦍
🦓
🦴
👀
🤍
🤍
🦴
👣
🦓
 
 
 
 
 
🦍
🥷
🦓
🦍
🖤
🖤
🦓
👣
🦴
👀
👻
🦓
💀
💀
💀
💀
💀
👀
👣
🦓
💀
👻
 
 
 
 
💀
🥷
🖤
🦍
🦍
🖤
🥷
🥷
🦍
👣
🦴
👀
👀
👻
💀
👣
👻
👣
🥷
👣
👻
👻
👣
 
 
 
 
 
🥷
🖤
🦍
🦍
🖤
🥷
🥷
🦍
🦓
👣
💀
👻
👣
👣
🦓
🦍
🥷
💀
👻
👣
👻
👻
 
 
 
 
 
🦍
🥷
🦍
🦍
🥷
🥷
🥷
🦓
🦓
🦓
🦓
👣
👻
🦴
💀
🦓
👀
🦴
💀
💀
👣
 
 
 
 
 
 
🦍
🖤
🖤
🖤
🥷
🦍
🥷
🦓
🦍
👣
👣
👻
💀
🦴
💬
🤍
🦴
🦴
👻
🦓
👻
 
 
 
 
 
 
🦍
🖤
🖤
🖤
🖤
🥷
🦍
🦓
🦍
👣
👻
👀
🦴
🤍
🦴
🦴
💬
🦴
🦴
💀
👻
👻
 
 
 
 
 
🦍
🥷
🥷
🥷
🖤
🖤
🖤
🦍
🥷
🦍
💀
👻
🦴
🦴
👀
👻
👻
👀
🦴
👀
👻
 
 
 
 
 
👣
🥷
🥷
🦍
🦍
🦍
🥷
🥷
🦍
🖤
🦓
👣
💀
💀
💀
👻
👣
🦓
👻
🦴
👀
👻
 
 
 
 
 
🦓
🖤
🖤
🦓
🦓
🦓
🦍
🦍
🥷
🦓
🦓
🦍
👻
💀
👻
🦓
👻
👣
👣
💀
🦴
👀
 
 
 
 
 
🦓
🖤
🖤
🥷
🦍
🦓
🦍
🦍
🥷
🦍
 
 
 
 
👻
🥷
🥷
👻
🦴
🦴
🤍
🦴
 
 
 
 
 
 
🥷
🖤
🦍
👣
🦓
🦍
🦍
👣
 
 
 
 
 
 
 
 
👻
🦴
🦴
🦴
👀
 
 
 
 
 
 
🥷
🖤
🦍
🦓
🦍
🦓
👣
 
 
 
 
 
 
 
 
 
💀
🦴
💀
💀
 
 
 
 
 
 
 
💀
🥷
🦍
👻
🦍
👣
👻
 
 
 
 
 
 
 
 
 
💬
👀
👣
🤍
 
 
 
 
 
 
 
 
🥷
🥷
👣
👻
👻
 
 
 
 
 
 
 
 
 
 
 
🦍
🦍
💀
 
 
 
 
 
 
 
 
🥷
🥷
💀
💀
👻
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
🥷
🦓
💀
👀
💀
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
🦓
💀
💀
👀
👣
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
👀
 
💀
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Siden dette er på web’en så kan vi fint legge grånyansene til side og gå for farger. Da trenger vi bare en mapping fra farge + intensitet til emoji-tegn. Å lage mappingen for hånd høres kjedelig ut, men vi kan fint lage et program som automatiserer det for oss.

Del 2

Det neste jeg skal gjøre er å koble dette til webkamera-APIet. Å se seg selv om en strøm av emojier, hva kan vel være bedre enn det?

Her er bloggposten om videomoji

For de spesielt interesserte så kan du lese kildekoden her.