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"
+}