Skip to content
Snippets Groups Projects
Commit 077aed8a authored by Will Billingsley's avatar Will Billingsley
Browse files

Updated README for tutorial 4

parent 681772b0
Branches
No related tags found
No related merge requests found
# Tutorial 3
# Tutorial 4
For the third tutorial, we're going to work with TypeScript and d3.js
For this tutorial, we're going to introduce Webpack and Vue.js
And, yes, we're still working with Conway's Game of Life.
......@@ -11,255 +11,160 @@ First, get the code and put it somewhere you can load it in your browser.
To get the code, open a terminal, `cd` into the public directory of whichever webserver you are using, and:
```sh
git clone https://gitlab.une.edu.au/cosc360in2018/tutorial-conway-life-t3.git
git clone https://gitlab.une.edu.au/cosc360in2018/tutorial-conway-life-t4.git
```
This will clone this code using the git version control system. That will, amongst other things, let you switch between different branches (eg, the solution and the start of the exercise)
## Check it's working
Open `index.html` in the browser, and you should find yourself faced with Conway's Game of Life from Tutorial 2.
Open `index.html` in the browser, and you should find yourself faced with Conway's Game of Life from Tutorial 3.
## Set up Typescript
## Set up Webpack
Open a terminal in the `tutorial-conway-life-t3` directory.
1. Initialise npm
1. Install webpack, and loaders for typescript and vue
```sh
npm init
npm install --save-dev webpack ts-loader css-loader vue vue-loader vue-template-compiler
```
2. Install typescript locally (or, if you are on your own computer, globally)
2. Let's also install Vue's TypeScript-class like component definitions in case you'd like to use those
```sh
npm install --save-dev typescript
npm install --save-dev vue-class-component vue-property-decorator
```
You should now have a typescript compiler in `node_modules/typescript/bin/tsc`
3. Create a config file for typescript
```sh
node_modules/typescript/bin/tsc --init
```
And take a look at the `tsconfig.json` file it produces
4. Install the type definition files for d3.js
```sh
npm install --save-dev @types/d3
```
We're going to load d3.js directly from the web, but if we are going to call it from Typescript code, then we should give Typescript the definition file so it knows what functions are available.
5. Set the typescript compiler running in "watch" mode. This will cause it to look for changes in `*.tsc` files and compile them to `.js` files
3. Create a src directory, and move the typescript files into it
```sh
node_modules/typescript/bin/tsc --watch
```
You might get an error that there are no files to compile; that's ok -- we're about to create one.
## Start coding...
Time to open the code. I recommend Visual Studio Code as an editor for this. Especially as it understands Typescript, and for this exercise you'll find it will even be able to do syntax help with the d3 library.
### Life.ts
Create a file called `gameOfLife.ts`. The TypeScript compiler will quickly see your file and compile it to `gameOfLife.js`, so we'll be able to overwrite the old code.
First, let's declare a class `Life`, that will hold an array of boolean values
```ts
class Life {
board: Array<boolean>
mkdir src
mv *.ts src/
```
4. Edit `tsconfig.json` to use ECMA2015 as its module loader, node to resolve modules, and to turn on experimental decorators. Also ensure it looks for sources in the `src` subtree.
```json
{
"compilerOptions": {
"outDir": "./built/",
"sourceMap": true,
"strict": true,
"noImplicitReturns": true,
"module": "es2015",
"moduleResolution": "node",
"target": "es5",
"experimentalDecorators": true
},
"include": [
"./src/**/*"
]
}
```
We're going to need to create a constructor before the compiler will be happy. We'll also include a utility function for creating an array of false values. Inside Life:
5. Now, let's create `webpack.config.js`. Take a look at the following and see what it does.
```ts
emptyBoard():boolean[] {
let arr = <boolean[]>[]
for (let i = 0; i < this.h * this.w; i++) {
arr.push(false)
}
return arr
}
```js
const path = require('path');
constructor(public w:number = 40, public h:number = 20) {
this.board = this.emptyBoard()
module.exports = {
entry: './src/index.ts',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
}
```
Notice that we've used default arguments for `w` and `h`, and that we've specified their types. Also notice that in `emptyBoard()`, when we declared the empty array `arr`, we cast it to be an array of booleans using angle-brackets.
Now let's create some functions that will be able to work out x,y values from an index into the array, and vice-versa:
```ts
boardIndex(x:number, y:number):number {
return y * this.w + x
}
x(index:number) {
return index % this.w
}
y(index:number) {
return Math.floor(index / this.w)
]
},
resolve: {
extensions: [ '.tsx', '.ts', '.js' ]
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
```
And let's translate the `isAlive` and `toggleCell` functions from our JavaScript code:
6. Before we can run webpack, we're going to need to install its command-line interface. There are two, so let's install the official one
```ts
toggleCell(x:number, y:number):void {
let b = this.boardIndex(x,y)
this.board[b] = !this.board[b]
}
isAlive(x:number, y:number):boolean {
let b = this.boardIndex(x,y)
return (x >= 0) && (y >= 0) && (x < this.w) && (y < this.h)
&& this.board[b]
}
```sh
npm install webpack-cli
```
Now you should implement `liveNeighbours(x:number, y:number):number` and `stepGame()`
### render.ts
Now let's create `render.ts`, and have the game render from d3.js using TypeScript.
First, create the file. At the top of the file, we're going to put a *triple-slash directive* to tell the TypeScript compiler to load the type signatures for d3.js
7. If we run webpack, via `npx webpack` we'll find we get an error. We don't actually have a `src/index.ts` file. So, let's create it and put in it the contents from the script tag in our html:
```ts
/// <reference types="d3" />
render()
```
Next let's declare some constants:
We should now find that if we run `npx webpack` it generates a single js file `dist/build.js`
```ts
const cellSize = 20
const life = new Life()
const game = d3.select("#game").append("g")
```
Replace the script tag containing the call to `render()` in `index.html` with a script tag to load this file.
The cell-size is what you'd normally think of as a constant, but the other two are constant references.
Now reload the page, and the code should still work.
`life` instantiates a new instance of our Life class. `game` asks d3 to select the svg element with the id "game", and to create a `g` element within it. This is where we'll render our game.
8. Next, let's tell NPM that we build our project using webpack. In `package.json`, inside the `scripts` block, add
Before we write the `render()` function, pop across to `index.html` and let's do a few small edits:
```json
"build": "webpack",
```
* Load the d3.js script from the web
We can now tell npm to watch our project for changes, and rebuild it automatically:
```html
<script src="https://d3js.org/d3.v5.min.js"></script>
```sh
npm run build -- --watch
```
* As we've now defined `life` as a constant in `render.ts` we need to change the script in the html
9. Next, let's start using TypeScript modules. First, we should actually install d3, rather than only its types. If we're using modules, we can't also use globals.
```html
<button class="btn btn-primary"
onclick="javascript: { life.stepGame(); render(); }">
Step
</button>
```sh
npm install --save-dev d3
```
and
And let's alter our index.html to give the button and id, and remove the onclick that relies on a global.
```html
<script>
render()
</script>
<button id="step" class="btn btn-primary" >Step</button>
```
Now, if we reload the browser, we should find the game no longer renders (we haven't defined the function), but at the console you can call functions on the `life` instance. Try asking the console `life.isAlive(0,0)`
Now, let's get the grid rendering at all. Within the game area, we want to match `rect` elements against the game board.
10. In `src/gameOfLife.ts`, let's change the top line to:
```ts
let update = game.selectAll("rect").data(life.board)
export default class Life {
```
Next we need to think about what we want to do for the three sets (updated, entering, and exiting)
* As the grid isn't changing side, the `exit()` set should be empty
* The `enter()` set will need to set up and position the rectangles, and add their click handler
* The update set will just have to update whether the cell is a live or not.
Let's do the `enter()` set first, as it's hard to see if update() is working until after we've done `enter()`
To append a `rect` element for every cell, we would call:
and in `src/render.ts`
```ts
update.enter()
.append("rect")
import Life from "./gameOfLife"
import * as d3 from "d3"
```
Try it, reload the browser, and use the inspector to see if it worked (they'll be zero-sized rectangles, so you won't yet see them on-screen)
To set their widths and heights, we can just set an attribute to a constant. We can just chain the `attr` calls after the `append`:
and
```ts
update.enter()
.append("rect")
.attr("width", cellSize)
.attr("height", cellSize)
export default function render() {
```
Reload, and it should seem like there's a single black square
Now, let's set their position. To do this, we're going to use `attr` again, but instead of passing a constant as the second value, we'll pass a function. This function will accept the cell's value and its index in the array, and calculate the result.
We're using `life.x()` that we wrote in `gameOfLife.ts` to work out the x coordinate from the index in the array.
and in `src/index.ts`
```ts
.attr("x", (val,i) => {
return life.x(i) * cellSize
})
.attr("y", (val,i) => {
return life.y(i) * cellSize
})
```
Next, let's add a click handler. This uses d3's `on` function to wire up the event handler. Again, we're giving it a function that accepts the cell's value and index in the array. In this case we want to call `toggleCell` and then trigger another `render` to update the grid on the screen
import { render, life } from "./render"
```ts
.on("click", (d,i) => {
let x = life.x(i)
let y = life.y(i)
life.toggleCell(x,y)
render()
})
```
At this point, if you refresh the browser, it will still all look black because we haven't put the class attribute in. Our `rect` element should always have the "cell" class, but should only have the "alive" class if the cell is alive. The class can be set using d3's `attr` function. And in this case, our function only needs the cell's value (is it alive or not) not its index.
Here, I've used Typescript's *ternary operator*, which is a shorthand way of doing an *if ... else ...*.
```ts
.attr("class", (d) => {
return d ? "cell alive" : "cell"
document.getElementById("step")!.addEventListener("click", () => {
life.stepGame()
render()
})
```
If you refresh the browser, hopefully the grid renders ok, but clicking doesn't show any changes. We're only updating cells *when they are part of the entering set*. But after we've created the board, they are part of the update set and we haven't coded that.
But you should be able to, say, click the cell at (0,0) and then ask the console `life.isAlive(0,0)`
All going well, if you rebuild the code and then refresh the browser, Life is now working using TypeScript modules and webpack.
Now let's implement the update set. Well, as the cell is already positioned and already has its event listener, all we need to do is update its class as before.
```ts
update.attr("class", (d) => {
return d ? "cell alive" : "cell"
})
```
### Vue.js
Reload, and hopefully we now have Conway's game of life.
\ No newline at end of file
The second part is set as a challenge. Above the d3-rendered game of life, add some controls using Vue.js that will allow you to alter the board's dimensions (resetting the game) and that will allow you to start and stop a tick-timer moving the game forward automatically.
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment