Make builds of the editor and the website serve their own local fonts (#2186)

* WIP

* Done?

* Install fonts in CI

* Use absolute path so minified inlined CSS works

* Fix Bezier-rs demo fonts?

* Use opsz

* Revert removal of text balancer

* Pull in the text balancer from our static host
This commit is contained in:
Keavon Chambers 2025-05-19 02:38:29 -07:00 committed by GitHub
parent ea59f10b50
commit e57637aab1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 1305 additions and 755 deletions

View file

@ -13,7 +13,7 @@ on:
- website/** - website/**
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
INDEX_HTML_HEAD_INCLUSION: <script defer data-domain="graphite.rs" data-api="/visit/event" src="/visit/script.js"></script> INDEX_HTML_HEAD_INCLUSION: <script defer data-domain="graphite.rs" data-api="/visit/event" src="/visit/script.hash.js"></script>
jobs: jobs:
build: build:
@ -30,7 +30,7 @@ jobs:
- name: 🕸 Install Zola - name: 🕸 Install Zola
uses: taiki-e/install-action@v2 uses: taiki-e/install-action@v2
with: with:
tool: zola@0.19.1 tool: zola@0.20.0
- name: ✂ Replace template in <head> of index.html - name: ✂ Replace template in <head> of index.html
run: | run: |
@ -42,7 +42,8 @@ jobs:
MODE: prod MODE: prod
run: | run: |
cd website cd website
zola --config config_prod.toml build npm run install-fonts
zola --config config.toml build --minify
- name: 🔍 Check if `website/other` directory changed - name: 🔍 Check if `website/other` directory changed
uses: dorny/paths-filter@v3 uses: dorny/paths-filter@v3

6
Cargo.lock generated
View file

@ -3362,7 +3362,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"windows-targets 0.52.6", "windows-targets 0.48.5",
] ]
[[package]] [[package]]
@ -6635,9 +6635,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.44.0" version = "1.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9975ea0f48b5aa3972bf2d888c238182458437cc2a19374b81b25cdf1023fb3a" checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",

View file

@ -734,7 +734,7 @@ impl OverlayContext {
); );
} }
self.render_context.set_font("12px Source Sans Pro, Arial, sans-serif"); self.render_context.set_font(r#"12px "Source Sans Pro", Arial, sans-serif"#);
self.render_context.set_fill_style_str(font_color); self.render_context.set_fill_style_str(font_color);
self.render_context.fill_text(text, 0., 0.).expect("Failed to draw the text at the calculated position"); self.render_context.fill_text(text, 0., 0.).expect("Failed to draw the text at the calculated position");
self.render_context.reset_transform().expect("Failed to reset the render context transform"); self.render_context.reset_transform().expect("Failed to reset the render context transform");

View file

@ -22,7 +22,7 @@ Wraps the editor backend codebase (`/editor`) and provides a JS-centric API for
## ESLint configurations: `.eslintrc.js` ## ESLint configurations: `.eslintrc.js`
[ESLint](https://eslint.org/) is the tool which enforces style rules on the JS, TS, and Svelte files in our frontend codebase. As it is set up in this config file, ESLint will complain about bad practices and often help reformat code automatically when (in VS Code) the file is saved or `npm run lint` is executed. (If you don't use VS Code, remember to run this command before committing!) This config file for ESLint sets our style preferences and configures our usage of extensions/plugins for Svelte support, [Airbnb](https://github.com/airbnb/javascript)'s popular catalog of sane defaults, and [Prettier](https://prettier.io/)'s role as a code formatter. [ESLint](https://eslint.org/) is the tool which enforces style rules on the JS, TS, and Svelte files in our frontend codebase. As it is set up in this config file, ESLint will complain about bad practices and often help reformat code automatically when (in VS Code) the file is saved or `npm run lint` is executed. (If you don't use VS Code, remember to run this command before committing!) This config file for ESLint sets our style preferences and configures our usage of extensions/plugins for Svelte support and [Prettier](https://prettier.io/)'s role as a code formatter.
## npm ecosystem packages: `package.json` ## npm ecosystem packages: `package.json`

View file

@ -1,79 +1,54 @@
<!DOCTYPE html> <!doctype html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<title>Graphite</title> <title>Graphite</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?display=block&family=Source+Sans+Pro:ital,wght@0,400;0,700;1,400;1,700&family=Inconsolata:wght@400;700"> <link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png" />
<link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png"> <link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png"> <link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png" />
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png"> <link rel="manifest" href="site.webmanifest" />
<link rel="manifest" href="site.webmanifest"> <link rel="mask-icon" href="safari-pinned-tab.svg" color="#473a3a" />
<link rel="mask-icon" href="safari-pinned-tab.svg" color="#473a3a"> <meta name="viewport" content="width=device-width, initial-scale=0.5, maximum-scale=0.5, minimum-scale=0.5" />
<meta name="viewport" content="width=device-width, initial-scale=0.5, maximum-scale=0.5, minimum-scale=0.5"> <meta name="apple-mobile-web-app-title" content="Graphite" />
<meta name="apple-mobile-web-app-title" content="Graphite"> <meta name="application-name" content="Graphite" />
<meta name="application-name" content="Graphite"> <meta name="msapplication-TileColor" content="#ffffff" />
<meta name="msapplication-TileColor" content="#ffffff"> <meta name="theme-color" content="#ffffff" />
<meta name="theme-color" content="#ffffff"> <meta name="color-scheme" content="dark only" />
<meta name="color-scheme" content="dark only"> <meta name="darkreader-lock" />
<meta name="darkreader-lock"> <!-- INDEX_HTML_HEAD_REPLACEMENT -->
<!-- INDEX_HTML_HEAD_REPLACEMENT --> </head>
</head> <body>
<body> <noscript>JavaScript is required</noscript>
<noscript>JavaScript is required</noscript> <style>
<style> body {
body { background: #222;
background: #222; height: 100%;
height: 100%; margin: 0;
margin: 0;
}
body::after {
content: "";
display: block;
position: absolute;
left: 50%;
top: 50%;
width: 60px;
height: 60px;
border-radius: 50%;
border: 4px solid #eee;
border-color: #eee transparent #eee transparent;
animation: spinning-loading-indicator 1s linear infinite;
}
@keyframes spinning-loading-indicator {
0% {
transform: translate(-30px, -30px) rotate(0deg);
} }
100% {
transform: translate(-30px, -30px) rotate(360deg); body::after {
content: "";
display: block;
position: absolute;
left: 50%;
top: 50%;
width: 60px;
height: 60px;
border-radius: 50%;
border: 4px solid #eee;
border-color: #eee transparent #eee transparent;
animation: spinning-loading-indicator 1s linear infinite;
} }
}
</style>
<script>
// Confirm the browser is compatible before initializing the application
// Display an error if the browser is incompatible with a required API @keyframes spinning-loading-indicator {
// This is run outside the JS code bundle in case unsupported features cause a syntax error when parsing the bundle, preventing the any bundle code from running 0% {
let incompatibility; transform: translate(-30px, -30px) rotate(0deg);
if (!("BigUint64Array" in window)) { }
incompatibility = ` 100% {
<style> transform: translate(-30px, -30px) rotate(360deg);
body::after { content: none; } }
h2, p, a { text-align: center; color: #eee; font-family: "Source Sans Pro", Arial, sans-serif; } }
</style> </style>
<h2>This browser is too old to run Graphite</h2> <script type="module" src="src/main.ts"></script>
<p>Please upgrade to a modern web browser such as the latest Firefox, Chrome, Edge, or Safari version 15 or newer.</p> </body>
<p>(The <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt64Array#browser_compatibility" target="_blank"><code>BigInt64Array</code></a>
JavaScript API must be supported by the browser for Graphite to function.)</p>
`.trim();
}
// Load the editor application or display the incompatibility message
if (incompatibility) {
document.body.innerHTML += incompatibility;
}
</script>
<script type="module" src="src/main.ts"></script>
</body>
</html> </html>

View file

@ -7,7 +7,8 @@
"name": "graphite-web-frontend", "name": "graphite-web-frontend",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@tauri-apps/api": "^2.2.0", "@fontsource/inconsolata": "^5.2.5",
"@fontsource/source-sans-pro": "^5.2.5",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"idb-keyval": "^6.2.1", "idb-keyval": "^6.2.1",
"reflect-metadata": "^0.2.2" "reflect-metadata": "^0.2.2"
@ -555,6 +556,21 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0" "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
} }
}, },
"node_modules/@fontsource/inconsolata": {
"version": "5.2.5",
"resolved": "https://registry.npmjs.org/@fontsource/inconsolata/-/inconsolata-5.2.5.tgz",
"integrity": "sha512-OvzkZY5qYghv/jEV6cfGZzFhdFTvSnU+ExPC7WcZ7w8PdRhtiu/SpcBWOBt+3LXgS0n9qyepgq4zZmxlDTlGGQ==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource/source-sans-pro": {
"version": "5.2.5",
"resolved": "https://registry.npmjs.org/@fontsource/source-sans-pro/-/source-sans-pro-5.2.5.tgz",
"integrity": "sha512-ypendqc4pYUc+EgF7qqPY9iVYEz1t/Qr03VojKxG/2g3dnpHa1B6DOlDxWQjQXDj5QrG6inEqGT0g+edjALZyg==",
"license": "OFL-1.1"
},
"node_modules/@humanwhocodes/config-array": { "node_modules/@humanwhocodes/config-array": {
"version": "0.13.0", "version": "0.13.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
@ -1050,16 +1066,6 @@
"vite": "^5.0.0" "vite": "^5.0.0"
} }
}, },
"node_modules/@tauri-apps/api": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.2.0.tgz",
"integrity": "sha512-R8epOeZl1eJEl603aUMIGb4RXlhPjpgxbGVEaqY+0G5JG9vzV/clNlzTeqc+NLYXVqXcn8mb4c5b9pJIUDEyAg==",
"license": "Apache-2.0 OR MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/tauri"
}
},
"node_modules/@tsconfig/node10": { "node_modules/@tsconfig/node10": {
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",

View file

@ -29,6 +29,8 @@
"wasm:watch-production": "cargo watch --postpone --watch-when-idle --workdir=wasm --shell \"wasm-pack build . --release --target=web -- --color=always\"" "wasm:watch-production": "cargo watch --postpone --watch-when-idle --workdir=wasm --shell \"wasm-pack build . --release --target=web -- --color=always\""
}, },
"dependencies": { "dependencies": {
"@fontsource/inconsolata": "^5.2.5",
"@fontsource/source-sans-pro": "^5.2.5",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"idb-keyval": "^6.2.1", "idb-keyval": "^6.2.1",
"reflect-metadata": "^0.2.2" "reflect-metadata": "^0.2.2"

View file

@ -1,6 +1,13 @@
// This file is the browser's entry point for the JS bundle // This file is the browser's entry point for the JS bundle
// reflect-metadata allows for runtime reflection of types in JavaScript. // Fonts
import "@fontsource/inconsolata";
import "@fontsource/source-sans-pro/400-italic.css";
import "@fontsource/source-sans-pro/400.css";
import "@fontsource/source-sans-pro/700-italic.css";
import "@fontsource/source-sans-pro/700.css";
// `reflect-metadata` allows for runtime reflection of types in JavaScript.
// It is needed for class-transformer to work and is imported as a side effect. // It is needed for class-transformer to work and is imported as a side effect.
// The library replaces the Reflect API on the window to support more features. // The library replaces the Reflect API on the window to support more features.
import "reflect-metadata"; import "reflect-metadata";

View file

@ -9,11 +9,13 @@ module.exports = {
ecmaVersion: 2020, ecmaVersion: 2020,
}, },
extends: [ extends: [
// JS defaults "eslint:recommended",
"airbnb-base", "plugin:import/recommended",
// General Prettier defaults "plugin:@typescript-eslint/recommended",
"plugin:import/typescript",
"prettier", "prettier",
], ],
plugins: ["import", "@typescript-eslint", "prettier"],
settings: { settings: {
// https://github.com/import-js/eslint-plugin-import#resolvers // https://github.com/import-js/eslint-plugin-import#resolvers
"import/resolver": { "import/resolver": {
@ -30,7 +32,6 @@ module.exports = {
"!.*.js", "!.*.js",
"!.*.ts", "!.*.ts",
], ],
plugins: ["prettier"],
rules: { rules: {
// Standard ESLint config // Standard ESLint config
indent: "off", indent: "off",

2
website/.gitignore vendored
View file

@ -1,3 +1,5 @@
node_modules/ node_modules/
public/ public/
static/fonts/
static/syntax-highlighting.css static/syntax-highlighting.css
static/text-balancer.js

View file

@ -1,17 +0,0 @@
title = "Graphite"
description = "2D raster & vector editor that melds traditional layers & tools with a modern node-based procedural workflow."
base_url = "https://graphite.rs"
feed_filenames = ["rss.xml"]
compile_sass = true
minify_html = true
[markdown]
highlight_code = true
highlight_theme = "css"
highlight_themes_css = [
{ theme = "kronuz", filename = "syntax-highlighting.css" },
]
[extra]
# Put all your custom variables here

View file

@ -3,12 +3,14 @@ title = "Free online vector editor & procedural design tool"
template = "section.html" template = "section.html"
[extra] [extra]
css = ["/page/index.css", "/component/carousel.css", "/component/feature-icons.css", "/component/feature-box.css", "/component/youtube-embed.css", "/layout/balance-text.css"] css = ["/page/index.css", "/component/carousel.css", "/component/feature-icons.css", "/component/feature-box.css", "/component/youtube-embed.css"]
js = ["/js/carousel.js", "/js/youtube-embed.js", "/js/video-autoplay.js"] js = ["/js/carousel.js", "/js/youtube-embed.js", "/js/video-autoplay.js"]
linked_js = ["https://static.graphite.rs/text-balancer/text-balancer.js"] linked_js = []
meta_description = "Open source free software. A vector graphics creativity suite with a clean, intuitive interface. Opens instantly (no signup) and runs locally in a browser. Exports SVG, PNG, JPG." meta_description = "Open source free software. A vector graphics creativity suite with a clean, intuitive interface. Opens instantly (no signup) and runs locally in a browser. Exports SVG, PNG, JPG."
+++ +++
<!-- replacements::text_balancer() -->
<!-- ▛ LOGO ▜ --> <!-- ▛ LOGO ▜ -->
<section id="logo"> <section id="logo">
<div class="block"> <div class="block">

View file

@ -3,7 +3,6 @@ title = "About Graphite"
[extra] [extra]
css = ["/page/about.css", "/component/feature-box.css"] css = ["/page/about.css", "/component/feature-box.css"]
linked_css = ["https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&display=swap"]
+++ +++
<section> <section>
@ -117,7 +116,7 @@ It's easy to learn and teach, yet Graphite's accessible design does not sacrific
<img src="https://static.graphite.rs/content/about/core-team-photo-keavon-chambers.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" alt="Photo of Keavon Chambers" /> <img src="https://static.graphite.rs/content/about/core-team-photo-keavon-chambers.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" alt="Photo of Keavon Chambers" />
## Keavon Chambers <span class="handle">(@Keavon)</span> <span class="flag" title="American">🇺🇸</span> ## Keavon Chambers <span class="handle">(@Keavon)</span> <img src="https://static.graphite.rs/icons/flags/us.png" class="flag" title="American" />
***Founder, UI & product design, frontend, editor systems*** ***Founder, UI & product design, frontend, editor systems***
@ -128,7 +127,7 @@ Keavon is a creative generalist with a love for the fusion of arts and technolog
<img src="https://static.graphite.rs/content/about/core-team-photo-dennis-kobert.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" alt="Photo of Dennis Kobert" /> <img src="https://static.graphite.rs/content/about/core-team-photo-dennis-kobert.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" alt="Photo of Dennis Kobert" />
## Dennis Kobert <span class="handle">(@TrueDoctor)</span> <span class="flag" title="German">🇩🇪</span> ## Dennis Kobert <span class="handle">(@TrueDoctor)</span> <img src="https://static.graphite.rs/icons/flags/de.png" class="flag" title="German" />
***Graphene node engine, research, architecture*** ***Graphene node engine, research, architecture***
@ -143,7 +142,7 @@ Dennis is a mix between a mathematician and a mad scientist. While still enjoyin
<img src="https://static.graphite.rs/content/about/core-team-photo-hypercube__2.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" alt="Photo of Hypercube" /> <img src="https://static.graphite.rs/content/about/core-team-photo-hypercube__2.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" alt="Photo of Hypercube" />
## "Hypercube" <span class="handle">(@0Hypercube)</span> <span class="flag" title="British">🇬🇧</span> ## "Hypercube" <span class="handle">(@0Hypercube)</span> <img src="https://static.graphite.rs/icons/flags/gb.png" class="flag" title="British" />
***Editor systems, nodes, tools, architecture*** ***Editor systems, nodes, tools, architecture***
@ -155,7 +154,7 @@ Dennis is a mix between a mathematician and a mad scientist. While still enjoyin
<img src="https://static.graphite.rs/content/about/core-team-photo-adam-gerhant.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" alt="Photo of Adam Gerhant" /> <img src="https://static.graphite.rs/content/about/core-team-photo-adam-gerhant.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" alt="Photo of Adam Gerhant" />
## Adam Gerhant <span class="handle">(@pendapia)</span> <span class="flag" title="American">🇺🇸</span> ## Adam Gerhant <span class="handle">(@pendapia)</span> <img src="https://static.graphite.rs/icons/flags/us.png" class="flag" title="American" />
***Editor graph tooling, node data formats*** ***Editor graph tooling, node data formats***

View file

@ -145,17 +145,21 @@ Always on the bleeding edge and built to last— Graphite is written on a robust
<img class="atlas" style="--atlas-index: 7" src="https://static.graphite.rs/icons/icon-atlas-roadmap__3.png" alt="" /> <img class="atlas" style="--atlas-index: 7" src="https://static.graphite.rs/icons/icon-atlas-roadmap__3.png" alt="" />
<span>Desktop app (Windows, Mac, Linux)</span> <span>Desktop app (Windows, Mac, Linux)</span>
</div> </div>
<div class="feature-icon ongoing"> <div class="feature-icon ongoing" title="Development Ongoing">
<img class="atlas" style="--atlas-index: 40" src="https://static.graphite.rs/icons/icon-atlas-roadmap__3.png" alt="" /> <img class="atlas" style="--atlas-index: 40" src="https://static.graphite.rs/icons/icon-atlas-roadmap__3.png" alt="" />
<span>Simplified main properties panel</span> <span>Simplified main properties panel</span>
</div> </div>
<div class="feature-icon"> <div class="feature-icon ongoing" title="Development Ongoing">
<img class="atlas" style="--atlas-index: 12" src="https://static.graphite.rs/icons/icon-atlas-roadmap__3.png" alt="" /> <img class="atlas" style="--atlas-index: 12" src="https://static.graphite.rs/icons/icon-atlas-roadmap__3.png" alt="" />
<span>GPU-accelerated raster rendering</span> <span>GPU-accelerated raster rendering</span>
</div> </div>
<div class="feature-icon"> <div class="feature-icon">
<img class="atlas" style="--atlas-index: 14" src="https://static.graphite.rs/icons/icon-atlas-roadmap__3.png" alt="" /> <img class="atlas" style="--atlas-index: 14" src="https://static.graphite.rs/icons/icon-atlas-roadmap__3.png" alt="" />
<span>Infinitely zoomable/panable content</span> <span>Infinitely zoomable/panable raster</span>
</div>
<div class="feature-icon">
<img class="atlas" style="--atlas-index: 21" src="https://static.graphite.rs/icons/icon-atlas-roadmap__3.png" alt="" />
<span>Select mode (marquee masking)</span>
</div> </div>
<div class="feature-icon"> <div class="feature-icon">
<img class="atlas" style="--atlas-index: 41" src="https://static.graphite.rs/icons/icon-atlas-roadmap__3.png" alt="" /> <img class="atlas" style="--atlas-index: 41" src="https://static.graphite.rs/icons/icon-atlas-roadmap__3.png" alt="" />
@ -171,7 +175,7 @@ Always on the bleeding edge and built to last— Graphite is written on a robust
</div> </div>
<div class="feature-icon"> <div class="feature-icon">
<img class="atlas" style="--atlas-index: 57" src="https://static.graphite.rs/icons/icon-atlas-roadmap__3.png" alt="" /> <img class="atlas" style="--atlas-index: 57" src="https://static.graphite.rs/icons/icon-atlas-roadmap__3.png" alt="" />
<span>Signed distance fields</span> <span>Signed distance field rendering</span>
</div> </div>
<div class="feature-icon"> <div class="feature-icon">
<img class="atlas" style="--atlas-index: 56" src="https://static.graphite.rs/icons/icon-atlas-roadmap__3.png" alt="" /> <img class="atlas" style="--atlas-index: 56" src="https://static.graphite.rs/icons/icon-atlas-roadmap__3.png" alt="" />
@ -181,10 +185,6 @@ Always on the bleeding edge and built to last— Graphite is written on a robust
<img class="atlas" style="--atlas-index: 52" src="https://static.graphite.rs/icons/icon-atlas-roadmap__3.png" alt="" /> <img class="atlas" style="--atlas-index: 52" src="https://static.graphite.rs/icons/icon-atlas-roadmap__3.png" alt="" />
<span>Command palette and context menus</span> <span>Command palette and context menus</span>
</div> </div>
<div class="feature-icon">
<img class="atlas" style="--atlas-index: 21" src="https://static.graphite.rs/icons/icon-atlas-roadmap__3.png" alt="" />
<span>Select mode (marquee masking)</span>
</div>
<div class="feature-icon"> <div class="feature-icon">
<img class="atlas" style="--atlas-index: 53" src="https://static.graphite.rs/icons/icon-atlas-roadmap__3.png" alt="" /> <img class="atlas" style="--atlas-index: 53" src="https://static.graphite.rs/icons/icon-atlas-roadmap__3.png" alt="" />
<span>Local fonts access</span> <span>Local fonts access</span>

194
website/install-fonts.js Normal file
View file

@ -0,0 +1,194 @@
const fs = require("fs");
const https = require("https");
const path = require("path");
// Define basePath
const basePath = path.resolve(__dirname);
// Define files to copy as [source, destination] pairs
// Files with the same destination will be concatenated
const FILES_TO_COPY = [
["node_modules/@fontsource-variable/inter/opsz.css", "static/fonts/common.css"],
["node_modules/@fontsource-variable/inter/opsz-italic.css", "static/fonts/common.css"],
["node_modules/@fontsource/bona-nova/700.css", "static/fonts/common.css"],
];
// Define directories to copy recursively as [source, destination] pairs
const DIRECTORIES_TO_COPY = [
["node_modules/@fontsource-variable/inter/files", "static/fonts/files"],
["node_modules/@fontsource/bona-nova/files", "static/fonts/files"],
];
// Track processed destination files and CSS content
const processedDestinations = new Set();
const cssDestinations = new Set();
const allCopiedFiles = new Set();
// Process each file
FILES_TO_COPY.forEach(([source, dest]) => {
// Convert relative paths to absolute paths
const sourcePath = path.join(basePath, source);
const destPath = path.join(basePath, dest);
// Track CSS destinations for later analysis
if (dest.endsWith(".css")) {
cssDestinations.add(destPath);
}
// Ensure destination directory exists
const destDir = path.dirname(destPath);
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true });
console.log(`Created directory: ${destDir}`);
}
try {
// Read source file content
const content = fs.readFileSync(sourcePath, "utf8");
// Check if destination has been processed before
if (processedDestinations.has(destPath)) {
// Append to existing file
fs.appendFileSync(destPath, "\n\n" + content);
console.log(`Appended: ${sourcePath}${destPath}`);
} else {
// First time writing to this destination - copy the file
fs.writeFileSync(destPath, content);
processedDestinations.add(destPath);
console.log(`Copied: ${sourcePath}${destPath}`);
}
// Replace all occurrences of "./files" with "/fonts" in the destination file
let destFileContent = fs.readFileSync(destPath, "utf8");
destFileContent = destFileContent.replaceAll("./files/", "/fonts/files/");
fs.writeFileSync(destPath, destFileContent);
} catch (error) {
console.error(`Error processing ${sourcePath} to ${destPath}:`, error);
process.exit(1);
}
});
// Function to recursively copy a directory
function copyDirectoryRecursive(source, destination) {
// Ensure destination directory exists
if (!fs.existsSync(destination)) {
fs.mkdirSync(destination, { recursive: true });
console.log(`Created directory: ${destination}`);
}
// Get all items in the source directory
const items = fs.readdirSync(source);
// Process each item
items.forEach((item) => {
const sourcePath = path.join(source, item);
const destPath = path.join(destination, item);
// Check if item is a directory or file
const stats = fs.statSync(sourcePath);
if (stats.isDirectory()) {
// Recursively copy subdirectory
copyDirectoryRecursive(sourcePath, destPath);
} else {
// Copy file and track it
fs.copyFileSync(sourcePath, destPath);
allCopiedFiles.add(destPath);
console.log(`Copied: ${sourcePath}${destPath}`);
}
});
}
// Process each directory
DIRECTORIES_TO_COPY.forEach(([source, dest]) => {
// Convert relative paths to absolute paths
const sourcePath = path.join(basePath, source);
const destPath = path.join(basePath, dest);
try {
copyDirectoryRecursive(sourcePath, destPath);
console.log(`Copied directory: ${sourcePath}${destPath}`);
} catch (error) {
console.error(`Error copying directory ${sourcePath} to ${destPath}:`, error);
process.exit(1);
}
});
console.log("All files and directories copied successfully!");
// Now check which of the copied files are actually referenced in CSS
console.log("\nChecking for unused font files...");
// Read all CSS content and join it
let allCssContent = "";
cssDestinations.forEach((cssPath) => {
try {
const content = fs.readFileSync(cssPath, "utf8");
allCssContent += content;
} catch (error) {
console.error(`Error reading CSS file ${cssPath}:`, error);
}
});
// Filter files that aren't referenced in CSS
const unusedFiles = [];
allCopiedFiles.forEach((filePath) => {
const fileName = path.basename(filePath);
// Check if the file name is mentioned in any CSS
if (!allCssContent.includes(fileName)) {
unusedFiles.push(filePath);
}
});
// Delete unused files
if (unusedFiles.length > 0) {
console.log(`Found ${unusedFiles.length} unused font files to delete:`);
unusedFiles.forEach((filePath) => {
try {
fs.unlinkSync(filePath);
console.log(`Deleted unused file: ${filePath}`);
} catch (error) {
console.error(`Error deleting file ${filePath}:`, error);
}
});
} else {
console.log("No unused font files found.");
}
console.log("\nFont installation complete!");
// Fetch and save text-balancer.js, which we don't commit to the repo so we're not version controlling dependency code
const textBalancerUrl = "https://static.graphite.rs/text-balancer/text-balancer.js";
const textBalancerDest = path.join(basePath, "static", "text-balancer.js");
console.log("\nDownloading text-balancer.js...");
https
.get(textBalancerUrl, (res) => {
if (res.statusCode !== 200) {
console.error(`Failed to download text-balancer.js. Status code: ${res.statusCode}`);
res.resume();
return;
}
let data = "";
res.on("data", (chunk) => {
data += chunk;
});
res.on("end", () => {
try {
// Ensure destination directory exists
const destDir = path.dirname(textBalancerDest);
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true });
console.log(`Created directory: ${destDir}`);
}
fs.writeFileSync(textBalancerDest, data, "utf8");
console.log(`Downloaded and saved: ${textBalancerDest}`);
} catch (error) {
console.error(`Error saving text-balancer.js:`, error);
}
});
})
.on("error", (err) => {
console.error(`Error downloading text-balancer.js:`, err);
});

View file

@ -4,9 +4,7 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>Bezier-rs Interactive Documentation</title> <title>Bezier-rs Interactive Documentation</title>
<link rel="stylesheet" href="./style.css" /> <link rel="stylesheet" href="./style.css" />
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link href="fonts.css" rel="stylesheet" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Bona+Nova:wght@700&family=Inter:wght@500&display=swap" rel="stylesheet" />
</head> </head>
<body> <body>
<noscript>JavaScript is required</noscript> <noscript>JavaScript is required</noscript>

View file

@ -8,7 +8,7 @@
html, html,
body { body {
font-family: "Inter", sans-serif; font-family: "Inter Variable", sans-serif;
text-align: center; text-align: center;
background-color: white; background-color: white;
} }

View file

@ -29,5 +29,6 @@ mkdir dist/libraries/bezier-rs
cd bezier-rs-demos cd bezier-rs-demos
npm ci npm ci
NODE_ENV=production npm run build NODE_ENV=production npm run build
cp ../../static/fonts/common.css dist/fonts.css
mv dist/* ../dist/libraries/bezier-rs mv dist/* ../dist/libraries/bezier-rs
cd .. cd ..

1507
website/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,6 @@
"name": "graphite-website", "name": "graphite-website",
"description": "Graphite's website. This npm package is for dev tooling only, such as eslint.", "description": "Graphite's website. This npm package is for dev tooling only, such as eslint.",
"private": true, "private": true,
"scripts": {},
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/GraphiteEditor/Graphite.git" "url": "git+https://github.com/GraphiteEditor/Graphite.git"
@ -10,15 +9,22 @@
"author": "Graphite Authors <contact@graphite.rs>", "author": "Graphite Authors <contact@graphite.rs>",
"license": "Apache-2.0", "license": "Apache-2.0",
"homepage": "https://graphite.rs", "homepage": "https://graphite.rs",
"scripts": {
"install-fonts": "npm ci && node install-fonts.js"
},
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "^8.7.0", "@typescript-eslint/eslint-plugin": "^8.7.0",
"@typescript-eslint/parser": "^8.7.0", "@typescript-eslint/parser": "^8.7.0",
"eslint": "^9.11.1", "eslint": "^9.11.1",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.3",
"eslint-plugin-import": "^2.30.0", "eslint-plugin-import": "^2.30.0",
"eslint-plugin-prettier": "^5.2.1", "eslint-plugin-prettier": "^5.2.1",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"sass": "^1.78.0" "sass": "1.78.0"
},
"dependencies": {
"@fontsource-variable/inter": "^5.2.5",
"@fontsource/bona-nova": "^5.2.5"
} }
} }

View file

@ -39,7 +39,7 @@ html,
body { body {
color: var(--color-navy); color: var(--color-navy);
background: white; background: white;
font-family: "Inter", sans-serif; font-family: "Inter Variable", sans-serif;
line-height: 1.5; line-height: 1.5;
font-weight: 500; font-weight: 500;
font-size: 18px; font-size: 18px;
@ -157,12 +157,12 @@ body > .page {
background-image: url('data:image/svg+xml;utf8,\ background-image: url('data:image/svg+xml;utf8,\
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M8,15C5.12471,9.753694 0.5,8.795225 0.5,4.736524 C0.5,-0.507473 7.468734,0 8,4.967381 C8.531266,0 15.5,-0.507473 15.5,4.736524 C15.5,8.795225 10.87529,9.753694 8,15z" fill="%23cc304f" /></svg>\ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M8,15C5.12471,9.753694 0.5,8.795225 0.5,4.736524 C0.5,-0.507473 7.468734,0 8,4.967381 C8.531266,0 15.5,-0.507473 15.5,4.736524 C15.5,8.795225 10.87529,9.753694 8,15z" fill="%23cc304f" /></svg>\
'); ');
display: inline-block; display: inline-block;
width: 0.75em; width: 0.75em;
height: 0.75em; height: 0.75em;
margin-left: 0.25em; margin-left: 0.25em;
margin-bottom: -0.1em; margin-bottom: -0.1em;
vertical-align: baseline; vertical-align: baseline;
} }
} }
@ -323,7 +323,6 @@ body > .page {
margin-top: 20px; margin-top: 20px;
} }
} }
} }
details[open] summary::before { details[open] summary::before {
@ -436,7 +435,7 @@ h3,
h4, h4,
h5, h5,
h6 { h6 {
font-family: "Inter", sans-serif; font-family: "Inter Variable", sans-serif;
line-height: 1.5; line-height: 1.5;
font-weight: 800; font-weight: 800;
display: inline-block; display: inline-block;
@ -663,7 +662,7 @@ hr,
.arrow::after { .arrow::after {
content: " »"; content: " »";
font-family: "Inter", sans-serif; font-family: "Inter Variable", sans-serif;
} }
.video-background { .video-background {

View file

@ -38,7 +38,7 @@ pre {
content: attr(data-lang); content: attr(data-lang);
color: rgba(var(--color-seaside-rgb), 0.5); color: rgba(var(--color-seaside-rgb), 0.5);
text-transform: lowercase; text-transform: lowercase;
font-family: "Inter", sans-serif; font-family: "Inter Variable", sans-serif;
font-size: 0.75em; font-size: 0.75em;
font-weight: 700; font-weight: 700;
font-style: italic; font-style: italic;

View file

@ -46,7 +46,7 @@
h1.feature-box-header.feature-box-header { h1.feature-box-header.feature-box-header {
&, &,
& a { & a {
font-family: "Inter", sans-serif; font-family: "Inter Variable", sans-serif;
line-height: 1.5; line-height: 1.5;
font-weight: 800; font-weight: 800;
text-transform: uppercase; text-transform: uppercase;

View file

@ -1,22 +0,0 @@
// CSS component of the JS text balancer script.
// This must be loaded as an inline stylesheet in the head of the
// document to avoid the possibility of a visible layout shift.
.balance-text {
visibility: hidden;
}
@media (scripting: none) {
.balance-text {
visibility: visible !important;
}
}
@supports (text-wrap: balance) {
.balance-text,
.balanced-text {
text-align: left;
text-wrap: balance;
visibility: visible;
}
}

View file

@ -5,8 +5,12 @@
animation: fadeInAfterWait 2s ease-in 2s forwards; animation: fadeInAfterWait 2s ease-in 2s forwards;
@keyframes fadeInAfterWait { @keyframes fadeInAfterWait {
0% { opacity: 0; } 0% {
100% { opacity: 1; } opacity: 0;
}
100% {
opacity: 1;
}
} }
} }
@ -34,11 +38,9 @@
} }
.flag { .flag {
font-family: "Noto Color Emoji", sans-serif; width: 1em;
font-style: normal; height: 1em;
vertical-align: middle; vertical-align: middle;
font-size: 0.8em;
cursor: default;
} }
} }
} }

View file

@ -43,7 +43,7 @@
</div> </div>
</section> </section>
{%- if not page.summary -%} {%- if not page.summary -%}
{{ throw(message = "ARTICLE HAS NO SUMMARY! After the first paragraph (or two short ones), a `<!-- more -->` comment must be inserted in the markdown. Otherwise the blog page would be missing its preview text." | safe) }} {{ throw(message = "------------------------------------------------------------> ARTICLE HAS NO SUMMARY! After the first paragraph (or two short ones), a `<!-- more -->` comment must be inserted in the markdown. Otherwise the blog page would be missing its preview text." | safe) }}
{%- endif -%} {%- endif -%}
{%- endblock content -%} {%- endblock content -%}

View file

@ -27,15 +27,19 @@
{% block rss -%} {% block rss -%}
<link rel="alternate" type="application/rss+xml" title="RSS" href="{{ get_url(path = 'blog/rss.xml', trailing_slash = false) | safe }}" /> <link rel="alternate" type="application/rss+xml" title="RSS" href="{{ get_url(path = 'blog/rss.xml', trailing_slash = false) | safe }}" />
{%- endblock %} {%- endblock %}
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
{#- ======================================================================== -#}
{#- ON EVERY PAGE OF THE SITE: CSS AND JS TO LOAD EITHER AS A LINK OR INLINE -#} {#- ON EVERY PAGE OF THE SITE: CSS AND JS TO LOAD EITHER AS A LINK OR INLINE -#}
{#- ======================================================================== -#} {#- ======================================================================== -#}
{%- set global_linked_css = ["https://fonts.googleapis.com/css2?family=Bona+Nova:wght@700&family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"] -%}
{%- set global_linked_js = [] -%} {%- set global_linked_js = [] -%}
{%- set global_css = ["/base.css"] -%} {%- set global_linked_css = [] -%}
{%- set global_js = ["/js/text-justification.js", "/js/navbar.js"] -%} {%- set global_js = ["/js/text-justification.js", "/js/navbar.js"] -%}
{%- set global_css = ["/base.css", "/fonts/common.css"] -%}
{%- set fonts_loaded = load_data(path="static/fonts/common.css", format="plain", required=false) -%}
{%- if not fonts_loaded -%}
{{ throw(message="------------------------------------------------------------> FONTS ARE NOT INSTALLED! Before running Zola, execute `npm run install-fonts` from the `/website` directory.") }}
{%- endif -%}
{#- RETRIEVE FROM TEMPLATES AND PAGES: CSS AND JS TO LOAD EITHER AS A LINK OR INLINE -#} {#- RETRIEVE FROM TEMPLATES AND PAGES: CSS AND JS TO LOAD EITHER AS A LINK OR INLINE -#}
{#- ================================================================================ -#} {#- ================================================================================ -#}
@ -89,6 +93,7 @@
{{ load_data(path = path) | safe }} {{ load_data(path = path) | safe }}
{{ "</" ~ "script>" | safe }} {{ "</" ~ "script>" | safe }}
{%- endfor %} {%- endfor %}
{{- get_env(name = "INDEX_HTML_HEAD_INCLUSION", default = "") | safe }} {{- get_env(name = "INDEX_HTML_HEAD_INCLUSION", default = "") | safe }}
</head> </head>
<body> <body>
@ -119,8 +124,10 @@
</header> </header>
<main> <main>
{%- filter replace(from = "<!-- replacements::blog_posts(count = 2) -->", to = replacements::blog_posts(count = 2)) -%} {%- filter replace(from = "<!-- replacements::blog_posts(count = 2) -->", to = replacements::blog_posts(count = 2)) -%}
{%- filter replace(from = "<!-- replacements::text_balancer() -->", to = replacements::text_balancer()) -%}
{%- block content -%}{%- endblock -%} {%- block content -%}{%- endblock -%}
{%- endfilter -%} {%- endfilter -%}
{%- endfilter -%}
</main> </main>
<footer> <footer>
<hr /> <hr />

View file

@ -12,3 +12,29 @@
</div> </div>
{% endfor %} {% endfor %}
{% endmacro blog_posts %} {% endmacro blog_posts %}
{% macro text_balancer() %}
<style>
.balance-text {
visibility: hidden;
}
@media (scripting: none) {
.balance-text {
visibility: visible !important;
}
}
@supports (text-wrap: balance) {
.balance-text,
.balanced-text {
text-align: left;
text-wrap: balance;
visibility: visible;
}
}
</style>
<script>
{{ load_data(path="static/text-balancer.js", format="plain") | safe }}
</script>
{% endmacro text_balancer %}