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.
$@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 === " " ? " ": 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
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.
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.