A lightweight JavaScript game engine
git clone git://soophie.de/game-engine
1
[2
{3
"id": "js",4
"name": "JavaScript",5
"url": "/js",6
"posts": [7
{8
"id": "js.game",9
"name": "Game",10
"syntax": "Game(width, height, { fps })",11
"description": "By extending the Game class you have access to a newly created canvas with game loop, which calls the update and render methods.",12
"url": "/js/game.js",13
"code": [14
"class DinoGame extends Game {",15
"\tconstructor() {",16
"\t\tsuper(900, 600);",17
"\t\tthis.point = { x: 100, y: 100 };",18
"\t}",19
"\tupdate() {",20
"\t\tthis.point.x += 1;",21
"\t}",22
"\trender(g) {",23
"\t\tsuper.render(g); // clear screen",24
"\t\tconst {x, y} = this.point;",25
"\t\tg.fillRect(x, y, 50, 50);",26
"\t}",27
"}",28
"",29
"const game = new DinoGame();",30
"document.body.append(game.canvas);",31
"",32
"game.start();"33
]34
},35
{36
"id": "js.surface",37
"name": "Surface",38
"syntax": "Surface(x, y, width, height)",39
"description": "Represents a rectangular surface with its own update and render method.",40
"url": "/js/game.js",41
"code": [42
"class Player extends Surface {",43
"\tconstructor(x, y) {",44
"\t\tsuper(x, y, 50, 50);",45
"\t}",46
"\tupdate() {",47
"\t\tthis.rect.x += 1;",48
"\t}",49
"\trender(g) {",50
"\t\tconst {x, y, width, height} = this.rect;",51
"\t\tg.fillRect(x, y, width, height);",52
"\t}",53
"}"54
]55
},56
{57
"id": "js.group",58
"name": "Group",59
"syntax": "Group()",60
"description": "Can be used to group specific elements for update and rendering purposes.",61
"url": "/js/game.js",62
"code": [63
"const group = new Group();",64
"group.add(...);",65
"",66
"group.update();",67
"group.render(g);"68
]69
},70
{71
"id": "js.rect",72
"name": "Rect",73
"syntax": "Rect(x, y, width, height)",74
"description": "Represents a rectangular hitbox.",75
"url": "/js/game.js"76
},77
{78
"id": "js.scalecanvas",79
"name": "scaleCanvas",80
"syntax": "scaleCanvas(canvas, width, height)",81
"description": "Scales a canvas to the device related pixel ratio.",82
"url": "/js/canvas.js"83
}84
]85
},86
{87
"id": "less",88
"name": "Less",89
"url": "/less",90
"posts": [91
{92
"id": "less.normalize",93
"name": "Normalize",94
"description": "Normalizes spacing and interactive elements and adds additional alignment classes.",95
"url": "/less/normalize.less",96
"code": [97
"// Required variables (define before import)",98
"@gap: px - Space between column elements",99
"@full-width: px - Maximum width for centered container elements",100
"",101
"// Alignment classes",102
".col - Horizontal spacing",103
".row - Inverts horziontal spacing",104
".container - Align centered with horizontal spacing",105
".container-fluid - Align full width with horizontal spacing",106
"",107
"// Structure",108
".container-fluid {",109
"\t.row {",110
"\t\t.container {",111
"\t\t\t...",112
"\t\t}",113
"\t}",114
"}"115
]116
},117
{118
"id": "less.color",119
"name": "Color",120
"syntax": "@color-{color}[-(50,100,200,300,400,500,600,700,800,900)]",121
"description": "A collection of material colors inspired by the Google Material Colors.",122
"url": "/less/color.less",123
"code": [124
"@color-black;",125
"@color-teal-50;",126
"@color-blue-500;"127
]128
},129
{130
"id": "less.icons",131
"name": "Icons",132
"syntax": ".ico",133
"description": "Imports the Google Material Icons Fonts.",134
"url": "/less/icons.less",135
"code": [136
"<span class=\"ico\">close</span>"137
]138
}139
]140
}141
]141
\ No newline at end of file
24
</header>25
<main class="container-fluid">26
<div class="row">27
<div class="container">28
<h3>Overview</h3>29
<ul class="overview">30
<li>31
<p>JavaScript</p>32
<ul>33
<li>34
<a href="#game">Game</a>35
</li>36
<li>37
<a href="#group">Group</a>38
</li>39
<li>40
<a href="#surface">Surface</a>41
</li>42
<li>43
<a href="#rect">Rect</a>44
</li>45
<li>46
<a href="#scaleCanvas">scaleCanvas</a>47
</li>48
</ul>49
</li>50
<li>51
<p>CSS</p>52
<ul>53
<li>54
<a href="#normalize">Normalize</a>55
</li>56
<li>57
<a href="#color">Color</a>58
</li>59
<li>60
<a href="#icons">Icons</a>61
</li>62
</ul>63
</li>64
</ul>65
<h2>JavaScript</h2>66
<article>67
<label><a name="game">Game</a></label>68
<pre>Game(width, height, { fps })</pre>69
<p>By extending the Game class you have access to a newly created canvas with game loop, which calls the update and render methods.</p>70
<code><pre>import { Game } from 'https://git.typable.dev/std/js/game.js';</pre></code>71
<code><pre>class DinoGame extends Game {72
constructor() {73
super(900, 600);74
this.point = { x: 100, y: 100 };75
}76
update() {77
this.point.x += 1;78
}79
render(g) {80
super.render(); // screen clear81
const { x, y } = this.point;82
g.fillRect(x, y, 50, 50);83
}84
}86
const game = new DinoGame();87
document.body.append(game.canvas);89
game.start();</pre></code>90
</article>91
<article>92
<label><a name="group">Group</a></label>93
<pre>Group()</pre>94
<p>Can be used to group specific elements for update and rendering purposes.</p>95
<code><pre>import { Group } from 'https://git.typable.dev/std/js/game.js';</pre></code>96
<code><pre>const group = new Group();97
group.add(...);99
group.update();100
group.render(g);</pre></code>101
</article>102
<article>103
<label><a name="surface">Surface</a></label>104
<pre>Surface(x, y, width, height)</pre>105
<p>Represents a rectangular surface with its own update and render method.</p>106
<code><pre>import { Surface } from 'https://git.typable.dev/std/js/game.js';</pre></code>107
<code><pre>class Player extends Surface {108
constructor(x, y) {109
super(x, y, 50, 50);110
}111
update() {112
this.rect.x += 1;113
}114
render(g) {115
const { x, y, width, height } = this.rect;116
g.fillRect(x, y, width, height);117
}118
}</pre></code>119
</article>120
<article>121
<label><a name="rect">Rect</a></label>122
<pre>Rect(x, y, width, height)</pre>123
<p>Represents a rectangular hitbox.</p>124
<code><pre>import { Rect } from 'https://git.typable.dev/std/js/game.js';</pre></code>125
</article>126
<article>127
<label><a name="scaleCanvas">scaleCanvas</a></label>128
<pre>scaleCanvas(canvas, width, height)</pre>129
<p>Scales a canvas to the device related pixel ratio.</p>130
<code><pre>import { scaleCanvas } from 'https://git.typable.dev/std/js/canvas.js';</pre></code>131
<code><pre>const canvas = document.createElement('canvas');132
scaleCanvas(canvas, 900, 600); // (width, height)133
document.body.append(canvas);</pre></code>134
</article>135
<h2>CSS</h2>136
<article>137
<label><a name="normalize">Normalize</a></label>138
<p>Normalizes spacing and interactive elements and adds additional alignment classes.</p>139
<code><pre>@import url('https://git.typable.dev/std/less/normalize.less');</pre></code>140
<p>Following variables are required (insert before import):</p>141
<code><pre>@gap: px - Space between column elements142
@full-width: px - Maximum width for centered container elements</pre></code>143
<p>Additional alignment classes:</p>144
<code><pre>.col - Horizontal spacing145
.row - Inverts horziontal spacing146
.container - Align centered with horizontal spacing147
.container-fluid - Align full width with horizontal spacing</pre></code>148
<p>Structure:</p>149
<code><pre>.container-fluid {150
.row {151
.container {152
...153
}154
}155
}</pre></code>156
</article>157
<article>158
<label><a name="color">Color</a></label>159
<pre>@color-{color}[-(50,100,200,300,400,500,600,700,800,900)]</pre>160
<p>A collection of material colors inspired by the Google Material Colors.</p>161
<code><pre>@import url('https://git.typable.dev/std/less/color.less');</pre></code>162
<code><pre>@color-black;163
@color-teal-50;164
@color-blue-500;</pre></code>165
</article>166
<article>167
<label><a name="icons">Icons</a></label>168
<pre>.ico</pre>169
<p>Imports the Google Material Icons Fonts.</p>170
<code><pre>@import url('https://git.typable.dev/std/less/icons.less');</pre></code>171
<code><pre><span class="ico">close</span></pre></code>172
</article>173
</div>27
<div class="container"></div>28
</div>29
</main>30
<footer class="container-fluid">
1
import { html, escape } from './util.js';3
export const Overview = props => {4
return html`<div>5
<h3>Overview</h3>6
<ul class="overview">7
${props.map(category => {8
return html`<li>9
<p>${category.name}</p>10
<ul>11
${category.posts.map(post => {12
const goto = () => {13
const target = document.querySelector(`[data-id="${post.id}"]`);14
if(target) {15
window.scrollTo({ top: target.offsetTop - 20, behavior: 'smooth' });16
}17
};18
return html`<li>19
<a onclick=${goto}>${post.name}</a>20
</li>`;21
})}22
</ul>23
</li>`;24
})}25
</ul>26
</div>`;27
};29
export const Category = props => {30
return html`<div>31
<h2>${props.name}</h2>32
${props.posts.map(Post)}33
</div>`;34
};36
export const Post = props => {37
const url = 'https://git.typable.dev/std' + props.url;38
return html`<article data-id="${props.id}">39
<label>${props.name}</label>40
${props.syntax ? `<pre>${escape(props.syntax)}</pre>` : ''}41
<p>${props.description}</p>42
<code>43
<pre>${escape(url)}</pre>44
${CopyButton({ value: url })}45
</code>46
${props.code ? (47
html`<code>48
<pre>${escape(props.code.join('\n'))}</pre>49
${CopyButton({ value: props.code.join('\n') })}50
</code>`51
) : ''}52
</article>`;53
};55
export const CopyButton = props => {56
const copy = () => navigator.clipboard.writeText(props.value);57
return html`<div class="clipboard ico" title="Copy" onclick=${copy}>link</div>`;58
};60
export const Message = props => {61
return html`<div class="message">62
<h3>${props.message}</h3>63
${props.description ? `<p>${props.description}</p>` : ''}64
</div>`;65
}65
\ No newline at end of file
1
window.onload = function() {2
append_clipboard_button();3
};5
function append_clipboard_button() {6
for(const code of document.querySelectorAll('code')) {7
const clipboard = document.createElement('div');8
clipboard.classList.add('clipboard');9
clipboard.classList.add('ico');10
clipboard.textContent = 'link';11
clipboard.title = 'Copy';12
code.append(clipboard);1
import { Overview, Category, Message } from './component.js';14
clipboard.onclick = function() {15
navigator.clipboard.writeText(code.querySelector('pre').textContent);3
window.onload = async function() {4
const main = document.querySelector('main .container');5
try {6
const data = await fetchPostData();7
if(!data || data.length === 0) {8
throw new Error('Response body is empty: 500');9
}10
main.append(Overview(data));11
for(const category of data) {12
main.append(Category(category));13
}14
}15
catch(error) {16
console.error(error);17
main.append(Message({18
message: 'Something went wrong!',19
description: error.message20
}));21
}22
};24
async function fetchPostData() {25
const response = await fetch('/data/post.json');26
if(response.status !== 200) {27
throw new Error('Invalid status code: ' + response.status);28
}18
}29
return await response.json();30
}30
\ No newline at end of file
1
export function html(func) {2
return {3
build: function(args) {4
let template = document.createElement('template');5
template.innerHTML = func(args);6
let fragment = template.content.cloneNode(true);7
return {8
element: fragment.firstElementChild,9
render: function(parent) {10
parent.appendChild(fragment);1
export function html(strings, ...props) {2
let code = '';3
let events = [];4
let children = [];5
for(let i = 0; i < strings.length; i++) {6
code += strings[i];7
if(i < props.length) {8
if(typeof props[i] === 'function') {9
const exp = new RegExp(/on(\w+)=/, 'g');10
const match = exp.exec(strings[i]);11
if(match) {12
const [, type] = match;13
code = code.slice(0, -(type.length + 3));14
code += `data-event-ref="${events.length}"`;15
events.push({ type, handler: props[i] });16
}12
};17
continue;18
}19
if(props[i] instanceof Array) {20
for(const item of props[i]) {21
if(item instanceof HTMLElement) {22
code += `<div data-child-ref="${children.length}"></div>`;23
children.push(item);24
continue;25
}26
code += item;27
}28
continue;29
}30
if(props[i] instanceof HTMLElement) {31
code += `<div data-child-ref="${children.length}"></div>`;32
children.push(props[i]);33
continue;34
}35
code += props[i];36
}37
}38
const template = document.createElement('template');39
template.innerHTML = code;40
const fragment = template.content.cloneNode(true);41
const element = fragment.firstElementChild;42
for(const [id, {type, handler}] of Object.entries(events)) {43
let target;44
if(element.getAttribute('data-event-ref') === id) {45
target = element;46
}47
else {48
target = element.querySelector(`[data-event-ref="${id}"]`);49
}14
};50
target.addEventListener(type, handler);51
target.removeAttribute('data-event-ref');52
}53
for(const [id, child] of Object.entries(children)) {54
element.querySelector(`[data-child-ref="${id}"]`).replaceWith(child);55
}56
return element;57
}59
export function escape(string) {60
return string.replace(/&/g, '&')61
.replace(/</g, '<')62
.replace(/>/g, '>')63
.replace(/"/g, '"')64
.replace(/'/g, ''');65
}65
\ No newline at end of file
78
.overview {79
padding-left: 20px;80
margin-bottom: 50px;81
display: inline-block;83
li {84
margin-bottom: 25px;86
p {87
font-weight: 500;87
margin-bottom: 5px;88
margin-bottom: 10px;89
}91
ul {
99
a {100
color: #424242;101
font-size: 14px;102
text-decoration: underline;103
cursor: pointer;104
}105
}106
}
237
}238
}239
}241
.message {242
border: 1px solid #CCC;243
border-radius: 4px;244
padding: 20px;246
h3 {247
margin-bottom: 10px;248
}250
p {251
color: #666666;252
}253
}253
\ No newline at end of file