ColourPicker.vue 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. <template>
  2. <div class='colour_picker'>
  3. <div class='colour_picker__selected_header'>
  4. <div class='colour_picker__selected_header__text'>Selected colour</div>
  5. <div
  6. class='colour_picker__selected'
  7. :style='{
  8. "background-color": colour
  9. }'
  10. ></div>
  11. </div>
  12. <div class='colour_picker__selector_divider'>
  13. <div
  14. class='colour_picker__palette_picker'
  15. :style='{
  16. left: palettePicker.left + "px",
  17. top: palettePicker.top + "px"
  18. }'
  19. @mousedown.prevent.stop='palettePicker.dragging = true'
  20. @mouseup.prevent.stop='palettePicker.dragging = false; emit()'
  21. ></div>
  22. <canvas
  23. class='colour_picker__palette'
  24. ref='palette'
  25. :width='dimensions'
  26. :height='dimensions'
  27. @click='(e) => { updatePalettePicker(e); emit(); }'
  28. >
  29. </canvas>
  30. </div>
  31. <div class='colour_picker__selector_divider'>
  32. <div
  33. class='colour_picker__hue_picker'
  34. :style='{
  35. left: huePicker.left + "px"
  36. }'
  37. @mousedown.prevent.stop='huePicker.dragging = true'
  38. @mouseup.prevent.stop='huePicker.dragging = false; emit();'
  39. ></div>
  40. <canvas
  41. class='colour_picker__hue'
  42. ref='hue'
  43. :width='dimensions'
  44. :height='hueHeight'
  45. @click='(e) => { updateHuePicker(e); emit(); }'
  46. >
  47. </canvas>
  48. </div>
  49. </div>
  50. </template>
  51. <script>
  52. export default {
  53. name: 'ColourPicker',
  54. props: ['value'],
  55. data () {
  56. return {
  57. dimensions: 100,
  58. hueHeight: 20,
  59. palettePicker: {
  60. left: 0,
  61. top: 0,
  62. dragging: false
  63. },
  64. huePicker: {
  65. left: 0,
  66. dragging: false
  67. },
  68. hue_: 0,
  69. saturation_: 0,
  70. lightness_: 0
  71. }
  72. },
  73. computed: {
  74. colour () {
  75. let hsl = (
  76. 'hsl(' +
  77. this.hue +
  78. ', ' +
  79. this.saturation +
  80. '%, ' +
  81. this.lightness +
  82. '%)'
  83. );
  84. return hsl;
  85. },
  86. hue: {
  87. get () {
  88. return this.hue_;
  89. },
  90. set (val) {
  91. this.hue_ = val;
  92. if(!this.$refs.hue) return;
  93. let width = this.$refs.hue.getBoundingClientRect().width;
  94. this.huePicker.left = Math.round((val * width) / 360 - 2);
  95. }
  96. },
  97. saturation: {
  98. get () {
  99. return this.saturation_;
  100. },
  101. set (val) {
  102. this.saturation_ = val;
  103. if(!this.$refs.palette) return;
  104. this.$refs.palette.getBoundingClientRect()
  105. let width = this.$refs.palette.getBoundingClientRect().width;
  106. this.palettePicker.left = Math.round((val * width) / 100 - 8);
  107. }
  108. },
  109. lightness: {
  110. get () {
  111. return this.lightness_;
  112. },
  113. set (val) {
  114. this.lightness_ = val;
  115. if(!this.$refs.palette) return;
  116. let height = this.$refs.palette.getBoundingClientRect().height;
  117. this.palettePicker.top = Math.round(height - (val * height) / 100 - 8);
  118. }
  119. }
  120. },
  121. watch: {
  122. value () {
  123. let e = document.createElement('span');
  124. e.style.backgroundColor = this.value;
  125. let rgbString = e.style.backgroundColor;
  126. let rgbArray = this.rgbStringToArray(rgbString);
  127. let hslArray = this.rgbToHsl(rgbArray);
  128. [this.hue, this.saturation, this.lightness] = hslArray;
  129. this.drawPalette();
  130. }
  131. },
  132. methods: {
  133. emit () {
  134. let e = document.createElement('span');
  135. e.style.backgroundColor = this.colour;
  136. this.$emit('input', e.style.backgroundColor);
  137. },
  138. drawPalette () {
  139. const ctx = this.$refs.palette.getContext('2d');
  140. ctx.clearRect(0, 0, this.dimensions, this.dimensions);
  141. for(let x = 0; x <= this.dimensions; x++) {
  142. for(let y = 0; y <= this.dimensions; y++) {
  143. let saturation = 100 * x / this.dimensions + '%';
  144. let lightness = 100 * (this.dimensions - y) / this.dimensions + '%';
  145. ctx.fillStyle = 'hsl(' + this.hue + ', ' + saturation + ', ' + lightness + ')';
  146. ctx.fillRect(x, y, 1, 1);
  147. }
  148. }
  149. },
  150. drawHue () {
  151. const ctx = this.$refs.hue.getContext('2d');
  152. for(let x = 0; x <= this.dimensions; x++) {
  153. let angle = (x / this.dimensions * 360);
  154. ctx.fillStyle = 'hsl(' + angle + ', 100%, 50%)'
  155. ctx.fillRect(x, 0, 1, this.hueHeight);
  156. }
  157. },
  158. updatePalettePicker (e) {
  159. //If the canvas is not loaded
  160. //Or there's no dragging and not a click event
  161. if(
  162. !this.$refs.palette ||
  163. (!this.palettePicker.dragging && e.type !== 'click')
  164. ) return;
  165. let rect = this.$refs.palette.getBoundingClientRect();
  166. let left = e.clientX - rect.left - 8;
  167. let top = e.clientY - rect.top - 8;
  168. if (e.clientX > rect.right) left = rect.width - 8;
  169. if (e.clientX < rect.left) left = -8;
  170. if (e.clientY > rect.bottom) top = rect.height - 8;
  171. if (e.clientY < rect.top) top = -8;
  172. this.palettePicker.left = left;
  173. this.palettePicker.top = top;
  174. let centerX = left + 8;
  175. let centerY = top + 8;
  176. this.saturation_ = Math.round(100 * centerX / rect.width);
  177. this.lightness_ = Math.round(100 * (rect.height - centerY) / rect.height);
  178. },
  179. updateHuePicker (e) {
  180. //If the canvas is not loaded
  181. //Or there's no dragging and not a click event
  182. if(
  183. !this.$refs.hue ||
  184. (!this.huePicker.dragging && e.type !== 'click')
  185. ) return;
  186. let rect = this.$refs.hue.getBoundingClientRect();
  187. let left = e.clientX - rect.left - 2;
  188. if (e.clientX > rect.right) left = rect.width - 2;
  189. if (e.clientX < rect.left) left = -2;
  190. this.huePicker.left = left;
  191. this.hue_ = Math.round(360 * (left + 2) / rect.width);
  192. this.drawPalette();
  193. },
  194. //rgb(1,2,3) => [1,2,3]
  195. rgbStringToArray (str) {
  196. return str.slice(4, -1).split(',').map(Number);
  197. },
  198. //[1,2,3] => [210, 50, 0.8]
  199. rgbToHsl (rgb) {
  200. let h, s, l;
  201. let normalised = rgb.map(v => v / 255);
  202. let [r, g, b] = normalised;
  203. let max = Math.max(...normalised);
  204. let min = Math.min(...normalised);
  205. l = 100 * (max + min) / 2;
  206. if(max === min) {
  207. return [this.hue, 0, l];
  208. } else {
  209. if(l < 50) {
  210. s = (max - min) / (max + min);
  211. } else {
  212. s = (max - min) / (2 - max - min);
  213. }
  214. //Turn into percentage
  215. s *= 100;
  216. }
  217. if(r === max) {
  218. h = (g - b) / (max - min);
  219. } else if (g === max) {
  220. h = 2 + (b - r) / (max - min);
  221. } else {
  222. h = 4 + (r - g) / (max - min);
  223. }
  224. //Convert to degrees
  225. h *= 60;
  226. return [h, s, l].map(v => Math.round(v));
  227. }
  228. },
  229. mounted () {
  230. this.drawPalette();
  231. this.drawHue();
  232. document.addEventListener('mousemove', e => {
  233. this.updatePalettePicker(e);
  234. this.updateHuePicker(e);
  235. })
  236. }
  237. }
  238. </script>
  239. <style lang='scss' scoped>
  240. @import '../assets/scss/variables.scss';
  241. .colour_picker {
  242. @at-root #{&}__selected_header {
  243. display: flex;
  244. justify-content: space-between;
  245. align-items: middle;
  246. @at-root #{&}__text {
  247. height: 1.5rem;
  248. line-height: 1.4rem;
  249. @include user-select(none);
  250. }
  251. }
  252. @at-root #{&}__selected {
  253. width: 1.5rem;
  254. height: 1.5rem;
  255. border: thin solid $color__gray--darkest;
  256. border-radius: 0.25rem;
  257. margin-bottom: 0.5rem;
  258. }
  259. @at-root #{&}__selector_divider {
  260. display: flex;
  261. position: relative;
  262. }
  263. @at-root #{&}__palette_picker, #{&}__hue_picker {
  264. position: absolute;
  265. background-color: #fff;
  266. border: thin solid rgba($color__darkgray--darker, 0.5);
  267. cursor: pointer;
  268. transition: box-shadow 0.2s;
  269. &:hover {
  270. box-shadow: 0 0 1px rgba(black, 0.3);
  271. }
  272. }
  273. @at-root #{&}__palette_picker {
  274. height: 15px;
  275. width: 15px;
  276. border-radius: 100%;
  277. }
  278. @at-root #{&}__hue_picker {
  279. height: 1.75rem;
  280. width: 5px;
  281. top: -0.125rem;
  282. border-radius: 1rem;
  283. }
  284. @at-root #{&}__palette, #{&}__hue {
  285. width: 100%;
  286. height: 8rem;
  287. border: thin solid $color__gray--darkest;
  288. border-radius: 0.25rem;
  289. margin-bottom: 0.5rem;
  290. }
  291. @at-root #{&}__hue {
  292. height: 1.5rem;
  293. }
  294. }
  295. </style>