| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154 |
- import { FillerData } from "./FillerData";
- import { FillerScene } from "./FillerScene";
- /**
- * DOM overlay 手指提示层
- * - 页面初始化后 800ms 自动指向当前颜色组第一个待填区块中心
- * - 用户每次交互(点击填色/点击失败)后重置 2s 闲置计时器
- * - 全图填完后停止
- */
- export class FingerHint {
- private el: HTMLDivElement;
- private idleTimer: number | null = null;
- private stopped = false;
- private static readonly IDLE_MS = 2000;
- /** 手指图片尺寸(CSS px) */
- private static readonly SIZE = 72;
- /**
- * 指尖在图片内的相对位置(0~1)。
- * finger.png 通常是食指斜向下指,指尖在图片左上角约 (25%, 12%)。
- */
- private static readonly TIP_X = 0.25;
- private static readonly TIP_Y = 0.12;
- constructor(
- private scene: FillerScene,
- private fillerData: FillerData,
- fingerUrl: string,
- ) {
- this.el = this.createEl(fingerUrl);
- document.body.appendChild(this.el);
- this.injectStyle();
- }
- // ─── 公开 API ──────────────────────────────────────────────────
- /** 初始化完成后调用,启动首次提示 */
- start() {
- this.scheduleHint(800);
- }
- /** 每次用户有交互动作(填色成功/失败)时调用 */
- onUserInteraction() {
- this.hide();
- if (!this.stopped) {
- this.scheduleHint(FingerHint.IDLE_MS);
- }
- }
- /** 全图填完时调用 */
- stop() {
- this.stopped = true;
- this.hide();
- if (this.idleTimer !== null) {
- clearTimeout(this.idleTimer);
- this.idleTimer = null;
- }
- }
- // ─── 私有实现 ──────────────────────────────────────────────────
- private scheduleHint(delay: number) {
- if (this.idleTimer !== null) clearTimeout(this.idleTimer);
- this.idleTimer = window.setTimeout(() => this.showHint(), delay);
- }
- private showHint() {
- if (this.stopped) return;
- const group = this.fillerData.currentGroup;
- if (!group) return;
- const area = group.firstUncoloredArea;
- if (!area) return;
- const [cssX, cssY] = this.contentToScreen(area.center.x, area.center.y);
- this.show(cssX, cssY);
- }
- /**
- * 将内容坐标(图片像素空间)转换为 CSS 页面坐标(视口 px)
- *
- * Scene 的 userMat 是列主序矩阵,对简单 scale+translate 变换:
- * physX = cx * mat[0] + cy * mat[4] + mat[12]
- * physY = cx * mat[1] + cy * mat[5] + mat[13]
- * 再除以 devicePixelRatio 得到 canvas 内 CSS px;
- * 最后加上 canvas 元素的视口偏移,以匹配 position:fixed 坐标系。
- */
- private contentToScreen(cx: number, cy: number): [number, number] {
- const mat = this.scene.userMat;
- const ratio = this.scene.ratio;
- const physX = cx * mat[0] + cy * mat[4] + mat[12];
- const physY = cx * mat[1] + cy * mat[5] + mat[13];
- // canvas 内 CSS 像素
- const canvasCssX = physX / ratio;
- const canvasCssY = physY / ratio;
- // 加上 canvas 在视口中的偏移,转换为 position:fixed 坐标
- const canvas = this.scene.gl.canvas as HTMLElement;
- const rect = canvas.getBoundingClientRect();
- return [canvasCssX + rect.left, canvasCssY + rect.top];
- }
- private show(cssX: number, cssY: number) {
- const size = FingerHint.SIZE;
- // 将指尖对齐到目标坐标
- this.el.style.left = `${cssX - size * FingerHint.TIP_X}px`;
- this.el.style.top = `${cssY - size * FingerHint.TIP_Y}px`;
- this.el.style.display = "block";
- this.el.classList.add("finger-tapping");
- }
- private hide() {
- this.el.style.display = "none";
- this.el.classList.remove("finger-tapping");
- }
- private createEl(fingerUrl: string): HTMLDivElement {
- const size = FingerHint.SIZE;
- const el = document.createElement("div");
- el.id = "finger-hint";
- el.style.cssText = `
- position: fixed;
- width: ${size}px;
- height: ${size}px;
- pointer-events: none;
- z-index: 999;
- display: none;
- transform-origin: ${FingerHint.TIP_X * 100}% ${FingerHint.TIP_Y * 100}%;
- `;
- const img = document.createElement("img");
- img.src = fingerUrl;
- img.style.cssText = "width:100%;height:100%;object-fit:contain;";
- img.draggable = false;
- el.appendChild(img);
- return el;
- }
- private injectStyle() {
- if (document.getElementById("finger-hint-style")) return;
- const style = document.createElement("style");
- style.id = "finger-hint-style";
- style.textContent = `
- @keyframes finger-tap {
- 0% { transform: scale(1) translateY(0); opacity: 1; }
- 35% { transform: scale(0.88) translateY(10px); opacity: 1; }
- 55% { transform: scale(0.88) translateY(10px); opacity: 1; }
- 80% { transform: scale(1) translateY(0); opacity: 1; }
- 100% { transform: scale(1) translateY(0); opacity: 1; }
- }
- #finger-hint.finger-tapping {
- animation: finger-tap 1s ease-in-out infinite;
- }
- `;
- document.head.appendChild(style);
- }
- }
|