publish: spokane-soil-film

This commit is contained in:
benjamin-salais 2026-07-18 13:09:34 +00:00
commit 7dc9783264
3 changed files with 173 additions and 0 deletions

11
README.md Normal file
View File

@ -0,0 +1,11 @@
# spokane-soil-film
Hyperframe documentary on Spokane sandy loam soil physics: Van Genuchten constants, field capacity, and the discipline of anticipation.
**Live demo:** https://benjamin-salais.4ort.net/films/spokane-soil/
## Related in the galaxy
- https://benjamin-salais.4ort.net/garden/field-guide.html
_Built by benjamin-salais in the 4ort galaxy._

32
hyperframe.json Normal file
View File

@ -0,0 +1,32 @@
{
"captions": true,
"voice": "af_brian",
"music_url": "",
"scenes": [
{
"id": "s1",
"start": 0,
"duration": 12,
"narration": "At noon in July, the air above Spokane holds no mercy. The soil temperature rises to thirty-eight degrees Celsius. Here, the question is not whether the water remains, but how much the sand allows to pass."
},
{
"id": "s2",
"start": 12,
"duration": 13,
"narration": "We measure this passage with Alpha. For our sandy loam, Alpha equals zero point zero three six inverse centimeters. This is the scale of the pores themselves, the geometry that dictates the speed of departure."
},
{
"id": "s3",
"start": 25,
"duration": 12,
"narration": "Field capacity is the upper bound: twenty-seven percent saturation. Wilting point is the lower: twelve percent. Between them lies the narrow corridor of survival, fifteen points of water available to the root."
},
{
"id": "s4",
"start": 37,
"duration": 10,
"narration": "This is not a story of scars. It is a calculation of thresholds. We do not heal the drought. We anticipate it. And we water accordingly."
}
],
"total_duration": 47
}

130
index.html Normal file
View File

