Skip to content

Instantly share code, notes, and snippets.

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.


(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.

Copy link

Each colour component should be expressed as a 2 digit, 0 padded hex string. To get the desired output format, you can use %02X (rather than %X) for each component:

(apply format "#%02X%02X%02X" [1 2 3])
=> "#010203"

Copy link

(defn from-hex [color-string]
  (->> (partition 2 (subs color-string 1))
       (map #(apply str %))
       (map #(Integer/parseInt % 16))))

(defn avg [& nums]
  (quot (/ (apply + nums) (count nums))))

(defn mix [colors]
  (->> colors
       (map from-hex)
       (apply map avg)
       (apply format "#%02X%02X%02X")))

(mix ["#FF0000" "#00FF00" "#0000FF"])
;; => "#555555"

(mix ["#FFFFFF" "#000000"])
;; => "#808080"

Copy link

(defn mix [hex-colors]
  (->> hex-colors
       (map #(Integer/decode %1))
       (map #(vector
              (bit-shift-right (bit-and 0xFF0000 %1) 16)
              (bit-shift-right (bit-and 0x00FF00 %1) 8)
              (bit-and 0x0000FF %1)))
       (apply map +)
       (map #(quot %1 (count hex-colors)))
       (map #(format "%02X" %1))
       (apply str "#")))

Copy link

KingCode commented Oct 21, 2020

"If it was very hard in Javascript, let it be hard in Clojure, too!" :)
Here is my labored version - the price to pay to be free of JVM/JS platform dependencies

(def hex-pows (iterate #(* 16 %) 1)) 
(def num->char (->> "0123456789ABCDEF" (zipmap (range))))
(def char->num (zipmap (vals num->char) (keys num->char)))

(defn ->dec [h]
  (->> h (map char->num)
       (reduce (fn [[n pow] x]
                 [(-> hex-pows (nth pow) (* x) (+ n)),
                  (inc pow)])
               [0 0])

(defn ->hex [n]
  (if (zero? n) 
     (loop [acc [] 
            num n 
            denom (->> hex-pows (take-while #(<= % n)) last)]
       (if (< denom 1)
         (let [[q r] ((juxt quot rem) num denom)]
           (recur (conj acc (num->char q)) 
                  (/ denom 16)))))
     (apply str))))

(def sum-parts 
  (fn [rf]
      ([tally] (->> tally (take 3) (mapv #(quot % (last tally)))))
      ([[rt gt bt k] [r g b]]
       (rf [(+ rt r) (+ gt g) (+ bt b) (inc k)])))))

(defn mix [RGBs]
 (let [xf (comp (map rest)
                (map #(partition 2 %))
                (map #(map (fn [hh] (->dec hh)) %))
       format #(->> % (map ->hex) (apply str "#"))]
   (->> RGBs 
        (transduce xf conj [0 0 0 0])

Copy link

(defn mix [colors]
  (let [hexstr->i (fn [s] (Long/parseLong s 16))
        i->hexstr (fn [i] (Long/toHexString i))
        parse-rgb (fn [c] (->> (re-find #"#(..)(..)(..)" c)
                               (map hexstr->i)))
        avg       (fn [ns] (/ (reduce + ns) (count ns) 1.0))
        avg-color (fn [cs]
                    [(Math/round (avg (map #(nth % 0) cs)))
                     (Math/round (avg (map #(nth % 1) cs)))
                     (Math/round (avg (map #(nth % 2) cs)))])]
    (->> (mapv parse-rgb colors)
         (map i->hexstr)
         (apply (partial str "#")))))

Copy link

g7s commented Oct 23, 2020

(defn mix
  (let [base-16 #(Integer/parseInt % 16)
        avg     #(Math/round (float (/ (apply + %&) (count %&))))
        rgb     #(map base-16 (re-seq #"\w\w" %))
        to-hex  #(Integer/toHexString (+ (bit-shift-left %1 16) (bit-shift-left %2 8) %3))]
    (->> colors
         (map rgb)
         (apply map avg)
         (apply to-hex)
         (str "#"))))

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]]
         '[ :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
                           #(re-matches #"#[\p{XDigit}]{6}" %))
                            (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
       (map rgb-string->tuple)
       (apply map (fn [& colour-values]
                    (int (/ (apply + colour-values) (count colour-values)))))

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

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).

Copy link

HughPowell commented Oct 25, 2020

Ah ha. Good point @KingCode :-)

(defspec mix-is-commutative
  (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 🤔

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))))))))

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]"
  (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"
  (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))))

  ;; 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:

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)))))

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