const PAPER_PRESETS = [
{
id: "bright-white",
label: "Bright White",
paperColor: "#ffffff",
scatterColor: "#f2e7c8",
},
{
id: "natural",
label: "Natural",
paperColor: "#f7f5ef",
scatterColor: "#edd7a1",
},
{
id: "ivory",
label: "Ivory",
paperColor: "#f4efe2",
scatterColor: "#e4c470",
},
];
export const DEFAULT_PAPER_PRESET_ID = "natural";
function clamp01(value) {
return Math.max(0, Math.min(1, value));
}
function hexToRgb(hex) {
if (typeof hex !== "string" || !/^#[0-9a-fA-F]{6}$/.test(hex)) {
return [1, 1, 1];
}
return [
parseInt(hex.slice(1, 3), 16) / 255,
parseInt(hex.slice(3, 5), 16) / 255,
parseInt(hex.slice(5, 7), 16) / 255,
];
}
function rgbToHex(rgb) {
return `#${rgb
.map(channel => Math.round(clamp01(channel) * 255).toString(16).padStart(2, "0"))
.join("")}`;
}
function mixRgb(a, b, t) {
const weight = clamp01(t);
return [
a[0] + (b[0] - a[0]) * weight,
a[1] + (b[1] - a[1]) * weight,
a[2] + (b[2] - a[2]) * weight,
];
}
function lighten(rgb, amount) {
return mixRgb(rgb, [1, 1, 1], amount);
}
function darken(rgb, amount) {
return mixRgb(rgb, [0, 0, 0], amount);
}
function buildPaperAppearance(preset) {
if (preset.id === "bright-white") {
return {
paperColor: preset.paperColor,
lightShadowColor: "#ebebeb",
lightHighlightColor: "#ffffff",
shadowTintColor: "#f2f2f2",
};
}
const paperRgb = hexToRgb(preset.paperColor);
const scatterRgb = mixRgb(paperRgb, hexToRgb(preset.scatterColor), 0.68);
const lightShadowRgb = darken(mixRgb(paperRgb, scatterRgb, 0.45), 0.08);
const lightHighlightRgb = lighten(mixRgb(paperRgb, [1, 0.985, 0.94], 0.38), 0.03);
const shadowTintRgb = darken(mixRgb(scatterRgb, [0.97, 0.84, 0.46], 0.5), 0.14);
return {
paperColor: preset.paperColor,
lightShadowColor: rgbToHex(lightShadowRgb),
lightHighlightColor: rgbToHex(lightHighlightRgb),
shadowTintColor: rgbToHex(shadowTintRgb),
};
}
/**
* Returns the available paper presets.
*
* @returns {Array<{id: string, label: string}>} Preset options.
*/
export function getPaperPresetOptions() {
return PAPER_PRESETS.map(({ id, label }) => ({ id, label }));
}
/**
* Returns a valid paper preset id, falling back to the default.
*
* @param {string} presetId Preset id to normalize.
* @returns {string} Valid preset id.
*/
export function normalizePaperPreset(presetId) {
return PAPER_PRESETS.some(preset => preset.id === presetId)
? presetId
: DEFAULT_PAPER_PRESET_ID;
}
/**
* Finds the paper preset id matching a paper color.
*
* @param {string} paperColor CSS hex paper color.
* @returns {string} Matching preset id, or the default preset id.
*/
export function getPaperPresetIdForColor(paperColor) {
const normalized = typeof paperColor === "string" ? paperColor.toLowerCase() : "";
return PAPER_PRESETS.find(preset => preset.paperColor.toLowerCase() === normalized)?.id || DEFAULT_PAPER_PRESET_ID;
}
/**
* Applies preset-derived paper colors to a display object.
*
* @param {Partial<Display>} display Display object to mutate.
* @param {string} presetId Paper preset id.
* @returns {Partial<Display>} The mutated display object.
*/
export function applyPaperPreset(display, presetId) {
const normalizedPresetId = normalizePaperPreset(presetId);
const preset = PAPER_PRESETS.find(entry => entry.id === normalizedPresetId);
Object.assign(display, {
paperPreset: normalizedPresetId,
...buildPaperAppearance(preset),
});
return display;
}