diff --git a/.agents/skills/svg-animations/SKILL.md b/.agents/skills/svg-animations/SKILL.md new file mode 100644 index 0000000..ed5a409 --- /dev/null +++ b/.agents/skills/svg-animations/SKILL.md @@ -0,0 +1,421 @@ +--- +name: svg-animations +description: Create beautiful, performant SVG animations and illustrations. Use this skill when the user asks to create SVG graphics, icons, illustrations, animated logos, path animations, morphing shapes, loading spinners, or any animated SVG content. Covers SMIL animations, CSS-driven SVG animation, path drawing effects, shape morphing, motion paths, gradients, masks, and filters. +--- + +This skill guides creation of handcrafted SVG animations — from simple animated icons to complex multi-stage path animations. SVGs are a markup language for images; every element is a DOM node you can style, animate, and script. + +## SVG Fundamentals + +### Coordinate System +SVGs use a coordinate system defined by `viewBox="minX minY width height"`. The viewBox is your canvas — all coordinates are relative to it, making SVGs resolution-independent. + +```svg + + + +``` + +### Shape Primitives + +```svg + + + + + + +``` + +### The `` Element — The Power Tool + +The `d` attribute defines a path using commands. Uppercase = absolute, lowercase = relative. + +| Command | Purpose | Syntax | +|---------|---------|--------| +| M/m | Move to | `M x y` | +| L/l | Line to | `L x y` | +| H/h | Horizontal line | `H x` | +| V/v | Vertical line | `V y` | +| C/c | Cubic bézier | `C x1 y1, x2 y2, x y` | +| S/s | Smooth cubic bézier | `S x2 y2, x y` | +| Q/q | Quadratic bézier | `Q x1 y1, x y` | +| T/t | Smooth quadratic | `T x y` | +| A/a | Elliptical arc | `A rx ry rotation large-arc sweep x y` | +| Z/z | Close path | `Z` | + +**Cubic Bézier** (`C`): Two control points define the curve. The first control point sets the departure angle, the second sets the arrival angle. + +```svg + +``` + +**Smooth Cubic** (`S`): Reflects the previous control point automatically — perfect for chaining fluid S-curves. + +```svg + +``` + +**Arc** (`A`): `rx ry x-rotation large-arc-flag sweep-flag x y` +- `large-arc-flag`: 0 = small arc, 1 = large arc (>180°) +- `sweep-flag`: 0 = counterclockwise, 1 = clockwise + +```svg + + +``` + +### Grouping and Transforms + +```svg + + + +``` + +Use `` to group elements for collective transforms, styling, and animation targets. + +### Gradients, Masks, and Filters + +```svg + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +--- + +## CSS Animations on SVG + +Many SVG attributes are valid CSS properties: `fill`, `stroke`, `opacity`, `transform`, `stroke-dasharray`, `stroke-dashoffset`, etc. + +### Basic CSS Animation + +```css +.pulse { + animation: pulse 2s ease-in-out infinite; + transform-origin: center; +} +@keyframes pulse { + 0%, 100% { transform: scale(1); opacity: 1; } + 50% { transform: scale(1.15); opacity: 0.7; } +} +``` + +### Stroke Drawing Animation (The Classic) + +The most iconic SVG animation. Uses `stroke-dasharray` and `stroke-dashoffset` to make a path appear to draw itself. + +**How it works:** +1. Set `stroke-dasharray` to the path's total length (one giant dash + one giant gap) +2. Set `stroke-dashoffset` to the same length (shifts the dash off-screen) +3. Animate `stroke-dashoffset` to 0 (slides the dash into view) + +```svg + + + + + +``` + +**Getting exact path length in JS:** +```js +const path = document.querySelector('.draw'); +const length = path.getTotalLength(); +path.style.strokeDasharray = length; +path.style.strokeDashoffset = length; +``` + +### Staggered Multi-Path Drawing + +```css +.line-1 { animation-delay: 0s; } +.line-2 { animation-delay: 0.3s; } +.line-3 { animation-delay: 0.6s; } +``` + +### CSS `d` Property Animation + +Modern browsers support animating the `d` attribute directly in CSS: + +```css +path { + d: path("M 10,30 A 20,20 0,0,1 50,30 A 20,20 0,0,1 90,30 Q 90,60 50,90 Q 10,60 10,30 z"); + transition: d 0.5s ease; +} +path:hover { + d: path("M 10,50 A 20,20 0,0,1 50,10 A 20,20 0,0,1 90,50 Q 90,80 50,100 Q 10,80 10,50 z"); +} +``` + +**Requirement:** Both paths must have the same number and types of commands for interpolation to work. + +--- + +## SMIL Animations (Native SVG) + +SMIL animations are declared directly inside SVG markup. They work even when SVG is loaded as an `` or CSS `background-image` — where CSS and JS can't reach. + +### `` — Animate Any Attribute + +```svg + + + +``` + +With keyframes: +```svg + +``` + +### `` — Transform Animations + +```svg + + + +``` + +Types: `translate`, `scale`, `rotate`, `skewX`, `skewY` + +### `` — Move Along a Path + +```svg + + + + + + +``` + +`rotate="auto"` orients the element tangent to the path. `rotate="auto-reverse"` flips it 180°. + +### `` — Discrete Value Changes + +```svg + + + +``` + +### Timing and Synchronization + +```svg + + + + +``` + +Trigger values: +- `begin="click"` — on click +- `begin="2s"` — after 2 seconds +- `begin="other.end"` — when another animation ends +- `begin="other.end + 1s"` — 1s after another ends +- `begin="other.repeat(2)"` — on 2nd repeat of another + +### Easing with `calcMode` and `keySplines` + +```svg + +``` + +`calcMode` options: `linear` (default), `discrete`, `paced`, `spline` + +`keySplines` takes cubic-bezier control points (x1 y1 x2 y2) per interval. Common easings: +- Ease-in-out: `0.42 0 0.58 1` +- Ease-out: `0 0 0.58 1` +- Bounce-ish: `0.34 1.56 0.64 1` + +### Shape Morphing with SMIL + +Both shapes must have identical command structures (same number of points, same command types): + +```svg + + + +``` + +--- + +## Animation Patterns & Recipes + +### Loading Spinner + +```svg + + + + + + +``` + +### Animated Checkmark + +```svg + + + + +``` + +### Morphing Hamburger to X + +```svg + + + + + + + + + + + +``` + +### Gradient Animation (Color Shift) + +```svg + + + + + + + + + + + +``` + +### Breathing / Pulsing Glow + +```svg + + + + +``` + +### Wave / Liquid Effect + +```svg + + + +``` + +--- + +## Best Practices + +1. **Use `viewBox`, never hardcode `width`/`height` in the SVG** — let the container size it. This keeps it resolution-independent. + +2. **`` for reusable definitions** — gradients, filters, masks, clipPaths, and reusable shapes belong in ``. + +3. **Prefer SMIL for self-contained SVGs** (icons, logos loaded via ``) — CSS/JS won't work there. Use CSS animations when the SVG is inlined and you want to coordinate with the rest of the page. + +4. **Shape morphing requires matching commands** — same number of path commands, same types, same order. If shapes differ, add invisible intermediate points to equalize. + +5. **`stroke-linecap="round"`** makes line animations look polished. + +6. **`fill="freeze"`** in SMIL keeps the final animation state. Without it, the element snaps back. + +7. **`transform-origin: center`** in CSS — SVG transforms default to the origin (0,0), not the element center. Always set this explicitly. + +8. **Use `getTotalLength()`** in JS to get exact path lengths for stroke animations instead of guessing. + +9. **Layer animations with `` groups** — animate the group transform separately from individual element properties for complex choreography. + +10. **Performance:** SVG animations are GPU-composited when animating `transform` and `opacity`. Animating `d`, `points`, or layout attributes triggers repaints — use sparingly on complex SVGs. + +11. **`will-change: transform`** on animated SVG elements helps the browser optimize compositing. + +12. **Accessibility:** Add `role="img"` and `` / `<desc>` elements. Use `prefers-reduced-motion` media query to disable animations for users who request it: + +```css +@media (prefers-reduced-motion: reduce) { + svg * { + animation: none !important; + transition: none !important; + } +} +``` diff --git a/.gitignore b/.gitignore index 6aabe90..2aab1c8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,27 @@ +# Zola build output (Wrangler deploy source) public/ -.wrangler/ + +# Dependencies node_modules/ + +# Cloudflare +.wrangler/ +.dev.vars + +# Environment / secrets +.env +.env.* + +# OS junk +.DS_Store +Thumbs.db + +# Logs +*.log + +# Theme static: only the Vite-built bundle is source-controlled. +# Do not copy Zola public/ output here — it overrides the real homepage on build. +themes/openfreebooks/static/* +!themes/openfreebooks/static/js/ +themes/openfreebooks/static/js/* +!themes/openfreebooks/static/js/bundle.js diff --git a/data/contributors.json b/data/contributors.json new file mode 100644 index 0000000..7c0eec1 --- /dev/null +++ b/data/contributors.json @@ -0,0 +1,23 @@ +{ + "contributors": [ + { + "name": "Man Lung Yeung", + "avatar": "contributors/manlung.png", + "website": "https://manlung.work", + "job_title": "EdTech specialist / software engineer", + "education": "BEng in Computer Science (HKUST) + BBA (HKUST)", + "contributions": [ + "Founder", + "Math textbook", + "Physics textbook", + "Chemistry textbook" + ], + "socials": [ + { "type": "email", "url": "manlung.yeung@outlook.com" }, + { "type": "github", "url": "https://github.com/hananoshikaYomaru" }, + { "type": "x", "url": "https://x.com/yomaru_1999" }, + { "type": "youtube", "url": "https://www.youtube.com/@yomaru_1999" } + ] + } + ] +} diff --git a/data/contributors.types.ts b/data/contributors.types.ts new file mode 100644 index 0000000..82e5649 --- /dev/null +++ b/data/contributors.types.ts @@ -0,0 +1,51 @@ +/** + * Contributor data for Open Free Books (`data/contributors.json`). + * + * Use `website` for a single personal site. Put email, GitHub, X, and other + * profiles in `socials`. + */ + +export const CONTRIBUTOR_SOCIAL_TYPES = [ + "email", + "github", + "gitlab", + "x", + "youtube", + "bluesky", + "linkedin", + "mastodon", + "instagram", + "facebook", + "threads", + "tiktok", + "discord", + "orcid", + "scholar", + "medium", + "substack", + "rss", +] as const; + +export type ContributorSocialType = (typeof CONTRIBUTOR_SOCIAL_TYPES)[number]; + +export interface ContributorSocial { + type: ContributorSocialType; + /** Email address (no `mailto:`) or full https URL for other networks */ + url: string; +} + +export interface Contributor { + id?: string; + name: string; + avatar?: string; + /** Single personal website URL */ + website?: string; + job_title?: string; + education?: string; + contributions?: string[]; + socials?: ContributorSocial[]; +} + +export interface ContributorsFile { + contributors: Contributor[]; +} diff --git a/frontend/src/components/contributors-dialog.tsx b/frontend/src/components/contributors-dialog.tsx new file mode 100644 index 0000000..04bb45d --- /dev/null +++ b/frontend/src/components/contributors-dialog.tsx @@ -0,0 +1,44 @@ +import { onMount } from "solid-js"; + +function openContributorDialog(dialogId: string) { + const dialog = document.getElementById(dialogId); + if (dialog instanceof HTMLDialogElement) { + dialog.showModal(); + } +} + +function wireContributorDialogs() { + const buttons = document.querySelectorAll<HTMLButtonElement>("[data-contributor-dialog]"); + const dialogs = document.querySelectorAll<HTMLDialogElement>(".contributor-dialog"); + + const onButtonClick = (event: Event) => { + const button = event.currentTarget; + if (!(button instanceof HTMLButtonElement)) return; + const dialogId = button.dataset.contributorDialog; + if (dialogId) openContributorDialog(dialogId); + }; + + const onDialogClick = (event: MouseEvent) => { + const dialog = event.currentTarget; + if (!(dialog instanceof HTMLDialogElement)) return; + if (event.target === dialog) { + dialog.close(); + } + }; + + buttons.forEach((button) => button.addEventListener("click", onButtonClick)); + dialogs.forEach((dialog) => dialog.addEventListener("click", onDialogClick)); + + return () => { + buttons.forEach((button) => button.removeEventListener("click", onButtonClick)); + dialogs.forEach((dialog) => dialog.removeEventListener("click", onDialogClick)); + }; +} + +export function ContributorsDialogBootstrap() { + onMount(() => { + return wireContributorDialogs(); + }); + + return null; +} diff --git a/frontend/src/components/scroll-reveal.tsx b/frontend/src/components/scroll-reveal.tsx index 7bc4ce3..d452c84 100644 --- a/frontend/src/components/scroll-reveal.tsx +++ b/frontend/src/components/scroll-reveal.tsx @@ -1,5 +1,35 @@ import { onCleanup, onMount } from "solid-js"; +const STAGGER_MS = 80; + +function getStaggerGroup(card: HTMLElement): HTMLElement { + return ( + card.closest("[data-scroll-reveal]") ?? + card.closest("section") ?? + card.parentElement ?? + document.body + ); +} + +function isInViewport(el: HTMLElement): boolean { + const rect = el.getBoundingClientRect(); + return rect.top < window.innerHeight && rect.bottom > 0; +} + +function groupCards(cards: HTMLElement[]): Map<HTMLElement, HTMLElement[]> { + const groups = new Map<HTMLElement, HTMLElement[]>(); + for (const card of cards) { + const group = getStaggerGroup(card); + const list = groups.get(group); + if (list) { + list.push(card); + } else { + groups.set(group, [card]); + } + } + return groups; +} + export function ScrollRevealBootstrap() { onMount(() => { const cards = document.querySelectorAll<HTMLElement>("[data-scroll-reveal-card]"); @@ -23,10 +53,20 @@ export function ScrollRevealBootstrap() { { threshold: 0.14 } ); - cards.forEach((card, index) => { - card.style.transitionDelay = `${index * 80}ms`; - observer.observe(card); - }); + const groups = groupCards([...cards]); + + for (const groupCards of groups.values()) { + groupCards.forEach((card, index) => { + if (isInViewport(card)) { + card.style.transitionDelay = "0ms"; + card.classList.add("is-inview"); + return; + } + + card.style.transitionDelay = `${index * STAGGER_MS}ms`; + observer.observe(card); + }); + } onCleanup(() => observer.disconnect()); }); diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 274fbee..b5bc63a 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,6 +1,7 @@ import { render } from "solid-js/web"; import { SiteHeader } from "./components/site-header"; import { Marquee } from "./components/marquee"; +import { ContributorsDialogBootstrap } from "./components/contributors-dialog"; import { ScrollRevealBootstrap } from "./components/scroll-reveal"; import { initTheme } from "./theme"; @@ -56,4 +57,5 @@ if (marqueeRoot) { } render(() => <ScrollRevealBootstrap />, document.body); +render(() => <ContributorsDialogBootstrap />, document.body); mountFooterYear(); diff --git a/skills-lock.json b/skills-lock.json index c02a43c..d67c373 100644 --- a/skills-lock.json +++ b/skills-lock.json @@ -24,6 +24,12 @@ "sourceType": "github", "skillPath": "microinteractions/SKILL.md", "computedHash": "875eca02a058bca5467850587d6c7aed62f3de1cd9f509f52c95253ec267b518" + }, + "svg-animations": { + "source": "supermemoryai/skills", + "sourceType": "github", + "skillPath": "svg-animations/SKILL.md", + "computedHash": "9be4b3e0afe9d3cbb1bcf238f8504f9a23552f742350fbef9483070112ac1977" } } } diff --git a/static/contributors.json b/static/contributors.json new file mode 100644 index 0000000..c30c4b3 --- /dev/null +++ b/static/contributors.json @@ -0,0 +1,23 @@ +{ + "contributors": [ + { + "name": "Man Lung Yeung", + "avatar": "contributors/manlung.png", + "website": "https://manlung.work", + "job_title": "EdTech specialist / software engineer", + "education": "BEng in Computer Science (HKUST) + BBA (HKUST)", + "contributions": [ + "Project Creator", + "Math textbook", + "Physics textbook", + "Chemistry textbook" + ], + "socials": [ + { "type": "email", "url": "manlung.yeung@outlook.com" }, + { "type": "github", "url": "https://github.com/hananoshikaYomaru" }, + { "type": "x", "url": "https://x.com/yomaru_1999" }, + { "type": "youtube", "url": "https://www.youtube.com/@yomaru_1999" } + ] + } + ] +} diff --git a/static/contributors/manlung.png b/static/contributors/manlung.png new file mode 100644 index 0000000..51d8aae Binary files /dev/null and b/static/contributors/manlung.png differ diff --git a/themes/openfreebooks/sass/_contributors.scss b/themes/openfreebooks/sass/_contributors.scss new file mode 100644 index 0000000..0efd28e --- /dev/null +++ b/themes/openfreebooks/sass/_contributors.scss @@ -0,0 +1,303 @@ +.why-section--community { + --community-block-gap: 72px; +} + +.why-section--community .why-craft-logos-wrap { + margin-top: var(--community-block-gap); +} + +.why-section--community .community-contributors { + margin-top: var(--community-block-gap); + padding-bottom: 12px; +} + +.contributors-intro { + max-width: var(--prose-width); + margin: 0 0 24px; + color: var(--soft-ink); + font-size: 1.05rem; + line-height: 1.72; +} + +.contributors-avatar-grid { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin: 0; + padding: 0; + list-style: none; +} + +.contributors-avatar-item { + opacity: 0; + translate: 0 12px; + transition: opacity 400ms ease, translate 400ms ease; +} + +.contributors-avatar-item.is-inview { + opacity: 1; + translate: 0 0; +} + +.contributor-avatar-btn { + display: block; + margin: 0; + padding: 0; + border: 2px solid transparent; + border-radius: 50%; + background: none; + cursor: pointer; + line-height: 0; + transition: border-color 160ms ease, transform 160ms ease; +} + +.contributor-avatar-btn:hover, +.contributor-avatar-btn:focus-visible { + border-color: var(--accent); + transform: translateY(-2px); + outline: none; +} + +.contributor-avatar-img, +.contributor-avatar-fallback { + display: block; + width: 48px; + height: 48px; + border-radius: 50%; +} + +.contributor-avatar-img { + object-fit: cover; + border: 1px solid var(--card-border); + background: var(--paper); +} + +.contributor-avatar-fallback { + display: flex; + align-items: center; + justify-content: center; + background: var(--card-bg); + border: 1px solid var(--card-border); + color: var(--accent); + font-family: var(--font-heading); + font-size: 1.25rem; + font-weight: 400; + text-transform: uppercase; +} + +.contributor-dialog { + margin: auto; + padding: 0; + border: none; + max-width: min(92vw, 440px); + background-color: #f5f4f1; + color: var(--ink); + isolation: isolate; +} + +[data-theme="dark"] .contributor-dialog { + background-color: #1a1917; +} + +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) .contributor-dialog { + background-color: #1a1917; + } +} + +.contributor-dialog::backdrop { + background: rgba(26, 26, 26, 0.45); +} + +[data-theme="dark"] .contributor-dialog::backdrop { + background: rgba(0, 0, 0, 0.62); +} + +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) .contributor-dialog::backdrop { + background: rgba(0, 0, 0, 0.62); + } +} + +.contributor-dialog-panel { + display: block; + width: 100%; + box-sizing: border-box; + position: relative; + margin: 0; + padding: 28px 28px 24px; + border: 1px solid var(--card-border); + background-color: #f5f4f1; + box-shadow: 0 18px 48px var(--shadow-soft); +} + +[data-theme="dark"] .contributor-dialog-panel { + background-color: #1a1917; +} + +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) .contributor-dialog-panel { + background-color: #1a1917; + } +} + +.contributor-dialog-close { + position: absolute; + top: 12px; + right: 12px; + width: 36px; + height: 36px; + margin: 0; + padding: 0; + border: none; + border-radius: 50%; + background: transparent; + color: var(--muted); + font-size: 1.5rem; + line-height: 1; + cursor: pointer; +} + +.contributor-dialog-close:hover, +.contributor-dialog-close:focus-visible { + color: var(--ink); + background: var(--paper); + outline: none; +} + +.contributor-dialog-header { + display: flex; + align-items: center; + gap: 18px; + margin-bottom: 18px; + padding-right: 28px; +} + +.contributor-dialog-avatar { + flex-shrink: 0; + width: 80px; + height: 80px; + border-radius: 50%; + object-fit: cover; + border: 1px solid var(--card-border); + background: var(--paper); +} + +.contributor-dialog-name { + margin: 0 0 6px; + font-family: var(--font-heading); + font-size: clamp(1.35rem, 2vw, 1.75rem); + font-weight: 400; + line-height: 1.2; +} + +.contributor-dialog-job { + margin: 0; + color: var(--accent); + font-family: var(--mono); + font-size: 0.72rem; + letter-spacing: 0.1em; + text-transform: uppercase; + font-weight: 700; +} + +.contributor-dialog-meta, +.contributor-dialog-contributions { + margin: 0 0 14px; + color: var(--soft-ink); + font-size: 0.98rem; + line-height: 1.65; +} + +.contributor-meta-label { + display: block; + margin-bottom: 4px; + font-family: var(--mono); + font-size: 0.68rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--muted); + font-weight: 700; +} + +.contributor-contribution-list { + margin: 0; + padding: 0; + list-style: none; +} + +.contributor-contribution-list li { + position: relative; + padding-left: 1rem; + margin-bottom: 0.35rem; +} + +.contributor-contribution-list li::before { + content: "—"; + position: absolute; + left: 0; + color: var(--accent); +} + +.contributor-dialog-connect { + margin-top: 18px; + padding-top: 16px; + border-top: 1px solid var(--line); +} + +.contributor-social-list { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin: 8px 0 0; + padding: 0; + list-style: none; +} + +.contributor-social-link { + display: inline-flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border: 1px solid var(--card-border); + border-radius: 50%; + color: var(--ink); + text-decoration: none; + background: var(--paper); + transition: border-color 160ms ease, color 160ms ease, transform 160ms ease; +} + +.contributor-social-link:hover, +.contributor-social-link:focus-visible { + border-color: var(--accent); + color: var(--accent); + transform: translateY(-1px); + outline: none; +} + +.contributor-social-icon { + display: block; + flex-shrink: 0; +} + +.why-section--community .section-note a { + color: var(--accent); +} + +.why-section--community .community-contributors .section-note { + margin-top: 28px; +} + +@media (prefers-reduced-motion: reduce) { + .contributors-avatar-item { + opacity: 1; + translate: 0 0; + transition: none; + } + + .contributor-avatar-btn:hover, + .contributor-avatar-btn:focus-visible { + transform: none; + } +} diff --git a/themes/openfreebooks/sass/_grain.scss b/themes/openfreebooks/sass/_grain.scss index 036fe65..157f3b7 100644 --- a/themes/openfreebooks/sass/_grain.scss +++ b/themes/openfreebooks/sass/_grain.scss @@ -10,6 +10,12 @@ body::before { background-size: 180px 180px; } +/* Top-layer modals must stay opaque; grain blend would otherwise show through. */ +.contributor-dialog[open], +.contributor-dialog[open] .contributor-dialog-panel { + mix-blend-mode: normal; +} + [data-theme="dark"] body::before { opacity: 0.45; mix-blend-mode: soft-light; diff --git a/themes/openfreebooks/sass/_why-sections.scss b/themes/openfreebooks/sass/_why-sections.scss new file mode 100644 index 0000000..cf82fa9 --- /dev/null +++ b/themes/openfreebooks/sass/_why-sections.scss @@ -0,0 +1,267 @@ +.why-section { + padding: 64px 0; + border-top: 1px solid var(--line); +} + +.why-row { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: clamp(32px, 5vw, 56px); + align-items: center; +} + +.why-row--art-end .why-art { + order: 2; +} + +.why-row--art-end .why-copy { + order: 1; +} + +.why-art { + display: flex; + justify-content: center; + align-items: center; + opacity: 0; + translate: 0 20px; + transition: opacity 500ms ease, translate 500ms ease; +} + +.why-art.is-inview { + opacity: 1; + translate: 0 0; +} + +.why-art svg { + width: min(100%, 280px); + height: auto; + display: block; +} + +.why-copy { + max-width: var(--prose-width); + opacity: 0; + translate: 0 20px; + transition: opacity 500ms ease, translate 500ms ease; +} + +.why-copy.is-inview { + opacity: 1; + translate: 0 0; +} + +.why-copy .section-title { + margin-bottom: 16px; +} + +.why-copy p { + margin: 0 0 1rem; + color: var(--soft-ink); + font-size: 1.05rem; + line-height: 1.72; +} + +.why-copy p:last-child { + margin-bottom: 0; +} + +.why-copy strong { + color: var(--ink); + font-weight: 600; +} + +.why-section--community .why-craft-logos-wrap { + overflow: hidden; + opacity: 0; + translate: 0 20px; + transition: opacity 500ms ease, translate 500ms ease; +} + +.why-section--community .why-craft-logos-wrap.is-inview { + opacity: 1; + translate: 0 0; +} + +.university-logos-label { + margin: 0 0 24px; + text-align: center; + font-family: var(--mono); + font-size: 0.68rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--muted); +} + +.university-carousel { + display: flex; + flex-direction: column; + gap: 12px; + width: 100vw; + margin-left: calc(-50vw + 50%); + overflow: hidden; +} + +.university-carousel-row { + overflow: hidden; + mask-image: linear-gradient( + 90deg, + transparent 0%, + #000 6%, + #000 94%, + transparent 100% + ); +} + +.university-carousel-track { + display: flex; + width: max-content; + animation: university-carousel-forward 52s linear infinite; +} + +.university-carousel-row--reverse .university-carousel-track { + animation-name: university-carousel-reverse; + animation-duration: 58s; +} + +.university-carousel-row--slow .university-carousel-track { + animation-duration: 64s; +} + +.university-carousel-group { + display: flex; + flex-shrink: 0; + align-items: center; + gap: 12px; + margin: 0; + padding: 0 6px; + list-style: none; +} + +@keyframes university-carousel-forward { + from { + transform: translateX(0); + } + to { + transform: translateX(-50%); + } +} + +@keyframes university-carousel-reverse { + from { + transform: translateX(-50%); + } + to { + transform: translateX(0); + } +} + +.university-logo { + display: flex; + align-items: center; + justify-content: center; + min-height: 56px; + padding: 10px 12px; + background: var(--card-bg); + border: 1px solid var(--card-border); +} + +.university-logo svg { + width: 108px; + height: auto; + display: block; + color: var(--ink); +} + +$university-colors: ( + mit: (#a31f34, #e85a6f), + harvard: (#a51c30, #e88a96), + stanford: (#8c1515, #e88a8a), + berkeley: (#003262, #6a9fd4), + caltech: (#ff6c0c, #f5a623), + princeton: (#e77500, #f0a040), + yale: (#00356b, #6a9fd4), + columbia: (#003865, #7eb3e8), + cornell: (#b31b1b, #e85a6f), + cmu: (#c41230, #e87888), + oxford: (#002147, #7eb3e8), + cambridge: (#0072cf, #6ab0f0), + imperial: (#003e74, #6a9fd4), + lse: (#e41f13, #f07870), + eth: (#1f407a, #6a9fd4), + toronto: (#002a5c, #6a9fd4), + mcgill: (#ed1b2f, #f07888), + hku: (#006747, #3cb88a), + hkust: (#003366, #6a9fd4), + cuhk: (#7b2d8e, #c49ae0), + nus: (#ef7c00, #f5a623), + tsinghua: (#82318e, #c49ae0), + pku: (#94070a, #e87878), + utokyo: (#003f87, #6a9fd4), + anu: (#c4a337, #e8c860), + melbourne: (#094183, #6a9fd4), + iitb: (#003366, #6a9fd4) +); + +@each $slug, $pair in $university-colors { + .university-logo--#{$slug} { + color: nth($pair, 1); + } + + [data-theme="dark"] .university-logo--#{$slug} { + color: nth($pair, 2); + } +} + +@media (prefers-color-scheme: dark) { + @each $slug, $pair in $university-colors { + :root:not([data-theme="light"]) .university-logo--#{$slug} { + color: nth($pair, 2); + } + } +} + +@media (max-width: 900px) { + .why-row, + .why-row--art-end { + grid-template-columns: 1fr; + } + + .why-row--art-end .why-art, + .why-row--art-end .why-copy { + order: unset; + } + + .why-art svg { + width: min(100%, 220px); + } +} + +@media (prefers-reduced-motion: reduce) { + .why-art, + .why-copy, + .why-section--community .why-craft-logos-wrap { + opacity: 1; + translate: 0 0; + transition: none; + } + + .university-carousel-track { + animation: none; + flex-wrap: wrap; + width: 100%; + justify-content: center; + } + + .university-carousel-row { + mask-image: none; + } + + .university-carousel-group[aria-hidden="true"] { + display: none; + } + + .university-carousel { + width: 100%; + margin-left: 0; + } +} diff --git a/themes/openfreebooks/sass/main.scss b/themes/openfreebooks/sass/main.scss index c0708d6..96aafe4 100644 --- a/themes/openfreebooks/sass/main.scss +++ b/themes/openfreebooks/sass/main.scss @@ -2,3 +2,5 @@ @import "grain"; @import "layout"; @import "components"; +@import "why-sections"; +@import "contributors"; diff --git a/themes/openfreebooks/static/index.html b/themes/openfreebooks/static/index.html index 9ab313c..7456d95 100644 --- a/themes/openfreebooks/static/index.html +++ b/themes/openfreebooks/static/index.html @@ -58,6 +58,538 @@ <div id="marquee"></div> +<section class="why-section" id="why-textbook-costs"> + <div class="container why-row"> + <div class="why-art" data-scroll-reveal-card> + <svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg" role="img" aria-hidden="true"> + <title>Stack of textbooks and cost + + + + + + + $ + + + + + +
+

