bodev.vn

Bản vẽ thành công trình

Mặt đứng nhà phố tự vẽ dần theo cuộn: lưới trục, khối, chi tiết, kích thước, rồi quét ra phối cảnh.

Cách hoạt động

  1. 1.Nét liền tự vẽ bằng mẹo stroke-dasharray = độ dài nét, rồi tween stroke-dashoffset về 0. Không cần plugin DrawSVG.
  2. 2.Nét đứt (trục, cao độ) không dùng được mẹo trên vì sẽ mất mẫu nét đứt, nên kéo dài bằng scale.
  3. 3.Bản vẽ chia nhóm theo data-layer: truc, khoi, chi-tiet, kich-thuoc. Mỗi nhóm là một bước trên timeline.
  4. 4.Bước cuối lớp phối cảnh quét từ trái sang bằng clip-path, nét vẽ mờ đi còn lại như lớp chú thích.

Thử chỉnh

Dùng bản vẽ của bạn
Xuất mặt đứng từ AutoCAD/Revit ra SVG, dán vào .elevation. Nét muốn tự vẽ thì thêm class="ln", gom vào các nhóm data-layer.
Có bản vẽ, chưa có ảnh render
Chụp bản vẽ mặt đứng, đưa cho AI chỉnh ảnh (ví dụ Nano Banana) yêu cầu "render ảnh thật, giữ nguyên hình học và khung". Ảnh demo làm đúng cách này nên khớp nét vẽ.
Vẽ chậm hơn
Tăng end: "+=3000" trong ScrollTrigger, hoặc tăng duration trong hàm draw.
Đổi màu bản vẽ
Sửa --ink (nét chính) và --accent (trục, kích thước) ở đầu file CSS.

Code tham khảo

<!doctype html>
<html lang="vi">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Bản vẽ thành công trình · bodev.vn</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Be+Vietnam+Pro:wght@400;500;700;800&display=swap" rel="stylesheet" />
<style>
:root {
  --paper: #f1efe9;
  --ink: #1d3557;
  --muted: #6c7a89;
  --accent: #e76f51;
  --grid: rgba(29, 53, 87, 0.08);
}

* { box-sizing: border-box; }
body {
  margin: 0;
  font-family: "Be Vietnam Pro", system-ui, sans-serif;
  background: var(--paper);
  color: var(--ink);
}

.intro,
.outro {
  min-height: 70vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 40px clamp(20px, 6vw, 80px);
}
.kicker { margin: 0; color: var(--accent); font-size: 13px; letter-spacing: 0.12em; text-transform: uppercase; }
.intro h1 { max-width: 12ch; margin: 10px 0 0; font-size: clamp(38px, 6.5vw, 80px); font-weight: 800; line-height: 1; }
.lead { margin: 16px 0 0; color: var(--muted); }

.sheet {
  height: 100vh;
  display: grid;
  grid-template-columns: minmax(180px, 280px) 1fr;
  align-items: center;
  gap: 24px;
  padding: 24px clamp(20px, 5vw, 64px);
  overflow: hidden;
  /* giấy kẻ ô */
  background-image: linear-gradient(var(--grid) 1px, transparent 1px), linear-gradient(90deg, var(--grid) 1px, transparent 1px);
  background-size: 24px 24px;
}
.steps { margin: 0; padding: 0; list-style: none; display: grid; gap: 14px; }
.step { font-size: 18px; font-weight: 500; opacity: 0.3; }
.step b { display: inline-block; width: 2.4em; color: var(--accent); }

.board { position: relative; height: 100%; max-height: 88vh; aspect-ratio: 3 / 4; justify-self: center; }
.elevation,
.render-photo { position: absolute; inset: 0; width: 100%; height: 100%; }
.render-photo { object-fit: cover; }

