灵感来源
这周刚好看到一个大眼的玩具,感觉非常有意思。但是只能放在自己的网页上又感觉缺乏使用场景。因此我想到能把他翻成chrome插件,注入到平常浏览的网页上,这样使用场景就丰富了。
开始翻译
chrome插件使用的还是h5一套,因此改动并不算大。首先将代码全部复制下来,echart的源码也下载下来。然后做一些小改动。
首先考虑到我们要把大眼插入进网页,那么首先我们不需要背景色,也不需要一些通配的样式,避免对网页原本的样式造成破坏。
其次是大眼的位置,原本是处于网页居中的位置,在实际浏览中会遮挡住核心的浏览区域,因此我们考虑把大眼移到上方居中。
既然把大眼移动到了上方,那么环视四周的动作也应该加上一点俯视的感觉,这边加上了rotateX(-45deg)。
相应的监听鼠标相对位置的坐标原点也需要改动一下。
最后呢我希望大眼在插入页面后能自动唤醒,不用点击大眼唤醒。改一下也非常的简单。
经过这一些细节的改动,大眼本身就改完了。后面我们把他改成chrome插件。
chrome插件一共三个文件,manifest.json是插件的配置文件,background.js是chrome全局环境执行的,甚至浏览器关闭了也仍然会在后台执行。content_scripts是每个标签页中会注入执行的。可以看到这边把echarts.min.js也一并注入了。
首先是content-script.js,为了避免重复注入,在window里添加了个monitorLoaded的标志位,Init方法中将前面修改完的html和css添加进body中,js部分直接复制即可。
let bigEye; | |
let eyeball; | |
let eyeFilter; | |
let eyeballChart; | |
let leftRotSize = 0; | |
let ballSize = 0; | |
let ballColor = 'transparent' | |
let rotTimer; | |
let sleepTimer; | |
let isSleep = true; // 是否处于休眠状态 | |
if (!window.monitorLoaded) { | |
Init(); | |
window.monitorLoaded = true; | |
} | |
function Init() { | |
const div = document.createElement("div"); | |
div.innerHTML = `<div class="eyeSocket eyeSocketSleeping" id='bigEye'> | |
<div id="eyeball"></div> | |
</div> | |
<div class="filter"> | |
<div class="eyeSocket" id='eyeFilter'> | |
</div> | |
</div> | |
<svg width="0"> | |
<filter id='filter'> | |
<feTurbulence baseFrequency="1"> | |
<animate id="animate1" attributeName="baseFrequency" dur="1s" from="0.5" to="0.55" begin="0s;animate1.end"> | |
</animate> | |
<animate id="animate2" attributeName="baseFrequency" dur="1s" from="0.55" to="0.5" begin="animate2.end"> | |
</animate> | |
</feTurbulence> | |
<feDisplacementMap in="SourceGraphic" scale="50" xChannelSelector="R" yChannelSelector="B" /> | |
</filter> | |
</svg>`; | |
document.body.appendChild(div); | |
const style = document.createElement('style'); | |
style.innerHTML = ` | |
body { | |
perspective: 1000px; | |
--c-eyeSocket: rgb(41, 104, 217); | |
--c-eyeSocket-outer: #02ffff; | |
--c-eyeSocket-outer-shadow: transparent; | |
--c-eyeSocket-inner: rgb(35, 22, 140); | |
} | |
.filter { | |
width: 100%; | |
height: 100%; | |
filter: url('#filter'); | |
} | |
.eyeSocket, | |
.filter .eyeSocket { | |
position: absolute; | |
left: calc(50% - 75px); | |
top: 17px; | |
width: 150px; | |
aspect-ratio: 1; | |
border-radius: 50%; | |
border: 4px solid var(--c-eyeSocket); | |
box-shadow: 0px 0px 50px var(--c-eyeSocket-outer-shadow); | |
transition: border 0.5s ease-in-out, box-shadow 0.5s ease-in-out; | |
z-index: 1000000; | |
} | |
.filter .eyeSocket { | |
opacity: 0; | |
left: calc(50% - 92px); | |
top: 0px; | |
transition: all 0.5s ease-in-out; | |
} | |
.eyeSocket::before, | |
.eyeSocket::after { | |
content: ""; | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
border-radius: 50%; | |
transition: all 0.5s ease-in-out; | |
box-sizing: border-box; | |
} | |
.eyeSocket::before { | |
width: calc(100% + 20px); | |
height: calc(100% + 20px); | |
border: 6px solid var(--c-eyeSocket-outer); | |
} | |
.eyeSocket::after { | |
width: 100%; | |
height: 100%; | |
border: 4px solid var(--c-eyeSocket-inner); | |
box-shadow: inset 0px 0px 30px var(--c-eyeSocket-inner); | |
} | |
#eyeball { | |
width: 100%; | |
height: 100%; | |
} | |
.eyeSocketSleeping { | |
animation: sleeping 6s infinite; | |
} | |
.eyeSocketLooking { | |
animation: lookAround 2.5s; | |
} | |
@keyframes sleeping { | |
0% { | |
transform: scale(1); | |
} | |
50% { | |
transform: scale(1.2); | |
} | |
100% { | |
transform: scale(1); | |
} | |
} | |
@keyframes lookAround { | |
0% { | |
transform: translateX(0) rotateY(0); | |
} | |
10% { | |
transform: translateX(0) rotateY(0); | |
} | |
40% { | |
transform: translateX(-70px) rotateX(-45deg) rotateY(-30deg); | |
} | |
80% { | |
transform: translateX(70px) rotateX(-45deg) rotateY(30deg); | |
} | |
100% { | |
transform: translateX(0) rotateY(0); | |
} | |
}` | |
document.body.appendChild(style); | |
bigEye = document.getElementById('bigEye'); | |
eyeball = document.getElementById('eyeball'); | |
eyeFilter = document.getElementById('eyeFilter'); | |
eyeballChart = echarts.init(eyeball); | |
setTimeout(() => { | |
clickToWeakup(); | |
}, 2000); | |
bigEye.addEventListener('click', () => { | |
if (!isSleep) return; | |
clickToWeakup(); | |
}) | |
bigEye.addEventListener('webkitAnimationEnd', () => { | |
new Promise(res => { | |
clearInterval(rotTimer); | |
rotTimer = setInterval(() => { | |
getEyeballChart() | |
ballSize > 0 && (ballSize -= 0.5); | |
leftRotSize === 360 ? (leftRotSize = 0) : (leftRotSize += 0.1); | |
if (ballSize === 0) { | |
clearInterval(rotTimer); | |
res(); | |
} | |
}, 10); | |
}).then(() => { | |
eyeFilter.style.opacity = '0' | |
eyeFilter.className = bigEye.className = 'eyeSocket'; | |
setNormal(); | |
document.body.addEventListener('mousemove', focusOnMouse); | |
rotTimer = setInterval(() => { | |
getEyeballChart() | |
ballSize <= 12 && (ballSize += 0.1); | |
leftRotSize === 360 ? (leftRotSize = 0) : (leftRotSize += 0.1); | |
}, 10); | |
}) | |
}) | |
chrome.runtime.onMessage.addListener( | |
function(request, sender, sendResponse) { | |
switch (request.type) { | |
case "gethref": | |
sendResponse({ href: window.location.href.toLocaleLowerCase() }); | |
break; | |
case "setAngry": | |
setAngry(); | |
break; | |
case "setNormal": | |
setNormal(); | |
break; | |
default: | |
break; | |
} | |
}); | |
} | |
// 画眼球 | |
function getEyeballChart() { | |
eyeballChart.setOption({ | |
series: [{ | |
type: 'gauge', | |
radius: '-20%', | |
clockwise: false, | |
startAngle: `${0 + leftRotSize * 5}`, | |
endAngle: `${270 + leftRotSize * 5}`, | |
splitNumber: 3, | |
detail: false, | |
axisLine: { | |
show: false, | |
}, | |
axisTick: false, | |
splitLine: { | |
show: true, | |
length: ballSize, | |
lineStyle: { | |
shadowBlur: 20, | |
shadowColor: ballColor, | |
shadowOffsetY: '0', | |
color: ballColor, | |
width: 4, | |
} | |
}, | |
axisLabel: false | |
}, { | |
type: 'gauge', | |
radius: '-20%', | |
clockwise: false, | |
startAngle: `${45 + leftRotSize * 5}`, | |
endAngle: `${315 + leftRotSize * 5}`, | |
splitNumber: 3, | |
detail: false, | |
axisLine: { | |
show: false, | |
}, | |
axisTick: false, | |
splitLine: { | |
show: true, | |
length: ballSize, | |
lineStyle: { | |
shadowBlur: 20, | |
shadowColor: ballColor, | |
shadowOffsetY: '0', | |
color: ballColor, | |
width: 4, | |
} | |
}, | |
axisLabel: false | |
}] | |
}) | |
} | |
// 休眠 | |
function toSleep() { | |
isSleep = true; | |
clearInterval(rotTimer); | |
rotTimer = setInterval(() => { | |
getEyeballChart() | |
if (ballSize > 0) { | |
ballSize -= 0.1; | |
} else { | |
bigEye.className = 'eyeSocket eyeSocketSleeping' | |
} | |
leftRotSize === 360 ? (leftRotSize = 0) : (leftRotSize += 0.1); | |
}, 10); | |
document.body.removeEventListener('mousemove', focusOnMouse); | |
bigEye.style.transform = `rotateY(0deg) rotateX(0deg)`; | |
eyeball.style.transform = `translate(0px, 0px)`; | |
} | |
// 唤醒 | |
function clickToWeakup() { | |
isSleep = false; | |
eyeFilter.style.opacity = '1' | |
eyeFilter.className = bigEye.className = 'eyeSocket eyeSocketLooking' | |
setAngry(); | |
clearInterval(rotTimer); | |
rotTimer = setInterval(() => { | |
getEyeballChart(); | |
ballSize <= 50 && (ballSize += 1); | |
leftRotSize === 360 ? (leftRotSize = 0) : (leftRotSize += 0.5); | |
}, 10); | |
} | |
// 生气模式 | |
function setAngry() { | |
document.body.style.setProperty('--c-eyeSocket', 'rgb(255,187,255)') | |
document.body.style.setProperty('--c-eyeSocket-outer', 'rgb(238,85,135)') | |
document.body.style.setProperty('--c-eyeSocket-outer-shadow', 'rgb(255, 60, 86)') | |
document.body.style.setProperty('--c-eyeSocket-inner', 'rgb(208,14,74)') | |
ballColor = 'rgb(208,14,74)'; | |
} | |
// 常态模式 | |
function setNormal() { | |
document.body.style.setProperty('--c-eyeSocket', 'rgb(41, 104, 217)') | |
document.body.style.setProperty('--c-eyeSocket-outer', '#02ffff') | |
document.body.style.setProperty('--c-eyeSocket-outer-shadow', 'transparent') | |
document.body.style.setProperty('--c-eyeSocket-inner', 'rgb(35, 22, 140)') | |
ballColor = 'rgb(0,238,255)'; | |
} | |
// 关注鼠标 | |
function focusOnMouse(e) { | |
// 视口尺寸 | |
let clientWidth = document.body.clientWidth; | |
let clientHeight = document.body.clientHeight; | |
// 原点,即bigEye中心位置,页面中心 | |
let origin = [clientWidth / 2, 0]; | |
// 鼠标坐标 | |
let mouseCoords = [e.clientX - origin[0], origin[1] - e.clientY]; | |
let eyeXDeg = mouseCoords[1] / clientHeight * 80; | |
let eyeYDeg = mouseCoords[0] / clientWidth * 60; | |
bigEye.style.transform = `rotateY(${eyeYDeg}deg) rotateX(${eyeXDeg}deg)`; | |
eyeball.style.transform = `translate(${eyeYDeg / 1.5}px, ${-eyeXDeg / 1.5}px)`; | |
// 设置休眠 | |
if (sleepTimer) clearTimeout(sleepTimer); | |
sleepTimer = setTimeout(() => { | |
toSleep(); | |
}, 30000); | |
} |
加入chrome插件间消息的处理,后面会在background中对浏览的页面进行统计,这边先加上了消息处理。可以根据消息返回当前页面url,设置大眼状态。
然后是background.js,chrome.runtime.onInstalled是安装插件的时候,chrome.runtime.onStartup是启动浏览器的时候,chrome.action.onClicked是点击扩展里本插件的图标的时候。 chrome.tabs.sendMessage是给标签页发送消息。第一个参数tab.id即标签页的id,来选择发送消息的目标tab页,第二个参数是消息体,第三个参数是回调方法。 chrome.notifications.create是发送桌面通知。
在background中我监听了当前正在浏览的页面,当开始监听时,会弹出问候语。 之后每隔60秒会检测一次正在访问的页面。如果是正在浏览工作的页面,那么情绪值会上升。如果是在看娱乐页面,那么情绪值会下降。当情绪值>80时,会将大眼设置成angry的状态,并且会弹出一些安慰的话语。同样当情绪值恢复到60以下时,会将大眼设置回normal的状态。当关闭浏览器时,会弹出一个总结的通知。
let logintime; | |
let emotion; | |
let worknotice; | |
let Interval; | |
let openSign = false; | |
let type = [ | |
{ href: "baidu", type: "work" }, | |
{ href: "juejin", type: "work" }, | |
{ href: "cloud.tencent", type: "work" }, | |
{ href: "192.168", type: "work" }, | |
{ href: "localhost", type: "work" }, | |
{ href: "csdn", type: "work" }, | |
{ href: "bilibili", type: "fun" }, | |
] | |
chrome.runtime.onInstalled.addListener(() => { | |
start(); | |
monitor(); | |
}); | |
chrome.runtime.onStartup.addListener(() => { | |
start(); | |
monitor(); | |
}); | |
chrome.action.onClicked.addListener(() => { | |
start(); | |
// chrome.tabs.executeScript({ | |
// file: 'content-script.js', | |
// }); | |
// chrome.tabs.executeScript({ | |
// file: 'echarts/4.3.0/echarts.min.js', | |
// }); | |
}); | |
function checkWindow() { | |
chrome.windows.getAll({}, function(windows) { | |
let windowCount = windows.length; | |
if (windowCount == 0) { | |
close(); | |
} | |
}); | |
} | |
function monitor() { | |
if (!Interval) { | |
Interval = setInterval(async() => { | |
let tab = await getActiveTab(); | |
if (tab != null) { | |
chrome.tabs.sendMessage( | |
tab.id, { type: "gethref" }, | |
(response) => { | |
if (!response) return; | |
let href = response.href; | |
type.forEach(o => { | |
if (href.indexOf(o.href) >= 0) { | |
if (o.type == "work") { | |
emotion = emotion + 1; | |
} | |
if (o.type == "fun") { | |
emotion = emotion - 1; | |
} | |
if (emotion > 100) emotion = 100; | |
if (emotion < 0) emotion = 0; | |
if (emotion >= 80) { | |
chrome.tabs.sendMessage(tab.id, { type: "setAngry" }, () => {}); | |
if (!worknotice) { | |
let week = logintime.getDay(); | |
if (week == "0" || week == "6") { | |
chrome.notifications.create(null, { | |
type: 'basic', | |
iconUrl: 'icon.png', | |
title: "太累了!", | |
message: "周末还加班!你老板真不是人!", | |
}); | |
} else { | |
chrome.notifications.create(null, { | |
type: 'basic', | |
iconUrl: 'icon.png', | |
title: "太累了!", | |
message: "加班辛苦了!休息一下吧!", | |
}); | |
} | |
worknotice = true; | |
} | |
} | |
if (emotion < 60) { | |
chrome.tabs.sendMessage(tab.id, { type: "setNormal" }, () => {}); | |
} | |
} | |
}); | |
}); | |
} else { | |
checkWindow(); | |
} | |
}, 60000) | |
} | |
} | |
function start() { | |
logintime = new Date(); | |
emotion = 50; | |
worknotice = false; | |
openSign = true; | |
chrome.notifications.create(null, { | |
type: 'basic', | |
iconUrl: 'icon.png', | |
title: gettitle(logintime), | |
message: getmessage(logintime), | |
}); | |
} | |
function close() { | |
if (openSign) { | |
if (emotion < 20) { | |
chrome.notifications.create(null, { | |
type: 'basic', | |
iconUrl: 'icon.png', | |
title: "这么早?", | |
message: "这么早就不玩啦?不多整两把?", | |
}); | |
} | |
if (emotion > 80) { | |
chrome.notifications.create(null, { | |
type: 'basic', | |
iconUrl: 'icon.png', | |
title: "太累了!", | |
message: "活终于干完了!休息一下吧!", | |
}); | |
} | |
} | |
openSign = false; | |
} | |
function gettitle(date) { | |
let hour = date.getHours(); | |
if (hour < 12 && hour > 5) { | |
return "早上好!"; | |
} else if (hour >= 12 && hour < 14) { | |
return "中午好!"; | |
} else if (hour >= 14 && hour < 18) { | |
return "下午好"; | |
} else if (hour >= 18 && hour < 24) { | |
return "下午好"; | |
} else { | |
return "滚去睡觉!"; | |
} | |
} | |
function getmessage(date) { | |
let fmtlogintime = dateFormat(date, 'yyyy-MM-dd hh:mm:ss'); | |
//let week = data.getDay(); | |
let message = `当前时间:${fmtlogintime}`; | |
return message; | |
} | |
function dateFormat(date, fmt) { // author: meizz | |
const o = { | |
'M+': date.getMonth() + 1, // 月份 | |
'd+': date.getDate(), // 日 | |
'h+': date.getHours(), // 小时 | |
'm+': date.getMinutes(), // 分 | |
's+': date.getSeconds(), // 秒 | |
'q+': Math.floor((date.getMonth() + 3) / 3), // 季度 | |
'S': date.getMilliseconds() // 毫秒 | |
}; | |
if (/(y+)/.test(fmt)) { | |
fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length)); | |
} | |
for (const k in o) { | |
if (new RegExp('(' + k + ')').test(fmt)) { | |
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : (('00' + o[k]).substr(('' + o[k]).length))); | |
} | |
} | |
return fmt; | |
} | |
// 获取活跃的 tab,通常是用户正在浏览的页面 | |
async function getActiveTab() { | |
return new Promise((resolve) => { | |
chrome.tabs.query({ | |
active: true, | |
currentWindow: true, | |
}, | |
(tabs) => { | |
if (tabs.length > 0) { | |
resolve(tabs[0]); | |
} else { | |
resolve(null); | |
} | |
} | |
); | |
}); | |
} |
这样一个简单的chrome插件就完成了。还是非常的简单的!
这是我的github地址,需要源码的小伙伴可以直接获取。