clauddy 1.28.3 → 1.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -7
- package/main.js +39 -2
- package/package.json +1 -1
- package/preload.js +2 -1
- package/renderer/index.html +169 -127
- package/renderer/pet.js +224 -22
- package/renderer/pets.css +916 -0
- package/renderer/pets.js +579 -0
- package/renderer/style.css +354 -94
package/renderer/pet.js
CHANGED
|
@@ -51,6 +51,50 @@ const SPRITE = [
|
|
|
51
51
|
})
|
|
52
52
|
})()
|
|
53
53
|
|
|
54
|
+
// ---- selectable pets (pets.js) ----------------------------------------------
|
|
55
|
+
// The classic sprite stays in #claude; another pet is drawn inside it as #skin,
|
|
56
|
+
// so every mood animation on #claude (sleep bob, slump, poke…) still applies.
|
|
57
|
+
// Working swaps the shared hop for the pet's own move (pets.css).
|
|
58
|
+
const SKIN_ID = 'skin'
|
|
59
|
+
let shownPet = null
|
|
60
|
+
let pickedPet = null // the Settings selection, previewed live until Save/Cancel
|
|
61
|
+
function showPet(id) {
|
|
62
|
+
if (id === shownPet) return
|
|
63
|
+
shownPet = id
|
|
64
|
+
el(SKIN_ID)?.remove()
|
|
65
|
+
const b = document.body.classList
|
|
66
|
+
for (const c of [...b]) if (c.startsWith('pet-move-')) b.remove(c)
|
|
67
|
+
const pet = Pets.PETS.find((p) => p.id === id)
|
|
68
|
+
el('claude').classList.toggle('skinned', !!pet)
|
|
69
|
+
if (!pet) return
|
|
70
|
+
el('claude').insertAdjacentHTML(
|
|
71
|
+
'beforeend',
|
|
72
|
+
Pets.petMarkup(
|
|
73
|
+
id,
|
|
74
|
+
'sk',
|
|
75
|
+
`id="${SKIN_ID}" x="-8" y="-26" width="116" height="116" preserveAspectRatio="xMidYMax meet"`,
|
|
76
|
+
),
|
|
77
|
+
)
|
|
78
|
+
b.add(`pet-move-${pet.move}`)
|
|
79
|
+
}
|
|
80
|
+
const chosenPet = () => currentConfig.pet || Pets.DEFAULT_PET
|
|
81
|
+
function refreshPet() {
|
|
82
|
+
showPet(pickedPet ?? Pets.activePet(chosenPet()))
|
|
83
|
+
}
|
|
84
|
+
// holidays start and end at midnight, with the widget open
|
|
85
|
+
setInterval(refreshPet, 60_000)
|
|
86
|
+
// What the eyes follow: the sprite's eye group, or the pet's (its blink groups,
|
|
87
|
+
// moved with `translate` so it composes with the blink's own transform). The
|
|
88
|
+
// pet's viewBox is about twice the sprite's, so its units are half as big.
|
|
89
|
+
function lookTargets() {
|
|
90
|
+
const skin = el(SKIN_ID)
|
|
91
|
+
return skin ? [...skin.querySelectorAll('.blink')] : [el('eyes')]
|
|
92
|
+
}
|
|
93
|
+
function lookAt(x, y) {
|
|
94
|
+
const skin = el(SKIN_ID)
|
|
95
|
+
if (!skin) return el('eyes').setAttribute('transform', `translate(${x} ${y})`)
|
|
96
|
+
for (const g of lookTargets()) g.style.translate = `${x * 2}px ${y * 2}px`
|
|
97
|
+
}
|
|
54
98
|
// night scene backdrop (clouds, crescent moon, stars, dotted ground/sky)
|
|
55
99
|
;(function buildScene() {
|
|
56
100
|
const s = el('scene')
|
|
@@ -950,8 +994,34 @@ const activityTracker = Companion.createActivityTracker()
|
|
|
950
994
|
function setPetPeek(on) {
|
|
951
995
|
if (!document.body.classList.contains('pet-only')) return
|
|
952
996
|
clearTimeout(peekTimer)
|
|
997
|
+
// placed once as it appears: re-placing on every click or focus made it jump
|
|
998
|
+
if (on && !document.body.classList.contains('pet-peek')) placeGlance()
|
|
953
999
|
document.body.classList.toggle('pet-peek', on)
|
|
954
1000
|
}
|
|
1001
|
+
// The glance bubble sits just above the pet's head, and every pet's head is at
|
|
1002
|
+
// its own height (the bat's ears, the wizard's hat), so measure the drawing's
|
|
1003
|
+
// top instead of a fixed spot. Parts that only show while working (the owl,
|
|
1004
|
+
// sparks, snow) don't count: they come and go. The pet is mid-animation (a
|
|
1005
|
+
// bob, a hop, levitating), so the head is measured against #claude itself and
|
|
1006
|
+
// laid on #claude's resting spot, at the foot of the still #pet box.
|
|
1007
|
+
function placeGlance() {
|
|
1008
|
+
const skin = el(SKIN_ID)
|
|
1009
|
+
const parts = skin
|
|
1010
|
+
? [...skin.children].filter((c) => !c.matches('defs, .only-work'))
|
|
1011
|
+
: [el('body')]
|
|
1012
|
+
const tops = parts
|
|
1013
|
+
.map((part) => part.getBoundingClientRect())
|
|
1014
|
+
.filter((r) => r.width > 0)
|
|
1015
|
+
.map((r) => r.top)
|
|
1016
|
+
if (!tops.length) return
|
|
1017
|
+
const zoom = Number.parseFloat(document.body.style.zoom) || 1
|
|
1018
|
+
const sprite = el('claude').getBoundingClientRect()
|
|
1019
|
+
const resting = el('pet').getBoundingClientRect().bottom - sprite.height
|
|
1020
|
+
const head =
|
|
1021
|
+
(Math.min(...tops) - sprite.top + resting - el('card').getBoundingClientRect().top) / zoom
|
|
1022
|
+
const glance = el('pet-glance')
|
|
1023
|
+
glance.style.top = `${Math.max(2, Math.round(head - glance.offsetHeight - 10))}px`
|
|
1024
|
+
}
|
|
955
1025
|
function setDisplaySize(size, remember = true) {
|
|
956
1026
|
if (!['expanded', 'compact', 'pet'].includes(size)) size = 'expanded'
|
|
957
1027
|
if (currentConfig.mode === 'menubar' && size === 'pet') size = 'compact'
|
|
@@ -1026,6 +1096,9 @@ function paintCompanion(view) {
|
|
|
1026
1096
|
button.setAttribute('aria-pressed', String(provider === view))
|
|
1027
1097
|
button.classList.toggle('urgent', session?.pct >= warnAt())
|
|
1028
1098
|
el(`glance-${provider}-value`).textContent = pctText(session?.pct)
|
|
1099
|
+
// the bar under each reading fills with the session %; none when unknown
|
|
1100
|
+
const pct = session?.pct
|
|
1101
|
+
button.style.setProperty('--pct', pct == null ? '0' : String(Math.max(0, Math.min(100, pct))))
|
|
1029
1102
|
const stale = provider !== 'claude' && readingStale(dataOf(provider))
|
|
1030
1103
|
button.title = stale
|
|
1031
1104
|
? 'Last recorded usage · waiting for a fresh reading'
|
|
@@ -1100,15 +1173,18 @@ function reactToProvider(provider, data) {
|
|
|
1100
1173
|
if (!window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) {
|
|
1101
1174
|
// in tab order: Claude to the left, Codex to the right, Cursor down-right
|
|
1102
1175
|
const [x, y] = { claude: [-3, 0], codex: [3, 0], cursor: [3, 1] }[provider] || [0, 0]
|
|
1103
|
-
el(
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1176
|
+
const k = el(SKIN_ID) ? 2 : 1
|
|
1177
|
+
for (const target of lookTargets()) {
|
|
1178
|
+
target.animate(
|
|
1179
|
+
[
|
|
1180
|
+
{ translate: '0 0' },
|
|
1181
|
+
{ translate: `${x * k}px ${y * k}px`, offset: 0.3 },
|
|
1182
|
+
{ translate: `${x * k}px ${y * k}px`, offset: 0.7 },
|
|
1183
|
+
{ translate: '0 0' },
|
|
1184
|
+
],
|
|
1185
|
+
{ duration: 1100, easing: 'ease-in-out' },
|
|
1186
|
+
)
|
|
1187
|
+
}
|
|
1112
1188
|
}
|
|
1113
1189
|
}
|
|
1114
1190
|
|
|
@@ -1363,7 +1439,9 @@ function paint() {
|
|
|
1363
1439
|
// fit the window to the content (no leftover border)
|
|
1364
1440
|
let lastH = 0
|
|
1365
1441
|
let lastW = 0
|
|
1366
|
-
|
|
1442
|
+
// `animate` eases the window to its new size (opening and closing Settings);
|
|
1443
|
+
// every other resize is immediate, so the widget tracks its content exactly
|
|
1444
|
+
function fitSize(animate = false) {
|
|
1367
1445
|
requestAnimationFrame(() => {
|
|
1368
1446
|
const collapsed = document.body.classList.contains('collapsed')
|
|
1369
1447
|
const zoom = Number.parseFloat(document.body.style.zoom) || 1
|
|
@@ -1372,7 +1450,7 @@ function fitSize() {
|
|
|
1372
1450
|
const dual = document.body.classList.contains('dual')
|
|
1373
1451
|
const trio = document.body.classList.contains('trio')
|
|
1374
1452
|
const petOnly = document.body.classList.contains('pet-only')
|
|
1375
|
-
const w = (petOnly ? (trio ? 212 : 168) : collapsed ? (dual ? 240 :
|
|
1453
|
+
const w = (petOnly ? (trio ? 212 : 168) : collapsed ? (dual ? 240 : 170) : 304) * zoom
|
|
1376
1454
|
// offsetHeight never reflects a CSS zoom applied to an ancestor (confirmed
|
|
1377
1455
|
// empirically against this Electron build) — so measure the unzoomed content
|
|
1378
1456
|
// height, then scale the whole thing (content + margin) ourselves
|
|
@@ -1380,7 +1458,7 @@ function fitSize() {
|
|
|
1380
1458
|
if (Math.abs(h - lastH) > 2 || w !== lastW) {
|
|
1381
1459
|
lastH = h
|
|
1382
1460
|
lastW = w
|
|
1383
|
-
window.api.resize(w, h)
|
|
1461
|
+
window.api.resize(w, h, animate)
|
|
1384
1462
|
}
|
|
1385
1463
|
})
|
|
1386
1464
|
}
|
|
@@ -1404,6 +1482,8 @@ window.api.onDebugState((o) => {
|
|
|
1404
1482
|
if (lastData) render(lastData)
|
|
1405
1483
|
})
|
|
1406
1484
|
window.api.onUsage(render)
|
|
1485
|
+
// menu-bar popover: where its nib sits, so it points at the tray icon
|
|
1486
|
+
window.api.onPopoverAnchor((x) => document.body.style.setProperty('--nib-x', `${x}px`))
|
|
1407
1487
|
window.api.onError((msg) => {
|
|
1408
1488
|
el('status-text').textContent = 'error'
|
|
1409
1489
|
console.error(msg)
|
|
@@ -1428,6 +1508,7 @@ window.api.onConfig((cfg) => {
|
|
|
1428
1508
|
setDisplaySize('compact', false)
|
|
1429
1509
|
}
|
|
1430
1510
|
heardConfig = true
|
|
1511
|
+
refreshPet()
|
|
1431
1512
|
paintMute()
|
|
1432
1513
|
paint()
|
|
1433
1514
|
applyZoom(currentConfig.zoom)
|
|
@@ -1946,6 +2027,7 @@ function snapshotSettings() {
|
|
|
1946
2027
|
zoom: el('set-zoom').value,
|
|
1947
2028
|
talk: el('set-talk').checked,
|
|
1948
2029
|
sound: el('set-sound').checked,
|
|
2030
|
+
pet: pickedPet,
|
|
1949
2031
|
})
|
|
1950
2032
|
}
|
|
1951
2033
|
function refreshSaveDirty() {
|
|
@@ -1954,12 +2036,19 @@ function refreshSaveDirty() {
|
|
|
1954
2036
|
const b = el('set-save')
|
|
1955
2037
|
b.classList.toggle('dirty', dirty)
|
|
1956
2038
|
b.disabled = !dirty // nothing to save, nothing to click
|
|
2039
|
+
paintSettingsFooter(dirty)
|
|
2040
|
+
}
|
|
2041
|
+
// nothing changed: a single Done; once something has, Cancel and Save
|
|
2042
|
+
function paintSettingsFooter(dirty) {
|
|
2043
|
+
el('settings').classList.toggle('dirty', dirty)
|
|
2044
|
+
el('set-cancel').textContent = dirty ? 'Cancel' : 'Done'
|
|
1957
2045
|
}
|
|
1958
2046
|
function clearSaveDirty() {
|
|
1959
2047
|
settingsBaseline = snapshotSettings()
|
|
1960
2048
|
const b = el('set-save')
|
|
1961
2049
|
b.classList.remove('dirty')
|
|
1962
2050
|
b.disabled = true
|
|
2051
|
+
paintSettingsFooter(false)
|
|
1963
2052
|
}
|
|
1964
2053
|
function reflectAlertsOn() {
|
|
1965
2054
|
el('set-rows').classList.toggle('alerts-off', !el('set-alerts').checked)
|
|
@@ -1989,6 +2078,14 @@ function populateSettings() {
|
|
|
1989
2078
|
el('set-talk').checked = c.talk !== false
|
|
1990
2079
|
el('set-sound').checked = !!c.sound
|
|
1991
2080
|
reflectTalkOn()
|
|
2081
|
+
pickedPet = Pets.findPet(c.pet) ? c.pet : Pets.DEFAULT_PET
|
|
2082
|
+
paintPetPicker()
|
|
2083
|
+
const visiting = Pets.findPet(Pets.seasonalPet())
|
|
2084
|
+
el('pet-season').hidden = !visiting
|
|
2085
|
+
if (visiting) {
|
|
2086
|
+
el('pet-season').textContent =
|
|
2087
|
+
`${visiting.name} takes over for the holiday, then your pick comes back.`
|
|
2088
|
+
}
|
|
1992
2089
|
clearSaveDirty() // fields now match the saved config
|
|
1993
2090
|
}
|
|
1994
2091
|
// `provider` opens that service's connection card straight away
|
|
@@ -1996,10 +2093,42 @@ function openSettings(provider = null) {
|
|
|
1996
2093
|
hideBubble()
|
|
1997
2094
|
setDisplaySize('expanded', false)
|
|
1998
2095
|
populateSettings()
|
|
2096
|
+
showSettingsTab(typeof provider === 'string' ? 'accounts' : settingsTab)
|
|
1999
2097
|
if (typeof provider === 'string') openConnection(provider)
|
|
2000
2098
|
document.body.classList.add('settings-open')
|
|
2001
|
-
fitSize()
|
|
2002
|
-
}
|
|
2099
|
+
fitSize(true)
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
// Settings tabs: one pane at a time, each at its own height, and the window
|
|
2103
|
+
// eases between them rather than keeping the tallest one's empty space
|
|
2104
|
+
let settingsTab = 'accounts'
|
|
2105
|
+
function showSettingsTab(tab) {
|
|
2106
|
+
const changed = tab !== settingsTab
|
|
2107
|
+
settingsTab = tab
|
|
2108
|
+
for (const b of document.querySelectorAll('.set-tab')) {
|
|
2109
|
+
const on = b.dataset.tab === tab
|
|
2110
|
+
b.classList.toggle('on', on)
|
|
2111
|
+
b.setAttribute('aria-selected', String(on))
|
|
2112
|
+
b.tabIndex = on ? 0 : -1
|
|
2113
|
+
el(`pane-${b.dataset.tab}`).classList.toggle('on', on)
|
|
2114
|
+
}
|
|
2115
|
+
if (changed && document.body.classList.contains('settings-open')) fitSize(true)
|
|
2116
|
+
}
|
|
2117
|
+
for (const b of document.querySelectorAll('.set-tab')) {
|
|
2118
|
+
b.addEventListener('click', () => showSettingsTab(b.dataset.tab))
|
|
2119
|
+
}
|
|
2120
|
+
// arrow keys move between tabs, as a tablist should
|
|
2121
|
+
el('set-tabs').addEventListener('keydown', (e) => {
|
|
2122
|
+
const tabs = [...document.querySelectorAll('.set-tab')]
|
|
2123
|
+
const step = { ArrowRight: 1, ArrowLeft: -1 }[e.key]
|
|
2124
|
+
if (!step) return
|
|
2125
|
+
const next =
|
|
2126
|
+
tabs[(tabs.findIndex((t) => t.dataset.tab === settingsTab) + step + tabs.length) % tabs.length]
|
|
2127
|
+
showSettingsTab(next.dataset.tab)
|
|
2128
|
+
next.focus()
|
|
2129
|
+
e.preventDefault()
|
|
2130
|
+
})
|
|
2131
|
+
showSettingsTab(settingsTab)
|
|
2003
2132
|
|
|
2004
2133
|
// Settings → Connections: a tile per service says whether it's on (a check and
|
|
2005
2134
|
// its plan); a click opens its card underneath, one at a time
|
|
@@ -2049,10 +2178,13 @@ function closeSettings() {
|
|
|
2049
2178
|
document.body.classList.remove('settings-open')
|
|
2050
2179
|
document.body.classList.add('settings-closing')
|
|
2051
2180
|
clearTimeout(closingTimer)
|
|
2052
|
-
closingTimer = setTimeout(() => document.body.classList.remove('settings-closing'),
|
|
2181
|
+
closingTimer = setTimeout(() => document.body.classList.remove('settings-closing'), 420)
|
|
2053
2182
|
clearSaveDirty()
|
|
2054
2183
|
applyZoom(currentConfig?.zoom != null ? currentConfig.zoom : 100) // undo the preview
|
|
2055
|
-
|
|
2184
|
+
pickedPet = null
|
|
2185
|
+
el('pet-modal').hidden = true
|
|
2186
|
+
refreshPet()
|
|
2187
|
+
fitSize(true)
|
|
2056
2188
|
}
|
|
2057
2189
|
el('gear').addEventListener('click', () => {
|
|
2058
2190
|
if (document.body.classList.contains('settings-open')) closeSettings()
|
|
@@ -2073,6 +2205,74 @@ for (const b of document.querySelectorAll('.num-btn')) {
|
|
|
2073
2205
|
if (input.id === 'set-zoom') previewZoom()
|
|
2074
2206
|
})
|
|
2075
2207
|
}
|
|
2208
|
+
// the pet picker: one button per pet, grouped by pack; a click previews it
|
|
2209
|
+
function clauddyThumb() {
|
|
2210
|
+
// one path per colour: separate squares leave hairline seams when scaled
|
|
2211
|
+
const cells = (pick) =>
|
|
2212
|
+
SPRITE.flatMap((row, r) =>
|
|
2213
|
+
[...row].map((ch, c) => (pick(ch) ? `M${c} ${r}h1v1h-1z` : '')),
|
|
2214
|
+
).join('')
|
|
2215
|
+
return `<svg viewBox="-1 -1 12 11" aria-hidden="true"><path d="${cells((ch) => ch !== '.')}" fill="var(--pixel)"/><path d="${cells((ch) => ch === 'o')}" fill="var(--eye)"/></svg>`
|
|
2216
|
+
}
|
|
2217
|
+
function buildPetPicker() {
|
|
2218
|
+
for (const pack of Pets.PACKS) {
|
|
2219
|
+
const pets = Pets.PETS.filter((p) => p.pack === pack.id)
|
|
2220
|
+
if (pack.id === 'everyday') pets.unshift(Pets.findPet(Pets.DEFAULT_PET))
|
|
2221
|
+
const group = document.createElement('div')
|
|
2222
|
+
group.innerHTML = `<div class="pet-pack-name">${pack.name}</div><div class="pet-grid"></div>`
|
|
2223
|
+
for (const pet of pets) {
|
|
2224
|
+
const b = document.createElement('button')
|
|
2225
|
+
b.type = 'button'
|
|
2226
|
+
b.className = 'pet-opt'
|
|
2227
|
+
b.dataset.pet = pet.id
|
|
2228
|
+
b.setAttribute('role', 'radio')
|
|
2229
|
+
b.setAttribute('aria-label', pet.name)
|
|
2230
|
+
b.innerHTML =
|
|
2231
|
+
pet.id === Pets.DEFAULT_PET ? clauddyThumb() : Pets.petMarkup(pet.id, `th-${pet.id}-`)
|
|
2232
|
+
b.addEventListener('click', () => {
|
|
2233
|
+
pickedPet = pet.id
|
|
2234
|
+
paintPetPicker()
|
|
2235
|
+
showPet(pet.id)
|
|
2236
|
+
refreshSaveDirty()
|
|
2237
|
+
closePetModal()
|
|
2238
|
+
})
|
|
2239
|
+
group.querySelector('.pet-grid').appendChild(b)
|
|
2240
|
+
}
|
|
2241
|
+
el('set-pet').appendChild(group)
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
function paintPetPicker() {
|
|
2245
|
+
for (const b of el('set-pet').querySelectorAll('.pet-opt')) {
|
|
2246
|
+
const on = b.dataset.pet === pickedPet
|
|
2247
|
+
b.classList.toggle('on', on)
|
|
2248
|
+
b.setAttribute('aria-checked', String(on))
|
|
2249
|
+
}
|
|
2250
|
+
// the row shows the pick; the full catalog waits behind the pencil
|
|
2251
|
+
const pet = Pets.findPet(pickedPet)
|
|
2252
|
+
el('pet-current-name').textContent = pet?.name || ''
|
|
2253
|
+
el('pet-current-thumb').innerHTML =
|
|
2254
|
+
pickedPet === Pets.DEFAULT_PET ? clauddyThumb() : Pets.petMarkup(pickedPet, 'cur-')
|
|
2255
|
+
}
|
|
2256
|
+
function openPetModal() {
|
|
2257
|
+
el('pet-modal').hidden = false
|
|
2258
|
+
;(el('set-pet').querySelector('.pet-opt.on') || el('set-pet').querySelector('.pet-opt'))?.focus()
|
|
2259
|
+
}
|
|
2260
|
+
function closePetModal() {
|
|
2261
|
+
if (el('pet-modal').hidden) return
|
|
2262
|
+
el('pet-modal').hidden = true
|
|
2263
|
+
el('pet-edit').focus()
|
|
2264
|
+
}
|
|
2265
|
+
buildPetPicker()
|
|
2266
|
+
el('pet-edit').addEventListener('click', openPetModal)
|
|
2267
|
+
el('pet-modal-close').addEventListener('click', closePetModal)
|
|
2268
|
+
// a click on the dimmed backdrop, outside the card, puts it away too
|
|
2269
|
+
el('pet-modal').addEventListener('click', (e) => {
|
|
2270
|
+
if (e.target === el('pet-modal')) closePetModal()
|
|
2271
|
+
})
|
|
2272
|
+
document.addEventListener('keydown', (e) => {
|
|
2273
|
+
if (e.key === 'Escape') closePetModal()
|
|
2274
|
+
})
|
|
2275
|
+
|
|
2076
2276
|
// light up Save whenever an editable field changes
|
|
2077
2277
|
for (const id of [
|
|
2078
2278
|
'set-alerts',
|
|
@@ -2118,7 +2318,11 @@ el('set-save').addEventListener('click', () => {
|
|
|
2118
2318
|
zoom,
|
|
2119
2319
|
talk: el('set-talk').checked,
|
|
2120
2320
|
sound: el('set-sound').checked,
|
|
2321
|
+
pet: pickedPet,
|
|
2121
2322
|
})
|
|
2323
|
+
const saved = pickedPet
|
|
2324
|
+
pickedPet = null
|
|
2325
|
+
showPet(Pets.activePet(saved))
|
|
2122
2326
|
clearSaveDirty()
|
|
2123
2327
|
document.body.classList.remove('settings-open')
|
|
2124
2328
|
fitSize()
|
|
@@ -2140,7 +2344,6 @@ el('pet').addEventListener('click', () => {
|
|
|
2140
2344
|
})
|
|
2141
2345
|
|
|
2142
2346
|
// eyes follow the cursor
|
|
2143
|
-
const eyesG = el('eyes')
|
|
2144
2347
|
window.addEventListener('mousemove', (e) => {
|
|
2145
2348
|
const b = document.body.classList
|
|
2146
2349
|
if (b.contains('state-sleeping') || b.contains('state-tired')) return
|
|
@@ -2148,13 +2351,10 @@ window.addEventListener('mousemove', (e) => {
|
|
|
2148
2351
|
const dx = e.clientX - (r.left + r.width / 2)
|
|
2149
2352
|
const dy = e.clientY - (r.top + r.height / 2)
|
|
2150
2353
|
const len = Math.hypot(dx, dy) || 1
|
|
2151
|
-
|
|
2152
|
-
'transform',
|
|
2153
|
-
`translate(${((dx / len) * 3).toFixed(2)} ${((dy / len) * 2).toFixed(2)})`,
|
|
2154
|
-
)
|
|
2354
|
+
lookAt(((dx / len) * 3).toFixed(2), ((dy / len) * 2).toFixed(2))
|
|
2155
2355
|
})
|
|
2156
2356
|
window.addEventListener('mouseout', (e) => {
|
|
2157
|
-
if (!e.relatedTarget)
|
|
2357
|
+
if (!e.relatedTarget) lookAt(0, 0)
|
|
2158
2358
|
})
|
|
2159
2359
|
|
|
2160
2360
|
// welcome wave
|
|
@@ -2188,5 +2388,7 @@ if (typeof module === 'object' && module.exports) {
|
|
|
2188
2388
|
paintCompanion,
|
|
2189
2389
|
activityTracker,
|
|
2190
2390
|
blipper,
|
|
2391
|
+
showPet,
|
|
2392
|
+
refreshPet,
|
|
2191
2393
|
}
|
|
2192
2394
|
}
|