왜 만들었나
밤하늘 촬영은 달·박명·광해·은하수 고도를 따로 찾아보고 셔터·ISO를 감으로 정해야 해서 어렵습니다. 위치와 날짜, 장비 정보만 넣으면 시각별 하늘 상태로 설정값을 계산해 줍니다.
주요 기능
- 대상 6가지별 ISO·조리개·셔터 추천과 대안 설정
- NPF 규칙·500 법칙으로 별이 점으로 남는 최대 노출 계산
- 하늘 지도(파노라마/전천 돔)와 화각 사각형, 밤 고도 차트
- 휴대폰 나침반으로 방향 안내(자기 편각 보정)
- Open-Meteo 구름량 예보, 적색 모드, 오프라인 PWA
숫자로 보면
촬영 대상 6가지
밝은 별 145개 · 유성우 10개 · 은하수 영역 6곳 데이터
단위 테스트 25개
어떻게 동작하나
밤 단위 계산
computeNight()가 일몰·박명·월출/월몰 시각을 찾고, 하룻밤을 5분 간격으로 나눠 해·달·행성·은하 중심·은하수 영역의 고도/방위를 샘플링합니다.
js/astro.js어두운 시간 구간
intervals()로 해 고도 -18° 이하이면서 달빛 영향이 없는 구간을 뽑고, 천문박명이 없는 날은 -12° 기준으로 낮춥니다.
js/astro.js대상별 분석
analyzeMilkyWay()가 샘플마다 영역 가중치 × 고도 점수 × 달빛 계수를 매겨 오늘 밤 가장 좋은 은하수 영역과 시각을 고릅니다.
js/astro.js장면 밝기
sceneEV()가 보틀 등급별 어두운 하늘 EV에 달빛 EV와 박명 EV를 빛의 양(2^EV)으로 더해 목표 노출을 구합니다.
js/camera.js카메라 설정
starfieldBase()가 화소 피치로 NPF 최대 셔터를 구하고, ISO = 100·N²/(t·2^EV) 식으로 ISO를 정해 표준 단계로 반올림합니다.
js/camera.js화면 갱신
시간 막대를 움직이면 그 시각의 snapshot으로 설정·방향·하늘 지도를 다시 그립니다.
js/app.js
핵심 코드
/** NPF 규칙 (간이식): 별이 점으로 남는 최대 노출 시간 */
export function npfSeconds(aperture, pitchUm, focal) {
return (35 * aperture + 30 * pitchUm) / focal;
}
// …
function starfieldBase(ctx, { evShift = 0, maxT = 30, minT = 0 } = {}) {
const sensor = sensorOf(ctx.gear.sensor);
const pitch = pixelPitch(sensor, ctx.gear.mp);
const n = wideAperture(ctx.gear.maxAperture);
const npf = npfSeconds(n, pitch, ctx.gear.focal);
const r500 = rule500(ctx.gear.focal, sensor.crop);
const t = floorShutter(Math.max(minT, Math.min(npf, maxT)));
const ev = sceneEV(ctx);
const target = ev.value + evShift;
const rawIso = isoFor(n, t, target);
return { sensor, pitch, n, npf, r500, t, ev, target, rawIso, iso: roundISO(rawIso) };
}NPF 규칙으로 별이 점으로 남는 최대 셔터를 정하고, 장면 EV에서 필요한 ISO를 역산하는 핵심 계산입니다.
어려웠던 점
달빛·박명·광해는 밝기 단위가 EV(로그)라 그냥 더할 수 없어서, 각각 2^EV로 바꿔 더한 뒤 다시 log2를 취해 합칩니다. 그 위에 밝은 렌즈는 코마 수차를 줄이려 한 스톱 조이고, NPF 결과를 30초 이하 표준 셔터 단계로 내림해 현장에서 바로 쓸 수 있는 값으로 만듭니다.

