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:
parent
ae432e7654
commit
c8dc24d3d1
2 changed files with 111 additions and 0 deletions
|
|
@ -105,6 +105,35 @@
|
|||
// reference (idle, tap); anything richer waits on her own rig.
|
||||
posture: function (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) {}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -286,6 +286,88 @@ Singleton {
|
|||
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) {
|
||||
root._send({ op: "eval", script: script });
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue