Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions src/components/tree-rings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// Decorative dendrochronology cross-section for the home hero. Ring geometry
// is generated once at module load from a fixed seed so the figure is
// identical across renders and tests. Irregular ring spacing and wobble are
// deliberate — real growth rings vary year to year.

function mulberry32(seed: number) {
let a = seed
return () => {
a |= 0
a = (a + 0x6d2b79f5) | 0
let t = Math.imul(a ^ (a >>> 15), 1 | a)
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
}
}

const CX = 300
const CY = 300

function ringPath(base: number, rng: () => number): string {
const points: string[] = []
const n = 96
const amp1 = 1.5 + rng() * 2.5
const k1 = 2 + Math.floor(rng() * 3)
const ph1 = rng() * Math.PI * 2
const amp2 = 0.8 + rng() * 1.4
const k2 = 5 + Math.floor(rng() * 4)
const ph2 = rng() * Math.PI * 2
for (let i = 0; i < n; i++) {
const t = (i / n) * Math.PI * 2
const r =
base + amp1 * Math.sin(k1 * t + ph1) + amp2 * Math.sin(k2 * t + ph2)
points.push(
`${(CX + r * Math.cos(t)).toFixed(1)} ${(CY + r * Math.sin(t)).toFixed(1)}`
)
}
return `M${points.join("L")}Z`
}

interface Ring {
d: string
width: number
opacity: number
index: number
}

const RINGS: Ring[] = (() => {
const rng = mulberry32(7)
const rings: Ring[] = []
let radius = 66
let index = 0
while (radius < 258) {
radius += 5 + rng() * 10
rings.push({
d: ringPath(radius, rng),
// Occasional heavy ring reads as latewood.
width: rng() < 0.22 ? 2.6 : 1.2 + rng() * 0.7,
opacity: 0.45 + rng() * 0.3,
index: index++,
})
}
return rings
})()

const CAMBIUM = RINGS.length // green living layer, just under the bark
const BARK = RINGS.length + 1

export function TreeRings({ className }: { className?: string }) {
const rng = mulberry32(23)
const cambium = ringPath(266, rng)
const bark = ringPath(276, rng)

return (
<svg
viewBox="0 0 600 600"
aria-hidden="true"
className={`tree-rings ${className ?? ""}`}
>
<defs>
<clipPath id="tree-rings-pith">
<circle cx={CX} cy={CY} r={54} />
</clipPath>
</defs>

{RINGS.map((ring) => (
<path
key={ring.index}
d={ring.d}
pathLength={1}
fill="none"
stroke="var(--ring-ink)"
strokeWidth={ring.width}
strokeOpacity={ring.opacity}
style={{ "--ring-i": ring.index } as React.CSSProperties}
/>
))}

<path
d={cambium}
pathLength={1}
fill="none"
stroke="var(--tp-green)"
strokeWidth={2}
strokeOpacity={0.55}
style={{ "--ring-i": CAMBIUM } as React.CSSProperties}
/>
<path
d={bark}
pathLength={1}
fill="none"
stroke="var(--ring-ink)"
strokeWidth={6}
strokeOpacity={0.75}
style={{ "--ring-i": BARK } as React.CSSProperties}
/>

<circle
cx={CX}
cy={CY}
r={58}
fill="var(--pith)"
stroke="var(--ring-ink)"
strokeWidth={1.5}
strokeOpacity={0.6}
/>
{/* Oversized and positioned so the artwork's bottom caption band falls
below the clip circle entirely, leaving just the tree and figures. */}
<image
href="/images/tp_logo_transparent_bg.png"
x={CX - 75}
y={CY - 64}
width={150}
height={150}
clipPath="url(#tree-rings-pith)"
/>
</svg>
)
}
30 changes: 29 additions & 1 deletion src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@
--font-body: "Lora", Georgia, serif;

