feat(website): add the fl.narl.io site
Check and Windows preview / check (anvil) (push) Successful in 49s
Check and Windows preview / check (crucible) (push) Successful in 48s
Check and Windows preview / check (knurl) (push) Successful in 40s
Check and Windows preview / check (lathe) (push) Successful in 41s
Check and Windows preview / check (patina) (push) Successful in 56s
Check and Windows preview / check (rivet) (push) Successful in 45s
Check and Windows preview / check (temper) (push) Successful in 43s
Check and Windows preview / check (weld) (push) Successful in 1m20s
Check and Windows preview / previews (push) Successful in 1m15s
Check and Windows preview / windows (push) Successful in 2m31s
Check and Windows preview / check (anvil) (push) Successful in 49s
Check and Windows preview / check (crucible) (push) Successful in 48s
Check and Windows preview / check (knurl) (push) Successful in 40s
Check and Windows preview / check (lathe) (push) Successful in 41s
Check and Windows preview / check (patina) (push) Successful in 56s
Check and Windows preview / check (rivet) (push) Successful in 45s
Check and Windows preview / check (temper) (push) Successful in 43s
Check and Windows preview / check (weld) (push) Successful in 1m20s
Check and Windows preview / previews (push) Successful in 1m15s
Check and Windows preview / windows (push) Successful in 2m31s
Astro site for the suite: a home page with the plugin cards, install steps, principles, the metalwork names and the ideas on the bench, one page per plugin, and the Impressum and privacy policy. Plugin names, versions, download links and editor previews come from the catalog, the wrapper manifests and docs/*/assets at build time. Served by unprivileged nginx in Docker behind Traefik on fl.narl.io, the same setup as proton.narl.io: no access logs, CSP 'self', no JavaScript, legal pages filled from the environment at start.
This commit is contained in:
1 parent
5cc3770737
commit
7e1776bb04
40 files changed
+8515
No files matched your search
@@ -2,6 +2,8 @@
|
||||
|
||||
**Rust CLAP plugins for writing, designing and mixing bass music in FL Studio.**
|
||||
|
||||
Website: [fl.narl.io](https://fl.narl.io)
|
||||
|
||||

|
||||

|
||||

|
||||
@@ -163,6 +165,7 @@ scripts/ Build, check, packaging, release and analysis tools
|
||||
docs/<plugin>/ Guide, changelog and editor preview for each plugin
|
||||
docs/ideas.md Problems in bass-music production and the plugin ideas for them
|
||||
references/ Local reference audio; never distributed
|
||||
website/ The fl.narl.io site: Astro, nginx container, reads catalog and previews
|
||||
```
|
||||
|
||||
Each product follows the same split:
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Copy to .env and fill in. The container refuses to start while a required value is missing,
|
||||
# so the site never goes live without an Impressum.
|
||||
|
||||
# Public URL of the site, used for canonical links, the sitemap and robots.txt (build time).
|
||||
SITE_URL=https://fl.narl.io
|
||||
|
||||
# Impressum (§ 5 DDG) and controller in the privacy policy. Required.
|
||||
# The address must be one where legal documents can be served: no P.O. box.
|
||||
IMPRESSUM_NAME=Max Mustermann
|
||||
IMPRESSUM_STREET=Musterstraße 1
|
||||
IMPRESSUM_CITY=12345 Musterstadt
|
||||
IMPRESSUM_EMAIL=kontakt@example.org
|
||||
|
||||
# Optional. Defaults to "Deutschland".
|
||||
IMPRESSUM_COUNTRY=Deutschland
|
||||
# Optional. Leave empty to hide the phone line.
|
||||
IMPRESSUM_PHONE=
|
||||
# Optional. Name and address of the hosting provider, if one processes visitor data for you
|
||||
# (Art. 28 GDPR), e.g. "Hetzner Online GmbH, Industriestr. 25, 91710 Gunzenhausen".
|
||||
# Leave empty to hide that paragraph.
|
||||
HOSTER=
|
||||
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.astro/
|
||||
.env
|
||||
@@ -0,0 +1,30 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# Build context is the repository root, so the site can read plugins/catalog.json, the
|
||||
# plugin versions and the editor screenshots. Dockerfile.dockerignore keeps the context small.
|
||||
|
||||
FROM node:24-alpine AS build
|
||||
WORKDIR /repo/website
|
||||
COPY website/package.json website/package-lock.json ./
|
||||
RUN npm ci --no-audit --no-fund
|
||||
COPY . /repo
|
||||
ARG SITE_URL=https://fl.narl.io
|
||||
ENV SITE_URL=$SITE_URL
|
||||
RUN npm run build
|
||||
|
||||
FROM nginxinc/nginx-unprivileged:stable-alpine
|
||||
COPY website/docker/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY website/docker/security-headers.conf /etc/nginx/security-headers.conf
|
||||
COPY website/docker/40-legal-notice.sh /docker-entrypoint.d/40-legal-notice.sh
|
||||
COPY --from=build /repo/website/dist /usr/share/nginx/html
|
||||
COPY --from=build /repo/website/dist/impressum/index.html /opt/legal-templates/impressum/index.html
|
||||
COPY --from=build /repo/website/dist/datenschutz/index.html /opt/legal-templates/datenschutz/index.html
|
||||
|
||||
# Only the two legal pages are rewritten at start-up; everything else stays read-only.
|
||||
USER root
|
||||
RUN rm -f /etc/nginx/conf.d/default.conf \
|
||||
&& chown 101:101 /usr/share/nginx/html/impressum/index.html /usr/share/nginx/html/datenschutz/index.html
|
||||
USER 101
|
||||
|
||||
EXPOSE 8080
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
|
||||
CMD wget -q -O /dev/null http://127.0.0.1:8080/healthz || exit 1
|
||||
@@ -0,0 +1,10 @@
|
||||
# Allowlist for the repository-root build context: only what the site reads.
|
||||
*
|
||||
!website/
|
||||
!plugins/catalog.json
|
||||
!plugins/*/plugin/Cargo.toml
|
||||
!docs/*/assets/editor.png
|
||||
website/node_modules
|
||||
website/dist
|
||||
website/.astro
|
||||
website/.env
|
||||
@@ -0,0 +1,98 @@
|
||||
# Bass Foundry website
|
||||
|
||||
The site at [fl.narl.io](https://fl.narl.io): one static [Astro](https://astro.build) site
|
||||
with a home page, one page per plugin and the legal pages. An unprivileged nginx container
|
||||
serves it behind Traefik.
|
||||
|
||||
The site reads its plugin data from the repository. When a plugin is added or released, the
|
||||
next build picks it up:
|
||||
|
||||
- names, descriptions and CLAP IDs come from `plugins/catalog.json`;
|
||||
- versions come from `plugins/<plugin>/plugin/Cargo.toml`, and the download links point to
|
||||
`<plugin>-v<version>` on Gitea;
|
||||
- editor previews come from `docs/<plugin>/assets/editor.png`.
|
||||
|
||||
The site's own copy for each plugin (role, problem, highlights, where to load it, latency) is in
|
||||
`src/data/plugins.ts`. The build fails if a catalog entry has no copy there. The "On the bench"
|
||||
ideas are copied from `docs/ideas.md`, so update them by hand when that list changes.
|
||||
|
||||
Download links go to the release tag of the version in `Cargo.toml`. Deploy the site after the
|
||||
release is published, not after the version bump, or the link for that plugin is dead until
|
||||
the release exists.
|
||||
|
||||
The site sets no cookies, stores nothing in the browser and loads nothing from third parties.
|
||||
Fonts, images and styles all come from the same server, there is no JavaScript, and nginx
|
||||
writes no access logs. The privacy policy (`src/pages/datenschutz.astro`) says exactly this,
|
||||
so keep the two in step.
|
||||
|
||||
## Development
|
||||
|
||||
```sh
|
||||
cd website
|
||||
npm ci
|
||||
npm run dev # http://localhost:4321/
|
||||
npm run build # astro check, then a static build into dist/
|
||||
npm run preview
|
||||
npm run og # re-renders public/og.png
|
||||
```
|
||||
|
||||
`npm run og` sets its text in Roboto Condensed from `plugins/common/ui/assets`, so no font
|
||||
needs to be installed. The logo is `public/favicon.svg` (copied to `src/assets/logo.svg`): a
|
||||
riveted steel plate with a knurled saw wave.
|
||||
|
||||
The Impressum and the privacy policy contain `%%IMPRESSUM_…%%` placeholders. The container
|
||||
fills them in when it starts, so the operator's name and address never end up in git.
|
||||
|
||||
## Running with Docker
|
||||
|
||||
The build context is the repository root, so the image can read the catalog, the versions and
|
||||
the screenshots. `Dockerfile.dockerignore` keeps the context to those files and `website/`.
|
||||
|
||||
```sh
|
||||
cd website
|
||||
cp .env.example .env # fill in the Impressum details
|
||||
docker compose up -d --build
|
||||
curl -I http://127.0.0.1:8080/
|
||||
```
|
||||
|
||||
| Variable | Required | Used for |
|
||||
| ------------------- | -------- | -------------------------------------------------------------- |
|
||||
| `SITE_URL` | build | Canonical links, sitemap, robots.txt, `og:image`. Rebuild to change. |
|
||||
| `IMPRESSUM_NAME` | yes | Impressum and controller in the privacy policy |
|
||||
| `IMPRESSUM_STREET` | yes | Street and number (no P.O. box) |
|
||||
| `IMPRESSUM_CITY` | yes | Postcode and city |
|
||||
| `IMPRESSUM_EMAIL` | yes | Contact address, written out as HTML entities |
|
||||
| `IMPRESSUM_COUNTRY` | no | Defaults to `Deutschland` |
|
||||
| `IMPRESSUM_PHONE` | no | Hides the phone line when empty |
|
||||
| `HOSTER` | no | Names the hosting provider (Art. 28 GDPR); hidden when empty |
|
||||
|
||||
The container refuses to start while a required variable is missing, so the site cannot go
|
||||
live without an Impressum. The values are escaped and applied to fresh copies of the two pages
|
||||
on every start, so after changing `.env`, recreating the container (`docker compose up -d`) is
|
||||
enough. The container listens on 8080 as user 101 with all capabilities dropped. `/healthz`
|
||||
answers `ok` for health checks.
|
||||
|
||||
### Behind Traefik
|
||||
|
||||
`compose.override.yaml` puts the site on `fl.narl.io`: router labels, the external `narl`
|
||||
network, the `websecure` entry point with the `https_resolver` certificate resolver, and
|
||||
`ports: !reset []` so nothing is published on the host. Docker Compose reads the override file
|
||||
automatically. TLS ends at Traefik. Redirects are relative, so they keep the proxy's scheme and
|
||||
host.
|
||||
|
||||
**If Traefik writes access logs for this router, the privacy policy is wrong.** It promises no
|
||||
access logs. Either turn them off for the router, or edit section 3 of `datenschutz.astro` (both
|
||||
languages): what is logged, why, and for how long.
|
||||
|
||||
## Before going live
|
||||
|
||||
- **Legal texts.** The Impressum and the privacy policy are written for a private,
|
||||
non-commercial project hosted in Germany. They are no substitute for legal advice. Update the
|
||||
"Stand" date when you change them.
|
||||
- **git.narl.io.** The privacy policy says that the downloads and the source code are on
|
||||
git.narl.io and that its own privacy information applies there. If you run that Gitea
|
||||
instance yourself, make sure it has that information.
|
||||
|
||||
## License
|
||||
|
||||
ISC, like the plugins. Roboto Condensed and Inter are under the SIL Open Font License.
|
||||
@@ -0,0 +1,23 @@
|
||||
// @ts-check
|
||||
import { defineConfig } from 'astro/config';
|
||||
import sitemap from '@astrojs/sitemap';
|
||||
|
||||
export default defineConfig({
|
||||
site: process.env.SITE_URL || 'https://fl.narl.io',
|
||||
trailingSlash: 'always',
|
||||
build: {
|
||||
// Everything goes into files under /_astro/ so the CSP can stay at 'self'
|
||||
// without hashes or 'unsafe-inline'.
|
||||
inlineStylesheets: 'never',
|
||||
},
|
||||
vite: {
|
||||
build: { assetsInlineLimit: 0 },
|
||||
// The plugin data and editor screenshots live in the repository above this folder.
|
||||
server: { fs: { allow: ['..'] } },
|
||||
},
|
||||
integrations: [
|
||||
sitemap({
|
||||
filter: (page) => !/\/(impressum|datenschutz)\/$/.test(page),
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
# fl.narl.io behind Traefik on the external "narl" network. TLS ends at Traefik;
|
||||
# the container itself is not published on any host port.
|
||||
services:
|
||||
web:
|
||||
build:
|
||||
args:
|
||||
SITE_URL: https://fl.narl.io
|
||||
ports: !reset []
|
||||
networks:
|
||||
- narl
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=narl"
|
||||
- "traefik.http.routers.bass-foundry-website.rule=Host(`fl.narl.io`)"
|
||||
- "traefik.http.routers.bass-foundry-website.entrypoints=websecure"
|
||||
- "traefik.http.routers.bass-foundry-website.tls.certresolver=https_resolver"
|
||||
- "traefik.http.services.bass-foundry-website.loadbalancer.server.port=8080"
|
||||
|
||||
networks:
|
||||
narl:
|
||||
external: true
|
||||
@@ -0,0 +1,27 @@
|
||||
# Generic setup: the site on 127.0.0.1:8080. Put the reverse proxy (Traefik labels, networks)
|
||||
# in a compose.override.yaml and drop the port mapping there if the proxy reaches the
|
||||
# container over a Docker network. The build context is the repository root, because the
|
||||
# site reads the plugin catalog, versions and screenshots from it.
|
||||
services:
|
||||
web:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: website/Dockerfile
|
||||
args:
|
||||
SITE_URL: ${SITE_URL:-https://fl.narl.io}
|
||||
image: bass-foundry-website:latest
|
||||
env_file:
|
||||
- path: .env
|
||||
required: false
|
||||
ports:
|
||||
- "127.0.0.1:8080:8080"
|
||||
restart: unless-stopped
|
||||
cap_drop:
|
||||
- ALL
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "1m"
|
||||
max-file: "3"
|
||||
Executable
+74
@@ -0,0 +1,74 @@
|
||||
#!/bin/sh
|
||||
# Fills the Impressum and the privacy policy with the operator's details from the environment.
|
||||
# Runs from the nginx image's /docker-entrypoint.d/ hook on every start. It works from pristine
|
||||
# copies, so changing a variable and restarting the container is enough.
|
||||
set -eu
|
||||
|
||||
HTML=/usr/share/nginx/html
|
||||
TEMPLATES=/opt/legal-templates
|
||||
PAGES="impressum/index.html datenschutz/index.html"
|
||||
|
||||
missing=""
|
||||
for var in IMPRESSUM_NAME IMPRESSUM_STREET IMPRESSUM_CITY IMPRESSUM_EMAIL; do
|
||||
eval "value=\${$var:-}"
|
||||
[ -n "$value" ] || missing="$missing $var"
|
||||
done
|
||||
if [ -n "$missing" ]; then
|
||||
echo "40-legal-notice: missing required variables:$missing" >&2
|
||||
echo "40-legal-notice: refusing to serve the site without an Impressum (see .env.example)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "$IMPRESSUM_EMAIL" in
|
||||
*@*.*) ;;
|
||||
*)
|
||||
echo "40-legal-notice: IMPRESSUM_EMAIL does not look like an email address" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# One line, HTML-escaped.
|
||||
html() {
|
||||
printf '%s' "$1" | tr '\r\n' ' ' |
|
||||
sed -e 's/&/\&/g' -e 's/</\</g' -e 's/>/\>/g' -e 's/"/\"/g' -e "s/'/\'/g"
|
||||
}
|
||||
|
||||
# Every character as a numeric entity, which keeps the address away from naive scrapers.
|
||||
entities() {
|
||||
printf '%s' "$1" | od -An -tu1 | tr -s ' \n' '\n\n' | sed -e '/^$/d' -e 's/.*/\&#&;/' | tr -d '\n'
|
||||
}
|
||||
|
||||
# Escapes a replacement for sed's s|…|…| (backslash, the | delimiter and &).
|
||||
repl() {
|
||||
printf '%s' "$1" | sed -e 's/[\\|&]/\\&/g'
|
||||
}
|
||||
|
||||
script=$(mktemp)
|
||||
trap 'rm -f "$script"' EXIT
|
||||
{
|
||||
echo "s|%%IMPRESSUM_NAME%%|$(repl "$(html "$IMPRESSUM_NAME")")|g"
|
||||
echo "s|%%IMPRESSUM_STREET%%|$(repl "$(html "$IMPRESSUM_STREET")")|g"
|
||||
echo "s|%%IMPRESSUM_CITY%%|$(repl "$(html "$IMPRESSUM_CITY")")|g"
|
||||
echo "s|%%IMPRESSUM_COUNTRY%%|$(repl "$(html "${IMPRESSUM_COUNTRY:-Deutschland}")")|g"
|
||||
echo "s|%%IMPRESSUM_EMAIL%%|$(repl "$(entities "$IMPRESSUM_EMAIL")")|g"
|
||||
for pair in phone:IMPRESSUM_PHONE hoster:HOSTER; do
|
||||
key=${pair%%:*}
|
||||
var=${pair#*:}
|
||||
eval "value=\${$var:-}"
|
||||
if [ -n "$value" ]; then
|
||||
echo "s|%%$var%%|$(repl "$(html "$value")")|g"
|
||||
else
|
||||
echo "s|<p data-optional=\"$key\"[^>]*>[^<]*</p>||g"
|
||||
fi
|
||||
done
|
||||
} >"$script"
|
||||
|
||||
for page in $PAGES; do
|
||||
sed -f "$script" "$TEMPLATES/$page" >"$HTML/$page"
|
||||
if grep -q '%%[A-Z_]*%%' "$HTML/$page"; then
|
||||
echo "40-legal-notice: unfilled placeholder left in $page" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "40-legal-notice: filled in the Impressum and the privacy policy"
|
||||
@@ -0,0 +1,63 @@
|
||||
# The privacy policy (src/pages/datenschutz.astro) promises that no access logs are written.
|
||||
# Keep access_log off and the error log at "crit" unless that text changes with it.
|
||||
worker_processes auto;
|
||||
error_log /dev/stderr crit;
|
||||
pid /tmp/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
client_body_temp_path /tmp/client_temp;
|
||||
proxy_temp_path /tmp/proxy_temp;
|
||||
fastcgi_temp_path /tmp/fastcgi_temp;
|
||||
uwsgi_temp_path /tmp/uwsgi_temp;
|
||||
scgi_temp_path /tmp/scgi_temp;
|
||||
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
server_tokens off;
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
keepalive_timeout 65;
|
||||
charset utf-8;
|
||||
charset_types text/css application/javascript image/svg+xml application/xml text/plain;
|
||||
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/css application/javascript image/svg+xml application/xml text/plain application/json;
|
||||
|
||||
server {
|
||||
listen 8080;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
# Relative Location headers, so redirects keep the scheme and host of the reverse proxy.
|
||||
absolute_redirect off;
|
||||
|
||||
error_page 404 /404.html;
|
||||
|
||||
include /etc/nginx/security-headers.conf;
|
||||
add_header Cache-Control "no-cache" always;
|
||||
|
||||
location /_astro/ {
|
||||
include /etc/nginx/security-headers.conf;
|
||||
add_header Cache-Control "public, max-age=31536000, immutable" always;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location = /healthz {
|
||||
default_type text/plain;
|
||||
return 200 "ok\n";
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
# Included in every location that sets its own headers, because add_header is not inherited
|
||||
# into a location that has any add_header of its own.
|
||||
add_header Content-Security-Policy "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; connect-src 'self'; manifest-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()" always;
|
||||
add_header Cross-Origin-Opener-Policy "same-origin" always;
|
||||
Generated
+5243
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "bass-foundry-website",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "1.0.0",
|
||||
"description": "Website for the Bass Foundry plugin suite",
|
||||
"license": "ISC",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro check && astro build",
|
||||
"preview": "astro preview",
|
||||
"check": "astro check",
|
||||
"og": "node scripts/og-image.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/sitemap": "^3.7.4",
|
||||
"@fontsource-variable/inter": "^5.3.0",
|
||||
"@fontsource-variable/roboto-condensed": "^5.3.0",
|
||||
"astro": "^7.3.5",
|
||||
"sharp": "^0.35.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/check": "^0.9.10",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<defs>
|
||||
<linearGradient id="p" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#22252a"/>
|
||||
<stop offset="1" stop-color="#121417"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="2" y="2" width="60" height="60" rx="14" fill="url(#p)" stroke="#3a3e45" stroke-width="2"/>
|
||||
<g fill="#5a6069">
|
||||
<circle cx="12" cy="12" r="2.6"/>
|
||||
<circle cx="52" cy="12" r="2.6"/>
|
||||
<circle cx="12" cy="52" r="2.6"/>
|
||||
<circle cx="52" cy="52" r="2.6"/>
|
||||
</g>
|
||||
<path d="M11 42 L24 20 L24 42 L36 29 L36 42 L50 20 L50 42" fill="none" stroke="#ff0160" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 695 B |
Binary file not shown.
|
After Width: | Height: | Size: 235 KiB |
@@ -0,0 +1,82 @@
|
||||
// Renders public/og.png (1200×630), the preview image for link shares: the name, the
|
||||
// waveform from the hero and three editor previews. Run with `npm run og` after the look
|
||||
// changes. The text is set in Roboto Condensed, loaded from the plugins' own font folder.
|
||||
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const root = new URL('../', import.meta.url);
|
||||
const repo = new URL('../', root);
|
||||
const W = 1200;
|
||||
const H = 630;
|
||||
|
||||
// librsvg finds fonts through fontconfig; point it at the embedded editor fonts.
|
||||
const fonts = await mkdtemp(join(tmpdir(), 'og-fonts-'));
|
||||
await writeFile(
|
||||
join(fonts, 'fonts.conf'),
|
||||
`<?xml version="1.0"?><!DOCTYPE fontconfig SYSTEM "fonts.dtd"><fontconfig>
|
||||
<dir>${new URL('plugins/common/ui/assets/', repo).pathname}</dir><cachedir>${fonts}</cachedir>
|
||||
</fontconfig>`,
|
||||
);
|
||||
process.env.FONTCONFIG_FILE = join(fonts, 'fonts.conf');
|
||||
const { default: sharp } = await import('sharp');
|
||||
|
||||
// Knurl's Fry pattern on a tanh saw, as in src/components/Hero.astro.
|
||||
const pattern = [0, -14, -5, -14].map((db) => 10 ** (db / 20));
|
||||
const points = [];
|
||||
for (let i = 0; i <= 700; i++) {
|
||||
const phase = (i / 700) * 6;
|
||||
const cycle = Math.floor(phase);
|
||||
const frac = phase - cycle;
|
||||
let g = pattern[cycle % 4];
|
||||
if (frac > 0.85) {
|
||||
const t = (frac - 0.85) / 0.15;
|
||||
g += (pattern[(cycle + 1) % 4] - g) * (0.5 - 0.5 * Math.cos(Math.PI * t));
|
||||
}
|
||||
const y = Math.tanh(2.2 * (2 * frac - 1)) / Math.tanh(2.2);
|
||||
points.push(`${(70 + (i / 700) * 1060).toFixed(1)},${(492 - y * g * 68).toFixed(1)}`);
|
||||
}
|
||||
|
||||
const background = Buffer.from(`<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}">
|
||||
<defs>
|
||||
<radialGradient id="glow" cx="0.5" cy="1" r="0.75">
|
||||
<stop offset="0" stop-color="#ff0160" stop-opacity="0.32"/>
|
||||
<stop offset="1" stop-color="#ff0160" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="title" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0" stop-color="#ff0160"/>
|
||||
<stop offset="0.6" stop-color="#ff5c95"/>
|
||||
<stop offset="1" stop-color="#b8c0cc"/>
|
||||
</linearGradient>
|
||||
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
|
||||
<path d="M40 0H0V40" fill="none" stroke="#b8c0cc" stroke-opacity="0.06"/>
|
||||
</pattern>
|
||||
<filter id="blur"><feGaussianBlur stdDeviation="7"/></filter>
|
||||
</defs>
|
||||
<rect width="${W}" height="${H}" fill="#0b0c0e"/>
|
||||
<rect width="${W}" height="${H}" fill="url(#grid)"/>
|
||||
<rect width="${W}" height="${H}" fill="url(#glow)"/>
|
||||
<text x="70" y="118" font-family="Roboto Condensed" font-weight="500" font-size="26" letter-spacing="6" fill="#ff0160">CLAP PLUGINS FOR FL STUDIO</text>
|
||||
<text x="66" y="236" font-family="Roboto Condensed" font-weight="500" font-size="128" fill="#e8e9ec">BASS</text>
|
||||
<text x="66" y="348" font-family="Roboto Condensed" font-weight="500" font-size="128" fill="url(#title)">FOUNDRY</text>
|
||||
<path d="M${points.join('L')}" fill="none" stroke="#ff0160" stroke-width="9" opacity="0.4" filter="url(#blur)"/>
|
||||
<path d="M${points.join('L')}" fill="none" stroke="#ff0160" stroke-width="3" stroke-linejoin="round"/>
|
||||
<text x="70" y="596" font-family="Roboto Condensed" font-size="24" letter-spacing="3" fill="#8c929c">RIDDIM · DUBSTEP · BASS MUSIC — FL.NARL.IO</text>
|
||||
</svg>`);
|
||||
|
||||
const shot = async (name, width) => {
|
||||
const img = sharp(await readFile(new URL(`docs/${name}/assets/editor.png`, repo))).resize(width);
|
||||
const { height } = await img.clone().toBuffer({ resolveWithObject: true }).then((r) => r.info);
|
||||
const mask = Buffer.from(`<svg width="${width}" height="${height}"><rect width="${width}" height="${height}" rx="10"/></svg>`);
|
||||
return img.composite([{ input: mask, blend: 'dest-in' }]).png().toBuffer();
|
||||
};
|
||||
|
||||
await sharp(background)
|
||||
.composite([
|
||||
{ input: await shot('lathe', 430), left: 700, top: 36 },
|
||||
{ input: await shot('knurl', 430), left: 640, top: 116 },
|
||||
])
|
||||
.png({ compressionLevel: 9 })
|
||||
.toFile(new URL('public/og.png', root).pathname);
|
||||
await rm(fonts, { recursive: true });
|
||||
console.log('wrote public/og.png');
|
||||
@@ -0,0 +1,16 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<defs>
|
||||
<linearGradient id="p" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#22252a"/>
|
||||
<stop offset="1" stop-color="#121417"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="2" y="2" width="60" height="60" rx="14" fill="url(#p)" stroke="#3a3e45" stroke-width="2"/>
|
||||
<g fill="#5a6069">
|
||||
<circle cx="12" cy="12" r="2.6"/>
|
||||
<circle cx="52" cy="12" r="2.6"/>
|
||||
<circle cx="12" cy="52" r="2.6"/>
|
||||
<circle cx="52" cy="52" r="2.6"/>
|
||||
</g>
|
||||
<path d="M11 42 L24 20 L24 42 L36 29 L36 42 L50 20 L50 42" fill="none" stroke="#ff0160" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 695 B |
@@ -0,0 +1,80 @@
|
||||
---
|
||||
import { IDEAS, REPO } from '../data/plugins';
|
||||
---
|
||||
|
||||
<section id="bench" class="section">
|
||||
<div class="container">
|
||||
<div class="section-head">
|
||||
<p class="eyebrow">On the bench</p>
|
||||
<h2>What could come next</h2>
|
||||
<p>
|
||||
Ideas from the <a href={`${REPO}/src/branch/main/docs/ideas.md`}>ideas list</a>, each written against a problem
|
||||
seen in bass-music production. Most of the new ones are about texture. These are ideas, not promises.
|
||||
</p>
|
||||
</div>
|
||||
<ul class="grid" role="list">
|
||||
{
|
||||
IDEAS.map((idea) => (
|
||||
<li class={idea.texture ? 'texture' : undefined}>
|
||||
<div class="head">
|
||||
<h3>{idea.name}</h3>
|
||||
{idea.texture && <span class="badge badge-accent">Texture</span>}
|
||||
</div>
|
||||
<p class="label">{idea.role}</p>
|
||||
<p class="text">{idea.text}</p>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
li {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 0.45rem;
|
||||
padding: 1.4rem 1.5rem 1.5rem;
|
||||
border: 1px dashed var(--line-strong);
|
||||
border-radius: var(--radius);
|
||||
background: rgb(255 255 255 / 0.012);
|
||||
}
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.4rem;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 0.975rem;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,138 @@
|
||||
---
|
||||
import logo from '../assets/logo.svg';
|
||||
import { PLUGINS, REPO } from '../data/plugins';
|
||||
|
||||
const year = new Date().getFullYear();
|
||||
---
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="knurl" aria-hidden="true"></div>
|
||||
<div class="container grid">
|
||||
<div class="about">
|
||||
<a class="brand" href="/">
|
||||
<img src={logo.src} alt="" width="30" height="30" />
|
||||
<span>Bass <b>Foundry</b></span>
|
||||
</a>
|
||||
<p>Open-source CLAP plugins for writing, designing and mixing bass music in FL Studio.</p>
|
||||
</div>
|
||||
|
||||
<nav aria-label="Plugins">
|
||||
<p class="label">Plugins</p>
|
||||
<ul role="list">
|
||||
{
|
||||
PLUGINS.map((p) => (
|
||||
<li>
|
||||
<a href={`/plugins/${p.slug}/`}>{p.name}</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<nav aria-label="Project">
|
||||
<p class="label">Project</p>
|
||||
<ul role="list">
|
||||
<li><a href={REPO}>Source code</a></li>
|
||||
<li><a href={`${REPO}/releases`}>All releases</a></li>
|
||||
<li><a href={`${REPO}/src/branch/main/docs/ideas.md`}>Ideas</a></li>
|
||||
<li><a href={`${REPO}/src/branch/main/LICENSE`}>License (ISC)</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<nav aria-label="Legal">
|
||||
<p class="label">Legal</p>
|
||||
<ul role="list">
|
||||
<li><a href="/impressum/">Impressum</a></li>
|
||||
<li><a href="/datenschutz/">Datenschutz</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="container fine">
|
||||
<p>
|
||||
FL Studio is a trademark of Image-Line. Bass Foundry is an independent, non-commercial project and is not
|
||||
affiliated with Image-Line.
|
||||
</p>
|
||||
<p>© {year} Nils Pukropp · No cookies, no tracking.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
.site-footer {
|
||||
margin-top: 2rem;
|
||||
background: var(--bg-2);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr repeat(3, 1fr);
|
||||
gap: 2.5rem;
|
||||
padding-block: 3.5rem 2.5rem;
|
||||
}
|
||||
|
||||
.about {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 1rem;
|
||||
max-width: 340px;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
color: var(--text);
|
||||
font-family: var(--display);
|
||||
font-size: 1.1rem;
|
||||
letter-spacing: 0.14em;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.brand b {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
ul {
|
||||
display: grid;
|
||||
gap: 0.4rem;
|
||||
margin: 0.8rem 0 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
nav a {
|
||||
color: var(--muted);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
nav a:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.fine {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem 2rem;
|
||||
padding-block: 1.5rem 2.25rem;
|
||||
border-top: 1px solid var(--line);
|
||||
color: var(--faint);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.fine p:first-child {
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.about {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,91 @@
|
||||
---
|
||||
import { PLUGINS } from '../data/plugins';
|
||||
---
|
||||
|
||||
<section id="foundry" class="section foundry">
|
||||
<div class="container cols">
|
||||
<div class="section-head">
|
||||
<p class="eyebrow">The foundry</p>
|
||||
<h2>Named after metalwork</h2>
|
||||
<p>
|
||||
The suite is the foundry: the workshop where the bass gets cast, shaped and finished. Each plugin is named after
|
||||
the craft that does the same job to metal.
|
||||
</p>
|
||||
</div>
|
||||
<dl class="names">
|
||||
{
|
||||
PLUGINS.map((p) => (
|
||||
<div>
|
||||
<dt>
|
||||
<a href={`/plugins/${p.slug}/`}>{p.name}</a>
|
||||
</dt>
|
||||
<dd>{p.metal}</dd>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.foundry {
|
||||
border-block: 1px solid var(--line);
|
||||
background:
|
||||
radial-gradient(60% 80% at 100% 0%, rgb(255 1 96 / 0.07), transparent 60%),
|
||||
var(--bg-2);
|
||||
}
|
||||
|
||||
.cols {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
|
||||
gap: clamp(2rem, 5vw, 4rem);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.section-head {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.names {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.names div {
|
||||
padding: 1.1rem 1.25rem 1.1rem 0;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
dt a {
|
||||
color: var(--text);
|
||||
font-family: var(--display);
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
dt a:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0.2rem 0 0;
|
||||
font-size: 0.975rem;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.cols {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 540px) {
|
||||
.names {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,110 @@
|
||||
---
|
||||
import logo from '../assets/logo.svg';
|
||||
import { REPO } from '../data/plugins';
|
||||
import Icon from './Icon.astro';
|
||||
|
||||
interface Props {
|
||||
/** Without section links, for the legal pages and the 404 page. */
|
||||
minimal?: boolean;
|
||||
}
|
||||
|
||||
const { minimal = false } = Astro.props;
|
||||
---
|
||||
|
||||
<header class="site-header">
|
||||
<div class="container bar">
|
||||
<a class="brand" href="/" aria-label="Bass Foundry, home">
|
||||
<img src={logo.src} alt="" width="34" height="34" />
|
||||
<span>Bass <b>Foundry</b></span>
|
||||
</a>
|
||||
{
|
||||
!minimal && (
|
||||
<nav aria-label="Sections">
|
||||
<a href="/#plugins">Plugins</a>
|
||||
<a href="/#install">Install</a>
|
||||
<a href="/#principles">Principles</a>
|
||||
<a href="/#bench">On the bench</a>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
<a class="btn btn-ghost btn-sm" href={REPO}><Icon name="code" />Source</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
.site-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: rgb(11 12 14 / 0.78);
|
||||
backdrop-filter: blur(14px) saturate(1.4);
|
||||
}
|
||||
|
||||
.bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
min-height: 4.25rem;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.7rem;
|
||||
color: var(--text);
|
||||
font-family: var(--display);
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.14em;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.brand b {
|
||||
color: var(--accent);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.brand:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
gap: 1.75rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
nav a {
|
||||
color: var(--muted);
|
||||
font-family: var(--display);
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.12em;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
nav a:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
nav + .btn {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
nav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
nav + .btn {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,244 @@
|
||||
---
|
||||
import { PLUGINS, REPO } from '../data/plugins';
|
||||
import Icon from './Icon.astro';
|
||||
|
||||
// The scope shows what Knurl's "Fry" pattern does to a saw bass: every cycle gets its own
|
||||
// level (0 / −14 / −5 / −14 dB), with a raised-cosine crossfade over 15 % of a cycle.
|
||||
const W = 1200;
|
||||
const H = 300;
|
||||
const CYCLES = 8;
|
||||
const STEPS = 900;
|
||||
const pattern = [0, -14, -5, -14].map((db) => 10 ** (db / 20));
|
||||
const edge = 0.15;
|
||||
|
||||
function gainAt(phase: number): number {
|
||||
const cycle = Math.floor(phase);
|
||||
const frac = phase - cycle;
|
||||
const g = pattern[cycle % pattern.length];
|
||||
if (frac < 1 - edge) return g;
|
||||
const next = pattern[(cycle + 1) % pattern.length];
|
||||
const t = (frac - (1 - edge)) / edge;
|
||||
return g + (next - g) * (0.5 - 0.5 * Math.cos(Math.PI * t));
|
||||
}
|
||||
|
||||
function wave(gain: (phase: number) => number): string {
|
||||
const points: string[] = [];
|
||||
for (let i = 0; i <= STEPS; i++) {
|
||||
const phase = (i / STEPS) * CYCLES;
|
||||
// A tanh-shaped saw, as in the Knurl prototype.
|
||||
const saw = 2 * (phase - Math.floor(phase)) - 1;
|
||||
const y = Math.tanh(2.2 * saw) / Math.tanh(2.2);
|
||||
const x = (i / STEPS) * W;
|
||||
points.push(`${x.toFixed(1)},${(H / 2 - y * gain(phase) * (H * 0.4)).toFixed(1)}`);
|
||||
}
|
||||
return `M${points.join('L')}`;
|
||||
}
|
||||
|
||||
const dry = wave(() => 1);
|
||||
const knurled = wave(gainAt);
|
||||
const count = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten'][PLUGINS.length] ?? PLUGINS.length;
|
||||
---
|
||||
|
||||
<section class="hero">
|
||||
<div class="glow" aria-hidden="true"></div>
|
||||
<div class="container inner">
|
||||
<p class="eyebrow">CLAP plugins for FL Studio</p>
|
||||
<h1>Bass <span>Foundry</span></h1>
|
||||
<p class="lead">
|
||||
{count[0].toUpperCase() + count.slice(1)} open-source plugins for writing, designing and mixing riddim, dubstep and
|
||||
bass music. Most of them follow the played note, so a setting means the same thing on every note.
|
||||
</p>
|
||||
<div class="ctas">
|
||||
<a class="btn btn-primary" href="#plugins">Browse the plugins<Icon name="arrow" /></a>
|
||||
<a class="btn btn-ghost" href={`${REPO}/releases`}><Icon name="download" />All releases</a>
|
||||
</div>
|
||||
<ul class="facts" role="list">
|
||||
<li>Free & open source</li>
|
||||
<li>CLAP</li>
|
||||
<li>Windows x64</li>
|
||||
<li>FL Studio 2024.1+</li>
|
||||
<li>No accounts, no telemetry</li>
|
||||
</ul>
|
||||
|
||||
<figure class="scope plate">
|
||||
<figcaption>
|
||||
<span class="label"><b>Knurl</b> / cycle texture · Fry pattern</span>
|
||||
<span class="label readout">F1 · 43.7 Hz · 8 cycles</span>
|
||||
</figcaption>
|
||||
<svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label="A saw bass in grey, and the same bass in pink after Knurl gives each cycle its own level">
|
||||
<defs>
|
||||
<filter id="hero-glow" x="-5%" y="-20%" width="110%" height="140%">
|
||||
<feGaussianBlur stdDeviation="6" />
|
||||
</filter>
|
||||
</defs>
|
||||
<g class="grid">
|
||||
{Array.from({ length: CYCLES + 1 }, (_, i) => <line x1={(i * W) / CYCLES} x2={(i * W) / CYCLES} y1="0" y2={H} />)}
|
||||
<line class="axis" x1="0" x2={W} y1={H / 2} y2={H / 2} />
|
||||
</g>
|
||||
<path class="dry" d={dry} />
|
||||
<path class="wave halo" d={knurled} pathLength="1" filter="url(#hero-glow)" />
|
||||
<path class="wave" d={knurled} pathLength="1" />
|
||||
</svg>
|
||||
</figure>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.hero {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding-block: clamp(4rem, 9vw, 7rem) clamp(1rem, 3vw, 2rem);
|
||||
}
|
||||
|
||||
.hero::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
linear-gradient(rgb(184 192 204 / 0.045) 1px, transparent 1px) 0 0 / 48px 48px,
|
||||
linear-gradient(90deg, rgb(184 192 204 / 0.045) 1px, transparent 1px) 0 0 / 48px 48px;
|
||||
mask-image: linear-gradient(180deg, #000 40%, transparent);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.glow {
|
||||
position: absolute;
|
||||
inset: auto -20% -10% -20%;
|
||||
height: 70%;
|
||||
background: radial-gradient(50% 60% at 50% 70%, rgb(255 1 96 / 0.16), transparent 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.inner {
|
||||
position: relative;
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(3.6rem, 1.5rem + 9vw, 8.5rem);
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.01em;
|
||||
line-height: 0.92;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1 span {
|
||||
display: block;
|
||||
color: transparent;
|
||||
background: linear-gradient(90deg, var(--accent), #ff5c95 55%, var(--steel));
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.lead {
|
||||
max-width: 640px;
|
||||
color: #c3c7ce;
|
||||
font-size: clamp(1.1rem, 1rem + 0.45vw, 1.3rem);
|
||||
}
|
||||
|
||||
.ctas {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.8rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.facts {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem 1.4rem;
|
||||
margin: 0.25rem 0 0;
|
||||
padding: 0;
|
||||
color: var(--faint);
|
||||
font-family: var(--display);
|
||||
font-size: 0.9rem;
|
||||
letter-spacing: 0.1em;
|
||||
list-style: none;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.facts li {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.facts li::before {
|
||||
content: '';
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.scope {
|
||||
margin: clamp(2rem, 5vw, 3.5rem) 0 0;
|
||||
padding: 1rem 1.25rem 1.25rem;
|
||||
}
|
||||
|
||||
figcaption {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 0 0.6rem 0.75rem;
|
||||
}
|
||||
|
||||
figcaption b {
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.readout {
|
||||
color: var(--steel);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.grid line {
|
||||
stroke: rgb(184 192 204 / 0.08);
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.grid .axis {
|
||||
stroke: rgb(184 192 204 / 0.16);
|
||||
}
|
||||
|
||||
.dry {
|
||||
fill: none;
|
||||
stroke: var(--dim);
|
||||
stroke-width: 1.5;
|
||||
stroke-dasharray: 5 5;
|
||||
}
|
||||
|
||||
.wave {
|
||||
fill: none;
|
||||
stroke: var(--accent);
|
||||
stroke-width: 2.25;
|
||||
stroke-linejoin: round;
|
||||
stroke-dasharray: 1;
|
||||
stroke-dashoffset: 1;
|
||||
animation: draw 2.6s cubic-bezier(0.3, 0.6, 0.2, 1) 0.2s forwards;
|
||||
}
|
||||
|
||||
.halo {
|
||||
stroke-width: 8;
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
@keyframes draw {
|
||||
to {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.readout {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
// A small stroke icon set, 24 × 24, drawn in currentColor.
|
||||
const PATHS = {
|
||||
download: ['M12 4v11', 'M7 10l5 5 5-5', 'M5 20h14'],
|
||||
code: ['M8 7l-5 5 5 5', 'M16 7l5 5-5 5', 'M14 4l-4 16'],
|
||||
book: ['M4 5a2 2 0 0 1 2-2h13v16H6a2 2 0 0 0-2 2z', 'M4 21V5', 'M8 7h7'],
|
||||
arrow: ['M5 12h14', 'M13 6l6 6-6 6'],
|
||||
back: ['M19 12H5', 'M11 6l-6 6 6 6'],
|
||||
tag: ['M3 12V4a1 1 0 0 1 1-1h8l9 9-9 9z', 'M8 8h.01'],
|
||||
list: ['M8 6h12', 'M8 12h12', 'M8 18h12', 'M4 6h.01', 'M4 12h.01', 'M4 18h.01'],
|
||||
note: ['M9 18V5l11-2v13', 'M9 18a3 3 0 1 1-3-3 3 3 0 0 1 3 3z', 'M20 16a3 3 0 1 1-3-3 3 3 0 0 1 3 3z'],
|
||||
shield: ['M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6z', 'M9 12l2 2 4-4'],
|
||||
clock: ['M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18z', 'M12 7v5l3 2'],
|
||||
plug: ['M9 3v5', 'M15 3v5', 'M6 8h12v3a6 6 0 0 1-12 0z', 'M12 17v4'],
|
||||
open: ['M12 3l2.5 5.5L20 9l-4 4 1 6-5-3-5 3 1-6-4-4 5.5-.5z'],
|
||||
cpu: ['M7 7h10v10H7z', 'M10 3v4', 'M14 3v4', 'M10 17v4', 'M14 17v4', 'M3 10h4', 'M3 14h4', 'M17 10h4', 'M17 14h4'],
|
||||
eye: ['M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12z', 'M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z'],
|
||||
} as const;
|
||||
|
||||
interface Props {
|
||||
name: keyof typeof PATHS;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const { name, class: className } = Astro.props;
|
||||
---
|
||||
|
||||
<svg
|
||||
class={className}
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
{PATHS[name].map((d) => <path d={d} />)}
|
||||
</svg>
|
||||
@@ -0,0 +1,182 @@
|
||||
---
|
||||
// Two harmonic ladders: a fixed 130 Hz cut lands on a different harmonic of every note,
|
||||
// while Lathe's "harmonic 3" stays on harmonic 3.
|
||||
const NOTES = [
|
||||
{ name: 'F1', hz: 43.65 },
|
||||
{ name: 'C2', hz: 65.41 },
|
||||
{ name: 'C3', hz: 130.81 },
|
||||
];
|
||||
const LO = 30;
|
||||
const HI = 1000;
|
||||
const W = 560;
|
||||
const PAD = 54;
|
||||
const ROW = 46;
|
||||
const H = NOTES.length * ROW + 40;
|
||||
const x = (hz: number) => PAD + ((Math.log(hz) - Math.log(LO)) / (Math.log(HI) - Math.log(LO))) * (W - PAD - 12);
|
||||
const FIXED = 130;
|
||||
|
||||
function rows(pick: (f0: number) => number) {
|
||||
return NOTES.map((n, i) => {
|
||||
const target = pick(n.hz);
|
||||
const harmonics = [];
|
||||
for (let k = 1; k * n.hz <= HI; k++) harmonics.push({ k, x: x(k * n.hz), hit: k === target });
|
||||
return { ...n, y: 24 + i * ROW, harmonics, target };
|
||||
});
|
||||
}
|
||||
|
||||
const fixed = rows((f0) => Math.round(FIXED / f0));
|
||||
const tracked = rows(() => 3);
|
||||
const ticks = [50, 100, 200, 500, 1000];
|
||||
---
|
||||
|
||||
<section id="idea" class="section idea">
|
||||
<div class="container">
|
||||
<div class="section-head">
|
||||
<p class="eyebrow">The idea</p>
|
||||
<h2>One setting, every note</h2>
|
||||
<p>
|
||||
Riddim and dubstep basses change patch on every switch-up and move across an octave or two. Fixed-frequency tools
|
||||
then act differently on every note: the sub jumps, an EQ cut lands on another harmonic, the distortion fizzes on
|
||||
one note and goes dull on the next. Anvil, Lathe, Temper and Knurl track the pitch, from the audio or from MIDI,
|
||||
and put their work on the note itself.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="ladders">
|
||||
{
|
||||
[
|
||||
{ title: 'A normal EQ: −12 dB at 130 Hz', note: 'hits harmonic', data: fixed, cls: 'fixed' },
|
||||
{ title: 'Lathe: −12 dB on harmonic 3', note: 'hits harmonic', data: tracked, cls: 'tracked' },
|
||||
].map((panel) => (
|
||||
<figure class={`plate ladder ${panel.cls}`}>
|
||||
<figcaption class="label">{panel.title}</figcaption>
|
||||
<svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label={`${panel.title}: ${panel.data.map((r) => `${r.name} harmonic ${r.target}`).join(', ')}`}>
|
||||
{ticks.map((t) => (
|
||||
<g class="tick">
|
||||
<line x1={x(t)} x2={x(t)} y1="6" y2={H - 22} />
|
||||
<text x={x(t)} y={H - 6}>{t >= 1000 ? '1k' : t}</text>
|
||||
</g>
|
||||
))}
|
||||
{panel.cls === 'fixed' && <line class="cut" x1={x(FIXED)} x2={x(FIXED)} y1="4" y2={H - 22} />}
|
||||
{panel.data.map((r) => (
|
||||
<g>
|
||||
<text class="note" x="8" y={r.y + 5}>{r.name}</text>
|
||||
<line class="rail" x1={PAD} x2={W - 12} y1={r.y} y2={r.y} />
|
||||
{r.harmonics.map((h) => (
|
||||
<circle class={h.hit ? 'hit' : 'harm'} cx={h.x} cy={r.y} r={h.hit ? 7 : 3.2} />
|
||||
))}
|
||||
{r.harmonics
|
||||
.filter((h) => h.hit)
|
||||
.map((h) => (
|
||||
<text class="hit-label" x={h.x + 12} y={r.y - 8}>
|
||||
h{h.k}
|
||||
</text>
|
||||
))}
|
||||
</g>
|
||||
))}
|
||||
</svg>
|
||||
</figure>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<p class="caption">
|
||||
Dots are the harmonics of each note on a log frequency axis (Hz). The fixed cut takes the 3rd harmonic of F1, the
|
||||
2nd of C2 and the fundamental of C3. The tracked cut stays on the 3rd.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.idea {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.ladders {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.ladder {
|
||||
margin: 0;
|
||||
padding: 1.25rem 1.5rem 1rem;
|
||||
}
|
||||
|
||||
figcaption {
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.tick line {
|
||||
stroke: rgb(184 192 204 / 0.08);
|
||||
}
|
||||
|
||||
.tick text {
|
||||
fill: var(--faint);
|
||||
font-family: var(--display);
|
||||
font-size: 12px;
|
||||
text-anchor: middle;
|
||||
}
|
||||
|
||||
.note {
|
||||
fill: var(--steel);
|
||||
font-family: var(--display);
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.rail {
|
||||
stroke: rgb(184 192 204 / 0.16);
|
||||
}
|
||||
|
||||
.harm {
|
||||
fill: var(--dim);
|
||||
}
|
||||
|
||||
.cut {
|
||||
stroke: var(--accent);
|
||||
stroke-width: 2;
|
||||
stroke-dasharray: 5 4;
|
||||
}
|
||||
|
||||
.fixed .hit {
|
||||
fill: var(--accent);
|
||||
}
|
||||
|
||||
.tracked .hit {
|
||||
fill: var(--focus);
|
||||
}
|
||||
|
||||
.hit {
|
||||
stroke: var(--bg);
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.hit-label {
|
||||
font-family: var(--display);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.fixed .hit-label {
|
||||
fill: #ff6b9f;
|
||||
}
|
||||
|
||||
.tracked .hit-label {
|
||||
fill: var(--focus);
|
||||
}
|
||||
|
||||
.caption {
|
||||
max-width: 720px;
|
||||
margin-top: 1rem;
|
||||
color: var(--faint);
|
||||
font-size: 0.925rem;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.ladders {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,132 @@
|
||||
---
|
||||
import { REPO } from '../data/plugins';
|
||||
---
|
||||
|
||||
<section id="install" class="section install">
|
||||
<div class="container cols">
|
||||
<div class="section-head">
|
||||
<p class="eyebrow">Install</p>
|
||||
<h2>Into the rack in a minute</h2>
|
||||
<p>
|
||||
The plugins are built for FL Studio 2024.1 or newer on Windows x64. Nothing to register, no installer, no
|
||||
account.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ol class="steps" role="list">
|
||||
<li class="plate">
|
||||
<span class="n">01</span>
|
||||
<div>
|
||||
<h3>Download</h3>
|
||||
<p>
|
||||
Pick a plugin above, or open its release on <a href={`${REPO}/releases`}>Gitea</a>. Each ZIP has a
|
||||
<code>.sha256</code> file next to it to check the download.
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li class="plate">
|
||||
<span class="n">02</span>
|
||||
<div>
|
||||
<h3>Copy the .clap file</h3>
|
||||
<p>Extract the ZIP and copy the <code>.clap</code> file into</p>
|
||||
<pre><code>C:\Program Files\Common Files\CLAP\</code></pre>
|
||||
</div>
|
||||
</li>
|
||||
<li class="plate">
|
||||
<span class="n">03</span>
|
||||
<div>
|
||||
<h3>Rescan in FL Studio</h3>
|
||||
<p>
|
||||
Open <b>Options → Manage plugins</b>, enable <b>Verify plugins</b> and click <b>Find installed plugins</b>.
|
||||
The plugins appear under the vendor <b>Bass Foundry</b>.
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li class="plate">
|
||||
<span class="n">04</span>
|
||||
<div>
|
||||
<h3>Load it</h3>
|
||||
<p>
|
||||
Rivet goes in the Channel Rack. All the others go in mixer effect slots; each plugin’s page says where it
|
||||
sits best.
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.install {
|
||||
border-block: 1px solid var(--line);
|
||||
background: linear-gradient(180deg, var(--bg-2), var(--bg));
|
||||
}
|
||||
|
||||
.cols {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
|
||||
gap: clamp(2rem, 5vw, 4rem);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.section-head {
|
||||
position: sticky;
|
||||
top: 6.5rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.steps {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.steps li {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 1.25rem;
|
||||
padding: 1.5rem 1.75rem;
|
||||
}
|
||||
|
||||
.n {
|
||||
color: var(--accent);
|
||||
font-family: var(--display);
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 0.4rem;
|
||||
font-size: 1.35rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
b {
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0.7rem 0 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
pre code {
|
||||
display: block;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.cols {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.section-head {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,145 @@
|
||||
---
|
||||
import { Picture } from 'astro:assets';
|
||||
|
||||
import type { Plugin } from '../data/plugins';
|
||||
import Icon from './Icon.astro';
|
||||
|
||||
interface Props {
|
||||
plugin: Plugin;
|
||||
}
|
||||
|
||||
const { plugin: p } = Astro.props;
|
||||
---
|
||||
|
||||
<li class="card plate">
|
||||
<a class="shot" href={`/plugins/${p.slug}/`} tabindex="-1" aria-hidden="true">
|
||||
<Picture
|
||||
src={p.shot}
|
||||
alt=""
|
||||
formats={['avif', 'webp']}
|
||||
fallbackFormat="webp"
|
||||
widths={[480, 720, 1040]}
|
||||
sizes="(max-width: 860px) 100vw, 560px"
|
||||
loading="lazy"
|
||||
/>
|
||||
</a>
|
||||
<div class="body">
|
||||
<div class="title">
|
||||
<h3><a href={`/plugins/${p.slug}/`}>{p.name}</a></h3>
|
||||
<span class="label">{p.role}</span>
|
||||
<span class="badge">v{p.version}</span>
|
||||
</div>
|
||||
<p class="text">{p.description}.</p>
|
||||
<dl class="meta">
|
||||
<div><dt class="label">Load on</dt><dd>{p.loadOn}</dd></div>
|
||||
<div><dt class="label">Latency</dt><dd>{p.latency}</dd></div>
|
||||
</dl>
|
||||
<div class="links">
|
||||
<a class="btn btn-primary btn-sm" href={p.download}>
|
||||
<Icon name="download" />Download<span class="visually-hidden"> {p.name} {p.version} for Windows</span>
|
||||
</a>
|
||||
<a class="btn btn-ghost btn-sm" href={`/plugins/${p.slug}/`}>Details<span class="visually-hidden"> on {p.name}</span></a>
|
||||
{p.tracksNotes && <span class="badge badge-accent"><Icon name="note" />Follows the note</span>}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<style>
|
||||
.card {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
overflow: hidden;
|
||||
transition:
|
||||
border-color 0.2s,
|
||||
transform 0.2s;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: var(--line-strong);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.shot {
|
||||
display: block;
|
||||
margin: 0.85rem 0.85rem 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg);
|
||||
aspect-ratio: 16 / 10;
|
||||
}
|
||||
|
||||
.shot :global(img) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: top center;
|
||||
transition: transform 0.4s;
|
||||
}
|
||||
|
||||
.card:hover .shot :global(img) {
|
||||
transform: scale(1.025);
|
||||
}
|
||||
|
||||
.body {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 0.9rem;
|
||||
padding: 1.25rem 1.5rem 1.5rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 0.4rem 0.9rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.85rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h3 a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.title .badge {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.text {
|
||||
color: #c3c7ce;
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
margin: 0;
|
||||
padding-block: 0.75rem;
|
||||
border-block: 1px solid var(--line);
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0.15rem 0 0;
|
||||
color: var(--text);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.links .badge-accent {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.badge :global(svg) {
|
||||
width: 0.95em;
|
||||
height: 0.95em;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
import { PLUGINS } from '../data/plugins';
|
||||
import PluginCard from './PluginCard.astro';
|
||||
---
|
||||
|
||||
<section id="plugins" class="section">
|
||||
<div class="container">
|
||||
<div class="section-head">
|
||||
<p class="eyebrow">The plugins</p>
|
||||
<h2>Cast, shaped and finished</h2>
|
||||
<p>
|
||||
Each plugin is a separate product with its own version, guide and release. Every download is a ZIP with one
|
||||
<code>.clap</code> file and a checksum. The editor previews below are rendered from the plugins’ own code.
|
||||
</p>
|
||||
</div>
|
||||
<ul class="grid" role="list">
|
||||
{PLUGINS.map((plugin) => <PluginCard plugin={plugin} />)}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,116 @@
|
||||
---
|
||||
import Icon from './Icon.astro';
|
||||
|
||||
const ITEMS = [
|
||||
{
|
||||
icon: 'note',
|
||||
title: 'Follows the note',
|
||||
text: 'Pitch comes from the audio with a short lookahead, or from MIDI. Settings are counted in harmonics and cycles, not in Hz.',
|
||||
},
|
||||
{
|
||||
icon: 'cpu',
|
||||
title: 'Realtime-safe',
|
||||
text: 'Every audio effect has a test that fails if the audio thread allocates memory. The audio thread never waits on a lock.',
|
||||
},
|
||||
{
|
||||
icon: 'clock',
|
||||
title: 'Honest latency',
|
||||
text: 'Plugins that look ahead report it to the host, so FL Studio compensates the delay and the mix stays in time.',
|
||||
},
|
||||
{
|
||||
icon: 'shield',
|
||||
title: 'Tested without a DAW',
|
||||
text: 'CI loads each plugin through the CLAP ABI and checks ports, latency, state and audio at 44.1, 48 and 96 kHz.',
|
||||
},
|
||||
{
|
||||
icon: 'eye',
|
||||
title: 'Shows its work',
|
||||
text: 'The editors draw what the plugin hears and what it changes: cuts, harmonics, recovered level, cycle shapes.',
|
||||
},
|
||||
{
|
||||
icon: 'open',
|
||||
title: 'Open and private',
|
||||
text: 'ISC-licensed Rust. No accounts, no licence keys, no network access and no telemetry.',
|
||||
},
|
||||
] as const;
|
||||
---
|
||||
|
||||
<section id="principles" class="section">
|
||||
<div class="container">
|
||||
<div class="section-head">
|
||||
<p class="eyebrow">Principles</p>
|
||||
<h2>Built like tools</h2>
|
||||
<p>The same rules hold for every plugin in the suite. CI checks the testable ones on every push.</p>
|
||||
</div>
|
||||
<ul class="grid" role="list">
|
||||
{
|
||||
ITEMS.map((item) => (
|
||||
<li>
|
||||
<span class="icon">
|
||||
<Icon name={item.icon} />
|
||||
</span>
|
||||
<h3>{item.title}</h3>
|
||||
<p>{item.text}</p>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 1px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
background: var(--line);
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
li {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 0.6rem;
|
||||
padding: 1.75rem;
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 2.6rem;
|
||||
height: 2.6rem;
|
||||
margin-bottom: 0.4rem;
|
||||
border: 1px solid rgb(255 1 96 / 0.35);
|
||||
border-radius: 10px;
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.icon :global(svg) {
|
||||
width: 1.3rem;
|
||||
height: 1.3rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.3rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,267 @@
|
||||
// Plugin data for the site. Names, descriptions and CLAP IDs come from plugins/catalog.json,
|
||||
// versions from each wrapper's Cargo.toml and screenshots from docs/<plugin>/assets, so the
|
||||
// site follows the repository. The texts below are the site's own copy.
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
import type { ImageMetadata } from 'astro';
|
||||
|
||||
export const REPO = 'https://git.narl.io/nvrl/bass-foundry';
|
||||
|
||||
// Astro runs from website/, both locally and in the Docker build.
|
||||
const ROOT = resolve(process.cwd(), '..');
|
||||
|
||||
interface CatalogEntry {
|
||||
name: string;
|
||||
description: string;
|
||||
manifest: string;
|
||||
documentation: string;
|
||||
clap_id: string;
|
||||
}
|
||||
|
||||
const catalog: Record<string, CatalogEntry> = JSON.parse(readFileSync(resolve(ROOT, 'plugins/catalog.json'), 'utf8'));
|
||||
|
||||
const shots = import.meta.glob<ImageMetadata>('../../../docs/*/assets/editor.png', {
|
||||
eager: true,
|
||||
import: 'default',
|
||||
});
|
||||
|
||||
function version(manifest: string): string {
|
||||
const text = readFileSync(resolve(ROOT, manifest), 'utf8');
|
||||
const package_ = text.split(/^\[/m).find((s) => s.startsWith('package]'));
|
||||
const match = package_?.match(/^version\s*=\s*"([^"]+)"/m);
|
||||
if (!match) throw new Error(`no version in ${manifest}`);
|
||||
return match[1];
|
||||
}
|
||||
|
||||
interface Copy {
|
||||
/** Short role, shown like the editor header ("LATHE / HARMONIC EQ"). */
|
||||
role: string;
|
||||
/** What the name means in metalwork. */
|
||||
metal: string;
|
||||
/** The problem in a bass-music session. */
|
||||
problem: string;
|
||||
points: string[];
|
||||
loadOn: string;
|
||||
latency: string;
|
||||
/** Takes its pitch from the audio or from MIDI. */
|
||||
tracksNotes: boolean;
|
||||
}
|
||||
|
||||
const COPY: Record<string, Copy> = {
|
||||
rivet: {
|
||||
role: 'Storyboard composition',
|
||||
metal: 'A rivet joins separate pieces into one.',
|
||||
problem:
|
||||
'A good four-bar motif rarely becomes a phrase on its own. Copying and varying it by hand gives loops, not a line that goes somewhere.',
|
||||
points: [
|
||||
'Lay out a storyboard of roles: statement, answer, departure, return, rest.',
|
||||
'Lock the notes, pitches or rhythms that define the motif; they survive into every block.',
|
||||
'Join seams with a cut, a breath, a pickup or an approach note.',
|
||||
'Audition with the internal voice or send MIDI to your synths, then export the phrase as MIDI.',
|
||||
],
|
||||
loadOn: 'Channel Rack',
|
||||
latency: 'none',
|
||||
tracksNotes: false,
|
||||
},
|
||||
patina: {
|
||||
role: 'Resonance smoothing',
|
||||
metal: 'A patina changes the surface of a metal and keeps its character.',
|
||||
problem:
|
||||
'Distortion chains build up harsh, ringing resonances. A static EQ cut takes out the tone along with the ring.',
|
||||
points: [
|
||||
'Finds ringing regions as they happen and pulls only those down.',
|
||||
'Plain harmonic tone and transients pass; Selectivity sets what counts as harsh.',
|
||||
'Four focus points on the plot to cut more or protect a region.',
|
||||
'Delta monitoring to hear exactly what is removed.',
|
||||
],
|
||||
loadOn: 'Any mixer track',
|
||||
latency: 'one analysis frame',
|
||||
tracksNotes: false,
|
||||
},
|
||||
anvil: {
|
||||
role: 'Sub anchor',
|
||||
metal: 'An anvil is the solid base that everything is struck on.',
|
||||
problem:
|
||||
'Every switch-up patch brings its own sub: a different level, a different phase. The weight of the drop jumps from note to note.',
|
||||
points: [
|
||||
'Replaces the band below Split with a clean mono sine that follows the bass pitch.',
|
||||
'The same peak level and the same start phase on every note, so the kick lands the same.',
|
||||
'Pitch from detection with 80 ms lookahead, or from MIDI.',
|
||||
'Linkwitz–Riley crossover; the stereo above the split is kept.',
|
||||
],
|
||||
loadOn: 'Bass bus',
|
||||
latency: '80 ms',
|
||||
tracksNotes: true,
|
||||
},
|
||||
crucible: {
|
||||
role: 'Resampler',
|
||||
metal: 'A crucible melts material down so it can be cast again.',
|
||||
problem:
|
||||
'Resampling is the core loop of riddim sound design: bounce, chop, trim, declick, normalise, find the key, name, save. Every time.',
|
||||
points: [
|
||||
'Always records, so the take you liked is already there when you press Capture.',
|
||||
'Captures the last 1–16 bars on the FL bar grid.',
|
||||
'Slices on a grid or on transients, then trims, declicks and normalises.',
|
||||
'Exports key-labelled WAVs, the full loop and an optional Serum/Vital wavetable.',
|
||||
],
|
||||
loadOn: 'End of the chain to resample',
|
||||
latency: 'none',
|
||||
tracksNotes: false,
|
||||
},
|
||||
lathe: {
|
||||
role: 'Harmonic EQ',
|
||||
metal: 'A lathe turns a piece so every cut lands the same all the way round.',
|
||||
problem:
|
||||
'A cut at 130 Hz takes out the 3rd harmonic of F1, the 2nd of C2 and the fundamental of C3. The same bass sounds different on every note.',
|
||||
points: [
|
||||
'One band on each of harmonics 1–32 of the played note.',
|
||||
'"−12 dB on harmonic 3" stays on harmonic 3, whatever note plays.',
|
||||
'Odd/even balance, tilt over harmonic number, and Focus between the harmonics.',
|
||||
'Pitch from the audio, or from MIDI.',
|
||||
],
|
||||
loadOn: 'Bass, after the synth',
|
||||
latency: '≈ 67 ms',
|
||||
tracksNotes: true,
|
||||
},
|
||||
temper: {
|
||||
role: 'Tracked distortion',
|
||||
metal: 'Tempering sets how hard and how tough a metal ends up.',
|
||||
problem:
|
||||
'A 2 kHz lowpass after a distortion lets 45 harmonics of F1 through but only 22 of F2, and a quieter patch is driven less. Some notes fizz, others go dull.',
|
||||
points: [
|
||||
'Evens out each note’s level before the shaper, so drive means the same on every patch.',
|
||||
'Tone filters set in multiples of the note: Floor, Push and Bright follow the pitch.',
|
||||
'Soft, Hard, Fold and Tube curves with bias for even harmonics.',
|
||||
'4× oversampled, with auto gain for level-matched comparison.',
|
||||
],
|
||||
loadOn: 'Bass',
|
||||
latency: '≈ 67 ms + oversampling',
|
||||
tracksNotes: true,
|
||||
},
|
||||
weld: {
|
||||
role: 'Layer phase alignment',
|
||||
metal: 'A weld fuses two pieces into one.',
|
||||
problem:
|
||||
'Layered bass patches partly cancel where the layers overlap in the mids. The sum gets thin and hollow, and it changes with every switch-up.',
|
||||
points: [
|
||||
'Hears the other layer through the sidechain input.',
|
||||
'Fits polarity and up to four allpass sections: phase only, the layer sounds the same on its own.',
|
||||
'Shows what the cancellation cost and how much Weld recovered.',
|
||||
'No latency. Learn on a drop, then hold the fit.',
|
||||
],
|
||||
loadOn: 'One layer’s track, other layer as sidechain',
|
||||
latency: 'none',
|
||||
tracksNotes: false,
|
||||
},
|
||||
knurl: {
|
||||
role: 'Cycle texture',
|
||||
metal: 'Knurling presses a ridged grip pattern into a surface.',
|
||||
problem:
|
||||
'Texture makes a riddim patch, but it usually comes from long chains of distortion, comb, formant and resampling, and it changes from note to note.',
|
||||
points: [
|
||||
'A level pattern over 1–16 cycles: in-tune subharmonic growl, rattle and fry.',
|
||||
'Random level and timing per cycle, the same on every note or free-running.',
|
||||
'A noise burst inside every cycle, tuned to the note.',
|
||||
'Counted in cycles, not Hz or tempo, so the grain is the same on F1 and on C2.',
|
||||
],
|
||||
loadOn: 'Bass',
|
||||
latency: '≈ 71 ms',
|
||||
tracksNotes: true,
|
||||
},
|
||||
};
|
||||
|
||||
export interface Plugin extends Copy {
|
||||
slug: string;
|
||||
name: string;
|
||||
description: string;
|
||||
clapId: string;
|
||||
version: string;
|
||||
tag: string;
|
||||
zip: string;
|
||||
download: string;
|
||||
release: string;
|
||||
guide: string;
|
||||
changelog: string;
|
||||
shot: ImageMetadata;
|
||||
}
|
||||
|
||||
export const PLUGINS: Plugin[] = Object.entries(catalog).map(([slug, entry]) => {
|
||||
const copy = COPY[slug];
|
||||
if (!copy) throw new Error(`no site copy for ${slug}; add it to src/data/plugins.ts`);
|
||||
const shot = shots[`../../../docs/${slug}/assets/editor.png`];
|
||||
if (!shot) throw new Error(`no screenshot for ${slug}`);
|
||||
const v = version(entry.manifest);
|
||||
const tag = `${slug}-v${v}`;
|
||||
const zip = `${slug}-windows-x86_64-${v}.zip`;
|
||||
return {
|
||||
...copy,
|
||||
slug,
|
||||
name: entry.name,
|
||||
description: entry.description,
|
||||
clapId: entry.clap_id,
|
||||
version: v,
|
||||
tag,
|
||||
zip,
|
||||
download: `${REPO}/releases/download/${tag}/${zip}`,
|
||||
release: `${REPO}/releases/tag/${tag}`,
|
||||
guide: `${REPO}/src/branch/main/${entry.documentation}/README.md`,
|
||||
changelog: `${REPO}/src/branch/main/${entry.documentation}/CHANGELOG.md`,
|
||||
shot,
|
||||
};
|
||||
});
|
||||
|
||||
/** Ideas from docs/ideas.md that are not built yet. */
|
||||
export const IDEAS: { name: string; role: string; text: string; texture?: boolean }[] = [
|
||||
{
|
||||
name: 'Ballast',
|
||||
role: 'Switch-up loudness leveller',
|
||||
text: 'Measures perceived loudness per switch-up and sets gain only, so every patch lands at the same loudness without compression.',
|
||||
},
|
||||
{
|
||||
name: 'Gauge',
|
||||
role: 'Low-end mono check',
|
||||
text: 'Correlation and mono-sum loss per band and per note in the bass range, with the notes where widening breaks the sum marked.',
|
||||
},
|
||||
{
|
||||
name: 'Bellows',
|
||||
role: 'Pitch-tracked resonator',
|
||||
text: 'A comb resonator tuned to the played note: noise, distortion or drums turn into metallic tone that is in key on every note.',
|
||||
},
|
||||
{
|
||||
name: 'Anneal',
|
||||
role: 'Tone and grit split',
|
||||
text: 'Splits a sound into harmonic tone, the grit between the harmonics, and transients. More grit, same body and sub.',
|
||||
texture: true,
|
||||
},
|
||||
{
|
||||
name: 'Swarf',
|
||||
role: 'In-key granular shavings',
|
||||
text: 'Sprays grains of the bass back as a layer, each a whole number of cycles and retuned to the current note.',
|
||||
texture: true,
|
||||
},
|
||||
{
|
||||
name: 'Etch',
|
||||
role: 'Texture imprint',
|
||||
text: 'Imprints the fine surface of a scrape, a vocal or a field recording on the bass, locked to its harmonics.',
|
||||
texture: true,
|
||||
},
|
||||
{
|
||||
name: 'Chase',
|
||||
role: 'Talking formants',
|
||||
text: 'A vowel pad with drawn paths. Snap moves each formant peak onto a harmonic, so no vowel vanishes between two harmonics.',
|
||||
texture: true,
|
||||
},
|
||||
{
|
||||
name: 'Damascus',
|
||||
role: 'Texture step sequencer',
|
||||
text: 'Four gain-matched texture slots and a step grid that switches between them: a switch-up inside one held note.',
|
||||
texture: true,
|
||||
},
|
||||
{
|
||||
name: 'Loupe',
|
||||
role: 'Texture meter',
|
||||
text: 'Roughness, grit, metal and brightness per note, with every switch-up of a drop placed on a texture map.',
|
||||
texture: true,
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
import '../styles/global.css';
|
||||
|
||||
import Footer from '../components/Footer.astro';
|
||||
import Header from '../components/Header.astro';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
noindex?: boolean;
|
||||
/** Header without section links, for pages outside the main flow (legal pages, 404). */
|
||||
minimal?: boolean;
|
||||
lang?: string;
|
||||
}
|
||||
|
||||
const { title, description, noindex = false, minimal = false, lang = 'en' } = Astro.props;
|
||||
const site = Astro.site!;
|
||||
const canonical = new URL(Astro.url.pathname, site);
|
||||
const ogImage = new URL('/og.png', site);
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang={lang} dir="ltr">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
<meta name="theme-color" content="#0b0c0e" />
|
||||
<meta name="color-scheme" content="dark" />
|
||||
{noindex && <meta name="robots" content="noindex" />}
|
||||
<link rel="canonical" href={canonical} />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="sitemap" href="/sitemap-index.xml" />
|
||||
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="Bass Foundry" />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:url" content={canonical} />
|
||||
<meta property="og:image" content={ogImage} />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:locale" content="en_US" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip-link" href="#main">Skip to content</a>
|
||||
<Header minimal={minimal} />
|
||||
<main id="main">
|
||||
<slot />
|
||||
</main>
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
import Base from './Base.astro';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const { title, description } = Astro.props;
|
||||
---
|
||||
|
||||
<Base title={title} description={description} noindex minimal>
|
||||
<article class="container legal">
|
||||
<nav class="langs" aria-label="Sprache / Language">
|
||||
<a href="#de" hreflang="de">Deutsch</a>
|
||||
<span aria-hidden="true">·</span>
|
||||
<a href="#en" hreflang="en">English</a>
|
||||
</nav>
|
||||
<slot />
|
||||
</article>
|
||||
</Base>
|
||||
|
||||
<style>
|
||||
.legal {
|
||||
max-width: 780px;
|
||||
padding-block: 4rem 5rem;
|
||||
color: #c3c7ce;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.langs {
|
||||
display: flex;
|
||||
gap: 0.6rem;
|
||||
margin-bottom: 2rem;
|
||||
color: var(--faint);
|
||||
font-size: 0.925rem;
|
||||
font-family: var(--display);
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.legal :global(section + section) {
|
||||
margin-top: 4.5rem;
|
||||
padding-top: 3.5rem;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.legal :global(h1) {
|
||||
font-size: clamp(2rem, 1.6rem + 1.6vw, 2.75rem);
|
||||
margin-bottom: 1.5rem;
|
||||
color: var(--text);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.legal :global(h2) {
|
||||
font-size: 1.3rem;
|
||||
margin: 2.25rem 0 0.75rem;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.legal :global(p),
|
||||
.legal :global(ul),
|
||||
.legal :global(address) {
|
||||
margin: 0 0 0.9rem;
|
||||
}
|
||||
|
||||
.legal :global(address) {
|
||||
font-style: normal;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.legal :global(ul) {
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
|
||||
.legal :global(li + li) {
|
||||
margin-top: 0.35rem;
|
||||
}
|
||||
|
||||
.legal :global(.note) {
|
||||
padding: 0.9rem 1.1rem;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--panel);
|
||||
color: var(--muted);
|
||||
font-size: 0.925rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
---
|
||||
|
||||
<Base title="Not found · Bass Foundry" description="This page does not exist." noindex minimal>
|
||||
<section class="container nf">
|
||||
<p class="code">404</p>
|
||||
<h1>Nothing cast here</h1>
|
||||
<p>This page does not exist, or it moved.</p>
|
||||
<a class="btn btn-primary" href="/">Back to the foundry</a>
|
||||
</section>
|
||||
</Base>
|
||||
|
||||
<style>
|
||||
.nf {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 1rem;
|
||||
padding-block: 7rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.code {
|
||||
color: var(--accent);
|
||||
font-family: var(--display);
|
||||
font-size: clamp(5rem, 3rem + 8vw, 9rem);
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,193 @@
|
||||
---
|
||||
// Keep this text in step with docker/nginx.conf: it promises no access logs, no cookies and
|
||||
// no third-party requests. The %%…%% placeholders are filled in when the container starts.
|
||||
import Legal from '../layouts/Legal.astro';
|
||||
---
|
||||
|
||||
<Legal title="Datenschutzerklärung · Bass Foundry" description="Datenschutzerklärung / Privacy policy">
|
||||
<section id="de" lang="de">
|
||||
<h1>Datenschutzerklärung</h1>
|
||||
|
||||
<h2>1. Verantwortlicher</h2>
|
||||
<p>Verantwortlich für die Datenverarbeitung auf dieser Website im Sinne der Datenschutz-Grundverordnung (DSGVO):</p>
|
||||
<address>
|
||||
%%IMPRESSUM_NAME%%<br />
|
||||
%%IMPRESSUM_STREET%%<br />
|
||||
%%IMPRESSUM_CITY%%<br />
|
||||
%%IMPRESSUM_COUNTRY%%<br />
|
||||
E-Mail: <a href="mailto:%%IMPRESSUM_EMAIL%%">%%IMPRESSUM_EMAIL%%</a>
|
||||
</address>
|
||||
|
||||
<h2>2. Das Wichtigste in Kürze</h2>
|
||||
<ul>
|
||||
<li>Diese Website ist eine reine Informationsseite. Es gibt keine Registrierung, keine Formulare und keine Kommentare.</li>
|
||||
<li>Es werden keine Cookies gesetzt und keine Daten im Browser gespeichert (kein Local Storage, kein Session Storage).</li>
|
||||
<li>Es gibt kein Tracking, keine Reichweitenmessung, keine Werbung und keine Social-Media-Plugins.</li>
|
||||
<li>
|
||||
Alle Bestandteile der Seite (Schriften, Bilder, Skripte, Stylesheets) werden von diesem Server ausgeliefert.
|
||||
Inhalte Dritter werden nicht eingebunden; Ihr Browser baut beim Aufruf keine Verbindung zu anderen Servern auf.
|
||||
</li>
|
||||
<li>Der Webserver schreibt keine Zugriffsprotokolle.</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. Bereitstellung der Website</h2>
|
||||
<p>
|
||||
Wenn Sie diese Website aufrufen, übermittelt Ihr Browser technisch bedingt Daten an den Server: Ihre IP-Adresse,
|
||||
Datum und Uhrzeit der Anfrage, die angeforderte Adresse, die Browserkennung (User-Agent), gegebenenfalls die zuvor
|
||||
besuchte Seite (Referrer) und Ihre bevorzugten Sprachen (Accept-Language).
|
||||
</p>
|
||||
<p>
|
||||
Diese Daten werden ausschließlich verarbeitet, um die Seite auszuliefern, und nur für die Dauer der jeweiligen
|
||||
Verbindung im Arbeitsspeicher gehalten. Zugriffsprotokolle werden nicht geführt. Protokolliert werden lediglich
|
||||
schwerwiegende technische Fehler des Servers selbst; solche Einträge werden durch die automatische
|
||||
Protokollrotation fortlaufend überschrieben.
|
||||
</p>
|
||||
<p>
|
||||
Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO. Das berechtigte Interesse liegt in der sicheren und funktionsfähigen
|
||||
Bereitstellung der Website. Die Verbindung ist per TLS verschlüsselt.
|
||||
</p>
|
||||
<p data-optional="hoster">
|
||||
Die Website wird bei folgendem Anbieter betrieben, der die genannten Daten in meinem Auftrag verarbeitet (Art. 28
|
||||
DSGVO): %%HOSTER%%
|
||||
</p>
|
||||
|
||||
<h2>4. Cookies und Speicherung im Browser</h2>
|
||||
<p>
|
||||
Diese Website setzt keine Cookies und greift nicht auf Informationen in Ihrem Endgerät zu, die über das für die
|
||||
Auslieferung technisch Notwendige hinausgehen. Eine Einwilligung nach § 25 TDDDG ist daher nicht erforderlich.
|
||||
</p>
|
||||
|
||||
<h2>5. Links zu anderen Websites</h2>
|
||||
<p>
|
||||
Quellcode, Anleitungen und Downloads der Plugins liegen auf git.narl.io, einer Gitea-Instanz. Erst wenn Sie einen
|
||||
solchen Link anklicken oder einen Download starten, baut Ihr Browser eine Verbindung zu diesem Server auf; ab dann
|
||||
gelten die dortigen Datenschutzhinweise. Vorher werden keine Daten dorthin übertragen.
|
||||
</p>
|
||||
|
||||
<h2>6. Kontakt per E-Mail</h2>
|
||||
<p>
|
||||
Wenn Sie mir eine E-Mail schreiben, verarbeite ich Ihre Adresse und den Inhalt der Nachricht, um Ihr Anliegen zu
|
||||
bearbeiten (Art. 6 Abs. 1 lit. f DSGVO, bei vertragsbezogenen Anfragen lit. b). Die Daten werden gelöscht, sobald sie
|
||||
dafür nicht mehr benötigt werden und keine gesetzlichen Aufbewahrungspflichten bestehen.
|
||||
</p>
|
||||
|
||||
<h2>7. Ihre Rechte</h2>
|
||||
<p>Sie haben gegenüber dem Verantwortlichen das Recht auf</p>
|
||||
<ul>
|
||||
<li>Auskunft über die zu Ihnen gespeicherten Daten (Art. 15 DSGVO),</li>
|
||||
<li>Berichtigung unrichtiger Daten (Art. 16 DSGVO),</li>
|
||||
<li>Löschung (Art. 17 DSGVO) und Einschränkung der Verarbeitung (Art. 18 DSGVO),</li>
|
||||
<li>Datenübertragbarkeit (Art. 20 DSGVO),</li>
|
||||
<li>Widerspruch gegen Verarbeitungen, die auf Art. 6 Abs. 1 lit. f DSGVO beruhen (Art. 21 DSGVO).</li>
|
||||
</ul>
|
||||
<p>
|
||||
Außerdem können Sie sich bei einer Datenschutz-Aufsichtsbehörde beschweren (Art. 77 DSGVO), insbesondere in dem
|
||||
Mitgliedstaat Ihres Aufenthaltsorts, Ihres Arbeitsplatzes oder des Orts des mutmaßlichen Verstoßes.
|
||||
</p>
|
||||
|
||||
<h2>8. Keine automatisierte Entscheidungsfindung</h2>
|
||||
<p>Eine automatisierte Entscheidungsfindung einschließlich Profiling (Art. 22 DSGVO) findet nicht statt.</p>
|
||||
|
||||
<h2>9. Die vorgestellte Software</h2>
|
||||
<p>
|
||||
Diese Erklärung betrifft nur diese Website. Die vorgestellten Plugins laufen ausschließlich auf Ihrem Rechner in
|
||||
Ihrer Musiksoftware. Sie bauen keine Netzwerkverbindungen auf, benötigen kein Konto und senden keine
|
||||
Nutzungsdaten. Dateien, die ein Plugin schreibt (etwa die Exporte von Crucible), bleiben auf Ihrem Rechner. Der
|
||||
Entwickler erhält über die Plugins keine Daten.
|
||||
</p>
|
||||
|
||||
<p class="note">Stand: November 2026</p>
|
||||
</section>
|
||||
|
||||
<section id="en" lang="en">
|
||||
<h1>Privacy policy</h1>
|
||||
<p class="note">Courtesy translation. The German version above is the legally binding one.</p>
|
||||
|
||||
<h2>1. Controller</h2>
|
||||
<p>The controller responsible for data processing on this website under the General Data Protection Regulation (GDPR) is:</p>
|
||||
<address>
|
||||
%%IMPRESSUM_NAME%%<br />
|
||||
%%IMPRESSUM_STREET%%<br />
|
||||
%%IMPRESSUM_CITY%%<br />
|
||||
%%IMPRESSUM_COUNTRY%%<br />
|
||||
Email: <a href="mailto:%%IMPRESSUM_EMAIL%%">%%IMPRESSUM_EMAIL%%</a>
|
||||
</address>
|
||||
|
||||
<h2>2. In short</h2>
|
||||
<ul>
|
||||
<li>This website only provides information. There is no registration, no forms and no comments.</li>
|
||||
<li>No cookies are set and nothing is stored in your browser (no local storage, no session storage).</li>
|
||||
<li>There is no tracking, no analytics, no advertising and no social media plugins.</li>
|
||||
<li>
|
||||
Every part of the page (fonts, images, scripts, stylesheets) is served by this server. No third-party content is
|
||||
embedded, and your browser connects to no other server while loading it.
|
||||
</li>
|
||||
<li>The web server writes no access logs.</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. Serving the website</h2>
|
||||
<p>
|
||||
When you open this website, your browser necessarily sends data to the server: your IP address, the date and time of
|
||||
the request, the requested address, the browser identification (user agent), possibly the previously visited page
|
||||
(referrer), and your preferred languages (Accept-Language).
|
||||
</p>
|
||||
<p>
|
||||
This data is processed only to deliver the page and is held in memory only for the duration of the connection. No
|
||||
access logs are kept. Only serious technical errors of the server itself are logged, and automatic log rotation
|
||||
continuously overwrites such entries.
|
||||
</p>
|
||||
<p>
|
||||
The legal basis is Art. 6 (1) (f) GDPR. The legitimate interest is serving the website securely and reliably. The
|
||||
connection is encrypted with TLS.
|
||||
</p>
|
||||
<p data-optional="hoster">
|
||||
The website is operated by the following provider, which processes this data on my behalf (Art. 28 GDPR): %%HOSTER%%
|
||||
</p>
|
||||
|
||||
<h2>4. Cookies and browser storage</h2>
|
||||
<p>
|
||||
This website sets no cookies and accesses no information on your device beyond what is technically required to
|
||||
deliver it. Consent under § 25 TDDDG is therefore not required.
|
||||
</p>
|
||||
|
||||
<h2>5. Links to other websites</h2>
|
||||
<p>
|
||||
The plugins’ source code, guides and downloads are on git.narl.io, a Gitea instance. Your browser only connects
|
||||
to that server when you click such a link or start a download; from then on, the privacy information there
|
||||
applies. No data is sent there before that.
|
||||
</p>
|
||||
|
||||
<h2>6. Contact by email</h2>
|
||||
<p>
|
||||
If you email me, I process your address and the content of your message to handle your request (Art. 6 (1) (f) GDPR,
|
||||
or (b) for contract-related requests). The data is deleted as soon as it is no longer needed for that purpose and no
|
||||
statutory retention obligations apply.
|
||||
</p>
|
||||
|
||||
<h2>7. Your rights</h2>
|
||||
<p>You have the right to</p>
|
||||
<ul>
|
||||
<li>access the data stored about you (Art. 15 GDPR),</li>
|
||||
<li>rectification of inaccurate data (Art. 16 GDPR),</li>
|
||||
<li>erasure (Art. 17 GDPR) and restriction of processing (Art. 18 GDPR),</li>
|
||||
<li>data portability (Art. 20 GDPR),</li>
|
||||
<li>object to processing based on Art. 6 (1) (f) GDPR (Art. 21 GDPR).</li>
|
||||
</ul>
|
||||
<p>
|
||||
You may also lodge a complaint with a data protection supervisory authority (Art. 77 GDPR), in particular in the
|
||||
member state of your residence, your place of work or the place of the alleged infringement.
|
||||
</p>
|
||||
|
||||
<h2>8. No automated decision-making</h2>
|
||||
<p>No automated decision-making, including profiling (Art. 22 GDPR), takes place.</p>
|
||||
|
||||
<h2>9. The software presented here</h2>
|
||||
<p>
|
||||
This policy covers only this website. The plugins presented here run only on your computer, inside your music
|
||||
software. They make no network connections, need no account and send no usage data. Files a plugin writes (such
|
||||
as Crucible’s exports) stay on your computer. The developer receives no data through the plugins.
|
||||
</p>
|
||||
|
||||
<p class="note">Last updated: November 2026</p>
|
||||
</section>
|
||||
</Legal>
|
||||
@@ -0,0 +1,140 @@
|
||||
---
|
||||
// The %%…%% placeholders are filled in when the container starts, from the IMPRESSUM_*
|
||||
// environment variables (docker/40-legal-notice.sh). Elements marked data-optional are
|
||||
// removed when their variable is empty.
|
||||
import Legal from '../layouts/Legal.astro';
|
||||
---
|
||||
|
||||
<Legal title="Impressum · Bass Foundry" description="Impressum / Legal notice">
|
||||
<section id="de" lang="de">
|
||||
<h1>Impressum</h1>
|
||||
|
||||
<h2>Angaben gemäß § 5 DDG</h2>
|
||||
<address>
|
||||
%%IMPRESSUM_NAME%%<br />
|
||||
%%IMPRESSUM_STREET%%<br />
|
||||
%%IMPRESSUM_CITY%%<br />
|
||||
%%IMPRESSUM_COUNTRY%%
|
||||
</address>
|
||||
|
||||
<h2>Kontakt</h2>
|
||||
<p>E-Mail: <a href="mailto:%%IMPRESSUM_EMAIL%%">%%IMPRESSUM_EMAIL%%</a></p>
|
||||
<p data-optional="phone">Telefon: %%IMPRESSUM_PHONE%%</p>
|
||||
|
||||
<h2>Verantwortlich für den Inhalt nach § 18 Abs. 2 MStV</h2>
|
||||
<address>
|
||||
%%IMPRESSUM_NAME%%<br />
|
||||
%%IMPRESSUM_STREET%%<br />
|
||||
%%IMPRESSUM_CITY%%
|
||||
</address>
|
||||
|
||||
<h2>Über dieses Angebot</h2>
|
||||
<p>
|
||||
Diese Website stellt „Bass Foundry“ vor, eine private, nicht kommerzielle Sammlung von Open-Source-Audio-Plugins
|
||||
im CLAP-Format. Die Plugins werden kostenlos und ohne Gewähr bereitgestellt. Das Projekt steht in keiner
|
||||
Verbindung zur Image-Line Software NV und wird von ihr weder unterstützt noch empfohlen. „FL Studio“ ist eine
|
||||
Marke von Image-Line; der Name wird hier ausschließlich verwendet, um zu beschreiben, mit welcher Software die
|
||||
Plugins zusammenarbeiten.
|
||||
</p>
|
||||
|
||||
<h2>Verbraucherstreitbeilegung</h2>
|
||||
<p>
|
||||
Ich bin nicht bereit und nicht verpflichtet, an Streitbeilegungsverfahren vor einer
|
||||
Verbraucherschlichtungsstelle teilzunehmen.
|
||||
</p>
|
||||
|
||||
<h2>Haftung für Inhalte</h2>
|
||||
<p>
|
||||
Die Inhalte dieser Seiten wurden mit größter Sorgfalt erstellt. Für die Richtigkeit, Vollständigkeit und
|
||||
Aktualität der Inhalte kann ich jedoch keine Gewähr übernehmen. Als Diensteanbieter bin ich gemäß § 7 Abs. 1
|
||||
DDG für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 DDG
|
||||
bin ich jedoch nicht verpflichtet, übermittelte oder gespeicherte fremde Informationen zu überwachen oder
|
||||
nach Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen. Verpflichtungen zur Entfernung
|
||||
oder Sperrung der Nutzung von Informationen nach den allgemeinen Gesetzen bleiben hiervon unberührt. Eine
|
||||
diesbezügliche Haftung ist erst ab dem Zeitpunkt der Kenntnis einer konkreten Rechtsverletzung möglich. Bei
|
||||
Bekanntwerden entsprechender Rechtsverletzungen werde ich diese Inhalte umgehend entfernen.
|
||||
</p>
|
||||
|
||||
<h2>Haftung für Links</h2>
|
||||
<p>
|
||||
Diese Website enthält Links zu externen Websites Dritter (zum Beispiel den Quellcode und die Downloads auf git.narl.io),
|
||||
auf deren Inhalte ich keinen Einfluss habe. Für diese fremden Inhalte ist stets der jeweilige Anbieter oder
|
||||
Betreiber der Seiten verantwortlich. Die verlinkten Seiten wurden zum Zeitpunkt der Verlinkung auf mögliche
|
||||
Rechtsverstöße überprüft; rechtswidrige Inhalte waren zu diesem Zeitpunkt nicht erkennbar. Eine permanente
|
||||
inhaltliche Kontrolle der verlinkten Seiten ist ohne konkrete Anhaltspunkte einer Rechtsverletzung nicht
|
||||
zumutbar. Bei Bekanntwerden von Rechtsverletzungen werde ich derartige Links umgehend entfernen.
|
||||
</p>
|
||||
|
||||
<h2>Urheberrecht</h2>
|
||||
<p>
|
||||
Die Plugins von „Bass Foundry“ stehen unter der ISC-Lizenz. Die in die Plugins eingebettete Schrift Roboto
|
||||
Condensed sowie die auf dieser Website verwendeten Schriften Roboto Condensed und Inter stehen unter der SIL Open
|
||||
Font License 1.1. Texte, Grafiken und Screenshots dieser Website sind, soweit nicht anders angegeben, eigene Werke;
|
||||
die Screenshots werden aus dem Code der Plugins erzeugt.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="en" lang="en">
|
||||
<h1>Legal notice</h1>
|
||||
<p class="note">
|
||||
Courtesy translation. The German version above is the legally binding one.
|
||||
</p>
|
||||
|
||||
<h2>Information pursuant to § 5 DDG (German Digital Services Act)</h2>
|
||||
<address>
|
||||
%%IMPRESSUM_NAME%%<br />
|
||||
%%IMPRESSUM_STREET%%<br />
|
||||
%%IMPRESSUM_CITY%%<br />
|
||||
%%IMPRESSUM_COUNTRY%%
|
||||
</address>
|
||||
|
||||
<h2>Contact</h2>
|
||||
<p>Email: <a href="mailto:%%IMPRESSUM_EMAIL%%">%%IMPRESSUM_EMAIL%%</a></p>
|
||||
<p data-optional="phone">Phone: %%IMPRESSUM_PHONE%%</p>
|
||||
|
||||
<h2>Responsible for the content pursuant to § 18 (2) MStV</h2>
|
||||
<address>
|
||||
%%IMPRESSUM_NAME%%<br />
|
||||
%%IMPRESSUM_STREET%%<br />
|
||||
%%IMPRESSUM_CITY%%
|
||||
</address>
|
||||
|
||||
<h2>About this website</h2>
|
||||
<p>
|
||||
This website presents “Bass Foundry”, a private, non-commercial collection of open-source audio plugins in the
|
||||
CLAP format. The plugins are provided free of charge and without warranty. The project is not affiliated with,
|
||||
endorsed by, or supported by Image-Line Software NV. “FL Studio” is a trademark of Image-Line, used here only to
|
||||
describe the software the plugins work with.
|
||||
</p>
|
||||
|
||||
<h2>Consumer dispute resolution</h2>
|
||||
<p>I am neither willing nor obliged to take part in dispute resolution proceedings before a consumer arbitration board.</p>
|
||||
|
||||
<h2>Liability for content</h2>
|
||||
<p>
|
||||
The content of these pages was prepared with great care, but I cannot guarantee that it is correct, complete
|
||||
or up to date. As a service provider I am responsible for my own content on these pages under general law
|
||||
(§ 7 (1) DDG). Under §§ 8 to 10 DDG I am not obliged to monitor third-party information that is transmitted or
|
||||
stored, or to investigate circumstances that indicate illegal activity. Obligations to remove or block the use
|
||||
of information under general law remain unaffected. Liability in this respect is only possible from the time
|
||||
a specific infringement becomes known. I will remove such content as soon as I learn of an infringement.
|
||||
</p>
|
||||
|
||||
<h2>Liability for links</h2>
|
||||
<p>
|
||||
This website links to external third-party websites (for example the source code and downloads on git.narl.io) whose
|
||||
content I cannot influence. The respective provider or operator is always responsible for the content of linked
|
||||
pages. The linked pages were checked for possible legal violations when the link was created, and no illegal
|
||||
content was apparent at that time. Permanent monitoring of linked pages is not reasonable without concrete
|
||||
evidence of an infringement. I will remove such links as soon as I learn of an infringement.
|
||||
</p>
|
||||
|
||||
<h2>Copyright</h2>
|
||||
<p>
|
||||
The “Bass Foundry” plugins are released under the ISC License. The Roboto Condensed font embedded in the plugins,
|
||||
and the Roboto Condensed and Inter fonts used on this website, are under the SIL Open Font License 1.1. Unless
|
||||
stated otherwise, the text, graphics and screenshots on this website are my own work; the screenshots are
|
||||
rendered from the plugins’ code.
|
||||
</p>
|
||||
</section>
|
||||
</Legal>
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
import Bench from '../components/Bench.astro';
|
||||
import Foundry from '../components/Foundry.astro';
|
||||
import Hero from '../components/Hero.astro';
|
||||
import Idea from '../components/Idea.astro';
|
||||
import Install from '../components/Install.astro';
|
||||
import Plugins from '../components/Plugins.astro';
|
||||
import Principles from '../components/Principles.astro';
|
||||
import Base from '../layouts/Base.astro';
|
||||
---
|
||||
|
||||
<Base
|
||||
title="Bass Foundry · CLAP plugins for bass music in FL Studio"
|
||||
description="Open-source CLAP plugins for riddim, dubstep and bass music in FL Studio: sub anchor, harmonic EQ, tracked distortion, layer phase alignment, cycle texture, resampler and more."
|
||||
>
|
||||
<Hero />
|
||||
<Idea />
|
||||
<Plugins />
|
||||
<Install />
|
||||
<Principles />
|
||||
<Foundry />
|
||||
<Bench />
|
||||
</Base>
|
||||
@@ -0,0 +1,297 @@
|
||||
---
|
||||
import { Picture } from 'astro:assets';
|
||||
|
||||
import Icon from '../../components/Icon.astro';
|
||||
import { PLUGINS, type Plugin } from '../../data/plugins';
|
||||
import Base from '../../layouts/Base.astro';
|
||||
|
||||
export function getStaticPaths() {
|
||||
return PLUGINS.map((plugin, i) => ({
|
||||
params: { slug: plugin.slug },
|
||||
props: {
|
||||
plugin,
|
||||
prev: PLUGINS[(i + PLUGINS.length - 1) % PLUGINS.length],
|
||||
next: PLUGINS[(i + 1) % PLUGINS.length],
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
interface Props {
|
||||
plugin: Plugin;
|
||||
prev: Plugin;
|
||||
next: Plugin;
|
||||
}
|
||||
|
||||
const { plugin: p, prev, next } = Astro.props;
|
||||
---
|
||||
|
||||
<Base title={`${p.name} · ${p.role} · Bass Foundry`} description={`${p.name}: ${p.description}. A free CLAP plugin for FL Studio.`}>
|
||||
<article class="container page">
|
||||
<a class="back" href="/#plugins"><Icon name="back" />All plugins</a>
|
||||
|
||||
<header class="head">
|
||||
<p class="eyebrow">{p.role}</p>
|
||||
<h1>{p.name}</h1>
|
||||
<p class="lead">{p.description}.</p>
|
||||
<div class="ctas">
|
||||
<a class="btn btn-primary" href={p.download}><Icon name="download" />Download {p.version}</a>
|
||||
<a class="btn btn-ghost" href={p.guide}><Icon name="book" />Full guide</a>
|
||||
</div>
|
||||
<p class="file">{p.zip} · Windows x64 · CLAP</p>
|
||||
</header>
|
||||
|
||||
<figure class="shot plate">
|
||||
<Picture
|
||||
src={p.shot}
|
||||
alt={`The ${p.name} editor`}
|
||||
formats={['avif', 'webp']}
|
||||
fallbackFormat="webp"
|
||||
widths={[720, 1040, p.shot.width]}
|
||||
sizes="(max-width: 1180px) 100vw, 1120px"
|
||||
loading="eager"
|
||||
/>
|
||||
</figure>
|
||||
|
||||
<div class="cols">
|
||||
<div class="text">
|
||||
<section>
|
||||
<h2>The problem</h2>
|
||||
<p>{p.problem}</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>What {p.name} does</h2>
|
||||
<ul class="points" role="list">{p.points.map((point) => <li>{point}</li>)}</ul>
|
||||
</section>
|
||||
<p class="metal">{p.metal}</p>
|
||||
</div>
|
||||
|
||||
<aside class="facts plate" aria-label={`${p.name} facts`}>
|
||||
<dl>
|
||||
<div><dt class="label">Version</dt><dd>{p.version}</dd></div>
|
||||
<div><dt class="label">Load on</dt><dd>{p.loadOn}</dd></div>
|
||||
<div><dt class="label">Latency</dt><dd>{p.latency}{p.latency !== 'none' && ', reported to the host'}</dd></div>
|
||||
{p.tracksNotes && <div><dt class="label">Pitch</dt><dd>Follows the note, from audio or MIDI</dd></div>}
|
||||
<div><dt class="label">Format</dt><dd>CLAP · Windows x64 · FL Studio 2024.1+</dd></div>
|
||||
<div><dt class="label">CLAP ID</dt><dd><code>{p.clapId}</code></dd></div>
|
||||
</dl>
|
||||
<ul class="links" role="list">
|
||||
<li><a href={p.release}><Icon name="tag" />Release {p.tag}</a></li>
|
||||
<li><a href={p.changelog}><Icon name="list" />Changelog</a></li>
|
||||
<li><a href={p.guide}><Icon name="book" />Guide</a></li>
|
||||
</ul>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<nav class="pager" aria-label="More plugins">
|
||||
<a href={`/plugins/${prev.slug}/`}>
|
||||
<span class="label">Previous</span>
|
||||
<span class="name"><Icon name="back" />{prev.name}</span>
|
||||
</a>
|
||||
<a class="next" href={`/plugins/${next.slug}/`}>
|
||||
<span class="label">Next</span>
|
||||
<span class="name">{next.name}<Icon name="arrow" /></span>
|
||||
</a>
|
||||
</nav>
|
||||
</article>
|
||||
</Base>
|
||||
|
||||
<style>
|
||||
.page {
|
||||
padding-block: 2.5rem 5rem;
|
||||
}
|
||||
|
||||
.back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
color: var(--muted);
|
||||
font-family: var(--display);
|
||||
font-size: 0.9rem;
|
||||
letter-spacing: 0.12em;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.back :global(svg),
|
||||
.links :global(svg),
|
||||
.name :global(svg) {
|
||||
width: 1.1em;
|
||||
height: 1.1em;
|
||||
}
|
||||
|
||||
.head {
|
||||
display: grid;
|
||||
gap: 1.1rem;
|
||||
max-width: 760px;
|
||||
margin: 2.5rem 0 2.75rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(3.5rem, 2rem + 6vw, 6.5rem);
|
||||
font-weight: 700;
|
||||
line-height: 0.95;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.lead {
|
||||
color: #c3c7ce;
|
||||
font-size: clamp(1.15rem, 1rem + 0.5vw, 1.4rem);
|
||||
}
|
||||
|
||||
.ctas {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.file {
|
||||
color: var(--faint);
|
||||
font-family: var(--mono);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.shot {
|
||||
margin: 0;
|
||||
padding: clamp(0.6rem, 2vw, 1.4rem);
|
||||
}
|
||||
|
||||
.shot :global(img) {
|
||||
width: 100%;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.cols {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
|
||||
gap: clamp(2rem, 5vw, 4rem);
|
||||
margin-top: clamp(2.5rem, 5vw, 4rem);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.text {
|
||||
display: grid;
|
||||
gap: 2.25rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 1.7rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.points {
|
||||
display: grid;
|
||||
gap: 0.7rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.points li {
|
||||
position: relative;
|
||||
padding-left: 1.6rem;
|
||||
color: #c3c7ce;
|
||||
}
|
||||
|
||||
.points li::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0.62em;
|
||||
left: 0;
|
||||
width: 0.65rem;
|
||||
height: 2px;
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.metal {
|
||||
padding-left: 1rem;
|
||||
border-left: 2px solid var(--accent);
|
||||
color: var(--steel);
|
||||
font-family: var(--display);
|
||||
font-size: 1.15rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.facts {
|
||||
position: sticky;
|
||||
top: 6rem;
|
||||
padding: 1.5rem 1.75rem;
|
||||
}
|
||||
|
||||
dl {
|
||||
display: grid;
|
||||
gap: 0.9rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0.1rem 0 0;
|
||||
color: var(--text);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.links {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
margin: 1.4rem 0 0;
|
||||
padding: 1.2rem 0 0;
|
||||
border-top: 1px solid var(--line);
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.links a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.pager {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
margin-top: 4.5rem;
|
||||
padding-top: 2rem;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.pager a {
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
padding: 1.1rem 1.4rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.pager a:hover {
|
||||
border-color: var(--line-strong);
|
||||
}
|
||||
|
||||
.pager .next {
|
||||
justify-items: end;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.name {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--text);
|
||||
font-family: var(--display);
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.cols {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.facts {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
|
||||
export const GET: APIRoute = ({ site }) =>
|
||||
new Response(`User-agent: *\nAllow: /\n\nSitemap: ${new URL('/sitemap-index.xml', site)}\n`, {
|
||||
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
||||
});
|
||||
@@ -0,0 +1,320 @@
|
||||
@import '@fontsource-variable/inter';
|
||||
@import '@fontsource-variable/roboto-condensed';
|
||||
|
||||
/* The palette of the plugin editors (plugins/common/ui), slightly deepened for a page. */
|
||||
:root {
|
||||
--bg: #0b0c0e;
|
||||
--bg-2: #0e0f11;
|
||||
--panel: #15171a;
|
||||
--panel-2: #191b1f;
|
||||
--line: rgb(184 192 204 / 0.12);
|
||||
--line-strong: rgb(184 192 204 / 0.24);
|
||||
--text: #e8e9ec;
|
||||
--muted: #9aa0a9;
|
||||
--faint: #6b717b;
|
||||
--dim: #464a52;
|
||||
--accent: #ff0160;
|
||||
--accent-soft: rgb(255 1 96 / 0.14);
|
||||
--steel: #b8c0cc;
|
||||
--focus: #7ea7d8;
|
||||
|
||||
--display: 'Roboto Condensed Variable', 'Roboto Condensed', 'Arial Narrow', sans-serif;
|
||||
--body: 'Inter Variable', Inter, system-ui, sans-serif;
|
||||
--mono: ui-monospace, 'SFMono-Regular', 'JetBrains Mono', Menlo, Consolas, monospace;
|
||||
|
||||
--radius: 14px;
|
||||
--radius-sm: 8px;
|
||||
--max: 1180px;
|
||||
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
scroll-padding-top: 5rem;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--muted);
|
||||
font-family: var(--body);
|
||||
font-size: 1.0625rem;
|
||||
line-height: 1.65;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
font-family: var(--display);
|
||||
font-weight: 600;
|
||||
line-height: 1.08;
|
||||
letter-spacing: -0.005em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--text);
|
||||
text-decoration-color: var(--dim);
|
||||
text-underline-offset: 0.2em;
|
||||
transition: color 0.15s, text-decoration-color 0.15s;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--accent);
|
||||
text-decoration-color: currentColor;
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--focus);
|
||||
outline-offset: 3px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
img {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
code,
|
||||
kbd {
|
||||
font-family: var(--mono);
|
||||
font-size: 0.88em;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 0.12em 0.4em;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 5px;
|
||||
background: var(--panel-2);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--accent);
|
||||
color: #0b0c0e;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: var(--max);
|
||||
margin-inline: auto;
|
||||
padding-inline: clamp(1.1rem, 4vw, 2rem);
|
||||
}
|
||||
|
||||
.section {
|
||||
padding-block: clamp(4rem, 8vw, 6.5rem);
|
||||
}
|
||||
|
||||
.section-head {
|
||||
display: grid;
|
||||
gap: 0.9rem;
|
||||
max-width: 720px;
|
||||
margin-bottom: clamp(2.5rem, 5vw, 3.75rem);
|
||||
}
|
||||
|
||||
.section-head h2 {
|
||||
font-size: clamp(2.1rem, 1.5rem + 2.4vw, 3.25rem);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.section-head p {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
/* Small uppercase label, the way the editors label their panels. */
|
||||
.eyebrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
color: var(--accent);
|
||||
font-family: var(--display);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.eyebrow::before {
|
||||
content: '';
|
||||
width: 1.6rem;
|
||||
height: 2px;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: var(--faint);
|
||||
font-family: var(--display);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* A steel plate with four rivets in the corners. */
|
||||
.plate {
|
||||
position: relative;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
background:
|
||||
radial-gradient(circle at 12px 12px, var(--dim) 2.5px, transparent 3px),
|
||||
radial-gradient(circle at calc(100% - 12px) 12px, var(--dim) 2.5px, transparent 3px),
|
||||
radial-gradient(circle at 12px calc(100% - 12px), var(--dim) 2.5px, transparent 3px),
|
||||
radial-gradient(circle at calc(100% - 12px) calc(100% - 12px), var(--dim) 2.5px, transparent 3px),
|
||||
linear-gradient(180deg, var(--panel-2), var(--panel));
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgb(255 255 255 / 0.04),
|
||||
0 20px 50px -30px rgb(0 0 0 / 0.8);
|
||||
}
|
||||
|
||||
/* Knurled band: the crossed ridges of a knurled grip. */
|
||||
.knurl {
|
||||
height: 10px;
|
||||
border-block: 1px solid var(--line);
|
||||
background:
|
||||
repeating-linear-gradient(45deg, rgb(184 192 204 / 0.16) 0 1px, transparent 1px 7px),
|
||||
repeating-linear-gradient(-45deg, rgb(184 192 204 / 0.16) 0 1px, transparent 1px 7px),
|
||||
var(--bg-2);
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.55rem;
|
||||
min-height: 2.9rem;
|
||||
padding: 0.6rem 1.2rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
font-family: var(--display);
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
line-height: 1;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
transition:
|
||||
background 0.15s,
|
||||
border-color 0.15s,
|
||||
color 0.15s,
|
||||
transform 0.15s,
|
||||
box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.btn svg {
|
||||
width: 1.1em;
|
||||
height: 1.1em;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
color: #0b0c0e;
|
||||
box-shadow: 0 8px 28px -10px rgb(255 1 96 / 0.7);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #ff2b7a;
|
||||
color: #0b0c0e;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 12px 34px -10px rgb(255 1 96 / 0.85);
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
border-color: var(--line-strong);
|
||||
background: rgb(255 255 255 / 0.02);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
border-color: var(--steel);
|
||||
background: rgb(255 255 255 / 0.05);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
min-height: 2.35rem;
|
||||
padding: 0.45rem 0.85rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.2rem 0.6rem;
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: 999px;
|
||||
color: var(--steel);
|
||||
font-family: var(--display);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.06em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.badge-accent {
|
||||
border-color: rgb(255 1 96 / 0.45);
|
||||
background: var(--accent-soft);
|
||||
color: #ff6b9f;
|
||||
}
|
||||
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
top: -100px;
|
||||
left: 1rem;
|
||||
z-index: 100;
|
||||
padding: 0.6rem 1rem;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--accent);
|
||||
color: #0b0c0e;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.skip-link:focus {
|
||||
top: 1rem;
|
||||
}
|
||||
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"include": [".astro/types.d.ts", "**/*"],
|
||||
"exclude": ["dist"]
|
||||
}
|
||||
Reference in new issue
Block a user