Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Vue 3] Component migrated #13

Closed
dathacky opened this issue Apr 21, 2023 · 4 comments
Closed

[Vue 3] Component migrated #13

dathacky opened this issue Apr 21, 2023 · 4 comments

Comments

@dathacky
Copy link

<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>

@dathacky
Copy link
Author

.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;
}

@lantrinh1999
Copy link

<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.

@dathacky
Copy link
Author

dathacky commented Apr 25, 2023

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

import style.scss ?
@lantrinh1999

@dathacky dathacky mentioned this issue Apr 25, 2023
@XiaoLin1995
Copy link
Owner

Please upgrade to the latest version to support vue3.

npm install vue-fortune-wheel@latest

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants