141 lines
4.9 KiB
Vue
141 lines
4.9 KiB
Vue
<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>
|