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

jintwo commented Oct 19, 2020

(defn mix [colors]
  (let [read-hex-int #(read-string (str "0X" %))
        mix-components #(quot (reduce + (map read-hex-int %1)) (count %1))
        colors-lists (map #(re-seq #"\w{2}" %) colors)
        groups (partition (count colors) (apply interleave colors-lists))
        hex-color (map mix-components groups)]
    (str "#" (str/join "" (map #(format "%02X" %) hex-color)))))

Copy link

nbardiuk commented Oct 19, 2020

(defn parse-rgb [rgb]
  (let [i (Integer/parseInt (subs rgb 1) 16)]
    {:r (bit-shift-right i 16)
     :g (bit-shift-right (bit-and i 0x00FF00) 8)
     :b (bit-and i 0x0000FF)}))

(defn format-rgb [{:keys [r g b]}]
  (format "#%X%X%X" r g b))

(defn avg-by-component [colors]
  (let [avg #(quot (reduce + %) (count %))
        avg-by #(->> colors (map %) avg)]
    {:r (avg-by :r)
     :g (avg-by :g)
     :b (avg-by :b)}))

(defn mix [colors]
  (->> colors
       (map parse-rgb)

Copy link

(defn numeric-components
  "Returns a [red green blue] tuple of numbers parsed from the RGB string."
  (map #(Integer/parseInt % 16) (re-seq #"\w{2}" rgb-string)))

(defn mix
  (apply format
         (apply map
                (fn [& component] (quot (reduce + component) (count component)))
                (map numeric-components colours))))

Copy link

steffan-westcott commented Oct 19, 2020

(defn rgb->hex [rgb]
  (apply format "#%02X%02X%02X" rgb))

(defn hex->rgb [s]
  (map #(Integer/parseInt % 16) (re-seq #"\w\w" s)))

(defn mean [& ns]
  (quot (reduce + ns) (count ns)))

(defn mix [colours]
  (->> colours (map hex->rgb) (apply map mean) rgb->hex))

Copy link

(defn pad [n] (if (= 2 (count n)) n (str "0" n)))
(defn hex->rgb [h] (Integer/parseInt h 16))
(defn rgb->hex [rgb] (Integer/toString rgb 16))
(defn hex->rgbs [h] (->> h (re-seq #"\w\w") (map hex->rgb)))
(defn rgbs->hex [rgbs] (->> rgbs (map (comp pad rgb->hex)) (clojure.string/join "") (str "#")))
(defn avgs [xs] (->> xs (apply map +) (map #(/ % (count xs)))))

(defn mix
  (->> hexs (map hex->rgbs) avgs rgbs->hex))

(mix ["#FFFFFF" "#000000"])                          ; "#7F7F7F"
(mix ["#FF0000" "#00FF00" "#0000FF"])                ; "#555555"
(mix ["#FFFF00", "#FF0000"])                         ; "##ff7f00"
(mix ["#FFFF00", "#0000FF"])                         ; "#7f7f7f"
(mix ["#B60E73", "#0EAEB6"])                         ; "#625E94"

Copy link

@steffan-westcott I really enjoy clear solution. It looks like something I had in mind but could not achive with maps

Copy link

mchampine commented Oct 19, 2020

(defn mix [cs]
  (letfn [(avg [& cn] (quot (apply + cn) (count cn)))
          (rgbs [s] (map read-string (map #(apply str "0x" %) (partition 2 (rest s)))))]
    (apply format "#%X%X%X" (apply map avg (map rgbs cs)))))

Copy link

stuartstein777 commented Oct 19, 2020

(defn pad [hex]
  (if (= 2 (count hex))
    (str "0" hex)))

(defn hex->rgb [hex]
  (->> (subs hex 1)
       (partition 2)
       (map (partial apply str))
       (map #(str "0x" %))
       (map read-string)))

(defn mix [colours]
  (->> (map hex->rgb colours)
       (apply map +)
       (map #(/ % (count colours)))
       (map #(format "%x" (int %)))
       (map pad)
       (map str/upper-case)
       (apply str)
       (str "#")))

(mix ["#FFFFFF" "#000000"])                          ; "#7F7F7F"
(mix ["#FF0000" "#00FF00" "#0000FF"])                ; "#555555"
(mix ["#FFFF00", "#FF0000"])                         ; "#ff7f00"

Copy link

hby commented Oct 19, 2020

(defn mix [rgbs]
  (let [is (partition 2 1 [1 3 5 7])]
    (->> rgbs
         (map (fn [rgb] (map #(Integer/parseInt (apply subs rgb %) 16) is)))
         (apply map vector)
         (map #(int (/ (apply + %) (count rgbs))))
         (map #(format "%2x" %))
         (apply str "#"))))

Copy link

aggerdom commented Oct 20, 2020

(defn rgb-components [hex-color]
  (->> (rest (re-matches #"(?i)#?([A-F0-9]{2})([A-F0-9]{2})([A-F0-9]{2})" hex-color))
       (map #(Integer/parseInt % 16))))

(defn rgb-format [r g b]
  (format "#%02X%02X%02X" r g b))

(defn mix [hex-colors]
  (->> hex-colors
       (map rgb-components) ;; [(r1 g1 b1) (r2 g2 b2) (r3 g3 b3)]
       (apply map vector)   ;; [(r1 r2 r3) (g1 g2 g3) (b1 b2 b3)]
       (map mean)           ;; [MEAN(r) MEAN(g) MEAN(b)]
       (map int)            ;; round
       (apply rgb-format))) ;; Format as hex

Copy link

sztamas commented Oct 20, 2020

(defn- rgb-str->int [rgb]
  (if-let [groups (re-matches #"(?i)(?:#)([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})" rgb)]
    (map (comp read-string (partial str "0x")) (rest groups))
    (throw (IllegalArgumentException. (format "Invalid RGB string '%s'" rgb)))))

(defn mix [rgbs]
  (->> rgbs
       (map rgb-str->int)
       (apply map +)
       (map #(quot % (count rgbs)))
       (map (partial format "%02X"))
       (apply str "#")))

Copy link

kthu commented Oct 20, 2020

  (defn hexcolor->ints
    (->> color
         (partition 2)
         (map #(str "0x" (apply str %)))
         (map read-string)))

  (defn ints->hexcolor
    [[r g b]]
    (format "#%02x%02x%02x" r g b))

  (defn average-segment
    (int (/ (reduce + segment)
            (count segment))))

  (def transpose (partial apply map list))

  (defn mix
    (->> colors
         (map hexcolor->ints)
         (map average-segment)

Copy link

zelark commented Oct 20, 2020

(defn mix [hexs]
  (->> hexs
       (map #(re-seq #"\w\w" %))
       (apply map #(map (fn [h] (Integer/parseInt h 16)) %&))
       (map #(quot (apply + %) (count %)))
       (apply format "#%02X%02X%02X"))) ;; fixed after steffan-westcott's comment below

@mchampine, thanks for the idea of using (apply format "#%X%X%X"). Btw, in your solution you could just use #(apply str "0x" %) and it would work.

Copy link

@zelark, thanks, yes I missed that simplification! (using 'format' in various ways for the hex conversion was pioneered here by others though :) )

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