XiaoLin1995/vue-fortune-wheel

[Vue 3] Component migrated

Closed this issue · 4 comments

<template>
  <div class="fw-container">
    <!-- wheel -->
    <div
      class="fw-wheel"
      :style="rotateStyle"
      @transitionend="onRotateEnd"
      @webkitTransitionend="onRotateEnd"
    >
      <canvas
        v-if="type === 'canvas'"
        ref="wheel"
        :width="canvasConfig.radius * 2"
        :height="canvasConfig.radius * 2"
      ></canvas>
      <slot name="wheel" v-else></slot>
    </div>
    <!-- button -->
    <div class="fw-btn">
      <div
        v-if="type === 'canvas'"
        class="fw-btn__btn"
        :style="{ width: canvasConfig.btnWidth + 'px', height: canvasConfig.btnWidth + 'px' }"
        @click="handleClick"
      >
        {{ canvasConfig.btnText }}
      </div>
      <div v-else class="fw-btn__image" @click="handleClick">
        <slot name="button"></slot>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, watch, onMounted, toRef } from 'vue'
import sumBy from 'lodash/sumBy'
import random from 'lodash/random'

const canvasDefaultConfig = {
  radius: 250,
  textRadius: 190,
  textLength: 6,
  textDirection: 'horizontal',
  lineHeight: 20,
  borderWidth: 0,
  borderColor: 'transparent',
  btnText: 'GO',
  btnWidth: 140,
  fontSize: 34
}

const props = defineProps({
  type: {
    type: String,
    default: 'canvas' // canvas || image
  },
  useWeight: {
    type: Boolean,
    default: false
  },
  disabled: {
    type: Boolean,
    default: false
  },
  verify: {
    type: Boolean,
    default: false
  },
  canvas: {
    type: Object,
    default: () => ({
      radius: 250,
      textRadius: 190,
      textLength: 6,
      textDirection: 'horizontal',
      lineHeight: 20,
      borderWidth: 0,
      borderColor: 'transparent',
      btnText: 'GO',
      btnWidth: 140,
      fontSize: 34
    })
  },
  duration: {
    type: Number,
    default: 6000
  },
  timingFun: {
    type: String,
    default: 'cubic-bezier(0.36, 0.95, 0.64, 1)'
  },
  angleBase: {
    type: Number,
    default: 10
  },
  prizeId: {
    type: Number,
    default: 0
  },
  prizes: {
    type: Array,
    required: true,
    default: () => []
  }
})
const emits = defineEmits(['rotateStart', 'rotateEnd'])
const prizeId = toRef(props, 'prizeId')

const wheel = ref(null)
const isRotating = ref(false)
const rotateEndDeg = ref(false)
const prizeRes = ref({})

const canvasConfig = computed(() => Object.assign(canvasDefaultConfig, props.canvas))
const probabilityTotal = computed(() => {
  if (props.useWeight) return 100
  return sumBy(props.prizes, (row) => row.probability || 0)
})
const prizesIdArr = computed(() => {
  const idArr = []
  props.prizes.forEach((row) => {
    const count = props.useWeight ? row.weight || 0 : (row.probability || 0) * decimalSpaces.value
    const arr = new Array(count).fill(row.id)
    idArr.push(...arr)
  })
  return idArr
})
const decimalSpaces = computed(() => {
  if (props.useWeight) return 0
  const sortArr = [...props.prizes].sort((a, b) => {
    const aRes = String(a.probability).split('.')[1]
    const bRes = String(b.probability).split('.')[1]
    const aLen = aRes ? aRes.length : 0
    const bLen = bRes ? bRes.length : 0
    return bLen - aLen
  })
  const maxRes = String(sortArr[0].probability).split('.')[1]
  const idx = maxRes ? maxRes.length : 0
  return [1, 10, 100, 1000, 10000][idx > 4 ? 4 : idx]
})
const rotateStyle = computed(() => ({
  '-webkit-transform': `rotateZ(${rotateEndDeg.value}deg)`,
  transform: `rotateZ(${rotateEndDeg.value}deg)`,
  '-webkit-transition-duration': `${rotateDuration.value}s`,
  'transition-duration': `${rotateDuration.value}s`,
  '-webkit-transition-timing-function:': props.timingFun,
  'transition-timing-function': props.timingFun
}))

const rotateDuration = computed(() => (isRotating.value ? props.duration / 1000 : 0))
const rotateBase = computed(() => {
  let angle = props.angleBase * 360
  if (props.angleBase < 0) angle -= 360
  return angle
})
const canRotate = computed(
  () => !props.disabled && !isRotating.value && probabilityTotal.value === 100
)

