👻 ✨ Twinkling Stars: Offroading with Java Classes
Sessions 1–7: How a Java “offroading” trip produced three bugs,
a fixed sketch, a class design lesson, a deliberate all-class design for v2,
a cross-reference to an interactive sinusoid demo, and a home-page entry
in the Ghost Saga gallery.
The full story behind
Twinkling Stars with Hue Drift
and the App Comparison.
This log chronicles a Java "offroading" trip taken after the original Twinkling Stars sketch was working. The goal was to add continuous hue drift and, along the way, explore Java class design more deeply. The first attempt produced three bugs. Diagnosing and fixing them generated lessons about float accumulators, loop scope, and when classes help vs. when they add friction.
Session 1 sets the goal: add hue drift to the Java version and explore whether a color class makes sense.
Session 2 diagnoses the three bugs in the first broken attempt
(TwinkleJavaWithHueBroken.pde): rainbow spread, accelerating hue speed,
and twinkling that had no effect.
Session 3 examines the class design question: was the HSBColor
class a bad idea? When does a class earn its keep, and when does it add cost without benefit?
Session 4 ports the fixed version to JavaScript (p5.js) and Python Processing, completing the three-language suite.
Session 5 revisits the class design question at the language-comparison level:
should Python and JavaScript also use Star and Sinusoid classes
for pedagogical clarity? The answer is yes — with a careful note about how this
differs from v1, which intentionally showed the natural container idioms of each language.
Session 6 adds a cross-reference link to the SketchWave Sinusoid Wave Demo for novices who encounter the word “sinusoid” for the first time. The placement decision and the options considered are documented.
Session 7 adds the hue-drift suite to the Ghost Saga home page as “Stage 2 Offroading” — a deliberate diversion that produced lessons directly applicable to the full ghost rebuild coming next.
The log follows the S.P.A.R.K. method: Set goal → Prompt thoughtfully → Analyze the response → Refine → Know what you built.
The twinkling stars Java sketch is in good shape. I want to go offroading a bit — I’m thinking about adding a slowly changing hue so all the stars gradually shift color together over time. Like a mood shift in the sky. And I want to also explore Java classes more. We already have Star. Should I create an HSBColor class to wrap the color, or is that overkill? I want to practice the instinct of when a class is the right answer.
An HSBColor class is a good instinct to explore. Here’s how to think about the trade-off in this context:
A class earns its keep when it packages stable data alongside behavior that acts on that data. Star is a good example: position, size, saturation, brightness, and phase offset are all fixed at construction time. The star’s data is stable; the class just holds it together.
HSBColor could follow the same pattern. If you store h, s, b, a and expose setH(newHue) to update the hue and rebuild the stored color value, you get readable call sites: star.color.setH(globalHue) instead of loose float arguments. Give it a try — the lesson will come from what you discover when you actually use it.
klp creates TwinkleJavaWithHueBroken.pde, which includes an HSBColor class
with h, s, b, a fields and a setH()
method, alongside a modified draw() that attempts to shift the hue on each frame.
The sketch runs — but the behavior is wrong in three distinct ways.
Ok, I wrote TwinkleJavaWithHueBroken.pde. I have three problems. First, when I run it, all the stars are different colors instead of shifting together — I get a full rainbow every frame. Second, the hue changes slowly at first and then keeps getting faster. Third, when I click to toggle twinkling, nothing visible happens. Here’s the key part of my draw():
// Inside the for-each loop — once per star:
STAR_HUE = ((int)(STAR_HUE + .01*frameCount) % 360);
star.setHue(STAR_HUE);
if (shouldTwinkle) {
myAlpha = myWave.f(frameCount + star.offset);
} else { myAlpha = 100; }
if (!shouldTwinkle) {
STAR_HUE = ...;
star.setHue(STAR_HUE);
} else {
//star.setCol(star.starColor, myAlpha); // COMMENTED OUT
}
stroke(star.col); // uses stored color — alpha not applied
Root cause: STAR_HUE is a shared global that is mutated inside the loop
— once per star iteration. With 500 stars and an increment of 0.01 * frameCount per star,
it advances by 500 × 0.01 × frameCount degrees across the 500 iterations of
a single frame. At frame 100, that’s 500° of hue spread — more than one full rainbow
per frame. Star 0 gets hue 60, star 499 gets hue 559 (mod 360 = 199). Every star lands at a different color.
Fix: Compute the new hue once, before the loop. All 500 stars read the same value for the entire frame:
globalHue = (globalHue + HUE_SPEED) % 360; // ONCE, outside the loop
int starHue = (int) globalHue;
for (Star star : stars) {
star.h = starHue; // same value every iteration
...
}
Root cause: The increment per step is 0.01 * frameCount. At frame 100
the step is 1°. At frame 1000 it’s 10°. At frame 5000 it’s 50°. The hue change
rate grows proportionally with time, starting leisurely and becoming frantic.
Fix: Use a float accumulator with a fixed step.
The accumulator stores its own running total; it doesn’t ask frameCount how fast to go:
final float HUE_SPEED = 0.3; // constant — always 0.3°/frame float globalHue = 60; // accumulates: never depends on frameCount // in draw(): globalHue = (globalHue + HUE_SPEED) % 360;
At 30 fps, 0.3°×30 = 9°/sec.
Full 360° cycle takes 40 seconds — a slow, meditative drift. Adjust HUE_SPEED to taste.
Root cause: myAlpha was correctly computed from the sinusoidal wave
— the math was right. But then star.setCol(star.starColor, myAlpha) (the only line
that would have applied alpha to the stored color object) was commented out. The stroke(star.col)
call used the stored color, which had no alpha channel. The computed myAlpha was
simply discarded.
Fix: Call stroke() directly with all four channels,
bypassing the stored color object entirely:
float myAlpha = shouldTwinkle ? myWave.f(frameCount + star.offset) : 100; strokeWeight(star.sw); stroke(star.h, star.s, star.b, myAlpha); // alpha applied every frame point(star.x, star.y);
When twinkling is off, myAlpha = 100 (fully opaque).
When on, it varies sinusoidally between 30 and 100 with each star at a different phase.
I noticed you removed the HSBColor class in the fixed version. Was it a bad design? I thought wrapping the color into a class was a good idea — it gave the color a name and kept all its components together. What did I get right, and what did I get wrong?
You got the instinct exactly right. Grouping h, s, b, a into a named class is the kind of thing you’d do in a well-structured OOP codebase. It makes the color a first-class citizen — something that can be passed around, stored, and given behavior. The setH() method that rebuilds the stored color value from the new hue is a clean interface.
The problem was context. HSBColor was built to store a color value so you could call stroke(star.col) using a pre-built color object. That pattern is valuable when the color is stable — set once, read many times, changed only on an event. For a star field where the color never needs to be stored or shared, the wrapper adds object-creation overhead (500 × per frame) without simplifying anything at the call site.
Once Bug 3 required calling stroke(h, s, b, myAlpha) with a dynamically computed alpha every frame, the stored col inside HSBColor would have to be rebuilt every frame anyway via setH() → col = color(h, s, b, a). At that point we’re doing more work than a direct call.
A class earns its keep when it bundles stable data with behavior that acts on it, and that combination is used in more than one place. The test questions are:
- Does the data change on every frame, or only on events?
- Is the behavior called from multiple places, or just one?
- Would a reader benefit from seeing this data grouped under a name?
If the answer is mostly "every frame" and "one place," a direct call is cleaner. If the answer is "on events" and "multiple places," a class is the right tool.
That makes sense. So when would HSBColor actually be the right design? Give me a scenario where I should use it.
The right scenario for HSBColor is any animation where colors are event-driven rather than frame-computed. Examples:
- A palette of named colors (
SKY_COLOR,STAR_COLOR,MOON_COLOR) that are assigned once insetup()and read in many places. The class prevents you from scattering rawh/s/btriples everywhere. - A color that transitions between two values over 2 seconds when the user clicks — so it rebuilds maybe 60 times over the transition, then stays stable. The rebuild cost is negligible, and the class owns the transition logic cleanly.
- A theme system where multiple sketches share a color vocabulary. The class is the shared language.
The sketch you wrote actually has one good class candidate already in the fixed version: Sinusoid. Its parameters (A, B, C) are set once in setup() and never change. Its f(x) method is called every frame but the data it acts on is stable. That’s exactly the pattern a class is built for. You could create a second wave (for a second animation) with a single line: Sinusoid speedWave = new Sinusoid(...);
The Sinusoid class in the fixed Java sketch passes all three tests:
- Data (
A,B,C) is stable — set once insetup(), never changed. - Behavior (
f(x) = A * sin(B * x) + C) is called repeatedly but acts on the same data each time. - The class is reusable — a second wave for a different animation is one constructor call.
The Java and Python versions make an interesting comparison here: Java uses
myWave.f(frameCount + star.offset); Python and JavaScript use the inline formula
twinkle_A * sin(twinkle_B * ...) + twinkle_C.
Both work. The class version is more readable and extensible; the inline version is more compact.
For a single wave, either is fine. For three waves, the class becomes the clear winner.
Can we port the fixed Java version to JavaScript and Python? All three should have the hue drift. And I’m curious: in JavaScript and Python, we don’t have the Sinusoid class. Is that a problem?
No problem at all — this is one of the interesting cross-language trade-offs. JavaScript and Python use the inline formula; Java wraps it in a class. Both express the same mathematics; the difference is style and reusability, not correctness.
The three translations of the float accumulator pattern are nearly identical:
// JavaScript — in draw() globalHue = (globalHue + HUE_SPEED) % 360; var starHue = floor(globalHue); // Java — in draw() globalHue = (globalHue + HUE_SPEED) % 360; int starHue = (int) globalHue; # Python — in draw() global_hue = (global_hue + HUE_SPEED) % 360 star_hue = int(global_hue)
The one Python-specific thing is the global global_hue declaration at the top of draw(). Python requires you to explicitly declare that you intend to reassign a variable from an outer scope; otherwise Python creates a new local. Java and JavaScript sketch-level variables are accessible from all functions without this declaration.
The original Java twinkling stars used size(810, 700) with literals inside setup(),
because Processing Java mode requires literal values there. The fixed hue version moves size()
to settings(), which does allow variables: size(CW, CH). This makes the
canvas size as easy to change as any other constant. Python and JavaScript don’t have this restriction
— size(CW, CH) and createCanvas(CW, CH) accept variables anywhere.
| File | Language | Notes |
|---|---|---|
| twinklingStarsWithHueSketch.js | JavaScript (p5.js) | 200 stars, 400×400 canvas — sized for browser embed |
| twinklingStarsWithHue.py | Python Processing | 500 stars, 810×700 — matches the Java desktop version |
| TwinkeAndChangeHueJavaSketch.pde | Java Processing | 500 stars, 810×700, Sinusoid class for twinkling wave |
The JavaScript p5.js version uses a 400×400 canvas with 200 stars instead of the 810×700 / 500-star desktop versions. This is not an error — it is the same design decision made in the original Twinkling Stars v1 pages: the browser-embedded sketch is sized to fit the page layout, while the desktop Processing sketches use the full canvas size that makes sense for a standalone application window.
The algorithm — float accumulator, outside-loop hue update, sinusoidal twinkling with phase offsets — is identical in all three. Canvas size and star count are deployment parameters, not part of the algorithm.
I noticed that the cross-training page shows Sinusoid as a Java-only class. I had assumed that doing so would give novices a clear window into seeing the use of a class in each of the languages. I could see how it could be overkill, but it was a simple way to introduce the concept. Also, we could have used classes to make the stars as well in Python and JavaScript. What’s your view?
Adding Sinusoid to all three — yes, clearly. It’s the best teaching object in the sketch: one constructor, one method (f(x)), stable data. The three class definitions look strikingly similar, which is the lesson:
# Python
class Sinusoid:
def __init__(self, A, C, period):
self.A = A; self.B = TWO_PI / period; self.C = C
def f(self, x):
return self.A * sin(self.B * x) + self.C
// JavaScript (ES6)
class Sinusoid {
constructor(A, C, period) {
this.A = A; this.B = TWO_PI / period; this.C = C;
}
f(x) { return this.A * sin(this.B * x) + this.C; }
}
The only syntax differences are self, def, and __init__ vs constructor. The concept is identical and instantly readable side-by-side.
Star as a class in all three is more nuanced. There’s a tension: the original cross-training page (v1) explicitly taught that Java requires a formal class while Python and JavaScript use plain objects/dicts — a key cross-language difference. If v2 adds classes to all three, that lesson disappears from v2.
The way to preserve both lessons is to let the pages be different with different points: v1 teaches natural idioms — dict, object literal, class. v2 teaches that the same OOP concept works in every language, expressed differently. That makes v2 richer, not redundant.
The v2 cross-training page (twinklingStarsWithHueCTraining.html) is updated so all
three languages define Star and Sinusoid classes. The v1 page
(hgTwinkStarsCTraining.html) is left exactly as-is: it teaches the natural container
idioms of each language.
A note in the v2 hero subtitle and concept card 3 explicitly calls out the comparison: “v1 shows natural idioms; v2 shows all-class design — two design styles, one algorithm.” The contrast between the two pages is now a lesson in itself.
The same seven fields, three different declarations:
Java: class Star { float x, y, sw, h, s, b; int offset;
Star(float x, ...) { this.x = x; ... } }
JavaScript: class Star { constructor(x, y, sw, h, s, b, offset) {
this.x = x; ... } }
Python: class Star:
def __init__(self, x, y, sw, h, s, b, offset):
self.x = x ...
Java declares field types; JavaScript has no type declarations;
Python uses self as the explicit first parameter and __init__
instead of constructor. The data model is identical in all three.
I know that the idea of a ‘sinusoid’ is new to many novices. We have a demo page for a sinusoid elsewhere at TNT. Can you notify novices about its presence so they can visit it for more clarity? Please document your decisions about placement in the chatlog.
Concept card 3 of twinklingStarsWithHueCTraining.html is the right home. That card introduces Sinusoid by name, explains why its parameters are stable, and immediately raises the question “but what is a sinusoid?” in any novice reader. The link belongs exactly there — after the Sinusoid explanation, before the lang-row pill — so it intercepts the question at the moment it arises.
The other three candidates don’t fit as cleanly:
- Showcase page (
twinklingStarsWithHue.html): its job is showing the running sketch. A reference link there pulls experienced readers into a detour before novices are ready for it. - Mid-chatlog (any session): chatlogs are discovery narratives. An embedded reference link reads as an interruption to the story — it belongs in the teaching pages, not here.
- v1 Sinusoidal Twinkling concept card (
hgTwinkStarsCTraining.html): the most natural long-term home — that card already explains the math in depth and the interactive demo would complement it perfectly. Flagged for a future update rather than added now to keep the scope contained.
The link uses target="_blank" so students
return to the concept cards after exploring the demo without losing their place.
Many novices encounter the word “sinusoid” for the first time in this sketch. The wave concept is briefly explained in the Sinusoidal Twinkling card on the v1 cross-training page, but an interactive experience is more valuable than prose. TNT already has a dedicated demo at SketchWaveTNT_Current/classDemos/swSinusoidWaveDemo.html where students can slide amplitude, period, phase shift, and vertical offset in real time.
Placement chosen: Concept card 3 of
twinklingStarsWithHueCTraining.html, immediately after the paragraph
that introduces Sinusoid as the “cleaner teaching object for classes.”
That is exactly where a novice who has just learned the class name would ask
“but what is a sinusoid?”
Placements not chosen, and why:
- Showcase page (
twinklingStarsWithHue.html): that page’s job is to show the running sketch; adding a teaching reference link there breaks the flow for experienced readers without helping novices who haven’t read the concept cards yet. - Chatlog sessions: chatlogs are discovery narratives; embedding a reference link mid-dialogue reads as an interruption.
- v1 cross-training Sinusoidal Twinkling card: a natural future addition — that card explains the math in depth and the demo would complement it directly. Deferred to avoid scope creep.
The link opens in a new tab (target="_blank")
so students return to their place in the concept cards after exploring the demo.
I think we should add the hue-changing stars app to our home page for Henry’s Ghost Saga. It is a slight diversion from the saga, but from there we can continue with the development of the ghost using ideas we’ve developed so far. Let’s add this prompt to our chatlog as well.
It belongs in the gallery, labeled honestly as an offroading trip rather than a numbered stage. The reasons:
- The hue-drift work has three complete pages — it is as polished as any other saga entry and deserves equal visibility.
- The class design lesson (
Star,Sinusoid,HSBColor) is directly applicable to the ghost rebuild. A reader who sees the Stage 2 Offroading card before Stage 3 will arrive at the ghost rebuild already holding the right mental models. - Calling it “Stage 2 Offroading” is more honest than quietly inserting it as a numbered stage — it signals that the saga takes productive detours, and that detours produce real lessons.
Placement in the gallery: insert the new card between Stage 2 (Twinkling Stars) and Stage 3 (Coming Soon), so the narrative reads: original → improved → offroaded → full rebuild.
A new entry card labeled Stage 2 Offroading was added to
henrysGhostSagaHome.html between the Twinkling Stars card and the
Stage 3 Coming Soon placeholder. The card links to the three hue-drift pages
(live sketch, cross-training, chatlog) and notes that the lessons here —
class design, float accumulators, three-language syntax comparison —
directly inform the full ghost rebuild.
The label “Stage 2 Offroading” is intentional: it signals a productive detour rather than a numbered step in the main sequence, preserving the saga’s honest narrative about how real software evolves.