update landing page with grassy cloud fields
This commit is contained in:
@@ -5,13 +5,8 @@ This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm install
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
@@ -2,3 +2,8 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
|
||||
/* Tone down the bright sky so the white type reads on top of it */
|
||||
.woiken-hero .scroll-expand__media {
|
||||
filter: brightness(0.72) saturate(1.1);
|
||||
}
|
||||
|
||||
+23
-18
@@ -1,26 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import ScrollExpand from "@/components/ScrollExpand/ScrollExpand";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0b0b08] text-white">
|
||||
<header className="mx-auto flex w-full max-w-6xl items-center justify-center px-6 py-6 md:px-10">
|
||||
<div className="text-lg font-semibold tracking-wide">Woiken</div>
|
||||
</header>
|
||||
|
||||
<motion.main
|
||||
initial={{ opacity: 0, y: 18 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.55, ease: "easeOut" }}
|
||||
className="mx-auto flex min-h-[calc(100vh-84px)] w-full max-w-6xl flex-col items-center justify-center px-6 pb-20 text-center md:px-10"
|
||||
<ScrollExpand
|
||||
className="woiken-hero"
|
||||
src="/grass-field.jpg"
|
||||
alt="Clouds drifting over a green field under a blue sky"
|
||||
title="Woiken"
|
||||
scrollHint="Scroll"
|
||||
startWidth={46}
|
||||
startHeight={56}
|
||||
overlayScrim={0.55}
|
||||
useWindowScroll
|
||||
>
|
||||
<section className="max-w-3xl">
|
||||
<h1 className="text-4xl font-medium leading-tight tracking-tight md:text-6xl">
|
||||
Woiken is coming soon.
|
||||
</h1>
|
||||
</section>
|
||||
</motion.main>
|
||||
<p className="mb-4 text-xs font-medium uppercase tracking-[0.3em] text-white/70 md:text-sm">
|
||||
Woiken
|
||||
</p>
|
||||
<h1 className="text-5xl font-semibold leading-[1.05] tracking-tight md:text-7xl lg:text-8xl">
|
||||
A new cloud
|
||||
</h1>
|
||||
<p className="mt-5 text-lg text-white/80 md:text-2xl">coming soon.</p>
|
||||
</ScrollExpand>
|
||||
|
||||
<footer className="px-6 py-10 text-center text-sm text-white/40">
|
||||
© {new Date().getFullYear()} Woiken
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
.scroll-expand {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.scroll-expand--scroller {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.scroll-expand--scroller::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.scroll-expand__track {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.scroll-expand__stage {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
--se-title-size: 4rem;
|
||||
}
|
||||
|
||||
.scroll-expand__frame {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
clip-path: inset(21% 29% 21% 29% round 24px);
|
||||
will-change: clip-path;
|
||||
}
|
||||
|
||||
.scroll-expand__media {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
will-change: transform;
|
||||
transform-origin: center;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
|
||||
.scroll-expand__scrim {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, 0.75), rgba(0, 0, 0, 0.1) 45%, rgba(0, 0, 0, 0.35));
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.scroll-expand__overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 6%;
|
||||
opacity: 0;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
|
||||
.scroll-expand__title {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
padding: 0 6%;
|
||||
text-align: center;
|
||||
font-size: var(--se-title-size);
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.03em;
|
||||
line-height: 1;
|
||||
color: #fff;
|
||||
text-shadow: 0 2px 24px rgba(0, 0, 0, 0.45);
|
||||
pointer-events: none;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
|
||||
.scroll-expand__hint {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 1.25rem;
|
||||
text-align: center;
|
||||
font-size: 0.8125rem;
|
||||
letter-spacing: 0.02em;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
pointer-events: none;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import type { CSSProperties, HTMLAttributes, ReactNode } from "react";
|
||||
|
||||
export interface ScrollExpandProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
|
||||
src?: string;
|
||||
mediaType?: "image" | "video";
|
||||
poster?: string;
|
||||
alt?: string;
|
||||
title?: string;
|
||||
scrollHint?: string;
|
||||
startWidth?: number;
|
||||
startHeight?: number;
|
||||
startRadius?: number;
|
||||
endRadius?: number;
|
||||
mediaZoom?: number;
|
||||
scrollDistance?: number;
|
||||
holdDistance?: number;
|
||||
smoothing?: number;
|
||||
overlayScrim?: number;
|
||||
useWindowScroll?: boolean;
|
||||
enabled?: boolean;
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
declare const ScrollExpand: (props: ScrollExpandProps) => JSX.Element;
|
||||
|
||||
export default ScrollExpand;
|
||||
@@ -0,0 +1,243 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useLayoutEffect, useRef } from 'react';
|
||||
|
||||
import './ScrollExpand.css';
|
||||
|
||||
const clamp = (v, a, b) => (v < a ? a : v > b ? b : v);
|
||||
|
||||
const smoothstep = (edge0, edge1, x) => {
|
||||
const t = clamp((x - edge0) / (edge1 - edge0 || 1e-6), 0, 1);
|
||||
return t * t * (3 - 2 * t);
|
||||
};
|
||||
|
||||
const ScrollExpand = ({
|
||||
src = '',
|
||||
mediaType = 'image',
|
||||
poster = '',
|
||||
alt = '',
|
||||
title = '',
|
||||
scrollHint = '',
|
||||
startWidth = 42,
|
||||
startHeight = 58,
|
||||
startRadius = 24,
|
||||
endRadius = 0,
|
||||
mediaZoom = 1.35,
|
||||
scrollDistance = 1.2,
|
||||
holdDistance = 0.35,
|
||||
smoothing = 0.1,
|
||||
overlayScrim = 0.45,
|
||||
useWindowScroll = false,
|
||||
enabled = true,
|
||||
children,
|
||||
className = '',
|
||||
style,
|
||||
...rest
|
||||
}) => {
|
||||
const rootRef = useRef(null);
|
||||
const trackRef = useRef(null);
|
||||
const stageRef = useRef(null);
|
||||
const frameRef = useRef(null);
|
||||
const mediaRef = useRef(null);
|
||||
const titleRef = useRef(null);
|
||||
const overlayRef = useRef(null);
|
||||
const scrimRef = useRef(null);
|
||||
const hintRef = useRef(null);
|
||||
|
||||
const propsRef = useRef({});
|
||||
useLayoutEffect(() => {
|
||||
propsRef.current = {
|
||||
startWidth,
|
||||
startHeight,
|
||||
startRadius,
|
||||
endRadius,
|
||||
mediaZoom,
|
||||
scrollDistance,
|
||||
holdDistance,
|
||||
smoothing,
|
||||
overlayScrim,
|
||||
useWindowScroll,
|
||||
enabled
|
||||
};
|
||||
});
|
||||
|
||||
const applyProgress = useCallback(p => {
|
||||
const frame = frameRef.current;
|
||||
const media = mediaRef.current;
|
||||
if (!frame || !media) return;
|
||||
const c = propsRef.current;
|
||||
|
||||
const e = smoothstep(0, 1, p);
|
||||
|
||||
const w = c.startWidth + (100 - c.startWidth) * e;
|
||||
const h = c.startHeight + (100 - c.startHeight) * e;
|
||||
const ix = Math.max(0, (100 - w) / 2);
|
||||
const iy = Math.max(0, (100 - h) / 2);
|
||||
const r = c.startRadius + (c.endRadius - c.startRadius) * e;
|
||||
frame.style.clipPath = `inset(${iy}% ${ix}% ${iy}% ${ix}% round ${r}px)`;
|
||||
|
||||
media.style.transform = `scale(${c.mediaZoom + (1 - c.mediaZoom) * e})`;
|
||||
|
||||
if (scrimRef.current) scrimRef.current.style.opacity = `${c.overlayScrim * e}`;
|
||||
|
||||
if (titleRef.current) {
|
||||
const out = smoothstep(0.4, 0.88, p);
|
||||
titleRef.current.style.opacity = `${1 - out}`;
|
||||
titleRef.current.style.transform = `translate3d(0, ${-28 * out}px, 0) scale(${1 + 0.06 * out})`;
|
||||
}
|
||||
|
||||
if (hintRef.current) {
|
||||
const gone = smoothstep(0, 0.12, p);
|
||||
hintRef.current.style.opacity = `${1 - gone}`;
|
||||
hintRef.current.style.transform = `translate3d(0, ${8 * gone}px, 0)`;
|
||||
}
|
||||
|
||||
if (overlayRef.current) {
|
||||
const inn = smoothstep(0.68, 1, p);
|
||||
overlayRef.current.style.opacity = `${inn}`;
|
||||
overlayRef.current.style.transform = `translate3d(0, ${18 * (1 - inn)}px, 0)`;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const root = rootRef.current;
|
||||
const track = trackRef.current;
|
||||
const stage = stageRef.current;
|
||||
if (!root || !track || !stage) return;
|
||||
|
||||
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
|
||||
let raf = 0;
|
||||
let current = 0;
|
||||
let target = 0;
|
||||
let stageH = 0;
|
||||
let running = false;
|
||||
|
||||
const measure = () => {
|
||||
const c = propsRef.current;
|
||||
stageH = c.useWindowScroll ? window.innerHeight : root.clientHeight;
|
||||
if (stageH <= 0) return;
|
||||
stage.style.height = `${stageH}px`;
|
||||
track.style.height = `${stageH * (1 + Math.max(0, c.scrollDistance) + Math.max(0, c.holdDistance))}px`;
|
||||
|
||||
const w = root.clientWidth || stageH;
|
||||
stage.style.setProperty('--se-title-size', `${clamp(w * 0.075, 20, 84)}px`);
|
||||
};
|
||||
|
||||
const readProgress = () => {
|
||||
const c = propsRef.current;
|
||||
if (!c.enabled) return 1;
|
||||
const span = stageH * Math.max(0.01, c.scrollDistance);
|
||||
if (c.useWindowScroll) {
|
||||
const top = track.getBoundingClientRect().top;
|
||||
return clamp(-top / span, 0, 1);
|
||||
}
|
||||
return clamp(root.scrollTop / span, 0, 1);
|
||||
};
|
||||
|
||||
const tick = () => {
|
||||
const c = propsRef.current;
|
||||
const k = c.smoothing <= 0 ? 1 : 1 - Math.exp(-1 / (60 * c.smoothing));
|
||||
current += (target - current) * k;
|
||||
if (Math.abs(target - current) < 0.0004) {
|
||||
current = target;
|
||||
running = false;
|
||||
}
|
||||
applyProgress(current);
|
||||
raf = running ? requestAnimationFrame(tick) : 0;
|
||||
};
|
||||
|
||||
const kick = () => {
|
||||
if (running) return;
|
||||
running = true;
|
||||
if (!raf) raf = requestAnimationFrame(tick);
|
||||
};
|
||||
|
||||
const onScroll = () => {
|
||||
target = readProgress();
|
||||
if (propsRef.current.smoothing <= 0 || reduceMotion) {
|
||||
current = target;
|
||||
applyProgress(current);
|
||||
return;
|
||||
}
|
||||
kick();
|
||||
};
|
||||
|
||||
const onResize = () => {
|
||||
measure();
|
||||
target = readProgress();
|
||||
current = target;
|
||||
applyProgress(current);
|
||||
};
|
||||
|
||||
measure();
|
||||
target = readProgress();
|
||||
current = target;
|
||||
applyProgress(current);
|
||||
|
||||
const scroller = useWindowScroll ? window : root;
|
||||
scroller.addEventListener('scroll', onScroll, { passive: true });
|
||||
window.addEventListener('resize', onResize);
|
||||
const ro = new ResizeObserver(onResize);
|
||||
ro.observe(root);
|
||||
|
||||
return () => {
|
||||
if (raf) cancelAnimationFrame(raf);
|
||||
scroller.removeEventListener('scroll', onScroll);
|
||||
window.removeEventListener('resize', onResize);
|
||||
ro.disconnect();
|
||||
};
|
||||
}, [applyProgress, useWindowScroll]);
|
||||
|
||||
const media =
|
||||
mediaType === 'video' ? (
|
||||
<video
|
||||
ref={mediaRef}
|
||||
className="scroll-expand__media"
|
||||
src={src}
|
||||
poster={poster}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
/>
|
||||
) : (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img ref={mediaRef} className="scroll-expand__media" src={src} alt={alt} draggable={false} />
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={rootRef}
|
||||
className={`scroll-expand ${useWindowScroll ? '' : 'scroll-expand--scroller'} ${className}`.trim()}
|
||||
style={style}
|
||||
{...rest}
|
||||
>
|
||||
<div ref={trackRef} className="scroll-expand__track">
|
||||
<div ref={stageRef} className="scroll-expand__stage">
|
||||
<div ref={frameRef} className="scroll-expand__frame">
|
||||
{media}
|
||||
<div ref={scrimRef} className="scroll-expand__scrim" />
|
||||
{children ? (
|
||||
<div ref={overlayRef} className="scroll-expand__overlay">
|
||||
{children}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{title ? (
|
||||
<div ref={titleRef} className="scroll-expand__title">
|
||||
{title}
|
||||
</div>
|
||||
) : null}
|
||||
{scrollHint ? (
|
||||
<div ref={hintRef} className="scroll-expand__hint">
|
||||
{scrollHint}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScrollExpand;
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.5 MiB |
Reference in New Issue
Block a user