Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion packages/live2d/src/components/Live2dCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
clearCurrentLive2dModel,
setCurrentLive2dModel,
} from "@/live2d/live2d/model-store";
import { DESKTOP_LIVE2D_CANVAS_SIZE } from "@/live2d/utils/responsive";
import { consume } from "@lit/context";
import { type PropertyValues, type TemplateResult, html } from "lit";
import { property, query, state } from "lit/decorators.js";
Expand All @@ -22,6 +23,9 @@ export class Live2dCanvas extends UnoLitElement {
@state()
private model: Model | null = null;

@property({ type: Number })
public canvasSize = DESKTOP_LIVE2D_CANVAS_SIZE;

@query("#live2d")
private _live2d!: HTMLCanvasElement;

Expand Down Expand Up @@ -54,6 +58,13 @@ export class Live2dCanvas extends UnoLitElement {
}
}

protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (changedProperties.has("canvasSize") && this.model) {
void this.model.resize(this.canvasSize);
}
}

getModel(): Model | null {
return this.model;
}
Expand All @@ -66,7 +77,15 @@ export class Live2dCanvas extends UnoLitElement {
this._modelInitialized = true;

try {
this.model = await Model.create(this._live2d, this.config);
const initialCanvasSize = this.canvasSize;
this.model = await Model.create(
this._live2d,
this.config,
initialCanvasSize,
);
if (initialCanvasSize !== this.canvasSize) {
await this.model.resize(this.canvasSize);
}
setCurrentLive2dModel(this.model);
window.dispatchEvent(new ModelReadyEvent({ model: this.model }));
} catch (error) {
Expand Down
14 changes: 12 additions & 2 deletions packages/live2d/src/components/Live2dTips.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ export class Live2dTips extends UnoLitElement {
@property({ attribute: false })
public config?: Live2dConfig;

@property({ type: Boolean })
public compact = false;

@state()
private _isShow = false;
@state()
Expand Down Expand Up @@ -76,7 +79,7 @@ export class Live2dTips extends UnoLitElement {
"animate-shake": true,
"animate-delay-5s": true,
"min-h-18": true,
"w-63": true,
"box-border": true,
"bg-tips": true,
border: true,
"border-tips": true,
Expand All @@ -95,8 +98,15 @@ export class Live2dTips extends UnoLitElement {
"opacity-0": !this._isShow,
"select-none": true,
};
const width = this.compact
? "min(220px, calc(100vw - 1rem))"
: "min(15.75rem, calc(100vw - 1rem))";
return html`
<div id="live2d-tips" class=${classMap(classes)}>
<div
id="live2d-tips"
class=${classMap(classes)}
style="width: ${width};"
>
${unsafeHTML(renderedMessage)}
</div>
`;
Expand Down
9 changes: 6 additions & 3 deletions packages/live2d/src/components/Live2dToggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import {
configContext,
} from "@/live2d/context/config-context";
import { ToggleCanvasEvent } from "@/live2d/events/toggle-canvas";
import { WIDGET_DRAWER_DURATION_MS } from "@/live2d/helpers/widgetDrawer";
import { getWidgetDrawerDuration } from "@/live2d/helpers/widgetDrawer";
import {
clearWidgetDismissal,
readWidgetSuppression,
rememberWidgetDismissal,
} from "@/live2d/helpers/widgetVisibility";
import { DraggableMixin } from "@/live2d/mixins/draggable";
import { isCompactViewport } from "@/live2d/utils/responsive";
import { consume } from "@lit/context";
import { type TemplateResult, html } from "lit";
import { property } from "lit/decorators.js";
Expand Down Expand Up @@ -41,7 +42,8 @@ export class Live2dToggle extends DraggableUnoLitElement {
window.addEventListener("live2d:toggle-canvas", this.handleGlobalToggle);

Promise.resolve().then(() => {
if (readWidgetSuppression(localStorage)) {
const widgetSuppressed = readWidgetSuppression(localStorage);
if (widgetSuppressed) {
this._isShow = true;
this.requestUpdate();
return;
Expand Down Expand Up @@ -71,6 +73,7 @@ export class Live2dToggle extends DraggableUnoLitElement {
role="button"
tabindex="0"
aria-label="打开看板娘"
style="bottom: calc(4rem + env(safe-area-inset-bottom, 0px));"
@keydown=${this.handleKeydown}
>
<span
Expand Down Expand Up @@ -109,7 +112,7 @@ export class Live2dToggle extends DraggableUnoLitElement {
this.revealTimer = undefined;
this._isShow = true;
this.requestUpdate();
}, WIDGET_DRAWER_DURATION_MS);
}, getWidgetDrawerDuration(isCompactViewport()));
};

private clearRevealTimer(): void {
Expand Down
165 changes: 156 additions & 9 deletions packages/live2d/src/components/Live2dTools.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { defaultToolNames, toolRegistry } from "@/live2d/live2d/tools";
import { CustomTool } from "@/live2d/live2d/tools/custom-tool";
import type { Tool } from "@/live2d/live2d/tools/tools";
import { consume } from "@lit/context";
import { type TemplateResult, html } from "lit";
import { type PropertyValues, type TemplateResult, html } from "lit";
import { property, state } from "lit/decorators.js";
import "iconify-icon";

Expand All @@ -20,34 +20,106 @@ export class Live2dTools extends UnoLitElement {
@property({ attribute: false })
public config?: Live2dConfig;

@property({ type: Boolean })
public compact = false;

private model?: Model | null;

@state()
private _tools: Tool[] = [];

@state()
private _isExpanded = false;

@state()
private _hoveredToolIndex: number | null = null;

private readonly onModelReady = (event: Event) => {
this.handleModelReady(event as ModelReadyEvent);
};

render(): TemplateResult {
if (this._tools.length === 0) {
return html``;
}

const isExpanded = !this.compact || this._isExpanded;
const shellClass = [
"flex min-w-11 flex-col items-center overflow-hidden rounded-full",
"border border-[#f3d7b8]/80 p-1",
"transition-[gap,background-color,border-color] duration-300",
isExpanded ? "gap-1" : "gap-0",
].join(" ");
const shellBackground = this.compact
? "linear-gradient(145deg, rgba(255, 250, 244, 0.86), rgba(255, 255, 255, 0.68))"
: "linear-gradient(145deg, rgba(255, 250, 244, 0.72), rgba(255, 255, 255, 0.5))";
const toolsClass = [
"relative flex w-9 flex-col items-center gap-1 overflow-x-hidden overflow-y-auto",
"overscroll-contain transition-[max-height,opacity,transform] duration-300",
isExpanded
? "pointer-events-auto translate-y-0 opacity-100"
: "pointer-events-none -translate-y-2 opacity-0",
].join(" ");
const toolsMaxHeight = isExpanded
? this.compact
? "min(10rem, calc(42vh - 3rem))"
: "calc(100vh - 4rem)"
: "0px";

return html`<div
id="live2d-tools"
class="flex flex-col gap-2 items-center justify-center"
id="live2d-tools-shell"
class=${shellClass}
style="background: ${shellBackground}; border-color: rgba(243, 215, 184, 0.82); box-shadow: 0 3px 10px rgba(139, 94, 52, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.9); backdrop-filter: blur(14px) saturate(1.15); -webkit-backdrop-filter: blur(14px) saturate(1.15);"
@keydown=${this.handleDrawerKeydown}
>
${this._tools.map(this.renderTool)}
<div
id="live2d-tools"
class=${toolsClass}
style="max-height: ${toolsMaxHeight}; scrollbar-width: thin; scrollbar-color: rgba(139, 94, 52, 0.28) transparent;"
aria-hidden=${isExpanded ? "false" : "true"}
@mouseleave=${this.clearHoveredTool}
@focusout=${this.handleToolsFocusOut}
>
${this.renderHoverIndicator()}
${this._tools.map((tool, index) =>
this.renderTool(tool, isExpanded, index),
)}
</div>
${this.renderDrawerToggle()}
</div>`;
}

renderTool(tool: Tool): TemplateResult {
return html`<span
renderTool(tool: Tool, isExpanded: boolean, index: number): TemplateResult {
const buttonClass =
"relative z-1 inline-flex h-9 w-9 flex-none cursor-pointer items-center justify-center rounded-full border-none bg-transparent p-0 color-[#7b8c9d] transition-colors duration-200 hover:color-[#0684bd] focus-visible:color-[#0684bd] focus-visible:outline-2 focus-visible:outline-[#ffb86c] focus-visible:outline-offset-[-2px]";
return html`<button
id="live2d-tool-${tool.name()}"
@click=${() => void tool.triggerExecute()}
type="button"
class=${buttonClass}
title=${tool.name()}
aria-label=${tool.name()}
tabindex=${isExpanded ? "0" : "-1"}
@mouseenter=${() => {
this._hoveredToolIndex = index;
}}
@focus=${() => {
this._hoveredToolIndex = index;
}}
@click=${() => this.executeTool(tool)}
>
<iconify-icon
class="inline-block w-4 h-4 text-size-xl cursor-pointer color-#7b8c9d hover:color-#0684bd"
class="inline-block h-5 w-5 text-size-xl"
icon="${tool.icon()}"
></iconify-icon>
</span>`;
</button>`;
}

protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (changedProperties.has("compact") && this.compact) {
this._isExpanded = false;
this._hoveredToolIndex = null;
}
}

connectedCallback(): void {
Expand All @@ -74,6 +146,81 @@ export class Live2dTools extends UnoLitElement {
}
}

private renderDrawerToggle(): TemplateResult | undefined {
if (!this.compact) {
return;
}

return html`<button
id="live2d-tools-toggle"
type="button"
class="inline-flex h-9 w-9 flex-none cursor-pointer items-center justify-center rounded-full border-none bg-[#ffb86c] p-0 color-white shadow-[inset_0_1px_0_rgba(255,255,255,0.38)] transition-colors duration-200 hover:bg-[#ffa952] focus-visible:outline-2 focus-visible:outline-[#8b5e34] focus-visible:outline-offset-2"
aria-controls="live2d-tools"
aria-expanded=${this._isExpanded ? "true" : "false"}
aria-label=${this._isExpanded ? "收起工具箱" : "展开工具箱"}
title=${this._isExpanded ? "收起工具箱" : "展开工具箱"}
@click=${() => {
this._isExpanded = !this._isExpanded;
}}
>
<iconify-icon
icon=${this._isExpanded ? "ph:caret-down-bold" : "ph:dots-nine-bold"}
width="20"
height="20"
></iconify-icon>
</button>`;
}

private readonly handleDrawerKeydown = (event: KeyboardEvent): void => {
if (!this.compact || event.key !== "Escape" || !this._isExpanded) {
return;
}
event.preventDefault();
this._isExpanded = false;
this.updateComplete.then(() => {
this.renderRoot
.querySelector<HTMLButtonElement>("#live2d-tools-toggle")
?.focus();
});
};

private renderHoverIndicator(): TemplateResult {
const offset = (this._hoveredToolIndex ?? 0) * 40;
const isVisible = this._hoveredToolIndex !== null;
return html`<span
id="live2d-tools-hover-indicator"
class="pointer-events-none absolute left-0 top-0 h-9 w-9 box-border rounded-full border border-white/90"
style="opacity: ${
isVisible ? "1" : "0"
}; transform: translate3d(0, ${offset}px, 0); background: rgba(255, 255, 255, 0.85); box-shadow: 0 1px 5px rgba(139, 94, 52, 0.09), inset 0 1px 0 rgba(255, 255, 255, 0.92); transition: transform 220ms cubic-bezier(0.22, 1, 0.36, 1), opacity 140ms ease;"
aria-hidden="true"
></span>`;
}

private readonly clearHoveredTool = (): void => {
this._hoveredToolIndex = null;
};

private readonly handleToolsFocusOut = (event: FocusEvent): void => {
const nextTarget = event.relatedTarget;
if (
nextTarget instanceof Node &&
event.currentTarget instanceof HTMLElement &&
event.currentTarget.contains(nextTarget)
) {
return;
}
this._hoveredToolIndex = null;
};

private executeTool(tool: Tool): void {
this._hoveredToolIndex = null;
if (this.compact) {
this._isExpanded = false;
}
void tool.triggerExecute();
}

private initializeTools(): void {
const presetToolsList = this.getPresetTools();
const customTools = this.getCustomTools();
Expand Down
Loading
Loading