[READ-ONLY] Mirror of https://github.com/flo-bit/ui-kit. 🦊 fox ui, svelte 5 and tailwind 4 flo-bit.dev/ui-kit/
svelte tailwindcss ui-components
0

Configure Feed

Select the types of activity you want to include in your feed.

more refactoring

Florian (Mar 22, 2025, 8:18 PM +0100) 57cb5fc4 7267f798

+1189 -1214
+5 -3
src/lib/index.ts
··· 30 30 export { Textarea } from '$lib/components/base/textarea'; 31 31 export { Tooltip } from '$lib/components/base/tooltip'; 32 32 33 + // colors components 34 + export { default as ColorGradientPicker } from '$lib/components/colors/color-gradient-picker/ColorGradientPicker.svelte'; 35 + export { ColorPicker, PopoverColorPicker } from '$lib/components/colors/color-picker'; 36 + export { ColorSelect } from '$lib/components/colors/color-select'; 37 + 33 38 // extra components 34 - export { default as ColorGradientPicker } from '$lib/components/extra/color-gradient-picker/ColorGradientPicker.svelte'; 35 - export { ColorPicker, PopoverColorPicker } from '$lib/components/extra/color-picker/'; 36 39 export { default as Excalidraw } from '$lib/components/extra/excalidraw/Excalidraw.svelte'; 37 40 export { default as Undraw } from '$lib/components/extra/undraw/Undraw.svelte'; 38 - export { ColorSelect } from '$lib/components/extra/color-select'; 39 41 export { default as Stopwatch } from '$lib/components/extra/stopwatch/Stopwatch.svelte'; 40 42 export { default as Timer } from '$lib/components/extra/timer/Timer.svelte'; 41 43 export { StopwatchState } from '$lib/components/extra/stopwatch/StopwatchState.svelte';
-99
src/docs/preview/SelectTheme.svelte
··· 1 - <script lang="ts" module> 2 - let accentColor = $state({ 3 - class: '', 4 - label: '' 5 - }); 6 - 7 - let baseColor = $state({ 8 - class: '', 9 - label: '' 10 - }); 11 - </script> 12 - 13 - <script lang="ts"> 14 - import ColorSelect from '$lib/components/extra/color-select/ColorSelect.svelte'; 15 - import Subheading from '$lib/components/base/heading/Subheading.svelte'; 16 - import Text from '$lib/components/base/text/Text.svelte'; 17 - import { onMount } from 'svelte'; 18 - 19 - let accentColors = [ 20 - { class: 'text-red-500', label: 'red' }, 21 - { class: 'text-orange-500', label: 'orange' }, 22 - { class: 'text-amber-500', label: 'amber' }, 23 - { class: 'text-yellow-500', label: 'yellow' }, 24 - { class: 'text-lime-500', label: 'lime' }, 25 - { class: 'text-green-500', label: 'green' }, 26 - { class: 'text-emerald-500', label: 'emerald' }, 27 - { class: 'text-teal-500', label: 'teal' }, 28 - { class: 'text-cyan-500', label: 'cyan' }, 29 - { class: 'text-sky-500', label: 'sky' }, 30 - { class: 'text-blue-500', label: 'blue' }, 31 - { class: 'text-indigo-500', label: 'indigo' }, 32 - { class: 'text-violet-500', label: 'violet' }, 33 - { class: 'text-purple-500', label: 'purple' }, 34 - { class: 'text-fuchsia-500', label: 'fuchsia' }, 35 - { class: 'text-pink-500', label: 'pink' }, 36 - { class: 'text-rose-500', label: 'rose' } 37 - ]; 38 - 39 - let baseColors = [ 40 - { class: 'text-gray-500', label: 'gray' }, 41 - { class: 'text-stone-500', label: 'stone' }, 42 - { class: 'text-zinc-500', label: 'zinc' }, 43 - { class: 'text-neutral-500', label: 'neutral' }, 44 - { class: 'text-slate-500', label: 'slate' } 45 - ]; 46 - 47 - onMount(() => { 48 - let savedAccentColor = localStorage.getItem('accentColor'); 49 - if (savedAccentColor) { 50 - accentColor.label = JSON.parse(savedAccentColor); 51 - } else { 52 - accentColor.label = 'pink'; 53 - } 54 - 55 - let savedBaseColor = localStorage.getItem('baseColor'); 56 - if (savedBaseColor) { 57 - baseColor.label = JSON.parse(savedBaseColor); 58 - } else { 59 - baseColor.label = 'stone'; 60 - } 61 - }); 62 - 63 - let { showTitle = true } = $props(); 64 - </script> 65 - 66 - {#if showTitle} 67 - <Subheading class="mb-4">Select Theme</Subheading> 68 - {/if} 69 - 70 - <Text class="mb-2">Accent Color</Text> 71 - <ColorSelect 72 - bind:selected={accentColor} 73 - colors={accentColors} 74 - onselected={(color, previous) => { 75 - document.documentElement.classList.remove(previous.label.toLowerCase()); 76 - document.documentElement.classList.add(color.label.toLowerCase()); 77 - 78 - localStorage.setItem('accentColor', JSON.stringify(color.label)); 79 - 80 - window.dispatchEvent( 81 - new CustomEvent('theme-changed', { detail: { accentColor: color.label } }) 82 - ); 83 - }} 84 - class="w-64" 85 - /> 86 - 87 - <Text class="mt-4 mb-2">Base Color</Text> 88 - <ColorSelect 89 - bind:selected={baseColor} 90 - colors={baseColors} 91 - onselected={(color, previous) => { 92 - document.documentElement.classList.remove(previous.label.toLowerCase()); 93 - document.documentElement.classList.add(color.label.toLowerCase()); 94 - 95 - localStorage.setItem('baseColor', JSON.stringify(color.label)); 96 - 97 - window.dispatchEvent(new CustomEvent('theme-changed', { detail: { baseColor: color.label } })); 98 - }} 99 - />
+107
src/docs/site-components/SelectTheme.svelte
··· 1 + <script lang="ts" module> 2 + let accentColor = $state({ 3 + class: '', 4 + label: '' 5 + }); 6 + 7 + let baseColor = $state({ 8 + class: '', 9 + label: '' 10 + }); 11 + </script> 12 + 13 + <script lang="ts"> 14 + import { ColorSelect } from '$lib'; 15 + import Subheading from '$lib/components/base/heading/Subheading.svelte'; 16 + import Text from '$lib/components/base/text/Text.svelte'; 17 + import { onMount } from 'svelte'; 18 + 19 + let accentColors = [ 20 + { class: 'text-red-500', label: 'red' }, 21 + { class: 'text-orange-500', label: 'orange' }, 22 + { class: 'text-amber-500', label: 'amber' }, 23 + { class: 'text-yellow-500', label: 'yellow' }, 24 + { class: 'text-lime-500', label: 'lime' }, 25 + { class: 'text-green-500', label: 'green' }, 26 + { class: 'text-emerald-500', label: 'emerald' }, 27 + { class: 'text-teal-500', label: 'teal' }, 28 + { class: 'text-cyan-500', label: 'cyan' }, 29 + { class: 'text-sky-500', label: 'sky' }, 30 + { class: 'text-blue-500', label: 'blue' }, 31 + { class: 'text-indigo-500', label: 'indigo' }, 32 + { class: 'text-violet-500', label: 'violet' }, 33 + { class: 'text-purple-500', label: 'purple' }, 34 + { class: 'text-fuchsia-500', label: 'fuchsia' }, 35 + { class: 'text-pink-500', label: 'pink' }, 36 + { class: 'text-rose-500', label: 'rose' } 37 + ]; 38 + 39 + let baseColors = [ 40 + { class: 'text-gray-500', label: 'gray' }, 41 + { class: 'text-stone-500', label: 'stone' }, 42 + { class: 'text-zinc-500', label: 'zinc' }, 43 + { class: 'text-neutral-500', label: 'neutral' }, 44 + { class: 'text-slate-500', label: 'slate' } 45 + ]; 46 + 47 + onMount(() => { 48 + let savedAccentColor = localStorage.getItem('accentColor'); 49 + if (savedAccentColor) { 50 + accentColor.label = JSON.parse(savedAccentColor); 51 + } else { 52 + accentColor.label = 'pink'; 53 + } 54 + 55 + let savedBaseColor = localStorage.getItem('baseColor'); 56 + if (savedBaseColor) { 57 + baseColor.label = JSON.parse(savedBaseColor); 58 + } else { 59 + baseColor.label = 'stone'; 60 + } 61 + }); 62 + 63 + let { showTitle = true } = $props(); 64 + </script> 65 + 66 + {#if showTitle} 67 + <Subheading class="mb-4">Select Theme</Subheading> 68 + {/if} 69 + 70 + <Text class="mb-2">Accent Color</Text> 71 + <ColorSelect 72 + bind:selected={accentColor} 73 + colors={accentColors} 74 + onselected={(color, previous) => { 75 + if (typeof previous === 'string' || typeof color === 'string') { 76 + return; 77 + } 78 + 79 + document.documentElement.classList.remove(previous.label.toLowerCase()); 80 + document.documentElement.classList.add(color.label.toLowerCase()); 81 + 82 + localStorage.setItem('accentColor', JSON.stringify(color.label)); 83 + 84 + window.dispatchEvent( 85 + new CustomEvent('theme-changed', { detail: { accentColor: color.label } }) 86 + ); 87 + }} 88 + class="w-64" 89 + /> 90 + 91 + <Text class="mt-4 mb-2">Base Color</Text> 92 + <ColorSelect 93 + bind:selected={baseColor} 94 + colors={baseColors} 95 + onselected={(color, previous) => { 96 + if (typeof previous === 'string' || typeof color === 'string') { 97 + return; 98 + } 99 + 100 + document.documentElement.classList.remove(previous.label.toLowerCase()); 101 + document.documentElement.classList.add(color.label.toLowerCase()); 102 + 103 + localStorage.setItem('baseColor', JSON.stringify(color.label)); 104 + 105 + window.dispatchEvent(new CustomEvent('theme-changed', { detail: { baseColor: color.label } })); 106 + }} 107 + />
+1 -1
src/docs/site-components/ThemeSelectDropdown.svelte
··· 1 1 <script> 2 2 import { buttonVariants } from '$lib/components/base/button'; 3 3 import * as Popover from '$lib/components/base/popover'; 4 - import SelectTheme from '$docs/preview/SelectTheme.svelte'; 4 + import SelectTheme from './SelectTheme.svelte'; 5 5 import { cn } from '$lib/utils'; 6 6 </script> 7 7
+1 -1
src/docs/cards/colors/CardColorPicker.svelte
··· 1 1 <script lang="ts"> 2 2 import { ColorPicker } from '$lib'; 3 - import { oklch_string_to_oklch, oklch_to_rgb } from '$lib/components/extra/color-picker/base/color'; 3 + import { oklch_string_to_oklch, oklch_to_rgb } from '$lib/components/colors/color-picker/base/color'; 4 4 import { ThemeWatcher } from '$lib/helper/ThemeWatcher.svelte'; 5 5 import { onMount } from 'svelte'; 6 6
+200
src/lib/components/colors/color-gradient-picker/ColorGradientPicker.svelte
··· 1 + <script lang="ts"> 2 + import { cn } from '$lib'; 3 + import * as Popover from '$lib/components/base/popover'; 4 + import { 5 + ColorPicker, 6 + type OKhsv, 7 + type OKlab, 8 + type RGB 9 + } from '$lib/components/colors/color-picker/base'; 10 + import { onMount } from 'svelte'; 11 + import { type OKlch } from '../color-picker/base/color'; 12 + import { DragGesture } from '@use-gesture/vanilla'; 13 + import Button from '$lib/components/base/button/Button.svelte'; 14 + 15 + 16 + type ColorStop = { rgb: RGB; position: number }; 17 + 18 + let initialColors: ColorStop[] = $state([ 19 + { rgb: { r: 0, g: 0, b: 0 }, position: 0 }, 20 + { rgb: { r: 1, g: 0, b: 0 }, position: 0.25 }, 21 + { rgb: { r: 0, g: 1, b: 0 }, position: 0.5 }, 22 + { rgb: { r: 0, g: 0, b: 1 }, position: 0.75 }, 23 + { rgb: { r: 1, g: 1, b: 0 }, position: 1 } 24 + ]); 25 + 26 + let { 27 + colors = $bindable(initialColors), 28 + class: className, 29 + onchange 30 + }: { 31 + rgb?: RGB; 32 + 33 + colors?: ColorStop[]; 34 + 35 + class?: string; 36 + 37 + onchange?: (color: { hex: string; rgb: RGB; oklab: OKlab; okhsv: OKhsv; oklch: OKlch }) => void; 38 + } = $props(); 39 + 40 + const gradientString = $derived( 41 + `linear-gradient(to right, ${colors 42 + .toSorted((a, b) => a.position - b.position) 43 + .map( 44 + ({ rgb, position }) => 45 + `rgb(${rgb.r * 255}, ${rgb.g * 255}, ${rgb.b * 255}) ${position * 100}%` 46 + ) 47 + .join(', ')})` 48 + ); 49 + 50 + $inspect(gradientString); 51 + 52 + let colorsRef = $state<(HTMLDivElement | null)[]>(new Array(colors.length).fill(null)); 53 + let gradientRef = $state<HTMLDivElement | undefined>(undefined); 54 + let isDragging = $state(new Array(colors.length).fill(false)); 55 + 56 + $effect(() => { 57 + let gestures: DragGesture[] = []; 58 + 59 + colorsRef.forEach((ref, i) => { 60 + if (!ref) return; 61 + 62 + gestures.push( 63 + new DragGesture(ref, (state) => { 64 + console.log(state); 65 + const { delta } = state; 66 + 67 + const newPosition = delta[0] / (gradientRef?.clientWidth ?? 1); 68 + 69 + colors[i].position += newPosition; 70 + colors[i].position = Math.max(0, Math.min(1, colors[i].position)); 71 + 72 + if (Math.abs(state.offset[0]) > 10) { 73 + isDragging[i] = state.active; 74 + } else { 75 + isDragging[i] = state.active; 76 + } 77 + 78 + // let offset = Math.hypot(state.movement[0], state.movement[1]); 79 + if (state.tap) { 80 + allOpen[i] = !allOpen[i]; 81 + } 82 + }) 83 + ); 84 + }); 85 + 86 + return () => { 87 + gestures.forEach((gesture) => gesture.destroy()); 88 + }; 89 + }); 90 + 91 + let allOpen = $state(new Array(colors.length).fill(false)); 92 + 93 + function getOpen(i: number) { 94 + if (isDragging[i]) { 95 + return false; 96 + } 97 + return allOpen[i]; 98 + } 99 + </script> 100 + 101 + <!-- svelte-ignore a11y_no_static_element_interactions --> 102 + <!-- svelte-ignore a11y_click_events_have_key_events --> 103 + <div 104 + class={cn( 105 + 'border-base-400 dark:border-base-600 relative h-8 w-full cursor-pointer touch-none rounded-2xl border', 106 + className 107 + )} 108 + style={'background: ' + gradientString} 109 + bind:this={gradientRef} 110 + onclick={(e) => { 111 + // get target 112 + const target = e.target as HTMLDivElement; 113 + // if target is not the gradient itself return 114 + if (target !== gradientRef) { 115 + return; 116 + } 117 + let rect = gradientRef?.getBoundingClientRect(); 118 + // get the position of the click 119 + const position = (e.clientX - rect.left) / (gradientRef?.clientWidth ?? 1); 120 + // add a new color 121 + colors.push({ rgb: { r: 0, g: 0, b: 0 }, position: position }); 122 + colorsRef.push(null); 123 + allOpen.push(true); 124 + }} 125 + onkeydown={(e) => { 126 + if (e.key === '+') { 127 + colors.push({ rgb: { r: 0, g: 0, b: 0 }, position: 0.5 }); 128 + colorsRef.push(null); 129 + allOpen.push(true); 130 + } 131 + }} 132 + > 133 + {#each colors as color, i} 134 + <Popover.Root bind:open={() => getOpen(i), (open) => {}}> 135 + <Popover.Trigger 136 + class={cn( 137 + 'absolute -top-4 left-0 touch-none', 138 + 'focus-visible:outline-base-900 dark:focus-visible:outline-base-100 cursor-pointer rounded-full focus-visible:outline-2 focus-visible:outline-offset-2' 139 + )} 140 + style={`left: calc(${color.position * 100}% - 16px)`} 141 + bind:ref={colorsRef[i]} 142 + tabindex={Math.floor(color.position * 100 + 10)} 143 + onkeydown={(e) => { 144 + if (e.key === 'Enter') { 145 + allOpen[i] = !allOpen[i]; 146 + } else if (e.key === 'Backspace' && colors.length > 2) { 147 + colors.splice(i, 1); 148 + } 149 + }} 150 + > 151 + <div 152 + class="border-base-400 dark:border-base-500 focus-visible:outline-accent-500 shadow-base-900/50 z-10 size-8 rounded-full border shadow-lg" 153 + style={`background-color: rgb(${color.rgb.r * 255}, ${color.rgb.g * 255}, ${color.rgb.b * 255});`} 154 + ></div> 155 + </Popover.Trigger> 156 + <Popover.Content 157 + side="bottom" 158 + sideOffset={10} 159 + onInteractOutside={() => { 160 + allOpen[i] = false; 161 + }} 162 + onEscapeKeydown={() => { 163 + allOpen[i] = false; 164 + }} 165 + onkeydown={(e) => { 166 + if (e.key === 'Backspace' && colors.length > 2) { 167 + colors.splice(i, 1); 168 + } 169 + }} 170 + > 171 + <ColorPicker bind:rgb={colors[i].rgb} class={className} /> 172 + 173 + {#if colors.length > 2} 174 + <Button 175 + class="mt-4" 176 + onclick={() => { 177 + colors.splice(i, 1); 178 + }} 179 + size="sm" 180 + > 181 + <svg 182 + xmlns="http://www.w3.org/2000/svg" 183 + viewBox="0 0 24 24" 184 + fill="currentColor" 185 + class="size-6" 186 + > 187 + <path 188 + fill-rule="evenodd" 189 + d="M16.5 4.478v.227a48.816 48.816 0 0 1 3.878.512.75.75 0 1 1-.256 1.478l-.209-.035-1.005 13.07a3 3 0 0 1-2.991 2.77H8.084a3 3 0 0 1-2.991-2.77L4.087 6.66l-.209.035a.75.75 0 0 1-.256-1.478A48.567 48.567 0 0 1 7.5 4.705v-.227c0-1.564 1.213-2.9 2.816-2.951a52.662 52.662 0 0 1 3.369 0c1.603.051 2.815 1.387 2.815 2.951Zm-6.136-1.452a51.196 51.196 0 0 1 3.273 0C14.39 3.05 15 3.684 15 4.478v.113a49.488 49.488 0 0 0-6 0v-.113c0-.794.609-1.428 1.364-1.452Zm-.355 5.945a.75.75 0 1 0-1.5.058l.347 9a.75.75 0 1 0 1.499-.058l-.346-9Zm5.48.058a.75.75 0 1 0-1.498-.058l-.347 9a.75.75 0 0 0 1.5.058l.345-9Z" 190 + clip-rule="evenodd" 191 + /> 192 + </svg> 193 + 194 + Delete Gradient Stop</Button 195 + > 196 + {/if} 197 + </Popover.Content> 198 + </Popover.Root> 199 + {/each} 200 + </div>
+2
src/lib/components/colors/color-picker/index.ts
··· 1 + export { default as ColorPicker } from './base/ColorPicker.svelte'; 2 + export { default as PopoverColorPicker } from './popover/PopoverColorPicker.svelte';
+85
src/lib/components/colors/color-select/ColorSelect.svelte
··· 1 + <script lang="ts"> 2 + import type { WithElementRef, WithoutChildrenOrChild } from 'bits-ui'; 3 + import type { HTMLAttributes } from 'svelte/elements'; 4 + import { cn } from '$lib/utils'; 5 + 6 + type Color = { class?: string; label: string; value?: string } | string; 7 + 8 + let { 9 + ref = $bindable(null), 10 + 11 + class: className, 12 + colorsClass, 13 + colors = $bindable([]), 14 + selected = $bindable(colors[0]), 15 + 16 + name = $bindable(crypto.randomUUID()), 17 + 18 + onselected, 19 + ...restProps 20 + }: WithElementRef<WithoutChildrenOrChild<HTMLAttributes<HTMLDivElement>>> & { 21 + colors: Color[]; 22 + 23 + selected?: Color; 24 + 25 + colorsClass?: string; 26 + 27 + name?: string; 28 + 29 + onselected?: (color: Color, previous: Color) => void; 30 + } = $props(); 31 + 32 + function getLabel(c: Color) { 33 + if (typeof c === 'string') return c; 34 + return c.label; 35 + } 36 + 37 + function getValue(c: Color) { 38 + if (typeof c === 'string') return c; 39 + return c.value; 40 + } 41 + 42 + function getColorClass(c: Color) { 43 + if (typeof c === 'string') return ''; 44 + return c.class; 45 + } 46 + </script> 47 + 48 + <div 49 + class={cn('group flex flex-wrap items-center gap-2', className)} 50 + {...restProps} 51 + bind:this={ref} 52 + > 53 + {#each colors as color} 54 + <label 55 + aria-label={getLabel(color)} 56 + class={cn( 57 + 'group relative flex cursor-pointer items-center justify-center rounded-full p-0.5 ring-current', 58 + 'has-focus-visible:outline-base-900 dark:has-focus-visible:outline-base-100 has-focus-visible:outline-2 has-focus-visible:outline-offset-0', 59 + 'has-checked:ring-3', 60 + colorsClass, 61 + getColorClass(color) 62 + )} 63 + style={getValue(color) ? `color: ${getValue(color)}` : undefined} 64 + > 65 + <input 66 + type="radio" 67 + {name} 68 + value={getLabel(color)} 69 + class="sr-only" 70 + onchange={() => { 71 + let previous = selected; 72 + selected = color; 73 + 74 + if (onselected) onselected(color, previous); 75 + }} 76 + checked={getLabel(selected) === getLabel(color)} 77 + /> 78 + 79 + <span 80 + aria-hidden="true" 81 + class="size-8 rounded-full bg-current" 82 + ></span> 83 + </label> 84 + {/each} 85 + </div>
+1
src/lib/components/colors/color-select/index.ts
··· 1 + export { default as ColorSelect } from './ColorSelect.svelte';
-200
src/lib/components/extra/color-gradient-picker/ColorGradientPicker.svelte
··· 1 - <script lang="ts"> 2 - import { cn } from '$lib'; 3 - import * as Popover from '$lib/components/base/popover'; 4 - import { 5 - ColorPicker, 6 - type OKhsv, 7 - type OKlab, 8 - type RGB 9 - } from '$lib/components/extra/color-picker/base'; 10 - import { onMount } from 'svelte'; 11 - import { type OKlch } from '../color-picker/base/color'; 12 - import { DragGesture } from '@use-gesture/vanilla'; 13 - import Button from '$lib/components/base/button/Button.svelte'; 14 - 15 - 16 - type ColorStop = { rgb: RGB; position: number }; 17 - 18 - let initialColors: ColorStop[] = $state([ 19 - { rgb: { r: 0, g: 0, b: 0 }, position: 0 }, 20 - { rgb: { r: 1, g: 0, b: 0 }, position: 0.25 }, 21 - { rgb: { r: 0, g: 1, b: 0 }, position: 0.5 }, 22 - { rgb: { r: 0, g: 0, b: 1 }, position: 0.75 }, 23 - { rgb: { r: 1, g: 1, b: 0 }, position: 1 } 24 - ]); 25 - 26 - let { 27 - colors = $bindable(initialColors), 28 - class: className, 29 - onchange 30 - }: { 31 - rgb?: RGB; 32 - 33 - colors?: ColorStop[]; 34 - 35 - class?: string; 36 - 37 - onchange?: (color: { hex: string; rgb: RGB; oklab: OKlab; okhsv: OKhsv; oklch: OKlch }) => void; 38 - } = $props(); 39 - 40 - const gradientString = $derived( 41 - `linear-gradient(to right, ${colors 42 - .toSorted((a, b) => a.position - b.position) 43 - .map( 44 - ({ rgb, position }) => 45 - `rgb(${rgb.r * 255}, ${rgb.g * 255}, ${rgb.b * 255}) ${position * 100}%` 46 - ) 47 - .join(', ')})` 48 - ); 49 - 50 - $inspect(gradientString); 51 - 52 - let colorsRef = $state<(HTMLDivElement | null)[]>(new Array(colors.length).fill(null)); 53 - let gradientRef = $state<HTMLDivElement | undefined>(undefined); 54 - let isDragging = $state(new Array(colors.length).fill(false)); 55 - 56 - $effect(() => { 57 - let gestures: DragGesture[] = []; 58 - 59 - colorsRef.forEach((ref, i) => { 60 - if (!ref) return; 61 - 62 - gestures.push( 63 - new DragGesture(ref, (state) => { 64 - console.log(state); 65 - const { delta } = state; 66 - 67 - const newPosition = delta[0] / (gradientRef?.clientWidth ?? 1); 68 - 69 - colors[i].position += newPosition; 70 - colors[i].position = Math.max(0, Math.min(1, colors[i].position)); 71 - 72 - if (Math.abs(state.offset[0]) > 10) { 73 - isDragging[i] = state.active; 74 - } else { 75 - isDragging[i] = state.active; 76 - } 77 - 78 - // let offset = Math.hypot(state.movement[0], state.movement[1]); 79 - if (state.tap) { 80 - allOpen[i] = !allOpen[i]; 81 - } 82 - }) 83 - ); 84 - }); 85 - 86 - return () => { 87 - gestures.forEach((gesture) => gesture.destroy()); 88 - }; 89 - }); 90 - 91 - let allOpen = $state(new Array(colors.length).fill(false)); 92 - 93 - function getOpen(i: number) { 94 - if (isDragging[i]) { 95 - return false; 96 - } 97 - return allOpen[i]; 98 - } 99 - </script> 100 - 101 - <!-- svelte-ignore a11y_no_static_element_interactions --> 102 - <!-- svelte-ignore a11y_click_events_have_key_events --> 103 - <div 104 - class={cn( 105 - 'border-base-400 dark:border-base-600 relative h-8 w-full cursor-pointer touch-none rounded-2xl border', 106 - className 107 - )} 108 - style={'background: ' + gradientString} 109 - bind:this={gradientRef} 110 - onclick={(e) => { 111 - // get target 112 - const target = e.target as HTMLDivElement; 113 - // if target is not the gradient itself return 114 - if (target !== gradientRef) { 115 - return; 116 - } 117 - let rect = gradientRef?.getBoundingClientRect(); 118 - // get the position of the click 119 - const position = (e.clientX - rect.left) / (gradientRef?.clientWidth ?? 1); 120 - // add a new color 121 - colors.push({ rgb: { r: 0, g: 0, b: 0 }, position: position }); 122 - colorsRef.push(null); 123 - allOpen.push(true); 124 - }} 125 - onkeydown={(e) => { 126 - if (e.key === '+') { 127 - colors.push({ rgb: { r: 0, g: 0, b: 0 }, position: 0.5 }); 128 - colorsRef.push(null); 129 - allOpen.push(true); 130 - } 131 - }} 132 - > 133 - {#each colors as color, i} 134 - <Popover.Root bind:open={() => getOpen(i), (open) => {}}> 135 - <Popover.Trigger 136 - class={cn( 137 - 'absolute -top-4 left-0 touch-none', 138 - 'focus-visible:outline-base-900 dark:focus-visible:outline-base-100 cursor-pointer rounded-full focus-visible:outline-2 focus-visible:outline-offset-2' 139 - )} 140 - style={`left: calc(${color.position * 100}% - 16px)`} 141 - bind:ref={colorsRef[i]} 142 - tabindex={Math.floor(color.position * 100 + 10)} 143 - onkeydown={(e) => { 144 - if (e.key === 'Enter') { 145 - allOpen[i] = !allOpen[i]; 146 - } else if (e.key === 'Backspace' && colors.length > 2) { 147 - colors.splice(i, 1); 148 - } 149 - }} 150 - > 151 - <div 152 - class="border-base-400 dark:border-base-500 focus-visible:outline-accent-500 shadow-base-900/50 z-10 size-8 rounded-full border shadow-lg" 153 - style={`background-color: rgb(${color.rgb.r * 255}, ${color.rgb.g * 255}, ${color.rgb.b * 255});`} 154 - ></div> 155 - </Popover.Trigger> 156 - <Popover.Content 157 - side="bottom" 158 - sideOffset={10} 159 - onInteractOutside={() => { 160 - allOpen[i] = false; 161 - }} 162 - onEscapeKeydown={() => { 163 - allOpen[i] = false; 164 - }} 165 - onkeydown={(e) => { 166 - if (e.key === 'Backspace' && colors.length > 2) { 167 - colors.splice(i, 1); 168 - } 169 - }} 170 - > 171 - <ColorPicker bind:rgb={colors[i].rgb} class={className} /> 172 - 173 - {#if colors.length > 2} 174 - <Button 175 - class="mt-4" 176 - onclick={() => { 177 - colors.splice(i, 1); 178 - }} 179 - size="sm" 180 - > 181 - <svg 182 - xmlns="http://www.w3.org/2000/svg" 183 - viewBox="0 0 24 24" 184 - fill="currentColor" 185 - class="size-6" 186 - > 187 - <path 188 - fill-rule="evenodd" 189 - d="M16.5 4.478v.227a48.816 48.816 0 0 1 3.878.512.75.75 0 1 1-.256 1.478l-.209-.035-1.005 13.07a3 3 0 0 1-2.991 2.77H8.084a3 3 0 0 1-2.991-2.77L4.087 6.66l-.209.035a.75.75 0 0 1-.256-1.478A48.567 48.567 0 0 1 7.5 4.705v-.227c0-1.564 1.213-2.9 2.816-2.951a52.662 52.662 0 0 1 3.369 0c1.603.051 2.815 1.387 2.815 2.951Zm-6.136-1.452a51.196 51.196 0 0 1 3.273 0C14.39 3.05 15 3.684 15 4.478v.113a49.488 49.488 0 0 0-6 0v-.113c0-.794.609-1.428 1.364-1.452Zm-.355 5.945a.75.75 0 1 0-1.5.058l.347 9a.75.75 0 1 0 1.499-.058l-.346-9Zm5.48.058a.75.75 0 1 0-1.498-.058l-.347 9a.75.75 0 0 0 1.5.058l.345-9Z" 190 - clip-rule="evenodd" 191 - /> 192 - </svg> 193 - 194 - Delete Gradient Stop</Button 195 - > 196 - {/if} 197 - </Popover.Content> 198 - </Popover.Root> 199 - {/each} 200 - </div>
-2
src/lib/components/extra/color-picker/index.ts
··· 1 - export { default as ColorPicker } from './base/ColorPicker.svelte'; 2 - export { default as PopoverColorPicker } from './popover/PopoverColorPicker.svelte';
-85
src/lib/components/extra/color-select/ColorSelect.svelte
··· 1 - <script lang="ts"> 2 - import type { WithElementRef, WithoutChildrenOrChild } from 'bits-ui'; 3 - import type { HTMLAttributes } from 'svelte/elements'; 4 - import { cn } from '$lib/utils'; 5 - 6 - type Color = { class?: string; label: string; value?: string } | string; 7 - 8 - let { 9 - ref = $bindable(null), 10 - 11 - class: className, 12 - colorsClass, 13 - colors = $bindable([]), 14 - selected = $bindable(colors[0]), 15 - 16 - name = $bindable(crypto.randomUUID()), 17 - 18 - onselected, 19 - ...restProps 20 - }: WithElementRef<WithoutChildrenOrChild<HTMLAttributes<HTMLDivElement>>> & { 21 - colors: Color[]; 22 - 23 - selected?: Color; 24 - 25 - colorsClass?: string; 26 - 27 - name?: string; 28 - 29 - onselected?: (color: Color, previous: Color) => void; 30 - } = $props(); 31 - 32 - function getLabel(c: Color) { 33 - if (typeof c === 'string') return c; 34 - return c.label; 35 - } 36 - 37 - function getValue(c: Color) { 38 - if (typeof c === 'string') return c; 39 - return c.value; 40 - } 41 - 42 - function getColorClass(c: Color) { 43 - if (typeof c === 'string') return ''; 44 - return c.class; 45 - } 46 - </script> 47 - 48 - <div 49 - class={cn('group flex flex-wrap items-center gap-2', className)} 50 - {...restProps} 51 - bind:this={ref} 52 - > 53 - {#each colors as color} 54 - <label 55 - aria-label={getLabel(color)} 56 - class={cn( 57 - 'group relative flex cursor-pointer items-center justify-center rounded-full p-0.5 ring-current', 58 - 'has-focus-visible:outline-base-900 dark:has-focus-visible:outline-base-100 has-focus-visible:outline-2 has-focus-visible:outline-offset-0', 59 - 'has-checked:ring-3', 60 - colorsClass, 61 - getColorClass(color) 62 - )} 63 - style={getValue(color) ? `color: ${getValue(color)}` : undefined} 64 - > 65 - <input 66 - type="radio" 67 - {name} 68 - value={getLabel(color)} 69 - class="sr-only" 70 - onchange={() => { 71 - let previous = selected; 72 - selected = color; 73 - 74 - if (onselected) onselected(color, previous); 75 - }} 76 - checked={getLabel(selected) === getLabel(color)} 77 - /> 78 - 79 - <span 80 - aria-hidden="true" 81 - class="size-8 rounded-full bg-current" 82 - ></span> 83 - </label> 84 - {/each} 85 - </div>
-1
src/lib/components/extra/color-select/index.ts
··· 1 - export { default as ColorSelect } from './ColorSelect.svelte';
-21
src/lib/components/extra/undraw/Undraw.svelte
··· 4 4 import { cn } from '$lib/utils'; 5 5 6 6 import { load, type CheerioAPI } from 'cheerio'; 7 - import { okhsv_to_rgb, rgb_to_hex, rgb_to_okhsv } from '../color-picker/base/color'; 8 7 9 8 let { 10 9 ref = $bindable(null), ··· 74 73 loadedSvg('line').each((_, el) => { 75 74 applyClasses(loadedSvg, el); 76 75 }); 77 - 78 - 79 - // loadedSvg('text').each((_, el) => { 80 - // loadedSvg(el).removeAttr('fill'); 81 - // loadedSvg(el).addClass('fill-base-800 dark:fill-base-100'); 82 - // }); 83 - 84 - // loadedSvg('rect').each((_, el) => { 85 - // loadedSvg(el).removeAttr('fill'); 86 - // loadedSvg(el).addClass('fill-accent-600 dark:fill-accent-500'); 87 - // }); 88 - 89 - // loadedSvg('path').each((_, el) => { 90 - // loadedSvg(el).removeAttr('stroke'); 91 - // loadedSvg(el).addClass('stroke-accent-600 dark:stroke-accent-500'); 92 - // if (loadedSvg(el).attr('fill') !== 'none') { 93 - // loadedSvg(el).addClass('fill-accent-600 dark:fill-accent-500'); 94 - // loadedSvg(el).removeAttr('fill'); 95 - // } 96 - // }); 97 76 98 77 return loadedSvg.html(); 99 78 }
+252
src/lib/components/colors/color-picker/base/ColorPicker.svelte
··· 1 + <script lang="ts"> 2 + import { eps, picker_size, slider_width, border_size, gap_size } from './constants'; 3 + import { render_main_image, render_slider_image } from './render'; 4 + import type { RGB, OKlab, OKhsv, OKlch } from './color'; 5 + import { 6 + okhsv_to_oklab, 7 + okhsv_to_rgb, 8 + okhsv_to_oklch, 9 + rgb_to_hex, 10 + oklab_to_okhsv, 11 + rgb_to_okhsv 12 + } from './color'; 13 + import { cn } from '$lib'; 14 + 15 + let { 16 + rgb = $bindable(), 17 + oklab = $bindable(), 18 + okhsv = $bindable(), 19 + class: className, 20 + onchange 21 + }: { 22 + rgb?: RGB; 23 + oklab?: OKlab; 24 + okhsv?: OKhsv; 25 + class?: string; 26 + onchange?: (color: { hex: string; rgb: RGB; oklab: OKlab; okhsv: OKhsv; oklch: OKlch }) => void; 27 + } = $props(); 28 + 29 + const width = picker_size + slider_width + gap_size + border_size * 2; 30 + const height = picker_size + border_size * 2; 31 + 32 + let color = $derived(convertToInternal(rgb, oklab, okhsv)); 33 + let uihsv = $derived(scale_to_ui(color)); 34 + 35 + function scale_to_ui(okhsv: OKhsv): OKhsv { 36 + return { 37 + h: clamp_ui(okhsv.h / 360), 38 + s: clamp_ui(okhsv.s), 39 + v: clamp_ui(1 - okhsv.v) 40 + }; 41 + } 42 + 43 + function clamp(x: number) { 44 + return x < eps ? eps : x > 1 - eps ? 1 - eps : x; 45 + } 46 + 47 + function clamp_ui(value: number) { 48 + return value < 0 ? 0 : value > 1 ? picker_size : value * picker_size; 49 + } 50 + 51 + function convertToInternal( 52 + rgb: RGB | undefined, 53 + oklab: OKlab | undefined, 54 + okhsv: OKhsv | undefined 55 + ) { 56 + if (okhsv) { 57 + return okhsv; 58 + } 59 + 60 + if (oklab) { 61 + return oklab_to_okhsv(oklab); 62 + } 63 + 64 + if (rgb) { 65 + return rgb_to_okhsv(rgb); 66 + } 67 + 68 + throw 'rgb, oklab, or okhsv required'; 69 + } 70 + 71 + async function update_input() { 72 + let new_rgb = okhsv_to_rgb(color); 73 + let new_oklab = okhsv_to_oklab(color); 74 + let new_oklch = okhsv_to_oklch(color); 75 + let new_hex = rgb_to_hex(new_rgb); 76 + let new_okhsv = color; 77 + 78 + if (okhsv) { 79 + okhsv = color; 80 + } 81 + 82 + if (oklab) { 83 + oklab = new_oklab; 84 + } 85 + 86 + if (rgb) { 87 + rgb = new_rgb; 88 + } 89 + 90 + onchange?.({ 91 + rgb: new_rgb, 92 + oklab: new_oklab, 93 + okhsv: new_okhsv, 94 + oklch: new_oklch, 95 + hex: new_hex 96 + }); 97 + } 98 + 99 + function update_sv(x: number, y: number) { 100 + let new_s = clamp(x / picker_size); 101 + let new_v = clamp(1 - y / picker_size); 102 + 103 + color.s = new_s; 104 + color.v = new_v; 105 + 106 + update_input(); 107 + } 108 + 109 + function update_h(x: number, y: number) { 110 + let h = clamp(y / picker_size); 111 + color.h = h * 360; 112 + color.s = Math.max(color.s, 0.00001); 113 + color.v = Math.max(color.v, 0.00001); 114 + 115 + update_input(); 116 + } 117 + 118 + function pointer(node: HTMLCanvasElement, fn: (x: number, y: number) => void) { 119 + let active = false; 120 + 121 + function update(event: PointerEvent) { 122 + const x = event.offsetX; 123 + const y = event.offsetY; 124 + fn(x, y); 125 + } 126 + 127 + function onpointerdown(event: PointerEvent) { 128 + event.stopPropagation(); 129 + node.setPointerCapture(event.pointerId); 130 + update(event); 131 + active = true; 132 + } 133 + 134 + function onpointermove(event: PointerEvent) { 135 + event.stopPropagation(); 136 + if (active) { 137 + update(event); 138 + } 139 + } 140 + 141 + function onpointerend(event: PointerEvent) { 142 + event.stopPropagation(); 143 + node.releasePointerCapture(event.pointerId); 144 + active = false; 145 + } 146 + 147 + node.addEventListener('pointerdown', onpointerdown, { passive: true }); 148 + node.addEventListener('pointermove', onpointermove, { passive: true }); 149 + node.addEventListener('pointerup', onpointerend, { passive: true }); 150 + node.addEventListener('pointercancel', onpointerend, { passive: true }); 151 + 152 + return { 153 + destroy() { 154 + node.removeEventListener('pointerdown', onpointerdown); 155 + node.removeEventListener('pointermove', onpointermove); 156 + node.removeEventListener('pointerup', onpointerend); 157 + node.removeEventListener('pointercancel', onpointerend); 158 + } 159 + }; 160 + } 161 + 162 + function onKeydown(event: KeyboardEvent) { 163 + const keyHandled = () => { 164 + event.preventDefault(); 165 + event.stopPropagation(); 166 + }; 167 + 168 + const step = event.shiftKey ? 10 : 1; 169 + 170 + switch (event.key) { 171 + case 'ArrowUp': 172 + if (event.altKey) { 173 + update_h(0, Math.round(uihsv.h! - step)); 174 + } else { 175 + update_sv(uihsv.s, Math.round(uihsv.v - step)); 176 + } 177 + keyHandled(); 178 + break; 179 + 180 + case 'ArrowDown': 181 + if (event.altKey) { 182 + update_h(0, Math.round(uihsv.h! + step)); 183 + } else { 184 + update_sv(uihsv.s, Math.round(uihsv.v + step)); 185 + } 186 + keyHandled(); 187 + break; 188 + 189 + case 'ArrowLeft': 190 + update_sv(Math.round(uihsv.s - step), uihsv.v); 191 + keyHandled(); 192 + break; 193 + 194 + case 'ArrowRight': 195 + update_sv(Math.round(uihsv.s + step), uihsv.v); 196 + keyHandled(); 197 + break; 198 + } 199 + } 200 + </script> 201 + 202 + <!-- svelte-ignore a11y_no_noninteractive_tabindex --> 203 + <!-- svelte-ignore a11y_no_static_element_interactions --> 204 + <div 205 + class={cn( 206 + 'focus-visible:outline-base-900 dark:focus-visible:outline-base-100 relative rounded-xl border-none focus-visible:outline-2 focus-visible:outline-offset-2', 207 + className 208 + )} 209 + tabindex="0" 210 + style:width="{width}px" 211 + style:height="{height}px" 212 + onkeydown={onKeydown} 213 + > 214 + <canvas 215 + id="okhsv_sv_canvas" 216 + width={picker_size} 217 + height={picker_size} 218 + style:top="{border_size}px" 219 + style:left="{border_size}px" 220 + class="absolute touch-none rounded-xl" 221 + use:pointer={update_sv} 222 + use:render_main_image={color.h} 223 + ></canvas> 224 + <canvas 225 + width={slider_width} 226 + height={picker_size} 227 + style:top="{border_size}px" 228 + style:left="{picker_size + gap_size}px" 229 + class="absolute touch-none rounded-xl" 230 + use:pointer={update_h} 231 + use:render_slider_image 232 + ></canvas> 233 + 234 + <svg {width} {height} class="pointer-events-none absolute touch-none"> 235 + <g transform="translate({border_size},{border_size})"> 236 + <g transform="translate({uihsv.s},{uihsv.v})"> 237 + <circle cx="0" cy="0" r="5" fill="none" stroke-width="1.75" class="stroke-base-50" /> 238 + <circle cx="0" cy="0" r="6" fill="none" stroke-width="1.25" class="stroke-base-950" /> 239 + </g> 240 + </g> 241 + <g transform="translate({picker_size + gap_size},{border_size})"> 242 + <g transform="translate(0,{uihsv.h})"> 243 + <polygon points="-7,-4 -1,0 -7,4" stroke-width="0.8" class="stroke-base-950 fill-base-50" /> 244 + <polygon 245 + points="{slider_width + 7},-4 {slider_width + 1},0 {slider_width + 7},4" 246 + stroke-width="0.8" 247 + class="stroke-base-950 fill-base-50" 248 + /> 249 + </g> 250 + </g> 251 + </svg> 252 + </div>
+138
src/lib/components/colors/color-picker/base/color.ts
··· 1 + import { convert, OKHSL, OKHSV, OKLab, OKLCH, sRGB } from '@texel/color'; 2 + 3 + export type Color = string | RGB | OKlab | OKhsv | OKlch; 4 + 5 + export type SetColorFunction = (color: Color) => void; 6 + 7 + export interface RGB { 8 + r: number; 9 + g: number; 10 + b: number; 11 + } 12 + 13 + export interface OKlab { 14 + l: number; 15 + a: number; 16 + b: number; 17 + } 18 + 19 + export interface OKhsv { 20 + h: number; 21 + s: number; 22 + v: number; 23 + } 24 + 25 + export interface OKhsl { 26 + h: number; 27 + s: number; 28 + l: number; 29 + } 30 + 31 + export interface OKlch { 32 + l: number; 33 + c: number; 34 + h: number; 35 + } 36 + 37 + export function okhsv_to_oklch(okhsv: OKhsv): OKlch { 38 + const [l, c, h] = convert([okhsv.h, okhsv.s, okhsv.v], OKHSV, OKLCH); 39 + return { l: l, c: c, h: h }; 40 + } 41 + 42 + export function oklch_to_okhsv(oklch: OKlch): OKhsv { 43 + const [h, s, v] = convert([oklch.h, oklch.c, oklch.l], OKLCH, OKHSV); 44 + return { h: h ?? 0, s, v }; 45 + } 46 + 47 + export function oklab_to_okhsv(oklab: OKlab): OKhsv { 48 + const [h, s, v] = convert([oklab.l, oklab.a, oklab.b], OKLab, OKHSV); 49 + return { h: h ?? 0, s, v }; 50 + } 51 + 52 + export function okhsv_to_rgb(okhsv: OKhsv): RGB { 53 + const [r, g, b] = convert([okhsv.h, okhsv.s, okhsv.v], OKHSV, sRGB); 54 + return { r, g, b }; 55 + } 56 + 57 + export function okhsv_to_oklab(okhsv: OKhsv): OKlab { 58 + const [l, a, b] = convert([okhsv.h, okhsv.s, okhsv.v], OKHSV, OKLab); 59 + return { l, a, b }; 60 + } 61 + 62 + export function rgb_to_okhsv(rgb: RGB): OKhsv { 63 + const [h, s, v] = convert([rgb.r, rgb.g, rgb.b], sRGB, OKHSV); 64 + return { h, s, v }; 65 + } 66 + 67 + export function rgb_to_oklab(rgb: RGB): OKlab { 68 + const [l, a, b] = convert([rgb.r, rgb.g, rgb.b], sRGB, OKLab); 69 + return { l, a, b }; 70 + } 71 + 72 + export function okhsl_to_rgb(okhsl: OKhsl): RGB { 73 + const [r, g, b] = convert([okhsl.h, okhsl.s, okhsl.l], OKHSL, sRGB); 74 + return { r, g, b }; 75 + } 76 + 77 + export function oklab_to_oklch(oklab: OKlab): OKlch { 78 + const [l, c, h] = convert([oklab.l, oklab.a, oklab.b], OKLab, OKLCH); 79 + return { l, c, h }; 80 + } 81 + 82 + export function rgb_to_oklch(rgb: RGB): OKlch { 83 + const [l, c, h] = convert([rgb.r, rgb.g, rgb.b], sRGB, OKLCH); 84 + return { l, c, h }; 85 + } 86 + 87 + export function oklab_to_rgb(oklab: OKlab): RGB { 88 + const [r, g, b] = convert([oklab.l, oklab.a, oklab.b], OKLab, sRGB); 89 + return { r, g, b }; 90 + } 91 + 92 + export function oklch_to_rgb(oklch: OKlch): RGB { 93 + const [r, g, b] = convert([oklch.l, oklch.c, oklch.h], OKLCH, sRGB); 94 + return { r, g, b }; 95 + } 96 + 97 + export function oklch_string_to_oklch(oklch: string): OKlch { 98 + const converted = oklch.split('oklch(')[1].split(')')[0].split(' '); 99 + return { l: parseFloat(converted[0]), c: parseFloat(converted[1]), h: parseFloat(converted[2]) }; 100 + } 101 + 102 + export function rgb_to_hex(rgb: RGB): string { 103 + const r = Math.round(rgb.r * 255); 104 + const g = Math.round(rgb.g * 255); 105 + const b = Math.round(rgb.b * 255); 106 + return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`; 107 + } 108 + 109 + export function hex_to_rgb(hex: string): RGB { 110 + if (hex.startsWith('#')) { 111 + hex = hex.slice(1); 112 + } else if (hex.startsWith('0x')) { 113 + hex = hex.slice(2); 114 + } 115 + 116 + if (hex.length !== 6) { 117 + console.warn('Invalid hex color', hex); 118 + return { r: 0, g: 0, b: 0 }; 119 + } 120 + 121 + const r = parseInt(hex.slice(0, 2), 16); 122 + const g = parseInt(hex.slice(2, 4), 16); 123 + const b = parseInt(hex.slice(4, 6), 16); 124 + const r_ = r / 255; 125 + const g_ = g / 255; 126 + const b_ = b / 255; 127 + return { r: r_, g: g_, b: b_ }; 128 + } 129 + 130 + export function hex_to_okhsv(hex: string): OKhsv { 131 + const rgb = hex_to_rgb(hex); 132 + return rgb_to_okhsv(rgb); 133 + } 134 + 135 + export function okhsv_to_hex(okhsv: OKhsv): string { 136 + const rgb = okhsv_to_rgb(okhsv); 137 + return rgb_to_hex(rgb); 138 + }
+5
src/lib/components/colors/color-picker/base/constants.ts
··· 1 + export const picker_size = 200; 2 + export const slider_width = 16; 3 + export const border_size = 10; 4 + export const gap_size = 30; 5 + export const eps = 0.0001;
+5
src/lib/components/colors/color-picker/base/index.ts
··· 1 + // adapted from https://github.com/CaptainCodeman/svelte-color-select 2 + 3 + export { default } from './ColorPicker.svelte'; 4 + export { default as ColorPicker } from './ColorPicker.svelte'; 5 + export type { RGB, OKlab, OKhsv } from './color';
+100
src/lib/components/colors/color-picker/base/render.ts
··· 1 + import { f_shader, v_shader } from './shaders'; 2 + import { picker_size, slider_width } from './constants'; 3 + import { okhsl_to_rgb } from './color'; 4 + 5 + export function render_slider_image(canvas: HTMLCanvasElement) { 6 + const ctx = canvas.getContext('2d')!; 7 + const data = new Uint8ClampedArray(picker_size * slider_width * 4); 8 + 9 + for (let i = 0; i < picker_size; i++) { 10 + const a_ = Math.cos((2 * Math.PI * i) / picker_size); 11 + const b_ = Math.sin((2 * Math.PI * i) / picker_size); 12 + 13 + const rgb = okhsl_to_rgb({ 14 + h: (i / picker_size) * 360, 15 + s: 0.9, 16 + l: 0.65 + 0.17 * b_ - 0.08 * a_ 17 + }); 18 + 19 + for (let j = 0; j < slider_width; j++) { 20 + const index = 4 * (i * slider_width + j); 21 + data[index + 0] = rgb.r * 255; 22 + data[index + 1] = rgb.g * 255; 23 + data[index + 2] = rgb.b * 255; 24 + data[index + 3] = 255; 25 + } 26 + } 27 + 28 + const imageData = new ImageData(data, slider_width); 29 + ctx.putImageData(imageData, 0, 0); 30 + } 31 + 32 + export function render_main_image(canvas: HTMLCanvasElement, hue: number) { 33 + const gl = canvas.getContext('webgl')!; 34 + 35 + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); 36 + 37 + const shaderProgram = initShaderProgram(gl, v_shader, f_shader)!; 38 + 39 + const position = gl.getAttribLocation(shaderProgram, 'position'); 40 + const resolution = gl.getUniformLocation(shaderProgram, 'resolution'); 41 + const hue_rad = gl.getUniformLocation(shaderProgram, 'hue_rad'); 42 + 43 + gl.useProgram(shaderProgram); 44 + gl.uniform2fv(resolution, [gl.canvas.width, gl.canvas.height]); 45 + 46 + const positions = [1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, -1.0]; 47 + const positionBuffer = gl.createBuffer(); 48 + 49 + gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); 50 + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW); 51 + gl.enableVertexAttribArray(position); 52 + gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0); 53 + 54 + function update(hue: number) { 55 + requestAnimationFrame(() => { 56 + gl.uniform1f(hue_rad, hue / 360); 57 + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 58 + }); 59 + } 60 + 61 + update(hue); 62 + 63 + return { 64 + update, 65 + destroy() { 66 + gl.deleteProgram(shaderProgram); 67 + } 68 + }; 69 + } 70 + 71 + function initShaderProgram(gl: WebGLRenderingContext, vsSource: string, fsSource: string) { 72 + const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource)!; 73 + const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource)!; 74 + 75 + const shaderProgram = gl.createProgram()!; 76 + gl.attachShader(shaderProgram, vertexShader); 77 + gl.attachShader(shaderProgram, fragmentShader); 78 + gl.linkProgram(shaderProgram); 79 + 80 + if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { 81 + throw `unable to init shader: ${gl.getProgramInfoLog(shaderProgram)}`; 82 + } 83 + 84 + return shaderProgram; 85 + } 86 + 87 + function loadShader(gl: WebGLRenderingContext, type: number, source: string) { 88 + const shader = gl.createShader(type)!; 89 + 90 + gl.shaderSource(shader, source); 91 + gl.compileShader(shader); 92 + 93 + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { 94 + const info = gl.getShaderInfoLog(shader); 95 + gl.deleteShader(shader); 96 + throw `error compiling shaders: ${info}`; 97 + } 98 + 99 + return shader; 100 + }
+7
src/lib/components/colors/color-picker/base/shaders.ts
··· 1 + /// <reference types="vite-plugin-glsl/ext" /> 2 + 3 + import f_shader_src from './shaders/f_shader.glsl'; 4 + import v_shader_src from './shaders/v_shader.glsl'; 5 + 6 + export const f_shader = f_shader_src; 7 + export const v_shader = v_shader_src;
+67
src/lib/components/colors/color-picker/popover/PopoverColorPicker.svelte
··· 1 + <script lang="ts"> 2 + import { cn } from '$lib'; 3 + import * as Popover from '$lib/components/base/popover'; 4 + import { 5 + ColorPicker, 6 + type OKhsv, 7 + type OKlab, 8 + type RGB 9 + } from '$lib/components/colors/color-picker/base'; 10 + import { okhsv_to_rgb, oklab_to_rgb, type OKlch } from '../base/color'; 11 + 12 + let { 13 + rgb = $bindable(), 14 + oklab = $bindable(), 15 + okhsv = $bindable(), 16 + sideOffset = 10, 17 + side = 'bottom', 18 + class: className, 19 + onchange 20 + }: { 21 + rgb?: RGB; 22 + oklab?: OKlab; 23 + okhsv?: OKhsv; 24 + class?: string; 25 + sideOffset?: number; 26 + side?: 'top' | 'right' | 'bottom' | 'left'; 27 + onchange?: (color: { hex: string; rgb: RGB; oklab: OKlab; okhsv: OKhsv; oklch: OKlch }) => void; 28 + } = $props(); 29 + 30 + let internalColor = $derived(convertToInternal(rgb, oklab, okhsv)); 31 + 32 + function convertToInternal( 33 + rgb: RGB | undefined, 34 + oklab: OKlab | undefined, 35 + okhsv: OKhsv | undefined 36 + ): RGB { 37 + if (okhsv) { 38 + return okhsv_to_rgb(okhsv); 39 + } 40 + 41 + if (oklab) { 42 + return oklab_to_rgb(oklab); 43 + } 44 + 45 + if (rgb) { 46 + return rgb; 47 + } 48 + 49 + throw 'rgb, oklab, or okhsv required'; 50 + } 51 + </script> 52 + 53 + <Popover.Root> 54 + <Popover.Trigger 55 + class={cn( 56 + 'focus-visible:outline-base-900 dark:focus-visible:outline-base-100 cursor-pointer rounded-full focus-visible:outline-2 focus-visible:outline-offset-2' 57 + )} 58 + > 59 + <div 60 + class="border-base-300 dark:border-base-700 focus-visible:outline-accent-500 z-10 size-8 rounded-full border" 61 + style={`background-color: rgb(${internalColor.r * 255}, ${internalColor.g * 255}, ${internalColor.b * 255});`} 62 + ></div> 63 + </Popover.Trigger> 64 + <Popover.Content side={side} sideOffset={sideOffset}> 65 + <ColorPicker bind:rgb bind:oklab bind:okhsv class={className} onchange={onchange} /> 66 + </Popover.Content> 67 + </Popover.Root>
-252
src/lib/components/extra/color-picker/base/ColorPicker.svelte
··· 1 - <script lang="ts"> 2 - import { eps, picker_size, slider_width, border_size, gap_size } from './constants'; 3 - import { render_main_image, render_slider_image } from './render'; 4 - import type { RGB, OKlab, OKhsv, OKlch } from './color'; 5 - import { 6 - okhsv_to_oklab, 7 - okhsv_to_rgb, 8 - okhsv_to_oklch, 9 - rgb_to_hex, 10 - oklab_to_okhsv, 11 - rgb_to_okhsv 12 - } from './color'; 13 - import { cn } from '$lib'; 14 - 15 - let { 16 - rgb = $bindable(), 17 - oklab = $bindable(), 18 - okhsv = $bindable(), 19 - class: className, 20 - onchange 21 - }: { 22 - rgb?: RGB; 23 - oklab?: OKlab; 24 - okhsv?: OKhsv; 25 - class?: string; 26 - onchange?: (color: { hex: string; rgb: RGB; oklab: OKlab; okhsv: OKhsv; oklch: OKlch }) => void; 27 - } = $props(); 28 - 29 - const width = picker_size + slider_width + gap_size + border_size * 2; 30 - const height = picker_size + border_size * 2; 31 - 32 - let color = $derived(convertToInternal(rgb, oklab, okhsv)); 33 - let uihsv = $derived(scale_to_ui(color)); 34 - 35 - function scale_to_ui(okhsv: OKhsv): OKhsv { 36 - return { 37 - h: clamp_ui(okhsv.h / 360), 38 - s: clamp_ui(okhsv.s), 39 - v: clamp_ui(1 - okhsv.v) 40 - }; 41 - } 42 - 43 - function clamp(x: number) { 44 - return x < eps ? eps : x > 1 - eps ? 1 - eps : x; 45 - } 46 - 47 - function clamp_ui(value: number) { 48 - return value < 0 ? 0 : value > 1 ? picker_size : value * picker_size; 49 - } 50 - 51 - function convertToInternal( 52 - rgb: RGB | undefined, 53 - oklab: OKlab | undefined, 54 - okhsv: OKhsv | undefined 55 - ) { 56 - if (okhsv) { 57 - return okhsv; 58 - } 59 - 60 - if (oklab) { 61 - return oklab_to_okhsv(oklab); 62 - } 63 - 64 - if (rgb) { 65 - return rgb_to_okhsv(rgb); 66 - } 67 - 68 - throw 'rgb, oklab, or okhsv required'; 69 - } 70 - 71 - async function update_input() { 72 - let new_rgb = okhsv_to_rgb(color); 73 - let new_oklab = okhsv_to_oklab(color); 74 - let new_oklch = okhsv_to_oklch(color); 75 - let new_hex = rgb_to_hex(new_rgb); 76 - let new_okhsv = color; 77 - 78 - if (okhsv) { 79 - okhsv = color; 80 - } 81 - 82 - if (oklab) { 83 - oklab = new_oklab; 84 - } 85 - 86 - if (rgb) { 87 - rgb = new_rgb; 88 - } 89 - 90 - onchange?.({ 91 - rgb: new_rgb, 92 - oklab: new_oklab, 93 - okhsv: new_okhsv, 94 - oklch: new_oklch, 95 - hex: new_hex 96 - }); 97 - } 98 - 99 - function update_sv(x: number, y: number) { 100 - let new_s = clamp(x / picker_size); 101 - let new_v = clamp(1 - y / picker_size); 102 - 103 - color.s = new_s; 104 - color.v = new_v; 105 - 106 - update_input(); 107 - } 108 - 109 - function update_h(x: number, y: number) { 110 - let h = clamp(y / picker_size); 111 - color.h = h * 360; 112 - color.s = Math.max(color.s, 0.00001); 113 - color.v = Math.max(color.v, 0.00001); 114 - 115 - update_input(); 116 - } 117 - 118 - function pointer(node: HTMLCanvasElement, fn: (x: number, y: number) => void) { 119 - let active = false; 120 - 121 - function update(event: PointerEvent) { 122 - const x = event.offsetX; 123 - const y = event.offsetY; 124 - fn(x, y); 125 - } 126 - 127 - function onpointerdown(event: PointerEvent) { 128 - event.stopPropagation(); 129 - node.setPointerCapture(event.pointerId); 130 - update(event); 131 - active = true; 132 - } 133 - 134 - function onpointermove(event: PointerEvent) { 135 - event.stopPropagation(); 136 - if (active) { 137 - update(event); 138 - } 139 - } 140 - 141 - function onpointerend(event: PointerEvent) { 142 - event.stopPropagation(); 143 - node.releasePointerCapture(event.pointerId); 144 - active = false; 145 - } 146 - 147 - node.addEventListener('pointerdown', onpointerdown, { passive: true }); 148 - node.addEventListener('pointermove', onpointermove, { passive: true }); 149 - node.addEventListener('pointerup', onpointerend, { passive: true }); 150 - node.addEventListener('pointercancel', onpointerend, { passive: true }); 151 - 152 - return { 153 - destroy() { 154 - node.removeEventListener('pointerdown', onpointerdown); 155 - node.removeEventListener('pointermove', onpointermove); 156 - node.removeEventListener('pointerup', onpointerend); 157 - node.removeEventListener('pointercancel', onpointerend); 158 - } 159 - }; 160 - } 161 - 162 - function onKeydown(event: KeyboardEvent) { 163 - const keyHandled = () => { 164 - event.preventDefault(); 165 - event.stopPropagation(); 166 - }; 167 - 168 - const step = event.shiftKey ? 10 : 1; 169 - 170 - switch (event.key) { 171 - case 'ArrowUp': 172 - if (event.altKey) { 173 - update_h(0, Math.round(uihsv.h! - step)); 174 - } else { 175 - update_sv(uihsv.s, Math.round(uihsv.v - step)); 176 - } 177 - keyHandled(); 178 - break; 179 - 180 - case 'ArrowDown': 181 - if (event.altKey) { 182 - update_h(0, Math.round(uihsv.h! + step)); 183 - } else { 184 - update_sv(uihsv.s, Math.round(uihsv.v + step)); 185 - } 186 - keyHandled(); 187 - break; 188 - 189 - case 'ArrowLeft': 190 - update_sv(Math.round(uihsv.s - step), uihsv.v); 191 - keyHandled(); 192 - break; 193 - 194 - case 'ArrowRight': 195 - update_sv(Math.round(uihsv.s + step), uihsv.v); 196 - keyHandled(); 197 - break; 198 - } 199 - } 200 - </script> 201 - 202 - <!-- svelte-ignore a11y_no_noninteractive_tabindex --> 203 - <!-- svelte-ignore a11y_no_static_element_interactions --> 204 - <div 205 - class={cn( 206 - 'focus-visible:outline-base-900 dark:focus-visible:outline-base-100 relative rounded-xl border-none focus-visible:outline-2 focus-visible:outline-offset-2', 207 - className 208 - )} 209 - tabindex="0" 210 - style:width="{width}px" 211 - style:height="{height}px" 212 - onkeydown={onKeydown} 213 - > 214 - <canvas 215 - id="okhsv_sv_canvas" 216 - width={picker_size} 217 - height={picker_size} 218 - style:top="{border_size}px" 219 - style:left="{border_size}px" 220 - class="absolute touch-none rounded-xl" 221 - use:pointer={update_sv} 222 - use:render_main_image={color.h} 223 - ></canvas> 224 - <canvas 225 - width={slider_width} 226 - height={picker_size} 227 - style:top="{border_size}px" 228 - style:left="{picker_size + gap_size}px" 229 - class="absolute touch-none rounded-xl" 230 - use:pointer={update_h} 231 - use:render_slider_image 232 - ></canvas> 233 - 234 - <svg {width} {height} class="pointer-events-none absolute touch-none"> 235 - <g transform="translate({border_size},{border_size})"> 236 - <g transform="translate({uihsv.s},{uihsv.v})"> 237 - <circle cx="0" cy="0" r="5" fill="none" stroke-width="1.75" class="stroke-base-50" /> 238 - <circle cx="0" cy="0" r="6" fill="none" stroke-width="1.25" class="stroke-base-950" /> 239 - </g> 240 - </g> 241 - <g transform="translate({picker_size + gap_size},{border_size})"> 242 - <g transform="translate(0,{uihsv.h})"> 243 - <polygon points="-7,-4 -1,0 -7,4" stroke-width="0.8" class="stroke-base-950 fill-base-50" /> 244 - <polygon 245 - points="{slider_width + 7},-4 {slider_width + 1},0 {slider_width + 7},4" 246 - stroke-width="0.8" 247 - class="stroke-base-950 fill-base-50" 248 - /> 249 - </g> 250 - </g> 251 - </svg> 252 - </div>
-138
src/lib/components/extra/color-picker/base/color.ts
··· 1 - import { convert, OKHSL, OKHSV, OKLab, OKLCH, sRGB } from '@texel/color'; 2 - 3 - export type Color = string | RGB | OKlab | OKhsv | OKlch; 4 - 5 - export type SetColorFunction = (color: Color) => void; 6 - 7 - export interface RGB { 8 - r: number; 9 - g: number; 10 - b: number; 11 - } 12 - 13 - export interface OKlab { 14 - l: number; 15 - a: number; 16 - b: number; 17 - } 18 - 19 - export interface OKhsv { 20 - h: number; 21 - s: number; 22 - v: number; 23 - } 24 - 25 - export interface OKhsl { 26 - h: number; 27 - s: number; 28 - l: number; 29 - } 30 - 31 - export interface OKlch { 32 - l: number; 33 - c: number; 34 - h: number; 35 - } 36 - 37 - export function okhsv_to_oklch(okhsv: OKhsv): OKlch { 38 - const [l, c, h] = convert([okhsv.h, okhsv.s, okhsv.v], OKHSV, OKLCH); 39 - return { l: l, c: c, h: h }; 40 - } 41 - 42 - export function oklch_to_okhsv(oklch: OKlch): OKhsv { 43 - const [h, s, v] = convert([oklch.h, oklch.c, oklch.l], OKLCH, OKHSV); 44 - return { h: h ?? 0, s, v }; 45 - } 46 - 47 - export function oklab_to_okhsv(oklab: OKlab): OKhsv { 48 - const [h, s, v] = convert([oklab.l, oklab.a, oklab.b], OKLab, OKHSV); 49 - return { h: h ?? 0, s, v }; 50 - } 51 - 52 - export function okhsv_to_rgb(okhsv: OKhsv): RGB { 53 - const [r, g, b] = convert([okhsv.h, okhsv.s, okhsv.v], OKHSV, sRGB); 54 - return { r, g, b }; 55 - } 56 - 57 - export function okhsv_to_oklab(okhsv: OKhsv): OKlab { 58 - const [l, a, b] = convert([okhsv.h, okhsv.s, okhsv.v], OKHSV, OKLab); 59 - return { l, a, b }; 60 - } 61 - 62 - export function rgb_to_okhsv(rgb: RGB): OKhsv { 63 - const [h, s, v] = convert([rgb.r, rgb.g, rgb.b], sRGB, OKHSV); 64 - return { h, s, v }; 65 - } 66 - 67 - export function rgb_to_oklab(rgb: RGB): OKlab { 68 - const [l, a, b] = convert([rgb.r, rgb.g, rgb.b], sRGB, OKLab); 69 - return { l, a, b }; 70 - } 71 - 72 - export function okhsl_to_rgb(okhsl: OKhsl): RGB { 73 - const [r, g, b] = convert([okhsl.h, okhsl.s, okhsl.l], OKHSL, sRGB); 74 - return { r, g, b }; 75 - } 76 - 77 - export function oklab_to_oklch(oklab: OKlab): OKlch { 78 - const [l, c, h] = convert([oklab.l, oklab.a, oklab.b], OKLab, OKLCH); 79 - return { l, c, h }; 80 - } 81 - 82 - export function rgb_to_oklch(rgb: RGB): OKlch { 83 - const [l, c, h] = convert([rgb.r, rgb.g, rgb.b], sRGB, OKLCH); 84 - return { l, c, h }; 85 - } 86 - 87 - export function oklab_to_rgb(oklab: OKlab): RGB { 88 - const [r, g, b] = convert([oklab.l, oklab.a, oklab.b], OKLab, sRGB); 89 - return { r, g, b }; 90 - } 91 - 92 - export function oklch_to_rgb(oklch: OKlch): RGB { 93 - const [r, g, b] = convert([oklch.l, oklch.c, oklch.h], OKLCH, sRGB); 94 - return { r, g, b }; 95 - } 96 - 97 - export function oklch_string_to_oklch(oklch: string): OKlch { 98 - const converted = oklch.split('oklch(')[1].split(')')[0].split(' '); 99 - return { l: parseFloat(converted[0]), c: parseFloat(converted[1]), h: parseFloat(converted[2]) }; 100 - } 101 - 102 - export function rgb_to_hex(rgb: RGB): string { 103 - const r = Math.round(rgb.r * 255); 104 - const g = Math.round(rgb.g * 255); 105 - const b = Math.round(rgb.b * 255); 106 - return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`; 107 - } 108 - 109 - export function hex_to_rgb(hex: string): RGB { 110 - if (hex.startsWith('#')) { 111 - hex = hex.slice(1); 112 - } else if (hex.startsWith('0x')) { 113 - hex = hex.slice(2); 114 - } 115 - 116 - if (hex.length !== 6) { 117 - console.warn('Invalid hex color', hex); 118 - return { r: 0, g: 0, b: 0 }; 119 - } 120 - 121 - const r = parseInt(hex.slice(0, 2), 16); 122 - const g = parseInt(hex.slice(2, 4), 16); 123 - const b = parseInt(hex.slice(4, 6), 16); 124 - const r_ = r / 255; 125 - const g_ = g / 255; 126 - const b_ = b / 255; 127 - return { r: r_, g: g_, b: b_ }; 128 - } 129 - 130 - export function hex_to_okhsv(hex: string): OKhsv { 131 - const rgb = hex_to_rgb(hex); 132 - return rgb_to_okhsv(rgb); 133 - } 134 - 135 - export function okhsv_to_hex(okhsv: OKhsv): string { 136 - const rgb = okhsv_to_rgb(okhsv); 137 - return rgb_to_hex(rgb); 138 - }
-5
src/lib/components/extra/color-picker/base/constants.ts
··· 1 - export const picker_size = 200; 2 - export const slider_width = 16; 3 - export const border_size = 10; 4 - export const gap_size = 30; 5 - export const eps = 0.0001;
-5
src/lib/components/extra/color-picker/base/index.ts
··· 1 - // adapted from https://github.com/CaptainCodeman/svelte-color-select 2 - 3 - export { default } from './ColorPicker.svelte'; 4 - export { default as ColorPicker } from './ColorPicker.svelte'; 5 - export type { RGB, OKlab, OKhsv } from './color';
-100
src/lib/components/extra/color-picker/base/render.ts
··· 1 - import { f_shader, v_shader } from './shaders'; 2 - import { picker_size, slider_width } from './constants'; 3 - import { okhsl_to_rgb } from './color'; 4 - 5 - export function render_slider_image(canvas: HTMLCanvasElement) { 6 - const ctx = canvas.getContext('2d')!; 7 - const data = new Uint8ClampedArray(picker_size * slider_width * 4); 8 - 9 - for (let i = 0; i < picker_size; i++) { 10 - const a_ = Math.cos((2 * Math.PI * i) / picker_size); 11 - const b_ = Math.sin((2 * Math.PI * i) / picker_size); 12 - 13 - const rgb = okhsl_to_rgb({ 14 - h: (i / picker_size) * 360, 15 - s: 0.9, 16 - l: 0.65 + 0.17 * b_ - 0.08 * a_ 17 - }); 18 - 19 - for (let j = 0; j < slider_width; j++) { 20 - const index = 4 * (i * slider_width + j); 21 - data[index + 0] = rgb.r * 255; 22 - data[index + 1] = rgb.g * 255; 23 - data[index + 2] = rgb.b * 255; 24 - data[index + 3] = 255; 25 - } 26 - } 27 - 28 - const imageData = new ImageData(data, slider_width); 29 - ctx.putImageData(imageData, 0, 0); 30 - } 31 - 32 - export function render_main_image(canvas: HTMLCanvasElement, hue: number) { 33 - const gl = canvas.getContext('webgl')!; 34 - 35 - gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); 36 - 37 - const shaderProgram = initShaderProgram(gl, v_shader, f_shader)!; 38 - 39 - const position = gl.getAttribLocation(shaderProgram, 'position'); 40 - const resolution = gl.getUniformLocation(shaderProgram, 'resolution'); 41 - const hue_rad = gl.getUniformLocation(shaderProgram, 'hue_rad'); 42 - 43 - gl.useProgram(shaderProgram); 44 - gl.uniform2fv(resolution, [gl.canvas.width, gl.canvas.height]); 45 - 46 - const positions = [1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, -1.0]; 47 - const positionBuffer = gl.createBuffer(); 48 - 49 - gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); 50 - gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW); 51 - gl.enableVertexAttribArray(position); 52 - gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0); 53 - 54 - function update(hue: number) { 55 - requestAnimationFrame(() => { 56 - gl.uniform1f(hue_rad, hue / 360); 57 - gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 58 - }); 59 - } 60 - 61 - update(hue); 62 - 63 - return { 64 - update, 65 - destroy() { 66 - gl.deleteProgram(shaderProgram); 67 - } 68 - }; 69 - } 70 - 71 - function initShaderProgram(gl: WebGLRenderingContext, vsSource: string, fsSource: string) { 72 - const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource)!; 73 - const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource)!; 74 - 75 - const shaderProgram = gl.createProgram()!; 76 - gl.attachShader(shaderProgram, vertexShader); 77 - gl.attachShader(shaderProgram, fragmentShader); 78 - gl.linkProgram(shaderProgram); 79 - 80 - if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { 81 - throw `unable to init shader: ${gl.getProgramInfoLog(shaderProgram)}`; 82 - } 83 - 84 - return shaderProgram; 85 - } 86 - 87 - function loadShader(gl: WebGLRenderingContext, type: number, source: string) { 88 - const shader = gl.createShader(type)!; 89 - 90 - gl.shaderSource(shader, source); 91 - gl.compileShader(shader); 92 - 93 - if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { 94 - const info = gl.getShaderInfoLog(shader); 95 - gl.deleteShader(shader); 96 - throw `error compiling shaders: ${info}`; 97 - } 98 - 99 - return shader; 100 - }
-7
src/lib/components/extra/color-picker/base/shaders.ts
··· 1 - /// <reference types="vite-plugin-glsl/ext" /> 2 - 3 - import f_shader_src from './shaders/f_shader.glsl'; 4 - import v_shader_src from './shaders/v_shader.glsl'; 5 - 6 - export const f_shader = f_shader_src; 7 - export const v_shader = v_shader_src;
-67
src/lib/components/extra/color-picker/popover/PopoverColorPicker.svelte
··· 1 - <script lang="ts"> 2 - import { cn } from '$lib'; 3 - import * as Popover from '$lib/components/base/popover'; 4 - import { 5 - ColorPicker, 6 - type OKhsv, 7 - type OKlab, 8 - type RGB 9 - } from '$lib/components/extra/color-picker/base'; 10 - import { okhsv_to_rgb, oklab_to_rgb, type OKlch } from '../base/color'; 11 - 12 - let { 13 - rgb = $bindable(), 14 - oklab = $bindable(), 15 - okhsv = $bindable(), 16 - sideOffset = 10, 17 - side = 'bottom', 18 - class: className, 19 - onchange 20 - }: { 21 - rgb?: RGB; 22 - oklab?: OKlab; 23 - okhsv?: OKhsv; 24 - class?: string; 25 - sideOffset?: number; 26 - side?: 'top' | 'right' | 'bottom' | 'left'; 27 - onchange?: (color: { hex: string; rgb: RGB; oklab: OKlab; okhsv: OKhsv; oklch: OKlch }) => void; 28 - } = $props(); 29 - 30 - let internalColor = $derived(convertToInternal(rgb, oklab, okhsv)); 31 - 32 - function convertToInternal( 33 - rgb: RGB | undefined, 34 - oklab: OKlab | undefined, 35 - okhsv: OKhsv | undefined 36 - ): RGB { 37 - if (okhsv) { 38 - return okhsv_to_rgb(okhsv); 39 - } 40 - 41 - if (oklab) { 42 - return oklab_to_rgb(oklab); 43 - } 44 - 45 - if (rgb) { 46 - return rgb; 47 - } 48 - 49 - throw 'rgb, oklab, or okhsv required'; 50 - } 51 - </script> 52 - 53 - <Popover.Root> 54 - <Popover.Trigger 55 - class={cn( 56 - 'focus-visible:outline-base-900 dark:focus-visible:outline-base-100 cursor-pointer rounded-full focus-visible:outline-2 focus-visible:outline-offset-2' 57 - )} 58 - > 59 - <div 60 - class="border-base-300 dark:border-base-700 focus-visible:outline-accent-500 z-10 size-8 rounded-full border" 61 - style={`background-color: rgb(${internalColor.r * 255}, ${internalColor.g * 255}, ${internalColor.b * 255});`} 62 - ></div> 63 - </Popover.Trigger> 64 - <Popover.Content side={side} sideOffset={sideOffset}> 65 - <ColorPicker bind:rgb bind:oklab bind:okhsv class={className} onchange={onchange} /> 66 - </Popover.Content> 67 - </Popover.Root>
+3 -3
src/routes/(main)/components/colors/color-gradient-picker/Example.svelte
··· 1 1 <script lang="ts"> 2 - import ColorGradientPicker from '$lib/components/extra/color-gradient-picker/ColorGradientPicker.svelte'; 3 - import type { OKlch } from '$lib/components/extra/color-picker/base/color'; 2 + import { ColorGradientPicker } from '$lib'; 3 + import type { OKlch } from '$lib/components/colors/color-picker/base/color'; 4 4 import { 5 5 oklch_string_to_oklch, 6 6 oklch_to_rgb 7 - } from '$lib/components/extra/color-picker/base/color'; 7 + } from '$lib/components/colors/color-picker/base/color'; 8 8 import { ThemeWatcher } from '$lib/helper/ThemeWatcher.svelte'; 9 9 import { onMount } from 'svelte'; 10 10
+3 -17
src/routes/(main)/components/colors/color-picker/Example.svelte
··· 1 1 <script lang="ts"> 2 - import { ColorPicker } from '$lib/components/extra/color-picker/base'; 3 - import * as Popover from '$lib/components/base/popover'; 4 - import { cn } from '$lib/utils'; 5 - import Subheading from '$lib/components/base/heading/Subheading.svelte'; 6 - import Button from '$lib/components/base/button/Button.svelte'; 7 - import PopoverColorPicker from '$lib/components/extra/color-picker/popover/PopoverColorPicker.svelte'; 2 + import { ColorPicker, PopoverColorPicker } from '$lib'; 8 3 import { ThemeWatcher } from '$lib/helper/ThemeWatcher.svelte'; 9 - import { hex_to_rgb, oklch_string_to_oklch, oklch_to_rgb } from '$lib/components/extra/color-picker/base/color'; 4 + import { oklch_string_to_oklch, oklch_to_rgb } from '$lib/components/colors/color-picker/base/color'; 10 5 import { onMount } from 'svelte'; 11 6 12 7 let rgb = $state({ ··· 33 28 34 29 <h3>Popover Color Picker</h3> 35 30 36 - <PopoverColorPicker bind:rgb /> 37 - <!-- 38 - <Button 39 - onclick={() => { 40 - // themewatcher 41 - const theme = new ThemeWatcher(); 42 - const color = theme.getCSSVar('--color-accent-500'); 43 - rgb = oklch_to_rgb(oklch_string_to_oklch(color)); 44 - }}>Reset Color</Button 45 - > --> 31 + <PopoverColorPicker bind:rgb />
+1 -1
src/routes/(main)/components/colors/color-select/Example.svelte
··· 1 1 <script> 2 - import ColorSelect from '$lib/components/extra/color-select/ColorSelect.svelte'; 2 + import { ColorSelect } from '$lib'; 3 3 </script> 4 4 5 5 <ColorSelect
+16
src/lib/components/colors/color-picker/base/shaders/f_shader.glsl
··· 1 + precision mediump float; 2 + uniform vec2 resolution; 3 + uniform float hue_rad; 4 + 5 + #include library; 6 + 7 + void main() { 8 + vec2 uv = gl_FragCoord.xy / resolution; 9 + 10 + float l = uv.y; 11 + float h = hue_rad; 12 + 13 + vec3 hsl = vec3(h, uv.x, uv.y); 14 + vec3 hsvRGB = okhsv_to_srgb(hsl); 15 + gl_FragColor = vec4(hsvRGB, 1.0); 16 + }
+183
src/lib/components/colors/color-picker/base/shaders/library.glsl
··· 1 + // Copyright(c) 2021 Björn Ottosson 2 + // 3 + // Permission is hereby granted, free of charge, to any person obtaining a copy of 4 + // this softwareand associated documentation files(the "Software"), to deal in 5 + // the Software without restriction, including without limitation the rights to 6 + // use, copy, modify, merge, publish, distribute, sublicense, and /or sell copies 7 + // of the Software, and to permit persons to whom the Software is furnished to do 8 + // so, subject to the following conditions : 9 + // The above copyright noticeand this permission notice shall be included in all 10 + // copies or substantial portions of the Software. 11 + // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 + // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 + // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 14 + // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 + // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 + // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 17 + // SOFTWARE. 18 + 19 + precision mediump float; 20 + #define M_PI 3.1415926536 21 + 22 + float cbrt(float x) { 23 + return sign(x)*pow(abs(x), 1.0 / 3.0); 24 + } 25 + 26 + float srgb_transfer_function(float a) { 27 + return .0031308 >= a ? 12.92 * a : 1.055 * pow(a, .4166666666666667) - .055; 28 + } 29 + 30 + vec3 oklab_to_linear_srgb(vec3 c) { 31 + float l_ = c.x + 0.3963377774 * c.y + 0.2158037573 * c.z; 32 + float m_ = c.x - 0.1055613458 * c.y - 0.0638541728 * c.z; 33 + float s_ = c.x - 0.0894841775 * c.y - 1.2914855480 * c.z; 34 + 35 + float l = l_ * l_ * l_; 36 + float m = m_ * m_ * m_; 37 + float s = s_ * s_ * s_; 38 + 39 + return vec3( 40 + +4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s, 41 + -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s, 42 + -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s 43 + ); 44 + } 45 + 46 + // Finds the maximum saturation possible for a given hue that fits in sRGB 47 + // Saturation here is defined as S = C/L 48 + // a and b must be normalized so a^2 + b^2 == 1 49 + float compute_max_saturation(float a, float b) { 50 + // Max saturation will be when one of r, g or b goes below zero. 51 + 52 + // Select different coefficients depending on which component goes below zero first 53 + float k0, k1, k2, k3, k4, wl, wm, ws; 54 + 55 + if (-1.88170328 * a - 0.80936493 * b > 1.0) 56 + { 57 + // Red component 58 + k0 = +1.19086277; k1 = +1.76576728; k2 = +0.59662641; k3 = +0.75515197; k4 = +0.56771245; 59 + wl = +4.0767416621; wm = -3.3077115913; ws = +0.2309699292; 60 + } 61 + else if (1.81444104 * a - 1.19445276 * b > 1.0) 62 + { 63 + // Green component 64 + k0 = +0.73956515; k1 = -0.45954404; k2 = +0.08285427; k3 = +0.12541070; k4 = +0.14503204; 65 + wl = -1.2684380046; wm = +2.6097574011; ws = -0.3413193965; 66 + } 67 + else 68 + { 69 + // Blue component 70 + k0 = +1.35733652; k1 = -0.00915799; k2 = -1.15130210; k3 = -0.50559606; k4 = +0.00692167; 71 + wl = -0.0041960863; wm = -0.7034186147; ws = +1.7076147010; 72 + } 73 + 74 + // Approximate max saturation using a polynomial: 75 + float S = k0 + k1 * a + k2 * b + k3 * a * a + k4 * a * b; 76 + 77 + // Do one step Halley's method to get closer 78 + // this gives an error less than 10e6, except for some blue hues where the dS/dh is close to infinite 79 + // this should be sufficient for most applications, otherwise do two/three steps 80 + 81 + float k_l = +0.3963377774 * a + 0.2158037573 * b; 82 + float k_m = -0.1055613458 * a - 0.0638541728 * b; 83 + float k_s = -0.0894841775 * a - 1.2914855480 * b; 84 + 85 + { 86 + float l_ = 1.0 + S * k_l; 87 + float m_ = 1.0 + S * k_m; 88 + float s_ = 1.0 + S * k_s; 89 + 90 + float l = l_ * l_ * l_; 91 + float m = m_ * m_ * m_; 92 + float s = s_ * s_ * s_; 93 + 94 + float l_dS = 3.0 * k_l * l_ * l_; 95 + float m_dS = 3.0 * k_m * m_ * m_; 96 + float s_dS = 3.0 * k_s * s_ * s_; 97 + 98 + float l_dS2 = 6.0 * k_l * k_l * l_; 99 + float m_dS2 = 6.0 * k_m * k_m * m_; 100 + float s_dS2 = 6.0 * k_s * k_s * s_; 101 + 102 + float f = wl * l + wm * m + ws * s; 103 + float f1 = wl * l_dS + wm * m_dS + ws * s_dS; 104 + float f2 = wl * l_dS2 + wm * m_dS2 + ws * s_dS2; 105 + 106 + S = S - f * f1 / (f1 * f1 - 0.5 * f * f2); 107 + } 108 + 109 + return S; 110 + } 111 + 112 + // finds L_cusp and C_cusp for a given hue 113 + // a and b must be normalized so a^2 + b^2 == 1 114 + vec2 find_cusp(float a, float b) { 115 + // First, find the maximum saturation (saturation S = C/L) 116 + float S_cusp = compute_max_saturation(a, b); 117 + 118 + // Convert to linear sRGB to find the first point where at least one of r,g or b >= 1: 119 + vec3 rgb_at_max = oklab_to_linear_srgb(vec3( 1, S_cusp * a, S_cusp * b )); 120 + float L_cusp = cbrt(1.0 / max(max(rgb_at_max.r, rgb_at_max.g), rgb_at_max.b)); 121 + float C_cusp = L_cusp * S_cusp; 122 + 123 + return vec2( L_cusp , C_cusp ); 124 + } 125 + 126 + float toe_inv(float x) { 127 + float k_1 = 0.206; 128 + float k_2 = 0.03; 129 + float k_3 = (1.0 + k_1) / (1.0 + k_2); 130 + return (x * x + k_1 * x) / (k_3 * (x + k_2)); 131 + } 132 + 133 + vec2 to_ST(vec2 cusp) { 134 + float L = cusp.x; 135 + float C = cusp.y; 136 + return vec2( C / L, C / (1.0 - L) ); 137 + } 138 + 139 + vec3 okhsv_to_srgb(vec3 hsv) { 140 + float h = hsv.x; 141 + float s = hsv.y; 142 + float v = hsv.z; 143 + 144 + float a_ = cos(2.0 * M_PI * h); 145 + float b_ = sin(2.0 * M_PI * h); 146 + 147 + vec2 cusp = find_cusp(a_, b_); 148 + vec2 ST_max = to_ST(cusp); 149 + float S_max = ST_max.x; 150 + float T_max = ST_max.y; 151 + float S_0 = 0.5; 152 + float k = 1.0- S_0 / S_max; 153 + 154 + // first we compute L and V as if the gamut is a perfect triangle: 155 + 156 + // L, C when v==1: 157 + float L_v = 1.0 - s * S_0 / (S_0 + T_max - T_max * k * s); 158 + float C_v = s * T_max * S_0 / (S_0 + T_max - T_max * k * s); 159 + 160 + float L = v * L_v; 161 + float C = v * C_v; 162 + 163 + // then we compensate for both toe and the curved top part of the triangle: 164 + float L_vt = toe_inv(L_v); 165 + float C_vt = C_v * L_vt / L_v; 166 + 167 + float L_new = toe_inv(L); 168 + C = C * L_new / L; 169 + L = L_new; 170 + 171 + vec3 rgb_scale = oklab_to_linear_srgb(vec3( L_vt, a_ * C_vt, b_ * C_vt )); 172 + float scale_L = cbrt(1.0 / max(max(rgb_scale.r, rgb_scale.g), max(rgb_scale.b, 0.0))); 173 + 174 + L = L * scale_L; 175 + C = C * scale_L; 176 + 177 + vec3 rgb = oklab_to_linear_srgb(vec3( L, C * a_, C * b_ )); 178 + return vec3( 179 + srgb_transfer_function(rgb.r), 180 + srgb_transfer_function(rgb.g), 181 + srgb_transfer_function(rgb.b) 182 + ); 183 + }
+2
src/lib/components/colors/color-picker/base/shaders/readme.md
··· 1 + shaders adapted from https://github.com/dokozero/okcolor 2 + the source in this folder is minified and included in a ts file to enable svelte-package to be used
+5
src/lib/components/colors/color-picker/base/shaders/v_shader.glsl
··· 1 + attribute vec4 position; 2 + 3 + void main() { 4 + gl_Position = position; 5 + }
-16
src/lib/components/extra/color-picker/base/shaders/f_shader.glsl
··· 1 - precision mediump float; 2 - uniform vec2 resolution; 3 - uniform float hue_rad; 4 - 5 - #include library; 6 - 7 - void main() { 8 - vec2 uv = gl_FragCoord.xy / resolution; 9 - 10 - float l = uv.y; 11 - float h = hue_rad; 12 - 13 - vec3 hsl = vec3(h, uv.x, uv.y); 14 - vec3 hsvRGB = okhsv_to_srgb(hsl); 15 - gl_FragColor = vec4(hsvRGB, 1.0); 16 - }
-183
src/lib/components/extra/color-picker/base/shaders/library.glsl
··· 1 - // Copyright(c) 2021 Björn Ottosson 2 - // 3 - // Permission is hereby granted, free of charge, to any person obtaining a copy of 4 - // this softwareand associated documentation files(the "Software"), to deal in 5 - // the Software without restriction, including without limitation the rights to 6 - // use, copy, modify, merge, publish, distribute, sublicense, and /or sell copies 7 - // of the Software, and to permit persons to whom the Software is furnished to do 8 - // so, subject to the following conditions : 9 - // The above copyright noticeand this permission notice shall be included in all 10 - // copies or substantial portions of the Software. 11 - // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 - // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 - // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 14 - // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 - // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 - // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 17 - // SOFTWARE. 18 - 19 - precision mediump float; 20 - #define M_PI 3.1415926536 21 - 22 - float cbrt(float x) { 23 - return sign(x)*pow(abs(x), 1.0 / 3.0); 24 - } 25 - 26 - float srgb_transfer_function(float a) { 27 - return .0031308 >= a ? 12.92 * a : 1.055 * pow(a, .4166666666666667) - .055; 28 - } 29 - 30 - vec3 oklab_to_linear_srgb(vec3 c) { 31 - float l_ = c.x + 0.3963377774 * c.y + 0.2158037573 * c.z; 32 - float m_ = c.x - 0.1055613458 * c.y - 0.0638541728 * c.z; 33 - float s_ = c.x - 0.0894841775 * c.y - 1.2914855480 * c.z; 34 - 35 - float l = l_ * l_ * l_; 36 - float m = m_ * m_ * m_; 37 - float s = s_ * s_ * s_; 38 - 39 - return vec3( 40 - +4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s, 41 - -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s, 42 - -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s 43 - ); 44 - } 45 - 46 - // Finds the maximum saturation possible for a given hue that fits in sRGB 47 - // Saturation here is defined as S = C/L 48 - // a and b must be normalized so a^2 + b^2 == 1 49 - float compute_max_saturation(float a, float b) { 50 - // Max saturation will be when one of r, g or b goes below zero. 51 - 52 - // Select different coefficients depending on which component goes below zero first 53 - float k0, k1, k2, k3, k4, wl, wm, ws; 54 - 55 - if (-1.88170328 * a - 0.80936493 * b > 1.0) 56 - { 57 - // Red component 58 - k0 = +1.19086277; k1 = +1.76576728; k2 = +0.59662641; k3 = +0.75515197; k4 = +0.56771245; 59 - wl = +4.0767416621; wm = -3.3077115913; ws = +0.2309699292; 60 - } 61 - else if (1.81444104 * a - 1.19445276 * b > 1.0) 62 - { 63 - // Green component 64 - k0 = +0.73956515; k1 = -0.45954404; k2 = +0.08285427; k3 = +0.12541070; k4 = +0.14503204; 65 - wl = -1.2684380046; wm = +2.6097574011; ws = -0.3413193965; 66 - } 67 - else 68 - { 69 - // Blue component 70 - k0 = +1.35733652; k1 = -0.00915799; k2 = -1.15130210; k3 = -0.50559606; k4 = +0.00692167; 71 - wl = -0.0041960863; wm = -0.7034186147; ws = +1.7076147010; 72 - } 73 - 74 - // Approximate max saturation using a polynomial: 75 - float S = k0 + k1 * a + k2 * b + k3 * a * a + k4 * a * b; 76 - 77 - // Do one step Halley's method to get closer 78 - // this gives an error less than 10e6, except for some blue hues where the dS/dh is close to infinite 79 - // this should be sufficient for most applications, otherwise do two/three steps 80 - 81 - float k_l = +0.3963377774 * a + 0.2158037573 * b; 82 - float k_m = -0.1055613458 * a - 0.0638541728 * b; 83 - float k_s = -0.0894841775 * a - 1.2914855480 * b; 84 - 85 - { 86 - float l_ = 1.0 + S * k_l; 87 - float m_ = 1.0 + S * k_m; 88 - float s_ = 1.0 + S * k_s; 89 - 90 - float l = l_ * l_ * l_; 91 - float m = m_ * m_ * m_; 92 - float s = s_ * s_ * s_; 93 - 94 - float l_dS = 3.0 * k_l * l_ * l_; 95 - float m_dS = 3.0 * k_m * m_ * m_; 96 - float s_dS = 3.0 * k_s * s_ * s_; 97 - 98 - float l_dS2 = 6.0 * k_l * k_l * l_; 99 - float m_dS2 = 6.0 * k_m * k_m * m_; 100 - float s_dS2 = 6.0 * k_s * k_s * s_; 101 - 102 - float f = wl * l + wm * m + ws * s; 103 - float f1 = wl * l_dS + wm * m_dS + ws * s_dS; 104 - float f2 = wl * l_dS2 + wm * m_dS2 + ws * s_dS2; 105 - 106 - S = S - f * f1 / (f1 * f1 - 0.5 * f * f2); 107 - } 108 - 109 - return S; 110 - } 111 - 112 - // finds L_cusp and C_cusp for a given hue 113 - // a and b must be normalized so a^2 + b^2 == 1 114 - vec2 find_cusp(float a, float b) { 115 - // First, find the maximum saturation (saturation S = C/L) 116 - float S_cusp = compute_max_saturation(a, b); 117 - 118 - // Convert to linear sRGB to find the first point where at least one of r,g or b >= 1: 119 - vec3 rgb_at_max = oklab_to_linear_srgb(vec3( 1, S_cusp * a, S_cusp * b )); 120 - float L_cusp = cbrt(1.0 / max(max(rgb_at_max.r, rgb_at_max.g), rgb_at_max.b)); 121 - float C_cusp = L_cusp * S_cusp; 122 - 123 - return vec2( L_cusp , C_cusp ); 124 - } 125 - 126 - float toe_inv(float x) { 127 - float k_1 = 0.206; 128 - float k_2 = 0.03; 129 - float k_3 = (1.0 + k_1) / (1.0 + k_2); 130 - return (x * x + k_1 * x) / (k_3 * (x + k_2)); 131 - } 132 - 133 - vec2 to_ST(vec2 cusp) { 134 - float L = cusp.x; 135 - float C = cusp.y; 136 - return vec2( C / L, C / (1.0 - L) ); 137 - } 138 - 139 - vec3 okhsv_to_srgb(vec3 hsv) { 140 - float h = hsv.x; 141 - float s = hsv.y; 142 - float v = hsv.z; 143 - 144 - float a_ = cos(2.0 * M_PI * h); 145 - float b_ = sin(2.0 * M_PI * h); 146 - 147 - vec2 cusp = find_cusp(a_, b_); 148 - vec2 ST_max = to_ST(cusp); 149 - float S_max = ST_max.x; 150 - float T_max = ST_max.y; 151 - float S_0 = 0.5; 152 - float k = 1.0- S_0 / S_max; 153 - 154 - // first we compute L and V as if the gamut is a perfect triangle: 155 - 156 - // L, C when v==1: 157 - float L_v = 1.0 - s * S_0 / (S_0 + T_max - T_max * k * s); 158 - float C_v = s * T_max * S_0 / (S_0 + T_max - T_max * k * s); 159 - 160 - float L = v * L_v; 161 - float C = v * C_v; 162 - 163 - // then we compensate for both toe and the curved top part of the triangle: 164 - float L_vt = toe_inv(L_v); 165 - float C_vt = C_v * L_vt / L_v; 166 - 167 - float L_new = toe_inv(L); 168 - C = C * L_new / L; 169 - L = L_new; 170 - 171 - vec3 rgb_scale = oklab_to_linear_srgb(vec3( L_vt, a_ * C_vt, b_ * C_vt )); 172 - float scale_L = cbrt(1.0 / max(max(rgb_scale.r, rgb_scale.g), max(rgb_scale.b, 0.0))); 173 - 174 - L = L * scale_L; 175 - C = C * scale_L; 176 - 177 - vec3 rgb = oklab_to_linear_srgb(vec3( L, C * a_, C * b_ )); 178 - return vec3( 179 - srgb_transfer_function(rgb.r), 180 - srgb_transfer_function(rgb.g), 181 - srgb_transfer_function(rgb.b) 182 - ); 183 - }
-2
src/lib/components/extra/color-picker/base/shaders/readme.md
··· 1 - shaders adapted from https://github.com/dokozero/okcolor 2 - the source in this folder is minified and included in a ts file to enable svelte-package to be used
-5
src/lib/components/extra/color-picker/base/shaders/v_shader.glsl
··· 1 - attribute vec4 position; 2 - 3 - void main() { 4 - gl_Position = position; 5 - }