├── .babelrc ├── .env ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .prettierrc.js ├── LICENSE ├── README.md ├── demo └── roulette-demo.gif ├── dist ├── assets │ ├── roulette-pointer.png │ └── roulette-selector.png ├── bundle.js ├── components │ ├── Wheel │ │ ├── index.d.ts │ │ ├── index.js │ │ ├── styles.d.ts │ │ ├── styles.js │ │ ├── types.d.ts │ │ └── types.js │ ├── WheelCanvas │ │ ├── index.d.ts │ │ ├── index.js │ │ ├── styles.d.ts │ │ └── styles.js │ └── common │ │ ├── images.d.ts │ │ ├── images.js │ │ ├── styledComponents.d.ts │ │ └── styledComponents.js ├── index.d.ts ├── index.js ├── index.test.d.ts ├── index.test.js ├── serviceWorker.d.ts ├── serviceWorker.js ├── setupTests.d.ts ├── setupTests.js ├── src │ ├── components │ │ ├── Wheel │ │ │ ├── index.d.ts │ │ │ └── types.d.ts │ │ └── WheelCanvas │ │ │ └── index.d.ts │ ├── index.d.ts │ ├── index.test.d.ts │ ├── serviceWorker.d.ts │ ├── setupTests.d.ts │ └── utils.d.ts ├── strings.d.ts ├── strings.js ├── styles.d.ts ├── styles.js ├── utils.d.ts └── utils.js ├── example ├── .gitignore ├── README.md ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.css │ ├── App.test.tsx │ ├── App.tsx │ ├── index.css │ ├── index.tsx │ ├── logo.svg │ ├── react-app-env.d.ts │ ├── serviceWorker.ts │ └── setupTests.ts ├── tsconfig.json └── yarn.lock ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── assets │ └── roulette-pointer.png ├── components │ ├── Wheel │ │ ├── index.tsx │ │ ├── styles.js │ │ └── types.ts │ ├── WheelCanvas │ │ ├── index.tsx │ │ └── styles.js │ └── common │ │ ├── images.js │ │ └── styledComponents.js ├── index.css ├── index.test.tsx ├── index.tsx ├── react-app-env.d.ts ├── serviceWorker.ts ├── setupTests.ts ├── strings.js └── utils.ts ├── tsconfig.json ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"], 3 | "plugins": ["transform-object-rest-spread", "transform-react-jsx"] 4 | } 5 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // .eslintrc.js 2 | module.exports = { 3 | plugins: ['react'], 4 | extends: ['airbnb-typescript-prettier', 'plugin:react/recommended'], 5 | rules: { 6 | 'no-plusplus': 'off', 7 | '@typescript-eslint/no-explicit-any': 'off', 8 | 'import/prefer-default-export': 'off', 9 | 'import/no-extraneous-dependencies': 'off', 10 | 'import/no-unresolved': 'off', 11 | '@typescript-eslint/no-use-before-define': 'off', 12 | '@typescript-eslint/no-unused-vars': 'off', 13 | 'react-hooks/exhaustive-deps': 'off', 14 | 'no-use-before-define': 'off', 15 | 'react/require-default-props': 'off', 16 | }, 17 | env: { 18 | jest: true, 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | demo 2 | example 3 | src 4 | public 5 | coverage -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | // .prettierrc.js 2 | module.exports = { 3 | printWidth: 80, 4 | singleQuote: true, 5 | trailingComma: 'es5', 6 | tabWidth: 2, 7 | arrowParens: 'avoid', 8 | }; 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Effectus Software 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

React Custom Roulette

