feat: add core for 'simple' concept

This commit is contained in:
Victor Westerlund 2022-07-18 10:20:11 +02:00
parent 7ce5ceea9e
commit 9f36aee9f0
8 changed files with 320 additions and 190 deletions

View file

@ -1,205 +1,209 @@
:root {
--color-base: 0, 0, 0;
--color-contrast: 256, 256, 256;
--padding: clamp(40px, 2vw, 2vw);
--border-size: clamp(4px, .25vw, .25vw);
}
/* -- Cornerstones -- */ /* -- Cornerstones -- */
:root {
--primer-color-base: 255, 255, 255;
--primer-color-contrast: 0, 0, 0;
--color-base: rgb(var(--primer-color-base));
--color-contrast: rgb(var(--primer-color-contrast));
--padding: 20px;
}
* { * {
margin: 0; margin: 0;
font-family: "Monaco", "Consolas", monospace, sans-serif; box-sizing: border-box;
color: rgb(var(--color-contrast)); font-family: 'Courier', sans-serif;
font-size: 20px;
color: inherit;
} }
*::selection { ::selection {
background-color: rgb(var(--color-contrast)); background-color: var(--color-contrast);
color: rgb(var(--color-base)); color: var(--color-base);
}
::placeholder {
color: rgba(var(--primer-color-contrast), .5);
} }
html,
body { body {
width: 100%; display: flex;
height: 100%; flex-direction: column;
overflow-x: hidden; align-items: center;
color: var(--color-contrast);
background-color: var(--color-base);
gap: var(--padding);
margin: var(--padding) 0;
margin-bottom: 30vh;
} }
html { a:not(p > a) {
background-color: rgba(var(--color-base), .7); text-decoration: none;
background-size: cover;
background-blend-mode: overlay;
background-position: center;
background-attachment: fixed;
}
picture {
display: contents;
}
h1 {
font-size: clamp(45px, 7vw, 6vh);
}
p, a {
font-size: clamp(20px, 3vw, 2vh);
text-align: justify;
} }
/* -- Components -- */ /* -- Components -- */
body { input {
letter-spacing: 5px;
}
input,
.button {
background-color: var(--color-contrast);
color: var(--color-base);
padding: calc(var(--padding) / 2) calc(var(--padding) * 1.5);
text-align: left;
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
justify-items: center; justify-content: center;
gap: var(--padding, 30px); gap: var(--padding);
height: 3.5em;
} }
body > div { input,
padding: calc(var(--padding) / 2); .button.phantom {
background-color: transparent;
border: solid 3px var(--color-contrast);
color: var(--color-contrast);
} }
:is(#intro, #card) a { img,
--padding-vert: clamp(17px, 1.1vw, 1.1vw); .button {
display: inline-block;
text-decoration: none;
text-align: center;
user-select: none; user-select: none;
background-color: rgba(var(--color-contrast), .13);
backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px);
box-shadow:
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);
} }
/* --- */ .button :not(p) {
height: 2em;
#intro {
padding: calc(var(--padding) / 2);
} }
#intro a { .interact::before {
padding: var(--padding-vert) 2vw; content: "tap";
border-radius: 100px; }
border: solid var(--border-size) rgba(var(--color-contrast), 0);
/* ---- */
.spacer {
display: grid;
justify-items: center;
}
body > .spacer {
margin: var(--padding) 0; margin: var(--padding) 0;
width: calc(100% - ((var(--padding) / 2) + var(--border-size)));
} }
#intro p { .spacer div {
margin: 1vh 0; width: 205px;
font-size: clamp(20px, 3vw, 3vh); height: 7px;
background-color: var(--color-contrast);
transform: rotate(-4deg);
} }
/* --- */ /* -- Content -- */
#card, section {
#card > div { max-width: 600px;
margin: var(--padding) calc(var(--padding) * 1.5);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: calc(var(--padding) / 2); text-align: justify;
gap: var(--padding);
} }
#card { section > *,
--portrait-size: clamp(128px, 12vw, 12vh); section picture > img {
position: relative;
max-width: 600px;
padding: var(--padding);
border-radius: clamp(18px, 1vw, 1vw);
backdrop-filter: saturate(100) brightness(.4);
-webkit-backdrop-filter: saturate(100) brightness(.4);
border: solid var(--border-size) rgba(var(--color-contrast), .1);
box-shadow: 0 1vh 2vh rgba(0, 0, 0, .19), 0 6px 6px rgba(0, 0, 0, .23);
}
#card img {
width: var(--portrait-size);
height: var(--portrait-size);
position: absolute;
border-radius: 100%;
top: calc(((var(--portrait-size) / 2) + var(--border-size)) * -1);
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%; width: 100%;
padding: var(--padding-vert) 0;
margin-top: calc(var(--padding) / 2);
border-radius: clamp(9px, .5vw, .5vw);
} }
/* -- Media Queries -- */ section#intro {
max-width: 250px;
text-align: center;
}
@supports ((not ((backdrop-filter: saturate(1)) and (backdrop-filter: brightness(1)))) and (not ((-webkit-backdrop-filter: saturate(1)) and (-webkit-backdrop-filter: brightness(1))))) { section#contact > div {
#card { display: grid;
background-color: rgba(var(--color-base), .7); grid-template-rows: repeat(2, 1fr);
gap: var(--padding);
}
section#sky .button img {
height: 50%;
}
section#coffee > img {
width: 100px;
margin-bottom: calc(var(--padding) * 2);
}
/* ---- */
.banner {
width: 100%;
background-color: rgba(var(--primer-color-contrast), .05);
text-align: center;
padding: calc(var(--padding) / 2) 0;
margin: var(--padding) 0;
}
/* -- Media queries -- */
@media (hover: hover) {
.button:hover {
background-color: rgba(var(--primer-color-contrast), .75);
}
.button:active {
background-color: rgba(var(--primer-color-contrast), .7);
}
/* ---- */
.button.phantom:hover {
background-color: rgba(var(--primer-color-contrast), .05);
}
.button.phantom:active {
background-color: rgba(var(--primer-color-contrast), .1);
} }
} }
@media (pointer: fine) { @media (pointer: fine) {
:is(#intro, #card) a { .button {
--transition-speed: 200ms; cursor: pointer;
transition:
var(--transition-speed) background-color,
var(--transition-speed) box-shadow,
var(--transition-speed) border-color;
} }
:is(#intro, #card) a:hover { .interact::before {
background-color: rgba(var(--color-contrast), .2); content: "click";
border-color: rgba(var(--color-contrast), .2);
box-shadow:
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 {
background-color: rgba(var(--color-contrast), .15);
} }
} }
@media (max-width: 330px) { @media (prefers-color-scheme: dark) {
p, a { :root {
text-align: left; --primer-color-base: 0, 0, 0;
font-size: 18px; --primer-color-contrast: 255, 255, 255;
} }
#card { section#coffee > img {
padding: calc(var(--padding) / 2); filter: invert(1);
} }
} }
@media (min-aspect-ratio: 14/9) and (min-height: 600px) { @media (max-width: 303px) {
body { section#sky img {
display: none;
}
}
@media (min-width: 600px) {
section#code {
display: grid; display: grid;
grid-template-columns: 1fr 40px 1fr;
width: 100%;
}
section#contact > div {
grid-template-rows: 1fr;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
gap: unset;
}
body > div {
display: grid;
align-items: center;
}
body > div:last-of-type {
padding: calc(var(--padding) * 2);
}
#intro a {
width: unset;
}
#card {
min-width: 300px;
max-width: 30vw;
} }
} }

