@solidrt/3d 0.0.60 → 0.0.61

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -47,12 +47,12 @@
47
47
  // control, not two mounts. Only the initial pose is copied at creation;
48
48
  // later pose changes go through set() and the verbs.
49
49
 
50
- import { createMemo, createSignal, untrack } from "@solidjs/signals"
50
+ import { untrack } from "@solidjs/signals"
51
51
  import { createAxes } from "@solidrt/core/input"
52
52
  import type { Axes, Vec2 } from "@solidrt/core/input"
53
53
  import type { CameraUpdate } from "./camera.ts"
54
54
  import type { Vec3 } from "./math.ts"
55
- import { easeStep, GLIDE_EASE, GLIDE_EPSILON } from "./motion.ts"
55
+ import { checkShake, createActivity, createLanes, easeStep, GLIDE_EASE, GLIDE_EPSILON } from "@solidrt/core/camera-control"
56
56
 
57
57
  // One element height of drag sweeps this many turns of look (half a turn:
58
58
  // a drag across the screen turns the walker around).
@@ -61,6 +61,8 @@ const DRAG_TURNS = 0.5
61
61
  const LOOK_RATE = 0.4
62
62
  // Default walking speed, world units per second.
63
63
  const MOVE_SPEED = 3
64
+ // Default `boostSpeed`: the multiplier on moveSpeed while `boost` is held.
65
+ const BOOST_SPEED = 2
64
66
  // Pitch clamps stop short of the poles so the look direction never
65
67
  // degenerates against world up.
66
68
  const PITCH_LIMIT = Math.PI / 2 - 0.01
