feat: IdleCompanion widget (v0.25.0)
Mascota animada reutilizable que aparece tras inactividad:
- Se asoma por bordes de paneles aleatorios
- Clipping correcto (respeta límites del panel)
- Ojos que miran izq/der, salto de pánico
- Estados: hidden → peeking → watching → hiding
- Diseño gato: orejas puntiagudas, pupilas verticales, mejillas
Uso:
const IdleCompanion = zcatgui.widgets.idle_companion;
var state: IdleCompanion.State = .{};
IdleCompanion.draw(ctx, &panels, &state, last_activity, color);
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
d8f04f85bc
commit
3af97f6174
4 changed files with 528 additions and 3 deletions
28
CHANGELOG.md
28
CHANGELOG.md
|
|
@ -44,6 +44,7 @@
|
|||
| 2025-12-19 | v0.22.2 | Cursor blink rate: 500ms→300ms (más responsive durante edición) |
|
||||
| 2025-12-30 | v0.23.0 | ⭐ FilledTriangle primitive: scanline rasterizer for 3D graphics |
|
||||
| 2025-12-30 | v0.24.0 | ⭐ FilledCircle primitive: Midpoint Circle Algorithm (Bresenham) |
|
||||
| 2025-12-30 | v0.25.0 | ⭐ IdleCompanion widget: mascota animada que aparece tras inactividad |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -86,3 +87,30 @@ Nuevas primitivas para gráficos 2D y mascotas animadas:
|
|||
|
||||
→ Archivos: `core/command.zig`, `render/software.zig`
|
||||
→ Doc: `zsimifactu/docs/PLAN_CIRCULOS_Y_ZCAT_2025-12-30.md`
|
||||
|
||||
### v0.25.0 - IdleCompanion Widget (2025-12-30)
|
||||
Sistema de mascota animada reutilizable para cualquier aplicación:
|
||||
|
||||
- **Características:**
|
||||
- Aparece tras periodo configurable de inactividad (default: 15s)
|
||||
- Se asoma por bordes de paneles/rectángulos aleatorios
|
||||
- Animación suave con clipping correcto (respeta bordes)
|
||||
- Ojos que miran a los lados (izq/centro/der)
|
||||
- "Salto de pánico" al detectar actividad del usuario
|
||||
- Diseño de gato con orejas puntiagudas, pupilas verticales, mejillas y bigotes
|
||||
|
||||
- **Estados de animación:**
|
||||
- `hidden`: Esperando idle
|
||||
- `peeking`: Asomándose lentamente (4s)
|
||||
- `watching`: Completamente visible, mirando alrededor (3s)
|
||||
- `hiding`: Bajando normal (1s) o salto de pánico (0.2s)
|
||||
|
||||
- **Uso:**
|
||||
```zig
|
||||
const IdleCompanion = zcatgui.widgets.idle_companion;
|
||||
var state: IdleCompanion.State = .{};
|
||||
const panels = [_]IdleCompanion.Rect{ ... };
|
||||
IdleCompanion.draw(ctx, &panels, &state, last_activity, color);
|
||||
```
|
||||
|
||||
→ Archivo: `widgets/idle_companion.zig`
|
||||
|
|
|
|||
20
CLAUDE.md
20
CLAUDE.md
|
|
@ -247,9 +247,9 @@ zcatgui/
|
|||
|
||||
---
|
||||
|
||||
## ESTADO ACTUAL (v0.21.2)
|
||||
## ESTADO ACTUAL (v0.25.0)
|
||||
|
||||
### Widgets (37 total)
|
||||
### Widgets (38 total)
|
||||
|
||||
| Categoría | Widgets |
|
||||
|-----------|---------|
|
||||
|
|
@ -258,7 +258,7 @@ zcatgui/
|
|||
| **Datos** | List, Table, Tree, ReorderableList, VirtualScroll |
|
||||
| **Feedback** | Progress, Tooltip, Toast, Spinner |
|
||||
| **Input avanzado** | AutoComplete, Select, TextArea, ColorPicker, DatePicker |
|
||||
| **Especial** | Image, Icon, Canvas, Chart, RichText |
|
||||
| **Especial** | Image, Icon, Canvas, Chart, RichText, **IdleCompanion** |
|
||||
| **Navegación** | Breadcrumb, Focus, Badge/TagGroup |
|
||||
|
||||
### Backends (5 plataformas)
|
||||
|
|
@ -285,6 +285,20 @@ zcatgui/
|
|||
|
||||
## HITOS RECIENTES
|
||||
|
||||
### IdleCompanion Widget ✅ (2025-12-30)
|
||||
Mascota animada que aparece tras inactividad del usuario:
|
||||
- Se asoma por bordes de paneles aleatorios
|
||||
- Ojos que miran a los lados, orejas con movimiento
|
||||
- Salto de pánico al detectar actividad
|
||||
- Clipping correcto (respeta límites del panel)
|
||||
→ Archivo: `widgets/idle_companion.zig`
|
||||
|
||||
### Primitivas Gráficas 2D ✅ (2025-12-30)
|
||||
Nuevas primitivas para formas orgánicas:
|
||||
- **FilledTriangle**: Rasterización por scanlines (v0.23.0)
|
||||
- **FilledCircle**: Algoritmo Midpoint/Bresenham (v0.24.0)
|
||||
→ Archivos: `core/command.zig`, `render/software.zig`
|
||||
|
||||
### Refactorización Modular ✅ (2025-12-29)
|
||||
Archivos grandes modularizados en carpetas:
|
||||
- `autocomplete/` (910→571 LOC hub, -37%): state, types, filtering
|
||||
|
|
|
|||
474
src/widgets/idle_companion.zig
Normal file
474
src/widgets/idle_companion.zig
Normal file
|
|
@ -0,0 +1,474 @@
|
|||
//! Idle Companion System - Zcat "The Drifter"
|
||||
//!
|
||||
//! Un sistema de mascota animada que aparece cuando el usuario está inactivo.
|
||||
//! El gatito se asoma por los bordes de rectángulos definidos, mira alrededor,
|
||||
//! y desaparece con un "salto de pánico" si detecta actividad.
|
||||
//!
|
||||
//! ## Características
|
||||
//! - Aparece tras periodo configurable de inactividad (default: 15s)
|
||||
//! - Se asoma por bordes de paneles/rectángulos aleatorios
|
||||
//! - Animación suave con clipping correcto
|
||||
//! - Ojos que miran a los lados
|
||||
//! - "Salto de pánico" al detectar actividad del usuario
|
||||
//! - Diseño de gato con orejas puntiagudas, pupilas verticales, mejillas y bigotes
|
||||
//!
|
||||
//! ## Uso básico
|
||||
//! ```zig
|
||||
//! const zcatgui = @import("zcatgui");
|
||||
//! const IdleCompanion = zcatgui.widgets.IdleCompanion;
|
||||
//!
|
||||
//! // Estado persistente
|
||||
//! var companion_state: IdleCompanion.State = .{};
|
||||
//!
|
||||
//! // Lista de rectángulos donde puede aparecer
|
||||
//! const panels = [_]zcatgui.Rect{
|
||||
//! .{ .x = 0, .y = 50, .w = 300, .h = 400 },
|
||||
//! .{ .x = 304, .y = 50, .w = 300, .h = 200 },
|
||||
//! };
|
||||
//!
|
||||
//! // En el loop de renderizado:
|
||||
//! IdleCompanion.draw(ctx, &panels, &companion_state, last_activity_timestamp, theme.primary);
|
||||
//! ```
|
||||
//!
|
||||
//! ## Personalización
|
||||
//! Los tiempos son configurables mediante constantes en State:
|
||||
//! - IDLE_THRESHOLD_MS: Tiempo de inactividad para aparecer (15s)
|
||||
//! - PEEK_DURATION_MS: Duración del asomarse (4s)
|
||||
//! - WATCH_DURATION_MS: Duración mirando alrededor (3s)
|
||||
//! - HIDE_DURATION_MS: Duración al esconderse (1s)
|
||||
//! - PANIC_HIDE_DURATION_MS: Duración del salto de pánico (0.2s)
|
||||
//! - RELOCATE_DELAY_MS: Pausa antes de reaparecer (5s)
|
||||
|
||||
const std = @import("std");
|
||||
const Context = @import("../core/context.zig").Context;
|
||||
const Style = @import("../core/style.zig");
|
||||
const Color = Style.Color;
|
||||
|
||||
/// Rectángulo (compatible con zcatgui.Rect)
|
||||
pub const Rect = struct {
|
||||
x: i32,
|
||||
y: i32,
|
||||
w: u32,
|
||||
h: u32,
|
||||
};
|
||||
|
||||
/// Estado de animación del compañero
|
||||
pub const AnimState = enum {
|
||||
/// No visible, esperando idle
|
||||
hidden,
|
||||
/// Asomándose lentamente, mirando izq/der
|
||||
peeking,
|
||||
/// Completamente visible, mirando alrededor
|
||||
watching,
|
||||
/// Bajando (normal o salto de pánico)
|
||||
hiding,
|
||||
};
|
||||
|
||||
/// Estado completo del sistema Idle Companion
|
||||
pub const State = struct {
|
||||
/// Estado actual de la animación
|
||||
anim_state: AnimState = .hidden,
|
||||
|
||||
/// Índice del panel/rectángulo donde está el compañero
|
||||
panel_index: usize = 0,
|
||||
|
||||
/// Posición X relativa al panel (0.0 - 1.0)
|
||||
x_ratio: f32 = 0.5,
|
||||
|
||||
/// Progreso de aparición (0 = oculto, 1 = completamente visible)
|
||||
appear_progress: f32 = 0.0,
|
||||
|
||||
/// Dirección de mirada (-1 = izq, 0 = centro, 1 = der)
|
||||
look_dir: i8 = 0,
|
||||
|
||||
/// Tiempo en que entró al estado actual
|
||||
state_start_time: i64 = 0,
|
||||
|
||||
/// Fase de animación general (para orejas)
|
||||
anim_phase: f32 = 0,
|
||||
|
||||
/// Si el usuario causó actividad durante la animación (salto de pánico)
|
||||
panic_jump: bool = false,
|
||||
|
||||
/// Último timestamp de actividad registrado
|
||||
last_known_activity: i64 = 0,
|
||||
|
||||
/// Semilla para números pseudo-aleatorios (LCG simple)
|
||||
random_seed: u32 = 12345,
|
||||
|
||||
// =========================================================================
|
||||
// Constantes de tiempo (personalizables)
|
||||
// =========================================================================
|
||||
|
||||
/// Tiempo de inactividad para aparecer (ms)
|
||||
pub const IDLE_THRESHOLD_MS: i64 = 15_000;
|
||||
/// Duración del estado peeking (ms)
|
||||
pub const PEEK_DURATION_MS: i64 = 4_000;
|
||||
/// Duración del estado watching (ms)
|
||||
pub const WATCH_DURATION_MS: i64 = 3_000;
|
||||
/// Duración al esconderse normal (ms)
|
||||
pub const HIDE_DURATION_MS: i64 = 1_000;
|
||||
/// Duración del salto de pánico (ms)
|
||||
pub const PANIC_HIDE_DURATION_MS: i64 = 200;
|
||||
/// Pausa antes de reaparecer en otro lugar (ms)
|
||||
pub const RELOCATE_DELAY_MS: i64 = 5_000;
|
||||
|
||||
/// Generador de números pseudo-aleatorios simple (LCG)
|
||||
fn nextRandom(self: *State) u32 {
|
||||
self.random_seed = self.random_seed *% 1103515245 +% 12345;
|
||||
return self.random_seed >> 16;
|
||||
}
|
||||
|
||||
/// Random float 0.0 - 1.0
|
||||
fn randomFloat(self: *State) f32 {
|
||||
return @as(f32, @floatFromInt(self.nextRandom() % 10000)) / 10000.0;
|
||||
}
|
||||
|
||||
/// Elige un panel y posición aleatorios
|
||||
fn pickRandomLocation(self: *State, num_panels: usize) void {
|
||||
if (num_panels == 0) return;
|
||||
|
||||
// Panel aleatorio
|
||||
self.panel_index = self.nextRandom() % @as(u32, @intCast(num_panels));
|
||||
|
||||
// Posición X aleatoria (25% - 75% del ancho)
|
||||
self.x_ratio = 0.25 + self.randomFloat() * 0.5;
|
||||
|
||||
// Resetear
|
||||
self.appear_progress = 0.0;
|
||||
self.look_dir = 0;
|
||||
}
|
||||
|
||||
/// Actualiza el estado según el tiempo de inactividad
|
||||
pub fn update(self: *State, last_activity: i64, num_panels: usize) void {
|
||||
const now = std.time.milliTimestamp();
|
||||
const idle_time = now - last_activity;
|
||||
const state_elapsed = now - self.state_start_time;
|
||||
|
||||
// Detectar si hubo nueva actividad (para salto de pánico)
|
||||
if (last_activity > self.last_known_activity and self.anim_state != .hidden) {
|
||||
self.panic_jump = true;
|
||||
self.last_known_activity = last_activity;
|
||||
if (self.anim_state != .hiding) {
|
||||
self.anim_state = .hiding;
|
||||
self.state_start_time = now;
|
||||
}
|
||||
}
|
||||
self.last_known_activity = last_activity;
|
||||
|
||||
// Actualizar fase de animación general
|
||||
if (self.anim_state != .hidden) {
|
||||
self.anim_phase = @as(f32, @floatFromInt(@mod(now, 2000))) / 2000.0 * std.math.pi * 2.0;
|
||||
}
|
||||
|
||||
// Máquina de estados
|
||||
switch (self.anim_state) {
|
||||
.hidden => {
|
||||
if (idle_time >= IDLE_THRESHOLD_MS and num_panels > 0) {
|
||||
self.pickRandomLocation(num_panels);
|
||||
self.anim_state = .peeking;
|
||||
self.state_start_time = now;
|
||||
self.panic_jump = false;
|
||||
}
|
||||
},
|
||||
|
||||
.peeking => {
|
||||
// Subir lentamente (0 -> 1) con ease-out
|
||||
const progress = @as(f32, @floatFromInt(@min(state_elapsed, PEEK_DURATION_MS))) /
|
||||
@as(f32, @floatFromInt(PEEK_DURATION_MS));
|
||||
self.appear_progress = 1.0 - (1.0 - progress) * (1.0 - progress);
|
||||
|
||||
// Mirar a los lados
|
||||
const look_cycle = @mod(state_elapsed, 2000);
|
||||
if (look_cycle < 600) {
|
||||
self.look_dir = -1;
|
||||
} else if (look_cycle < 1200) {
|
||||
self.look_dir = 1;
|
||||
} else {
|
||||
self.look_dir = 0;
|
||||
}
|
||||
|
||||
if (state_elapsed >= PEEK_DURATION_MS) {
|
||||
self.anim_state = .watching;
|
||||
self.state_start_time = now;
|
||||
self.appear_progress = 1.0;
|
||||
}
|
||||
},
|
||||
|
||||
.watching => {
|
||||
// Mirar alrededor más rápido
|
||||
const look_cycle = @mod(state_elapsed, 1200);
|
||||
if (look_cycle < 400) {
|
||||
self.look_dir = -1;
|
||||
} else if (look_cycle < 800) {
|
||||
self.look_dir = 1;
|
||||
} else {
|
||||
self.look_dir = 0;
|
||||
}
|
||||
|
||||
if (state_elapsed >= WATCH_DURATION_MS) {
|
||||
self.anim_state = .hiding;
|
||||
self.state_start_time = now;
|
||||
self.panic_jump = false;
|
||||
}
|
||||
},
|
||||
|
||||
.hiding => {
|
||||
const hide_duration = if (self.panic_jump) PANIC_HIDE_DURATION_MS else HIDE_DURATION_MS;
|
||||
const progress = @as(f32, @floatFromInt(@min(state_elapsed, hide_duration))) /
|
||||
@as(f32, @floatFromInt(hide_duration));
|
||||
|
||||
if (self.panic_jump) {
|
||||
// Salto de pánico: bajada muy rápida (ease-in)
|
||||
self.appear_progress = 1.0 - progress * progress;
|
||||
} else {
|
||||
// Bajada suave
|
||||
self.appear_progress = 1.0 - progress;
|
||||
}
|
||||
|
||||
if (state_elapsed >= hide_duration) {
|
||||
self.anim_state = .hidden;
|
||||
self.state_start_time = now + RELOCATE_DELAY_MS;
|
||||
self.appear_progress = 0;
|
||||
self.panic_jump = false;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Retorna true si el compañero es visible
|
||||
pub fn isVisible(self: *const State) bool {
|
||||
return self.anim_state != .hidden;
|
||||
}
|
||||
};
|
||||
|
||||
/// Dimensiones del gato
|
||||
const CAT_WIDTH: i32 = 40;
|
||||
const CAT_HEIGHT: i32 = 35;
|
||||
|
||||
/// Dibuja el compañero idle en los paneles especificados
|
||||
///
|
||||
/// Parámetros:
|
||||
/// - ctx: Contexto de renderizado
|
||||
/// - panels: Slice de rectángulos donde puede aparecer el compañero
|
||||
/// - state: Estado persistente del compañero
|
||||
/// - last_activity: Timestamp de la última actividad del usuario (ms)
|
||||
/// - color: Color principal del pelaje
|
||||
pub fn draw(
|
||||
ctx: *Context,
|
||||
panels: []const Rect,
|
||||
state: *State,
|
||||
last_activity: i64,
|
||||
color: Color,
|
||||
) void {
|
||||
state.update(last_activity, panels.len);
|
||||
|
||||
if (!state.isVisible()) return;
|
||||
if (state.panel_index >= panels.len) return;
|
||||
|
||||
const panel_rect = panels[state.panel_index];
|
||||
drawCat(ctx, panel_rect, state, color);
|
||||
}
|
||||
|
||||
/// Dibuja el gato asomándose por el borde inferior del panel
|
||||
fn drawCat(ctx: *Context, panel_rect: Rect, state: *const State, color: Color) void {
|
||||
const panel_w = @as(i32, @intCast(panel_rect.w));
|
||||
const panel_h = @as(i32, @intCast(panel_rect.h));
|
||||
|
||||
// Posición X centrada en el ratio del panel
|
||||
const cat_x = panel_rect.x + @as(i32, @intFromFloat(state.x_ratio * @as(f32, @floatFromInt(panel_w)))) - CAT_WIDTH / 2;
|
||||
|
||||
// Cuánto del gato es visible
|
||||
const visible_amount = @as(i32, @intFromFloat(state.appear_progress * @as(f32, CAT_HEIGHT)));
|
||||
|
||||
// Posición Y: sube desde el borde inferior
|
||||
const cat_y = panel_rect.y + panel_h - visible_amount;
|
||||
|
||||
// === ESTABLECER CLIP al interior del panel ===
|
||||
ctx.pushCommand(.{ .clip = .{
|
||||
.x = panel_rect.x,
|
||||
.y = panel_rect.y,
|
||||
.w = panel_rect.w,
|
||||
.h = panel_rect.h,
|
||||
} });
|
||||
|
||||
// Colores del gato
|
||||
const fur_color = color;
|
||||
const dark_fur = Color{
|
||||
.r = color.r -| 40,
|
||||
.g = color.g -| 40,
|
||||
.b = color.b -| 40,
|
||||
.a = 255,
|
||||
};
|
||||
const light_fur = Color{
|
||||
.r = color.r +| 30,
|
||||
.g = color.g +| 20,
|
||||
.b = color.b,
|
||||
.a = 255,
|
||||
};
|
||||
const pink = Color{ .r = 255, .g = 170, .b = 170, .a = 255 };
|
||||
const eye_white = Color{ .r = 255, .g = 255, .b = 255, .a = 255 };
|
||||
const eye_pupil = Color{ .r = 30, .g = 30, .b = 30, .a = 255 };
|
||||
|
||||
// Offset de mirada
|
||||
const look_offset: i32 = state.look_dir * 2;
|
||||
|
||||
// Animación de orejas
|
||||
const ear_wiggle = @as(i32, @intFromFloat(std.math.sin(state.anim_phase * 3.0) * 1.5));
|
||||
|
||||
// === OREJAS ===
|
||||
// Oreja izquierda
|
||||
ctx.pushCommand(.{ .filled_triangle = .{
|
||||
.x1 = cat_x + 5,
|
||||
.y1 = cat_y + 18,
|
||||
.x2 = cat_x + 15,
|
||||
.y2 = cat_y + 16,
|
||||
.x3 = cat_x + 4 + ear_wiggle,
|
||||
.y3 = cat_y - 2,
|
||||
.color = fur_color,
|
||||
} });
|
||||
ctx.pushCommand(.{ .filled_triangle = .{
|
||||
.x1 = cat_x + 7,
|
||||
.y1 = cat_y + 16,
|
||||
.x2 = cat_x + 13,
|
||||
.y2 = cat_y + 15,
|
||||
.x3 = cat_x + 6 + ear_wiggle,
|
||||
.y3 = cat_y + 2,
|
||||
.color = pink,
|
||||
} });
|
||||
|
||||
// Oreja derecha
|
||||
ctx.pushCommand(.{ .filled_triangle = .{
|
||||
.x1 = cat_x + 25,
|
||||
.y1 = cat_y + 16,
|
||||
.x2 = cat_x + 35,
|
||||
.y2 = cat_y + 18,
|
||||
.x3 = cat_x + 36 - ear_wiggle,
|
||||
.y3 = cat_y - 2,
|
||||
.color = fur_color,
|
||||
} });
|
||||
ctx.pushCommand(.{ .filled_triangle = .{
|
||||
.x1 = cat_x + 27,
|
||||
.y1 = cat_y + 15,
|
||||
.x2 = cat_x + 33,
|
||||
.y2 = cat_y + 16,
|
||||
.x3 = cat_x + 34 - ear_wiggle,
|
||||
.y3 = cat_y + 2,
|
||||
.color = pink,
|
||||
} });
|
||||
|
||||
// === CABEZA ===
|
||||
ctx.pushCommand(.{ .filled_circle = .{
|
||||
.cx = cat_x + 20,
|
||||
.cy = cat_y + 20,
|
||||
.radius = 14,
|
||||
.color = fur_color,
|
||||
} });
|
||||
|
||||
// Mejillas
|
||||
ctx.pushCommand(.{ .filled_circle = .{
|
||||
.cx = cat_x + 8,
|
||||
.cy = cat_y + 24,
|
||||
.radius = 6,
|
||||
.color = light_fur,
|
||||
} });
|
||||
ctx.pushCommand(.{ .filled_circle = .{
|
||||
.cx = cat_x + 32,
|
||||
.cy = cat_y + 24,
|
||||
.radius = 6,
|
||||
.color = light_fur,
|
||||
} });
|
||||
|
||||
// === OJOS ===
|
||||
// Ojo izquierdo
|
||||
ctx.pushCommand(.{ .filled_circle = .{
|
||||
.cx = cat_x + 13 + look_offset,
|
||||
.cy = cat_y + 17,
|
||||
.radius = 5,
|
||||
.color = eye_white,
|
||||
} });
|
||||
ctx.pushCommand(.{ .rect = .{
|
||||
.x = cat_x + 12 + look_offset,
|
||||
.y = cat_y + 14,
|
||||
.w = 3,
|
||||
.h = 7,
|
||||
.color = eye_pupil,
|
||||
} });
|
||||
ctx.pushCommand(.{ .rect = .{
|
||||
.x = cat_x + 11 + look_offset,
|
||||
.y = cat_y + 14,
|
||||
.w = 2,
|
||||
.h = 2,
|
||||
.color = eye_white,
|
||||
} });
|
||||
|
||||
// Ojo derecho
|
||||
ctx.pushCommand(.{ .filled_circle = .{
|
||||
.cx = cat_x + 27 + look_offset,
|
||||
.cy = cat_y + 17,
|
||||
.radius = 5,
|
||||
.color = eye_white,
|
||||
} });
|
||||
ctx.pushCommand(.{ .rect = .{
|
||||
.x = cat_x + 26 + look_offset,
|
||||
.y = cat_y + 14,
|
||||
.w = 3,
|
||||
.h = 7,
|
||||
.color = eye_pupil,
|
||||
} });
|
||||
ctx.pushCommand(.{ .rect = .{
|
||||
.x = cat_x + 25 + look_offset,
|
||||
.y = cat_y + 14,
|
||||
.w = 2,
|
||||
.h = 2,
|
||||
.color = eye_white,
|
||||
} });
|
||||
|
||||
// === NARIZ ===
|
||||
ctx.pushCommand(.{ .filled_triangle = .{
|
||||
.x1 = cat_x + 17,
|
||||
.y1 = cat_y + 22,
|
||||
.x2 = cat_x + 23,
|
||||
.y2 = cat_y + 22,
|
||||
.x3 = cat_x + 20,
|
||||
.y3 = cat_y + 26,
|
||||
.color = pink,
|
||||
} });
|
||||
|
||||
// === BOCA ===
|
||||
ctx.pushCommand(.{ .line = .{
|
||||
.x1 = cat_x + 20,
|
||||
.y1 = cat_y + 26,
|
||||
.x2 = cat_x + 15,
|
||||
.y2 = cat_y + 29,
|
||||
.color = dark_fur,
|
||||
} });
|
||||
ctx.pushCommand(.{ .line = .{
|
||||
.x1 = cat_x + 20,
|
||||
.y1 = cat_y + 26,
|
||||
.x2 = cat_x + 25,
|
||||
.y2 = cat_y + 29,
|
||||
.color = dark_fur,
|
||||
} });
|
||||
|
||||
// === BIGOTES ===
|
||||
// Izquierdos
|
||||
ctx.pushCommand(.{ .line = .{ .x1 = cat_x + 10, .y1 = cat_y + 23, .x2 = cat_x - 8, .y2 = cat_y + 20, .color = dark_fur } });
|
||||
ctx.pushCommand(.{ .line = .{ .x1 = cat_x + 10, .y1 = cat_y + 25, .x2 = cat_x - 8, .y2 = cat_y + 25, .color = dark_fur } });
|
||||
ctx.pushCommand(.{ .line = .{ .x1 = cat_x + 10, .y1 = cat_y + 27, .x2 = cat_x - 8, .y2 = cat_y + 30, .color = dark_fur } });
|
||||
// Derechos
|
||||
ctx.pushCommand(.{ .line = .{ .x1 = cat_x + 30, .y1 = cat_y + 23, .x2 = cat_x + 48, .y2 = cat_y + 20, .color = dark_fur } });
|
||||
ctx.pushCommand(.{ .line = .{ .x1 = cat_x + 30, .y1 = cat_y + 25, .x2 = cat_x + 48, .y2 = cat_y + 25, .color = dark_fur } });
|
||||
ctx.pushCommand(.{ .line = .{ .x1 = cat_x + 30, .y1 = cat_y + 27, .x2 = cat_x + 48, .y2 = cat_y + 30, .color = dark_fur } });
|
||||
|
||||
// === PATITAS ===
|
||||
if (state.appear_progress > 0.6) {
|
||||
ctx.pushCommand(.{ .filled_circle = .{ .cx = cat_x + 8, .cy = cat_y + 32, .radius = 4, .color = fur_color } });
|
||||
ctx.pushCommand(.{ .filled_circle = .{ .cx = cat_x + 8, .cy = cat_y + 34, .radius = 2, .color = pink } });
|
||||
ctx.pushCommand(.{ .filled_circle = .{ .cx = cat_x + 32, .cy = cat_y + 32, .radius = 4, .color = fur_color } });
|
||||
ctx.pushCommand(.{ .filled_circle = .{ .cx = cat_x + 32, .cy = cat_y + 34, .radius = 2, .color = pink } });
|
||||
}
|
||||
|
||||
// === RESTAURAR CLIP ===
|
||||
ctx.pushCommand(.clip_end);
|
||||
}
|
||||
|
|
@ -69,6 +69,9 @@ pub const table_core = @import("table_core/table_core.zig");
|
|||
// Advanced widgets
|
||||
pub const advanced_table = @import("advanced_table/advanced_table.zig");
|
||||
|
||||
// Idle companion (mascot that appears when user is idle)
|
||||
pub const idle_companion = @import("idle_companion.zig");
|
||||
|
||||
// =============================================================================
|
||||
// Re-exports for convenience
|
||||
// =============================================================================
|
||||
|
|
@ -436,6 +439,12 @@ pub const SelectableColors = selectable.Colors;
|
|||
pub const SelectableResult = selectable.Result;
|
||||
pub const SelectionGroup = selectable.SelectionGroup;
|
||||
|
||||
// IdleCompanion (Zcat mascot)
|
||||
pub const IdleCompanion = idle_companion;
|
||||
pub const IdleCompanionState = idle_companion.State;
|
||||
pub const IdleCompanionAnimState = idle_companion.AnimState;
|
||||
pub const IdleCompanionRect = idle_companion.Rect;
|
||||
|
||||
// =============================================================================
|
||||
// Tests
|
||||
// =============================================================================
|
||||
|
|
|
|||
Loading…
Reference in a new issue