|
@@ -28,7 +28,8 @@ export class FingerHint {
|
|
|
fingerUrl: string,
|
|
fingerUrl: string,
|
|
|
) {
|
|
) {
|
|
|
this.el = this.createEl(fingerUrl);
|
|
this.el = this.createEl(fingerUrl);
|
|
|
- document.body.appendChild(this.el);
|
|
|
|
|
|
|
+ const gameArea = document.getElementById("game-area");
|
|
|
|
|
+ (gameArea || document.body).appendChild(this.el);
|
|
|
this.injectStyle();
|
|
this.injectStyle();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -39,6 +40,12 @@ export class FingerHint {
|
|
|
this.scheduleHint(800);
|
|
this.scheduleHint(800);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /** 画布尺寸或矩阵变化后调用,重新按最新 userMat 定位当前提示 */
|
|
|
|
|
+ refresh() {
|
|
|
|
|
+ if (this.stopped || this.el.style.display === "none") return;
|
|
|
|
|
+ this.showHint();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/** 每次用户有交互动作(填色成功/失败)时调用 */
|
|
/** 每次用户有交互动作(填色成功/失败)时调用 */
|
|
|
onUserInteraction() {
|
|
onUserInteraction() {
|
|
|
this.hide();
|
|
this.hide();
|
|
@@ -73,7 +80,11 @@ export class FingerHint {
|
|
|
|
|
|
|
|
private scheduleHint(delay: number) {
|
|
private scheduleHint(delay: number) {
|
|
|
if (this.idleTimer !== null) clearTimeout(this.idleTimer);
|
|
if (this.idleTimer !== null) clearTimeout(this.idleTimer);
|
|
|
- this.idleTimer = window.setTimeout(() => this.showHint(), delay);
|
|
|
|
|
|
|
+ this.idleTimer = window.setTimeout(() => {
|
|
|
|
|
+ requestAnimationFrame(() => {
|
|
|
|
|
+ requestAnimationFrame(() => this.showHint());
|
|
|
|
|
+ });
|
|
|
|
|
+ }, delay);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private showHint() {
|
|
private showHint() {
|
|
@@ -83,36 +94,33 @@ export class FingerHint {
|
|
|
const area = group.firstUncoloredArea;
|
|
const area = group.firstUncoloredArea;
|
|
|
if (!area) return;
|
|
if (!area) return;
|
|
|
|
|
|
|
|
- const [cssX, cssY] = this.contentToScreen(area.center.x, area.center.y);
|
|
|
|
|
|
|
+ const [cssX, cssY] = this.contentToCanvasCss(area.center.x, area.center.y);
|
|
|
this.show(cssX, cssY);
|
|
this.show(cssX, cssY);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 将内容坐标(图片像素空间)转换为 CSS 页面坐标(视口 px)
|
|
|
|
|
|
|
+ * 将内容坐标(图片像素空间)转换为 canvas 内 CSS 坐标。
|
|
|
*
|
|
*
|
|
|
* Scene 的 userMat 是列主序矩阵,对简单 scale+translate 变换:
|
|
* Scene 的 userMat 是列主序矩阵,对简单 scale+translate 变换:
|
|
|
* physX = cx * mat[0] + cy * mat[4] + mat[12]
|
|
* physX = cx * mat[0] + cy * mat[4] + mat[12]
|
|
|
* physY = cx * mat[1] + cy * mat[5] + mat[13]
|
|
* physY = cx * mat[1] + cy * mat[5] + mat[13]
|
|
|
- * 再除以 devicePixelRatio 得到 canvas 内 CSS px;
|
|
|
|
|
- * 最后加上 canvas 元素的视口偏移,以匹配 position:fixed 坐标系。
|
|
|
|
|
|
|
+ * 再除以 canvas 实际像素尺寸 / CSS 尺寸,得到 canvas 内 CSS px。
|
|
|
|
|
+ * 手指 DOM 挂在 #game-area 内部,因此无需再叠加 viewport 偏移。
|
|
|
*/
|
|
*/
|
|
|
- private contentToScreen(cx: number, cy: number): [number, number] {
|
|
|
|
|
|
|
+ private contentToCanvasCss(cx: number, cy: number): [number, number] {
|
|
|
const mat = this.scene.userMat;
|
|
const mat = this.scene.userMat;
|
|
|
- const ratio = this.scene.ratio;
|
|
|
|
|
const physX = cx * mat[0] + cy * mat[4] + mat[12];
|
|
const physX = cx * mat[0] + cy * mat[4] + mat[12];
|
|
|
const physY = cx * mat[1] + cy * mat[5] + mat[13];
|
|
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 canvas = this.scene.gl.canvas as HTMLCanvasElement;
|
|
|
const rect = canvas.getBoundingClientRect();
|
|
const rect = canvas.getBoundingClientRect();
|
|
|
- return [canvasCssX + rect.left, canvasCssY + rect.top];
|
|
|
|
|
|
|
+ const scaleX = canvas.width / rect.width;
|
|
|
|
|
+ const scaleY = canvas.height / rect.height;
|
|
|
|
|
+ return [physX / scaleX, physY / scaleY];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private show(cssX: number, cssY: number) {
|
|
private show(cssX: number, cssY: number) {
|
|
|
const size = FingerHint.SIZE;
|
|
const size = FingerHint.SIZE;
|
|
|
- // 将指尖对齐到目标坐标
|
|
|
|
|
|
|
+ // 将指尖严格对齐到目标坐标;不要 clamp 容器位置,否则会破坏指尖与内容坐标的对应关系。
|
|
|
this.el.style.left = `${cssX - size * FingerHint.TIP_X}px`;
|
|
this.el.style.left = `${cssX - size * FingerHint.TIP_X}px`;
|
|
|
this.el.style.top = `${cssY - size * FingerHint.TIP_Y}px`;
|
|
this.el.style.top = `${cssY - size * FingerHint.TIP_Y}px`;
|
|
|
this.el.style.display = "block";
|
|
this.el.style.display = "block";
|
|
@@ -129,7 +137,7 @@ export class FingerHint {
|
|
|
const el = document.createElement("div");
|
|
const el = document.createElement("div");
|
|
|
el.id = "finger-hint";
|
|
el.id = "finger-hint";
|
|
|
el.style.cssText = `
|
|
el.style.cssText = `
|
|
|
- position: fixed;
|
|
|
|
|
|
|
+ position: absolute;
|
|
|
width: ${size}px;
|
|
width: ${size}px;
|
|
|
height: ${size}px;
|
|
height: ${size}px;
|
|
|
pointer-events: none;
|
|
pointer-events: none;
|