Watch
1
0
Fork
You've already forked souveraine
0

face: follow the finger anywhere on the panel

Subscribes to viewtop's gaze feed and dispatches a synthetic pointer move on
the canvas, so live2d.js's own mousemove path drives head angle and eye
direction with its existing damping. Synthetic is the point: she is
deliberately not being given those contacts, and this lets her watch them
anyway.

Screen coordinates in, her window's coordinates out — the origin comes from
the compositor's furniture report rather than assumed, because the layout
decides it and it moves with the zone.
This commit is contained in:
Fimeg 2026-08-07 01:01:05 -04:00
commit c8dc24d3d1
2 changed files with 111 additions and 0 deletions

View file

@ -105,6 +105,35 @@
// reference (idle, tap); anything richer waits on her own rig. // reference (idle, tap); anything richer waits on her own rig.
posture: function (name) { posture: function (name) {
if (typeof window.live2dMotion === 'function') window.live2dMotion(name); if (typeof window.live2dMotion === 'function') window.live2dMotion(name);
},
// Look at a point on the panel, in her window's own CSS pixels.
//
// Dispatched as a synthetic pointer move on the canvas rather than driven
// through the rig's parameters by hand: `live2d.js` already binds
// mousemove/touchmove and already turns a pointer into head angle and eye
// direction with its own damping. Feeding its path costs nothing and
// cannot drift from what a real finger does, because it *is* what a real
// finger does.
//
// Synthetic events are why this works at all now. Her input region is her
// silhouette, so the compositor stops handing her contacts that land on
// the wallpaper — which is what made her stop tracking unless you touched
// her first. These do not go through the compositor, so she can follow a
// finger she is deliberately not being given.
lookAt: function (x, y) {
try {
canvas.dispatchEvent(new MouseEvent('mousemove', {
clientX: x, clientY: y, bubbles: true
}));
} catch (e) {}
},
// Nobody is touching the glass. Without this she holds the last position
// she was told about and stares at it until someone touches the screen
// again.
lookAway: function () {
try {
canvas.dispatchEvent(new MouseEvent('mouseout', { bubbles: true }));
} catch (e) {}
} }
}; };

View file

@ -286,6 +286,88 @@ Singleton {
sock.write(JSON.stringify(msg) + "\n"); sock.write(JSON.stringify(msg) + "\n");
} }
// Where fingers are, straight from the compositor, so she can look at them.
//
// Only open while she is up, because the compositor throttles but does not
// stop: a feed nobody is reading is a socket buffer filling behind a face
// that is not on screen.
//
// Screen coordinates come in; her window's own coordinates go out. The
// compositor reports in logical panel pixels and the page thinks in CSS
// pixels inside her window, so the origin has to be subtracted or she
// looks at a point offset by however far down the panel she is standing.
Socket {
id: gaze
path: (Quickshell.env("XDG_RUNTIME_DIR") || "/run/user/1000") + "/souveraine/viewtop.sock"
connected: root.joined
onConnectedChanged: {
if (gaze.connected)
gaze.write('{"op":"gaze"}\n');
}
parser: SplitParser {
splitMarker: "\n"
onRead: line => {
let m;
try {
m = JSON.parse(line);
} catch (e) {
return;
}
if (m.ok !== undefined && m.down === undefined)
return;
if (!m.down) {
root._eval("window.face.lookAway()");
return;
}
root._eval(`window.face.lookAt(${m.x - root.originX}, ${m.y - root.originY})`);
}
}
}
// Where her window sits on the panel. Read from the compositor's own
// furniture report rather than assumed, because the layout decides it and
// it moves with the zone.
property real originX: 0
property real originY: 0
Socket {
id: whereAmI
path: (Quickshell.env("XDG_RUNTIME_DIR") || "/run/user/1000") + "/souveraine/viewtop.sock"
onConnectedChanged: {
if (whereAmI.connected)
whereAmI.write('{"op":"state"}\n');
}
parser: SplitParser {
splitMarker: "\n"
onRead: line => {
try {
const s = JSON.parse(line);
const f = (s.furniture ?? [])[0];
if (f?.at) {
root.originX = f.at.x;
root.originY = f.at.y;
}
} catch (e) {}
whereAmI.connected = false;
}
}
}
// Asked once she is up, and again a moment later: the first answer can
// land before the compositor has stood her up, and then her origin is
// whatever the last window left there.
Timer {
running: root.joined
interval: 2000
repeat: true
triggeredOnStart: true
onTriggered: whereAmI.connected = true
}
function _eval(script) { function _eval(script) {
root._send({ op: "eval", script: script }); root._send({ op: "eval", script: script });
} }