wip(22w7f): refactor glitch

This commit is contained in:
Victor Westerlund 2022-02-18 15:37:14 +01:00
parent 9fc0b00cec
commit 173007ba71
14 changed files with 214 additions and 193 deletions

View file

@ -2,11 +2,7 @@
--color-base: 0, 0, 0; --color-base: 0, 0, 0;
--color-contrast: 256, 256, 256; --color-contrast: 256, 256, 256;
--padding: 40px; --padding: clamp(40px, 2vw, 2vw);
--font-min: 30px;
--font-tar: 10vw;
--font-max: 3vh;
} }
/* -- Cornerstones -- */ /* -- Cornerstones -- */
@ -33,26 +29,17 @@ body {
background-position: fixed; background-position: fixed;
} }
:is(p, h1) { picture {
font-size: var(--font-min); display: contents;
user-select: none;
}
:is(#intro, #card) a {
--padding-vert: 17px;
display: inline-block;
text-decoration: none;
text-align: center;
font-size: 20px;
} }
h1 { h1 {
font-size: clamp(var(--font-min), var(--font-tar), var(--font-max)); font-size: clamp(45px, 7vw, 6vh);
} }
p { p, a {
font-size: clamp(calc(var(--font-min) / 2), calc(var(--font-tar) / 2), calc(var(--font-max) / 2)); font-size: clamp(20px, 3vw, 2vh);
text-align: justify;
} }
/* -- Components -- */ /* -- Components -- */
@ -69,24 +56,32 @@ body > div {
padding: calc(var(--padding) / 2); 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 { #intro {
--font-tar: 13vw;
--font-max: 5vh;
padding: calc(var(--padding) / 2); padding: calc(var(--padding) / 2);
} }
#intro a { #intro a {
padding: var(--padding-vert) 40px; padding: var(--padding-vert) 2vw;
border-radius: 100px; 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; margin: var(--padding) 0;
width: calc(100% - (var(--padding) * 2)); width: calc(100% - var(--padding));
} }
#intro p { #intro p {
margin: 10px 0; margin: 1vh 0;
font-size: clamp(20px, 3vw, 3vh);
} }
/* --- */ /* --- */
@ -100,37 +95,37 @@ body > div {
} }
#card { #card {
--portrait-size: 128px; --portrait-size: clamp(128px, 12vw, 12vh);
gap: var(--padding);
position: relative; position: relative;
max-width: 600px; max-width: 600px;
padding: var(--padding); padding: var(--padding);
padding-top: calc(var(--portrait-size) - (var(--padding) / 2)); border-radius: clamp(18px, 1vw, 1vw);
border-radius: 18px; backdrop-filter: saturate(100) brightness(.4);
backdrop-filter: saturate(100) brightness(.3); -webkit-backdrop-filter: saturate(100) brightness(.4);
-webkit-backdrop-filter: saturate(100) brightness(.3); box-shadow: 0 1vh 2vh rgba(0, 0, 0, .19), 0 6px 6px rgba(0, 0, 0, .23);
box-shadow: 0 10px 20px rgba(0, 0, 0, .19), 0 6px 6px rgba(0, 0, 0, .23);
} }
#card > img { #card img {
width: var(--portrait-size); width: var(--portrait-size);
height: var(--portrait-size); height: var(--portrait-size);
position: absolute; position: absolute;
border-radius: 100%; border-radius: 100%;
top: calc((var(--portrait-size) / 2) * -1); 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 { #card a {
width: 100%; width: 100%;
padding: var(--padding-vert) 0; 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); background-color: rgba(var(--color-contrast), .13);
box-shadow: box-shadow:
inset 0 3px 16px rgba(0, 0, 0, 0), inset 0 .3vh 1.6vh rgba(0, 0, 0, 0),
0 1px 3px rgba(0, 0, 0, .12), 0 .1vh .3vh rgba(0, 0, 0, .12),
0 1px 2px rgba(0, 0, 0, .24); 0 .1vh .2vh rgba(0, 0, 0, .24);
} }
/* -- Media Queries -- */ /* -- Media Queries -- */
@ -150,9 +145,9 @@ body > div {
:is(#intro, #card) a:hover { :is(#intro, #card) a:hover {
background-color: rgba(var(--color-contrast), .2); background-color: rgba(var(--color-contrast), .2);
box-shadow: box-shadow:
inset 0 3px 16px rgba(0, 0, 0, .16), inset 0 .3vh 1.6vh rgba(0, 0, 0, .16),
0 3px 6px rgba(0, 0, 0, .16), 0 .3vh .6vh rgba(0, 0, 0, .16),
0 3px 6px rgba(0, 0, 0, .23); 0 .3vh .6vh rgba(0, 0, 0, .23);
} }
:is(#intro, #card) a:active { :is(#intro, #card) a:active {
@ -164,6 +159,7 @@ body > div {
body { body {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
gap: unset;
} }
body > div { body > div {
@ -181,5 +177,6 @@ body > div {
#card { #card {
min-width: 300px; min-width: 300px;
max-width: 30vw;
} }
} }

View 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;
}
}

View 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;
}
}
}

View 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();

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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"; const logging = "https://victorwesterlund-logging-dnzfgzf6za-lz.a.run.app";
async function openModal(name) { // Log link clicks
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
for(let link of document.getElementsByTagName("a")) { for(let link of document.getElementsByTagName("a")) {
link.addEventListener("click", event => { link.addEventListener("click", event => {
event.preventDefault(); event.preventDefault();
navigator?.sendBeacon(logging, event); // Log link click navigator?.sendBeacon(logging, event);
window.location.href = event.target.href;
// 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"));
}); });
} }
new Glitch(document.body); window.glitch = new Glitch(document.body);

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -20,12 +20,17 @@
<h1>full-stack</h1> <h1>full-stack</h1>
<h1>developer</h1> <h1>developer</h1>
<p>from Sweden</p> <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>
<div> <div>
<div id="card"> <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> <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>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> <p>And beyond computer science, I'm also an armchair rabbit-holer for engineering, physics and astronomy</p>
@ -33,7 +38,7 @@
<div> <div>
<p>...and ☕, full-time</p> <p>...and ☕, full-time</p>
</div> </div>
<a href="about">stalk me 😬</a> <a href="contact" data-func>contact me</a>
</div> </div>
</div> </div>
<script type="module" src="assets/js/script.mjs"></script> <script type="module" src="assets/js/script.mjs"></script>