deckcraft/crates/model/src/theme.rs

270 lines
9.5 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Themes: colour scheme, font scheme and format scheme (the fill/line/effect "style matrix").
use deckcraft_color::{ColorScheme, ColorTransform, Rgba, SchemeSlot};
use serde::{Deserialize, Serialize};
use crate::style::{ColorRef, Effects, Fill, Gradient, GradientShape, GradientStop, Line, Shadow};
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct FontSet {
pub latin: String,
pub ea: String,
pub cs: String,
/// Per-script typefaces (`a:font script="Arab" typeface="…"`), in document order.
#[serde(skip_serializing_if = "Vec::is_empty")]
pub scripts: Vec<ScriptFont>,
}
/// A theme font for one script (ISO 15924 tag, e.g. `Arab`).
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct ScriptFont {
pub script: String,
pub typeface: String,
}
impl FontSet {
/// The typeface the theme gives `script` (`"Arab"`), if any.
pub fn script(&self, script: &str) -> Option<&str> {
self.scripts.iter().find(|f| f.script.eq_ignore_ascii_case(script) && !f.typeface.is_empty()).map(|f| f.typeface.as_str())
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct FontScheme {
pub name: String,
/// Headings.
pub major: FontSet,
/// Body.
pub minor: FontSet,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct FormatScheme {
pub name: String,
/// Subtle, moderate, intense (style index 1–3).
pub fills: Vec<Fill>,
pub lines: Vec<Line>,
pub effects: Vec<Effects>,
/// Background fills (index 1001–1003).
pub bg_fills: Vec<Fill>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Theme {
pub name: String,
pub colors: ColorScheme,
pub fonts: FontScheme,
pub format: FormatScheme,
/// Raw theme XML extras kept for round-trip (object defaults, extra colour lists).
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_extra: Option<String>,
}
impl Default for Theme {
fn default() -> Self {
builtin_themes().into_iter().next().unwrap_or_else(|| Theme {
name: "DeckCraft".into(),
colors: ColorScheme { name: "DeckCraft".into(), colors: [Rgba::BLACK; 12] },
fonts: FontScheme::default(),
format: FormatScheme::default(),
raw_extra: None,
})
}
}
impl Theme {
/// Resolve a theme font reference (`+mj-lt`, `+mn-ea`…) to a family name.
pub fn font(&self, name: &str) -> String {
match name {
"+mj-lt" => self.fonts.major.latin.clone(),
"+mn-lt" => self.fonts.minor.latin.clone(),
"+mj-ea" => self.fonts.major.ea.clone(),
"+mn-ea" => self.fonts.minor.ea.clone(),
"+mj-cs" => self.fonts.major.cs.clone(),
"+mn-cs" => self.fonts.minor.cs.clone(),
other => other.to_string(),
}
}
}
fn hex(s: &str) -> Rgba {
Rgba::from_hex(s).unwrap_or(Rgba::BLACK)
}
fn scheme(name: &str, c: [&str; 12]) -> ColorScheme {
ColorScheme { name: name.into(), colors: c.map(hex) }
}
fn ph() -> ColorRef {
ColorRef::scheme(SchemeSlot::PhClr)
}
/// Our default format scheme: flat solid fills, a soft gradient, and a gentle shadow for "intense".
pub fn default_format() -> FormatScheme {
let grad = |a: i32, b: i32| {
Fill::Gradient(Gradient {
stops: vec![
GradientStop { pos: 0.0, color: ph().with(ColorTransform::LumMod(a)).with(ColorTransform::SatMod(103000)) },
GradientStop { pos: 1.0, color: ph().with(ColorTransform::LumMod(b)) },
],
shape: GradientShape::Linear { angle: 90.0, scaled: false },
rotate_with_shape: true,
})
};
FormatScheme {
name: "DeckCraft".into(),
fills: vec![Fill::solid(ph()), grad(110000, 92000), grad(105000, 80000)],
lines: vec![Line::solid(ph(), 0.75), Line::solid(ph(), 1.0), Line::solid(ph(), 1.5)],
effects: vec![
Effects::default(),
Effects::default(),
Effects {
outer_shadow: Some(Shadow {
color: ColorRef::rgb(Rgba::BLACK).with(ColorTransform::Alpha(40000)),
blur: 6.0,
dist: 3.0,
dir: 90.0,
inner: false,
sx: 1.0,
sy: 1.0,
kx: 0.0,
ky: 0.0,
align: "t".into(),
rotate_with_shape: false,
}),
..Default::default()
},
],
bg_fills: vec![
Fill::solid(ph()),
Fill::solid(ph().with(ColorTransform::Tint(95000)).with(ColorTransform::SatMod(170000))),
grad(102000, 85000),
],
}
}
/// Our original built-in themes (shown in Design ▸ Themes). Colours and names are our own.
pub fn builtin_themes() -> Vec<Theme> {
let t = |name: &str, colors: ColorScheme, major: &str, minor: &str| Theme {
name: name.into(),
colors,
fonts: FontScheme {
name: name.into(),
major: FontSet { latin: major.into(), ..Default::default() },
minor: FontSet { latin: minor.into(), ..Default::default() },
},
format: default_format(),
raw_extra: None,
};
vec![
t(
"DeckCraft",
scheme(
"DeckCraft",
["000000", "FFFFFF", "1E2B3C", "EDEBE7", "2E6FD8", "F26B1D", "2EA36F", "8A5CD8", "D83F6B", "E3B21C", "2E6FD8", "7A5BA6"],
),
"Inter",
"Inter",
),
t(
"Harbor",
scheme(
"Harbor",
["0B1F2A", "FFFFFF", "12384D", "E4EEF2", "1B7FA6", "36B3A8", "F2C14E", "F78154", "5D576B", "8AA29E", "1B7FA6", "5D576B"],
),
"Merriweather",
"Open Sans",
),
t(
"Ember",
scheme("Ember", ["1A1A1A", "FFFCF7", "3D2B1F", "F6EDE3", "D9480F", "F59F00", "A61E4D", "5C940D", "1971C2", "862E9C", "D9480F", "862E9C"]),
"Montserrat",
"Lato",
),
t(
"Meadow",
scheme(
"Meadow",
["14281D", "FFFFFF", "2D4A37", "EEF5EE", "4C9A2A", "A4C639", "2F6F4E", "E9B44C", "9B2915", "50A2A7", "2F6F4E", "9B2915"],
),
"Merriweather",
"Open Sans",
),
t(
"Nocturne",
scheme(
"Nocturne",
["FFFFFF", "121826", "F2F4F8", "1E2638", "7C5CFF", "00C2A8", "FF6B9A", "FFC53D", "4DA3FF", "B2F35F", "7C5CFF", "FF6B9A"],
),
"Inter",
"Inter",
),
t(
"Paper",
scheme("Paper", ["262626", "FFFFFF", "404040", "F2F2F2", "595959", "8C8C8C", "C0504D", "4F81BD", "9BBB59", "F79646", "4F81BD", "8064A2"]),
"Playfair Display",
"Lato",
),
t(
"Coral Reef",
scheme(
"Coral Reef",
["0F2A33", "FFFFFF", "0E4D64", "FDF1EC", "FF6F59", "254441", "43AA8B", "B2B09B", "EF3054", "3F88C5", "3F88C5", "EF3054"],
),
"Poppins",
"Nunito Sans",
),
t(
"Slate",
scheme("Slate", ["111827", "FFFFFF", "374151", "F3F4F6", "6366F1", "0EA5E9", "10B981", "F59E0B", "EF4444", "8B5CF6", "6366F1", "8B5CF6"]),
"Roboto",
"Roboto",
),
]
}
/// Named colour schemes for Design ▸ Variants ▸ Colors.
pub fn builtin_color_schemes() -> Vec<ColorScheme> {
let mut v: Vec<ColorScheme> = builtin_themes().into_iter().map(|t| t.colors).collect();
v.push(scheme(
"Grayscale",
["000000", "FFFFFF", "000000", "F8F8F8", "DDDDDD", "B2B2B2", "969696", "808080", "5F5F5F", "4D4D4D", "5F5F5F", "919191"],
));
v.push(scheme("Ocean", ["000000", "FFFFFF", "13293D", "E8F1F2", "006494", "247BA0", "1B98E0", "6CC1E8", "13293D", "0B3954", "1B98E0", "6CC1E8"]));
v.push(scheme(
"Sunset",
["2B2118", "FFFFFF", "6F1A07", "FFF3E6", "FF9F1C", "FFBF69", "CB997E", "E76F51", "F4A261", "2A9D8F", "E76F51", "2A9D8F"],
));
v.push(scheme(
"Forest",
["1B1B1B", "FFFFFF", "283618", "FEFAE0", "606C38", "DDA15E", "BC6C25", "283618", "8A9A5B", "4F772D", "606C38", "BC6C25"],
));
v.push(scheme("Berry", ["1F0A1D", "FFFFFF", "4A1942", "F7E8F3", "893168", "C6426E", "EB6A8F", "4A1942", "7D7ABC", "56A3A6", "893168", "56A3A6"]));
v
}
/// Named font pairs for Design ▸ Variants ▸ Fonts.
pub fn builtin_font_schemes() -> Vec<FontScheme> {
let f = |name: &str, major: &str, minor: &str| FontScheme {
name: name.into(),
major: FontSet { latin: major.into(), ..Default::default() },
minor: FontSet { latin: minor.into(), ..Default::default() },
};
vec![
f("Inter", "Inter", "Inter"),
f("Classic", "Merriweather", "Open Sans"),
f("Geometric", "Montserrat", "Lato"),
f("Editorial", "Playfair Display", "Lato"),
f("Humanist", "Merriweather", "Open Sans"),
f("Friendly", "Poppins", "Nunito Sans"),
f("Neutral", "Roboto", "Roboto"),
f("Office Compatible", "Carlito", "Carlito"),
f("Times / Arial", "Liberation Serif", "Liberation Sans"),
]
}