mirror of
https://github.com/scratchfoundation/scratch-blocks.git
synced 2025-06-03 16:34:59 -04:00
850 lines
25 KiB
JavaScript
850 lines
25 KiB
JavaScript
/**
|
|
* @license
|
|
* Visual Blocks Editor
|
|
*
|
|
* Copyright 2018 Massachusetts Institute of Technology
|
|
* All rights reserved.
|
|
*
|
|
* 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 Note input field, for selecting a musical note on a piano.
|
|
* @author ericr@media.mit.edu (Eric Rosenbaum)
|
|
*/
|
|
'use strict';
|
|
|
|
goog.provide('Blockly.FieldNote');
|
|
|
|
goog.require('Blockly.DropDownDiv');
|
|
goog.require('Blockly.FieldTextInput');
|
|
goog.require('goog.math');
|
|
goog.require('goog.userAgent');
|
|
|
|
/**
|
|
* Class for a note input field, for selecting a musical note on a piano.
|
|
* @param {(string|number)=} opt_value The initial content of the field. The
|
|
* value should cast to a number, and if it does not, '0' will be used.
|
|
* @param {Function=} opt_validator An optional function that is called
|
|
* to validate any constraints on what the user entered. Takes the new
|
|
* text as an argument and returns the accepted text or null to abort
|
|
* the change.
|
|
* @extends {Blockly.FieldTextInput}
|
|
* @constructor
|
|
*/
|
|
Blockly.FieldNote = function(opt_value, opt_validator) {
|
|
opt_value = (opt_value && !isNaN(opt_value)) ? String(opt_value) : '0';
|
|
Blockly.FieldNote.superClass_.constructor.call(
|
|
this, opt_value, opt_validator);
|
|
this.addArgType('note');
|
|
|
|
/**
|
|
* Width of the field. Computed when drawing it, and used for animation.
|
|
* @type {number}
|
|
* @private
|
|
*/
|
|
this.fieldEditorWidth_ = 0;
|
|
|
|
/**
|
|
* Height of the field. Computed when drawing it.
|
|
* @type {number}
|
|
* @private
|
|
*/
|
|
this.fieldEditorHeight_ = 0;
|
|
|
|
/**
|
|
* The piano SVG.
|
|
* @type {SVGElement}
|
|
* @private
|
|
*/
|
|
this.pianoSVG_ = null;
|
|
|
|
/**
|
|
* Array of SVG elements representing the clickable piano keys.
|
|
* @type {!Array<SVGElement>}
|
|
* @private
|
|
*/
|
|
this.keySVGs_ = [];
|
|
|
|
/**
|
|
* Note name indicator at the top of the field.
|
|
* @type {SVGElement}
|
|
* @private
|
|
*/
|
|
this.noteNameText_ = null;
|
|
|
|
/**
|
|
* Note name indicator on the low C key.
|
|
* @type {SVGElement}
|
|
* @private
|
|
*/
|
|
this.lowCText_ = null;
|
|
|
|
/**
|
|
* Note name indicator on the low C key.
|
|
* @type {SVGElement}
|
|
* @private
|
|
*/
|
|
this.highCText_ = null;
|
|
|
|
/**
|
|
* Octave number of the currently displayed range of keys.
|
|
* @type {number}
|
|
* @private
|
|
*/
|
|
this.displayedOctave_ = null;
|
|
|
|
/**
|
|
* Current animation position of the piano SVG, as it shifts left or right to
|
|
* change octaves.
|
|
* @type {number}
|
|
* @private
|
|
*/
|
|
this.animationPos_ = 0;
|
|
|
|
/**
|
|
* Target position for the animation as the piano SVG shifts left or right.
|
|
* @type {number}
|
|
* @private
|
|
*/
|
|
this.animationTarget_ = 0;
|
|
|
|
/**
|
|
* A flag indicating that the mouse is currently down. Used in combination with
|
|
* mouse enter events to update the key selection while dragging.
|
|
* @type {boolean}
|
|
* @private
|
|
*/
|
|
this.mouseIsDown_ = false;
|
|
|
|
/**
|
|
* An array of wrappers for mouse down events on piano keys.
|
|
* @type {!Array.<!Array>}
|
|
* @private
|
|
*/
|
|
this.mouseDownWrappers_ = [];
|
|
|
|
/**
|
|
* A wrapper for the mouse up event.
|
|
* @type {!Array.<!Array>}
|
|
* @private
|
|
*/
|
|
this.mouseUpWrapper_ = null;
|
|
|
|
/**
|
|
* An array of wrappers for mouse enter events on piano keys.
|
|
* @type {!Array.<!Array>}
|
|
* @private
|
|
*/
|
|
this.mouseEnterWrappers_ = [];
|
|
|
|
/**
|
|
* A wrapper for the mouse down event on the octave down button.
|
|
* @type {!Array.<!Array>}
|
|
* @private
|
|
*/
|
|
this.octaveDownMouseDownWrapper_ = null;
|
|
|
|
/**
|
|
* A wrapper for the mouse down event on the octave up button.
|
|
* @type {!Array.<!Array>}
|
|
* @private
|
|
*/
|
|
this.octaveUpMouseDownWrapper_ = null;
|
|
};
|
|
goog.inherits(Blockly.FieldNote, Blockly.FieldTextInput);
|
|
|
|
/**
|
|
* Inset in pixels of content displayed in the field, caused by parent properties.
|
|
* The inset is actually determined by the CSS property blocklyDropDownDiv- it is
|
|
* the sum of the padding and border thickness.
|
|
*/
|
|
Blockly.FieldNote.INSET = 5;
|
|
|
|
/**
|
|
* Height of the top area of the field, in px.
|
|
* @type {number}
|
|
* @const
|
|
*/
|
|
Blockly.FieldNote.TOP_MENU_HEIGHT = 32 - Blockly.FieldNote.INSET;
|
|
|
|
/**
|
|
* Padding on the top and sides of the field, in px.
|
|
* @type {number}
|
|
* @const
|
|
*/
|
|
Blockly.FieldNote.EDGE_PADDING = 1;
|
|
|
|
/**
|
|
* Height of the drop shadow on the piano, in px.
|
|
* @type {number}
|
|
* @const
|
|
*/
|
|
Blockly.FieldNote.SHADOW_HEIGHT = 4;
|
|
|
|
/**
|
|
* Color for the shadow on the piano.
|
|
* @type {string}
|
|
* @const
|
|
*/
|
|
Blockly.FieldNote.SHADOW_COLOR = '#000';
|
|
|
|
/**
|
|
* Opacity for the shadow on the piano.
|
|
* @type {string}
|
|
* @const
|
|
*/
|
|
Blockly.FieldNote.SHADOW_OPACITY = .2;
|
|
|
|
/**
|
|
* A color for the white piano keys.
|
|
* @type {string}
|
|
* @const
|
|
*/
|
|
Blockly.FieldNote.WHITE_KEY_COLOR = '#FFFFFF';
|
|
|
|
/**
|
|
* A color for the black piano keys.
|
|
* @type {string}
|
|
* @const
|
|
*/
|
|
Blockly.FieldNote.BLACK_KEY_COLOR = '#323133';
|
|
|
|
/**
|
|
* A color for stroke around black piano keys.
|
|
* @type {string}
|
|
* @const
|
|
*/
|
|
Blockly.FieldNote.BLACK_KEY_STROKE = '#555555';
|
|
|
|
/**
|
|
* A color for the selected state of a piano key.
|
|
* @type {string}
|
|
* @const
|
|
*/
|
|
Blockly.FieldNote.KEY_SELECTED_COLOR = '#b0d6ff';
|
|
|
|
/**
|
|
* The number of white keys in one octave on the piano.
|
|
* @type {number}
|
|
* @const
|
|
*/
|
|
Blockly.FieldNote.NUM_WHITE_KEYS = 8;
|
|
|
|
/**
|
|
* Height of a white piano key, in px.
|
|
* @type {string}
|
|
* @const
|
|
*/
|
|
Blockly.FieldNote.WHITE_KEY_HEIGHT = 72;
|
|
|
|
/**
|
|
* Width of a white piano key, in px.
|
|
* @type {string}
|
|
* @const
|
|
*/
|
|
Blockly.FieldNote.WHITE_KEY_WIDTH = 40;
|
|
|
|
/**
|
|
* Height of a black piano key, in px.
|
|
* @type {string}
|
|
* @const
|
|
*/
|
|
Blockly.FieldNote.BLACK_KEY_HEIGHT = 40;
|
|
|
|
/**
|
|
* Width of a black piano key, in px.
|
|
* @type {string}
|
|
* @const
|
|
*/
|
|
Blockly.FieldNote.BLACK_KEY_WIDTH = 32;
|
|
|
|
/**
|
|
* Radius of the curved bottom corner of a piano key, in px.
|
|
* @type {string}
|
|
* @const
|
|
*/
|
|
Blockly.FieldNote.KEY_RADIUS = 6;
|
|
|
|
/**
|
|
* Bottom padding for the labels on C keys.
|
|
* @type {string}
|
|
* @const
|
|
*/
|
|
Blockly.FieldNote.KEY_LABEL_PADDING = 8;
|
|
|
|
/**
|
|
* An array of objects with data describing the keys on the piano.
|
|
* @type {Array.<{name: String, pitch: Number, isBlack: boolean}>}
|
|
* @const
|
|
*/
|
|
Blockly.FieldNote.KEY_INFO = [
|
|
{name: 'C', pitch: 0},
|
|
{name: 'C♯', pitch: 1, isBlack: true},
|
|
{name: 'D', pitch: 2},
|
|
{name: 'E♭', pitch: 3, isBlack: true},
|
|
{name: 'E', pitch: 4},
|
|
{name: 'F', pitch: 5},
|
|
{name: 'F♯', pitch: 6, isBlack: true},
|
|
{name: 'G', pitch: 7},
|
|
{name: 'G♯', pitch: 8, isBlack: true},
|
|
{name: 'A', pitch: 9},
|
|
{name: 'B♭', pitch: 10, isBlack: true},
|
|
{name: 'B', pitch: 11},
|
|
{name: 'C', pitch: 12}
|
|
];
|
|
|
|
/**
|
|
* The MIDI note number of the highest note selectable on the piano.
|
|
* @type {number}
|
|
* @const
|
|
*/
|
|
Blockly.FieldNote.MAX_NOTE = 130;
|
|
|
|
/**
|
|
* The fraction of the distance to the target location to move the piano at each
|
|
* step of the animation.
|
|
* @type {number}
|
|
* @const
|
|
*/
|
|
Blockly.FieldNote.ANIMATION_FRACTION = 0.2;
|
|
|
|
/**
|
|
* Path to the arrow svg icon, used on the octave buttons.
|
|
* @type {string}
|
|
* @const
|
|
*/
|
|
Blockly.FieldNote.ARROW_SVG_PATH = 'icons/arrow_button.svg';
|
|
|
|
/**
|
|
* The size of the square octave buttons.
|
|
* @type {number}
|
|
* @const
|
|
*/
|
|
Blockly.FieldNote.OCTAVE_BUTTON_SIZE = 32;
|
|
|
|
/**
|
|
* Construct a FieldNote from a JSON arg object.
|
|
* @param {!Object} options A JSON object with options.
|
|
* @returns {!Blockly.FieldNote} The new field instance.
|
|
* @package
|
|
* @nocollapse
|
|
*/
|
|
Blockly.FieldNote.fromJson = function(options) {
|
|
return new Blockly.FieldNote(options['note']);
|
|
};
|
|
|
|
/**
|
|
* Clean up this FieldNote, as well as the inherited FieldTextInput.
|
|
* @return {!Function} Closure to call on destruction of the WidgetDiv.
|
|
* @private
|
|
*/
|
|
Blockly.FieldNote.prototype.dispose_ = function() {
|
|
var thisField = this;
|
|
return function() {
|
|
Blockly.FieldNote.superClass_.dispose_.call(thisField)();
|
|
thisField.mouseDownWrappers_.forEach(function(wrapper) {
|
|
Blockly.unbindEvent_(wrapper);
|
|
});
|
|
thisField.mouseEnterWrappers_.forEach(function(wrapper) {
|
|
Blockly.unbindEvent_(wrapper);
|
|
});
|
|
if (thisField.mouseUpWrapper_) {
|
|
Blockly.unbindEvent_(thisField.mouseUpWrapper_);
|
|
}
|
|
if (thisField.octaveDownMouseDownWrapper_) {
|
|
Blockly.unbindEvent_(thisField.octaveDownMouseDownWrapper_);
|
|
}
|
|
if (thisField.octaveUpMouseDownWrapper_) {
|
|
Blockly.unbindEvent_(thisField.octaveUpMouseDownWrapper_);
|
|
}
|
|
this.pianoSVG_ = null;
|
|
this.keySVGs_.length = 0;
|
|
this.noteNameText_ = null;
|
|
this.lowCText_ = null;
|
|
this.highCText_ = null;
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Show a field with piano keys.
|
|
* @private
|
|
*/
|
|
Blockly.FieldNote.prototype.showEditor_ = function() {
|
|
// Mobile browsers have issues with in-line textareas (focus & keyboards).
|
|
Blockly.FieldNote.superClass_.showEditor_.call(this, this.useTouchInteraction_);
|
|
|
|
// If there is an existing drop-down someone else owns, hide it immediately and clear it.
|
|
Blockly.DropDownDiv.hideWithoutAnimation();
|
|
Blockly.DropDownDiv.clearContent();
|
|
|
|
// Build the SVG DOM.
|
|
var div = Blockly.DropDownDiv.getContentDiv();
|
|
|
|
this.fieldEditorWidth_ = Blockly.FieldNote.NUM_WHITE_KEYS * Blockly.FieldNote.WHITE_KEY_WIDTH +
|
|
Blockly.FieldNote.EDGE_PADDING;
|
|
this.fieldEditorHeight_ = Blockly.FieldNote.TOP_MENU_HEIGHT +
|
|
Blockly.FieldNote.WHITE_KEY_HEIGHT +
|
|
Blockly.FieldNote.EDGE_PADDING;
|
|
|
|
var svg = Blockly.utils.createSvgElement('svg', {
|
|
'xmlns': 'http://www.w3.org/2000/svg',
|
|
'xmlns:html': 'http://www.w3.org/1999/xhtml',
|
|
'xmlns:xlink': 'http://www.w3.org/1999/xlink',
|
|
'version': '1.1',
|
|
'height': this.fieldEditorHeight_ + 'px',
|
|
'width': this.fieldEditorWidth_ + 'px'
|
|
}, div);
|
|
|
|
// Add the white and black keys
|
|
// Since we are adding the keys from left to right in order, they need
|
|
// to be in two groups in order to layer correctly.
|
|
this.pianoSVG_ = Blockly.utils.createSvgElement('g', {}, svg);
|
|
var whiteKeyGroup = Blockly.utils.createSvgElement('g', {}, this.pianoSVG_);
|
|
var blackKeyGroup = Blockly.utils.createSvgElement('g', {}, this.pianoSVG_);
|
|
|
|
// Add three piano octaves, so we can animate moving up or down an octave.
|
|
// Only the middle octave gets bound to events.
|
|
this.keySVGs_ = [];
|
|
this.addPianoOctave_(-this.fieldEditorWidth_ + Blockly.FieldNote.EDGE_PADDING,
|
|
whiteKeyGroup, blackKeyGroup, null);
|
|
this.addPianoOctave_(0, whiteKeyGroup, blackKeyGroup, this.keySVGs_);
|
|
this.addPianoOctave_(this.fieldEditorWidth_ - Blockly.FieldNote.EDGE_PADDING,
|
|
whiteKeyGroup, blackKeyGroup, null);
|
|
|
|
// Note name indicator at the top of the field
|
|
this.noteNameText_ = Blockly.utils.createSvgElement('text',
|
|
{
|
|
'x': this.fieldEditorWidth_ / 2,
|
|
'y': Blockly.FieldNote.TOP_MENU_HEIGHT / 2,
|
|
'class': 'blocklyText',
|
|
'text-anchor': 'middle',
|
|
'dominant-baseline': 'middle',
|
|
}, svg);
|
|
|
|
// Note names on the low and high C keys
|
|
var lowCX = Blockly.FieldNote.WHITE_KEY_WIDTH / 2;
|
|
this.lowCText_ = this.addCKeyLabel_(lowCX, svg);
|
|
var highCX = lowCX + (Blockly.FieldNote.WHITE_KEY_WIDTH *
|
|
(Blockly.FieldNote.NUM_WHITE_KEYS - 1));
|
|
this.highCText_ = this.addCKeyLabel_(highCX, svg);
|
|
|
|
// Horizontal line at the top of the keys
|
|
Blockly.utils.createSvgElement('line',
|
|
{
|
|
'stroke': this.sourceBlock_.getColourTertiary(),
|
|
'x1': 0,
|
|
'y1': Blockly.FieldNote.TOP_MENU_HEIGHT,
|
|
'x2': this.fieldEditorWidth_,
|
|
'y2': Blockly.FieldNote.TOP_MENU_HEIGHT
|
|
}, svg);
|
|
|
|
// Drop shadow at the top of the keys
|
|
Blockly.utils.createSvgElement('rect',
|
|
{
|
|
'x': 0,
|
|
'y': Blockly.FieldNote.TOP_MENU_HEIGHT,
|
|
'width': this.fieldEditorWidth_,
|
|
'height': Blockly.FieldNote.SHADOW_HEIGHT,
|
|
'fill': Blockly.FieldNote.SHADOW_COLOR,
|
|
'fill-opacity': Blockly.FieldNote.SHADOW_OPACITY
|
|
}, svg);
|
|
|
|
// Octave buttons
|
|
this.octaveDownButton = this.addOctaveButton_(0, true, svg);
|
|
this.octaveUpButton = this.addOctaveButton_(
|
|
(this.fieldEditorWidth_ + Blockly.FieldNote.INSET * 2) -
|
|
Blockly.FieldNote.OCTAVE_BUTTON_SIZE, false, svg);
|
|
|
|
this.octaveDownMouseDownWrapper_ =
|
|
Blockly.bindEvent_(this.octaveDownButton, 'mousedown', this, function() {
|
|
this.changeOctaveBy_(-1);
|
|
});
|
|
this.octaveUpMouseDownWrapper_ =
|
|
Blockly.bindEvent_(this.octaveUpButton, 'mousedown', this,function() {
|
|
this.changeOctaveBy_(1);
|
|
});
|
|
Blockly.DropDownDiv.setColour(this.sourceBlock_.parentBlock_.getColour(),
|
|
this.sourceBlock_.getColourTertiary());
|
|
Blockly.DropDownDiv.setCategory(this.sourceBlock_.parentBlock_.getCategory());
|
|
Blockly.DropDownDiv.showPositionedByBlock(this, this.sourceBlock_);
|
|
|
|
this.updateSelection_();
|
|
};
|
|
|
|
/**
|
|
* Add one octave of piano keys drawn using SVG.
|
|
* @param {number} x The x position of the left edge of this octave of keys.
|
|
* @param {SVGElement} whiteKeyGroup The group for all white piano keys.
|
|
* @param {SvgElement} blackKeyGroup The group for all black piano keys.
|
|
* @param {!Array.<SvgElement>} keySVGarray An array containing all the key SVGs.
|
|
* @private
|
|
*/
|
|
Blockly.FieldNote.prototype.addPianoOctave_ = function(x, whiteKeyGroup, blackKeyGroup, keySVGarray) {
|
|
var xIncrement, width, height, fill, stroke, group;
|
|
x += Blockly.FieldNote.EDGE_PADDING / 2;
|
|
var y = Blockly.FieldNote.TOP_MENU_HEIGHT;
|
|
for (var i = 0; i < Blockly.FieldNote.KEY_INFO.length; i++) {
|
|
// Draw a black or white key
|
|
if (Blockly.FieldNote.KEY_INFO[i].isBlack) {
|
|
// Black keys are shifted back half a key
|
|
x -= Blockly.FieldNote.BLACK_KEY_WIDTH / 2;
|
|
xIncrement = Blockly.FieldNote.BLACK_KEY_WIDTH / 2;
|
|
width = Blockly.FieldNote.BLACK_KEY_WIDTH;
|
|
height = Blockly.FieldNote.BLACK_KEY_HEIGHT;
|
|
fill = Blockly.FieldNote.BLACK_KEY_COLOR;
|
|
stroke = Blockly.FieldNote.BLACK_KEY_STROKE;
|
|
group = blackKeyGroup;
|
|
} else {
|
|
xIncrement = Blockly.FieldNote.WHITE_KEY_WIDTH;
|
|
width = Blockly.FieldNote.WHITE_KEY_WIDTH;
|
|
height = Blockly.FieldNote.WHITE_KEY_HEIGHT;
|
|
fill = Blockly.FieldNote.WHITE_KEY_COLOR;
|
|
stroke = this.sourceBlock_.getColourTertiary();
|
|
group = whiteKeyGroup;
|
|
}
|
|
var attr = {
|
|
'd': this.getPianoKeyPath_(x, y, width, height),
|
|
'fill': fill,
|
|
'stroke': stroke
|
|
};
|
|
x += xIncrement;
|
|
|
|
var keySVG = Blockly.utils.createSvgElement('path', attr, group);
|
|
|
|
if (keySVGarray) {
|
|
keySVGarray[i] = keySVG;
|
|
keySVG.setAttribute('data-pitch', Blockly.FieldNote.KEY_INFO[i].pitch);
|
|
keySVG.setAttribute('data-name', Blockly.FieldNote.KEY_INFO[i].name);
|
|
keySVG.setAttribute('data-isBlack', Blockly.FieldNote.KEY_INFO[i].isBlack);
|
|
|
|
this.mouseDownWrappers_[i] =
|
|
Blockly.bindEvent_(keySVG, 'mousedown', this, this.onMouseDownOnKey_);
|
|
this.mouseEnterWrappers_[i] =
|
|
Blockly.bindEvent_(keySVG, 'mouseenter', this, this.onMouseEnter_);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Construct the SVG path string for a piano key shape: a rectangle with rounded
|
|
* corners at the bottom.
|
|
* @param {number} x the x position for the key.
|
|
* @param {number} y the y position for the key.
|
|
* @param {number} width the width of the key.
|
|
* @param {number} height the height of the key.
|
|
* @returns {string} the SVG path as a string.
|
|
* @private
|
|
*/
|
|
Blockly.FieldNote.prototype.getPianoKeyPath_ = function(x, y, width, height) {
|
|
return 'M' + x + ' ' + y + ' ' +
|
|
'L' + x + ' ' + (y + height - Blockly.FieldNote.KEY_RADIUS) + ' ' +
|
|
'Q' + x + ' ' + (y + height) + ' ' +
|
|
(x + Blockly.FieldNote.KEY_RADIUS) + ' ' + (y + height) + ' ' +
|
|
'L' + (x + width - Blockly.FieldNote.KEY_RADIUS) + ' ' + (y + height) + ' ' +
|
|
'Q' + (x + width) + ' ' + (y + height) + ' ' +
|
|
(x + width) + ' ' + (y + height - Blockly.FieldNote.KEY_RADIUS) + ' ' +
|
|
'L' + (x + width) + ' ' + y + ' ' +
|
|
'L' + x + ' ' + y;
|
|
};
|
|
|
|
/**
|
|
* Add a button for switching the displayed octave of the piano up or down.
|
|
* @param {number} x The x position of the button.
|
|
* @param {boolean} flipped If true, the icon should be flipped.
|
|
* @param {SvgElement} svg The svg element to add the buttons to.
|
|
* @returns {SvgElement} A group containing the button SVG elements.
|
|
* @private
|
|
*/
|
|
Blockly.FieldNote.prototype.addOctaveButton_ = function(x, flipped, svg) {
|
|
var group = Blockly.utils.createSvgElement('g', {}, svg);
|
|
var imageSize = Blockly.FieldNote.OCTAVE_BUTTON_SIZE;
|
|
var arrow = Blockly.utils.createSvgElement('image',
|
|
{
|
|
'width': imageSize,
|
|
'height': imageSize,
|
|
'x': x - Blockly.FieldNote.INSET,
|
|
'y': -1 * Blockly.FieldNote.INSET
|
|
}, group);
|
|
arrow.setAttributeNS(
|
|
'http://www.w3.org/1999/xlink',
|
|
'xlink:href',
|
|
Blockly.mainWorkspace.options.pathToMedia + Blockly.FieldNote.ARROW_SVG_PATH
|
|
);
|
|
Blockly.utils.createSvgElement('line',
|
|
{
|
|
'stroke': this.sourceBlock_.getColourTertiary(),
|
|
'x1': x - Blockly.FieldNote.INSET,
|
|
'y1': 0,
|
|
'x2': x - Blockly.FieldNote.INSET,
|
|
'y2': Blockly.FieldNote.TOP_MENU_HEIGHT - Blockly.FieldNote.INSET
|
|
}, group);
|
|
if (flipped) {
|
|
var translateX = -1 * Blockly.FieldNote.OCTAVE_BUTTON_SIZE + (Blockly.FieldNote.INSET * 2);
|
|
group.setAttribute('transform', 'scale(-1, 1) ' +
|
|
'translate(' + translateX + ', 0)');
|
|
}
|
|
return group;
|
|
};
|
|
|
|
/**
|
|
* Add an SVG text label for display on the C keys of the piano.
|
|
* @param {number} x The x position for the label.
|
|
* @param {SvgElement} svg The SVG element to add the label to.
|
|
* @returns {SvgElement} The SVG element containing the label.
|
|
* @private
|
|
*/
|
|
Blockly.FieldNote.prototype.addCKeyLabel_ = function(x, svg) {
|
|
return Blockly.utils.createSvgElement('text',
|
|
{
|
|
'x': x,
|
|
'y': Blockly.FieldNote.TOP_MENU_HEIGHT + Blockly.FieldNote.WHITE_KEY_HEIGHT -
|
|
Blockly.FieldNote.KEY_LABEL_PADDING,
|
|
'class': 'scratchNotePickerKeyLabel',
|
|
'text-anchor': 'middle'
|
|
}, svg);
|
|
};
|
|
|
|
/**
|
|
* Set the visibility of the C key labels.
|
|
* @param {boolean} visible If true, set labels to be visible.
|
|
* @private
|
|
*/
|
|
Blockly.FieldNote.prototype.setCKeyLabelsVisible_ = function(visible) {
|
|
if (visible) {
|
|
this.fadeSvgToOpacity_(this.lowCText_, 1);
|
|
this.fadeSvgToOpacity_(this.highCText_, 1);
|
|
} else {
|
|
this.fadeSvgToOpacity_(this.lowCText_, 0);
|
|
this.fadeSvgToOpacity_(this.highCText_, 0);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Animate an SVG to fade it in or out to a target opacity.
|
|
* @param {SvgElement} svg The SVG element to apply the fade to.
|
|
* @param {number} opacity The target opacity.
|
|
* @private
|
|
*/
|
|
Blockly.FieldNote.prototype.fadeSvgToOpacity_ = function(svg, opacity) {
|
|
svg.setAttribute('style', 'opacity: ' + opacity + '; transition: opacity 0.1s;');
|
|
};
|
|
|
|
/**
|
|
* Handle the mouse down event on a piano key.
|
|
* @param {!Event} e Mouse down event.
|
|
* @private
|
|
*/
|
|
Blockly.FieldNote.prototype.onMouseDownOnKey_ = function(e) {
|
|
this.mouseIsDown_ = true;
|
|
this.mouseUpWrapper_ = Blockly.bindEvent_(document.body, 'mouseup', this, this.onMouseUp_);
|
|
this.selectNoteWithMouseEvent_(e);
|
|
};
|
|
|
|
/**
|
|
* Handle the mouse up event following a mouse down on a piano key.
|
|
* @private
|
|
*/
|
|
Blockly.FieldNote.prototype.onMouseUp_ = function() {
|
|
this.mouseIsDown_ = false;
|
|
Blockly.unbindEvent_(this.mouseUpWrapper_);
|
|
};
|
|
|
|
/**
|
|
* Handle the event when the mouse enters a piano key.
|
|
* @param {!Event} e Mouse enter event.
|
|
* @private
|
|
*/
|
|
Blockly.FieldNote.prototype.onMouseEnter_ = function(e) {
|
|
if (this.mouseIsDown_) {
|
|
this.selectNoteWithMouseEvent_(e);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Use the data in a mouse event to select a new note, and play it.
|
|
* @param {!Event} e Mouse event.
|
|
* @private
|
|
*/
|
|
Blockly.FieldNote.prototype.selectNoteWithMouseEvent_ = function(e) {
|
|
var newNoteNum = Number(e.target.getAttribute('data-pitch')) + this.displayedOctave_ * 12;
|
|
this.setNoteNum_(newNoteNum);
|
|
this.playNoteInternal_();
|
|
};
|
|
|
|
/**
|
|
* Play a note, by calling the externally overriden play note function.
|
|
* @private
|
|
*/
|
|
Blockly.FieldNote.prototype.playNoteInternal_ = function() {
|
|
if (Blockly.FieldNote.playNote_) {
|
|
Blockly.FieldNote.playNote_(
|
|
this.getValue(),
|
|
this.sourceBlock_.parentBlock_.getCategory()
|
|
);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Function to play a musical note corresponding to the key selected.
|
|
* Overridden externally.
|
|
* @param {number} noteNum the MIDI note number to play.
|
|
* @param {string} id An id to select a scratch extension to play the note.
|
|
* @private
|
|
*/
|
|
Blockly.FieldNote.playNote_ = function(/* noteNum, id*/) {
|
|
return;
|
|
};
|
|
|
|
/**
|
|
* Change the selected note by a number of octaves, and start the animation.
|
|
* @param {number} octaves The number of octaves to change by.
|
|
* @private
|
|
*/
|
|
Blockly.FieldNote.prototype.changeOctaveBy_ = function(octaves) {
|
|
this.displayedOctave_ += octaves;
|
|
if (this.displayedOctave_ < 0) {
|
|
this.displayedOctave_ = 0;
|
|
return;
|
|
}
|
|
var maxOctave = Math.floor(Blockly.FieldNote.MAX_NOTE / 12);
|
|
if (this.displayedOctave_ > maxOctave) {
|
|
this.displayedOctave_ = maxOctave;
|
|
return;
|
|
}
|
|
|
|
var newNote = Number(this.getText()) + (octaves * 12);
|
|
this.setNoteNum_(newNote);
|
|
|
|
this.animationTarget_ = this.fieldEditorWidth_ * octaves * -1;
|
|
this.animationPos_ = 0;
|
|
this.stepOctaveAnimation_();
|
|
this.setCKeyLabelsVisible_(false);
|
|
};
|
|
|
|
/**
|
|
* Animate the piano up or down an octave by sliding it to the left or right.
|
|
* @private
|
|
*/
|
|
Blockly.FieldNote.prototype.stepOctaveAnimation_ = function() {
|
|
var absDiff = Math.abs(this.animationPos_ - this.animationTarget_);
|
|
if (absDiff < 1) {
|
|
this.pianoSVG_.setAttribute('transform', 'translate(0, 0)');
|
|
this.setCKeyLabelsVisible_(true);
|
|
this.playNoteInternal_();
|
|
return;
|
|
}
|
|
this.animationPos_ += (this.animationTarget_ - this.animationPos_) *
|
|
Blockly.FieldNote.ANIMATION_FRACTION;
|
|
this.pianoSVG_.setAttribute('transform', 'translate(' + this.animationPos_ + ',0)');
|
|
requestAnimationFrame(this.stepOctaveAnimation_.bind(this));
|
|
};
|
|
|
|
/**
|
|
* Set the selected note number, and update the piano display and the input field.
|
|
* @param {number} noteNum The MIDI note number to select.
|
|
* @private
|
|
*/
|
|
Blockly.FieldNote.prototype.setNoteNum_ = function(noteNum) {
|
|
noteNum = this.callValidator(noteNum);
|
|
this.setValue(noteNum);
|
|
Blockly.FieldTextInput.htmlInput_.value = noteNum;
|
|
};
|
|
|
|
/**
|
|
* Sets the text in this field. Triggers a rerender of the source block, and
|
|
* updates the selection on the field.
|
|
* @param {?string} text New text.
|
|
*/
|
|
Blockly.FieldNote.prototype.setText = function(text) {
|
|
Blockly.FieldNote.superClass_.setText.call(this, text);
|
|
if (!this.textElement_) {
|
|
// Not rendered yet.
|
|
return;
|
|
}
|
|
this.updateSelection_();
|
|
// Cached width is obsolete. Clear it.
|
|
this.size_.width = 0;
|
|
};
|
|
|
|
/**
|
|
* For a MIDI note number, find the index of the corresponding piano key.
|
|
* @param {number} noteNum The note number.
|
|
* @returns {number} The index of the piano key.
|
|
* @private
|
|
*/
|
|
Blockly.FieldNote.prototype.noteNumToKeyIndex_ = function(noteNum) {
|
|
return Math.floor(noteNum) - (this.displayedOctave_ * 12);
|
|
};
|
|
|
|
/**
|
|
* Update the selected note and labels on the field.
|
|
* @private
|
|
*/
|
|
Blockly.FieldNote.prototype.updateSelection_ = function() {
|
|
var noteNum = Number(this.getText());
|
|
|
|
// If the note is outside the currently displayed octave, update it
|
|
if (this.displayedOctave_ == null ||
|
|
noteNum > ((this.displayedOctave_ * 12) + 12) ||
|
|
noteNum < (this.displayedOctave_ * 12)) {
|
|
this.displayedOctave_ = Math.floor(noteNum / 12);
|
|
}
|
|
|
|
var index = this.noteNumToKeyIndex_(noteNum);
|
|
|
|
// Clear the highlight on all keys
|
|
this.keySVGs_.forEach(function(svg) {
|
|
var isBlack = svg.getAttribute('data-isBlack');
|
|
if (isBlack === 'true') {
|
|
svg.setAttribute('fill', Blockly.FieldNote.BLACK_KEY_COLOR);
|
|
} else {
|
|
svg.setAttribute('fill', Blockly.FieldNote.WHITE_KEY_COLOR);
|
|
}
|
|
});
|
|
// Set the highlight on the selected key
|
|
if (this.keySVGs_[index]) {
|
|
this.keySVGs_[index].setAttribute('fill', Blockly.FieldNote.KEY_SELECTED_COLOR);
|
|
// Update the note name text
|
|
var noteName = Blockly.FieldNote.KEY_INFO[index].name;
|
|
this.noteNameText_.textContent = noteName + ' (' + Math.floor(noteNum) + ')';
|
|
// Update the low and high C note names
|
|
var lowCNum = this.displayedOctave_ * 12;
|
|
this.lowCText_.textContent = 'C(' + lowCNum + ')';
|
|
this.highCText_.textContent = 'C(' + (lowCNum + 12) + ')';
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Ensure that only a valid MIDI note number may be entered.
|
|
* @param {string} text The user's text.
|
|
* @return {?string} A string representing a valid note number, or null if invalid.
|
|
*/
|
|
Blockly.FieldNote.prototype.classValidator = function(text) {
|
|
if (text === null) {
|
|
return null;
|
|
}
|
|
var n = parseFloat(text || 0);
|
|
if (isNaN(n)) {
|
|
return null;
|
|
}
|
|
if (n < 0) {
|
|
n = 0;
|
|
}
|
|
if (n > Blockly.FieldNote.MAX_NOTE) {
|
|
n = Blockly.FieldNote.MAX_NOTE;
|
|
}
|
|
return String(n);
|
|
};
|
|
|
|
Blockly.Field.register('field_note', Blockly.FieldNote);
|