本文主要是介绍Scratch Blocks自定义组件之「旋律播放」,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
一、背景
看到microbit edit有旋律编辑器,就在scratch块中也写了一个,如下图所示
二、功能配置说明
支持8个音符8拍旋律控制
三、使用说明
(1)引入添加field_tone.js到core文件夹中,代码在下文
(2)添加css样式到core/css.js中,代码在下文
(3)封装音频播放组件Blockly.AudioContext,主要实现音频播放API,基于Web AudioContext实现音频播放,在core/audio_context.js,代码在下文
(4)将field_tone注册到Blockly中,这样在任意地方都可以使用,如果不想注入,直接用script标签引入也行,在core/blockly.js写如下代码:
goog.require('Blockly.FieldTone');
(5)block定义代码如下,下面代码是直接集成到一个完整block中
// ZE3P蜂鸣器 旋律播放
Blockly.Blocks['ZE3P_play_melody'] = {init: function () {this.jsonInit({"message0": "%1%2","args0": [{"type": "field_image","src": Blockly.mainWorkspace.options.pathToMedia + "/extensions/ZE3P.png","width": 24,"height": 24},{"type": "field_vertical_separator"}],"message1": "播放旋律%1","args1": [{"type": "field_tone","name": "TONE",}],"category": Blockly.Categories.sounds,"extensions": ["colours_sounds", "shape_statement"]});}
}
(6)添加toolbox配置,代码如下:
<block type="ZE3P_play_melody" id="ZE3P_play_melody"></block>
(7)转码实现以python为例,块生成的value格式为:字符串"C5 - - G F E D C 200",以空格为分隔符,前八个为音符,每一个代表特定的频率,其中'-'为休止符,最后一个数字为BPM,和持续时间的关系为 time = 60000 / BPM,代码如下:
Blockly.Python['ZE3P_play_melody'] = function (block) {const toneString = block.getFieldValue('TONE') || "";const melody = toneString.split(" ");const time = parseInt(melody.pop());console.log(melody, time)// 根据实际代码而定return `music.play_melody("${melody.join(" ")}", ${time})\n`;
};
提示:如果采用注册方法,最好本地编译一下,使用javascript引入则不需要
四、效果展示
五、完整代码
完整field_tone.js代码如下:
'use strict';goog.provide('Blockly.FieldTone');goog.require('Blockly.DropDownDiv');
goog.require('Blockly.AudioContext');/*** 构造器* @param music* @constructor*/
Blockly.FieldTone = function (music) {music = "C5 B A G F E D C 200";// 初始化this.tones_ = [];Blockly.FieldTone.superClass_.constructor.call(this, music);this.addArgType('music');// 缩略图节点按钮this.buttonThumbNodes_ = [];// 编辑节点按钮this.buttonNodes_ = [];// 播放状态this.isPlaying = false;// 播放定时器this.playTimer = null;// 事件句柄this.bpmWrapper_ = null;this.toneWrapper_ = null;this.playWrapper_ = null;this.finishWrapper_ = null;
};
goog.inherits(Blockly.FieldTone, Blockly.Field);/*** Json解析*/
Blockly.FieldTone.fromJson = function (options) {return new Blockly.FieldTone(options['music']);
};/*** 缩略图节点宽度* @type {number}* @const*/
Blockly.FieldTone.THUMBNAIL_NODE_SIZE = 10;/*** 缩略图节点圆角* @type {number}* @const*/
Blockly.FieldTone.THUMBNAIL_NODE_ROUND = 2;/*** 缩略图节点之间间距* @type {number}* @const*/
Blockly.FieldTone.THUMBNAIL_NODE_GAP = 3;/*** 缩略图节点父块顶部距离* @type {number}* @const*/
Blockly.FieldTone.THUMBNAIL_NODE_TOP = 5;/*** 节点宽度* @type {number}* @const*/
Blockly.FieldTone.EDIT_NODE_SIZE = 20;/*** 节点圆角* @type {number}* @const*/
Blockly.FieldTone.EDIT_NODE_ROUND = 3;/*** 节点之间间距* @type {number}* @const*/
Blockly.FieldTone.EDIT_NODE_GAP = 6;/*** 编辑音频面板内边距* @type {number}* @const*/
Blockly.FieldTone.EDIT_DROPDOWN_PAD = 10;/*** 节拍个数* @type {number}* @const*/
Blockly.FieldTone.NODE_SIZE = 8;/*** 最大节拍* @type {number}* @const*/
Blockly.FieldTone.MAX_BPM = 1000;/*** 最大节拍* @type {number}* @const*/
Blockly.FieldTone.MIN_BPM = 1;/*** 默认值* @type {string}*/
Blockly.FieldTone.DEFAULT_NOTE = "- - - - - - - - " + Blockly.FieldTone.MIN_BPM;/*** 初始化*/
Blockly.FieldTone.prototype.init = function () {if (this.fieldGroup_) {return;}// 重建DOMthis.fieldGroup_ = Blockly.utils.createSvgElement('g', {}, null);this.size_.width = Blockly.FieldTone.THUMBNAIL_NODE_SIZE * 11 +Blockly.FieldTone.THUMBNAIL_NODE_GAP * 7 + 24;this.sourceBlock_.getSvgRoot().appendChild(this.fieldGroup_);if (!this.sourceBlock_.isShadow()) {this.box_ = Blockly.utils.createSvgElement('rect', {'rx': Blockly.BlockSvg.NUMBER_FIELD_CORNER_RADIUS,'ry': Blockly.BlockSvg.NUMBER_FIELD_CORNER_RADIUS,'x': 0,'y': 0,'width': this.size_.width,'height': this.size_.height,'stroke': this.sourceBlock_.getColourTertiary(),'fill': this.sourceBlock_.getColour(),'class': 'blocklyBlockBackground','fill-opacity': 1}, null);this.fieldGroup_.insertBefore(this.box_, this.textElement_);}const nodeSize = Blockly.FieldTone.THUMBNAIL_NODE_SIZE;const nodeGap = Blockly.FieldTone.THUMBNAIL_NODE_GAP;const nodeHeight = this.size_.height - Blockly.FieldTone.THUMBNAIL_NODE_TOP * 2;const nodeRound = Blockly.FieldTone.THUMBNAIL_NODE_ROUND;// 创建图标this.iconElement_ = Blockly.utils.createSvgElement('image', {'height': '24px', 'width': '24px','x': nodeSize, 'y': "4px",}, this.fieldGroup_);this.iconElement_.setAttributeNS('http://www.w3.org/1999/xlink','xlink:href', Blockly.mainWorkspace.options.pathToMedia + '/extensions/music-block-icon.svg');// 创建缩略按键节点for (let i = 0; i < Blockly.FieldTone.NODE_SIZE; i++) {const x = nodeSize * 1.5 + (nodeSize + nodeGap) * i + 24;const attr = {'x': x, 'y': Blockly.FieldTone.THUMBNAIL_NODE_TOP,'width': nodeSize, 'height': nodeHeight,'rx': nodeRound, 'ry': nodeRound,'fill': '#dcdcdc','stroke': '#f0f0f0'};this.buttonThumbNodes_.push(Blockly.utils.createSvgElement('rect', attr, this.fieldGroup_));}this.updateTone_();this.mouseDownWrapper_ = Blockly.bindEventWithChecks_(this.getClickTarget_(), 'mousedown', this, this.onMouseDown_);
};/*** 设置值*/
Blockly.FieldTone.prototype.setValue = function (newValue) {// 解析值newValue = this.parseValue(newValue);// 无改变if (newValue === null || newValue === this.value_) {return;}if (this.sourceBlock_ && Blockly.Events.isEnabled()) {Blockly.Events.fire(new Blockly.Events.Change(this.sourceBlock_, 'field', this.name, this.value_, newValue));}// 更新值this.value_ = newValue;this.tones_ = this.valueToTones(newValue);// 更新缩略图this.updateTone_();
};/*** 获取值*/
Blockly.FieldTone.prototype.getValue = function () {return this.value_;
};/*** 更新* @private*/
Blockly.FieldTone.prototype.updateTone_ = function () {if (!this.buttonThumbNodes_) {return;}for (let i = 0; i < this.buttonThumbNodes_.length; i++) {// 更新缩略this.buttonThumbNodes_[i].setAttribute("fill", this.getToneColor(this.tones_[i]));// 更新按钮if (this.buttonNodes_ && this.buttonNodes_.length === 64) {for (let j = 0; j < Blockly.FieldTone.NODE_SIZE; j++) {const color = (this.getToneIndex(this.tones_[i]) === j) ?this.getToneColor(this.tones_[i]) : '#dcdcdc';this.buttonNodes_[j * 8 + i].setAttribute("fill", color);}}}
}/*** 解析值* @param value*/
Blockly.FieldTone.prototype.parseValue = function (value) {if (!value) {return Blockly.FieldTone.DEFAULT_NOTE;}// 删除边界空白value = value.trim();// 分割提取值let tones = value.split(" ");if (tones.length !== 9) {return Blockly.FieldTone.DEFAULT_NOTE;}// 异常音符值for (let i = 0; i < 8; i++) {if (!this.isValidNote(tones[i])) {return Blockly.FieldTone.DEFAULT_NOTE;}}// 异常节拍const bpm = parseInt(tones[8]);if (isNaN(bpm) || bpm > Blockly.FieldTone.MAX_BPM || bpm < Blockly.FieldTone.MIN_BPM) {return Blockly.FieldTone.DEFAULT_NOTE;}return value;
}/*** 数据值转换位音符值* @param value*/
Blockly.FieldTone.prototype.valueToTones = function (value) {value = this.parseValue(value);let data = value.split(" ");const tones = [];for (let i = 0; i < 8; i++) {tones.push(data[i]);}tones.push(parseInt(data[8]));return tones;
}/*** 数据值转换位音符值* @param tones*/
Blockly.FieldTone.prototype.tonesToValue = function (tones = []) {if (tones.length !== 9) {return Blockly.FieldTone.DEFAULT_NOTE;}return this.tones_.join(" ");
}/*** 判断音符是否有效* @param tone* @returns {boolean}*/
Blockly.FieldTone.prototype.isValidNote = function (tone) {const tones = ["C", "D", "E", "F", "G", "A", "B", "C5", "-"];return tones.includes(tone);
}/*** 获取音符颜色* @param tone* @returns {string}*/
Blockly.FieldTone.prototype.getToneColor = function (tone) {const toneColors = {"C": "#A80000","D": "#D83B01","E": "#FFB900","F": "#107C10","G": "#008272","A": "#bb0dd5","B": "#5C2D91","C5": "#f88fe9",}return toneColors[tone] || "#DCDCDC";
}/*** 获取音符值* @param index* @returns {Number}*/
Blockly.FieldTone.prototype.getToneValue = function (index) {const tones = ["C", "D", "E", "F", "G", "A", "B", "C5"];if (index >= 0 && index < 8) {return tones[index];}return "-";
}/*** 获取颜色下标* @param tone* @returns {Number}*/
Blockly.FieldTone.prototype.getToneIndex = function (tone) {const tones = ["C", "D", "E", "F", "G", "A", "B", "C5"];return tones.indexOf(tone);
}/*** 选择音符* @param index* @returns {number}*/
Blockly.FieldTone.prototype.getToneCore = function (index) {const toneValues = [523, 494, 440, 392, 349, 330, 294, 262];if (index >= 0 && index < 8) {return toneValues[index];}return 0;
}/*** 获取播放时长* @returns {number}*/
Blockly.FieldTone.prototype.getDuration = function () {return 60000 / parseInt(this.tones_[8]);
}/*** 显示下拉*/
Blockly.FieldTone.prototype.showEditor_ = function () {Blockly.DropDownDiv.hideWithoutAnimation();Blockly.DropDownDiv.clearContent();// 创建下拉面板内容const contentDiv = Blockly.DropDownDiv.getContentDiv();const nodeSize = Blockly.FieldTone.NODE_SIZE;const toneSize = Blockly.FieldTone.EDIT_NODE_SIZE * nodeSize +Blockly.FieldTone.EDIT_NODE_GAP * (nodeSize - 1) +Blockly.FieldTone.EDIT_DROPDOWN_PAD * 2;this.toneStage_ = Blockly.utils.createSvgElement('svg', {'xmlns': 'http://www.w3.org/2000/svg','height': toneSize + 'px','width': toneSize + 'px'}, contentDiv);// 清空面板按钮this.buttonNodes_ = [];// 创建音符按钮for (let i = 0; i < nodeSize; i++) {for (let j = 0; j < nodeSize; j++) {// 计算按钮位置const x = (Blockly.FieldTone.EDIT_NODE_SIZE * j) +(Blockly.FieldTone.EDIT_NODE_GAP * j) +Blockly.FieldTone.EDIT_DROPDOWN_PAD;const y = (Blockly.FieldTone.EDIT_NODE_SIZE * i) +(Blockly.FieldTone.EDIT_NODE_GAP * i) +Blockly.FieldTone.EDIT_DROPDOWN_PAD;// 设置显示颜色const color = (this.getToneIndex(this.tones_[j]) === i) ?this.getToneColor(this.tones_[j]) : '#dcdcdc';const attr = {'x': x, 'y': y,'width': Blockly.FieldTone.EDIT_NODE_SIZE,'height': Blockly.FieldTone.EDIT_NODE_SIZE,'rx': Blockly.FieldTone.EDIT_NODE_ROUND,'ry': Blockly.FieldTone.EDIT_NODE_ROUND,'fill': color, 'stroke': 'white',"cursor": "pointer"};const tone = Blockly.utils.createSvgElement('rect', attr, this.toneStage_);this.toneStage_.appendChild(tone);this.buttonNodes_.push(tone);}}// 工具栏const toolDiv = document.createElement('div');toolDiv.className = "FieldToneTool";contentDiv.appendChild(toolDiv);// 左侧布局const leftDiv = document.createElement('div');toolDiv.appendChild(leftDiv);this.bpmInput = document.createElement('input');this.bpmInput.className = "blocklyHtmlInput";this.bpmInput.value = this.tones_[8];leftDiv.appendChild(this.bpmInput);const bpmSpan = document.createElement('span');bpmSpan.innerText = "BPM";leftDiv.appendChild(bpmSpan);// 按钮组const rightDiv = document.createElement('div');toolDiv.appendChild(rightDiv);this.playButton = document.createElement('button');this.playButton.innerText = "播放";rightDiv.appendChild(this.playButton);const finishButton = document.createElement('button');finishButton.innerText = "完成"rightDiv.appendChild(finishButton);// 计算下拉面板位置const scale = this.sourceBlock_.workspace.scale;let bBox = {width: this.size_.width, height: this.size_.height};bBox.width *= scale;bBox.height *= scale;const position = this.fieldGroup_.getBoundingClientRect();const primaryX = position.left + bBox.width / 2;const primaryY = position.top + bBox.height;const secondaryX = primaryX;const secondaryY = position.top;// 设置颜色Blockly.DropDownDiv.setBoundsElement(this.sourceBlock_.workspace.getParentSvg().parentNode);Blockly.DropDownDiv.show(this, primaryX, primaryY, secondaryX, secondaryY, this.onHide_.bind(this));const primaryColour = (this.sourceBlock_.isShadow()) ?this.sourceBlock_.parentBlock_.getColour() : this.sourceBlock_.getColour();Blockly.DropDownDiv.setColour(primaryColour, this.sourceBlock_.getColourTertiary());const category = (this.sourceBlock_.isShadow()) ?this.sourceBlock_.parentBlock_.getCategory() : this.sourceBlock_.getCategory();Blockly.DropDownDiv.setCategory(category);// 事件处理this.bpmWrapper_ = Blockly.bindEvent_(this.bpmInput, 'input', this, this.onBpmChange);this.toneWrapper_ = Blockly.bindEvent_(this.toneStage_, 'mousedown', this, this.onNoteClick);this.playWrapper_ = Blockly.bindEvent_(this.playButton, 'click', this, this.onPlayClick);this.finishWrapper_ = Blockly.bindEvent_(finishButton, 'click', this, this.onFinishClick);
};/*** 校验是否点击音符* @param e* @returns {number}* @private*/
Blockly.FieldTone.prototype.checkForNote_ = function (e) {const bBox = this.toneStage_.getBoundingClientRect();const size = Blockly.FieldTone.NODE_SIZE;const nodeSize = Blockly.FieldTone.EDIT_NODE_SIZE;const nodePad = Blockly.FieldTone.EDIT_NODE_GAP;const dx = e.clientX - bBox.left;const dy = e.clientY - bBox.top;for (let i = 0; i < size; i++) {for (let j = 0; j < size; j++) {const item = this.buttonNodes_[i * size + j];const x = parseInt(item.getAttribute("x"));const y = parseInt(item.getAttribute("y"));if (x <= dx && dx < x + nodeSize && y <= dy && dy < y + nodeSize) {return i * size + j;}}}return -1;
};/*** 节拍输入事件*/
Blockly.FieldTone.prototype.onBpmChange = function () {this.stopMelody_();let value = this.bpmInput.value;value = Number(parseInt(value));if (isNaN(value) || value <= Blockly.FieldTone.MIN_BPM) {value = Blockly.FieldTone.MIN_BPM;} else if (value >= Blockly.FieldTone.MAX_BPM) {value = Blockly.FieldTone.MAX_BPM;}this.tones_[8] = value;this.bpmInput.value = value;// 更新值this.setValue(this.tonesToValue(this.tones_));
}/*** 点击音符按钮*/
Blockly.FieldTone.prototype.onNoteClick = function (e) {const index = this.checkForNote_(e);if (index >= this.buttonNodes_.length || index === -1)return;const row = Math.trunc(index / Blockly.FieldTone.NODE_SIZE);const col = index % Blockly.FieldTone.NODE_SIZE;const tone = this.getToneValue(row);if (this.tones_[col] === tone) {this.tones_[col] = "-";} else {this.tones_[col] = this.getToneValue(row);// 播放点击音符this.playTone_(this.getToneCore(row));}// 更新值this.setValue(this.tonesToValue(this.tones_));
}/*** 播放单个音符* @param toneValue*/
Blockly.FieldTone.prototype.playTone_ = function (toneValue) {this.stopMelody_();this.updateGrid_();Blockly.AudioContext.tone(toneValue);setTimeout(() => {Blockly.AudioContext.stop();}, this.getDuration());
}/*** 点击播放按钮*/
Blockly.FieldTone.prototype.onPlayClick = function () {this.isPlaying = !this.isPlaying;this.playButton.innerText = this.isPlaying ? "暂停" : "播放";if (this.isPlaying) {this.playMelody_(0);} else {this.stopMelody_();}
}/*** 停止旋律播放*/
Blockly.FieldTone.prototype.stopMelody_ = function () {clearTimeout(this.playTimer);Blockly.AudioContext.stop();this.playTimer = null;this.playButton.innerText = "播放";this.isPlaying = false;
}/*** 开始播放旋律* @param index*/
Blockly.FieldTone.prototype.playMelody_ = function (index) {if (index >= 8) {this.stopMelody_();this.updateGrid_();} else {// 更新格子,显示出当前播放的列this.updateGrid_(index);// 播放当前音符const tone = this.getToneCore(this.getToneIndex(this.tones_[index++]));if (tone) {Blockly.AudioContext.tone(tone);} else {Blockly.AudioContext.stop();}this.playTimer = setTimeout(() => {this.playMelody_(index);}, this.getDuration());}
}/*** 更新格子样式* @param col*/
Blockly.FieldTone.prototype.updateGrid_ = function (col = -1) {const nodeSize = Blockly.FieldTone.NODE_SIZE;for (let i = 0; i < nodeSize; i++) {for (let j = 0; j < nodeSize; j++) {const index = j * nodeSize + i;const fill = this.buttonNodes_[index].getAttribute("fill");// 只更新休止符if (fill === "#dcdcdc") {if (i === col) {this.buttonNodes_[index].setAttribute("fill-opacity", ".7");} else {this.buttonNodes_[index].setAttribute("fill-opacity", "1");}}}}
};/*** 点击完成按钮*/
Blockly.FieldTone.prototype.onFinishClick = function () {Blockly.DropDownDiv.hide();this.stopMelody_();
}/*** 更新颜色* @package*/
Blockly.FieldTone.prototype.updateColour = function () {if (this.fieldGroup_ && this.sourceBlock_) {this.fieldGroup_.setAttribute('stroke', this.sourceBlock_.getColourTertiary());if (this.sourceBlock_ && (this.sourceBlock_.disabled || this.sourceBlock_.getInheritedDisabled())) {this.fieldGroup_.setAttribute('stroke-opacity', ".25");this.fieldGroup_.setAttribute('fill-opacity', ".25");} else {this.fieldGroup_.setAttribute('stroke-opacity', "1");this.fieldGroup_.setAttribute('fill-opacity', "1");}}
};/*** 隐藏时*/
Blockly.FieldTone.prototype.onHide_ = function () {if (this.bpmWrapper_)Blockly.unbindEvent_(this.bpmWrapper_);if (this.toneWrapper_)Blockly.unbindEvent_(this.toneWrapper_);if (this.playWrapper_)Blockly.unbindEvent_(this.playWrapper_);if (this.finishWrapper_)Blockly.unbindEvent_(this.finishWrapper_);
};Blockly.Field.register('field_tone', Blockly.FieldTone);
完成audio_context.js代码图下:
'use strict';goog.provide('Blockly.AudioContext');/*** 音频管理* @constructor*/
Blockly.AudioContext = function () {
}/*** mute audio* @type {boolean}* @private*/
Blockly.AudioContext._mute = false;/*** AudioContext* @type {null}* @private*/
Blockly.AudioContext._context = null;/*** frequency* @type {number}* @private*/
Blockly.AudioContext._frequency = 0;/*** gain* @type {null}* @private*/
Blockly.AudioContext._gain = null;/*** OscillatorNode* @type {null}* @private*/
Blockly.AudioContext._vco = null;/*** Create context* @returns {null}*/
Blockly.AudioContext.context = function () {if (!Blockly.AudioContext._context)Blockly.AudioContext._context = Blockly.AudioContext.freshContext();return Blockly.AudioContext._context;
}/*** freshContext* @returns {undefined|AudioContext}*/
Blockly.AudioContext.freshContext = function () {if (window.AudioContext) {try {return new window.AudioContext();} catch (e) {}}return undefined;
}Blockly.AudioContext.mute = function (mute) {if (!Blockly.AudioContext._context)return;Blockly.AudioContext._mute = mute;Blockly.stop();if (mute && Blockly.AudioContext._vco) {Blockly.AudioContext._vco.disconnect();Blockly.AudioContext.gain.disconnect();Blockly.AudioContext._vco = undefined;Blockly.AudioContext.gain = undefined;}
}/*** Stop*/
Blockly.AudioContext.stop = function () {if (!Blockly.AudioContext._context)return;Blockly.AudioContext._gain.gain.setTargetAtTime(0, Blockly.AudioContext._context.currentTime, 0.015);Blockly.AudioContext._frequency = 0;
}/*** frequency* @returns {number|*}*/
Blockly.AudioContext.frequency = function () {return Blockly.AudioContext._frequency;
}/*** Play* @param frequency*/
Blockly.AudioContext.tone = function (frequency) {if (Blockly.AudioContext._mute)return;if (isNaN(frequency) || frequency < 0)return;Blockly.AudioContext._frequency = frequency;let ctx = Blockly.AudioContext.context();if (!ctx)return;try {if (!Blockly.AudioContext._vco) {Blockly.AudioContext._vco = ctx.createOscillator();Blockly.AudioContext._vco.type = 'triangle';Blockly.AudioContext._gain = ctx.createGain();Blockly.AudioContext._gain.gain.value = 0;Blockly.AudioContext._gain.connect(ctx.destination);Blockly.AudioContext._vco.connect(Blockly.AudioContext._gain);Blockly.AudioContext._vco.start(0);}Blockly.AudioContext._vco.frequency.linearRampToValueAtTime(frequency, Blockly.AudioContext._context.currentTime);Blockly.AudioContext._gain.gain.setTargetAtTime(.2, Blockly.AudioContext._context.currentTime, 0.015);} catch (e) {Blockly.AudioContext._vco = undefined;}
}
完成css样式代码,可以添加到core/css.js中,也可以写在页面的html中,我是直接写在了css.js中,直接追加到Blockly.Css.CONTENT中就行,代码如下:
'.FieldToneTool{','display: flex;','margin-bottom: 8px;','padding: 0 10px;','align-items: center;','justify-content: space-between;','}','.FieldToneTool > div> input {','width: 42px;','height: 24px;','border-radius: 3px;',"margin-right: 4px;",'font-size: 12px;','}','.FieldToneTool > div> span {','color: white;','font-size: 12px;','}','.FieldToneTool >div> button {','padding: 0 8px;','border-radius: 3px;','cursor: pointer;','height: 24px;','outline: none;','transition: all .1s;','border: 1px solid rgba(0,0,0,.2);','background-color:rgba(0,0,0,.1);','font-size: 12px;','color: white;','margin-left: 8px;','}','.FieldToneTool >div> button:hover {','background-color:rgba(0,0,0,.2);','}','.FieldToneTool >div> button:active {','box-shadow: 0px 0px 0px 4px ' + Blockly.Colours.fieldShadow + ';','}',
六、关于我
作者:陆志敏
联系:761324428@qq.com
这篇关于Scratch Blocks自定义组件之「旋律播放」的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!