diff --git a/public/icons/clock-icon-192.png b/public/icons/clock-icon-192.png index b051c49..d46288c 100644 Binary files a/public/icons/clock-icon-192.png and b/public/icons/clock-icon-192.png differ diff --git a/public/icons/clock-icon-512.png b/public/icons/clock-icon-512.png index beac4b7..023675c 100644 Binary files a/public/icons/clock-icon-512.png and b/public/icons/clock-icon-512.png differ diff --git a/public/icons/clock-maskable-512.png b/public/icons/clock-maskable-512.png index c04c1d7..9778c7f 100644 Binary files a/public/icons/clock-maskable-512.png and b/public/icons/clock-maskable-512.png differ diff --git a/scripts/build-icons.mjs b/scripts/build-icons.mjs index f4bbc9f..21957e0 100644 --- a/scripts/build-icons.mjs +++ b/scripts/build-icons.mjs @@ -162,95 +162,206 @@ function chatMaskableSvg(size, fontPx) { `; } -// Non-maskable clock icon (192 and 512). Transparent background. -// Layout: orange clock ring with tick marks, cube mark as the centre -// hub, "CLOCK" wordmark underneath — same brand DNA as the dashboard -// and chat icons. -function clockSvg(size, fontPx) { +// MEGA Clock icon — dark background with a clock face on top of an +// isometric cube. Each cube face has a bold "M" carved out in the +// background colour, and the cube has two small orange triangular +// "shadow" wedges at the bottom. No wordmark; this icon stands on +// its own the same way the dashboard and chat icons do. +function clockFaceAndHands() { const cx = 256; - const cy = 190; - const r = 130; + const cy = 175; + const r = 110; - const cubeSize = 90; - const cubeX = cx - cubeSize / 2; - const cubeY = cy - cubeSize / 2; + // Clock hands: minute straight up, hour to 4 o'clock. + // 4 o'clock = 120° clockwise from 12 (measured around the dial). + const hourAngle = (120 * Math.PI) / 180; + const hourLen = r * 0.42; + const hourEndX = cx + hourLen * Math.sin(hourAngle); + const hourEndY = cy - hourLen * Math.cos(hourAngle); + const minuteEndY = cy - r * 0.82; - const wordY = 420; + // Orange arc on the upper-right: 12 o'clock → 4 o'clock. + const arcEndAngle = (120 * Math.PI) / 180; + const arcEndX = cx + r * Math.sin(arcEndAngle); + const arcEndY = cy - r * Math.cos(arcEndAngle); - const ticks = []; - for (let i = 0; i < 12; i++) { - const angle = (i * Math.PI * 2) / 12; - const inner = r - 14; - const outer = r - 2; - const x1 = cx + inner * Math.sin(angle); - const y1 = cy - inner * Math.cos(angle); - const x2 = cx + outer * Math.sin(angle); - const y2 = cy - outer * Math.cos(angle); - ticks.push( - ``, - ); - } + return ` + + + - return ` - - - + + - - - ${ticks.join("\n ")} + + - - - - - - + + + + + + `; +} + +// A bold, geometric "M" centred at (cx, cy), width w, height h, +// thickness t. The result is a single filled path that can be tinted +// via fill — used cut into the cube faces (fill = background colour). +function mShapePath(cx, cy, w, h, t) { + const xL = cx - w / 2; + const xR = cx + w / 2; + const xLi = xL + t; + const xRi = xR - t; + const yT = cy - h / 2; + const yB = cy + h / 2; + // Outer peak drops about 40% down from the top; inner peak sits + // slightly above to keep the diagonal strokes consistent. + const peakOuter = cy - h / 2 + h * 0.42; + const peakInner = cy - h / 2 + h * 0.62; + return ` + `; +} + +// Isometric cube with the bold "M" cut into each of its two visible +// faces, plus the two orange base wedges below it. (x, y) is the +// top-centre of the cube; width of each face is 200px each side, so +// the cube spans from x-200 to x+200. +function clockCube(x, y) { + const topY = y; + const outerTopY = y + 30; + const outerBotY = y + 210; + const botY = y + 240; + const halfW = 200; + + const xC = x; + const xL = x - halfW; + const xR = x + halfW; + + // The cube faces are tilted parallelograms (top edges slope up to + // the centre by 30px across 200px → atan(30/200) ≈ 8.5°), so each + // M is rotated to sit on its face rather than floating upright. + const leftMCenter = { x: x - 100, y: y + 125 }; + const rightMCenter = { x: x + 100, y: y + 125 }; + + return ` + + + + + + + ${mShapePath(leftMCenter.x, leftMCenter.y, 70, 110, 16)} + + + ${mShapePath(rightMCenter.x, rightMCenter.y, 70, 110, 16)} + + + + + `; +} - - CLOCK +function clockSvg(size, fontPx) { + return ` + + + + ${clockFaceAndHands()} + + ${clockCube(256, 310)} `; } // Maskable variant — orange fills the whole canvas. Content lives // inside the centre ~40% safe-zone (~205x205 inside a 512 canvas). +// Uses the same clock + cube + M composition, recoloured as negative +// space (orange on black becomes black on orange). function clockMaskableSvg(size, fontPx) { - const scale = 205 / 512; - const offset = (512 - 205) / 2; + const W = 512; + const H = 512; + const offset = (W - 205) / 2; + const cx = W / 2; + const cy = offset + 60; + const r = 50; - const cx = offset + 102; - const cy = offset + 75; - const r = 55 * (scale / scale); // keep in the 205-wide safe zone + // Hour hand to 4 o'clock, minute to 12. + const hourAngle = (120 * Math.PI) / 180; + const hourLen = r * 0.55; + const hourEndX = cx + hourLen * Math.sin(hourAngle); + const hourEndY = cy - hourLen * Math.cos(hourAngle); + const minuteEndY = cy - r * 0.82; - const cubeSize = 38; - const cubeX = cx - cubeSize / 2; - const cubeY = cy - cubeSize / 2; + const arcEndAngle = (120 * Math.PI) / 180; + const arcEndX = cx + r * Math.sin(arcEndAngle); + const arcEndY = cy - r * Math.cos(arcEndAngle); - const wordY = offset + 175; + // Tiny isometric cube below the clock, fully inside the safe zone. + const topY = offset + 130; + const outerTopY = topY + 8; + const outerBotY = topY + 50; + const botY = topY + 58; + const halfW = 60; + const xL = cx - halfW; + const xR = cx + halfW; - const ticks = []; - for (let i = 0; i < 12; i++) { - const angle = (i * Math.PI * 2) / 12; - const inner = r - 6; - const outer = r - 1; - const x1 = cx + inner * Math.sin(angle); - const y1 = cy - inner * Math.cos(angle); - const x2 = cx + outer * Math.sin(angle); - const y2 = cy - outer * Math.cos(angle); - ticks.push( - ``, - ); - } + return ` + + - return ` + + + + + + + + + + + + + + + + + + + ${mShapePath(cx - 30, topY + 30, 24, 36, 6)} + ${mShapePath(cx + 30, topY + 30, 24, 36, 6)} + + `; +} + +// 1200x630 Open Graph share image — left: the dark clock + cube +// icon at a generous size; right: "MEGA Clock" wordmark and a +// short tagline. Generated as a static PNG so Slack/iMessage/etc. +// pick it up automatically from the route segment. +function clockOgSvg() { + const W = 1200; + const H = 630; + + // Subtle dot grid + faint orange glow upper-right, same family as + // the dashboard OG image so the share previews feel related. + return ` + + + + + + + + - - + + + + - - - ${ticks.join("\n ")} + + + + ${clockFaceAndHands()} + ${clockCube(256, 310)} + - - - - - - + + MEGA INFRASTRUCTURE + + + MEGA Clock + + + Scan a worker's badge to clock in or out. + + + + + + - - - CLOCK `; } @@ -328,13 +457,28 @@ await renderAppleIcon( ); // MEGA Clock icon set (non-maskable 192/512, maskable 512) -await renderSvg(clockSvg(512, 80), path.join(ROOT, "public/icons/clock-icon-512.png"), 512); -await renderSvg(clockSvg(192, 30), path.join(ROOT, "public/icons/clock-icon-192.png"), 192); -await renderSvg(clockMaskableSvg(512, 38), path.join(ROOT, "public/icons/clock-maskable-512.png"), 512); +await renderSvg(clockSvg(512, 0), path.join(ROOT, "public/icons/clock-icon-512.png"), 512); +await renderSvg(clockSvg(192, 0), path.join(ROOT, "public/icons/clock-icon-192.png"), 192); +await renderSvg(clockMaskableSvg(512, 0), path.join(ROOT, "public/icons/clock-maskable-512.png"), 512); await renderAppleIcon( path.join(ROOT, "public/icons/clock-icon-512.png"), path.join(ROOT, "src/app/clock/apple-icon.png"), ); +// Open Graph share image for /clock. Next.js auto-detects +// `opengraph-image.png` placed in a route segment and serves it +// with the matching tag, so Slack / +// iMessage / etc. preview the icon + title when the link is shared. +{ + const out = path.join(ROOT, "src/app/clock/opengraph-image.png"); + const buf = Buffer.from(clockOgSvg()); + await sharp(buf, { density: 144 }) + .resize({ width: 1200, height: 630 }) + .png({ compressionLevel: 9 }) + .toFile(out); + const stat = await fs.stat(out); + console.log(`Wrote ${out} (${(stat.size / 1024).toFixed(1)} KB, 1200x630)`); +} + console.log("All icons rendered."); \ No newline at end of file diff --git a/src/app/clock/apple-icon.png b/src/app/clock/apple-icon.png index d18491a..531dca4 100644 Binary files a/src/app/clock/apple-icon.png and b/src/app/clock/apple-icon.png differ diff --git a/src/app/clock/opengraph-image.alt.txt b/src/app/clock/opengraph-image.alt.txt new file mode 100644 index 0000000..3c2f14a --- /dev/null +++ b/src/app/clock/opengraph-image.alt.txt @@ -0,0 +1 @@ +MEGA Clock — the standalone QR timekeeping PWA. The icon shows a clock face with an orange upper-right arc, hands at 12 and 4 o'clock, sitting on top of an isometric cube with the MEGA "M" carved into both faces. \ No newline at end of file diff --git a/src/app/clock/opengraph-image.png b/src/app/clock/opengraph-image.png new file mode 100644 index 0000000..5f26e8e Binary files /dev/null and b/src/app/clock/opengraph-image.png differ diff --git a/src/app/clock/page.tsx b/src/app/clock/page.tsx index 8630136..21807a7 100644 --- a/src/app/clock/page.tsx +++ b/src/app/clock/page.tsx @@ -3,12 +3,24 @@ import { ClockView } from "./clock-view"; export const metadata: Metadata = { title: "Clock · MEGA INFRASTRUCTURE", + description: + "Scan a worker's badge to clock in or out. MEGA Clock is the standalone QR timekeeping PWA for site phones.", manifest: "/clock.webmanifest", appleWebApp: { capable: true, title: "Clock", statusBarStyle: "default", }, + openGraph: { + title: "MEGA Clock", + description: "Scan a worker's badge to clock in or out.", + type: "website", + }, + twitter: { + card: "summary_large_image", + title: "MEGA Clock", + description: "Scan a worker's badge to clock in or out.", + }, }; export default function ClockPage() {