///|
/// Named motif recipes used by interactive previews and repeatable exports.
pub(all) struct PatternRecipeSpec {
name : String
family : String
bounds : Rect
density : Int
seed : UInt
palette : Palette
} derive(Debug)
///|
pub(all) struct PatternRecipeResult {
spec : PatternRecipeSpec
batch : PatternBatch
layer : PatternLayer
document : SvgDocument
quality : DesignQualityMetrics
} derive(Debug)
///|
pub fn PatternRecipeSpec::new(
name : String,
family : String,
bounds : Rect,
density : Int,
seed : UInt,
palette : Palette,
) -> PatternRecipeSpec {
{ name, family, bounds, density: density.max(1), seed, palette }
}
///|
pub fn PatternRecipeSpec::with_density(
self : PatternRecipeSpec,
density : Int,
) -> PatternRecipeSpec {
{ ..self, density: density.max(1) }
}
///|
pub fn pattern_recipe_batch(spec : PatternRecipeSpec) -> PatternBatch {
let center = spec.bounds.center()
match spec.family {
"dots" =>
catalog_dot_field(
spec.bounds,
spec.density,
spec.density,
2.0,
12.0,
spec.palette,
spec.seed,
)
"sunburst" =>
catalog_sunburst(
center,
spec.bounds.width.min(spec.bounds.height) / 2.0,
spec.density * 2,
4.0,
spec.palette,
spec.seed,
)
"moire" =>
catalog_moire(
center,
spec.bounds.width.min(spec.bounds.height) / 2.0,
spec.density * 2,
0.2,
8.0,
tx_color_at(spec.palette, 0),
spec.seed,
)
"frames" =>
catalog_nested_frames(
spec.bounds,
spec.density,
spec.bounds.width.min(spec.bounds.height) /
(spec.density * 3).to_double(),
spec.palette,
spec.seed,
)
"isometric" =>
catalog_isometric(
spec.bounds,
spec.density,
spec.density,
spec.bounds.width / spec.density.to_double(),
spec.bounds.height / spec.density.to_double(),
spec.palette,
spec.seed,
)
"orbits" =>
catalog_orbits(
center,
spec.density,
spec.density.min(12),
10.0,
12.0,
spec.palette,
spec.seed,
)
"spiral" =>
catalog_spiral_field(
center,
spec.density * 4,
4.0,
2.0,
spec.bounds.width.min(spec.bounds.height) / 2.0,
spec.palette,
spec.seed,
)
"barcode" =>
catalog_barcode(
spec.bounds,
spec.density * 2,
2.0,
spec.bounds.width / spec.density.to_double(),
spec.palette,
spec.seed,
)
"checker" =>
catalog_round_checker(
spec.bounds,
spec.density,
spec.density,
3.0,
tx_color_at(spec.palette, 0),
tx_color_at(spec.palette, 1),
spec.seed,
)
"cross" =>
catalog_cross_weave(
spec.bounds,
spec.density,
spec.density,
spec.bounds.width / spec.density.to_double() * 0.6,
spec.palette,
spec.seed,
)
_ =>
catalog_flow_segments(
spec.bounds,
spec.density,
spec.density,
12.0,
ValueNoise::new(0.02, spec.seed),
spec.palette,
spec.seed,
)
}
}
///|
pub fn pattern_recipe_result(spec : PatternRecipeSpec) -> PatternRecipeResult {
let batch = pattern_recipe_batch(spec)
let layer = batch.to_layer(1.0)
let composition = Composition::new(
spec.bounds.width,
spec.bounds.height,
background=tx_color_at(spec.palette, 0),
)
composition.add_layer(layer)
let document = composition.render()
{
spec,
batch,
layer,
document,
quality: composition_quality_metrics(composition),
}
}
///|
pub fn pattern_recipe_asset(result : PatternRecipeResult) -> SvgAsset {
svg_asset_from_document(
"recipe",
result.spec.name,
1,
result.document,
result.spec.family,
)
}
///|
pub fn pattern_recipe_report(result : PatternRecipeResult) -> String {
"name=" +
result.spec.name +
" family=" +
result.spec.family +
" motifs=" +
result.batch.count().to_string() +
" quality=" +
result.quality.score.to_string() +
" checksum=" +
svg_document_checksum(result.document).to_string()
}
///|
pub fn pattern_recipe_manifest(results : Array[PatternRecipeResult]) -> String {
let builder = StringBuilder()
builder.write_string("name,family,motifs,quality,coverage,checksum\n")
for result in results {
builder.write_string(
result.spec.name +
"," +
result.spec.family +
"," +
result.batch.count().to_string() +
"," +
result.quality.score.to_string() +
"," +
result.quality.coverage.to_string() +
"," +
svg_document_checksum(result.document).to_string() +
"\n",
)
}
builder.to_string()
}
///|
pub fn pattern_recipe_grid(
bounds : Rect,
names : Array[String],
density : Int,
palette : Palette,
seed : UInt,
) -> Array[PatternRecipeResult] {
let result = []
let columns = names.length().max(1).min(4)
let rows = if names.length() == 0 {
0
} else {
(names.length() + columns - 1) / columns
}
let cell_width = bounds.width / columns.to_double()
let cell_height = if rows == 0 {
0.0
} else {
bounds.height / rows.to_double()
}
for index, name in names {
let cell = Rect::new(
bounds.x + (index % columns).to_double() * cell_width,
bounds.y + (index / columns).to_double() * cell_height,
cell_width,
cell_height,
)
result.push(
pattern_recipe_result(
PatternRecipeSpec::new(
name,
name,
cell,
density,
seed + index.reinterpret_as_uint(),
palette,
),
),
)
}
result
}
///|
pub fn pattern_recipe_bundle(
results : Array[PatternRecipeResult],
) -> SvgAssetBundle {
let bundle = SvgAssetBundle::new("pattern-recipes", "1.0.0")
for result in results {
let _ = bundle.add(pattern_recipe_asset(result))
}
bundle
}
///|
pub fn pattern_recipe_layer(
results : Array[PatternRecipeResult],
opacity : Double,
) -> PatternLayer {
let layer = PatternLayer::new("recipe-stack", opacity~)
for result in results {
for motif in result.layer.motifs {
layer.add(motif)
}
}
layer
}
///|
pub fn pattern_recipe_document(
results : Array[PatternRecipeResult],
width : Double,
height : Double,
background : Color,
) -> DesignDocument {
let composition = Composition::new(width, height, background~)
composition.add_layer(pattern_recipe_layer(results, 1.0))
DesignDocument::new(
composition,
DocumentMetadata::new(
"Pattern Recipe Sheet", "MoonBit Generative Patterns", 0, "recipe preview",
).tag("recipes"),
)
}
///|
pub fn pattern_recipe_rank(
results : Array[PatternRecipeResult],
) -> Array[(String, Double)] {
let ranked = []
for result in results {
ranked.push((result.spec.name, result.quality.score))
}
for i in 1.. 0 && ranked[position - 1].1 < value.1 {
ranked[position] = ranked[position - 1]
position -= 1
}
ranked[position] = value
}
ranked
}
///|
pub fn pattern_recipe_filter(
results : Array[PatternRecipeResult],
minimum_score : Double,
) -> Array[PatternRecipeResult] {
let result = []
for item in results {
if item.quality.score >= minimum_score {
result.push(item)
}
}
result
}
///|
pub fn pattern_recipe_translate(
result : PatternRecipeResult,
offset : Vec2,
) -> PatternRecipeResult {
let batch = result.batch.translate(offset)
let layer = batch.to_layer(1.0)
let composition = Composition::new(
result.spec.bounds.width,
result.spec.bounds.height,
background=tx_color_at(result.spec.palette, 0),
)
composition.add_layer(layer)
let document = composition.render()
{
..result,
batch,
layer,
document,
quality: composition_quality_metrics(composition),
}
}
///|
pub fn pattern_recipe_scale(
result : PatternRecipeResult,
factor : Double,
) -> PatternRecipeResult {
let batch = result.batch.translate(
result.spec.bounds.center().scale(1.0 - factor.max(0.0)),
)
let layer = batch.to_layer(factor.max(0.0))
let composition = Composition::new(
result.spec.bounds.width,
result.spec.bounds.height,
background=tx_color_at(result.spec.palette, 0),
)
composition.add_layer(layer)
let document = composition.render()
{
..result,
batch,
layer,
document,
quality: composition_quality_metrics(composition),
}
}
///|
pub fn pattern_recipe_validate(result : PatternRecipeResult) -> Array[String] {
let errors = []
if result.spec.name.length() == 0 {
errors.push("recipe name is empty")
}
if result.batch.count() == 0 {
errors.push("recipe has no motifs")
}
if result.document.width <= 0.0 || result.document.height <= 0.0 {
errors.push("recipe document dimensions invalid")
}
errors
}
///|
pub fn pattern_recipe_thumbnail(
result : PatternRecipeResult,
size : Int,
) -> Raster {
let raster = Raster::new(size, size, fill=tx_color_at(result.spec.palette, 0))
raster_plus_draw_motifs(raster, result.layer.motifs)
}
///|
pub fn pattern_recipe_thumbnail_quality(
result : PatternRecipeResult,
size : Int,
) -> RasterQualityMetrics {
raster_quality_metrics(pattern_recipe_thumbnail(result, size), 0.02)
}
///|
pub fn pattern_recipe_families() -> Array[String] {
[
"dots", "sunburst", "moire", "frames", "isometric", "orbits", "spiral", "barcode",
"checker", "cross", "flow",
]
}
///|
pub fn pattern_recipe_default_suite(
bounds : Rect,
density : Int,
palette : Palette,
seed : UInt,
) -> Array[PatternRecipeResult] {
pattern_recipe_grid(bounds, pattern_recipe_families(), density, palette, seed)
}
///|
pub fn pattern_recipe_suite_report(
bounds : Rect,
density : Int,
palette : Palette,
seed : UInt,
) -> String {
let results = pattern_recipe_default_suite(bounds, density, palette, seed)
pattern_recipe_manifest(results)
}
///|
pub fn pattern_recipe_export_plan(
bounds : Rect,
density : Int,
palette : Palette,
seed : UInt,
) -> ExportPlan {
let _ = density
let job = export_job_standard("recipe-suite", seed, palette)
job.add_variant("sheet", bounds.width, bounds.height)
export_plan(job)
}
///|
pub fn pattern_recipe_document_svg(
bounds : Rect,
density : Int,
palette : Palette,
seed : UInt,
) -> String {
let results = pattern_recipe_default_suite(bounds, density, palette, seed)
pattern_recipe_document(
results,
bounds.width,
bounds.height,
tx_color_at(palette, 0),
).to_svg()
}