Skip to main content

Command Palette

Search for a command to run...

[TypeScript] 변환 원점 찾기

Published
3 min read
[TypeScript] 변환 원점 찾기

잠깐! transform에 대해 아직 잘 모른다면 [CSS] 변환 행렬을 먼저 읽어보세요!

상황에 따라 동적으로 뷰포트에 대한 transform-origin의 좌표를 구해야 할 수 있습니다. 대상이 변환이 수행되지 않은 경우라면 getBoundingClientRect를 사용해 쉽게 구할 수 있습니다. 하지만 변환이 수행된 경우는 생각보다 까다롭습니다.

차근차근 살펴보도록 하겠습니다. 우선 변환이 고려되지 않은 상황, 그중에서도 대상 영역에 대한 transform-origin의 상대 좌표를 가져오는 방법은 아래와 같습니다.

const [
    transformOriginX,
    transformOriginY
] = getComputedStyle(target)
    .transformOrigin
    .split(" ")
    .map(p => parseFloat(p));

style 속성은 지정된 경우에만 가져올 수 있을뿐더러 단위에 대한 일관성도 없기 때문에 실제 계산된 결과를 가져와야 합니다. 이는 getComputedStyle을 통해 비교적 간단히 수행할 수 있습니다.

이제 transform-origin대한 각 꼭짓점의 상대 좌표를 얻을 차례입니다. 영역에 대한 transform-origin의 상대 좌표를 \((O_x, O_y)\)라 하고 영역의 너비를 $w$, 높이를 $h$라 할 때 각 꼭짓점의 좌표는 아래와 같습니다.

$$\begin{aligned} & A(-O_x, -O_y) \\ & B(w - O_x, -O_y) \\ & C(w - O_x, h - O_y) \\ & D(-O_x, h - O_y) \end{aligned}$$

그리고 변환 행렬은 함수로 나타낼 때 \(m: (x, y) \mapsto (ax + cy + e, bx + dy + f)\) 이므로 좌표는 각각 아래와 같이 변환됩니다.

$$\begin{aligned} x & = -O_x \\ x' & = ax + cy + e \\ \\ y & = -O_y \\ y' & = bx + dy + f \end{aligned}$$

라 하면

$$\begin{aligned} A(x, y) & \xrightarrow{m} A'(x', y') \\ B(x + w, y) & \xrightarrow{m} B'(x' + aw, y' + bw) \\ C(x + w, y + h) & \xrightarrow{m} C'(x' + aw + ch, y' + bw + dh) \\ D(x, y + h) & \xrightarrow{m} D'(x' + ch, y' + dh) \end{aligned}$$

이제 다음은 무엇을 해야 할까요? 새로운 영역의 한 꼭짓점 좌표를 구해야 합니다. 이 좌표와 getBoundingClientRect를 통해 얻을 수 있는 실제 좌표의 차를 구하면 초기 변환 원점 즉, 변환 이전 transform-origin의 뷰포트에 대한 상대 좌표를 구할 수 있습니다.

바로 가봅시다. 어느 꼭짓점이라도 상관없지만 가장 조건이 단순한 점은 좌측 상단일 것입니다. 모든 축에 대해 가장 작은 경우이니 말이지요.

$$\begin{aligned} x_{min} & = x' + min(min(aw, ch), min(0, aw + ch)) \\ & = ax + cy + e + min(min(aw, ch), min(0, aw + ch))\\ \\ y_{min} & = y' + min(min(bw, dh), min(0, bw + dh)) \\ & = bx + dy + f + min(min(bw, dh), min(0, bw + dh)) \end{aligned}$$

따라서 getBoundingClientRect로 얻은 좌측 상단 좌표를 $(l, t)$라 할 때,

$$\begin{aligned} x_{transformOrigin} & = l - x_{min} \\ & = l - (ax + cy + e + min(min(aw, ch), min(0, aw + ch)))\\ \\ y_{transformOrigin} & = t - y_{min} \\ & = t - (bx + dy + f + min(min(bw, dh), min(0, bw + dh))) \end{aligned}$$

결국 변환 후 transform-origin은 아래와 같습니다.

