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 themes "src/themes"
import ui "src/ui" import ui "src/ui"
TRANSITION_SPEED :: 4 // screen fade-ins per second
// Where Escape takes you from each screen (nil = quit) // Where Escape takes you from each screen (nil = quit)
parent_screen :: proc(screen: themes.Screen) -> Maybe(themes.Screen) { parent_screen :: proc(screen: themes.Screen) -> Maybe(themes.Screen) {
switch screen { switch screen {
@@ -53,6 +55,7 @@ main :: proc() {
if saved, ok := reflect.enum_from_name(memory.Difficulty, profile.data.difficulty); ok { if saved, ok := reflect.enum_from_name(memory.Difficulty, profile.data.difficulty); ok {
difficulty = saved difficulty = saved
} }
transition_t: f32 = 1 // fades the screen in from the background color
running := true running := true
for running && !raylib.WindowShouldClose() { for running && !raylib.WindowShouldClose() {
dt := raylib.GetFrameTime() dt := raylib.GetFrameTime()
@@ -60,6 +63,7 @@ main :: proc() {
if raylib.IsKeyPressed(.ESCAPE) { if raylib.IsKeyPressed(.ESCAPE) {
if parent, ok := parent_screen(screen).?; ok { if parent, ok := parent_screen(screen).?; ok {
screen = parent screen = parent
transition_t = 1
} else { } else {
running = false running = false
} }
@@ -90,6 +94,11 @@ main :: proc() {
case .MEMORY: case .MEMORY:
next = memory.draw() 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() ui.end_frame()
raylib.EndDrawing() raylib.EndDrawing()
free_all(context.temp_allocator) free_all(context.temp_allocator)
@@ -99,6 +108,7 @@ main :: proc() {
if deck, ok := start_deck.?; ok { if deck, ok := start_deck.?; ok {
memory.start(deck, difficulty) memory.start(deck, difficulty)
} }
if next != screen { transition_t = 1 }
screen = next screen = next
} }
} }
+21 -5
View File
@@ -15,6 +15,8 @@ ROUNDNESS :: 0.16
SEGMENTS :: 16 SEGMENTS :: 16
FLIP_SPEED :: 2.6 // flips per second 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 PULSE_DURATION :: 0.35 // seconds
HOVER_LIFT :: 4 HOVER_LIFT :: 4
@@ -28,6 +30,7 @@ Card :: struct {
matched: bool, matched: bool,
flip_t: f32, // 0 = back showing, 1 = face showing flip_t: f32, // 0 = back showing, 1 = face showing
pulse_t: f32, // counts down after a match pulse_t: f32, // counts down after a match
deal_t: f32, // > 0 while being dealt onto the table (0 = resting)
face_art: raylib.RenderTexture2D, face_art: raylib.RenderTexture2D,
} }
@@ -214,6 +217,14 @@ update :: proc(card: ^Card, dt: f32) {
if card.pulse_t > 0 { if card.pulse_t > 0 {
card.pulse_t = max(card.pulse_t - dt, 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) { 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) hovered := mouse_over || (clickable && focused)
if hovered { lift += HOVER_LIFT } 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 { if card.pulse_t > 0 {
progress := 1 - card.pulse_t / PULSE_DURATION progress := 1 - card.pulse_t / PULSE_DURATION
scale += 0.09 * math.sin(progress * math.PI) 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 width := card.rect.width * scale
height := card.rect.height * 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 stays on the table and spreads as the card lifts
shadow := raylib.Rectangle{ shadow := raylib.Rectangle{
center.x - width * squash / 2 + 2, 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), max(width * squash, 2),
height, 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 // Darken the card while it is edge-on, to fake lighting
shade := u8(255 - (1 - squash) * 70) 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 { if hovered && !showing_face {
tint = raylib.ColorBrightness(tint, 0.08) 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_x := (themes.SCREEN_WIDTH - total_w) / 2
start_y := themes.GRID_TOP + (themes.SCREEN_HEIGHT - 16 - themes.GRID_TOP - total_h) / 2 start_y := themes.GRID_TOP + (themes.SCREEN_HEIGHT - 16 - themes.GRID_TOP - total_h) / 2
for &c, i in active_cards() { for &c, i in active_cards() {
card.start_deal(&c, i)
c.rect = { c.rect = {
start_x + f32(i % layout.cols) * step_x, start_x + f32(i % layout.cols) * step_x,
start_y + f32(i / layout.cols) * step_y, start_y + f32(i / layout.cols) * step_y,
@@ -134,6 +135,11 @@ unload :: proc() {
for &c in cards { card.unload_face(&c) } 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 { can_pick :: proc(i: int) -> bool {
if i < 0 || i >= card_count { return false } if i < 0 || i >= card_count { return false }
c := &cards[i] c := &cards[i]
@@ -190,7 +196,7 @@ handle_keyboard :: proc() {
} }
if raylib.IsKeyPressed(.ENTER) || raylib.IsKeyPressed(.SPACE) { if raylib.IsKeyPressed(.ENTER) || raylib.IsKeyPressed(.SPACE) {
keyboard_active = true keyboard_active = true
select_card(focus) if can_interact(focus) { select_card(focus) }
} }
} }
@@ -236,7 +242,7 @@ update :: proc(dt: f32) {
elapsed += dt elapsed += dt
if raylib.IsMouseButtonPressed(.LEFT) { if raylib.IsMouseButtonPressed(.LEFT) {
for &c, i in active_cards() { 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 break
} }
} }
@@ -271,7 +277,7 @@ draw :: proc() -> themes.Screen {
} }
for &c, i in active_cards() { 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 { if state == .OVER {