|
@@ -0,0 +1,1087 @@
|
|
|
+<template>
|
|
|
+ <view class="almost-lottery">
|
|
|
+ <view class="almost-lottery__wrap" :style="{ width: lotterySize + 'rpx', height: lotterySize + 'rpx' }">
|
|
|
+ <view class="lottery-action" :style="{ width: actionSize + 'rpx', height: actionSize + 'rpx', left: canvasMarginOutside + 'rpx' }"></view>
|
|
|
+ <view class="str-margin-outside" :style="{ left: strMarginOutside + 'rpx' }"></view>
|
|
|
+ <view class="img-margin-str" :style="{ left: imgMarginStr + 'rpx' }"></view>
|
|
|
+ <view class="img-size" :style="{ width: imgWidth + 'rpx', height: imgHeight + 'rpx' }"></view>
|
|
|
+ <template v-if="lotteryImg">
|
|
|
+ <image
|
|
|
+ class="almost-lottery__bg"
|
|
|
+ mode="widthFix"
|
|
|
+ :src="lotteryBg"
|
|
|
+ :style="{
|
|
|
+ width: lotteryPxSize + 'px',
|
|
|
+ height: lotteryPxSize + 'px'
|
|
|
+ }"
|
|
|
+ ></image>
|
|
|
+ <image
|
|
|
+ :class="[
|
|
|
+ 'almost-lottery__canvas-img',
|
|
|
+ { 'almost-lottery__canvas-img-other': !selfRotaty },
|
|
|
+ { 'almost-lottery__canvas-img-self': selfRotated }
|
|
|
+ ]"
|
|
|
+ mode="widthFix"
|
|
|
+ :src="lotteryImg"
|
|
|
+ :style="{
|
|
|
+ width: canvasImgPxSize + 'px',
|
|
|
+ height: canvasImgPxSize + 'px',
|
|
|
+ left: canvasImgToLeftPx + 'px',
|
|
|
+ top: canvasImgToLeftPx + 'px',
|
|
|
+ transform: `rotate(${canvasAngle + targetAngle}deg)`,
|
|
|
+ transitionDuration: `${transitionDuration}s`
|
|
|
+ }"
|
|
|
+ ></image>
|
|
|
+ <image
|
|
|
+ class="almost-lottery__action-bg"
|
|
|
+ mode="widthFix"
|
|
|
+ :src="actionBg"
|
|
|
+ :style="{
|
|
|
+ width: actionPxSize + 'px',
|
|
|
+ height: actionPxSize + 'px',
|
|
|
+ left: actionBgToLeftPx + 'px',
|
|
|
+ top: actionBgToLeftPx + 'px',
|
|
|
+ transform: `rotate(${actionAngle + targetActionAngle}deg)`,
|
|
|
+ transitionDuration: `${transitionDuration}s`
|
|
|
+ }"
|
|
|
+ @click="handleActionStart"
|
|
|
+ ></image>
|
|
|
+ </template>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 为了兼容 app 端 ctx.measureText 所需的标签 -->
|
|
|
+ <text class="almost-lottery__measureText" :style="{ fontSize: higtFontSize + 'px' }">{{ measureText }}</text>
|
|
|
+
|
|
|
+ <!-- #ifdef MP-ALIPAY -->
|
|
|
+ <canvas
|
|
|
+ :class="className"
|
|
|
+ :id="canvasId"
|
|
|
+ :width="higtCanvasSize"
|
|
|
+ :height="higtCanvasSize"
|
|
|
+ :style="{
|
|
|
+ width: higtCanvasSize + 'px',
|
|
|
+ height: higtCanvasSize + 'px'
|
|
|
+ }"
|
|
|
+ />
|
|
|
+ <!-- #endif -->
|
|
|
+ <!-- #ifndef MP-ALIPAY -->
|
|
|
+ <canvas
|
|
|
+ :class="className"
|
|
|
+ :canvas-id="canvasId"
|
|
|
+ :width="higtCanvasSize"
|
|
|
+ :height="higtCanvasSize"
|
|
|
+ :style="{
|
|
|
+ width: higtCanvasSize + 'px',
|
|
|
+ height: higtCanvasSize + 'px'
|
|
|
+ }"
|
|
|
+ />
|
|
|
+ <!-- #endif -->
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+ import { getStore, setStore, clearStore, circleImg, clacTextLen, downloadFile, pathToBase64, base64ToPath } from '@/uni_modules/almost-lottery/utils/almost-utils.js'
|
|
|
+ export default {
|
|
|
+ name: 'AlmostLottery',
|
|
|
+ props: {
|
|
|
+ // 设计稿的像素比基准值
|
|
|
+ pixelRatio: {
|
|
|
+ type: Number,
|
|
|
+ default: 2
|
|
|
+ },
|
|
|
+ // canvas 标识
|
|
|
+ canvasId: {
|
|
|
+ type: String,
|
|
|
+ default: 'almostLottery'
|
|
|
+ },
|
|
|
+ // 渲染延迟
|
|
|
+ renderDelay: {
|
|
|
+ type: Number,
|
|
|
+ default: 0
|
|
|
+ },
|
|
|
+ // 抽奖转盘的整体尺寸
|
|
|
+ lotterySize: {
|
|
|
+ type: Number,
|
|
|
+ default: 600
|
|
|
+ },
|
|
|
+ // 抽奖按钮的尺寸
|
|
|
+ actionSize: {
|
|
|
+ type: Number,
|
|
|
+ default: 200
|
|
|
+ },
|
|
|
+ // canvas边缘距离转盘边缘的距离
|
|
|
+ canvasMarginOutside: {
|
|
|
+ type: Number,
|
|
|
+ default: 90
|
|
|
+ },
|
|
|
+ // 奖品列表
|
|
|
+ prizeList: {
|
|
|
+ type: Array,
|
|
|
+ required: true,
|
|
|
+ validator: (value) => {
|
|
|
+ return value.length > 1
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 中奖奖品在列表中的下标
|
|
|
+ prizeIndex: {
|
|
|
+ type: Number,
|
|
|
+ required: true
|
|
|
+ },
|
|
|
+ // 奖品区块对应背景颜色
|
|
|
+ colors: {
|
|
|
+ type: Array,
|
|
|
+ default: () => [
|
|
|
+ '#FFFFFF',
|
|
|
+ '#FFBF05'
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ // 转盘外环背景图
|
|
|
+ lotteryBg: {
|
|
|
+ type: String,
|
|
|
+ default: '/uni_modules/almost-lottery/static/almost-lottery/almost-lottery__bg2x.png'
|
|
|
+ },
|
|
|
+ // 抽奖按钮背景图
|
|
|
+ actionBg: {
|
|
|
+ type: String,
|
|
|
+ default: '/uni_modules/almost-lottery/static/almost-lottery/almost-lottery__action2x.png'
|
|
|
+ },
|
|
|
+ // 是否绘制奖品名称
|
|
|
+ prizeNameDrawed: {
|
|
|
+ type: Boolean,
|
|
|
+ default: true
|
|
|
+ },
|
|
|
+ // 是否开启奖品区块描边
|
|
|
+ stroked: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false
|
|
|
+ },
|
|
|
+ // 描边颜色
|
|
|
+ strokeColor: {
|
|
|
+ type: String,
|
|
|
+ default: '#FFBF05'
|
|
|
+ },
|
|
|
+ // 旋转的类型
|
|
|
+ rotateType: {
|
|
|
+ type: String,
|
|
|
+ default: 'roulette'
|
|
|
+ },
|
|
|
+ // 是否开启自转
|
|
|
+ selfRotaty: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false
|
|
|
+ },
|
|
|
+ // 自转时,最少转多少毫秒
|
|
|
+ selfTime: {
|
|
|
+ type: Number,
|
|
|
+ default: 1000
|
|
|
+ },
|
|
|
+ // 旋转动画时间 单位s
|
|
|
+ duration: {
|
|
|
+ type: Number,
|
|
|
+ default: 8
|
|
|
+ },
|
|
|
+ // 旋转的圈数
|
|
|
+ ringCount: {
|
|
|
+ type: Number,
|
|
|
+ default: 8
|
|
|
+ },
|
|
|
+ // 指针位置
|
|
|
+ pointerPosition: {
|
|
|
+ type: String,
|
|
|
+ default: 'edge',
|
|
|
+ validator: (value) => {
|
|
|
+ return value === 'edge' || value === 'middle'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 文字方向
|
|
|
+ strDirection: {
|
|
|
+ type: String,
|
|
|
+ default: 'horizontal',
|
|
|
+ validator: (value) => {
|
|
|
+ return value === 'horizontal' || value === 'vertical'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 字体颜色
|
|
|
+ strFontColors: {
|
|
|
+ type: Array,
|
|
|
+ default: () => [
|
|
|
+ '#FFBF05',
|
|
|
+ '#FFFFFF'
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ // 文字的大小
|
|
|
+ strFontSize: {
|
|
|
+ type: Number,
|
|
|
+ default: 24
|
|
|
+ },
|
|
|
+ // 奖品文字距离边缘的距离
|
|
|
+ strMarginOutside: {
|
|
|
+ type: Number,
|
|
|
+ default: 0
|
|
|
+ },
|
|
|
+ // 奖品图片距离奖品文字的距离
|
|
|
+ imgMarginStr: {
|
|
|
+ type: Number,
|
|
|
+ default: 60
|
|
|
+ },
|
|
|
+ // 奖品文字多行情况下的行高
|
|
|
+ strLineHeight: {
|
|
|
+ type: Number,
|
|
|
+ default: 1.2
|
|
|
+ },
|
|
|
+ // 奖品文字总长度限制
|
|
|
+ strMaxLen: {
|
|
|
+ type: Number,
|
|
|
+ default: 12
|
|
|
+ },
|
|
|
+ // 奖品文字多行情况下第一行文字长度
|
|
|
+ strLineLen: {
|
|
|
+ type: Number,
|
|
|
+ default: 6
|
|
|
+ },
|
|
|
+ // 奖品图片的宽
|
|
|
+ imgWidth: {
|
|
|
+ type: Number,
|
|
|
+ default: 50
|
|
|
+ },
|
|
|
+ // 奖品图片的高
|
|
|
+ imgHeight: {
|
|
|
+ type: Number,
|
|
|
+ default: 50
|
|
|
+ },
|
|
|
+ // 是否绘制奖品图片
|
|
|
+ imgDrawed: {
|
|
|
+ type: Boolean,
|
|
|
+ default: true
|
|
|
+ },
|
|
|
+ // 奖品图片是否裁切为圆形
|
|
|
+ imgCircled: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false
|
|
|
+ },
|
|
|
+ // 转盘绘制成功的提示
|
|
|
+ successMsg: {
|
|
|
+ type: String,
|
|
|
+ default: '奖品准备就绪,快来参与抽奖吧'
|
|
|
+ },
|
|
|
+ // 转盘绘制失败的提示
|
|
|
+ failMsg: {
|
|
|
+ type: String,
|
|
|
+ default: '奖品仍在准备中,请稍后再来...'
|
|
|
+ },
|
|
|
+ // 是否开启画板的缓存
|
|
|
+ canvasCached: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ // 画板className
|
|
|
+ className: 'almost-lottery__canvas',
|
|
|
+ // 高清固定 2 倍,不再从 system 中动态获取,因为 h5、app-vue 中单个尺寸过大时存在 iOS/Safari 无法绘制的问题,且 2 倍基本也可以解决模糊的问题
|
|
|
+ systemPixelRatio: 2,
|
|
|
+ // 抽奖转盘的整体px尺寸
|
|
|
+ lotteryPxSize: 0,
|
|
|
+ // 画板的px尺寸
|
|
|
+ canvasImgPxSize: 0,
|
|
|
+ // 抽奖按钮的px尺寸
|
|
|
+ actionPxSize: 0,
|
|
|
+ // 奖品文字距离转盘边缘的距离
|
|
|
+ strMarginPxOutside: 0,
|
|
|
+ // 奖品图片相对奖品文字的距离
|
|
|
+ imgMarginPxStr: 0,
|
|
|
+ // 奖品图片的宽、高
|
|
|
+ imgPxWidth: 0,
|
|
|
+ imgPxHeight: 0,
|
|
|
+ // 画板导出的图片
|
|
|
+ lotteryImg: '',
|
|
|
+ // 旋转到奖品目标需要的角度
|
|
|
+ targetAngle: 0,
|
|
|
+ targetActionAngle: 0,
|
|
|
+ // 配合自转使用
|
|
|
+ selfRotated: false,
|
|
|
+ selfRotatyStartTime: null,
|
|
|
+ // 是否正在旋转
|
|
|
+ isRotate: false,
|
|
|
+ // 当前停留在那个奖品的序号
|
|
|
+ stayIndex: 0,
|
|
|
+ // 当前中奖奖品的序号
|
|
|
+ targetIndex: 0,
|
|
|
+ // 是否存在可用的缓存转盘图
|
|
|
+ isCacheImg: false,
|
|
|
+ oldLotteryImg: '',
|
|
|
+ // 解决 app 不支持 measureText 的问题
|
|
|
+ // app 已在 2.9.3 的版本中提供了对 measureText 的支持,将在后续版本逐渐稳定后移除相关兼容代码
|
|
|
+ measureText: ''
|
|
|
+ }
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ // 高清尺寸
|
|
|
+ higtCanvasSize() {
|
|
|
+ return this.canvasImgPxSize * this.systemPixelRatio
|
|
|
+ },
|
|
|
+ // 高清字体
|
|
|
+ higtFontSize() {
|
|
|
+ return Math.round(this.strFontSize / this.pixelRatio) * this.systemPixelRatio
|
|
|
+ },
|
|
|
+ // 高清行高
|
|
|
+ higtHeightMultiple() {
|
|
|
+ return Math.round(this.strFontSize / this.pixelRatio) * this.strLineHeight * this.systemPixelRatio
|
|
|
+ },
|
|
|
+ canvasImgToLeftPx () {
|
|
|
+ return (this.lotteryPxSize - this.canvasImgPxSize) / 2
|
|
|
+ },
|
|
|
+ actionBgToLeftPx () {
|
|
|
+ return (this.lotteryPxSize - this.actionPxSize) / 2
|
|
|
+ },
|
|
|
+ // 根据奖品列表计算 canvas 旋转角度
|
|
|
+ canvasAngle() {
|
|
|
+ let result = 0
|
|
|
+
|
|
|
+ let prizeCount = this.prizeList.length
|
|
|
+ let prizeClip = 360 / prizeCount
|
|
|
+ let diffNum = 90 / prizeClip
|
|
|
+ if (this.pointerPosition === 'edge' || this.rotateType === 'pointer') {
|
|
|
+ result = -(prizeClip * diffNum)
|
|
|
+ } else {
|
|
|
+ result = -(prizeClip * diffNum + prizeClip / 2)
|
|
|
+ }
|
|
|
+ return result
|
|
|
+ },
|
|
|
+ actionAngle() {
|
|
|
+ return 0
|
|
|
+ },
|
|
|
+ // 外圆的半径
|
|
|
+ outsideRadius() {
|
|
|
+ return this.higtCanvasSize / 2
|
|
|
+ },
|
|
|
+ // 内圆的半径
|
|
|
+ insideRadius() {
|
|
|
+ return 20 * this.systemPixelRatio
|
|
|
+ },
|
|
|
+ // 文字距离边缘的距离
|
|
|
+ textRadius() {
|
|
|
+ return this.strMarginPxOutside * this.systemPixelRatio || (this.higtFontSize / 2)
|
|
|
+ },
|
|
|
+ // 根据画板的宽度计算奖品文字与中心点的距离
|
|
|
+ textDistance() {
|
|
|
+ const textZeroY = Math.round(this.outsideRadius - (this.insideRadius / 2))
|
|
|
+ return textZeroY - this.textRadius
|
|
|
+ },
|
|
|
+ // 旋转动画时间 单位 s
|
|
|
+ transitionDuration () {
|
|
|
+ return this.selfRotaty ? 2 : this.duration
|
|
|
+ }
|
|
|
+ },
|
|
|
+ watch: {
|
|
|
+ // 监听获奖序号的变动
|
|
|
+ prizeIndex(newVal, oldVal) {
|
|
|
+ if (newVal > -1) {
|
|
|
+ if (this.selfRotaty) {
|
|
|
+ const diffTime = Date.now() - this.selfRotatyStartTime
|
|
|
+ const timeDelay = diffTime < this.selfTime ? this.selfTime : 0
|
|
|
+ setTimeout(() => {
|
|
|
+ this.selfRotated = false
|
|
|
+ this.targetIndex = newVal
|
|
|
+ this.onRotateStart()
|
|
|
+ }, timeDelay)
|
|
|
+ } else {
|
|
|
+ setTimeout(() => {
|
|
|
+ this.targetIndex = newVal
|
|
|
+ this.onRotateStart()
|
|
|
+ }, 0)
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ console.info('旋转结束,prizeIndex 已重置')
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ // 开始旋转
|
|
|
+ onRotateStart() {
|
|
|
+ // 奖品总数
|
|
|
+ if (!this.selfRotaty) {
|
|
|
+ if (this.isRotate) return
|
|
|
+ this.isRotate = true
|
|
|
+ }
|
|
|
+
|
|
|
+ let prizeCount = this.prizeList.length
|
|
|
+ let baseAngle = 360 / prizeCount
|
|
|
+ let angles = 0
|
|
|
+
|
|
|
+ let ringCount = this.selfRotaty ? 1 : this.ringCount
|
|
|
+
|
|
|
+ if (this.rotateType === 'pointer') {
|
|
|
+ if (this.targetActionAngle === 0) {
|
|
|
+ // 第一次旋转
|
|
|
+ angles = (this.targetIndex - this.stayIndex) * baseAngle + baseAngle / 2 - this.actionAngle
|
|
|
+ } else {
|
|
|
+ // 后续旋转
|
|
|
+ // 后续继续旋转 就只需要计算停留的位置与目标位置的角度
|
|
|
+ angles = (this.targetIndex - this.stayIndex) * baseAngle
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新目前序号
|
|
|
+ this.stayIndex = this.targetIndex
|
|
|
+ // 转 8 圈,圈数越多,转的越快
|
|
|
+ this.targetActionAngle += angles + 360 * ringCount
|
|
|
+ // console.log('targetActionAngle', this.targetActionAngle)
|
|
|
+ } else {
|
|
|
+ if (this.targetAngle === 0) {
|
|
|
+ // 第一次旋转
|
|
|
+ // 因为第一个奖品是从0°开始的,即水平向右方向
|
|
|
+ // 第一次旋转角度 = 270度 - (停留的序号-目标序号) * 每个奖品区间角度 - 每个奖品区间角度的一半 - canvas自身旋转的度数
|
|
|
+ angles = (270 - (this.targetIndex - this.stayIndex) * baseAngle - baseAngle / 2) - this.canvasAngle
|
|
|
+ } else {
|
|
|
+ // 后续旋转
|
|
|
+ // 后续继续旋转 就只需要计算停留的位置与目标位置的角度
|
|
|
+ angles = -(this.targetIndex - this.stayIndex) * baseAngle
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新目前序号
|
|
|
+ this.stayIndex = this.targetIndex
|
|
|
+ // 转 8 圈,圈数越多,转的越快
|
|
|
+ this.targetAngle += angles + 360 * ringCount
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计算转盘结束的时间,预加一些延迟确保转盘停止后触发结束事件
|
|
|
+ let endTime = this.selfRotaty ? 0 : (this.transitionDuration * 1000 + 100)
|
|
|
+ let endTimer = setTimeout(() => {
|
|
|
+ clearTimeout(endTimer)
|
|
|
+ endTimer = null
|
|
|
+
|
|
|
+ this.isRotate = false
|
|
|
+ this.$emit('draw-end')
|
|
|
+ }, endTime)
|
|
|
+
|
|
|
+ let resetPrizeTimer = setTimeout(() => {
|
|
|
+ clearTimeout(resetPrizeTimer)
|
|
|
+ resetPrizeTimer = null
|
|
|
+
|
|
|
+ // 每次抽奖结束后都要重置父级组件的 prizeIndex
|
|
|
+ this.$emit('reset-index')
|
|
|
+ }, endTime + 50)
|
|
|
+ },
|
|
|
+ // 点击 开始抽奖 按钮
|
|
|
+ handleActionStart() {
|
|
|
+ if (!this.lotteryImg) return
|
|
|
+ if (this.isRotate) return
|
|
|
+
|
|
|
+ this.$emit('draw-before', (shouldContinue) => {
|
|
|
+ console.log('shouldContinue', shouldContinue)
|
|
|
+ if (!shouldContinue) return
|
|
|
+
|
|
|
+ const ringDuration = (this.duration / this.ringCount).toFixed(1)
|
|
|
+ if (ringDuration >= 2.5) {
|
|
|
+ console.warn('当前每一圈的旋转可能过慢,请检查 duration 和 ringCount 这 2 个参数是否设置合理')
|
|
|
+ } else if (ringDuration < 1) {
|
|
|
+ console.warn('当前每一圈的旋转可能过快,请检查 duration 和 ringCount 这 2 个参数是否设置合理')
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.selfRotaty) {
|
|
|
+ this.isRotate = true
|
|
|
+ this.selfRotated = true
|
|
|
+ this.selfRotatyStartTime = Date.now()
|
|
|
+ }
|
|
|
+
|
|
|
+ this.$emit('draw-start')
|
|
|
+ })
|
|
|
+ },
|
|
|
+ // 渲染转盘
|
|
|
+ async onCreateCanvas() {
|
|
|
+ // 获取 canvas 画布
|
|
|
+ const canvasId = this.canvasId
|
|
|
+ const ctx = uni.createCanvasContext(canvasId, this)
|
|
|
+
|
|
|
+ // canvas 的宽高
|
|
|
+ let canvasW = this.higtCanvasSize
|
|
|
+ let canvasH = this.higtCanvasSize
|
|
|
+
|
|
|
+ // 根据奖品个数计算 角度
|
|
|
+ let prizeCount = this.prizeList.length
|
|
|
+ let baseAngle = Math.PI * 2 / prizeCount
|
|
|
+
|
|
|
+ // 设置字体
|
|
|
+ ctx.setFontSize(this.higtFontSize)
|
|
|
+
|
|
|
+ // 注意,开始画的位置是从0°角的位置开始画的。也就是水平向右的方向。
|
|
|
+ // 画具体内容
|
|
|
+ for (let i = 0; i < prizeCount; i++) {
|
|
|
+ let prizeItem = this.prizeList[i]
|
|
|
+ // 当前角度
|
|
|
+ let angle = i * baseAngle
|
|
|
+
|
|
|
+ // 保存当前画布的状态
|
|
|
+ ctx.save()
|
|
|
+
|
|
|
+ // x => 圆弧对应的圆心横坐标 x
|
|
|
+ // y => 圆弧对应的圆心横坐标 y
|
|
|
+ // radius => 圆弧的半径大小
|
|
|
+ // startAngle => 圆弧开始的角度,单位是弧度
|
|
|
+ // endAngle => 圆弧结束的角度,单位是弧度
|
|
|
+ // anticlockwise(可选) => 绘制方向,true 为逆时针,false 为顺时针
|
|
|
+
|
|
|
+ ctx.beginPath()
|
|
|
+ // 外圆
|
|
|
+ ctx.arc(canvasW * 0.5, canvasH * 0.5, this.outsideRadius, angle, angle + baseAngle, false)
|
|
|
+ // 内圆
|
|
|
+ ctx.arc(canvasW * 0.5, canvasH * 0.5, this.insideRadius, angle + baseAngle, angle, true)
|
|
|
+
|
|
|
+ // 每个奖品区块背景填充颜色
|
|
|
+ if (this.colors.length === 2) {
|
|
|
+ ctx.setFillStyle(this.colors[i % 2])
|
|
|
+ } else {
|
|
|
+ ctx.setFillStyle(this.colors[i])
|
|
|
+ }
|
|
|
+ // 填充颜色
|
|
|
+ ctx.fill()
|
|
|
+
|
|
|
+ // 开启描边
|
|
|
+ if (this.stroked) {
|
|
|
+ // 设置描边颜色
|
|
|
+ ctx.setStrokeStyle(`${this.strokeColor}`)
|
|
|
+ // 描边
|
|
|
+ ctx.stroke()
|
|
|
+ }
|
|
|
+
|
|
|
+ // 开始绘制奖品内容
|
|
|
+ // 重新映射画布上的 (0,0) 位置
|
|
|
+ let translateX = canvasW * 0.5 + Math.cos(angle + baseAngle / 2) * this.textDistance
|
|
|
+ let translateY = canvasH * 0.5 + Math.sin(angle + baseAngle / 2) * this.textDistance
|
|
|
+ ctx.translate(translateX, translateY)
|
|
|
+
|
|
|
+ // 绘制奖品名称
|
|
|
+ let rewardName = this.strLimit(prizeItem.prizeName)
|
|
|
+
|
|
|
+ // 设置文字颜色
|
|
|
+ if (this.strFontColors.length === 1) {
|
|
|
+ ctx.setFillStyle(this.strFontColors[0])
|
|
|
+ } else if (this.strFontColors.length === 2) {
|
|
|
+ ctx.setFillStyle(this.strFontColors[i % 2])
|
|
|
+ } else {
|
|
|
+ ctx.setFillStyle(this.strFontColors[i])
|
|
|
+ }
|
|
|
+
|
|
|
+ // rotate方法旋转当前的绘图,因为文字是和当前扇形中心线垂直的
|
|
|
+ ctx.rotate(angle + (baseAngle / 2) + (Math.PI / 2))
|
|
|
+
|
|
|
+ // 设置文本位置并处理换行
|
|
|
+ if (this.strDirection === 'horizontal') {
|
|
|
+ // 是否需要换行
|
|
|
+ if (rewardName && this.prizeNameDrawed) {
|
|
|
+ let realLen = clacTextLen(rewardName).realLen
|
|
|
+ let isLineBreak = realLen > this.strLineLen
|
|
|
+ if (isLineBreak) {
|
|
|
+ // 获得多行文本数组
|
|
|
+ let textCount = 0
|
|
|
+ let tempTxt = ''
|
|
|
+ let rewardNames = []
|
|
|
+ for (let j = 0; j < rewardName.length; j++) {
|
|
|
+ textCount += clacTextLen(rewardName[j]).byteLen
|
|
|
+ tempTxt += rewardName[j]
|
|
|
+
|
|
|
+ if (textCount >= (this.strLineLen * 2)) {
|
|
|
+ rewardNames.push(tempTxt)
|
|
|
+ textCount = 0
|
|
|
+ tempTxt = ''
|
|
|
+ } else {
|
|
|
+ if ((rewardName.length - 1) === j) {
|
|
|
+ rewardNames.push(tempTxt)
|
|
|
+ textCount = 0
|
|
|
+ tempTxt = ''
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 循环文本数组,计算每一行的文本宽度
|
|
|
+ for (let j = 0; j < rewardNames.length; j++) {
|
|
|
+ if (ctx.measureText && ctx.measureText(rewardNames[j]).width > 0) {
|
|
|
+ // 文本的宽度信息
|
|
|
+ let tempStrSize = ctx.measureText(rewardNames[j])
|
|
|
+ let tempStrWidth = -(tempStrSize.width / 2).toFixed(2)
|
|
|
+ ctx.fillText(rewardNames[j], tempStrWidth, j * this.higtHeightMultiple)
|
|
|
+ } else {
|
|
|
+ this.measureText = rewardNames[j]
|
|
|
+
|
|
|
+ // 等待页面重新渲染
|
|
|
+ await this.$nextTick()
|
|
|
+
|
|
|
+ let textWidth = await this.getTextWidth()
|
|
|
+ let tempStrWidth = -(textWidth / 2).toFixed(2)
|
|
|
+ ctx.fillText(rewardNames[j], tempStrWidth, j * this.higtHeightMultiple)
|
|
|
+ // console.log(rewardNames[j], textWidth, j)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ if (ctx.measureText && ctx.measureText(rewardName).width > 0) {
|
|
|
+ // 文本的宽度信息
|
|
|
+ let tempStrSize = ctx.measureText(rewardName)
|
|
|
+ let tempStrWidth = -(tempStrSize.width / 2).toFixed(2)
|
|
|
+ ctx.fillText(rewardName, tempStrWidth, 0)
|
|
|
+ } else {
|
|
|
+ this.measureText = rewardName
|
|
|
+
|
|
|
+ // 等待页面重新渲染
|
|
|
+ await this.$nextTick()
|
|
|
+
|
|
|
+ let textWidth = await this.getTextWidth()
|
|
|
+ let tempStrWidth = -(textWidth / 2).toFixed(2)
|
|
|
+ ctx.fillText(rewardName, tempStrWidth, 0)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ let rewardNames = rewardName.split('')
|
|
|
+ for (let j = 0; j < rewardNames.length; j++) {
|
|
|
+ if (ctx.measureText && ctx.measureText(rewardNames[j]).width > 0) {
|
|
|
+ // 文本的宽度信息
|
|
|
+ let tempStrSize = ctx.measureText(rewardNames[j])
|
|
|
+ let tempStrWidth = -(tempStrSize.width / 2).toFixed(2)
|
|
|
+ ctx.fillText(rewardNames[j], tempStrWidth, j * this.higtHeightMultiple)
|
|
|
+ } else {
|
|
|
+ this.measureText = rewardNames[j]
|
|
|
+
|
|
|
+ // 等待页面重新渲染
|
|
|
+ await this.$nextTick()
|
|
|
+
|
|
|
+ let textWidth = await this.getTextWidth()
|
|
|
+ let tempStrWidth = -(textWidth / 2).toFixed(2)
|
|
|
+ ctx.fillText(rewardNames[j], tempStrWidth, j * this.higtHeightMultiple)
|
|
|
+ // console.log(rewardNames[j], textWidth, i)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ // 绘制奖品图片,文字竖向展示时,不支持图片展示
|
|
|
+ if (this.imgDrawed && prizeItem.prizeImage && this.strDirection !== 'vertical') {
|
|
|
+ // App-Android平台 系统 webview 更新到 Chrome84+ 后 canvas 组件绘制本地图像 uni.canvasToTempFilePath 会报错
|
|
|
+ // 统一将图片处理成 base64
|
|
|
+ // https://ask.dcloud.net.cn/question/103303
|
|
|
+ let reg = /^(https|http)/g
|
|
|
+ // 处理远程图片
|
|
|
+ if (reg.test(prizeItem.prizeImage)) {
|
|
|
+ let platformTips = ''
|
|
|
+ // #ifdef APP-PLUS
|
|
|
+ platformTips = ''
|
|
|
+ // #endif
|
|
|
+ // #ifdef MP
|
|
|
+ platformTips = '需要处理好下载域名的白名单问题,'
|
|
|
+ // #endif
|
|
|
+ // #ifdef H5
|
|
|
+ platformTips = '需要处理好跨域问题,'
|
|
|
+ // #endif
|
|
|
+ console.warn(`###当前数据列表中的奖品图片为网络图片,${platformTips}开始尝试下载图片...###`)
|
|
|
+ let res = await downloadFile(prizeItem.prizeImage)
|
|
|
+ console.log('处理远程图片', res)
|
|
|
+ if (res.ok) {
|
|
|
+ let tempFilePath = res.tempFilePath
|
|
|
+ // #ifndef MP
|
|
|
+ prizeItem.prizeImage = await pathToBase64(tempFilePath)
|
|
|
+ // #endif
|
|
|
+ // #ifdef MP
|
|
|
+ prizeItem.prizeImage = tempFilePath
|
|
|
+ // #endif
|
|
|
+ } else {
|
|
|
+ this.handlePrizeImgSuc({
|
|
|
+ ok: false,
|
|
|
+ data: res.data,
|
|
|
+ msg: res.msg
|
|
|
+ })
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // #ifndef MP
|
|
|
+ // 不是小程序环境,把本地图片处理成 base64
|
|
|
+ if (prizeItem.prizeImage.indexOf(';base64,') === -1) {
|
|
|
+ console.log('开始处理本地图片', prizeItem.prizeImage)
|
|
|
+ prizeItem.prizeImage = await pathToBase64(prizeItem.prizeImage)
|
|
|
+ console.log('处理本地图片结束', prizeItem.prizeImage)
|
|
|
+ }
|
|
|
+ // #endif
|
|
|
+
|
|
|
+ // #ifdef MP-WEIXIN
|
|
|
+ // 小程序环境,把 base64 处理成小程序的本地临时路径
|
|
|
+ if (prizeItem.prizeImage.indexOf(';base64,') !== -1) {
|
|
|
+ console.log('开始处理BASE64图片', prizeItem.prizeImage)
|
|
|
+ prizeItem.prizeImage = await base64ToPath(prizeItem.prizeImage)
|
|
|
+ console.log('处理BASE64图片完成', prizeItem.prizeImage)
|
|
|
+ }
|
|
|
+ // #endif
|
|
|
+ }
|
|
|
+
|
|
|
+ let prizeImageX = -(this.imgPxWidth * this.systemPixelRatio / 2)
|
|
|
+ let prizeImageY = this.imgMarginPxStr * this.systemPixelRatio
|
|
|
+ let prizeImageW = this.imgPxWidth * this.systemPixelRatio
|
|
|
+ let prizeImageH = this.imgPxHeight * this.systemPixelRatio
|
|
|
+ if (this.imgCircled) {
|
|
|
+ // 重新设置每个圆形的背景色
|
|
|
+ if (this.colors.length === 2) {
|
|
|
+ ctx.setFillStyle(this.colors[i % 2])
|
|
|
+ } else {
|
|
|
+ ctx.setFillStyle(this.colors[i])
|
|
|
+ }
|
|
|
+ circleImg(ctx, prizeItem.prizeImage, prizeImageX, prizeImageY, prizeImageW, prizeImageH)
|
|
|
+ } else {
|
|
|
+ ctx.drawImage(prizeItem.prizeImage, prizeImageX, prizeImageY, prizeImageW, prizeImageH)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ctx.restore()
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保存绘图并导出图片
|
|
|
+ ctx.draw(true, () => {
|
|
|
+ let drawTimer = setTimeout(() => {
|
|
|
+ clearTimeout(drawTimer)
|
|
|
+ drawTimer = null
|
|
|
+
|
|
|
+ // #ifdef MP-ALIPAY
|
|
|
+ ctx.toTempFilePath({
|
|
|
+ destWidth: this.higtCanvasSize,
|
|
|
+ destHeight: this.higtCanvasSize,
|
|
|
+ success: (res) => {
|
|
|
+ // console.log(res.apFilePath)
|
|
|
+ this.handlePrizeImg({
|
|
|
+ ok: true,
|
|
|
+ data: res.apFilePath,
|
|
|
+ msg: '画布导出生成图片成功'
|
|
|
+ })
|
|
|
+ },
|
|
|
+ fail: (err) => {
|
|
|
+ this.handlePrizeImg({
|
|
|
+ ok: false,
|
|
|
+ data: err,
|
|
|
+ msg: '画布导出生成图片失败'
|
|
|
+ })
|
|
|
+ }
|
|
|
+ })
|
|
|
+ // #endif
|
|
|
+
|
|
|
+ // #ifndef MP-ALIPAY
|
|
|
+ uni.canvasToTempFilePath({
|
|
|
+ canvasId: this.canvasId,
|
|
|
+ destWidth: this.higtCanvasSize,
|
|
|
+ destHeight: this.higtCanvasSize,
|
|
|
+ success: (res) => {
|
|
|
+ // 在 H5 平台下,tempFilePath 为 base64
|
|
|
+ // console.log(res.tempFilePath)
|
|
|
+ this.handlePrizeImg({
|
|
|
+ ok: true,
|
|
|
+ data: res.tempFilePath,
|
|
|
+ msg: '画布导出生成图片成功'
|
|
|
+ })
|
|
|
+ },
|
|
|
+ fail: (err) => {
|
|
|
+ this.handlePrizeImg({
|
|
|
+ ok: false,
|
|
|
+ data: err,
|
|
|
+ msg: '画布导出生成图片失败'
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }, this)
|
|
|
+ // #endif
|
|
|
+ }, 500)
|
|
|
+ })
|
|
|
+ },
|
|
|
+ // 处理导出的图片
|
|
|
+ handlePrizeImg(res) {
|
|
|
+ if (res.ok) {
|
|
|
+ let data = res.data
|
|
|
+
|
|
|
+ if (!this.canvasCached) {
|
|
|
+ this.lotteryImg = data
|
|
|
+ this.handlePrizeImgSuc(res)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // #ifndef H5
|
|
|
+ if (this.isCacheImg) {
|
|
|
+ uni.getSavedFileList({
|
|
|
+ success: (sucRes) => {
|
|
|
+ let fileList = sucRes.fileList
|
|
|
+ // console.log('getSavedFileList Cached', fileList)
|
|
|
+
|
|
|
+ let cached = false
|
|
|
+
|
|
|
+ if (fileList.length) {
|
|
|
+ for (let i = 0; i < fileList.length; i++) {
|
|
|
+ let item = fileList[i]
|
|
|
+ if (item.filePath === data) {
|
|
|
+ cached = true
|
|
|
+ this.lotteryImg = data
|
|
|
+
|
|
|
+ console.info('经查,本地缓存中存在的转盘图可用,本次将不再绘制转盘')
|
|
|
+ this.handlePrizeImgSuc(res)
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!cached) {
|
|
|
+ console.info('经查,本地缓存中存在的转盘图不可用,需要重新初始化转盘绘制')
|
|
|
+ this.initCanvasDraw()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ fail: (err) => {
|
|
|
+ this.initCanvasDraw()
|
|
|
+ }
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ uni.saveFile({
|
|
|
+ tempFilePath: data,
|
|
|
+ success: (sucRes) => {
|
|
|
+ let filePath = sucRes.savedFilePath
|
|
|
+ // console.log('saveFile', filePath)
|
|
|
+ setStore(`${this.canvasId}LotteryImg`, filePath)
|
|
|
+ this.lotteryImg = filePath
|
|
|
+ this.handlePrizeImgSuc({
|
|
|
+ ok: true,
|
|
|
+ data: filePath,
|
|
|
+ msg: '画布导出生成图片成功'
|
|
|
+ })
|
|
|
+ },
|
|
|
+ fail: (err) => {
|
|
|
+ this.handlePrizeImg({
|
|
|
+ ok: false,
|
|
|
+ data: err,
|
|
|
+ msg: '画布导出生成图片失败'
|
|
|
+ })
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ // #endif
|
|
|
+ // #ifdef H5
|
|
|
+ setStore(`${this.canvasId}LotteryImg`, data)
|
|
|
+ this.lotteryImg = data
|
|
|
+ this.handlePrizeImgSuc(res)
|
|
|
+
|
|
|
+ // console info
|
|
|
+ let consoleText = this.isCacheImg ? '缓存' : '导出'
|
|
|
+ console.info(`当前为 H5 端,使用${consoleText}中的 base64 图`)
|
|
|
+ // #endif
|
|
|
+ } else {
|
|
|
+ console.error(res.msg, res)
|
|
|
+ // #ifdef H5
|
|
|
+ console.error('###当前为 H5 端,下载网络图片需要后端配置允许跨域###')
|
|
|
+ // #endif
|
|
|
+ // #ifdef MP
|
|
|
+ console.error('###当前为小程序端,下载网络图片需要配置域名白名单###')
|
|
|
+ // #endif
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 处理图片完成
|
|
|
+ handlePrizeImgSuc (res) {
|
|
|
+ this.$emit('finish', {
|
|
|
+ ok: res.ok,
|
|
|
+ data: res.data,
|
|
|
+ msg: res.ok ? this.successMsg : this.failMsg
|
|
|
+ })
|
|
|
+ },
|
|
|
+ // 兼容 app 端不支持 ctx.measureText
|
|
|
+ // 已知问题:初始绘制时,低端安卓机 平均耗时 2s
|
|
|
+ // hbx 2.8.12+ 已在 app 端支持
|
|
|
+ getTextWidth() {
|
|
|
+ console.warn('正在采用兼容方式获取文本的 size 信息')
|
|
|
+ let query = uni.createSelectorQuery().in(this)
|
|
|
+ let nodesRef = query.select('.almost-lottery__measureText')
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ nodesRef.fields({
|
|
|
+ size: true,
|
|
|
+ }, (res) => {
|
|
|
+ resolve(res.width)
|
|
|
+ }).exec()
|
|
|
+ })
|
|
|
+ },
|
|
|
+ // 处理文字溢出
|
|
|
+ strLimit(value) {
|
|
|
+ let maxLength = this.strMaxLen
|
|
|
+ if (!value || !maxLength) return value
|
|
|
+ return clacTextLen(value).realLen > maxLength ? value.slice(0, maxLength - 1) + '..' : value
|
|
|
+ },
|
|
|
+ // 检查本地缓存中是否存在转盘图
|
|
|
+ checkCacheImg () {
|
|
|
+ console.log('检查本地缓存中是否存在转盘图')
|
|
|
+ // 检查是否已有缓存的转盘图
|
|
|
+ // 检查是否与本次奖品数据相同
|
|
|
+ this.oldLotteryImg = getStore(`${this.canvasId}LotteryImg`)
|
|
|
+ let oldPrizeList = getStore(`${this.canvasId}PrizeList`)
|
|
|
+ let newPrizeList = JSON.stringify(this.prizeList)
|
|
|
+ if (this.oldLotteryImg) {
|
|
|
+ console.log(`经查,本地缓存中存在转盘图 => ${this.oldLotteryImg},继续判断这张缓存图是否可用`)
|
|
|
+ if (oldPrizeList === newPrizeList) {
|
|
|
+ this.isCacheImg = true
|
|
|
+
|
|
|
+ console.log('缓存图可用')
|
|
|
+ this.handlePrizeImg({
|
|
|
+ ok: true,
|
|
|
+ data: this.oldLotteryImg,
|
|
|
+ msg: '画布导出生成图片成功'
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ this.initCanvasDraw()
|
|
|
+ },
|
|
|
+ // 初始化绘制
|
|
|
+ initCanvasDraw () {
|
|
|
+ console.log('开始初始化转盘绘制')
|
|
|
+ this.isCacheImg = false
|
|
|
+ this.lotteryImg = ''
|
|
|
+ clearStore(`${this.canvasId}LotteryImg`)
|
|
|
+ setStore(`${this.canvasId}PrizeList`, this.prizeList)
|
|
|
+ this.onCreateCanvas()
|
|
|
+ },
|
|
|
+ // 预处理初始化
|
|
|
+ async beforeInit () {
|
|
|
+ let query = uni.createSelectorQuery().in(this)
|
|
|
+ // 处理 rpx 自适应尺寸
|
|
|
+ let lotterySize = await new Promise((resolve) => {
|
|
|
+ query.select('.almost-lottery__wrap').boundingClientRect((rects) => {
|
|
|
+ resolve(rects)
|
|
|
+ // console.log('处理 lottery rpx 的自适应', rects)
|
|
|
+ }).exec()
|
|
|
+ })
|
|
|
+ let actionSize = await new Promise((resolve) => {
|
|
|
+ query.select('.lottery-action').boundingClientRect((rects) => {
|
|
|
+ resolve(rects)
|
|
|
+ // console.log('处理 action rpx 的自适应', rects)
|
|
|
+ }).exec()
|
|
|
+ })
|
|
|
+ let strMarginSize = await new Promise((resolve) => {
|
|
|
+ query.select('.str-margin-outside').boundingClientRect((rects) => {
|
|
|
+ resolve(rects)
|
|
|
+ // console.log('处理 str-margin-outside rpx 的自适应', rects)
|
|
|
+ }).exec()
|
|
|
+ })
|
|
|
+ let imgMarginStr = await new Promise((resolve) => {
|
|
|
+ query.select('.img-margin-str').boundingClientRect((rects) => {
|
|
|
+ resolve(rects)
|
|
|
+ // console.log('处理 img-margin-str rpx 的自适应', rects)
|
|
|
+ }).exec()
|
|
|
+ })
|
|
|
+ let imgSize = await new Promise((resolve) => {
|
|
|
+ query.select('.img-size').boundingClientRect((rects) => {
|
|
|
+ resolve(rects)
|
|
|
+ // console.log('处理 img-size rpx 的自适应', rects)
|
|
|
+ }).exec()
|
|
|
+ })
|
|
|
+
|
|
|
+ this.lotteryPxSize = Math.floor(lotterySize.width)
|
|
|
+ this.canvasImgPxSize = this.lotteryPxSize - Math.floor(actionSize.left) + Math.floor(lotterySize.left)
|
|
|
+ this.actionPxSize = Math.floor(actionSize.width)
|
|
|
+
|
|
|
+ this.strMarginPxOutside = Math.floor(strMarginSize.left) - Math.floor(lotterySize.left)
|
|
|
+ this.imgMarginPxStr = Math.floor(imgMarginStr.left) - Math.floor(lotterySize.left)
|
|
|
+ this.imgPxWidth = Math.floor(imgSize.width)
|
|
|
+ this.imgPxHeight = Math.floor(imgSize.height)
|
|
|
+
|
|
|
+ // console.log(this.lotteryPxSize, this.canvasImgPxSize, this.actionPxSize)
|
|
|
+
|
|
|
+ let stoTimer = setTimeout(() => {
|
|
|
+ clearTimeout(stoTimer)
|
|
|
+ stoTimer = null
|
|
|
+
|
|
|
+ // 判断画板是否设置缓存
|
|
|
+ if (this.canvasCached) {
|
|
|
+ this.checkCacheImg()
|
|
|
+ } else {
|
|
|
+ this.initCanvasDraw()
|
|
|
+ }
|
|
|
+ }, 50)
|
|
|
+ }
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ this.$nextTick(() => {
|
|
|
+ let delay = 50 + this.renderDelay
|
|
|
+
|
|
|
+ let stoTimer = setTimeout(() => {
|
|
|
+ clearTimeout(stoTimer)
|
|
|
+ stoTimer = null
|
|
|
+
|
|
|
+ this.beforeInit()
|
|
|
+ }, delay)
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+ .almost-lottery {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 以下元素不可见,是 canvas 的实例
|
|
|
+ .almost-lottery__canvas {
|
|
|
+ position: absolute;
|
|
|
+ left: -9999px;
|
|
|
+ opacity: 0;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 以下元素不可见,用于获得自适应的值
|
|
|
+ .lottery-action,
|
|
|
+ .str-margin-outside,
|
|
|
+ .img-margin-str,
|
|
|
+ .img-size {
|
|
|
+ position: absolute;
|
|
|
+ left: 0;
|
|
|
+ top: 0;
|
|
|
+ z-index: -1;
|
|
|
+ // background-color: blue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 以下元素不可见,用于计算文本的宽度
|
|
|
+ .almost-lottery__measureText {
|
|
|
+ position: absolute;
|
|
|
+ left: 0;
|
|
|
+ top: 0;
|
|
|
+ white-space: nowrap;
|
|
|
+ font-size: 12px;
|
|
|
+ opacity: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 以下为可见内容的样式
|
|
|
+ .almost-lottery__wrap {
|
|
|
+ position: relative;
|
|
|
+ // display: flex;
|
|
|
+ // justify-content: center;
|
|
|
+ // align-items: center;
|
|
|
+ // background-color: #FFFFFF;
|
|
|
+ }
|
|
|
+
|
|
|
+ .almost-lottery__bg,
|
|
|
+ .almost-lottery__canvas-img,
|
|
|
+ .almost-lottery__action-bg {
|
|
|
+ position: absolute;
|
|
|
+ left: 0;
|
|
|
+ top: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .almost-lottery__canvas-img-other {
|
|
|
+ transition: transform cubic-bezier(.34, .12, .05, .95);
|
|
|
+ }
|
|
|
+
|
|
|
+ @keyframes selfRotate {
|
|
|
+ 0% {
|
|
|
+ transform: rotate(0deg);
|
|
|
+ }
|
|
|
+ 50% {
|
|
|
+ transform: rotate(180deg);
|
|
|
+ }
|
|
|
+ 100% {
|
|
|
+ transform: rotate(360deg);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .almost-lottery__canvas-img-self {
|
|
|
+ transition: transform ease-in;
|
|
|
+ animation: selfRotate .6s linear infinite;
|
|
|
+ }
|
|
|
+</style>
|