@ideare/kit 0.0.0-stage → 0.1.0-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +148 -2
- package/THIRD_PARTY.md +52 -0
- package/dist/brand/schema.d.ts +265 -0
- package/dist/brand/schema.js +271 -0
- package/dist/check/copy.d.ts +27 -0
- package/dist/check/copy.js +139 -0
- package/dist/check/index.d.ts +54 -0
- package/dist/check/index.js +568 -0
- package/dist/color/contrast.d.ts +41 -0
- package/dist/color/contrast.js +84 -0
- package/dist/color/palette.d.ts +27 -0
- package/dist/color/palette.js +114 -0
- package/dist/color/wcag.d.ts +22 -0
- package/dist/color/wcag.js +50 -0
- package/dist/draft/draft.d.ts +61 -0
- package/dist/draft/draft.js +383 -0
- package/dist/editor/history.d.ts +78 -0
- package/dist/editor/history.js +221 -0
- package/dist/editor/operations.d.ts +112 -0
- package/dist/editor/operations.js +621 -0
- package/dist/files/docs.d.ts +49 -0
- package/dist/files/docs.js +392 -0
- package/dist/files/generate.d.ts +47 -0
- package/dist/files/generate.js +254 -0
- package/dist/files/png.d.ts +40 -0
- package/dist/files/png.js +168 -0
- package/dist/files/raster.d.ts +14 -0
- package/dist/files/raster.js +0 -0
- package/dist/files/templates.d.ts +162 -0
- package/dist/files/templates.js +390 -0
- package/dist/files/tokens.d.ts +22 -0
- package/dist/files/tokens.js +204 -0
- package/dist/fonts/catalog.d.ts +3 -0
- package/dist/fonts/catalog.js +345 -0
- package/dist/fonts/ofl.d.ts +5 -0
- package/dist/fonts/ofl.js +5 -0
- package/dist/fonts/pick.d.ts +33 -0
- package/dist/fonts/pick.js +73 -0
- package/dist/imports/folder.d.ts +80 -0
- package/dist/imports/folder.js +124 -0
- package/dist/imports/unzip.d.ts +32 -0
- package/dist/imports/unzip.js +128 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +34 -0
- package/dist/marks/draw.d.ts +48 -0
- package/dist/marks/draw.js +149 -0
- package/dist/marks/icons.data.d.ts +5 -0
- package/dist/marks/icons.data.js +600 -0
- package/dist/marks/one-color.d.ts +45 -0
- package/dist/marks/one-color.js +304 -0
- package/dist/marks/svg.d.ts +53 -0
- package/dist/marks/svg.js +393 -0
- package/dist/node/index.d.ts +13 -0
- package/dist/node/index.js +41 -0
- package/dist/posts/pix.d.ts +19 -0
- package/dist/posts/pix.js +49 -0
- package/dist/posts/templates.d.ts +45 -0
- package/dist/posts/templates.js +868 -0
- package/dist/resvg/index.d.ts +2 -0
- package/dist/resvg/index.js +22 -0
- package/dist/scene/brand-art.d.ts +61 -0
- package/dist/scene/brand-art.js +217 -0
- package/dist/scene/frame.d.ts +89 -0
- package/dist/scene/frame.js +213 -0
- package/dist/scene/grain.d.ts +7 -0
- package/dist/scene/grain.js +7 -0
- package/dist/scene/qr.d.ts +10 -0
- package/dist/scene/qr.js +359 -0
- package/dist/scene/render.d.ts +76 -0
- package/dist/scene/render.js +557 -0
- package/dist/scene/schema.d.ts +271 -0
- package/dist/scene/schema.js +257 -0
- package/dist/sign.d.ts +47 -0
- package/dist/sign.js +88 -0
- package/dist/text/engine.d.ts +128 -0
- package/dist/text/engine.js +245 -0
- package/dist/text/fonts.d.ts +56 -0
- package/dist/text/fonts.js +65 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/dist/wasm/index.d.ts +15 -0
- package/dist/wasm/index.js +46 -0
- package/package.json +78 -5
- package/schema/brand.v1.json +442 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { clampChroma, formatHex, modeOklch, modeRgb, useMode } from "culori/fn";
|
|
2
|
+
import { contrast, luminance, normalizeHex } from "./wcag.js";
|
|
3
|
+
// culori's tree-shakable entry: register only the two spaces the kit converts between.
|
|
4
|
+
const toOklchColor = useMode(modeOklch);
|
|
5
|
+
useMode(modeRgb);
|
|
6
|
+
/** A hex color's OKLCH coordinates. A gray has no hue; it reads as 0. */
|
|
7
|
+
export function toOklch(hex) {
|
|
8
|
+
const color = toOklchColor(normalizeHex(hex));
|
|
9
|
+
return { l: color?.l ?? 0, c: color?.c ?? 0, h: color?.h ?? 0 };
|
|
10
|
+
}
|
|
11
|
+
/** Distance in OKLab between two hex colors: under 0.02 looks the same, 0.1 is clearly different. */
|
|
12
|
+
export function oklabDistance(a, b) {
|
|
13
|
+
const lab = (hex) => {
|
|
14
|
+
const { l, c, h } = toOklch(hex);
|
|
15
|
+
const radians = (h * Math.PI) / 180;
|
|
16
|
+
return [l, c * Math.cos(radians), c * Math.sin(radians)];
|
|
17
|
+
};
|
|
18
|
+
const [l1, a1, b1] = lab(a);
|
|
19
|
+
const [l2, a2, b2] = lab(b);
|
|
20
|
+
return Math.hypot(l1 - l2, a1 - a2, b1 - b2);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The hex for an OKLCH color, brought into sRGB by lowering chroma at the same lightness and hue
|
|
24
|
+
* (culori's clampChroma). Lightness is clamped to 0..1.
|
|
25
|
+
*/
|
|
26
|
+
export function fromOklch({ l, c, h }) {
|
|
27
|
+
const color = clampChroma({ mode: "oklch", l: Math.min(1, Math.max(0, l)), c: Math.max(0, c), h }, "oklch", "rgb");
|
|
28
|
+
return formatHex(color);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The closest color to `fg`, at the same OKLCH hue and chroma where sRGB allows, that reaches
|
|
32
|
+
* `min` contrast against `bg`. It searches lightness both ways (darker and lighter), keeps the
|
|
33
|
+
* smaller change that passes, and measures WCAG 2 on the final hex, so rounding to 8 bits can
|
|
34
|
+
* never push it under the line.
|
|
35
|
+
*/
|
|
36
|
+
export function ensureContrast(fg, bg, min) {
|
|
37
|
+
const start = normalizeHex(fg);
|
|
38
|
+
const startRatio = contrast(start, bg);
|
|
39
|
+
if (startRatio >= min)
|
|
40
|
+
return { hex: start, ratio: startRatio, ok: true, changed: false };
|
|
41
|
+
const base = toOklch(start);
|
|
42
|
+
const candidates = [0, 1]
|
|
43
|
+
.map((extreme) => searchLightness(base, extreme, bg, min))
|
|
44
|
+
.filter((found) => !!found)
|
|
45
|
+
.sort((a, b) => a.distance - b.distance);
|
|
46
|
+
const best = candidates[0];
|
|
47
|
+
if (best)
|
|
48
|
+
return { hex: best.hex, ratio: best.ratio, ok: true, changed: best.hex !== start };
|
|
49
|
+
// Nothing reaches the minimum: give back whichever of black and white comes closest.
|
|
50
|
+
const fallback = contrast("#000000", bg) >= contrast("#ffffff", bg) ? "#000000" : "#ffffff";
|
|
51
|
+
return { hex: fallback, ratio: contrast(fallback, bg), ok: false, changed: true };
|
|
52
|
+
}
|
|
53
|
+
function searchLightness(base, extreme, bg, min) {
|
|
54
|
+
const at = (l) => fromOklch({ ...base, l });
|
|
55
|
+
const far = at(extreme);
|
|
56
|
+
if (contrast(far, bg) < min)
|
|
57
|
+
return undefined;
|
|
58
|
+
// `pass` always holds a lightness whose measured hex passes; `fail` one that does not.
|
|
59
|
+
let fail = base.l;
|
|
60
|
+
let pass = extreme;
|
|
61
|
+
for (let step = 0; step < 24; step++) {
|
|
62
|
+
const mid = (fail + pass) / 2;
|
|
63
|
+
if (contrast(at(mid), bg) >= min)
|
|
64
|
+
pass = mid;
|
|
65
|
+
else
|
|
66
|
+
fail = mid;
|
|
67
|
+
}
|
|
68
|
+
const hex = at(pass);
|
|
69
|
+
return { hex, ratio: contrast(hex, bg), distance: Math.abs(pass - base.l) };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The better of the candidates (by default the given ink and white) to put on `bg`, measured.
|
|
73
|
+
* smartgenius's onColor, generalized: a brand fill keeps its own color, and the text on it picks
|
|
74
|
+
* the side that reads.
|
|
75
|
+
*/
|
|
76
|
+
export function onColor(bg, ink = "#141414", light = "#ffffff") {
|
|
77
|
+
const dark = { hex: normalizeHex(ink), ratio: contrast(ink, bg) };
|
|
78
|
+
const bright = { hex: normalizeHex(light), ratio: contrast(light, bg) };
|
|
79
|
+
return dark.ratio >= bright.ratio ? dark : bright;
|
|
80
|
+
}
|
|
81
|
+
/** True when `bg` is light enough that dark text is the natural pick. */
|
|
82
|
+
export function isLight(hex) {
|
|
83
|
+
return luminance(hex) > 0.179;
|
|
84
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ColorRole, Palette } from "../brand/schema.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Every pair of roles a brand promises to keep readable, with its WCAG 2 floor: 4.5 for text,
|
|
4
|
+
* 3 for lines, fills and focus (non-text). check() measures these on every brand, and
|
|
5
|
+
* generatePalette() is built to pass them.
|
|
6
|
+
*/
|
|
7
|
+
export declare const CONTRAST_PAIRS: readonly {
|
|
8
|
+
fg: ColorRole;
|
|
9
|
+
bg: ColorRole;
|
|
10
|
+
min: number;
|
|
11
|
+
what: string;
|
|
12
|
+
}[];
|
|
13
|
+
export declare const MOODS: readonly ["calm", "bold", "warm", "fresh", "classic", "playful"];
|
|
14
|
+
export type Mood = (typeof MOODS)[number];
|
|
15
|
+
export type PaletteSeed = {
|
|
16
|
+
hue: number;
|
|
17
|
+
mood: Mood;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* A light and a dark role set from one hue and a mood. Lightness and chroma come from the mood;
|
|
21
|
+
* every role is then pushed (in OKLCH lightness only) until each of CONTRAST_PAIRS passes, so the
|
|
22
|
+
* result is readable by construction and keeps the hue the owner picked.
|
|
23
|
+
*/
|
|
24
|
+
export declare function generatePalette({ hue, mood }: PaletteSeed): {
|
|
25
|
+
light: Palette;
|
|
26
|
+
dark: Palette;
|
|
27
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { ensureContrast, fromOklch, onColor } from "./contrast.js";
|
|
2
|
+
/**
|
|
3
|
+
* Every pair of roles a brand promises to keep readable, with its WCAG 2 floor: 4.5 for text,
|
|
4
|
+
* 3 for lines, fills and focus (non-text). check() measures these on every brand, and
|
|
5
|
+
* generatePalette() is built to pass them.
|
|
6
|
+
*/
|
|
7
|
+
export const CONTRAST_PAIRS = [
|
|
8
|
+
{ fg: "ink", bg: "ground", min: 4.5, what: "text" },
|
|
9
|
+
{ fg: "ink", bg: "surface", min: 4.5, what: "text on cards" },
|
|
10
|
+
{ fg: "muted", bg: "ground", min: 4.5, what: "quiet text" },
|
|
11
|
+
{ fg: "muted", bg: "surface", min: 4.5, what: "quiet text on cards" },
|
|
12
|
+
{ fg: "line", bg: "ground", min: 3, what: "field borders" },
|
|
13
|
+
{ fg: "line", bg: "surface", min: 3, what: "field borders on cards" },
|
|
14
|
+
{ fg: "primary", bg: "ground", min: 3, what: "the brand color as a button" },
|
|
15
|
+
{ fg: "onPrimary", bg: "primary", min: 4.5, what: "text on the brand color" },
|
|
16
|
+
{ fg: "accent", bg: "ground", min: 3, what: "the accent as a fill" },
|
|
17
|
+
{ fg: "onAccent", bg: "accent", min: 4.5, what: "text on the accent" },
|
|
18
|
+
{ fg: "focus", bg: "ground", min: 3, what: "the focus ring" },
|
|
19
|
+
{ fg: "danger", bg: "ground", min: 4.5, what: "error text" },
|
|
20
|
+
{ fg: "success", bg: "ground", min: 4.5, what: "success text" },
|
|
21
|
+
{ fg: "warning", bg: "ground", min: 4.5, what: "warning text" },
|
|
22
|
+
];
|
|
23
|
+
export const MOODS = ["calm", "bold", "warm", "fresh", "classic", "playful"];
|
|
24
|
+
const MOOD = {
|
|
25
|
+
calm: { chroma: 0.09, accentShift: 150, accentChroma: 0.07, tint: 0.006 },
|
|
26
|
+
bold: { chroma: 0.2, accentShift: 180, accentChroma: 0.17, tint: 0.004 },
|
|
27
|
+
warm: { chroma: 0.14, accentShift: 40, accentChroma: 0.12, tint: 0.012 },
|
|
28
|
+
fresh: { chroma: 0.15, accentShift: 120, accentChroma: 0.13, tint: 0.008 },
|
|
29
|
+
classic: { chroma: 0.07, accentShift: 30, accentChroma: 0.05, tint: 0.004 },
|
|
30
|
+
playful: { chroma: 0.19, accentShift: 210, accentChroma: 0.16, tint: 0.01 },
|
|
31
|
+
};
|
|
32
|
+
const STATUS_HUE = { danger: 27, success: 150, warning: 70, focus: 255 };
|
|
33
|
+
/**
|
|
34
|
+
* A light and a dark role set from one hue and a mood. Lightness and chroma come from the mood;
|
|
35
|
+
* every role is then pushed (in OKLCH lightness only) until each of CONTRAST_PAIRS passes, so the
|
|
36
|
+
* result is readable by construction and keeps the hue the owner picked.
|
|
37
|
+
*/
|
|
38
|
+
export function generatePalette({ hue, mood }) {
|
|
39
|
+
const spec = MOOD[mood];
|
|
40
|
+
const h = ((hue % 360) + 360) % 360;
|
|
41
|
+
const light = buildTheme("light", h, spec);
|
|
42
|
+
const dark = buildTheme("dark", h, spec);
|
|
43
|
+
// The fleet's retune rule: the brand color is never the same in both themes.
|
|
44
|
+
if (dark.primary === light.primary)
|
|
45
|
+
dark.primary = fromOklch({ l: 0.8, c: spec.chroma, h });
|
|
46
|
+
return { light, dark };
|
|
47
|
+
}
|
|
48
|
+
function buildTheme(theme, h, spec) {
|
|
49
|
+
const isLight = theme === "light";
|
|
50
|
+
const ground = isLight
|
|
51
|
+
? fromOklch({ l: 0.993, c: spec.tint * 0.5, h })
|
|
52
|
+
: fromOklch({ l: 0.17, c: spec.tint * 1.5, h });
|
|
53
|
+
const surface = isLight
|
|
54
|
+
? fromOklch({ l: 0.965, c: spec.tint * 1.5, h })
|
|
55
|
+
: fromOklch({ l: 0.22, c: spec.tint * 2, h });
|
|
56
|
+
const grounds = [ground, surface];
|
|
57
|
+
const ink = readable(fromOklch({ l: isLight ? 0.22 : 0.95, c: 0.02, h }), grounds, 4.5);
|
|
58
|
+
const muted = readable(fromOklch({ l: isLight ? 0.48 : 0.74, c: 0.025, h }), grounds, 4.5);
|
|
59
|
+
const line = readable(fromOklch({ l: isLight ? 0.64 : 0.52, c: 0.02, h }), grounds, 3);
|
|
60
|
+
// Text on a fill: ink or white in light themes; the dark ground or the ink in dark ones.
|
|
61
|
+
const darkText = isLight ? ink : ground;
|
|
62
|
+
const lightText = isLight ? "#ffffff" : ink;
|
|
63
|
+
const primaryPair = fill(fromOklch({ l: isLight ? 0.52 : 0.74, c: spec.chroma, h }), ground, {
|
|
64
|
+
dark: darkText,
|
|
65
|
+
light: lightText,
|
|
66
|
+
prefer: isLight ? lightText : darkText,
|
|
67
|
+
});
|
|
68
|
+
const accentPair = fill(fromOklch({
|
|
69
|
+
l: isLight ? 0.58 : 0.76,
|
|
70
|
+
c: spec.accentChroma,
|
|
71
|
+
h: (h + spec.accentShift) % 360,
|
|
72
|
+
}), ground, { dark: darkText, light: lightText, prefer: isLight ? lightText : darkText });
|
|
73
|
+
const status = (hueOf, l, c, min) => readable(fromOklch({ l, c, h: hueOf }), grounds, min);
|
|
74
|
+
return {
|
|
75
|
+
ground,
|
|
76
|
+
surface,
|
|
77
|
+
ink,
|
|
78
|
+
muted,
|
|
79
|
+
line,
|
|
80
|
+
primary: primaryPair.fill,
|
|
81
|
+
onPrimary: primaryPair.on,
|
|
82
|
+
accent: accentPair.fill,
|
|
83
|
+
onAccent: accentPair.on,
|
|
84
|
+
focus: status(STATUS_HUE.focus, isLight ? 0.55 : 0.72, 0.17, 3),
|
|
85
|
+
danger: status(STATUS_HUE.danger, isLight ? 0.52 : 0.72, 0.19, 4.5),
|
|
86
|
+
success: status(STATUS_HUE.success, isLight ? 0.5 : 0.75, 0.13, 4.5),
|
|
87
|
+
warning: status(STATUS_HUE.warning, isLight ? 0.52 : 0.8, 0.12, 4.5),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/** Push `color` until it reaches `min` against every background in `grounds`. */
|
|
91
|
+
function readable(color, grounds, min) {
|
|
92
|
+
let hex = color;
|
|
93
|
+
// Two passes: fixing it against one ground can, rarely, bring it back under on another.
|
|
94
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
95
|
+
for (const ground of grounds)
|
|
96
|
+
hex = ensureContrast(hex, ground, min).hex;
|
|
97
|
+
}
|
|
98
|
+
return hex;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* A fill that reads as a control on the ground (3:1) and carries 4.5:1 text. The text is the
|
|
102
|
+
* dark or the light candidate, whichever reads; when neither does, the fill moves away from the
|
|
103
|
+
* preferred one (darker under white in light themes, lighter over the dark ground in dark ones).
|
|
104
|
+
*/
|
|
105
|
+
function fill(color, ground, text) {
|
|
106
|
+
let hex = ensureContrast(color, ground, 3).hex;
|
|
107
|
+
let on = onColor(hex, text.dark, text.light);
|
|
108
|
+
if (on.ratio < 4.5) {
|
|
109
|
+
hex = ensureContrast(hex, text.prefer, 4.5).hex;
|
|
110
|
+
hex = ensureContrast(hex, ground, 3).hex;
|
|
111
|
+
on = onColor(hex, text.dark, text.light);
|
|
112
|
+
}
|
|
113
|
+
return { fill: hex, on: on.hex };
|
|
114
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WCAG 2 contrast on hex colors. The fleet's contrast gates measure the same way, so a brand
|
|
3
|
+
* that passes here passes there.
|
|
4
|
+
*/
|
|
5
|
+
export type Rgb = {
|
|
6
|
+
r: number;
|
|
7
|
+
g: number;
|
|
8
|
+
b: number;
|
|
9
|
+
};
|
|
10
|
+
/** `#rrggbb` (or `#rgb`) to 0..255 channels. Throws on anything else. */
|
|
11
|
+
export declare function parseHex(hex: string): Rgb;
|
|
12
|
+
export declare function toHex({ r, g, b }: Rgb): string;
|
|
13
|
+
/** Lowercase `#rrggbb`. */
|
|
14
|
+
export declare function normalizeHex(hex: string): string;
|
|
15
|
+
/** WCAG 2 relative luminance, 0 (black) to 1 (white). */
|
|
16
|
+
export declare function luminance(hex: string): number;
|
|
17
|
+
/** WCAG 2 contrast ratio, 1 to 21. Order does not matter. */
|
|
18
|
+
export declare function contrast(a: string, b: string): number;
|
|
19
|
+
/** The ratio rounded down to 2 decimals, the way people should see it: 4.499 is not 4.5. */
|
|
20
|
+
export declare function formatRatio(ratio: number): string;
|
|
21
|
+
/** Composite a color with alpha over an opaque ground, for contrast on translucent paint. */
|
|
22
|
+
export declare function over(hex: string, alpha: number, ground: string): string;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WCAG 2 contrast on hex colors. The fleet's contrast gates measure the same way, so a brand
|
|
3
|
+
* that passes here passes there.
|
|
4
|
+
*/
|
|
5
|
+
/** `#rrggbb` (or `#rgb`) to 0..255 channels. Throws on anything else. */
|
|
6
|
+
export function parseHex(hex) {
|
|
7
|
+
const match = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());
|
|
8
|
+
if (!match?.[1])
|
|
9
|
+
throw new TypeError(`Not a hex color: ${hex}`);
|
|
10
|
+
const digits = match[1].length === 3 ? [...match[1]].map((digit) => digit + digit).join("") : match[1];
|
|
11
|
+
const value = Number.parseInt(digits, 16);
|
|
12
|
+
return { r: (value >> 16) & 255, g: (value >> 8) & 255, b: value & 255 };
|
|
13
|
+
}
|
|
14
|
+
export function toHex({ r, g, b }) {
|
|
15
|
+
const channel = (value) => Math.max(0, Math.min(255, Math.round(value)))
|
|
16
|
+
.toString(16)
|
|
17
|
+
.padStart(2, "0");
|
|
18
|
+
return `#${channel(r)}${channel(g)}${channel(b)}`;
|
|
19
|
+
}
|
|
20
|
+
/** Lowercase `#rrggbb`. */
|
|
21
|
+
export function normalizeHex(hex) {
|
|
22
|
+
return toHex(parseHex(hex));
|
|
23
|
+
}
|
|
24
|
+
function linear(channel) {
|
|
25
|
+
const value = channel / 255;
|
|
26
|
+
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
|
27
|
+
}
|
|
28
|
+
/** WCAG 2 relative luminance, 0 (black) to 1 (white). */
|
|
29
|
+
export function luminance(hex) {
|
|
30
|
+
const { r, g, b } = parseHex(hex);
|
|
31
|
+
return 0.2126 * linear(r) + 0.7152 * linear(g) + 0.0722 * linear(b);
|
|
32
|
+
}
|
|
33
|
+
/** WCAG 2 contrast ratio, 1 to 21. Order does not matter. */
|
|
34
|
+
export function contrast(a, b) {
|
|
35
|
+
const first = luminance(a);
|
|
36
|
+
const second = luminance(b);
|
|
37
|
+
const [light, dark] = first > second ? [first, second] : [second, first];
|
|
38
|
+
return (light + 0.05) / (dark + 0.05);
|
|
39
|
+
}
|
|
40
|
+
/** The ratio rounded down to 2 decimals, the way people should see it: 4.499 is not 4.5. */
|
|
41
|
+
export function formatRatio(ratio) {
|
|
42
|
+
return `${(Math.floor(ratio * 100) / 100).toFixed(2)}:1`;
|
|
43
|
+
}
|
|
44
|
+
/** Composite a color with alpha over an opaque ground, for contrast on translucent paint. */
|
|
45
|
+
export function over(hex, alpha, ground) {
|
|
46
|
+
const top = parseHex(hex);
|
|
47
|
+
const bottom = parseHex(ground);
|
|
48
|
+
const mix = (a, b) => a * alpha + b * (1 - alpha);
|
|
49
|
+
return toHex({ r: mix(top.r, bottom.r), g: mix(top.g, bottom.g), b: mix(top.b, bottom.b) });
|
|
50
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Brand, Locale } from "../brand/schema.ts";
|
|
2
|
+
import { type Mood } from "../color/palette.ts";
|
|
3
|
+
import type { FontResolver } from "../text/fonts.ts";
|
|
4
|
+
/**
|
|
5
|
+
* A draft brand from a business's name and one line about it, drawn by code alone. The same
|
|
6
|
+
* input always draws the same brand, so a link that carries the input (`draftParams`) redraws it
|
|
7
|
+
* anywhere: the site's try-it makes one in the browser and the studio picks it up from the link.
|
|
8
|
+
*
|
|
9
|
+
* `seed` keeps the look steady while the name is typed: colors, fonts and the kind of logo come
|
|
10
|
+
* from the seed, and change only when a word says something about the business ("padaria",
|
|
11
|
+
* "salon"), so mostly the letters change as you type.
|
|
12
|
+
* `picks` count each "Try another" press, per part.
|
|
13
|
+
*/
|
|
14
|
+
export type DraftPicks = {
|
|
15
|
+
colors: number;
|
|
16
|
+
fonts: number;
|
|
17
|
+
logo: number;
|
|
18
|
+
};
|
|
19
|
+
export type DraftInput = {
|
|
20
|
+
name: string;
|
|
21
|
+
/** What the business does, in the owner's words. It becomes the tagline. */
|
|
22
|
+
idea: string;
|
|
23
|
+
locale: Locale;
|
|
24
|
+
seed: number;
|
|
25
|
+
picks: DraftPicks;
|
|
26
|
+
};
|
|
27
|
+
/** The longest name (the wordmark's limit) and line (the copy check's tagline limit) a draft
|
|
28
|
+
* takes: every file the kit makes has room for them. */
|
|
29
|
+
export declare const DRAFT_LIMITS: {
|
|
30
|
+
readonly name: 40;
|
|
31
|
+
readonly idea: 60;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* The font pairs a draft picks from. Few on purpose: a browser carries every file
|
|
35
|
+
* (`draftFontFiles`), so the try-it draws with no font server. Each display face has one weight,
|
|
36
|
+
* and the text faces are sans or serif opposites of their display.
|
|
37
|
+
*/
|
|
38
|
+
export declare const DRAFT_FONT_PAIRS: readonly {
|
|
39
|
+
display: string;
|
|
40
|
+
weight: number;
|
|
41
|
+
text: string;
|
|
42
|
+
moods: readonly Mood[];
|
|
43
|
+
}[];
|
|
44
|
+
/** Every font file a draft can ask for, so a page can serve them from its own origin. */
|
|
45
|
+
export declare function draftFontFiles(): {
|
|
46
|
+
id: string;
|
|
47
|
+
weight: number;
|
|
48
|
+
}[];
|
|
49
|
+
/** A fresh seed for a new draft. */
|
|
50
|
+
export declare function newDraftSeed(): number;
|
|
51
|
+
/**
|
|
52
|
+
* Draw the draft. The name and the line are trimmed and cut to DRAFT_LIMITS; an empty line makes
|
|
53
|
+
* the name the tagline. Loads the display font (for a monogram) through `fonts`.
|
|
54
|
+
*/
|
|
55
|
+
export declare function draftBrand(input: DraftInput, ctx: {
|
|
56
|
+
fonts: FontResolver;
|
|
57
|
+
}): Promise<Brand>;
|
|
58
|
+
/** The draft as link parameters: name, idea, locale, seed, colors, fonts and logo. */
|
|
59
|
+
export declare function draftParams(input: DraftInput): URLSearchParams;
|
|
60
|
+
/** A draft read back from link parameters, or null when any part is missing or out of range. */
|
|
61
|
+
export declare function readDraftParams(params: URLSearchParams): DraftInput | null;
|