@johnmorrisdotca/uchusen-rockets 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/NOTICE.md +2 -0
  3. package/README.md +78 -3
  4. package/dist/assets/droneship-asog-landed.glb +0 -0
  5. package/dist/assets/droneship-asog.glb +0 -0
  6. package/dist/assets/droneship-jrti-landed.glb +0 -0
  7. package/dist/assets/droneship-jrti.glb +0 -0
  8. package/dist/assets/droneship-ocisly-landed.glb +0 -0
  9. package/dist/assets/droneship-ocisly.glb +0 -0
  10. package/dist/assets/falcon-9-block-5-dragon-2-first-stage.glb +0 -0
  11. package/dist/assets/falcon-9-block-5-dragon-2.glb +0 -0
  12. package/dist/assets/falcon-9-block-5-first-stage.glb +0 -0
  13. package/dist/assets/falcon-9-block-5.glb +0 -0
  14. package/dist/assets/falcon-9-ft-first-stage.glb +0 -0
  15. package/dist/assets/falcon-9-ft.glb +0 -0
  16. package/dist/assets/falcon-9-v1.1-first-stage.glb +0 -0
  17. package/dist/assets/falcon-9-v1.1.glb +0 -0
  18. package/dist/assets/falcon-heavy-falcon-heavy-centre.glb +0 -0
  19. package/dist/assets/falcon-heavy-falcon-heavy-side-left.glb +0 -0
  20. package/dist/assets/falcon-heavy-falcon-heavy-side-right.glb +0 -0
  21. package/dist/assets/falcon-heavy-falcon-heavy.glb +0 -0
  22. package/dist/assets/landing-zone-lz-1-landed.glb +0 -0
  23. package/dist/assets/landing-zone-lz-1.glb +0 -0
  24. package/dist/assets/landing-zone-lz-4-landed.glb +0 -0
  25. package/dist/assets/landing-zone-lz-4.glb +0 -0
  26. package/dist/assets/launch-tower-catch-booster.glb +0 -0
  27. package/dist/assets/launch-tower-catch.glb +0 -0
  28. package/dist/assets/launch-tower-stack-booster.glb +0 -0
  29. package/dist/assets/launch-tower-stack-ship.glb +0 -0
  30. package/dist/assets/launch-tower-stack.glb +0 -0
  31. package/dist/assets/starship-2019.glb +0 -0
  32. package/dist/assets/starship-v1-booster.glb +0 -0
  33. package/dist/assets/starship-v1-flight1-booster.glb +0 -0
  34. package/dist/assets/starship-v1-flight1-ship.glb +0 -0
  35. package/dist/assets/starship-v1-flight1.glb +0 -0
  36. package/dist/assets/starship-v1-ship.glb +0 -0
  37. package/dist/assets/starship-v1.glb +0 -0
  38. package/dist/assets/starship-v2-booster.glb +0 -0
  39. package/dist/assets/starship-v2-ship.glb +0 -0
  40. package/dist/assets/starship-v2.glb +0 -0
  41. package/dist/assets/starship-v3-booster.glb +0 -0
  42. package/dist/assets/starship-v3-ship.glb +0 -0
  43. package/dist/assets/starship-v3.glb +0 -0
  44. package/dist/assets/starship-v4-booster.glb +0 -0
  45. package/dist/assets/starship-v4-ship.glb +0 -0
  46. package/dist/assets/starship-v4.glb +0 -0
  47. package/dist/build/glb.js +15 -7
  48. package/dist/build/index.d.ts +5 -0
  49. package/dist/build/index.js +16 -3
  50. package/dist/data/flights.d.ts +7 -0
  51. package/dist/data/flights.js +160 -13
  52. package/dist/data/propellants.d.ts +59 -0
  53. package/dist/data/propellants.js +43 -0
  54. package/dist/data/sources.d.ts +70 -0
  55. package/dist/data/sources.js +9 -0
  56. package/dist/draw/plume.d.ts +10 -1
  57. package/dist/draw/plume.js +15 -5
  58. package/dist/element/plumes.d.ts +7 -8
  59. package/dist/element/plumes.js +36 -21
  60. package/dist/element/scene.js +30 -5
  61. package/dist/element.js +3 -2
  62. package/dist/flight.d.ts +2 -0
  63. package/dist/flight.js +8 -1
  64. package/dist/flightbar.js +1 -1
  65. package/dist/index.d.ts +3 -0
  66. package/dist/index.js +2 -0
  67. package/dist/model/falcon.js +2 -2
  68. package/dist/model/moves.d.ts +36 -0
  69. package/dist/model/moves.js +70 -0
  70. package/dist/model/pieces.d.ts +31 -0
  71. package/dist/model/pieces.js +51 -9
  72. package/dist/model/starship.d.ts +6 -3
  73. package/dist/model/starship.js +12 -7
  74. package/dist/react.d.ts +2 -2
  75. package/package.json +18 -3
