diff --git a/libs/azureiot/test.ts b/libs/azureiot/test.ts index c1eb9a3b..e52742e9 100644 --- a/libs/azureiot/test.ts +++ b/libs/azureiot/test.ts @@ -1,9 +1,9 @@ -export interface Secrets { +interface Secrets { connString: string; wifi: pxt.StringMap; } // this is to be overridden in a separate file -export let secrets: Secrets; +let secrets: Secrets; function test() { const log = console.log; diff --git a/libs/color/README.md b/libs/color/README.md new file mode 100644 index 00000000..9c1ea699 --- /dev/null +++ b/libs/color/README.md @@ -0,0 +1,3 @@ +# Colors + +Color manipulation \ No newline at end of file diff --git a/libs/color/colorbuffer.ts b/libs/color/colorbuffer.ts new file mode 100644 index 00000000..6b04c26a --- /dev/null +++ b/libs/color/colorbuffer.ts @@ -0,0 +1,100 @@ +namespace color { + export enum ColorBufferLayout { + /** + * 24bit RGB color + */ + RGB, + /** + * 32bit RGB color with alpha + */ + ARGB + } + + /** + * A buffer of colors + */ + export class ColorBuffer { + layout: ColorBufferLayout; + buf: Buffer; + + constructor(buf: Buffer, layout?: ColorBufferLayout) { + this.buf = buf; + this.layout = layout || ColorBufferLayout.RGB; + } + + get stride() { + return this.layout == ColorBufferLayout.RGB ? 3 : 4; + } + + get length() { + return Math.idiv(this.buf.length, this.stride); + } + + color(index: number): number { + index = index | 0; + if (index < 0 || index >= this.length) return -1; + + const s = this.stride; + const start = index * s; + let c = 0; + for (let i = 0; i < s; ++i) + c = (c << 8) | (this.buf[start + i] & 0xff); + return c; + } + + setColor(index: number, color: number) { + index = index | 0; + if (index < 0 || index >= this.length) return; + + const s = this.stride; + const start = index * s; + for (let i = s - 1; i >= 0; --i) { + this.buf[start + i] = color & 0xff; + color = color >> 8; + } + } + + slice(start?: number, length?: number): ColorBuffer { + const s = this.stride; + return new ColorBuffer(this.buf.slice(start ? start * s : start, length ? length * s : length)); + } + + /** + * Writes the content of the src color buffer starting at the start dstOffset in the current buffer + * @param dstOffset + * @param src + */ + write(dstOffset: number, src: ColorBuffer): void { + if (this.layout == src.layout) { + const d = (dstOffset | 0) * this.stride; + this.buf.write(d, src.buf); + } else { + // different color layout + const n = Math.min(src.length, this.length - dstOffset); + for (let i = 0; i < n; ++i) + this.setColor(dstOffset + i, src.color(i)); + } + } + } + + /** + * Converts an array of colors into a color buffer + */ + export function createBuffer(colors: number[], layout?: ColorBufferLayout): color.ColorBuffer { + const n = colors.length; + layout = layout || ColorBufferLayout.RGB; + const stride = layout == ColorBufferLayout.RGB ? 3 : 4; + const buf = control.createBuffer(n * stride); + const p = new ColorBuffer(buf); + let k = 0; + for (let i = 0; i < n; i++) { + let color = colors[i]; + for (let j = stride - 1; j >= 0; --j) { + p.buf[k + j] = color & 0xff; + color = color >> 8; + } + k += stride; + } + return p; + } +} \ No newline at end of file diff --git a/libs/color/colors.ts b/libs/color/colors.ts new file mode 100644 index 00000000..b337b730 --- /dev/null +++ b/libs/color/colors.ts @@ -0,0 +1,213 @@ +/** + * Well known colors + */ +const enum Colors { + //% block=red + Red = 0xFF0000, + //% block=orange + Orange = 0xFF7F00, + //% block=yellow + Yellow = 0xFFFF00, + //% block=green + Green = 0x00FF00, + //% block=blue + Blue = 0x0000FF, + //% block=indigo + Indigo = 0x4b0082, + //% block=violet + Violet = 0x8a2be2, + //% block=purple + Purple = 0xA033E5, + //% block=pink + Pink = 0xFF007F, + //% block=white + White = 0xFFFFFF, + //% block=black + Black = 0x000000 +} + +/** + * Well known color hues + */ +const enum ColorHues { + //% block=red + Red = 0, + //% block=orange + Orange = 29, + //% block=yellow + Yellow = 43, + //% block=green + Green = 86, + //% block=aqua + Aqua = 125, + //% block=blue + Blue = 170, + //% block=purple + Purple = 191, + //% block=magenta + Magenta = 213, + //% block=pink + Pink = 234 +} + +/** + * Color manipulation + */ +//% advanced=1 +namespace color { + /** + * Converts red, green, blue channels into a RGB color + * @param red value of the red channel between 0 and 255. eg: 255 + * @param green value of the green channel between 0 and 255. eg: 255 + * @param blue value of the blue channel between 0 and 255. eg: 255 + */ + //% blockId="colorsrgb" block="red %red|green %green|blue %blue" + //% red.min=0 red.max=255 green.min=0 green.max=255 blue.min=0 blue.max=255 + //% help="colors/rgb" + //% weight=19 blockGap=8 + //% blockHidden=true + export function rgb(red: number, green: number, blue: number): number { + return ((red & 0xFF) << 16) | ((green & 0xFF) << 8) | (blue & 0xFF); + } + + export function argb(alpha: number, red: number, green: number, blue: number): number { + return ((alpha & 0xFF) << 24) | ((red & 0xFF) << 16) | ((green & 0xFF) << 8) | (blue & 0xFF); + } + + /** + * Get the RGB value of a known color + */ + //% blockId=colorscolors block="%color" + //% help="colors/well-known" + //% shim=TD_ID + //% weight=20 blockGap=8 + //% blockHidden=true + export function wellKnown(color: Colors): number { + return color; + } + + /** + * Convert an HSV (hue, saturation, value) color to RGB + * @param hue value of the hue channel between 0 and 255. eg: 255 + * @param sat value of the saturation channel between 0 and 255. eg: 255 + * @param val value of the value channel between 0 and 255. eg: 255 + */ + + //% blockId="colorshsv" block="hue %hue|sat %sat|val %val" + //% hue.min=0 hue.max=255 sat.min=0 sat.max=255 val.min=0 val.max=255 + //% help="colors/hsv" + //% weight=17 + //% blockHidden=true + export function hsv(hue: number, sat: number = 255, val: number = 255): number { + let h = (hue % 255) >> 0; + if (h < 0) h += 255; + // scale down to 0..192 + h = (h * 192 / 255) >> 0; + + //reference: based on FastLED's hsv2rgb rainbow algorithm [https://github.com/FastLED/FastLED](MIT) + let invsat = 255 - sat; + let brightness_floor = ((val * invsat) / 255) >> 0; + let color_amplitude = val - brightness_floor; + let section = (h / 0x40) >> 0; // [0..2] + let offset = (h % 0x40) >> 0; // [0..63] + + let rampup = offset; + let rampdown = (0x40 - 1) - offset; + + let rampup_amp_adj = ((rampup * color_amplitude) / (255 / 4)) >> 0; + let rampdown_amp_adj = ((rampdown * color_amplitude) / (255 / 4)) >> 0; + + let rampup_adj_with_floor = (rampup_amp_adj + brightness_floor); + let rampdown_adj_with_floor = (rampdown_amp_adj + brightness_floor); + + let r: number; + let g: number; + let b: number; + if (section) { + if (section == 1) { + // section 1: 0x40..0x7F + r = brightness_floor; + g = rampdown_adj_with_floor; + b = rampup_adj_with_floor; + } else { + // section 2; 0x80..0xBF + r = rampup_adj_with_floor; + g = brightness_floor; + b = rampdown_adj_with_floor; + } + } else { + // section 0: 0x00..0x3F + r = rampdown_adj_with_floor; + g = rampup_adj_with_floor; + b = brightness_floor; + } + return rgb(r, g, b); + } + + /** + * Fade the color by the brightness + * @param color color to fade + * @param brightness the amount of brightness to apply to the color, eg: 128 + */ + //% blockId="colorsfade" block="fade %color=neopixel_colors|by %brightness" + //% brightness.min=0 brightness.max=255 + //% help="light/fade" + //% group="Color" weight=18 blockGap=8 + //% blockHidden=true + export function fade(color: number, brightness: number): number { + brightness = Math.max(0, Math.min(255, brightness >> 0)); + if (brightness < 255) { + let red = unpackR(color); + let green = unpackG(color); + let blue = unpackB(color); + + red = (red * brightness) >> 8; + green = (green * brightness) >> 8; + blue = (blue * brightness) >> 8; + + color = rgb(red, green, blue); + } + return color; + } + + export function unpackR(rgb: number): number { + return (rgb >> 16) & 0xFF; + } + export function unpackG(rgb: number): number { + return (rgb >> 8) & 0xFF; + } + export function unpackB(rgb: number): number { + return (rgb >> 0) & 0xFF; + } + + export function parseColor(color: string): number { + switch (color) { + case "RED": + case "red": + return Colors.Red; + case "GREEN": + case "green": + return Colors.Green; + case "BLUE": + case "blue": + return Colors.Blue; + case "WHITE": + case "white": + return Colors.White; + case "ORANGE": + case "orange": + return Colors.Orange; + case "PURPLE": + case "purple": + return Colors.Purple; + case "YELLOW": + case "yellow": + return Colors.Yellow; + case "PINK": + case "pink": + return Colors.Pink; + default: + return parseInt(color) || 0; + } + } +} \ No newline at end of file diff --git a/libs/color/pxt.json b/libs/color/pxt.json new file mode 100644 index 00000000..f11c5363 --- /dev/null +++ b/libs/color/pxt.json @@ -0,0 +1,14 @@ +{ + "name": "color", + "description": "Color manipulation", + "files": [ + "colors.ts", + "colorbuffer.ts", + "README.md" + ], + "public": true, + "weight": 1, + "dependencies": { + "core": "file:../core" + } +} diff --git a/libs/game/game.ts b/libs/game/game.ts index 978e700b..a0ffc883 100644 --- a/libs/game/game.ts +++ b/libs/game/game.ts @@ -328,7 +328,8 @@ namespace game { */ export function addScenePushHandler(handler: (oldScene: scene.Scene) => void) { if (!_scenePushHandlers) _scenePushHandlers = []; - _scenePushHandlers.push(handler); + if (_scenePushHandlers.indexOf(handler) < 0) + _scenePushHandlers.push(handler); } /** @@ -351,7 +352,8 @@ namespace game { */ export function addScenePopHandler(handler: (oldScene: scene.Scene) => void) { if (!_scenePopHandlers) _scenePopHandlers = []; - _scenePopHandlers.push(handler); + if (_scenePopHandlers.indexOf(handler) < 0) + _scenePopHandlers.push(handler); } /** diff --git a/libs/light/neopixel.ts b/libs/light/neopixel.ts index 73ca0a71..e62af9ee 100644 --- a/libs/light/neopixel.ts +++ b/libs/light/neopixel.ts @@ -1,55 +1,3 @@ -/** - * Well known colors - */ -const enum Colors { - //% block=red - Red = 0xFF0000, - //% block=orange - Orange = 0xFF7F00, - //% block=yellow - Yellow = 0xFFFF00, - //% block=green - Green = 0x00FF00, - //% block=blue - Blue = 0x0000FF, - //% block=indigo - Indigo = 0x4b0082, - //% block=violet - Violet = 0x8a2be2, - //% block=purple - Purple = 0xA033E5, - //% block=pink - Pink = 0xFF007F, - //% block=white - White = 0xFFFFFF, - //% block=black - Black = 0x000000 -} - -/** - * Well known color hues - */ -const enum ColorHues { - //% block=red - Red = 0, - //% block=orange - Orange = 29, - //% block=yellow - Yellow = 43, - //% block=green - Green = 86, - //% block=aqua - Aqua = 125, - //% block=blue - Blue = 170, - //% block=purple - Purple = 191, - //% block=magenta - Magenta = 213, - //% block=pink - Pink = 234 -} - /** * Different modes for RGB or RGB+W NeoPixel strips */ @@ -195,9 +143,9 @@ namespace light { //% advanced=true setAll(rgb: number) { rgb = rgb | 0; - const red = unpackR(rgb); - const green = unpackG(rgb); - const blue = unpackB(rgb); + const red = color.unpackR(rgb); + const green = color.unpackG(rgb); + const blue = color.unpackB(rgb); const end = this._start + this._length; const stride = this.stride(); @@ -216,12 +164,12 @@ namespace light { //% weight=79 blockGap=8 //% group="More" advanced=true blockHidden=true setGradient(startColor: number, endColor: number, easing?: (t: number) => number) { - const sr = unpackR(startColor); - const sg = unpackG(startColor); - const sb = unpackB(startColor); - const er = unpackR(endColor); - const eg = unpackG(endColor); - const eb = unpackB(endColor); + const sr = color.unpackR(startColor); + const sg = color.unpackG(startColor); + const sb = color.unpackB(startColor); + const er = color.unpackR(endColor); + const eg = color.unpackG(endColor); + const eb = color.unpackB(endColor); const end = this._start + this._length; const n1 = this._length - 1; @@ -299,9 +247,9 @@ namespace light { //% help="light/neopixelstrip/set-pixel-color" //% weight=79 blockGap=8 //% group="More" advanced=true - setPixelColor(pixeloffset: number, color: number): void { + setPixelColor(pixeloffset: number, c: number): void { pixeloffset = pixeloffset | 0; - color = color | 0; + c = c | 0; if (pixeloffset < 0 || pixeloffset >= this._length) @@ -309,9 +257,9 @@ namespace light { const stride = this.stride(); pixeloffset = (pixeloffset + this._start) * stride; - const red = unpackR(color); - const green = unpackG(color); - const blue = unpackB(color); + const red = color.unpackR(c); + const green = color.unpackG(c); + const blue = color.unpackB(c); this.setBufferRGB(pixeloffset, red, green, blue) this.autoShow(); } @@ -353,7 +301,7 @@ namespace light { break; } - return rgb(red, green, blue); + return color.rgb(red, green, blue); } /** @@ -663,7 +611,7 @@ namespace light { //% group="Photon" advanced=true setPhotonPenHue(hue: number) { hue = hue | 0; - this.setPhotonPenColor(hsv(hue, 0xff, 0xff)); + this.setPhotonPenColor(color.hsv(hue, 0xff, 0xff)); } //% deprecated=1 blockHidden=1 @@ -821,7 +769,7 @@ namespace light { tempColor += currChar; if ((isSpace || i == leds.length) && tempColor) { - this.setPixelColor(pi++, parseColor(tempColor)) + this.setPixelColor(pi++, color.parseColor(tempColor)) tempColor = ""; if (pi == n) { this.show(); @@ -1238,7 +1186,7 @@ namespace light { //% help="light/rgb" //% group="Color" weight=19 blockGap=8 export function rgb(red: number, green: number, blue: number): number { - return ((red & 0xFF) << 16) | ((green & 0xFF) << 8) | (blue & 0xFF); + return color.rgb(red, green, blue); } /** @@ -1252,19 +1200,6 @@ namespace light { return color; } - function unpackR(rgb: number): number { - let r = (rgb >> 16) & 0xFF; - return r; - } - function unpackG(rgb: number): number { - let g = (rgb >> 8) & 0xFF; - return g; - } - function unpackB(rgb: number): number { - let b = (rgb >> 0) & 0xFF; - return b; - } - /** * Convert an HSV (hue, saturation, value) color to RGB * @param hue value of the hue channel between 0 and 255. eg: 255 @@ -1277,53 +1212,11 @@ namespace light { //% help="light/hsv" //% group="Color" weight=17 export function hsv(hue: number, sat: number = 255, val: number = 255): number { - let h = (hue % 255) >> 0; - if (h < 0) h += 255; - // scale down to 0..192 - h = (h * 192 / 255) >> 0; - - //reference: based on FastLED's hsv2rgb rainbow algorithm [https://github.com/FastLED/FastLED](MIT) - let invsat = 255 - sat; - let brightness_floor = ((val * invsat) / 255) >> 0; - let color_amplitude = val - brightness_floor; - let section = (h / 0x40) >> 0; // [0..2] - let offset = (h % 0x40) >> 0; // [0..63] - - let rampup = offset; - let rampdown = (0x40 - 1) - offset; - - let rampup_amp_adj = ((rampup * color_amplitude) / (255 / 4)) >> 0; - let rampdown_amp_adj = ((rampdown * color_amplitude) / (255 / 4)) >> 0; - - let rampup_adj_with_floor = (rampup_amp_adj + brightness_floor); - let rampdown_adj_with_floor = (rampdown_amp_adj + brightness_floor); - - let r: number; - let g: number; - let b: number; - if (section) { - if (section == 1) { - // section 1: 0x40..0x7F - r = brightness_floor; - g = rampdown_adj_with_floor; - b = rampup_adj_with_floor; - } else { - // section 2; 0x80..0xBF - r = rampup_adj_with_floor; - g = brightness_floor; - b = rampdown_adj_with_floor; - } - } else { - // section 0: 0x00..0x3F - r = rampdown_adj_with_floor; - g = rampup_adj_with_floor; - b = brightness_floor; - } - return rgb(r, g, b); + return color.hsv(hue, sat, val); } /** - * Fade the color by the brightness + * Use color.fade instead * @param color color to fade * @param brightness the amount of brightness to apply to the color, eg: 128 */ @@ -1331,52 +1224,9 @@ namespace light { //% brightness.min=0 brightness.max=255 //% help="light/fade" //% group="Color" weight=18 blockGap=8 - //% blockHidden=true - export function fade(color: number, brightness: number): number { - brightness = Math.max(0, Math.min(255, brightness >> 0)); - if (brightness < 255) { - let red = unpackR(color); - let green = unpackG(color); - let blue = unpackB(color); - - red = (red * brightness) >> 8; - green = (green * brightness) >> 8; - blue = (blue * brightness) >> 8; - - color = rgb(red, green, blue); - } - return color; - } - - function parseColor(color: string) { - switch (color) { - case "RED": - case "red": - return Colors.Red; - case "GREEN": - case "green": - return Colors.Green; - case "BLUE": - case "blue": - return Colors.Blue; - case "WHITE": - case "white": - return Colors.White; - case "ORANGE": - case "orange": - return Colors.Orange; - case "PURPLE": - case "purple": - return Colors.Purple; - case "YELLOW": - case "yellow": - return Colors.Yellow; - case "PINK": - case "pink": - return Colors.Pink; - default: - return parseInt(color) || 0; - } + //% blockHidden=true deprecated + export function fade(c: number, brightness: number): number { + return color.fade(c, brightness); } /** @@ -1407,7 +1257,7 @@ namespace light { let hueOffset = 0; return () => { for (let i = 0; i < n; i++) { - strip.setPixelColor(i, hsv(((i * 256) / (n - 1) + hueOffset) % 0xff, 0xff, 0xff)); + strip.setPixelColor(i, color.hsv(((i * 256) / (n - 1) + hueOffset) % 0xff, 0xff, 0xff)); } hueOffset += Math.ceil(128 / n); if (hueOffset >= 0xff) { @@ -1447,7 +1297,7 @@ namespace light { step++; for (let i = 0; i < l; i++) { const level = (Math.isin(i + step) * 127) + 128; - strip.setPixelColor(i, rgb(level * this.red / 255, level * this.green / 255, level * this.blue / 255)); + strip.setPixelColor(i, color.rgb(level * this.red / 255, level * this.green / 255, level * this.blue / 255)); } iteration++; return true; @@ -1489,7 +1339,7 @@ namespace light { return () => { for (let i = 0; i < l; i++) { offsets[i] = (offsets[i] + (step * 2)) % 255 - strip.setPixelColor(i, rgb(255 - offsets[i], this.green, this.blue)); + strip.setPixelColor(i, color.rgb(255 - offsets[i], this.green, this.blue)); } step++; if (step * 2 > 0xff) { @@ -1510,7 +1360,7 @@ namespace light { constructor(red: number, green: number, blue: number, delay: number) { super(); - this.rgb = rgb(red, green, blue); + this.rgb = color.rgb(red, green, blue); this.delay = delay; } @@ -1587,7 +1437,7 @@ namespace light { constructor(red: number, green: number, blue: number, delay: number) { super(); - this.rgb = rgb(red, green, blue); + this.rgb = color.rgb(red, green, blue); this.delay = delay; } diff --git a/libs/light/pxt.json b/libs/light/pxt.json index 2ea18f8a..5575b44a 100644 --- a/libs/light/pxt.json +++ b/libs/light/pxt.json @@ -22,6 +22,7 @@ "public": true, "dependencies": { "core": "file:../core", + "color": "file:../color", "jacdac": "file:../jacdac" } } diff --git a/libs/palette/README.md b/libs/palette/README.md new file mode 100644 index 00000000..71e2d63c --- /dev/null +++ b/libs/palette/README.md @@ -0,0 +1,3 @@ +# Palette + +Helpers to manipulate palette in games. \ No newline at end of file diff --git a/libs/palette/palette.ts b/libs/palette/palette.ts new file mode 100644 index 00000000..b6e006fd --- /dev/null +++ b/libs/palette/palette.ts @@ -0,0 +1,60 @@ +/** + * Update the current scene palette + */ +namespace palette { + /** + * The default palette buffer for the project + */ + //% whenUsed + const defaultPaletteBuffer = hex`__palette` + + /** + * Returns a clone of the default palette + */ + export function defaultPalette(): color.ColorBuffer { + return new color.ColorBuffer(defaultPaletteBuffer.slice()); + } + + const FIELD = "__palette"; + /** + * Dynamically set all or part of the game's current palette + * + * @param palette The colors to set + * @param pOffset The offset to start copying from the palette + */ + export function setColors(palette: color.ColorBuffer, pOffset = 0) { + const scene = game.currentScene(); + let userPalette = scene.data[FIELD] as color.ColorBuffer; + if (!userPalette) + userPalette = scene.data[FIELD] = defaultPalette(); + userPalette.write(pOffset, palette); + image.setPalette(userPalette.buf); + + // make sure to clean up + game.addScenePushHandler(scenePush); + game.addScenePopHandler(scenePop); + } + + function scenePush(scene: scene.Scene) { + if (scene.data[FIELD]) { + const userPalette = scene.data[FIELD] as color.ColorBuffer; + image.setPalette(userPalette.buf); + } + } + + function scenePop(scene: scene.Scene) { + if (scene.data[FIELD]) { + scene.data[FIELD] = undefined; + image.setPalette(defaultPaletteBuffer); + } + } + + /** + * Reset to default palette + */ + export function reset() { + const scene = game.currentScene(); + scene.data[FIELD] = undefined; + image.setPalette(defaultPaletteBuffer); + } +} diff --git a/libs/palette/pxt.json b/libs/palette/pxt.json new file mode 100644 index 00000000..4a9996be --- /dev/null +++ b/libs/palette/pxt.json @@ -0,0 +1,17 @@ +{ + "name": "palette", + "description": "Palette manipulations", + "files": [ + "palette.ts", + "README.md" + ], + "testFiles": [ + "test.ts" + ], + "public": true, + "weight": 10, + "dependencies": { + "color": "file:../color", + "game": "file:../game" + } +} diff --git a/libs/palette/test.ts b/libs/palette/test.ts new file mode 100644 index 00000000..08c66887 --- /dev/null +++ b/libs/palette/test.ts @@ -0,0 +1,21 @@ + +let mySprite = sprites.create(img` +0 1 2 3 +4 5 6 7 +8 9 a b +c d e f +`.doubled().doubled().doubled().doubled(), SpriteKind.Player) + +controller.A.onEvent(ControllerButtonEvent.Pressed, function () { + const p = palette.defaultPalette(); + for (let i = 0; i < p.length; ++i) { + p.setColor(i, color.rgb(i * 16, 0, 255 - i * 16)); + } + p.setColor(0, 0) + palette.setColors(p) +}) + +controller.B.onEvent(ControllerButtonEvent.Pressed, function () { + palette.reset() +}) + diff --git a/libs/storyboard/README.md b/libs/storyboard/README.md new file mode 100644 index 00000000..ebde964a --- /dev/null +++ b/libs/storyboard/README.md @@ -0,0 +1,3 @@ +# Storyboard + +Orchestrate scenes \ No newline at end of file diff --git a/libs/storyboard/loader.ts b/libs/storyboard/loader.ts new file mode 100644 index 00000000..e79240de --- /dev/null +++ b/libs/storyboard/loader.ts @@ -0,0 +1,24 @@ +namespace storyboard { + function loader(done: () => void) { + const font = image.font8; + let m = 40; + let w = screen.width - 2 * m; + let c = 2; + let y = screen.height / 2 - c; + let x = 0; + game.onPaint(function() { + screen.printCenter("MakeCode Arcade", y - font.charHeight - c, 1, font); + screen.drawRect(m, y, w, 2 * c, 1) + screen.fillRect(m, y + 1, x, 2 * c - 2, 3); + + x++; + if (x == w) done(); + }) + } + + /** + * Default boot sequence + */ + //% block="loader" fixedInstance whenUsed + export const loaderBootSequence = new BootSequence(loader, 0); +} \ No newline at end of file diff --git a/libs/storyboard/pxt.json b/libs/storyboard/pxt.json new file mode 100644 index 00000000..6f1713fe --- /dev/null +++ b/libs/storyboard/pxt.json @@ -0,0 +1,17 @@ +{ + "name": "storyboard", + "description": "Scene manager", + "files": [ + "storyboard.ts", + "loader.ts", + "README.md" + ], + "testFiles": [ + "test.ts" + ], + "public": true, + "weight": 10, + "dependencies": { + "game": "file:../game" + } +} diff --git a/libs/storyboard/storyboard.ts b/libs/storyboard/storyboard.ts new file mode 100644 index 00000000..fc1ce717 --- /dev/null +++ b/libs/storyboard/storyboard.ts @@ -0,0 +1,156 @@ +/** + * A manager of scenes + */ +//% icon="\uf009" +//% weight=87 color="#401255" +namespace storyboard { + export interface FrameOptions { + background?: number; + } + + export class Frame { + start: () => void; + options: FrameOptions; + + constructor(start: () => void, options: FrameOptions) { + this.start = start; + this.options = options || {}; + } + } + + //% fixedInstances + export class BootSequence { + start: (done: () => void) => void; + background: number; + constructor(start: (done: () => void) => void, background: number) { + this.start = start; + this.background = background; + } + + /** + * Registers the boot sequence + */ + //% block="storyboard register %boot| boot sequence" blockId=storyboardregister + register() { + registerBootSequence(this); + } + } + + let _boots: BootSequence[]; + let _scenes: { + [index: string]: Frame + }; + let _nav: Frame[]; + + function registerBootSequence(boot: BootSequence) { + if (!_boots) + _boots = []; + if (_boots.indexOf(boot) < 0) + _boots.push(boot); + } + + /** + * Registers a scene + * @param name + * @param scene + */ + export function registerScene(name: string, start: () => void, options?: FrameOptions) { + if (!name) return; + if (!_scenes) { + _scenes = {}; + } + _scenes[name] = new Frame(start, options); + } + + function consumeBootSequence() { + // run boot sequences if any + let boot: BootSequence; + while (boot = _boots && _boots.shift()) { + game.pushScene(); + let isDone = false; + boot.start(() => isDone = true); + pauseUntil(() => isDone); + game.popScene(); + } + } + + /** + * Starts the story board by running boot sequences then entering a scene + * @param name + */ + //% block="storyboard start at $name" blockId=storyboardstart + export function start(name?: string) { + consumeBootSequence(); + // grab the first frame + push(name || (_scenes && Object.keys(_scenes)[0])); + } + + function isActive(name: string): boolean { + const scene = name && _scenes && _scenes[name]; + return scene && (_nav && _nav.length && _nav[_nav.length - 1] == scene); + } + + /** + * Replace the current scene with the given scene + * @param name + */ + //% block="storyboard replace scene $name" blockId=storyboardreplace + export function replace(name: string) { + if (isActive(name)) return; + + const scene = name && _scenes && _scenes[name]; + if (!scene) return; // not found + + if (!_nav) _nav = []; + if (_nav.length) { + console.log('drop current scene') + _nav.pop(); + game.popScene(); + } + console.log('replace scene') + _nav.push(scene); + game.pushScene(); + scene.start(); + } + + /** + * Transition to a registered scene + * @param name + */ + //% block="storyboard push scene $name" blockId=storyboardpush + export function push(name: string) { + if (isActive(name)) return; + + const scene = name && _scenes && _scenes[name]; + if (!scene) return; // not found + + if (!_nav) _nav = []; + if (_nav.length) { + console.log('drop scene') + game.popScene(); + } + console.log(`push ${name}`) + _nav.push(scene); + game.pushScene(); + scene.start(); + } + + /** + * Stops the current scene and restart the previous scene + */ + //% block="storyboard pop frame" blockId=storyboardpop + export function pop() { + const n = _nav && _nav.pop(); + if (n) { + console.log('pop scene') + game.popScene(); + } + // restart previous + if (_nav && _nav.length) { + console.log('restart scene') + const sc = _nav[_nav.length - 1]; + game.pushScene(); + sc.start(); + } + } +} \ No newline at end of file diff --git a/libs/storyboard/test.ts b/libs/storyboard/test.ts new file mode 100644 index 00000000..9c274041 --- /dev/null +++ b/libs/storyboard/test.ts @@ -0,0 +1,84 @@ +let mySprite = sprites.create(img` + . . . . . . b b b b a a . . . . + . . . . b b d d d 3 3 3 a a . . + . . . b d d d 3 3 3 3 3 3 a a . + . . b d d 3 3 3 3 3 3 3 3 3 a . + . b 3 d 3 3 3 3 3 b 3 3 3 3 a b + . b 3 3 3 3 3 a a 3 3 3 3 3 a b + b 3 3 3 3 3 a a 3 3 3 3 d a 4 b + b 3 3 3 3 b a 3 3 3 3 3 d a 4 b + b 3 3 3 3 3 3 3 3 3 3 d a 4 4 e + a 3 3 3 3 3 3 3 3 3 d a 4 4 4 e + a 3 3 3 3 3 3 3 d d a 4 4 4 e . + a a 3 3 3 d d d a a 4 4 4 e e . + . e a a a a a a 4 4 4 4 e e . . + . . e e b b 4 4 4 4 b e e . . . + . . . e e e e e e e e . . . . . + . . . . . . . . . . . . . . . . +`, SpriteKind.Player) +mySprite.x = 10 +controller.moveSprite(mySprite) +storyboard.loaderBootSequence.register() +storyboard.registerScene("lemon", function () { + let mySprite2 = sprites.create(img` + 4 4 4 . . 4 4 4 4 4 . . . . . . + 4 5 5 4 4 5 5 5 5 5 4 4 . . . . + b 4 5 5 1 5 1 1 1 5 5 5 4 . . . + . b 5 5 5 5 1 1 5 5 1 1 5 4 . . + . b d 5 5 5 5 5 5 5 5 1 1 5 4 . + b 4 5 5 5 5 5 5 5 5 5 5 1 5 4 . + c d 5 5 5 5 5 5 5 5 5 5 5 5 5 4 + c d 4 5 5 5 5 5 5 5 5 5 5 1 5 4 + c 4 5 5 5 d 5 5 5 5 5 5 5 5 5 4 + c 4 d 5 4 5 d 5 5 5 5 5 5 5 5 4 + . c 4 5 5 5 5 d d d 5 5 5 5 5 b + . c 4 d 5 4 5 d 4 4 d 5 5 5 4 c + . . c 4 4 d 4 4 4 4 4 d d 5 d c + . . . c 4 4 4 4 4 4 4 4 5 5 5 4 + . . . . c c b 4 4 4 b b 4 5 4 4 + . . . . . . c c c c c c b b 4 . + `, SpriteKind.Player) + mySprite2.y = 20 + controller.moveSprite(mySprite2) + controller.A.onEvent(ControllerButtonEvent.Pressed, function () { + storyboard.push("burger"); + }) + controller.B.onEvent(ControllerButtonEvent.Pressed, function () { + storyboard.pop(); + }) +}) +storyboard.registerScene("burger", function () { + let mySprite3 = sprites.create(img` + . . . . c c c b b b b b . . . . + . . c c b 4 4 4 4 4 4 b b b . . + . c c 4 4 4 4 4 5 4 4 4 4 b c . + . e 4 4 4 4 4 4 4 4 4 5 4 4 e . + e b 4 5 4 4 5 4 4 4 4 4 4 4 b c + e b 4 4 4 4 4 4 4 4 4 4 5 4 4 e + e b b 4 4 4 4 4 4 4 4 4 4 4 b e + . e b 4 4 4 4 4 5 4 4 4 4 b e . + 8 7 e e b 4 4 4 4 4 4 b e e 6 8 + 8 7 2 e e e e e e e e e e 2 7 8 + e 6 6 2 2 2 2 2 2 2 2 2 2 6 c e + e c 6 7 6 6 7 7 7 6 6 7 6 c c e + e b e 8 8 c c 8 8 c c c 8 e b e + e e b e c c e e e e e c e b e e + . e e b b 4 4 4 4 4 4 4 4 e e . + . . . c c c c c e e e e e . . . + `, SpriteKind.Player) + mySprite3.y = 80 + controller.moveSprite(mySprite3) + controller.A.onEvent(ControllerButtonEvent.Pressed, function () { + storyboard.replace("lemon"); + }) + controller.B.onEvent(ControllerButtonEvent.Pressed, function () { + storyboard.pop() + }) +}) +controller.A.onEvent(ControllerButtonEvent.Pressed, function () { + storyboard.start("lemon") +}) +controller.B.onEvent(ControllerButtonEvent.Pressed, function () { + storyboard.start("burger") +}) + diff --git a/pxtarget.json b/pxtarget.json index ad252452..1bf00650 100644 --- a/pxtarget.json +++ b/pxtarget.json @@ -45,6 +45,9 @@ "libs/esp32spi", "libs/net", "libs/mqtt", - "libs/azureiot" + "libs/azureiot", + "libs/color", + "libs/game", + "libs/storyboard" ] }