feat:加上拍照模式与自定义颜色渐变
This commit is contained in:
@@ -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
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user