chore: removed course of empire from main db27095a
Steve · 2026-05-08 19:44 10 file(s) · +0 −473
bun.lock +0 −3
9 9
        "@astrojs/rss": "4.0.14",
10 10
        "@astrojs/ts-plugin": "1.10.6",
11 11
        "astro": "5.16.7",
12 -
        "gsap": "^3.15.0",
13 12
        "markdown-it": "^14.1.1",
14 13
        "rehype-external-links": "^3.0.0",
15 14
        "sanitize-html": "^2.17.3",
562 561
    "glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="],
563 562
564 563
    "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
565 -
566 -
    "gsap": ["gsap@3.15.0", "", {}, "sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A=="],
567 564
568 565
    "h3": ["h3@1.15.11", "", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="],
569 566
package.json +0 −1
31 31
		"@astrojs/rss": "4.0.14",
32 32
		"@astrojs/ts-plugin": "1.10.6",
33 33
		"astro": "5.16.7",
34 -
		"gsap": "^3.15.0",
35 34
		"markdown-it": "^14.1.1",
36 35
		"rehype-external-links": "^3.0.0",
37 36
		"sanitize-html": "^2.17.3",
src/assets/desolation.jpg (deleted) +0 −0

Binary file — no preview.

src/assets/destruction.jpg (deleted) +0 −0

Binary file — no preview.

src/assets/the-consumation-of-empire.jpg (deleted) +0 −0

Binary file — no preview.

src/assets/the-pastoral-state.jpg (deleted) +0 −0

Binary file — no preview.

src/assets/the-savage-state.jpg (deleted) +0 −0

Binary file — no preview.

src/components/page/EmpireScroll.astro (deleted) +0 −264
1 -
---
2 -
import { Image } from "astro:assets";
3 -
import { paintings } from "@/data/courseOfEmpire";
4 -
---
5 -
6 -
<article class="empire">
7 -
  {paintings.map((p, idx) => (
8 -
    <section
9 -
      class="empire-stage"
10 -
      data-slug={p.slug}
11 -
      data-details={p.details.length}
12 -
    >
13 -
      <div class="empire-frame" data-frame>
14 -
        <Image
15 -
          src={p.src}
16 -
          widths={[960, 1440, 1920, 2880]}
17 -
          sizes="100vw"
18 -
          formats={["avif", "webp"]}
19 -
          alt={`${p.title} by Thomas Cole`}
20 -
          loading={idx === 0 ? "eager" : "lazy"}
21 -
          fetchpriority={idx === 0 ? "high" : "auto"}
22 -
          decoding="async"
23 -
          class="empire-img"
24 -
        />
25 -
      </div>
26 -
27 -
      <figcaption class="empire-intro" data-intro>
28 -
        <span class="empire-counter">{String(idx + 1).padStart(2, "0")} / 05</span>
29 -
        <a class="empire-title" href={p.link} target="_blank" rel="noreferrer">
30 -
          {p.title} <span class="empire-year">({p.year})</span>
31 -
        </a>
32 -
        <p class="empire-desc">{p.intro}</p>
33 -
      </figcaption>
34 -
35 -
      {p.details.map((d, i) => (
36 -
        <figcaption class="empire-caption" data-caption data-idx={i}>
37 -
          <p>{d.caption}</p>
38 -
        </figcaption>
39 -
      ))}
40 -
    </section>
41 -
  ))}
42 -
</article>
43 -
44 -
<style>
45 -
  .empire {
46 -
    background: #0a0a0c;
47 -
    color: #f3f1ec;
48 -
  }
49 -
50 -
  .empire-stage {
51 -
    position: relative;
52 -
    height: 100vh;
53 -
    width: 100%;
54 -
    overflow: hidden;
55 -
  }
56 -
57 -
  .empire-frame {
58 -
    position: absolute;
59 -
    inset: 0;
60 -
    transform-origin: 0 0;
61 -
    will-change: transform;
62 -
  }
63 -
64 -
  .empire-img {
65 -
    width: 100%;
66 -
    height: 100%;
67 -
    object-fit: cover;
68 -
    display: block;
69 -
  }
70 -
71 -
  .empire-intro,
72 -
  .empire-caption {
73 -
    position: absolute;
74 -
    left: 1.5rem;
75 -
    bottom: 1.5rem;
76 -
    max-width: min(60ch, calc(100% - 3rem));
77 -
    padding: 1rem 1.25rem;
78 -
    background: rgba(10, 10, 12, 0.72);
79 -
    backdrop-filter: blur(8px);
80 -
    -webkit-backdrop-filter: blur(8px);
81 -
    border-left: 2px solid rgba(243, 241, 236, 0.6);
82 -
    opacity: 0;
83 -
    pointer-events: none;
84 -
    z-index: 2;
85 -
  }
86 -
87 -
  .empire-intro {
88 -
    pointer-events: auto;
89 -
  }
90 -
91 -
  .empire-counter {
92 -
    display: block;
93 -
    font-size: 0.75rem;
94 -
    letter-spacing: 0.12em;
95 -
    text-transform: uppercase;
96 -
    color: rgba(243, 241, 236, 0.55);
97 -
    margin-bottom: 0.4rem;
98 -
  }
99 -
100 -
  .empire-title {
101 -
    font-weight: 600;
102 -
    font-size: 1.05rem;
103 -
    color: #fff;
104 -
    text-decoration: none;
105 -
    display: inline-block;
106 -
    margin-bottom: 0.5rem;
107 -
  }
108 -
  .empire-title:hover { text-decoration: underline; }
109 -
110 -
  .empire-year {
111 -
    font-weight: 400;
112 -
    color: rgba(243, 241, 236, 0.55);
113 -
  }
114 -
115 -
  .empire-desc,
116 -
  .empire-caption p {
117 -
    font-size: 0.875rem;
118 -
    line-height: 1.6;
119 -
    color: rgba(243, 241, 236, 0.88);
120 -
    margin: 0;
121 -
  }
122 -
123 -
  @media (prefers-reduced-motion: reduce) {
124 -
    .empire-stage {
125 -
      height: auto;
126 -
    }
127 -
    .empire-frame {
128 -
      position: relative;
129 -
      transform: none !important;
130 -
    }
131 -
    .empire-img {
132 -
      height: auto;
133 -
      aspect-ratio: 3000 / 1865;
134 -
    }
135 -
    .empire-intro,
136 -
    .empire-caption {
137 -
      position: static;
138 -
      opacity: 1;
139 -
      max-width: none;
140 -
      margin: 0.75rem 1.5rem 0;
141 -
    }
142 -
  }
143 -
</style>
144 -
145 -
<script>
146 -
  import gsap from "gsap";
147 -
  import { ScrollTrigger } from "gsap/ScrollTrigger";
148 -
  import { paintings } from "@/data/courseOfEmpire";
149 -
150 -
  gsap.registerPlugin(ScrollTrigger);
151 -
152 -
  type Detail = { x: number; y: number; scale: number };
153 -
154 -
  const detailsBySlug: Record<string, Detail[]> = Object.fromEntries(
155 -
    paintings.map((p) => [
156 -
      p.slug,
157 -
      p.details.map((d) => ({ x: d.x, y: d.y, scale: d.scale })),
158 -
    ]),
159 -
  );
160 -
161 -
  function focusFor(d: Detail, w: number, h: number, scaleMult: number) {
162 -
    const s = 1 + (d.scale - 1) * scaleMult;
163 -
    return {
164 -
      scale: s,
165 -
      x: w / 2 - d.x * w * s,
166 -
      y: h / 2 - d.y * h * s,
167 -
    };
168 -
  }
169 -
170 -
  const triggers: ScrollTrigger[] = [];
171 -
172 -
  function buildTimelines(scaleMult: number, pinFactor: number) {
173 -
    triggers.forEach((t) => t.kill());
174 -
    triggers.length = 0;
175 -
176 -
    const stages = document.querySelectorAll<HTMLElement>(".empire-stage");
177 -
178 -
    stages.forEach((stage) => {
179 -
      const slug = stage.dataset.slug ?? "";
180 -
      const details = detailsBySlug[slug] ?? [];
181 -
      const frame = stage.querySelector<HTMLElement>("[data-frame]");
182 -
      const intro = stage.querySelector<HTMLElement>("[data-intro]");
183 -
      const captions = stage.querySelectorAll<HTMLElement>("[data-caption]");
184 -
      if (!frame) return;
185 -
186 -
      const w = stage.clientWidth;
187 -
      const h = stage.clientHeight;
188 -
189 -
      gsap.set(frame, { scale: 1, x: 0, y: 0 });
190 -
      gsap.set(intro, { opacity: 0 });
191 -
      gsap.set(captions, { opacity: 0 });
192 -
193 -
      const tl = gsap.timeline({
194 -
        scrollTrigger: {
195 -
          trigger: stage,
196 -
          start: "top top",
197 -
          end: `+=${window.innerHeight * (1 + details.length * pinFactor)}`,
198 -
          pin: true,
199 -
          scrub: 0.6,
200 -
          anticipatePin: 1,
201 -
        },
202 -
      });
203 -
204 -
      tl.to(intro, { opacity: 1, duration: 0.08 }, 0);
205 -
      tl.to(intro, { opacity: 0, duration: 0.06 }, 0.16);
206 -
207 -
      const slotStart = 0.22;
208 -
      const slotEnd = 0.94;
209 -
      const span = slotEnd - slotStart;
210 -
      const slot = span / Math.max(details.length, 1);
211 -
212 -
      details.forEach((d, i) => {
213 -
        const f = focusFor(d, w, h, scaleMult);
214 -
        const t0 = slotStart + i * slot;
215 -
        const tPan = slot * 0.4;
216 -
        const tHold = slot * 0.5;
217 -
        const cap = captions[i];
218 -
219 -
        tl.to(frame, { ...f, ease: "power2.inOut", duration: tPan }, t0);
220 -
        if (cap) {
221 -
          tl.to(cap, { opacity: 1, duration: tPan * 0.6 }, t0 + tPan * 0.4);
222 -
          tl.to(cap, { opacity: 0, duration: tPan * 0.4 }, t0 + tPan + tHold * 0.7);
223 -
        }
224 -
      });
225 -
226 -
      tl.to(
227 -
        frame,
228 -
        { scale: 1, x: 0, y: 0, ease: "power2.inOut", duration: 0.06 },
229 -
        0.94,
230 -
      );
231 -
232 -
      if (tl.scrollTrigger) triggers.push(tl.scrollTrigger);
233 -
    });
234 -
  }
235 -
236 -
  const mm = gsap.matchMedia();
237 -
238 -
  mm.add(
239 -
    {
240 -
      reduce: "(prefers-reduced-motion: reduce)",
241 -
      mobile: "(prefers-reduced-motion: no-preference) and (max-width: 768px)",
242 -
      desktop: "(prefers-reduced-motion: no-preference) and (min-width: 769px)",
243 -
    },
244 -
    (ctx) => {
245 -
      const c = ctx.conditions as {
246 -
        reduce: boolean;
247 -
        mobile: boolean;
248 -
        desktop: boolean;
249 -
      };
250 -
      if (c.reduce) return;
251 -
      const scaleMult = c.mobile ? 0.65 : 1;
252 -
      const pinFactor = c.mobile ? 0.7 : 1.0;
253 -
      buildTimelines(scaleMult, pinFactor);
254 -
      return () => {
255 -
        triggers.forEach((t) => t.kill());
256 -
        triggers.length = 0;
257 -
      };
258 -
    },
259 -
  );
260 -
261 -
  if (document.fonts?.ready) {
262 -
    document.fonts.ready.then(() => ScrollTrigger.refresh());
263 -
  }
264 -
</script>
src/data/courseOfEmpire.ts (deleted) +0 −173
1 -
import type { ImageMetadata } from "astro";
2 -
import savage from "@/assets/the-savage-state.jpg";
3 -
import pastoral from "@/assets/the-pastoral-state.jpg";
4 -
import consumation from "@/assets/the-consumation-of-empire.jpg";
5 -
import destruction from "@/assets/destruction.jpg";
6 -
import desolation from "@/assets/desolation.jpg";
7 -
8 -
export interface Detail {
9 -
	x: number;
10 -
	y: number;
11 -
	scale: number;
12 -
	caption: string;
13 -
}
14 -
15 -
export interface Painting {
16 -
	slug: string;
17 -
	src: ImageMetadata;
18 -
	title: string;
19 -
	year: string;
20 -
	intro: string;
21 -
	link: string;
22 -
	details: Detail[];
23 -
}
24 -
25 -
export const paintings: Painting[] = [
26 -
	{
27 -
		slug: "savage",
28 -
		src: savage,
29 -
		title: "The Savage State",
30 -
		year: "1834",
31 -
		intro:
32 -
			"Dawn breaks over a wilderness as nomadic hunters chase a wounded deer and storm clouds wreathe the mountain. Nature is supreme; man clings to its edges.",
33 -
		link: "https://explorethomascole.org/project/the-savage-state/",
34 -
		details: [
35 -
			{
36 -
				x: 0.28,
37 -
				y: 0.73,
38 -
				scale: 2.7,
39 -
				caption: "A hunter draws his bow. Skins, not stone — the only architecture is the chase.",
40 -
			},
41 -
			{
42 -
				x: 0.95,
43 -
				y: 0.40,
44 -
				scale: 2.2,
45 -
				caption: "Storm wreathes the peak. The mountain will outlast every empire to come.",
46 -
			},
47 -
			{
48 -
				x: 1.50,
49 -
				y: 0.50,
50 -
				scale: 2.6,
51 -
				caption: "Smoke from a clustered camp at the shore. The first faint claim on the land.",
52 -
			},
53 -
		],
54 -
	},
55 -
	{
56 -
		slug: "pastoral",
57 -
		src: pastoral,
58 -
		title: "The Pastoral or Arcadian State",
59 -
		year: "1834",
60 -
		intro:
61 -
			"Morning light, cleared fields, a Stonehenge-like temple, an old man tracing geometry in the dirt while a boy sketches a figure on stone. Civilization stirs; agriculture, science, art, all still close to the earth.",
62 -
		link: "https://explorethomascole.org/project/the-arcadian-or-pastoral-state/"
63 -
,
64 -
		details: [
65 -
			{
66 -
				x: 0.15,
67 -
				y: 0.90,
68 -
				scale: 2.6,
69 -
				caption: "An old man traces geometry in the dirt. Knowledge begins as scratched lines.",
70 -
			},
71 -
			{
72 -
				x: 0.6,
73 -
				y: 0.45,
74 -
				scale: 2.2,
75 -
				caption: "A trilithon temple on the rise. Ritual housed in stone for the first time.",
76 -
			},
77 -
			{
78 -
				x: 1.20,
79 -
				y: 0.7,
80 -
				scale: 2.5,
81 -
				caption: "Shepherds and a circle of dancers. Labor and leisure still share a field.",
82 -
			},
83 -
		],
84 -
	},
85 -
	{
86 -
		slug: "consummation",
87 -
		src: consumation,
88 -
		title: "The Consummation of Empire",
89 -
		year: "1836",
90 -
		intro:
91 -
			"High noon over a marble city. A triumphant ruler crosses the bridge in an elephant-drawn car, temples crowd the bay, the harbor brims with ships. Splendor so total it can only precede collapse.",
92 -
		link: "https://explorethomascole.org/project/the-consummation-of-empire/"
93 -
,
94 -
		details: [
95 -
			{
96 -
				x: 0.3,
97 -
				y: 0.70,
98 -
				scale: 2.6,
99 -
				caption: "The conqueror crosses the bridge in an elephant-drawn car. Power on parade.",
100 -
			},
101 -
			{
102 -
				x: 0.18,
103 -
				y: 0.4,
104 -
				scale: 2.3,
105 -
				caption: "Colonnades march to the water. Marble built to outlast the men who quarried it.",
106 -
			},
107 -
			{
108 -
				x: 0.82,
109 -
				y: 0.78,
110 -
				scale: 2.4,
111 -
				caption: "The harbor packed with galleys. Trade has become indistinguishable from triumph.",
112 -
			},
113 -
		],
114 -
	},
115 -
	{
116 -
		slug: "destruction",
117 -
		src: destruction,
118 -
		title: "Destruction",
119 -
		year: "1836",
120 -
		intro:
121 -
			"Storm and sack. Invaders pour through the gates, the temple porch becomes a catapult, ships burn in the harbor, a headless warrior presides over the slaughter as the sky turns to fire.",
122 -
		link: "https://explorethomascole.org/project/destruction/",
123 -
		details: [
124 -
			{
125 -
				x: 0.42,
126 -
				y: 0.58,
127 -
				scale: 2.6,
128 -
				caption: "The headless warrior. Even the colossus loses its face when the city falls.",
129 -
			},
130 -
			{
131 -
				x: 0.7,
132 -
				y: 0.75,
133 -
				scale: 2.5,
134 -
				caption: "Bridge collapses under the press of bodies. Architecture eats its makers.",
135 -
			},
136 -
			{
137 -
				x: 0.2,
138 -
				y: 0.35,
139 -
				scale: 2.2,
140 -
				caption: "Sky turns to fire. The same harbor — only the light has changed.",
141 -
			},
142 -
		],
143 -
	},
144 -
	{
145 -
		slug: "desolation",
146 -
		src: desolation,
147 -
		title: "Desolation",
148 -
		year: "1836",
149 -
		intro:
150 -
			"Sunset and moonrise over silent ruins. Vines climb a lone column, a bird nests where a temple stood, deer roam the broken friezes. No human figures remain. Nature outlasts everything man builds.",
151 -
		link: "https://explorethomascole.org/project/desolation/",
152 -
		details: [
153 -
			{
154 -
				x: 0.28,
155 -
				y: 0.5,
156 -
				scale: 2.5,
157 -
				caption: "A single column, ivy-wrapped. The last vertical line man left standing.",
158 -
			},
159 -
			{
160 -
				x: 0.68,
161 -
				y: 0.78,
162 -
				scale: 2.4,
163 -
				caption: "Deer cross the broken frieze. The wilderness has come back through the gate.",
164 -
			},
165 -
			{
166 -
				x: 0.55,
167 -
				y: 0.32,
168 -
				scale: 2.2,
169 -
				caption: "Moonrise where the temple stood. The mountain unchanged across all five.",
170 -
			},
171 -
		],
172 -
	},
173 -
];
src/pages/course-of-empire.astro (deleted) +0 −32
1 -
---
2 -
import PageLayout from "@/layouts/Base.astro";
3 -
import EmpireScroll from "@/components/page/EmpireScroll.astro";
4 -
5 -
const meta = {
6 -
	title: "The Course of Empire",
7 -
	description:
8 -
		"A scroll-driven tour through Thomas Cole's five-painting cycle.",
9 -
};
10 -
---
11 -
12 -
<PageLayout meta={meta}>
13 -
	<EmpireScroll />
14 -
	<style is:global>
15 -
		body.empire-page {
16 -
			padding: 0 !important;
17 -
      margin: 0 !important;
18 -
			max-width: none !important;
19 -
		}
20 -
		body.empire-page #main-header,
21 -
		body.empire-page footer {
22 -
			display: none !important;
23 -
		}
24 -
		body.empire-page #main {
25 -
			flex: none;
26 -
		}
27 -
	</style>
28 -
	<script>
29 -
		document.body.classList.add("empire-page");
30 -
		document.documentElement.classList.add("empire-html");
31 -
	</script>
32 -
</PageLayout>