Do gammagroups in canvas

This commit is contained in:
Jordan Eldredge 2021-06-30 19:25:31 -07:00
parent 9f121f1dff
commit 2aa5efd993
9 changed files with 175 additions and 141 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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