Custom image comparison slider component

This commit is contained in:
Martin Prokoph
2025-01-01 14:08:25 +01:00
parent 00aaba5baa
commit e2cbde4126
12 changed files with 268 additions and 91 deletions

3
.astro/icon.d.ts vendored
View File

@@ -1,5 +1,5 @@
// Automatically generated by astro-icon
// cf597ed5793ac3a0bc17ee825940f5c6cf4b61a3087f37ebca0a0576a301fee0
// e9c4a8f68baebb6ebd5f479be9d5bb2351f0c7b5d632483958d8b375af75b69b
declare module 'virtual:astro-icon' {
export type Icon =
@@ -14410,5 +14410,6 @@ declare module 'virtual:astro-icon' {
| "simple-icons:zulip"
| "simple-icons:zwave"
| "simple-icons:zyte"
| "double-arrow-slider"
| "linux";
}

15
package-lock.json generated
View File

@@ -10,8 +10,7 @@
"dependencies": {
"@astrojs/alpinejs": "^0.4.1",
"@types/alpinejs": "^3.13.10",
"alpinejs": "^3.14.1",
"img-comparison-slider": "^8.0.6"
"alpinejs": "^3.14.1"
},
"devDependencies": {
"@astrojs/mdx": "^4.0.3",
@@ -6791,12 +6790,6 @@
"node": ">= 4"
}
},
"node_modules/img-comparison-slider": {
"version": "8.0.6",
"resolved": "https://registry.npmjs.org/img-comparison-slider/-/img-comparison-slider-8.0.6.tgz",
"integrity": "sha512-ej4de7mWyjcXZvDgHq8K2a/dG8Vv+qYTdUjZa3cVILf316rLtDrHyGbh9fPvixmAFgbs30zTLfmaRDa7abjtzw==",
"license": "MIT"
},
"node_modules/immutable": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz",
@@ -9313,9 +9306,9 @@
}
},
"node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"dev": true,
"funding": [
{

View File

@@ -37,7 +37,6 @@
"dependencies": {
"@astrojs/alpinejs": "^0.4.1",
"@types/alpinejs": "^3.13.10",
"alpinejs": "^3.14.1",
"img-comparison-slider": "^8.0.6"
"alpinejs": "^3.14.1"
}
}

View File

Before

Width:  |  Height:  |  Size: 30 MiB

After

Width:  |  Height:  |  Size: 30 MiB

View File

Before

Width:  |  Height:  |  Size: 708 KiB

After

Width:  |  Height:  |  Size: 708 KiB

View File

@@ -0,0 +1,139 @@
---
interface Props {
firstTitle?: string
secondTitle?: string
sliderColor?: string
sliderShadowColor?: string
}
const { firstTitle, secondTitle, sliderColor = "white", sliderShadowColor = "black" } = Astro.props
import { Icon } from 'astro-icon/components'
---
<image-comparison>
<section class="my-6 overflow-hidden">
<div class="relative">
<div class="left-half relative">
<div class="w-full">
<slot name="first-image" />
</div>
<slot name="first-title" >
<p class="absolute top-1/2 left-1 translate-x-1/2 -translate-y-1/2 text-2xl font-bold text-center bg-[var(--background)] px-2 py-1 rounded-md">{firstTitle}</p>
</slot>
</div>
<div class="right-half absolute left-0 top-0 right-0 h-full">
<div class="clip-area">
<div class="w-full">
<slot name="second-image" />
</div>
<slot name="second-title">
<p class="absolute top-1/2 right-1 -translate-x-1/2 -translate-y-1/2 text-2xl font-bold text-center bg-[var(--background)] px-2 py-1 rounded-md">{secondTitle}</p>
</slot>
</div>
<div class="handle-container px-4 top-0 absolute z-10 h-full">
<div class="divider w-0.5 h-full block" style=`background-color: ${sliderColor}; box-shadow: 0 0 8px ${sliderShadowColor};`/>
<div class="handle top-[50%] absolute pointer-events-none ml-[1px] w-8 h-8" id="handle" style=`color: ${sliderColor};`>
<slot name="handle-icon">
<Icon name="double-arrow-slider" width="32" height="32" />
</slot>
</div>
</div>
</div>
</div>
</image-comparison>
<script>
// Define the behaviour for our new type of HTML element.
class ImageComparison extends HTMLElement {
connectedCallback() {
const buttons = this.querySelectorAll('.right-half');
buttons.forEach((button) => {
console.log(button);
dragElement(button);
});
const clamp = (value, min, max) => Math.min(Math.max(value, min), max)
function dragElement(elmnt) {
elmnt.onmousedown = dragMouseDown;
elmnt.addEventListener("touchstart", () => {
elmnt.addEventListener("touchmove", elementTouch, false);
}, false);
function dragMouseDown(e) {
e = e || window.event;
e.preventDefault();
document.onmousemove = elementDrag;
document.onmouseup = closeDragElement;
setProgress(e.clientX);
}
function setProgress(clientX) {
let rect = elmnt.getBoundingClientRect();
let progress = (clientX - rect.left) / elmnt.offsetWidth * 100;
progress = clamp(progress, 0, 100);
elmnt.style.setProperty('--progress', `${progress}%`);
}
function elementDrag(e) {
e = e || window.event;
e.preventDefault();
elmnt.style.setProperty('--transition-time', `0ms`);
elmnt.style.setProperty('cursor', 'col-resize');
setProgress(e.clientX);
}
function elementTouch(e) {
e = e || window.event;
e.preventDefault();
elmnt.addEventListener("touchend", closeDragElement, false);
elmnt.style.setProperty('--transition-time', `0ms`);
setProgress(e.touches[0].clientX);
}
function closeDragElement() {
// stop moving when mouse button is released:
document.onmouseup = null;
document.onmousemove = null;
elmnt.removeEventListener("touchend", closeDragElement, false);
elmnt.removeEventListener("touchmove", elementTouch, false);
elmnt.style.setProperty('--transition-time', '0.25s');
elmnt.style.setProperty('cursor', 'initial');
}
}
}
}
customElements.define('image-comparison', ImageComparison);
</script>
<style lang="scss" is:global>
image-comparison {
img {
max-width: 100%;
width: 100%;
}
.right-half {
--progress: 50%;
--transition-time: 0.25s;
.clip-area {
clip-path: inset(0 0 0 var(--progress));
transition: clip-path var(--transition-time);
}
.handle-container {
left: calc(var(--progress) - 1rem);
transition: left var(--transition-time),bottom var(--transition-time);
.handle {
box-sizing: border-box;
transform: translate(-50%, -50%);
color: var(--action-color);
}
}
}
}
</style>

View File

@@ -1,71 +0,0 @@
---
interface Props {
imgBefore: string
imgAfter: string
captionBefore?: string
captionAfter?: string
}
const { imgBefore, imgAfter, captionBefore, captionAfter } = Astro.props
---
<img-comparison-slider class="slider-with-animated-handle">
<figure slot="first" class="before">
<img width="100%" src={imgBefore}>
{captionBefore ? <figcaption>{captionBefore}</figcaption> : ""}
</figure>
<figure slot="second" class="after">
<img width="100%" src={imgAfter}>
{captionAfter ? <figcaption>{captionAfter}</figcaption> : ""}
</figure>
</img-comparison-slider>
<style lang="scss" is:global>
#handle {
transition: transform 0.2s;
}
#handle:hover #handle {
transform: scale(1.2);
}
img-comparison-slider:focus, img-comparison-slider:focus-visible {
outline: unset;
box-shadow: unset;
}
.before,
.after {
margin: 0;
}
.before figcaption,
.after figcaption {
background: #fff;
border: 1px solid #c0c0c0;
border-radius: 12px;
color: #2e3452;
opacity: 0.8;
padding: 12px;
position: absolute;
top: 50%;
transform: translateY(-50%);
line-height: 100%;
}
.before figcaption {
left: 12px;
}
.after figcaption {
right: 12px;
}
</style>
<script>
import "img-comparison-slider/dist/index"
import "img-comparison-slider/dist/styles.css"
</script>
<!-- <script>
import { HTMLImgComparisonSliderElement } from "img-comparison-slider";
customElements.define('img-comparison-slider', HTMLImgComparisonSliderElement);
</script> -->

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" basedOn="https://icon-sets.iconify.design/line-md/?icon-filter=double-arrow-horizontal" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path stroke-dasharray="8" stroke-dashoffset="8" d="M3 12l4 4M21 12l-4 4M3 12l4 -4M21 12l-4 -4">
<animate fill="freeze" attributeName="stroke-dashoffset" begin="0.0s" dur="0.3s" values="8;0" />
</path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@@ -4,7 +4,8 @@ title: Life is changing
author: Martin Prokoph
date: 11th October 2024
---
import ImgComparisonSlider from '../../components/ImgComparisonSlider.astro'
import ImageComparison from '../../components/ImageComparison.astro'
import { Image } from 'astro:assets'
I started modding games back in 2018, when I was just 12 years old.
In fact, Round Trees one of my most popular resourcepacks to this day had its first release back then.
@@ -24,7 +25,10 @@ MidnightLib also deserves a mention.
After becoming frustrated about the large file size of other config libraries, I had decided to take it on myself and write a tiny but still feature-packed config library, a task that I think I absolutely nailed.
It is not only the basis for the vast majority of my mods, but also a lot of third-party ones, such as Effective, The Bumblezone, Bewitchment, as well as many others.
<ImgComparisonSlider imgBefore="/blog/life/midnightlib-0.2.4.png" imgAfter="/blog/life/midnightlib-1.6.3.png" captionBefore='MidnightLib 0.2.4' captionAfter='MidnightLib 1.6.3'/>
<ImageComparison firstTitle="MidnightLib 0.2.4" secondTitle="MidnightLib 1.6.3" sliderColor='var(--action-color)' sliderShadowColor='var(--action-color)'>
<Image slot="first-image" width="1920" height="1080" alt="A config screen generated with MidnightLib 0.2.4" src="/blog/life/midnightlib-0.2.4.png"/>
<Image slot="second-image" width="1920" height="1080" alt="A config screen generated with MidnightLib 1.6.3" src="/blog/life/midnightlib-1.6.3.png"/>
</ImageComparison>
<p class="text-center italic">MidnightLib has come a long way</p>
Fast forward to today, I've created over 25 mods and resourcepacks, which have almost reached a total download amount of a staggering 100 Million!

