P2~P7 리노컷 캐스트 원화·리그와 세션 활성화
- 추천 원화(P2 b·P3 v2b·P4 v2b·P5 v2a·P6 v2b·P7 v2b)로 얼굴 없는 기본형·몸 원본 codex 편집, 정렬·정규화 후 공통 파이프라인 실행 - 게시 자산 apps/web/public/avatar/v3/p2~p7, 리그 rigs/p2Rig~p7Rig - 세션 레지스트리 P1~P7 전원 활성, Lab은 생성된 리그 자동 수집 - P3·P6 기본 눈 감김을 줄여 중립 렌더를 원화 눈매에 맞춤 - 표정 E2E를 v3 렌더러 기대로 갱신(검사 의도 유지)
|
|
@ -53,26 +53,35 @@ test.describe("avatar expression lab", () => {
|
|||
);
|
||||
await expect(panel.locator('[data-expression-card="true"]')).toHaveCount(28);
|
||||
|
||||
const cardRenderers = panel.locator(
|
||||
'.axl__card .vg-avatar [data-avatar-renderer="linocut"]',
|
||||
);
|
||||
await expect(cardRenderers).toHaveCount(28);
|
||||
await expect
|
||||
.poll(
|
||||
async () =>
|
||||
cardRenderers.evaluateAll(
|
||||
(els) => els.filter((el) => el.getAttribute("data-load-state") === "ready").length,
|
||||
),
|
||||
{ timeout: 20_000 },
|
||||
)
|
||||
.toBe(28);
|
||||
|
||||
const sectionMetrics = await panel.evaluate((el) => {
|
||||
const avatars = Array.from(el.querySelectorAll<HTMLElement>(".axl__card .vg-avatar"));
|
||||
const invalid = avatars
|
||||
.map((avatar) => {
|
||||
const neck = avatar.querySelector<SVGGraphicsElement>('[data-avatar-neck="true"]');
|
||||
const svg = avatar.querySelector("svg");
|
||||
const renderer = avatar.querySelector<HTMLElement>('[data-avatar-renderer="linocut"]');
|
||||
const svg = avatar.querySelector(".linocut-avatar");
|
||||
return {
|
||||
affect: avatar.getAttribute("data-affect"),
|
||||
primitives: avatar.querySelectorAll("svg path, svg ellipse, svg circle, svg line, svg rect")
|
||||
.length,
|
||||
neckBox: neck?.getBoundingClientRect().toJSON(),
|
||||
loadState: renderer?.getAttribute("data-load-state"),
|
||||
svgBox: svg?.getBoundingClientRect().toJSON(),
|
||||
};
|
||||
})
|
||||
.filter((item) => {
|
||||
return (
|
||||
item.primitives < 12 ||
|
||||
!item.neckBox ||
|
||||
item.neckBox.width <= 8 ||
|
||||
item.neckBox.height <= 14 ||
|
||||
item.loadState !== "ready" ||
|
||||
!item.svgBox ||
|
||||
item.svgBox.width <= 0 ||
|
||||
item.svgBox.height <= 0
|
||||
|
|
|
|||
|
|
@ -197,6 +197,10 @@ test.describe("persona avatar expression rig", () => {
|
|||
await expect(avatar).toBeVisible();
|
||||
await expect(avatar).toHaveAttribute("data-affect", persona.expectedExpression);
|
||||
|
||||
const v3 = avatar.locator('[data-avatar-renderer="linocut"]');
|
||||
await expect(v3).toHaveAttribute("data-load-state", "ready", { timeout: 15_000 });
|
||||
await expect(v3.locator(".linocut-avatar")).toBeVisible();
|
||||
|
||||
const metrics = await avatar.evaluate((el) => ({
|
||||
expressionCount: Number(el.getAttribute("data-expression-count")),
|
||||
live2dSchema: el.getAttribute("data-live2d-schema"),
|
||||
|
|
@ -207,10 +211,7 @@ test.describe("persona avatar expression rig", () => {
|
|||
live2dExpressionCount: Number(el.getAttribute("data-live2d-expression-count")),
|
||||
renderMode: el.getAttribute("data-render-mode"),
|
||||
rasterCount: el.querySelectorAll(".vg-raster").length,
|
||||
primitiveCount: el.querySelectorAll("svg path, svg ellipse, svg circle, svg line, svg rect")
|
||||
.length,
|
||||
neckBox: el.querySelector<SVGGraphicsElement>('[data-avatar-neck="true"]')?.getBoundingClientRect().toJSON(),
|
||||
svgBox: el.querySelector("svg")?.getBoundingClientRect().toJSON(),
|
||||
v3Box: el.querySelector(".linocut-avatar")?.getBoundingClientRect().toJSON(),
|
||||
}));
|
||||
expect(metrics.expressionCount, `${persona.code} expression count`).toBeGreaterThanOrEqual(20);
|
||||
expect(metrics.live2dSchema, `${persona.code} Live2D schema`).toBe("vignette.live2d.v1");
|
||||
|
|
@ -225,10 +226,8 @@ test.describe("persona avatar expression rig", () => {
|
|||
expect(metrics.live2dExpressionCount, `${persona.code} Live2D expressions`).toBeGreaterThanOrEqual(20);
|
||||
expect(metrics.renderMode, `${persona.code} render mode`).toBe("svg");
|
||||
expect(metrics.rasterCount, `${persona.code} raster rig should be disabled`).toBe(0);
|
||||
expect(metrics.primitiveCount, `${persona.code} SVG primitives`).toBeGreaterThan(10);
|
||||
expect(metrics.neckBox?.width, `${persona.code} SVG neck`).toBeGreaterThan(0);
|
||||
expect(metrics.svgBox?.width, `${persona.code} SVG rig`).toBeGreaterThan(0);
|
||||
expect(metrics.svgBox?.height, `${persona.code} SVG rig`).toBeGreaterThan(0);
|
||||
expect(metrics.v3Box?.width, `${persona.code} linocut SVG`).toBeGreaterThan(0);
|
||||
expect(metrics.v3Box?.height, `${persona.code} linocut SVG`).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -261,9 +260,10 @@ test.describe("persona avatar expression rig", () => {
|
|||
|
||||
const activeAvatar = page.locator(".sx-page--active .vg-avatar").first();
|
||||
await expect(activeAvatar).toBeVisible();
|
||||
await expect(activeAvatar).toHaveAttribute("data-render-mode", "svg");
|
||||
const activeV3 = activeAvatar.locator('[data-avatar-renderer="linocut"]');
|
||||
await expect(activeV3).toHaveAttribute("data-load-state", "ready", { timeout: 15_000 });
|
||||
await expect(activeV3.locator(".linocut-avatar")).toBeVisible();
|
||||
await expect(activeAvatar.locator(".vg-raster")).toHaveCount(0);
|
||||
await expect(activeAvatar.locator(".vg-avatar__svg")).toBeVisible();
|
||||
await expect(activeAvatar).toHaveAttribute("data-expression-count", "28");
|
||||
await expect(activeAvatar).toHaveAttribute("data-live2d-expression-count", "28");
|
||||
await expect(activeAvatar).toHaveAttribute("data-live2d-model", "vignette-p4-live2d");
|
||||
|
|
@ -316,9 +316,10 @@ test.describe("persona avatar expression rig", () => {
|
|||
await page.getByRole("button", { name: "회기 시작" }).click();
|
||||
await expect(page.locator(".sx-page--active")).toBeVisible();
|
||||
const activeAvatar = page.locator(".sx-page--active .vg-avatar").first();
|
||||
await expect(activeAvatar).toHaveAttribute("data-render-mode", "svg");
|
||||
const activeV3 = activeAvatar.locator('[data-avatar-renderer="linocut"]');
|
||||
await expect(activeV3).toHaveAttribute("data-load-state", "ready", { timeout: 15_000 });
|
||||
await expect(activeV3.locator(".linocut-avatar")).toBeVisible();
|
||||
await expect(activeAvatar.locator(".vg-raster")).toHaveCount(0);
|
||||
await expect(activeAvatar.locator(".vg-avatar__svg")).toBeVisible();
|
||||
await expect(activeAvatar).toHaveAttribute("data-live2d-motion", "anxious");
|
||||
|
||||
await setLearnerUtterance(page, "조금 안정된 것 같아요.");
|
||||
|
|
|
|||
BIN
apps/web/public/avatar/v3/p2/body.webp
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
apps/web/public/avatar/v3/p2/brow-left.webp
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
apps/web/public/avatar/v3/p2/brow-right.webp
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
apps/web/public/avatar/v3/p2/face-detail.webp
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
apps/web/public/avatar/v3/p2/hair-front.webp
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
apps/web/public/avatar/v3/p2/head.webp
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
apps/web/public/avatar/v3/p2/jaw-detail.webp
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
apps/web/public/avatar/v3/p2/jaw-head.webp
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
apps/web/public/avatar/v3/p2/lip-lower.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
apps/web/public/avatar/v3/p2/lip-shadow.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
apps/web/public/avatar/v3/p2/lip-upper.webp
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
apps/web/public/avatar/v3/p2/paper-grain.webp
Normal file
|
After Width: | Height: | Size: 1 KiB |
BIN
apps/web/public/avatar/v3/p3/body.webp
Normal file
|
After Width: | Height: | Size: 192 KiB |
BIN
apps/web/public/avatar/v3/p3/brow-left.webp
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
apps/web/public/avatar/v3/p3/brow-right.webp
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
apps/web/public/avatar/v3/p3/face-detail.webp
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
apps/web/public/avatar/v3/p3/hair-front.webp
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
apps/web/public/avatar/v3/p3/head.webp
Normal file
|
After Width: | Height: | Size: 135 KiB |
BIN
apps/web/public/avatar/v3/p3/jaw-detail.webp
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
apps/web/public/avatar/v3/p3/jaw-head.webp
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
apps/web/public/avatar/v3/p3/lip-lower.webp
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
apps/web/public/avatar/v3/p3/lip-shadow.webp
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
apps/web/public/avatar/v3/p3/lip-upper.webp
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
apps/web/public/avatar/v3/p3/paper-grain.webp
Normal file
|
After Width: | Height: | Size: 980 B |
BIN
apps/web/public/avatar/v3/p4/body.webp
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
apps/web/public/avatar/v3/p4/brow-left.webp
Normal file
|
After Width: | Height: | Size: 5 KiB |
BIN
apps/web/public/avatar/v3/p4/brow-right.webp
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
apps/web/public/avatar/v3/p4/face-detail.webp
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
apps/web/public/avatar/v3/p4/hair-front.webp
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
apps/web/public/avatar/v3/p4/head.webp
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
apps/web/public/avatar/v3/p4/jaw-detail.webp
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
apps/web/public/avatar/v3/p4/jaw-head.webp
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
apps/web/public/avatar/v3/p4/lip-lower.webp
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
apps/web/public/avatar/v3/p4/lip-shadow.webp
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
apps/web/public/avatar/v3/p4/lip-upper.webp
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
apps/web/public/avatar/v3/p4/paper-grain.webp
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
apps/web/public/avatar/v3/p5/body.webp
Normal file
|
After Width: | Height: | Size: 139 KiB |
BIN
apps/web/public/avatar/v3/p5/brow-left.webp
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
apps/web/public/avatar/v3/p5/brow-right.webp
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
apps/web/public/avatar/v3/p5/face-detail.webp
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
apps/web/public/avatar/v3/p5/hair-front.webp
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
apps/web/public/avatar/v3/p5/head.webp
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
apps/web/public/avatar/v3/p5/jaw-detail.webp
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
apps/web/public/avatar/v3/p5/jaw-head.webp
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
apps/web/public/avatar/v3/p5/lip-lower.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
apps/web/public/avatar/v3/p5/lip-shadow.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
apps/web/public/avatar/v3/p5/lip-upper.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
apps/web/public/avatar/v3/p5/paper-grain.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
apps/web/public/avatar/v3/p6/body.webp
Normal file
|
After Width: | Height: | Size: 167 KiB |
BIN
apps/web/public/avatar/v3/p6/brow-left.webp
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
apps/web/public/avatar/v3/p6/brow-right.webp
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
apps/web/public/avatar/v3/p6/face-detail.webp
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
apps/web/public/avatar/v3/p6/hair-front.webp
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
apps/web/public/avatar/v3/p6/head.webp
Normal file
|
After Width: | Height: | Size: 167 KiB |
BIN
apps/web/public/avatar/v3/p6/jaw-detail.webp
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
apps/web/public/avatar/v3/p6/jaw-head.webp
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
apps/web/public/avatar/v3/p6/lip-lower.webp
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
apps/web/public/avatar/v3/p6/lip-shadow.webp
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
apps/web/public/avatar/v3/p6/lip-upper.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
apps/web/public/avatar/v3/p6/paper-grain.webp
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
apps/web/public/avatar/v3/p7/body.webp
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
apps/web/public/avatar/v3/p7/brow-left.webp
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
apps/web/public/avatar/v3/p7/brow-right.webp
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
apps/web/public/avatar/v3/p7/face-detail.webp
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
apps/web/public/avatar/v3/p7/hair-front.webp
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
apps/web/public/avatar/v3/p7/head.webp
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
apps/web/public/avatar/v3/p7/jaw-detail.webp
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
apps/web/public/avatar/v3/p7/jaw-head.webp
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
apps/web/public/avatar/v3/p7/lip-lower.webp
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
apps/web/public/avatar/v3/p7/lip-shadow.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
apps/web/public/avatar/v3/p7/lip-upper.webp
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
apps/web/public/avatar/v3/p7/paper-grain.webp
Normal file
|
After Width: | Height: | Size: 980 B |
|
|
@ -88,9 +88,11 @@ const P2 = demeanor({
|
|||
speech: { articulation: 0.85, syllablesPerSec: 6.4, gesture: 0.9, gazeAvertProb: 0.5 },
|
||||
});
|
||||
|
||||
/* P3 지우 — 28세 미혼모, 역할 부담, 따뜻하지만 지친 톤. */
|
||||
/* P3 지우 — 28세 미혼모, 역할 부담, 따뜻하지만 지친 톤.
|
||||
눈 감김(eyeOpen)은 −0.15에서 −0.05로 줄였다(2026-10-01): 리노컷 원화가 지친 눈매를 이미
|
||||
그리고 있어 기본 자세로 더 감기면 원화보다 졸린 눈이 된다(결정문 §8.4.1 "중립 = 원화"). */
|
||||
const P3 = demeanor({
|
||||
basePose: { eyeOpen: -0.15, eyeSmile: 0.08, shoulderRaise: -0.15 },
|
||||
basePose: { eyeOpen: -0.05, eyeSmile: 0.08, shoulderRaise: -0.15 },
|
||||
expressivity: 0.9,
|
||||
blinkIntervalMs: [2200, 4600],
|
||||
blinkDurationMs: 140,
|
||||
|
|
@ -143,9 +145,10 @@ const P5 = demeanor({
|
|||
speech: { articulation: 0.65, syllablesPerSec: 5.8, gesture: 0.6, gazeAvertProb: 0.6 },
|
||||
});
|
||||
|
||||
/* P6 하린 — 고3, 진로갈등, 똑부러지지만 지친 톤. */
|
||||
/* P6 하린 — 고3, 진로갈등, 똑부러지지만 지친 톤.
|
||||
눈 감김 기본 자세(−0.08)는 뺐다(2026-10-01, P3과 같은 이유 — 원화의 다크서클이 지침을 맡는다). */
|
||||
const P6 = demeanor({
|
||||
basePose: { eyeOpen: -0.08 },
|
||||
basePose: {},
|
||||
expressivity: 0.9,
|
||||
blinkIntervalMs: [2000, 4200],
|
||||
blinkDurationMs: 120,
|
||||
|
|
|
|||
|
|
@ -1,16 +1,50 @@
|
|||
/* =====================================================================
|
||||
아바타 v3 리그 레지스트리 — 결정문 §8.5 "분기". 페르소나 코드 → 리노컷
|
||||
리그. 지금은 P1만 있고 나머지는 null(기존 SVG로 남는다). 빌드 플래그
|
||||
VITE_AVATAR_V3=0 이면 항상 null(기본은 켜짐).
|
||||
리그. 세션용(linocutRigFor)은 오케스트레이터가 표정 시트를 판정한 페르소나만
|
||||
켠다 — 2026-10-01 P1~P7 전원(결정문 §8.4.1). 목록에 없으면 null(기존 SVG).
|
||||
빌드 플래그 VITE_AVATAR_V3=0 이면 항상 null(기본은 켜짐).
|
||||
===================================================================== */
|
||||
|
||||
import type { LinocutRig } from "../linocutRig";
|
||||
import { P1_LINOCUT_RIG } from "./p1Rig";
|
||||
import { P2_LINOCUT_RIG } from "./p2Rig";
|
||||
import { P3_LINOCUT_RIG } from "./p3Rig";
|
||||
import { P4_LINOCUT_RIG } from "./p4Rig";
|
||||
import { P5_LINOCUT_RIG } from "./p5Rig";
|
||||
import { P6_LINOCUT_RIG } from "./p6Rig";
|
||||
import { P7_LINOCUT_RIG } from "./p7Rig";
|
||||
|
||||
const LINOCUT_RIGS: Partial<Record<string, LinocutRig>> = { P1: P1_LINOCUT_RIG };
|
||||
const LINOCUT_RIGS: Partial<Record<string, LinocutRig>> = {
|
||||
P1: P1_LINOCUT_RIG,
|
||||
P2: P2_LINOCUT_RIG,
|
||||
P3: P3_LINOCUT_RIG,
|
||||
P4: P4_LINOCUT_RIG,
|
||||
P5: P5_LINOCUT_RIG,
|
||||
P6: P6_LINOCUT_RIG,
|
||||
P7: P7_LINOCUT_RIG,
|
||||
};
|
||||
|
||||
export function linocutRigFor(code: string | null | undefined): LinocutRig | null {
|
||||
if (import.meta.env.VITE_AVATAR_V3 === "0") return null;
|
||||
if (!code) return null;
|
||||
return LINOCUT_RIGS[code.toUpperCase()] ?? null;
|
||||
}
|
||||
|
||||
/* Lab(AvatarLab.tsx)용 — 공통 파이프라인(scripts/export_rig.py)이 만드는 ./pN Rig.ts를
|
||||
전부 모아 rig.persona로 색인한다. 세션 화면(linocutRigFor)과는 무관하다 —
|
||||
persona.json이 아직 없는 pNRig.ts가 생기는 즉시 Lab 선택지에 나타나지만,
|
||||
세션에는 영향이 없다(세션은 위 LINOCUT_RIGS 목록만 본다). */
|
||||
const labRigModules = import.meta.glob("./p*Rig.ts", { eager: true }) as Record<string, Record<string, unknown>>;
|
||||
|
||||
export function labLinocutRigs(): Partial<Record<string, LinocutRig>> {
|
||||
const out: Partial<Record<string, LinocutRig>> = {};
|
||||
for (const mod of Object.values(labRigModules)) {
|
||||
for (const exported of Object.values(mod)) {
|
||||
const rig = exported as Partial<LinocutRig> | null;
|
||||
if (rig && typeof rig === "object" && rig.schemaVersion === "vignette.avatar.v3.rig.v1" && typeof rig.persona === "string") {
|
||||
out[rig.persona.toUpperCase()] = rig as LinocutRig;
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
|
|
|||
86
apps/web/src/components/avatar/v3/rigs/p2Rig.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
/* 생성 파일 — docs/avatar-art/linocut-pipeline/scripts/export_rig.py 가 만든다. 손으로 고치지 않는다. */
|
||||
import type { LinocutRig } from "../linocutRig";
|
||||
|
||||
export const P2_LINOCUT_RIG: LinocutRig = {
|
||||
schemaVersion: "vignette.avatar.v3.rig.v1",
|
||||
persona: "P2",
|
||||
canvas: { w: 1005, h: 1566 },
|
||||
layers: {
|
||||
body: { href: "/avatar/v3/p2/body.webp", x: 0, y: 751, w: 1005, h: 815 },
|
||||
head: { href: "/avatar/v3/p2/head.webp", x: 164, y: 84, w: 667, h: 877 },
|
||||
hairFront: { href: "/avatar/v3/p2/hair-front.webp", x: 240, y: 398, w: 498, h: 460 },
|
||||
faceDetail: { href: "/avatar/v3/p2/face-detail.webp", x: 274, y: 405, w: 449, h: 550 },
|
||||
},
|
||||
grain: { href: "/avatar/v3/p2/paper-grain.webp", size: 256 },
|
||||
lipTexture: {
|
||||
upper: { href: "/avatar/v3/p2/lip-upper.webp", x: 404, y: 772, w: 182, h: 51 },
|
||||
lower: { href: "/avatar/v3/p2/lip-lower.webp", x: 404, y: 813, w: 181, h: 48 },
|
||||
shadow: { href: "/avatar/v3/p2/lip-shadow.webp", x: 414, y: 817, w: 155, h: 52 },
|
||||
},
|
||||
jaw: {
|
||||
head: { href: "/avatar/v3/p2/jaw-head.webp", x: 246, y: 658, w: 506, h: 333 },
|
||||
detail: { href: "/avatar/v3/p2/jaw-detail.webp", x: 246, y: 658, w: 506, h: 333 },
|
||||
},
|
||||
pivots: {
|
||||
neck: [496.4, 1021.2],
|
||||
body: [502, 1566],
|
||||
face: [492, 658.9],
|
||||
},
|
||||
crops: {
|
||||
portrait: [0, 0, 1005, 1566],
|
||||
bust: [0, 40, 1005, 1005],
|
||||
face: [181.6, 364.6, 630.6, 630.6],
|
||||
},
|
||||
faceOval: [[492.2, 398.4], [558, 399.3], [614.2, 407.7], [665.7, 426.4], [699, 456.1], [719.9, 494.6], [731.3, 536], [736, 586.9], [734.6, 635.4], [730.5, 685.8], [721.5, 739], [704.8, 795.1], [682.2, 840.4], [657.5, 873.5], [627.1, 903.3], [601, 924.5], [572.8, 943.3], [539.3, 957.3], [496.4, 961.4], [453.8, 957.3], [421, 943.3], [393.2, 924.7], [367.4, 904.2], [338.1, 874.9], [315, 842], [293.7, 797.6], [277.1, 741.8], [267, 688.9], [261.6, 639.6], [257.9, 591.7], [260.7, 541.3], [270.6, 499.5], [290.1, 460.8], [321.9, 430.6], [371.3, 410.5], [426.6, 401]],
|
||||
landmarks: {
|
||||
eyeLeft: {
|
||||
inner: [436.9, 589.7],
|
||||
outer: [345.9, 578.6],
|
||||
upperLidTop: [382.5, 563.5],
|
||||
lowerLidBottom: [387, 595.5],
|
||||
iris: { center: [391, 576], radius: 20 },
|
||||
lineScale: 2.5,
|
||||
},
|
||||
eyeRight: {
|
||||
inner: [553.4, 588.3],
|
||||
outer: [643.7, 575.1],
|
||||
upperLidTop: [606.6, 560.8],
|
||||
lowerLidBottom: [603.5, 593],
|
||||
iris: { center: [598.5, 573.5], radius: 20.5 },
|
||||
lineScale: 2.5,
|
||||
},
|
||||
browLeft: { inner: [459.2, 529.9], peak: [359.4, 517.4], outer: [307.8, 526.2], halfWidth: [12.8, 16.5, 2.4], sprite: { href: "/avatar/v3/p2/brow-left.webp", x: 307, y: 500, w: 158, h: 55 } },
|
||||
browRight: { inner: [522.2, 526.9], peak: [630.8, 512.7], outer: [686.8, 520.9], halfWidth: [15.3, 12.2, 6.8], sprite: { href: "/avatar/v3/p2/brow-right.webp", x: 515, y: 492, w: 176, h: 57 } },
|
||||
noseTip: [494.7, 718.4],
|
||||
mouthCornerLeft: [417.4, 818.3],
|
||||
mouthCornerRight: [572.3, 816.9],
|
||||
upperLipTop: [494.6, 787.6],
|
||||
lowerLipBottom: [494.1, 843.9],
|
||||
mouthCenter: [494.6, 812.3],
|
||||
chinTip: [496.4, 950.7],
|
||||
},
|
||||
palette: {
|
||||
ink: "#1E1E1D",
|
||||
sclera: "#D8CEBD",
|
||||
iris: "#4F3B2C",
|
||||
irisRing: "#1E1F1F",
|
||||
lipUpper: "#9D7253",
|
||||
lipLower: "#A87857",
|
||||
mouthLine: "#242220",
|
||||
mouthInner: "#3B201B",
|
||||
teeth: "#E9E0CF",
|
||||
blush: "#C0624A",
|
||||
tear: "#EEE5D3",
|
||||
pallor: "#9AA3A6",
|
||||
paper: "#EEE5D3",
|
||||
motifPetal: "#D0A362",
|
||||
motifLeaf: "#53626C",
|
||||
},
|
||||
backdrop: {
|
||||
cognitive: "#ECE3D1",
|
||||
positive: "#F1DEC2",
|
||||
negative: "#DCE0E2",
|
||||
defensive: "#E6DAD3",
|
||||
energy: "#E2E0D0",
|
||||
},
|
||||
};
|
||||
86
apps/web/src/components/avatar/v3/rigs/p3Rig.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
/* 생성 파일 — docs/avatar-art/linocut-pipeline/scripts/export_rig.py 가 만든다. 손으로 고치지 않는다. */
|
||||
import type { LinocutRig } from "../linocutRig";
|
||||
|
||||
export const P3_LINOCUT_RIG: LinocutRig = {
|
||||
schemaVersion: "vignette.avatar.v3.rig.v1",
|
||||
persona: "P3",
|
||||
canvas: { w: 1005, h: 1566 },
|
||||
layers: {
|
||||
body: { href: "/avatar/v3/p3/body.webp", x: 0, y: 848, w: 1005, h: 718 },
|
||||
head: { href: "/avatar/v3/p3/head.webp", x: 132, y: 156, w: 742, h: 888 },
|
||||
hairFront: { href: "/avatar/v3/p3/hair-front.webp", x: 237, y: 398, w: 540, h: 535 },
|
||||
faceDetail: { href: "/avatar/v3/p3/face-detail.webp", x: 279, y: 397, w: 443, h: 542 },
|
||||
},
|
||||
grain: { href: "/avatar/v3/p3/paper-grain.webp", size: 256 },
|
||||
lipTexture: {
|
||||
upper: { href: "/avatar/v3/p3/lip-upper.webp", x: 424, y: 756, w: 173, h: 51 },
|
||||
lower: { href: "/avatar/v3/p3/lip-lower.webp", x: 423, y: 792, w: 171, h: 54 },
|
||||
shadow: { href: "/avatar/v3/p3/lip-shadow.webp", x: 433, y: 791, w: 148, h: 63 },
|
||||
},
|
||||
jaw: {
|
||||
head: { href: "/avatar/v3/p3/jaw-head.webp", x: 243, y: 644, w: 512, h: 332 },
|
||||
detail: { href: "/avatar/v3/p3/jaw-detail.webp", x: 243, y: 644, w: 512, h: 332 },
|
||||
},
|
||||
pivots: {
|
||||
neck: [518, 1005],
|
||||
body: [502, 1566],
|
||||
face: [492, 652],
|
||||
},
|
||||
crops: {
|
||||
portrait: [0, 0, 1005, 1566],
|
||||
bust: [0, 40, 1005, 1005],
|
||||
face: [185.9, 356.9, 621.9, 621.9],
|
||||
},
|
||||
faceOval: [[475.8, 393.7], [540.7, 390.2], [596.5, 395.8], [649.6, 413.2], [685.4, 442.2], [709.3, 480.3], [724.7, 521.2], [733.9, 571.8], [736.4, 619.7], [736.4, 669.4], [731, 722.5], [718.2, 777.9], [698.5, 822.4], [674.5, 854.4], [643.8, 883.5], [618.4, 904.1], [591.4, 922.9], [559.9, 938.4], [519, 945.4], [477.9, 944.3], [445.2, 933.5], [416.7, 919.2], [389.2, 903.2], [355.4, 879.7], [328.1, 851.8], [302.7, 811.3], [282.3, 758], [269.2, 706.1], [262.4, 657.2], [257.2, 610.1], [258.2, 560], [266.7, 517.2], [283.8, 476], [312.7, 441.9], [359, 416], [411.7, 401.4]],
|
||||
landmarks: {
|
||||
eyeLeft: {
|
||||
inner: [435.1, 593.8],
|
||||
outer: [346.3, 589.4],
|
||||
upperLidTop: [382.5, 570.1],
|
||||
lowerLidBottom: [386.7, 602.2],
|
||||
iris: { center: [389, 580], radius: 20 },
|
||||
lineScale: 1,
|
||||
},
|
||||
eyeRight: {
|
||||
inner: [552.1, 583.6],
|
||||
outer: [640.8, 564.7],
|
||||
upperLidTop: [600.8, 552.1],
|
||||
lowerLidBottom: [601, 583.9],
|
||||
iris: { center: [593.5, 558.5], radius: 19.5 },
|
||||
lineScale: 1,
|
||||
},
|
||||
browLeft: { inner: [438, 513.2], peak: [354.9, 519.8], outer: [312, 530.8], halfWidth: [7, 8.3, 3.8], sprite: { href: "/avatar/v3/p3/brow-left.webp", x: 314, y: 502, w: 129, h: 38 } },
|
||||
browRight: { inner: [521.3, 505.4], peak: [609.3, 496.5], outer: [654.7, 501], halfWidth: [3.9, 7, 2], sprite: { href: "/avatar/v3/p3/brow-right.webp", x: 517, y: 486, w: 142, h: 32 } },
|
||||
noseTip: [499.6, 704.4],
|
||||
mouthCornerLeft: [432.6, 803.4],
|
||||
mouthCornerRight: [583.6, 792.2],
|
||||
upperLipTop: [505.2, 769.8],
|
||||
lowerLipBottom: [509.4, 832.4],
|
||||
mouthCenter: [507.2, 796.4],
|
||||
chinTip: [518.3, 935.2],
|
||||
},
|
||||
palette: {
|
||||
ink: "#242322",
|
||||
sclera: "#D8CEBD",
|
||||
iris: "#4F3B2C",
|
||||
irisRing: "#1E1F1F",
|
||||
lipUpper: "#B4835D",
|
||||
lipLower: "#BD8A62",
|
||||
mouthLine: "#76553F",
|
||||
mouthInner: "#3B201B",
|
||||
teeth: "#E9E0CF",
|
||||
blush: "#C0624A",
|
||||
tear: "#EEE5D3",
|
||||
pallor: "#9AA3A6",
|
||||
paper: "#EEE5D3",
|
||||
motifPetal: "#D0A362",
|
||||
motifLeaf: "#53626C",
|
||||
},
|
||||
backdrop: {
|
||||
cognitive: "#ECE3D1",
|
||||
positive: "#F1DEC2",
|
||||
negative: "#DCE0E2",
|
||||
defensive: "#E6DAD3",
|
||||
energy: "#E2E0D0",
|
||||
},
|
||||
};
|
||||
86
apps/web/src/components/avatar/v3/rigs/p4Rig.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
/* 생성 파일 — docs/avatar-art/linocut-pipeline/scripts/export_rig.py 가 만든다. 손으로 고치지 않는다. */
|
||||
import type { LinocutRig } from "../linocutRig";
|
||||
|
||||
export const P4_LINOCUT_RIG: LinocutRig = {
|
||||
schemaVersion: "vignette.avatar.v3.rig.v1",
|
||||
persona: "P4",
|
||||
canvas: { w: 1005, h: 1566 },
|
||||
layers: {
|
||||
body: { href: "/avatar/v3/p4/body.webp", x: 0, y: 436, w: 1005, h: 1130 },
|
||||
head: { href: "/avatar/v3/p4/head.webp", x: 161, y: 97, w: 661, h: 924 },
|
||||
hairFront: { href: "/avatar/v3/p4/hair-front.webp", x: 234, y: 405, w: 534, h: 485 },
|
||||
faceDetail: { href: "/avatar/v3/p4/face-detail.webp", x: 266, y: 394, w: 470, h: 551 },
|
||||
},
|
||||
grain: { href: "/avatar/v3/p4/paper-grain.webp", size: 256 },
|
||||
lipTexture: {
|
||||
upper: { href: "/avatar/v3/p4/lip-upper.webp", x: 410, y: 777, w: 179, h: 46 },
|
||||
lower: { href: "/avatar/v3/p4/lip-lower.webp", x: 410, y: 816, w: 178, h: 51 },
|
||||
shadow: { href: "/avatar/v3/p4/lip-shadow.webp", x: 420, y: 814, w: 159, h: 61 },
|
||||
},
|
||||
jaw: {
|
||||
head: { href: "/avatar/v3/p4/jaw-head.webp", x: 250, y: 668, w: 504, h: 313 },
|
||||
detail: { href: "/avatar/v3/p4/jaw-detail.webp", x: 250, y: 668, w: 504, h: 313 },
|
||||
},
|
||||
pivots: {
|
||||
neck: [502.7, 1011.1],
|
||||
body: [502, 1566],
|
||||
face: [496, 655],
|
||||
},
|
||||
crops: {
|
||||
portrait: [0, 0, 1005, 1566],
|
||||
bust: [0, 40, 1005, 1005],
|
||||
face: [184.9, 354.2, 631.2, 631.2],
|
||||
},
|
||||
faceOval: [[495.7, 388], [569.6, 388.6], [631.4, 396.4], [687.2, 414.1], [721.3, 442.7], [739.9, 479.9], [748.2, 519.6], [748.9, 568.1], [743.7, 615], [735.8, 663.9], [723.6, 715.1], [704.8, 769.9], [681.9, 816.1], [657.7, 851.5], [628.5, 883.7], [603.9, 907.3], [576.9, 929.1], [544.6, 946.1], [502.7, 951.6], [461.8, 946.3], [430.4, 929.7], [403.7, 908.3], [378.7, 885.5], [348.8, 854], [324, 819.1], [300.1, 773.9], [280.7, 719], [267.6, 667.8], [259.3, 619.2], [252.7, 572.5], [252.2, 524.3], [258.8, 484.1], [275.1, 446.8], [306.7, 417.8], [360.2, 398.8], [421.5, 390.1]],
|
||||
landmarks: {
|
||||
eyeLeft: {
|
||||
inner: [430.6, 594.2],
|
||||
outer: [337.6, 576.7],
|
||||
upperLidTop: [375.9, 563.9],
|
||||
lowerLidBottom: [380.9, 598.5],
|
||||
iris: { center: [385, 576], radius: 20 },
|
||||
lineScale: 2.5,
|
||||
},
|
||||
eyeRight: {
|
||||
inner: [566.4, 593.4],
|
||||
outer: [659.2, 572.9],
|
||||
upperLidTop: [621.4, 561.7],
|
||||
lowerLidBottom: [616.8, 596.6],
|
||||
iris: { center: [611.5, 574.5], radius: 18.5 },
|
||||
lineScale: 2.5,
|
||||
},
|
||||
browLeft: { inner: [449.4, 524.6], peak: [353.2, 508.4], outer: [303.6, 507.6], halfWidth: [6.8, 6.5, 2.5], sprite: { href: "/avatar/v3/p4/brow-left.webp", x: 299, y: 499, w: 156, h: 36 } },
|
||||
browRight: { inner: [540.9, 523.7], peak: [642.6, 502.3], outer: [695.1, 500.2], halfWidth: [2.4, 6.7, 1.5], sprite: { href: "/avatar/v3/p4/brow-right.webp", x: 536, y: 491, w: 164, h: 42 } },
|
||||
noseTip: [498.6, 728.2],
|
||||
mouthCornerLeft: [426.2, 819.4],
|
||||
mouthCornerRight: [571.5, 818.6],
|
||||
upperLipTop: [497.9, 791.6],
|
||||
lowerLipBottom: [498.7, 852.3],
|
||||
mouthCenter: [498.4, 815.7],
|
||||
chinTip: [502.7, 940.6],
|
||||
},
|
||||
palette: {
|
||||
ink: "#22211F",
|
||||
sclera: "#D8CEBD",
|
||||
iris: "#4F3B2C",
|
||||
irisRing: "#1E1F1F",
|
||||
lipUpper: "#B48460",
|
||||
lipLower: "#C08E69",
|
||||
mouthLine: "#332B26",
|
||||
mouthInner: "#3B201B",
|
||||
teeth: "#E9E0CF",
|
||||
blush: "#C0624A",
|
||||
tear: "#EEE5D3",
|
||||
pallor: "#9AA3A6",
|
||||
paper: "#EEE5D3",
|
||||
motifPetal: "#D0A362",
|
||||
motifLeaf: "#53626C",
|
||||
},
|
||||
backdrop: {
|
||||
cognitive: "#ECE3D1",
|
||||
positive: "#F1DEC2",
|
||||
negative: "#DCE0E2",
|
||||
defensive: "#E6DAD3",
|
||||
energy: "#E2E0D0",
|
||||
},
|
||||
};
|
||||
86
apps/web/src/components/avatar/v3/rigs/p5Rig.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
/* 생성 파일 — docs/avatar-art/linocut-pipeline/scripts/export_rig.py 가 만든다. 손으로 고치지 않는다. */
|
||||
import type { LinocutRig } from "../linocutRig";
|
||||
|
||||
export const P5_LINOCUT_RIG: LinocutRig = {
|
||||
schemaVersion: "vignette.avatar.v3.rig.v1",
|
||||
persona: "P5",
|
||||
canvas: { w: 1005, h: 1566 },
|
||||
layers: {
|
||||
body: { href: "/avatar/v3/p5/body.webp", x: 0, y: 696, w: 1005, h: 870 },
|
||||
head: { href: "/avatar/v3/p5/head.webp", x: 144, y: 118, w: 688, h: 846 },
|
||||
hairFront: { href: "/avatar/v3/p5/hair-front.webp", x: 231, y: 401, w: 513, h: 436 },
|
||||
faceDetail: { href: "/avatar/v3/p5/face-detail.webp", x: 262, y: 410, w: 455, h: 523 },
|
||||
},
|
||||
grain: { href: "/avatar/v3/p5/paper-grain.webp", size: 256 },
|
||||
lipTexture: {
|
||||
upper: { href: "/avatar/v3/p5/lip-upper.webp", x: 423, y: 749, w: 124, h: 54 },
|
||||
lower: { href: "/avatar/v3/p5/lip-lower.webp", x: 423, y: 791, w: 136, h: 46 },
|
||||
shadow: { href: "/avatar/v3/p5/lip-shadow.webp", x: 433, y: 797, w: 116, h: 48 },
|
||||
},
|
||||
jaw: {
|
||||
head: { href: "/avatar/v3/p5/jaw-head.webp", x: 236, y: 633, w: 512, h: 337 },
|
||||
detail: { href: "/avatar/v3/p5/jaw-detail.webp", x: 236, y: 633, w: 512, h: 337 },
|
||||
},
|
||||
pivots: {
|
||||
neck: [488, 1001],
|
||||
body: [502, 1566],
|
||||
face: [490, 659],
|
||||
},
|
||||
crops: {
|
||||
portrait: [0, 0, 1005, 1566],
|
||||
bust: [0, 40, 1005, 1005],
|
||||
face: [191.6, 370.8, 601.3, 601.3],
|
||||
},
|
||||
faceOval: [[496.1, 403], [564, 405], [621.1, 414.5], [672.8, 434.4], [705.1, 464.2], [723.6, 501.7], [732.1, 541], [732.9, 589.2], [728.9, 634.4], [723.7, 681], [714.5, 730.1], [698.1, 782.2], [675.7, 823.6], [650.1, 853], [619, 879.9], [592.9, 900], [565.2, 919.2], [531.9, 934.8], [488.2, 939.9], [444.8, 934.7], [411.7, 918.9], [384.1, 899.3], [358.4, 879], [328.1, 851.8], [303.5, 822.2], [281.3, 781.3], [265.8, 729], [257.4, 680], [253.9, 634], [251.6, 589.2], [255.3, 541.7], [265.7, 502], [286.1, 464.7], [319.6, 435], [371.3, 414.7], [428.2, 405.3]],
|
||||
landmarks: {
|
||||
eyeLeft: {
|
||||
inner: [431.6, 589],
|
||||
outer: [343.8, 578.5],
|
||||
upperLidTop: [381.9, 566.8],
|
||||
lowerLidBottom: [383, 594],
|
||||
iris: { center: [387.5, 574.5], radius: 18.5 },
|
||||
lineScale: 2.5,
|
||||
},
|
||||
eyeRight: {
|
||||
inner: [553.9, 588.2],
|
||||
outer: [641.8, 576.3],
|
||||
upperLidTop: [605.4, 564.5],
|
||||
lowerLidBottom: [602.9, 593],
|
||||
iris: { center: [594, 573], radius: 18 },
|
||||
lineScale: 2.5,
|
||||
},
|
||||
browLeft: { inner: [454.2, 526.5], peak: [355, 512.3], outer: [303.8, 521.4], halfWidth: [7.1, 13, 3.4], sprite: { href: "/avatar/v3/p5/brow-left.webp", x: 299, y: 495, w: 161, h: 48 } },
|
||||
browRight: { inner: [524.9, 526.2], peak: [625.3, 510.7], outer: [677.1, 518.6], halfWidth: [2.1, 13, 1.9], sprite: { href: "/avatar/v3/p5/brow-right.webp", x: 520, y: 495, w: 162, h: 44 } },
|
||||
noseTip: [493.1, 693.9],
|
||||
mouthCornerLeft: [418.9, 801.4],
|
||||
mouthCornerRight: [557.3, 801.8],
|
||||
upperLipTop: [490.9, 759.4],
|
||||
lowerLipBottom: [489.5, 828.3],
|
||||
mouthCenter: [490.7, 791],
|
||||
chinTip: [488.3, 929.7],
|
||||
},
|
||||
palette: {
|
||||
ink: "#222221",
|
||||
sclera: "#D8CEBD",
|
||||
iris: "#4F3B2C",
|
||||
irisRing: "#1E1F1F",
|
||||
lipUpper: "#B1845F",
|
||||
lipLower: "#CC926C",
|
||||
mouthLine: "#A5694F",
|
||||
mouthInner: "#3B201B",
|
||||
teeth: "#E9E0CF",
|
||||
blush: "#C0624A",
|
||||
tear: "#EEE5D3",
|
||||
pallor: "#9AA3A6",
|
||||
paper: "#EEE5D3",
|
||||
motifPetal: "#D0A362",
|
||||
motifLeaf: "#53626C",
|
||||
},
|
||||
backdrop: {
|
||||
cognitive: "#ECE3D1",
|
||||
positive: "#F1DEC2",
|
||||
negative: "#DCE0E2",
|
||||
defensive: "#E6DAD3",
|
||||
energy: "#E2E0D0",
|
||||
},
|
||||
};
|
||||
86
apps/web/src/components/avatar/v3/rigs/p6Rig.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
/* 생성 파일 — docs/avatar-art/linocut-pipeline/scripts/export_rig.py 가 만든다. 손으로 고치지 않는다. */
|
||||
import type { LinocutRig } from "../linocutRig";
|
||||
|
||||
export const P6_LINOCUT_RIG: LinocutRig = {
|
||||
schemaVersion: "vignette.avatar.v3.rig.v1",
|
||||
persona: "P6",
|
||||
canvas: { w: 1005, h: 1566 },
|
||||
layers: {
|
||||
body: { href: "/avatar/v3/p6/body.webp", x: 0, y: 844, w: 1005, h: 722 },
|
||||
head: { href: "/avatar/v3/p6/head.webp", x: 37, y: 83, w: 887, h: 1063 },
|
||||
hairFront: { href: "/avatar/v3/p6/hair-front.webp", x: 257, y: 399, w: 463, h: 529 },
|
||||
faceDetail: { href: "/avatar/v3/p6/face-detail.webp", x: 272, y: 404, w: 439, h: 529 },
|
||||
},
|
||||
grain: { href: "/avatar/v3/p6/paper-grain.webp", size: 256 },
|
||||
lipTexture: {
|
||||
upper: { href: "/avatar/v3/p6/lip-upper.webp", x: 420, y: 765, w: 158, h: 49 },
|
||||
lower: { href: "/avatar/v3/p6/lip-lower.webp", x: 419, y: 804, w: 160, h: 48 },
|
||||
shadow: { href: "/avatar/v3/p6/lip-shadow.webp", x: 429, y: 807, w: 140, h: 53 },
|
||||
},
|
||||
jaw: {
|
||||
head: { href: "/avatar/v3/p6/jaw-head.webp", x: 251, y: 656, w: 482, h: 315 },
|
||||
detail: { href: "/avatar/v3/p6/jaw-detail.webp", x: 251, y: 656, w: 482, h: 315 },
|
||||
},
|
||||
pivots: {
|
||||
neck: [506, 1001],
|
||||
body: [502, 1566],
|
||||
face: [488, 661],
|
||||
},
|
||||
crops: {
|
||||
portrait: [0, 0, 1005, 1566],
|
||||
bust: [0, 40, 1005, 1005],
|
||||
face: [184.6, 365, 608.6, 608.6],
|
||||
},
|
||||
faceOval: [[480.8, 398.4], [546.8, 397.6], [602, 404.4], [651.9, 421], [683.6, 447.7], [702.8, 483.5], [713.5, 522.5], [717.9, 570.3], [717, 615.8], [714.2, 663.3], [706.6, 713.5], [692.8, 766.8], [673.8, 811.4], [651.7, 845], [623.5, 875.2], [600.1, 897], [575.1, 917.5], [545.4, 934.4], [506.6, 941], [467.6, 937.9], [437.3, 924], [410.8, 905.8], [385.5, 886.6], [354, 859], [328.2, 826.9], [304.3, 783.8], [285.2, 730.7], [272.8, 680.6], [265.7, 633.6], [260, 588.5], [260, 540.9], [267.4, 500.8], [283.6, 463.8], [312.3, 434.4], [359.6, 413.5], [414.2, 402.7]],
|
||||
landmarks: {
|
||||
eyeLeft: {
|
||||
inner: [432.4, 593.7],
|
||||
outer: [341.4, 583.9],
|
||||
upperLidTop: [378.1, 566],
|
||||
lowerLidBottom: [383.4, 600.1],
|
||||
iris: { center: [386.5, 577.5], radius: 20.5 },
|
||||
lineScale: 1,
|
||||
},
|
||||
eyeRight: {
|
||||
inner: [548.9, 589.6],
|
||||
outer: [639.8, 570.8],
|
||||
upperLidTop: [601.4, 555.9],
|
||||
lowerLidBottom: [598.4, 591.8],
|
||||
iris: { center: [595, 568], radius: 20 },
|
||||
lineScale: 1,
|
||||
},
|
||||
browLeft: { inner: [451.9, 530.3], peak: [367.6, 517.4], outer: [324.1, 521.5], halfWidth: [15.7, 11, 7.9], sprite: { href: "/avatar/v3/p6/brow-left.webp", x: 323, y: 504, w: 134, h: 49 } },
|
||||
browRight: { inner: [518.3, 525.4], peak: [607, 510.3], outer: [652.7, 515.6], halfWidth: [5.7, 25.5, 4.9], sprite: { href: "/avatar/v3/p6/brow-right.webp", x: 514, y: 492, w: 137, h: 57 } },
|
||||
noseTip: [495.6, 716.5],
|
||||
mouthCornerLeft: [423.8, 811],
|
||||
mouthCornerRight: [575.1, 805.1],
|
||||
upperLipTop: [498.5, 778],
|
||||
lowerLipBottom: [501.5, 841.2],
|
||||
mouthCenter: [499.8, 803.5],
|
||||
chinTip: [506.1, 930.6],
|
||||
},
|
||||
palette: {
|
||||
ink: "#242322",
|
||||
sclera: "#D8CEBD",
|
||||
iris: "#4F3B2C",
|
||||
irisRing: "#1E1F1F",
|
||||
lipUpper: "#A27659",
|
||||
lipLower: "#A47459",
|
||||
mouthLine: "#2C2625",
|
||||
mouthInner: "#3B201B",
|
||||
teeth: "#E9E0CF",
|
||||
blush: "#C0624A",
|
||||
tear: "#EEE5D3",
|
||||
pallor: "#9AA3A6",
|
||||
paper: "#EEE5D3",
|
||||
motifPetal: "#D0A362",
|
||||
motifLeaf: "#53626C",
|
||||
},
|
||||
backdrop: {
|
||||
cognitive: "#ECE3D1",
|
||||
positive: "#F1DEC2",
|
||||
negative: "#DCE0E2",
|
||||
defensive: "#E6DAD3",
|
||||
energy: "#E2E0D0",
|
||||
},
|
||||
};
|
||||
86
apps/web/src/components/avatar/v3/rigs/p7Rig.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
/* 생성 파일 — docs/avatar-art/linocut-pipeline/scripts/export_rig.py 가 만든다. 손으로 고치지 않는다. */
|
||||
import type { LinocutRig } from "../linocutRig";
|
||||
|
||||
export const P7_LINOCUT_RIG: LinocutRig = {
|
||||
schemaVersion: "vignette.avatar.v3.rig.v1",
|
||||
persona: "P7",
|
||||
canvas: { w: 1005, h: 1566 },
|
||||
layers: {
|
||||
body: { href: "/avatar/v3/p7/body.webp", x: 0, y: 725, w: 1005, h: 841 },
|
||||
head: { href: "/avatar/v3/p7/head.webp", x: 174, y: 151, w: 648, h: 892 },
|
||||
hairFront: { href: "/avatar/v3/p7/hair-front.webp", x: 220, y: 413, w: 535, h: 423 },
|
||||
faceDetail: { href: "/avatar/v3/p7/face-detail.webp", x: 276, y: 423, w: 425, h: 513 },
|
||||
},
|
||||
grain: { href: "/avatar/v3/p7/paper-grain.webp", size: 256 },
|
||||
lipTexture: {
|
||||
upper: { href: "/avatar/v3/p7/lip-upper.webp", x: 406, y: 763, w: 170, h: 50 },
|
||||
lower: { href: "/avatar/v3/p7/lip-lower.webp", x: 408, y: 801, w: 164, h: 49 },
|
||||
shadow: { href: "/avatar/v3/p7/lip-shadow.webp", x: 428, y: 807, w: 126, h: 51 },
|
||||
},
|
||||
jaw: {
|
||||
head: { href: "/avatar/v3/p7/jaw-head.webp", x: 251, y: 652, w: 478, h: 321 },
|
||||
detail: { href: "/avatar/v3/p7/jaw-detail.webp", x: 251, y: 652, w: 478, h: 321 },
|
||||
},
|
||||
pivots: {
|
||||
neck: [492, 1002],
|
||||
body: [502, 1566],
|
||||
face: [489, 660],
|
||||
},
|
||||
crops: {
|
||||
portrait: [0, 0, 1005, 1566],
|
||||
bust: [0, 40, 1005, 1005],
|
||||
face: [193.3, 384.2, 590, 590],
|
||||
},
|
||||
faceOval: [[490.5, 415.8], [555.3, 417.3], [609.5, 425.4], [658.6, 442.5], [688.8, 469], [706, 502.9], [714.3, 539.4], [716.2, 584.5], [712.6, 628.1], [706.8, 673.7], [696.8, 722.2], [681.3, 774.6], [662, 818.1], [641.5, 850.8], [615.8, 880.4], [592.8, 902], [566.9, 921.9], [534.8, 937.4], [491.9, 942.6], [449, 938.5], [416.4, 923.9], [389.5, 904.8], [365.4, 883.9], [338.9, 854.2], [317.7, 821], [298, 777], [282.2, 723.6], [271.5, 674.4], [265.1, 628.9], [260.4, 585.3], [262.1, 540.3], [270.9, 503.5], [289.1, 469.4], [320.3, 442.7], [369.9, 425.3], [425, 417.4]],
|
||||
landmarks: {
|
||||
eyeLeft: {
|
||||
inner: [433.4, 588.7],
|
||||
outer: [345.6, 578.8],
|
||||
upperLidTop: [381.3, 566.3],
|
||||
lowerLidBottom: [387.8, 592.8],
|
||||
iris: { center: [387.5, 574.5], radius: 16.5 },
|
||||
lineScale: 2.5,
|
||||
},
|
||||
eyeRight: {
|
||||
inner: [550.7, 589.9],
|
||||
outer: [637.7, 578.5],
|
||||
upperLidTop: [601.5, 566.1],
|
||||
lowerLidBottom: [596.3, 593.2],
|
||||
iris: { center: [593.5, 572.5], radius: 16.5 },
|
||||
lineScale: 2.5,
|
||||
},
|
||||
browLeft: { inner: [455.8, 527], peak: [369, 521.2], outer: [324.2, 527.5], halfWidth: [5.4, 8, 3.4], sprite: { href: "/avatar/v3/p7/brow-left.webp", x: 324, y: 509, w: 137, h: 29 } },
|
||||
browRight: { inner: [526.5, 526.8], peak: [619.5, 519.8], outer: [667.5, 527], halfWidth: [6.3, 8, 2.9], sprite: { href: "/avatar/v3/p7/brow-right.webp", x: 521, y: 509, w: 150, h: 28 } },
|
||||
noseTip: [493.5, 712.5],
|
||||
mouthCornerLeft: [422.6, 806.5],
|
||||
mouthCornerRight: [558.9, 808.4],
|
||||
upperLipTop: [493.8, 774],
|
||||
lowerLipBottom: [492.3, 835],
|
||||
mouthCenter: [493.2, 800.3],
|
||||
chinTip: [491.8, 932.4],
|
||||
},
|
||||
palette: {
|
||||
ink: "#202122",
|
||||
sclera: "#D8CEBD",
|
||||
iris: "#4F3B2C",
|
||||
irisRing: "#1E1F1F",
|
||||
lipUpper: "#BA9069",
|
||||
lipLower: "#C4936D",
|
||||
mouthLine: "#322F2B",
|
||||
mouthInner: "#3B201B",
|
||||
teeth: "#E9E0CF",
|
||||
blush: "#C0624A",
|
||||
tear: "#EEE5D3",
|
||||
pallor: "#9AA3A6",
|
||||
paper: "#EEE5D3",
|
||||
motifPetal: "#D0A362",
|
||||
motifLeaf: "#53626C",
|
||||
},
|
||||
backdrop: {
|
||||
cognitive: "#ECE3D1",
|
||||
positive: "#F1DEC2",
|
||||
negative: "#DCE0E2",
|
||||
defensive: "#E6DAD3",
|
||||
energy: "#E2E0D0",
|
||||
},
|
||||
};
|
||||
|
|
@ -8,7 +8,7 @@ import { useEffect, useMemo, useRef, useState, type ChangeEvent } from "react";
|
|||
import DebugFace from "../components/avatar/v3/DebugFace";
|
||||
import LinocutAvatar from "../components/avatar/v3/LinocutAvatar";
|
||||
import LinocutMotif from "../components/avatar/v3/LinocutMotif";
|
||||
import { P1_LINOCUT_RIG } from "../components/avatar/v3/rigs/p1Rig";
|
||||
import { labLinocutRigs } from "../components/avatar/v3/rigs";
|
||||
import type { LinocutRig } from "../components/avatar/v3/linocutRig";
|
||||
import { CHANNEL_IDS } from "../components/avatar/engine/channels";
|
||||
import { createAvatarEngine, type AvatarEngine, type DebugSnapshot } from "../components/avatar/engine/engine";
|
||||
|
|
@ -30,8 +30,10 @@ const PERSONA_OPTIONS: Array<{ code: string; label: string }> = [
|
|||
{ code: "P7", label: "P7 도현" },
|
||||
];
|
||||
|
||||
/** 리노컷 리그가 있는 페르소나만. 없는 페르소나는 debug 렌더러를 강제한다. */
|
||||
const LINOCUT_RIGS: Partial<Record<string, LinocutRig>> = { P1: P1_LINOCUT_RIG };
|
||||
/** 리노컷 리그가 있는 페르소나만. 없는 페르소나는 debug 렌더러를 강제한다.
|
||||
공통 파이프라인이 만든 pNRig.ts를 전부 모은 값이라, 새 페르소나가 빌드되는
|
||||
즉시(재빌드 없이) 이 Lab에서 보인다 — v3/rigs/index.ts labLinocutRigs() 참고. */
|
||||
const LINOCUT_RIGS: Partial<Record<string, LinocutRig>> = labLinocutRigs();
|
||||
|
||||
type RendererMode = "linocut" | "debug";
|
||||
type FeedbackMode = "immersive" | "ambient" | "coached";
|
||||
|
|
@ -80,11 +82,19 @@ function seedFromQuery(): number {
|
|||
return Number.isFinite(parsed) ? parsed : 1;
|
||||
}
|
||||
|
||||
/** ?persona=P3 같은 쿼리로 처음 고를 페르소나를 정한다(캡처 도구용). 모르는 코드거나
|
||||
없으면 기본 P1. */
|
||||
function personaFromQuery(): string {
|
||||
const raw = new URLSearchParams(window.location.search).get("persona");
|
||||
const upper = raw?.toUpperCase() ?? "";
|
||||
return PERSONA_OPTIONS.some((p) => p.code === upper) ? upper : "P1";
|
||||
}
|
||||
|
||||
export default function AvatarLab() {
|
||||
const seed = useMemo(seedFromQuery, []);
|
||||
const clock = useRef<Clock>(createClock()).current;
|
||||
|
||||
const [personaCode, setPersonaCode] = useState("P1");
|
||||
const [personaCode, setPersonaCode] = useState(personaFromQuery);
|
||||
const [avatarState, setAvatarState] = useState<AvatarState>("idle");
|
||||
const [openness, setOpenness] = useState(0.4);
|
||||
const [reducedMotion, setReducedMotion] = useState(false);
|
||||
|
|
|
|||