// MathBTI 前端逻辑 — 12题 → 16结果 → 4族
let testData = null, answers = [], currentQ = 0, myResult = null;
async function init() {
try {
const r = await fetch('/data/seed_mathbti.json');
testData = await r.json();
renderIntro();
} catch (e) {
document.getElementById('introScreen').innerHTML =
'
';
}
}
function renderIntro() {
const axesEl = document.getElementById('introAxes');
if (axesEl && testData.axes) {
axesEl.innerHTML = testData.axes.map(a =>
'' +
'' + (a.emoji_left || '') + '' +
'' + a.name + '' +
'' + (a.emoji_right || '') + '' +
'
'
).join('');
}
document.querySelector('.intro-meta').textContent =
'⏱ 约2分钟 · 4选项 · 16种人格';
}
function startQuiz() {
answers = new Array(testData.questions.length).fill(-1);
currentQ = 0;
switchScreen('quizScreen');
renderQuestion();
}
function renderQuestion() {
const q = testData.questions[currentQ];
document.getElementById('quizQuestion').textContent = q.question;
const optEl = document.getElementById('quizOptions');
const sel = answers[currentQ];
optEl.innerHTML = q.options.map((o, i) =>
''
).join('');
// Progress
const pct = Math.round((currentQ + 1) / testData.questions.length * 100);
document.getElementById('progressFill').style.width = pct + '%';
// 最后一题时进度条末尾显示葫芦图标
const isLast=currentQ===testData.questions.length-1;
document.getElementById('progressFill').style.setProperty('--is-last',isLast?'1':'0');
document.getElementById('progressText').textContent = (currentQ + 1) + '/' + testData.questions.length;
// Next button
const btn = document.getElementById('btnNext');
btn.disabled = answers[currentQ] === -1;
btn.textContent = currentQ === testData.questions.length - 1 ? '查看结果' : '下一题';
}
function selectOption(idx) {
answers[currentQ] = idx;
document.getElementById('btnNext').disabled = false;
// Update visual
document.querySelectorAll('.quiz-option').forEach((el, i) => {
el.classList.toggle('selected', i === idx);
});
}
async function nextQuestion() {
if (answers[currentQ] === -1) return;
if (currentQ < testData.questions.length - 1) {
currentQ++;
renderQuestion();
return;
}
// Last question → calculate locally
const btn = document.getElementById('btnNext');
btn.disabled = true;
btn.textContent = '计算中…';
try {
myResult = calculateResult(answers);
// 自动获得对应的数学人物卡
const mbtiCode=myResult.binary_code;
let gotNewCard=false;
if(mbtiCode){
let cardState2=JSON.parse(localStorage.getItem('hulu_card_state')||'{}');
if(!cardState2.owned)cardState2.owned=[];
if(!cardState2.drawHistory)cardState2.drawHistory=[];
if(cardState2.owned.indexOf(mbtiCode)===-1){
cardState2.owned.push(mbtiCode);
cardState2.drawHistory.push({code:mbtiCode,date:new Date().toISOString().slice(0,10),method:'mathbti'});
localStorage.setItem('hulu_card_state',JSON.stringify(cardState2));
gotNewCard=true;
}
}
renderResult();
// 卡片领取提示
if(gotNewCard){
const res=myResult.result;
const notice=document.getElementById('cardNotice');
if(notice){notice.style.display='block';document.getElementById('cardNoticeName').textContent=res?res.name:'—'}
showToast('🎴 获得数学人物小卡!前往葫芦数学查看卡册');
}
switchScreen('resultScreen');
} catch (e) {
showToast('提交失败,请重试');
btn.disabled = false;
btn.textContent = currentQ === testData.questions.length - 1 ? '查看结果' : '下一题';
}
}
function calculateResult(answers) {
const axes = testData.scoring.axes; // ["style","purpose","era","social"]
const maxPerAxis = testData.scoring.max_per_axis;
const cutoff = testData.scoring.cutoff;
// 计算每个轴的得分
const axisScores = {};
axes.forEach(axis => {
axisScores[axis] = 0;
});
testData.questions.forEach((q, i) => {
const score = q.options[answers[i]].score;
axisScores[q.axis] += score;
});
// 生成二进制代码 (≤cutoff=0左极, ≥cutoff+1=1右极)
const binaryCode = axes.map(axis => axisScores[axis] <= cutoff ? '0' : '1').join('');
// 获取结果
const result = testData.results[binaryCode];
return {
binary_code: binaryCode,
axis_scores: axisScores,
result: result
};
}
function renderResult() {
const res = myResult.result;
if (!res) return;
// Portrait
const portraitEl = document.getElementById('resultPortrait');
if (res.portrait) {
portraitEl.innerHTML = '
' + (res.icon || 'Σ') + ' ';
} else {
portraitEl.innerHTML = '' + (res.icon || 'Σ') + '
';
}
// Name + Clan
document.getElementById('resultName').innerHTML = res.name + ' ' + myResult.binary_code + '';
document.getElementById('resultTagline').textContent = res.tagline || '';
// Clan badge
const clanData = testData.clans[res.clan];
document.getElementById('resultMathematician').innerHTML =
(clanData ? '' + clanData.emoji + ' ' + clanData.name + ' · ' + res.mathematician + '' : '') +
'代表数学家:' + (res.mathematician || '—') + '';
// Keywords
const kwEl = document.getElementById('resultKeywords');
if (res.keywords) {
kwEl.innerHTML = res.keywords.map(k => '' + k + '').join('');
kwEl.style.display = 'flex';
} else {
kwEl.style.display = 'none';
}
// Description
document.getElementById('resultDesc').textContent = res.description || '';
document.getElementById('resultShort').textContent = res.short_desc || '';
// Bio
document.getElementById('bioTitle').textContent = '关于 ' + (res.mathematician || '这位数学家');
document.getElementById('bioText').textContent = res.bio || '';
document.getElementById('bioFact').textContent = res.bio_fact || '';
// Axes bars
const axesEl = document.getElementById('resultAxes');
if (testData.axes) {
axesEl.innerHTML = testData.axes.map(a => {
const score = myResult.axis_scores[a.id] || 0;
const maxScore = testData.scoring.max_per_axis || 9;
const pct = Math.round(score / maxScore * 100);
return '' +
'
' + (a.emoji_left || '') + ' ' + a.left + '' +
'
' +
'
' + a.right + ' ' + (a.emoji_right || '') + '' +
'
';
}).join('');
axesEl.style.display = 'block';
}
// Five-dimension ability card from 16位数学家五维能力表
const s5El = document.getElementById('resultStats5');
if (s5El && res.stats5) {
const labels = ['眼光', '人文', '侦探', '建模', '联结'];
s5El.innerHTML = '五维能力
' + labels.map(k => {
const v = res.stats5[k] || 0;
return '' +
'
' + k + '' +
'
' +
'
' + v + '' +
'
';
}).join('');
s5El.style.display = 'block';
}
// Adopt pet
const petData = JSON.parse(localStorage.getItem('mb_pet') || '{}');
const adoptBtn = document.querySelector('.btn-adopt');
if (petData.type) {
adoptBtn.textContent = '✅ 已领养 · 我的数学精灵';
adoptBtn.classList.add('adopted');
}
// Apply clan color
if (clanData) {
document.getElementById('resultScreen').style.setProperty('--clan-color', clanData.color);
}
}
// ---- Bio Expander ----
function toggleBio() {
const body = document.getElementById('bioBody');
const arrow = document.getElementById('bioArrow');
const expanded = body.style.maxHeight && body.style.maxHeight !== '0px';
if (expanded) {
body.style.maxHeight = '0px';
arrow.textContent = '▾';
} else {
body.style.maxHeight = body.scrollHeight + 'px';
arrow.textContent = '▴';
}
}
// ---- Adopt Pet ----
function adoptPet() {
if (!myResult) return;
const res = myResult.result;
const dims = {};
['眼光', '人文', '侦探', '建模', '联结'].forEach(k => {
dims[k] = Math.max(10, Math.min(100, Number((res.stats5 && res.stats5[k]) || 0)));
});
const pet = {
name: res.name || '—',
type: res.name || '—',
icon: res.icon || 'Σ',
color: res.color || '#5B6AF0',
mathematician: res.mathematician || '',
clan: res.clan || '',
binary_code: myResult.binary_code,
level: 1,
exp: 0,
dims: dims
};
localStorage.setItem('mb_pet', JSON.stringify(pet));
localStorage.setItem('mathbrain_pet', JSON.stringify(pet));
const btn = document.querySelector('.btn-adopt');
btn.textContent = '✅ 已领养 · 我的数学精灵';
btn.classList.add('adopted');
showToast('🧬 数学精灵已领养!每天来葫芦数学 App 喂它成长');
}
// ---- Share ----
function copyShare() {
if (!myResult) return;
const res = myResult.result;
const clanData = testData.clans[res.clan];
const text = '我的数学人格是「' + res.name + '」' + (res.icon || '') + '\n' +
(clanData ? clanData.emoji + ' ' + clanData.name + ' · ' + res.mathematician : '') + '\n' +
(res.short_desc || '') + '\n\n' +
'👉 你也来测测:' + window.location.href + '\n\n' +
'#数学人格测试 #MathBTI';
navigator.clipboard.writeText(text).then(() => showToast('已复制分享文案!'));
}
function shareResult() {
if (!myResult) return;
const res = myResult.result;
const text = '我的数学人格是「' + res.name + '」' + (res.icon || '') + '!' + (res.short_desc || '');
if (navigator.share) {
navigator.share({ title: 'MathBTI · 数学人格测试', text: text, url: window.location.href }).catch(() => {});
} else {
copyShare();
}
}
function generateShareCard() {
if (!myResult) return;
const res = myResult.result;
const clanData = testData.clans[res.clan];
const canvas = document.getElementById('shareCardCanvas');
const ctx = canvas.getContext('2d');
canvas.width = 600;
canvas.height = 800;
// Background
const gradient = ctx.createLinearGradient(0, 0, 600, 800);
gradient.addColorStop(0, (res.color || '#5B6AF0') + '22');
gradient.addColorStop(1, (res.color || '#5B6AF0') + '08');
ctx.fillStyle = '#FAFBFC';
ctx.fillRect(0, 0, 600, 800);
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 600, 800);
// Top accent bar
ctx.fillStyle = res.color || '#5B6AF0';
ctx.fillRect(0, 0, 600, 6);
// Portrait placeholder
ctx.fillStyle = res.icon_bg || '#F0F0F0';
ctx.beginPath();
ctx.arc(300, 180, 70, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#334155';
ctx.font = '60px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(res.icon || 'Σ', 300, 180);
// Name
ctx.fillStyle = '#1E293B';
ctx.font = 'bold 36px "PingFang SC", sans-serif';
ctx.fillText(res.name, 300, 280);
// Clan + Mathematician
ctx.fillStyle = '#64748B';
ctx.font = '18px "PingFang SC", sans-serif';
if (clanData) {
ctx.fillText(clanData.emoji + ' ' + clanData.name + ' · ' + res.mathematician, 300, 315);
}
// Tagline
ctx.fillStyle = res.color || '#5B6AF0';
ctx.font = 'italic 15px "PingFang SC", sans-serif';
const tagline = res.tagline || '';
wrapText(ctx, tagline, 300, 350, 480, 22);
// Separator
ctx.fillStyle = '#E2E8F0';
ctx.fillRect(60, 380, 480, 1);
// Short desc
ctx.fillStyle = '#334155';
ctx.font = '15px "PingFang SC", sans-serif';
const desc = res.short_desc || '';
wrapText(ctx, desc, 300, 410, 480, 24);
// Axes
let ay = 450;
if (testData.axes && myResult.axis_scores) {
ctx.fillStyle = '#64748B';
ctx.font = '14px "PingFang SC", sans-serif';
testData.axes.forEach(a => {
const s = myResult.axis_scores[a.id] || 0;
const maxS = testData.scoring.max_per_axis || 9;
const pct = Math.round(s / maxS * 100);
ctx.fillText(a.emoji_left + ' ' + a.left + ' — ' + pct + '% — ' + a.right + ' ' + a.emoji_right, 300, ay);
ay += 28;
});
}
// Bottom
ctx.fillStyle = '#94A3B8';
ctx.font = '13px "PingFang SC", sans-serif';
ctx.fillText('葫芦数学 · MathBTI', 300, 730);
// 葫芦水印
ctx.save();ctx.globalAlpha=.06;ctx.font='80px sans-serif';ctx.fillStyle='#A8907E';
ctx.fillText('🏮',480,680);
ctx.restore();
ctx.fillText('你也来测测你的数学人格类型 →', 300, 755);
// QR placeholder
ctx.fillStyle = '#E2E8F0';
ctx.fillRect(255, 640, 90, 90);
ctx.fillStyle = '#94A3B8';
ctx.font = '11px sans-serif';
ctx.fillText('扫码测', 300, 685);
document.getElementById('shareCardPreview').style.display = 'block';
document.getElementById('shareCardPreview').scrollIntoView({ behavior: 'smooth' });
}
function downloadShareCard() {
const canvas = document.getElementById('shareCardCanvas');
const link = document.createElement('a');
link.download = 'mathbti_' + (myResult ? myResult.binary_code : 'result') + '.png';
link.href = canvas.toDataURL('image/png');
link.click();
showToast('📸 图片已保存!');
}
function wrapText(ctx, text, x, y, maxWidth, lineHeight) {
const lines = [];
let current = '';
for (let i = 0; i < text.length; i++) {
const test = current + text[i];
if (ctx.measureText(test).width > maxWidth && current.length > 0) {
lines.push(current);
current = text[i];
} else {
current = test;
}
}
if (current) lines.push(current);
lines.forEach((line, idx) => {
ctx.fillText(line, x, y + idx * lineHeight);
});
}
// ---- Retry ----
function retryQuiz() {
answers = [];
currentQ = 0;
myResult = null;
document.getElementById('bioBody').style.maxHeight = '0px';
document.getElementById('bioArrow').textContent = '▾';
document.getElementById('shareCardPreview').style.display = 'none';
switchScreen('introScreen');
}
// ---- Helpers ----
function switchScreen(id) {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
document.getElementById(id).classList.add('active');
window.scrollTo({ top: 0, behavior: 'smooth' });
}
let toastTimer = null;
function showToast(msg) {
const el = document.getElementById('toast');
el.textContent = msg;
el.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => el.classList.remove('show'), 2200);
}
// Bootstrap
document.addEventListener('DOMContentLoaded', init);