diff --git a/src/mtlfns.m b/src/mtlfns.m index 71308907891..01be4988138 100644 --- a/src/mtlfns.m +++ b/src/mtlfns.m @@ -314,6 +314,23 @@ MODE is an integer 0-7: NSUInteger m = (NSUInteger)XFIXNAT (mode); if (m > 7) error ("mtl-cursor-mode: mode must be 0-7"); g_mtl_cursor_mode = (MtlCursorMode)m; + + /* Propagate to live animators: each MtlAnimator caches cursorMode at init, + so without this a running frame keeps its old mode (e.g. the default + spring) and the change only takes effect on the next (mtl-enable). */ + Lisp_Object tail, frame; + FOR_EACH_FRAME (tail, frame) + { + struct frame *f = XFRAME (frame); + if (!FRAME_LIVE_P (f) || !FRAME_NS_P (f)) + continue; + MtlFrameData *fd = mtl_get_frame_data (f); + if (fd && fd.animator) + { + fd.animator.cursorMode = g_mtl_cursor_mode; + fd.animator.trailCount = 0; /* drop any stale trail from old mode */ + } + } return mode; } @@ -332,6 +349,18 @@ EFFECT is an integer 0-5: NSUInteger e = (NSUInteger)XFIXNAT (effect); if (e > 5) error ("mtl-scroll-effect: effect must be 0-5"); g_mtl_scroll_easing = (MtlScrollEasing)e; + + /* Propagate to live animators (they cache scrollEasing at init). */ + Lisp_Object tail, frame; + FOR_EACH_FRAME (tail, frame) + { + struct frame *f = XFRAME (frame); + if (!FRAME_LIVE_P (f) || !FRAME_NS_P (f)) + continue; + MtlFrameData *fd = mtl_get_frame_data (f); + if (fd && fd.animator) + fd.animator.scrollEasing = g_mtl_scroll_easing; + } return effect; } diff --git a/src/mtlterm.h b/src/mtlterm.h index a02da746bf3..3f14cd507db 100644 --- a/src/mtlterm.h +++ b/src/mtlterm.h @@ -88,6 +88,8 @@ typedef struct mtl_spring { /* Trail history (torpedo mode) — C arrays cannot be @property */ float trailX[MTL_TRAIL_LEN]; float trailY[MTL_TRAIL_LEN]; + /* Seconds since each trail sample was emitted; drives the smooth fade. */ + float trailAge[MTL_TRAIL_LEN]; /* Particles (pixiedust, sonicboom, ripple) */ MtlParticle particles[MTL_MAX_PARTICLES]; } diff --git a/src/mtlterm.m b/src/mtlterm.m index fc3d66373f2..a8a8338359c 100644 --- a/src/mtlterm.m +++ b/src/mtlterm.m @@ -791,6 +791,10 @@ mtl_setup_frame (struct frame *f) #define SPRING_OMEGA 8.0f /* sqrt(k/m): settles in ~150ms */ +/* How long a torpedo trail sample stays visible before it fully fades. + The tail dissipates on its own this many seconds after the cursor stops. */ +#define MTL_TRAIL_LIFETIME 0.40f + static void spring_update (MtlSpring1D *s, float target, float dt) { @@ -876,8 +880,9 @@ easing_apply (MtlScrollEasing mode, float t) && (fabsf(dx) > 1 || fabsf(dy) > 1)) { NSUInteger slot = (self.trailHead + self.trailCount) % MTL_TRAIL_LEN; - trailX[slot] = self.curTargetX; - trailY[slot] = self.curTargetY; + trailX[slot] = self.curTargetX; + trailY[slot] = self.curTargetY; + trailAge[slot] = 0.0f; /* fresh sample, fully opaque */ if (self.trailCount < g_mtl_trail_len) self.trailCount++; else @@ -930,9 +935,8 @@ easing_apply (MtlScrollEasing mode, float t) BOOL needsComposite = NO; - /* Update spring cursor */ - if (self.cursorMode == MTL_CURSOR_SPRING - || self.cursorMode == MTL_CURSOR_TORPEDO) + /* Update spring cursor (spring mode only; torpedo snaps instantly) */ + if (self.cursorMode == MTL_CURSOR_SPRING) { MtlSpring1D sx = self.springX, sy = self.springY; spring_update (&sx, self.curTargetX, dt); @@ -944,6 +948,25 @@ easing_apply (MtlScrollEasing mode, float t) if (dx > 0.5f || dy > 0.5f) needsComposite = YES; } + /* Age the torpedo trail so it fades out smoothly on its own. Samples are + pushed oldest-first at trailHead, so the head always holds the oldest one; + retire it once it outlives MTL_TRAIL_LIFETIME. */ + if (self.cursorMode == MTL_CURSOR_TORPEDO && self.trailCount > 0) + { + for (NSUInteger i = 0; i < self.trailCount; i++) + { + NSUInteger slot = (self.trailHead + i) % MTL_TRAIL_LEN; + trailAge[slot] += dt; + } + while (self.trailCount > 0 + && trailAge[self.trailHead] >= MTL_TRAIL_LIFETIME) + { + self.trailHead = (self.trailHead + 1) % MTL_TRAIL_LEN; + self.trailCount--; + } + if (self.trailCount > 0) needsComposite = YES; + } + /* Update particles */ if (self.nParticles > 0) { @@ -1280,9 +1303,23 @@ easing_apply (MtlScrollEasing mode, float t) for (NSUInteger i = 0; i < tlen; i++) { NSUInteger slot = (anim.trailHead + i) % MTL_TRAIL_LEN; - float alpha = (float)(i + 1) / (float)(tlen + 1) * 0.6f; + /* Two combined falloffs so the head stays a sharp instant cursor + and the rest reads as a temporary comet tail: + - spatial: samples nearer the cursor (higher i) are brighter, + so even while moving the tail tapers instead of forming a + uniform bright bar that looks like the cursor sliding; + - temporal: every sample fades with age, so the whole tail + dissipates on its own shortly after the cursor stops. */ + float pos = (float)(i + 1) / (float)tlen; /* 0=tail .. 1=head */ + float frac = anim->trailAge[slot] / MTL_TRAIL_LIFETIME; + if (frac > 1.0f) frac = 1.0f; + float life = 1.0f - frac; + float alpha = pos * life * life * 0.5f; /* spatial x temporal */ + float scale = 0.35f + 0.55f * pos; /* narrow toward the tail */ float tx = anim->trailX[slot], ty = anim->trailY[slot]; - float x0=tx, y0=ty, x1=tx+cw, y1=ty+ch; + float iw = cw * scale, ih = ch * scale; + float ox = tx + (cw - iw) * 0.5f, oy = ty + (ch - ih) * 0.5f; + float x0=ox, y0=oy, x1=ox+iw, y1=oy+ih; typedef struct { float x,y,r,g,b,a; } RV; float r2=ccr,g2=ccg,b2=ccb; RV v[6] = {