cursorBuddy: Feat - Sourced Oneko, Allow Changing Oneko's Color (#634)
* cursorBuddy: QOL - Updated Copyright Date * cursorBuddy: QOL - Sourced `oneko.js` * cursorBuddy: QOL - Changed Tab Spacing for `oneko.js` * cursorBuddy: QOL - Reworked Plugin to Use Local Oneko Source * cursorBuddy: Feat - Oneko Color Changing * cursorBuddy: QOL - Better Descriptions * cursorBuddy: QOL - Add Sections to Settings Menu * cursorBuddy: Docs - Updated Authors * cursorBuddy: Fix - Corrected Minor Spelling Mistake 🥀 * cursorBuddy: Fix - Corrected Minor Spelling Mistake 🥀 * cursorBuddy: legal - Added Licensing Info for Oneko Source
This commit is contained in:
parent
9ac7dee47c
commit
dd2ccb6225
2 changed files with 401 additions and 29 deletions
|
|
@ -1,29 +1,19 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2026 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { isPluginEnabled } from "@api/PluginManager";
|
||||
import { definePluginSettings, migratePluginSettings } from "@api/Settings";
|
||||
import { Divider } from "@components/Divider";
|
||||
import { Heading } from "@components/Heading";
|
||||
import { Devs, EquicordDevs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
|
||||
import fathorse from "./fathorse";
|
||||
import oneko from "./oneko";
|
||||
|
||||
const ONEKO_SCRIPT = "https://raw.githubusercontent.com/adryd325/oneko.js/5281d057c4ea9bd4f6f997ee96ba30491aed16c0/oneko.js";
|
||||
const ONEKO_IMAGE = "https://raw.githubusercontent.com/adryd325/oneko.js/5281d057c4ea9bd4f6f997ee96ba30491aed16c0/oneko.gif";
|
||||
const FATASS_HORSE_IMAGE = "https://raw.githubusercontent.com/nexpid/fatass-horse/08bc4042750d5f995c55327f7b6c6710158f5263/sheet.png";
|
||||
|
||||
|
|
@ -58,6 +48,38 @@ const settings = definePluginSettings({
|
|||
isValid: (value: number) => value > 0 || "Framerate must be bigger than 0",
|
||||
onChange: load
|
||||
},
|
||||
// Oneko Specific
|
||||
onekoSection: {
|
||||
type: OptionType.COMPONENT,
|
||||
component: () => (
|
||||
<div>
|
||||
<Heading style={{ fontSize: "1.6em", marginTop: "10px" }}>Oneko</Heading>
|
||||
<Divider style={{ marginBottom: "-10px" }}></Divider>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
furColor: {
|
||||
description: "Fur hex color for Oneko",
|
||||
type: OptionType.STRING,
|
||||
default: "#FFFFFF",
|
||||
onChange: load,
|
||||
},
|
||||
outlineColor: {
|
||||
description: "Outline hex color for Oneko",
|
||||
type: OptionType.STRING,
|
||||
default: "#000000",
|
||||
onChange: load,
|
||||
},
|
||||
// Fatass Horse Specific
|
||||
fathorseSection: {
|
||||
type: OptionType.COMPONENT,
|
||||
component: () => (
|
||||
<div>
|
||||
<Heading style={{ fontSize: "1.6em", marginTop: "10px" }}>Fatass Horse</Heading>
|
||||
<Divider style={{ marginBottom: "-10px" }}></Divider>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
size: {
|
||||
description: "Size of the fatass horse",
|
||||
type: OptionType.NUMBER,
|
||||
|
|
@ -81,9 +103,17 @@ const settings = definePluginSettings({
|
|||
description: "If the horse should shake the window when it's walking",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false,
|
||||
onChange: load
|
||||
onChange: load,
|
||||
},
|
||||
}, {
|
||||
// Oneko Specific
|
||||
furColor: {
|
||||
disabled() { return this.store.buddy !== "oneko"; }
|
||||
},
|
||||
outlineColor: {
|
||||
disabled() { return this.store.buddy !== "oneko"; }
|
||||
},
|
||||
// Fatass Horse Specific
|
||||
size: {
|
||||
disabled() { return this.store.buddy !== "fathorse"; },
|
||||
},
|
||||
|
|
@ -109,16 +139,14 @@ function load() {
|
|||
|
||||
switch (settings.store.buddy) {
|
||||
case "oneko": {
|
||||
fetch(ONEKO_SCRIPT)
|
||||
.then(x => x.text())
|
||||
.then(s => s
|
||||
.replace("(isReducedMotion)", "(false)")
|
||||
.replace("persistPosition = true;", "persistPosition = false;")
|
||||
.replace("./oneko.gif", ONEKO_IMAGE)
|
||||
.replace("nekoSpeed = 10;", `nekoSpeed = ${settings.store.speed};`)
|
||||
.replace(" > 100", ` > ${1000 / settings.store.fps}`)
|
||||
)
|
||||
.then(s => Function(s)());
|
||||
oneko({
|
||||
speed: settings.store.speed,
|
||||
fps: settings.store.fps,
|
||||
image: ONEKO_IMAGE,
|
||||
persistPosition: false,
|
||||
furColor: settings.store.furColor,
|
||||
outlineColor: settings.store.outlineColor
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "fathorse": {
|
||||
|
|
@ -139,7 +167,7 @@ migratePluginSettings("CursorBuddy", "Oneko", "oneko");
|
|||
export default definePlugin({
|
||||
name: "CursorBuddy",
|
||||
description: "only a slightly annoying plugin",
|
||||
authors: [Devs.Ven, Devs.adryd, EquicordDevs.nexpid],
|
||||
authors: [Devs.Ven, Devs.adryd, EquicordDevs.nexpid, EquicordDevs.ZcraftElite],
|
||||
settings,
|
||||
isModified: true,
|
||||
|
||||
|
|
|
|||
344
src/equicordplugins/cursorBuddy/oneko.js
Normal file
344
src/equicordplugins/cursorBuddy/oneko.js
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2026 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
/*
|
||||
* Sourced from https://raw.githubusercontent.com/adryd325/oneko.js/5281d057c4ea9bd4f6f997ee96ba30491aed16c0/oneko.js
|
||||
* Licensed under https://github.com/adryd325/oneko.js/blob/main/LICENSE
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
export default function oneko(options = {}) {
|
||||
const {
|
||||
speed = 10,
|
||||
fps = 24,
|
||||
image = "./oneko.gif",
|
||||
persistPosition = true,
|
||||
furColor = "#FFFFFF",
|
||||
outlineColor = "#000000",
|
||||
} = options;
|
||||
|
||||
const isReducedMotion =
|
||||
window.matchMedia(`(prefers-reduced-motion: reduce)`) === true ||
|
||||
window.matchMedia(`(prefers-reduced-motion: reduce)`).matches === true;
|
||||
|
||||
if (isReducedMotion) return;
|
||||
|
||||
const nekoEl = document.createElement("div");
|
||||
let nekoPosX = 32;
|
||||
let nekoPosY = 32;
|
||||
|
||||
let mousePosX = 0;
|
||||
let mousePosY = 0;
|
||||
|
||||
let frameCount = 0;
|
||||
let idleTime = 0;
|
||||
let idleAnimation = null;
|
||||
let idleAnimationFrame = 0;
|
||||
|
||||
const nekoSpeed = speed;
|
||||
const spriteSets = {
|
||||
idle: [[-3, -3]],
|
||||
alert: [[-7, -3]],
|
||||
scratchSelf: [
|
||||
[-5, 0],
|
||||
[-6, 0],
|
||||
[-7, 0],
|
||||
],
|
||||
scratchWallN: [
|
||||
[0, 0],
|
||||
[0, -1],
|
||||
],
|
||||
scratchWallS: [
|
||||
[-7, -1],
|
||||
[-6, -2],
|
||||
],
|
||||
scratchWallE: [
|
||||
[-2, -2],
|
||||
[-2, -3],
|
||||
],
|
||||
scratchWallW: [
|
||||
[-4, 0],
|
||||
[-4, -1],
|
||||
],
|
||||
tired: [[-3, -2]],
|
||||
sleeping: [
|
||||
[-2, 0],
|
||||
[-2, -1],
|
||||
],
|
||||
N: [
|
||||
[-1, -2],
|
||||
[-1, -3],
|
||||
],
|
||||
NE: [
|
||||
[0, -2],
|
||||
[0, -3],
|
||||
],
|
||||
E: [
|
||||
[-3, 0],
|
||||
[-3, -1],
|
||||
],
|
||||
SE: [
|
||||
[-5, -1],
|
||||
[-5, -2],
|
||||
],
|
||||
S: [
|
||||
[-6, -3],
|
||||
[-7, -2],
|
||||
],
|
||||
SW: [
|
||||
[-5, -3],
|
||||
[-6, -1],
|
||||
],
|
||||
W: [
|
||||
[-4, -2],
|
||||
[-4, -3],
|
||||
],
|
||||
NW: [
|
||||
[-1, 0],
|
||||
[-1, -1],
|
||||
],
|
||||
};
|
||||
|
||||
function recolorImage(src, furColor, outlineColor) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.crossOrigin = "anonymous"; // * Needed for CORS
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
const imageData = ctx.getImageData(
|
||||
0,
|
||||
0,
|
||||
canvas.width,
|
||||
canvas.height
|
||||
);
|
||||
const data = imageData.data;
|
||||
|
||||
const furRGB = hexToRgb(furColor);
|
||||
const outlineRGB = hexToRgb(outlineColor);
|
||||
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
const r = data[i];
|
||||
const g = data[i + 1];
|
||||
const b = data[i + 2];
|
||||
const a = data[i + 3];
|
||||
|
||||
if (r === 255 && g === 255 && b === 255) {
|
||||
data[i] = furRGB.r;
|
||||
data[i + 1] = furRGB.g;
|
||||
data[i + 2] = furRGB.b;
|
||||
} else if (r === 0 && g === 0 && b === 0) {
|
||||
data[i] = outlineRGB.r;
|
||||
data[i + 1] = outlineRGB.g;
|
||||
data[i + 2] = outlineRGB.b;
|
||||
}
|
||||
}
|
||||
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
resolve(canvas.toDataURL());
|
||||
};
|
||||
img.onerror = reject;
|
||||
img.src = src;
|
||||
});
|
||||
}
|
||||
|
||||
function hexToRgb(hex) {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result
|
||||
? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16),
|
||||
}
|
||||
: { r: 0, g: 0, b: 0 };
|
||||
}
|
||||
|
||||
function init() {
|
||||
const nekoFile = image; // Use passed-in image URL
|
||||
|
||||
if (persistPosition) {
|
||||
let storedNeko = JSON.parse(window.localStorage.getItem("oneko"));
|
||||
if (storedNeko !== null) {
|
||||
nekoPosX = storedNeko.nekoPosX;
|
||||
nekoPosY = storedNeko.nekoPosY;
|
||||
mousePosX = storedNeko.mousePosX;
|
||||
mousePosY = storedNeko.mousePosY;
|
||||
frameCount = storedNeko.frameCount;
|
||||
idleTime = storedNeko.idleTime;
|
||||
idleAnimation = storedNeko.idleAnimation;
|
||||
idleAnimationFrame = storedNeko.idleAnimationFrame;
|
||||
nekoEl.style.backgroundPosition = storedNeko.bgPos;
|
||||
}
|
||||
}
|
||||
|
||||
nekoEl.id = "oneko";
|
||||
nekoEl.ariaHidden = true;
|
||||
nekoEl.style.width = "32px";
|
||||
nekoEl.style.height = "32px";
|
||||
nekoEl.style.position = "fixed";
|
||||
nekoEl.style.pointerEvents = "none";
|
||||
nekoEl.style.imageRendering = "pixelated";
|
||||
nekoEl.style.left = `${nekoPosX - 16}px`;
|
||||
nekoEl.style.top = `${nekoPosY - 16}px`;
|
||||
nekoEl.style.zIndex = 2147483647;
|
||||
|
||||
recolorImage(nekoFile, furColor, outlineColor)
|
||||
.then((recoloredImageUrl) => {
|
||||
nekoEl.style.backgroundImage = `url(${recoloredImageUrl})`;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to recolor Oneko image:", err);
|
||||
nekoEl.style.backgroundImage = `url(${nekoFile})`; // ! Error handling for tests
|
||||
});
|
||||
|
||||
document.body.appendChild(nekoEl);
|
||||
|
||||
document.addEventListener("mousemove", function (event) {
|
||||
mousePosX = event.clientX;
|
||||
mousePosY = event.clientY;
|
||||
});
|
||||
|
||||
if (persistPosition) {
|
||||
window.addEventListener("beforeunload", function (event) {
|
||||
window.localStorage.setItem(
|
||||
"oneko",
|
||||
JSON.stringify({
|
||||
nekoPosX: nekoPosX,
|
||||
nekoPosY: nekoPosY,
|
||||
mousePosX: mousePosX,
|
||||
mousePosY: mousePosY,
|
||||
frameCount: frameCount,
|
||||
idleTime: idleTime,
|
||||
idleAnimation: idleAnimation,
|
||||
idleAnimationFrame: idleAnimationFrame,
|
||||
bgPos: nekoEl.style.backgroundPosition,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
window.requestAnimationFrame(onAnimationFrame);
|
||||
}
|
||||
|
||||
let lastFrameTimestamp;
|
||||
|
||||
function onAnimationFrame(timestamp) {
|
||||
if (!nekoEl.isConnected) return;
|
||||
if (!lastFrameTimestamp) lastFrameTimestamp = timestamp;
|
||||
if (timestamp - lastFrameTimestamp > 1000 / fps) {
|
||||
lastFrameTimestamp = timestamp;
|
||||
frame();
|
||||
}
|
||||
window.requestAnimationFrame(onAnimationFrame);
|
||||
}
|
||||
|
||||
function setSprite(name, frame) {
|
||||
const sprite = spriteSets[name][frame % spriteSets[name].length];
|
||||
nekoEl.style.backgroundPosition = `${sprite[0] * 32}px ${
|
||||
sprite[1] * 32
|
||||
}px`;
|
||||
}
|
||||
|
||||
function resetIdleAnimation() {
|
||||
idleAnimation = null;
|
||||
idleAnimationFrame = 0;
|
||||
}
|
||||
|
||||
function idle() {
|
||||
idleTime += 1;
|
||||
|
||||
if (
|
||||
idleTime > 10 &&
|
||||
Math.floor(Math.random() * 200) == 0 &&
|
||||
idleAnimation == null
|
||||
) {
|
||||
let avalibleIdleAnimations = ["sleeping", "scratchSelf"];
|
||||
if (nekoPosX < 32) avalibleIdleAnimations.push("scratchWallW");
|
||||
if (nekoPosY < 32) avalibleIdleAnimations.push("scratchWallN");
|
||||
if (nekoPosX > window.innerWidth - 32)
|
||||
avalibleIdleAnimations.push("scratchWallE");
|
||||
if (nekoPosY > window.innerHeight - 32)
|
||||
avalibleIdleAnimations.push("scratchWallS");
|
||||
idleAnimation =
|
||||
avalibleIdleAnimations[
|
||||
Math.floor(Math.random() * avalibleIdleAnimations.length)
|
||||
];
|
||||
}
|
||||
|
||||
switch (idleAnimation) {
|
||||
case "sleeping":
|
||||
if (idleAnimationFrame < 8) {
|
||||
setSprite("tired", 0);
|
||||
break;
|
||||
}
|
||||
setSprite("sleeping", Math.floor(idleAnimationFrame / 4));
|
||||
if (idleAnimationFrame > 192) {
|
||||
resetIdleAnimation();
|
||||
}
|
||||
break;
|
||||
case "scratchWallN":
|
||||
case "scratchWallS":
|
||||
case "scratchWallE":
|
||||
case "scratchWallW":
|
||||
case "scratchSelf":
|
||||
setSprite(idleAnimation, idleAnimationFrame);
|
||||
if (idleAnimationFrame > 9) {
|
||||
resetIdleAnimation();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
setSprite("idle", 0);
|
||||
return;
|
||||
}
|
||||
idleAnimationFrame += 1;
|
||||
}
|
||||
|
||||
function frame() {
|
||||
frameCount += 1;
|
||||
const diffX = nekoPosX - mousePosX;
|
||||
const diffY = nekoPosY - mousePosY;
|
||||
const distance = Math.sqrt(diffX ** 2 + diffY ** 2);
|
||||
|
||||
if (distance < nekoSpeed || distance < 48) {
|
||||
idle();
|
||||
return;
|
||||
}
|
||||
|
||||
idleAnimation = null;
|
||||
idleAnimationFrame = 0;
|
||||
|
||||
if (idleTime > 1) {
|
||||
setSprite("alert", 0);
|
||||
idleTime = Math.min(idleTime, 7);
|
||||
idleTime -= 1;
|
||||
return;
|
||||
}
|
||||
|
||||
let direction;
|
||||
direction = diffY / distance > 0.5 ? "N" : "";
|
||||
direction += diffY / distance < -0.5 ? "S" : "";
|
||||
direction += diffX / distance > 0.5 ? "W" : "";
|
||||
direction += diffX / distance < -0.5 ? "E" : "";
|
||||
setSprite(direction, frameCount);
|
||||
|
||||
nekoPosX -= (diffX / distance) * nekoSpeed;
|
||||
nekoPosY -= (diffY / distance) * nekoSpeed;
|
||||
|
||||
nekoPosX = Math.min(Math.max(16, nekoPosX), window.innerWidth - 16);
|
||||
nekoPosY = Math.min(Math.max(16, nekoPosY), window.innerHeight - 16);
|
||||
|
||||
nekoEl.style.left = `${nekoPosX - 16}px`;
|
||||
nekoEl.style.top = `${nekoPosY - 16}px`;
|
||||
}
|
||||
|
||||
init();
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue