///|
pub(all) struct TemplateContent {
eyebrow : String
title : String
subtitle : String
body : String
cta : String
footer : String
image_label : String
image_asset_id : String
} derive(Debug, Eq, FromJson, ToJson)
///|
fn canvas_frame(preset : @preset.PosterPreset) -> @scene.Rect {
{ x: 0, y: 0, width: preset.canvas.width, height: preset.canvas.height }
}
///|
fn eyebrow_style(theme : @theme.Theme) -> @scene.TextStyle {
{
..@scene.TextStyle::body(theme),
size: theme.caption_size,
line_height: theme.caption_size + 8,
weight: 700,
color: theme.accent,
}
}
///|
fn caption_style(theme : @theme.Theme) -> @scene.TextStyle {
{
..@scene.TextStyle::body(theme),
size: theme.caption_size,
line_height: theme.caption_size + 10,
color: theme.muted,
}
}
///|
fn launch_layout(
preset : @preset.PosterPreset,
theme : @theme.Theme,
content : TemplateContent,
) -> @scene.PosterDocument {
let frame = canvas_frame(preset).inset(theme.padding)
let sections = @layout.stack_vertical(
region=frame,
item_heights=[theme.caption_size + 10, 260, 380, 140],
gap=32,
)
let document = @scene.PosterDocument::empty(
title=content.title,
preset~,
theme~,
)
let headline_style = @scene.TextStyle::display(theme)
let body_style = @scene.TextStyle::body(theme)
let footer_style = caption_style(theme)
let body_block = @layout.anchor(
region=sections[3],
size={ width: sections[3].width, height: sections[3].height },
h_align=@layout.HorizontalAlign::Start,
v_align=@layout.VerticalAlign::Top,
margin=@core.EdgeInsets::zero(),
)
document
.push(
@scene.Element::Block(
id="launch-shell",
rect=frame,
fill=@theme.Color::hex("#fffaf2"),
radius=36,
),
)
.push(
@scene.Element::Text(
id="launch-eyebrow",
content=content.eyebrow,
position=@scene.Point::at(
sections[0].x,
sections[0].y + theme.caption_size,
),
style=eyebrow_style(theme),
max_width=sections[0].width,
),
)
.push(
@scene.Element::Text(
id="launch-subtitle",
content=content.subtitle,
position=@scene.Point::at(sections[1].x, sections[1].y + 34),
style=body_style,
max_width=sections[1].width,
),
)
.push(
@scene.Element::ImageSlot(
id="launch-hero",
asset_id=content.image_asset_id,
rect=sections[2],
label=content.image_label,
fit=@scene.ImageFit::Cover,
),
)
.push(
@scene.Element::Text(
id="launch-cta",
content=content.cta,
position=@scene.Point::at(
sections[3].x,
sections[3].y + theme.caption_size,
),
style=eyebrow_style(theme),
max_width=sections[3].width,
),
)
.push(
@scene.Element::Text(
id="launch-footer",
content=content.footer,
position=@scene.Point::at(body_block.x, body_block.y + 96),
style=footer_style,
max_width=sections[3].width,
),
)
.push_iter(
@textflow.paragraph_to_elements(
id_prefix="launch-title",
region=sections[1],
content=content.title,
style=headline_style,
).iter(),
)
.push_iter(
@textflow.paragraph_to_elements(
id_prefix="launch-body",
region={
x: sections[3].x,
y: sections[3].y + 40,
width: sections[3].width,
height: sections[3].height - 48,
},
content=content.body,
style=body_style,
).iter(),
)
}
///|
fn editorial_layout(
preset : @preset.PosterPreset,
theme : @theme.Theme,
content : TemplateContent,
) -> @scene.PosterDocument {
let frame = canvas_frame(preset).inset(theme.padding)
let grid = @layout.grid(region=frame, spec=@layout.GridSpec::{
columns: 1,
rows: 3,
gap_x: 0,
gap_y: 28,
padding: @core.EdgeInsets::zero(),
})
let document = @scene.PosterDocument::empty(
title=content.title,
preset~,
theme~,
)
let headline_style = {
..@scene.TextStyle::display(theme),
size: theme.title_size + 12,
line_height: theme.title_size + 18,
color: theme.accent,
}
document
.push(
@scene.Element::ImageSlot(
id="editorial-cover",
asset_id=content.image_asset_id,
rect=grid[0],
label=content.image_label,
fit=@scene.ImageFit::Cover,
),
)
.push(
@scene.Element::Block(
id="editorial-kicker",
rect={ x: grid[1].x, y: grid[1].y, width: 280, height: 52 },
fill=theme.accent,
radius=12,
),
)
.push(
@scene.Element::Text(
id="editorial-kicker-text",
content=content.eyebrow,
position=@scene.Point::at(grid[1].x + 18, grid[1].y + 32),
style={ ..caption_style(theme), color: @theme.Color::hex("#ffffff") },
max_width=244,
),
)
.push_iter(
@textflow.paragraph_to_elements(
id_prefix="editorial-title",
region={
x: grid[1].x,
y: grid[1].y + 84,
width: grid[1].width,
height: grid[1].height - 84,
},
content=content.title,
style=headline_style,
).iter(),
)
.push_iter(
@textflow.paragraph_to_elements(
id_prefix="editorial-subtitle",
region=grid[2],
content=content.subtitle + " " + content.body,
style=@scene.TextStyle::body(theme),
).iter(),
)
.push(
@scene.Element::Text(
id="editorial-footer",
content=content.footer,
position=@scene.Point::at(grid[2].x, grid[2].y + grid[2].height - 16),
style=caption_style(theme),
max_width=grid[2].width,
),
)
}
///|
fn event_layout(
preset : @preset.PosterPreset,
theme : @theme.Theme,
content : TemplateContent,
) -> @scene.PosterDocument {
let frame = canvas_frame(preset).inset(theme.padding)
let columns = @layout.grid(region=frame, spec=@layout.GridSpec::{
columns: 2,
rows: 2,
gap_x: 24,
gap_y: 24,
padding: @core.EdgeInsets::zero(),
})
let document = @scene.PosterDocument::empty(
title=content.title,
preset~,
theme~,
)
let left_top = columns[0]
let right_top = columns[1]
let footer : @scene.Rect = {
x: columns[2].x,
y: columns[2].y,
width: columns[2].width + 24 + columns[3].width,
height: columns[2].height,
}
document
.push(
@scene.Element::Block(
id="event-panel",
rect=frame,
fill=@theme.Color::hex("#fffaf2"),
radius=32,
),
)
.push(
@scene.Element::ImageSlot(
id="event-image",
asset_id=content.image_asset_id,
rect=right_top,
label=content.image_label,
fit=@scene.ImageFit::Cover,
),
)
.push_iter(
@textflow.paragraph_to_elements(
id_prefix="event-title",
region=left_top,
content=content.title,
style=@scene.TextStyle::display(theme),
).iter(),
)
.push_iter(
@textflow.paragraph_to_elements(
id_prefix="event-detail",
region={
x: left_top.x,
y: left_top.y + 220,
width: left_top.width,
height: left_top.height - 220,
},
content=content.subtitle + " " + content.body + " " + content.cta,
style=@scene.TextStyle::body(theme),
).iter(),
)
.push(
@scene.Element::Text(
id="event-footer",
content=content.footer,
position=@scene.Point::at(footer.x, footer.y + theme.caption_size),
style=eyebrow_style(theme),
max_width=footer.width,
),
)
.push(
@scene.Element::Text(
id="event-eyebrow",
content=content.eyebrow,
position=@scene.Point::at(footer.x, footer.y + 72),
style=caption_style(theme),
max_width=footer.width,
),
)
}
///|
fn quote_layout(
preset : @preset.PosterPreset,
theme : @theme.Theme,
content : TemplateContent,
) -> @scene.PosterDocument {
let frame = canvas_frame(preset).inset(theme.padding)
let sections = @layout.stack_vertical(
region=frame,
item_heights=[theme.caption_size + 12, 420, 260],
gap=28,
)
let document = @scene.PosterDocument::empty(
title=content.title,
preset~,
theme~,
)
let quote_style = {
..@scene.TextStyle::display(theme),
size: theme.title_size + 10,
line_height: theme.title_size + 22,
}
let body_style = @scene.TextStyle::body(theme)
let byline_style = { ..eyebrow_style(theme), color: theme.foreground }
let portrait_origin = @layout.anchor(
region=sections[2],
size={ width: 220, height: 220 },
h_align=@layout.HorizontalAlign::End,
v_align=@layout.VerticalAlign::Top,
margin=@core.EdgeInsets::zero(),
)
let portrait_rect : @scene.Rect = {
x: portrait_origin.x,
y: portrait_origin.y,
width: 220,
height: 220,
}
let support_region : @scene.Rect = {
x: sections[2].x,
y: sections[2].y + 48,
width: sections[2].width - 260,
height: sections[2].height - 56,
}
document
.push(
@scene.Element::Block(
id="quote-shell",
rect=frame,
fill=@theme.Color::hex("#fffaf4"),
radius=36,
),
)
.push(
@scene.Element::Block(
id="quote-kicker",
rect={ x: sections[0].x, y: sections[0].y, width: 260, height: 54 },
fill=theme.accent,
radius=14,
),
)
.push(
@scene.Element::Text(
id="quote-kicker-text",
content=content.eyebrow,
position=@scene.Point::at(sections[0].x + 18, sections[0].y + 34),
style={ ..caption_style(theme), color: @theme.Color::hex("#ffffff") },
max_width=220,
),
)
.push_iter(
@textflow.paragraph_to_elements(
id_prefix="quote-title",
region=sections[1],
content=content.title,
style=quote_style,
).iter(),
)
.push(
@scene.Element::Text(
id="quote-author",
content=content.subtitle,
position=@scene.Point::at(
sections[2].x,
sections[2].y + theme.caption_size,
),
style=byline_style,
max_width=support_region.width,
),
)
.push_iter(
@textflow.paragraph_to_elements(
id_prefix="quote-support",
region=support_region,
content=content.body + " " + content.cta,
style=body_style,
).iter(),
)
.push(
@scene.Element::Text(
id="quote-footer",
content=content.footer,
position=@scene.Point::at(
sections[2].x,
sections[2].y + sections[2].height - 18,
),
style=caption_style(theme),
max_width=support_region.width,
),
)
.push(
@scene.Element::ImageSlot(
id="quote-portrait",
asset_id=content.image_asset_id,
rect=portrait_rect,
label=content.image_label,
fit=@scene.ImageFit::Cover,
),
)
}
///|
fn digest_layout(
preset : @preset.PosterPreset,
theme : @theme.Theme,
content : TemplateContent,
) -> @scene.PosterDocument {
let frame = canvas_frame(preset).inset(theme.padding)
let grid = @layout.grid(region=frame, spec=@layout.GridSpec::{
columns: 2,
rows: 3,
gap_x: 24,
gap_y: 24,
padding: @core.EdgeInsets::zero(),
})
let lead : @scene.Rect = {
x: grid[0].x,
y: grid[0].y,
width: grid[0].width,
height: grid[0].height + 24 + grid[2].height,
}
let hero : @scene.Rect = {
x: grid[1].x,
y: grid[0].y,
width: grid[1].width,
height: grid[1].height + 24 + grid[3].height,
}
let footer : @scene.Rect = {
x: grid[4].x,
y: grid[4].y,
width: grid[4].width + 24 + grid[5].width,
height: grid[4].height,
}
let document = @scene.PosterDocument::empty(
title=content.title,
preset~,
theme~,
)
let headline_style = {
..@scene.TextStyle::display(theme),
size: theme.title_size + 4,
line_height: theme.title_size + 20,
color: theme.accent,
}
let body_style = @scene.TextStyle::body(theme)
document
.push(
@scene.Element::Block(
id="digest-shell",
rect=frame,
fill=@theme.Color::hex("#fffef8"),
radius=32,
),
)
.push(
@scene.Element::ImageSlot(
id="digest-hero",
asset_id=content.image_asset_id,
rect=hero,
label=content.image_label,
fit=@scene.ImageFit::Cover,
),
)
.push(
@scene.Element::Text(
id="digest-eyebrow",
content=content.eyebrow,
position=@scene.Point::at(lead.x, lead.y + theme.caption_size),
style=eyebrow_style(theme),
max_width=lead.width,
),
)
.push_iter(
@textflow.paragraph_to_elements(
id_prefix="digest-title",
region={ x: lead.x, y: lead.y + 48, width: lead.width, height: 220 },
content=content.title,
style=headline_style,
).iter(),
)
.push_iter(
@textflow.paragraph_to_elements(
id_prefix="digest-summary",
region={
x: lead.x,
y: lead.y + 280,
width: lead.width,
height: lead.height - 280,
},
content=content.subtitle + " " + content.body,
style=body_style,
).iter(),
)
.push(
@scene.Element::Block(
id="digest-footer-panel",
rect=footer,
fill=@theme.Color::hex("#111111"),
radius=24,
),
)
.push(
@scene.Element::Text(
id="digest-cta",
content=content.cta,
position=@scene.Point::at(
footer.x + 28,
footer.y + theme.caption_size + 8,
),
style={ ..eyebrow_style(theme), color: @theme.Color::hex("#ffffff") },
max_width=footer.width - 56,
),
)
.push(
@scene.Element::Text(
id="digest-footer",
content=content.footer,
position=@scene.Point::at(footer.x + 28, footer.y + 86),
style={ ..caption_style(theme), color: @theme.Color::hex("#f3f3f3") },
max_width=footer.width - 56,
),
)
}
///|
pub fn render_template(
template~ : String,
preset~ : @preset.PosterPreset,
theme~ : @theme.Theme,
content~ : TemplateContent,
) -> @scene.PosterDocument {
match template {
"digest" => digest_layout(preset, theme, content)
"editorial" => editorial_layout(preset, theme, content)
"event" => event_layout(preset, theme, content)
"quote" => quote_layout(preset, theme, content)
_ => launch_layout(preset, theme, content)
}
}