View file

@ -0,0 +1,15 @@
export class Dialog {
constructor(target) {
this.dialog = document.createElement("dialog");
if (typeof this.dialog.showModal !== "function") {
throw new Error("Browser does not support HTMLDialogElement");
}
document.body.appendChild(this.dialog);
}
open() {
this.dialog.showModal();
}
}

View file

@ -0,0 +1,37 @@
// Create a new AudioPlayer from template
export class AudioPlayer extends Audio {
constructor(target) {
if (!target instanceof HTMLElement) {
throw new Error("Target must be an HTMLElement");
}
const src = "src" in target.dataset ? target.dataset.src : "";
super(src);
// Bind interaction with the player as play/stop
target.addEventListener("click", () => this.playState());
// Start playback when ready
this.addEventListener("canplaythrough", () => this.play(), { once: true });
// Restart playback if paused (treat as stop)
this.addEventListener("pause", () => this.currentTime = 0);
}
// Play or stop media
playState() {
// Ignore if media is still buffering
if (this.readyState !== HTMLMediaElement.HAVE_ENOUGH_DATA) {
return false;
}
// Stop media if playing
if (this.paused === false && this.ended === false) {
this.pause();
this.currentTime = 0;
return false;
}
// Play media
return this.play();
}
}

View file

@ -1,6 +0,0 @@
const search = document.getElementById("search").children[0];
const results = document.getElementById("results").children[0];
search.style.setProperty("display","none");
results.classList.add("error");
results.innerText = "Sorry, your browser isn't supported yet";

