cubing / AnimCubeJS

▶️ Play around with a Rubik's Cube simulator.
https://animcubejs.cubing.net/animcubejs.html
MIT License
29 stars 10 forks source link

Rotate cube using keys #30

Closed raquelhortab closed 3 weeks ago

raquelhortab commented 1 year ago

Hi, I am wondering if someone could point me out how to allow rotation of the cube axes. Something similar to move=x, move=y or move=z but that I can control any time. The idea would be to bind that to some keys to allow a more confortable rotation of the axes since the mouse does not always feel natural. I've looked around the code a bit but since many vars have cryptic names it is difficult to figure out. I'm looking to implement a function that maybe calls whatever function animates the cube but with a specific move (x,y,z) instead of the move parameter.

raquelhortab commented 1 year ago

I'm gonna leave here whatever progress I make.

This seems to work


document.addEventListener("keydown", (event)=>{
    let allowed = ["x", "y", "z"];
    if(allowed.includes(event.key)){
        moveAnimated = true;
        let m = event.key;
        nowServing = 0;
        if(event.ctrlKey){
            m += "' ";
        }
        t = move = getMove(m);
        run(0, !0);
    }
});

it will for sure mess up with pretty much everything though, I am not showing any control bar so I don't actually have steps but I still need to see if it works with a scrambled cube.

raquelhortab commented 1 year ago

seems to work with scrambled cube as well (without button bar or move text).

This is a preview, I made it work with A-S-D keys and when I press shift+key it reverses the move. The letters on screen are just whatever I am typing, just to show how it works.

screencast-bpconcjcammlapcogcnnelfmaeghhagj-2023.07.06-10_33_01.webm

This is the code:

    document.addEventListener("keydown", (event)=>{
        let allowed = {"a":"x", "s":"y", "d":"z"};
        let key = event.key.toLowerCase();
        if(Object.keys(allowed).includes(key)){
            moveAnimated = true;
            nowServing = 0;
            let m = allowed[key];
            if(event.shiftKey){
                m += "' ";
            }
            t = move = getMove(m);
            run(0, !0);
        }
    });
raquelhortab commented 1 year ago

Additionally, I also made a piece of code that allows rotating the view with the arrow keys:

var pressedKeys = {};
    document.addEventListener("keyup", (event)=> {
        pressedKeys[event.key] = false;
    });
    document.addEventListener("keydown", (event)=>{
        pressedKeys[event.key] = true;

// .... other code

        let arrows = ['ArrowDown', "ArrowUp", "ArrowLeft", "ArrowRight"];
        if(arrows.includes(event.key)){
            let n = pressedKeys['ArrowDown'] ? 2 : pressedKeys['ArrowUp'] ? -2 : 0;
            let i = pressedKeys['ArrowRight'] ? 2 : pressedKeys['ArrowLeft'] ? -2 : 0;
            let o = 0, a = 0;
            vNorm(vAdd(eye, vScale(vCopy(eyeD, eyeX), -.016 * i)));
            vNorm(vMul(eyeX, eyeY, eye));
            vNorm(vAdd(eye, vScale(vCopy(eyeD, eyeY), .016 * n)));
            vNorm(vMul(eyeY, eye, eyeX));
            lastX = o;
            lastY = a;
            currentAngle = .01 * (dragX * i + dragY * n) / Math.sqrt(dragX * dragX + dragY * dragY);
            paint();
        }
    });

screencast-bpconcjcammlapcogcnnelfmaeghhagj-2023.07.06-12_35_04.webm

raquelhortab commented 1 year ago

I've switched to using the unminified code, which is a bit more readable. However, I really need help understanding the run function. I'm trying to do a 90deg turn of the cube on whatever axes is in front at the moment.

mfeather1 commented 1 year ago

It may help to look at the java code prior to the conversion: http://software.rubikscube.info/AnimCube/AnimCube.java I had to combine the run and spin functions into one to get it to work with javascripts's requestAnimationFrame (the vars outerLoopTop, innerLoopTop, innerLoopBot, outerLoopBot are used to emulate a loop within a loop for the spin function).

mfeather1 commented 1 year ago

