diff --git a/website/config.toml b/website/config.toml new file mode 100644 index 00000000..b1e2d4ae --- /dev/null +++ b/website/config.toml @@ -0,0 +1,16 @@ +# The URL the site will be built for +base_url = "https://graphite.rs" + +# Whether to automatically compile all Sass files in the sass directory +compile_sass = true + +# Whether to build a search index to be used later on by a JavaScript library +build_search_index = true + +[markdown] +# Whether to do syntax highlighting +# Theme can be customised by setting the `highlight_theme` variable to a theme supported by Zola +highlight_code = true + +[extra] +# Put all your custom variables here diff --git a/website/content/_index.md b/website/content/_index.md new file mode 100644 index 00000000..d8a1dcd7 --- /dev/null +++ b/website/content/_index.md @@ -0,0 +1,4 @@ ++++ +title = "Redefining state-of-the-art graphics editing." +template = "index.html" ++++ \ No newline at end of file diff --git a/website/sass/base.scss b/website/sass/base.scss new file mode 100644 index 00000000..a793b85b --- /dev/null +++ b/website/sass/base.scss @@ -0,0 +1,441 @@ +:root { + --color-walnut: #473a3a; + --color-manilla: #f1decd; + --color-mustard: #deba92; + --color-crimson: #803847; + --color-navy: #16323f; + --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); +} + +// Global element styles + +html, +body { + width: 100%; + height: 100%; + margin: 0; + background: var(--color-manilla); + font-family: "Inter", sans-serif; + line-height: 1.5; + font-weight: 500; + font-size: 18px; + color: var(--color-navy); +} + +a { + color: var(--color-crimson); +} + +h1, +h2, +h3, +h4 { + margin: 0; + display: inline-block; +} + +h1 { + font-family: "Bona Nova", serif; + font-feature-settings: "lnum"; + line-height: 1.25; + font-weight: 700; + font-size: 60px; + + ~ p { + font-size: 22px; + } +} + +h2 { + font-family: "Bona Nova", serif; + font-feature-settings: "lnum"; + line-height: 1.25; + font-weight: 700; + font-size: 40px; +} + +h3 { + font-size: 24px; + font-weight: 800; + text-transform: uppercase; + + span { + white-space: pre; + } + + ~ .divider { + margin-top: 20px; + } + + + .divider + .diptych, + + .divider + .triptych { + margin-top: 40px; + } +} + +h4 { + font-family: "Bona Nova", serif; + font-feature-settings: "lnum"; + line-height: 1.25; + font-size: 32px; + font-weight: 400; +} + +p { + margin: 0; + hyphens: auto; + text-align: justify; +} + +h1 ~ p, +h2 ~ p, +h4 ~ p, +h4 ~ img, +p + .link { + margin-top: 20px; +} + +p + p { + margin-top: calc(1em * 1.5); +} + +.link { + display: inline-block; + font-size: 24px; + font-weight: 800; + text-decoration: none; + color: var(--color-crimson); + white-space: nowrap; + + &:not(.not-uppercase) { + text-transform: uppercase; + } +} + +.button.button.button.button { + color: var(--color-crimson); + display: inline-block; + border: 2px solid currentColor; + height: 48px; + line-height: calc(48px - 2 * 2px); + font-size: 24px; + padding: 0 24px; + box-sizing: border-box; + text-decoration: none; + font-weight: 800; + white-space: nowrap; +} + +.arrow::after { + content: " »"; + font-family: "Inter", sans-serif; +} + +.divider { + margin: 0 32px; + width: calc(100% - 32px * 2); + height: 2px; + background: currentColor; + position: relative; + + &::before { + left: -40px; + border-width: 0 0 2px 40px; + } + + &::after { + right: -40px; + border-width: 0 40px 2px 0; + } + + &::before, + &::after { + content: ""; + display: block; + width: 0; + height: 0; + position: absolute; + border-color: transparent transparent currentColor transparent; + border-style: solid; + } +} + +.section-row { + display: flex; + align-items: stretch; + gap: calc(40 * var(--variable-px)) calc(80 * var(--variable-px)); + + &.right { + flex-direction: row-reverse; + } + + @media screen and (max-width: 800px) { + &.section-row { + flex-direction: column; + align-items: center; + } + } + + .graphic { + max-width: 280px; + flex: 1 1 100%; + display: flex; + + img { + display: block; + width: 100%; + height: auto; + } + } + + .section { + display: flex; + flex-direction: column; + align-items: flex-start; + } +} + +.feature-box { + padding: calc(80 * var(--variable-px)); + background-image: url("https://static.graphite.rs/textures/noise.png"); + background-blend-mode: soft-light; + background-position: center; + + &.feature-box.feature-box { + max-width: unset; + } + + @media screen and (max-width: 1000px) { + &.feature-box.feature-box { + margin-left: -40px; + margin-right: -40px; + } + } + + .box { + max-width: var(--max-width); + margin: 0 auto; + + .section h4 + .graphic { + margin-top: 20px; + + img { + margin: auto; + } + } + } +} + +.diptych, +.triptych { + display: flex; + flex-wrap: wrap; + gap: calc(80 * var(--variable-px)); + + .section { + flex: 1 1 0; + } + + &.diptych .section { + min-width: 320px; + } + + &.triptych .section { + min-width: 280px; + } + + img[alt=""]{ + display: block; + + &::after { + content: ""; + display: block; + width: 100%; + height: 240px; + background: var(--color-crimson); + } + } + + + div { + margin-top: calc(80 * var(--variable-px)); + } +} + +// Page content + +.page { + box-sizing: border-box; + overflow: hidden; + min-width: 600px; + + header { + padding: 0 40px; + color: var(--color-walnut); + + // max-width + (80px + 32px) * 2 + @media screen and (max-width: 1824px) { + .divider { + &.ripple { + width: calc(100% + (40px * 2)); + margin-left: -40px; + margin-right: -40px; + } + + &:not(.ripple) { + display: none; + } + } + } + + nav { + margin: auto; + max-width: var(--max-width); + + .row { + display: flex; + justify-content: space-between; + padding: 30px 0; + gap: 80px; + + .left, + .right { + display: flex; + align-items: center; + gap: 40px; + + a { + color: inherit; + font-family: "Bona Nova", serif; + font-feature-settings: "lnum"; + line-height: 1.25; + font-weight: 700; + text-decoration: none; + --height: 60px; + --button-padding: 24px; + --font-size: 36px; + font-size: var(--font-size); + + &.button { + height: var(--height); + padding-left: var(--button-padding); + padding-right: var(--button-padding); + line-height: calc(var(--height) - 2 * 2px); + font-size: var(--font-size); + + &::after { + content: "»"; + margin-left: 8px + } + } + + svg { + fill: currentColor; + display: block; + width: var(--height); + height: var(--height); + } + } + } + + @media screen and (max-width: 960px) { + gap: 60px; + + .left, + .right { + gap: 30px; + + a { + --height: 50px; + --button-padding: 16px; + --font-size: 26px; + } + } + } + + @media screen and (max-width: 760px) { + gap: 40px; + + .left, + .right { + gap: 20px; + + a { + --height: 40px; + --button-padding: 16px; + --font-size: 22px; + } + } + } + } + } + + .divider { + background: none; + + &.ripple { + display: block; + fill: none; + stroke: currentColor; + --ripple-height: 16px; + height: var(--ripple-height); + margin-top: calc(-1 * var(--ripple-height) + 2px); + margin-bottom: -2px; + stroke-width: 2px; + + &::before, + &::after { + content: none; + } + } + } + } + + main { + padding: 0 40px; + + .content { + padding: calc(120 * var(--variable-px)) 0; + + section { + max-width: var(--max-width); + margin: auto; + // Puts the content in front of the hexagon decoration + position: relative; + z-index: 1; + + ~ section { + margin-top: calc(80 * var(--variable-px)); + } + } + + } + } + + footer { + display: flex; + flex-direction: column; + align-items: center; + gap: 40px; + padding: 40px; + padding-top: 0; + + nav { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 8px 40px; + + @media screen and (max-width: 800px) { + max-width: 500px; + } + } + + span { + text-align: center; + } + } +} diff --git a/website/sass/index.scss b/website/sass/index.scss new file mode 100644 index 00000000..2dc94363 --- /dev/null +++ b/website/sass/index.scss @@ -0,0 +1,327 @@ +#logo { + display: flex; + + svg { + display: block; + height: auto; + } +} + +#quick-links { + margin-bottom: calc(120 * var(--variable-px)); + display: flex; + gap: 20px; + flex-wrap: wrap; +} + +.pencil-texture { + position: absolute; + --remaining-width-to-full: calc(var(--max-width-plus-padding) - min(calc(100vw - 100px), var(--max-width-plus-padding))); + left: Max(calc(-1 * var(--remaining-width-to-full)), -40px); + width: 100px; + mix-blend-mode: multiply; +} + +.hexagons { + max-width: var(--max-width); + margin: auto; + position: relative; + bottom: calc(-80 * var(--variable-px)); + + div { + position: absolute; + top: 0; + right: 10%; + + svg { + position: absolute; + margin: auto; + top: 0; + right: 0; + bottom: 0; + left: 0; + transform: translate(-50%) rotate(347deg); + opacity: 0.25; + width: Max(1000px, Min(1400px, calc(100vw * 1400 / 1920))); + height: auto; + + polygon { + fill: none; + stroke: gray; + stroke-width: 1px; + } + } + } +} + +#screenshots { + transform: translate(0); + + .carousel { + display: flex; + white-space: nowrap; + touch-action: pan-y pinch-zoom; + cursor: grab; + + img { + position: relative; + display: inline-block; + width: 100%; + flex: 0 0 auto; + padding: 0 20px; + + &:first-child { + margin-left: -20px; + } + + &:last-child { + margin-right: -20px; + } + } + } + + &:not(.dragging) .carousel img { + transition: transform 500ms; + } + + .carousel:not(.torn) { + overflow: hidden; + } + + .carousel.torn { + position: fixed; + top: 0; + z-index: -1; + // Torn edge mask + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-size: contain; + mask-size: contain; + + &.left { + padding-left: 120px; + margin-left: -120px; + -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; + mask-position: top left; + } + + &.right { + padding-right: 120px; + margin-right: -120px; + -webkit-mask-image: url("https://static.graphite.rs/textures/torn-edge-right.png"); + mask-image: url("https://static.graphite.rs/textures/torn-edge-right.png"); + -webkit-mask-position: top right; + mask-position: top right; + } + } + + .screenshot-details { + display: flex; + margin: 20px 0; + gap: 20px 40px; + + @media screen and (max-width: 800px) { + flex-wrap: wrap; + justify-content: center; + } + + .carousel-controls { + display: flex; + align-items: center; + + button { + outline: none; + background: none; + border: none; + padding: 0; + color: inherit; + cursor: pointer; + + svg { + display: block; + } + + + button { + margin-left: 20px; + } + } + + .direction { + fill: currentColor; + } + + .dot { + width: 16px; + height: 16px; + box-sizing: border-box; + border-radius: 50%; + border: 2px solid currentColor; + + &.active { + border: none; + background: var(--color-crimson); + } + } + } + + .screenshot-description { + display: flex; + align-items: center; + min-height: calc(2em * 1.5); + + p + p { + margin: 0; + } + + p:not(.active) { + display: none; + } + } + } + + @media screen and (max-width: 1000px) { + margin-left: -40px; + margin-right: -40px; + + .screenshot-details { + margin-left: 40px; + margin-right: 40px; + } + + .divider.divider { + width: calc(100% - (32px + 40px) * 2); + margin-left: auto; + margin-right: auto; + } + } +} + +#upcoming-tech { + background-color: var(--color-navy); + color: var(--color-fog); + + a { + color: var(--color-mustard); + } +} + +#community { + #newsletter form { + margin-top: 40px; + display: flex; + gap: 20px; + flex-wrap: wrap; + + .same-line { + display: flex; + gap: 20px; + flex: 100000 1 0; + + @media screen and (max-width: 1200px) { + flex-direction: column; + flex: 1 1 100%; + + &.name, + &.email { + flex: 1 1 100%; + } + } + } + + .column { + display: flex; + flex-direction: column; + justify-content: flex-end; + + &.name { + flex: 1 0 0; + min-width: 240px; + } + + &.email { + flex: 1 0 0; + min-width: 240px; + } + + &.submit { + flex: 1 0 0; + } + + label, + input { + flex: 0 0 auto; + } + + label { + font-size: 24px; + font-weight: 800; + margin-bottom: 10px; + line-height: 1; + } + + input:not([type="submit"]) { + flex: 0 0 auto; + width: 100%; + height: 48px; + font-size: 22px; + color: inherit; + border: 2px solid currentColor; + outline: none; + margin: 0; + padding: 0 24px; + font-family: inherit; + font-weight: inherit; + box-sizing: border-box; + + &:focus { + border-color: var(--color-mustard); + } + } + + input[type="submit"] { + background: none; + outline: none; + cursor: pointer; + + &:focus { + border-color: var(--color-mustard); + color: var(--color-mustard); + } + } + } + } + + #social .social-links { + display: flex; + flex-wrap: wrap; + gap: 20px 80px; + margin-top: 40px; + + .column { + display: flex; + flex-direction: column; + gap: 20px; + + a { + text-decoration: none; + display: flex; + + span { + line-height: 48px; + margin-left: 20px; + } + } + } + } +} + +#recent-news { + background-color: var(--color-mustard); + color: var(--color-navy); + + a { + color: var(--color-crimson); + } +} diff --git a/website/static/images/graphics/alpha.svg b/website/static/images/graphics/alpha.svg new file mode 100644 index 00000000..c29a4e47 --- /dev/null +++ b/website/static/images/graphics/alpha.svg @@ -0,0 +1,3 @@ + + + diff --git a/website/static/images/graphics/brush.svg b/website/static/images/graphics/brush.svg new file mode 100644 index 00000000..eb34378c --- /dev/null +++ b/website/static/images/graphics/brush.svg @@ -0,0 +1,4 @@ + + + + diff --git a/website/static/images/graphics/ferris-artist.svg b/website/static/images/graphics/ferris-artist.svg new file mode 100644 index 00000000..1fdc1430 --- /dev/null +++ b/website/static/images/graphics/ferris-artist.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/website/static/images/graphics/graphite-logo.svg b/website/static/images/graphics/graphite-logo.svg new file mode 100644 index 00000000..ceaeb10e --- /dev/null +++ b/website/static/images/graphics/graphite-logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/website/static/js/carousel.js b/website/static/js/carousel.js new file mode 100644 index 00000000..2294a8b8 --- /dev/null +++ b/website/static/js/carousel.js @@ -0,0 +1,157 @@ +let carouselImages; +let carouselDirectionPrev; +let carouselDirectionNext; +let carouselDots; +let carouselDescriptions; +let carouselDragLastClientX; +let velocityDeltaWindow = Array.from({ length: 20 }, () => ({ time: 0, delta: 0 })); + +window.addEventListener("DOMContentLoaded", initializeCarousel); +window.addEventListener("pointerup", dragEnd); +window.addEventListener("scroll", dragEnd); +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", false, true)); + carouselDirectionNext.addEventListener("click", () => slideDirection("next", false, true)); + Array.from(carouselDots).forEach((dot) => dot.addEventListener("click", (event) => { + const index = Array.from(carouselDots).indexOf(event.target); + slideTo(index, true); + })); +} + +function slideDirection(direction, clamped = false, smooth) { + 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() { + 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; + // 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) > 10) { + // 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", true, false); + 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 + if (closestImageIndex <= activeDotIndex) { + slideDirection("next", true, false); + return; + } + } + } + + // If we didn't slide in a direction due to clear velocity, just snap to the closest image + 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(); + 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..466ad066 --- /dev/null +++ b/website/static/js/navbar.js @@ -0,0 +1,115 @@ +const NAV_BUTTON_INITIAL_FONT_SIZE = 36; +const RIPPLE_ANIMATION_MILLISECONDS = 100; +const RIPPLE_WIDTH = 150; +const HANDLE_STRETCH = 0.4; + +let ripplesInitialized; +let navButtons; +let navButtonFontSize; +let rippleSvg; +let ripplePath; +let fullRippleHeight; +let ripples; +let activeRippleIndex; + +window.addEventListener("DOMContentLoaded", initializeRipples); +window.addEventListener("resize", () => animate(true)); + +function setRipples(mediaQueryScaleFactor) { + const rippleSvgRect = rippleSvg.getBoundingClientRect(); + const rippleSvgLeft = rippleSvgRect.left; + const rippleSvgWidth = rippleSvgRect.width; + + let path = `M 0,${fullRippleHeight + 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 = fullRippleHeight * (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},${fullRippleHeight + 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 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) - 4; + + ripples = Array.from(navButtons).map((button) => ({ + element: button, + animationStartTime: null, + animationEndTime: null, + goingUp: false, + })); + + activeRippleIndex = ripples.findIndex((ripple) => ripple.element.getAttribute("href") === window.location.pathname); + + + 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(); + }; + + ripple.element.addEventListener("pointerenter", () => updateTimings(true)); + ripple.element.addEventListener("pointerleave", () => updateTimings(false)); + }); + + ripples[activeRippleIndex] = { + ...ripples[activeRippleIndex], + animationStartTime: Date.now(), + animationEndTime: Date.now() + RIPPLE_ANIMATION_MILLISECONDS, + goingUp: true, + }; + + animate(); +} + +function animate(forceRefresh) { + if (!ripplesInitialized) return; + + navButtonFontSize = Number.parseInt(window.getComputedStyle(navButtons[0]).fontSize) || 36; + const mediaQueryScaleFactor = navButtonFontSize / NAV_BUTTON_INITIAL_FONT_SIZE; + + const animateThisFrame = ripples.some((ripple) => ripple.animationStartTime && ripple.animationEndTime && Date.now() <= ripple.animationEndTime); + if (animateThisFrame || forceRefresh) { + setRipples(mediaQueryScaleFactor); + window.requestAnimationFrame(animate); + } +} + +function ease(x) { + return 1 - (1 - x) * (1 - x); +} diff --git a/website/templates/404.html b/website/templates/404.html new file mode 100644 index 00000000..5ca96b89 --- /dev/null +++ b/website/templates/404.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} +{% block title %}Page not found.{% endblock title %} + +{% block content %} +
+
+