View file

@ -1,14 +1,47 @@
import { default as Glitch } from "./glitch/Glitch.mjs"; // Bind dialog box interaction
[...document.getElementsByClassName("dialog")].forEach(dialog => {
dialog.addEventListener("click", () => {
import("./modules/Dialog.mjs").then(mod => {
// Initialize dialog with interacted element
const dialogElement = new mod.Dialog(dialog);
const logging = "https://victorwesterlund-logging-dnzfgzf6za-lz.a.run.app"; // Will open data-src
dialogElement.open();
// Log link clicks
for(let link of document.getElementsByTagName("a")) {
link.addEventListener("click", event => {
event.preventDefault();
navigator?.sendBeacon(logging, event);
window.location.href = event.target.href;
}); });
});
});
// Create AudioPlayers from template on interaction
[...document.getElementsByClassName("player")].forEach(player => {
player.addEventListener("click", () => {
import("./modules/Player.mjs").then(mod => {
// Initialize AudioPlayer from template
const audioPlayer = new mod.AudioPlayer(player);
let animation;
// Animate a progress bar as media is playing
audioPlayer.addEventListener("playing", () => {
const color = "rgba(var(--primer-color-contrast), .1)";
const keyframes = [
{ boxShadow: `inset 0 0 0 0 ${color}` },
{ boxShadow: `inset ${player.offsetWidth}px 0 0 0 ${color}` }
];
const timing = {
duration: 38000, // Robot36 TX + calibration header
iterations: 1
} }
window.glitch = new Glitch(document.body.parentElement); animation = player.animate(keyframes, timing);
player.querySelector(".playstate").innerText = "stop";
});
// Stop animation if playback is interrupted
audioPlayer.addEventListener("pause", () => {
animation.cancel();
player.querySelector(".playstate").innerText = "play";
});
});
}, { once: true });
});

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.2 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 46.056 22.944"><path d="M3.143 11.479s6.452-20.136 18.27 0 21.315 0 21.315 0" fill="none" stroke="#000" stroke-linecap="round" stroke-width="5"/></svg>

After

Width:  |  Height:  |  Size: 204 B

View file

