Skip to content

feat: transport-ship trail transition effect + animated store swatch#4455

Merged
evanpelle merged 2 commits into
mainfrom
boat-transition
Jun 30, 2026
Merged

feat: transport-ship trail transition effect + animated store swatch#4455
evanpelle merged 2 commits into
mainfrom
boat-transition

Conversation

@evanpelle

Copy link
Copy Markdown
Collaborator

What

Adds a second transport-ship trail style, transition, alongside the existing gradient (#4454). Where gradient paints a spatial band of colors along the trail, transition makes the whole trail one color at a time, cross-fading through the color list over time.

"attributes": {
  "type": "transition",
  "colors": ["#002aff", "#4805ff"],
  "frequency": 1
}

How

  • Schema (CosmeticSchemas.ts) — TransportShipTrailAttributesSchema is now a discriminated union on type:
    • gradient: { colors, colorSize, movementSpeed }
    • transition: { colors, frequency }frequency = color changes per second.
  • Renderer — the effect texture gained a styleId discriminator (row 1's alpha; 0 = gradient, 1 = transition), with the gradient scalars shifted down a row.
    • WebGLFrameBuilder.ts encodes styleId + the style's scalars.
    • trail.frag.glsl: for transition, the trail color is mix(colors[i], colors[i+1], fract(t)) with i = floor(uTime · frequency) mod count — one color step every 1/frequency seconds.
  • Store/picker swatch (EffectPreview.ts) — the swatch is now a <trail-swatch> Lit element. For transition it cross-fades through the colors via the Web Animations API, timed to match the shader (each step 1/frequency s); gradient/solid stay static. The animation is canceled on disconnect.

Notes

  • Animation is render-only (local time) — no simulation/determinism impact.
  • gradient swatches remain static (they don't scroll like the in-game trail) — easy to add later if wanted.

Testing

  • tsc --noEmit, ESLint, Prettier, build-prod all clean.
  • Schema tests cover the transition member (parse + required frequency); 95 tests pass.
  • The animated swatch is visual-only (no automated coverage) and not yet verified in a running store.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 16a90312-4e44-4cf3-a033-8a7cda1c4bd8

📥 Commits

Reviewing files that changed from the base of the PR and between a15338e and cbd759a.

📒 Files selected for processing (3)
  • src/client/EffectsInput.ts
  • src/client/components/CosmeticButton.ts
  • src/client/components/EffectPreview.ts
💤 Files with no reviewable changes (1)
  • src/client/components/EffectPreview.ts

Walkthrough

Adds a "transition" variant to TransportShipTrailAttributesSchema, updates effect-palette encoding for trail styles, and replaces the static trail preview helper with a TrailSwatch custom element used in two UI render paths.

Changes

Trail Transition Variant

Layer / File(s) Summary
Discriminated union schema and tests
src/core/CosmeticSchemas.ts, tests/CosmeticSchemas.test.ts
TransportShipTrailAttributesSchema now accepts "gradient" and "transition" variants, and tests cover valid parsing plus missing-field rejection for transition attributes.
WebGL palette encoding
src/client/WebGLFrameBuilder.ts
writeEffectEntry() now writes styleId, scalar0, and scalar1 into palette rows 1–3, with row 0 still storing the color count.
TrailSwatch preview element
src/client/components/EffectPreview.ts
TrailSwatch renders neutral, flat, gradient, or transition backgrounds and uses Web Animations API cross-fades for transition trails.
Preview wiring
src/client/EffectsInput.ts, src/client/components/CosmeticButton.ts
Both render paths now register EffectPreview.ts for side effects and use <trail-swatch> with the trail data bound to .trail.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • openfrontio/OpenFrontIO#4418: Adds earlier transport-ship-trail preview UI work that this PR extends with the new "transition" trail behavior.
  • openfrontio/OpenFrontIO#4454: Touches the same trail effect palette path in src/client/WebGLFrameBuilder.ts and related shader data handling.

Poem

A trail now changes shape and hue,
With typed unions keeping things true.
The swatch now lives and gently glows,
While WebGL writes what the shader knows. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main changes: the new transport-ship trail transition effect and animated swatch.
Description check ✅ Passed The description matches the changeset and explains the new trail style, schema, renderer, and swatch updates.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@evanpelle evanpelle added this to the v33 milestone Jun 30, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/client/components/EffectPreview.ts`:
- Around line 29-45: The EffectPreview swatch is using raw trail colors
directly, which can disagree with the renderer when invalid color strings are
present. Update EffectPreview.render() to normalize/filter this.trail?.colors
the same way the renderer logic does before building the CSS background, and
fall back to EMPTY_BG when no valid colors remain. Keep the logic in render()
aligned with the trail color handling used by
WebGLFrameBuilder.writeEffectEntry() so the preview matches the actual effect.

In `@src/core/CosmeticSchemas.ts`:
- Around line 121-124: The transition schema in CosmeticSchemas is currently
allowing negative frequency values, which can later break preview/render
behavior. Update the z.object validator for the transition type to reject
negative frequencies (or require strictly positive if zero should be disallowed)
so the contract matches TrailSwatch.updated() and trail.frag.glsl. Add a
regression test covering the transition schema validation to ensure invalid
negative frequency values are rejected.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 85bb73bb-ae32-47d9-b6b1-e105b19b3e1a

📥 Commits

Reviewing files that changed from the base of the PR and between 7c151e7 and a15338e.

⛔ Files ignored due to path filters (1)
  • src/client/render/gl/shaders/map-overlay/trail.frag.glsl is excluded by !**/*.glsl
📒 Files selected for processing (4)
  • src/client/WebGLFrameBuilder.ts
  • src/client/components/EffectPreview.ts
  • src/core/CosmeticSchemas.ts
  • tests/CosmeticSchemas.test.ts

Comment on lines +29 to +45
render(): TemplateResult {
const colors = this.trail?.colors ?? [];
let background: string;
if (colors.length === 0) {
background = EMPTY_BG;
} else if (this.trail?.type === "transition") {
// The animation (see updated) cross-fades from here through the list.
background = colors[0];
} else if (colors.length === 1) {
background = colors[0];
} else {
background = `linear-gradient(90deg,${colors.join(",")})`;
}
return html`<div
class="w-full h-full rounded-md"
style="background:${background};"
></div>`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Filter preview colors the same way the renderer does.

TransportShipTrailAttributesSchema allows raw strings, and WebGLFrameBuilder.writeEffectEntry() drops any color it can't parse. Here Lines 30-40 and Lines 55-66 use those raw values directly in CSS/WAAPI, so one bad entry can make the swatch blank or disagree with the actual trail. Normalize/filter first, then fall back to EMPTY_BG when nothing valid remains.

💡 Proposed fix
+import { colord } from "colord";
 import { html, LitElement, TemplateResult } from "lit";
 import { customElement, property } from "lit/decorators.js";
 import { TransportShipTrailAttributes } from "../../core/CosmeticSchemas";
 
 // Neutral fallback when a trail has no usable colors.
 const EMPTY_BG = "`#444`";
+
+function getRenderableColors(
+  trail: TransportShipTrailAttributes | null,
+): string[] {
+  return (trail?.colors ?? []).filter((color) => colord(color).isValid());
+}
@@
   render(): TemplateResult {
-    const colors = this.trail?.colors ?? [];
+    const colors = getRenderableColors(this.trail);
@@
-    const colors = attrs.colors;
+    const colors = getRenderableColors(attrs);
     if (colors.length < 2 || attrs.frequency <= 0) return;

Also applies to: 53-69

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/components/EffectPreview.ts` around lines 29 - 45, The
EffectPreview swatch is using raw trail colors directly, which can disagree with
the renderer when invalid color strings are present. Update
EffectPreview.render() to normalize/filter this.trail?.colors the same way the
renderer logic does before building the CSS background, and fall back to
EMPTY_BG when no valid colors remain. Keep the logic in render() aligned with
the trail color handling used by WebGLFrameBuilder.writeEffectEntry() so the
preview matches the actual effect.

Comment on lines +121 to +124
z.object({
type: z.literal("transition"),
colors: z.array(z.string()),
frequency: z.number(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject negative transition frequencies.

Line 124 currently accepts negative values. TrailSwatch.updated() treats non-positive frequencies as “don’t animate”, but trail.frag.glsl still uses the encoded value in modulo math, so a negative catalog entry can preview as static and render with invalid color indexing. Validate this as non-negative here (or positive if zero should also be rejected), and add a regression test for that contract.

💡 Proposed fix
   z.object({
     type: z.literal("transition"),
     colors: z.array(z.string()),
-    frequency: z.number(),
+    frequency: z.number().nonnegative(),
   }),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
z.object({
type: z.literal("transition"),
colors: z.array(z.string()),
frequency: z.number(),
z.object({
type: z.literal("transition"),
colors: z.array(z.string()),
frequency: z.number().nonnegative(),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/CosmeticSchemas.ts` around lines 121 - 124, The transition schema in
CosmeticSchemas is currently allowing negative frequency values, which can later
break preview/render behavior. Update the z.object validator for the transition
type to reject negative frequencies (or require strictly positive if zero should
be disallowed) so the contract matches TrailSwatch.updated() and
trail.frag.glsl. Add a regression test covering the transition schema validation
to ensure invalid negative frequency values are rejected.

@github-project-automation github-project-automation Bot moved this from Triage to Development in OpenFront Release Management Jun 30, 2026
Use the <trail-swatch> element directly in CosmeticButton and EffectsInput
and drop the thin wrapper; the named imports become side-effect imports so
the element still registers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@evanpelle evanpelle merged commit 200f276 into main Jun 30, 2026
12 of 14 checks passed
@evanpelle evanpelle deleted the boat-transition branch June 30, 2026 04:53
@github-project-automation github-project-automation Bot moved this from Development to Complete in OpenFront Release Management Jun 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Complete

Development

Successfully merging this pull request may close these issues.

1 participant