From c6472f9662de2fa9f2bdbca8182a4e0ffe06a966 Mon Sep 17 00:00:00 2001 From: Florian Blasius Date: Tue, 28 Mar 2023 12:10:47 +0000 Subject: [PATCH] add js memory tutorial (#2421) --- .github/workflows/build_docs.yaml | 6 +- README.md | 2 +- docs/site/index.html | 2 + docs/tutorial/node/book.toml | 12 +++ docs/tutorial/node/src/SUMMARY.md | 11 +++ docs/tutorial/node/src/conclusion.md | 11 +++ .../node/src/creating_the_tiles_from_js.md | 19 ++++ .../node/src/from_one_to_multiple_tiles.md | 33 +++++++ docs/tutorial/node/src/game_logic_in_js.md | 38 ++++++++ docs/tutorial/node/src/getting_started.md | 46 +++++++++ docs/tutorial/node/src/icons | 1 + .../tutorial/node/src/ideas_for_the_reader.md | 14 +++ docs/tutorial/node/src/introduction.md | 14 +++ docs/tutorial/node/src/main_game_logic.js | 66 +++++++++++++ docs/tutorial/node/src/main_initial.js | 11 +++ docs/tutorial/node/src/main_tiles_from_js.js | 23 +++++ docs/tutorial/node/src/memory.slint | 12 +++ .../tutorial/node/src/memory_game_logic.slint | 93 +++++++++++++++++++ docs/tutorial/node/src/memory_tile.md | 42 +++++++++ docs/tutorial/node/src/memory_tile.slint | 20 ++++ .../node/src/memory_tiles_from_cpp.slint | 80 ++++++++++++++++ docs/tutorial/node/src/package.json | 11 +++ docs/tutorial/node/src/polishing_the_tile.md | 32 +++++++ 23 files changed, 597 insertions(+), 2 deletions(-) create mode 100644 docs/tutorial/node/book.toml create mode 100644 docs/tutorial/node/src/SUMMARY.md create mode 100644 docs/tutorial/node/src/conclusion.md create mode 100644 docs/tutorial/node/src/creating_the_tiles_from_js.md create mode 100644 docs/tutorial/node/src/from_one_to_multiple_tiles.md create mode 100644 docs/tutorial/node/src/game_logic_in_js.md create mode 100644 docs/tutorial/node/src/getting_started.md create mode 120000 docs/tutorial/node/src/icons create mode 100644 docs/tutorial/node/src/ideas_for_the_reader.md create mode 100644 docs/tutorial/node/src/introduction.md create mode 100644 docs/tutorial/node/src/main_game_logic.js create mode 100644 docs/tutorial/node/src/main_initial.js create mode 100644 docs/tutorial/node/src/main_tiles_from_js.js create mode 100644 docs/tutorial/node/src/memory.slint create mode 100644 docs/tutorial/node/src/memory_game_logic.slint create mode 100644 docs/tutorial/node/src/memory_tile.md create mode 100644 docs/tutorial/node/src/memory_tile.slint create mode 100644 docs/tutorial/node/src/memory_tiles_from_cpp.slint create mode 100644 docs/tutorial/node/src/package.json create mode 100644 docs/tutorial/node/src/polishing_the_tile.md diff --git a/.github/workflows/build_docs.yaml b/.github/workflows/build_docs.yaml index 21413079fa3..9c2e1c2b93d 100644 --- a/.github/workflows/build_docs.yaml +++ b/.github/workflows/build_docs.yaml @@ -83,6 +83,9 @@ jobs: - name: "C++ Tutorial Docs" run: mdbook build working-directory: docs/tutorial/cpp + - name: "NodeJS Tutorial Docs" + run: mdbook build + working-directory: docs/tutorial/node - name: "Slint Language Documentation" run: cargo xtask slintdocs --show-warnings - name: "Node docs" @@ -102,8 +105,9 @@ jobs: docs/site docs/tutorial/rust/book/html docs/tutorial/cpp/book/html + docs/tutorial/node/book/html - name: "Check for docs warnings in internal crates" run: cargo +nightly doc --workspace --no-deps --all-features --exclude slint-node --exclude mcu-board-support --exclude printerdemo_mcu --exclude carousel --exclude test-* --exclude plotter - name: Clean cache # Don't cache docs to avoid them including removed classes being published run: | - rm -rf target/doc target/cppdocs target/slintdocs api/node/docs docs/tutorial/rust/book docs/tutorial/cpp/book + rm -rf target/doc target/cppdocs target/slintdocs api/node/docs docs/tutorial/rust/book docs/tutorial/cpp/book docs/tutorial/node/book diff --git a/README.md b/README.md index f1d20e1673e..7e5d4756a3f 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ Refer to the README of each language directory in the `api` folder: - [C++](api/cpp) ([Documentation](https://slint-ui.com/docs/cpp) | [Tutorial](https://slint-ui.com/docs/tutorial/cpp) | [Tutorial Video](https://youtu.be/_-Hxr6ZrHyo) | [Getting Started Template](https://github.com/slint-ui/slint-cpp-template)) - [Rust](api/rs/slint) [![Crates.io](https://img.shields.io/crates/v/slint)](https://crates.io/crates/slint) ([Documentation](https://slint-ui.com/docs/rust/slint/) | [Tutorial](https://slint-ui.com/docs/tutorial/rust) | [Tutorial Video](https://youtu.be/_-Hxr6ZrHyo) | [Getting Started Template](https://github.com/slint-ui/slint-rust-template)) -- [JavaScript/NodeJS (Beta)](api/node) [![npm](https://img.shields.io/npm/v/slint-ui)](https://www.npmjs.com/package/sixtyfps) ([Documentation](https://slint-ui.com/docs/node)) +- [JavaScript/NodeJS (Beta)](api/node) [![npm](https://img.shields.io/npm/v/slint-ui)](https://www.npmjs.com/package/sixtyfps) ([Documentation](https://slint-ui.com/docs/node) | [Tutorial](https://slint-ui.com/docs/tutorial/node) | [Getting Started Template](https://github.com/slint-ui/slint-nodejs-template)) ## Demos diff --git a/docs/site/index.html b/docs/site/index.html index da76ebda76d..469b1793c30 100644 --- a/docs/site/index.html +++ b/docs/site/index.html @@ -550,6 +550,8 @@

Documentation

/> Using Slint in a NodeJS Project (Beta) NodeJS Documentation +
+ NodeJS Tutorial diff --git a/docs/tutorial/node/book.toml b/docs/tutorial/node/book.toml new file mode 100644 index 00000000000..a3b627bf15e --- /dev/null +++ b/docs/tutorial/node/book.toml @@ -0,0 +1,12 @@ +[book] +authors = ["Slint Developers "] +language = "en" +multilingual = false +src = "src" +title = "Slint Memory Game Tutorial (node)" + +[output.html] +theme = "../theme" + +[output.linkcheck] # enable the "mdbook-linkcheck" renderer +optional = true diff --git a/docs/tutorial/node/src/SUMMARY.md b/docs/tutorial/node/src/SUMMARY.md new file mode 100644 index 00000000000..05c2de2e3c3 --- /dev/null +++ b/docs/tutorial/node/src/SUMMARY.md @@ -0,0 +1,11 @@ +# Summary + +- [Introduction](./introduction.md) +- [Getting Started](./getting_started.md) +- [Memory Tile](./memory_tile.md) +- [Polishing the Tile](./polishing_the_tile.md) +- [From One To Multiple Tiles](./from_one_to_multiple_tiles.md) +- [Creating The Tiles From JavaScript](./creating_the_tiles_from_js.md) +- [Game Logic In JavaScript](./game_logic_in_js.md) +- [Ideas For The Reader](./ideas_for_the_reader.md) +- [Conclusion](./conclusion.md) diff --git a/docs/tutorial/node/src/conclusion.md b/docs/tutorial/node/src/conclusion.md new file mode 100644 index 00000000000..1b490b3cf79 --- /dev/null +++ b/docs/tutorial/node/src/conclusion.md @@ -0,0 +1,11 @@ +# Conclusion + +In this tutorial, we have demonstrated how to combine some built-in Slint elements with JavaScript code to build a little +game. There are many more features that we haven't talked about, such as layouts, widgets, or styling. + +We recommend the following links to continue: + + * [Examples](https://github.com/slint-ui/slint/tree/master/examples): In the Slint repository we have collected a few demos and examples. These are a great starting point to learn how to use many Slint features. + * [Todo Example](https://github.com/slint-ui/slint/tree/master/examples/todo): This is one of the examples that implements a classic use-case. + * [Memory Puzzle](https://github.com/slint-ui/slint/tree/master/examples/memory): This is a slightly more polished version of the code in this example. And you can play the wasm version in your browser. + * [Slint API Docs](https://slint-ui.com/docs/node/): The reference documentation for the NodeJS library. diff --git a/docs/tutorial/node/src/creating_the_tiles_from_js.md b/docs/tutorial/node/src/creating_the_tiles_from_js.md new file mode 100644 index 00000000000..2dfa8e90053 --- /dev/null +++ b/docs/tutorial/node/src/creating_the_tiles_from_js.md @@ -0,0 +1,19 @@ +# Creating The Tiles From JavaScript + +What we'll do is take the list of tiles declared in the .slint language, duplicate it, and shuffle it. +We'll do so by accessing the `memory_tiles` property through the JavaScript code - in our case `memory_tiles`. +Since `memory_tiles` is an array in the `.slint` language, it's represented as a JavaScript [`Array`](https://slint-ui.com/releases/0.3.5/docs/node/). +We can't modify the model generated by the .slint, but we can extract the tiles from it, and put it +in a [`slint.ArrayModel`](https://slint-ui.com/releases/0.3.5/docs/node/classes/arraymodel.html) which implements the [`Model`](https://slint-ui.com/releases/0.3.5/docs/node/interfaces/model.html) interface. +`ArrayModel` allows us to make modifications and we can use it to replace the static generated model. + +We modify the main function like so: + +```js +{{#include main_tiles_from_js.js:main}} +``` + +Running this gives us a window on the screen that now shows a 4 by 4 grid of rectangles, which can show or obscure +the icons when clicking. There's only one last aspect missing now, the rules for the game. + + diff --git a/docs/tutorial/node/src/from_one_to_multiple_tiles.md b/docs/tutorial/node/src/from_one_to_multiple_tiles.md new file mode 100644 index 00000000000..55d20044527 --- /dev/null +++ b/docs/tutorial/node/src/from_one_to_multiple_tiles.md @@ -0,0 +1,33 @@ +# From One To Multiple Tiles + +After modeling a single tile, let's create a grid of them. For the grid to be our game board, we need two features: + +1. A data model: This shall be an array where each element describes the tile data structure, such as the + url of the image, whether the image shall be visible and if this tile has been solved. We modify the model + from JS code. +2. A way of creating many instances of the tiles, with the above `.slint` markup code. + +In Slint we can declare an array of structures using brackets, to create a model. We can use the for loop +to create many instances of the same element. In `.slint` the for loop is declarative and automatically updates when +the model changes. We instantiate all the different MemoryTile elements and place them on a grid based on their +index with a little bit of spacing between the tiles. + +First, we copy the tile data structure definition and paste it at top inside the `memory.slint` file: + +```slint +{{#include ../../rust/src/main_multiple_tiles.rs:tile_data}} +``` + +Next, we replace the _export component MainWindow inherits Window { ... }_ section at the bottom of the `memory.slint` file with the following snippet: + +```slint +{{#include ../../rust/src/main_multiple_tiles.rs:main_window}} +``` + +The for tile\[i\] in memory_tiles: syntax declares a variable `tile` which contains the data of one element from the `memory_tiles` array, +and a variable `i` which is the index of the tile. We use the `i` index to calculate the position of tile based on its row and column, +using the modulo and integer division to create a 4 by 4 grid. + +Running this gives us a window that shows 8 tiles, which can be opened individually. + + diff --git a/docs/tutorial/node/src/game_logic_in_js.md b/docs/tutorial/node/src/game_logic_in_js.md new file mode 100644 index 00000000000..e688c7e150a --- /dev/null +++ b/docs/tutorial/node/src/game_logic_in_js.md @@ -0,0 +1,38 @@ +# Game Logic In JavaScript + +We'll implement the rules of the game in JavaScript as well. The general philosophy of Slint is that merely the user +interface is implemented in the `.slint` language and the business logic in your favorite programming +language. The game rules shall enforce that at most two tiles have their curtain open. If the tiles match, then we +consider them solved and they remain open. Otherwise we wait for a little while, so the player can memorize +the location of the icons, and then close them again. + +We'll modify the `.slint` markup in the `memory.slint` file to signal to the JavaScript code when the user clicks on a tile. +Two changes to MainWindow are needed: We need to add a way for the MainWindow to call to the JavaScript code that it should +check if a pair of tiles has been solved. And we need to add a property that JavaScript code can toggle to disable further +tile interaction, to prevent the player from opening more tiles than allowed. No cheating allowed! First, we paste +the callback and property declarations into MainWindow: + +```slint +{{#include ../../rust/src/main_game_logic_in_rust.rs:mainwindow_interface}} +``` + +The last change to the `.slint` markup is to act when the MemoryTile signals that it was clicked on. +We add the following handler in MainWindow: + +```slint +{{#include ../../rust/src/main_game_logic_in_rust.rs:tile_click_logic}} +``` + +On the JavaScript side, we can now add an handler to the `check_if_pair_solved` callback, that will check if +two tiles are opened. If they match, the `solved` property is set to true in the model. If they don't +match, start a timer that will close them after one second. While the timer is running, we disable every tile so +one can't click anything during this time. + +Insert this code before the `mainWindow.run()` call: + +```js +{{#include main_game_logic.js:game_logic}} +``` + +These were the last changes and running the result gives us a window on the screen that allows us +to play the game by the rules. diff --git a/docs/tutorial/node/src/getting_started.md b/docs/tutorial/node/src/getting_started.md new file mode 100644 index 00000000000..a842c6edb91 --- /dev/null +++ b/docs/tutorial/node/src/getting_started.md @@ -0,0 +1,46 @@ +# Getting Started + +In this tutorial, we use JavaScript as the host programming language. We also support other programming languages like +[Rust](https://slint-ui.com/docs/rust/slint/) or [C++](https://slint-ui.com/docs/cpp/). + +You'll need a development environment with [Node.js 16](https://nodejs.org/download/release/v16.19.1/) and [npm](https://www.npmjs.com/) installed. More recent +versions of NodeJS are currently not supported, for details check [Issue #2220](https://github.com/slint-ui/slint/issues/2220). +Since Slint is implemented in the Rust programming language, you also need to install a Rust compiler (1.66 or newer). You can easily install a Rust compiler +following the instruction from [the Rust website](https://www.rust-lang.org/learn/get-started). + +We're going to use `slint-ui` as `npm` dependency. + +In a new directory, we create a new `package.json` file. + +```json +{{#include package.json}} +``` + +This should look familiar to people familiar with NodeJS. We see that this package.json +references a `main.js`, which we will add later. We must then create, in the same directory, +the `memory.slint` file. Let's just fill it with a hello world for now: + +```slint +{{#include memory.slint:main_window}} +``` + +What's still missing is the `main.js`: + +```js +{{#include main_initial.js:main}} +``` + +To recap, we now have a directory with a `package.json`, `memory.slint`, and `main.js`. + +We can now compile and run the program: + +```sh +npm install +npm start +``` + +and a window will appear with the green "Hello World" greeting. + +![Screenshot of initial tutorial app showing Hello World](https://slint-ui.com/blog/memory-game-tutorial/getting-started.png "Hello World") + +Feel free to use your favorite IDE for this purpose. \ No newline at end of file diff --git a/docs/tutorial/node/src/icons b/docs/tutorial/node/src/icons new file mode 120000 index 00000000000..db19f75f4dd --- /dev/null +++ b/docs/tutorial/node/src/icons @@ -0,0 +1 @@ +../../../../examples/memory/icons \ No newline at end of file diff --git a/docs/tutorial/node/src/ideas_for_the_reader.md b/docs/tutorial/node/src/ideas_for_the_reader.md new file mode 100644 index 00000000000..0261ef5464a --- /dev/null +++ b/docs/tutorial/node/src/ideas_for_the_reader.md @@ -0,0 +1,14 @@ +# Ideas For The Reader + +The game is visually a little bare. Here are some ideas how you could make further changes to enhance it: + +- The tiles could have rounded corners, to look a little less sharp. The [border-radius](https://slint-ui.com/docs/slint/src/builtins/elements.html#rectangle) + property of _Rectangle_ can be used to achieve that. + +- In real world memory games, the back of the tiles often have some common graphic. You could add an image with + the help of another _[Image](https://slint-ui.com/docs/slint/src/builtins/elements.html#image)_ + element. Note that you may have to use _Rectangle_'s _clip property_ + element around it to ensure that the image is clipped away when the curtain effect opens. + +Let us know in the comments on [Github Discussions](https://github.com/slint-ui/slint/discussions) +how you polished your code, or feel free to ask questions about how to implement something. diff --git a/docs/tutorial/node/src/introduction.md b/docs/tutorial/node/src/introduction.md new file mode 100644 index 00000000000..2d9c12e6d83 --- /dev/null +++ b/docs/tutorial/node/src/introduction.md @@ -0,0 +1,14 @@ +# Introduction + +This tutorial will introduce you to the Slint UI framework in a playful way by implementing a little memory game. We're going to combine the `.slint` language for the graphics with the game rules implemented in JavaScript. + +The game consists of a grid of 16 rectangular tiles. Clicking on a tile uncovers an icon underneath. +We know that there are 8 different icons in total, so each tile has a sibling somewhere in the grid with the +same icon. The objective is to locate all icon pairs. You can uncover two tiles at the same time. If they +aren't the same, the icons will be obscured again. +If you uncover two tiles with the same icon, then they remain visible - they're solved. + +This is how the game looks like in action: + + \ No newline at end of file diff --git a/docs/tutorial/node/src/main_game_logic.js b/docs/tutorial/node/src/main_game_logic.js new file mode 100644 index 00000000000..59f7c50abd6 --- /dev/null +++ b/docs/tutorial/node/src/main_game_logic.js @@ -0,0 +1,66 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial + + +// main.js +let slint = require("slint-ui"); +let ui = require("./memory.slint"); +let mainWindow = new ui.MainWindow(); + +let initial_tiles = mainWindow.memory_tiles; +let tiles = initial_tiles.concat(initial_tiles.map((tile) => Object.assign({}, tile))); + +for (let i = tiles.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * i); + [tiles[i], tiles[j]] = [tiles[j], tiles[i]]; +} + +// ANCHOR: game_logic +let model = new slint.ArrayModel(tiles); +mainWindow.memory_tiles = model; + +mainWindow.check_if_pair_solved.setHandler(function () { + let flipped_tiles = []; + tiles.forEach((tile, index) => { + if (tile.image_visible && !tile.solved) { + flipped_tiles.push({ + index, + tile + }); + } + }); + + if (flipped_tiles.length == 2) { + let { + tile: tile1, + index: tile1_index + } = flipped_tiles[0]; + + let { + tile: tile2, + index: tile2_index + } = flipped_tiles[1]; + + let is_pair_solved = tile1.image === tile2.image; + if (is_pair_solved) { + tile1.solved = true; + model.setRowData(tile1_index, tile1); + tile2.solved = true; + model.setRowData(tile2_index, tile2); + } else { + mainWindow.disable_tiles = true; + slint.Timer.singleShot(1000, () => { + mainWindow.disable_tiles = false; + tile1.image_visible = false; + model.setRowData(tile1_index, tile1); + tile2.image_visible = false; + model.setRowData(tile2_index, tile2); + }) + + } + } +}); + +mainWindow.run(); + +// ANCHOR_END: game_logic diff --git a/docs/tutorial/node/src/main_initial.js b/docs/tutorial/node/src/main_initial.js new file mode 100644 index 00000000000..d0a090e5c49 --- /dev/null +++ b/docs/tutorial/node/src/main_initial.js @@ -0,0 +1,11 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial + +// ANCHOR: main +// main.js +require("slint-ui"); +let ui = require("./memory.slint"); +let mainWindow = new ui.MainWindow(); +mainWindow.run(); + +// ANCHOR_END: main diff --git a/docs/tutorial/node/src/main_tiles_from_js.js b/docs/tutorial/node/src/main_tiles_from_js.js new file mode 100644 index 00000000000..1e112fd444c --- /dev/null +++ b/docs/tutorial/node/src/main_tiles_from_js.js @@ -0,0 +1,23 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial + +// ANCHOR: main +// main.js +let slint = require("slint-ui"); +let ui = require("./memory.slint"); +let mainWindow = new ui.MainWindow(); + +let initial_tiles = mainWindow.memory_tiles; +let tiles = initial_tiles.concat(initial_tiles.map((tile) => Object.assign({}, tile))); + +for (let i = tiles.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * i); + [tiles[i], tiles[j]] = [tiles[j], tiles[i]]; +} + +let model = new slint.ArrayModel(tiles); +mainWindow.memory_tiles = model; + +mainWindow.run(); + +// ANCHOR_END: main diff --git a/docs/tutorial/node/src/memory.slint b/docs/tutorial/node/src/memory.slint new file mode 100644 index 00000000000..75661fb37c3 --- /dev/null +++ b/docs/tutorial/node/src/memory.slint @@ -0,0 +1,12 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial + +// ANCHOR: main_window +// memory.slint +export component MainWindow inherits Window { + Text { + text: "hello world"; + color: green; + } +} +// ANCHOR_END: main_window diff --git a/docs/tutorial/node/src/memory_game_logic.slint b/docs/tutorial/node/src/memory_game_logic.slint new file mode 100644 index 00000000000..9b872ab5776 --- /dev/null +++ b/docs/tutorial/node/src/memory_game_logic.slint @@ -0,0 +1,93 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial + +struct TileData { + image: image, + image_visible: bool, + solved: bool, +} + +component MemoryTile inherits Rectangle { + callback clicked; + in property open_curtain; + in property solved; + in property icon; + + height: 64px; + width: 64px; + background: solved ? #34CE57 : #3960D5; + animate background { duration: 800ms; } + + Image { + source: icon; + width: parent.width; + height: parent.height; + } + + // Left curtain + Rectangle { + background: #193076; + width: open_curtain ? 0px : (parent.width / 2); + height: parent.height; + animate width { duration: 250ms; easing: ease-in; } + } + + // Right curtain + Rectangle { + background: #193076; + x: open_curtain ? parent.width : (parent.width / 2); + width: open_curtain ? 0px : (parent.width / 2); + height: parent.height; + animate width { duration: 250ms; easing: ease-in; } + animate x { duration: 250ms; easing: ease-in; } + } + + TouchArea { + clicked => { + // Delegate to the user of this element + root.clicked(); + } + } +} + +// ANCHOR: mainwindow_interface +export component MainWindow inherits Window { + width: 326px; + height: 326px; + + callback check_if_pair_solved(); // Added + in property disable_tiles; // Added + + in-out property <[TileData]> memory_tiles: [ + { image: @image-url("icons/at.png") }, +// ANCHOR_END: mainwindow_interface + { image: @image-url("icons/balance-scale.png") }, + { image: @image-url("icons/bicycle.png") }, + { image: @image-url("icons/bus.png") }, + { image: @image-url("icons/cloud.png") }, + { image: @image-url("icons/cogs.png") }, + { image: @image-url("icons/motorcycle.png") }, + { image: @image-url("icons/video.png") }, + ]; + + // ANCHOR: tile_click_logic + for tile[i] in memory_tiles : MemoryTile { + x: mod(i, 4) * 74px; + y: floor(i / 4) * 74px; + width: 64px; + height: 64px; + icon: tile.image; + open_curtain: tile.image_visible || tile.solved; + // propagate the solved status from the model to the tile + solved: tile.solved; + clicked => { + // old: tile.image_visible = !tile.image_visible; + // new: + if (!root.disable_tiles) { + tile.image_visible = !tile.image_visible; + root.check_if_pair_solved(); + } + } + } + // ANCHOR_END: tile_click_logic +} diff --git a/docs/tutorial/node/src/memory_tile.md b/docs/tutorial/node/src/memory_tile.md new file mode 100644 index 00000000000..593edc4934f --- /dev/null +++ b/docs/tutorial/node/src/memory_tile.md @@ -0,0 +1,42 @@ +# Memory Tile + +With the skeleton in place, let's look at the first element of the game, the memory tile. It will be the +visual building block that consists of an underlying filled rectangle background, the icon image. Later we'll add a +covering rectangle that acts as a curtain. The background rectangle is declared to be 64 logical pixels wide and tall, +and it's filled with a soothing tone of blue. Note how lengths in the `.slint` language have a unit, here +the `px` suffix. That makes the code easier to read and the compiler can detect when your are accidentally +mixing values with different units attached to them. + +We copy the following code into the `memory.slint` file: + +```slint +{{#include memory_tile.slint:main_window}} +``` + +Note that we export the MainWindow component. This is necessary so that we can later access it +from our business logic. + +Inside the Rectangle we place an Image element that +loads an icon with the @image-url() macro. The path is relative to the folder in which +the `memory.slint` is located. This icon and others we're going to use later need to be installed first. You can download a +[Zip archive](https://slint-ui.com/blog/memory-game-tutorial/icons.zip) that we have prepared. + +If you are on Linux or macOS, download and extract it with the following two commands: + +```sh +curl -O https://slint-ui.com/blog/memory-game-tutorial/icons.zip +unzip icons.zip +``` + +If you are on Windows, use the following commands: + +``` +powershell curl -Uri https://slint-ui.com/blog/memory-game-tutorial/icons.zip -Outfile icons.zip +powershell Expand-Archive -Path icons.zip -DestinationPath . +``` + +This should unpack an `icons` directory containing a bunch of icons. + +We running the program with `npm start` and it gives us a window on the screen that shows the icon of a bus on a blue background. + +![Screenshot of the first tile](https://slint-ui.com/blog/memory-game-tutorial/memory-tile.png "Memory Tile Screenshot") diff --git a/docs/tutorial/node/src/memory_tile.slint b/docs/tutorial/node/src/memory_tile.slint new file mode 100644 index 00000000000..221e45b4fdd --- /dev/null +++ b/docs/tutorial/node/src/memory_tile.slint @@ -0,0 +1,20 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial + +// ANCHOR: main_window +component MemoryTile inherits Rectangle { + width: 64px; + height: 64px; + background: #3960D5; + + Image { + source: @image-url("icons/bus.png"); + width: parent.width; + height: parent.height; + } +} + +export component MainWindow inherits Window { + MemoryTile {} +} +// ANCHOR_END: main_window diff --git a/docs/tutorial/node/src/memory_tiles_from_cpp.slint b/docs/tutorial/node/src/memory_tiles_from_cpp.slint new file mode 100644 index 00000000000..d0b7c122817 --- /dev/null +++ b/docs/tutorial/node/src/memory_tiles_from_cpp.slint @@ -0,0 +1,80 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial + +struct TileData { + image: image, + image_visible: bool, + solved: bool, +} + +component MemoryTile inherits Rectangle { + callback clicked; + in property open_curtain; + in property solved; + in property icon; + + height: 64px; + width: 64px; + background: solved ? #34CE57 : #3960D5; + animate background { duration: 800ms; } + + Image { + source: icon; + width: parent.width; + height: parent.height; + } + + // Left curtain + Rectangle { + background: #193076; + width: open_curtain ? 0px : (parent.width / 2); + height: parent.height; + animate width { duration: 250ms; easing: ease-in; } + } + + // Right curtain + Rectangle { + background: #193076; + x: open_curtain ? parent.width : (parent.width / 2); + width: open_curtain ? 0px : (parent.width / 2); + height: parent.height; + animate width { duration: 250ms; easing: ease-in; } + animate x { duration: 250ms; easing: ease-in; } + } + + TouchArea { + clicked => { + // Delegate to the user of this element + root.clicked(); + } + } +} + +export component MainWindow inherits Window { + width: 326px; + height: 326px; + + in-out property <[TileData]> memory_tiles: [ + { image: @image-url("icons/at.png") }, + { image: @image-url("icons/balance-scale.png") }, + { image: @image-url("icons/bicycle.png") }, + { image: @image-url("icons/bus.png") }, + { image: @image-url("icons/cloud.png") }, + { image: @image-url("icons/cogs.png") }, + { image: @image-url("icons/motorcycle.png") }, + { image: @image-url("icons/video.png") }, + ]; + for tile[i] in memory_tiles : MemoryTile { + x: mod(i, 4) * 74px; + y: floor(i / 4) * 74px; + width: 64px; + height: 64px; + icon: tile.image; + open_curtain: tile.image_visible || tile.solved; + // propagate the solved status from the model to the tile + solved: tile.solved; + clicked => { + tile.image_visible = !tile.image_visible; + } + } +} diff --git a/docs/tutorial/node/src/package.json b/docs/tutorial/node/src/package.json new file mode 100644 index 00000000000..d08b73d4f85 --- /dev/null +++ b/docs/tutorial/node/src/package.json @@ -0,0 +1,11 @@ +{ + "name": "memory", + "version": "1.0.0", + "main": "main.js", + "dependencies": { + "slint-ui": "^1.0.0" + }, + "scripts": { + "start": "node ." + } +} \ No newline at end of file diff --git a/docs/tutorial/node/src/polishing_the_tile.md b/docs/tutorial/node/src/polishing_the_tile.md new file mode 100644 index 00000000000..c9d37568f99 --- /dev/null +++ b/docs/tutorial/node/src/polishing_the_tile.md @@ -0,0 +1,32 @@ +# Polishing the Tile + +Next, let's add a curtain like cover that opens up when clicking. We achieve this by declaring two rectangles +below the Image, so that they are drawn afterwards and thus on top of the image. +The TouchArea element declares a transparent rectangular region that allows +reacting to user input such as a mouse click or tap. We use that to forward a callback to the MainWindow +that the tile was clicked on. In the MainWindow we react by flipping a custom open_curtain property. +That in turn is used in property bindings for the animated width and x properties. Let's look at the two states a bit +more in detail: + +| *open_curtain* value: | false | true | +| --- | --- | --- | +| Left curtain rectangle | Fill the left half by setting the width *width* to half the parent's width | Width of zero makes the rectangle invisible | +| Right curtain rectangle | Fill the right half by setting *x* and *width* to half of the parent's width | *width* of zero makes the rectangle invisible. *x* is moved to the right, to slide the curtain open when animated | + +In order to make our tile extensible, the hard-coded icon name is replaced with an *icon* +property that can be set from the outside when instantiating the element. For the final polish, we add a +*solved* property that we use to animate the color to a shade of green when we've found a pair, later. We +replace the code inside the `memory.slint` file with the following: + +```slint +{{#include ../../rust/src/main_polishing_the_tile.rs:tile}} +``` + +Note the use of `root` and `self` in the code. `root` refers to the outermost +element in the component, that's the MemoryTile in this case. `self` refers +to the current element. + +Running this gives us a window on the screen with a rectangle that opens up to show us the bus icon, when clicking on +it. Subsequent clicks will close and open the curtain again. + +