The problem

+

Textbook fees hit families hard

+

+ In many countries, families pay hundreds of dollars per child each year for printed textbooks — + on top of tuition, devices, and supplies. When money is tight, students share worn copies, skip + buying books, or fall behind before the term begins. +

+

+ That cost is not a small inconvenience. It is a barrier to showing up prepared. Open Free Books + exists so every learner can start the year with complete materials, not a receipt. +

+
+ + + +
+
+
+ + +
+
+

The medium

+

Long live the web — not the PDF

+

+ Paper textbooks are fixed: one layout, one edition, no room to fix a typo until the next print run. + Most “digital” versions are the same pages exported to PDF — still static, still hard to search, + still impossible to embed a simulation or a live diagram. +

+

+ HTML on the open web is different. Chapters can link to sources, resize for any screen, + include interactive figures, and improve over time. The book becomes something you use — not something + you only carry. +

+
+
+
+ +
+
+
+ + +
+
+

People & books

+

Replace textbooks, not teachers

+

+ A great teacher — whether in person or, someday, assisted by thoughtful tools — still sets pace, + asks hard questions, and notices when a student is stuck. That relationship is not what we are + trying to automate. +

+

+ Textbooks are different. They are reference and structure: explanations, exercises, diagrams. + Those can be free, forkable, and kept current on the web so teachers spend less + time hunting permissions and more time teaching. +

