Skip to content

Instantly share code, notes, and snippets.

@ericnormand
Last active March 19, 2021 04:26
Show Gist options
  • Save ericnormand/24704c0e4804580c991b68aab29b1c30 to your computer and use it in GitHub Desktop.
Save ericnormand/24704c0e4804580c991b68aab29b1c30 to your computer and use it in GitHub Desktop.

RGB Color Mixing

Here's an algorithm for mixing multiple RGB colors to create a single color.

  1. Separate the colors into Red, Green, and Blue components.
  2. Average all the Reds together, average all the Greens together, average all the Blues together.
  3. Put the average values back together into a resulting RGB.

Is this the right way to do it? I don't know! But it's the way we're going to implement in this problem. Your task is to take a collection of RGB strings of the form "#FF021A", mix them like the algorithm above, and return the resulting RGB string.

Note that each RGB string contains two hexadecimal digits for each component. You can round the averages to integers however you want.

Examples

(mix ["#FFFFFF" "#000000"]) ;=> "#7F7F7F" or "#808080" depending on how you round
(mix ["#FF0000" "#00FF00" "#0000FF"]) ;=> "#555555"

Thanks to this site for the challenge idea where it is considered Very Hard level in JavaScript.

Please submit your solutions as comments on this gist.

@HughPowell
Copy link

HughPowell commented Oct 24, 2020

I'm trying to use this to expand my property testing skills. Does anyone have any thoughts on the properties that can be tested? The only one I've managed to come up with so far is idempotence.