function getStrArray(str, len) {
  const arr = []
  while (str !== '') {
    let text = str.substr(0, len)
    if (str.charAt(len) !== '' && str.charAt(len) !== ' ') {
      const index = text.lastIndexOf(' ')
      if (index !== -1) text = text.substr(0, index)
    }
    str = str.replace(text, '').trim()
    arr.push(text)
  }
  return arr
}

function getTargetDeg(prizeId) {
  const angle = 360 / props.prizes.length
  const num = props.prizes.findIndex((row) => row.id === prizeId)
  prizeRes.value = props.prizes[num]
  return 360 - (angle * num + angle / 2)
}

function checkProbability() {
  if (probabilityTotal.value !== 100) {
    throw new Error('Prizes Is Error: Sum of probabilities is not 100!')
  }
  return true
}

function drawPrizeText(ctx, angle, arc, name) {
  const { lineHeight, textLength, textDirection } = canvasConfig.value
  const content = getStrArray(name, textLength)
  if (content === null) return
  textDirection === 'vertical'
    ? ctx.rotate(angle + arc / 2 + Math.PI)
    : ctx.rotate(angle + arc / 2 + Math.PI / 2)
  content.forEach((text, idx) => {
    let textX = -ctx.measureText(text).width / 2
    let textY = (idx + 1) * lineHeight
    if (textDirection === 'vertical') {
      textX = 0
      textY = (idx + 1) * lineHeight - (content.length * lineHeight) / 2
    }
    ctx.fillText(text, textX, textY)
  })
}

function drawCanvas() {
  const canvasEl = wheel.value
  if (canvasEl.getContext) {
    const { radius, textRadius, borderWidth, borderColor, fontSize } = canvasConfig.value
    const arc = Math.PI / (props.prizes.length / 2)
    const ctx = canvasEl.getContext('2d')
    ctx.clearRect(0, 0, radius * 2, radius * 2)
    ctx.strokeStyle = borderColor
    ctx.lineWidth = borderWidth * 2
    ctx.font = `${fontSize}px Arial`
    props.prizes.forEach((row, i) => {
      const angle = i * arc - Math.PI / 2
      ctx.fillStyle = row.bgColor
      ctx.beginPath()
      ctx.arc(radius, radius, radius - borderWidth, angle, angle + arc, false)
      ctx.stroke()
      ctx.arc(radius, radius, 0, angle + arc, angle, true)
      ctx.fill()
      ctx.save()
      ctx.fillStyle = row.color
      ctx.translate(
        radius + Math.cos(angle + arc / 2) * textRadius,
        radius + Math.sin(angle + arc / 2) * textRadius
      )
      drawPrizeText(ctx, angle, arc, row.name)
      ctx.restore()
    })
  }
}

function handleClick() {
  if (!canRotate.value) return
  if (props.verify) {
    emits('rotateStart', onRotateStart)
    return
  }
  emits('rotateStart')
  onRotateStart()
}

function onRotateStart() {
  isRotating.value = true
  const prizeId = props.prizeId || getRandomPrize()
  rotateEndDeg.value = rotateBase.value + getTargetDeg(prizeId)
}

function onRotateEnd() {
  isRotating.value = false
  rotateEndDeg.value %= 360
  emits('rotateEnd', prizeRes.value)
}

function getRandomPrize() {
  const len = prizesIdArr.value.length
  const prizeId = prizesIdArr.value[random(0, len - 1)]
  return prizeId
}

onMounted(() => {
  checkProbability()
  if (props.type === 'canvas') drawCanvas()
})

// prizeId
watch(prizeId, (newVal) => {
  if (!isRotating.value) return
  let newAngle = getTargetDeg(newVal)
  if (props.angleBase < 0) newAngle -= 360
  const prevEndDeg = rotateEndDeg.value
  let nowEndDeg = props.angleBase * 360 + newAngle
  const angle = 360 * Math.floor((nowEndDeg - prevEndDeg) / 360)
  if (props.angleBase >= 0) {
    nowEndDeg += Math.abs(angle)
  } else {
    nowEndDeg += -360 - angle
  }
  rotateEndDeg.value = nowEndDeg
})
</script>

<style scoped lang="scss">
@import './style.scss';
</style>

.fw-container {
  position: relative;
  display: inline-block;
  font-size: 0;
  overflow: hidden;
  canvas,
  img {
    display: block;
    width: 100%;
  }
}

.fw-btn {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
}

