feat: initial commit
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
Ashhhleyyy 2023-07-06 01:47:43 +01:00
commit de21064032
Signed by: ash
GPG key ID: 83B789081A0878FB
17 changed files with 517 additions and 0 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
result/
build/
__pycache__/
.direnv/

19
.woodpecker.yml Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 530 B

BIN
assets/rain_fall.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
assets/rain_splash.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 B

BIN
assets/rainfall.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

5
build.sh Executable file
View 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
View 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
View 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
View file

@ -0,0 +1 @@
/nix/store/p8pj5f23xgkfyqm8cf3k0ycdvj5aq2j2-ash-stories

31
src/background-music.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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 Im 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