diff --git a/packages/webamp-modern-2/assets/MMD3.wal b/packages/webamp-modern-2/assets/MMD3.wal index 0194990a..061d0cb3 100644 Binary files a/packages/webamp-modern-2/assets/MMD3.wal and b/packages/webamp-modern-2/assets/MMD3.wal differ diff --git a/packages/webamp-modern-2/src/UIRoot.ts b/packages/webamp-modern-2/src/UIRoot.ts index 08ab31b6..98410518 100644 --- a/packages/webamp-modern-2/src/UIRoot.ts +++ b/packages/webamp-modern-2/src/UIRoot.ts @@ -18,6 +18,9 @@ export class UIRoot { _colors: Color[] = []; _groupDefs: XmlElement[] = []; _gammaSets: Map = new Map(); + _gammaNames = {}; + _dummyGammaGroup: GammaGroup = null; + _activeGammaSetName: string = ""; _xuiElements: XmlElement[] = []; _activeGammaSet: GammaGroup[] | null = null; _containers: Container[] = []; @@ -38,6 +41,7 @@ export class UIRoot { this._xuiElements = []; this._activeGammaSet = null; this._containers = []; + this._gammaNames = {}; // A list of all objects created for this skin. this._objects = []; @@ -123,8 +127,15 @@ export class UIRoot { return this._containers; } + findContainer(id: string): Container { + const container = findLast(this.getContainers(), (ct) => ct.hasId(id)); + return container; + } + addGammaSet(id: string, gammaSet: GammaGroup[]) { - this._gammaSets.set(id.toLowerCase(), gammaSet); + const lower = id.toLowerCase(); + this._gammaNames[lower] = id; + this._gammaSets.set(lower, gammaSet); } enableGammaSet(id: string) { @@ -135,6 +146,7 @@ export class UIRoot { this._gammaSets.keys() ).join(", ")}` ); + this._activeGammaSetName = id; this._activeGammaSet = found; this._setCssVars(); } @@ -145,6 +157,9 @@ export class UIRoot { } _getGammaGroup(id: string): GammaGroup | null { + if (!id) { + return this._getGammaGroupDummy(); + } const lower = id.toLowerCase(); const found = findLast(this._activeGammaSet, (gammaGroup) => { return gammaGroup.getId().toLowerCase() === lower; @@ -152,25 +167,35 @@ export class UIRoot { return found ?? null; } + _getGammaGroupDummy() { + if (!this._dummyGammaGroup) { + //lazy create + this._dummyGammaGroup = new GammaGroup(); + this._dummyGammaGroup.setXmlAttributes({ + id: "dummy", + value: "0,0,0", + }); + } + return this._dummyGammaGroup; + } + _setCssVars() { - const map = new Map(); const cssRules = []; for (const bitmap of this._bitmaps) { const img = bitmap.getImg(); + if (!img) { + console.warn(`Bitmap/font ${bitmap.getId()} has no img!`); + continue; + } 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. + const gammaGroup = this._getGammaGroup(groupId); + const url = gammaGroup.transformImage( + img, + bitmap._x, + bitmap._y, + bitmap._width, + bitmap._height + ); cssRules.push(` ${bitmap.getCSSVar()}: url(${url});`); } cssRules.unshift(":root{"); @@ -189,11 +214,7 @@ export class UIRoot { return found ?? null; } - dispatch( - action: string, - param: string | null | number, - actionTarget: string | null - ) { + dispatch(action: string, param: string | null, actionTarget: string | null) { switch (action.toLowerCase()) { case "play": this.audio.play(); @@ -213,11 +234,22 @@ export class UIRoot { case "eject": this.audio.eject(); break; + case "toggle": + this.toggleContainer(param); + break; default: assume(false, `Unknown global action: ${action}`); } } + + toggleContainer(param: string) { + const container = this.findContainer(param); + assume(container != null, `Can not toggle on unknown container: ${param}`); + container.toggle(); + } + draw() { + this._div.setAttribute("id", "ui-root"); this._div.style.imageRendering = "pixelated"; for (const container of this.getContainers()) { container.draw(); diff --git a/packages/webamp-modern-2/src/clip_path.html b/packages/webamp-modern-2/src/clip_path.html new file mode 100644 index 00000000..633bf8cd --- /dev/null +++ b/packages/webamp-modern-2/src/clip_path.html @@ -0,0 +1,43 @@ + + + + + + + Webamp Modern Test Suite + + + +
+
+ click me! +-> + + + + + + + +
+ +
+ +
+
+ + +
+
Top:
+
Right:
+
Bottom:
+
Left:
+
+ + \ No newline at end of file diff --git a/packages/webamp-modern-2/src/clip_path.ts b/packages/webamp-modern-2/src/clip_path.ts new file mode 100644 index 00000000..8bf61b9c --- /dev/null +++ b/packages/webamp-modern-2/src/clip_path.ts @@ -0,0 +1,35 @@ +import { Edges } from "./skin/Clippath"; + +document.getElementById("clickable").onclick = (ev) => { + alert("click on green!"); +}; +document.getElementById("img1").onclick = (event) => { + alert("click on IMAGE."); + event.stopPropagation(); +}; + +function main() { + const oriImg = document.getElementById("img1"); + const img2 = new Image(); + img2.onload = (ev) => { + // const canvas = document.createElement('canvas'); + const canvas = document.getElementById("canvas") as HTMLCanvasElement; + canvas.width = img2.width; + canvas.height = img2.height; + const ctx = canvas.getContext("2d"); + ctx.clearRect(0, 0, canvas.width, canvas.height); + + ctx.drawImage(img2, 0, 0); + const edge = new Edges(); + edge.parseCanvasTransparency(canvas); + document.getElementById("top").textContent = edge.gettop().replace(/px/gi, "").replace(/\,\s/gi, "\n"); + document.getElementById("right").textContent = edge.getright().replace(/px/gi, "").replace(/\,\s/gi, "\n"); + document.getElementById("bottom").textContent = edge.getbottom().replace(/px/gi, "").replace(/\,\s/gi, "\n"); + document.getElementById("left").textContent = edge.getleft().replace(/px/gi, "").replace(/\,\s/gi, "\n"); + // document.getElementById('app').style.clipPath = `polygon(${edge.top}, ${edge.bottom})`; + document.getElementById("app").style.clipPath = edge.getPolygon(); + }; + img2.setAttribute("src", oriImg.getAttribute("src")); +} + +main(); diff --git a/packages/webamp-modern-2/src/index.html b/packages/webamp-modern-2/src/index.html index 49744368..9f270196 100644 --- a/packages/webamp-modern-2/src/index.html +++ b/packages/webamp-modern-2/src/index.html @@ -4,30 +4,48 @@ - + Webamp Modern @@ -52,4 +193,4 @@
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 f15d0f6b..48b600b4 100644 --- a/packages/webamp-modern-2/src/index.ts +++ b/packages/webamp-modern-2/src/index.ts @@ -22,7 +22,7 @@ function setStatus(status: string) { async function main() { setStatus("Downloading skin..."); - const skinPath = getUrlQuery(window.location, 'skin') || "assets/MMD3.wal"; + const skinPath = getUrlQuery(window.location, "skin") || "assets/MMD3.wal"; const response = await fetch(skinPath); const data = await response.blob(); await loadSkin(data); diff --git a/packages/webamp-modern-2/src/maki/interpreter.ts b/packages/webamp-modern-2/src/maki/interpreter.ts index da010012..b8bc89a3 100644 --- a/packages/webamp-modern-2/src/maki/interpreter.ts +++ b/packages/webamp-modern-2/src/maki/interpreter.ts @@ -294,12 +294,16 @@ class Interpreter { let argCount: number = klass.prototype[methodName].length; const methodDefinition = getMethod(guid, methodName); - assert( - argCount === (methodDefinition.parameters.length ?? 0), - `Arg count mismatch. Expected ${ - methodDefinition.parameters.length ?? 0 - } arguments, but found ${argCount} for ${klass.name}.${methodName}` - ); + if (methodName.toLowerCase() != "init") { + assert( + argCount === (methodDefinition.parameters.length ?? 0), + `Arg count mismatch. Expected ${ + methodDefinition.parameters.length ?? 0 + } arguments, but found ${argCount} for ${ + klass.name + }.${methodName}` + ); + } const methodArgs = []; while (argCount--) { @@ -312,7 +316,18 @@ class Interpreter { obj.value != null, `Guru Meditation: Tried to call method ${klass.name}.${methodName} on null object` ); - let value = obj.value[methodName](...methodArgs); + + // let value = obj.value[methodName](...methodArgs); + let value; + try { + value = obj.value[methodName](...methodArgs); + } catch (err) { + console.warn( + `error call: ${klass.name}.${methodName}(...${JSON.stringify(methodArgs)})`, + `err: ${err.message} obj: ${JSON.stringify(obj)}` + ); + value = null; + } if (value === undefined && returnType !== "NULL") { throw new Error( diff --git a/packages/webamp-modern-2/src/skin/Bitmap.ts b/packages/webamp-modern-2/src/skin/Bitmap.ts index 9ce453dc..1cd408b0 100644 --- a/packages/webamp-modern-2/src/skin/Bitmap.ts +++ b/packages/webamp-modern-2/src/skin/Bitmap.ts @@ -21,15 +21,12 @@ export default class Bitmap { } } - setXmlAttr(_key: string, value: string) { + setXmlAttr(_key: string, value: string): boolean { const key = _key.toLowerCase(); switch (key) { case "id": this._id = value; - this._cssVar = `--bitmap-${this.getId().replace( - /[^a-zA-Z0-9]/g, - "-" - )}-${getId()}`; + this._cssVar = `--bitmap-${this.getId().replace(/[^a-zA-Z0-9]/g, "-")}`; break; case "x": this._x = num(value) ?? 0; @@ -86,14 +83,14 @@ export default class Bitmap { "Tried to ensure a Bitmap was laoded more than once." ); + //force. also possibly set null: this._img = await imageManager.getImage(this._file); - - if (this._width == null && this._height == null) { - this.setXmlAttr("w", String(this._img.width)); - this.setXmlAttr("h", String(this._img.height)); + if (this._img) { + if (this._width == null && this._height == null) { + this.setXmlAttr("w", String(this._img.width)); + this.setXmlAttr("h", String(this._img.height)); + } } - - // this.setUrl(imgUrl); } _getBackgrondImageCSSAttribute(): string { @@ -112,26 +109,26 @@ export default class Bitmap { return `${width} ${height}`; } - _setAsBackground(div: HTMLDivElement, prefix: string) { + _setAsBackground(div: HTMLElement, prefix: string) { div.style.setProperty( `--${prefix}background-image`, this._getBackgrondImageCSSAttribute() ); - div.style.setProperty( - `--${prefix}background-position`, - this._getBackgrondPositionCSSAttribute() - ); } - setAsBackground(div: HTMLDivElement) { + setAsBackground(div: HTMLElement) { this._setAsBackground(div, ""); } - setAsActiveBackground(div: HTMLDivElement) { + setAsDownBackground(div: HTMLElement) { + this._setAsBackground(div, "down-"); + } + + setAsActiveBackground(div: HTMLElement) { this._setAsBackground(div, "active-"); } - setAsHoverBackground(div: HTMLDivElement) { + setAsHoverBackground(div: HTMLElement) { this._setAsBackground(div, "hover-"); } @@ -142,7 +139,18 @@ export default class Bitmap { 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()); + // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage + ctx.drawImage( + this._img, + this._x, + this._y, + this.getWidth(), + this.getHeight(), + 0, + 0, + this.getWidth(), + this.getHeight() + ); } return this._canvas; } diff --git a/packages/webamp-modern-2/src/skin/Clippath.ts b/packages/webamp-modern-2/src/skin/Clippath.ts new file mode 100644 index 00000000..a2f20230 --- /dev/null +++ b/packages/webamp-modern-2/src/skin/Clippath.ts @@ -0,0 +1,162 @@ +export class Edges { + _top: string[] = []; + _right: string[] = []; + _bottom: string[] = []; + _left: string[] = []; + + parseCanvasTransparency(canvas: HTMLCanvasElement) { + const w = canvas.width; + const h = canvas.height; + const ctx = canvas.getContext("2d"); + const data = ctx.getImageData(0, 0, w, h).data; + let points: string[] = []; + var x, y, lastX, lastY; + var lastfoundx: number, lastfoundy: number, pending: boolean; + var first: boolean; + + //? return true if not transparent + function opaque(ax: number, ay: number): boolean { + return data[(ax + ay * w) * 4 + 3] != 0; + } + + function post(ax: number, ay: number) { + points.push(`${ax}px ${ay}px`); + } + + //? top ------------------------------------------------- + points = []; + lastY = 0; + first = true; + pending = false; + for (x = 0; x < w; x++) { + //? scan top, left->right + for (y = 0; y < h; y++) { + //? find most top of non-transparent + if (opaque(x, y)) { + if (!first && y != lastY && pending) { + post(lastfoundx + 1, lastfoundy); + } + if (first || y != lastY || x == w) { + first = false; + post(x, y); + lastY = y; + pending = false; + } else if (x == w && pending) { + post(lastfoundx, lastfoundy); + } else { + pending = true; + } + lastfoundx = x; + lastfoundy = y; + break; + } + } + if (x == w - 1 && pending) { + post(lastfoundx + 1, lastfoundy); + } + } + this._top = points; // points.join(', \n') + + //? Right ------------------------------------------------- + points = []; + lastX = 0; + first = true; + pending = false; + for (y = 0; y <= h; y++) { + //? scan right, top->bottom + for (x = w - 1; x >= 0; x--) { + //? find most right of non-transparent + if (opaque(x, y)) { + if (!first && x != lastX && pending) { + post(lastfoundx + 1, lastfoundy); + } + if (first || x != lastX || y == h - 1) { + first = false; + post(x + 1, y); + lastX = x; + pending = false; + } else if (y == h && pending) { + post(lastfoundx + 1, lastfoundy); + pending = false; + } else { + pending = true; + } + lastfoundx = x; + lastfoundy = y; + break; + } + } + if (y == h - 1 && pending) { + // last + post(lastfoundx + 1, lastfoundy); + } + } + this._right = points; // points.join(', \n') + + //? bottom ------------------------------------------------- + points = []; + lastY = h - 1; + first = true; + pending = false; + for (x = w; x >= 0; x--) { + //? scan bottom, right->left + for (y = h - 1; y >= 0; y--) { + //? find most top of non-transparent + if (opaque(x, y)) { + if (!first && y != lastY && pending) { + post(lastfoundx, lastfoundy + 1); + } + if (first || y != lastY || x == 0) { + first = false; + post(x, y + 1); + lastY = y; + pending = false; + } else if (x == 0 && pending) { + post(lastfoundx, lastfoundy + 1); + pending = false; + } else { + pending = true; + } + lastfoundx = x; + lastfoundy = y; + break; + } + } + if (x == 0 && pending) { + // last + post(lastfoundx, lastfoundy + 1); + } + } + this._bottom = points; // points.join(', \n') + } + + gettop(): string { + return this._top.join(", "); + } + getright(): string { + return this._right.join(", "); + } + + getbottom(): string { + return this._bottom.join(", "); + } + + getleft(): string { + return this._left.join(", "); + } + + isSimpleRect(): boolean { + return this._top.length == 2 && this._bottom.length == 2; + } + + getPolygon(): string { + // to avoid empty between two comma separator, we explode values befor join(). + return `polygon(${[ + ...this._top, + ...this._right, + ...this._bottom, + ...this._left, + ].join(", ")})`; + // TODO: detect if first points in bottom has ben detected by right. + } +} diff --git a/packages/webamp-modern-2/src/skin/ColorThemesList.ts b/packages/webamp-modern-2/src/skin/ColorThemesList.ts index 356164c5..d6b2e338 100644 --- a/packages/webamp-modern-2/src/skin/ColorThemesList.ts +++ b/packages/webamp-modern-2/src/skin/ColorThemesList.ts @@ -29,9 +29,10 @@ export default class ColorThemesList extends GuiObj { for (const key of UI_ROOT._gammaSets.keys()) { const option = document.createElement("option"); option.value = key; - option.innerText = key; + option.innerText = UI_ROOT._gammaNames[key]; this._select.appendChild(option); } + this._select.value = UI_ROOT._activeGammaSetName; } handleAction( diff --git a/packages/webamp-modern-2/src/skin/Cursor.ts b/packages/webamp-modern-2/src/skin/Cursor.ts new file mode 100644 index 00000000..ff7c4599 --- /dev/null +++ b/packages/webamp-modern-2/src/skin/Cursor.ts @@ -0,0 +1,8 @@ +// We use a bitmask to encode the possible combinations of cursor attributes as a single number. +// https://en.wikipedia.org/wiki/Mask_(computing) +export const LEFT = 1 << 1; +export const RIGHT = 1 << 2; +export const TOP = 1 << 3; +export const BOTTOM = 1 << 4; +export const MOVE = 1 << 0 | TOP | LEFT; +export const CURSOR = 1 << 31; \ No newline at end of file diff --git a/packages/webamp-modern-2/src/skin/GammaGroup.ts b/packages/webamp-modern-2/src/skin/GammaGroup.ts index a5a6cebd..67c3f517 100644 --- a/packages/webamp-modern-2/src/skin/GammaGroup.ts +++ b/packages/webamp-modern-2/src/skin/GammaGroup.ts @@ -46,28 +46,32 @@ export default class GammaGroup { return `rgb(${this._value})`; } - // TODO: Figure out how to actually implement this. - transformImage(img: HTMLImageElement): string { - // Toggle this to play with gl transforming - if (false) { - return glTransformImage(img); - } + transformImage( + img: HTMLImageElement, + x: number, + y: number, + w: number, + h: number + ): string { const [r, g, b] = this._value.split(",").map((v) => { return Number(v) / 4096 + 1.0; }); + // because some didn't has explicit "w" attribute + const safeWidth = w || img.width; + const safeHeight = h || img.height; + // because some didn't has explicit "x" attribute + // if it is any, we threat it as background-position coordinate + const safeLeft = x ? -x : 0; + const safeTop = y ? -y : 0; const canvas = document.createElement("canvas"); - canvas.width = img.width; - canvas.height = img.height; + canvas.width = safeWidth; + canvas.height = safeHeight; const ctx = canvas.getContext("2d"); - ctx.drawImage(img, 0, 0); - const imageData = ctx.getImageData(0, 0, img.width, img.height); + // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage + ctx.drawImage(img, safeLeft, safeTop); + const imageData = ctx.getImageData(0, 0, safeWidth, safeHeight); 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 - } let [ir, ig, ib] = [data[i], data[i + 1], data[i + 2]]; if (this._gray != 0) { if (this._gray == 2) ir = (ir + ig + ib) / 3; @@ -75,9 +79,12 @@ export default class GammaGroup { ig = ir; ib = ir; } - data[i] = clamp(ir * r, 0, 255); // red - data[i + 1] = clamp(ig * g, 0, 255); // green - data[i + 2] = clamp(ib * b, 0, 255); // blue + const mult = this._boost == 2 ? 4 : 1; + const brightness = this._boost == 1 ? 128 : this._boost == 2 ? 32 : 0; + + data[i + 0] = clamp((ir + brightness) * mult * r, 0, 255); // red + data[i + 1] = clamp((ig + brightness) * mult * g, 0, 255); // green + data[i + 2] = clamp((ib + brightness) * mult * b, 0, 255); // blue } ctx.putImageData(imageData, 0, 0); return canvas.toDataURL(); diff --git a/packages/webamp-modern-2/src/skin/makiClasses/Button.ts b/packages/webamp-modern-2/src/skin/makiClasses/Button.ts index 8865e28f..055392de 100644 --- a/packages/webamp-modern-2/src/skin/makiClasses/Button.ts +++ b/packages/webamp-modern-2/src/skin/makiClasses/Button.ts @@ -7,6 +7,8 @@ export default class Button extends GuiObj { static GUID = "698eddcd4fec8f1e44f9129b45ff09f9"; _image: string; _downimage: string; + _hoverimage: string; + _activeimage: string; _active: boolean = false; _action: string | null = null; _param: string | null = null; @@ -39,6 +41,14 @@ export default class Button extends GuiObj { this._downimage = value; this._renderBackground(); break; + case "hoverimage": + this._hoverimage = value; + this._renderBackground(); + break; + case "activeimage": + this._activeimage = value; + this._renderBackground(); + break; case "action": this._action = value; break; @@ -86,6 +96,11 @@ export default class Button extends GuiObj { if (onoff !== this._active) { this._active = onoff; + if (this._active) { + this._div.classList.add("active"); + } else { + this._div.classList.remove("active"); + } UI_ROOT.vm.dispatch(this, "onactivate", [V.newBool(onoff)]); } } @@ -108,20 +123,36 @@ export default class Button extends GuiObj { if (this._downimage != null) { const downBitmap = UI_ROOT.getBitmap(this._downimage); - this.setActiveBackgroundImage(downBitmap); + this.setDownBackgroundImage(downBitmap); + } else { + this.setDownBackgroundImage(null); + } + + if (this._hoverimage != null) { + const hoverimage = UI_ROOT.getBitmap(this._hoverimage); + this.setHoverBackgroundImage(hoverimage); + } else { + this.setHoverBackgroundImage(null); + } + + if (this._activeimage != null) { + const activeimage = UI_ROOT.getBitmap(this._activeimage); + this.setActiveBackgroundImage(activeimage); } else { this.setActiveBackgroundImage(null); } } _handleMouseDown(e: MouseEvent) { - this.setactivated(!this._active); + // don't send to parent to start move/resizing + e.stopPropagation(); + // buttonToggle will handle it } draw() { super.draw(); - this._div.setAttribute("data-obj-name", "Button"); this._div.classList.add("webamp--img"); + this._div.style.pointerEvents = "auto"; this._renderBackground(); } diff --git a/packages/webamp-modern-2/src/skin/makiClasses/Container.ts b/packages/webamp-modern-2/src/skin/makiClasses/Container.ts index 0a39d95b..d3b26d3c 100644 --- a/packages/webamp-modern-2/src/skin/makiClasses/Container.ts +++ b/packages/webamp-modern-2/src/skin/makiClasses/Container.ts @@ -1,5 +1,5 @@ import UI_ROOT from "../../UIRoot"; -import { assert, px, removeAllChildNodes, toBool } from "../../utils"; +import { assert, num, px, removeAllChildNodes, toBool } from "../../utils"; import Layout from "./Layout"; import XmlObj from "../XmlObj"; @@ -13,9 +13,13 @@ export default class Container extends XmlObj { static GUID = "e90dc47b4ae7840d0b042cb0fcf775d2"; _layouts: Layout[] = []; _activeLayout: Layout | null = null; - _defaultVisible: boolean = true; + _visible: boolean = true; _id: string; - _div: HTMLDivElement = document.createElement("div"); + _x: number = 0; + _y: number = 0; + _componentGuid: string; // eg. "guid:{1234-...-0ABC}" + _componentAlias: string; // eg. "guid:pl" + _div: HTMLElement = document.createElement("container"); constructor() { super(); } @@ -29,8 +33,22 @@ export default class Container extends XmlObj { case "id": this._id = value.toLowerCase(); break; + case "component": + this._componentGuid = value.toLowerCase().split(":")[1]; + this.resolveAlias(); + break; case "default_visible": - this._defaultVisible = toBool(value); + this._visible = toBool(value); + break; + case "x": + case "default_x": + this._x = num(value) ?? 0; + this._renderDimensions(); + break; + case "y": + case "default_y": + this._y = num(value) ?? 0; + this._renderDimensions(); break; default: return false; @@ -44,11 +62,46 @@ export default class Container extends XmlObj { } } + resolveAlias() { + const knownContainerGuids = { + "{0000000a-000c-0010-ff7b-01014263450c}": "vis", + "{45f3f7c1-a6f3-4ee6-a15e-125e92fc3f8d}": "pl", + "{6b0edf80-c9a5-11d3-9f26-00c04f39ffc6}": "ml", + "{7383a6fb-1d01-413b-a99a-7e6f655f4591}": "con", + "{7a8b2d76-9531-43b9-91a1-ac455a7c8242}": "lir", + "{a3ef47bd-39eb-435a-9fb3-a5d87f6f17a5}": "dl", + "{f0816d7b-fffc-4343-80f2-e8199aa15cc3}": "video", + }; + const guid = this._componentGuid; + this._componentAlias = knownContainerGuids[guid]; + if (this._componentGuid && !this._componentAlias) { + console.warn( + `unknown component alias for guid:${this._componentGuid}`, + `for id:${this.getId()}` + ); + } + } + + /** + * Container sometime identified with a guid, or a-guid alias + * so which one correct (id, 'guid:{abcde}, 'guid:pl') is acceptable. + */ + hasId(id: string): boolean { + if (!id) return false; + id = id.toLowerCase(); + const useGuid = id.startsWith("guid:"); + if (useGuid) { + id = id.substring(5); + return this._componentGuid == id || this._componentAlias == id; + } else { + return this._id == id; + } + } getId() { return this._id; } - getDiv(): HTMLDivElement { + getDiv(): HTMLElement { return this._div; } @@ -66,6 +119,26 @@ export default class Container extends XmlObj { this._div.style.left = px((width - this.getWidth()) / 2); } + show() { + if (!this._activeLayout) { + this.switchToLayout(this._layouts[0]._id); + } + this._visible = true; + this._renderLayout(); + } + hide() { + this._visible = false; + this._renderLayout(); + } + toggle() { + if (!this._visible) this.show(); + else this.hide(); + } + close() { + this._activeLayout = null; + this.hide(); + } + /* Required for Maki */ /** * Get the layout associated with the an id. @@ -97,15 +170,11 @@ export default class Container extends XmlObj { removeAllChildNodes(this._div); } - setLayout(id: string) { - const layout = this.getlayout(id); - assert(layout != null, `Could not find layout with id "${id}".`); - UI_ROOT.vm.dispatch(this, "onswitchtolayout", [ - { type: "OBJECT", value: this._activeLayout }, - { type: "OBJECT", value: layout }, - ]); - this._activeLayout = layout; + switchToLayout(layout_id: string) { + const layout = this.getlayout(layout_id); + assert(layout != null, `Could not find layout with id "${layout_id}".`); this._clearCurrentLayout(); + this._activeLayout = layout; this._renderLayout(); UI_ROOT.vm.dispatch(this, "onswitchtolayout", [ { type: "OBJECT", value: layout }, @@ -119,15 +188,20 @@ export default class Container extends XmlObj { ) { switch (action) { case "SWITCH": - this.setLayout(param); + this.switchToLayout(param); break; default: UI_ROOT.dispatch(action, param, actionTarget); } } + _renderDimensions() { + this._div.style.left = px(this._x); + this._div.style.top = px(this._y); + } + _renderLayout() { - if (this._defaultVisible && this._activeLayout) { + if (this._visible && this._activeLayout) { this._activeLayout.draw(); this._div.appendChild(this._activeLayout.getDiv()); // this.center(); @@ -137,9 +211,7 @@ export default class Container extends XmlObj { } draw() { - this._div.setAttribute("data-xml-id", this.getId()); - this._div.setAttribute("data-obj-name", "Container"); - this._div.style.position = "absolute"; + this._div.setAttribute("id", this.getId()); this._renderLayout(); } } diff --git a/packages/webamp-modern-2/src/skin/makiClasses/Group.ts b/packages/webamp-modern-2/src/skin/makiClasses/Group.ts index 682e580a..e4500eaf 100644 --- a/packages/webamp-modern-2/src/skin/makiClasses/Group.ts +++ b/packages/webamp-modern-2/src/skin/makiClasses/Group.ts @@ -2,19 +2,17 @@ import * as Utils from "../../utils"; import UI_ROOT from "../../UIRoot"; import GuiObj from "./GuiObj"; import SystemObject from "./SystemObject"; +import Movable from "./Movable"; // http://wiki.winamp.com/wiki/XML_GUI_Objects#.3Cgroup.2F.3E -export default class Group extends GuiObj { +export default class Group extends Movable { static GUID = "45be95e5419120725fbb5c93fd17f1f9"; _parent: Group; _instanceId: string; _background: string; _desktopAlpha: boolean; _drawBackground: boolean = true; - _minimumHeight: number; - _maximumHeight: number; - _minimumWidth: number; - _maximumWidth: number; + _isLayout: boolean = false; _systemObjects: SystemObject[] = []; _children: GuiObj[] = []; @@ -35,18 +33,6 @@ export default class Group extends GuiObj { this._drawBackground = Utils.toBool(value); this._renderBackground(); 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; } @@ -106,28 +92,38 @@ export default class Group extends GuiObj { ); } + getparentlayout(): Group { + let obj: Group = this; + while (obj._parent) { + if (obj._isLayout) { + break; + } + obj = obj._parent; + } + if (!obj) { + console.warn("getParentLayout", this.getId(), "failed!"); + } + return obj; + } + // This shadows `getheight()` on GuiObj getheight(): number { - if (this._height) { - return this._height; - } - if (this._background != null) { + const h = super.getheight(); + if (h == null && this._background != null) { const bitmap = UI_ROOT.getBitmap(this._background); - return bitmap.getHeight(); + if (bitmap) return bitmap.getHeight(); } - return super.getheight(); + return h; } // This shadows `getwidth()` on GuiObj getwidth(): number { - if (this._width) { - return this._width; - } - if (this._background != null) { + const w = super.getwidth(); + if (w == null && this._background != null) { const bitmap = UI_ROOT.getBitmap(this._background); - return bitmap.getWidth(); + if (bitmap) return bitmap.getWidth(); } - return super.getwidth(); + return w; } _renderBackground() { @@ -141,13 +137,17 @@ export default class Group extends GuiObj { draw() { super.draw(); - this._div.setAttribute("data-obj-name", "Group"); this._div.classList.add("webamp--img"); // It seems Groups are not responsive to click events. + if (this._movable || this._resizable) { + // this._div.style.removeProperty('pointer-events'); + this._div.style.pointerEvents = "auto"; + } else { + this._div.style.pointerEvents = "none"; + } + //TODO: allow move/resize if has ._image this._div.style.pointerEvents = "none"; - this._div.style.overflow = "hidden"; - this._div.style.height = Utils.px(this._maximumHeight); - this._div.style.width = Utils.px(this._maximumWidth); + // this._div.style.overflow = "hidden"; this._renderBackground(); for (const child of this._children) { child.draw(); diff --git a/packages/webamp-modern-2/src/skin/makiClasses/GuiObj.ts b/packages/webamp-modern-2/src/skin/makiClasses/GuiObj.ts index 74661dae..a1042634 100644 --- a/packages/webamp-modern-2/src/skin/makiClasses/GuiObj.ts +++ b/packages/webamp-modern-2/src/skin/makiClasses/GuiObj.ts @@ -1,9 +1,12 @@ import UI_ROOT from "../../UIRoot"; -import { assert, num, toBool, px, assume } from "../../utils"; +import { assert, num, toBool, px, assume, relative } from "../../utils"; import Bitmap from "../Bitmap"; import Group from "./Group"; import XmlObj from "../XmlObj"; +let BRING_LEAST: number = -1; +let BRING_MOST_TOP: number = 1; + // http://wiki.winamp.com/wiki/XML_GUI_Objects#GuiObject_.28Global_params.29 export default class GuiObj extends XmlObj { static GUID = "4ee3e1994becc636bc78cd97b028869c"; @@ -13,10 +16,22 @@ export default class GuiObj extends XmlObj { _height: number; _x: number = 0; _y: number = 0; + _minimumHeight: number = 0; + _maximumHeight: number = 0; + _minimumWidth: number = 0; + _maximumWidth: number = 0; + _relatx: string; + _relaty: string; + _relatw: string; + _relath: string; + // _resize: string; _droptarget: string; _visible: boolean = true; _alpha: number = 255; _ghost: boolean = false; + _sysregion: number = 0; + // _movable: boolean = false; + // _resizable: number = 0; _tooltip: string = ""; _targetX: number | null = null; _targetY: number | null = null; @@ -24,13 +39,106 @@ export default class GuiObj extends XmlObj { _targetHeight: number | null = null; _targetAlpha: number | null = null; _targetSpeed: number | null = null; - _div: HTMLDivElement = document.createElement("div"); + _goingToTarget: boolean = false; + _div: HTMLElement; _backgroundBitmap: Bitmap | null = null; + // _resizingEventsRegisterd: boolean = false; + // _movingEventsRegisterd: boolean = false; constructor() { super(); + this._div = document.createElement( + this.getElTag().toLowerCase().replace("_", "") + ); + } + + getElTag(): string { + return this.constructor.name; + } + + setParent(group: Group) { + this._parent = group; + } + + setXmlAttr(_key: string, value: string): boolean { + const key = _key.toLowerCase(); + switch (key) { + case "id": + this._id = value.toLowerCase(); + break; + case "w": + case "default_w": + this._width = num(value); + this._renderWidth(); + break; + case "h": + case "default_h": + this._height = num(value); + this._renderHeight(); + break; + case "x": + case "default_x": + this._x = num(value) ?? 0; + this._renderX(); + break; + case "y": + case "default_y": + this._y = num(value) ?? 0; + this._renderY(); + break; + case "minimum_h": + this._minimumHeight = num(value); + break; + case "minimum_w": + this._minimumWidth = num(value); + break; + case "maximum_h": + this._maximumHeight = num(value); + break; + case "maximum_w": + this._maximumWidth = num(value); + break; + case "relatw": + this._relatw = value; + break; + case "relath": + this._relath = value; + break; + case "relatx": + this._relatx = value; + break; + case "relaty": + this._relaty = value; + break; + case "droptarget": + this._droptarget = value; + break; + case "ghost": + this._ghost = toBool(value); + break; + case "visible": + this._visible = toBool(value); + this._renderVisibility(); + break; + case "tooltip": + this._tooltip = value; + break; + // (int) An integer [0,255] specifying the alpha blend mode of the object (0 is transparent, 255 is opaque). Default is 255. + case "alpha": + this._alpha = num(value); + case "sysregion": + this._sysregion = num(value); + break; + default: + return false; + } + return true; + } + + init() { this._div.addEventListener("mousedown", (e) => { + e.stopPropagation(); /* if (this._backgroundBitmap != null) { const { clientX, clientY } = e; @@ -58,6 +166,7 @@ export default class GuiObj extends XmlObj { this.onLeftButtonDown(e.clientX, e.clientY); const mouseUpHandler = (e) => { + // e.stopPropagation(); this.onLeftButtonUp(e.clientX, e.clientY); this._div.removeEventListener("mouseup", mouseUpHandler); }; @@ -72,59 +181,7 @@ export default class GuiObj extends XmlObj { }); } - setParent(group: Group) { - this._parent = group; - } - - setXmlAttr(_key: string, value: string): boolean { - const key = _key.toLowerCase(); - switch (key) { - case "id": - this._id = value.toLowerCase(); - break; - case "w": - this._width = num(value); - this._renderWidth(); - break; - case "h": - this._height = num(value); - this._renderHeight(); - break; - case "x": - this._x = num(value) ?? 0; - this._renderX(); - break; - case "y": - this._y = num(value) ?? 0; - this._renderY(); - break; - case "droptarget": - this._droptarget = value; - break; - case "ghost": - this._ghost = toBool(value); - break; - case "visible": - this._visible = toBool(value); - this._renderVisibility(); - break; - case "tooltip": - this._tooltip = value; - break; - // (int) An integer [0,255] specifying the alpha blend mode of the object (0 is transparent, 255 is opaque). Default is 255. - case "alpha": - this._alpha = num(value); - default: - return false; - } - return true; - } - - init() { - // pass - } - - getDiv(): HTMLDivElement { + getDiv(): HTMLElement { return this._div; } @@ -155,7 +212,7 @@ export default class GuiObj extends XmlObj { * @ret The top edge's position (in screen coordinates). */ gettop(): number { - return this._div.getBoundingClientRect().y; + return this._y; } /** @@ -165,7 +222,7 @@ export default class GuiObj extends XmlObj { * @ret The left edge's position (in screen coordinates). */ getleft(): number { - return this._div.getBoundingClientRect().x; + return this._x; } /** @@ -174,14 +231,12 @@ export default class GuiObj extends XmlObj { * @ret The height of the object. */ getheight(): number { - /* - assert( - this._height != null, - `Expected GUIObj to have a height in ${this.getId()}.` - ); - */ - // FIXME - return this._height ?? 0; + if (this._height || this._minimumHeight || this._maximumHeight) { + let h = Math.max(this._height || 0, this._minimumHeight); + h = Math.min(h, this._maximumHeight || h); + return h; + } + return this._height; } /** @@ -190,13 +245,14 @@ export default class GuiObj extends XmlObj { * @ret The width of the object. */ getwidth(): number { - /* - assert( - this._width != null, - `Expected GUIObj to have a width in ${this.getId()}.` - ); - */ - return this._width ?? 0; + if (this._width || this._minimumWidth || this._maximumWidth) { + let w = Math.max(this._width || 0, this._minimumWidth); + if (this._maximumHeight) { + w = Math.min(w, this._maximumWidth || w); + } + return w; + } + return this._width; } /** @@ -358,6 +414,7 @@ export default class GuiObj extends XmlObj { * Begin transition to previously set target. */ gototarget() { + this._goingToTarget = true; const duration = this._targetSpeed * 1000; const startTime = performance.now(); @@ -370,30 +427,47 @@ export default class GuiObj extends XmlObj { ]; const changes: { - [key: string]: { start: number; delta: number; renderKey: string }; + [key: string]: { + start: number; + delta: number; + renderKey: string; + target: number; + positive: boolean; + }; } = {}; for (const [key, targetKey, renderKey] of pairs) { const target = this[targetKey]; if (target != null) { const start = this[key]; + const positive = target > start; const delta = target - start; - changes[key] = { start, delta, renderKey }; + changes[key] = { start, delta, renderKey, target, positive }; } } + const clamp = (current, target, positive) => { + if (positive) { + return Math.min(current, target); + } else { + return Math.max(current, target); + } + }; + const update = (time: number) => { const timeDiff = time - startTime; const progress = timeDiff / duration; - for (const [key, { start, delta, renderKey }] of Object.entries( - changes - )) { - this[key] = start + delta * progress; + for (const [ + key, + { start, delta, renderKey, target, positive }, + ] of Object.entries(changes)) { + this[key] = clamp(start + delta * progress, target, positive); this[renderKey](); } if (timeDiff < duration) { window.requestAnimationFrame(update); } else { + this._goingToTarget = false; // TODO: Clear targets? UI_ROOT.vm.dispatch(this, "ontargetreached"); } @@ -487,6 +561,22 @@ export default class GuiObj extends XmlObj { return this._alpha; } + getparentlayout(): Group { + if (this._parent) { + return this._parent.getparentlayout(); + } + } + + bringtofront() { + BRING_MOST_TOP += 1; + this._div.style.zIndex = String(BRING_MOST_TOP); + } + + bringtoback() { + BRING_LEAST -= 1; + this._div.style.zIndex = String(BRING_LEAST); + } + handleAction( action: string, param: string | null, @@ -508,10 +598,18 @@ export default class GuiObj extends XmlObj { } _renderAlpha() { - this._div.style.opacity = `${this._alpha / 255}`; + if (this._alpha != 255) { + this._div.style.opacity = `${this._alpha / 255}`; + } else { + this._div.style.removeProperty("opacity"); + } } _renderVisibility() { - this._div.style.display = this._visible ? "inline-block" : "none"; + if (!this._visible) { + this._div.style.display = "none"; + } else { + this._div.style.removeProperty("display"); + } } _renderTransate() { this._div.style.transform = `translate(${px(this._x ?? 0)}, ${px( @@ -519,16 +617,23 @@ export default class GuiObj extends XmlObj { )})`; } _renderX() { - this._div.style.left = px(this._x ?? 0); + this._div.style.left = + this._relatx == "1" ? relative(this._x ?? 0) : px(this._x ?? 0); } + _renderY() { - this._div.style.top = px(this._y ?? 0); + this._div.style.top = + this._relaty == "1" ? relative(this._y ?? 0) : px(this._y ?? 0); } + _renderWidth() { - this._div.style.width = px(this.getwidth()); + this._div.style.width = + this._relatw == "1" ? relative(this._width ?? 0) : px(this.getwidth()); } + _renderHeight() { - this._div.style.height = px(this.getheight()); + this._div.style.height = + this._relath == "1" ? relative(this._height ?? 0) : px(this.getheight()); } _renderDimensions() { @@ -544,13 +649,24 @@ export default class GuiObj extends XmlObj { bitmap.setAsBackground(this._div); } else { this._div.style.setProperty(`--background-image`, "none"); - this._div.style.setProperty(`--background-position`, "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. + setDownBackgroundImage(bitmap: Bitmap | null) { + if (bitmap != null) { + bitmap.setAsDownBackground(this._div); + } + } + + setHoverBackgroundImage(bitmap: Bitmap | null) { + if (bitmap != null) { + bitmap.setAsHoverBackground(this._div); + } + } + setActiveBackgroundImage(bitmap: Bitmap | null) { if (bitmap != null) { bitmap.setAsActiveBackground(this._div); @@ -558,10 +674,8 @@ export default class GuiObj extends XmlObj { } draw() { - this._div.setAttribute("data-id", this.getId()); - this._div.setAttribute("data-obj-name", "GuiObj"); + this.getId() && this._div.setAttribute("id", this.getId()); this._renderVisibility(); - this._div.style.position = "absolute"; this._renderAlpha(); if (this._tooltip) { this._div.setAttribute("title", this._tooltip); diff --git a/packages/webamp-modern-2/src/skin/makiClasses/Layer.ts b/packages/webamp-modern-2/src/skin/makiClasses/Layer.ts index 9b1ea842..88503d8d 100644 --- a/packages/webamp-modern-2/src/skin/makiClasses/Layer.ts +++ b/packages/webamp-modern-2/src/skin/makiClasses/Layer.ts @@ -1,19 +1,25 @@ import GuiObj from "./GuiObj"; import UI_ROOT from "../../UIRoot"; +import Movable from "./Movable"; +import { Edges } from "../Clippath"; // http://wiki.winamp.com/wiki/XML_GUI_Objects#.3Clayer.2F.3E -export default class Layer extends GuiObj { +export default class Layer extends Movable { static GUID = "5ab9fa1545579a7d5765c8aba97cc6a6"; _image: string; setXmlAttr(key: string, value: string): boolean { if (super.setXmlAttr(key, value)) { + if (key == "sysregion") { + this._renderRegion(); + } return true; } switch (key) { case "image": this._image = value; this._renderBackground(); + this._renderRegion(); break; default: return false; @@ -50,9 +56,21 @@ export default class Layer extends GuiObj { this.setBackgroundImage(bitmap); } + _renderRegion() { + if (this._sysregion == 1 && this._image) { + const canvas = UI_ROOT.getBitmap(this._image).getCanvas(); + const edge = new Edges(); + edge.parseCanvasTransparency(canvas); + if (edge.isSimpleRect()) { + this.setXmlAttr("sysregion", "0"); + } else { + this._div.style.clipPath = edge.getPolygon(); + } + } + } + draw() { super.draw(); - this._div.setAttribute("data-obj-name", "Layer"); this._div.classList.add("webamp--img"); this._renderBackground(); } diff --git a/packages/webamp-modern-2/src/skin/makiClasses/Layout.ts b/packages/webamp-modern-2/src/skin/makiClasses/Layout.ts index 753e3ffb..da6aed05 100644 --- a/packages/webamp-modern-2/src/skin/makiClasses/Layout.ts +++ b/packages/webamp-modern-2/src/skin/makiClasses/Layout.ts @@ -1,6 +1,8 @@ import Group from "./Group"; import * as Utils from "../../utils"; import Container from "./Container"; +import { LEFT, RIGHT, TOP, BOTTOM, CURSOR, MOVE } from "../Cursor"; +import { px } from "../../utils"; // > A layout is a special kind of group, which shown inside a container. Each // > layout represents an appearance for that window. Layouts give you the ability @@ -13,6 +15,17 @@ import Container from "./Container"; export default class Layout extends Group { static GUID = "60906d4e482e537e94cc04b072568861"; _parentContainer: Container | null = null; + _resizingDiv: HTMLDivElement = null; + _resizing: boolean = false; + _resizable: number = 0; // combination of 4 directions: N/E/W/S + _movingStartX: number; //container XY + _movingStartY: number; + _moving: boolean = false; + + constructor() { + super(); + this._isLayout = true; + } setXmlAttr(key: string, value: string): boolean { if (super.setXmlAttr(key, value)) { @@ -53,8 +66,90 @@ export default class Layout extends Group { } } - draw() { - super.draw(); - this._div.setAttribute("data-obj-name", "Layout"); + setResizing(cmd: string, dx: number, dy: number) { + const clampW = (w): number => { + w = this._maximumWidth ? Math.min(w, this._maximumWidth) : w; + w = this._minimumWidth ? Math.max(w, this._minimumWidth) : w; + return w; + }; + const clampH = (h): number => { + h = this._maximumHeight ? Math.min(h, this._maximumHeight) : h; + h = this._minimumHeight ? Math.max(h, this._minimumHeight) : h; + return h; + }; + const r = this._div.getBoundingClientRect(); + if (cmd == "constraint") { + this._resizable = dx; + } else if (cmd == "start") { + this.bringtofront(); + this._resizing = true; + this._resizingDiv = document.createElement("div"); + this._resizingDiv.className = "resizing"; + this._resizingDiv.style.cssText = "position:absolute; top:0; left:0;"; + this._resizingDiv.style.width = px(r.width); + this._resizingDiv.style.height = px(r.height); + this._div.appendChild(this._resizingDiv); + } else if (dx == CURSOR && dy == CURSOR) { + this._resizingDiv.style.cursor = cmd; + } else if (cmd == "move") { + if (!this._resizing) { + return; + } + // console.log(`resizing dx:${dx} dy:${dy}`); + if (this._resizable & RIGHT) + this._resizingDiv.style.width = px(clampW(r.width + dx)); + if (this._resizable & BOTTOM) + this._resizingDiv.style.height = px(clampH(r.height + dy)); + if (this._resizable & LEFT) { + this._resizingDiv.style.left = px(dx); + this._resizingDiv.style.width = px(clampW(r.width + -dx)); + } + if (this._resizable & TOP) { + this._resizingDiv.style.top = px(dy); + this._resizingDiv.style.height = px(clampH(r.height + -dy)); + } + } else if (cmd == "final") { + if (!this._resizing) { + return; + } + this._resizing = false; + this.setXmlAttr("w", this._resizingDiv.offsetWidth.toString()); + this.setXmlAttr("h", this._resizingDiv.offsetHeight.toString()); + const container = this._parentContainer; + container.setXmlAttr( + "x", + (container._x + this._resizingDiv.offsetLeft).toString() + ); + container.setXmlAttr( + "y", + (container._y + this._resizingDiv.offsetTop).toString() + ); + this._resizingDiv.remove(); + this._resizingDiv = null; + } + } + + // MOVING THINGS ===================== + setMoving(cmd: string, dx: number, dy: number) { + const container = this._parentContainer; + if (cmd == "start") { + this._moving = true; + this._movingStartX = container._x; + this._movingStartY = container._y; + this.bringtofront(); + } else if (dx == CURSOR && dy == CURSOR) { + } else if (cmd == "move") { + if (!this._moving) { + return; + } + // console.log(`moving dx:${dx} dy:${dy}`); + container.setXmlAttr("x", (this._movingStartX + dx).toString()); + container.setXmlAttr("y", (this._movingStartY + dy).toString()); + } else if (cmd == "final") { + if (!this._moving) { + return; + } + this._moving = false; + } } } diff --git a/packages/webamp-modern-2/src/skin/makiClasses/Movable.ts b/packages/webamp-modern-2/src/skin/makiClasses/Movable.ts new file mode 100644 index 00000000..cda2a4a1 --- /dev/null +++ b/packages/webamp-modern-2/src/skin/makiClasses/Movable.ts @@ -0,0 +1,203 @@ +/** + * this file is needed to workaround of button-moving-layout issue + */ + +import { toBool } from "../../utils"; +import GuiObj from "./GuiObj"; + +import Layout from "./Layout"; +import { LEFT, RIGHT, TOP, BOTTOM, CURSOR, MOVE } from "../Cursor"; + +export default class Movable extends GuiObj { + _movable: boolean = false; + _resizable: number = 0; + _resize: string; + _resizingEventsRegistered: boolean = false; + _movingEventsRegistered: boolean = false; + + setXmlAttr(_key: string, value: string): boolean { + const key = _key.toLowerCase(); + if (super.setXmlAttr(key, value)) { + return true; + } + switch (key) { + case "move": + this._movable = toBool(value); + this._renderCssCursor(); + break; + case "resize": + this._resize = value == "0" ? "" : value; + this._renderCssCursor(); + break; + + default: + return false; + } + return true; + } + + _renderCssCursor() { + // only one of this possible: movable or resizable. not both + if (this._movable) { + this._unregisterResizingEvents(); + // winamp cursor for movable area is default/arrow. + this._div.style.removeProperty("cursor"); + this._resizable = MOVE; // = left + top - (width, height) + this._registerMovingEvents(); + } else { + this._unregisterMovingEvents(); + + switch (this._resize) { + case "right": + this._div.style.cursor = "e-resize"; + this._resizable = RIGHT; + break; + case "left": + this._div.style.cursor = "w-resize"; + this._resizable = LEFT; + break; + case "top": + this._div.style.cursor = "n-resize"; + this._resizable = TOP; + break; + case "bottom": + this._div.style.cursor = "s-resize"; + this._resizable = BOTTOM; + break; + case "topleft": + this._div.style.cursor = "nw-resize"; + this._resizable = TOP | LEFT; + break; + case "topright": + this._div.style.cursor = "ne-resize"; + this._resizable = TOP | RIGHT; + break; + case "bottomleft": + this._div.style.cursor = "sw-resize"; + this._resizable = BOTTOM | LEFT; + break; + case "bottomright": + this._div.style.cursor = "se-resize"; + this._resizable = BOTTOM | RIGHT; + break; + default: + this._div.style.removeProperty("cursor"); + this._resizable = 0; + } + + if (this._resizable != 0) { + this._registerResizingEvents(); + } else { + this._unregisterResizingEvents(); + } + } + } + + _registerResizingEvents() { + if (this._resizingEventsRegistered) { + return; + } + this._resizingEventsRegistered = true; + this._div.addEventListener("mousedown", this._handleResizing); + } + + _unregisterResizingEvents() { + if (this._resizingEventsRegistered) { + this._div.removeEventListener("mousedown", this._handleResizing); + this._resizingEventsRegistered = false; + } + } + + _handleResizing = (downEvent: MouseEvent) => { + downEvent.stopPropagation(); + if (downEvent.button != 0) return; // only care LeftButton + const layout = this.getparentlayout() as Layout; + layout.setResizing("constraint", this._resizable, 0); + layout.setResizing("start", 0, 0); + layout.setResizing( + this._div.style.getPropertyValue("cursor"), + CURSOR, + CURSOR + ); + const startX = downEvent.clientX; + const startY = downEvent.clientY; + + const handleMove = (moveEvent: MouseEvent) => { + const newMouseX = moveEvent.clientX; + const newMouseY = moveEvent.clientY; + const deltaY = newMouseY - startY; + const deltaX = newMouseX - startX; + layout.setResizing("move", deltaX, deltaY); + }; + + const handleMouseUp = (upEvent: MouseEvent) => { + upEvent.stopPropagation(); + if (upEvent.button != 0) return; // only care LeftButton + document.removeEventListener("mousemove", handleMove); + document.removeEventListener("mouseup", handleMouseUp); + const newMouseX = upEvent.clientX; + const newMouseY = upEvent.clientY; + const deltaY = newMouseY - startY; + const deltaX = newMouseX - startX; + layout.setResizing("final", deltaX, deltaY); + }; + document.addEventListener("mousemove", handleMove); + document.addEventListener("mouseup", handleMouseUp); + }; + + _registerMovingEvents() { + if (this._movingEventsRegistered) { + return; + } + this._movingEventsRegistered = true; + this._div.addEventListener("mousedown", this._handleMoving); + } + + _unregisterMovingEvents() { + if (this._movingEventsRegistered) { + this._div.removeEventListener("mousedown", this._handleMoving); + this._movingEventsRegistered = false; + } + } + + _handleMoving = (downEvent: MouseEvent) => { + downEvent.stopPropagation(); + if (downEvent.button != 0) return; // only care LeftButton + const layout = this.getparentlayout() as Layout; + layout.setMoving("start", 0, 0); + const startX = downEvent.clientX; + const startY = downEvent.clientY; + + const handleMove = (moveEvent: MouseEvent) => { + const newMouseX = moveEvent.clientX; + const newMouseY = moveEvent.clientY; + const deltaY = newMouseY - startY; + const deltaX = newMouseX - startX; + layout.setMoving("move", deltaX, deltaY); + }; + + const handleMouseUp = (upEvent: MouseEvent) => { + if (upEvent.button != 0) return; // only care LeftButton + upEvent.stopPropagation(); + document.removeEventListener("mousemove", handleMove); + document.removeEventListener("mouseup", handleMouseUp); + const newMouseX = upEvent.clientX; + const newMouseY = upEvent.clientY; + const deltaY = newMouseY - startY; + const deltaX = newMouseX - startX; + layout.setMoving("final", deltaX, deltaY); + }; + document.addEventListener("mousemove", handleMove); + document.addEventListener("mouseup", handleMouseUp); + }; + + draw() { + super.draw(); + if (this._movable || this._resizable) { + this._div.style.pointerEvents = "auto"; + } else if (this._ghost) { + this._div.style.pointerEvents = "none"; + this._div.style.setProperty("--pointer-events-by", "movable"); + } + } +} diff --git a/packages/webamp-modern-2/src/skin/makiClasses/Slider.ts b/packages/webamp-modern-2/src/skin/makiClasses/Slider.ts index 8a788683..1a6a5fcf 100644 --- a/packages/webamp-modern-2/src/skin/makiClasses/Slider.ts +++ b/packages/webamp-modern-2/src/skin/makiClasses/Slider.ts @@ -30,6 +30,7 @@ export default class Slider extends GuiObj { constructor() { super(); this._thumbDiv.addEventListener("mousedown", (downEvent: MouseEvent) => { + downEvent.stopPropagation(); const bitmap = UI_ROOT.getBitmap(this._thumb); const startX = downEvent.clientX; const startY = downEvent.clientY; @@ -38,6 +39,7 @@ export default class Slider extends GuiObj { const initialPostition = this._position; const handleMove = (moveEvent: MouseEvent) => { + moveEvent.stopPropagation(); const newMouseX = moveEvent.clientX; const newMouseY = moveEvent.clientY; const deltaY = newMouseY - startY; @@ -53,7 +55,8 @@ export default class Slider extends GuiObj { this.onsetposition(this.getposition()); }; - const handleMouseUp = () => { + const handleMouseUp = (upEvent: MouseEvent) => { + upEvent.stopPropagation(); UI_ROOT.vm.dispatch(this, "onsetfinalposition", [ { type: "INT", value: this.getposition() }, ]); diff --git a/packages/webamp-modern-2/src/skin/makiClasses/SystemObject.ts b/packages/webamp-modern-2/src/skin/makiClasses/SystemObject.ts index 9151041d..c9f1aaf7 100644 --- a/packages/webamp-modern-2/src/skin/makiClasses/SystemObject.ts +++ b/packages/webamp-modern-2/src/skin/makiClasses/SystemObject.ts @@ -20,10 +20,12 @@ export default class SystemObject extends BaseObject { static GUID = "d6f50f6449b793fa66baf193983eaeef"; _parentGroup: Group; _parsedScript: ParsedMaki; + _param: string; - constructor(parsedScript: ParsedMaki) { + constructor(parsedScript: ParsedMaki, param: string) { super(); this._parsedScript = parsedScript; + this._param = param; UI_ROOT.audio.onSeek(() => { UI_ROOT.vm.dispatch(this, "onseek", [ { type: "INT", value: UI_ROOT.audio.getCurrentTimePercent() * 255 }, @@ -481,8 +483,8 @@ export default class SystemObject extends BaseObject { * * @ret The parameter for the script. */ - getparam() { - // TODO + getparam(): string { + return this._param; } /** @@ -543,7 +545,7 @@ export default class SystemObject extends BaseObject { * @param group_id The identifier for the group you want to create. */ newgroup(group_id: string): Group { - //TODO + return this._parentGroup.findobject(group_id) as Group; } /** @@ -1069,7 +1071,7 @@ export default class SystemObject extends BaseObject { * The index starts at 0, not 1, so be careful. * Here's a short example: * getToken("1,2,3,4,5", ",", 3); - * Would return, 3. If the token you ask for doesn't exist, an + * Would return, 4. If the token you ask for doesn't exist, an * empty string is returned. * * @ret The token requested. @@ -1078,7 +1080,9 @@ export default class SystemObject extends BaseObject { * @param tokennum The token to retreive. */ gettoken(str: string, separator: string, tokennum: number) { - // TODO + // getToken("28,39,-56,-84,0,0,1,1", ",", 3) will return "-84" + const commas = str.split(separator); + return commas[tokennum] || ""; } /** diff --git a/packages/webamp-modern-2/src/skin/makiClasses/ToggleButton.ts b/packages/webamp-modern-2/src/skin/makiClasses/ToggleButton.ts index 1525bfac..29fc940c 100644 --- a/packages/webamp-modern-2/src/skin/makiClasses/ToggleButton.ts +++ b/packages/webamp-modern-2/src/skin/makiClasses/ToggleButton.ts @@ -3,15 +3,19 @@ import Button from "./Button"; // http://wiki.winamp.com/wiki/XML_GUI_Objects#.3Cbutton.2F.3E_.26_.3Ctogglebutton.2F.3E export default class ToggleButton extends Button { static GUID = "b4dccfff4bcc81fe0f721b96ff0fbed5"; - setXmlAttr(key: string, value: string): boolean { - if (super.setXmlAttr(key, value)) { - return true; - } - switch (key) { - default: - return false; - } - return true; + + getElTag(): string { + return "button"; + } + + /** + * This method is called by Button + */ + _handleMouseDown(e: MouseEvent) { + // don't send to parent to start move/resizing + e.stopPropagation(); + // implementation of standard mouse down + this.setactivated(!this._active); } draw() { diff --git a/packages/webamp-modern-2/src/skin/makiClasses/WasabiFrame.ts b/packages/webamp-modern-2/src/skin/makiClasses/WasabiFrame.ts new file mode 100644 index 00000000..78dfacb9 --- /dev/null +++ b/packages/webamp-modern-2/src/skin/makiClasses/WasabiFrame.ts @@ -0,0 +1,60 @@ +import Group from "./Group"; +import UI_ROOT from "../../UIRoot"; +import { num } from "../../utils"; + +export default class WasabiFrame extends Group { + __inited: boolean = false; + _content: string; + _shade: string; + _padtitleleft: string; + _padtitleright: string; + + getElTag(): string { + return "wasabiframe"; + } + + setXmlAttr(_key: string, value: string): boolean { + const lowerkey = _key.toLowerCase(); + // console.log('wasabi:frame.key=',lowerkey,':=', value) + if (super.setXmlAttr(lowerkey, value)) { + return true; + } + switch (lowerkey) { + case "content": + this._content = value; + break; + case "shade": + this._shade = value; + break; + case "padtitleleft": + this._padtitleleft = value; + break; + case "padtitleright": + this._padtitleright = value; + break; + default: + return false; + } + return true; + } + + init() { + // console.error('wasabi:standard->> INITing:', this._content) + if (this.__inited) return; + this.__inited = true; + + super.init(); + + for (const systemObject of this._systemObjects) { + ["content", "padtitleleft", "padtitleright", "shade"].forEach((att) => { + const myValue = this["_" + att]; + if (myValue != null) { + UI_ROOT.vm.dispatch(systemObject, "onsetxuiparam", [ + { type: "STRING", value: att }, + { type: "STRING", value: myValue }, + ]); + } + }); + } + } +} diff --git a/packages/webamp-modern-2/src/skin/parse.ts b/packages/webamp-modern-2/src/skin/parse.ts index d24828b9..714947f3 100644 --- a/packages/webamp-modern-2/src/skin/parse.ts +++ b/packages/webamp-modern-2/src/skin/parse.ts @@ -23,6 +23,7 @@ import Color from "./Color"; import GammaGroup from "./GammaGroup"; import ColorThemesList from "./ColorThemesList"; import { UIRoot } from "../UIRoot"; +import WasabiFrame from "./makiClasses/WasabiFrame"; class ParserContext { container: Container | null = null; @@ -142,13 +143,17 @@ export default class SkinParser { return this.colorThemesList(node); case "status": return this.status(node); - case "wasabi:standardframe:nostatus": case "wasabi:mainframe:nostatus": + case "wasabi:medialibraryframe:nostatus": + case "wasabi:playlistframe:nostatus": + case "wasabi:standardframe:nostatus": + case "wasabi:standardframe:status": + case "wasabi:visframe:nostatus": + return this.wasabiFrame(node); case "nstatesbutton": case "componentbucket": case "playlisteditor": case "wasabi:tabsheet": - case "wasabi:standardframe:status": case "snappoint": case "accelerators": case "elementalias": @@ -200,6 +205,70 @@ export default class SkinParser { this.addToGroup(group); } + async wasabiFrame(node: XmlElement) { + const frame = new WasabiFrame(); + const previousParent = this._context.parentGroup; + + //? Search Wasabi Inheritace + const xuitag: string = node.name; //Wasabi:MainFrame:NoStatus + const xuiEl: XmlElement = this._uiRoot.getXuiElement(xuitag); + if (xuiEl) { + const xuiFrame = new XmlElement("dummy", { id: xuiEl.attributes.id }); + await this.maybeApplyGroupDef(frame, xuiFrame); + } else { + const groupdef_id = this._getWasabiGroupDef(node.name); + const groupDef = this._uiRoot.getGroupDef(groupdef_id); + if (groupDef) { + await this.maybeApplyGroupDef(frame, groupDef); + // console.log('WasabiFrame success to apply groupDef.id=', groupdef_id) + } else { + // console.warn('WasabiFrame failed to apply groupDef.id=', groupdef_id) + } + } + frame.setXmlAttributes(node.attributes); + + //?content + if (node.attributes.content) { + this._context.parentGroup = frame; + await this.group( + new XmlElement("group", { + id: node.attributes.content, + w: "0", + h: "0", + relatw: "1", + relath: "1", + }) + ); + } + this._context.parentGroup = previousParent; + this.addToGroup(frame); + } + + /** taken from Winamp Modern skin */ + _getWasabiGroupDef(xmlTag: string): string { + switch (xmlTag.toLowerCase()) { + case "wasabi:mainframe:nostatus": + return "wasabi.mainframe.nostatusbar"; + case "wasabi:medialibraryframe:nostatus": + return "wasabi.medialibraryframe.nostatusbar"; + case "wasabi:playlistframe:nostatus": + return "wasabi.playlistframe.nostatusbar"; + case "wasabi:standardframe:modal": + return "wasabi.standardframe.modal"; + case "wasabi:standardframe:nostatus": + return "wasabi.standardframe.nostatusbar"; + case "wasabi:standardframe:static": + return "wasabi.standardframe.static"; + case "wasabi:standardframe:status": + return "wasabi.standardframe.statusbar"; + case "wasabi:visframe:nostatus": + return "wasabi.visframe.nostatusbar"; + default: + console.warn(`Unhandled : ${xmlTag}`); + return; + } + } + async bitmap(node: XmlElement) { assume( node.children.length === 0, @@ -248,7 +317,7 @@ export default class SkinParser { "Unexpected children in