.fw-btn__btn {
  position: relative;
  width: 100%;
  height: 100%;
  background: #fff;
  border: 6px solid #fff;
  border-radius: 50%;
  background: #15bd96;
  color: #fff;
  text-align: center;
  font-size: 42px;
  font-weight: bold;
  line-height: 1;
  display: flex;
  justify-content: center;
  align-items: center;
  &:after {
    content: '';
    display: block;
    width: 0;
    height: 0;
    border-left: 18px solid transparent;
    border-right: 18px solid transparent;
    border-bottom: 22px #fff solid;
    position: absolute;
    bottom: 100%;
    left: 50%;
    transform: translateX(-50%);
  }
  &:before {
    content: '';
    display: block;
    width: 0;
    height: 0;
    border-left: 12px solid transparent;
    border-right: 12px solid transparent;
    border-bottom: 18px #15bd96 solid;
    position: absolute;
    bottom: 100%;
    left: 50%;
    transform: translate(-50%) translateY(6px);
    z-index: 10;
  }
}

.fw-btn__image {
  display: inline-block;
}

<template>
  <div class="fw-container">
    <!-- wheel -->
    <div
      class="fw-wheel"
      :style="rotateStyle"
      @transitionend="onRotateEnd"
      @webkitTransitionend="onRotateEnd"
    >
      <canvas
        v-if="type === 'canvas'"
        ref="wheel"
        :width="canvasConfig.radius * 2"
        :height="canvasConfig.radius * 2"
      ></canvas>
      <slot name="wheel" v-else></slot>
    </div>
    <!-- button -->
    <div class="fw-btn">
      <div
        v-if="type === 'canvas'"
        class="fw-btn__btn"
        :style="{ width: canvasConfig.btnWidth + 'px', height: canvasConfig.btnWidth + 'px' }"
        @click="handleClick"
      >
        {{ canvasConfig.btnText }}
      </div>
      <div v-else class="fw-btn__image" @click="handleClick">
        <slot name="button"></slot>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, watch, onMounted, toRef } from 'vue'
import sumBy from 'lodash/sumBy'
import random from 'lodash/random'

const canvasDefaultConfig = {
  radius: 250,
  textRadius: 190,
  textLength: 6,
  textDirection: 'horizontal',
  lineHeight: 20,
  borderWidth: 0,
  borderColor: 'transparent',
  btnText: 'GO',
  btnWidth: 140,
  fontSize: 34
}

const props = defineProps({
  type: {
    type: String,
    default: 'canvas' // canvas || image
  },
  useWeight: {
    type: Boolean,
    default: false
  },
  disabled: {
    type: Boolean,
    default: false
  },
  verify: {
    type: Boolean,
    default: false
  },
  canvas: {
    type: Object,
    default: () => ({
      radius: 250,
      textRadius: 190,
      textLength: 6,
      textDirection: 'horizontal',
      lineHeight: 20,
      borderWidth: 0,
      borderColor: 'transparent',
      btnText: 'GO',
      btnWidth: 140,
      fontSize: 34
    })
  },
  duration: {
    type: Number,
    default: 6000
  },
  timingFun: {
    type: String,
    default: 'cubic-bezier(0.36, 0.95, 0.64, 1)'
  },
  angleBase: {
    type: Number,
    default: 10
  },
  prizeId: {
    type: Number,
    default: 0
  },
  prizes: {
    type: Array,
    required: true,
    default: () => []
  }
})
const emits = defineEmits(['rotateStart', 'rotateEnd'])
const prizeId = toRef(props, 'prizeId')

const wheel = ref(null)
const isRotating = ref(false)
const rotateEndDeg = ref(false)
const prizeRes = ref({})

const canvasConfig = computed(() => Object.assign(canvasDefaultConfig, props.canvas))
const probabilityTotal = computed(() => {
  if (props.useWeight) return 100
  return sumBy(props.prizes, (row) => row.probability || 0)
})
const prizesIdArr = computed(() => {
  const idArr = []
  props.prizes.forEach((row) => {
    const count = props.useWeight ? row.weight || 0 : (row.probability || 0) * decimalSpaces.value
    const arr = new Array(count).fill(row.id)
    idArr.push(...arr)
  })
  return idArr
})
const decimalSpaces = computed(() => {
  if (props.useWeight) return 0
  const sortArr = [...props.prizes].sort((a, b) => {
    const aRes = String(a.probability).split('.')[1]
    const bRes = String(b.probability).split('.')[1]
    const aLen = aRes ? aRes.length : 0
    const bLen = bRes ? bRes.length : 0
    return bLen - aLen
  })
  const maxRes = String(sortArr[0].probability).split('.')[1]
  const idx = maxRes ? maxRes.length : 0
  return [1, 10, 100, 1000, 10000][idx > 4 ? 4 : idx]
})
const rotateStyle = computed(() => ({
  '-webkit-transform': `rotateZ(${rotateEndDeg.value}deg)`,
  transform: `rotateZ(${rotateEndDeg.value}deg)`,
  '-webkit-transition-duration': `${rotateDuration.value}s`,
  'transition-duration': `${rotateDuration.value}s`,
  '-webkit-transition-timing-function:': props.timingFun,
  'transition-timing-function': props.timingFun
}))

