138 lines
7.7 KiB
JavaScript
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;
|
|
}
|