Flutter Project : ทำ The Matrix Wallpaper
สวัสดีครับ ช่วงนี้เหนื่อยๆ เครียดๆ วันนี้เลยหาอะไรทำแก้เครียดเสียหน่อย ไม่รู้อะไรดลใจที่เห็นหน้าจอ Desktop ของผมที่ตั้งเป็นรูป The Matrix เอาไว้ ก็เลยได้ความคิดว่า ลองทำ The Matrix Wallpaper ใน Flutter กันดีกว่า ทำสนุกๆแบบง่ายๆ
ขอแปะรูป Desktop บันทึกไว้หน่อย
เริ่มต้น
เริ่มต้นจากแสดง Text เป็นแบบตาราง แสดงให้เต็มจอ โดยลอง fix เลข row , col ไปก่อน
ใส่พื้นหลังสีดำ ตัวหนังสือสีเขียว
class _MyMatrixPageState extends State<MyMatrixPage> { static const int ROW = 54; static const int COL = 32; @override Widget build(BuildContext context) { return Scaffold( body: Container(color: Colors.black, child: buildMatrixTable()) ); } Column buildMatrixTable() { List<Widget> listRow = List(); for (int row = 0; row < ROW; row++) { List<Widget> listCol = List(); for (int col = 0; col < COL; col++) { listCol.add(Container(width: 12, height: 12, child: Text("A", style: TextStyle(color: Color(0xff3b6d43))))); } listRow.add(Row(children: listCol)); } return Column(children: listRow); } }
จะได้แบบนี้
สร้างคลาสสำหรับเก็บค่าแต่ละช่อง ในที่นี้ผมใช้ชื่อว่า Pixel ก็เก็บแค่ตัวหนังสือในช่อง
class Pixel { String data; Pixel({this.data = ""}); }
ประกาศให้ Pixel เป็น array 2 มิติ ชื่อ matrixTable
class _MyMatrixPageState extends State<MyMatrixPage> { static const int ROW = 54; static const int COL = 32; List<List<Pixel>> matrixTable;
ทำการ initMatrixTable ก็คือสร้าง instance ให้กับตารางให้ครบตาม row , col
แล้วก็ผมเพิ่ม method การสุ่มค่าด้วย แล้วกำหนดค่าให้ Pixel
initState() { initMatrixTable(); super.initState(); } String randomData() { const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; Random rand = Random(); return chars[rand.nextInt(chars.length)]; } void initMatrixTable() { matrixTable = List(); for (int row = 0; row < ROW; row++) { List<Pixel> list = List(); for (int col = 0; col < COL; col++) { list.add(Pixel(data: randomData())); } matrixTable.add(list); } }
จากนั้นที่ build() ก็กำหนด matrixTable ให้กับ Text
Column buildMatrixTable() { ... child: Text(matrixTable[row][col].data, ... }
ได้ผลลัพธ์เป็นแบบนี้
ทำให้เคลื่อนไหว
ต่อมาทำส่วนให้ มีแถวเคลื่อนที่ลงมาจากข้างบนลงด้านล่าง
สร้างคลาส Line โดยจะเก็บตำแหน่งของหัวเอาไว้ โดยหัวจะอยู่ด้านล่างนะ ก็คือ index ของ row จะ +1 เรื่อยๆ จนสิ้นสุด
class Line { int rowHead; int colHead; int size; Line({this.rowHead = 0, this.colHead = 0, this.size = 10}); moveDown() { rowHead = rowHead + 1; } }
จากนั้นเพิ่ม Timer ให้ทำงานวนไปตามเวลาที่กำหนด ผมลองกำหนด 500 ms ก่อน แล้วก็เพิ่ม ตัวแปร Line
ตอนนี้ราจะลองก่อน 1 แถว โดยทุกๆเวลาที่กำหนดจะรัน method process() ซึ่งจะทำการขยับแถวลงมา พร้อมสุ่มค่าใหม่ให้กับ Pixel
class _MyMatrixPageState extends State<MyMatrixPage> { ... Timer timer; Line line; initState() { initMatrixTable(); line = Line(rowHead: 10, colHead: 10, size: 10); timer = Timer.periodic( Duration(milliseconds: 500), (Timer t) => process()); super.initState(); } process() { setState(() { initMatrixTable(); addLineToMatrixTable(matrixTable, line); line.moveDown(); }); } void addLineToMatrixTable(List<List<Pixel>> matrixTable, Line line) { for (int i = 0; i < line.size; i++) { int row = line.rowHead - i; if (row >= 0) { matrixTable[row][line.colHead].data = randomData(); } } } void initMatrixTable() { matrixTable = List(); for (int row = 0; row < ROW; row++) { List<Pixel> list = List(); for (int col = 0; col < COL; col++) { list.add(Pixel()); } matrixTable.add(list); } } String randomData() { const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; Random rand = Random(); return chars[rand.nextInt(chars.length)]; }
ลองรัน แถวเลื่อนลงมาแล้ว
เพิ่มแถว
เพิ่มแถวให้มากขึ้น โดยเปลี่ยนเป็น List<Line> แทน
class _MyMatrixPageState extends State<MyMatrixPage> { ... List<Line> listLine;
เพิ่มการ initLine โดยวนลูปเพิ่ม Line เข้าไปใน list พร้อมสุ่มตำแหน่ง และขนาด
initState() { ... initLine(); ... super.initState(); } void initLine() { listLine = List(); for (int i = 0; i < 30; i++) { listLine.add(Line( rowHead: randomNumber(max: ROW), colHead: randomNumber(max: COL), size: randomNumber(min: 10, max: 20))); } } int randomNumber({int min = 0, int max = 10}) { Random rand = Random(); return rand.nextInt(max) + min; }
process() { setState(() { initMatrixTable(); for (Line line in listLine) { addLineToMatrixTable(matrixTable, line); line.moveDown(); } }); }
ดูเหมือน Matrix มากขึ้นอีกนิด
เพิ่มเงา
Matrix เรายังไม่เนียนเท่าไหร่ เพราะเรากำหนดสีตัว Text ตายตัว ดังนั้นน่าจะปรับสีไล่เฉดสักหน่อย
เพิ่มตัวแปร color ให้ Pixel
class Pixel { String data; Color color; Pixel({this.data = "", this.color = Colors.black}); }
กำหนดสีในแต่ละ Index ของแถวไล่สีจากเข้มไปจาง
โดยใน Flutter จะกำหนด Alpha ได้จากค่าหลังตัว x เช่น 0xAARRGGBB
class _MyMatrixPageState extends State<MyMatrixPage> { ... List<Color> color = [ Color(0xff3b6d43), Color(0xff3b6d43), Color(0xff3b6d43), Color(0xff3b6d43), Color(0xee3b6d43), Color(0xdd3b6d43), Color(0xcc3b6d43), Color(0xbb3b6d43), Color(0xaa3b6d43), Color(0x993b6d43), Color(0x883b6d43), Color(0x773b6d43), Color(0x663b6d43), Color(0x553b6d43), Color(0x443b6d43), Color(0x333b6d43), Color(0x223b6d43) ];
ปรับให้ตอน addLineToMatrixTable เพิ่มสีลงไปด้วย
void addLineToMatrixTable(List<List<Pixel>> matrixTable, Line line) { for (int i = 0; i < line.size; i++) { int row = line.rowHead - i; if (row >= 0 && row < ROW) { matrixTable[row][line.colHead].data = randomData(); // Add if(i < color.length) { matrixTable[row][line.colHead].color = color[i]; }else{ matrixTable[row][line.colHead].color = color.last; } } } }
จากนั้น ก็เอาสีของ matrixTable ไปใส่ Text ด้วย
Column buildMatrixTable() { ... child: Text(matrixTable[row][col].data, style: TextStyle(color: matrixTable[row][col].color)))); ... }
มาแล้ว Matrix
เริ่มแถวใหม่ เมื่อจบ
ตอนนี้แถวเมื่อเลื่อนมาจนสุดก็จะหายไปเลย เพราะ index มันเลยค่าของ ROW
ดังนั้นเรามาปรับให้เมื่อแถวเลื่อนลงมาจนสุดก็ ลบแถวนั้นแล้วสุ่มแถวใหม่ลงมาใน list แทน
แค่นี้ Matrix เราจะรันไปเรื่อยๆแล้ว
process() { setState(() { initMatrixTable(); for (Line line in listLine) { addLineToMatrixTable(matrixTable, line); line.moveDown(); if (line.rowHead - line.size >= ROW) { listLine.remove(line); listLine.add(randomLine(maxRow: 1)); } } }); } Line randomLine({int maxRow = ROW, int maxCol = COL}) { return Line( rowHead: randomNumber(max: maxRow), colHead: randomNumber(max: maxCol), size: randomNumber(min: 10, max: 20)); }
เพิ่อความสวยงาม ผมชอบสีสดๆ เลยขอปรับเป็นสีสดๆขึ้นหน่อย
List<Color> color = [ Color(0xff10d630), Color(0xff10d630), Color(0xff10d630), Color(0xff10d630), Color(0xee10d630), Color(0xdd10d630), Color(0xcc10d630), Color(0xbb10d630), Color(0xaa10d630), Color(0x9910d630), Color(0x8810d630), Color(0x7710d630), Color(0x6610d630), Color(0x5510d630), Color(0x4410d630), Color(0x3310d630), Color(0x2210d630) ];
ได้แบบนี้
ทำให้รองรับทุกขนาดหน้าจอ
ตอนนี้เรากำหนด row ,col ตายตัว ทำให้เมื่อไปรันบนแท็บเลต มันเลยแสดงไม่ถูกต้อง
ปรับให้ ค่า ROW ,COL มาจากขนาดความกว้าง สูง ของจอ
โดยหารกับ 12 เพราะ ช่องนึงมัน 12×12
~/ คือ การหารแล้ว cast เป็น int
Widget buildMatrixTable() { ROW = MediaQuery .of(context) .size .height ~/ 12; COL = MediaQuery .of(context) .size .width ~/ 12; if (matrixTable != null) { ... return Column(children: listRow); } else { return Container(); } }
ปัญหาต่อมาคือ ในเมื่อเราไม่รู้ row ,col ทำให้ไม่สามารถ initMatrixTable ใน initState ได้
initState() { timer = Timer.periodic( Duration(milliseconds: 250), (Timer t) => process()); super.initState(); }
ให้มาทำใน process() แทน
process() { if (matrixTable == null ) { initMatrixTable(); initLine(); } ... }
มาแล้ว แต่ดูมันโล่งๆ เพราะเรากำหนดจำนวน Line ตายตัว ตอน initLine
ดังนั้น น่าจะปรับจำนวนตาม ความกว้างของเจอ สัก 2 เท่าของ COL กำลังดี
void initLine() { listLine = List(); for (int i = 0; i < COL * 2; i++) { listLine.add(randomLine(maxRow: ROW, maxCol: COL)); } }
ทำให้เลื่อนไม่เท่ากัน
เพิ่มเติมอีกนิด ตอนนี้แถวทุกแถวมันเลื่อนในอัตราที่เท่ากัน ก็ปรับให้มีการสุ่มการเลื่อน
20% เลื่อน 2 ช่อง
60 % เลื่อน 1 ช่อง
20% ไม่เลื่อน
process() { ... setState(() { initMatrixTable(); for (Line line in listLine) { addLineToMatrixTable(matrixTable, line); Random random = Random(); int number = random.nextInt(100); if (number < 50) { line.moveDown(move: 2); } else if (number < 80) { line.moveDown(); } else { // } ... }
แถวมันเลื่อนไม่เท่ากันก็จริง แต่อัตราการอัพเดทแถวยังเท่ากัน ดังนั้นเลยปรับให้มี process เพื่มอีกตัว ให้เวลามันเหลื่อมๆกันนิดหน่อย
initState() { timer = Timer.periodic(Duration(milliseconds: 500), (Timer t) => process()); timer2 = Timer.periodic(Duration(milliseconds: 300), (Timer t) => process()); super.initState(); }
จบแล้ว
บล็อกนี้เป็นโปรเจคสนุกๆ เล็กๆ ง่ายๆ เกี่ยวกับการสร้าง Matrix Wallpaper ครับ จริงๆแล้ววิธีการนี้น่าจะยังไม่ดีเท่าไหร่ เพราะต้องวนลูปจำนวนมาก แล้วก็จริงๆอยากจะทำ matrixtable ซ้อนไปด้านหลังอีกชั้น แล้วเอา Stack ครอบ จะได้เหมือน 3 มิติ มากขึ้น แต่ว่าดูเหมือนคอมผมจะเอาไม่ไหวแล้ว พอเท่านี้ก่อนละกัน หวังว่าจะเป็นไอเดียให้กับผู้อ่านครับ (:
โค้ดอยู่ที่ Github ครับ
https://github.com/benznest/the-matrix-wallpaper-flutter