Warm tip: This article is reproduced from stackoverflow.com, please click
algorithm css html javascript matrix

Javascript/Css matrix3d onscroll function shrinks element when rotating

发布于 2020-04-05 23:42:55

I am trying to make a function that uses Css matrix3d to transform elements on scroll. I am using the rematrix library found here ReMatrix to calculate the matrix then using a percentage progress in the onscroll function to calculate the percentage that the element has moved through the scene. All of this works fine.

The issue is when rotating the element seems to shrink and then grow back to normal size through the progress of the scene. Is this the expected behavior of matrix3d?

The some of the initial values in 3dmatrix are 1 so I am accounting for that by adding 1 then subtracting 1. It works fine for everything except rotation.

Am I missing something in the math here that I am too stupid to figure out it starts and ends on the correct value but shrinks and grows throughout the scene.

Here is an example fiddle and snippet Fiddle Demo

Note: I am just using 700px for scene progress in the demo. You can ignore the effects after scrolling 700px or when the box rotates past 90deg this is just a demo.

let matrix;
const el = document.querySelector('.box');

const updateScroll = () => {

  const scrollPos = window.scrollY;
  const progress = scrollPos / 700;
  
  let m = [...matrix];
  m[0] = progress * (matrix[0] - 1) + 1;
  m[1] = progress * matrix[1];
  m[2] = progress * matrix[2];
  m[3] = progress * matrix[3];
  m[4] = progress * matrix[4];
  m[5] = progress * (matrix[5] - 1) + 1;
  m[6] = progress * matrix[6];
  m[7] = progress * matrix[7];
  m[8] = progress * matrix[8];
  m[9] = progress * matrix[9];
  m[10] = progress * (matrix[10] - 1) + 1;
  m[11] = progress * matrix[11];
  m[12] = progress * (matrix[12] / 100) * 100;
  m[13] = progress * (matrix[13] / 100) * 100;
  m[14] = progress * (matrix[14] / 100) * 100;
  m[15] = progress * (matrix[15] - 1) + 1;
  
  setTransform(el, toString(m));
}

const init = () => {
  const r1 = rotateZ(90);
  const t1 = translateY(700);
  matrix = multiply(t1,r1);
  window.addEventListener('scroll', updateScroll);
}

const setTransform = (el, transform) => {
  el.style.transform = transform;
  el.style.WebkitTransform = transform;
};

/*
*  
* REMATRIX Functions
* https://github.com/jlmakes/rematrix
*
*/


function translateY(distance) {
  const matrix = identity();
  matrix[13] = distance;
  return matrix;
}

function toString(source) {
  return `matrix3d(${format(source).join(', ')})`;
}

function rotateZ(angle) {
  const theta = (Math.PI / 180) * angle;
  const matrix = identity();

  matrix[0] = matrix[5] = Math.cos(theta).toFixed(6);
  matrix[1] = matrix[4] = Math.sin(theta).toFixed(6);
  matrix[4] *= -1;

  return matrix;
}

function format(source) {
  if (source.constructor !== Array) {
    throw new TypeError('Expected array.');
  }
  if (source.length === 16) {
    return source;
  }
  if (source.length === 6) {
    const matrix = identity();
    matrix[0] = source[0];
    matrix[1] = source[1];
    matrix[4] = source[2];
    matrix[5] = source[3];
    matrix[12] = source[4];
    matrix[13] = source[5];
    return matrix;
  }
  throw new RangeError('Expected array with either 6 or 16 values.');
}

function identity() {
  const matrix = [];
  for (let i = 0; i < 16; i++) {
    i % 5 == 0 ? matrix.push(1) : matrix.push(0);
  }
  return matrix;
}

function multiply(m, x) {
  const fm = format(m);
  const fx = format(x);
  const product = [];

  for (let i = 0; i < 4; i++) {
    const row = [fm[i], fm[i + 4], fm[i + 8], fm[i + 12]];
    for (let j = 0; j < 4; j++) {
      const k = j * 4;
      const col = [fx[k], fx[k + 1], fx[k + 2], fx[k + 3]];
      const result =
        row[0] * col[0] + row[1] * col[1] + row[2] * col[2] + row[3] * col[3];

      product[i + k] = result;
    }
  }

  return product;
}

