|
1 | 1 | import { useEffect, useMemo, useRef, useState } from "react"; |
2 | 2 | import workspaceShot from "@/assets/workspace.png"; |
3 | 3 | import modelPickerShot from "@/assets/model-picker.png"; |
| 4 | +import heroPlate from "@/assets/hero.webp"; |
4 | 5 |
|
5 | 6 | /* OpenScience. CMU Concrete, warm dark, coral accents. |
6 | 7 | Same design family as the Atlas landing page. |
@@ -259,100 +260,25 @@ function Section({ |
259 | 260 | ); |
260 | 261 | } |
261 | 262 |
|
262 | | -/* --------------------------- Hero constellation ------------------------- */ |
263 | | -/* A slow, drifting field of faint points joined into a constellation. |
264 | | - Cream nodes, a few coral. Pure canvas, no library. */ |
265 | | - |
266 | | -function Constellation() { |
267 | | - const ref = useRef<HTMLCanvasElement>(null); |
268 | | - useEffect(() => { |
269 | | - const canvas = ref.current; |
270 | | - if (!canvas) return; |
271 | | - const ctx = canvas.getContext("2d"); |
272 | | - if (!ctx) return; |
273 | | - const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches; |
274 | | - const dpr = Math.min(window.devicePixelRatio || 1, 2); |
275 | | - let raf = 0; |
276 | | - let w = 0; |
277 | | - let h = 0; |
278 | | - type Pt = { x: number; y: number; vx: number; vy: number; r: number; hot: boolean }; |
279 | | - let pts: Pt[] = []; |
280 | | - |
281 | | - const resize = () => { |
282 | | - const rect = canvas.getBoundingClientRect(); |
283 | | - w = rect.width; |
284 | | - h = rect.height; |
285 | | - canvas.width = Math.round(w * dpr); |
286 | | - canvas.height = Math.round(h * dpr); |
287 | | - ctx.setTransform(dpr, 0, 0, dpr, 0, 0); |
288 | | - const count = Math.min(120, Math.max(50, Math.floor((w * h) / 15000))); |
289 | | - pts = Array.from({ length: count }, () => ({ |
290 | | - x: Math.random() * w, |
291 | | - y: Math.random() * h, |
292 | | - vx: (Math.random() - 0.5) * 0.14, |
293 | | - vy: (Math.random() - 0.5) * 0.14, |
294 | | - r: Math.random() < 0.14 ? 1.7 : 1.05, |
295 | | - hot: Math.random() < 0.055, |
296 | | - })); |
297 | | - }; |
298 | | - |
299 | | - const LINK = 120; |
300 | | - const draw = () => { |
301 | | - ctx.clearRect(0, 0, w, h); |
302 | | - for (let i = 0; i < pts.length; i++) { |
303 | | - const a = pts[i]; |
304 | | - for (let j = i + 1; j < pts.length; j++) { |
305 | | - const b = pts[j]; |
306 | | - const dx = a.x - b.x; |
307 | | - const dy = a.y - b.y; |
308 | | - const d2 = dx * dx + dy * dy; |
309 | | - if (d2 < LINK * LINK) { |
310 | | - const t = 1 - Math.sqrt(d2) / LINK; |
311 | | - ctx.strokeStyle = `hsla(42, 32%, 88%, ${(t * 0.13).toFixed(3)})`; |
312 | | - ctx.lineWidth = 0.6; |
313 | | - ctx.beginPath(); |
314 | | - ctx.moveTo(a.x, a.y); |
315 | | - ctx.lineTo(b.x, b.y); |
316 | | - ctx.stroke(); |
317 | | - } |
318 | | - } |
319 | | - } |
320 | | - for (const p of pts) { |
321 | | - ctx.beginPath(); |
322 | | - ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); |
323 | | - ctx.fillStyle = p.hot ? "hsla(14, 70%, 64%, 0.75)" : "hsla(42, 32%, 88%, 0.4)"; |
324 | | - ctx.fill(); |
325 | | - } |
326 | | - }; |
327 | | - |
328 | | - const step = () => { |
329 | | - for (const p of pts) { |
330 | | - p.x += p.vx; |
331 | | - p.y += p.vy; |
332 | | - if (p.x < -8) p.x = w + 8; |
333 | | - if (p.x > w + 8) p.x = -8; |
334 | | - if (p.y < -8) p.y = h + 8; |
335 | | - if (p.y > h + 8) p.y = -8; |
336 | | - } |
337 | | - draw(); |
338 | | - raf = window.requestAnimationFrame(step); |
339 | | - }; |
340 | | - |
341 | | - resize(); |
342 | | - draw(); |
343 | | - if (!reduced) raf = window.requestAnimationFrame(step); |
344 | | - const onResize = () => { |
345 | | - resize(); |
346 | | - draw(); |
347 | | - }; |
348 | | - window.addEventListener("resize", onResize); |
349 | | - return () => { |
350 | | - window.cancelAnimationFrame(raf); |
351 | | - window.removeEventListener("resize", onResize); |
352 | | - }; |
353 | | - }, []); |
354 | | - return <canvas ref={ref} className="absolute inset-0 w-full h-full" aria-hidden />; |
355 | | -} |
| 263 | +/* ----------------------------- Hero plate ------------------------------- */ |
| 264 | +/* The Pharos of Alexandria engraving — the beam sweeps from the tower at the |
| 265 | + right down to a small ship steering by its light at bottom-left. The plate |
| 266 | + is already monochrome warm sepia, the same hue family as the site's cream, |
| 267 | + so it ships unfiltered. Anchored right; on wide viewports the background |
| 268 | + color fills the left edge and the veil blends the seam. */ |
| 269 | + |
| 270 | +const HERO_NOISE = |
| 271 | + "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E\")"; |
| 272 | + |
| 273 | +/* Veil: a light uniform scrim softens the plate; a soft top-left shield sits |
| 274 | + under the wordmark and a bottom-right shield under the copy, so the beam |
| 275 | + and the ship at bottom-left stay in view. */ |
| 276 | +const HERO_VEIL = [ |
| 277 | + "linear-gradient(hsl(30 14% 7% / 0.2), hsl(30 14% 7% / 0.2))", |
| 278 | + "radial-gradient(ellipse 55% 45% at 5% 6%, hsl(30 14% 7% / 0.8) 0%, hsl(30 14% 7% / 0.4) 55%, transparent 85%)", |
| 279 | + "radial-gradient(ellipse 85% 75% at 96% 94%, hsl(var(--background)) 0%, hsl(30 14% 7% / 0.84) 38%, hsl(30 14% 7% / 0.28) 66%, transparent 90%)", |
| 280 | + "linear-gradient(180deg, hsl(28 18% 4% / 0.5) 0%, transparent 15%)", |
| 281 | +].join(", "); |
356 | 282 |
|
357 | 283 | /* ------------------------------ Hero ----------------------------------- */ |
358 | 284 |
|
@@ -389,46 +315,44 @@ function Hero() { |
389 | 315 | return ( |
390 | 316 | <section className="relative h-screen min-h-[680px] w-full bg-background overflow-hidden grain"> |
391 | 317 | <div ref={backdrop} className="absolute inset-0 will-change-transform" aria-hidden> |
392 | | - <div className="absolute inset-0 graticule opacity-[0.05]" /> |
393 | 318 | <div |
394 | | - className="absolute inset-0" |
395 | | - style={{ |
396 | | - maskImage: "linear-gradient(to bottom, black 55%, transparent 96%)", |
397 | | - WebkitMaskImage: "linear-gradient(to bottom, black 55%, transparent 96%)", |
398 | | - }} |
399 | | - > |
400 | | - <Constellation /> |
401 | | - </div> |
| 319 | + className="absolute inset-0 bg-background bg-no-repeat [background-position:62%_14%] [background-size:cover] sm:[background-position:right_center] sm:[background-size:auto_114%]" |
| 320 | + style={{ backgroundImage: `url(${heroPlate})` }} |
| 321 | + /> |
| 322 | + <div |
| 323 | + className="absolute inset-0 opacity-[0.32] mix-blend-overlay" |
| 324 | + style={{ backgroundImage: HERO_NOISE }} |
| 325 | + /> |
| 326 | + <div className="absolute inset-0" style={{ background: HERO_VEIL }} /> |
402 | 327 | </div> |
403 | 328 |
|
404 | 329 | <div |
405 | 330 | className="pointer-events-none absolute inset-x-0 bottom-0 z-[5]" |
406 | 331 | style={{ |
407 | | - height: "56%", |
408 | | - background: |
409 | | - "linear-gradient(to top, hsl(var(--background)) 0%, hsl(var(--background) / 0.9) 18%, hsl(var(--background) / 0.55) 46%, hsl(var(--background) / 0) 100%)", |
| 332 | + height: "16%", |
| 333 | + background: "linear-gradient(to top, hsl(var(--background)) 0%, hsl(var(--background) / 0) 100%)", |
410 | 334 | }} |
411 | 335 | /> |
412 | 336 |
|
413 | 337 | <div className="absolute inset-0 z-10 mx-auto flex h-full max-w-[1400px] flex-col px-6 sm:px-10"> |
414 | | - <div className="hero-text rise self-end text-right mt-[9vh]" style={{ animationDelay: "120ms" }}> |
| 338 | + <div className="hero-text rise self-start mt-[9vh]" style={{ animationDelay: "120ms" }}> |
415 | 339 | <div className="text-[clamp(40px,6.4vw,96px)] leading-[0.9] tracking-[-0.04em]">openscience</div> |
416 | | - <div className="mt-3 text-[13px] tracking-[0.04em] text-foreground/50">by Synthetic Sciences</div> |
| 340 | + <div className="mt-3 text-[13px] tracking-[0.04em] text-foreground/55">by Synthetic Sciences</div> |
417 | 341 | </div> |
418 | 342 |
|
419 | | - <div ref={copy} className="hero-text mt-auto mb-[7vh] max-w-[820px]"> |
| 343 | + <div ref={copy} className="hero-text mt-auto mb-[7vh] self-end text-right max-w-[820px]"> |
420 | 344 | <div className="rise" style={{ animationDelay: "260ms" }}> |
421 | 345 | <h1 className="text-balance text-[clamp(34px,4.6vw,62px)] leading-[1.04] tracking-[-0.024em] text-foreground"> |
422 | 346 | The open-source AI workbench for scientists. |
423 | 347 | </h1> |
424 | 348 | </div> |
425 | 349 | <div className="rise" style={{ animationDelay: "420ms" }}> |
426 | | - <p className={`mt-6 max-w-[54ch] ${P_BIG} text-foreground/80`}> |
| 350 | + <p className={`mt-6 ml-auto max-w-[44ch] ${P_BIG} text-foreground/85`}> |
427 | 351 | One environment for the whole loop: literature, code, experiments, figures, |
428 | 352 | and the write-up. In your browser, on any model, with your keys. |
429 | 353 | </p> |
430 | 354 | </div> |
431 | | - <div className="rise mt-9 flex flex-wrap items-center gap-3 [text-shadow:none]" style={{ animationDelay: "580ms" }}> |
| 355 | + <div className="rise mt-9 flex flex-wrap items-center justify-end gap-3 [text-shadow:none]" style={{ animationDelay: "580ms" }}> |
432 | 356 | <Cta href="#install">Install OpenScience</Cta> |
433 | 357 | <Cta href={GITHUB} variant="ghost" arrow={false} external> |
434 | 358 | <svg width="15" height="15" viewBox="0 0 16 16" fill="currentColor" aria-hidden> |
|
0 commit comments