index.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587
  1. import { BgLayer, BgType } from "../base/BgLayer";
  2. import { BorderLayer } from "../base/BorderLayer";
  3. import { BoxLayer } from "../base/BoxLayer";
  4. import { DebugLayer } from "../base/DebugLayer";
  5. import { FrameLayer } from "../base/FrameLayer";
  6. import { TouchTracker } from "../base/Gesture";
  7. import { ImageShaders } from "../base/ImageShaders";
  8. import { Padding, Scene } from "../base/Scene";
  9. import { TextureLayer } from "../base/TextureLayer";
  10. import { loadImage } from "../base/utils";
  11. import AudioPlayer, { AudioType } from "./Audio";
  12. import { AreaGroup, AreaGroups, Color, Settings } from "./common";
  13. import Explosion from "./explosion";
  14. import {
  15. FillerConfig,
  16. FillerData,
  17. FillerResource,
  18. FillerCallback,
  19. } from "./FillerData";
  20. import { FillerScene } from "./FillerScene";
  21. import { HintLayer } from "./HintLayer";
  22. import { LineArtLayer } from "./LineArtLayer";
  23. import { NumberLayer } from "./NumberLayer";
  24. import { WorkLayer } from "./WorkLayer";
  25. import { LoadingController } from "./LoadingController";
  26. import { FingerHint } from "./FingerHint";
  27. import JSConfetti from "js-confetti";
  28. import { initCta, ctaHighlight } from "./cta";
  29. import { adPlatform } from "./ad-platform/current";
  30. // 静态导入资源,Vite 构建时会将它们转为 data URI 内联进 HTML
  31. import configRaw from "/assets/res/6a18f7d9957ac783bad75479/config.json?raw";
  32. import pageUrl from "/assets/res/6a18f7d9957ac783bad75479/page.png?url";
  33. import mapUrl from "/assets/res/6a18f7d9957ac783bad75479/map.png?url";
  34. import specialUrl from "/assets/res/6a18f7d9957ac783bad75479/special.jpeg?url";
  35. import numberFontUrl from "/assets/fonts/numbers_roboto_500.png?url";
  36. import fingerUrl from "/assets/img/finger.png?url";
  37. import logoUrl from "/assets/img/logo.png?url";
  38. import logoTxtUrl from "/assets/img/logo-txt.png?url";
  39. import coloringPagesUrl from "/assets/img/coloring-pages.png?url";
  40. import slogonUrl from "/assets/img/slogon.png?url";
  41. window.addEventListener("load", () => {
  42. document.body.dataset.adPlatform = adPlatform.platform;
  43. adPlatform.init();
  44. initCta(adPlatform);
  45. init();
  46. });
  47. async function test() {
  48. let canvas = document.querySelector("#canvas") as HTMLCanvasElement;
  49. canvas.width = canvas.clientWidth * window.devicePixelRatio;
  50. canvas.height = canvas.clientHeight * window.devicePixelRatio;
  51. let resource = await loadResource();
  52. let ctx = canvas.getContext("2d")!;
  53. ctx.fillStyle = "red";
  54. ctx.fillRect(0, 0, canvas.width, canvas.height);
  55. ctx.drawImage(resource.page, 0, 0);
  56. }
  57. async function init() {
  58. const loadingController = new LoadingController({
  59. minDisplayTime: 1000, // 最小显示时间1秒
  60. fadeDuration: 300, // 淡出动画300ms
  61. });
  62. if (adPlatform.shouldUseCustomLoading()) {
  63. loadingController.show();
  64. }
  65. let canvas = document.querySelector("#canvas") as HTMLCanvasElement;
  66. let gl = canvas.getContext("webgl2", {
  67. premultipliedAlpha: false,
  68. }) as WebGL2RenderingContext;
  69. let pixelRatio = window.devicePixelRatio;
  70. /** 根据 game-area 的实际渲染尺寸同步 canvas 像素尺寸 */
  71. function syncCanvasSize() {
  72. const gameArea = document.getElementById("game-area")!;
  73. const w = Math.round(gameArea.clientWidth * pixelRatio);
  74. const h = Math.round(gameArea.clientHeight * pixelRatio);
  75. if (canvas.width !== w || canvas.height !== h) {
  76. canvas.width = w;
  77. canvas.height = h;
  78. scene?.updateViewport();
  79. }
  80. }
  81. canvas.width = canvas.clientWidth * pixelRatio;
  82. canvas.height = canvas.clientHeight * pixelRatio;
  83. let scene = new FillerScene(gl, pixelRatio);
  84. // 确保 canvas 像素尺寸与实际 flex 布局一致(body.onload 时 dvh 可能还未最终结算)
  85. syncCanvasSize();
  86. let audio = new AudioPlayer();
  87. // 加载设置项,学个新语法 ...
  88. let settings: Settings = {
  89. hintStyle: "default",
  90. sound: true,
  91. vibrate: true,
  92. autoNext: true,
  93. };
  94. window.addEventListener("resize", () => syncCanvasSize());
  95. let resource = await loadResource();
  96. if (adPlatform.shouldUseCustomLoading()) {
  97. loadingController.hide();
  98. }
  99. adPlatform.onResourcesLoaded();
  100. // 设置固定顶栏 logo(图标 + 文字)
  101. (document.getElementById("app-logo") as HTMLImageElement).src = logoUrl;
  102. (document.getElementById("app-logo-txt") as HTMLImageElement).src =
  103. logoTxtUrl;
  104. // 预加载 promo 图片,避免动画播放时图片刚加载导致布局跳变
  105. (document.getElementById("promo-coloring") as HTMLImageElement).src =
  106. coloringPagesUrl;
  107. (document.getElementById("promo-slogon") as HTMLImageElement).src = slogonUrl;
  108. let taskList: number[] = [];
  109. // fingerHint 先声明,init 末尾赋值后回调中就能使用
  110. let fingerHint: FingerHint | null = null;
  111. let fillerData = new FillerData(
  112. new FillerConfig(settings),
  113. resource,
  114. gl,
  115. taskList,
  116. {
  117. onFillFailed() {
  118. adPlatform.onGameStart();
  119. console.log("填充失败");
  120. fingerHint?.onUserInteraction();
  121. },
  122. onFillSuccess() {
  123. adPlatform.onGameStart();
  124. console.log("填充成功");
  125. fingerHint?.onUserInteraction();
  126. if (fillerData.config.settings.vibrate) {
  127. vibrate();
  128. }
  129. cssAdjustProgress(fillerData.data.coloredPercent);
  130. cssSetColorProgress(
  131. fillerData.currentGroupIndex,
  132. fillerData.currentGroup!.progressPercent,
  133. );
  134. if (fillerData.currentGroup?.isAllColored) {
  135. if (fillerData.config.settings.sound) {
  136. audio.playAudio(AudioType.ColorDone);
  137. }
  138. cssColorDone(fillerData);
  139. }
  140. },
  141. onSwitchGroup() {
  142. let currentGroupIndex = fillerData.currentGroupIndex;
  143. console.log(`切换到下一个group ${currentGroupIndex}`);
  144. cssSelectColor(currentGroupIndex);
  145. cssAdjustScroll(fillerData);
  146. },
  147. onFinish() {
  148. fingerHint?.stop();
  149. adPlatform.onGameEnd();
  150. cssOnFinish(scene, workLayer, hintLayer, numberLayer, audio);
  151. },
  152. },
  153. );
  154. scene.fillerData = fillerData;
  155. console.log("resource", resource);
  156. // canvas 区域与所有 UI 完全分离,只保留极小视觉边距
  157. scene.setContentPadding(new Padding(20, 20, 20, 20));
  158. //let page = await loadImage('/webgl/page.png')
  159. //let page = await loadImage('/assets/resources/friend/page_gray.png')
  160. let width = fillerData.width;
  161. let height = fillerData.height;
  162. scene.setContentSize(width, height);
  163. // // 最底层:透明背景,canvas 透出 body 渐变色,与整体 UI 融为一体
  164. // scene.addLayer(new BackgroundLayer(scene, scene.width, scene.height, [0, 0, 0, 0]));
  165. if (resource.bg) {
  166. scene.addLayer(
  167. new BgLayer(scene, resource.bg, scene.width, scene.height, BgType.Repeat),
  168. );
  169. }
  170. scene.addLayer(new BoxLayer(scene, 0, 0, width, height));
  171. let hintLayer = new HintLayer(scene, fillerData);
  172. scene.addLayer(hintLayer);
  173. let numberLayer = new NumberLayer(scene, resource.numberImage, fillerData, 1);
  174. scene.addLayer(numberLayer);
  175. let workLayer = new WorkLayer(scene, fillerData);
  176. scene.addLayer(workLayer);
  177. //scene.addLayer(new TextureLayer(scene, workLayer.mask.texture, fillerData.width, fillerData.height, fillerData.width, fillerData.height))
  178. scene.addLayer(new LineArtLayer(scene, resource.page, width, height));
  179. scene.addLayer(
  180. new BorderLayer(
  181. scene,
  182. 0,
  183. 0,
  184. fillerData.width,
  185. fillerData.height,
  186. 0xff222222,
  187. ),
  188. );
  189. // let mask = new Mask(gl, fillerData)
  190. // resource.config[0].areas.forEach(area => mask.addArea(area))
  191. // mask.flush()
  192. //scene.addTestLayer(new TextureLayer(scene, mask.texture, fillerData.width, fillerData.height, width, height, ImageShaders.CommonGaussianBlurCut))
  193. //scene.addTestLayer(new TextureLayer(scene, mask.texture, fillerData.width, fillerData.height, width, height, ))
  194. //scene.addLayer(new TextureLayer(scene, hintLayer.mask.texture, fillerData.width, fillerData.height, width, height, ImageShaders.CommonGaussianBlur ))
  195. //scene.addLayer(new DebugLayer(scene))
  196. //scene.addLayer(new ImageLayer(scene, resource.page, width, height, gl.LINEAR, ImageShaders.CommonGaussianBlur))
  197. //scene.addLayer(new TextureLayer(scene, fillerData.mapTexure, width, height, width, height))
  198. createButtons(fillerData);
  199. cssAdjustProgress(fillerData.data.coloredPercent);
  200. workLayer.initTask();
  201. if (fillerData.data.coloredPercent >= 100) {
  202. adPlatform.onGameEnd();
  203. cssOnFinish(scene, workLayer, hintLayer, numberLayer, null);
  204. } else {
  205. // 创建手指提示,全图未完成时启动引导
  206. fingerHint = new FingerHint(scene, fillerData, fingerUrl);
  207. fingerHint.start();
  208. }
  209. }
  210. async function loadResource(): Promise<FillerResource> {
  211. const config = JSON.parse(configRaw) as AreaGroups;
  212. const [page, map, special, numberImage] = await Promise.all([
  213. loadImage(pageUrl),
  214. loadImage(mapUrl),
  215. loadImage(specialUrl),
  216. loadImage(numberFontUrl),
  217. ]);
  218. return new FillerResource(
  219. config,
  220. page as HTMLImageElement,
  221. map as HTMLImageElement,
  222. numberImage as HTMLImageElement,
  223. [],
  224. special as HTMLImageElement,
  225. );
  226. }
  227. function createButtons(fillerData: FillerData) {
  228. let data = fillerData.data.areaGroups;
  229. let htmlUndoned = [];
  230. let htmlDone = [];
  231. let html = [];
  232. let areaGroup: AreaGroup;
  233. let color: Color;
  234. for (var i = 0; i < data.length; i++) {
  235. areaGroup = data[i];
  236. color = new Color(areaGroup.color);
  237. if (areaGroup.isAllColored) {
  238. htmlDone.push(
  239. `<div id="color-btn-container-${i}" class="color-btn-container" onclick="selectColor(this, event, ${i})">`,
  240. );
  241. htmlDone.push(
  242. `<svg id="color-btn-progress-ring-${i}" class="color-btn-progress-ring" viewBox="0 0 48 48"><circle class="color-btn-progress-ring-track" cx="50%" cy="50%" r="45%" fill="transparent" stroke="#fff" stroke-width="5%" /> <circle id="color-btn-progress-ring-value-${i}" class="color-btn-progress-ring-value" cx="50%" cy="50%" r="45%" fill="transparent" stroke="#2ecc71" stroke-width="5%" stroke-linecap="round" /></svg>`,
  243. );
  244. htmlDone.push(
  245. `<div id="color-btn-${i}" class="color-btn" style="background-color:${color.css()}; color: ${color.gray < 192 ? "white" : "black"}">`,
  246. );
  247. htmlDone.push(`✓`);
  248. htmlDone.push(`</div>`);
  249. htmlDone.push(`</div>`);
  250. } else {
  251. if (
  252. areaGroup === fillerData.currentGroup ||
  253. areaGroup.progressPercent > 0.0
  254. ) {
  255. } // 需要展示circle progress
  256. htmlUndoned.push(
  257. `<div id="color-btn-container-${i}" class="color-btn-container" onclick="selectColor(this, event, ${i})">`,
  258. );
  259. htmlUndoned.push(
  260. `<svg id="color-btn-progress-ring-${i}" class="color-btn-progress-ring" viewBox="0 0 48 48"><circle class="color-btn-progress-ring-track" cx="50%" cy="50%" r="45%" fill="transparent" stroke="#fff" stroke-width="5%" /> <circle id="color-btn-progress-ring-value-${i}" class="color-btn-progress-ring-value" cx="50%" cy="50%" r="45%" fill="transparent" stroke="#2ecc71" stroke-width="5%" stroke-linecap="round" /></svg>`,
  261. );
  262. htmlUndoned.push(
  263. `<div id="color-btn-${i}" class="color-btn" style="background-color:${color.css()}; color: ${color.gray < 192 ? "white" : "black"}">`,
  264. );
  265. htmlUndoned.push(`${i + 1}`);
  266. htmlUndoned.push(`</div>`);
  267. htmlUndoned.push(`</div>`);
  268. }
  269. }
  270. html = htmlUndoned.concat(htmlDone);
  271. let c = document.querySelector("#color-btns");
  272. if (c != null) {
  273. c.innerHTML = html.join("");
  274. }
  275. // 选中当前color
  276. cssSelectColor(fillerData.currentGroupIndex);
  277. // 调整每个color item的进度
  278. for (var i = 0; i < data.length; i++) {
  279. let areaGroup = data[i];
  280. let percent = areaGroup.progressPercent;
  281. cssInitColorProgress(i, percent);
  282. }
  283. (window as any).selectColor = function (
  284. el: HTMLElement,
  285. event: MouseEvent,
  286. i: number,
  287. ) {
  288. console.log("select", el, event, i);
  289. fillerData.setCurrentGroup(i);
  290. document.querySelectorAll(".color-btn-container").forEach((item) => {
  291. item.classList.remove("color-btn-container-selected");
  292. });
  293. el.classList.add("color-btn-container-selected");
  294. };
  295. }
  296. /**
  297. * 调整整体完成进度条
  298. */
  299. function cssAdjustProgress(percent: number) {
  300. let progressElem = document.getElementById("progress") as HTMLDivElement;
  301. let percentElem = document.getElementById("percent") as HTMLDivElement;
  302. percentElem.innerText = `${percent}%`;
  303. progressElem.style.width = `${percent}%`;
  304. }
  305. // 选中某个color, 自动切换到下一个颜色的时候触发调用
  306. function cssSelectColor(i: number) {
  307. document.querySelectorAll(".color-btn-container").forEach((item) => {
  308. item.classList.remove("color-btn-container-selected");
  309. });
  310. let elem = document.getElementById(`color-btn-container-${i}`);
  311. elem?.classList.add("color-btn-container-selected");
  312. }
  313. // 设置color button的进度,即areagroup的进度
  314. // SVG viewBox 常量,所有strokeDasharray/offset计算均在此坐标系内
  315. const COLOR_BTN_VIEWBOX = 48;
  316. const COLOR_BTN_RING_RATIO = 0.45;
  317. const COLOR_BTN_RADIUS = COLOR_BTN_VIEWBOX * COLOR_BTN_RING_RATIO; // 21.6
  318. const COLOR_BTN_CIRCUMFERENCE = 2 * Math.PI * COLOR_BTN_RADIUS;
  319. function cssSetColorProgress(i: number, percent: number) {
  320. const progressCircle = document.getElementById(
  321. `color-btn-progress-ring-value-${i}`,
  322. )!;
  323. const offset =
  324. COLOR_BTN_CIRCUMFERENCE - (percent * COLOR_BTN_CIRCUMFERENCE) / 100;
  325. progressCircle.style.strokeDashoffset = offset.toString();
  326. if (percent >= 100) {
  327. // 如果此color已经完成,文本变为✓, 同时移动到队列后面
  328. const item = document.getElementById(`color-btn-${i}`)!;
  329. item.innerText = "✓";
  330. }
  331. }
  332. // 初始化color button进度
  333. function cssInitColorProgress(i: number, percent: number) {
  334. const progressCircle = document.getElementById(
  335. `color-btn-progress-ring-value-${i}`,
  336. )!;
  337. const offset =
  338. COLOR_BTN_CIRCUMFERENCE - (percent * COLOR_BTN_CIRCUMFERENCE) / 100;
  339. progressCircle.style.strokeDasharray = `${COLOR_BTN_CIRCUMFERENCE} ${COLOR_BTN_CIRCUMFERENCE}`;
  340. progressCircle.style.strokeDashoffset = offset.toString();
  341. }
  342. // 调整滚动条
  343. function cssAdjustScroll(fd: FillerData) {
  344. let wrapper = document.getElementById("color-btns")!;
  345. let count =
  346. document.body.clientWidth /
  347. (wrapper.scrollWidth / fd.data.areaGroups.length); // 视窗内显示了多少个元素
  348. let width = document.body.clientWidth / count; // 每个元素占用的宽度
  349. // 计算位置
  350. let scrollpos =
  351. (wrapper.scrollWidth / fd.data.areaGroups.length) *
  352. (fd.currentGroupIndex - fd.doneBeforeCount) -
  353. (count * width) / 2 +
  354. width / 2;
  355. scrollpos = scrollpos < 0 ? 0 : scrollpos;
  356. // wrapper.scrollLeft = scrollpos ; // 不要直接设置scrollLeft,太突然没效果
  357. try {
  358. wrapper.scroll({ left: scrollpos, top: 0, behavior: "smooth" }); // 发现有些低端机不支持
  359. } catch (e) {
  360. console.log(e);
  361. wrapper.scrollLeft = scrollpos;
  362. }
  363. }
  364. /**
  365. * 数字item爆破动画
  366. */
  367. function cssExplosionAnimation(
  368. numElem: HTMLElement,
  369. color: string,
  370. onfinish: Function,
  371. ) {
  372. let aniCanvas = document.createElement("canvas");
  373. aniCanvas.className = "finish-ani-canvas";
  374. let coords = numElem.getBoundingClientRect();
  375. aniCanvas.style.left = `${coords.left - 50}px`;
  376. aniCanvas.style.top = `${coords.top - 50}px`;
  377. aniCanvas.style.width = `${coords.width + 100}px`;
  378. aniCanvas.style.height = `${coords.height + 100}px`;
  379. document.body.append(aniCanvas);
  380. let explosion = new Explosion(aniCanvas, color, 50, () => {
  381. onfinish();
  382. aniCanvas.remove();
  383. });
  384. explosion.explode();
  385. }
  386. // 某个颜色填充完毕
  387. function cssColorDone(fillerData: FillerData) {
  388. let i = fillerData.currentGroupIndex;
  389. let color = new Color(fillerData.currentGroup!.color).css();
  390. let wrapper = document.getElementById("color-btns")!;
  391. let buttonWrap = document.getElementById(`color-btn-container-${i}`)!;
  392. setTimeout(() => {
  393. //等环形进度条走完,开始爆破动画
  394. buttonWrap.style.visibility = "hidden";
  395. cssExplosionAnimation(buttonWrap, color, () => {
  396. // 爆破动画结束后,将元素移至末尾
  397. wrapper.lastElementChild!.after(buttonWrap);
  398. buttonWrap.style.visibility = "visible";
  399. });
  400. }, 300);
  401. }
  402. // 全部填完
  403. function cssOnFinish(
  404. scene: FillerScene,
  405. workLayer: WorkLayer,
  406. hintLayer: HintLayer,
  407. numberLayer: NumberLayer,
  408. audio: AudioPlayer | null,
  409. ) {
  410. // 立即将 canvas 底色改为透明,让 body 渐变色从绘图区域透出,避免 replay/消失动画中出现局部白色块
  411. scene.setClearTransparent();
  412. // 隐藏toolbar
  413. const toolbarBottom = document.getElementById(
  414. "toolbar-bottom",
  415. ) as HTMLDivElement;
  416. // 爆破动画结束后隐藏元素
  417. setTimeout(() => {
  418. if (scene.fillerData!.config.settings.sound) {
  419. audio?.playAudio(AudioType.AllDone);
  420. }
  421. scene.resetToResult();
  422. toolbarBottom.classList.add("hidden-toolbar-bottom"); // 添加 hidden 类,触发动画
  423. // 动画结束后隐藏元素
  424. setTimeout(() => {
  425. // toolbarBottom.style.display = 'none';
  426. scene.addLayer(
  427. new FrameLayer(
  428. scene,
  429. -4,
  430. -4,
  431. scene.fillerData!.width + 8,
  432. scene.fillerData!.height + 8,
  433. 0xff9bceed,
  434. 16,
  435. ),
  436. );
  437. hintLayer.enabled = false; // replay 期间隐藏 hint 覆盖层
  438. numberLayer.enabled = false; // replay 期间隐藏 number 覆盖层
  439. scene.invalidate();
  440. workLayer.replay().then(() => showPromoScreen());
  441. }, 500); // 过渡动画持续 0.5 秒
  442. }, 300); // 粒子爆破动画持续 0.3 秒
  443. setTimeout(() => {
  444. confetti();
  445. }, 300);
  446. }
  447. function showPromoScreen() {
  448. const canvas = document.getElementById("canvas") as HTMLCanvasElement;
  449. const promoScreen = document.getElementById("promo-screen") as HTMLDivElement;
  450. const promoColoring = document.getElementById(
  451. "promo-coloring",
  452. ) as HTMLImageElement;
  453. const promoSlogon = document.getElementById(
  454. "promo-slogon",
  455. ) as HTMLImageElement;
  456. // 预设图片资源(已在 init 阶段预加载,此处保留赋值保证兼容性)
  457. promoColoring.src = coloringPagesUrl;
  458. promoSlogon.src = slogonUrl;
  459. // 1. 将画布缩小至消失
  460. canvas.classList.add("canvas-shrink-out");
  461. // 2. canvas 过渡完成后(~600ms)展示宣传界面 + logo 淡入
  462. setTimeout(() => {
  463. canvas.style.display = "none";
  464. promoScreen.classList.add("visible");
  465. // 双 rAF:确保浏览器渲染初始态后再触发 transition
  466. requestAnimationFrame(() => {
  467. requestAnimationFrame(() => {
  468. promoColoring.classList.add("animate-in");
  469. promoSlogon.classList.add("animate-in");
  470. // 宣传界面出现后强化 CTA 按钮
  471. ctaHighlight();
  472. });
  473. });
  474. }, 620);
  475. }
  476. function onHint(audio: AudioPlayer, scene: FillerScene) {
  477. if (scene.fillerData?.config.settings.sound) {
  478. audio.playAudio(AudioType.Hint);
  479. }
  480. scene.hint();
  481. }
  482. function goback() {
  483. if (document.referrer.includes(window.location.hostname)) {
  484. window.history.back();
  485. } else {
  486. window.location.href = "/"; // 无来源时跳首页
  487. }
  488. }
  489. function vibrate() {
  490. // 判断浏览器是否支持震动
  491. var supportVibrate = "vibrate" in navigator;
  492. console.log("supportVibrate: " + supportVibrate);
  493. if (supportVibrate) {
  494. navigator.vibrate(30);
  495. }
  496. }
  497. function showToast(message: string) {
  498. const toast = document.getElementById("toast");
  499. if (toast) {
  500. toast.textContent = message;
  501. toast.classList.remove("toast-hidden");
  502. setTimeout(() => toast.classList.add("toast-hidden"), 2500);
  503. }
  504. }
  505. // 撒花动画,使用js-confetti库
  506. function confetti() {
  507. const jsConfetti = new JSConfetti();
  508. jsConfetti.addConfetti();
  509. }
  510. function onActionSheetClick(evt: Event) {
  511. evt.stopPropagation();
  512. }