Skip to content

Commit

Permalink
Improve bat theme (#516)
Browse files Browse the repository at this point in the history
* Improve bat theme

* Simplify lock

* Namespace CSS variables

* Dont show bats on print

---------

Co-authored-by: Kresten Laust <[email protected]>
  • Loading branch information
atjn and krestenlaust authored Oct 29, 2024
1 parent fccc11d commit 124ba7f
Show file tree
Hide file tree
Showing 10 changed files with 300 additions and 65 deletions.
89 changes: 71 additions & 18 deletions stregsystem/static/stregsystem/themes/bat/bat.css
Original file line number Diff line number Diff line change
@@ -1,22 +1,75 @@
.bat-container {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
@property --bat-x {
syntax: "<number>";
inherits: true;
initial-value: 0;
}
@property --bat-y {
syntax: "<number>";
inherits: true;
initial-value: 0;
}
@property --bat-direction {
syntax: "<number>";
inherits: true;
initial-value: 0;
}
@property --bat-delay {
syntax: "<number>";
inherits: true;
initial-value: 0;
}

#bat-container {
position: fixed;
inset: 1lvw 1lvh;
margin: 0;
transition: opacity 2s ease;
pointer-events: none;

&.stationary {
opacity: 0.7;

.bat {
position: fixed;
left: 0;
top: 0;
opacity: 0;
transition-property: top, left, opacity;
transition-delay: 0s;
transition-timing-function: linear;
@media (prefers-contrast) or (prefers-reduced-transparency) {
display: none;
}
}

@media not screen {
display: none;
}

& .bat {
position: absolute;
width: 10%;
height: 5%;
left: 0;
top: 0;
scale: var(--bat-direction) 1;
translate: calc(9% * var(--bat-x)) calc(19% * var(--bat-y));
background: url(./sprites/0.png) no-repeat center / contain;
animation: 600ms steps(1, end) calc(-6ms * var(--bat-delay)) infinite bat-flap;
image-rendering: pixelated;

@media (prefers-reduced-motion) {
animation-play-state: paused !important;
}
}
}

.bat img {
image-rendering: pixelated;
}
@keyframes bat-flap {
0% {
background-image: url(sprites/0.png);
}
33.33% {
background-image: url(sprites/1.png);
}
50% {
background-image: url(sprites/2.png);
}
66.67% {
background-image: url(sprites/3.png);
}
83.33% {
background-image: url(sprites/4.png);
}
}
236 changes: 190 additions & 46 deletions stregsystem/static/stregsystem/themes/bat/bat.js
Original file line number Diff line number Diff line change
@@ -1,47 +1,191 @@
/*! JS Bat 2013 - v1.2 - Eric Grange - www.delphitools.info */

function spawn_bat() {
var v = document.createElement('video');
var s = document.createElement('source');
var z = document.createElement('div');
var zs = z.style;
var a = window.innerWidth * Math.random();
var b = window.innerHeight * Math.random();
z.classList.add("bat");
z.appendChild(v);
v.appendChild(s);
v.width = "48";
v.height = "48";
v.autoplay = true;
v.loop = true;
v.muted = true;
s.src = themes_static_url + "bat/bat.webm";
s.type = "video/webm";
document.body.querySelector(".bat-container").appendChild(z);

function R(o, m) {
return Math.max(Math.min(o + (Math.random() - 0.5) * 400, m - 50), 50);
}

function A(){
var x = R(a, window.innerWidth);
var y = R(b, window.innerHeight);
var d = Math.round(10 * Math.sqrt((a - x) * (a - x) + (b - y) * (b - y)));
zs.opacity = 1;
zs.transitionDuration = zs.webkitTransitionDuration = d + 'ms';
// zs.transform = zs.webkitTransform = 'translate(' + x + 'px, ' + y + 'px)';
zs.left = x + 'px';
zs.top = y + 'px';
v.style.transform = v.style.webkitTransform = (a > x) ? '' : 'scaleX(-1)';
a = x;
b = y;
setTimeout(A,d);
}
setTimeout(A, Math.random() * 3e3);
};

const d = new Date();

for(let n_bats=0; n_bats < d.getDate(); n_bats++){
spawn_bat();
// Fallback for WebKit: https://caniuse.com/requestidlecallback
// TODO: remove this crap as soon as WebKit supports the real deal
const requestIdleCallback =
globalThis.requestIdleCallback ??
((func, { timeout }) => {
const maxWait = Math.min(timeout ?? Infinity, 100);
return setTimeout(func, maxWait);
});
const cancelIdleCallback = globalThis.cancelIdleCallback ?? clearTimeout;

// The minimum amount of time that CSS animations should be buffered for
const minAnimationBuffer = 5_000;
// The maximum amount of time that CSS animations should be buffered for
const maxAnimationBuffer = 10_000;
// Make this bigger to make bats go faster
const speedMultiplier = 150;

// The HTML container that our bats exist in
const container = document.querySelector("#bat-container");

// The ID of the timeout that is currently waiting to call `pointAndShoot`
let timeoutId;
// A queue of all the bats, ordered by when they will need new coordinates
const batQueue = [];

// Initial setup of the bats queue
for (const element of container.querySelectorAll(".bat")) {
batQueue.push({
element,
nextFly: 0,
});
}

// Ensure that we disable this stuff if the user prefers reduced motion,
// or if the user is not looking at the page
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion)");
prefersReducedMotion.addEventListener("change", handleStationaryChange);
document.addEventListener("visibilitychange", handleStationaryChange);
handleStationaryChange();
function handleStationaryChange() {
if (prefersReducedMotion.matches || document.visibilityState === "hidden") {
pauseShooting();
} else {
resumeShooting();
}
}
/**
* Makes sure everything stops running and removes any animations.
*/
function pauseShooting() {
// Let CSS know to stop moving
container.classList.add("stationary");
// Stop shooting bats around
clearTimeout(timeoutId);
cancelIdleCallback(timeoutId);
timeoutId = undefined;
}
/**
* Kickstarts everything again.
*/
function resumeShooting() {
// Let CSS know to start moving again
container.classList.remove("stationary");
// Start shooting bats around again
prepareNextShot();
}

/**
* Call this function to ensure that bats are getting new coordinates.
* DO NOT call the `pointAndShoot` function directly, as that might
* cause two functions to be running simultaneously.
*/
function prepareNextShot() {
if (timeoutId !== undefined) {
return;
}
const bat = batQueue[0];
const timeToNextFly = Math.max(0, bat.nextFly - Date.now());
if (timeToNextFly < 10) {
// If the bat needs new coordinates RIGHT NOW, schedule it quick
// with a very short timeout.
// We do not run it directly, as running this back-to-back 30 times
// in a row would block user input and make the page feel janky.
timeoutId = setTimeout(pointAndShoot, 0);
} else if (timeToNextFly < maxAnimationBuffer) {
// If the bat needs new coordinates sometime before the max buffer time,
// schedule an idle callback, so it doesn't interfere with more important tasks.
timeoutId = requestIdleCallback(pointAndShoot, { timeout: timeToNextFly });
} else {
// If the bat needs new coordinates later than our max buffer time,
// take a chill pill and schedule a timeout that wakes us up when
// we get close to our min buffer time limit.
timeoutId = setTimeout(() => {
timeoutId = undefined;
prepareNextShot();
}, timeToNextFly - minAnimationBuffer);
}
}
/**
* Gives the next bat in the queue some new coordinates.
* DO NOT call this directly, call `prepareNextShot` instead.
*/
function pointAndShoot() {
// Make it clear that a new timeout can be scheduled
timeoutId = undefined;

// Get the next bat in the queue.
const bat = batQueue.shift();

// On first load, we need to get the bat position from the HTML
bat.x ??= Number(bat.element.style.getPropertyValue("--bat-x"));
bat.y ??= Number(bat.element.style.getPropertyValue("--bat-y"));

// Calculate new coordinates
const { coordinate: newX, direction } = newCoordinate(bat.x);
const { coordinate: newY } = newCoordinate(bat.y);
// Calculate the animation time based on how far
// the new coordinates are from the previous
const distance = Math.sqrt((bat.x - newX) ** 2 + (bat.y - newY) ** 2);
const flyTime = speedMultiplier * distance;

const now = Date.now();
// If we are late to the party, we pretend that the bat was supposed to fly right now
bat.nextFly = Math.max(bat.nextFly, now);

// Set the animation in the DOM
const batDirection = direction * -1;
bat.element.animate(
[
{ "--bat-x": bat.x, "--bat-y": bat.y, "--bat-direction": batDirection },
{ "--bat-x": newX, "--bat-y": newY, "--bat-direction": batDirection },
],
{
delay: bat.nextFly - now,
duration: flyTime,
fill: "forwards",
},
);

// Set everything in our local bat object so we know what's up next time
bat.nextFly += flyTime;
bat.x = newX;
bat.y = newY;

// Put it back in the bats array.
// Bats must be ordered such that the first element is always the next one that needs to be shot.
let i;
for (i = 0; i < batQueue.length; i++) {
if (batQueue[i].nextFly > bat.nextFly) {
break;
}
}
batQueue.splice(i, 0, bat);

// Move on to the next bat in need
prepareNextShot();
}

/**
* Generates a new coordinate that is at least 2% different from
* the previous, and at most 15% different.
*
* @param {number} previous - The previous coordinate.
*/
function newCoordinate(previous) {
// The minimum amount it can change in percentage
const min = 2;
// The amount it can change beyond the minimum, in percentage
const maxRange = 13;

const change = min + maxRange * Math.random();
let direction = Math.random() < 0.5 ? 1 : -1;
let coordinate = previous + change * direction;
// Make sure that the bat doesn't move outside the page
if (coordinate < 0 || 100 < coordinate) {
direction *= -1;
coordinate = previous + change * direction;
}
return { coordinate, direction };
}

/**
* Ensure that the value is somewhere between a min and max value.
*
* @param {number} min - The smallest permitted value.
* @param {number} value - The value to clamp.
* @param {number} max - The largest permitted value.
*/
function clamp(min, value, max) {
return Math.max(min, Math.min(max, value));
}
Binary file removed stregsystem/static/stregsystem/themes/bat/bat.webm
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 17 additions & 1 deletion stregsystem/templates/stregsystem/themes/bat/bat.html
Original file line number Diff line number Diff line change
@@ -1 +1,17 @@
<div class="bat-container"></div>
{% load themes %}
{% load stregsystem_extras %}
{% day_of_month as day %}

<div id="bat-container" class="stationary">
{% for i in day|to_range %}
<div
class="bat"
style="
--bat-x: {% random 0 100 %};
--bat-y: {% random 0 100 %};
--bat-direction: {% random_choice 1 -1 %};
--bat-delay: {% random 0 100 %};
"
></div>
{% endfor %}
</div>
22 changes: 22 additions & 0 deletions stregsystem/templatetags/stregsystem_extras.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from datetime import datetime
from random import randint, choice
from django import template
from django.template.loader import get_template
from django.utils import timezone
Expand Down Expand Up @@ -45,3 +47,23 @@ def product_id_and_alias_string(product_id):
else:
#
return str(product_id)


@register.simple_tag
def day_of_month():
return datetime.now().day


@register.simple_tag
def random(min, max):
return randint(min, max)


@register.simple_tag
def random_choice(str1, str2):
return choice([str1, str2])


@register.filter
def to_range(value):
return range(value)

0 comments on commit 124ba7f

Please sign in to comment.