Gisle Aune
1 year ago
30 changed files with 52989 additions and 521 deletions
-
29cmd/bustest/main.go
-
47cmd/generate-k-image/main.go
-
32cmd/generate-xy-image/main.go
-
8effects/manual.go
-
3frontend/src/app.html
-
294frontend/src/lib/components/AssignmentState.svelte
-
108frontend/src/lib/components/ColorPicker.svelte
-
2frontend/src/lib/components/Icon.svelte
-
98frontend/src/lib/components/bforms/BFormColorOption.svelte
-
8frontend/src/lib/components/bforms/BFormDeleteOption.svelte
-
28frontend/src/lib/components/bforms/BFormIntensityOption.svelte
-
12frontend/src/lib/components/bforms/BFormLine.svelte
-
72frontend/src/lib/components/bforms/BFormOption.svelte
-
103frontend/src/lib/components/bforms/BFormParameter.svelte
-
17frontend/src/lib/components/bforms/BFormPowerOption.svelte
-
20frontend/src/lib/components/bforms/BFormTemperatureOption.svelte
-
25frontend/src/lib/components/scripting/ScriptAssignmentState.svelte
-
170frontend/src/lib/contexts/StateContext.svelte
-
89frontend/src/lib/modals/DeviceModal.svelte
-
34frontend/src/lib/models/color.ts
-
4frontend/src/lib/models/uistate.ts
-
1107frontend/src/lib/utils/color-k.json
-
51007frontend/src/lib/utils/color-xy.json
-
78frontend/src/lib/utils/color.ts
-
16frontend/src/routes/+page.svelte
-
BINfrontend/static/color-hsk.png
-
BINfrontend/static/color-xy.png
-
14internal/color/color.go
-
4internal/color/xy.go
-
81services/httpapiv1/service.go
@ -1,29 +0,0 @@ |
|||||
package main |
|
||||
|
|
||||
import ( |
|
||||
lucifer3 "git.aiterp.net/lucifer3/server" |
|
||||
"git.aiterp.net/lucifer3/server/services" |
|
||||
"git.aiterp.net/lucifer3/server/services/effectenforcer" |
|
||||
"git.aiterp.net/lucifer3/server/services/hue" |
|
||||
"git.aiterp.net/lucifer3/server/services/mill" |
|
||||
"git.aiterp.net/lucifer3/server/services/nanoleaf" |
|
||||
"git.aiterp.net/lucifer3/server/services/tradfri" |
|
||||
"time" |
|
||||
) |
|
||||
|
|
||||
func main() { |
|
||||
bus := lucifer3.EventBus{} |
|
||||
|
|
||||
resolver := services.NewResolver() |
|
||||
sceneMap := services.NewSceneMap(resolver) |
|
||||
|
|
||||
bus.JoinPrivileged(resolver) |
|
||||
bus.JoinPrivileged(sceneMap) |
|
||||
bus.Join(effectenforcer.NewService(resolver, sceneMap)) |
|
||||
bus.Join(nanoleaf.NewService()) |
|
||||
bus.Join(hue.NewService()) |
|
||||
bus.Join(tradfri.NewService()) |
|
||||
bus.Join(mill.NewService()) |
|
||||
|
|
||||
time.Sleep(time.Hour) |
|
||||
} |
|
@ -0,0 +1,47 @@ |
|||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"git.aiterp.net/lucifer3/server/internal/color" |
||||
|
"image" |
||||
|
color2 "image/color" |
||||
|
"image/png" |
||||
|
"math" |
||||
|
"os" |
||||
|
) |
||||
|
|
||||
|
func main() { |
||||
|
img := image.NewRGBA(image.Rect(0, 0, 1000, 1000)) |
||||
|
for y := 0; y < 1000; y += 1 { |
||||
|
for x := 0; x < 1000; x += 1 { |
||||
|
vecX := float64(x-500) / 500.0 |
||||
|
vecY := float64(y-500) / 500.0 |
||||
|
dist := math.Sqrt(vecX*vecX + vecY*vecY) |
||||
|
angle := math.Mod((math.Atan2(vecY/dist, vecX/dist)*(180/math.Pi))+360+90, 360) |
||||
|
|
||||
|
if dist >= 0.9 && dist < 1 && angle > 2.5 && angle < 357.5 { |
||||
|
k := 1000 + int(11000*((angle-2.5)/355)) |
||||
|
c := color.Color{K: &k} |
||||
|
rgb, _ := c.ToRGB() |
||||
|
img.Set(x, y, color2.RGBA{ |
||||
|
R: uint8(rgb.RGB.Red * 255.0), |
||||
|
G: uint8(rgb.RGB.Green * 255.0), |
||||
|
B: uint8(rgb.RGB.Blue * 255.0), |
||||
|
A: 255, |
||||
|
}) |
||||
|
} else if dist < 0.85 { |
||||
|
rgb := (color.HueSat{Hue: math.Mod(angle+180, 360), Sat: dist / 0.85}).ToRGB() |
||||
|
|
||||
|
img.Set(x, y, color2.RGBA{ |
||||
|
R: uint8(rgb.Red * 255.0), |
||||
|
G: uint8(rgb.Green * 255.0), |
||||
|
B: uint8(rgb.Blue * 255.0), |
||||
|
A: 255, |
||||
|
}) |
||||
|
} else { |
||||
|
img.Set(x, y, color2.RGBA{R: 0, G: 0, B: 0, A: 0}) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
_ = png.Encode(os.Stdout, img) |
||||
|
} |
@ -0,0 +1,32 @@ |
|||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"git.aiterp.net/lucifer3/server/internal/color" |
||||
|
"image" |
||||
|
color2 "image/color" |
||||
|
"image/png" |
||||
|
"log" |
||||
|
"os" |
||||
|
) |
||||
|
|
||||
|
func main() { |
||||
|
img := image.NewRGBA(image.Rect(0, 0, 500, 500)) |
||||
|
for y := 0; y < 500; y += 1 { |
||||
|
for x := 0; x < 500; x += 1 { |
||||
|
rgb := (color.XY{X: float64(x) / 499, Y: float64(y) / 499}).ToRGB() |
||||
|
|
||||
|
if y == 300 { |
||||
|
log.Println(rgb.Red, rgb.Green, rgb.Blue) |
||||
|
} |
||||
|
|
||||
|
img.Set(x, y, color2.RGBA{ |
||||
|
R: uint8(rgb.Red * 255.0), |
||||
|
G: uint8(rgb.Green * 255.0), |
||||
|
B: uint8(rgb.Blue * 255.0), |
||||
|
A: 255, |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
_ = png.Encode(os.Stdout, img) |
||||
|
} |
@ -1,294 +0,0 @@ |
|||||
<script lang="ts" context="module"> |
|
||||
const cache: Record<string, ColorRGB> = {}; |
|
||||
|
|
||||
function getColor(color: string) { |
|
||||
if (cache.hasOwnProperty(color)) { |
|
||||
return Promise.resolve(cache[color]); |
|
||||
} |
|
||||
|
|
||||
return fetchColor(color).then(r => { |
|
||||
cache[color] = r.rgb || {red: 255, green: 255, blue: 255}; |
|
||||
return cache[color]; |
|
||||
}).catch(err => { |
|
||||
delete cache[color]; |
|
||||
throw err; |
|
||||
}); |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<script lang="ts"> |
|
||||
import type { State } from "$lib/models/device"; |
|
||||
import Icon from "./Icon.svelte"; |
|
||||
import type { ColorRGB } from '$lib/models/color'; |
|
||||
import { fetchColor } from '$lib/client/lucifer'; |
|
||||
import { createEventDispatcher } from "svelte"; |
|
||||
|
|
||||
export let value: State; |
|
||||
export let deletable: boolean = false; |
|
||||
|
|
||||
const dispatch = createEventDispatcher() |
|
||||
|
|
||||
function togglePower() { |
|
||||
switch (value.power) { |
|
||||
case null: value.power = true; break; |
|
||||
case true: value.power = false; break; |
|
||||
case false: value.power = null; break; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
function toggleIntensity() { |
|
||||
if (value.intensity === null) { |
|
||||
value.intensity = 0.5; |
|
||||
} else { |
|
||||
value.intensity = null; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
function toggleTemperature() { |
|
||||
if (value.temperature === null) { |
|
||||
value.temperature = 20; |
|
||||
} else { |
|
||||
value.temperature = null; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
function toggleColor() { |
|
||||
switch (value.color?.split(":")[0]) { |
|
||||
case undefined: value.color = "k:2750"; break; |
|
||||
case "k": value.color = "hs:180,0.5"; break; |
|
||||
case "hs": value.color = "rgb:1.000,0.800,0.066"; break; |
|
||||
case "rgb": value.color = "xy:0.2000,0.2000"; break; |
|
||||
case "xy": value.color = null; break; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
function computeColorInputs(color: string | null) { |
|
||||
const kind = color?.split(":")[0]; |
|
||||
const values = color?.split(":")[1]?.split(",").map(v => parseFloat(v)) || [0,0]; |
|
||||
|
|
||||
switch (kind) { |
|
||||
case undefined: colorKind = "null"; break; |
|
||||
case "k": colorKind = "k"; colorX = values[0]; colorY = values[1]||0; break; |
|
||||
case "hs": colorKind = "hs"; colorX = values[0]; colorY = values[1]||0; break; |
|
||||
case "xy": colorKind = "xy"; colorX = values[0]; colorY = values[1]||0; break; |
|
||||
case "rgb": colorKind = "rgb"; colorX = values[0]; colorY = values[1]||0, colorZ = values[2]||0; break; |
|
||||
} |
|
||||
|
|
||||
computedColor = color; |
|
||||
} |
|
||||
|
|
||||
function updateColor(kind: string, x: number, y: number, z: number) { |
|
||||
x = x || 0; |
|
||||
y = y || 0; |
|
||||
z = z || 0; |
|
||||
|
|
||||
switch (kind) { |
|
||||
case "k": value.color = `k:${x.toFixed(0)}`; break; |
|
||||
case "xy": value.color = `xy:${x.toFixed(4)},${y.toFixed(4)}`; break; |
|
||||
case "hs": value.color = `hs:${x.toFixed(0)},${y.toFixed(3)}`; break; |
|
||||
case "rgb": value.color = `rgb:${x.toFixed(3)},${y.toFixed(3)},${z.toFixed(3)}`; break; |
|
||||
} |
|
||||
|
|
||||
computedColor = value.color; |
|
||||
} |
|
||||
|
|
||||
let intensityColor = "none"; |
|
||||
$: intensityColor = value.intensity !== null ? "off" : "none"; |
|
||||
|
|
||||
let temperatureColor = "none"; |
|
||||
$: temperatureColor = value.temperature !== null ? "off" : "none"; |
|
||||
|
|
||||
let powerColor = ""; |
|
||||
$: if (value.power != null) { |
|
||||
powerColor = value.power ? "on" : "off" |
|
||||
} else { |
|
||||
powerColor = "none" |
|
||||
} |
|
||||
|
|
||||
let computedColor: string | null = ""; |
|
||||
let colorButton = "none"; |
|
||||
let colorKind = "null"; |
|
||||
let colorX = 0; |
|
||||
let colorY = 0; |
|
||||
let colorZ = 0; |
|
||||
|
|
||||
$: if (value.color !== computedColor) { |
|
||||
computeColorInputs(value.color); |
|
||||
} |
|
||||
|
|
||||
$: updateColor(colorKind, colorX, colorY, colorZ); |
|
||||
|
|
||||
let timeout: NodeJS.Timeout | null = null; |
|
||||
let rgb = ""; |
|
||||
$: if (value.color !== null) { |
|
||||
let before = value.color; |
|
||||
|
|
||||
if (timeout !== null) { |
|
||||
clearTimeout(timeout); |
|
||||
} |
|
||||
|
|
||||
timeout = setTimeout(() => { |
|
||||
if (value.color !== null) { |
|
||||
getColor(value.color).then(v => { |
|
||||
if (value.color === before) { |
|
||||
rgb = `rgb(${v.red*255}, ${v.green*255}, ${v.blue*255})`; |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
}, 50) |
|
||||
} else { |
|
||||
rgb = "hsl(240, 8%, 21%)"; |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events --> |
|
||||
<div class="assignment-state"> |
|
||||
<div class="option {powerColor}"><Icon on:click={togglePower} block name="power" /></div> |
|
||||
<div class="option {intensityColor}"> |
|
||||
<Icon on:click={toggleIntensity} block name="cirlce_notch" /> |
|
||||
{#if value.intensity != null} |
|
||||
<input class="custom" type="number" min={0} max={1} step={0.01} bind:value={value.intensity} /> |
|
||||
{/if} |
|
||||
</div> |
|
||||
<div class="option {temperatureColor}"> |
|
||||
<Icon on:click={toggleTemperature} block name="temperature_half" /> |
|
||||
{#if value.temperature != null} |
|
||||
<input class="custom" type="number" min={10} max={40} step={0.5} bind:value={value.temperature} /> |
|
||||
{/if} |
|
||||
</div> |
|
||||
<div class="option {colorButton}" style="--color: {rgb}"> |
|
||||
<Icon on:click={toggleColor} block name="palette" /> |
|
||||
{#if colorKind !== "null"} |
|
||||
{#if colorKind === "k"} |
|
||||
<div class="color-input"> |
|
||||
<label for="color_x">Kelvin</label> |
|
||||
<input class="custom" name="color_x" type="number" min={1000} max={12000} step={10} bind:value={colorX} /> |
|
||||
</div> |
|
||||
{/if} |
|
||||
{#if colorKind === "hs"} |
|
||||
<div class="color-input"> |
|
||||
<label for="color_x">Hue</label> |
|
||||
<input class="custom" name="color_x" type="number" bind:value={colorX} /> |
|
||||
</div> |
|
||||
<div class="color-input"> |
|
||||
<label for="color_y">Sat</label> |
|
||||
<input class="custom" name="color_y" type="number" min={0} max={1} step={0.01} bind:value={colorY} /> |
|
||||
</div> |
|
||||
{/if} |
|
||||
{#if colorKind === "xy"} |
|
||||
<div class="color-input"> |
|
||||
<label for="color_x">X</label> |
|
||||
<input class="custom" name="color_x" type="number" min={0} max={1} step={0.0001} bind:value={colorX} /> |
|
||||
</div> |
|
||||
<div class="color-input"> |
|
||||
<label for="color_y">Y</label> |
|
||||
<input class="custom" name="color_y" type="number" min={0} max={1} step={0.0001} bind:value={colorY} /> |
|
||||
</div> |
|
||||
{/if} |
|
||||
{#if colorKind === "rgb"} |
|
||||
<div class="color-input"> |
|
||||
<label class="short" for="color_x">Red</label> |
|
||||
<input class="custom short" name="color_x" type="number" min={0} max={1} step={0.001} bind:value={colorX} /> |
|
||||
</div> |
|
||||
<div class="color-input"> |
|
||||
<label class="short" for="color_y">Green</label> |
|
||||
<input class="custom short" name="color_y" type="number" min={0} max={1} step={0.001} bind:value={colorY} /> |
|
||||
</div> |
|
||||
<div class="color-input"> |
|
||||
<label class="short" for="color_z">Blue</label> |
|
||||
<input class="custom short" name="color_z" type="number" min={0} max={1} step={0.001} bind:value={colorZ} /> |
|
||||
</div> |
|
||||
{/if} |
|
||||
{/if} |
|
||||
</div> |
|
||||
{#if deletable} |
|
||||
<div class="option red"> |
|
||||
<Icon on:click={() => dispatch("delete")} block name="trash" /> |
|
||||
</div> |
|
||||
{/if} |
|
||||
</div> |
|
||||
|
|
||||
<style lang="sass"> |
|
||||
@import "$lib/css/colors.sass" |
|
||||
|
|
||||
div.assignment-state |
|
||||
display: flex |
|
||||
user-select: none |
|
||||
font-size: 1rem |
|
||||
width: 100% |
|
||||
flex-wrap: wrap |
|
||||
|
|
||||
@media screen and (max-width: 749px) |
|
||||
font-size: 0.66rem |
|
||||
|
|
||||
> div.option |
|
||||
box-shadow: 1px 1px 1px #000 |
|
||||
display: flex |
|
||||
margin: 0.25em 0.25ch |
|
||||
cursor: pointer |
|
||||
|
|
||||
:global(.icon) |
|
||||
padding: 0.1em 0.5ch |
|
||||
padding-top: 0.35em |
|
||||
color: var(--color) |
|
||||
|
|
||||
input, :global(div.rangeSlider) |
|
||||
width: 4rem |
|
||||
font-size: 0.9rem |
|
||||
padding-left: 0 |
|
||||
background: $color-main1 |
|
||||
outline: none |
|
||||
border: none |
|
||||
text-align: center |
|
||||
font-size: 1rem |
|
||||
color: $color-main9 |
|
||||
&::-webkit-inner-spin-button |
|
||||
-webkit-appearance: none |
|
||||
margin: 0 |
|
||||
|
|
||||
moz-appearance: textfield |
|
||||
|
|
||||
&:focus |
|
||||
color: $color-main13 |
|
||||
|
|
||||
> div.color-input |
|
||||
display: flex |
|
||||
flex-direction: column |
|
||||
|
|
||||
> label |
|
||||
font-size: 0.5em |
|
||||
color: $color-main5 |
|
||||
text-align: center |
|
||||
margin: 0 |
|
||||
line-height: 1em |
|
||||
margin-top: 0.15rem !important |
|
||||
padding-top: 0 |
|
||||
margin-bottom: -0.2em |
|
||||
width: 2.5rem |
|
||||
|
|
||||
&.short |
|
||||
width: 2.2rem |
|
||||
|
|
||||
|
|
||||
> input |
|
||||
width: 2.5rem |
|
||||
font-size: 0.8em |
|
||||
background: none |
|
||||
|
|
||||
&.short |
|
||||
width: 2.2rem |
|
||||
|
|
||||
|
|
||||
&.none |
|
||||
background: $color-main1 |
|
||||
color: $color-main2 |
|
||||
&.off |
|
||||
background-color: $color-main2 |
|
||||
color: $color-main6 |
|
||||
&.on |
|
||||
background-color: $color-main2 |
|
||||
color: #00FF00 |
|
||||
&.red |
|
||||
background-color: $color-main2-red |
|
||||
color: $color-main6-redder |
|
||||
</style> |
|
@ -0,0 +1,108 @@ |
|||||
|
<script lang="ts" context="module"> |
||||
|
export const selectedColorPicker = writable<any>(null);</script> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import type { Color } from "$lib/models/color"; |
||||
|
import { rgb2hsv } from "$lib/utils/color"; |
||||
|
import { tick } from "svelte"; |
||||
|
import { writable } from "svelte/store"; |
||||
|
|
||||
|
export let color: Color; |
||||
|
export let id: any = null; |
||||
|
|
||||
|
let xy: boolean; |
||||
|
let x: number; |
||||
|
let y: number; |
||||
|
|
||||
|
$: { |
||||
|
if (color.xy) { |
||||
|
xy = true; |
||||
|
x = color.xy.x; |
||||
|
y = color.xy.y; |
||||
|
} else if (color.hs) { |
||||
|
xy = false; |
||||
|
x = Math.sin((360-color.hs.hue) * (Math.PI / 180)) * (color.hs.sat * 0.85); |
||||
|
y = Math.cos((360-color.hs.hue) * (Math.PI / 180)) * (color.hs.sat * 0.85); |
||||
|
} else if (color.k) { |
||||
|
const angle = 2.5 + ((color.k - 1000) / 11000) * 355; |
||||
|
|
||||
|
x = Math.sin((180-angle) * (Math.PI / 180)) * 0.95; |
||||
|
y = Math.cos((180-angle) * (Math.PI / 180)) * 0.95; |
||||
|
} else if (color.rgb) { |
||||
|
const hs = rgb2hsv(color.rgb); |
||||
|
|
||||
|
xy = false; |
||||
|
x = Math.sin((360-hs.hue) * (Math.PI / 180)) * (hs.sat * 0.85); |
||||
|
y = Math.cos((360-hs.hue) * (Math.PI / 180)) * (hs.sat * 0.85); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function onClickColor(event: MouseEvent & { currentTarget: EventTarget & HTMLDivElement; }) { |
||||
|
if (xy) { |
||||
|
const x = Math.floor((event.offsetX / event.currentTarget.offsetWidth) * 1000) / 1000; |
||||
|
const y = Math.floor((event.offsetY / event.currentTarget.offsetHeight) * 1000) / 1000; |
||||
|
color = { xy: { x, y } }; |
||||
|
} else { |
||||
|
const x = 2 * ((event.offsetX / event.currentTarget.offsetWidth) - 0.505); |
||||
|
const y = 2 * ((event.offsetY / event.currentTarget.offsetHeight) - 0.505); |
||||
|
|
||||
|
const dist = Math.sqrt((x*x)+(y*y)); |
||||
|
const angle = ((Math.atan2(y/dist, x/dist)*(180/Math.PI))+360) % 360.0 |
||||
|
|
||||
|
if (dist > 0.9) { |
||||
|
color = { k: Math.max(Math.min(Math.floor((((((angle + 90) % 360)-2.5)/355) * 11000) + 1000), 12000), 1000) } |
||||
|
} else { |
||||
|
color = { hs: { hue: Math.floor((270+angle)%360), sat: Math.floor(Math.min(dist / 0.85, 1) * 100) / 100 } }; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
tick().then(() => { |
||||
|
tick().then(() => { $selectedColorPicker = id; }); |
||||
|
}); |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
{#if $selectedColorPicker == id} |
||||
|
<!-- svelte-ignore a11y-click-events-have-key-events --> |
||||
|
<div class="color-picker" on:click={onClickColor}> |
||||
|
{#if xy} |
||||
|
<img draggable="false" alt="color wheel" src="/color-xy.png" /> |
||||
|
<div class="dot" style="left: calc({x} * 10ch); top: calc({y-1} * 10ch)"> |
||||
|
<div class="ring"></div> |
||||
|
</div> |
||||
|
{:else} |
||||
|
<img draggable="false" alt="color wheel" src="/color-hsk.png" /> |
||||
|
<div class="dot" style="left: calc({((x/2)+0.5)} * 10ch + 0.5px); top: calc({((y/2)+0.5)-1} * 10ch + 0.5px)"> |
||||
|
<div class="ring"></div> |
||||
|
</div> |
||||
|
{/if} |
||||
|
</div> |
||||
|
{/if} |
||||
|
|
||||
|
<style lang="sass"> |
||||
|
@import "$lib/css/colors.sass" |
||||
|
|
||||
|
div.color-picker |
||||
|
position: relative |
||||
|
width: 10ch |
||||
|
height: 10ch |
||||
|
|
||||
|
> img |
||||
|
width: 10ch |
||||
|
height: 10ch |
||||
|
|
||||
|
div.dot |
||||
|
position: relative |
||||
|
z-index: 10 |
||||
|
pointer-events: none |
||||
|
|
||||
|
div.ring |
||||
|
position: relative |
||||
|
left: -0.525ch |
||||
|
top: -0.95ch |
||||
|
width: 1ch |
||||
|
height: 1ch |
||||
|
border: 2px solid $color-main4 |
||||
|
box-sizing: border-box |
||||
|
border-radius: 1ch |
||||
|
</style> |
@ -0,0 +1,98 @@ |
|||||
|
<script lang="ts" context="module"> |
||||
|
let nextId = 0; |
||||
|
</script> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { rgbString, type Color, stringifyColor, parseColor } from "$lib/models/color"; |
||||
|
import { hsToHsl, kToRgb, xyToRgb } from "$lib/utils/color"; |
||||
|
import ColorPicker, { selectedColorPicker } from "../ColorPicker.svelte"; |
||||
|
import BFormOption from "./BFormOption.svelte"; |
||||
|
import BFormParameter from "./BFormParameter.svelte"; |
||||
|
|
||||
|
export let value: string | null; |
||||
|
|
||||
|
let colorPickerId = `BFormColorOption:${++nextId}` |
||||
|
|
||||
|
let color: Color | null; |
||||
|
$: color = parseColor(value); |
||||
|
$: submit(color); |
||||
|
|
||||
|
function toggle(event: MouseEvent & { currentTarget: EventTarget & HTMLDivElement; }) { |
||||
|
if (event.shiftKey) { |
||||
|
if (color !== null) { |
||||
|
if ($selectedColorPicker !== colorPickerId) { |
||||
|
$selectedColorPicker = colorPickerId; |
||||
|
} else { |
||||
|
$selectedColorPicker = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return |
||||
|
} |
||||
|
|
||||
|
if (color === null || (color.hs == null && color.k == null)) { |
||||
|
color = { k: 2750 } |
||||
|
$selectedColorPicker = colorPickerId; |
||||
|
} else { |
||||
|
color = null; |
||||
|
|
||||
|
if ($selectedColorPicker === colorPickerId) { |
||||
|
$selectedColorPicker = null; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function submit(color: Color | null) { |
||||
|
value = stringifyColor(color); |
||||
|
} |
||||
|
|
||||
|
let colorStr = ""; |
||||
|
$: { |
||||
|
if (color !== null) { |
||||
|
if (color.rgb != null) { |
||||
|
colorStr = rgbString(color.rgb); |
||||
|
} else if (color.hs != null) { |
||||
|
colorStr = hsToHsl(color.hs); |
||||
|
} else if (color.xy != null) { |
||||
|
colorStr = rgbString(xyToRgb(color.xy)); |
||||
|
} else { |
||||
|
colorStr = rgbString(kToRgb(color.k || 2750)); |
||||
|
} |
||||
|
} else { |
||||
|
colorStr = "#000000"; |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<BFormOption on:click={toggle} icon="palette" state={(color !== null) || null} color={colorStr}> |
||||
|
{#if color != null} |
||||
|
<div class="picker-wrapper"> |
||||
|
<ColorPicker bind:color={color} id={colorPickerId} /> |
||||
|
</div> |
||||
|
{/if} |
||||
|
{#if color?.xy != null} |
||||
|
<BFormParameter narrower label="X" type="number" max={1} step={0.01} bind:value={color.xy.x} /> |
||||
|
<BFormParameter narrower label="Y" type="number" max={1} step={0.01} bind:value={color.xy.y} /> |
||||
|
{/if} |
||||
|
{#if color?.hs != null} |
||||
|
<BFormParameter narrower label="Hue" type="number" max={360} step={1} bind:value={color.hs.hue} /> |
||||
|
<BFormParameter narrower label="Sat" type="number" max={1} step={0.01} bind:value={color.hs.sat} /> |
||||
|
{/if} |
||||
|
{#if color?.rgb != null} |
||||
|
<BFormParameter narrowest label="R" type="number" max={1} step={0.01} bind:value={color.rgb.red} /> |
||||
|
<BFormParameter narrowest label="G" type="number" max={1} step={0.01} bind:value={color.rgb.green} /> |
||||
|
<BFormParameter narrowest label="B" type="number" max={1} step={0.01} bind:value={color.rgb.blue} /> |
||||
|
{/if} |
||||
|
{#if color?.k != null} |
||||
|
<BFormParameter narrow label="Kelvin" type="number" min={1000} max={12000} step={50} bind:value={color.k} /> |
||||
|
{/if} |
||||
|
</BFormOption> |
||||
|
|
||||
|
<style lang="sass"> |
||||
|
div.picker-wrapper |
||||
|
position: relative |
||||
|
left: 0.5ch |
||||
|
top: -11ch |
||||
|
width: 0 |
||||
|
height: 0 |
||||
|
</style> |
@ -0,0 +1,8 @@ |
|||||
|
<script lang="ts"> |
||||
|
import { createEventDispatcher } from "svelte"; |
||||
|
import BFormOption from "./BFormOption.svelte"; |
||||
|
|
||||
|
const dispatch = createEventDispatcher(); |
||||
|
</script> |
||||
|
|
||||
|
<BFormOption on:click={() => dispatch("delete")} red state icon=trash></BFormOption> |
@ -0,0 +1,28 @@ |
|||||
|
<script lang="ts"> |
||||
|
import BFormOption from "./BFormOption.svelte"; |
||||
|
import BFormParameter from "./BFormParameter.svelte"; |
||||
|
|
||||
|
export let value: number | null; |
||||
|
|
||||
|
function update(percentage: number) { |
||||
|
if (value !== null) { |
||||
|
value = percentage / 100; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function toggle() { |
||||
|
if (value === null) { |
||||
|
value = 0.50; |
||||
|
} else { |
||||
|
value = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
let percentage: number |
||||
|
$: percentage = Math.max(Math.min((value || 0) * 100, 100), 0); |
||||
|
$: update(percentage); |
||||
|
</script> |
||||
|
|
||||
|
<BFormOption on:click={toggle} icon="cirlce_notch" state={(value !== null) || null}> |
||||
|
<BFormParameter type="number" label="%" bind:value={percentage} min={0} max={100} step={1} narrower /> |
||||
|
</BFormOption> |
@ -0,0 +1,12 @@ |
|||||
|
<div class="bform-body"> |
||||
|
<slot></slot> |
||||
|
</div> |
||||
|
|
||||
|
<style lang="sass"> |
||||
|
div.bform-body |
||||
|
display: flex |
||||
|
user-select: none |
||||
|
font-size: 1rem |
||||
|
width: 100% |
||||
|
flex-wrap: wrap |
||||
|
</style> |
@ -0,0 +1,72 @@ |
|||||
|
<script lang="ts" context="module"> |
||||
|
import { getContext, setContext } from "svelte"; |
||||
|
|
||||
|
export const ctxKey = {}; |
||||
|
|
||||
|
export function getBFormOptionEnabled() { |
||||
|
return getContext(ctxKey) as Readable<boolean> |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import Icon, { type IconName } from "../Icon.svelte"; |
||||
|
import { writable, type Readable } from "svelte/store"; |
||||
|
|
||||
|
export let state: boolean | null = false; |
||||
|
export let color: string = "hsl(240, 8%, 56%)"; |
||||
|
export let icon: IconName = "check"; |
||||
|
export let unclickable: boolean = false; |
||||
|
export let red: boolean = false; |
||||
|
|
||||
|
const enabled = writable(state === true); |
||||
|
$: $enabled = state === true; |
||||
|
|
||||
|
setContext(ctxKey, { subscribe: enabled.subscribe }); |
||||
|
</script> |
||||
|
|
||||
|
<div class="bform-option" class:unclickable class:on={state === true} class:off={state === false} style="--color: {color}"> |
||||
|
<Icon on:click block name={icon} /> |
||||
|
<div class="bform-option-body"> |
||||
|
<slot></slot> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<style lang="sass"> |
||||
|
@import "$lib/css/colors.sass" |
||||
|
|
||||
|
div.bform-option |
||||
|
box-shadow: 1px 1px 1px #000 |
||||
|
display: flex |
||||
|
margin: 0.25em 0.25ch |
||||
|
min-height: 1.9rem |
||||
|
cursor: pointer |
||||
|
background: $color-main1 |
||||
|
|
||||
|
:global(.icon) |
||||
|
padding: 0.1em 0.5ch |
||||
|
padding-top: 0.45em |
||||
|
color: $color-main2 |
||||
|
|
||||
|
&.off |
||||
|
background-color: $color-main2 |
||||
|
:global(.icon) |
||||
|
color: $color-main4 |
||||
|
|
||||
|
&.on |
||||
|
background-color: $color-main2 |
||||
|
:global(.icon) |
||||
|
color: var(--color) |
||||
|
|
||||
|
&.red |
||||
|
background-color: $color-main2-red |
||||
|
:global(.icon) |
||||
|
color: $color-main6-redder |
||||
|
|
||||
|
&.unclickable |
||||
|
cursor: default |
||||
|
|
||||
|
div.bform-option-body |
||||
|
display: flex |
||||
|
flex-direction: row |
||||
|
cursor: default |
||||
|
</style> |
@ -0,0 +1,103 @@ |
|||||
|
<script lang="ts"> |
||||
|
import { getBFormOptionEnabled } from "./BFormOption.svelte"; |
||||
|
|
||||
|
export let label: string; |
||||
|
export let type: "number" | "text" | "select"; |
||||
|
export let value: number | string; |
||||
|
export let wide: boolean = false; |
||||
|
export let narrow: boolean = false; |
||||
|
export let narrower: boolean = false; |
||||
|
export let narrowest: boolean = false; |
||||
|
export let min: number = 0; |
||||
|
export let max: number = 100; |
||||
|
export let step: number = 1; |
||||
|
export let options: {value: string | number, label: string}[] = []; |
||||
|
|
||||
|
const enabled = getBFormOptionEnabled(); |
||||
|
</script> |
||||
|
|
||||
|
{#if $enabled} |
||||
|
<div class="bform-parameter" class:wide class:narrow class:narrower class:narrowest> |
||||
|
<label for="input_{label}">{label}</label> |
||||
|
{#if type === "number"} |
||||
|
<input class="custom" on:blur on:focus type="number" bind:value={value} {min} {max} {step} /> |
||||
|
{:else if type === "text"} |
||||
|
<input class="custom" on:blur on:focus type="text" bind:value={value} /> |
||||
|
{:else if type === "select"} |
||||
|
<select class="custom" bind:value={value}> |
||||
|
{#each options as opt (opt.value)} |
||||
|
<option value={opt.value}>{opt.label}</option> |
||||
|
{/each} |
||||
|
</select> |
||||
|
{/if} |
||||
|
</div> |
||||
|
{/if} |
||||
|
|
||||
|
<style lang="sass"> |
||||
|
@import "$lib/css/colors.sass" |
||||
|
|
||||
|
div.bform-parameter |
||||
|
display: flex |
||||
|
flex-direction: column |
||||
|
background: $color-main1 |
||||
|
height: 100% |
||||
|
|
||||
|
> label |
||||
|
font-size: 0.5em |
||||
|
color: $color-main5 |
||||
|
text-align: center |
||||
|
margin: 0 |
||||
|
line-height: 1em |
||||
|
margin-top: 0.15rem !important |
||||
|
padding-top: 0 |
||||
|
margin-bottom: -0em |
||||
|
width: 10rem |
||||
|
|
||||
|
> input, > select |
||||
|
width: 10rem |
||||
|
font-size: 0.9rem |
||||
|
padding-left: 0 |
||||
|
background: none |
||||
|
outline: none |
||||
|
border: none |
||||
|
text-align: center |
||||
|
font-size: 1rem |
||||
|
color: $color-main9 |
||||
|
|
||||
|
> option |
||||
|
background: $color-main1 |
||||
|
|
||||
|
&::-webkit-inner-spin-button |
||||
|
-webkit-appearance: none |
||||
|
margin: 0 |
||||
|
|
||||
|
-webkit-appearance: none |
||||
|
-moz-appearance: none |
||||
|
moz-appearance: none |
||||
|
appearance: none |
||||
|
|
||||
|
&:focus |
||||
|
color: $color-main13 |
||||
|
|
||||
|
> select |
||||
|
margin-top: 0.08rem |
||||
|
|
||||
|
&.wide |
||||
|
> input, > label, > select |
||||
|
text-align: left |
||||
|
width: 20rem |
||||
|
> label |
||||
|
padding-left: 0.2rch |
||||
|
|
||||
|
&.narrow |
||||
|
> input, > label, > select |
||||
|
width: calc(6rem + 0.22ch) |
||||
|
|
||||
|
&.narrower |
||||
|
> input, > label, > select |
||||
|
width: 3rem |
||||
|
|
||||
|
&.narrowest |
||||
|
> input, > label, > select |
||||
|
width: calc(2rem - 0.11ch) |
||||
|
</style> |
@ -0,0 +1,17 @@ |
|||||
|
<script lang="ts"> |
||||
|
import BFormOption from "./BFormOption.svelte"; |
||||
|
|
||||
|
export let value: boolean | null; |
||||
|
|
||||
|
function toggle() { |
||||
|
if (value === null) { |
||||
|
value = true; |
||||
|
} else if (value === true) { |
||||
|
value = false; |
||||
|
} else { |
||||
|
value = null; |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<BFormOption icon="power" on:click={toggle} state={value} color="#00FF00"></BFormOption> |
@ -0,0 +1,20 @@ |
|||||
|
<script lang="ts"> |
||||
|
import BFormOption from "./BFormOption.svelte"; |
||||
|
import BFormParameter from "./BFormParameter.svelte"; |
||||
|
|
||||
|
export let value: number | null; |
||||
|
|
||||
|
function toggle() { |
||||
|
if (value === null) { |
||||
|
value = 20; |
||||
|
} else { |
||||
|
value = null; |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<BFormOption on:click={toggle} icon="temperature_half" state={(value !== null) || null}> |
||||
|
{#if value != null} |
||||
|
<BFormParameter type="number" label="Celsius" bind:value={value} min={10} max={32} step={1} narrower /> |
||||
|
{/if} |
||||
|
</BFormOption> |
@ -0,0 +1,25 @@ |
|||||
|
<script lang="ts"> |
||||
|
import type { State } from "$lib/models/device"; |
||||
|
import type { ColorRGB } from '$lib/models/color'; |
||||
|
import BFormLine from "../bforms/BFormLine.svelte"; |
||||
|
import BFormTemperatureOption from "../bforms/BFormTemperatureOption.svelte"; |
||||
|
import BFormColorOption from "../bforms/BFormColorOption.svelte"; |
||||
|
import BFormIntensityOption from "../bforms/BFormIntensityOption.svelte"; |
||||
|
import BFormPowerOption from "../bforms/BFormPowerOption.svelte"; |
||||
|
import BFormDeleteOption from "../bforms/BFormDeleteOption.svelte"; |
||||
|
|
||||
|
export let value: State; |
||||
|
export let deletable: boolean = false; |
||||
|
|
||||
|
$: console.log(value); |
||||
|
</script> |
||||
|
|
||||
|
<BFormLine> |
||||
|
<BFormPowerOption bind:value={value.power} /> |
||||
|
<BFormIntensityOption bind:value={value.intensity} /> |
||||
|
<BFormColorOption bind:value={value.color} /> |
||||
|
<BFormTemperatureOption bind:value={value.temperature} /> |
||||
|
{#if deletable} |
||||
|
<BFormDeleteOption on:delete /> |
||||
|
{/if} |
||||
|
</BFormLine> |
1107
frontend/src/lib/utils/color-k.json
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
51007
frontend/src/lib/utils/color-xy.json
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -1,8 +1,84 @@ |
|||||
import type { ColorRGB } from "$lib/models/color"; |
|
||||
|
import type { ColorHS, ColorRGB, ColorXY } from "$lib/models/color"; |
||||
|
|
||||
|
import XY_MAP from "./color-xy.json"; |
||||
|
import K_MAP from "./color-k.json"; |
||||
|
|
||||
export function rgbToHex(rgb: ColorRGB): string { |
export function rgbToHex(rgb: ColorRGB): string { |
||||
const r = Math.min(Math.round(rgb.red * 255), 255).toString(16).padStart(2, "0"); |
const r = Math.min(Math.round(rgb.red * 255), 255).toString(16).padStart(2, "0"); |
||||
const g = Math.min(Math.round(rgb.green * 255), 255).toString(16).padStart(2, "0"); |
const g = Math.min(Math.round(rgb.green * 255), 255).toString(16).padStart(2, "0"); |
||||
const b = Math.min(Math.round(rgb.blue * 255), 255).toString(16).padStart(2, "0"); |
const b = Math.min(Math.round(rgb.blue * 255), 255).toString(16).padStart(2, "0"); |
||||
return ["#", r, g, b].join(""); |
return ["#", r, g, b].join(""); |
||||
|
} |
||||
|
|
||||
|
export function kToRgb(k: number) { |
||||
|
if (k < 1000) { |
||||
|
k = 1000; |
||||
|
} |
||||
|
if (k > 12000) { |
||||
|
k = 12000; |
||||
|
} |
||||
|
|
||||
|
const key = (Math.round(k / 50) * 50).toFixed(0); |
||||
|
|
||||
|
return (K_MAP as Record<string, ColorRGB>)[key]; |
||||
|
} |
||||
|
|
||||
|
export function xyToRgb(xy: ColorXY) { |
||||
|
const x = (Math.round((xy.x||0) * 100) / 100); |
||||
|
const y = (Math.round((xy.y||0) * 100) / 100); |
||||
|
|
||||
|
return (XY_MAP as Record<string, ColorRGB>)[`${x.toFixed(2)},${y.toFixed(2)}`]; |
||||
|
} |
||||
|
|
||||
|
export function hsToHsl(hs: ColorHS): string { |
||||
|
const l=1-(hs.sat/2); |
||||
|
const m=Math.min(l,1-l); |
||||
|
|
||||
|
return `hsl(${hs.hue}, ${(m?(1-l)/m:0)*100}%, ${l*100}%)`; |
||||
|
} |
||||
|
|
||||
|
export function rgb2hsv (rgb: ColorRGB): ColorHS { |
||||
|
const r = rgb.red; |
||||
|
const g = rgb.green; |
||||
|
const b = rgb.blue; |
||||
|
|
||||
|
let rabs: number, gabs: number, babs: number; |
||||
|
let rr: number, gg: number, bb: number; |
||||
|
let h: number = 0, s: number = 0, v: number; |
||||
|
let diff: number; |
||||
|
|
||||
|
const diffc = (c: number) => (v - c) / 6 / diff + 1 / 2; |
||||
|
const percentRoundFn = (num: number) => Math.round(num * 100) / 100; |
||||
|
|
||||
|
rabs = r / 255; |
||||
|
gabs = g / 255; |
||||
|
babs = b / 255; |
||||
|
v = Math.max(rabs, gabs, babs), |
||||
|
diff = v - Math.min(rabs, gabs, babs); |
||||
|
if (diff == 0) { |
||||
|
h = s = 0; |
||||
|
} else { |
||||
|
s = diff / v; |
||||
|
rr = diffc(rabs); |
||||
|
gg = diffc(gabs); |
||||
|
bb = diffc(babs); |
||||
|
|
||||
|
if (rabs === v) { |
||||
|
h = bb - gg; |
||||
|
} else if (gabs === v) { |
||||
|
h = (1 / 3) + rr - bb; |
||||
|
} else if (babs === v) { |
||||
|
h = (2 / 3) + gg - rr; |
||||
|
} |
||||
|
if (h < 0) { |
||||
|
h += 1; |
||||
|
} else if (h > 1) { |
||||
|
h -= 1; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
hue: Math.round(h * 360), |
||||
|
sat: s, |
||||
|
}; |
||||
} |
} |
After Width: 1000 | Height: 1000 | Size: 173 KiB |
After Width: 500 | Height: 500 | Size: 45 KiB |
Write
Preview
Loading…
Cancel
Save
Reference in new issue