Page not found (yet).

+

+ While the home page is now online to get early feedback pending the full site launch, the remaining pages are still actively under construction. +

+

+ Please check back in a few days! +

+ Home Page +
+
+{% endblock content %} diff --git a/website/templates/base.html b/website/templates/base.html new file mode 100644 index 00000000..e5b1c238 --- /dev/null +++ b/website/templates/base.html @@ -0,0 +1,86 @@ + + + + + + Graphite | {% block title %}{% endblock title %} + {% block head %}{% endblock head %} + + + + + + + + +
+
+ + + + +
+
+
+
+ {% block content %} {% endblock %} +
+
+ +
+ + + diff --git a/website/templates/index.html b/website/templates/index.html new file mode 100644 index 00000000..c75efc56 --- /dev/null +++ b/website/templates/index.html @@ -0,0 +1,350 @@ +{% extends "base.html" %} + +{% block title %}{{ section.title }}{% endblock title %} + +{% block head %} + +{% endblock head %} + +{% block content %} + + + + + + +
+

Redefining state-of-the-art graphics editing.

+

Graphite is an in-development raster and vector 2D graphics editor that is free and open source. It is powered by a node graph compositing engine that supercharges your + layer stack, providing a completely non-destructive editing experience.

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

View of the current alpha version of the Graphite editor with a blank canvas. Try this out at editor.graphite.rs + right in your browser. Send in your artwork to potentially be featured here in place of this blank canvas.

