disk-color-picker
Version:
A color picker component
2 lines (1 loc) • 17 kB
JavaScript
function t(t){let s=-1;return self.Touch&&t instanceof Touch?s=t.identifier:e(t)&&(s=t.pointerId),{id:s,nativeEvent:t,clientX:t.clientX,clientY:t.clientY}}const e=t=>self.PointerEvent&&t instanceof PointerEvent;class s{constructor(s,i){this.startPointers=[],this.currentPointers=[],this.pointerStart=s=>{if(0===s.button&&this.triggerPointerStart(t(s),s))if(e(s)){(s.target&&"setPointerCapture"in s.target?s.target:this._e).setPointerCapture(s.pointerId),this.ael("pointermove",this.move),this.ael("pointerup",this.pointerEnd),this.ael("pointercancel",this.pointerEnd)}else window.addEventListener("mousemove",this.move),window.addEventListener("mouseup",this.pointerEnd)},this.touchStart=e=>{for(const s of Array.from(e.changedTouches))this.triggerPointerStart(t(s),e)},this.move=e=>{const s="changedTouches"in e?Array.from(e.changedTouches).map((e=>t(e))):[t(e)],i=[];for(const t of s){const e=this.currentPointers.findIndex((e=>e.id===t.id));-1!==e&&(i.push(t),this.currentPointers[e]=t)}0!==i.length&&this._h.onMove(i,e)},this._end=(t,e)=>{const s=this.currentPointers.findIndex((e=>e.id===t.id));return-1!==s&&(this.currentPointers.splice(s,1),this.startPointers.splice(s,1),this._h.onEnd&&this._h.onEnd(t,e,"touchcancel"===e.type||"pointercancel"===e.type),!0)},this.pointerEnd=s=>{if(this._end(t(s),s))if(e(s)){if(this.currentPointers.length)return;this.rel("pointermove",this.move),this.rel("pointerup",this.pointerEnd),this.rel("pointercancel",this.pointerEnd)}else window.removeEventListener("mousemove",this.move),window.removeEventListener("mouseup",this.pointerEnd)},this.touchEnd=e=>{for(const s of Array.from(e.changedTouches))this._end(t(s),e)},this._e=s,this._h=i,self.PointerEvent?this.ael("pointerdown",this.pointerStart):(this.ael("mousedown",this.pointerStart),this.ael("touchstart",this.touchStart),this.ael("touchmove",this.move),this.ael("touchend",this.touchEnd),this.ael("touchcancel",this.touchEnd))}ael(t,e){this._e.addEventListener(t,e)}rel(t,e){this._e.removeEventListener(t,e)}stop(){this.rel("pointerdown",this.pointerStart),this.rel("mousedown",this.pointerStart),this.rel("touchstart",this.touchStart),this.rel("touchmove",this.move),this.rel("touchend",this.touchEnd),this.rel("touchcancel",this.touchEnd),this.rel("pointermove",this.move),this.rel("pointerup",this.pointerEnd),this.rel("pointercancel",this.pointerEnd),window.removeEventListener("mousemove",this.move),window.removeEventListener("mouseup",this.pointerEnd)}triggerPointerStart(t,e){return!!this._h.onStart(t,e)&&(this.currentPointers.push(t),this.startPointers.push(t),!0)}}function i(t){return 180/Math.PI*t}function n(t){return Math.PI/180*t}class h{constructor(t,e,i,n=0,h=360){this.anchor=[0,0,0,0],this.degrees=0,this.e=t,this.ri=e,this.ro=i,this.a1=n,this.a2=h,this.tracker=new s(this.e,this)}clamp(t){for(;t<0;)t+=360;return Math.abs(t%360)}isAngleInRange(t){if(this.a1<0){if(t>=0&&t<=this.a2)return!0;const e=t-360;return e>=this.a1&&e<=0}return t>=this.a1&&t<=this.a2}isOnDial(t,e){const s=t-.5,n=e-.5,h=Math.sqrt(s*s+n*n);if(h>=this.ri&&h<=this.ro){const t=this.clamp(Math.round(i(Math.atan2(n,s))));return this.isAngleInRange(t)}return!1}onStart(t,e){e.preventDefault();const s=this.e.getBoundingClientRect();this.anchor=[s.left||s.x,s.top||s.y,s.width,s.height];const i=this.anchor[2],n=this.anchor[3],h=i?(t.clientX-this.anchor[0])/i:0,o=n?(t.clientY-this.anchor[1])/n:0;return!!this.isOnDial(h,o)&&(this.setPosition(h,o),this.e.style.cursor="pointer",!0)}onMove(t){const e=t[0];if(e){const t=this.anchor[2],s=this.anchor[3],i=t?(e.clientX-this.anchor[0])/t:0,n=s?(e.clientY-this.anchor[1])/s:0;this.setPosition(i,n)}}onEnd(){this.e.style.cursor=""}setPosition(t,e){const s=this.clamp(Math.round(i(Math.atan2(e-.5,t-.5))));return!(!this.isAngleInRange(s)||s===this.degrees)&&(this.degrees=s,this.fire(),!0)}fire(){this.e.dispatchEvent(new CustomEvent("p-input",{bubbles:!0,composed:!0,detail:{angle:this.degrees}}))}detach(){this.tracker.stop()}get angle(){return this.degrees}set angle(t){this.degrees=Math.max(this.a1,Math.min(this.a2,this.clamp(t)))}}class o extends HTMLElement{constructor(){super(),this.__n=new Map,this.root=this.attachShadow({mode:"open"})}static get observedAttributes(){return["value"]}attributeChangedCallback(t,e,s){"value"===t&&(this.value=s)}$(t){if(this.__n.has(t))return this.__n.get(t);const e=this.root.querySelector("#"+t);return this.__n.set(t,e),e}$add(t,e,s){"string"==typeof t&&(t=this.$(t)),t&&t.addEventListener(e,s)}$remove(t,e,s){"string"==typeof t&&(t=this.$(t)),t&&t.removeEventListener(e,s)}fire(t,e){!function(t,e,s){t.dispatchEvent(new CustomEvent(e,{bubbles:!0,composed:!0,detail:s}))}(this,t,e)}disconnectedCallback(){this.__n.clear()}}const r=/^hsla\s*\(\s*(\d+.?\d*)\s*,\s*(\d+.?\d*)%\s*,\s*(\d+.?\d*)%\s*,\s*(\d+.?\d*)\s*\)$/i,a=/^hsl\s*\(\s*(\d+.?\d*)\s*,\s*(\d+.?\d*)%\s*,\s*(\d+.?\d*)%\s*\)$/i,d=/^rgba\s*\(\s*(\d+.?\d*)\s*,\s*(\d+.?\d*)\s*,\s*(\d+.?\d*)\s*,\s*(\d+.?\d*)\s*\)$/i,l=/^rgb\s*\(\s*(\d+.?\d*)\s*,\s*(\d+.?\d*)\s*,\s*(\d+.?\d*)\s*\)$/i,c=t=>`hsl(${t[0]%360}, ${t[1]}%, ${t[2]}%)`;function u(t,e,s){t%=360,e/=100,s/=100;const i=(1-Math.abs(2*s-1))*e,n=i*(1-Math.abs(t/60%2-1)),h=s-i/2;let o=0,r=0,a=0;return[o,r,a]=t<60?[i,n,0]:t<120?[n,i,0]:t<180?[0,i,n]:t<240?[0,n,i]:t<300?[n,0,i]:[i,0,n],[Math.round(255*(o+h)),Math.round(255*(r+h)),Math.round(255*(a+h))]}function p(t,e,s){t/=255,e/=255,s/=255;const i=Math.max(t,e,s),n=Math.min(t,e,s),h=i-n;let o=0;0===h?o=0:i===t?o=(e-s)/h%6*60:i===e?o=60*((s-t)/h+2):i===s&&(o=60*((t-e)/h+4));const r=(i+n)/2,a=0===h?0:h/(1-Math.abs(2*r-1));return[Math.round((360+o)%360),Math.round(100*a),Math.round(100*r)]}function m(t,e,s,i){i=Math.round(255*i);let n=Math.round(t).toString(16),h=Math.round(e).toString(16),o=Math.round(s).toString(16),r=i.toString(16);return 1===n.length&&(n="0"+n),1===h.length&&(h="0"+h),1===o.length&&(o="0"+o),1===r.length&&(r="0"+r),(255===i?`#${n}${h}${o}`:`#${n}${h}${o}${r}`).toUpperCase()}class b{constructor(t,e,i){this.anchor=[0,0,0,0],this._a=0,this._d=0,this.e=t,e&&(this._a=this.clampAngle(e)),i&&(this._d=this.clampDist(i)),this.tracker=new s(this.e,this)}clampAngle(t){for(;t<0;)t+=360;return Math.abs(t%360)}clampDist(t){return Math.max(0,Math.min(1,t))}onMove(t){const e=t[0];if(e){const t=this.anchor[2],s=this.anchor[3],i=t?(e.clientX-this.anchor[0])/t:0,n=s?(e.clientY-this.anchor[1])/s:0;this.setPosition(i,n)}}onEnd(){this.e.style.cursor=""}onStart(t,e){e.preventDefault();const s=this.e.getBoundingClientRect();this.anchor=[s.left||s.x,s.top||s.y,s.width,s.height];const i=this.anchor[2],n=this.anchor[3],h=i?(t.clientX-this.anchor[0])/i:0,o=n?(t.clientY-this.anchor[1])/n:0;return this.setPosition(h,o),this.e.style.cursor="pointer",!0}setPosition(t,e){let s=!1;const n=this.clampAngle(Math.round(i(Math.atan2(e-.5,t-.5))));n!==this._a&&(this._a=n,s=!0);const h=t-.5,o=e-.5,r=this.clampDist(2*Math.sqrt(h*h+o*o));return r!==this._d&&(this._d=r,s=!0),s&&this.fire(),s}fire(){this.e.dispatchEvent(new CustomEvent("p-input",{bubbles:!0,composed:!0,detail:{angle:this._a,distance:this._d}}))}detach(){this.tracker.stop()}get angle(){return this._a}set angle(t){this._a=this.clampAngle(t)}get distance(){return this._d}set distance(t){this._d=this.clampDist(t)}}class g extends o{constructor(){super(),this._hsla=[0,100,50,1],this.prevWheelParams=[-1,1],this.onWheelFocus=()=>this.$("wheelThumb").classList.add("focused"),this.onWheelBlur=()=>this.$("wheelThumb").classList.remove("focused"),this.onDiskFocus=()=>this.$("diskThumb").classList.add("focused"),this.onDiskBlur=()=>this.$("diskThumb").classList.remove("focused"),this.onWheelKeyDown=t=>{let e=!0;const s=t.code,i=this._hsla[2];switch(s){case"ArrowUp":case"ArrowRight":i>0&&(this._hsla[2]=Math.ceil(i)-1,this.updateColor());break;case"ArrowLeft":case"ArrowDown":i<100&&(this._hsla[2]=Math.floor(i)+1,this.updateColor());break;case"End":0!==i&&(this._hsla[2]=0,this.updateColor());break;case"Home":100!==i&&(this._hsla[2]=100,this.updateColor());break;case"Escape":this.$("wheelThumbInput").blur();break;default:e=!1}e&&(t.preventDefault(),t.stopPropagation())},this.onDiskKeyDown=t=>{let e=!0;switch(t.code){case"ArrowRight":this._hsla[0]=(this._hsla[0]+2)%360,this.updateColor();break;case"ArrowLeft":this._hsla[0]=((this._hsla[0]||360)-2)%360,this.updateColor();break;case"ArrowUp":this._hsla[1]<100&&(this._hsla[1]++,this.updateColor());break;case"ArrowDown":this._hsla[1]>0&&(this._hsla[1]--,this.updateColor());break;case"Escape":this.$("diskThumbInput").blur();break;default:e=!1}e&&(t.preventDefault(),t.stopPropagation())},this._renderPending=!1,this.handleDialInput=t=>{this.$("wheelThumbInput").focus();const e=t.detail.angle;this._hsla[2]=100/180*e,this.updateColor(),this._fire()},this.handleDiskInput=t=>{this.$("diskThumbInput").focus();const{angle:e,distance:s}=t.detail;this._hsla[0]=e,this._hsla[1]=Math.round(100*s),this.updateColor(),this._fire()},this.root.innerHTML='\n <style>\n \n* {box-sizing: border-box;}\n.horizontal {display: flex; flex-direction: row;}\n.vertical {display: flex; flex-direction: column;}\n.center {align-items: center;}\n.flex {flex: 1;}\n\n :host {\n display: inline-block;\n touch-action: none;\n }\n canvas {\n display: block;\n margin: 0 auto;\n }\n #base {\n position: relative;\n }\n #diskPanel {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n display: grid;\n place-items: center;\n place-content: center;\n pointer-events: none;\n padding: 0 0 6px;\n }\n .knob {\n position: relative;\n width: 20px;\n height: 20px;\n border: 2px solid #fff;\n box-shadow: 0 2px 1px -1px rgba(0,0,0,.2), 0 1px 1px 0 rgba(0,0,0,.14), 0 1px 3px 0 rgba(0,0,0,.12);\n border-radius: 50%;\n background: var(--ecp-i-thumb-color);\n }\n .thumb {\n position: absolute;\n width: 40px;\n height: 40px;\n padding: 10px;\n border-radius: 50%;\n overflow: hidden;\n background: transparent;\n top: -20px;\n left: -20px;\n }\n .thumb::before {\n content: \'\';\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n border-radius: 50%;\n opacity: 0.2;\n background: var(--ecp-i-thumb-shadow-color);\n pointer-events: none;\n transform: scale(0);\n transition: transform 0.18s ease;\n }\n .thumb.focused::before {\n transform: scale(1);\n }\n #wheelThumb {\n pointer-events: none;\n }\n #diskThumb {\n pointer-events: auto;\n cursor: pointer;\n }\n #disk {\n border-radius: 50%;\n }\n #diskTarget {\n position: relative;\n border-radius: 50%;\n pointer-events: auto;\n }\n input {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n border: none;\n cursor: pointer;\n opacity: 0;\n font-size: 16px;\n }\n </style>\n <div id="base">\n <canvas id="wheel" width="280" height="280"></canvas>\n <div id="wheelThumb" class="thumb">\n <div class="knob"></div>\n <input readonly id="wheelThumbInput" tabindex="2">\n </div>\n <div id="diskPanel">\n <div id="diskTarget">\n <canvas id="disk" width="180" height="180"></canvas>\n <div id="diskThumb" class="thumb">\n <div class="knob"></div>\n <input readonly id="diskThumbInput" tabindex="1">\n </div>\n </div>\n </div>\n </div>\n '}connectedCallback(){const t=this.$("wheel"),{width:e,height:s}=t,i=Math.min(e,s),n=i/2-2+10,o=n-50;this.dialC=new h(t,o/i,n/i,0,180),this.$add(t,"p-input",this.handleDialInput);const r=this.$("diskTarget");this.diskC=new b(r,0,0),this.$add(r,"p-input",this.handleDiskInput),this.$add("wheelThumbInput","focus",this.onWheelFocus),this.$add("wheelThumbInput","blur",this.onWheelBlur),this.$add("wheelThumbInput","keydown",this.onWheelKeyDown),this.$add("diskThumbInput","focus",this.onDiskFocus),this.$add("diskThumbInput","blur",this.onDiskBlur),this.$add("diskThumbInput","keydown",this.onDiskKeyDown),this.renderDisk(),this.updateColor()}disconnectedCallback(){this.dialC&&(this.dialC.detach(),this.dialC=void 0),this.diskC&&(this.diskC.detach(),this.diskC=void 0),this.$remove("wheelThumbInput","focus",this.onWheelFocus),this.$remove("wheelThumbInput","blur",this.onWheelBlur),this.$remove("wheelThumbInput","keydown",this.onWheelKeyDown),this.$remove("diskThumbInput","focus",this.onDiskFocus),this.$remove("diskThumbInput","blur",this.onDiskBlur),this.$remove("diskThumbInput","keydown",this.onDiskKeyDown),this.$remove("diskTarget","p-input",this.handleDiskInput),this.$remove("wheel","p-input",this.handleDialInput),super.disconnectedCallback()}updateColor(){const[t,e,s]=this._hsla;this.diskC&&(this.diskC.angle=t,this.diskC.distance=e/100),this.dialC&&(this.dialC.angle=1.8*s),this.deferredRender()}deferredRender(){this._renderPending||(this._renderPending=!0,requestAnimationFrame((()=>{this._renderPending=!1,this.updateDiskThumb(),this.updateWheelThumb(),this.renderWheel()})))}updateDiskThumb(){const t=this.$("diskThumb");if(t&&this.diskC){const e=this.diskC.angle,s=90,i=this.diskC.distance*s,h=i*Math.cos(n(e))+s,o=i*Math.sin(n(e))+s;t.style.transform=`translate3d(${h}px, ${o}px, 0)`,t.style.setProperty("--ecp-i-thumb-color",c(this._hsla));const[r,a]=this._hsla;t.style.setProperty("--ecp-i-thumb-shadow-color",c([r,a,40,1]))}}updateWheelThumb(){const t=this.$("wheelThumb");if(t){const[e,s,i]=this._hsla,h=1.8*i,o=this.$("wheel"),{width:r,height:a}=o,d=Math.min(r,a)/2-10-2.5,l=r/2+d*Math.cos(n(h)),u=a/2+d*Math.sin(n(h));t.style.transform=`translate3d(${l}px, ${u}px, 0)`,t.style.setProperty("--ecp-i-thumb-color",c(this._hsla)),t.style.setProperty("--ecp-i-thumb-shadow-color",c([e,s,50,1]))}}renderWheel(){const[t,e]=this._hsla,[s,i]=this.prevWheelParams;if(t===s&&e===i)return;const n=this.$("wheel"),h=n.getContext("2d"),{width:o,height:r}=n;h.clearRect(0,0,o,r);const a=Math.PI/40,d=Math.min(o,r)/2-2,l=d-20,u=o/2,p=r/2;for(let s=1;s<=40;s++){const i=s*a,n=(s-1)*a,o=l+(d-l)/2,r=Math.cos(i)*o+u,m=Math.sin(i)*o+p,b=Math.cos(n)*o+u,g=Math.sin(n)*o+p,f=[t,e,2.5*s,1],v=[t,e,2.5*(s-1),1],w=h.createLinearGradient(r,m,b,g);w.addColorStop(0,c(f)),w.addColorStop(1,c(v));const k=.002;h.beginPath(),h.arc(u,p,d,n-k,i+k,!1),h.arc(u,p,l,i+k,n-k,!0),h.fillStyle=w,h.fill()}this.prevWheelParams=[t,e]}renderDisk(){const t=180,e=this.$("disk").getContext("2d");e.clearRect(0,0,180,180);const s=document.createElement("canvas");s.width=s.height=t;const i=s.getContext("2d");let h=0;const o=[255,0,0];let r=0;const a=4.322;for(;h++<360;){const d=(r+3-1)%3;o[r]<255?o[r]=o[r]+a>255?255:o[r]+a:o[d]>0?o[d]=o[d]>a?o[d]-a:0:o[r]>=255&&(o[r]=255,r=(r+1)%3),i.clearRect(0,0,t,t);const l=i.createRadialGradient(90,90,0,90,90,90);l.addColorStop(0,"white"),l.addColorStop(1,"rgb("+o.map((function(t){return Math.floor(t)})).join(",")+")"),i.fillStyle=l,i.globalCompositeOperation="source-over",i.beginPath(),i.arc(90,90,90,0,2*Math.PI),i.closePath(),i.fill(),i.globalCompositeOperation="destination-out",i.beginPath(),i.arc(90,90,0,n(h+1),n(h+1)),i.arc(90,90,91,n(h+1),n(h+1)),i.arc(90,90,91,n(h+1),n(h-1)),i.arc(90,90,0,n(h+1),n(h-1)),i.closePath(),i.fill(),e.drawImage(s,0,0)}}_fire(){this.fire("change")}get hsl(){return[...this._hsla]}get rgb(){const[t,e,s]=u(this._hsla[0],this._hsla[1],this._hsla[2]);return[t,e,s,this._hsla[3]]}get value(){return m(...this.rgb)}set value(t){const e=function(t){let e=(t=t.trim()).match(r);if(e){const t=+e[1],s=+e[2],i=+e[3],n=+e[4],[h,o,r]=u(t,s,i);return{hex:m(h,o,r,n),hsla:[t,s,i,n],rgba:[h,o,r,n]}}if(e=t.match(a),e){const t=+e[1],s=+e[2],i=+e[3],[n,h,o]=u(t,s,i);return{hex:m(n,h,o,1),hsla:[t,s,i,1],rgba:[n,h,o,1]}}if(e=t.match(d),e){const t=+e[1],s=+e[2],i=+e[3],n=+e[4],[h,o,r]=p(t,s,i);return{hex:m(t,s,i,n),hsla:[h,o,r,n],rgba:[t,s,i,n]}}if(e=t.match(l),e){const t=+e[1],s=+e[2],i=+e[3],[n,h,o]=p(t,s,i);return{hex:m(t,s,i,1),hsla:[n,h,o,1],rgba:[t,s,i,1]}}let s=t;const i=s.lastIndexOf("#");i>=0&&(s=s.substring(i+1));const n=function(t){switch(t.length){case 3:return[+`0x${t[0]}${t[0]}`,+`0x${t[1]}${t[1]}`,+`0x${t[2]}${t[2]}`,1];case 4:return[+`0x${t[0]}${t[0]}`,+`0x${t[1]}${t[1]}`,+`0x${t[2]}${t[2]}`,+`0x${t[3]}${t[3]}`/255];case 6:return[+`0x${t[0]}${t[1]}`,+`0x${t[2]}${t[3]}`,+`0x${t[4]}${t[5]}`,1];case 8:return[+`0x${t[0]}${t[1]}`,+`0x${t[2]}${t[3]}`,+`0x${t[4]}${t[5]}`,+`0x${t[6]}${t[7]}`/255]}return null}(s);if(n){const[t,e,s,i]=n;if(isNaN(t)||isNaN(e)||isNaN(s)||isNaN(i))return null;const[h,o,r]=p(t,e,s);return{hex:m(t,e,s,i),hsla:[h,o,r,i],rgba:[t,e,s,i]}}return null}(t);e&&(this._hsla=[...e.hsla],this.updateColor())}}customElements.define("disk-color-picker",g);export{g as DiskColorPicker};