feat: optimize image hash
This commit is contained in:
		
							
								
								
									
										24
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								package.json
									
									
									
									
									
								
							| @@ -10,31 +10,31 @@ | ||||
|     "astro": "astro" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@astrojs/check": "^0.5.9", | ||||
|     "@astrojs/mdx": "^2.2.1", | ||||
|     "@astrojs/svelte": "^5.2.0", | ||||
|     "@astrojs/check": "^0.5.10", | ||||
|     "@astrojs/mdx": "^2.2.4", | ||||
|     "@astrojs/svelte": "^5.3.0", | ||||
|     "@astrojs/tailwind": "^5.1.0", | ||||
|     "astro": "^4.5.5", | ||||
|     "astro": "^4.5.16", | ||||
|     "astro-i18n-aut": "^0.7.0", | ||||
|     "svelte": "^4.2.12", | ||||
|     "svelte-gestures": "^4.0.0", | ||||
|     "tailwindcss": "^3.4.1", | ||||
|     "tailwindcss": "^3.4.3", | ||||
|     "thumbhash": "^0.1.1", | ||||
|     "typescript": "^5.4.2" | ||||
|     "typescript": "^5.4.4" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@astrojs/sitemap": "^3.1.1", | ||||
|     "@astrojs/sitemap": "^3.1.2", | ||||
|     "@iconify-json/tabler": "^1.1.109", | ||||
|     "@types/markdown-it": "^13.0.7", | ||||
|     "@unocss/preset-icons": "^0.58.8", | ||||
|     "@unocss/reset": "^0.58.8", | ||||
|     "astro-font": "^0.0.78", | ||||
|     "@types/markdown-it": "^14.0.0", | ||||
|     "@unocss/preset-icons": "^0.59.0", | ||||
|     "@unocss/reset": "^0.59.0", | ||||
|     "astro-font": "^0.0.80", | ||||
|     "markdown-it": "^14.1.0", | ||||
|     "ogl": "^1.0.6", | ||||
|     "prettier": "^3.2.5", | ||||
|     "prettier-plugin-astro": "^0.13.0", | ||||
|     "sharp": "^0.33.3", | ||||
|     "unocss": "^0.58.8", | ||||
|     "unocss": "^0.59.0", | ||||
|     "unplugin-icons": "^0.18.5", | ||||
|     "vite-plugin-glsl": "^1.3.0" | ||||
|   } | ||||
|   | ||||
							
								
								
									
										1362
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1362
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -25,7 +25,6 @@ article a, | ||||
|   color: var(--fill) !important; | ||||
| } | ||||
|  | ||||
|  | ||||
| .dark .noise::before { | ||||
|   opacity: 1; | ||||
| } | ||||
| @@ -65,3 +64,34 @@ video { | ||||
| article iframe { | ||||
|   border-radius: 10px; | ||||
| } | ||||
|  | ||||
| picture.thumb::before { | ||||
|   content: ""; | ||||
|   position: absolute; | ||||
|   bottom: 10px; | ||||
|   right: 10px; | ||||
|   width: 30px; | ||||
|   height: 30px; | ||||
|   opacity: 0; | ||||
|   transition: opacity 0.3s; | ||||
|   pointer-events: none; | ||||
|   animation: spin 1s linear infinite; | ||||
|   background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' class='icon icon-tabler icons-tabler-outline icon-tabler-loader-2'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'/%3E%3Cpath d='M12 3a9 9 0 1 0 9 9' /%3E%3C/svg%3E"); | ||||
|   background-size: cover; | ||||
|   z-index: 2; | ||||
| } | ||||
|  | ||||
|  | ||||
| picture.thumb-loading::before { | ||||
|   opacity: 0.5; | ||||
| } | ||||
|  | ||||
| @keyframes spin { | ||||
|   0% { | ||||
|     transform: rotate(0deg); | ||||
|   } | ||||
|  | ||||
|   100% { | ||||
|     transform: rotate(360deg); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -10,12 +10,14 @@ interface Props { | ||||
|   maxWidth?: number; | ||||
| } | ||||
| import { rgbaToThumbHash } from "thumbhash"; | ||||
| import sharp from "sharp"; | ||||
|  | ||||
| const { src: image, hash = true, alt, maxWidth } = Astro.props; | ||||
|  | ||||
| let thumbhash = ""; | ||||
|  | ||||
| const s = await import("sharp"); | ||||
| const sharp = s.default; | ||||
|  | ||||
| if (hash) { | ||||
|   const scaleFactor = 100 / Math.max(image.width, image.height); | ||||
|  | ||||
| @@ -57,7 +59,7 @@ const sizes = [ | ||||
|   src={image} | ||||
|   alt={alt} | ||||
|   data-thumbhash={thumbhash} | ||||
|   pictureAttributes={{ class: hash ? "block h-full" : "" }} | ||||
|   pictureAttributes={{ class: hash ? "block h-full relative" : "" }} | ||||
|   class={Astro.props.class} | ||||
|   widths={sizes.map((size) => size.width)} | ||||
|   sizes={sizes | ||||
|   | ||||
							
								
								
									
										0
									
								
								src/components/Max.png
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										0
									
								
								src/components/Max.png
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -11,6 +11,8 @@ | ||||
|     }, 600); | ||||
|   } | ||||
|  | ||||
|   console.log("Thumbhash script loaded"); | ||||
|  | ||||
|   document.querySelectorAll("[data-thumbhash]").forEach((entry) => { | ||||
|     const parent = entry?.parentNode as HTMLPictureElement; | ||||
|     const img = entry as HTMLImageElement; | ||||
| @@ -35,6 +37,10 @@ | ||||
|     parent.style.background = `url(${dataURL})`; | ||||
|     parent.style.backgroundSize = "cover"; | ||||
|  | ||||
|     if (img.complete) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     img.style.opacity = "0"; | ||||
|     img.style.filter = "blur(5px)"; | ||||
|     img.style.transition = "opacity 0.6s ease, filter 0.8s ease"; | ||||
|   | ||||
| @@ -8,7 +8,6 @@ interface Props { | ||||
| } | ||||
|  | ||||
| const { title, width = "compact" } = Astro.props; | ||||
| import Thumbhash from "@components/Thumbhash.astro"; | ||||
| --- | ||||
|  | ||||
| <!doctype html> | ||||
| @@ -103,7 +102,7 @@ import Thumbhash from "@components/Thumbhash.astro"; | ||||
|         --text-light: white; | ||||
|       } | ||||
|     </style> | ||||
|     <script> | ||||
|     <script is:inline> | ||||
|       (function () { | ||||
|         try { | ||||
|           var mode = localStorage.getItem("theme"); | ||||
| @@ -127,12 +126,72 @@ import Thumbhash from "@components/Thumbhash.astro"; | ||||
|     <footer> | ||||
|       <LanguagePicker /> | ||||
|     </footer> | ||||
|     <Thumbhash /> | ||||
|     <style> | ||||
|       .layout-compact { | ||||
|         max-width: 600px; | ||||
|         margin: 0 auto; | ||||
|       } | ||||
|     </style> | ||||
|     <script is:inline> | ||||
|       (function () { | ||||
|         // prettier-ignore | ||||
|         function rgbaToDataURL(w, h, rgba) { let row = w * 4 + 1; let idat = 6 + h * (5 + row); let bytes = [ 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, w >> 8, w & 255, 0, 0, h >> 8, h & 255, 8, 6, 0, 0, 0, 0, 0, 0, 0, idat >>> 24, (idat >> 16) & 255, (idat >> 8) & 255, idat & 255, 73, 68, 65, 84, 120, 1, ]; let table = [ 0, 498536548, 997073096, 651767980, 1994146192, 1802195444, 1303535960, 1342533948, -306674912, -267414716, -690576408, -882789492, -1687895376, -2032938284, -1609899400, -1111625188, ]; let a = 1, b = 0; for (let y = 0, i = 0, end = row - 1; y < h; y++, end += row - 1) { bytes.push( y + 1 < h ? 0 : 1, row & 255, row >> 8, ~row & 255, (row >> 8) ^ 255, 0,); for (b = (b + a) % 65521; i < end; i++) { let u = rgba[i] & 255; bytes.push(u); a = (a + u) % 65521; b = (b + a) % 65521; } } bytes.push( b >> 8, b & 255, a >> 8, a & 255, 0, 0, 0, 0, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130,); for (let [start, end] of [ [12, 29], [37, 41 + idat], ]) { let c = ~0; for (let i = start; i < end; i++) { c ^= bytes[i]; c = (c >>> 4) ^ table[c & 15]; c = (c >>> 4) ^ table[c & 15]; } c = ~c; bytes[end++] = c >>> 24; bytes[end++] = (c >> 16) & 255; bytes[end++] = (c >> 8) & 255; bytes[end++] = c & 255; } return "data:image/png;base64," + btoa(String.fromCharCode(...bytes)); } | ||||
|         // prettier-ignore | ||||
|         function thumbHashToApproximateAspectRatio(hash) { let header = hash[3]; let hasAlpha = hash[2] & 0x80; let isLandscape = hash[4] & 0x80; let lx = isLandscape ? (hasAlpha ? 5 : 7) : header & 7; let ly = isLandscape ? header & 7 : hasAlpha ? 5 : 7; return lx / ly; } | ||||
|         // prettier-ignore | ||||
|         function thumbHashToRGBA(hash) { let { PI, min, max, cos, round } = Math; let header24 = hash[0] | (hash[1] << 8) | (hash[2] << 16); let header16 = hash[3] | (hash[4] << 8); let l_dc = (header24 & 63) / 63; let p_dc = ((header24 >> 6) & 63) / 31.5 - 1; let q_dc = ((header24 >> 12) & 63) / 31.5 - 1; let l_scale = ((header24 >> 18) & 31) / 31; let hasAlpha = header24 >> 23; let p_scale = ((header16 >> 3) & 63) / 63; let q_scale = ((header16 >> 9) & 63) / 63; let isLandscape = header16 >> 15; let lx = max(3, isLandscape ? (hasAlpha ? 5 : 7) : header16 & 7); let ly = max(3, isLandscape ? header16 & 7 : hasAlpha ? 5 : 7); let a_dc = hasAlpha ? (hash[5] & 15) / 15 : 1; let a_scale = (hash[5] >> 4) / 15; let ac_start = hasAlpha ? 6 : 5; let ac_index = 0; let decodeChannel = (nx, ny, scale) => { let ac = []; for (let cy = 0; cy < ny; cy++) for (let cx = cy ? 0 : 1; cx * ny < nx * (ny - cy); cx++) ac.push( (((hash[ac_start + (ac_index >> 1)] >> ((ac_index++ & 1) << 2)) & 15) / 7.5 - 1) * scale,); return ac; }; let l_ac = decodeChannel(lx, ly, l_scale); let p_ac = decodeChannel(3, 3, p_scale * 1.25); let q_ac = decodeChannel(3, 3, q_scale * 1.25); let a_ac = hasAlpha && decodeChannel(5, 5, a_scale); let ratio = thumbHashToApproximateAspectRatio(hash); let w = round(ratio > 1 ? 32 : 32 * ratio); let h = round(ratio > 1 ? 32 / ratio : 32); let rgba = new Uint8Array(w * h * 4), fx = [], fy = []; for (let y = 0, i = 0; y < h; y++) { for (let x = 0; x < w; x++, i += 4) { let l = l_dc, p = p_dc, q = q_dc, a = a_dc; for (let cx = 0, n = max(lx, hasAlpha ? 5 : 3); cx < n; cx++) fx[cx] = cos((PI / w) * (x + 0.5) * cx); for (let cy = 0, n = max(ly, hasAlpha ? 5 : 3); cy < n; cy++) fy[cy] = cos((PI / h) * (y + 0.5) * cy); for (let cy = 0, j = 0; cy < ly; cy++) for ( let cx = cy ? 0 : 1, fy2 = fy[cy] * 2; cx * ly < lx * (ly - cy); cx++, j++) l += l_ac[j] * fx[cx] * fy2; for (let cy = 0, j = 0; cy < 3; cy++) { for ( let cx = cy ? 0 : 1, fy2 = fy[cy] * 2; cx < 3 - cy; cx++, j++) { let f = fx[cx] * fy2; p += p_ac[j] * f; q += q_ac[j] * f; } } if (hasAlpha) for (let cy = 0, j = 0; cy < 5; cy++) for ( let cx = cy ? 0 : 1, fy2 = fy[cy] * 2; cx < 5 - cy; cx++, j++) a += a_ac[j] * fx[cx] * fy2; let b = l - (2 / 3) * p; let r = (3 * l - b + q) / 2; let g = r - q; rgba[i] = max(0, 255 * min(1, r)); rgba[i + 1] = max(0, 255 * min(1, g)); rgba[i + 2] = max(0, 255 * min(1, b)); rgba[i + 3] = max(0, 255 * min(1, a)); } } return { w, h, rgba }; } | ||||
|  | ||||
|         function show(img) { | ||||
|           img.style.opacity = "1"; | ||||
|           img.style.filter = "blur(0px)"; | ||||
|           setTimeout(() => { | ||||
|             if (img.parentNode) { | ||||
|               img.parentNode.style.background = ""; | ||||
|               img.parentNode.classList.remove("thumb-loading"); | ||||
|             } | ||||
|           }, 600); | ||||
|         } | ||||
|  | ||||
|         document.querySelectorAll("[data-thumbhash]").forEach((entry) => { | ||||
|           const parent = entry?.parentNode; | ||||
|           const img = entry; | ||||
|  | ||||
|           if (parent?.nodeName !== "PICTURE") return; | ||||
|           parent.classList.add("thumb"); | ||||
|  | ||||
|           const hash = img.getAttribute("data-thumbhash"); | ||||
|           if (!hash) return; | ||||
|  | ||||
|           // its only browser, why you have to be mad | ||||
|           const decodedString = window.atob(hash); | ||||
|  | ||||
|           // Create Uint8Array from decoded string | ||||
|           const buffer = new Uint8Array(decodedString.length); | ||||
|           for (let i = 0; i < decodedString.length; i++) { | ||||
|             buffer[i] = decodedString.charCodeAt(i); | ||||
|           } | ||||
|  | ||||
|           const image = thumbHashToRGBA(buffer); | ||||
|           const dataURL = rgbaToDataURL(image.w, image.h, image.rgba); | ||||
|  | ||||
|           parent.classList.add("thumb-loading"); | ||||
|           parent.style.background = `url(${dataURL})`; | ||||
|           parent.style.backgroundSize = "cover"; | ||||
|  | ||||
|           if (img.complete) return; | ||||
|  | ||||
|           img.style.opacity = "0"; | ||||
|           img.style.filter = "blur(5px)"; | ||||
|           img.style.transition = "opacity 0.6s ease, filter 0.8s ease"; | ||||
|  | ||||
|           const sources = parent.querySelectorAll("source"); | ||||
|  | ||||
|           img.onload = () => show(img); | ||||
|           for (const source of sources) { | ||||
|             source.addEventListener("load", () => show(img)); | ||||
|           } | ||||
|         }); | ||||
|       })(); | ||||
|     </script> | ||||
|   </body> | ||||
| </html> | ||||
|   | ||||
| @@ -12,7 +12,7 @@ export default defineConfig({ | ||||
|     "bg": ['bg-neutral-000', "dark:bg-neutral-500"], | ||||
|     "bg-light": ['bg-neutral-100', "dark:bg-neutral-400"], | ||||
|     "text-neutral": ['text-neutral-900', "dark:text-neutral-100"], | ||||
|     "border-neutral": "border-neutral-300 dark:border-neutral-1000", | ||||
|     "border-neutral": "border-neutral-300 dark:border-neutral-400", | ||||
|     "border-light": "border-neutral-100 dark:border-neutral-500", | ||||
|     "divide-x-neutral": ['divide-x-neutral-300', "dark:divide-x-neutral-1000"], | ||||
|     "gradient": "bg-gradient-to-br from-neutral-000 to-neutral-000 dark:from-neutral-500 dark:to-neutral-800", | ||||
| @@ -34,6 +34,7 @@ export default defineConfig({ | ||||
|         "700": "var(--neutral-700)", | ||||
|         "800": "var(--neutral-800)", | ||||
|         "900": "var(--neutral-900)", | ||||
|         "1000": "var(--neutral-1000)", | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user