
📝 Rust Code
use nannou::prelude::*;
use std::fs;
use std::path::Path;
const WINDOW_SIZE: u32 = 1080;
const LINE_HEIGHT: f32 = 50.0;
const START_Y: f32 = -540.0;
const SCROLL_SPEED: f32 = 1.0;
const TITLE_FADE_SPEED: f32 = 0.01;
const END_DELAY_FRAMES: u32 = 300;
const FADE_DURATION_FRAMES: f32 = 120.0;
const LAST_LINE_INDEX: usize = 17;
const TITLE_TEXTS: [&str; 3] = [
"He Wishes for the Cloths of Heaven",
"by W.B. Yeats",
"<CLICK TO START>",
];
const POEM_LINES: [&str; 17] = [
"",
"Had I the heavens' embroidered cloths,",
"",
"Enwrought with golden and silver light,",
"",
"The blue and the dim and the dark cloths",
"",
"Of night and light and the half-light,",
"",
"I would spread the cloths under your feet:",
"",
"But I, being poor, have only my dreams;",
"",
"I have spread my dreams under your feet;",
"",
"Tread softly because you tread on my dreams.",
"",
];
struct Model {
font: text::Font,
title_font: text::Font,
scroll_offset: f32,
title_opacity: f32,
is_scrolling: bool,
poem_ended: bool,
end_frame_count: u32,
}
fn main() {
nannou::app(model).update(update).run();
}
fn model(app: &App) -> Model {
app.new_window()
.size(WINDOW_SIZE, WINDOW_SIZE)
.view(view)
.mouse_pressed(mouse_pressed)
.build()
.unwrap();
let assets_path = app.assets_path().expect("assets path");
let title_font = load_font(&assets_path, "font.ttf");
let font = load_font(&assets_path, "font1.ttf");
Model {
font,
title_font,
scroll_offset: 0.0,
title_opacity: 1.0,
is_scrolling: false,
poem_ended: false,
end_frame_count: 0,
}
}
fn load_font(assets_path: &Path, filename: &str) -> text::Font {
let font_path = assets_path.join("fonts").join(filename);
let bytes = fs
::read(&font_path)
.unwrap_or_else(|_| panic!("Failed to read font file: {:?}", font_path));
text::Font::from_bytes(bytes).unwrap_or_else(|_| panic!("Failed to parse font: {}", filename))
}
fn mouse_pressed(_app: &App, model: &mut Model, _button: MouseButton) {
model.is_scrolling = true;
}
fn update(_app: &App, model: &mut Model, _update: Update) {
if model.is_scrolling && !model.poem_ended {
model.scroll_offset -= SCROLL_SPEED;
model.title_opacity = (model.title_opacity - TITLE_FADE_SPEED).max(0.0);
let last_line_y = START_Y - (LAST_LINE_INDEX as f32) * LINE_HEIGHT - model.scroll_offset;
if last_line_y > 540.0 {
model.poem_ended = true;
model.end_frame_count = 0;
}
}
if model.poem_ended {
model.end_frame_count += 1;
}
}
fn view(app: &App, model: &Model, frame: Frame) {
let draw = app.draw();
draw.background().color(rgb(0.02, 0.02, 0.04));
draw_title(&draw, model);
if !model.poem_ended {
draw_poem(&draw, model);
}
draw_thanks(&draw, model);
draw.to_frame(app, &frame).unwrap();
}
fn draw_title(draw: &Draw, model: &Model) {
if model.title_opacity <= 0.0 {
return;
}
let color = rgba(0.8, 0.6, 0.3, model.title_opacity);
let font = &model.title_font;
let font_small = &model.font;
draw.text(TITLE_TEXTS[0])
.font(font.clone())
.font_size(96)
.justify(text::Justify::Center)
.xy(pt2(0.0, 30.0))
.color(color)
.no_line_wrap();
draw.text(TITLE_TEXTS[1])
.font(font.clone())
.font_size(52)
.justify(text::Justify::Center)
.xy(pt2(0.0, -70.0))
.color(color)
.no_line_wrap();
draw.text(TITLE_TEXTS[2])
.font(font_small.clone())
.font_size(24)
.justify(text::Justify::Center)
.xy(pt2(0.0, -400.0))
.color(rgba(1.0, 1.0, 1.0, 0.4))
.no_line_wrap();
}
fn draw_poem(draw: &Draw, model: &Model) {
for (i, line) in POEM_LINES.iter().enumerate() {
let y_pos = START_Y - (i as f32) * LINE_HEIGHT - model.scroll_offset;
draw.text(line)
.font(model.font.clone())
.font_size(28)
.justify(text::Justify::Center)
.xy(pt2(0.0, y_pos))
.color(WHITE)
.no_line_wrap();
}
}
fn draw_thanks(draw: &Draw, model: &Model) {
if model.poem_ended && model.end_frame_count >= END_DELAY_FRAMES {
let fade_progress = (
((model.end_frame_count - END_DELAY_FRAMES) as f32) / FADE_DURATION_FRAMES
).min(1.0);
draw.text("Thanks for watching")
.font(model.font.clone())
.font_size(36)
.justify(text::Justify::Center)
.xy(pt2(0.0, 0.0))
.color(rgba(1.0, 1.0, 1.0, fade_progress))
.no_line_wrap();
}
}