diff --git a/src/components/Tutorial/index.module.scss b/src/components/Tutorial/index.module.scss index e5294723c..c73284e34 100644 --- a/src/components/Tutorial/index.module.scss +++ b/src/components/Tutorial/index.module.scss @@ -72,6 +72,7 @@ transform: translateY(-5%); opacity: 0; transition: all 0.2s ease-out; + z-index: 2; a { color: var(--ifm-font-color-base); diff --git a/src/tutorials/v8.0.0/chooChooTrain/step1-content.md b/src/tutorials/v8.0.0/chooChooTrain/step1-content.md index cc24e9ce9..1156ec35b 100644 --- a/src/tutorials/v8.0.0/chooChooTrain/step1-content.md +++ b/src/tutorials/v8.0.0/chooChooTrain/step1-content.md @@ -1 +1,25 @@ -# Choo Choo Train \ No newline at end of file +# Onboard the Choo Choo Train! + +Welcome to the Choo Choo Train workshop! + +We are going to handcraft a cute little scene of a train moving through a landscape at night. We will solely be using the [Graphics](https://pixijs.com/guides/components/graphics) API to draw out the whole scene. In this tutorial, we will be exploring a handful of methods it provides to draw a variety of shapes. For the full list of methods, please check out the Graphics [documentation](https://pixijs.download/release/docs/PIXI.Graphics.html). + +Please go through the tutorial steps at your own pace and challenge yourself using the editor on the right hand side. Here PixiJS has already been included as guided under the [Getting Started](/guides/basics/getting-started#loading-pixijs) section. Let's start off by creation a PixiJS application, initialize it, add its canvas to the DOM, and preload the required assets ahead of the subsequent steps. + +We will be using an asynchronous immediately invoked function expression ([IIFE](https://developer.mozilla.org/en-US/docs/Glossary/IIFE)), but you are free to switch to use promises instead. + +## Application Setup + +Let's create the application outside of the IIFE just so that it can be referenced across other functions declared outside. The initialization and appending the application's canvas will be done from within the `setup` function which is called inside the IIFE. + +```javascript +async function setup() +{ + await app.init({ background: '#021f4b', resizeTo: window }); + document.body.appendChild(app.canvas); +} +``` + +At this point, you should see the preview filled with an empty light blue background. + +When you are ready, proceed to the next exercise using the _Next >_ button below, or feel free to skip to any exercise using the drop-down menu on the top right hand corner of the card. \ No newline at end of file diff --git a/src/tutorials/v8.0.0/chooChooTrain/step10-code.js b/src/tutorials/v8.0.0/chooChooTrain/step10-code.js index 98d520442..9c27cd494 100644 --- a/src/tutorials/v8.0.0/chooChooTrain/step10-code.js +++ b/src/tutorials/v8.0.0/chooChooTrain/step10-code.js @@ -102,11 +102,11 @@ function addMountains() // Reposition the mountain groups when they move off screen. if (group1.x <= -app.screen.width) { - group1.x = app.screen.width; + group1.x += app.screen.width * 2; } if (group2.x <= -app.screen.width) { - group2.x = app.screen.width; + group2.x += app.screen.width * 2; } }); } @@ -175,7 +175,6 @@ function createMountainGroup() ) .fill({ color: colorRight }); - // Add the mountains to the stage. return graphics; } @@ -233,7 +232,7 @@ function addTrees() }); } -function createTree(width = 200, height = 250) +function createTree(width, height) { // Define the dimensions of the tree trunk. const trunkWidth = 30; @@ -246,7 +245,7 @@ function createTree(width = 200, height = 250) const crownWidthIncrement = width / crownLevels; // Define the colors of the parts. - const treeColor = 0x264d3d; + const crownColor = 0x264d3d; const trunkColor = 0x563929; const graphics = new Graphics() @@ -265,7 +264,7 @@ function createTree(width = 200, height = 250) .moveTo(-levelWidth / 2, y) .lineTo(0, y - crownLevelHeight - offset) .lineTo(levelWidth / 2, y) - .fill({ color: treeColor }); + .fill({ color: crownColor }); } return graphics; @@ -391,9 +390,9 @@ function createTrainHead() const frontRadius = frontHeight / 2; // Define the dimensions of the cabin. - const containerHeight = 200; - const containerWidth = 150; - const containerRadius = 15; + const cabinHeight = 200; + const cabinWidth = 150; + const cabinRadius = 15; // Define the dimensions of the chimney. const chimneyBaseWidth = 30; @@ -401,7 +400,7 @@ function createTrainHead() const chimneyHeight = 70; const chimneyDomeHeight = 25; const chimneyTopOffset = (chimneyTopWidth - chimneyBaseWidth) / 2; - const chimneyStartX = containerWidth + frontWidth - frontRadius - chimneyBaseWidth; + const chimneyStartX = cabinWidth + frontWidth - frontRadius - chimneyBaseWidth; const chimneyStartY = -frontHeight; // Define the dimensions of the roof. @@ -409,10 +408,10 @@ function createTrainHead() const roofExcess = 20; // Define the dimensions of the door. - const doorWidth = containerWidth * 0.7; - const doorHeight = containerHeight * 0.7; - const doorStartX = (containerWidth - doorWidth) * 0.5; - const doorStartY = -(containerHeight - doorHeight) * 0.5 - doorHeight; + const doorWidth = cabinWidth * 0.7; + const doorHeight = cabinHeight * 0.7; + const doorStartX = (cabinWidth - doorWidth) * 0.5; + const doorStartY = -(cabinHeight - doorHeight) * 0.5 - doorHeight; // Define the dimensions of the window. const windowWidth = doorWidth * 0.8; @@ -434,24 +433,24 @@ function createTrainHead() // Draw the head front .roundRect( - containerWidth - frontRadius - containerRadius, + cabinWidth - frontRadius - cabinRadius, -frontHeight, - frontWidth + frontRadius + containerRadius, + frontWidth + frontRadius + cabinRadius, frontHeight, frontRadius, ) .fill({ color: 0x7f3333 }) // Draw the cabin - .roundRect(0, -containerHeight, containerWidth, containerHeight, containerRadius) + .roundRect(0, -cabinHeight, cabinWidth, cabinHeight, cabinRadius) .fill({ color: 0x725f19 }) // Draw the roof - .rect(-roofExcess / 2, containerRadius - containerHeight - roofHeight, containerWidth + roofExcess, roofHeight) + .rect(-roofExcess / 2, cabinRadius - cabinHeight - roofHeight, cabinWidth + roofExcess, roofHeight) .fill({ color: 0x52431c }) // Draw the door - .roundRect(doorStartX, doorStartY, doorWidth, doorHeight, containerRadius) + .roundRect(doorStartX, doorStartY, doorWidth, doorHeight, cabinRadius) .stroke({ color: 0x52431c, width: 3 }) // Draw the window @@ -605,8 +604,8 @@ function addSmokes() smokeGroup.circle(x, y, radius); } - // Fill the smoke group with a semi-transparent gray. - smokeGroup.fill({ color: 0xc9c9c9, alpha: 0.5 }); + // Fill the smoke group with gray color. + smokeGroup.fill({ color: 0xc9c9c9 }); // Position the smoke group. smokeGroup.x = baseX; @@ -635,6 +634,7 @@ function addSmokes() group.x = baseX - Math.pow(group.tick, 2) * 400; group.y = baseY - group.tick * 200; group.scale.set(Math.pow(group.tick, 0.75)); + group.alpha = 1 - Math.pow(group.tick, 0.5); }); }); } diff --git a/src/tutorials/v8.0.0/chooChooTrain/step10-content.md b/src/tutorials/v8.0.0/chooChooTrain/step10-content.md index 9842bf319..c23909b01 100644 --- a/src/tutorials/v8.0.0/chooChooTrain/step10-content.md +++ b/src/tutorials/v8.0.0/chooChooTrain/step10-content.md @@ -1,3 +1,5 @@ # You did it! -Congratulations! Hope you enjoyed the journey. Feel free to head back to the gallery and explore other tutorials. \ No newline at end of file +Congratulations, hope you enjoyed the journey! Now you are an expert on the Graphics API. Make sure to explore other features that the API has to offer on the official [documentation](https://pixijs.download/release/docs/PIXI.Graphics.html), like the ability to cut shapes out from existing ones, advance lines and curves, using gradients or textures for fill and stroke - just to list a few. + +Feel free to head back to the gallery and explore other tutorials. \ No newline at end of file diff --git a/src/tutorials/v8.0.0/chooChooTrain/step2-content.md b/src/tutorials/v8.0.0/chooChooTrain/step2-content.md index 3598b241e..28e61d06c 100644 --- a/src/tutorials/v8.0.0/chooChooTrain/step2-content.md +++ b/src/tutorials/v8.0.0/chooChooTrain/step2-content.md @@ -1 +1,30 @@ -# Adding Stars \ No newline at end of file +# Adding Stars + +Let's start with the sky! It's a little plain and boring right now, so how about adding some stars to it? + +Because we will be drawing many different elements on the remaining steps, let's separate the building of each element into its own function to be called from within the main IIFE. Here, the `addStars` function has been set up for you to fill out. + +Graphics API has a built-in `star(x, y, points, radius, innerRadius?, rotation?)` method for this with the ability to specify number of star points, its rotation, radius and even inner radius if you prefer it with a hollow. + +Here, we will use a for-loop to create a number of 5-point stars with randomized radius, rotation and deterministically randomized positions across the whole scene. Let create 20 scattered stars with a radius size between 2 - 5 units to start under a single Graphics instance. After drawing out the individual invisible shape, we can then use the `fill(style)` method to color it in, specifying the color and the opacity calculated from the percentage of random radius to the max radius. + +> _**TIPS:** The Graphics API methods (with a few exceptions) return back the Graphics instance so it can be used for chained as you will see in the future steps_ + +```javascript +const starCount = 20; +const graphics = new Graphics(); + +for (let index = 0; index < starCount; index++) +{ + const x = (index * 0.78695 * app.screen.width) % app.screen.width; + const y = (index * 0.9382 * app.screen.height) % app.screen.height; + const radius = 2 + Math.random() * 3; + const rotation = Math.random() * Math.PI * 2; + + graphics.star(x, y, 5, radius, 0, rotation).fill({ color: 0xffdf00, alpha: radius / 5 }); +} + +app.stage.addChild(graphics); +``` + +Now we have a starry sky! But let's take it a little further to lighten up our sky even more on the next step. \ No newline at end of file diff --git a/src/tutorials/v8.0.0/chooChooTrain/step3-content.md b/src/tutorials/v8.0.0/chooChooTrain/step3-content.md index ad2c37beb..23291c44a 100644 --- a/src/tutorials/v8.0.0/chooChooTrain/step3-content.md +++ b/src/tutorials/v8.0.0/chooChooTrain/step3-content.md @@ -1 +1,31 @@ -# Adding Moon \ No newline at end of file +# Adding Moon + +For the moon crescent, we will cheat a little bit with an existing moon SVG code here: + +```svg + + + + +``` + +Graphics API also has a built-in `svg(svgString)` method for drawing vector graphics using SVG data. Have a go at it on the set up `addMoon` function. + +```javascript +const svg = `-- SVG DATA STRING --` +const graphics = new Graphics().svg(svg); + +graphics.x = app.screen.width / 2 + 100; +graphics.y = app.screen.height / 8; +app.stage.addChild(graphics); +``` + +Think the sky is enough, let's us now proceed to add some landscape elements! \ No newline at end of file diff --git a/src/tutorials/v8.0.0/chooChooTrain/step4-completed-code.js b/src/tutorials/v8.0.0/chooChooTrain/step4-completed-code.js index 6359430bd..8ffb2dd37 100644 --- a/src/tutorials/v8.0.0/chooChooTrain/step4-completed-code.js +++ b/src/tutorials/v8.0.0/chooChooTrain/step4-completed-code.js @@ -95,11 +95,11 @@ function addMountains() // Reposition the mountain groups when they move off screen. if (group1.x <= -app.screen.width) { - group1.x = app.screen.width; + group1.x += app.screen.width * 2; } if (group2.x <= -app.screen.width) { - group2.x = app.screen.width; + group2.x += app.screen.width * 2; } }); } @@ -168,6 +168,5 @@ function createMountainGroup() ) .fill({ color: colorRight }); - // Add the mountains to the stage. return graphics; } diff --git a/src/tutorials/v8.0.0/chooChooTrain/step4-content.md b/src/tutorials/v8.0.0/chooChooTrain/step4-content.md index 45f2c3a42..4137247fe 100644 --- a/src/tutorials/v8.0.0/chooChooTrain/step4-content.md +++ b/src/tutorials/v8.0.0/chooChooTrain/step4-content.md @@ -1 +1,104 @@ -# Adding Mountains \ No newline at end of file +# Adding Mountains + +For the background let's put up some mountains, shall we? We will also animate them to the left to give an impression that the scene is moving rightwards. + +## Create Mountain Groups + +Since we are moving the mountains to the left, they will eventually go off the screen and at the same time leaving empty spaces to the right. To fix this, we will be looping them back to the right of the scene once they go out of the scene view. In order to make the loop seamless, we will be making 2 mountain groups where each covers the whole scene. Then we will offset one group off the screen to the right. This is so that the second group and slowly filling in the screen from the right as the first group moving off the screen to the left before looping back to be offscreen to the right of the second group and repeating the process. + +Let start by filling in the logic for creating a mountain group in the `createMountainGroup()` function which will return a Graphics instance of a mountain group. We will use this to create the 2 group instances later. + +Here, we are using a single Graphics instance for a group of mountains. Taking into account the screen dimension we can draw out 3 mountains with different heights and colors. In this case, we will imagine the Graphics instance as a pen and for each of the mountain we move the pen to the starting position using Graphics API's `moveTo(x, y)` method and then contour out the mountain arc using `bezierCurveTo(cx1, cy1, cx2, cy2, x, y)` method, where [`cx`, `cy`] positions are control point coordinates for the curve going from where it was to the [`x`, `y`] position. Again, we then need to fill the resulted shape with `fill(style)`. + +> _**TIPS:** In this case, we do not have to connect the end point to the starting point as the Graphics' context will automatically infer a closed shape by doing so for the fill. + +```javascript +const graphics = new Graphics(); +const width = app.screen.width / 2; +const startY = app.screen.height; +const startXLeft = 0; +const startXMiddle = Number(app.screen.width) / 4; +const startXRight = app.screen.width / 2; +const heightLeft = app.screen.height / 2; +const heightMiddle = (app.screen.height * 4) / 5; +const heightRight = (app.screen.height * 2) / 3; +const colorLeft = 0xc1c0c2; +const colorMiddle = 0x7e818f; +const colorRight = 0x8c919f; + +graphics + // Draw the middle mountain + .moveTo(startXMiddle, startY) + .bezierCurveTo( + startXMiddle + width / 2, + startY - heightMiddle, + startXMiddle + width / 2, + startY - heightMiddle, + startXMiddle + width, + startY, + ) + .fill({ color: colorMiddle }) + + // Draw the left mountain + .moveTo(startXLeft, startY) + .bezierCurveTo( + startXLeft + width / 2, + startY - heightLeft, + startXLeft + width / 2, + startY - heightLeft, + startXLeft + width, + startY, + ) + .fill({ color: colorLeft }) + + // Draw the right mountain + .moveTo(startXRight, startY) + .bezierCurveTo( + startXRight + width / 2, + startY - heightRight, + startXRight + width / 2, + startY - heightRight, + startXRight + width, + startY, + ) + .fill({ color: colorRight }); + +return graphics; +``` + +## Set Up Mountain Groups + +With the `createMountainGroup()` helper function, we can then create 2 instances of the mountain group and offset one of them off the screen to the right. + +```javascript +const group1 = createMountainGroup(); +const group2 = createMountainGroup(); + +group2.x = app.screen.width; +app.stage.addChild(group1, group2); +``` + +You should now see a single group of mountains covering the whole scene. + +## Animate Mountains + +Using the application's ticker, we can add a callback function which will reposition the mountain groups every ticker update, creating a continuous animation. The callback function will be supplied with the Ticker object in which time-related data can be inferred like the `deltaTime` that we will be using to calculate the distance for the mountain to move consistently. Remember to reposition the groups when they moved completely off the screen. + +```javascript +app.ticker.add((time) => +{ + const dx = time.deltaTime * 0.5; + + group1.x -= dx; + group2.x -= dx; + + if (group1.x <= -app.screen.width) + { + group1.x += app.screen.width * 2; + } + if (group2.x <= -app.screen.width) + { + group2.x += app.screen.width * 2; + } +}); +``` \ No newline at end of file diff --git a/src/tutorials/v8.0.0/chooChooTrain/step5-code.js b/src/tutorials/v8.0.0/chooChooTrain/step5-code.js index daea5d9a9..64bd36320 100644 --- a/src/tutorials/v8.0.0/chooChooTrain/step5-code.js +++ b/src/tutorials/v8.0.0/chooChooTrain/step5-code.js @@ -96,11 +96,11 @@ function addMountains() // Reposition the mountain groups when they move off screen. if (group1.x <= -app.screen.width) { - group1.x = app.screen.width; + group1.x += app.screen.width * 2; } if (group2.x <= -app.screen.width) { - group2.x = app.screen.width; + group2.x += app.screen.width * 2; } }); } @@ -169,7 +169,6 @@ function createMountainGroup() ) .fill({ color: colorRight }); - // Add the mountains to the stage. return graphics; } @@ -178,7 +177,7 @@ function addTrees() /** -- INSERT CODE HERE -- */ } -function createTree(width = 200, height = 250) +function createTree(width, height) { /** -- INSERT CODE HERE -- */ } diff --git a/src/tutorials/v8.0.0/chooChooTrain/step5-completed-code.js b/src/tutorials/v8.0.0/chooChooTrain/step5-completed-code.js index 3f2219a27..5c93c6973 100644 --- a/src/tutorials/v8.0.0/chooChooTrain/step5-completed-code.js +++ b/src/tutorials/v8.0.0/chooChooTrain/step5-completed-code.js @@ -96,11 +96,11 @@ function addMountains() // Reposition the mountain groups when they move off screen. if (group1.x <= -app.screen.width) { - group1.x = app.screen.width; + group1.x += app.screen.width * 2; } if (group2.x <= -app.screen.width) { - group2.x = app.screen.width; + group2.x += app.screen.width * 2; } }); } @@ -169,7 +169,6 @@ function createMountainGroup() ) .fill({ color: colorRight }); - // Add the mountains to the stage. return graphics; } @@ -227,7 +226,7 @@ function addTrees() }); } -function createTree(width = 200, height = 250) +function createTree(width, height) { // Define the dimensions of the tree trunk. const trunkWidth = 30; @@ -240,7 +239,7 @@ function createTree(width = 200, height = 250) const crownWidthIncrement = width / crownLevels; // Define the colors of the parts. - const treeColor = 0x264d3d; + const crownColor = 0x264d3d; const trunkColor = 0x563929; const graphics = new Graphics() @@ -259,7 +258,7 @@ function createTree(width = 200, height = 250) .moveTo(-levelWidth / 2, y) .lineTo(0, y - crownLevelHeight - offset) .lineTo(levelWidth / 2, y) - .fill({ color: treeColor }); + .fill({ color: crownColor }); } return graphics; diff --git a/src/tutorials/v8.0.0/chooChooTrain/step5-content.md b/src/tutorials/v8.0.0/chooChooTrain/step5-content.md index 1152b5eb5..6e9c60f17 100644 --- a/src/tutorials/v8.0.0/chooChooTrain/step5-content.md +++ b/src/tutorials/v8.0.0/chooChooTrain/step5-content.md @@ -1 +1,86 @@ -# Adding Trees \ No newline at end of file +# Adding Trees + +Let's apply the same principles we used on the mountains step and do the same thing for the trees layer. + +## Create Tree + +Starting off with the helper function to create a tree, `createTree(width, height)` which will instantiate a Graphics element with a tree of specified width and height drawn on. We begin with drawing the trunk using Graphics API's `rect(x, y, width, height)` method and fill it out with `fill(style)` method as usual. + +```javascript +const trunkWidth = 30; +const trunkHeight = height / 4; +const trunkColor = 0x563929; +const graphics = new Graphics() + .rect(-trunkWidth / 2, -trunkHeight, trunkWidth, trunkHeight) + .fill({ color: trunkColor }); +``` + +Then for the crown, we will draw 4 stacking triangles with each triangle being thinner as we move upwards and the top triangles slightly overlapping the lower ones. Here's an example of how we can achieve that iteratively: + +```javascript +const crownHeight = height - trunkHeight; +const crownLevels = 4; +const crownLevelHeight = crownHeight / crownLevels; +const crownWidthIncrement = width / crownLevels; +const crownColor = 0x264d3d; + +for (let index = 0; index < crownLevels; index++) +{ + const y = -trunkHeight - crownLevelHeight * index; + const levelWidth = width - crownWidthIncrement * index; + const offset = index < crownLevels - 1 ? crownLevelHeight / 2 : 0; + + graphics + .moveTo(-levelWidth / 2, y) + .lineTo(0, y - crownLevelHeight - offset) + .lineTo(levelWidth / 2, y) + .fill({ color: crownColor }); +} + +return graphics; +``` + +## Set Up Trees + +Now in the `addTree()` function we can instantiate as many trees as we need to cover the screen horizontally, with a few additions as offscreen buffers. + +```javascript +const treeWidth = 200; +const y = app.screen.height - 20; +const spacing = 15; +const count = app.screen.width / (treeWidth + spacing) + 1; +const trees = []; + +for (let index = 0; index < count; index++) +{ + const treeHeight = 225 + Math.random() * 50; + const tree = createTree(treeWidth, treeHeight); + + tree.x = index * (treeWidth + spacing); + tree.y = y; + + app.stage.addChild(tree); + trees.push(tree); +} +``` + +## Animate Trees + +Then do the same animation animation setup as we did for the mountains using the application's ticker. However, we will make the rate of change (`dx`) faster than that of the mountains to simulate the trees being closer to the camera, which should make them go by faster due to the parallax effect. + +```javascript +app.ticker.add((time) => +{ + const dx = time.deltaTime * 3; + + trees.forEach((tree) => + { + tree.x -= dx; + + if (tree.x <= -(treeWidth / 2 + spacing)) + { + tree.x += count * (treeWidth + spacing) + spacing * 3; + } + }); +}); +``` \ No newline at end of file diff --git a/src/tutorials/v8.0.0/chooChooTrain/step6-code.js b/src/tutorials/v8.0.0/chooChooTrain/step6-code.js index 9fac87581..c34df40a5 100644 --- a/src/tutorials/v8.0.0/chooChooTrain/step6-code.js +++ b/src/tutorials/v8.0.0/chooChooTrain/step6-code.js @@ -97,11 +97,11 @@ function addMountains() // Reposition the mountain groups when they move off screen. if (group1.x <= -app.screen.width) { - group1.x = app.screen.width; + group1.x += app.screen.width * 2; } if (group2.x <= -app.screen.width) { - group2.x = app.screen.width; + group2.x += app.screen.width * 2; } }); } @@ -170,7 +170,6 @@ function createMountainGroup() ) .fill({ color: colorRight }); - // Add the mountains to the stage. return graphics; } @@ -228,7 +227,7 @@ function addTrees() }); } -function createTree(width = 200, height = 250) +function createTree(width, height) { // Define the dimensions of the tree trunk. const trunkWidth = 30; @@ -241,7 +240,7 @@ function createTree(width = 200, height = 250) const crownWidthIncrement = width / crownLevels; // Define the colors of the parts. - const treeColor = 0x264d3d; + const crownColor = 0x264d3d; const trunkColor = 0x563929; const graphics = new Graphics() @@ -260,7 +259,7 @@ function createTree(width = 200, height = 250) .moveTo(-levelWidth / 2, y) .lineTo(0, y - crownLevelHeight - offset) .lineTo(levelWidth / 2, y) - .fill({ color: treeColor }); + .fill({ color: crownColor }); } return graphics; diff --git a/src/tutorials/v8.0.0/chooChooTrain/step6-completed-code.js b/src/tutorials/v8.0.0/chooChooTrain/step6-completed-code.js index 99e5ded01..87b54b45f 100644 --- a/src/tutorials/v8.0.0/chooChooTrain/step6-completed-code.js +++ b/src/tutorials/v8.0.0/chooChooTrain/step6-completed-code.js @@ -97,11 +97,11 @@ function addMountains() // Reposition the mountain groups when they move off screen. if (group1.x <= -app.screen.width) { - group1.x = app.screen.width; + group1.x += app.screen.width * 2; } if (group2.x <= -app.screen.width) { - group2.x = app.screen.width; + group2.x += app.screen.width * 2; } }); } @@ -170,7 +170,6 @@ function createMountainGroup() ) .fill({ color: colorRight }); - // Add the mountains to the stage. return graphics; } @@ -228,7 +227,7 @@ function addTrees() }); } -function createTree(width = 200, height = 250) +function createTree(width, height) { // Define the dimensions of the tree trunk. const trunkWidth = 30; @@ -241,7 +240,7 @@ function createTree(width = 200, height = 250) const crownWidthIncrement = width / crownLevels; // Define the colors of the parts. - const treeColor = 0x264d3d; + const crownColor = 0x264d3d; const trunkColor = 0x563929; const graphics = new Graphics() @@ -260,7 +259,7 @@ function createTree(width = 200, height = 250) .moveTo(-levelWidth / 2, y) .lineTo(0, y - crownLevelHeight - offset) .lineTo(levelWidth / 2, y) - .fill({ color: treeColor }); + .fill({ color: crownColor }); } return graphics; diff --git a/src/tutorials/v8.0.0/chooChooTrain/step6-content.md b/src/tutorials/v8.0.0/chooChooTrain/step6-content.md index cff0feb6d..81298e4a8 100644 --- a/src/tutorials/v8.0.0/chooChooTrain/step6-content.md +++ b/src/tutorials/v8.0.0/chooChooTrain/step6-content.md @@ -1 +1,82 @@ -# Adding Ground \ No newline at end of file +# Adding Ground + +The trees are floating in space right at this point, but that's because we left some space for the ground layer. Let's fill that up together now! + +We will be making 3 layers of the ground with the bottom-most being the snow and the top two being the train track parts. + +## Snow Layer + +For this, we can simply draw a long rectangle strip across the screen and fill in the color of the snow. + +```javascript +const width = app.screen.width; +const groundHeight = 20; +const groundY = app.screen.height; +const ground = new Graphics() + .rect(0, groundY - groundHeight, width, groundHeight) + .fill({ color: 0xdddddd }); + +app.stage.addChild(ground); +``` + +## Track's Planks + +For the planks, we will be doing the same thing as we did for the trees. First by defining the dimensions of each plank and determining the amount needed to cover the width of the scene with a few additional offscreen buffers as we will be animating them as well. We will position them on top of the snow layer. + +```javascript +const trackHeight = 15; +const plankWidth = 50; +const plankHeight = trackHeight / 2; +const plankGap = 20; +const plankCount = width / (plankWidth + plankGap) + 1; +const plankY = groundY - groundHeight; +const planks = []; + +for (let index = 0; index < plankCount; index++) +{ + const plank = new Graphics() + .rect(0, plankY - plankHeight, plankWidth, plankHeight) + .fill({ color: 0x241811 }); + + plank.x = index * (plankWidth + plankGap); + app.stage.addChild(plank); + planks.push(plank); +} +``` + +Then add the animation to the planks in the similar manner to the trees animation. Again, making the rate of change (`dx`) even faster than the trees to simulate the track being closer to the camera, and hence travel faster across the screen (Parallax Effect). + +```javascript +app.ticker.add((time) => +{ + const dx = time.deltaTime * 6; + + planks.forEach((plank) => + { + plank.x -= dx; + + if (plank.x <= -(plankWidth + plankGap)) + { + plank.x += plankCount * (plankWidth + plankGap) + plankGap * 1.5; + } + }); +}); +``` + +## Track's Rail + +For the metal rail for the train's wheels to go onto, it will be another simple rectangle strip just like the ground and we will place them above the planks layer. + +```javascript +const railHeight = trackHeight / 2; +const railY = plankY - plankHeight; +const rail = new Graphics() + .rect(0, railY - railHeight, width, railHeight) + .fill({ color: 0x5c5c5c }); + +app.stage.addChild(rail); +``` + +
+ +With the layers coming together, it should sell an effect of the track being passed by. Next, we can finally move on to work on the main star of the workshop - the train! \ No newline at end of file diff --git a/src/tutorials/v8.0.0/chooChooTrain/step7-code.js b/src/tutorials/v8.0.0/chooChooTrain/step7-code.js index 503c9d7ea..fc90084bb 100644 --- a/src/tutorials/v8.0.0/chooChooTrain/step7-code.js +++ b/src/tutorials/v8.0.0/chooChooTrain/step7-code.js @@ -101,11 +101,11 @@ function addMountains() // Reposition the mountain groups when they move off screen. if (group1.x <= -app.screen.width) { - group1.x = app.screen.width; + group1.x += app.screen.width * 2; } if (group2.x <= -app.screen.width) { - group2.x = app.screen.width; + group2.x += app.screen.width * 2; } }); } @@ -174,7 +174,6 @@ function createMountainGroup() ) .fill({ color: colorRight }); - // Add the mountains to the stage. return graphics; } @@ -232,7 +231,7 @@ function addTrees() }); } -function createTree(width = 200, height = 250) +function createTree(width, height) { // Define the dimensions of the tree trunk. const trunkWidth = 30; @@ -245,7 +244,7 @@ function createTree(width = 200, height = 250) const crownWidthIncrement = width / crownLevels; // Define the colors of the parts. - const treeColor = 0x264d3d; + const crownColor = 0x264d3d; const trunkColor = 0x563929; const graphics = new Graphics() @@ -264,7 +263,7 @@ function createTree(width = 200, height = 250) .moveTo(-levelWidth / 2, y) .lineTo(0, y - crownLevelHeight - offset) .lineTo(levelWidth / 2, y) - .fill({ color: treeColor }); + .fill({ color: crownColor }); } return graphics; diff --git a/src/tutorials/v8.0.0/chooChooTrain/step7-completed-code.js b/src/tutorials/v8.0.0/chooChooTrain/step7-completed-code.js index 5303c6c89..2c3c7964d 100644 --- a/src/tutorials/v8.0.0/chooChooTrain/step7-completed-code.js +++ b/src/tutorials/v8.0.0/chooChooTrain/step7-completed-code.js @@ -101,11 +101,11 @@ function addMountains() // Reposition the mountain groups when they move off screen. if (group1.x <= -app.screen.width) { - group1.x = app.screen.width; + group1.x += app.screen.width * 2; } if (group2.x <= -app.screen.width) { - group2.x = app.screen.width; + group2.x += app.screen.width * 2; } }); } @@ -174,7 +174,6 @@ function createMountainGroup() ) .fill({ color: colorRight }); - // Add the mountains to the stage. return graphics; } @@ -232,7 +231,7 @@ function addTrees() }); } -function createTree(width = 200, height = 250) +function createTree(width, height) { // Define the dimensions of the tree trunk. const trunkWidth = 30; @@ -245,7 +244,7 @@ function createTree(width = 200, height = 250) const crownWidthIncrement = width / crownLevels; // Define the colors of the parts. - const treeColor = 0x264d3d; + const crownColor = 0x264d3d; const trunkColor = 0x563929; const graphics = new Graphics() @@ -264,7 +263,7 @@ function createTree(width = 200, height = 250) .moveTo(-levelWidth / 2, y) .lineTo(0, y - crownLevelHeight - offset) .lineTo(levelWidth / 2, y) - .fill({ color: treeColor }); + .fill({ color: crownColor }); } return graphics; @@ -369,9 +368,9 @@ function createTrainHead() const frontRadius = frontHeight / 2; // Define the dimensions of the cabin. - const containerHeight = 200; - const containerWidth = 150; - const containerRadius = 15; + const cabinHeight = 200; + const cabinWidth = 150; + const cabinRadius = 15; // Define the dimensions of the chimney. const chimneyBaseWidth = 30; @@ -379,7 +378,7 @@ function createTrainHead() const chimneyHeight = 70; const chimneyDomeHeight = 25; const chimneyTopOffset = (chimneyTopWidth - chimneyBaseWidth) / 2; - const chimneyStartX = containerWidth + frontWidth - frontRadius - chimneyBaseWidth; + const chimneyStartX = cabinWidth + frontWidth - frontRadius - chimneyBaseWidth; const chimneyStartY = -frontHeight; // Define the dimensions of the roof. @@ -387,10 +386,10 @@ function createTrainHead() const roofExcess = 20; // Define the dimensions of the door. - const doorWidth = containerWidth * 0.7; - const doorHeight = containerHeight * 0.7; - const doorStartX = (containerWidth - doorWidth) * 0.5; - const doorStartY = -(containerHeight - doorHeight) * 0.5 - doorHeight; + const doorWidth = cabinWidth * 0.7; + const doorHeight = cabinHeight * 0.7; + const doorStartX = (cabinWidth - doorWidth) * 0.5; + const doorStartY = -(cabinHeight - doorHeight) * 0.5 - doorHeight; // Define the dimensions of the window. const windowWidth = doorWidth * 0.8; @@ -412,24 +411,24 @@ function createTrainHead() // Draw the head front .roundRect( - containerWidth - frontRadius - containerRadius, + cabinWidth - frontRadius - cabinRadius, -frontHeight, - frontWidth + frontRadius + containerRadius, + frontWidth + frontRadius + cabinRadius, frontHeight, frontRadius, ) .fill({ color: 0x7f3333 }) // Draw the cabin - .roundRect(0, -containerHeight, containerWidth, containerHeight, containerRadius) + .roundRect(0, -cabinHeight, cabinWidth, cabinHeight, cabinRadius) .fill({ color: 0x725f19 }) // Draw the roof - .rect(-roofExcess / 2, containerRadius - containerHeight - roofHeight, containerWidth + roofExcess, roofHeight) + .rect(-roofExcess / 2, cabinRadius - cabinHeight - roofHeight, cabinWidth + roofExcess, roofHeight) .fill({ color: 0x52431c }) // Draw the door - .roundRect(doorStartX, doorStartY, doorWidth, doorHeight, containerRadius) + .roundRect(doorStartX, doorStartY, doorWidth, doorHeight, cabinRadius) .stroke({ color: 0x52431c, width: 3 }) // Draw the window diff --git a/src/tutorials/v8.0.0/chooChooTrain/step7-content.md b/src/tutorials/v8.0.0/chooChooTrain/step7-content.md index 9e8b4528c..c1d52da17 100644 --- a/src/tutorials/v8.0.0/chooChooTrain/step7-content.md +++ b/src/tutorials/v8.0.0/chooChooTrain/step7-content.md @@ -1 +1,155 @@ -# Adding Train Head \ No newline at end of file +# Adding Train Head + +We will start by making the head of the train first, and to do so we will be separating them into parts: + +- Cabin +- Door +- Window +- Roof +- Front +- Chimney +- Wheels + +Apart from the wheels, the parts will be drawn using a single Graphics instance. Let wrap all of the logic for this inside the already set-up `createTrainHead()` function that will return a Container element holding all the parts together. + +## Body + +The body parts includes the cabin with its overlaying door and window topped with a roof, and the protruding front with the chimney on top. + +```javascript +const frontHeight = 100; +const frontWidth = 140; +const frontRadius = frontHeight / 2; + +const cabinHeight = 200; +const cabinWidth = 150; +const cabinRadius = 15; + +const chimneyBaseWidth = 30; +const chimneyTopWidth = 50; +const chimneyHeight = 70; +const chimneyDomeHeight = 25; +const chimneyTopOffset = (chimneyTopWidth - chimneyBaseWidth) / 2; +const chimneyStartX = cabinWidth + frontWidth - frontRadius - chimneyBaseWidth; +const chimneyStartY = -frontHeight; + +const roofHeight = 25; +const roofExcess = 20; + +const doorWidth = cabinWidth * 0.7; +const doorHeight = cabinHeight * 0.7; +const doorStartX = (cabinWidth - doorWidth) * 0.5; +const doorStartY = -(cabinHeight - doorHeight) * 0.5 - doorHeight; + +const windowWidth = doorWidth * 0.8; +const windowHeight = doorHeight * 0.4; +const offset = (doorWidth - windowWidth) / 2; + +const graphics = new Graphics() + // Draw the chimney + .moveTo(chimneyStartX, chimneyStartY) + .lineTo(chimneyStartX - chimneyTopOffset, chimneyStartY - chimneyHeight + chimneyDomeHeight) + .quadraticCurveTo( + chimneyStartX + chimneyBaseWidth / 2, + chimneyStartY - chimneyHeight - chimneyDomeHeight, + chimneyStartX + chimneyBaseWidth + chimneyTopOffset, + chimneyStartY - chimneyHeight + chimneyDomeHeight, + ) + .lineTo(chimneyStartX + chimneyBaseWidth, chimneyStartY) + .fill({ color: 0x121212 }) + + // Draw the head front + .roundRect( + cabinWidth - frontRadius - cabinRadius, + -frontHeight, + frontWidth + frontRadius + cabinRadius, + frontHeight, + frontRadius, + ) + .fill({ color: 0x7f3333 }) + + // Draw the cabin + .roundRect(0, -cabinHeight, cabinWidth, cabinHeight, cabinRadius) + .fill({ color: 0x725f19 }) + + // Draw the roof + .rect(-roofExcess / 2, cabinRadius - cabinHeight - roofHeight, cabinWidth + roofExcess, roofHeight) + .fill({ color: 0x52431c }) + + // Draw the door + .roundRect(doorStartX, doorStartY, doorWidth, doorHeight, cabinRadius) + .stroke({ color: 0x52431c, width: 3 }) + + // Draw the window + .roundRect(doorStartX + offset, doorStartY + offset, windowWidth, windowHeight, 10) + .fill({ color: 0x848484 }); +``` + +## Wheels + +For the wheels, lets make a helper function that will instantiate individual wheel given a radius. This has been set up for you as the `createTrainWheel(radius)` function. + +Inside a wheel, we can split it further into parts as: + +- Wheel base +- Tyre surrounding the base +- Spokes on the base + +```javascript +const strokeThickness = radius / 3; +const innerRadius = radius - strokeThickness; + +return ( + new Graphics() + .circle(0, 0, radius) + // Draw the wheel + .fill({ color: 0x848484 }) + // Draw the tyre + .stroke({ color: 0x121212, width: strokeThickness, alignment: 1 }) + // Draw the spokes + .rect(-strokeThickness / 2, -innerRadius, strokeThickness, innerRadius * 2) + .rect(-innerRadius, -strokeThickness / 2, innerRadius * 2, strokeThickness) + .fill({ color: 0x4f4f4f }) +); +``` + +Then we can this helper function inside the `createTrainHead()` function to create the 3 wheels for the train head which include one larger wheel at the back and two standard sized ones in front. + +```javascript +const bigWheelRadius = 55; +const smallWheelRadius = 35; +const wheelGap = 5; +const wheelOffsetY = 5; + +const backWheel = createTrainWheel(bigWheelRadius); +const midWheel = createTrainWheel(smallWheelRadius); +const frontWheel = createTrainWheel(smallWheelRadius); + +backWheel.x = bigWheelRadius; +backWheel.y = wheelOffsetY; +midWheel.x = backWheel.x + bigWheelRadius + smallWheelRadius + wheelGap; +midWheel.y = backWheel.y + bigWheelRadius - smallWheelRadius; +frontWheel.x = midWheel.x + smallWheelRadius * 2 + wheelGap; +frontWheel.y = midWheel.y; +``` + +## Combine and Animate + +Now that we have the Graphics instance of the train head's body and its wheels, let add them all onto a wrapping container and then animate the spinning of the wheels before returning the container as the result. Notice here that we make the back wheel rotate proportionally slower like it logically should as it's bigger with more circumference to cover in a revolution. + +```javascript +const container = new Container(); + +container.addChild(graphics, backWheel, midWheel, frontWheel); + +app.ticker.add((time) => +{ + const dr = time.deltaTime * 0.15; + + backWheel.rotation += dr * (smallWheelRadius / bigWheelRadius); + midWheel.rotation += dr; + frontWheel.rotation += dr; +}); + +return container; +``` \ No newline at end of file diff --git a/src/tutorials/v8.0.0/chooChooTrain/step8-code.js b/src/tutorials/v8.0.0/chooChooTrain/step8-code.js index 9a6904ce5..9a2ee6d5e 100644 --- a/src/tutorials/v8.0.0/chooChooTrain/step8-code.js +++ b/src/tutorials/v8.0.0/chooChooTrain/step8-code.js @@ -101,11 +101,11 @@ function addMountains() // Reposition the mountain groups when they move off screen. if (group1.x <= -app.screen.width) { - group1.x = app.screen.width; + group1.x += app.screen.width * 2; } if (group2.x <= -app.screen.width) { - group2.x = app.screen.width; + group2.x += app.screen.width * 2; } }); } @@ -174,7 +174,6 @@ function createMountainGroup() ) .fill({ color: colorRight }); - // Add the mountains to the stage. return graphics; } @@ -232,7 +231,7 @@ function addTrees() }); } -function createTree(width = 200, height = 250) +function createTree(width, height) { // Define the dimensions of the tree trunk. const trunkWidth = 30; @@ -245,7 +244,7 @@ function createTree(width = 200, height = 250) const crownWidthIncrement = width / crownLevels; // Define the colors of the parts. - const treeColor = 0x264d3d; + const crownColor = 0x264d3d; const trunkColor = 0x563929; const graphics = new Graphics() @@ -264,7 +263,7 @@ function createTree(width = 200, height = 250) .moveTo(-levelWidth / 2, y) .lineTo(0, y - crownLevelHeight - offset) .lineTo(levelWidth / 2, y) - .fill({ color: treeColor }); + .fill({ color: crownColor }); } return graphics; @@ -372,9 +371,9 @@ function createTrainHead() const frontRadius = frontHeight / 2; // Define the dimensions of the cabin. - const containerHeight = 200; - const containerWidth = 150; - const containerRadius = 15; + const cabinHeight = 200; + const cabinWidth = 150; + const cabinRadius = 15; // Define the dimensions of the chimney. const chimneyBaseWidth = 30; @@ -382,7 +381,7 @@ function createTrainHead() const chimneyHeight = 70; const chimneyDomeHeight = 25; const chimneyTopOffset = (chimneyTopWidth - chimneyBaseWidth) / 2; - const chimneyStartX = containerWidth + frontWidth - frontRadius - chimneyBaseWidth; + const chimneyStartX = cabinWidth + frontWidth - frontRadius - chimneyBaseWidth; const chimneyStartY = -frontHeight; // Define the dimensions of the roof. @@ -390,10 +389,10 @@ function createTrainHead() const roofExcess = 20; // Define the dimensions of the door. - const doorWidth = containerWidth * 0.7; - const doorHeight = containerHeight * 0.7; - const doorStartX = (containerWidth - doorWidth) * 0.5; - const doorStartY = -(containerHeight - doorHeight) * 0.5 - doorHeight; + const doorWidth = cabinWidth * 0.7; + const doorHeight = cabinHeight * 0.7; + const doorStartX = (cabinWidth - doorWidth) * 0.5; + const doorStartY = -(cabinHeight - doorHeight) * 0.5 - doorHeight; // Define the dimensions of the window. const windowWidth = doorWidth * 0.8; @@ -415,24 +414,24 @@ function createTrainHead() // Draw the head front .roundRect( - containerWidth - frontRadius - containerRadius, + cabinWidth - frontRadius - cabinRadius, -frontHeight, - frontWidth + frontRadius + containerRadius, + frontWidth + frontRadius + cabinRadius, frontHeight, frontRadius, ) .fill({ color: 0x7f3333 }) // Draw the cabin - .roundRect(0, -containerHeight, containerWidth, containerHeight, containerRadius) + .roundRect(0, -cabinHeight, cabinWidth, cabinHeight, cabinRadius) .fill({ color: 0x725f19 }) // Draw the roof - .rect(-roofExcess / 2, containerRadius - containerHeight - roofHeight, containerWidth + roofExcess, roofHeight) + .rect(-roofExcess / 2, cabinRadius - cabinHeight - roofHeight, cabinWidth + roofExcess, roofHeight) .fill({ color: 0x52431c }) // Draw the door - .roundRect(doorStartX, doorStartY, doorWidth, doorHeight, containerRadius) + .roundRect(doorStartX, doorStartY, doorWidth, doorHeight, cabinRadius) .stroke({ color: 0x52431c, width: 3 }) // Draw the window diff --git a/src/tutorials/v8.0.0/chooChooTrain/step8-completed-code.js b/src/tutorials/v8.0.0/chooChooTrain/step8-completed-code.js index 233aed1c7..564677452 100644 --- a/src/tutorials/v8.0.0/chooChooTrain/step8-completed-code.js +++ b/src/tutorials/v8.0.0/chooChooTrain/step8-completed-code.js @@ -101,11 +101,11 @@ function addMountains() // Reposition the mountain groups when they move off screen. if (group1.x <= -app.screen.width) { - group1.x = app.screen.width; + group1.x += app.screen.width * 2; } if (group2.x <= -app.screen.width) { - group2.x = app.screen.width; + group2.x += app.screen.width * 2; } }); } @@ -174,7 +174,6 @@ function createMountainGroup() ) .fill({ color: colorRight }); - // Add the mountains to the stage. return graphics; } @@ -232,7 +231,7 @@ function addTrees() }); } -function createTree(width = 200, height = 250) +function createTree(width, height) { // Define the dimensions of the tree trunk. const trunkWidth = 30; @@ -245,7 +244,7 @@ function createTree(width = 200, height = 250) const crownWidthIncrement = width / crownLevels; // Define the colors of the parts. - const treeColor = 0x264d3d; + const crownColor = 0x264d3d; const trunkColor = 0x563929; const graphics = new Graphics() @@ -264,7 +263,7 @@ function createTree(width = 200, height = 250) .moveTo(-levelWidth / 2, y) .lineTo(0, y - crownLevelHeight - offset) .lineTo(levelWidth / 2, y) - .fill({ color: treeColor }); + .fill({ color: crownColor }); } return graphics; @@ -390,9 +389,9 @@ function createTrainHead() const frontRadius = frontHeight / 2; // Define the dimensions of the cabin. - const containerHeight = 200; - const containerWidth = 150; - const containerRadius = 15; + const cabinHeight = 200; + const cabinWidth = 150; + const cabinRadius = 15; // Define the dimensions of the chimney. const chimneyBaseWidth = 30; @@ -400,7 +399,7 @@ function createTrainHead() const chimneyHeight = 70; const chimneyDomeHeight = 25; const chimneyTopOffset = (chimneyTopWidth - chimneyBaseWidth) / 2; - const chimneyStartX = containerWidth + frontWidth - frontRadius - chimneyBaseWidth; + const chimneyStartX = cabinWidth + frontWidth - frontRadius - chimneyBaseWidth; const chimneyStartY = -frontHeight; // Define the dimensions of the roof. @@ -408,10 +407,10 @@ function createTrainHead() const roofExcess = 20; // Define the dimensions of the door. - const doorWidth = containerWidth * 0.7; - const doorHeight = containerHeight * 0.7; - const doorStartX = (containerWidth - doorWidth) * 0.5; - const doorStartY = -(containerHeight - doorHeight) * 0.5 - doorHeight; + const doorWidth = cabinWidth * 0.7; + const doorHeight = cabinHeight * 0.7; + const doorStartX = (cabinWidth - doorWidth) * 0.5; + const doorStartY = -(cabinHeight - doorHeight) * 0.5 - doorHeight; // Define the dimensions of the window. const windowWidth = doorWidth * 0.8; @@ -433,24 +432,24 @@ function createTrainHead() // Draw the head front .roundRect( - containerWidth - frontRadius - containerRadius, + cabinWidth - frontRadius - cabinRadius, -frontHeight, - frontWidth + frontRadius + containerRadius, + frontWidth + frontRadius + cabinRadius, frontHeight, frontRadius, ) .fill({ color: 0x7f3333 }) // Draw the cabin - .roundRect(0, -containerHeight, containerWidth, containerHeight, containerRadius) + .roundRect(0, -cabinHeight, cabinWidth, cabinHeight, cabinRadius) .fill({ color: 0x725f19 }) // Draw the roof - .rect(-roofExcess / 2, containerRadius - containerHeight - roofHeight, containerWidth + roofExcess, roofHeight) + .rect(-roofExcess / 2, cabinRadius - cabinHeight - roofHeight, cabinWidth + roofExcess, roofHeight) .fill({ color: 0x52431c }) // Draw the door - .roundRect(doorStartX, doorStartY, doorWidth, doorHeight, containerRadius) + .roundRect(doorStartX, doorStartY, doorWidth, doorHeight, cabinRadius) .stroke({ color: 0x52431c, width: 3 }) // Draw the window diff --git a/src/tutorials/v8.0.0/chooChooTrain/step8-content.md b/src/tutorials/v8.0.0/chooChooTrain/step8-content.md index 407ee1e28..c8ee22baa 100644 --- a/src/tutorials/v8.0.0/chooChooTrain/step8-content.md +++ b/src/tutorials/v8.0.0/chooChooTrain/step8-content.md @@ -1 +1,103 @@ -# Adding Train Carriage \ No newline at end of file +# Adding Train Carriage + +Accompanying the head, let's add a trailing carriage to complete a running train. Here we will be doing the same procedures as when we were building the head inside the new `createTrainCarriage()` function. The carriage consists of 4 parts: + +- Container +- Top Edge +- Connectors +- Wheels + +We can re-use the `createTrainWheel(radius)` function to create the two standard sized wheels which will be animated in the same manner as before. + +```javascript +const container = new Container(); + +const containerHeight = 125; +const containerWidth = 200; +const containerRadius = 15; +const edgeHeight = 25; +const edgeExcess = 20; +const connectorWidth = 30; +const connectorHeight = 10; +const connectorGap = 10; +const connectorOffsetY = 20; + +const graphics = new Graphics() + // Draw the body + .roundRect(edgeExcess / 2, -containerHeight, containerWidth, containerHeight, containerRadius) + .fill({ color: 0x725f19 }) + + // Draw the top edge + .rect(0, containerRadius - containerHeight - edgeHeight, containerWidth + edgeExcess, edgeHeight) + .fill({ color: 0x52431c }) + + // Draw the connectors + .rect(containerWidth + edgeExcess / 2, -connectorOffsetY - connectorHeight, connectorWidth, connectorHeight) + .rect( + containerWidth + edgeExcess / 2, + -connectorOffsetY - connectorHeight * 2 - connectorGap, + connectorWidth, + connectorHeight, + ) + .fill({ color: 0x121212 }); + +const wheelRadius = 35; +const wheelGap = 40; +const centerX = (containerWidth + edgeExcess) / 2; +const offsetX = wheelRadius + wheelGap / 2; + +const backWheel = createTrainWheel(wheelRadius); +const frontWheel = createTrainWheel(wheelRadius); + +backWheel.x = centerX - offsetX; +frontWheel.x = centerX + offsetX; +frontWheel.y = backWheel.y = 25; + +container.addChild(graphics, backWheel, frontWheel); + +app.ticker.add((time) => +{ + const dr = time.deltaTime * 0.15; + + backWheel.rotation += dr; + frontWheel.rotation += dr; +}); + +return container; +``` + +## Assemble Train + +With the `createTrainHead()` and `createTrainCarriage()` functions completed, let's use them to create the sections, adding them to a wrapping container, offsetting the trailing carriage to be behind the train head. We can then top it up with a little bobble up and down to simulate shaking due to the travel along the track. + +```javascript +const head = createTrainHead(); +const carriage = createTrainCarriage(); + +carriage.x = -carriage.width; + +trainContainer.addChild(head, carriage); +app.stage.addChild(trainContainer); + +const scale = 0.75; + +trainContainer.scale.set(scale); +trainContainer.x = app.screen.width / 2 - head.width / 2; + +let elapsed = 0; +const shakeDistance = 3; +const baseY = app.screen.height - 35 - 55 * scale; +const speed = 0.5; + +trainContainer.y = baseY; + +app.ticker.add((time) => +{ + elapsed += time.deltaTime; + const offset = (Math.sin(elapsed * 0.5 * speed) * 0.5 + 0.5) * shakeDistance; + + trainContainer.y = baseY + offset; +}); +``` + +We have now successfully crafted a evening scene of a training moving through the landscape with just the Graphics API. But what's the point of having a chimney without any smoke! \ No newline at end of file diff --git a/src/tutorials/v8.0.0/chooChooTrain/step9-code.js b/src/tutorials/v8.0.0/chooChooTrain/step9-code.js index 13567eca2..78f66d69a 100644 --- a/src/tutorials/v8.0.0/chooChooTrain/step9-code.js +++ b/src/tutorials/v8.0.0/chooChooTrain/step9-code.js @@ -102,11 +102,11 @@ function addMountains() // Reposition the mountain groups when they move off screen. if (group1.x <= -app.screen.width) { - group1.x = app.screen.width; + group1.x += app.screen.width * 2; } if (group2.x <= -app.screen.width) { - group2.x = app.screen.width; + group2.x += app.screen.width * 2; } }); } @@ -175,7 +175,6 @@ function createMountainGroup() ) .fill({ color: colorRight }); - // Add the mountains to the stage. return graphics; } @@ -233,7 +232,7 @@ function addTrees() }); } -function createTree(width = 200, height = 250) +function createTree(width, height) { // Define the dimensions of the tree trunk. const trunkWidth = 30; @@ -246,7 +245,7 @@ function createTree(width = 200, height = 250) const crownWidthIncrement = width / crownLevels; // Define the colors of the parts. - const treeColor = 0x264d3d; + const crownColor = 0x264d3d; const trunkColor = 0x563929; const graphics = new Graphics() @@ -265,7 +264,7 @@ function createTree(width = 200, height = 250) .moveTo(-levelWidth / 2, y) .lineTo(0, y - crownLevelHeight - offset) .lineTo(levelWidth / 2, y) - .fill({ color: treeColor }); + .fill({ color: crownColor }); } return graphics; @@ -391,9 +390,9 @@ function createTrainHead() const frontRadius = frontHeight / 2; // Define the dimensions of the cabin. - const containerHeight = 200; - const containerWidth = 150; - const containerRadius = 15; + const cabinHeight = 200; + const cabinWidth = 150; + const cabinRadius = 15; // Define the dimensions of the chimney. const chimneyBaseWidth = 30; @@ -401,7 +400,7 @@ function createTrainHead() const chimneyHeight = 70; const chimneyDomeHeight = 25; const chimneyTopOffset = (chimneyTopWidth - chimneyBaseWidth) / 2; - const chimneyStartX = containerWidth + frontWidth - frontRadius - chimneyBaseWidth; + const chimneyStartX = cabinWidth + frontWidth - frontRadius - chimneyBaseWidth; const chimneyStartY = -frontHeight; // Define the dimensions of the roof. @@ -409,10 +408,10 @@ function createTrainHead() const roofExcess = 20; // Define the dimensions of the door. - const doorWidth = containerWidth * 0.7; - const doorHeight = containerHeight * 0.7; - const doorStartX = (containerWidth - doorWidth) * 0.5; - const doorStartY = -(containerHeight - doorHeight) * 0.5 - doorHeight; + const doorWidth = cabinWidth * 0.7; + const doorHeight = cabinHeight * 0.7; + const doorStartX = (cabinWidth - doorWidth) * 0.5; + const doorStartY = -(cabinHeight - doorHeight) * 0.5 - doorHeight; // Define the dimensions of the window. const windowWidth = doorWidth * 0.8; @@ -434,24 +433,24 @@ function createTrainHead() // Draw the head front .roundRect( - containerWidth - frontRadius - containerRadius, + cabinWidth - frontRadius - cabinRadius, -frontHeight, - frontWidth + frontRadius + containerRadius, + frontWidth + frontRadius + cabinRadius, frontHeight, frontRadius, ) .fill({ color: 0x7f3333 }) // Draw the cabin - .roundRect(0, -containerHeight, containerWidth, containerHeight, containerRadius) + .roundRect(0, -cabinHeight, cabinWidth, cabinHeight, cabinRadius) .fill({ color: 0x725f19 }) // Draw the roof - .rect(-roofExcess / 2, containerRadius - containerHeight - roofHeight, containerWidth + roofExcess, roofHeight) + .rect(-roofExcess / 2, cabinRadius - cabinHeight - roofHeight, cabinWidth + roofExcess, roofHeight) .fill({ color: 0x52431c }) // Draw the door - .roundRect(doorStartX, doorStartY, doorWidth, doorHeight, containerRadius) + .roundRect(doorStartX, doorStartY, doorWidth, doorHeight, cabinRadius) .stroke({ color: 0x52431c, width: 3 }) // Draw the window diff --git a/src/tutorials/v8.0.0/chooChooTrain/step9-completed-code.js b/src/tutorials/v8.0.0/chooChooTrain/step9-completed-code.js index 98d520442..9c27cd494 100644 --- a/src/tutorials/v8.0.0/chooChooTrain/step9-completed-code.js +++ b/src/tutorials/v8.0.0/chooChooTrain/step9-completed-code.js @@ -102,11 +102,11 @@ function addMountains() // Reposition the mountain groups when they move off screen. if (group1.x <= -app.screen.width) { - group1.x = app.screen.width; + group1.x += app.screen.width * 2; } if (group2.x <= -app.screen.width) { - group2.x = app.screen.width; + group2.x += app.screen.width * 2; } }); } @@ -175,7 +175,6 @@ function createMountainGroup() ) .fill({ color: colorRight }); - // Add the mountains to the stage. return graphics; } @@ -233,7 +232,7 @@ function addTrees() }); } -function createTree(width = 200, height = 250) +function createTree(width, height) { // Define the dimensions of the tree trunk. const trunkWidth = 30; @@ -246,7 +245,7 @@ function createTree(width = 200, height = 250) const crownWidthIncrement = width / crownLevels; // Define the colors of the parts. - const treeColor = 0x264d3d; + const crownColor = 0x264d3d; const trunkColor = 0x563929; const graphics = new Graphics() @@ -265,7 +264,7 @@ function createTree(width = 200, height = 250) .moveTo(-levelWidth / 2, y) .lineTo(0, y - crownLevelHeight - offset) .lineTo(levelWidth / 2, y) - .fill({ color: treeColor }); + .fill({ color: crownColor }); } return graphics; @@ -391,9 +390,9 @@ function createTrainHead() const frontRadius = frontHeight / 2; // Define the dimensions of the cabin. - const containerHeight = 200; - const containerWidth = 150; - const containerRadius = 15; + const cabinHeight = 200; + const cabinWidth = 150; + const cabinRadius = 15; // Define the dimensions of the chimney. const chimneyBaseWidth = 30; @@ -401,7 +400,7 @@ function createTrainHead() const chimneyHeight = 70; const chimneyDomeHeight = 25; const chimneyTopOffset = (chimneyTopWidth - chimneyBaseWidth) / 2; - const chimneyStartX = containerWidth + frontWidth - frontRadius - chimneyBaseWidth; + const chimneyStartX = cabinWidth + frontWidth - frontRadius - chimneyBaseWidth; const chimneyStartY = -frontHeight; // Define the dimensions of the roof. @@ -409,10 +408,10 @@ function createTrainHead() const roofExcess = 20; // Define the dimensions of the door. - const doorWidth = containerWidth * 0.7; - const doorHeight = containerHeight * 0.7; - const doorStartX = (containerWidth - doorWidth) * 0.5; - const doorStartY = -(containerHeight - doorHeight) * 0.5 - doorHeight; + const doorWidth = cabinWidth * 0.7; + const doorHeight = cabinHeight * 0.7; + const doorStartX = (cabinWidth - doorWidth) * 0.5; + const doorStartY = -(cabinHeight - doorHeight) * 0.5 - doorHeight; // Define the dimensions of the window. const windowWidth = doorWidth * 0.8; @@ -434,24 +433,24 @@ function createTrainHead() // Draw the head front .roundRect( - containerWidth - frontRadius - containerRadius, + cabinWidth - frontRadius - cabinRadius, -frontHeight, - frontWidth + frontRadius + containerRadius, + frontWidth + frontRadius + cabinRadius, frontHeight, frontRadius, ) .fill({ color: 0x7f3333 }) // Draw the cabin - .roundRect(0, -containerHeight, containerWidth, containerHeight, containerRadius) + .roundRect(0, -cabinHeight, cabinWidth, cabinHeight, cabinRadius) .fill({ color: 0x725f19 }) // Draw the roof - .rect(-roofExcess / 2, containerRadius - containerHeight - roofHeight, containerWidth + roofExcess, roofHeight) + .rect(-roofExcess / 2, cabinRadius - cabinHeight - roofHeight, cabinWidth + roofExcess, roofHeight) .fill({ color: 0x52431c }) // Draw the door - .roundRect(doorStartX, doorStartY, doorWidth, doorHeight, containerRadius) + .roundRect(doorStartX, doorStartY, doorWidth, doorHeight, cabinRadius) .stroke({ color: 0x52431c, width: 3 }) // Draw the window @@ -605,8 +604,8 @@ function addSmokes() smokeGroup.circle(x, y, radius); } - // Fill the smoke group with a semi-transparent gray. - smokeGroup.fill({ color: 0xc9c9c9, alpha: 0.5 }); + // Fill the smoke group with gray color. + smokeGroup.fill({ color: 0xc9c9c9 }); // Position the smoke group. smokeGroup.x = baseX; @@ -635,6 +634,7 @@ function addSmokes() group.x = baseX - Math.pow(group.tick, 2) * 400; group.y = baseY - group.tick * 200; group.scale.set(Math.pow(group.tick, 0.75)); + group.alpha = 1 - Math.pow(group.tick, 0.5); }); }); } diff --git a/src/tutorials/v8.0.0/chooChooTrain/step9-content.md b/src/tutorials/v8.0.0/chooChooTrain/step9-content.md index 259c026d9..726275a87 100644 --- a/src/tutorials/v8.0.0/chooChooTrain/step9-content.md +++ b/src/tutorials/v8.0.0/chooChooTrain/step9-content.md @@ -1 +1,58 @@ -# Adding Smokes \ No newline at end of file +# Adding Smokes + +For the final touch, let's create groups of smoke particles animating in from the train chimney and out off the screen. + +## Create Smoke Groups + +First we need to create the individual groups of circular particles of varying size and position within the cluster, each group under a single Graphics instance. For the purpose of animation, we then assign a custom `tick` property to each group which will be used to reference the percentage of the animation from the chimney to the disappearing point. + +```javascript +const groupCount = 5; +const particleCount = 7; +const groups = []; +const baseX = trainContainer.x + 170; +const baseY = trainContainer.y - 120; + +for (let index = 0; index < groupCount; index++) +{ + const smokeGroup = new Graphics(); + + for (let i = 0; i < particleCount; i++) + { + const radius = 20 + Math.random() * 20; + const x = (Math.random() * 2 - 1) * 40; + const y = (Math.random() * 2 - 1) * 40; + + smokeGroup.circle(x, y, radius); + } + + smokeGroup.fill({ color: 0xc9c9c9, alpha: 0.5 }); + + smokeGroup.x = baseX; + smokeGroup.y = baseY; + smokeGroup.tick = index * (1 / groupCount); + + groups.push(smokeGroup); +} +``` + +## Animate Smokes + +As you can see, we previously offset the `tick` value on each group initially to distribute them out so that it illustrates the constant line of smokes coming out from the chimney. We then use the same technique of using the application's ticker for the animation, incrementing the `tick` value on all groups which is then used to calculate the position and scale of each. The value is modulated so that it goes back to the starting point when it finishes at the disappearing point, ie. the value will loop infinitely from 0 -> 1. + +```javascript +app.ticker.add((time) => +{ + const dt = time.deltaTime * 0.01; + + groups.forEach((group) => + { + group.tick = (group.tick + dt) % 1; + group.x = baseX - Math.pow(group.tick, 2) * 400; + group.y = baseY - group.tick * 200; + group.scale.set(Math.pow(group.tick, 0.75)); + }); +}); +``` + +And that is a wrap! \ No newline at end of file diff --git a/src/tutorials/v8.0.0/fishPond/step1-content.md b/src/tutorials/v8.0.0/fishPond/step1-content.md index d8fc43152..d399d76ed 100644 --- a/src/tutorials/v8.0.0/fishPond/step1-content.md +++ b/src/tutorials/v8.0.0/fishPond/step1-content.md @@ -2,6 +2,8 @@ Welcome to the Fish Pond workshop! +We are going to build a virtual pond and fill them with a number of colorful fishes. In the process, we will be learning about basic manipulation of [Sprites](/guides/components/sprites), [TilingSprite](https://pixijs.download/release/docs/PIXI.TilingSprite.html) and Filter, specifically the [Displacement Filter](https://pixijs.download/release/docs/PIXI.DisplacementFilter.html). + Please go through the tutorial steps at your own pace and challenge yourself using the editor on the right hand side. Here PixiJS has already been included as guided under the [Getting Started](/guides/basics/getting-started#loading-pixijs) section. Let's start off by creation a PixiJS application, initialize it, add its canvas to the DOM, and preload the required assets ahead of the subsequent steps. We will be using an asynchronous immediately invoked function expression ([IIFE](https://developer.mozilla.org/en-US/docs/Glossary/IIFE)), but you are free to switch to use promises instead.