2 | 3 |
4 | 5 | [![npm version](https://img.shields.io/npm/v/react-custom-roulette)](https://www.npmjs.com/package/react-custom-roulette) 6 | [![Types](https://img.shields.io/npm/types/react-custom-roulette)](https://www.typescriptlang.org/index.html) 7 | [![npm downloads](https://img.shields.io/npm/dm/react-custom-roulette)](https://www.npmjs.com/package/react-custom-roulette) 8 | 9 |
10 | 11 |

Customizable React roulette wheel with spinning animation

12 | 13 |
14 | 15 | ![React Custom Roulette](https://github.com/effectussoftware/react-custom-roulette/raw/master/demo/roulette-demo.gif) 16 | 17 |
18 | 19 | ## Features 20 | 21 | - Customizable design 22 | - Prize selection with props 23 | - Spinning animation (customizable spin duration) 24 | - **[NEW!]** Images as items (see [Types](#types)) 25 | - **[NEW!]** Customizable pointer image 26 | - Multiple consecutive spins (see [Multi Spin](#multi-spin)) 27 | - Compatible with TypeScript 28 | 29 | ## Install 30 | 31 | $ npm install react-custom-roulette 32 | 33 | or 34 | 35 | $ yarn add react-custom-roulette 36 | 37 | ## Quickstart 38 | 39 | #### Wheel Component 40 | 41 | ```jsx 42 | import React from 'react' 43 | import { Wheel } from 'react-custom-roulette' 44 | 45 | const data = [ 46 | { option: '0', style: { backgroundColor: 'green', textColor: 'black' } }, 47 | { option: '1', style: { backgroundColor: 'white' } }, 48 | { option: '2' }, 49 | ] 50 | 51 | export default () => ( 52 | <> 53 | 60 | 61 | ) 62 | ``` 63 | 64 | #### Props 65 | 66 | | **Prop** | **Type** | **Default** | **Description** | 67 | |---------------------------------|--------------------|---------------------------|--------------------------------------------------------------------| 68 | | mustStartSpinning _(required)_ | `boolean` | - | Sets when the roulette must start the spinning animation | 69 | | prizeNumber _(required)_ | `number` | - | Sets the winning option. It's value must be between 0 and data.lenght-1 | 70 | | data _(required)_ | `Array` | - | Array of options. Can contain styling information for a specific option (see [WheelData](#wheeldata)) | 71 | | onStopSpinning | `function` | () => null | Callback function that is called when the roulette ends the spinning animation | 72 | | backgroundColors | `Array` | ['darkgrey', 'lightgrey'] | Array of colors that will fill the background of the roulette options, starting from option 0 | 73 | | textColors | `Array` | ['black'] | Array of colors that will fill the text of the roulette options, starting from option 0 | 74 | | outerBorderColor | `string` | 'black' | Color of the roulette's outer border line | 75 | | outerBorderWidth | `number` | 5 | Width of the roulette's outer border line (0 represents no outer border line) | 76 | | innerRadius | `number [0..100]` | 0 | Distance of the inner radius from the center of the roulette | 77 | | innerBorderColor | `string` | 'black' | Color of the roulette's inner border line | 78 | | innerBorderWidth | `number` | 0 | Width of the roulette's inner border line (0 represents no inner border line) | 79 | | radiusLineColor | `string` | 'black' | Color of the radial lines that separate each option | 80 | | radiusLineWidth | `number` | 5 | Width of the radial lines that separate each option (0 represents no radial lines) | 81 | | fontFamily | `string` | 'Helvetica, Arial' | Global font family of the option string. Non-Web safe fonts are fetched from https://fonts.google.com/. All available fonts can be found there. | 82 | | fontSize | `number` | 20 | Global font size of the option string | 83 | | fontWeight | `number | string` | 'bold' | Global font weight of the option string | 84 | | fontStyle | `string` | 'normal' | Global font style of the option string | 85 | | perpendicularText | `boolean` | false | When 'true', sets the option texts perpendicular to the roulette's radial lines | 86 | | textDistance | `number [0..100]` | 60 | Distance of the option texts from the center of the roulette | 87 | | spinDuration | `number [0.01 ..]` | 1.0 | Coefficient to adjust the default spin duration | 88 | | startingOptionIndex | `number` | - | Set which option (through its index in the `data` array) will be initially selected by the roulette (before spinning). If not specified the roulette will render without choosing a starting option | 89 | | pointerProps | `PointerProps` | { src: roulettePointer } | Image source and CSS styling to apply to the pointer image. | 90 | | disableInitialAnimation | `boolean` | false | When 'true', disables the initial backwards wheel animation | 91 | 92 | ## Types 93 | 94 | #### WheelData 95 | 96 | ```jsx 97 | interface WheelData { 98 | option?: string; 99 | image?: ImageProps; 100 | style?: StyleType; // Optional 101 | optionSize?: number; // Optional 102 | } 103 | ``` 104 | 105 | | **Prop** | **Type** | **Default** | **Description** | 106 | |------------|--------------|-------------|-------------------------------------------------------------------------------------------------------| 107 | | option | `string` | '' | String to be rendered inside an option. | 108 | | image | `ImageProps` | - | Image to be rendered inside an option. It is configured through [ImageProps](#imageprops) | 109 | | style | `StyleType` | - | Styles for option. It is configured through [StyleType](#styletype) | 110 | | optionSize | `number` | 1 | Integer that sets the size of the option measured in roulette pieces. For example: if `data` provides 2 options A and B, and you set A's `optionSize` to `2`, B's `optionSize` to `1`, the roulette will render `3` pieces: 2 corresponding to A and 1 corresponding to B. Therefore, A will appear to be twice as big as B. | 111 | 112 | #### StyleType 113 | 114 | ```jsx 115 | interface StyleType { 116 | backgroundColor?: string; // Optional 117 | textColor?: string; // Optional 118 | fontFamily?: string; // Optional 119 | fontSize?: number; // Optional 120 | fontWeight?: number | string; // Optional 121 | fontStyle?: string; // Optional 122 | } 123 | ``` 124 | 125 | | **Prop** | **Type** | **Default** | **Description** | 126 | |-----------------|-------------------|---------------------------|--------------------------------------------------------------------| 127 | | backgroundColor | `string` | 'darkgrey' or 'lightgrey' | Background color for option | 128 | | textColor | `string` | 'black' | Text color | 129 | | fontFamily | `string` | 'Helvetica, Arial' | String containing text font and its fallbacks separated by commas | 130 | | fontSize | `number` | 20 | Font size number | 131 | | fontWeight | `number | string` | 'bold' | Font weight string or number | 132 | | fontStyle | `string` | 'normal' | Font style string | 133 | 134 | #### ImageProps 135 | 136 | ```jsx 137 | interface ImageProps { 138 | uri: string; 139 | offsetX?: number; // Optional 140 | offsetY?: number; // Optional 141 | sizeMultiplier?: number; // Optional 142 | landscape?: boolean; // Optional 143 | } 144 | ``` 145 | 146 | | **Prop** | **Type** | **Default** | **Description** | 147 | |----------------|-----------|-------------|---------------------------------------------------------------------------------------| 148 | | uri | `string` | - | Image source. It can be url or path. | 149 | | offsetX | `number` | 0 | Image offset in its X axis | 150 | | offsetY | `number` | 0 | Image offset in its Y axis | 151 | | sizeMultiplier | `number` | 1 | A value of 1 means image height is calculated as `200px * sizeMultiplier` and width will be calculated to keep aspect ratio. | 152 | | landscape | `boolean` | false | If true, image will be rotated 90 degrees so as to render in a landscape orientation. | 153 | 154 | #### PointerProps 155 | 156 | ```jsx 157 | interface PointerProps { 158 | src?: string; // Optional 159 | style?: React.CSSProperties; // Optional 160 | } 161 | ``` 162 | 163 | | **Prop** | **Type** | **Default** | **Description** | 164 | |----------|-----------------------|---------------------------|-----------------------------| 165 | | src | `string` | - | Image src. | 166 | | style | `React.CSSProperties` | - | Styling for pointer image. | 167 | 168 | ## Multi Spin 169 | 170 | #### Example (using useState) 171 | 172 | ```jsx 173 | import React, { useState } from 'react' 174 | import { Wheel } from 'react-custom-roulette' 175 | 176 | const data = [ 177 | { option: '0' }, 178 | { option: '1' }, 179 | { option: '2' }, 180 | ] 181 | 182 | export default () => { 183 | const [mustSpin, setMustSpin] = useState(false); 184 | const [prizeNumber, setPrizeNumber] = useState(0); 185 | 186 | const handleSpinClick = () => { 187 | if (!mustSpin) { 188 | const newPrizeNumber = Math.floor(Math.random() * data.length); 189 | setPrizeNumber(newPrizeNumber); 190 | setMustSpin(true); 191 | } 192 | } 193 | 194 | return ( 195 | <> 196 | { 202 | setMustSpin(false); 203 | }} 204 | /> 205 | 206 | 207 | ) 208 | } 209 | ``` 210 | 211 | ## Contributors 212 | 213 | This project exists thanks to all the people who contribute! 214 | 215 | 224 | 225 | ## License 226 | 227 | This project is licensed under the MIT license, Copyright (c) 2023 Effectus Software. [[License](LICENSE)] 228 | -------------------------------------------------------------------------------- /demo/roulette-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/effectussoftware/react-custom-roulette/0049461cb3bf89a754d9c40e167518b6996cf732/demo/roulette-demo.gif -------------------------------------------------------------------------------- /dist/assets/roulette-pointer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/effectussoftware/react-custom-roulette/0049461cb3bf89a754d9c40e167518b6996cf732/dist/assets/roulette-pointer.png -------------------------------------------------------------------------------- /dist/assets/roulette-selector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/effectussoftware/react-custom-roulette/0049461cb3bf89a754d9c40e167518b6996cf732/dist/assets/roulette-selector.png -------------------------------------------------------------------------------- /dist/bundle.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("react")):"function"==typeof define&&define.amd?define(["React"],t):"object"==typeof exports?exports.Wheel=t(require("react")):e.Wheel=t(e.React)}(window,(function(e){return function(e){var t={};function n(r){if(t[r])return t[r].exports;var i=t[r]={i:r,l:!1,exports:{}};return e[r].call(i.exports,i,i.exports,n),i.l=!0,i.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)n.d(r,i,function(t){return e[t]}.bind(null,i));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=11)}([function(t,n){t.exports=e},function(e,t,n){"use strict";(function(e){var r=n(2),i=n(0),o=n.n(i),a=n(6),s=n.n(a),c=n(7),l=n(8),u=n(4),f=n(3),h=n.n(f);function d(){return(d=Object.assign||function(e){for(var t=1;t1?t-1:0),r=1;r0?" Args: "+n.join(", "):""))}var x=function(){function e(e){this.groupSizes=new Uint32Array(512),this.length=512,this.tag=e}var t=e.prototype;return t.indexOfGroup=function(e){for(var t=0,n=0;n=this.groupSizes.length){for(var n=this.groupSizes,r=n.length,i=r;e>=i;)(i<<=1)<0&&A(16,""+e);this.groupSizes=new Uint32Array(i),this.groupSizes.set(n),this.length=i;for(var o=r;o=this.length||0===this.groupSizes[e])return t;for(var n=this.groupSizes[e],r=this.indexOfGroup(e),i=r+n,o=r;o=0;n--){var r=t[n];if(r&&1===r.nodeType&&r.hasAttribute(S))return r}}(n),o=void 0!==i?i.nextSibling:null;r.setAttribute(S,"active"),r.setAttribute("data-styled-version","5.2.1");var a=W();return a&&r.setAttribute("nonce",a),n.insertBefore(r,o),r},D=function(){function e(e){var t=this.element=z(e);t.appendChild(document.createTextNode("")),this.sheet=function(e){if(e.sheet)return e.sheet;for(var t=document.styleSheets,n=0,r=t.length;n=0){var n=document.createTextNode(t),r=this.nodes[e];return this.element.insertBefore(n,r||null),this.length++,!0}return!1},t.deleteRule=function(e){this.element.removeChild(this.nodes[e]),this.length--},t.getRule=function(e){return e0&&(l+=e+",")})),r+=""+s+c+'{content:"'+l+'"}/*!sc*/\n'}}}return r}(this)},e}(),K=/(a)(d)/gi,$=function(e){return String.fromCharCode(e+(e>25?39:97))};function U(e){var t,n="";for(t=Math.abs(e);t>52;t=t/52|0)n=$(t%52)+n;return($(t%52)+n).replace(K,"$1-$2")}var q=function(e,t){for(var n=t.length;n;)e=33*e^t.charCodeAt(--n);return e},X=function(e){return q(5381,e)};function J(e){for(var t=0;t>>0);if(!t.hasNameForId(r,a)){var s=n(o,"."+a,void 0,r);t.insertRules(r,a,s)}i.push(a),this.staticRulesId=a}else{for(var c=this.rules.length,l=q(this.baseHash,n.hash),u="",f=0;f>>0);if(!t.hasNameForId(r,g)){var m=n(u,"."+g,void 0,r);t.insertRules(r,g,m)}i.push(g)}}return i.join(" ")},e}(),V=/^\s*\/\/.*$/gm,Z=[":","[",".","#"];function ee(e){var t,n,r,i,o=void 0===e?v:e,a=o.options,s=void 0===a?v:a,l=o.plugins,u=void 0===l?m:l,f=new c.a(s),h=[],d=function(e){function t(t){if(t)try{e(t+"}")}catch(e){}}return function(n,r,i,o,a,s,c,l,u,f){switch(n){case 1:if(0===u&&64===r.charCodeAt(0))return e(r+";"),"";break;case 2:if(0===l)return r+"/*|*/";break;case 3:switch(l){case 102:case 112:return e(i[0]+r),"";default:return r+(0===f?"/*|*/":"")}case-2:r.split("/*|*/}").forEach(t)}}}((function(e){h.push(e)})),p=function(e,r,o){return 0===r&&Z.includes(o[n.length])||o.match(i)?e:"."+t};function g(e,o,a,s){void 0===s&&(s="&");var c=e.replace(V,""),l=o&&a?a+" "+o+" { "+c+" }":c;return t=s,n=o,r=new RegExp("\\"+n+"\\b","g"),i=new RegExp("(\\"+n+"\\b){2,}"),f(a||!o?"":o,l)}return f.use([].concat(u,[function(e,t,i){2===e&&i.length&&i[0].lastIndexOf(n)>0&&(i[0]=i[0].replace(r,p))},d,function(e){if(-2===e){var t=h;return h=[],t}}])),g.hash=u.length?u.reduce((function(e,t){return t.name||A(15),q(e,t.name)}),5381).toString():"",g}var te=o.a.createContext(),ne=(te.Consumer,o.a.createContext()),re=(ne.Consumer,new _),ie=ee();function oe(){return Object(i.useContext)(te)||re}function ae(){return Object(i.useContext)(ne)||ie}function se(e){var t=Object(i.useState)(e.stylisPlugins),n=t[0],r=t[1],a=oe(),c=Object(i.useMemo)((function(){var t=a;return e.sheet?t=e.sheet:e.target&&(t=t.reconstructWithOptions({target:e.target},!1)),e.disableCSSOMInjection&&(t=t.reconstructWithOptions({useCSSOMInjection:!1})),t}),[e.disableCSSOMInjection,e.sheet,e.target]),l=Object(i.useMemo)((function(){return ee({options:{prefix:!e.disableVendorPrefixes},plugins:n})}),[e.disableVendorPrefixes,n]);return Object(i.useEffect)((function(){s()(n,e.stylisPlugins)||r(e.stylisPlugins)}),[e.stylisPlugins]),o.a.createElement(te.Provider,{value:c},o.a.createElement(ne.Provider,{value:l},e.children))}var ce=function(){function e(e,t){var n=this;this.inject=function(e,t){void 0===t&&(t=ie);var r=n.name+t.hash;e.hasNameForId(n.id,r)||e.insertRules(n.id,r,t(n.rules,r,"@keyframes"))},this.toString=function(){return A(12,String(n.name))},this.name=e,this.id="sc-keyframes-"+e,this.rules=t}return e.prototype.getName=function(e){return void 0===e&&(e=ie),this.name+e.hash},e}(),le=/([A-Z])/,ue=/([A-Z])/g,fe=/^ms-/,he=function(e){return"-"+e.toLowerCase()};function de(e){return le.test(e)?e.replace(ue,he).replace(fe,"-ms-"):e}var pe=function(e){return null==e||!1===e||""===e};function ge(e,t,n,r){if(Array.isArray(e)){for(var i,o=[],a=0,s=e.length;a1?t-1:0),r=1;r?@[\\\]^`{|}~-]+/g,be=/(^-|-$)/g;function we(e){return e.replace(ye,"-").replace(be,"")}var Se=function(e){return U(X(e)>>>0)};function ke(e){return"string"==typeof e&&!0}var Ce=function(e){return"function"==typeof e||"object"==typeof e&&null!==e&&!Array.isArray(e)},Ae=function(e){return"__proto__"!==e&&"constructor"!==e&&"prototype"!==e};function xe(e,t,n){var r=e[n];Ce(t)&&Ce(r)?Oe(r,t):e[n]=t}function Oe(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),r=1;r=0||(i[n]=e[n]);return i}(t,["componentId"]),o=r&&r+"-"+(ke(e)?e:we(b(e)));return Pe(e,d({},i,{attrs:k,componentId:o}),n)},Object.defineProperty(A,"defaultProps",{get:function(){return this._foldedDefaultProps},set:function(t){this._foldedDefaultProps=r?Oe({},e.defaultProps,t):t}}),A.toString=function(){return"."+A.styledComponentId},a&&h()(A,e,{attrs:!0,componentStyle:!0,displayName:!0,foldedComponentIds:!0,shouldForwardProp:!0,styledComponentId:!0,target:!0,withComponent:!0}),A}var Ee=function(e){return function e(t,n,i){if(void 0===i&&(i=v),!Object(r.isValidElementType)(n))return A(1,String(n));var o=function(){return t(n,i,me.apply(void 0,arguments))};return o.withConfig=function(r){return e(t,n,d({},i,{},r))},o.attrs=function(r){return e(t,n,d({},i,{attrs:Array.prototype.concat(i.attrs,r).filter(Boolean)}))},o}(Pe,e)};["a","abbr","address","area","article","aside","audio","b","base","bdi","bdo","big","blockquote","body","br","button","canvas","caption","cite","code","col","colgroup","data","datalist","dd","del","details","dfn","dialog","div","dl","dt","em","embed","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","iframe","img","input","ins","kbd","keygen","label","legend","li","link","main","map","mark","marquee","menu","menuitem","meta","meter","nav","noscript","object","ol","optgroup","option","output","p","param","picture","pre","progress","q","rp","rt","ruby","s","samp","script","section","select","small","source","span","strong","style","sub","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","title","tr","track","u","ul","var","video","wbr","circle","clipPath","defs","ellipse","foreignObject","g","image","line","linearGradient","marker","mask","path","pattern","polygon","polyline","radialGradient","rect","stop","svg","text","tspan"].forEach((function(e){Ee[e]=Ee(e)}));!function(){function e(e,t){this.rules=e,this.componentId=t,this.isStatic=J(e),_.registerId(this.componentId+1)}var t=e.prototype;t.createStyles=function(e,t,n,r){var i=r(ge(this.rules,t,n,r).join(""),""),o=this.componentId+e;n.insertRules(o,o,i)},t.removeStyles=function(e,t){t.clearRules(this.componentId+e)},t.renderStyles=function(e,t,n,r){e>2&&_.registerId(this.componentId+e),this.removeStyles(e,n),this.createStyles(e,t,n,r)}}();!function(){function e(){var e=this;this._emitSheetCSS=function(){var t=e.instance.toString(),n=W();return""},this.getStyleTags=function(){return e.sealed?A(2):e._emitSheetCSS()},this.getStyleElement=function(){var t;if(e.sealed)return A(2);var n=((t={})[S]="",t["data-styled-version"]="5.2.1",t.dangerouslySetInnerHTML={__html:e.instance.toString()},t),r=W();return r&&(n.nonce=r),[o.a.createElement("style",d({},n,{key:"sc-0-0"}))]},this.seal=function(){e.sealed=!0},this.instance=new _({isServer:!0}),this.sealed=!1}var t=e.prototype;t.collectStyles=function(e){return this.sealed?A(2):o.a.createElement(se,{sheet:this.instance},e)},t.interleaveWithNodeStream=function(e){return A(3)}}();t.a=Ee}).call(this,n(9))},function(e,t,n){"use strict";e.exports=n(10)},function(e,t,n){"use strict";var r=n(2),i={childContextTypes:!0,contextType:!0,contextTypes:!0,defaultProps:!0,displayName:!0,getDefaultProps:!0,getDerivedStateFromError:!0,getDerivedStateFromProps:!0,mixins:!0,propTypes:!0,type:!0},o={name:!0,length:!0,prototype:!0,caller:!0,callee:!0,arguments:!0,arity:!0},a={$$typeof:!0,compare:!0,defaultProps:!0,displayName:!0,propTypes:!0,type:!0},s={};function c(e){return r.isMemo(e)?a:s[e.$$typeof]||i}s[r.ForwardRef]={$$typeof:!0,render:!0,defaultProps:!0,displayName:!0,propTypes:!0},s[r.Memo]=a;var l=Object.defineProperty,u=Object.getOwnPropertyNames,f=Object.getOwnPropertySymbols,h=Object.getOwnPropertyDescriptor,d=Object.getPrototypeOf,p=Object.prototype;e.exports=function e(t,n,r){if("string"!=typeof n){if(p){var i=d(n);i&&i!==p&&e(t,i,r)}var a=u(n);f&&(a=a.concat(f(n)));for(var s=c(t),g=c(n),m=0;m=t.f?i():e.fonts.load(function(e){return x(e)+" "+e.f+"00 300px "+C(e.c)}(t.a),t.h).then((function(e){1<=e.length?r():setTimeout(o,25)}),(function(){i()}))}()})),i=null,o=new Promise((function(e,n){i=setTimeout(n,t.f)}));Promise.race([o,r]).then((function(){i&&(clearTimeout(i),i=null),t.g(t.a)}),(function(){t.j(t.a)}))};var W={D:"serif",C:"sans-serif"},z=null;function D(){if(null===z){var e=/AppleWebKit\/([0-9]+)(?:\.([0-9]+))/.exec(window.navigator.userAgent);z=!!e&&(536>parseInt(e[1],10)||536===parseInt(e[1],10)&&11>=parseInt(e[2],10))}return z}function B(e,t,n){for(var r in W)if(W.hasOwnProperty(r)&&t===e.f[W[r]]&&n===e.f[W[r]])return!0;return!1}function F(e){var t,n=e.g.a.offsetWidth,r=e.h.a.offsetWidth;(t=n===e.f.serif&&r===e.f["sans-serif"])||(t=D()&&B(e,n,r)),t?s()-e.A>=e.w?D()&&B(e,n,r)&&(null===e.u||e.u.hasOwnProperty(e.a.c))?H(e,e.v):H(e,e.B):function(e){setTimeout(a((function(){F(this)}),e),50)}(e):H(e,e.v)}function H(e,t){setTimeout(a((function(){h(this.g.a),h(this.h.a),h(this.j.a),h(this.m.a),t(this.a)}),e),0)}function G(e,t,n){this.c=e,this.a=t,this.f=0,this.m=this.j=!1,this.s=n}N.prototype.start=function(){this.f.serif=this.j.a.offsetWidth,this.f["sans-serif"]=this.m.a.offsetWidth,this.A=s(),F(this)};var _=null;function K(e){0==--e.f&&e.j&&(e.m?((e=e.a).g&&d(e.f,[e.a.c("wf","active")],[e.a.c("wf","loading"),e.a.c("wf","inactive")]),P(e,"active")):j(e.a))}function $(e){this.j=e,this.a=new E,this.h=0,this.f=this.g=!0}function U(e,t,n,r,i){var o=0==--e.h;(e.f||e.g)&&setTimeout((function(){var e=i||null,s=r||{};if(0===n.length&&o)j(t.a);else{t.f+=n.length,o&&(t.j=o);var c,l=[];for(c=0;cr&&(r=(t=t.trim()).charCodeAt(0)),r){case 38:return t.replace(g,"$1"+e.trim());case 58:return e.trim()+t.replace(g,"$1"+e.trim());default:if(0<1*n&&0c.charCodeAt(8))break;case 115:a=a.replace(c,"-webkit-"+c)+";"+a;break;case 207:case 102:a=a.replace(c,"-webkit-"+(102s.charCodeAt(0)&&(s=s.trim()),s=[s],0d)&&(B=(G=G.replace(" ",":")).length),01)for(var n=1;nt/2?-360+a:a},f=function(e,t,n){return Math.min(Math.max(e,+n),t)},h=function(e){return!!e&&!l.includes(e.toLowerCase())},d=function(e){return e.slice(-1)[0].slice(-1)[0]+1},p="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";const g=new Image;g.src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARAAAAENCAMAAADwnMpiAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAACZUExURUdwTP+OROJKK+JKK/6PReJKK/+QQ+JKK+JKK+JKK+JKK/2MQ/+LRv2LQeNLK+JKK/BrNuRNLPyJQeNLK/d+PfFvN/iFQONLK/BtOPV3OvmCPfFxOPR4PeNMLO5oNexiNPmBPudVL+hXL+pcMepfMuZSLvV7PORPLPFyPedULuhYMOpfM+5nNetdMfupXexhM+2dRuNOLeJKK+Smm3cAAAAydFJOUwAY9Okb+hT+8f3uIRYm5fdz1yvdQWQx4mtRNV1L0HqNOsOwo5W6RcRWz6qGgLYFnA6eKwdCNwAACLhJREFUGBntwNeSg8C1BdANdHMaGLJyzprRaNL+/4+7df1iV7lsgyI0Wnh5eXl5eXl5eXl5+S/8t3jQG/ez5W779bX+f1+b7fI8ms4mseOjS5yo937erk0aCP+FCP9BJHU/NstpL3JgPSeeZafc1SIkhf+FiDYf29EheoOlfLXq/+RGC4WVSertF+PoDbZRg9HP0ATC+kRc7zQaOLCGH83OH6EWXk50eDxPFCzgR++7oRHh1XS4ziYO2k31PksjwpsQSb1NP/bRVm9FtjaaNyWmXPYU2kj1dqUrvLkg8DbjCG0TvW88zTsJzHoU+2gPP+qvTcA7CkyeFW9oibg/94R3JibPCh8tEE2PYcAHkDDvx2g6NduHAR8kMMd3hSZ7m2wTzQcKvFPPQWPFWenywdLhcuCjkdRsbQI+nsn7EZrHL5aJ5lME4WbloGHUbG74LJKWWYxGic9JyicKwn3PQWP4q00ofK60zGI0hHrPXT5dEG5WPpogWgw1m8DNpwrPV+y8gM2gk2WMJ/NXeyNsCgk3Ex/P5PTmLpvEzHsOnkeNS5fN4uZjhWdR0zJl06TlVOE51Gio2Tx62Fd4BtUfajaRTrIIj6f6Q81mCpJFhEdT/aFmU4n3GeGx1LTUbDBvGeGRnHGp2Wjep8Lj+L08ZcMlmcLDTOYumy4YTh08SLE3bD5dHnw8RLQzbAN3PsEjqMwTtoK7KXB//ngYsCXMMsLdTfKUbSHeSOHO4o3L9gjKg4+7UouQbZLOB7gnf5YIW8VsI9xRMU/ZMuHIwd2onWHbBGUPdzNL2D7pusCdxEfNFjKfCnfxlhm2kSRjH/ewKgO2UjovcAdqm7KlzFLh9sYeWyuZ4ebivWZr6WOMG/Onhi1msjfcVpEHbLGgXOGmnEXIVku3CrdUlMJWE+8dN+QsDFtOH2PcTlEK2870fdyKnxm2XpAXuJU4F7afOTu4kZGhBaQc4DaitdAG7qeDmxgbWkHKCW5BbQLaIf10cAM9j5aQcoDrvS01beEufFytKGkNyWNcLXNpDzPCtaK90B6yV7jSwdAm4QHXedtp2kTv3nCVoqRdyhhXGbm0izvCNdReaBfZO7hCL6Rtwgku539q2kYvcLkop32OChd7N7RPuMKlnJ3QPnqBS8WJ0D6ydnChvksbhQNcxtkKbaT7uEzh0Uqy9XGRUUo7JREu4ZyEdjIHXKLwaCkZ4RKjlJaSrY/6nI3QVomD+gqP1jIT1DdKaa1gjNqcjdBackZthUeLbVDbKKXFyjfU5JyEFnMVaio82kyvUFM/pc1khnqcrdBmkqGeOKHdlqhn6tJuX6jF3wnt9oFa1FBot9BHHTNDu4l+Qx1noeWCCDWoD6HlpEANg5C2kwFqGGlab4LqnJPQej1UF3m0Xw/Vvbu0Xw/VLYX266EylbMDeqhsYtgBPVSWBeyAHqpyNsIOWKGqyGMHyABVjV12gMSo6izsAB2hIvUh7IDUR0VFyC5IfFTU1+wA+UBF/k7YBV+oSCXshCUq6oXshAwVjYRdELyjGuck7IJghWpUwk4wMao5uOwEz0E1mbATvnxU4myEXSBLVBN57ASZopqDy05IV6gmE3ZCGKES5yTshPUbKlEeO0F2qKbnshOCKaoZCTshnKASfyvshKFCJSphJ8jORyUTw07QfVQzDdgJ4QDV/Ai7QD4UKnE+2AlyRjWxYSeYGaqZaXaBDCNUsxB2gfz4qMT/Yie4U1SjPHaBJDGq6bnsgmDroJqpsAvMFBX9CDtAhjGqcT7YBbJzUE1k2AXhOyqaaXaA5BEqyoQdkC58VOOf2AXJBBWphB2gtw4qGhh2gBmjqnFA+8kxQlVnof1Mhqr8De0neYyqHI/2c88+qhoYWk/KASo7BLReunRQWSa0nQwnqG5J67mfDqpb03ZSDlBDQtuZs4MaUlouyAvUoWm5cOqjhkhot3QfoY53Wi45oJY97WaWCrXMaTWdF6gnpdW8dx+1/GrazGwj1HOgzXQ+QE2ftJgkYx81zWkxc1aoq6S93H2Mur41rZXmK9RWBLRVMBz7qG1EW4mXKdS3p63CXYQL5LSUOcW4REo7ufsBLvEd0ErufIWLDGglNz/4uMiINkrLmYPL7GmhtJwpXCinfdxyrHChX5fWcfOZwqViTdu4856Di02FlnH3Kx+X29Ey4WmAa+S0SuAtY1zFo03SYRbhKr+aFnHzscJ1YqE1JNysfFzpndZIk3OMqy1pCzMfK1xvTjtob1f4uIGENpAwH0e4hW9NC6TDz8LHTTgBWy/wTisHN7Ji2wXm+K5wMyO2m5i8H+OGNmwzcctF4eOWSraXmPJcOLgtw7aSMM8KBzf2G7CddLjvRz5uLhK2kKTJz0HhHsZsHRGTLwYO7uOTLSNpeJpGPu5lzjYRMfl5pXBHQ7aHpN52Gvm4p++UbaHNcTFwcGd+wFYQU+4OEe5vIGw+Sb3tNPbxCFM2nehwPyocPMiOzabDdTZx8DhzNpgOj4uJwkN5bCodrhcrhQf71mwi0eE6myg83m/AxpHU22QDB09RsFlETLLtFw6eZcwGEW3yz3H0hidasinE9TbZROHJjmwC0ebjcxw5eD6PzyZikl1/oNAMLp9JxPW+sp7y0RTfwmcRSb39YhY5aJJf4TOIuN5XNoscNE3Mh5PAJKfsEDlooh4fSrT5+OmvlI+mGvFRRFzvazGOHTTajg8gos3HbrpSPhrvg/clos3Hz+gQOWgHw7sR0ebjp9+LHLQI70IkNeuffi9y0Da8MQnScL1dzCaOj1ZKeSuivfnfzEHLubye6GT+965ghSOvITqZ/72rb9jjFPASIql3/JspWGcasibRZr7pT3zYKTpqVhXoZL6bxr+w2mci/F9Eu+UxO6hvdED0Z4T/kWgv/5sOftEhh7+Q/050Wh4XM/WL7ln9DTX/SXSYn/oT/xud9dv/y8MgkCAdzj/H0S9evtVk1hv433h5eXl5ebna/wE/LWKN4f9AUgAAAABJRU5ErkJggg==";var m=n(1);const v=m.a.img` 10 | -webkit-user-drag: none; 11 | -khtml-user-drag: none; 12 | -moz-user-drag: none; 13 | -o-user-drag: none; 14 | user-drag: none; 15 | `,y=m.a.div` 16 | position: relative; 17 | width: 80vw; 18 | max-width: 445px; 19 | height: 80vw; 20 | max-height: 445px; 21 | object-fit: contain; 22 | flex-shrink: 0; 23 | z-index: 5; 24 | pointer-events: none; 25 | `,b=m.a.div` 26 | position: absolute; 27 | width: 100%; 28 | left: 0px; 29 | right: 0px; 30 | top: 0px; 31 | bottom: 0px; 32 | display: flex; 33 | justify-content: center; 34 | align-items: center; 35 | transform: rotate(${e=>e.startRotationDegrees}deg); 36 | 37 | &.started-spinning { 38 | animation: spin-${({classKey:e})=>e} ${({startSpinningTime:e})=>e/1e3}s cubic-bezier( 39 | 0.71, 40 | ${e=>e.disableInitialAnimation?0:-.29}, 41 | 0.96, 42 | 0.9 43 | ) 0s 1 normal forwards running, 44 | continueSpin-${({classKey:e})=>e} ${({continueSpinningTime:e})=>e/1e3}s linear ${({startSpinningTime:e})=>e/1e3}s 1 normal forwards running, 45 | stopSpin-${({classKey:e})=>e} ${({stopSpinningTime:e})=>e/1e3}s cubic-bezier(0, 0, 0.35, 1.02) ${({startSpinningTime:e,continueSpinningTime:t})=>(e+t)/1e3}s 1 normal forwards 46 | running; 47 | } 48 | 49 | @keyframes spin-${({classKey:e})=>e} { 50 | from { 51 | transform: rotate(${e=>e.startRotationDegrees}deg); 52 | } 53 | to { 54 | transform: rotate(${e=>e.startRotationDegrees+360}deg); 55 | } 56 | } 57 | @keyframes continueSpin-${({classKey:e})=>e} { 58 | from { 59 | transform: rotate(${e=>e.startRotationDegrees}deg); 60 | } 61 | to { 62 | transform: rotate(${e=>e.startRotationDegrees+360}deg); 63 | } 64 | } 65 | @keyframes stopSpin-${({classKey:e})=>e} { 66 | from { 67 | transform: rotate(${e=>e.startRotationDegrees}deg); 68 | } 69 | to { 70 | transform: rotate(${e=>1440+e.finalRotationDegrees}deg); 71 | } 72 | } 73 | `,w=Object(m.a)(v)` 74 | position: absolute; 75 | z-index: 5; 76 | width: 17%; 77 | right: 6px; 78 | top: 15px; 79 | `,S=m.a.canvas` 80 | width: 98%; 81 | height: 98%; 82 | `;var k=function(e,t,n,r,i,o){e.beginPath(),e.moveTo(t+(r+1)*Math.cos(o),n+(r+1)*Math.sin(o)),e.lineTo(t+(i-1)*Math.cos(o),n+(i-1)*Math.sin(o)),e.closePath(),e.stroke()},C=function(e){var t=e.width,n=e.height,o=e.data,a=e.outerBorderColor,s=e.outerBorderWidth,c=e.innerRadius,l=e.innerBorderColor,u=e.innerBorderWidth,h=e.radiusLineColor,p=e.radiusLineWidth,g=e.fontFamily,m=e.fontWeight,v=e.fontSize,y=e.fontStyle,b=e.perpendicularText,w=e.prizeMap,C=e.rouletteUpdater,A=e.textDistance,x=Object(r.createRef)(),O={outerBorderColor:a,outerBorderWidth:s,innerRadius:c,innerBorderColor:l,innerBorderWidth:u,radiusLineColor:h,radiusLineWidth:p,fontFamily:g,fontWeight:m,fontSize:v,fontStyle:y,perpendicularText:b,prizeMap:w,rouletteUpdater:C,textDistance:A};return Object(r.useEffect)((function(){!function(e,t,n){var r,i,o,a,s,c=n.outerBorderColor,l=n.outerBorderWidth,u=n.innerRadius,h=n.innerBorderColor,p=n.innerBorderWidth,g=n.radiusLineColor,m=n.radiusLineWidth,v=n.fontFamily,y=n.fontWeight,b=n.fontSize,w=n.fontStyle,S=n.perpendicularText,C=n.prizeMap,A=n.textDistance,x=d(C);l*=2,p*=2,m*=2;var O=e.current;if(null==O?void 0:O.getContext("2d")){var T=O.getContext("2d");T.clearRect(0,0,500,500),T.strokeStyle="transparent",T.lineWidth=0;for(var j=0,P=O.width/2-10,E=P*f(0,100,A)/100,R=P*f(0,100,u)/100,I=O.width/2,M=O.height/2,L=0;L0)try{a.a.load({google:{families:Array.from(new Set(b.filter((function(e){return!!e}))))},timeout:1e3,fontactive:function(){Me(!Ie)},active:function(){Ge(!0),Me(!Ie)}})}catch(e){console.log("Error loading webfonts:",e)}else Ge(!0);le(x([],y,!0)),he(m),Qe(ne,m),Ee(!0)}),[o,S,O]),Object(r.useEffect)((function(){var e;if(t&&!Oe){Te(!0),Ye();var r=fe[n][Math.floor(Math.random()*(null===(e=fe[n])||void 0===e?void 0:e.length))],i=u(r,d(fe));ye(i)}}),[t]),Object(r.useEffect)((function(){Ce&&(Te(!1),ge(ve))}),[Ce]);var Ye=function(){Se(!0),Ae(!1),_e.current=!0,setTimeout((function(){_e.current&&(_e.current=!1,Se(!1),Ae(!0),m())}),Je)},Qe=function(e,t){var n;if(ne>=0){var r=Math.floor(e)%(null==t?void 0:t.length),i=t[r][Math.floor((null===(n=t[r])||void 0===n?void 0:n.length)/2)];ge(u(i,d(t),!1))}};return Pe?i.a.createElement(y,{style:!He||De>0&&Ne!==De?{visibility:"hidden"}:{}},i.a.createElement(b,{className:we?"started-spinning":"",classKey:Ke,startSpinningTime:Ue,continueSpinningTime:qe,stopSpinningTime:Xe,startRotationDegrees:pe,finalRotationDegrees:ve,disableInitialAnimation:ae},i.a.createElement(C,{width:"900",height:"900",data:ce,outerBorderColor:j,outerBorderWidth:E,innerRadius:I,innerBorderColor:L,innerBorderWidth:W,radiusLineColor:D,radiusLineWidth:F,fontFamily:G,fontWeight:U,fontStyle:X,fontSize:K,perpendicularText:Y,prizeMap:fe,rouletteUpdater:Ie,textDistance:V})),i.a.createElement(w,{style:null==ie?void 0:ie.style,src:(null==ie?void 0:ie.src)||g.src,alt:"roulette-static"})):null}}])})); -------------------------------------------------------------------------------- /dist/components/Wheel/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { PointerProps, WheelData } from './types'; 3 | interface Props { 4 | mustStartSpinning: boolean; 5 | prizeNumber: number; 6 | data: WheelData[]; 7 | onStopSpinning?: () => any; 8 | backgroundColors?: string[]; 9 | textColors?: string[]; 10 | outerBorderColor?: string; 11 | outerBorderWidth?: number; 12 | innerRadius?: number; 13 | innerBorderColor?: string; 14 | innerBorderWidth?: number; 15 | radiusLineColor?: string; 16 | radiusLineWidth?: number; 17 | fontFamily?: string; 18 | fontSize?: number; 19 | fontWeight?: number | string; 20 | fontStyle?: string; 21 | perpendicularText?: boolean; 22 | textDistance?: number; 23 | spinDuration?: number; 24 | startingOptionIndex?: number; 25 | pointerProps?: PointerProps; 26 | disableInitialAnimation?: boolean; 27 | } 28 | export declare const Wheel: ({ mustStartSpinning, prizeNumber, data, onStopSpinning, backgroundColors, textColors, outerBorderColor, outerBorderWidth, innerRadius, innerBorderColor, innerBorderWidth, radiusLineColor, radiusLineWidth, fontFamily, fontSize, fontWeight, fontStyle, perpendicularText, textDistance, spinDuration, startingOptionIndex, pointerProps, disableInitialAnimation, }: Props) => JSX.Element | null; 29 | export {}; 30 | -------------------------------------------------------------------------------- /dist/components/Wheel/index.js: -------------------------------------------------------------------------------- 1 | var __assign = (this && this.__assign) || function () { 2 | __assign = Object.assign || function(t) { 3 | for (var s, i = 1, n = arguments.length; i < n; i++) { 4 | s = arguments[i]; 5 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) 6 | t[p] = s[p]; 7 | } 8 | return t; 9 | }; 10 | return __assign.apply(this, arguments); 11 | }; 12 | var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { 13 | if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { 14 | if (ar || !(i in from)) { 15 | if (!ar) ar = Array.prototype.slice.call(from, 0, i); 16 | ar[i] = from[i]; 17 | } 18 | } 19 | return to.concat(ar || Array.prototype.slice.call(from)); 20 | }; 21 | import React, { useEffect, useRef, useState } from 'react'; 22 | import WebFont from 'webfontloader'; 23 | import { getQuantity, getRotationDegrees, isCustomFont, makeClassKey, } from '../../utils'; 24 | import { roulettePointer } from '../common/images'; 25 | import { RotationContainer, RouletteContainer, RoulettePointerImage, } from './styles'; 26 | import { DEFAULT_BACKGROUND_COLORS, DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE, DEFAULT_FONT_STYLE, DEFAULT_FONT_WEIGHT, DEFAULT_INNER_BORDER_COLOR, DEFAULT_INNER_BORDER_WIDTH, DEFAULT_INNER_RADIUS, DEFAULT_OUTER_BORDER_COLOR, DEFAULT_OUTER_BORDER_WIDTH, DEFAULT_RADIUS_LINE_COLOR, DEFAULT_RADIUS_LINE_WIDTH, DEFAULT_SPIN_DURATION, DEFAULT_TEXT_COLORS, DEFAULT_TEXT_DISTANCE, WEB_FONTS, DISABLE_INITIAL_ANIMATION, } from '../../strings'; 27 | import WheelCanvas from '../WheelCanvas'; 28 | var STARTED_SPINNING = 'started-spinning'; 29 | var START_SPINNING_TIME = 2600; 30 | var CONTINUE_SPINNING_TIME = 750; 31 | var STOP_SPINNING_TIME = 8000; 32 | export var Wheel = function (_a) { 33 | var mustStartSpinning = _a.mustStartSpinning, prizeNumber = _a.prizeNumber, data = _a.data, _b = _a.onStopSpinning, onStopSpinning = _b === void 0 ? function () { return null; } : _b, _c = _a.backgroundColors, backgroundColors = _c === void 0 ? DEFAULT_BACKGROUND_COLORS : _c, _d = _a.textColors, textColors = _d === void 0 ? DEFAULT_TEXT_COLORS : _d, _e = _a.outerBorderColor, outerBorderColor = _e === void 0 ? DEFAULT_OUTER_BORDER_COLOR : _e, _f = _a.outerBorderWidth, outerBorderWidth = _f === void 0 ? DEFAULT_OUTER_BORDER_WIDTH : _f, _g = _a.innerRadius, innerRadius = _g === void 0 ? DEFAULT_INNER_RADIUS : _g, _h = _a.innerBorderColor, innerBorderColor = _h === void 0 ? DEFAULT_INNER_BORDER_COLOR : _h, _j = _a.innerBorderWidth, innerBorderWidth = _j === void 0 ? DEFAULT_INNER_BORDER_WIDTH : _j, _k = _a.radiusLineColor, radiusLineColor = _k === void 0 ? DEFAULT_RADIUS_LINE_COLOR : _k, _l = _a.radiusLineWidth, radiusLineWidth = _l === void 0 ? DEFAULT_RADIUS_LINE_WIDTH : _l, _m = _a.fontFamily, fontFamily = _m === void 0 ? WEB_FONTS[0] : _m, _o = _a.fontSize, fontSize = _o === void 0 ? DEFAULT_FONT_SIZE : _o, _p = _a.fontWeight, fontWeight = _p === void 0 ? DEFAULT_FONT_WEIGHT : _p, _q = _a.fontStyle, fontStyle = _q === void 0 ? DEFAULT_FONT_STYLE : _q, _r = _a.perpendicularText, perpendicularText = _r === void 0 ? false : _r, _s = _a.textDistance, textDistance = _s === void 0 ? DEFAULT_TEXT_DISTANCE : _s, _t = _a.spinDuration, spinDuration = _t === void 0 ? DEFAULT_SPIN_DURATION : _t, _u = _a.startingOptionIndex, startingOptionIndex = _u === void 0 ? -1 : _u, _v = _a.pointerProps, pointerProps = _v === void 0 ? {} : _v, _w = _a.disableInitialAnimation, disableInitialAnimation = _w === void 0 ? DISABLE_INITIAL_ANIMATION : _w; 34 | var _x = useState(__spreadArray([], data, true)), wheelData = _x[0], setWheelData = _x[1]; 35 | var _y = useState([[0]]), prizeMap = _y[0], setPrizeMap = _y[1]; 36 | var _z = useState(0), startRotationDegrees = _z[0], setStartRotationDegrees = _z[1]; 37 | var _0 = useState(0), finalRotationDegrees = _0[0], setFinalRotationDegrees = _0[1]; 38 | var _1 = useState(false), hasStartedSpinning = _1[0], setHasStartedSpinning = _1[1]; 39 | var _2 = useState(false), hasStoppedSpinning = _2[0], setHasStoppedSpinning = _2[1]; 40 | var _3 = useState(false), isCurrentlySpinning = _3[0], setIsCurrentlySpinning = _3[1]; 41 | var _4 = useState(false), isDataUpdated = _4[0], setIsDataUpdated = _4[1]; 42 | var _5 = useState(false), rouletteUpdater = _5[0], setRouletteUpdater = _5[1]; 43 | var _6 = useState(0), loadedImagesCounter = _6[0], setLoadedImagesCounter = _6[1]; 44 | var _7 = useState(0), totalImages = _7[0], setTotalImages = _7[1]; 45 | var _8 = useState(false), isFontLoaded = _8[0], setIsFontLoaded = _8[1]; 46 | var mustStopSpinning = useRef(false); 47 | var classKey = makeClassKey(5); 48 | var normalizedSpinDuration = Math.max(0.01, spinDuration); 49 | var startSpinningTime = START_SPINNING_TIME * normalizedSpinDuration; 50 | var continueSpinningTime = CONTINUE_SPINNING_TIME * normalizedSpinDuration; 51 | var stopSpinningTime = STOP_SPINNING_TIME * normalizedSpinDuration; 52 | var totalSpinningTime = startSpinningTime + continueSpinningTime + stopSpinningTime; 53 | useEffect(function () { 54 | var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k; 55 | var initialMapNum = 0; 56 | var auxPrizeMap = []; 57 | var dataLength = (data === null || data === void 0 ? void 0 : data.length) || 0; 58 | var wheelDataAux = [{ option: '', optionSize: 1 }]; 59 | var fontsToFetch = isCustomFont(fontFamily === null || fontFamily === void 0 ? void 0 : fontFamily.trim()) ? [fontFamily] : []; 60 | var _loop_1 = function (i) { 61 | var fontArray = ((_c = (_b = (_a = data[i]) === null || _a === void 0 ? void 0 : _a.style) === null || _b === void 0 ? void 0 : _b.fontFamily) === null || _c === void 0 ? void 0 : _c.split(',')) || []; 62 | fontArray = fontArray.map(function (font) { return font.trim(); }).filter(isCustomFont); 63 | fontsToFetch.push.apply(fontsToFetch, fontArray); 64 | wheelDataAux[i] = __assign(__assign({}, data[i]), { style: { 65 | backgroundColor: ((_d = data[i].style) === null || _d === void 0 ? void 0 : _d.backgroundColor) || 66 | (backgroundColors === null || backgroundColors === void 0 ? void 0 : backgroundColors[i % (backgroundColors === null || backgroundColors === void 0 ? void 0 : backgroundColors.length)]) || 67 | DEFAULT_BACKGROUND_COLORS[0], 68 | fontFamily: ((_e = data[i].style) === null || _e === void 0 ? void 0 : _e.fontFamily) || fontFamily || DEFAULT_FONT_FAMILY, 69 | fontSize: ((_f = data[i].style) === null || _f === void 0 ? void 0 : _f.fontSize) || fontSize || DEFAULT_FONT_SIZE, 70 | fontWeight: ((_g = data[i].style) === null || _g === void 0 ? void 0 : _g.fontWeight) || fontWeight || DEFAULT_FONT_WEIGHT, 71 | fontStyle: ((_h = data[i].style) === null || _h === void 0 ? void 0 : _h.fontStyle) || fontStyle || DEFAULT_FONT_STYLE, 72 | textColor: ((_j = data[i].style) === null || _j === void 0 ? void 0 : _j.textColor) || 73 | (textColors === null || textColors === void 0 ? void 0 : textColors[i % (textColors === null || textColors === void 0 ? void 0 : textColors.length)]) || 74 | DEFAULT_TEXT_COLORS[0], 75 | } }); 76 | auxPrizeMap.push([]); 77 | for (var j = 0; j < (wheelDataAux[i].optionSize || 1); j++) { 78 | auxPrizeMap[i][j] = initialMapNum++; 79 | } 80 | if (data[i].image) { 81 | setTotalImages(function (prevCounter) { return prevCounter + 1; }); 82 | var img_1 = new Image(); 83 | img_1.src = ((_k = data[i].image) === null || _k === void 0 ? void 0 : _k.uri) || ''; 84 | img_1.onload = function () { 85 | var _a, _b, _c, _d, _e, _f; 86 | img_1.height = 200 * (((_a = data[i].image) === null || _a === void 0 ? void 0 : _a.sizeMultiplier) || 1); 87 | img_1.width = (img_1.naturalWidth / img_1.naturalHeight) * img_1.height; 88 | wheelDataAux[i].image = { 89 | uri: ((_b = data[i].image) === null || _b === void 0 ? void 0 : _b.uri) || '', 90 | offsetX: ((_c = data[i].image) === null || _c === void 0 ? void 0 : _c.offsetX) || 0, 91 | offsetY: ((_d = data[i].image) === null || _d === void 0 ? void 0 : _d.offsetY) || 0, 92 | landscape: ((_e = data[i].image) === null || _e === void 0 ? void 0 : _e.landscape) || false, 93 | sizeMultiplier: ((_f = data[i].image) === null || _f === void 0 ? void 0 : _f.sizeMultiplier) || 1, 94 | _imageHTML: img_1, 95 | }; 96 | setLoadedImagesCounter(function (prevCounter) { return prevCounter + 1; }); 97 | setRouletteUpdater(function (prevState) { return !prevState; }); 98 | }; 99 | } 100 | }; 101 | for (var i = 0; i < dataLength; i++) { 102 | _loop_1(i); 103 | } 104 | if ((fontsToFetch === null || fontsToFetch === void 0 ? void 0 : fontsToFetch.length) > 0) { 105 | try { 106 | WebFont.load({ 107 | google: { 108 | families: Array.from(new Set(fontsToFetch.filter(function (font) { return !!font; }))), 109 | }, 110 | timeout: 1000, 111 | fontactive: function () { 112 | setRouletteUpdater(!rouletteUpdater); 113 | }, 114 | active: function () { 115 | setIsFontLoaded(true); 116 | setRouletteUpdater(!rouletteUpdater); 117 | }, 118 | }); 119 | } 120 | catch (err) { 121 | console.log('Error loading webfonts:', err); 122 | } 123 | } 124 | else { 125 | setIsFontLoaded(true); 126 | } 127 | setWheelData(__spreadArray([], wheelDataAux, true)); 128 | setPrizeMap(auxPrizeMap); 129 | setStartingOption(startingOptionIndex, auxPrizeMap); 130 | setIsDataUpdated(true); 131 | }, [data, backgroundColors, textColors]); 132 | useEffect(function () { 133 | var _a; 134 | if (mustStartSpinning && !isCurrentlySpinning) { 135 | setIsCurrentlySpinning(true); 136 | startSpinning(); 137 | var selectedPrize = prizeMap[prizeNumber][Math.floor(Math.random() * ((_a = prizeMap[prizeNumber]) === null || _a === void 0 ? void 0 : _a.length))]; 138 | var finalRotationDegreesCalculated = getRotationDegrees(selectedPrize, getQuantity(prizeMap)); 139 | setFinalRotationDegrees(finalRotationDegreesCalculated); 140 | } 141 | }, [mustStartSpinning]); 142 | useEffect(function () { 143 | if (hasStoppedSpinning) { 144 | setIsCurrentlySpinning(false); 145 | setStartRotationDegrees(finalRotationDegrees); 146 | } 147 | }, [hasStoppedSpinning]); 148 | var startSpinning = function () { 149 | setHasStartedSpinning(true); 150 | setHasStoppedSpinning(false); 151 | mustStopSpinning.current = true; 152 | setTimeout(function () { 153 | if (mustStopSpinning.current) { 154 | mustStopSpinning.current = false; 155 | setHasStartedSpinning(false); 156 | setHasStoppedSpinning(true); 157 | onStopSpinning(); 158 | } 159 | }, totalSpinningTime); 160 | }; 161 | var setStartingOption = function (optionIndex, optionMap) { 162 | var _a; 163 | if (startingOptionIndex >= 0) { 164 | var idx = Math.floor(optionIndex) % (optionMap === null || optionMap === void 0 ? void 0 : optionMap.length); 165 | var startingOption = optionMap[idx][Math.floor(((_a = optionMap[idx]) === null || _a === void 0 ? void 0 : _a.length) / 2)]; 166 | setStartRotationDegrees(getRotationDegrees(startingOption, getQuantity(optionMap), false)); 167 | } 168 | }; 169 | var getRouletteClass = function () { 170 | if (hasStartedSpinning) { 171 | return STARTED_SPINNING; 172 | } 173 | return ''; 174 | }; 175 | if (!isDataUpdated) { 176 | return null; 177 | } 178 | return (React.createElement(RouletteContainer, { style: !isFontLoaded || 179 | (totalImages > 0 && loadedImagesCounter !== totalImages) 180 | ? { visibility: 'hidden' } 181 | : {} }, 182 | React.createElement(RotationContainer, { className: getRouletteClass(), classKey: classKey, startSpinningTime: startSpinningTime, continueSpinningTime: continueSpinningTime, stopSpinningTime: stopSpinningTime, startRotationDegrees: startRotationDegrees, finalRotationDegrees: finalRotationDegrees, disableInitialAnimation: disableInitialAnimation }, 183 | React.createElement(WheelCanvas, { width: "900", height: "900", data: wheelData, outerBorderColor: outerBorderColor, outerBorderWidth: outerBorderWidth, innerRadius: innerRadius, innerBorderColor: innerBorderColor, innerBorderWidth: innerBorderWidth, radiusLineColor: radiusLineColor, radiusLineWidth: radiusLineWidth, fontFamily: fontFamily, fontWeight: fontWeight, fontStyle: fontStyle, fontSize: fontSize, perpendicularText: perpendicularText, prizeMap: prizeMap, rouletteUpdater: rouletteUpdater, textDistance: textDistance })), 184 | React.createElement(RoulettePointerImage, { style: pointerProps === null || pointerProps === void 0 ? void 0 : pointerProps.style, src: (pointerProps === null || pointerProps === void 0 ? void 0 : pointerProps.src) || roulettePointer.src, alt: "roulette-static" }))); 185 | }; 186 | -------------------------------------------------------------------------------- /dist/components/Wheel/styles.d.ts: -------------------------------------------------------------------------------- 1 | export const RouletteContainer: any; 2 | export const RotationContainer: any; 3 | export const RoulettePointerImage: any; 4 | -------------------------------------------------------------------------------- /dist/components/Wheel/styles.js: -------------------------------------------------------------------------------- 1 | var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { 2 | if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } 3 | return cooked; 4 | }; 5 | import styled from 'styled-components'; 6 | import { NonDraggableImage } from '../common/styledComponents'; 7 | export var RouletteContainer = styled.div(templateObject_1 || (templateObject_1 = __makeTemplateObject(["\n position: relative;\n width: 80vw;\n max-width: 445px;\n height: 80vw;\n max-height: 445px;\n object-fit: contain;\n flex-shrink: 0;\n z-index: 5;\n pointer-events: none;\n"], ["\n position: relative;\n width: 80vw;\n max-width: 445px;\n height: 80vw;\n max-height: 445px;\n object-fit: contain;\n flex-shrink: 0;\n z-index: 5;\n pointer-events: none;\n"]))); 8 | export var RotationContainer = styled.div(templateObject_2 || (templateObject_2 = __makeTemplateObject(["\n position: absolute;\n width: 100%;\n left: 0px;\n right: 0px;\n top: 0px;\n bottom: 0px;\n display: flex;\n justify-content: center;\n align-items: center;\n transform: rotate(", "deg);\n\n &.started-spinning {\n animation: spin-", " ", "s cubic-bezier(\n 0.71,\n ", ",\n 0.96,\n 0.9\n ) 0s 1 normal forwards running,\n continueSpin-", " ", "s linear ", "s 1 normal forwards running,\n stopSpin-", " ", "s cubic-bezier(0, 0, 0.35, 1.02) ", "s 1 normal forwards\n running;\n }\n\n @keyframes spin-", " {\n from {\n transform: rotate(", "deg);\n }\n to {\n transform: rotate(", "deg);\n }\n }\n @keyframes continueSpin-", " {\n from {\n transform: rotate(", "deg);\n }\n to {\n transform: rotate(", "deg);\n }\n }\n @keyframes stopSpin-", " {\n from {\n transform: rotate(", "deg);\n }\n to {\n transform: rotate(", "deg);\n }\n }\n"], ["\n position: absolute;\n width: 100%;\n left: 0px;\n right: 0px;\n top: 0px;\n bottom: 0px;\n display: flex;\n justify-content: center;\n align-items: center;\n transform: rotate(", "deg);\n\n &.started-spinning {\n animation: spin-", " ", "s cubic-bezier(\n 0.71,\n ", ",\n 0.96,\n 0.9\n ) 0s 1 normal forwards running,\n continueSpin-", " ", "s linear ", "s 1 normal forwards running,\n stopSpin-", " ", "s cubic-bezier(0, 0, 0.35, 1.02) ", "s 1 normal forwards\n running;\n }\n\n @keyframes spin-", " {\n from {\n transform: rotate(", "deg);\n }\n to {\n transform: rotate(", "deg);\n }\n }\n @keyframes continueSpin-", " {\n from {\n transform: rotate(", "deg);\n }\n to {\n transform: rotate(", "deg);\n }\n }\n @keyframes stopSpin-", " {\n from {\n transform: rotate(", "deg);\n }\n to {\n transform: rotate(", "deg);\n }\n }\n"])), function (props) { return props.startRotationDegrees; }, function (_a) { 9 | var classKey = _a.classKey; 10 | return classKey; 11 | }, function (_a) { 12 | var startSpinningTime = _a.startSpinningTime; 13 | return startSpinningTime / 1000; 14 | }, function (props) { return (props.disableInitialAnimation ? 0 : -0.29); }, function (_a) { 15 | var classKey = _a.classKey; 16 | return classKey; 17 | }, function (_a) { 18 | var continueSpinningTime = _a.continueSpinningTime; 19 | return continueSpinningTime / 1000; 20 | }, function (_a) { 21 | var startSpinningTime = _a.startSpinningTime; 22 | return startSpinningTime / 1000; 23 | }, function (_a) { 24 | var classKey = _a.classKey; 25 | return classKey; 26 | }, function (_a) { 27 | var stopSpinningTime = _a.stopSpinningTime; 28 | return stopSpinningTime / 1000; 29 | }, function (_a) { 30 | var startSpinningTime = _a.startSpinningTime, continueSpinningTime = _a.continueSpinningTime; 31 | return (startSpinningTime + continueSpinningTime) / 1000; 32 | }, function (_a) { 33 | var classKey = _a.classKey; 34 | return classKey; 35 | }, function (props) { return props.startRotationDegrees; }, function (props) { return props.startRotationDegrees + 360; }, function (_a) { 36 | var classKey = _a.classKey; 37 | return classKey; 38 | }, function (props) { return props.startRotationDegrees; }, function (props) { return props.startRotationDegrees + 360; }, function (_a) { 39 | var classKey = _a.classKey; 40 | return classKey; 41 | }, function (props) { return props.startRotationDegrees; }, function (props) { return 1440 + props.finalRotationDegrees; }); 42 | export var RoulettePointerImage = styled(NonDraggableImage)(templateObject_3 || (templateObject_3 = __makeTemplateObject(["\n position: absolute;\n z-index: 5;\n width: 17%;\n right: 6px;\n top: 15px;\n"], ["\n position: absolute;\n z-index: 5;\n width: 17%;\n right: 6px;\n top: 15px;\n"]))); 43 | var templateObject_1, templateObject_2, templateObject_3; 44 | -------------------------------------------------------------------------------- /dist/components/Wheel/types.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | interface ImagePropsLocal extends ImageProps { 3 | _imageHTML?: HTMLImageElement; 4 | } 5 | export interface WheelData { 6 | image?: ImagePropsLocal; 7 | option?: string; 8 | style?: StyleType; 9 | optionSize?: number; 10 | } 11 | export interface StyleType { 12 | backgroundColor?: string; 13 | textColor?: string; 14 | fontFamily?: string; 15 | fontSize?: number; 16 | fontWeight?: number | string; 17 | fontStyle?: string; 18 | } 19 | export interface PointerProps { 20 | src?: string; 21 | style?: React.CSSProperties; 22 | } 23 | export interface ImageProps { 24 | uri: string; 25 | offsetX?: number; 26 | offsetY?: number; 27 | sizeMultiplier?: number; 28 | landscape?: boolean; 29 | } 30 | export {}; 31 | -------------------------------------------------------------------------------- /dist/components/Wheel/types.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /dist/components/WheelCanvas/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { WheelData } from '../Wheel/types'; 3 | interface WheelCanvasProps extends DrawWheelProps { 4 | width: string; 5 | height: string; 6 | data: WheelData[]; 7 | } 8 | interface DrawWheelProps { 9 | outerBorderColor: string; 10 | outerBorderWidth: number; 11 | innerRadius: number; 12 | innerBorderColor: string; 13 | innerBorderWidth: number; 14 | radiusLineColor: string; 15 | radiusLineWidth: number; 16 | fontFamily: string; 17 | fontWeight: number | string; 18 | fontSize: number; 19 | fontStyle: string; 20 | perpendicularText: boolean; 21 | prizeMap: number[][]; 22 | rouletteUpdater: boolean; 23 | textDistance: number; 24 | } 25 | declare const WheelCanvas: ({ width, height, data, outerBorderColor, outerBorderWidth, innerRadius, innerBorderColor, innerBorderWidth, radiusLineColor, radiusLineWidth, fontFamily, fontWeight, fontSize, fontStyle, perpendicularText, prizeMap, rouletteUpdater, textDistance, }: WheelCanvasProps) => JSX.Element; 26 | export default WheelCanvas; 27 | -------------------------------------------------------------------------------- /dist/components/WheelCanvas/index.js: -------------------------------------------------------------------------------- 1 | import React, { createRef, useEffect } from 'react'; 2 | import { WheelCanvasStyle } from './styles'; 3 | import { clamp, getQuantity } from '../../utils'; 4 | var drawRadialBorder = function (ctx, centerX, centerY, insideRadius, outsideRadius, angle) { 5 | ctx.beginPath(); 6 | ctx.moveTo(centerX + (insideRadius + 1) * Math.cos(angle), centerY + (insideRadius + 1) * Math.sin(angle)); 7 | ctx.lineTo(centerX + (outsideRadius - 1) * Math.cos(angle), centerY + (outsideRadius - 1) * Math.sin(angle)); 8 | ctx.closePath(); 9 | ctx.stroke(); 10 | }; 11 | var drawWheel = function (canvasRef, data, drawWheelProps) { 12 | var _a, _b, _c, _d, _e; 13 | /* eslint-disable prefer-const */ 14 | var outerBorderColor = drawWheelProps.outerBorderColor, outerBorderWidth = drawWheelProps.outerBorderWidth, innerRadius = drawWheelProps.innerRadius, innerBorderColor = drawWheelProps.innerBorderColor, innerBorderWidth = drawWheelProps.innerBorderWidth, radiusLineColor = drawWheelProps.radiusLineColor, radiusLineWidth = drawWheelProps.radiusLineWidth, fontFamily = drawWheelProps.fontFamily, fontWeight = drawWheelProps.fontWeight, fontSize = drawWheelProps.fontSize, fontStyle = drawWheelProps.fontStyle, perpendicularText = drawWheelProps.perpendicularText, prizeMap = drawWheelProps.prizeMap, textDistance = drawWheelProps.textDistance; 15 | var QUANTITY = getQuantity(prizeMap); 16 | outerBorderWidth *= 2; 17 | innerBorderWidth *= 2; 18 | radiusLineWidth *= 2; 19 | var canvas = canvasRef.current; 20 | if (canvas === null || canvas === void 0 ? void 0 : canvas.getContext('2d')) { 21 | var ctx = canvas.getContext('2d'); 22 | ctx.clearRect(0, 0, 500, 500); 23 | ctx.strokeStyle = 'transparent'; 24 | ctx.lineWidth = 0; 25 | var startAngle = 0; 26 | var outsideRadius = canvas.width / 2 - 10; 27 | var clampedContentDistance = clamp(0, 100, textDistance); 28 | var contentRadius = (outsideRadius * clampedContentDistance) / 100; 29 | var clampedInsideRadius = clamp(0, 100, innerRadius); 30 | var insideRadius = (outsideRadius * clampedInsideRadius) / 100; 31 | var centerX = canvas.width / 2; 32 | var centerY = canvas.height / 2; 33 | for (var i = 0; i < data.length; i++) { 34 | var _f = data[i], optionSize = _f.optionSize, style = _f.style; 35 | var arc = (optionSize && (optionSize * (2 * Math.PI)) / QUANTITY) || 36 | (2 * Math.PI) / QUANTITY; 37 | var endAngle = startAngle + arc; 38 | ctx.fillStyle = (style && style.backgroundColor); 39 | ctx.beginPath(); 40 | ctx.arc(centerX, centerY, outsideRadius, startAngle, endAngle, false); 41 | ctx.arc(centerX, centerY, insideRadius, endAngle, startAngle, true); 42 | ctx.stroke(); 43 | ctx.fill(); 44 | ctx.save(); 45 | // WHEEL RADIUS LINES 46 | ctx.strokeStyle = radiusLineWidth <= 0 ? 'transparent' : radiusLineColor; 47 | ctx.lineWidth = radiusLineWidth; 48 | drawRadialBorder(ctx, centerX, centerY, insideRadius, outsideRadius, startAngle); 49 | if (i === data.length - 1) { 50 | drawRadialBorder(ctx, centerX, centerY, insideRadius, outsideRadius, endAngle); 51 | } 52 | // WHEEL OUTER BORDER 53 | ctx.strokeStyle = 54 | outerBorderWidth <= 0 ? 'transparent' : outerBorderColor; 55 | ctx.lineWidth = outerBorderWidth; 56 | ctx.beginPath(); 57 | ctx.arc(centerX, centerY, outsideRadius - ctx.lineWidth / 2, 0, 2 * Math.PI); 58 | ctx.closePath(); 59 | ctx.stroke(); 60 | // WHEEL INNER BORDER 61 | ctx.strokeStyle = 62 | innerBorderWidth <= 0 ? 'transparent' : innerBorderColor; 63 | ctx.lineWidth = innerBorderWidth; 64 | ctx.beginPath(); 65 | ctx.arc(centerX, centerY, insideRadius + ctx.lineWidth / 2 - 1, 0, 2 * Math.PI); 66 | ctx.closePath(); 67 | ctx.stroke(); 68 | // CONTENT FILL 69 | ctx.translate(centerX + Math.cos(startAngle + arc / 2) * contentRadius, centerY + Math.sin(startAngle + arc / 2) * contentRadius); 70 | var contentRotationAngle = startAngle + arc / 2; 71 | if (data[i].image) { 72 | // CASE IMAGE 73 | contentRotationAngle += 74 | data[i].image && !((_a = data[i].image) === null || _a === void 0 ? void 0 : _a.landscape) ? Math.PI / 2 : 0; 75 | ctx.rotate(contentRotationAngle); 76 | var img = ((_b = data[i].image) === null || _b === void 0 ? void 0 : _b._imageHTML) || new Image(); 77 | ctx.drawImage(img, (img.width + (((_c = data[i].image) === null || _c === void 0 ? void 0 : _c.offsetX) || 0)) / -2, -(img.height - 78 | (((_d = data[i].image) === null || _d === void 0 ? void 0 : _d.landscape) ? 0 : 90) + // offsetY correction for non landscape images 79 | (((_e = data[i].image) === null || _e === void 0 ? void 0 : _e.offsetY) || 0)) / 2, img.width, img.height); 80 | } 81 | else { 82 | // CASE TEXT 83 | contentRotationAngle += perpendicularText ? Math.PI / 2 : 0; 84 | ctx.rotate(contentRotationAngle); 85 | var text = data[i].option; 86 | ctx.font = "".concat((style === null || style === void 0 ? void 0 : style.fontStyle) || fontStyle, " ").concat((style === null || style === void 0 ? void 0 : style.fontWeight) || fontWeight, " ").concat(((style === null || style === void 0 ? void 0 : style.fontSize) || fontSize) * 2, "px ").concat((style === null || style === void 0 ? void 0 : style.fontFamily) || fontFamily, ", Helvetica, Arial"); 87 | ctx.fillStyle = (style && style.textColor); 88 | ctx.fillText(text || '', -ctx.measureText(text || '').width / 2, fontSize / 2.7); 89 | } 90 | ctx.restore(); 91 | startAngle = endAngle; 92 | } 93 | } 94 | }; 95 | var WheelCanvas = function (_a) { 96 | var width = _a.width, height = _a.height, data = _a.data, outerBorderColor = _a.outerBorderColor, outerBorderWidth = _a.outerBorderWidth, innerRadius = _a.innerRadius, innerBorderColor = _a.innerBorderColor, innerBorderWidth = _a.innerBorderWidth, radiusLineColor = _a.radiusLineColor, radiusLineWidth = _a.radiusLineWidth, fontFamily = _a.fontFamily, fontWeight = _a.fontWeight, fontSize = _a.fontSize, fontStyle = _a.fontStyle, perpendicularText = _a.perpendicularText, prizeMap = _a.prizeMap, rouletteUpdater = _a.rouletteUpdater, textDistance = _a.textDistance; 97 | var canvasRef = createRef(); 98 | var drawWheelProps = { 99 | outerBorderColor: outerBorderColor, 100 | outerBorderWidth: outerBorderWidth, 101 | innerRadius: innerRadius, 102 | innerBorderColor: innerBorderColor, 103 | innerBorderWidth: innerBorderWidth, 104 | radiusLineColor: radiusLineColor, 105 | radiusLineWidth: radiusLineWidth, 106 | fontFamily: fontFamily, 107 | fontWeight: fontWeight, 108 | fontSize: fontSize, 109 | fontStyle: fontStyle, 110 | perpendicularText: perpendicularText, 111 | prizeMap: prizeMap, 112 | rouletteUpdater: rouletteUpdater, 113 | textDistance: textDistance, 114 | }; 115 | useEffect(function () { 116 | drawWheel(canvasRef, data, drawWheelProps); 117 | }, [canvasRef, data, drawWheelProps, rouletteUpdater]); 118 | return React.createElement(WheelCanvasStyle, { ref: canvasRef, width: width, height: height }); 119 | }; 120 | export default WheelCanvas; 121 | -------------------------------------------------------------------------------- /dist/components/WheelCanvas/styles.d.ts: -------------------------------------------------------------------------------- 1 | export const WheelCanvasStyle: any; 2 | -------------------------------------------------------------------------------- /dist/components/WheelCanvas/styles.js: -------------------------------------------------------------------------------- 1 | var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { 2 | if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } 3 | return cooked; 4 | }; 5 | import styled from 'styled-components'; 6 | export var WheelCanvasStyle = styled.canvas(templateObject_1 || (templateObject_1 = __makeTemplateObject(["\n width: 98%;\n height: 98%;\n"], ["\n width: 98%;\n height: 98%;\n"]))); 7 | var templateObject_1; 8 | -------------------------------------------------------------------------------- /dist/components/common/images.d.ts: -------------------------------------------------------------------------------- 1 | export const roulettePointer: HTMLImageElement; 2 | -------------------------------------------------------------------------------- /dist/components/common/images.js: -------------------------------------------------------------------------------- 1 | // IMAGES 2 | import Icon from '../../assets/roulette-pointer.png'; 3 | var roulettePointer = new Image(); 4 | roulettePointer.src = Icon; 5 | export { roulettePointer }; 6 | -------------------------------------------------------------------------------- /dist/components/common/styledComponents.d.ts: -------------------------------------------------------------------------------- 1 | export const NonDraggableImage: any; 2 | -------------------------------------------------------------------------------- /dist/components/common/styledComponents.js: -------------------------------------------------------------------------------- 1 | var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { 2 | if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } 3 | return cooked; 4 | }; 5 | import styled from 'styled-components'; 6 | export var NonDraggableImage = styled.img(templateObject_1 || (templateObject_1 = __makeTemplateObject(["\n -webkit-user-drag: none;\n -khtml-user-drag: none;\n -moz-user-drag: none;\n -o-user-drag: none;\n user-drag: none;\n"], ["\n -webkit-user-drag: none;\n -khtml-user-drag: none;\n -moz-user-drag: none;\n -o-user-drag: none;\n user-drag: none;\n"]))); 7 | var templateObject_1; 8 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | import { WheelData } from './components/Wheel/types'; 2 | export { Wheel } from './components/Wheel'; 3 | export declare type WheelDataType = WheelData; 4 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | export { Wheel } from './components/Wheel'; 2 | -------------------------------------------------------------------------------- /dist/index.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /dist/index.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { act } from 'react-dom/test-utils'; 4 | import { Wheel } from '.'; 5 | // test('renders Wheel component', () => { 6 | var data = [{ option: '0' }]; 7 | var prizeNumber = 0; 8 | var mustStartSpinning = false; 9 | var backgroundColors = ['#3e3e3e', '#df3428']; 10 | var textColors = ['white']; 11 | var outerBorderColor = '#d8a35a'; 12 | var outerBorderWidth = 8; 13 | var innerBorderColor = '#d8a35a'; 14 | var innerBorderWidth = 17; 15 | var innerRadius = 40; 16 | var radiusLineColor = '#dddddd'; 17 | var radiusLineWidth = 3; 18 | var fontSize = 20; 19 | var textDistance = 86; 20 | var onStopSpinning = function () { return null; }; 21 | jest.useFakeTimers(); 22 | var container; 23 | beforeEach(function () { 24 | container = document.createElement('div'); 25 | document.body.appendChild(container); 26 | }); 27 | afterEach(function () { 28 | document.body.removeChild(container); 29 | container = null; 30 | }); 31 | describe('Render Wheel', function () { 32 | it('required props only', function () { 33 | ReactDOM.render(React.createElement(Wheel, { data: data, prizeNumber: prizeNumber, mustStartSpinning: mustStartSpinning }), container); 34 | }); 35 | it('innerBorderWidth = 0', function () { 36 | ReactDOM.render(React.createElement(Wheel, { data: data, prizeNumber: prizeNumber, mustStartSpinning: mustStartSpinning, innerBorderWidth: 0 }), container); 37 | }); 38 | it('outerBorderWidth = 0', function () { 39 | ReactDOM.render(React.createElement(Wheel, { data: data, prizeNumber: prizeNumber, mustStartSpinning: mustStartSpinning, outerBorderWidth: 0 }), container); 40 | }); 41 | it('radiusLineWidth = 0', function () { 42 | ReactDOM.render(React.createElement(Wheel, { data: data, prizeNumber: prizeNumber, mustStartSpinning: mustStartSpinning, radiusLineWidth: 0 }), container); 43 | }); 44 | it('all props defined', function () { 45 | ReactDOM.render(React.createElement(Wheel, { data: data, prizeNumber: prizeNumber, mustStartSpinning: mustStartSpinning, backgroundColors: backgroundColors, textColors: textColors, fontSize: fontSize, outerBorderColor: outerBorderColor, outerBorderWidth: outerBorderWidth, innerRadius: innerRadius, innerBorderColor: innerBorderColor, innerBorderWidth: innerBorderWidth, radiusLineColor: radiusLineColor, radiusLineWidth: radiusLineWidth, perpendicularText: true, textDistance: textDistance, onStopSpinning: onStopSpinning }), container); 46 | }); 47 | it('render spin', function () { 48 | act(function () { 49 | ReactDOM.render(React.createElement(Wheel, { data: data, prizeNumber: prizeNumber, mustStartSpinning: true }), container); 50 | jest.runOnlyPendingTimers(); 51 | }); 52 | }); 53 | it('render callback trigger', function () { 54 | var hasBeenCalled = false; 55 | act(function () { 56 | ReactDOM.render(React.createElement(Wheel, { data: data, prizeNumber: prizeNumber, mustStartSpinning: true, onStopSpinning: function () { 57 | hasBeenCalled = true; 58 | return null; 59 | } }), container); 60 | expect(hasBeenCalled).not.toBe(true); 61 | jest.runAllTimers(); 62 | }); 63 | expect(hasBeenCalled).toBe(true); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /dist/serviceWorker.d.ts: -------------------------------------------------------------------------------- 1 | declare type Config = { 2 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 3 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 4 | }; 5 | export declare function register(config?: Config): void; 6 | export declare function unregister(): void; 7 | export {}; 8 | -------------------------------------------------------------------------------- /dist/serviceWorker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // This optional code is used to register a service worker. 3 | // register() is not called by default. 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | // To learn more about the benefits of this model and instructions on how to 10 | // opt-in, read https://bit.ly/CRA-PWA 11 | var isLocalhost = Boolean(window.location.hostname === 'localhost' || 12 | // [::1] is the IPv6 localhost address. 13 | window.location.hostname === '[::1]' || 14 | // 127.0.0.0/8 are considered localhost for IPv4. 15 | window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)); 16 | export function register(config) { 17 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 18 | // The URL constructor is available in all browsers that support SW. 19 | var publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 20 | if (publicUrl.origin !== window.location.origin) { 21 | // Our service worker won't work if PUBLIC_URL is on a different origin 22 | // from what our page is served on. This might happen if a CDN is used to 23 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 24 | return; 25 | } 26 | window.addEventListener('load', function () { 27 | var swUrl = "".concat(process.env.PUBLIC_URL, "/service-worker.js"); 28 | if (isLocalhost) { 29 | // This is running on localhost. Let's check if a service worker still exists or not. 30 | checkValidServiceWorker(swUrl, config); 31 | // Add some additional logging to localhost, pointing developers to the 32 | // service worker/PWA documentation. 33 | navigator.serviceWorker.ready.then(function () { 34 | console.log('This web app is being served cache-first by a service ' + 35 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'); 36 | }); 37 | } 38 | else { 39 | // Is not localhost. Just register service worker 40 | registerValidSW(swUrl, config); 41 | } 42 | }); 43 | } 44 | } 45 | function registerValidSW(swUrl, config) { 46 | navigator.serviceWorker 47 | .register(swUrl) 48 | .then(function (registration) { 49 | registration.onupdatefound = function () { 50 | var installingWorker = registration.installing; 51 | if (installingWorker == null) { 52 | return; 53 | } 54 | installingWorker.onstatechange = function () { 55 | if (installingWorker.state === 'installed') { 56 | if (navigator.serviceWorker.controller) { 57 | // At this point, the updated precached content has been fetched, 58 | // but the previous service worker will still serve the older 59 | // content until all client tabs are closed. 60 | console.log('New content is available and will be used when all ' + 61 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'); 62 | // Execute callback 63 | if (config && config.onUpdate) { 64 | config.onUpdate(registration); 65 | } 66 | } 67 | else { 68 | // At this point, everything has been precached. 69 | // It's the perfect time to display a 70 | // "Content is cached for offline use." message. 71 | console.log('Content is cached for offline use.'); 72 | // Execute callback 73 | if (config && config.onSuccess) { 74 | config.onSuccess(registration); 75 | } 76 | } 77 | } 78 | }; 79 | }; 80 | }) 81 | .catch(function (error) { 82 | console.error('Error during service worker registration:', error); 83 | }); 84 | } 85 | function checkValidServiceWorker(swUrl, config) { 86 | // Check if the service worker can be found. If it can't reload the page. 87 | fetch(swUrl, { 88 | headers: { 'Service-Worker': 'script' }, 89 | }) 90 | .then(function (response) { 91 | // Ensure service worker exists, and that we really are getting a JS file. 92 | var contentType = response.headers.get('content-type'); 93 | if (response.status === 404 || 94 | (contentType != null && contentType.indexOf('javascript') === -1)) { 95 | // No service worker found. Probably a different app. Reload the page. 96 | navigator.serviceWorker.ready.then(function (registration) { 97 | registration.unregister().then(function () { 98 | window.location.reload(); 99 | }); 100 | }); 101 | } 102 | else { 103 | // Service worker found. Proceed as normal. 104 | registerValidSW(swUrl, config); 105 | } 106 | }) 107 | .catch(function () { 108 | console.log('No internet connection found. App is running in offline mode.'); 109 | }); 110 | } 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready 114 | .then(function (registration) { 115 | registration.unregister(); 116 | }) 117 | .catch(function (error) { 118 | console.error(error.message); 119 | }); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /dist/setupTests.d.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect'; 2 | import 'jest-canvas-mock'; 3 | -------------------------------------------------------------------------------- /dist/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | import 'jest-canvas-mock'; 7 | -------------------------------------------------------------------------------- /dist/src/components/Wheel/index.d.ts: -------------------------------------------------------------------------------- 1 | import { PointerProps, WheelData } from './types'; 2 | interface Props { 3 | mustStartSpinning: boolean; 4 | prizeNumber: number; 5 | data: WheelData[]; 6 | onStopSpinning?: () => any; 7 | backgroundColors?: string[]; 8 | textColors?: string[]; 9 | outerBorderColor?: string; 10 | outerBorderWidth?: number; 11 | innerRadius?: number; 12 | innerBorderColor?: string; 13 | innerBorderWidth?: number; 14 | radiusLineColor?: string; 15 | radiusLineWidth?: number; 16 | fontFamily?: string; 17 | fontSize?: number; 18 | fontWeight?: number | string; 19 | fontStyle?: string; 20 | perpendicularText?: boolean; 21 | textDistance?: number; 22 | spinDuration?: number; 23 | startingOptionIndex?: number; 24 | pointerProps?: PointerProps; 25 | disableInitialAnimation?: boolean; 26 | } 27 | export declare const Wheel: ({ mustStartSpinning, prizeNumber, data, onStopSpinning, backgroundColors, textColors, outerBorderColor, outerBorderWidth, innerRadius, innerBorderColor, innerBorderWidth, radiusLineColor, radiusLineWidth, fontFamily, fontSize, fontWeight, fontStyle, perpendicularText, textDistance, spinDuration, startingOptionIndex, pointerProps, disableInitialAnimation, }: Props) => JSX.Element | null; 28 | export {}; 29 | -------------------------------------------------------------------------------- /dist/src/components/Wheel/types.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | interface ImagePropsLocal extends ImageProps { 3 | _imageHTML?: HTMLImageElement; 4 | } 5 | export interface WheelData { 6 | image?: ImagePropsLocal; 7 | option?: string; 8 | style?: StyleType; 9 | optionSize?: number; 10 | } 11 | export interface StyleType { 12 | backgroundColor?: string; 13 | textColor?: string; 14 | fontFamily?: string; 15 | fontSize?: number; 16 | fontWeight?: number | string; 17 | fontStyle?: string; 18 | } 19 | export interface PointerProps { 20 | src?: string; 21 | style?: React.CSSProperties; 22 | } 23 | export interface ImageProps { 24 | uri: string; 25 | offsetX?: number; 26 | offsetY?: number; 27 | sizeMultiplier?: number; 28 | landscape?: boolean; 29 | } 30 | export {}; 31 | -------------------------------------------------------------------------------- /dist/src/components/WheelCanvas/index.d.ts: -------------------------------------------------------------------------------- 1 | import { WheelData } from '../Wheel/types'; 2 | interface WheelCanvasProps extends DrawWheelProps { 3 | width: string; 4 | height: string; 5 | data: WheelData[]; 6 | } 7 | interface DrawWheelProps { 8 | outerBorderColor: string; 9 | outerBorderWidth: number; 10 | innerRadius: number; 11 | innerBorderColor: string; 12 | innerBorderWidth: number; 13 | radiusLineColor: string; 14 | radiusLineWidth: number; 15 | fontFamily: string; 16 | fontWeight: number | string; 17 | fontSize: number; 18 | fontStyle: string; 19 | perpendicularText: boolean; 20 | prizeMap: number[][]; 21 | rouletteUpdater: boolean; 22 | textDistance: number; 23 | } 24 | declare const WheelCanvas: ({ width, height, data, outerBorderColor, outerBorderWidth, innerRadius, innerBorderColor, innerBorderWidth, radiusLineColor, radiusLineWidth, fontFamily, fontWeight, fontSize, fontStyle, perpendicularText, prizeMap, rouletteUpdater, textDistance, }: WheelCanvasProps) => JSX.Element; 25 | export default WheelCanvas; 26 | -------------------------------------------------------------------------------- /dist/src/index.d.ts: -------------------------------------------------------------------------------- 1 | import { WheelData } from './components/Wheel/types'; 2 | export { Wheel } from './components/Wheel'; 3 | export declare type WheelDataType = WheelData; 4 | -------------------------------------------------------------------------------- /dist/src/index.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /dist/src/serviceWorker.d.ts: -------------------------------------------------------------------------------- 1 | declare type Config = { 2 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 3 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 4 | }; 5 | export declare function register(config?: Config): void; 6 | export declare function unregister(): void; 7 | export {}; 8 | -------------------------------------------------------------------------------- /dist/src/setupTests.d.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect'; 2 | import 'jest-canvas-mock'; 3 | -------------------------------------------------------------------------------- /dist/src/utils.d.ts: -------------------------------------------------------------------------------- 1 | export declare const getRotationDegrees: (prizeNumber: number, numberOfPrizes: number, randomDif?: boolean) => number; 2 | export declare const clamp: (min: number, max: number, val: number) => number; 3 | export declare const isCustomFont: (font: string) => boolean; 4 | export declare const getQuantity: (prizeMap: number[][]) => number; 5 | export declare const makeClassKey: (length: number) => string; 6 | -------------------------------------------------------------------------------- /dist/strings.d.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_BACKGROUND_COLORS: string[]; 2 | export const DEFAULT_TEXT_COLORS: string[]; 3 | export const DEFAULT_OUTER_BORDER_COLOR: "black"; 4 | export const DEFAULT_OUTER_BORDER_WIDTH: 5; 5 | export const DEFAULT_INNER_RADIUS: 0; 6 | export const DEFAULT_INNER_BORDER_COLOR: "black"; 7 | export const DEFAULT_INNER_BORDER_WIDTH: 0; 8 | export const DEFAULT_RADIUS_LINE_COLOR: "black"; 9 | export const DEFAULT_RADIUS_LINE_WIDTH: 5; 10 | export const DEFAULT_FONT_FAMILY: "Nunito"; 11 | export const DEFAULT_FONT_SIZE: 20; 12 | export const DEFAULT_FONT_WEIGHT: "bold"; 13 | export const DEFAULT_FONT_STYLE: "normal"; 14 | export const DEFAULT_TEXT_DISTANCE: 60; 15 | export const DEFAULT_SPIN_DURATION: 1; 16 | export const DISABLE_INITIAL_ANIMATION: false; 17 | export const WEB_FONTS: string[]; 18 | -------------------------------------------------------------------------------- /dist/strings.js: -------------------------------------------------------------------------------- 1 | export var DEFAULT_BACKGROUND_COLORS = ['darkgrey', 'lightgrey']; 2 | export var DEFAULT_TEXT_COLORS = ['black']; 3 | export var DEFAULT_OUTER_BORDER_COLOR = 'black'; 4 | export var DEFAULT_OUTER_BORDER_WIDTH = 5; 5 | export var DEFAULT_INNER_RADIUS = 0; 6 | export var DEFAULT_INNER_BORDER_COLOR = 'black'; 7 | export var DEFAULT_INNER_BORDER_WIDTH = 0; 8 | export var DEFAULT_RADIUS_LINE_COLOR = 'black'; 9 | export var DEFAULT_RADIUS_LINE_WIDTH = 5; 10 | export var DEFAULT_FONT_FAMILY = 'Nunito'; 11 | export var DEFAULT_FONT_SIZE = 20; 12 | export var DEFAULT_FONT_WEIGHT = 'bold'; 13 | export var DEFAULT_FONT_STYLE = 'normal'; 14 | export var DEFAULT_TEXT_DISTANCE = 60; 15 | export var DEFAULT_SPIN_DURATION = 1.0; 16 | export var DISABLE_INITIAL_ANIMATION = false; 17 | export var WEB_FONTS = [ 18 | 'arial', 19 | 'verdana', 20 | 'tahoma', 21 | 'trebuchet ms', 22 | 'times', 23 | 'garamond', 24 | 'brush script mt', 25 | 'courier new', 26 | 'georgia', 27 | 'helvetica', 28 | 'times new roman', 29 | 'serif', 30 | 'sans-serif', 31 | 'monospace', 32 | 'cursive', 33 | 'fantasy', 34 | ]; 35 | -------------------------------------------------------------------------------- /dist/styles.d.ts: -------------------------------------------------------------------------------- 1 | export const AppContainer: any; 2 | -------------------------------------------------------------------------------- /dist/styles.js: -------------------------------------------------------------------------------- 1 | var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { 2 | if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } 3 | return cooked; 4 | }; 5 | import styled from 'styled-components'; 6 | export var AppContainer = styled.div(templateObject_1 || (templateObject_1 = __makeTemplateObject(["\n width: 100%;\n height: 100vh;\n display: flex;\n flex-direction: column;\n justify-content: center;\n align-items: center;\n position: relative;\n background-color: #2d2d2d;\n overflow: hidden;\n"], ["\n width: 100%;\n height: 100vh;\n display: flex;\n flex-direction: column;\n justify-content: center;\n align-items: center;\n position: relative;\n background-color: #2d2d2d;\n overflow: hidden;\n"]))); 7 | var templateObject_1; 8 | -------------------------------------------------------------------------------- /dist/utils.d.ts: -------------------------------------------------------------------------------- 1 | export declare const getRotationDegrees: (prizeNumber: number, numberOfPrizes: number, randomDif?: boolean) => number; 2 | export declare const clamp: (min: number, max: number, val: number) => number; 3 | export declare const isCustomFont: (font: string) => boolean; 4 | export declare const getQuantity: (prizeMap: number[][]) => number; 5 | export declare const makeClassKey: (length: number) => string; 6 | -------------------------------------------------------------------------------- /dist/utils.js: -------------------------------------------------------------------------------- 1 | import { WEB_FONTS } from './strings'; 2 | export var getRotationDegrees = function (prizeNumber, numberOfPrizes, randomDif) { 3 | if (randomDif === void 0) { randomDif = true; } 4 | var degreesPerPrize = 360 / numberOfPrizes; 5 | var initialRotation = 43 + degreesPerPrize / 2; 6 | var randomDifference = (-1 + Math.random() * 2) * degreesPerPrize * 0.35; 7 | var perfectRotation = degreesPerPrize * (numberOfPrizes - prizeNumber) - initialRotation; 8 | var imperfectRotation = degreesPerPrize * (numberOfPrizes - prizeNumber) - 9 | initialRotation + 10 | randomDifference; 11 | var prizeRotation = randomDif ? imperfectRotation : perfectRotation; 12 | return numberOfPrizes - prizeNumber > numberOfPrizes / 2 13 | ? -360 + prizeRotation 14 | : prizeRotation; 15 | }; 16 | export var clamp = function (min, max, val) { 17 | return Math.min(Math.max(min, +val), max); 18 | }; 19 | export var isCustomFont = function (font) { 20 | return !!font && !WEB_FONTS.includes(font.toLowerCase()); 21 | }; 22 | export var getQuantity = function (prizeMap) { 23 | return prizeMap.slice(-1)[0].slice(-1)[0] + 1; 24 | }; 25 | var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 26 | export var makeClassKey = function (length) { 27 | var result = ''; 28 | var charactersLength = characters.length; 29 | for (var i = 0; i < length; i++) { 30 | result += characters.charAt(Math.floor(Math.random() * charactersLength)); 31 | } 32 | return result; 33 | }; 34 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | Tested on Node v16.10.0 2 | 3 | ## Running the example 4 | 5 | In order to run the example app with hot reloading, you have to follow these steps: 6 | 7 | On ./example: 8 | 9 | 1. `yarn && yarn link-roulette` 10 | (Installs all the required dependencies and links the local roulette files to the example project) 11 | 12 | 2. `yarn watch-roulette` 13 | (Runs webpack watch, required for hot-reloading in the example project on every change done to the local roulette files) 14 | 15 | 3. `yarn start` 16 | (Runs the example app) 17 | 18 | After following these steps, you should see the app running on [http://localhost:3000](http://localhost:3000). Every change to the roulette files should trigger a reload and be visible instantaneously on the app. 19 | 20 | ## Available Scripts 21 | 22 | In the project directory, you can run: 23 | 24 | ### `yarn start` 25 | 26 | Runs the app in the development mode.
27 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 28 | 29 | The page will reload if you make edits.
30 | You will also see any lint errors in the console. 31 | 32 | ### `yarn test` 33 | 34 | Launches the test runner in the interactive watch mode.
35 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 36 | 37 | ### `yarn build` 38 | 39 | Builds the app for production to the `build` folder.
40 | It correctly bundles React in production mode and optimizes the build for the best performance. 41 | 42 | The build is minified and the filenames include the hashes.
43 | Your app is ready to be deployed! 44 | 45 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 46 | 47 | ### `yarn eject` 48 | 49 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 50 | 51 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 52 | 53 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 54 | 55 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 56 | 57 | ### `yarn link-roulette` 58 | 59 | Installs all the required dependencies and links the local roulette files to the example project 60 | 61 | ### `yarn watch-roulette` 62 | 63 | Runs `webpack --watch` on the roulette's root directory. 64 | 65 | This command is required to enable hot-reloading in the example project for every change done to the local roulette files 66 | 67 | ## Learn More 68 | 69 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 70 | 71 | To learn React, check out the [React documentation](https://reactjs.org/). 72 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.3.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "@types/jest": "^24.0.0", 10 | "@types/node": "^12.0.0", 11 | "@types/react": "^16.9.0", 12 | "react": "^18.2.0", 13 | "react-custom-roulette": "../", 14 | "react-dom": "^18.2.0", 15 | "react-scripts": "^4.0.3", 16 | "typescript": "~3.7.2", 17 | "@types/react-dom": "^18.0.11" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test", 23 | "eject": "react-scripts eject", 24 | "link-roulette": "cd .. && yarn && yarn link-react && yarn link && cd example && yarn link react-custom-roulette", 25 | "watch-roulette": "cd .. && yarn start" 26 | }, 27 | "eslintConfig": { 28 | "extends": "react-app" 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/effectussoftware/react-custom-roulette/0049461cb3bf89a754d9c40e167518b6996cf732/example/public/favicon.ico -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /example/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/effectussoftware/react-custom-roulette/0049461cb3bf89a754d9c40e167518b6996cf732/example/public/logo192.png -------------------------------------------------------------------------------- /example/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/effectussoftware/react-custom-roulette/0049461cb3bf89a754d9c40e167518b6996cf732/example/public/logo512.png -------------------------------------------------------------------------------- /example/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /example/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /example/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | /* background-color: #282c34; */ 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | .spin-button { 32 | margin-top: 22px; 33 | background-color: rgb(20, 20, 20); 34 | border: 0; 35 | color: rgb(223, 223, 223); 36 | width: 100px; 37 | height: 35px; 38 | font-size: 18px; 39 | font-weight: 600; 40 | border-radius: 10px; 41 | outline: 0; 42 | transition: 0.1s linear; 43 | cursor: pointer; 44 | letter-spacing: 0.7px; 45 | padding-top: 3px; 46 | } 47 | 48 | .spin-button:active { 49 | padding-top: 5px; 50 | background-color: rgb(41, 41, 41); 51 | } 52 | 53 | @keyframes App-logo-spin { 54 | from { 55 | transform: rotate(0deg); 56 | } 57 | to { 58 | transform: rotate(360deg); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /example/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import './App.css'; 3 | 4 | import { Wheel } from 'react-custom-roulette'; 5 | 6 | const data = [ 7 | { option: 'REACT' }, 8 | { option: 'CUSTOM' }, 9 | { option: 'ROULETTE', style: { textColor: '#f9dd50' } }, 10 | { option: 'WHEEL' }, 11 | { option: 'REACT' }, 12 | { option: 'CUSTOM' }, 13 | { option: 'ROULETTE', style: { textColor: '#70bbe0' } }, 14 | { option: 'WHEEL' }, 15 | ]; 16 | 17 | const backgroundColors = ['#ff8f43', '#70bbe0', '#0b3351', '#f9dd50']; 18 | const textColors = ['#0b3351']; 19 | const outerBorderColor = '#eeeeee'; 20 | const outerBorderWidth = 10; 21 | const innerBorderColor = '#30261a'; 22 | const innerBorderWidth = 0; 23 | const innerRadius = 0; 24 | const radiusLineColor = '#eeeeee'; 25 | const radiusLineWidth = 8; 26 | const fontFamily = 'Nunito'; 27 | const fontWeight = 'bold'; 28 | const fontSize = 20; 29 | const fontStyle = 'normal'; 30 | const textDistance = 60; 31 | const spinDuration = 1.0; 32 | 33 | const App = () => { 34 | const [mustSpin, setMustSpin] = useState(false); 35 | const [prizeNumber, setPrizeNumber] = useState(0); 36 | 37 | const handleSpinClick = () => { 38 | if (!mustSpin) { 39 | const newPrizeNumber = Math.floor(Math.random() * data.length); 40 | setPrizeNumber(newPrizeNumber); 41 | setMustSpin(true); 42 | } 43 | }; 44 | 45 | return ( 46 |
47 |
48 | { 70 | setMustSpin(false); 71 | }} 72 | /> 73 | 76 |
77 |
78 | ); 79 | }; 80 | 81 | export default App; 82 | -------------------------------------------------------------------------------- /example/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /example/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import * as ReactDOMClient from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | const container = document.getElementById('root') 8 | 9 | if (container === null) throw new Error('Missing root') 10 | 11 | const root = ReactDOMClient.createRoot(container) 12 | 13 | root.render( 14 | 15 | 16 | 17 | ); 18 | 19 | // If you want your app to work offline and load faster, you can change 20 | // unregister() to register() below. Note this comes with some pitfalls. 21 | // Learn more about service workers: https://bit.ly/CRA-PWA 22 | serviceWorker.unregister(); 23 | -------------------------------------------------------------------------------- /example/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /example/src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | process.env.PUBLIC_URL, 33 | window.location.href 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won't work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener('load', () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let's check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config?: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then(registration => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker == null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === 'installed') { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log( 81 | 'New content is available and will be used when all ' + 82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 83 | ); 84 | 85 | // Execute callback 86 | if (config && config.onUpdate) { 87 | config.onUpdate(registration); 88 | } 89 | } else { 90 | // At this point, everything has been precached. 91 | // It's the perfect time to display a 92 | // "Content is cached for offline use." message. 93 | console.log('Content is cached for offline use.'); 94 | 95 | // Execute callback 96 | if (config && config.onSuccess) { 97 | config.onSuccess(registration); 98 | } 99 | } 100 | } 101 | }; 102 | }; 103 | }) 104 | .catch(error => { 105 | console.error('Error during service worker registration:', error); 106 | }); 107 | } 108 | 109 | function checkValidServiceWorker(swUrl: string, config?: Config) { 110 | // Check if the service worker can be found. If it can't reload the page. 111 | fetch(swUrl, { 112 | headers: { 'Service-Worker': 'script' } 113 | }) 114 | .then(response => { 115 | // Ensure service worker exists, and that we really are getting a JS file. 116 | const contentType = response.headers.get('content-type'); 117 | if ( 118 | response.status === 404 || 119 | (contentType != null && contentType.indexOf('javascript') === -1) 120 | ) { 121 | // No service worker found. Probably a different app. Reload the page. 122 | navigator.serviceWorker.ready.then(registration => { 123 | registration.unregister().then(() => { 124 | window.location.reload(); 125 | }); 126 | }); 127 | } else { 128 | // Service worker found. Proceed as normal. 129 | registerValidSW(swUrl, config); 130 | } 131 | }) 132 | .catch(() => { 133 | console.log( 134 | 'No internet connection found. App is running in offline mode.' 135 | ); 136 | }); 137 | } 138 | 139 | export function unregister() { 140 | if ('serviceWorker' in navigator) { 141 | navigator.serviceWorker.ready 142 | .then(registration => { 143 | registration.unregister(); 144 | }) 145 | .catch(error => { 146 | console.error(error.message); 147 | }); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /example/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react", 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-custom-roulette", 3 | "version": "1.4.1", 4 | "description": "Customizable React roulette wheel with spinning animation", 5 | "main": "./dist/bundle.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/effectussoftware/react-custom-roulette" 9 | }, 10 | "keywords": [ 11 | "react", 12 | "custom", 13 | "roulette", 14 | "spinning", 15 | "wheel", 16 | "fortune", 17 | "prize" 18 | ], 19 | "license": "MIT", 20 | "types": "./dist/index.d.ts", 21 | "private": false, 22 | "peerDependencies": { 23 | "react": "^18.2.0", 24 | "react-dom": "^18.2.0" 25 | }, 26 | "devDependencies": { 27 | "@testing-library/jest-dom": "^5.9.0", 28 | "@testing-library/react": "^10.0.6", 29 | "@testing-library/user-event": "^11.0.0", 30 | "@types/jest": "^25.2.3", 31 | "@types/node": "^14.0.9", 32 | "@types/react": "^18.0.28", 33 | "@types/react-dom": "^18.0.11", 34 | "@types/webfontloader": "^1.6.34", 35 | "@typescript-eslint/eslint-plugin": "^4.16.1", 36 | "@typescript-eslint/parser": "^4.16.1", 37 | "babel-cli": "^6.26.0", 38 | "babel-core": "^6.26.3", 39 | "babel-loader": "8.1.0", 40 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 41 | "babel-plugin-transform-react-jsx": "^6.24.1", 42 | "babel-preset-env": "^1.7.0", 43 | "css-loader": "^3.5.3", 44 | "eslint": "^7.21.0", 45 | "eslint-config-airbnb-typescript-prettier": "^2.1.1", 46 | "eslint-plugin-react": "^7.22.0", 47 | "husky": "^4.2.5", 48 | "jest-canvas-mock": "^2.2.0", 49 | "prettier": "^2.0.5", 50 | "react-scripts": "^4.0.3", 51 | "styled-components": "^5.1.1", 52 | "ts-loader": "8.0.7", 53 | "typescript": "^4.6.2", 54 | "url-loader": "^4.1.0", 55 | "webfontloader": "^1.6.28", 56 | "webpack": "4.44.2", 57 | "webpack-cli": "^3.3.11", 58 | "webpack-dev-server": "3.11.1" 59 | }, 60 | "scripts": { 61 | "start-server": "react-scripts start", 62 | "start": "webpack --watch", 63 | "build": "webpack && tsc && cp -R ./src/assets ./dist", 64 | "test": "react-scripts test", 65 | "coverage": "CI=true npm test -- --env=jsdom --coverage", 66 | "eject": "react-scripts eject", 67 | "format": "prettier --write src/**/*.{js,ts,tsx}", 68 | "lint": "eslint src/**/*.{js,ts,tsx}", 69 | "link-react": "cd example/node_modules/react && yarn link && cd ../react-dom && yarn link && cd ../../.. && yarn link react react-dom" 70 | }, 71 | "husky": { 72 | "hooks": { 73 | "pre-commit": "yarn format && yarn lint" 74 | } 75 | }, 76 | "jest": { 77 | "coveragePathIgnorePatterns": [ 78 | "src/serviceWorker.ts" 79 | ] 80 | }, 81 | "eslintConfig": { 82 | "extends": "react-app" 83 | }, 84 | "browserslist": { 85 | "production": [ 86 | ">0.2%", 87 | "not dead", 88 | "not op_mini all" 89 | ], 90 | "development": [ 91 | "last 1 chrome version", 92 | "last 1 firefox version", 93 | "last 1 safari version" 94 | ] 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/effectussoftware/react-custom-roulette/0049461cb3bf89a754d9c40e167518b6996cf732/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Wheel of Fortune 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/effectussoftware/react-custom-roulette/0049461cb3bf89a754d9c40e167518b6996cf732/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/effectussoftware/react-custom-roulette/0049461cb3bf89a754d9c40e167518b6996cf732/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/assets/roulette-pointer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/effectussoftware/react-custom-roulette/0049461cb3bf89a754d9c40e167518b6996cf732/src/assets/roulette-pointer.png -------------------------------------------------------------------------------- /src/components/Wheel/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react'; 2 | import WebFont from 'webfontloader'; 3 | 4 | import { 5 | getQuantity, 6 | getRotationDegrees, 7 | isCustomFont, 8 | makeClassKey, 9 | } from '../../utils'; 10 | import { roulettePointer } from '../common/images'; 11 | import { 12 | RotationContainer, 13 | RouletteContainer, 14 | RoulettePointerImage, 15 | } from './styles'; 16 | import { 17 | DEFAULT_BACKGROUND_COLORS, 18 | DEFAULT_FONT_FAMILY, 19 | DEFAULT_FONT_SIZE, 20 | DEFAULT_FONT_STYLE, 21 | DEFAULT_FONT_WEIGHT, 22 | DEFAULT_INNER_BORDER_COLOR, 23 | DEFAULT_INNER_BORDER_WIDTH, 24 | DEFAULT_INNER_RADIUS, 25 | DEFAULT_OUTER_BORDER_COLOR, 26 | DEFAULT_OUTER_BORDER_WIDTH, 27 | DEFAULT_RADIUS_LINE_COLOR, 28 | DEFAULT_RADIUS_LINE_WIDTH, 29 | DEFAULT_SPIN_DURATION, 30 | DEFAULT_TEXT_COLORS, 31 | DEFAULT_TEXT_DISTANCE, 32 | WEB_FONTS, 33 | DISABLE_INITIAL_ANIMATION, 34 | } from '../../strings'; 35 | import { PointerProps, WheelData } from './types'; 36 | import WheelCanvas from '../WheelCanvas'; 37 | 38 | interface Props { 39 | mustStartSpinning: boolean; 40 | prizeNumber: number; 41 | data: WheelData[]; 42 | onStopSpinning?: () => any; 43 | backgroundColors?: string[]; 44 | textColors?: string[]; 45 | outerBorderColor?: string; 46 | outerBorderWidth?: number; 47 | innerRadius?: number; 48 | innerBorderColor?: string; 49 | innerBorderWidth?: number; 50 | radiusLineColor?: string; 51 | radiusLineWidth?: number; 52 | fontFamily?: string; 53 | fontSize?: number; 54 | fontWeight?: number | string; 55 | fontStyle?: string; 56 | perpendicularText?: boolean; 57 | textDistance?: number; 58 | spinDuration?: number; 59 | startingOptionIndex?: number; 60 | pointerProps?: PointerProps; 61 | disableInitialAnimation?: boolean; 62 | } 63 | 64 | const STARTED_SPINNING = 'started-spinning'; 65 | 66 | const START_SPINNING_TIME = 2600; 67 | const CONTINUE_SPINNING_TIME = 750; 68 | const STOP_SPINNING_TIME = 8000; 69 | 70 | export const Wheel = ({ 71 | mustStartSpinning, 72 | prizeNumber, 73 | data, 74 | onStopSpinning = () => null, 75 | backgroundColors = DEFAULT_BACKGROUND_COLORS, 76 | textColors = DEFAULT_TEXT_COLORS, 77 | outerBorderColor = DEFAULT_OUTER_BORDER_COLOR, 78 | outerBorderWidth = DEFAULT_OUTER_BORDER_WIDTH, 79 | innerRadius = DEFAULT_INNER_RADIUS, 80 | innerBorderColor = DEFAULT_INNER_BORDER_COLOR, 81 | innerBorderWidth = DEFAULT_INNER_BORDER_WIDTH, 82 | radiusLineColor = DEFAULT_RADIUS_LINE_COLOR, 83 | radiusLineWidth = DEFAULT_RADIUS_LINE_WIDTH, 84 | fontFamily = WEB_FONTS[0], 85 | fontSize = DEFAULT_FONT_SIZE, 86 | fontWeight = DEFAULT_FONT_WEIGHT, 87 | fontStyle = DEFAULT_FONT_STYLE, 88 | perpendicularText = false, 89 | textDistance = DEFAULT_TEXT_DISTANCE, 90 | spinDuration = DEFAULT_SPIN_DURATION, 91 | startingOptionIndex = -1, 92 | pointerProps = {}, 93 | disableInitialAnimation = DISABLE_INITIAL_ANIMATION, 94 | }: Props): JSX.Element | null => { 95 | const [wheelData, setWheelData] = useState([...data]); 96 | const [prizeMap, setPrizeMap] = useState([[0]]); 97 | const [startRotationDegrees, setStartRotationDegrees] = useState(0); 98 | const [finalRotationDegrees, setFinalRotationDegrees] = useState(0); 99 | const [hasStartedSpinning, setHasStartedSpinning] = useState(false); 100 | const [hasStoppedSpinning, setHasStoppedSpinning] = useState(false); 101 | const [isCurrentlySpinning, setIsCurrentlySpinning] = useState(false); 102 | const [isDataUpdated, setIsDataUpdated] = useState(false); 103 | const [rouletteUpdater, setRouletteUpdater] = useState(false); 104 | const [loadedImagesCounter, setLoadedImagesCounter] = useState(0); 105 | const [totalImages, setTotalImages] = useState(0); 106 | const [isFontLoaded, setIsFontLoaded] = useState(false); 107 | const mustStopSpinning = useRef(false); 108 | 109 | const classKey = makeClassKey(5); 110 | 111 | const normalizedSpinDuration = Math.max(0.01, spinDuration); 112 | 113 | const startSpinningTime = START_SPINNING_TIME * normalizedSpinDuration; 114 | const continueSpinningTime = CONTINUE_SPINNING_TIME * normalizedSpinDuration; 115 | const stopSpinningTime = STOP_SPINNING_TIME * normalizedSpinDuration; 116 | 117 | const totalSpinningTime = 118 | startSpinningTime + continueSpinningTime + stopSpinningTime; 119 | 120 | useEffect(() => { 121 | let initialMapNum = 0; 122 | const auxPrizeMap: number[][] = []; 123 | const dataLength = data?.length || 0; 124 | const wheelDataAux = [{ option: '', optionSize: 1 }] as WheelData[]; 125 | const fontsToFetch = isCustomFont(fontFamily?.trim()) ? [fontFamily] : []; 126 | 127 | for (let i = 0; i < dataLength; i++) { 128 | let fontArray = data[i]?.style?.fontFamily?.split(',') || []; 129 | fontArray = fontArray.map(font => font.trim()).filter(isCustomFont); 130 | fontsToFetch.push(...fontArray); 131 | 132 | wheelDataAux[i] = { 133 | ...data[i], 134 | style: { 135 | backgroundColor: 136 | data[i].style?.backgroundColor || 137 | backgroundColors?.[i % backgroundColors?.length] || 138 | DEFAULT_BACKGROUND_COLORS[0], 139 | fontFamily: 140 | data[i].style?.fontFamily || fontFamily || DEFAULT_FONT_FAMILY, 141 | fontSize: data[i].style?.fontSize || fontSize || DEFAULT_FONT_SIZE, 142 | fontWeight: 143 | data[i].style?.fontWeight || fontWeight || DEFAULT_FONT_WEIGHT, 144 | fontStyle: 145 | data[i].style?.fontStyle || fontStyle || DEFAULT_FONT_STYLE, 146 | textColor: 147 | data[i].style?.textColor || 148 | textColors?.[i % textColors?.length] || 149 | DEFAULT_TEXT_COLORS[0], 150 | }, 151 | }; 152 | auxPrizeMap.push([]); 153 | for (let j = 0; j < (wheelDataAux[i].optionSize || 1); j++) { 154 | auxPrizeMap[i][j] = initialMapNum++; 155 | } 156 | if (data[i].image) { 157 | setTotalImages(prevCounter => prevCounter + 1); 158 | 159 | const img = new Image(); 160 | img.src = data[i].image?.uri || ''; 161 | img.onload = () => { 162 | img.height = 200 * (data[i].image?.sizeMultiplier || 1); 163 | img.width = (img.naturalWidth / img.naturalHeight) * img.height; 164 | wheelDataAux[i].image = { 165 | uri: data[i].image?.uri || '', 166 | offsetX: data[i].image?.offsetX || 0, 167 | offsetY: data[i].image?.offsetY || 0, 168 | landscape: data[i].image?.landscape || false, 169 | sizeMultiplier: data[i].image?.sizeMultiplier || 1, 170 | _imageHTML: img, 171 | }; 172 | setLoadedImagesCounter(prevCounter => prevCounter + 1); 173 | setRouletteUpdater(prevState => !prevState); 174 | }; 175 | } 176 | } 177 | 178 | if (fontsToFetch?.length > 0) { 179 | try { 180 | WebFont.load({ 181 | google: { 182 | families: Array.from(new Set(fontsToFetch.filter(font => !!font))), 183 | }, 184 | timeout: 1000, 185 | fontactive() { 186 | setRouletteUpdater(!rouletteUpdater); 187 | }, 188 | active() { 189 | setIsFontLoaded(true); 190 | setRouletteUpdater(!rouletteUpdater); 191 | }, 192 | }); 193 | } catch (err) { 194 | console.log('Error loading webfonts:', err); 195 | } 196 | } else { 197 | setIsFontLoaded(true); 198 | } 199 | 200 | setWheelData([...wheelDataAux]); 201 | setPrizeMap(auxPrizeMap); 202 | setStartingOption(startingOptionIndex, auxPrizeMap); 203 | setIsDataUpdated(true); 204 | }, [data, backgroundColors, textColors]); 205 | 206 | useEffect(() => { 207 | if (mustStartSpinning && !isCurrentlySpinning) { 208 | setIsCurrentlySpinning(true); 209 | startSpinning(); 210 | const selectedPrize = 211 | prizeMap[prizeNumber][ 212 | Math.floor(Math.random() * prizeMap[prizeNumber]?.length) 213 | ]; 214 | const finalRotationDegreesCalculated = getRotationDegrees( 215 | selectedPrize, 216 | getQuantity(prizeMap) 217 | ); 218 | setFinalRotationDegrees(finalRotationDegreesCalculated); 219 | } 220 | }, [mustStartSpinning]); 221 | 222 | useEffect(() => { 223 | if (hasStoppedSpinning) { 224 | setIsCurrentlySpinning(false); 225 | setStartRotationDegrees(finalRotationDegrees); 226 | } 227 | }, [hasStoppedSpinning]); 228 | 229 | const startSpinning = () => { 230 | setHasStartedSpinning(true); 231 | setHasStoppedSpinning(false); 232 | mustStopSpinning.current = true; 233 | setTimeout(() => { 234 | if (mustStopSpinning.current) { 235 | mustStopSpinning.current = false; 236 | setHasStartedSpinning(false); 237 | setHasStoppedSpinning(true); 238 | onStopSpinning(); 239 | } 240 | }, totalSpinningTime); 241 | }; 242 | 243 | const setStartingOption = (optionIndex: number, optionMap: number[][]) => { 244 | if (startingOptionIndex >= 0) { 245 | const idx = Math.floor(optionIndex) % optionMap?.length; 246 | const startingOption = 247 | optionMap[idx][Math.floor(optionMap[idx]?.length / 2)]; 248 | setStartRotationDegrees( 249 | getRotationDegrees(startingOption, getQuantity(optionMap), false) 250 | ); 251 | } 252 | }; 253 | 254 | const getRouletteClass = () => { 255 | if (hasStartedSpinning) { 256 | return STARTED_SPINNING; 257 | } 258 | return ''; 259 | }; 260 | 261 | if (!isDataUpdated) { 262 | return null; 263 | } 264 | 265 | return ( 266 | 0 && loadedImagesCounter !== totalImages) 270 | ? { visibility: 'hidden' } 271 | : {} 272 | } 273 | > 274 | 284 | 304 | 305 | 310 | 311 | ); 312 | }; 313 | -------------------------------------------------------------------------------- /src/components/Wheel/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { NonDraggableImage } from '../common/styledComponents'; 4 | 5 | export const RouletteContainer = styled.div` 6 | position: relative; 7 | width: 80vw; 8 | max-width: 445px; 9 | height: 80vw; 10 | max-height: 445px; 11 | object-fit: contain; 12 | flex-shrink: 0; 13 | z-index: 5; 14 | pointer-events: none; 15 | `; 16 | 17 | export const RotationContainer = styled.div` 18 | position: absolute; 19 | width: 100%; 20 | left: 0px; 21 | right: 0px; 22 | top: 0px; 23 | bottom: 0px; 24 | display: flex; 25 | justify-content: center; 26 | align-items: center; 27 | transform: rotate(${props => props.startRotationDegrees}deg); 28 | 29 | &.started-spinning { 30 | animation: spin-${({ classKey }) => classKey} ${({ startSpinningTime }) => 31 | startSpinningTime / 1000}s cubic-bezier( 32 | 0.71, 33 | ${props => (props.disableInitialAnimation ? 0 : -0.29)}, 34 | 0.96, 35 | 0.9 36 | ) 0s 1 normal forwards running, 37 | continueSpin-${({ classKey }) => classKey} ${({ continueSpinningTime }) => 38 | continueSpinningTime / 1000}s linear ${({ startSpinningTime }) => 39 | startSpinningTime / 1000}s 1 normal forwards running, 40 | stopSpin-${({ classKey }) => classKey} ${({ stopSpinningTime }) => 41 | stopSpinningTime / 1000}s cubic-bezier(0, 0, 0.35, 1.02) ${({ 42 | startSpinningTime, 43 | continueSpinningTime, 44 | }) => (startSpinningTime + continueSpinningTime) / 1000}s 1 normal forwards 45 | running; 46 | } 47 | 48 | @keyframes spin-${({ classKey }) => classKey} { 49 | from { 50 | transform: rotate(${props => props.startRotationDegrees}deg); 51 | } 52 | to { 53 | transform: rotate(${props => props.startRotationDegrees + 360}deg); 54 | } 55 | } 56 | @keyframes continueSpin-${({ classKey }) => classKey} { 57 | from { 58 | transform: rotate(${props => props.startRotationDegrees}deg); 59 | } 60 | to { 61 | transform: rotate(${props => props.startRotationDegrees + 360}deg); 62 | } 63 | } 64 | @keyframes stopSpin-${({ classKey }) => classKey} { 65 | from { 66 | transform: rotate(${props => props.startRotationDegrees}deg); 67 | } 68 | to { 69 | transform: rotate(${props => 1440 + props.finalRotationDegrees}deg); 70 | } 71 | } 72 | `; 73 | 74 | export const RoulettePointerImage = styled(NonDraggableImage)` 75 | position: absolute; 76 | z-index: 5; 77 | width: 17%; 78 | right: 6px; 79 | top: 15px; 80 | `; 81 | -------------------------------------------------------------------------------- /src/components/Wheel/types.ts: -------------------------------------------------------------------------------- 1 | interface ImagePropsLocal extends ImageProps { 2 | _imageHTML?: HTMLImageElement; 3 | } 4 | 5 | export interface WheelData { 6 | image?: ImagePropsLocal; 7 | option?: string; 8 | style?: StyleType; 9 | optionSize?: number; 10 | } 11 | 12 | export interface StyleType { 13 | backgroundColor?: string; 14 | textColor?: string; 15 | fontFamily?: string; 16 | fontSize?: number; 17 | fontWeight?: number | string; 18 | fontStyle?: string; 19 | } 20 | 21 | export interface PointerProps { 22 | src?: string; 23 | style?: React.CSSProperties; 24 | } 25 | 26 | export interface ImageProps { 27 | uri: string; 28 | offsetX?: number; 29 | offsetY?: number; 30 | sizeMultiplier?: number; 31 | landscape?: boolean; 32 | } 33 | -------------------------------------------------------------------------------- /src/components/WheelCanvas/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { createRef, RefObject, useEffect } from 'react'; 2 | 3 | import { WheelCanvasStyle } from './styles'; 4 | import { WheelData } from '../Wheel/types'; 5 | import { clamp, getQuantity } from '../../utils'; 6 | 7 | interface WheelCanvasProps extends DrawWheelProps { 8 | width: string; 9 | height: string; 10 | data: WheelData[]; 11 | } 12 | 13 | interface DrawWheelProps { 14 | outerBorderColor: string; 15 | outerBorderWidth: number; 16 | innerRadius: number; 17 | innerBorderColor: string; 18 | innerBorderWidth: number; 19 | radiusLineColor: string; 20 | radiusLineWidth: number; 21 | fontFamily: string; 22 | fontWeight: number | string; 23 | fontSize: number; 24 | fontStyle: string; 25 | perpendicularText: boolean; 26 | prizeMap: number[][]; 27 | rouletteUpdater: boolean; 28 | textDistance: number; 29 | } 30 | 31 | const drawRadialBorder = ( 32 | ctx: CanvasRenderingContext2D, 33 | centerX: number, 34 | centerY: number, 35 | insideRadius: number, 36 | outsideRadius: number, 37 | angle: number 38 | ) => { 39 | ctx.beginPath(); 40 | ctx.moveTo( 41 | centerX + (insideRadius + 1) * Math.cos(angle), 42 | centerY + (insideRadius + 1) * Math.sin(angle) 43 | ); 44 | ctx.lineTo( 45 | centerX + (outsideRadius - 1) * Math.cos(angle), 46 | centerY + (outsideRadius - 1) * Math.sin(angle) 47 | ); 48 | ctx.closePath(); 49 | ctx.stroke(); 50 | }; 51 | 52 | const drawWheel = ( 53 | canvasRef: RefObject, 54 | data: WheelData[], 55 | drawWheelProps: DrawWheelProps 56 | ) => { 57 | /* eslint-disable prefer-const */ 58 | let { 59 | outerBorderColor, 60 | outerBorderWidth, 61 | innerRadius, 62 | innerBorderColor, 63 | innerBorderWidth, 64 | radiusLineColor, 65 | radiusLineWidth, 66 | fontFamily, 67 | fontWeight, 68 | fontSize, 69 | fontStyle, 70 | perpendicularText, 71 | prizeMap, 72 | textDistance, 73 | } = drawWheelProps; 74 | 75 | const QUANTITY = getQuantity(prizeMap); 76 | 77 | outerBorderWidth *= 2; 78 | innerBorderWidth *= 2; 79 | radiusLineWidth *= 2; 80 | 81 | const canvas = canvasRef.current; 82 | if (canvas?.getContext('2d')) { 83 | const ctx = canvas.getContext('2d') as CanvasRenderingContext2D; 84 | ctx.clearRect(0, 0, 500, 500); 85 | ctx.strokeStyle = 'transparent'; 86 | ctx.lineWidth = 0; 87 | 88 | let startAngle = 0; 89 | const outsideRadius = canvas.width / 2 - 10; 90 | 91 | const clampedContentDistance = clamp(0, 100, textDistance); 92 | const contentRadius = (outsideRadius * clampedContentDistance) / 100; 93 | 94 | const clampedInsideRadius = clamp(0, 100, innerRadius); 95 | const insideRadius = (outsideRadius * clampedInsideRadius) / 100; 96 | 97 | const centerX = canvas.width / 2; 98 | const centerY = canvas.height / 2; 99 | 100 | for (let i = 0; i < data.length; i++) { 101 | const { optionSize, style } = data[i]; 102 | 103 | const arc = 104 | (optionSize && (optionSize * (2 * Math.PI)) / QUANTITY) || 105 | (2 * Math.PI) / QUANTITY; 106 | const endAngle = startAngle + arc; 107 | 108 | ctx.fillStyle = (style && style.backgroundColor) as string; 109 | 110 | ctx.beginPath(); 111 | ctx.arc(centerX, centerY, outsideRadius, startAngle, endAngle, false); 112 | ctx.arc(centerX, centerY, insideRadius, endAngle, startAngle, true); 113 | ctx.stroke(); 114 | ctx.fill(); 115 | ctx.save(); 116 | 117 | // WHEEL RADIUS LINES 118 | ctx.strokeStyle = radiusLineWidth <= 0 ? 'transparent' : radiusLineColor; 119 | ctx.lineWidth = radiusLineWidth; 120 | drawRadialBorder( 121 | ctx, 122 | centerX, 123 | centerY, 124 | insideRadius, 125 | outsideRadius, 126 | startAngle 127 | ); 128 | if (i === data.length - 1) { 129 | drawRadialBorder( 130 | ctx, 131 | centerX, 132 | centerY, 133 | insideRadius, 134 | outsideRadius, 135 | endAngle 136 | ); 137 | } 138 | 139 | // WHEEL OUTER BORDER 140 | ctx.strokeStyle = 141 | outerBorderWidth <= 0 ? 'transparent' : outerBorderColor; 142 | ctx.lineWidth = outerBorderWidth; 143 | ctx.beginPath(); 144 | ctx.arc( 145 | centerX, 146 | centerY, 147 | outsideRadius - ctx.lineWidth / 2, 148 | 0, 149 | 2 * Math.PI 150 | ); 151 | ctx.closePath(); 152 | ctx.stroke(); 153 | 154 | // WHEEL INNER BORDER 155 | ctx.strokeStyle = 156 | innerBorderWidth <= 0 ? 'transparent' : innerBorderColor; 157 | ctx.lineWidth = innerBorderWidth; 158 | ctx.beginPath(); 159 | ctx.arc( 160 | centerX, 161 | centerY, 162 | insideRadius + ctx.lineWidth / 2 - 1, 163 | 0, 164 | 2 * Math.PI 165 | ); 166 | ctx.closePath(); 167 | ctx.stroke(); 168 | 169 | // CONTENT FILL 170 | ctx.translate( 171 | centerX + Math.cos(startAngle + arc / 2) * contentRadius, 172 | centerY + Math.sin(startAngle + arc / 2) * contentRadius 173 | ); 174 | let contentRotationAngle = startAngle + arc / 2; 175 | 176 | if (data[i].image) { 177 | // CASE IMAGE 178 | contentRotationAngle += 179 | data[i].image && !data[i].image?.landscape ? Math.PI / 2 : 0; 180 | ctx.rotate(contentRotationAngle); 181 | 182 | const img = data[i].image?._imageHTML || new Image(); 183 | ctx.drawImage( 184 | img, 185 | (img.width + (data[i].image?.offsetX || 0)) / -2, 186 | -( 187 | img.height - 188 | (data[i].image?.landscape ? 0 : 90) + // offsetY correction for non landscape images 189 | (data[i].image?.offsetY || 0) 190 | ) / 2, 191 | img.width, 192 | img.height 193 | ); 194 | } else { 195 | // CASE TEXT 196 | contentRotationAngle += perpendicularText ? Math.PI / 2 : 0; 197 | ctx.rotate(contentRotationAngle); 198 | 199 | const text = data[i].option; 200 | ctx.font = `${style?.fontStyle || fontStyle} ${ 201 | style?.fontWeight || fontWeight 202 | } ${(style?.fontSize || fontSize) * 2}px ${ 203 | style?.fontFamily || fontFamily 204 | }, Helvetica, Arial`; 205 | ctx.fillStyle = (style && style.textColor) as string; 206 | ctx.fillText( 207 | text || '', 208 | -ctx.measureText(text || '').width / 2, 209 | fontSize / 2.7 210 | ); 211 | } 212 | 213 | ctx.restore(); 214 | 215 | startAngle = endAngle; 216 | } 217 | } 218 | }; 219 | 220 | const WheelCanvas = ({ 221 | width, 222 | height, 223 | data, 224 | outerBorderColor, 225 | outerBorderWidth, 226 | innerRadius, 227 | innerBorderColor, 228 | innerBorderWidth, 229 | radiusLineColor, 230 | radiusLineWidth, 231 | fontFamily, 232 | fontWeight, 233 | fontSize, 234 | fontStyle, 235 | perpendicularText, 236 | prizeMap, 237 | rouletteUpdater, 238 | textDistance, 239 | }: WheelCanvasProps): JSX.Element => { 240 | const canvasRef = createRef(); 241 | const drawWheelProps = { 242 | outerBorderColor, 243 | outerBorderWidth, 244 | innerRadius, 245 | innerBorderColor, 246 | innerBorderWidth, 247 | radiusLineColor, 248 | radiusLineWidth, 249 | fontFamily, 250 | fontWeight, 251 | fontSize, 252 | fontStyle, 253 | perpendicularText, 254 | prizeMap, 255 | rouletteUpdater, 256 | textDistance, 257 | }; 258 | 259 | useEffect(() => { 260 | drawWheel(canvasRef, data, drawWheelProps); 261 | }, [canvasRef, data, drawWheelProps, rouletteUpdater]); 262 | 263 | return ; 264 | }; 265 | 266 | export default WheelCanvas; 267 | -------------------------------------------------------------------------------- /src/components/WheelCanvas/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const WheelCanvasStyle = styled.canvas` 4 | width: 98%; 5 | height: 98%; 6 | `; 7 | -------------------------------------------------------------------------------- /src/components/common/images.js: -------------------------------------------------------------------------------- 1 | // IMAGES 2 | 3 | import Icon from '../../assets/roulette-pointer.png'; 4 | 5 | const roulettePointer = new Image(); 6 | roulettePointer.src = Icon; 7 | 8 | export { roulettePointer }; 9 | -------------------------------------------------------------------------------- /src/components/common/styledComponents.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const NonDraggableImage = styled.img` 4 | -webkit-user-drag: none; 5 | -khtml-user-drag: none; 6 | -moz-user-drag: none; 7 | -o-user-drag: none; 8 | user-drag: none; 9 | `; 10 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { act } from 'react-dom/test-utils'; 4 | 5 | import { Wheel } from '.'; 6 | 7 | // test('renders Wheel component', () => { 8 | const data = [{ option: '0' }]; 9 | const prizeNumber = 0; 10 | const mustStartSpinning = false; 11 | 12 | const backgroundColors = ['#3e3e3e', '#df3428']; 13 | const textColors = ['white']; 14 | const outerBorderColor = '#d8a35a'; 15 | const outerBorderWidth = 8; 16 | const innerBorderColor = '#d8a35a'; 17 | const innerBorderWidth = 17; 18 | const innerRadius = 40; 19 | const radiusLineColor = '#dddddd'; 20 | const radiusLineWidth = 3; 21 | const fontSize = 20; 22 | const textDistance = 86; 23 | const onStopSpinning = () => null; 24 | 25 | jest.useFakeTimers(); 26 | 27 | let container: HTMLDivElement; 28 | 29 | beforeEach(() => { 30 | container = document.createElement('div'); 31 | document.body.appendChild(container); 32 | }); 33 | 34 | afterEach(() => { 35 | document.body.removeChild(container); 36 | container = (null as unknown) as HTMLDivElement; 37 | }); 38 | 39 | describe('Render Wheel', () => { 40 | it('required props only', () => { 41 | ReactDOM.render( 42 | , 47 | container 48 | ); 49 | }); 50 | 51 | it('innerBorderWidth = 0', () => { 52 | ReactDOM.render( 53 | , 59 | container 60 | ); 61 | }); 62 | 63 | it('outerBorderWidth = 0', () => { 64 | ReactDOM.render( 65 | , 71 | container 72 | ); 73 | }); 74 | 75 | it('radiusLineWidth = 0', () => { 76 | ReactDOM.render( 77 | , 83 | container 84 | ); 85 | }); 86 | 87 | it('all props defined', () => { 88 | ReactDOM.render( 89 | , 107 | container 108 | ); 109 | }); 110 | 111 | it('render spin', () => { 112 | act(() => { 113 | ReactDOM.render( 114 | , 115 | container 116 | ); 117 | jest.runOnlyPendingTimers(); 118 | }); 119 | }); 120 | 121 | it('render callback trigger', () => { 122 | let hasBeenCalled = false; 123 | 124 | act(() => { 125 | ReactDOM.render( 126 | { 131 | hasBeenCalled = true; 132 | return null; 133 | }} 134 | />, 135 | container 136 | ); 137 | 138 | expect(hasBeenCalled).not.toBe(true); 139 | jest.runAllTimers(); 140 | }); 141 | expect(hasBeenCalled).toBe(true); 142 | }); 143 | }); 144 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { WheelData } from './components/Wheel/types'; 2 | 3 | export { Wheel } from './components/Wheel'; 4 | 5 | export type WheelDataType = WheelData; 6 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | // / 2 | -------------------------------------------------------------------------------- /src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // This optional code is used to register a service worker. 3 | // register() is not called by default. 4 | 5 | // This lets the app load faster on subsequent visits in production, and gives 6 | // it offline capabilities. However, it also means that developers (and users) 7 | // will only see deployed updates on subsequent visits to a page, after all the 8 | // existing tabs open on the page have been closed, since previously cached 9 | // resources are updated in the background. 10 | 11 | // To learn more about the benefits of this model and instructions on how to 12 | // opt-in, read https://bit.ly/CRA-PWA 13 | 14 | const isLocalhost = Boolean( 15 | window.location.hostname === 'localhost' || 16 | // [::1] is the IPv6 localhost address. 17 | window.location.hostname === '[::1]' || 18 | // 127.0.0.0/8 are considered localhost for IPv4. 19 | window.location.hostname.match( 20 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 21 | ) 22 | ); 23 | 24 | type Config = { 25 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 26 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 27 | }; 28 | 29 | export function register(config?: Config) { 30 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 31 | // The URL constructor is available in all browsers that support SW. 32 | const publicUrl = new URL( 33 | process.env.PUBLIC_URL as string, 34 | window.location.href 35 | ); 36 | if (publicUrl.origin !== window.location.origin) { 37 | // Our service worker won't work if PUBLIC_URL is on a different origin 38 | // from what our page is served on. This might happen if a CDN is used to 39 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 40 | return; 41 | } 42 | 43 | window.addEventListener('load', () => { 44 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 45 | 46 | if (isLocalhost) { 47 | // This is running on localhost. Let's check if a service worker still exists or not. 48 | checkValidServiceWorker(swUrl, config); 49 | 50 | // Add some additional logging to localhost, pointing developers to the 51 | // service worker/PWA documentation. 52 | navigator.serviceWorker.ready.then(() => { 53 | console.log( 54 | 'This web app is being served cache-first by a service ' + 55 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 56 | ); 57 | }); 58 | } else { 59 | // Is not localhost. Just register service worker 60 | registerValidSW(swUrl, config); 61 | } 62 | }); 63 | } 64 | } 65 | 66 | function registerValidSW(swUrl: string, config?: Config) { 67 | navigator.serviceWorker 68 | .register(swUrl) 69 | .then(registration => { 70 | registration.onupdatefound = () => { 71 | const installingWorker = registration.installing; 72 | if (installingWorker == null) { 73 | return; 74 | } 75 | installingWorker.onstatechange = () => { 76 | if (installingWorker.state === 'installed') { 77 | if (navigator.serviceWorker.controller) { 78 | // At this point, the updated precached content has been fetched, 79 | // but the previous service worker will still serve the older 80 | // content until all client tabs are closed. 81 | console.log( 82 | 'New content is available and will be used when all ' + 83 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 84 | ); 85 | 86 | // Execute callback 87 | if (config && config.onUpdate) { 88 | config.onUpdate(registration); 89 | } 90 | } else { 91 | // At this point, everything has been precached. 92 | // It's the perfect time to display a 93 | // "Content is cached for offline use." message. 94 | console.log('Content is cached for offline use.'); 95 | 96 | // Execute callback 97 | if (config && config.onSuccess) { 98 | config.onSuccess(registration); 99 | } 100 | } 101 | } 102 | }; 103 | }; 104 | }) 105 | .catch(error => { 106 | console.error('Error during service worker registration:', error); 107 | }); 108 | } 109 | 110 | function checkValidServiceWorker(swUrl: string, config?: Config) { 111 | // Check if the service worker can be found. If it can't reload the page. 112 | fetch(swUrl, { 113 | headers: { 'Service-Worker': 'script' }, 114 | }) 115 | .then(response => { 116 | // Ensure service worker exists, and that we really are getting a JS file. 117 | const contentType = response.headers.get('content-type'); 118 | if ( 119 | response.status === 404 || 120 | (contentType != null && contentType.indexOf('javascript') === -1) 121 | ) { 122 | // No service worker found. Probably a different app. Reload the page. 123 | navigator.serviceWorker.ready.then(registration => { 124 | registration.unregister().then(() => { 125 | window.location.reload(); 126 | }); 127 | }); 128 | } else { 129 | // Service worker found. Proceed as normal. 130 | registerValidSW(swUrl, config); 131 | } 132 | }) 133 | .catch(() => { 134 | console.log( 135 | 'No internet connection found. App is running in offline mode.' 136 | ); 137 | }); 138 | } 139 | 140 | export function unregister() { 141 | if ('serviceWorker' in navigator) { 142 | navigator.serviceWorker.ready 143 | .then(registration => { 144 | registration.unregister(); 145 | }) 146 | .catch(error => { 147 | console.error(error.message); 148 | }); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | import 'jest-canvas-mock'; 7 | -------------------------------------------------------------------------------- /src/strings.js: -------------------------------------------------------------------------------- 1 | export const DEFAULT_BACKGROUND_COLORS = ['darkgrey', 'lightgrey']; 2 | export const DEFAULT_TEXT_COLORS = ['black']; 3 | export const DEFAULT_OUTER_BORDER_COLOR = 'black'; 4 | export const DEFAULT_OUTER_BORDER_WIDTH = 5; 5 | export const DEFAULT_INNER_RADIUS = 0; 6 | export const DEFAULT_INNER_BORDER_COLOR = 'black'; 7 | export const DEFAULT_INNER_BORDER_WIDTH = 0; 8 | export const DEFAULT_RADIUS_LINE_COLOR = 'black'; 9 | export const DEFAULT_RADIUS_LINE_WIDTH = 5; 10 | export const DEFAULT_FONT_FAMILY = 'Nunito'; 11 | export const DEFAULT_FONT_SIZE = 20; 12 | export const DEFAULT_FONT_WEIGHT = 'bold'; 13 | export const DEFAULT_FONT_STYLE = 'normal'; 14 | export const DEFAULT_TEXT_DISTANCE = 60; 15 | export const DEFAULT_SPIN_DURATION = 1.0; 16 | export const DISABLE_INITIAL_ANIMATION = false; 17 | export const WEB_FONTS = [ 18 | 'arial', 19 | 'verdana', 20 | 'tahoma', 21 | 'trebuchet ms', 22 | 'times', 23 | 'garamond', 24 | 'brush script mt', 25 | 'courier new', 26 | 'georgia', 27 | 'helvetica', 28 | 'times new roman', 29 | 'serif', 30 | 'sans-serif', 31 | 'monospace', 32 | 'cursive', 33 | 'fantasy', 34 | ]; 35 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { WEB_FONTS } from './strings'; 2 | 3 | export const getRotationDegrees = ( 4 | prizeNumber: number, 5 | numberOfPrizes: number, 6 | randomDif = true 7 | ): number => { 8 | const degreesPerPrize = 360 / numberOfPrizes; 9 | 10 | const initialRotation = 43 + degreesPerPrize / 2; 11 | 12 | const randomDifference = (-1 + Math.random() * 2) * degreesPerPrize * 0.35; 13 | 14 | const perfectRotation = 15 | degreesPerPrize * (numberOfPrizes - prizeNumber) - initialRotation; 16 | 17 | const imperfectRotation = 18 | degreesPerPrize * (numberOfPrizes - prizeNumber) - 19 | initialRotation + 20 | randomDifference; 21 | 22 | const prizeRotation = randomDif ? imperfectRotation : perfectRotation; 23 | 24 | return numberOfPrizes - prizeNumber > numberOfPrizes / 2 25 | ? -360 + prizeRotation 26 | : prizeRotation; 27 | }; 28 | 29 | export const clamp = (min: number, max: number, val: number): number => 30 | Math.min(Math.max(min, +val), max); 31 | 32 | export const isCustomFont = (font: string): boolean => 33 | !!font && !WEB_FONTS.includes(font.toLowerCase()); 34 | 35 | export const getQuantity = (prizeMap: number[][]): number => 36 | prizeMap.slice(-1)[0].slice(-1)[0] + 1; 37 | 38 | const characters = 39 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 40 | 41 | export const makeClassKey = (length: number): string => { 42 | let result = ''; 43 | const charactersLength = characters.length; 44 | for (let i = 0; i < length; i++) { 45 | result += characters.charAt(Math.floor(Math.random() * charactersLength)); 46 | } 47 | 48 | return result; 49 | }; 50 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "declaration": true, 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "react", 16 | "outDir": "./dist/", 17 | "noImplicitAny": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "module": "esnext" 21 | }, 22 | "include": ["src"] 23 | } 24 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | module.exports = { 3 | mode: 'production', 4 | entry: './src/index.tsx', 5 | output: { 6 | filename: 'bundle.js', 7 | path: path.resolve(__dirname, 'dist'), 8 | library: 'Wheel', 9 | libraryTarget: 'umd', 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.tsx?$/, 15 | include: path.resolve(__dirname, 'src'), 16 | exclude: /(node_modules|bower_components|dist)/, 17 | use: 'ts-loader', 18 | }, 19 | { 20 | test: /\.(png|jpg|gif)$/i, 21 | use: [ 22 | { 23 | loader: 'url-loader', 24 | options: { 25 | limit: 8192, 26 | }, 27 | }, 28 | ], 29 | }, 30 | { 31 | test: /\.css$/i, 32 | use: ['style-loader', 'css-loader'], 33 | }, 34 | ], 35 | }, 36 | resolve: { 37 | extensions: ['.tsx', '.ts', '.js'], 38 | }, 39 | externals: { 40 | react: { 41 | commonjs: 'react', 42 | commonjs2: 'react', 43 | amd: 'React', 44 | root: 'React', 45 | }, 46 | 'react-dom': { 47 | commonjs: 'react-dom', 48 | commonjs2: 'react-dom', 49 | amd: 'ReactDOM', 50 | root: 'ReactDOM', 51 | }, 52 | }, 53 | } 54 | --------------------------------------------------------------------------------