1#!/usr/bin/env node
2
3import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
4import * as NodeServices from "@effect/platform-node/NodeServices";
5import * as Console from "effect/Console";
6import * as Effect from "effect/Effect";
7import * as FileSystem from "effect/FileSystem";
8import * as Path from "effect/Path";
9import * as Schema from "effect/Schema";
10import sharp from "sharp";
11
12import { BRAND_ASSET_PATHS } from "./lib/brand-assets.ts";
13
14export class AndroidIconRenderError extends Schema.TaggedError<AndroidIconRenderError>()(
15 "AndroidIconRenderError",
16 { cause: Schema.Defect() },
17) {}
18
19const decodeProject = Schema.decodeEffect(
20 Schema.fromJsonString(Schema.Struct({ fill: Schema.Struct({ solid: Schema.String }) })),
21);
22const decodeColor = Schema.decodeUnknownEffect(
23 Schema.Tuple([Schema.Number, Schema.Number, Schema.Number, Schema.Number]),
24);
25
26const exportAndroidIcons = Effect.gen(function* () {
27 const fs = yield* FileSystem.FileSystem;
28 const path = yield* Path.Path;
29 const repositoryRoot = path.resolve(import.meta.dirname, "..");
30 const projectRoot = path.join(repositoryRoot, BRAND_ASSET_PATHS.productionIconComposerProject);
31 const project = yield* decodeProject(
32 yield* fs.readFileString(path.join(projectRoot, "icon.json")),
33 );
34 const [r, g, b, alpha] = yield* decodeColor(
35 project.fill.solid.slice("srgb:".length).split(",").map(Number),
36 );
37 const background = { r: r * 255, g: g * 255, b: b * 255, alpha };
38 const source = yield* fs.readFile(path.join(projectRoot, "Assets", "knife.png"));
39
40 for (const [name, size] of [
41 ["android-foreground.png", 432],
42 ["android-splash.png", 1152],
43 ] as const) {
44 const png = yield* Effect.tryPromise({
45 try: async () => {
46 // Android masks the central two thirds; the blade must stay inside that circle.
47 const foreground = await sharp(source)
48 .resize(Math.round(size * 0.57), Math.round(size * 0.57), { fit: "inside" })
49 .png()
50 .toBuffer();
51 return sharp({
52 create: {
53 width: size,
54 height: size,
55 channels: 4,
56 background: name === "android-foreground.png" ? "transparent" : background,
57 },
58 })
59 .composite([{ input: foreground, gravity: "centre" }])
60 .png()
61 .toBuffer();
62 },
63 catch: (cause) => new AndroidIconRenderError({ cause }),
64 });
65 const output = path.join(repositoryRoot, "assets", "knife", name);
66 yield* fs.writeFile(output, png);
67 yield* Console.log(`wrote ${output}`);
68 }
69});
70
71if (import.meta.main) {
72 exportAndroidIcons.pipe(Effect.provide(NodeServices.layer), NodeRuntime.runMain);
73}