FWIW, this shows which face is currently facing front. Seems to work ok although there is a minor issue. It doesn't always return a value for the face which can be seen by adding an "else c = 'Null';" at the end of the if statement (it's commented out in code below) and rotating the cube with the mouse. I used a global var for c so it would retain the previous value when else is not used which seems to be a better solution. The 2/3 value in the f function is somewhat arbitrary, I set it to try to minimize the occurrence of Null while rotating (when using the else). Perhaps a better solution by someone more knowledgeable will come along.

<!doctype html>
<html>
<head>
<script src=AnimCube3.js></script>
</head>
<body>
<center>
<div id=c1 style="width:200px; height:219px"></div>
<br>
<div id=status></div>
</center>
<script>
document.addEventListener('mousemove', mousemove);
var c;  
var acjs_eye = [];
AnimCube3('id=c1');
mousemove();
function mousemove() {
  var e = acjs_eye['c1'];
  if      (f(e[0]) == -1) c = 'Left';
  else if (f(e[0]) ==  1) c = 'Right';
  else if (f(e[1]) == -1) c = 'Up';
  else if (f(e[1]) ==  1) c = 'Down';
  else if (f(e[2]) == -1) c = 'Front';
  else if (f(e[2]) ==  1) c = 'Back';
  // else c = 'Null';
  document.getElementById('status').innerHTML = c + ' Face';
}
function f(n) {
  var v = 2/3;
  return ((n >= v) ? 1 : ((n <= -v) ? -1 : 0));
}
</script>
</body>
</html>
raquelhortab commented 1 year ago

it did help, thanks! I was near since I already had a function that centers an eye but I needed the logic behind which face was which. I also did a function that centers the cube on the most visible face:

