diff --git a/frontend/public/demo/candidates/boy.png b/frontend/public/demo/candidates/boy.png new file mode 100644 index 0000000..7755f3c Binary files /dev/null and b/frontend/public/demo/candidates/boy.png differ diff --git a/frontend/public/demo/candidates/lirael.png b/frontend/public/demo/candidates/lirael.png new file mode 100644 index 0000000..6b70c1f Binary files /dev/null and b/frontend/public/demo/candidates/lirael.png differ diff --git a/frontend/public/demo/candidates/samurai.png b/frontend/public/demo/candidates/samurai.png new file mode 100644 index 0000000..1d0e6de Binary files /dev/null and b/frontend/public/demo/candidates/samurai.png differ diff --git a/frontend/public/demo/candidates/skeleton.png b/frontend/public/demo/candidates/skeleton.png new file mode 100644 index 0000000..43fd0b6 Binary files /dev/null and b/frontend/public/demo/candidates/skeleton.png differ diff --git a/frontend/public/demo/samurai/idle-01.png b/frontend/public/demo/samurai/idle-01.png new file mode 100644 index 0000000..da5c1fe Binary files /dev/null and b/frontend/public/demo/samurai/idle-01.png differ diff --git a/frontend/public/demo/samurai/idle-02.png b/frontend/public/demo/samurai/idle-02.png new file mode 100644 index 0000000..8d97893 Binary files /dev/null and b/frontend/public/demo/samurai/idle-02.png differ diff --git a/frontend/public/demo/samurai/idle-03.png b/frontend/public/demo/samurai/idle-03.png new file mode 100644 index 0000000..68abb1c Binary files /dev/null and b/frontend/public/demo/samurai/idle-03.png differ diff --git a/frontend/public/demo/samurai/idle-04.png b/frontend/public/demo/samurai/idle-04.png new file mode 100644 index 0000000..1b5bd85 Binary files /dev/null and b/frontend/public/demo/samurai/idle-04.png differ diff --git a/frontend/public/demo/samurai/idle-05.png b/frontend/public/demo/samurai/idle-05.png new file mode 100644 index 0000000..3881743 Binary files /dev/null and b/frontend/public/demo/samurai/idle-05.png differ diff --git a/frontend/public/demo/samurai/idle-06.png b/frontend/public/demo/samurai/idle-06.png new file mode 100644 index 0000000..86c6c87 Binary files /dev/null and b/frontend/public/demo/samurai/idle-06.png differ diff --git a/frontend/public/demo/samurai/idle-07.png b/frontend/public/demo/samurai/idle-07.png new file mode 100644 index 0000000..86e2219 Binary files /dev/null and b/frontend/public/demo/samurai/idle-07.png differ diff --git a/frontend/public/demo/samurai/idle-08.png b/frontend/public/demo/samurai/idle-08.png new file mode 100644 index 0000000..0cfe75f Binary files /dev/null and b/frontend/public/demo/samurai/idle-08.png differ diff --git a/frontend/public/demo/samurai/idle-09.png b/frontend/public/demo/samurai/idle-09.png new file mode 100644 index 0000000..ffda4a2 Binary files /dev/null and b/frontend/public/demo/samurai/idle-09.png differ diff --git a/frontend/public/demo/samurai/idle-10.png b/frontend/public/demo/samurai/idle-10.png new file mode 100644 index 0000000..8ec6eb4 Binary files /dev/null and b/frontend/public/demo/samurai/idle-10.png differ diff --git a/frontend/public/demo/samurai/idle-11.png b/frontend/public/demo/samurai/idle-11.png new file mode 100644 index 0000000..fc5fb65 Binary files /dev/null and b/frontend/public/demo/samurai/idle-11.png differ diff --git a/frontend/public/demo/samurai/idle-12.png b/frontend/public/demo/samurai/idle-12.png new file mode 100644 index 0000000..a448eba Binary files /dev/null and b/frontend/public/demo/samurai/idle-12.png differ diff --git a/frontend/public/demo/samurai/idle-13.png b/frontend/public/demo/samurai/idle-13.png new file mode 100644 index 0000000..1a1405a Binary files /dev/null and b/frontend/public/demo/samurai/idle-13.png differ diff --git a/frontend/public/demo/samurai/idle-14.png b/frontend/public/demo/samurai/idle-14.png new file mode 100644 index 0000000..59b68dd Binary files /dev/null and b/frontend/public/demo/samurai/idle-14.png differ diff --git a/frontend/public/demo/samurai/idle-15.png b/frontend/public/demo/samurai/idle-15.png new file mode 100644 index 0000000..6f0b575 Binary files /dev/null and b/frontend/public/demo/samurai/idle-15.png differ diff --git a/frontend/public/demo/samurai/idle-16.png b/frontend/public/demo/samurai/idle-16.png new file mode 100644 index 0000000..982742f Binary files /dev/null and b/frontend/public/demo/samurai/idle-16.png differ diff --git a/frontend/public/demo/samurai/idle-17.png b/frontend/public/demo/samurai/idle-17.png new file mode 100644 index 0000000..0b2a076 Binary files /dev/null and b/frontend/public/demo/samurai/idle-17.png differ diff --git a/frontend/public/demo/samurai/idle-18.png b/frontend/public/demo/samurai/idle-18.png new file mode 100644 index 0000000..d0e109a Binary files /dev/null and b/frontend/public/demo/samurai/idle-18.png differ diff --git a/frontend/public/demo/samurai/idle-19.png b/frontend/public/demo/samurai/idle-19.png new file mode 100644 index 0000000..6ee2908 Binary files /dev/null and b/frontend/public/demo/samurai/idle-19.png differ diff --git a/frontend/public/demo/samurai/idle-20.png b/frontend/public/demo/samurai/idle-20.png new file mode 100644 index 0000000..8dee9e1 Binary files /dev/null and b/frontend/public/demo/samurai/idle-20.png differ diff --git a/frontend/public/demo/samurai/idle-21.png b/frontend/public/demo/samurai/idle-21.png new file mode 100644 index 0000000..05b3cc5 Binary files /dev/null and b/frontend/public/demo/samurai/idle-21.png differ diff --git a/frontend/public/demo/samurai/idle-22.png b/frontend/public/demo/samurai/idle-22.png new file mode 100644 index 0000000..c8509d4 Binary files /dev/null and b/frontend/public/demo/samurai/idle-22.png differ diff --git a/frontend/public/demo/samurai/idle-23.png b/frontend/public/demo/samurai/idle-23.png new file mode 100644 index 0000000..d9b3715 Binary files /dev/null and b/frontend/public/demo/samurai/idle-23.png differ diff --git a/frontend/public/demo/samurai/idle-24.png b/frontend/public/demo/samurai/idle-24.png new file mode 100644 index 0000000..76cd35c Binary files /dev/null and b/frontend/public/demo/samurai/idle-24.png differ diff --git a/frontend/public/demo/samurai/idle-25.png b/frontend/public/demo/samurai/idle-25.png new file mode 100644 index 0000000..de8991e Binary files /dev/null and b/frontend/public/demo/samurai/idle-25.png differ diff --git a/frontend/public/demo/samurai/idle-26.png b/frontend/public/demo/samurai/idle-26.png new file mode 100644 index 0000000..60aee13 Binary files /dev/null and b/frontend/public/demo/samurai/idle-26.png differ diff --git a/frontend/public/demo/samurai/idle-27.png b/frontend/public/demo/samurai/idle-27.png new file mode 100644 index 0000000..dfcd050 Binary files /dev/null and b/frontend/public/demo/samurai/idle-27.png differ diff --git a/frontend/public/demo/samurai/idle-28.png b/frontend/public/demo/samurai/idle-28.png new file mode 100644 index 0000000..dbbc079 Binary files /dev/null and b/frontend/public/demo/samurai/idle-28.png differ diff --git a/frontend/public/demo/samurai/idle-29.png b/frontend/public/demo/samurai/idle-29.png new file mode 100644 index 0000000..dd27908 Binary files /dev/null and b/frontend/public/demo/samurai/idle-29.png differ diff --git a/frontend/public/demo/samurai/idle-30.png b/frontend/public/demo/samurai/idle-30.png new file mode 100644 index 0000000..e419af7 Binary files /dev/null and b/frontend/public/demo/samurai/idle-30.png differ diff --git a/frontend/public/demo/samurai/idle-31.png b/frontend/public/demo/samurai/idle-31.png new file mode 100644 index 0000000..d48bb38 Binary files /dev/null and b/frontend/public/demo/samurai/idle-31.png differ diff --git a/frontend/public/demo/samurai/idle-32.png b/frontend/public/demo/samurai/idle-32.png new file mode 100644 index 0000000..d37c3ea Binary files /dev/null and b/frontend/public/demo/samurai/idle-32.png differ diff --git a/frontend/public/demo/samurai/idle-33.png b/frontend/public/demo/samurai/idle-33.png new file mode 100644 index 0000000..2dbbf6a Binary files /dev/null and b/frontend/public/demo/samurai/idle-33.png differ diff --git a/frontend/public/demo/samurai/idle-34.png b/frontend/public/demo/samurai/idle-34.png new file mode 100644 index 0000000..10964cb Binary files /dev/null and b/frontend/public/demo/samurai/idle-34.png differ diff --git a/frontend/public/demo/samurai/idle-35.png b/frontend/public/demo/samurai/idle-35.png new file mode 100644 index 0000000..ea84bfc Binary files /dev/null and b/frontend/public/demo/samurai/idle-35.png differ diff --git a/frontend/public/demo/samurai/idle-36.png b/frontend/public/demo/samurai/idle-36.png new file mode 100644 index 0000000..7b3acbf Binary files /dev/null and b/frontend/public/demo/samurai/idle-36.png differ diff --git a/frontend/public/demo/samurai/jump-01.png b/frontend/public/demo/samurai/jump-01.png new file mode 100644 index 0000000..f153e3e Binary files /dev/null and b/frontend/public/demo/samurai/jump-01.png differ diff --git a/frontend/public/demo/samurai/jump-02.png b/frontend/public/demo/samurai/jump-02.png new file mode 100644 index 0000000..cc0a98d Binary files /dev/null and b/frontend/public/demo/samurai/jump-02.png differ diff --git a/frontend/public/demo/samurai/jump-03.png b/frontend/public/demo/samurai/jump-03.png new file mode 100644 index 0000000..91a913b Binary files /dev/null and b/frontend/public/demo/samurai/jump-03.png differ diff --git a/frontend/public/demo/samurai/jump-04.png b/frontend/public/demo/samurai/jump-04.png new file mode 100644 index 0000000..7031f7f Binary files /dev/null and b/frontend/public/demo/samurai/jump-04.png differ diff --git a/frontend/public/demo/samurai/jump-05.png b/frontend/public/demo/samurai/jump-05.png new file mode 100644 index 0000000..eac0547 Binary files /dev/null and b/frontend/public/demo/samurai/jump-05.png differ diff --git a/frontend/public/demo/samurai/jump-06.png b/frontend/public/demo/samurai/jump-06.png new file mode 100644 index 0000000..851ec5c Binary files /dev/null and b/frontend/public/demo/samurai/jump-06.png differ diff --git a/frontend/public/demo/samurai/jump-07.png b/frontend/public/demo/samurai/jump-07.png new file mode 100644 index 0000000..3c1c056 Binary files /dev/null and b/frontend/public/demo/samurai/jump-07.png differ diff --git a/frontend/public/demo/samurai/jump-08.png b/frontend/public/demo/samurai/jump-08.png new file mode 100644 index 0000000..25d213e Binary files /dev/null and b/frontend/public/demo/samurai/jump-08.png differ diff --git a/frontend/public/demo/samurai/jump-09.png b/frontend/public/demo/samurai/jump-09.png new file mode 100644 index 0000000..4bd4114 Binary files /dev/null and b/frontend/public/demo/samurai/jump-09.png differ diff --git a/frontend/public/demo/samurai/jump-10.png b/frontend/public/demo/samurai/jump-10.png new file mode 100644 index 0000000..f60c58a Binary files /dev/null and b/frontend/public/demo/samurai/jump-10.png differ diff --git a/frontend/public/demo/samurai/jump-11.png b/frontend/public/demo/samurai/jump-11.png new file mode 100644 index 0000000..fc8a57a Binary files /dev/null and b/frontend/public/demo/samurai/jump-11.png differ diff --git a/frontend/public/demo/samurai/jump-12.png b/frontend/public/demo/samurai/jump-12.png new file mode 100644 index 0000000..f37b24e Binary files /dev/null and b/frontend/public/demo/samurai/jump-12.png differ diff --git a/frontend/public/demo/samurai/jump-13.png b/frontend/public/demo/samurai/jump-13.png new file mode 100644 index 0000000..1a7ba67 Binary files /dev/null and b/frontend/public/demo/samurai/jump-13.png differ diff --git a/frontend/public/demo/samurai/jump-14.png b/frontend/public/demo/samurai/jump-14.png new file mode 100644 index 0000000..8079503 Binary files /dev/null and b/frontend/public/demo/samurai/jump-14.png differ diff --git a/frontend/public/demo/samurai/jump-15.png b/frontend/public/demo/samurai/jump-15.png new file mode 100644 index 0000000..3a9a11f Binary files /dev/null and b/frontend/public/demo/samurai/jump-15.png differ diff --git a/frontend/public/demo/samurai/jump-16.png b/frontend/public/demo/samurai/jump-16.png new file mode 100644 index 0000000..01c90da Binary files /dev/null and b/frontend/public/demo/samurai/jump-16.png differ diff --git a/frontend/public/demo/samurai/jump-17.png b/frontend/public/demo/samurai/jump-17.png new file mode 100644 index 0000000..b8e5629 Binary files /dev/null and b/frontend/public/demo/samurai/jump-17.png differ diff --git a/frontend/public/demo/samurai/jump-18.png b/frontend/public/demo/samurai/jump-18.png new file mode 100644 index 0000000..a44d036 Binary files /dev/null and b/frontend/public/demo/samurai/jump-18.png differ diff --git a/frontend/public/demo/samurai/jump-19.png b/frontend/public/demo/samurai/jump-19.png new file mode 100644 index 0000000..f7c8876 Binary files /dev/null and b/frontend/public/demo/samurai/jump-19.png differ diff --git a/frontend/public/demo/samurai/jump-20.png b/frontend/public/demo/samurai/jump-20.png new file mode 100644 index 0000000..b12bc03 Binary files /dev/null and b/frontend/public/demo/samurai/jump-20.png differ diff --git a/frontend/public/demo/samurai/jump-21.png b/frontend/public/demo/samurai/jump-21.png new file mode 100644 index 0000000..3316d8d Binary files /dev/null and b/frontend/public/demo/samurai/jump-21.png differ diff --git a/frontend/public/demo/samurai/jump-22.png b/frontend/public/demo/samurai/jump-22.png new file mode 100644 index 0000000..2926c9e Binary files /dev/null and b/frontend/public/demo/samurai/jump-22.png differ diff --git a/frontend/public/demo/samurai/jump-23.png b/frontend/public/demo/samurai/jump-23.png new file mode 100644 index 0000000..c95351b Binary files /dev/null and b/frontend/public/demo/samurai/jump-23.png differ diff --git a/frontend/public/demo/samurai/jump-24.png b/frontend/public/demo/samurai/jump-24.png new file mode 100644 index 0000000..6a96462 Binary files /dev/null and b/frontend/public/demo/samurai/jump-24.png differ diff --git a/frontend/public/demo/samurai/jump-25.png b/frontend/public/demo/samurai/jump-25.png new file mode 100644 index 0000000..2d6357c Binary files /dev/null and b/frontend/public/demo/samurai/jump-25.png differ diff --git a/frontend/public/demo/samurai/jump-26.png b/frontend/public/demo/samurai/jump-26.png new file mode 100644 index 0000000..efd3c01 Binary files /dev/null and b/frontend/public/demo/samurai/jump-26.png differ diff --git a/frontend/public/demo/samurai/jump-27.png b/frontend/public/demo/samurai/jump-27.png new file mode 100644 index 0000000..94e22ec Binary files /dev/null and b/frontend/public/demo/samurai/jump-27.png differ diff --git a/frontend/public/demo/samurai/jump-28.png b/frontend/public/demo/samurai/jump-28.png new file mode 100644 index 0000000..7b59707 Binary files /dev/null and b/frontend/public/demo/samurai/jump-28.png differ diff --git a/frontend/public/demo/samurai/jump-29.png b/frontend/public/demo/samurai/jump-29.png new file mode 100644 index 0000000..f08f20c Binary files /dev/null and b/frontend/public/demo/samurai/jump-29.png differ diff --git a/frontend/public/demo/samurai/jump-30.png b/frontend/public/demo/samurai/jump-30.png new file mode 100644 index 0000000..93cdc69 Binary files /dev/null and b/frontend/public/demo/samurai/jump-30.png differ diff --git a/frontend/public/demo/samurai/jump-31.png b/frontend/public/demo/samurai/jump-31.png new file mode 100644 index 0000000..2ccd9ed Binary files /dev/null and b/frontend/public/demo/samurai/jump-31.png differ diff --git a/frontend/public/demo/samurai/jump-32.png b/frontend/public/demo/samurai/jump-32.png new file mode 100644 index 0000000..dcd000e Binary files /dev/null and b/frontend/public/demo/samurai/jump-32.png differ diff --git a/frontend/public/demo/samurai/jump-33.png b/frontend/public/demo/samurai/jump-33.png new file mode 100644 index 0000000..2814694 Binary files /dev/null and b/frontend/public/demo/samurai/jump-33.png differ diff --git a/frontend/public/demo/samurai/jump-34.png b/frontend/public/demo/samurai/jump-34.png new file mode 100644 index 0000000..a76bc2c Binary files /dev/null and b/frontend/public/demo/samurai/jump-34.png differ diff --git a/frontend/public/demo/samurai/jump-35.png b/frontend/public/demo/samurai/jump-35.png new file mode 100644 index 0000000..84d5e77 Binary files /dev/null and b/frontend/public/demo/samurai/jump-35.png differ diff --git a/frontend/public/demo/samurai/jump-36.png b/frontend/public/demo/samurai/jump-36.png new file mode 100644 index 0000000..7891541 Binary files /dev/null and b/frontend/public/demo/samurai/jump-36.png differ diff --git a/frontend/public/demo/samurai/walk-01.png b/frontend/public/demo/samurai/walk-01.png new file mode 100644 index 0000000..2a79fb4 Binary files /dev/null and b/frontend/public/demo/samurai/walk-01.png differ diff --git a/frontend/public/demo/samurai/walk-02.png b/frontend/public/demo/samurai/walk-02.png new file mode 100644 index 0000000..c79fbc9 Binary files /dev/null and b/frontend/public/demo/samurai/walk-02.png differ diff --git a/frontend/public/demo/samurai/walk-03.png b/frontend/public/demo/samurai/walk-03.png new file mode 100644 index 0000000..676a44c Binary files /dev/null and b/frontend/public/demo/samurai/walk-03.png differ diff --git a/frontend/public/demo/samurai/walk-04.png b/frontend/public/demo/samurai/walk-04.png new file mode 100644 index 0000000..c6c8b38 Binary files /dev/null and b/frontend/public/demo/samurai/walk-04.png differ diff --git a/frontend/public/demo/samurai/walk-05.png b/frontend/public/demo/samurai/walk-05.png new file mode 100644 index 0000000..a92273e Binary files /dev/null and b/frontend/public/demo/samurai/walk-05.png differ diff --git a/frontend/public/demo/samurai/walk-06.png b/frontend/public/demo/samurai/walk-06.png new file mode 100644 index 0000000..a761d9f Binary files /dev/null and b/frontend/public/demo/samurai/walk-06.png differ diff --git a/frontend/public/demo/samurai/walk-07.png b/frontend/public/demo/samurai/walk-07.png new file mode 100644 index 0000000..3f5a51a Binary files /dev/null and b/frontend/public/demo/samurai/walk-07.png differ diff --git a/frontend/public/demo/samurai/walk-08.png b/frontend/public/demo/samurai/walk-08.png new file mode 100644 index 0000000..1c1cdb3 Binary files /dev/null and b/frontend/public/demo/samurai/walk-08.png differ diff --git a/frontend/public/demo/samurai/walk-09.png b/frontend/public/demo/samurai/walk-09.png new file mode 100644 index 0000000..52ae586 Binary files /dev/null and b/frontend/public/demo/samurai/walk-09.png differ diff --git a/frontend/public/demo/samurai/walk-10.png b/frontend/public/demo/samurai/walk-10.png new file mode 100644 index 0000000..378eeb0 Binary files /dev/null and b/frontend/public/demo/samurai/walk-10.png differ diff --git a/frontend/public/demo/samurai/walk-11.png b/frontend/public/demo/samurai/walk-11.png new file mode 100644 index 0000000..a62a9f8 Binary files /dev/null and b/frontend/public/demo/samurai/walk-11.png differ diff --git a/frontend/public/demo/samurai/walk-12.png b/frontend/public/demo/samurai/walk-12.png new file mode 100644 index 0000000..bf25c9c Binary files /dev/null and b/frontend/public/demo/samurai/walk-12.png differ diff --git a/frontend/public/demo/samurai/walk-13.png b/frontend/public/demo/samurai/walk-13.png new file mode 100644 index 0000000..c169532 Binary files /dev/null and b/frontend/public/demo/samurai/walk-13.png differ diff --git a/frontend/public/demo/samurai/walk-14.png b/frontend/public/demo/samurai/walk-14.png new file mode 100644 index 0000000..a3b60f8 Binary files /dev/null and b/frontend/public/demo/samurai/walk-14.png differ diff --git a/frontend/public/demo/samurai/walk-15.png b/frontend/public/demo/samurai/walk-15.png new file mode 100644 index 0000000..d4619e7 Binary files /dev/null and b/frontend/public/demo/samurai/walk-15.png differ diff --git a/frontend/public/demo/samurai/walk-16.png b/frontend/public/demo/samurai/walk-16.png new file mode 100644 index 0000000..882dd36 Binary files /dev/null and b/frontend/public/demo/samurai/walk-16.png differ diff --git a/frontend/public/demo/samurai/walk-17.png b/frontend/public/demo/samurai/walk-17.png new file mode 100644 index 0000000..cb9cad8 Binary files /dev/null and b/frontend/public/demo/samurai/walk-17.png differ diff --git a/frontend/public/demo/samurai/walk-18.png b/frontend/public/demo/samurai/walk-18.png new file mode 100644 index 0000000..d1040c1 Binary files /dev/null and b/frontend/public/demo/samurai/walk-18.png differ diff --git a/frontend/public/demo/samurai/walk-19.png b/frontend/public/demo/samurai/walk-19.png new file mode 100644 index 0000000..e612d87 Binary files /dev/null and b/frontend/public/demo/samurai/walk-19.png differ diff --git a/frontend/public/demo/samurai/walk-20.png b/frontend/public/demo/samurai/walk-20.png new file mode 100644 index 0000000..57598ac Binary files /dev/null and b/frontend/public/demo/samurai/walk-20.png differ diff --git a/frontend/public/demo/samurai/walk-21.png b/frontend/public/demo/samurai/walk-21.png new file mode 100644 index 0000000..a0bc755 Binary files /dev/null and b/frontend/public/demo/samurai/walk-21.png differ diff --git a/frontend/public/demo/samurai/walk-22.png b/frontend/public/demo/samurai/walk-22.png new file mode 100644 index 0000000..abc4c48 Binary files /dev/null and b/frontend/public/demo/samurai/walk-22.png differ diff --git a/frontend/public/demo/samurai/walk-23.png b/frontend/public/demo/samurai/walk-23.png new file mode 100644 index 0000000..c0780a7 Binary files /dev/null and b/frontend/public/demo/samurai/walk-23.png differ diff --git a/frontend/public/demo/samurai/walk-24.png b/frontend/public/demo/samurai/walk-24.png new file mode 100644 index 0000000..316befc Binary files /dev/null and b/frontend/public/demo/samurai/walk-24.png differ diff --git a/frontend/public/demo/samurai/walk-25.png b/frontend/public/demo/samurai/walk-25.png new file mode 100644 index 0000000..60de01f Binary files /dev/null and b/frontend/public/demo/samurai/walk-25.png differ diff --git a/frontend/public/demo/samurai/walk-26.png b/frontend/public/demo/samurai/walk-26.png new file mode 100644 index 0000000..27bbc70 Binary files /dev/null and b/frontend/public/demo/samurai/walk-26.png differ diff --git a/frontend/public/demo/samurai/walk-27.png b/frontend/public/demo/samurai/walk-27.png new file mode 100644 index 0000000..580b2e0 Binary files /dev/null and b/frontend/public/demo/samurai/walk-27.png differ diff --git a/frontend/public/demo/samurai/walk-28.png b/frontend/public/demo/samurai/walk-28.png new file mode 100644 index 0000000..fc45088 Binary files /dev/null and b/frontend/public/demo/samurai/walk-28.png differ diff --git a/frontend/public/demo/samurai/walk-29.png b/frontend/public/demo/samurai/walk-29.png new file mode 100644 index 0000000..b1547c2 Binary files /dev/null and b/frontend/public/demo/samurai/walk-29.png differ diff --git a/frontend/public/demo/samurai/walk-30.png b/frontend/public/demo/samurai/walk-30.png new file mode 100644 index 0000000..e4c0b8d Binary files /dev/null and b/frontend/public/demo/samurai/walk-30.png differ diff --git a/frontend/public/demo/samurai/walk-31.png b/frontend/public/demo/samurai/walk-31.png new file mode 100644 index 0000000..456aa64 Binary files /dev/null and b/frontend/public/demo/samurai/walk-31.png differ diff --git a/frontend/public/demo/samurai/walk-32.png b/frontend/public/demo/samurai/walk-32.png new file mode 100644 index 0000000..21a1acb Binary files /dev/null and b/frontend/public/demo/samurai/walk-32.png differ diff --git a/frontend/public/demo/samurai/walk-33.png b/frontend/public/demo/samurai/walk-33.png new file mode 100644 index 0000000..30085a8 Binary files /dev/null and b/frontend/public/demo/samurai/walk-33.png differ diff --git a/frontend/public/demo/samurai/walk-34.png b/frontend/public/demo/samurai/walk-34.png new file mode 100644 index 0000000..e73e577 Binary files /dev/null and b/frontend/public/demo/samurai/walk-34.png differ diff --git a/frontend/public/demo/samurai/walk-35.png b/frontend/public/demo/samurai/walk-35.png new file mode 100644 index 0000000..9ff1a08 Binary files /dev/null and b/frontend/public/demo/samurai/walk-35.png differ diff --git a/frontend/public/demo/samurai/walk-36.png b/frontend/public/demo/samurai/walk-36.png new file mode 100644 index 0000000..aa205f3 Binary files /dev/null and b/frontend/public/demo/samurai/walk-36.png differ diff --git a/frontend/src/app/app.tsx b/frontend/src/app/app.tsx index b46ae87..be0986c 100644 --- a/frontend/src/app/app.tsx +++ b/frontend/src/app/app.tsx @@ -25,6 +25,7 @@ export function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/app/layout/index.tsx b/frontend/src/app/layout/index.tsx index aa21b82..0be1f6d 100644 --- a/frontend/src/app/layout/index.tsx +++ b/frontend/src/app/layout/index.tsx @@ -21,6 +21,9 @@ export function AppShell({ children }: AppShellProps) { 项目 + + 工作流编辑器 +
{children}
diff --git a/frontend/src/features/canvas-flow/index.test.ts b/frontend/src/features/canvas-flow/index.test.ts new file mode 100644 index 0000000..573d074 --- /dev/null +++ b/frontend/src/features/canvas-flow/index.test.ts @@ -0,0 +1,90 @@ +import { describe, expect, it } from 'vitest' + +import { + addCard, + autoAppend, + createGraph, + descendants, + layout, + plusOptions, + removeCard, + updateCard, + type Card, +} from './index' + +/** 把图推进到「母版已确认」,后面几个用例都从这里开始。 */ +function graphWithMaster() { + let graph = createGraph() + const origin = graph.cards[0] + graph = autoAppend(updateCard(graph, origin.id, { status: 'confirmed' }), origin.id, 'candidates') + const candidates = graph.cards[1] + graph = autoAppend( + updateCard(graph, candidates.id, { status: 'confirmed' }), + candidates.id, + 'master', + ) + const master = graph.cards[2] + return { graph: updateCard(graph, master.id, { status: 'confirmed' }), masterId: master.id } +} + +describe('加号只列合法下游', () => { + it('未确认的卡片没有下游,用户无从往下走', () => { + const graph = createGraph() + expect(plusOptions(graph.cards[0])).toEqual([]) + }) + + it('母版给出生成动作、静态资产与导出,静态资产本期禁用', () => { + const { graph, masterId } = graphWithMaster() + const options = plusOptions(graph.cards.find((card) => card.id === masterId) as Card) + expect(options.map((option) => option.id)).toEqual(['action', 'static', 'export']) + expect(options.find((option) => option.id === 'static')?.disabled).toBe(true) + }) + + it('首帧只有完整动画一个下游,因此界面不弹菜单', () => { + const { graph, masterId } = graphWithMaster() + const next = addCard(graph, masterId, 'preset:walk') + const firstFrame = next.cards[next.cards.length - 1] + expect( + plusOptions(updateCard(next, firstFrame.id, { status: 'confirmed' }).cards.at(-1) as Card), + ).toHaveLength(1) + }) +}) + +describe('挂动作', () => { + it('在母版上重复展开加号可以挂多条并列分支', () => { + const { graph, masterId } = graphWithMaster() + const withTwo = addCard(addCard(graph, masterId, 'preset:walk'), masterId, 'preset:idle') + const branches = withTwo.cards.filter((card) => card.kind === 'first-frame') + expect(branches).toHaveLength(2) + expect(branches.every((card) => card.parentId === masterId)).toBe(true) + }) + + it('自定义动作与预设动作是同一种卡片,只是定义来源不同', () => { + const { graph, masterId } = graphWithMaster() + const preset = addCard(graph, masterId, 'preset:walk').cards.at(-1) as Card + const custom = addCard(graph, masterId, 'custom').cards.at(-1) as Card + expect(preset.kind).toBe(custom.kind) + expect(preset.action?.kind).toBe('preset') + expect(custom.action?.kind).toBe('custom') + }) + + it('并列分支各占一行,主干留在第 0 行', () => { + const { graph, masterId } = graphWithMaster() + const withTwo = addCard(addCard(graph, masterId, 'preset:walk'), masterId, 'preset:idle') + const positions = layout(withTwo) + const [walk, idle] = withTwo.cards.filter((card) => card.kind === 'first-frame') + expect(positions[masterId].y).toBe(0) + expect(positions[walk.id].y).not.toBe(positions[idle.id].y) + }) +}) + +describe('删除连带下游', () => { + it('删掉首帧会一并移除它下面的动画', () => { + const { graph, masterId } = graphWithMaster() + const withWalk = addCard(graph, masterId, 'preset:walk') + const firstFrame = withWalk.cards.at(-1) as Card + const withAnimation = addCard(withWalk, firstFrame.id, 'animation') + expect(descendants(withAnimation, firstFrame.id)).toHaveLength(2) + expect(removeCard(withAnimation, firstFrame.id).cards).toHaveLength(3) + }) +}) diff --git a/frontend/src/features/canvas-flow/index.ts b/frontend/src/features/canvas-flow/index.ts new file mode 100644 index 0000000..575ec58 --- /dev/null +++ b/frontend/src/features/canvas-flow/index.ts @@ -0,0 +1,395 @@ +import type { ActionKind, ActionType } from '@/entities' + +/** + * 画布的卡片流程模型。 + * + * 与 entities/workflow-run 的关系:WorkflowRun 描述的是一条线性推进的运行记录, + * 这里描述的是画布上「哪张卡片接在哪张卡片下面」的树。两者不是同一个东西, + * 本验证 PR 不试图把它们合并——真接后端时再决定谁承载谁。 + */ + +/** 卡片种类。种类由产品定义,用户只能从加号菜单里选,不能自己造。 */ +export type CardKind = + | 'origin' + | 'candidates' + | 'master' + | 'first-frame' + | 'animation' + | 'review' + | 'export' + +/** + * 卡片的生命周期。 + * idle 表示等待用户填写或提交,running 表示产物正在逐个到达, + * review 表示产物已到齐等待确认,confirmed 表示已确认、可作为下游的上游。 + */ +export type CardStatus = 'idle' | 'running' | 'review' | 'confirmed' + +/** + * 一个动作在这条分支上的规格。 + * kind 区分定义来源(预设 / 自定义),type 是业务语义,两者独立—— + * 自定义来源同样可以描述 walk,这与 entities/character 的定义一致。 + */ +export interface ActionSpec { + kind: ActionKind + type: ActionType + name: string + /** 预设动作填一句步态描述;自定义动作还要填名称。 */ + brief: string + frameCount: number + fps: number + /** 帧数被用户改过则为 true,卡片上据此显示覆盖记号。 */ + frameCountOverridden: boolean +} + +export interface Card { + id: string + kind: CardKind + /** 上游卡片;起点卡片没有上游。连线由这个字段推出来,用户不画线。 */ + parentId: string | null + status: CardStatus + /** 只有动作分支上的卡片(首帧、完整动画、审核)带规格。 */ + action: ActionSpec | null + /** 已到达的产物 URL,按到达顺序排列。 */ + images: string[] + /** 本次生成预期的产物总数;未开始生成时为 0。 */ + total: number +} + +export interface CanvasGraph { + cards: Card[] + /** 起点卡片里填的角色描述。 */ + characterBrief: string + /** 已选中的母版候选 ID。 */ + selectedCandidateId: string | null +} + +/* ---------- 演示素材 ---------- */ + +/** 母版候选。四张都是 live demo 里已有的真实角色底图。 */ +export const CANDIDATES = [ + { id: 'samurai', label: '武士', imageUrl: '/demo/candidates/samurai.png' }, + { id: 'boy', label: '少年', imageUrl: '/demo/candidates/boy.png' }, + { id: 'lirael', label: '法师', imageUrl: '/demo/candidates/lirael.png' }, + { id: 'skeleton', label: '骷髅', imageUrl: '/demo/candidates/skeleton.png' }, +] as const + +/** + * 预设动作。规格已预先优化到位,用户只填一句描述。 + * 三个都有 samurai 的真实帧,所以演示全程不用占位图。 + */ +export const PRESET_ACTIONS: readonly { type: ActionType; name: string; placeholder: string }[] = [ + { type: 'idle', name: 'Idle 待机', placeholder: '描述呼吸节奏、重心与待机时的小动作' }, + { type: 'walk', name: 'Walk 行走', placeholder: '描述步幅、重心起伏与行进速度' }, + { type: 'jump', name: 'Jump 跳跃', placeholder: '描述起跳发力、滞空姿态与落地缓冲' }, +] + +const FRAME_SOURCE: Record = { idle: 'idle', walk: 'walk', jump: 'jump' } + +/** 某个动作的帧路径。自定义动作没有对应素材,统一借用 jump 的帧演示。 */ +export function framesOf(type: ActionType, count: number): string[] { + const source = FRAME_SOURCE[type] ?? 'jump' + return Array.from( + { length: count }, + (_, i) => `/demo/samurai/${source}-${String((i % 36) + 1).padStart(2, '0')}.png`, + ) +} + +/* ---------- 加号菜单 ---------- */ + +export interface PlusOption { + id: string + label: string + /** 菜单项下方的一行说明;没有则不显示。 */ + hint?: string + /** 本期不做但保留在菜单里的项,用于说明菜单可扩展。 */ + disabled?: boolean + children?: PlusOption[] +} + +/** + * 一张卡片的全部合法下游。 + * 菜单只列合法项,所以用户挑不到非法组合,也就不存在连接后再报错的路径。 + */ +export function plusOptions(card: Card): PlusOption[] { + if (card.status !== 'confirmed') return [] + if (card.kind === 'master') { + return [ + { + id: 'action', + label: '生成动作', + children: [ + ...PRESET_ACTIONS.map((preset) => ({ + id: `preset:${preset.type}`, + label: preset.name, + hint: '预设动作 · 逐帧生成', + })), + { id: 'custom', label: '自定义动作', hint: '自己描述 · 图生视频' }, + ], + }, + { + id: 'static', + label: '生成静态资产', + hint: '本期不做,需单独提案', + disabled: true, + }, + { id: 'export', label: '导出', hint: '打包已确认的动作' }, + ] + } + if (card.kind === 'first-frame') { + return [{ id: 'animation', label: '完整动画' }] + } + // 审核通过的动作可以直接进导出,不必绕回母版。 + if (card.kind === 'review') { + return [{ id: 'export', label: '导出', hint: '打包已确认的动作' }] + } + return [] +} + +/* ---------- 图操作 ---------- */ + +let seq = 0 +const nextId = (kind: CardKind) => `${kind}-${++seq}` + +export function createGraph(): CanvasGraph { + seq = 0 + return { + cards: [ + { + id: nextId('origin'), + kind: 'origin', + parentId: null, + status: 'idle', + action: null, + images: [], + total: 0, + }, + ], + characterBrief: '', + selectedCandidateId: null, + } +} + +export function findCard(graph: CanvasGraph, id: string): Card | undefined { + return graph.cards.find((card) => card.id === id) +} + +/** 一张卡片的全部下游,含自身。删除时用它算影响范围。 */ +export function descendants(graph: CanvasGraph, id: string): string[] { + const collected = [id] + for (let i = 0; i < collected.length; i += 1) { + for (const card of graph.cards) { + if (card.parentId === collected[i]) collected.push(card.id) + } + } + return collected +} + +export function removeCard(graph: CanvasGraph, id: string): CanvasGraph { + const doomed = new Set(descendants(graph, id)) + return { ...graph, cards: graph.cards.filter((card) => !doomed.has(card.id)) } +} + +export function updateCard(graph: CanvasGraph, id: string, patch: Partial): CanvasGraph { + return { + ...graph, + cards: graph.cards.map((card) => (card.id === id ? { ...card, ...patch } : card)), + } +} + +function appendCard(graph: CanvasGraph, card: Card): CanvasGraph { + return { ...graph, cards: [...graph.cards, card] } +} + +/** 默认帧数与帧率来自动作定义,卡片正面不问,用户要改得展开高级项。 */ +const DEFAULT_FRAME_COUNT = 36 +const DEFAULT_FPS = 8 + +/** + * 按加号菜单的选择追加一张卡片。 + * optionId 是 plusOptions 里叶子节点的 id;调用方保证它合法。 + */ +export function addCard(graph: CanvasGraph, parentId: string, optionId: string): CanvasGraph { + const parent = findCard(graph, parentId) + if (!parent) return graph + + if (optionId === 'export') { + // 导出是整张图的收口,母版和任一审核卡片都能开出它,但只留一张。 + if (graph.cards.some((card) => card.kind === 'export')) return graph + return appendCard(graph, { + id: nextId('export'), + kind: 'export', + parentId, + status: 'idle', + action: null, + images: [], + total: 0, + }) + } + + if (optionId === 'animation' && parent.action) { + return appendCard(graph, { + id: nextId('animation'), + kind: 'animation', + parentId, + status: 'idle', + action: parent.action, + images: [], + total: 0, + }) + } + + if (optionId.startsWith('preset:') || optionId === 'custom') { + const custom = optionId === 'custom' + const preset = custom ? null : PRESET_ACTIONS.find((item) => `preset:${item.type}` === optionId) + if (!custom && !preset) return graph + return appendCard(graph, { + id: nextId('first-frame'), + kind: 'first-frame', + parentId, + status: 'idle', + action: { + kind: (custom ? 'custom' : 'preset') as ActionKind, + type: custom ? 'custom' : preset!.type, + name: custom ? '' : preset!.name, + brief: '', + frameCount: DEFAULT_FRAME_COUNT, + fps: DEFAULT_FPS, + frameCountOverridden: false, + }, + images: [], + total: 0, + }) + } + + return graph +} + +/** + * 起点确认后自动接上母版候选;候选选定后自动接上母版。审核同理,不经加号。 + * + * status 默认 idle,但母版是个例外:它在候选被确认的那一刻就已锁定, + * 自身没有可执行的动作,因此直接以 confirmed 落地——加号挂在它身上, + * 若停在 idle 则整张图再也长不出下游。 + */ +export function autoAppend( + graph: CanvasGraph, + parentId: string, + kind: CardKind, + status: CardStatus = 'idle', +): CanvasGraph { + const parent = findCard(graph, parentId) + if (!parent) return graph + if (graph.cards.some((card) => card.parentId === parentId && card.kind === kind)) return graph + return appendCard(graph, { + id: nextId(kind), + kind, + parentId, + status, + action: parent.action, + images: [], + total: 0, + }) +} + +/* ---------- 布局 ---------- */ + +const COLUMN: Record = { + origin: 0, + candidates: 1, + master: 2, + 'first-frame': 3, + animation: 4, + review: 5, + export: 6, +} + +export const CARD_WIDTH: Record = { + origin: 300, + candidates: 320, + master: 260, + 'first-frame': 300, + animation: 400, + review: 260, + export: 280, +} + +const COLUMN_X = [0, 360, 740, 1060, 1420, 1880, 2220] +const ROW_HEIGHT = 420 + +/** + * 卡片位置由系统排布:列按种类固定,行按分支创建顺序。 + * 主干(起点到母版)占第 0 行,每条动作分支往下占一行,导出排在全部分支之后。 + */ +export function layout(graph: CanvasGraph): Record { + const rowOf = new Map() + let branch = 0 + for (const card of graph.cards) { + if (card.parentId === null) { + rowOf.set(card.id, 0) + continue + } + if (card.kind === 'first-frame') { + branch += 1 + rowOf.set(card.id, branch) + continue + } + rowOf.set(card.id, rowOf.get(card.parentId) ?? 0) + } + const exportRow = branch + 1 + const positions: Record = {} + for (const card of graph.cards) { + const row = card.kind === 'export' ? exportRow : (rowOf.get(card.id) ?? 0) + positions[card.id] = { x: COLUMN_X[COLUMN[card.kind]], y: row * ROW_HEIGHT } + } + return positions +} + +/* ---------- 假执行 ---------- */ + +/** 每张产物之间的间隔,够看清逐张到达又不至于让演示拖沓。 */ +const ARRIVAL_INTERVAL_MS = 120 + +export interface GenerationPlan { + total: number + urls: string[] +} + +/** 一张卡片这次生成会产出什么。全部来自本地素材,不发请求。 */ +export function planFor(card: Card): GenerationPlan { + if (card.kind === 'candidates') { + return { total: CANDIDATES.length, urls: CANDIDATES.map((item) => item.imageUrl) } + } + if (card.kind === 'first-frame' && card.action) { + return { total: 1, urls: framesOf(card.action.type, 1) } + } + if (card.kind === 'animation' && card.action) { + const urls = framesOf(card.action.type, card.action.frameCount) + return { total: urls.length, urls } + } + if (card.kind === 'export') { + return { total: 1, urls: [] } + } + return { total: 0, urls: [] } +} + +/** + * 逐张推送产物,模拟生成过程中的到达。 + * 返回取消函数;卡片被删除或组件卸载时调用,避免往已消失的卡片上写状态。 + */ +export function runGeneration( + plan: GenerationPlan, + onArrive: (images: string[]) => void, + onDone: () => void, +): () => void { + let index = 0 + const timer = setInterval(() => { + index += 1 + onArrive(plan.urls.slice(0, index)) + if (index >= plan.total) { + clearInterval(timer) + onDone() + } + }, ARRIVAL_INTERVAL_MS) + return () => clearInterval(timer) +} diff --git a/frontend/src/pages/workflow-editor/canvas-card.tsx b/frontend/src/pages/workflow-editor/canvas-card.tsx new file mode 100644 index 0000000..994077a --- /dev/null +++ b/frontend/src/pages/workflow-editor/canvas-card.tsx @@ -0,0 +1,346 @@ +import { useState } from 'react' + +import { + CANDIDATES, + PRESET_ACTIONS, + plusOptions, + type ActionSpec, + type Card, +} from '@/features/canvas-flow' +import { FrameArrivalStrip, VisualOptionPicker } from '@/shared/ui' +import { PlusMenu } from './plus-menu' + +const EYEBROW: Record = { + origin: '01 · SOURCE', + candidates: '02 · GENERATE', + master: '03 · IDENTITY', + 'first-frame': '04 · KEYFRAME', + animation: '05 · SEQUENCE', + review: '06 · REVIEW', + export: '07 · DELIVERY', +} + +const STATUS_LABEL: Record = { + idle: '待填写', + running: '生成中', + review: '待确认', + confirmed: '已确认', +} + +export interface CanvasCardProps { + card: Card + brief: string + selectedCandidateId: string | null + onBriefChange: (value: string) => void + onActionChange: (patch: Partial) => void + onSelectCandidate: (id: string) => void + onGenerate: () => void + onConfirm: () => void + onPick: (optionId: string) => void + onRemove: () => void +} + +/** 画布上的一张卡片。种类决定正面放什么,状态决定给哪个动作。 */ +export function CanvasCard(props: CanvasCardProps) { + const { card } = props + const [menuOpen, setMenuOpen] = useState(false) + const [advanced, setAdvanced] = useState(false) + const options = plusOptions(card) + + return ( +
+
+ {EYEBROW[card.kind]} +

{titleOf(card)}

+ {STATUS_LABEL[card.status]} +
+ +
{renderBody(props, advanced, setAdvanced)}
+ + {/* 只有用户自己从加号挂上去的卡片能删;候选、母版、审核是系统接上的,删了会让图断掉。 */} + {card.kind === 'first-frame' || card.kind === 'export' ? ( + + ) : null} + + {options.length > 0 ? ( +
+ + {menuOpen ? ( + setMenuOpen(false)} /> + ) : null} +
+ ) : null} +
+ ) +} + +function titleOf(card: Card): string { + switch (card.kind) { + case 'origin': + return '角色起点' + case 'candidates': + return '母版候选' + case 'master': + return '身份母版' + case 'first-frame': + return `${actionLabel(card)} · 首帧` + case 'animation': + return `${actionLabel(card)} · 完整动画` + case 'review': + return `${actionLabel(card)} · 审核` + case 'export': + return '导出资源包' + } +} + +function actionLabel(card: Card): string { + if (!card.action) return '动作' + return card.action.name.trim() || '未命名动作' +} + +function renderBody( + props: CanvasCardProps, + advanced: boolean, + setAdvanced: (next: boolean) => void, +) { + const { card } = props + + if (card.kind === 'origin') { + return ( + <> +