add-mouse-driven-orbit

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Add Mouse-Driven Orbit

添加鼠标驱动轨道

Turn one damped pointer target into a shallow camera arc and smaller object rotations. Reach for
threejs
with
OrbitControls
when the user must inspect a product directly; reach for
build-game-camera-controls
for drag, zoom, occlusion, or gameplay cameras. This Skill is passive cinematic depth, not direct manipulation.
Extracted from
inner-green-3d.html
, where a procedural moss root had to turn toward the pointer without sliding away from the headline, cards, and pinned silhouette landmarks.
将一个经过阻尼处理的指针目标转化为平缓的相机弧线运动和小型对象旋转。当用户需要直接检查产品时,可搭配
OrbitControls
使用
threejs
;若需拖拽、缩放、遮挡或游戏玩法相关的相机,可使用
build-game-camera-controls
。本Skill提供的是被动电影级深度效果,而非直接操控功能。
该方案提取自
inner-green-3d.html
,其中程序化生成的苔藓根须需要朝向指针移动,但不能偏离标题、卡片和固定的轮廓地标。

Record intent, not layout

记录交互意图,而非布局信息

In the pointer handler, write normalized coordinates only:
js
function recordPointer(event) {
  if (event.pointerType === "touch") return;
  target.x = (event.clientX / viewport.width) * 2 - 1;
  target.y = (event.clientY / viewport.height) * 2 - 1;
}
Cache the interactive rectangle from
ResizeObserver
. Do not call
getBoundingClientRect()
for every layer on every
pointermove
; synchronous layout turns a light effect into frame spikes.
On
pointerleave
, set the target back to
0, 0
. On coarse pointers, keep the authored center pose. Passive orbit should never require touch dragging to reveal content.
在指针处理函数中,仅记录归一化坐标:
js
function recordPointer(event) {
  if (event.pointerType === "touch") return;
  target.x = (event.clientX / viewport.width) * 2 - 1;
  target.y = (event.clientY / viewport.height) * 2 - 1;
}
通过
ResizeObserver
缓存交互区域的矩形信息。不要在每次
pointermove
事件中为每个层级调用
getBoundingClientRect()
;同步布局会让轻量效果出现帧率波动。
当指针离开视口时,将目标坐标重置为
0, 0
。对于粗指针输入,保持预设的中心姿态。被动轨道效果不应要求用户通过触摸拖拽来展示内容。

Dampen in the frame loop

在帧循环中添加阻尼效果

The source uses 0.055 per 60 Hz frame. Preserve that feel across refresh rates:
js
const alpha = 1 - Math.pow(1 - 0.055, dt * 60);
smooth.x += (target.x - smooth.x) * alpha;
smooth.y += (target.y - smooth.y) * alpha;
Clamp
dt
to 1/30 s and reset the time base after resume. A fixed per-frame lerp feels heavy at 30 Hz and twitchy at 120 Hz; an unclamped delta jumps after backgrounding.
Stop style or uniform writes once the rounded value settles. Three decimals are finer than one pixel of the landed travel:
js
const x = Math.round(smooth.x * 1000) / 1000;
const y = Math.round(smooth.y * 1000) / 1000;
if (x !== lastX || y !== lastY) publish(x, y);
源码在60Hz帧率下使用0.055的阻尼系数。为了在不同刷新率下保持一致的手感:
js
const alpha = 1 - Math.pow(1 - 0.055, dt * 60);
smooth.x += (target.x - smooth.x) * alpha;
smooth.y += (target.y - smooth.y) * alpha;
dt
限制为1/30秒,并在恢复运行后重置时间基准。固定每帧的线性插值在30Hz下会显得沉重,在120Hz下会过于灵敏;未限制的时间增量在页面后台运行后会出现跳跃。
当数值四舍五入后稳定时,停止样式或uniform变量的写入。保留三位小数的精度已超过实际移动距离的一个像素:
js
const x = Math.round(smooth.x * 1000) / 1000;
const y = Math.round(smooth.y * 1000) / 1000;
if (x !== lastX || y !== lastY) publish(x, y);

Split the motion

拆分运动效果

Use opposing, unequal layers so the scene pivots rather than translates as one slab:
js
camera.position.x = -smooth.x * 26;
camera.position.y =  smooth.y * 16;
camera.lookAt(camera.position.x * 0.42, camera.position.y * 0.42, 0);

nearGroup.rotation.y = smooth.x * 0.055;
nearGroup.rotation.x = smooth.y * 0.026;
farGroup.rotation.y  = smooth.x * 0.030;
Treat these as the landed values for a scene framed in stage-pixel world units:
layerhorizontalverticalreason
camera translation-26+16establishes the shallow arc
camera look-at carry42%42%keeps the subject near its pinned composition
near object yaw0.055 radexposes surface depth without showing its flank
near object pitch0.026 radprevents the top surface from flattening
far object yaw0.030 radseparates planes without matching the foreground
Do not rotate everything by the same amount. Equal movement reads as a flat poster following the cursor. Do not aim the camera at the fixed origin while translating it; the subject visibly slides away from the layout.
Apply CSS parallax from the same
smooth
pair, but give text and controls smaller depth coefficients than the 3D form. Keep transforms free for parallax; use clip, opacity, or child wrappers for unrelated reveals so animations do not overwrite each other.
使用方向相反、幅度不同的层级,让场景产生枢轴转动而非整体平移:
js
camera.position.x = -smooth.x * 26;
camera.position.y =  smooth.y * 16;
camera.lookAt(camera.position.x * 0.42, camera.position.y * 0.42, 0);

