├── .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 | [](https://www.npmjs.com/package/react-custom-roulette)
6 | [](https://www.typescriptlang.org/index.html)
7 | [](https://www.npmjs.com/package/react-custom-roulette)
8 |
9 |
10 |
11 | Customizable React roulette wheel with spinning animation
12 |
13 |
14 |
15 | 
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 |
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 |
--------------------------------------------------------------------------------