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
-
1frontend/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
-
168frontend/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
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