1+ <!DOCTYPE html>
2+ < html lang ="en ">
3+ < head >
4+ < meta charset ="utf-8 ">
5+ < meta name ="viewport " content ="width=device-width, initial-scale=1 ">
6+ < title > CRM Acquisition: Grain Growth at Room Temperature</ title >
7+ < script src ="https://cdn.plot.ly/plotly-2.35.2.min.js "> </ script >
8+ < style >
9+ @import url ('https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,300;0,8..60,400;0,8..60,600;1,8..60,400&family=JetBrains+Mono:wght@400&display=swap' );
10+
11+ * {box-sizing : border-box;margin : 0 ;padding : 0 }
12+ body {
13+ font-family : 'Source Serif 4' , Georgia, serif;
14+ background : # f8f7f4 ;
15+ color : # 1e293b ;
16+ padding : 20px 20px 40px ;
17+ min-height : 100vh ;
18+ }
19+ .container {max-width : 620px ;margin : 0 auto}
20+
21+ .description {
22+ font-size : 0.84rem ;
23+ color : # 475569 ;
24+ line-height : 1.55 ;
25+ margin-bottom : 14px ;
26+ }
27+ .description b {color : # 1e293b }
28+ .description .pill {
29+ font-family : 'JetBrains Mono' , monospace;
30+ font-size : 0.8rem ;
31+ background : # ece9e3 ;
32+ padding : 1px 5px ;border-radius : 3px ;
33+ }
34+
35+ .controls {
36+ display : flex;align-items : center;gap : 12px ;
37+ flex-wrap : wrap;
38+ margin-bottom : 12px ;
39+ padding : 10px 14px ;
40+ background : # fff ;
41+ border : 1px solid # d6d3cd ;
42+ border-radius : 8px ;
43+ box-shadow : 0 1px 3px rgba (0 , 0 , 0 , 0.04 );
44+ }
45+ .controls label {font-size : 0.9rem ;font-weight : 600 ;white-space : nowrap}
46+ .slider-wrap {flex : 1 ;min-width : 180px ;display : flex;align-items : center;gap : 10px }
47+ .slider-wrap input [type = range ]{
48+ flex : 1 ;height : 6px ;-webkit-appearance : none;appearance : none;
49+ background : # d6d3cd ;border-radius : 3px ;outline : none;cursor : pointer;
50+ }
51+ .slider-wrap input [type = range ]::-webkit-slider-thumb {
52+ -webkit-appearance : none;width : 18px ;height : 18px ;
53+ background : # b91c1c ;border : 2px solid # fff ;border-radius : 50% ;
54+ box-shadow : 0 1px 4px rgba (0 , 0 , 0 , 0.25 );cursor : pointer;
55+ }
56+ .slider-wrap input [type = range ]::-moz-range-thumb {
57+ width : 18px ;height : 18px ;
58+ background : # b91c1c ;border : 2px solid # fff ;border-radius : 50% ;
59+ box-shadow : 0 1px 4px rgba (0 , 0 , 0 , 0.25 );cursor : pointer;
60+ }
61+ .time-val {
62+ font-family : 'JetBrains Mono' , monospace;
63+ font-size : 1.05rem ;font-weight : 600 ;
64+ min-width : 90px ;text-align : right;
65+ color : # b91c1c ;
66+ }
67+ .btn {
68+ font-family : inherit;font-size : 0.82rem ;font-weight : 600 ;
69+ padding : 5px 12px ;border : 1px solid # d6d3cd ;border-radius : 5px ;
70+ background : # fff ;color : # 1e293b ;cursor : pointer;
71+ transition : background 0.15s , border-color 0.15s ;
72+ white-space : nowrap;
73+ }
74+ .btn : hover {background : # f1f0ec ;border-color : # 94a3b8 }
75+ .btn .active {background : # b91c1c ;color : # fff ;border-color : # b91c1c }
76+
77+ .status {
78+ font-size : 0.86rem ;line-height : 1.6 ;
79+ padding : 7px 14px ;
80+ margin-bottom : 10px ;
81+ background : # fff ;
82+ border : 1px solid # d6d3cd ;
83+ border-radius : 8px ;
84+ }
85+ .status .lbl {font-weight : 600 }
86+
87+ # plot {
88+ background : # fff ;
89+ border : 1px solid # d6d3cd ;
90+ border-radius : 8px ;
91+ box-shadow : 0 1px 3px rgba (0 , 0 , 0 , 0.04 );
92+ overflow : hidden;
93+ }
94+ </ style >
95+ </ head >
96+ < body >
97+ < div class ="container ">
98+ < div class ="description ">
99+ At fixed temperature, < b > hematite</ b > grains precipitating from solution
100+ grow through their blocking volumes, acquiring a
101+ < b > chemical remanent magnetization</ b > (CRM). The iso-τ contours
102+ are static; as the grain population (ellipse) grows upward in volume,
103+ grains crossing the τ = 100 s boundary transition from
104+ < b style ="color:#0072B2 "> superparamagnetic</ b > to
105+ < b style ="color:#B8860B "> blocked CRM</ b > carriers.
106+
107+ < span class ="pill "> Hematite</ span > ·
108+ < span class ="pill "> T = 20 °C</ span > ·
109+ < span class ="pill "> τ₀ = 10⁻⁹ s</ span >
110+ </ div >
111+
112+ < div class ="controls ">
113+ < label > Grain volume:</ label >
114+ < div class ="slider-wrap ">
115+ < input type ="range " id ="tSlider " min ="0 " max ="29 " value ="0 " step ="1 ">
116+ < div class ="time-val " id ="tVal "> 3 %</ div >
117+ </ div >
118+ < button class ="btn " id ="btnPlay "> ▶ Grow</ button >
119+ < button class ="btn " id ="btnReset "> Reset</ button >
120+ </ div >
121+
122+ < div class ="status " id ="status ">
123+ < span class ="lbl "> Grain Volume:</ span > 3 % of maximum
124+ </ div >
125+
126+ < div id ="plot "> </ div >
127+ </ div >
128+
129+ < script >
130+ /* --- Physics constants --- */
131+ var kB = 1.380649e-23 ;
132+ var tau0 = 1e-9 ;
133+ var T_K = 293.15 ; /* fixed room temperature */
134+
135+ /* Hematite grain population (reference = fully grown) */
136+ var refCx = 0.6 ; /* kJ/m³ — center of K distribution */
137+ var refCy = 0.35 ; /* zm³ — center volume at full growth */
138+ var ellW = 0.8 ; /* kJ/m³ — spread in K */
139+ var ellH = 0.20 ; /* zm³ — spread in volume at full growth */
140+
141+ /* --- Growth steps: 30 steps, volume factor 0.03 to 1.0 --- */
142+ var nSteps = 30 ;
143+ var gFactors = [ ] ;
144+ for ( var i = 0 ; i < nSteps ; i ++ )
145+ gFactors [ i ] = 0.03 + 0.97 * i / ( nSteps - 1 ) ;
146+
147+ /* --- K axis (kJ/m³) --- */
148+ var nK = 300 ;
149+ var K_kJ = new Array ( nK ) ;
150+ var K_J = new Array ( nK ) ;
151+ for ( var i = 0 ; i < nK ; i ++ ) {
152+ K_kJ [ i ] = 0.01 + ( 5.0 - 0.01 ) * i / ( nK - 1 ) ;
153+ K_J [ i ] = K_kJ [ i ] * 1000 ;
154+ }
155+
156+ /* --- Ellipse angular coords --- */
157+ var nE = 200 ;
158+ var ex = new Array ( nE ) ;
159+ var ey_base = new Array ( nE ) ; /* at full growth (g = 1) */
160+ for ( var i = 0 ; i < nE ; i ++ ) {
161+ var th = 2 * Math . PI * i / ( nE - 1 ) ;
162+ ex [ i ] = refCx + ( ellW / 2 ) * Math . cos ( th ) ;
163+ ey_base [ i ] = refCy + ( ellH / 2 ) * Math . sin ( th ) ;
164+ }
165+
166+ /* --- Reference tau values --- */
167+ var tauDefs = [
168+ { label : '100 s' , val : 100 , color : '#0072B2' , w : 2.5 } ,
169+ { label : '1 Myr' , val : 1e6 * 3.15e7 , color : '#E69F00' , w : 2.5 } ,
170+ { label : '4.5 Gyr' , val : 4.5e9 * 3.15e7 , color : '#D55E00' , w : 2.5 }
171+ ] ;
172+
173+ function calcV ( tau , kj ) {
174+ return ( kB * T_K * Math . log ( tau / tau0 ) ) / Math . max ( kj , 1e-5 ) * 1e21 ;
175+ }
176+
177+ function interp ( x_new , x_arr , y_arr ) {
178+ var n = x_new . length , nArr = x_arr . length ;
179+ var result = new Array ( n ) ;
180+ for ( var i = 0 ; i < n ; i ++ ) {
181+ var xn = x_new [ i ] ;
182+ if ( xn <= x_arr [ 0 ] ) { result [ i ] = y_arr [ 0 ] ; continue ; }
183+ if ( xn >= x_arr [ nArr - 1 ] ) { result [ i ] = y_arr [ nArr - 1 ] ; continue ; }
184+ var lo = 0 , hi = nArr - 1 ;
185+ while ( hi - lo > 1 ) {
186+ var mid = ( lo + hi ) >> 1 ;
187+ if ( x_arr [ mid ] <= xn ) lo = mid ; else hi = mid ;
188+ }
189+ var frac = ( xn - x_arr [ lo ] ) / ( x_arr [ hi ] - x_arr [ lo ] ) ;
190+ result [ i ] = y_arr [ lo ] + frac * ( y_arr [ hi ] - y_arr [ lo ] ) ;
191+ }
192+ return result ;
193+ }
194+
195+ /* --- Precompute static tau curves --- */
196+ var tauCurves = [ ] ;
197+ for ( var t = 0 ; t < tauDefs . length ; t ++ ) {
198+ var curve = new Array ( nK ) ;
199+ for ( var i = 0 ; i < nK ; i ++ )
200+ curve [ i ] = Math . min ( Math . max ( calcV ( tauDefs [ t ] . val , K_J [ i ] ) , 0 ) , 0.5 ) ;
201+ tauCurves [ t ] = curve ;
202+ }
203+
204+ /* Precompute boundary (tau = 100 s) at ellipse x positions — constant */
205+ var bnd_at_ex = interp ( ex , K_kJ , tauCurves [ 0 ] ) ;
206+
207+ /* --- Initial state (smallest grains) --- */
208+ var g0 = gFactors [ 0 ] ;
209+ var initEy = ey_base . map ( function ( y ) { return y * g0 ; } ) ;
210+ var initSsd = initEy . map ( function ( y , i ) {
211+ return Math . min ( Math . max ( y , bnd_at_ex [ i ] ) , 0.6 ) ;
212+ } ) ;
213+ var initSp = initEy . map ( function ( y , i ) {
214+ return Math . min ( y , bnd_at_ex [ i ] ) ;
215+ } ) ;
216+
217+ /* --- Traces ---
218+ 0: SSD fill (gold) — dynamic (y only)
219+ 1: SP fill (light blue) — dynamic (y only)
220+ 2: Ellipse outline — dynamic (y only)
221+ 3–5: tau curves — STATIC
222+ */
223+ var ff = 'Source Serif 4, Georgia, serif' ;
224+ var gridC = '#e8e6e1' , axC = '#334155' ;
225+
226+ var traces = [
227+ { x : ex , y : initSsd , fill : 'toself' , mode : 'lines' ,
228+ line : { width : 0.5 , color : 'gray' } , fillcolor : 'rgba(255,215,0,0.5)' ,
229+ hoverinfo : 'skip' , showlegend : false } ,
230+
231+ { x : ex , y : initSp , fill : 'toself' , mode : 'lines' ,
232+ line : { width : 0.5 , color : 'gray' } , fillcolor : 'rgba(173,216,230,0.6)' ,
233+ hoverinfo : 'skip' , showlegend : false } ,
234+
235+ { x : ex , y : initEy , mode : 'lines' ,
236+ line : { width : 1.5 , color : 'gray' } ,
237+ hoverinfo : 'skip' , showlegend : false }
238+ ] ;
239+
240+ for ( var t = 0 ; t < tauDefs . length ; t ++ ) {
241+ traces . push ( {
242+ x : K_kJ , y : tauCurves [ t ] , mode : 'lines' ,
243+ line : { color : tauDefs [ t ] . color , width : tauDefs [ t ] . w } ,
244+ name : '\u03C4 = ' + tauDefs [ t ] . label ,
245+ showlegend : true
246+ } ) ;
247+ }
248+
249+ var layout = {
250+ width : 600 , height : 390 ,
251+ margin : { l : 68 , r : 80 , t : 10 , b : 50 } ,
252+ paper_bgcolor : 'transparent' ,
253+ plot_bgcolor : '#fff' ,
254+ font : { family : ff , size : 12 , color : '#1e293b' } ,
255+
256+ xaxis : {
257+ title : { text : 'Anisotropy Energy Density (kJ/m\u00B3)' , font : { size : 13 } } ,
258+ range : [ 0 , 3 ] ,
259+ showgrid : true , gridcolor : gridC , gridwidth : 1 ,
260+ zeroline : false , linecolor : axC , linewidth : 1.5 , mirror : true ,
261+ ticks : 'inside' , ticklen : 5 ,
262+ tickfont : { family : ff , size : 11 }
263+ } ,
264+ yaxis : {
265+ title : { text : 'Grain Volume (zm\u00B3)' , font : { size : 13 } } ,
266+ range : [ 0 , 0.48 ] ,
267+ showgrid : true , gridcolor : gridC , gridwidth : 1 ,
268+ zeroline : false , linecolor : axC , linewidth : 1.5 , mirror : true ,
269+ ticks : 'inside' , ticklen : 5 ,
270+ tickfont : { family : ff , size : 11 }
271+ } ,
272+
273+ legend : {
274+ bgcolor : 'rgba(255,255,255,0.92)' ,
275+ bordercolor : '#d6d3cd' , borderwidth : 1 ,
276+ font : { size : 11 } ,
277+ x : 0.98 , y : 0.98 , xanchor : 'right' , yanchor : 'top' ,
278+ borderpad : 6
279+ } ,
280+
281+ annotations : [
282+ { x : 0.15 , y : 0.04 , text : 'Superparamagnetic<br>(growing)' ,
283+ showarrow : false , font : { size : 12 , color : '#0072B2' , family : ff } ,
284+ bgcolor : 'rgba(255,255,255,0.85)' , borderpad : 2 } ,
285+ { x : 1.8 , y : 0.38 , text : 'Blocked<br>(CRM acquired)' ,
286+ showarrow : false , font : { size : 12 , color : '#B8860B' , family : ff } ,
287+ bgcolor : 'rgba(255,255,255,0.85)' , borderpad : 2 }
288+ ] ,
289+
290+ hovermode : 'closest'
291+ } ;
292+
293+ Plotly . newPlot ( 'plot' , traces , layout , { displayModeBar : false , responsive : true } ) ;
294+
295+ /* --- Interactivity --- */
296+ var slider = document . getElementById ( 'tSlider' ) ;
297+ var tValEl = document . getElementById ( 'tVal' ) ;
298+ var statusEl = document . getElementById ( 'status' ) ;
299+ var btnPlay = document . getElementById ( 'btnPlay' ) ;
300+ var btnReset = document . getElementById ( 'btnReset' ) ;
301+
302+ function updateToIdx ( idx ) {
303+ var g = gFactors [ idx ] ;
304+ var ey_new = ey_base . map ( function ( y ) { return y * g ; } ) ;
305+ var ssd_y = ey_new . map ( function ( y , i ) {
306+ return Math . min ( Math . max ( y , bnd_at_ex [ i ] ) , 0.6 ) ;
307+ } ) ;
308+ var sp_y = ey_new . map ( function ( y , i ) {
309+ return Math . min ( y , bnd_at_ex [ i ] ) ;
310+ } ) ;
311+
312+ /* Only traces 0–2 update; tau curves (3–5) are static */
313+ Plotly . restyle ( 'plot' , { y : [ ssd_y , sp_y , ey_new ] } , [ 0 , 1 , 2 ] ) ;
314+
315+ var pct = Math . round ( g * 100 ) ;
316+ tValEl . textContent = pct + ' %' ;
317+ slider . value = idx ;
318+ statusEl . innerHTML =
319+ '<span class="lbl">Grain Volume:</span> ' + pct + ' % of maximum' ;
320+ }
321+
322+ slider . addEventListener ( 'input' , function ( ) {
323+ updateToIdx ( parseInt ( this . value ) ) ;
324+ } ) ;
325+
326+ var playing = false , playTimer = null ;
327+
328+ function stopPlay ( ) {
329+ playing = false ;
330+ clearInterval ( playTimer ) ;
331+ btnPlay . innerHTML = '▶ Grow' ;
332+ btnPlay . classList . remove ( 'active' ) ;
333+ }
334+
335+ btnPlay . addEventListener ( 'click' , function ( ) {
336+ if ( playing ) { stopPlay ( ) ; return ; }
337+ playing = true ;
338+ btnPlay . innerHTML = '▮▮ Pause' ;
339+ btnPlay . classList . add ( 'active' ) ;
340+ var idx = parseInt ( slider . value ) ;
341+ playTimer = setInterval ( function ( ) {
342+ idx ++ ;
343+ if ( idx >= nSteps ) { stopPlay ( ) ; return ; }
344+ updateToIdx ( idx ) ;
345+ } , 200 ) ;
346+ } ) ;
347+
348+ btnReset . addEventListener ( 'click' , function ( ) {
349+ stopPlay ( ) ;
350+ updateToIdx ( 0 ) ;
351+ } ) ;
352+ </ script >
353+ </ body >
354+ </ html >
0 commit comments