+
+
+
+ +
+
+
+ + +
+
+

Agency

+

Do not outsource your learning

+

+ Finishing a chapter is not the same as understanding it. Skimming for answers, copying worked + solutions, or rushing to the end leaves gaps that show up on the next test — and in the years after. +

+

+ Read slowly. Try the problems before you peek at hints. Sit with confusion long enough to name what + you do not get. The textbook is a guide; the thinking still has to be yours. That habit + is what school is really training, whether the subject is algebra or history. +

+
+
+
+ +
+
+
+ + +
+
+

Planet

+

Open textbooks are gentler on the Earth

+

+ Core curriculum changes slowly. Newton’s laws, the structure of a cell, and how fractions work do not + need a new AI run every September. Regenerating the same chapter thousands of times burns electricity + in data centers — for text that could have been written once and shared openly. +

+

+ One carefully reviewed book, published on the web for everyone, means less redundant + compute, less e-waste from disposable PDFs, and fewer trucks moving heavy print runs. Students get + stable material they can trust; the environment gets less waste from doing the same work over and over. +

+
+
+
+ +
+
+
+ + +
+
+

Craft

+

Carefully crafted by brilliant people on Earth

+

+ A textbook is not a bulk download. It is years of lectures distilled, diagrams redrawn until they + click, exercises tested in real classrooms, and arguments tightened by reviewers who care about + getting it right. +

+

+ Open Free Books gathers that work from educators and researchers at institutions around the world — + people who have already spent their careers making hard ideas legible. We publish their care + openly so the next student inherits the best version, not a disposable draft. +

+
+
+
+

Contributors and partners from leading universities include

+ + +
+
+ +

Catalog

@@ -108,6 +640,106 @@
+ + +
+
+

People

+

Contributors

+

+ Open Free Books is written and maintained in the open. These are the people building and reviewing the textbooks. +

+
    + + +
  • + +
  • + +
+ + + + +
+ +
+ + + +
+

Man Lung Yeung

+ +

EdTech specialist / software engineer

+ +
+
+ +

+ Education + BEng in Computer Science (HKUST) + BBA (HKUST) +

+ + +
+ Contributes +
    + +
  • Founder
  • + +
  • Math textbook
  • + +
  • Physics textbook
  • + +
  • Chemistry textbook
  • + +
+
+ + +
+
+ + +

+ Want to contribute? Open a pull request on GitHub. +

+
+
+ +