Files
astro/moon/svg/occultation_moon_phase.go
T
b612 2bf8478639 feat: 完善日月食与月掩几何链路并扩展历法接口
- 新增日月食中心带、偏食带、阴影足迹、等时线、食分线及升落边界计算,支持极区与混合食拓扑
- 新增日食单时刻阴影求解器、站心状态查询、批量采样和 ΔT 覆盖接口
- 重构恒星与行星月掩路径,补充有限盘面接触、站心修正、掩带宽度、极区投影及升落边界
- 扩展 SVG 与 GeoJSON 输出,支持详细面板、全球/极区/地球投影、边界闭合、时间标记和拓扑签名
- 扩展日月食候选搜索、局地搜索、沙罗序列预计算与范围外推,补充系列锚点和成员一致性校验
- 补齐古历纪年、儒略历独有闰日、多公历候选、历法改革跨日及精确日期运算接口
- 优化 ΔT、章动、恒星时、月球地平线、事件根搜索和本地星历缓存,降低重复计算开销并提升边界稳定
2026-09-17 12:27:40 +08:00

106 lines
3.7 KiB
Go

package svg
import (
"fmt"
"html"
"math"
"strings"
"time"
"b612.me/astro/moon"
)
// localOccultationMoonAppearance contains the apparent lunar illumination used
// by the fixed-site occultation diagrams. The limb angle is topocentric so the
// rendered bright side follows the same observer geometry as the occultation.
type localOccultationMoonAppearance struct {
illumination float64
brightLimbPA float64
}
func localOccultationMoonAppearanceAt(date time.Time, observer moon.Observer) localOccultationMoonAppearance {
illumination := moon.Phase(date)
if !starOccultationFinite(illumination) {
illumination = 1
}
illumination = math.Max(0, math.Min(1, illumination))
brightLimbPA := moon.TopocentricBrightLimbPositionAngle(
date, observer.Longitude, observer.Latitude, observer.Height,
)
if !starOccultationFinite(brightLimbPA) {
brightLimbPA = 0
}
return localOccultationMoonAppearance{
illumination: illumination,
brightLimbPA: brightLimbPA,
}
}
// writeLocalOccultationMoon draws a textured lunar disk with a phase-aware
// terminator. clipID is unique within the SVG document because each stage
// can have a different event time and therefore a different lunar phase.
func writeLocalOccultationMoon(
b *strings.Builder,
cx, cy, radius float64,
class string,
appearance localOccultationMoonAppearance,
clipID string,
) {
if !starOccultationFinite(radius) || radius <= 0 {
return
}
illumination := math.Max(0, math.Min(1, appearance.illumination))
brightLimbPA := appearance.brightLimbPA
if !starOccultationFinite(brightLimbPA) {
brightLimbPA = 0
}
fmt.Fprintf(b, `<g class="%s" data-illumination="%.6f" data-bright-limb-position-angle="%.3f">`,
html.EscapeString(class), illumination, brightLimbPA)
if illumination < 1-1e-6 {
fmt.Fprintf(b, `<circle class="moon-phase-dark" cx="%.3f" cy="%.3f" r="%.3f" fill="#24272a"/>`, cx, cy, radius)
}
if illumination > 1e-6 {
if illumination >= 1-1e-6 {
writeLocalOccultationMoonTexture(b, cx, cy, radius)
} else {
path := localOccultationMoonPhasePath(radius, illumination)
rotation := localOccultationMoonSVGRotation(brightLimbPA)
fmt.Fprintf(b, `<clipPath id="%s"><path d="%s" transform="translate(%.3f %.3f) rotate(%.3f)"/></clipPath>`,
html.EscapeString(clipID), path, cx, cy, rotation)
fmt.Fprintf(b, `<g class="moon-phase-lit" clip-path="url(#%s)">`, html.EscapeString(clipID))
writeLocalOccultationMoonTexture(b, cx, cy, radius)
b.WriteString(`</g>`)
}
}
fmt.Fprintf(b, `<circle cx="%.3f" cy="%.3f" r="%.3f" fill="none" stroke="#4f5d60" stroke-width="1.1" opacity="0.9"/>`, cx, cy, radius)
b.WriteString(`</g>`)
}
func writeLocalOccultationMoonTexture(b *strings.Builder, cx, cy, radius float64) {
fmt.Fprintf(b, `<use href="#le-moon" x="%.3f" y="%.3f" width="%.3f" height="%.3f"/>`,
cx-radius, cy-radius, radius*2, radius*2)
}
// The chart uses north-up and east-left coordinates. SVG rotations are
// clockwise from +X, so a celestial position angle of 0 points upward and 90
// points left after this conversion.
func localOccultationMoonSVGRotation(brightLimbPA float64) float64 {
return -brightLimbPA - 90
}
// localOccultationMoonPhasePath returns the illuminated portion of a unit
// projected lunar disk. The outer arc is the visible limb; the inner ellipse
// is the projected terminator. The +X side is the bright side before rotation.
func localOccultationMoonPhasePath(radius, illumination float64) string {
cosPhase := 2*illumination - 1
terminatorRadius := radius * math.Abs(cosPhase)
sweep := 1
if cosPhase < 0 {
sweep = 0
}
return fmt.Sprintf(
"M 0 %.3f A %.3f %.3f 0 0 1 0 %.3f A %.3f %.3f 0 0 %d 0 %.3f Z",
-radius, radius, radius, radius, terminatorRadius, radius, sweep, -radius,
)
}