colors.lisp 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. (in-package :cl-colors2)
  2. (defparameter *x11-colors-list* '())
  3. (defparameter *svg-colors-list* '())
  4. (defparameter *svg-extended-colors-list* '())
  5. ;;; color representations
  6. (deftype unit-real ()
  7. "Real number in [0,1]."
  8. '(real 0 1))
  9. (defstruct (rgb (:constructor rgb (red green blue)))
  10. "RGB color."
  11. (red nil :type unit-real :read-only t)
  12. (green nil :type unit-real :read-only t)
  13. (blue nil :type unit-real :read-only t))
  14. (defmethod make-load-form ((p rgb) &optional env)
  15. (declare (ignore env))
  16. (make-load-form-saving-slots p))
  17. (defun gray (value)
  18. "Create an RGB representation of a gray color (value in [0,1)."
  19. (rgb value value value))
  20. (defstruct (hsv (:constructor hsv (hue saturation value)))
  21. "HSV color."
  22. (hue nil :type (real 0 360) :read-only t)
  23. (saturation nil :type unit-real :read-only t)
  24. (value nil :type unit-real :read-only t))
  25. (defmethod make-load-form ((p hsv) &optional env)
  26. (declare (ignore env))
  27. (make-load-form-saving-slots p))
  28. (defun normalize-hue (hue)
  29. "Normalize hue to the interval [0,360)."
  30. (mod hue 360))
  31. ;;; conversions
  32. (defun rgb-to-hsv (rgb &optional (undefined-hue 0))
  33. "Convert RGB to HSV representation. When hue is undefined (saturation is
  34. zero), UNDEFINED-HUE will be assigned."
  35. (flet ((normalize (constant right left delta)
  36. (let ((hue (+ constant (/ (* 60 (- right left)) delta))))
  37. (if (minusp hue)
  38. (+ hue 360)
  39. hue))))
  40. (let* ((red (rgb-red rgb))
  41. (green (rgb-green rgb))
  42. (blue (rgb-blue rgb))
  43. (value (max red green blue))
  44. (delta (- value (min red green blue)))
  45. (saturation (if (plusp value)
  46. (/ delta value)
  47. 0))
  48. (hue (cond
  49. ((zerop saturation) undefined-hue) ; undefined
  50. ((= red value) (normalize 0 green blue delta)) ; dominant red
  51. ((= green value) (normalize 120 blue red delta)) ; dominant green
  52. (t (normalize 240 red green delta)))))
  53. (hsv hue saturation value))))
  54. (defun hsv-to-rgb (hsv)
  55. "Convert HSV to RGB representation. When SATURATION is zero, HUE is
  56. ignored."
  57. (let* ((hue (hsv-hue hsv))
  58. (saturation (hsv-saturation hsv))
  59. (value (hsv-value hsv)))
  60. ;; if saturation=0, color is on the gray line
  61. (when (zerop saturation)
  62. (return-from hsv-to-rgb (gray value)))
  63. ;; nonzero saturation: normalize hue to [0,6)
  64. (let* ((h (/ (normalize-hue hue) 60)))
  65. (multiple-value-bind (quotient remainder)
  66. (floor h)
  67. (let* ((p (* value (- 1 saturation)))
  68. (q (* value (- 1 (* saturation remainder))))
  69. (r (* value (- 1 (* saturation (- 1 remainder))))))
  70. (multiple-value-bind (red green blue)
  71. (case quotient
  72. (0 (values value r p))
  73. (1 (values q value p))
  74. (2 (values p value r))
  75. (3 (values p q value))
  76. (4 (values r p value))
  77. (t (values value p q)))
  78. (rgb red green blue)))))))
  79. (defun hex-to-rgb (string)
  80. "Parse hexadecimal notation (eg ff0000 or f00 for red) into an RGB color."
  81. (multiple-value-bind (width max)
  82. (case (length string)
  83. (3 (values 1 15))
  84. (6 (values 2 255))
  85. (t (error "string ~A doesn't have length 3 or 6, can't parse as ~
  86. RGB specification" string)))
  87. (flet ((parse (index)
  88. (/ (parse-integer string
  89. :start (* index width)
  90. :end (* (1+ index) width)
  91. :radix 16)
  92. max)))
  93. (rgb (parse 0) (parse 1) (parse 2)))))
  94. ;;; conversion with generic functions
  95. (define-compiler-macro as-hsv (&whole form color)
  96. (if (constantp color)
  97. (funcall #'as-hsv color)
  98. (progn form)))
  99. (defgeneric as-hsv (color &optional undefined-hue)
  100. (:documentation "Coerce an RGB, an HSV, or a HEX string into a HSV structure. HEX string is parsed as an RGB specification.")
  101. (:method ((color rgb) &optional (undefined-hue 0))
  102. (rgb-to-hsv color undefined-hue))
  103. (:method ((color hsv) &optional undefined-hue)
  104. (declare (ignore undefined-hue))
  105. color)
  106. (:method ((string string) &optional (undefined-hue 0))
  107. (rgb-to-hsv
  108. (handler-case
  109. (hex-to-rgb string)
  110. (error ()
  111. (or (cdr (assoc string *x11-colors-list* :test #'equalp))
  112. (error "Color can not be parsed"))))
  113. undefined-hue)))
  114. (define-compiler-macro as-rgb (&whole form color)
  115. (if (constantp color)
  116. (funcall #'as-rgb color)
  117. (progn form)))
  118. (defgeneric as-rgb (color)
  119. (:documentation "Coerce an RGB, an HSV, or a HEX string into a RGB structure")
  120. (:method ((rgb rgb))
  121. rgb)
  122. (:method ((hsv hsv))
  123. (hsv-to-rgb hsv))
  124. (:method ((string string))
  125. (handler-case
  126. (hex-to-rgb string)
  127. (error ()
  128. (or (cdr (assoc string *x11-colors-list* :test #'equalp))
  129. (error "Color can not be parsed"))))))
  130. ;;; combinations
  131. (declaim (inline cc))
  132. (defun cc (a b alpha)
  133. "Convex combination (1-ALPHA)*A+ALPHA*B, ie ALPHA is the weight of A."
  134. (declare (type (real 0 1) alpha))
  135. (+ (* (- 1 alpha) a) (* alpha b)))
  136. (defun rgb-combination (color1 color2 alpha)
  137. "Color combination in RGB space."
  138. (flet ((c (c1 c2) (cc c1 c2 alpha)))
  139. (let ((rgb-1 (as-rgb color1))
  140. (rgb-2 (as-rgb color2)))
  141. (rgb (c (rgb-red rgb-1) (rgb-red rgb-2))
  142. (c (rgb-green rgb-1) (rgb-green rgb-2))
  143. (c (rgb-blue rgb-1) (rgb-blue rgb-2))))))
  144. (defun hsv-combination (hsv1 hsv2 alpha &optional (positive? t))
  145. "Color combination in HSV space. POSITIVE? determines whether the hue
  146. combination is in the positive or negative direction on the color wheel."
  147. (flet ((c (c1 c2) (cc c1 c2 alpha)))
  148. (let* ((hsv-1 (as-hsv hsv1))
  149. (hsv-2 (as-hsv hsv2))
  150. (hue-1 (hsv-hue hsv-1))
  151. (saturation-1 (hsv-saturation hsv-1))
  152. (value-1 (hsv-value hsv-1))
  153. (hue-2 (hsv-hue hsv-2))
  154. (saturation-2 (hsv-saturation hsv-2))
  155. (value-2 (hsv-value hsv-2)))
  156. (hsv (cond
  157. ((and positive? (> hue-1 hue-2))
  158. (normalize-hue (c hue-1 (+ hue-2 360))))
  159. ((and (not positive?) (< hue-1 hue-2))
  160. (normalize-hue (c (+ hue-1 360) hue-2)))
  161. (t (c hue-1 hue-2)))
  162. (c saturation-1 saturation-2)
  163. (c value-1 value-2)))))
  164. ;; equality
  165. (defun eps= (a b &optional (epsilon 1e-10))
  166. (<= (abs (- a b)) epsilon))
  167. (defgeneric color-equals (a b &key tolerance)
  168. (:documentation "Compare two colors under a given floating point tolerance."))
  169. (defmethod color-equals ((a rgb) (b rgb) &key (tolerance 1e-10))
  170. (and (eps= (rgb-red a)
  171. (rgb-red b)
  172. tolerance)
  173. (eps= (rgb-green a)
  174. (rgb-green b)
  175. tolerance)
  176. (eps= (rgb-blue a)
  177. (rgb-blue b)
  178. tolerance)))
  179. (defmethod color-equals ((a hsv) (b hsv) &key (tolerance 1e-10))
  180. (and (eps= (hsv-hue a)
  181. (hsv-hue b)
  182. tolerance)
  183. (eps= (hsv-saturation a)
  184. (hsv-saturation b)
  185. tolerance)
  186. (eps= (hsv-value a)
  187. (hsv-value b)
  188. tolerance)))
  189. (defmethod color-equals ((a hsv) (b rgb) &key (tolerance 1e-10))
  190. (color-equals a (as-hsv b) :tolerance tolerance))
  191. (defmethod color-equals ((a rgb) (b hsv) &key (tolerance 1e-10))
  192. (color-equals (as-hsv a) b :tolerance tolerance))
  193. ;;; macros used by the autogenerated files
  194. (defun colorname->constant-name (name)
  195. (symbolicate #\+
  196. (cl-ppcre:regex-replace-all "\\s+" (string-upcase name) "-")
  197. #\+))
  198. (defmacro define-rgb-color (name red green blue)
  199. "Macro for defining color constants. Used by the automatically generated color file."
  200. (let ((constant-name (colorname->constant-name name)))
  201. `(progn
  202. (define-constant ,constant-name (rgb ,red ,green ,blue)
  203. :test #'equalp :documentation ,(format nil "X11 color ~A." name)))))