New article
All checks were successful
Deploy / build-and-publish (push) Successful in 1m48s
Deploy / deploy (push) Successful in 13s

This commit is contained in:
2024-10-08 02:15:02 +03:00
parent 4d15eb0112
commit 93f92efefc
17 changed files with 914 additions and 11 deletions

View File

@@ -17,7 +17,9 @@ export const highlighter = await createHighlighter({
'mdx', 'mdx',
'bash', 'bash',
'svelte', 'svelte',
'yaml' 'yaml',
'lua',
'python'
] ]
}); });

View File

@@ -0,0 +1,45 @@
<script lang="ts">
let isHovered = false;
let x: number;
let y: number;
function mouseOver(event: MouseEvent) {
isHovered = true;
x = event.pageX + 5;
y = event.pageY + 5;
}
function mouseMove(event: MouseEvent) {
x = event.pageX + 5;
y = event.pageY + 5;
}
function mouseLeave() {
isHovered = false;
}
</script>
<span
on:mouseover={mouseOver}
on:mouseleave={mouseLeave}
on:mousemove={mouseMove}
>
<slot />
</span>
{#if isHovered}
<div
style="top: {y}px; left: {x}px;"
class="tooltip"
>
<slot name="title"></slot>
</div>
{/if}
<style>
.tooltip {
position: absolute;
padding: 8px 16px;
background-color: theme(colors.slate.900);
border: 1px solid theme(colors.slate.500);
border-radius: 8px;
}
</style>

View File

@@ -13,28 +13,49 @@ import Navbar from "$lib/components/Navbar.svelte";
</article> </article>
<style lang="postcss"> <style lang="postcss">
:global(h1) { :global(article h1) {
@apply text-5xl font-semibold my-6; @apply text-5xl font-semibold my-6;
} }
:global(h2) { :global(article h2) {
@apply text-3xl my-5; @apply text-3xl my-5;
} }
:global(h3) { :global(article h3) {
@apply text-2xl my-4; @apply text-2xl my-4;
} }
:global(h4) { :global(article h4) {
@apply text-xl my-3; @apply text-xl my-3;
} }
:global(pre) { :global(article ul) {
margin-block: 16px;
}
:global(article ul li) {
list-style-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%221em%22%20height%3D%221em%22%20viewBox%3D%220%200%2024%2024%22%3E%3Cpath%20fill%3D%22%23C3E88D%22%20d%3D%22M12%2021q-.425%200-.712-.288T11%2020v-5.6l-3.95%203.975q-.3.3-.712.3t-.713-.3t-.3-.712t.3-.713L9.6%2013H4q-.425%200-.712-.288T3%2012t.288-.712T4%2011h5.6L5.625%207.05q-.3-.3-.3-.712t.3-.713t.713-.3t.712.3L11%209.6V4q0-.425.288-.712T12%203t.713.288T13%204v5.6l3.95-3.975q.3-.3.713-.3t.712.3t.3.713t-.3.712L14.4%2011H20q.425%200%20.713.288T21%2012t-.288.713T20%2013h-5.6l3.975%203.95q.3.3.3.713t-.3.712t-.712.3t-.713-.3L13%2014.4V20q0%20.425-.288.713T12%2021%22%2F%3E%3C%2Fsvg%3E");
list-style-position: outside;
margin-left: 16px;
margin-bottom: 8px;
}
:global(article ul li::marker) {
margin-right: 16px;
}
:global(article img, article video) {
margin-block: 24px;
border-radius: 8px;
overflow: hidden;
}
:global(article pre) {
@apply p-4 !my-4 overflow-auto rounded-lg relative; @apply p-4 !my-4 overflow-auto rounded-lg relative;
max-width: calc(100vw - 16px); max-width: calc(100vw - 16px);
} }
:global(pre code) { :global(article pre code) {
min-width: calc(100% - 16px); min-width: calc(100% - 16px);
@apply p-0 relative inline-block; @apply p-0 relative inline-block;
@@ -43,20 +64,20 @@ import Navbar from "$lib/components/Navbar.svelte";
} }
} }
:global(code) { :global(article code) {
background: #263238; background: #263238;
color: #C3E88D; color: #C3E88D;
@apply px-2 py-0.5 rounded; @apply px-2 py-0.5 rounded;
} }
:global(code .highlighted) { :global(article code .highlighted) {
background: #334752; background: #334752;
display: inline-block; display: inline-block;
width: calc(100% + 32px); width: calc(100% + 32px);
@apply px-4 -mx-4; @apply px-4 -mx-4;
} }
:global(code .diff.add) { :global(article code .diff.add) {
background: #526a4f; background: #526a4f;
display: inline-block; display: inline-block;
width: calc(100% + 48px); width: calc(100% + 48px);

View File

@@ -1,7 +1,12 @@
import {metadata as htmlInCssMetadata} from "./html-in-css/+page.svx" import {metadata as htmlInCssMetadata} from "./html-in-css/+page.svx"
import {metadata as thisBlogMetadata} from "./this-blog/+page.svx" import {metadata as thisBlogMetadata} from "./this-blog/+page.svx"
import {metadata as shortsMetadata} from "./shorts/+page.svx"
const posts = [ const posts = [
{
href: '/blog/shorts',
...shortsMetadata
},
{ {
href: '/blog/this-blog', href: '/blog/this-blog',
...thisBlogMetadata ...thisBlogMetadata

View File

@@ -0,0 +1,361 @@
---
title: "Как я написал программу шортсогенератор"
date: "2024-10-08"
---
<script>
import ArticleTitle from "$lib/components/ArticleTitle.svelte"
import MaterialSymbolsErrorCircleRoundedOutline from '~icons/material-symbols/error-circle-rounded-outline';
import Tooltip from "$lib/components/Tooltip.svelte"
</script>
<ArticleTitle metadata={metadata} />
На днях мне вечером позвонил давний друг.
Я на тот момент уже успел уснуть, поэтому его звонок застал меня врасплох, особенно учитывая,
что он начал пытаться мне объяснить как заработать на создании шортсов на Youtube.
Он увидел [такое видео](https://www.youtube.com/shorts/iMBJH3Eo8M0) и вбил его в [Socialblade](https://socialblade.com/youtube/channel/UCBcGha5FjnmsaOhMJJeXtIQ).
У автора приблизительный доход -- **1.5 \- 24 (к$/мес)**.
<img
src="/images/blog/shorts/ball-escape.avif"
alt="Скриншот видео, которое мы будем имитировать"
class="max-h-[400px] mx-auto"
/>
Я давно разочаровался в методах заработка, которые вращаются вокруг того, что надо стать знаменитым в интернете.
Но я сказал, что посмотрю и поразбираюсь в том, как можно было бы сделать такое видео,
но Youtube каналом заниматься не буду.
Если честно, то я не очень, то и рассчитывал всерьез этим заниматься, но я уже проснулся,
спать было неохота и я уселся думать над тем, как можно повторить такую симуляцию.
Друг ко мне обратился, потому что знал, что я когда-то <Tooltip><MaterialSymbolsErrorCircleRoundedOutline
class='inline'
/><div slot="title">{new Date().getFullYear()-2015} лет назад</div></Tooltip> [делал игры](https://games.kopyl.dev).
Чтобы не терять время на изучение нового игрового движка, я решил освежить свои навыки Lua.
Музыкальных навыков у меня нет, но я знаю про [формат Midi](https://ru.wikipedia.org/wiki/MIDI) и догадываюсь, что получить
нужную аудиодорожку можно, если воспроизводить ноты синхронно с ударами мяча.
Возвращаясь к графической части, инструмент, на который в младенчестве пал мой выбор -- [LÖVE](https://love2d.org).
Фреймворк с очень низким порогом вхождения, позволяющий создавать 2D игры на Lua.
Единственная его проблема -- сообщество очень любит "велосипеды".
Библиотек по пальцам пересчитать и пакета способного воспроизводить Midi нет и в помине.
Но я решил решать проблемы по порядку и взялся за то, что точно могу сделать -- графику.
## Графика
Создадим файл `main.lua` и начнем с перечисления констант, которые будут задавать поведение симуляции:
```lua
local windowW = 720
local windowH = 720
local centerX = windowW / 2
local centerY = windowH / 2
local circleCount = 6
local radiusIncrement = 50
local circleResolution = 36
local gravity = 180
local circles = {}
local ball = {
x = centerX,
y = centerY,
radius = 7,
speedX = 100 * love.math.random() - 50,
speedY = 100 * love.math.random() - 50,
color = {1, 1, 1}
}
```
Любая игра на LÖVE разделена на 3 основные функции:
- `love.load()` -- выполняется единожды при запуске игры,
- `love.update()` -- выполняется каждый кадр,
- `love.draw()` -- выполняется каждый кадр, отвечает за отрисовку графики.
### `love.load()`
В функции `love.load()` создадим нужные нам окружности.
Они состоят из арок и, чтобы арки на внешних окружностях не были слишком большими,
умножаем разрешение окружностей на их номер по порядку от центра.
```lua
function love.load()
love.window.setMode(720, 720, {highdpi = true})
for i = 1, circleCount do
local radius = radiusIncrement * i
local angleStart = 0
local angleEnd = 360
local segments = {}
local boostedResolution = circleResolution / i
for angle = angleStart, angleEnd - boostedResolution, boostedResolution do
local angleRadStart = math.rad(angle)
local angleRadEnd = math.rad(angle + boostedResolution)
local startX = centerX + radius * math.cos(angleRadStart)
local startY = centerY + radius * math.sin(angleRadStart)
local endX = centerX + radius * math.cos(angleRadEnd)
local endY = centerY + radius * math.sin(angleRadEnd)
segments[#segments + 1] = {
startX = startX,
startY = startY,
endX = endX,
endY = endY,
angleStart = angle,
angleEnd = angle + boostedResolution,
isAlive = true
}
end
circles[i] = {
color = {
love.math.random(),
love.math.random(),
love.math.random()
},
segments = segments
}
end
end
```
Получаем структуру:
```lua
circles = {
{
color = rgba,
segments = {
{
startX = number,
startY = number,
endX = number,
endY = number,
angleStart = number,
angleEnd = number,
isAlive = boolean
},
...
}
},
...
}
-- Я понятия не имею как описывать типы в Lua
```
### `love.update()`
Здесь мы опишем то, как мяч изменяет свою скорость с течением времени,
как от этого меняются его координаты и опишем логику столкновения с окружностями.
Для этого сначала опишем функцию поиска пересечения окружности и линии.
В роли окружности будет наш мячик, а в роли линии сегмент круга.
Конечно сегмент на самом деле не прямая линия, а арка, но никто не заметит, что при столкновении он ведет себя как линия.
```lua
function isCollidingWithLine(ball, arc)
local function pointToLineDistance(px, py, ax, ay, bx, by)
local abx = bx - ax
local aby = by - ay
local apx = px - ax
local apy = py - ay
local ab2 = abx * abx + aby * aby
local ap_ab = apx * abx + apy * aby
local t = ap_ab / ab2
t = math.max(0, math.min(t, 1))
local nearestX = ax + t * abx
local nearestY = ay + t * aby
local distX = px - nearestX
local distY = py - nearestY
return math.sqrt(distX * distX + distY * distY), nearestX, nearestY
end
local distToSegment, nearestX, nearestY = pointToLineDistance(
ball.x, ball.y,
arc.startX, arc.startY,
arc.endX, arc.endY
)
if distToSegment < ball.radius then
return true, nearestX, nearestY
end
return false
end
```
Может произойти, что наш шарик одновременно пересечет 2 сегмента.
Если никак от этого не защититься, то получится, что он 2 раза за кадр поменяет свое направление на 180º
и как будто пролетит сквозь препятствие.
Поэтому храним информацию о том сталкивался ли уже мячик с чем-то и один раз в кадр даем ему это сделать.
```lua
function love.update(dt)
ball.collided = false -- [!code highlight]
-- Гравитация
ball.speedY = ball.speedY + gravity * dt
ball.speedY = math.min(ball.speedY, maxSpeed)
ball.speedX = math.min(ball.speedX, maxSpeed)
ball.x = ball.x + ball.speedX * dt
ball.y = ball.y + ball.speedY * dt
-- Проверяем столкновения мячика и окружностей
for i, circle in ipairs(circles) do
for j, arc in ipairs(circle.segments) do
if arc.isAlive and not ball.collided then -- [!code highlight]
local collided, impactX, impactY = isCollidingWithLine(ball, arc)
if collided then
arc.isAlive = false
ball.collided = true -- [!code highlight]
-- Рассчитываем нормаль к точке столкновения
local normalX = ball.x - impactX
local normalY = ball.y - impactY
local normalLength = math.sqrt(normalX * normalX + normalY * normalY)
normalX = normalX / normalLength
normalY = normalY / normalLength
-- Отражаем скорость относительно нормали
local dotProduct = ball.speedX * normalX + ball.speedY * normalY
ball.speedX = ball.speedX - 2 * dotProduct * normalX
ball.speedY = ball.speedY - 2 * dotProduct * normalY
-- Увеличиваем скорость после столкновения, так веселее
ball.speedX = ball.speedX * 1.1
ball.speedY = ball.speedY * 1.1
end
end
end
end
end
```
### `love.draw()`
Ну и наконец отрисовка
```lua
function love.draw()
-- Окружности
for i, circle in ipairs(circles) do
for _, arc in ipairs(circle.segments) do
if arc.isAlive then
love.graphics.setColor(circle.color)
love.graphics.arc(
"line",
"open",
windowW/2,
windowH/2,
radiusIncrement * i,
math.rad(arc.angleStart),
math.rad(arc.angleEnd)
)
end
end
end
-- Мячик
love.graphics.setColor(ball.color)
love.graphics.circle("fill", ball.x, ball.y, ball.radius)
end
```
Добавим немного эффектов и получаем такой результат:
<iframe
style='width: 100%; aspect-ratio: 1; border-radius: 8px; margin-block: 16px; cursor: pointer;'
src="/game/index.html"
></iframe>
Клик по окну перезапускает "игру".
## Звук
Раз возможности воспроизводить Midi файлы из самой программы у нас нет, напишем отдельный скрипт на Python,
который будет за это отвечать. Благо в экосистеме Питона нет недостатка готовых библиотек.
Принцип в том, что графическая часть будет писать в файл время каждого удара, а скрипт на питоне можно
будет потом запустить и получить звук, который будет синхронизирован с видеорядом, если мы таковой
записали с экрана.
Midi файл представляет собой очень плотно упакованный бинарный файл, который просто так, как JSON не откроешь,
но если его расшифровать то он состоит из событий, они делятся на несколько типов:
- Note Off,
- Note On,
- Poly Key Pressure,
- Controller Change,
- Program Change,
- Channel Pressure,
- Pitch Bend.
Нас интересует "Note On". Их мы будем поочередно задерживать до следующего таймстампа,
а остальные события будем просто пропускать без задержки.
```python
import time
import mido
timestamps = [
# Вставляем сюда список таймстампов
]
midi_file = mido.MidiFile('midis/tetris.mid')
print(mido.get_output_names()) # Выводим список Midi устройств
port = mido.open_output('IAC Driver Bus 1') # Вводим сюда имя нужного нам устройства
def play_midi_file(midi_file, timestamps):
current_time = 0
index = 0
for msg in midi_file.play():
print(msg)
if (msg.type == 'note_on'):
timestamp = timestamps[index]
time.sleep(timestamp - current_time)
current_time = timestamp
index += 1
port.send(msg) # Воспроизводим звук
play_midi_file(midi_file, timestamps)
```
Этот скрипт можно воспринимать как будто мы подключили к компьютеру клавиатуру и скрипт играет на
ней выбранную нами песню в нужном нам темпе.
Проблема в том, что если вы подключите физическую клавиатуру к компьютеру и начнете нажимать по клавишам,
то вы ничего не услышите. Компьютеру неоткуда знать, что делать с этим вводом.
Чтобы услышать что-то при запуске скрипта, надо чтобы на компьютере был запущен виртуальный синтезатор.
В моем случае это Garage Band.
![](/images/blog/shorts/garage-band.avif)
## Результат
Теперь можно взять запись экрана первой программы и совместить ее с записью звука в вашем любимом видеоредакторе.
На выходе получаем такой довольно залипательный шортс/рилс/тикток:
<video
class="max-h-[400px] mx-auto"
src="/images/blog/shorts/tetris.webm"
controls
></video>

View File

@@ -1,5 +1,5 @@
--- ---
title: "Как сделан этот блог?" title: "Как сделать блог на Svelte + MDX?"
date: "2024-10-04" date: "2024-10-04"
--- ---

BIN
static/game/game.data Normal file

Binary file not shown.

289
static/game/game.js Normal file
View File

@@ -0,0 +1,289 @@
var Module;
if (typeof Module === 'undefined') Module = eval('(function() { try { return Module || {} } catch(e) { return {} } })()');
if (!Module.expectedDataFileDownloads) {
Module.expectedDataFileDownloads = 0;
Module.finishedDataFileDownloads = 0;
}
Module.expectedDataFileDownloads++;
(function() {
var loadPackage = function(metadata) {
var PACKAGE_PATH;
if (typeof window === 'object') {
PACKAGE_PATH = window['encodeURIComponent'](window.location.pathname.toString().substring(0, window.location.pathname.toString().lastIndexOf('/')) + '/');
} else if (typeof location !== 'undefined') {
// worker
PACKAGE_PATH = encodeURIComponent(location.pathname.toString().substring(0, location.pathname.toString().lastIndexOf('/')) + '/');
} else {
throw 'using preloaded data can only be done on a web page or in a web worker';
}
var PACKAGE_NAME = 'game.data';
var REMOTE_PACKAGE_BASE = 'game.data';
if (typeof Module['locateFilePackage'] === 'function' && !Module['locateFile']) {
Module['locateFile'] = Module['locateFilePackage'];
Module.printErr('warning: you defined Module.locateFilePackage, that has been renamed to Module.locateFile (using your locateFilePackage for now)');
}
var REMOTE_PACKAGE_NAME = typeof Module['locateFile'] === 'function' ?
Module['locateFile'](REMOTE_PACKAGE_BASE) :
((Module['filePackagePrefixURL'] || '') + REMOTE_PACKAGE_BASE);
var REMOTE_PACKAGE_SIZE = metadata.remote_package_size;
var PACKAGE_UUID = metadata.package_uuid;
function fetchRemotePackage(packageName, packageSize, callback, errback) {
var xhr = new XMLHttpRequest();
xhr.open('GET', packageName, true);
xhr.responseType = 'arraybuffer';
xhr.onprogress = function(event) {
var url = packageName;
var size = packageSize;
if (event.total) size = event.total;
if (event.loaded) {
if (!xhr.addedTotal) {
xhr.addedTotal = true;
if (!Module.dataFileDownloads) Module.dataFileDownloads = {};
Module.dataFileDownloads[url] = {
loaded: event.loaded,
total: size
};
} else {
Module.dataFileDownloads[url].loaded = event.loaded;
}
var total = 0;
var loaded = 0;
var num = 0;
for (var download in Module.dataFileDownloads) {
var data = Module.dataFileDownloads[download];
total += data.total;
loaded += data.loaded;
num++;
}
total = Math.ceil(total * Module.expectedDataFileDownloads/num);
if (Module['setStatus']) Module['setStatus']('Downloading data... (' + loaded + '/' + total + ')');
} else if (!Module.dataFileDownloads) {
if (Module['setStatus']) Module['setStatus']('Downloading data...');
}
};
xhr.onerror = function(event) {
throw new Error("NetworkError for: " + packageName);
}
xhr.onload = function(event) {
if (xhr.status == 200 || xhr.status == 304 || xhr.status == 206 || (xhr.status == 0 && xhr.response)) { // file URLs can return 0
var packageData = xhr.response;
callback(packageData);
} else {
throw new Error(xhr.statusText + " : " + xhr.responseURL);
}
};
xhr.send(null);
};
function handleError(error) {
console.error('package error:', error);
};
function runWithFS() {
function assert(check, msg) {
if (!check) throw msg + new Error().stack;
}
function DataRequest(start, end, crunched, audio) {
this.start = start;
this.end = end;
this.crunched = crunched;
this.audio = audio;
}
DataRequest.prototype = {
requests: {},
open: function(mode, name) {
this.name = name;
this.requests[name] = this;
Module['addRunDependency']('fp ' + this.name);
},
send: function() {},
onload: function() {
var byteArray = this.byteArray.subarray(this.start, this.end);
this.finish(byteArray);
},
finish: function(byteArray) {
var that = this;
Module['FS_createDataFile'](this.name, null, byteArray, true, true, true); // canOwn this data in the filesystem, it is a slide into the heap that will never change
Module['removeRunDependency']('fp ' + that.name);
this.requests[this.name] = null;
}
};
var files = metadata.files;
for (i = 0; i < files.length; ++i) {
new DataRequest(files[i].start, files[i].end, files[i].crunched, files[i].audio).open('GET', files[i].filename);
}
var indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
var IDB_RO = "readonly";
var IDB_RW = "readwrite";
var DB_NAME = "EM_PRELOAD_CACHE";
var DB_VERSION = 1;
var METADATA_STORE_NAME = 'METADATA';
var PACKAGE_STORE_NAME = 'PACKAGES';
function openDatabase(callback, errback) {
try {
var openRequest = indexedDB.open(DB_NAME, DB_VERSION);
} catch (e) {
return errback(e);
}
openRequest.onupgradeneeded = function(event) {
var db = event.target.result;
if(db.objectStoreNames.contains(PACKAGE_STORE_NAME)) {
db.deleteObjectStore(PACKAGE_STORE_NAME);
}
var packages = db.createObjectStore(PACKAGE_STORE_NAME);
if(db.objectStoreNames.contains(METADATA_STORE_NAME)) {
db.deleteObjectStore(METADATA_STORE_NAME);
}
var metadata = db.createObjectStore(METADATA_STORE_NAME);
};
openRequest.onsuccess = function(event) {
var db = event.target.result;
callback(db);
};
openRequest.onerror = function(error) {
errback(error);
};
};
/* Check if there's a cached package, and if so whether it's the latest available */
function checkCachedPackage(db, packageName, callback, errback) {
var transaction = db.transaction([METADATA_STORE_NAME], IDB_RO);
var metadata = transaction.objectStore(METADATA_STORE_NAME);
var getRequest = metadata.get("metadata/" + packageName);
getRequest.onsuccess = function(event) {
var result = event.target.result;
if (!result) {
return callback(false);
} else {
return callback(PACKAGE_UUID === result.uuid);
}
};
getRequest.onerror = function(error) {
errback(error);
};
};
function fetchCachedPackage(db, packageName, callback, errback) {
var transaction = db.transaction([PACKAGE_STORE_NAME], IDB_RO);
var packages = transaction.objectStore(PACKAGE_STORE_NAME);
var getRequest = packages.get("package/" + packageName);
getRequest.onsuccess = function(event) {
var result = event.target.result;
callback(result);
};
getRequest.onerror = function(error) {
errback(error);
};
};
function cacheRemotePackage(db, packageName, packageData, packageMeta, callback, errback) {
var transaction_packages = db.transaction([PACKAGE_STORE_NAME], IDB_RW);
var packages = transaction_packages.objectStore(PACKAGE_STORE_NAME);
var putPackageRequest = packages.put(packageData, "package/" + packageName);
putPackageRequest.onsuccess = function(event) {
var transaction_metadata = db.transaction([METADATA_STORE_NAME], IDB_RW);
var metadata = transaction_metadata.objectStore(METADATA_STORE_NAME);
var putMetadataRequest = metadata.put(packageMeta, "metadata/" + packageName);
putMetadataRequest.onsuccess = function(event) {
callback(packageData);
};
putMetadataRequest.onerror = function(error) {
errback(error);
};
};
putPackageRequest.onerror = function(error) {
errback(error);
};
};
function processPackageData(arrayBuffer) {
Module.finishedDataFileDownloads++;
assert(arrayBuffer, 'Loading data file failed.');
assert(arrayBuffer instanceof ArrayBuffer, 'bad input to processPackageData');
var byteArray = new Uint8Array(arrayBuffer);
var curr;
// copy the entire loaded file into a spot in the heap. Files will refer to slices in that. They cannot be freed though
// (we may be allocating before malloc is ready, during startup).
if (Module['SPLIT_MEMORY']) Module.printErr('warning: you should run the file packager with --no-heap-copy when SPLIT_MEMORY is used, otherwise copying into the heap may fail due to the splitting');
var ptr = Module['getMemory'](byteArray.length);
Module['HEAPU8'].set(byteArray, ptr);
DataRequest.prototype.byteArray = Module['HEAPU8'].subarray(ptr, ptr+byteArray.length);
var files = metadata.files;
for (i = 0; i < files.length; ++i) {
DataRequest.prototype.requests[files[i].filename].onload();
}
Module['removeRunDependency']('datafile_game.data');
};
Module['addRunDependency']('datafile_game.data');
if (!Module.preloadResults) Module.preloadResults = {};
function preloadFallback(error) {
console.error(error);
console.error('falling back to default preload behavior');
fetchRemotePackage(REMOTE_PACKAGE_NAME, REMOTE_PACKAGE_SIZE, processPackageData, handleError);
};
openDatabase(
function(db) {
checkCachedPackage(db, PACKAGE_PATH + PACKAGE_NAME,
function(useCached) {
Module.preloadResults[PACKAGE_NAME] = {fromCache: useCached};
if (useCached) {
console.info('loading ' + PACKAGE_NAME + ' from cache');
fetchCachedPackage(db, PACKAGE_PATH + PACKAGE_NAME, processPackageData, preloadFallback);
} else {
console.info('loading ' + PACKAGE_NAME + ' from remote');
fetchRemotePackage(REMOTE_PACKAGE_NAME, REMOTE_PACKAGE_SIZE,
function(packageData) {
cacheRemotePackage(db, PACKAGE_PATH + PACKAGE_NAME, packageData, {uuid:PACKAGE_UUID}, processPackageData,
function(error) {
console.error(error);
processPackageData(packageData);
});
}
, preloadFallback);
}
}
, preloadFallback);
}
, preloadFallback);
if (Module['setStatus']) Module['setStatus']('Downloading...');
}
if (Module['calledRun']) {
runWithFS();
} else {
if (!Module['preRun']) Module['preRun'] = [];
Module["preRun"].push(runWithFS); // FS is not initialized yet, wait for it
}
}
loadPackage({"package_uuid":"b2ccb610-64e8-49d6-bb7b-84b12df0c523","remote_package_size":654049,"files":[{"filename":"/game.love","crunched":0,"start":0,"end":654049,"audio":false}]});
})();

108
static/game/index.html Normal file
View File

@@ -0,0 +1,108 @@
<!doctype html>
<html lang="en-us">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, minimum-scale=1, maximum-scale=1">
<title></title>
<!-- Load custom style sheet -->
<link rel="stylesheet" type="text/css" href="theme/love.css">
</head>
<body>
<div>
<canvas id="loadingCanvas" oncontextmenu="event.preventDefault()" width="720" height="720"></canvas>
<canvas id="canvas" oncontextmenu="event.preventDefault()"></canvas>
</div>
<script type='text/javascript'>
function goFullScreen(){
var canvas = document.getElementById("canvas");
if(canvas.requestFullScreen)
canvas.requestFullScreen();
else if(canvas.webkitRequestFullScreen)
canvas.webkitRequestFullScreen();
else if(canvas.mozRequestFullScreen)
canvas.mozRequestFullScreen();
}
function FullScreenHook(){
var canvas = document.getElementById("canvas");
canvas.width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
canvas.height = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
}
var loadingContext = document.getElementById('loadingCanvas').getContext('2d');
function drawLoadingText(text) {
var canvas = loadingContext.canvas;
loadingContext.fillStyle = "rgb(142, 195, 227)";
loadingContext.fillRect(0, 0, canvas.scrollWidth, canvas.scrollHeight);
loadingContext.font = '2em arial';
loadingContext.textAlign = 'center'
loadingContext.fillStyle = "rgb( 11, 86, 117 )";
loadingContext.fillText(text, canvas.scrollWidth / 2, canvas.scrollHeight / 2);
loadingContext.fillText("Powered By Emscripten.", canvas.scrollWidth / 2, canvas.scrollHeight / 4);
loadingContext.fillText("Powered By LÖVE.", canvas.scrollWidth / 2, canvas.scrollHeight / 4 * 3);
}
window.onload = function () { window.focus(); };
window.onclick = function () { window.focus(); };
window.addEventListener("keydown", function(e) {
// space and arrow keys
if([32, 37, 38, 39, 40].indexOf(e.keyCode) > -1) {
e.preventDefault();
}
}, false);
var Module = {
arguments: ["./game.love"],
INITIAL_MEMORY: 16777216,
printErr: console.error.bind(console),
canvas: (function() {
var canvas = document.getElementById('canvas');
// As a default initial behavior, pop up an alert when webgl context is lost. To make your
// application robust, you may want to override this behavior before shipping!
// See http://www.khronos.org/registry/webgl/specs/latest/1.0/#5.15.2
canvas.addEventListener("webglcontextlost", function(e) { alert('WebGL context lost. You will need to reload the page.'); e.preventDefault(); }, false);
return canvas;
})(),
setStatus: function(text) {
if (text) {
drawLoadingText(text);
} else if (Module.remainingDependencies === 0) {
document.getElementById('loadingCanvas').style.display = 'none';
document.getElementById('canvas').style.visibility = 'visible';
}
},
totalDependencies: 0,
remainingDependencies: 0,
monitorRunDependencies: function(left) {
this.remainingDependencies = left;
this.totalDependencies = Math.max(this.totalDependencies, left);
Module.setStatus(left ? 'Preparing... (' + (this.totalDependencies-left) + '/' + this.totalDependencies + ')' : 'All downloads complete.');
}
};
Module.setStatus('Downloading...');
window.onerror = function(event) {
// TODO: do not warn on ok events like simulating an infinite loop or exitStatus
Module.setStatus('Exception thrown, see JavaScript console');
Module.setStatus = function(text) {
if (text) Module.printErr('[post-exception status] ' + text);
};
};
var applicationLoad = function(e) {
Love(Module);
}
</script>
<script type="text/javascript" src="game.js"></script>
<script async type="text/javascript" src="love.js" onload="applicationLoad(this)"></script>
<footer>
<p>Built with <a href="https://github.com/Davidobot/love.js">love.js</a> <button onclick="goFullScreen();">Go Fullscreen</button><br>Hint: Reload the page if screen is blank</p>
</footer>
</body>
</html>

22
static/game/love.js Normal file

File diff suppressed because one or more lines are too long

BIN
static/game/love.wasm Normal file

Binary file not shown.

BIN
static/game/theme/bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@@ -0,0 +1,50 @@
* {
box-sizing: border-box;
}
h1 {
font-family: arial;
color: rgb( 11, 86, 117 );
}
body {
background-image: url(bg.png);
background-repeat: no-repeat;
font-family: arial;
margin: 0;
padding: none;
background-color: rgb( 154, 205, 237 );
color: rgb( 28, 78, 104 );
}
footer {
font-family: arial;
font-size: 12px;
padding-left: 10px;
position:absolute;
bottom: 0;
width: 100%;
}
/* Links */
a {
text-decoration: none;
}
a:link {
color: rgb( 233, 73, 154 );
}
a:visited {
color: rgb( 110, 30, 71 );
}
a:hover {
color: rgb( 252, 207, 230 );
}
/* the canvas *must not* have any border or padding, or mouse coords will be wrong */
#canvas {
padding-right: 0;
display: block;
border: 0 none;
visibility: hidden;
width: 100%;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.