เบื้องหลังการสร้าง · Behind the Blueprint
หน้านี้อธิบายวิธีคิดและวิธีสร้างเว็บไซต์สตูดิโอเส้นดินสอทั้งหมด ตั้งแต่แนวคิดศิลป์ไปจนถึงโค้ดระดับเทคนิค เขียนให้คนที่ไม่ใช่โปรแกรมเมอร์อ่านรู้เรื่อง โดยวงเล็บศัพท์เทคนิคกำกับไว้ให้คนที่อยากไปค้นต่อ
01 · แนวคิดและทิศทางศิลป์
โจทย์คือเว็บของบริษัทสถาปนิก ดังนั้นแทนที่จะใช้รูปถ่ายอาคารเหมือนเว็บทั่วไป เราเลือกทิศทางเดียวแล้วดันให้สุด: "พิมพ์เขียว" (blueprint aesthetic) — ทุกอย่างบนหน้าเว็บต้องดูเหมือนถูกวาดขึ้นด้วยมือสถาปนิก ไม่มีรูปถ่ายแม้แต่รูปเดียว ทุกภาพคือลายเส้นที่เขียนด้วยโค้ดล้วน ๆ
องค์ประกอบที่ยึดเป็นภาษาเดียวกันทั้งเว็บ: พื้นหลังกระดาษกราฟ, เส้นบอกระยะพร้อมหัวลูกศรเฉียง (dimension lines), กรอบกระดาษแบบพร้อมช่องชื่อแบบมุมขวาล่าง (title block), ตราประทับ "ตรวจแล้ว", เลขแผ่นงานกำกับทุกส่วน (Sheet A-01, A-02, …) และเคอร์เซอร์กากบาทแบบโปรแกรมเขียนแบบ ทั้งหมดนี้ทำให้เว็บ "เล่นบทเดียว" ตั้งแต่บนสุดจนล่างสุด — ซึ่งคือหัวใจของงานออกแบบระดับพรีเมียม: เลือกหนึ่งไอเดีย แล้วซื่อสัตย์กับมันทุกพิกเซล
02 · ชุดสี
พิมพ์เขียวของจริงมีแค่สองสี: น้ำเงินเข้มกับเส้นขาว เราเพิ่ม "กระดาษไข" เป็นสีที่สามไว้สลับจังหวะ ให้หน้าเว็บหายใจได้ และสร้างสีรอง (สีน้ำเงินจาง สีน้ำเงินลึก) จากการปรับความโปร่งใสของสีหลักเท่านั้น ไม่เพิ่มสีใหม่
03 · ตัวอักษร
ทุกตารางเมตร เริ่มจากเส้นดินสอ
Chakra Petch — หัวเรื่องและเนื้อความSHEET A-201 · SCALE 1:100 · ฿2,200/SQ.M.
JetBrains Mono — ป้ายกำกับ ตัวเลข มิติChakra Petch เป็นฟอนต์จากนักออกแบบไทย รองรับภาษาไทยเต็มรูปแบบ และมีบุคลิกเหลี่ยม ๆ เชิงเทคนิคที่เข้ากับงานเขียนแบบพอดี ส่วน JetBrains Mono เป็นฟอนต์ความกว้างคงที่ (monospace) ใช้กับทุกอย่างที่เป็น "ข้อมูล" — เลขแผ่นงาน ระยะ ราคา เบอร์โทร — เลียนแบบตัวหนังสือพิมพ์ดีดบนแบบก่อสร้างจริง กติกาสำคัญคือเนื้อความภาษาไทยตั้งระยะบรรทัด (line-height) 1.7–1.8 เพื่อให้สระบน-ล่างไม่ชนกัน
04 · เทคนิคที่ใช้ ทีละตัว
Technique 01SVG getTotalLength() · stroke-dashoffset
รูปด้านหน้าบ้านในส่วนแรกไม่ใช่รูปภาพ แต่เป็นภาพเวกเตอร์ (SVG) ที่วาดเส้นต่อเส้นด้วยโค้ด เดิมทีใช้แอตทริบิวต์ pathLength="1" ให้เบราว์เซอร์ปรับหน่วยเส้นประให้เอง แต่เทคนิคนี้พึ่งฟีเจอร์ SVG2 ที่ Safari เพิ่งรองรับตั้งแต่เวอร์ชัน 16 (ปี 2565) — บนเบราว์เซอร์รุ่นเก่ากว่านั้น เส้นจะกลายเป็นจุดประเล็ก ๆ แทนที่จะเป็นเส้นเต็ม จึงเปลี่ยนมาใช้ JavaScript วัดความยาวเส้นจริงด้วย path.getTotalLength() ทันทีที่หน้าโหลด แล้วค่อยตั้ง stroke-dasharray/stroke-dashoffset เป็นหน่วยพิกเซลจริง — ใช้ได้ทุกเบราว์เซอร์ ไม่ต้องพึ่งฟีเจอร์ใหม่ ค่าเริ่มต้นก่อนสคริปต์ทำงาน (หรือกรณี JS พัง) คือเส้นเต็มเสมอ (ไม่มีจุดประ) จึงไม่มีทางเห็นเส้นขาด ๆ แหว่ง ๆ แต่ละเส้นตั้งเวลาเริ่มต่างกันด้วยตัวแปร --d เพื่อให้วาดไล่ลำดับเหมือนคนเขียนแบบจริง
// วัดความยาวเส้นจริง แทนการพึ่ง pathLength (Safari <16 ไม่รองรับ)
path.style.setProperty('--len', path.getTotalLength());
path.classList.add('pl-ready');
.draw{stroke-dasharray:none; stroke-dashoffset:0} /* ค่าเริ่มต้น = เส้นเต็ม */
.draw.pl-ready{
stroke-dasharray:var(--len); stroke-dashoffset:var(--len);
animation:drawline 1.7s forwards; animation-delay:var(--d);
}
@keyframes drawline{to{stroke-dashoffset:0}}
Technique 02CSS 3D transforms — no WebGL
บ้านสามมิติกลางหน้าเว็บไม่ได้ใช้โปรแกรม 3D ใด ๆ — มันคือกล่อง <div> ธรรมดาที่ถูกหมุนในสามมิติด้วย CSS ฉากทั้งหมดถูกเอียงด้วย rotateX(60deg) rotateZ(-45deg) (มุมมองไอโซเมตริกแบบแบบก่อสร้าง) ผนังแต่ละด้านคือ div ที่ตั้งขึ้นด้วย rotateX(90deg) หรือ rotateY(-90deg) รอบขอบของมัน (transform-origin: 0 0) หลังคาจั่วคือระนาบสองแผ่นเอียง 27.8° และหน้าจั่วสามเหลี่ยมตัดรูปด้วย clip-path ทุกผิวโปร่งใสขอบขาว จึงออกมาเป็น "โครงลายเส้น" เหมือนแบบ 3D ของสถาปนิกพอดี
.iso{transform:rotateX(60deg) rotateZ(-45deg);
transform-style:preserve-3d}
.f.wn{transform-origin:0 0; transform:rotateX(90deg)} /* ผนัง */
.gable{transform:rotateZ(-90deg) rotateX(90deg);
clip-path:polygon(0 100%,100% 100%,50% 0)} /* จั่ว */
Technique 03GSAP ScrollTrigger — pin + scrub
จังหวะ "ว้าว" ของเว็บ: หน้าจอถูกตรึงไว้ (pin) ขณะผู้ชมเลื่อน แล้วความคืบหน้าของการเลื่อนถูกผูกกับไทม์ไลน์แอนิเมชัน (scrub) — เลื่อนมาก บ้านประกอบมาก เลื่อนกลับ บ้านถอดชิ้นกลับ ชิ้นส่วนแต่ละชั้น (ฐานราก → ผนังชั้น 1 → ชั้น 2 → หลังคา → โรงรถ → ตราอนุมัติ) หล่นลงมาตามแกน Z ของฉากสามมิติ พร้อมแถบเปอร์เซ็นต์และรายการขั้นตอนที่สว่างไล่ตามจังหวะจริง
gsap.timeline({scrollTrigger:{
trigger:'#build', start:'top top',
end:'+=2800', scrub:.65, pin:true
}})
.from('.el-slab',{z:'+=200', autoAlpha:0})
.from('.el-f1', {z:'+=240', autoAlpha:0}) // ...ไล่ทีละชั้น
Technique 04CSS transform + transition · IntersectionObserver
เส้นบอกระยะใต้หัวข้อสร้างจาก div เส้นเดียว + ขีดปลายเฉียงสองข้าง (ตามธรรมเนียมเขียนแบบไทยที่ใช้ขีดเฉียงแทนหัวลูกศร) เมื่อองค์ประกอบเลื่อนเข้าจอ ตัวตรวจจับ (IntersectionObserver) จะเติมคลาส .in ให้ เส้นจึงยืดจากซ้ายไปขวาด้วย transform: scaleX(0→1) แล้วตัวเลขระยะค่อยปรากฏตาม — ทั้งหมดใช้เฉพาะ transform กับ opacity จึงลื่น 60fps
Technique 05Layered CSS linear-gradient
พื้นหลังตารางทุกส่วนคือการซ้อน gradient เส้นตรง 4 ชั้น: เส้นถี่ทุก 8px สองทิศ + เส้นหลักเข้มกว่าทุก 40px สองทิศ ได้กระดาษกราฟที่คมทุกขนาดจอ น้ำหนักไฟล์เป็นศูนย์ และเปลี่ยนสีได้ในบรรทัดเดียว
background-image:
linear-gradient(rgba(20,50,156,.05) 1px,transparent 1px),
linear-gradient(90deg,rgba(20,50,156,.05) 1px,transparent 1px),
linear-gradient(rgba(20,50,156,.11) 1px,transparent 1px),
linear-gradient(90deg,rgba(20,50,156,.11) 1px,transparent 1px);
background-size:8px 8px,8px 8px,40px 40px,40px 40px;
Technique 06requestAnimationFrame + pointer events
บนจอที่ใช้เมาส์ เส้นแนวนอน-แนวตั้งเต็มจอจะวิ่งตามเคอร์เซอร์ พร้อมพิกัด X·Y แบบโปรแกรมเขียนแบบ ตำแหน่งอัปเดตผ่าน requestAnimationFrame และขยับด้วย transform เท่านั้น (ไม่แตะ layout จึงไม่กระตุก) วงแหวนกลางขยายขึ้นเมื่อชี้ปุ่มหรือการ์ด บนมือถือและโหมดลดการเคลื่อนไหว ระบบนี้ปิดตัวเองอัตโนมัติ
Technique 07SVG feTurbulence data URI · mix-blend-mode
ชั้นความหยาบบาง ๆ ที่ทับทุกพื้นผิวของเว็บ ไม่ได้มาจากไฟล์รูป JPG ที่โหลดช้า แต่เป็น SVG ตัวกรอง feTurbulence (สุ่มลายจุดแบบ noise) เข้ารหัสเป็น data URI ฝังตรงใน CSS แล้ววางเป็น background-image ของชั้นทับด้วย mix-blend-mode:multiply ที่ความทึบ 5% — ได้พื้นผิวกระดาษใต้แสงจริง น้ำหนักไฟล์เป็นศูนย์ไบต์
background-image:url("data:image/svg+xml,...feTurbulence...");
mix-blend-mode:multiply; opacity:.05;
Technique 08scroll listener + rAF throttling
เส้นบางใต้ Header ค่อย ๆ เติมเต็มตามสัดส่วนที่เลื่อนอ่านทั้งหน้า ขยายภาษา "เส้นบอกระยะ" ที่เป็นจุดขายของเว็บให้ครอบคลุมทั้งประสบการณ์ ไม่ใช่แค่ส่วนบ้านไอโซเมตริก คำนวณเปอร์เซ็นต์จาก scrollY ÷ (scrollHeight − innerHeight) แล้วสั่ง scaleX — ฟัง event scroll ตัวเดียวแต่หน่วงด้วย requestAnimationFrame เพื่อไม่ให้คำนวณถี่เกินจำเป็นและไม่กระทบเฟรมเรต
ExtraAccessibility & performance
ทุกแอนิเมชันเคารพ prefers-reduced-motion (ผู้ใช้ที่ตั้งค่าลดการเคลื่อนไหวจะเห็นหน้าเว็บนิ่งครบเนื้อหา) เนื้อหาทั้งหมดอ่านได้แม้ JavaScript ล่ม เพราะสถานะ "ซ่อนก่อนเผย" จะทำงานก็ต่อเมื่อสคริปต์เติมคลาส js สำเร็จแล้วเท่านั้น ตัวหนังสือทุกจุดผ่านเกณฑ์ความต่างของสี (contrast) และปุ่มทุกปุ่มมีสถานะโฟกัสเป็นกรอบเส้นประ — เข้าธีมพิมพ์เขียวพอดี
05 · กระบวนการสร้าง ทีละขั้น
ตัดสินใจว่า "ทุกภาพต้องเป็นลายเส้น ห้ามมีรูปถ่าย" — ข้อจำกัดนี้บังคับให้ทั้งเว็บมีเอกลักษณ์เดียว
สี ระยะ ขนาดตัวอักษร เก็บใน CSS variables ทั้งหมด และใช้ clamp() ให้ตัวอักษรย่อ-ขยายตามจอเอง (fluid type scale)
9 ส่วนหลัก พร้อมราคา เบอร์โทร ที่อยู่ และคำโปรยที่เขียนเหมือนธุรกิจจริง — ไม่มีข้อความหลอก (lorem ipsum) แม้บรรทัดเดียว
รูปด้านหน้าบ้าน ผังพื้น รูปด้านอาคาร 3 โครงการ และแผนที่ — คำนวณพิกัดเส้นทีละเส้นให้สัดส่วนสมจริงแบบแบบก่อสร้าง
ขึ้นโครงกล่องทีละชั้นจาก div คำนวณมุมหลังคาจากตรีโกณ (สูง 58 ÷ ครึ่งกว้าง 110 = เอียง 27.8°) แล้วผูกกับ ScrollTrigger
เคอร์เซอร์ CAD, เส้นบอกระยะ, ตัวเลขนับขึ้น, แถบข้อความวิ่ง, สถานะ hover ของการ์ดบริการและผลงาน
รอบแรกไล่เก็บจังหวะ ระยะ คอนทราสต์ และจอมือถือ 390px รอบสองเพิ่มความลึกและลูกเล่นละเอียด รอบสามให้รีวิวเวอร์ภายนอกอ่านทวนทั้งไฟล์แล้วแก้จุดที่กระทบผู้ใช้จริง (ตัวหนังสือเล็กเกินอ่านบนมือถือ ฟอนต์ไทยผิดธรรมชาติ เมนูมือถือหาย) — บันทึกไว้ในหมวดถัดไป
06 · บันทึกการเก็บงาน (Iteration Log)
.iso-stage ขนาดคงที่ 560×560px ที่ถูกย่อด้วย transform:scale() เพื่อให้พอดีจอเล็ก — แต่ transform ไม่ลดขนาดจริงของกล่องในระบบ layout ทำให้ CSS Grid คำนวณความกว้างขั้นต่ำของคอลัมน์ผิด และดันข้อความอธิบายทั้งหมดล้นขวาออกนอกจอ อ่านไม่ได้บนมือถือ แก้ด้วยการเติม min-width:0 ให้ทุกคอลัมน์ของกริดนี้ — บั๊กหายทันที ไม่กระทบเลย์เอาต์เดสก์ท็อปwhite-space:nowrap ให้ปุ่มอยู่บรรทัดเดียวเสมอwidth:100% จริง ๆ (ไม่ใช่แค่ transform).pinpulse ที่ตั้งใจไว้แต่ไม่เคยมี CSS จริงรองรับ — เพิ่มวงแหวนเรดาร์ที่ขยายและจางหายวนซ้ำ (@keyframes pinpulse ผ่าน transform:scale() + opacity) ให้ตำแหน่งสตูดิโอบนแผนที่มีชีวิตขึ้น พร้อม fallback ปิดแอนิเมชันเมื่อผู้ใช้ตั้งค่าลดการเคลื่อนไหวfeTurbulence แบบ data URI ผสมโหมด mix-blend-mode:multiply ความทึบ 5% — ให้ความรู้สึก "กระดาษใต้กระจกโต๊ะเขียนแบบ" โดยไม่มีไฟล์ภาพเพิ่มเลยสักไบต์ และไม่กระทบการอ่านrequestAnimationFrame ผูกกับ scroll event เดียว ไม่กระทบเฟรมเรต และปิดตัวเองเมื่อผู้ใช้ลดการเคลื่อนไหวtransform/opacity/background เพื่อรักษา 60fpstransform:scale(var(--s)) ย่อทั้งฉากลงเหลือ 50% บนจอแคบกว่า 420px — ปัญหาคือป้ายกำกับตัวหนังสือทั้งหมดที่อยู่ในฉาก (เลขที่ดิน, เส้นบอกระยะ, ป้ายระดับชั้น, ตราอนุมัติแบบ) ถูกย่อไปด้วย จนเหลือแค่ ~5px อ่านไม่ออกเลยบนมือถือ ซึ่งเป็นอุปกรณ์หลักของกลุ่มเป้าหมาย แก้ด้วยการ "ถีบขนาดตัวอักษรกลับ" ทุกป้าย — ตั้ง font-size:calc(12px / var(--s)) แทนค่าคงที่ ทำให้หลังผ่าน transform ของฉากแม่แล้ว ตัวหนังสือแสดงผลจริงคงที่ที่ ~12px เสมอไม่ว่าจอจะเล็กแค่ไหน โดยรูปทรง 3 มิติของบ้านยังคงย่อ-ขยายตามเดิมทุกจุดdisplay:none ทิ้งตรง ๆ ที่จอแคบกว่า 820px โดยไม่มีอะไรมาแทน ทำให้ผู้ชมบนมือถือกระโดดไปหมวดต่าง ๆ ไม่ได้เลย เพิ่มปุ่มแฮมเบอร์เกอร์ (พร้อม aria-expanded) ที่กดแล้วเลื่อนเมนูลงมาเป็นดรอปดาวน์ พร้อม fallback ให้เมนูแสดงแบบเรียงตามปกติถ้า JavaScript ไม่ทำงานpathLength ที่ Safari รองรับตั้งแต่เวอร์ชัน 16 เท่านั้น — บนเบราว์เซอร์เก่ากว่านั้น เส้นจะกลายเป็นจุดประเล็ก ๆ กระจายทั่วภาพแทนเส้นเต็ม เปลี่ยนมาวัดความยาวเส้นจริงด้วย getTotalLength() ผ่าน JavaScript แล้วตั้งค่า dash ด้วยหน่วยพิกเซลจริงแทน ใช้ได้ทุกเบราว์เซอร์ และค่าเริ่มต้นก่อนสคริปต์ทำงาน (หรือกรณี JS ปิด) คือเส้นแสดงผลเต็มเสมอaria-hidden ให้ทุกป้ายกำกับในฉากบ้านไอโซเมตริก เพราะ .iso-viewport มี aria-label สรุปภาพไว้ให้แล้ว ป้องกันโปรแกรมอ่านหน้าจอพูดซ้ำซ้อนcursor:crosshair ของเบราว์เซอร์ เป็น cursor:none เมื่อสคริปต์ทำงานสำเร็จ ให้เคอร์เซอร์กากบาทแบบ CAD ที่วาดเองเป็นตัวชี้ตำแหน่งเพียงตัวเดียว ไม่มีเคอร์เซอร์สองแบบซ้อนกัน