View File

@@ -0,0 +1,29 @@
---
layout: ../../layouts/MarkdownLayout.astro
title: Image Comparison Slider for Astro
---
import ImageComparison from '../../components/ImageComparison.astro'
import { Image } from 'astro:assets';
import { Icon } from 'astro-icon/components'
# Image Comparison Slider for Astro
Image comparison sliders are useful for comparing a previous state to the current one.
Since I wasn't fully satisfied with existing solutions, I created my own.
<ImageComparison firstTitle="lol" secondTitle="omg">
<Image slot="first-image" alt="alt" width="500" height="500" src="https://images.unsplash.com/photo-1532680281192-617c0da3810c?q=80&w=2970&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"/>
<img slot="second-image" src="https://images.unsplash.com/photo-1539757812543-fe5fb5746951?q=80&w=3039&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"/>
</ImageComparison>
You can even customize the style to best fit your needs.
<ImageComparison sliderColor='var(--action-color)' sliderShadowColor='var(--action-color)'>
<Image slot="first-image" alt="alt" width="500" height="500" src="https://images.unsplash.com/photo-1532680281192-617c0da3810c?q=80&w=2970&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"/>
<img slot="second-image" src="https://images.unsplash.com/photo-1539757812543-fe5fb5746951?q=80&w=3039&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"/>
<p slot="first-title" class="absolute top-1/2 left-1 translate-x-1/2 -translate-y-1/2 text-6xl font-bold text-center text-white [text-shadow:_0_0_15px_rgb(0_0_0_/_40%)]">lol</p>
<p slot="second-title" class="absolute top-1/2 right-1 -translate-x-1/2 -translate-y-1/2 text-6xl font-bold text-center text-white [text-shadow:_0_0_15px_rgb(0_0_0_/_40%)]">omg</p>
<Icon slot="handle-icon" name="line-md:star-pulsating-filled-loop" width={32} height={32}/>
</ImageComparison>

