Liquid Glass for the web: the rim refraction, light and motion of iOS 27 glass on any rounded element. React bindings and pill tabs. MIT.
| 1 | import { describe, expect, it } from "vitest"; |
| 2 | import { hueMatrix, resolveTone, TONES, toneFilters, toneWash } from "./tone"; |
| 3 | |
| 4 | describe("toneFilters", () => { |
| 5 | it("writes each layer's filter as literal values", () => { |
| 6 | const filters = toneFilters(TONES.regular.dark); |
| 7 | expect(filters.body.full).toBe("blur(1.5px) saturate(0.8) contrast(0.53) brightness(0.43)"); |
| 8 | expect(filters.shade.full).toBe("brightness(1.33)"); |
| 9 | expect(filters.glow.full).toBe("saturate(1.1) brightness(1.3)"); |
| 10 | expect(filters.edge.full).toBe("brightness(0.66)"); |
| 11 | expect(filters.highlight.full).toBe("contrast(1.1) brightness(2.4)"); |
| 12 | }); |
| 13 | |
| 14 | it("diffuses floating glass more", () => { |
| 15 | expect(toneFilters(TONES.regular.dark, true).body.full).toBe("blur(6px) saturate(0.8) contrast(0.53) brightness(0.43)"); |
| 16 | expect(toneFilters(TONES.regular.light, true).body.full).toBe("blur(7px) saturate(0.8) contrast(0.3) brightness(1.73)"); |
| 17 | }); |
| 18 | |
| 19 | it("drops identity functions and clears the same list", () => { |
| 20 | const filters = toneFilters(TONES.clear.dark); |
| 21 | expect(filters.body).toEqual({ full: "blur(1.5px)", clear: "blur(0px)" }); |
| 22 | expect(toneFilters(TONES.clear.light).glow).toEqual({ full: "brightness(1.02)", clear: "brightness(1)" }); |
| 23 | }); |
| 24 | |
| 25 | it("leaves a layer unfiltered when every value is identity", () => { |
| 26 | const tone = { ...TONES.clear.dark, blur: 0 }; |
| 27 | expect(toneFilters(tone).body).toEqual({ full: "none", clear: "none" }); |
| 28 | }); |
| 29 | }); |
| 30 | |
| 31 | describe("resolveTone", () => { |
| 32 | it("overrides only the given appearance", () => { |
| 33 | const overrides = { dark: { tint: 0.5 } }; |
| 34 | expect(resolveTone("regular", "dark", overrides).tint).toBe(0.5); |
| 35 | expect(resolveTone("regular", "light", overrides).tint).toBe(TONES.regular.light.tint); |
| 36 | }); |
| 37 | }); |
| 38 | |
| 39 | describe("toneWash", () => { |
| 40 | it("mixes the tint by the tone's share", () => { |
| 41 | expect(toneWash(TONES.regular.light, "var(--surface)")).toBe("color-mix(in srgb, var(--surface) 40%, transparent)"); |
| 42 | expect(toneWash(TONES.regular.dark, "black")).toBe("color-mix(in srgb, black 50%, transparent)"); |
| 43 | expect(toneWash(TONES.clear.dark, "black")).toBe("transparent"); |
| 44 | }); |
| 45 | }); |
| 46 | |
| 47 | describe("hueMatrix", () => { |
| 48 | it("leaves neutral colours untinted", () => { |
| 49 | expect(hueMatrix([128, 128, 128], 1)).toBeNull(); |
| 50 | expect(hueMatrix([255, 255, 255], 1)).toBeNull(); |
| 51 | }); |
| 52 | |
| 53 | it("keeps alpha and shifts channels toward the hue", () => { |
| 54 | const rows = hueMatrix([20, 30, 60], 1)?.split(" ").map(Number) ?? []; |
| 55 | expect(rows).toHaveLength(20); |
| 56 | expect(rows.slice(15)).toEqual([0, 0, 0, 1, 0]); |
| 57 | // Blue gains, red loses: the blue row's own weight rises above red's. |
| 58 | expect(rows[12]).toBeGreaterThan(rows[0] ?? 0); |
| 59 | }); |
| 60 | |
| 61 | it("scales with strength", () => { |
| 62 | const full = hueMatrix([20, 30, 60], 1)?.split(" ").map(Number) ?? []; |
| 63 | const half = hueMatrix([20, 30, 60], 0.5)?.split(" ").map(Number) ?? []; |
| 64 | expect(Math.abs((half[12] ?? 0) - 1)).toBeCloseTo(Math.abs((full[12] ?? 0) - 1) / 2, 3); |
| 65 | }); |
| 66 | }); |
| 67 |