@@ -36,7 +36,7 @@ function starshipFlight(options) {
36
36
  { id: "boostback-end", name: "Boostback ends", what: "The booster falls back, steering with its grid fins.", at: time(times.boostbackEnd, source), burns: { ship: six }, pose: { booster: { ...away, pitch: 180 }, ship: { dx: 0.2, dy: 0.08 } }, ground: 1.6, air: 1 },
37
37
  { id: "booster-landing", name: "Booster's landing burn", what: "The booster lights its inner thirteen, then keeps only the inner three.", at: time(times.landing, source), burns: { booster: landingBurn, ship: six }, pose: { booster: { dx: -0.23, dy: -0.1 }, ship: { dx: 0.2, dy: 0.09 } }, ground: 0.45, air: 0.1 },
38
38
  { id: "booster-three", name: "Inner three", what: "The last of the landing burn, on the three middle engines.", at: blank("The page gives no time for this step."), burns: { booster: count(3, SH), ship: six }, pose: { booster: { dx: -0.23, dy: -0.32 }, ship: { dx: 0.2, dy: 0.1 } }, ground: 0.3, air: 0 },
39
- { id: "booster-down", name: "Booster down", what: "The landing burn ends.", at: time(times.landingEnd, source), burns: { ship: six }, pose: { booster: { dx: -0.23, rest: true }, ship: { dx: 0.2, dy: 0.11 } }, ground: 0, air: 0.8 },
39
+ { id: "booster-down", name: "Booster down", what: "The landing burn ends.", at: time(times.landingEnd, source), burns: { ship: six }, pose: { booster: { dx: -0.23, rest: true }, ship: { dx: 0.2, dy: 0.11 } }, ground: 0, air: 0.8, ...(options.boosterSpeed ? { speed: options.boosterSpeed } : {}) },
40
40
  { id: "seco", name: "Ship's engines cut off", what: "The Ship coasts on, above the air.", at: time(times.seco, source), burns: {}, pose: { booster: { dx: -0.23, rest: true }, ship: { dx: 0.2, dy: 0.12 } }, ground: 0, ...(options.apogee ? { altitude: options.apogee } : {}) },
41
41
  { id: "entry", name: "Entry", what: "The Ship comes back into the air, belly first on its tiles.", at: time(times.entry, source), burns: {}, pose: { booster: { dx: -0.23, rest: true }, ship: { dx: 0.2, dy: 0.12, pitch: -80 } }, ground: 0 },
42
42
  ...(seconds(times.flip) <= seconds(times.shipBurn)
@@ -64,18 +64,15 @@ const FLIGHT_12 = "wikipedia-flight-12";
64
64
  const FH = "wikipedia-fh-test-flight";
65
65
  const STARLINK = "sfn-starlink-2019";
66
66
  const CRS16 = "sfn-crs-16";
67
+ const FLIGHT_1 = "wikipedia-flight-1";
68
+ const CASSIOPE = "sfn-cassiope";
69
+ const ABS = "sfn-abs-eutelsat";
70
+ const F9 = "wikipedia-falcon-9";
71
+ const GRASSHOPPER = "wikipedia-grasshopper";
72
+ const nineMerlin = (note) => count(9, F9, note);
73
+ const oneMerlin = (note) => count(1, F9, note);
67
74
  const lower = (dy, extra = {}) => ({ dx: -0.26, dy, ...extra });
68
- /**
69
- * Every flight the package can play, one for each variant that has a timeline. Use `flightOf` to find one.
70
- *
71
- * @example
72
- * ```ts
73
- * import { FLIGHTS } from "@johnmorrisdotca/uchusen-rockets";
74
- *
75
- * console.log(FLIGHTS.map((flight) => `${flight.rocket}:${flight.variant}`));
76
- * ```
77
- */
78
- export const FLIGHTS = [
75
+ const BASE_FLIGHTS = [
79
76
  starshipFlight({
80
77
  id: "starship-flight-5",
81
78
  variant: "v1",
@@ -100,6 +97,7 @@ export const FLIGHTS = [
100
97
  stageEngines: count(3, SH, "The article's mission profile: all but three of its centre engines."),
101
98
  backEngines: count(13, SH, "The article's mission profile: ten more lit beside the three. On this flight nine of the ten lit."),
102
99
  landingBurn: count(13, FLIGHT_7, "All 13 engines ignited."),
100
+ apogee: { value: "146 km: where the Ship was when it was lost, 27 seconds before the planned cutoff", source: FLIGHT_7 },
103
101
  shipLanding: [],
104
102
  }),
105
103
  starshipFlight({
@@ -112,6 +110,8 @@ export const FLIGHTS = [
112
110
  stageEngines: count(5, SH, "The article's interstage section, for this booster: all but five inner engines. Its older mission profile says three."),
113
111
  backEngines: count(33, SH, "The article's propulsion section: the ascent and boostback burns use all thirty-three. The flight's page counts 28 to relight, beside the five already burning."),
114
112
  landingBurn: count(13, SH, "The inner thirteen."),
113
+ apogee: { value: "195 km, the highest of the Ship's orbit", source: FLIGHT_12 },
114
+ boosterSpeed: { value: "1,450 km/h when the booster hit the water in the Gulf, which it did not land from", source: FLIGHT_12 },
115
115
  shipLanding: [
116
116
  { id: "ship-two", name: "Three to two engines", what: "The Ship's landing burn drops to two.", at: time("+01:05:17", FLIGHT_12), burn: count(2, FLIGHT_12) },
117
117
  { id: "ship-one", name: "Two to one engine", what: "And to one, for the last seconds.", at: time("+01:05:24", FLIGHT_12), burn: count(1, FLIGHT_12) },
@@ -158,7 +158,7 @@ export const FLIGHTS = [
158
158
  { id: "entry-burn", name: "Entry burn", what: "Three engines again, to slow the fall.", at: time("+00:06:35", CRS16), burns: { "first-stage": THREE, "second-stage": UPPER }, pose: { "first-stage": lower(-0.14), "second-stage": { dx: 0.22, dy: 0.08 }, trunk: { dx: 0.22, dy: 0.08 }, capsule: { dx: 0.22, dy: 0.08 } }, ground: 0.8, air: 0.5, altitude: { value: "from about 70 to 40 km", source: "wikipedia-landing-tests", note: "The landing tests article, for the burn in general." } },
159
159
  { id: "landing-burn", name: "Landing burn", what: "Only the centre engine, just before touchdown.", at: blank("This timeline gives no time for its start."), burns: { "first-stage": ONE, "second-stage": UPPER }, pose: { "first-stage": lower(-0.3), "second-stage": { dx: 0.22, dy: 0.17 }, trunk: { dx: 0.22, dy: 0.17 }, capsule: { dx: 0.22, dy: 0.17 } }, ground: 0.22, air: 0 },
160
160
  { id: "landing", name: "Landing", what: "The planned landing, on plain ground here.", at: time("+00:08:17", CRS16), burns: { "second-stage": UPPER }, pose: { "first-stage": { dx: -0.26, rest: true }, "second-stage": { dx: 0.22, dy: 0.19 }, trunk: { dx: 0.22, dy: 0.19 }, capsule: { dx: 0.22, dy: 0.19 } }, ground: 0, air: 1 },
161
- { id: "seco", name: "SECO", what: "The second stage shuts down.", at: time("+00:08:51", CRS16), burns: {}, pose: { "first-stage": { dx: -0.26, rest: true }, "second-stage": { dx: 0.22, dy: 0.11 }, trunk: { dx: 0.22, dy: 0.11 }, capsule: { dx: 0.22, dy: 0.11 } }, ground: 0 },
161
+ { id: "seco", name: "SECO", what: "The second stage shuts down.", at: time("+00:08:51", CRS16), burns: {}, pose: { "first-stage": { dx: -0.26, rest: true }, "second-stage": { dx: 0.22, dy: 0.11 }, trunk: { dx: 0.22, dy: 0.11 }, capsule: { dx: 0.22, dy: 0.11 } }, ground: 0, altitude: { value: "an orbit with a low point of about 124 miles (200 km) and a high point of about 223 miles (360 km), as planned", source: CRS16 } },
162
162
  { id: "dragon", name: "Dragon separates", what: "The spacecraft comes away from the second stage.", at: time("+00:09:51", CRS16), burns: {}, pose: { "first-stage": { dx: -0.26, rest: true }, "second-stage": { dx: 0.22, dy: 0.17 }, trunk: { dx: 0.22, dy: 0.25 }, capsule: { dx: 0.22, dy: 0.25 } }, ground: 0 },
163
163
  ],
164
164
  },
@@ -184,4 +184,151 @@ export const FLIGHTS = [
184
184
  { id: "side-landing", name: "Side cores land", what: "Both side cores down, side by side.", at: time("+00:07:58", FH), burns: {}, pose: { "side-left": { dx: -0.23, rest: true }, "side-right": { dx: 0.34, rest: true } }, ground: 0 },
185
185
  ],
186
186
  },
187
+ {
188
+ id: "starship-flight-1",
189
+ rocket: "starship",
190
+ variant: "v1-flight1",
191
+ name: "Starship flight test 1",
192
+ basis: "SpaceX's timeline as Wikipedia gives it, for the flight of 20 April 2023, the first of Starship. It is what happened, not a plan: the stages never separated, and the flight termination system destroyed the stack. The planned steps after that (the boostback, the landing burn, the Ship's burn and its entry) were never reached, so they are not here.",
193
+ source: FLIGHT_1,
194
+ note: "No separation, tower or flaps and fins to move: the stack stays one piece to the end. Which of the booster's engines burned after the first minute, the page does not say, so the flames drawn there are a picture.",
195
+ phases: [
196
+ { id: "ignition", name: "Ignition", what: "The booster's engines start while it is held on the pad. Three of them were stopped at ignition because the flight software did not find them healthy enough.", at: time("-00:00:06", FLIGHT_1), burns: { booster: count(30, FLIGHT_1, "Thirty-three engines, three of whose ignition was terminated: the page's words, and the sum is this package's.") }, pose: { booster: {}, ship: {} }, ground: 0, smoke: 0.7 },
197
+ { id: "liftoff", name: "Liftoff", what: "A successful liftoff that significantly damaged the launch pad, the page says.", at: time("+00:00:05", FLIGHT_1), burns: { booster: count(30, FLIGHT_1, "The thirty that lit.") }, pose: { booster: {}, ship: {} }, ground: 0.02, smoke: 1 },
198
+ { id: "max-q", name: "Max q", what: "The throttle goes down for max q, later and lower than planned.", at: time("+00:00:55", FLIGHT_1), burns: { booster: count(30, FLIGHT_1, "The thirty that lit; the page does not count the failures that came during the climb.") }, pose: { booster: {}, ship: {} }, ground: 1.6, air: 0.25 },
199
+ { id: "meco", name: "Most engines cut off", what: "Fires in the booster's aft end led to a loss of communication with most of its engines.", at: time("+00:02:49", FLIGHT_1), burns: { booster: { engines: blank("The page gives no count of the engines that burned here."), drawn: 3 } }, pose: { booster: {}, ship: {} }, ground: 1.6, air: 0.7 },
200
+ { id: "separation", name: "Separation fails", what: "The stages did not separate. The vehicle lost control and began spinning.", at: time("+00:02:52", FLIGHT_1), burns: { booster: { engines: blank("The page gives no count of the engines that burned here."), drawn: 3 } }, pose: { booster: {}, ship: {} }, ground: 1.6, air: 0.85, altitude: { value: "about 39 km, the highest it rose", source: FLIGHT_1 } },
201
+ { id: "termination", name: "Flight termination", what: "The flight termination system was triggered.", at: time("+00:03:10", FLIGHT_1), burns: { booster: { engines: blank("The page gives no count of the engines that burned here."), drawn: 3 } }, pose: { booster: {}, ship: {} }, ground: 1.6, air: 1 },
202
+ { id: "destroyed", name: "Destroyed", what: "The stack was destroyed. The booster's engines had kept firing until about four minutes in.", at: time("+00:04:01", FLIGHT_1), burns: {}, pose: { booster: {}, ship: {} }, ground: 1.6, air: 1, altitude: { value: "29 km: the height at which the page says the booster's engines were still firing, at about four minutes", source: FLIGHT_1 } },
203
+ ],
204
+ },
205
+ {
206
+ id: "falcon-9-cassiope",
207
+ rocket: "falcon-9",
208
+ variant: "v1.1",
209
+ name: "Falcon 9 v1.1, CASSIOPE",
210
+ basis: "Spaceflight Now's live coverage of 29 September 2013, the first launch of the Falcon 9 v1.1. The minutes are the live coverage's own, rounded as it said them, and they are what happened, not a plan.",
211
+ source: CASSIOPE,
212
+ note: "The page does not follow the first stage after it separates, so it leaves the picture. A fairing is drawn though the satellites went inside it: that is this version's drawing. Altitudes are the live coverage's words, in the units it used.",
213
+ phases: [
214
+ { id: "ignition", name: "Ignition", what: "The nine engines start on the pad.", at: blank("The live coverage starts at liftoff."), burns: { "first-stage": nineMerlin("The nine Merlin 1D of this version, from its page.") }, pose: {}, ground: 0, smoke: 0.7 },
215
+ { id: "liftoff", name: "Liftoff", what: "Liftoff of the next-generation Falcon 9, at 1600 GMT.", at: time("+00:00:00", CASSIOPE), burns: { "first-stage": nineMerlin("The nine Merlin 1D of this version, from its page.") }, pose: {}, ground: 0.02, smoke: 1 },
216
+ { id: "max-q", name: "Max q", what: "The rocket nears the speed of sound, in the phase of maximum aerodynamic pressure.", at: time("+00:01:00", CASSIOPE), burns: { "first-stage": nineMerlin("The nine Merlin 1D of this version, from its page.") }, pose: {}, ground: 1.6, air: 0.25 },
217
+ { id: "climb", name: "Two minutes", what: "Climbing, with the first stage still burning.", at: time("+00:02:00", CASSIOPE), burns: { "first-stage": nineMerlin("The nine Merlin 1D of this version, from its page.") }, pose: {}, ground: 1.6, air: 0.6, altitude: { value: "more than 20 miles", source: CASSIOPE } },
218
+ { id: "separation", name: "Stage separation", what: "The first stage's engines cut off and the stages separate; the second stage's engine lights for about six minutes.", at: time("+00:03:20", CASSIOPE), burns: { "second-stage": oneMerlin("The one vacuum Merlin of this version, from its page.") }, pose: { "first-stage": lower(-0.05), "second-stage": { dx: 0.14, dy: 0.04 }, fairing: { dx: 0.14, dy: 0.04 } }, ground: 1.6, air: 0.9 },
219
+ { id: "fairing", name: "Fairing separation", what: "The payload fairing has separated as planned.", at: time("+00:04:00", CASSIOPE), burns: { "second-stage": oneMerlin("The one vacuum Merlin of this version, from its page.") }, pose: { "first-stage": lower(-0.1, { gone: true }), "second-stage": { dx: 0.22, dy: 0.08 }, fairing: { dx: 0.4, dy: 0.2, gone: true } }, ground: 1.6, air: 1 },
220
+ { id: "altitude", name: "Seven minutes twenty", what: "Still burning, high above the air.", at: time("+00:07:20", CASSIOPE), burns: { "second-stage": oneMerlin("The one vacuum Merlin of this version, from its page.") }, pose: { "first-stage": { gone: true }, "second-stage": { dx: 0.22, dy: 0.1 }, fairing: { gone: true } }, ground: 1.6, air: 1, altitude: { value: "310 kilometers", source: CASSIOPE } },
221
+ { id: "seco", name: "Second stage cutoff", what: "The second stage's engine shuts down, confirmed at 1609 GMT.", at: time("+00:09:00", CASSIOPE, { note: "From the page's GMT clock: liftoff at 1600, cutoff confirmed at 1609." }), burns: {}, pose: { "first-stage": { gone: true }, "second-stage": { dx: 0.22, dy: 0.12 }, fairing: { gone: true } }, ground: 1.6 },
222
+ { id: "deploy", name: "CASSIOPE deploys", what: "The satellite is released first, at T+14:15.", at: time("+00:14:15", CASSIOPE), burns: {}, pose: { "first-stage": { gone: true }, "second-stage": { dx: 0.22, dy: 0.16 }, fairing: { gone: true } }, ground: 1.6, altitude: { value: "a planned orbit between 155 miles and 900 miles high", source: CASSIOPE } },
223
+ ],
224
+ },
225
+ {
226
+ id: "falcon-9-abs-eutelsat",
227
+ rocket: "falcon-9",
228
+ variant: "ft",
229
+ name: "Falcon 9 Full Thrust, ABS-2A and Eutelsat 117 West B",
230
+ basis: "Spaceflight Now's timeline for the flight of 15 June 2016, the plan as it was published before the launch. Its page does not name the version; Wikipedia's article on ABS-2A says it was a Falcon 9 Full Thrust.",
231
+ source: ABS,
232
+ note: "The page says only that the first stage would try to land on a barge in the Atlantic and gives no time for it, so the stage is not followed after it separates. The second stage burns twice, and the page gives each time.",
233
+ phases: [
234
+ { id: "ignition", name: "Ignition", what: "The nine engines start on the pad.", at: blank("The timeline starts at liftoff."), burns: { "first-stage": nineMerlin("The nine Merlin 1D+ of this version, from its page.") }, pose: {}, ground: 0, smoke: 0.7 },
235
+ { id: "liftoff", name: "Liftoff", what: "Nine Merlin engines at full thrust.", at: time("+00:00:00", ABS), burns: { "first-stage": nineMerlin("The nine Merlin 1D+ of this version, from its page.") }, pose: {}, ground: 0.02, smoke: 1 },
236
+ { id: "mach-1", name: "Mach 1", what: "The rocket reaches the speed of sound.", at: time("+00:01:13", ABS), burns: { "first-stage": nineMerlin("The nine Merlin 1D+ of this version, from its page.") }, pose: {}, ground: 1.2, air: 0.1, speed: { value: "Mach 1", source: ABS } },
237
+ { id: "max-q", name: "Max Q", what: "The point of maximum aerodynamic pressure.", at: time("+00:01:17", ABS), burns: { "first-stage": nineMerlin("The nine Merlin 1D+ of this version, from its page.") }, pose: {}, ground: 1.6, air: 0.25 },
238
+ { id: "meco", name: "MECO", what: "The nine first stage engines shut down.", at: time("+00:02:36", ABS), burns: {}, pose: {}, ground: 1.6, air: 0.8 },
239
+ { id: "separation", name: "Stage separation", what: "The first stage falls away. It is not followed from here.", at: time("+00:02:39", ABS), burns: {}, pose: { "first-stage": lower(-0.05), "second-stage": { dx: 0.14, dy: 0.04 }, fairing: { dx: 0.14, dy: 0.04 } }, ground: 1.6, air: 0.9 },
240
+ { id: "second-stage", name: "Second stage ignition", what: "The vacuum Merlin lights for about six and a half minutes, to a parking orbit.", at: time("+00:02:47", ABS), burns: { "second-stage": oneMerlin("The one vacuum Merlin of this version, from its page.") }, pose: { "first-stage": lower(-0.1, { gone: true }), "second-stage": { dx: 0.22, dy: 0.08 }, fairing: { dx: 0.22, dy: 0.08 } }, ground: 1.6, air: 1 },
241
+ { id: "fairing", name: "Fairing jettison", what: "The payload fairing comes away.", at: time("+00:03:34", ABS), burns: { "second-stage": oneMerlin("The one vacuum Merlin of this version, from its page.") }, pose: { "first-stage": { gone: true }, "second-stage": { dx: 0.22, dy: 0.1 }, fairing: { dx: 0.4, dy: 0.2, gone: true } }, ground: 1.6, air: 1 },
242
+ { id: "seco-1", name: "SECO 1", what: "The second stage shuts down in its parking orbit.", at: time("+00:09:22", ABS), burns: {}, pose: { "first-stage": { gone: true }, "second-stage": { dx: 0.22, dy: 0.12 }, fairing: { gone: true } }, ground: 1.6 },
243
+ { id: "restart", name: "Second burn", what: "The second stage's engine restarts for the burn to the transfer orbit.", at: time("+00:25:54", ABS), burns: { "second-stage": oneMerlin("The one vacuum Merlin of this version, from its page.") }, pose: { "first-stage": { gone: true }, "second-stage": { dx: 0.22, dy: 0.14 }, fairing: { gone: true } }, ground: 1.6, air: 1 },
244
+ { id: "seco-2", name: "SECO 2", what: "The second stage shuts down again.", at: time("+00:26:58", ABS), burns: {}, pose: { "first-stage": { gone: true }, "second-stage": { dx: 0.22, dy: 0.16 }, fairing: { gone: true } }, ground: 1.6 },
245
+ { id: "eutelsat", name: "Eutelsat 117 West B separates", what: "The first of the two satellites is released.", at: time("+00:30:29", ABS), burns: {}, pose: { "first-stage": { gone: true }, "second-stage": { dx: 0.22, dy: 0.18 }, fairing: { gone: true } }, ground: 1.6 },
246
+ { id: "abs", name: "ABS-2A separates", what: "The second satellite is released.", at: time("+00:35:29", ABS), burns: {}, pose: { "first-stage": { gone: true }, "second-stage": { dx: 0.22, dy: 0.2 }, fairing: { gone: true } }, ground: 1.6 },
247
+ ],
248
+ },
249
+ {
250
+ id: "grasshopper-hop-8",
251
+ rocket: "grasshopper",
252
+ variant: "grasshopper",
253
+ name: "Grasshopper's highest hop",
254
+ basis: "Wikipedia's table of Grasshopper's eight hops: the last, on 7 October 2013, rose 744 m in 79 seconds. The page gives the height and the length of each hop and nothing between, so the steps between are a picture.",
255
+ source: GRASSHOPPER,
256
+ note: "The page gives a hop's height and duration only, so the engine's start, its throttling and the descent have no times here. The seven earlier hops were lower (the first rose 1.8 m in 3 s) and are not played.",
257
+ phases: [
258
+ { id: "ignition", name: "Ignition", what: "The one Merlin 1D starts.", at: blank("The page gives each hop's height and length, not when its engine started."), burns: { vehicle: count(1, GRASSHOPPER, "A single Merlin-1D engine, the page says.") }, pose: {}, ground: 0, smoke: 0.5 },
259
+ { id: "liftoff", name: "Liftoff", what: "The hop begins.", at: time("+00:00:00", GRASSHOPPER, { note: "The start of the hop's 79 seconds." }), burns: { vehicle: count(1, GRASSHOPPER, "A single Merlin-1D engine, the page says.") }, pose: {}, ground: 0.02, smoke: 0.8 },
260
+ { id: "top", name: "The top of the hop", what: "The highest point.", at: blank("The page gives the height, not the moment."), burns: { vehicle: count(1, GRASSHOPPER, "A single Merlin-1D engine, the page says.") }, pose: {}, ground: 1.6, air: 0.2, altitude: { value: "744 m, the highest of its eight hops", source: GRASSHOPPER } },
261
+ { id: "landing", name: "Landing", what: "It comes down on its fixed legs, 79 seconds after the start.", at: time("+00:01:19", GRASSHOPPER, { note: "The hop's duration, 79 s, from its start." }), burns: {}, pose: { vehicle: { rest: true } }, ground: 0 },
262
+ ],
263
+ },
187
264
  ];
265
+ const MOVE_SOURCES = {
266
+ falcon: { source: "wikipedia-landing-tests", says: "The grid fins are \"stowed on ascent and deployed on reentry\"; the landing burn comes, then the legs open, then the touchdown." },
267
+ superHeavy: { source: "wikipedia-super-heavy", says: "After the boostback the booster descends \"using its grid fins for minor course corrections\"." },
268
+ ship: { source: "wikipedia-ship", says: "The Ship \"controls its reentry with four flaps\", two aft and two forward." },
269
+ };
270
+ const MOVES_OF = {
271
+ "falcon-9-starlink-2019": ["falcon"],
272
+ "falcon-9-crs-16": ["falcon"],
273
+ "falcon-heavy-test-flight": ["falcon"],
274
+ "starship-flight-5": ["superHeavy", "ship"],
275
+ "starship-flight-7": ["superHeavy", "ship"],
276
+ "starship-flight-12": ["superHeavy", "ship"],
277
+ };
278
+ /**
279
+ * What turns in each flight. The pages say the Falcon 9's grid fins are "stowed on ascent and deployed on reentry" and that its legs open in the landing burn, before the touch down (Falcon 9 first-stage landing tests);
280
+ * that Super Heavy steers with its grid fins for minor course corrections after the boostback (Super Heavy); and that the Ship controls its reentry with four flaps, two aft and two forward (Starship). How far a group turns,
281
+ * and the moment inside a phase, are a picture. A painted model that is one shell has nothing here to turn: nothing is cut to fake a move.
282
+ */
283
+ const ARTICULATIONS = {
284
+ "falcon-9-starlink-2019": { "first-stage": [["ignition", { "grid-fins": 85, legs: 0 }], ["entry-burn", { "grid-fins": 0, legs: 0 }], ["landing-burn", { "grid-fins": 0, legs: 60 }]] },
285
+ "falcon-9-crs-16": { "first-stage": [["ignition", { "grid-fins": 85, legs: 0 }], ["entry-burn", { "grid-fins": 0, legs: 0 }], ["landing-burn", { "grid-fins": 0, legs: 60 }]] },
286
+ "falcon-heavy-test-flight": { "side-left": [["side-start", { legs: 0 }], ["side-burn", { legs: 60 }]], "side-right": [["side-start", { legs: 0 }], ["side-burn", { legs: 60 }]] },
287
+ "starship-flight-5": {
288
+ booster: [["boostback-end", { "grid-fins": 10 }], ["booster-landing", { "grid-fins": -10 }], ["booster-three", { "grid-fins": 6 }], ["booster-down", { "grid-fins": 0 }]],
289
+ ship: [["entry", { "forward-flaps": 8, "aft-flaps": 8 }], ["flip", { "forward-flaps": 60, "aft-flaps": 20 }]],
290
+ },
291
+ "starship-flight-7": {
292
+ booster: [["boostback-end", { "grid-fins": 10 }], ["booster-landing", { "grid-fins": -10 }], ["booster-three", { "grid-fins": 6 }], ["booster-down", { "grid-fins": 0 }]],
293
+ ship: [["entry", { "forward-flaps": 8, "aft-flaps": 8 }], ["flip", { "forward-flaps": 60, "aft-flaps": 20 }]],
294
+ },
295
+ "starship-flight-12": {
296
+ booster: [["boostback-end", { "grid-fins": 10 }], ["booster-landing", { "grid-fins": -10 }], ["booster-three", { "grid-fins": 6 }], ["booster-down", { "grid-fins": 0 }]],
297
+ ship: [["entry", { "forward-flaps": 8, "aft-flaps": 8 }], ["ship-burn", { "forward-flaps": 60, "aft-flaps": 20 }]],
298
+ },
299
+ };
300
+ function articulated(flight) {
301
+ const rules = ARTICULATIONS[flight.id];
302
+ if (!rules)
303
+ return flight;
304
+ let note = flight.note ?? "";
305
+ note = note.replace(/No tower, catch arms or moving flaps and fins yet: /, "No tower or catch arms yet: ").replace(/No drone ship or moving legs yet: /, "No drone ship yet: ").replace(/No moving legs or fins yet: /, "").replace(/No tower, catch arms or moving flaps and fins yet\b/, "No tower or catch arms yet");
306
+ return {
307
+ ...flight,
308
+ note: `${note}${note ? " " : ""}Its moving parts turn where the pages say they do: how far, and the moment inside a phase, are a picture.`.trim(),
309
+ moves: (MOVES_OF[flight.id] ?? []).map((key) => MOVE_SOURCES[key]),
310
+ phases: flight.phases.map((phase) => {
311
+ const pose = { ...phase.pose };
312
+ for (const [slot, steps] of Object.entries(rules)) {
313
+ let angles = {};
314
+ for (const [id, at] of steps) {
315
+ if (flight.phases.findIndex((one) => one.id === id) <= flight.phases.indexOf(phase))
316
+ angles = at;
317
+ }
318
+ pose[slot] = { ...(pose[slot] ?? {}), moves: angles };
319
+ }
320
+ return { ...phase, pose };
321
+ }),
322
+ };
323
+ }
324
+ /**
325
+ * Every flight the package can play, one for each variant that has a timeline. Use `flightOf` to find one.
326
+ *
327
+ * @example
328
+ * ```ts
329
+ * import { FLIGHTS } from "@johnmorrisdotca/uchusen-rockets";
330
+ *
331
+ * console.log(FLIGHTS.map((flight) => `${flight.rocket}:${flight.variant}`));
332
+ * ```
333
+ */
334
+ export const FLIGHTS = BASE_FLIGHTS.map(articulated);
@@ -0,0 +1,59 @@
1
+ import type { Figure } from "./types.ts";
2
+ /**
3
+ * The kinds of flame the pictures have a colour for. `neutral` is for an engine whose propellant no page read here names.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * import type { FlameKind } from "@johnmorrisdotca/uchusen-rockets";
8
+ *
9
+ * const kinds: FlameKind[] = ["kerosene", "methane", "hydrogen", "solid", "hypergolic", "neutral"];
10
+ * console.log(kinds.length);
11
+ * ```
12
+ */
13
+ export type FlameKind = "kerosene" | "methane" | "hydrogen" | "solid" | "hypergolic" | "neutral";
14
+ /**
15
+ * What one engine burns: its name as the variants' data spells it, the propellant as its page words it (a figure with its source), and the kind of flame that picks the colour.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * import { PROPELLANTS, type EnginePropellant } from "@johnmorrisdotca/uchusen-rockets";
20
+ *
21
+ * const merlin: EnginePropellant | undefined = PROPELLANTS.find((one) => one.id === "merlin");
22
+ * console.log(merlin?.propellant.value, merlin?.flame);
23
+ * ```
24
+ */
25
+ export interface EnginePropellant {
26
+ id: string;
27
+ name: string;
28
+ /** Words that, found in a variant's engine text, say this is the engine. */
29
+ names: readonly string[];
30
+ /** Left out: any rocket. A rocket's id when the same words mean another engine elsewhere (a "solid motor"). */
31
+ rocket?: string;
32
+ propellant: Figure<string>;
33
+ flame: Exclude<FlameKind, "neutral">;
34
+ }
35
+ /**
36
+ * The engines whose propellant a page read here names. Each propellant is a figure with its page; the flame kind follows from it (a test holds the two together).
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * import { PROPELLANTS } from "@johnmorrisdotca/uchusen-rockets";
41
+ *
42
+ * console.log(PROPELLANTS.map((one) => `${one.name}: ${one.propellant.value}`));
43
+ * ```
44
+ */
45
+ export declare const PROPELLANTS: readonly EnginePropellant[];
46
+ /**
47
+ * The record of what an engine burns, found by the engine's name in a variant's engine text, or null when no page read here names it. A rocket's `rocket` makes "solid motor" mean its own.
48
+ *
49
+ * @param rocket A rocket's id.
50
+ * @param engineText The engine text of a part, as a variant's data words it ("9 Merlin 1D"), or null or undefined.
51
+ * @returns What that engine burns, or null.
52
+ * @example
53
+ * ```ts
54
+ * import { propellantOf } from "@johnmorrisdotca/uchusen-rockets";
55
+ *
56
+ * console.log(propellantOf("falcon-9", "9 Merlin 1D")?.propellant.value, propellantOf("starship", "3 sea-level and 3 vacuum"));
57
+ * ```
58
+ */
59
+ export declare function propellantOf(rocket: string, engineText: string | null | undefined): EnginePropellant | null;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The engines whose propellant a page read here names. Each propellant is a figure with its page; the flame kind follows from it (a test holds the two together).
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * import { PROPELLANTS } from "@johnmorrisdotca/uchusen-rockets";
7
+ *
8
+ * console.log(PROPELLANTS.map((one) => `${one.name}: ${one.propellant.value}`));
9
+ * ```
10
+ */
11
+ export const PROPELLANTS = [
12
+ { id: "merlin", name: "Merlin", names: ["Merlin"], propellant: { value: "LOX / RP-1", source: "wikipedia-merlin", date: "2026-11-03", note: "The article's lead: \"Merlin engines use RP-1 and liquid oxygen as rocket propellants in a gas-generator power cycle.\"" }, flame: "kerosene" },
13
+ { id: "kestrel", name: "Kestrel", names: ["Kestrel"], propellant: { value: "LOX / RP-1", source: "wikipedia-kestrel", date: "2026-11-03", note: "The infobox, and the lead: \"an LOX/RP-1 pressure-fed rocket engine\"." }, flame: "kerosene" },
14
+ { id: "raptor", name: "Raptor", names: ["Raptor"], propellant: { value: "LOX / CH4", source: "wikipedia-raptor", date: "2026-11-03", note: "The infobox, and the lead: \"cryogenic liquid methane and liquid oxygen, a combination known as methalox\"." }, flame: "methane" },
15
+ { id: "f-1", name: "F-1", names: ["F-1"], propellant: { value: "LOX / RP-1", source: "wikipedia-f-1", date: "2026-11-03", note: "The infobox." }, flame: "kerosene" },
16
+ { id: "j-2", name: "J-2", names: ["J-2"], propellant: { value: "Liquid oxygen / Liquid hydrogen", source: "wikipedia-j-2", date: "2026-11-03", note: "The infobox, and the lead: \"burned cryogenic liquid hydrogen (LH2) and liquid oxygen (LOX) propellants\"." }, flame: "hydrogen" },
17
+ { id: "rs-25", name: "RS-25", names: ["RS-25"], propellant: { value: "LOX / LH2", source: "wikipedia-rs-25", date: "2026-11-03", note: "The infobox, and the lead: \"burns cryogenic (very low temperature) liquid hydrogen and liquid oxygen propellants\"." }, flame: "hydrogen" },
18
+ { id: "shuttle-srb", name: "Space Shuttle solid rocket booster", names: ["solid motor"], rocket: "space-shuttle", propellant: { value: "Ammonium perchlorate, aluminium powder and PBAN (APCP)", source: "wikipedia-shuttle-srb", date: "2026-11-03", note: "\"The primary propellants were ammonium perchlorate as the oxidizer along with aluminum powder and PBAN as fuel.\"" }, flame: "solid" },
19
+ { id: "sls-srb", name: "SLS solid rocket booster", names: ["solid motor"], rocket: "sls", propellant: { value: "PBAN-APCP", source: "wikipedia-sls-srb", date: "2026-11-03", note: "The infobox." }, flame: "solid" },
20
+ { id: "be-4", name: "BE-4", names: ["BE-4"], propellant: { value: "LOX / CH4", source: "wikipedia-be-4", date: "2026-11-03", note: "The infobox, and the lead: \"It uses liquefied methane fuel\"." }, flame: "methane" },
21
+ { id: "be-3", name: "BE-3", names: ["BE-3"], propellant: { value: "LOX / LH2", source: "wikipedia-be-3", date: "2026-11-03", note: "The infobox of the BE-3 and of the BE-3U." }, flame: "hydrogen" },
22
+ { id: "rutherford", name: "Rutherford", names: ["Rutherford"], propellant: { value: "LOX / RP-1", source: "wikipedia-rutherford", date: "2026-11-03", note: "The infobox, and the lead: \"LOX (liquid oxygen) and RP-1 (refined kerosene)\"." }, flame: "kerosene" },
23
+ { id: "rd-107", name: "RD-107 and RD-108", names: ["RD-107", "RD-108"], propellant: { value: "LOX / RG-1", source: "wikipedia-rd-107", date: "2026-11-03", note: "The infobox; its design section: \"liquid oxygen and highly refined kerosene (RG-1)\"." }, flame: "kerosene" },
24
+ { id: "lm-descent", name: "Lunar module descent engine", names: ["LM descent engine"], propellant: { value: "Aerozine 50 fuel / nitrogen tetroxide oxidizer", source: "wikipedia-lunar-module", date: "2026-11-03", note: "The article's specifications: \"DPS propellants: Aerozine 50 fuel / nitrogen tetroxide oxidizer\"." }, flame: "hypergolic" },
25
+ ];
26
+ /**
27
+ * The record of what an engine burns, found by the engine's name in a variant's engine text, or null when no page read here names it. A rocket's `rocket` makes "solid motor" mean its own.
28
+ *
29
+ * @param rocket A rocket's id.
30
+ * @param engineText The engine text of a part, as a variant's data words it ("9 Merlin 1D"), or null or undefined.
31
+ * @returns What that engine burns, or null.
32
+ * @example
33
+ * ```ts
34
+ * import { propellantOf } from "@johnmorrisdotca/uchusen-rockets";
35
+ *
36
+ * console.log(propellantOf("falcon-9", "9 Merlin 1D")?.propellant.value, propellantOf("starship", "3 sea-level and 3 vacuum"));
37
+ * ```
38
+ */
39
+ export function propellantOf(rocket, engineText) {
40
+ if (!engineText)
41
+ return null;
42
+ return PROPELLANTS.find((one) => (one.rocket === undefined || one.rocket === rocket) && one.names.some((name) => engineText.includes(name))) ?? null;
43
+ }
@@ -269,6 +269,76 @@ export declare const SOURCES: {
269
269
  readonly read: "direct";
270
270
  readonly licence: "CC BY-SA 4.0";
271
271
  };
272
+ readonly "sfn-cassiope": {
273
+ readonly title: "The launch of the first Falcon 9 v1.1, with CASSIOPE (Spaceflight Now's live coverage, its status page)";
274
+ readonly publisher: "Spaceflight Now";
275
+ readonly url: "https://spaceflightnow.com/falcon9/006/cassiopestatus.html";
276
+ readonly date: "2013-09-29";
277
+ readonly read: "direct";
278
+ };
279
+ readonly "sfn-abs-eutelsat": {
280
+ readonly title: "Mission events timeline for Falcon 9's launch for ABS and Eutelsat";
281
+ readonly publisher: "Spaceflight Now";
282
+ readonly url: "https://spaceflightnow.com/2016/06/14/mission-events-timeline-for-falcon-9s-launch-for-abs-and-eutelsat/";
283
+ readonly date: "2016-06-14";
284
+ readonly read: "direct";
285
+ };
286
+ readonly "wikipedia-abs-2a": {
287
+ readonly title: "ABS-2A (its infobox: the rocket was a Falcon 9 Full Thrust)";
288
+ readonly publisher: "Wikipedia";
289
+ readonly url: "https://en.wikipedia.org/wiki/ABS-2A";
290
+ readonly date: "2026-11-03";
291
+ readonly read: "direct";
292
+ readonly licence: "CC BY-SA 4.0";
293
+ };
294
+ readonly "wikipedia-be-4": {
295
+ readonly title: "Blue Origin BE-4 (the infobox)";
296
+ readonly publisher: "Wikipedia";
297
+ readonly url: "https://en.wikipedia.org/wiki/Blue_Origin_BE-4";
298
+ readonly date: "2026-11-03";
299
+ readonly read: "direct";
300
+ readonly licence: "CC BY-SA 4.0";
301
+ };
302
+ readonly "wikipedia-be-3": {
303
+ readonly title: "Blue Origin BE-3 (the infobox, for the BE-3 and the BE-3U)";
304
+ readonly publisher: "Wikipedia";
305
+ readonly url: "https://en.wikipedia.org/wiki/Blue_Origin_BE-3";
306
+ readonly date: "2026-11-03";
307
+ readonly read: "direct";
308
+ readonly licence: "CC BY-SA 4.0";
309
+ };
310
+ readonly "wikipedia-rutherford": {
311
+ readonly title: "Rutherford (rocket engine) (the infobox and the lead)";
312
+ readonly publisher: "Wikipedia";
313
+ readonly url: "https://en.wikipedia.org/wiki/Rutherford_(rocket_engine)";
314
+ readonly date: "2026-11-03";
315
+ readonly read: "direct";
316
+ readonly licence: "CC BY-SA 4.0";
317
+ };
318
+ readonly "wikipedia-rd-107": {
319
+ readonly title: "RD-107 (the infobox and its design section, for the RD-107 and the RD-108)";
320
+ readonly publisher: "Wikipedia";
321
+ readonly url: "https://en.wikipedia.org/wiki/RD-107";
322
+ readonly date: "2026-11-03";
323
+ readonly read: "direct";
324
+ readonly licence: "CC BY-SA 4.0";
325
+ };
326
+ readonly "wikipedia-kestrel": {
327
+ readonly title: "SpaceX Kestrel (the infobox and the lead)";
328
+ readonly publisher: "Wikipedia";
329
+ readonly url: "https://en.wikipedia.org/wiki/SpaceX_Kestrel";
330
+ readonly date: "2026-11-03";
331
+ readonly read: "direct";
332
+ readonly licence: "CC BY-SA 4.0";
333
+ };
334
+ readonly "wikipedia-sls-srb": {
335
+ readonly title: "Space Launch System Solid Rocket Booster (the infobox)";
336
+ readonly publisher: "Wikipedia";
337
+ readonly url: "https://en.wikipedia.org/wiki/Space_Launch_System_Solid_Rocket_Booster";
338
+ readonly date: "2026-11-03";
339
+ readonly read: "direct";
340
+ readonly licence: "CC BY-SA 4.0";
341
+ };
272
342
  readonly "wikipedia-shuttle-srb": {
273
343
  readonly title: "Space Shuttle Solid Rocket Booster (the infobox)";
274
344
  readonly publisher: "Wikipedia";
@@ -42,6 +42,15 @@ export const SOURCES = {
42
42
  "wikipedia-space-shuttle": { title: "Space Shuttle (the infobox)", publisher: "Wikipedia", url: "https://en.wikipedia.org/wiki/Space_Shuttle", date: "2026-11-02", read: "direct", licence: "CC BY-SA 4.0" },
43
43
  "wikipedia-shuttle-orbiter": { title: "Space Shuttle orbiter (the infobox)", publisher: "Wikipedia", url: "https://en.wikipedia.org/wiki/Space_Shuttle_orbiter", date: "2026-11-02", read: "direct", licence: "CC BY-SA 4.0" },
44
44
  "wikipedia-shuttle-tank": { title: "Space Shuttle external tank (the infobox)", publisher: "Wikipedia", url: "https://en.wikipedia.org/wiki/Space_Shuttle_external_tank", date: "2026-11-02", read: "direct", licence: "CC BY-SA 4.0" },
45
+ "sfn-cassiope": { title: "The launch of the first Falcon 9 v1.1, with CASSIOPE (Spaceflight Now's live coverage, its status page)", publisher: "Spaceflight Now", url: "https://spaceflightnow.com/falcon9/006/cassiopestatus.html", date: "2013-09-29", read: "direct" },
46
+ "sfn-abs-eutelsat": { title: "Mission events timeline for Falcon 9's launch for ABS and Eutelsat", publisher: "Spaceflight Now", url: "https://spaceflightnow.com/2016/06/14/mission-events-timeline-for-falcon-9s-launch-for-abs-and-eutelsat/", date: "2016-06-14", read: "direct" },
47
+ "wikipedia-abs-2a": { title: "ABS-2A (its infobox: the rocket was a Falcon 9 Full Thrust)", publisher: "Wikipedia", url: "https://en.wikipedia.org/wiki/ABS-2A", date: "2026-11-03", read: "direct", licence: "CC BY-SA 4.0" },
48
+ "wikipedia-be-4": { title: "Blue Origin BE-4 (the infobox)", publisher: "Wikipedia", url: "https://en.wikipedia.org/wiki/Blue_Origin_BE-4", date: "2026-11-03", read: "direct", licence: "CC BY-SA 4.0" },
49
+ "wikipedia-be-3": { title: "Blue Origin BE-3 (the infobox, for the BE-3 and the BE-3U)", publisher: "Wikipedia", url: "https://en.wikipedia.org/wiki/Blue_Origin_BE-3", date: "2026-11-03", read: "direct", licence: "CC BY-SA 4.0" },
50
+ "wikipedia-rutherford": { title: "Rutherford (rocket engine) (the infobox and the lead)", publisher: "Wikipedia", url: "https://en.wikipedia.org/wiki/Rutherford_(rocket_engine)", date: "2026-11-03", read: "direct", licence: "CC BY-SA 4.0" },
51
+ "wikipedia-rd-107": { title: "RD-107 (the infobox and its design section, for the RD-107 and the RD-108)", publisher: "Wikipedia", url: "https://en.wikipedia.org/wiki/RD-107", date: "2026-11-03", read: "direct", licence: "CC BY-SA 4.0" },
52
+ "wikipedia-kestrel": { title: "SpaceX Kestrel (the infobox and the lead)", publisher: "Wikipedia", url: "https://en.wikipedia.org/wiki/SpaceX_Kestrel", date: "2026-11-03", read: "direct", licence: "CC BY-SA 4.0" },
53
+ "wikipedia-sls-srb": { title: "Space Launch System Solid Rocket Booster (the infobox)", publisher: "Wikipedia", url: "https://en.wikipedia.org/wiki/Space_Launch_System_Solid_Rocket_Booster", date: "2026-11-03", read: "direct", licence: "CC BY-SA 4.0" },
45
54
  "wikipedia-shuttle-srb": { title: "Space Shuttle Solid Rocket Booster (the infobox)", publisher: "Wikipedia", url: "https://en.wikipedia.org/wiki/Space_Shuttle_Solid_Rocket_Booster", date: "2026-11-02", read: "direct", licence: "CC BY-SA 4.0" },
46
55
  "wikipedia-rs-25": { title: "RS-25 (the infobox and the nozzle's sizes)", publisher: "Wikipedia", url: "https://en.wikipedia.org/wiki/RS-25", date: "2026-11-02", read: "direct", licence: "CC BY-SA 4.0" },
47
56
  "wikipedia-sls": { title: "Space Launch System (the infobox)", publisher: "Wikipedia", url: "https://en.wikipedia.org/wiki/Space_Launch_System", date: "2026-11-02", read: "direct", licence: "CC BY-SA 4.0" },
@@ -2,7 +2,16 @@ import type { Camera } from "./svg.ts";
2
2
  import type { Vec3 } from "../framing.ts";
3
3
  import { type Bell } from "../engines.ts";
4
4
  import type { LookName } from "../looks.ts";
5
+ import type { FlameKind } from "../data/propellants.ts";
6
+ /** A flame's colours in a picture, by the kind its engine's propellant gives: the hot core, the middle and the cool edge, and the line looks' stroke. A picture, not a measurement of a flame. */
7
+ export declare const FLAME_COLOURS: Readonly<Record<FlameKind, {
8
+ core: string;
9
+ mid: string;
10
+ edge: string;
11
+ line: string;
12
+ inner: string;
13
+ }>>;
5
14
  /** The flames for these bells, in picture coordinates. Empty when there are none. */
6
- export declare function plumeMarkup(bells: readonly Bell[], at: Vec3, cam: Camera, look: LookName, prefix: string): string;
15
+ export declare function plumeMarkup(bells: readonly Bell[], at: Vec3, cam: Camera, look: LookName, prefix: string, kind?: FlameKind): string;
7
16
  /** The engines of a part seen from below, each in its own place, the burning ones filled: `engine by engine`. Its words are for a screen reader. */
8
17
  export declare function engineChart(bells: readonly Bell[], burning: readonly Bell[], label: string): string;
@@ -1,4 +1,13 @@
1
1
  import { PLUME_DIAMONDS, plumeLength } from "../engines.js";
2
+ /** A flame's colours in a picture, by the kind its engine's propellant gives: the hot core, the middle and the cool edge, and the line looks' stroke. A picture, not a measurement of a flame. */
3
+ export const FLAME_COLOURS = {
4
+ kerosene: { core: "#fff4d2", mid: "#ff9a3a", edge: "#ff4a12", line: "#e7a24a", inner: "#ffd7a1" },
5
+ methane: { core: "#fff1e6", mid: "#9aa5ff", edge: "#5d6dff", line: "#8f9bf0", inner: "#d9ddff" },
6
+ hydrogen: { core: "#ffffff", mid: "#b8dcff", edge: "#6fa8ff", line: "#9cc7f5", inner: "#e4f1ff" },
7
+ solid: { core: "#fff1c4", mid: "#ffb02e", edge: "#ff3d0a", line: "#f0a030", inner: "#ffe0a0" },
8
+ hypergolic: { core: "#ffffff", mid: "#e6ecff", edge: "#b8c4ee", line: "#c3cbe3", inner: "#f2f5ff" },
9
+ neutral: { core: "#ffffff", mid: "#d5dae3", edge: "#9aa3b2", line: "#b4bcc9", inner: "#e6e9ef" },
10
+ };
2
11
  const n = (v) => (Math.round(v * 100) / 100).toString();
3
12
  function hull(points) {
4
13
  const p = [...points].sort((a, b) => a[0] - b[0] || a[1] - b[1]);
@@ -19,9 +28,10 @@ function hull(points) {
19
28
  }
20
29
  const pathOf = (pts) => `M${pts.map(([x, y]) => `${n(x)} ${n(y)}`).join("L")}Z`;
21
30
  /** The flames for these bells, in picture coordinates. Empty when there are none. */
22
- export function plumeMarkup(bells, at, cam, look, prefix) {
31
+ export function plumeMarkup(bells, at, cam, look, prefix, kind = "neutral") {
23
32
  if (bells.length === 0)
24
33
  return "";
34
+ const colour = FLAME_COLOURS[kind];
25
35
  const painted = look === "painted";
26
36
  const stroke = look === "thick" ? 2.4 : 1.2;
27
37
  const parts = [];
@@ -45,17 +55,17 @@ export function plumeMarkup(bells, at, cam, look, prefix) {
45
55
  const rx = Math.max(1.2, (Math.max(...xs) - Math.min(...xs)) / 2);
46
56
  const ry = Math.max(1.2, (Math.max(...ys) - Math.min(...ys)) / 2);
47
57
  const defs = painted
48
- ? `<defs><linearGradient id="${id}" gradientUnits="userSpaceOnUse" x1="${n(mouthPoint[0])}" y1="${n(mouthPoint[1])}" x2="${n(tipPoint[0])}" y2="${n(tipPoint[1])}"><stop offset="0" stop-color="#fff4d2"/><stop offset="0.45" stop-color="#ff9a3a"/><stop offset="1" stop-color="#ff4a12" stop-opacity="0"/></linearGradient><radialGradient id="${glow}" gradientUnits="userSpaceOnUse" cx="${n(cx)}" cy="${n(cy)}" r="${n(Math.max(rx, ry))}"><stop offset="0" stop-color="#fff1c4"/><stop offset="1" stop-color="#ff7a1a" stop-opacity="0"/></radialGradient></defs>`
58
+ ? `<defs><linearGradient id="${id}" gradientUnits="userSpaceOnUse" x1="${n(mouthPoint[0])}" y1="${n(mouthPoint[1])}" x2="${n(tipPoint[0])}" y2="${n(tipPoint[1])}"><stop offset="0" stop-color="${colour.core}"/><stop offset="0.45" stop-color="${colour.mid}"/><stop offset="1" stop-color="${colour.edge}" stop-opacity="0"/></linearGradient><radialGradient id="${glow}" gradientUnits="userSpaceOnUse" cx="${n(cx)}" cy="${n(cy)}" r="${n(Math.max(rx, ry))}"><stop offset="0" stop-color="${colour.core}"/><stop offset="1" stop-color="${colour.mid}" stop-opacity="0"/></radialGradient></defs>`
49
59
  : "";
50
- const flame = shape.length >= 3 ? (painted ? `<path data-plume="1" d="${pathOf(shape)}" fill="url(#${id})" stroke="none"/>` : `<path data-plume="1" d="${pathOf(shape)}" fill="none" stroke="#e7a24a" stroke-width="${stroke}"/><path d="M${n(mouthPoint[0])} ${n(mouthPoint[1])}L${n(tipPoint[0])} ${n(tipPoint[1])}" fill="none" stroke="#ffd7a1" stroke-width="${stroke * 0.6}"/>`) : "";
51
- const ring = painted ? `<ellipse data-glow="1" cx="${n(cx)}" cy="${n(cy)}" rx="${n(rx * 1.35)}" ry="${n(ry * 1.35)}" fill="url(#${glow})"/>` : `<ellipse data-glow="1" cx="${n(cx)}" cy="${n(cy)}" rx="${n(rx * 1.2)}" ry="${n(ry * 1.2)}" fill="none" stroke="#e7a24a" stroke-width="${stroke}"/>`;
60
+ const flame = shape.length >= 3 ? (painted ? `<path data-plume="1" d="${pathOf(shape)}" fill="url(#${id})" stroke="none"/>` : `<path data-plume="1" d="${pathOf(shape)}" fill="none" stroke="${colour.line}" stroke-width="${stroke}"/><path d="M${n(mouthPoint[0])} ${n(mouthPoint[1])}L${n(tipPoint[0])} ${n(tipPoint[1])}" fill="none" stroke="${colour.inner}" stroke-width="${stroke * 0.6}"/>`) : "";
61
+ const ring = painted ? `<ellipse data-glow="1" cx="${n(cx)}" cy="${n(cy)}" rx="${n(rx * 1.35)}" ry="${n(ry * 1.35)}" fill="url(#${glow})"/>` : `<ellipse data-glow="1" cx="${n(cx)}" cy="${n(cy)}" rx="${n(rx * 1.2)}" ry="${n(ry * 1.2)}" fill="none" stroke="${colour.line}" stroke-width="${stroke}"/>`;
52
62
  // the first few shock diamonds, as small rings down the flame: a picture
53
63
  let diamonds = "";
54
64
  for (let k = 1; k <= 3; k++) {
55
65
  const along = k / PLUME_DIAMONDS;
56
66
  const [dx, dy] = cam.project([mouth[0], mouth[1] - plumeLength(bell.exit) * along, mouth[2]]);
57
67
  const size = n(Math.max(0.9, rx * 0.5 * (1 - along)));
58
- diamonds += painted ? `<ellipse data-diamond="1" cx="${n(dx)}" cy="${n(dy)}" rx="${size}" ry="${size}" fill="#fff4d2" fill-opacity="0.7"/>` : `<ellipse data-diamond="1" cx="${n(dx)}" cy="${n(dy)}" rx="${size}" ry="${size}" fill="none" stroke="#e7a24a" stroke-width="${stroke * 0.6}"/>`;
68
+ diamonds += painted ? `<ellipse data-diamond="1" cx="${n(dx)}" cy="${n(dy)}" rx="${size}" ry="${size}" fill="${colour.core}" fill-opacity="0.7"/>` : `<ellipse data-diamond="1" cx="${n(dx)}" cy="${n(dy)}" rx="${size}" ry="${size}" fill="none" stroke="${colour.line}" stroke-width="${stroke * 0.6}"/>`;
59
69
  }
60
70
  if (flame !== "")
61
71
  parts.push(`<g>${defs}${ring}${flame}${diamonds}</g>`);
@@ -1,7 +1,6 @@
1
+ import type { FlameKind } from "../data/propellants.ts";
1
2
  import { BufferGeometry, Group, PointLight, ShaderMaterial } from "three";
2
3
  import { type Bell } from "../engines.ts";
3
- /** What burns in each engine's flame, as a picture: a fuel's flame is drawn in its own colours. */
4
- export type Fuel = "methane" | "kerosene";
5
4
  /** One part's flames: at most as many as it has bells. Its buffers are made once and written when the engines that burn change. */
6
5
  export declare class PartPlumes {
7
6
  readonly group: Group<import("three").Object3DEventMap>;
@@ -9,23 +8,23 @@ export declare class PartPlumes {
9
8
  private flames;
10
9
  private glows;
11
10
  private key;
12
- constructor(kit: PlumeKit, bells: readonly Bell[]);
11
+ constructor(kit: PlumeKit, bells: readonly Bell[], kind: FlameKind);
13
12
  /** The bells that burn now. Nothing is written when they are the ones that burned last. */
14
13
  light(burning: readonly Bell[]): void;
15
14
  dispose(): void;
16
15
  }
17
- /** One set of everything a flame needs, for one viewer. */
16
+ /** One set of everything a flame needs, for one viewer. The materials of each kind of flame share one program and one set of clocks; only the two colours differ. */
18
17
  export declare class PlumeKit {
19
18
  readonly shape: BufferGeometry<import("three").NormalBufferAttributes, import("three").BufferGeometryEventMap>;
20
19
  readonly quad: BufferGeometry<import("three").NormalBufferAttributes, import("three").BufferGeometryEventMap>;
21
- readonly flame: ShaderMaterial;
22
- readonly glow: ShaderMaterial;
23
20
  /** The hull's own glow in the painted look. Always in the scene, at no strength when no engine burns, so lighting engines never makes the materials build again. */
24
21
  readonly light: PointLight;
25
22
  private uniforms;
26
- constructor();
23
+ private kinds;
24
+ private kind;
25
+ flameFor(kind: FlameKind): ShaderMaterial;
26
+ glowFor(kind: FlameKind): ShaderMaterial;
27
27
  setLook(painted: boolean): void;
28
- setFuel(fuel: Fuel): void;
29
28
  /** 0 near the ground, 1 high up where a flame fans out. */
30
29
  setAir(air: number): void;
31
30
  /** Where the ground stands, for a flame to stop at. */