-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscript.js
More file actions
252 lines (225 loc) · 23.4 KB
/
Copy pathscript.js
File metadata and controls
252 lines (225 loc) · 23.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
const KO_TO_EN = new Map([
["본문으로 이동", "Skip to main content"],
["개요", "Overview"],
["시스템", "System"],
["항법", "Navigation"],
["제어", "Control"],
["검증", "Verification"],
["STM32 기반 비행제어기와 항법·제어 소프트웨어", "STM32-Based Flight Controller and Navigation & Control Software"],
["STRIX FC는 자체 제작한 flight controller 보드에서 센서 수집, GNSS/INS/Baro 항법 추정, 자세·속도·위치·고도 제어, 모터 PWM 출력, RF 명령/텔레메트리, SD 로그를 하나의 펌웨어로 구성한 프로젝트다.", "STRIX FC integrates sensor acquisition, GNSS/INS/Baro navigation estimation, attitude, velocity, position and altitude control, motor PWM output, RF commands and telemetry, and SD logging into a single firmware stack running on a custom flight controller board."],
["STM32F405 기반 FC 보드와 센서/통신 모듈 구성.", "STM32F405-based FC board with sensor and communication modules."],
["프로젝트 핵심 구성", "Core project components"],
["비행제어기 보드 사진", "Flight controller board photo"],
["프로젝트 목적", "Project Objective"],
["완제품 flight controller를 사용하는 대신, MCU 펌웨어에서 비행에 필요한 주요 경로를 직접 구현하고 로그와 지상국으로 검증 가능한 형태로 만드는 것이 목표다.", "The goal is to implement the core flight paths directly in MCU firmware instead of relying on a commercial flight controller, and to make their behavior verifiable through logs and the ground control system."],
["프로젝트 대상 기체.", "Project airframe."],
["프로젝트 대상 VTOL 기체", "Project VTOL airframe"],
["대상 플랫폼", "Target Platform"],
["STRIX FC는 이 기체에서 센서 수집, 항법 추정, 자세·속도·위치·고도 제어, 모터 출력, 무선 통신과 로그 기록을 담당하도록 구성했다.", "STRIX FC is responsible for sensor acquisition, navigation estimation, attitude, velocity, position and altitude control, motor output, wireless communication, and logging on this airframe."],
["비행제어기 펌웨어", "Flight Controller Firmware"],
["STM32F405에서 IMU, GNSS, 기압계, RF, SD, 모터 PWM을 초기화하고 FreeRTOS 태스크로 분리해 실행한다.", "The firmware initializes the IMU, GNSS, barometer, RF, SD, and motor PWM on the STM32F405 and runs each function in dedicated FreeRTOS tasks."],
["항법 상태 추정", "Navigation State Estimation"],
["IMU 기반 예측, GNSS 보정, 기압계 고도 보정, 초기 정지 정렬, GNSS 기준점 확정, 정지 상태 보정을 사용해 NED 기준 위치와 속도를 추정한다.", "NED-frame position and velocity are estimated using IMU propagation, GNSS updates, barometric altitude updates, initial stationary alignment, GNSS origin initialization, and stationary-state corrections."],
["제어 루프", "Control Loops"],
["quaternion 기반 자세 제어, body-rate PD 제어, quad X mixer, 모터 PWM 출력을 연결한다. Hover/PositionHold/AltHold 모드에서는 EKF 상태 기반 외부 루프도 사용한다.", "Quaternion-based attitude control, body-rate PD control, a quad-X mixer, and motor PWM output form the inner control path. Hover, PositionHold, and AltHold also use EKF-state-based outer loops."],
["검증 도구", "Verification Tools"],
["NRF24 텔레메트리, SD 로그, GroundControlSystem 대시보드, 시연 영상을 통해 항법과 제어 상태를 확인한다.", "Navigation and control states are verified using NRF24 telemetry, SD logs, the GroundControlSystem dashboard, and demonstration videos."],
["현재 검증 초점", "Current Verification Focus"],
["자세 제어와 데이터 수집 경로를 기반으로, GNSS/INS/Baro EKF의 수평 속도 안정성, 위치·고도 유지, hover 진입 조건을 점검 중이다.", "Current work evaluates GNSS/INS/Baro EKF horizontal-velocity stability, position and altitude hold, and hover-entry conditions on top of the validated attitude-control and data-acquisition paths."],
["설명 기준", "Scope of This Page"],
["주요 설명은 현재 펌웨어에서 동작하는 항법, 제어, 통신, 기록 흐름을 기준으로 정리했다.", "The descriptions reflect the navigation, control, communication, and logging paths implemented in the current firmware."],
["시스템 구성", "System Architecture"],
["펌웨어는 STM32CubeIDE 프로젝트를 기반으로 하며, 주변장치 초기화는 CubeMX 설정과 HAL 드라이버를 사용한다. 비행 관련 기능은 센서 수집, 항법 추정, 제어, 통신, 기록 작업으로 나누어 실행된다.", "The firmware is built as an STM32CubeIDE project, with peripheral initialization generated from CubeMX configuration and implemented through HAL drivers. Flight functions run as separate sensor acquisition, navigation estimation, control, communication, and logging tasks."],
["하드웨어와 펌웨어 구성", "Hardware and firmware configuration"],
["영역", "Area"],
["구성", "Interface / Component"],
["역할", "Role"],
["168 MHz Cortex-M4F, FreeRTOS 기반 태스크 실행", "168 MHz Cortex-M4F running FreeRTOS-based tasks"],
["quaternion, gyro, accel 수집 및 내부 제어 루프 입력", "Acquires quaternion, gyro, and acceleration data for the inner control loop"],
["위도/경도/고도, NED 속도, 공분산을 EKF 보정에 사용", "Provides latitude, longitude, altitude, NED velocity, and covariance for EKF updates"],
["상대 고도 측정, bias 보정, Baro-EKF 수직 보정, 고도 유지 조건 판단", "Measures relative altitude, corrects bias, updates the Baro-EKF vertical state, and supports altitude-hold gating"],
["명령 수신, IMU/GNSS/status/EKF 텔레메트리 송신", "Receives commands and transmits IMU, GNSS, status, and EKF telemetry"],
["쿼드 X 모터 혼합 결과를 ESC PWM으로 출력", "Outputs quad-X motor mixing results as ESC PWM signals"],
["TIM1 PWM 4채널", "TIM1 PWM, 4 channels"],
["센서, 항법, GNSS/Baro 보정, ZUPT, 위치·고도 제어 진단값을 25 Hz 기준으로 저장", "Stores sensor, navigation, GNSS/Baro update, ZUPT, and position/altitude-control diagnostics at 25 Hz"],
["상태 관리", "State Management"],
["센서 상태, RF 상태, 항법 상태, 제어 명령, 모터 출력을 하나의 실행 흐름 안에서 연결한다.", "Connects sensor state, RF state, navigation state, control commands, and motor output in one execution flow."],
["항법 처리", "Navigation Processing"],
["IMU 예측, GNSS 보정, 기압계 수직 보정, 정지 상태 보정, 초기 정렬, 기준점 설정을 단계적으로 수행한다.", "Performs IMU propagation, GNSS updates, barometric vertical updates, stationary-state corrections, initial alignment, and origin initialization in sequence."],
["제어 처리", "Control Processing"],
["자세 목표, 각속도 제어, 속도·위치·고도 기반 보정, 모터 혼합, PWM 출력을 순서대로 연결한다.", "Connects attitude targets, angular-rate control, velocity, position and altitude corrections, motor mixing, and PWM output in sequence."],
["주요 처리 영역", "Main processing areas"],
["GNSS/INS 항법 구조", "GNSS/INS Navigation Architecture"],
["항법 상태는 NED 좌표계 기준 위치, 속도, quaternion, 가속도/자이로 bias를 포함한다. 명목 상태는 16차원, 오차 상태 공분산은 15차원으로 관리하며 GNSS, 기압계, ZUPT 보정을 함께 적용한다.", "The navigation state contains NED-frame position and velocity, quaternion attitude, and accelerometer and gyro biases. It uses a 16-state nominal model with a 15-state error covariance and combines GNSS, barometer, and ZUPT updates."],
["입력", "Input"],
["센서 입력 정리", "Sensor Input Preparation"],
["IMU는 20 ms, GNSS는 500 ms 안에 갱신된 데이터를 유효한 입력으로 판단한다. GNSS 위치·속도·공분산은 EKF 입력 단위로 변환된다.", "IMU data updated within 20 ms and GNSS data updated within 500 ms are treated as valid inputs. GNSS position, velocity, and covariance are converted into EKF input units."],
["정렬", "Alignment"],
["초기 정지 정렬", "Initial Stationary Alignment"],
["초기 IMU 샘플로 자이로 바이어스와 기준 자세를 잡고, GNSS 기준점은 대기 시간과 평균 조건을 만족한 뒤 확정한다.", "Initial IMU samples establish gyro bias and reference attitude. The GNSS origin is fixed after the required wait time and averaging conditions are satisfied."],
["예측", "Propagation"],
["IMU 기반 상태 예측", "IMU-Based State Propagation"],
["새 IMU 샘플이 들어오면 관성항법 계산으로 위치와 속도를 예측하고, 모터 상태에 따라 잡음 수준을 조정한다.", "Each new IMU sample propagates position and velocity through inertial navigation, with process-noise levels adjusted according to motor state."],
["보정", "Update"],
["GNSS / 정지 상태 보정", "GNSS / Stationary-State Updates"],
["GNSS 위치·속도 보정과 정지 상태 보정을 분리 적용한다. 정지 또는 지상 모터 조건에서는 수평/수직 속도 보정을 보수적으로 다룬다.", "GNSS position and velocity updates are applied separately from stationary-state corrections. Horizontal and vertical velocity updates are handled conservatively while stationary or under ground-motor conditions."],
["고도", "Altitude"],
["Baro-EKF 수직 보정", "Baro-EKF Vertical Update"],
["BMP390L 상대 고도를 EKF의 Down 축 관측값으로 사용한다. PWM, 고도 변화, innovation 크기에 따라 측정 잡음을 조정하고 gate를 통과한 값만 반영한다.", "BMP390L relative altitude is used as a Down-axis EKF observation. Measurement noise is adjusted using PWM, altitude change, and innovation magnitude, and only measurements that pass the gate are applied."],
["GNSS/INS EKF 항법 처리 흐름도.", "GNSS/INS EKF navigation processing flowchart."],
["GNSS/INS 항법 구조 흐름도", "GNSS/INS navigation architecture flowchart"],
["NED 기준 항법 좌표계와 기체 좌표계 정리.", "Relationship between the NED navigation frame and body frame."],
["NED 좌표계와 기체 좌표계 관계", "Relationship between the NED frame and body frame"],
["출력 게이트", "Output Gate"],
["EKF가 초기화되었다는 사실만으로 상태를 공개하지 않는다. 초기 정렬과 GNSS 기준점 설정이 끝나 항법 상태가 충분히 준비된 뒤 RF EKF telemetry와 제어 입력에 사용한다.", "EKF initialization alone does not make its state available. RF EKF telemetry and control inputs are enabled only after initial alignment and GNSS-origin initialization are complete and the navigation state is ready."],
["GNSS 기준점: 정지 조건, 샘플 수, 수평 정확도, 위치 분산, 속도 RMS 조건 확인", "GNSS origin: checks stationary conditions, sample count, horizontal accuracy, position variance, and velocity RMS"],
["EKF 상태: N/E/D 위치와 속도, 자세, 바이어스", "EKF state: N/E/D position and velocity, attitude, and biases"],
["진단 값: GNSS 보정 잔차, Baro innovation/test ratio, ZUPT 적용 여부, 보정 전후 속도 변화", "Diagnostics: GNSS update residuals, Baro innovation/test ratio, ZUPT application, and velocity changes before and after updates"],
["제어 및 모터 출력", "Control and Motor Output"],
["제어기는 내부 자세/각속도 루프와 hover 외부 루프로 나뉜다. 실제 PWM 출력 전에는 시동 상태, 비행 모드, 항법 준비 상태, 기압계 준비 상태, 모터 출력 허용 조건을 다시 확인한다.", "The controller consists of inner attitude and angular-rate loops and hover outer loops. Before PWM is applied, it rechecks arming state, flight mode, navigation readiness, barometer readiness, and motor-output permission."],
["제어 경로: EKF/명령 입력에서 roll·pitch 목표, 각속도 제어, 모터 혼합, PWM 출력으로 이어진다.", "Control path from EKF and command inputs through roll/pitch targets, angular-rate control, motor mixing, and PWM output."],
["STRIX 제어기 흐름도", "STRIX controller flowchart"],
["내부 자세 제어", "Inner Attitude Control"],
["IMU에서 받은 자세와 각속도 데이터를 기준으로 roll/pitch 목표 각속도를 만든다. yaw는 별도 주기로 목표 각속도를 유지해 자세 제어 흐름에 반영한다.", "Roll and pitch angular-rate targets are generated from IMU attitude and angular-rate data. Yaw maintains a target rate on a separate cycle and feeds it into the attitude-control path."],
["각속도 제어와 모터 혼합", "Angular-Rate Control and Motor Mixing"],
["자이로 값을 필터링한 뒤 각속도 제어를 수행하고, 쿼드 X 모터 혼합으로 4개 모터 명령을 만든다. 출력은 정규화 명령에서 ESC PWM us로 변환된다.", "Filtered gyro values drive angular-rate control, and a quad-X mixer produces four motor commands. Normalized output commands are converted to ESC PWM in microseconds."],
["외부 속도 제어", "Outer Velocity Control"],
["Hover 모드에서 EKF 수평 속도를 읽고 목표 속도 0 m/s 기준의 roll/pitch 목표를 생성한다. 속도 입력과 미분항은 저역통과 필터를 거치고, 가속도와 기울기 명령은 각각 제한한다.", "In Hover mode, EKF horizontal velocity is used to generate roll and pitch targets around a 0 m/s velocity command. Velocity inputs and derivative terms are low-pass filtered, while acceleration and tilt commands are independently limited."],
["위치·고도 유지", "Position and Altitude Hold"],
["PositionHold는 현재 N/E 위치를 latch한 뒤 위치 오차를 속도 목표로 변환한다. AltHold와 Land는 EKF 고도·수직 속도, 기압계 준비 상태, 자세 한계를 확인한 뒤 PWM 보정량을 계산한다.", "PositionHold latches the current N/E position and converts position error into a velocity target. AltHold and Land calculate PWM corrections after checking EKF altitude and vertical velocity, barometer readiness, and attitude limits."],
["FreeRTOS 태스크 설계", "FreeRTOS Task Design"],
["초기화 태스크가 센서와 모터, RF, SD를 준비한 뒤 주요 실행 작업을 시작한다. IMU와 GNSS는 interrupt/DMA 수신 이벤트에 맞춰 처리하고, 주기 작업은 각 태스크의 실행 주기에 맞춰 반복된다.", "After the initialization task prepares the sensors, motors, RF, and SD interface, it starts the main runtime tasks. IMU and GNSS data are processed on interrupt/DMA receive events, while periodic work runs at each task's configured interval."],
["높은 우선순위", "High Priority"],
["자세 제어 태스크", "Attitude Control Task"],
["IMU 수신마다 자세/각속도 제어 계산과 모터 PWM 출력을 수행한다.", "Runs attitude and angular-rate control calculations and motor PWM output on each IMU update."],
["항법 태스크", "Navigation Task"],
["IMU 예측, GNSS 보정, 정지 상태 보정, 항법 상태 전송을 담당한다.", "Handles IMU propagation, GNSS updates, stationary-state corrections, and navigation-state transmission."],
["중간 우선순위", "Medium Priority"],
["외부 제어 태스크", "Outer Control Task"],
["Hover 속도 제어, PositionHold 위치 목표 변환, AltHold/Land 고도 보정을 주기 실행하고 내부 루프에 목표값을 전달한다.", "Periodically runs Hover velocity control, PositionHold position-to-velocity conversion, and AltHold/Land altitude correction, then passes targets to the inner loop."],
["RF 통신 태스크", "RF Communication Task"],
["RF 명령 수신을 먼저 처리하고 시스템 상태, GNSS, EKF, IMU 정보를 우선순위대로 전송한다.", "Processes incoming RF commands first, then transmits system status, GNSS, EKF, and IMU data according to priority."],
["일반 우선순위", "Normal Priority"],
["GNSS 처리 태스크", "GNSS Processing Task"],
["USART3 DMA로 받은 GNSS 패킷을 해석하고 위치, 속도, 정확도 상태를 갱신한다.", "Parses GNSS packets received through USART3 DMA and updates position, velocity, and accuracy states."],
["낮은 우선순위", "Low Priority"],
["기압 / 로그 태스크", "Barometer / Logging Tasks"],
["기압계는 25 Hz로 읽어 bias/상대고도 상태를 갱신하고, SD 기록은 센서, 항법, 제어 상태를 25 Hz 기준으로 저장한다.", "The barometer is sampled at 25 Hz to update bias and relative-altitude states, while SD logging stores sensor, navigation, and control states at 25 Hz."],
["통신, 로그, 지상국", "Communication, Logging, and Ground Control"],
["RF 패킷은 32바이트 고정 크기이며, 명령과 텔레메트리를 packet type으로 구분한다. GroundControlSystem은 FC 상태 확인, 명령 송신, 로그 기반 분석을 위한 보조 도구다.", "RF packets use a fixed 32-byte format, with packet types distinguishing commands from telemetry. GroundControlSystem supports FC state monitoring, command transmission, and log-based analysis."],
["GroundControlSystem 대시보드: attitude, EKF trajectory, GPS, covariance, command 상태 확인.", "GroundControlSystem dashboard for attitude, EKF trajectory, GPS, covariance, and command-state monitoring."],
["Heartbeat, Arm/Disarm, Hover, AltHold, PositionHold, Land, attitude trim, Emergency Hover/Land/Disarm, MotorTest, base throttle 명령을 수신한다.", "Receives Heartbeat, Arm/Disarm, Hover, AltHold, PositionHold, Land, attitude trim, Emergency Hover/Land/Disarm, MotorTest, and base-throttle commands."],
["IMU 20 Hz, GNSS 2 Hz, system status 5 Hz, EKF state 5 Hz, EKF covariance 2 Hz 기준으로 전송하고 EKF 고도를 RF altitude 값으로 공유한다.", "Transmits IMU at 20 Hz, GNSS at 2 Hz, system status at 5 Hz, EKF state at 5 Hz, and EKF covariance at 2 Hz, with EKF altitude shared as the RF altitude value."],
["센서 상태, GNSS 정확도, 모터 출력, GNSS/Baro/ZUPT 보정 잔차, 속도·위치·고도 제어 진단값을 SD에 저장해 사후 분석에 사용한다.", "Stores sensor state, GNSS accuracy, motor output, GNSS/Baro/ZUPT update residuals, and velocity, position and altitude-control diagnostics on SD for post-flight analysis."],
["검증 자료", "Verification Results"],
["자세 추정, 제어 응답, EKF 대시보드, hovering 영상, GCS 화면을 함께 배치해 어떤 기능을 검증했는지 확인할 수 있게 했다.", "Attitude-estimation, control-response, EKF-dashboard, hovering, and GCS material are presented together to show which functions have been verified."],
["검증 항목", "Verification Items"],
["IMU 좌표 변환과 자세 추정", "IMU frame transformation and attitude estimation"],
["GNSS 기준점 설정 후 NED 궤적과 EKF 상태 전송", "NED trajectory and EKF-state transmission after GNSS-origin initialization"],
["GNSS/Baro/ZUPT 보정 게이트와 EKF 수직·수평 속도 안정성", "GNSS/Baro/ZUPT update gates and EKF vertical/horizontal velocity stability"],
["자세 제어 응답, hover 외부 루프, 모터 출력 제한 상태", "Attitude-control response, hover outer loop, and motor-output limiting"],
["RF 명령 수신, 텔레메트리 전송, SD 로그 기반 진단 상태", "RF command reception, telemetry transmission, and SD-log-based diagnostics"],
["맨 위로", "Back to top"],
]);
const EN_TO_KO = new Map(Array.from(KO_TO_EN, ([ko, en]) => [en, ko]));
const I18N_ATTRIBUTES = ["aria-label", "alt"];
function normalizeTranslationText(value) {
return value.replace(/\s+/g, " ").trim();
}
function translateTextNodes(dictionary) {
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
const nodes = [];
while (walker.nextNode()) {
nodes.push(walker.currentNode);
}
nodes.forEach((node) => {
const current = normalizeTranslationText(node.nodeValue);
const translated = dictionary.get(current);
if (translated) {
node.nodeValue = translated;
}
});
}
function translateAttributes(dictionary) {
document.querySelectorAll(I18N_ATTRIBUTES.map((name) => `[${name}]`).join(", ")).forEach((element) => {
I18N_ATTRIBUTES.forEach((name) => {
if (!element.hasAttribute(name)) {
return;
}
const current = normalizeTranslationText(element.getAttribute(name));
const translated = dictionary.get(current);
if (translated) {
element.setAttribute(name, translated);
}
});
});
}
function applyLanguage(language) {
const isEnglish = language === "en";
const dictionary = isEnglish ? KO_TO_EN : EN_TO_KO;
const description = document.querySelector('meta[name="description"]');
translateTextNodes(dictionary);
translateAttributes(dictionary);
document.documentElement.lang = language;
document.querySelector(".site-nav").setAttribute("aria-label", isEnglish ? "Page sections" : "페이지 섹션");
document.querySelector(".language-switcher").setAttribute("aria-label", isEnglish ? "Language" : "언어");
description.setAttribute(
"content",
isEnglish
? "An overview of the STRIX Flight Controller project, including its STM32-based architecture, GNSS/INS/Baro EKF, hover control, RTOS tasks, communications, and verification results."
: "STRIX Flight Controller 프로젝트의 목적, STM32 기반 비행제어기 구조, GNSS/INS/Baro EKF, hover 제어, RTOS 태스크, 통신과 검증 자료를 정리한 소개 페이지."
);
document.querySelectorAll("[data-language]").forEach((button) => {
button.setAttribute("aria-pressed", String(button.dataset.language === language));
});
}
document.querySelectorAll("[data-language]").forEach((button) => {
button.addEventListener("click", () => applyLanguage(button.dataset.language));
});
applyLanguage("en");
const navLinks = Array.from(document.querySelectorAll(".site-nav a"));
const sections = navLinks
.map((link) => document.querySelector(link.getAttribute("href")))
.filter(Boolean);
const revealTargets = document.querySelectorAll(
".section-heading, .info-card, .flow-step, .media-card, .detail-card, .task-board article, .packet-list article, .video-card, .table-card, .module-stack article, .status-strip, .verification-notes"
);
revealTargets.forEach((target) => target.classList.add("reveal"));
if ("IntersectionObserver" in window) {
const revealObserver = new IntersectionObserver(
(entries, observer) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) {
return;
}
entry.target.classList.add("is-visible");
observer.unobserve(entry.target);
});
},
{ rootMargin: "0px 0px -8% 0px", threshold: 0.12 }
);
revealTargets.forEach((target) => revealObserver.observe(target));
const navObserver = new IntersectionObserver(
(entries) => {
const visible = entries
.filter((entry) => entry.isIntersecting)
.sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
if (!visible) {
return;
}
navLinks.forEach((link) => {
link.classList.toggle("active", link.getAttribute("href") === `#${visible.target.id}`);
});
},
{ rootMargin: "-25% 0px -58% 0px", threshold: [0.15, 0.35, 0.6] }
);
sections.forEach((section) => navObserver.observe(section));
} else {
revealTargets.forEach((target) => target.classList.add("is-visible"));
}
document.addEventListener("keydown", (event) => {
if (event.key !== "Escape") {
return;
}
document.activeElement?.blur();
});