Tip of the day
Equipping a gear set while at a bank will automatically withdraw all the items from the bank.
MediaWiki:Gadget-AnimatedCursorFollower.js: Difference between revisions
From Walkscape Walkthrough
mNo edit summary |
mNo edit summary |
||
| (4 intermediate revisions by the same user not shown) | |||
| Line 10: | Line 10: | ||
speed: 0.5, | speed: 0.5, | ||
offsetX: 30, | offsetX: 30, | ||
offsetY: 30 | offsetY: 30, | ||
defaultMode: 'page' | |||
}; | |||
const STORAGE_KEYS = { | |||
mode: 'animatedCursorFollowerMode', | |||
sprite: 'animatedCursorFollowerSprite' | |||
}; | }; | ||
| Line 21: | Line 27: | ||
reqId: null, | reqId: null, | ||
mounted: false, | mounted: false, | ||
hasMouse: false | hasMouse: false, | ||
wrapper: null, | |||
pet: null, | |||
centerOffset: 24 | |||
}; | }; | ||
function getMode() { | |||
const mode = localStorage.getItem(STORAGE_KEYS.mode); | |||
if (mode === 'saved' || mode === 'page') { | |||
return mode; | |||
} | |||
return CONFIG.defaultMode; | |||
} | |||
function setMode(mode) { | |||
if (mode !== 'saved' && mode !== 'page') return; | |||
localStorage.setItem(STORAGE_KEYS.mode, mode); | |||
} | |||
function getFrameSize(sprite) { | function getFrameSize(sprite) { | ||
const styles = window.getComputedStyle(sprite); | const styles = window.getComputedStyle(sprite); | ||
return parseInt(styles.getPropertyValue('--frame'), 10) || 48; | return parseInt(styles.getPropertyValue('--frame'), 10) || 48; | ||
} | |||
function resetSpriteFlip(sprite) { | |||
const transform = sprite.style.transform; | |||
if (!transform) return; | |||
sprite.style.transform = transform | |||
.replace(/scaleX\(\s*-1\s*\)/g, 'scaleX(1)') | |||
.trim(); | |||
} | |||
function saveSpriteData(sprite) { | |||
if (!sprite || !sprite.dataset.spriteApplied) return; | |||
const styles = window.getComputedStyle(sprite); | |||
const spriteUrl = | |||
sprite.getAttribute('data-sprite') || | |||
sprite.dataset.sprite || | |||
''; | |||
if (!spriteUrl) return; | |||
const data = { | |||
className: sprite.className, | |||
sprite: spriteUrl, | |||
title: sprite.getAttribute('title') || '', | |||
style: sprite.getAttribute('style') || '', | |||
frame: styles.getPropertyValue('--frame') || '48px', | |||
frames: styles.getPropertyValue('--frames') || '', | |||
frameMS: styles.getPropertyValue('--frameMS') || '', | |||
sheetW: styles.getPropertyValue('--sheetW') || '', | |||
backgroundImage: styles.backgroundImage || '' | |||
}; | |||
localStorage.setItem(STORAGE_KEYS.sprite, JSON.stringify(data)); | |||
} | |||
function loadSavedSprite() { | |||
const raw = localStorage.getItem(STORAGE_KEYS.sprite); | |||
if (!raw) return null; | |||
try { | |||
const data = JSON.parse(raw); | |||
if (!data || !data.sprite) return null; | |||
const sprite = document.createElement('span'); | |||
sprite.className = data.className || 'ws-sprite'; | |||
sprite.dataset.sprite = data.sprite; | |||
sprite.dataset.spriteApplied = '1'; | |||
if (data.title) { | |||
sprite.title = data.title; | |||
} | |||
if (data.style) { | |||
sprite.setAttribute('style', data.style); | |||
} | |||
sprite.style.backgroundImage = `url("${data.sprite}")`; | |||
if (data.frame) { | |||
sprite.style.setProperty('--frame', data.frame); | |||
} | |||
if (data.frames) { | |||
sprite.style.setProperty('--frames', data.frames); | |||
} | |||
if (data.frameMS) { | |||
sprite.style.setProperty('--frameMS', data.frameMS); | |||
} | |||
if (data.sheetW) { | |||
sprite.style.setProperty('--sheetW', data.sheetW); | |||
} | |||
sprite.style.setProperty('--scale', String(CONFIG.scale)); | |||
return sprite; | |||
} catch (e) { | |||
return null; | |||
} | |||
} | |||
function findPageSprite() { | |||
return document.querySelector( | |||
'span.ws-sprite[data-sprite-applied="1"]:not(.animated-cursor-follower-pet)' | |||
); | |||
} | |||
function chooseInitialSprite() { | |||
const mode = getMode(); | |||
const pageSprite = findPageSprite(); | |||
if (mode === 'saved') { | |||
return loadSavedSprite() || pageSprite; | |||
} | |||
return pageSprite; | |||
} | |||
function updateWrapperSize(sprite) { | |||
const frameSize = getFrameSize(sprite); | |||
state.centerOffset = (frameSize * CONFIG.scale) / 2; | |||
Object.assign(state.wrapper.style, { | |||
width: `${frameSize * CONFIG.scale}px`, | |||
height: `${frameSize * CONFIG.scale}px` | |||
}); | |||
} | |||
function setPetSource(newSourceNode, shouldSave) { | |||
if (!newSourceNode || !state.wrapper) return; | |||
const newPet = newSourceNode.cloneNode(true); | |||
newPet.classList.add('animated-cursor-follower-pet'); | |||
newPet.style.setProperty('--scale', String(CONFIG.scale)); | |||
Object.assign(newPet.style, { | |||
position: 'static', | |||
margin: '0', | |||
pointerEvents: 'none' | |||
}); | |||
resetSpriteFlip(newPet); | |||
updateWrapperSize(newPet); | |||
state.wrapper.replaceChildren(newPet); | |||
state.pet = newPet; | |||
if (shouldSave) { | |||
saveSpriteData(newSourceNode); | |||
} | |||
} | |||
function createSettingsPanel() { | |||
mw.loader.using('mediawiki.util').then(function () { | |||
const portletId = document.getElementById('p-personal') | |||
? 'p-personal' | |||
: 'p-tb'; | |||
const linkNode = mw.util.addPortletLink( | |||
portletId, | |||
'#', | |||
'Cursor pet', | |||
't-animated-cursor-follower', | |||
'Choose cursor pet behavior' | |||
); | |||
if (!linkNode) return; | |||
const link = linkNode.querySelector('a'); | |||
if (!link) return; | |||
const menu = document.createElement('div'); | |||
const currentMode = getMode(); | |||
menu.className = 'animated-cursor-follower-menu'; | |||
Object.assign(menu.style, { | |||
position: 'absolute', | |||
zIndex: '100000', | |||
display: 'none', | |||
background: '#fff', | |||
color: '#202122', | |||
border: '1px solid #a2a9b1', | |||
borderRadius: '4px', | |||
padding: '8px 10px', | |||
fontSize: '12px', | |||
lineHeight: '1.5', | |||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.25)', | |||
minWidth: '150px' | |||
}); | |||
menu.innerHTML = ` | |||
<div style="font-weight: bold; margin-bottom: 4px;">Cursor pet</div> | |||
<label style="display: block; white-space: nowrap;"> | |||
<input type="radio" name="animated-cursor-follower-mode" value="page"> | |||
Page sprite | |||
</label> | |||
<label style="display: block; white-space: nowrap;"> | |||
<input type="radio" name="animated-cursor-follower-mode" value="saved"> | |||
Saved sprite | |||
</label> | |||
`; | |||
document.body.appendChild(menu); | |||
const inputs = menu.querySelectorAll( | |||
'input[name="animated-cursor-follower-mode"]' | |||
); | |||
inputs.forEach(function (input) { | |||
input.checked = input.value === currentMode; | |||
input.addEventListener('change', function () { | |||
if (!input.checked) return; | |||
setMode(input.value); | |||
const selectedSprite = chooseInitialSprite(); | |||
if (selectedSprite) { | |||
setPetSource(selectedSprite, false); | |||
} | |||
}); | |||
}); | |||
link.addEventListener('click', function (e) { | |||
e.preventDefault(); | |||
const rect = link.getBoundingClientRect(); | |||
menu.style.left = `${rect.left + window.scrollX}px`; | |||
menu.style.top = `${rect.bottom + window.scrollY + 4}px`; | |||
menu.style.display = | |||
menu.style.display === 'none' ? 'block' : 'none'; | |||
}); | |||
document.addEventListener('click', function (e) { | |||
if ( | |||
menu.style.display === 'none' || | |||
menu.contains(e.target) || | |||
link.contains(e.target) | |||
) { | |||
return; | |||
} | |||
menu.style.display = 'none'; | |||
}); | |||
}); | |||
} | } | ||
| Line 33: | Line 297: | ||
state.mounted = true; | state.mounted = true; | ||
const wrapper = document.createElement('span'); | const wrapper = document.createElement('span'); | ||
state.wrapper = wrapper; | |||
wrapper.className = 'animated-cursor-follower-wrapper'; | wrapper.className = 'animated-cursor-follower-wrapper'; | ||
| Line 49: | Line 311: | ||
margin: '0', | margin: '0', | ||
padding: '0', | padding: '0', | ||
willChange: 'transform', | willChange: 'transform', | ||
display: 'none' | display: 'none' | ||
}); | }); | ||
document.body.appendChild(wrapper); | |||
setPetSource(sourceNode, false); | |||
createSettingsPanel(); | |||
document.addEventListener('mousemove', function (e) { | document.addEventListener('mousemove', function (e) { | ||
if (!state.hasMouse) { | if (!state.hasMouse) { | ||
state.hasMouse = true; | state.hasMouse = true; | ||
state.targetX = e.clientX; | state.targetX = e.clientX; | ||
state.targetY = e.clientY; | state.targetY = e.clientY; | ||
state.currentX = e.clientX; | state.currentX = e.clientX; | ||
state.currentY = e.clientY; | state.currentY = e.clientY; | ||
wrapper.style.display = ''; | wrapper.style.display = ''; | ||
} | } | ||
state.targetX = e.clientX; | state.targetX = e.clientX; | ||
state.targetY = e.clientY; | state.targetY = e.clientY; | ||
}, { passive: true }); | }, { passive: true }); | ||
document.documentElement.addEventListener('mouseout', function (e) { | document.documentElement.addEventListener('mouseout', function (e) { | ||
if (!e.relatedTarget && !e.toElement) { | if (!e.relatedTarget && !e.toElement) { | ||
| Line 94: | Line 343: | ||
} | } | ||
}); | }); | ||
document.addEventListener('click', function (e) { | |||
const clickedSprite = e.target.closest('span.ws-sprite'); | |||
if (!clickedSprite) return; | |||
if (clickedSprite.classList.contains('animated-cursor-follower-pet')) { | |||
return; | |||
} | |||
if (!clickedSprite.dataset.spriteApplied) { | |||
return; | |||
} | |||
setPetSource(clickedSprite, true); | |||
}, { passive: true }); | |||
function render() { | function render() { | ||
| Line 100: | Line 365: | ||
return; | return; | ||
} | } | ||
state.currentX += (state.targetX - state.currentX) * CONFIG.speed; | state.currentX += (state.targetX - state.currentX) * CONFIG.speed; | ||
state.currentY += (state.targetY - state.currentY) * CONFIG.speed; | state.currentY += (state.targetY - state.currentY) * CONFIG.speed; | ||
if (state.targetX > state.currentX + 1) { | if (state.targetX > state.currentX + 1) { | ||
state.facingRight = true; | state.facingRight = true; | ||
| Line 109: | Line 374: | ||
state.facingRight = false; | state.facingRight = false; | ||
} | } | ||
const scaleX = state.facingRight ? 1 : -1; | const scaleX = state.facingRight ? 1 : -1; | ||
const xOffset = state.facingRight | const xOffset = state.facingRight | ||
? -CONFIG.offsetX | ? -CONFIG.offsetX | ||
: CONFIG.offsetX; | : CONFIG.offsetX; | ||
wrapper.style.transform = | wrapper.style.transform = | ||
`translate3d(` + | `translate3d(` + | ||
`${state.currentX + xOffset - centerOffset}px, ` + | `${state.currentX + xOffset - state.centerOffset}px, ` + | ||
`${state.currentY + CONFIG.offsetY - centerOffset}px, 0)` + | `${state.currentY + CONFIG.offsetY - state.centerOffset}px, 0)` + | ||
` scaleX(${scaleX})`; | ` scaleX(${scaleX})`; | ||
state.reqId = requestAnimationFrame(render); | state.reqId = requestAnimationFrame(render); | ||
} | } | ||
| Line 128: | Line 393: | ||
} | } | ||
function init() { | function init(attempt) { | ||
const | const selectedSprite = chooseInitialSprite(); | ||
if (!selectedSprite) { | |||
if ((attempt || 0) < 40) { | |||
setTimeout(function () { | |||
init((attempt || 0) + 1); | |||
}, 250); | |||
} | |||
return; | return; | ||
} | } | ||
mountFollower( | mountFollower(selectedSprite); | ||
} | } | ||
if (document.readyState === 'complete') { | if (document.readyState === 'complete') { | ||
init(); | init(0); | ||
} else { | } else { | ||
window.addEventListener('load', init); | window.addEventListener('load', function () { | ||
init(0); | |||
}); | |||
} | } | ||
}()); | }()); | ||
Latest revision as of 01:52, 23 May 2026
/**
* Animated Cursor Follower
*/
(function () {
'use strict';
const CONFIG = {
scale: 1,
speed: 0.5,
offsetX: 30,
offsetY: 30,
defaultMode: 'page'
};
const STORAGE_KEYS = {
mode: 'animatedCursorFollowerMode',
sprite: 'animatedCursorFollowerSprite'
};
const state = {
targetX: 0,
targetY: 0,
currentX: 0,
currentY: 0,
facingRight: true,
reqId: null,
mounted: false,
hasMouse: false,
wrapper: null,
pet: null,
centerOffset: 24
};
function getMode() {
const mode = localStorage.getItem(STORAGE_KEYS.mode);
if (mode === 'saved' || mode === 'page') {
return mode;
}
return CONFIG.defaultMode;
}
function setMode(mode) {
if (mode !== 'saved' && mode !== 'page') return;
localStorage.setItem(STORAGE_KEYS.mode, mode);
}
function getFrameSize(sprite) {
const styles = window.getComputedStyle(sprite);
return parseInt(styles.getPropertyValue('--frame'), 10) || 48;
}
function resetSpriteFlip(sprite) {
const transform = sprite.style.transform;
if (!transform) return;
sprite.style.transform = transform
.replace(/scaleX\(\s*-1\s*\)/g, 'scaleX(1)')
.trim();
}
function saveSpriteData(sprite) {
if (!sprite || !sprite.dataset.spriteApplied) return;
const styles = window.getComputedStyle(sprite);
const spriteUrl =
sprite.getAttribute('data-sprite') ||
sprite.dataset.sprite ||
'';
if (!spriteUrl) return;
const data = {
className: sprite.className,
sprite: spriteUrl,
title: sprite.getAttribute('title') || '',
style: sprite.getAttribute('style') || '',
frame: styles.getPropertyValue('--frame') || '48px',
frames: styles.getPropertyValue('--frames') || '',
frameMS: styles.getPropertyValue('--frameMS') || '',
sheetW: styles.getPropertyValue('--sheetW') || '',
backgroundImage: styles.backgroundImage || ''
};
localStorage.setItem(STORAGE_KEYS.sprite, JSON.stringify(data));
}
function loadSavedSprite() {
const raw = localStorage.getItem(STORAGE_KEYS.sprite);
if (!raw) return null;
try {
const data = JSON.parse(raw);
if (!data || !data.sprite) return null;
const sprite = document.createElement('span');
sprite.className = data.className || 'ws-sprite';
sprite.dataset.sprite = data.sprite;
sprite.dataset.spriteApplied = '1';
if (data.title) {
sprite.title = data.title;
}
if (data.style) {
sprite.setAttribute('style', data.style);
}
sprite.style.backgroundImage = `url("${data.sprite}")`;
if (data.frame) {
sprite.style.setProperty('--frame', data.frame);
}
if (data.frames) {
sprite.style.setProperty('--frames', data.frames);
}
if (data.frameMS) {
sprite.style.setProperty('--frameMS', data.frameMS);
}
if (data.sheetW) {
sprite.style.setProperty('--sheetW', data.sheetW);
}
sprite.style.setProperty('--scale', String(CONFIG.scale));
return sprite;
} catch (e) {
return null;
}
}
function findPageSprite() {
return document.querySelector(
'span.ws-sprite[data-sprite-applied="1"]:not(.animated-cursor-follower-pet)'
);
}
function chooseInitialSprite() {
const mode = getMode();
const pageSprite = findPageSprite();
if (mode === 'saved') {
return loadSavedSprite() || pageSprite;
}
return pageSprite;
}
function updateWrapperSize(sprite) {
const frameSize = getFrameSize(sprite);
state.centerOffset = (frameSize * CONFIG.scale) / 2;
Object.assign(state.wrapper.style, {
width: `${frameSize * CONFIG.scale}px`,
height: `${frameSize * CONFIG.scale}px`
});
}
function setPetSource(newSourceNode, shouldSave) {
if (!newSourceNode || !state.wrapper) return;
const newPet = newSourceNode.cloneNode(true);
newPet.classList.add('animated-cursor-follower-pet');
newPet.style.setProperty('--scale', String(CONFIG.scale));
Object.assign(newPet.style, {
position: 'static',
margin: '0',
pointerEvents: 'none'
});
resetSpriteFlip(newPet);
updateWrapperSize(newPet);
state.wrapper.replaceChildren(newPet);
state.pet = newPet;
if (shouldSave) {
saveSpriteData(newSourceNode);
}
}
function createSettingsPanel() {
mw.loader.using('mediawiki.util').then(function () {
const portletId = document.getElementById('p-personal')
? 'p-personal'
: 'p-tb';
const linkNode = mw.util.addPortletLink(
portletId,
'#',
'Cursor pet',
't-animated-cursor-follower',
'Choose cursor pet behavior'
);
if (!linkNode) return;
const link = linkNode.querySelector('a');
if (!link) return;
const menu = document.createElement('div');
const currentMode = getMode();
menu.className = 'animated-cursor-follower-menu';
Object.assign(menu.style, {
position: 'absolute',
zIndex: '100000',
display: 'none',
background: '#fff',
color: '#202122',
border: '1px solid #a2a9b1',
borderRadius: '4px',
padding: '8px 10px',
fontSize: '12px',
lineHeight: '1.5',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.25)',
minWidth: '150px'
});
menu.innerHTML = `
<div style="font-weight: bold; margin-bottom: 4px;">Cursor pet</div>
<label style="display: block; white-space: nowrap;">
<input type="radio" name="animated-cursor-follower-mode" value="page">
Page sprite
</label>
<label style="display: block; white-space: nowrap;">
<input type="radio" name="animated-cursor-follower-mode" value="saved">
Saved sprite
</label>
`;
document.body.appendChild(menu);
const inputs = menu.querySelectorAll(
'input[name="animated-cursor-follower-mode"]'
);
inputs.forEach(function (input) {
input.checked = input.value === currentMode;
input.addEventListener('change', function () {
if (!input.checked) return;
setMode(input.value);
const selectedSprite = chooseInitialSprite();
if (selectedSprite) {
setPetSource(selectedSprite, false);
}
});
});
link.addEventListener('click', function (e) {
e.preventDefault();
const rect = link.getBoundingClientRect();
menu.style.left = `${rect.left + window.scrollX}px`;
menu.style.top = `${rect.bottom + window.scrollY + 4}px`;
menu.style.display =
menu.style.display === 'none' ? 'block' : 'none';
});
document.addEventListener('click', function (e) {
if (
menu.style.display === 'none' ||
menu.contains(e.target) ||
link.contains(e.target)
) {
return;
}
menu.style.display = 'none';
});
});
}
function mountFollower(sourceNode) {
if (state.mounted) return;
state.mounted = true;
const wrapper = document.createElement('span');
state.wrapper = wrapper;
wrapper.className = 'animated-cursor-follower-wrapper';
Object.assign(wrapper.style, {
position: 'fixed',
top: '0',
left: '0',
pointerEvents: 'none',
zIndex: '99999',
margin: '0',
padding: '0',
willChange: 'transform',
display: 'none'
});
document.body.appendChild(wrapper);
setPetSource(sourceNode, false);
createSettingsPanel();
document.addEventListener('mousemove', function (e) {
if (!state.hasMouse) {
state.hasMouse = true;
state.targetX = e.clientX;
state.targetY = e.clientY;
state.currentX = e.clientX;
state.currentY = e.clientY;
wrapper.style.display = '';
}
state.targetX = e.clientX;
state.targetY = e.clientY;
}, { passive: true });
document.documentElement.addEventListener('mouseout', function (e) {
if (!e.relatedTarget && !e.toElement) {
state.hasMouse = false;
wrapper.style.display = 'none';
}
});
document.addEventListener('click', function (e) {
const clickedSprite = e.target.closest('span.ws-sprite');
if (!clickedSprite) return;
if (clickedSprite.classList.contains('animated-cursor-follower-pet')) {
return;
}
if (!clickedSprite.dataset.spriteApplied) {
return;
}
setPetSource(clickedSprite, true);
}, { passive: true });
function render() {
if (!state.hasMouse) {
state.reqId = requestAnimationFrame(render);
return;
}
state.currentX += (state.targetX - state.currentX) * CONFIG.speed;
state.currentY += (state.targetY - state.currentY) * CONFIG.speed;
if (state.targetX > state.currentX + 1) {
state.facingRight = true;
} else if (state.targetX < state.currentX - 1) {
state.facingRight = false;
}
const scaleX = state.facingRight ? 1 : -1;
const xOffset = state.facingRight
? -CONFIG.offsetX
: CONFIG.offsetX;
wrapper.style.transform =
`translate3d(` +
`${state.currentX + xOffset - state.centerOffset}px, ` +
`${state.currentY + CONFIG.offsetY - state.centerOffset}px, 0)` +
` scaleX(${scaleX})`;
state.reqId = requestAnimationFrame(render);
}
render();
}
function init(attempt) {
const selectedSprite = chooseInitialSprite();
if (!selectedSprite) {
if ((attempt || 0) < 40) {
setTimeout(function () {
init((attempt || 0) + 1);
}, 250);
}
return;
}
mountFollower(selectedSprite);
}
if (document.readyState === 'complete') {
init(0);
} else {
window.addEventListener('load', function () {
init(0);
});
}
}());
