December 5, 2019

MECCA Pix

MECCA ROMs

So now we've got a nice raster image of our pixel art. How do we import it into our app? This is the job of MECCA Pix.

First we need to be able to upload a file. I'll provide one here as a default, but this is a real upload form; feel free to replace it with a (small, low-res!) image of your own:

(ns mecca.klipse
  (:require [reagent.core :as r]
            [goog.object :as o]))

(def file-atom (r/atom "data:image/png;base64,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"))

(defn file-upload []
  [:div
   [:h1 "File upload"]
   [:input#input
    {:type      "file"
     :on-change 
     (fn [e]
       (let [dom    (o/get e "target")
             file   (o/getValueByKeys dom #js ["files" 0])
             reader (js/FileReader.)]
         (.readAsDataURL reader file)
         (set! (.-onload reader)
               #(reset! file-atom (-> % .-target .-result)))))}]])

[file-upload]

We have read the file in as a base64 URL and stored the string in our file-atom:

@file-atom

And now we can display it as an img element:

(defn img-el [url]
  [:div
   [:img {:width "100%" :src url}]])
   
[img-el @file-atom]

To extract the pixel data, we'll draw the image onto an HTML Canvas, and make use of its getImageData method which returns a Uint8ClampedArray of rgba values (0-255 inclusive):

(defn img->data [file]
  (let [img (js/Image.)
        canvas (.createElement js/document "canvas")
        ctx    (.getContext canvas "2d")]
    (set! (.-src img) file)
    (set! (.-width canvas) (.-width img))
    (set! (.-height canvas) (.-height img))
    (.drawImage ctx img 0 0)
    (.-data (.getImageData ctx 0 0 (.-width img) (.-height img)))))

[:textarea 
 {:rows      10
  :cols      48
  :value     (str (img->data @file-atom))
  :read-only true}]

If the above snippet gives an error, you can reload it by deleting the last character and reentering it.

Next we will divide the pixels into color paths and optimize them.

To be continued...

Happy Advent!

Tags: MECCA Music Platform Advent of Parens