+

Interface mockup showcasing a photo editing project that utilizes Graphite's raster graphics pipeline, one of the upcoming roadmap milestones. Photo editing + is not yet supported.

+

This last screenshot is currently identical to the previous one, serving as a placeholder. Work is ongoing to replace this image with a node graph mockup.

+
+
+
+
+ +
+
+ +
+
+

Professional 2D content creation for everyone.

+

+ With great power comes great accessibility. Graphite is built on the belief that the best creative tools can be powerful and within reach of all. +

+

+ The software is designed with a friendly and intuitive interface where a delightful user experience is of first-class importance. It is available for free under an open source license and usable instantly through a web + browser or an upcoming native client on Windows, Mac, and Linux. +

+

+ The accessible design of Graphite does not sacrifice versatility for simplicity. The node-based workflow (coming soon) will open doors to an ecosystem of powerful capabilities catering to + the casual and professional user alike, encompassing a wide set of use cases at every skill level. +

+ Mission Statement +
+
+ +
+
+ +
+
+

Available now for alpha testing.

+

+ One year ago, Graphite was merely an idea. Today, the first milestone of the alpha release series is available for testing. +

+

+ Milestone 1 focused on building an editor interface with basic vector design and illustration tools. Now the alpha release series moves toward milestone 2: developing a novel node-based + vector graphics workflow. After that, raster graphics and more are planned in the roadmap. +

