In every other kind of casting you throw a weight and the line follows it. In fly casting the fly weighs thirty milligrams, which is nothing, and the line is the projectile. Ten metres of it, one gram per metre, thrown by being peeled off a fold that travels from the rod tip to the fly while the whole assembly falls out of the sky.
So the loop is not a style choice. It is the delivery mechanism, and nothing in this module knows what one looks like. There is a chain of point masses with inextensible links, gravity, and air; you move one end of it and stop. The loop is what happens next.
Which meant I could ask the question that fly-casting instruction never answers with a number: of everything you are doing, how much of it matters?
The two angles
A cast has two directions in it. One is the forward stroke — where you take the rod tip — and it is the one you are thinking about, the one every lesson is built around, the one you can feel. The other is where the line already happens to be lying when the stroke begins, which is the wreckage of a back cast that finished behind your head where you could not see it.
Same rig, same line, same stroke length and speed. Rows vary the back cast; columns vary the forward stroke. Carry in metres, each cell the mean of three tip speeds:
| back cast | stroke −15° | −8° | 0° | +8° | +15° | mean |
|---|---|---|---|---|---|---|
| −20° | 7.08 | 7.42 | 8.32 | 8.87 | 8.97 | 8.13 |
| −15° | 8.53 | 8.21 | 8.99 | 9.13 | 9.68 | 8.91 |
| −10° | 8.14 | 7.40 | 9.15 | 9.80 | 9.55 | 8.81 |
| −5° | 8.11 | 9.86 | 9.77 | 9.78 | 8.84 | 9.27 |
| 0° | 7.48 | 8.74 | 9.40 | 9.05 | 8.90 | 8.72 |
| +5° | 6.75 | 8.31 | 7.96 | 7.64 | 7.48 | 7.63 |
| +10° | 5.37 | 5.62 | 5.92 | 6.00 | 6.17 | 5.81 |
| +15° | 4.45 | 4.44 | 4.51 | 4.77 | 4.73 | 4.58 |
| +20° | 3.22 | 3.54 | 3.58 | 3.69 | 3.77 | 3.56 |
| +25° | 2.39 | 2.65 | 2.73 | 2.75 | 2.70 | 2.64 |
| +30° | 1.79 | 1.94 | 1.97 | 2.05 | 1.94 | 1.94 |
| mean | 5.75 | 6.19 | 6.57 | 6.68 | 6.61 |
Read the last row, then the last column.
Over the same matched ±15°, the forward stroke moves the cast 16% — 5.75 to 6.68 — and the back cast moves it 95%, 8.91 down to 4.58. Give the back cast the full range it was measured over and it runs 9.27 to 1.94: a factor of 4.8, on the axis you are not looking at.
And the columns inside the bad rows barely move at all. The +30° row is 1.79, 1.94, 1.97, 2.05, 1.94 — the same dead cast five times over, thrown five different directions. The forward stroke is not compensating. There is nothing to compensate with.
I went in expecting the folk rule — the forward stroke must go 180° opposite the back cast — and expecting the diagonal of that table to light up. It does not. There is no diagonal. There is a back cast that works and a back cast that does not, and the stroke you put on it is a rounding error.
You cannot power out of it
The obvious objection is that a bad back cast is just a cast that needs more effort. So: every dial a caster consciously turns, run against a good back cast (−5°) and a high one (+25°).
| back −5° | back +25° | cost | |
|---|---|---|---|
| tip speed 20 m/s | 6.13 ± 0.19 m | 1.05 ± 0.16 m | −5.08 m |
| tip speed 28 m/s | 10.06 ± 0.16 m | 2.76 ± 0.03 m | −7.29 m |
| tip speed 36 m/s | 9.12 ± 1.04 m | 3.68 ± 0.09 m | −5.45 m |
| tip speed 44 m/s | 9.13 ± 0.72 m | 4.16 ± 0.15 m | −4.96 m |
| stroke 2.0 m | 7.92 ± 1.39 m | 2.80 ± 0.56 m | −5.12 m |
| stroke 3.0 m | 9.80 ± 0.78 m | 2.71 ± 0.51 m | −7.09 m |
| stroke 4.0 m | 11.12 ± 0.46 m | 2.46 ± 0.53 m | −8.66 m |
| stop in 15 ms | 6.56 ± 1.76 m | 2.12 ± 0.57 m | −4.44 m |
| stop in 28 ms | 9.80 ± 0.78 m | 2.71 ± 0.51 m | −7.09 m |
| stop in 50 ms | 9.68 ± 0.56 m | 2.95 ± 0.52 m | −6.73 m |
Every row is negative and no row is close to zero. Taking the tip from 20 m/s to 44 — from a competent cast to a speed no arm produces — buys back 3.1 m on the ruined cast and still lands 5.9 m short of what a level back cast gets for free at 28. Lengthening the stroke widens the gap, because a longer stroke is worth 3.2 m on a good back cast and −0.3 m on a bad one: it is the one input whose value depends on the geometry being right first.
The stop is my favourite row and I did not predict it. A crisper stop is worse on both — 6.56 against 9.80 on the good back cast. Fifteen milliseconds is a deceleration so sharp that the shock outruns the line’s own ability to carry it, and the energy goes into a kink instead of into the fold. There is a right amount of crispness and it is not “more”.
The air is doing half the work
The line is 1.2 mm across and 1 g/m. Ten metres of it takes about a second to turn over, and a second is a long time to be falling. So I took the air away:
| carry | loop | |
|---|---|---|
| as modelled | 8.59 ± 1.57 m | 0.42 m |
| no skin friction along the line | 8.85 ± 1.87 m | 0.43 m |
| no broadside drag | 3.96 ± 2.31 m | 0.59 m |
| no air at all | 4.02 ± 3.15 m | 0.55 m |
| half the broadside drag | 7.04 ± 1.73 m | 0.45 m |
| twice the broadside drag | 6.27 ± 1.29 m | 0.41 m |
| four times the broadside drag | 4.43 ± 0.99 m | 0.37 m |
Air resistance is worth 54% of the cast. Delete it and the line free-falls instead of settling to a terminal 3 m/s, hits the water at four metres, and never turns over. Skin friction — the part that actually slows the line down, the part you would think of as drag — is worth nothing at all: 8.85 against 8.59, inside the noise.
And the real value sits on top of the hill. Half is worse, double is worse, four
times is much worse. That is not a fitted result; nothing was tuned for it. cn
is a cylinder in crossflow and ct is a turbulent flat plate, both read off
standard correlations, and a fly line at 1.2 mm and 20 m/s lands near the peak
of its own drag curve by itself.
The two coefficients differ by a factor of 340. Getting that ratio wrong was my first bug: with skin friction set to a bluff-body value the cast died at nine metres every time, and the reason is the thing the whole piece turns on. A straight leg of line moving along its own length is almost frictionless; only the fold is broadside. Loop width is a drag budget.
What you actually do in the demo
You steer the rod tip, not the rod. The pointer is where the top of the rod is trying to be; the bend you can see is drawn to reach it, not simulated. That is deliberate — see the gotchas — and it means the cast you make here and the cast in the tables above are driven by the same input.
- Move the mouse — the tip follows at up to 34 m/s. Take it back, let the line straighten behind you, then bring it forward and stop.
- The panel reads your back cast while it is still behind you, in degrees above horizontal, against a band: green to +12°, amber to +20°, red past it. Once you commit, the number is held, because that is the reading the cast flew on.
- Click to deliver — the running line is released and shoots whatever the loop is still pulling for. Hold space to haul.
- Every landed cast writes a row: back cast, stroke elevation, carry. After six of them you have rebuilt the first table by hand, and the middle column will not correlate with anything.
1a level back cast ·2the identical stroke on a high one ·3the same high one hit much harder ·cthe coach wedge ·rreset.
2 and 3 keep 1 on screen as a dashed ghost. They are the tables as a
picture: 8.3 m, 2.0 m, and 3.3 m.
Reuse
src/flycast.mjs is framework-free and draws nothing. Metres, kilograms,
seconds; +x downstream, +y up, y = 0 is the water.
createCast({ drive, lineOut, reserve, hand })—drive: 'tip'means you placestate.rod[ROD.nodes-1]yourself each step and the module reads the velocity from the move;'hand'means you setstate.handAngleand the rigid rod places its own tip.step(state, dt)— one step.dt = 1/2000is plenty; see the convergence table below for why that is a claim and not a hope.layoutBehind(state, tip, angleDeg)— the initial condition: a straightened back cast at rest, trailingangleDegabove horizontal. This is the variable the piece is about.tipStroke({ a, b, dev, speed, accel, stopDur })— a stroke as a speed profile: accelerate as t^accel tospeed, then a cosine-squared ramp to rest overstopDur. Returns(t) => {x, y, done, speed}.loopMetrics(state, tipX)—{ width, crossed, rodLeg, flyLeg, apex, travel }. Width is the median gap between the legs from 0.3 m to 1.5 m behind the fold;crossedis a tailing loop, detected as the gap changing sign.simulateCast(opts)— the headless rig. Drives the tip along a prescribed path, lets the loop unroll, and returns carry, loop width, KE retained, fold travel and turnover time. Every table above is a call to it;scripts/measure.mjsreproduces them (885 casts, a couple of minutes).state.hauling(m/s of line-hand pull) andstate.shootingdrive the running line;state.tensionis the tension in the first link, in newtons, which is what the guides feel.
A whole cast at dt = 1/4000 is about 130 ms in Node, so a few hundred of them
is a coffee.
Gotchas
-
The rod does not bend, and that is the largest omission in the model. A real rod is a spring: it loads under the line and the tip finishes somewhere the hand did not go. Leaving it out costs the piece the whole subject of rod action. It buys two things worth more here. First, a rigid rod turned about a fixed hand can draw exactly one shape — an arc of its own length — so if you want a straight tip path you must drive the tip, and driving the tip is what makes the demo and the measuring rig the same experiment. Second, when a cast fails there is no third party to blame it on. The geometry you set up is the geometry that flew.
-
The line is solved as a chain of rigid links, not with PBD, and this was not a preference. Iterated position-based dynamics on a fifty-node chain does not converge in one step: tension propagates about one node per sweep, so a hard stop at the rod never reaches the far end, and the velocity-from-position update quietly deletes the kinetic energy that did not make it. That loss is per step, so halving the timestep doubles it. The same cast came out 92% extended at
dt = 1/1000and 34% extended atdt = 1/4000— every number above would have been a property of the integrator. Requiring zero relative velocity along every link simultaneously is a tridiagonal system in the link tensions; a forward sweep and a back substitution solve it exactly, in O(n), at any timestep. Here is the same bank at four:dt back −5° back +25° gap 1/1000 9.86 ± 0.76 m 2.64 ± 0.61 m −7.22 m 1/2000 9.87 ± 0.72 m 2.69 ± 0.53 m −7.18 m 1/4000 9.80 ± 0.78 m 2.71 ± 0.51 m −7.09 m 1/8000 9.81 ± 0.73 m 2.73 ± 0.51 m −7.08 m Eight times the resolution moves the good cast by 0.5% and the gap by 2%.
-
A stop that takes one timestep is an infinite deceleration, and what comes back from it is a property of the timestep. This cost me an afternoon of results that changed with
dtfor a reason that had nothing to do with the solver.tipStroketherefore specifies a speed profile with a real 28 ms ramp to rest, which is also roughly what a stop takes. -
LINE.dampis the one parameter with no independent anchor. A chain of point masses has no bending stiffness and no hysteresis, so the shock off the stop rings up and down the line for the entire flight and the loop never looks like a loop. A real fly line is a braided core in a plastic coat and does not do this. The term is a viscosity on curvature: 45/s cuts the wobble by a factor of five for 3% of the distance. It is tuned by eye against footage, and if you are porting this it is the number to reach for first. It also, to my surprise, made every table more monotone rather than less — the geometry column above is clean now and was noisy before. -
One cast is not a measurement. An unrolling loop is a shock travelling down a chain, and where the fly happens to be when the line touches down moves by the better part of a metre for reasons that are not about the cast. Single casts here have a spread of ±1 m, which is larger than most effects worth reporting, so every number above is a mean over a bank of 9–36 casts that differ in the things not being asked about. An effect smaller than the spread is not an effect — which is exactly why the forward-stroke column is reported as “nothing” and not as a small positive.
-
A claim this piece does not make. I built the whole thing to measure a different rule — that loop width equals the deviation of the tip path from a straight line, which is the single most repeated sentence in casting instruction. It does not hold here. Width rises on both sides of zero, but not monotonically and not by a stable ratio, and after two rounds of trying to make the metric behave I concluded the model was telling me the effect is smaller than its own noise rather than that my ruler was bent.
tipStrokestill takes adevargument andstrokeMetricsstill measures it, so the experiment is there to rerun on a model with a bending rod in it. I would start there. -
The line lands and stays landed. A node touching y = 0 is frozen with a little drag; carry is the fly’s x the first time it touches. Nothing here models a line lifting off the water, so the demo’s reset is a new cast rather than a pickup.
-
demo/bundles its own copy of the module (self-contained by contract). If you touchsrc/, re-copy it intodemo/.