init();
body{
  min-height: 400vh;
}

.box{
  position:absolute;
  top:0;
  left:0;
  right:0;
  bottom:0;
  margin: auto;
  width: 100px;
  height: 100px;
  background: green;
}
<div class="box"></div>

Anyone help here would be appreciated. Thanks.

Questioner
user3331344
Viewed
55
Kaiido 2020-02-01 12:53

Matrix rotation is not just a linear interpolation of the values.

You already have a correct implementation of single axis rotation, implying modifying both the scale and skew values:

function rotateZ(angle) {
  const theta = (Math.PI / 180) * angle;
  const matrix = identity();

  matrix[0] = matrix[5] = Math.cos(theta).toFixed(6);
  matrix[1] = matrix[4] = Math.sin(theta).toFixed(6);
  matrix[4] *= -1;

  return matrix;
}

So your algebra is all broken there, which will mess up the scale values and will shrink your element.

But you already have all you need, just use it.

const el = document.querySelector('.box');

const updateScroll = () => {

  const scrollPos = window.scrollY;
  const progress = Math.min(scrollPos / 700, 1);
  // create a new Matrix, correctly translated and rotated
  const t1 = translateY(scrollPos);
  const r1 = rotateZ( 360 * progress );
  const matrix = multiply(t1, r1);
  setTransform(el, toString(matrix));
}

const init = () => {
  window.addEventListener('scroll', updateScroll);
}

const setTransform = (el, transform) => {
  el.style.transform = transform;
  el.style.WebkitTransform = transform;
};

/*
*  
* REMATRIX Functions
* https://github.com/jlmakes/rematrix
*
*/


function translateY(distance) {
  const matrix = identity();
  matrix[13] = distance;
  return matrix;
}

function toString(source) {
  return `matrix3d(${format(source).join(', ')})`;
}

function rotateZ(angle) {
  const theta = (Math.PI / 180) * angle;
  const matrix = identity();

  matrix[0] = matrix[5] = Math.cos(theta).toFixed(6);
  matrix[1] = matrix[4] = Math.sin(theta).toFixed(6);
  matrix[4] *= -1;

  return matrix;
}

function format(source) {
  if (source.constructor !== Array) {
    throw new TypeError('Expected array.');
  }
  if (source.length === 16) {
    return source;
  }
  if (source.length === 6) {
    const matrix = identity();
    matrix[0] = source[0];
    matrix[1] = source[1];
    matrix[4] = source[2];
    matrix[5] = source[3];
    matrix[12] = source[4];
    matrix[13] = source[5];
    return matrix;
  }
  throw new RangeError('Expected array with either 6 or 16 values.');
}

function identity() {
  const matrix = [];
  for (let i = 0; i < 16; i++) {
    i % 5 == 0 ? matrix.push(1) : matrix.push(0);
  }
  return matrix;
}

function multiply(m, x) {
  const fm = format(m);
  const fx = format(x);
  const product = [];

  for (let i = 0; i < 4; i++) {
    const row = [fm[i], fm[i + 4], fm[i + 8], fm[i + 12]];
    for (let j = 0; j < 4; j++) {
      const k = j * 4;
      const col = [fx[k], fx[k + 1], fx[k + 2], fx[k + 3]];
      const result =
        row[0] * col[0] + row[1] * col[1] + row[2] * col[2] + row[3] * col[3];

      product[i + k] = result;
    }
  }

  return product;
}

init();
body{
  height: 400vh;
  min-height: 700px;
}

.box{
  position:absolute;
  top:0;
  left:0;
  right:0;
  bottom:0;
  margin: auto;
  width: 100px;
  height: 100px;
  background: green;
}
<div class="box"></div>