Files
ShortVideoGenerator/projects/water-hydrogen-bond-surface-tension/renderer/scene-plan.mjs
T

138 lines
7.7 KiB
JavaScript

// Coordinates use the verified 945 x 1680 source PNG canvas.
// Each region changes only an explanatory element; the camera stays fixed.
export const plans = {
c01: {kind: 'patch', regions: [[420, 160, 480, 610]]},
c02: {kind: 'patch', regions: [[540, 200, 340, 340]]},
c03: {kind: 'annotation'}, c04: {kind: 'annotation'},
c05: {kind: 'hold'}, c06: {kind: 'annotation'},
c07: {kind: 'annotation', direction: 'x'}, c08: {kind: 'hold'},
c09: {kind: 'annotation'}, c10: {kind: 'annotation'},
c11: {kind: 'annotation', direction: 'y'},
c12: {kind: 'annotation', direction: 'x'}, c13: {kind: 'hold'},
c14: {kind: 'annotation'},
c15: {kind: 'annotation', direction: 'y'}, c16: {kind: 'annotation'},
c17: {kind: 'hold'}, c18: {kind: 'deform'},
c19: {kind: 'annotation'}, c20: {kind: 'deform'}, c21: {kind: 'deform'},
c22: {kind: 'annotation'}, c23: {kind: 'hold'}, c24: {kind: 'annotation'},
c25: {kind: 'hold'},
c26: {kind: 'patch', regions: [[400, 1030, 495, 225]], delayMs: 1900},
c27: {kind: 'annotation'},
c28: {kind: 'patch', regions: [[420, 160, 480, 610]]},
c29: {kind: 'annotation', direction: 'x'},
c30: {kind: 'deform'},
c31: {kind: 'deform', regions: [[510, 150, 395, 425], [430, 395, 140, 260], [515, 500, 230, 170]]},
};
// Spoken-term timing comes from the approved ASR-aligned 07/08 stages.
export const labels = [
{start: 4380, end: 7920, text: '확대 개념도', x: 704, y: 600},
{start: 7920, end: 11920, text: '산소 1개 · 수소 2개', x: 472, y: 430},
{start: 11920, end: 18080, text: '부분 전하 · 분자 전체는 중성', x: 472, y: 450},
{start: 11920, end: 18080, text: 'δ−', x: 472, y: 710, size: 56},
{start: 14780, end: 18080, text: 'δ+', x: 263, y: 920, size: 52},
{start: 14780, end: 18080, text: 'δ+', x: 677, y: 920, size: 52},
{start: 24380 + 1800, end: 32720, text: '수소결합', x: 510, y: 575},
{start: 28480, end: 32720, text: '분자 안', x: 177, y: 475},
{start: 28480, end: 32720, text: '분자 사이', x: 530, y: 985},
{start: 45120, end: 49360, text: '물끼리 뭉치는 성질', x: 472, y: 1220},
{start: 52080, end: 56120, text: '물 안쪽', x: 472, y: 1085},
{start: 56120, end: 63440, text: '공기 쪽', x: 472, y: 375},
{start: 56120, end: 63440, text: '표면', x: 820, y: 435},
{start: 61000, end: 63440, text: '물 분자 이웃이 적음', x: 472, y: 250},
{start: 67440 + 700, end: 70820, text: '에너지', x: 203, y: 665, size: 32},
{start: 67440, end: 70820, text: '에너지를 보태야 함', x: 472, y: 1080},
{start: 63440, end: 81520, text: '같은 물의 양 · 입체 물방울의 개념도', x: 472, y: 1190, size: 28},
{start: 63440 + 16760, end: 81520, text: '표면장력', x: 472, y: 570},
{start: 81520, end: 87380, text: '같은 물의 양 · 개념도', x: 472, y: 115},
{start: 82960 + 1200, end: 87380, text: '겉면이 가장 작음', x: 670, y: 940},
{start: 87380, end: 97760, text: '외부 영향이 작을 때', x: 225, y: 955, size: 30},
{start: 93420, end: 97760, text: '큰 물방울 · 바닥 접촉 예시', x: 641, y: 955, size: 29},
{start: 96160, end: 97760, text: '중력·바닥의 영향', x: 641, y: 1340, size: 30},
{start: 97760 + 700, end: 107360, text: '확대 개념도', x: 704, y: 130, size: 30},
{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;
}