first commit

This commit is contained in:
s.esmaeili
2026-09-10 01:58:29 +03:30
commit 077da7eacd
201 changed files with 25072 additions and 0 deletions
@@ -0,0 +1,214 @@
<script setup lang="ts">
import { toFarsiDigits, WheelDesignProps } from '@lucky-wheel/shared-types';
import { computed, watch } from 'vue';
import { useWheelRotation } from '../../composables/useWheelRotation';
const props = defineProps<WheelDesignProps>();
const emit = defineEmits<{
(e: 'spin-finished'): void;
}>();
const { wheelStyle, pointerStyle, isSpinning, spinTo } = useWheelRotation({
segmentCount: () => props.segments.length,
pegCount: () => 24,
onFinished: () => emit('spin-finished'),
});
watch(
[() => props.spinning, () => props.targetSegmentIndex],
([newSpinning, newTargetIdx]) => {
if (newSpinning && newTargetIdx !== null && newTargetIdx !== undefined && !isSpinning.value) {
spinTo(newTargetIdx);
}
},
{ flush: 'post' },
);
const totalSegments = computed(() => props.segments.length);
const sliceAngle = computed(() => (totalSegments.value > 0 ? 360 / totalSegments.value : 0));
// Generate SVG sector path (starting from 12 o'clock, clockwise)
function getSlicePath(index: number): string {
if (totalSegments.value <= 1) return '';
const r = 140;
const cx = 150;
const cy = 150;
// -90 deg puts index 0 at the top (12 o'clock)
const startAngle = (index * sliceAngle.value - 90) * (Math.PI / 180);
const endAngle = ((index + 1) * sliceAngle.value - 90) * (Math.PI / 180);
const x1 = cx + r * Math.cos(startAngle);
const y1 = cy + r * Math.sin(startAngle);
const x2 = cx + r * Math.cos(endAngle);
const y2 = cy + r * Math.sin(endAngle);
const largeArc = sliceAngle.value > 180 ? 1 : 0;
return `M ${cx} ${cy} L ${x1} ${y1} A ${r} ${r} 0 ${largeArc} 1 ${x2} ${y2} Z`;
}
function getTextRotation(index: number): string {
const midAngle = index * sliceAngle.value + sliceAngle.value / 2;
// Rotate around center (150, 150)
return `rotate(${midAngle} 150 150)`;
}
// Casino rim lights (24 bulb positions)
const bulbs = computed(() => {
const count = 24;
return Array.from({ length: count }, (_, i) => {
const angle = (i * (360 / count) - 90) * (Math.PI / 180);
return {
x: 150 + 144 * Math.cos(angle),
y: 150 + 144 * Math.sin(angle),
active: i % 2 === 0,
};
});
});
</script>
<template>
<div class="relative w-full max-w-[340px] xs:max-w-[380px] sm:max-w-[420px] aspect-square mx-auto select-none">
<!-- Casino Outer Golden Bezel Frame -->
<div
class="absolute inset-0 rounded-full shadow-[0_10px_35px_rgba(0,0,0,0.6)]"
style="background: radial-gradient(circle, #ffe259 0%, #b8860b 60%, #543d03 100%); padding: 10px"
>
<div class="w-full h-full rounded-full border-4 border-amber-300 relative overflow-hidden bg-stone-900">
<!-- SVG Spinning Wheel Content -->
<svg
class="w-full h-full"
:style="wheelStyle"
viewBox="0 0 300 300"
>
<!-- Segment slices -->
<g v-for="(seg, idx) in segments" :key="seg.id">
<path
:d="getSlicePath(idx)"
:fill="seg.color || (idx % 2 === 0 ? '#b91c1c' : '#1e293b')"
stroke="#fbbf24"
stroke-width="1.5"
/>
<!-- Slice Content: Title & Image -->
<g :transform="getTextRotation(idx)">
<text
x="150"
y="45"
text-anchor="middle"
fill="#ffffff"
font-size="11"
font-weight="700"
class="wheel-slice-text tracking-tight font-sans"
>
{{ toFarsiDigits(seg.title) }}
</text>
<image
v-if="seg.image"
:href="seg.image"
x="138"
y="54"
width="24"
height="24"
/>
</g>
</g>
<!-- Embossed 3D Casino Rim Studs / Rivets (24 pegs) -->
<g v-for="(bulb, bIdx) in bulbs" :key="bIdx">
<!-- Socket bevel shadow -->
<circle
:cx="bulb.x"
:cy="bulb.y"
r="4.4"
fill="#1c1917"
stroke="#543d03"
stroke-width="0.8"
/>
<!-- 3D Convex Metallic Stud -->
<circle
:cx="bulb.x"
:cy="bulb.y"
r="3.5"
fill="url(#casinoStud3D)"
stroke="#fde047"
stroke-width="0.5"
/>
<!-- Specular Highlight Glint -->
<circle
:cx="bulb.x - 1"
:cy="bulb.y - 1.1"
r="0.9"
fill="#ffffff"
opacity="0.85"
/>
</g>
<!-- Center Brass Hub -->
<circle cx="150" cy="150" r="32" fill="url(#casinoCenterGrad)" stroke="#fef08a" stroke-width="2.5" />
<circle cx="150" cy="150" r="22" fill="#78350f" stroke="#fbbf24" stroke-width="1.5" />
<text x="150" y="155" text-anchor="middle" font-size="16" fill="#fef08a">★</text>
<defs>
<radialGradient id="casinoCenterGrad" cx="40%" cy="40%" r="60%">
<stop offset="0%" stop-color="#fff" />
<stop offset="40%" stop-color="#fbbf24" />
<stop offset="100%" stop-color="#92400e" />
</radialGradient>
<!-- 3D Spherical Stud Radial Gradient -->
<radialGradient id="casinoStud3D" cx="35%" cy="30%" r="70%">
<stop offset="0%" stop-color="#ffffff" />
<stop offset="25%" stop-color="#fef08a" />
<stop offset="65%" stop-color="#d97706" />
<stop offset="100%" stop-color="#451a03" />
</radialGradient>
</defs>
</svg>
</div>
</div>
<!-- Casino Retro Pointer at 12 o'clock with Tactile Peg Deflection -->
<div class="absolute -top-3 left-1/2 -translate-x-1/2 z-20 pointer-events-none drop-shadow-[0_4px_8px_rgba(0,0,0,0.7)]">
<div :style="pointerStyle" class="w-10 h-[46px] origin-[50%_12px] will-change-transform">
<svg
width="40"
height="46"
viewBox="0 0 40 46"
fill="none"
class="overflow-visible"
>
<!-- Needle / Stopper Body -->
<path
d="M20 46L6 14C4 10 7 4 12 4H28C33 4 36 10 34 14L20 46Z"
fill="url(#pointerGold)"
stroke="#ffffff"
stroke-width="1.5"
/>
<!-- Inner Bevel Highlight -->
<path
d="M20 41L9 14C8 11 10 7 13 7H27C30 7 32 11 31 14L20 41Z"
fill="none"
stroke="rgba(255,255,255,0.45)"
stroke-width="0.9"
/>
<!-- Center Hinge Pin Outer Ring -->
<circle cx="20" cy="12" r="5.5" fill="#78350f" stroke="#fbbf24" stroke-width="1.2" />
<!-- Center Ruby / Gold Pin Core -->
<circle cx="20" cy="12" r="4" fill="url(#rubyPinGrad)" />
<circle cx="19" cy="10.8" r="1.2" fill="#ffffff" opacity="0.85" />
<defs>
<linearGradient id="pointerGold" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#fffbeb" />
<stop offset="35%" stop-color="#fef08a" />
<stop offset="70%" stop-color="#d97706" />
<stop offset="100%" stop-color="#78350f" />
</linearGradient>
<radialGradient id="rubyPinGrad" cx="35%" cy="35%" r="65%">
<stop offset="0%" stop-color="#fca5a5" />
<stop offset="40%" stop-color="#dc2626" />
<stop offset="100%" stop-color="#7f1d1d" />
</radialGradient>
</defs>
</svg>
</div>
</div>
</div>
</template>
@@ -0,0 +1,174 @@
<script setup lang="ts">
import { toFarsiDigits, WheelDesignProps } from '@lucky-wheel/shared-types';
import { computed, watch } from 'vue';
import { useWheelRotation } from '../../composables/useWheelRotation';
const props = defineProps<WheelDesignProps>();
const emit = defineEmits<{
(e: 'spin-finished'): void;
}>();
const { wheelStyle, pointerStyle, isSpinning, spinTo } = useWheelRotation({
segmentCount: () => props.segments.length,
pegCount: () => 24,
onFinished: () => emit('spin-finished'),
});
watch(
[() => props.spinning, () => props.targetSegmentIndex],
([newSpinning, newTargetIdx]) => {
if (newSpinning && newTargetIdx !== null && newTargetIdx !== undefined && !isSpinning.value) {
spinTo(newTargetIdx);
}
},
{ flush: 'post' },
);
const totalSegments = computed(() => props.segments.length);
const sliceAngle = computed(() => (totalSegments.value > 0 ? 360 / totalSegments.value : 0));
function getSlicePath(index: number): string {
if (totalSegments.value <= 1) return '';
const r = 138;
const cx = 150;
const cy = 150;
const startAngle = (index * sliceAngle.value - 90) * (Math.PI / 180);
const endAngle = ((index + 1) * sliceAngle.value - 90) * (Math.PI / 180);
const x1 = cx + r * Math.cos(startAngle);
const y1 = cy + r * Math.sin(startAngle);
const x2 = cx + r * Math.cos(endAngle);
const y2 = cy + r * Math.sin(endAngle);
const largeArc = sliceAngle.value > 180 ? 1 : 0;
return `M ${cx} ${cy} L ${x1} ${y1} A ${r} ${r} 0 ${largeArc} 1 ${x2} ${y2} Z`;
}
function getTextRotation(index: number): string {
const midAngle = index * sliceAngle.value + sliceAngle.value / 2;
return `rotate(${midAngle} 150 150)`;
}
// 24 Luxury embossed golden pegs/studs along rim
const pegs = computed(() => {
const count = 24;
return Array.from({ length: count }, (_, i) => {
const angle = (i * (360 / count) - 90) * (Math.PI / 180);
return {
x: 150 + 143 * Math.cos(angle),
y: 150 + 143 * Math.sin(angle),
};
});
});
</script>
<template>
<div class="relative w-full max-w-[340px] xs:max-w-[380px] sm:max-w-[420px] aspect-square mx-auto select-none">
<!-- Luxury Gold Outer Ring -->
<div
class="absolute inset-0 rounded-full p-3 shadow-[0_15px_40px_rgba(0,0,0,0.8)]"
style="background: linear-gradient(135deg, #d4af37 0%, #f3e5ab 25%, #aa7c11 50%, #fdf6c7 75%, #8a6408 100%)"
>
<div class="w-full h-full rounded-full border-2 border-amber-200/60 relative overflow-hidden bg-gradient-to-b from-stone-900 to-black">
<!-- SVG Wheel -->
<svg
class="w-full h-full"
:style="wheelStyle"
viewBox="0 0 300 300"
>
<!-- Segments -->
<g v-for="(seg, idx) in segments" :key="seg.id">
<path
:d="getSlicePath(idx)"
:fill="seg.color || (idx % 2 === 0 ? '#1c1917' : '#0c0a09')"
stroke="#d4af37"
stroke-width="1.5"
/>
<g :transform="getTextRotation(idx)">
<text
x="150"
y="45"
text-anchor="middle"
fill="#fdf6c7"
font-size="11"
font-weight="700"
class="wheel-slice-text font-sans tracking-wider"
>
{{ toFarsiDigits(seg.title) }}
</text>
<image
v-if="seg.image"
:href="seg.image"
x="138"
y="54"
width="24"
height="24"
/>
</g>
</g>
<!-- 24 Luxury Embossed Gold Pegs/Studs -->
<g v-for="(peg, pIdx) in pegs" :key="pIdx">
<circle :cx="peg.x" :cy="peg.y" r="4.2" fill="#1c1917" stroke="#aa7c11" stroke-width="0.7" />
<circle :cx="peg.x" :cy="peg.y" r="3.3" fill="url(#luxuryPeg3D)" stroke="#fdf6c7" stroke-width="0.5" />
<circle :cx="peg.x - 0.9" :cy="peg.y - 1" r="0.8" fill="#ffffff" opacity="0.85" />
</g>
<!-- Center VIP Medallion -->
<circle cx="150" cy="150" r="30" fill="url(#luxuryGoldGrad)" stroke="#fdf6c7" stroke-width="2" />
<circle cx="150" cy="150" r="22" fill="#1c1917" stroke="#aa7c11" stroke-width="1" />
<text x="150" y="156" text-anchor="middle" font-size="14" font-family="serif" font-weight="bold" fill="#fdf6c7">VIP</text>
<defs>
<linearGradient id="luxuryGoldGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#fdf6c7" />
<stop offset="50%" stop-color="#d4af37" />
<stop offset="100%" stop-color="#8a6408" />
</linearGradient>
<radialGradient id="luxuryPeg3D" cx="35%" cy="30%" r="70%">
<stop offset="0%" stop-color="#ffffff" />
<stop offset="30%" stop-color="#fdf6c7" />
<stop offset="70%" stop-color="#d4af37" />
<stop offset="100%" stop-color="#543d03" />
</radialGradient>
</defs>
</svg>
</div>
</div>
<!-- Ornate Royal Gold Pointer with Tactile Peg Deflection -->
<div class="absolute -top-3.5 left-1/2 -translate-x-1/2 z-20 pointer-events-none drop-shadow-[0_4px_10px_rgba(212,175,55,0.6)]">
<div :style="pointerStyle" class="w-9 h-[46px] origin-[50%_12px] will-change-transform">
<svg
width="36"
height="46"
viewBox="0 0 36 46"
fill="none"
class="overflow-visible"
>
<path
d="M18 46L5 14C3 9 7 3 12 3H24C29 3 33 9 31 14L18 46Z"
fill="url(#luxuryPointerGrad)"
stroke="#fff"
stroke-width="1.2"
/>
<path
d="M18 41L8 14C7 11 9 6 13 6H23C27 6 29 11 28 14L18 41Z"
fill="none"
stroke="rgba(255,255,255,0.4)"
stroke-width="0.8"
/>
<circle cx="18" cy="12" r="4.5" fill="#1c1917" stroke="#fdf6c7" stroke-width="1.5" />
<circle cx="17.2" cy="10.8" r="1.1" fill="#ffffff" opacity="0.85" />
<defs>
<linearGradient id="luxuryPointerGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#fdf6c7" />
<stop offset="50%" stop-color="#d4af37" />
<stop offset="100%" stop-color="#543d03" />
</linearGradient>
</defs>
</svg>
</div>
</div>
</div>
</template>
@@ -0,0 +1,140 @@
<script setup lang="ts">
import { toFarsiDigits, WheelDesignProps } from '@lucky-wheel/shared-types';
import { computed, watch } from 'vue';
import { useWheelRotation } from '../../composables/useWheelRotation';
const props = defineProps<WheelDesignProps>();
const emit = defineEmits<{
(e: 'spin-finished'): void;
}>();
const { wheelStyle, pointerStyle, isSpinning, spinTo } = useWheelRotation({
segmentCount: () => props.segments.length,
pegCount: () => 24,
onFinished: () => emit('spin-finished'),
});
watch(
[() => props.spinning, () => props.targetSegmentIndex],
([newSpinning, newTargetIdx]) => {
if (newSpinning && newTargetIdx !== null && newTargetIdx !== undefined && !isSpinning.value) {
spinTo(newTargetIdx);
}
},
{ flush: 'post' },
);
const totalSegments = computed(() => props.segments.length);
const sliceAngle = computed(() => (totalSegments.value > 0 ? 360 / totalSegments.value : 0));
function getSlicePath(index: number): string {
if (totalSegments.value <= 1) return '';
const r = 142;
const cx = 150;
const cy = 150;
const startAngle = (index * sliceAngle.value - 90) * (Math.PI / 180);
const endAngle = ((index + 1) * sliceAngle.value - 90) * (Math.PI / 180);
const x1 = cx + r * Math.cos(startAngle);
const y1 = cy + r * Math.sin(startAngle);
const x2 = cx + r * Math.cos(endAngle);
const y2 = cy + r * Math.sin(endAngle);
const largeArc = sliceAngle.value > 180 ? 1 : 0;
return `M ${cx} ${cy} L ${x1} ${y1} A ${r} ${r} 0 ${largeArc} 1 ${x2} ${y2} Z`;
}
function getTextRotation(index: number): string {
const midAngle = index * sliceAngle.value + sliceAngle.value / 2;
return `rotate(${midAngle} 150 150)`;
}
// 24 Minimal Rim Dots
const minimalPegs = computed(() => {
const count = 24;
return Array.from({ length: count }, (_, i) => {
const angle = (i * (360 / count) - 90) * (Math.PI / 180);
return {
x: 150 + 144 * Math.cos(angle),
y: 150 + 144 * Math.sin(angle),
};
});
});
</script>
<template>
<div class="relative w-full max-w-[340px] xs:max-w-[380px] sm:max-w-[420px] aspect-square mx-auto select-none">
<!-- Minimal Matte Outer Frame -->
<div
class="absolute inset-0 rounded-full p-2 bg-slate-800/80 backdrop-blur border border-slate-700 shadow-2xl"
>
<div class="w-full h-full rounded-full relative overflow-hidden bg-slate-900">
<!-- SVG Spinning Wheel -->
<svg
class="w-full h-full"
:style="wheelStyle"
viewBox="0 0 300 300"
>
<g v-for="(seg, idx) in segments" :key="seg.id">
<path
:d="getSlicePath(idx)"
:fill="seg.color || (idx % 2 === 0 ? '#3b82f6' : '#6366f1')"
stroke="#ffffff"
stroke-width="1"
/>
<g :transform="getTextRotation(idx)">
<text
x="150"
y="45"
text-anchor="middle"
fill="#ffffff"
font-size="11"
font-weight="600"
class="wheel-slice-text tracking-normal font-sans"
>
{{ toFarsiDigits(seg.title) }}
</text>
<image
v-if="seg.image"
:href="seg.image"
x="138"
y="54"
width="24"
height="24"
/>
</g>
</g>
<!-- 24 Minimal Rim Dots -->
<circle
v-for="(peg, pIdx) in minimalPegs"
:key="pIdx"
:cx="peg.x"
:cy="peg.y"
r="2"
fill="#ffffff"
opacity="0.8"
/>
<!-- Clean Minimal Center -->
<circle cx="150" cy="150" r="28" fill="#ffffff" filter="drop-shadow(0 2px 6px rgba(0,0,0,0.2))" />
<circle cx="150" cy="150" r="14" fill="#0f172a" />
</svg>
</div>
</div>
<!-- Minimal Triangular Pointer with Tactile Peg Deflection -->
<div class="absolute -top-2.5 left-1/2 -translate-x-1/2 z-20 pointer-events-none drop-shadow-md">
<div :style="pointerStyle" class="w-7 h-[34px] origin-[50%_6px] will-change-transform">
<svg width="28" height="34" viewBox="0 0 28 34" fill="none" class="overflow-visible">
<path
d="M14 34L2 6C0.5 3 2.5 0 6 0H22C25.5 0 27.5 3 26 6L14 34Z"
fill="#3b82f6"
stroke="#ffffff"
stroke-width="2"
/>
</svg>
</div>
</div>
</div>
</template>
@@ -0,0 +1,140 @@
<script setup lang="ts">
import { toFarsiDigits, WheelDesignProps } from '@lucky-wheel/shared-types';
import { computed, watch } from 'vue';
import { useWheelRotation } from '../../composables/useWheelRotation';
const props = defineProps<WheelDesignProps>();
const emit = defineEmits<{
(e: 'spin-finished'): void;
}>();
const { wheelStyle, pointerStyle, isSpinning, spinTo } = useWheelRotation({
segmentCount: () => props.segments.length,
pegCount: () => 24,
onFinished: () => emit('spin-finished'),
});
watch(
[() => props.spinning, () => props.targetSegmentIndex],
([newSpinning, newTargetIdx]) => {
if (newSpinning && newTargetIdx !== null && newTargetIdx !== undefined && !isSpinning.value) {
spinTo(newTargetIdx);
}
},
{ flush: 'post' },
);
const totalSegments = computed(() => props.segments.length);
const sliceAngle = computed(() => (totalSegments.value > 0 ? 360 / totalSegments.value : 0));
function getSlicePath(index: number): string {
if (totalSegments.value <= 1) return '';
const r = 138;
const cx = 150;
const cy = 150;
const startAngle = (index * sliceAngle.value - 90) * (Math.PI / 180);
const endAngle = ((index + 1) * sliceAngle.value - 90) * (Math.PI / 180);
const x1 = cx + r * Math.cos(startAngle);
const y1 = cy + r * Math.sin(startAngle);
const x2 = cx + r * Math.cos(endAngle);
const y2 = cy + r * Math.sin(endAngle);
const largeArc = sliceAngle.value > 180 ? 1 : 0;
return `M ${cx} ${cy} L ${x1} ${y1} A ${r} ${r} 0 ${largeArc} 1 ${x2} ${y2} Z`;
}
function getTextRotation(index: number): string {
const midAngle = index * sliceAngle.value + sliceAngle.value / 2;
return `rotate(${midAngle} 150 150)`;
}
// 24 Cyberpunk Glowing Rim Pegs
const cyberPegs = computed(() => {
const count = 24;
return Array.from({ length: count }, (_, i) => {
const angle = (i * (360 / count) - 90) * (Math.PI / 180);
return {
x: 150 + 143 * Math.cos(angle),
y: 150 + 143 * Math.sin(angle),
};
});
});
</script>
<template>
<div class="relative w-full max-w-[340px] xs:max-w-[380px] sm:max-w-[420px] aspect-square mx-auto select-none">
<!-- Cyberpunk Glowing Bezel -->
<div
class="absolute inset-0 rounded-full p-2 bg-gradient-to-tr from-cyan-500 via-fuchsia-600 to-blue-600 shadow-[0_0_30px_rgba(0,240,255,0.4)] animate-neon-pulse"
>
<div class="w-full h-full rounded-full border-2 border-cyan-400 relative overflow-hidden bg-[#070913]">
<!-- Rotating SVG Wheel -->
<svg
class="w-full h-full"
:style="wheelStyle"
viewBox="0 0 300 300"
>
<!-- Neon Grids & Segments -->
<g v-for="(seg, idx) in segments" :key="seg.id">
<path
:d="getSlicePath(idx)"
:fill="seg.color || (idx % 2 === 0 ? '#111827' : '#1e1b4b')"
stroke="#00f0ff"
stroke-width="1.5"
stroke-opacity="0.8"
/>
<g :transform="getTextRotation(idx)">
<text
x="150"
y="45"
text-anchor="middle"
fill="#ffffff"
font-size="11"
font-weight="800"
class="wheel-slice-text tracking-wide uppercase font-sans"
>
{{ toFarsiDigits(seg.title) }}
</text>
<image
v-if="seg.image"
:href="seg.image"
x="138"
y="54"
width="24"
height="24"
/>
</g>
</g>
<!-- 24 Neon Rim Nodes -->
<g v-for="(peg, pIdx) in cyberPegs" :key="pIdx">
<circle :cx="peg.x" :cy="peg.y" r="3.2" fill="#05070e" stroke="#00f0ff" stroke-width="1" />
<circle :cx="peg.x" :cy="peg.y" r="1.5" fill="#00f0ff" />
</g>
<!-- High-Tech Center Arc Reactor -->
<circle cx="150" cy="150" r="32" fill="#05070e" stroke="#00f0ff" stroke-width="2" />
<circle cx="150" cy="150" r="22" fill="#1e1b4b" stroke="#ec4899" stroke-width="2" />
<circle cx="150" cy="150" r="10" fill="#00f0ff" class="animate-ping" opacity="0.3" />
<circle cx="150" cy="150" r="8" fill="#00f0ff" />
</svg>
</div>
</div>
<!-- Cyberpunk Laser Pointer at 12 o'clock with Tactile Peg Deflection -->
<div class="absolute -top-3 left-1/2 -translate-x-1/2 z-20 pointer-events-none drop-shadow-[0_0_12px_#00f0ff]">
<div :style="pointerStyle" class="w-[34px] h-[42px] origin-[50%_10px] will-change-transform">
<svg width="34" height="42" viewBox="0 0 34 42" fill="none" class="overflow-visible">
<path
d="M17 42L4 12C2 7 6 2 11 2H23C28 2 32 7 30 12L17 42Z"
fill="#00f0ff"
stroke="#ffffff"
stroke-width="1.5"
/>
<circle cx="17" cy="10" r="4" fill="#ff007f" />
</svg>
</div>
</div>
</div>
</template>
@@ -0,0 +1,142 @@
<script setup lang="ts">
import { toFarsiDigits, WheelDesignProps } from '@lucky-wheel/shared-types';
import { computed, watch } from 'vue';
import { useWheelRotation } from '../../composables/useWheelRotation';
const props = defineProps<WheelDesignProps>();
const emit = defineEmits<{
(e: 'spin-finished'): void;
}>();
const { wheelStyle, pointerStyle, isSpinning, spinTo } = useWheelRotation({
segmentCount: () => props.segments.length,
pegCount: () => 24,
onFinished: () => emit('spin-finished'),
});
watch(
[() => props.spinning, () => props.targetSegmentIndex],
([newSpinning, newTargetIdx]) => {
if (newSpinning && newTargetIdx !== null && newTargetIdx !== undefined && !isSpinning.value) {
spinTo(newTargetIdx);
}
},
{ flush: 'post' },
);
const totalSegments = computed(() => props.segments.length);
const sliceAngle = computed(() => (totalSegments.value > 0 ? 360 / totalSegments.value : 0));
function getSlicePath(index: number): string {
if (totalSegments.value <= 1) return '';
const r = 138;
const cx = 150;
const cy = 150;
const startAngle = (index * sliceAngle.value - 90) * (Math.PI / 180);
const endAngle = ((index + 1) * sliceAngle.value - 90) * (Math.PI / 180);
const x1 = cx + r * Math.cos(startAngle);
const y1 = cy + r * Math.sin(startAngle);
const x2 = cx + r * Math.cos(endAngle);
const y2 = cy + r * Math.sin(endAngle);
const largeArc = sliceAngle.value > 180 ? 1 : 0;
return `M ${cx} ${cy} L ${x1} ${y1} A ${r} ${r} 0 ${largeArc} 1 ${x2} ${y2} Z`;
}
function getTextRotation(index: number): string {
const midAngle = index * sliceAngle.value + sliceAngle.value / 2;
return `rotate(${midAngle} 150 150)`;
}
const cartoonColors = ['#f43f5e', '#8b5cf6', '#06b6d4', '#10b981', '#f59e0b', '#ec4899'];
// 24 Chunky Candy Rim Pegs
const candyPegs = computed(() => {
const count = 24;
return Array.from({ length: count }, (_, i) => {
const angle = (i * (360 / count) - 90) * (Math.PI / 180);
return {
x: 150 + 142 * Math.cos(angle),
y: 150 + 142 * Math.sin(angle),
color: cartoonColors[i % cartoonColors.length],
};
});
});
</script>
<template>
<div class="relative w-full max-w-[340px] xs:max-w-[380px] sm:max-w-[420px] aspect-square mx-auto select-none">
<!-- Chunky Cartoon Outer Ring -->
<div
class="absolute inset-0 rounded-full p-3.5 bg-amber-400 border-4 border-amber-600 shadow-[0_12px_0_#b45309,0_20px_25px_rgba(0,0,0,0.5)]"
>
<div class="w-full h-full rounded-full border-4 border-white relative overflow-hidden bg-amber-50">
<!-- SVG Wheel -->
<svg
class="w-full h-full"
:style="wheelStyle"
viewBox="0 0 300 300"
>
<g v-for="(seg, idx) in segments" :key="seg.id">
<path
:d="getSlicePath(idx)"
:fill="seg.color || cartoonColors[idx % cartoonColors.length]"
stroke="#ffffff"
stroke-width="3"
/>
<g :transform="getTextRotation(idx)">
<text
x="150"
y="45"
text-anchor="middle"
fill="#ffffff"
font-size="12"
font-weight="900"
class="wheel-slice-text font-sans tracking-wide"
style="paint-order: stroke fill; stroke: rgba(0,0,0,0.3); stroke-width: 2px"
>
{{ toFarsiDigits(seg.title) }}
</text>
<image
v-if="seg.image"
:href="seg.image"
x="138"
y="54"
width="24"
height="24"
/>
</g>
</g>
<!-- 24 Chunky Candy Rim Pegs -->
<g v-for="(peg, pIdx) in candyPegs" :key="pIdx">
<circle :cx="peg.x" :cy="peg.y" r="4" fill="#ffffff" stroke="#b45309" stroke-width="1.5" />
<circle :cx="peg.x" :cy="peg.y" r="2.8" :fill="peg.color" />
<circle :cx="peg.x - 0.8" :cy="peg.y - 0.8" r="0.8" fill="#ffffff" />
</g>
<!-- Chunky Candy Center Hub -->
<circle cx="150" cy="150" r="32" fill="#ef4444" stroke="#ffffff" stroke-width="4" />
<circle cx="150" cy="150" r="22" fill="#facc15" stroke="#b45309" stroke-width="2" />
<text x="150" y="157" text-anchor="middle" font-size="20">✨</text>
</svg>
</div>
</div>
<!-- Bouncy Cartoon Pointer at 12 o'clock with Tactile Peg Deflection -->
<div class="absolute -top-4 left-1/2 -translate-x-1/2 z-20 pointer-events-none drop-shadow-[0_6px_0_#991b1b]">
<div :style="pointerStyle" class="w-[42px] h-[50px] origin-[50%_14px] will-change-transform">
<svg width="42" height="50" viewBox="0 0 42 50" fill="none" class="overflow-visible">
<path
d="M21 50L6 16C4 11 8 4 14 4H28C34 4 38 11 36 16L21 50Z"
fill="#ef4444"
stroke="#ffffff"
stroke-width="3"
/>
<circle cx="21" cy="14" r="6" fill="#facc15" stroke="#ffffff" stroke-width="2" />
</svg>
</div>
</div>
</div>
</template>