UIButton+Glossy.m 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. #import "UIButton+Glossy.h"
  2. typedef struct
  3. {
  4. CGFloat color[4];
  5. CGFloat caustic[4];
  6. float expCoefficient;
  7. float expOffset;
  8. float expScale;
  9. float initialWhite;
  10. float finalWhite;
  11. } GlossyParams;
  12. static void rgb_to_hsv(const CGFloat* inputComponents, CGFloat* outputComponents)
  13. {
  14. // Unpack r,g,b for conciseness
  15. CGFloat r = inputComponents[0];
  16. CGFloat g = inputComponents[1];
  17. CGFloat b = inputComponents[2];
  18. // Rather tediously, find the min and max values, and the max component
  19. char max_elt = 'r';
  20. CGFloat max_val=r, min_val=r;
  21. if (g > max_val)
  22. {
  23. max_val = g;
  24. max_elt = 'g';
  25. }
  26. if (b > max_val)
  27. {
  28. max_val = b;
  29. max_elt = 'b';
  30. }
  31. if (g < min_val) min_val = g;
  32. if (b < min_val) min_val = b;
  33. // Cached
  34. CGFloat max_minus_min = max_val - min_val;
  35. // Calculate h as a degree (0 - 360) measurement
  36. CGFloat h = 0;
  37. switch (max_elt)
  38. {
  39. case 'r':
  40. h = !max_minus_min?0:60*(g-b)/max_minus_min + 360;
  41. if (h >= 360) h -= 360;
  42. break;
  43. case 'g':
  44. h = !max_minus_min?0:60*(b-r)/max_minus_min + 120;
  45. break;
  46. case 'b':
  47. default:
  48. h = !max_minus_min?0:60*(r-g)/max_minus_min + 240;
  49. break;
  50. }
  51. // Normalize h
  52. h /= 360;
  53. // Calculate s
  54. CGFloat s = 0;
  55. if (max_val) s = max_minus_min/max_val;
  56. // Store HSV triple; v is just the max
  57. outputComponents[0] = h;
  58. outputComponents[1] = s;
  59. outputComponents[2] = max_val;
  60. }
  61. static float perceptualGlossFractionForColor(CGFloat* inputComponents)
  62. {
  63. static const float REFLECTION_SCALE_NUMBER = 0.2;
  64. static const float NTSC_RED_FRACTION = 0.299;
  65. static const float NTSC_GREEN_FRACTION = 0.587;
  66. static const float NTSC_BLUE_FRACTION = 0.114;
  67. float glossScale = NTSC_RED_FRACTION * inputComponents[0] +
  68. NTSC_GREEN_FRACTION * inputComponents[1] +
  69. NTSC_BLUE_FRACTION * inputComponents[2];
  70. return pow(glossScale, REFLECTION_SCALE_NUMBER);
  71. }
  72. static void perceptualCausticColorForColor(CGFloat* inputComponents, CGFloat* outputComponents)
  73. {
  74. static const float CAUSTIC_FRACTION = 0.35;
  75. static const float COSINE_ANGLE_SCALE = 1.4;
  76. static const float MIN_RED_THRESHOLD = 0.95;
  77. static const float MAX_BLUE_THRESHOLD = 0.7;
  78. static const float GRAYSCALE_CAUSTIC_SATURATION = 0.2;
  79. CGFloat temp[3];
  80. rgb_to_hsv(inputComponents, temp);
  81. CGFloat hue=temp[0], saturation=temp[1], brightness=temp[2];
  82. rgb_to_hsv(CGColorGetComponents([[UIColor yellowColor] CGColor]), temp);
  83. CGFloat targetHue=temp[0], targetBrightness=temp[2];
  84. if (saturation < 1e-3)
  85. {
  86. hue = targetHue;
  87. saturation = GRAYSCALE_CAUSTIC_SATURATION;
  88. }
  89. if (hue > MIN_RED_THRESHOLD)
  90. {
  91. hue -= 1.0;
  92. }
  93. else if (hue > MAX_BLUE_THRESHOLD)
  94. {
  95. rgb_to_hsv(CGColorGetComponents([[UIColor magentaColor] CGColor]), temp);
  96. targetHue=temp[0];
  97. targetBrightness=temp[2];
  98. }
  99. float scaledCaustic = CAUSTIC_FRACTION * 0.5 * (1.0 + cos(COSINE_ANGLE_SCALE * M_PI * (hue - targetHue)));
  100. UIColor* caustic = [UIColor colorWithHue:hue * (1.0 - scaledCaustic) + targetHue * scaledCaustic
  101. saturation:saturation
  102. brightness:brightness * (1.0 - scaledCaustic) + targetBrightness * scaledCaustic
  103. alpha:inputComponents[3]];
  104. const CGFloat* causticComponents = CGColorGetComponents([caustic CGColor]);
  105. for (int j = 3; j >= 0; j--) outputComponents[j] = causticComponents[j];
  106. }
  107. static void calc_glossy_color(void* info, const CGFloat* in, CGFloat* out)
  108. {
  109. GlossyParams* params = (GlossyParams*) info;
  110. float progress = *in;
  111. if (progress < 0.5)
  112. {
  113. progress = progress * 2.0;
  114. progress = 1.0 - params->expScale * (expf(progress * -params->expCoefficient) - params->expOffset);
  115. float currentWhite = progress * (params->finalWhite - params->initialWhite) + params->initialWhite;
  116. out[0] = params->color[0] * (1.0 - currentWhite) + currentWhite;
  117. out[1] = params->color[1] * (1.0 - currentWhite) + currentWhite;
  118. out[2] = params->color[2] * (1.0 - currentWhite) + currentWhite;
  119. out[3] = params->color[3] * (1.0 - currentWhite) + currentWhite;
  120. }
  121. else
  122. {
  123. progress = (progress - 0.5) * 2.0;
  124. progress = params->expScale * (expf((1.0 - progress) * -params->expCoefficient) - params->expOffset);
  125. out[0] = params->color[0] * (1.0 - progress) + params->caustic[0] * progress;
  126. out[1] = params->color[1] * (1.0 - progress) + params->caustic[1] * progress;
  127. out[2] = params->color[2] * (1.0 - progress) + params->caustic[2] * progress;
  128. out[3] = params->color[3] * (1.0 - progress) + params->caustic[3] * progress;
  129. }
  130. }
  131. @implementation UIButton (Glossy)
  132. + (void)setPathToRoundedRect:(CGRect)rect forInset:(NSUInteger)inset inContext:(CGContextRef)context
  133. {
  134. // Experimentally determined
  135. static const NSUInteger cornerRadius = 8;
  136. // Unpack size for compactness, find minimum dimension
  137. CGFloat w = rect.size.width;
  138. CGFloat h = rect.size.height;
  139. CGFloat m = w<h?w:h;
  140. // Special case: Degenerate rectangles abort this method
  141. if (m <= 0) return;
  142. // Bounds
  143. CGFloat b = rect.origin.y;
  144. CGFloat t = b + h;
  145. CGFloat l = rect.origin.x;
  146. CGFloat r = l + w;
  147. // Adjust radius for inset, and limit it to 1/2 of the rectangle's shortest axis
  148. CGFloat d = (inset<cornerRadius)?(cornerRadius-inset):0;
  149. d = (d>0.5*m)?(0.5*m):d;
  150. // Define a CW path in the CG co-ordinate system (origin at LL)
  151. CGContextBeginPath(context);
  152. CGContextMoveToPoint(context, (l+r)/2, t); // Begin at TDC
  153. CGContextAddArcToPoint(context, r, t, r, b, d); // UR corner
  154. CGContextAddArcToPoint(context, r, b, l, b, d); // LR corner
  155. CGContextAddArcToPoint(context, l, b, l, t, d); // LL corner
  156. CGContextAddArcToPoint(context, l, t, r, t, d); // UL corner
  157. CGContextClosePath(context); // End at TDC
  158. }
  159. + (void)drawGlossyRect:(CGRect)rect withColor:(UIColor*)color inContext:(CGContextRef)context
  160. {
  161. static const float EXP_COEFFICIENT = 4.0;
  162. static const float REFLECTION_MAX = 0.80;
  163. static const float REFLECTION_MIN = 0.20;
  164. static const CGFloat normalizedRanges[8] = {0, 1, 0, 1, 0, 1, 0, 1};
  165. static const CGFunctionCallbacks callbacks = {0, calc_glossy_color, NULL};
  166. // Prepare gradient configuration struct
  167. GlossyParams params;
  168. // Set the base color
  169. const CGFloat* colorComponents = CGColorGetComponents([color CGColor]);
  170. int j = (int) CGColorGetNumberOfComponents([color CGColor]);
  171. if (j == 4)
  172. {
  173. for (j--; j >= 0; j--) params.color[j] = colorComponents[j];
  174. }
  175. else if (j == 2)
  176. {
  177. for (; j >= 0; j--) params.color[j] = colorComponents[0];
  178. params.color[3] = colorComponents[1];
  179. }
  180. else
  181. {
  182. // I dunno
  183. return;
  184. }
  185. // Set the caustic color
  186. perceptualCausticColorForColor(params.color, params.caustic);
  187. // Set the exponent curve parameters
  188. params.expCoefficient = EXP_COEFFICIENT;
  189. params.expOffset = expf(-params.expCoefficient);
  190. params.expScale = 1.0/(1.0 - params.expOffset);
  191. // Set the highlight intensities
  192. float glossScale = perceptualGlossFractionForColor(params.color);
  193. params.initialWhite = glossScale * REFLECTION_MAX;
  194. params.finalWhite = glossScale * REFLECTION_MIN;
  195. CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
  196. CGFunctionRef function = CGFunctionCreate(&params, 1, normalizedRanges, 4, normalizedRanges, &callbacks);
  197. CGPoint sp = CGPointMake(CGRectGetMidX(rect), CGRectGetMaxY(rect));
  198. CGPoint ep = CGPointMake(CGRectGetMidX(rect), CGRectGetMinY(rect));
  199. CGShadingRef shader = CGShadingCreateAxial(colorSpace, sp, ep, function, NO, NO);
  200. CGFunctionRelease(function);
  201. CGColorSpaceRelease(colorSpace);
  202. CGContextDrawShading(context, shader);
  203. CGShadingRelease(shader);
  204. }
  205. + (void)setBackgroundToGlossyButton:(UIButton*)button forColor:(UIColor*)color withBorder:(BOOL)border forState:(UIControlState)state{
  206. static const float MIN_SIZE = 4;
  207. // Get and check size
  208. CGSize size = button.frame.size;
  209. if ((size.width < MIN_SIZE) || (size.height < MIN_SIZE)) return;
  210. // Create and get a pointer to context
  211. UIGraphicsBeginImageContext(size);
  212. CGContextRef context = UIGraphicsGetCurrentContext();
  213. // Convert co-ordinate system to Cocoa's (origin in UL, not LL)
  214. CGContextTranslateCTM(context, 0, size.height);
  215. CGContextConcatCTM(context, CGAffineTransformMakeScale(1, -1));
  216. // Set stroke color
  217. CGContextSetStrokeColorWithColor(context, [[UIColor colorWithRed:159.0/255 green:159.0/255 blue:159.0/255 alpha:1] CGColor]);
  218. // Draw background image
  219. if (border)
  220. {
  221. // Draw border
  222. [UIButton setPathToRoundedRect:CGRectMake(0.5, 0.5, size.width-1, size.height-1) forInset:0 inContext:context];
  223. CGContextStrokePath(context);
  224. // Prepare clipping region
  225. [UIButton setPathToRoundedRect:CGRectMake(1, 1, size.width-2, size.height-2) forInset:1 inContext:context];
  226. CGContextClip(context);
  227. // Draw glossy image
  228. [UIButton drawGlossyRect:CGRectMake(1, 1, size.width-2, size.height-2) withColor:color inContext:context];
  229. }
  230. else
  231. {
  232. // Prepare clipping region
  233. [UIButton setPathToRoundedRect:CGRectMake(0, 0, size.width, size.height) forInset:0 inContext:context];
  234. CGContextClip(context);
  235. // Draw glossy image
  236. [UIButton drawGlossyRect:CGRectMake(0, 0, size.width, size.height) withColor:color inContext:context];
  237. }
  238. // Create and assign image
  239. [button setBackgroundImage:UIGraphicsGetImageFromCurrentImageContext() forState:state];
  240. // Release image context
  241. UIGraphicsEndImageContext();
  242. }
  243. @end