diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index ff31102d3..8565f14e9 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -84,6 +84,9 @@ jobs: - name: Check demo loops working-directory: website run: npm run check:loops -- --live + - name: Check responsive backgrounds + working-directory: website + run: npm run check:backgrounds -- --live # The recreation's data is generated from the app's own source and a # vendored project document. It is committed, so it can go stale in either # direction — a locale key moves, a timeline constant changes — and the @@ -145,4 +148,4 @@ jobs: id-token: write steps: - id: deployment - uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 \ No newline at end of file + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts index e374aa2bd..17bb8e980 100644 --- a/website/docusaurus.config.ts +++ b/website/docusaurus.config.ts @@ -675,7 +675,7 @@ export default async function createConfig(): Promise { // link item only takes a string label, so the download glyph moves // to a CSS mask on .navbar-download-cta. to: "/download", - label: "Download", + label: "Downloads", className: "navbar-download-cta", position: "right", }, diff --git a/website/i18n/code.source.json b/website/i18n/code.source.json index 5d3e9b4fd..6d2e6482c 100644 --- a/website/i18n/code.source.json +++ b/website/i18n/code.source.json @@ -38,14 +38,11 @@ "download.preRelease.body": "Release candidates ship between stable versions, alongside older releases, checksums, and full release notes.", "download.preRelease.cta": "Browse all releases", "home.meta.title": "Free open-source screen recorder & video editor", - "home.meta.description": "OpenScreen is a free, open-source screen recorder and video editor for Windows, macOS, and Linux — native capture, on-device captions, no watermarks.", - "home.hero.badge.new": "NEW", - "home.hero.badge.text": "1.11 exports faster on macOS and Linux", - "home.hero.titleTagline": "A free, open-source screen recorder and video editor", - "home.hero.tagline": "Screen recording with native capture, local AI and no paywall.", - "home.hero.download": "Download", - "home.hero.readDocs": "Read the docs", - "home.hero.scrollHint": "Scroll down", + "home.meta.description": "Turn screen recordings into polished demos with automatic zooms, styled backgrounds and an editable cursor. Free and open source for Windows, macOS and Linux. No watermark.", + "home.hero.title": "Beautiful demos on the first take.", + "home.hero.tagline": "Automatic zooms, smooth cursor movement, and a frame that looks finished. Record, adjust, share.", + "home.hero.reassurance": "Free and open source. No watermark. No account.", + "home.features.platforms.title": "Windows, macOS, Linux", "home.features.kicker": "Also true", "home.features.title": "Free, local, cross-platform: three things a screenshot can't show.", "home.features.summary": "OpenScreen is a free, open-source screen recorder and video editor for Windows, macOS, and Linux: a raw capture goes in and a finished demo comes out, in the category {screenStudio} defined. It is MIT licensed, with no watermark and no account, and it continues the {originalProject}, which its creator archived after v1.5.0.", @@ -55,7 +52,6 @@ "home.features.free.body": "No paywalls, no premium tier, no usage caps. Every feature ships free for personal and commercial use.", "home.features.local.title": "Nothing is uploaded", "home.features.local.body": "Recording, transcription and rendering all happen on your machine, and your video never leaves it. Text leaves only when you ask: the chat panel and caption translation, each with a key you supply. Transcription downloads its 264 MB Whisper model once, on first run.", - "home.features.platforms.title": "Windows, macOS, Linux", "home.features.platforms.body": "One source tree, native capture on each. A Microsoft Store listing, a .dmg, an .exe, a .deb, a .rpm, a .pacman, an AppImage and a Nix flake.", "home.install.kicker": "Quick start", "home.install.title": "Download and install", @@ -72,6 +68,46 @@ "home.install.note.windows": "Windows", "home.install.note.mac": "Mac", "home.install.note.linux": "Linux", + "editor.skipLink": "Skip the editor — go to downloads", + "editor.title": "Five things you will actually do", + "demoLoop.pause": "Pause video", + "demoLoop.play": "Play video", + "demoLoop.classic-zoom": "A task board recorded in OpenScreen. The view zooms in on a checklist item and a Ship button as they are clicked, then pulls back to the whole board.", + "demoLoop.3d-camera": "The same recording seen through the 3D camera, which tilts the screen and orbits around it while it follows the cursor.", + "demoLoop.3d-cursors": "One recorded pointer, redrawn in five cursor styles one after another while the recording keeps playing.", + "demoLoop.automatic-subtitles": "A narrated recording with captions under it, switching from English to French, Spanish and Korean.", + "demoLoop.caption-styles": "One caption shown in five styles: a plain caption, a bold yellow one, a handwritten one, a monospaced one, and one showing a single word at a time.", + "demoLoop.edit-by-transcript": "In the editor's transcript, a sentence is selected and deleted, and the matching cut appears on the timeline.", + "demoLoop.tighten-pauses": "Two clicks on the silence markers in the transcript, and both pauses are cut from the timeline.", + "demoLoop.background-picker": "The editor's background picker going through wallpapers, a gradient and a solid color, then turning on the animated Aurora background.", + "demoLoop.every-format": "The same recording exported in 16:9, then square, then vertical, with the framing following the cursor.", + "demoLoop.sensitive-data-mask": "A sign-in form being filled in. The email and password fields stay blurred while they are typed.", + "demoLoop.camera-blur": "A square webcam vignette over a screen recording. The view moves in on it while the room behind the speaker blurs, then pulls back.", + "demoLoop.every-layout": "One recording with a webcam, cycling through the layouts: picture in picture, side by side, stacked, full camera, then screen only.", + "demoLoop.device-frames": "The same playing recording framed in turn as a browser window, a phone, a laptop and a desktop monitor.", + "demoLoop.animated-backgrounds": "A recording on a plain background, then on a blurred photo background that moves slowly behind it.", + "demoLoop.beautiful-by-default": "A raw screen capture, then the same take as OpenScreen opens it, already zoomed, framed and with a restyled cursor, then in several looks.", + "demoLoop.simpler-editor": "The OpenScreen editor. One click on a background thumbnail restyles the whole video, then the side panel switches to the transcript.", + "demoLoop.edit-like-a-doc": "Text selected in the transcript is deleted like text in a document, and the video is cut to match.", + "demoLoop.ask-the-agent": "A request typed into the editor's chat panel, and the edits the agent makes appearing on the timeline.", + "demoLoop.title.classic-zoom": "Automatic zoom on clicks", + "demoLoop.title.3d-camera": "A 3D camera orbiting the screen", + "demoLoop.title.3d-cursors": "Cursor packs, drawn in 3D", + "demoLoop.title.automatic-subtitles": "Captions translated into other languages", + "demoLoop.title.caption-styles": "Five caption styles", + "demoLoop.title.edit-by-transcript": "Cutting video by deleting text in the transcript", + "demoLoop.title.tighten-pauses": "Cutting silences from the transcript", + "demoLoop.title.background-picker": "Changing the background behind a recording", + "demoLoop.title.every-format": "One recording in 16:9, 1:1 and 9:16", + "demoLoop.title.sensitive-data-mask": "Blurring an email address and a password", + "demoLoop.title.camera-blur": "Webcam background blur", + "demoLoop.title.every-layout": "Webcam layouts", + "demoLoop.title.device-frames": "Device frames", + "demoLoop.title.animated-backgrounds": "Animated backgrounds", + "demoLoop.title.beautiful-by-default": "From a raw screen capture to a finished demo", + "demoLoop.title.simpler-editor": "The OpenScreen editor", + "demoLoop.title.edit-like-a-doc": "Editing a video like a document", + "demoLoop.title.ask-the-agent": "Asking the editor's agent for an edit", "films.default.kicker": "Out of the box", "films.default.title": "Good-looking from the first take.", "films.default.lead": "Every new recording opens {zoomed}, on a wallpaper, with padding, a shadow and rounded corners. Change any of it, or export it as it is.", @@ -121,46 +157,8 @@ "films.ship.blur": "{t} Smooth or mosaic, as a rectangle, an oval or a freehand shape, over an email, a key or a password.", "films.ship.blur.t": "Blur regions.", "films.label": "More of the app, in motion", - "editor.skipLink": "Skip the editor — go to downloads", - "editor.title": "Five things you will actually do", - "demoLoop.pause": "Pause video", - "demoLoop.play": "Play video", - "demoLoop.classic-zoom": "A task board recorded in OpenScreen. The view zooms in on a checklist item and a Ship button as they are clicked, then pulls back to the whole board.", - "demoLoop.3d-camera": "The same recording seen through the 3D camera, which tilts the screen and orbits around it while it follows the cursor.", - "demoLoop.3d-cursors": "One recorded pointer, redrawn in five cursor styles one after another while the recording keeps playing.", - "demoLoop.automatic-subtitles": "A narrated recording with captions under it, switching from English to French, Spanish and Korean.", - "demoLoop.caption-styles": "One caption shown in five styles: a plain caption, a bold yellow one, a handwritten one, a monospaced one, and one showing a single word at a time.", - "demoLoop.edit-by-transcript": "In the editor's transcript, a sentence is selected and deleted, and the matching cut appears on the timeline.", - "demoLoop.tighten-pauses": "Two clicks on the silence markers in the transcript, and both pauses are cut from the timeline.", - "demoLoop.background-picker": "The editor's background picker going through wallpapers, a gradient and a solid color, then turning on the animated Aurora background.", - "demoLoop.every-format": "The same recording exported in 16:9, then square, then vertical, with the framing following the cursor.", - "demoLoop.sensitive-data-mask": "A sign-in form being filled in. The email and password fields stay blurred while they are typed.", - "demoLoop.camera-blur": "A square webcam vignette over a screen recording. The view moves in on it while the room behind the speaker blurs, then pulls back.", - "demoLoop.every-layout": "One recording with a webcam, cycling through the layouts: picture in picture, side by side, stacked, full camera, then screen only.", - "demoLoop.device-frames": "The same playing recording framed in turn as a browser window, a phone, a laptop and a desktop monitor.", - "demoLoop.animated-backgrounds": "A recording on a plain background, then on a blurred photo background that moves slowly behind it.", - "demoLoop.beautiful-by-default": "A raw screen capture, then the same take as OpenScreen opens it, already zoomed, framed and with a restyled cursor, then in several looks.", - "demoLoop.simpler-editor": "The OpenScreen editor. One click on a background thumbnail restyles the whole video, then the side panel switches to the transcript.", - "demoLoop.edit-like-a-doc": "Text selected in the transcript is deleted like text in a document, and the video is cut to match.", - "demoLoop.ask-the-agent": "A request typed into the editor's chat panel, and the edits the agent makes appearing on the timeline.", - "demoLoop.title.classic-zoom": "Automatic zoom on clicks", - "demoLoop.title.3d-camera": "A 3D camera orbiting the screen", - "demoLoop.title.3d-cursors": "Cursor packs, drawn in 3D", - "demoLoop.title.automatic-subtitles": "Captions translated into other languages", - "demoLoop.title.caption-styles": "Five caption styles", - "demoLoop.title.edit-by-transcript": "Cutting video by deleting text in the transcript", - "demoLoop.title.tighten-pauses": "Cutting silences from the transcript", - "demoLoop.title.background-picker": "Changing the background behind a recording", - "demoLoop.title.every-format": "One recording in 16:9, 1:1 and 9:16", - "demoLoop.title.sensitive-data-mask": "Blurring an email address and a password", - "demoLoop.title.camera-blur": "Webcam background blur", - "demoLoop.title.every-layout": "Webcam layouts", - "demoLoop.title.device-frames": "Device frames", - "demoLoop.title.animated-backgrounds": "Animated backgrounds", - "demoLoop.title.beautiful-by-default": "From a raw screen capture to a finished demo", - "demoLoop.title.simpler-editor": "The OpenScreen editor", - "demoLoop.title.edit-like-a-doc": "Editing a video like a document", - "demoLoop.title.ask-the-agent": "Asking the editor's agent for an edit", + "announcement.v2.link": "See what's new", + "announcement.v2.title": "OpenScreen v2 is here", "recreation.style.kicker": "Style", "recreation.style.title": "Swap the background", "recreation.style.sub": "Image, color or gradient behind your recording — no re-shoot.", @@ -176,6 +174,15 @@ "recreation.transcript.kicker": "Transcript", "recreation.transcript.title": "Edit video like text", "recreation.transcript.sub": "Delete a word or a silence; the cut lands on the timeline. Nothing destructive.", + "recreation.pause": "Pause animation", + "recreation.play": "Play animation", + "home.hero.explore": "Scroll to explore the editor", + "download.cta.mac": "Download for Mac", + "download.cta.windows": "Download for Windows", + "download.cta.linux": "Download for Linux", + "home.hero.download": "Download", + "download.cta.chooseMac": "Choose your Mac", + "download.cta.otherVersions": "Other versions", "showcase.record.kicker": "record", "showcase.record.claim": "It records with the operating system, not around it.", "showcase.record.body": "Pick a window or a display. macOS goes through ScreenCaptureKit, Windows through Windows Graphics Capture, Linux through PipeWire and the ScreenCast portal — on each, the capture path the system itself provides. The pointer is recorded as data rather than burned into the pixels, which is the only reason you could restyle it further up this page.", diff --git a/website/i18n/de/code.json b/website/i18n/de/code.json index b27e742e0..54f4f97d6 100644 --- a/website/i18n/de/code.json +++ b/website/i18n/de/code.json @@ -122,30 +122,14 @@ "message": "Kostenloser Open-Source-Bildschirmrekorder und Videoeditor" }, "home.meta.description": { - "message": "OpenScreen: kostenloser Open-Source-Bildschirmrekorder & Videoeditor für Windows, macOS und Linux. Native Aufnahme, lokale Untertitel, kein Wasserzeichen." - }, - "home.hero.badge.new": { - "message": "NEU" - }, - "home.hero.badge.text": { - "message": "1.11 exportiert schneller (Mac, Linux)", - "description": "Links to an English-only blog post. Must fit on one line on a 375px phone." - }, - "home.hero.titleTagline": { - "message": "Ein kostenloser Open-Source-Bildschirmrekorder und Videoeditor" + "message": "Machen Sie aus Bildschirmaufnahmen ansprechende Demos mit automatischen Zooms, gestalteten Hintergründen und einem bearbeitbaren Cursor. Kostenlos und Open Source für Windows, macOS und Linux. Ohne Wasserzeichen." }, "home.hero.tagline": { - "message": "Bildschirmaufnahme mit nativer Erfassung, lokaler KI und ohne Bezahlschranke." + "message": "Automatische Zooms, flüssige Cursorbewegungen und ein ansprechender Rahmen. Aufnehmen, anpassen, teilen." }, "home.hero.download": { "message": "Download" }, - "home.hero.readDocs": { - "message": "Doku lesen" - }, - "home.hero.scrollHint": { - "message": "Nach unten scrollen" - }, "home.features.kicker": { "message": "Auch das stimmt" }, @@ -1060,5 +1044,42 @@ "demoLoop.title.ask-the-agent": { "message": "Den Agenten des Editors um eine Änderung bitten", "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "home.hero.title": { + "message": "Schöne Demos beim ersten Versuch." + }, + "home.hero.reassurance": { + "message": "Kostenlos und Open Source. Ohne Wasserzeichen. Ohne Konto." + }, + "home.hero.explore": { + "message": "Scrollen, um den Editor zu entdecken" + }, + "recreation.pause": { + "message": "Animation pausieren" + }, + "recreation.play": { + "message": "Animation abspielen" + }, + "announcement.v2.title": { + "message": "OpenScreen v2 ist da" + }, + "announcement.v2.link": { + "message": "Die Neuerungen (auf Englisch)", + "description": "Links to an English-only blog post. Mention English in translated labels." + }, + "download.cta.mac": { + "message": "Für Mac herunterladen" + }, + "download.cta.windows": { + "message": "Für Windows herunterladen" + }, + "download.cta.linux": { + "message": "Für Linux herunterladen" + }, + "download.cta.chooseMac": { + "message": "Mac auswählen" + }, + "download.cta.otherVersions": { + "message": "Andere Versionen" } } diff --git a/website/i18n/de/docusaurus-theme-classic/navbar.json b/website/i18n/de/docusaurus-theme-classic/navbar.json index c8aba7a40..dc2219272 100644 --- a/website/i18n/de/docusaurus-theme-classic/navbar.json +++ b/website/i18n/de/docusaurus-theme-classic/navbar.json @@ -23,8 +23,8 @@ "message": "Discord", "description": "Navbar item with label Discord" }, - "item.label.Download": { - "message": "Download", - "description": "Navbar item with label Download" + "item.label.Downloads": { + "message": "Downloads", + "description": "Navbar item with label Downloads" } } diff --git a/website/i18n/es/code.json b/website/i18n/es/code.json index 14b2bbdc4..9735cf318 100644 --- a/website/i18n/es/code.json +++ b/website/i18n/es/code.json @@ -109,30 +109,14 @@ "message": "Grabador de pantalla y editor de video gratis y de código abierto" }, "home.meta.description": { - "message": "OpenScreen, grabador de pantalla y editor de video gratis y de código abierto (Windows, macOS, Linux): captura nativa, subtítulos locales, sin marca de agua." - }, - "home.hero.badge.new": { - "message": "NUEVO" - }, - "home.hero.badge.text": { - "message": "1.11 exporta más rápido en macOS y Linux", - "description": "Links to an English-only blog post. Must fit on one line on a 375px phone." - }, - "home.hero.titleTagline": { - "message": "Un grabador de pantalla y editor de video gratis y de código abierto" + "message": "Convierte tus grabaciones de pantalla en demos cuidadas con zooms automáticos, fondos personalizados y un cursor editable. Gratis y de código abierto para Windows, macOS y Linux. Sin marca de agua." }, "home.hero.tagline": { - "message": "Grabación de pantalla con captura nativa, IA local y sin muro de pago." + "message": "Zooms automáticos, un cursor fluido y un marco cuidado. Graba, ajusta y comparte." }, "home.hero.download": { "message": "Descargar" }, - "home.hero.readDocs": { - "message": "Leer la documentación" - }, - "home.hero.scrollHint": { - "message": "Desplázate hacia abajo" - }, "home.features.kicker": { "message": "Además" }, @@ -1060,5 +1044,42 @@ "demoLoop.title.ask-the-agent": { "message": "Pedir una edición al agente del editor", "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "home.hero.title": { + "message": "Demos atractivas a la primera." + }, + "home.hero.reassurance": { + "message": "Gratis y de código abierto. Sin marca de agua. Sin cuenta." + }, + "home.hero.explore": { + "message": "Desplázate para explorar el editor" + }, + "recreation.pause": { + "message": "Pausar la animación" + }, + "recreation.play": { + "message": "Reproducir la animación" + }, + "announcement.v2.title": { + "message": "OpenScreen v2 ya está aquí" + }, + "announcement.v2.link": { + "message": "Ver las novedades (en inglés)", + "description": "Links to an English-only blog post. Mention English in translated labels." + }, + "download.cta.mac": { + "message": "Descargar para Mac" + }, + "download.cta.windows": { + "message": "Descargar para Windows" + }, + "download.cta.linux": { + "message": "Descargar para Linux" + }, + "download.cta.chooseMac": { + "message": "Elige tu Mac" + }, + "download.cta.otherVersions": { + "message": "Otras versiones" } } diff --git a/website/i18n/es/docusaurus-theme-classic/navbar.json b/website/i18n/es/docusaurus-theme-classic/navbar.json index ae2a9a232..a48baacc3 100644 --- a/website/i18n/es/docusaurus-theme-classic/navbar.json +++ b/website/i18n/es/docusaurus-theme-classic/navbar.json @@ -23,8 +23,8 @@ "message": "Discord", "description": "Navbar item with label Discord" }, - "item.label.Download": { - "message": "Descargar", - "description": "Navbar item with label Download" + "item.label.Downloads": { + "message": "Descargas", + "description": "Navbar item with label Downloads" } } diff --git a/website/i18n/fr/code.json b/website/i18n/fr/code.json index 10edea176..d7c118fc9 100644 --- a/website/i18n/fr/code.json +++ b/website/i18n/fr/code.json @@ -109,30 +109,14 @@ "message": "Enregistreur d'écran gratuit et open source avec éditeur vidéo" }, "home.meta.description": { - "message": "OpenScreen, enregistreur d'écran et éditeur vidéo gratuit et open source pour Windows, macOS et Linux : capture native, sous-titres en local, sans filigrane." - }, - "home.hero.badge.new": { - "message": "NOUVEAU" - }, - "home.hero.badge.text": { - "message": "La 1.11 exporte plus vite (Mac, Linux)", - "description": "Links to an English-only blog post. Must fit on one line on a 375px phone." - }, - "home.hero.titleTagline": { - "message": "Un enregistreur d'écran et éditeur vidéo gratuit et open source" + "message": "Transformez vos enregistrements en démos soignées avec des zooms automatiques, des fonds personnalisés et un curseur modifiable. Gratuit et open source pour Windows, macOS et Linux. Sans filigrane." }, "home.hero.tagline": { - "message": "Enregistrement d'écran avec capture native et IA locale, sans fonction payante." + "message": "Des zooms automatiques, un curseur fluide et un cadre soigné. Enregistrez, ajustez, partagez." }, "home.hero.download": { "message": "Télécharger" }, - "home.hero.readDocs": { - "message": "Lire la documentation" - }, - "home.hero.scrollHint": { - "message": "Faites défiler" - }, "home.features.kicker": { "message": "Tout aussi vrai" }, @@ -1060,5 +1044,42 @@ "demoLoop.title.ask-the-agent": { "message": "Demander une modification à l'agent de l'éditeur", "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "home.hero.title": { + "message": "De belles démos du premier coup." + }, + "home.hero.reassurance": { + "message": "Gratuit et open source. Sans filigrane. Sans compte." + }, + "home.hero.explore": { + "message": "Faites défiler pour découvrir l’éditeur" + }, + "recreation.pause": { + "message": "Mettre l’animation en pause" + }, + "recreation.play": { + "message": "Lire l’animation" + }, + "announcement.v2.title": { + "message": "OpenScreen v2 est là" + }, + "announcement.v2.link": { + "message": "Les nouveautés (en anglais)", + "description": "Links to an English-only blog post. Mention English in translated labels." + }, + "download.cta.mac": { + "message": "Télécharger pour Mac" + }, + "download.cta.windows": { + "message": "Télécharger pour Windows" + }, + "download.cta.linux": { + "message": "Télécharger pour Linux" + }, + "download.cta.chooseMac": { + "message": "Choisir votre Mac" + }, + "download.cta.otherVersions": { + "message": "Autres versions" } } diff --git a/website/i18n/fr/docusaurus-theme-classic/navbar.json b/website/i18n/fr/docusaurus-theme-classic/navbar.json index 0bf8c13d7..edf1122a5 100644 --- a/website/i18n/fr/docusaurus-theme-classic/navbar.json +++ b/website/i18n/fr/docusaurus-theme-classic/navbar.json @@ -23,8 +23,8 @@ "message": "Discord", "description": "Navbar item with label Discord" }, - "item.label.Download": { - "message": "Télécharger", - "description": "Navbar item with label Download" + "item.label.Downloads": { + "message": "Téléchargements", + "description": "Navbar item with label Downloads" } } diff --git a/website/i18n/ja/code.json b/website/i18n/ja/code.json index 04e120608..7ca5e1f07 100644 --- a/website/i18n/ja/code.json +++ b/website/i18n/ja/code.json @@ -109,30 +109,14 @@ "message": "無料・オープンソースの画面録画・動画編集ソフト" }, "home.meta.description": { - "message": "OpenScreen は Windows・macOS・Linux に対応した、無料・オープンソースの画面録画・動画編集ソフトです。OS ネイティブのキャプチャ、端末上で生成する字幕、透かしなし。" - }, - "home.hero.badge.new": { - "message": "NEW" - }, - "home.hero.badge.text": { - "message": "1.11:macOS・Linux でエクスポート高速化", - "description": "Links to an English-only blog post. Must fit on one line on a 375px phone." - }, - "home.hero.titleTagline": { - "message": "無料・オープンソースの画面録画・動画編集ソフト" + "message": "画面録画を、見栄えのよいデモ動画に。自動ズーム、背景のカスタマイズ、カーソル編集に対応。Windows、macOS、Linuxで使える無料のオープンソース。透かしなし。" }, "home.hero.tagline": { - "message": "ネイティブキャプチャ、ローカル AI、課金不要の画面録画。" + "message": "自動ズーム、なめらかなカーソル、見栄えのよいフレーム。録画して、調整して、共有しましょう。" }, "home.hero.download": { "message": "ダウンロード" }, - "home.hero.readDocs": { - "message": "ドキュメントを読む" - }, - "home.hero.scrollHint": { - "message": "下にスクロール" - }, "home.features.kicker": { "message": "さらに" }, @@ -1060,5 +1044,42 @@ "demoLoop.title.ask-the-agent": { "message": "エディターのエージェントに編集を依頼", "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "home.hero.title": { + "message": "一度の録画で、美しいデモに。" + }, + "home.hero.reassurance": { + "message": "無料でオープンソース。透かしなし。アカウント不要。" + }, + "home.hero.explore": { + "message": "スクロールしてエディターを見る" + }, + "recreation.pause": { + "message": "アニメーションを一時停止" + }, + "recreation.play": { + "message": "アニメーションを再生" + }, + "announcement.v2.title": { + "message": "OpenScreen v2が登場" + }, + "announcement.v2.link": { + "message": "新機能を見る(英語)", + "description": "Links to an English-only blog post. Mention English in translated labels." + }, + "download.cta.mac": { + "message": "Mac用をダウンロード" + }, + "download.cta.windows": { + "message": "Windows用をダウンロード" + }, + "download.cta.linux": { + "message": "Linux用をダウンロード" + }, + "download.cta.chooseMac": { + "message": "Macの種類を選択" + }, + "download.cta.otherVersions": { + "message": "その他のバージョン" } } diff --git a/website/i18n/ja/docusaurus-theme-classic/navbar.json b/website/i18n/ja/docusaurus-theme-classic/navbar.json index 59be03582..f25346e86 100644 --- a/website/i18n/ja/docusaurus-theme-classic/navbar.json +++ b/website/i18n/ja/docusaurus-theme-classic/navbar.json @@ -23,8 +23,8 @@ "message": "Discord", "description": "Navbar item with label Discord" }, - "item.label.Download": { + "item.label.Downloads": { "message": "ダウンロード", - "description": "Navbar item with label Download" + "description": "Navbar item with label Downloads" } } diff --git a/website/i18n/pt-BR/code.json b/website/i18n/pt-BR/code.json index 794350748..a58628762 100644 --- a/website/i18n/pt-BR/code.json +++ b/website/i18n/pt-BR/code.json @@ -109,30 +109,14 @@ "message": "Gravador de tela e editor de vídeo grátis e de código aberto" }, "home.meta.description": { - "message": "OpenScreen: gravador de tela e editor de vídeo grátis e de código aberto para Windows, macOS e Linux. Captura nativa, legendas locais, sem marca d'água." - }, - "home.hero.badge.new": { - "message": "NOVO" - }, - "home.hero.badge.text": { - "message": "1.11 exporta mais rápido (macOS, Linux)", - "description": "Links to an English-only blog post. Must fit on one line on a 375px phone." - }, - "home.hero.titleTagline": { - "message": "Gravador de tela e editor de vídeo grátis e de código aberto" + "message": "Transforme gravações de tela em demos bem-acabadas com zooms automáticos, fundos personalizados e um cursor editável. Grátis e de código aberto para Windows, macOS e Linux. Sem marca-d’água." }, "home.hero.tagline": { - "message": "Gravação de tela com captura nativa, IA local e sem paywall." + "message": "Zooms automáticos, um cursor suave e uma moldura bem-acabada. Grave, ajuste e compartilhe." }, "home.hero.download": { "message": "Baixar" }, - "home.hero.readDocs": { - "message": "Ler a documentação" - }, - "home.hero.scrollHint": { - "message": "Role para baixo" - }, "home.features.kicker": { "message": "Também é verdade" }, @@ -1060,5 +1044,42 @@ "demoLoop.title.ask-the-agent": { "message": "Pedir uma edição ao agente do editor", "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "home.hero.title": { + "message": "Belas demos logo de primeira." + }, + "home.hero.reassurance": { + "message": "Grátis e de código aberto. Sem marca-d’água. Sem conta." + }, + "home.hero.explore": { + "message": "Role para explorar o editor" + }, + "recreation.pause": { + "message": "Pausar animação" + }, + "recreation.play": { + "message": "Reproduzir animação" + }, + "announcement.v2.title": { + "message": "OpenScreen v2 chegou" + }, + "announcement.v2.link": { + "message": "Veja as novidades (em inglês)", + "description": "Links to an English-only blog post. Mention English in translated labels." + }, + "download.cta.mac": { + "message": "Baixar para Mac" + }, + "download.cta.windows": { + "message": "Baixar para Windows" + }, + "download.cta.linux": { + "message": "Baixar para Linux" + }, + "download.cta.chooseMac": { + "message": "Escolha seu Mac" + }, + "download.cta.otherVersions": { + "message": "Outras versões" } } diff --git a/website/i18n/pt-BR/docusaurus-theme-classic/navbar.json b/website/i18n/pt-BR/docusaurus-theme-classic/navbar.json index 9d3984357..696322c45 100644 --- a/website/i18n/pt-BR/docusaurus-theme-classic/navbar.json +++ b/website/i18n/pt-BR/docusaurus-theme-classic/navbar.json @@ -23,8 +23,8 @@ "message": "Discord", "description": "Navbar item with label Discord" }, - "item.label.Download": { - "message": "Baixar", - "description": "Navbar item with label Download" + "item.label.Downloads": { + "message": "Downloads", + "description": "Navbar item with label Downloads" } } diff --git a/website/i18n/zh-CN/code.json b/website/i18n/zh-CN/code.json index c10117b85..c858952db 100644 --- a/website/i18n/zh-CN/code.json +++ b/website/i18n/zh-CN/code.json @@ -122,30 +122,14 @@ "message": "免费开源的录屏与视频剪辑软件" }, "home.meta.description": { - "message": "OpenScreen 是一款免费、开源的录屏与视频剪辑软件,支持 Windows、macOS 和 Linux:采用系统原生屏幕采集,字幕在本机生成,导出视频无水印。" - }, - "home.hero.badge.new": { - "message": "新" - }, - "home.hero.badge.text": { - "message": "1.11:macOS 和 Linux 导出更快", - "description": "Links to an English-only blog post. Must fit on one line on a 375px phone." - }, - "home.hero.titleTagline": { - "message": "免费、开源的录屏与视频剪辑软件" + "message": "把屏幕录制变成精美的演示视频:自动缩放、自定义背景、可编辑光标。适用于 Windows、macOS 和 Linux,免费开源,无水印。" }, "home.hero.tagline": { - "message": "原生采集、本地 AI、没有付费墙的屏幕录制。" + "message": "自动缩放、流畅光标和精美边框。录制、调整、分享。" }, "home.hero.download": { "message": "下载" }, - "home.hero.readDocs": { - "message": "阅读文档" - }, - "home.hero.scrollHint": { - "message": "向下滚动" - }, "home.features.kicker": { "message": "此外" }, @@ -1060,5 +1044,42 @@ "demoLoop.title.ask-the-agent": { "message": "请编辑器的代理完成修改", "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "home.hero.title": { + "message": "一次录制,就有精美的演示。" + }, + "home.hero.reassurance": { + "message": "免费开源。无水印。无需账号。" + }, + "home.hero.explore": { + "message": "滚动以探索编辑器" + }, + "recreation.pause": { + "message": "暂停动画" + }, + "recreation.play": { + "message": "播放动画" + }, + "announcement.v2.title": { + "message": "OpenScreen v2 来了" + }, + "announcement.v2.link": { + "message": "了解新功能(英语)", + "description": "Links to an English-only blog post. Mention English in translated labels." + }, + "download.cta.mac": { + "message": "下载 Mac 版" + }, + "download.cta.windows": { + "message": "下载 Windows 版" + }, + "download.cta.linux": { + "message": "下载 Linux 版" + }, + "download.cta.chooseMac": { + "message": "选择你的 Mac" + }, + "download.cta.otherVersions": { + "message": "其他版本" } } diff --git a/website/i18n/zh-CN/docusaurus-theme-classic/navbar.json b/website/i18n/zh-CN/docusaurus-theme-classic/navbar.json index 5886ca40e..6d0904217 100644 --- a/website/i18n/zh-CN/docusaurus-theme-classic/navbar.json +++ b/website/i18n/zh-CN/docusaurus-theme-classic/navbar.json @@ -23,8 +23,8 @@ "message": "Discord", "description": "Navbar item with label Discord" }, - "item.label.Download": { + "item.label.Downloads": { "message": "下载", - "description": "Navbar item with label Download" + "description": "Navbar item with label Downloads" } } diff --git a/website/i18n/zh-TW/code.json b/website/i18n/zh-TW/code.json index d5e508073..128f1472f 100644 --- a/website/i18n/zh-TW/code.json +++ b/website/i18n/zh-TW/code.json @@ -122,30 +122,14 @@ "message": "免費開源的螢幕錄影與影片剪輯軟體" }, "home.meta.description": { - "message": "OpenScreen 是一款免費、開源的螢幕錄影與影片剪輯軟體,適用於 Windows、macOS 與 Linux 電腦:使用系統原生的螢幕擷取,字幕直接在你的裝置上產生,匯出的影片也沒有浮水印。" - }, - "home.hero.badge.new": { - "message": "最新" - }, - "home.hero.badge.text": { - "message": "1.11 在 macOS 與 Linux 上匯出更快", - "description": "Links to an English-only blog post. Must fit on one line on a 375px phone." - }, - "home.hero.titleTagline": { - "message": "免費開源的螢幕錄影與影片剪輯軟體" + "message": "把螢幕錄製變成精美的展示影片:自動縮放、自訂背景、可編輯游標。適用於 Windows、macOS 和 Linux,免費開源,無浮水印。" }, "home.hero.tagline": { - "message": "原生擷取、本機 AI、沒有付費牆的螢幕錄影。" + "message": "自動縮放、流暢游標和精美外框。錄製、調整、分享。" }, "home.hero.download": { "message": "下載" }, - "home.hero.readDocs": { - "message": "閱讀文件" - }, - "home.hero.scrollHint": { - "message": "向下捲動" - }, "home.features.kicker": { "message": "此外" }, @@ -1060,5 +1044,42 @@ "demoLoop.title.ask-the-agent": { "message": "請編輯器的代理完成修改", "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "home.hero.title": { + "message": "一次錄製,就有精美的示範。" + }, + "home.hero.reassurance": { + "message": "免費開源。無浮水印。無須帳號。" + }, + "home.hero.explore": { + "message": "捲動以探索編輯器" + }, + "recreation.pause": { + "message": "暫停動畫" + }, + "recreation.play": { + "message": "播放動畫" + }, + "announcement.v2.title": { + "message": "OpenScreen v2 登場" + }, + "announcement.v2.link": { + "message": "了解新功能(英文)", + "description": "Links to an English-only blog post. Mention English in translated labels." + }, + "download.cta.mac": { + "message": "下載 Mac 版" + }, + "download.cta.windows": { + "message": "下載 Windows 版" + }, + "download.cta.linux": { + "message": "下載 Linux 版" + }, + "download.cta.chooseMac": { + "message": "選擇你的 Mac" + }, + "download.cta.otherVersions": { + "message": "其他版本" } } diff --git a/website/i18n/zh-TW/docusaurus-theme-classic/navbar.json b/website/i18n/zh-TW/docusaurus-theme-classic/navbar.json index 84c8e32cd..04a1d5556 100644 --- a/website/i18n/zh-TW/docusaurus-theme-classic/navbar.json +++ b/website/i18n/zh-TW/docusaurus-theme-classic/navbar.json @@ -23,8 +23,8 @@ "message": "Discord", "description": "Navbar item with label Discord" }, - "item.label.Download": { + "item.label.Downloads": { "message": "下載", - "description": "Navbar item with label Download" + "description": "Navbar item with label Downloads" } } diff --git a/website/media/README.md b/website/media/README.md index cb864848d..c00cf7a67 100644 --- a/website/media/README.md +++ b/website/media/README.md @@ -33,3 +33,24 @@ off. On the `getopenscreen.com` zone: a cache rule keeping the loops at the edge for a year with query strings out of the cache key, a WAF rule that serves only GET/HEAD under `/loops/` with no query string, a 120 requests / 10 s per-IP rate limit, and billing alerts at any spend and at half of the free tier. + +## Responsive backgrounds + +The live homepage uses original wallpapers from `public/wallpapers/`, cropped +to 16:9 and encoded offline in AVIF (quality 80, 4:4:4, 10-bit) and WebP +(quality 90), at 960, 1920 and 2560 pixels wide. `` selects one format +and size; the driver retains two image layers and decodes the next background +before fading it in. Small picker thumbnails and the emergency JPEG fallbacks +remain local. CDN transforms are unnecessary. + +1. `node scripts/media/encode-backgrounds.mjs --dir ` +2. `node scripts/media/publish-backgrounds.mjs --cut --dir ` +3. `npm run check:backgrounds -- --live`, then commit `backgrounds.json` and + `src/lib/background-media.ts`. The manifest records source hashes, encoder + settings, dimensions, byte sizes and output hashes. Each file is capped at + 120 KB. Never rewrite a published cut or request it before uploading. + +Backgrounds live under `/loops/backgrounds//`, reusing the custom domain's +existing WAF and year-long cache rules for GET/HEAD under `/loops/` without +query strings. The bucket, rate limit and billing alerts are shared; no new +Cloudflare product or cache/WAF rule is required. `masters/` remains private. diff --git a/website/media/backgrounds.json b/website/media/backgrounds.json new file mode 100644 index 000000000..6aec2e09b --- /dev/null +++ b/website/media/backgrounds.json @@ -0,0 +1,182 @@ +{ + "cut": "2026-10a", + "base": "https://media.getopenscreen.com/loops/backgrounds/2026-10a", + "sources": [ + { + "path": "public/wallpapers/wallpaper2.jpg", + "sha256": "0a841d3d70b0282706abc764e40fb19b1468c39adc8b1b7a7390d279ac899c06" + }, + { + "path": "public/wallpapers/wallpaper5.jpg", + "sha256": "1a24ef74b68cd68c57421352223cd3ee2c8e24cce815c94c116ee6343e2a9429" + }, + { + "path": "public/wallpapers/wallpaper8.jpg", + "sha256": "1310c9d66385e9ecb41309e186c2f83dfb612a9c3ee2fccb59cfb4d3e853d90c" + }, + { + "path": "public/wallpapers/wallpaper11.jpg", + "sha256": "8224024049d8044878020e50447c2974b855ee3fc8d69bee5c95a0d652833dfb" + } + ], + "encoder": { + "sharp": "0.35.4", + "avif": { + "quality": 80, + "effort": 6, + "chromaSubsampling": "4:4:4", + "bitdepth": 10 + }, + "webp": { + "quality": 90, + "effort": 6, + "smartSubsample": true + } + }, + "files": { + "bg-1-960.avif": { + "bytes": 13892, + "width": 960, + "height": 540, + "sha256": "db8645812a9fdac76b1113a15114ba3f3a699ed567d842c71aa40d44241e10c6" + }, + "bg-1-960.webp": { + "bytes": 20436, + "width": 960, + "height": 540, + "sha256": "c6368eda97c246723203cda2243bcb4a019fd3b1fa9334608a6ba473d0a93a65" + }, + "bg-1-1920.avif": { + "bytes": 32577, + "width": 1920, + "height": 1080, + "sha256": "6d52c6acf9a795f983ad4bd9f16073e6d2e0419cd8e039358e695a1a98f983ec" + }, + "bg-1-1920.webp": { + "bytes": 48794, + "width": 1920, + "height": 1080, + "sha256": "8b03b985c8506458cf029f33c0b27fb12132b0879bef3b8e11eb2428c75acc04" + }, + "bg-1-2560.avif": { + "bytes": 48725, + "width": 2560, + "height": 1440, + "sha256": "be54006d49b90398650cb5979d4512e20bef752b396ca74ac6fda71f7891ef69" + }, + "bg-1-2560.webp": { + "bytes": 71392, + "width": 2560, + "height": 1440, + "sha256": "b839687d148b38c7e6206cdf21827a98ff8c26d68b5c3f54ce04b3b81e37fd2d" + }, + "bg-2-960.avif": { + "bytes": 11465, + "width": 960, + "height": 540, + "sha256": "48820969f49f3ee547d21ab520ae36c3926918678bd12dfc4c6f4ea11079f321" + }, + "bg-2-960.webp": { + "bytes": 18902, + "width": 960, + "height": 540, + "sha256": "7491c80af7d0f434dc0ff7841909727e5a7803f95bdb6b1e5f85d21fecf1f3e5" + }, + "bg-2-1920.avif": { + "bytes": 46312, + "width": 1920, + "height": 1080, + "sha256": "7942f65f4bd24757c1cb1735cceaa10651f5c979df68b3fda9c3d13ff8073867" + }, + "bg-2-1920.webp": { + "bytes": 55880, + "width": 1920, + "height": 1080, + "sha256": "59b5c66401332740def4fc6efbbc4f301ac1c09f0fc8bf003abde08edaf31b24" + }, + "bg-2-2560.avif": { + "bytes": 92043, + "width": 2560, + "height": 1440, + "sha256": "e73772de7c0a6ac850764958580eb3d794c6e47e8d2422a79bf149d18c96e4f3" + }, + "bg-2-2560.webp": { + "bytes": 91988, + "width": 2560, + "height": 1440, + "sha256": "4dbde134e0c6e094939b25b07790b436976e90712021306a364f77b399fdb459" + }, + "bg-3-960.avif": { + "bytes": 11708, + "width": 960, + "height": 540, + "sha256": "203b231ebde26eb8469a8b0ccd2800902704934412dfcbbbbe6501ecf3bf12df" + }, + "bg-3-960.webp": { + "bytes": 17022, + "width": 960, + "height": 540, + "sha256": "998861eb9e4a9cce00e7043dcf22842e4812eb05d900d627fdbb6f9543178007" + }, + "bg-3-1920.avif": { + "bytes": 35963, + "width": 1920, + "height": 1080, + "sha256": "afdcb77d2ff634c32dd5076a4daa9d58769ef356dbb304fb0e16c63ee0e979a3" + }, + "bg-3-1920.webp": { + "bytes": 46942, + "width": 1920, + "height": 1080, + "sha256": "02163a3eea63a7e66901bd7c04af4ba2fb5a4c1c5cd354777c973074e9e7c596" + }, + "bg-3-2560.avif": { + "bytes": 66082, + "width": 2560, + "height": 1440, + "sha256": "480a05d1b89712d334598de88840c951e524692fa72b074840de8e146c262aac" + }, + "bg-3-2560.webp": { + "bytes": 69668, + "width": 2560, + "height": 1440, + "sha256": "d09ef5ed9dbc2027f3f08b072cc081bc7c6610a8a88fbad99d53a83bacee4734" + }, + "bg-4-960.avif": { + "bytes": 16401, + "width": 960, + "height": 540, + "sha256": "88faa4f459e9c18f2164f418043ae7f5398617a150088838f22bcc8d82cb922c" + }, + "bg-4-960.webp": { + "bytes": 25330, + "width": 960, + "height": 540, + "sha256": "f61bb6fbe5254cce4e86bbd3b9cfebdd2d96856f5bdd4d78c1ceed53191ee267" + }, + "bg-4-1920.avif": { + "bytes": 39291, + "width": 1920, + "height": 1080, + "sha256": "8b83a3ccd6f6b45480e080307e1a31b7e197639f7ca36aa2c3a73af6ecb129f5" + }, + "bg-4-1920.webp": { + "bytes": 62498, + "width": 1920, + "height": 1080, + "sha256": "531f87d4afd6753217439616e86d65b4f5ea0a1e37af156f63a2d34a4bf8dab8" + }, + "bg-4-2560.avif": { + "bytes": 57882, + "width": 2560, + "height": 1440, + "sha256": "875843b37a7fb3e2d3c491bf9838689e6f9ca0ad322c9cf55090673951bd0bcf" + }, + "bg-4-2560.webp": { + "bytes": 89628, + "width": 2560, + "height": 1440, + "sha256": "7bb94eeaeae99b4df980b201d979f752b851dadb9eaf3d61421ead2c77db6005" + } + } +} diff --git a/website/package-lock.json b/website/package-lock.json index 4b13dc1c2..99e29127c 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -24,6 +24,7 @@ "@docusaurus/module-type-aliases": "~3.10.1", "@docusaurus/tsconfig": "~3.10.1", "@docusaurus/types": "~3.10.1", + "sharp": "^0.35.4", "typescript": "~5.6.2" }, "engines": { @@ -4182,6 +4183,533 @@ "@hapi/hoek": "^9.0.0" } }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", + "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", + "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", + "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", + "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", + "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", + "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", + "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", + "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", + "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", + "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", + "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", + "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", + "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", + "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", + "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", + "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", + "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", + "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", + "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", + "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", + "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", + "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", + "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", + "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", + "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", + "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -17527,6 +18055,56 @@ "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", "license": "MIT" }, + "node_modules/sharp": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", + "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.4", + "@img/sharp-darwin-x64": "0.35.4", + "@img/sharp-freebsd-wasm32": "0.35.4", + "@img/sharp-libvips-darwin-arm64": "1.3.3", + "@img/sharp-libvips-darwin-x64": "1.3.3", + "@img/sharp-libvips-linux-arm": "1.3.3", + "@img/sharp-libvips-linux-arm64": "1.3.3", + "@img/sharp-libvips-linux-ppc64": "1.3.3", + "@img/sharp-libvips-linux-riscv64": "1.3.3", + "@img/sharp-libvips-linux-s390x": "1.3.3", + "@img/sharp-libvips-linux-x64": "1.3.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", + "@img/sharp-libvips-linuxmusl-x64": "1.3.3", + "@img/sharp-linux-arm": "0.35.4", + "@img/sharp-linux-arm64": "0.35.4", + "@img/sharp-linux-ppc64": "0.35.4", + "@img/sharp-linux-riscv64": "0.35.4", + "@img/sharp-linux-s390x": "0.35.4", + "@img/sharp-linux-x64": "0.35.4", + "@img/sharp-linuxmusl-arm64": "0.35.4", + "@img/sharp-linuxmusl-x64": "0.35.4", + "@img/sharp-webcontainers-wasm32": "0.35.4", + "@img/sharp-win32-arm64": "0.35.4", + "@img/sharp-win32-ia32": "0.35.4", + "@img/sharp-win32-x64": "0.35.4" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/website/package.json b/website/package.json index 8e0a4fccb..01d46f3a4 100644 --- a/website/package.json +++ b/website/package.json @@ -14,7 +14,8 @@ "i18n:check": "node scripts/i18n.mjs check", "i18n:sync": "node scripts/i18n.mjs sync", "test": "node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --test \"src/**/*.test.ts\"", - "check:loops": "node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON scripts/check-loops.mjs" + "check:loops": "node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON scripts/check-loops.mjs", + "check:backgrounds": "node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON scripts/check-backgrounds.mjs" }, "dependencies": { "@docusaurus/core": "~3.10.1", @@ -33,6 +34,7 @@ "@docusaurus/module-type-aliases": "~3.10.1", "@docusaurus/tsconfig": "~3.10.1", "@docusaurus/types": "~3.10.1", + "sharp": "^0.35.4", "typescript": "~5.6.2" }, "browserslist": { diff --git a/website/scripts/check-backgrounds.mjs b/website/scripts/check-backgrounds.mjs new file mode 100644 index 000000000..615f99b08 --- /dev/null +++ b/website/scripts/check-backgrounds.mjs @@ -0,0 +1,68 @@ +#!/usr/bin/env node +/** Hold the responsive wallpaper URLs to their immutable publication manifest. */ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { + BACKGROUND_BASE, + BACKGROUND_WALLPAPERS, + BACKGROUND_WIDTHS, +} from "../src/lib/background-media.ts"; + +const manifest = JSON.parse( + readFileSync(resolve(import.meta.dirname, "../media/backgrounds.json"), "utf8"), +); +const live = process.argv.includes("--live"); +const problems = []; +if (manifest.base !== BACKGROUND_BASE) + problems.push("background manifest and code point at different cuts"); +const names = BACKGROUND_WALLPAPERS.flatMap((_, index) => + BACKGROUND_WIDTHS.flatMap((width) => + ["avif", "webp"].map((format) => `bg-${index + 1}-${width}.${format}`), + ), +); +if (Object.keys(manifest.files).length !== names.length) + problems.push("unexpected number of variants"); +for (const name of names) { + const file = manifest.files[name]; + const width = Number(name.match(/-(\d+)\./)[1]); + if ( + !file || + file.width !== width || + file.height !== Math.round((width * 9) / 16) || + file.bytes <= 0 || + file.bytes > 120_000 || + !/^[a-f0-9]{64}$/.test(file.sha256) + ) { + problems.push(`${name}: missing or invalid publication record`); + } +} +if (live && problems.length === 0) { + // Share the video's conservative pacing, below the zone's 120 requests / 10 s. + for (let offset = 0; offset < names.length; offset += 6) { + if (offset) await new Promise((resolve) => setTimeout(resolve, 750)); + await Promise.all( + names.slice(offset, offset + 6).map(async (name) => { + try { + const response = await fetch(`${BACKGROUND_BASE}/${name}`, { + method: "HEAD", + signal: AbortSignal.timeout(15_000), + }); + if (!response.ok) problems.push(`${name}: HTTP ${response.status}`); + else if ( + Number(response.headers.get("content-length")) !== manifest.files[name].bytes || + response.headers.get("content-type")?.split(";")[0] !== + `image/${name.split(".").at(-1)}` + ) { + problems.push(`${name}: served size or format differs from its record`); + } + } catch (error) { + problems.push(`${name}: ${error.message}`); + } + }), + ); + } +} +if (problems.length) throw new Error(`Backgrounds: ${problems.join("\n")}`); +console.log( + `Backgrounds OK: ${names.length} variants${live ? ", all live at their recorded sizes" : ""}`, +); diff --git a/website/scripts/media/encode-backgrounds.mjs b/website/scripts/media/encode-backgrounds.mjs new file mode 100644 index 000000000..c35392de8 --- /dev/null +++ b/website/scripts/media/encode-backgrounds.mjs @@ -0,0 +1,32 @@ +#!/usr/bin/env node +/** node scripts/media/encode-backgrounds.mjs --dir ; originals stay in public/wallpapers. */ +import { mkdir, writeFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import { parseArgs } from "node:util"; +import sharp from "sharp"; +import { BACKGROUND_WALLPAPERS, BACKGROUND_WIDTHS } from "../../src/lib/background-media.ts"; + +const { values } = parseArgs({ options: { dir: { type: "string" } } }); +if (!values.dir) throw new Error("usage: encode-backgrounds.mjs --dir "); +const dir = resolve(values.dir); +await mkdir(dir, { recursive: true }); +const root = resolve(import.meta.dirname, "../../.."); +// Run sequentially: AV1 encoding is an offline task, not a reason to monopolise the developer's CPU. +for (const [index, wallpaper] of BACKGROUND_WALLPAPERS.entries()) { + for (const width of BACKGROUND_WIDTHS) { + const source = resolve(root, "public/wallpapers", `wallpaper${wallpaper}.jpg`); + const resized = sharp(source).resize(width, Math.round((width * 9) / 16), { fit: "cover" }); + for (const format of ["avif", "webp"]) { + const image = resized.clone(); + const output = + format === "avif" + ? await image + .avif({ quality: 80, effort: 6, chromaSubsampling: "4:4:4", bitdepth: 10 }) + .toBuffer() + : await image.webp({ quality: 90, effort: 6, smartSubsample: true }).toBuffer(); + const name = `bg-${index + 1}-${width}.${format}`; + await writeFile(resolve(dir, name), output); + console.log(`${name}: ${output.length} B`); + } + } +} diff --git a/website/scripts/media/publish-backgrounds.mjs b/website/scripts/media/publish-backgrounds.mjs new file mode 100644 index 000000000..1bec59705 --- /dev/null +++ b/website/scripts/media/publish-backgrounds.mjs @@ -0,0 +1,149 @@ +#!/usr/bin/env node +/** Immutable responsive wallpaper cut. Upload before requesting its public URLs. + * node scripts/media/publish-backgrounds.mjs --cut 2026-10a --dir + * --no-upload prepares the manifest offline; it does not publish any assets. + * CF_CLI can name the cf CLI's JS entry point when its executable is not on PATH. + */ +import { execFileSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { readdirSync, readFileSync, writeFileSync } from "node:fs"; +import { join, resolve } from "node:path"; +import { parseArgs } from "node:util"; +import sharp from "sharp"; + +import { BACKGROUND_WALLPAPERS, BACKGROUND_WIDTHS } from "../../src/lib/background-media.ts"; + +const { values } = parseArgs({ + options: { + cut: { type: "string" }, + dir: { type: "string" }, + "no-upload": { type: "boolean" }, + }, +}); +if (!values.dir || !values.cut || !/^[0-9a-z-]+$/.test(values.cut)) { + throw new Error( + "usage: publish-backgrounds.mjs --cut --dir [--no-upload]", + ); +} +const root = resolve(import.meta.dirname, "../.."); +const dir = resolve(values.dir); +// Reuse the existing public /loops/ namespace and its year-long edge cache. +const prefix = `loops/backgrounds/${values.cut}/`; +const names = BACKGROUND_WALLPAPERS.flatMap((_, index) => + BACKGROUND_WIDTHS.flatMap((width) => + ["avif", "webp"].map((format) => `bg-${index + 1}-${width}.${format}`), + ), +); +const actual = readdirSync(dir).filter((name) => /\.(avif|webp)$/.test(name)); +if (actual.length !== names.length || actual.some((name) => !names.includes(name))) { + throw new Error("Incomplete or unexpected background variants; nothing uploaded or written"); +} +const files = {}; +const hashes = {}; +for (const name of names) { + const bytes = readFileSync(join(dir, name)); + const image = await sharp(bytes).metadata(); + const width = Number(name.match(/-(\d+)\./)[1]); + if ( + image.width !== width || + image.height !== Math.round((width * 9) / 16) || + bytes.length > 120_000 + ) { + throw new Error(`${name}: unexpected dimensions or exceeds 120 KB`); + } + files[name] = { + bytes: bytes.length, + width: image.width, + height: image.height, + sha256: createHash("sha256").update(bytes).digest("hex"), + }; + hashes[name] = createHash("md5").update(bytes).digest("hex"); +} + +if (!values["no-upload"]) { + const cf = (args) => + execFileSync( + process.env.CF_CLI ? process.execPath : "cf", + process.env.CF_CLI ? [process.env.CF_CLI, ...args] : args, + { encoding: "utf8", maxBuffer: 1 << 24 }, + ); + const listed = JSON.parse( + cf([ + "r2", + "objects", + "list", + "--bucket-name", + "openscreen-media", + "--prefix", + prefix, + "--per-page", + "1000", + ]), + ); + if (!Array.isArray(listed) || listed.length >= 1000) + throw new Error("Unrecognized or truncated R2 listing"); + const remote = new Map(listed.map((object) => [object.key.slice(prefix.length), object])); + for (const [name, object] of remote) { + if (!files[name] || object.etag.replaceAll('"', "") !== hashes[name]) { + throw new Error(`${prefix} is already published with different bytes; use a new cut`); + } + } + for (const name of names) { + if (!remote.has(name)) { + cf([ + "r2", + "objects", + "put", + `${prefix}${name}`, + "--bucket-name", + "openscreen-media", + "--file", + join(dir, name), + "--content-type", + `image/${name.split(".").at(-1)}`, + "-q", + ]); + } + console.log(`${remote.has(name) ? "already there" : "uploaded"}: ${name}`); + } +} + +const base = `https://media.getopenscreen.com/${prefix.slice(0, -1)}`; +const sources = BACKGROUND_WALLPAPERS.map((wallpaper) => { + const path = `public/wallpapers/wallpaper${wallpaper}.jpg`; + return { + path, + sha256: createHash("sha256") + .update(readFileSync(resolve(root, "..", path))) + .digest("hex"), + }; +}); +writeFileSync( + resolve(root, "media/backgrounds.json"), + `${JSON.stringify( + { + cut: values.cut, + base, + sources, + encoder: { + sharp: sharp.versions.sharp, + avif: { quality: 80, effort: 6, chromaSubsampling: "4:4:4", bitdepth: 10 }, + webp: { quality: 90, effort: 6, smartSubsample: true }, + }, + files, + }, + null, + "\t", + )}\n`, +); +const libPath = resolve(root, "src/lib/background-media.ts"); +writeFileSync( + libPath, + readFileSync(libPath, "utf8").replace( + /export const BACKGROUND_BASE = "[^"]+";/, + `export const BACKGROUND_BASE = "${base}";`, + ), +); +console.log( + `${names.length} variants recorded${values["no-upload"] ? " offline; upload before using the CDN URLs" : " and published"}`, +); diff --git a/website/src/components/Editor/index.tsx b/website/src/components/Editor/index.tsx index 516410d7d..19740d6d5 100644 --- a/website/src/components/Editor/index.tsx +++ b/website/src/components/Editor/index.tsx @@ -1,5 +1,6 @@ /** - * The page's centre of gravity: nine viewports of editor, and nothing above it. + * The page's centre of gravity: a live preview that docks into nine viewports + * of editor as the reader scrolls. * * It sits immediately under the hero because it is the argument. Everything * after it is either a claim the editor cannot make on its own (the recorder, diff --git a/website/src/components/Editor/styles.module.css b/website/src/components/Editor/styles.module.css index 2e4213aec..b2288eb3a 100644 --- a/website/src/components/Editor/styles.module.css +++ b/website/src/components/Editor/styles.module.css @@ -11,10 +11,8 @@ viewport — so a section without a background sits on the wrong grey. */ background: var(--os-bg); color: var(--os-fg); - /* Down from 104px, which was the air a section heading needed. With the - heading gone the hero should hand over to the editor the way the design - has it: the demo begins as the hero leaves. */ - padding: 24px 0 8px; + /* The full-width live preview begins directly beneath the promise. */ + padding: 0 0 8px; } .head { diff --git a/website/src/components/PlatformDownload/PlatformIcon.tsx b/website/src/components/PlatformDownload/PlatformIcon.tsx new file mode 100644 index 000000000..5d38e825e --- /dev/null +++ b/website/src/components/PlatformDownload/PlatformIcon.tsx @@ -0,0 +1,20 @@ +import { Download } from "lucide-react"; + +import type { DownloadTarget } from "../../lib/download-target"; +// Apple and Tux paths from Simple Icons, distributed under CC0. +import AppleIcon from "./apple.svg"; +import LinuxIcon from "./linux.svg"; + +export default function PlatformIcon({ os }: { os: DownloadTarget["os"] }) { + if (os === "macos") + return