/* Texture knobs — paper grain overlays the page shell; wood tints the
header/footer accent strips. */
header/footer accent strips; ring-ink draws the hero cross-section. */
--paper-opacity: 0.14;
--wood: oklch(0.48 0.055 60);
--ring-ink: oklch(0.42 0.055 60);
/* Pith disc behind the hero woodcut — stays light in both modes so the
black-ink figures remain visible. */
--pith: oklch(0.95 0.02 85);

/* Green candidates — change --tp-green to compare */
--tp-green-mint: #4ade80;
Expand Down Expand Up @@ -63,6 +67,8 @@
.dark {
--paper-opacity: 0.11;
--wood: oklch(0.34 0.045 55);
--ring-ink: oklch(0.62 0.05 60);
--pith: oklch(0.87 0.035 80);
--background: oklch(0.16 0.01 50);
--foreground: oklch(0.95 0.005 80);
--card: oklch(0.19 0.01 50);
Expand Down Expand Up @@ -195,6 +201,28 @@
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='420' height='60'><filter id='w'><feTurbulence type='fractalNoise' baseFrequency='0.006 0.14' numOctaves='3' seed='7' stitchTiles='stitch'/><feColorMatrix type='saturate' values='0'/></filter><rect width='100%25' height='100%25' filter='url(%23w)' opacity='0.4'/></svg>");
}

/* Hero tree-ring cross-section — rings draw in from the pith outward on
load (each path has pathLength=1, so dasharray/offset of 1 hides it). */
.tree-rings path {
stroke-dasharray: 1;
stroke-dashoffset: 1;
animation: ring-draw 0.9s cubic-bezier(0.4, 0, 0.2, 1) forwards;
animation-delay: calc(var(--ring-i, 0) * 70ms);
}

@keyframes ring-draw {
to {
stroke-dashoffset: 0;
}
}

@media (prefers-reduced-motion: reduce) {
.tree-rings path {
animation: none;
stroke-dashoffset: 0;
}
}

/* Ghost Content API HTML styling */
.prose-ghost .kg-image-card img {
@apply rounded-lg shadow-md;
Expand Down
40 changes: 22 additions & 18 deletions src/pages/home/home-page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Link } from "@tanstack/react-router"
import { ArrowRight } from "lucide-react"
import { AuthorBio } from "@/components/author-bio"
import { TreeRings } from "@/components/tree-rings"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { usePosts } from "@/lib/use-ghost"
Expand All @@ -11,24 +12,27 @@ export function HomePage() {

return (
<div className="flex flex-1 flex-col">
{/* Hero */}
<section className="flex flex-col items-center justify-center gap-6 px-6 py-20 text-center lg:py-32">
<img
src="/images/treepolitics_logo.png"
alt="Tree Politics"
className="h-32 w-32 rounded-xl lg:h-40 lg:w-40"
/>
<h1 className="text-4xl sm:text-5xl lg:text-6xl">Tree Politics</h1>
<p className="text-muted-foreground max-w-2xl text-lg leading-relaxed lg:text-xl">
Woody Political Ecology, the use and abuse of trees in history, and
tree facts to impress your friends
</p>
<Button asChild size="lg" className="mt-2">
<Link to="/blog">
Read the Blog
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
{/* Hero — tree-ring cross-section; rings record history the way the
blog does. */}
<section className="px-6 py-16 lg:py-24">
<div className="mx-auto grid max-w-6xl items-center gap-12 lg:grid-cols-[minmax(0,1fr)_minmax(0,480px)]">
<div className="text-center lg:text-left">
<h1 className="text-6xl leading-[0.95] sm:text-7xl lg:text-8xl">
Tree <span className="block">Politics</span>
</h1>
<p className="text-muted-foreground mx-auto mt-6 max-w-xl text-lg leading-relaxed lg:mx-0 lg:text-xl">
Woody Political Ecology, the use and abuse of trees in history,
and tree facts to impress your friends
</p>
<Button asChild size="lg" className="mt-8">
<Link to="/blog">
Read the Blog
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</div>
<TreeRings className="mx-auto w-72 max-w-full sm:w-96 lg:w-full" />
</div>
</section>

{/* Recent Posts */}
Expand Down
Loading