diff --git a/animeCat.js b/animeCat.js index ff33193..e78e568 100644 --- a/animeCat.js +++ b/animeCat.js @@ -1,5 +1,6 @@ // animeCat.js -//miau +// miau! + window.AnimeCat = class AnimeCat { /** * @param {HTMLElement} container @@ -12,10 +13,10 @@ window.AnimeCat = class AnimeCat { 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.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; @@ -29,48 +30,88 @@ window.AnimeCat = class AnimeCat { } _createElements() { + // Flexbox: Katze und Bubble nebeneinander this.wrapper = document.createElement('div'); - this.wrapper.style.position = 'relative'; - this.wrapper.style.display = 'inline-block'; + 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 + // --- Cat image --- this.img = document.createElement('img'); this.img.src = this.images.default; - // disable drag & selection this.img.draggable = false; - this.img.style.userSelect = 'none'; + this.img.style.userSelect = 'none'; this.img.style.webkitUserSelect = 'none'; - this.img.style.MozUserSelect = 'none'; - this.img.style.msUserSelect = 'none'; - // some browsers need this to stop the default drag ghost - this.img.style.webkitUserDrag = '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'; - this.wrapper.appendChild(this.img); - - // speech bubble + // --- Speech bubble --- this.bubble = document.createElement('div'); Object.assign(this.bubble.style, { - position: 'absolute', - bottom: '100%', - left: '50%', - transform: 'translateX(-50%)', - padding: '8px 12px', + position: 'relative', // jetzt relativ zur wrapper-Flexbox + marginLeft: '14px', + padding: '10px 16px', background: 'white', border: '1px solid #ccc', - borderRadius: '4px', - boxShadow: '0 2px 6px rgba(0,0,0,0.2)', + borderRadius: '16px 16px 16px 0', + boxShadow: '0 2px 8px rgba(0,0,0,0.18)', opacity: '0', transition: 'opacity 0.3s', - maxWidth: '200px', - wordWrap: 'break-word', + maxWidth: '240px', + wordBreak: 'break-word', fontFamily: 'sans-serif', - fontSize: '14px', + fontSize: '15px', color: '#333', - pointerEvents: 'none' + pointerEvents: 'none', + minHeight: '30px', + zIndex: '10', + overflowWrap: 'anywhere', + display: 'flex', + alignItems: 'center' }); - this.wrapper.appendChild(this.bubble); + // --- 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() { @@ -90,7 +131,6 @@ window.AnimeCat = class AnimeCat { _bindMouseHold() { let holdTimer = null; - const closeEyes = () => { clearTimeout(holdTimer); this.img.src = this.images.eyesClosed; @@ -103,13 +143,10 @@ window.AnimeCat = class AnimeCat { }; this.img.addEventListener('mousedown', () => { - // if currently talking, ignore hold-to-close if (this._isSpeaking) return; closeEyes(); - // force reopen after max 5s holdTimer = setTimeout(reopenEyes, 4000); }); - ['mouseup', 'mouseleave'].forEach(evt => this.img.addEventListener(evt, reopenEyes) ); @@ -124,7 +161,15 @@ window.AnimeCat = class AnimeCat { this._mouthOpen = false; this.img.src = this.images.default; this.bubble.style.opacity = '1'; - this.bubble.textContent = ''; + 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(); + }); + this.bubblePointer.style.display = ''; + // Neuen (leeren) Textnode einfügen: + this._bubbleTextNode = document.createTextNode(''); + this.bubble.appendChild(this._bubbleTextNode); this._talkIntervalId = setInterval(() => { this._mouthOpen = !this._mouthOpen; @@ -136,7 +181,8 @@ window.AnimeCat = class AnimeCat { /** Append a chunk of streamed text */ appendSpeech(chunk) { - this.bubble.textContent += chunk; + if (this._bubbleTextNode) + this._bubbleTextNode.textContent += chunk; } /** Call when the stream ends */ @@ -145,6 +191,7 @@ window.AnimeCat = class AnimeCat { this.img.src = this.images.default; this._speechTimeout = setTimeout(() => { this.bubble.style.opacity = '0'; + this.bubblePointer.style.display = 'none'; this._isSpeaking = false; }, 3000); }