Skip to content

Commit 7198ace

Browse files
Makey Makey extension (#1782)
* Initial working makey makey extension * Cleanup and localization * Add block icon * Localization and cleanup * Docs and cleanup * Update block icon * Cleanup * Fix key press args
1 parent ff2566f commit 7198ace

File tree

2 files changed

+375
-1
lines changed

2 files changed

+375
-1
lines changed

src/extension-support/extension-manager.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const Scratch3TranslateBlocks = require('../extensions/scratch3_translate');
1616
const Scratch3VideoSensingBlocks = require('../extensions/scratch3_video_sensing');
1717
const Scratch3Speech2TextBlocks = require('../extensions/scratch3_speech2text');
1818
const Scratch3Ev3Blocks = require('../extensions/scratch3_ev3');
19+
const Scratch3MakeyMakeyBlocks = require('../extensions/scratch3_makeymakey');
1920

2021
const builtinExtensions = {
2122
pen: Scratch3PenBlocks,
@@ -26,7 +27,8 @@ const builtinExtensions = {
2627
translate: Scratch3TranslateBlocks,
2728
videoSensing: Scratch3VideoSensingBlocks,
2829
speech2text: Scratch3Speech2TextBlocks,
29-
ev3: Scratch3Ev3Blocks
30+
ev3: Scratch3Ev3Blocks,
31+
makeymakey: Scratch3MakeyMakeyBlocks
3032
};
3133

3234
/**
Lines changed: 372 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,372 @@
1+
const formatMessage = require('format-message');
2+
const ArgumentType = require('../../extension-support/argument-type');
3+
const BlockType = require('../../extension-support/block-type');
4+
const Cast = require('../../util/cast');
5+
6+
/**
7+
* Icon svg to be displayed at the left edge of each extension block, encoded as a data URI.
8+
* @type {string}
9+
*/
10+
// eslint-disable-next-line max-len
11+
const blockIconURI = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHN0eWxlPi5zdDJ7ZmlsbDpyZWR9LnN0M3tmaWxsOiNlMGUwZTB9LnN0NHtmaWxsOm5vbmU7c3Ryb2tlOiM2NjY7c3Ryb2tlLXdpZHRoOi41O3N0cm9rZS1taXRlcmxpbWl0OjEwfTwvc3R5bGU+PHBhdGggZD0iTTM1IDI4SDVhMSAxIDAgMCAxLTEtMVYxMmMwLS42LjQtMSAxLTFoMzBjLjUgMCAxIC40IDEgMXYxNWMwIC41LS41IDEtMSAxeiIgZmlsbD0iI2ZmZiIgaWQ9IkxheWVyXzYiLz48ZyBpZD0iTGF5ZXJfNCI+PHBhdGggY2xhc3M9InN0MiIgZD0iTTQgMjVoMzJ2Mi43SDR6TTEzIDI0aC0yLjJhMSAxIDAgMCAxLTEtMXYtOS43YzAtLjYuNC0xIDEtMUgxM2MuNiAwIDEgLjQgMSAxVjIzYzAgLjYtLjUgMS0xIDF6Ii8+PHBhdGggY2xhc3M9InN0MiIgZD0iTTYuMSAxOS4zdi0yLjJjMC0uNS40LTEgMS0xaDkuN2MuNSAwIDEgLjUgMSAxdjIuMmMwIC41LS41IDEtMSAxSDcuMWExIDEgMCAwIDEtMS0xeiIvPjxjaXJjbGUgY2xhc3M9InN0MiIgY3g9IjIyLjgiIGN5PSIxOC4yIiByPSIzLjQiLz48Y2lyY2xlIGNsYXNzPSJzdDIiIGN4PSIzMC42IiBjeT0iMTguMiIgcj0iMy40Ii8+PHBhdGggY2xhc3M9InN0MiIgZD0iTTQuMiAyN2gzMS45di43SDQuMnoiLz48L2c+PGcgaWQ9IkxheWVyXzUiPjxjaXJjbGUgY2xhc3M9InN0MyIgY3g9IjIyLjgiIGN5PSIxOC4yIiByPSIyLjMiLz48Y2lyY2xlIGNsYXNzPSJzdDMiIGN4PSIzMC42IiBjeT0iMTguMiIgcj0iMi4zIi8+PHBhdGggY2xhc3M9InN0MyIgZD0iTTEyLjUgMjIuOWgtMS4yYy0uMyAwLS41LS4yLS41LS41VjE0YzAtLjMuMi0uNS41LS41aDEuMmMuMyAwIC41LjIuNS41djguNGMwIC4zLS4yLjUtLjUuNXoiLz48cGF0aCBjbGFzcz0ic3QzIiBkPSJNNy4yIDE4Ljd2LTEuMmMwLS4zLjItLjUuNS0uNWg4LjRjLjMgMCAuNS4yLjUuNXYxLjJjMCAuMy0uMi41LS41LjVINy43Yy0uMyAwLS41LS4yLS41LS41ek00IDI2aDMydjJINHoiLz48L2c+PGcgaWQ9IkxheWVyXzMiPjxwYXRoIGNsYXNzPSJzdDQiIGQ9Ik0zNS4yIDI3LjlINC44YTEgMSAwIDAgMS0xLTFWMTIuMWMwLS42LjUtMSAxLTFoMzAuNWMuNSAwIDEgLjQgMSAxVjI3YTEgMSAwIDAgMS0xLjEuOXoiLz48cGF0aCBjbGFzcz0ic3Q0IiBkPSJNMzUuMiAyNy45SDQuOGExIDEgMCAwIDEtMS0xVjEyLjFjMC0uNi41LTEgMS0xaDMwLjVjLjUgMCAxIC40IDEgMVYyN2ExIDEgMCAwIDEtMS4xLjl6Ii8+PC9nPjwvc3ZnPg==';
12+
13+
/**
14+
* Length of the buffer to store key presses for the "when keys pressed in order" hat
15+
* @type {number}
16+
*/
17+
const KEY_BUFFER_LENGTH = 100;
18+
19+
/**
20+
* Timeout in milliseconds to reset the completed flag for a sequence.
21+
* @type {number}
22+
*/
23+
const SEQUENCE_HAT_TIMEOUT = 100;
24+
25+
/**
26+
* An id for the space key on a keyboard.
27+
*/
28+
const KEY_ID_SPACE = 'SPACE';
29+
30+
/**
31+
* An id for the left arrow key on a keyboard.
32+
*/
33+
const KEY_ID_LEFT = 'LEFT';
34+
35+
/**
36+
* An id for the right arrow key on a keyboard.
37+
*/
38+
const KEY_ID_RIGHT = 'RIGHT';
39+
40+
/**
41+
* An id for the up arrow key on a keyboard.
42+
*/
43+
const KEY_ID_UP = 'UP';
44+
45+
/**
46+
* An id for the down arrow key on a keyboard.
47+
*/
48+
const KEY_ID_DOWN = 'DOWN';
49+
50+
/**
51+
* Names used by keyboard io for keys used in scratch.
52+
* @enum {string}
53+
*/
54+
const SCRATCH_KEY_NAME = {
55+
[KEY_ID_SPACE]: 'space',
56+
[KEY_ID_LEFT]: 'left arrow',
57+
[KEY_ID_UP]: 'up arrow',
58+
[KEY_ID_RIGHT]: 'right arrow',
59+
[KEY_ID_DOWN]: 'down arrow'
60+
};
61+
62+
/**
63+
* Class for the makey makey blocks in Scratch 3.0
64+
* @constructor
65+
*/
66+
class Scratch3MakeyMakeyBlocks {
67+
constructor (runtime) {
68+
/**
69+
* The runtime instantiating this block package.
70+
* @type {Runtime}
71+
*/
72+
this.runtime = runtime;
73+
74+
/**
75+
* A toggle that alternates true and false each frame, so that an
76+
* edge-triggered hat can trigger on every other frame.
77+
* @type {boolean}
78+
*/
79+
this.frameToggle = false;
80+
81+
// Set an interval that toggles the frameToggle every frame.
82+
setInterval(() => {
83+
this.frameToggle = !this.frameToggle;
84+
}, this.runtime.currentStepTime);
85+
86+
this.keyPressed = this.keyPressed.bind(this);
87+
this.runtime.on('KEY_PRESSED', this.keyPressed);
88+
89+
/*
90+
* An object containing a set of sequence objects.
91+
* These are the key sequences currently being detected by the "when
92+
* keys pressed in order" hat block. Each sequence is keyed by its
93+
* string representation (the sequence's value in the menu, which is a
94+
* string of KEY_IDs separated by spaces). Each sequence object
95+
* has an array property (an array of KEY_IDs) and a boolean
96+
* completed property that is true when the sequence has just been
97+
* pressed.
98+
* @type {object}
99+
*/
100+
this.sequences = {};
101+
102+
/*
103+
* An array of the key codes of recently pressed keys.
104+
* @type {array}
105+
*/
106+
this.keyPressBuffer = [];
107+
}
108+
109+
/*
110+
* Localized short-form names of the space bar and arrow keys, for use in the
111+
* displayed menu items of the "when keys pressed in order" block.
112+
* @type {object}
113+
*/
114+
get KEY_TEXT_SHORT () {
115+
return {
116+
[KEY_ID_SPACE]: formatMessage({
117+
id: 'makeymakey.spaceKey',
118+
default: 'space',
119+
description: 'The space key on a computer keyboard.'
120+
}),
121+
[KEY_ID_LEFT]: formatMessage({
122+
id: 'makeymakey.leftArrowShort',
123+
default: 'left',
124+
description: 'Short name for the left arrow key on a computer keyboard.'
125+
}),
126+
[KEY_ID_UP]: formatMessage({
127+
id: 'makeymakey.upArrowShort',
128+
default: 'up',
129+
description: 'Short name for the up arrow key on a computer keyboard.'
130+
}),
131+
[KEY_ID_RIGHT]: formatMessage({
132+
id: 'makeymakey.rightArrowShort',
133+
default: 'right',
134+
description: 'Short name for the right arrow key on a computer keyboard.'
135+
}),
136+
[KEY_ID_DOWN]: formatMessage({
137+
id: 'makeymakey.downArrowShort',
138+
default: 'down',
139+
description: 'Short name for the down arrow key on a computer keyboard.'
140+
})
141+
};
142+
}
143+
144+
/*
145+
* An array of strings of KEY_IDs representing the default set of
146+
* key sequences for use by the "when keys pressed in order" block.
147+
* @type {array}
148+
*/
149+
get DEFAULT_SEQUENCES () {
150+
return [
151+
`${KEY_ID_LEFT} ${KEY_ID_UP} ${KEY_ID_RIGHT}`,
152+
`${KEY_ID_RIGHT} ${KEY_ID_UP} ${KEY_ID_LEFT}`,
153+
`${KEY_ID_LEFT} ${KEY_ID_RIGHT}`,
154+
`${KEY_ID_RIGHT} ${KEY_ID_LEFT}`,
155+
`${KEY_ID_UP} ${KEY_ID_DOWN}`,
156+
`${KEY_ID_DOWN} ${KEY_ID_UP}`,
157+
`${KEY_ID_UP} ${KEY_ID_RIGHT} ${KEY_ID_DOWN} ${KEY_ID_LEFT}`,
158+
`${KEY_ID_SPACE} ${KEY_ID_SPACE} ${KEY_ID_SPACE}`,
159+
`${KEY_ID_UP} ${KEY_ID_UP} ${KEY_ID_DOWN} ${KEY_ID_DOWN} ` +
160+
`${KEY_ID_LEFT} ${KEY_ID_RIGHT} ${KEY_ID_LEFT} ${KEY_ID_RIGHT}`
161+
];
162+
}
163+
164+
/**
165+
* @returns {object} metadata for this extension and its blocks.
166+
*/
167+
getInfo () {
168+
return {
169+
id: 'makeymakey',
170+
name: 'Makey Makey',
171+
blockIconURI: blockIconURI,
172+
blocks: [
173+
{
174+
opcode: 'whenMakeyKeyPressed',
175+
text: 'when [KEY] key pressed',
176+
blockType: BlockType.HAT,
177+
arguments: {
178+
KEY: {
179+
type: ArgumentType.STRING,
180+
menu: 'KEY',
181+
defaultValue: KEY_ID_SPACE
182+
}
183+
}
184+
},
185+
{
186+
opcode: 'whenCodePressed',
187+
text: 'when [SEQUENCE] pressed in order',
188+
blockType: BlockType.HAT,
189+
arguments: {
190+
SEQUENCE: {
191+
type: ArgumentType.STRING,
192+
menu: 'SEQUENCE',
193+
defaultValue: this.DEFAULT_SEQUENCES[0]
194+
}
195+
}
196+
}
197+
],
198+
menus: {
199+
KEY: [
200+
{
201+
text: formatMessage({
202+
id: 'makeymakey.spaceKey',
203+
default: 'space',
204+
description: 'The space key on a computer keyboard.'
205+
}),
206+
value: KEY_ID_SPACE
207+
},
208+
{
209+
text: formatMessage({
210+
id: 'makeymakey.leftArrow',
211+
default: 'left arrow',
212+
description: 'The left arrow key on a computer keyboard.'
213+
}),
214+
value: KEY_ID_LEFT
215+
},
216+
{
217+
text: formatMessage({
218+
id: 'makeymakey.rightArrow',
219+
default: 'right arrow',
220+
description: 'The right arrow key on a computer keyboard.'
221+
}),
222+
value: KEY_ID_RIGHT
223+
},
224+
{
225+
text: formatMessage({
226+
id: 'makeymakey.downArrow',
227+
default: 'down arrow',
228+
description: 'The down arrow key on a computer keyboard.'
229+
}),
230+
value: KEY_ID_DOWN
231+
},
232+
{
233+
text: formatMessage({
234+
id: 'makeymakey.upArrow',
235+
default: 'up arrow',
236+
description: 'The up arrow key on a computer keyboard.'
237+
}),
238+
value: KEY_ID_UP
239+
},
240+
{text: 'w', value: 'w'},
241+
{text: 'a', value: 'a'},
242+
{text: 's', value: 's'},
243+
{text: 'd', value: 'd'},
244+
{text: 'f', value: 'f'},
245+
{text: 'g', value: 'g'}
246+
],
247+
SEQUENCE: this.buildSequenceMenu(this.DEFAULT_SEQUENCES)
248+
}
249+
};
250+
}
251+
252+
/*
253+
* Build the menu of key sequences.
254+
* @param {array} sequencesArray an array of strings of KEY_IDs.
255+
* @returns {array} an array of objects with text and value properties.
256+
*/
257+
buildSequenceMenu (sequencesArray) {
258+
return sequencesArray.map(
259+
str => this.getMenuItemForSequenceString(str)
260+
);
261+
}
262+
263+
/*
264+
* Create a menu item for a sequence string.
265+
* @param {string} sequenceString a string of KEY_IDs.
266+
* @return {object} an object with text and value properties.
267+
*/
268+
getMenuItemForSequenceString (sequenceString) {
269+
let sequenceArray = sequenceString.split(' ');
270+
sequenceArray = sequenceArray.map(str => this.KEY_TEXT_SHORT[str]);
271+
return {
272+
text: sequenceArray.join(' '),
273+
value: sequenceString
274+
};
275+
}
276+
277+
/*
278+
* Check whether a keyboard key is currently pressed.
279+
* Also, toggle the results of the test on alternate frames, so that the
280+
* hat block fires repeatedly.
281+
* @param {object} args - the block arguments.
282+
* @property {number} KEY - a key code.
283+
* @param {object} util - utility object provided by the runtime.
284+
*/
285+
whenMakeyKeyPressed (args, util) {
286+
let key = args.KEY;
287+
// Convert the key arg, if it is a KEY_ID, to the key name used by
288+
// the Keyboard io module.
289+
if (SCRATCH_KEY_NAME[args.KEY]) {
290+
key = SCRATCH_KEY_NAME[args.KEY];
291+
}
292+
const isDown = util.ioQuery('keyboard', 'getKeyIsDown', [key]);
293+
return (isDown && this.frameToggle);
294+
}
295+
296+
/*
297+
* A function called on the KEY_PRESSED event, to update the key press
298+
* buffer and check if any of the key sequences have been completed.
299+
* @param {string} key A scratch key name.
300+
*/
301+
keyPressed (key) {
302+
// Store only the first word of the Scratch key name, so that e.g. when
303+
// "left arrow" is pressed, we store "LEFT", which matches KEY_ID_LEFT
304+
key = key.split(' ')[0];
305+
key = key.toUpperCase();
306+
this.keyPressBuffer.push(key);
307+
// Keep the buffer under the length limit
308+
if (this.keyPressBuffer.length > KEY_BUFFER_LENGTH) {
309+
this.keyPressBuffer.shift();
310+
}
311+
// Check the buffer for each sequence in use
312+
for (const str in this.sequences) {
313+
const arr = this.sequences[str].array;
314+
// Bail out if we don't have enough presses for this sequence
315+
if (this.keyPressBuffer.length < arr.length) {
316+
continue;
317+
}
318+
let missFlag = false;
319+
// Slice the buffer to the length of the sequence we're checking
320+
const bufferSegment = this.keyPressBuffer.slice(-1 * arr.length);
321+
for (let i = 0; i < arr.length; i++) {
322+
if (arr[i] !== bufferSegment[i]) {
323+
missFlag = true;
324+
}
325+
}
326+
// If the miss flag is false, the sequence matched the buffer
327+
if (!missFlag) {
328+
this.sequences[str].completed = true;
329+
// Clear the completed flag after a timeout. This is necessary because
330+
// the hat is edge-triggered (not event triggered). Multiple hats
331+
// may be checking the same sequence, so this timeout gives them enough
332+
// time to all trigger before resetting the flag.
333+
setTimeout(() => {
334+
this.sequences[str].completed = false;
335+
}, SEQUENCE_HAT_TIMEOUT);
336+
}
337+
}
338+
}
339+
340+
/*
341+
* Add a key sequence to the set currently being checked on each key press.
342+
* @param {string} sequenceString a string of space-separated KEY_IDs.
343+
* @param {array} sequenceArray an array of KEY_IDs.
344+
*/
345+
addSequence (sequenceString, sequenceArray) {
346+
// If we already have this sequence string, return.
347+
if (this.sequences.hasOwnProperty(sequenceString)) {
348+
return;
349+
}
350+
this.sequences[sequenceString] = {
351+
array: sequenceArray,
352+
completed: false
353+
};
354+
}
355+
356+
/*
357+
* Check whether a key sequence was recently completed.
358+
* @param {object} args The block arguments.
359+
* @property {number} SEQUENCE A string of KEY_IDs.
360+
*/
361+
whenCodePressed (args) {
362+
const sequenceString = Cast.toString(args.SEQUENCE).toUpperCase();
363+
const sequenceArray = sequenceString.split(' ');
364+
if (sequenceArray.length < 2) {
365+
return;
366+
}
367+
this.addSequence(sequenceString, sequenceArray);
368+
369+
return this.sequences[sequenceString].completed;
370+
}
371+
}
372+
module.exports = Scratch3MakeyMakeyBlocks;

0 commit comments

Comments
 (0)