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:
Fathony Luthfillah 2022-03-20 14:47:13 +07:00 committed by GitHub
parent 9217429145
commit 8ae08dce40
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 1373 additions and 243 deletions

View file

@ -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();

View 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! +-&gt;
<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>

View 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();

View file

@ -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>

View file

@ -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);

View file

@ -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(

View file

@ -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;
}

View 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.
}
}

View file

@ -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(

View 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;

View file

@ -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();

View file

@ -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();
}

View file

@ -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();
}
}

View file

@ -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();

View file

@ -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);

View file

@ -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();
}

View file

@ -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;
}
}
}

View 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");
}
}
}

View file

@ -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() },
]);

View file

@ -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] || "";
}
/**

View file

@ -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() {

View 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 },
]);
}
});
}
}
}

View file

@ -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

View file

@ -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",