ascii-side-of-the-moon 1.0.7 → 1.0.9

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.
package/dist/index.d.cts CHANGED
@@ -11,6 +11,13 @@ interface MoonPhase {
11
11
  illuminatedFraction: number;
12
12
  /** If true, bright limb on the right (waxing). If false, bright limb on the left (waning). */
13
13
  isWaxing?: boolean;
14
+ /**
15
+ * Position angle of the bright limb in degrees.
16
+ * Measured from celestial north, eastward (counterclockwise when looking at the sky).
17
+ * 0° = north, 90° = east, 180° = south, 270° = west.
18
+ * This determines the orientation of the terminator line.
19
+ */
20
+ brightLimbAngle?: number;
14
21
  }
15
22
  interface MoonSize {
16
23
  /** Geocentric distance (km). */
@@ -18,12 +25,30 @@ interface MoonSize {
18
25
  /** Apparent angular diameter (deg). */
19
26
  angularDiameterDeg: number;
20
27
  }
28
+ interface ObserverLocation {
29
+ /** Latitude in degrees (positive north). */
30
+ latitude: number;
31
+ /** Longitude in degrees (positive east). */
32
+ longitude: number;
33
+ /** Elevation above mean sea level in meters. Defaults to 0 if omitted. */
34
+ elevationMeters?: number;
35
+ }
36
+ interface MoonPosition {
37
+ /** Azimuth (degrees): 0=North, 90=East, etc. */
38
+ azimuth: number;
39
+ /** Altitude (degrees): +90=Zenith, 0=Horizon, -90=Nadir. */
40
+ altitude: number;
41
+ /** Parallactic angle (degrees): Rotation of the moon's disk relative to the zenith (positive = clockwise). */
42
+ parallacticAngle: number;
43
+ }
21
44
  interface MoonState {
22
45
  date: Date;
23
46
  /** Phase angle (deg): 0=full, 180=new (per Astronomy Engine convention for phase angle). */
24
47
  phase: MoonPhase;
25
48
  size: MoonSize;
26
49
  libration: MoonLibration;
50
+ /** Observer-dependent position. Undefined if no location provided. */
51
+ position?: MoonPosition;
27
52
  }
28
53
  interface RenderOptions {
29
54
  /** Number of text rows to use. (Renderer currently fixes output to FRAME_H.) */
@@ -32,9 +57,11 @@ interface RenderOptions {
32
57
  invert?: boolean;
33
58
  /** If true, prefer legacy block elements instead of twelfth-circle glyphs. (Unused now.) */
34
59
  forceLegacySymbols?: boolean;
60
+ /** Controls whether the renderer draws the horizon overlay (default true). */
61
+ showHorizon?: boolean;
35
62
  }
36
63
 
37
- declare function getMoonState(date: Date): MoonState;
64
+ declare function getMoonState(date: Date, observerLocation?: ObserverLocation): MoonState;
38
65
  /**
39
66
  * Returns the English name for the moon phase using phase angle (preferred) with small tolerances.
40
67
  * Conventions (Astronomy Engine):
@@ -49,11 +76,20 @@ declare function getMoonPhase(moonState: MoonState): string;
49
76
 
50
77
  /**
51
78
  * Render a 60×29 moon using pre-rendered ASCII art.
52
- * - Uses nearest pre-rendered moon for distance and libration
53
- * - Applies phase masking via Lambertian lighting
54
- * - Fallback: if nothing lights up (ultra-thin crescent), select the top
55
- * illuminatedFraction of disc cells by raw incidence (n·s) to force a sliver.
79
+ *
80
+ * Algorithm:
81
+ * 1. Select the nearest pre-rendered moon texture (based on distance and libration)
82
+ * 2. Calculate sun direction in CELESTIAL frame (standard north-up orientation)
83
+ * 3. Apply Lambertian phase lighting to create illumination mask (in celestial frame)
84
+ * 4. Combine texture with phase mask (both in celestial frame)
85
+ * 5. Rotate the COMBINED result by parallactic angle to match observer's view
86
+ * 6. Optionally overlay horizon line
87
+ *
88
+ * Key insight: The phase illumination is a property of the moon itself (which physical
89
+ * areas are lit by the sun). When we rotate the moon's appearance for the observer,
90
+ * the illuminated areas rotate WITH the texture because they're physically attached
91
+ * to the lunar surface.
56
92
  */
57
93
  declare function renderMoon(state: MoonState, _options?: RenderOptions): string;
58
94
 
59
- export { type MoonLibration, type MoonPhase, type MoonSize, type MoonState, type RenderOptions, getMoonPhase, getMoonState, renderMoon };
95
+ export { type MoonLibration, type MoonPhase, type MoonPosition, type MoonSize, type MoonState, type ObserverLocation, type RenderOptions, getMoonPhase, getMoonState, renderMoon };
package/dist/index.d.ts CHANGED
@@ -11,6 +11,13 @@ interface MoonPhase {
11
11
  illuminatedFraction: number;
12
12
  /** If true, bright limb on the right (waxing). If false, bright limb on the left (waning). */
13
13
  isWaxing?: boolean;
14
+ /**
15
+ * Position angle of the bright limb in degrees.
16
+ * Measured from celestial north, eastward (counterclockwise when looking at the sky).
17
+ * 0° = north, 90° = east, 180° = south, 270° = west.
18
+ * This determines the orientation of the terminator line.
19
+ */
20
+ brightLimbAngle?: number;
14
21
  }
15
22
  interface MoonSize {
16
23
  /** Geocentric distance (km). */
@@ -18,12 +25,30 @@ interface MoonSize {
18
25
  /** Apparent angular diameter (deg). */
19
26
  angularDiameterDeg: number;
20
27
  }
28
+ interface ObserverLocation {
29
+ /** Latitude in degrees (positive north). */
30
+ latitude: number;
31
+ /** Longitude in degrees (positive east). */
32
+ longitude: number;
33
+ /** Elevation above mean sea level in meters. Defaults to 0 if omitted. */
34
+ elevationMeters?: number;
35
+ }
36
+ interface MoonPosition {
37
+ /** Azimuth (degrees): 0=North, 90=East, etc. */
38
+ azimuth: number;
39
+ /** Altitude (degrees): +90=Zenith, 0=Horizon, -90=Nadir. */
40
+ altitude: number;
41
+ /** Parallactic angle (degrees): Rotation of the moon's disk relative to the zenith (positive = clockwise). */
42
+ parallacticAngle: number;
43
+ }
21
44
  interface MoonState {
22
45
  date: Date;
23
46
  /** Phase angle (deg): 0=full, 180=new (per Astronomy Engine convention for phase angle). */
24
47
  phase: MoonPhase;
25
48
  size: MoonSize;
26
49
  libration: MoonLibration;
50
+ /** Observer-dependent position. Undefined if no location provided. */
51
+ position?: MoonPosition;
27
52
  }
28
53
  interface RenderOptions {
29
54
  /** Number of text rows to use. (Renderer currently fixes output to FRAME_H.) */
@@ -32,9 +57,11 @@ interface RenderOptions {
32
57
  invert?: boolean;
33
58
  /** If true, prefer legacy block elements instead of twelfth-circle glyphs. (Unused now.) */
34
59
  forceLegacySymbols?: boolean;
60
+ /** Controls whether the renderer draws the horizon overlay (default true). */
61
+ showHorizon?: boolean;
35
62
  }
36
63
 
37
- declare function getMoonState(date: Date): MoonState;
64
+ declare function getMoonState(date: Date, observerLocation?: ObserverLocation): MoonState;
38
65
  /**
39
66
  * Returns the English name for the moon phase using phase angle (preferred) with small tolerances.
40
67
  * Conventions (Astronomy Engine):
@@ -49,11 +76,20 @@ declare function getMoonPhase(moonState: MoonState): string;
49
76
 
50
77
  /**
51
78
  * Render a 60×29 moon using pre-rendered ASCII art.
52
- * - Uses nearest pre-rendered moon for distance and libration
53
- * - Applies phase masking via Lambertian lighting
54
- * - Fallback: if nothing lights up (ultra-thin crescent), select the top
55
- * illuminatedFraction of disc cells by raw incidence (n·s) to force a sliver.
79
+ *
80
+ * Algorithm:
81
+ * 1. Select the nearest pre-rendered moon texture (based on distance and libration)
82
+ * 2. Calculate sun direction in CELESTIAL frame (standard north-up orientation)
83
+ * 3. Apply Lambertian phase lighting to create illumination mask (in celestial frame)
84
+ * 4. Combine texture with phase mask (both in celestial frame)
85
+ * 5. Rotate the COMBINED result by parallactic angle to match observer's view
86
+ * 6. Optionally overlay horizon line
87
+ *
88
+ * Key insight: The phase illumination is a property of the moon itself (which physical
89
+ * areas are lit by the sun). When we rotate the moon's appearance for the observer,
90
+ * the illuminated areas rotate WITH the texture because they're physically attached
91
+ * to the lunar surface.
56
92
  */
57
93
  declare function renderMoon(state: MoonState, _options?: RenderOptions): string;
58
94
 
59
- export { type MoonLibration, type MoonPhase, type MoonSize, type MoonState, type RenderOptions, getMoonPhase, getMoonState, renderMoon };
95
+ export { type MoonLibration, type MoonPhase, type MoonPosition, type MoonSize, type MoonState, type ObserverLocation, type RenderOptions, getMoonPhase, getMoonState, renderMoon };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import*as K from'astronomy-engine';var c=K.default??K;function u($,t){let n=new Date($.getTime());return n.setDate(n.getDate()+t),n}function k($){let t=c.Illumination(c.Body.Moon,$).phase_fraction;return c.Illumination(c.Body.Moon,u($,1)).phase_fraction>t}function z($){let t=c.Illumination(c.Body.Moon,$),n=c.Libration($);return {date:$,phase:{phaseAngleDeg:t.phase_angle,illuminatedFraction:t.phase_fraction,isWaxing:k($)},size:{distanceKm:n.dist_km,angularDiameterDeg:n.diam_deg},libration:{elon:n.elon,elat:n.elat}}}function j($){let{phaseAngleDeg:t,illuminatedFraction:n}=$.phase,M=$.phase.isWaxing??t>90,_=(t%360+360)%360,a=_>180?360-_:_,y=10,s=10,B=8,e=.98,W=.02,o=.02;return a<=y||n>=e?"Full Moon":a>=180-s||n<=W?"New Moon":Math.abs(a-90)<=B||Math.abs(n-.5)<=o?M?"First Quarter":"Last Quarter":a<90?M?"Waxing Gibbous":"Waning Gibbous":M?"Waxing Crescent":"Waning Crescent"}var d={moons:[{index:0,distance_km:405493.5,libration_elat:-0.888,libration_elon:-0.412,ascii:`
1
+ import*as T from'astronomy-engine';var s=T.default??T;function p($,g){let n=new Date($.getTime());return n.setDate(n.getDate()+g),n}function z($){return ($%360+360)%360}function P($){let g=s.GeoVector(s.Body.Sun,$,true),n=s.GeoVector(s.Body.Moon,$,true),t=s.EquatorFromVector(g),_=s.EquatorFromVector(n),B=(t.ra-_.ra)*15*s.DEG2RAD,M=_.dec*s.DEG2RAD,a=t.dec*s.DEG2RAD,o=Math.sin(B),i=Math.cos(B),e=o,m=Math.cos(M)*Math.tan(a)-Math.sin(M)*i,A=Math.atan2(e,m)*s.RAD2DEG;return z(A)}function D($){let g=($%24+24)%24;return g>12&&(g-=24),g}function X($){let g=s.Illumination(s.Body.Moon,$).phase_fraction;return s.Illumination(s.Body.Moon,p($,1)).phase_fraction>g}function N($,g,n){let t=s.SiderealTime($),_=g.longitude/15,B=D(_+t-n.ra)*15*s.DEG2RAD,M=g.latitude*s.DEG2RAD,a=n.dec*s.DEG2RAD,o=Math.sin(B),i=Math.tan(M)*Math.cos(a)-Math.sin(a)*Math.cos(B);return Math.atan2(o,i)*s.RAD2DEG}function Y($,g){let n=new s.Observer(g.latitude,g.longitude,g.elevationMeters??0),t=s.Equator(s.Body.Moon,$,n,true,true),_=s.Horizon($,n,t.ra,t.dec,"normal");return {azimuth:z(_.azimuth),altitude:_.altitude,parallacticAngle:N($,g,t)}}function G($,g){let n=s.Illumination(s.Body.Moon,$),t=s.Libration($);return {date:$,phase:{phaseAngleDeg:n.phase_angle,illuminatedFraction:n.phase_fraction,isWaxing:X($),brightLimbAngle:P($)},size:{distanceKm:t.dist_km,angularDiameterDeg:t.diam_deg},libration:{elon:t.elon,elat:t.elat},position:g?Y($,g):void 0}}function H($){let{phaseAngleDeg:g,illuminatedFraction:n}=$.phase,t=$.phase.isWaxing??g>90,_=(g%360+360)%360,R=_>180?360-_:_,B=10,M=10,a=8,o=.98,i=.02,e=.02;return R<=B||n>=o?"Full Moon":R>=180-M||n<=i?"New Moon":Math.abs(R-90)<=a||Math.abs(n-.5)<=e?t?"First Quarter":"Last Quarter":R<90?t?"Waxing Gibbous":"Waning Gibbous":t?"Waxing Crescent":"Waning Crescent"}var K={moons:[{index:0,distance_km:405493.5,libration_elat:-0.888,libration_elon:-0.412,ascii:`
2
2
 
3
3
  ._ . - .
4
4
  ,_++> a, +. \`-.
@@ -894,8 +894,12 @@ import*as K from'astronomy-engine';var c=K.default??K;function u($,t){let n=new
894
894
  'FFF'"Z"@MGR^^'\`
895
895
 
896
896
 
897
- `}]};var r=60,l=29;function P($){let t=$.split(`
898
- `),n=1/0,M=-1/0,_=1/0,a=-1/0;if(t.forEach((B,e)=>{let W=B.search(/\S/);if(W!==-1){let o=B.search(/\s+$/);n=Math.min(n,W),M=Math.max(M,o===-1?B.length-1:o-1),_===1/0&&(_=e),a=e;}}),n===1/0)return {width:r,height:l,centerX:Math.floor(r/2),centerY:Math.floor(l/2)};let y=M-n+1,s=a-_+1;return {width:y,height:s,centerX:n+Math.floor(y/2),centerY:_+Math.floor(s/2)}}function X($,t,n){return Math.min(n,Math.max(t,$))}function Z($){return $*Math.PI/180}function N($,t,n=0){let M=Z($),_=Z(n),y=(t?1:-1)*Math.sin(M),s=0,B=Math.cos(M),e=-B*Math.sin(_),W=B*Math.cos(_);s=e,B=W;let o=Math.sqrt(y*y+s*s+B*B);return {sx:y/o,sy:s/o,sz:B/o}}function h($,t,n,M,_){let a=1/r,y=1/l,s=[-0.5,-0.25,0,.25,.5],B=0,e=0;for(let W of s)for(let o of s){let A=$+W*a,w=t+o*y,b=A*A+w*w;if(b>1)continue;let g=Math.sqrt(Math.max(0,1-b)),R=A*n+w*M+g*_;B+=Math.max(0,R),e++;}return e===0?0:B/e}function Y($){let M=0,_=1/0;for(let a=0;a<d.moons.length;a++){let y=d.moons[a],s=Math.abs(y.distance_km-$.size.distanceKm)*1,B=Math.abs(y.libration_elat-$.libration.elat)*1e4,e=Math.abs(y.libration_elon-$.libration.elon)*1e4,W=s+B+e;W<_&&(_=W,M=a);}return d.moons[M]}function p($,t={}){let n=Y($),M=n.ascii.split(`
899
- `),_=P(n.ascii),{sx:a,sy:y,sz:s}=N($.phase.phaseAngleDeg,$.phase.isWaxing,$.libration.elat),B=Array.from({length:l},()=>Array(r).fill(0)),e=Array.from({length:l},()=>Array(r).fill(-1)),W=Array.from({length:l},()=>Array(r).fill(false)),o=0,A=0;for(let g=0;g<l;g++)for(let R=0;R<r;R++){let m=R-_.centerX,i=g-_.centerY,E=m/(_.width/2),F=i/(_.height/2),T=E*E+F*F;if(T>1){B[g][R]=0,e[g][R]=-1;continue}W[g][R]=true,o++;let x=h(E,F,a,y,s);B[g][R]=x,x>0&&A++;let f=Math.sqrt(Math.max(0,1-T));e[g][R]=E*a+F*y+f*s;}let w=Array.from({length:l},()=>Array(r).fill(false));if(A>0)for(let g=0;g<l;g++)for(let R=0;R<r;R++)w[g][R]=B[g][R]>0;else {let g=X($.phase.illuminatedFraction??0,0,1),R=Math.max(1,Math.round(g*o)),m=[];for(let i=0;i<l;i++)for(let E=0;E<r;E++)W[i][E]&&m.push({ix:E,iy:i,v:e[i][E]});m.sort((i,E)=>E.v-i.v);for(let i=0;i<Math.min(R,m.length);i++){let{ix:E,iy:F}=m[i];w[F][E]=true;}}let b=[];for(let g=0;g<l;g++){let R="",m=M[g]??"";for(let i=0;i<r;i++)w[g][i]?R+=m[i]??" ":R+=" ";b.push(R);}return b.join(`
900
- `)}export{j as getMoonPhase,z as getMoonState,p as renderMoon};//# sourceMappingURL=index.mjs.map
897
+ `}]};var c=60,d=29,J=-45;function V($){let g=$.split(`
898
+ `),n=1/0,t=-1/0,_=1/0,R=-1/0;if(g.forEach((a,o)=>{let i=a.search(/\S/);if(i!==-1){let e=a.search(/\s+$/);n=Math.min(n,i),t=Math.max(t,e===-1?a.length-1:e-1),_===1/0&&(_=o),R=o;}}),n===1/0)return {width:c,height:d,centerX:Math.floor(c/2),centerY:Math.floor(d/2)};let B=t-n+1,M=R-_+1;return {width:B,height:M,centerX:n+Math.floor(B/2),centerY:_+Math.floor(M/2)}}function L($,g,n){return Math.min(n,Math.max(g,$))}function x($){return $*Math.PI/180}function Q($,g,n,t=0){let _=x($),R=x(t),B;g!==void 0?B=x(g):B=x(n?270:90);let M=-Math.sin(B),a=-Math.cos(B),o=M*Math.sin(_),i=a*Math.sin(_),e=Math.cos(_),m=i*Math.cos(R)-e*Math.sin(R),A=i*Math.sin(R)+e*Math.cos(R);i=m,e=A;let W=Math.sqrt(o*o+i*i+e*e);return {sx:o/W,sy:i/W,sz:e/W}}function I($,g,n,t,_){let R=1/c,B=1/d,M=[-0.5,-0.25,0,.25,.5],a=0,o=0;for(let i of M)for(let e of M){let m=$+i*R,A=g+e*B,W=m*m+A*A;if(W>1)continue;let w=Math.sqrt(Math.max(0,1-W)),b=m*n+A*t+w*_;a+=Math.max(0,b),o++;}return o===0?0:a/o}function S($){let t=0,_=1/0;for(let R=0;R<K.moons.length;R++){let B=K.moons[R],M=Math.abs(B.distance_km-$.size.distanceKm)*1,a=Math.abs(B.libration_elat-$.libration.elat)*1e4,o=Math.abs(B.libration_elon-$.libration.elon)*1e4,i=M+a+o;i<_&&(_=i,t=R);}return K.moons[t]}var k=22/10;function q($,g,n,t){let _=(g%360+360)%360;if(Math.abs(_)<.1)return $;let R=$.split(`
899
+ `),B=R.length,M=R[0]?.length??0,a=n??(M-1)/2,o=t??(B-1)/2,i=_*Math.PI/180,e=Math.cos(i),m=Math.sin(i),A=Array.from({length:B},()=>Array(M).fill(" "));for(let W=0;W<B;W++)for(let w=0;w<M;w++){let b=w-a,F=(W-o)*k,r=b*e-F*m,E=b*m+F*e,u=Math.round(r+a),y=Math.round(E/k+o);if(u>=0&&u<M&&y>=0&&y<B){let l=R[y]?.[u]??" ";A[W][w]=l;}}return A.map(W=>W.join("")).join(`
900
+ `)}function C($,g={}){let t=(g??{}).showHorizon!==false,_=S($),R=_.ascii.split(`
901
+ `),B=V(_.ascii),{sx:M,sy:a,sz:o}=Q($.phase.phaseAngleDeg,$.phase.brightLimbAngle,$.phase.isWaxing,$.libration.elat),i=Array.from({length:d},()=>Array(c).fill(0)),e=Array.from({length:d},()=>Array(c).fill(-1)),m=Array.from({length:d},()=>Array(c).fill(false)),A=0,W=0;for(let r=0;r<d;r++)for(let E=0;E<c;E++){let u=E-B.centerX,y=r-B.centerY,l=u/(B.width/2),f=y/(B.height/2),h=l*l+f*f;if(h>1){i[r][E]=0,e[r][E]=-1;continue}m[r][E]=true,A++;let Z=I(l,f,M,a,o);i[r][E]=Z,Z>0&&W++;let j=Math.sqrt(Math.max(0,1-h));e[r][E]=l*M+f*a+j*o;}let w=Array.from({length:d},()=>Array(c).fill(false));if(W>0)for(let r=0;r<d;r++)for(let E=0;E<c;E++)w[r][E]=i[r][E]>0;else {let r=L($.phase.illuminatedFraction??0,0,1),E=Math.max(1,Math.round(r*A)),u=[];for(let y=0;y<d;y++)for(let l=0;l<c;l++)m[y][l]&&u.push({ix:l,iy:y,v:e[y][l]});u.sort((y,l)=>l.v-y.v);for(let y=0;y<Math.min(E,u.length);y++){let{ix:l,iy:f}=u[y];w[f][l]=true;}}let b=[];for(let r=0;r<d;r++){let E="",u=R[r]??"";for(let y=0;y<c;y++)w[r][y]?E+=u[y]??" ":E+=" ";b.push(E);}let F=b.join(`
902
+ `);if($.position?.parallacticAngle!==void 0){let r=-$.position.parallacticAngle+J;Math.abs(r)>.1&&(F=q(F,r,B.centerX,B.centerY));}return t?U(F,$,B):F}function U($,g,n){let t=g.position;if(!t||g.size.angularDiameterDeg<=0)return $;let _=t.altitude??0,R=g.size.angularDiameterDeg/2,B=_+R;if(_-R>=0)return $;let a=$.split(`
903
+ `),o=g.size.angularDiameterDeg/Math.max(1,n.height),i=n.centerY+_/o;i=Math.round(L(i,0,d-1));let e;return B<=0?e=`${Math.abs(_).toFixed(1).replace(/\.0$/,"")}-deg-below-horizon`:e="horizon",a[i]=O(e),a.join(`
904
+ `)}function O($){let n=`--${$.trim().replace(/\s+/g,"-")}--`;if(n.length>c&&(n=n.slice(0,c)),n.length===c)return n;let t=c-n.length,_=Math.floor(t/2),R=t-_;return "-".repeat(_)+n+"-".repeat(R)}export{H as getMoonPhase,G as getMoonState,C as renderMoon};//# sourceMappingURL=index.mjs.map
901
905
  //# sourceMappingURL=index.mjs.map