|
@@ -0,0 +1,542 @@
|
|
|
+<template>
|
|
|
+ <div class="Metronome">
|
|
|
+ <div class="headCon"><img class="closeImg" src="/src/page-instrument/header-top/image/closeImg.png" /></div>
|
|
|
+ <div class="MetronomeBox">
|
|
|
+ <div class="beatWrap">
|
|
|
+ <div class="dot" ref="dotDom" @mousedown="handleDotMousedown" @touchstart="handleDotTouchstart"></div>
|
|
|
+ <div class="round" ref="roundDom"></div>
|
|
|
+ <canvas ref="mycanvasDom" class="mycanvas" @click="handleEventCanvas"></canvas>
|
|
|
+ <div class="beatCon">
|
|
|
+ <img
|
|
|
+ class="optImg"
|
|
|
+ src="./imgs/jia.png"
|
|
|
+ @click="
|
|
|
+ () => {
|
|
|
+ if (speedNum < 200) {
|
|
|
+ speedNum++
|
|
|
+ }
|
|
|
+ }
|
|
|
+ "
|
|
|
+ />
|
|
|
+ <div class="optMid">
|
|
|
+ <img class="yinImg" src="./imgs/yin.png" />
|
|
|
+ <div class="speedNum">{{ speedNum }}</div>
|
|
|
+ </div>
|
|
|
+ <img
|
|
|
+ class="optImg"
|
|
|
+ src="./imgs/jian.png"
|
|
|
+ @click="
|
|
|
+ () => {
|
|
|
+ if (speedNum > 50) {
|
|
|
+ speedNum--
|
|
|
+ }
|
|
|
+ }
|
|
|
+ "
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="selectCon">
|
|
|
+ <popover v-model:show="beatSymbolShow" placement="top" :show-arrow="false" class="selectBeatPopover" :offset="[0, 6]">
|
|
|
+ <div class="selectBeatCon">
|
|
|
+ <div
|
|
|
+ class="selectBeatItem"
|
|
|
+ :class="{ actBeatItem: beatSymbol === item.value }"
|
|
|
+ v-for="(item, index) in beatSymbolOpt"
|
|
|
+ :key="item.value"
|
|
|
+ >
|
|
|
+ <div :class="['beatSymbolImg', `beatSymbolImg${index + 1}`]"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <template #reference>
|
|
|
+ <div class="beatSymbolSel">
|
|
|
+ <div class="beatSymbol"></div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </popover>
|
|
|
+ </div>
|
|
|
+ <div class="sliderList">
|
|
|
+ <img src="./imgs/sound.png" alt="" />
|
|
|
+ <Slider v-model="volumeNum" :step="1">
|
|
|
+ <template #button>
|
|
|
+ <img class="thumbDot" src="./imgs/dot1.png" alt="" />
|
|
|
+ </template>
|
|
|
+ </Slider>
|
|
|
+ <span class="sliderText">{{ volumeNum }}</span>
|
|
|
+ </div>
|
|
|
+ <div v-if="playState === 'pause'" class="playBtn" @click="startPlay">
|
|
|
+ <span>播放</span>
|
|
|
+ <img src="./imgs/play.png" />
|
|
|
+ </div>
|
|
|
+ <div v-else class="playBtn" @click="pausePlay">
|
|
|
+ <span>暂停</span>
|
|
|
+ <img src="./imgs/pause.png" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, reactive, onMounted, onUnmounted, h, watch } from "vue"
|
|
|
+import { Popover, Slider } from "vant"
|
|
|
+import useMetronome from "./useMetronome"
|
|
|
+
|
|
|
+const emits = defineEmits<{
|
|
|
+ (e: "playStateChange", state: "play" | "pause"): void
|
|
|
+ (e: "close"): void
|
|
|
+}>()
|
|
|
+
|
|
|
+/* dom */
|
|
|
+const mycanvasDom = ref<HTMLCanvasElement>()
|
|
|
+const dotDom = ref<HTMLElement>()
|
|
|
+const roundDom = ref<HTMLElement>()
|
|
|
+const fontSizeScale = window.fontSize / 37.5
|
|
|
+/* select */
|
|
|
+const beatVal = ref("1")
|
|
|
+const beatValOpt = [
|
|
|
+ {
|
|
|
+ value: "0"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ value: "1"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ value: "1-1"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ value: "1-1-1"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ value: "1-1-1-1"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ value: "1-1-1-1-1"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ value: "1-1-1-1-1-1"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ value: "1-1-1-1-1-1-1"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ value: "1-1-1-1-1-1-1-1"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ value: "1-1-1-1-1-1-1-1-1"
|
|
|
+ }
|
|
|
+]
|
|
|
+const beatSymbol = ref("1")
|
|
|
+const beatSymbolShow = ref(false)
|
|
|
+const beatSymbolOpt = [
|
|
|
+ {
|
|
|
+ value: "1"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ value: "0.5-0.5"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ value: "0.3333333-0.3333333-0.3333333"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ value: "0.25-0.25-0.25-0.25"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ value: "0.6666666-0.3333333"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ value: "0.75-0.25"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ value: "0.5-0.25-0.25"
|
|
|
+ }
|
|
|
+]
|
|
|
+
|
|
|
+const { volumeNum, playState, speedNum, startPlay, pausePlay } = useMetronome(beatVal, beatSymbol)
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ getCircleBar(speedToScalc(speedNum.value))
|
|
|
+})
|
|
|
+
|
|
|
+watch(playState, () => {
|
|
|
+ emits("playStateChange", playState.value)
|
|
|
+})
|
|
|
+watch(speedNum, () => {
|
|
|
+ if (playState.value === "play") {
|
|
|
+ pausePlay()
|
|
|
+ }
|
|
|
+ getCircleBar(speedToScalc(speedNum.value))
|
|
|
+})
|
|
|
+watch([beatVal, beatSymbol], () => {
|
|
|
+ if (playState.value === "play") {
|
|
|
+ pausePlay()
|
|
|
+ }
|
|
|
+})
|
|
|
+function speedToScalc(speed: number) {
|
|
|
+ return ((speed - 50) / 150) * 100
|
|
|
+}
|
|
|
+function scalcToSpeed(scalc: number) {
|
|
|
+ return Math.round((scalc * 150) / 100 + 50)
|
|
|
+}
|
|
|
+const handleEventCanvas = (e: MouseEvent | TouchEvent) => {
|
|
|
+ const circle = mycanvasDom.value!.getBoundingClientRect()
|
|
|
+ const centerX = circle.left + circle.width / 2
|
|
|
+ const centerY = circle.top + circle.height / 2
|
|
|
+ const event = isTouchEvent(e) ? e.touches[0] : e
|
|
|
+ const angle = Math.atan2(event.clientY - centerY, event.clientX - centerX)
|
|
|
+ const percentage = (angle * (180 / Math.PI) + 180) / 360
|
|
|
+ const scalc = Math.round(percentage * 100) - 25
|
|
|
+ speedNum.value = scalcToSpeed(scalc < 0 ? 100 + scalc : scalc)
|
|
|
+}
|
|
|
+function handleDotMousedown() {
|
|
|
+ function onMouseup() {
|
|
|
+ document.removeEventListener("mousemove", onMousemove)
|
|
|
+ document.removeEventListener("mouseup", onMouseup)
|
|
|
+ }
|
|
|
+ document.addEventListener("mousemove", onMousemove)
|
|
|
+ document.addEventListener("mouseup", onMouseup)
|
|
|
+}
|
|
|
+function handleDotTouchstart() {
|
|
|
+ function onTouchend() {
|
|
|
+ document.removeEventListener("touchmove", onTouchmove)
|
|
|
+ document.removeEventListener("touchend", onTouchend)
|
|
|
+ }
|
|
|
+ document.addEventListener("touchmove", onTouchmove)
|
|
|
+ document.addEventListener("touchend", onTouchend)
|
|
|
+}
|
|
|
+function onMousemove(e: MouseEvent) {
|
|
|
+ handleEventCanvas(e)
|
|
|
+}
|
|
|
+function onTouchmove(e: TouchEvent) {
|
|
|
+ handleEventCanvas(e)
|
|
|
+}
|
|
|
+// 根据百分比画圆
|
|
|
+function getCircleBar(steps: number) {
|
|
|
+ const radius = 74 * fontSizeScale //半径
|
|
|
+ const colorList = ["#04C8BB", "#60E0C5"]
|
|
|
+ const mycanvas = mycanvasDom.value!
|
|
|
+ mycanvas.width = 153 * fontSizeScale
|
|
|
+ mycanvas.height = 153 * fontSizeScale
|
|
|
+ const ctx = mycanvas.getContext("2d")!
|
|
|
+ // 每次进来清空画布
|
|
|
+ ctx.clearRect(0, 0, mycanvas.width, mycanvas.height)
|
|
|
+ //找到画布的中心点
|
|
|
+ const canvasX = mycanvas.width / 2
|
|
|
+ const canvasY = mycanvas.height / 2
|
|
|
+ //进度条是100%,所以要把一圈360度分成100份
|
|
|
+ const progress = (Math.PI * 2) / 100
|
|
|
+ const ao = -Math.PI / 2 + steps * progress
|
|
|
+ const x1 = radius + Math.cos(ao) * radius
|
|
|
+ const y1 = radius + Math.sin(ao) * radius
|
|
|
+ // 这里开始算坐标
|
|
|
+ dotDom.value!.style.left = x1 - 5 * fontSizeScale + "Px"
|
|
|
+ dotDom.value!.style.top = y1 - 5 * fontSizeScale + "Px"
|
|
|
+ // 绘制渐变色
|
|
|
+ const gradientDirection = {
|
|
|
+ x1: 0,
|
|
|
+ y1: 0,
|
|
|
+ x2: radius * 2, // 半径*2
|
|
|
+ y2: 0
|
|
|
+ }
|
|
|
+ gradientDirection.y2 = radius * 2
|
|
|
+ const gradient = ctx.createLinearGradient(gradientDirection.x1, gradientDirection.y1, gradientDirection.x2, gradientDirection.y2)
|
|
|
+ colorList.map((color, index) => {
|
|
|
+ gradient.addColorStop(index, color)
|
|
|
+ })
|
|
|
+ ctx.strokeStyle = gradient
|
|
|
+ ctx.lineWidth = 5.1 * fontSizeScale
|
|
|
+ ctx.save()
|
|
|
+ ctx.beginPath()
|
|
|
+ ctx.arc(canvasX, canvasY, radius, -Math.PI / 2, -Math.PI / 2 + steps * progress, false)
|
|
|
+ ctx.stroke()
|
|
|
+ ctx.closePath()
|
|
|
+ ctx.restore()
|
|
|
+}
|
|
|
+function isTouchEvent(e: MouseEvent | TouchEvent): e is TouchEvent {
|
|
|
+ return window.TouchEvent && e instanceof window.TouchEvent
|
|
|
+}
|
|
|
+defineExpose({
|
|
|
+ startPlay,
|
|
|
+ pausePlay,
|
|
|
+ speedNum,
|
|
|
+ beatSymbol
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="less" scoped>
|
|
|
+.Metronome {
|
|
|
+ width: 266px;
|
|
|
+ height: 308px;
|
|
|
+ background: #ffffff;
|
|
|
+ border-radius: 16px;
|
|
|
+ position: relative;
|
|
|
+ .headCon {
|
|
|
+ position: absolute;
|
|
|
+ right: -38px;
|
|
|
+ top: -16px;
|
|
|
+ .closeImg {
|
|
|
+ width: 28px;
|
|
|
+ height: 28px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .MetronomeBox {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ z-index: 2;
|
|
|
+ }
|
|
|
+ .beatWrap {
|
|
|
+ flex-shrink: 0;
|
|
|
+ margin-top: 20px;
|
|
|
+ width: 152px;
|
|
|
+ height: 152px;
|
|
|
+ position: relative;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ .dot {
|
|
|
+ position: absolute;
|
|
|
+ width: 14px;
|
|
|
+ height: 14px;
|
|
|
+ background: url("./imgs/dot.png") no-repeat;
|
|
|
+ background-size: 100% 100%;
|
|
|
+ cursor: pointer;
|
|
|
+ z-index: 3;
|
|
|
+ }
|
|
|
+ .round {
|
|
|
+ position: absolute;
|
|
|
+ left: 0;
|
|
|
+ top: 0;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ border-radius: 50%;
|
|
|
+ border: 5px solid #d1d1d1;
|
|
|
+ z-index: 1;
|
|
|
+ }
|
|
|
+ .mycanvas {
|
|
|
+ position: absolute;
|
|
|
+ left: 0;
|
|
|
+ top: 0;
|
|
|
+ z-index: 2;
|
|
|
+ }
|
|
|
+ .beatCon {
|
|
|
+ border-radius: 50%;
|
|
|
+ z-index: 4;
|
|
|
+ width: 122px;
|
|
|
+ height: 122px;
|
|
|
+ background: url("./imgs/yuan.png") no-repeat;
|
|
|
+ background-size: 100% 100%;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ .optMid {
|
|
|
+ margin: 6px 0;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ .yinImg {
|
|
|
+ width: 16px;
|
|
|
+ height: 16px;
|
|
|
+ }
|
|
|
+ .speedNum {
|
|
|
+ margin-left: 3px;
|
|
|
+ font-weight: bold;
|
|
|
+ font-size: 26px;
|
|
|
+ color: #000000;
|
|
|
+ line-height: 30px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .optImg {
|
|
|
+ cursor: pointer;
|
|
|
+ width: 20px;
|
|
|
+ height: 20px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .selectCon {
|
|
|
+ display: flex;
|
|
|
+ margin-top: 12px;
|
|
|
+ .beatSymbolSel {
|
|
|
+ width: 78px;
|
|
|
+ height: 28px;
|
|
|
+ background: #f6f6f6;
|
|
|
+ border-radius: 14px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .sliderList {
|
|
|
+ margin-top: 13px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ & > img {
|
|
|
+ width: 18px;
|
|
|
+ height: 18px;
|
|
|
+ margin-right: 8px;
|
|
|
+ }
|
|
|
+ &:deep(.van-slider) {
|
|
|
+ height: 3px;
|
|
|
+ width: 120px;
|
|
|
+ background: #ebebeb;
|
|
|
+ border-radius: 2px;
|
|
|
+ .thumbDot {
|
|
|
+ width: 12px;
|
|
|
+ height: 12px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .sliderText {
|
|
|
+ width: 24px;
|
|
|
+ margin-left: 8px;
|
|
|
+ font-weight: 500;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #04c8bb;
|
|
|
+ line-height: 20px;
|
|
|
+ }
|
|
|
+ .thumbDot {
|
|
|
+ width: 21px;
|
|
|
+ height: 21px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .playBtn {
|
|
|
+ cursor: pointer;
|
|
|
+ margin-top: 13px;
|
|
|
+ width: 162px;
|
|
|
+ height: 36px;
|
|
|
+ background: linear-gradient(135deg, #04c8bb 0%, #60e0c5 100%);
|
|
|
+ border-radius: 18px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ font-weight: 600;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #ffffff;
|
|
|
+ line-height: 20px;
|
|
|
+ & > img {
|
|
|
+ margin-left: 4px;
|
|
|
+ width: 12px;
|
|
|
+ height: 12px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|
|
|
+<style lang="less">
|
|
|
+.selectBeatPopover {
|
|
|
+ .selectBeatCon {
|
|
|
+ width: 82px;
|
|
|
+ height: 176px;
|
|
|
+ background: #ffffff;
|
|
|
+ box-shadow: 0px 2px 9px 0px rgba(0, 0, 0, 0.09);
|
|
|
+ border-radius: 10px;
|
|
|
+ padding: 4px 6px;
|
|
|
+ overflow-y: auto;
|
|
|
+ &::-webkit-scrollbar {
|
|
|
+ width: 0;
|
|
|
+ display: none;
|
|
|
+ }
|
|
|
+ .selectBeatItem {
|
|
|
+ width: 100%;
|
|
|
+ height: 34px;
|
|
|
+ border-radius: 6px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ cursor: pointer;
|
|
|
+ &.actBeatItem {
|
|
|
+ .beatSymbolImg {
|
|
|
+ &.beatSymbolImg1 {
|
|
|
+ background: url("./imgs/a1.png") no-repeat;
|
|
|
+ background-size: 100% 100%;
|
|
|
+ }
|
|
|
+ &.beatSymbolImg2 {
|
|
|
+ background: url("./imgs/a2.png") no-repeat;
|
|
|
+ background-size: 100% 100%;
|
|
|
+ }
|
|
|
+ &.beatSymbolImg3 {
|
|
|
+ background: url("./imgs/a3.png") no-repeat;
|
|
|
+ background-size: 100% 100%;
|
|
|
+ }
|
|
|
+ &.beatSymbolImg4 {
|
|
|
+ background: url("./imgs/a4.png") no-repeat;
|
|
|
+ background-size: 100% 100%;
|
|
|
+ }
|
|
|
+ &.beatSymbolImg5 {
|
|
|
+ background: url("./imgs/a5.png") no-repeat;
|
|
|
+ background-size: 100% 100%;
|
|
|
+ }
|
|
|
+ &.beatSymbolImg6 {
|
|
|
+ background: url("./imgs/a6.png") no-repeat;
|
|
|
+ background-size: 100% 100%;
|
|
|
+ }
|
|
|
+ &.beatSymbolImg7 {
|
|
|
+ background: url("./imgs/a7.png") no-repeat;
|
|
|
+ background-size: 100% 100%;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ &:hover {
|
|
|
+ background: #04c8bb;
|
|
|
+ .beatSymbolImg {
|
|
|
+ &.beatSymbolImg1 {
|
|
|
+ background: url("./imgs/c1.png") no-repeat;
|
|
|
+ background-size: 100% 100%;
|
|
|
+ }
|
|
|
+ &.beatSymbolImg2 {
|
|
|
+ background: url("./imgs/c2.png") no-repeat;
|
|
|
+ background-size: 100% 100%;
|
|
|
+ }
|
|
|
+ &.beatSymbolImg3 {
|
|
|
+ background: url("./imgs/c3.png") no-repeat;
|
|
|
+ background-size: 100% 100%;
|
|
|
+ }
|
|
|
+ &.beatSymbolImg4 {
|
|
|
+ background: url("./imgs/c4.png") no-repeat;
|
|
|
+ background-size: 100% 100%;
|
|
|
+ }
|
|
|
+ &.beatSymbolImg5 {
|
|
|
+ background: url("./imgs/c5.png") no-repeat;
|
|
|
+ background-size: 100% 100%;
|
|
|
+ }
|
|
|
+ &.beatSymbolImg6 {
|
|
|
+ background: url("./imgs/c6.png") no-repeat;
|
|
|
+ background-size: 100% 100%;
|
|
|
+ }
|
|
|
+ &.beatSymbolImg7 {
|
|
|
+ background: url("./imgs/c7.png") no-repeat;
|
|
|
+ background-size: 100% 100%;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .beatSymbolImg {
|
|
|
+ width: 35px;
|
|
|
+ height: 37px;
|
|
|
+ &.beatSymbolImg1 {
|
|
|
+ background: url("./imgs/b1.png") no-repeat;
|
|
|
+ background-size: 100% 100%;
|
|
|
+ }
|
|
|
+ &.beatSymbolImg2 {
|
|
|
+ background: url("./imgs/b2.png") no-repeat;
|
|
|
+ background-size: 100% 100%;
|
|
|
+ }
|
|
|
+ &.beatSymbolImg3 {
|
|
|
+ background: url("./imgs/b3.png") no-repeat;
|
|
|
+ background-size: 100% 100%;
|
|
|
+ }
|
|
|
+ &.beatSymbolImg4 {
|
|
|
+ background: url("./imgs/b4.png") no-repeat;
|
|
|
+ background-size: 100% 100%;
|
|
|
+ }
|
|
|
+ &.beatSymbolImg5 {
|
|
|
+ background: url("./imgs/b5.png") no-repeat;
|
|
|
+ background-size: 100% 100%;
|
|
|
+ }
|
|
|
+ &.beatSymbolImg6 {
|
|
|
+ background: url("./imgs/b6.png") no-repeat;
|
|
|
+ background-size: 100% 100%;
|
|
|
+ }
|
|
|
+ &.beatSymbolImg7 {
|
|
|
+ background: url("./imgs/b7.png") no-repeat;
|
|
|
+ background-size: 100% 100%;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|