::BASIC_27_Text_Animation_3

BamgasiJM·2025년 10월 24일

Nannou <BASIC>

목록 보기
37/41
post-thumbnail

📝 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; // 5초 (60fps 기준)
const FADE_DURATION_FRAMES: f32 = 120.0; // 2초
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();
    }
}

profile
Coding Art with Blender / oF / Processing / p5.js / nannou

0개의 댓글