document.addEventListener("keydown", (event)=>{
//...
if (event.key == "c") {
      centerEye(eyeX), centerEye(eyeY), centerEye(eyeD), centerEye(eye);
      paint();
    }
//...
}
function centerEye(arr){
    let abs_arr = arr.map((el)=>Math.abs(el));
    let index = abs_arr.indexOf(Math.max(...abs_arr));
    let sign = arr[index] > 0 ? 1 : -1;
    arr.forEach((el, i)=>{
      arr[i] = i == index ? sign * 1 : 0;
    });
  }

although it gets buggy if the cube is showing three faces similarly, maybe if you could explain the logic behind eyeX ,eyeY and eyeD I'd be able to fix it

raquelhortab commented 1 year ago

for now, I've done this so the user does not lose the cube in case of a centering bug

if(bugged(eyeX) || bugged(eyeY) || bugged(eye)){
        eyeX = [0.8660254037844387, 0, 0.49999999999999994];
        eyeY = [-0.24999999999999994, -0.8660254037844387, 0.4330127018922193];
        eyeD = [];
        eye = [0.4330127018922193, -0.49999999999999994, -0.7500000000000001];
      }

function bugged(arr){
    return arr.every(e => e === 0) || arr.find(e =>  isNaN(e))
  }
mfeather1 commented 1 year ago

I'm getting better results using your centerEye function, I'm not seeing the Null when rotating the cube anymore. I added a button to center the cube and I'm getting better results by using vNorm to set the values for eyeY although I can still occasionally get an error which these two examples show:

Example 1:

console.log(acjs_eyeX[id], acjs_eyeY[id], acjs_eye[id]);

Array(3) [ 0.7015167273604913, 0.13888624159923105, -0.6989884785372833 ]

Array(3) [ 0.31802027377995495, 0.8167407389466405, 0.4814537057805437 ]

Array(3) [ 0.6377596621756104, -0.5600403353682089, 0.52878864971018 ]

Array(3) [ 1, 0, 0 ]

Array(3) [ NaN, NaN, NaN ]

Array(3) [ 1, 0, 0 ]


Example 2:

Array(3) [ 0.7099383267427315, 0.6785061014991024, 0.1887247796401431 ]

Array(3) [ 0.32609255599643416, -0.554215173760169, 0.7658388773741498 ]

Array(3) [ 0.624220487604695, -0.48215662538481774, -0.6147143820117448 ]

Array(3) [ 1, 0, 0 ]

Array(3) [ NaN, NaN, NaN ]

Array(3) [ 1, 0, 0 ]

So can't have 1 in same position in both eye & eyeX apparently.

The vector.js included in the code below is a copy/paste of the vector functions from AnimCube3.js.

<!doctype html>
<html>
<head>
<meta name=viewport content="width=device-width, initial-scale=1">
<script src=AnimCube3.js></script>
<script src=vector.js></script>
</head>
<body>
<center>
<div id=c1 style="width:200px; height:219px"></div>
<br>
<button onclick=center()>Center</button>
<br><br>
<div id=status></div>
</center>
<script>
document.addEventListener('mousemove', mousemove);
document.addEventListener('touchmove', mousemove, {passive: false});
var id = 'c1';
var acjs_eye = [];
var acjs_eyeX = [];
var acjs_eyeY = [];
var acjs_paint = [];
AnimCube3('id=c1');
mousemove();
function center() {
  console.log(acjs_eyeX[id], acjs_eyeY[id], acjs_eye[id]);
  centerEye(acjs_eye[id]);
  centerEye(acjs_eyeX[id]);
  vNorm(vMul(acjs_eyeY[id], acjs_eye[id], acjs_eyeX[id]));
  console.log(acjs_eyeX[id], acjs_eyeY[id], acjs_eye[id]);
  acjs_paint[id]();
}
function centerEye(arr){
  let abs_arr = arr.map((el)=>Math.abs(el));
  let index = abs_arr.indexOf(Math.max(...abs_arr));
  let sign = arr[index] > 0 ? 1 : -1;
  arr.forEach((el, i)=>{
    arr[i] = i == index ? sign * 1 : 0;
  });
}
function mousemove() {
  var f;
  var e = [];
  vCopy(e, acjs_eye[id]);
  centerEye(e);
  if      (e[0] == -1) f = 'Left';
  else if (e[0] ==  1) f = 'Right';
  else if (e[1] == -1) f = 'Up';
  else if (e[1] ==  1) f = 'Down';
  else if (e[2] == -1) f = 'Front';
  else if (e[2] ==  1) f = 'Back';
  else f = 'Null';
  document.getElementById('status').innerHTML = f + ' Face';
}
</script>
</body>
</html>
raquelhortab commented 1 year ago

With my function, I get bugs when I try to center when 3 faces are shown almost equally. This is an example of centering gone wrong:

eyeX   [1, 0, 0]
eyeY  [0, -1, 0]
eyeD  [-1, 0, 0]
eye   [0, -1, 0]

and a screencast of what is displayed when it happens:

screencast-0.0.0.0_3000-2023.07.11-09_54_54.webm

Do you know the logic behind each array? I understand eye can tell us which face is visible but what about eyeX, eyeY and eyeD?

Anyway, I can leave it like thin and it won't be too bad as it should not happen very often that someone centers when 3 faces are shown.

Thanks for the help and feedback :)

mfeather1 commented 1 year ago

I'll defer to a 3-D expert for a proper explanation but as I understand it the three "eyes" are the coordinates on the unit sphere for each of the the three cube axes, eyeD is just a temp vector used for calculations in the mousemove function so don't need it for this purpose.

I made the following table which shows all the possibilities for centered faces (in the table, eye is labeled eyeZ). It shows results of rotating each face to the front and then rotating the cube into the other three positions that keep that face in front (y1, y2, y3). The eyeZ in the first row for each table matches faceNormals in AnimCube3.js.

