Smooth LeeSori dance pose transitions
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "LeeSoriDance",
|
"name": "LeeSoriDance",
|
||||||
"status": "leesori_solo3_pose_sequence_no_overlap_2026_07_04",
|
"status": "leesori_solo3_motion_tween_2026_07_04",
|
||||||
"renderMode": "poseSequence",
|
"renderMode": "poseSequence",
|
||||||
"canvas": {
|
"canvas": {
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
},
|
},
|
||||||
"imageBase": "./Images/",
|
"imageBase": "./Images/",
|
||||||
"poseBase": "./Gestures/dance/solo3/",
|
"poseBase": "./Gestures/dance/solo3/",
|
||||||
"note": "Solo Dance 3 inspired reusable pose-sequence gesture. Runtime shows exactly one normalized frame at a time to avoid residual image overlap; original parts are retained under partsRig for future rigging.",
|
"note": "Solo Dance 3 inspired reusable pose-sequence gesture. Runtime crossfades normalized frames with per-frame transform offsets so the motion reads as a continuous upper-body dance while still avoiding residual image overlap.",
|
||||||
"gestures": [
|
"gestures": [
|
||||||
{
|
{
|
||||||
"id": "leesori.dance.solo3",
|
"id": "leesori.dance.solo3",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
"idle",
|
"idle",
|
||||||
"success"
|
"success"
|
||||||
],
|
],
|
||||||
"frameMs": 260,
|
"frameMs": 340,
|
||||||
"loop": true,
|
"loop": true,
|
||||||
"tags": [
|
"tags": [
|
||||||
"upper-body",
|
"upper-body",
|
||||||
@@ -33,29 +33,222 @@
|
|||||||
"frames": [
|
"frames": [
|
||||||
{
|
{
|
||||||
"image": "frame_01_ready.png",
|
"image": "frame_01_ready.png",
|
||||||
"label": "ready bounce"
|
"label": "ready bounce",
|
||||||
|
"motion": {
|
||||||
|
"target": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"rotate": -0.6,
|
||||||
|
"scale": 1
|
||||||
|
},
|
||||||
|
"enter": {
|
||||||
|
"x": -5,
|
||||||
|
"y": 5,
|
||||||
|
"rotate": -0.8,
|
||||||
|
"scale": 0.992
|
||||||
|
},
|
||||||
|
"exit": {
|
||||||
|
"x": 4,
|
||||||
|
"y": -3,
|
||||||
|
"rotate": 0.8,
|
||||||
|
"scale": 1.006
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"image": "frame_03_wrist_wave.png",
|
||||||
|
"label": "wrist wave prep",
|
||||||
|
"motion": {
|
||||||
|
"target": {
|
||||||
|
"x": -4,
|
||||||
|
"y": -2,
|
||||||
|
"rotate": 0.4,
|
||||||
|
"scale": 1.012
|
||||||
|
},
|
||||||
|
"enter": {
|
||||||
|
"x": -10,
|
||||||
|
"y": 4,
|
||||||
|
"rotate": -0.7,
|
||||||
|
"scale": 0.99
|
||||||
|
},
|
||||||
|
"exit": {
|
||||||
|
"x": 5,
|
||||||
|
"y": -3,
|
||||||
|
"rotate": 0.7,
|
||||||
|
"scale": 1.004
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"image": "frame_02_hand_forward.png",
|
"image": "frame_02_hand_forward.png",
|
||||||
"label": "hand forward"
|
"label": "hand forward",
|
||||||
|
"motion": {
|
||||||
|
"target": {
|
||||||
|
"x": -8,
|
||||||
|
"y": -5,
|
||||||
|
"rotate": 0.8,
|
||||||
|
"scale": 1.026
|
||||||
|
},
|
||||||
|
"enter": {
|
||||||
|
"x": -12,
|
||||||
|
"y": 5,
|
||||||
|
"rotate": -0.8,
|
||||||
|
"scale": 0.988
|
||||||
|
},
|
||||||
|
"exit": {
|
||||||
|
"x": 8,
|
||||||
|
"y": -4,
|
||||||
|
"rotate": 0.9,
|
||||||
|
"scale": 0.996
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"image": "frame_03_wrist_wave.png",
|
"image": "frame_03_wrist_wave.png",
|
||||||
"label": "wrist wave"
|
"label": "wrist wave return",
|
||||||
|
"motion": {
|
||||||
|
"target": {
|
||||||
|
"x": 3,
|
||||||
|
"y": -2,
|
||||||
|
"rotate": -0.3,
|
||||||
|
"scale": 1.014
|
||||||
|
},
|
||||||
|
"enter": {
|
||||||
|
"x": 8,
|
||||||
|
"y": 4,
|
||||||
|
"rotate": 0.9,
|
||||||
|
"scale": 0.992
|
||||||
|
},
|
||||||
|
"exit": {
|
||||||
|
"x": -5,
|
||||||
|
"y": -4,
|
||||||
|
"rotate": -0.8,
|
||||||
|
"scale": 1.002
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"image": "frame_04_arms_up.png",
|
"image": "frame_04_arms_up.png",
|
||||||
"label": "arms up"
|
"label": "arms up lift",
|
||||||
|
"motion": {
|
||||||
|
"target": {
|
||||||
|
"x": 0,
|
||||||
|
"y": -12,
|
||||||
|
"rotate": 0.2,
|
||||||
|
"scale": 1.022
|
||||||
|
},
|
||||||
|
"enter": {
|
||||||
|
"x": -5,
|
||||||
|
"y": 7,
|
||||||
|
"rotate": -0.7,
|
||||||
|
"scale": 0.99
|
||||||
|
},
|
||||||
|
"exit": {
|
||||||
|
"x": 5,
|
||||||
|
"y": -5,
|
||||||
|
"rotate": 0.6,
|
||||||
|
"scale": 1.004
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"image": "frame_05_hair_touch.png",
|
"image": "frame_05_hair_touch.png",
|
||||||
"label": "hair touch"
|
"label": "hair touch",
|
||||||
|
"motion": {
|
||||||
|
"target": {
|
||||||
|
"x": -5,
|
||||||
|
"y": -4,
|
||||||
|
"rotate": 1,
|
||||||
|
"scale": 1.018
|
||||||
|
},
|
||||||
|
"enter": {
|
||||||
|
"x": 6,
|
||||||
|
"y": 5,
|
||||||
|
"rotate": -0.6,
|
||||||
|
"scale": 0.992
|
||||||
|
},
|
||||||
|
"exit": {
|
||||||
|
"x": -6,
|
||||||
|
"y": -3,
|
||||||
|
"rotate": -0.8,
|
||||||
|
"scale": 1.002
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"image": "frame_04_arms_up.png",
|
||||||
|
"label": "arms up release",
|
||||||
|
"motion": {
|
||||||
|
"target": {
|
||||||
|
"x": 4,
|
||||||
|
"y": -8,
|
||||||
|
"rotate": -0.4,
|
||||||
|
"scale": 1.018
|
||||||
|
},
|
||||||
|
"enter": {
|
||||||
|
"x": -7,
|
||||||
|
"y": 4,
|
||||||
|
"rotate": 0.8,
|
||||||
|
"scale": 0.992
|
||||||
|
},
|
||||||
|
"exit": {
|
||||||
|
"x": 6,
|
||||||
|
"y": -3,
|
||||||
|
"rotate": 0.7,
|
||||||
|
"scale": 1.004
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"image": "frame_03_wrist_wave.png",
|
"image": "frame_03_wrist_wave.png",
|
||||||
"label": "return wave"
|
"label": "settle wave",
|
||||||
|
"motion": {
|
||||||
|
"target": {
|
||||||
|
"x": -2,
|
||||||
|
"y": -1,
|
||||||
|
"rotate": 0.2,
|
||||||
|
"scale": 1.01
|
||||||
|
},
|
||||||
|
"enter": {
|
||||||
|
"x": 7,
|
||||||
|
"y": 5,
|
||||||
|
"rotate": -0.7,
|
||||||
|
"scale": 0.992
|
||||||
|
},
|
||||||
|
"exit": {
|
||||||
|
"x": -4,
|
||||||
|
"y": -3,
|
||||||
|
"rotate": 0.7,
|
||||||
|
"scale": 1.002
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"image": "frame_01_ready.png",
|
||||||
|
"label": "ready settle",
|
||||||
|
"motion": {
|
||||||
|
"target": {
|
||||||
|
"x": 1,
|
||||||
|
"y": 1,
|
||||||
|
"rotate": 0.3,
|
||||||
|
"scale": 1.004
|
||||||
|
},
|
||||||
|
"enter": {
|
||||||
|
"x": 6,
|
||||||
|
"y": 4,
|
||||||
|
"rotate": 0.6,
|
||||||
|
"scale": 0.994
|
||||||
|
},
|
||||||
|
"exit": {
|
||||||
|
"x": -5,
|
||||||
|
"y": -3,
|
||||||
|
"rotate": -0.6,
|
||||||
|
"scale": 1.002
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"transitionMs": 190
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"partsRig": {
|
"partsRig": {
|
||||||
|
|||||||
@@ -82,19 +82,85 @@ function selectGesture(rig, state) {
|
|||||||
|| null;
|
|| null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function motionTransform(frame, phase) {
|
||||||
|
const motion = frame?.motion || {};
|
||||||
|
const target = motion.target || { x: 0, y: 0, rotate: 0, scale: 1 };
|
||||||
|
const offset = phase === "target"
|
||||||
|
? { x: 0, y: 0, rotate: 0, scale: 1 }
|
||||||
|
: (motion[phase] || { x: 0, y: 0, rotate: 0, scale: 1 });
|
||||||
|
const x = (target.x || 0) + (offset.x || 0);
|
||||||
|
const y = (target.y || 0) + (offset.y || 0);
|
||||||
|
const rotate = (target.rotate || 0) + (offset.rotate || 0);
|
||||||
|
const scale = (target.scale || 1) * (offset.scale || 1);
|
||||||
|
return `translate(${x}px, ${y}px) rotate(${rotate}deg) scale(${scale})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function frameForElement(element) {
|
||||||
|
const frames = loadedRig?.activeGesture?.frames || [];
|
||||||
|
return frames[Number(element.dataset.frameIndex)] || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideInactiveFrame(frame) {
|
||||||
|
frame.classList.remove("is-active", "is-entering", "is-exiting");
|
||||||
|
frame.style.opacity = "0";
|
||||||
|
frame.style.zIndex = "1";
|
||||||
|
}
|
||||||
|
|
||||||
function setActiveGestureFrame(index) {
|
function setActiveGestureFrame(index) {
|
||||||
const frames = [...puppet.querySelectorAll(".puppet-frame")];
|
const frames = [...puppet.querySelectorAll(".puppet-frame")];
|
||||||
if (!frames.length) return;
|
if (!frames.length) return;
|
||||||
|
|
||||||
const activeIndex = ((index % frames.length) + frames.length) % frames.length;
|
const activeIndex = ((index % frames.length) + frames.length) % frames.length;
|
||||||
|
const transitionMs = Math.max(80, Number(loadedRig?.activeGesture?.transitionMs || 170));
|
||||||
|
const current = frames.find(frame => frame.classList.contains("is-active"));
|
||||||
|
const next = frames[activeIndex];
|
||||||
|
if (!next || current === next) return;
|
||||||
|
|
||||||
for (const frame of frames) {
|
for (const frame of frames) {
|
||||||
frame.classList.toggle("is-active", Number(frame.dataset.frameIndex) === activeIndex);
|
if (frame !== current && frame !== next) {
|
||||||
|
hideInactiveFrame(frame);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (current) {
|
||||||
|
current.classList.remove("is-active", "is-entering");
|
||||||
|
current.classList.add("is-exiting");
|
||||||
|
current.style.transitionDuration = `${transitionMs}ms`;
|
||||||
|
current.style.zIndex = "2";
|
||||||
|
current.style.opacity = "0";
|
||||||
|
current.style.transform = motionTransform(frameForElement(current), "exit");
|
||||||
|
window.setTimeout(() => {
|
||||||
|
if (current.classList.contains("is-exiting")) hideInactiveFrame(current);
|
||||||
|
}, transitionMs + 40);
|
||||||
|
}
|
||||||
|
|
||||||
|
next.classList.remove("is-exiting");
|
||||||
|
next.classList.add("is-entering");
|
||||||
|
next.style.transitionDuration = `${transitionMs}ms`;
|
||||||
|
next.style.zIndex = "3";
|
||||||
|
next.style.opacity = "0";
|
||||||
|
next.style.transform = motionTransform(frameForElement(next), "enter");
|
||||||
|
void next.offsetWidth;
|
||||||
|
next.classList.add("is-active");
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
next.style.opacity = "1";
|
||||||
|
next.style.transform = motionTransform(frameForElement(next), "target");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function startPoseSequence(gesture) {
|
function startPoseSequence(gesture) {
|
||||||
stopGestureTimer();
|
stopGestureTimer();
|
||||||
setActiveGestureFrame(0);
|
loadedRig.activeGesture = gesture;
|
||||||
const frameMs = Math.max(80, Number(gesture.frameMs || 260));
|
const first = puppet.querySelector('.puppet-frame[data-frame-index="0"]');
|
||||||
|
if (first) {
|
||||||
|
first.style.transitionDuration = "0ms";
|
||||||
|
first.style.opacity = "1";
|
||||||
|
first.style.zIndex = "3";
|
||||||
|
first.style.transform = motionTransform(frameForElement(first), "target");
|
||||||
|
first.classList.add("is-active");
|
||||||
|
}
|
||||||
|
|
||||||
|
const frameMs = Math.max(120, Number(gesture.frameMs || 340));
|
||||||
if (gesture.loop !== false && (gesture.frames?.length || 0) > 1) {
|
if (gesture.loop !== false && (gesture.frames?.length || 0) > 1) {
|
||||||
gestureTimer = window.setInterval(() => {
|
gestureTimer = window.setInterval(() => {
|
||||||
gestureFrame += 1;
|
gestureFrame += 1;
|
||||||
@@ -123,6 +189,7 @@ function renderPoseSequence(rig, active, gesture) {
|
|||||||
img.src = `${poseBase}${frame.image}?v=${assetVersion}`;
|
img.src = `${poseBase}${frame.image}?v=${assetVersion}`;
|
||||||
img.alt = "";
|
img.alt = "";
|
||||||
img.decoding = "async";
|
img.decoding = "async";
|
||||||
|
img.style.transform = motionTransform(frame, "enter");
|
||||||
fragment.appendChild(img);
|
fragment.appendChild(img);
|
||||||
}
|
}
|
||||||
puppet.replaceChildren(fragment);
|
puppet.replaceChildren(fragment);
|
||||||
@@ -256,3 +323,4 @@ window.addEventListener("DOMContentLoaded", async () => {
|
|||||||
setState("idle");
|
setState("idle");
|
||||||
post("hostReady");
|
post("hostReady");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -76,13 +76,20 @@ body {
|
|||||||
.puppet-frame {
|
.puppet-frame {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
transform-origin: 50% 38%;
|
transform-origin: 50% 40%;
|
||||||
|
transition-property: opacity, transform;
|
||||||
|
transition-duration: 180ms;
|
||||||
|
transition-timing-function: cubic-bezier(.22,.84,.32,1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.puppet-frame.is-active {
|
.puppet-frame.is-active {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.puppet-frame.is-exiting {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
#puppet[data-mode="pose-sequence"] .puppet-part {
|
#puppet[data-mode="pose-sequence"] .puppet-part {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -456,3 +463,4 @@ body {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+203
-10
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "LeeSoriDance",
|
"name": "LeeSoriDance",
|
||||||
"status": "leesori_solo3_pose_sequence_no_overlap_2026_07_04",
|
"status": "leesori_solo3_motion_tween_2026_07_04",
|
||||||
"renderMode": "poseSequence",
|
"renderMode": "poseSequence",
|
||||||
"canvas": {
|
"canvas": {
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
},
|
},
|
||||||
"imageBase": "./Images/",
|
"imageBase": "./Images/",
|
||||||
"poseBase": "./Gestures/dance/solo3/",
|
"poseBase": "./Gestures/dance/solo3/",
|
||||||
"note": "Solo Dance 3 inspired reusable pose-sequence gesture. Runtime shows exactly one normalized frame at a time to avoid residual image overlap; original parts are retained under partsRig for future rigging.",
|
"note": "Solo Dance 3 inspired reusable pose-sequence gesture. Runtime crossfades normalized frames with per-frame transform offsets so the motion reads as a continuous upper-body dance while still avoiding residual image overlap.",
|
||||||
"gestures": [
|
"gestures": [
|
||||||
{
|
{
|
||||||
"id": "leesori.dance.solo3",
|
"id": "leesori.dance.solo3",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
"idle",
|
"idle",
|
||||||
"success"
|
"success"
|
||||||
],
|
],
|
||||||
"frameMs": 260,
|
"frameMs": 340,
|
||||||
"loop": true,
|
"loop": true,
|
||||||
"tags": [
|
"tags": [
|
||||||
"upper-body",
|
"upper-body",
|
||||||
@@ -33,29 +33,222 @@
|
|||||||
"frames": [
|
"frames": [
|
||||||
{
|
{
|
||||||
"image": "frame_01_ready.png",
|
"image": "frame_01_ready.png",
|
||||||
"label": "ready bounce"
|
"label": "ready bounce",
|
||||||
|
"motion": {
|
||||||
|
"target": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"rotate": -0.6,
|
||||||
|
"scale": 1
|
||||||
|
},
|
||||||
|
"enter": {
|
||||||
|
"x": -5,
|
||||||
|
"y": 5,
|
||||||
|
"rotate": -0.8,
|
||||||
|
"scale": 0.992
|
||||||
|
},
|
||||||
|
"exit": {
|
||||||
|
"x": 4,
|
||||||
|
"y": -3,
|
||||||
|
"rotate": 0.8,
|
||||||
|
"scale": 1.006
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"image": "frame_03_wrist_wave.png",
|
||||||
|
"label": "wrist wave prep",
|
||||||
|
"motion": {
|
||||||
|
"target": {
|
||||||
|
"x": -4,
|
||||||
|
"y": -2,
|
||||||
|
"rotate": 0.4,
|
||||||
|
"scale": 1.012
|
||||||
|
},
|
||||||
|
"enter": {
|
||||||
|
"x": -10,
|
||||||
|
"y": 4,
|
||||||
|
"rotate": -0.7,
|
||||||
|
"scale": 0.99
|
||||||
|
},
|
||||||
|
"exit": {
|
||||||
|
"x": 5,
|
||||||
|
"y": -3,
|
||||||
|
"rotate": 0.7,
|
||||||
|
"scale": 1.004
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"image": "frame_02_hand_forward.png",
|
"image": "frame_02_hand_forward.png",
|
||||||
"label": "hand forward"
|
"label": "hand forward",
|
||||||
|
"motion": {
|
||||||
|
"target": {
|
||||||
|
"x": -8,
|
||||||
|
"y": -5,
|
||||||
|
"rotate": 0.8,
|
||||||
|
"scale": 1.026
|
||||||
|
},
|
||||||
|
"enter": {
|
||||||
|
"x": -12,
|
||||||
|
"y": 5,
|
||||||
|
"rotate": -0.8,
|
||||||
|
"scale": 0.988
|
||||||
|
},
|
||||||
|
"exit": {
|
||||||
|
"x": 8,
|
||||||
|
"y": -4,
|
||||||
|
"rotate": 0.9,
|
||||||
|
"scale": 0.996
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"image": "frame_03_wrist_wave.png",
|
"image": "frame_03_wrist_wave.png",
|
||||||
"label": "wrist wave"
|
"label": "wrist wave return",
|
||||||
|
"motion": {
|
||||||
|
"target": {
|
||||||
|
"x": 3,
|
||||||
|
"y": -2,
|
||||||
|
"rotate": -0.3,
|
||||||
|
"scale": 1.014
|
||||||
|
},
|
||||||
|
"enter": {
|
||||||
|
"x": 8,
|
||||||
|
"y": 4,
|
||||||
|
"rotate": 0.9,
|
||||||
|
"scale": 0.992
|
||||||
|
},
|
||||||
|
"exit": {
|
||||||
|
"x": -5,
|
||||||
|
"y": -4,
|
||||||
|
"rotate": -0.8,
|
||||||
|
"scale": 1.002
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"image": "frame_04_arms_up.png",
|
"image": "frame_04_arms_up.png",
|
||||||
"label": "arms up"
|
"label": "arms up lift",
|
||||||
|
"motion": {
|
||||||
|
"target": {
|
||||||
|
"x": 0,
|
||||||
|
"y": -12,
|
||||||
|
"rotate": 0.2,
|
||||||
|
"scale": 1.022
|
||||||
|
},
|
||||||
|
"enter": {
|
||||||
|
"x": -5,
|
||||||
|
"y": 7,
|
||||||
|
"rotate": -0.7,
|
||||||
|
"scale": 0.99
|
||||||
|
},
|
||||||
|
"exit": {
|
||||||
|
"x": 5,
|
||||||
|
"y": -5,
|
||||||
|
"rotate": 0.6,
|
||||||
|
"scale": 1.004
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"image": "frame_05_hair_touch.png",
|
"image": "frame_05_hair_touch.png",
|
||||||
"label": "hair touch"
|
"label": "hair touch",
|
||||||
|
"motion": {
|
||||||
|
"target": {
|
||||||
|
"x": -5,
|
||||||
|
"y": -4,
|
||||||
|
"rotate": 1,
|
||||||
|
"scale": 1.018
|
||||||
|
},
|
||||||
|
"enter": {
|
||||||
|
"x": 6,
|
||||||
|
"y": 5,
|
||||||
|
"rotate": -0.6,
|
||||||
|
"scale": 0.992
|
||||||
|
},
|
||||||
|
"exit": {
|
||||||
|
"x": -6,
|
||||||
|
"y": -3,
|
||||||
|
"rotate": -0.8,
|
||||||
|
"scale": 1.002
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"image": "frame_04_arms_up.png",
|
||||||
|
"label": "arms up release",
|
||||||
|
"motion": {
|
||||||
|
"target": {
|
||||||
|
"x": 4,
|
||||||
|
"y": -8,
|
||||||
|
"rotate": -0.4,
|
||||||
|
"scale": 1.018
|
||||||
|
},
|
||||||
|
"enter": {
|
||||||
|
"x": -7,
|
||||||
|
"y": 4,
|
||||||
|
"rotate": 0.8,
|
||||||
|
"scale": 0.992
|
||||||
|
},
|
||||||
|
"exit": {
|
||||||
|
"x": 6,
|
||||||
|
"y": -3,
|
||||||
|
"rotate": 0.7,
|
||||||
|
"scale": 1.004
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"image": "frame_03_wrist_wave.png",
|
"image": "frame_03_wrist_wave.png",
|
||||||
"label": "return wave"
|
"label": "settle wave",
|
||||||
|
"motion": {
|
||||||
|
"target": {
|
||||||
|
"x": -2,
|
||||||
|
"y": -1,
|
||||||
|
"rotate": 0.2,
|
||||||
|
"scale": 1.01
|
||||||
|
},
|
||||||
|
"enter": {
|
||||||
|
"x": 7,
|
||||||
|
"y": 5,
|
||||||
|
"rotate": -0.7,
|
||||||
|
"scale": 0.992
|
||||||
|
},
|
||||||
|
"exit": {
|
||||||
|
"x": -4,
|
||||||
|
"y": -3,
|
||||||
|
"rotate": 0.7,
|
||||||
|
"scale": 1.002
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"image": "frame_01_ready.png",
|
||||||
|
"label": "ready settle",
|
||||||
|
"motion": {
|
||||||
|
"target": {
|
||||||
|
"x": 1,
|
||||||
|
"y": 1,
|
||||||
|
"rotate": 0.3,
|
||||||
|
"scale": 1.004
|
||||||
|
},
|
||||||
|
"enter": {
|
||||||
|
"x": 6,
|
||||||
|
"y": 4,
|
||||||
|
"rotate": 0.6,
|
||||||
|
"scale": 0.994
|
||||||
|
},
|
||||||
|
"exit": {
|
||||||
|
"x": -5,
|
||||||
|
"y": -3,
|
||||||
|
"rotate": -0.6,
|
||||||
|
"scale": 1.002
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"transitionMs": 190
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"partsRig": {
|
"partsRig": {
|
||||||
|
|||||||
@@ -82,19 +82,85 @@ function selectGesture(rig, state) {
|
|||||||
|| null;
|
|| null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function motionTransform(frame, phase) {
|
||||||
|
const motion = frame?.motion || {};
|
||||||
|
const target = motion.target || { x: 0, y: 0, rotate: 0, scale: 1 };
|
||||||
|
const offset = phase === "target"
|
||||||
|
? { x: 0, y: 0, rotate: 0, scale: 1 }
|
||||||
|
: (motion[phase] || { x: 0, y: 0, rotate: 0, scale: 1 });
|
||||||
|
const x = (target.x || 0) + (offset.x || 0);
|
||||||
|
const y = (target.y || 0) + (offset.y || 0);
|
||||||
|
const rotate = (target.rotate || 0) + (offset.rotate || 0);
|
||||||
|
const scale = (target.scale || 1) * (offset.scale || 1);
|
||||||
|
return `translate(${x}px, ${y}px) rotate(${rotate}deg) scale(${scale})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function frameForElement(element) {
|
||||||
|
const frames = loadedRig?.activeGesture?.frames || [];
|
||||||
|
return frames[Number(element.dataset.frameIndex)] || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideInactiveFrame(frame) {
|
||||||
|
frame.classList.remove("is-active", "is-entering", "is-exiting");
|
||||||
|
frame.style.opacity = "0";
|
||||||
|
frame.style.zIndex = "1";
|
||||||
|
}
|
||||||
|
|
||||||
function setActiveGestureFrame(index) {
|
function setActiveGestureFrame(index) {
|
||||||
const frames = [...puppet.querySelectorAll(".puppet-frame")];
|
const frames = [...puppet.querySelectorAll(".puppet-frame")];
|
||||||
if (!frames.length) return;
|
if (!frames.length) return;
|
||||||
|
|
||||||
const activeIndex = ((index % frames.length) + frames.length) % frames.length;
|
const activeIndex = ((index % frames.length) + frames.length) % frames.length;
|
||||||
|
const transitionMs = Math.max(80, Number(loadedRig?.activeGesture?.transitionMs || 170));
|
||||||
|
const current = frames.find(frame => frame.classList.contains("is-active"));
|
||||||
|
const next = frames[activeIndex];
|
||||||
|
if (!next || current === next) return;
|
||||||
|
|
||||||
for (const frame of frames) {
|
for (const frame of frames) {
|
||||||
frame.classList.toggle("is-active", Number(frame.dataset.frameIndex) === activeIndex);
|
if (frame !== current && frame !== next) {
|
||||||
|
hideInactiveFrame(frame);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (current) {
|
||||||
|
current.classList.remove("is-active", "is-entering");
|
||||||
|
current.classList.add("is-exiting");
|
||||||
|
current.style.transitionDuration = `${transitionMs}ms`;
|
||||||
|
current.style.zIndex = "2";
|
||||||
|
current.style.opacity = "0";
|
||||||
|
current.style.transform = motionTransform(frameForElement(current), "exit");
|
||||||
|
window.setTimeout(() => {
|
||||||
|
if (current.classList.contains("is-exiting")) hideInactiveFrame(current);
|
||||||
|
}, transitionMs + 40);
|
||||||
|
}
|
||||||
|
|
||||||
|
next.classList.remove("is-exiting");
|
||||||
|
next.classList.add("is-entering");
|
||||||
|
next.style.transitionDuration = `${transitionMs}ms`;
|
||||||
|
next.style.zIndex = "3";
|
||||||
|
next.style.opacity = "0";
|
||||||
|
next.style.transform = motionTransform(frameForElement(next), "enter");
|
||||||
|
void next.offsetWidth;
|
||||||
|
next.classList.add("is-active");
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
next.style.opacity = "1";
|
||||||
|
next.style.transform = motionTransform(frameForElement(next), "target");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function startPoseSequence(gesture) {
|
function startPoseSequence(gesture) {
|
||||||
stopGestureTimer();
|
stopGestureTimer();
|
||||||
setActiveGestureFrame(0);
|
loadedRig.activeGesture = gesture;
|
||||||
const frameMs = Math.max(80, Number(gesture.frameMs || 260));
|
const first = puppet.querySelector('.puppet-frame[data-frame-index="0"]');
|
||||||
|
if (first) {
|
||||||
|
first.style.transitionDuration = "0ms";
|
||||||
|
first.style.opacity = "1";
|
||||||
|
first.style.zIndex = "3";
|
||||||
|
first.style.transform = motionTransform(frameForElement(first), "target");
|
||||||
|
first.classList.add("is-active");
|
||||||
|
}
|
||||||
|
|
||||||
|
const frameMs = Math.max(120, Number(gesture.frameMs || 340));
|
||||||
if (gesture.loop !== false && (gesture.frames?.length || 0) > 1) {
|
if (gesture.loop !== false && (gesture.frames?.length || 0) > 1) {
|
||||||
gestureTimer = window.setInterval(() => {
|
gestureTimer = window.setInterval(() => {
|
||||||
gestureFrame += 1;
|
gestureFrame += 1;
|
||||||
@@ -123,6 +189,7 @@ function renderPoseSequence(rig, active, gesture) {
|
|||||||
img.src = `${poseBase}${frame.image}?v=${assetVersion}`;
|
img.src = `${poseBase}${frame.image}?v=${assetVersion}`;
|
||||||
img.alt = "";
|
img.alt = "";
|
||||||
img.decoding = "async";
|
img.decoding = "async";
|
||||||
|
img.style.transform = motionTransform(frame, "enter");
|
||||||
fragment.appendChild(img);
|
fragment.appendChild(img);
|
||||||
}
|
}
|
||||||
puppet.replaceChildren(fragment);
|
puppet.replaceChildren(fragment);
|
||||||
@@ -256,3 +323,4 @@ window.addEventListener("DOMContentLoaded", async () => {
|
|||||||
setState("idle");
|
setState("idle");
|
||||||
post("hostReady");
|
post("hostReady");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -76,13 +76,20 @@ body {
|
|||||||
.puppet-frame {
|
.puppet-frame {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
transform-origin: 50% 38%;
|
transform-origin: 50% 40%;
|
||||||
|
transition-property: opacity, transform;
|
||||||
|
transition-duration: 180ms;
|
||||||
|
transition-timing-function: cubic-bezier(.22,.84,.32,1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.puppet-frame.is-active {
|
.puppet-frame.is-active {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.puppet-frame.is-exiting {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
#puppet[data-mode="pose-sequence"] .puppet-part {
|
#puppet[data-mode="pose-sequence"] .puppet-part {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -456,3 +463,4 @@ body {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user