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-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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
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";
|
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);
|
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>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>
|
||||||
|
|
Loading…
Add table
Reference in a new issue