$$\begin{aligned} x'_{transformOrigin} & = x_{transformOrigin} + e \\ & = l - (ax + cy + e + min(min(aw, ch), min(0, aw + ch))) + e \\ & = l - (ax + cy + min(min(aw, ch), min(0, aw + ch))) \\ \\ y'_{transformOrigin} & = y_{transformOrigin} + f \\ & = t - (bx + dy + f + min(min(bw, dh), min(0, bw + dh))) + f \\ & = t - (bx + dy + min(min(bw, dh), min(0, bw + dh))) \end{aligned}$$

이를 코드로 나타내면 아래와 같습니다.

interface Point {
    x: number;
    y: number;
}

function findTransformOrigin(target: HTMLElement): Point {
    const [ x, y ] = getComputedStyle(target)
        .transformOrigin
        .split(" ")
        .map(p => -parseFloat(p));
    const { a, b, c, d } = new DOMMatrix(getComputedStyle(target).transform);
    const { offsetWidth: w, offsetHeight: h } = box;

    const { left: l, top: t } = box.getBoundingClientRect();

    return {
        x: l - (
            a * x + c * y + Math.min(
                Math.min(a * w, c * h),
                Math.min(0, a * w + c * h)
            )
        ),
        y: t - (
            b * x + d * y + Math.min(
                Math.min(b * w, d * h),
                Math.min(0, b * w + d * h)
            )
        )
    };
}

읽어주셔서 감사합니다!

묻고 답하기

개인적인 판단에 의해 적절하다고 여겨지는 경우, 모두가 볼 수 있도록 이곳에 문답이 추가됩니다. 그렇지 않더라도 질문에 대한 답변은 별도로 이루어집니다.

More from this blog

[TypeScript] 자료 구조로 담아내기. #14 - 연결 리스트(with. 이중 연결)

이번 편은 이전 편으로부터 이어집니다. 연결 리스트의 또 다른 형태는 이중 연결 리스트입니다. 후임자의 참조만 저장하는 단일 연결 리스트와 달리 이중 연결 리스트는 선임자의 참조도 함께 저장합니다. interface DoublyLinkedListNode<T> extends DoublyLinkedList<T> { linkBefore(succ: DoublyLinkedListNode<T>): void; linkAfter(pred: D...

May 3, 20253 min read9
[TypeScript] 자료 구조로 담아내기. #14 - 연결 리스트(with. 이중 연결)

[TypeScript] 자료 구조로 담아내기. #12 - 연결 리스트(with. null 노드)

이번 편은 이전 편으로부터 이어집니다. 조금 예리하신 분들은 이전 편에서 다룬 헤드 노드만으로는 충분치 않다는 것을 느끼셨을 것입니다. 최소한 첫 노드에 예외를 부여하는 상황은 피했으나 후임자가 null임을 확인하는 구현 또한 만족스럽지는 않습니다. 카이로의 코끼리 카이로의 코끼리에 대해 알고 계십니까? 링크를 통해 확인하실 수 있습니다. 꽤 재밌으므로 한번 읽어 보시길 추천합니다. 여기서 중요한 아이디어는 끝 위치에 미리 코끼리를 둔다는 것...

Apr 19, 20252 min read12
[TypeScript] 자료 구조로 담아내기. #12 - 연결 리스트(with. null 노드)

[TypeScript] 자료 구조로 담아내기. #11 - 연결 리스트(with. 헤드 노드)

이번 편은 이전 편으로부터 이어집니다. 이전 편에서는 연결 리스트의 접근자는 첫 노드의 선임자가 적절하다는 것을 알았습니다. 얼핏 보기엔 문제없어 보이는데 어떤가요? 최상위 연결 리스트 즉, 가장 처음에 위치한 노드로 시작하는 경우를 생각해 봅시다. 가장 처음에 위치한 노드의 선임자는 무엇인가요? 맞습니다. 선임자가 존재하지 않겠지요. 그럼 어떻게 해야 할까요? 선임자를 가지지 않는 첫 노드를 갖는 연결 리스트에 대해 예외를 적용해야 할까요?...

Apr 12, 20252 min read13
[TypeScript] 자료 구조로 담아내기. #11 - 연결 리스트(with. 헤드 노드)

고라니드로의 블로그

47 posts