@@ -84,6 +86,11 @@ export type FirstPersonCameraOptions = FirstPersonPose & {
84
86
  maxPitch?: number
85
87
  /** Movement speed in world units per second (default 3). */
86
88
  moveSpeed?: number
89
+ /** Multiplier on `moveSpeed` while the `boost` axis reads non-zero (a
90
+ * held Shift, a pressed stick; default 2): the sprint, walking and
91
+ * flying alike. Scales rates only; a `move` delta is a step in world
92
+ * units regardless. */
93
+ boostSpeed?: number
87
94
  /** Multiplier over the built-in look sensitivities (drags, mouse motion
88
95
  * and rates alike). */
89
96
  lookSpeed?: number
@@ -100,7 +107,7 @@ export type FirstPersonCameraOptions = FirstPersonPose & {
100
107
  clampPosition?: (next: Vec3, current: Vec3) => Vec3
101
108
  }
102
109
 
103
- export type FirstPersonAxes = { look: "vec2"; move: "vec2"; rise: "axis" }
110
+ export type FirstPersonAxes = { look: "vec2"; move: "vec2"; rise: "axis"; boost: "axis" }
104
111
 
105
112
  export type FirstPersonCamera = {
106
113
  /** Eye position (a fresh array per call). */
@@ -127,7 +134,9 @@ export type FirstPersonCamera = {
127
134
  update(dt: number): boolean
128
135
  /** The input abstraction: `look` (vec2, element heights of drag / turns
129
136
  * per second), `move` (vec2 [right, forward], forward = -y; world units
130
- * per delta, `moveSpeed` per second) and `rise` (axis, fly only). */
137
+ * per delta, `moveSpeed` per second), `rise` (axis, fly only) and
138
+ * `boost` (axis: non-zero multiplies the move and rise rates by
139
+ * `boostSpeed`; a button binding reads 1 while held). */
131
140
  axes: Axes<FirstPersonAxes>
132
141
  /** Turn by radians (yaw positive left, pitch positive up; clamps
133
142
  * apply) and push. */
@@ -136,6 +145,13 @@ export type FirstPersonCamera = {
136
145
  * projection when walking, the view direction when flying) and up
137
146
  * (world up, fly mode only), through `clampPosition`, and push. */
138
147
  moveBy(right: number, forward: number, up?: number): void
148
+ /** A shake on the lane (okf/design/camera-controls.md): a view KICK,
149
+ * the first-person convention - `strength` is the peak rotation in
150
+ * TURNS (0.01 is 3.6 degrees) of yaw (x) and pitch (y), the look
151
+ * axis's unit, `duration` seconds, an optional frequency and direction
152
+ * ([1, 0] yaws only). Shakes sum and decay; the pose is untouched - a
153
+ * hit, a recoil, a footstep. */
154
+ shake(strength: number, duration: number, opts?: { frequency?: number; direction?: Vec2 }): void
139
155
  }
140
156
 
141
157
  // The goal pose a glide eases toward (see the header).
@@ -174,6 +190,7 @@ export function createFirstPersonCamera(camera: FirstPersonTarget, options: Firs
174
190
  // Everything below the pose is read from `options` where it applies.
175
191
  let lookSpeed = () => options.lookSpeed ?? 1
176
192
  let moveSpeed = () => options.moveSpeed ?? MOVE_SPEED
193
+ let boostSpeed = () => options.boostSpeed ?? BOOST_SPEED
177
194
  let clampedPitch = (v: number) => clampNum(v, options.minPitch ?? -PITCH_LIMIT, options.maxPitch ?? PITCH_LIMIT)
178
195
  let clampPitch = () => {
179
196
  pitch = clampedPitch(pitch)
@@ -184,22 +201,25 @@ export function createFirstPersonCamera(camera: FirstPersonTarget, options: Firs
184
201
  // that starts or drops it refreshes (ownedWrite: entries run from
185
202
  // component bodies and handlers alike).
186
203
  let motion: Motion | null = null
187
- let [motionActive, setMotionActive] = createSignal(false, { ownedWrite: true })
188
- let notify = () => {
189
- let now = motion !== null
190
- if (now !== untrack(motionActive)) setMotionActive(now)
191
- }
204
+ let lanes = createLanes()
205
+ // The frame-loop gate (core's createActivity), built once the axes exist.
206
+ let activity!: ReturnType<typeof createActivity>
207
+ let notify = () => activity.notify()
192
208
  let interrupt = () => {
193
209
  motion = null
194
210
  }
195
211
 
196
- let forward = (): Vec3 => {
197
- let cp = Math.cos(pitch)
198
- return [-Math.sin(yaw) * cp, Math.sin(pitch), -Math.cos(yaw) * cp]
212
+ let forwardOf = (y: number, p: number): Vec3 => {
213
+ let cp = Math.cos(p)
214
+ return [-Math.sin(y) * cp, Math.sin(p), -Math.cos(y) * cp]
199
215
  }
216
+ let forward = (): Vec3 => forwardOf(yaw, pitch)
217
+ // The shake lane kicks the view: the pushed look direction turns by the
218
+ // lane's yaw and pitch (turns), the pose and the eye stay.
200
219
  let push = () => {
201
220
  changed = true
202
- let f = forward()
221
+ let [ky, kp] = lanes.total(undefined)
222
+ let f = ky === 0 && kp === 0 ? forward() : forwardOf(yaw - ky * 2 * Math.PI, clampedPitch(pitch - kp * 2 * Math.PI))
203
223
  camera.setCamera({ position: copy(position), target: [position[0] + f[0], position[1] + f[1], position[2] + f[2]] })
204
224
  }
205
225
  let look = (dYaw: number, dPitch: number) => {
@@ -241,7 +261,7 @@ export function createFirstPersonCamera(camera: FirstPersonTarget, options: Firs
241
261
  }
242
262
 
243
263
  let axes = createAxes<FirstPersonAxes>(
244
- { look: "vec2", move: "vec2", rise: "axis" },
264
+ { look: "vec2", move: "vec2", rise: "axis", boost: "axis" },
245
265
  {
246
266
  onBegin: () => {
247
267
  interrupt()
@@ -264,7 +284,8 @@ export function createFirstPersonCamera(camera: FirstPersonTarget, options: Firs
264
284
  },
265
285
  },
266
286
  )
267
- let active = createMemo(() => motionActive() || axes.active())
287
+ activity = createActivity(() => motion !== null || lanes.active(), () => axes.active())
288
+ let active = activity.active
268
289
 
269
290
  clampPitch()
270
291
  push()
@@ -309,7 +330,9 @@ export function createFirstPersonCamera(camera: FirstPersonTarget, options: Firs
309
330
  let [mx, my] = untrack(() => axes.rate("move"))
310
331
  let rise = untrack(() => axes.rate("rise"))
311
332
  if (mx !== 0 || my !== 0 || rise !== 0) {
312
- let speed = moveSpeed() * dt
333
+ // A held boost (any non-zero read) scales the rates, not a nudge.
334
+ let boost = untrack(() => axes.rate("boost")) !== 0 ? boostSpeed() : 1
335
+ let speed = moveSpeed() * boost * dt
313
336
  interrupt()
314
337
  step(mx * speed, -my * speed, rise * speed)
315
338
  moved = true
@@ -318,6 +341,10 @@ export function createFirstPersonCamera(camera: FirstPersonTarget, options: Firs
318
341
  glideStep(dt)
319
342
  moved = true
320
343
  }
344
+ if (lanes.active() && dt > 0) {
345
+ lanes.step(dt)
346
+ moved = true
347
+ }
321
348
  if (moved) push()
322
349
  notify()
323
350
  let result = changed
@@ -341,5 +368,10 @@ export function createFirstPersonCamera(camera: FirstPersonTarget, options: Firs
341
368
  push()
342
369
  notify()
343
370
  },
371
+ shake(strength, duration, opts) {
372
+ checkShake("createFirstPersonCamera", strength, duration, opts)
373
+ lanes.shake(strength, duration, opts)
374
+ notify()
375
+ },
344
376
  }
345
377
  }