diff --git a/packages/webamp-modern-2/src/UIRoot.ts b/packages/webamp-modern-2/src/UIRoot.ts index 29dfe94e..ca009f06 100644 --- a/packages/webamp-modern-2/src/UIRoot.ts +++ b/packages/webamp-modern-2/src/UIRoot.ts @@ -5,6 +5,7 @@ import { assert, assume } from "./utils"; import BitmapFont from "./skin/BitmapFont"; import Color from "./skin/Color"; import AUDIO_PLAYER from "./skin/AudioPlayer"; +import GammaGroup from "./skin/GammaGroup"; class UIRoot { // Just a temporary place to stash things @@ -13,6 +14,7 @@ class UIRoot { _fonts: (TrueTypeFont | BitmapFont)[] = []; _colors: Color[] = []; _groupDefs: XmlElement[] = []; + _gammaSets: Map = new Map(); _xuiElements: XmlElement[] = []; addBitmap(bitmap: Bitmap) { @@ -52,13 +54,15 @@ class UIRoot { return found; } - getFont(id: string): TrueTypeFont | BitmapFont { + getFont(id: string): TrueTypeFont | BitmapFont | null { const found = this._fonts.find( (font) => font.getId().toLowerCase() === id.toLowerCase() ); - assert(found != null, `Could not find true type font with id ${id}.`); - return found; + if (found == null) { + console.warn(`Could not find true type font with id ${id}.`); + } + return found ?? null; } addGroupDef(groupDef: XmlElement) { @@ -77,6 +81,21 @@ class UIRoot { return found ?? null; } + addGammaSet(id: string, gammaSet: GammaGroup[]) { + this._gammaSets.set(id.toLowerCase(), gammaSet); + } + + getGammaSet(id: string): GammaGroup[] { + const found = this._gammaSets.get(id.toLowerCase()); + assume( + found != null, + `Could not find gammaset for id "${id}" from set of ${Array.from( + this._gammaSets.keys() + ).join(", ")}` + ); + return found; + } + getXuiElement(name: string): XmlElement | null { const lowercaseName = name.toLowerCase(); const found = this._xuiElements.find( diff --git a/packages/webamp-modern-2/src/index.ts b/packages/webamp-modern-2/src/index.ts index ff0b5afa..80ae678e 100644 --- a/packages/webamp-modern-2/src/index.ts +++ b/packages/webamp-modern-2/src/index.ts @@ -2,6 +2,8 @@ import JSZip from "jszip"; // This module is imported early here in order to avoid a circular dependency. import { classResolver } from "./skin/resolver"; import SkinParser from "./skin/parse"; +import UI_ROOT from "./UIRoot"; +import GammaGroup from "./skin/GammaGroup"; function hack() { // Without this Snowpack will try to treeshake out resolver causing a circular @@ -27,13 +29,55 @@ async function main() { node.appendChild(container.getDiv()); } + const gammaSet = UI_ROOT.getGammaSet("clean | orange (default)"); + + const div = document.createElement("div"); + div.innerHTML = makeGammaSetSVG(gammaSet); + document.body.appendChild(div); + document.body.appendChild(node); console.log("RENDER"); + window.color = function (name = "clean | lightblue") { + const gammaSet = UI_ROOT.getGammaSet(name); + + div.innerHTML = makeGammaSetSVG(gammaSet); + }; + for (const container of parser._containers) { container.init({ containers: parser._containers }); } console.log("INIT"); } +function makeGammaSetSVG(gammaSet: GammaGroup[]) { + // 4000,510,-4000 + return ` + + ${gammaSet.map((group) => makeGammaGroupSVG(group)).join("\n")} + +`; +} + +function colorToFrac(num: string) { + // -4096 to 4096 + return Number(num) / 4096; +} + +// https://webplatform.github.io/docs/svg/elements/feFuncA/ +// amplitude * pow(C, exponent) + offset (see below for amplitude, exponent, and offset) +// https://www.pawelporwisz.pl/winamp/wct_en.php +// TODO: Avoid XSS +function makeGammaGroupSVG(gammaGroup: GammaGroup) { + const [r, g, b] = gammaGroup._value.split(",").map(colorToFrac); + return ` + + + + + + +`; +} + main(); diff --git a/packages/webamp-modern-2/src/maki/interpreter.tsx b/packages/webamp-modern-2/src/maki/interpreter.tsx index 1e765672..a5ba23e3 100644 --- a/packages/webamp-modern-2/src/maki/interpreter.tsx +++ b/packages/webamp-modern-2/src/maki/interpreter.tsx @@ -564,11 +564,6 @@ class Interpreter { // ! (not) case 74: { const a = this.stack.pop(); - switch (a.type) { - case "STRING": - case "OBJECT": - throw new Error("Tried ! a string or object."); - } this.push(V.newInt(!a.value)); break; } diff --git a/packages/webamp-modern-2/src/skin/Bitmap.ts b/packages/webamp-modern-2/src/skin/Bitmap.ts index 3126b74d..e133cbc9 100644 --- a/packages/webamp-modern-2/src/skin/Bitmap.ts +++ b/packages/webamp-modern-2/src/skin/Bitmap.ts @@ -1,8 +1,10 @@ import * as Utils from "../utils"; -import { assert } from "../utils"; +import { assert, getId } from "../utils"; import ImageManager from "./ImageManager"; +// http://wiki.winamp.com/wiki/XML_Elements#.3Cbitmap.2F.3E export default class Bitmap { + _uniqueId: number = getId(); _id: string; _url: string; _img: HTMLImageElement; @@ -61,7 +63,14 @@ export default class Bitmap { return this._height; } + getCSSVar(): string { + return `--bitmap-${this.getId().replace(/[^a-zA-Z0-9]/g, "-")}-${ + this._uniqueId + }`; + } + setUrl(url: string) { + document.documentElement.style.setProperty(this.getCSSVar(), `url(${url})`); this._url = url; } @@ -85,7 +94,7 @@ export default class Bitmap { } getBackgrondImageCSSAttribute(): string { - return `url(${this._url})`; + return `var(${this.getCSSVar()})`; } getBackgrondPositionCSSAttribute(): string { @@ -100,6 +109,10 @@ export default class Bitmap { return `${width} ${height}`; } + getBackdropFilterCSSAttribute(): string { + return `url(#${Utils.normalizeDomId(this._gammagroup)})`; + } + getCanvas(): HTMLCanvasElement { if (this._canvas == null) { assert(this._img != null, "Expected bitmap image to be loaded"); diff --git a/packages/webamp-modern-2/src/skin/Button.ts b/packages/webamp-modern-2/src/skin/Button.ts index 474b0624..c1489e3c 100644 --- a/packages/webamp-modern-2/src/skin/Button.ts +++ b/packages/webamp-modern-2/src/skin/Button.ts @@ -77,11 +77,15 @@ export default class Button extends GuiObj { if (this._image != null) { const bitmap = UI_ROOT.getBitmap(this._image); this.setBackgroundImage(bitmap); + } else { + this.setBackgroundImage(null); } if (this._downimage != null) { const downBitmap = UI_ROOT.getBitmap(this._downimage); this.setActiveBackgroundImage(downBitmap); + } else { + this.setActiveBackgroundImage(null); } } diff --git a/packages/webamp-modern-2/src/skin/GammaGroup.ts b/packages/webamp-modern-2/src/skin/GammaGroup.ts new file mode 100644 index 00000000..96eb0b21 --- /dev/null +++ b/packages/webamp-modern-2/src/skin/GammaGroup.ts @@ -0,0 +1,46 @@ +import { normalizeDomId, num, toBool } from "../utils"; + +export default class GammaGroup { + _id: string; + _value: string; + _boost: number = 0; + _gray: number = 0; + + 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 "boost": + this._boost = num(value); + break; + case "gray": + this._gray = num(value); + default: + return false; + } + return true; + } + + getId() { + return this._id; + } + + getDomId() { + return normalizeDomId(this._id); + } + + getRgb() { + return `rgb(${this._value})`; + } +} diff --git a/packages/webamp-modern-2/src/skin/Group.ts b/packages/webamp-modern-2/src/skin/Group.ts index effc904a..ccfa91a9 100644 --- a/packages/webamp-modern-2/src/skin/Group.ts +++ b/packages/webamp-modern-2/src/skin/Group.ts @@ -117,8 +117,9 @@ export default class Group extends GuiObj { if (this._background != null && this._drawBackground) { const bitmap = UI_ROOT.getBitmap(this._background); this.setBackgroundImage(bitmap); + } else { + this.setBackgroundImage(null); } - // TODO: Clear background } draw() { diff --git a/packages/webamp-modern-2/src/skin/GuiObj.ts b/packages/webamp-modern-2/src/skin/GuiObj.ts index 2335cbb3..06bebc94 100644 --- a/packages/webamp-modern-2/src/skin/GuiObj.ts +++ b/packages/webamp-modern-2/src/skin/GuiObj.ts @@ -217,6 +217,22 @@ export default class GuiObj extends XmlObj { ]); } + /** + * Hookable. Event happens when the mouse + * enters the objects area. + */ + onEnterArea() { + VM.dispatch(this, "onenterarea"); + } + + /** + * Hookable. Event happens when the mouse + * leaves the objects area. + */ + onLeaveArea() { + VM.dispatch(this, "onleavearea"); + } + /** * Set a target X position, in the screen, for * the object. @@ -359,28 +375,29 @@ export default class GuiObj extends XmlObj { this._renderHeight(); } - setBackgroundImage(bitmap: Bitmap) { + setBackgroundImage(bitmap: Bitmap | null) { this._div.style.setProperty( "--background-image", - bitmap.getBackgrondImageCSSAttribute() + bitmap?.getBackgrondImageCSSAttribute() ?? "none" ); this._div.style.setProperty( "--background-position", - bitmap.getBackgrondPositionCSSAttribute() + bitmap?.getBackgrondPositionCSSAttribute() ?? "none" ); + this._div.style.filter = bitmap?.getBackdropFilterCSSAttribute() ?? "none"; } // JS Can't set the :active pseudo selector. Instead we have a hard-coded // pseduo-selector in our stylesheet which references a CSS variable and then // we control the value of that variable from JS. - setActiveBackgroundImage(bitmap: Bitmap) { + setActiveBackgroundImage(bitmap: Bitmap | null) { this._div.style.setProperty( "--active-background-image", - bitmap.getBackgrondImageCSSAttribute() + bitmap?.getBackgrondImageCSSAttribute() ?? "none" ); this._div.style.setProperty( "--active-background-position", - bitmap.getBackgrondPositionCSSAttribute() + bitmap?.getBackgrondPositionCSSAttribute() ?? "none" ); } @@ -406,5 +423,12 @@ export default class GuiObj extends XmlObj { this._div.addEventListener("mousedown", (e) => { this.onLeftButtonDown(e.clientX, e.clientY); }); + this._div.addEventListener("mouseenter", (e) => { + this.onEnterArea(); + }); + + this._div.addEventListener("mouseleave", (e) => { + this.onLeaveArea(); + }); } } diff --git a/packages/webamp-modern-2/src/skin/Slider.ts b/packages/webamp-modern-2/src/skin/Slider.ts index 977e579d..0badc15d 100644 --- a/packages/webamp-modern-2/src/skin/Slider.ts +++ b/packages/webamp-modern-2/src/skin/Slider.ts @@ -1,5 +1,5 @@ import UI_ROOT from "../UIRoot"; -import { clamp, num, px } from "../utils"; +import { assume, clamp, num, px } from "../utils"; import GuiObj from "./GuiObj"; import { VM } from "./VM"; @@ -129,8 +129,6 @@ export default class Slider extends GuiObj { const startX = rect.x; const startY = rect.y; const width = this.getwidth(); - const startMouseX = downEvent.clientX; - const startMouseY = downEvent.clientY; const handleMove = (moveEvent: MouseEvent) => { const newMouseX = moveEvent.clientX; @@ -138,16 +136,23 @@ export default class Slider extends GuiObj { const deltaX = newMouseX - startX; const deltaY = newMouseY - startY; + // TODO: What about vertical sliders? const xPos = clamp(deltaX, 0, width); this._position = xPos / width; this._renderThumbPosition(); this.onsetposition(this.getposition()); }; - function handleMouseUp() { + const handleMouseUp = () => { + VM.dispatch(this, "onsetfinalposition", [ + { type: "INT", value: this.getposition() }, + ]); + VM.dispatch(this, "onpostedposition", [ + { type: "INT", value: this.getposition() }, + ]); document.removeEventListener("mousemove", handleMove); document.removeEventListener("mouseup", handleMouseUp); - } + }; document.addEventListener("mousemove", handleMove); document.addEventListener("mouseup", handleMouseUp); }); @@ -156,6 +161,9 @@ export default class Slider extends GuiObj { draw() { super.draw(); this._div.setAttribute("data-obj-name", "Slider"); + assume(this._barLeft == null, "Need to handle Slider barleft"); + assume(this._barRight == null, "Need to handle Slider barright"); + assume(this._barMiddle == null, "Need to handle Slider barmiddle"); this._renderThumb(); this._renderThumbPosition(); this._div.appendChild(this._thumbDiv); diff --git a/packages/webamp-modern-2/src/skin/Text.ts b/packages/webamp-modern-2/src/skin/Text.ts index c975844f..ed353b26 100644 --- a/packages/webamp-modern-2/src/skin/Text.ts +++ b/packages/webamp-modern-2/src/skin/Text.ts @@ -94,7 +94,11 @@ offsety - (int) Extra pixels to be added to or subtracted from the calculated x getText() { if (this._display) { - switch (this._display) { + switch (this._display.toLowerCase()) { + case "pe_info": + return "pe_info"; + case "vid_info": + return "vid_info"; case "time": return "01:58"; case "songlength": @@ -140,6 +144,9 @@ offsety - (int) Extra pixels to be added to or subtracted from the calculated x } // + } else if (font == null) { + this._div.innerText = this.getText(); + this._div.style.fontFamily = "Ariel"; } else { throw new Error("Unexpected font"); } diff --git a/packages/webamp-modern-2/src/skin/parse.ts b/packages/webamp-modern-2/src/skin/parse.ts index 0f3e8e89..50150555 100644 --- a/packages/webamp-modern-2/src/skin/parse.ts +++ b/packages/webamp-modern-2/src/skin/parse.ts @@ -21,6 +21,7 @@ import AnimatedLayer from "./AnimatedLayer"; import Vis from "./Vis"; import BitmapFont from "./BitmapFont"; import Color from "./Color"; +import GammaGroup from "./GammaGroup"; class ParserContext { container: Container | null = null; @@ -33,6 +34,7 @@ export default class SkinParser { _path: string[] = []; _context: ParserContext = new ParserContext(); _containers: Container[] = []; + _gammaSet: GammaGroup[] = []; constructor(zip: JSZip) { this._zip = zip; @@ -409,11 +411,19 @@ export default class SkinParser { } async gammaset(node: XmlElement) { + this._gammaSet = []; await this.traverseChildren(node); + UI_ROOT.addGammaSet(node.attributes.id, this._gammaSet); } async gammagroup(node: XmlElement) { - await this.traverseChildren(node); + assume( + node.children.length === 0, + "Unexpected children in XML node." + ); + const gammaGroup = new GammaGroup(); + gammaGroup.setXmlAttributes(node.attributes); + this._gammaSet.push(gammaGroup); } async component(node: XmlElement) { diff --git a/packages/webamp-modern-2/src/utils.ts b/packages/webamp-modern-2/src/utils.ts index af0988a4..0ef24cf5 100644 --- a/packages/webamp-modern-2/src/utils.ts +++ b/packages/webamp-modern-2/src/utils.ts @@ -31,7 +31,10 @@ export function px(size: number): string { } export function toBool(str: string) { - assert(str === "0" || str === "1", 'Expected bool value to be "0" or "1".'); + assert( + str === "0" || str === "1", + `Expected bool value to be "0" or "1", but it was "${str}".` + ); return str === "1"; } @@ -48,3 +51,7 @@ export function ensureVmInt(num: number): number { export function clamp(num: number, min: number, max: number): number { return Math.max(min, Math.min(num, max)); } + +export function normalizeDomId(id: string) { + return id.replace(/[^a-zA-Z0-9]/g, "-"); +}