package card import "core:math" import raylib "vendor:raylib" import assets "../assets" import themes "../themes" import ui "../ui" // Card art is rendered once into textures at 2x size and scaled down when drawn. // This lets the flip animation squash the whole card (text included) horizontally. ART_SCALE :: 2 ART_WIDTH :: themes.CARD_WIDTH * ART_SCALE ART_HEIGHT :: themes.CARD_HEIGHT * ART_SCALE 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 Card :: struct { rect: raylib.Rectangle, // resting position on screen text: cstring, hint: cstring, // small second line on reading cards is_symbol: bool, // script symbol (vs. its reading) pair_id: int, face_up: bool, // logical state; the animation follows it 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, } @(private) back_art: [themes.Flag]raylib.RenderTexture2D // --------------------------------------------------------------------------- // Art generation // --------------------------------------------------------------------------- @(private) art_rect :: proc(inset: f32 = 0) -> raylib.Rectangle { return {inset, inset, ART_WIDTH - 2 * inset, ART_HEIGHT - 2 * inset} } @(private) new_art_texture :: proc(edge_color: raylib.Color) -> raylib.RenderTexture2D { rt := raylib.LoadRenderTexture(ART_WIDTH, ART_HEIGHT) raylib.SetTextureFilter(rt.texture, .BILINEAR) raylib.BeginTextureMode(rt) // Transparent, but with the edge's RGB so filtering doesn't create dark fringes raylib.ClearBackground({edge_color.r, edge_color.g, edge_color.b, 0}) raylib.EndTextureMode() return rt } @(private) draw_frame :: proc(fill, border: raylib.Color) { raylib.DrawRectangleRounded(art_rect(), ROUNDNESS, SEGMENTS, fill) raylib.DrawRectangleRoundedLinesEx(art_rect(3), ROUNDNESS, SEGMENTS, 6, border) } // Japan: Hinomaru, a crimson sun disc on white, with a double crimson border @(private) draw_japan_back :: proc() { draw_frame(themes.WHITE, themes.HINOMARU_RED) raylib.DrawRectangleRoundedLinesEx(art_rect(16), ROUNDNESS, SEGMENTS, 2, themes.HINOMARU_RED) center := raylib.Vector2{ART_WIDTH / 2, ART_HEIGHT / 2} raylib.DrawCircleV(center, ART_WIDTH * 0.3, themes.HINOMARU_RED) } // One Taegukgi trigram: three bars, each either solid or broken in the middle @(private) draw_trigram :: proc(center: raylib.Vector2, angle_deg: f32, solid: [3]bool, scale: f32) { length := 46 * scale thick := 8 * scale gap := 6 * scale rad := angle_deg * math.RAD_PER_DEG along := raylib.Vector2{math.cos(rad), math.sin(rad)} // bar direction across := raylib.Vector2{-along.y, along.x} // stacking direction for i in 0 ..< 3 { bar_center := center + across * (f32(i - 1) * (thick + gap)) if solid[i] { raylib.DrawRectanglePro({bar_center.x, bar_center.y, length, thick}, {length / 2, thick / 2}, angle_deg, themes.BLACK) } else { half := (length - gap) / 2 for side in ([2]f32{-1, 1}) { c := bar_center + along * (side * (half + gap) / 2) raylib.DrawRectanglePro({c.x, c.y, half, thick}, {half / 2, thick / 2}, angle_deg, themes.BLACK) } } } } // Korea: Taegukgi, a red/blue taeguk circle with the four black trigrams in the corners @(private) draw_korea_back :: proc() { draw_frame(themes.WHITE, themes.TAEGUK_BLUE) center := raylib.Vector2{ART_WIDTH / 2, ART_HEIGHT / 2} radius: f32 = ART_WIDTH * 0.26 // The flag's taeguk is tilted along the diagonal of a 3:2 rectangle tilt := math.atan2(f32(2), 3) * math.DEG_PER_RAD raylib.DrawCircleSector(center, radius, 180 + tilt, 360 + tilt, 48, themes.TAEGUK_RED) raylib.DrawCircleSector(center, radius, tilt, 180 + tilt, 48, themes.TAEGUK_BLUE) offset := raylib.Vector2{math.cos(tilt * math.RAD_PER_DEG), math.sin(tilt * math.RAD_PER_DEG)} * (radius / 2) // Red head toward the upper-left (geon ☰), blue head toward the lower-right (gon ☷) raylib.DrawCircleV(center - offset, radius / 2, themes.TAEGUK_RED) raylib.DrawCircleV(center + offset, radius / 2, themes.TAEGUK_BLUE) scale: f32 = 0.8 dx: f32 = ART_WIDTH * 0.24 dy: f32 = ART_HEIGHT * 0.26 draw_trigram(center + {-dx, -dy}, -45, {true, true, true}, scale) // geon ☰ draw_trigram(center + {dx, -dy}, 45, {false, true, false}, scale) // gam ☵ draw_trigram(center + {-dx, dy}, 45, {true, false, true}, scale) // ri ☲ draw_trigram(center + {dx, dy}, -45, {false, false, false}, scale) // gon ☷ } // Thailand: Thong Trairong, red-white-blue-white-red stripes (blue twice as wide) @(private) draw_thailand_back :: proc() { draw_frame(themes.WHITE, themes.THAI_BLUE) panel := art_rect(18) unit := panel.height / 6 stripes := [5]struct{ color: raylib.Color, units: f32 }{ {themes.THAI_RED, 1}, {themes.WHITE, 1}, {themes.THAI_BLUE, 2}, {themes.WHITE, 1}, {themes.THAI_RED, 1}, } y := panel.y for stripe in stripes { raylib.DrawRectangleRec({panel.x, y, panel.width, unit * stripe.units}, stripe.color) y += unit * stripe.units } raylib.DrawRectangleLinesEx(panel, 2, themes.THAI_BLUE) } // Builds the card back textures. Call once after the window is created. load_art :: proc() { edges := [themes.Flag]raylib.Color{.JAPAN = themes.HINOMARU_RED, .KOREA = themes.TAEGUK_BLUE, .THAILAND = themes.THAI_BLUE} for flag in themes.Flag { back_art[flag] = new_art_texture(edges[flag]) raylib.BeginTextureMode(back_art[flag]) switch flag { case .JAPAN: draw_japan_back() case .KOREA: draw_korea_back() case .THAILAND: draw_thailand_back() } raylib.EndTextureMode() } } unload_art :: proc() { for rt in back_art { raylib.UnloadRenderTexture(rt) } } get_back_art :: proc(flag: themes.Flag) -> raylib.Texture2D { return back_art[flag].texture } // Font size that fits text inside the card width @(private) fit_size :: proc(text: cstring, size: f32, style: assets.Font_Style) -> f32 { max_width: f32 = ART_WIDTH * 0.78 width := ui.measure_text(text, size, style).x return width > max_width ? size * max_width / width : size } // Renders the card's face into its own texture. Symbols use their script's font. build_face :: proc(card: ^Card, theme: themes.Theme, symbol_style: assets.Font_Style) { unload_face(card) card.face_art = new_art_texture(theme.primary) raylib.BeginTextureMode(card.face_art) draw_frame(theme.card_face, theme.primary) raylib.DrawRectangleRoundedLinesEx(art_rect(16), ROUNDNESS, SEGMENTS, 2, raylib.Fade(theme.primary, 0.35)) if card.is_symbol { ui.draw_text_in_rect(card.text, art_rect(), fit_size(card.text, 132, symbol_style), theme.ink, symbol_style) } else if card.hint == nil { ui.draw_text_in_rect(card.text, art_rect(), fit_size(card.text, 96, .BOLD), theme.primary, .BOLD) } else { // Reading on top, hint underneath main_rect := art_rect() main_rect.height -= 56 ui.draw_text_in_rect(card.text, main_rect, fit_size(card.text, 88, .BOLD), theme.primary, .BOLD) hint_rect := raylib.Rectangle{0, ART_HEIGHT * 0.62, ART_WIDTH, 60} ui.draw_text_in_rect(card.hint, hint_rect, fit_size(card.hint, 34, .REGULAR), theme.ink, .REGULAR) } raylib.EndTextureMode() } unload_face :: proc(card: ^Card) { if card.face_art.id != 0 { raylib.UnloadRenderTexture(card.face_art) card.face_art = {} } } // --------------------------------------------------------------------------- // Behaviour // --------------------------------------------------------------------------- is_animating :: proc(card: ^Card) -> bool { target: f32 = card.face_up ? 1 : 0 return card.flip_t != target } update :: proc(card: ^Card, dt: f32) { target: f32 = card.face_up ? 1 : 0 step := dt * FLIP_SPEED if card.flip_t < target { card.flip_t = min(card.flip_t + step, target) } else if card.flip_t > target { card.flip_t = max(card.flip_t - step, target) } 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) { card.pulse_t = PULSE_DURATION } is_hovered :: proc(card: ^Card) -> bool { return raylib.CheckCollisionPointRec(raylib.GetMousePosition(), card.rect) } @(private) smoothstep :: proc(t: f32) -> f32 { return t * t * (3 - 2 * t) } draw :: proc(card: ^Card, theme: themes.Theme, clickable: bool, focused := false) { eased := smoothstep(card.flip_t) squash := abs(math.cos(eased * math.PI)) // 1 → 0 → 1 across the flip showing_face := eased >= 0.5 // Cards lift as they turn, and a little on hover lift := math.sin(eased * math.PI) * 6 mouse_over := clickable && is_hovered(card) if mouse_over { ui.set_hovered() } hovered := mouse_over || (clickable && focused) if hovered { lift += HOVER_LIFT } // 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) } 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 + 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 + deal_offset, max(width * squash, 2), height, } 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, u8(255 * dealt)} if hovered && !showing_face { tint = raylib.ColorBrightness(tint, 0.08) } texture := showing_face ? card.face_art.texture : back_art[theme.flag].texture source := raylib.Rectangle{0, 0, f32(texture.width), -f32(texture.height)} // render textures are upside down dest := raylib.Rectangle{center.x, center.y, width * squash, height} raylib.DrawTexturePro(texture, source, dest, {dest.width / 2, dest.height / 2}, 0, tint) if focused { ring := raylib.Rectangle{center.x - width * squash / 2 - 6, center.y - height / 2 - 6, width * squash + 12, height + 12} raylib.DrawRectangleRoundedLinesEx(ring, ROUNDNESS, SEGMENTS, 3, raylib.Fade(theme.ink, 0.75)) } if card.matched && card.flip_t == 1 { outline := raylib.Rectangle{center.x - width / 2 - 3, center.y - height / 2 - 3, width + 6, height + 6} raylib.DrawRectangleRoundedLinesEx(outline, ROUNDNESS, SEGMENTS, 3, theme.accent) } }