个人纪录 CSS 中的变换和Matrix与四元数的使用

49 阅读2分钟

css中的2d矩阵的写法

//  transform: matrix(a, b, c, d, e, f);

// | a  c  e |
// | b  d  f |
// | 0  0  1 |


使用矩阵表示选择

//  [
//      1  0  0
//      0  1  0
//      0  0  1
//  ]
//  known (x,y) startAngle(a) rotatedAngle(o) r
//  unchanged variable r
//  changed variable x'  y'
//  unknown (x',y')

旋转之后目标点的计算
//  x' = r cos(a + o)    y' = r sin(a + o)

二角和公式
//  x' = r [cos(a)cos(o) + sin(a)sin(o)]
//  y' = r [sin(a)cos(o) - cos(a)sin(o)]

把x和y带入到 x' 和 y' 之后化简
// a = cos(θ)
// b = sin(θ)
// c = -sin(θ)
// d = cos(θ)

不用对坐标进行变换
// e = translateX (translate the element in the x-direction)
// f = translateY (translate the element in the y-direction)

使用四元数表示旋转

//  quaternion
//  multiply
//  rotate v by q
//  v' = q * v * q^*
//  |q| = sqrt(w^2 + x^2 + y^2 + z^2) = 1
//  q^* = w - x*i - y*j - z*k

四元数的简单实现,gpt生成的

class Quaternion {
    constructor(w, x, y, z) {
        this.w = w;
        this.x = x;
        this.y = y;
        this.z = z;
    }

    multiply(q) {
        return new Quaternion(this.w * q.w - this.x * q.x - this.y * q.y - this.z * q.z,
            this.w * q.x + this.x * q.w + this.y * q.z - this.z * q.y,
            this.w * q.y - this.x * q.z + this.y * q.w + this.z * q.x,
            this.w * q.z + this.x * q.y - this.y * q.x + this.z * q.w)
    }

   static fromAxisRad(axis, rad) {

        const half_rad = rad / 2;

        const cos = Math.cos(half_rad);
        const sin = Math.sin(half_rad);

        const {x, y, z} = axis;
        return new Quaternion(cos, sin * x, sin * y, sin * z)
    }

    //  v' = q * v * q^*
    rotateVector(v){
        const qVector = new Quaternion(0, v.x, v.y, v.z);
        const conjQ = this.conjugate();
        const {x,y,z} = this.multiply(qVector).multiply(conjQ);
        return {x,y,z}
    }

    conjugate() {
        const {w, x, y, z} = this
        return new Quaternion(w, x, y, z)
    }
}

使用四元数来旋转

const axis = { x: 0, y: 1, z: 0 };  // 绕Y轴旋转
const angle = Math.PI / 4;  // 45度
const q = Quaternion.fromAxisRad(axis, angle);

结合vue来实现这个有趣的动画 这里我们想要沿着某个中心来运动,而不是以自己为中心来旋转

<script setup lang="ts">

import {CoroutineManager} from "~/utils/Coroutine";
import {computedTransMatrix} from "~/utils/Math";


const containerRef:Ref<HTMLDivElement | null> = ref(null)

const data = reactive({
  boxes: [
    {classNames: "bg-pink-100", style: ""},
    {classNames: "bg-pink-200", style: ""},
    {classNames: "bg-pink-300", style: ""},
    {classNames: "bg-pink-400", style: ""},
    {classNames: "bg-pink-500", style: ""},
    {classNames: "bg-pink-600", style: ""},
    {classNames: "bg-pink-700", style: ""},
    {classNames: "bg-pink-800", style: ""},
    {classNames: "bg-blue-300", style: ""},
    {classNames: "bg-blue-400", style: ""},
  ],

  rotateAngle:0,
})

onMounted(() => {
  // const group = document.querySelector(".group");

  // const boxes = Array.from(group.children);

  GCM.startCoroutine(animationCo())

  GCM.startCoroutine(tickerCo())

})


async function* tickerCo() {
  while (1) {
    yield await CoroutineManager.WaitForEndOfFrame()

    data.rotateAngle +=.1
  }
}


async function* animationCo() {
  while (1) {

    render()

    yield await CoroutineManager.WaitForEndOfFrame()

  }
}


function render() {

  let containerEl = unref(containerRef);

  if(!containerEl){
    return
  }

  const {clientWidth: contW, clientHeight: contH} = containerEl;

  for (let i = 0; i < data.boxes.length; i++) {
    const box = data.boxes[i];

    // const {clientWidth: boxW, clientHeight: boxH} = box;

    const boxW = 48;
    const boxH = 48;

    const t = computedTransMatrix(boxW, boxH, contW, contH, data.rotateAngle * (i+1), 100);
    box.style = `transform:${t}`
  }
}


</script>

<template>
  <div ref="containerRef" class="size-[400px] relative">
    <div v-for="(box,i) in data.boxes" :key="box.classNames" class="absolute left-0 top-0 size-12 rounded"  :class="box.classNames" :style="box.style">
      <div class="size-full grid place-items-center text-white font-bold">
        {{i}}
      </div>
    </div>
  </div>
</template>

<style scoped lang="scss">

</style>

相关数学工具的函数

export const angle2Rad = (ang:number) => {
    return ang / 180 * Math.PI
}

export  const getCenterPos = (nodeW:number, nodeH:number, containerW:number, containerH:number) => {
    return {
        x: Math.floor(containerH / 2) - Math.floor(nodeH / 2),
        y: Math.floor(containerH / 2) - Math.floor(nodeH / 2),
    }
}

export const getMoveByAngle = (rotatedAng:number, moveDistance:number) => {
    const rad = angle2Rad(rotatedAng);

    const r = moveDistance
    const y = Math.sin(rad) * r;
    const x = Math.cos(rad) * r;

    return {x, y}
}


export const computedTransMatrix = (nodeW:number, nodeH:number, containerW:number, containerH:number, rotatedAng:number, moveDistance:number) => {

    const centerPos = getCenterPos(nodeW, nodeH, containerW, containerH);

    const moveDis = getMoveByAngle(rotatedAng, moveDistance);

    //  calc final pos
    const {x, y} = Array.from([
        moveDis,
        centerPos
    ]).reduceRight((a, {x, y}) => {
        return {x: a.x + x, y: a.y + y}
    });

    return `matrix(1,0,0,1,${x},${y})`
}