Files
onboarding-project/src/templateConfig/beforeResourcesLoadedCb.ts
T
2026-08-14 15:03:43 +03:00

138 lines
6.2 KiB
TypeScript

import {
CameraC_internal,
CameraType,
JoystickC,
Physics_internal,
Template,
Template3d,
ThreeC_internal,
TweenC,
FilterScene,
InstallBanner,
} from "@hitplay/playable_template";
import { CameraC } from "../controllers/CameraC";
import { CameraFollowC } from "../controllers/CameraFollowC";
import { ThreeC } from "../controllers/ThreeC";
import { Color } from "three";
import { Vec3 } from "cannon-es";
export const beforeResourcesLoadedCb = () => {
FilterScene.init();
InstallBanner.init();
Template3d.init();
CameraC_internal.init(CameraType.perspective);
CameraC.setCamera(window.screenSize.portrait);
ThreeC_internal.init();
ThreeC.createBaseLights();
ThreeC.setupDirectionalLight();
Physics_internal.init(new Vec3(0, -9.81, 0));
TweenC.init();
// zone is omitted on purpose: JoystickC auto-creates a full-screen zone
// when none is given, so the joystick can appear wherever the player taps
JoystickC.init({
mode: "dynamic",
fadeTime: 100,
restJoystick: true,
restOpacity: 0,
follow: false,
// nipplejs's own default is 100; the inner thumb is always exactly
// half of this by nipplejs's own rule — irrelevant here since .front's
// size is overridden independently in main.css.
size: 105,
// "front" (the thumb/finger position) is a single flat color. Its
// actual on-screen SIZE is overridden separately in main.css, since
// nipplejs ties it to a hardcoded half of "size" above with no
// independent option. "back" (the ring) is a single flat, darkened
// color around most of its circumference, except a wide arc right
// where the thumb sits, which brightens toward the thumb's own color
// as if lit by it. --joy-angle (WHERE) and --joy-force (HOW STRONG)
// are live CSS vars driven from onJoysticMove below. The ring's
// transparent middle and the thumb's forced-opaque override
// (nipplejs hardcodes the thumb at 50% opacity) also live in
// main.css, since `color` can't express either.
//
// Alpha is fixed (not interpolated to 0) at every stop, only the RGB
// channels shift with --joy-force — interpolating alpha down to 0
// at rest made the highlight's position read as a see-through gap
// instead of matching the base color. Alpha itself still ramps up
// slightly with force (0.75 -> 0.95) so the highlight reads as the
// thumb's actual saturated color rather than diluted by the sandy
// background showing through it.
color: {
back: "conic-gradient(from var(--joy-angle, 0deg), rgba(calc(70 + 185 * var(--joy-force, 0)), calc(55 + 85 * var(--joy-force, 0)), calc(45 - 5 * var(--joy-force, 0)), calc(0.75 + 0.2 * var(--joy-force, 0))) 0deg, rgba(70, 55, 45, 0.75) 85deg, rgba(70, 55, 45, 0.75) 275deg, rgba(calc(70 + 185 * var(--joy-force, 0)), calc(55 + 85 * var(--joy-force, 0)), calc(45 - 5 * var(--joy-force, 0)), calc(0.75 + 0.2 * var(--joy-force, 0))) 360deg)",
front: "#ff8c28",
},
});
// Keeps --joy-angle/--joy-force (read by the conic-gradient above)
// tracking the thumb in real time.
//
// Angle: uses the thumb's own pixel offset from center (frontPosition —
// what nipplejs itself uses to `translate()` the thumb) through
// Math.atan2, not nipplejs's own `angle` field (flipped into a
// "human-readable" convention internally and rotated again for
// orientation-lock logical space — neither matches plain CSS/screen
// angle math). atan2 and CSS conic-gradient both sweep clockwise as the
// angle increases, but their zero points differ by 90° (atan2's 0° is
// 3 o'clock/east; conic-gradient's `from 0deg` is 12 o'clock/north) — the
// fix is a flat +90 added once, not a sign flip.
//
// Force: nipplejs documents `force` as normalized 0..1, but it's
// actually computed from the RAW pre-clamp touch distance, while
// frontPosition (and so the angle above) uses the CLAMPED distance — so
// force keeps climbing past 1 the further you drag beyond the ring's
// edge even though the thumb itself is visibly pinned there. Every
// calc() above assumes force is 0..1, so it's clamped once here rather
// than in every calc() in the gradient string.
//
// `data.instance` isn't in the SDK's own (stale) type declaration for
// this event, though it's really there on every nipplejs 1.x payload —
// the cast below bridges that gap.
JoystickC.onJoysticMove.addDelegate(({ data }) => {
const { instance, force } = data as unknown as {
instance?: { frontPosition: { x: number; y: number }; ui: { el: HTMLElement } };
force?: number;
};
if (!instance) return;
const { x, y } = instance.frontPosition;
const angleDeg = Math.atan2(y, x) * (180 / Math.PI) + 90;
const clampedForce = Math.min(Math.max(force ?? 0, 0), 1);
instance.ui.el.style.setProperty("--joy-angle", `${angleDeg}deg`);
instance.ui.el.style.setProperty("--joy-force", `${clampedForce}`);
});
Template.updateVariableConfig.addDelegate(([category, variable, value]) => {
if (category === "global") {
switch (variable) {
case "light_intensity":
ThreeC_internal.defaultAmbientLight.intensity = Number(value);
break;
case "light_color":
ThreeC_internal.defaultAmbientLight.color = new Color(value as any);
break;
default:
// The camera's config is split between two owners on purpose:
// FOV lives on the camera itself (CameraC), while the rig —
// offset and angle — belongs to CameraFollowC, which is the only
// thing allowed to write camera.rotation. Routing each field to
// its own owner is what keeps them from stomping each other.
if (["camera_fov_p", "camera_fov_l"].includes(variable)) {
CameraC.setCamera(window.screenSize.portrait);
} else if (
["camera_position_p", "camera_position_l", "camera_rotation_p", "camera_rotation_l"].includes(variable)
) {
// No-ops safely before the scene exists: CameraFollowC reads
// the same config again in its own init().
CameraFollowC.applyRigFromConfig();
}
break;
}
return;
}
});
};