not bad
This commit is contained in:
parent
2741b93dea
commit
434f07cc4e
103 changed files with 8387 additions and 8152 deletions
378
js/ui_input_shared.js
Normal file
378
js/ui_input_shared.js
Normal file
|
|
@ -0,0 +1,378 @@
|
|||
"use strict";
|
||||
|
||||
(function (global) {
|
||||
function cursorForTool(tool) {
|
||||
if (tool === "water_hose") return "crosshair";
|
||||
if (tool === "pinch") return "grab";
|
||||
if (tool === "observe") return "default";
|
||||
if (tool === "poke" || tool === "delete") return "pointer";
|
||||
return "crosshair";
|
||||
}
|
||||
|
||||
function createInputContext({ canvas, world, ui, uiCache }) {
|
||||
if (!canvas || !world || !uiCache) return null;
|
||||
const inputModeManager = global.TarinaiInputMode;
|
||||
|
||||
function setMobileInputMode(mode = "auto") {
|
||||
mode = inputModeManager?.setMode?.(mode) || (["auto", "camera", "tool", "family"].includes(mode) ? mode : "auto");
|
||||
uiCache.mobileInputMode = mode;
|
||||
ui.mobileModeControls?.querySelectorAll("[data-mobile-mode]").forEach(btn => btn.classList.toggle("active", btn.dataset.mobileMode === mode));
|
||||
document.body.classList.toggle("mobile-mode-camera", mode === "camera");
|
||||
document.body.classList.toggle("mobile-mode-tool", mode === "tool");
|
||||
document.body.classList.toggle("mobile-mode-family", mode === "family");
|
||||
return mode;
|
||||
}
|
||||
|
||||
function updateTouchMobileUiClass() {
|
||||
const enabled = Boolean(inputModeManager?.refresh?.().touchFirst);
|
||||
document.body.classList.toggle("touch-mobile-ui", enabled);
|
||||
if (ui.mobileModeControls) ui.mobileModeControls.hidden = !enabled;
|
||||
return enabled;
|
||||
}
|
||||
|
||||
function mobileInputMode() {
|
||||
return inputModeManager?.currentMode || uiCache.mobileInputMode || "auto";
|
||||
}
|
||||
|
||||
function bindMobileModeControls() {
|
||||
updateTouchMobileUiClass();
|
||||
window.addEventListener("resize", updateTouchMobileUiClass, { passive: true });
|
||||
window.matchMedia?.("(pointer: coarse)")?.addEventListener?.("change", updateTouchMobileUiClass);
|
||||
setMobileInputMode(uiCache.mobileInputMode || "auto");
|
||||
ui.mobileModeControls?.addEventListener("click", (e) => {
|
||||
const btn = e.target.closest("[data-mobile-mode]");
|
||||
if (!btn) return;
|
||||
audio.uiClick?.();
|
||||
setMobileInputMode(btn.dataset.mobileMode || "auto");
|
||||
showToast(`\u30b9\u30de\u30db\u64cd\u4f5c: ${btn.textContent || btn.dataset.mobileMode}`);
|
||||
});
|
||||
}
|
||||
|
||||
function touchPoint(touches, index = 0) {
|
||||
const t = touches?.[index];
|
||||
if (!t) return null;
|
||||
return { clientX: t.clientX, clientY: t.clientY };
|
||||
}
|
||||
|
||||
function touchCenter(touches) {
|
||||
const a = touchPoint(touches, 0);
|
||||
const b = touchPoint(touches, 1);
|
||||
if (!a) return null;
|
||||
if (!b) return a;
|
||||
return { clientX: (a.clientX + b.clientX) / 2, clientY: (a.clientY + b.clientY) / 2 };
|
||||
}
|
||||
|
||||
function touchDistance(touches) {
|
||||
const a = touchPoint(touches, 0);
|
||||
const b = touchPoint(touches, 1);
|
||||
if (!a || !b) return 0;
|
||||
return Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY);
|
||||
}
|
||||
|
||||
function screenToWorldClient(clientX, clientY) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const sx = clientX - rect.left;
|
||||
const sy = clientY - rect.top;
|
||||
return world.screenToWorld ? world.screenToWorld(sx, sy) : { x: sx, y: sy, inside: sx >= 0 && sy >= 0 };
|
||||
}
|
||||
|
||||
function screenToWorldEvent(e) {
|
||||
if (typeof screenToWorld === "function") return screenToWorld(e);
|
||||
return screenToWorldClient(e.clientX, e.clientY);
|
||||
}
|
||||
|
||||
function clientToWorldRaw(clientX, clientY) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const sx = clientX - rect.left;
|
||||
const sy = clientY - rect.top;
|
||||
const scale = world.viewScale ? world.viewScale() : 1;
|
||||
const off = world.fieldScreenOffset ? world.fieldScreenOffset() : { x: 0, y: 0 };
|
||||
const rawX = (sx - off.x) / scale + (world.cameraX || 0);
|
||||
const rawY = (sy - off.y) / scale + (world.cameraY || 0);
|
||||
return { x: rawX, y: rawY, inside: rawX >= 0 && rawY >= 0 && rawX <= world.w && rawY <= world.h };
|
||||
}
|
||||
|
||||
function setZoomAroundClientPoint(clientX, clientY, nextZoom) {
|
||||
if (!world.setFieldZoom) return false;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const sx = clientX - rect.left;
|
||||
const sy = clientY - rect.top;
|
||||
const focus = world.screenToWorld ? world.screenToWorld(sx, sy) : { x: sx, y: sy };
|
||||
if (!world.setFieldZoom(nextZoom)) return false;
|
||||
const off = world.fieldScreenOffset ? world.fieldScreenOffset() : { x: 0, y: 0 };
|
||||
const scale = world.viewScale ? world.viewScale() : 1;
|
||||
world.cameraX = focus.x - (sx - off.x) / scale;
|
||||
world.cameraY = focus.y - (sy - off.y) / scale;
|
||||
world.clampCamera?.();
|
||||
return true;
|
||||
}
|
||||
|
||||
function isClientOverFrozenPanel(clientX, clientY) {
|
||||
const panel = document.getElementById("frozenPanel");
|
||||
if (!panel) return false;
|
||||
const r = panel.getBoundingClientRect();
|
||||
return clientX >= r.left && clientX <= r.right && clientY >= r.top && clientY <= r.bottom;
|
||||
}
|
||||
|
||||
function rememberGrabOrigin(target) {
|
||||
if (!target || uiCache.grabKind !== "tarinai") return;
|
||||
uiCache.grabOrigin = { x: target.x, y: target.y, vx: target.vx || 0, vy: target.vy || 0 };
|
||||
}
|
||||
|
||||
function finishTarinaiGrab(target, clientX, clientY) {
|
||||
if (!target || uiCache.grabKind !== "tarinai") return false;
|
||||
if (isClientOverFrozenPanel(clientX, clientY) && global.TarinaiFreezeSystem?.freezeTarinai) {
|
||||
global.TarinaiFreezeSystem.freezeTarinai(target, world);
|
||||
uiCache.grabOrigin = null;
|
||||
return true;
|
||||
}
|
||||
const p = screenToWorldClient(clientX, clientY);
|
||||
if (!p.inside && uiCache.grabOrigin) {
|
||||
target.x = uiCache.grabOrigin.x;
|
||||
target.y = uiCache.grabOrigin.y;
|
||||
target.vx = uiCache.grabOrigin.vx || 0;
|
||||
target.vy = uiCache.grabOrigin.vy || 0;
|
||||
target.thought = "\u30d5\u30a3\u30fc\u30eb\u30c9\u306b\u623b\u3055\u308c\u305f";
|
||||
target.pinchThoughtTimer = Math.max(target.pinchThoughtTimer || 0, 0.8);
|
||||
showToast("\u30d5\u30a3\u30fc\u30eb\u30c9\u5916\u306a\u306e\u3067\u5143\u306e\u4f4d\u7f6e\u306b\u623b\u3057\u307e\u3057\u305f\u3002");
|
||||
uiCache.grabOrigin = null;
|
||||
return true;
|
||||
}
|
||||
uiCache.grabOrigin = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
function prepareGrabTarget(target) {
|
||||
if (uiCache.grabKind === "tarinai") {
|
||||
const lodged = target.currentLodgedPin?.() || (world.items || []).find(it => it && isPinType(it.type) && it.pinState === "lodged" && it.pinTargetId === target.id);
|
||||
if (lodged?.detachPushpin) lodged.detachPushpin(world, "pinch");
|
||||
target.sleeping = false;
|
||||
if (target.state === "sleep" || target.state === "seek_bed") target.goIdle?.("\u3064\u307e\u307e\u308c\u3066\u3044\u308b");
|
||||
target.surpriseTimer = Math.max(target.surpriseTimer || 0, 0.18);
|
||||
target.thought = "\u3064\u307e\u307e\u308c\u3066\u3044\u308b";
|
||||
target.pinchThoughtTimer = Math.max(target.pinchThoughtTimer || 0, 1.2);
|
||||
} else {
|
||||
if (isPinType(target.type) && target.pinState === "lodged" && target.detachPushpin) target.detachPushpin(world, "pinch");
|
||||
target.dropTimer = 0;
|
||||
target.dropMax = 0;
|
||||
target.dropImpactDone = true;
|
||||
}
|
||||
}
|
||||
|
||||
function beginGrab(p, clientX, clientY, options = {}) {
|
||||
const target = world.findGrabTargetAt ? world.findGrabTargetAt(p.x, p.y) : null;
|
||||
if (!target) {
|
||||
if (options.notifyNoTarget) showToast("\u3064\u307e\u3081\u308b\u5bfe\u8c61\u304c\u3042\u308a\u307e\u305b\u3093\u3002");
|
||||
return false;
|
||||
}
|
||||
audio.grab?.();
|
||||
uiCache.grabbing = true;
|
||||
uiCache.grabMoved = false;
|
||||
uiCache.grabTarget = target;
|
||||
uiCache.grabKind = target instanceof Tarinai ? "tarinai" : "item";
|
||||
rememberGrabOrigin(target);
|
||||
uiCache.grabLastWorldX = p.x;
|
||||
uiCache.grabLastWorldY = p.y;
|
||||
uiCache.grabStartX = clientX || 0;
|
||||
uiCache.grabStartY = clientY || 0;
|
||||
prepareGrabTarget(target);
|
||||
if (options.touchMode) uiCache.touchMode = options.touchMode;
|
||||
renderStats();
|
||||
return true;
|
||||
}
|
||||
|
||||
function moveGrab(p, clientX, clientY, options = {}) {
|
||||
const target = uiCache.grabTarget;
|
||||
if (!target || target.dead) {
|
||||
uiCache.grabbing = false;
|
||||
uiCache.grabTarget = null;
|
||||
if (options.touch) uiCache.touchMode = "";
|
||||
canvas.style.cursor = world.tool === "pinch" ? "grab" : "default";
|
||||
return false;
|
||||
}
|
||||
const dx = p.x - (uiCache.grabLastWorldX ?? p.x);
|
||||
const dy = p.y - (uiCache.grabLastWorldY ?? p.y);
|
||||
uiCache.grabLastWorldX = p.x;
|
||||
uiCache.grabLastWorldY = p.y;
|
||||
const pad = target.radius || target.r || 16;
|
||||
if (uiCache.grabKind === "tarinai" && !p.inside) {
|
||||
const raw = clientToWorldRaw(clientX, clientY);
|
||||
target.x = raw.x;
|
||||
target.y = raw.y;
|
||||
} else {
|
||||
target.x = clamp(p.x, CONFIG.worldPadding + pad * 0.15, world.w - CONFIG.worldPadding - pad * 0.15);
|
||||
target.y = clamp(p.y, CONFIG.worldPadding + pad * 0.15, world.h - CONFIG.worldPadding - pad * 0.15);
|
||||
}
|
||||
target.vx = clamp(dx * 16, -160, 160);
|
||||
target.vy = clamp(dy * 16, -160, 160);
|
||||
if (uiCache.grabKind === "tarinai") {
|
||||
target.goIdle?.("\u3064\u307e\u307e\u308c\u3066\u3044\u308b");
|
||||
if (options.touch) target.pinchThoughtTimer = Math.max(target.pinchThoughtTimer || 0, 0.8);
|
||||
} else if (target.type === "ball") {
|
||||
target.prevX = target.x - dx;
|
||||
target.prevY = target.y - dy;
|
||||
target.spinVelocity = clamp((target.spinVelocity || 0) + dx * 0.06, -38, 38);
|
||||
}
|
||||
world.drawListDirty = true;
|
||||
const now = performance.now();
|
||||
if (!uiCache.grabLastSpatialAt || now - uiCache.grabLastSpatialAt > 80) {
|
||||
world.rebuildSpatial?.();
|
||||
uiCache.grabLastSpatialAt = now;
|
||||
}
|
||||
canvas.style.cursor = "grabbing";
|
||||
if (options.renderFrame) render();
|
||||
return true;
|
||||
}
|
||||
|
||||
function endGrab(clientX, clientY) {
|
||||
if (!uiCache.grabbing) return false;
|
||||
const target = uiCache.grabTarget;
|
||||
if (target) {
|
||||
if (uiCache.grabKind === "tarinai") {
|
||||
target.target = null;
|
||||
target.targetKey = "";
|
||||
target.surpriseTimer = Math.max(target.surpriseTimer || 0, 0.15);
|
||||
finishTarinaiGrab(target, clientX, clientY);
|
||||
} else if (target.type === "ball") {
|
||||
target.prevX = target.x;
|
||||
target.prevY = target.y;
|
||||
}
|
||||
}
|
||||
audio.drop?.();
|
||||
uiCache.grabbing = false;
|
||||
uiCache.grabTarget = null;
|
||||
uiCache.grabKind = "";
|
||||
world.rebuildSpatial?.();
|
||||
uiCache.grabLastSpatialAt = 0;
|
||||
canvas.style.cursor = cursorForTool(world.tool);
|
||||
renderStats();
|
||||
return true;
|
||||
}
|
||||
|
||||
function beginHose(p, clientX, clientY, options = {}) {
|
||||
audio.waterHose?.();
|
||||
uiCache.hosing = true;
|
||||
uiCache.hoseMoved = false;
|
||||
uiCache.hoseLastWorldX = p.x;
|
||||
uiCache.hoseLastWorldY = p.y;
|
||||
uiCache.hoseStartX = clientX;
|
||||
uiCache.hoseStartY = clientY;
|
||||
world.applyWaterHose?.(p.x, p.y, 0, 0, 0.10);
|
||||
if (options.touchMode) uiCache.touchMode = options.touchMode;
|
||||
render();
|
||||
return true;
|
||||
}
|
||||
|
||||
function moveHose(p, clientX, clientY, options = {}) {
|
||||
const dx = p.x - (uiCache.hoseLastWorldX ?? p.x);
|
||||
const dy = p.y - (uiCache.hoseLastWorldY ?? p.y);
|
||||
uiCache.hoseLastWorldX = p.x;
|
||||
uiCache.hoseLastWorldY = p.y;
|
||||
const totalMove = Math.hypot(clientX - (uiCache.hoseStartX || clientX), clientY - (uiCache.hoseStartY || clientY));
|
||||
if (totalMove > 3) {
|
||||
uiCache.hoseMoved = true;
|
||||
if (options.touch) uiCache.touchMoved = true;
|
||||
}
|
||||
if (p.inside) world.applyWaterHose?.(p.x, p.y, dx, dy, 0.10);
|
||||
canvas.style.cursor = "crosshair";
|
||||
render();
|
||||
}
|
||||
|
||||
function endHose() {
|
||||
if (!uiCache.hosing) return false;
|
||||
uiCache.hosing = false;
|
||||
canvas.style.cursor = cursorForTool(world.tool);
|
||||
world.rebuildSpatial?.();
|
||||
renderStats();
|
||||
return true;
|
||||
}
|
||||
|
||||
function beginPan(clientX, clientY, button = null) {
|
||||
uiCache.panning = true;
|
||||
uiCache.panMoved = false;
|
||||
uiCache.panButton = button;
|
||||
uiCache.panStartX = clientX;
|
||||
uiCache.panStartY = clientY;
|
||||
uiCache.panLastX = clientX;
|
||||
uiCache.panLastY = clientY;
|
||||
}
|
||||
|
||||
function movePan(clientX, clientY) {
|
||||
if (!uiCache.panning) return false;
|
||||
const dx = clientX - uiCache.panLastX;
|
||||
const dy = clientY - uiCache.panLastY;
|
||||
uiCache.panLastX = clientX;
|
||||
uiCache.panLastY = clientY;
|
||||
const totalMove = Math.hypot(clientX - uiCache.panStartX, clientY - uiCache.panStartY);
|
||||
if (totalMove > 8) {
|
||||
canvas.style.cursor = "grabbing";
|
||||
world.panCamera?.(-dx, -dy);
|
||||
uiCache.panMoved = true;
|
||||
render();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function endPan(button = null) {
|
||||
if (!uiCache.panning) return false;
|
||||
if (uiCache.panButton !== null && button !== null && button !== uiCache.panButton) return false;
|
||||
uiCache.panning = false;
|
||||
uiCache.panButton = null;
|
||||
canvas.style.cursor = cursorForTool(world.tool);
|
||||
return true;
|
||||
}
|
||||
|
||||
function updatePointerFromEvent(e) {
|
||||
const p = screenToWorldEvent(e);
|
||||
const prevX = world.pointer.x;
|
||||
const prevY = world.pointer.y;
|
||||
world.pointer.x = p.x;
|
||||
world.pointer.y = p.y;
|
||||
world.pointer.inside = !!p.inside;
|
||||
const delta = Math.hypot((p.x || 0) - (prevX || 0), (p.y || 0) - (prevY || 0));
|
||||
world.pointer.motion = clamp(delta / 18, 0, 2);
|
||||
world.pointer.movedAt = performance.now();
|
||||
}
|
||||
|
||||
function cancelPointerActions() {
|
||||
uiCache.touchMode = "";
|
||||
uiCache.touchMoved = false;
|
||||
uiCache.hosing = false;
|
||||
uiCache.grabbing = false;
|
||||
uiCache.grabTarget = null;
|
||||
uiCache.grabKind = "";
|
||||
}
|
||||
|
||||
return {
|
||||
canvas,
|
||||
world,
|
||||
ui,
|
||||
uiCache,
|
||||
cursorForTool,
|
||||
setMobileInputMode,
|
||||
updateTouchMobileUiClass,
|
||||
mobileInputMode,
|
||||
bindMobileModeControls,
|
||||
touchPoint,
|
||||
touchCenter,
|
||||
touchDistance,
|
||||
screenToWorldClient,
|
||||
screenToWorldEvent,
|
||||
clientToWorldRaw,
|
||||
setZoomAroundClientPoint,
|
||||
beginGrab,
|
||||
moveGrab,
|
||||
endGrab,
|
||||
beginHose,
|
||||
moveHose,
|
||||
endHose,
|
||||
beginPan,
|
||||
movePan,
|
||||
endPan,
|
||||
updatePointerFromEvent,
|
||||
cancelPointerActions,
|
||||
};
|
||||
}
|
||||
|
||||
global.TarinaiUIInputShared = Object.freeze({ createInputContext, cursorForTool });
|
||||
})(window);
|
||||
Loading…
Add table
Add a link
Reference in a new issue