mirror of
https://github.com/captbaritone/webamp.git
synced 2026-08-03 07:23:55 +00:00
Show ColorTheme Form, Movable window/layer (#1161)
* Toggle container (action) it works but the container is currently empty. * Wasabi:Frame:Xxx : parse,load,init this patch has no error, but not visible due relatW/relatH not available * implement relatW/relatH This patch shows the bigColorTheme window * movable,resizable Tested with MMD3 only * bugfix: unexpected gotoTarget result * allow unregister eventListener, etc. minor. * make the group movable * trial to move MMD3 main window * Prettier. * bugfix invalid clip-path value * speedup devtool: simplify tag,style * bugfix clip-path. Issue: the bigColorTheme didn't rendered properly * mute console * avoid lowercasing the color-theme-name * avoid double negative historically, it was easy to understand : 100% - 12px due I think there shall never be 100% + 12px. BUT they did! there is something 100% + 99px ! Co-authored-by: Jordan Eldredge <jordan@jordaneldredge.com> * del dead code Co-authored-by: Jordan Eldredge <jordan@jordaneldredge.com> * del dead code Co-authored-by: Jordan Eldredge <jordan@jordaneldredge.com> * unregisterable self-event-listener Co-authored-by: Jordan Eldredge <jordan@jordaneldredge.com> * del dead code Co-authored-by: Jordan Eldredge <jordan@jordaneldredge.com> * improve readability Co-authored-by: Jordan Eldredge <jordan@jordaneldredge.com> * improve readability Co-authored-by: Jordan Eldredge <jordan@jordaneldredge.com> * del dead code Co-authored-by: Jordan Eldredge <jordan@jordaneldredge.com> * del dead code Co-authored-by: Jordan Eldredge <jordan@jordaneldredge.com> * del dead code Co-authored-by: Jordan Eldredge <jordan@jordaneldredge.com> * del dead code Co-authored-by: Jordan Eldredge <jordan@jordaneldredge.com> * relat() -> relative() * del "../winamp modern" (specific skin only) * prettier * avoid exposing private on other class. * del dead code, +comments * del dead code Co-authored-by: Jordan Eldredge <jordan@jordaneldredge.com> * del dead code Co-authored-by: Jordan Eldredge <jordan@jordaneldredge.com> * del dead code Co-authored-by: Jordan Eldredge <jordan@jordaneldredge.com> * typo Co-authored-by: Jordan Eldredge <jordan@jordaneldredge.com> * del dead code Co-authored-by: Jordan Eldredge <jordan@jordaneldredge.com> * +comment, prettier * remove bind() for better unregister event listener * del dead code Co-authored-by: Jordan Eldredge <jordan@jordaneldredge.com> * del dead code Co-authored-by: Jordan Eldredge <jordan@jordaneldredge.com> * add note of what going on. Co-authored-by: Jordan Eldredge <jordan@jordaneldredge.com> * warn for premateur bitmap that has no id. * del dead code * warn for unknown/unlisted guid * remove seekerGhost transparency hack, prettier. * typo * guarding move vs resize to be not overlapped * bugfix invalid css caused by empty array * del dead var * del premateur logic (no use case with MMD3) * add comment: clarify that getToken is 0 based. * bugfix typo of ambiguous var name. @captbaritone: having w and W in the same namespace is too confusing. * del confusing code Co-authored-by: Fathony <fathony@smart-leaders.net> Co-authored-by: Jordan Eldredge <jordan@jordaneldredge.com>
This commit is contained in:
parent
9217429145
commit
8ae08dce40
25 changed files with 1373 additions and 243 deletions
Binary file not shown.
|
|
@ -18,6 +18,9 @@ export class UIRoot {
|
|||
_colors: Color[] = [];
|
||||
_groupDefs: XmlElement[] = [];
|
||||
_gammaSets: Map<string, GammaGroup[]> = 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();
|
||||
|
|
|
|||
43
packages/webamp-modern-2/src/clip_path.html
Normal file
43
packages/webamp-modern-2/src/clip_path.html
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="Webamp Modern" />
|
||||
<title>Webamp Modern Test Suite</title>
|
||||
<style>
|
||||
body { margin: 0; padding: 50px; image-rendering: pixelated;}
|
||||
.i {background: fuchsia; padding: 0;}
|
||||
u {background: goldenrod; display: inline-flex; line-height: 1; padding: 0; pointer-events: all;}
|
||||
.centered {padding: 10px; background-color: aquamarine;}
|
||||
.table {display: flex; width: 800px;}
|
||||
.col { min-width: 25%; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="table">
|
||||
<div id="clickable" class="centered">
|
||||
click me! +->
|
||||
<u id="app">
|
||||
<!-- <img id="img1" src="/assets/skins/webamp/window/main.png"/> -->
|
||||
<!-- <img id="img1" src="/assets/skins/webamp/window/main2.png"/> -->
|
||||
<!-- <img id="img1" src="/assets/extracted/skins/MMD3/player/Bgoverlay.png"/> -->
|
||||
<img id="img1" src="/assets/extracted/skins/MMD3/component/comp-right-stretch-bg.png"/>
|
||||
<!-- <img id="img1" src="/assets/skins/webamp/player/videoavs_button_bg.png" /> -->
|
||||
</u>
|
||||
</div>
|
||||
|
||||
<div class="i">
|
||||
<canvas id="canvas"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="clip_path.js"></script>
|
||||
|
||||
<div class="table">
|
||||
<div class="col">Top: <pre id="top"></pre></div>
|
||||
<div class="col">Right: <pre id="right"></pre></div>
|
||||
<div class="col">Bottom: <pre id="bottom"></pre></div>
|
||||
<div class="col">Left: <pre id="left"></pre></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
35
packages/webamp-modern-2/src/clip_path.ts
Normal file
35
packages/webamp-modern-2/src/clip_path.ts
Normal file
|
|
@ -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();
|
||||
|
|
@ -4,30 +4,48 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="Webamp Modern" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="assets/favicon.ico"/>
|
||||
<link rel="shortcut icon" type="image/x-icon" href="assets/favicon.ico" />
|
||||
<title>Webamp Modern</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background-color: rgb(58, 110, 165);
|
||||
}
|
||||
textarea,
|
||||
input,
|
||||
select {
|
||||
font-size: 10.5px;
|
||||
}
|
||||
textarea:focus,
|
||||
input:focus,
|
||||
select:focus {
|
||||
outline: none;
|
||||
}
|
||||
.webamp--img {
|
||||
background-image: var(--background-image);
|
||||
background-position: var(--background-position);
|
||||
background-position: top 0 left 0;
|
||||
}
|
||||
|
||||
.webamp--img:active {
|
||||
background-image: var(--active-background-image, var(--background-image));
|
||||
background-position: var(--active-background-position, var(--background-position));
|
||||
background-image: var(--down-background-image, var(--background-image));
|
||||
}
|
||||
.webamp--img:hover {
|
||||
background-image: var(--hover-background-image, var(--background-image));
|
||||
background-position: var(--hover-background-position, var(--background-position));
|
||||
background-image: var(
|
||||
--hover-background-image,
|
||||
var(--background-image)
|
||||
);
|
||||
}
|
||||
|
||||
/* 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));
|
||||
background-image: var(--down-background-image, var(--background-image));
|
||||
}
|
||||
|
||||
.webamp--img.active {
|
||||
background-image: var(
|
||||
--active-background-image,
|
||||
var(--background-image)
|
||||
);
|
||||
}
|
||||
|
||||
#experimental {
|
||||
|
|
@ -44,6 +62,129 @@
|
|||
left: 0;
|
||||
text-align: center;
|
||||
}
|
||||
button {
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
slider {
|
||||
overflow: hidden;
|
||||
}
|
||||
text {
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
/* padding: 2px; */
|
||||
}
|
||||
text i {
|
||||
pointer-events: none;
|
||||
font-style: normal;
|
||||
}
|
||||
text wrap {
|
||||
background-image: inherit;
|
||||
background-size: 0px;
|
||||
position: relative;
|
||||
/* line-height: 1; */
|
||||
height: 100%;
|
||||
width: var(--full-width);
|
||||
min-width: 100%;
|
||||
}
|
||||
text wrap[font="BitmapFont"] {
|
||||
display: flex;
|
||||
/* vertical align: */
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
text span {
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
display: inline-block;
|
||||
background-image: inherit;
|
||||
/* vertical-align: bottom; */
|
||||
color: transparent;
|
||||
width: var(--charwidth);
|
||||
height: var(--charheight);
|
||||
background-position-x: var(--x);
|
||||
background-position-y: var(--y);
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
/* body > div * */
|
||||
container {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
layout,
|
||||
grid,
|
||||
progressgrid,
|
||||
group,
|
||||
layer,
|
||||
animatedlayer,
|
||||
button,
|
||||
slider,
|
||||
text,
|
||||
wasabiframe,
|
||||
wasabititlebar,
|
||||
colorthemeslist {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
display: block;
|
||||
}
|
||||
wasabititlebar {
|
||||
text-align: center;
|
||||
}
|
||||
grid,
|
||||
progressgrid {
|
||||
display: flex;
|
||||
}
|
||||
grid *,
|
||||
progressgrid * {
|
||||
height: 100%;
|
||||
background-image: var(--background-image);
|
||||
}
|
||||
grid middle {
|
||||
flex-grow: 1;
|
||||
}
|
||||
/* grid left, gridprogress left {left: 0;}
|
||||
grid right, gridprogress right {right: 0;} */
|
||||
/* body > div select>option {
|
||||
position: unset;
|
||||
display: block;
|
||||
} */
|
||||
.wasabi-button {
|
||||
border: 3px solid transparent;
|
||||
--border-image: var(--bitmap-studio-button);
|
||||
border-image: var(--border-image) 3 fill/ 1 / 0px stretch;
|
||||
}
|
||||
.wasabi-button:active {
|
||||
--border-image: var(--bitmap-studio-button-pressed);
|
||||
}
|
||||
|
||||
.autowidthsource {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* titleBar active state */
|
||||
[inactivealpha="0"] {
|
||||
opacity: 0;
|
||||
}
|
||||
[inactivealpha="128"] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
container:focus-within [activealpha="0"],
|
||||
container:active [activealpha="0"] {
|
||||
opacity: 0;
|
||||
}
|
||||
container:focus-within [inactivealpha],
|
||||
container:active [inactivealpha] {
|
||||
opacity: 1;
|
||||
}
|
||||
.resizing {
|
||||
border: 1px solid blue;
|
||||
background-color: rgba(74, 74, 251, 0.205);
|
||||
z-index: 1000;
|
||||
}
|
||||
</style>
|
||||
<style id="bitmap-css"></style>
|
||||
</head>
|
||||
|
|
@ -52,4 +193,4 @@
|
|||
<div id="experimental">Work in Progress</div>
|
||||
<script type="module" src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
162
packages/webamp-modern-2/src/skin/Clippath.ts
Normal file
162
packages/webamp-modern-2/src/skin/Clippath.ts
Normal file
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
8
packages/webamp-modern-2/src/skin/Cursor.ts
Normal file
8
packages/webamp-modern-2/src/skin/Cursor.ts
Normal file
|
|
@ -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;
|
||||
|
|
@ -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 <bitmap> didn't has explicit "w" attribute
|
||||
const safeWidth = w || img.width;
|
||||
const safeHeight = h || img.height;
|
||||
// because some <bitmap> 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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
203
packages/webamp-modern-2/src/skin/makiClasses/Movable.ts
Normal file
203
packages/webamp-modern-2/src/skin/makiClasses/Movable.ts
Normal file
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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() },
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -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] || "";
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
60
packages/webamp-modern-2/src/skin/makiClasses/WasabiFrame.ts
Normal file
60
packages/webamp-modern-2/src/skin/makiClasses/WasabiFrame.ts
Normal file
|
|
@ -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 },
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 <Wasabi:Frame:Tag>: ${xmlTag}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async bitmap(node: XmlElement) {
|
||||
assume(
|
||||
node.children.length === 0,
|
||||
|
|
@ -248,7 +317,7 @@ export default class SkinParser {
|
|||
"Unexpected children in <script> XML node."
|
||||
);
|
||||
|
||||
const { file, id } = node.attributes;
|
||||
const { file, param } = node.attributes;
|
||||
assert(file != null, "Script element missing `file` attribute");
|
||||
// assert(id != null, "Script element missing `id` attribute");
|
||||
|
||||
|
|
@ -260,7 +329,7 @@ export default class SkinParser {
|
|||
// TODO: Try catch?
|
||||
const parsedScript = parseMaki(scriptContents);
|
||||
|
||||
const systemObj = new SystemObject(parsedScript);
|
||||
const systemObj = new SystemObject(parsedScript, param);
|
||||
|
||||
// TODO: Need to investigate how scripts find their group. In corneramp, the
|
||||
// script itself is not in any group. `xml/player.xml:8
|
||||
|
|
|
|||
|
|
@ -30,6 +30,11 @@ export function px(size: number): string {
|
|||
return `${size}px`;
|
||||
}
|
||||
|
||||
export function relative(size: number): string {
|
||||
if (size === 0) return "100%";
|
||||
return `calc(100% + ${size}px)`;
|
||||
}
|
||||
|
||||
export function toBool(str: string) {
|
||||
assert(
|
||||
str === "0" || str === "1",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue