Skip to content

Instantly share code, notes, and snippets.

@Epicinver
Created June 28, 2024 14:55
Show Gist options
  • Select an option

  • Save Epicinver/40e254c1a381bd4044e2d6758e9079de to your computer and use it in GitHub Desktop.

Select an option

Save Epicinver/40e254c1a381bd4044e2d6758e9079de to your computer and use it in GitHub Desktop.
I d k
/*
TODO:
- smooth the angle by taking weighted average of past positions
- use dynamic SVG to render proper shadow for all angles
- fix TS errors
*/
import React, { useEffect, useRef } from "react";
import "./styles.css";
export default function App() {
return (
<div className="App">
<Cursor />
<h1>Adaptive Cursor</h1>
<h2>Cursor angle adapts to the direction where it is heading.</h2>
</div>
);
}
type Point = { x: number; y: number };
const sum = (a: Point, b: Point) => ({ x: a.x + b.x, y: a.y + b.y });
const divide = (a: Point, s: number) => ({ x: a.x / s, y: a.y / s });
const average = (a: Point[]) => divide(a.reduce(sum), a.length);
const angle = (a: Point, b: Point) =>
(Math.atan2(b.y - a.y, b.x - a.x) * 180) / Math.PI;
const takeRight = (a: any[], n = 1) => a.slice(Math.max(a.length - n, 0));
const getPositionFromMouseEvent = (e: MouseEvent) => ({
x: e.clientX,
y: e.clientY
});
const getPositionFromTouchEvent = (e: TouchMoveEvent) => ({
x: e.touches[0].clientX,
y: e.touches[0].clientY
});
function Cursor() {
const cursorRef = useRef<SVGSVGElement>(null);
const previousPositionsRef = useRef<Point[]>([]);
const windowLength = 30;
const mutateCursorStyle = (position: Point, angle: number) => {
const cursorEl = cursorRef.current;
if (cursorEl !== null) {
cursorEl.style.top = `${position.y}px`;
cursorEl.style.left = `${position.x}px`;
// const normalizedAngle = (angle + 450) % 360
// cursorEl.style.filter = `drop-shadow(0 ${
// (angle + 90) > 90 ? "1px" : "-1px"
// } 1px rgba(0, 0, 0, .4))`;
// ugh
cursorEl.style.transform = `translate(-10px, -9px) rotate(20deg) rotate(${
angle + 90
}deg)`;
}
};
const hideRealCursor = () => {
const el = document.documentElement;
const originalCursor = el.style.cursor;
el.style.cursor = "none";
return () => {
el.style.cursor = originalCursor;
};
};
const moveFakeCursor = () => {
const onMouseMove = (e: MouseEvent) => {
const previousPositions = previousPositionsRef.current;
const currentPosition = getPositionFromMouseEvent(e);
previousPositions.push(currentPosition);
mutateCursorStyle(
currentPosition,
angle(average(previousPositions), currentPosition)
);
previousPositionsRef.current = takeRight(previousPositions, windowLength);
};
document.addEventListener("mousemove", onMouseMove);
return () => {
document.removeEventListener("mousemove", onMouseMove);
};
};
const moveFakeCursorViaTouch = () => {
const onTouchMove = (e: TouchEvent) => {
const previousPositions = previousPositionsRef.current;
const currentPosition = getPositionFromTouchEvent(e);
previousPositions.push(currentPosition);
mutateCursorStyle(
currentPosition,
angle(average(previousPositions), currentPosition)
);
previousPositionsRef.current = takeRight(previousPositions, windowLength);
};
document.addEventListener("touchmove", onTouchMove);
return () => {
document.removeEventListener("mousemove", onTouchMove);
};
};
useEffect(moveFakeCursorViaTouch, []);
useEffect(moveFakeCursor, []);
useEffect(hideRealCursor, []);
return (
<svg
ref={cursorRef}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 28 28"
width={28}
height={28}
style={{
position: "fixed",
transition: "transform 10ms linear",
filter: "drop-shadow(0 1px 1px rgba(0, 0, 0, .4))",
transformOrigin: "10px 9px",
pointerEvents: "none",
userSelect: "none"
}}
>
<polygon
fill="#FFFFFF"
points="8.2,20.9 8.2,4.9 19.8,16.5 13,16.5 12.6,16.6 "
/>
<polygon fill="#FFFFFF" points="17.3,21.6 13.7,23.1 9,12 12.7,10.5 " />
<rect
x="12.5"
y="13.6"
transform="matrix(0.9221 -0.3871 0.3871 0.9221 -5.7605 6.5909)"
width="2"
height="8"
/>
<polygon points="9.2,7.3 9.2,18.5 12.2,15.6 12.6,15.5 17.4,15.5 " />
</svg>
);
}
import * as React from "react";
import { render } from "react-dom";
import App from "./App";
const rootElement = document.getElementById("root");
render(<App />, rootElement);
{
"name": "adaptive-cursor",
"version": "1.0.0",
"description": "",
"keywords": [],
"main": "src/index.tsx",
"dependencies": {
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-scripts": "3.3.0"
},
"devDependencies": {
"@types/react": "16.9.19",
"@types/react-dom": "16.9.5",
"typescript": "3.7.5"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}
.App {
font-family: sans-serif;
text-align: center;
}
{
"include": [
"./src/*"
],
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"lib": [
"dom",
"es2015"
],
"jsx": "react"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment