승인된 Blender 컷의 실제 합성과 최종 검수 근거 추가

This commit is contained in:
NINI
2026-10-10 04:11:08 +09:00
parent 78fcd49a54
commit ff28b7b47c
26 changed files with 6955 additions and 220 deletions
@@ -52,3 +52,86 @@ export const labels = [
{start: 101980 + 700, end: 104560, text: '서로 끌림', x: 704, y: 600},
{start: 104560 + 600, end: 107360, text: '표면 넓힘에 저항', x: 472, y: 1080},
];
// 새 합성 좌표는 Blender의 실제 카메라 투영(1080×1920)과 논리 프레임 키다.
export function anchorAt(cut, id, frame) {
const motion = cut.motion.find((m) => m.anchor_id === id);
if (!motion) throw new Error(`${cut.cut_id}: 없는 실제 앵커 ${id}`);
const keys = motion.keys;
const after = keys.findIndex((k) => k[0] > frame);
const a = after < 0 ? keys.at(-1) : keys[Math.max(0, after - 1)];
const b = after < 0 ? a : keys[after];
const t = a[0] === b[0] ? 0 : Math.max(0, Math.min(1, (frame - a[0]) / (b[0] - a[0])));
return {x: a[1] + (b[1] - a[1]) * t, y: a[2] + (b[2] - a[2]) * t,
target: [a[3] + (b[3] - a[3]) * t, a[4] + (b[4] - a[4]) * t]};
}
export function blenderLabels(clip, frame) {
const ms = frame * 20, result = [];
const at = (id, text, {dx = 0, dy = 0, size = 42, leader = false} = {}) => {
const a = anchorAt(clip.anchors, id, frame);
// 연결선은 mesh 위쪽 가장자리까지만 닿아 원자를 가리지 않는다.
result.push({...a, target: [a.target[0], a.y + 32], x: a.x + dx, y: a.y + dy, text, size, leader});
};
const heading = (text, y = 450, size = 42) => result.push({text, x: 540, y, size});
switch (clip.scene_name) {
case 'OpeningDroplet':
if (ms >= 4380) heading('이제 물속 분자로 확대', 560, 40);
break;
case 'SingleMolecule':
if (ms < 11920) {
heading('산소 1개 · 수소 2개', 500);
at('single_O_delta_minus', '산소', {dy: -15});
at('single_H_delta_plus', '수소'); at('single_H2_delta_plus', '수소');
} else {
heading('부분 전하 · 분자 전체는 중성', 500, 38);
at('single_O_delta_minus', 'δ−', {dy: -12, size: 58});
if (ms >= 14780) {
at('single_H_delta_plus', 'δ+', {size: 56}); at('single_H2_delta_plus', 'δ+', {size: 56});
}
}
break;
case 'BondPair':
at('pair_H_delta_plus', 'δ+', {size: 52}); at('pair_O_delta_minus', 'δ−', {size: 54});
if (ms >= 24380) heading('수소결합', 530, 48);
if (ms >= 28480) heading('실선: 분자 안 · 점선: 분자 사이', 620, 36);
break;
case 'LiquidNetwork':
heading(ms < 45120 ? '연결이 생겼다 끊어져요' : '물끼리 함께 뭉쳐요', 500, 44);
break;
case 'Interface':
heading('물 표면의 개념도', 460, 38);
if (ms >= 52080 && ms < 56120) at('water_interior', '물 안쪽', {dx: -240, dy: 450, size: 38});
if (ms >= 56120) {
at('air_region', '공기 쪽', {dy: -80, size: 40});
at('water_surface', '표면', {dx: 300, dy: -35, size: 38, leader: true});
}
if (ms >= 61000) heading('공기 쪽에 물 분자 이웃이 적음', 560, 36);
break;
case 'DropletStretch':
at('same_volume_drop', '같은 물의 양 · 개념도', {dy: -140, size: 36});
if (ms >= 67440 && ms < 70820) heading('표면을 넓히려면 에너지가 필요', 1380, 38);
if (ms >= 80200) heading('표면장력', 1400, 48);
break;
case 'EqualVolumeComparison':
heading('같은 물의 양 · 개념도', 500, 40);
at('equal_volume_left', '늘어난 모양', {dy: -70, size: 38});
at('equal_volume_right', '둥근 모양', {dy: -70, size: 38});
if (ms >= 84160) at('equal_volume_right', '겉면이 가장 작음', {dy: 490, size: 36});
break;
case 'ShapeConditions':
heading('크기·바닥 조건이 다른 예시', 470, 38);
at('small_free_condition', '작은 물방울', {dy: -110, size: 36});
at('small_free_condition', '외부 영향이 작을 때', {dy: 510, size: 32});
if (ms >= 93420) at('large_contact_condition', '큰 물방울 · 바닥 접촉', {dy: -120, size: 32});
if (ms >= 96160) at('contact_floor', '중력·바닥의 영향', {dy: 400, size: 34});
break;
case 'AnswerDroplet':
// 확대도를 처음 보여주는 프레임부터 명시한다. 공기 중 실제 분자 배치가 아니다.
at('answer_inset_O', '확대개념도', {dy: -95, size: 40});
if (ms >= 102680 && ms < 104560) at('answer_inset_O', '서로 끌림', {dy: 170, size: 36});
if (ms >= 105160 && ms < 107360) heading('표면 넓힘에 저항', 1390, 40);
break;
default: throw new Error(`미지원 실제 Scene ${clip.scene_name}`);
}
return result;
}