1/*
2 * Copyright (C) 2013 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 *    notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 *    notice, this list of conditions and the following disclaimer in the
11 *    documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26WebInspector.ColorWheel = function()
27{
28    WebInspector.Object.call(this);
29
30    this._rawCanvas = document.createElement("canvas");
31    this._tintedCanvas = document.createElement("canvas");
32    this._finalCanvas = document.createElement("canvas");
33
34    this._crosshair = document.createElement("div");
35    this._crosshair.className = "crosshair";
36
37    this._element = document.createElement("div");
38    this._element.className = "color-wheel";
39
40    this._element.appendChild(this._finalCanvas);
41    this._element.appendChild(this._crosshair);
42
43    this._finalCanvas.addEventListener("mousedown", this);
44}
45
46WebInspector.ColorWheel.prototype = {
47    contructor: WebInspector.ColorWheel,
48    __proto__: WebInspector.Object.prototype,
49
50    // Public
51
52    set dimension(dimension)
53    {
54        this._finalCanvas.width = this._tintedCanvas.width = this._rawCanvas.width = dimension * window.devicePixelRatio;
55        this._finalCanvas.height = this._tintedCanvas.height = this._rawCanvas.height = dimension * window.devicePixelRatio;
56
57        this._finalCanvas.style.width = this._finalCanvas.style.height = dimension + "px";
58
59        this._dimension = dimension;
60        // We shrink the radius a bit for better anti-aliasing.
61        this._radius = dimension / 2 - 2;
62
63        this._setCrosshairPosition(new WebInspector.Point(dimension / 2, dimension / 2));
64
65        this._drawRawCanvas();
66        this._draw();
67    },
68
69    get element()
70    {
71        return this._element;
72    },
73
74    get brightness()
75    {
76        return this._brightness;
77    },
78
79    set brightness(brightness)
80    {
81        this._brightness = brightness;
82        this._draw();
83    },
84
85    get tintedColor()
86    {
87        if (this._crosshairPosition)
88            return this._colorAtPointWithBrightness(this._crosshairPosition.x * window.devicePixelRatio, this._crosshairPosition.y * window.devicePixelRatio, this._brightness);
89
90        return new WebInspector.Color(WebInspector.Color.Format.RGBA, [0, 0, 0, 0]);
91    },
92
93    set tintedColor(tintedColor)
94    {
95        var data = this._tintedColorToPointAndBrightness(tintedColor);
96        this._setCrosshairPosition(data.point);
97        this.brightness = data.brightness;
98    },
99
100    get rawColor()
101    {
102        if (this._crosshairPosition)
103            return this._colorAtPointWithBrightness(this._crosshairPosition.x * window.devicePixelRatio, this._crosshairPosition.y * window.devicePixelRatio, 1);
104
105        return new WebInspector.Color(WebInspector.Color.Format.RGBA, [0, 0, 0, 0]);
106    },
107
108    // Protected
109
110    handleEvent: function(event)
111    {
112        switch (event.type) {
113        case "mousedown":
114            this._handleMousedown(event);
115            break;
116        case "mousemove":
117            this._handleMousemove(event);
118            break;
119        case "mouseup":
120            this._handleMouseup(event);
121            break;
122        }
123    },
124
125    // Private
126
127    _handleMousedown: function(event)
128    {
129        window.addEventListener("mousemove", this, true);
130        window.addEventListener("mouseup", this, true);
131
132        this._updateColorForMouseEvent(event);
133    },
134
135    _handleMousemove: function(event)
136    {
137        this._updateColorForMouseEvent(event);
138    },
139
140    _handleMouseup: function(event)
141    {
142        window.removeEventListener("mousemove", this, true);
143        window.removeEventListener("mouseup", this, true);
144    },
145
146    _pointInCircleForEvent: function(event)
147    {
148        function distance(a, b)
149        {
150            return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2));
151        }
152
153        function angleFromCenterToPoint(center, point)
154        {
155            return Math.atan2(point.y - center.y, point.x - center.x);
156        }
157
158        function pointOnCircumference(c, r, a)
159        {
160            return new WebInspector.Point(c.x + r * Math.cos(a), c.y + r * Math.sin(a));
161        }
162
163        var dimension = this._dimension;
164        var point = window.webkitConvertPointFromPageToNode(this._finalCanvas, new WebKitPoint(event.pageX, event.pageY));
165        var center = new WebInspector.Point(dimension / 2, dimension / 2);
166        if (distance(point, center) > this._radius) {
167            var angle = angleFromCenterToPoint(center, point);
168            point = pointOnCircumference(center, this._radius, angle);
169        }
170        return point;
171    },
172
173    _updateColorForMouseEvent: function(event)
174    {
175        var point = this._pointInCircleForEvent(event);
176
177        this._setCrosshairPosition(point);
178
179        if (this.delegate && typeof this.delegate.colorWheelColorDidChange === "function")
180            this.delegate.colorWheelColorDidChange(this);
181    },
182
183    _setCrosshairPosition: function(point)
184    {
185        this._crosshairPosition = point;
186        this._crosshair.style.webkitTransform = "translate(" + Math.round(point.x) + "px, " + Math.round(point.y) + "px)";
187    },
188
189    _tintedColorToPointAndBrightness: function(color)
190    {
191        var rgb = color.rgb;
192        var hsv = WebInspector.Color.rgb2hsv(rgb[0], rgb[1], rgb[2]);
193        var cosHue = Math.cos(hsv[0] * Math.PI / 180);
194        var sinHue = Math.sin(hsv[0] * Math.PI / 180);
195        var center = this._dimension / 2;
196        var x = center + (center * cosHue * hsv[1]);
197        var y = center - (center * sinHue * hsv[1]);
198        return {
199            point: new WebInspector.Point(x, y),
200            brightness: hsv[2]
201        };
202    },
203
204    _drawRawCanvas: function() {
205        var ctx = this._rawCanvas.getContext("2d");
206
207        var dimension = this._dimension * window.devicePixelRatio;
208        var center = dimension / 2;
209
210        ctx.fillStyle = "white";
211        ctx.fillRect(0, 0, dimension, dimension);
212
213        var imageData = ctx.getImageData(0, 0, dimension, dimension);
214        var data = imageData.data;
215        for (var j = 0; j < dimension; ++j) {
216            for (var i = 0; i < dimension; ++i) {
217                var color = this._colorAtPointWithBrightness(i, j, 1);
218                if (!color)
219                    continue;
220                var pos = (j * dimension + i) * 4;
221                data[pos] = color.rgb[0];
222                data[pos + 1] = color.rgb[1];
223                data[pos + 2] = color.rgb[2];
224            }
225        }
226        ctx.putImageData(imageData, 0, 0);
227    },
228
229    _colorAtPointWithBrightness: function(x, y, brightness)
230    {
231        var center = this._dimension / 2 * window.devicePixelRatio;
232        var xDis = x - center;
233        var yDis = y - center;
234        var distance = Math.sqrt(xDis * xDis + yDis * yDis);
235
236        if (distance - center > 0.001)
237            return new WebInspector.Color(WebInspector.Color.Format.RGBA, [0, 0, 0, 0]);
238
239        var h = Math.atan2(y - center, center - x) * 180 / Math.PI;
240        h = (h + 180) % 360;
241        var v = brightness;
242        var s = Math.max(0, distance) / center;
243
244        var rgb = WebInspector.Color.hsv2rgb(h, s, v);
245        return new WebInspector.Color(WebInspector.Color.Format.RGBA, [
246            Math.round(rgb[0] * 255),
247            Math.round(rgb[1] * 255),
248            Math.round(rgb[2] * 255),
249            1
250        ]);
251    },
252
253    _drawTintedCanvas: function()
254    {
255        var ctx = this._tintedCanvas.getContext("2d");
256        var dimension = this._dimension * window.devicePixelRatio;
257
258        ctx.save();
259        ctx.drawImage(this._rawCanvas, 0, 0, dimension, dimension);
260        if (this._brightness !== 1) {
261            ctx.globalAlpha = 1 - this._brightness;
262            ctx.fillStyle = "black";
263            ctx.fillRect(0, 0, dimension, dimension);
264        }
265        ctx.restore();
266    },
267
268    _draw: function()
269    {
270        this._drawTintedCanvas();
271
272        var ctx = this._finalCanvas.getContext("2d");
273        var dimension = this._dimension * window.devicePixelRatio;
274        var radius = this._radius * window.devicePixelRatio;
275
276        ctx.save();
277        ctx.clearRect(0, 0, dimension, dimension);
278        ctx.beginPath();
279        ctx.arc(dimension / 2, dimension / 2, radius + 1, 0, Math.PI * 2, true);
280        ctx.closePath();
281        ctx.clip();
282        ctx.drawImage(this._tintedCanvas, 0, 0, dimension, dimension);
283        ctx.restore();
284    }
285};
286