react-prize-wheel
Version:
A simple, performant React prize wheel component
3 lines (2 loc) • 9.4 kB
JavaScript
import{jsxs as e,jsx as t}from"react/jsx-runtime";import n,{useMemo as r,useState as o,useRef as i,useCallback as s,useEffect as a}from"react";function l(e){if(0===e.length)throw new Error("Cannot select from empty segments array");const t=e.filter(e=>!e.disabled);if(0===t.length)throw new Error("No enabled segments available for selection");const n=t.reduce((e,t)=>e+(t.weight??1),0);if(n<=0){const n=t[Math.floor(Math.random()*t.length)];return{segment:n,index:e.indexOf(n)}}const r=Math.random()*n;let o=0;for(const n of t)if(o+=n.weight??1,r<=o)return{segment:n,index:e.indexOf(n)};const i=t[t.length-1];return{segment:i,index:e.indexOf(i)}}function d(e,t,n=5,r="top"){let o;if("string"==typeof t){if(o=e.findIndex(e=>e.id===t),-1===o)throw new Error(`Segment with ID "${t}" not found`)}else if(o=t,o<0||o>=e.length)throw new Error(`Target index ${o} is out of bounds`);if(e[o]?.disabled)throw new Error("Target segment is disabled and cannot be targeted");const i=e.map(e=>e.weight??1),s=i.reduce((e,t)=>e+t,0);let a=0;for(let e=0;e<o;e++){a+=i[e]/s*360}const l=i[o]/s*360;return 360*n+(a+l/2+c(r))%360+(Math.random()-.5)*l*.8}function c(e){switch(e){case"top":default:return 0;case"right":return 90;case"bottom":return 180;case"left":return 270}}function u(e,t,n="top"){let r=(t%360+360)%360;r=(r-c(n)+360)%360,r=(270-r+360)%360;const o=e.map(e=>e.weight??1),i=o.reduce((e,t)=>e+t,0);let s=0,a=null,l=-1;for(let t=0;t<e.length;t++){const n=s+o[t]/i*360;if(r>=s&&r<n){a=e[t],l=t;break}s=n}return null===a&&(a=e[e.length-1],l=e.length-1),{segment:a,index:l}}function g(e){if("string"!=typeof e)return!1;return/^(#([0-9A-Fa-f]{3}){1,2}|rgb\((\d{1,3},\s*){2}\d{1,3}\)|rgba\((\d{1,3},\s*){3}(0(\.\d+)?|1(\.0+)?)\)|hsl\(\d{1,3},\s*\d{1,3}%,\s*\d{1,3}%\)|hsla\(\d{1,3},\s*\d{1,3}%,\s*\d{1,3}%,\s*(0(\.\d+)?|1(\.0+)?)\)|[a-zA-Z]+)$/.test(e.trim())}function p({segments:e,animation:t={},onSpinStart:n,onSpinComplete:c,disabled:p=!1,predefinedResult:h,pointerPosition:m="top"}){const f=r(()=>({duration:3e3,easing:"ease-out",spins:5,...t}),[t]),[b,x]=o({rotation:0,isSpinning:!1,lastResult:null,animationStartTime:0,progress:0}),w=i(),S=i(0),y=i(0),v=i(0),I=function(e){const t=[];if(!e||!Array.isArray(e))return t.push("Segments must be a valid array"),{isValid:!1,errors:t};e.length<2&&t.push("Wheel must have at least 2 segments"),e.length>100&&t.push("Wheel cannot have more than 100 segments"),e.forEach((e,n)=>{var r;e.id||t.push(`Segment ${n+1}: Missing required 'id' property`),e.text?e.text="string"!=typeof(r=e.text)||null==r?"":r.replace(/[<>'"]/g,"").replace(/&/g," ").replace(/\s+/g," ").trim().substring(0,100):t.push(`Segment ${n+1}: Missing required 'text' property`),e.color?g(e.color)||t.push(`Segment ${n+1}: Invalid color format`):t.push(`Segment ${n+1}: Missing required 'color' property`),e.textColor&&!g(e.textColor)&&t.push(`Segment ${n+1}: Invalid text color format`),e.borderColor&&!g(e.borderColor)&&t.push(`Segment ${n+1}: Invalid border color format`),void 0!==e.weight&&e.weight<=0&&t.push(`Segment ${n+1}: Weight must be positive`)});const n=e.map(e=>e.id),r=n.filter((e,t)=>n.indexOf(e)!==t);return r.length>0&&t.push(`Duplicate segment IDs: ${r.join(", ")}`),{isValid:0===t.length,errors:t}}(e),$=s(()=>{if(void 0!==h)if("string"==typeof h){const t=e.findIndex(e=>e.id===h);if(-1!==t){if(!e[t]?.disabled)return{targetIndex:t,targetSegment:e[t]};console.warn(`Predefined result segment "${h}" is disabled, using random selection`)}else console.warn(`Predefined result segment "${h}" not found, using random selection`)}else if("number"==typeof h&&h>=0&&h<e.length){if(!e[h]?.disabled)return{targetIndex:h,targetSegment:e[h]};console.warn(`Predefined result segment at index ${h} is disabled, using random selection`)}const t=l(e);return{targetIndex:t.index,targetSegment:t.segment}},[e,h]),T=s((e,t)=>{switch(t){case"linear":default:return e;case"ease-in":return e*e;case"ease-out":return 1-(1-e)**2;case"ease-in-out":return e<.5?2*e*e:1-(-2*e+2)**2/2}},[]),k=s(t=>{S.current||(S.current=t);const n=t-S.current,r=Math.min(n/f.duration,1),o=T(r,f.easing),i=v.current-y.current,s=y.current+i*o;if(x(e=>({...e,rotation:s,progress:r})),r<1)w.current=requestAnimationFrame(k);else{const t=s%360,r=u(e,t,m);if(r.segment.disabled){const t=$(),n=d(e,t.targetIndex,2,m);return S.current=0,y.current=s,v.current=s+n,void(w.current=requestAnimationFrame(k))}const o={segment:r.segment,index:r.index,angle:t,duration:n,timestamp:Date.now()};x(e=>({...e,isSpinning:!1,lastResult:o,rotation:s})),c?.(o)}},[f,e,c,T,m,$]),C=s(()=>{if(p||b.isSpinning||!I.isValid)return;const{targetIndex:t}=$(),r=d(e,t,f.spins,m);S.current=0,y.current=b.rotation,v.current=b.rotation+r,x(e=>({...e,isSpinning:!0,animationStartTime:Date.now(),progress:0})),n?.(),w.current=requestAnimationFrame(k)},[p,b.isSpinning,b.rotation,I.isValid,$,e,f,m,n,k]),M=s(()=>{w.current&&(cancelAnimationFrame(w.current),w.current=void 0)},[]),P=s(()=>{M(),x({rotation:0,isSpinning:!1,lastResult:null,animationStartTime:0,progress:0})},[M]);return a(()=>()=>{M()},[M]),{rotation:b.rotation,isSpinning:b.isSpinning,lastResult:b.lastResult,progress:b.progress,isValid:I.isValid,validationErrors:I.errors,spin:C,reset:P,animationConfig:f}}function h({segments:o,size:s=400,onSpinComplete:l,onSpinStart:d,theme:c={},animation:u={},pointer:g={},disabled:h=!1,className:m="",style:f={},showSpinButton:b=!0,spinButtonText:x="SPIN",predefinedResult:w}){const S=i(null),y=i(null),v={background:"#ffffff",border:"#dee2e6",text:"#212529",...c},I=n.useMemo(()=>({style:"arrow",color:"#ff0000",size:20,position:"top",...g}),[g]),{rotation:$,isSpinning:T,lastResult:k,isValid:C,validationErrors:M,spin:P}=p({segments:o,animation:u,pointerPosition:I.position,...d&&{onSpinStart:d},...l&&{onSpinComplete:l},disabled:h,...void 0!==w&&{predefinedResult:w}}),R=r(()=>function(e){const t=e.map(e=>e.weight??1),n=t.reduce((e,t)=>e+t,0);return t.map(e=>e/n*360)}(o),[o]),A=n.useCallback((e,t,n,r)=>{const{style:o,color:i,size:s,position:a}=I;switch(e.save(),e.translate(t,n),a){case"right":e.rotate(Math.PI/2);break;case"bottom":e.rotate(Math.PI);break;case"left":e.rotate(-Math.PI/2)}const l=-(r+10);switch(e.beginPath(),e.fillStyle=i,o){case"arrow":e.moveTo(0,l+s),e.lineTo(-s/4,l+2*s/3),e.lineTo(-s/8,l+2*s/3),e.lineTo(-s/8,l),e.lineTo(s/8,l),e.lineTo(s/8,l+2*s/3),e.lineTo(s/4,l+2*s/3),e.lineTo(0,l+s);break;case"triangle":e.moveTo(0,l+s),e.lineTo(-s/2,l),e.lineTo(s/2,l),e.lineTo(0,l+s);break;case"circle":e.arc(0,l+s/2,s/2,0,2*Math.PI)}e.closePath(),e.fill(),e.restore()},[I]),N=n.useCallback((e,t,n,r,o,i)=>{const s=t.split(" ");let a="";const l=[];for(let t=0;t<s.length;t++){const n=`${a+s[t]} `;e.measureText(n).width>o&&t>0?(l.push(a),a=`${s[t]} `):a=n}l.push(a);const d=r-(l.length-1)*i/2;l.forEach((t,r)=>{e.fillText(t.trim(),n,d+r*i)})},[]),E=n.useCallback(()=>{const e=S.current;if(!e||!C)return;const t=e.getContext("2d");if(!t)return;const n=s/2,r=s/2,i=(s-40)/2;t.clearRect(0,0,s,s),t.save(),t.translate(n,r),t.rotate($*Math.PI/180);let a=0;o.forEach((e,n)=>{const r=R[n]*Math.PI/180;t.beginPath(),t.moveTo(0,0),t.arc(0,0,i,a,a+r),t.closePath(),t.fillStyle=e.color,t.fill(),e.borderColor&&e.borderWidth&&(t.strokeStyle=e.borderColor,t.lineWidth=e.borderWidth,t.stroke());const s=a+r/2,l=.7*i,d=Math.cos(s)*l,c=Math.sin(s)*l;t.save(),t.translate(d,c),t.rotate(s+Math.PI/2);const u=Math.min(16,i/o.length);t.font=`bold ${u}px Arial, sans-serif`,t.fillStyle=e.textColor||function(e){const t=e.replace("#","");return(.299*Number.parseInt(t.substr(0,2),16)+.587*Number.parseInt(t.substr(2,2),16)+.114*Number.parseInt(t.substr(4,2),16))/255>.5?"#000000":"#ffffff"}(e.color),t.textAlign="center",t.textBaseline="middle";const g=.4*i;N(t,e.text,0,0,g,1.2*u),t.restore(),a+=r}),t.restore(),A(t,n,r,i)},[s,C,$,o,R,A,N]);return a(()=>{E()},[E]),a(()=>{const e=S.current;e&&(e.width=s,e.height=s,e.style.width=`${s}px`,e.style.height=`${s}px`,E())},[s,E]),e("div",C?{ref:y,className:`spin-wheel-container ${m}`,style:{display:"inline-block",textAlign:"center",userSelect:"none",...f},children:[e("button",{type:"button",className:"spin-wheel",style:{position:"relative",display:"inline-block",background:v.background,borderRadius:"50%",border:`2px solid ${v.border}`,cursor:h?"not-allowed":"pointer",opacity:h?.6:1,padding:0},onClick:h||T?void 0:P,disabled:h||T,"aria-label":"Spin the wheel",children:[t("canvas",{ref:S,style:{display:"block",borderRadius:"50%"}}),b&&t("button",{type:"button",className:"spin-wheel-button",onClick:P,disabled:h||T,style:{position:"absolute",top:"50%",left:"50%",transform:"translate(-50%, -50%)",padding:"8px 16px",backgroundColor:"#007bff",color:"white",border:"none",borderRadius:"20px",cursor:h||T?"not-allowed":"pointer",fontSize:"14px",fontWeight:"bold",opacity:h||T?.6:1,zIndex:10},children:T?"Spinning...":x})]}),k&&!T&&e("div",{className:"spin-wheel-result",style:{marginTop:"16px",padding:"12px",backgroundColor:v.background,border:`1px solid ${v.border}`,borderRadius:"8px",color:v.text},children:[t("strong",{children:"Result: "}),k.segment.text]})]}:{className:`spin-wheel-error ${m}`,style:f,children:[t("p",{children:"Invalid wheel configuration:"}),t("ul",{children:M.map(e=>t("li",{children:e},e))})]})}const m="1.0.0";export{h as SpinWheel,m as VERSION,d as calculateTargetAngle,u as getSegmentAtAngle,l as getWeightedSegment,p as useSpinWheel};
//# sourceMappingURL=index.esm.js.map