UNPKG

ng-spinner-wheel

Version:

A customizable standalone Angular spinner wheel component that supports weighted items. Useful for raffles, lucky draws, and gamified selections where some items have higher chances of winning.

357 lines (349 loc) 14.3 kB
import * as i0 from '@angular/core'; import { Injectable, EventEmitter, Component, ViewChild, Input, Output } from '@angular/core'; class NgSpinnerWheelService { constructor() { } getRandomColor() { const letters = '0123456789ABCDEF'; let color = '#'; for (let i = 0; i < 6; i++) { color += letters[Math.floor(Math.random() * 16)]; } return color; } items = []; addItemFoePool(item, weight) { if (weight > 0) { this.items.push({ item, weight }); } else { throw new Error("Weight must be greater than 0"); } } selectRandomItem() { const totalWeight = this.items.reduce((sum, item) => sum + item.weight, 0); if (totalWeight === 0) { return null; // No items in the pool } let randomValue = Math.random() * totalWeight; let accumulatedWeight = 0; for (const { item, weight } of this.items) { accumulatedWeight += weight; if (randomValue <= accumulatedWeight) { return item; } } return null; // Shouldn't reach here, but just in case } getTextColor(backgroundHexColor) { const hex = backgroundHexColor.replace(/^#/, ''); const r = parseInt(hex.substring(0, 2), 16) / 255; const g = parseInt(hex.substring(2, 4), 16) / 255; const b = parseInt(hex.substring(4, 6), 16) / 255; const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b; return luminance > 0.5 ? '#000000' : '#FFFFFF'; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NgSpinnerWheelService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NgSpinnerWheelService, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NgSpinnerWheelService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [] }); class NgSpinnerWheelComponent { util; ctx; canvasEl; allItems = []; btnWidth = 30; width = 260; spinCompleted = new EventEmitter(); center; deg = 0; speed = 10; isStopped = false; lock = false; isSpinning = false; slowDownRand = 0; canvas; currentWinner; fontSize = 15; forceStop = false; constructor(util) { this.util = util; } async ngAfterViewInit() { this.regenerate(); } regenerate() { setTimeout(() => { this.onInIt2(); this.loadDataInit(); this.createSpinner(); }, 1000); } ngOnChanges(changes) { if (changes['allItems']) { this.regenerate(); } } onInIt2() { this.canvas = this.canvasEl.nativeElement; this.ctx = this.canvas.getContext('2d'); this.width = this.canvas.width; this.center = this.width / 2; } async loadDataInit() { const totalWeight = this.allItems.reduce((sum, item) => sum + (item.menuWeight ?? 1), 0); await this.allItems.forEach((element) => { if (!element.menuWeight) element.menuWeight = 1; element.percentage = ((element.menuWeight / totalWeight) * 100); if (!element.backColor) { element.backColor = this.util.getRandomColor(); } if (!element.textColor) { element.textColor = this.util.getTextColor(element.backColor); } if (!element.fontSize) { element.fontSize = "15px"; } }); this.allItems = this.shuffleArray(this.allItems); console.log(this.allItems); } startSpin() { if (this.lock || this.isStopped || this.isSpinning) { return; } const randomNumber = Math.floor(Math.random() * (8501 - 5000) + 5000); setTimeout(() => { this.stops(); }, randomNumber); this.lock = false; this.forceStop = false; this.isStopped = false; this.isSpinning = true; this.spin(); } spin() { let spin = () => { let slicesData = this.allItems; // const color = slicesData.map(slice => slice.color); const label = slicesData.map((slice) => slice.menuTitle); const slices = label.length; // const sliceDeg = 360 / slices; this.deg += this.speed; this.deg %= 360; if (this.forceStop) { this.lock = false; this.isStopped = false; this.isSpinning = false; this.speed = 0; return; } // Increment speed if (!this.isStopped && this.speed < 6) { this.speed = this.speed + 1 * 0.1; } // Decrement Speed if (this.isStopped) { if (!this.lock) { this.lock = true; this.slowDownRand = this.rand(0.990, 0.970); } this.speed = this.speed > 0.2 ? this.speed *= this.slowDownRand : 0; } if (this.lock && !this.speed) { this.lock = false; this.isStopped = false; this.isSpinning = false; // Determine winner based on final degree const finalDeg = (360 - this.deg + 270) % 360; // 270 is the top pointer let angleAccumulator = 0; for (let slice of slicesData) { const sliceAngle = (slice.percentage ?? 0) * 3.6; if (finalDeg >= angleAccumulator && finalDeg < angleAccumulator + sliceAngle) { this.currentWinner = slice; break; } angleAccumulator += sliceAngle; } this.spinCompleted.emit(this.currentWinner); // alert(this.currentWinner?.menu_title || 'No winner found!'); return; } this.ctx?.clearRect(0, 0, this.width, this.width); this.currentWinner = null; let allTtl = this.deg; slicesData.forEach((slice, i) => { if (this.ctx) { this.ctx.beginPath(); this.ctx.fillStyle = slice.backColor || "#000000"; this.ctx.moveTo(this.center, this.center); this.ctx.arc(this.center, this.center, this.width / 2, this.deg2rad(this.deg), this.deg2rad(this.deg + ((slice.percentage ?? 0) * 3.6))); this.ctx.lineTo(this.center, this.center); this.ctx.fill(); allTtl += ((slice.percentage ?? 0) * 3.6); const drawText_deg = this.deg + ((slice.percentage ?? 0) * 3.6) / 2; // console.log(this.deg,this.deg%360,slice.menu_title); if ((allTtl % 360) > 270 && !this.currentWinner) { this.currentWinner = slice; } this.ctx.save(); this.ctx.translate(this.center, this.center); this.ctx.rotate(this.deg2rad(drawText_deg)); this.ctx.textAlign = "right"; this.ctx.fillStyle = slice.textColor || "#000000"; ; var textvalArr = this.toMultiLine(slice.menuTitle); var linespacing = 15; var startX = (this.width / 2) - 10; var startY = 5; for (var i = 0; i < textvalArr.length; i++) { this.ctx.fillText(textvalArr[i], startX, startY, (this.width / 2) - 20); startY += linespacing; } // this.ctx.font = 'bold ' + this.fontSize + 'px sans-serif'; // this.ctx.fillText(slice.menu_title, 100, 5); this.ctx.restore(); this.deg += ((slice.percentage ?? 0) * 3.6); } }); window.requestAnimationFrame(() => spin()); }; spin(); //window.requestAnimationFrame(spin); } stops() { if (!this.isSpinning || this.isStopped) { return; } this.isStopped = true; } deg2rad(deg) { return deg * Math.PI / 180; } rand(min, max) { return Math.random() * (max - min) + min; } setFontSize() { const baseFontSize = this.width / 20; // adjust divisor as needed const itemCount = this.allItems?.length || 0; if (itemCount <= 5) { this.fontSize = baseFontSize; } else if (itemCount <= 10) { this.fontSize = baseFontSize * 0.8; } else { this.fontSize = baseFontSize * 0.6; } } createSpinner() { this.setFontSize(); let slicesData = this.allItems; const ctx = this.ctx; const width = this.width; const center = width / 2; if (ctx) { ctx.clearRect(0, 0, width, width); const totalDegrees = 360; let currentDegrees = 0; slicesData.forEach((slice) => { const name = slice.menuTitle; // const id = slice.id; const degree = (slice.percentage ?? 0) * 3.6; // const color = slice.color; // const { name, id, degree, color } = {1,1,1,1}; ctx.beginPath(); ctx.fillStyle = slice.backColor || "#000000"; ctx.moveTo(center, center); ctx.arc(center, center, width / 2, this.deg2rad(currentDegrees), this.deg2rad(currentDegrees + degree)); ctx.lineTo(center, center); ctx.fill(); const drawText_deg = currentDegrees + degree / 2; ctx.save(); ctx.translate(center, center); ctx.rotate(this.deg2rad(drawText_deg)); ctx.textAlign = "right"; ctx.fillStyle = slice.textColor || "#000000"; ; ctx.font = `bold ${slice.fontSize} sans-serif`; var textvalArr = this.toMultiLine(name); var linespacing = 15; var startX = (this.width / 2) - 5; var startY = 5; for (var i = 0; i < textvalArr.length; i++) { ctx.fillText(textvalArr[i], startX, startY, (this.width / 2) - 20); startY += linespacing; } ctx.restore(); currentDegrees += degree; }); } } toMultiLine(text) { var textArr = new Array(); text = text.replace(/\n\r?/g, '<br/>'); textArr = text.split("<br/>"); return textArr; } shuffleArray(array) { const shuffled = [...array]; // copy to avoid mutating original array for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } return shuffled; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NgSpinnerWheelComponent, deps: [{ token: NgSpinnerWheelService }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: NgSpinnerWheelComponent, isStandalone: true, selector: "lib-ng-spinner-wheel", inputs: { allItems: "allItems", btnWidth: "btnWidth", width: "width" }, outputs: { spinCompleted: "spinCompleted" }, viewQueries: [{ propertyName: "canvasEl", first: true, predicate: ["canvas"], descendants: true }], usesOnChanges: true, ngImport: i0, template: ` <div id="wheel" class="ion-text-center"> <div style="position: relative;"> <canvas id="canvas" #canvas [width]="width" [height]="width"></canvas> <div> <img [style.height]="btnWidth+'px'" [style.width]="btnWidth+'px'" [style.top]="(width/2)-(btnWidth/2)+'px'" [style.left]="(width/2)-(btnWidth/2)+'px'" style="position: absolute;" (click)="startSpin()" src="https://i.postimg.cc/hj1XkSfG/spin.png" /> </div> </div> </div> `, isInline: true, styles: [""] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NgSpinnerWheelComponent, decorators: [{ type: Component, args: [{ selector: 'lib-ng-spinner-wheel', standalone: true, imports: [], template: ` <div id="wheel" class="ion-text-center"> <div style="position: relative;"> <canvas id="canvas" #canvas [width]="width" [height]="width"></canvas> <div> <img [style.height]="btnWidth+'px'" [style.width]="btnWidth+'px'" [style.top]="(width/2)-(btnWidth/2)+'px'" [style.left]="(width/2)-(btnWidth/2)+'px'" style="position: absolute;" (click)="startSpin()" src="https://i.postimg.cc/hj1XkSfG/spin.png" /> </div> </div> </div> ` }] }], ctorParameters: () => [{ type: NgSpinnerWheelService }], propDecorators: { canvasEl: [{ type: ViewChild, args: ['canvas'] }], allItems: [{ type: Input, args: ["allItems"] }], btnWidth: [{ type: Input, args: ["btnWidth"] }], width: [{ type: Input, args: ["width"] }], spinCompleted: [{ type: Output }] } }); /* * Public API Surface of ng-spinner-wheel */ /** * Generated bundle index. Do not edit. */ export { NgSpinnerWheelComponent, NgSpinnerWheelService }; //# sourceMappingURL=ng-spinner-wheel.mjs.map