index.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. /**
  2. * ease:
  3. * 'linear' 动画从头到尾的速度是相同的
  4. * 'ease' 动画以低速开始,然后加快,在结束前变慢
  5. * 'ease-in' 动画以低速开始
  6. *
  7. * 'ease-in-out' 动画以低速开始和结束
  8. * 'ease-out' 动画以低速结束
  9. * 'step-start' 动画第一帧就跳至结束状态直到结束
  10. * 'step-end' 动画一直保持开始状态,最后一帧跳到结束状态
  11. */
  12. let config = {
  13. size: {
  14. width: '560rpx',
  15. height: '560rpx'
  16. }, // 转盘宽高
  17. bgColors: ['#FFC53F', '#FFED97'], // 转盘间隔背景色 支持多种颜色交替
  18. fontSize: 12, // 文字大小
  19. fontColor: '#C31A34', // 文字颜色
  20. nameMarginTop: 12, // 最外文字边距
  21. nameLength: 6, // 最外文字个数
  22. iconWidth: 32, // 图标宽度
  23. iconHeight: 32, // 图标高度
  24. iconAndTextPadding: 4, // 最内文字与图标的边距
  25. duration: 3000, // 转盘转动动画时长
  26. rate: 1.5, // 由时长s / 圈数得到
  27. border: 'border: 10rpx solid #FEFAE4;', // 转盘边框
  28. ease: 'ease-out' // 转盘动画
  29. };
  30. let preAngle = 0; // 上一次选择角度
  31. let preAngle360 = 0; // 上一次选择角度和360度之间的差
  32. let retryCount = 10; // 报错重试次数
  33. let retryTimer; // 重试setTimeout
  34. let drawTimer; // 绘制setTimeout
  35. Component({
  36. properties: {
  37. // 是否可用
  38. enable: {
  39. type: Boolean,
  40. value: true
  41. },
  42. // 数据
  43. gifts: {
  44. type: Array,
  45. value: []
  46. },
  47. // 中奖id
  48. prizeId: {
  49. type: String,
  50. value: ''
  51. },
  52. // 配置项 传入后和默认的配置进行合并
  53. config: {
  54. type: Object,
  55. value: {}
  56. },
  57. // 抽奖次数
  58. count: {
  59. type: Number,
  60. default: ""
  61. },
  62. },
  63. data: {
  64. lotteryCount: null,
  65. cost: null,
  66. turnCanvasInfo: { width: 0, height: 0 },
  67. size: config.size,
  68. giftModule: [],
  69. disable: false,
  70. canvasImgUrl: '',
  71. border: config.border,
  72. infos: []
  73. },
  74. methods: {
  75. async getCanvasContainerInfo(id) {
  76. return new Promise((resolve) => {
  77. const query = wx.createSelectorQuery().in(this);
  78. query.select(id).boundingClientRect(function (res) {
  79. const { width, height } = res;
  80. resolve({ width, height });
  81. }).exec();
  82. });
  83. },
  84. async init() {
  85. try {
  86. const info = await this.getCanvasContainerInfo('#turn');
  87. if (info.width && info.height) {
  88. this.setData({
  89. turnCanvasInfo: info
  90. });
  91. this.drawTurn();
  92. } else {
  93. wx.showToast({
  94. icon: 'nont',
  95. title: '获取转盘宽高失败'
  96. })
  97. }
  98. } catch (e) {
  99. if (retryCount <= 0) {
  100. return;
  101. }
  102. retryCount--;
  103. if (retryTimer) {
  104. clearTimeout(retryTimer);
  105. }
  106. retryTimer = setTimeout(async () => {
  107. await this.init();
  108. }, 100);
  109. }
  110. },
  111. drawTurn() {
  112. const turnCanvasInfo = this.data.turnCanvasInfo;
  113. const giftModule = this.properties.gifts;
  114. const ctx = wx.createCanvasContext('turn', this);
  115. // 计算没个扇区弧度
  116. const radian = Number((2 * Math.PI / giftModule.length).toFixed(2));
  117. // 绘制扇区并记录每个扇区信息
  118. const infos = this.drawSector(radian, giftModule, ctx, turnCanvasInfo);
  119. // 记录旋转角度
  120. this.recordTheRotationAngle(infos);
  121. // 绘制扇区文本及图片
  122. this.drawTextAndImage(giftModule, ctx, turnCanvasInfo, radian);
  123. ctx.draw(false, () => {
  124. this.saveToTempPath(turnCanvasInfo);
  125. });
  126. },
  127. saveToTempPath(turnCanvasInfo) {
  128. if (drawTimer) {
  129. clearTimeout(drawTimer);
  130. }
  131. drawTimer = setTimeout(() => {
  132. wx.canvasToTempFilePath({
  133. canvasId: 'turn',
  134. quality: 1,
  135. x: 0,
  136. y: 0,
  137. width: turnCanvasInfo.width,
  138. height: turnCanvasInfo.height,
  139. success: (res) => {
  140. this.setData({
  141. canvasImgUrl: res.tempFilePath
  142. });
  143. },
  144. fail: (error) => {
  145. console.log(error);
  146. }
  147. }, this);
  148. }, 500);
  149. },
  150. drawSector(radian, giftModule, ctx, turnCanvasInfo) {
  151. const halfRadian = Number((radian / 2).toFixed(2));
  152. let startRadian = -Math.PI / 2 - halfRadian;
  153. const angle = 360 / giftModule.length;
  154. const halfAngle = angle / 2;
  155. let startAngle = -90 - halfAngle;
  156. const infos = [];
  157. // 绘制扇形
  158. for (let i = 0; i < giftModule.length; i++) {
  159. // 保存当前状态
  160. ctx.save();
  161. // 开始一条新路径
  162. ctx.beginPath();
  163. ctx.moveTo(turnCanvasInfo.width / 2, turnCanvasInfo.height / 2);
  164. ctx.arc(turnCanvasInfo.width / 2, turnCanvasInfo.height / 2, turnCanvasInfo.width / 2, startRadian, startRadian + radian);
  165. if (giftModule[i].bgColor) {
  166. ctx.setFillStyle(giftModule[i].bgColor);
  167. } else {
  168. ctx.setFillStyle(config.bgColors[i % config.bgColors.length]);
  169. }
  170. ctx.fill();
  171. ctx.closePath();
  172. ctx.restore();
  173. infos.push({
  174. id: giftModule[i].objectId,
  175. angle: (startAngle + startAngle + angle) / 2
  176. });
  177. startRadian += radian;
  178. startAngle += angle;
  179. }
  180. return infos;
  181. },
  182. drawTextAndImage(giftModule, ctx, turnCanvasInfo, radian) {
  183. let startRadian = 0;
  184. // 绘制扇形文字和logo
  185. for (let i = 0; i < giftModule.length; i++) {
  186. // 保存当前状态
  187. ctx.save();
  188. // 开始一条新路径
  189. ctx.beginPath();
  190. ctx.translate(turnCanvasInfo.width / 2, turnCanvasInfo.height / 2);
  191. ctx.rotate(startRadian);
  192. ctx.translate(-turnCanvasInfo.width / 2, -turnCanvasInfo.height / 2);
  193. if (giftModule[i].fontSize) {
  194. ctx.setFontSize(giftModule[i].fontSize);
  195. } else {
  196. ctx.setFontSize(config.fontSize);
  197. }
  198. ctx.setTextAlign('center');
  199. if (giftModule[i].fontColor) {
  200. ctx.setFillStyle(giftModule[i].fontColor);
  201. } else {
  202. ctx.setFillStyle(config.fontColor);
  203. }
  204. ctx.setTextBaseline('top');
  205. if (giftModule[i].name) {
  206. ctx.fillText(giftModule[i].name, turnCanvasInfo.width / 2, config.nameMarginTop);
  207. }
  208. if (giftModule[i].subname) {
  209. ctx.fillText(giftModule[i].subname ? giftModule[i].subname : '', turnCanvasInfo.width / 2, config.nameMarginTop + config.fontSize + 2);
  210. }
  211. if (giftModule[i].imgUrl) {
  212. ctx.drawImage(giftModule[i].imgUrl,
  213. turnCanvasInfo.width / 2 - config.iconWidth / 2,
  214. config.nameMarginTop + config.fontSize * 2 + 2 + config.iconAndTextPadding,
  215. config.iconWidth, config.iconHeight);
  216. }
  217. ctx.closePath();
  218. ctx.restore();
  219. startRadian += radian;
  220. }
  221. },
  222. recordTheRotationAngle(infos) {
  223. for (let i = infos.length - 1; i >= 0; i--) {
  224. infos[i].angle -= infos[0].angle;
  225. infos[i].angle = 360 - infos[i].angle;
  226. }
  227. // 记录id及滚动的角度
  228. this.setData({
  229. infos: infos
  230. });
  231. },
  232. luckDrawHandle() {
  233. if (this.data.disable || !this.data.canvasImgUrl) {
  234. return;
  235. }
  236. this.setData({
  237. disable: true
  238. });
  239. console.log('开始抽奖')
  240. this.triggerEvent('LuckDraw');
  241. },
  242. startAnimation(angle) {
  243. if (this.data.lotteryCount - this.data.cost < 0) {
  244. this.setData({
  245. disable: false
  246. });
  247. this.triggerEvent('NotEnough', '积分不足!');
  248. return;
  249. }
  250. // 抽奖次数减一
  251. this.setData({
  252. lotteryCount: this.data.lotteryCount - this.data.cost
  253. });
  254. const currentAngle = preAngle;
  255. preAngle += Math.floor((config.duration / 1000) / config.rate) * 360 + angle + preAngle360;
  256. this.animate('#canvas-img', [
  257. { rotate: currentAngle, ease: 'linear' },
  258. { rotate: preAngle, ease: config.ease },
  259. ], config.duration, () => {
  260. this.setData({
  261. disable: false
  262. });
  263. preAngle360 = 360 - angle;
  264. this.triggerEvent('LuckDrawFinish');
  265. });
  266. },
  267. downloadHandle(url) {
  268. return new Promise((resolve, reject) => {
  269. wx.downloadFile({
  270. url: url, // 仅为示例,并非真实的资源
  271. success: (res) => {
  272. // 只要服务器有响应数据,就会把响应内容写入文件并进入 success 回调,业务需要自行判断是否下载到了想要的内容
  273. if (res.statusCode === 200) {
  274. resolve(res.tempFilePath);
  275. } else {
  276. reject();
  277. }
  278. },
  279. fail: () => {
  280. reject();
  281. }
  282. });
  283. });
  284. },
  285. async downloadImg(imgs) {
  286. let result;
  287. try {
  288. const downloadHandles = [];
  289. for (const url of imgs) {
  290. if (this.isAbsoluteUrl(url)) { // 是网络地址
  291. downloadHandles.push(this.downloadHandle(url));
  292. } else {
  293. downloadHandles.push(Promise.resolve(url));
  294. }
  295. }
  296. result = await Promise.all(downloadHandles);
  297. } catch (e) {
  298. console.log(e);
  299. result = [];
  300. }
  301. return result;
  302. },
  303. clearTimeout() {
  304. if (retryTimer) {
  305. clearTimeout(retryTimer);
  306. }
  307. if (drawTimer) {
  308. clearTimeout(drawTimer);
  309. }
  310. },
  311. isAbsoluteUrl(url) {
  312. return /(^[a-z][a-z\d\+\-\.]*:)?\/\//i.test(url);
  313. },
  314. async initData(data) {
  315. let name;
  316. let subname;
  317. let imgUrls = [];
  318. if (this.properties.config) {
  319. config = Object.assign(config, this.properties.config);
  320. }
  321. for (const d of data) {
  322. name = d.name;
  323. imgUrls.push(d.imgUrl);
  324. d.imgUrl = '';
  325. if (name.length > config.nameLength) {
  326. d.name = name.slice(0, config.nameLength);
  327. subname = name.slice(config.nameLength);
  328. if (subname.length > config.nameLength - 2) {
  329. d['subname'] = subname.slice(0, config.nameLength - 2) + '...';
  330. } else {
  331. d['subname'] = subname;
  332. /* console.log('是否开启了概率???', that.data.probability);
  333. //开启概率 probability这属性必须要传个ture
  334. if (that.data.probability) {
  335. r = that._openProbability();
  336. } */
  337. }
  338. }
  339. }
  340. imgUrls = await this.downloadImg(imgUrls);
  341. for (let i = 0; i < imgUrls.length; i++) {
  342. data[i].imgUrl = imgUrls[i];
  343. }
  344. this.setData({
  345. giftModule: data
  346. });
  347. await this.init();
  348. }
  349. },
  350. observers: {
  351. 'gifts': async function (gifts) {
  352. if (!gifts || !gifts.length) {
  353. return;
  354. }
  355. await this.initData(gifts);
  356. },
  357. 'enable': function (enable) {
  358. this.setData({
  359. disable: !enable
  360. });
  361. },
  362. 'prizeId': function (id) {
  363. if (!id) {
  364. this.setData({
  365. disable: false
  366. });
  367. return;
  368. }
  369. try {
  370. const infos = this.data.infos;
  371. console.log(infos, id)
  372. const info = infos.find((item) => item.id == id);
  373. console.log(info)
  374. this.startAnimation(info.angle);
  375. } catch (e) {
  376. this.setData({
  377. disable: false
  378. });
  379. }
  380. },
  381. 'count': function (lotteryCount) {
  382. console.log(lotteryCount)
  383. this.setData({
  384. lotteryCount
  385. });
  386. },
  387. // 'cost': function(cost) {
  388. // console.log(cost)
  389. // this.setData({
  390. // cost
  391. // });
  392. // },
  393. },
  394. lifetimes: {
  395. detached() {
  396. this.clearTimeout();
  397. }
  398. },
  399. pageLifetimes: {
  400. hide() {
  401. this.clearTimeout();
  402. }
  403. }
  404. });