| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587 |
- import { BgLayer, BgType } from "../base/BgLayer";
- import { BorderLayer } from "../base/BorderLayer";
- import { BoxLayer } from "../base/BoxLayer";
- import { DebugLayer } from "../base/DebugLayer";
- import { FrameLayer } from "../base/FrameLayer";
- import { TouchTracker } from "../base/Gesture";
- import { ImageShaders } from "../base/ImageShaders";
- import { Padding, Scene } from "../base/Scene";
- import { TextureLayer } from "../base/TextureLayer";
- import { loadImage } from "../base/utils";
- import AudioPlayer, { AudioType } from "./Audio";
- import { AreaGroup, AreaGroups, Color, Settings } from "./common";
- import Explosion from "./explosion";
- import {
- FillerConfig,
- FillerData,
- FillerResource,
- FillerCallback,
- } from "./FillerData";
- import { FillerScene } from "./FillerScene";
- import { HintLayer } from "./HintLayer";
- import { LineArtLayer } from "./LineArtLayer";
- import { NumberLayer } from "./NumberLayer";
- import { WorkLayer } from "./WorkLayer";
- import { LoadingController } from "./LoadingController";
- import { FingerHint } from "./FingerHint";
- import JSConfetti from "js-confetti";
- import { initCta, ctaHighlight } from "./cta";
- import { adPlatform } from "./ad-platform/current";
- // 静态导入资源,Vite 构建时会将它们转为 data URI 内联进 HTML
- import configRaw from "/assets/res/6a18f7d9957ac783bad75479/config.json?raw";
- import pageUrl from "/assets/res/6a18f7d9957ac783bad75479/page.png?url";
- import mapUrl from "/assets/res/6a18f7d9957ac783bad75479/map.png?url";
- import specialUrl from "/assets/res/6a18f7d9957ac783bad75479/special.jpeg?url";
- import numberFontUrl from "/assets/fonts/numbers_roboto_500.png?url";
- import fingerUrl from "/assets/img/finger.png?url";
- import logoUrl from "/assets/img/logo.png?url";
- import logoTxtUrl from "/assets/img/logo-txt.png?url";
- import coloringPagesUrl from "/assets/img/coloring-pages.png?url";
- import slogonUrl from "/assets/img/slogon.png?url";
- window.addEventListener("load", () => {
- document.body.dataset.adPlatform = adPlatform.platform;
- adPlatform.init();
- initCta(adPlatform);
- init();
- });
- async function test() {
- let canvas = document.querySelector("#canvas") as HTMLCanvasElement;
- canvas.width = canvas.clientWidth * window.devicePixelRatio;
- canvas.height = canvas.clientHeight * window.devicePixelRatio;
- let resource = await loadResource();
- let ctx = canvas.getContext("2d")!;
- ctx.fillStyle = "red";
- ctx.fillRect(0, 0, canvas.width, canvas.height);
- ctx.drawImage(resource.page, 0, 0);
- }
- async function init() {
- const loadingController = new LoadingController({
- minDisplayTime: 1000, // 最小显示时间1秒
- fadeDuration: 300, // 淡出动画300ms
- });
- if (adPlatform.shouldUseCustomLoading()) {
- loadingController.show();
- }
- let canvas = document.querySelector("#canvas") as HTMLCanvasElement;
- let gl = canvas.getContext("webgl2", {
- premultipliedAlpha: false,
- }) as WebGL2RenderingContext;
- let pixelRatio = window.devicePixelRatio;
- /** 根据 game-area 的实际渲染尺寸同步 canvas 像素尺寸 */
- function syncCanvasSize() {
- const gameArea = document.getElementById("game-area")!;
- const w = Math.round(gameArea.clientWidth * pixelRatio);
- const h = Math.round(gameArea.clientHeight * pixelRatio);
- if (canvas.width !== w || canvas.height !== h) {
- canvas.width = w;
- canvas.height = h;
- scene?.updateViewport();
- }
- }
- canvas.width = canvas.clientWidth * pixelRatio;
- canvas.height = canvas.clientHeight * pixelRatio;
- let scene = new FillerScene(gl, pixelRatio);
- // 确保 canvas 像素尺寸与实际 flex 布局一致(body.onload 时 dvh 可能还未最终结算)
- syncCanvasSize();
- let audio = new AudioPlayer();
- // 加载设置项,学个新语法 ...
- let settings: Settings = {
- hintStyle: "default",
- sound: true,
- vibrate: true,
- autoNext: true,
- };
- window.addEventListener("resize", () => syncCanvasSize());
- let resource = await loadResource();
- if (adPlatform.shouldUseCustomLoading()) {
- loadingController.hide();
- }
- adPlatform.onResourcesLoaded();
- // 设置固定顶栏 logo(图标 + 文字)
- (document.getElementById("app-logo") as HTMLImageElement).src = logoUrl;
- (document.getElementById("app-logo-txt") as HTMLImageElement).src =
- logoTxtUrl;
- // 预加载 promo 图片,避免动画播放时图片刚加载导致布局跳变
- (document.getElementById("promo-coloring") as HTMLImageElement).src =
- coloringPagesUrl;
- (document.getElementById("promo-slogon") as HTMLImageElement).src = slogonUrl;
- let taskList: number[] = [];
- // fingerHint 先声明,init 末尾赋值后回调中就能使用
- let fingerHint: FingerHint | null = null;
- let fillerData = new FillerData(
- new FillerConfig(settings),
- resource,
- gl,
- taskList,
- {
- onFillFailed() {
- adPlatform.onGameStart();
- console.log("填充失败");
- fingerHint?.onUserInteraction();
- },
- onFillSuccess() {
- adPlatform.onGameStart();
- console.log("填充成功");
- fingerHint?.onUserInteraction();
- if (fillerData.config.settings.vibrate) {
- vibrate();
- }
- cssAdjustProgress(fillerData.data.coloredPercent);
- cssSetColorProgress(
- fillerData.currentGroupIndex,
- fillerData.currentGroup!.progressPercent,
- );
- if (fillerData.currentGroup?.isAllColored) {
- if (fillerData.config.settings.sound) {
- audio.playAudio(AudioType.ColorDone);
- }
- cssColorDone(fillerData);
- }
- },
- onSwitchGroup() {
- let currentGroupIndex = fillerData.currentGroupIndex;
- console.log(`切换到下一个group ${currentGroupIndex}`);
- cssSelectColor(currentGroupIndex);
- cssAdjustScroll(fillerData);
- },
- onFinish() {
- fingerHint?.stop();
- adPlatform.onGameEnd();
- cssOnFinish(scene, workLayer, hintLayer, numberLayer, audio);
- },
- },
- );
- scene.fillerData = fillerData;
- console.log("resource", resource);
- // canvas 区域与所有 UI 完全分离,只保留极小视觉边距
- scene.setContentPadding(new Padding(20, 20, 20, 20));
- //let page = await loadImage('/webgl/page.png')
- //let page = await loadImage('/assets/resources/friend/page_gray.png')
- let width = fillerData.width;
- let height = fillerData.height;
- scene.setContentSize(width, height);
- // // 最底层:透明背景,canvas 透出 body 渐变色,与整体 UI 融为一体
- // scene.addLayer(new BackgroundLayer(scene, scene.width, scene.height, [0, 0, 0, 0]));
- if (resource.bg) {
- scene.addLayer(
- new BgLayer(scene, resource.bg, scene.width, scene.height, BgType.Repeat),
- );
- }
- scene.addLayer(new BoxLayer(scene, 0, 0, width, height));
- let hintLayer = new HintLayer(scene, fillerData);
- scene.addLayer(hintLayer);
- let numberLayer = new NumberLayer(scene, resource.numberImage, fillerData, 1);
- scene.addLayer(numberLayer);
- let workLayer = new WorkLayer(scene, fillerData);
- scene.addLayer(workLayer);
- //scene.addLayer(new TextureLayer(scene, workLayer.mask.texture, fillerData.width, fillerData.height, fillerData.width, fillerData.height))
- scene.addLayer(new LineArtLayer(scene, resource.page, width, height));
- scene.addLayer(
- new BorderLayer(
- scene,
- 0,
- 0,
- fillerData.width,
- fillerData.height,
- 0xff222222,
- ),
- );
- // let mask = new Mask(gl, fillerData)
- // resource.config[0].areas.forEach(area => mask.addArea(area))
- // mask.flush()
- //scene.addTestLayer(new TextureLayer(scene, mask.texture, fillerData.width, fillerData.height, width, height, ImageShaders.CommonGaussianBlurCut))
- //scene.addTestLayer(new TextureLayer(scene, mask.texture, fillerData.width, fillerData.height, width, height, ))
- //scene.addLayer(new TextureLayer(scene, hintLayer.mask.texture, fillerData.width, fillerData.height, width, height, ImageShaders.CommonGaussianBlur ))
- //scene.addLayer(new DebugLayer(scene))
- //scene.addLayer(new ImageLayer(scene, resource.page, width, height, gl.LINEAR, ImageShaders.CommonGaussianBlur))
- //scene.addLayer(new TextureLayer(scene, fillerData.mapTexure, width, height, width, height))
- createButtons(fillerData);
- cssAdjustProgress(fillerData.data.coloredPercent);
- workLayer.initTask();
- if (fillerData.data.coloredPercent >= 100) {
- adPlatform.onGameEnd();
- cssOnFinish(scene, workLayer, hintLayer, numberLayer, null);
- } else {
- // 创建手指提示,全图未完成时启动引导
- fingerHint = new FingerHint(scene, fillerData, fingerUrl);
- fingerHint.start();
- }
- }
- async function loadResource(): Promise<FillerResource> {
- const config = JSON.parse(configRaw) as AreaGroups;
- const [page, map, special, numberImage] = await Promise.all([
- loadImage(pageUrl),
- loadImage(mapUrl),
- loadImage(specialUrl),
- loadImage(numberFontUrl),
- ]);
- return new FillerResource(
- config,
- page as HTMLImageElement,
- map as HTMLImageElement,
- numberImage as HTMLImageElement,
- [],
- special as HTMLImageElement,
- );
- }
- function createButtons(fillerData: FillerData) {
- let data = fillerData.data.areaGroups;
- let htmlUndoned = [];
- let htmlDone = [];
- let html = [];
- let areaGroup: AreaGroup;
- let color: Color;
- for (var i = 0; i < data.length; i++) {
- areaGroup = data[i];
- color = new Color(areaGroup.color);
- if (areaGroup.isAllColored) {
- htmlDone.push(
- `<div id="color-btn-container-${i}" class="color-btn-container" onclick="selectColor(this, event, ${i})">`,
- );
- htmlDone.push(
- `<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>`,
- );
- htmlDone.push(
- `<div id="color-btn-${i}" class="color-btn" style="background-color:${color.css()}; color: ${color.gray < 192 ? "white" : "black"}">`,
- );
- htmlDone.push(`✓`);
- htmlDone.push(`</div>`);
- htmlDone.push(`</div>`);
- } else {
- if (
- areaGroup === fillerData.currentGroup ||
- areaGroup.progressPercent > 0.0
- ) {
- } // 需要展示circle progress
- htmlUndoned.push(
- `<div id="color-btn-container-${i}" class="color-btn-container" onclick="selectColor(this, event, ${i})">`,
- );
- htmlUndoned.push(
- `<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>`,
- );
- htmlUndoned.push(
- `<div id="color-btn-${i}" class="color-btn" style="background-color:${color.css()}; color: ${color.gray < 192 ? "white" : "black"}">`,
- );
- htmlUndoned.push(`${i + 1}`);
- htmlUndoned.push(`</div>`);
- htmlUndoned.push(`</div>`);
- }
- }
- html = htmlUndoned.concat(htmlDone);
- let c = document.querySelector("#color-btns");
- if (c != null) {
- c.innerHTML = html.join("");
- }
- // 选中当前color
- cssSelectColor(fillerData.currentGroupIndex);
- // 调整每个color item的进度
- for (var i = 0; i < data.length; i++) {
- let areaGroup = data[i];
- let percent = areaGroup.progressPercent;
- cssInitColorProgress(i, percent);
- }
- (window as any).selectColor = function (
- el: HTMLElement,
- event: MouseEvent,
- i: number,
- ) {
- console.log("select", el, event, i);
- fillerData.setCurrentGroup(i);
- document.querySelectorAll(".color-btn-container").forEach((item) => {
- item.classList.remove("color-btn-container-selected");
- });
- el.classList.add("color-btn-container-selected");
- };
- }
- /**
- * 调整整体完成进度条
- */
- function cssAdjustProgress(percent: number) {
- let progressElem = document.getElementById("progress") as HTMLDivElement;
- let percentElem = document.getElementById("percent") as HTMLDivElement;
- percentElem.innerText = `${percent}%`;
- progressElem.style.width = `${percent}%`;
- }
- // 选中某个color, 自动切换到下一个颜色的时候触发调用
- function cssSelectColor(i: number) {
- document.querySelectorAll(".color-btn-container").forEach((item) => {
- item.classList.remove("color-btn-container-selected");
- });
- let elem = document.getElementById(`color-btn-container-${i}`);
- elem?.classList.add("color-btn-container-selected");
- }
- // 设置color button的进度,即areagroup的进度
- // SVG viewBox 常量,所有strokeDasharray/offset计算均在此坐标系内
- const COLOR_BTN_VIEWBOX = 48;
- const COLOR_BTN_RING_RATIO = 0.45;
- const COLOR_BTN_RADIUS = COLOR_BTN_VIEWBOX * COLOR_BTN_RING_RATIO; // 21.6
- const COLOR_BTN_CIRCUMFERENCE = 2 * Math.PI * COLOR_BTN_RADIUS;
- function cssSetColorProgress(i: number, percent: number) {
- const progressCircle = document.getElementById(
- `color-btn-progress-ring-value-${i}`,
- )!;
- const offset =
- COLOR_BTN_CIRCUMFERENCE - (percent * COLOR_BTN_CIRCUMFERENCE) / 100;
- progressCircle.style.strokeDashoffset = offset.toString();
- if (percent >= 100) {
- // 如果此color已经完成,文本变为✓, 同时移动到队列后面
- const item = document.getElementById(`color-btn-${i}`)!;
- item.innerText = "✓";
- }
- }
- // 初始化color button进度
- function cssInitColorProgress(i: number, percent: number) {
- const progressCircle = document.getElementById(
- `color-btn-progress-ring-value-${i}`,
- )!;
- const offset =
- COLOR_BTN_CIRCUMFERENCE - (percent * COLOR_BTN_CIRCUMFERENCE) / 100;
- progressCircle.style.strokeDasharray = `${COLOR_BTN_CIRCUMFERENCE} ${COLOR_BTN_CIRCUMFERENCE}`;
- progressCircle.style.strokeDashoffset = offset.toString();
- }
- // 调整滚动条
- function cssAdjustScroll(fd: FillerData) {
- let wrapper = document.getElementById("color-btns")!;
- let count =
- document.body.clientWidth /
- (wrapper.scrollWidth / fd.data.areaGroups.length); // 视窗内显示了多少个元素
- let width = document.body.clientWidth / count; // 每个元素占用的宽度
- // 计算位置
- let scrollpos =
- (wrapper.scrollWidth / fd.data.areaGroups.length) *
- (fd.currentGroupIndex - fd.doneBeforeCount) -
- (count * width) / 2 +
- width / 2;
- scrollpos = scrollpos < 0 ? 0 : scrollpos;
- // wrapper.scrollLeft = scrollpos ; // 不要直接设置scrollLeft,太突然没效果
- try {
- wrapper.scroll({ left: scrollpos, top: 0, behavior: "smooth" }); // 发现有些低端机不支持
- } catch (e) {
- console.log(e);
- wrapper.scrollLeft = scrollpos;
- }
- }
- /**
- * 数字item爆破动画
- */
- function cssExplosionAnimation(
- numElem: HTMLElement,
- color: string,
- onfinish: Function,
- ) {
- let aniCanvas = document.createElement("canvas");
- aniCanvas.className = "finish-ani-canvas";
- let coords = numElem.getBoundingClientRect();
- aniCanvas.style.left = `${coords.left - 50}px`;
- aniCanvas.style.top = `${coords.top - 50}px`;
- aniCanvas.style.width = `${coords.width + 100}px`;
- aniCanvas.style.height = `${coords.height + 100}px`;
- document.body.append(aniCanvas);
- let explosion = new Explosion(aniCanvas, color, 50, () => {
- onfinish();
- aniCanvas.remove();
- });
- explosion.explode();
- }
- // 某个颜色填充完毕
- function cssColorDone(fillerData: FillerData) {
- let i = fillerData.currentGroupIndex;
- let color = new Color(fillerData.currentGroup!.color).css();
- let wrapper = document.getElementById("color-btns")!;
- let buttonWrap = document.getElementById(`color-btn-container-${i}`)!;
- setTimeout(() => {
- //等环形进度条走完,开始爆破动画
- buttonWrap.style.visibility = "hidden";
- cssExplosionAnimation(buttonWrap, color, () => {
- // 爆破动画结束后,将元素移至末尾
- wrapper.lastElementChild!.after(buttonWrap);
- buttonWrap.style.visibility = "visible";
- });
- }, 300);
- }
- // 全部填完
- function cssOnFinish(
- scene: FillerScene,
- workLayer: WorkLayer,
- hintLayer: HintLayer,
- numberLayer: NumberLayer,
- audio: AudioPlayer | null,
- ) {
- // 立即将 canvas 底色改为透明,让 body 渐变色从绘图区域透出,避免 replay/消失动画中出现局部白色块
- scene.setClearTransparent();
- // 隐藏toolbar
- const toolbarBottom = document.getElementById(
- "toolbar-bottom",
- ) as HTMLDivElement;
- // 爆破动画结束后隐藏元素
- setTimeout(() => {
- if (scene.fillerData!.config.settings.sound) {
- audio?.playAudio(AudioType.AllDone);
- }
- scene.resetToResult();
- toolbarBottom.classList.add("hidden-toolbar-bottom"); // 添加 hidden 类,触发动画
- // 动画结束后隐藏元素
- setTimeout(() => {
- // toolbarBottom.style.display = 'none';
- scene.addLayer(
- new FrameLayer(
- scene,
- -4,
- -4,
- scene.fillerData!.width + 8,
- scene.fillerData!.height + 8,
- 0xff9bceed,
- 16,
- ),
- );
- hintLayer.enabled = false; // replay 期间隐藏 hint 覆盖层
- numberLayer.enabled = false; // replay 期间隐藏 number 覆盖层
- scene.invalidate();
- workLayer.replay().then(() => showPromoScreen());
- }, 500); // 过渡动画持续 0.5 秒
- }, 300); // 粒子爆破动画持续 0.3 秒
- setTimeout(() => {
- confetti();
- }, 300);
- }
- function showPromoScreen() {
- const canvas = document.getElementById("canvas") as HTMLCanvasElement;
- const promoScreen = document.getElementById("promo-screen") as HTMLDivElement;
- const promoColoring = document.getElementById(
- "promo-coloring",
- ) as HTMLImageElement;
- const promoSlogon = document.getElementById(
- "promo-slogon",
- ) as HTMLImageElement;
- // 预设图片资源(已在 init 阶段预加载,此处保留赋值保证兼容性)
- promoColoring.src = coloringPagesUrl;
- promoSlogon.src = slogonUrl;
- // 1. 将画布缩小至消失
- canvas.classList.add("canvas-shrink-out");
- // 2. canvas 过渡完成后(~600ms)展示宣传界面 + logo 淡入
- setTimeout(() => {
- canvas.style.display = "none";
- promoScreen.classList.add("visible");
- // 双 rAF:确保浏览器渲染初始态后再触发 transition
- requestAnimationFrame(() => {
- requestAnimationFrame(() => {
- promoColoring.classList.add("animate-in");
- promoSlogon.classList.add("animate-in");
- // 宣传界面出现后强化 CTA 按钮
- ctaHighlight();
- });
- });
- }, 620);
- }
- function onHint(audio: AudioPlayer, scene: FillerScene) {
- if (scene.fillerData?.config.settings.sound) {
- audio.playAudio(AudioType.Hint);
- }
- scene.hint();
- }
- function goback() {
- if (document.referrer.includes(window.location.hostname)) {
- window.history.back();
- } else {
- window.location.href = "/"; // 无来源时跳首页
- }
- }
- function vibrate() {
- // 判断浏览器是否支持震动
- var supportVibrate = "vibrate" in navigator;
- console.log("supportVibrate: " + supportVibrate);
- if (supportVibrate) {
- navigator.vibrate(30);
- }
- }
- function showToast(message: string) {
- const toast = document.getElementById("toast");
- if (toast) {
- toast.textContent = message;
- toast.classList.remove("toast-hidden");
- setTimeout(() => toast.classList.add("toast-hidden"), 2500);
- }
- }
- // 撒花动画,使用js-confetti库
- function confetti() {
- const jsConfetti = new JSConfetti();
- jsConfetti.addConfetti();
- }
- function onActionSheetClick(evt: Event) {
- evt.stopPropagation();
- }
|