@ -7,7 +7,7 @@
<meta name="description" content="Full-stack web developer from Stockholm, Sweden."> <meta name="description" content="Full-stack web developer from Stockholm, Sweden.">
<meta name="theme-color" content="#000000"> <meta name="theme-color" content="#000000">
<meta property='og:title' content='Victor Westerlund'> <meta property='og:title' content='Victor Westerlund'>
<meta property='og:image' content='//victorwesterlund.com/assets/media/banner.jpg'> <meta property='og:image' content='assets/media/banner.jpg'>
<meta property='og:description' content='Full-stack web developer from Stockholm, Sweden.'> <meta property='og:description' content='Full-stack web developer from Stockholm, Sweden.'>
<meta property='og:url' content='https://victorwesterlund.com'> <meta property='og:url' content='https://victorwesterlund.com'>
<link rel="icon" href="assets/media/favicon-dark.png" media="(prefers-color-scheme:no-preference)"> <link rel="icon" href="assets/media/favicon-dark.png" media="(prefers-color-scheme:no-preference)">
@ -16,34 +16,79 @@
<link rel="stylesheet" href="assets/css/style.css"> <link rel="stylesheet" href="assets/css/style.css">
</head> </head>
<body> <body>
<div> <section id="intro">
<div id="intro"> <p><strong>victor westerlund</strong></p>
<p>hello, my name is</p> <div class="spacer">
<h1>victor</h1> <div></div>
<p>I'm a</p>
<h1>full-stack</h1>
<h1>developer</h1>
<p>from Sweden</p>
<a href="https://github.com/VictorWesterlund">my github -></a>
</div> </div>
<p>I make things with code and coffee</p>
</section>
<section id="code">
<a href="https://github.com/VictorWesterlund" target="_blank" rel="noreferrer noopener">
<div class="button">
<img src="assets/media/favicon-light.png" alt="Hand-drawn icon of the GitHub mascot"/>
<p>on github</p>
</div> </div>
</a>
<p>and</p>
<a href="#" target="_blank" rel="noreferrer noopener">
<div class="button">
<p>elsewhere</p>
</div>
</a>
</section>
<section id="contact">
<p>Wanna chat? The best way to get in touch with me is by sending a mail. The time is currently 00:00 in Sweden, and I will reply as soon as possible.</p>
<div> <div>
<div id="card"> <a href="#">
<div class="button">
<p>copy email address</p>
</div>
</a>
<a href="#">
<div class="button phantom dialog" data-src="">
<p>🔑 show PGP key</p>
</div>
</a>
</div>
</section>
<div class="banner">
<p><strong>random stuff</strong></p>
</div>
<section id="sky">
<picture> <picture>
<source srcset="assets/media/pfp/256.webp" type="image/webp" media="(min-width: 600px)"> <source srcset="https://friday.victorwesterlund.com/sky/original.webp" type="image/webp">
<source srcset="assets/media/pfp/128.webp" type="image/webp"> <img src="https://friday.victorwesterlund.com/sky/original.jpg" style="aspect-ratio: 16 / 9" alt="Webcam stillframe of the sky from my balcony"/>
<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> </picture>
<div> <p>Do you have an SSTV receiver? Tune in to a sneaky surprise and this beautiful view of the sky from my balcony as it looked like <strong>4 minutes ago</strong>. Or why not check out this pretty neat time-lapse.</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> <a href="#">
<p>And beyond computer science, I'm also an armchair rabbit-holer for engineering, physics and astronomy</p> <div class="button">
<p>view timelapse</p>
</div> </div>
<div> </a>
<p>...and ☕, full-time</p> <div class="button phantom player" data-src="https://friday.victorwesterlund.com/sky/robot36.wav">
<img src="assets/media/wave.svg"/>
<p><strong>Robot36</strong><br><span class="interact"></span> to <span class="playstate">play</span></p>
</div> </div>
<!--<a href="contact">contact me</a>--> <p>⚠️ <i><strong>warning:</strong> loud f*cking noise so adjust your volume accordingly.</i></p>
</section>
<div class="spacer">
<div></div>
</div> </div>
<section id="coffee">
<img src="assets/media/coffee.svg" style="aspect-ratio: 1 / 1" alt="Hand-drawn coffee icon"/>
<p>Did I mention that I like coffee? Well in the past 7 days I have had <strong>4 cups of coffee</strong>, which is roughly equivalent to <strong>0.2 cups per day</strong>. Not too impressive given that my weekly average is 2.2 cups per day.</p>
</section>
<div class="spacer">
<div></div>
</div>
<section id="sneaky">
<p>Enter your top sneaky code here. You will know when you've struck the jackpot.</p>
<input placeholder="uuddlrlrabs"></input>
<p>So you don't have a sneaky code?<br><a href="#">Solve this puzzle</a> and I'll buy you a cup of coffee.</p>
</section>
<div class="spacer">
<div></div>
</div> </div>
<script type="module" src="assets/js/script.mjs"></script> <script type="module" src="assets/js/script.mjs"></script>
</body> </body>