SlideShow controls - prev, next, play and download (not finished)
This commit is contained in:
@@ -90,6 +90,7 @@ export const Harmonie: Component<{}> = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div class={styles.column}>
|
<div class={styles.column}>
|
||||||
<SlideShow
|
<SlideShow
|
||||||
|
getIsLoading={getIsLoading}
|
||||||
getCanvas={getCanvas}
|
getCanvas={getCanvas}
|
||||||
getImgList={getImgList}
|
getImgList={getImgList}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,32 +1,89 @@
|
|||||||
import { Accessor, Component } from 'solid-js'
|
import { Accessor, Component, createSignal } from 'solid-js'
|
||||||
|
|
||||||
import styles from './harmonie.module.css'
|
import styles from './harmonie.module.css'
|
||||||
|
import { downloadImagesAsZip } from '../helpers/download'
|
||||||
|
|
||||||
export const SlideShow: Component<{
|
export const SlideShow: Component<{
|
||||||
|
getIsLoading: Accessor<boolean>,
|
||||||
getCanvas: Accessor<HTMLCanvasElement | undefined>,
|
getCanvas: Accessor<HTMLCanvasElement | undefined>,
|
||||||
getImgList: Accessor<[string, ImageBitmap | undefined][]>,
|
getImgList: Accessor<[string, ImageBitmap | undefined][]>,
|
||||||
}> = ({
|
}> = ({
|
||||||
|
getIsLoading,
|
||||||
getCanvas,
|
getCanvas,
|
||||||
getImgList
|
getImgList
|
||||||
}) => {
|
}) => {
|
||||||
function draw(img: ImageBitmap | undefined) {
|
const [getActive, setActive] = createSignal(-1)
|
||||||
|
const [getIsPlaying, setIsPlaying] = createSignal(false)
|
||||||
|
|
||||||
|
function draw(i: number) {
|
||||||
const canvas = getCanvas()!
|
const canvas = getCanvas()!
|
||||||
const ctx = canvas.getContext('2d')!
|
const ctx = canvas.getContext('2d')!
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||||
|
setActive(i)
|
||||||
|
const img = getImgList()[i][1]
|
||||||
if (!img) return;
|
if (!img) return;
|
||||||
|
|
||||||
ctx.drawImage(img, 0, 0)
|
ctx.drawImage(img, 0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ul class={styles.slideShow}>
|
function next(delta = 1) {
|
||||||
{ getImgList().map(([forecastDate, img]) =>
|
const count = getImgList().length
|
||||||
<li class={img ? styles.withImg : ''} onClick={() => draw(img)}>
|
if (!count) return;
|
||||||
{ format(forecastDate) }
|
|
||||||
</li>)}
|
const result = (getActive() + delta) % count
|
||||||
</ul>
|
const nextValue = result >= 0 ? result : count - 1
|
||||||
|
setActive(nextValue)
|
||||||
|
draw(nextValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
function prev() { next(-1) }
|
||||||
|
|
||||||
|
function areControlsVisible(): boolean {
|
||||||
|
return getImgList().length > 0 && !getIsLoading()
|
||||||
|
}
|
||||||
|
|
||||||
|
let playingTimeout = 0
|
||||||
|
function play() {
|
||||||
|
if (getIsPlaying()) {
|
||||||
|
clearTimeout(playingTimeout)
|
||||||
|
setIsPlaying(false)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loop() {
|
||||||
|
next()
|
||||||
|
playingTimeout = setTimeout(loop, 300)
|
||||||
|
}
|
||||||
|
setIsPlaying(true)
|
||||||
|
loop()
|
||||||
|
}
|
||||||
|
|
||||||
|
function download() {
|
||||||
|
const imgs = getImgList().filter(([,img]) => !!img) as [string, ImageBitmap][]
|
||||||
|
downloadImagesAsZip(imgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<div class={styles.slideShowControls} style={{visibility: areControlsVisible() ? 'visible' : 'hidden'}}>
|
||||||
|
<div class={styles.leftButtons}>
|
||||||
|
<input type='button' value='prev' onClick={prev} />
|
||||||
|
<input type='button' value={getIsPlaying()?'pause':'play'} onClick={play} />
|
||||||
|
<input type='button' value='next' onClick={() => next()} />
|
||||||
|
</div>
|
||||||
|
<input type='button' value='download all' onClick={download} />
|
||||||
|
</div>
|
||||||
|
<ul class={styles.slideShowList}>
|
||||||
|
{ getImgList().map(([forecastDate, img], i) =>
|
||||||
|
<li
|
||||||
|
class={`${img?styles.withImg:''} ${i===getActive()?styles.active:''}`}
|
||||||
|
onClick={() => draw(i)}
|
||||||
|
>
|
||||||
|
{ format(forecastDate) }
|
||||||
|
</li>)}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
function format(date: string) {
|
function format(date: string) {
|
||||||
return date.slice(11, 13)
|
return date.slice(11, 13) // 2025-02-23T1500Z -> 15
|
||||||
// return date
|
|
||||||
}
|
}
|
||||||
@@ -70,17 +70,34 @@
|
|||||||
padding: 10px 3px;
|
padding: 10px 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slideShow {
|
|
||||||
|
|
||||||
|
.slideShowControls {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slideShowControls .leftButtons {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slideShowList {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slideShow li {
|
.slideShowList li {
|
||||||
display: inline;
|
display: inline;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
padding: 3px 3px;
|
padding: 3px 1px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
border: 2px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.withImg {
|
.withImg {
|
||||||
background-color: rgb(187, 247, 187);
|
background-color: rgb(213, 248, 213);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slideShowList li.active {
|
||||||
|
border: 2px solid green;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
export async function downloadImagesAsZip(images: [string, ImageBitmap][]) {
|
||||||
|
downloadCompressedImage(images[0][1], '00.gzip')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function compressImage(bitmap: ImageBitmap): Promise<Blob> {
|
||||||
|
// First convert ImageBitmap to WebP using canvas
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = bitmap.width;
|
||||||
|
canvas.height = bitmap.height;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) throw new Error('Could not get canvas context');
|
||||||
|
|
||||||
|
ctx.drawImage(bitmap, 0, 0);
|
||||||
|
|
||||||
|
// Get WebP blob
|
||||||
|
const webpBlob = await new Promise<Blob>((resolve) => {
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
|
if (blob) resolve(blob);
|
||||||
|
}, 'image/webp', 0.8); // Quality 0.8, adjust as needed
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert Blob to ReadableStream
|
||||||
|
const webpStream = webpBlob.stream();
|
||||||
|
|
||||||
|
// Apply GZIP compression
|
||||||
|
const compressedStream = webpStream.pipeThrough(
|
||||||
|
new CompressionStream('gzip')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Return as Blob
|
||||||
|
return new Response(compressedStream).blob();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage example:
|
||||||
|
async function downloadCompressedImage(bitmap: ImageBitmap, filename: string) {
|
||||||
|
const compressedBlob = await compressImage(bitmap);
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(compressedBlob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename;
|
||||||
|
link.click();
|
||||||
|
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user