Skip to content

Commit 52c36f7

Browse files
esaruohoclaude
andcommitted
Address PR feedback: speed-independent trail persistence with time-constant model
Replaces the draw-call-counter scope fade with a wall-clock-based exponential decay. The user setting now means a literal trail persistence in milliseconds, not an opaque alpha number. Per @pfalstad on PR #240: - "vary alpha instead of the number of draw calls between fade outs" - "should look the same regardless of the simulation speed setting" - "It still seems too persistent even at the lowest setting" Implementation: alpha = 1 - exp(-elapsed_ms / trailPersistence) This is the analytic solution for an exponential decay with time constant `trailPersistence`. Same fade rate at 30fps or 144fps, because elapsed wall-clock time appears in the exponent. The user sees and sets the time constant directly (default 200 ms). Range 0..2000 ms. Setting trailPersistence = 0 maps to alpha = 1 (instant erase, no trail at all) so "lowest setting" really is no trail, addressing the "still too persistent" complaint. Persisted per-scope as XML attribute "tp"; omitted when at the default to keep file size small. The MOSFET lambda part of the original PR #240 has been superseded by PR #313 (MosfetModel class), so this PR is now scope-fade-only. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7cf753f commit 52c36f7

File tree

2 files changed

+48
-11
lines changed

2 files changed

+48
-11
lines changed

src/com/lushprojects/circuitjs1/client/Scope.java

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,10 @@ class Scope {
262262
Canvas imageCanvas;
263263
Context2d imageContext;
264264
int alphaCounter =0;
265+
// XY-plot trail persistence (wall-clock ms). 0 = instant erase (no trail).
266+
int trailPersistence = 200;
267+
static final int DEFAULT_TRAIL_PERSISTENCE = 200;
268+
long lastTrailTime;
265269
// scopeTimeStep to check if sim timestep has changed from previous value when redrawing
266270
double scopeTimeStep;
267271
double scale[]; // Max value to scale the display to show - indexed for each value of UNITS - e.g. UNITS_V, UNITS_A etc.
@@ -1054,17 +1058,25 @@ void draw2d(Graphics g) {
10541058
g.context.translate(rect.x, rect.y);
10551059
g.clipRect(0, 0, rect.width, rect.height);
10561060

1057-
alphaCounter++;
1058-
1059-
if (alphaCounter>2) {
1060-
// fade out plot
1061-
alphaCounter=0;
1062-
imageContext.setGlobalAlpha(0.01);
1063-
if (app.isPrintable()) {
1061+
// Wall-clock-based exponential fade. alpha = 1 - exp(-elapsed/persistence)
1062+
// makes the trail look the same across simulation speeds: at 60fps a 200ms
1063+
// persistence drops a pixel by 1-exp(-16/200) = 7.7% per frame; at 30fps
1064+
// the same persistence drops by 1-exp(-33/200) = 15% per frame, so total
1065+
// decay over a fixed wall-clock interval is constant.
1066+
long now = System.currentTimeMillis();
1067+
long elapsed = (lastTrailTime == 0) ? 16 : (now - lastTrailTime);
1068+
lastTrailTime = now;
1069+
double fadeAlpha;
1070+
if (trailPersistence <= 0)
1071+
fadeAlpha = 1.0; // instant erase = no trail
1072+
else
1073+
fadeAlpha = 1.0 - Math.exp(-elapsed / (double) trailPersistence);
1074+
if (fadeAlpha > 0) {
1075+
imageContext.setGlobalAlpha(fadeAlpha);
1076+
if (app.isPrintable())
10641077
imageContext.setFillStyle("#ffffff");
1065-
} else {
1078+
else
10661079
imageContext.setFillStyle("#000000");
1067-
}
10681080
imageContext.fillRect(0,0,rect.width,rect.height);
10691081
imageContext.setGlobalAlpha(1.0);
10701082
}
@@ -2211,6 +2223,8 @@ void dumpXml(Document doc, Element root) {
22112223

22122224
if (text != null)
22132225
xmlElm.setAttribute("x", text);
2226+
if (trailPersistence != DEFAULT_TRAIL_PERSISTENCE)
2227+
XMLSerializer.dumpAttr(xmlElm, "tp", trailPersistence);
22142228
}
22152229

22162230
void undumpXml(XMLDeserializer xml) {
@@ -2226,6 +2240,7 @@ void undumpXml(XMLDeserializer xml) {
22262240
position = xml.parseIntAttr("p", 0);
22272241
manDivisions = xml.parseIntAttr("md", 8);
22282242
text = xml.parseStringAttr("x", (String)null);
2243+
trailPersistence = xml.parseIntAttr("tp", DEFAULT_TRAIL_PERSISTENCE);
22292244
// Read trigger settings from parent <o> element before iterating children,
22302245
// because parseChildElement() changes the XML context to child <p> elements
22312246
int xmlTriggerMode = xml.parseIntAttr("triggerMode", TRIGGER_FREERUN);

src/com/lushprojects/circuitjs1/client/ScopePropertiesDialog.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ public class ScopePropertiesDialog extends Dialog implements ValueChangeHandler<
5858
CheckBox elmInfoBox;
5959
TextBox labelTextBox, manualScaleTextBox, divisionsTextBox;
6060
Button applyButton, scaleUpButton, scaleDownButton;
61-
Scrollbar speedBar,positionBar;
61+
Scrollbar speedBar, positionBar, trailBar;
62+
Label trailLabel;
6263
Scope scope;
6364
Grid grid, vScaleGrid, hScaleGrid;
6465
int nx, ny;
@@ -518,6 +519,20 @@ public void onClick(ClickEvent event) {
518519
viBox.addValueChangeHandler(this);
519520
addItemToGrid(grid, xyBox = new ScopeCheckBox(Locale.LS("Plot X/Y"), "plotxy"));
520521
xyBox.addValueChangeHandler(this);
522+
Grid trailGrid = new Grid(1, 3);
523+
trailGrid.setWidget(0, 0, new Label(Locale.LS("Trail Persistence (ms)")));
524+
// Range 0..2000 ms; step 50; 0 = instant erase (no trail)
525+
trailBar = new Scrollbar(Scrollbar.HORIZONTAL, scope.trailPersistence, 1, 0, 2050, new Command() {
526+
public void execute() {
527+
scope.trailPersistence = trailBar.getValue();
528+
setTrailLabel();
529+
}
530+
});
531+
trailGrid.setWidget(0, 1, trailBar);
532+
trailLabel = new Label("");
533+
trailGrid.setWidget(0, 2, trailLabel);
534+
fp.add(trailGrid);
535+
setTrailLabel();
521536
if (transistor) {
522537
addItemToGrid(grid, vceIcBox = new ScopeCheckBox(Locale.LS("Show Vce vs Ic"), "showvcevsic"));
523538
vceIcBox.addValueChangeHandler(this);
@@ -625,7 +640,14 @@ void updateRowVisibility() {
625640
void setScopeSpeedLabel() {
626641
scopeSpeedLabel.setText(CircuitElm.getUnitText(scope.calcGridStepX(), "s")+"/div");
627642
}
628-
643+
644+
void setTrailLabel() {
645+
if (scope.trailPersistence <= 0)
646+
trailLabel.setText(Locale.LS("none"));
647+
else
648+
trailLabel.setText(scope.trailPersistence + " ms");
649+
}
650+
629651
void addItemToGrid(Grid g, FocusWidget scb) {
630652
g.setWidget(ny, nx, scb);
631653
if (++nx >= grid.getColumnCount()) {

0 commit comments

Comments
 (0)