From ec72da29f6a3ce20fcbd80cb00fc6071552834fc Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Mon, 26 May 2025 03:29:06 +0200 Subject: [PATCH] auto-git: [change] animeCat.js --- animeCat.js | 205 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) diff --git a/animeCat.js b/animeCat.js index e69de29..1a062b2 100644 --- a/animeCat.js +++ b/animeCat.js @@ -0,0 +1,205 @@ +// animeCat.js +// miau! + +window.AnimeCat = class AnimeCat { + /** + * @param {HTMLElement} container + * @param {Object} [options] + */ + constructor(container, options = {}) { + this.container = container; + this.images = Object.assign({ + default: 'default.png', + eyesClosed: 'eyes_closed.png', + mouthOpen: 'mouth_open.png' + }, options.images); + this.blinkMin = options.blinkMin ?? 5000; + this.blinkMax = options.blinkMax ?? 15000; + this.blinkDuration = options.blinkDuration ?? 175; + this.talkInterval = options.talkInterval ?? 300; + + this._isSpeaking = false; + this._blinkTimeout = null; + this._talkIntervalId = null; + this._speechTimeout = null; + this._mouthOpen = false; + + this._createElements(); + this._bindMouseHold(); + this._startBlinking(); + } + + _createElements() { + // Flexbox: Katze und Bubble nebeneinander + this.wrapper = document.createElement('div'); + Object.assign(this.wrapper.style, { + position: 'relative', + display: 'flex', + flexDirection: 'row', + alignItems: 'flex-end', + minHeight: '60px', + minWidth: '180px', // Passe an falls du willst + zIndex: '1' + }); + + // --- Cat image --- + this.img = document.createElement('img'); + this.img.src = this.images.default; + this.img.draggable = false; + this.img.style.userSelect = 'none'; + this.img.style.webkitUserSelect = 'none'; + this.img.style.MozUserSelect = 'none'; + this.img.style.msUserSelect = 'none'; + this.img.style.webkitUserDrag = 'none'; + this.img.style.width = '60px'; + this.img.style.height = '60px'; + this.img.style.zIndex = '2'; + + // --- Speech bubble --- + this.bubble = document.createElement('div'); + Object.assign(this.bubble.style, { + position: 'relative', // jetzt relativ zur wrapper-Flexbox + marginLeft: '14px', + marginBottom: '11px', + padding: '10px 16px', + background: 'white', + border: '1px solid #ccc', + borderRadius: '16px 16px 16px 16px', + boxShadow: '0 2px 8px rgba(0,0,0,0.18)', + opacity: '0', + transition: 'opacity 0.3s', + maxWidth: '240px', + wordBreak: 'break-word', + fontFamily: 'sans-serif', + fontSize: '15px', + color: '#333', + pointerEvents: 'none', + minHeight: '30px', + zIndex: '10', + overflowWrap: 'anywhere', + display: 'flex', + alignItems: 'center' + }); + + // --- Bubble "Tail" (kleines Dreieck) --- + this.bubblePointer = document.createElement('div'); + Object.assign(this.bubblePointer.style, { + position: 'absolute', + left: '-13px', + bottom: '12px', + width: '0', + height: '0', + borderTop: '8px solid transparent', + borderBottom: '8px solid transparent', + borderRight: '13px solid #fff', + filter: 'drop-shadow(-2px 1px 2px rgba(0,0,0,0.10))', + zIndex: '11' + }); + this.bubble.appendChild(this.bubblePointer); + + // --- Zusammenbauen --- + this.wrapper.appendChild(this.img); + this.wrapper.appendChild(this.bubble); + this.container.appendChild(this.wrapper); + + // Optional: Passe die Bubble-Dynamik an das Fenster an + // window.addEventListener('resize', () => this._adaptBubbleWidth()); + // this._adaptBubbleWidth(); + } + + // Passe Bubble-Breite an verfügbaren Platz an (optional) + _adaptBubbleWidth() { + const rect = this.wrapper.getBoundingClientRect(); + const parentRect = this.container.getBoundingClientRect(); + const spaceRight = parentRect.right - rect.right - 14; + this.bubble.style.maxWidth = Math.max(140, Math.min(spaceRight, 320)) + 'px'; + } + + _startBlinking() { + const delay = this.blinkMin + Math.random() * (this.blinkMax - this.blinkMin); + this._blinkTimeout = setTimeout(() => { + if (!this._isSpeaking) { + this.img.src = this.images.eyesClosed; + setTimeout(() => { + this.img.src = this.images.default; + this._startBlinking(); + }, this.blinkDuration); + } else { + this._startBlinking(); + } + }, delay); + } + + _bindMouseHold() { + let holdTimer = null; + const closeEyes = () => { + clearTimeout(holdTimer); + this.img.src = this.images.eyesClosed; + }; + const reopenEyes = () => { + clearTimeout(holdTimer); + if (!this._isSpeaking) { + this.img.src = this.images.default; + } + }; + + this.img.addEventListener('mousedown', () => { + if (this._isSpeaking) return; + closeEyes(); + holdTimer = setTimeout(reopenEyes, 4000); + }); + ['mouseup', 'mouseleave'].forEach(evt => + this.img.addEventListener(evt, reopenEyes) + ); + } + + /** Call when streaming text begins */ + beginSpeech() { + clearTimeout(this._speechTimeout); + clearInterval(this._talkIntervalId); + + this._isSpeaking = true; + this._mouthOpen = false; + this.img.src = this.images.default; + this.bubble.style.opacity = '1'; + this.bubble.style.visibility = 'visible'; + // Bubble-Inhalt zurücksetzen (ohne den Pointer zu löschen!) + Array.from(this.bubble.childNodes).forEach(node => { + if (node !== this.bubblePointer) node.remove(); + }); + // Neuen (leeren) Textnode einfügen: + this._bubbleTextNode = document.createTextNode(''); + this.bubble.appendChild(this._bubbleTextNode); + + this._talkIntervalId = setInterval(() => { + this._mouthOpen = !this._mouthOpen; + this.img.src = this._mouthOpen + ? this.images.mouthOpen + : this.images.default; + }, this.talkInterval / 2); + } + + /** Append a chunk of streamed text */ + appendSpeech(chunk) { + if (this._bubbleTextNode) + this._bubbleTextNode.textContent += chunk; + } + + /** Call when the stream ends */ + endSpeech() { + clearInterval(this._talkIntervalId); + this.img.src = this.images.default; + this._speechTimeout = setTimeout(() => { + this.bubble.style.opacity = '0'; + this._isSpeaking = false; + }, 6000); + } + + /** Clean up timers & DOM */ + destroy() { + clearTimeout(this._blinkTimeout); + clearInterval(this._talkIntervalId); + clearTimeout(this._speechTimeout); + this.wrapper.remove(); + } +} \ No newline at end of file