mirror of
https://github.com/captbaritone/webamp.git
synced 2026-07-22 09:37:17 +00:00
Do gammagroups in canvas
This commit is contained in:
parent
9f121f1dff
commit
2aa5efd993
9 changed files with 175 additions and 141 deletions
|
|
@ -10,12 +10,12 @@ import GammaGroup from "./skin/GammaGroup";
|
|||
class UIRoot {
|
||||
// Just a temporary place to stash things
|
||||
_bitmaps: Bitmap[] = [];
|
||||
_bitmapImages: Map<string, HTMLImageElement> = new Map();
|
||||
_fonts: (TrueTypeFont | BitmapFont)[] = [];
|
||||
_colors: Color[] = [];
|
||||
_groupDefs: XmlElement[] = [];
|
||||
_gammaSets: Map<string, GammaGroup[]> = new Map();
|
||||
_xuiElements: XmlElement[] = [];
|
||||
_activeGammaSet: GammaGroup[] | null = null;
|
||||
|
||||
addBitmap(bitmap: Bitmap) {
|
||||
this._bitmaps.push(bitmap);
|
||||
|
|
@ -32,10 +32,6 @@ class UIRoot {
|
|||
return found;
|
||||
}
|
||||
|
||||
addBitmapImage(id: string, image: HTMLImageElement) {
|
||||
this._bitmapImages.set(id, image);
|
||||
}
|
||||
|
||||
addFont(font: TrueTypeFont | BitmapFont) {
|
||||
this._fonts.push(font);
|
||||
}
|
||||
|
|
@ -85,7 +81,7 @@ class UIRoot {
|
|||
this._gammaSets.set(id.toLowerCase(), gammaSet);
|
||||
}
|
||||
|
||||
getGammaSet(id: string): GammaGroup[] {
|
||||
enableGammaSet(id: string) {
|
||||
const found = this._gammaSets.get(id.toLowerCase());
|
||||
assume(
|
||||
found != null,
|
||||
|
|
@ -93,10 +89,11 @@ class UIRoot {
|
|||
this._gammaSets.keys()
|
||||
).join(", ")}`
|
||||
);
|
||||
return found;
|
||||
this._activeGammaSet = found;
|
||||
this._setCssVars();
|
||||
}
|
||||
|
||||
getDefaultGammaSet(): GammaGroup[] {
|
||||
enableDefaultGammaSet() {
|
||||
const found = Array.from(this._gammaSets.values())[0];
|
||||
assume(
|
||||
found != null,
|
||||
|
|
@ -104,9 +101,44 @@ class UIRoot {
|
|||
this._gammaSets.keys()
|
||||
).join(", ")}`
|
||||
);
|
||||
this._activeGammaSet = found;
|
||||
this._setCssVars();
|
||||
}
|
||||
|
||||
_getGammaGroup(id: string): GammaGroup {
|
||||
const lower = id.toLowerCase();
|
||||
const found = this._activeGammaSet.find((gammaGroup) => {
|
||||
return gammaGroup.getId().toLowerCase() === lower;
|
||||
});
|
||||
assume(found != null, `Cold not find a gammagroup for "${id}"`);
|
||||
return found;
|
||||
}
|
||||
|
||||
_setCssVars() {
|
||||
const map = new Map();
|
||||
for (const bitmap of this._bitmaps) {
|
||||
const img = bitmap.getImg();
|
||||
const groupId = bitmap.getGammaGroup();
|
||||
if (!map.has(img)) {
|
||||
map.set(img, new Map());
|
||||
}
|
||||
const imgCache = map.get(img);
|
||||
if (!imgCache.has(groupId)) {
|
||||
const gammaGroup =
|
||||
groupId != null ? this._getGammaGroup(groupId) : null;
|
||||
const url =
|
||||
gammaGroup == null ? img.src : gammaGroup.transformImage(img);
|
||||
imgCache.set(groupId, url);
|
||||
}
|
||||
const url = imgCache.get(groupId);
|
||||
// TODO: Techincally we only need one per image/gammagroup.
|
||||
document.documentElement.style.setProperty(
|
||||
bitmap.getCSSVar(),
|
||||
`url(${url})`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getXuiElement(name: string): XmlElement | null {
|
||||
const lowercaseName = name.toLowerCase();
|
||||
const found = this._xuiElements.find(
|
||||
|
|
|
|||
|
|
@ -10,27 +10,37 @@
|
|||
margin: 0;
|
||||
background-color: rgb(58, 110, 165);
|
||||
}
|
||||
.webamp--guiobj {
|
||||
.webamp--img {
|
||||
background-image: var(--background-image);
|
||||
background-position: var(--background-position);
|
||||
}
|
||||
.webamp--guiobj:active {
|
||||
.webamp--img:active {
|
||||
background-image: var(--active-background-image, var(--background-image));
|
||||
background-position: var(--active-background-position, var(--background-position));
|
||||
}
|
||||
.webamp--guiobj:hover {
|
||||
.webamp--img:hover {
|
||||
background-image: var(--hover-background-image, var(--background-image));
|
||||
background-position: var(--hover-background-position, var(--background-position));
|
||||
}
|
||||
|
||||
.webamp--guiobj:hover:active {
|
||||
/* TODO: Should this fallback to hover? */
|
||||
.webamp--img:hover:active {
|
||||
background-image: var(--active-background-image, var(--background-image));
|
||||
background-position: var(--active-background-position, var(--background-position));
|
||||
}
|
||||
|
||||
#experimental {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
color: white;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<div id="experimental">Work in Progress</div>
|
||||
<script type="module" src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -3,7 +3,6 @@ import JSZip from "jszip";
|
|||
import { classResolver } from "./skin/resolver";
|
||||
import SkinParser from "./skin/parse";
|
||||
import UI_ROOT from "./UIRoot";
|
||||
import GammaGroup from "./skin/GammaGroup";
|
||||
|
||||
function hack() {
|
||||
// Without this Snowpack will try to treeshake out resolver causing a circular
|
||||
|
|
@ -24,15 +23,31 @@ async function main() {
|
|||
|
||||
let node = document.createElement("div");
|
||||
|
||||
UI_ROOT.enableDefaultGammaSet();
|
||||
|
||||
for (const container of parser._containers) {
|
||||
container.draw();
|
||||
node.appendChild(container.getDiv());
|
||||
}
|
||||
|
||||
const gammaSet = UI_ROOT.getDefaultGammaSet();
|
||||
const select = document.createElement("select");
|
||||
select.style.position = "absolute";
|
||||
select.style.bottom = "0px";
|
||||
select.style.left = "0px";
|
||||
select.addEventListener("change", (e) => {
|
||||
console.log(e.target.value);
|
||||
UI_ROOT.enableGammaSet(e.target.value);
|
||||
});
|
||||
for (const set of UI_ROOT._gammaSets.keys()) {
|
||||
const option = document.createElement("option");
|
||||
option.innerText = set;
|
||||
option.value = set;
|
||||
select.appendChild(option);
|
||||
}
|
||||
|
||||
document.body.appendChild(select);
|
||||
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = makeGammaSetSVG(gammaSet);
|
||||
document.body.appendChild(div);
|
||||
|
||||
document.body.appendChild(node);
|
||||
|
|
@ -44,34 +59,4 @@ async function main() {
|
|||
console.log("INIT");
|
||||
}
|
||||
|
||||
function makeGammaSetSVG(gammaSet: GammaGroup[]) {
|
||||
// 4000,510,-4000
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 300">
|
||||
<defs>
|
||||
${gammaSet.map((group) => makeGammaGroupSVG(group)).join("\n")}
|
||||
</defs>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
function colorToFrac(num: string) {
|
||||
// -4096 to 4096
|
||||
return Number(num) / 4096;
|
||||
}
|
||||
|
||||
// https://webplatform.github.io/docs/svg/elements/feFuncA/
|
||||
// amplitude * pow(C, exponent) + offset (see below for amplitude, exponent, and offset)
|
||||
// https://www.pawelporwisz.pl/winamp/wct_en.php
|
||||
// TODO: Avoid XSS
|
||||
function makeGammaGroupSVG(gammaGroup: GammaGroup) {
|
||||
const [r, g, b] = gammaGroup._value.split(",").map(colorToFrac);
|
||||
return `<filter id="${gammaGroup.getDomId()}" x="0" y="0" width="100%" height="100%">
|
||||
<feComponentTransfer>
|
||||
<feFuncR type="gamma" amplitude="1" exponent="1" offset="${r}" />
|
||||
<feFuncG type="gamma" amplitude="1" exponent="1" offset="${g}" />
|
||||
<feFuncB type="gamma" amplitude="1" exponent="1" offset="${b}" />
|
||||
|
||||
</feComponentTransfer>
|
||||
</filter>`;
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import * as Utils from "../utils";
|
||||
import { assert, getId } from "../utils";
|
||||
import { assert, getId, normalizeDomId, num, px } from "../utils";
|
||||
import ImageManager from "./ImageManager";
|
||||
|
||||
// http://wiki.winamp.com/wiki/XML_Elements#.3Cbitmap.2F.3E
|
||||
export default class Bitmap {
|
||||
_uniqueId: number = getId();
|
||||
_id: string;
|
||||
_cssVar: string;
|
||||
_url: string;
|
||||
_img: HTMLImageElement;
|
||||
_canvas: HTMLCanvasElement;
|
||||
|
|
@ -27,21 +26,26 @@ export default class Bitmap {
|
|||
switch (key) {
|
||||
case "id":
|
||||
this._id = value;
|
||||
this._cssVar = `--bitmap-${this.getId().replace(
|
||||
/[^a-zA-Z0-9]/g,
|
||||
"-"
|
||||
)}-${getId()}`;
|
||||
break;
|
||||
case "x":
|
||||
this._x = Utils.num(value) ?? 0;
|
||||
this._x = num(value) ?? 0;
|
||||
break;
|
||||
case "y":
|
||||
this._y = Utils.num(value) ?? 0;
|
||||
this._y = num(value) ?? 0;
|
||||
break;
|
||||
case "w":
|
||||
this._width = Utils.num(value);
|
||||
this._width = num(value);
|
||||
break;
|
||||
case "h":
|
||||
this._height = Utils.num(value);
|
||||
this._height = num(value);
|
||||
break;
|
||||
case "file":
|
||||
this._file = value;
|
||||
break;
|
||||
case "gammagroup":
|
||||
this._gammagroup = value;
|
||||
break;
|
||||
|
|
@ -64,53 +68,71 @@ export default class Bitmap {
|
|||
}
|
||||
|
||||
getCSSVar(): string {
|
||||
return `--bitmap-${this.getId().replace(/[^a-zA-Z0-9]/g, "-")}-${
|
||||
this._uniqueId
|
||||
}`;
|
||||
return this._cssVar;
|
||||
}
|
||||
|
||||
setUrl(url: string) {
|
||||
document.documentElement.style.setProperty(this.getCSSVar(), `url(${url})`);
|
||||
this._url = url;
|
||||
getGammaGroup(): string {
|
||||
return this._gammagroup;
|
||||
}
|
||||
|
||||
getImg(): HTMLImageElement {
|
||||
return this._img;
|
||||
}
|
||||
|
||||
// Ensure we've loaded the image into our image loader.
|
||||
async ensureImageLoaded(imageManager: ImageManager) {
|
||||
Utils.assert(
|
||||
assert(
|
||||
this._url == null,
|
||||
"Tried to ensure a Bitmap was laoded more than once."
|
||||
);
|
||||
const imgUrl = await imageManager.getUrl(this._file);
|
||||
|
||||
this._img = await imageManager.getImage(imgUrl);
|
||||
this._img = await imageManager.getImage(this._file);
|
||||
|
||||
if (this._width == null && this._height == null) {
|
||||
const size = await imageManager.getSize(imgUrl);
|
||||
this.setXmlAttr("w", String(size.width));
|
||||
this.setXmlAttr("h", String(size.height));
|
||||
this.setXmlAttr("w", String(this._img.width));
|
||||
this.setXmlAttr("h", String(this._img.height));
|
||||
}
|
||||
|
||||
this.setUrl(imgUrl);
|
||||
// this.setUrl(imgUrl);
|
||||
}
|
||||
|
||||
getBackgrondImageCSSAttribute(): string {
|
||||
_getBackgrondImageCSSAttribute(): string {
|
||||
return `var(${this.getCSSVar()})`;
|
||||
}
|
||||
|
||||
getBackgrondPositionCSSAttribute(): string {
|
||||
const x = Utils.px(-(this._x ?? 0));
|
||||
const y = Utils.px(-(this._y ?? 0));
|
||||
_getBackgrondPositionCSSAttribute(): string {
|
||||
const x = px(-(this._x ?? 0));
|
||||
const y = px(-(this._y ?? 0));
|
||||
return `${x} ${y}`;
|
||||
}
|
||||
|
||||
getBackgrondSizeCSSAttribute(): string {
|
||||
const width = Utils.px(this._width);
|
||||
const height = Utils.px(this._height);
|
||||
_getBackgrondSizeCSSAttribute(): string {
|
||||
const width = px(this._width);
|
||||
const height = px(this._height);
|
||||
return `${width} ${height}`;
|
||||
}
|
||||
|
||||
getBackdropFilterCSSAttribute(): string {
|
||||
return `url(#${Utils.normalizeDomId(this._gammagroup)})`;
|
||||
_setAsBackground(div: HTMLDivElement, prefix: string) {
|
||||
div.style.setProperty(
|
||||
`--${prefix}background-image`,
|
||||
this._getBackgrondImageCSSAttribute()
|
||||
);
|
||||
div.style.setProperty(
|
||||
`--${prefix}background-position`,
|
||||
this._getBackgrondPositionCSSAttribute()
|
||||
);
|
||||
}
|
||||
|
||||
setAsBackground(div: HTMLDivElement) {
|
||||
this._setAsBackground(div, "");
|
||||
}
|
||||
|
||||
setAsActiveBackground(div: HTMLDivElement) {
|
||||
this._setAsBackground(div, "active-");
|
||||
}
|
||||
|
||||
setAsHoverBackground(div: HTMLDivElement) {
|
||||
this._setAsBackground(div, "hover-");
|
||||
}
|
||||
|
||||
getCanvas(): HTMLCanvasElement {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import GuiObj from "./GuiObj";
|
||||
import UI_ROOT from "../UIRoot";
|
||||
import { px } from "../utils";
|
||||
import { VM } from "./VM";
|
||||
|
||||
// http://wiki.winamp.com/wiki/XML_GUI_Objects#.3Cbutton.2F.3E_.26_.3Ctogglebutton.2F.3E
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { normalizeDomId, num, toBool } from "../utils";
|
||||
import { clamp, normalizeDomId, num, toBool } from "../utils";
|
||||
|
||||
// https://www.pawelporwisz.pl/winamp/wct_en.php
|
||||
export default class GammaGroup {
|
||||
_id: string;
|
||||
_value: string;
|
||||
|
|
@ -43,4 +44,31 @@ export default class GammaGroup {
|
|||
getRgb() {
|
||||
return `rgb(${this._value})`;
|
||||
}
|
||||
|
||||
// TODO: Figure out how to actually implement this.
|
||||
transformImage(img: HTMLImageElement): string {
|
||||
const [r, g, b] = this._value.split(",").map((v) => {
|
||||
return (Number(v) / 4096) * 255;
|
||||
});
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const imageData = ctx.getImageData(0, 0, img.width, img.height);
|
||||
const data = imageData.data;
|
||||
for (var i = 0; i < data.length; i += 4) {
|
||||
if (this._boost) {
|
||||
data[i] = (data[i] >> 1, 0, 255); // red
|
||||
data[i + 1] = (data[i + 1] >> 1, 0, 255); // green
|
||||
data[i + 2] = (data[i + 2] >> 1, 0, 255); // blue
|
||||
}
|
||||
|
||||
data[i] = clamp(data[i] + r, 0, 255); // red
|
||||
data[i + 1] = clamp(data[i + 1] + g, 0, 255); // green
|
||||
data[i + 2] = clamp(data[i + 2] + b, 0, 255); // blue
|
||||
}
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
return canvas.toDataURL();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -376,35 +376,24 @@ export default class GuiObj extends XmlObj {
|
|||
}
|
||||
|
||||
setBackgroundImage(bitmap: Bitmap | null) {
|
||||
this._div.style.setProperty(
|
||||
"--background-image",
|
||||
bitmap?.getBackgrondImageCSSAttribute() ?? "none"
|
||||
);
|
||||
this._div.style.setProperty(
|
||||
"--background-position",
|
||||
bitmap?.getBackgrondPositionCSSAttribute() ?? "none"
|
||||
);
|
||||
this._div.style.filter = bitmap?.getBackdropFilterCSSAttribute() ?? "none";
|
||||
if (bitmap != null) {
|
||||
bitmap.setAsBackground(this._div);
|
||||
}
|
||||
}
|
||||
|
||||
// JS Can't set the :active pseudo selector. Instead we have a hard-coded
|
||||
// pseduo-selector in our stylesheet which references a CSS variable and then
|
||||
// we control the value of that variable from JS.
|
||||
setActiveBackgroundImage(bitmap: Bitmap | null) {
|
||||
this._div.style.setProperty(
|
||||
"--active-background-image",
|
||||
bitmap?.getBackgrondImageCSSAttribute() ?? "none"
|
||||
);
|
||||
this._div.style.setProperty(
|
||||
"--active-background-position",
|
||||
bitmap?.getBackgrondPositionCSSAttribute() ?? "none"
|
||||
);
|
||||
if (bitmap != null) {
|
||||
bitmap.setAsActiveBackground(this._div);
|
||||
}
|
||||
}
|
||||
|
||||
draw() {
|
||||
this._div.setAttribute("data-id", this.getId());
|
||||
this._div.setAttribute("data-obj-name", "GuiObj");
|
||||
this._div.classList.add("webamp--guiobj");
|
||||
this._div.classList.add("webamp--img");
|
||||
this._renderVisibility();
|
||||
this._div.style.position = "absolute";
|
||||
this._renderAlpha();
|
||||
|
|
|
|||
|
|
@ -1,15 +1,11 @@
|
|||
import JSZip from "jszip";
|
||||
import { getCaseInsensitiveFile } from "../utils";
|
||||
import { assert, getCaseInsensitiveFile, getId } from "../utils";
|
||||
|
||||
export default class ImageManager {
|
||||
_urlCache: Map<string, string>;
|
||||
_imgCache: Map<string, HTMLImageElement>;
|
||||
_sizeCache: Map<string, { width: number; height: number }>;
|
||||
constructor(private _zip: JSZip) {
|
||||
this._urlCache = new Map();
|
||||
this._sizeCache = new Map();
|
||||
this._imgCache = new Map();
|
||||
}
|
||||
_urlCache: Map<string, string> = new Map();
|
||||
_imgCache: Map<string, HTMLImageElement> = new Map();
|
||||
_cssVarCache: Map<string, string> = new Map();
|
||||
constructor(private _zip: JSZip) {}
|
||||
|
||||
async getUrl(filePath: string): Promise<string | null> {
|
||||
if (!this._urlCache.has(filePath)) {
|
||||
|
|
@ -19,26 +15,20 @@ export default class ImageManager {
|
|||
}
|
||||
const imgBlob = await zipFile.async("blob");
|
||||
const imgUrl = await getUrlFromBlob(imgBlob);
|
||||
// const img = await this.getImage(imgUrl);
|
||||
// const transformedUrl = transformImage(img);
|
||||
this._urlCache.set(filePath, imgUrl);
|
||||
}
|
||||
return this._urlCache.get(filePath);
|
||||
}
|
||||
|
||||
async getSize(url: string): Promise<{ width: number; height: number }> {
|
||||
if (!this._sizeCache.has(url)) {
|
||||
const size = await this.getImage(url);
|
||||
this._sizeCache.set(url, size);
|
||||
}
|
||||
return this._sizeCache.get(url);
|
||||
}
|
||||
|
||||
async getImage(url: string): Promise<HTMLImageElement> {
|
||||
if (!this._imgCache.has(url)) {
|
||||
async getImage(filePath: string): Promise<HTMLImageElement> {
|
||||
if (!this._imgCache.has(filePath)) {
|
||||
// TODO: We could cache this
|
||||
const img = await loadImage(url);
|
||||
this._imgCache.set(url, img);
|
||||
const img = await loadImage(await this.getUrl(filePath));
|
||||
this._imgCache.set(filePath, img);
|
||||
}
|
||||
return this._imgCache.get(url);
|
||||
return this._imgCache.get(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -75,43 +75,22 @@ export default class Slider extends GuiObj {
|
|||
_renderThumb() {
|
||||
this._thumbDiv.style.position = "absolute";
|
||||
this._thumbDiv.setAttribute("data-obj-name", "Slider::Handle");
|
||||
this._thumbDiv.classList.add("webamp--guiobj");
|
||||
this._thumbDiv.classList.add("webamp--img");
|
||||
if (this._thumb != null) {
|
||||
const bitmap = UI_ROOT.getBitmap(this._thumb);
|
||||
this._thumbDiv.style.width = px(bitmap.getWidth());
|
||||
this._thumbDiv.style.height = px(bitmap.getHeight());
|
||||
this._thumbDiv.style.setProperty(
|
||||
"--background-image",
|
||||
bitmap.getBackgrondImageCSSAttribute()
|
||||
);
|
||||
this._thumbDiv.style.setProperty(
|
||||
"--background-position",
|
||||
bitmap.getBackgrondPositionCSSAttribute()
|
||||
);
|
||||
bitmap.setAsBackground(this._thumbDiv);
|
||||
}
|
||||
|
||||
if (this._downThumb != null) {
|
||||
const bitmap = UI_ROOT.getBitmap(this._downThumb);
|
||||
this._thumbDiv.style.setProperty(
|
||||
"--active-background-image",
|
||||
bitmap.getBackgrondImageCSSAttribute()
|
||||
);
|
||||
this._thumbDiv.style.setProperty(
|
||||
"--active-background-position",
|
||||
bitmap.getBackgrondPositionCSSAttribute()
|
||||
);
|
||||
bitmap.setAsActiveBackground(this._thumbDiv);
|
||||
}
|
||||
|
||||
if (this._hoverThumb != null) {
|
||||
const bitmap = UI_ROOT.getBitmap(this._hoverThumb);
|
||||
this._thumbDiv.style.setProperty(
|
||||
"--hover-background-image",
|
||||
bitmap.getBackgrondImageCSSAttribute()
|
||||
);
|
||||
this._thumbDiv.style.setProperty(
|
||||
"--hover-background-position",
|
||||
bitmap.getBackgrondPositionCSSAttribute()
|
||||
);
|
||||
bitmap.setAsHoverBackground(this._thumbDiv);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue