บทความ
เอา Design Token มาใช้กับ Native iOS กันเถอะ: ตอนที่ 2
จากตอนที่แล้ว เอา Design Token มาใช้กับ Native iOS กันเถอะ: ตอนที่ 1 เราได้ทำการ Generate ไฟล์ swift จาก Design Token แบบง่าย ๆไปแล้ว ครั้งนี้เราจะมาเพิ่มความท้าทายขึ้นไปอีก ด้วยการเข้ามาเล่นกับ Gradient ที่เรา Parse ได้มาจาก Design Token
จาก Design Token เราจะเห็นว่าค่าของมันถูกส่งมาเป็นค่าของ CSS gradient ซึ่งเราเอามาใช้กับ iOS ไม่ได้
เราจึงต้องทำการ Extract ค่าของ linear-gradient(180deg, #adee2b 0%, #ffff19 100%) ออกมา เราจะได้ค่ามาดังนี้ 180deg, #adee2b, 0%, #ffff19, 100% เราก็จะสามารถสรุปข้อมูลได้ว่า
สี #adee2b เป็นสีเริ่มต้นที่ตำแหน่ง 0%
สี #ffff19 เป็นสีสิ้นสุดที่ตำแหน่ง 100%
Gradient ทำมุม 180 องศา
ทีนี้เรามาดูทางฝั่ง Swift กันบ้างว่าการจะทำ Gradient ต้องใช้อะไร
จะพบว่าสิ่งที่เราเอามาใช้ได้คือสี (.colors) และตำแหน่งของสี (.locations) เท่านั้น ส่วน startPoint และ endPoint เราไม่สามารถนำค่ามาใส่ได้ เราจึงต้องทำการเปลี่ยนค่าจากมุมองศาเป็นพิกัด 2 มิติก่อน และนี่ก็คือเหตุผลว่าทำไมเราต้องเรียนวิชาคณิตศาสตร์ครับ
เชื่อว่าเราทุกคนต้องเคยได้เรียนวงกลม 1 หน่วยกันมาบ้าง ถ้าเราสังเกตสักนิด เราจะพบว่าตัว startPoint และ endPoint เนี่ย มันก็คือ Cartesian coordinate ดี ๆนี่เอง ถ้าแปลงกันแบบง่าย ๆเลย เราจะได้ค่ามาแบบนี้
x = r * cos(θ); y = r * sin(θ) วงกลม 1 หน่วย r จะเท่ากับ 1
0° = (cos(0), sin(0)) = (1,0)
ได้ startPoint = (0,0), endPoint = (1,0)
90° = (cos(90), sin(90) = (0,1)
ได้ startPoint = (0,0), endPoint = (0,1)
180° = (cos(180), sin(180) = (-1,0)
ได้ startPoint = (0,0), endPoint = (-1,0)
270° = (cos(270), sin(270) = (0,-1)
ได้ startPoint = (0,0), endPoint = (0,-1)
แต่ว่าในการใช้งาน Gradient จริง ๆบน UIView ขนาด 1x1 เส้น Gradient แบบ 0° ส่วนใหญ่เราจะลากเส้นกันที่กึ่งกลางของ View ดังนั้นจุดเริ่มมันต้องไม่ใช่ (0,0) แน่นอน แต่จะเป็น (0,0.5) และจุดจบมันจะไปอยู่ที่ (1, 0.5) แต่ถ้าเป็นมุม 45° มันจะเป็นเส้นทแยงมุมจาก (0,0) ไปยัง (1,1) แทน ตามภาพ
กรณีที่เป็นมุมที่หาร 45 ลงตัวนี่มันจะง่ายครับเพราะค่า sin, cos มันจะมีแค่ 0 กับ 1 เราแค่จับมา x 0.5 แล้ว + 0.5 อีกทีมันก็จะ Transpose ตำแหน่งไปยังที่ ๆควรจะอยู่ครับ เช่นมุม 0° ถ้าเราหาจาก (cos(0),sin(0)) มันจะได้ (0,0) -> (1,0) เราก็ Transpose แกน y ขึ้นไปครับ จะได้
(0, 0.5 * 0 + 0.5) -> (1, 0.5 * 0 + 0.5) = (0,0.5) -> (1,0.5)
หรือในมุม 45° ก็จะได้
(0, 0.5 * 0 + 0.5) -> (1, 0.5 * 1 + 0.5) = (0,0) -> (1, 1)
แต่ถ้าเป็นมุมที่มากกว่า 45° เราจะต้องมา Transpose แกน x แทน เช่นมุม 90° เพื่อให้เส้นของ Gradient อยู่ในตำแหน่งที่ถูกต้อง
(0.5 * 0 + 0.5, 0) -> (0.5 * 0+ 0.5, 1) = (0.5,0) -> (0.5, 1)
และมีสิ่งที่เราต้องคำนึงเพิ่มเติมคือในกรณีที่มุมมันไม่ใช่ 0°, 45°, 90° มันจะไม่ได้ค่าเป็น 0,1 อีก (จริงๆมุม 45° sin กับ cos บนวงกลม 1 หน่วยมันจะได้ sqrt(2)/2 นะครับ แต่เราอนุมานได้ว่าเวลามันวางอยู่บน gradient space ถ้าลากจาก 0,0 มันจะไปสุดทางในกรอบสี่เหลี่ยมที่ 1,1) เช่น Gradient ที่ทำมุม 10° แบบนี้
เราทราบค่า ab ว่าเป็น 1 (ในวงกลม 1 หน่วย) นั่นแปลว่าเรารู้แล้วว่า x ของจุด c เป็น 1 แต่เราไม่รู้ค่า y ของจุด c
เราสามารถหาตำแหน่งของ y ได้จากความยาว bc ถ้าเราพอจำสูตรได้ เราสามารถหาความยาวด้านตรงข้ามมุมฉากได้จาก sqrt(ab² + bc²) แต่เราก็ไม่รู้ความยาวของ ac อยู่ดี ถ้าเป็นแบบนี้เราหาความยาวของ bc ได้จากที่ไหน คำตอบอยู่ในวิชาคณิตฯม.ปลายครับ วิชาตรีโกณมิตินั่นเอง bc/ab = tan(θ) และเมื่อเรารู้ค่าของ ab อยู่แล้วว่าเท่ากับ 1 เมื่อแทนค่าลงไปจะได้ความยาว bc เป็น bc = tan(10) แต่ว่ามันยังใช้ไม่ได้ครับ เพราะ tan มาจาก sin/cos ถ้าเป็น tan(90) ที่มาจาก sin(90)/cos(90) มันจะได้ค่าเป็น 1/0 หรือ divided by zero ครับโค้ดพังแน่นอน เราจึงควรเปลี่ยนมุมจาก Degree เป็น Radian ซะก่อน โดยที่เราสามารถแปลงกลับไปกลับมาระหว่าง Degree กับ Radian ได้ด้วยสูตร
1° = π / 180rad ↔ 1rad = 180° / π
ดังนั้นค่า bc จะเท่ากับ tan(10 * π / 180 ) นั่นเอง และเมื่อเราต้อง Transpose มัน เราก็จะได้เป็น ตำแหน่งจุด a ที่ (0,0.5 * 0 + 0.5) และตำแหน่งจุด c ที่ (1, 0.5 * tan(10 * π / 180 ) + 0.5) จะได้ค่าประมาณ (0,0.5) -> (1, 0.588) นั่นเอง
ทำไปทำมาปรากฏว่าเคยมีคนโพสต์ตารางคำนวณกับโค้ดไว้ใน StackOverflow ให้แล้วซะงั้น 🥲 ถือว่าดีครับจะได้ไม่ต้องงมเองมาก จากนี้ก็ขอยกตารางของเขามาอธิบายเลยดีกว่า
จากตารางจะเห็นว่าเขาใช้จุดเริ่ม a (θ) เป็นด้านตรงข้ามกับจุดสิ้นสุด b (-θ)
และถ้าดูจาก Code ที่เขาเขียนมาจะเห็นว่าใน Statement แรกเขาได้ทำการกลับด้านมุมก่อนแล้วแปลงให้เป็นมุมที่อยู่ตรงข้ามกัน
var ang = (-angle).truncatingRemainder(dividingBy: 360)
ถ้าเราใส่มุม 45° มันจะได้ 315° (315–360 = -45°) และถ้าเราใส่ -45° จะได้ 45° (-45° คือ 315°)
ใน Statement if ang < 0 { ang = 360 + ang } ก็คือถ้าหากมุมมันติดลบ จะหมายความว่ามุมมันหมุนทวนเข็มนาฬิกาจึงสามารถนำไปหักกับ 360 ได้เลย
เมื่อ Transpose แล้วหากนำจุด a และ b มาต่อกันน่าจะได้ภาพประมาณนี้ โดยที่ b = (1,0.5) และ b’ = (0, 0.5)
ใน Statement let a = CGPoint(x: n * tanx(ang — 90) + n, y: 1) เราจะเห็นว่าเขาได้นำมุม 90° มาลบ สาเหตุที่ต้องทำแบบนั้นคือเขาได้ทำการเปลี่ยนจากการ Transpose แกน y ไปเป็น แกน x ซึ่งมันจะต้องกลับด้านตัวสามเหลี่ยมมุมฉากนั่นเอง
เมื่อเราได้สูตรและทำความเข้าใจกับมันแล้วค่อยเอามาใช้งานจริงครับ อนึ่งเราควรสร้างนิสัยที่ดีในการใช้ StackOverflow เราต้องเข้าใจก่อนว่าโค้ดที่เราจะเอามาใช้มันทำงานยังไง อย่าเอาแต่ copy & paste ในโค้ดเราอย่างเดียวจนมันกลายเป็นแฟรงเกนสไตน์ครับ
มาถึงโค้ดของเรากันบ้าง ให้เราเตรียม Struct ไว้รองรับค่าที่เราจะ Generate ขึ้นมาก่อน
แล้วเราก็มาจัดการกับตัว Formatter ของ StyleDictionary ครับ
จะเห็นว่ามันเหมือนกับโค้ด Depth First Search ใน ตอนที่ 1 เลย ใช่ครับมันคือโค้ดอันเดิมนี่แหละ แต่ได้ทำการเพิ่มให้มันแปลง Gradient ให้ด้วย เพราะดีไซเนอร์ของเราส่งค่า Gradient เข้ามาในคีย์ของ color นั่นเอง
ให้เราสังเกตตั้งแต่บรรทัดที่ 10 เป็นต้นไปครับ มันคือการตัด String ไปเป็น Array แบบธรรมดา ๆเลย แล้วเราก็นำเอาค่าที่ได้ไปต่อเป็น String เพื่อเขียนลงไปในไฟล์ Swift ครับ โดยที่เราสร้าง Gradient ออกมาเป็น Struct เลย
ทั้งนี้ถ้าเป็นที่อื่นอาจจะเจอไม่เหมือนกันนะครับ หากโชคดีมันมี Token สำหรับ iOS โดยเฉพาะเค้าก็จะส่งมาเป็นค่าที่พร้อมใช้งานโดยที่ไม่ต้องมาแปลงเองตามเรื่องตรีโกณมิติที่เขียนไว้ข้างบนเลยครับ แต่พอดีของผมไปเอาของ Web Front end มาใช้ก็เลยต้องเล่นท่ายากนิดหน่อย
กลับมาที่ XCode เราจะสร้าง Extension ของ UIView เพื่อให้ทุก ๆ Class ที่ extend UIView (เช่น UIButton) สามารถใส่ Gradient ได้
ทีนี้พอเราต้องการใช้ Gradient กับ View ใด ๆเราก็สามารถนำมาใช้ได้ทันทีเลยแบบนี้
เสร็จแล้วเราจะได้ Gradient View ตามที่กำหนดมาใน Design System ครับ
ก็จบไปแล้วสำหรับตอนที่ 2 ตอนต่อไปเราจะมาทำ Shadow จาก Design Token กันครับ รอบหน้าน่าจะไม่ยาวเท่านี้แล้ว ยังไงก็ฝากติดตามตอนต่อไป เอา Design Token มาใช้กับ Native iOS กันเถอะ: ตอนที่ 3 ด้วยครับ
ที่มา:
Medium