I am having trouble trying to implement a "prize wheel," using canvas. I am using something similar to the canvas "roulette wheel" http://jsfiddle.net/wYhvB/4/ that is floating around StackOverflow. My dilema is when you click spin, in the background I make an API call that returns an id of which prize should actually be chosen, the interface is nothing more than eye candy. I am pushing all the prize descriptions into the first array, how can I add an id into each one of the arcs and stop on a particular one instead of stopping on a specifically random time? I.E. If the API returns "car," I want this wheel to spin a few times and stop on car.
var colors = ["##eaeaea", "##cccccc", "##eaeaea", "##cccccc",
"##eaeaea", "##cccccc", "##eaeaea", "##cccccc"];
// NEED to pre load this data prior
var prize_descriptions = ["car","house","etc..."]; // These are injected on an init call from an api
var current_user_status = {};
var startAngle = 0;
var arc = Math.PI / 4;
var spinTimeout = null;
var spinArcStart = 10;
var spinTime = 0;
var spinTimeTotal = 0;
var current_user_status = null;
var spin_results = null;
var ctx;
function drawSpinnerWheel() {
var canvas = document.getElementById("canvas");
if (canvas.getContext) {
var outsideRadius = 200;
var textRadius = 160;
var insideRadius = 125;
ctx = canvas.getContext("2d");
ctx.clearRect(0,0,500,500);
ctx.strokeStyle = "black";
ctx.lineWidth = 2;
ctx.font = 'bold 12px Helvetica, Arial';
for(var i = 0; i < 8; i++) {
var angle = startAngle + i * arc;
ctx.fillStyle = colors[i];
ctx.beginPath();
ctx.arc(250, 250, outsideRadius, angle, angle + arc, false);
ctx.arc(250, 250, insideRadius, angle + arc, angle, true);
ctx.stroke();
ctx.fill();
ctx.save();
ctx.shadowOffsetX = -1;
ctx.shadowOffsetY = -1;
ctx.shadowBlur = 0;
ctx.shadowColor = "rgb(220,220,220)";
ctx.fillStyle = "black";
ctx.translate(250 + Math.cos(angle + arc / 2) * textRadius,
250 + Math.sin(angle + arc / 2) * textRadius);
ctx.rotate(angle + arc / 2 + Math.PI / 2);
var text = prize_descriptions[i];
if (text == undefined)
text = "Not this time! "+i;
ctx.fillText(text, -ctx.measureText(text).width / 2, 0);
ctx.restore();
}
//Arrow
ctx.fillStyle = "black";
ctx.beginPath();
ctx.moveTo(250 - 4, 250 - (outsideRadius + 5));
ctx.lineTo(250 + 4, 250 - (outsideRadius + 5));
ctx.lineTo(250 + 4, 250 - (outsideRadius - 5));
ctx.lineTo(250 + 9, 250 - (outsideRadius - 5));
ctx.lineTo(250 + 0, 250 - (outsideRadius - 13));
ctx.lineTo(250 - 9, 250 - (outsideRadius - 5));
ctx.lineTo(250 - 4, 250 - (outsideRadius - 5));
ctx.lineTo(250 - 4, 250 - (outsideRadius + 5));
ctx.fill();
}
}
function spin() {
spinAngleStart = Math.random() * 10 + 10;
spinTime = 0;
spinTimeTotal = Math.random() * 3 + 4 * 1000;
rotateWheel();
}
function rotateWheel() {
spinTime += 30;
if(spinTime >= spinTimeTotal) {
stopRotateWheel();
return;
}
var spinAngle = spinAngleStart - easeOut(spinTime, 0, spinAngleStart, spinTimeTotal);
startAngle += (spinAngle * Math.PI / 180);
drawSpinnerWheel();
spinTimeout = setTimeout('rotateWheel()', 30);
}
function stopRotateWheel() {
clearTimeout(spinTimeout);
var degrees = startAngle * 180 / Math.PI + 90;
var arcd = arc * 180 / Math.PI;
var index = Math.floor((360 - degrees % 360) / arcd);
ctx.save();
ctx.font = 'bold 30px Helvetica, Arial';
var text = prize_descriptions[index];
ctx.fillText(text, 250 - ctx.measureText(text).width / 2, 250 + 10);
ctx.restore();
}
function easeOut(t, b, c, d) {
var ts = (t/=d)*t;
var tc = ts*t;
return b+c*(tc + -3*ts + 3*t);
}
drawSpinnerWheel();
$("#spin").bind('click', function(e) {
e.preventDefault();
spin();
});
I have some experience in creating HTML5 canvas winning wheels, the way that I solved the problem of getting the wheel to stop at at a particular prize determined by a server-side process was to define an array of prizes that correspond to what is shown on the wheel, specifying the start and end angle of each prize in degrees, and then before the wheel is rotated setting a targetAngle to a random value between the start and end angle of the prize in question plus adding a multiple of 360 degrees so that the wheel spins a few times before slowing to a stop at the predetermined prize.
For example if the wheel has 4 prizes then the prizes array is...
var prizes = new Array();
prizes[0] = {"name" : "Prize 1", "startAngle" : 0, "endAngle" : 89};
prizes[1] = {"name" : "Prize 2", "startAngle" : 90, "endAngle" : 179};
prizes[2] = {"name" : "Prize 3", "startAngle" : 180, "endAngle" : 269};
prizes[3] = {"name" : "Prize 4", "startAngle" : 270, "endAngle" : 359};
And the code to set the targetAngle is something like...
targetAngle = Math.floor(prizes[determinedPrize]['startAngle'] + (Math.random() * (prizes[determinedPrize]['endAngle'] - prizes[determinedPrize]['startAngle'])));
targetAngle += (360 * 18);
The spinning function of the wheel then loops until the current angle of the wheel equals the targetAngle.
A working example of my prize wheel and the fully commented source code is available at http://www.dougtesting.net. The pre-determined feature is not enabled in the online example, but can easily be turned on in the source code (winwheel.js) once downloaded.
The easiest way I can think of is to take the current position of the wheel, then calculate the distance from this point to the prize. Add a random number of multiples of the wheel diameter circumference and then you have a distance. The edge of the wheel must travel through this distance to end up at the prize.
Just like you can use linear or cubic interpolation to move an element from 1 position to another in a specified number of steps, you can use the same approach to rotate the wheel from point 0 (start point) to point 1 (end point) from time=0 to time=1
This page Math: Ease In, ease Out a displacement using Hermite curve with time constraint is a good read. It's where I managed to wrap my head around doing basically the same thing - just up/down/left/right, rather than rotationally.
It's a bit choppy while im look at iot just now. Dont know if it's jsfiddle, the missing images or the 25 browser tabs & programs I have running.
Anyway, the point is to use non-linear interpolation to get to a specified distance away in a specified number of steps. It should get there in a specified time, but not with 25 windows open.. :laughs:
Check out the SO link above. It's got some great pictures that really explain quite well.
Here's a fiddle of cubic-spline interpolation for the time.
http://jsfiddle.net/enhzflep/XKzGF/
And here's the full code:
<!DOCTYPE html>
<html>
<head>
<script>
var continuePlaying = true, isPlaying=false;
function byId(a){return document.getElementById(a)}
function myInit()
{
}
window.addEventListener("load",myInit,!1);
function cubicHermite(a,b,d,e,c){var g=a*a,f=g*a;return(2*f-3*g+1)*b+(f-2*g+a)*e+(-2*f+3*g)*d+(f-g)*c}
function interp(a,b,d,e,c){var g,f;f=e/(a/2+b+d/2);g=f*a/2;f*=b;return result=c<=a?cubicHermite(c/a,0,g,0,f/b*a):c<=a+b?g+f*(c-a)/b:cubicHermite((c-a-b)/d,g+f,e,f/b*d,0)}
function linear(a){return a}
function cubic(a){return interp(0.35,0.3,0.35,1,a)}
function getSize(a){return{left:a.offsetLeft,top:a.offsetTop,width:a.clientWidth,height:a.clientHeight}}
function doAnim2(a,b,d,e){var c=a/b;setTimeout(function(){doAnimStep(0,b,c,d,e)},c)}
function doAnimStep(a,b,d,e,c){a<=b?(setTimeout(function(){doAnimStep(a,b,d,e,c)},d),e(a/b),a++):void 0!=c&&null!=c&&c()}
//scroll with cubic interpolation of the current scroll position
function cubicScrollDown(b,callback)
{
var a=byId(b),c=a.scrollHeight-a.clientHeight;
doAnim2(500,c,function(b){a.scrollTop=c*cubic(b)},callback);
}
function cubicScrollUp(b,callback)
{
var a=byId(b),c=a.scrollHeight-a.clientHeight;
doAnim2(500,c,function(b){ a.scrollTop=c*(1-cubic(b)) },callback );
}
//scroll with cubic interpolation of the current scroll position
function linearScrollDown(b, callback)
{
var a=byId(b),c=a.scrollHeight-a.clientHeight;
doAnim2(500,c,function(b){a.scrollTop=c*linear(b)}, callback);
}
function linearScrollUp(b, callback)
{
var a=byId(b),c=a.scrollHeight-a.clientHeight;
doAnim2(1000,c,function(b){ a.scrollTop=c*(1-linear(b)) }, callback );
}
function animFadeOut(elem, callback)
{
doAnim2(500,50,function(raw){elem.style.opacity=1-cubic(raw)},callback);
}
function animFadeIn(elem, callback)
{
doAnim2(500,50,function(raw){elem.style.opacity=cubic(raw)},callback);
}
function cubicBounce(b)
{
cubicScrollDown(b, downCallback);
function downCallback()
{
cubicScrollUp(b, upCallback);
}
function upCallback()
{
if (continuePlaying===true)
setTimeout( function(){cubicBounce(b);}, 0);
else
continuePlaying = true;
}
}
function linearBounce(b)
{
linearScrollDown(b, downCallback);
function downCallback()
{
linearScrollUp(b, upCallback);
}
function upCallback()
{
if (continuePlaying===true)
setTimeout( function(){linearBounce(b);}, 0);
else
continuePlaying = true;
}
}
function fadeOutIn(tgtListIdStr)
{
var tgt = byId(tgtListIdStr);
animFadeOut(tgt,fadedOutCallback);
function fadedOutCallback()
{
animFadeIn(tgt);
}
}
function prependChild(parent, element)
{
if (parent.childNodes)
parent.insertBefore(element, parent.childNodes[0]);
else
parent.appendChild(element)
}
function slideUpRemove(tgtListIdStr)
{
var tgt = byId(tgtListIdStr);
var listItems = tgt.getElementsByTagName('li');
mHeight = listItems[0].clientHeight;
animFadeOut(listItems[0], slideUp);
function slideUp()
{
doAnim2(500, 50, slideUpStep, slideUpDone);
function slideUpStep(raw)
{
listItems[0].style.height = (cubic(1-raw) * mHeight) + 'px';
}
function slideUpDone()
{
dummy = listItems[0];
tgt.appendChild(dummy);
//dummy.removeAttribute('style');
dummy.style.height = null;
dummy.style.opacity = null;
}
}
}
function slideDownInsert(tgtListIdStr)
{
// get the container, it's items and the height of the last LI item.
var tgt = byId(tgtListIdStr);
var listItems = tgt.getElementsByTagName('li');
mHeight = listItems[listItems.length-1].clientHeight;
// create a dummy to take the place of the last item, set it's size and height. make it the first child of the containing list
var dummy = document.createElement('li');
dummy.style.opacity = 0;
dummy.style.height = 0 + 'px';
prependChild(tgt, dummy);
// animate it!
doAnim2(500, 50, slideDownStep,slideDownDone);
function slideDownStep(raw)
{
dummy.style.height = (cubic(raw) * mHeight)+'px';
}
function slideDownDone()
{
// remove the dummy
var newItem = listItems[listItems.length-1];
newItem.style.opacity = 0;
prependChild(tgt, newItem);
tgt.removeChild(dummy);
animFadeIn(newItem, function(){newItem.removeAttribute('style')});
}
}
</script>
<style>
#myListDiv
{
width: 256px;
padding: 6px;
height: 128px;
overflow-y: hidden; /*scroll;*/
border-radius: 6px;
border: solid 1px transparent;
border-color: rgba(0,0,0,0.2) rgba(255,255,255,0.4) rgba(255,255,255,0.4) rgba(0,0,0,0.2);
/* background-image: url(img/rss128.png); */
background-color: rgba(0,0,0,0.1);
}
h4, p
{
margin: 6px 0;
}
ul
{
padding: 0;
list-style: none;
margin: 0;
}
ul#mList li
{
padding: 0 8px;
margin: 0 6px;
display: block;
border: solid 1px #cccccc;
border-bottom-color: #000;
border-color: #ccc transparent #000 transparent;
vertical-align: middle;
background-color: rgba(150,150,150,0.95);
overflow: hidden;
}
.thumb
{
width: 48px;
height: 48px;
float: left;
}
.thumb img
{
height: 48px;
}
#mPanel
{
display: inline-block;
float: left;
padding: 32px;
background-color: hsl(80,50%,20%);
}
</style>
</head>
<body>
<div id='mPanel'>
<div id='myListDiv'>
<ul id='mList'>
<li><div class='thumb'><img src='img/opera.svg'></div><div class='itemData'><h4><a>Item #1</a></h4><p>some assorted text</p></div></li>
<li><div class='thumb'><img src='img/chromeEyes.svg'></div><h4><a>Item #2</a></h4><p>some assorted text</p></li>
<li><div class='thumb'><img src='img/girl.png'></div><h4><a>Item #3</a></h4><p>some assorted text</p></li>
<li><div class='thumb'><img src='img/chuck-norris.jpg'></div><h4><a>Item #1</a></h4><p>some assorted text</p></li>
<li><div class='thumb'><img src='img/redBaron.jpg'></div><h4><a>Item #2</a></h4><p>some assorted text</p></li>
<li><div class='thumb'><img src='img/default.png'></div><h4><a>Item #3</a></h4><p>some assorted text</p></li>
</ul>
</div>
</div>
<button onclick='cubicScrollDown("myListDiv")'>Cubic down</button>
<button onclick='cubicScrollUp("myListDiv")'>Cubic up</button><br>
<button onclick='cubicBounce("myListDiv")'>cubic bounce</button>
<button onclick='linearBounce("myListDiv")'>linear bounce</button><br>
<input type='button' onclick='slideUpRemove("mList")' value='newest'/>
<input type='button' onclick='slideDownInsert("mList")' value='Slide Down'/><br>
<button onclick='continuePlaying=false'>Stop Anim cycle</button>
<input type='button' onclick='fadeOutIn("mList");' value='fadeOutIn'/><br>
</body>
</html>
Related
I started learning Three js and I was looking for a way to convert a color map into a normal map. What I want to do is to try and make the normal map based on this color map [image 1], by changing the pixels based on their color so it looks like this normal map [image 2]. I don't want to simply upload the files since I'm trying to minimize the weight of the project as much as possible. Here is what I already tried :
let img = new Image();
img.src = './texture/color.jpg';
img.onload = function () {
let canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
document.getElementById('body').appendChild(canvas)
const c = canvas.getContext('2d')
c.clearRect(0, 0, canvas.width, canvas.height);
c.fillStyle = '#EEEEEE';
c.fillRect(0,0,canvas.width, canvas.height);
//draw background image
c.drawImage(img, 0, 0);
//draw a box over the top
c.fillStyle = "rgba(200, 0, 0, 0)";
c.fillRect(0, 0, canvas.width, canvas.height);
draw(c, canvas);
};
function draw(c, canvas)
{
let img2 = c.getImageData(0, 0, canvas.width,canvas.height);
console.log(img2.data)
let d = img2.data;
for (let i=0; i<d.length; i+=4) {
let r = d[i];
let g = d[i+1];
let b = d[i+2];
v1 = r < 75 ? r / (50 - r) : r * (255 - r);
v2 = g > 75 ? g / (50 - g) : g * (255 - g);
v3 = b > 75 ? b / (50 - b) : b * (255 - b);
d[i] = v1;
d[i+1] = v2;
d[i+2] = v3;
}
console.log(img2.data)
c.putImageData(img2, 0, 0);
}
I can't say what Three.js can or can't do because all I really know of it is that it makes integrating 3d assets with canvases a breeze.
Aside from that, I wrote a pure javascript function that serves the purpose of generating normal maps from color maps quite effectively. Keep in mind, however, that this is a quick port to js of a function I wrote for C# winforms about 4 years ago, one that loops through all the pixels of a given image to extrapolate the data required for the conversion. It's slow. Seriously, painfully slow and it is so because getting nice, crisp, accurate normal maps from recursive algorithms is painfully slow.
But it does exactly what you want it to do; generate a very nice, clean, precise normal map from a given color map and it's simple enough to understand its functionality.
I've set this up as a live demo so you can see it / feel it in action.
There is, of course, a much faster solution involving making a single call for pixel data, iterating over its corresponding 1d array, saving calculated data back to that array then plopping the entire array, itself, down on the output canvas all at once but that involves some interesting virtual multi-dimensional trickery for Sobel but, for the sake of providing a clear, understandable example that works, I'm going with old-school nested recursion so you can see Sobel in action and how pixels are manipulated to arrive at normalization.
I did not implement any fancy asynchronous updates so you'll only know this is processing because, once initiated, the hand cursor used for the button won't return to the default arrow until map generation is complete.
I've also included 4 variations of your original image to play with, all in code with 3 of the 4 commented out. The app starts at 256x256 as the time it takes to generate a normal map from that with recursion is reasonable. Their sizes range from 128 to the original 1024, though I highly advise not engaging the full scale variant as your browser may whine about how long the operation takes.
As with the C# variant, you can implement a means by which client can control the intensity of the resulting normal calculations by adjusting the brightness parameters. Beyond the C# variant, this definitely can be a basis for generating normal maps to visualize in real-time as applied to geometry with Three.js. And by "real-time", I mean however long it takes to generate an x-scale map with nested recursion because the actual application of a completed map to geometry occurs in milliseconds.
Here's a screenshot of the results after processing the 256x256:
To accompany the live demo, here's the code:
normalize.htm
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Normalizer</title>
<link rel="stylesheet" href="css/normalize.css">
</head>
<body onload="startup()">
<canvas id="input" class="canvas"></canvas>
<canvas id="output" class="canvas"></canvas>
<div class="progress">
<input type="button" class="button" onclick="totallyNormal()" value="Normalize!" />
<span id="progress">Ready to rock and / or roll on your command!</span>
</div>
<script src="js/normalize.js"></script>
</body>
</html>
normalize.css
html {
margin: 0px;
padding: 0px;
width: 100vw;
height: 100vh;
}
body {
margin: 0px;
padding: 0px;
width: 100vw;
height: 100vh;
overflow: hidden;
font-family: "Arial Unicode MS";
background: linear-gradient(330deg, rgb(150, 150, 150), rgb(200, 200, 200));
background-color: rgb(200, 200, 200);
display: flex;
align-items: center;
justify-content: center;
}
.canvas {
outline: 1px solid hsla(0, 0%, 0%, 0.25);
}
.progress {
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 40px;
display: flex;
}
.progress span {
width: calc(100% - 160px);
height: 40px;
line-height: 40px;
color: hsl(0, 0%, 0%);
text-align: center;
}
input[type="button"] {
margin: 0px;
width: 120px;
height: 40px;
cursor: pointer;
display: inline;
}
normalize.js
// Javascript Normal Map Generator
// Copyright © Brian "BJS3D" Spencer 2022
// Incorporating W3C proposed algorithm to
// calculate pixel brightness in conjunction
// with the Sobel Operator.
var input, output, ctx_i, ctx_o, w, h;
function startup() {
var img;
input = document.getElementById("input");
ctx_i = input.getContext("2d");
ctx_i.clearRect(0, 0,input.width, input.height);
img = new Image();
img.crossOrigin = "Anonymous";
//img.src = "https://i.imgur.com/a4N2Aj4.jpg"; //128x128 - Tiny but fast.
img.src = "https://i.imgur.com/wFe4EG7.jpg"; //256x256 - Takes about a minute.
//img.src = "https://i.imgur.com/bm4pXrn.jpg"; //512x512 - May take 5 or 10 minutes.
//img.src = "https://i.imgur.com/aUIdxHH.jpg"; //original - Don't do it! It'll take hours.
img.onload = function () {
w = img.width - 1;
h = img.height - 1;
input.width = w + 1;
input.height = h + 1;
ctx_i.drawImage(img, 0, 0);
output = document.getElementById("output");
ctx_o = output.getContext("2d");
output.width = w + 1;
output.height = h + 1;
};
}
function totallyNormal() {
var pixel, x_vector, y_vector;
for (var y = 0; y < w + 1; y += 1) {
for (var x = 0; x < h + 1; x += 1) {
var data = [0, 0, 0, 0, x > 0, x < w, y > 1, y < h, x - 1, x + 1, x, x, y, y, y - 1, y + 1];
for (var z = 0; z < 4; z +=1) {
if (data[z + 4]) {
pixel = ctx_i.getImageData(data[z + 8], data[z + 12], 1, 1);
data[z] = ((0.299 * (pixel.data[0] / 100)) + (0.587 * (pixel.data[1] / 100)) + (0.114 * (pixel.data[2] / 100)) / 3);
} else {
pixel = ctx_i.getImageData(x, y, 1, 1);
data[z] = ((0.299 * (pixel.data[0] / 100)) + (0.587 * (pixel.data[1] / 100)) + (0.114 * (pixel.data[2] / 100)) / 3);
}
}
x_vector = parseFloat((Math.abs(data[0] - data[1]) + 1) * 0.5) * 255;
y_vector = parseFloat((Math.abs(data[2] - data[3]) + 1) * 0.5) * 255;
ctx_o.fillStyle = "rgba(" + x_vector + "," + y_vector + ",255,255)";
ctx_o.fillRect(x, y, 1, 1);
}
}
document.getElementById("progress").innerHTML = "Normal map generation complete.";
}
I have square that follows my cursor.
Its border top is red to see if the rotation is right.
I'm trying to rotate it depending on mouse movement angle. Like if mouse goes 45deg top right then square must rotate by 45deg.
The problem is that when I move my mouse slowly the square starts to rotate like crazy. But if I move my mouse fast enough square rotates pretty smooth.
Actually it's just a part of my task that I'm trying to accomplish. My whole task is to make custom circle cursor that stretches when mouse moving. The idea I'm trying to implement:
rotate circle by mouse movement angle and then just scaleX it to make stretching effect. But I cannot do it because of problem I described above. I need my follower to rotate smoothly when mouse speed is slow.
class Cursor {
constructor() {
this.prevX = null;
this.prevY = null;
this.curX = null;
this.curY = null;
this.angle = null;
this.container = document.querySelector(".cursor");
this.follower = this.container.querySelector(".cursor-follower");
document.addEventListener("mousemove", (event) => {
this.curX = event.clientX;
this.curY = event.clientY;
});
this.position();
}
position(timestamp) {
this.follower.style.top = `${this.curY}px`;
this.follower.style.left = `${this.curX}px`;
this.angle = Math.atan2(this.curY - this.prevY, this.curX - this.prevX) * 180/Math.PI;
console.log(this.angle + 90);
this.follower.style.transform = `rotateZ(${this.angle + 90}deg)`;
this.prevX = this.curX;
this.prevY = this.curY;
requestAnimationFrame(this.position.bind(this));
}
}
const cursor = new Cursor();
.cursor-follower {
position: fixed;
top: 0;
left: 0;
z-index: 9999;
pointer-events: none;
user-select: none;
width: 76px;
height: 76px;
margin: -38px;
border: 1.5px solid #000;
border-top: 1.5px solid red;
}
<div class="cursor">
<div class="cursor-follower"></div>
</div>
Following the cursor tangent smoothly isn't as simple as it first feels. In modern browsers mousemove event fires nearby at the frame rate (typically 60 FPS). When the mouse is moving slowly, the cursor moves only a pixel or two between the events. When calculating the angle, vertical + horizontal move of 1px resolves to 45deg. Then there's another problem, the event firing rate is not consistent, during the mouse is moving, event firing rate can drop to 30 FPS or even to 24 FPS, which actually helps to get more accurate angle, but makes the scale calculations heavily inaccurate (your real task seems to need scale calculations too).
One solution is to use CSS Transitions to make the animation smoother. However, adding a transition makes the angle calculations much more complex, because the jumps between negative and positive angles Math.atan2 returns when crossing PI will become visible when using transition.
Here's a sample code of how to use transition to make the cursor follower smoother.
class Follower {
// Default options
threshold = 4;
smoothness = 10;
stretchRate = 100;
stretchMax = 100;
stretchSlow = 100;
baseAngle = Math.PI / 2;
// Class initialization
initialized = false;
// Listens mousemove event
static moveCursor (e) {
if (Follower.active) {
Follower.prototype.crsrMove.call(Follower.active, e);
}
}
static active = null;
// Adds/removes mousemove listener
static init () {
if (this.initialized) {
document.removeEventListener('mousemove', this.moveCursor);
if (this.active) {
this.active.cursor.classList.add('hidden');
}
} else {
document.addEventListener('mousemove', this.moveCursor);
}
this.initialized = !this.initialized;
}
// Base values of instances
x = -1000;
y = -1000;
angle = 0;
restoreTimer = -1;
stamp = 0;
speed = [0];
// Prototype properties
constructor (selector) {
this.cursor = document.querySelector(selector);
this.restore = this.restore.bind(this);
}
// Activates a new cursor
activate (options = {}) {
// Remove the old cursor
if (Follower.active) {
Follower.active.cursor.classList.add('hidden');
Follower.active.cursor.classList.remove('cursor', 'transitioned');
}
// Set the new cursor
Object.assign(this, options);
this.setCss = this.cursor.style.setProperty.bind(this.cursor.style);
this.cursor.classList.remove('hidden');
this.cHW = this.cursor.offsetWidth / 2;
this.cHH = this.cursor.offsetHeight / 2;
this.setCss('--smoothness', this.smoothness / 100 + 's');
this.cursor.classList.add('cursor');
setTimeout(() => this.cursor.classList.add('transitioned'), 0); // Snap to the current angle
this.crsrMove({
clientX: this.x,
clientY: this.y
});
Follower.active = this;
return this;
}
// Moves the cursor with effects
crsrMove (e) {
clearTimeout(this.restoreTimer); // Cancel reset timer
const PI = Math.PI,
pi = PI / 2,
x = e.clientX,
y = e.clientY,
dX = x - this.x,
dY = y - this.y,
dist = Math.hypot(dX, dY);
let rad = this.angle + this.baseAngle,
dTime = e.timeStamp - this.stamp,
len = this.speed.length,
sSum = this.speed.reduce((a, s) => a += s),
speed = dTime
? ((1000 / dTime) * dist + sSum) / len
: this.speed[len - 1], // Old speed when dTime = 0
scale = Math.min(
this.stretchMax / 100,
Math.max(speed / (500 - this.stretchRate || 1),
this.stretchSlow / 100
)
);
// Update base values and rotation angle
if (isNaN(dTime)) {
scale = this.scale;
} // Prevents a snap of a new cursor
if (len > 5) {
this.speed.length = 1;
}
// Update angle only when mouse has moved enough from the previous update
if (dist > this.threshold) {
let angle = Math.atan2(dY, dX),
dAngle = angle - this.angle,
adAngle = Math.abs(dAngle),
cw = 0;
// Smoothen small angles
if (adAngle < PI / 90) {
angle += dAngle * 0.5;
}
// Crossing ±PI angles
if (adAngle >= 3 * pi) {
cw = -Math.sign(dAngle) * Math.sign(dX); // Rotation direction: -1 = CW, 1 = CCW
angle += cw * 2 * PI - dAngle; // Restores the current position with negated angle
// Update transform matrix without transition & rendering
this.cursor.classList.remove('transitioned');
this.setCss('--angle', `${angle + this.baseAngle}rad`);
this.cursor.offsetWidth; // Matrix isn't updated without layout recalculation
this.cursor.classList.add('transitioned');
adAngle = 0; // The angle was handled, prevent further adjusts
}
// Orthogonal mouse turns
if (adAngle >= pi && adAngle < 3 * pi) {
this.cursor.classList.remove('transitioned');
setTimeout(() => this.cursor.classList.add('transitioned'), 0);
}
rad = angle + this.baseAngle;
this.x = x;
this.y = y;
this.angle = angle;
}
this.scale = scale;
this.stamp = e.timeStamp;
this.speed.push(speed);
// Transform the cursor
this.setCss('--angle', `${rad}rad`);
this.setCss('--scale', `${scale}`);
this.setCss('--tleft', `${x - this.cHW}px`);
this.setCss('--ttop', `${y - this.cHH}px`);
// Reset the cursor when mouse stops
this.restoreTimer = setTimeout(this.restore, this.smoothness + 100, x, y);
}
// Returns the position parameters of the cursor
position () {
const {x, y, angle, scale, speed} = this;
return {x, y, angle, scale, speed};
}
// Restores the cursor
restore (x, y) {
this.state = 0;
this.setCss('--scale', 1);
this.scale = 1;
this.speed = [0];
this.x = x;
this.y = y;
}
}
Follower.init();
const crsr = new Follower('.crsr').activate();
body {
margin: 0px;
}
.crsr {
width: 76px;
height: 76px;
border: 2px solid #000;
border-radius: 0%;
text-align: center;
font-size: 20px;
}
.cursor {
position: fixed;
cursor: default;
user-select: none;
left: var(--tleft);
top: var(--ttop);
transform: rotate(var(--angle)) scaleY(var(--scale));
}
.transitioned {
transition: transform var(--smoothness) linear;
}
.hidden {
display: none;
}
<div class="crsr hidden">A</div>
The basic idea of the code is to wait until the mouse has moved enough pixels (threshold) to calculate the angle. The "mad circle" effect is tackled by setting the angle to the same position, but at the negated angle when crossing PI. This change is made invisibly between the renderings.
CSS variables are used for the actual values in transform, this allows to change a single parameter of the transform functions at the time, you don't have to rewrite the entire rule. setCss method is just syntactic sugar, it makes the code a little bit shorter.
The current parameters are showing a rectangle follower as it is in your question. Setting ex. stretchMax = 300 and stretchSlow = 125 and adding 50% border radius to CSS might be near to what you finally need. stretchRate defines the stretch related to the speed of the mouse. If the slow motion is still not smooth enough for your purposes, you can create a better algorithm to // Smoothen small angles section (in crsrMove method). You can play with the parameters at jsFiddle.
Try like this
class Cursor {
constructor() {
this.prevX = null;
this.prevY = null;
this.curX = null;
this.curY = null;
this.angle = null;
this.container = document.querySelector(".cursor");
this.follower = this.container.querySelector(".cursor-follower");
document.addEventListener("mousemove", (event) => {
this.curX = event.clientX;
this.curY = event.clientY;
});
this.position();
}
position(timestamp) {
this.follower.style.top = `${this.curY}px`;
this.follower.style.left = `${this.curX}px`;
if (this.curY !== this.prevY && this.curX !== this.prevX) {
this.angle = Math.atan2(this.curY - this.prevY, this.curX - this.prevX) * 180/Math.PI;
}
console.log(this.angle + 90);
this.follower.style.transform = `rotateZ(${this.angle + 90}deg)`;
this.prevX = this.curX;
this.prevY = this.curY;
requestAnimationFrame(this.position.bind(this));
}
}
const cursor = new Cursor();
I have a case where I want to draw 3 arc lines and erase them.
First Arc CA should be drawn progressively and then it should be erased progressively. Then arc AB should be drawn and erased and then arc BC should do the same. And then repeat.
My approach:
Using canvas and JS:
I started with canvas, but the anti-aliasing does not effect here. So I thought may be SVG will be better.
var currentEndAngle = 0;
var currentStartAngle = 0;
var currentColor = 'black';
var lineRadius = 300;
var lineWidth = 5;
setInterval(draw, 5);
function draw() {
var can = document.getElementById('canvas1'); // GET LE CANVAS
var canvas = document.getElementById("canvas1");
var context = canvas.getContext("2d");
var x = canvas.width / 2;
var y = canvas.height / 2;
var radius;
var width;
var startAngle = currentStartAngle * Math.PI;
var endAngle = (currentEndAngle) * Math.PI;
currentStartAngle = currentEndAngle - 0.01;
currentEndAngle = currentEndAngle + 0.01;
if (Math.floor(currentStartAngle / 2) % 2) {
currentColor = "white";
radius = lineRadius - 1;
width = lineWidth + 3;
} else {
currentColor = "black";
radius = lineRadius;
width = lineWidth;
}
var counterClockwise = false;
context.beginPath();
context.arc(x, y, radius, startAngle, endAngle, counterClockwise);
context.lineWidth = width;
// line color
context.strokeStyle = currentColor;
context.stroke();
/************************************************/
}
body {
text-align: center;
background: blue;
}
#canvas1 {
width: 500px;
height: 500px;
margin: 0 auto;
}
<canvas id="canvas1" width="700" height="700"></canvas>
Using SVG and CSS
The SVG approach looks smoother. But I don't understand how I can modify the dasharray, dashoffset and radius of circle to get 3 arcs animating.
circle {
fill: transparent;
stroke: black;
stroke-width: 2;
stroke-dasharray: 250;
stroke-dashoffset: 0;
animation: rotate 5s linear infinite;
}
#keyframes rotate {
0% {
stroke-dashoffset: 500;
}
100% {
stroke-dashoffset: 0;
}
}
<svg height="400" width="400">
<circle cx="100" cy="100" r="40" />
</svg>
So if anyone can help me extend the code or give guidance on how I can create three arcs from the svg circle and how the dasharray, dashoffset and radius should be set?
In case you have a better solution then the above 2 approaches then please let me know.
I have also tried to use the drawsvg plugin from GSAP and I guess that might be easier but I am not allowed to use the 'drawsvg' plugin for my project.
For the canvas version, as stated in comments, your antialiasing problem is that you are redrawing over and over on the same pixels.
To avoid this, clear your whole canvas every frame and redraw everything.
For your requested animation, you would have to store both your start angle and your end angle. Then you'll increment one after the other, while swithing when you've passed the division size threshold.
Here is an annotated snippet that will make things more clear I hope.
// settings
var divisions = 3;
var duration = 3000; // in ms
var canvas = document.getElementById("canvas1");
var context = canvas.getContext("2d");
var x = canvas.width / 2;
var y = canvas.height / 2;
var radius = (canvas.width / 7) * 2;
context.lineWidth = 4;
// init
var currentSplit = 0;
var splitAngle = (Math.PI * 2) / divisions;
var splitTime = (duration / (divisions*2)); // how much time per split per end
var angles = [0,0]; // here we store both start and end angle
var current = 0;
var startTime = performance.now();
draw();
function draw(currentTime) {
// first convert the elapsed time to an angle
var timedAngle = ((currentTime - startTime) / splitTime) * splitAngle;
// set the current end to this timed angle + the current position on the circle
angles[current] = timedAngle + (splitAngle * currentSplit);
if (timedAngle >= splitAngle) { // one split is done for this end
// it should not go farther than the threshold
angles[current] = (splitAngle * (currentSplit + 1));
current = +(!current) // switch which end should move
startTime = currentTime; // reset the timer
if(!current){ // we go back to the start
currentSplit = (currentSplit + 1) % divisions; // increment our split index
}
}
if(angles[1] > Math.PI*2){ // we finished one complete revolution
angles[0] = angles[1] = current = 0; // reset everything
}
// at every frame we clear everything
context.clearRect(0, 0, canvas.width, canvas.height);
// and redraw
context.beginPath();
context.arc(x, y, radius, angles[0], angles[1], true);
context.stroke();
requestAnimationFrame(draw); // loop at screen refresh rate
}
body {
text-align: center;
}
#canvas1 {
width: 250px;
height: 150px;
}
<canvas id="canvas1" width="500" height="300"></canvas>
You don't really want to modify stroke-dashoffset, because that just shifts the dash patter around the circle.
You have to modify the dash array values anyway, so you might as well just do it all by animating the values in the dash array.
Your circle has radius 40, so the circumference is 251.33. Meaning that each of your three arc has a length of 83.78.
For each of the three stages, we grow the "on" part of the dash from 0 to 83.78. Then we shrink it back down again, while simultaneously growing the previous gap from 83.78 to 167.55. That is so that the tail gets pushed around to the end.
That works for the first two steps, but since the dash pattern starts and ends at the 3 o'clock position (and doesn't wrap through that point), we have to do the tail push for the last stage by using an extra empty dash pair at the start. We grow the gap on that one from 0 to 83.78 instead.
circle {
fill: transparent;
stroke: black;
stroke-width: 2;
animation: rotate 5s linear infinite;
}
#keyframes rotate {
0% { stroke-dasharray: 0 0 0 83.78 0 83.78 0 83.78; }
16.7% { stroke-dasharray: 0 0 0 83.78 83.78 0 0 83.78; }
33.3% { stroke-dasharray: 0 0 0 167.55 0 0 0 83.78; }
50% { stroke-dasharray: 0 0 0 83.78 0 83.78 83.78 0; }
66.6% { stroke-dasharray: 0 0 0 83.78 0 167.55 0 0; }
83.3% { stroke-dasharray: 0 0 83.78 0 0 83.78 0 83.78; }
100% { stroke-dasharray: 0 83.78 0 0 0 83.78 0 83.78; }
}
<svg height="400" width="400">
<circle cx="100" cy="100" r="40" />
</svg>
Javascript extends HTML
Canvas, (or CSS, HTML, SVG) combined with javascript always wins out over CSS, SVG, HTML alone because Javascript is far more adaptable. HTML, CSS and SVG are declarative languages, while JavaScript is a fully functional imperative language that can do anything any other programing language can do.
You use javascript to add to the HTML, CSS, SVG functionality, effectively declaring new behaviour for these languages.
Once you have defined the Javascript functionality you can forget about the javascript and use the HTML, CSS, or SVG calling upon the new behaviours as needed.
In this case all elements with the class name "segmentedProgress" will become an animated progress. You can set up as many properties as you like to control the behaviour and add them to the element's data attribute.
eg
<div class="segmentedProgress"></div>
<!-- showing defaults as above element will be setup -->
<div class="segmentedProgress"
data-angle-steps = 3 <!-- number of segments. (integers only) -->
data-speed = 1000 <!-- Time per segment in ms -->
data-easing = "1.2" <!-- easing power -->
data-line-width = "0.1" <!-- as fraction of radius -->
data-radial-size = "0.33" <!-- as fraction of shortest dimension -->
data-color = "black" <!-- colour of line -->
></div>
As long as the Javascript has been included the progress will automatically appear on the page for each element that is correctly configured. If you have your server setup to recognise page content dependencies then the above is all you need to do to add the behaviour to the page as the server will add what is needed to make it run.
The javascript
It does not take much javascript to implement. You find all the elements that have the appropriate class name and add them to an array of progress items. Then animate them as needed.
document.addEventListener("load", function(){
var elements = [...document.body.querySelectorAll(".segmentedProgress")];
if(elements.length === 0){ // exit if nothing found
return;
}
// singleton to isolate from onload
(function(){
const error = 0.01; // Math too perfect causes zero len arc to draw nothing. Error makes sure there is always some length in the drawn arc
const items = []; // array of progress items
// each progress item defaults
var defaults = {
angleSteps : 3, // number of segments. (integers only)
speed : 1000, // Time per segment in ms
easing : 1.2, // easing power where 1 = no easing 2 = normal quadratic easing 1/2= inverse quadratic easing
lineWidth : 0.1, // as fraction of radius
radialSize : 0.33,// as fraction of shortest dimension
color : "black", // colour of line
complete : false, // not used
resize () { // resize the canvas and set size dependent vars
this.bounds = this.element.getBoundingClientRect();
this.w = this.canvas.width = this.bounds.width;
this.h = this.canvas.height = this.bounds.height;
this.canvas.style.top = (this.bounds.top + scrollY) + "px";
this.canvas.style.left = (this.bounds.left + scrollX) + "px";
this.pos = { x : this.w / 2, y : this.h / 2}; // position of circle
this.radius = Math.min(this.w, this.h) * this.radialSize; // radius of circle
// set canvas state constants
this.ctx.lineCap = "round";
},
update (time) { // updates and renders
var segStart, segProgress, pp, ctx, ang;
ctx = this.ctx; // alias to this.ctx
// clear the canvas
ctx.clearRect(0, 0, this.w, this.h);
// get current selment angle
ang = Math.PI * 2 / this.angleSteps, // Radians per segment
// set the time at the correct speed
time /= this.speed;
// get the segment start position in radians
segStart = Math.floor(time % this.angleSteps) * ang;
// get the unit progress of this stage doubled for grow and shrink stages
var segProgress = (time % 1) * 2;
var pp = segProgress % 1; // pp partial progress
pp = (pp ** this.easing) / ((pp ** this.easing) + (1 - pp) ** this.easing); // add some easing
ctx.beginPath();
// first half of progress is growth
if(segProgress <= 1){
ctx.arc(this.pos.x, this.pos.y, this.radius, segStart, segStart + pp * ang + error);
}else{
// second half of progress is shrink
ctx.arc(this.pos.x, this.pos.y, this.radius, segStart + pp * ang - error, segStart + ang);
}
ctx.strokeStyle = this.color;
ctx.lineWidth = this.radius * this.lineWidth;
ctx.stroke();
}
}
// create prgress item for each found element
elements.forEach(element => {
var pItem = {...defaults}; // progress item
pItem.element = element;
// get any element setting that overwrite the defaults
Object.keys(defaults).forEach(key => {
if(typeof defaults[key] !== "function"){
if(element.dataset[key] !== undefined){
pItem[key] = element.dataset[key];
if(! isNaN(element.dataset[key])){
pItem[key] = Number(pItem[key]);
}
}
}
});
pItem.canvas = document.createElement("canvas");
pItem.ctx = pItem.canvas.getContext("2d");
pItem.canvas.style.position = "absolute";
pItem.resize();
items.push(pItem);
element.appendChild(pItem.canvas);
});
elements.length = 0; // let go of elements
// change size on resize
window.addEventListener("resize", () =>{
items.forEach(pItem => pItem.resize());
});
// start the animation
requestAnimationFrame(update);
// main update loop
function update (time) {
items.forEach(pItem => {
pItem.update(time);
});
requestAnimationFrame(update);
}
}());
}());
As a demo
//document.addEventListener("load",()=>{
;(function(){
var elements = [...document.body.querySelectorAll(".segmentedProgress")];
if (elements.length === 0) { return }
(function () {
const error = 0.001; // Math too perfect causes zero len arc to draw nothing. Error makes sure there is always some length in the drawn arc
const items = []; // array of progress items
var defaults = {
angleSteps : 3, // number of segments. (integers only)
speed : 1000, // Time per segment in ms
easing : 1.2, // easing power where 1 = no easing 2 = normal quadratic easing 1/2= inverse quadratic easing
lineWidth : 0.1, // as fraction of radius
radialSize : 0.33,// as fraction of shortest dimension
color : "black", // colour of line
complete : false, // not used
resize () { // resize the canvas and set size dependent vars
this.bounds = this.element.getBoundingClientRect();
this.w = this.canvas.width = this.bounds.width;
this.h = this.canvas.height = this.bounds.height;
this.canvas.style.top = (this.bounds.top + scrollY) + "px";
this.canvas.style.left = (this.bounds.left + scrollX) + "px";
this.pos = { x : this.w / 2, y : this.h / 2}; // position of circle
this.radius = Math.min(this.w, this.h) * this.radialSize; // radius of circle
this.ctx.lineCap = "round";
},
update (time) { // updates and renders
var segStart, segProgress, pp, ctx, ang;
ctx = this.ctx; // alias to this.ctx
ctx.clearRect(0, 0, this.w, this.h);
ang = Math.PI * 2 / this.angleSteps, // Radians per segment
time /= this.speed;
segStart = Math.floor(time % this.angleSteps) * ang;
var segProgress = (time % 1) * 2;
var pp = segProgress % 1; // pp partial progress
// babel can not handle the following line even though most
// browsers can
// pp = (pp ** this.easing) / ((pp ** this.easing) + (1 - pp) ** this.easing); // add some easing
// to cover babel error
pp = Math.pow(pp,this.easing) / (Math.pow(pp,this.easing) + Math.pow(1 - pp, this.easing)); // add some easing
ctx.beginPath();
if(segProgress <= 1){
ctx.arc(this.pos.x, this.pos.y, this.radius, segStart, segStart + pp * ang + error);
}else{
ctx.arc(this.pos.x, this.pos.y, this.radius, segStart + pp * ang - error, segStart + ang);
}
ctx.strokeStyle = this.color;
ctx.lineWidth = this.radius * this.lineWidth;
ctx.stroke();
}
}
elements.forEach(element => {
var pItem = {...defaults}; // progress item
pItem.element = element;
Object.keys(defaults).forEach(key => {
if(typeof defaults[key] !== "function"){
if(element.dataset[key] !== undefined){
pItem[key] = element.dataset[key];
if(! isNaN(element.dataset[key])){
pItem[key] = Number(pItem[key]);
}
}
}
});
pItem.canvas = document.createElement("canvas");
pItem.ctx = pItem.canvas.getContext("2d");
pItem.canvas.style.position = "absolute";
pItem.resize();
items.push(pItem);
element.appendChild(pItem.canvas);
});
elements.length = 0;
window.addEventListener("resize", () =>{ items.forEach(pItem => pItem.resize()) });
requestAnimationFrame(update);
function update (time) {
items.forEach(pItem => { pItem.update(time) });
requestAnimationFrame(update);
}
}());
}());
.segmentedProgress {
width : 100px;
height : 100px;
}
.big {
width : 200px;
height : 200px;
}
.large {
width : 512px;
height : 512px;
background : #4AF;
}
4 segment fast.
<div class="segmentedProgress" data-color="red" data-speed ="250" data-line-width="0.3" data-angle-steps=4 ></div>
Default Progress
<div class="segmentedProgress" ></div>
Big progress
<div class="big segmentedProgress" data-color="blue" data-speed ="2500" data-line-width="0.3" data-angle-steps=2 ></div>
60 Seconds two overlap
<div class="large segmentedProgress" data-color="white" data-speed ="1000" data-line-width="0.02" data-angle-steps=60 >
<div class="large segmentedProgress" data-color="white" data-speed ="1000" data-line-width="0.02" data-angle-steps=2 data-radial-size = "0.34">
</div>
I've just gotten started with coding on my new school, and I'm making a website as my first project. Now I'm trying to make this "matrix" animation appear as my background (made mostly in javascript), but I just cant get it to work. Could anyone help me with this issue? If it's even possible, that is. Thanks. (I know the code is kind of messy... first time posting, no idea how to do this)
HTML
<div class="inhoud">
<div id="container">
<div id="over_stuff">
Here's some stuff over #pixie!
</div>
<canvas id="c"></canvas>
</div>
</div>
CSS
/*basic reset*/
#container {
overflow: hidden;
position: relative;
z-index: 1;
}
#c {
z-index: 0;
background: #010222;
background:
}
#over_stuff {
color: white;
position: absolute;
top: 0px;
left: 0px;
z-index: 5;
padding: 10px;
}
* {
margin: 0;
padding: 0;
}
/*adding a black bg to the body to make things clearer*/
body {
background: black;
}
canvas {
display: block;
}
Javascript
var c = document.getElementById("c");
var ctx = c.getContext("2d");
//making the canvas full screen
c.height = window.innerHeight;
c.width = window.innerWidth;
//chinese characters - taken from the unicode charset
var chinese = "dirk";
//converting the string into an array of single characters
chinese = chinese.split("");
var font_size = 10;
var columns = c.width / font_size; //number of columns for the rain
//an array of drops - one per column
var drops = [];
//x below is the x coordinate
//1 = y co-ordinate of the drop(same for every drop initially)
for (var x = 0; x < columns; x++)
drops[x] = 1;
//drawing the characters
function draw() {
//Black BG for the canvas
//translucent BG to show trail
ctx.fillStyle = "rgba(0, 0, 0, 0.05)";
ctx.fillRect(0, 0, c.width, c.height);
ctx.fillStyle = "#555"; //green text
ctx.font = font_size + "px arial";
//looping over drops
for (var i = 0; i < drops.length; i++) {
//a random chinese character to print
var text = chinese[Math.floor(Math.random() * chinese.length)];
//x = i*font_size, y = value of drops[i]*font_size
ctx.fillText(text, i * font_size, drops[i] * font_size);
//sending the drop back to the top randomly after it has crossed the screen
//adding a randomness to the reset to make the drops scattered on the Y axis
if (drops[i] * font_size > c.height && Math.random() > 0.975)
drops[i] = 0;
//incrementing Y coordinate
drops[i]++;
}
}
setInterval(draw, 33);
I FIXED IT! all it took was to add
Postion:fixed to the code, and then give that a z-index of -10. thanks for taking the time to look at my code anyway ;)
Let's say I have svg, drawn like this:
var myArc = d3.svg.arc()
.innerRadius(width / 5)
.outerRadius(width / 2);
myArc.startAngle(function(d, i) {
return radians * i
});
myArc.endAngle(function(d, i) {
return radians * (i + 1)
...and is later filled with some color, etc. It looks like this:
Now, I want on svg click to get angle degree. Example: I want to get between ~ 310° and 50° when I click on red, 70° or so when I click on yellow etc.
How would I do that? Thanks in advance.
I do some research, didn't find any simpler solution.
I am using the way you mentioned: get center of circle and clicked position, then calculate angle based on those two. I don't think this is too complex to use.
Note: because the circle is not really circle, so there are some numerical errors.
Hope this is useful.
(function($){
var center = {};
var calculateAngle = function(x, y){
var k = Math.abs(y) / Math.abs(x);
var angle = Math.atan(k) * 180 / Math.PI;
if(y * x > 0){
angle = 90 - angle + (y < 0 ? 180 : 0);
} else {
angle = angle + (y < 0 ? 90 : 270);
}
return angle;
}
$(document).ready(function(){
var $container = $('#container');
var $selector = $('#selector');
var $result = $('#result');
center = {
x: $container.width() / 2,
y: $container.height() / 2
}
$selector.css({left:center.x, top:center.y});
$container.on('click', function(e){
var x = e.offsetX;
var y = e.offsetY;
$selector.css({left:x, top:y});
var angle = calculateAngle(x - center.x, center.y - y);
$result.text(angle);
});
});
})(jQuery)
#container{
display: inline-block;
border: 1px solid red;
width: auto;
position: relative;
}
#selector{
position: absolute;
border:1px solid black;
width: 5px;
height: 5px;
border-radius: 50%;
transform: translate(-50%, -50%);
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<div id="container">
<span id="selector"></span>
<img src="http://i.stack.imgur.com/4X8A7.png">
</div>
<div>
<span>Result: </span>
<span id="result"></span>
</div>