.drawing .ln { fill: none; stroke: var(--ink); stroke-width: 1.4; }
.drawing .bold { stroke-width: 2.6; }
.drawing .thin { stroke-width: 0.8; }
.drawing .axis { stroke: var(--accent); stroke-width: 0.8; stroke-dasharray: 14 4 2 4; }
.drawing .level { stroke: var(--accent); stroke-width: 0.6; stroke-dasharray: 4 4; }
.drawing .dim { stroke: var(--accent); stroke-width: 1; }
.drawing text { fill: var(--ink); font-size: 13px; font-weight: 700; }
.drawing .lv { fill: var(--accent); font-size: 11px; }

.stamp {
  position: absolute;
  right: 0;
  bottom: -6px;
  margin: 0;
  font-size: 11px;
  letter-spacing: 0.12em;
  color: var(--muted);
}

.outro { align-items: flex-start; }
.outro h2 { max-width: 20ch; margin: 0 0 24px; font-size: clamp(28px, 4vw, 48px); }
.btn {
  /* CTA trong demo là <button>, không gắn link. Nối vào form hoặc popup của bạn. */
  border: 0;
  font: inherit;
  cursor: pointer;
  padding: 14px 26px;
  border-radius: 999px;
  background: var(--ink);
  color: var(--paper);
  font-weight: 700;
}

@media (max-width: 760px) {
  .sheet { grid-template-columns: 1fr; grid-template-rows: auto 1fr; }
  .steps { grid-auto-flow: column; overflow: hidden; gap: 10px; }
  .step { font-size: 13px; white-space: nowrap; }
  .step b { width: auto; margin-right: 4px; }
  /* Mobile: khung tính theo chiều ngang, không tràn mép. */
  .board { width: 100%; height: auto; max-height: none; justify-self: stretch; }
}
</style>
</head>
<body>
<section class="intro">
  <p class="kicker">Kiến trúc · Nhà phố 5×20m</p>
  <h1>Từ bản vẽ tới công trình</h1>
  <p class="lead">Cuộn xuống để xem bản vẽ tự dựng.</p>
</section>

<section class="sheet">
  <ol class="steps">
    <li class="step"><b>01</b> Lưới trục, cao độ</li>
    <li class="step"><b>02</b> Phác khối</li>
    <li class="step"><b>03</b> Chi tiết mặt đứng</li>
    <li class="step"><b>04</b> Kích thước</li>
    <li class="step"><b>05</b> Phối cảnh</li>
  </ol>

  <div class="board">
    <!--
      Bản vẽ mặt đứng. Thay bằng SVG xuất từ AutoCAD/Revit/Illustrator cũng được:
      nét nào muốn tự vẽ thì thêm class="ln", chia nhóm theo data-layer như bên dưới.
    -->
    <!-- Ảnh phối cảnh nằm dưới nét vẽ. Ảnh phải cùng tỉ lệ 3:4 và cùng khung với bản vẽ. -->
    <img class="render-photo" src="https://bodev.vn/lab-assets/ban-ve-thanh-cong-trinh/mat-dung.webp" alt="Phối cảnh mặt tiền nhà phố" width="1200" height="1608" />
    <svg class="elevation" viewBox="0 0 600 800" role="img" aria-label="Mặt đứng chính nhà phố 4 tầng">
      <g class="drawing">
        <g data-layer="truc">
          <line class="ln axis" x1="200" y1="60" x2="200" y2="790" />
          <line class="ln axis" x1="400" y1="60" x2="400" y2="790" />
          <circle class="ln" cx="200" cy="40" r="14" />
          <circle class="ln" cx="400" cy="40" r="14" />
          <text class="txt" x="200" y="45" text-anchor="middle">1</text>
          <text class="txt" x="400" y="45" text-anchor="middle">2</text>
          <line class="ln level" x1="150" y1="740" x2="480" y2="740" />
          <line class="ln level" x1="150" y1="604" x2="480" y2="604" />
          <line class="ln level" x1="150" y1="468" x2="480" y2="468" />
          <line class="ln level" x1="150" y1="332" x2="480" y2="332" />
          <line class="ln level" x1="150" y1="196" x2="480" y2="196" />
          <line class="ln level" x1="150" y1="116" x2="480" y2="116" />
        </g>

        <g data-layer="khoi">
          <line class="ln bold" x1="20" y1="740" x2="580" y2="740" />
          <path class="ln thin" d="M60 740 V300 H200 M400 240 H540 V740" />
          <path class="ln bold" d="M200 740 V156 H400 V740" />
          <line class="ln" x1="200" y1="196" x2="400" y2="196" />
          <line class="ln" x1="200" y1="332" x2="400" y2="332" />
          <line class="ln" x1="200" y1="468" x2="400" y2="468" />
          <rect class="ln" x="195" y="596" width="210" height="10" />
        </g>

        <g data-layer="chi-tiet">
          <!-- tầng 1: cửa cuốn + cửa đi -->
          <rect class="ln" x="210" y="636" width="120" height="104" />
          <path class="ln thin" d="M210 650 H330 M210 664 H330 M210 678 H330 M210 692 H330 M210 706 H330 M210 720 H330" />
          <rect class="ln" x="345" y="640" width="45" height="100" />
          <!-- tầng 2–4: cửa kính + lam gỗ -->
          <rect class="ln" x="215" y="488" width="75" height="106" />
          <rect class="ln" x="215" y="352" width="75" height="106" />
          <rect class="ln" x="215" y="216" width="75" height="106" />
          <path class="ln thin" d="M252 488 V594 M252 352 V458 M252 216 V322" />
          <path class="ln thin" d="M305 468 V604 M317 468 V604 M329 468 V604 M341 468 V604 M353 468 V604 M365 468 V604 M377 468 V604 M389 468 V604" />
          <path class="ln thin" d="M305 332 V468 M317 332 V468 M329 332 V468 M341 332 V468 M353 332 V468 M365 332 V468 M377 332 V468 M389 332 V468" />
          <path class="ln thin" d="M305 196 V332 M317 196 V332 M329 196 V332 M341 196 V332 M353 196 V332 M365 196 V332 M377 196 V332 M389 196 V332" />
          <path class="ln" d="M205 562 H300 M205 426 H300 M205 290 H300" />
          <!-- mái: lan can + giàn hoa -->
          <path class="ln" d="M215 156 V116 H385 V156 M300 156 V116" />
          <circle class="ln" cx="240" cy="136" r="18" />
          <circle class="ln" cx="360" cy="138" r="16" />
          <!-- cây -->
          <path class="ln" d="M120 740 V610" />
          <circle class="ln" cx="120" cy="560" r="62" />
        </g>

        <g data-layer="kich-thuoc">
          <path class="ln dim" d="M200 770 H400 M200 762 V778 M400 762 V778" />
          <text class="txt" x="300" y="764" text-anchor="middle">5.000</text>
          <path class="ln dim" d="M560 740 V116 M552 740 H568 M552 116 H568" />
          <text class="txt" x="574" y="432" transform="rotate(-90 574 432)" text-anchor="middle">16.600</text>
          <text class="txt lv" x="484" y="737">±0.000</text>
          <text class="txt lv" x="484" y="601">+3.400</text>
          <text class="txt lv" x="484" y="465">+6.800</text>
          <text class="txt lv" x="484" y="329">+10.200</text>
          <text class="txt lv" x="484" y="193">+13.600</text>
          <text class="txt lv" x="484" y="113">+16.600</text>
        </g>
      </g>
    </svg>
    <p class="stamp">MẶT ĐỨNG TRỤC 1–2 · TL 1:100</p>
  </div>
</section>

<section class="outro">
  <h2>Có bản vẽ rồi, muốn khách thấy nó sống động?</h2>
  <button type="button" class="btn">Gửi bản vẽ cho chúng tôi</button>
</section>

