Gisle Aune
3 years ago
7 changed files with 894 additions and 38 deletions
-
39cmd/xy/main.go
-
257internal/drivers/hue2/bridge.go
-
167internal/drivers/hue2/client.go
-
144internal/drivers/hue2/data.go
-
120models/colorvalue.go
-
204models/colorxy.go
-
1models/device.go
@ -0,0 +1,39 @@ |
|||
package main |
|||
|
|||
import ( |
|||
"context" |
|||
"encoding/json" |
|||
"fmt" |
|||
"git.aiterp.net/lucifer/new-server/internal/drivers/hue2" |
|||
"git.aiterp.net/lucifer/new-server/models" |
|||
"log" |
|||
) |
|||
|
|||
func main() { |
|||
client := hue2.NewClient("10.80.8.8", "o2XKGgmVUGNBghYFdLUCVuinOTMxFH4pHV9PuTbU") |
|||
bridge := hue2.NewBridge(client) |
|||
|
|||
err := bridge.RefreshAll(context.Background()) |
|||
if err != nil { |
|||
log.Fatalln(err) |
|||
} |
|||
j, _ := json.Marshal(bridge.GenerateDevices()) |
|||
fmt.Println(string(j)) |
|||
|
|||
for _, device := range bridge.GenerateDevices() { |
|||
switch device.InternalID { |
|||
case "6d5a45b0-ec69-4417-8588-717358b05086": |
|||
c, _ := models.ParseColorValue("xy:0.22,0.18") |
|||
device.State.Color = c |
|||
device.State.Intensity = 1 |
|||
bridge.Update(device) |
|||
case "a71128f4-5295-4ae4-9fbc-5541abc8739b": |
|||
c, _ := models.ParseColorValue("k:2000") |
|||
device.State.Color = c |
|||
device.State.Intensity = 0.2 |
|||
bridge.Update(device) |
|||
} |
|||
} |
|||
|
|||
fmt.Println(bridge.MakeCongruent(context.Background())) |
|||
} |
@ -0,0 +1,257 @@ |
|||
package hue2 |
|||
|
|||
import ( |
|||
"context" |
|||
"fmt" |
|||
"git.aiterp.net/lucifer/new-server/models" |
|||
"golang.org/x/sync/errgroup" |
|||
"math" |
|||
"strings" |
|||
"sync" |
|||
"time" |
|||
) |
|||
|
|||
type Bridge struct { |
|||
mu sync.Mutex |
|||
client *Client |
|||
devices map[string]models.Device |
|||
resources map[string]*ResourceData |
|||
} |
|||
|
|||
func NewBridge(client *Client) *Bridge { |
|||
return &Bridge{ |
|||
client: client, |
|||
devices: make(map[string]models.Device, 64), |
|||
resources: make(map[string]*ResourceData, 256), |
|||
} |
|||
} |
|||
|
|||
func (b *Bridge) Update(devices ...models.Device) { |
|||
b.mu.Lock() |
|||
for _, device := range devices { |
|||
b.devices[device.InternalID] = device |
|||
} |
|||
b.mu.Unlock() |
|||
} |
|||
|
|||
func (b *Bridge) MakeCongruent(ctx context.Context) (int, error) { |
|||
b.mu.Lock() |
|||
dur := time.Millisecond * 200 |
|||
updates := make(map[string]ResourceUpdate) |
|||
for _, device := range b.devices { |
|||
resource := b.resources[device.InternalID] |
|||
if lightID := resource.ServiceID("light"); lightID != nil { |
|||
light := b.resources[*lightID] |
|||
update := ResourceUpdate{TransitionDuration: &dur} |
|||
changed := false |
|||
|
|||
if light.ColorTemperature != nil && device.State.Color.IsKelvin() { |
|||
mirek := 1000000 / device.State.Color.Kelvin |
|||
if mirek < light.ColorTemperature.MirekSchema.MirekMinimum { |
|||
mirek = light.ColorTemperature.MirekSchema.MirekMinimum |
|||
} |
|||
if mirek > light.ColorTemperature.MirekSchema.MirekMaximum { |
|||
mirek = light.ColorTemperature.MirekSchema.MirekMaximum |
|||
} |
|||
if light.ColorTemperature.Mirek == nil || mirek != *light.ColorTemperature.Mirek { |
|||
update.Mirek = &mirek |
|||
changed = true |
|||
} |
|||
} else if xy, ok := device.State.Color.ToXY(); ok && light.Color != nil { |
|||
xy = light.Color.Gamut.Conform(xy).Round() |
|||
if xy.DistanceTo(light.Color.XY) > 0.00015 || (light.ColorTemperature != nil && light.ColorTemperature.Mirek != nil) { |
|||
update.ColorXY = &xy |
|||
changed = true |
|||
} |
|||
} |
|||
if light.Power != nil && light.Power.On != device.State.Power { |
|||
update.Power = &device.State.Power |
|||
changed = true |
|||
} |
|||
if light.Dimming != nil && math.Abs(light.Dimming.Brightness/100-device.State.Intensity) > 0.005 { |
|||
brightness := math.Abs(math.Min(device.State.Intensity*100, 100)) |
|||
update.Brightness = &brightness |
|||
changed = true |
|||
} |
|||
|
|||
if changed { |
|||
updates["light/"+light.ID] = update |
|||
} |
|||
} |
|||
} |
|||
b.mu.Unlock() |
|||
|
|||
if len(updates) == 0 { |
|||
return 0, nil |
|||
} |
|||
|
|||
eg, ctx := errgroup.WithContext(ctx) |
|||
for key := range updates { |
|||
update := updates[key] |
|||
split := strings.SplitN(key, "/", 2) |
|||
link := ResourceLink{Kind: split[0], ID: split[1]} |
|||
eg.Go(func() error { |
|||
return b.client.UpdateResource(ctx, link, update) |
|||
}) |
|||
} |
|||
|
|||
return len(updates), eg.Wait() |
|||
} |
|||
|
|||
func (b *Bridge) GenerateDevices() []models.Device { |
|||
b.mu.Lock() |
|||
resources := b.resources |
|||
b.mu.Unlock() |
|||
|
|||
devices := make([]models.Device, 0, 16) |
|||
for _, resource := range resources { |
|||
if resource.Type != "device" || strings.HasPrefix(resource.Metadata.Archetype, "bridge") { |
|||
continue |
|||
} |
|||
|
|||
device := models.Device{ |
|||
InternalID: resource.ID, |
|||
Name: resource.Metadata.Name, |
|||
DriverProperties: map[string]interface{}{ |
|||
"archetype": resource.Metadata.Archetype, |
|||
"name": resource.ProductData.ProductName, |
|||
"product": resource.ProductData, |
|||
"legacyId": resource.LegacyID, |
|||
}, |
|||
} |
|||
|
|||
// Set icon
|
|||
if resource.ProductData.ProductName == "Hue dimmer switch" { |
|||
device.Icon = "switch" |
|||
} else if resource.ProductData.ProductName == "Hue motion sensor" { |
|||
device.Icon = "sensor" |
|||
} else { |
|||
device.Icon = "lightbulb" |
|||
} |
|||
|
|||
buttonCount := 0 |
|||
for _, ptr := range resource.Services { |
|||
switch ptr.Kind { |
|||
case "device_power": |
|||
{ |
|||
device.DriverProperties["battery"] = resources[ptr.ID].PowerState |
|||
} |
|||
case "button": |
|||
{ |
|||
buttonCount += 1 |
|||
} |
|||
case "zigbee_connectivity": |
|||
{ |
|||
device.DriverProperties["zigbee"] = resources[ptr.ID].Status |
|||
} |
|||
case "motion": |
|||
{ |
|||
device.Capabilities = append(device.Capabilities, models.DCPresence) |
|||
} |
|||
case "temperature": |
|||
{ |
|||
device.Capabilities = append(device.Capabilities, models.DCTemperatureSensor) |
|||
} |
|||
case "light": |
|||
{ |
|||
light := resources[ptr.ID] |
|||
|
|||
if light.Power != nil { |
|||
device.State.Power = light.Power.On |
|||
device.Capabilities = append(device.Capabilities, models.DCPower) |
|||
} |
|||
if light.Dimming != nil { |
|||
device.State.Intensity = light.Dimming.Brightness / 100 |
|||
device.Capabilities = append(device.Capabilities, models.DCIntensity) |
|||
} |
|||
if light.ColorTemperature != nil { |
|||
if light.ColorTemperature.Mirek != nil { |
|||
device.State.Color = models.ColorValue{ |
|||
Kelvin: int(1000000 / *light.ColorTemperature.Mirek), |
|||
} |
|||
} |
|||
device.Capabilities = append(device.Capabilities, models.DCColorKelvin) |
|||
device.DriverProperties["maxTemperature"] = 1000000 / light.ColorTemperature.MirekSchema.MirekMinimum |
|||
device.DriverProperties["minTemperature"] = 1000000 / light.ColorTemperature.MirekSchema.MirekMaximum |
|||
} |
|||
if light.Color != nil { |
|||
if device.State.Color.IsEmpty() { |
|||
device.State.Color = models.ColorValue{ |
|||
XY: &light.Color.XY, |
|||
} |
|||
} |
|||
device.DriverProperties["colorGamut"] = light.Color.Gamut |
|||
device.DriverProperties["colorGamutType"] = light.Color.GamutType |
|||
device.Capabilities = append(device.Capabilities, models.DCColorHS, models.DCColorXY) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
if buttonCount == 4 { |
|||
device.ButtonNames = []string{"On", "DimUp", "DimDown", "Off"} |
|||
} else if buttonCount == 1 { |
|||
device.ButtonNames = []string{"Button"} |
|||
} else { |
|||
for n := 1; n <= buttonCount; n++ { |
|||
device.ButtonNames = append(device.ButtonNames, fmt.Sprint("Button", n)) |
|||
} |
|||
} |
|||
|
|||
devices = append(devices, device) |
|||
} |
|||
|
|||
return devices |
|||
} |
|||
|
|||
func (b *Bridge) Refresh(ctx context.Context, kind string) error { |
|||
if kind == "device" { |
|||
// Device refresh requires the full deal as services are taken for granted.
|
|||
return b.RefreshAll(ctx) |
|||
} |
|||
|
|||
resources, err := b.client.Resources(ctx, kind) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
b.mu.Lock() |
|||
oldResources := b.resources |
|||
b.mu.Unlock() |
|||
|
|||
newResources := make(map[string]*ResourceData, len(b.resources)) |
|||
for key, value := range oldResources { |
|||
if value.Type != kind { |
|||
newResources[key] = value |
|||
} |
|||
} |
|||
for i := range resources { |
|||
resource := resources[i] |
|||
newResources[resource.ID] = &resource |
|||
} |
|||
|
|||
b.mu.Lock() |
|||
b.resources = newResources |
|||
b.mu.Unlock() |
|||
|
|||
return nil |
|||
} |
|||
|
|||
func (b *Bridge) RefreshAll(ctx context.Context) error { |
|||
allResources, err := b.client.AllResources(ctx) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
resources := make(map[string]*ResourceData, len(allResources)) |
|||
|
|||
for i := range allResources { |
|||
resource := allResources[i] |
|||
resources[resource.ID] = &resource |
|||
} |
|||
|
|||
b.mu.Lock() |
|||
b.resources = resources |
|||
b.mu.Unlock() |
|||
|
|||
return nil |
|||
} |
@ -0,0 +1,167 @@ |
|||
package hue2 |
|||
|
|||
import ( |
|||
"bytes" |
|||
"context" |
|||
"crypto/tls" |
|||
"encoding/json" |
|||
"fmt" |
|||
"io" |
|||
"net" |
|||
"net/http" |
|||
"strings" |
|||
"time" |
|||
) |
|||
|
|||
func NewClient(host, token string) *Client { |
|||
ch := make(chan struct{}, 5) |
|||
for i := 0; i < 3; i++ { |
|||
ch <- struct{}{} |
|||
} |
|||
|
|||
return &Client{ |
|||
host: host, |
|||
token: token, |
|||
ch: ch, |
|||
} |
|||
} |
|||
|
|||
type Client struct { |
|||
host string |
|||
token string |
|||
ch chan struct{} |
|||
} |
|||
|
|||
func (c *Client) AllResources(ctx context.Context) ([]ResourceData, error) { |
|||
res := struct { |
|||
Error interface{} |
|||
Data []ResourceData |
|||
}{} |
|||
|
|||
err := c.get(ctx, "clip/v2/resource", &res) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
return res.Data, nil |
|||
} |
|||
|
|||
func (c *Client) Resources(ctx context.Context, kind string) ([]ResourceData, error) { |
|||
res := struct { |
|||
Error interface{} |
|||
Data []ResourceData |
|||
}{} |
|||
|
|||
err := c.get(ctx, "clip/v2/resource/"+kind, &res) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
return res.Data, nil |
|||
} |
|||
|
|||
func (c *Client) UpdateResource(ctx context.Context, link ResourceLink, update ResourceUpdate) error { |
|||
return c.put(ctx, link.Path(), update, nil) |
|||
} |
|||
|
|||
func (c *Client) get(ctx context.Context, path string, target interface{}) error { |
|||
select { |
|||
case <-ctx.Done(): |
|||
return ctx.Err() |
|||
case <-c.ch: |
|||
defer func() { |
|||
c.ch <- struct{}{} |
|||
}() |
|||
} |
|||
|
|||
req, err := http.NewRequest("GET", fmt.Sprintf("https://%s/%s", c.host, path), nil) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
req.Header.Set("hue-application-key", c.token) |
|||
|
|||
res, err := httpClient.Do(req.WithContext(ctx)) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
defer res.Body.Close() |
|||
|
|||
if target == nil { |
|||
return nil |
|||
} |
|||
|
|||
return json.NewDecoder(res.Body).Decode(&target) |
|||
} |
|||
|
|||
func (c *Client) put(ctx context.Context, path string, body interface{}, target interface{}) error { |
|||
select { |
|||
case <-ctx.Done(): |
|||
return ctx.Err() |
|||
case <-c.ch: |
|||
defer func() { |
|||
c.ch <- struct{}{} |
|||
}() |
|||
} |
|||
|
|||
rb, err := reqBody(body) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
req, err := http.NewRequest("PUT", fmt.Sprintf("https://%s/%s", c.host, path), rb) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
req.Header.Set("hue-application-key", c.token) |
|||
|
|||
res, err := httpClient.Do(req.WithContext(ctx)) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
defer res.Body.Close() |
|||
|
|||
if target == nil { |
|||
return nil |
|||
} |
|||
|
|||
return json.NewDecoder(res.Body).Decode(&target) |
|||
} |
|||
|
|||
func reqBody(body interface{}) (io.Reader, error) { |
|||
if body == nil { |
|||
return nil, nil |
|||
} |
|||
|
|||
switch v := body.(type) { |
|||
case []byte: |
|||
return bytes.NewReader(v), nil |
|||
case string: |
|||
return strings.NewReader(v), nil |
|||
case io.Reader: |
|||
return v, nil |
|||
default: |
|||
jsonData, err := json.Marshal(v) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
return bytes.NewReader(jsonData), nil |
|||
} |
|||
} |
|||
|
|||
var httpClient = &http.Client{ |
|||
Transport: &http.Transport{ |
|||
Proxy: http.ProxyFromEnvironment, |
|||
DialContext: (&net.Dialer{ |
|||
Timeout: 30 * time.Second, |
|||
KeepAlive: 30 * time.Second, |
|||
}).DialContext, |
|||
MaxIdleConns: 256, |
|||
MaxIdleConnsPerHost: 16, |
|||
IdleConnTimeout: 10 * time.Minute, |
|||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, |
|||
}, |
|||
Timeout: time.Minute, |
|||
} |
@ -0,0 +1,144 @@ |
|||
package hue2 |
|||
|
|||
import ( |
|||
"fmt" |
|||
"git.aiterp.net/lucifer/new-server/models" |
|||
"strings" |
|||
"time" |
|||
) |
|||
|
|||
type DeviceData struct { |
|||
ID string `json:"id"` |
|||
LegacyID string `json:"id_v1"` |
|||
Metadata DeviceMetadata `json:"metadata"` |
|||
Type string `json:"type"` |
|||
|
|||
ProductData DeviceProductData `json:"product_data"` |
|||
Services []ResourceLink `json:"services"` |
|||
} |
|||
|
|||
type DeviceMetadata struct { |
|||
Archetype string `json:"archetype"` |
|||
Name string `json:"name"` |
|||
} |
|||
|
|||
type DeviceProductData struct { |
|||
Certified bool `json:"certified"` |
|||
ManufacturerName string `json:"manufacturer_name"` |
|||
ModelID string `json:"model_id"` |
|||
ProductArchetype string `json:"product_archetype"` |
|||
ProductName string `json:"product_name"` |
|||
SoftwareVersion string `json:"software_version"` |
|||
} |
|||
|
|||
type ResourceData struct { |
|||
ID string `json:"id"` |
|||
LegacyID string `json:"id_v1"` |
|||
Metadata DeviceMetadata `json:"metadata"` |
|||
Type string `json:"type"` |
|||
|
|||
Mode *string `json:"mode"` |
|||
|
|||
Owner *ResourceLink `json:"owner"` |
|||
ProductData *DeviceProductData `json:"product_data"` |
|||
Services []ResourceLink `json:"services"` |
|||
Power *LightPower `json:"on"` |
|||
Color *LightColor `json:"color"` |
|||
ColorTemperature *LightCT `json:"color_temperature"` |
|||
Dimming *LightDimming `json:"dimming"` |
|||
Dynamics *LightDynamics `json:"dynamics"` |
|||
Alert *LightAlert `json:"alert"` |
|||
PowerState *PowerState `json:"power_state"` |
|||
Status *string `json:"status"` |
|||
} |
|||
|
|||
func (res *ResourceData) ServiceID(kind string) *string { |
|||
for _, ptr := range res.Services { |
|||
if ptr.Kind == kind { |
|||
return &ptr.ID |
|||
} |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
type PowerState struct { |
|||
BatteryState string `json:"battery_state"` |
|||
BatteryLevel float64 `json:"battery_level"` |
|||
} |
|||
|
|||
type LightPower struct { |
|||
On bool `json:"on"` |
|||
} |
|||
|
|||
type LightDimming struct { |
|||
Brightness float64 `json:"brightness"` |
|||
} |
|||
|
|||
type LightColor struct { |
|||
Gamut models.ColorGamut `json:"gamut"` |
|||
GamutType string `json:"gamut_type"` |
|||
XY models.ColorXY `json:"xy"` |
|||
} |
|||
|
|||
type LightCT struct { |
|||
Mirek *int `json:"mirek"` |
|||
MirekSchema LightCTMirekSchema `json:"mirek_schema"` |
|||
MirekValid bool `json:"mirek_valid"` |
|||
} |
|||
|
|||
type LightCTMirekSchema struct { |
|||
MirekMaximum int `json:"mirek_maximum"` |
|||
MirekMinimum int `json:"mirek_minimum"` |
|||
} |
|||
|
|||
type LightDynamics struct { |
|||
Speed float64 `json:"speed"` |
|||
SpeedValid bool `json:"speed_valid"` |
|||
Status string `json:"status"` |
|||
StatusValues []string `json:"status_values"` |
|||
} |
|||
|
|||
type LightAlert struct { |
|||
ActionValues []string `json:"action_values"` |
|||
} |
|||
|
|||
type ResourceUpdate struct { |
|||
Power *bool |
|||
ColorXY *models.ColorXY |
|||
Brightness *float64 |
|||
Mirek *int |
|||
TransitionDuration *time.Duration |
|||
} |
|||
|
|||
func (r ResourceUpdate) MarshalJSON() ([]byte, error) { |
|||
chunks := make([]string, 0, 4) |
|||
if r.Power != nil { |
|||
chunks = append(chunks, fmt.Sprintf(`"on":{"on":%v}`, *r.Power)) |
|||
} |
|||
if r.ColorXY != nil { |
|||
chunks = append(chunks, fmt.Sprintf(`"color":{"xy":{"x":%f,"y":%f}}`, r.ColorXY.X, r.ColorXY.Y)) |
|||
} |
|||
if r.Brightness != nil { |
|||
chunks = append(chunks, fmt.Sprintf(`"dimming":{"brightness":%f}`, *r.Brightness)) |
|||
} |
|||
if r.Mirek != nil { |
|||
chunks = append(chunks, fmt.Sprintf(`"color_temperature":{"mirek":%d}`, *r.Mirek)) |
|||
} |
|||
if r.TransitionDuration != nil { |
|||
chunks = append(chunks, fmt.Sprintf(`"dynamics":{"duration":%d}`, r.TransitionDuration.Truncate(time.Millisecond*100).Milliseconds())) |
|||
} |
|||
|
|||
fmt.Println(fmt.Sprintf("{%s}", strings.Join(chunks, ","))) |
|||
|
|||
return []byte(fmt.Sprintf("{%s}", strings.Join(chunks, ","))), nil |
|||
} |
|||
|
|||
type ResourceLink struct { |
|||
ID string `json:"rid"` |
|||
Kind string `json:"rtype"` |
|||
} |
|||
|
|||
func (rl *ResourceLink) Path() string { |
|||
return fmt.Sprintf("/clip/v2/resource/%s/%s", rl.Kind, rl.ID) |
|||
} |
@ -0,0 +1,204 @@ |
|||
package models |
|||
|
|||
import ( |
|||
"github.com/lucasb-eyer/go-colorful" |
|||
"math" |
|||
) |
|||
|
|||
const eps = 0.0001 |
|||
const epsSquare = eps * eps |
|||
|
|||
type ColorGamut struct { |
|||
Red ColorXY `json:"red"` |
|||
Green ColorXY `json:"green"` |
|||
Blue ColorXY `json:"blue"` |
|||
} |
|||
|
|||
func (cg *ColorGamut) side(x1, y1, x2, y2, x, y float64) float64 { |
|||
return (y2-y1)*(x-x1) + (-x2+x1)*(y-y1) |
|||
} |
|||
|
|||
func (cg *ColorGamut) naiveContains(color ColorXY) bool { |
|||
x, y := color.X, color.Y |
|||
x1, y1 := cg.Red.X, cg.Red.Y |
|||
x2, y2 := cg.Green.X, cg.Green.Y |
|||
x3, y3 := cg.Blue.X, cg.Blue.Y |
|||
|
|||
checkSide1 := cg.side(x1, y1, x2, y2, x, y) < 0 |
|||
checkSide2 := cg.side(x2, y2, x3, y3, x, y) < 0 |
|||
checkSide3 := cg.side(x3, y3, x1, y1, x, y) < 0 |
|||
|
|||
return checkSide1 && checkSide2 && checkSide3 |
|||
} |
|||
|
|||
func (cg *ColorGamut) getBounds() (xMin, xMax, yMin, yMax float64) { |
|||
x1, y1 := cg.Red.X, cg.Red.Y |
|||
x2, y2 := cg.Green.X, cg.Green.Y |
|||
x3, y3 := cg.Blue.X, cg.Blue.Y |
|||
|
|||
xMin = math.Min(x1, math.Min(x2, x3)) - eps |
|||
xMax = math.Max(x1, math.Max(x2, x3)) + eps |
|||
yMin = math.Min(y1, math.Min(y2, y3)) - eps |
|||
yMax = math.Max(y1, math.Max(y2, y3)) + eps |
|||
|
|||
return |
|||
} |
|||
|
|||
func (cg *ColorGamut) isInBounds(color ColorXY) bool { |
|||
x, y := color.X, color.Y |
|||
xMin, xMax, yMin, yMax := cg.getBounds() |
|||
|
|||
return !(x < xMin || xMax < x || y < yMin || yMax < y) |
|||
} |
|||
|
|||
func (cg *ColorGamut) distanceSquarePointToSegment(x1, y1, x2, y2, x, y float64) float64 { |
|||
sqLength1 := (x2-x1)*(x2-x1) + (y2-y1)*(y2-y1) |
|||
dotProduct := ((x-x1)*(x2-x1) + (y-y1)*(y2-y1)) / sqLength1 |
|||
if dotProduct < 0 { |
|||
return (x-x1)*(x-x1) + (y-y1)*(y-y1) |
|||
} else if dotProduct <= 1 { |
|||
sqLength2 := (x1-x)*(x1-x) + (y1-y)*(y1-y) |
|||
return sqLength2 - dotProduct*dotProduct*sqLength1 |
|||
} else { |
|||
return (x-x2)*(x-x2) + (y-y2)*(y-y2) |
|||
} |
|||
} |
|||
|
|||
func (cg *ColorGamut) atTheEdge(color ColorXY) bool { |
|||
x, y := color.X, color.Y |
|||
x1, y1 := cg.Red.X, cg.Red.Y |
|||
x2, y2 := cg.Green.X, cg.Green.Y |
|||
x3, y3 := cg.Blue.X, cg.Blue.Y |
|||
|
|||
if cg.distanceSquarePointToSegment(x1, y1, x2, y2, x, y) <= epsSquare { |
|||
return true |
|||
} |
|||
if cg.distanceSquarePointToSegment(x2, y2, x3, y3, x, y) <= epsSquare { |
|||
return true |
|||
} |
|||
if cg.distanceSquarePointToSegment(x3, y3, x1, y1, x, y) <= epsSquare { |
|||
return true |
|||
} |
|||
|
|||
return false |
|||
} |
|||
|
|||
func (cg *ColorGamut) Contains(color ColorXY) bool { |
|||
if cg == nil { |
|||
return true |
|||
} |
|||
|
|||
return cg.isInBounds(color) && (cg.naiveContains(color) || cg.atTheEdge(color)) |
|||
} |
|||
|
|||
func (cg *ColorGamut) Conform(color ColorXY) ColorXY { |
|||
if cg.Contains(color) { |
|||
return color |
|||
} |
|||
|
|||
var best *ColorXY |
|||
|
|||
xMin, xMax, yMin, yMax := cg.getBounds() |
|||
|
|||
for x := xMin; x < xMax; x += 0.001 { |
|||
for y := yMin; y < yMax; y += 0.001 { |
|||
color2 := ColorXY{X: x, Y: y} |
|||
|
|||
if cg.Contains(color2) { |
|||
if best == nil || color.DistanceTo(color2) < color.DistanceTo(*best) { |
|||
best = &color2 |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
if best == nil { |
|||
centerX := (cg.Red.X + cg.Green.X + cg.Blue.X) / 3 |
|||
centerY := (cg.Red.Y + cg.Green.Y + cg.Blue.Y) / 3 |
|||
|
|||
stepX := (centerX - color.X) / 5000 |
|||
stepY := (centerY - color.Y) / 5000 |
|||
|
|||
for !cg.Contains(color) { |
|||
color.X += stepX |
|||
color.Y += stepY |
|||
} |
|||
|
|||
return color |
|||
} |
|||
|
|||
for x := best.X - 0.001; x < best.X+0.001; x += 0.0001 { |
|||
for y := best.Y - 0.001; y < best.Y+0.001; y += 0.0001 { |
|||
color2 := ColorXY{X: x, Y: y} |
|||
|
|||
if cg.atTheEdge(color2) { |
|||
if best == nil || color.DistanceTo(color2) < color.DistanceTo(*best) { |
|||
best = &color2 |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
for x := best.X - 0.0001; x < best.X+0.0001; x += 0.00001 { |
|||
for y := best.Y - 0.0001; y < best.Y+0.0001; y += 0.00001 { |
|||
color2 := ColorXY{X: x, Y: y} |
|||
|
|||
if cg.atTheEdge(color2) { |
|||
if best == nil || color.DistanceTo(color2) < color.DistanceTo(*best) { |
|||
best = &color2 |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
return *best |
|||
} |
|||
|
|||
type ColorXY struct { |
|||
X float64 `json:"x"` |
|||
Y float64 `json:"y"` |
|||
} |
|||
|
|||
func (xy ColorXY) DistanceTo(other ColorXY) float64 { |
|||
return math.Sqrt(math.Pow(xy.X-other.X, 2) + math.Pow(xy.Y-other.Y, 2)) |
|||
} |
|||
|
|||
func (xy ColorXY) Round() ColorXY { |
|||
return ColorXY{ |
|||
X: math.Round(xy.X*10000) / 10000, |
|||
Y: math.Round(xy.Y*10000) / 10000, |
|||
} |
|||
} |
|||
|
|||
func hsToXY(hue, sat float64) ColorXY { |
|||
c := colorful.Hsv(hue, sat, 1) |
|||
red255, green255, blue255 := c.RGB255() |
|||
red := float64(red255) / 255.0 |
|||
green := float64(green255) / 255.0 |
|||
blue := float64(blue255) / 255.0 |
|||
|
|||
return screenRGBToXY(red, green, blue) |
|||
} |
|||
|
|||
func rgbToXY(red float64, green float64, blue float64) ColorXY { |
|||
x := red*0.649926 + green*0.103455 + blue*0.197109 |
|||
y := red*0.234327 + green*0.743075 + blue*0.022598 |
|||
z := green*0.053077 + blue*1.035763 |
|||
|
|||
return ColorXY{ |
|||
X: x / (x + y + z), |
|||
Y: y / (x + y + z), |
|||
} |
|||
} |
|||
|
|||
func screenRGBToXY(red, green, blue float64) ColorXY { |
|||
for _, component := range []*float64{&red, &green, &blue} { |
|||
if *component > 0.04045 { |
|||
*component = math.Pow((*component+0.055)/(1.055), 2.4) |
|||
} else { |
|||
*component /= 12.92 |
|||
} |
|||
} |
|||
|
|||
return rgbToXY(red, green, blue) |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue