diff --git a/animeCat.js b/animeCat.js index fddebbd..e69de29 100644 --- a/animeCat.js +++ b/animeCat.js @@ -1,555 +0,0 @@ -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', - blink: 'blink.png', - mouthOpen: 'mouth_open.png', - joy: 'joy.png', - mischievous: 'mischievous.png' - }, options.images); - - this.blinkMin = options.blinkMin ?? 5000; - this.blinkMax = options.blinkMax ?? 15000; - this.blinkDuration = options.blinkDuration ?? 175; - this.talkInterval = options.talkInterval ?? 300; - this._consolationCount = 0; - - this._isSpeaking = false; - this._blinkTimeout = null; - this._talkIntervalId = null; - this._speechTimeout = null; - this._mouthOpen = false; - this._pettingActive = false; - - this._createElements(); - this._bindMouseHold(); - this._startBlinking(); - } - - _createElements() { - // Outer wrapper (relativ für absolute Kinder!) - this.wrapper = document.createElement('div'); - Object.assign(this.wrapper.style, { - position: 'relative', - width: '100px', - height: '80px', - display: 'flex', - alignItems: 'flex-end', - zIndex: 1 - }); - - // --- Glow (unter der Katze) --- - this.glow = document.createElement('div'); - this.glow.id = 'cat-glow'; - Object.assign(this.glow.style, { - position: 'absolute', - left: '50%', - top: '50%', - transform: 'translate(-50%, -52%)', - borderRadius: '50%', - width: '120px', - height: '80px', - pointerEvents: 'none', - zIndex: 1, - transition: 'background 0.4s, width 0.2s, height 0.2s, opacity 0.3s' - }); - this.wrapper.appendChild(this.glow); - - // --- Cat image (über Glow) --- - this.img = document.createElement('img'); - this.img.src = this.images.default; - Object.assign(this.img.style, { - position: 'absolute', - left: '50%', - top: '50%', - transform: 'translate(-50%, -60%)', - width: '62px', - height: '50px', - zIndex: 2, - userSelect: 'none', - webkitUserSelect: 'none', - MozUserSelect: 'none', - msUserSelect: 'none', - webkitUserDrag: 'none', - pointerEvents: 'auto' - }); - this.img.draggable = false; - this.wrapper.appendChild(this.img); - - // --- Speech bubble (daneben, nicht überlagert) --- - this.bubble = document.createElement('div'); - Object.assign(this.bubble.style, { - position: 'relative', - marginLeft: '14px', - marginBottom: '11px', - padding: '10px 16px', - background: 'white', - border: '1px solid #ccc', - borderRadius: '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); - - // --- Wrapper komplettieren --- - this.wrapper.appendChild(this.bubble); - this.container.appendChild(this.wrapper); - - // --- Herz-Emitter (für Animationen) --- - this.heartEmitter = document.createElement('div'); - Object.assign(this.heartEmitter.style, { - position: 'absolute', - left: '82px', - bottom: '32px', - pointerEvents: 'none', - width: '1px', - height: '1px', - zIndex: 20 - }); - this.container.appendChild(this.heartEmitter); - } - - -// Hilfsfunktionen zur Farbinterpolation -_lerp(a, b, t) { return a + (b - a) * t; } -_lerpColor(a, b, t) { - return [ - Math.round(this._lerp(a[0], b[0], t)), - Math.round(this._lerp(a[1], b[1], t)), - Math.round(this._lerp(a[2], b[2], t)), - ]; -} - -// Animiert den Glow je nach Commit-Anzahl -animateCatGlow(commitCount) { - const glow = this.glow; - if (!glow) return; - - // Glow-Farbstufen für die Commit-Bereiche - const stops = [ - { c: 0, color: [60, 230, 100], opacity: 0.00 }, // unsichtbar → grün - { c: 10, color: [60, 230, 100], opacity: 0.25 }, // grün sichtbar - { c: 50, color: [100, 180, 255], opacity: 0.40 }, // grün → blau - { c: 100, color: [180, 120, 255], opacity: 0.55 }, // blau → lila - { c: 500, color: [255, 180, 60], opacity: 0.90 }, // lila → orange - ]; - - // Richtige Interpolationsstufe finden: - let lower = stops[0], upper = stops[stops.length - 1]; - for (let i = 0; i < stops.length - 1; ++i) { - if (commitCount >= stops[i].c && commitCount < stops[i+1].c) { - lower = stops[i]; - upper = stops[i+1]; - break; - } - } - const range = upper.c - lower.c || 1; - const t = Math.min(Math.max((commitCount - lower.c) / range, 0), 1); - - // Farbe und Opazität sanft interpolieren - const [r, g, b] = this._lerpColor(lower.color, upper.color, t); - const opacity = this._lerp(lower.opacity, upper.opacity, t); - - // Größe interpolieren (0–500 Commits) - const minSize = 80, maxSize = 170; - const sizeFactor = Math.min(commitCount / 500, 1); - const size = minSize + sizeFactor * (maxSize - minSize); - - // Styles setzen - glow.style.width = `${size}px`; - glow.style.height = `${size}px`; // immer Kreis! - glow.style.opacity = opacity; - glow.style.background = `radial-gradient(circle, rgba(${r},${g},${b},0.85) 0%, rgba(${r},${g},${b},0.14) 70%, rgba(0,0,0,0) 100%)`; -} - - // Bubble-Position absolut anpassen, wenn detached - _positionBubbleDetached() { - // Katze relativ im Container finden - const catRect = this.img.getBoundingClientRect(); - const contRect = this.container.getBoundingClientRect(); - // "Andockpunkt": rechts neben der Katze, leicht versetzt nach oben - const left = (catRect.right - contRect.left) + 12; - const bottom = (contRect.bottom - catRect.bottom) + 8; - Object.assign(this.bubble.style, { - position: 'absolute', - left: `${left}px`, - bottom: `${bottom}px`, - marginLeft: '0', - marginBottom: '0' - }); - // Tail bleibt am linken Rand der Bubble! - this.bubblePointer.style.left = '-13px'; - this.bubblePointer.style.bottom = '12px'; - } - _resetBubbleAttach() { - Object.assign(this.bubble.style, { - position: 'relative', - left: '', - bottom: '', - marginLeft: '14px', - marginBottom: '11px' - }); - this.bubblePointer.style.left = '-13px'; - this.bubblePointer.style.bottom = '12px'; - } - - _startBlinking() { - const delay = this.blinkMin + Math.random() * (this.blinkMax - this.blinkMin); - this._blinkTimeout = setTimeout(() => { - if (!this._isSpeaking) { - this.img.src = this.images.blink; - setTimeout(() => { - if (!this._pettingActive) { - this.img.src = this.images.default; - } - this._startBlinking(); - }, this.blinkDuration); - } else { - this._startBlinking(); - } - }, delay); - } - - _bindMouseHold() { - let holdTimer = null; - let joyActive = false; - let mouseDown = false; - let mouseDownAt = null; - let lastPos = null; - let moveDist = 0; - - const CAT_TOLERANCE = 15; - const MOVE_THRESHOLD = 350; - - const isMouseNearCat = (e) => { - const rect = this.img.getBoundingClientRect(); - return ( - e.clientX >= rect.left - CAT_TOLERANCE && - e.clientX <= rect.right + CAT_TOLERANCE && - e.clientY >= rect.top - CAT_TOLERANCE && - e.clientY <= rect.bottom + CAT_TOLERANCE - ); - }; - - const closeEyes = () => { this.img.src = this.images.eyesClosed; }; - const reopenEyes = () => { - if (!joyActive && !this._isSpeaking && !this._pettingActive) this.img.src = this.images.default; - }; - - this.img.addEventListener('mousedown', (e) => { - if (this._isSpeaking || joyActive) return; - if (!isMouseNearCat(e)) return; - this._pettingActive = true; - - mouseDown = true; - mouseDownAt = Date.now(); - lastPos = { x: e.clientX, y: e.clientY }; - moveDist = 0; - closeEyes(); - clearTimeout(this._blinkTimeout); // Blinzeln pausieren - - function onMove(ev) { - if (!mouseDown) return; - if (!isMouseNearCat(ev)) { - cleanup(); - reopenEyes(); - mouseDown = false; - this._startBlinking(); - return; - } - if (lastPos) { - const dx = ev.clientX - lastPos.x; - const dy = ev.clientY - lastPos.y; - moveDist += Math.sqrt(dx * dx + dy * dy); - lastPos = { x: ev.clientX, y: ev.clientY }; - } - } - - const onMoveBound = onMove.bind(this); - - holdTimer = setTimeout(() => { - if (!mouseDown) return; // Schon abgebrochen - if (moveDist >= MOVE_THRESHOLD) { - cleanup(); - joyActive = true; - this._runJoyAnimation(() => { - joyActive = false; - reopenEyes(); - this._startBlinking(); - }); - mouseDown = false; - } else { - this._consolation(mouseDownAt); - mouseDown = false; - } - }, 4000); - - function onUp() { - if (!mouseDown) return; - cleanup(); - const heldFor = Date.now() - mouseDownAt; - if (heldFor >= 4000) return; // already handled by timer above - if (heldFor >= 4000 && moveDist >= MOVE_THRESHOLD) { - joyActive = true; - this._runJoyAnimation(() => { - joyActive = false; - reopenEyes(); - this._startBlinking(); - }); - } else if (heldFor > 1000) { - this._consolation(mouseDownAt); - } else { - reopenEyes(); - this._startBlinking(); - } - mouseDown = false; - } - - const onUpBound = onUp.bind(this); - - const cleanup = () => { - window.removeEventListener('mousemove', onMoveBound); - window.removeEventListener('mouseup', onUpBound); - if (holdTimer) clearTimeout(holdTimer); - holdTimer = null; - this._pettingActive = false; - }; - window.addEventListener('mousemove', onMoveBound); - window.addEventListener('mouseup', onUpBound); - }); - } - - _consolation(mouseDownAt) { - // Trostpreis: Augen auf, dann mouth_open oder mischievous - this._pettingActive = false; - this.img.src = this.images.default; - const heldFor = Date.now() - mouseDownAt; - if (heldFor > 1000) { - const mouthOpenTime = Math.max(heldFor - 1000, 1000); - let imgToShow = this.images.mouthOpen || this.images.default; - if (this._consolationCount >= 3) { - if ( - this._consolationCount === 3 || - Math.random() < 0.2 - ) { - imgToShow = this.images.mischievous || imgToShow; - } - } - this.img.src = imgToShow; - this._consolationCount++; - this._startBlinking(); - setTimeout(() => { - if (!this._isSpeaking && !this._pettingActive) { - this.img.src = this.images.default; - } - }, mouthOpenTime); - } else { - this._startBlinking(); - } - } - -_runJoyAnimation(onFinish) { - const img = this.img; - const origTransition = img.style.transition; - const origTransform = img.style.transform; - const origOrigin = img.style.transformOrigin; - - img.src = this.images.joy || this.images.default; - - // 20% Chance auf Salto! - const salto = Math.random() < 0.2; - - // Bubble abkoppeln wie gehabt - this.container.appendChild(this.bubble); - this._positionBubbleDetached(); - - // Herz-Emitter richtig platzieren - const catRect = this.img.getBoundingClientRect(); - const contRect = this.container.getBoundingClientRect(); - // <-- Hier kannst du x/y anpassen! - const emitterX = catRect.right - contRect.left + 16; // <-- X-Versatz, mehr = weiter rechts - const emitterY = contRect.bottom - catRect.bottom + 40; // <-- Y-Versatz, mehr = weiter oben - this.heartEmitter.style.left = emitterX + 'px'; - this.heartEmitter.style.bottom = emitterY + 'px'; - - const spawnHearts = () => this._spawnHearts(12); - - if (salto) { - // Setzt das Zentrum der Drehung auf die Bildmitte - img.style.transformOrigin = '50% 70%'; - // Salto und Sprung gleichzeitig - img.style.transition = 'transform 1.2s cubic-bezier(.19,1,.22,1)'; - img.style.transform = 'translateY(-40px) rotate(360deg)'; - spawnHearts(); - - setTimeout(() => { - // Nur runterfallen, Drehung bleibt auf 360° - img.style.transition = 'transform 0.8s cubic-bezier(.19,1,.22,1)'; - img.style.transform = 'translateY(0) rotate(360deg)'; - img.src = this.images.mouthOpen || this.images.default; - setTimeout(() => { - if (!this._pettingActive) img.src = this.images.default; - setTimeout(() => { - img.style.transition = origTransition || ''; - img.style.transform = origTransform || ''; - img.style.transformOrigin = origOrigin || ''; - this.wrapper.appendChild(this.bubble); - this._resetBubbleAttach(); - if (typeof onFinish === 'function') onFinish(); - }, 700); - }, 2000); - }, 1200); // Erst springen + drehen, dann runterfallen - } else { - // Normale Joy-Animation - img.style.transformOrigin = '50% 70%'; - img.style.transition = 'transform 0.6s cubic-bezier(.19,1,.22,1)'; - img.style.transform = 'translateY(-40px) rotate(12deg)'; - setTimeout(() => { - spawnHearts(); - setTimeout(() => { - img.style.transition = 'transform 0.8s cubic-bezier(.19,1,.22,1)'; - img.style.transform = 'translateY(0) rotate(0deg)'; - img.src = this.images.mouthOpen || this.images.default; - setTimeout(() => { - if (!this._pettingActive) img.src = this.images.default; - setTimeout(() => { - img.style.transition = origTransition || ''; - img.style.transform = origTransform || ''; - img.style.transformOrigin = origOrigin || ''; - this.wrapper.appendChild(this.bubble); - this._resetBubbleAttach(); - if (typeof onFinish === 'function') onFinish(); - }, 700); - }, 2000); - }, 2200); - }, 400); - } -} - - _spawnHearts(count = 10) { - for (let i = 0; i < count; ++i) { - setTimeout(() => this._makeHeart(), Math.random() * 300); - } - } - - _makeHeart() { - const emoji = Math.random() < 0.7 ? '❤️' : '💕'; - - const heart = document.createElement('span'); - heart.textContent = emoji; - heart.style.position = 'absolute'; - heart.style.left = '0%'; - heart.style.bottom= '0%'; - heart.style.fontSize = `${16 + Math.random() * 14}px`; - heart.style.pointerEvents = 'none'; - heart.style.opacity = '0.9'; - heart.style.zIndex = 100; - - // Start/End-Pos, Flugwinkel - const angle = (Math.random() * Math.PI) - (Math.PI/2); // spread -90° to 90° - const distance = 60 + Math.random() * 45; - const dx = Math.cos(angle) * distance; - const dy = Math.sin(angle) * distance; - - heart.animate([ - { - transform: 'translate(-50%, 0) scale(1)', - opacity: 0.95 - }, - { - transform: `translate(calc(-50% + ${dx}px), ${-dy}px) scale(${1.3 + Math.random()*0.4}) rotate(${Math.random()*60-30}deg)`, - opacity: 0.3 - } - ], { - duration: 1100 + Math.random()*800, - easing: 'cubic-bezier(.28,1.01,.57,.99)' - }); - - // Remove after animation - setTimeout(() => heart.remove(), 1600); - - // Im Herzen-Emitter platzieren! - this.heartEmitter.appendChild(heart); - } - - /** 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'; - Array.from(this.bubble.childNodes).forEach(node => { - if (node !== this.bubblePointer) node.remove(); - }); - 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); - } - - appendSpeech(chunk) { - if (this._bubbleTextNode) - this._bubbleTextNode.textContent += chunk; - } - - endSpeech() { - clearInterval(this._talkIntervalId); - if (!this._pettingActive) { - this.img.src = this.images.default; - } - this._speechTimeout = setTimeout(() => { - this.bubble.style.opacity = '0'; - this._isSpeaking = false; - }, 6000); - } - - destroy() { - clearTimeout(this._blinkTimeout); - clearInterval(this._talkIntervalId); - clearTimeout(this._speechTimeout); - this.wrapper.remove(); - this.heartEmitter.remove(); - } -} \ No newline at end of file