(require '[clojure.string :as string]
         '[clojure.spec.alpha :as spec]
         '[clojure.spec.gen.alpha :as spec-gen]
         '[clojure.test.check.clojure-test :refer [defspec]]
         '[clojure.test.check.properties :as check-properties])

(def hex-characters
  (set (map char (concat (range (int \A) (int \F))
                         (range (int \0) (int \9))))))

(spec/def ::rgb-string (spec/with-gen
                         (spec/and
                           string?
                           #(re-matches #"#[\p{XDigit}]{6}" %))
                         #(spec-gen/fmap
                            (fn [h] (apply str "#" h))
                            (spec-gen/vector (spec/gen hex-characters) 6))))

(spec/def ::rgb-strings (spec/coll-of ::rgb-string :kind sequential? :min-count 1))

(defn- rgb-string->tuple [rgb]
  (->> rgb
       (re-seq #"[\p{XDigit}]{2}")
       (map (fn [h] (read-string (apply str "0x" h))))))

(defn- tuple->rgb-string [t]
  (string/replace (apply format "#%2H%2H%2H" t) #" " "0"))

(defn mix
  "Average out the provided colours"
  [rgb-strings]
  (->> rgb-strings
       (map rgb-string->tuple)
       (apply map (fn [& colour-values]
                    (int (/ (apply + colour-values) (count colour-values)))))
       tuple->rgb-string))

(defspec mix-is-idempotent
         100
  (check-properties/for-all [rgb-strings (spec/gen ::rgb-strings)]
    (let [result (mix rgb-strings)]
      (= result (mix [result])))))

@KingCode
Copy link

@HughPowell Nice work!...Other than idempotency, the only one I can think of is commutativity (changing the input list ordering). Regarding associativity, it does not apply because e.g. (avg 1 5 10) together weighs the values evenly as opposed to (avg (avg 1 5) 10).

@HughPowell
Copy link

HughPowell commented Oct 25, 2020

Ah ha. Good point @KingCode :-)

(defspec mix-is-commutative
         100
  (check-properties/for-all [[original shuffled] (spec-gen/bind
                                                   (spec/gen ::rgb-strings)
                                                   (fn [xs]
                                                     (spec-gen/fmap (fn [ys] [xs ys])
                                                                    (spec-gen/shuffle xs))))]
    (= (mix original) (mix shuffled))))

This still isn't quite sufficient. We could replace the implementation of mix with

(first (sort rgb-strings))

and both the current property tests would pass 🤔

@germ13
Copy link

germ13 commented Oct 29, 2020

(defn rgbs [rgb]
  (let [a (subs rgb 1 3)
        b (subs rgb 3 5)
        c (subs rgb 5)]
    (map #(read-string (str "0x" %)) [a b c])))

(defn mix[colors]
  (let [[& color] (map rgbs colors)]
    (str "#" (clojure.string/join ""
                                  (map #(format "%X" %)
                                       (map byte (map #(/ % (count colors))
                                                      (apply map + color))))))))

@andyfry01
Copy link

andyfry01 commented Mar 2, 2021

I'm a bit late to the party on this one, but here's my approach:

(defn average
  "Util for averaging many numbers, and rounding them down to the nearest whole number"
  [& nums]
  (int (Math/floor (/ (apply + nums) (count nums)))))

(defn hex->numtriple 
  "Takes a hex number and turns it into a vector of base-10 numbers, e.g.  #FFFFFF -> [255 255 255]"
  [hex]
  (as-> hex $
    (drop 1 $)
    (partition 2 $)
    (mapv (fn [chars]
           (->> (apply str chars)
                (str "0x")
                (read-string))) $)))

(defn numtriple->hex 
  "Takes a vector of base-10 numbers and turns them into a hex color, e.g. [255 255 255] -> #FFFFFF"
  [numtriple]
  (reduce str (cons "#" (map #(format "%02X" %) numtriple))))

(defn mix 
  "Magic happens here, e.g (mix '#FFFFFF' '#000000') -> '#7F7F7F'"
  [& stuff]
  (let [rgb-nums (map hex->numtriple stuff)]
    (numtriple->hex (apply map average rgb-nums))))

(comment
  ;; Function demos!
  ;;   
  (mix "#000000" "#FFFFFF") ; -> "#7F7F7F"
  (mix "#000000" "#7F7F7F" "#111111") ; -> "#303030"
  (mix "#000000") ; -> "#000000"
  )

Solution also available here, along with a bunch of other challenges I've done: https://github.com/andyfry01/clojure-coding-challenges/blob/master/src/main/color-averaging.clj

@Sinha-Ujjawal
Copy link

My solution to the problem-

(defn hex->num [s]
    (Integer/parseInt s 16))

(defn filter-index [pred coll]
    (map second (filter (fn [[i x]] (pred i)) (map-indexed vector coll))))

(defn pick-even [coll]
    (filter-index (fn [i] (= 0 (bit-and i 1))) coll))

(defn pick-odd [coll]
    (filter-index (fn [i] (= 1 (bit-and i 1))) coll))

(defn hex->rgb [s]
    (let [s (rest s)] (map hex->num (map str (pick-even s) (pick-odd s)))))

(defn rgb->hex [[r g b]]
    (str "#" (clojure.string/join #"" (map (fn [x] (format "%x" x)) [r g b]))))

(defn mix [colors]
    (rgb->hex (let [n (count colors)] (reduce (fn [[x y z] [a b c]] (map (fn [[x y]] (+ x (int (/ y n)))) [[x a] [y b] [z c]])) [0 0 0] (map hex->rgb colors)))))

@Sinha-Ujjawal
Copy link

Sinha-Ujjawal commented Mar 19, 2021

Another solution (thx @andyfry01)-

(defn average [& ns]
    (reduce + (map (fn [n] (/ n (count ns))) ns)))

(defn hex->num [s]
    (Integer/parseInt s 16))

(defn hex->rgb [s]
    (map hex->num (map (fn [[x y]] (str x y)) (partition 2 (rest s)))))

(defn rgb->hex [[r g b]]
    (str "#" (clojure.string/join #"" (map (fn [x] (format "%x" x)) [r g b]))))

(defn mix [colors]
    (rgb->hex (apply map (fn int-average [& ns] (int (Math/floor (apply average ns)))) (map hex->rgb colors))))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment