feat:加上拍照模式与自定义颜色渐变

This commit is contained in:
Edom
2026-08-22 22:56:46 +08:00
parent 62f39d27e7
commit f3b74b572a
23 changed files with 2707 additions and 126 deletions
+84 -7
View File
@@ -73,6 +73,7 @@
this.sizePx = opts.sizePx || null;
this.eyeColor = opts.eyeColor || null;
this.inkFlat = opts.inkFlat || null;
this.inkGrad = opts.inkGrad || null;
this.geo = opts.geo || g.GROK_GEO;
this.playlists = opts.playlist || EYE_PLAYLIST;
@@ -421,20 +422,65 @@
setColor(id, scheme) {
this.colorId = id;
if (scheme) this.scheme = scheme;
if (this.inkFlat) {
this.svg.style.setProperty("--fg", this.inkFlat);
} else if (this.loginWrap) {
this.svg.style.setProperty("--fg", inkFg(id));
if (this.inkGrad) {
this._paintInkGrad();
} else {
const pal = this.geo.palette[id] || this.geo.palette.black;
this.svg.style.setProperty("--fg", this.scheme === "dark" ? pal.dark : pal.light);
if (this.inkFlat) {
this.svg.style.setProperty("--fg", this.inkFlat);
} else if (this.loginWrap) {
this.svg.style.setProperty("--fg", inkFg(id));
} else {
const pal = this.geo.palette[id] || this.geo.palette.black;
this.svg.style.setProperty("--fg", this.scheme === "dark" ? pal.dark : pal.light);
}
this.svg.style.setProperty("--ink", inkCss(id));
this.body.setAttribute("fill", "var(--fg, #000)");
}
this.svg.style.setProperty("--ink", inkCss(id));
this.svg.style.setProperty("--bg", this.eyeColor || EYE_BG);
}
_paintInkGrad() {
const spec = this.inkGrad;
const vb = this.geo.viewBox;
const cx = vb.minX + vb.width / 2;
const cy = vb.minY + vb.height / 2;
if (spec.radial) {
const r = (Math.hypot(vb.width, vb.height) / 2) * 0.72;
this.inkRadialEl.setAttribute("cx", cx);
this.inkRadialEl.setAttribute("cy", vb.minY + vb.height * 0.42);
this.inkRadialEl.setAttribute("r", r);
this.inkRadialStops[0].setAttribute("stop-color", spec.from);
this.inkRadialStops[1].setAttribute("stop-color", spec.to);
this.body.setAttribute("fill", `url(#${this.inkRadialId})`);
this.svg.style.setProperty("--fg", spec.from);
this.svg.style.setProperty("--ink", `radial-gradient(circle at 50% 42%, ${spec.from}, ${spec.to})`);
return;
}
const deg = ((Number(spec.angle) || 0) % 360 + 360) % 360;
const rad = (deg * Math.PI) / 180;
const span = Math.hypot(vb.width, vb.height) / 2;
const dx = Math.sin(rad) * span;
const dy = -Math.cos(rad) * span;
this.inkGradEl.setAttribute("x1", cx - dx);
this.inkGradEl.setAttribute("y1", cy - dy);
this.inkGradEl.setAttribute("x2", cx + dx);
this.inkGradEl.setAttribute("y2", cy + dy);
this.inkStops[0].setAttribute("stop-color", spec.from);
this.inkStops[1].setAttribute("stop-color", spec.to);
this.body.setAttribute("fill", `url(#${this.inkGradId})`);
this.svg.style.setProperty("--fg", spec.from);
this.svg.style.setProperty("--ink", `linear-gradient(${deg}deg, ${spec.from}, ${spec.to})`);
}
setInk(flat) {
this.inkFlat = flat || null;
if (this.inkFlat) this.inkGrad = null;
this.setColor(this.colorId);
}
setInkGrad(grad) {
this.inkGrad = grad ? { ...grad } : null;
if (this.inkGrad) this.inkFlat = null;
this.setColor(this.colorId);
}
@@ -738,6 +784,28 @@
this.clipPath = document.createElementNS(ns, "path");
clip.appendChild(this.clipPath);
defs.appendChild(clip);
this.inkGradId = `grok-ink-${Math.random().toString(36).slice(2, 8)}`;
this.inkGradEl = document.createElementNS(ns, "linearGradient");
this.inkGradEl.setAttribute("id", this.inkGradId);
this.inkGradEl.setAttribute("gradientUnits", "userSpaceOnUse");
this.inkStops = [0, 1].map((offset) => {
const s = document.createElementNS(ns, "stop");
s.setAttribute("offset", String(offset));
this.inkGradEl.appendChild(s);
return s;
});
defs.appendChild(this.inkGradEl);
this.inkRadialId = `grok-ink-r-${Math.random().toString(36).slice(2, 8)}`;
this.inkRadialEl = document.createElementNS(ns, "radialGradient");
this.inkRadialEl.setAttribute("id", this.inkRadialId);
this.inkRadialEl.setAttribute("gradientUnits", "userSpaceOnUse");
this.inkRadialStops = [0, 1].map((offset) => {
const s = document.createElementNS(ns, "stop");
s.setAttribute("offset", String(offset));
this.inkRadialEl.appendChild(s);
return s;
});
defs.appendChild(this.inkRadialEl);
this.svg.appendChild(defs);
this.group = document.createElementNS(ns, "g");
@@ -1318,12 +1386,21 @@
manualMix: 0,
});
if (holdMix > 0.002) {
// Eye-sphere rim = visible head half-width in overlay units, so the
// fold/slice lands on the silhouette like the original lab.
let rim = 95;
if (restRing) {
let hw = 0;
for (const p of restRing) hw = Math.max(hw, Math.abs(p[0] - R));
rim = (hw * 300) / (R * 3.16);
}
EY.blendCustomEyes({
eyeEls: this.eyeEls,
pose: this._poseNow(),
eyes: this.eyeTune,
svg: this.svg,
mix: holdMix,
rim,
});
}
+28 -13
View File
@@ -301,19 +301,25 @@
return pts;
}
function projectEye(pose, home, faceX, faceY, lx, ly, angle) {
function projectEye(pose, home, faceX, faceY, lx, ly, angle, rim) {
const a = (angle || 0) * Math.PI / 180;
const c = Math.cos(a);
const s = Math.sin(a);
const x = faceX + lx * c - ly * s;
const y = faceY + lx * s + ly * c;
const lon = x / FACE_R;
const lat = y / FACE_R;
const cl = Math.cos(lat);
const rotated = quatRotate(screenOf(pose, home), [cl * Math.sin(lon), Math.sin(lat), cl * Math.cos(lon)]);
const depth = rotated[2] * FACE_R;
// Face coords are authored against FACE_R=120 (original lab units) but the
// physical head rides `rim` overlay units so the eye sphere's silhouette
// lands on the visible head edge. Past the rim points stay raw on the z=0
// plane (ellipsoidFrontSample semantics): the eye rides over the edge and
// gets sliced by the head clip instead of folding back inward.
const headR = rim || 95;
const dx = x / FACE_R;
const dy = y / FACE_R;
const dz = Math.sqrt(Math.max(0, 1 - dx * dx - dy * dy));
const rotated = quatRotate(screenOf(pose, home), [dx, dy, dz]);
const depth = rotated[2] * headR;
const sc = FOCAL / Math.max(FOCAL - depth, 0.0001);
return [rotated[0] * FACE_R * sc, rotated[1] * FACE_R * sc, rotated[2]];
return [rotated[0] * headR * sc, rotated[1] * headR * sc, rotated[2]];
}
function overlayToSvg(svg, x, y) {
@@ -369,7 +375,7 @@
return pts;
}
function customEyePolys({ pose, eyes, svg, n = 48, home = EYE_HOME }) {
function customEyePolys({ pose, eyes, svg, n = 48, home = EYE_HOME, rim }) {
if (!svg || !pose) return null;
const src = eyes || DEFAULT_EYES;
const sides = ["left", "right"];
@@ -380,18 +386,23 @@
const local = roundedRect(Math.max(tune.width, 1), Math.max(tune.height, 1));
let z = 0;
const projected = local.map(([lx, ly]) => {
const p = projectEye(pose, home, faceX, faceY, lx, ly, tune.angle);
const p = projectEye(pose, home, faceX, faceY, lx, ly, tune.angle, rim);
z += p[2];
return overlayToSvg(svg, p[0], p[1]);
});
return { pts: resampleRing(projected, n), visible: z > 0 };
return { pts: resampleRing(projected, n), visible: z > 0, zAvg: z / Math.max(local.length, 1) };
});
}
function blendCustomEyes({ eyeEls, pose, eyes, svg, mix, n = 48 }) {
// Visibility hysteresis: drag jitter around the silhouette boundary would
// otherwise pop the eye in and out every few frames.
const VIS_HIDE = -0.006;
const VIS_SHOW = 0.004;
function blendCustomEyes({ eyeEls, pose, eyes, svg, mix, n = 48, rim }) {
const k = clamp(mix, 0, 1);
if (k <= 0.002 || !eyeEls) return;
const custom = customEyePolys({ pose, eyes, svg, n });
const custom = customEyePolys({ pose, eyes, svg, n, rim });
if (!custom) return;
const gate = 0.22;
const shapeK = k >= gate ? 1 : Dke(k / gate);
@@ -399,9 +410,13 @@
const el = eyeEls[i];
const expr = sampleEl(el, n);
if (!el || !expr || !custom[i]) continue;
let shown = el.__eyeShown != null ? el.__eyeShown : custom[i].visible;
if (shown && custom[i].zAvg < VIS_HIDE) shown = false;
if (!shown && custom[i].zAvg > VIS_SHOW) shown = true;
el.__eyeShown = shown;
el.setAttribute("d", polyPath(lerpPoly(custom[i].pts, expr, 1 - shapeK)));
el.removeAttribute("transform");
if (shapeK > 0.55) el.style.display = custom[i].visible ? "" : "none";
if (shapeK > 0.55) el.style.display = shown ? "" : "none";
}
}