diff --git a/landing/app/layout.tsx b/landing/app/layout.tsx index 0b2e6b10..ff11a8ed 100644 --- a/landing/app/layout.tsx +++ b/landing/app/layout.tsx @@ -20,9 +20,9 @@ export const metadata: Metadata = { siteName: "AgentCompany", locale: "en_US", type: "website", - images: [{ url: "/images/og.jpg", width: 1200, height: 630, alt: "The AgentCompany desktop app" }], + images: [{ url: "/images/og.png", width: 1200, height: 630, alt: "AgentCompany in 8-bit: run an autonomous AI company on your desktop" }], }, - twitter: { card: "summary_large_image", title, description, images: ["/images/og.jpg"] }, + twitter: { card: "summary_large_image", title, description, images: ["/images/og.png"] }, robots: { index: true, follow: true }, icons: { icon: [ diff --git a/landing/package.json b/landing/package.json index 4f51da08..f0c79774 100644 --- a/landing/package.json +++ b/landing/package.json @@ -9,7 +9,8 @@ "start": "next start", "preview": "wrangler pages dev out", "test:e2e": "next build && playwright test", - "test:unit": "node --import ./tests/unit/resolve-ts.mjs --test 'tests/unit/*.test.ts'" + "test:unit": "node --import ./tests/unit/resolve-ts.mjs --test 'tests/unit/*.test.ts'", + "og": "node scripts/og.mjs" }, "dependencies": { "next": "16.2.6", diff --git a/landing/public/images/ambient/connectors.webp b/landing/public/images/ambient/connectors.webp deleted file mode 100644 index 3f93f820..00000000 Binary files a/landing/public/images/ambient/connectors.webp and /dev/null differ diff --git a/landing/public/images/ambient/cost-controls.webp b/landing/public/images/ambient/cost-controls.webp deleted file mode 100644 index 9b251d9b..00000000 Binary files a/landing/public/images/ambient/cost-controls.webp and /dev/null differ diff --git a/landing/public/images/ambient/dashboard.webp b/landing/public/images/ambient/dashboard.webp deleted file mode 100644 index b23d57f4..00000000 Binary files a/landing/public/images/ambient/dashboard.webp and /dev/null differ diff --git a/landing/public/images/ambient/messages.webp b/landing/public/images/ambient/messages.webp deleted file mode 100644 index 0e2d4785..00000000 Binary files a/landing/public/images/ambient/messages.webp and /dev/null differ diff --git a/landing/public/images/ambient/office.webp b/landing/public/images/ambient/office.webp deleted file mode 100644 index b9f6bd46..00000000 Binary files a/landing/public/images/ambient/office.webp and /dev/null differ diff --git a/landing/public/images/ambient/org-chart.webp b/landing/public/images/ambient/org-chart.webp deleted file mode 100644 index 5cd63c87..00000000 Binary files a/landing/public/images/ambient/org-chart.webp and /dev/null differ diff --git a/landing/public/images/connectors.webp b/landing/public/images/connectors.webp deleted file mode 100644 index e6cf73fb..00000000 Binary files a/landing/public/images/connectors.webp and /dev/null differ diff --git a/landing/public/images/cost-controls.webp b/landing/public/images/cost-controls.webp deleted file mode 100644 index 7e6ca6e8..00000000 Binary files a/landing/public/images/cost-controls.webp and /dev/null differ diff --git a/landing/public/images/dashboard.webp b/landing/public/images/dashboard.webp deleted file mode 100644 index 789e3cf6..00000000 Binary files a/landing/public/images/dashboard.webp and /dev/null differ diff --git a/landing/public/images/messages.webp b/landing/public/images/messages.webp deleted file mode 100644 index a7c375ac..00000000 Binary files a/landing/public/images/messages.webp and /dev/null differ diff --git a/landing/public/images/office.webp b/landing/public/images/office.webp deleted file mode 100644 index 69d01dcb..00000000 Binary files a/landing/public/images/office.webp and /dev/null differ diff --git a/landing/public/images/og.jpg b/landing/public/images/og.jpg deleted file mode 100644 index ba53d5db..00000000 Binary files a/landing/public/images/og.jpg and /dev/null differ diff --git a/landing/public/images/og.png b/landing/public/images/og.png new file mode 100644 index 00000000..6acd367a Binary files /dev/null and b/landing/public/images/og.png differ diff --git a/landing/public/images/org-chart.webp b/landing/public/images/org-chart.webp deleted file mode 100644 index 63a96983..00000000 Binary files a/landing/public/images/org-chart.webp and /dev/null differ diff --git a/landing/scripts/og.mjs b/landing/scripts/og.mjs new file mode 100644 index 00000000..b6ca8188 --- /dev/null +++ b/landing/scripts/og.mjs @@ -0,0 +1,143 @@ +// Draws the 8-bit Open Graph card at 240×126 and writes it ×5 as public/images/og.png. +import { writeFileSync } from "node:fs"; +import { deflateSync } from "node:zlib"; + +const W = 240; +const H = 126; +const SCALE = 5; + +const PAL = [ + "#0f0f1b", "#1b1b3a", "#2a2a5e", "#3c3c86", "#565aa8", "#fcfcfc", "#f8d878", "#f0a030", + "#a8e4fc", "#58d854", "#d82800", +].map((hex) => [1, 3, 5].map((i) => Number.parseInt(hex.slice(i, i + 2), 16))); +const [VOID, NIGHT, DUSK, HAZE, ROOF, WHITE, LIGHT, GOLD, ICE, LEAF, RED] = PAL.keys(); + +const px = new Uint8Array(W * H); +const set = (x, y, c) => { if (x >= 0 && y >= 0 && x < W && y < H) px[y * W + x] = c; }; +const rect = (x, y, w, h, c) => { for (let j = y; j < y + h; j++) for (let i = x; i < x + w; i++) set(i, j, c); }; + +const FONT = { + A: "01110 10001 10001 11111 10001 10001 10001", H: "10001 10001 10001 11111 10001 10001 10001", + C: "01111 10000 10000 10000 10000 10000 01111", D: "11110 10001 10001 10001 10001 10001 11110", + E: "11111 10000 11110 10000 10000 10000 11111", G: "01111 10000 10000 10111 10001 10001 01111", + I: "11111 00100 00100 00100 00100 00100 11111", K: "10001 10010 11100 10010 10001 10001 10001", + L: "10000 10000 10000 10000 10000 10000 11111", M: "10001 11011 10101 10101 10001 10001 10001", + N: "10001 11001 10101 10011 10001 10001 10001", O: "01110 10001 10001 10001 10001 10001 01110", + P: "11110 10001 10001 11110 10000 10000 10000", R: "11110 10001 10001 11110 10100 10010 10001", + S: "01111 10000 01110 00001 00001 00001 11110", T: "11111 00100 00100 00100 00100 00100 00100", + U: "10001 10001 10001 10001 10001 10001 01110", V: "10001 10001 10001 10001 01010 01010 00100", + W: "10001 10001 10001 10101 10101 11011 10001", X: "10001 01010 00100 00100 01010 10001 10001", + Y: "10001 01010 00100 00100 00100 00100 00100", 1: "00100 01100 00100 00100 00100 00100 01110", + 5: "11111 10000 11110 00001 00001 10001 01110", ".": "00000 00000 00000 00000 00000 00000 00100", + "-": "00000 00000 00000 11111 00000 00000 00000", "/": "00001 00010 00010 00100 01000 01000 10000", + "·": "00000 00000 00000 00100 00000 00000 00000", "▶": "10000 11000 11100 11110 11100 11000 10000", + "!": "00100 00100 00100 00100 00100 00000 00100", " ": "00000 00000 00000 00000 00000 00000 00000", +}; + +function text(str, x, y, c, s = 1, shadow = -1) { + [...str].forEach((ch, n) => { + const rows = FONT[ch]; + if (!rows) throw new Error(`No glyph for "${ch}".`); + rows.split(" ").forEach((row, j) => [...row].forEach((bit, i) => { + if (bit !== "1") return; + const gx = x + n * 6 * s + i * s; + const gy = y + j * s; + if (shadow >= 0) rect(gx + s, gy + s, s, s, shadow); + rect(gx, gy, s, s, c); + })); + }); +} +const width = (str, s = 1) => str.length * 6 * s - s; + +function sprite(art, x, y, map) { + art.trim().split("\n").forEach((row, j) => [...row.trim()].forEach((ch, i) => { if (ch !== ".") set(x + i, y + j, map[ch]); })); +} + +// Night sky in dithered bands, stars and a crescent moon. +const bands = [NIGHT, NIGHT, DUSK, DUSK, HAZE]; +for (let y = 0; y < 92; y++) { + const f = (y / 92) * (bands.length - 1); + const lo = Math.floor(f); + for (let x = 0; x < W; x++) set(x, y, f - lo > ((x + y * 2) % 4) / 4 + 0.12 ? bands[lo + 1] : bands[lo]); +} +let seed = 7; +const rand = () => (seed = (seed * 1103515245 + 12345) & 0x7fffffff) / 0x7fffffff; +for (let n = 0; n < 70; n++) set(Math.floor(rand() * W), Math.floor(rand() * 60), rand() > 0.85 ? LIGHT : WHITE); +for (let y = -9; y <= 9; y++) for (let x = -9; x <= 9; x++) { + if (x * x + y * y <= 81 && (x - 4) ** 2 + (y - 3) ** 2 > 64) set(206 + x, 18 + y, LIGHT); +} + +// Skyline with lit windows. +for (let x = 0, n = 0; x < W; n++) { + const w = 10 + Math.floor(rand() * 14); + const h = x < 150 ? 8 + Math.floor(rand() * 12) : 18 + Math.floor(rand() * 26); + rect(x, 104 - h, w, h, n % 2 ? VOID : NIGHT); + for (let wy = 104 - h + 3; wy < 101; wy += 4) for (let wx = x + 2; wx < x + w - 2; wx += 3) if (rand() > 0.55) set(wx, wy, rand() > 0.2 ? LIGHT : GOLD); + x += w; +} +rect(0, 104, W, 22, VOID); + +// The tower and the CEO on its roof. +rect(160, 58, 34, 46, NIGHT); +rect(158, 56, 38, 3, ROOF); +for (let y = 62; y < 102; y += 5) for (let x = 163; x < 192; x += 5) rect(x, y, 3, 3, (x + y) % 3 ? LIGHT : GOLD); +sprite(` +..G.G.G.. +..GGGGG.. +..SSSSS.. +..KKSKK.. +..SSSSS.. +.WBBRBBW. +.SBBRBBS. +..BBBBB.. +..BB.BB.. +..KK.KK.. +`, 172, 44, { G: GOLD, S: LIGHT, K: VOID, W: WHITE, B: DUSK, R: RED }); + +// Speech bubble from the CEO. +rect(150, 26, 47, 12, WHITE); +for (let i = 0; i < 4; i++) rect(174 + i, 38 + i, 4 - i, 1, WHITE); +text("HIRING!", 153, 28, VOID); + +// Logotype, tagline and the dialog box. +text("AGENT", 10, 12, GOLD, 3, VOID); +text("COMPANY", 10, 38, WHITE, 3, VOID); +text("RUN AN AUTONOMOUS AI", 11, 66, ICE, 1, VOID); +text("COMPANY ON YOUR DESKTOP.", 11, 76, ICE, 1, VOID); + +rect(6, 98, 228, 24, VOID); +rect(7, 99, 226, 22, WHITE); +rect(8, 100, 224, 20, NIGHT); +text("V1.5.1 · MACOS · WINDOWS · LINUX", 12, 103, WHITE); +text("▶", 12, 112, GOLD); +text("AGENT-COMPANY.DEV", 20, 112, LIGHT); +const free = "NO ACCOUNT"; +text(free, 228 - width(free), 112, LEAF); + +// PNG encoder: ×SCALE nearest-neighbour, RGB, no filter. +const rowLen = W * SCALE * 3 + 1; +const raw = Buffer.alloc(rowLen * H * SCALE); +for (let y = 0; y < H * SCALE; y++) { + raw[y * rowLen] = 0; + for (let x = 0; x < W * SCALE; x++) { + const [r, g, b] = PAL[px[Math.floor(y / SCALE) * W + Math.floor(x / SCALE)]]; + raw.set([r, g, b], y * rowLen + 1 + x * 3); + } +} +const crcTable = Array.from({ length: 256 }, (_, n) => { let c = n; for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; return c >>> 0; }); +const crc = (buf) => { let c = 0xffffffff; for (const byte of buf) c = crcTable[(c ^ byte) & 255] ^ (c >>> 8); return (c ^ 0xffffffff) >>> 0; }; +const chunk = (type, data) => { + const out = Buffer.alloc(12 + data.length); + out.writeUInt32BE(data.length, 0); + out.write(type, 4, "ascii"); + data.copy(out, 8); + out.writeUInt32BE(crc(out.subarray(4, 8 + data.length)), 8 + data.length); + return out; +}; +const ihdr = Buffer.alloc(13); +ihdr.writeUInt32BE(W * SCALE, 0); +ihdr.writeUInt32BE(H * SCALE, 4); +ihdr.set([8, 2, 0, 0, 0], 8); +const png = Buffer.concat([Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), chunk("IHDR", ihdr), chunk("IDAT", deflateSync(raw, { level: 9 })), chunk("IEND", Buffer.alloc(0))]); +writeFileSync(new URL("../public/images/og.png", import.meta.url), png); +console.log(`og.png ${W * SCALE}×${H * SCALE}, ${png.length} bytes`);