diff --git a/packages/webamp-modern-2/assets/.gitignore b/packages/webamp-modern-2/assets/.gitignore new file mode 100644 index 00000000..25941ab3 --- /dev/null +++ b/packages/webamp-modern-2/assets/.gitignore @@ -0,0 +1 @@ +CornerAmp_Redux/ \ No newline at end of file diff --git a/packages/webamp-modern-2/assets/CornerAmp_Redux.zip b/packages/webamp-modern-2/assets/CornerAmp_Redux.zip new file mode 100644 index 00000000..7b7f2166 Binary files /dev/null and b/packages/webamp-modern-2/assets/CornerAmp_Redux.zip differ diff --git a/packages/webamp-modern-2/src/UIRoot.ts b/packages/webamp-modern-2/src/UIRoot.ts index 1c515214..419e1de8 100644 --- a/packages/webamp-modern-2/src/UIRoot.ts +++ b/packages/webamp-modern-2/src/UIRoot.ts @@ -1 +1,24 @@ -export default class UIRoot {} +import Bitmap from "./skin/Bitmap"; +import { assert } from "./utils"; + +class UIRoot { + // Just a temporary place to stash things + _bitmaps: Bitmap[] = []; + + addBitmap(bitmap: Bitmap) { + this._bitmaps.push(bitmap); + } + + getBitmap(id: string): Bitmap { + const found = this._bitmaps.find( + (bitmap) => bitmap._id.toLowerCase() === id.toLowerCase() + ); + + assert(found != null, `Could not find bitmap with id ${id}.`); + return found; + } +} + +// Global Singleton for now +const UI_ROOT = new UIRoot(); +export default UI_ROOT; diff --git a/packages/webamp-modern-2/src/index.ts b/packages/webamp-modern-2/src/index.ts index f6a49498..49e6db89 100644 --- a/packages/webamp-modern-2/src/index.ts +++ b/packages/webamp-modern-2/src/index.ts @@ -9,6 +9,10 @@ async function main() { const parser = new SkinParser(zip); await parser.parse(); + + for (const container of parser._containers) { + document.body.appendChild(container.getDebugDom()); + } } main(); diff --git a/packages/webamp-modern-2/src/skin/Bitmap.ts b/packages/webamp-modern-2/src/skin/Bitmap.ts index e2a4eefc..29786aba 100644 --- a/packages/webamp-modern-2/src/skin/Bitmap.ts +++ b/packages/webamp-modern-2/src/skin/Bitmap.ts @@ -1,3 +1,5 @@ +import * as Utils from "../utils"; +import ImageManager from "./ImageManager"; export default class Bitmap { _id: string; _url: string; @@ -5,27 +7,74 @@ export default class Bitmap { _y: number; _width: number; _height: number; + _file: string; + _gammagroup: string; - constructor({ - id, - url, - x, - y, - width, - height, - }: { - id: string; - url: string; - x: number; - y: number; - width: number; - height: number; - }) { - this._id = id; + setXmlAttributes(attributes: { [attrName: string]: string }) { + for (const [key, value] of Object.entries(attributes)) { + this.setXmlAttr(key, value); + } + } + + setXmlAttr(key: string, value: string) { + switch (key) { + case "id": + this._id = value; + break; + case "x": + this._x = Utils.num(value) ?? 0; + break; + case "y": + this._y = Utils.num(value) ?? 0; + break; + case "w": + this._width = Utils.num(value); + break; + case "h": + this._height = Utils.num(value); + break; + case "file": + this._file = value; + case "gammagroup": + this._gammagroup = value; + break; + default: + console.log({ key }); + } + } + + getWidth() { + return this._width; + } + + getHeight() { + return this._height; + } + + setUrl(url: string) { this._url = url; - this._x = x; - this._y = y; - this._width = width; - this._height = height; + } + + // Ensure we've loaded the image into our image loader. + async ensureImageLoaded(imageManager: ImageManager) { + Utils.assert( + this._url == null, + "Tried to ensure a Bitmap was laoded more than once." + ); + const imgUrl = await imageManager.getUrl(this._file); + + if (this._width == null && this._height == null) { + const size = await imageManager.getSize(imgUrl); + this.setXmlAttr("w", String(size.width)); + this.setXmlAttr("h", String(size.height)); + } + + this.setUrl(imgUrl); + } + + getBackgrondCSSAttribute(): string { + const width = Utils.px(this._width); + const height = Utils.px(this._height); + return `url(${this._url}) ${width} ${height}`; } } diff --git a/packages/webamp-modern-2/src/skin/Container.ts b/packages/webamp-modern-2/src/skin/Container.ts new file mode 100644 index 00000000..6a14f078 --- /dev/null +++ b/packages/webamp-modern-2/src/skin/Container.ts @@ -0,0 +1,34 @@ +import Group from "./Group"; +import Layout from "./Layout"; +import XmlObj from "./XmlObj"; + +export default class Container extends XmlObj { + _layouts: Layout[] = []; + constructor() { + super(); + } + + setXmlAttr(key: string, value: string): boolean { + if (super.setXmlAttr(key, value)) { + return true; + } + switch (key) { + default: + return false; + } + return true; + } + + addLayout(layout: Layout) { + layout.setParent(this); + this._layouts.push(layout); + } + + getDebugDom(): HTMLDivElement { + const div = window.document.createElement("div"); + for (const layout of this._layouts) { + div.appendChild(layout.getDebugDom()); + } + return div; + } +} diff --git a/packages/webamp-modern-2/src/skin/Group.ts b/packages/webamp-modern-2/src/skin/Group.ts new file mode 100644 index 00000000..bcfca8a4 --- /dev/null +++ b/packages/webamp-modern-2/src/skin/Group.ts @@ -0,0 +1,54 @@ +import * as Utils from "../utils"; +import UI_ROOT from "../UIRoot"; +import GuiObj from "./GuiObj"; + +export default class Group extends GuiObj { + _background: string; + _desktopAlpha: boolean; + _drawBackground: boolean; + _minimumHeight: number; + _maximumHeight: number; + _minimumWidth: number; + _maximumWidth: number; + + // TODO: Some of these might belong back on Layout + setXmlAttr(key: string, value: string): boolean { + if (super.setXmlAttr(key, value)) { + return true; + } + switch (key) { + case "background": + this._background = value; + break; + case "drawbackground": + this._drawBackground = Utils.toBool(value); + break; + case "minimum_h": + this._minimumHeight = Utils.num(value); + break; + case "minimum_w": + this._minimumWidth = Utils.num(value); + break; + case "maximum_h": + this._maximumHeight = Utils.num(value); + break; + case "maximum_w": + this._maximumWidth = Utils.num(value); + break; + default: + return false; + } + return true; + } + + getDebugDom(): HTMLDivElement { + const div = super.getDebugDom(); + div.style.height = Utils.px(this._maximumHeight); + div.style.width = Utils.px(this._maximumWidth); + if (this._background != null) { + const bitmap = UI_ROOT.getBitmap(this._background); + div.style.background = bitmap.getBackgrondCSSAttribute(); + } + return div; + } +} diff --git a/packages/webamp-modern-2/src/skin/GuiObj.ts b/packages/webamp-modern-2/src/skin/GuiObj.ts new file mode 100644 index 00000000..2e8e9559 --- /dev/null +++ b/packages/webamp-modern-2/src/skin/GuiObj.ts @@ -0,0 +1,56 @@ +import * as Utils from "../utils"; +import XmlObj from "./XmlObj"; + +export default class GuiObj extends XmlObj { + _id: string; + _width: number; + _height: number; + _x: number = 0; + _y: number = 0; + _droptarget: string; + + setXmlAttr(key: string, value: string): boolean { + switch (key) { + case "id": + this._id = value; + break; + case "w": + this._width = Utils.num(value); + break; + case "h": + this._height = Utils.num(value); + break; + case "x": + this._x = Utils.num(value) ?? 0; + break; + case "y": + this._y = Utils.num(value) ?? 0; + break; + case "droptarget": + this._droptarget = value; + break; + default: + return false; + } + return true; + } + + getDebugDom(): HTMLDivElement { + const div = window.document.createElement("div"); + div.style.display = "inline-block"; + div.style.position = "absolute"; + if (this._x) { + div.style.left = Utils.px(this._x); + } + if (this._y) { + div.style.top = Utils.px(this._y); + } + if (this._width) { + div.style.width = Utils.px(this._width); + } + if (this._height) { + div.style.height = Utils.px(this._height); + } + return div; + } +} diff --git a/packages/webamp-modern-2/src/skin/Layer.ts b/packages/webamp-modern-2/src/skin/Layer.ts new file mode 100644 index 00000000..987c371e --- /dev/null +++ b/packages/webamp-modern-2/src/skin/Layer.ts @@ -0,0 +1,35 @@ +import GuiObj from "./GuiObj"; +import UI_ROOT from "../UIRoot"; +import { px } from "../utils"; + +export default class Layer extends GuiObj { + _image: string; + + setXmlAttr(key: string, value: string): boolean { + if (super.setXmlAttr(key, value)) { + return true; + } + switch (key) { + case "image": + this._image = value; + default: + return false; + } + return true; + } + + getDebugDom(): HTMLDivElement { + const div = super.getDebugDom(); + if (this._image != null) { + const bitmap = UI_ROOT.getBitmap(this._image); + div.style.background = bitmap.getBackgrondCSSAttribute(); + if (div.style.width === "" && bitmap.getWidth()) { + div.style.width = px(bitmap.getWidth()); + } + if (div.style.height === "" && bitmap.getHeight()) { + div.style.height = px(bitmap.getHeight()); + } + } + return div; + } +} diff --git a/packages/webamp-modern-2/src/skin/Layout.ts b/packages/webamp-modern-2/src/skin/Layout.ts new file mode 100644 index 00000000..f0d4684e --- /dev/null +++ b/packages/webamp-modern-2/src/skin/Layout.ts @@ -0,0 +1,41 @@ +import Group from "./Group"; +import * as Utils from "../utils"; +import Container from "./Container"; +import Layer from "./Layer"; + +export default class Layout extends Group { + _parent: Container | null = null; + // TODO: I don't think Layers are actually children of Layouts + _layers: Layer[] = []; + + setXmlAttr(key: string, value: string): boolean { + if (super.setXmlAttr(key, value)) { + return true; + } + switch (key) { + case "desktopalpha": + this._desktopAlpha = Utils.toBool(value); + break; + default: + return false; + } + return true; + } + + setParent(container: Container) { + this._parent = container; + } + + // TODO: I don't think this is right. + addLayer(layer: Layer) { + this._layers.push(layer); + } + + getDebugDom(): HTMLDivElement { + const div = super.getDebugDom(); + for (const layer of this._layers) { + div.appendChild(layer.getDebugDom()); + } + return div; + } +} diff --git a/packages/webamp-modern-2/src/skin/XmlObj.ts b/packages/webamp-modern-2/src/skin/XmlObj.ts new file mode 100644 index 00000000..850664ea --- /dev/null +++ b/packages/webamp-modern-2/src/skin/XmlObj.ts @@ -0,0 +1,17 @@ +import * as Utils from "../utils"; +import UI_ROOT from "../UIRoot"; + +export default class XmlObj { + _id: string; + _droptarget: string; + + setXmlAttributes(attributes: { [attrName: string]: string }) { + for (const [key, value] of Object.entries(attributes)) { + this.setXmlAttr(key, value); + } + } + + setXmlAttr(_key: string, _value: string): boolean { + return false; + } +} diff --git a/packages/webamp-modern-2/src/skin/parse.ts b/packages/webamp-modern-2/src/skin/parse.ts index af6546d2..690fb98d 100644 --- a/packages/webamp-modern-2/src/skin/parse.ts +++ b/packages/webamp-modern-2/src/skin/parse.ts @@ -1,20 +1,28 @@ -import parseXml, { XmlElement } from "@rgrove/parse-xml"; -import { assert, num, getCaseInsensitiveFile, px } from "../utils"; -import UIRoot from "../UIRoot"; +import parseXml, { XmlDocument, XmlElement } from "@rgrove/parse-xml"; +import { assert, num, getCaseInsensitiveFile, px, toBool } from "../utils"; +import UI_ROOT from "../UIRoot"; import JSZip, { JSZipObject } from "jszip"; import Bitmap from "./Bitmap"; import ImageManager from "./ImageManager"; +import Layout from "./Layout"; +import Group from "./Group"; +import Container from "./Container"; +import Layer from "./Layer"; + +class ParserContext { + container: Container | null = null; + layout: Layout | null = null; +} export default class SkinParser { _zip: JSZip; _imageManager: ImageManager; - _path: string[]; - _root: UIRoot; + _path: string[] = []; + _context: ParserContext = new ParserContext(); + _containers: Container[] = []; constructor(zip: JSZip) { this._zip = zip; - this._path = []; - this._root = new UIRoot(); this._imageManager = new ImageManager(zip); } async parse() { @@ -26,16 +34,16 @@ export default class SkinParser { // A different XML parser library might make this unnessesary. const parsed = parseXml(includedXml); - this.traverseChildren(parsed); + await this.traverseChildren(parsed); } - async traverseChildren(parent) { + async traverseChildren(parent: XmlElement | XmlDocument) { for (const child of parent.children) { if (child instanceof XmlElement) { await this.traverseChild(child); } } } - async traverseChild(node) { + async traverseChild(node: XmlElement) { switch (node.name.toLowerCase()) { case "wasabixml": return this.wasabiXml(node); @@ -119,6 +127,8 @@ export default class SkinParser { } async group(node: XmlElement) { + const group = new Group(); + group.setXmlAttributes(node.attributes); await this.traverseChildren(node); } @@ -127,46 +137,11 @@ export default class SkinParser { node.children.length === 0, "Unexpected children in XML node." ); - const { file } = node.attributes; - assert(file != null, "Expected bitmap node to have a `file` attribute"); + const bitmap = new Bitmap(); + bitmap.setXmlAttributes(node.attributes); + await bitmap.ensureImageLoaded(this._imageManager); - const imgUrl = await this._imageManager.getUrl(file); - assert(imgUrl != null, `Could not find bitmap at path ${file}`); - - const id = node.attributes.id; - const x = num(node.attributes.x) ?? 0; - const y = num(node.attributes.y) ?? 0; - let width = num(node.attributes.w); - let height = num(node.attributes.h); - - if (width == null || height == null) { - assert( - x != null && y != null, - "Expected images with unknown size to not have offsets." - ); - assert( - width == null && height == null, - "Expected both dimensions to be missing." - ); - const size = await this._imageManager.getSize(imgUrl); - width = size.width; - height = size.height; - } - - // prettier-ignore - const bitmap = new Bitmap({ url: imgUrl, id, x, y, width, height }); - - // TODO: Store this somewhere. For now, we can just show it. - const div = document.createElement("div"); - div.style.height = px(bitmap._height); - div.style.width = px(bitmap._width); - div.style.backgroundImage = `url(${bitmap._url})`; - div.style.backgroundPositionX = px(-bitmap._x); - div.style.backgroundPositionY = px(-bitmap._y); - div.style.display = "inline-block"; - div.style.imageRendering = "pixelated"; - - document.body.appendChild(div); + UI_ROOT.addBitmap(bitmap); } async text(node: XmlElement) { @@ -250,10 +225,25 @@ export default class SkinParser { } async layer(node: XmlElement) { + const layer = new Layer(); + layer.setXmlAttributes(node.attributes); + const { layout } = this._context; + if (layout == null) { + console.warn("FIXME: Expected to be within a "); + return; + } + layout.addLayer(layer); await this.traverseChildren(node); } async layout(node: XmlElement) { + const layout = new Layout(); + layout.setXmlAttributes(node.attributes); + const { container } = this._context; + assert(container != null, "Expected to be in a "); + container.addLayout(layout); + + this._context.layout = layout; await this.traverseChildren(node); } @@ -270,6 +260,10 @@ export default class SkinParser { } async container(node: XmlElement) { + const container = new Container(); + container.setXmlAttributes(node.attributes); + this._context.container = container; + this._containers.push(container); await this.traverseChildren(node); } @@ -349,7 +343,7 @@ export default class SkinParser { // A different XML parser library might make this unnessesary. const parsed = parseXml(`${includedXml}`); - this.traverseChildren(parsed); + await this.traverseChildren(parsed); for (const _dir of directories) { this._path.pop(); diff --git a/packages/webamp-modern-2/src/utils.ts b/packages/webamp-modern-2/src/utils.ts index 877a84c2..faf97bac 100644 --- a/packages/webamp-modern-2/src/utils.ts +++ b/packages/webamp-modern-2/src/utils.ts @@ -20,3 +20,8 @@ export function num(str: string | void): number | null { export function px(size: number): string { return `${size}px`; } + +export function toBool(str: string) { + assert(str === "0" || str === "1", 'Expected bool value to be "0" or "1".'); + return str === "1"; +}