<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/ScrollTrigger.min.js"></script>
<script>
// Bản vẽ thành công trình — bodev.vn/lab/ban-ve-thanh-cong-trinh
// Cần: gsap + ScrollTrigger. Không cần plugin DrawSVG: tự vẽ nét bằng stroke-dashoffset.

gsap.registerPlugin(ScrollTrigger);

const svg = document.querySelector(".elevation");
const steps = document.querySelectorAll(".step");
const inLayer = (name, selector) => svg.querySelectorAll(`[data-layer="${name}"] ${selector}`);
// Lớp phối cảnh: bản SVG tô màu (.render) hoặc ảnh render thật (.render-photo).
const renders = document.querySelectorAll(".render, .render-photo");

// Nét liền: đặt dasharray = độ dài nét, dịch offset hết độ dài là nét "biến mất".
// Tween offset về 0 là nét tự vẽ ra.
function draw(tl, elements, at, stagger) {
  const solid = [...elements].filter((el) => !el.matches(".axis, .level"));
  solid.forEach((el) => {
    const len = el.getTotalLength();
    el.style.strokeDasharray = `${len}`;
    el.style.strokeDashoffset = `${len}`;
  });
  tl.to(solid, { strokeDashoffset: 0, duration: 0.8, stagger, ease: "power1.inOut" }, at);
}

// Nét đứt (trục, cao độ) giữ mẫu nét đứt, nên kéo dài bằng scale thay vì dashoffset.
function stretch(tl, at) {
  tl.from(inLayer("truc", ".axis"), { scaleY: 0, transformOrigin: "50% 0%", duration: 0.8, stagger: 0.1 }, at).from(
    inLayer("truc", ".level"),
    { scaleX: 0, transformOrigin: "0% 50%", duration: 0.8, stagger: 0.06 },
    at + 0.2,
  );
}

function highlight(tl, n, at) {
  tl.to(steps, { opacity: 0.3, x: 0, duration: 0.2 }, at).to(steps[n], { opacity: 1, x: 8, duration: 0.2 }, at);
}

gsap.set(renders, { clipPath: "inset(0% 100% 0% 0%)" });

const tl = gsap.timeline({ defaults: { ease: "power2.out" } });

highlight(tl, 0, 0);
stretch(tl, 0);
draw(tl, inLayer("truc", "circle"), 0.3, 0.1);
tl.from(inLayer("truc", ".txt"), { opacity: 0, duration: 0.3 }, 0.8);

highlight(tl, 1, 1.2);
draw(tl, inLayer("khoi", ".ln"), 1.2, 0.12);

highlight(tl, 2, 2.4);
draw(tl, inLayer("chi-tiet", ".ln"), 2.4, 0.05);

highlight(tl, 3, 3.6);
draw(tl, inLayer("kich-thuoc", ".ln"), 3.6, 0.15);
tl.from(inLayer("kich-thuoc", ".txt"), { opacity: 0, y: 6, duration: 0.3, stagger: 0.05 }, 3.8);

highlight(tl, 4, 4.8);
// Phối cảnh quét từ trái sang, nét vẽ mờ đi còn lại như lớp chú thích.
tl.to(renders, { clipPath: "inset(0% 0% 0% 0%)", duration: 1.2, ease: "power2.inOut" }, 4.8).to(
  ".drawing",
  { opacity: 0.25, duration: 1 },
  5,
);

if (matchMedia("(prefers-reduced-motion: reduce)").matches) {
  tl.progress(1);
} else {
  ScrollTrigger.create({
    trigger: ".sheet",
    start: "top top",
    end: "+=3000",
    pin: true,
    scrub: 1,
    animation: tl,
  });
}

gsap.from(".intro > *", { opacity: 0, y: 30, duration: 0.8, stagger: 0.1, ease: "expo.out" });
</script>
</body>
</html>

Code để học cách làm, không phải thư viện. Không đảm bảo chạy ngay trên site khác, dùng thì tự kiểm tra.

Tiếp trong Kiến trúc & kỹ sưTách tầng công trình