-
+
+ ${this.renderLive2dTools()}
`;
}
diff --git a/packages/live2d/src/context/config-context.ts b/packages/live2d/src/context/config-context.ts
index 8720415..f75eb62 100644
--- a/packages/live2d/src/context/config-context.ts
+++ b/packages/live2d/src/context/config-context.ts
@@ -37,15 +37,40 @@ export interface TipConfig {
message: TipMessage;
}
-export interface Live2dConfig {
+export interface Live2dToolsConfig {
+ // 是否显示右侧工具栏
+ isTools?: boolean;
+ // openai 图标
+ openaiIcon?: string;
+ // 一言图标
+ hitokotoIcon?: string;
+ // 一言API
+ hitokotoApi?: string;
+ // 小宇宙游戏图标
+ asteroidsIcon?: string;
+ // 切换模型图标
+ switchModelIcon?: string;
+ // 切换纹理图标
+ switchTextureIcon?: string;
+ // 截图生成的图片名称
+ screenshotName?: string;
+ // 截图图标
+ screenshotIcon?: string;
+ // 信息图标
+ infoIcon?: string;
+ // 信息站点地址
+ infoSite?: string;
+ // 退出 Live2d 图标
+ exitIcon?: string;
+}
+
+export interface Live2dConfig extends Live2dToolsConfig {
// Live2d API 路径
apiPath: string;
// Live2d 定位
live2dLocation: "left" | "right";
// 是否在控制台显示状态
consoleShowStatus?: boolean;
- // 是否显示右侧工具栏
- isTools?: boolean;
// 是否强制使用默认配置
isForceUseDefaultConfig?: boolean;
// 模型编号
diff --git a/packages/live2d/src/libs/asteroids.min.js b/packages/live2d/src/libs/asteroids.min.js
new file mode 100644
index 0000000..78fc3ca
--- /dev/null
+++ b/packages/live2d/src/libs/asteroids.min.js
@@ -0,0 +1,640 @@
+// http://www.websiteasteroids.com
+function Asteroids() {
+ if (!window.ASTEROIDS) window.ASTEROIDS = {
+ enemiesKilled: 0
+ };
+ class Vector {
+ constructor(x, y) {
+ if (typeof x === "Object") {
+ this.x = x.x;
+ this.y = x.y;
+ } else {
+ this.x = x;
+ this.y = y;
+ }
+ }
+ cp() {
+ return new Vector(this.x, this.y);
+ }
+ mul(factor) {
+ this.x *= factor;
+ this.y *= factor;
+ return this;
+ }
+ mulNew(factor) {
+ return new Vector(this.x * factor, this.y * factor);
+ }
+ add(vec) {
+ this.x += vec.x;
+ this.y += vec.y;
+ return this;
+ }
+ addNew(vec) {
+ return new Vector(this.x + vec.x, this.y + vec.y);
+ }
+ sub(vec) {
+ this.x -= vec.x;
+ this.y -= vec.y;
+ return this;
+ }
+ subNew(vec) {
+ return new Vector(this.x - vec.x, this.y - vec.y);
+ }
+ rotate(angle) {
+ const x = this.x, y = this.y;
+ this.x = x * Math.cos(angle) - Math.sin(angle) * y;
+ this.y = x * Math.sin(angle) + Math.cos(angle) * y;
+ return this;
+ }
+ rotateNew(angle) {
+ return this.cp().rotate(angle);
+ }
+ setAngle(angle) {
+ const l = this.len();
+ this.x = Math.cos(angle) * l;
+ this.y = Math.sin(angle) * l;
+ return this;
+ }
+ setAngleNew(angle) {
+ return this.cp().setAngle(angle);
+ }
+ setLength(length) {
+ const l = this.len();
+ if (l) this.mul(length / l);
+ else this.x = this.y = length;
+ return this;
+ }
+ setLengthNew(length) {
+ return this.cp().setLength(length);
+ }
+ normalize() {
+ const l = this.len();
+ this.x /= l;
+ this.y /= l;
+ return this;
+ }
+ normalizeNew() {
+ return this.cp().normalize();
+ }
+ angle() {
+ return Math.atan2(this.y, this.x);
+ }
+ collidesWith(rect) {
+ return this.x > rect.x && this.y > rect.y && this.x < rect.x + rect.width && this.y < rect.y + rect.height;
+ }
+ len() {
+ const l = Math.sqrt(this.x * this.x + this.y * this.y);
+ if (l < 0.005 && l > -0.005) return 0;
+ return l;
+ }
+ is(test) {
+ return typeof test === "object" && this.x === test.x && this.y === test.y;
+ }
+ toString() {
+ return "[Vector(" + this.x + ", " + this.y + ") angle: " + this.angle() + ", length: " + this.len() + "]";
+ }
+ }
+
+ class Line {
+ constructor(p1, p2) {
+ this.p1 = p1;
+ this.p2 = p2;
+ }
+ shift(pos) {
+ this.p1.add(pos);
+ this.p2.add(pos);
+ }
+ intersectsWithRect(rect) {
+ const LL = new Vector(rect.x, rect.y + rect.height);
+ const UL = new Vector(rect.x, rect.y);
+ const LR = new Vector(rect.x + rect.width, rect.y + rect.height);
+ const UR = new Vector(rect.x + rect.width, rect.y);
+ if (this.p1.x > LL.x && this.p1.x < UR.x && this.p1.y < LL.y && this.p1.y > UR.y && this.p2.x > LL.x && this.p2.x < UR.x && this.p2.y < LL.y && this.p2.y > UR.y) return true;
+ if (this.intersectsLine(new Line(UL, LL))) return true;
+ if (this.intersectsLine(new Line(LL, LR))) return true;
+ if (this.intersectsLine(new Line(UL, UR))) return true;
+ if (this.intersectsLine(new Line(UR, LR))) return true;
+ return false;
+ }
+ intersectsLine(line2) {
+ const v1 = this.p1, v2 = this.p2;
+ const v3 = line2.p1, v4 = line2.p2;
+ const denom = ((v4.y - v3.y) * (v2.x - v1.x)) - ((v4.x - v3.x) * (v2.y - v1.y));
+ const numerator = ((v4.x - v3.x) * (v1.y - v3.y)) - ((v4.y - v3.y) * (v1.x - v3.x));
+ const numerator2 = ((v2.x - v1.x) * (v1.y - v3.y)) - ((v2.y - v1.y) * (v1.x - v3.x));
+ if (denom === 0.0) {
+ return false;
+ }
+ const ua = numerator / denom;
+ const ub = numerator2 / denom;
+ return (ua >= 0.0 && ua <= 1.0 && ub >= 0.0 && ub <= 1.0);
+ }
+ }
+ const that = this;
+ const isIE = !! window.ActiveXObject;
+ let w = document.documentElement.clientWidth, h = document.documentElement.clientHeight;
+ const playerWidth = 20, playerHeight = 30;
+ const playerVerts = [
+ [-1 * playerHeight / 2, -1 * playerWidth / 2],
+ [-1 * playerHeight / 2, playerWidth / 2],
+ [playerHeight / 2, 0]
+ ];
+ const ignoredTypes = ["HTML", "HEAD", "BODY", "SCRIPT", "TITLE", "META", "STYLE", "LINK", "SHAPE", "LINE", "GROUP", "IMAGE", "STROKE", "FILL", "SKEW", "PATH", "TEXTPATH"];
+ const hiddenTypes = ["BR", "HR"];
+ const FPS = 50;
+ const acc = 300;
+ const maxSpeed = 600;
+ const rotSpeed = 360;
+ const bulletSpeed = 700;
+ const particleSpeed = 400;
+ const timeBetweenFire = 150;
+ const timeBetweenBlink = 250;
+ const bulletRadius = 2;
+ const maxParticles = isIE ? 20 : 40;
+ const maxBullets = isIE ? 10 : 20;
+ this.flame = {
+ r: [],
+ y: []
+ };
+ this.toggleBlinkStyle = function() {
+ if (this.updated.blink.isActive) {
+ document.body.classList.remove("ASTEROIDSBLINK");
+ } else {
+ document.body.classList.add("ASTEROIDSBLINK");
+ }
+ this.updated.blink.isActive = !this.updated.blink.isActive;
+ };
+ addStylesheet(".ASTEROIDSBLINK .ASTEROIDSYEAHENEMY", "outline: 2px dotted red;");
+ this.pos = new Vector(100, 100);
+ this.lastPos = false;
+ this.vel = new Vector(0, 0);
+ this.dir = new Vector(0, 1);
+ this.keysPressed = {};
+ this.firedAt = false;
+ this.updated = {
+ enemies: false,
+ flame: new Date().getTime(),
+ blink: {
+ time: 0,
+ isActive: false
+ }
+ };
+ this.scrollPos = new Vector(0, 0);
+ this.bullets = [];
+ this.enemies = [];
+ this.dying = [];
+ this.totalEnemies = 0;
+ this.particles = [];
+
+ function updateEnemyIndex() {
+ for (let enemy of that.enemies) {
+ enemy.classList.remove("ASTEROIDSYEAHENEMY");
+ }
+ const all = document.body.getElementsByTagName("*");
+ that.enemies = [];
+ for (let i = 0, el; el = all[i]; i++) {
+ if (!(ignoredTypes.includes(el.tagName.toUpperCase())) && el.prefix !== "g_vml_" && hasOnlyTextualChildren(el) && el.className !== "ASTEROIDSYEAH" && el.offsetHeight > 0) {
+ el.aSize = size(el);
+ that.enemies.push(el);
+ el.classList.add("ASTEROIDSYEAHENEMY");
+ if (!el.aAdded) {
+ el.aAdded = true;
+ that.totalEnemies++;
+ }
+ }
+ }
+ };
+ updateEnemyIndex();
+ let createFlames;
+ (function() {
+ const rWidth = playerWidth, rIncrease = playerWidth * 0.1, yWidth = playerWidth * 0.6, yIncrease = yWidth * 0.2, halfR = rWidth / 2, halfY = yWidth / 2, halfPlayerHeight = playerHeight / 2;
+ createFlames = function() {
+ that.flame.r = [
+ [-1 * halfPlayerHeight, -1 * halfR]
+ ];
+ that.flame.y = [
+ [-1 * halfPlayerHeight, -1 * halfY]
+ ];
+ for (let x = 0; x < rWidth; x += rIncrease) {
+ that.flame.r.push([-random(2, 7) - halfPlayerHeight, x - halfR]);
+ }
+ that.flame.r.push([-1 * halfPlayerHeight, halfR]);
+ for (let x = 0; x < yWidth; x += yIncrease) {
+ that.flame.y.push([-random(2, 7) - halfPlayerHeight, x - halfY]);
+ }
+ that.flame.y.push([-1 * halfPlayerHeight, halfY]);
+ };
+ })();
+ createFlames();
+
+ function radians(deg) {
+ return deg * Math.PI / 180;
+ };
+
+ function random(from, to) {
+ return Math.floor(Math.random() * (to + 1) + from);
+ };
+
+ function boundsCheck(vec) {
+ if (vec.x > w) vec.x = 0;
+ else if (vec.x < 0) vec.x = w;
+ if (vec.y > h) vec.y = 0;
+ else if (vec.y < 0) vec.y = h;
+ };
+
+ function size(element) {
+ let el = element, left = 0, top = 0;
+ do {
+ left += el.offsetLeft || 0;
+ top += el.offsetTop || 0;
+ el = el.offsetParent;
+ } while (el);
+ return {
+ x: left,
+ y: top,
+ width: element.offsetWidth || 10,
+ height: element.offsetHeight || 10
+ };
+ };
+
+ function applyVisibility(vis) {
+ for (let p of window.ASTEROIDSPLAYERS) {
+ p.gameContainer.style.visibility = vis;
+ }
+ }
+
+ function getElementFromPoint(x, y) {
+ applyVisibility("hidden");
+ let element = document.elementFromPoint(x, y);
+ if (!element) {
+ applyVisibility("visible");
+ return false;
+ }
+ if (element.nodeType === 3) element = element.parentNode;
+ applyVisibility("visible");
+ return element;
+ };
+
+ function addParticles(startPos) {
+ const time = new Date().getTime();
+ const amount = maxParticles;
+ for (let i = 0; i < amount; i++) {
+ that.particles.push({
+ dir: (new Vector(Math.random() * 20 - 10, Math.random() * 20 - 10)).normalize(),
+ pos: startPos.cp(),
+ cameAlive: time
+ });
+ }
+ };
+
+ function setScore() {
+ that.points.innerHTML = window.ASTEROIDS.enemiesKilled * 10;
+ };
+
+ function hasOnlyTextualChildren(element) {
+ if (element.offsetLeft < -100 && element.offsetWidth > 0 && element.offsetHeight > 0) return false;
+ if (hiddenTypes.includes(element.tagName)) return true;
+ if (element.offsetWidth === 0 && element.offsetHeight === 0) return false;
+ for (let i = 0; i < element.childNodes.length; i++) {
+ if (!(hiddenTypes.includes(element.childNodes[i].tagName)) && element.childNodes[i].childNodes.length !== 0) return false;
+ }
+ return true;
+ };
+
+ function addStylesheet(selector, rules) {
+ const stylesheet = document.createElement("style");
+ stylesheet.rel = "stylesheet";
+ stylesheet.id = "ASTEROIDSYEAHSTYLES";
+ try {
+ stylesheet.innerHTML = selector + "{" + rules + "}";
+ } catch (e) {
+ stylesheet.styleSheet.addRule(selector, rules);
+ }
+ document.getElementsByTagName("head")[0].appendChild(stylesheet);
+ };
+
+ function removeStylesheet(name) {
+ const stylesheet = document.getElementById(name);
+ if (stylesheet) {
+ stylesheet.parentNode.removeChild(stylesheet);
+ }
+ };
+ this.gameContainer = document.createElement("div");
+ this.gameContainer.className = "ASTEROIDSYEAH";
+ document.body.appendChild(this.gameContainer);
+ this.canvas = document.createElement("canvas");
+ this.canvas.setAttribute("width", w);
+ this.canvas.setAttribute("height", h);
+ this.canvas.className = "ASTEROIDSYEAH";
+ Object.assign(this.canvas.style, {
+ width: w + "px",
+ height: h + "px",
+ position: "fixed",
+ top: "0px",
+ left: "0px",
+ bottom: "0px",
+ right: "0px",
+ zIndex: "10000"
+ });
+ this.canvas.addEventListener("mousedown", function(e) {
+ const message = document.createElement("span");
+ message.style.position = "absolute";
+ message.style.color = "red";
+ message.innerHTML = "Press Esc to Quit";
+ document.body.appendChild(message);
+ const x = e.pageX || (e.clientX + document.documentElement.scrollLeft);
+ const y = e.pageY || (e.clientY + document.documentElement.scrollTop);
+ message.style.left = x - message.offsetWidth / 2 + "px";
+ message.style.top = y - message.offsetHeight / 2 + "px";
+ setTimeout(function() {
+ try {
+ message.parentNode.removeChild(message);
+ } catch (e) {}
+ }, 1000);
+ }, false);
+ const eventResize = function() {
+ that.canvas.style.display = "none";
+ w = document.documentElement.clientWidth;
+ h = document.documentElement.clientHeight;
+ that.canvas.setAttribute("width", w);
+ that.canvas.setAttribute("height", h);
+ Object.assign(that.canvas.style, {
+ display: "block",
+ width: w + "px",
+ height: h + "px"
+ });
+ };
+ window.addEventListener("resize", eventResize, false);
+ this.gameContainer.appendChild(this.canvas);
+ this.ctx = this.canvas.getContext("2d");
+ this.ctx.fillStyle = "black";
+ this.ctx.strokeStyle = "black";
+ if (!document.getElementById("ASTEROIDS-NAVIGATION")) {
+ this.navigation = document.createElement("div");
+ this.navigation.id = "ASTEROIDS-NAVIGATION";
+ this.navigation.className = "ASTEROIDSYEAH";
+ Object.assign(this.navigation.style, {
+ fontFamily: "Arial,sans-serif",
+ position: "fixed",
+ zIndex: "10001",
+ bottom: "20px",
+ right: "10px",
+ textAlign: "right"
+ });
+ this.navigation.innerHTML = "(Press Esc to Quit) ";
+ this.gameContainer.appendChild(this.navigation);
+ this.points = document.createElement("span");
+ this.points.id = "ASTEROIDS-POINTS";
+ this.points.style.font = "28pt Arial, sans-serif";
+ this.points.style.fontWeight = "bold";
+ this.points.className = "ASTEROIDSYEAH";
+ this.navigation.appendChild(this.points);
+ } else {
+ this.navigation = document.getElementById("ASTEROIDS-NAVIGATION");
+ this.points = document.getElementById("ASTEROIDS-POINTS");
+ }
+ setScore();
+ const eventKeydown = function(event) {
+ that.keysPressed[event.key] = true;
+ switch (event.key) {
+ case " ":
+ that.firedAt = 1;
+ break;
+ }
+ if (["ArrowUp", "ArrowDown", "ArrowRight", "ArrowLeft", " ", "b", "w", "a", "s", "d"].includes(event.key)) {
+ if (event.preventDefault) event.preventDefault();
+ if (event.stopPropagation) event.stopPropagation();
+ event.returnValue = false;
+ event.cancelBubble = true;
+ return false;
+ }
+ };
+ document.addEventListener("keydown", eventKeydown, false);
+ const eventKeypress = function(event) {
+ if (["ArrowUp", "ArrowDown", "ArrowRight", "ArrowLeft", " ", "w", "a", "s", "d"].includes(event.key)) {
+ if (event.preventDefault) event.preventDefault();
+ if (event.stopPropagation) event.stopPropagation();
+ event.returnValue = false;
+ event.cancelBubble = true;
+ return false;
+ }
+ };
+ document.addEventListener("keypress", eventKeypress, false);
+ const eventKeyup = function(event) {
+ that.keysPressed[event.key] = false;
+ if (["ArrowUp", "ArrowDown", "ArrowRight", "ArrowLeft", " ", "b", "w", "a", "s", "d"].includes(event.key)) {
+ if (event.preventDefault) event.preventDefault();
+ if (event.stopPropagation) event.stopPropagation();
+ event.returnValue = false;
+ event.cancelBubble = true;
+ return false;
+ }
+ };
+ document.addEventListener("keyup", eventKeyup, false);
+ this.ctx.clear = function() {
+ this.clearRect(0, 0, w, h);
+ };
+ this.ctx.clear();
+ this.ctx.drawLine = function(xFrom, yFrom, xTo, yTo) {
+ this.beginPath();
+ this.moveTo(xFrom, yFrom);
+ this.lineTo(xTo, yTo);
+ this.lineTo(xTo + 1, yTo + 1);
+ this.closePath();
+ this.fill();
+ };
+ this.ctx.tracePoly = function(verts) {
+ this.beginPath();
+ this.moveTo(verts[0][0], verts[0][1]);
+ for (let i = 1; i < verts.length; i++)
+ this.lineTo(verts[i][0], verts[i][1]);
+ this.closePath();
+ };
+ this.ctx.drawPlayer = function() {
+ this.save();
+ this.translate(that.pos.x, that.pos.y);
+ this.rotate(that.dir.angle());
+ this.tracePoly(playerVerts);
+ this.fillStyle = "white";
+ this.fill();
+ this.tracePoly(playerVerts);
+ this.stroke();
+ this.restore();
+ };
+ this.ctx.drawBullets = function(bullets) {
+ for (let i = 0; i < bullets.length; i++) {
+ this.beginPath();
+ this.arc(bullets[i].pos.x, bullets[i].pos.y, bulletRadius, 0, Math.PI * 2, true);
+ this.closePath();
+ this.fill();
+ }
+ };
+ const randomParticleColor = function() {
+ return (["red", "yellow"])[random(0, 1)];
+ };
+ this.ctx.drawParticles = function(particles) {
+ const oldColor = this.fillStyle;
+ for (let i = 0; i < particles.length; i++) {
+ this.fillStyle = randomParticleColor();
+ this.drawLine(particles[i].pos.x, particles[i].pos.y, particles[i].pos.x - particles[i].dir.x * 10, particles[i].pos.y - particles[i].dir.y * 10);
+ }
+ this.fillStyle = oldColor;
+ };
+ this.ctx.drawFlames = function(flame) {
+ this.save();
+ this.translate(that.pos.x, that.pos.y);
+ this.rotate(that.dir.angle());
+ const oldColor = this.strokeStyle;
+ this.strokeStyle = "red";
+ this.tracePoly(flame.r);
+ this.stroke();
+ this.strokeStyle = "yellow";
+ this.tracePoly(flame.y);
+ this.stroke();
+ this.strokeStyle = oldColor;
+ this.restore();
+ }
+ addParticles(this.pos);
+ document.body.classList.add("ASTEROIDSYEAH");
+ let lastUpdate = new Date().getTime();
+ function updateFunc() {
+ that.update.call(that);
+ };
+ setTimeout(updateFunc, 1000 / FPS);
+ this.update = function() {
+ let forceChange = false;
+ const nowTime = new Date().getTime();
+ const tDelta = (nowTime - lastUpdate) / 1000;
+ lastUpdate = nowTime;
+ let drawFlame = false;
+ if (nowTime - this.updated.flame > 50) {
+ createFlames();
+ this.updated.flame = nowTime;
+ }
+ this.scrollPos.x = window.pageXOffset || document.documentElement.scrollLeft;
+ this.scrollPos.y = window.pageYOffset || document.documentElement.scrollTop;
+ if ((this.keysPressed["ArrowUp"]) || (this.keysPressed["w"])) {
+ this.vel.add(this.dir.mulNew(acc * tDelta));
+ drawFlame = true;
+ } else {
+ this.vel.mul(0.96);
+ }
+ if ((this.keysPressed["ArrowLeft"]) || (this.keysPressed["a"])) {
+ forceChange = true;
+ this.dir.rotate(radians(rotSpeed * tDelta * -1));
+ }
+ if ((this.keysPressed["ArrowRight"]) || (this.keysPressed["d"])) {
+ forceChange = true;
+ this.dir.rotate(radians(rotSpeed * tDelta));
+ }
+ if (this.keysPressed[" "] && nowTime - this.firedAt > timeBetweenFire) {
+ this.bullets.unshift({
+ dir: this.dir.cp(),
+ pos: this.pos.cp(),
+ startVel: this.vel.cp(),
+ cameAlive: nowTime
+ });
+ this.firedAt = nowTime;
+ if (this.bullets.length > maxBullets) {
+ this.bullets.pop();
+ }
+ }
+ if (this.keysPressed["b"]) {
+ if (!this.updated.enemies) {
+ updateEnemyIndex();
+ this.updated.enemies = true;
+ }
+ forceChange = true;
+ this.updated.blink.time += tDelta * 1000;
+ if (this.updated.blink.time > timeBetweenBlink) {
+ this.toggleBlinkStyle();
+ this.updated.blink.time = 0;
+ }
+ } else {
+ this.updated.enemies = false;
+ }
+ if (this.keysPressed["Escape"]) {
+ destroy.apply(this);
+ return;
+ }
+ if (this.vel.len() > maxSpeed) {
+ this.vel.setLength(maxSpeed);
+ }
+ this.pos.add(this.vel.mulNew(tDelta));
+ if (this.pos.x > w) {
+ window.scrollTo(this.scrollPos.x + 50, this.scrollPos.y);
+ this.pos.x = 0;
+ } else if (this.pos.x < 0) {
+ window.scrollTo(this.scrollPos.x - 50, this.scrollPos.y);
+ this.pos.x = w;
+ }
+ if (this.pos.y > h) {
+ window.scrollTo(this.scrollPos.x, this.scrollPos.y + h * 0.75);
+ this.pos.y = 0;
+ } else if (this.pos.y < 0) {
+ window.scrollTo(this.scrollPos.x, this.scrollPos.y - h * 0.75);
+ this.pos.y = h;
+ }
+ for (let i = this.bullets.length - 1; i >= 0; i--) {
+ if (nowTime - this.bullets[i].cameAlive > 2000) {
+ this.bullets.splice(i, 1);
+ forceChange = true;
+ continue;
+ }
+ const bulletVel = this.bullets[i].dir.setLengthNew(bulletSpeed * tDelta).add(this.bullets[i].startVel.mulNew(tDelta));
+ this.bullets[i].pos.add(bulletVel);
+ boundsCheck(this.bullets[i].pos);
+ const murdered = getElementFromPoint(this.bullets[i].pos.x, this.bullets[i].pos.y);
+ if (murdered && murdered.tagName && !(ignoredTypes.includes(murdered.tagName.toUpperCase())) && hasOnlyTextualChildren(murdered) && murdered.className !== "ASTEROIDSYEAH") {
+ addParticles(this.bullets[i].pos);
+ this.dying.push(murdered);
+ this.bullets.splice(i, 1);
+ continue;
+ }
+ }
+ if (this.dying.length) {
+ for (let i = this.dying.length - 1; i >= 0; i--) {
+ try {
+ if (this.dying[i].parentNode) window.ASTEROIDS.enemiesKilled++;
+ this.dying[i].parentNode.removeChild(this.dying[i]);
+ } catch (e) {}
+ }
+ setScore();
+ this.dying = [];
+ }
+ for (let i = this.particles.length - 1; i >= 0; i--) {
+ this.particles[i].pos.add(this.particles[i].dir.mulNew(particleSpeed * tDelta * Math.random()));
+ if (nowTime - this.particles[i].cameAlive > 1000) {
+ this.particles.splice(i, 1);
+ forceChange = true;
+ continue;
+ }
+ }
+ if (forceChange || this.bullets.length !== 0 || this.particles.length !== 0 || !this.pos.is(this.lastPos) || this.vel.len() > 0) {
+ this.ctx.clear();
+ this.ctx.drawPlayer();
+ if (drawFlame) this.ctx.drawFlames(that.flame);
+ if (this.bullets.length) {
+ this.ctx.drawBullets(this.bullets);
+ }
+ if (this.particles.length) {
+ this.ctx.drawParticles(this.particles);
+ }
+ }
+ this.lastPos = this.pos;
+ setTimeout(updateFunc, 1000 / FPS);
+ }
+
+ function destroy() {
+ document.removeEventListener("keydown", eventKeydown, false);
+ document.removeEventListener("keypress", eventKeypress, false);
+ document.removeEventListener("keyup", eventKeyup, false);
+ window.removeEventListener("resize", eventResize, false);
+ removeStylesheet("ASTEROIDSYEAHSTYLES");
+ document.body.classList.remove("ASTEROIDSYEAH");
+ this.gameContainer.parentNode.removeChild(this.gameContainer);
+ };
+}
+
+if (!window.ASTEROIDSPLAYERS) window.ASTEROIDSPLAYERS = [];
+window.ASTEROIDSPLAYERS.push(new Asteroids());
\ No newline at end of file
diff --git a/packages/live2d/src/live2d/tools/ai-chat.ts b/packages/live2d/src/live2d/tools/ai-chat.ts
new file mode 100644
index 0000000..a239136
--- /dev/null
+++ b/packages/live2d/src/live2d/tools/ai-chat.ts
@@ -0,0 +1,20 @@
+import { isNotEmptyString } from "../../utils/isString";
+import { Tool } from "./tools";
+
+/**
+ * AI 聊天工具
+ */
+export class AIChatTool extends Tool {
+ name() {
+ return "AIChat";
+ }
+
+ icon() {
+ const icon = this.getConfig().aiChatUrl;
+ return isNotEmptyString(icon) ? icon : "ph-chats-circle-fill";
+ }
+
+ execute() {
+ // TODO: 打开 AI 聊天
+ }
+}
\ No newline at end of file
diff --git a/packages/live2d/src/live2d/tools/asteroids.ts b/packages/live2d/src/live2d/tools/asteroids.ts
new file mode 100644
index 0000000..d96a4b1
--- /dev/null
+++ b/packages/live2d/src/live2d/tools/asteroids.ts
@@ -0,0 +1,29 @@
+import { isNotEmptyString } from "../../utils/isString";
+import { Tool } from "./tools";
+
+declare global {
+ interface Window {
+ ASTEROIDSPLAYERS: unknown[];
+ }
+}
+
+/**
+ * 小宇宙小游戏工具
+ */
+export class AsteroidsTool extends Tool {
+ name() {
+ return "Asteroids";
+ }
+
+ icon() {
+ const icon = this.getConfig().asteroidsIcon;
+ return isNotEmptyString(icon) ? icon : "ph-paper-plane-tilt-fill";
+ }
+
+ execute() {
+ // @ts-ignore
+ import("@libs/asteroids.min.js").then((module) => {
+ new module.default();
+ });
+ }
+}
\ No newline at end of file
diff --git a/packages/live2d/src/live2d/tools/custom-tool.ts b/packages/live2d/src/live2d/tools/custom-tool.ts
new file mode 100644
index 0000000..53e56eb
--- /dev/null
+++ b/packages/live2d/src/live2d/tools/custom-tool.ts
@@ -0,0 +1,39 @@
+import type { Live2dConfig } from "context/config-context";
+import { isNotEmptyString } from "../../utils/isString";
+import { Tool } from "./tools";
+import { _getFullOrDefaultTips } from '../../events/tip-events';
+
+export type CustomToolConfig = {
+ name: string;
+ icon?: string;
+ execute: () => void;
+}
+
+/**
+ * 自定义工具
+ */
+export class CustomTool extends Tool {
+ _name: string;
+ _icon?: string;
+ _execute: () => void;
+
+ constructor(config: Live2dConfig, { name, icon, execute }: CustomToolConfig) {
+ super(config);
+ this._name = name;
+ this._icon = icon;
+ this._execute = execute;
+ }
+
+ name() {
+ return this._name;
+ }
+
+ icon() {
+ const icon = this._icon;
+ return isNotEmptyString(icon) ? icon : "ph-question-fill";
+ }
+
+ execute() {
+ this._execute.bind(this)();
+ }
+}
\ No newline at end of file
diff --git a/packages/live2d/src/live2d/tools/hitokoto.ts b/packages/live2d/src/live2d/tools/hitokoto.ts
new file mode 100644
index 0000000..5ef04f8
--- /dev/null
+++ b/packages/live2d/src/live2d/tools/hitokoto.ts
@@ -0,0 +1,73 @@
+import queryString from "query-string";
+import { isNotEmptyString } from "../../utils/isString";
+import { Tool } from "./tools";
+import { sendMessage } from "../../helpers/sendMessage";
+
+/**
+ * 一言工具,使用一言接口获取一句话
+ * 如需使用自定义的接口,则需要满足 hitokoto 的接口规范。
+ *
+ * @link https://developer.hitokoto.cn/sentence/demo.html
+ */
+export class HitokotoTool extends Tool {
+ _default_api = "https://v1.hitokoto.cn";
+
+ name() {
+ return "Hitokoto";
+ }
+
+ icon() {
+ const icon = this.getConfig().aiChatUrl;
+ return isNotEmptyString(icon) ? icon : "ph-chat-circle-fill";
+ }
+
+ async execute() {
+ const { hitokoto, description } = await this._getHitokotoMessage() || {};
+ if (isNotEmptyString(hitokoto)) {
+ sendMessage(hitokoto, 6000, 2);
+ setTimeout(() => {
+ sendMessage(description, 4000, 2);
+ }, 6000);
+ }
+ }
+
+ private async _getHitokotoMessage(): Promise<{ hitokoto: string, description: string } | undefined> {
+ const unverifiedApi = this.getConfig().hitokotoApi || this._default_api;
+ const parsedApi = queryString.parseUrl(unverifiedApi);
+ const newParams = { ...parsedApi.query, encode: "json", charset: "utf-8" };
+ const hitokotoApi = `${parsedApi.url}?${queryString.stringify(newParams)}`;
+ const { hitokoto, from, creator } = await this._fetchHitokoto(hitokotoApi);
+ if (isNotEmptyString(hitokoto)) {
+ return {
+ hitokoto: "hitokoto",
+ description: `这句一言来自
「${from}」,是
${creator} 在 hitokoto.cn 投稿的。`
+ }
+ }
+ }
+
+ private _fetchHitokoto(hitokotoApi: string): Promise
{
+ return fetch(hitokotoApi)
+ .then(res => res.json())
+ .then((result: HitokotoResult) => {
+ return result;
+ });
+ }
+}
+
+/**
+ * @link https://developer.hitokoto.cn/sentence/#%E8%BF%94%E5%9B%9E%E4%BF%A1%E6%81%AF
+ */
+export type HitokotoResult = {
+ id?: number;
+ hitokoto?: string;
+ type?: string;
+ from?: string;
+ from_who?: string;
+ creator?: string;
+ creator_uid?: number;
+ reviewer?: number;
+ uuid?: string;
+ commit_from?: string;
+ created_at?: string;
+ length?: number;
+}
\ No newline at end of file
diff --git a/packages/live2d/src/live2d/tools/info.ts b/packages/live2d/src/live2d/tools/info.ts
new file mode 100644
index 0000000..8629ca4
--- /dev/null
+++ b/packages/live2d/src/live2d/tools/info.ts
@@ -0,0 +1,21 @@
+import { isNotEmptyString } from "../../utils/isString";
+import { Tool } from "./tools";
+
+/**
+ * 前往站点工具
+ */
+export class InfoTool extends Tool {
+ name() {
+ return "InfoTool";
+ }
+
+ icon() {
+ const icon = this.getConfig().infoIcon;
+ return isNotEmptyString(icon) ? icon : "ph-info-fill";
+ }
+
+ execute() {
+ const siteUrl = this.getConfig().infoSite || "https://github.com/LIlGG/plugin-live2d";
+ window.open(siteUrl);
+ }
+}
\ No newline at end of file
diff --git a/packages/live2d/src/live2d/tools/screenshot.ts b/packages/live2d/src/live2d/tools/screenshot.ts
new file mode 100644
index 0000000..8d9ccb9
--- /dev/null
+++ b/packages/live2d/src/live2d/tools/screenshot.ts
@@ -0,0 +1,29 @@
+import { sendMessage } from "helpers/sendMessage";
+import { isNotEmptyString } from "../../utils/isString";
+import { Tool } from "./tools";
+
+declare const Live2D: {
+ captureName: string;
+ captureFrame: boolean;
+};
+
+/**
+ * 截图工具
+ */
+export class ScreenshotTool extends Tool {
+ name() {
+ return "Screenshot";
+ }
+
+ icon() {
+ const icon = this.getConfig().screenshotIcon;
+ return isNotEmptyString(icon) ? icon : "ph-arrows-counter-clockwise-fill";
+ }
+
+ execute() {
+ sendMessage("照好了嘛,是不是很可爱呢?", 6000, 2);
+ const screenshotName = this.getConfig().screenshotName || 'live2d';
+ Live2D.captureName = `${screenshotName}.png`;
+ Live2D.captureFrame = true;
+ }
+}
\ No newline at end of file
diff --git a/packages/live2d/src/live2d/tools/switch-model.ts b/packages/live2d/src/live2d/tools/switch-model.ts
new file mode 100644
index 0000000..52cc0f2
--- /dev/null
+++ b/packages/live2d/src/live2d/tools/switch-model.ts
@@ -0,0 +1,20 @@
+import { isNotEmptyString } from "../../utils/isString";
+import { Tool } from "./tools";
+
+/**
+ * 切换模型工具
+ */
+export class SwitchModelTool extends Tool {
+ name() {
+ return "SwitchModel";
+ }
+
+ icon() {
+ const icon = this.getConfig().switchModelIcon;
+ return isNotEmptyString(icon) ? icon : "ph-dress-fill";
+ }
+
+ execute() {
+ console.log("Model switch event emitted.");
+ }
+}
\ No newline at end of file
diff --git a/packages/live2d/src/live2d/tools/switch-texture.ts b/packages/live2d/src/live2d/tools/switch-texture.ts
new file mode 100644
index 0000000..a54b70d
--- /dev/null
+++ b/packages/live2d/src/live2d/tools/switch-texture.ts
@@ -0,0 +1,20 @@
+import { isNotEmptyString } from "../../utils/isString";
+import { Tool } from "./tools";
+
+/**
+ * 切换纹理工具
+ */
+export class SwitchTextureTool extends Tool {
+ name() {
+ return "SwitchTexture";
+ }
+
+ icon() {
+ const icon = this.getConfig().switchTextureIcon;
+ return isNotEmptyString(icon) ? icon : "ph-camera-fill";
+ }
+
+ execute() {
+ // 发出切换模型的事件
+ }
+}
\ No newline at end of file
diff --git a/packages/live2d/src/live2d/tools/tools.ts b/packages/live2d/src/live2d/tools/tools.ts
new file mode 100644
index 0000000..40cb404
--- /dev/null
+++ b/packages/live2d/src/live2d/tools/tools.ts
@@ -0,0 +1,19 @@
+import type { Live2dConfig } from "../../context/config-context";
+
+export abstract class Tool {
+ private _config: Live2dConfig;
+
+ constructor(config: Live2dConfig) {
+ this._config = config;
+ }
+
+ abstract name(): string;
+
+ abstract icon(): string;
+
+ abstract execute(): void;
+
+ protected getConfig(): Live2dConfig {
+ return this._config;
+ }
+}
\ No newline at end of file
diff --git a/packages/live2d/tsconfig.json b/packages/live2d/tsconfig.json
index 0b26a03..7becaa1 100644
--- a/packages/live2d/tsconfig.json
+++ b/packages/live2d/tsconfig.json
@@ -1,31 +1,36 @@
{
- "compilerOptions": {
- "lib": ["ES2020", "DOM", "DOM.Iterable"],
- "jsx": "react-jsx",
- "target": "ES2020",
- "noEmit": true,
- "skipLibCheck": true,
+ "compilerOptions": {
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "jsx": "react-jsx",
+ "target": "ES2020",
+ "noEmit": true,
+ "skipLibCheck": true,
- /* modules */
- "module": "ESNext",
- "moduleResolution": "bundler",
- "allowImportingTsExtensions": true,
- "resolveJsonModule": true,
- "isolatedModules": true,
+ /* modules */
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
- /* Linting */
- "strict": true,
- "noUnusedLocals": true,
- "noUnusedParameters": true,
- "noFallthroughCasesInSwitch": true,
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
- "experimentalDecorators": true,
- "useDefineForClassFields": false,
- "plugins": [
- {
- "name": "ts-lit-plugin"
- }
- ]
- },
- "include": ["src"]
+ "experimentalDecorators": true,
+ "useDefineForClassFields": false,
+ "plugins": [
+ {
+ "name": "ts-lit-plugin"
+ }
+ ],
+ "baseUrl": "./src",
+ "paths": {
+ "@libs/*": ["libs/*"], // 定义别名
+ "@utils/*": ["utils/*"]
+ }
+ },
+ "include": ["src"]
}
From 874b8ffd64fbf21327f719b535784cbc848be2f9 Mon Sep 17 00:00:00 2001
From: LIlGG <1103069291@qq.com>
Date: Thu, 20 Feb 2025 01:33:21 +0800
Subject: [PATCH 09/12] refactor: restructure Live2D events
---
.../live2d/src/components/Live2dCanvas.tsx | 9 +-
packages/live2d/src/components/Live2dTips.tsx | 3 +-
.../live2d/src/components/Live2dToggle.tsx | 100 +++++++++---------
.../live2d/src/components/Live2dTools.tsx | 27 ++---
.../live2d/src/components/Live2dWidget.tsx | 4 +-
.../live2d/src/events/add-default-message.ts | 23 ++++
packages/live2d/src/events/before-init.ts | 23 ++++
packages/live2d/src/events/event.d.ts | 55 ----------
packages/live2d/src/events/send-message.ts | 28 +++++
packages/live2d/src/events/tip-events.ts | 12 +--
packages/live2d/src/events/toggle-canvas.ts | 26 +++++
packages/live2d/src/events/types.ts | 5 +
.../live2d/src/live2d/tools/custom-tool.ts | 4 +-
packages/live2d/src/live2d/tools/exit.ts | 26 +++++
packages/live2d/tsconfig.json | 4 +-
pnpm-lock.yaml | 31 ++++++
16 files changed, 243 insertions(+), 137 deletions(-)
create mode 100644 packages/live2d/src/events/add-default-message.ts
create mode 100644 packages/live2d/src/events/before-init.ts
delete mode 100644 packages/live2d/src/events/event.d.ts
create mode 100644 packages/live2d/src/events/send-message.ts
create mode 100644 packages/live2d/src/events/toggle-canvas.ts
create mode 100644 packages/live2d/src/events/types.ts
create mode 100644 packages/live2d/src/live2d/tools/exit.ts
diff --git a/packages/live2d/src/components/Live2dCanvas.tsx b/packages/live2d/src/components/Live2dCanvas.tsx
index 5d738a3..134ab68 100644
--- a/packages/live2d/src/components/Live2dCanvas.tsx
+++ b/packages/live2d/src/components/Live2dCanvas.tsx
@@ -7,6 +7,7 @@ import Model from "../live2d/model";
import { consume } from "@lit/context";
import { configContext, type Live2dConfig } from "../context/config-context";
import "../libs/live2d.min.js";
+import { BeforeInitEvent } from "../events/before-init.js";
export class Live2dCanvas extends UnoLitElement {
@consume({ context: configContext })
@@ -30,13 +31,7 @@ export class Live2dCanvas extends UnoLitElement {
connectedCallback(): void {
super.connectedCallback();
// 发出 Live2d beforeInit 事件
- window.dispatchEvent(
- new CustomEvent("live2d:before-init", {
- detail: {
- config: this.config,
- },
- }),
- );
+ window.dispatchEvent(new BeforeInitEvent({ config: this.config }));
}
protected firstUpdated(_changedProperties: PropertyValues): void {
diff --git a/packages/live2d/src/components/Live2dTips.tsx b/packages/live2d/src/components/Live2dTips.tsx
index 0fecdfe..552ad2f 100644
--- a/packages/live2d/src/components/Live2dTips.tsx
+++ b/packages/live2d/src/components/Live2dTips.tsx
@@ -9,6 +9,7 @@ import { isNotEmpty } from "../utils/isNotEmpty";
import { randomSelection } from "../utils/randomSelection";
import { unsafeHTML } from "lit/directives/unsafe-html.js";
import { classMap } from "lit/directives/class-map.js";
+import { SendMessageEvent } from "../events/send-message";
export class Live2dTips extends UnoLitElement {
@consume({ context: configContext })
@@ -52,7 +53,7 @@ export class Live2dTips extends UnoLitElement {
window.removeEventListener("live2d:send-message", this.handleMessage.bind(this));
}
- handleMessage(e: CustomEvent): void {
+ handleMessage(e: SendMessageEvent): void {
const { text, timeout, priority } = e.detail;
if (!isNotEmpty(text)) {
return;
diff --git a/packages/live2d/src/components/Live2dToggle.tsx b/packages/live2d/src/components/Live2dToggle.tsx
index aa46cb5..209faa0 100644
--- a/packages/live2d/src/components/Live2dToggle.tsx
+++ b/packages/live2d/src/components/Live2dToggle.tsx
@@ -3,66 +3,68 @@ import { UnoLitElement } from "../common/UnoLitElement";
import { createComponent } from "@lit/react";
import React from "react";
import { state } from "lit/decorators.js";
+import { ToggleCanvasEvent } from "../events/toggle-canvas";
export class Live2dToggle extends UnoLitElement {
- @state()
- // 当前工具栏是否显示
- private _isShow = false;
+ @state()
+ // 当前工具栏是否显示
+ private _isShow = false;
- connectedCallback(): void {
- super.connectedCallback();
- this.addEventListener("click", this.handleClick);
+ connectedCallback(): void {
+ super.connectedCallback();
+ this.addEventListener("click", this.handleClick);
- // 初始化时,判断是否已经隐藏看板娘超过一天,如果是,则显示看板娘。否则,继续隐藏看板娘。
- const live2dDisplay = localStorage.getItem("live2d-display");
- if (live2dDisplay) {
- if (Date.now() - Number.parseInt(live2dDisplay) <= 24 * 60 * 60 * 1000) {
- this.triggerToggleLive2d(false);
- return;
- }
- }
- this.triggerToggleLive2d(true);
- }
+ // 初始化时,判断是否已经隐藏看板娘超过一天,如果是,则显示看板娘。否则,继续隐藏看板娘。
+ const live2dDisplay = localStorage.getItem("live2d-display");
+ if (live2dDisplay) {
+ if (Date.now() - Number.parseInt(live2dDisplay) <= 24 * 60 * 60 * 1000) {
+ this.triggerToggleLive2d(false);
+ return;
+ }
+ }
+ this.triggerToggleLive2d(true);
+ }
- disconnectedCallback(): void {
- super.disconnectedCallback();
- this.removeEventListener("click", this.handleClick);
- }
+ disconnectedCallback(): void {
+ super.disconnectedCallback();
+ this.removeEventListener("click", this.handleClick);
+ }
- render(): TemplateResult {
- return html`
- 看板娘
-
`;
- }
+ render(): TemplateResult {
+ return html`
+ 看板娘
+
`;
+ }
- handleClick() {
- this.triggerToggleLive2d(!!this._isShow);
- }
+ handleClick() {
+ this.triggerToggleLive2d(!!this._isShow);
+ }
- triggerToggleLive2d(isShow: boolean) {
- // 当前切换栏与 Live2d 的显示状态相反
- this._isShow = !isShow;
- if (isShow) {
- localStorage.removeItem("live2d-display");
- } else {
- localStorage.setItem("live2d-display", Date.now().toString());
- }
- this.dispatchEvent(
- new CustomEvent("live2d:toggle-canvas", {
- bubbles: true,
- composed: true,
- detail: {
- isShow,
- },
- }),
- );
- }
+ triggerToggleLive2d(isShow: boolean) {
+ // 当前切换栏与 Live2d 的显示状态相反
+ this._isShow = !isShow;
+ if (isShow) {
+ localStorage.removeItem("live2d-display");
+ } else {
+ localStorage.setItem("live2d-display", Date.now().toString());
+ }
+ this.dispatchEvent(
+ new ToggleCanvasEvent({
+ isShow,
+ })
+ );
+ }
}
customElements.define("live2d-toggle", Live2dToggle);
export const Live2dToggleComponent = createComponent({
- tagName: "live2d-toggle",
- elementClass: Live2dToggle,
- react: React,
+ tagName: "live2d-toggle",
+ elementClass: Live2dToggle,
+ react: React,
});
diff --git a/packages/live2d/src/components/Live2dTools.tsx b/packages/live2d/src/components/Live2dTools.tsx
index 65821ef..5f14ab4 100644
--- a/packages/live2d/src/components/Live2dTools.tsx
+++ b/packages/live2d/src/components/Live2dTools.tsx
@@ -7,23 +7,24 @@ import { configContext, type Live2dConfig } from "../context/config-context";
import { property } from "lit/decorators.js";
export class Live2dTools extends UnoLitElement {
- @consume({ context: configContext })
- @property({ attribute: false })
- public config?: Live2dConfig;
+ @consume({ context: configContext })
+ @property({ attribute: false })
+ public config?: Live2dConfig;
- render(): TemplateResult {
- return html`
-
-
-
- `;
- }
+ constructor() {
+ super();
+ console.log("Live2dTools constructor", this.config);
+ }
+
+ render(): TemplateResult {
+ return html` `;
+ }
}
customElements.define("live2d-tools", Live2dTools);
export const Live2dToolsComponent = createComponent({
- tagName: "live2d-tools",
- elementClass: Live2dTools,
- react: React,
+ tagName: "live2d-tools",
+ elementClass: Live2dTools,
+ react: React,
});
diff --git a/packages/live2d/src/components/Live2dWidget.tsx b/packages/live2d/src/components/Live2dWidget.tsx
index 289a3c4..6fd1414 100644
--- a/packages/live2d/src/components/Live2dWidget.tsx
+++ b/packages/live2d/src/components/Live2dWidget.tsx
@@ -9,6 +9,7 @@ import "./Live2dToggle";
import "./Live2dTips";
import "./Live2dCanvas";
import "./Live2dTools";
+import { ToggleCanvasEvent } from "../events/toggle-canvas";
export class Live2dWidget extends UnoLitElement {
@consume({ context: configContext })
@@ -43,7 +44,8 @@ export class Live2dWidget extends UnoLitElement {
}
}
- handleToggleWidget(e: CustomEvent) {
+ handleToggleWidget(e: ToggleCanvasEvent) {
+ console.log(e);
this._isShow = e.detail.isShow;
}
}
diff --git a/packages/live2d/src/events/add-default-message.ts b/packages/live2d/src/events/add-default-message.ts
new file mode 100644
index 0000000..b00a15f
--- /dev/null
+++ b/packages/live2d/src/events/add-default-message.ts
@@ -0,0 +1,23 @@
+import { Live2dEvent } from "./types";
+
+export const ADD_DEFAULT_MESSAGE_EVENT_NAME = "live2d:add-default-message" as const;
+
+declare global {
+ interface GlobalEventHandlersEventMap {
+ [ADD_DEFAULT_MESSAGE_EVENT_NAME]: AddDefaultMessageEvent;
+ }
+}
+
+export interface Live2dAddDefaultMessageEventDetail {
+ // 默认消息
+ message: string[] | string;
+}
+
+/**
+ * 添加默认消息事件
+ */
+export class AddDefaultMessageEvent extends Live2dEvent {
+ constructor(detail: Live2dAddDefaultMessageEventDetail) {
+ super(ADD_DEFAULT_MESSAGE_EVENT_NAME, detail);
+ }
+}
\ No newline at end of file
diff --git a/packages/live2d/src/events/before-init.ts b/packages/live2d/src/events/before-init.ts
new file mode 100644
index 0000000..7d62f1c
--- /dev/null
+++ b/packages/live2d/src/events/before-init.ts
@@ -0,0 +1,23 @@
+import { Live2dConfig } from "context/config-context";
+import { Live2dEvent } from "./types";
+
+export const BEFORE_INIT_EVENT_NAME = "live2d:before-init" as const;
+
+declare global {
+ interface GlobalEventHandlersEventMap {
+ [BEFORE_INIT_EVENT_NAME]: BeforeInitEvent;
+ }
+}
+
+export interface Live2dBeforeInitEventDetail {
+ config?: Live2dConfig
+}
+
+/**
+ * Live2d 初始化前事件
+ */
+export class BeforeInitEvent extends Live2dEvent {
+ constructor(detail: Live2dBeforeInitEventDetail) {
+ super(BEFORE_INIT_EVENT_NAME, detail);
+ }
+}
diff --git a/packages/live2d/src/events/event.d.ts b/packages/live2d/src/events/event.d.ts
deleted file mode 100644
index 417d7ca..0000000
--- a/packages/live2d/src/events/event.d.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/CustomEvent) */
-interface CustomEvent extends Event {
- /**
- * Returns any custom data event was created with. Typically used for synthetic events.
- *
- * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CustomEvent/detail)
- */
- readonly detail: T;
- /**
- * @deprecated
- *
- * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CustomEvent/initCustomEvent)
- */
- initCustomEvent(type: keyof GlobalEventHandlersEventMap, bubbles?: boolean, cancelable?: boolean, detail?: T): void;
-}
-
-
-// 扩展全局事件映射
-interface GlobalEventHandlersEventMap {
- // Live2d beforeInit 事件
- "live2d:before-init": CustomEvent;
-
- // 切换 canvas 显示状态事件
- "live2d:toggle-canvas": CustomEvent;
-
- // 发送 Live2D 消息事件
- "live2d:send-message": CustomEvent;
-
- // 添加默认消息事件
- "live2d:add-default-message": CustomEvent;
-}
-
-interface Live2dMessageEventDetail {
- // 消息内容
- text: string[] | string;
- // 消息显示时间
- timeout: number;
- // 消息优先级
- priority: number;
-}
-
-interface Live2dBeforeInitEventDetail {
- // Live2D 配置
- config: Live2dConfig
-}
-
-interface Live2dToggleEventDetail {
- // 是否显示看板娘
- isShow: boolean;
-}
-
-interface Live2dAddDefaultMessageEventDetail {
- // 默认消息
- message: string[] | string;
-}
diff --git a/packages/live2d/src/events/send-message.ts b/packages/live2d/src/events/send-message.ts
new file mode 100644
index 0000000..126642a
--- /dev/null
+++ b/packages/live2d/src/events/send-message.ts
@@ -0,0 +1,28 @@
+import { Live2dEvent } from "./types";
+
+export const SEND_MESSAGE_EVENT_NAME = "live2d:send-message" as const;
+
+declare global {
+ interface GlobalEventHandlersEventMap {
+ [SEND_MESSAGE_EVENT_NAME]: SendMessageEvent;
+ }
+}
+
+export interface Live2dMessageEventDetail {
+ // 消息内容
+ text: string[] | string;
+ // 消息显示时间
+ timeout: number;
+ // 消息优先级
+ priority: number;
+}
+
+
+/**
+ * 发送消息事件
+ */
+export class SendMessageEvent extends Live2dEvent {
+ constructor(detail: Live2dMessageEventDetail) {
+ super(SEND_MESSAGE_EVENT_NAME, detail);
+ }
+}
diff --git a/packages/live2d/src/events/tip-events.ts b/packages/live2d/src/events/tip-events.ts
index 8b26ddb..372b1d1 100644
--- a/packages/live2d/src/events/tip-events.ts
+++ b/packages/live2d/src/events/tip-events.ts
@@ -8,9 +8,13 @@ import { timeWithinRange } from "../helpers/timeWithinRange";
import { isNotEmptyString, isString } from "../utils/isString";
import { randomSelection } from "../utils/randomSelection";
import { documentTitle, getReferrerDomain, hasWebsiteHome } from "../utils/util";
+import { AddDefaultMessageEvent } from "./add-default-message";
window.addEventListener("live2d:before-init", async (e) => {
const config = e.detail.config;
+ if (!config) {
+ return;
+ }
const tips = await _loadTips(config);
if (!tips) {
return;
@@ -43,13 +47,7 @@ const _welcomeEvent = (times: TipTime[]) => {
const _holidayEvent = (seasons: TipSeason[]) => {
for (const { date, text } of seasons) {
if (dataWithinRange(date)) {
- window.dispatchEvent(
- new CustomEvent("live2d:add-default-message", {
- detail: {
- message: text,
- }
- })
- );
+ window.dispatchEvent(new AddDefaultMessageEvent({ message: text }));
}
}
}
diff --git a/packages/live2d/src/events/toggle-canvas.ts b/packages/live2d/src/events/toggle-canvas.ts
new file mode 100644
index 0000000..3c107d5
--- /dev/null
+++ b/packages/live2d/src/events/toggle-canvas.ts
@@ -0,0 +1,26 @@
+import { Live2dEvent } from "./types";
+
+export const TOGGLE_CANVAS_EVENT_NAME = "live2d:toggle-canvas" as const;
+
+declare global {
+ interface GlobalEventHandlersEventMap {
+ [TOGGLE_CANVAS_EVENT_NAME]: ToggleCanvasEvent;
+ }
+}
+
+export interface Live2dToggleCanvasEventDetail {
+ // 是否显示看板娘
+ isShow: boolean;
+}
+
+/**
+ * 切换 canvas 显示状态事件
+ */
+export class ToggleCanvasEvent extends Live2dEvent {
+ constructor(detail: Live2dToggleCanvasEventDetail) {
+ super(TOGGLE_CANVAS_EVENT_NAME, detail, {
+ bubbles: true,
+ composed: true,
+ });
+ }
+}
diff --git a/packages/live2d/src/events/types.ts b/packages/live2d/src/events/types.ts
new file mode 100644
index 0000000..a79b7e9
--- /dev/null
+++ b/packages/live2d/src/events/types.ts
@@ -0,0 +1,5 @@
+export abstract class Live2dEvent extends CustomEvent {
+ constructor(type: string, detail: T, options?: EventInit) {
+ super(type, { detail, ...options });
+ }
+}
\ No newline at end of file
diff --git a/packages/live2d/src/live2d/tools/custom-tool.ts b/packages/live2d/src/live2d/tools/custom-tool.ts
index 53e56eb..41bf65f 100644
--- a/packages/live2d/src/live2d/tools/custom-tool.ts
+++ b/packages/live2d/src/live2d/tools/custom-tool.ts
@@ -1,7 +1,7 @@
import type { Live2dConfig } from "context/config-context";
-import { isNotEmptyString } from "../../utils/isString";
+import { isNotEmptyString } from "utils/isString";
import { Tool } from "./tools";
-import { _getFullOrDefaultTips } from '../../events/tip-events';
+import { _getFullOrDefaultTips } from '../events/tip-events';
export type CustomToolConfig = {
name: string;
diff --git a/packages/live2d/src/live2d/tools/exit.ts b/packages/live2d/src/live2d/tools/exit.ts
new file mode 100644
index 0000000..6bde45b
--- /dev/null
+++ b/packages/live2d/src/live2d/tools/exit.ts
@@ -0,0 +1,26 @@
+import { sendMessage } from "helpers/sendMessage";
+import { isNotEmptyString } from "@utils/isString";
+import { Tool } from "./tools";
+import { ToggleCanvasEvent } from "../events/toggle-canvas";
+
+/**
+ * 退出 Live2d 工具
+ */
+export class ExitTool extends Tool {
+ name() {
+ return "ExitTool";
+ }
+
+ icon() {
+ const icon = this.getConfig().exitIcon;
+ return isNotEmptyString(icon) ? icon : "ph-x-bold";
+ }
+
+ execute() {
+ sendMessage("愿你有一天能与重要的人重逢。", 2000, 4);
+ setTimeout(() => {
+ // 触发退出 Live2d 事件
+ window.dispatchEvent(new ToggleCanvasEvent({ isShow: false }));
+ }, 3000)
+ }
+}
\ No newline at end of file
diff --git a/packages/live2d/tsconfig.json b/packages/live2d/tsconfig.json
index 7becaa1..3107442 100644
--- a/packages/live2d/tsconfig.json
+++ b/packages/live2d/tsconfig.json
@@ -28,8 +28,8 @@
],
"baseUrl": "./src",
"paths": {
- "@libs/*": ["libs/*"], // 定义别名
- "@utils/*": ["utils/*"]
+ "@libs/*": ["libs/*"],
+ "@utils/*": ["utils/*"],
}
},
"include": ["src"]
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4286167..9acb9aa 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -26,6 +26,9 @@ importers:
lit:
specifier: ^3.2.1
version: 3.2.1
+ query-string:
+ specifier: ^9.1.1
+ version: 9.1.1
react:
specifier: ^19.0.0
version: 19.0.0
@@ -916,6 +919,10 @@ packages:
supports-color:
optional: true
+ decode-uri-component@0.4.1:
+ resolution: {integrity: sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==}
+ engines: {node: '>=14.16'}
+
defu@6.1.4:
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
@@ -979,6 +986,10 @@ packages:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
+ filter-obj@5.1.0:
+ resolution: {integrity: sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==}
+ engines: {node: '>=14.16'}
+
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -1164,6 +1175,10 @@ packages:
resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==}
engines: {node: ^10 || ^12 || >=14}
+ query-string@9.1.1:
+ resolution: {integrity: sha512-MWkCOVIcJP9QSKU52Ngow6bsAWAPlPK2MludXvcrS2bGZSl+T1qX9MZvRIkqUIkGLJquMJHWfsT6eRqUpp4aWg==}
+ engines: {node: '>=18'}
+
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
@@ -1228,6 +1243,10 @@ packages:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
+ split-on-first@3.0.0:
+ resolution: {integrity: sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==}
+ engines: {node: '>=12'}
+
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
@@ -2227,6 +2246,8 @@ snapshots:
dependencies:
ms: 2.1.3
+ decode-uri-component@0.4.1: {}
+
defu@6.1.4: {}
destr@2.0.3: {}
@@ -2326,6 +2347,8 @@ snapshots:
dependencies:
to-regex-range: 5.0.1
+ filter-obj@5.1.0: {}
+
fsevents@2.3.3:
optional: true
@@ -2495,6 +2518,12 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
+ query-string@9.1.1:
+ dependencies:
+ decode-uri-component: 0.4.1
+ filter-obj: 5.1.0
+ split-on-first: 3.0.0
+
queue-microtask@1.2.3: {}
react-dom@19.0.0(react@19.0.0):
@@ -2568,6 +2597,8 @@ snapshots:
source-map@0.6.1:
optional: true
+ split-on-first@3.0.0: {}
+
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
From c0124d233d6e721a68e33792d23d7d407107c3e4 Mon Sep 17 00:00:00 2001
From: LIlGG <1103069291@qq.com>
Date: Tue, 25 Feb 2025 01:01:06 +0800
Subject: [PATCH 10/12] feat: complete most functionalities of the toolbar
---
packages/live2d/package.json | 1 +
.../live2d/src/components/Live2dCanvas.tsx | 2 +-
.../live2d/src/components/Live2dTools.tsx | 86 +++++++++++++++++--
.../live2d/src/components/Live2dWidget.tsx | 65 +++++++-------
packages/live2d/src/context/config-context.ts | 5 ++
packages/live2d/src/helpers/sendMessage.ts | 25 +++---
packages/live2d/src/live2d/tools/ai-chat.ts | 6 +-
packages/live2d/src/live2d/tools/asteroids.ts | 9 +-
.../live2d/src/live2d/tools/custom-tool.ts | 32 +++++--
packages/live2d/src/live2d/tools/exit.ts | 14 ++-
packages/live2d/src/live2d/tools/hitokoto.ts | 22 ++---
packages/live2d/src/live2d/tools/index.ts | 25 ++++++
packages/live2d/src/live2d/tools/info.ts | 11 ++-
.../live2d/src/live2d/tools/screenshot.ts | 12 ++-
.../live2d/src/live2d/tools/switch-model.ts | 6 +-
.../live2d/src/live2d/tools/switch-texture.ts | 8 +-
packages/live2d/src/live2d/tools/tools.ts | 10 ++-
packages/live2d/tsconfig.json | 9 +-
packages/live2d/tsconfig.tsbuildinfo | 2 +-
packages/live2d/vite.config.ts | 36 +++++---
20 files changed, 251 insertions(+), 135 deletions(-)
create mode 100644 packages/live2d/src/live2d/tools/index.ts
diff --git a/packages/live2d/package.json b/packages/live2d/package.json
index 1ea064e..0c484a4 100644
--- a/packages/live2d/package.json
+++ b/packages/live2d/package.json
@@ -22,6 +22,7 @@
"dependencies": {
"@lit/context": "^1.1.3",
"@lit/react": "^1.0.7",
+ "iconify-icon": "^2.3.0",
"lit": "^3.2.1",
"query-string": "^9.1.1",
"react": "^19.0.0",
diff --git a/packages/live2d/src/components/Live2dCanvas.tsx b/packages/live2d/src/components/Live2dCanvas.tsx
index 134ab68..bfacf06 100644
--- a/packages/live2d/src/components/Live2dCanvas.tsx
+++ b/packages/live2d/src/components/Live2dCanvas.tsx
@@ -22,7 +22,7 @@ export class Live2dCanvas extends UnoLitElement {
render(): TemplateResult {
return html`
-