diff --git a/web/src/harmonie/Harmonie.tsx b/web/src/harmonie/Harmonie.tsx index f444a9b..1212b5d 100644 --- a/web/src/harmonie/Harmonie.tsx +++ b/web/src/harmonie/Harmonie.tsx @@ -90,6 +90,7 @@ export const Harmonie: Component<{}> = () => {
diff --git a/web/src/harmonie/SlideShow.tsx b/web/src/harmonie/SlideShow.tsx index 0283b83..8e3be9d 100644 --- a/web/src/harmonie/SlideShow.tsx +++ b/web/src/harmonie/SlideShow.tsx @@ -1,32 +1,89 @@ -import { Accessor, Component } from 'solid-js' +import { Accessor, Component, createSignal } from 'solid-js' import styles from './harmonie.module.css' +import { downloadImagesAsZip } from '../helpers/download' export const SlideShow: Component<{ + getIsLoading: Accessor, getCanvas: Accessor, getImgList: Accessor<[string, ImageBitmap | undefined][]>, }> = ({ + getIsLoading, getCanvas, getImgList }) => { - function draw(img: ImageBitmap | undefined) { + const [getActive, setActive] = createSignal(-1) + const [getIsPlaying, setIsPlaying] = createSignal(false) + + function draw(i: number) { const canvas = getCanvas()! const ctx = canvas.getContext('2d')! ctx.clearRect(0, 0, canvas.width, canvas.height) + setActive(i) + const img = getImgList()[i][1] if (!img) return; ctx.drawImage(img, 0, 0) } - return
    - { getImgList().map(([forecastDate, img]) => -
  • draw(img)}> - { format(forecastDate) } -
  • )} -
+ function next(delta = 1) { + const count = getImgList().length + if (!count) return; + + const result = (getActive() + delta) % count + 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 <> +
+
+ + + next()} /> +
+ +
+
    + { getImgList().map(([forecastDate, img], i) => +
  • draw(i)} + > + { format(forecastDate) } +
  • )} +
+ } function format(date: string) { - return date.slice(11, 13) - // return date + return date.slice(11, 13) // 2025-02-23T1500Z -> 15 } \ No newline at end of file diff --git a/web/src/harmonie/harmonie.module.css b/web/src/harmonie/harmonie.module.css index bb2c8e1..d2a6fb4 100644 --- a/web/src/harmonie/harmonie.module.css +++ b/web/src/harmonie/harmonie.module.css @@ -70,17 +70,34 @@ padding: 10px 3px; } -.slideShow { + + +.slideShowControls { + display: flex; + justify-content: space-between; +} + +.slideShowControls .leftButtons { + display: flex; + gap: 5px; +} + +.slideShowList { padding: 0; } -.slideShow li { +.slideShowList li { display: inline; font-size: 14px; - padding: 3px 3px; + padding: 3px 1px; border-radius: 5px; + border: 2px solid transparent; } .withImg { - background-color: rgb(187, 247, 187); + background-color: rgb(213, 248, 213); +} + +.slideShowList li.active { + border: 2px solid green; } \ No newline at end of file diff --git a/web/src/helpers/download.ts b/web/src/helpers/download.ts new file mode 100644 index 0000000..0daeca8 --- /dev/null +++ b/web/src/helpers/download.ts @@ -0,0 +1,46 @@ +export async function downloadImagesAsZip(images: [string, ImageBitmap][]) { + downloadCompressedImage(images[0][1], '00.gzip') +} + +async function compressImage(bitmap: ImageBitmap): Promise { + // 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((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); + } \ No newline at end of file