Skip to content

Commit 61599a4

Browse files
LIlGGCopilot
andcommitted
Align Live2D tips with model
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 368793c commit 61599a4

5 files changed

Lines changed: 91 additions & 6 deletions

File tree

packages/live2d/src/components/Live2dTips.tsx

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
type Live2dConfig,
44
configContext,
55
} from "@/live2d/context/config-context";
6+
import type { ModelLayoutEvent } from "@/live2d/events/model-layout";
67
import type { SendMessageEvent } from "@/live2d/events/send-message";
78
import type {
89
StreamMessageEvent,
@@ -18,6 +19,9 @@ import { classMap } from "lit/directives/class-map.js";
1819
import { unsafeHTML } from "lit/directives/unsafe-html.js";
1920

2021
export class Live2dTips extends UnoLitElement {
22+
private static readonly DEFAULT_BOTTOM_OFFSET = 250;
23+
private static readonly MODEL_OVERLAP_OFFSET = 20;
24+
2125
@consume({ context: configContext })
2226
@property({ attribute: false })
2327
public config?: Live2dConfig;
@@ -26,6 +30,8 @@ export class Live2dTips extends UnoLitElement {
2630
private _isShow = false;
2731
@state()
2832
private _message = "";
33+
@state()
34+
private _bottomOffset = Live2dTips.DEFAULT_BOTTOM_OFFSET;
2935
private priority = -1;
3036
private messageTimer: number | null = null;
3137
// 流式消息模式标志
@@ -42,6 +48,9 @@ export class Live2dTips extends UnoLitElement {
4248
private readonly onStreamStop = (event: Event) => {
4349
this.handleStreamStop(event as StreamMessageStopEvent);
4450
};
51+
private readonly onModelLayout = (event: Event) => {
52+
this.handleModelLayout(event as ModelLayoutEvent);
53+
};
4554

4655
constructor() {
4756
super();
@@ -73,21 +82,20 @@ export class Live2dTips extends UnoLitElement {
7382
"select-none": true,
7483
};
7584
return html`
76-
<div
77-
id="live2d-tips"
78-
class=${classMap(classes)}
79-
>
85+
<div id="live2d-tips" class=${classMap(classes)}>
8086
${unsafeHTML(this._message)}
8187
</div>
8288
`;
8389
}
8490

8591
connectedCallback(): void {
8692
super.connectedCallback();
93+
this.applyHostPosition();
8794
window.addEventListener("live2d:send-message", this.onMessage);
8895
window.addEventListener("live2d:stream-message-start", this.onStreamStart);
8996
window.addEventListener("live2d:stream-message", this.onStreamMessage);
9097
window.addEventListener("live2d:stream-message-stop", this.onStreamStop);
98+
window.addEventListener("live2d:model-layout", this.onModelLayout);
9199
}
92100

93101
disconnectedCallback(): void {
@@ -99,6 +107,7 @@ export class Live2dTips extends UnoLitElement {
99107
);
100108
window.removeEventListener("live2d:stream-message", this.onStreamMessage);
101109
window.removeEventListener("live2d:stream-message-stop", this.onStreamStop);
110+
window.removeEventListener("live2d:model-layout", this.onModelLayout);
102111
}
103112

104113
handleMessage(e: SendMessageEvent): void {
@@ -190,6 +199,18 @@ export class Live2dTips extends UnoLitElement {
190199
this.isStreamMode = false;
191200
}, showTimeout);
192201
}
202+
203+
handleModelLayout(e: ModelLayoutEvent): void {
204+
const { topY, canvasHeight } = e.detail;
205+
this._bottomOffset = Math.round(
206+
canvasHeight - topY - Live2dTips.MODEL_OVERLAP_OFFSET,
207+
);
208+
this.applyHostPosition();
209+
}
210+
211+
private applyHostPosition(): void {
212+
this.style.bottom = `${this._bottomOffset}px`;
213+
}
193214
}
194215

195216
customElements.define("live2d-tips", Live2dTips);

packages/live2d/src/components/Live2dWidget.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,9 @@ export class Live2dWidget extends UnoLitElement {
7272
<div
7373
class="group flex flex-col items-center relative translate-y-1 transition-transform duration-300 hover:translate-y-0"
7474
>
75-
<live2d-tips class="-mb-10"></live2d-tips>
75+
<live2d-tips
76+
class="pointer-events-none absolute left-1/2 -translate-x-1/2"
77+
></live2d-tips>
7678
<live2d-canvas
7779
class="inline-block h-[300px] w-[300px] z-1"
7880
></live2d-canvas>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Live2dEvent } from "@/live2d/events/types";
2+
3+
export const MODEL_LAYOUT_EVENT_NAME = "live2d:model-layout" as const;
4+
5+
declare global {
6+
interface GlobalEventHandlersEventMap {
7+
[MODEL_LAYOUT_EVENT_NAME]: ModelLayoutEvent;
8+
}
9+
}
10+
11+
export interface ModelLayoutEventDetail {
12+
topY: number;
13+
canvasHeight: number;
14+
}
15+
16+
export class ModelLayoutEvent extends Live2dEvent<ModelLayoutEventDetail> {
17+
constructor(detail: ModelLayoutEventDetail) {
18+
super(MODEL_LAYOUT_EVENT_NAME, detail);
19+
}
20+
}

packages/live2d/src/live2d/model.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Live2dConfig } from "@/live2d/context/config-context";
2+
import { ModelLayoutEvent } from "@/live2d/events/model-layout";
23
import { loadMergedTips } from "@/live2d/helpers/loadMergedTips";
34
import { sendMessage } from "@/live2d/helpers/sendMessage";
45
import { logConsoleStatus } from "@/live2d/live2d/console-status";
@@ -36,6 +37,9 @@ const MESSAGE_TIMEOUT_MS = 4000;
3637
const LOADING_MESSAGE_DELAY_MS = 1200;
3738
const DEFAULT_LOADING_MESSAGE =
3839
"稍等一下下哦,人家正在梳理小裙摆,马上就来陪你啦~";
40+
const HEAD_HIT_AREA_PATTERN = /(head|flickhead)/i;
41+
const SPEECH_ANCHOR_MIN_RATIO = 0.04;
42+
const SPEECH_ANCHOR_MAX_RATIO = 0.14;
3943

4044
interface LoadModelOptions {
4145
loadSequence?: number;
@@ -174,6 +178,13 @@ class Model {
174178
app.screen.width / 2,
175179
app.screen.height * LIVE2D_BOTTOM_OFFSET,
176180
);
181+
const modelTopY = this.getSpeechAnchorTopY(nextModel, bounds);
182+
window.dispatchEvent(
183+
new ModelLayoutEvent({
184+
topY: modelTopY,
185+
canvasHeight: app.screen.height,
186+
}),
187+
);
177188

178189
if (this.#currentModel) {
179190
app.stage.removeChild(this.#currentModel);
@@ -185,6 +196,37 @@ class Model {
185196
this.#currentModel = nextModel;
186197
}
187198

199+
private getSpeechAnchorTopY(
200+
model: Live2DModel,
201+
bounds: { y: number; height: number },
202+
): number {
203+
const anchorTop = this.getHeadTopY(model);
204+
const minAnchorTop = bounds.y + bounds.height * SPEECH_ANCHOR_MIN_RATIO;
205+
const maxAnchorTop = bounds.y + bounds.height * SPEECH_ANCHOR_MAX_RATIO;
206+
const preferredAnchorTop = anchorTop ?? bounds.y;
207+
const localTopY = Math.min(
208+
Math.max(preferredAnchorTop, minAnchorTop),
209+
maxAnchorTop,
210+
);
211+
return model.position.y + (localTopY - model.pivot.y) * model.scale.y;
212+
}
213+
214+
private getHeadTopY(model: Live2DModel): number | undefined {
215+
const headHitArea = Object.values(model.internalModel.hitAreas).find(
216+
({ name }) => HEAD_HIT_AREA_PATTERN.test(name),
217+
);
218+
if (!headHitArea) {
219+
return;
220+
}
221+
222+
const headBounds = model.internalModel.getDrawableBounds(headHitArea.index);
223+
if (!Number.isFinite(headBounds.y) || headBounds.height <= 0) {
224+
return;
225+
}
226+
227+
return headBounds.y;
228+
}
229+
188230
/**
189231
* 为 Live2d 加载模型。
190232
*
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"root":["./src/demo.tsx","./src/env.d.ts","./src/halo-config.ts","./src/halo.ts","./src/index.ts","./src/api/chat-api.ts","./src/common/unolitelement.ts","./src/components/live2dcanvas.tsx","./src/components/live2dchatwindow.tsx","./src/components/live2dcontext.tsx","./src/components/live2dtips.tsx","./src/components/live2dtoggle.tsx","./src/components/live2dtools.tsx","./src/components/live2dwidget.tsx","./src/config/default-config.ts","./src/config/normalize-config.ts","./src/config/normalize-helpers.ts","./src/config/custom-tools/normalize-custom-tools.ts","./src/context/config-context.ts","./src/events/add-default-message.ts","./src/events/before-init.ts","./src/events/index.ts","./src/events/model-ready.ts","./src/events/send-message.ts","./src/events/stream-message.ts","./src/events/tip-events.ts","./src/events/toggle-canvas.ts","./src/events/toggle-chat-window.ts","./src/events/types.ts","./src/helpers/createstreammessage.ts","./src/helpers/datewithinrange.ts","./src/helpers/getplugintips.ts","./src/helpers/loadfulltipsresource.ts","./src/helpers/loadmergedtips.ts","./src/helpers/loadtipsresource.ts","./src/helpers/mergetips.ts","./src/helpers/sendmessage.ts","./src/helpers/timewithinrange.ts","./src/helpers/widgetdrawer.ts","./src/helpers/widgetvisibility.ts","./src/live2d/console-status.ts","./src/live2d/model.ts","./src/live2d/runtime.ts","./src/live2d/tools/ai-chat.ts","./src/live2d/tools/asteroids.ts","./src/live2d/tools/custom-tool-config.ts","./src/live2d/tools/custom-tool.ts","./src/live2d/tools/exit.ts","./src/live2d/tools/hitokoto.ts","./src/live2d/tools/index.ts","./src/live2d/tools/info.ts","./src/live2d/tools/screenshot.ts","./src/live2d/tools/switch-model.ts","./src/live2d/tools/switch-texture.ts","./src/live2d/tools/tools.ts","./src/live2d/tools/custom-tool-actions/actions.generated.ts","./src/live2d/tools/custom-tool-actions/index.ts","./src/live2d/tools/custom-tool-actions/types.ts","./src/live2d/tools/custom-tool-actions/actions/emit-event.ts","./src/live2d/tools/custom-tool-actions/actions/load-model.ts","./src/live2d/tools/custom-tool-actions/actions/open-url.ts","./src/live2d/tools/custom-tool-actions/actions/screenshot.ts","./src/live2d/tools/custom-tool-actions/actions/send-message.ts","./src/live2d/tools/custom-tool-actions/actions/switch-model.ts","./src/live2d/tools/custom-tool-actions/actions/switch-texture.ts","./src/live2d/tools/custom-tool-actions/actions/toggle-chat.ts","./src/live2d/tools/custom-tool-actions/actions/widget-visibility.ts","./src/styles/unocss.global.css.d.ts","./src/types/assets.d.ts","./src/utils/distinctarray.ts","./src/utils/isnotempty.ts","./src/utils/isstring.ts","./src/utils/randomselection.ts","./src/utils/unomixin.ts","./src/utils/util.ts"],"version":"5.7.3"}
1+
{"root":["./src/demo.tsx","./src/env.d.ts","./src/halo-config.ts","./src/halo.ts","./src/index.ts","./src/api/chat-api.ts","./src/common/unolitelement.ts","./src/components/live2dcanvas.tsx","./src/components/live2dchatwindow.tsx","./src/components/live2dcontext.tsx","./src/components/live2dtips.tsx","./src/components/live2dtoggle.tsx","./src/components/live2dtools.tsx","./src/components/live2dwidget.tsx","./src/config/default-config.ts","./src/config/normalize-config.ts","./src/config/normalize-helpers.ts","./src/config/custom-tools/normalize-custom-tools.ts","./src/context/config-context.ts","./src/events/add-default-message.ts","./src/events/before-init.ts","./src/events/index.ts","./src/events/model-layout.ts","./src/events/model-ready.ts","./src/events/send-message.ts","./src/events/stream-message.ts","./src/events/tip-events.ts","./src/events/toggle-canvas.ts","./src/events/toggle-chat-window.ts","./src/events/types.ts","./src/helpers/createstreammessage.ts","./src/helpers/datewithinrange.ts","./src/helpers/getplugintips.ts","./src/helpers/loadfulltipsresource.ts","./src/helpers/loadmergedtips.ts","./src/helpers/loadtipsresource.ts","./src/helpers/mergetips.ts","./src/helpers/sendmessage.ts","./src/helpers/timewithinrange.ts","./src/helpers/widgetdrawer.ts","./src/helpers/widgetvisibility.ts","./src/live2d/console-status.ts","./src/live2d/model.ts","./src/live2d/runtime.ts","./src/live2d/tools/ai-chat.ts","./src/live2d/tools/asteroids.ts","./src/live2d/tools/custom-tool-config.ts","./src/live2d/tools/custom-tool.ts","./src/live2d/tools/exit.ts","./src/live2d/tools/hitokoto.ts","./src/live2d/tools/index.ts","./src/live2d/tools/info.ts","./src/live2d/tools/screenshot.ts","./src/live2d/tools/switch-model.ts","./src/live2d/tools/switch-texture.ts","./src/live2d/tools/tools.ts","./src/live2d/tools/custom-tool-actions/actions.generated.ts","./src/live2d/tools/custom-tool-actions/index.ts","./src/live2d/tools/custom-tool-actions/types.ts","./src/live2d/tools/custom-tool-actions/actions/emit-event.ts","./src/live2d/tools/custom-tool-actions/actions/load-model.ts","./src/live2d/tools/custom-tool-actions/actions/open-url.ts","./src/live2d/tools/custom-tool-actions/actions/screenshot.ts","./src/live2d/tools/custom-tool-actions/actions/send-message.ts","./src/live2d/tools/custom-tool-actions/actions/switch-model.ts","./src/live2d/tools/custom-tool-actions/actions/switch-texture.ts","./src/live2d/tools/custom-tool-actions/actions/toggle-chat.ts","./src/live2d/tools/custom-tool-actions/actions/widget-visibility.ts","./src/styles/unocss.global.css.d.ts","./src/types/assets.d.ts","./src/utils/distinctarray.ts","./src/utils/isnotempty.ts","./src/utils/isstring.ts","./src/utils/randomselection.ts","./src/utils/unomixin.ts","./src/utils/util.ts"],"version":"5.7.3"}

0 commit comments

Comments
 (0)