v1.php 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. <?php
  2. namespace zin;
  3. class avatar extends wg
  4. {
  5. /**
  6. * @var mixed[]
  7. */
  8. protected static $defineProps = array(
  9. 'className?: string',
  10. 'style?: array',
  11. 'size?: int=32',
  12. 'circle?: bool=true',
  13. 'rounded?: string|int',
  14. 'background?: string',
  15. 'foreColor?: string',
  16. 'text?: string',
  17. 'code?: string',
  18. 'maxTextLength?: int=2',
  19. 'hueDistance?: int=43',
  20. 'saturation?: int=0.4',
  21. 'lightness?: int=0.6',
  22. 'src?: string'
  23. );
  24. private $textLen = 0;
  25. private $displayTextLen = 0;
  26. private $actualSize = 32;
  27. private $finalClass = array('avatar');
  28. private $finalStyle;
  29. /**
  30. * @param mixed $child
  31. */
  32. protected function onAddChild($child)
  33. {
  34. if(is_string($child) && !$this->props->has('text'))
  35. {
  36. $this->setProp('text', $child);
  37. return false;
  38. }
  39. return $child;
  40. }
  41. protected function build()
  42. {
  43. /* Attach classes. */
  44. $this->finalClass[] = $this->prop('className');
  45. /* Init style. */
  46. $this->finalStyle = new stdClass();
  47. $this->finalStyle->background = $this->prop('background');
  48. $this->finalStyle->color = $this->prop('foreColor');
  49. foreach($this->props->style->toJSON() as $attr => $val) $this->finalStyle->{$attr} = $val;
  50. /* Init avatar size. */
  51. $this->initSize();
  52. /* Init avatar shape. */
  53. $this->initShape();
  54. $content = $this->getContent();
  55. $finalStyle = json_decode(json_encode($this->finalStyle), true);
  56. return h::div
  57. (
  58. setClass($this->finalClass),
  59. setStyle($finalStyle),
  60. set($this->getRestProps()),
  61. $content,
  62. $this->children()
  63. );
  64. }
  65. private function initSize()
  66. {
  67. $size = $this->prop('size');
  68. $this->actualSize = $size;
  69. if(!$size) return;
  70. if(is_numeric($size))
  71. {
  72. $fontSize = intval($size/2) > 12 ? intval($size/2) : 12;
  73. $this->finalStyle->width = "{$size}px";
  74. $this->finalStyle->height = "{$size}px";
  75. $this->finalStyle->{'font-size'} = "{$fontSize}px";
  76. return;
  77. }
  78. $sizeMap = array('xs' => 20, 'sm' => 24, 'lg' => 48, 'xl' => 80);
  79. $this->finalClass[] = "size-{$size}";
  80. $this->actualSize = isset($sizeMap[$size]) ? $sizeMap[$size] : 20;
  81. }
  82. private function initShape()
  83. {
  84. $circle = $this->prop('circle');
  85. $rounded = $this->prop('rounded');
  86. /* Set circle. */
  87. if($circle)
  88. {
  89. $this->finalClass[] = 'rounded-full';
  90. }
  91. else if($rounded)
  92. {
  93. if(is_numeric($rounded)) $this->finalStyle->{'border-radius'} = "{$rounded}px";
  94. else $this->finalClass[] = "rounded-{$rounded}";
  95. }
  96. }
  97. private function getAvatarText()
  98. {
  99. $maxTextLen = intval($this->prop('maxTextLength', 2));
  100. $text = strtoupper($this->prop('text', ''));
  101. $mbLength = mb_strlen($text, 'utf-8');
  102. $strLength = strlen($text);
  103. $displayText = '';
  104. if($strLength === $mbLength)
  105. {
  106. /* Pure alphabet or numbers 纯英文情况 */
  107. $displayText = strtoupper($text[0]);
  108. }
  109. else if($strLength % $mbLength == 0 && $strLength % 3 == 0)
  110. {
  111. /* Pure chinese characters 纯中文的情况 */
  112. $displayText = $mbLength <= $maxTextLen ? $text : mb_substr($text, $mbLength - $maxTextLen, $mbLength, 'utf-8');
  113. }
  114. else
  115. {
  116. /* Mix of Chinese and English 中英文混合的情况 */
  117. $displayText = $mbLength <= $maxTextLen ? $text : mb_substr($text, 0, $maxTextLen, 'utf-8');
  118. }
  119. $this->textLen = mb_strlen($displayText, 'utf-8');
  120. $this->displayTextLen = $this->textLen + ((strlen($displayText) - $this->textLen) / 2);
  121. return $displayText;
  122. }
  123. /**
  124. * Convert HSL values to RGB value.
  125. *
  126. * @param int $h
  127. * @param int $s
  128. * @param int $l
  129. * @access private
  130. * @return array
  131. */
  132. private function hslToRgb($h, $s, $l)
  133. {
  134. $h = ($h % 360) / 360;
  135. $s = ($s > 0 ? $s : 0);
  136. $s = ($s > 255) ? 255 : $s;
  137. $l = ($l > 0 ? $l : 0);
  138. $l = ($l > 255) ? 255 : $l;
  139. $m2 = ($l <= 0.5) ? ($l * ($s + 1)) : ($l + $s - $l * $s);
  140. $m1 = $l * 2 - $m2;
  141. $hueFn = function($val, $m1, $m2)
  142. {
  143. $val = $val < 0 ? $val + 1 : ($val > 1 ? $val - 1 : $val);
  144. if($val * 6 < 1) return $m1 + ($m2 - $m1) * $val * 6;
  145. elseif($val * 2 < 1) return $m2;
  146. elseif($val * 3 < 2) return $m1 + ($m2 - $m1) * (2/3 - $val) * 6;
  147. return $m1;
  148. };
  149. return array(
  150. 'r' => $hueFn($h + 1/3, $m1, $m2) * 255,
  151. 'g' => $hueFn($h, $m1, $m2) * 255,
  152. 'b' => $hueFn($h - 1/3, $m1, $m2) * 255
  153. );
  154. }
  155. private function hex2Rgb($hex)
  156. {
  157. if(!str_starts_with($hex, '#') || !preg_match('/#[0-9A-F]{3,6}$/', $hex)) throw new \Exception('incorrect data format');
  158. $r = 0;
  159. $g = 0;
  160. $b = 0;
  161. if(strlen($hex) == 4) list($r, $g, $b) = sscanf($hex, "#%01x%01x%01x");
  162. elseif(strlen($hex) == 7) list($r, $g, $b) = sscanf($hex, "#%02x%02x%02x");
  163. else throw new \Exception('incorrect RGB value');
  164. return array(
  165. 'r' => $r,
  166. 'g' => $g,
  167. 'b' => $b
  168. );
  169. }
  170. /*
  171. * Get contrast color.
  172. *
  173. * @param array|string $rgb
  174. * @param string $theme dark|light
  175. * @access private
  176. * @return string
  177. */
  178. private function contrastColor($rgb, $themeDark = null, $themeLight = null)
  179. {
  180. $rgb = is_array($rgb) ? $rgb : $this->hex2Rgb($rgb);
  181. $r = $rgb['r'];
  182. $g = $rgb['g'];
  183. $b = $rgb['b'];
  184. if(($r * 0.299 + $g * 0.587 + $b * 0.114) > 186)
  185. {
  186. /* Is light color. */
  187. return $themeDark ? $themeDark : '#333333';
  188. }
  189. return $themeLight ? $themeLight : '#ffffff';
  190. }
  191. private function getTextStyle()
  192. {
  193. $hueDistance = intval($this->prop('hueDistance'));
  194. $saturation = $this->prop('saturation');
  195. $lightness = $this->prop('lightness');
  196. $background = $this->prop('background');
  197. $foreColor = $this->prop('foreColor');
  198. $code = $this->prop('code');
  199. $avatarCode = $code ? $code : $this->prop('text');
  200. if(!$background)
  201. {
  202. $val = 0;
  203. if(is_numeric($avatarCode))
  204. {
  205. $val = intval($avatarCode);
  206. }
  207. else
  208. {
  209. for($i = 0; $i < strlen($avatarCode); $i++) $val += ord($avatarCode[$i]);
  210. }
  211. $hue = $val * $hueDistance % 360;
  212. $actualSat = $saturation * 100;
  213. $actualLight = $lightness * 100;
  214. $this->finalStyle->background = "hsl({$hue}, {$actualSat}%, {$actualLight}%)";
  215. if(!$foreColor)
  216. {
  217. $rgb = $this->hslToRgb($hue, $saturation, $lightness);
  218. $this->finalStyle->color = $this->contrastColor($rgb);
  219. }
  220. }
  221. elseif (!$foreColor && $background)
  222. {
  223. $this->finalStyle->color = $this->contrastColor($background);
  224. }
  225. $textStyle = array();
  226. if($this->actualSize and $this->actualSize < (10 * $this->displayTextLen))
  227. {
  228. $textStyle = array(
  229. 'transform' => 'scale(' . $this->actualSize / (10 * $this->displayTextLen) . ')',
  230. 'white-space' => 'nowrap'
  231. );
  232. }
  233. return $textStyle;
  234. }
  235. private function getContent()
  236. {
  237. $src = $this->prop('src');
  238. $text = $this->prop('text');
  239. $code = $this->prop('code');
  240. /* With avatar. */
  241. if($src)
  242. {
  243. $this->finalClass[] = 'has-img';
  244. return h::img
  245. (
  246. setClass('avatar-img'),
  247. set('src', $src),
  248. set('alt', $text),
  249. set('data-code', $code)
  250. );
  251. }
  252. /* Without text and image. */
  253. if(!$text) return null;
  254. $displayText = $this->getAvatarText();
  255. $this->finalClass[] = 'has-text';
  256. $this->finalClass[] = 'has-text-' . $this->textLen;
  257. $textStyle = $this->getTextStyle();
  258. return h::div
  259. (
  260. setClass('avatar-text'),
  261. set('data-actualSize', $this->actualSize),
  262. $textStyle ? setStyle($textStyle) : null,
  263. $displayText
  264. );
  265. }
  266. }