1
0

auto-git:

[change] animeCat.js
This commit is contained in:
2025-05-26 05:35:44 +02:00
parent 2aa71895b0
commit 2d91ba6f82

View File

@@ -15,12 +15,12 @@ window.AnimeCat = class AnimeCat {
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;
@@ -28,7 +28,6 @@ window.AnimeCat = class AnimeCat {
this._speechTimeout = null;
this._mouthOpen = false;
this._pettingActive = false;
this._consolationCount = 0;
this._createElements();
this._bindMouseHold();
@@ -44,7 +43,7 @@ window.AnimeCat = class AnimeCat {
flexDirection: 'row',
alignItems: 'flex-end',
minHeight: '60px',
minWidth: '180px', // Passe an falls du willst
minWidth: '180px',
zIndex: '1'
});
@@ -64,13 +63,13 @@ window.AnimeCat = class AnimeCat {
// --- Speech bubble ---
this.bubble = document.createElement('div');
Object.assign(this.bubble.style, {
position: 'relative', // jetzt relativ zur wrapper-Flexbox
position: 'relative',
marginLeft: '14px',
marginBottom: '11px',
padding: '10px 16px',
background: 'white',
border: '1px solid #ccc',
borderRadius: '16px 16px 16px 16px',
borderRadius: '16px',
boxShadow: '0 2px 8px rgba(0,0,0,0.18)',
opacity: '0',
transition: 'opacity 0.3s',
@@ -108,17 +107,49 @@ window.AnimeCat = class AnimeCat {
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();
// Emitter für Herzen (abgekoppelt!)
this.heartEmitter = document.createElement('div');
Object.assign(this.heartEmitter.style, {
position: 'absolute',
left: '82px', // rechts neben der Katze
bottom: '32px',
pointerEvents: 'none',
width: '1px',
height: '1px',
zIndex: 20
});
this.container.appendChild(this.heartEmitter);
}
// 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';
// 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() {
@@ -138,49 +169,13 @@ window.AnimeCat = class AnimeCat {
}, delay);
}
_consolation({ cleanup, mouseDown, mouseDownAt, reopenEyes, joyActive }) {
cleanup();
mouseDown = false;
reopenEyes();
const heldFor = Date.now() - mouseDownAt;
if (heldFor > 1000) {
// mind. 1000ms oder (heldFor - 1000), je nachdem was größer ist
const mouthOpenTime = Math.max(heldFor - 1000, 1000);
// Bildwahl: mischievous ab 4. Mal, sonst mouth_open
let imgToShow = this.images.mouthOpen || this.images.default;
if (this._consolationCount >= 3) {
// Ab dem vierten Mal: mischievous immer, danach 20% Chance
if (
this._consolationCount === 3 ||
Math.random() < 0.2
) {
imgToShow = this.images.mischievous || imgToShow;
}
}
this.img.src = imgToShow;
this._consolationCount++;
this._startBlinking();
setTimeout(() => {
if (!joyActive && !this._isSpeaking && !this._pettingActive) {
this.img.src = this.images.default;
}
}, mouthOpenTime);
} else {
this._startBlinking();
}
}
_bindMouseHold() {
let holdTimer = null;
let joyTimeout = null;
let joyActive = false;
let mouseDown = false; // <---- NEU! Initialisieren
let mouseDownAt = null; // <---- NEU!
let lastPos = null; // <---- NEU!
let moveDist = 0; // <---- NEU!
let mouseDown = false;
let mouseDownAt = null;
let lastPos = null;
let moveDist = 0;
const CAT_TOLERANCE = 15;
const MOVE_THRESHOLD = 350;
@@ -212,7 +207,6 @@ _bindMouseHold() {
closeEyes();
clearTimeout(this._blinkTimeout); // Blinzeln pausieren
// Bewegung tracken
function onMove(ev) {
if (!mouseDown) return;
if (!isMouseNearCat(ev)) {
@@ -234,8 +228,6 @@ _bindMouseHold() {
holdTimer = setTimeout(() => {
if (!mouseDown) return; // Schon abgebrochen
// Joy-Bedingung
if (moveDist >= MOVE_THRESHOLD) {
cleanup();
joyActive = true;
@@ -246,14 +238,8 @@ _bindMouseHold() {
});
mouseDown = false;
} else {
// Trostpreis: Augen auf, dann mouth_open oder mischievous
this._consolation({
cleanup,
mouseDown,
mouseDownAt,
reopenEyes,
joyActive
});
this._consolation(mouseDownAt);
mouseDown = false;
}
}, 4000);
@@ -262,7 +248,6 @@ _bindMouseHold() {
cleanup();
const heldFor = Date.now() - mouseDownAt;
if (heldFor >= 4000) return; // already handled by timer above
// Joy: wenn beide Bedingungen erfüllt
if (heldFor >= 4000 && moveDist >= MOVE_THRESHOLD) {
joyActive = true;
this._runJoyAnimation(() => {
@@ -270,15 +255,12 @@ _bindMouseHold() {
reopenEyes();
this._startBlinking();
});
} else if (heldFor > 1000) {
this._consolation(mouseDownAt);
} else {
reopenEyes();
this._startBlinking();
}
// Trostpreis: Augen auf, dann mouth_open oder mischievous
this._consolation({
cleanup,
mouseDown,
mouseDownAt,
reopenEyes,
joyActive
});
mouseDown = false;
}
@@ -289,100 +271,123 @@ _bindMouseHold() {
window.removeEventListener('mouseup', onUpBound);
if (holdTimer) clearTimeout(holdTimer);
holdTimer = null;
this._pettingActive = false; // Korrekt!
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 wrapper = this.wrapper;
const img = this.img;
const container = this.container;
const origWrapperTransition = wrapper.style.transition;
const origWrapperTransform = wrapper.style.transform;
const origImgTransition = img.style.transition;
const origImgTransform = img.style.transform;
const origTransition = wrapper.style.transition;
const origTransform = wrapper.style.transform;
// Bild auf joy.png setzen:
img.src = this.images.joy || this.images.default;
// 20% Chance auf Salto!
const salto = Math.random() < 0.2;
const upTime = 600;
const hangTime = salto ? 500 : 1800;
const downTime = 800;
// Herzen erzeugen, aber referenzen speichern
const hearts = [];
for (let i = 0; i < 12; ++i) {
// Bubble abkoppeln
const bubbleDetached = true;
if (bubbleDetached) {
this.container.appendChild(this.bubble);
this._positionBubbleDetached();
}
// Herzen-Emitter abkoppeln und passend setzen
const catRect = this.img.getBoundingClientRect();
const contRect = this.container.getBoundingClientRect();
this.heartEmitter.style.left = (catRect.right - contRect.left + 15) + 'px';
this.heartEmitter.style.bottom = (contRect.bottom - catRect.bottom + 12) + 'px';
// Herzchen-Explosion NACH dem Sprung (also im Peak)!
const spawnHearts = () => this._spawnHearts(12);
if (salto) {
// Salto: beim Springen schon drehen!
wrapper.style.transition = 'transform 0.8s cubic-bezier(.19,1,.22,1)';
wrapper.style.transform = 'translateY(-40px) rotate(0deg)';
setTimeout(() => {
const heart = this._makeHeart();
hearts.push(heart);
}, Math.random() * 300);
}
// Sprung + Drehung sofort starten
wrapper.style.transition = `transform ${upTime}ms cubic-bezier(.19,1,.22,1)`;
wrapper.style.transform = 'translateY(-40px)';
img.style.transition = `transform ${upTime}ms cubic-bezier(.19,1,.22,1)`;
img.style.transform = salto ? 'rotate(360deg)' : 'rotate(12deg)';
wrapper.style.transition = 'transform 0.6s cubic-bezier(.19,1,.22,1)';
wrapper.style.transform = 'translateY(-40px) rotate(360deg)';
spawnHearts();
setTimeout(() => {
// Bubble abkoppeln (wie gehabt)
if (this.bubble.parentNode === wrapper) {
container.appendChild(this.bubble);
this.bubble.style.position = 'absolute';
this.bubble.style.left = wrapper.offsetLeft + wrapper.offsetWidth + 10 + 'px';
this.bubble.style.bottom = '10px';
this.bubble.style.zIndex = '100';
}
// **Herzen abkoppeln**
for (const heart of hearts) {
if (heart && heart.parentNode === wrapper) {
// Absolut zu Cat-Container platzieren (fixiere ihre aktuelle Position!)
const rect = heart.getBoundingClientRect();
const parentRect = container.getBoundingClientRect();
heart.style.left = (rect.left - parentRect.left) + "px";
heart.style.bottom = (parentRect.bottom - rect.bottom) + "px";
heart.style.position = "absolute";
container.appendChild(heart);
}
}
// Ab jetzt fällt nur noch die Katze
wrapper.style.transition = `transform ${downTime}ms cubic-bezier(.19,1,.22,1)`;
wrapper.style.transform = 'translateY(0)';
wrapper.style.transition = 'transform 0.8s cubic-bezier(.19,1,.22,1)';
wrapper.style.transform = 'translateY(0) rotate(360deg)';
img.src = this.images.mouthOpen || this.images.default;
if (!salto) {
img.style.transition = `transform ${downTime}ms cubic-bezier(.19,1,.22,1)`;
img.style.transform = 'rotate(0deg)';
}
setTimeout(() => {
if (!this._pettingActive) {
this.img.src = this.images.default;
}
setTimeout(() => {
// Bubble wieder ankoppeln, falls nötig
if (this.bubble.parentNode === container) {
wrapper.appendChild(this.bubble);
this.bubble.style.position = '';
this.bubble.style.left = '';
this.bubble.style.bottom = '';
this.bubble.style.zIndex = '';
}
// Styles zurücksetzen
wrapper.style.transition = origWrapperTransition || '';
wrapper.style.transform = origWrapperTransform || '';
img.style.transition = origImgTransition || '';
img.style.transform = origImgTransform || '';
wrapper.style.transition = origTransition || '';
wrapper.style.transform = origTransform || '';
// Bubble und Herzen zurück
this.wrapper.appendChild(this.bubble);
this._resetBubbleAttach();
if (typeof onFinish === 'function') onFinish();
}, 700);
}, downTime);
}, upTime + hangTime);
}, 2000);
}, 600);
}, 400);
} else {
// Normale Joy-Animation: leicht drehen
wrapper.style.transition = 'transform 0.6s cubic-bezier(.19,1,.22,1)';
wrapper.style.transform = 'translateY(-40px) rotate(12deg)';
setTimeout(() => {
spawnHearts();
setTimeout(() => {
wrapper.style.transition = 'transform 0.8s cubic-bezier(.19,1,.22,1)';
wrapper.style.transform = 'translateY(0) rotate(0deg)';
img.src = this.images.mouthOpen || this.images.default;
setTimeout(() => {
if (!this._pettingActive) {
this.img.src = this.images.default;
}
setTimeout(() => {
wrapper.style.transition = origTransition || '';
wrapper.style.transform = origTransform || '';
// Bubble zurück
this.wrapper.appendChild(this.bubble);
this._resetBubbleAttach();
if (typeof onFinish === 'function') onFinish();
}, 700);
}, 2000);
}, 2200);
}, 400);
}
}
_spawnHearts(count = 10) {
@@ -393,27 +398,16 @@ _runJoyAnimation(onFinish) {
_makeHeart() {
const emoji = Math.random() < 0.7 ? '❤️' : '💕';
const heart = document.createElement('span');
heart.textContent = emoji;
heart.style.position = 'absolute';
const catRect = this.img.getBoundingClientRect();
const containerRect = this.container.getBoundingClientRect();
// >>> HIER Offset nach rechts (z.B. +18px oder +25px ausprobieren)
const HEART_X_OFFSET = 36; // nach rechts relativ zur Katzenmitte
const left = (catRect.left + catRect.width * 0.5 + HEART_X_OFFSET) - containerRect.left;
// Oder noch präziser: Mundhöhe (etwas tiefer als Mittelpunkt)
const HEART_Y_OFFSET = 0.45; // Prozentuale Höhe der Katze (0.5 = Mitte)
const bottom = (containerRect.bottom - (catRect.top + catRect.height * HEART_Y_OFFSET));
heart.style.left = `${left}px`;
heart.style.bottom = `${bottom}px`;
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 = 1000;
heart.style.zIndex = 100;
// Start/End-Pos, Flugwinkel
const angle = (Math.random() * Math.PI) - (Math.PI/2); // spread -90° to 90°
@@ -435,12 +429,11 @@ _makeHeart() {
easing: 'cubic-bezier(.28,1.01,.57,.99)'
});
// Remove after animation
setTimeout(() => heart.remove(), 1600);
// Jetzt direkt in den Container anhängen (NICHT an this.wrapper!)
this.container.appendChild(heart);
return heart;
// Im Herzen-Emitter platzieren!
this.heartEmitter.appendChild(heart);
}
/** Call when streaming text begins */
@@ -453,11 +446,9 @@ _makeHeart() {
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);
@@ -469,13 +460,11 @@ _makeHeart() {
}, 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);
if (!this._pettingActive) {
@@ -487,11 +476,11 @@ _makeHeart() {
}, 6000);
}
/** Clean up timers & DOM */
destroy() {
clearTimeout(this._blinkTimeout);
clearInterval(this._talkIntervalId);
clearTimeout(this._speechTimeout);
this.wrapper.remove();
this.heartEmitter.remove();
}
}