feat: initial commit
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
commit
de21064032
17 changed files with 517 additions and 0 deletions
1
.envrc
Normal file
1
.envrc
Normal file
|
@ -0,0 +1 @@
|
|||
use flake
|
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
result/
|
||||
build/
|
||||
__pycache__/
|
||||
.direnv/
|
19
.woodpecker.yml
Normal file
19
.woodpecker.yml
Normal file
|
@ -0,0 +1,19 @@
|
|||
platform: linux/arm64
|
||||
|
||||
pipeline:
|
||||
build:
|
||||
image: nixos/nix
|
||||
commands:
|
||||
- ./build.sh
|
||||
deploy:
|
||||
image: woodpeckerci/plugin-s3
|
||||
settings:
|
||||
bucket: stories
|
||||
source: build/**/*
|
||||
target: /
|
||||
path_style: true
|
||||
endpoint: https://minio.ashhhleyyy.dev
|
||||
access_key:
|
||||
from_secret: minio_user
|
||||
secret_key:
|
||||
from_secret: minio_password
|
BIN
assets/background.png
Normal file
BIN
assets/background.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 530 B |
BIN
assets/rain_fall.gif
Normal file
BIN
assets/rain_fall.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 64 KiB |
BIN
assets/rain_splash.gif
Normal file
BIN
assets/rain_splash.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 214 B |
BIN
assets/rainfall.gif
Normal file
BIN
assets/rainfall.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 271 KiB |
5
build.sh
Executable file
5
build.sh
Executable file
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/env bash
|
||||
out_dir="$(nix --experimental-features 'nix-command flakes' build --no-link --print-out-paths)"
|
||||
cp -r "$out_dir" ./build
|
||||
# fix permissions that are copied from the nix store
|
||||
chmod +w -R ./build
|
43
flake.lock
Normal file
43
flake.lock
Normal file
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"locked": {
|
||||
"lastModified": 1667395993,
|
||||
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1671268780,
|
||||
"narHash": "sha256-9Okbivo10bcXEGCtmAQNfJt1Zpk6B3tjkSQ2CIXmTCg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "80c24eeb9ff46aa99617844d0c4168659e35175f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
30
flake.nix
Normal file
30
flake.nix
Normal file
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
flake-utils.lib.eachDefaultSystem(system:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
};
|
||||
inherit (pkgs) lib;
|
||||
in
|
||||
{
|
||||
packages.default = pkgs.stdenv.mkDerivation rec {
|
||||
name = "ash-stories";
|
||||
src = ./.;
|
||||
phases = "installPhase";
|
||||
installPhase = ''
|
||||
mkdir -p $out/src
|
||||
mkdir -p $out/stories
|
||||
cp $src/stories/* $out/stories/
|
||||
cp $src/src/*.js $out/src/
|
||||
cp $src/src/*.css $out/src/
|
||||
'';
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
1
result
Symbolic link
1
result
Symbolic link
|
@ -0,0 +1 @@
|
|||
/nix/store/p8pj5f23xgkfyqm8cf3k0ycdvj5aq2j2-ash-stories
|
31
src/background-music.js
Normal file
31
src/background-music.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
(function() {
|
||||
const player = document.getElementById('background-music');
|
||||
if (!player) {
|
||||
console.warn('no element with id `background-music`');
|
||||
return;
|
||||
}
|
||||
|
||||
let song = '';
|
||||
if (document.currentScript.dataset.song) {
|
||||
song = ' (' + document.currentScript.dataset.song + ')';
|
||||
}
|
||||
|
||||
const toggle = document.createElement('a');
|
||||
toggle.href = '#';
|
||||
toggle.innerText = '🎶 Enable music' + song;
|
||||
toggle.addEventListener('click', function () {
|
||||
if (player.paused) {
|
||||
player.play();
|
||||
toggle.innerText = '🎶 Disable music' + song;
|
||||
} else {
|
||||
player.pause();
|
||||
toggle.innerText = '🎶 Enable music' + song;
|
||||
}
|
||||
});
|
||||
toggle.style.position = "fixed";
|
||||
toggle.style.bottom = "8px";
|
||||
toggle.style.right = "8px";
|
||||
toggle.style.color = "white";
|
||||
toggle.classList.add('music-toggle');
|
||||
document.body.appendChild(toggle);
|
||||
})();
|
77
src/drifting.css
Normal file
77
src/drifting.css
Normal file
|
@ -0,0 +1,77 @@
|
|||
@import url(https://fonts.bunny.net/css?family=nanum-pen-script:400);
|
||||
|
||||
html, body {
|
||||
font-family: 'Nanum Pen Script', handwriting;
|
||||
font-size: 24px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: rgb(34, 32, 52);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.background {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
/*z-index: -100;*/
|
||||
}
|
||||
|
||||
img.sharp {
|
||||
image-rendering: pixelated;
|
||||
image-rendering: crisp-edges;
|
||||
image-rendering: -moz-crisp-edges;
|
||||
image-rendering: -o-pixelated;
|
||||
-ms-interpolation-mode: nearest-neighbor;
|
||||
}
|
||||
|
||||
#story {
|
||||
color: white;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
font-size: 36px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page {
|
||||
opacity: 1;
|
||||
transition: opacity 500ms ease;
|
||||
}
|
||||
|
||||
.page.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page.changing {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.again {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
margin-top: 2rem;
|
||||
animation: late-fade 1000ms ease;
|
||||
}
|
||||
|
||||
@keyframes late-fade {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
114
src/make_rain.py
Normal file
114
src/make_rain.py
Normal file
|
@ -0,0 +1,114 @@
|
|||
import math
|
||||
from PIL import Image, ImageSequence
|
||||
import random
|
||||
|
||||
IMAGE_WIDTH = 256
|
||||
IMAGE_HEIGHT = 144
|
||||
|
||||
SPLASH_WIDTH = 9
|
||||
SPLASH_HEIGHT = 8
|
||||
|
||||
BACKGROUND_COLOUR = (34, 32, 52)
|
||||
RAINDROP_COLOUR = (63, 63, 116)
|
||||
WATER_COLOUR = (91, 110, 225)
|
||||
FALL_SPEED = 2
|
||||
|
||||
def create_rain_column_frames():
|
||||
frames = []
|
||||
for y in range(0, IMAGE_HEIGHT - SPLASH_HEIGHT + 2, FALL_SPEED):
|
||||
frame = Image.new('RGBA', (SPLASH_WIDTH, IMAGE_HEIGHT), (0,0,0))
|
||||
for x in range(frame.width):
|
||||
frame.putpixel((x, frame.height - 1), WATER_COLOUR)
|
||||
for yOffset in range(-1, 1):
|
||||
finalY = y + yOffset
|
||||
if finalY > 0 and finalY < frame.height:
|
||||
frame.putpixel((int(SPLASH_WIDTH / 2), finalY), RAINDROP_COLOUR)
|
||||
frames.append(frame)
|
||||
return frames
|
||||
|
||||
def create_splash_frames():
|
||||
frames = []
|
||||
with Image.open("assets/rain_splash.gif") as im:
|
||||
for origFrame in ImageSequence.Iterator(im):
|
||||
frame = Image.new('RGBA', (SPLASH_WIDTH, IMAGE_HEIGHT), BACKGROUND_COLOUR)
|
||||
frame.paste(origFrame, (0, IMAGE_HEIGHT - origFrame.height))
|
||||
frames.append(frame)
|
||||
return frames
|
||||
|
||||
def save_gif(frames, filename, duration):
|
||||
frames[0].save(filename, format='GIF',
|
||||
append_images=frames[0:], save_all=True, duration=duration,
|
||||
optimize=False, disposal=2, loop=0)
|
||||
|
||||
def create_background():
|
||||
im = Image.new('RGBA', (IMAGE_WIDTH, IMAGE_HEIGHT), BACKGROUND_COLOUR)
|
||||
for x in range(IMAGE_WIDTH):
|
||||
im.putpixel((x, IMAGE_HEIGHT - 1), WATER_COLOUR)
|
||||
im.save("assets/background.png")
|
||||
|
||||
class RainParticle:
|
||||
def __init__(self, x, image_width, image_height, height, colour, animation_offset, splash_frames, animation_delay, speed):
|
||||
self.x = x
|
||||
self.image_width = image_width
|
||||
self.image_height = image_height
|
||||
self.height = height
|
||||
self.colour = colour
|
||||
self.animation_offset = animation_offset
|
||||
self.splash_frames = splash_frames
|
||||
self.animation_delay = animation_delay
|
||||
self.speed = speed
|
||||
self.animation_duration = ((self.image_height + self.height) / self.speed) + self.splash_frames.n_frames + self.animation_delay
|
||||
def draw(self, canvas, frame_number):
|
||||
animationPosition = (frame_number + self.animation_offset) % self.animation_duration
|
||||
if animationPosition < (self.image_height + self.height):
|
||||
yPosition = animationPosition - self.height
|
||||
for yOffset in range(self.height):
|
||||
y = yPosition + yOffset
|
||||
if y >= 0 and y < self.image_height:
|
||||
canvas.putpixel((self.x, y), self.colour)
|
||||
else:
|
||||
frameNum = animationPosition - (self.image_height + self.height)
|
||||
frame = self.splash_frames
|
||||
frame.seek(frameNum)
|
||||
xOffset = int(frame.width / 2)
|
||||
y = self.image_height - frame.height
|
||||
canvas.paste(frame, (self.x - xOffset, y))
|
||||
|
||||
def fully_animated():
|
||||
splash_frames = Image.open("assets/rain_splash.gif")
|
||||
|
||||
random.seed(123456789)
|
||||
particles = []
|
||||
for i in range(128):
|
||||
particles.append(RainParticle(
|
||||
i * 2,
|
||||
IMAGE_WIDTH,
|
||||
IMAGE_HEIGHT,
|
||||
5,
|
||||
RAINDROP_COLOUR,
|
||||
random.randint(0, IMAGE_HEIGHT),
|
||||
splash_frames,
|
||||
0,
|
||||
))
|
||||
|
||||
animationLength = math.lcm(*map(lambda p: p.animation_duration, particles))
|
||||
frames = []
|
||||
print(f"Generating {animationLength} frames...")
|
||||
for frameNum in range(animationLength):
|
||||
canvas = Image.new('RGBA', (IMAGE_WIDTH, IMAGE_HEIGHT), BACKGROUND_COLOUR)
|
||||
for particle in particles:
|
||||
particle.draw(canvas, frameNum)
|
||||
frames.append(canvas)
|
||||
|
||||
save_gif(frames, "assets/rainfall.gif", 5)
|
||||
|
||||
def main():
|
||||
create_background()
|
||||
fully_animated()
|
||||
#column_frames = create_rain_column_frames()
|
||||
#splash_frames = create_splash_frames()
|
||||
#print(len(column_frames), len(splash_frames))
|
||||
#save_gif(column_frames + splash_frames, "assets/rain_fall.gif", 10)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
94
src/rain.js
Normal file
94
src/rain.js
Normal file
|
@ -0,0 +1,94 @@
|
|||
(function() {
|
||||
function createRandomParticle(currentTime, width) {
|
||||
return {
|
||||
startTime: currentTime,
|
||||
width: 3,
|
||||
length: 75 + Math.random() * 50,
|
||||
colour: '#3f3f74',
|
||||
speed: 750 + Math.random() * 400,
|
||||
x: Math.round(Math.random() * width),
|
||||
};
|
||||
}
|
||||
|
||||
let imgCount = 0;
|
||||
function spawnSplash(x) {
|
||||
const img = document.createElement('img');
|
||||
img.src = '../assets/rain_splash.gif?' + Math.random();
|
||||
const SCALE = 5;
|
||||
img.width = 9 * SCALE;
|
||||
img.height = 7 * SCALE;
|
||||
img.classList.add('sharp');
|
||||
img.style.position = 'absolute';
|
||||
img.style.bottom = 0;
|
||||
img.style.left = (x - 4 * SCALE) + 'px';
|
||||
document.body.appendChild(img);
|
||||
imgCount += 1;
|
||||
setTimeout(function() {
|
||||
img.remove();
|
||||
imgCount -= 1;
|
||||
}, 600);
|
||||
}
|
||||
|
||||
function drawParticle(time, particle, canvas, context) {
|
||||
const t = time - particle.startTime;
|
||||
const y = (t * particle.speed) - particle.length;
|
||||
//console.log(y);
|
||||
if (y > canvas.height) {
|
||||
return true;
|
||||
}
|
||||
const gradient = context.createLinearGradient(particle.x, y, particle.x, y + particle.length);
|
||||
gradient.addColorStop(0, particle.colour + '00');
|
||||
gradient.addColorStop(1, particle.colour + 'ff');
|
||||
context.fillStyle = gradient;
|
||||
context.fillRect(particle.x, y, particle.width, particle.length);
|
||||
return false;
|
||||
}
|
||||
|
||||
let particles = [];
|
||||
let nextSpawnTime = -1;
|
||||
const canvas = document.getElementById("rainfall");
|
||||
|
||||
function init() {
|
||||
particles = [];
|
||||
nextSpawnTime = -1;
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
}
|
||||
|
||||
const particleCount = document.getElementById('particle-count');
|
||||
function update(t) {
|
||||
// Convert microseconds -> seconds
|
||||
const time = t / 1000;
|
||||
const context = canvas.getContext('2d');
|
||||
context.fillStyle = '#222034';
|
||||
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
const particlesForRemoval = [];
|
||||
for (const particle of particles) {
|
||||
const r = drawParticle(time, particle, canvas, context);
|
||||
if (r) {
|
||||
particlesForRemoval.push(particle);
|
||||
//spawnSplash(particle.x);
|
||||
}
|
||||
}
|
||||
particlesForRemoval.forEach(function (p) {
|
||||
particles.splice(particles.indexOf(p), 1)
|
||||
});
|
||||
particleCount.innerText = 'Particles: ' + particles.length;
|
||||
while (nextSpawnTime === -1 || time > nextSpawnTime) {
|
||||
particles.push(createRandomParticle(nextSpawnTime, canvas.width));
|
||||
if (nextSpawnTime === -1) {
|
||||
nextSpawnTime = time;
|
||||
} else {
|
||||
nextSpawnTime = nextSpawnTime + Math.random() * 0.01;
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(update);
|
||||
}
|
||||
|
||||
init();
|
||||
requestAnimationFrame(update);
|
||||
|
||||
window.addEventListener("resize", function() {
|
||||
init();
|
||||
})
|
||||
})();
|
55
src/story-player.js
Normal file
55
src/story-player.js
Normal file
|
@ -0,0 +1,55 @@
|
|||
(function() {
|
||||
const story = document.getElementById('story');
|
||||
if (!story) {
|
||||
console.warn('Could not locate story element');
|
||||
return;
|
||||
}
|
||||
|
||||
const pages = [];
|
||||
for (let i = 0; i < story.children.length; i++) {
|
||||
const ele = story.children.item(i);
|
||||
pages.push(ele);
|
||||
}
|
||||
|
||||
let currentPage = 0;
|
||||
|
||||
function displayPage(index) {
|
||||
if (index >= pages.length) return;
|
||||
currentPage = index;
|
||||
const pageEle = pages[index];
|
||||
if (!(pageEle.classList.contains('hidden') || pageEle.classList.contains('changing'))) {
|
||||
// Page is already active
|
||||
return;
|
||||
}
|
||||
for (const page of pages) {
|
||||
page.classList.add('changing');
|
||||
}
|
||||
setTimeout(function() {
|
||||
for (const page of pages) {
|
||||
if (page !== pageEle) {
|
||||
page.classList.remove('changing');
|
||||
}
|
||||
page.classList.add('hidden');
|
||||
}
|
||||
pageEle.classList.remove('hidden');
|
||||
setTimeout(function() {
|
||||
pageEle.classList.remove('changing');
|
||||
}, 100);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
for (const page of pages) {
|
||||
page.classList.add('page');
|
||||
page.classList.add('hidden');
|
||||
}
|
||||
|
||||
displayPage(0);
|
||||
window.addEventListener('keydown', function(ev) {
|
||||
if (ev.key === 'ArrowDown' || ev.key === ' ') {
|
||||
displayPage(currentPage + 1);
|
||||
}
|
||||
});
|
||||
|
||||
// Allow usage from event handlers
|
||||
window.displayStoryPage = displayPage;
|
||||
})();
|
43
stories/drifting.html
Normal file
43
stories/drifting.html
Normal file
|
@ -0,0 +1,43 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Drifting</title>
|
||||
|
||||
<link rel="stylesheet" href="../src/drifting.css">
|
||||
</head>
|
||||
<body>
|
||||
<canvas class="background" id="rainfall"></canvas>
|
||||
<div style="display: none;" id="particle-count">Particles: </div>
|
||||
|
||||
<!-- Main story content -->
|
||||
<div id="story">
|
||||
<div>
|
||||
<h1>Drifting</h1>
|
||||
<h2>By Ashley B</h2>
|
||||
<p>Press space to begin</p>
|
||||
</div>
|
||||
<p>When I’m down</p>
|
||||
<p>And no-one's around</p>
|
||||
<p>It's like I'm drifting</p>
|
||||
<p>Drifting without direction</p>
|
||||
<p>Drifiting without control</p>
|
||||
<p>
|
||||
Drifting to some place unknown
|
||||
<br>
|
||||
<button class="again" onclick="displayStoryPage(0)">
|
||||
Read again
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script src="../src/story-player.js"></script>
|
||||
|
||||
<script src="../src/rain.js"></script>
|
||||
<audio id="background-music" loop="loop">
|
||||
<source src="https://cdn.ashhhleyyy.dev/file/ashs-magic-bucket/stories/unknown_waters.mp3">
|
||||
</audio>
|
||||
<script src="../src/background-music.js" data-song="unknown waters - lilypichu"></script>
|
||||
</body>
|
||||
</html
|
Loading…
Reference in a new issue