A lightweight JavaScript game engine
git clone git://soophie.de/game-engine
14
<script src="/js/index.js" type="module"></script>15
</head>16
<body>17
<header class="container-fluid">18
<div class="row">19
<div class="container">20
<h1>git.typable</h1>21
<p>A place for commonly used code snippets.</p>22
</div>23
</div>24
</header>25
<main class="container-fluid">26
<div class="row">27
<div class="container"></div>28
</div>29
</main>30
<footer class="container-fluid">31
<div class="row">32
<div class="container">33
<span>© typable 2021</span>34
<a href="https://typable.dev/imprint">Imprint</a>35
<a href="https://typable.dev/terms">Terms</a>36
</div>37
</div>38
</footer>17
<div id="root"></div>18
</body>19
</html>
1
import { html } from '../std/js/render.js';2
import { Overview, Category, Message } from './component.js';4
export const App = async props => {5
let body;6
try {7
const data = await fetchPostData();8
if(!data || data.length === 0) {9
throw new Error('Response body is empty: 500');10
}11
body = html`<div>12
${Overview(data)}13
${data.map(Category)}14
</div>`;15
}16
catch(error) {17
console.error(error);18
body = Message({19
message: 'Something went wrong!',20
description: error.message21
});22
}23
return html`<div>24
<header class="container-fluid">25
<div class="row">26
<div class="container">27
<h1>git.typable</h1>28
<p>A place for commonly used code snippets.</p>29
</div>30
</div>31
</header>32
<main class="container-fluid">33
<div class="row">34
<div class="container">${body}</div>35
</div>36
</main>37
<footer class="container-fluid">38
<div class="row">39
<div class="container">40
<span>© typable ${new Date().getFullYear()}</span>41
<a href="https://typable.dev/imprint">Imprint</a>42
<a href="https://typable.dev/terms">Terms</a>43
</div>44
</div>45
</footer>46
</div>`;47
};49
async function fetchPostData() {50
const response = await fetch('/data/post.json');51
if(response.status !== 200) {52
throw new Error('Invalid status code: ' + response.status);53
}54
return await response.json();55
}55
\ No newline at end of file
1
import { html, escape } from './util.js';1
import { html, escape } from '../std/js/render.js';3
export const Message = props => {4
return html`<div class="message">5
<h3>${props.message}</h3>6
${props.description ? `<p>${props.description}</p>` : ''}7
</div>`;8
}10
export const Overview = props => {11
return html`<div>
62
export const CopyButton = props => {63
const copy = () => navigator.clipboard.writeText(props.value);64
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 file65
};65
\ No newline at end of file
1
import { Overview, Category, Message } from './component.js';1
import { App } from './app.js';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
}29
return await response.json();30
}30
\ No newline at end of file4
document.querySelector('#root').append(await App());5
};5
\ No newline at end of file
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
}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
}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
1
* {2
margin: 0;3
padding: 0;4
font-family: 'Roboto', sans-serif;5
box-sizing: border-box;7
&:hover,8
&:focus {9
outline: none;10
}11
}13
html {14
position: relative;15
overflow-x: hidden;16
min-height: 100vh;17
}19
body {20
width: 100%;21
font-size: 14px;22
min-height: 100vh;23
}25
img {26
pointer-events: none;27
user-select: none;28
}30
input {31
-webkit-appearance: none;32
}34
button {35
user-select: none;36
cursor: pointer;37
}39
.col-auto {40
padding-left: calc(@grid-gutter-width / 2);41
padding-right: calc(@grid-gutter-width / 2);42
}44
.hidden {45
display: none !important;46
}
1
@import 'variable';2
@import 'normalize';1
@import './variable.less';2
@import '../std/less/normalize.less';3
@import '../std/less/color.less';4
@import '../std/less/icons.less';4
@import url('https://fonts.googleapis.com/icon?family=Material+Icons');6
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700');7
@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;500');9
* {9
color: #212121;10
color: @color-gray-900;11
font-family: 'Inter';12
}13
.container-fluid {14
.col-auto;15
}17
.container {18
max-width: @width;19
margin: 0 auto;20
.col-auto;21
}23
.row {24
margin-left: calc(-@grid-gutter-width / 2);25
margin-right: calc(-@grid-gutter-width / 2);26
}28
.ico {29
font-family: 'Material Icons';30
font-weight: normal;31
font-style: normal;32
font-size: 24px;33
display: inline-block;34
line-height: 1;35
text-transform: none;36
letter-spacing: normal;37
word-wrap: normal;38
white-space: nowrap;39
direction: ltr;40
-webkit-font-smoothing: antialiased;41
text-rendering: optimizedLegibility;42
-moz-osx-font-smoothing: grayscale;43
font-feature-settings: 'liga';44
user-select: none;45
}14
header {15
padding: 50px 0;16
margin-bottom: 20px;
23
p {24
font-size: 16px;58
color: #666666;25
color: @color-gray-700;26
}27
}
30
margin-bottom: 100px;32
h2 {66
color: #212121;33
font-size: 24px;34
margin-top: 80px;35
margin-bottom: 30px;36
}38
h3 {73
color: #212121;39
font-size: 20px;40
margin-bottom: 20px;41
}
61
}63
a {99
color: #424242;64
color: @color-gray-800;65
font-size: 14px;66
text-decoration: underline;67
cursor: pointer;
70
}72
article {108
border: 1px solid #CCC;73
border: 1px solid @color-gray-300;74
border-radius: 4px;75
padding: 20px;76
margin-bottom: 20px;
89
font-family: 'Roboto Mono';90
line-height: 19px;91
padding: 6px 6px;127
border: 1px solid #EEE;92
border: 1px solid @color-gray-200;93
border-radius: 2px;94
margin-bottom: 10px;95
overflow-x: auto;
100
}102
&::-webkit-scrollbar-track {138
background: #EEE;103
background: @color-gray-200;104
}106
&::-webkit-scrollbar-thumb {142
background: #AAA;107
background: @color-gray-400;109
&:hover {145
background: #888;110
background: @color-gray-500;111
}112
}113
}
115
p {116
font-size: 14px;117
font-weight: 400;153
color: #666666;118
color: @color-gray-700;119
line-height: 20px;120
margin-bottom: 15px;121
}
123
code {124
display: block;125
position: relative;161
background: #F5F5F5;126
background: @color-gray-100;127
border-radius: 2px;128
margin-bottom: 15px;129
overflow-x: hidden;
142
}144
&::-webkit-scrollbar-track {180
background: #EEE;145
background: @color-gray-200;146
}148
&::-webkit-scrollbar-thumb {184
background: #AAA;149
background: @color-gray-400;151
&:hover {187
background: #888;152
background: @color-gray-500;153
}154
}155
}
161
width: 30px;162
height: 30px;163
border-radius: 50%;199
color: #888888;164
color: @color-gray-500;165
display: grid;166
place-content: center;167
cursor: pointer;169
&:hover {205
color: #424242;170
color: @color-gray-800;171
}172
}173
}
189
span {190
flex: 1;191
align-self: flex-start;227
color: #666666;192
color: @color-gray-700;193
}195
a {196
margin-left: 15px;232
color: #666666;197
color: @color-gray-700;199
&:hover {235
color: #424242;200
color: @color-gray-800;201
}202
}203
}205
.message {241
border: 1px solid #CCC;206
border: 1px solid @color-gray-300;207
border-radius: 4px;208
padding: 20px;
212
}214
p {250
color: #666666;215
color: @color-gray-700;216
}217
}217
\ No newline at end of file
1
@grid-gutter-width: 40px;2
@width: 700px;1
@gap: 20px;2
@full-width: 700px;2
\ No newline at end of file
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] });2
try {3
let code = '';4
let events = [];5
let children = [];6
for(let i = 0; i < strings.length; i++) {7
code += strings[i];8
if(i < props.length) {9
if(typeof props[i] === 'function') {10
const exp = new RegExp(/on(\w+)=/, 'g');11
const match = exp.exec(strings[i]);12
if(match) {13
const [, type] = match;14
code = code.slice(0, -(type.length + 3));15
code += `data-event-ref="${events.length}"`;16
events.push({ type, handler: props[i] });17
}18
continue;19
}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;20
if(props[i] instanceof Array) {21
for(const item of props[i]) {22
if(item instanceof HTMLElement) {23
code += `<div data-child-ref="${children.length}"></div>`;24
children.push(item);25
continue;26
}27
code += item;28
}26
code += item;29
continue;30
}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;31
if(props[i] instanceof HTMLElement) {32
code += `<div data-child-ref="${children.length}"></div>`;33
children.push(props[i]);34
continue;35
}36
code += props[i];37
}35
code += props[i];38
}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;39
const template = document.createElement('template');40
template.innerHTML = code;41
const fragment = template.content.cloneNode(true);42
const element = fragment.firstElementChild;43
for(const [id, {type, handler}] of Object.entries(events)) {44
let target;45
if(element.getAttribute('data-event-ref') === id) {46
target = element;47
}48
else {49
target = element.querySelector(`[data-event-ref="${id}"]`);50
}51
target.addEventListener(type, handler);52
target.removeAttribute('data-event-ref');53
}47
else {48
target = element.querySelector(`[data-event-ref="${id}"]`);54
for(const [id, child] of Object.entries(children)) {55
element.querySelector(`[data-child-ref="${id}"]`).replaceWith(child);56
}50
target.addEventListener(type, handler);51
target.removeAttribute('data-event-ref');57
return element;58
}53
for(const [id, child] of Object.entries(children)) {54
element.querySelector(`[data-child-ref="${id}"]`).replaceWith(child);59
catch(error) {60
console.error(error);61
}56
return element;62
}64
export function escape(string) {