refactorings + basis of script editor.

This commit is contained in:
2023-09-26 22:43:27 +02:00
parent 2188d489ea
commit 13da56009e
30 changed files with 52989 additions and 521 deletions
@@ -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>
+2
View File
@@ -54,6 +54,7 @@
import { faCircleDot } from "@fortawesome/free-solid-svg-icons/faCircleDot";
import { faMasksTheater } from "@fortawesome/free-solid-svg-icons/faMasksTheater";
import { faTag } from "@fortawesome/free-solid-svg-icons/faTag";
import { faFilter } from "@fortawesome/free-solid-svg-icons/faFilter";
const icons = {
"clock": faClock,
@@ -88,6 +89,7 @@
"circle_dot": faCircleDot,
"masks_theater": faMasksTheater,
"tag": faTag,
"filter": faFilter,
};
export type IconName = keyof typeof icons;
@@ -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>