Tip of the day
Want to find something particular about the game and don't mind spoilers? Check the Wiki!

MediaWiki:Gadget-AnimatedCursorFollower.js: Difference between revisions

From Walkscape Walkthrough
mNo edit summary
mNo edit summary
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) {
Line 28: Line 53:
return parseInt(styles.getPropertyValue('--frame'), 10) || 48;
return parseInt(styles.getPropertyValue('--frame'), 10) || 48;
}
}
 
function resetSpriteFlip(sprite) {
function resetSpriteFlip(sprite) {
const transform = sprite.style.transform;
const transform = sprite.style.transform;
 
if (!transform) return;
if (!transform) return;
 
sprite.style.transform = transform
sprite.style.transform = transform
.replace(/scaleX\(\s*-1\s*\)/g, 'scaleX(1)')
.replace(/scaleX\(\s*-1\s*\)/g, 'scaleX(1)')
.trim();
.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() {
const panel = document.createElement('div');
const currentMode = getMode();
panel.className = 'animated-cursor-follower-settings';
Object.assign(panel.style, {
position: 'fixed',
right: '12px',
bottom: '12px',
zIndex: '100000',
background: 'rgba(255, 255, 255, 0.92)',
color: '#202122',
border: '1px solid #a2a9b1',
borderRadius: '6px',
padding: '6px 8px',
fontSize: '12px',
lineHeight: '1.4',
boxShadow: '0 2px 6px rgba(0, 0, 0, 0.2)'
});
panel.innerHTML = `
<div style="font-weight: bold; margin-bottom: 3px;">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(panel);
const inputs = panel.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);
}
});
});
}
}


Line 44: Line 253:


const wrapper = document.createElement('span');
const wrapper = document.createElement('span');
const frameSize = getFrameSize(sourceNode);
const centerOffset = (frameSize * CONFIG.scale) / 2;


let pet = null;
state.wrapper = wrapper;


wrapper.className = 'animated-cursor-follower-wrapper';
wrapper.className = 'animated-cursor-follower-wrapper';
Line 59: Line 266:
margin: '0',
margin: '0',
padding: '0',
padding: '0',
width: `${frameSize * CONFIG.scale}px`,
height: `${frameSize * CONFIG.scale}px`,
willChange: 'transform',
willChange: 'transform',
display: 'none'
display: 'none'
});
});


function setPetSource(newSourceNode) {
document.body.appendChild(wrapper);
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);
wrapper.replaceChildren(newPet);
pet = newPet;
}


setPetSource(sourceNode);
setPetSource(sourceNode, false);
document.body.appendChild(wrapper);
createSettingsPanel();


document.addEventListener('mousemove', function (e) {
document.addEventListener('mousemove', function (e) {
Line 123: Line 312:
}
}


setPetSource(clickedSprite);
setPetSource(clickedSprite, true);
}, { passive: true });
}, { passive: true });


Line 149: Line 338:
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})`;


Line 159: Line 348:
}
}


function init() {
function init(attempt) {
const sprite = document.querySelector('span.ws-sprite');
const selectedSprite = chooseInitialSprite();
 
if (!selectedSprite) {
if ((attempt || 0) < 40) {
setTimeout(function () {
init((attempt || 0) + 1);
}, 250);
}


if (!sprite || !sprite.dataset.spriteApplied) {
setTimeout(init, 250);
return;
return;
}
}


mountFollower(sprite);
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);
});
}
}


}());
}());

Revision as of 01:47, 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() {
		const panel = document.createElement('div');
		const currentMode = getMode();

		panel.className = 'animated-cursor-follower-settings';

		Object.assign(panel.style, {
			position: 'fixed',
			right: '12px',
			bottom: '12px',
			zIndex: '100000',
			background: 'rgba(255, 255, 255, 0.92)',
			color: '#202122',
			border: '1px solid #a2a9b1',
			borderRadius: '6px',
			padding: '6px 8px',
			fontSize: '12px',
			lineHeight: '1.4',
			boxShadow: '0 2px 6px rgba(0, 0, 0, 0.2)'
		});

		panel.innerHTML = `
			<div style="font-weight: bold; margin-bottom: 3px;">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(panel);

		const inputs = panel.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);
				}
			});
		});
	}

	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);
		});
	}

}());