diff --git a/.gitattributes b/.gitattributes index 9c7178e..d22a7ab 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ *.ipynb linguist-documentation=true +*.mp4 filter=lfs diff=lfs merge=lfs -text diff --git a/docs/assets/Anise010DynamicFrameAnnouncement.mp4 b/docs/assets/Anise010DynamicFrameAnnouncement.mp4 new file mode 100644 index 0000000..ca0cfde --- /dev/null +++ b/docs/assets/Anise010DynamicFrameAnnouncement.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f4e6419b655cbc298b9042488cb81bf20f3ca25ce9900f28db2d6f6562ba1eca +size 3204757 diff --git a/firebase.json b/firebase.json index 642d828..e36dd4f 100644 --- a/firebase.json +++ b/firebase.json @@ -5,6 +5,17 @@ "firebase.json", "**/.*", "**/node_modules/**" + ], + "headers": [ + { + "source": "/assets/**/*.@(png|jpg|jpeg|gif|pdf|svg)", + "headers": [ + { + "key": "Cache-Control", + "value": "public, max-age=31536000, immutable" + } + ] + } ] } } diff --git a/nyx-animations/.gitignore b/nyx-animations/.gitignore new file mode 100644 index 0000000..7f69fa5 --- /dev/null +++ b/nyx-animations/.gitignore @@ -0,0 +1,2 @@ +__pycache__ +media diff --git a/nyx-animations/anise-0.10.py b/nyx-animations/anise-0.10.py new file mode 100644 index 0000000..5a2747f --- /dev/null +++ b/nyx-animations/anise-0.10.py @@ -0,0 +1,558 @@ +""" +ANISE 0.10 announcement animation +Dynamic frame ID parsing -> DCM construction + +Render: + manim -pqh anise_dynamic_frame_announcement.py Anise010DynamicFrameAnnouncement + +Notes: +- This scene avoids LaTeX so it is easier to render on a clean Manim install. +- It is intentionally storyboard-like: the values are illustrative, while the decoding + matches ANISE's documented packed dynamic-frame orientation ID format. +""" + +from __future__ import annotations + +from dataclasses import dataclass + +import numpy as np +from manim import * + +# Manim's default coordinate frame is 14.222... x 8 units for 16:9 output. +# These safe bounds leave room for titles, captions, and video-platform cropping. +SAFE_WIDTH = 12.6 +SAFE_HEIGHT = 6.5 + + +# ----------------------------------------------------------------------------- +# Visual constants +# ----------------------------------------------------------------------------- +BG = "#0D1117" +PANEL = "#161B22" +PANEL_2 = "#1F2937" +FG = "#E6EDF3" +MUTED = "#8B949E" +CYAN = "#56D4DD" +BLUE = "#58A6FF" +GREEN = "#7EE787" +YELLOW = "#F2CC60" +ORANGE = "#FFA657" +RED = "#FF7B72" +PURPLE = "#D2A8FF" +GRAY = "#30363D" + +FONT = "DejaVu Sans" +MONO = "DejaVu Sans Mono" + + +@dataclass(frozen=True) +class ByteSpec: + value: str + name: str + meaning: str + color: str + + +DYNAMIC_ID_BYTES = [ + ByteSpec("0xA0", "prefix", "ANISE dynamic frame", CYAN), + ByteSpec("0xE1", "family", "Earth TOD", BLUE), + ByteSpec("0x03", "primary", "IAU 2006 precession", GREEN), + ByteSpec("0x03", "secondary", "IAU 2006 / 2000A nutation", ORANGE), +] + + +class Anise010DynamicFrameAnnouncement(MovingCameraScene): + """Announcement animation for ANISE 0.10 dynamic frames.""" + + def construct(self) -> None: + self.camera.background_color = BG + self.reset_camera_frame() + + self.title_card() + self.standard_id() + self.dynamic_id_decode() + self.rotate_call() + self.dcm_pipeline() + self.time_scale_conversion() + self.frozen_and_force_inertial() + self.end_card() + + # ------------------------------------------------------------------ + # Scene sections + # ------------------------------------------------------------------ + def title_card(self) -> None: + self.next_section("title") + title = Text("ANISE 0.10", font=FONT, font_size=68, weight=BOLD, color=FG) + subtitle = Text( + "Dynamic frames encoded directly in orientation IDs", + font=FONT, + font_size=31, + color=MUTED, + ) + group = VGroup(title, subtitle).arrange(DOWN, buff=0.25) + + marker = self.code_chip("0xA0 FF AA BB", color=CYAN, font_size=34) + marker.next_to(group, DOWN, buff=0.65) + self.camera_fit(group, marker) + + self.play(FadeIn(title, shift=0.35 * UP), run_time=0.8) + self.play(FadeIn(subtitle), FadeIn(marker, shift=0.2 * UP), run_time=0.9) + self.wait(2) + self.play(FadeOut(group), FadeOut(marker), run_time=0.5) + + def standard_id(self) -> None: + self.next_section("standard orientation id") + header = self.section_header("1. The ordinary case") + frame_box = self.id_card( + title="orientation_id", + value="399", + subtitle="Earth IAU model", + color=GREEN, + ).scale(1.08) + lookup = self.pipeline_box("lookup orientation data", GREEN) + dcm = self.block_dcm(label="DCM from stored model", highlight=GREEN) + + group = VGroup(frame_box, lookup, dcm).arrange(RIGHT, buff=0.65).move_to(ORIGIN) + self.fit_to_frame(group, max_width=SAFE_WIDTH, max_height=4.7) + arrows = VGroup( + self.arrow_between(frame_box, lookup), + self.arrow_between(lookup, dcm), + ) + + note = Text( + "No packed-byte interpretation: the ID directly routes to an orientation model.", + font=FONT, + font_size=24, + color=MUTED, + ).next_to(group, DOWN, buff=0.45) + self.camera_fit(header, group, arrows, note) + + self.play(FadeIn(header), FadeIn(frame_box, shift=0.25 * UP), run_time=0.8) + self.play(GrowArrow(arrows[0]), FadeIn(lookup), run_time=0.55) + self.play(GrowArrow(arrows[1]), FadeIn(dcm), run_time=0.7) + self.play(FadeIn(note), run_time=0.4) + self.wait(5.0) + self.play(FadeOut(VGroup(header, group, arrows, note)), run_time=0.55) + + def dynamic_id_decode(self) -> None: + self.next_section("dynamic id decoding") + header = self.section_header("2. Dynamic frames start with 0xA0") + + signed = Text( + "orientation_id = 0xA0E10303 // signed i32: -1595866365", + font=MONO, + font_size=28, + color=FG, + ).to_edge(UP, buff=1.35) + self.fit_to_frame(signed, max_width=SAFE_WIDTH) + + bytes_group = VGroup(*[self.byte_cell(spec) for spec in DYNAMIC_ID_BYTES]) + bytes_group.arrange(RIGHT, buff=0.17).scale(1.02).move_to(0.25 * UP) + + equation = Text( + "0xA0 FF AA BB → DynamicFrame::EarthTrueOfDate { precession, nutation }", + font=MONO, + font_size=23, + color=MUTED, + ).next_to(bytes_group, DOWN, buff=0.45) + + self.camera_fit(header, signed, bytes_group, equation) + + self.play(FadeIn(header), FadeIn(signed, shift=0.2 * UP), run_time=0.7) + self.play(LaggedStart(*[FadeIn(c, shift=0.3 * UP) for c in bytes_group], lag_ratio=0.12), run_time=1.2) + + prefix_box = SurroundingRectangle(bytes_group[0], color=CYAN, buff=0.08, corner_radius=0.08) + self.play(Create(prefix_box), run_time=0.35) + self.play(Indicate(bytes_group[0], color=CYAN, scale_factor=1.05), run_time=0.6) + self.play(Transform(prefix_box, SurroundingRectangle(bytes_group[1:], color=BLUE, buff=0.08, corner_radius=0.08)), run_time=0.55) + self.play(FadeIn(equation), run_time=0.55) + self.wait(3.0) + self.play(FadeOut(VGroup(header, signed, bytes_group, equation, prefix_box)), run_time=0.55) + + def rotate_call(self) -> None: + self.next_section("rotate call") + header = self.section_header("3. The public call stays small") + + code = self.code_panel( + [ + "let dcm = almanac.rotate(", + "from_frame,", + "to_frame,", + "epoch)?", + ], + highlight_lines={0: BLUE, 3: PURPLE}, + ).scale(1.0).move_to(ORIGIN + 0.25 * UP) + + left = self.small_frame_card("from_frame", "EME2000", BLUE) + right = self.small_frame_card("to_frame", "0xA0E10303", CYAN) + epoch = self.small_frame_card("epoch", "2024-02-29T05:17:31 TCL", PURPLE, True) + params = VGroup(left, right, epoch).arrange(RIGHT, buff=0.35).next_to(code, DOWN, buff=0.55) + self.fit_to_frame(VGroup(code, params), max_width=SAFE_WIDTH, max_height=5.0) + + pulse = SurroundingRectangle(params, color=PURPLE, buff=0.16, corner_radius=0.12) + self.camera_fit(header, code, params, pulse) + + self.play(FadeIn(header), FadeIn(code, shift=0.25 * UP), run_time=0.8) + self.play(LaggedStart(*[FadeIn(p, shift=0.2 * UP) for p in params], lag_ratio=0.15), run_time=0.8) + self.play(Create(pulse), run_time=0.35) + self.play(pulse.animate.set_stroke(opacity=0.0), run_time=0.55) + self.wait(5.0) + self.play(FadeOut(VGroup(header, code, params, pulse)), run_time=0.55) + + def dcm_pipeline(self) -> None: + self.next_section("dcm pipeline") + header = self.section_header("4. Decode, evaluate, compose") + + stages = VGroup( + self.pipeline_box("parse orientation_id", CYAN), + self.pipeline_box("select dynamic model", BLUE), + self.pipeline_box("evaluate at epoch", PURPLE), + self.pipeline_box("compose DCM", GREEN), + ).arrange(RIGHT, buff=0.4).move_to(0.4 * UP) + arrows = VGroup(*[self.arrow_between(stages[i], stages[i + 1]) for i in range(len(stages) - 1)]) + + detail_lines = [ + ("0xA0", "dynamic marker", CYAN), + ("0xE1", "Earth True Equator, True Equinox of Date", BLUE), + ("0x03", "IAU 2006 precession-bias", GREEN), + ("0x03", "IAU 2006 / 2000A-compatible nutation", ORANGE), + ] + details = VGroup(*[self.key_value(k, v, c) for k, v, c in detail_lines]).arrange(DOWN, aligned_edge=LEFT, buff=0.14) + details.next_to(stages, DOWN, buff=0.55).align_to(stages, LEFT) + + dcm = self.block_dcm(label="6×6 DCM", highlight=GREEN).next_to(details, RIGHT, buff=1.0) + self.fit_to_frame(VGroup(stages, arrows, details, dcm), max_width=SAFE_WIDTH, max_height=5.4) + self.camera_fit(header, stages, arrows, details, dcm) + + self.play(FadeIn(header), LaggedStart(*[FadeIn(s, shift=0.2 * UP) for s in stages], lag_ratio=0.1), run_time=1.0) + self.play(LaggedStart(*[GrowArrow(a) for a in arrows], lag_ratio=0.15), run_time=0.6) + self.play(FadeIn(details, shift=0.25 * UP), run_time=0.75) + self.play(FadeIn(dcm, scale=0.92), run_time=0.65) + self.wait(5.0) + self.play(FadeOut(VGroup(header, stages, arrows, details, dcm)), run_time=0.55) + + def time_scale_conversion(self) -> None: + self.next_section("time scale conversion") + header = self.section_header("5. SOFA models need the right time argument") + + card_epoch = self.id_card( + title="incoming epoch", + value="2024-02-29T05:17:31 TCL", + subtitle="lunar coordinate time scale", + color=PURPLE, + large=True, + ) + convert = self.pipeline_box("convert to TT", YELLOW) + sofa = self.pipeline_box("SOFA prec. / nutation", BLUE, width=3.05) + matrix = self.block_dcm(label="R(TT), Ṙ(TT)", highlight=BLUE) + + group = VGroup(card_epoch, convert, sofa, matrix).arrange(RIGHT, buff=0.45).move_to(0.15 * UP) + arrows = VGroup(*[self.arrow_between(group[i], group[i + 1]) for i in range(3)]) + + code = self.code_panel( + [ + "let eval_epoch = frame.frozen_epoch.unwrap_or(epoch);", + "let tt = eval_epoch.to_time_scale(TimeScale::TT);", + "let r = sofa_precession_nutation(tt, model);", + ], + font_size=22, + highlight_lines={0: PURPLE, 1: YELLOW, 2: BLUE}, + ).next_to(group, DOWN, buff=0.5) + self.fit_to_frame(VGroup(group, arrows, code), max_width=SAFE_WIDTH, max_height=5.4) + self.camera_fit(header, group, arrows, code) + + self.play(FadeIn(header), FadeIn(card_epoch, shift=0.25 * UP), run_time=0.7) + self.play(GrowArrow(arrows[0]), FadeIn(convert), run_time=0.45) + self.play(GrowArrow(arrows[1]), FadeIn(sofa), run_time=0.45) + self.play(GrowArrow(arrows[2]), FadeIn(matrix), run_time=0.55) + self.play(FadeIn(code, shift=0.25 * UP), run_time=0.75) + self.wait(5.0) + self.play(FadeOut(VGroup(header, group, arrows, code)), run_time=0.55) + + def frozen_and_force_inertial(self) -> None: + self.next_section("frozen and forced inertial") + header = self.section_header("6. Frame fields decide whether the DCM evolves") + + live = self.block_dcm(label="dynamic", highlight=GREEN).scale(0.96) + frozen = self.block_dcm(label="frozen_epoch", highlight=YELLOW).scale(0.96) + forced = self.block_dcm(label="force_inertial", highlight=RED).scale(0.96) + cards = VGroup(live, frozen, forced).arrange(RIGHT, buff=1.0).move_to(0.25 * UP) + + live_note = self.caption_badge( + "default", + "R and Ṙ evolve\nwith the epoch", + GREEN, + ).next_to(live, DOWN, buff=0.28) + frozen_note = self.caption_badge( + "frozen", + "evaluate at\nfrozen_epoch", + YELLOW, + ).next_to(frozen, DOWN, buff=0.28) + forced_note = self.caption_badge( + "inertialized", + "zero derivative\nblock: Ṙ = 0", + RED, + ).next_to(forced, DOWN, buff=0.28) + notes = VGroup(live_note, frozen_note, forced_note) + + section = VGroup(cards, notes) + self.fit_to_frame(section, max_width=SAFE_WIDTH, max_height=4.7) + self.camera_fit(header, section) + + self.play(FadeIn(header), FadeIn(live, shift=0.25 * UP), FadeIn(live_note), run_time=0.75) + self.play(FadeIn(frozen, shift=0.25 * UP), FadeIn(frozen_note), run_time=0.65) + self.play(Indicate(frozen, color=YELLOW, scale_factor=1.04), run_time=0.6) + self.play(FadeIn(forced, shift=0.25 * UP), FadeIn(forced_note), run_time=0.65) + zero_box = SurroundingRectangle(self.dcm_rdot_cell(forced), color=RED, buff=0.05, corner_radius=0.05) + self.play(Create(zero_box), run_time=0.35) + self.play(Indicate(self.dcm_rdot_cell(forced), color=RED, scale_factor=1.15), run_time=0.55) + self.wait(5.0) + self.play(FadeOut(VGroup(header, section, zero_box)), run_time=0.55) + + def end_card(self) -> None: + self.next_section("end") + title = Text("ANISE 0.10", font=FONT, font_size=62, weight=BOLD, color=FG) + subtitle = Text( + "Static IDs, dynamic frames, one rotation API.", + font=FONT, + font_size=31, + color=MUTED, + ) + bits = self.code_chip("0xA0 FF AA BB → DCM(epoch)", color=CYAN, font_size=34) + code = self.code_panel( + [ + "pip install --upgrade anise", + "cargo add anise", + "https://nyxspace.com" + ], + highlight_lines={0: GREEN, 1: ORANGE, 2: BLUE}, + ).scale(1.0).move_to(ORIGIN + 0.25 * UP) + + stack = VGroup(title, subtitle, bits, code).arrange(DOWN, buff=0.32) + self.camera_fit(stack) + self.play(FadeIn(stack, shift=0.25 * UP), run_time=0.85) + self.wait(5) + self.play(FadeOut(stack), run_time=0.6) + + # ------------------------------------------------------------------ + # Reusable visual primitives + # ------------------------------------------------------------------ + def section_header(self, text: str) -> VGroup: + label = Text(text, font=FONT, font_size=34, weight=BOLD, color=FG) + line = Line(LEFT, RIGHT, color=GRAY, stroke_width=2).set_width(11.6) + group = VGroup(label, line).arrange(DOWN, aligned_edge=LEFT, buff=0.16) + group.to_edge(UP, buff=0.35).to_edge(LEFT, buff=0.55) + return group + + def id_card(self, title: str, value: str, subtitle: str, color: str, large: bool=False) -> VGroup: + rect = RoundedRectangle( + width=7.5 if large else 3.25, + height=1.65, + corner_radius=0.13, + stroke_color=color, + stroke_width=2, + fill_color=PANEL, + fill_opacity=0.95, + ) + t = Text(title, font=FONT, font_size=20, color=MUTED) + v = Text(value, font=MONO, font_size=34, color=color, weight=BOLD) + s = Text(subtitle, font=FONT, font_size=18, color=FG) + stack = VGroup(t, v, s).arrange(DOWN, buff=0.12).move_to(rect.get_center()) + return VGroup(rect, stack) + + def small_frame_card(self, title: str, value: str, color: str, large:bool=False) -> VGroup: + rect = RoundedRectangle( + width=5.5 if large else 3.25, + height=1.0, + corner_radius=0.11, + stroke_color=color, + stroke_width=1.6, + fill_color=PANEL, + fill_opacity=0.92, + ) + t = Text(title, font=FONT, font_size=18, color=MUTED) + v = Text(value, font=MONO, font_size=21, color=FG) + stack = VGroup(t, v).arrange(DOWN, buff=0.08).move_to(rect.get_center()) + return VGroup(rect, stack) + + def byte_cell(self, spec: ByteSpec) -> VGroup: + rect = RoundedRectangle( + width=2.35, + height=1.85, + corner_radius=0.12, + stroke_color=spec.color, + stroke_width=2, + fill_color=PANEL, + fill_opacity=0.94, + ) + value = Text(spec.value, font=MONO, font_size=32, color=spec.color, weight=BOLD) + name = Text(spec.name, font=FONT, font_size=18, color=MUTED) + meaning = Text(spec.meaning, font=FONT, font_size=16, color=FG, line_spacing=0.9) + meaning.set_width(2.05) + stack = VGroup(value, name, meaning).arrange(DOWN, buff=0.12).move_to(rect.get_center()) + return VGroup(rect, stack) + + def pipeline_box(self, text: str, color: str, width: float = 2.55) -> VGroup: + rect = RoundedRectangle( + width=width, + height=0.95, + corner_radius=0.12, + stroke_color=color, + stroke_width=1.8, + fill_color=PANEL, + fill_opacity=0.94, + ) + label = Text(text, font=FONT, font_size=21, color=FG) + label.set_width(width - 0.35) if label.width > width - 0.35 else None + label.move_to(rect) + return VGroup(rect, label) + + def code_panel( + self, + lines: list[str], + font_size: int = 25, + highlight_lines: dict[int, str] | None = None, + ) -> VGroup: + highlight_lines = highlight_lines or {} + text_lines = VGroup() + for i, line in enumerate(lines): + color = highlight_lines.get(i, FG) + text_lines.add(Text(line, font=MONO, font_size=font_size, color=color)) + text_lines.arrange(DOWN, aligned_edge=LEFT, buff=0.10) + bg = RoundedRectangle( + width=max(text_lines.width + 0.62, 6.0), + height=text_lines.height + 0.45, + corner_radius=0.12, + stroke_color=GRAY, + stroke_width=1.2, + fill_color=PANEL, + fill_opacity=0.96, + ) + text_lines.move_to(bg.get_center()).align_to(bg, LEFT).shift(0.31 * RIGHT) + return VGroup(bg, text_lines) + + def code_chip(self, text: str, color: str, font_size: int = 28) -> VGroup: + label = Text(text, font=MONO, font_size=font_size, color=color) + rect = RoundedRectangle( + width=label.width + 0.62, + height=label.height + 0.32, + corner_radius=0.14, + stroke_color=color, + stroke_width=1.6, + fill_color=PANEL, + fill_opacity=0.95, + ) + label.move_to(rect) + return VGroup(rect, label) + + def caption_badge(self, title: str, body: str, color: str) -> VGroup: + rect = RoundedRectangle( + width=2.55, + height=1.02, + corner_radius=0.11, + stroke_color=color, + stroke_width=1.3, + fill_color=PANEL, + fill_opacity=0.92, + ) + t = Text(title, font=FONT, font_size=16, color=color, weight=BOLD) + b = Text(body, font=FONT, font_size=17, color=FG, line_spacing=0.85) + stack = VGroup(t, b).arrange(DOWN, buff=0.06).move_to(rect) + return VGroup(rect, stack) + + def key_value(self, key: str, value: str, color: str) -> VGroup: + k = self.code_chip(key, color=color, font_size=20) + v = Text(value, font=FONT, font_size=22, color=FG) + arrow = Text("→", font=FONT, font_size=22, color=MUTED) + row = VGroup(k, arrow, v).arrange(RIGHT, buff=0.18) + return row + + def reset_camera_frame(self) -> None: + """Restore the normal 16:9 Manim camera frame.""" + self.camera.frame.set(width=config.frame_width) + self.camera.frame.move_to(ORIGIN) + + def camera_fit(self, *mobjects: Mobject, margin: float = 1.12) -> None: + """Zoom the camera frame out until all supplied mobjects are visible. + + This is more robust than scaling individual objects because it fixes the + actual thing responsible for clipping: the camera frame. + """ + group = VGroup(*mobjects) + aspect = config.frame_width / config.frame_height + required_width = max(group.width * margin, group.height * margin * aspect, config.frame_width) + self.camera.frame.set(width=required_width) + self.camera.frame.move_to(group.get_center()) + + def fit_to_frame( + self, + mob: Mobject, + max_width: float = SAFE_WIDTH, + max_height: float = SAFE_HEIGHT, + ) -> Mobject: + """Scale a layout down only if it would otherwise exceed the chosen safe area.""" + scale = min(max_width / mob.width if mob.width else 1.0, max_height / mob.height if mob.height else 1.0, 1.0) + if scale < 1.0: + mob.scale(scale) + return mob + + def dcm_rdot_cell(self, dcm: VGroup) -> Mobject: + """Return the lower-left R-dot cell in block_dcm().""" + # block_dcm -> [title, full_grid] + # full_grid -> [left bracket, grid, right bracket] + # grid -> [top row, bottom row] + # bottom row -> [Rdot cell, R cell] + return dcm[1][1][1][0] + + def arrow_between(self, left: Mobject, right: Mobject, color: str = MUTED) -> Arrow: + return Arrow( + start=left.get_right(), + end=right.get_left(), + buff=0.12, + stroke_width=3, + color=color, + max_tip_length_to_length_ratio=0.16, + ) + + def block_dcm(self, label: str, highlight: str) -> VGroup: + """Compact visual stand-in for ANISE's 6x6 DCM. + + The group layout is: [title, grid]. The grid children are cells ordered + row-major. The bottom-left cell is the Rdot block, useful for highlighting + force_inertial behavior. + """ + title = Text(label, font=FONT, font_size=22, color=FG) + cells = VGroup() + data = [ + ("R", highlight, 0.24), + ("0", MUTED, 0.08), + ("Ṙ", ORANGE, 0.22), + ("R", highlight, 0.24), + ] + for text, color, opacity in data: + rect = Square(side_length=0.72, stroke_color=color, stroke_width=1.8, fill_color=color, fill_opacity=opacity) + label_mob = Text(text, font=FONT, font_size=23, color=FG) + label_mob.move_to(rect) + cells.add(VGroup(rect, label_mob)) + grid = VGroup( + VGroup(cells[0], cells[1]).arrange(RIGHT, buff=0.06), + VGroup(cells[2], cells[3]).arrange(RIGHT, buff=0.06), + ).arrange(DOWN, buff=0.06) + bracket_l = Line(UP, DOWN, color=MUTED, stroke_width=2).set_height(grid.height + 0.15).next_to(grid, LEFT, buff=0.08) + bracket_r = Line(UP, DOWN, color=MUTED, stroke_width=2).set_height(grid.height + 0.15).next_to(grid, RIGHT, buff=0.08) + full_grid = VGroup(bracket_l, grid, bracket_r) + group = VGroup(title, full_grid).arrange(DOWN, buff=0.18) + return group + + +# Optional: a shorter social-clip version that cuts the standard-ID intro. +class Anise010DynamicFrameShort(Anise010DynamicFrameAnnouncement): + def construct(self) -> None: + self.camera.background_color = BG + self.reset_camera_frame() + self.title_card() + self.dynamic_id_decode() + self.rotate_call() + self.dcm_pipeline() + self.frozen_and_force_inertial() + self.end_card() diff --git a/nyx-animations/main.py b/nyx-animations/main.py new file mode 100644 index 0000000..e35f357 --- /dev/null +++ b/nyx-animations/main.py @@ -0,0 +1,80 @@ + + +from manim import * +import numpy as np + +class FrozenFrameKinematics(ThreeDScene): + def construct(self): + # 1. Setup the Camera and ICRF (The Absolute Anchor) + self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + + icrf_axes = ThreeDAxes(x_range=[-3, 3], y_range=[-3, 3], z_range=[-3, 3]) + icrf_labels = icrf_axes.get_axis_labels( + Text("X_icrf").scale(0.5), + Text("Y_icrf").scale(0.5), + Text("Z_icrf").scale(0.5) + ) + self.add(icrf_axes, icrf_labels) + + # Title Overlay + title = Text("ANISE v0.10: Dynamic & Frozen Frames", font_size=36) + title.to_corner(UL) + self.add_fixed_in_frame_mobjects(title) + + # --- THE NEW ADDITION: SOFA/IAU Telemetry HUD --- + telemetry_text = ( + "Engine: SOFA\n" + "Precession-Nutation: IAU 2006A / 2000A\n" + "Base Frame: ICRS" + ) + telemetry = Text(telemetry_text, font_size=18, color=LIGHT_GRAY, line_spacing=1.5) + telemetry.to_corner(DL) # Bottom Left + self.add_fixed_in_frame_mobjects(telemetry) + # ------------------------------------------------ + + # 2. The Dynamic Frame (Earth True of Date) + tod_axes = ThreeDAxes( + x_range=[-2.5, 2.5], y_range=[-2.5, 2.5], z_range=[-2.5, 2.5], + axis_config={"color": YELLOW} + ) + + time_tracker = ValueTracker(0) + + def update_tod(axes): + t = time_tracker.get_value() + precession = t * 0.5 + nutation = np.sin(t * 2) * 0.2 + + axes.become(ThreeDAxes( + x_range=[-2.5, 2.5], y_range=[-2.5, 2.5], z_range=[-2.5, 2.5], + axis_config={"color": YELLOW} + )) + axes.rotate(precession, axis=OUT) + axes.rotate(nutation, axis=RIGHT) + + tod_axes.add_updater(update_tod) + self.add(tod_axes) + + tod_label = Text("Evaluating: Earth TOD", color=YELLOW, font_size=24) + tod_label.to_corner(UR) # Top Right + self.add_fixed_in_frame_mobjects(tod_label) + + # Let the dynamic frame drift + self.play(time_tracker.animate.set_value(4), run_time=4, rate_func=linear) + + # 3. The Freeze Event (PR #697) + frozen_axes = tod_axes.copy() + frozen_axes.clear_updaters() + frozen_axes.set_color(BLUE) + frozen_axes.set_opacity(0.6) + + flash = Flash(ORIGIN, color=WHITE, line_length=1, num_lines=12) + self.play(FadeIn(frozen_axes), flash, run_time=0.5) + + frozen_label = Text("Instantiated: Explicitly Frozen TOD @ Epoch", color=BLUE, font_size=24) + frozen_label.next_to(tod_label, DOWN, aligned_edge=RIGHT) + self.add_fixed_in_frame_mobjects(frozen_label) + + # 4. Divergence + self.play(time_tracker.animate.set_value(10), run_time=6, rate_func=linear) + self.wait(2) diff --git a/nyx-animations/manim.cfg b/nyx-animations/manim.cfg new file mode 100644 index 0000000..a2201e3 --- /dev/null +++ b/nyx-animations/manim.cfg @@ -0,0 +1,8 @@ +[CLI] +frame_rate = 30 +pixel_height = 1920 +pixel_width = 1080 +background_color = BLACK +background_opacity = 1 +scene_names = Default + diff --git a/nyx-animations/opt2.py b/nyx-animations/opt2.py new file mode 100644 index 0000000..ffb420a --- /dev/null +++ b/nyx-animations/opt2.py @@ -0,0 +1,98 @@ +from manim import * + +class DynamicFrameParsing(Scene): + def construct(self): + # Step 1: The ID Contrast + title = Text("ANISE Frame Resolution Decoder").to_edge(UP) + self.play(Write(title)) + + legacy_id = Text("Legacy Frame ID: 399 (Earth IAU)", color=GRAY) + dynamic_id_str = "0xA0 | 01 | 01 | 018F" + dynamic_id = Text(f"Dynamic ID: {dynamic_id_str}", color=YELLOW) + + id_group = VGroup(legacy_id, dynamic_id).arrange(DOWN, center=False, aligned_edge=LEFT).move_to(UP * 2) + + self.play(FadeIn(legacy_id)) + self.wait(1) + self.play(Write(dynamic_id)) + + # Break down the dynamic ID + marker_brace = Brace(dynamic_id[12:16], DOWN) + marker_text = marker_brace.get_text("Marker") + + flags_brace = Brace(dynamic_id[19:26], DOWN) + flags_text = flags_brace.get_text("Flags (Inertial/Frozen)") + + body_brace = Brace(dynamic_id[29:], DOWN) + body_text = body_brace.get_text("Target (399)") + + self.play( + GrowFromCenter(marker_brace), FadeIn(marker_text), + GrowFromCenter(flags_brace), FadeIn(flags_text), + GrowFromCenter(body_brace), FadeIn(body_text) + ) + self.wait(1) + + # Step 2: The API Call + self.play(FadeOut(legacy_id), FadeOut(marker_brace), FadeOut(marker_text), FadeOut(flags_brace), FadeOut(flags_text), FadeOut(body_brace), FadeOut(body_text)) + self.play(dynamic_id.animate.scale(0.7).to_corner(UL)) + + api_call = Code( + code_string="almanac.rotate(target=399, observer=ICRF, epoch=tcl_epoch)", + language="rust", + ).next_to(dynamic_id, DOWN, aligned_edge=LEFT) + + self.play(FadeIn(api_call)) + self.wait(1) + + # Step 3: Time Scale Conversion (TCL -> TT) for SOFA + time_conversion = MathTex( + r"t_{TT} = t_{TCL} + \Delta T_{Relativistic}", + color=BLUE + ).next_to(api_call, DOWN, buff=1) + + time_label = Text("SOFA Requirement: Transform to TT", color=BLUE).next_to(time_conversion, UP) + + self.play(FadeIn(time_label), Write(time_conversion)) + self.wait(1) + + # Step 4: The DCM Evaluation + dcm_math = MathTex( + r"R(t_{TT})", r"=", r"\text{SOFA}(t_{TT})", + + ).next_to(time_conversion, DOWN, buff=0.5) + + dcm_deriv = MathTex( + r"\dot{R}(t_{TT})", r"=", r"\frac{d}{dt}\text{SOFA}(t_{TT})", + + ).next_to(dcm_math, DOWN) + + self.play(Write(dcm_math), Write(dcm_deriv)) + self.wait(1) + + # Step 5: Applying the parsed flags + + # Action 1: Force Inertial + inertial_flag_text = Text("Flag: force_inertial == true", color=RED).to_edge(RIGHT).shift(UP*1) + self.play(Write(inertial_flag_text)) + + # Cross out the derivative and set to 0 + cross = Cross(dcm_deriv[2]) + zero_val = MathTex("0", color=RED).move_to(dcm_deriv[2]) + + self.play(Create(cross)) + self.play(Transform(dcm_deriv[2], zero_val), FadeOut(cross)) + self.wait(1) + + # Action 2: Frozen Epoch + frozen_flag_text = Text("Flag: frozen_epoch == t_0", color=TEAL).next_to(inertial_flag_text, DOWN) + self.play(Write(frozen_flag_text)) + + # Transform t_TT to t_0 in the rotation matrix + frozen_math = MathTex( + r"R(t_0)", r"=", r"\text{SOFA}(t_0)", + color=TEAL + ).move_to(dcm_math) + + self.play(Transform(dcm_math, frozen_math)) + self.wait(2)