From 3b3959f5abecbd793665396dc0c46acde7132d04 Mon Sep 17 00:00:00 2001
From: "Tommy D. Rossi"
Date: Sat, 21 Feb 2026 15:22:39 +0100
Subject: [PATCH] website: add pixelated image placeholders with fade-in
transition
Build-time script generates tiny 32px-wide versions of all images in
public/ using sharp. At runtime, PixelatedImage component renders these
with CSS image-rendering: pixelated (nearest-neighbor / point sampling)
for a crisp mosaic effect, then fades in the real image on load.
- Auto-discovers images in public/, skips placeholder- prefixed files
- Mtime check skips regeneration when placeholder is newer than source
- Callback ref handles cached images where onLoad fires before mount
- Explicit z-index prevents placeholder from covering the real image
- Runs automatically as part of pnpm build in website package
---
pnpm-lock.yaml | 7 +-
website/package.json | 4 +-
website/public/placeholder-favicon-16.png | Bin 0 -> 1163 bytes
website/public/placeholder-favicon-32.png | Bin 0 -> 636 bytes
website/public/placeholder-og-image.png | Bin 0 -> 1158 bytes
website/public/placeholder-screenshot@2x.png | Bin 0 -> 380 bytes
website/scripts/generate-placeholders.ts | 67 ++++++++++++++
website/src/components/markdown.tsx | 90 ++++++++++++++++++-
website/src/routes/_index.tsx | 21 +++--
9 files changed, 179 insertions(+), 10 deletions(-)
create mode 100644 website/public/placeholder-favicon-16.png
create mode 100644 website/public/placeholder-favicon-32.png
create mode 100644 website/public/placeholder-og-image.png
create mode 100644 website/public/placeholder-screenshot@2x.png
create mode 100644 website/scripts/generate-placeholders.ts
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e34db31..1ca5341 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -491,6 +491,9 @@ importers:
'@xmorse/deployment-utils':
specifier: ^0.7.2
version: 0.7.2(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.20.6)(yaml@2.6.0)
+ sharp:
+ specifier: ^0.34.5
+ version: 0.34.5
tailwindcss:
specifier: ^4.2.0
version: 4.2.0
@@ -7856,8 +7859,7 @@ snapshots:
'@iarna/toml@2.2.5': {}
- '@img/colour@1.0.0':
- optional: true
+ '@img/colour@1.0.0': {}
'@img/sharp-darwin-arm64@0.34.5':
optionalDependencies:
@@ -12869,7 +12871,6 @@ snapshots:
'@img/sharp-win32-arm64': 0.34.5
'@img/sharp-win32-ia32': 0.34.5
'@img/sharp-win32-x64': 0.34.5
- optional: true
shebang-command@2.0.0:
dependencies:
diff --git a/website/package.json b/website/package.json
index d59bb83..7ebbd3f 100644
--- a/website/package.json
+++ b/website/package.json
@@ -3,7 +3,8 @@
"private": true,
"type": "module",
"scripts": {
- "build": "react-router build",
+ "placeholders": "tsx scripts/generate-placeholders.ts",
+ "build": "tsx scripts/generate-placeholders.ts && react-router build",
"dev": "react-router dev --port 8044",
"typecheck": "react-router typegen && tsc"
},
@@ -39,6 +40,7 @@
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.2",
"@xmorse/deployment-utils": "^0.7.2",
+ "sharp": "^0.34.5",
"tailwindcss": "^4.2.0",
"typescript": "^5.9.3",
"vite": "^7.3.0",
diff --git a/website/public/placeholder-favicon-16.png b/website/public/placeholder-favicon-16.png
new file mode 100644
index 0000000000000000000000000000000000000000..0d386bd5b8f978b4aa5394d940aaae2193780458
GIT binary patch
literal 1163
zcmV;61a$j}P){
zj~(oJ{?X9T@Vv3H@ue=VQ;Dm5RQaruRk`~^0DPBG!)cWdR&O14uxAYG2haGUs9xzy
zT^D<+b5iAtzWc6o9%Vkmp~ihu@&N%vPURc=&};R_uxCI(hxI(B@(I)^YHMqgDu#Yb
zoIcA6c4FA|nwy(nLyNuy*%vyAF$wWdL9&~
z--bEv=}*EY$QO_X0qAp4mPAAKi||ap`y&9@!5(%!TOw%^B`o%iJ|{fGFao$5a^V|x
zJzJ;LdM@ZWiw6bA9PWJG#%Cgt$(P($TDSD7)hoGx7nB;=
zPQpcvTrMYjdwViDIcY#+oKV&TfDVTuJlfpcG}#_C3Wb7UU~O$p#>U1B@M@hgvI>IL
zK$D>-92^{&MDx>*=NM;@Cr3v|va+&bzM%}Ga==wVXdehxLw9$#jE|3-ok=fbV
z@|GQS9!PQi=Lr7P({R{U>;40bm_s|BW8BYR*oi2?B}5;Fo(aQ@haEhEpxhBI82d+O
z!hdlpu5=iAbSnNA_ktjRiuZ#5elx0cH?()UT#a|byuD+F-wmT~$0UfoFJx-G9cS$=
d`(xPc_$Nbw(REquQ$hd$002ovPDHLkV1i3UA}jy^
literal 0
HcmV?d00001
diff --git a/website/public/placeholder-favicon-32.png b/website/public/placeholder-favicon-32.png
new file mode 100644
index 0000000000000000000000000000000000000000..425b1ad52df32547a3e60ccf1012ed8c3c5b1787
GIT binary patch
literal 636
zcmV-?0)zdDP)lOb8>&lWrae4y&%Jp<;Zb#IZTcnhs9xYI2vfvVW;CDA
z$uJD6)oN5M7LyXQW1?Y%3IMrhx7&>lhXbuvD;f@mRIOH@o_SM%#YEQ%6|h(=D99Yg
zq5XdU5ZG?Fsa!5ULlr}S&14mTlfmscxy)?arp;zUlgWgd%_fyfB~>8~ll69Nf|$eY
zs&+n~X}MfdqtQ?i;j$=|NI1kiu1%*?B`-t@Kp2p4h`FkrPNzp+Bq9KtAls2}sv#H*
z2Fl6ER2KQmG0d*lYb^3u0^m|)yUZ?^3-$Z`U-ExZ{q4JBmjBD9!S+fglFnX_j}-
z{Xn27NSaC6s2JEx>kkOVm^5clGncVdCMz?rjhGq|iNO63vuO~9iYYSJawgpKe4ovC
z@1vX951VVUlAv^ZAo@K}gLx3&
zJq0mT4)J3R#F=dn=XOJ!-w!c<2+9Q?6{L>*Cuy!^I5C;)L`Wh;i4&rd+CsIvp&TNB
zk03`2Ax@M+{oy63qcu>+nxOvlAtBlbaRA~%FBco20uh*-2hvlnO-V~Yf_pAdZnII@Z^6hREHfjC(S_4FH1&%Q;7_o1HO&4uXOl
zcyjnWg%>~iphVT>OSOL`UOCUy`hd8x?F4RLk_cq^t
z7nJcYpp1PEae61j*hh3`FBk8MIMW~5rm*}34N`x=X~yq!;i6T-FN^4e-H9u!oS1r!
zTT;i7xggU8`7kw@2l@DukVn=)K2-(zY%S!`M#x8N5$Y*N=wRWsdCJK$LNqYI$8@BV
zAU!+|JJcV6xZZ)eUR<81EAQ`da-IkHn#cZpTpglQW1ARoJJrsB-5mQOdGhD{(0rM;9Q*FJqkFa;-^{b2
z*SZt~OKkA1TngXQx$tkY!M}AG0^3(mD-mdt5U6n=P_+!fs$2wL(
zvmGO!N>GQ_Gr$hW6CDhGeUknq2lUA%nD0u3F3E({*|U&svtjOn1z4DwiHyZdke->1
zhtjh!Z@v}Q2Qx5p*1d>}H$Z1JL7$ietu6s+Gw;Eo#Sg=rl8Siaop>~B4&<&9D5K2`
ze(-nuY5hMYVXRgQU0giEV}f3fJ1iEM5{)n?CnLpThQ*u=GnHgYoW5o-BtWaZ9dWU-
z(+WRlG#EJ70)t+MTW`}M_x@B&?es!)y#V=4JCuuqH~1=o(C~-7a;5W_|OO*aZ2zIV%v2tX8ev3IWy=;C0rLde)F8R?-&cL@Pn|ltSz+hUh4S@|8DA
z0Ley#%2-j$Sy5kMMXhE&Y$inW6A#5zJ$GVn%jOC!%eW1s1X7J%M3-C-3w
z0Uo0Zc??)gJ@~07*qoM6N<$f&h~gy#N3J
literal 0
HcmV?d00001
diff --git a/website/public/placeholder-screenshot@2x.png b/website/public/placeholder-screenshot@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..fb654bc6dd3216f4ec15c77c9852ef3120a75e76
GIT binary patch
literal 380
zcmV-?0fYXDP)B#%tcllOL`eRJ`a!&i@ulj&aHyaoFu2%cx
zoDmor`sls;+>7lmPbx7p`@n8-RXbiFEpa+JVLU2lOEHOSI3^hdrT_o{32;bRa{vGf
z6951U69E94oEQKA0L4i}K~#9!ebLbtf*=qCP;kvqAq1tcvj6|gW<6qM;=Jg?Wne)R
zt(K)ys%rdDpZ_CH9}zA)WCn&s1>r&JEr2kVR {
+ if (name.startsWith(PLACEHOLDER_PREFIX)) {
+ return false;
+ }
+ const ext = path.extname(name).toLowerCase();
+ return IMAGE_EXTENSIONS.has(ext);
+ });
+
+ if (images.length === 0) {
+ console.error("No images found in public/");
+ return;
+ }
+
+ for (const name of images) {
+ const inputPath = path.join(PUBLIC_DIR, name);
+ const ext = path.extname(name);
+ const base = path.basename(name, ext);
+ const outputPath = path.join(PUBLIC_DIR, `${PLACEHOLDER_PREFIX}${base}${ext}`);
+
+ // Skip if placeholder already exists and is newer than source
+ if (fs.existsSync(outputPath)) {
+ const srcMtime = fs.statSync(inputPath).mtimeMs;
+ const outMtime = fs.statSync(outputPath).mtimeMs;
+ if (outMtime > srcMtime) {
+ continue;
+ }
+ }
+
+ await sharp(inputPath)
+ .resize(PLACEHOLDER_WIDTH)
+ .png({ compressionLevel: 9 })
+ .toFile(outputPath);
+
+ const stats = fs.statSync(outputPath);
+ console.error(
+ `Generated ${PLACEHOLDER_PREFIX}${base}${ext} (${PLACEHOLDER_WIDTH}px wide, ${stats.size} bytes)`,
+ );
+ }
+}
+
+main();
diff --git a/website/src/components/markdown.tsx b/website/src/components/markdown.tsx
index b993f51..02f4aee 100644
--- a/website/src/components/markdown.tsx
+++ b/website/src/components/markdown.tsx
@@ -6,7 +6,7 @@
* --link-accent, --page-border.
*/
-import { useEffect, useRef, useState } from "react";
+import { useCallback, useEffect, useRef, useState } from "react";
import Prism from "prismjs";
import "prismjs/components/prism-jsx";
import "prismjs/components/prism-tsx";
@@ -416,6 +416,94 @@ export function CodeBlock({ children, lang = "jsx", lineHeight = "1.85" }: { chi
);
}
+/* =========================================================================
+ Pixelated placeholder image
+ Uses a tiny pre-generated image with CSS image-rendering: pixelated
+ (nearest-neighbor / point sampling in GPU terms) for a crisp mosaic
+ effect. The real image fades in on top once loaded — no flash because
+ the placeholder stays underneath and the real image starts at opacity 0.
+ ========================================================================= */
+
+export function PixelatedImage({
+ src,
+ placeholder,
+ alt,
+ width,
+ height,
+ className = "",
+ style,
+}: {
+ src: string;
+ placeholder: string;
+ alt: string;
+ width: number;
+ height: number;
+ className?: string;
+ style?: React.CSSProperties;
+}) {
+ const [loaded, setLoaded] = useState(false);
+
+ // Handles both the normal onLoad event and the case where the image is
+ // already cached (img.complete is true before React mounts the handler).
+ const imgRef = useCallback((img: HTMLImageElement | null) => {
+ if (img?.complete && img.naturalWidth > 0) {
+ setLoaded(true);
+ }
+ }, []);
+
+ return (
+
+ {/* Placeholder: tiny image rendered with nearest-neighbor sampling */}
+

+ {/* Real image: starts invisible, fades in over the placeholder */}
+

{
+ setLoaded(true);
+ }}
+ style={{
+ position: "relative",
+ width: "100%",
+ height: "100%",
+ objectFit: "cover",
+ opacity: loaded ? 1 : 0,
+ transition: "opacity 0.4s ease",
+ zIndex: 1,
+ }}
+ />
+
+ );
+}
+
/* =========================================================================
Chart placeholder (dark box with animated line)
========================================================================= */
diff --git a/website/src/routes/_index.tsx b/website/src/routes/_index.tsx
index 049c8f2..8742067 100644
--- a/website/src/routes/_index.tsx
+++ b/website/src/routes/_index.tsx
@@ -18,6 +18,7 @@ import {
List,
OL,
Li,
+ PixelatedImage,
} from "website/src/components/markdown";
export const meta: MetaFunction = () => {
@@ -70,8 +71,9 @@ export default function IndexPage() {
-
- Three steps and your agent is browsing.
+ Four steps and your agent is browsing.
@@ -110,13 +112,22 @@ export default function IndexPage() {
Install the{" "}
Chrome extension
- - Click the extension icon on a tab — it turns green
- - Install CLI and run your first command:
+ - Click the extension icon on a tab {" \u2014 "} it turns green
+ - Install the CLI:
{dedent`
npm i -g playwriter
- playwriter -s 1 -e "await page.goto('https://example.com')"
+ `}
+
+
+ Then install the skill {" \u2014 "} it teaches your agent how to use
+ Playwriter: which selectors to use, how to avoid timeouts, how to
+ read snapshots, and all available utilities.
+
+
+
{dedent`
+ npx -y skills add remorses/playwriter
`}