nearGroup.rotation.y = smooth.x * 0.055;
nearGroup.rotation.x = smooth.y * 0.026;
farGroup.rotation.y  = smooth.x * 0.030;
以下是以舞台像素为世界单位的场景落地参数:
层级水平参数垂直参数原因
相机平移-26+16构建平缓的弧线运动
相机看向目标延续42%42%让主体保持在固定构图附近
近景对象偏航0.055 rad展现表面深度但不暴露侧面
近景对象俯仰0.026 rad防止顶部表面扁平化
远景对象偏航0.030 rad分离平面但不与前景完全同步
不要让所有元素以相同幅度旋转。相同的移动幅度会让场景看起来像跟随光标移动的平面海报。平移相机时不要始终瞄准固定原点;否则主体会明显偏离布局。
基于同一组
smooth
数值应用CSS视差效果,但为文本和控件设置比3D模型更小的深度系数。保留transform属性用于视差效果;使用裁剪、透明度或子容器实现无关的展示动画,避免动画相互覆盖。

Preserve framing across sizes

在不同尺寸下保持构图

Build the center pose first at every breakpoint. Recalculate camera aspect and any stage-to-world scale from a
ResizeObserver
, guard zero-sized roots, then apply orbit offsets. Verify both extreme pointer corners: no copy collision, card clipping, or exposed empty edge.
Use keyboard-operable X and Y range controls in a demo or configurator. They prove the orbit is parameterised and give non-pointer users access to the same authored states. Keep focus visible and announce motion-mode changes.
在每个断点处先构建中心姿态。通过
ResizeObserver
重新计算相机宽高比和舞台到世界的缩放比例,避免零尺寸根元素,然后应用轨道偏移。验证指针在极端角落时的效果:无文本重叠、卡片裁剪或空白边缘暴露。
在演示或配置工具中使用支持键盘操作的X和Y范围控件。这可以证明轨道效果是可参数化的,同时为非指针用户提供相同的预设状态访问权限。保持焦点可见,并播报运动模式的变化。

Respect motion and lifecycle

尊重运动偏好与生命周期

  • Under
    prefers-reduced-motion: reduce
    , render a designed three-quarter still at approximately
    x=.28, y=-.12
    ; keep the range controls live and redraw their selected still without interpolation.
  • Pause while hidden or offscreen and resume with a reset time base.
  • Cap DPR at 2 and clamp
    dt
    to 1/30 s.
  • Keep the canvas decorative unless the 3D object carries information. Preserve all labels and controls in semantic HTML.
  • On teardown, cancel the frame, disconnect observers, remove pointer/media listeners, and dispose Three.js resources.
  • prefers-reduced-motion: reduce
    时,渲染一个预设的四分之三视图静态画面,参数约为
    x=.28, y=-.12
    ;保持范围控件可用,并直接重绘选中的静态画面,不使用插值动画。
  • 页面隐藏或离开视口时暂停,恢复时重置时间基准。
  • 将设备像素比(DPR)限制为2,并将
    dt
    限制为1/30秒。
  • 除非3D对象承载信息,否则将画布视为装饰元素。所有标签和控件保留在语义化HTML中。
  • 销毁时,取消帧循环、断开观察者、移除指针/媒体监听器,并释放Three.js资源。

State the cost honestly

如实说明性能成本

The orbit math is negligible. The expensive work is the scene already being redrawn and any DOM style invalidation layered on top. Share one frame loop, round settled values, avoid layout reads in pointer handlers, and profile the underlying draw before removing the interaction. Moving the camera does not make a dense scene cheaper.
轨道运算的开销可以忽略不计。昂贵的部分是场景本身的重绘以及叠加的DOM样式失效。共享一个帧循环,对稳定后的数值进行四舍五入,避免在指针处理函数中读取布局信息,并在移除交互前分析底层绘制性能。移动相机并不会降低密集场景的开销。

Verify

验证清单

  • Compare center and four-corner poses with the source at 1440×900.
  • Confirm camera and objects move by different, opposing amounts.
  • Move quickly across the viewport; the scene must ease through the path, not snap.
  • Leave the viewport; confirm a smooth return to center.
  • Test 390×844 and coarse/touch input; center composition remains complete.
  • Tab through X/Y controls, change them with arrow keys, and confirm visible focus.
  • Test
    ?reduced=1
    : stable designed still, no autonomous easing, live controls.
  • Hide/show and scroll away/back; confirm no jump and only one frame loop.
  • Confirm a clean console at both sizes.
Use demo/index.html as the working proof and demo/PROMPT.md to recreate or remix it.
  • 在1440×900分辨率下对比中心和四个角落的姿态与源效果。
  • 确认相机和对象以不同、相反的幅度移动。
  • 快速移动光标穿过视口;场景必须平滑过渡路径,而非瞬间跳转。
  • 移出视口;确认平滑返回中心位置。
  • 测试390×844分辨率和粗指针/触摸输入;中心构图保持完整。
  • 切换X/Y控件的焦点,使用箭头键修改参数,确认焦点可见。
  • 测试
    ?reduced=1
    :静态预设画面稳定,无自动缓动,控件正常可用。
  • 隐藏/显示页面以及滚动离开/返回;确认无跳跃且仅存在一个帧循环。
  • 确认两种分辨率下控制台无报错。
可使用demo/index.html作为运行示例,demo/PROMPT.md用于重新创建或修改效果。