方程图像优化2.0
优化了性能体验,减少了性能开销,加载速度明显减少,视觉反馈不变,同时优化了加载时的等待显示,不再是卡在原界面不动,而是会在一个新页面上方显示你要绘制的图像,然后下方显示正在计算...
适配设备:Pro
作者:hughx
下载量:28 · 点赞:0
可信源码风险提示:保存或下载前请阅读源码和权限声明。
async function main() {
// ==================== 数学函数映射 ====================
const MATH_FUNCS = {
abs: Math.abs, sqrt: Math.sqrt, cbrt: Math.cbrt,
sin: Math.sin, cos: Math.cos, tan: Math.tan,
asin: Math.asin, acos: Math.acos, atan: Math.atan, atan2: Math.atan2,
sinh: Math.sinh, cosh: Math.cosh, tanh: Math.tanh,
exp: Math.exp, log: Math.log, ln: Math.log,
log2: Math.log2, log10: Math.log10,
pow: Math.pow, min: Math.min, max: Math.max,
round: Math.round, floor: Math.floor, ceil: Math.ceil,
sign: Math.sign, trunc: Math.trunc, hypot: Math.hypot
};
const MATH_CONSTS = {
pi: Math.PI, PI: Math.PI, e: Math.E, E: Math.E,
LN2: Math.LN2, LN10: Math.LN10, SQRT2: Math.SQRT2
};
function stripMath(name) {
return name.indexOf('Math.') === 0 ? name.slice(5) : name;
}
// ==================== 词法分析 ====================
function tokenize(src) {
const out = [];
let i = 0;
const isDigit = c => c >= '0' && c <= '9';
const isAlpha = c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c === '_';
while (i < src.length) {
const c = src[i];
if (c === ' ' || c === '\t' || c === '\n' || c === '\r') { i++; continue; }
if (isDigit(c) || (c === '.' && isDigit(src[i + 1]))) {
let j = i;
while (j < src.length && (isDigit(src[j]) || src[j] === '.')) j++;
if (j < src.length && (src[j] === 'e' || src[j] === 'E')) {
let k = j + 1;
if (k < src.length && (src[k] === '+' || src[k] === '-')) k++;
if (k < src.length && isDigit(src[k])) {
while (k < src.length && isDigit(src[k])) k++;
j = k;
}
}
out.push({ t: 'num', v: parseFloat(src.slice(i, j)) });
i = j;
continue;
}
if (isAlpha(c)) {
let j = i;
while (j < src.length && (isAlpha(src[j]) || isDigit(src[j]) || src[j] === '.')) j++;
out.push({ t: 'id', v: src.slice(i, j) });
i = j;
continue;
}
if ('+-*/%^(),'.indexOf(c) >= 0) { out.push({ t: 'op', v: c }); i++; continue; }
throw new Error('无法识别的字符 "' + c + '"');
}
return out;
}
// ==================== 语法分析 ====================
function parseTokens(tokens) {
let pos = 0;
function isOp(v) { const t = tokens[pos]; return t && t.t === 'op' && t.v === v; }
function eatOp(v) { if (isOp(v)) { pos++; return true; } return false; }
function parseExpr() { return parseAdd(); }
function parseAdd() {
let node = parseMul();
while (true) {
const t = tokens[pos];
if (t && t.t === 'op' && (t.v === '+' || t.v === '-')) {
pos++;
node = { k: 'bin', op: t.v, l: node, r: parseMul() };
} else break;
}
return node;
}
function parseMul() {
let node = parseUnary();
while (true) {
const t = tokens[pos];
if (t && t.t === 'op' && (t.v === '*' || t.v === '/' || t.v === '%')) {
pos++;
node = { k: 'bin', op: t.v, l: node, r: parseUnary() };
} else break;
}
return node;
}
function parseUnary() {
const t = tokens[pos];
if (t && t.t === 'op' && (t.v === '-' || t.v === '+')) {
pos++;
const a = parseUnary();
return t.v === '-' ? { k: 'neg', a: a } : a;
}
return parsePow();
}
function parsePow() {
const base = parseAtom();
if (isOp('^')) {
pos++;
const exp = parseUnary();
return { k: 'bin', op: '^', l: base, r: exp };
}
return base;
}
function parseAtom() {
const t = tokens[pos];
if (!t) throw new Error('表达式不完整');
if (t.t === 'num') { pos++; return { k: 'num', v: t.v }; }
if (t.t === 'id') {
pos++;
if (isOp('(')) {
pos++;
const args = [];
if (!isOp(')')) {
args.push(parseExpr());
while (eatOp(',')) args.push(parseExpr());
}
if (!eatOp(')')) throw new Error('缺少右括号');
return { k: 'call', name: t.v, args: args };
}
return { k: 'var', name: t.v };
}
if (t.t === 'op' && t.v === '(') {
pos++;
const e = parseExpr();
if (!eatOp(')')) throw new Error('缺少右括号');
return e;
}
throw new Error('意外的符号 "' + t.v + '"');
}
const ast = parseExpr();
if (pos !== tokens.length) throw new Error('表达式存在多余内容');
return ast;
}
// ==================== 编译为闭包树 ====================
function compileNode(node) {
switch (node.k) {
case 'num': { const v = node.v; return function () { return v; }; }
case 'var': {
const n = node.name;
if (n === 'x' || n === 'X') return function (x) { return x; };
if (n === 'y' || n === 'Y') return function (x, y) { return y; };
const key = stripMath(n);
if (Object.prototype.hasOwnProperty.call(MATH_CONSTS, key)) {
const v = MATH_CONSTS[key];
return function () { return v; };
}
throw new Error('未知变量 ' + n);
}
case 'neg': { const a = compileNode(node.a); return function (x, y) { return -a(x, y); }; }
case 'bin': {
const l = compileNode(node.l);
const r = compileNode(node.r);
const op = node.op;
if (op === '+') return function (x, y) { return l(x, y) + r(x, y); };
if (op === '-') return function (x, y) { return l(x, y) - r(x, y); };
if (op === '*') return function (x, y) { return l(x, y) * r(x, y); };
if (op === '/') return function (x, y) { return l(x, y) / r(x, y); };
if (op === '%') return function (x, y) { return l(x, y) % r(x, y); };
if (op === '^') return function (x, y) { return Math.pow(l(x, y), r(x, y)); };
throw new Error('未知运算符');
}
case 'call': {
const key = stripMath(node.name);
const f = MATH_FUNCS[key];
if (!f) throw new Error('未知函数 ' + node.name);
const args = node.args.map(compileNode);
const n = args.length;
if (n === 1) { const a0 = args[0]; return function (x, y) { return f(a0(x, y)); }; }
if (n === 2) { const a0 = args[0], a1 = args[1]; return function (x, y) { return f(a0(x, y), a1(x, y)); }; }
if (n === 3) { const a0 = args[0], a1 = args[1], a2 = args[2]; return function (x, y) { return f(a0(x, y), a1(x, y), a2(x, y)); }; }
return function (x, y) {
const vals = [];
for (let i = 0; i < n; i++) vals.push(args[i](x, y));
return f.apply(Math, vals);
};
}
}
throw new Error('未知表达式节点');
}
function hasVar(node, name) {
if (!node) return false;
if (node.k === 'var') { const n = node.name; return n === name || n === name.toUpperCase(); }
if (node.k === 'neg') return hasVar(node.a, name);
if (node.k === 'bin') return hasVar(node.l, name) || hasVar(node.r, name);
if (node.k === 'call') {
for (let i = 0; i < node.args.length; i++) {
if (hasVar(node.args[i], name)) return true;
}
}
return false;
}
function buildEvaluator(eqStr) {
const idx = eqStr.indexOf('=');
if (idx < 0) return null;
try {
const lhsAst = parseTokens(tokenize(eqStr.slice(0, idx)));
const rhsAst = parseTokens(tokenize(eqStr.slice(idx + 1)));
const lhsIsY = lhsAst.k === 'var' && (lhsAst.name === 'y' || lhsAst.name === 'Y');
const rhsHasY = hasVar(rhsAst, 'y');
if (lhsIsY && !rhsHasY) {
const rhsFn = compileNode(rhsAst);
const f = function (x) { return rhsFn(x, 0); };
const probe = f(1);
if (typeof probe !== 'number') return null;
return { mode: 'explicit', f: f };
}
const lhsFn = compileNode(lhsAst);
const rhsFn = compileNode(rhsAst);
const F = function (x, y) { return lhsFn(x, y) - rhsFn(x, y); };
const probe = F(1, 1);
if (typeof probe !== 'number') return null;
return { mode: 'implicit', F: F };
} catch (e) {
return null;
}
}
function buildCurve(eqStr, color) {
const ev = buildEvaluator(eqStr);
if (!ev) return null;
let F;
if (ev.mode === 'explicit') {
const f = ev.f;
F = function (x, y) { return y - f(x); };
} else {
F = ev.F;
}
return { eq: eqStr, ev: ev, F: F, color: color };
}
// ==================== 工具函数 ====================
function niceStep(range) {
const target = range / 2;
const cands = [1, 2, 5, 10, 15, 20, 30, 50];
for (let i = 0; i < cands.length; i++) {
if (target <= cands[i]) return cands[i];
}
return 50;
}
function fmtNum(v) {
const r = Math.round(v * 100) / 100;
if (Math.abs(r) < 1e-9) return '0';
if (Number.isInteger(r)) return String(r);
let s = r.toFixed(2);
while (s.length > 1 && s.charAt(s.length - 1) === '0') s = s.slice(0, -1);
return s;
}
// ==================== 隐式曲线采样 ====================
// STEP=4,相邻点最大距离 = 4×√2 ≈ 5.66px,配合 6px 点大小完全无空隙
function sampleImplicitCurve(F, GW, GH, OX, OY, scale, STEP) {
const BISECT = 6;
const cols = Math.floor(GW / STEP) + 2;
const rows = Math.floor(GH / STEP) + 2;
const vals = new Float64Array(cols * rows);
const ok = new Uint8Array(cols * rows);
const invScale = 1 / scale;
for (let j = 0; j < rows; j++) {
const yv = (OY - j * STEP) * invScale;
const base = j * cols;
for (let i = 0; i < cols; i++) {
const xv = (i * STEP - OX) * invScale;
let v;
try { v = F(xv, yv); } catch (e) { v = NaN; }
if (typeof v === 'number' && isFinite(v)) {
vals[base + i] = v;
ok[base + i] = 1;
}
}
}
const points = [];
const seen = {};
function pushPoint(px, py) {
if (px < 0 || py < 0 || px > GW || py > GH) return;
const ix = Math.round(px);
const iy = Math.round(py);
const key = ix + ':' + iy;
if (seen[key]) return;
seen[key] = 1;
points.push({ x: ix, y: iy });
}
function bisect(x1, y1, x2, y2, v1pos) {
let lo = 0, hi = 1;
const dxe = x2 - x1, dye = y2 - y1;
for (let k = 0; k < BISECT; k++) {
const mid = (lo + hi) * 0.5;
let vm;
try { vm = F(x1 + dxe * mid, y1 + dye * mid); } catch (e) { break; }
if (!isFinite(vm)) break;
if ((vm > 0) === v1pos) lo = mid; else hi = mid;
}
const t = (lo + hi) * 0.5;
return { x: x1 + dxe * t, y: y1 + dye * t };
}
for (let j = 0; j < rows - 1; j++) {
const rowBase = j * cols;
const y0 = (OY - j * STEP) * invScale;
const y1 = (OY - (j + 1) * STEP) * invScale;
for (let i = 0; i < cols - 1; i++) {
const i00 = rowBase + i;
const i10 = i00 + 1;
const i01 = i00 + cols;
const i11 = i01 + 1;
if (!ok[i00] || !ok[i10] || !ok[i01] || !ok[i11]) continue;
const v00 = vals[i00], v10 = vals[i10], v01 = vals[i01], v11 = vals[i11];
const p00 = v00 > 0, p10 = v10 > 0, p01 = v01 > 0, p11 = v11 > 0;
if (p00 === p10 && p10 === p01 && p01 === p11) continue;
const x0 = (i * STEP - OX) * invScale;
const x1 = ((i + 1) * STEP - OX) * invScale;
if (p00 !== p10) { const p = bisect(x0, y0, x1, y0, p00); pushPoint(OX + p.x * scale, OY - p.y * scale); }
if (p01 !== p11) { const p = bisect(x0, y1, x1, y1, p01); pushPoint(OX + p.x * scale, OY - p.y * scale); }
if (p00 !== p01) { const p = bisect(x0, y0, x0, y1, p00); pushPoint(OX + p.x * scale, OY - p.y * scale); }
if (p10 !== p11) { const p = bisect(x1, y0, x1, y1, p10); pushPoint(OX + p.x * scale, OY - p.y * scale); }
}
}
return points;
}
// ==================== 显式曲线采样(屏幕距离自适应加密) ====================
function sampleExplicitCurve(f, visX, GW, GH, OX, OY, scale, SAMPLES) {
const points = [];
const seen = {};
const x0 = -visX;
const dx = (visX * 2) / SAMPLES;
function tryPoint(xv) {
let yv;
try { yv = f(xv); } catch (e) { return null; }
if (typeof yv !== 'number' || !isFinite(yv)) return null;
return { xv: xv, px: OX + xv * scale, py: OY - yv * scale };
}
function pushPixel(px, py) {
if (px < 0 || py < 0 || px > GW || py > GH) return;
const ix = Math.round(px);
const iy = Math.round(py);
const key = ix + ':' + iy;
if (seen[key]) return;
seen[key] = 1;
points.push({ x: ix, y: iy });
}
let prev = null;
for (let i = 0; i <= SAMPLES; i++) {
const xv = x0 + i * dx;
const p = tryPoint(xv);
if (!p) { prev = null; continue; }
if (p.px >= -3 && p.px <= GW + 3 && p.py >= -3 && p.py <= GH + 3) {
pushPixel(p.px, p.py);
}
if (prev) {
const ddx = p.px - prev.px;
const ddy = p.py - prev.py;
const dist = Math.sqrt(ddx * ddx + ddy * ddy);
if (dist > 4) {
const steps = Math.min(6, Math.ceil(dist / 3));
for (let k = 1; k < steps; k++) {
const t = k / steps;
const mxv = prev.xv + (p.xv - prev.xv) * t;
const mp = tryPoint(mxv);
if (mp && mp.px >= -3 && mp.px <= GW + 3 && mp.py >= -3 && mp.py <= GH + 3) {
pushPixel(mp.px, mp.py);
}
}
}
}
prev = p;
}
return points;
}
// ==================== 牛顿迭代 ====================
function newton2(F1, F2, x0, y0) {
let x = x0, y = y0;
const e = 1e-7;
for (let k = 0; k < 12; k++) {
let f1, f2;
try { f1 = F1(x, y); f2 = F2(x, y); } catch (err) { return null; }
if (!isFinite(f1) || !isFinite(f2)) return null;
if (Math.abs(f1) < 1e-10 && Math.abs(f2) < 1e-10) break;
let f1x, f1y, f2x, f2y;
try {
f1x = (F1(x + e, y) - f1) / e;
f1y = (F1(x, y + e) - f1) / e;
f2x = (F2(x + e, y) - f2) / e;
f2y = (F2(x, y + e) - f2) / e;
} catch (err) { return null; }
if (!isFinite(f1x) || !isFinite(f1y) || !isFinite(f2x) || !isFinite(f2y)) return null;
const det = f1x * f2y - f1y * f2x;
if (Math.abs(det) < 1e-15) return null;
const dx = (f1 * f2y - f1y * f2) / det;
const dy = (f1x * f2 - f1 * f2x) / det;
x -= dx; y -= dy;
if (Math.abs(dx) < 1e-10 && Math.abs(dy) < 1e-10) break;
}
return { x: x, y: y };
}
// ==================== 双隐式交点(网格 + 牛顿) ====================
function findIntersectionsGrid(F1, F2, OX, OY, scale, GW, GH) {
const STEP = 8;
const cols = Math.floor(GW / STEP) + 2;
const rows = Math.floor(GH / STEP) + 2;
const A = new Float64Array(cols * rows);
const B = new Float64Array(cols * rows);
const invScale = 1 / scale;
for (let j = 0; j < rows; j++) {
const yv = (OY - j * STEP) * invScale;
const base = j * cols;
for (let i = 0; i < cols; i++) {
const xv = (i * STEP - OX) * invScale;
let v1 = NaN, v2 = NaN;
try { v1 = F1(xv, yv); } catch (e) {}
try { v2 = F2(xv, yv); } catch (e) {}
A[base + i] = v1;
B[base + i] = v2;
}
}
const results = [];
const seen = {};
function pushPoint(px, py) {
const key = Math.round(px * 300) + ':' + Math.round(py * 300);
if (seen[key]) return;
seen[key] = 1;
results.push({ x: px, y: py });
}
for (let j = 0; j < rows - 1; j++) {
const rowBase = j * cols;
for (let i = 0; i < cols - 1; i++) {
const i00 = rowBase + i;
const i10 = i00 + 1;
const i01 = i00 + cols;
const i11 = i01 + 1;
const a00 = A[i00], a10 = A[i10], a01 = A[i01], a11 = A[i11];
const b00 = B[i00], b10 = B[i10], b01 = B[i01], b11 = B[i11];
if (!isFinite(a00) || !isFinite(a10) || !isFinite(a01) || !isFinite(a11)) continue;
if (!isFinite(b00) || !isFinite(b10) || !isFinite(b01) || !isFinite(b11)) continue;
const pa = a00 > 0;
if ((a10 > 0) === pa && (a01 > 0) === pa && (a11 > 0) === pa) continue;
const pb = b00 > 0;
if ((b10 > 0) === pb && (b01 > 0) === pb && (b11 > 0) === pb) continue;
const cx = (i * STEP + STEP * 0.5 - OX) * invScale;
const cy = (OY - (j * STEP + STEP * 0.5)) * invScale;
const sol = newton2(F1, F2, cx, cy);
if (!sol || !isFinite(sol.x) || !isFinite(sol.y)) continue;
let f1, f2;
try { f1 = F1(sol.x, sol.y); f2 = F2(sol.x, sol.y); } catch (e) { continue; }
if (!isFinite(f1) || !isFinite(f2)) continue;
if (Math.abs(f1) > 1e-4 || Math.abs(f2) > 1e-4) continue;
pushPoint(sol.x, sol.y);
}
}
results.sort(function (p, q) { return p.x - q.x; });
return results;
}
// ==================== 交点总入口 ====================
function findIntersections(c1, c2, visX, visY, OX, OY, scale, GW, GH) {
if (c1.ev.mode === 'explicit' && c2.ev.mode === 'explicit') {
const f1 = c1.ev.f, f2 = c2.ev.f;
const g = function (x) { return f1(x) - f2(x); };
const xs = findRoots1D(g, -visX, visX, 80);
const out = [];
for (let i = 0; i < xs.length; i++) {
const yv = f1(xs[i]);
if (isFinite(yv) && Math.abs(yv) <= visY) out.push({ x: xs[i], y: yv });
}
return out;
}
if (c1.ev.mode === 'explicit') {
const f = c1.ev.f, F = c2.F;
const g = function (x) { return F(x, f(x)); };
const xs = findRoots1D(g, -visX, visX, 80);
const out = [];
for (let i = 0; i < xs.length; i++) {
const yv = f(xs[i]);
if (isFinite(yv) && Math.abs(yv) <= visY) out.push({ x: xs[i], y: yv });
}
return out;
}
if (c2.ev.mode === 'explicit') {
const f = c2.ev.f, F = c1.F;
const g = function (x) { return F(x, f(x)); };
const xs = findRoots1D(g, -visX, visX, 80);
const out = [];
for (let i = 0; i < xs.length; i++) {
const yv = f(xs[i]);
if (isFinite(yv) && Math.abs(yv) <= visY) out.push({ x: xs[i], y: yv });
}
return out;
}
return findIntersectionsGrid(c1.F, c2.F, OX, OY, scale, GW, GH);
}
// ==================== 一维求根 ====================
function findRoots1D(g, lo, hi, N) {
const step = (hi - lo) / N;
const EPS = 1e-9;
const roots = [];
const minGap = step * 0.5;
function safeEval(x) {
try {
const v = g(x);
return (typeof v === 'number' && isFinite(v)) ? v : NaN;
} catch (e) { return NaN; }
}
function pushRoot(x) {
for (let i = roots.length - 1; i >= 0; i--) {
if (Math.abs(roots[i] - x) < minGap) return;
}
roots.push(x);
}
let prevX = lo;
let prevY = safeEval(lo);
if (isFinite(prevY) && Math.abs(prevY) < EPS) pushRoot(lo);
for (let i = 1; i <= N; i++) {
const x = lo + i * step;
const y = safeEval(x);
if (isFinite(y)) {
if (Math.abs(y) < EPS) {
pushRoot(x);
} else if (isFinite(prevY) && Math.abs(prevY) >= EPS &&
((prevY > 0) !== (y > 0))) {
let a = prevX, b = x, fa = prevY;
for (let k = 0; k < 40; k++) {
const m = (a + b) * 0.5;
const fm = safeEval(m);
if (!isFinite(fm)) break;
if (fm === 0) { a = m; b = m; break; }
if ((fa > 0) !== (fm > 0)) { b = m; } else { a = m; fa = fm; }
}
const root = (a + b) * 0.5;
const gv = safeEval(root);
if (isFinite(gv) && Math.abs(gv) < 1e-3) pushRoot(root);
}
}
prevX = x;
prevY = y;
}
roots.sort(function (a, b) { return a - b; });
return roots;
}
// ==================== 输入流程 ====================
const modeRes = await dialog.select({
title: "绘制模式",
message: "选择要绘制的图像数量",
items: [
{ label: "单条图像", value: "single" },
{ label: "多条图像(最多3条)", value: "multi" }
],
value: "single"
});
if (modeRes.action !== "confirm") return;
const mode = modeRes.value;
let multiCount = 2;
if (mode === "multi") {
const cntRes = await dialog.select({
title: "图像数量",
message: "要绘制几条图像?",
items: [
{ label: "2条", value: 2 },
{ label: "3条", value: 3 }
],
value: 2
});
if (cntRes.action !== "confirm") return;
multiCount = cntRes.value;
}
const CURVE_COLORS = ['#ff4444', '#44ddff', '#88ff44'];
async function inputEquations(count, defaults) {
const out = [];
for (let i = 0; i < count; i++) {
const title = count === 1 ? "输入方程" : `输入第 ${i + 1} 条方程`;
const message = (count === 1)
? "支持 y=f(x) 与圆锥曲线,例如:\ny=x\nx^2+y^2=25\nx^2/9+y^2/4=1"
: undefined;
const r = await dialog.text({
title: title,
message: message,
value: defaults[i],
placeholder: "y = ... 或 x^2+y^2=25",
maxLength: 256
});
if (r.action !== "confirm") return null;
const eq = r.value.trim();
if (eq.indexOf("=") < 0) {
await dialog.alert({ title: "格式错误", message: `第 ${i + 1} 条必须包含等号 =` });
return null;
}
if (!buildEvaluator(eq)) {
await dialog.alert({ title: "无法解析", message: `第 ${i + 1} 条方程无法解析` });
return null;
}
out.push(eq);
}
return out;
}
const defaults = ["y=x", "y=-x", "y=0"];
const initialEqs = await inputEquations(
mode === "single" ? 1 : multiCount,
defaults
);
if (!initialEqs) return;
const xRangeRes = await dialog.number({
title: "X轴正负范围", value: 5, min: 1, max: 30, decimals: 1
});
if (xRangeRes.action !== "confirm") return;
const xMax = xRangeRes.value;
const yRangeRes = await dialog.number({
title: "Y轴正负范围", value: 5, min: 1, max: 30, decimals: 1
});
if (yRangeRes.action !== "confirm") return;
const yMax = yRangeRes.value;
// ==================== 状态 ====================
const state = ui.signal({
eqs: initialEqs,
xRange: xMax,
yRange: yMax,
computing: false,
computed: null,
gen: 0
});
ui.setTitle("方程图像绘制器");
ui.showHeader(true);
async function promptEquations() {
const cur = state.get();
return await inputEquations(cur.eqs.length, cur.eqs);
}
// ==================== 异步分步计算 ====================
function tick() {
return new Promise(function (resolve) { setTimeout(resolve, 0); });
}
async function recompute(gen) {
try {
const s = state.get();
if (s.gen !== gen) return;
const GW = 280;
const GH = 280;
const OX = GW / 2;
const OY = GH / 2;
const scale = Math.min(OX / s.xRange, OY / s.yRange);
const visX = OX / scale;
const visY = OY / scale;
const curves = [];
for (let i = 0; i < s.eqs.length; i++) {
const c = buildCurve(s.eqs[i], CURVE_COLORS[i % CURVE_COLORS.length]);
if (c) curves.push(c);
}
await tick();
if (state.get().gen !== gen) return;
// 采样:STEP=4,点 6px,视觉平滑且性能可控
const IMPLICIT_STEP = 4;
const DOT_SIZE = 6;
const EXP_SAMPLES = curves.length <= 1 ? 120 : (curves.length === 2 ? 100 : 80);
const EXP_DOT = 5;
const curvesData = [];
for (let i = 0; i < curves.length; i++) {
const c = curves[i];
let points, dotSize;
if (c.ev.mode === 'explicit') {
points = sampleExplicitCurve(c.ev.f, visX, GW, GH, OX, OY, scale, EXP_SAMPLES);
dotSize = EXP_DOT;
} else {
points = sampleImplicitCurve(c.F, GW, GH, OX, OY, scale, IMPLICIT_STEP);
dotSize = DOT_SIZE;
}
curvesData.push({ points: points, color: c.color, dotSize: dotSize });
await tick();
if (state.get().gen !== gen) return;
}
// 坐标轴交点
const axisResults = [];
for (let i = 0; i < curves.length; i++) {
const F = curves[i].F;
const gx = function (x) { return F(x, 0); };
const xs = findRoots1D(gx, -visX, visX, 80);
const xPts = [];
for (let k = 0; k < xs.length && k < 6; k++) {
if (Math.abs(xs[k]) > visX) continue;
xPts.push({ x: xs[k], y: 0 });
}
if (xPts.length > 0) {
axisResults.push({ label: `图像${i + 1} 与 x 轴`, pts: xPts });
}
const gy = function (y) { return F(0, y); };
const ys = findRoots1D(gy, -visY, visY, 80);
const yPts = [];
for (let k = 0; k < ys.length && k < 6; k++) {
if (Math.abs(ys[k]) > visY) continue;
yPts.push({ x: 0, y: ys[k] });
}
if (yPts.length > 0) {
axisResults.push({ label: `图像${i + 1} 与 y 轴`, pts: yPts });
}
await tick();
if (state.get().gen !== gen) return;
}
// 图像间交点
const pairResults = [];
for (let i = 0; i < curves.length; i++) {
for (let j = i + 1; j < curves.length; j++) {
const pts = findIntersections(curves[i], curves[j], visX, visY, OX, OY, scale, GW, GH);
const valid = [];
for (let k = 0; k < pts.length && k < 4; k++) {
const p = pts[k];
if (p.x < -visX || p.x > visX || p.y < -visY || p.y > visY) continue;
valid.push(p);
}
if (valid.length > 0) {
pairResults.push({ label: `图像${i + 1} ∩ 图像${j + 1}`, pts: valid });
}
await tick();
if (state.get().gen !== gen) return;
}
}
state.update(function (old) {
return {
...old,
computed: {
curvesData: curvesData,
axisResults: axisResults,
pairResults: pairResults,
GW: GW, GH: GH, OX: OX, OY: OY,
scale: scale, visX: visX, visY: visY
},
computing: false
};
});
} catch (e) {
console.error('计算失败', e);
state.update(function (old) { return { ...old, computing: false }; });
}
}
function requestRecompute() {
const gen = state.get().gen + 1;
state.update(function (old) { return { ...old, gen: gen, computing: true, computed: null }; });
recompute(gen);
}
// ==================== 渲染 ====================
function buildUi() {
const s = state.get();
const children = [];
if (s.eqs.length === 1) {
children.push(ui.text("方程:" + s.eqs[0], {
size: 16, lineHeight: 20, lines: 2, color: "#ffffff"
}));
} else {
children.push(ui.text("方程列表:", {
size: 16, lineHeight: 20, lines: 1, color: "#ffffff"
}));
for (let i = 0; i < s.eqs.length; i++) {
children.push(ui.text(
`图像${i + 1}:${s.eqs[i]}`,
{ size: 14, lineHeight: 18, lines: 2, color: CURVE_COLORS[i % CURVE_COLORS.length] }
));
}
}
children.push(ui.spacer(4));
if (s.computing || !s.computed) {
children.push(ui.stack([], {
width: 280, height: 280, background: "#000000", radius: 4
}));
children.push(ui.spacer(4));
children.push(ui.text("⏳ 正在计算,请稍候…", {
size: 18, lineHeight: 22, lines: 1, color: "#89ff89", align: "center"
}));
} else {
const d = s.computed;
const nodes = [];
nodes.push(ui.stack([], { width: d.GW, height: d.GH, background: "#000000", radius: 4 }));
nodes.push(ui.stack([], { x: 0, y: d.OY - 1, width: d.GW, height: 2, background: "#39ff39" }));
nodes.push(ui.stack([], { x: d.OX - 1, y: 0, width: 2, height: d.GH, background: "#39ff39" }));
const stepX = niceStep(d.visX);
const startX = -Math.floor(d.visX / stepX) * stepX;
for (let v = startX; v <= d.visX + 1e-9; v += stepX) {
const px = Math.round(d.OX + v * d.scale);
if (px < 0 || px > d.GW) continue;
nodes.push(ui.stack([], { x: px - 1, y: d.OY - 5, width: 2, height: 10, background: "#39ff39" }));
if (Math.abs(v) > 1e-9) {
const lx = Math.max(0, Math.min(d.GW - 40, px - 20));
nodes.push(ui.text(fmtNum(v), {
x: lx, y: d.OY + 7, width: 40, height: 18,
size: 16, lineHeight: 16, lines: 1, color: "#89ff89", align: "center"
}));
}
}
const stepY = niceStep(d.visY);
const startY = -Math.floor(d.visY / stepY) * stepY;
for (let v = startY; v <= d.visY + 1e-9; v += stepY) {
const py = Math.round(d.OY - v * d.scale);
if (py < 0 || py > d.GH) continue;
nodes.push(ui.stack([], { x: d.OX - 5, y: py - 1, width: 10, height: 2, background: "#39ff39" }));
if (Math.abs(v) > 1e-9) {
nodes.push(ui.text(fmtNum(v), {
x: d.OX - 48, y: py - 9, width: 40, height: 18,
size: 16, lineHeight: 16, lines: 1, color: "#89ff89", align: "right"
}));
}
}
nodes.push(ui.text("O", {
x: d.OX + 4, y: d.OY + 4, width: 18, height: 18,
size: 16, lineHeight: 16, lines: 1, color: "#ffffff"
}));
// 曲线点
for (let ci = 0; ci < d.curvesData.length; ci++) {
const cd = d.curvesData[ci];
const dotSize = cd.dotSize;
const half = Math.floor(dotSize / 2);
for (let k = 0; k < cd.points.length; k++) {
const p = cd.points[k];
const cx = p.x - half < 0 ? 0 : (p.x - half > d.GW - dotSize ? d.GW - dotSize : p.x - half);
const cy = p.y - half < 0 ? 0 : (p.y - half > d.GH - dotSize ? d.GH - dotSize : p.y - half);
nodes.push(ui.stack([], {
x: cx, y: cy, width: dotSize, height: dotSize,
background: cd.color, radius: half
}));
}
}
// 交点标记
for (let i = 0; i < d.pairResults.length; i++) {
const pts = d.pairResults[i].pts;
for (let k = 0; k < pts.length; k++) {
const px = d.OX + pts[k].x * d.scale;
const py = d.OY - pts[k].y * d.scale;
const cx = Math.max(0, Math.min(d.GW - 8, Math.round(px) - 4));
const cy = Math.max(0, Math.min(d.GH - 8, Math.round(py) - 4));
nodes.push(ui.stack([], {
x: cx, y: cy, width: 8, height: 8,
background: "#ffff44", radius: 4
}));
}
}
for (let i = 0; i < d.axisResults.length; i++) {
const pts = d.axisResults[i].pts;
for (let k = 0; k < pts.length; k++) {
const px = d.OX + pts[k].x * d.scale;
const py = d.OY - pts[k].y * d.scale;
const cx = Math.max(0, Math.min(d.GW - 8, Math.round(px) - 4));
const cy = Math.max(0, Math.min(d.GH - 8, Math.round(py) - 4));
nodes.push(ui.stack([], {
x: cx, y: cy, width: 8, height: 8,
background: "#ff88ff", radius: 4
}));
}
}
children.push(ui.stack(nodes, { width: d.GW, height: d.GH }));
children.push(ui.spacer(4));
if (d.axisResults.length > 0) {
children.push(ui.text("与坐标轴交点:", {
size: 16, lineHeight: 20, lines: 1, color: "#ff88ff"
}));
for (let i = 0; i < d.axisResults.length; i++) {
const r = d.axisResults[i];
const parts = [];
for (let k = 0; k < r.pts.length; k++) {
parts.push(`(${fmtNum(r.pts[k].x)}, ${fmtNum(r.pts[k].y)})`);
}
children.push(ui.text(r.label + ":" + parts.join(" "), {
size: 14, lineHeight: 18, lines: 3, color: "#ffaaff"
}));
}
}
if (d.pairResults.length > 0) {
children.push(ui.text("图像交点:", {
size: 16, lineHeight: 20, lines: 1, color: "#ffff44"
}));
for (let i = 0; i < d.pairResults.length; i++) {
const r = d.pairResults[i];
const parts = [];
for (let k = 0; k < r.pts.length; k++) {
parts.push(`(${fmtNum(r.pts[k].x)}, ${fmtNum(r.pts[k].y)})`);
}
children.push(ui.text(r.label + ":" + parts.join(" "), {
size: 14, lineHeight: 18, lines: 3, color: "#ffff88"
}));
}
}
}
children.push(ui.spacer(4));
const busy = s.computing;
children.push(ui.button("重新输入方程", async () => {
if (state.get().computing) return;
try {
const newEqs = await promptEquations();
if (newEqs) {
state.update(function (old) { return { ...old, eqs: newEqs }; });
requestRecompute();
}
} catch (e) { console.error(e); }
}, { id: "btnReInput", tone: "primary", disabled: busy }));
children.push(ui.button("修改 X/Y 范围", async () => {
if (state.get().computing) return;
try {
const cur = state.get();
const rx = await dialog.number({
title: "X正负范围", value: cur.xRange, min: 1, max: 30, decimals: 1
});
if (rx.action !== "confirm") return;
const ry = await dialog.number({
title: "Y正负范围", value: cur.yRange, min: 1, max: 30, decimals: 1
});
if (ry.action !== "confirm") return;
state.update(function (old) {
return { ...old, xRange: rx.value, yRange: ry.value };
});
requestRecompute();
} catch (e) { console.error(e); }
}, { id: "btnRange", tone: "neutral", disabled: busy }));
return ui.column(children, { padding: 8, gap: 6 });
}
ui.render(buildUi);
requestRecompute();
}
return main();