<div class="container">
  <div class="bound"></div>
  <div class="box-group">
    <div class="box">1</div>
    <div class="box">2</div>
    <div class="box">3</div>
    <div class="box">4</div>
    <div class="box">5</div>
  </div>
  
  <input class="angle-control" type="range" min="0" max="360" value="40">
</div>
body {
  margin: 0;
  min-height: 100vh;
  background:
    linear-gradient(to right, #0001 1px, transparent 1px) 0 0 / 8px 8px,
    linear-gradient(to bottom, #0001 1px, transparent 1px) 0 0 / 8px 8px;
  font-family: monospace;
}

.container {
  position: relative;
  min-height: 100vh;
}

.angle-control {
  position: fixed;
  top: 20px;
  left: 20px;
  z-index: 999;
}

.bound {
  position: absolute;
  top: 0;
  left: 0;
  outline: 1px dashed tomato;
}

.box-group {
  min-height: 100vh;
}

.box {
  display: grid;
  place-content: center;
  position: absolute;
  width: 100px;
  height: 100px;
  background-color: #ccc;
  font-size: 2rem;
}

.box:nth-child(1) {
  top: 50px;
  left: 200px;
  transform: rotate(15deg);
}

.box:nth-child(2) {
  top: 150px;
  left: 300px;
  transform: rotate(25deg);
}

.box:nth-child(3) {
  top: 80px;
  left: 80px;
  transform: translate(-25%, 50px) rotate(-20deg);
}

.box:nth-child(4) {
  top: 250px;
  left: 200px;
  transform: rotate(45deg);
}

.box:nth-child(5) {
  top: 20px;
  left: 300px;
  transform: translate(70%, 5px) rotate(-35deg);
}
import { vec2, mat2d, glMatrix } from 'https://cdn.skypack.dev/[email protected]';

function getElOffset(el, offsetParent = document.body) {
  let x = el.offsetLeft;
  let y = el.offsetTop;

  let op = el.offsetParent;

  while (op !== offsetParent && op !== null) {
    x += op.offsetLeft;
    y += op.offsetTop;
    op = op.offsetParent;
  }

  return [x, y];
}

function parseMatrix2D(str) {
  // only for 'matrix(a, b, c, d, tx, ty)'
  const [a, b, c, d, tx, ty] = str.slice(7, -1).split(',').map(parseFloat);
  return mat2d.fromValues(a, b, c, d, tx, ty);
}

const container = document.querySelector('.container');
const angleRange = container.querySelector('.angle-control');
const bound = container.querySelector('.bound');
const boxes = container.querySelectorAll('.box');

const points = [...boxes].flatMap((box, i) => {
  const w = box.offsetWidth;
  const h = box.offsetHeight;
  const [x, y] = getElOffset(box, container);

  const pts = [
    vec2.fromValues(x, y),
    vec2.fromValues(x + w, y),
    vec2.fromValues(x + w, y + h),
    vec2.fromValues(x, y + h),
  ];

  const mat = parseMatrix2D(getComputedStyle(box).transform);
  const origin = vec2.fromValues(x + w / 2, y + h / 2);

  pts.forEach((p) => {
    vec2.sub(p, p, origin);
    vec2.transformMat2d(p, p, mat);
    vec2.add(p, p, origin);
  });

  return pts;
});

function updateBbox(angle) {
  const rotMat = mat2d.fromRotation(mat2d.create(), glMatrix.toRadian(angle));
  const rotMatBack = mat2d.fromRotation(mat2d.create(), glMatrix.toRadian(-angle));

  const rotBackPoints = points.map((p) => {
    const np = vec2.create();
    return vec2.transformMat2d(np, p, rotMatBack);
  });

  const { x0, x1, y0, y1 } = rotBackPoints.reduce(
    (acc, [x, y]) => {
      acc.x0 = Math.min(acc.x0, x);
      acc.x1 = Math.max(acc.x1, x);
      acc.y0 = Math.min(acc.y0, y);
      acc.y1 = Math.max(acc.y1, y);
      return acc;
    },
    { x0: Infinity, x1: -Infinity, y0: Infinity, y1: -Infinity }
  );

  const bboxOrigin = vec2.fromValues((x0 + x1) / 2, (y0 + y1) / 2);
  vec2.transformMat2d(bboxOrigin, bboxOrigin, rotMat);

  const bboxCoord = vec2.fromValues(x0, y0);
  vec2.transformMat2d(bboxCoord, bboxCoord, rotMat);

  vec2.sub(bboxCoord, bboxCoord, bboxOrigin);
  vec2.transformMat2d(bboxCoord, bboxCoord, rotMatBack);
  vec2.add(bboxCoord, bboxCoord, bboxOrigin);

  const [bx, by] = bboxCoord;
  const bw = x1 - x0;
  const bh = y1 - y0;

  bound.style.transform = `translate(${bx}px, ${by}px) rotate(${angle}deg)`;
  bound.style.width = `${bw}px`;
  bound.style.height = `${bh}px`;
}

updateBbox(angleRange.valueAsNumber);

angleRange.addEventListener('input', event => {
  updateBbox(event.target.valueAsNumber);
});

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.