+

+ Features and fixes will continue rolling out every few days. Please report bugs and vote on issue prioritization through + GitHub. While you're there, give the project a star to help grow its momentum. +

+

+ Try Graphite instantly in your browser: +

+ Launch the Editor +
+
+ +
+
+

Upcoming Tech / More in the Roadmap

+
+
+
+

Non-destructive editing, powered by nodes.

+ +

When editing in Graphite, your work gets described in nodes within your layers. Their parameters can be altered anytime in the creative process. The + simpler layer tree and wickedly powerful node graph provide two equivalent and interchangeable ways to create art.

+ Node Graph +
+
+

Raster and vector art, crisp at any resolution.

+ +

Just like vector artwork, which is based on curves instead of pixels to preserve quality at any scale, Graphite's raster paintbrushes, generators, and other tools + work the same way. A resolution-agnostic render engine lets you zoom infinitely and export at any size.

+ Rendering +
+
+

Procedural superpowers, part of your art pipeline.

+ +

Graphite aims to be the ultimate 2D tool for every technical artist. From procedural artwork to data viz and automation, it is designed from the ground up to fit into studio content + pipelines. You can also integrate Graphite's render engine into your game, app, or server.

+ Graphene +
+
+
+ +
+

And more to explore.

+

+ RAW photo editing. Procedural texture generation. Advanced typesetting and desktop publishing. Motion graphics and animation. Physically-based digital painting. HDR and wide-gamut + color handling (ACES/OpenColorIO). Real-time collaboration. A rich ecosystem of custom nodes. +

