Add screen fade-in and card deal-in animations

Cards slide in one after another when a game starts and can't be picked
until they have landed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
SansanandClaude Opus 5.5 committed 2026-10-01 21:36:54 +02:00
1 parent 83ff9511ec
commit 3794a9e2e9
4 files changed
+1751 -1159

No files matched your search

File diff suppressed because it is too large. Load diff
+10
View File
@@ -13,6 +13,8 @@ import screens "src/screens"
import themes "src/themes"
import ui "src/ui"
TRANSITION_SPEED :: 4 // screen fade-ins per second
// Where Escape takes you from each screen (nil = quit)
parent_screen :: proc(screen: themes.Screen) -> Maybe(themes.Screen) {
switch screen {
@@ -53,6 +55,7 @@ main :: proc() {
if saved, ok := reflect.enum_from_name(memory.Difficulty, profile.data.difficulty); ok {
difficulty = saved
}
transition_t: f32 = 1 // fades the screen in from the background color
running := true
for running && !raylib.WindowShouldClose() {
dt := raylib.GetFrameTime()
@@ -60,6 +63,7 @@ main :: proc() {
if raylib.IsKeyPressed(.ESCAPE) {
if parent, ok := parent_screen(screen).?; ok {
screen = parent
transition_t = 1
} else {
running = false
}
@@ -90,6 +94,11 @@ main :: proc() {
case .MEMORY:
next = memory.draw()
}
if transition_t > 0 {
alpha := transition_t * transition_t
raylib.DrawRectangle(0, 0, themes.SCREEN_WIDTH, themes.SCREEN_HEIGHT, raylib.Fade(themes.MENU_BACKGROUND, alpha))
transition_t = max(transition_t - dt * TRANSITION_SPEED, 0)
}
ui.end_frame()
raylib.EndDrawing()
free_all(context.temp_allocator)
@@ -99,6 +108,7 @@ main :: proc() {
if deck, ok := start_deck.?; ok {
memory.start(deck, difficulty)
}
if next != screen { transition_t = 1 }
screen = next
}
}
+21 -5
View File
@@ -15,6 +15,8 @@ ROUNDNESS :: 0.16
SEGMENTS :: 16
FLIP_SPEED :: 2.6 // flips per second
DEAL_SPEED :: 3.5 // how fast a dealt card settles
DEAL_STAGGER :: 0.12 // delay between consecutive cards, in deal_t units
PULSE_DURATION :: 0.35 // seconds
HOVER_LIFT :: 4
@@ -28,6 +30,7 @@ Card :: struct {
matched: bool,
flip_t: f32, // 0 = back showing, 1 = face showing
pulse_t: f32, // counts down after a match
deal_t: f32, // > 0 while being dealt onto the table (0 = resting)
face_art: raylib.RenderTexture2D,
}
@@ -214,6 +217,14 @@ update :: proc(card: ^Card, dt: f32) {
if card.pulse_t > 0 {
card.pulse_t = max(card.pulse_t - dt, 0)
}
if card.deal_t > 0 {
card.deal_t = max(card.deal_t - dt * DEAL_SPEED, 0)
}
}
// Starts the deal-in animation; later cards in the deal arrive later
start_deal :: proc(card: ^Card, order: int) {
card.deal_t = 1 + f32(order) * DEAL_STAGGER
}
start_pulse :: proc(card: ^Card) {
@@ -241,7 +252,12 @@ draw :: proc(card: ^Card, theme: themes.Theme, clickable: bool, focused := false
hovered := mouse_over || (clickable && focused)
if hovered { lift += HOVER_LIFT }
scale: f32 = 1
// Dealing: slide up from below and grow in
dealt := smoothstep(1 - clamp(card.deal_t, 0, 1))
if dealt == 0 { return }
deal_offset := (1 - dealt) * 60
scale: f32 = 0.85 + 0.15 * dealt
if card.pulse_t > 0 {
progress := 1 - card.pulse_t / PULSE_DURATION
scale += 0.09 * math.sin(progress * math.PI)
@@ -249,20 +265,20 @@ draw :: proc(card: ^Card, theme: themes.Theme, clickable: bool, focused := false
width := card.rect.width * scale
height := card.rect.height * scale
center := raylib.Vector2{card.rect.x + card.rect.width / 2, card.rect.y + card.rect.height / 2 - lift}
center := raylib.Vector2{card.rect.x + card.rect.width / 2, card.rect.y + card.rect.height / 2 - lift + deal_offset}
// Shadow stays on the table and spreads as the card lifts
shadow := raylib.Rectangle{
center.x - width * squash / 2 + 2,
card.rect.y + card.rect.height * (1 - scale) / 2 + 4 + lift * 0.15,
card.rect.y + card.rect.height * (1 - scale) / 2 + 4 + lift * 0.15 + deal_offset,
max(width * squash, 2),
height,
}
raylib.DrawRectangleRounded(shadow, ROUNDNESS, SEGMENTS, raylib.Fade(themes.BLACK, 0.12 - lift * 0.004))
raylib.DrawRectangleRounded(shadow, ROUNDNESS, SEGMENTS, raylib.Fade(themes.BLACK, (0.12 - lift * 0.004) * dealt))
// Darken the card while it is edge-on, to fake lighting
shade := u8(255 - (1 - squash) * 70)
tint := raylib.Color{shade, shade, shade, 255}
tint := raylib.Color{shade, shade, shade, u8(255 * dealt)}
if hovered && !showing_face {
tint = raylib.ColorBrightness(tint, 0.08)
}
+9 -3
View File
@@ -106,6 +106,7 @@ deal :: proc(deck_id: decks.Deck_Id, game_difficulty: Difficulty) {
start_x := (themes.SCREEN_WIDTH - total_w) / 2
start_y := themes.GRID_TOP + (themes.SCREEN_HEIGHT - 16 - themes.GRID_TOP - total_h) / 2
for &c, i in active_cards() {
card.start_deal(&c, i)
c.rect = {
start_x + f32(i % layout.cols) * step_x,
start_y + f32(i / layout.cols) * step_y,
@@ -134,6 +135,11 @@ unload :: proc() {
for &c in cards { card.unload_face(&c) }
}
// Whether the player can click or press on card i right now (it must have finished being dealt)
can_interact :: proc(i: int) -> bool {
return can_pick(i) && cards[i].deal_t == 0
}
can_pick :: proc(i: int) -> bool {
if i < 0 || i >= card_count { return false }
c := &cards[i]
@@ -190,7 +196,7 @@ handle_keyboard :: proc() {
}
if raylib.IsKeyPressed(.ENTER) || raylib.IsKeyPressed(.SPACE) {
keyboard_active = true
select_card(focus)
if can_interact(focus) { select_card(focus) }
}
}
@@ -236,7 +242,7 @@ update :: proc(dt: f32) {
elapsed += dt
if raylib.IsMouseButtonPressed(.LEFT) {
for &c, i in active_cards() {
if card.is_hovered(&c) && select_card(i) {
if card.is_hovered(&c) && can_interact(i) && select_card(i) {
break
}
}
@@ -271,7 +277,7 @@ draw :: proc() -> themes.Screen {
}
for &c, i in active_cards() {
card.draw(&c, theme, can_pick(i), focused = keyboard_active && i == focus && state != .OVER)
card.draw(&c, theme, can_interact(i), focused = keyboard_active && i == focus && state != .OVER && c.deal_t == 0)
}
if state == .OVER {