diff --git a/website/content/contribute/_index.md b/website/content/contribute/_index.md index 7378e3ba..50b6e3c8 100644 --- a/website/content/contribute/_index.md +++ b/website/content/contribute/_index.md @@ -22,7 +22,7 @@ cd frontend npm install ``` -You only need to explicitly install Node.js dependencies. Rust's cargo dependencies will be installed automatically on your first build. One dependency in the build chain, `wasm-pack`, will be installed automatically on your system when the Node.js packages are installing. If you prefer to install this manually, you can get it from the [wasm-pack website](https://rustwasm.github.io/wasm-pack/), then install dependencies with `npm install --no-optional`. **This is necessary on Apple Silicon/M1 Macs** until [this bug](https://github.com/rustwasm/wasm-pack/issues/952) is fixed. +You only need to explicitly install Node.js dependencies. Rust's cargo dependencies will be installed automatically on your first build. One dependency in the build chain, `wasm-pack`, will be installed automatically on your system when the Node.js packages are installing. If you prefer to install this manually, you can get it from the [wasm-pack website](https://rustwasm.github.io/wasm-pack/), then install your npm dependencies with `npm install --no-optional`. To run the project while developing: ``` diff --git a/website/sass/base.scss b/website/sass/base.scss index cb7b945a..0d4bb76c 100644 --- a/website/sass/base.scss +++ b/website/sass/base.scss @@ -1,16 +1,11 @@ :root { - --color-walnut: #473a3a; - --color-manilla: #f1decd; - --color-mustard: #deba92; - --color-crimson: #803847; - --color-navy: #16323f; + --color-white: #ffffff; --color-fog: #eeeeee; - --color-black: #000000; - --max-width: 1600px; - --max-width-plus-padding: calc(var(--max-width) + 40px * 2); - --variable-px: Min(1px, 0.15vw); - --page-edge-padding: 40px; - --border-thickness: 2px; + --color-navy: #16323f; + --color-walnut: #473a3a; + --color-crimson: #803847; + --color-mustard: #e5c299; + --font-size-intro-heading: 60px; --font-size-intro-body: 22px; --font-size-link: 24px; @@ -20,6 +15,13 @@ --font-size-article-h1: 32px; --font-size-article-h2: 24px; --font-size-article-h3: 18px; + + --max-width: 1600px; + --max-width-plus-padding: calc(var(--max-width) + 40px * 2); + + --variable-px: Min(1px, 0.15vw); + --page-edge-padding: 40px; + --border-thickness: 2px; @media screen and (max-width: 760px) { --font-size-intro-heading: 40px; @@ -27,6 +29,7 @@ --font-size-link: 20px; --font-size-heading: 32px; --font-size-body: 16px; + --page-edge-padding: 28px; } @@ -36,8 +39,9 @@ --font-size-link: 16px; --font-size-heading: 28px; --font-size-body: 16px; - --border-thickness: 1px; + --page-edge-padding: 20px; + --border-thickness: 1px; } @media screen and (max-width: 400px) { @@ -46,6 +50,7 @@ --font-size-link: 14px; --font-size-heading: 24px; --font-size-body: 16px; + --page-edge-padding: 12px; } } @@ -57,7 +62,7 @@ body { width: 100%; height: 100%; margin: 0; - background: white; + background: var(--color-white); font-family: "Inter", sans-serif; line-height: 1.5; font-weight: 500; diff --git a/website/sass/index.scss b/website/sass/index.scss index bdbb4436..b1478d1f 100644 --- a/website/sass/index.scss +++ b/website/sass/index.scss @@ -125,8 +125,8 @@ mask-size: contain; &.left { - padding-left: 120px; - margin-left: -120px; + padding-left: 80px; + margin-left: -80px; -webkit-mask-image: url("https://static.graphite.rs/textures/torn-edge-left.png"); mask-image: url("https://static.graphite.rs/textures/torn-edge-left.png"); -webkit-mask-position: top left; @@ -208,16 +208,16 @@ } @media screen and (max-width: 1000px) { - margin-left: -40px; - margin-right: -40px; + margin-left: calc(-1 * var(--page-edge-padding)); + margin-right: calc(-1 * var(--page-edge-padding)); .screenshot-details { - margin-left: 40px; - margin-right: 40px; + margin-left: var(--page-edge-padding); + margin-right: var(--page-edge-padding); } hr { - width: calc(100% - (32px + 40px) * 2); + width: calc(100% - (32px + var(--page-edge-padding)) * 2); margin-left: auto; margin-right: auto; } diff --git a/website/sass/logo.scss b/website/sass/logo.scss index 9fe8383d..625c18da 100644 --- a/website/sass/logo.scss +++ b/website/sass/logo.scss @@ -11,12 +11,12 @@ } &.color { - background-color: var(--color-fog); - background-blend-mode: color-burn; + background-color: var(--color-white); + background-blend-mode: hard-light; } - + &.light { - background-color: var(--color-manilla); + background-color: var(--color-fog); background-blend-mode: color-burn; } diff --git a/website/static/android-chrome-192x192.png b/website/static/android-chrome-192x192.png new file mode 100644 index 00000000..9e3f315c Binary files /dev/null and b/website/static/android-chrome-192x192.png differ diff --git a/website/static/android-chrome-512x512.png b/website/static/android-chrome-512x512.png new file mode 100644 index 00000000..c02e22a2 Binary files /dev/null and b/website/static/android-chrome-512x512.png differ diff --git a/website/static/apple-touch-icon.png b/website/static/apple-touch-icon.png new file mode 100644 index 00000000..e16582e9 Binary files /dev/null and b/website/static/apple-touch-icon.png differ diff --git a/website/static/browserconfig.xml b/website/static/browserconfig.xml new file mode 100644 index 00000000..30515a45 --- /dev/null +++ b/website/static/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #f1decd + + + diff --git a/website/static/favicon-16x16.png b/website/static/favicon-16x16.png new file mode 100644 index 00000000..fe415500 Binary files /dev/null and b/website/static/favicon-16x16.png differ diff --git a/website/static/favicon-32x32.png b/website/static/favicon-32x32.png new file mode 100644 index 00000000..e94d3fba Binary files /dev/null and b/website/static/favicon-32x32.png differ diff --git a/website/static/favicon.ico b/website/static/favicon.ico new file mode 100644 index 00000000..484a28f3 Binary files /dev/null and b/website/static/favicon.ico differ diff --git a/website/static/js/carousel.js b/website/static/js/carousel.js new file mode 100644 index 00000000..a305d243 --- /dev/null +++ b/website/static/js/carousel.js @@ -0,0 +1,164 @@ +const FLING_VELOCITY_THRESHOLD = 10; +const FLING_VELOCITY_WINDOW_SIZE = 20; + +let carouselImages; +let carouselDirectionPrev; +let carouselDirectionNext; +let carouselDots; +let carouselDescriptions; +let carouselDragLastClientX; +const velocityDeltaWindow = Array.from({ length: FLING_VELOCITY_WINDOW_SIZE }, () => ({ time: 0, delta: 0 })); + +window.addEventListener("DOMContentLoaded", initializeCarousel); +window.addEventListener("pointerup", () => dragEnd(false)); +window.addEventListener("scroll", () => dragEnd(true)); +window.addEventListener("pointermove", dragMove); + +function initializeCarousel() { + carouselImages = document.querySelectorAll(".carousel img"); + carouselImages.forEach((image) => { + image.addEventListener("pointerdown", dragBegin); + }); + + carouselDirectionPrev = document.querySelector(".carousel-controls .direction.prev"); + carouselDirectionNext = document.querySelector(".carousel-controls .direction.next"); + carouselDots = document.querySelectorAll(".carousel-controls .dot"); + carouselDescriptions = document.querySelectorAll(".screenshot-description p"); + + carouselDirectionPrev.addEventListener("click", () => slideDirection("prev", true, false)); + carouselDirectionNext.addEventListener("click", () => slideDirection("next", true, false)); + Array.from(carouselDots).forEach((dot) => + dot.addEventListener("click", (event) => { + const index = Array.from(carouselDots).indexOf(event.target); + slideTo(index, true); + }) + ); +} + +function slideDirection(direction, smooth, clamped = false) { + const directionIndexOffset = { prev: -1, next: 1 }[direction]; + const offsetDotIndex = currentClosestImageIndex() + directionIndexOffset; + + const nextDotIndex = (offsetDotIndex + carouselDots.length) % carouselDots.length; + const unwrappedNextDotIndex = clamp(offsetDotIndex, 0, carouselDots.length - 1); + + if (clamped) slideTo(unwrappedNextDotIndex, smooth); + else slideTo(nextDotIndex, smooth); +} + +function slideTo(index, smooth) { + const activeDot = document.querySelector(".carousel-controls .dot.active"); + activeDot.classList.remove("active"); + carouselDots[index].classList.add("active"); + + const activeDescription = document.querySelector(".screenshot-description p.active"); + activeDescription.classList.remove("active"); + carouselDescriptions[index].classList.add("active"); + + setCurrentTransform(index * -100, "%", smooth); +} + +function currentTransform() { + const currentTransformMatrix = window.getComputedStyle(carouselImages[0]).transform; + // Grab the X value from the format that looks like: `matrix(1, 0, 0, 1, -1332.13, 0)` or `none` + return Number(currentTransformMatrix.split(",")[4] || "0"); +} + +function setCurrentTransform(x, unit, smooth) { + Array.from(carouselImages).forEach((image) => { + image.style.transitionTimingFunction = smooth ? "ease-in-out" : "cubic-bezier(0, 0, 0.2, 1)"; + image.style.transform = `translateX(${x}${unit})`; + }); +} + +function currentClosestImageIndex() { + const currentTransformX = -currentTransform(); + + const imageWidth = carouselImages[0].getBoundingClientRect().width; + return Math.round(currentTransformX / imageWidth); +} + +function currentActiveDotIndex() { + const activeDot = document.querySelector(".carousel-controls .dot.active"); + return Array.from(carouselDots).indexOf(activeDot); +} + +function dragBegin(event) { + event.preventDefault(); + + carouselDragLastClientX = event.clientX; + + setCurrentTransform(currentTransform(), "px", false); + document.querySelector("#screenshots").classList.add("dragging"); +} + +function dragEnd(dropWithoutVelocity) { + if (!carouselImages) return; + + carouselDragLastClientX = undefined; + + document.querySelector("#screenshots").classList.remove("dragging"); + + const onlyRecentVelocityDeltaWindow = velocityDeltaWindow.filter((delta) => delta.time > Date.now() - 1000); + const timeRange = Date.now() - (onlyRecentVelocityDeltaWindow[0]?.time ?? NaN); + // Weighted (higher by recency) sum of velocity deltas from previous window of frames + const recentVelocity = onlyRecentVelocityDeltaWindow.reduce((acc, entry) => { + const timeSinceNow = Date.now() - entry.time; + const recencyFactorScore = 1 - timeSinceNow / timeRange; + + return acc + entry.delta * recencyFactorScore; + }, 0); + + const closestImageIndex = currentClosestImageIndex(); + const activeDotIndex = currentActiveDotIndex(); + + // If the speed is fast enough, slide to the next or previous image in that direction + if (Math.abs(recentVelocity) > FLING_VELOCITY_THRESHOLD && !dropWithoutVelocity) { + // Positive velocity should go to the previous image + if (recentVelocity > 0) { + // Don't apply the velocity-based fling if we're already snapping to the next image + if (closestImageIndex >= activeDotIndex) { + slideDirection("prev", false, true); + return; + } + } + // Negative velocity should go to the next image + else { + // Don't apply the velocity-based fling if we're already snapping to the next image + // eslint-disable-next-line no-lonely-if + if (closestImageIndex <= activeDotIndex) { + slideDirection("next", false, true); + return; + } + } + } + + // If we didn't slide in a direction due to clear velocity, just snap to the closest image + // This can be reached either by not entering the if statement above, or by its inner if statements not returning early and exiting back to this scope + slideTo(clamp(closestImageIndex, 0, carouselDots.length - 1), true); +} + +function dragMove(event) { + if (carouselDragLastClientX === undefined) return; + + event.preventDefault(); + + const LEFT_MOUSE_BUTTON = 1; + if (!(event.buttons & LEFT_MOUSE_BUTTON)) { + dragEnd(false); + return; + } + + const deltaX = event.clientX - carouselDragLastClientX; + velocityDeltaWindow.shift(); + velocityDeltaWindow.push({ time: Date.now(), delta: deltaX }); + + const newTransformX = currentTransform() + deltaX; + setCurrentTransform(newTransformX, "px", false); + + carouselDragLastClientX = event.clientX; +} + +function clamp(value, min, max) { + return Math.min(Math.max(value, min), max); +} diff --git a/website/static/js/navbar.js b/website/static/js/navbar.js new file mode 100644 index 00000000..6b91a777 --- /dev/null +++ b/website/static/js/navbar.js @@ -0,0 +1,115 @@ +const NAV_BUTTON_INITIAL_FONT_SIZE = 32; +const RIPPLE_ANIMATION_MILLISECONDS = 100; +const RIPPLE_WIDTH = 140; +const HANDLE_STRETCH = 0.4; + +let ripplesInitialized; +let navButtons; +let rippleSvg; +let ripplePath; +let fullRippleHeight; +let ripples; +let activeRippleIndex; + +window.addEventListener("DOMContentLoaded", initializeRipples); +window.addEventListener("resize", () => animate(true)); + +function initializeRipples() { + ripplesInitialized = true; + + navButtons = document.querySelectorAll("header nav a"); + rippleSvg = document.querySelector("header .ripple"); + ripplePath = rippleSvg.querySelector("path"); + fullRippleHeight = Number.parseInt(window.getComputedStyle(rippleSvg).height, 10) - 4; + + ripples = Array.from(navButtons).map((button) => ({ + element: button, + animationStartTime: null, + animationEndTime: null, + goingUp: false, + })); + + activeRippleIndex = ripples.findIndex((ripple) => ripple.element.getAttribute("href").replace(/\//g, "") === window.location.pathname.replace(/\//g, "")); + + ripples.forEach((ripple) => { + const updateTimings = (goingUp) => { + const start = ripple.animationStartTime; + const now = Date.now(); + const stop = ripple.animationStartTime + RIPPLE_ANIMATION_MILLISECONDS; + + const elapsed = now - start; + const remaining = stop - now; + + ripple.animationStartTime = now < stop ? now - remaining : now; + ripple.animationEndTime = now < stop ? now + elapsed : now + RIPPLE_ANIMATION_MILLISECONDS; + + ripple.goingUp = goingUp; + animate(false); + }; + + ripple.element.addEventListener("pointerenter", () => updateTimings(true)); + ripple.element.addEventListener("pointerleave", () => updateTimings(false)); + }); + + ripples[activeRippleIndex] = { + ...ripples[activeRippleIndex], + animationStartTime: 1, + animationEndTime: 1 + RIPPLE_ANIMATION_MILLISECONDS, + goingUp: true, + }; + + setRipples(); +} + +function animate(forceRefresh) { + if (!ripplesInitialized) return; + + const animateThisFrame = ripples.some((ripple) => ripple.animationStartTime && ripple.animationEndTime && Date.now() <= ripple.animationEndTime); + + if (animateThisFrame || forceRefresh) { + setRipples(); + window.requestAnimationFrame(() => animate(false)); + } +} + +function setRipples() { + const navButtonFontSize = Number.parseInt(window.getComputedStyle(navButtons[0]).fontSize, 10) || NAV_BUTTON_INITIAL_FONT_SIZE; + const mediaQueryScaleFactor = navButtonFontSize / NAV_BUTTON_INITIAL_FONT_SIZE; + + const rippleHeight = fullRippleHeight * (mediaQueryScaleFactor * 0.5 + 0.5); + const rippleSvgRect = rippleSvg.getBoundingClientRect(); + const rippleSvgLeft = rippleSvgRect.left; + const rippleSvgWidth = rippleSvgRect.width; + + let path = `M 0,${rippleHeight + 3} `; + + ripples.forEach((ripple) => { + if (!ripple.animationStartTime || !ripple.animationEndTime) return; + + const t = Math.min((Date.now() - ripple.animationStartTime) / (ripple.animationEndTime - ripple.animationStartTime), 1); + const height = rippleHeight * (ripple.goingUp ? ease(t) : 1 - ease(t)); + + const buttonRect = ripple.element.getBoundingClientRect(); + + const buttonCenter = buttonRect.width / 2; + const rippleCenter = (RIPPLE_WIDTH / 2) * mediaQueryScaleFactor; + const rippleOffset = rippleCenter - buttonCenter; + + const rippleStartX = buttonRect.left - rippleSvgLeft - rippleOffset; + + const rippleRadius = (RIPPLE_WIDTH / 2) * mediaQueryScaleFactor; + const handleRadius = rippleRadius * HANDLE_STRETCH; + + path += `L ${rippleStartX},${rippleHeight + 3} `; + path += `c ${handleRadius},0 ${rippleRadius - handleRadius},${-height} ${rippleRadius},${-height} `; + path += `s ${rippleRadius - handleRadius},${height} ${rippleRadius},${height} `; + }); + + path += `l ${rippleSvgWidth},0`; + + ripplePath.setAttribute("d", path); +} + +function ease(x) { + return 1 - (1 - x) * (1 - x); +} diff --git a/website/static/mstile-144x144.png b/website/static/mstile-144x144.png new file mode 100644 index 00000000..44f54fd1 Binary files /dev/null and b/website/static/mstile-144x144.png differ diff --git a/website/static/mstile-150x150.png b/website/static/mstile-150x150.png new file mode 100644 index 00000000..30bebbdc Binary files /dev/null and b/website/static/mstile-150x150.png differ diff --git a/website/static/mstile-310x150.png b/website/static/mstile-310x150.png new file mode 100644 index 00000000..4b08b270 Binary files /dev/null and b/website/static/mstile-310x150.png differ diff --git a/website/static/mstile-310x310.png b/website/static/mstile-310x310.png new file mode 100644 index 00000000..5df1a3b7 Binary files /dev/null and b/website/static/mstile-310x310.png differ diff --git a/website/static/mstile-70x70.png b/website/static/mstile-70x70.png new file mode 100644 index 00000000..e6c25628 Binary files /dev/null and b/website/static/mstile-70x70.png differ diff --git a/website/static/safari-pinned-tab.svg b/website/static/safari-pinned-tab.svg new file mode 100644 index 00000000..1aa9c242 --- /dev/null +++ b/website/static/safari-pinned-tab.svg @@ -0,0 +1,85 @@ + + + + + + + + diff --git a/website/static/site.webmanifest b/website/static/site.webmanifest new file mode 100644 index 00000000..2920aae0 --- /dev/null +++ b/website/static/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "Graphite", + "short_name": "Graphite", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#f1decd", + "background_color": "#f1decd", + "display": "standalone" +}