diff --git a/colorm.go b/colorm.go index 5c453cc6d..87c7999f2 100644 --- a/colorm.go +++ b/colorm.go @@ -104,19 +104,57 @@ func (c *ColorM) Translate(r, g, b, a float64) { // RotateHue rotates the hue. func (c *ColorM) RotateHue(theta float64) { - sin, cos := math.Sincos(theta) - v1 := cos + (1.0-cos)/3.0 - v2 := (1.0/3.0)*(1.0-cos) - math.Sqrt(1.0/3.0)*sin - v3 := (1.0/3.0)*(1.0-cos) + math.Sqrt(1.0/3.0)*sin + c.ChangeHSV(theta, 1, 1) +} + +var ( + // The YCbCr value ranges are: + // Y: [ 0 - 1 ] + // Cb: [-0.5 - 0.5] + // Cr: [-0.5 - 0.5] + + rgbToYCbCr = ColorM{ + initialized: true, + es: [ColorMDim - 1][ColorMDim]float64{ + {0.2990, 0.5870, 0.1140, 0, 0}, + {-0.1687, -0.3313, 0.5000, 0, 0}, + {0.5000, -0.4187, -0.0813, 0, 0}, + {0, 0, 0, 1, 0}, + }, + } + yCbCrToRgb = ColorM{ + initialized: true, + es: [ColorMDim - 1][ColorMDim]float64{ + {1, 0, 1.40200, 0, 0}, + {1, -0.34414, -0.71414, 0, 0}, + {1, 1.77200, 0, 0, 0}, + {0, 0, 0, 1, 0}, + }, + } +) + +// ChangeHSV changes HSV (Hue-Saturation-Value) values. +// hueTheta is a radian value to ratate hue. +// saturationScale is a value to scale saturation. +// valueScale is a value to scale value (a.k.a. brightness). +// +// This conversion uses RGB to/from YCrCb conversion. +func (c *ColorM) ChangeHSV(hueTheta float64, saturationScale float64, valueScale float64) { + sin, cos := math.Sincos(hueTheta) + c.Concat(rgbToYCbCr) c.Concat(ColorM{ initialized: true, es: [ColorMDim - 1][ColorMDim]float64{ - {v1, v2, v3, 0, 0}, - {v3, v1, v2, 0, 0}, - {v2, v3, v1, 0, 0}, + {1, 0, 0, 0, 0}, + {0, cos, -sin, 0, 0}, + {0, sin, cos, 0, 0}, {0, 0, 0, 1, 0}, }, }) + s := saturationScale + v := valueScale + c.Scale(v, s*v, s*v, 1) + c.Concat(yCbCrToRgb) } // SetElement sets an element at (i, j). @@ -127,20 +165,15 @@ func (c *ColorM) SetElement(i, j int, element float64) { c.es[i][j] = element } +var monochrome ColorM + +func init() { + monochrome.ChangeHSV(0, 0, 1) +} + // Monochrome returns a color matrix to make an image monochrome. func Monochrome() ColorM { - const r = 6968.0 / 32768.0 - const g = 23434.0 / 32768.0 - const b = 2366.0 / 32768.0 - return ColorM{ - initialized: true, - es: [ColorMDim - 1][ColorMDim]float64{ - {r, g, b, 0, 0}, - {r, g, b, 0, 0}, - {r, g, b, 0, 0}, - {0, 0, 0, 1, 0}, - }, - } + return monochrome } // Deprecated as of 1.2.0-alpha. Use Scale instead. diff --git a/examples/hsv/main.go b/examples/hsv/main.go new file mode 100644 index 000000000..7d28b6a92 --- /dev/null +++ b/examples/hsv/main.go @@ -0,0 +1,104 @@ +// Copyright 2016 Hajime Hoshi +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + _ "image/jpeg" + "log" + "math" + + "github.com/hajimehoshi/ebiten" + "github.com/hajimehoshi/ebiten/ebitenutil" +) + +const ( + screenWidth = 320 + screenHeight = 240 +) + +var ( + hueInt = 0 + saturationInt = 128 + valueInt = 128 + gophersImage *ebiten.Image +) + +func clamp(v, min, max int) int { + if min > max { + panic("min must <= max") + } + if v < min { + return min + } + if max < v { + return max + } + return v +} + +func update(screen *ebiten.Image) error { + if ebiten.IsKeyPressed(ebiten.KeyQ) { + hueInt-- + } + if ebiten.IsKeyPressed(ebiten.KeyW) { + hueInt++ + } + if ebiten.IsKeyPressed(ebiten.KeyA) { + saturationInt-- + } + if ebiten.IsKeyPressed(ebiten.KeyS) { + saturationInt++ + } + if ebiten.IsKeyPressed(ebiten.KeyZ) { + valueInt-- + } + if ebiten.IsKeyPressed(ebiten.KeyX) { + valueInt++ + } + hueInt = clamp(hueInt, -256, 256) + saturationInt = clamp(saturationInt, 0, 256) + valueInt = clamp(valueInt, 0, 256) + + w, h := gophersImage.Size() + op := &ebiten.DrawImageOptions{} + op.GeoM.Translate(float64(screenWidth-w)/2, float64(screenHeight-h)/2) + hue := float64(hueInt) * 2 * math.Pi / 128 + saturation := float64(saturationInt) / 128 + value := float64(valueInt) / 128 + op.ColorM.ChangeHSV(hue, saturation, value) + if err := screen.DrawImage(gophersImage, op); err != nil { + return err + } + + msg := fmt.Sprintf(`Hue: %0.2f [Q][W] +Saturation: %0.2f [A][S] +Value: %0.2f [Z][X]`, hue, saturation, value) + if err := ebitenutil.DebugPrint(screen, msg); err != nil { + return err + } + return nil +} + +func main() { + var err error + gophersImage, _, err = ebitenutil.NewImageFromFile("_resources/images/gophers.jpg", ebiten.FilterNearest) + if err != nil { + log.Fatal(err) + } + if err := ebiten.Run(update, screenWidth, screenHeight, 2, "HSV (Ebiten Demo)"); err != nil { + log.Fatal(err) + } +}