View File

@@ -0,0 +1,71 @@
---
layout: ../../layouts/MarkdownLayout.astro
title: Tab Button Component for Astro
---
import { Icon } from 'astro-icon/components'
import CustomTabs from "../../components/CustomTabs.astro"
# Tab Button Component for Astro
Having a paragraph of your text seperated by tabs can help readers choose their own path.
It is therefore a great way to improve your website's interactivity.
<CustomTabs tabs={[{
name: `Tab 1`,
id: 'tab1',
icon: 'simple-icons:astro',
active: true
}, {
name: `Tab 2`,
id: 'tab2',
icon: 'line-md:cloud-alt-tags-filled-loop'
}]}>
<div id="tab1" data-active="true">
Content in these divs can be written as regular Markdown in MDX files.
Otherwise, you can just add any HTML tags as usual.
The tab icons are powered by the awesome [Astro Icon](https://www.astroicon.dev/) component.
</div>
<div id="tab2" class="hidden">
```java
You can even use code blocks in here :D
```
</div>
</CustomTabs>
<br/>
A common use case for these are installation guides for different operating systems:
<CustomTabs tabs={[{
name: `Windows`,
id: 'windows',
icon: 'simple-icons:windows',
active: true
}, {
name: `Linux`,
icon: 'linux',
id: 'linux'
}, {
name: `MacOS`,
icon: 'simple-icons:apple',
id: 'macos'
}, {
name: `BSD`,
icon: 'simple-icons:freebsd',
id: 'bsd'
}]}>
<div id="windows" data-active="true">
Step 1: Download a Linux ISO and flash it to a USB stick
Step 2: Uninstall Windows
Step 3: Install Linux
Step 4: Continue with the Linux section
</div>
<div id="linux" class="hidden">
Get yourself a nice meal, you're goated!
</div>
<div id="macos" class="hidden">
A monitor stand for over 1000€... Are you serious?
</div>
<div id="bsd" class="hidden">
Get a life. For real.
</div>
</CustomTabs>

View File

@@ -3,10 +3,13 @@ layout: ../../layouts/MarkdownLayout.astro
title: Better Leaves Wiki
---
import ImgComparisonSlider from '../../components/ImgComparisonSlider.astro'
import ImageComparison from '../../components/ImageComparison.astro'
import { Image } from 'astro:assets';
import { Icon } from 'astro-icon/components'
import { Notification } from 'accessible-astro-components'
import CustomTabs from "../../components/CustomTabs.astro"
import pythonCodeImage from '../../assets/betterleaves/script.png';
import ingameImage from '../../assets/betterleaves/ingame.png';
# Better Leaves Wiki
@@ -14,8 +17,10 @@ Welcome to the Better Leaves wiki.
This documentation aims to help you create your own flavour for any texturepack or mod.
<br/>
<ImgComparisonSlider imgBefore="/betterleaves/script.png" imgAfter="/betterleaves/ingame.png" captionBefore='Python Code' captionAfter='Ingame'/>
<p class="text-center italic">Better Leaves 9.0 is automatically generated using our Python script</p>
<ImageComparison firstTitle="Python Code" secondTitle="In-Game" sliderColor='var(--action-color)' sliderShadowColor='var(--action-color)'>
<Image slot="first-image" width="1920" height="1080" alt="A screenshot showing the Python script code" src={pythonCodeImage}/>
<Image slot="second-image" width="1920" height="1080" alt="A screenshot of the Better Leaves resourcepack in action" src={ingameImage}/>
</ImageComparison>
## Getting Started
First of all, you need to download the contents of the GitHub repository.
@@ -37,7 +42,7 @@ After that, follow the <a href="#building">Building</a> section to get your flav
In simple cases, it is enough to put the mod file in the /input/mods folder and continue with the <a href="#building">Building</a> section.
<center><img alt="An overview of the input/mods folder, showing the automatic unpacking process" src="/betterleaves/mods-unpacking.png" width="500"></img></center>
<p class="text-center italic">Put the .jar file ito input/mods, and the script will take care of extracting all textures with "leaves" in their name.
<p class="text-center italic">Put the .jar file into input/mods, and the script will take care of extracting all textures with "leaves" in their name.
<br/>The _temp folder is purely symbolic here, it will only exist for a few milliseconds during the extraction process.</p>
## Building