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
 - 
					148frontend/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