灵感来源
这周刚好看到一个大眼的玩具,感觉非常有意思。但是只能放在自己的网页上又感觉缺乏使用场景。因此我想到能把他翻成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地址,需要源码的小伙伴可以直接获取。