So we need a center function that matches one of these 24 configs (I'm still working on how to do that...)

F - Front Face (orange):
        eyeX        eyeY         eyeZ
   ------------ ------------ ------------
   [ 1,  0,  0] [ 0, -1,  0] [ 0,  0, -1]
y1 [ 0, -1,  0] [-1,  0,  0] [ 0,  0, -1]
y2 [-1,  0,  0] [ 0,  1,  0] [ 0,  0, -1]
y3 [ 0,  1,  0] [ 1,  0,  0] [ 0,  0, -1]

B - Back Face (red z2):
   [-1,  0,  0] [ 0, -1,  0] [ 0,  0,  1]
y1 [ 0, -1,  0] [ 1,  0,  0] [ 0,  0,  1]
y2 [ 1,  0,  0] [ 0,  1,  0] [ 0,  0,  1]
y3 [ 0,  1,  0] [-1,  0,  0] [ 0,  0,  1]

U - Up Face (white x'):
   [ 1,  0,  0] [ 0,  0,  1] [ 0, -1,  0]
y1 [ 0,  0,  1] [-1,  0,  0] [ 0, -1,  0]
y2 [-1,  0,  0] [ 0,  0, -1] [ 0, -1,  0]
y3 [ 0,  0, -1] [ 1,  0,  0] [ 0, -1,  0] 

D - Down Face (yellow x)
   [ 1,  0,  0] [ 0,  0, -1] [ 0,  1,  0]
y1 [ 0,  0, -1] [-1,  0,  0] [ 0,  1,  0]
y2 [-1,  0,  0] [ 0,  0,  1] [ 0,  1,  0]
y3 [ 0,  0,  1] [ 1,  0,  0] [ 0,  1,  0]

L - Left Face (green z')
   [ 0,  0, -1] [ 0, -1,  0] [-1,  0,  0]
y1 [ 0, -1,  0] [ 0,  0,  1] [-1,  0,  0]
y2 [ 0,  0,  1] [ 0,  1,  0] [-1,  0,  0]
y3 [ 0,  1,  0] [ 0,  0, -1] [-1,  0,  0] 

R - Right Face (blue z)
   [ 0,  0,  1] [ 0, -1,  0] [ 1,  0,  0]
y1 [ 0, -1,  0] [ 0,  0, -1] [ 1,  0,  0]
y2 [ 0,  0, -1] [ 0,  1,  0] [ 1,  0,  0]
y3 [ 0,  1,  0] [ 0,  0,  1] [ 1,  0,  0]
mfeather1 commented 1 year ago

This seems to be working (replace the center function in my previous code with the following), it vNorms eyeX if the vNorm on eyeY fails. There's also four test cases (p1-4) because I was having a hard time getting the problem to occur otherwise. You can probably find a better solution...

function center() {
  // p1();  // uncomment for test cases
  console.log(acjs_eyeX[id], acjs_eyeY[id], acjs_eye[id]);
  var tmpX = [], tmpY = [];
  vCopy(tmpX, acjs_eyeX[id]);
  vCopy(tmpY, acjs_eyeY[id]);
  centerEye(acjs_eye[id]);
  centerEye(acjs_eyeX[id]);
  vNorm(vMul(tmpY, acjs_eye[id], acjs_eyeX[id]));
  if (isNaN(tmpY[0])) {
    console.log('tmpY[0] is NaN, using vNorm on tmpX instead');
    centerEye(acjs_eyeY[id]);
    vNorm(vMul(tmpX, acjs_eyeY[id], acjs_eye[id]));
    vCopy(acjs_eyeX[id], tmpX);
  }
  else
    vCopy(acjs_eyeY[id], tmpY);
  console.log(acjs_eyeX[id], acjs_eyeY[id], acjs_eye[id]);
  acjs_paint[id]();
}
function p1() {
  vCopy(acjs_eyeX[id], [0.7015167273604913, 0.13888624159923105, -0.6989884785372833]);
  vCopy(acjs_eyeY[id], [0.31802027377995495, 0.8167407389466405, 0.4814537057805437]);
  vCopy(acjs_eye[id],  [0.6377596621756104, -0.5600403353682089, 0.52878864971018]);
}
function p2() {
  vCopy(acjs_eyeX[id], [0.7099383267427315, 0.6785061014991024, 0.1887247796401431]);
  vCopy(acjs_eyeY[id], [0.32609255599643416, -0.554215173760169, 0.7658388773741498]);
  vCopy(acjs_eye[id],  [0.624220487604695, -0.48215662538481774, -0.6147143820117448]);
}
function p3() {
  vCopy(acjs_eyeX[id], [0.7175114191210823, -0.03965716226474436, -0.6954169058284092]);
  vCopy(acjs_eyeY[id], [0.3749053288028392, -0.8194409302354926, 0.433546486884654]);
  vCopy(acjs_eye[id], [-0.5870462995932142, -0.5717900587942206, -0.5730905432809165]);
}
function p4() {
  vCopy(acjs_eyeX[id], [0.7067308967888647, 0.020516221824178976, -0.707184929255473]);
  vCopy(acjs_eyeY[id], [-0.4060922733643323, 0.8302772838564859, -0.38174428276751565]);
  vCopy(acjs_eye[id], [0.5793276318611047, 0.5569728149146577, 0.5951140885647447]);
}
mfeather1 commented 1 year ago

A bit cleaner center function and the centerEye function has a "return index;" added at the end.

function center() {
  var eyeX = acjs_eyeX[id];
  var eyeY = acjs_eyeY[id];
  var eyeZ = acjs_eye[id];
  if (centerEye(eyeX) != centerEye(eyeZ))
    vNorm(vMul(eyeY, eyeZ, eyeX));
  else {
    centerEye(eyeY);
    vNorm(vMul(eyeX, eyeY, eyeZ));
  }
  acjs_paint[id]();
}

function centerEye(arr){
  let abs_arr = arr.map((el)=>Math.abs(el));
  let index = abs_arr.indexOf(Math.max(...abs_arr));
  let sign = arr[index] > 0 ? 1 : -1;
  arr.forEach((el, i)=>{
    arr[i] = i == index ? sign * 1 : 0;
  });
  return index;
}
mfeather1 commented 1 year ago

This version does not use any of the vector functions. It does require the mod to centerEye mentioned above.

function center() {
  var idx = [[0,2,1], [2,0,0], [1,0,0]];
  var eyeX = acjs_eyeX[id];
  var eyeY = acjs_eyeY[id];
  var eyeZ = acjs_eye[id];
  var tmpX = [...eyeX];
  var xi = centerEye(eyeX);
  var zi = centerEye(eyeZ);
  if (xi != zi) {
    eyeY[xi] = 0;
    eyeY[zi] = 0;
    var i = idx[xi][zi];
    eyeY[i] = Math.sign(eyeY[i]); 
  }
  else {
    var yi = centerEye(eyeY);
    eyeX[yi] = 0;
    eyeX[zi] = 0;
    var i = idx[yi][zi];
    eyeX[i] = Math.sign(tmpX[i]); 
  }
  acjs_paint[id]();
}
raquelhortab commented 1 year ago

wow thanks for the effort! it was really not necessary for you to go through all this trouble! I'll try find the time to try this, although I got a bit occupied with a new task in hand. Thanks a lot! I'll write again as soon as I can test this :)

mfeather1 commented 1 year ago

No trouble at all, it's what I do. :)

This may be stating the obvious but to clarify what the problem was with your: eyeX [1, 0, 0] eyeY [0, -1, 0] eye [0, -1, 0] Can't have non-zero in same column for two axes.

My first solution checks for that condition between eye & eyeX and if it exists then use eye & eyeY instead, then compute the other axis from the first two using vNorm(vMul(...)). My second solution does the same but eliminates the need for the vector functions by just assigning the sign of the original value to the appropriate axis/coordinate (and zeroes to the other two coordinates in that axis). As always, I defer to a 3D expert for the best solution but in my testing these solutions have worked properly.

tarasovladislav commented 10 months ago

Hei, is there any way to trigger buttons with console? I want to make customized buttons

bcube2 commented 8 months ago

Hei, is there any way to trigger buttons with console? I want to make customized buttons

Hi, this is addressed here: https://github.com/cubing/AnimCubeJS/issues/33

bcube2 commented 8 months ago

Hi @raquelhortab,

I have tested this for you in FireFox, Chrome, Edge, Opera, and it works :-)

Also, thank you for your other ideas (transparency & applying moves to any cube position) which have been added to the Enhancement section.

Happy animating

tarasovladislav commented 8 months ago

@bcube2 @raquelhortab

Great job! Was looking for transparency but did not have much time to implement by myself, thanks!

Also, is there a way with JS to change the POS? I want do the same as x / y moves do but so it does not change the position of cube. (so when I recolor cube with other JS it applies to correct side)

bcube2 commented 8 months ago

Also, is there a way with JS to change the POS? I want do the same as x / y moves do but so it does not change the position of cube. (so when I recolor cube with other JS it applies to correct side)

Since this does not seem to be related to the original request in this thread, please file a new issue for your request.

While doing so, could you show us an example cube state before and after the change? Alternatively, you could share the before/after simulators with us using this tool - set parameters, then apply them to generate a URL link.

Thanks.