From 07154fdf9c641bf942a3d330c9d5fea8b09f8747 Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Sun, 27 Jun 2021 14:05:42 -0700 Subject: [PATCH] Maki map, colors and animations --- packages/webamp-modern-2/src/UIRoot.ts | 20 ++++++ .../webamp-modern-2/src/skin/AnimatedLayer.ts | 65 ++++++++++++++++--- packages/webamp-modern-2/src/skin/Bitmap.ts | 20 ++++++ packages/webamp-modern-2/src/skin/Color.ts | 39 +++++++++++ packages/webamp-modern-2/src/skin/GuiObj.ts | 4 +- .../webamp-modern-2/src/skin/ImageManager.ts | 24 +++---- packages/webamp-modern-2/src/skin/MakiMap.ts | 34 +++++++--- packages/webamp-modern-2/src/skin/Text.ts | 13 +++- packages/webamp-modern-2/src/skin/VM.ts | 3 +- packages/webamp-modern-2/src/skin/parse.ts | 6 +- packages/webamp-modern-2/src/utils.ts | 5 ++ 11 files changed, 198 insertions(+), 35 deletions(-) create mode 100644 packages/webamp-modern-2/src/skin/Color.ts diff --git a/packages/webamp-modern-2/src/UIRoot.ts b/packages/webamp-modern-2/src/UIRoot.ts index 2f6ba55a..c1f62ea5 100644 --- a/packages/webamp-modern-2/src/UIRoot.ts +++ b/packages/webamp-modern-2/src/UIRoot.ts @@ -3,11 +3,14 @@ import { XmlElement } from "@rgrove/parse-xml"; import TrueTypeFont from "./skin/TrueTypeFont"; import { assert } from "./utils"; import BitmapFont from "./skin/BitmapFont"; +import Color from "./skin/Color"; class UIRoot { // Just a temporary place to stash things _bitmaps: Bitmap[] = []; + _bitmapImages: Map = new Map(); _fonts: (TrueTypeFont | BitmapFont)[] = []; + _colors: Color[] = []; _groupDefs: XmlElement[] = []; _xuiElements: XmlElement[] = []; @@ -26,9 +29,26 @@ class UIRoot { return found; } + addBitmapImage(id: string, image: HTMLImageElement) { + this._bitmapImages.set(id, image); + } + addFont(font: TrueTypeFont | BitmapFont) { this._fonts.push(font); } + addColor(color: Color) { + this._colors.push(color); + } + + getColor(id: string): Color { + const lowercaseId = id.toLowerCase(); + const found = this._colors.find( + (color) => color._id.toLowerCase() === lowercaseId + ); + + assert(found != null, `Could not find color with id ${id}.`); + return found; + } getFont(id: string): TrueTypeFont | BitmapFont { const found = this._fonts.find( diff --git a/packages/webamp-modern-2/src/skin/AnimatedLayer.ts b/packages/webamp-modern-2/src/skin/AnimatedLayer.ts index 99501daa..2d84ea42 100644 --- a/packages/webamp-modern-2/src/skin/AnimatedLayer.ts +++ b/packages/webamp-modern-2/src/skin/AnimatedLayer.ts @@ -1,8 +1,14 @@ +import UI_ROOT from "../UIRoot"; +import { ensureVmInt, px } from "../utils"; import Layer from "./Layer"; +import { VM } from "./VM"; export default class AnimatedLayer extends Layer { _currentFrame: number = 0; - _frameCount: number = 0; + _startFrame: number = 0; + _endFrame: number = 0; + _speed: number = 0; + _animationInterval: NodeJS.Timeout | null = null; setXmlAttr(_key: string, value: string): boolean { const key = _key.toLowerCase(); if (super.setXmlAttr(key, value)) { @@ -14,36 +20,74 @@ export default class AnimatedLayer extends Layer { } return true; } + + _getImageHeight(): number { + const bitmap = UI_ROOT.getBitmap(this._image); + return bitmap.getHeight(); + } + getlength(): number { - return this._frameCount; + // TODO: What about other orientations? + return this._getImageHeight() / this.getheight(); } gotoframe(framenum: number) { - this._currentFrame = framenum; + this._currentFrame = ensureVmInt(framenum); + this._renderFrame(); + // VM.dispatch(this, "onframe", [{ type: "INT", value: this._currentFrame }]); } getcurframe(): number { return this._currentFrame; } setstartframe(framenum: number) { - // TODO + this._startFrame = ensureVmInt(framenum); } setendframe(framenum: number) { - // TODO + this._endFrame = ensureVmInt(framenum); } setspeed(msperframe: number) { - // TODO + this._speed = msperframe; } play() { - // TODO + if (this._animationInterval != null) { + clearInterval(this._animationInterval); + this._animationInterval = null; + } + const end = this._endFrame; + const start = this._startFrame; + + const change = end > start ? 1 : -1; + + let frame = this._startFrame; + this.gotoframe(frame); + if (frame === end) { + return; + } + this._animationInterval = setInterval(() => { + frame += change; + this.gotoframe(frame); + if (frame === end) { + clearInterval(this._animationInterval); + this._animationInterval = null; + } + }, this._speed); } pause() { // TODO } stop() { - // TODO + if (this._animationInterval != null) { + clearInterval(this._animationInterval); + this._animationInterval = null; + } } isplaying(): boolean { - // TODO - return false; + return this._animationInterval != null; + } + + _renderFrame() { + this._div.style.backgroundPositionY = px( + -(this._currentFrame * this.getheight()) + ); } /* @@ -63,6 +107,7 @@ extern AnimatedLayer.setRealtime(Boolean onoff); draw() { super.draw(); + this._renderFrame(); this._div.setAttribute("data-obj-name", "AnimatedLayer"); } } diff --git a/packages/webamp-modern-2/src/skin/Bitmap.ts b/packages/webamp-modern-2/src/skin/Bitmap.ts index d34df2c3..1fe069ee 100644 --- a/packages/webamp-modern-2/src/skin/Bitmap.ts +++ b/packages/webamp-modern-2/src/skin/Bitmap.ts @@ -1,8 +1,12 @@ import * as Utils from "../utils"; +import { assert } from "../utils"; import ImageManager from "./ImageManager"; + export default class Bitmap { _id: string; _url: string; + _img: HTMLImageElement; + _canvas: HTMLCanvasElement; _x: number; _y: number; _width: number; @@ -69,6 +73,8 @@ export default class Bitmap { ); const imgUrl = await imageManager.getUrl(this._file); + this._img = await imageManager.getImage(imgUrl); + if (this._width == null && this._height == null) { const size = await imageManager.getSize(imgUrl); this.setXmlAttr("w", String(size.width)); @@ -93,4 +99,18 @@ export default class Bitmap { const height = Utils.px(this._height); return `${width} ${height}`; } + + getCanvas(): HTMLCanvasElement { + if (this._canvas == null) { + assert(this._img != null, "Expected bitmap image to be loaded"); + this._canvas = document.createElement("canvas"); + this._canvas.width = this.getWidth(); + this._canvas.height = this.getHeight(); + const ctx = this._canvas.getContext("2d"); + ctx.drawImage(this._img, 0, 0, this.getWidth(), this.getHeight()); + document.body.appendChild(this._img); + document.body.appendChild(this._canvas); + } + return this._canvas; + } } diff --git a/packages/webamp-modern-2/src/skin/Color.ts b/packages/webamp-modern-2/src/skin/Color.ts new file mode 100644 index 00000000..9f2c6e91 --- /dev/null +++ b/packages/webamp-modern-2/src/skin/Color.ts @@ -0,0 +1,39 @@ +import ImageManager from "./ImageManager"; + +export default class Color { + _id: string; + _value: string; + _gammagroup: string; + + setXmlAttributes(attributes: { [attrName: string]: string }) { + for (const [key, value] of Object.entries(attributes)) { + this.setXmlAttr(key, value); + } + } + + setXmlAttr(_key: string, value: string) { + const key = _key.toLowerCase(); + switch (key) { + case "id": + this._id = value; + break; + case "value": + this._value = value; + break; + case "gammagroup": + this._gammagroup = value; + break; + default: + return false; + } + return true; + } + + getId() { + return this._id; + } + + getRgb() { + return `rgb(${this._value})`; + } +} diff --git a/packages/webamp-modern-2/src/skin/GuiObj.ts b/packages/webamp-modern-2/src/skin/GuiObj.ts index dd05948d..3b048559 100644 --- a/packages/webamp-modern-2/src/skin/GuiObj.ts +++ b/packages/webamp-modern-2/src/skin/GuiObj.ts @@ -246,11 +246,11 @@ export default class GuiObj extends XmlObj { this._renderDimensions(); this._div.addEventListener("mouseup", (e) => { - this.onLeftButtonUp(e.clientX, e.clientX); + this.onLeftButtonUp(e.clientX, e.clientY); }); this._div.addEventListener("mousedown", (e) => { - this.onLeftButtonDown(e.clientX, e.clientX); + this.onLeftButtonDown(e.clientX, e.clientY); }); } } diff --git a/packages/webamp-modern-2/src/skin/ImageManager.ts b/packages/webamp-modern-2/src/skin/ImageManager.ts index 77a8c588..7f1f189f 100644 --- a/packages/webamp-modern-2/src/skin/ImageManager.ts +++ b/packages/webamp-modern-2/src/skin/ImageManager.ts @@ -3,10 +3,12 @@ import { getCaseInsensitiveFile } from "../utils"; export default class ImageManager { _urlCache: Map; + _imgCache: Map; _sizeCache: Map; constructor(private _zip: JSZip) { this._urlCache = new Map(); this._sizeCache = new Map(); + this._imgCache = new Map(); } async getUrl(filePath: string): Promise { @@ -24,11 +26,20 @@ export default class ImageManager { async getSize(url: string): Promise<{ width: number; height: number }> { if (!this._sizeCache.has(url)) { - const size = await getImageSize(url); + const size = await this.getImage(url); this._sizeCache.set(url, size); } return this._sizeCache.get(url); } + + async getImage(url: string): Promise { + if (!this._imgCache.has(url)) { + // TODO: We could cache this + const img = await loadImage(url); + this._imgCache.set(url, img); + } + return this._imgCache.get(url); + } } // This is intentionally async since we may want to sub it out for an async @@ -48,9 +59,7 @@ async function getUrlFromBlob(blob: Blob): Promise { }); } -async function loadImage( - imgUrl: string -): Promise<{ width: number; height: number }> { +async function loadImage(imgUrl: string): Promise { return new Promise((resolve, reject) => { const img = new Image(); img.addEventListener("load", () => { @@ -62,10 +71,3 @@ async function loadImage( img.src = imgUrl; }); } - -async function getImageSize( - imgUrl: string -): Promise<{ width: number; height: number }> { - const { width, height } = await loadImage(imgUrl); - return { width, height }; -} diff --git a/packages/webamp-modern-2/src/skin/MakiMap.ts b/packages/webamp-modern-2/src/skin/MakiMap.ts index 7008d151..2ec7ea14 100644 --- a/packages/webamp-modern-2/src/skin/MakiMap.ts +++ b/packages/webamp-modern-2/src/skin/MakiMap.ts @@ -1,23 +1,39 @@ +import UI_ROOT from "../UIRoot"; +import { assert, assume } from "../utils"; import BaseObject from "./BaseObject"; +import Bitmap from "./Bitmap"; export default class MakiMap extends BaseObject { - loadmap(bitmapId: string) {} + _bitmap: Bitmap; + loadmap(bitmapId: string) { + this._bitmap = UI_ROOT.getBitmap(bitmapId); + } inregion(x: number, y: number): boolean { - // TODO + // Maybe this checks if the pixel is transparent? return true; } + + // 0-255 getvalue(x: number, y: number): number { - // TODO - return 12345; + const canvas = this._bitmap.getCanvas(); + const context = canvas.getContext("2d"); + const { data } = context.getImageData(x, y, 1, 1); + assert( + data[0] === data[1] && data[0] === data[2], + "Expected map image to be grey scale" + ); + assume(data[3] === 255, "Expected map image not have transparency"); + return data[0]; + } + getwidth(): number { + return this._bitmap.getWidth(); + } + geheight(): number { + return this._bitmap.getHeight(); } /* -extern Int Map.getValue(int x, int y); extern Int Map.getARGBValue(int x, int y, int channel); // requires wa 5.51 // channel: 0=Blue, 1=Green, 2=Red, 3=Alpha. if your img has a alpha channal the returned rgb value might not be exact -extern Boolean Map.inRegion(int x, int y); -extern Map.loadMap(String bitmapid); -extern Int Map.getWidth(); -extern Int Map.getHeight(); extern Region Map.getRegion(); */ } diff --git a/packages/webamp-modern-2/src/skin/Text.ts b/packages/webamp-modern-2/src/skin/Text.ts index bfe37baa..fd701131 100644 --- a/packages/webamp-modern-2/src/skin/Text.ts +++ b/packages/webamp-modern-2/src/skin/Text.ts @@ -14,6 +14,7 @@ export default class Text extends GuiObj { _align: string; _font: string; _fontSize: number; + _color: string; setXmlAttr(key: string, value: string): boolean { if (super.setXmlAttr(key, value)) { return true; @@ -50,13 +51,16 @@ export default class Text extends GuiObj { case "fontsize": // (int) The size to render the chosen font. this._fontSize = Utils.num(value); + + case "color": + // (int[sic?]) The comma delimited RGB color of the text. + this._color = value; /* ticker - (bool) Setting this flag causes the object to scroll left and right if the text does not fit the rectangular area of the text object. antialias - (bool) Setting this flag causes the text to be rendered antialiased if possible. default - (str) A parameter alias for text. align - (str) One of the following three possible strings: "left" "center" "right" -- Default is "left." valign - (str) One of the following three possible strings: "top" "center" "bottom" -- Default is "top." -color - (int) The comma delimited RGB color of the text. shadowcolor - (int) The comma delimited RGB color for underrendered shadow text. shadowx - (int) The x offset of the shadowrender. shadowy - (int) The y offset of the shadowrender. @@ -121,6 +125,13 @@ offsety - (int) Extra pixels to be added to or subtracted from the calculated x this._div.style.fontFamily = font.getFontFamily(); } + if (this._color) { + console.log(this._color); + const color = UI_ROOT.getColor(this._color); + console.log({ color }); + this._div.style.color = color.getRbg(); + } + this._div.style.fontSize = Utils.px(this._fontSize ?? 14); } diff --git a/packages/webamp-modern-2/src/skin/VM.ts b/packages/webamp-modern-2/src/skin/VM.ts index 955998ab..dc25b041 100644 --- a/packages/webamp-modern-2/src/skin/VM.ts +++ b/packages/webamp-modern-2/src/skin/VM.ts @@ -16,7 +16,8 @@ class Vm { script.methods[binding.methodOffset].name === event && script.variables[binding.variableOffset].value === object ) { - this.interpret(scriptId, binding.commandOffset, args); + const reversedArgs = [...args].reverse(); + this.interpret(scriptId, binding.commandOffset, reversedArgs); } } } diff --git a/packages/webamp-modern-2/src/skin/parse.ts b/packages/webamp-modern-2/src/skin/parse.ts index 6190ec4d..0f3e8e89 100644 --- a/packages/webamp-modern-2/src/skin/parse.ts +++ b/packages/webamp-modern-2/src/skin/parse.ts @@ -20,6 +20,7 @@ import GuiObj from "./GuiObj"; import AnimatedLayer from "./AnimatedLayer"; import Vis from "./Vis"; import BitmapFont from "./BitmapFont"; +import Color from "./Color"; class ParserContext { container: Container | null = null; @@ -317,7 +318,10 @@ export default class SkinParser { "Unexpected children in XML node." ); - // TODO: Parse colors + const color = new Color(); + color.setXmlAttributes(node.attributes); + + UI_ROOT.addColor(color); } async slider(node: XmlElement) { diff --git a/packages/webamp-modern-2/src/utils.ts b/packages/webamp-modern-2/src/utils.ts index 63d54ed9..e24c77fc 100644 --- a/packages/webamp-modern-2/src/utils.ts +++ b/packages/webamp-modern-2/src/utils.ts @@ -39,3 +39,8 @@ let id = 0; export function getId(): number { return id++; } + +// TODO: Delete this once we have proper type coersion in the VM. +export function ensureVmInt(num: number): number { + return Math.floor(num); +}