almost-lottery.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721
  1. <template>
  2. <view class="almost-lottery" :style="{ width: canvasWidth + 40 + 'px', height: canvasHeight + 40 + 'px'}">
  3. <view class="almost-lottery__wrap" :style="{width: canvasWidth + canvasMarginTotal + 'px', height: canvasHeight + canvasMarginTotal + 'px'}">
  4. <!-- #ifdef MP-ALIPAY -->
  5. <canvas :class="className" :id="canvasId" :width="canvasWidth" :height="canvasHeight" :style="{
  6. width: canvasWidth + 'px',
  7. height: canvasHeight + 'px'
  8. }" />
  9. <!-- #endif -->
  10. <!-- #ifndef MP-ALIPAY -->
  11. <canvas :class="className" :canvas-id="canvasId" :width="canvasWidth" :height="canvasHeight" :style="{
  12. width: canvasWidth + 'px',
  13. height: canvasHeight + 'px'
  14. }" />
  15. <!-- #endif -->
  16. <image class="canvas-img" :src="lotteryImg" :style="{
  17. width: canvasWidth + canvasMarginTotal + 'px',
  18. height: canvasHeight + canvasMarginTotal + 'px',
  19. transform: `rotate(${canvasAngle + targetAngle}deg)`,
  20. transitionDuration: `${transitionDuration}s`
  21. }"
  22. v-if="lotteryImg"></image>
  23. <view class="almost-lottery__action" :style="{
  24. width: actionSize + 'px',
  25. height: actionSize + 'px'
  26. }" @click="handleActionStart"></view>
  27. <!-- 为了兼容 app 端 ctx.measureText 所需的标签 -->
  28. <text class="almost-lottery__measureText">{{ measureText }}</text>
  29. </view>
  30. </view>
  31. </template>
  32. <script>
  33. import { pathToBase64 } from '@/almost-utils/image-tools.js'
  34. import { getStore, setStore, clearStore, downloadFile } from '@/almost-utils/almost-utils.js'
  35. export default {
  36. name: 'AlmostLottery',
  37. props: {
  38. // canvas 宽度
  39. canvasWidth: {
  40. type: Number,
  41. default: 240
  42. },
  43. // canvas 高度
  44. canvasHeight: {
  45. type: Number,
  46. default: 240
  47. },
  48. // 奖品列表
  49. prizeList: {
  50. type: Array,
  51. required: true,
  52. validator: (value) => {
  53. return value.length > 1
  54. }
  55. },
  56. // 中奖奖品在列表中的下标
  57. prizeIndex: {
  58. type: Number,
  59. required: true
  60. },
  61. // 奖品区块对应背景颜色
  62. colors: {
  63. type: Array,
  64. default: () => [
  65. '#FFFFFF',
  66. '#FFE9AA'
  67. ]
  68. },
  69. // 旋转动画时间 单位s
  70. duration: {
  71. type: Number,
  72. default: 8
  73. },
  74. // 旋转的圈数
  75. ringCount: {
  76. type: Number,
  77. default: 8
  78. },
  79. // 指针位置
  80. pointerPosition: {
  81. type: String,
  82. default: 'edge',
  83. validator: (value) => {
  84. return value === 'edge' || value === 'middle'
  85. }
  86. },
  87. // 字体颜色
  88. fontColor: {
  89. type: String,
  90. default: '#C30B29'
  91. },
  92. // 文字的大小
  93. fontSize: {
  94. type: Number,
  95. default: 12
  96. },
  97. // 奖品文字多行情况下的行高
  98. lineHeight: {
  99. type: Number,
  100. default: 16
  101. },
  102. // 奖品名称所对应的 key 值
  103. strKey: {
  104. type: String,
  105. default: 'name'
  106. },
  107. // 奖品文字总长度限制
  108. strMaxLen: {
  109. type: Number,
  110. default: 12
  111. },
  112. // 奖品文字多行情况下第一行文字长度
  113. strLineLen: {
  114. type: Number,
  115. default: 6
  116. },
  117. // 奖品图片的宽
  118. imageWidth: {
  119. type: Number,
  120. default: 30
  121. },
  122. // 奖品图片的高
  123. imageHeight: {
  124. type: Number,
  125. default: 30
  126. },
  127. // 转盘绘制成功的提示
  128. successMsg: {
  129. type: String,
  130. default: '奖品准备就绪,快来参与抽奖吧'
  131. },
  132. // 转盘绘制失败的提示
  133. failMsg: {
  134. type: String,
  135. default: '奖品仍在准备中,请稍后再来...'
  136. },
  137. // 是否开启画板的缓存
  138. canvasCached: {
  139. type: Boolean,
  140. default: true
  141. },
  142. // 内圈与外圈的间距
  143. canvasMargin: {
  144. type: Number,
  145. default: 5
  146. }
  147. },
  148. data() {
  149. return {
  150. // 画板className
  151. className: 'almost-lottery__canvas',
  152. // 画板标识
  153. canvasId: 'almostLotteryCanvas',
  154. // 画板导出的图片
  155. lotteryImg: '',
  156. // 旋转到奖品目标需要的角度
  157. targetAngle: 0,
  158. // 旋转动画时间 单位 s
  159. transitionDuration: 0,
  160. // 是否正在旋转
  161. isRotate: false,
  162. // 当前停留在那个奖品的序号
  163. stayIndex: 0,
  164. // 当前中奖奖品的序号
  165. targetIndex: 0,
  166. // 是否存在可用的缓存转盘图
  167. isCacheImg: false,
  168. oldLotteryImg: '',
  169. // 解决 app 不支持 measureText 的问题
  170. // app 已在 2.9.3 的版本中提供了对 measureText 的支持,将在后续版本逐渐稳定后移除相关兼容代码
  171. measureText: ''
  172. }
  173. },
  174. computed: {
  175. // 根据奖品列表计算 canvas 旋转角度
  176. canvasAngle() {
  177. let prizeCount = this.prizeList.length
  178. let prizeClip = 360 / prizeCount
  179. let result = 0
  180. let diffNum = 90 / prizeClip
  181. if (this.pointerPosition === 'edge') {
  182. result = -(prizeClip * diffNum)
  183. } else {
  184. result = -(prizeClip * diffNum + prizeClip / 2)
  185. }
  186. return result
  187. },
  188. // 外圆的半径
  189. outsideRadius() {
  190. return this.canvasWidth / 2
  191. },
  192. // 内圆的半径
  193. insideRadius() {
  194. return 20
  195. },
  196. // 字体的半径
  197. textRadius() {
  198. return this.fontSize / 2
  199. },
  200. // 根据画板的宽度计算奖品文字与中心点的距离
  201. textDistance() {
  202. const textZeroY = Math.round(this.outsideRadius - (this.insideRadius / 2))
  203. return textZeroY - this.textRadius
  204. },
  205. // 设备像素密度
  206. pixelRatio() {
  207. return uni.getSystemInfoSync().pixelRatio
  208. },
  209. // 内圈与外圈的距离
  210. canvasMarginTotal () {
  211. let diffNum = 5
  212. let margin = this.canvasMargin * 2
  213. if (this.canvasWidth > 240) {
  214. return -(this.canvasWidth / 240 * 2) - margin
  215. } else if (this.canvasWidth < 240) {
  216. return diffNum + (this.canvasWidth / 240 * 2) - margin
  217. } else {
  218. return diffNum - margin
  219. }
  220. },
  221. // 抽奖按钮的宽高
  222. actionSize () {
  223. return this.canvasWidth / 2.4
  224. }
  225. },
  226. watch: {
  227. // 监听获奖序号的变动
  228. prizeIndex(newVal, oldVal) {
  229. if (newVal > -1) {
  230. this.targetIndex = newVal
  231. this.onRotateStart()
  232. } else {
  233. console.info('旋转结束,prizeIndex 已重置')
  234. }
  235. }
  236. },
  237. methods: {
  238. resetAngle(){
  239. let prizeCount = this.prizeList.length
  240. let prizeClip = 360 / prizeCount
  241. this.targetAngle = this.targetAngle - prizeClip/2
  242. },
  243. // 开始旋转
  244. onRotateStart() {
  245. if (this.isRotate) return
  246. this.isRotate = true
  247. // 奖品总数
  248. let prizeCount = this.prizeList.length
  249. let baseAngle = 360 / prizeCount
  250. let angles = 0
  251. if (this.targetAngle === 0) {
  252. // 第一次旋转
  253. // 因为第一个奖品是从0°开始的,即水平向右方向
  254. // 第一次旋转角度 = 270度 - (停留的序号-目标序号) * 每个奖品区间角度 - 每个奖品区间角度的一半 - canvas自身旋转的度数
  255. angles = (270 - (this.targetIndex - this.stayIndex) * baseAngle - baseAngle / 2) - this.canvasAngle
  256. } else {
  257. // 后续旋转
  258. // 后续继续旋转 就只需要计算停留的位置与目标位置的角度
  259. angles = -(this.targetIndex - this.stayIndex) * baseAngle
  260. }
  261. // 更新目前序号
  262. this.stayIndex = this.targetIndex
  263. // 转 8 圈,圈数越多,转的越快
  264. this.targetAngle += angles + 360 * this.ringCount
  265. // 计算转盘结束的时间,预加一些延迟确保转盘停止后触发结束事件
  266. let endTime = this.transitionDuration * 1000 + 100
  267. let endTimer = setTimeout(() => {
  268. clearTimeout(endTimer)
  269. endTimer = null
  270. this.isRotate = false
  271. this.$emit('draw-end')
  272. }, endTime)
  273. let resetPrizeTimer = setTimeout(() => {
  274. clearTimeout(resetPrizeTimer)
  275. resetPrizeTimer = null
  276. // 每次抽奖结束后都要重置父级附件的 prizeIndex
  277. this.$emit('reset-index')
  278. }, endTime + 50)
  279. },
  280. // 点击 开始抽奖 按钮
  281. handleActionStart() {
  282. this.$emit('draw-start')
  283. },
  284. // 渲染转盘
  285. async onCreateCanvas() {
  286. // 获取 canvas 画布
  287. const canvasId = this.canvasId
  288. const ctx = uni.createCanvasContext(canvasId, this)
  289. // canvas 的宽高
  290. let canvasW = this.canvasWidth
  291. let canvasH = this.canvasHeight
  292. // 根据奖品个数计算 角度
  293. let prizeCount = this.prizeList.length
  294. let baseAngle = Math.PI * 2 / prizeCount
  295. // 设置描边颜色
  296. ctx.setStrokeStyle('#FFBE04')
  297. // 设置字体和字号
  298. // #ifndef MP
  299. let fontFamily =
  300. '-apple-system, BlinkMacSystemFont, \'PingFang SC\', \'Helvetica Neue\', STHeiti, \'Microsoft Yahei\', Tahoma, Simsun, sans-serif'
  301. ctx.font = `${this.fontSize}px ${fontFamily}`
  302. // #endif
  303. // #ifdef MP
  304. ctx.setFontSize(this.fontSize)
  305. // #endif
  306. // 注意,开始画的位置是从0°角的位置开始画的。也就是水平向右的方向。
  307. // 画具体内容
  308. for (let i = 0; i < prizeCount; i++) {
  309. let prizeItem = this.prizeList[i]
  310. // 当前角度
  311. let angle = i * baseAngle
  312. // 保存当前画布的状态
  313. ctx.save()
  314. // 开始画内容
  315. ctx.beginPath()
  316. // 开始画圆弧
  317. // x => 圆弧对应的圆心横坐标 x
  318. // y => 圆弧对应的圆心横坐标 y
  319. // radius => 圆弧的半径大小
  320. // startAngle => 圆弧开始的角度,单位是弧度
  321. // endAngle => 圆弧结束的角度,单位是弧度
  322. // anticlockwise(可选) => 绘制方向,true 为逆时针,false 为顺时针
  323. ctx.arc(canvasW * 0.5, canvasH * 0.5, this.outsideRadius, angle, angle + baseAngle, false)
  324. ctx.arc(canvasW * 0.5, canvasH * 0.5, this.insideRadius, angle + baseAngle, angle, true)
  325. // 开始链接线条
  326. ctx.stroke()
  327. // 每个奖品区块背景填充颜色
  328. if (this.colors.length === 2) {
  329. ctx.setFillStyle(this.colors[i % 2])
  330. } else {
  331. ctx.setFillStyle(this.colors[i])
  332. }
  333. // 填充颜色
  334. ctx.fill()
  335. // 开始绘制奖品内容
  336. // 重新映射画布上的 (0,0) 位置
  337. let translateX = canvasW * 0.5 + Math.cos(angle + baseAngle / 2) * this.textDistance
  338. let translateY = canvasH * 0.5 + Math.sin(angle + baseAngle / 2) * this.textDistance
  339. ctx.translate(translateX, translateY)
  340. // 绘制奖品名称
  341. ctx.setFillStyle(this.fontColor)
  342. let rewardName = this.strLimit(prizeItem[this.strKey])
  343. // rotate方法旋转当前的绘图,因为文字是和当前扇形中心线垂直的
  344. ctx.rotate(angle + (baseAngle / 2) + (Math.PI / 2))
  345. // 设置文本位置并处理换行
  346. // 是否需要换行
  347. let isLineBreak = rewardName.length > this.strLineLen
  348. let textOffsetX = this.fontSize === 12 ? 0 : this.textRadius
  349. if (isLineBreak) {
  350. // 获得多行文本数组
  351. rewardName = rewardName.substring(0, this.strLineLen) + ',' + rewardName.substring(this.strLineLen)
  352. let rewardNames = rewardName.split(',')
  353. // 循环文本数组,计算每一行的文本宽度
  354. for (let j = 0; j < rewardNames.length; j++) {
  355. if (ctx.measureText && ctx.measureText(rewardNames[j]).width) {
  356. // 文本的宽度信息
  357. let tempStrSize = ctx.measureText(rewardNames[j])
  358. ctx.fillText(rewardNames[j], -(tempStrSize.width / 2 + textOffsetX), j * this.lineHeight)
  359. } else {
  360. this.measureText = rewardNames[j]
  361. // 等待页面重新渲染
  362. await this.$nextTick()
  363. let textWidth = await this.getTextWidth()
  364. ctx.fillText(rewardNames[j], -(textWidth / 2 + textOffsetX), j * this.lineHeight)
  365. // console.log(rewardNames[j], textWidth, i)
  366. }
  367. }
  368. } else {
  369. if (ctx.measureText && ctx.measureText(rewardName).width) {
  370. // 文本的宽度信息
  371. let tempStrSize = ctx.measureText(rewardName)
  372. ctx.fillText(rewardName, -(tempStrSize.width / 2 + textOffsetX), 0)
  373. } else {
  374. this.measureText = rewardName
  375. // 等待页面重新渲染
  376. await this.$nextTick()
  377. let textWidth = await this.getTextWidth()
  378. ctx.fillText(rewardName, -(textWidth / 2 + textOffsetX), 0)
  379. }
  380. }
  381. // 绘制奖品图片
  382. if (prizeItem.prizeImage) {
  383. // App-Android平台 系统 webview 更新到 Chrome84+ 后 canvas 组件绘制本地图像 uni.canvasToTempFilePath 会报错
  384. // 统一将图片处理成 base64
  385. // https://ask.dcloud.net.cn/question/103303
  386. let reg = /^(https|http)/g
  387. // 处理远程图片
  388. if (reg.test(prizeItem.prizeImage)) {
  389. console.warn('###当前数据列表中的奖品图片为网络图片,开始下载图片...###')
  390. let res = await downloadFile(prizeItem.prizeImage)
  391. console.log('处理远程图片', res)
  392. if (res.ok) {
  393. let tempFilePath = res.tempFilePath
  394. // #ifndef MP
  395. prizeItem.prizeImage = await pathToBase64(tempFilePath)
  396. // #endif
  397. // #ifdef MP
  398. prizeItem.prizeImage = tempFilePath
  399. // #endif
  400. }
  401. } else {
  402. // #ifndef MP
  403. prizeItem.prizeImage = await pathToBase64(prizeItem.prizeImage)
  404. // #endif
  405. }
  406. ctx.drawImage(prizeItem.prizeImage, -(this.imageWidth / 2), canvasW / 10, this.imageWidth, this.imageHeight)
  407. }
  408. ctx.restore()
  409. }
  410. // 保存绘图并导出图片
  411. ctx.draw(true, () => {
  412. let drawTimer = setTimeout(() => {
  413. clearTimeout(drawTimer)
  414. drawTimer = null
  415. // #ifdef MP-ALIPAY
  416. ctx.toTempFilePath({
  417. destWidth: this.canvasWidth * this.pixelRatio,
  418. destHeight: this.canvasHeight * this.pixelRatio,
  419. success: (res) => {
  420. // console.log(res.apFilePath)
  421. this.handlePrizeImg({
  422. ok: true,
  423. data: res.apFilePath,
  424. msg: '画布导出生成图片成功'
  425. })
  426. },
  427. fail: (err) => {
  428. this.handlePrizeImg({
  429. ok: false,
  430. data: err,
  431. msg: '画布导出生成图片失败'
  432. })
  433. }
  434. })
  435. // #endif
  436. // #ifndef MP-ALIPAY
  437. uni.canvasToTempFilePath({
  438. destWidth: this.canvasWidth * this.pixelRatio,
  439. destHeight: this.canvasHeight * this.pixelRatio,
  440. canvasId: this.canvasId,
  441. success: (res) => {
  442. // 在 H5 平台下,tempFilePath 为 base64
  443. // console.log(res.tempFilePath)
  444. this.handlePrizeImg({
  445. ok: true,
  446. data: res.tempFilePath,
  447. msg: '画布导出生成图片成功'
  448. })
  449. },
  450. fail: (err) => {
  451. this.handlePrizeImg({
  452. ok: false,
  453. data: err,
  454. msg: '画布导出生成图片失败'
  455. })
  456. }
  457. }, this)
  458. // #endif
  459. }, 500)
  460. })
  461. },
  462. // 处理导出的图片
  463. handlePrizeImg(res) {
  464. if (res.ok) {
  465. let data = res.data
  466. if (!this.canvasCached) {
  467. this.lotteryImg = data
  468. this.handlePrizeImgSuc(res)
  469. return
  470. }
  471. // #ifndef H5
  472. if (this.isCacheImg) {
  473. uni.getSavedFileList({
  474. success: (sucRes) => {
  475. let fileList = sucRes.fileList
  476. // console.log('getSavedFileList Cached', fileList)
  477. let cached = false
  478. for (let i = 0; i < fileList.length; i++) {
  479. let item = fileList[i]
  480. if (item.filePath === data) {
  481. cached = true
  482. this.lotteryImg = data
  483. console.info('经查,本地缓存中存在转盘图可用,本次将不再绘制转盘')
  484. this.handlePrizeImgSuc(res)
  485. break
  486. }
  487. }
  488. if (!cached) {
  489. console.info('经查,本地缓存中存在转盘图不可用,需要重新初始化转盘绘制')
  490. this.initCanvasDraw()
  491. }
  492. },
  493. fail: (err) => {
  494. this.initCanvasDraw()
  495. }
  496. })
  497. } else {
  498. uni.saveFile({
  499. tempFilePath: data,
  500. success: (sucRes) => {
  501. let filePath = sucRes.savedFilePath
  502. // console.log('saveFile', filePath)
  503. setStore('lotteryImg', filePath)
  504. this.lotteryImg = filePath
  505. this.handlePrizeImgSuc({
  506. ok: true,
  507. data: filePath,
  508. msg: '画布导出生成图片成功'
  509. })
  510. },
  511. fail: (err) => {
  512. this.handlePrizeImg({
  513. ok: false,
  514. data: err,
  515. msg: '画布导出生成图片失败'
  516. })
  517. }
  518. })
  519. }
  520. // #endif
  521. // #ifdef H5
  522. console.info('当前为 H5 端,直接使用导出的/缓存中的 base64 图')
  523. setStore('lotteryImg', data)
  524. this.lotteryImg = data
  525. this.handlePrizeImgSuc(res)
  526. // #endif
  527. } else {
  528. console.error('处理导出的图片失败', res)
  529. uni.hideLoading()
  530. // #ifdef H5
  531. console.error('###当前为 H5 端,下载网络图片需要后端配置允许跨域###')
  532. // #endif
  533. // #ifdef MP
  534. console.error('###当前为小程序端,下载网络图片需要配置域名白名单###')
  535. // #endif
  536. }
  537. },
  538. // 处理图片完成
  539. handlePrizeImgSuc (res) {
  540. uni.hideLoading()
  541. this.$emit('finish', {
  542. ok: res.ok,
  543. data: res.data,
  544. msg: res.ok ? this.successMsg : this.failMsg
  545. })
  546. },
  547. // 兼容 app 端不支持 ctx.measureText
  548. // 已知问题:初始绘制时,低端安卓机 平均耗时 2s
  549. getTextWidth() {
  550. return new Promise((resolve, reject) => {
  551. uni.createSelectorQuery().in(this).select('.almost-lottery__measureText').fields({
  552. size: true,
  553. }, (res) => {
  554. resolve(res.width)
  555. }).exec()
  556. })
  557. },
  558. // 处理文字溢出
  559. strLimit(value) {
  560. let maxLength = this.strMaxLen
  561. if (!value || !maxLength) return value
  562. return value.length > maxLength ? value.slice(0, maxLength - 1) + '...' : value
  563. },
  564. // 检查本地缓存中是否存在转盘图
  565. checkCacheImg () {
  566. console.log('检查本地缓存中是否存在转盘图')
  567. // 检查是否已有缓存的转盘图
  568. // 检查是否与本次奖品数据相同
  569. this.oldLotteryImg = getStore('lotteryImg')
  570. let oldPrizeList = getStore('prizeList')
  571. let newPrizeList = JSON.stringify(this.prizeList)
  572. if (this.oldLotteryImg) {
  573. if (oldPrizeList === newPrizeList) {
  574. console.log(`经查,本地缓存中存在转盘图 => ${this.oldLotteryImg}`)
  575. this.isCacheImg = true
  576. console.log('需要继续判断这张缓存图是否可用')
  577. this.handlePrizeImg({
  578. ok: true,
  579. data: this.oldLotteryImg,
  580. msg: '画布导出生成图片成功'
  581. })
  582. return
  583. }
  584. }
  585. console.log('经查,本地缓存中不存在转盘图')
  586. this.initCanvasDraw()
  587. },
  588. // 初始化绘制
  589. initCanvasDraw () {
  590. console.log('开始初始化转盘绘制')
  591. this.isCacheImg = false
  592. this.lotteryImg = ''
  593. clearStore('lotteryImg')
  594. setStore('prizeList', this.prizeList)
  595. this.onCreateCanvas()
  596. }
  597. },
  598. mounted() {
  599. this.$nextTick(() => {
  600. let stoTimer = setTimeout(() => {
  601. clearTimeout(stoTimer)
  602. stoTimer = null
  603. if (this.canvasCached) {
  604. this.checkCacheImg()
  605. } else {
  606. this.onCreateCanvas()
  607. }
  608. this.transitionDuration = this.duration
  609. }, 50)
  610. })
  611. }
  612. }
  613. </script>
  614. <style lang="scss" scoped>
  615. $lotteryBgUrl: '~static/almost-lottery/almost-lottery__bg';
  616. $actionBgUrl: '~static/almost-lottery/almost-lottery__action';
  617. .almost-lottery {
  618. display: flex;
  619. justify-content: center;
  620. align-items: center;
  621. margin: 0 auto;
  622. background-repeat: no-repeat;
  623. background-position: center center;
  624. background-size: contain;
  625. background-image: url($lotteryBgUrl + ".png");
  626. @media (-webkit-min-device-pixel-ratio: 2), (min-device-pixel-ratio: 2) {
  627. background-image: url($lotteryBgUrl + "2x.png");
  628. }
  629. @media (-webkit-min-device-pixel-ratio: 3), (min-device-pixel-ratio: 3) {
  630. background-image: url($lotteryBgUrl + "3x.png");
  631. }
  632. }
  633. .almost-lottery__wrap {
  634. position: relative;
  635. display: flex;
  636. justify-content: center;
  637. align-items: center;
  638. }
  639. .almost-lottery__canvas {
  640. position: absolute;
  641. left: -9999px;
  642. opacity: 0;
  643. display: flex;
  644. justify-content: center;
  645. align-items: center;
  646. }
  647. .almost-lottery__action {
  648. position: absolute;
  649. background-repeat: no-repeat;
  650. background-position: center center;
  651. background-size: contain;
  652. background-image: url($actionBgUrl + ".png");
  653. @media (-webkit-min-device-pixel-ratio: 2), (min-device-pixel-ratio: 2) {
  654. background-image: url($actionBgUrl + "2x.png");
  655. }
  656. @media (-webkit-min-device-pixel-ratio: 3), (min-device-pixel-ratio: 3) {
  657. background-image: url($actionBgUrl + "3x.png");
  658. }
  659. }
  660. .almost-lottery__measureText {
  661. position: absolute;
  662. left: 0;
  663. top: 0;
  664. white-space: nowrap;
  665. font-size: 12px;
  666. opacity: 0;
  667. }
  668. .canvas-img {
  669. display: block;
  670. transition: transform cubic-bezier(.34, .12, .05, .95);
  671. }
  672. </style>