scratch-blocks/core/field_colour_slider.js

388 lines
14 KiB
JavaScript
Raw Normal View History

/**
* @license
* Visual Blocks Editor
*
* Copyright 2012 Google Inc.
* https://developers.google.com/blockly/
*
* 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.
*/
/**
* @fileoverview Colour input field.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.FieldColourSlider');
goog.require('Blockly.Field');
goog.require('Blockly.DropDownDiv');
goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.style');
goog.require('goog.color');
goog.require('goog.ui.Slider');
/**
* Class for a slider-based colour input field.
* @param {string} colour The initial colour in '#rrggbb' format.
* @param {Function=} opt_validator A function that is executed when a new
* colour is selected. Its sole argument is the new colour value. Its
* return value becomes the selected colour, unless it is undefined, in
* which case the new colour stands, or it is null, in which case the change
* is aborted.
* @extends {Blockly.Field}
* @constructor
*/
Blockly.FieldColourSlider = function(colour, opt_validator) {
Blockly.FieldColourSlider.superClass_.constructor.call(this, colour, opt_validator);
this.addArgType('colour');
// Flag to track whether or not the slider callbacks should execute
this.sliderCallbacksEnabled_ = false;
};
goog.inherits(Blockly.FieldColourSlider, Blockly.Field);
2018-04-17 14:15:18 -07:00
/**
* Construct a FieldColourSlider from a JSON arg object.
* @param {!Object} options A JSON object with options (colour).
* @returns {!Blockly.FieldColourSlider} The new field instance.
* @package
* @nocollapse
*/
Blockly.FieldColourSlider.fromJson = function(options) {
return new Blockly.FieldColourSlider(options['colour']);
};
/**
* Function to be called if eyedropper can be activated.
* If defined, an eyedropper button will be added to the color picker.
* The button calls this function with a callback to update the field value.
* BEWARE: This is not a stable API, so it is being marked as private. It may change.
* @private
*/
Blockly.FieldColourSlider.activateEyedropper_ = null;
/**
* Path to the eyedropper svg icon.
*/
Blockly.FieldColourSlider.EYEDROPPER_PATH = 'eyedropper.svg';
/**
* Install this field on a block.
* @param {!Blockly.Block} block The block containing this field.
*/
Blockly.FieldColourSlider.prototype.init = function(block) {
if (this.fieldGroup_) {
// Colour slider has already been initialized once.
return;
}
Blockly.FieldColourSlider.superClass_.init.call(this, block);
this.setValue(this.getValue());
};
/**
* Return the current colour.
* @return {string} Current colour in '#rrggbb' format.
*/
Blockly.FieldColourSlider.prototype.getValue = function() {
return this.colour_;
};
/**
* Set the colour.
* @param {string} colour The new colour in '#rrggbb' format.
*/
Blockly.FieldColourSlider.prototype.setValue = function(colour) {
if (this.sourceBlock_ && Blockly.Events.isEnabled() &&
this.colour_ != colour) {
Blockly.Events.fire(new Blockly.Events.BlockChange(
this.sourceBlock_, 'field', this.name, this.colour_, colour));
}
this.colour_ = colour;
if (this.sourceBlock_) {
// Set the colours to this value.
// The renderer expects to be able to use the secondary colour as the fill for a shadow.
this.sourceBlock_.setColour(colour, colour, this.sourceBlock_.getColourTertiary(),
this.sourceBlock_.getColourQuaternary());
}
this.updateSliderHandles_();
this.updateDom_();
};
/**
* Create the hue, saturation or value CSS gradient for the slide backgrounds.
* @param {string} channel Either "hue", "saturation" or "value".
* @return {string} Array colour hex colour stops for the given channel
2017-09-19 10:15:31 -04:00
* @private
*/
Blockly.FieldColourSlider.prototype.createColourStops_ = function(channel) {
var stops = [];
for(var n = 0; n <= 360; n += 20) {
switch (channel) {
case 'hue':
stops.push(goog.color.hsvToHex(n, this.saturation_, this.brightness_));
break;
case 'saturation':
stops.push(goog.color.hsvToHex(this.hue_, n / 360, this.brightness_));
break;
case 'brightness':
stops.push(goog.color.hsvToHex(this.hue_, this.saturation_, 255 * n / 360));
break;
2017-09-19 10:26:18 -04:00
default:
throw new Error("Unknown channel for colour sliders: " + channel);
}
}
return stops;
};
/**
* Set the gradient CSS properties for the given node and channel
* @param {Node} node - The DOM node the gradient will be set on.
* @param {string} channel Either "hue", "saturation" or "value".
2017-09-19 10:15:31 -04:00
* @private
*/
Blockly.FieldColourSlider.prototype.setGradient_ = function(node, channel) {
2017-09-19 10:15:31 -04:00
var gradient = this.createColourStops_(channel).join(',');
goog.style.setStyle(node, 'background',
2018-05-02 15:32:28 -07:00
'-moz-linear-gradient(left, ' + gradient + ')');
goog.style.setStyle(node, 'background',
2018-05-02 15:32:28 -07:00
'-webkit-linear-gradient(left, ' + gradient + ')');
goog.style.setStyle(node, 'background',
2018-05-02 15:32:28 -07:00
'-o-linear-gradient(left, ' + gradient + ')');
goog.style.setStyle(node, 'background',
2018-05-02 15:32:28 -07:00
'-ms-linear-gradient(left, ' + gradient + ')');
goog.style.setStyle(node, 'background',
2018-05-02 15:32:28 -07:00
'linear-gradient(left, ' + gradient + ')');
};
/**
* Update the readouts and slider backgrounds after value has changed.
2017-09-19 10:15:31 -04:00
* @private
*/
Blockly.FieldColourSlider.prototype.updateDom_ = function() {
if (this.hueSlider_) {
// Update the slider backgrounds
this.setGradient_(this.hueSlider_.getElement(), 'hue');
this.setGradient_(this.saturationSlider_.getElement(), 'saturation');
this.setGradient_(this.brightnessSlider_.getElement(), 'brightness');
// Update the readouts
this.hueReadout_.textContent = Math.floor(100 * this.hue_ / 360).toFixed(0);
this.saturationReadout_.textContent = Math.floor(100 * this.saturation_).toFixed(0);
this.brightnessReadout_.textContent = Math.floor(100 * this.brightness_ / 255).toFixed(0);
}
};
/**
* Update the slider handle positions from the current field value.
* @private
*/
Blockly.FieldColourSlider.prototype.updateSliderHandles_ = function() {
if (this.hueSlider_) {
// Don't let the following calls to setValue for each of the sliders
// trigger the slider callbacks (which then call setValue on this field again
// unnecessarily)
this.sliderCallbacksEnabled_ = false;
this.hueSlider_.setValue(this.hue_);
this.saturationSlider_.setValue(this.saturation_);
this.brightnessSlider_.setValue(this.brightness_);
this.sliderCallbacksEnabled_ = true;
}
};
/**
* Get the text from this field. Used when the block is collapsed.
* @return {string} Current text.
*/
Blockly.FieldColourSlider.prototype.getText = function() {
var colour = this.colour_;
// Try to use #rgb format if possible, rather than #rrggbb.
var m = colour.match(/^#(.)\1(.)\2(.)\3$/);
if (m) {
colour = '#' + m[1] + m[2] + m[3];
}
return colour;
};
/**
* Create label and readout DOM elements, returning the readout
* @param {string} labelText - Text for the label
* @return {Array} The container node and the readout node.
* @private
*/
Blockly.FieldColourSlider.prototype.createLabelDom_ = function(labelText) {
var labelContainer = document.createElement('div');
labelContainer.setAttribute('class', 'scratchColourPickerLabel');
var readout = document.createElement('span');
readout.setAttribute('class', 'scratchColourPickerReadout');
var label = document.createElement('span');
label.setAttribute('class', 'scratchColourPickerLabelText');
label.textContent = labelText;
labelContainer.appendChild(label);
labelContainer.appendChild(readout);
return [labelContainer, readout];
};
/**
* Factory for creating the different slider callbacks
* @param {string} channel - One of "hue", "saturation" or "brightness"
* @return {function} the callback for slider update
* @private
*/
Blockly.FieldColourSlider.prototype.sliderCallbackFactory_ = function(channel) {
var thisField = this;
return function(event) {
if (!thisField.sliderCallbacksEnabled_) return;
var channelValue = event.target.getValue();
switch (channel) {
case 'hue':
thisField.hue_ = channelValue;
break;
case 'saturation':
thisField.saturation_ = channelValue;
break;
case 'brightness':
thisField.brightness_ = channelValue;
break;
}
var colour = goog.color.hsvToHex(thisField.hue_, thisField.saturation_, thisField.brightness_);
if (thisField.sourceBlock_) {
2018-05-02 15:32:28 -07:00
// Call any validation function, and allow it to override.
colour = thisField.callValidator(colour);
}
if (colour !== null) {
thisField.setValue(colour, true);
}
};
};
/**
* Activate the eyedropper, passing in a callback for setting the field value.
* @private
*/
Blockly.FieldColourSlider.prototype.activateEyedropperInternal_ = function() {
var thisField = this;
Blockly.FieldColourSlider.activateEyedropper_(function(value) {
// Update the internal hue/saturation/brightness values so sliders update.
var hsv = goog.color.hexToHsv(value);
thisField.hue_ = hsv[0];
thisField.saturation_ = hsv[1];
thisField.brightness_ = hsv[2];
thisField.setValue(value);
});
};
/**
* Create hue, saturation and brightness sliders under the colour field.
* @private
*/
Blockly.FieldColourSlider.prototype.showEditor_ = function() {
Blockly.DropDownDiv.hideWithoutAnimation();
Blockly.DropDownDiv.clearContent();
var div = Blockly.DropDownDiv.getContentDiv();
// Init color component values that are used while the editor is open
// in order to keep the slider values stable.
var hsv = goog.color.hexToHsv(this.getValue());
this.hue_ = hsv[0];
this.saturation_ = hsv[1];
this.brightness_ = hsv[2];
var hueElements = this.createLabelDom_(Blockly.Msg.COLOUR_HUE_LABEL);
div.appendChild(hueElements[0]);
this.hueReadout_ = hueElements[1];
this.hueSlider_ = new goog.ui.Slider();
this.hueSlider_.setUnitIncrement(5);
this.hueSlider_.setMinimum(0);
this.hueSlider_.setMaximum(360);
this.hueSlider_.setMoveToPointEnabled(true);
this.hueSlider_.render(div);
2018-05-02 15:32:28 -07:00
var saturationElements =
this.createLabelDom_(Blockly.Msg.COLOUR_SATURATION_LABEL);
div.appendChild(saturationElements[0]);
this.saturationReadout_ = saturationElements[1];
this.saturationSlider_ = new goog.ui.Slider();
this.saturationSlider_.setMoveToPointEnabled(true);
this.saturationSlider_.setUnitIncrement(0.01);
this.saturationSlider_.setStep(0.001);
this.saturationSlider_.setMinimum(0);
this.saturationSlider_.setMaximum(1.0);
this.saturationSlider_.render(div);
2018-05-02 15:32:28 -07:00
var brightnessElements =
this.createLabelDom_(Blockly.Msg.COLOUR_BRIGHTNESS_LABEL);
div.appendChild(brightnessElements[0]);
this.brightnessReadout_ = brightnessElements[1];
this.brightnessSlider_ = new goog.ui.Slider();
this.brightnessSlider_.setUnitIncrement(2);
this.brightnessSlider_.setMinimum(0);
this.brightnessSlider_.setMaximum(255);
this.brightnessSlider_.setMoveToPointEnabled(true);
this.brightnessSlider_.render(div);
if (Blockly.FieldColourSlider.activateEyedropper_) {
var button = document.createElement('button');
button.setAttribute('class', 'scratchEyedropper');
var image = document.createElement('img');
image.src = Blockly.mainWorkspace.options.pathToMedia + Blockly.FieldColourSlider.EYEDROPPER_PATH;
button.appendChild(image);
div.appendChild(button);
2018-05-02 15:32:28 -07:00
Blockly.FieldColourSlider.eyedropperEventData_ =
Blockly.bindEventWithChecks_(button, 'click', this,
2018-05-02 15:32:28 -07:00
this.activateEyedropperInternal_);
}
Blockly.DropDownDiv.setColour('#ffffff', '#dddddd');
Blockly.DropDownDiv.setCategory(this.sourceBlock_.parentBlock_.getCategory());
Blockly.DropDownDiv.showPositionedByBlock(this, this.sourceBlock_);
// Set value updates the slider positions
// Do this before attaching callbacks to avoid extra events from initial set
this.setValue(this.getValue());
// Enable callbacks for the sliders
this.sliderCallbacksEnabled_ = true;
Blockly.FieldColourSlider.hueChangeEventKey_ = goog.events.listen(this.hueSlider_,
goog.ui.Component.EventType.CHANGE,
this.sliderCallbackFactory_('hue'));
Blockly.FieldColourSlider.saturationChangeEventKey_ = goog.events.listen(this.saturationSlider_,
goog.ui.Component.EventType.CHANGE,
this.sliderCallbackFactory_('saturation'));
Blockly.FieldColourSlider.brightnessChangeEventKey_ = goog.events.listen(this.brightnessSlider_,
goog.ui.Component.EventType.CHANGE,
this.sliderCallbackFactory_('brightness'));
};
2017-09-19 10:23:03 -04:00
Blockly.FieldColourSlider.prototype.dispose = function() {
if (Blockly.FieldColourSlider.hueChangeEventKey_) {
goog.events.unlistenByKey(Blockly.FieldColourSlider.hueChangeEventKey_);
}
if (Blockly.FieldColourSlider.saturationChangeEventKey_) {
goog.events.unlistenByKey(Blockly.FieldColourSlider.saturationChangeEventKey_);
}
if (Blockly.FieldColourSlider.brightnessChangeEventKey_) {
goog.events.unlistenByKey(Blockly.FieldColourSlider.brightnessChangeEventKey_);
}
if (Blockly.FieldColourSlider.eyedropperEventData_) {
Blockly.unbindEvent_(Blockly.FieldColourSlider.eyedropperEventData_);
}
Blockly.Events.setGroup(false);
2017-09-19 10:23:03 -04:00
Blockly.FieldColourSlider.superClass_.dispose.call(this);
};
2018-04-17 14:15:18 -07:00
Blockly.Field.register('field_colour_slider', Blockly.FieldColourSlider);