diff --git a/packages/webamp-modern-2/src/UIRoot.ts b/packages/webamp-modern-2/src/UIRoot.ts index 0216bc16..d3e4b451 100644 --- a/packages/webamp-modern-2/src/UIRoot.ts +++ b/packages/webamp-modern-2/src/UIRoot.ts @@ -10,12 +10,12 @@ import GammaGroup from "./skin/GammaGroup"; class UIRoot { // Just a temporary place to stash things _bitmaps: Bitmap[] = []; - _bitmapImages: Map = new Map(); _fonts: (TrueTypeFont | BitmapFont)[] = []; _colors: Color[] = []; _groupDefs: XmlElement[] = []; _gammaSets: Map = new Map(); _xuiElements: XmlElement[] = []; + _activeGammaSet: GammaGroup[] | null = null; addBitmap(bitmap: Bitmap) { this._bitmaps.push(bitmap); @@ -32,10 +32,6 @@ class UIRoot { return found; } - addBitmapImage(id: string, image: HTMLImageElement) { - this._bitmapImages.set(id, image); - } - addFont(font: TrueTypeFont | BitmapFont) { this._fonts.push(font); } @@ -85,7 +81,7 @@ class UIRoot { this._gammaSets.set(id.toLowerCase(), gammaSet); } - getGammaSet(id: string): GammaGroup[] { + enableGammaSet(id: string) { const found = this._gammaSets.get(id.toLowerCase()); assume( found != null, @@ -93,10 +89,11 @@ class UIRoot { this._gammaSets.keys() ).join(", ")}` ); - return found; + this._activeGammaSet = found; + this._setCssVars(); } - getDefaultGammaSet(): GammaGroup[] { + enableDefaultGammaSet() { const found = Array.from(this._gammaSets.values())[0]; assume( found != null, @@ -104,9 +101,44 @@ class UIRoot { this._gammaSets.keys() ).join(", ")}` ); + this._activeGammaSet = found; + this._setCssVars(); + } + + _getGammaGroup(id: string): GammaGroup { + const lower = id.toLowerCase(); + const found = this._activeGammaSet.find((gammaGroup) => { + return gammaGroup.getId().toLowerCase() === lower; + }); + assume(found != null, `Cold not find a gammagroup for "${id}"`); return found; } + _setCssVars() { + const map = new Map(); + for (const bitmap of this._bitmaps) { + const img = bitmap.getImg(); + const groupId = bitmap.getGammaGroup(); + if (!map.has(img)) { + map.set(img, new Map()); + } + const imgCache = map.get(img); + if (!imgCache.has(groupId)) { + const gammaGroup = + groupId != null ? this._getGammaGroup(groupId) : null; + const url = + gammaGroup == null ? img.src : gammaGroup.transformImage(img); + imgCache.set(groupId, url); + } + const url = imgCache.get(groupId); + // TODO: Techincally we only need one per image/gammagroup. + document.documentElement.style.setProperty( + bitmap.getCSSVar(), + `url(${url})` + ); + } + } + getXuiElement(name: string): XmlElement | null { const lowercaseName = name.toLowerCase(); const found = this._xuiElements.find( diff --git a/packages/webamp-modern-2/src/index.html b/packages/webamp-modern-2/src/index.html index 5ed42b52..c1a3e406 100644 --- a/packages/webamp-modern-2/src/index.html +++ b/packages/webamp-modern-2/src/index.html @@ -10,27 +10,37 @@ margin: 0; background-color: rgb(58, 110, 165); } - .webamp--guiobj { + .webamp--img { background-image: var(--background-image); background-position: var(--background-position); } - .webamp--guiobj:active { + .webamp--img:active { background-image: var(--active-background-image, var(--background-image)); background-position: var(--active-background-position, var(--background-position)); } - .webamp--guiobj:hover { + .webamp--img:hover { background-image: var(--hover-background-image, var(--background-image)); background-position: var(--hover-background-position, var(--background-position)); } - .webamp--guiobj:hover:active { + /* TODO: Should this fallback to hover? */ + .webamp--img:hover:active { background-image: var(--active-background-image, var(--background-image)); background-position: var(--active-background-position, var(--background-position)); } + + #experimental { + position: absolute; + bottom: 0; + right: 0; + color: white; + text-transform: uppercase; + }
+
Work in Progress
\ No newline at end of file diff --git a/packages/webamp-modern-2/src/index.ts b/packages/webamp-modern-2/src/index.ts index 678a295a..8850b2f9 100644 --- a/packages/webamp-modern-2/src/index.ts +++ b/packages/webamp-modern-2/src/index.ts @@ -3,7 +3,6 @@ import JSZip from "jszip"; 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 @@ -24,15 +23,31 @@ async function main() { let node = document.createElement("div"); + UI_ROOT.enableDefaultGammaSet(); + for (const container of parser._containers) { container.draw(); node.appendChild(container.getDiv()); } - const gammaSet = UI_ROOT.getDefaultGammaSet(); + const select = document.createElement("select"); + select.style.position = "absolute"; + select.style.bottom = "0px"; + select.style.left = "0px"; + select.addEventListener("change", (e) => { + console.log(e.target.value); + UI_ROOT.enableGammaSet(e.target.value); + }); + for (const set of UI_ROOT._gammaSets.keys()) { + const option = document.createElement("option"); + option.innerText = set; + option.value = set; + select.appendChild(option); + } + + document.body.appendChild(select); const div = document.createElement("div"); - div.innerHTML = makeGammaSetSVG(gammaSet); document.body.appendChild(div); document.body.appendChild(node); @@ -44,34 +59,4 @@ async function main() { 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/skin/Bitmap.ts b/packages/webamp-modern-2/src/skin/Bitmap.ts index e133cbc9..9ce453dc 100644 --- a/packages/webamp-modern-2/src/skin/Bitmap.ts +++ b/packages/webamp-modern-2/src/skin/Bitmap.ts @@ -1,11 +1,10 @@ -import * as Utils from "../utils"; -import { assert, getId } from "../utils"; +import { assert, getId, normalizeDomId, num, px } 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; + _cssVar: string; _url: string; _img: HTMLImageElement; _canvas: HTMLCanvasElement; @@ -27,21 +26,26 @@ export default class Bitmap { switch (key) { case "id": this._id = value; + this._cssVar = `--bitmap-${this.getId().replace( + /[^a-zA-Z0-9]/g, + "-" + )}-${getId()}`; break; case "x": - this._x = Utils.num(value) ?? 0; + this._x = num(value) ?? 0; break; case "y": - this._y = Utils.num(value) ?? 0; + this._y = num(value) ?? 0; break; case "w": - this._width = Utils.num(value); + this._width = num(value); break; case "h": - this._height = Utils.num(value); + this._height = num(value); break; case "file": this._file = value; + break; case "gammagroup": this._gammagroup = value; break; @@ -64,53 +68,71 @@ export default class Bitmap { } getCSSVar(): string { - return `--bitmap-${this.getId().replace(/[^a-zA-Z0-9]/g, "-")}-${ - this._uniqueId - }`; + return this._cssVar; } - setUrl(url: string) { - document.documentElement.style.setProperty(this.getCSSVar(), `url(${url})`); - this._url = url; + getGammaGroup(): string { + return this._gammagroup; + } + + getImg(): HTMLImageElement { + return this._img; } // Ensure we've loaded the image into our image loader. async ensureImageLoaded(imageManager: ImageManager) { - Utils.assert( + assert( this._url == null, "Tried to ensure a Bitmap was laoded more than once." ); - const imgUrl = await imageManager.getUrl(this._file); - this._img = await imageManager.getImage(imgUrl); + this._img = await imageManager.getImage(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.setXmlAttr("w", String(this._img.width)); + this.setXmlAttr("h", String(this._img.height)); } - this.setUrl(imgUrl); + // this.setUrl(imgUrl); } - getBackgrondImageCSSAttribute(): string { + _getBackgrondImageCSSAttribute(): string { return `var(${this.getCSSVar()})`; } - getBackgrondPositionCSSAttribute(): string { - const x = Utils.px(-(this._x ?? 0)); - const y = Utils.px(-(this._y ?? 0)); + _getBackgrondPositionCSSAttribute(): string { + const x = px(-(this._x ?? 0)); + const y = px(-(this._y ?? 0)); return `${x} ${y}`; } - getBackgrondSizeCSSAttribute(): string { - const width = Utils.px(this._width); - const height = Utils.px(this._height); + _getBackgrondSizeCSSAttribute(): string { + const width = px(this._width); + const height = px(this._height); return `${width} ${height}`; } - getBackdropFilterCSSAttribute(): string { - return `url(#${Utils.normalizeDomId(this._gammagroup)})`; + _setAsBackground(div: HTMLDivElement, prefix: string) { + div.style.setProperty( + `--${prefix}background-image`, + this._getBackgrondImageCSSAttribute() + ); + div.style.setProperty( + `--${prefix}background-position`, + this._getBackgrondPositionCSSAttribute() + ); + } + + setAsBackground(div: HTMLDivElement) { + this._setAsBackground(div, ""); + } + + setAsActiveBackground(div: HTMLDivElement) { + this._setAsBackground(div, "active-"); + } + + setAsHoverBackground(div: HTMLDivElement) { + this._setAsBackground(div, "hover-"); } getCanvas(): HTMLCanvasElement { diff --git a/packages/webamp-modern-2/src/skin/Button.ts b/packages/webamp-modern-2/src/skin/Button.ts index 91547bf1..862c3f8a 100644 --- a/packages/webamp-modern-2/src/skin/Button.ts +++ b/packages/webamp-modern-2/src/skin/Button.ts @@ -1,6 +1,5 @@ import GuiObj from "./GuiObj"; import UI_ROOT from "../UIRoot"; -import { px } from "../utils"; import { VM } from "./VM"; // http://wiki.winamp.com/wiki/XML_GUI_Objects#.3Cbutton.2F.3E_.26_.3Ctogglebutton.2F.3E diff --git a/packages/webamp-modern-2/src/skin/GammaGroup.ts b/packages/webamp-modern-2/src/skin/GammaGroup.ts index 96eb0b21..dcae6956 100644 --- a/packages/webamp-modern-2/src/skin/GammaGroup.ts +++ b/packages/webamp-modern-2/src/skin/GammaGroup.ts @@ -1,5 +1,6 @@ -import { normalizeDomId, num, toBool } from "../utils"; +import { clamp, normalizeDomId, num, toBool } from "../utils"; +// https://www.pawelporwisz.pl/winamp/wct_en.php export default class GammaGroup { _id: string; _value: string; @@ -43,4 +44,31 @@ export default class GammaGroup { getRgb() { return `rgb(${this._value})`; } + + // TODO: Figure out how to actually implement this. + transformImage(img: HTMLImageElement): string { + const [r, g, b] = this._value.split(",").map((v) => { + return (Number(v) / 4096) * 255; + }); + const canvas = document.createElement("canvas"); + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext("2d"); + ctx.drawImage(img, 0, 0); + const imageData = ctx.getImageData(0, 0, img.width, img.height); + const data = imageData.data; + for (var i = 0; i < data.length; i += 4) { + if (this._boost) { + data[i] = (data[i] >> 1, 0, 255); // red + data[i + 1] = (data[i + 1] >> 1, 0, 255); // green + data[i + 2] = (data[i + 2] >> 1, 0, 255); // blue + } + + data[i] = clamp(data[i] + r, 0, 255); // red + data[i + 1] = clamp(data[i + 1] + g, 0, 255); // green + data[i + 2] = clamp(data[i + 2] + b, 0, 255); // blue + } + ctx.putImageData(imageData, 0, 0); + return canvas.toDataURL(); + } } diff --git a/packages/webamp-modern-2/src/skin/GuiObj.ts b/packages/webamp-modern-2/src/skin/GuiObj.ts index 06bebc94..1f287797 100644 --- a/packages/webamp-modern-2/src/skin/GuiObj.ts +++ b/packages/webamp-modern-2/src/skin/GuiObj.ts @@ -376,35 +376,24 @@ export default class GuiObj extends XmlObj { } setBackgroundImage(bitmap: Bitmap | null) { - this._div.style.setProperty( - "--background-image", - bitmap?.getBackgrondImageCSSAttribute() ?? "none" - ); - this._div.style.setProperty( - "--background-position", - bitmap?.getBackgrondPositionCSSAttribute() ?? "none" - ); - this._div.style.filter = bitmap?.getBackdropFilterCSSAttribute() ?? "none"; + if (bitmap != null) { + bitmap.setAsBackground(this._div); + } } // 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 | null) { - this._div.style.setProperty( - "--active-background-image", - bitmap?.getBackgrondImageCSSAttribute() ?? "none" - ); - this._div.style.setProperty( - "--active-background-position", - bitmap?.getBackgrondPositionCSSAttribute() ?? "none" - ); + if (bitmap != null) { + bitmap.setAsActiveBackground(this._div); + } } draw() { this._div.setAttribute("data-id", this.getId()); this._div.setAttribute("data-obj-name", "GuiObj"); - this._div.classList.add("webamp--guiobj"); + this._div.classList.add("webamp--img"); this._renderVisibility(); this._div.style.position = "absolute"; this._renderAlpha(); diff --git a/packages/webamp-modern-2/src/skin/ImageManager.ts b/packages/webamp-modern-2/src/skin/ImageManager.ts index 7f1f189f..9258b684 100644 --- a/packages/webamp-modern-2/src/skin/ImageManager.ts +++ b/packages/webamp-modern-2/src/skin/ImageManager.ts @@ -1,15 +1,11 @@ import JSZip from "jszip"; -import { getCaseInsensitiveFile } from "../utils"; +import { assert, getCaseInsensitiveFile, getId } 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(); - } + _urlCache: Map = new Map(); + _imgCache: Map = new Map(); + _cssVarCache: Map = new Map(); + constructor(private _zip: JSZip) {} async getUrl(filePath: string): Promise { if (!this._urlCache.has(filePath)) { @@ -19,26 +15,20 @@ export default class ImageManager { } const imgBlob = await zipFile.async("blob"); const imgUrl = await getUrlFromBlob(imgBlob); + // const img = await this.getImage(imgUrl); + // const transformedUrl = transformImage(img); this._urlCache.set(filePath, imgUrl); } return this._urlCache.get(filePath); } - async getSize(url: string): Promise<{ width: number; height: number }> { - if (!this._sizeCache.has(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)) { + async getImage(filePath: string): Promise { + if (!this._imgCache.has(filePath)) { // TODO: We could cache this - const img = await loadImage(url); - this._imgCache.set(url, img); + const img = await loadImage(await this.getUrl(filePath)); + this._imgCache.set(filePath, img); } - return this._imgCache.get(url); + return this._imgCache.get(filePath); } } diff --git a/packages/webamp-modern-2/src/skin/Slider.ts b/packages/webamp-modern-2/src/skin/Slider.ts index 0badc15d..2be9d605 100644 --- a/packages/webamp-modern-2/src/skin/Slider.ts +++ b/packages/webamp-modern-2/src/skin/Slider.ts @@ -75,43 +75,22 @@ export default class Slider extends GuiObj { _renderThumb() { this._thumbDiv.style.position = "absolute"; this._thumbDiv.setAttribute("data-obj-name", "Slider::Handle"); - this._thumbDiv.classList.add("webamp--guiobj"); + this._thumbDiv.classList.add("webamp--img"); if (this._thumb != null) { const bitmap = UI_ROOT.getBitmap(this._thumb); this._thumbDiv.style.width = px(bitmap.getWidth()); this._thumbDiv.style.height = px(bitmap.getHeight()); - this._thumbDiv.style.setProperty( - "--background-image", - bitmap.getBackgrondImageCSSAttribute() - ); - this._thumbDiv.style.setProperty( - "--background-position", - bitmap.getBackgrondPositionCSSAttribute() - ); + bitmap.setAsBackground(this._thumbDiv); } if (this._downThumb != null) { const bitmap = UI_ROOT.getBitmap(this._downThumb); - this._thumbDiv.style.setProperty( - "--active-background-image", - bitmap.getBackgrondImageCSSAttribute() - ); - this._thumbDiv.style.setProperty( - "--active-background-position", - bitmap.getBackgrondPositionCSSAttribute() - ); + bitmap.setAsActiveBackground(this._thumbDiv); } if (this._hoverThumb != null) { const bitmap = UI_ROOT.getBitmap(this._hoverThumb); - this._thumbDiv.style.setProperty( - "--hover-background-image", - bitmap.getBackgrondImageCSSAttribute() - ); - this._thumbDiv.style.setProperty( - "--hover-background-position", - bitmap.getBackgrondPositionCSSAttribute() - ); + bitmap.setAsHoverBackground(this._thumbDiv); } }