mirror of
https://codeberg.org/vlw/victorwesterlund.com.git
synced 2025-09-14 03:23:41 +02:00
wip(22w7f): refactor glitch
This commit is contained in:
parent
9fc0b00cec
commit
173007ba71
14 changed files with 214 additions and 193 deletions
|
@ -2,11 +2,7 @@
|
|||
--color-base: 0, 0, 0;
|
||||
--color-contrast: 256, 256, 256;
|
||||
|
||||
--padding: 40px;
|
||||
|
||||
--font-min: 30px;
|
||||
--font-tar: 10vw;
|
||||
--font-max: 3vh;
|
||||
--padding: clamp(40px, 2vw, 2vw);
|
||||
}
|
||||
|
||||
/* -- Cornerstones -- */
|
||||
|
@ -33,26 +29,17 @@ body {
|
|||
background-position: fixed;
|
||||
}
|
||||
|
||||
:is(p, h1) {
|
||||
font-size: var(--font-min);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
:is(#intro, #card) a {
|
||||
--padding-vert: 17px;
|
||||
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
picture {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(var(--font-min), var(--font-tar), var(--font-max));
|
||||
font-size: clamp(45px, 7vw, 6vh);
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: clamp(calc(var(--font-min) / 2), calc(var(--font-tar) / 2), calc(var(--font-max) / 2));
|
||||
p, a {
|
||||
font-size: clamp(20px, 3vw, 2vh);
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
/* -- Components -- */
|
||||
|
@ -69,24 +56,32 @@ body > div {
|
|||
padding: calc(var(--padding) / 2);
|
||||
}
|
||||
|
||||
:is(#intro, #card) a {
|
||||
--padding-vert: clamp(17px, 1.1vw, 1.1vw);
|
||||
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* --- */
|
||||
|
||||
#intro {
|
||||
--font-tar: 13vw;
|
||||
--font-max: 5vh;
|
||||
padding: calc(var(--padding) / 2);
|
||||
}
|
||||
|
||||
#intro a {
|
||||
padding: var(--padding-vert) 40px;
|
||||
padding: var(--padding-vert) 2vw;
|
||||
border-radius: 100px;
|
||||
border: solid 4px rgba(var(--color-contrast), .3);
|
||||
border: solid clamp(4px, .25vw, .25vw) rgba(var(--color-contrast), .3);
|
||||
margin: var(--padding) 0;
|
||||
width: calc(100% - (var(--padding) * 2));
|
||||
width: calc(100% - var(--padding));
|
||||
}
|
||||
|
||||
#intro p {
|
||||
margin: 10px 0;
|
||||
margin: 1vh 0;
|
||||
font-size: clamp(20px, 3vw, 3vh);
|
||||
}
|
||||
|
||||
/* --- */
|
||||
|
@ -100,37 +95,37 @@ body > div {
|
|||
}
|
||||
|
||||
#card {
|
||||
--portrait-size: 128px;
|
||||
--portrait-size: clamp(128px, 12vw, 12vh);
|
||||
|
||||
gap: var(--padding);
|
||||
position: relative;
|
||||
max-width: 600px;
|
||||
padding: var(--padding);
|
||||
padding-top: calc(var(--portrait-size) - (var(--padding) / 2));
|
||||
border-radius: 18px;
|
||||
backdrop-filter: saturate(100) brightness(.3);
|
||||
-webkit-backdrop-filter: saturate(100) brightness(.3);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, .19), 0 6px 6px rgba(0, 0, 0, .23);
|
||||
border-radius: clamp(18px, 1vw, 1vw);
|
||||
backdrop-filter: saturate(100) brightness(.4);
|
||||
-webkit-backdrop-filter: saturate(100) brightness(.4);
|
||||
box-shadow: 0 1vh 2vh rgba(0, 0, 0, .19), 0 6px 6px rgba(0, 0, 0, .23);
|
||||
}
|
||||
|
||||
#card > img {
|
||||
#card img {
|
||||
width: var(--portrait-size);
|
||||
height: var(--portrait-size);
|
||||
position: absolute;
|
||||
border-radius: 100%;
|
||||
top: calc((var(--portrait-size) / 2) * -1);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0 , .19), 0 6px 6px rgba(0, 0, 0 , .23);
|
||||
background-color: rgb(var(--color-base));
|
||||
box-shadow: 0 1vh 2vh rgba(0, 0, 0 , .19), 0 6px 6px rgba(0, 0, 0 , .23);
|
||||
}
|
||||
|
||||
#card a {
|
||||
width: 100%;
|
||||
padding: var(--padding-vert) 0;
|
||||
border-radius: 9px;
|
||||
margin-top: calc(var(--padding) / 2);
|
||||
border-radius: clamp(9px, .5vw, .5vw);
|
||||
background-color: rgba(var(--color-contrast), .13);
|
||||
box-shadow:
|
||||
inset 0 3px 16px rgba(0, 0, 0, 0),
|
||||
0 1px 3px rgba(0, 0, 0, .12),
|
||||
0 1px 2px rgba(0, 0, 0, .24);
|
||||
inset 0 .3vh 1.6vh rgba(0, 0, 0, 0),
|
||||
0 .1vh .3vh rgba(0, 0, 0, .12),
|
||||
0 .1vh .2vh rgba(0, 0, 0, .24);
|
||||
}
|
||||
|
||||
/* -- Media Queries -- */
|
||||
|
@ -150,9 +145,9 @@ body > div {
|
|||
:is(#intro, #card) a:hover {
|
||||
background-color: rgba(var(--color-contrast), .2);
|
||||
box-shadow:
|
||||
inset 0 3px 16px rgba(0, 0, 0, .16),
|
||||
0 3px 6px rgba(0, 0, 0, .16),
|
||||
0 3px 6px rgba(0, 0, 0, .23);
|
||||
inset 0 .3vh 1.6vh rgba(0, 0, 0, .16),
|
||||
0 .3vh .6vh rgba(0, 0, 0, .16),
|
||||
0 .3vh .6vh rgba(0, 0, 0, .23);
|
||||
}
|
||||
|
||||
:is(#intro, #card) a:active {
|
||||
|
@ -164,6 +159,7 @@ body > div {
|
|||
body {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: unset;
|
||||
}
|
||||
|
||||
body > div {
|
||||
|
@ -181,5 +177,6 @@ body > div {
|
|||
|
||||
#card {
|
||||
min-width: 300px;
|
||||
max-width: 30vw;
|
||||
}
|
||||
}
|
||||
|
|
72
public/assets/js/glitch/Generator.mjs
Normal file
72
public/assets/js/glitch/Generator.mjs
Normal file
|
@ -0,0 +1,72 @@
|
|||
// Fetch and create glitchy background effects
|
||||
class Generator {
|
||||
constructor() {
|
||||
this.bg = {
|
||||
_this: this,
|
||||
_image: null,
|
||||
_dir: location,
|
||||
_dir_rel: "assets/media/b64/",
|
||||
count: 3,
|
||||
// Get or set current background
|
||||
get current () { return this._image; },
|
||||
set current (image) {
|
||||
this._image = image;
|
||||
this._this.setBg(image);
|
||||
},
|
||||
// Get or set the path to where base64 images are stored
|
||||
get dir () { return this._dir; },
|
||||
set dir (newPath) {
|
||||
this._dir = newPath + this._dir_rel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Genrate random int in range
|
||||
static randInt(min, max) {
|
||||
if(min === max) return min;
|
||||
return Math.round(Math.random() * (max - min) + min);
|
||||
}
|
||||
|
||||
// Generate random string of length from charset
|
||||
static randStr(length = 2) {
|
||||
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let output = "";
|
||||
for(let i = 0; i < length; i++) {
|
||||
output += charset.charAt(Math.floor(Math.random() * charset.length));
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
// Give generated background image to parent thread
|
||||
setBg(image) {
|
||||
if(typeof image !== "string") throw new TypeError("Image must be of type 'string'");
|
||||
postMessage(["BG_UPDATE", image]);
|
||||
}
|
||||
|
||||
// Generate and set a glitchy image
|
||||
glitch() {
|
||||
if(!this.bg.current) return;
|
||||
const image = this.bg.current.replaceAll(Generator.randStr(), Generator.randStr());
|
||||
this.setBg(image);
|
||||
}
|
||||
|
||||
// Fetch a base64 encoded background image
|
||||
async fetchBg(id) {
|
||||
const url = new URL(this.bg.dir);
|
||||
|
||||
url.pathname += id + ".txt";
|
||||
|
||||
const image = await fetch(url);
|
||||
if(!image.ok) throw new Error("Failed to fetch background image");
|
||||
|
||||
return image.text();
|
||||
}
|
||||
|
||||
// Load a random background from the image set
|
||||
async randBg() {
|
||||
const id = Generator.randInt(1, this.bg.count);
|
||||
|
||||
const image = await this.fetchBg(id);
|
||||
this.bg.current = image;
|
||||
}
|
||||
}
|
41
public/assets/js/glitch/Glitch.mjs
Normal file
41
public/assets/js/glitch/Glitch.mjs
Normal file
|
@ -0,0 +1,41 @@
|
|||
export default class Glitch {
|
||||
constructor(target) {
|
||||
this.worker = new Worker(this.getWorkerScriptURL());
|
||||
this.worker.addEventListener("message", event => this.message(event));
|
||||
|
||||
this.target = target ? target : document.body;
|
||||
}
|
||||
|
||||
// Update the target CSS background with an image URL
|
||||
setVisibleBg(image) {
|
||||
this.target.style.setProperty("background-image", `url(${image})`);
|
||||
}
|
||||
|
||||
// Get URL for the dedicated worker
|
||||
getWorkerScriptURL() {
|
||||
const name = "GlitchWorker.js";
|
||||
const url = new URL(import.meta.url);
|
||||
|
||||
// Replace pathname of this file with worker
|
||||
const path = url.pathname.split("/");
|
||||
path[path.length - 1] = name;
|
||||
|
||||
url.pathname = path.join("/");
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
// Event handler for messages from worker thread
|
||||
message(event) {
|
||||
const data = typeof event.data === "object" ? event.data : [event.data];
|
||||
|
||||
switch(data[0]) {
|
||||
case "READY":
|
||||
this.worker.postMessage(["START", new URL(location).toString()]);
|
||||
break;
|
||||
|
||||
case "BG_UPDATE":
|
||||
this.setVisibleBg(data[1]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
48
public/assets/js/glitch/GlitchWorker.js
Normal file
48
public/assets/js/glitch/GlitchWorker.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
importScripts("./Generator.mjs");
|
||||
|
||||
class GlitchWorker extends Generator {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Delay between these values
|
||||
this.config = {
|
||||
glitch: { min: 500, max: 2500 },
|
||||
randBg: { min: 5000, max: 5000 }
|
||||
}
|
||||
|
||||
this._timers = {};
|
||||
|
||||
self.addEventListener("message", event => this.message(event));
|
||||
self.postMessage("READY");
|
||||
}
|
||||
|
||||
test() {
|
||||
console.log("yes");
|
||||
}
|
||||
|
||||
// Run a scoped function on a random interval between
|
||||
queue(func) {
|
||||
clearTimeout(this._timers[func]);
|
||||
const next = Generator.randInt(this.config[func].min, this.config[func].max);
|
||||
this._timers[func] = setTimeout(() => this.queue(func), next);
|
||||
|
||||
this[func]?.();
|
||||
}
|
||||
|
||||
// Event handler for messages from parent thread
|
||||
message(event) {
|
||||
const data = typeof event.data === "object" ? event.data : [event.data];
|
||||
|
||||
switch(data[0]) {
|
||||
case "START":
|
||||
this.bg.dir = data[1];
|
||||
this.randBg();
|
||||
for(const func of Object.keys(this.config)) {
|
||||
this.queue(func);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.glitch = new GlitchWorker();
|
|
@ -1,57 +0,0 @@
|
|||
export default class Background {
|
||||
constructor(target) {
|
||||
this.images = {
|
||||
dir: "assets/media/b64/",
|
||||
count: 2
|
||||
}
|
||||
|
||||
this.image = null; // Will contain the original base64 image
|
||||
this.target = target ? target : document.body; // Set `background-image` of this element
|
||||
|
||||
// Update the background with a new image every now and then
|
||||
this.updateBg = {
|
||||
_this: this,
|
||||
_delay: 5000,
|
||||
_interval: null,
|
||||
set running (state = true) {
|
||||
clearInterval(this._interval);
|
||||
if(state) this._interval = setInterval(() => this._this.randBg(), this._delay);
|
||||
},
|
||||
set delay (delay) {
|
||||
this._delay = delay;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the target CSS background
|
||||
setBg(image = this.image) {
|
||||
this.target.style.setProperty("background-image", `url(${image})`);
|
||||
}
|
||||
|
||||
// Genrate random int in range
|
||||
randInt(min, max) {
|
||||
return Math.round(Math.random() * (max - min) + min);
|
||||
}
|
||||
|
||||
// Fetch a base64 encoded background image
|
||||
async fetchBg(id) {
|
||||
const url = new URL(window.location);
|
||||
|
||||
url.pathname += this.images.dir;
|
||||
url.pathname += id + ".txt";
|
||||
|
||||
const image = await fetch(url);
|
||||
if(!image.ok) throw new Error("Failed to fetch background image");
|
||||
|
||||
return image.text();
|
||||
}
|
||||
|
||||
// Load a random background from the image set
|
||||
async randBg() {
|
||||
const id = this.randInt(1, this.images.count);
|
||||
const image = await this.fetchBg(id);
|
||||
|
||||
this.image = image;
|
||||
this.setBg(image);
|
||||
}
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
import { default as Background } from "./Background.mjs";
|
||||
|
||||
export default class Glitch extends Background {
|
||||
constructor(target) {
|
||||
super(target);
|
||||
|
||||
this.interval = {
|
||||
_this: this,
|
||||
_interval: null,
|
||||
// Queue the next glitch
|
||||
set next (timeout) {
|
||||
clearTimeout(this._interval);
|
||||
if(timeout !== false) this._interval = setTimeout(() => this._this.glitch(), timeout);
|
||||
}
|
||||
}
|
||||
|
||||
// Stop playback when page is not visible
|
||||
document.addEventListener("visibilitychange",() => {
|
||||
if(document.visibilityState === "visible") return this.start();
|
||||
this.stop();
|
||||
});
|
||||
|
||||
this.start();
|
||||
}
|
||||
|
||||
// Generate random string of length from charset
|
||||
randStr(length = 2) {
|
||||
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let output = "";
|
||||
for(let i = 0; i < length; i++) {
|
||||
output += charset.charAt(Math.floor(Math.random() * charset.length));
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
// Create a glitchy image and queue the next one
|
||||
glitch() {
|
||||
if(!this.image) return;
|
||||
const image = this.image.replaceAll(this.randStr(), this.randStr());
|
||||
this.setBg(image);
|
||||
|
||||
this.interval.next = this.randInt(1500, 5000);
|
||||
}
|
||||
|
||||
start() {
|
||||
this.interval.next = 500;
|
||||
this.updateBg.running = true;
|
||||
this.randBg();
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.interval.next = false;
|
||||
this.updateBg.running = false;
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
export default class Modal {
|
||||
constructor() {
|
||||
this.assetsRoot = "/";
|
||||
|
||||
this.importStyleSheet();
|
||||
console.log("post", Symbol.for("modal.style"));
|
||||
}
|
||||
|
||||
importStyleSheet() {
|
||||
const style = document.createElement("link");
|
||||
style.setAttribute("rel", "stylesheet");
|
||||
style.setAttribute("href", this.assetsRoot + "css/modal.css");
|
||||
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
}
|
|
@ -1,29 +1,14 @@
|
|||
import { default as Glitch } from "./modules/Glitch.mjs";
|
||||
import { default as Glitch } from "./glitch/Glitch.mjs";
|
||||
|
||||
const logging = "https://victorwesterlund-logging-dnzfgzf6za-lz.a.run.app";
|
||||
|
||||
async function openModal(name) {
|
||||
const module = await import("./modules/Modal.mjs");
|
||||
if(!module) {
|
||||
alert("Failed to import module.");
|
||||
return;
|
||||
}
|
||||
|
||||
const modal = new module.default();
|
||||
modal.assetsRoot = window.location.href;
|
||||
}
|
||||
|
||||
// Bind click listerners to all links
|
||||
// Log link clicks
|
||||
for(let link of document.getElementsByTagName("a")) {
|
||||
link.addEventListener("click", event => {
|
||||
event.preventDefault();
|
||||
navigator?.sendBeacon(logging, event); // Log link click
|
||||
|
||||
// Treat tag without func data attribute as a normal link
|
||||
if(!"func" in event.target.dataset) window.location.href = event.target.href;
|
||||
|
||||
openModal(event.target.getAttribute("href"));
|
||||
navigator?.sendBeacon(logging, event);
|
||||
window.location.href = event.target.href;
|
||||
});
|
||||
}
|
||||
|
||||
new Glitch(document.body);
|
||||
window.glitch = new Glitch(document.body);
|
1
public/assets/media/b64/3.txt
Normal file
1
public/assets/media/b64/3.txt
Normal file
File diff suppressed because one or more lines are too long
BIN
public/assets/media/pfp/128.jpg
Normal file
BIN
public/assets/media/pfp/128.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
BIN
public/assets/media/pfp/128.webp
Normal file
BIN
public/assets/media/pfp/128.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
BIN
public/assets/media/pfp/256.jpg
Normal file
BIN
public/assets/media/pfp/256.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.1 KiB |
BIN
public/assets/media/pfp/256.webp
Normal file
BIN
public/assets/media/pfp/256.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
|
@ -20,12 +20,17 @@
|
|||
<h1>full-stack</h1>
|
||||
<h1>developer</h1>
|
||||
<p>from Sweden</p>
|
||||
<a href="contact" data-func>contact me</a>
|
||||
<a href="https://github.com/VictorWesterlund">my github -></a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div id="card">
|
||||
<img src="https://lh3.googleusercontent.com/a-/AOh14Ggkm-Fr7rjHKeJHKHNOZoM72lARq25kIJS73Wo0SU4=s128-c-rg-br100" alt="portrait of victor"/>
|
||||
<picture>
|
||||
<source srcset="assets/media/pfp/256.webp" type="image/webp" media="(min-width: 600px)">
|
||||
<source srcset="assets/media/pfp/128.webp" type="image/webp">
|
||||
<source srcset="assets/media/pfp/256.jpg" type="image/jpg" media="(min-width: 600px)">
|
||||
<img src="assets/media/pfp/128.jpg" alt="portrait of victor"/>
|
||||
</picture>
|
||||
<div>
|
||||
<p>I create things with code. When I'm not creating things with code, I enjoy skiing, watching movies and some occasional gaming</p>
|
||||
<p>And beyond computer science, I'm also an armchair rabbit-holer for engineering, physics and astronomy</p>
|
||||
|
@ -33,7 +38,7 @@
|
|||
<div>
|
||||
<p>...and ☕, full-time</p>
|
||||
</div>
|
||||
<a href="about">stalk me 😬</a>
|
||||
<a href="contact" data-func>contact me</a>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="assets/js/script.mjs"></script>
|
||||
|
|
Loading…
Add table
Reference in a new issue