Skip to content

Commit

Permalink
docs: Add html guidance
Browse files Browse the repository at this point in the history
  • Loading branch information
eonarheim committed Apr 29, 2024
1 parent 74e1038 commit c39cfa0
Show file tree
Hide file tree
Showing 4 changed files with 230 additions and 0 deletions.
136 changes: 136 additions & 0 deletions site/docs/05-user-interface/01-html.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
---
title: HTML, CSS, and JavaScript
slug: /html
section: User Interface
---

HTML is super great at building UI's for the web, there are many popular front end frameworks and tool kits that will deliver highly polished UIs, so we recommend that first.

Please consider html based first, but if the UI makes more sense in-canvas look to [screen elements](/docs/screen-elements).

Advantages of HTML
* Accessibility
* Built in keyboard/pointer events
* Powerful layout engine
* CSS styling

## Using HTML with Excalibur's Canvas

A common technique for building UI wit canvas based games is overlaying HTML elements over the canvas.

Check out this full demo [excaliburjs/sample-html](https://github.com/excaliburjs/sample-html)

```html
<!DOCTYPE html>
<html lang="en">
<head>
<style>
html,body {
padding: 0;
margin: 0;
background-color: black;
}
/* optionally center the game */
#game {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.menu {
position: absolute;
top: 10px;
left: 10px;
}
</style>
</head>
<body>
<main>
<canvas id="game"></canvas>
<div class="menu">
<button>Add Unit</button>
</div>
</main>
</body>
</html>
```

When positioning elements over the Excalibur canvas, it is important to set `pointer-events: none` on elements you want to pass through to Excalibur. For elements you want to receive clicks/taps be sure to set `pointer-events: all`.


## Positioning HTML Elements with Excalibur

Excalibur provides an API for converting game positions in world/screen space to page coordinates.

```typescript
const engine = new ex.Engine({...});

// Excalibur camera is centered at (100, 100)
engine.currentScene.camera.pos = ex.vec(100, 100);

// Finds the absolution page position that corresponds to the excalibur position (100, 100)
const pagePositionFromWorld = engine.screen.worldToPageCoordinates(ex.vec(100, 100));

// Finds the absolute page position that corresponds to the top left of the excalibur canvas, screen (0, 0)
const pagePositionFromScreen = engine.screen.screenToPageCoordinates(ex.vec(0, 0));

// Use pagePositions in page coordinates to position HTML elements
// Setting CSS variables is a convenient way to do this
document.documentElement.style.setProperty('--pointer-x', evt.pagePos.x.toString() + 'px');
document.documentElement.style.setProperty('--pointer-y', evt.pagePos.y.toString() + 'px');
```

Then to position any elements reference the CSS variables in your CSS

```css
.menu {
position: absolute;
/* position menu on click */
left: var(--pointer-x);
top: var(--pointer-y);
}
```

## Scaling between CSS & Excalibur Pixels

It is useful in CSS to scale your elements to match the Excalibur game canvas, this is done by calculating the ratio between between Excalibur pixels and browser pixels.

```typescript
const calculateExPixelConversion = (screen: ex.Screen) => {
const origin = screen.worldToPageCoordinates(Vector.Zero);
const singlePixel = screen.worldToPageCoordinates(vec(1, 0)).sub(origin);
const pixelConversion = singlePixel.x;
document.documentElement.style.setProperty('--pixel-conversion', pixelConversion.toString());
}

// Update pixel conversion on resize
game.screen.events.on('resize', () => calculateExPixelConversion(game.screen));

// Set initial conversion
game.start().then(() => {
calculateExPixelConversion(game.screen);
});
```


One of the lowest effort options is to apply a CSS transform to the container of your HTML UI based on the excalibur pixel conversion.

```css
.excalibur-scale {
/* transform from the top left of the element */
transform-origin: 0 0;
/* scale the ui */
transform: scale(var(--pixel-conversion), var(--pixel-conversion));
}
```

Another option is to manually apply conversion to specific CSS properties.

```css
.text {
/* Convert to excalibur 24px from page 24px */
font-size: calc(24px * var(--pixel-conversion));
}
```
52 changes: 52 additions & 0 deletions site/docs/05-user-interface/02-screen-elements.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
title: Screen Elements
slug: /screen-elements
section: User Interface
---

Please consider [html based first](#html-based-ui) but if the UI makes more sense in-canvas look to [screen elements](#screen-elements).

## Screen Elements

In Excalibur, if you want to display something like a HUD element or UI element inside the Excalibur canvas, you can create an instance of [[ScreenElement]]. A screen element has the following semantics that differ from a regular [actor](/docs/actors):

- They automatically [capture pointer events](/docs/input#actor-pointer-events)
- They do not participate in collisions
- They appear above all "normal" actors in a [scene](/docs/scenes)
- Invoking [[ScreenElement.contains]] will check against [screen coordinates](/docs/engine#screen-coordinates) by default.

Other than that, they are the same as normal actors where you can assign drawings, perform actions, etc.

```ts
import * as ex from 'excalibur'
import Resources from './resources'

class StartButton extends ex.ScreenElement {
constructor() {
super({
x: 50,
y: 50,
})
}

onInitialize() {
this.graphics.add('idle', Resources.StartButtonBackground)
this.graphics.add('hover', Resources.StartButtonHovered)

this.on('pointerup', () => {
alert("I've been clicked")
})

this.on('pointerenter', () => {
this.graphics.show('hover')
})

this.on('pointerleave', () => {
this.graphics.show('idle')
})
}
}

game.add(new StartButton())
game.start()
```
34 changes: 34 additions & 0 deletions site/docs/05-user-interface/03-web-fonts.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
title: Web Fonts
slug: /web-fonts
section: User Interface
---

When building UIs in order to prevent a flash of unstyled content (a moment of default font/styling before the font loads) Excalibur has a built in resource type to help with that.

## Using FontSource

Using a [[FontSource]] will ensure the font is loaded and the font face is ready before the game starts!

```typescript
const fontSource = new ex.FontSource('/my-font.ttf', 'My Font')
loader.addResource(fontSource)

game.start(loader).then(() => {
const font = fontSource.toFont() // returns ex.Font
})
```

Font options can be defined either at the source or at the `toFont()` call. If defined in both, `toFont(options)` will
override the options in the [[FontSource]].

```typescript
const fontSource = new ex.FontSource('/my-font.ttf', 'My Font', {
filtering: ex.ImageFiltering.Pixel,
size: 16, // set a default size
})
const font = fontSource.toFont({
// override just the size
size: 20,
})
```
8 changes: 8 additions & 0 deletions site/docs/05-user-interface/_category_.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"label": "User Interface",
"position": 5,
"link": {
"type": "generated-index",
"description": "5 minutes to learn the most important Excalibur.js concepts."
}
}

0 comments on commit c39cfa0

Please sign in to comment.