+

+ Learn more about the planned technology in forthcoming Graphite releases. +

+ Features +
+
+
+
+ +
+
+ +
+
+

Built for the future, powered by Rust.

+

+ Always on the bleeding edge and built to last— Graphite is written on a robust foundation with Rust, a modern programming language optimized for creating fast, reliable, future-proof + software. +

+

+ The underlying node graph engine that computes and renders Graphite documents is called Graphene. The Graphene engine is an extension of the Rust language, acting as a system for chaining + together modular functions into useful pipelines with GPU and parallel computation. Artists can harness these powerful capabilities directly in the Graphite editor without touching code. + Technical artists and programmers can write reusable Rust functions to extend the capabilities of Graphite and create new nodes to share with the community. +

+
+
+ +
+ +
+

Get involved.

+

+ The Graphite project could not exist without the community. Building its ambitious and diverse breadth of features will require contributions from developers, designers, technical experts, + creative professionals, and eagle-eyed bug hunters. Help build the future of digital art! Join the project Discord server and ask how you can + help. +

+

+ Rust, web, and graphics programmers should check out the development page for a quick technical overview and resources for getting started. Or just ask where to + begin in the #development channel on Discord and the community will get you set up. +

+
+
+ +
+
+
+

Stay in the loop.

+

+ Graphite is early in development and the most exciting, ambitious features are still to come. Enter your email below to receive only occasional announcements when major updates are + ready for your enjoyment. The first newsletter will likely be sent out for the Graphite alpha milestone 2 release, featuring node-based editing, later this year. +

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

Follow along.

+

+ High-quality open source software is a community endeavor. Whether you are a developer, an artist, or (like many of us) something in between— your presence in the Graphite community + will surely be valuable. As an avid user, tester, contributor, or just someone to cheer along from the sidelines, please join the conversation. +

+ +
+
+ +
+
+

Recent News / More in the Blog

+
+
+
+

Announcing Graphite alpha

+

+ February 12, 2022. After one year of development, the volunteer Graphite team is proud to present Graphite 0.1, the minimum viable product release of the Graphite Editor. This + milestone is only the beginning for the project— grand ambitions lie ahead as the development pace accelerates. We hope you will join us on the road to + 1.0 and beyond. +

+

+ This first release features the foundations of the Graphite Editor web client and simple SVG vector editing capabilities. It is presently suitable for basic vector illustration, + although many features remain unimplemented. Upcoming major milestones on the development roadmap will introduce the node-based non-destructive editing + workflow, raster graphics compositing, and a downloadable native desktop client for Windows, Mac, and Linux. +

+

+ Open the Editor instantly in your browser. +

+ Keep Reading +
+
+

The Graphite mission statement

+

+ February 12, 2022. This blog post is coming soon. Stay tuned! +

+ Keep Reading +
+
+
+
+ + +{% endblock content %}