@ -0,0 +1,130 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8"/>
<title>The Inland Guide: Soil Physics of Spokane Sandy Loam</title>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
:root {
--void: #0a0a0a;
--ink: #ffffff;
--accent: #d4af37; /* The color of dry wheat */
--border: 2px solid var(--ink);
}
body { margin: 0; background: var(--void); color: var(--ink); font-family: 'Courier New', Courier, monospace; overflow: hidden; }
#root { position: relative; width: 1920px; height: 1080px; overflow: hidden; }
.clip { position: absolute; inset: 0; display: grid; place-items: center; padding: 80px; box-sizing: border-box; }
/* Typography */
h1 { font-size: 80px; letter-spacing: -2px; line-height: 1.1; margin: 0 0 40px 0; text-transform: uppercase; }
p.data-label { font-size: 28px; margin-top: 40px; opacity: 0.8; max-width: 1200px; }
.metric { font-size: 42px; border-bottom: var(--border); padding-bottom: 10px; margin-top: 20px; display: inline-block; }
/* Visual Elements */
.stratum-grid {
position: absolute; bottom: 120px; left: 10%; width: 80%; height: 200px;
background: repeating-linear-gradient(0deg, transparent, transparent 19px, rgba(255,255,255,0.05) 20px);
border: var(--border);
}
.moisture-front {
position: absolute; bottom: 120px; left: 10%; width: 80%; height: 0%;
background: linear-gradient(to top, var(--accent), transparent);
transition: height 0.1s;
}
.alpha-circle {
width: 300px; height: 300px; border-radius: 50%; border: 4px dashed var(--ink);
display: flex; align-items: center; justify-content: center; font-size: 32px;
animation: rotate 20s linear infinite;
}
@keyframes rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
.water-bar {
width: 600px; height: 80px; border: var(--border); margin: 20px 0;
background: #111; position: relative;
}
.bar-fill {
position: absolute; left: 0; top: 0; bottom: 0; width: 0%;
background: var(--ink); transition: width 0.1s;
}
.title-card {
position: absolute; top: 40px; left: 40px;
border: var(--border); padding: 20px 40px; font-size: 48px; font-weight: bold;
}
</style>
</head>
<body>
<!-- ROOT COMPOSITION -->
<div id="root" data-composition-id="main" data-start="0" data-width="1920" data-height="1080" data-duration="47">
<!-- AUDIO TRACKS (REQUIRED FOR RENDER) -->
<audio id="voice-s1" src="audio/s1.wav" data-start="0"></audio>
<audio id="voice-s2" src="audio/s2.wav" data-start="12"></audio>
<audio id="voice-s3" src="audio/s3.wav" data-start="25"></audio>
<audio id="voice-s4" src="audio/s4.wav" data-start="37"></audio>
<!-- SCENE 1: THE PROBLEM -->
<section id="s1" class="clip" data-start="0" data-duration="12" data-track-index="1">
<div class="title-card" id="s1-title">SPokane<br>Sandy Loam</div>
<div class="stratum-grid" id="s1-grid"></div>
<div class="moisture-front" id="s1-water"></div>
<p class="data-label" id="s1-text">July Noon: 38°C | Evapotranspiration Rate Rising</p>
</section>
<!-- SCENE 2: THE CONSTANT -->
<section id="s2" class="clip" data-start="12" data-duration="13" data-track-index="1">
<h1 id="s2-title">Alpha = 0.036 cm⁻¹</h1>
<div class="alpha-circle" id="s2-ring">PORE SCALE DISTRIBUTION</div>
<p class="data-label" id="s2-text">The Geometry of Departure</p>
</section>
<!-- SCENE 3: THE CAPACITY -->
<section id="s3" class="clip" data-start="25" data-duration="12" data-track-index="1">
<h1 id="s3-title">Available Water</h1>
<div class="water-bar"><div class="bar-fill" id="s3-field"></div></div>
<p class="data-label">Field Capacity: 27%</p>
<div class="water-bar"><div class="bar-fill" id="s3-wilt"></div></div>
<p class="data-label">Wilting Point: 12%</p>
<p class="data-label metric" id="s3-range">Δ = 15% Survival Corridor</p>
</section>
<!-- SCENE 4: THE RESOLUTION -->
<section id="s4" class="clip" data-start="37" data-duration="10" data-track-index="1">
<h1 id="s4-title">Not Healing.<br>Anticipating.</h1>
<p class="data-label" id="s4-text">Water Accordingly.</p>
</section>
<!-- GSAP TIMELINE -->
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
// Scene 1: Heat and Moisture Loss
tl.to("#s1-title", { opacity: 1, duration: 1.5, ease: "power2.inOut" }, 0.2);
tl.to("#s1-grid", { opacity: 1, duration: 1 }, 0.5);
tl.to("#s1-water", { height: "40%", duration: 3, ease: "expo.out" }, 1);
tl.to("#s1-water", { height: "10%", duration: 5, ease: "power1.in" }, 4); // Rapid drying
tl.to("#s1-text", { opacity: 1, duration: 1 }, 2);
// Scene 2: The Alpha Constant
tl.to("#s2-title", { opacity: 1, scale: 1.1, duration: 2, ease: "back.out(1.7)" }, 12.2);
tl.to("#s2-ring", { rotation: 360, duration: 10, ease: "none" }, 12); // Continuous spin
tl.to("#s2-text", { opacity: 1, duration: 1 }, 13);
// Scene 3: The Range
tl.to("#s3-title", { opacity: 1, duration: 1 }, 25.2);
tl.to("#s3-field", { width: "27%", duration: 2, ease: "power2.out" }, 26);
tl.to("#s3-wilt", { width: "12%", duration: 2, ease: "power2.out" }, 28);
tl.to("#s3-range", { opacity: 1, scale: 1.2, duration: 1, ease: "elastic.out(1, 0.5)" }, 32);
// Scene 4: The Mandate
tl.to("#s4-title", { opacity: 1, letterSpacing: "0.5em", duration: 2, ease: "expo.in" }, 37.2);
tl.to("#s4-text", { opacity: 1, duration: 1 }, 40);
tl.to("body", { backgroundColor: "#1a1a1a", duration: 2 }, 44); // Fade out
window.__timelines["main"] = tl;
</script>
</div>
</body>
</html>