| ---
|
| import MessageBox from "@/components/common/PioMessageBox.astro";
|
| import type { Live2DModelConfig } from "@/types/config";
|
| import { url } from "@/utils/url-utils";
|
|
|
| interface Props {
|
| config: Live2DModelConfig;
|
| }
|
|
|
| const { config } = Astro.props;
|
|
|
|
|
| const position = config.position || {
|
| corner: "bottom-right" as const,
|
| offsetX: 20,
|
| offsetY: 20,
|
| };
|
| const size = config.size || { width: 280, height: 250 };
|
| ---
|
|
|
| <div
|
| id="live2d-widget"
|
| class="live2d-widget"
|
| style={`
|
| width: ${size.width}px;
|
| height: ${size.height}px;
|
| ${position.corner?.includes("right") ? "right" : "left"}: ${position.offsetX}px;
|
| ${position.corner?.includes("top") ? "top" : "bottom"}: ${position.offsetY}px;
|
| `}
|
| >
|
| <canvas id="live2d-canvas" width={size.width} height={size.height}></canvas>
|
| </div>
|
|
|
| <!-- 引入消息框组件 -->
|
| <MessageBox />
|
|
|
| <script is:inline define:vars={{ config, modelPathUrl: url(config.model.path), sdkPath: url("/pio/static/live2d-sdk/live2d.min.js") }}>
|
| let motionGroups = {};
|
| let hitAreas = {};
|
| let currentMotionGroup = "idle";
|
| let currentMotionIndex = 0;
|
|
|
|
|
| function initLive2D() {
|
| if (!window.loadlive2d) {
|
| console.error("loadlive2d function not available");
|
| return;
|
| }
|
|
|
| if (!config.model || !config.model.path) {
|
| console.error("No model path configured");
|
| return;
|
| }
|
|
|
| const modelPath = modelPathUrl;
|
|
|
|
|
| fetch(modelPath)
|
| .then((response) => response.json())
|
| .then((data) => {
|
| motionGroups = data.motions || {};
|
| hitAreas = data.hit_areas_custom || data.hit_areas || {};
|
|
|
| console.log("Loaded model data:", {
|
| motionGroups: Object.keys(motionGroups),
|
| hitAreas: Object.keys(hitAreas),
|
| });
|
|
|
|
|
| window.loadlive2d("live2d-canvas", modelPath);
|
|
|
|
|
| setTimeout(() => {
|
| setupInteractions();
|
| }, 2000);
|
| })
|
| .catch((error) => {
|
| console.error("Failed to load model data:", error);
|
| });
|
| }
|
|
|
|
|
| function setupInteractions() {
|
| const canvas = document.getElementById("live2d-canvas");
|
| const container = document.getElementById("live2d-widget");
|
| if (!canvas || !container) return;
|
|
|
| canvas.addEventListener("click", handleClick);
|
|
|
|
|
| let isDragging = false;
|
| let dragStart = { x: 0, y: 0 };
|
| let containerStart = { x: 0, y: 0 };
|
|
|
|
|
| container.addEventListener("mousedown", (e) => {
|
| if (e.button !== 0) return;
|
| isDragging = true;
|
| dragStart = { x: e.clientX, y: e.clientY };
|
|
|
| const rect = container.getBoundingClientRect();
|
| containerStart = { x: rect.left, y: rect.top };
|
|
|
| container.style.cursor = "grabbing";
|
| e.preventDefault();
|
| });
|
|
|
| document.addEventListener("mousemove", (e) => {
|
| if (!isDragging) return;
|
|
|
| const deltaX = e.clientX - dragStart.x;
|
| const deltaY = e.clientY - dragStart.y;
|
|
|
| const newX = containerStart.x + deltaX;
|
| const newY = containerStart.y + deltaY;
|
|
|
|
|
| const maxX = window.innerWidth - container.offsetWidth;
|
| const maxY = window.innerHeight - container.offsetHeight;
|
|
|
| const clampedX = Math.max(0, Math.min(newX, maxX));
|
| const clampedY = Math.max(0, Math.min(newY, maxY));
|
|
|
| container.style.left = clampedX + "px";
|
| container.style.right = "auto";
|
| container.style.top = clampedY + "px";
|
| container.style.bottom = "auto";
|
| });
|
|
|
| document.addEventListener("mouseup", () => {
|
| if (isDragging) {
|
| isDragging = false;
|
| container.style.cursor = "grab";
|
| }
|
| });
|
|
|
|
|
| container.addEventListener("touchstart", (e) => {
|
| if (e.touches.length !== 1) return;
|
| isDragging = true;
|
| const touch = e.touches[0];
|
| dragStart = { x: touch.clientX, y: touch.clientY };
|
|
|
| const rect = container.getBoundingClientRect();
|
| containerStart = { x: rect.left, y: rect.top };
|
|
|
| e.preventDefault();
|
| });
|
|
|
| document.addEventListener("touchmove", (e) => {
|
| if (!isDragging || e.touches.length !== 1) return;
|
|
|
| const touch = e.touches[0];
|
| const deltaX = touch.clientX - dragStart.x;
|
| const deltaY = touch.clientY - dragStart.y;
|
|
|
| const newX = containerStart.x + deltaX;
|
| const newY = containerStart.y + deltaY;
|
|
|
|
|
| const maxX = window.innerWidth - container.offsetWidth;
|
| const maxY = window.innerHeight - container.offsetHeight;
|
|
|
| const clampedX = Math.max(0, Math.min(newX, maxX));
|
| const clampedY = Math.max(0, Math.min(newY, maxY));
|
|
|
| container.style.left = clampedX + "px";
|
| container.style.right = "auto";
|
| container.style.top = clampedY + "px";
|
| container.style.bottom = "auto";
|
|
|
| e.preventDefault();
|
| });
|
|
|
| document.addEventListener("touchend", () => {
|
| isDragging = false;
|
| });
|
|
|
|
|
| container.style.cursor = "grab";
|
|
|
|
|
| window.addEventListener("resize", () => {
|
| const rect = container.getBoundingClientRect();
|
| const maxX = window.innerWidth - container.offsetWidth;
|
| const maxY = window.innerHeight - container.offsetHeight;
|
|
|
| if (rect.left > maxX) {
|
| container.style.left = maxX + "px";
|
| container.style.right = "auto";
|
| }
|
| if (rect.top > maxY) {
|
| container.style.top = maxY + "px";
|
| container.style.bottom = "auto";
|
| }
|
| });
|
|
|
| console.log("Live2D interactions and drag functionality setup complete");
|
| }
|
|
|
|
|
| function handleClick(event) {
|
| if (!motionGroups || Object.keys(motionGroups).length === 0) {
|
| console.log("No motion groups available");
|
| return;
|
| }
|
|
|
| const rect = event.target.getBoundingClientRect();
|
| const x = event.clientX - rect.left;
|
| const y = event.clientY - rect.top;
|
|
|
|
|
| const normalizedX = (x / rect.width) * 2 - 1;
|
| const normalizedY = -((y / rect.height) * 2 - 1);
|
|
|
| console.log("Click at:", { x: normalizedX, y: normalizedY });
|
|
|
|
|
| let motionGroup = "tap_body";
|
|
|
|
|
| if (hitAreas.head_x && hitAreas.head_y) {
|
| const [headXMin, headXMax] = hitAreas.head_x;
|
| const [headYMin, headYMax] = hitAreas.head_y;
|
|
|
| if (
|
| normalizedX >= headXMin &&
|
| normalizedX <= headXMax &&
|
| normalizedY >= headYMin &&
|
| normalizedY <= headYMax
|
| ) {
|
| motionGroup = "flick_head";
|
| console.log("Head area clicked - playing flick_head motion");
|
| }
|
| }
|
|
|
|
|
| if (motionGroup === "tap_body" && hitAreas.body_x && hitAreas.body_y) {
|
| const [bodyXMin, bodyXMax] = hitAreas.body_x;
|
| const [bodyYMin, bodyYMax] = hitAreas.body_y;
|
|
|
| if (
|
| normalizedX >= bodyXMin &&
|
| normalizedX <= bodyXMax &&
|
| normalizedY >= bodyYMin &&
|
| normalizedY <= bodyYMax
|
| ) {
|
| console.log("Body area clicked - playing tap_body motion");
|
| }
|
| }
|
|
|
|
|
| playMotion(motionGroup);
|
|
|
|
|
| showMessage();
|
| }
|
|
|
|
|
|
|
|
|
| function showMessage() {
|
| const messages = config.interactive?.clickMessages || [
|
| "你好!伊利雅~",
|
| "有什么需要帮助的吗?",
|
| "今天天气真不错呢!",
|
| "要不要一起玩游戏?",
|
| "记得按时休息哦!",
|
| ];
|
|
|
| const randomMessage = messages[Math.floor(Math.random() * messages.length)];
|
|
|
|
|
| if (window.showModelMessage) {
|
| window.showModelMessage(randomMessage, {
|
| containerId: "live2d-widget",
|
| displayTime: config.interactive?.messageDisplayTime || 3000
|
| });
|
| }
|
| }
|
|
|
|
|
| function playMotion(groupName) {
|
| if (!motionGroups[groupName] || motionGroups[groupName].length === 0) {
|
| console.log(`No motions available for group: ${groupName}`);
|
|
|
| const availableGroups = Object.keys(motionGroups).filter(
|
| (key) => motionGroups[key].length > 0
|
| );
|
| if (availableGroups.length > 0) {
|
| groupName = availableGroups[0];
|
| console.log(`Using fallback group: ${groupName}`);
|
| } else {
|
| return;
|
| }
|
| }
|
|
|
| const motions = motionGroups[groupName];
|
| let motionIndex;
|
|
|
| if (groupName === currentMotionGroup) {
|
|
|
| currentMotionIndex = (currentMotionIndex + 1) % motions.length;
|
| motionIndex = currentMotionIndex;
|
| } else {
|
|
|
| motionIndex = Math.floor(Math.random() * motions.length);
|
| currentMotionIndex = motionIndex;
|
| }
|
|
|
| currentMotionGroup = groupName;
|
|
|
| console.log(`Playing motion ${motionIndex} from group ${groupName}`);
|
|
|
|
|
|
|
| const canvas = document.getElementById("live2d-canvas");
|
| if (canvas && window.loadlive2d) {
|
|
|
| canvas.dataset.currentMotionGroup = groupName;
|
| canvas.dataset.currentMotionIndex = motionIndex;
|
|
|
|
|
| canvas.style.transform = "scale(1.05)";
|
| setTimeout(() => {
|
| canvas.style.transform = "scale(1)";
|
| }, 150);
|
| }
|
| }
|
|
|
|
|
| function loadLive2DSDK() {
|
|
|
| if (
|
| config.responsive?.hideOnMobile &&
|
| window.innerWidth <= (config.responsive.mobileBreakpoint || 768)
|
| ) {
|
| console.log("📱 Mobile device detected, skipping Live2D model initialization");
|
| const widget = document.getElementById("live2d-widget");
|
| if (widget) widget.style.display = "none";
|
| return;
|
| }
|
|
|
|
|
| if (window.loadlive2d) {
|
| initLive2D();
|
| return;
|
| }
|
|
|
|
|
| const script = document.createElement("script");
|
| script.src = sdkPath;
|
| script.onload = () => {
|
|
|
| setTimeout(() => {
|
| if (window.loadlive2d) {
|
| initLive2D();
|
| } else {
|
| console.error("loadlive2d function not found after loading SDK");
|
| }
|
| }, 100);
|
| };
|
| script.onerror = () => {
|
| console.error("Failed to load Live2D SDK");
|
| };
|
| document.head.appendChild(script);
|
| }
|
|
|
|
|
| function handleResponsive() {
|
| const widget = document.getElementById("live2d-widget");
|
| if (!widget) return;
|
|
|
| const responsive = config.responsive;
|
| if (responsive?.hideOnMobile) {
|
| const breakpoint = responsive.mobileBreakpoint || 768;
|
| if (window.innerWidth <= breakpoint) {
|
| widget.style.display = "none";
|
| } else {
|
| widget.style.display = "block";
|
| }
|
| }
|
| }
|
|
|
|
|
| if (document.readyState === "loading") {
|
| document.addEventListener("DOMContentLoaded", () => {
|
|
|
| if (!window.live2dModelInitialized) {
|
| loadLive2DSDK();
|
| window.live2dModelInitialized = true;
|
| }
|
| handleResponsive();
|
| });
|
| } else {
|
|
|
| if (!window.live2dModelInitialized) {
|
| loadLive2DSDK();
|
| window.live2dModelInitialized = true;
|
| }
|
| handleResponsive();
|
| }
|
|
|
|
|
| window.addEventListener("resize", handleResponsive);
|
|
|
|
|
| if (typeof window.swup !== "undefined" && window.swup.hooks) {
|
| window.swup.hooks.on("content:replace", () => {
|
|
|
| setTimeout(() => {
|
| handleResponsive();
|
| }, 100);
|
| });
|
| } else {
|
|
|
| document.addEventListener("swup:enable", () => {
|
| if (window.swup && window.swup.hooks) {
|
| window.swup.hooks.on("content:replace", () => {
|
| setTimeout(() => {
|
| handleResponsive();
|
| }, 100);
|
| });
|
| }
|
| });
|
| }
|
| </script>
|
|
|
| <style>
|
| .live2d-widget {
|
| position: fixed;
|
| z-index: 999;
|
| pointer-events: auto;
|
| cursor: grab;
|
| }
|
|
|
| #live2d-canvas {
|
| pointer-events: auto;
|
| cursor: pointer;
|
| transition: all 0.3s ease;
|
| width: 100%;
|
| height: 100%;
|
| }
|
|
|
| .live2d-widget:hover #live2d-canvas {
|
| opacity: 1;
|
| }
|
|
|
|
|
| .live2d-widget:active {
|
| cursor: grabbing;
|
| }
|
| </style>
|
|
|