const rotateDuration = computed(() => (isRotating.value ? props.duration / 1000 : 0))
const rotateBase = computed(() => {
  let angle = props.angleBase * 360
  if (props.angleBase < 0) angle -= 360
  return angle
})
const canRotate = computed(
  () => !props.disabled && !isRotating.value && probabilityTotal.value === 100
)

function getStrArray(str, len) {
  const arr = []
  while (str !== '') {
    let text = str.substr(0, len)
    if (str.charAt(len) !== '' && str.charAt(len) !== ' ') {
      const index = text.lastIndexOf(' ')
      if (index !== -1) text = text.substr(0, index)
    }
    str = str.replace(text, '').trim()
    arr.push(text)
  }
  return arr
}

function getTargetDeg(prizeId) {
  const angle = 360 / props.prizes.length
  const num = props.prizes.findIndex((row) => row.id === prizeId)
  prizeRes.value = props.prizes[num]
  return 360 - (angle * num + angle / 2)
}

function checkProbability() {
  if (probabilityTotal.value !== 100) {
    throw new Error('Prizes Is Error: Sum of probabilities is not 100!')
  }
  return true
}

function drawPrizeText(ctx, angle, arc, name) {
  const { lineHeight, textLength, textDirection } = canvasConfig.value
  const content = getStrArray(name, textLength)
  if (content === null) return
  textDirection === 'vertical'
    ? ctx.rotate(angle + arc / 2 + Math.PI)
    : ctx.rotate(angle + arc / 2 + Math.PI / 2)
  content.forEach((text, idx) => {
    let textX = -ctx.measureText(text).width / 2
    let textY = (idx + 1) * lineHeight
    if (textDirection === 'vertical') {
      textX = 0
      textY = (idx + 1) * lineHeight - (content.length * lineHeight) / 2
    }
    ctx.fillText(text, textX, textY)
  })
}

function drawCanvas() {
  const canvasEl = wheel.value
  if (canvasEl.getContext) {
    const { radius, textRadius, borderWidth, borderColor, fontSize } = canvasConfig.value
    const arc = Math.PI / (props.prizes.length / 2)
    const ctx = canvasEl.getContext('2d')
    ctx.clearRect(0, 0, radius * 2, radius * 2)
    ctx.strokeStyle = borderColor
    ctx.lineWidth = borderWidth * 2
    ctx.font = `${fontSize}px Arial`
    props.prizes.forEach((row, i) => {
      const angle = i * arc - Math.PI / 2
      ctx.fillStyle = row.bgColor
      ctx.beginPath()
      ctx.arc(radius, radius, radius - borderWidth, angle, angle + arc, false)
      ctx.stroke()
      ctx.arc(radius, radius, 0, angle + arc, angle, true)
      ctx.fill()
      ctx.save()
      ctx.fillStyle = row.color
      ctx.translate(
        radius + Math.cos(angle + arc / 2) * textRadius,
        radius + Math.sin(angle + arc / 2) * textRadius
      )
      drawPrizeText(ctx, angle, arc, row.name)
      ctx.restore()
    })
  }
}

function handleClick() {
  if (!canRotate.value) return
  if (props.verify) {
    emits('rotateStart', onRotateStart)
    return
  }
  emits('rotateStart')
  onRotateStart()
}

function onRotateStart() {
  isRotating.value = true
  const prizeId = props.prizeId || getRandomPrize()
  rotateEndDeg.value = rotateBase.value + getTargetDeg(prizeId)
}

function onRotateEnd() {
  isRotating.value = false
  rotateEndDeg.value %= 360
  emits('rotateEnd', prizeRes.value)
}

function getRandomPrize() {
  const len = prizesIdArr.value.length
  const prizeId = prizesIdArr.value[random(0, len - 1)]
  return prizeId
}

onMounted(() => {
  checkProbability()
  if (props.type === 'canvas') drawCanvas()
})

// prizeId
watch(prizeId, (newVal) => {
  if (!isRotating.value) return
  let newAngle = getTargetDeg(newVal)
  if (props.angleBase < 0) newAngle -= 360
  const prevEndDeg = rotateEndDeg.value
  let nowEndDeg = props.angleBase * 360 + newAngle
  const angle = 360 * Math.floor((nowEndDeg - prevEndDeg) / 360)
  if (props.angleBase >= 0) {
    nowEndDeg += Math.abs(angle)
  } else {
    nowEndDeg += -360 - angle
  }
  rotateEndDeg.value = nowEndDeg
})
</script>

<style scoped lang="scss">
@import './style.scss';
</style>

I used your code snippet, but it's not working.

I used your code snippet, but it's not working.

import style.scss ?
@lantrinh1999

Please upgrade to the latest version to support vue3.

npm install vue-fortune-wheel@latest